[Bug]: 本地导入的外观包无法激活——set_config 经产品控制通道校验,appearance.selection 的枚举白名单仅含内置主题
@kev1n77 is already working on this.
Since Sep 19, 2026.
Assessment
This issue has not been assessed yet.
Description
EN TL;DR — An imported appearance package can be imported successfully but can never be activated.
AppearanceServicepersists the selection viaconfigAPI.setConfig('appearance.selection', id)→ Tauriset_config, where the value is validated against the product-control enum (the 10 builtin ids only), so any imported package id is rejected. The runtime actually accepts the package (it is applied and rendered for ~370 ms), then the transaction rolls back. Perdocs/architecture/appearance-package-system.md, imported and builtin appearances are supposed to share the same contract, compiler and commit path — so the static enum whitelist looks like the defect.
环境
- OpenBitFun 1.0.1 桌面版
- Windows 10 22H2(19045)/ WebView2
- 外观包 schema:
openbitfun.appearancev2(由内置create-openbitfun-skinskill 生成) - 包已在本地通过校验:
validate返回 VALID,零警告;build成功,归档约 0.5 KB
复现步骤
- 用内置 skill 初始化并生成一个最小外观包:
python scripts/openbitfun_appearance.py init <dir> --id <custom-id> --name <name> --mode dark
python scripts/openbitfun_appearance.py validate <dir>
python scripts/openbitfun_appearance.py build <dir> --output <file>.openbitfun-appearance
manifest 只声明一个组件、两个部件、三个属性:
"components": {
"markdown": {
"parts": {
"codeContent": { "base": { "whiteSpace": "pre-wrap", "overflowWrap": "anywhere", "wordBreak": "break-word" } },
"codePre": { "base": { "whiteSpace": "pre-wrap", "overflowWrap": "anywhere", "wordBreak": "break-word" } }
}
}
}
- 设置 → 外观 → 导入外观包 → 选择该归档文件
- 包导入成功,出现在外观列表中
- 选中该包
实际结果
弹出错误,且选择被回滚到原主题:
⚠️ 外观包应用失败
Failed to set config: value must be one of: "system", "openbitfun-light", "openbitfun-monochrome",
"openbitfun-slate", "openbitfun-dark", "openbitfun-midnight", "openbitfun-china-style",
"openbitfun-china-night", "openbitfun-cyber", "openbitfun-tokyo-night"
对应日志(webview.log / app.log,毫秒级连续):
21:01:46.990 [INFO ][webview][AppearanceRuntime] Appearance applied {"appearanceId":"<custom-id>","revision":2}
21:01:46.991 [ERROR][openbitfun_desktop_lib::api::config_api] Failed to set config through product control:
path=appearance.selection, error=value must be one of: "system","openbitfun-light",...
21:01:47.351 [INFO ][...service::config::providers] Appearance config changed: selection = system
21:01:47.358 [INFO ][webview][AppearanceRuntime] Appearance applied {"appearanceId":"openbitfun-dark","revision":3}
也就是说:外观包已经被成功 apply 进 runtime(revision 递增到 2),约 368 ms 后因持久化失败,事务执行补偿回滚,回到原主题。
另有两个相关现象:
- 若把包 id 改为某个内置主题 id 以复用命名空间,导入阶段会被拒绝:
Imported appearance cannot replace a builtin appearance: openbitfun-dark
(见src/web-ui/src/infrastructure/appearance/runtime/AppearanceService.ts)
即:自定义 id 存不住,内置 id 不许占,两条路都不通。 - 已成功导入的包在界面上无法删除:点击删除后无任何反馈,且日志中零记录。
预期行为
按仓库自身的架构文档 docs/architecture/appearance-package-system.md:
AppearanceRuntime是唯一视觉 owner。内置外观和导入外观使用同一个AppearancePackage契约、同一个 compiler、同一个事务提交路径。选择值由正式配置键appearance.selection持久化;IndexedDBopenbitfun-appearance只保存导入包和 catalog,不保存当前选择。
即:导入外观应与内置外观享有同等地位,其选择值应当可以被持久化。当前行为与文档描述不一致。
根因定位(调用链)
从当前版本源码看,同一配置键存在两条口径不一致的通道:
| 通道 | 位置 | 对任意外观 id 的行为 |
|---|---|---|
| 配置服务 | src/crates/assembly/core/src/service/config/providers.rs → AppearanceConfigProvider::validate_config |
接受(仅校验非空) |
| 产品控制 | src/crates/contracts/product-domains/src/product_control.rs(错误文案 "value must be one of: {}") |
拒绝(静态枚举,仅 10 个内置 id) |
而界面写入走的是后者:
src/web-ui/src/infrastructure/appearance/runtime/AppearanceService.ts:504
await configAPI.setConfig(APPEARANCE_SELECTION_CONFIG_PATH, selected);src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts:160
await api.invoke('set_config', { ... })- 后端
openbitfun_desktop_lib::api::config_api对appearance.selection走产品控制校验 → 命中枚举白名单 → 拒绝 - 回到
AppearanceService.ts:524,catch 分支执行补偿回滚:
await configAPI.setConfig(APPEARANCE_SELECTION_CONFIG_PATH, previousPersistedSelectionId);
因此:合法导入包在"提交事务的最后一步"必然失败,失败与包本身无关(包已通过 schema 校验、compiler 与 runtime preflight,且 runtime 已经接受并渲染过它)。
建议
两种可能的修复方向(取决于设计本意):
- 让
appearance.selection的产品控制 value_schema 接受动态集合(内置 id ∪ 已安装外观包 id); - 或者让界面自身的配置写入不经过产品控制的枚举校验——该枚举更像是对 AI/自动化改设置的安全约束,而配置服务通道本就允许任意 id。
另外建议:持久化失败触发回滚时,错误信息最好带上"哪个键、哪个值、被哪条规则拒绝"。当前文案对用户和模型都不可定位,用户只能看到"应用失败",无法知道是自己的包有问题还是产品的问题。
补充
导入包的持久记录(IndexedDB openbitfun-appearance)中包含 localOverride、marketOrigin、importedAt 字段,说明系统对"本地导入"与"市场来源"是有区分的。
想确认一下:本地导入的包在当前版本的预期行为是什么?是否必须通过 Skin Market 安装才能被激活? 如果确实如此,建议在 UI 上明确提示(例如导入时就说明"本地包当前不可激活"),而不是在激活阶段失败并回滚。
- Dominant language
- Rust
- Stars
- 2.3k
- Forks
- 236
- Avg merge
- 2h 56m
- Merged PRs (30d)
- 619
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from GCWing/OpenBitFun
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
GCWing/OpenBitFun#3213 · 1 comment ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
GCWing/OpenBitFun#2363 ·
-
question
Difficulty 1/5 Under an hour Newbie friendliness 78/100
GCWing/OpenBitFun#2340 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 65/100
GCWing/OpenBitFun#3210 ·
-
GCWing/OpenBitFun#3195 · 2 comments · 1 assignee ·
All issues in GCWing/OpenBitFun
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
TheLarkInn/aipm#2413 ·
-
documentation
Difficulty 1/5 Under an hour Newbie friendliness 90/100
alexgorbatchev/simple-ptt#15 ·
-
tooling
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
todo:ticket
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
taikoxyz/taiko-mono#22168 · 1 comment ·