v3.0.0 新增
@avg-studio/studio-ui 是 Studio 提供的界面组件包,用来给扩展制作只在 Studio 里显示的编辑界面: 这两种界面都写在编辑期入口 src/studio.tsx 里,玩家的游戏不会加载这部分代码。

两个入口

带编辑界面的程序扩展有两个入口:
入口源文件构建产物谁会加载
游戏运行时入口src/index.tsxdist/index.jsStudio 预览和玩家的游戏
编辑期入口src/studio.tsxdist/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,或在扩展树中右键选择「构建扩展(自动装依赖)」。

已有扩展

1

新建编辑期入口

创建 src/studio.tsx,导出 settingsPage 或 inspectors。
2

声明清单字段

在 extension.json 加上 "studioEntry": "dist/studio.js"。
3

安装组件包

在 package.json 的 devDependencies 里加上 "@avg-studio/studio-ui": "file:./studio-ui" 和 @chakra-ui/react(Chakra UI v3,只用于类型提示)。在 Studio 里执行「同步 SDK」,组件包会拷到 studio-ui/,再运行 npm install。
4

改成两次构建

用 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
Segmented2~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 开发扩展。