Appearance
OpenClaw 的自定义主题导入功能允许用户从 tweakcn 链接导入一个浏览器本地存储的主题,在现有 claw、knot、dash 三套内置主题之外新增一个 custom 族。导入的 payload 必须同时包含 light 和 dark cssVars 块,否则会被拒绝。导入后通过 Settings > Appearance 中的 Custom 卡片选择,支持 light/dark/system 模式切换,仅在当前浏览器持久化,不写入网关配置。
OpenClaw 自定义主题导入配置:通过 tweakcn 链接添加主题
Status: approved in terminal on 2026-04-22
概要
在 OpenClaw Control UI 中新增一个精确的浏览器本地自定义主题插槽,可从 tweakcn 分享链接导入。现有内置主题族仍为 claw、knot 和 dash。新增的 custom 族的行为与普通 OpenClaw 主题族一致,当导入的 tweakcn payload 同时包含 light 和 dark token 集合时,支持 light、dark 和 system 模式。
导入的主题仅存储在当前浏览器配置文件中,与 Control UI 其他设置一同保存。它不会写入网关配置,也不会跨设备或浏览器同步。
问题
Control UI 主题系统目前封闭在三个硬编码主题族中:
ui/src/ui/theme.tsui/src/ui/views/config.tsui/src/styles/base.css
用户可以在内置族和模式变体之间切换,但无法在不编辑仓库 CSS 的情况下从 tweakcn 引入主题。当前需求小于一个完整的主题系统:保留三个内置族,并增加一个用户控制的导入插槽,可通过 tweakcn 链接替换。
目标
- 保持现有内置主题族不变。
- 仅增加一个导入的自定义插槽,而非主题库。
- 接受 tweakcn 分享链接或直接的
https://tweakcn.com/r/themes/{id}URL。 - 导入的主题仅持久化在浏览器本地存储中。
- 导入插槽与现有的
light、dark和system模式控制配合工作。 - 保持安全的失败行为:错误的导入绝不会破坏当前 UI 主题。
非目标
- 不支持多主题库或浏览器本地的导入列表。
- 不支持网关侧持久化或跨设备同步。
- 不支持任意 CSS 编辑器或原始主题 JSON 编辑器。
- 不会自动从 tweakcn 加载远程字体资源。
- 不支持仅暴露一种模式的 tweakcn payload。
- 不会对仓库进行超出 Control UI 所需接缝的全局主题重构。
已确定的用户决策
- 保留三个内置主题。
- 增加一个 tweakcn 驱动的导入插槽。
- 主题存储在浏览器中,而非网关配置。
- 导入插槽支持
light、dark和system。 - 后续导入覆盖自定义插槽是预期行为。
推荐方案
在 Control UI 主题模型中增加第四个主题族 ID custom。仅当存在有效的 tweakcn 导入时,custom 族才变为可选。导入的 payload 被归一化为 OpenClaw 特定的自定义主题记录,并与其他 UI 设置一起存储在浏览器本地存储中。
运行时,OpenClaw 渲染一个受控的 <style> 标签,定义解析后的自定义 CSS 变量块:
css
:root[data-theme="custom"] { ... }
:root[data-theme="custom-light"] { ... }这使自定义主题变量范围限定在 custom 族内,避免将内联 CSS 变量泄漏到内置族中。
架构
主题模型
更新 ui/src/ui/theme.ts:
- 将
ThemeName扩展为包含custom。 - 将
ResolvedTheme扩展为包含custom和custom-light。 - 扩展
VALID_THEME_NAMES。 - 更新
resolveTheme()使得custom镜像现有族行为:custom + dark->customcustom + light->custom-lightcustom + system-> 根据操作系统偏好返回custom或custom-light
不为 custom 添加旧别名。
持久化模型
扩展 UiSettings 持久化(ui/src/ui/storage.ts),增加一个可选的自定义主题 payload:
customTheme?: ImportedCustomTheme
推荐的存储形状:
ts
type ImportedCustomTheme = {
sourceUrl: string;
themeId: string;
label: string;
importedAt: string;
light: Record<string, string>;
dark: Record<string, string>;
};注意:
sourceUrl存储归一化后的原始用户输入。themeId是从 URL 中提取的 tweakcn 主题 ID。label为 tweakcnname字段(若存在),否则为Custom。light和dark是已经归一化的 OpenClaw token 映射,而非原始 tweakcn payload。- 导入的 payload 与其他浏览器本地设置并存,在相同的本地存储文档中序列化。
- 如果加载时存储的自定义主题数据缺失或无效,忽略该 payload,并在持久化的族为
custom时回退到theme: "claw"。
运行时应用
在 Control UI 运行时中,新增一个狭义的自定义主题样式表管理器,位于 ui/src/ui/app-settings.ts 和 ui/src/ui/theme.ts 附近。
职责:
- 在
document.head中创建或更新一个稳定的<style id="openclaw-custom-theme">标签。 - 仅当存在有效的自定义主题 payload 时发出 CSS。
- 当 payload 被清除时移除样式标签内容。
- 保持内置族 CSS 在
ui/src/styles/base.css中;不要将导入的 token 拼入检入的样式表。
该管理器在加载、保存、导入或清除设置时运行。
亮色模式选择器
实现应优先使用 data-theme-mode="light" 进行跨族亮色样式,而不特殊处理 custom-light。如果现有选择器钉在 data-theme="light" 并需要应用于每个亮色族,请在此工作中扩大范围。
导入 UX
更新 ui/src/ui/views/config.ts 中的 Appearance 部分:
- 在
Claw、Knot和Dash旁边添加一个Custom主题卡片。 - 当未导入自定义主题时,将卡片显示为禁用状态。
- 在主题网格下方添加导入面板,包含:
- 一个文本输入框,用于 tweakcn 分享链接或
/r/themes/{id}URL - 一个
Import按钮 - 当自定义 payload 已存在时,显示
Replace路径 - 当自定义 payload 已存在时,显示
Clear操作
- 一个文本输入框,用于 tweakcn 分享链接或
- 当 payload 存在时,显示导入的主题标签和来源主机。
- 如果当前活动主题是
custom,导入替换后立即生效。 - 如果当前活动主题不是
custom,导入仅存储新 payload,直到用户选择Custom卡片。
快速设置主题选择器(ui/src/ui/views/config-quick.ts)也应在 payload 存在时仅显示 Custom。
URL 解析与远程获取
浏览器导入路径接受:
https://tweakcn.com/themes/{id}https://tweakcn.com/r/themes/{id}
实现应将两种形式归一化为:
https://tweakcn.com/r/themes/{id}
然后浏览器直接获取归一化后的 /r/themes/{id} 端点。
对于外部 payload,使用窄模式验证器。因为这是一个不受信任的外部边界,推荐使用 zod schema。
所需的远程字段:
- 顶级
name为可选字符串 cssVars.theme为可选对象cssVars.light为对象cssVars.dark为对象
如果 cssVars.light 或 cssVars.dark 缺失,拒绝导入。这是刻意的:批准的产品行为是完整的模式支持,而不是尽力合成缺失的一侧。
Token 映射
不要盲目镜像 tweakcn 变量。将有限子集归一化为 OpenClaw token,并在一个辅助函数中推导其余部分。
直接导入的 Token
从每个 tweakcn 模式块中:
backgroundforegroundcardcard-foregroundpopoverpopover-foregroundprimaryprimary-foregroundsecondarysecondary-foregroundmutedmuted-foregroundaccentaccent-foregrounddestructivedestructive-foregroundborderinputringradius
从共享的 cssVars.theme(如果存在):
font-sansfont-mono
如果某个模式块覆盖了 font-sans、font-mono 或 radius,则以模式本地值为准。
为 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
推导规则位于一个纯辅助函数中,以便独立测试。精确的颜色混合公式属于实现细节,但辅助函数必须满足两个约束:
- 保持接近所导入主题意图的可读对比度
- 对同一导入 payload 产生稳定输出
v1 忽略的 Token
以下 tweakcn token 在第一个版本中故意忽略:
chart-*sidebar-*font-serifshadow-*tracking-*letter-spacingspacing
这保持范围在当前 Control UI 实际需要的 token 上。
字体
如果存在,会导入字体栈字符串,但 OpenClaw 在 v1 中不会加载远程字体资源。如果导入的栈引用了浏览器中不可用的字体,则应用正常的回退行为。
失败行为
错误的导入必须关闭失败。
- 无效的 URL 格式:显示内联验证错误,不发起获取。
- 不支持的主机或路径形状:显示内联验证错误,不发起获取。
- 网络失败、非 OK 响应或格式错误的 JSON:显示内联错误,保持当前存储的 payload 不变。
- 模式验证失败或缺少 light/dark 块:显示内联错误,保持当前存储的 payload 不变。
- 清除操作:
- 移除存储的自定义 payload
- 移除受管理的自定义样式标签内容
- 如果当前活动主题是
custom,将主题族切换回claw
- 首次加载时存储的自定义 payload 无效:
- 忽略存储的 payload
- 不发出自定义 CSS
- 如果持久化的主题族是
custom,回退到claw
在任何情况下,失败的导入都不能使活动文档应用部分自定义 CSS 变量。
预期变更的文件
主要文件:
ui/src/ui/theme.tsui/src/ui/storage.tsui/src/ui/app-settings.tsui/src/ui/views/config.tsui/src/ui/views/config-quick.tsui/src/styles/base.css
可能的新辅助文件:
ui/src/ui/custom-theme.ts
测试文件:
ui/src/ui/app-settings.test.tsui/src/ui/storage.node.test.tsui/src/ui/views/config.browser.test.ts- 新增针对 URL 解析和 payload 归一化的测试
测试要求
实现最低覆盖:
- 将分享链接 URL 解析为 tweakcn 主题 ID
- 将
/themes/{id}和/r/themes/{id}归一化为获取 URL - 拒绝不支持的主机和格式错误的 ID
- 验证 tweakcn payload 形状
- 将有效的 tweakcn payload 映射为归一化的 OpenClaw light 和 dark token 映射
- 在浏览器本地设置中加载和保存自定义 payload
- 为
light、dark和system解析custom - 当无 payload 时禁用
Custom选择 - 当
custom已激活时立即应用导入的主题 - 当清除自定义主题时回退到
claw
手动验证目标:
- 从 Settings 导入一个已知的 tweakcn 主题
- 在
light、dark和system之间切换 - 在
custom和内置族之间切换 - 重新加载页面并确认导入的自定义主题在本地持久化
发布说明
此功能特意保持较小规模。如果用户后续要求多个导入主题、重命名、导出或跨设备同步,请作为后续设计处理。不要在此实现中预构建主题库抽象。
常见问题
为什么导入 tweakcn 链接后 Custom 主题不可选?
只有当导入的 payload 同时包含 cssVars.light 和 cssVars.dark 对象时,Custom 卡片才会变为可选。如果缺少其中任一,导入失败且不会存储。请在 tweakcn 端确认主题包含完整的亮色和暗色模式变量。
导入的主题保存在哪里?重启浏览器后会丢失吗?
导入的主题以 JSON 形式存储在浏览器本地存储中,与 Control UI 其他设置(如 UiSettings)共存。只要不手动清除浏览器数据或点击 Custom 卡片上的 Clear 按钮,主题会在重启后保留。它不会跨设备或跨浏览器同步,也不会写入 OpenClaw 网关配置。
如何替换或删除已导入的自定义主题?
在 Settings > Appearance 页面中,如果已存在自定义主题,导入面板会显示 Replace 和 Clear 操作。点击 Clear 会删除存储的 payload 并将当前主题族切换到 claw。点击 Replace 并输入新链接会覆盖当前自定义主题。如果当前活动主题是 custom,替换后会立即生效。