dsh-claude-style 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.en.md +33 -30
- package/README.md +14 -12
- package/docs/STYLE.md +9 -0
- package/docs/architecture.md +18 -1
- package/lib/client.js +2389 -345
- package/lib/index.js +126 -12
- package/lib/model-descriptions.json +50 -46
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **冒烟测试 `npm run smoke`(`scripts/smoke.cjs`)**:零依赖、不需要运行中的 dsh,直接检查构建产物。宿主半边:用假的 cordis 上下文驱动 `lib/index.js`,对私有路由发跨站页面、局域网、DNS 重绑定、浏览器同源、桌面壳转发几种请求,分别走宿主的 `requestRejection()` 与本地替身,核对拒绝与放行、415 / 413 / 400 与数组上限。浏览器半边:在无头 Chrome/Edge 里把 `lib/client.js` 装进一个替身宿主页面(模块加载器、ctx、带账号菜单与两个插件条目的侧栏页脚、按宿主键位处理回车的输入框),核对启动完整、空闲时 0 pass、设置 / 账号 / 插件来的字符串不按 markup 解析、回车交给宿主、宿主 API 出故障时只退役对应特性(安装期与同步期各一例)、teardown(包括 pass 正在排队时的 teardown)不留任何节点、标记、属性与样式表。它第一次跑就抓到了两个此前没人发现的问题:对话页权限按钮让调度器空转,以及 teardown 之后复活的那一轮 pass。
|
|
7
|
+
- **账号行显示真实头像与昵称(桌面端登录)**:桌面端登录后,皮肤从官方 `remote.account.getProfile()` 取昵称(`value.name ?? contact`)与 `avatarUrl`,账号行改绘**正圆头像**(`border-radius: 50%`),照片加载失败时手绘星芒仍作为兜底显示;未登录 / 非桌面端 / 取数失败时保持原有的星芒与已存用户名。账号信息以 **60 秒轮询**保持新鲜(登录、登出、换头像无需刷新页面)。
|
|
8
|
+
- **账号抽屉接管宿主账号菜单**:抽屉按宿主菜单**动态镜像**条目(顺序、文案、图标、禁用态一致),点击**直连官方行为**——设置走宿主设置入口、意见反馈打开官方问卷、退出登录调 `remote.account.signOut()`、登录走 `startSignIn`;未知条目回退为驱动宿主菜单。宿主自己的账号触发器以 `visibility: hidden` 隐藏(保留盒子供转发),不会出现在界面上。
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **0.1.7 设置页选项能显示但存不进去**:浏览器 boot 给每个插件条目生成的是**随机 loader id**(`dsh-client-modules` 的 `create` 只传 `name`),而设置命名空间是本插件 profile 条目 id——此前直接拿 `ctx.fiber.entry.id` 当命名空间,`configForms.get(随机 id)` 绑到一个没人拥有的命名空间上:读停在默认值、写被宿主拒(`No configurable plugin entry`),且因为 `prefsForm` 非空,读路径连自建路由都不再走。现在命名空间从 `configForms` **已服务的名单**里按候选挑(客户端条目 id / 包名 `dsh-claude-style` / patch 声明的 `ui-skin-claude-style`),挑不到就不绑;官方表单没 ready 或被拒时,读 / 写都回落自建 `/dsh-claude-style/prefs` 路由。
|
|
12
|
+
- **斜杠命令 / @ 提及菜单打开时,回车直接把半截文字发了出去**:皮肤在 `document` 的捕获阶段拦截回车、阻止传播并直接点「发送」,宿主输入框(Lexical)的回车处理因此根本收不到这一键——而正是它负责先选中打开菜单的高亮项、照顾输入法(含 Safari 在 `compositionend` 之后才到的那次回车)、忽略按住不放的连发,最后才发送。所有受支持的宿主(0.1.5-rc.2 起)本来就是回车发送,这段拦截没有任何增益,已删除。**实测**:在模拟宿主键位的输入框里对打开的菜单按回车,旧版点了「发送」,新版由宿主选中了菜单项。
|
|
13
|
+
- **关掉插件或热重载后,皮肤又把自己画了回来**:调度器的 teardown 既不取消已经排上的那一帧 pass,也不拦之后的 `schedule()`(例如账号资料请求晚到时)。teardown 发生在 pass 排上之后——流式输出期间几乎每帧都是——这一轮 pass 就会对已拆掉的特性再跑一遍,把账号行、页脚标记、输入框的 body 属性重新建出来,而且再没有人会清理它们。**实测**(冒烟测试在有 pass 排队时 dispose):teardown 之后又冒出 11–38 个皮肤节点。现在 teardown 取消排队中的那一帧,之后的 `schedule()` 一律不生效。
|
|
14
|
+
- **一个特性出错,整张皮就半挂在页面上**:`apply()` 依次装 14 个特性,却在最后才把 teardown 交给宿主;调度器每轮 pass 也不逐个兜底——一个 sync 抛错,排在它后面的全部跳过,而且每轮如此。**实测**:宿主的 `remote.account.getProfile()` 返回非 Promise 时 `apply()` 中途抛错,样式表与 body 属性留在页面上、调度器没装上、teardown 没注册,关掉插件也清不掉。现在 teardown 最先注册,每个特性单独安装、单独同步:装不上或连续 3 轮同步失败的特性在控制台报一次、跑自己的 teardown 退役,其余照常工作;页脚接管与输入框重绘这两处会隐藏宿主控件的接管,在对应特性退役时把宿主原来的界面还回去;只有调度器本身装不上时才整体回滚。模型选择器的 teardown 此前只清变量、不撤 DOM,现在会真正交还宿主的模型座位。**实测**(无头 Chrome):同样的 `getProfile()` 故障下其余特性照常、页脚回到宿主原样;permissions 的 sync 持续抛错时 3 轮后报一次、输入框重绘关闭;teardown 后页面上不留任何皮肤节点、属性或样式表。
|
|
15
|
+
- **调度器在空闲时也每帧跑一轮 pass**:三处自持循环——① 账号抽屉每轮 pass 都把镜像的宿主菜单行删掉重建,哪怕内容没变、哪怕抽屉开着;② 页脚插件条目有两个以上时,镜像条目的排序按正序逐个插到「设置」行前,每插一个都落在已排好的后面,于是每轮都把每个条目挪一遍;③ 对话页(行内输入框)的权限按钮每轮 pass 都把同一段标签文字重写一遍——同值写 `textContent` 照样替换文本节点。前两处只在登录的桌面端或页脚插件较多时出现,第三处在任何一个打开的对话里都会出现(由新增的冒烟测试发现)。而 `<body>` 下的任何 DOM 变动都会排下一轮 pass,所以永不停歇。**实测**(无头 Chrome,真实 `lib/client.js`):空闲时约 160 passes/s(两处叠加约 320),在 2.2 万节点的模拟对话上每轮约 5.4 ms——60 Hz 下约三分之一的主线程在什么都没发生时被吃掉;抽屉开着时行被替换还会吞掉点击。现在账号行只在内容变化时重建、抽屉开着时不动(揭开前补一次同步),排序只挪真正错位的条目,空闲 pass 降为 0。顺带:账号资料晚到时直接请求一轮 pass(此前写一个 body 属性当「推一下」,但调度器的 `attributeFilter` 根本看不到它,一直靠上面的死循环掩盖;连带删掉从未被读取的头像探测图);只在宿主账号触发器存在时才去读它的菜单,读空或失败后最多每 5 秒重读一次(此前每轮 pass 都开关一次宿主菜单);登出后(`getProfile()` 返回 `null`)账号行回到本地用户名与星芒,不再停在旧昵称和头像上。
|
|
16
|
+
- **非桌面端把「在应用中打开」菜单收进账号抽屉**:宿主账号行被皮肤替换后,其菜单锚点不在 DOM 里,而旧的「页脚里第一个非我们的锚点」兜底会挑中「在应用中打开」,把文件资源管理器 / Cursor / VS Code 等条目收进抽屉。现在**按账号标签匹配触发器**(`账号菜单` / account),匹配不到就只显示「设置」,不再混入别的菜单。
|
|
17
|
+
- **「设置」行变成账号名、点击弹出账号菜单**:设置行的标签与点击目标此前兜底接受任意 button,桌面端该槽位上是宿主账号触发器。现在只接受 `aria-haspopup="dialog"` 的按钮,其次接受非 menu 锚点的按钮,**绝不接受 menu 锚点**。
|
|
18
|
+
- **图标型登出按钮没被排除**:宿主登出按钮是纯图标(无文案),此前按文案判断会漏。现在同时读 `aria-label` / `title` / 类名,并跳过宿主账号区容器内的条目。
|
|
19
|
+
- **设置存储误报「不可用」**:宿主注册命名空间晚于页面加载时,启动时的一次性判定会把设置页卡在「存储不可用」。现在读路径**不再做可用性判定**,控件保持可用;只有写入真的失败才提示。
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **松开推条后旋钮会先弹回原档位、再跳回新档位**:宿主的目录快照**滞后于提交**——`selectModel` 往返按秒计(注册了联网供应商时实测 2–8s),在这段时间里快照的 `effective` 仍是**旧档位**;而 `update()` 无条件跟随快照(`selected = next.index; place(...)`),于是松手后第一次调度 pass 就把旋钮拽回起点,等 echo 落地再拉回新档位。这是推条自带的缺陷(不是本轮换脸引入的,只是胶囊旋钮 + 填充段让这段回弹更显眼)。现在提交时置 `pendingEcho` 并记下所提交档位的 id,`update()` 在等待期间不跟随快照;快照真的报出该档位(echo)、梯子换了(换模型,旧 echo 不会来)、或 12s 兜底超时后才恢复跟随。**实测**(无头 Chrome 驱动真实 `createEffortControl`,模拟「松手 → 5 次陈旧 pass → echo」):把守卫摘掉的对照组旋钮轨迹是 `246 → 148 ×5 → 246`(148 即旧档位 High 的停靠位),守卫在场时全程 `246`,`onPick` 只发一次 `max`。另外 `commit()` 本就只在档位真的变化时才提交,所以「拖走又拖回原档」不会多发一次 RPC。
|
|
23
|
+
- **松手瞬间档位名连跳两下、最高档点阵动画先中止再重播**:**主因在 picker 的渲染,不在推条自身**——`renderModelBody()` 每次渲染都先把整个 footer 清空(`while (modelFooter.firstChild) removeChild`)再 `appendChild` 推条,而 DOM 里「移除再插入」会**重启该子树内所有 CSS 动画**:档位名的模糊聚焦、最高档点阵的入场扫入与闪烁相位全部从头再播。渲染签名里带 `status`,一次选择至少触发两次渲染(`selecting` → 回声后的 `idle`),于是正好「跳两下」。现在 footer 的布局交给新的 `layoutModelFooter()`:它只移除**除推条以外**的节点,推条节点始终挂载、动画时间线不被碰;「加载中/空目录」那条分支显式调 `layoutModelFooter(false, false)` 收掉整块 footer,渲染函数本身不再直接动 footer。**次因**是 `settle()` 的作画顺序:先 `paintValue()`、再用 `commit()` 移动 `selected`、然后又补一次 `paintValue()`,而 `paintValue()` 读的是 `selected`——第一次画的是**手势开始前的旧档位**(名字退回旧名并重播模糊、`data-apex` 关掉使点阵入场中止),提交后再画一次才回到新档。现在改成**先 `commit()` 再只画一次**。顺带给 `place()` 加同值守卫:位移与填充宽度没变就完全不碰 DOM,避免每个调度 pass 的重写打断正在进行的 0.18s 滑行。**实测**(无头 Chrome,三套独立证据):**(1) 机制**——把真实控件拆开再插入后 `animationstart` 计数为 名字 +1 / 入场 +2 / 闪烁 +1,证明「重插即重播」;**(2) 修复**——用真实抽出的 `layoutModelFooter` 源码连跑三次渲染,推条从未出现在 `removedNodes` 里(对照组:旧清空写法第一次渲染就把推条拆掉),且结构顺序为 `divider > effort > more`,同时对 `renderModelBody` 做结构断言(不再直接触碰 footer、必须经 `layoutModelFooter`);**(3) 点击轨迹**——挂钩 `textContent` / `setAttribute` / `setProperty` 的写入点,修复前是 8 步(`text:Max → apex:on → text:High → blur → apex:off → text:Max → blur → apex:on`),修复后收敛为 `text:Max → apex:on`。
|
|
24
|
+
|
|
25
|
+
- **拖动时滑块冲出终点线、并与填充脱开**:上一轮给滑块加的拖动放大用的是独立属性 `scale`,而位移仍写在 `transform` 里——两者的合成顺序是 `translate → scale → transform`,也就是 **`scale` 会把 `transform` 里的位移一起放大**:1.1 倍下终点处的 246px 位移被渲染成 270px(滑块冲出轨道右端 24px),中途则整体超前约 10%,于是滑块跑到填充前面、看着像「脱离」。修法是把位移改用独立属性 `translate`(合成顺序在 `scale` 之前,不被放大),过渡列表里的 `transform` 也换成 `translate`。另外填充右端此前是 8px 圆角,它在滑块中心线处收边、上下两角向内退,即使位置正确也会在槽的上下沿露出缝——右端改为直角(左端仍保留圆角与滑槽对齐),而右端始终藏在 16px 宽的滑块之下。**实测**(无头 Chrome,DOM 量测):修复前拖动到最右端 `knobRelLeft=270`(轨道宽 262,即冲出 25px),修复后为 `245`、`rightGap=-1`(仅放大本身的 0.8px),且 `fill→knobCentre=0`、填充右缘始终在滑块左缘之内 9px。
|
|
26
|
+
|
|
27
|
+
- **点击两个档位正中间有概率让档位选择器「崩溃」**:宿主每提交一次档位都会**重新枚举整个目录**(实测 2–8s),这段窗口里快照可以既没有分组、也没有当前席位,`modelEffort()` 于是返回 null;而新拆出的档位碎片把「读不到档位」直接当成「这个模型没有档位」,于是**在提交后的 RPC 期间把触发器和已打开的卡片一起摘掉了**——看起来就是选择器崩掉。老 picker 早就踩过同一个坑(它曾在每次选择时把整个列表清空,见下方 [0.5.0] 的修复),这次是把那条纪律重新漏掉了一遍。修法两步:`ui.model` 新增 `settled()`(「目录现在能不能报出席位」,就是 picker 自己那条判据的唯一来源),碎片在**未 settle 期间既不摘触发器也不关卡片**,并保留最后一次读到的档位梯子(`lastEffort`)继续画,只有**已 settle 且确实没有档位**时才收掉触发器、让控件回到空态。**实测**(无头 Chrome 用 stub 的 `ui` 驱动真实 `installEffortPicker`,模拟「提交 → 目录空白 6 次 sync → 回声」):触发器仍在、卡片仍开、滑块未进空态且档位名保持旧值,回声到达后更新为新档位;把 `settled()` 置真且梯子为空时触发器才被摘除。
|
|
28
|
+
- **点击滑槽其他位置时滑块瞬移**:`onPointerDown` 里先置 `dragging = true` 再写位移,而 `place()` 在 `dragging` 时走「直写」分支(本意是拖动跟手),于是单击远处时滑块**直接跳过去**;即便改成先动画再置标志也不行——`data-dragging` 在同一个任务里就被设上,浏览器在下一帧做样式重算时过渡列表里已经没有 `translate`,动画根本没开始(实测同步读与 40ms 读都是终点值)。现在把「按下」与「拖动」拆成两个状态:**`pressed`** 从 pointerdown 起(此时过渡仍在,滑块 180ms 滑向按下位置,读数与填充一起动),**`dragging`** 从第一次 pointermove 起(此时才加 `data-dragging`、改为直写跟手);`settle()` 与 `update()` 的守卫改看 `pressed`,`isDragging()` 也返回 `pressed`(悬停关闭的豁免要从按下就生效)。**实测**(无头 Chrome):按下后 40ms 时 `getAnimations()` 里有一条 `translate` 过渡在跑(180ms),260ms 到达按下位置、松手精确落在最近档位;只点不动(不触发 pointermove)也能正常提交。
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **档位名换档改成「交换」动画,不再生硬替换**:此前档位名只是重新赋值 + 一次模糊聚焦;现在旧名会先被复制到一个**绝对定位的 ghost**(位置取自 `valueEl.offsetLeft`,与名字原位精确重合,且不占布局),随后**旧名向上模糊淡出、新名自下模糊淡入**(`translateY(5px)` → 0,`blur(4px)` → 0,280ms)。两条动画都由 override 用 `!important` 内联重置来重播(样式表规则必须带 `!important` 才能压过宿主,普通内联 `none` 会在级联里输掉、重启变成空操作)。拖动中也照此执行:滑块每越过一个档位,名字就像滚动计数器一样换一次。**实测**(无头 Chrome 驱动真实控件):换档瞬间 ghost 文本为旧名 `High`、value 为 `Low`、`ghost.style.left` 与 `value.offsetLeft` 一致(58px = 58px)、两条动画分别在 `running`、360ms 后 ghost 计算透明度为 `0`。
|
|
32
|
+
- **档位两端文案随语境本地化(中文:更快 / 更强)**:此前 `effortFaster` / `effortSmarter` 的 `zh` 值刻意写的英文——按「它们说的是方向、不是档位名」的旧约定保持英文。现按用户要求改为中文语境下显示**更快 / 更强**,英文语境仍是 `Faster` / `Smarter`;只是文案表 `src/model-descriptions.json` 的数据改动,控件本身照旧走 `copyLabel()`,不需要改 JS。**实测**(把真实文案表注入预览页后读 DOM):两端渲染为 `更快` / `更强`。
|
|
33
|
+
- **最高档的点阵动画:纯哈希粒子**(这条改了四轮,最终回到第一版):最终形态是每块方块按自己的**哈希相位**闪烁,周期长度也各自抖动 ±8%,色调从 8 档里哈希取一档——**没有任何排序或方向**,读起来是一片各自为政的粒子;羽流形状(右端实、向左渐隐)由**逐块静态透明度**承担,颜色闪烁从它中间穿过,两者互不干扰。中途试过三种「有方向」的版本并被逐一否掉:严格按距离排序的波(像一根横条扫过去)、流动项 + 随机项(刷新向左偏置)、以及整片持续左移的传送带(那是真的在动,不是刷新倾向)——用户最终确认第一版的纯随机粒子最好,故全部回退,只保留动画效果本身的回退,其余(档位独立弹层、圆角、拖动放大、阻尼、触发器间距)不变。**实测**(无头 Chrome 读真实方块):无传送带节点、无滚动动画;260 块、相位散布 **1.37s**、周期 **1.334–1.565s**(抖动在)、**8** 种随机色调;列位置与平均延迟的相关系数 **-0.039**、左右三分之一落差 0.008s(即**没有方向偏置**);逐块透明度左 1/3 均值 **0.101** / 右 1/3 **0.998**(羽流形状);容器无 `mask-image`。
|
|
34
|
+
- **座位里模型与档位两枚触发器靠拢**:拆成两枚按钮后,两边的对称内边距在名字和档位之间留了 16px 的空档,读起来像两个不相干的控件;现在档位触发器 `margin-left: -6px` + 左内边距 2px,实测**名字与档位之间只剩 4px**,两枚读作一句「Opus 5.5 High」。
|
|
35
|
+
- **推理等级从模型弹层拆出来,成为独立的触发器与弹层**:此前模型选择器的 footer 里同时住着模型列表和档位推条——选模型和定思考力度是两件节奏完全不同的事(模型很少换、档位常微调),却共用一次开合与同一张卡片。现在座位上是**两枚并排触发器**:左边模型名(开模型弹层)、右边当前档位名(开档位弹层,用静音色,模型是主语、档位是修饰);模型弹层里只剩模型列表 + 更多模型(分隔线仍按「有没有更多模型」决定画不画),档位推条搬进自己的卡片。**结构上按仓库的 750 行停止线拆出一个新碎片** `src/overrides/effort-picker.js`(199 行):它持有档位触发器、档位卡片与开关/定位/清理,只向 `ui.model` 要三样东西——座位元素 `seat()`、档位描述 `effort()`、提交 `pickEffort()`;`model-picker.js` 相应摘掉推条、`effortSlider` 与 footer 里的滑块逻辑(`layoutModelFooter(showMore)` 现在只画分隔线 + 更多模型)。上一轮为「picker 重渲染拆节点导致动画重播」立的纪律跟着搬到新卡片:滑块节点建好后**永不重挂**,卡片每次 sync 只更新数值与位置。两枚触发器与两张卡片都遵守既有弹层纪律:`autoPopover === 'all'` 时悬停开、否则点击开,开一张关另一张,点击外部 / Esc 关闭(scheduler 新增 `ui.effort` 的 sync / owns / close 三处接线)。**实测**(无头 Chrome 用 stub 的 `ui` 驱动真实 `installEffortPicker`,16 项断言全过):触发器落在模型按钮右侧且标签随席位回声更新、点击开卡片并把模型卡片关掉、连续 6 次 sync 后滑块**从未**出现在 `removedNodes` 里、拖到第二档会经 `pickEffort('low')` 提交、模型无档位时触发器摘除且卡片关闭、teardown 两个节点都清理;另用真实抽出的 `layoutModelFooter` 源码断言新 footer 形状为 `divider > more` / 空。
|
|
36
|
+
- **推条手感三处打磨**:**(1) 圆角收小**——滑槽/填充/点阵从 999px 全圆改为 8px 圆角矩形,滑块从胶囊改为 5px 圆角矩形(录屏里滑槽端部约高度的 1/3、滑块顶边有明显平段,此前的全圆比 Claude 更圆)。**(2) 拖动放大 10%**——按住时滑块 `scale: 1.1`、松手回 1,0.12s 缓动;`translate` 被排除在过渡之外(拖动每帧写它),所以放大/缩回平滑而跟手不受影响。**(3) 档位点阻尼**——指针位移经阻尼曲线 `t − 0.8·sin(2πt)/2π` 映射到滑块:档位点附近滑块只走指针约**五分之一**的速度(斜率 1−D),段中点则以 1.8 倍速度反超(「离开档位点有阻尼」),段中点仍 1:1 跟手、后段略超前,视觉上像被吸向下一档;曲线在每个档位点与每个中点都精确不动且单调,所以「最近档位」的判定仍可用原始指针位置,松手也照旧精确落在档位上。**实测**(无头 Chrome 驱动真实控件):f=0.125 时指针 6px / 滑块 2px(33%),f=0.25 时指针 12px / 滑块 6px(50%),f=0.5 时两者同为 25px,f=1.0 时同为 49px;四处圆角 computed 为 8/5/8/8px;拖动中 `scale=1.1`、松手后为 1。
|
|
37
|
+
- **推理推条换脸成 Claude Desktop 同款:填充段 + 档位刻点 + 胶囊旋钮,最高档触发点阵动画**:对照 Claude 的真机录屏逐像素量过——滑槽不是此前的 14px 浅槽配圆珠,而是一条 **26px 的粗槽**,旋钮是 **16×30 的竖直圆角滑块**(上下各探出槽 2px;圆角随后又收小,见下方手感打磨一条)、两态都钉在纯白上(暗色里深旋钮会沉进槽里);槽里**从左端到旋钮中心铺一段填充**(标签墨色 26% 透明度,一条规则同时喂亮暗两态),**每个档位在槽心留一枚 4px 刻点**,刻点与填充同色——划过的刻度被填充吞掉、当前档被旋钮盖住,刻点只在裸槽上露面。**最高档是整条的华彩**:到达最后一档(拖动中的实时位置也算)时填充与刻点退场,槽内浮起一片**点阵**——5 行约 4px 的方块按整设备像素解算网格(不出现 7/8px 交替缝),每块带着哈希撒开的相位在深紫与更亮的峰值色之间闪烁、左端按掩码淡出回槽色、进入时从右向左扫入,旋钮同时染上紫光晕、档位名变紫——这正是 Claude「Ultracode」那一档的招牌动效,且「最高档即特殊档」(点阵技法参考了社区插件 domitor-syh/dsh-ui-skin-switcher 的实现:整设备像素网格、雪崩哈希散相、右起扫入)。**紫色是对仓库「陶烬橙唯一强调色」的一次显式破例**(用户指定:最高档沿用 Claude 的 Ultracode 紫,不做橙色转译),色值从录屏里量出——方块深紫 ≈ rgb(150,127,200)、亮紫 ≈ rgb(208,203,228)、档位名 ink 更深,落在 --dsh-claude-apex / --dsh-claude-apex-flash / --dsh-claude-apex-ink 三个令牌上(亮暗各一套)。**方块的闪烁色一律写在样式表里**:override 只给每块挑一个色调档(`data-tone`,四档从深紫铺到浅紫),行内只留动画相位——这条是有代价换来的:早先把逐块峰值 rgb 写在行内 `--dsh-flash-light` 上,样式表一换代,旧一代 DOM 里的行内橙色值不会跟着变,于是出现「底色已经变紫、闪烁却还是橙色方块」的残留态(实测新构建全新加载为 90 种颜色、0 个橙色像素,问题只出在热替换残留的节点上)。颜色回到样式表后,任何一次换表都会把每个方块一起重染。档位名在**非拖动**的换档时重播一次 280ms 的模糊聚焦;拖动中跨档保持清晰不糊。无档位的模型行为不变(旋钮照拖、松手回右端、不铺填充)。键盘路径(方向键/Home/End)同样走完填充/刻点/点阵的全套状态。
|
|
38
|
+
- **输入框底部的「背景色阴影」改成座位自己画的一条横条 + 一条渐变**:此前让消息在输入框一带淡出,靠的是在卡片(以及输入域)上刷三层 `--dsw-alias-bg-base` 阴影——亮/暗/聚焦/带附件每一态都要重写一遍(共 12 处),凡是重置卡片 `box-shadow` 的规则都得跟着重述(附件轨因此只能写成无阴影,hero 还得用四条规则把阴影剥掉)。现在由宿主那个 `[data-composer-seat]` 自己承担:座位整块刷成画布色,于是**横条高度就是座位高度**(卡片 + 工具行 + 统计句 + dock),不需要跟任何盒子对齐;座位顶边之上挂一条 `--dsh-composer-fade-h`(40px)的渐变,由画布色向上淡到透明。这也正是宿主自己的做法——它在 active 态本就给座位铺了一条 36px 的同款渐变,只是没盖住座位下半段,消息才从工具行后面透出来;皮肤现在只是把座位补满。`--dsh-composer-fade-h` 同时是消息区的底部留白(`viewArea` 的 `padding-bottom`),所以最后一轮永远停在渐变带的上沿,不会半行被吃掉。
|
|
39
|
+
- **模型介绍文案整体校订一轮**:事实未动,只修写法。**(1) 一处中英语义冲突**——`kimi-for-coding-highspeed` 中文括号读作「常规版约 180 token/s」、英文却说高速版自己跑 180 tokens/s,两句相反;现统一为「高速版约 180 tokens/s,为常规版的 5–6 倍」。**(2) 单句措辞**——`doubao-seed-2.1` 补上缺的「以」并消去重复的「智能体」、mercury「快达同类的 10 倍」、doubao「多模态可用」、hunyuan「中文与 Agent 场景均衡」等生硬或中英夹杂的句子改写通顺;`gpt-5.4` 的「原生电脑操作」改为「原生 computer use(电脑操控)」。**(3) 全文统一**——速度一律 `tokens/s`(TPS、token/s 不再混用)、上下文一律大写 `K`(256k/128k 消除)、价格英文一律 `per MTok`、「开放权重」取代混用的「开源权重 / open / open-source」、「编程」取代混用的「编码」、「图像」取代「图片」、「智能体」取代残留的英文 Agent 与「代理式」。**(4) 向仓库约定收口**——celeris 家族规则里钉死的 Celeris-1 数字(1,664 tokens/s、MMLU-Pro 75.9%)挪进新增的 `celeris-1` 精确条目,家族文案改为版本无关的「扩散架构大语言模型」,下一代 Celeris 不再继承过时数字;gpt 兜底删去「最新模型均」的时效断言。另修正两处中英不对齐:deepseek 兜底的「普惠」英文实为 affordable 而非 efficient、doubao 的「千亿级」英文实漏两个数量级。deepseek pro「比肩顶级闭源模型」(官方标题)与 mistral「前沿模型」两处擦边最高级保留不动。
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
- **用户名、账号昵称、头像地址与页脚插件条目不再拼进 HTML**:账号行、封号彩蛋的提示条、账号抽屉镜像的页脚插件条目,此前把这些字符串直接拼进 `innerHTML`——用户名来自设置(任何能写入设置的人都能改它),昵称和头像地址来自账号服务,条目文案与角标来自第三方插件。**实测**:把用户名设成 `<img src=x onerror=…>` 后,在无头 Chrome 里加载真实的 `lib/client.js`,这段脚本在 GUI 页面里执行了——而 GUI 页面能驱动会执行 shell 命令的智能体。现在这些值一律以文本写入(`textContent`),图标改为复制宿主/插件自己的节点、不再重新解析 markup。头像改为一个真正的 `<img>`(`referrerPolicy="no-referrer"`,与宿主自己的头像一致),只接受 http(s) 地址并以属性写入。顺带修好了**头像从未显示**:旧代码用 `JSON.stringify` 把地址拼进 `style="…"`,双引号提前截断了属性(实测解析结果只剩 `--dsh-claude-account-photo:url(`,地址碎成几个无意义的属性名);而且头像只在账号行首次创建时写入,账号信息晚到一步就再也不画。现在每次 pass 同步,加载失败时图片自行隐藏、露出下面的星芒。镜像条目的角标后来才出现时也会补上(此前只在建条目那一刻读一次)。
|
|
43
|
+
- **设置与用户名路由不再对任何人敞开**:宿主半边的 `/dsh-claude-style/prefs`(读写设置)与 `/dsh-claude-style/username`(系统用户名)挂在宿主 web server 上,而插件路由不经过宿主给 `/api` 的鉴权。**实测**:一个带陌生 Host / Origin、`Content-Type: text/plain`、不带 cookie 的 POST(跨站页面无需预检就能发出,局域网里也能直接 curl)返回 200 并写入了用户名;同样的请求还能读到全部设置命名空间与系统用户名,且请求体与快捷供应商数组不设上限(2 万条照单全收)。现在这两条路由先过宿主自己的请求栅栏(`connection.requestRejection()`:Host/Origin 校验 + 浏览器会话 cookie,与 `/api` 同一套,0.1.5-rc.2 起即有),写入还要求 JSON 内容类型、请求体不超过 16 KiB、快捷供应商至多 64 个;宿主没有该服务时退回只服务回环的本地栅栏。已核对浏览器同源请求与桌面壳转发的请求都能通过宿主真实的栅栏,跨站与 DNS 重绑定的请求被拒绝。模型文案与字体仍公开。
|
|
44
|
+
|
|
3
45
|
## [0.5.0] - 2026-09-22
|
|
4
46
|
|
|
5
47
|
### Added
|
package/README.en.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
<div align="center">
|
|
2
1
|
# DSH Claude Style
|
|
3
2
|
|
|
4
|
-
**A
|
|
3
|
+
**A theme plugin that recreates the look and feel of Claude Code Desktop for the DeepSeek Harness Web GUI.**
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
5
6
|
|
|
6
7
|
[](README.en.md) [](README.md)
|
|
7
8
|
|
|
@@ -19,71 +20,73 @@
|
|
|
19
20
|
</tr>
|
|
20
21
|
</table>
|
|
21
22
|
|
|
22
|
-
> Light: ivory canvas `#FCFCFB` with a
|
|
23
|
+
> Light: ivory canvas `#FCFCFB` with a pale sidebar `#FBFBF9`; dark: warm black `#141413`. The theme follows the system light/dark mode, and ember orange `#D97757` is the single action accent on both canvases.
|
|
23
24
|
|
|
24
25
|
## Fonts
|
|
25
26
|
|
|
26
|
-
> **Important: the
|
|
27
|
+
> **Important: the Anthropic fonts are not distributed with the npm package — they live in this repository under [`fonts/`](fonts/).** You can install them system-wide, or skip the install entirely: drop the two `.ttf` files into the plugin package's `fonts/` directory and the host serves them as webfonts the same way (identical files, identical result). Either way, refresh / restart the web UI.
|
|
27
28
|
|
|
28
29
|
| Font | Used for | File |
|
|
29
30
|
|---|---|---|
|
|
30
|
-
| Anthropic Sans Web Text | UI
|
|
31
|
-
| Anthropic Serif Web Text | Conversation body /
|
|
31
|
+
| Anthropic Sans Web Text | Interface / UI | [`fonts/AnthropicSansWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSansWebText.ttf) |
|
|
32
|
+
| Anthropic Serif Web Text | Conversation body / Markdown | [`fonts/AnthropicSerifWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSerifWebText.ttf) |
|
|
32
33
|
| JetBrains Mono Variable | Code / code blocks | [`fonts/JetBrainsMonoVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoVariable.ttf), [`fonts/JetBrainsMonoItalicVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoItalicVariable.ttf) |
|
|
33
34
|
|
|
34
|
-
Enabling the Anthropic
|
|
35
|
+
Enabling the Anthropic fonts (pick one):
|
|
36
|
+
|
|
37
|
+
① Install system-wide — on Windows, double-click the `.ttf` → "Install"; on macOS, import it with Font Book.
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
② No install — copy the `.ttf` files into the plugin package's `fonts/` directory. Refresh the page afterwards.
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
> The Anthropic Sans/Serif fonts are copyright Anthropic, licensed for personal use only, and are not covered by the MIT license.
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
## Installation
|
|
41
44
|
|
|
42
|
-
1.
|
|
45
|
+
1. From a terminal
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
48
|
dsh plugin --profile web add dsh-claude-style # npm package (recommended)
|
|
46
49
|
dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub source
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
2.
|
|
52
|
+
2. From the [plugin market](https://github.com/dsh-market/dsh-market)
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
Keep only one theme enabled at a time. After installing, **restart `dsh web`** and refresh the page.
|
|
52
55
|
|
|
53
|
-
##
|
|
56
|
+
## Features
|
|
54
57
|
|
|
55
|
-
1. **Theme** —
|
|
56
|
-
2. **
|
|
57
|
-
3. **Model picker**
|
|
58
|
-
4. **
|
|
59
|
-
5. **
|
|
60
|
-
6. **
|
|
58
|
+
1. **Theme** — global the moment it is installed, nothing to configure. Ivory `#FCFCFB` in light mode, warm black `#141413` in dark mode, with the same ember orange `#D97757` action accent on both; light/dark follows the system colour mode.
|
|
59
|
+
2. **Composer** — the input box is rebuilt end to end: a permission segment control (Full access / Read only / …), a model trigger carrying the vendor lockup, and the tool row and the stats sharing one line, with the send and stop keys unified into a 7px rounded rectangle. The stats sentence and the model trigger share one type size and colour.
|
|
60
|
+
3. **Model picker** — a new two-level popover: level one lists the official service plus the quick providers picked in settings, and the "More models" second level groups the rest by provider. Every row carries its vendor lockup and description; the foot holds the reasoning-effort slider (stepless — it settles on the nearest level when you let go) and the "More models" entry. Level two is bottom-aligned with level one, providers already shown in level one are not repeated, and a model without thinking support draws no slider at all. Hover opens after a 50ms dwell and closes after 150ms, and the sliver between the two cards does not count as leaving.
|
|
61
|
+
4. **Workspace** — the sidebar's "Workspace" heading becomes an **Active / Archived** segment control: Active keeps the host's own session tree, while Archived is the skin's own flat list (title, time, and an unarchive plus a delete button on every row). The rows line up with the host's own session rows item by item (row x=12 / width 251 / height 28 / title x=36). On a host without archiving support the control is not drawn and the plain heading stays.
|
|
62
|
+
5. **Sidebar** — the New session and Plugins rows take Claude's own shape: no plate at rest, a plate on hover, the "+" of New session set in a circular chip, and both icons turning 90° clockwise on hover (four-fold symmetric glyphs, so they land back on themselves); the account drawer and the ban-screen easter egg stay as they were.
|
|
63
|
+
6. **Account area** — after a desktop sign-in the row shows the **real avatar and nickname** (kept fresh by a 60-second poll); the account drawer mirrors the host's own menu (Settings / Feedback / Sign out — or Sign in), and each action calls the official behavior directly: the questionnaire opens in a new tab, sign-out hits the account namespace, and the host's own trigger stays out of sight. On hosts without the desktop account the drawer keeps only its Settings entry.
|
|
61
64
|
|
|
62
|
-
##
|
|
65
|
+
## Disabling and uninstalling
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
To pause the theme without uninstalling it, add this to the profile's `cordis.patch.yml` (`~/.dsh/profiles/web/cordis.patch.yml`):
|
|
65
68
|
|
|
66
69
|
```yaml
|
|
67
70
|
- id: ui-skin-claude-style
|
|
68
71
|
disabled: true
|
|
69
72
|
```
|
|
70
73
|
|
|
71
|
-
It
|
|
74
|
+
It takes effect within about a second; refresh the page to get the stock look back.
|
|
72
75
|
|
|
73
76
|
```bash
|
|
74
77
|
dsh plugin --profile web remove dsh-claude-style # uninstall
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
Then restart `dsh web`;
|
|
80
|
+
Then restart `dsh web`; if you added this theme's entry to `cordis.patch.yml` by hand, remove that too.
|
|
78
81
|
|
|
79
|
-
##
|
|
82
|
+
## Documentation
|
|
80
83
|
|
|
81
|
-
| Document |
|
|
84
|
+
| Document | What it covers |
|
|
82
85
|
| --- | --- |
|
|
83
|
-
| [Design
|
|
86
|
+
| [Design tokens](docs/STYLE.md) | Palette, fonts, shapes, source layout and the host-selector discipline (in English) |
|
|
84
87
|
| [Changelog](CHANGELOG.md) | Version history |
|
|
85
|
-
| [Contributing](CONTRIBUTING.md) |
|
|
88
|
+
| [Contributing](CONTRIBUTING.md) | Building from `src/`, commit conventions, screenshot and regression tooling (in English) |
|
|
86
89
|
|
|
87
|
-
##
|
|
90
|
+
## Friends
|
|
88
91
|
|
|
89
|
-
> Running several themes
|
|
92
|
+
> Running several themes at once? Try [dsh-skin-manager](https://github.com/xiaoyangcheng84-svg/dsh-skin-manager) — it switches between every installed theme from one settings page.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
## 字体
|
|
26
26
|
|
|
27
|
-
> **重要:
|
|
27
|
+
> **重要:Anthropic 字体不随 npm 包分发,仅在仓库 [`fonts/`](fonts/) 供下载**——既可以直接安装到系统,也可以免安装:把两个 `.ttf` 放进插件包的 `fonts/` 目录,宿主会以同样的 webfont 方式提供它们(两种方式的字体文件完全一致,效果相同)。生效均需刷新 / 重启 web。
|
|
28
28
|
|
|
29
29
|
| 字体 | 用途 | 文件 |
|
|
30
30
|
|---|---|---|
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
| Anthropic Serif Web Text | 对话正文 / Markdown | [`fonts/AnthropicSerifWebText.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/AnthropicSerifWebText.ttf) |
|
|
33
33
|
| JetBrains Mono Variable | 代码 / 代码块 | [`fonts/JetBrainsMonoVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoVariable.ttf)、[`fonts/JetBrainsMonoItalicVariable.ttf`](https://github.com/Nwflower/dsh-claude-style/raw/main/fonts/JetBrainsMonoItalicVariable.ttf) |
|
|
34
34
|
|
|
35
|
-
Anthropic
|
|
35
|
+
Anthropic 字体启用(二选一):
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
① 安装到系统——Windows 双击 `.ttf` → 「安装」,macOS 用「字体册」导入;
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
② 免安装——把 `.ttf` 复制到插件包的 `fonts/` 目录。完成后刷新页面生效。
|
|
40
|
+
|
|
41
|
+
> Anthropic Sans/Serif 字体版权归 Anthropic 所有,仅供个人使用,不适用 MIT 许可。
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
## 安装
|
|
42
44
|
|
|
43
45
|
1. 通过终端安装
|
|
44
46
|
|
|
@@ -51,14 +53,14 @@ dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub 源
|
|
|
51
53
|
|
|
52
54
|
同一时刻建议只启用一个主题。安装后**重启 `dsh web`** 并刷新页面即生效。
|
|
53
55
|
|
|
54
|
-
##
|
|
56
|
+
## 特点
|
|
55
57
|
|
|
56
|
-
1. **主题** ——
|
|
57
|
-
2.
|
|
58
|
-
3.
|
|
59
|
-
4.
|
|
60
|
-
5.
|
|
61
|
-
6.
|
|
58
|
+
1. **主题** —— 安装即全局生效,无需配置。亮色象牙白 `#FCFCFB`、暗色暖黑 `#141413`,两套画布共用同一个操作强调色陶烬橙 `#D97757`;亮暗跟随系统颜色模式切换。
|
|
59
|
+
2. **输入框** —— 输入框整块重做:权限分段控件(完全权限 / 只读 / …)、带品牌锁定标的模型触发器、工具栏与状态统计同一行排版,发送键与停止键统一成 7px 圆角。统计句与模型触发器共用一套字号与颜色。
|
|
60
|
+
3. **模型选择器** —— 全新的两级弹层:一级列官方服务与设置页勾选的快捷供应商,「更多模型」二级按供应商分组;每行带厂商锁定标与说明文案,底部是推理等级推条(无极滑动,松手对齐最近档位)与「更多模型」入口。二级底边与一级对齐,一级已显示的供应商不再重复;模型不支持思考时不画推条。悬停停留 50ms 打开、离开 150ms 关闭,两卡之间的空隙不算离开。
|
|
61
|
+
4. **工作区** —— 侧栏「工作区」标题改成 **进行中 / 已归档** 分段控件:进行中沿用宿主的会话树,已归档是皮肤自己的平铺列表(标题、时间、每行「取消归档」与「删除」两个图标按钮)。行排版与宿主的会话行逐项一致(行 x=12 / 宽 251 / 高 28 / 标题起点 x=36)。宿主没有归档能力时(如 0.1.5 的某些构建)不显示控件、保留原标题。
|
|
62
|
+
5. **侧栏** —— 新会话与插件两行取 Claude 真机形状:默认无底色、hover 才有底色,新会话的「+」套一枚圆形底,两行图标 hover 顺时针转 90°(四重对称图形,转回原位);底部账户抽屉、封号彩蛋一并保留。
|
|
63
|
+
6. **账号区** —— 桌面端登录后显示**真实头像与昵称**(60 秒轮询保持新鲜,失败回退手绘星芒);账号抽屉接管宿主账号菜单:**动态镜像**「设置 / 意见反馈 / 退出登录(或登录)」,点击直连官方行为(官方问卷、账号登出、宿主设置页),宿主自己的触发器隐藏不出现在界面上;非桌面端自动只保留「设置」入口。
|
|
62
64
|
|
|
63
65
|
## 停用与卸载
|
|
64
66
|
|
package/docs/STYLE.md
CHANGED
|
@@ -33,6 +33,15 @@ Rules:
|
|
|
33
33
|
- Radius: 4 / 8 / 16 px; pills for CTAs (`9999px`).
|
|
34
34
|
- Borders: 1px warm hairline.
|
|
35
35
|
- Spacing: 4 px rhythm.
|
|
36
|
+
- Bottom edge: the transcript never fades behind the composer by shadow. The
|
|
37
|
+
composer seat paints an opaque `var(--dsw-alias-bg-base)` bar across its own
|
|
38
|
+
box (so the bar's height *is* the composer's height) and one
|
|
39
|
+
`--dsh-composer-fade-h` (40px) gradient band above it, running to transparent.
|
|
40
|
+
The same token is the transcript's bottom clearance, so the last turn rests
|
|
41
|
+
exactly at the band's top edge. Never re-add background-coloured halo shadows
|
|
42
|
+
to the card to hide the transcript: they have to be restated in every
|
|
43
|
+
light / dark / focus / attachment rule, and every rule that resets the card's
|
|
44
|
+
`box-shadow` inherits the job.
|
|
36
45
|
- Focus: the composer field lights up — its hairline takes the input box's own
|
|
37
46
|
shadow colour (espresso `#141413` in light) with a 1px halo in the same tone
|
|
38
47
|
and a deeper drop shadow. Dark inverts the face: on the near-black canvas a
|
package/docs/architecture.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
## D2. 纯浏览器半边实现,不动宿主
|
|
21
21
|
|
|
22
22
|
- **背景**:主题是皮肤,不应 fork DSH;宿主升级要快跟随。
|
|
23
|
-
- **决定**:一切效果通过 CSS 覆盖与客户端 DOM override 实现;宿主半边(`lib/index.js`)只提供静态路由(模型文案 JSON
|
|
23
|
+
- **决定**:一切效果通过 CSS 覆盖与客户端 DOM override 实现;宿主半边(`lib/index.js`)只提供静态路由(模型文案 JSON、位图图标)。(后来宿主半边还承担了字体、设置读写与系统用户名路由;私有路由的安全约束见 D11。)
|
|
24
24
|
- **代价**:依赖宿主带哈希的 CSS-module 类名,宿主改版可能击穿选择器——用 D3 的纪律和 probe 回归对冲。
|
|
25
25
|
- **重审条件**:DSH 官方开放主题 API / 插槽覆盖所 target 的区域时,逐步迁移过去。
|
|
26
26
|
|
|
@@ -80,3 +80,20 @@
|
|
|
80
80
|
- 客户端:`ctx.configForms.get(entryId)` 可用就用官方表单(值 + 写队列 + revision 栅栏),否则回落到自建路由;设置界面在 0.1.7 注册成 `plugins.bundle.config`(键 = 包名,渲染在插件页上),旧宿主注册成 `settings.section` 整页。分流靠 `slots.inject` 的「槽被声明才触发」语义(旧宿主从不声明前者),再用 `configForms` 是否存在否决后者在 0.1.7 上的重复注册。
|
|
81
81
|
- **代价**:设置层有两套传输与两套席位,回归必须覆盖两种宿主形态(`.debug/settings-bridge-check.cjs` 跑宿主半边两代,`.debug/client-settings-check.cjs` 在无头浏览器里跑客户端两代);`Config` 的顶层 await 让宿主半边模块求值晚一步(loader 本就 await 导入,无实际影响)。
|
|
82
82
|
- **重审条件**:不再支持 0.1.5-rc.2(桌面端内置升级到 0.1.7+)时,删掉旧路径与两处探测,只留 `Config` + `plugins.bundle.config`。
|
|
83
|
+
|
|
84
|
+
## D11. 插件自有路由借宿主的请求栅栏;宿主/用户来源的字符串只以文本上屏
|
|
85
|
+
|
|
86
|
+
- **背景**:`webServer.register()` 交给插件的是裸请求。宿主自己的 `/api` 挂在 Host/Origin 栅栏与浏览器会话 cookie 之后(`connection.requestRejection()`),插件路由不在其内。本插件的 `/prefs`(写设置)与 `/username`(读系统用户名)因此曾经完全无鉴权:跨站页面用 `text/plain` 发 POST 无需预检即可写入设置,`dsh web` 绑定 `0.0.0.0` 时局域网里任何人都能直接写。写进去的用户名又被客户端拼进 `innerHTML`——实测在 GUI 页面里执行了脚本,而这个页面能驱动执行 shell 命令的智能体。
|
|
87
|
+
- **决定**:两层各守一道。宿主半边:`/prefs` 与 `/username` 处理前先调 `ctx.get('connection').requestRejection(req)`(0.1.5-rc.2 起即有),拒绝即回 401/403;`/prefs` 另要求 `Content-Type: application/json`(跨站页面发不出不经预检的 JSON)、请求体上限 16 KiB、快捷供应商至多 64 个短 id。宿主没有该服务时,插件自带的替身只服务回环(回环 Host、无跨站标记、Origin 与 Host 一致)。模型文案与字体等静态资产保持公开。客户端:一切来自设置、账号服务、系统或第三方插件的字符串只用 `textContent` / 元素属性写入,图标复制节点而非重新解析 markup。
|
|
88
|
+
- **代价**:路由依赖宿主的 connection 服务。已核对两种壳都能通过:浏览器同源请求带会话 cookie;0.1.7 桌面壳经 `forwardWebRequest` 转发到回环 Host、剥掉页面 Origin 并自带 cookie(宿主真实的 `isTrustedApiRequest` 对这两种请求放行,对跨站与 DNS 重绑定请求拒绝)。0.1.5-rc.2 桌面端根本不把插件路径路由到 webServer(非 `/api` 一律走静态资产),不受影响。
|
|
89
|
+
- **重审条件**:宿主为插件提供自带鉴权的路由注册(如 `connection.fetch.register`)时,迁移过去并删掉本地替身。
|
|
90
|
+
|
|
91
|
+
## D12. 特性级失败隔离:一个特性出错只关掉它自己
|
|
92
|
+
|
|
93
|
+
- **背景**:`apply()` 依次装 14 个特性,teardown 却在最后才交给 `ctx.effect`;每轮 pass 里各 `sync()` 也没有逐个兜底——一个抛错,排在它后面的全部跳过,而且每轮如此。实测:宿主 `remote.account.getProfile()` 返回非 Promise 时 `apply()` 中途抛错,样式表与 body 属性留在页面上、调度器没装上、teardown 没注册(关掉插件也清不掉),前面装好的特性的监听器一并泄漏。effort-picker 那次「Loading plugins…」卡死是同一类问题。
|
|
94
|
+
- **决定**:
|
|
95
|
+
- teardown 最先经 `ctx.effect` 注册且幂等;每个特性单独 try/catch 安装,装不上的报一次 `console.error` 并退役。调度器本身装不上时整体回滚到宿主原样——没有调度器,其余特性都不会同步,留着只是一张半套的皮。
|
|
96
|
+
- 每轮 pass 里每个特性的 `sync()` 单独 try/catch,连续失败 3 轮即报一次并退役(`ui.retire`)。
|
|
97
|
+
- 退役 = 跑该特性自己的 teardown,并把它接管的宿主界面还回去。页脚接管(`FOOTER_ATTR`,由偏好写)与 composer 重绘(`COMPOSER_ATTR`,由 permissions 的 pass 写)都会隐藏宿主控件,所以退役 `footer` / `permissions` 时对应的闸门强制关闭、不再随偏好打开。特性的 teardown 因此必须撤干净自己的 DOM 与标记(模型选择器此前只清变量,已补上);只做装饰的 pass 用自己的句柄名(`ui.settingsNav`),退役它不会卸掉设置页。
|
|
98
|
+
- **代价**:特性失效时界面上没有提示,只有控制台一行;依赖它的特性(例如都读 `ui.copy.isComposerActive()`)会各自连续失败、依次退役——降级而非崩溃。
|
|
99
|
+
- **重审条件**:宿主提供插件级的错误上报 / 健康面板时,把报告接过去。
|