@avg-studio/studio-ui 是 Studio 提供的界面组件包,用来给扩展制作只在 Studio 里显示的编辑界面:
这两种界面都写在编辑期入口 src/studio.tsx 里,玩家的游戏不会加载这部分代码。
两个入口
带编辑界面的程序扩展有两个入口:
| 入口 | 源文件 | 构建产物 | 谁会加载 |
|---|
| 游戏运行时入口 | src/index.tsx | dist/index.js | Studio 预览和玩家的游戏 |
| 编辑期入口 | src/studio.tsx | dist/studio.js | 只有 Studio |
设置和方法仍声明在运行时入口:static settings 决定设置的类型和默认值,方法的 schema 决定参数清单。编辑期入口只决定这些内容在 Studio 里怎么摆、怎么改。
清单字段 studioEntry
在 extension.json 里声明编辑期入口的构建产物:
{
"entry": "dist/index.js",
"studioEntry": "dist/studio.js"
}
路径要满足这些规则,否则清单校验不通过:
- 只有声明了
entry 的程序扩展才能声明;
- 必须是
dist/ 下的相对路径,以 .js 或 .mjs 结尾;
- 只能使用英文字母、数字和
-、_、.、/;
- 不能和
entry 相同,只差大小写也算相同。
编辑期入口导出什么
Studio 只读取具名导出:
| 导出名 | 内容 |
|---|
settingsPage | 自定义设置页组件 |
inspectors | 键是方法 id、值是检查器组件的对象 |
export const settingsPage = MySettingsPage;
export const inspectors = { "add-item": AddItemInspector };
两者可以只导出其中一个。组件不接收 props,可以是函数组件、类组件,或用 memo()、forwardRef() 包出来的组件。
默认导出、写错的名字、lazy() 包出来的组件和 <MyPage /> 这样的元素都会被忽略,Studio 继续显示自动表单,扩展日志里有说明。
两个入口要分两次构建到同一个 dist/:
npm run build # 依次运行 vite build 和 vite build --mode studio
npm run watch # 运行 scripts/watch.mjs,在一个终端里同时监听两个入口
构建配置照模板写。要自己改时,两次构建都要保持 emptyOutDir: false、inlineDynamicImports: true 和 minify: false。重新构建后,Studio 会自动刷新设置页和检查器。
编辑期入口能引用的只有 react、react-dom、react/jsx-runtime、react/jsx-dev-runtime、@avg-studio/sdk、@chakra-ui/react 和 @avg-studio/studio-ui,它们要保持外部依赖,其他第三方依赖都要打包进 dist/studio.js。共享包的其他子路径和 @emotion/*、@ark-ui/* 会被 Studio 拒绝加载。
开始使用
新扩展
选中扩展下的「程序」,初始化前打开「同时生成自定义设置页模板」,再点击初始化。除默认文件外,还会得到:
my-extension/
├── scripts/
│ └── watch.mjs # 同时监听两个入口
├── src/
│ └── studio.tsx # 编辑期入口,自带一个可运行的设置页
├── studio-ui/ # 组件包的本地副本、使用说明和模板
└── extension.json # 多了 "studioEntry": "dist/studio.js"
第一次要先运行 npm install && npm run build,或在扩展树中右键选择「构建扩展(自动装依赖)」。
已有扩展
新建编辑期入口
创建 src/studio.tsx,导出 settingsPage 或 inspectors。
声明清单字段
在 extension.json 加上 "studioEntry": "dist/studio.js"。
安装组件包
在 package.json 的 devDependencies 里加上 "@avg-studio/studio-ui": "file:./studio-ui" 和 @chakra-ui/react(Chakra UI v3,只用于类型提示)。在 Studio 里执行「同步 SDK」,组件包会拷到 studio-ui/,再运行 npm install。
改成两次构建
用 studio-ui/templates/vite.config.ts 覆盖现有的 vite.config.ts,并带上自己改过的配置。把 build 脚本改成 vite build && vite build --mode studio。需要同时监听时,添加下面的 scripts/watch.mjs,并把 watch 脚本改成 node scripts/watch.mjs。
import { build } from "vite";
await Promise.all([
build({ build: { watch: {} } }),
build({ mode: "studio", build: { watch: {} } }),
]);
使用规则
- 优先用组件包:Chakra UI(
@chakra-ui/react,目前是 v3)也能用,但不在稳定承诺之内,默认颜色也不跟随 Studio 主题。Chakra 只能从包的根路径引入,不要引入 CSS 文件、Emotion 或 Ark UI。
- 不写死颜色:只用 Studio 的语义颜色变量,可用的有
--fg-primary、--fg-secondary、--fg-tertiary、--fg-muted、--bg-canvas、--bg-subtle、--bg-sunken、--border-subtle、--border-default、--accent、--accent-fg、--warn。
- 文字自己翻译:Studio 原样显示页面里的文字,也原样朗读
ariaLabel,用 useStudioLocale() 选择译文。Chakra 的浮层和菜单要给内容加上 data-studio-user-content 属性,Studio 才不会翻译它。
- 不调用游戏运行时接口:编辑期入口里没有扩展上下文,不能调用
useExtensionContext() 这类接口。
- 不使用 Node.js:不要在编辑期入口里用
require、process、electron 和 fs,工坊审核会重点检查这部分。
- 每个读数据的行拆成小组件:
useSetting、useParam 只让读到变化内容的组件重新渲染。
组件清单
下面列出组件包的全部导出。所有组件和 Hook 的完整参数、示例,以及排查表都在扩展目录的 studio-ui/README.md 里。
布局件
| 组件 | 用途 | 主要参数 |
|---|
Panel | 整页容器,页面最外层放一个 | children |
Section | 带标题的分组卡片,可以折叠 | title、description、collapsible、defaultOpen、actions |
FieldRow | 一行:左侧标签,右侧你的控件 | label、description、disabled、divider |
Stack | 竖向排列 | gap:"xs"、"sm"、"md"、"lg"(4、8、12、20 像素,默认 "md") |
Row | 横向排列 | gap(默认 "sm")、align、justify(含 "between")、wrap |
Tabs | 页签 | items(每项 value、label、content、disabled)、value、defaultValue、onChange、ariaLabel |
FieldRow 禁用时,框内的组件包控件和嵌套的 FieldRow 也一起禁用。在检查器里,Panel、Section、FieldRow 会自动换成紧凑排版。
基础件
| 组件或函数 | 用途 | 主要参数 |
|---|
Button | 文字按钮 | variant:"primary"、"secondary"、"ghost"、"danger";size:"sm"、"md";disabled、title、onClick |
Select | 下拉 | value、options、onChange、placeholder、disabled、ariaLabel |
Segmented | 2~5 个短选项的分段选择 | value、options、onChange、fullWidth、disabled、ariaLabel |
TextInput | 文本框 | value、onChange、placeholder、multiline、disabled、ariaLabel |
Switch | 开关 | checked、onChange、disabled、ariaLabel |
SectionTitle | 分组内的小标题 | children、actions |
toast(options) | 轻提示 | title、description、type:"info"、"success"、"warning"、"error" |
confirm(options) | 确认框,返回 Promise<boolean> | title、description、confirmText、cancelText、danger |
useStudioLocale() | 读取 Studio 当前界面语言,切换语言时重新渲染 | 返回 "zh-CN"、"zh-TW"、"ja-JP"、"en-US"、"ar-SA"、"de-DE"、"fr-FR"、"it-IT" |
业务选择器
选择器都是受控组件:传入 value 和 onChange,也都支持 disabled 和 ariaLabel。字段已经写在设置清单里时,直接用 SettingField 更省事。
| 组件 | 选择什么 | 其他参数 |
|---|
AssetPicker | 工程素材,值是相对 assets/ 的路径 | accepts:"image"、"audio"、"video"、"any" 组成的数组 |
CharacterPicker | 角色,值是角色 id | |
CharacterExpressionPicker | 某个角色的表情,值是表情名 | characterId |
ColorPicker | 颜色 | allowAlpha(默认开启) |
FontPicker | 字体 | |
UIRefPicker | 扩展界面引用,也能选到本扩展的隐藏界面 | |
FragmentRefPicker | 片段,值是 { chapterId, fragmentId },两个 id 都为空表示「未设置」 | |
ShortcutInput | 录入快捷键并提示冲突 | name:这个快捷键所属的设置名,冲突检查会跳过它自己 |
NumberSlider | 滑杆加数字框,拖动结束、回车或失去焦点时才保存 | min、max(必填)、step、unit |
AssetPicker 只有在 accepts={["image"]} 时才打开素材浏览器,而且只列出 assets/ui/ 这一层的图片,值形如 ui/button.png。其他取值(包括默认的 ["any"])显示路径输入框。设置字段里的素材控件也是这样。
读写设置
只能在设置页或检查器里使用,见自定义设置页。
| 导出 | 用途 |
|---|
useSetting(name) | 读写一个设置,返回 [value, setValue, info] |
SettingField | 放入某个设置的现成一行,和自动表单完全一样 |
AutoSettings | 整段放入自动表单,可用 include、exclude 挑选设置 |
读写方法参数
只能在检查器组件里使用,见自定义检查器。
| 导出 | 用途 |
|---|
useParam(name) | 读写一个参数,返回 [value, setValue, info] |
useParams() | 一次读全部参数、一次写多个参数,返回 [values, setParams] |
ParamField | 放入某个参数的现成一行 |
AutoParams | 整段放入自动参数表单,可用 include、exclude 挑选参数 |
useInspectorEnvironment() | 读取显示位置、能否使用变量、是否只读 |
MethodParams(类型) | 从方法声明推导参数类型 |
读取和维护工程资料
这些 Hook 都可以在设置页和检查器里使用:
| 导出 | 用途 |
|---|
useDataCollection(alias) | 读取并编辑本扩展绑定的数据集合的原始资料,见项目数据库 API |
useStorySnapshot() | 读取全部剧本和场景库的只读快照 |
useGallerySources() | 读取当前鉴赏系统界面里 CG、音乐、片段组件的来源和作者资料 |
ImagePreview | 只读的图片或多层场景预览 |
useStorySnapshot() 返回 { ready, chapters, scenes },对应类型是 StudioStorySnapshot、StudioStoryChapter、StudioStoryBlock、StudioStoryScene。
- 章节按工程顺序排列,包含全部片段和原始块,也包含已禁用的内容,需要自己筛选;场景包含 id、名称、图层和光照。
- 工程或场景库还没加载好时,
ready 为 false,两个数组都是空的。
- 快照不含玩家变量和项目路径,修改它不会保存。
useGallerySources() 返回 StudioGallerySources:
| 字段 | 内容 |
|---|
ready / error | 是否读取完成、读取时的错误 |
galleries | 三类组件的 id、name、kind("cg"、"music"、"fragment")、sharedSource、slotSource |
entriesBySource | 按来源分组的作者原始记录 |
unavailableSources | 暂时读不到的来源 |
结果不含玩家存档和只在运行时才产生的条目。
ImagePreview 接收 layers(相对 assets/ 的图片路径,从底层到顶层)、alt 和可选的 fallback,合成一张 16:9 预览。数组为空或图片缺失时显示 fallback。
<ImagePreview
layers={["background/room.png", "foreground/window.png"]}
alt="房间"
fallback={<span>还没有封面</span>}
/>
SettingsPageComponent、InspectorComponent 和 StudioEntryExports 可以给编辑期入口的导出标注类型,写成 lazy(...) 时编译就会报错。各组件的参数类型也都从包根导出。
同步 SDK 与 studio-ui
studio-ui/ 是组件包的本地副本,和 sdk/ 一样由 Studio 的「同步 SDK」整份替换,不要手改,也不要提交到版本库。它里面有:
| 文件 | 内容 |
|---|
README.md | 组件包说明 |
templates/settings-page.tsx | 设置页模板 |
templates/simple-inspector.tsx | 简单检查器模板:现成行、一键填写和实时预览 |
templates/list-param-inspector.tsx | 列表参数检查器模板:每行一张卡片,可以排序 |
templates/vite.config.ts | 两次构建的 Vite 配置 |
studio-ui/ 缺失或版本落后时,也会出现「SDK 已更新」提示,同步一次即可。
提示「SDK 已同步,组件包没有同步」时,说明扩展根目录已有一个不是 Studio 管理的 studio-ui 文件夹。把这个文件夹改名或删掉后再同步一次。
提交到工坊
工坊审核会连同编辑期入口一起检查。Studio 的提交表单在上传前先核对:
| 提示 | 处理 |
|---|
| 编辑期入口的路径不符合要求 | 按上面的路径规则改正 extension.json |
| 找不到编辑期入口 | 运行 npm run build |
| 编辑期入口不是最新构建 | 重新运行 npm run build |
| 编辑期入口不会进入发行包 | 产物不是普通文件(例如符号链接),让构建把文件直接写到 dist/ 下 |
编辑期入口的大小上限是 10MB。直接使用 require、process、electron、Node 内置模块、eval、new Function 或全局对象的计算属性名,这个版本会被标为特权版本,由审核员读过文件后才能发布,也可能被拒绝。
误用时的报错
| 看到什么 | 原因 | 处理 |
|---|
扩展加载失败:the game runtime entry imports ... | 游戏运行时代码引用了 @chakra-ui/react 或 @avg-studio/studio-ui | 把这段代码移到 src/studio.tsx |
调用组件时抛错:@avg-studio/studio-ui: useSetting can only be used in the Studio editor entry (src/studio.tsx) ... | 在 Studio 之外调用了组件包,比如单元测试、运行时入口,或者构建时把组件包打进了产物 | 只在编辑期入口里使用,并把它保持为外部依赖;单元测试里自己 mock |
常见问题
| 看到什么 | 原因 | 处理 |
|---|
| 「自定义设置页还没有构建,先显示自动表单;构建完成后会自动切换。」 | dist/studio.js 不存在 | 运行 npm run build,或在 Studio 里构建扩展 |
| 构建成功后上面的提示还在 | studioEntry 和实际产物路径对不上 | 把鼠标停在提示上看 Studio 正在找的路径,改成一致 |
| 「下面这些包由 Studio 提供,不要打包进去……」 | 引用了共享包的子路径,或 @emotion/*、@ark-ui/* | 改成从包根引入;后两者改用 @chakra-ui/react 提供的写法 |
| 「编辑期入口只能引用 react、react-dom……」 | 引用了 Studio 不提供的第三方包 | 把它从 vite.config.ts 的外部依赖里去掉,打包进 dist/studio.js |
| 「编辑期入口被拆成了多个文件,或用了动态 import / lazy 加载……」 | 动态 import() 了 Studio 提供的包,或构建配置不对 | 用静态 import 引入共享包;构建配置照模板写 |
错误信息说入口必须 contain no import statements | 产物压缩过(minify: true) | 保持 minify: false |
自动表单,扩展日志说 settingsPage 被忽略 | 不是组件,或不是具名导出 | 写成 export const settingsPage = MyPage |
| 暗色下颜色不对 | 写死了颜色 | 改用 Studio 的语义颜色变量 |
设置页和检查器各自的问题见自定义设置页和自定义检查器。
参考示例
除了 studio-ui/templates/ 里的模板,官方扩展「Studio 组件包演示」也用遍了组件包的布局件、基础件和选择器,还有两个检查器。
让 AI 编程助手来写
AI 编程助手可以读 studio-ui/README.md,或通过 Studio MCP 读 studio://dev/studio-ui,内容相同。ext_reload 会回报编辑期入口的加载结果:有没有设置页、导出了哪些检查器、哪些检查器的键对不上方法(unknownInspectors),加载失败的原因在 studioEntry.error 里。
完整流程见使用 AI 开发扩展。