# 安装指南 面向人和 AI agent,每一步都可以直接复制执行。约定: - `$DSH_HOME` 是 DSH 的数据目录,默认 `~/.dsh`。**先解析它,不要硬编码**: ```bash DSH_HOME="${DSH_HOME:-$HOME/.dsh}" echo "$DSH_HOME" ``` - 本文假设 GUI 在 `http://127.0.0.1:3080`。端口不同的话把下面的 URL 一起换掉。 - 仓库路径以 `/path/to/dsh-claude-theme` 代指,实际用本仓库的绝对路径。 **皮肤有两种装法,二选一**: - **装法一:第三方皮肤中心** `@linxin666/dsh-client-ui-skin-center`(本皮肤在该插件的 **v0.3.23** 上测试)。在它的市场里搜 Claude 装上即可,第 4 步选中。第 1 步的手动拷贝可以跳过。 - **装法二:独立皮肤插件** `plugins/skin/`,**不需要皮肤中心**。它首次加载时会自己把皮肤装好,第 1–4 步都不用手动做,见下文第 5 步。 两种装法**不要同时用**(会各自注入一遍同一套样式表)。第 6–8 步的三个插件**都不经过皮肤中心**,只装插件的话前面的皮肤步骤都可以跳过。 检查皮肤中心是否已装(装法二不需要): ```bash cat "$DSH_HOME/profiles/web/node_modules/@linxin666/dsh-client-ui-skin-center/package.json" \ | grep '"version"' ``` --- ## 第 1 步:安装皮肤(手动,装法一可选) > 走**装法二**(独立插件)的话整步跳过——插件会自己装好。 > 走**装法一**且从市场下载的话也跳过。只有想手动放置皮肤时才需要这一步。 皮肤是纯资源目录:拷贝即可,不需要构建、不需要安装依赖。 ```bash DSH_HOME="${DSH_HOME:-$HOME/.dsh}" mkdir -p "$DSH_HOME/skins" # 目录名必须是 claude(清单里的 id 与目录名一致) rm -rf "$DSH_HOME/skins/claude" cp -r /path/to/dsh-claude-theme/claude "$DSH_HOME/skins/claude" ``` 装法二的插件走的是同一套逻辑,只是它把「只拷运行时需要的文件、不覆盖已有文件」 做在代码里:两份样式表、`skin.json`、4 个 woff2 字体、`LICENSE`—— 生成器的输入不会进你的皮肤目录。 安装后的布局: ``` $DSH_HOME/ ├── skins/ │ └── claude/ # ← 皮肤装在这里 │ ├── skin.json │ ├── skin.css │ ├── patches.css │ ├── assets/fonts/*.woff2 │ └── preview/ └── skin-center-active.json # 当前选中的皮肤 id(由皮肤中心读写) ``` 也就是说:**皮肤的根目录就是 `$DSH_HOME/skins//`,`skin.json` 直接位于其中**,不要再套一层目录。 --- ## 第 2 步:确认皮肤被发现 皮肤中心有一个目录接口,直接问它: ```bash curl -s http://127.0.0.1:3080/api/skin-center/v2/catalog \ | python3 -c " import json,sys skins = json.load(sys.stdin)['skins'] hit = [s for s in skins if s['manifest']['id'] == 'claude'] if not hit: print('NOT FOUND — 皮肤未被发现'); raise SystemExit(1) s = hit[0] print('id :', s['manifest']['id']) print('origin :', s.get('origin')) # 本地安装应为 user print('warnings :', s.get('warnings')) # 必须为空 print('name :', s['manifest']['name']) " ``` 期望输出: ``` id : claude origin : user warnings : [] ``` **判定标准:`warnings` 必须是空数组 `[]`。** 非空说明清单声明了磁盘上不存在的文件,或 CSS 被清洗器拒绝了内容——`warnings` 里会写原因,先修那个再看效果。 --- ## 第 3 步:确认 CSS 被正确送出 皮肤中心把清洗、作用域化之后的 CSS 从这些路径送出(注意用的是**清洗后**的字节数,不是源文件大小): ```bash for u in \ /api/skin-center/v2/skins/claude/stylesheet \ /api/skin-center/v2/skins/claude/patches \ /api/skin-center/v2/skins/claude/assets/fonts/inter-normal.woff2 \ ; do printf '%-58s HTTP %s %s bytes\n' "$u" \ "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:3080$u")" \ "$(curl -s "http://127.0.0.1:3080$u" | wc -c)" done ``` 期望(三个都是 HTTP 200): | 路径 | 说明 | 期望字节数 | | --- | --- | --- | | `/api/skin-center/v2/skins/claude/stylesheet` | 清洗并作用域化后的 `skin.css` | 47410 | | `/api/skin-center/v2/skins/claude/patches` | 清洗后的 `patches.css` | 一万出头(该文件仍在改动,不锁定具体值) | | `/api/skin-center/v2/skins/claude/assets/fonts/inter-normal.woff2` | 字体资产 | 48256 | 字节数量级对不上也没关系,**关键是不能 404**。404 说明清单里的 `contributes` 与实际文件不匹配。 一个容易踩的坑:**送出的字节数会比 `validate-skin.mjs` 打印的数字大几个到几十个字节**。校验器打印的是 JavaScript 字符串长度(字符数),而 HTTP 响应是 UTF-8 字节数;CSS 里的 `—`、`§`、`…` 这类字符一个占 3 字节。别把这个差值当成内容不一致。 确认 CSS 内容确实是本皮肤: ```bash curl -s http://127.0.0.1:3080/api/skin-center/v2/skins/claude/stylesheet | head -5 curl -s http://127.0.0.1:3080/api/skin-center/v2/skins/claude/stylesheet \ | grep -o -- '--dsw-[a-z0-9-]*' | sort -u | wc -l # 应输出 278 ``` **278 是去重后的 token 名数量**,这是判定皮肤完整的关键数字。不要用 `grep -c -- '--dsw-'`——那个数的是出现次数,送出的 CSS 里是 743(本地源文件里是 375),含义不同。 --- ## 第 4 步:选中皮肤(装法一:皮肤中心) 在 GUI 里打开 **设置 → 皮肤中心**,选中 **Claude**。 切换是在页面内原子完成的:不刷新、不重启。当前选中的 id 会被写进: ``` $DSH_HOME/skin-center-active.json ``` 也可以直接确认: ```bash curl -s http://127.0.0.1:3080/api/skin-center/v2/active # {"ok":true,"active":"claude", ...} ``` > 走**装法二**(独立皮肤插件)的话跳过这一步,直接看第 5 步。 --- ## 第 5 步:独立皮肤插件(装法二,与装法一互斥) 不想为了一个皮肤装整套皮肤中心的话,用这个。**第 1–4 步都可以整段跳过**——插件会在首次加载时自己把皮肤装到 `$DSH_HOME/skins/claude`。 ```bash dsh plugin --profile web add link:/path/to/dsh-claude-theme/plugins/skin # 然后重启 DSH 一次 ``` 重启后在 **设置 → Claude-X** 里选中 Claude。 - 包名 `dsh-claude-skin`。 - **自己装皮肤**:按插件自身位置解析到仓库的 `claude/`,只拷运行时需要的 8 个文件(两份样式表、`skin.json`、4 个 woff2 字体、`LICENSE`),**只补缺、不覆盖**已有文件。所以重复加载或升级插件都不会动你改过的东西。 - 它只做三件事:读皮肤目录、按[作用域契约](plugins/skin/README.md)注入样式表、把皮肤自带的字体和图片用路由送出去。 - **不依赖皮肤中心**,也**不依赖**品牌插件或 Clawd 插件。 - **不做 CSS 白名单过滤**(皮肤中心会拒绝远程 URL、`@import`、逃逸皮肤目录的路径)。只适合跑自己写的皮肤。 确认它进了 bundle 列表,并且皮肤已经被播种: ```bash DSH_HOME="${DSH_HOME:-$HOME/.dsh}" python3 -c " import json, os p = json.load(open(os.path.join(os.environ['DSH_HOME'], 'profiles/web/package.json'))) print('dependencies:', [k for k in p['dependencies'] if 'claude-skin' in k]) print('bundles :', [b for b in p['dsh']['profile']['bundles'] if 'claude-skin' in b]) " ls "$DSH_HOME/skins/claude/skin.json" && echo ' ↑ 皮肤已就位' ``` 顺带确认样式表真的被正确处理了。**页面需要登录 cookie**,裸请求会 401,所以先在本机浏览器里登录一次,把 cookie 导出成 Netscape 格式的 `cookies.txt`,再跑: ```bash python3 -c " import re raw = open('served.html', encoding='utf8').read() # curl -b cookies.txt http://127.0.0.1:3080/ -o served.html blocks = re.findall(r'', raw, re.S) skin = [b for b in blocks if 'data-dsh-skin' in b] print('注入的皮肤样式块数:', len(skin)) print('html 属性:', re.search(r']*>', raw).group(0)) print('裸 :root 选择器:', bool(re.search(r'(^|[\n}])\s*:root\s*[,{]', re.sub(r'html\[data-dsh-skin=\"[a-z0-9-]+\"\]', '', skin[0] if skin else '')))) " ``` 期望输出(本机实测): ``` 注入的皮肤样式块数: 1 html 属性: 裸 :root 选择器: False ``` 第三行是重点:`False` 说明样式表被正确改了作用域。若为 `True`,皮肤会看起来**完全没生效**。 懒得跑脚本的话,直接开页面按 F12,在 Elements 里看 `` 有没有 `data-dsh-skin="claude"`,再看 `` 里有没有一个装着皮肤样式表的 `