Skip to content

[Bug]: 本地导入的外观包无法激活——set_config 经产品控制通道校验,appearance.selection 的枚举白名单仅含内置主题 #3120

Description

@Cid-Nova

EN TL;DR — An imported appearance package can be imported successfully but can never be activated. AppearanceService persists the selection via configAPI.setConfig('appearance.selection', id) → Tauri set_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. Per docs/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.appearance v2(由内置 create-openbitfun-skin skill 生成)
  • 包已在本地通过校验:validate 返回 VALID,零警告;build 成功,归档约 0.5 KB

复现步骤

  1. 用内置 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" } }
    }
  }
}
  1. 设置 → 外观 → 导入外观包 → 选择该归档文件
  2. 导入成功,出现在外观列表中
  3. 选中该包

实际结果

弹出错误,且选择被回滚到原主题:

⚠️ 外观包应用失败
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 后因持久化失败,事务执行补偿回滚,回到原主题。

另有两个相关现象:

  1. 若把包 id 改为某个内置主题 id 以复用命名空间,导入阶段会被拒绝:
    Imported appearance cannot replace a builtin appearance: openbitfun-dark
    (见 src/web-ui/src/infrastructure/appearance/runtime/AppearanceService.ts
    即:自定义 id 存不住,内置 id 不许占,两条路都不通。
  2. 已成功导入的包在界面上无法删除:点击删除后无任何反馈,且日志中零记录。

预期行为

按仓库自身的架构文档 docs/architecture/appearance-package-system.md

AppearanceRuntime 是唯一视觉 owner。内置外观和导入外观使用同一个 AppearancePackage 契约、同一个 compiler、同一个事务提交路径。选择值由正式配置键 appearance.selection 持久化;IndexedDB openbitfun-appearance 只保存导入包和 catalog,不保存当前选择。

即:导入外观应与内置外观享有同等地位,其选择值应当可以被持久化。当前行为与文档描述不一致。

根因定位(调用链)

从当前版本源码看,同一配置键存在两条口径不一致的通道:

通道 位置 对任意外观 id 的行为
配置服务 src/crates/assembly/core/src/service/config/providers.rsAppearanceConfigProvider::validate_config 接受(仅校验非空)
产品控制 src/crates/contracts/product-domains/src/product_control.rs(错误文案 "value must be one of: {}" 拒绝(静态枚举,仅 10 个内置 id)

而界面写入走的是后者:

  1. src/web-ui/src/infrastructure/appearance/runtime/AppearanceService.ts:504
    await configAPI.setConfig(APPEARANCE_SELECTION_CONFIG_PATH, selected);
  2. src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts:160
    await api.invoke('set_config', { ... })
  3. 后端 openbitfun_desktop_lib::api::config_apiappearance.selection 走产品控制校验 → 命中枚举白名单 → 拒绝
  4. 回到 AppearanceService.ts:524,catch 分支执行补偿回滚:
    await configAPI.setConfig(APPEARANCE_SELECTION_CONFIG_PATH, previousPersistedSelectionId);

因此:合法导入包在"提交事务的最后一步"必然失败,失败与包本身无关(包已通过 schema 校验、compiler 与 runtime preflight,且 runtime 已经接受并渲染过它)。

建议

两种可能的修复方向(取决于设计本意):

  1. appearance.selection 的产品控制 value_schema 接受动态集合(内置 id ∪ 已安装外观包 id);
  2. 或者让界面自身的配置写入不经过产品控制的枚举校验——该枚举更像是对 AI/自动化改设置的安全约束,而配置服务通道本就允许任意 id。

另外建议:持久化失败触发回滚时,错误信息最好带上"哪个键、哪个值、被哪条规则拒绝"。当前文案对用户和模型都不可定位,用户只能看到"应用失败",无法知道是自己的包有问题还是产品的问题。

补充

导入包的持久记录(IndexedDB openbitfun-appearance)中包含 localOverridemarketOriginimportedAt 字段,说明系统对"本地导入"与"市场来源"是有区分的。

想确认一下:本地导入的包在当前版本的预期行为是什么?是否必须通过 Skin Market 安装才能被激活? 如果确实如此,建议在 UI 上明确提示(例如导入时就说明"本地包当前不可激活"),而不是在激活阶段失败并回滚。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions