快速开始

Tweakcn 自定义主题导入设计

Tweakcn 自定义主题导入设计

Status:已于 2026-04-22 在终端批准

摘要

添加正好一个浏览器本地的自定义 Control UI 主题槽,可从 tweakcn 分享链接导入。现有内置主题系列仍为 clawknotdash。新的 custom 系列表现得像普通的 OpenClaw 主题系列,并且在导入的 tweakcn 载荷包含明暗两套 token 集时,支持 lightdarksystem 模式。

导入的主题仅与其余 Control UI 设置一起存储在当前浏览器配置文件中。它不会写入 Gateway 网关配置,也不会跨设备或浏览器同步。

问题

Control UI 主题系统目前被限定在三个硬编码主题系列内:

  • ui/src/ui/theme.ts
  • ui/src/ui/views/config.ts
  • ui/src/styles/base.css

用户可以在内置系列和模式变体之间切换,但如果不编辑仓库 CSS,就无法引入 tweakcn 主题。请求的结果小于通用主题系统:保留三个内置主题,并添加一个由用户控制的导入槽,可从 tweakcn 链接替换。

目标

  • 保持现有内置主题系列不变。
  • 添加正好一个导入的自定义槽,而不是主题库。
  • 接受 tweakcn 分享链接或直接的 https://tweakcn.com/r/themes/{id} URL。
  • 仅在浏览器本地存储中持久化导入的主题。
  • 让导入槽与现有 lightdarksystem 模式控件配合工作。
  • 保持故障行为安全:错误导入绝不会破坏当前活动 UI 主题。

非目标

  • 不提供多主题库或浏览器本地导入列表。
  • 不提供 Gateway 网关侧持久化或跨设备同步。
  • 不提供任意 CSS 编辑器或原始主题 JSON 编辑器。
  • 不从 tweakcn 自动加载远程字体资产。
  • 不尝试支持只暴露一种模式的 tweakcn 载荷。
  • 不进行超出 Control UI 所需接缝之外的仓库级主题重构。

已做出的用户决策

  • 保留三个内置主题。
  • 添加一个由 tweakcn 驱动的导入槽。
  • 将导入的主题存储在浏览器中,而不是 Gateway 网关配置中。
  • 为导入槽支持 lightdarksystem
  • 用下一次导入覆盖自定义槽是预期行为。

推荐方案

向 Control UI 主题模型添加第四个主题系列 ID:custom。只有存在有效的 tweakcn 导入时,custom 系列才可选择。导入的载荷会规范化为 OpenClaw 专用的自定义主题记录,并与其余 UI 设置一起存储在浏览器本地存储中。

运行时,OpenClaw 渲染一个受管理的 <style> 标签,用于定义解析后的自定义 CSS 变量块:

:root[data-theme="custom"] { ... }
:root[data-theme="custom-light"] { ... }

这会将自定义主题变量限定在 custom 系列内,并避免内联 CSS 变量泄漏到内置系列中。

架构

主题模型

更新 ui/src/ui/theme.ts

  • 扩展 ThemeName 以包含 custom
  • 扩展 ResolvedTheme 以包含 customcustom-light
  • 扩展 VALID_THEME_NAMES
  • 更新 resolveTheme(),使 custom 映射现有系列行为:
    • custom + dark -> custom
    • custom + light -> custom-light
    • custom + system -> 根据操作系统偏好得到 customcustom-light

不会为 custom 添加旧版别名。

持久化模型

ui/src/ui/storage.ts 中扩展 UiSettings 持久化,加入一个可选的自定义主题载荷:

  • customTheme?: ImportedCustomTheme

推荐的存储形态:

type ImportedCustomTheme = {
  sourceUrl: string;
  themeId: string;
  label: string;
  importedAt: string;
  light: Record<string, string>;
  dark: Record<string, string>;
};

说明:

  • sourceUrl 存储规范化后的原始用户输入。
  • themeId 是从 URL 中提取的 tweakcn 主题 ID。
  • label 是存在时的 tweakcn name 字段,否则为 Custom
  • lightdark 是已规范化的 OpenClaw token 映射,而不是原始 tweakcn 载荷。
  • 导入的载荷与其他浏览器本地设置并列存在,并序列化在同一个本地存储文档中。
  • 如果加载时存储的自定义主题数据缺失或无效,则忽略该载荷;当持久化的系列为 custom 时,回退到 theme: "claw"

运行时应用

在 Control UI 运行时中添加一个窄范围的自定义主题样式表管理器,归属位置靠近 ui/src/ui/app-settings.tsui/src/ui/theme.ts

职责:

  • document.head 中创建或更新一个稳定的 <style id="openclaw-custom-theme"> 标签。
  • 仅当存在有效自定义主题载荷时输出 CSS。
  • 清除载荷时移除该样式标签内容。
  • 将内置系列 CSS 保留在 ui/src/styles/base.css 中;不要把导入的 token 拼接进已检入的样式表。

此管理器在设置加载、保存、导入或清除时运行。

浅色模式选择器

实现应优先使用 data-theme-mode="light" 来处理跨系列浅色样式,而不是为 custom-light 做特殊分支。如果现有选择器固定为 data-theme="light",且需要应用到每个浅色系列,则应作为这项工作的一部分扩展它。

导入 UX

更新 ui/src/ui/views/config.tsAppearance 部分:

  • ClawKnotDash 旁边添加一张 Custom 主题卡片。
  • 当不存在已导入的自定义主题时,将卡片显示为禁用。
  • 在主题网格下方添加一个导入面板,包含:
    • 一个用于 tweakcn 分享链接或 /r/themes/{id} URL 的文本输入
    • 一个 Import 按钮
    • 当自定义载荷已存在时提供一条 Replace 路径
    • 当自定义载荷已存在时提供一个 Clear 操作
  • 当载荷存在时,显示导入的主题标签和来源主机。
  • 如果当前活动主题是 custom,导入替换会立即应用。
  • 如果当前活动主题不是 custom,导入只会存储新的载荷,直到用户选择 Custom 卡片。

ui/src/ui/views/config-quick.ts 中的快速设置主题选择器也应仅在载荷存在时显示 Custom

URL 解析和远程获取

浏览器导入路径接受:

  • https://tweakcn.com/themes/{id}
  • https://tweakcn.com/r/themes/{id}

实现应将两种形式都规范化为:

  • https://tweakcn.com/r/themes/{id}

然后浏览器直接获取规范化后的 /r/themes/{id} 端点。

对外部载荷使用窄范围 schema 校验器。因为这是不可信的外部边界,优先使用 zod schema。

必需的远程字段:

  • 顶层 name,可选字符串
  • cssVars.theme,可选对象
  • cssVars.light,对象
  • cssVars.dark,对象

如果缺少 cssVars.lightcssVars.dark 中任意一个,则拒绝导入。这是有意为之:已批准的产品行为是完整模式支持,而不是尽力合成缺失的一侧。

Token 映射

不要盲目镜像 tweakcn 变量。将有界子集规范化为 OpenClaw token,并在辅助函数中派生其余 token。

直接导入的 token

从每个 tweakcn 模式块中:

  • background
  • foreground
  • card
  • card-foreground
  • popover
  • popover-foreground
  • primary
  • primary-foreground
  • secondary
  • secondary-foreground
  • muted
  • muted-foreground
  • accent
  • accent-foreground
  • destructive
  • destructive-foreground
  • border
  • input
  • ring
  • radius

从共享的 cssVars.theme 中(如果存在):

  • font-sans
  • font-mono

如果某个模式块覆盖 font-sansfont-monoradius,则模式本地值优先。

为 OpenClaw 派生的 token

导入器从导入的基础颜色派生 OpenClaw 专用变量:

  • --bg-accent
  • --bg-elevated
  • --bg-hover
  • --panel
  • --panel-strong
  • --panel-hover
  • --chrome
  • --chrome-strong
  • --text
  • --text-strong
  • --chat-text
  • --muted
  • --muted-strong
  • --accent-hover
  • --accent-muted
  • --accent-subtle
  • --accent-glow
  • --focus
  • --focus-ring
  • --focus-glow
  • --secondary
  • --secondary-foreground
  • --danger
  • --danger-muted
  • --danger-subtle

派生规则放在纯辅助函数中,以便独立测试。确切的颜色混合公式是实现细节,但该辅助函数必须满足两个约束:

  • 保持接近导入主题意图的可读对比度
  • 对相同导入载荷生成稳定输出

v1 中忽略的 token

这些 tweakcn token 在第一个版本中有意忽略:

  • chart-*
  • sidebar-*
  • font-serif
  • shadow-*
  • tracking-*
  • letter-spacing
  • spacing

这会把范围限定在当前 Control UI 实际需要的 token 上。

字体

如果存在字体栈字符串,则导入它们,但 OpenClaw 在 v1 中不会加载远程字体资产。如果导入的栈引用浏览器中不可用的字体,则适用正常回退行为。

故障行为

错误导入必须以关闭方式失败。

  • URL 格式无效:显示内联校验错误,不发起获取。
  • 不支持的主机或路径形态:显示内联校验错误,不发起获取。
  • 网络失败、非 OK 响应或 JSON 格式错误:显示内联错误,保持当前存储载荷不变。
  • Schema 失败或缺少 light/dark 块:显示内联错误,保持当前存储载荷不变。
  • 清除操作:
    • 移除存储的自定义载荷
    • 移除受管理的自定义样式标签内容
    • 如果 custom 当前处于活动状态,则将主题系列切回 claw
  • 首次加载时存储的自定义载荷无效:
    • 忽略存储的载荷
    • 不输出自定义 CSS
    • 如果持久化的主题系列为 custom,则回退到 claw

失败导入在任何时候都不应让活动文档应用了部分自定义 CSS 变量。

实现中预计会变更的文件

主要文件:

  • ui/src/ui/theme.ts
  • ui/src/ui/storage.ts
  • ui/src/ui/app-settings.ts
  • ui/src/ui/views/config.ts
  • ui/src/ui/views/config-quick.ts
  • ui/src/styles/base.css

可能新增的辅助函数:

  • ui/src/ui/custom-theme.ts

测试:

  • ui/src/ui/app-settings.test.ts
  • ui/src/ui/storage.node.test.ts
  • ui/src/ui/views/config.browser.test.ts
  • 针对 URL 解析和载荷规范化的新聚焦测试

测试

最低实现覆盖范围:

  • 将分享链接 URL 解析为 tweakcn 主题 ID
  • /themes/{id}/r/themes/{id} 规范化为获取 URL
  • 拒绝不支持的主机和格式错误的 ID
  • 校验 tweakcn 载荷形态
  • 将有效 tweakcn 载荷映射为规范化的 OpenClaw 浅色和深色 token 映射
  • 在浏览器本地设置中加载并保存自定义载荷
  • lightdarksystem 解析 custom
  • 当不存在载荷时禁用 Custom 选择
  • custom 已经处于活动状态时立即应用导入的主题
  • 当活动自定义主题被清除时回退到 claw

手动验证目标:

  • 从设置中导入一个已知的 tweakcn 主题
  • lightdarksystem 之间切换
  • custom 和内置系列之间切换
  • 重新加载页面并确认导入的自定义主题在本地持久存在

发布说明

此功能刻意保持较小范围。如果用户之后要求多个导入主题、重命名、导出或跨设备同步,请将其视为后续设计。不要在本次实现中预先构建主题库抽象。