---
description: "右侧实时组件栏：会话、机器、额度与成本，一眼看完。"
---

<p align="right"><a href="README.md">English</a> · <b>简体中文</b></p>

<p align="center">
  <img src="docs/icon/icon.svg" alt="dsh-widgets" width="104" height="104">
</p>

<h1 align="center">DeepSeek-Harness Widgets · 组件栏</h1>

<p align="center">
  <strong>对话右边那条空带，变成一格格实时卡片；想收起来，一键就收起。</strong>
</p>

<p align="center">
  61 张卡：会话 · 上下文 · 后台任务 · 这台机器 · 额度与花费 · 峰谷定价<br>
  每张独立安装、2×2 / 2×4 调尺寸、按实例调阈值、不看就关；指针划过有 macOS 坞站式放大波峰。
</p>

<p align="center">
  <img src="https://img.shields.io/npm/v/dsh-widgets?style=flat&label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC&color=4D6BFE" alt="最新版本">
  <img src="https://img.shields.io/npm/dt/dsh-widgets?style=flat&label=%E7%B4%AF%E8%AE%A1%E4%B8%8B%E8%BD%BD&color=4D6BFE" alt="累计下载">
  <a href="https://github.com/Physicolor/dsh-widgets/stargazers"><img src="https://img.shields.io/github/stars/Physicolor/dsh-widgets?style=flat&label=%E2%98%85&color=08C" alt="GitHub stars"></a>
  <img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License">
  <img src="https://img.shields.io/badge/DSH%200.1.x%20%2F%200.2.x-4493F8?style=flat-square" alt="支持 DeepSeek Harness 0.1.x 与 0.2.x">
</p>

<p align="center">
  <img src="docs/screenshots/cover.zh-CN.png" alt="DeepSeek-Harness Widgets 预览" width="100%">
</p>

<p align="center">
  <img src="docs/screenshots/rail-demo-still.png" alt="组件栏静息时的网格（波峰放大动图见「波峰放大（动图）」）" width="40%">
  <img src="docs/screenshots/desktop-market.png" alt="组件市场与已安装列表" width="48%">
  <br>
  <sub>装好之后的样子（左边的静帧点开是动效：<a href="docs/screenshots/rail-demo.gif">rail-demo.gif</a>，3.8 MB）· 以及安装、调尺寸、关掉每一张卡的组件市场。</sub>
</p>

**安装（30 秒）**：把下面两行整段复制给你的 DSH agent，或者自己在终端跑：

```sh
# `web` 是 `dsh web` 的默认 profile 名；你自己的 profile 名 = $DSH_HOME/profiles/ 下的目录名
dsh plugin --profile web add dsh-widgets
# 装完重启 dsh web，浏览器硬刷新一次（Ctrl+Shift+R）
```

> 不会用命令也没关系——跟你的 DSH 说一句「**装上 dsh-widgets 插件，重启 web 之后告诉我**」就行；装完点会话头部的 **组件** 图标，栏就出来了。

支持 DeepSeek Harness `0.1.0-rc.6+`（0.1.x）与 `0.2.0-rc.1+`（0.2.x）。组件栏挂在会话头部的
**组件** 图标后面；在 设置 → **组件** 里安装、排序与调整尺寸。

**你得到的**：61 张卡，装的、摆的、调的都随你——

- **实时会话**——轮次·步数、上下文水位与压缩、token 与缓存命中率、LLM 时长、首 token 延迟、工具调用、后台任务与子代理；
- **钱与额度**——OpenCode Go 的滚动 / 周 / 月窗口；Command Code 的 5 小时 / 周 / 月限额，点一下即在整个 Key 池之间切换，外加余额、月末外推与今日推荐；
- **时间，以及官方钱包**——时钟（指针或数字、六个时区）、日历（有 DSH 定时任务的日子打点；2×2 月历，2×4 多一列临近日程）、任务计时器（本回合已跑多久、此刻在等什么）、秒表（开始 / 暂停 / 复位）、倒计时（时长胶囊或最近的定时任务），以及 DeepSeek 余额卡（旁边是账本推导的当日花费）；
- **这台机器**——CPU / GPU / 内存 / 磁盘 / 网络 / 供电，含迷你折线、环形图，以及本地服务与代理出口看板；
- **你自己的视图**——每张卡都是独立单元：装它、选 2×2 或 2×4、改它的阈值，或者干脆关掉；其余由组件栏自行排布。

> **一句话：**装一个插件，对话右侧那条空带就变成一格格实时卡片——token 与缓存、上下文、后台任务、机器状态、额度与花费。每张卡都是独立单元，装、改尺寸、调阈值、关掉都随你；整条组件栏想收就收。

DeepSeek-Harness Widgets 会在 DeepSeek Harness 会话页的右侧放上一条**组件栏**。每张卡都是独立单元：从内置市场里挑，放进 1–4 列的网格，选 2×2 或 2×4，按实例单独配置，不看的直接关掉。组件栏只占用正文旁边那条空带，一键即可整栏收起。

**为什么值得装**：那些数字本来要另开标签页或另开面板才看得到——上下文还剩多少、这个会话花了多少、额度还有几天到期、机器烫不烫。它们更适合安静地待在对话旁边，扫一眼就走，不打断你正在做的事。

**如果它帮上了忙**：这个插件没有别的分发渠道，只有「被人看到」这一条——在 [Physicolor/dsh-widgets](https://github.com/Physicolor/dsh-widgets) 点一个 star，就是把它推到 DSH 各插件目录前列、让更多人看到的那一下（GitHub 的搜索与推荐都按 star 排序）。从视频过来的话，在弹幕或评论里说一句你装了哪几张卡，比 star 更能让我知道该往哪加；Bug 与组件想法在 [`issues`](https://github.com/Physicolor/dsh-widgets/issues) 里一直开着。

## 官网 / Showcase

项目官网位于 [`website/`](website/)，线上地址 **https://physicolor.github.io/dsh-widgets/**，是一座**设计系统站点**：讲 dsh-widgets 是什么、为什么存在、全部 61 个真实组件，以及 **DSH Widget Design Grammar**（直接取自 `src/client/index.ts` 的真实单位/间距/放大公式，并带一条交互式组件栏跑插件自己的放大曲线与右对齐回流）、**Widget Anatomy**（一张真实卡片放大 ×2，每一处内边距、间距与内缩都从 DOM 实测标注）、**DSH Visual Audit**（13 条明示规则对全部 61 个组件按真实 `getBoundingClientRect` 测量打分——规则驱动，不是模型打分）、部件单元化架构、生产 Workflow，以及「需求表 → Widget Specification」生成器。站点为纯 HTML/CSS/JS，无构建步骤，所有路径都是相对路径（适配项目 Pages 的 base path）。`node website/verify.mjs` 自校验（静态 + SEO + Edge 无头浏览器检查，共 88 项）；组件表、静态画廊 markup 与 JSON-LD `ItemList` 由 `node website/gen-site.mjs` 从各 manifest 生成（`--check` 在漂移时报错）；`node website/gen-og.mjs` 重新生成社交分享图。部署方式见 `website/README.md`。

---

## 当前功能

### 多列网格布局

| 项目 | 说明 |
| --- | --- |
| 最多列数 | 1 / 2 / 3 / 4（设置中下拉选择，默认 2 列）。这是**上限**：空间不足时自动逐级回退，空间充裕也不会超过它 |
| 最多行数 | 1–12（默认 5 行）。与「最多列数」相乘就是组件区一次能摆下的格子数，也就是**可放置组件的上限**：「最多组件数」按 `列 × 行` 生效（存储值不会丢，调大列或行就立刻恢复），设置行上有感叹号标记，悬停说明是被哪一个数字覆盖的 |
| 可用宽度从哪来（为什么还有空位却不加列） | 组件栏只吃正文**阅读宽度**之外的余量：`列宽 − 阅读宽度 − 74px − 24px`（74 = 官方滚动区两侧内边距与列宽拖拽带的保留，24 = 安全余量）。阅读宽度是官方发布的 CSS 变量——DSH 0.1 叫 `--dsh-chat-content-width`，**0.2 改名成了 `--dsh-chat-user-width`**；插件此前只读旧名，读不到就退回估算值（1578px 窗口下 831px 而不是真实的 748px），预算少算约 83px，于是「对话区两侧明明有空间却始终少一列」。两个名字现在都读。修正后同一窗口实测：列宽 1298、阅读宽度 748 → 可用约 452px，3 列 ×130px（占用 430px）或 4 列 ×100px；正文卡片保持 748px 不被压缩。左右两条空白里**只有右边的可被占用**（组件栏右锚定，靠给滚动区加 `padding-right` 占地，正文会重新居中；占得超过 `列宽 − 阅读宽度` 就会压小阅读宽度，这条线不能越），左边那条是正文居中的留白 |
| 2×4 长方形组件 | 宽度为两个 2×2 加一个间距，高度与 2×2 相同；同一组件可同时以两种尺寸安装 |
| 无空隙排列 | 组件按最优适配（best-fit）打包，2×4 留下的空格由后续 2×2 回填，拖动排序永不留下空洞。3 列时若 2×4 会被「截断」，它整体前移一格、被顶掉的 2×2 后移一格（类似四舍五入），长方块不会被孤立 |
| 拖宽实时响应 | 拖动对话宽度时每帧重算可用宽度，卡片在该列数下的档位边界处立即换档（拖动期间正文内边距不做补间，与对话区同步）；列数在阈值处增减。全部为 2×4 时降级跳过 3 列（2 ⇄ 4）；显式选择 3 列则始终保留 |
| 过渡手感 | 尺寸换档与列数变化都用**轻微回弹**（约 4% 过冲的弹簧曲线）平滑放大/缩小与滑到新格子，另加一段按排布顺序错开的微型沉降波（约 1.6% 幅度、逐卡 30ms）——不是整块组件区一起「弹」。组件栏自身的宽度与对话区留白也用**同一条弹簧**，所以换档时容器、卡片、正文三者同步移动，不会出现「容器已经跳过去、卡片还在追」 |
| 悬浮放大 | 多列网格同样生效；放大时行/列都按平面距离让位，间距恒定 |
| 滚轮 = 整行挡位滚动 | 滚轮以**整行**为挡位（位移恒为 `行高 = 卡片边长 + 间距` 的整数倍），因此顶部那行永远完整、不会被截断；一档把下一行拉到顶行的位置。底部被视口切掉的那张卡**直接不画**（见下方「只显示完整组件」），所以组件栏里永远只有完整卡片。滚动一律走浏览器自身的平滑滚动，**鼠标在卡片上、间隙里还是组件栏空白处都一样顺滑**，不会出现逐格硬跳 |

组件栏的**顶边**贴在会话页头的下沿（`--dsx-rail-top`，首行卡片正好压在这条线上；1578×950 实测 44px，首行卡片 50px）。锚点取「组件按钮（页头里的胶囊）所在那一行」与「页头下沿」两者中**更低**的一个：胶囊是用户读组件栏位置的参照物（实测它在距窗口顶 11px 处），但页头本身是**不透明白底 + `z-index: 21`**，而组件栏所在的 composer seat 只有 7 —— 画在页头下沿之上的卡片是被**盖住**而不是被裁剪（实测把栏顶放到胶囊的 11px，首行会被盖掉 33px）。所以默认值就是「贴到胶囊那一行所允许的最高位置」。

### 连续波峰悬浮放大

macOS Dock 式悬浮放大，两种跟随模式（在 **设置 → 组件 → 组件设置 → 悬浮放大 → 无极变化（连续跟随）** 中切换）：

- **无极变化（连续跟随）**：真正无极——每张卡片的缩放由它自身到指针的连续欧氏距离决定，指针任意移动，波峰都在卡片之间平滑滑动。它每一帧都落位到稳态的右对齐几何（`transition: none`），因此卡片右缘在移动过程中也始终与组件栏齐平，不会出现宽度/右缘失步。
- **离散（默认）**：复用同一套连续几何，只把指针吸附到量化格点（行/列中心 + 相邻中点：Y 方向 2·行数−1 个点、X 方向 2·列数−1 个点），波峰在格点之间滑动同样由那条弹簧驱动。

波动的进度是**一条每帧写入的弹簧**：`displayed = 1 + (target − 1) · p`。`target` 是指针的实时几何，`p` 在静息（`0`）与激活（`1`）之间按 Apple 参数化曲线取值（`response 200ms`、`bounce 0.1`——约 250ms 收敛、约 0.15% 过冲 ≈0.4px）。两者都连续，因此进入、跟随、离开在两个方向上都是**一条不中断的位移**；指针继续移动只会改变 `target`，永远不会让曲线重来（此前两种写法——朴素的 CSS transition、以及冻结目标——都正是败在这里，现已全部删除）。离开用同一公式反着跑，`prefers-reduced-motion` 下一帧到位。

两种模式下，放大后的 deck 都由固定悬浮层绘制在组件栏滚动裁剪盒**之外**，因此向左放大不会被裁剪，而静息时的组件栏宽度（以及对话列距离）始终不变。缩放保持正方形卡片与恒定间距；放大倍数可在设置中调节（`1.0–1.4`）。

进出悬浮是**一次连续位移**，不是两层叠图的交叉淡入：悬浮层在静息时与真实卡片逐像素重合，因此切换发生在几何完全一致的瞬间（不可见），随后约 250ms 内把每张卡片的 top/right/width/height 与缩放一起、按同一条弹簧写入波动位置；离开时反向走回静息位置后才交换回来。被悬浮的那张卡在放大层里也保留品牌蓝描边。

进入剩下的成本是**合成层**成本、不是重绘成本，修法是一个常驻提示：放大层的 15 个槽位（以及底部那颗**设置**齿轮）**始终**带 `will-change: transform`，合成层在指针到达之前就已存在——把提示挂在 hover 上实测：自然进入的首帧是 63ms 的层提升 + 栅格化帧；改成常驻后同一进入是 16ms、零掉帧。过去在组件栏空闲时把放大层翻出来 64ms 的「栅格预热」已**删除**：它几乎从不与真实悬浮重合，而常驻提示一到位，那次卡顿就再没复现。

组件栏与放大层由**同一个父级统一接管指针**（两者是必须的兄弟节点：放大层要逃出组件栏的滚动裁剪盒），而且这个指针面是**几何连续**的：波动激活时放大层自身可命中、并把命中盒按「卡片向左越出的最大量」向左扩宽（左内边距同步补偿，卡片不动），所以卡片之间、卡片与间隙之间、以及放大卡片越出组件栏左缘的那一条都不会掉出指针面——否则间隙的命中会落到后面的对话上，导致「悬浮到间隙就取消悬浮」和「在边缘来回移动时反复放大缩小」。指针真正离开时才结束波动（离开判定还带 6px 容差防抖）。**在放大卡片上滚动滚轮依然滚动组件栏**（放大层在滚动容器之外，滚轮被接管为整行挡位滚动并阻止穿透到后面的对话）。

放大层是**挂到 `<body>` 上的**（2026-10-01）：它要压过 `shell.overlay`（z-index 20，harness-ui-harmonizer 的居中卡片外框就在这一层），而组件栏所在的 composer seat 只有 7 —— 同一个堆叠上下文里再高的 z-index 也越不过去。挂到 body 之后放大层以 `z-index: 26` 参与根上下文的排序：在官方右侧面板（10）、shell overlay（20）之上，在设置面板（30）之下。随之而来的三件事都处理了：指针焦点改由 window 级 `mousemove` 驱动（放大层已经不在那个 wrapper 子树里了，否则指针落在放大卡片上时波动会僵住）、滚轮监听同时挂在放大层上（它激活时自己的盒子盖住整条组件栏，滚轮会打到它而不是组件栏）、以及「设置」格子的显示类同时挂在放大层上（它的镜像按钮也在 body 下）。

### 加载骨架

外部数据（OpenCode 用量 / Command Code / 系统监控）尚未返回时，卡片不再显示空值或 0，而是画出**它正在等待的那块内容的外形**（标题保留真实组件名，占位块为圆角填充物，带 1.5s 微光扫过，`prefers-reduced-motion` 下静止）。外形按组件定制，因此加载中的 rail 依然能看出是哪些卡：三个环形图画三个圆角方块，柱状图／折线图画一个大圆角矩形，热度图画一个宽块，数字行画它那几列短块，额度卡画三条堆叠的条；纯文字卡保留数值胶囊 + 正文行。数据返回后原地替换，卡片尺寸与位置都不跳动。

### 连续曲率圆角

卡片圆角是**超椭圆**（`corner-shape: squircle`），不是圆弧：曲率从直边连续过渡进来，这正是 iOS / Figma 式圆角矩形看起来「软」而不是「切」的原因。它是原生 CSS，所以卡片描边与 `box-shadow` 都沿同一条轮廓（用 `clip-path` 做超椭圆会把两者都切掉）；不支持 `corner-shape` 的引擎退化为普通圆角，其他行为不变。

两个控件都在 **设置 → 组件 → 组件设置**：开关（默认**开**，留着是为了能对比两种形状）与**圆角档位**——半径占卡片**短边**的比例（`12 / 16 / 20 / 24 %`，默认 16%）。用百分比而非像素，因为参照物保持的是圆角与边长的比例：150px 的卡片与放大到 190px 的副本不该拿到同样的圆角（`12%` 正好还原旧的固定 16px，边长 150 时）。该档位同时驱动组件栏的静息卡与放大卡、组件市场 / 组件配置的预览、以及加载骨架。

### 设置页的分组与控件规则

通用设置页（`组件设置` tab）按**作用面**分组，而不是按「哪次加进来的」排成一条长列表：

| 分组 | 内容 |
| --- | --- |
| 网格与尺寸 | 最多列数 / 可显示的最多行数 / 最多组件数 / 卡片基准边长 / 组件间距 / 只显示完整组件 |
| 卡片外观 | 连续曲率圆角 / 圆角档位 |
| 悬浮放大 | 无极变化（连续跟随）/ 悬浮放大倍数 |
| 打开与收起动画 | 展开方式 / 打开起始缩放 / 位移曲线 / 回弹幅度 / 缩放曲线 |
| 组件设置面板 | 面板宽度 |
| 对话区 | 隐藏输入框下方文字条 |

「最多列数 / 最多行数 / 最多组件数」三项相邻且都是数字选择器，因为第三项就是前两项的乘积（被覆盖时该行出现感叹号，悬停说明被哪个数字限制）。控件选择只有一条规则：

| 值 | 控件 |
| --- | --- |
| 数值区间 | `Stepper`（产品自带的数字控件） |
| 精选离散集合 | `Select`（产品菜单范式；本页只剩「圆角档位」这一处） |
| 布尔 | `Switch` |

这条规则被破坏过一次：`最多列数` 早期用 `Select` 实现，而后来加的 `最多行数` 用的是 `Stepper`——同类值两种控件。`node scripts/verify-rail-rework.cjs` 现在会逐行审计（列/行必须是 stepper、全页只允许一个 Select、并断言分组顺序），所以新增设置行要**先选分组再插入**，不存在「往末尾追加」。

### 打开/收起动画与「只显示完整组件」

「**整体缩放**」下组件栏从**右上角锚点**出现：既向右滑入（和官方右侧面板同一条 0.3s 过渡），又从起始缩放长回 100%。这两半是**两个元素、两条曲线**：外层 `translateX(位移)` 走「位移曲线」，内层 `scale(s0)` 走「缩放曲线」（一个 `transform` 只能带一条时间函数，所以拆成两层；两层都是 `inset:0` 的 fixed 盒子，组件栏的坐标因此完全不变），两者 `transform-origin: top right`——即视口自身的右上角，在屏幕右缘之外。参数都在 **设置 → 组件 → 组件设置 → 打开与收起动画**：

- **展开方式**（默认「逐个落入」）：**组件栏本身不动也不缩放**，而每张卡片走的正是**「整体缩放」时整块走的那条路径**——同一个锚点、同一个起始缩放、同一段位移、同样两条曲线；不同的只有**各自时机**，而且整个折叠的**总时长就是整块那条时长**：一张卡片的行程与逐拍步进是从它里面**切**出来的（`60% / 40%`），所以 14 格落在 9.23ms 的拍子上，最后一张恰好在整块本该到位的时刻到位——级联从组件区**左下角**那一格开始、**右上角**那一格收尾，收起按同一条时间线倒放（所以快速反向时能从当前进度续播）。每一格都有自己的延迟（**不封顶**）。于是卡片是从组件栏右缘之外一张接一张飞进来（组件栏会裁剪自己的子树，卡片轮到之前不绘制），而不是整块一起到位。全程只有 `transform`，**没有任何透明度变化**；底部那颗**设置**齿轮是同一张网格里的一格，所以跟着一起联动。「整体缩放」是另一种取舍：整块滑入 + 缩放，所有卡片同时到位。
- **打开起始缩放**（50–100%，默认 88%）：展开起始的缩放——「整体缩放」下作用于整块，「逐个落入」下作用于**每张卡片**。100% 表示纯平移。
- **位移曲线**（默认根号）：位移的进度曲线——「整体缩放」下是整栏的水平滑入，「逐个落入」下是每张卡片各自的同一条滑入。
- **回弹幅度**（0–20%，默认 **4%**）：位移收尾时的**过冲**幅度——卡片（「整体缩放」下是整栏）会越过座位 `回弹幅度 × 行程`（默认 ≈ **18px**，20% 时是 91px）再弹回来。这个数字就是过冲本身，不是控制点：代码会反解位移曲线的纵坐标，让曲线峰值恰好超过 1 这么多——正因如此两种展开方式仍是**同一张映射**：整块那条 CSS 过渡与逐卡级联算的是**同一条 cubic-bezier**。它只作用于位移是有意为之：所有曲线的纵坐标都被夹在 `[0,1]` 内，因为缩放过 100% 会让整栏长出它所在的列；所以缩放是单调的，回弹交给位移。默认值取自物理而非目测——同一族弹簧（Android `DAMPING_RATIO_LOW_BOUNCY` = 0.75、motion.dev 默认 `bounce` = 0.25 ≈ ζ 0.75）一次干净过冲约是行程的 3–5%，ζ = 0.70 时峰值落在时长约 57% 处，与本实现的 ~57% 一致。收起是**同一条时间线倒放**，所以这段回弹会变成离场前的一次短促**蓄力**，而不是留在结尾。
- **缩放曲线**（默认根号）：缩放的进度曲线（「逐个落入」下同样作用于每张卡片，由级联延迟错开）。

每条曲线就是那条 `cubic-bezier()` 本身：一条线段、两个可拖圆点（也可以用方向键微调），另给 5 个预设（线性 / 标准 / 缓出 / **根号** / 平滑）。预设只是这四个数字的命名取值，所以「自定义」是**推导出来的**（没有任何预设相等时显示），不存在第二份状态可能与图不一致。曲线下方的小圆点会实时重放这条曲线：横向匀速（时间）、纵向按曲线（进度）。

**只显示完整组件**（默认开）解决的是另一件事：整行挡位滚动保证了顶部不被截断，但视口高度不是行高的整数倍，所以最底下那张卡本来会被切掉一半。开启后这张卡**既不绘制也不参与命中测试**（它的位置是空白带，悬停不该唤醒波峰）。做法是给那一个卡片槽加一个类隐藏它，而**不是**把可视区域整体裁掉——可视区底边只在最后一张完整卡片下方 `间距`（默认 24px）处，而卡片的投影要伸出卡片本体约 28px，一刀裁下去会切掉你看得见的那张卡的阴影；隐藏卡片本体则让这条带子保持完整只是不画东西，所以每张可见卡片的阴影都是完整的。跨过边缘那一刻是**淡出**（`opacity` 0.18s，`visibility` 的翻转延后到淡出结束），滚回来时立即显示并淡入——`visibility` 不能插值，所以离散的那一半靠 `transition-delay` 延后，而延时只写在「被裁掉」这条规则上（transition 读的是变化之后的样式），于是裁掉＝淡出、恢复＝立刻出现再淡入。

这条规则判定的是**组件栏真实的滚动可视盒**：判定与挡位终点都读「栏自身 `padding-top` 的实测值」，而不是写死的 4px（2026-10-14）。原因是右侧列现在有同居者——置顶摘要卡片会把组件往下推 `!important` 的 `padding-top`（实测 79–131px），而栏的 `clientHeight` 一点都不变：旧实现按 4px 算，可视盒的**底边就比真实底边低了整整一个「推动量」**，于是被底边切掉一角的卡片被判为完整、连同探出屏幕的那半截一起画了出来（车主截图里最底下那排）。同一处修正也进了滚动终点公式（`lastRow` 用同一个实测 inset），所以被判截断的那张卡**仍然滚得到**——两半必须一起改，只改判定会让它永远消失。推动量在原地动画（约 260ms 内逐帧写样式）期间也跟得上：栏上一个 `style` 属性的 MutationObserver 以 rAF 合并重测，值没变就 `Object.is` 直接跳过。

底部那颗**设置**齿轮同样遵守这条规则（被切一半就不画），它同时是一个**悬停才出现**的入口：平时不绘制，指针进入组件栏（或放大层）时淡入。它的格子仍然保留在布局里——否则每次悬停都会改变栏高与滚动终点，卡片会跟着跳一下——被门控的只有绘制。它**点开的是产品自己的设置窗口**，并直接停在 **设置 → 组件** 这一页（2026-10-14）：那是全应用唯一的组件配置入口，插件不再自造第二套界面。实现上它替用户点两下——侧栏底部的设置触发行，然后是我们的 `组件` 导航行（沿用 `settings-nav-glyph` 自己的 `_settingsArea` / `_navList` / `data-dsx-nav` 约定定位），窗口还没开时开窗后再等导航行出现。插件原有的那张自造面板（组件配置 / 组件市场 / 组件设置三个 tab、可拖宽）**保留在代码里但不再走这条路**，只在本机 `localStorage['dsx-addpanel-local'] = '1'` 时重新接管齿轮（本机备用，不随发布推送）。

**滚动终点**（2026-10-01）：滚轮停在一个「最下面的组件刚好完整显示」的挡位上，而不是「最后一行顶到栏顶」。因为整行挡位保证顶部完整、整卡规则保证底部完整之后，再往下滚只会把最后一行往上拉、在下面留出一整条空白；现在栏底与最后一个组件之间最多只有一个卡片间距，浏览器的滚动范围也同步收到这个终点，触控板惯性冲不过去。

放大波峰是连续的：指针扫过时下面几行会被推开，被推出视口的那张卡仍会被视口裁掉——跟随裁剪意味着动画中卡片会不断闪现消失，所以静态判定（静息座位）是刻意的，波峰本身不受影响。放大层的**左侧余量在收回过程中同样保留**（`overhang` 现在跟随整个 morph，而不是只在「已激活」时计算）：否则指针离开的第一帧余量归零，而卡片还有约 25% 的放大没退回去，最左那张会被 `overflow: hidden` 切掉一条——就是那个「高亮消失时左侧被截断」的现象。

### 波峰放大（动图）

指针沿组件栏滑过时，卡片按欧氏距离连续放大、下面几行被推开，离开时原路退回——不是两张图交叉淡入。
首屏那张静帧只是装好之后的静息状态；动效在这里：

<p align="center">
  <img src="docs/screenshots/rail-demo.gif" alt="指针划过组件栏时的连续放大波峰" width="320">
  <br>
  <sub>480×784 · 13 fps · 88 色 GIF（3.8 MB）；原速 mp4 见 <a href="docs/screenshots/rail-demo.mp4">rail-demo.mp4</a>。</sub>
</p>

### 内置部件

| 部件 | 说明 |
| --- | --- |
| 轮次·步数 | 会话轮次与步骤计数 |
| LLM 时长 / 工具调用 | 推理与调用的累计耗时 |
| 首 token 平均 | 平均 TTFT |
| 速率 | 解码吞吐（tok/s） |
| 缓存命中 | 输入缓存命中比例 |
| Tokens | 输入 / 输出 token 计数 |
| 会话概览（2×4） | 把上面那排统计拆出来的会话数字并成**一张** 2×4：默认 **轮次 / LLM / 工具 / 速率**，**最多 10 个**，在组件配置里勾选与排序（`metrics` 字段：名称在左 · 开关在右 · **直接拖整行**排序，没有把手；插入指示用的是产品自己那套蓝色箭头横线）。每行最多 5 个，超过折成两行（6→3+3、8→4+4、10→5+5）。可选 12 个数字（轮次、步数、LLM、工具、TTFT、速率、缓存、输入、输出、上下文、进行、待办）走的是与单卡**同一套格式化函数**，同一个数字不会因为看哪张卡而变义；一行挤到 5 个时带不下的单位会自动退回纯数字。两个数字之间的间距就是卡片自身的内边距，与「系统监控」的环图同一套规则。2×2 单卡的大数字是 20px，看板数字是 13px——看板用来扫一排，单卡用来看一个，两者并存 |
| 上下文水位 | 系统 / 工具 / 消息三段占比条 + 明细；支持 2×2 与 2×4 |
| 系统监控 | 本机硬件族：CPU/GPU 利用率数字、内存、显存、GPU 温度——2×2 卡片 + 2×4 环形看板，另有 GPU 利用率折线 |
| 一键压缩 | 上下文占用百分比 + 角落圆钮（双击执行压缩） |
| 任务 | 进行中 / 已完成 / 待办计数 |
| 用量热度图 | GitHub 式日历热力图，自记账每日用量；2×2 = 近 3 个月日历，2×4 = 近半年全部用量点 |
| 用量柱状图 | 最近 7 天的垂直柱状图；柱区高度与日历网格一致 |
| 对话轨迹 | 官方「轨迹」三色泳道的卡片版：输入 / 模型 / 工具 每触发一次画一根色条，随模型调用工具在最近 30 拍窗口里向右滚动。**横向几何与官方时间轴一致**：圆角 1px、间隔 `min(条宽×8%, 1px)`、不足 2px 按 2px 画，三条泳道始终都在（该泳道这一窗口内没有拍就是空轨道）；**纵向按卡片**：三条泳道彼此贴紧、整体撑满灰色子标题到卡片内边距之间的全部高度（官方在 1300px 宽条上是 8px 条高 / 14px 间距，照搬到 160px 卡片会空掉三分之二）。**泳道宽度**可在组件配置里切换，对应官方工具栏的「时长」开关：`按时长`（默认，条宽 = 该拍耗时占窗口总时长的比例，空档被压缩掉）或 `等宽`（官方默认投影，一拍一格、等宽，槽位在 30 拍时冻结、不再随窗口滚动重算） |
| 今日寄语 | 随机鼓励语录；文字/对齐/换行可自定义 |
| OpenCode Go 用量 | 用量对比（三窗口柱状图）/ 用量环图（三窗口环形图）/ 滚动用量 / 每周用量 / 每月用量 |
| Command Code 账户用量 | 账户 / 用量（`4.9M tokens` 大字 + 底部 请求 · 成功率 · 消费 三栏）/ 额度（`69.16 credits` 大字 + 官网那套 5 小时 · 周 · 月 三条分段横条）/ 窗口 / 套餐（等级徽标 + 账期行），以及 5h 窗口 / 周窗口 / 月窗口三张单窗口数字卡 |
| 额度管理 | 2×2 月窗口版式：月末用量预测 + 今日用量 / 今日推荐 |
| 峰谷定价 | 仅 2×2：当前是否处于 DeepSeek 高峰计费时段（周末与中国法定节假日全天低谷） |

### 组件预览即卡面

组件市场与「组件配置」的预览把卡片钉在一个 unit 正方形里（`pinBox`），所见即轨道上落座的卡面：内容过高的卡片由卡片自身的 `overflow: hidden` 裁掉，而不是把预览台撑成一个非正方形的圆角矩形（额度卡改造前实测 200×250）。**没有多余的角色词**：数字自带单位（`4.9M tokens`、`69.16 credits`）或自带名字（等级徽标）的卡片，灰字行留给多池时的当前视图。

### 套餐卡（等级徽标 + 账期）

大数字是官方套餐表的**等级徽标**（`individual-goat` → GOAT、`individual-pro-v1` → PRO …，与 provider 一样按最长前缀解析），不再打印原始 plan id（旧的 `individual-goa…` 直接顶出卡片边缘）；图例行是**账期**，徽标落座卡片底部左下角。组件市场的预览点击会走完整条等级阶梯（GOAT → PRO → MAX → ULTRA → PROVIDER → GO → TEAMS PRO），没有那个套餐也能先看效果。

### Command Code 账户池（点卡片切换账户）

Command Code provider 的「账户轮换」卡片可以把多个订阅挂在一个安装下（`COMMANDCODE_API_KEY`、`COMMANDCODE_API_KEY_2` … `_4`）。host 侧的同源路由 `/api/commandcode-usage` **把每一把已配置的 Key 都读一遍**（各自四个官方端点），连同每个 Key 自己 `/alpha/whoami` 读到的账户名一起下发：

- **点任意一张 Command Code 卡片即可循环**：`AllUser → Physicolor → Sparxie → AllUser`。选择按卡片实例持久化在 `cardConfigs.ccView`，刷新不丢；它与 OpenCode 池的 `poolView` 是两个字段，两族互不干扰；
- **`AllUser` 是整池合计**，逐字段相加：窗口百分比 = `Σused / Σcap`（不能取百分比均值——一个用尽的池 + 一个没动的池，那是「用掉一半额度」，不是「每个池各用一半」）；月窗口按各成员自身的月窗口相加，**每个成员用自己套餐的额度**计量，所以两个 GOAT 池就是 2 × $70；账户卡在合计视图里显示通用标签 `AllUser`，底下一行列出各账户名。`AllUser` 是刻意的中英同形表达；
- **图例行带出当前视图**（`账户 · AllUser`、`窗口 · Physicolor`），与卡片角色词并列；悬停提示给出完整循环链；
- **只配置一把 Key → 完全没有切换器**：卡片与改动前逐像素一致（图例行只剩角色词，账户名回到正文）。两个池解析出同一个账户名时，用 Key 的尾 4 位区分。

### 额度管理（Coding Plan 分组，2×2）

蓝色标题在顶，**大数字（月末用量预测百分比）在标题下方一行**，灰色子标题（`账期 10-10`）在大数字**右侧**、**底边与大数字对齐**，底部两个数字：**今日用量**（实测 token）与**今日推荐**（把剩余额度均摊到剩余天数得到的每日上限）。这套「大数字在标题下、灰字在右侧」的版式与「上下文已用」卡一致；标题与大数字之间是标准 4px 间距（150px 卡）。**头部不再显示池视图名**：点击卡片照旧在 `AllUser → 账户…` 之间切换（数字跟着变），只是不再有那一行标签。

- **口径（只算这个套餐自己）**：今日用量与今日推荐都按 Command Code 的 provider 路由（`commandcode`）折出来的每日日志计算（`/api/widgets-usage-daily?provider=commandcode`），与 credits、账期同源。同一台机器上并行跑着的其它 provider（例如 OpenCode Go）当天的 token **不计入**——否则会把别人的用量算进这份套餐的额度（实测 2026-09-20：全机器 783M 里 474M 属该路由）。带口径的地图拿不到时两个数字显示 `—`，不会退回混合口径。
- **预测口径**：`已用% + 近期速率 × 剩余天数`，近期速率取**最近 3 个日当量**（前 2 个完整天 + 今天按已过比例折算）。因此预测与今日推荐**数学同调**：预测 > 100% ⇔ 近期速率 > 今日推荐速率——卡片绝不会一边说「今天没超推荐」一边说「整月超 100%」。
- **超过 100%**：数字本身变红并做 1.6s 呼吸闪烁，而不是卡片四周泛起红色光晕。
- **额度 → token 换算**：余额（credits）按本账期已实现的「本地 token ÷ 消耗 credits」汇率折算成 token，两个数字同口径；不混入 provider 自己的 token 计数（它对同一账期的计量约高 1.7 倍）。**新加的池成员本账期一分未花时没有自己的汇率**，此时预算借用整池已实现的汇率（剩余额度 ÷ 剩余天数仍然成立），而速率侧保持它自己的 0——本地日志是整台机器的，不会算到没出过力的账号头上。
- **永远不打印「数据不足」，缺什么就填什么**：还没拿到载荷 → 大数字 `-%`，能读到账期就照常显示账期，两个数字走 `—`；账期太年轻推不出预测（今天刚开的池成员）→ 显示它真实的已用百分比（0.0%）、真实预算与 0 用量；从未用过的成员 → 除了预算全是 0。只有「账期已结束」这类过期载荷才什么都不显示，因为它不再描述任何可花的东西。

### GitHub

市场里新增一个 `GitHub` 分组，五张卡由**一条** host 同源路由撑起——`GET /api/github?user=<登录名>&repos=<owner/name,…>`，浏览器不直连 `api.github.com`。凭据按三级阶梯解析，这就是它在**任何人**机器上零配置可用的原因：`credentials.resolve('GITHUB_TOKEN' | 'GH_TOKEN')` → 本机 `gh auth token`（跑过一次 `gh auth login` 就有）→ 匿名。两片数据各自在 host 侧缓存（日历 30 分钟、仓库脉搏 15 分钟），因为匿名 GitHub 是**每个出口 IP 60 次/小时**，与机器上其他 GitHub 工具共享。

两个配置项都可以留空：`user` 留空 = 「本机登录的那个账号」（从 token 自己的 `GET /user` 解析），`repos` 留空 = 该 token 能看到的最远推送的四个仓库。

| 组件 | 说明 |
| --- | --- |
| 提交热度图（2×2 / 2×4） | GitHub 同款 5 级绿色阶，颜色从 `--dsw-alias-state-success-primary` 派生（不是硬编码 hex），跟随亮/暗主题；复用 Token 日历同一个渲染器（新增 `heatmapPalette: 'github'`）。**2×2 = 近 3 个月（13 个周列），2×4 = 近一年（53 个）**，即 GitHub 个人页表头那个窗口。有 token 时走官方 GraphQL `contributionsCollection`（一次调用拿到逐日精确条数），没有则抓公开贡献页解析——两条路逐日结果已实测一致（`docs/probe-github.mjs`，两种阶梯各跑一遍）。 |
| Stars（2×2） | 大数字是 stars，标题下是仓库名，副行是 forks |
| 问题（2×2） | 未关闭 Issue，**不含 PR**（仓库字段的 `open_issues_count` 两者都算），副行是其中没有任何人回复的数量——这一项需要 token，没有时如实说明，而不是打一个没人测过的 0 |
| 提交（2×2） | 距最近一次 push 多久（`12s / 5m / 3h / 6d`），副行是最新 Release tag |
| 仓库脉搏（2×4） | Stars / 未关闭 Issue / 未回复 / 最近提交 并排，标题行给出仓库全名与最新 Release |

监控多个仓库时，每张仓库卡都带同一个点击循环（`ghRepo`），点任意一张就切换整个家族——与用量 / Command Code 的池切换同一套行为。

### 组件市场

- 浏览全部组件（系统 + 外部）、搜索、尺寸切换预览、按 `组件@尺寸` 安装；
- 已安装列表支持拖动排序、配置编辑，以及 `2×2 ↔ 2×4` 一键切换（自动去重——同一组件同一尺寸只保留一个实例）；
- 组件配置 tab 支持卡片级自定义（今日寄语、热度图窗口对齐等）。

### OpenCode Go 用量

滚动 / 每周 / 每月三个用量窗口 + 百分比 + 重置时间。Host 半注册同源路由代理 `opencode.ai`；浏览器不发任何跨域请求，密钥走 DSH credentials。两种呈现：**用量对比**（三窗口柱状图）与**用量环图**（三窗口环形图——环中心百分比、悬停显示精确值、按同一紧急度配色）。

### DeepSeek 余额

一张 2×2 卡，通过 host 同源路由 `/api/deepseek-balance` 读 DeepSeek **官方开放平台**账户——密钥来自 `credentials.resolve('DEEPSEEK_API_KEY')`（环境变量 → `$DSH_HOME/.credentials.yaml` → `.env`），浏览器永远看不到它，也没有任何输入框要你填。

| 卡面 | 是什么 |
| --- | --- |
| `¥110` | 官方**总余额**（`GET https://api.deepseek.com/user/balance`） |
| `刚刚更新 · 赠送 ¥10` | 这次采样的年龄，后面是赠送余额（只有账户真的有赠送时才印） |
| 充值 / 今日消耗 / 对话消耗 | 充值余额、今天的消耗，以及本次对话花掉的部分 |

**「今日消耗」是量出来的，不是查出来的。** 官方接口只给余额，没有用量也没有历史，所以 host 的做法是**采样余额**（当日台账落在 `profiles/web/dsh-widgets-balance.json`），把观测到的下降累加起来：充值或赠送不算负消耗，跨天（或换币种）重新起算。因此它覆盖的是 dsh 真正运行的那些时段——这件事写在悬停提示里，而不是让这个数字假装成一张账单。**对话消耗**复用 `session-cost` 的同一套纯函数定价（命中的规则带峰谷、而会话记录没有逐请求时间戳时印 `≈`）；没有价格表时印本会话的 token 并带单位，而不是编一个费率。

颜色在这里表示**等级**，这也是它所属家族里唯一用色的卡：低于你自己设的阈值（每卡设置「余额预警阈值」，默认 10）大数字转琥珀色，低于阈值一半转红色；`is_available: false`（DeepSeek 已停止为该账户提供服务）为红色，原因在悬停里。「没有数字」的每一种情形各有各的说法——没配密钥、密钥被拒、host 没重启、超时——而不是笼统一句「暂不可用」。

### 峰谷定价（市场组件）

仅 2×2 的峰谷定价卡片，显示当前是否处于 DeepSeek 高峰计费时段。高峰时段（北京时间 UTC+8）为周一至周五 **09:00–12:00** 与 **14:00–18:00**——按[官方定价页](https://api-docs.deepseek.com/quick_start/pricing)的说明，其余时间一律为低峰，**中国法定节假日全天**（不是「工作日之外才便宜」）以及调休上班的周末同样按低谷计费。后半句不是细节：只看工作日时钟，2026 年会把 **19 天**当成高峰报价（元旦 1/1–1/2、春节 2/16–2/20 与 2/23、清明 4/6、劳动节 5/1 与 5/4–5/5、端午 6/19、中秋 9/25、国庆 10/1–10/2 与 10/5–10/7）。低峰显示 **CHEAP**；处于高峰窗口时显示 **EXPENSIVE**，数字本身变红并做 1.6s 呼吸闪烁（文字级告警，卡片边框保持干净），同时标题下方对应的时段行亮起品牌蓝并微微放大。整天低谷的日子，标题下的计量行直接写明原因（`中秋节 · 全天低谷` / `周末 · 全天低谷`），而不是点亮一个根本不生效的时段。以上全部可按卡片配置：高峰时段、周末/节假日开关、时区（`北京 UTC+8` 或本机时区），以及一个**额外低谷日**列表（用于国务院尚未公布下一年放假安排的年份）——这种年份卡片会退回「工作日 + 周末」判断，并把这件事写进悬停提示，而不是猜。法定假期区间放在 [`src/widgets/peak-pricing/holidays.ts`](src/widgets/peak-pricing/holidays.ts)，整套规则由 `docs/verify-peak-pricing.cjs` 复核（34 条断言）。

---

## 工作原理

- **部件单元 + 构建期发现（ARCH-001）**：每个部件都是 [`src/widgets/<id>/`](src/widgets/) 下的独立单元——`manifest.json`（机器可读的那一半：id / order / group / builtin / defaultInstalled / sizes / 该部件的 locale，另可选 `source` 与 `skeleton`）+ `index.ts`（`defineWidget` 描述符：**纯数据**的 `render()`、名称/描述 thunk、configSchema、example）。注册表是**生成**的，从不手工维护：[`scripts/gen-registry.mjs`](scripts/gen-registry.mjs) 扫描各单元目录、拒绝未知 manifest 键与 id 三处不一致，并产出 `src/client/generated.registry.ts`（`WIDGETS` / `WIDGET_RUNTIME` / `ALL_IDS` / `ALL_INSTANCES` / `STATS_WIDGET_IDS` / `DEFAULT_INSTALLED` / 合并后的 `WIDGET_LOCALES`）。新增一个部件 = 新增一个单元目录；`pnpm build` 会重新生成，注册表过期时 `pnpm check:registry` 会直接报错。部件模板放在 [`src/widgets-template/`](src/widgets-template/)——在扫描根之外，因此永远不会被发现或注册；
- **契约**：[`src/client/lib/contract/`](src/client/lib/contract/)——`types.ts`（单元 / 渲染层 / 外壳之间交换的全部形状，**零 import**：只想要类型的消费方不会把 i18n 或 React 拖进自己的模块图）+ `helpers.ts`（`defineWidget`、标签解析器、实例键、`sizesOf`）。其余与框架无关的工具并排放在 `lib/` 下（`format.ts`、`heatmap-accounting.ts`、`quota-math.ts`、`morph-spring.ts`）；某个数据源的载荷解析与该源的卡片渲染在 `src/client/families/<族>/{data,renders}.ts`，族与族之间互不引用；
- **按部件 i18n**：部件文案放在各单元的 `manifest.json`（同族共用文案放 `src/widgets/_shared/locales.json` 一次）；外壳字典（`src/client/i18n.ts`）只管外壳 UI。生成的注册表把一切合并，外壳在 apply() 时向官方 locale 服务注册；
- **组件栏由三个模块构成**：`rail/geometry.ts`（空间与预算解算，纯计算）、`rail/measure.ts`（锚点探针、宽度跟踪、ResizeObserver 扇出与让位节拍——直接写 DOM，因此不会比 React 的提交晚一帧）、`rail/rail-view.tsx`（`createRailView(deps)`：网格、放大波、加号面板与滑动抽屉）。`client/index.ts` 只做装配——bridge、四个槽位注册、页头胶囊与设置页（308 行）；
- **放大波**：曲线在 `lib/morph-spring.ts`（`WAVE_SPRING`），布局与比例场在 `rail/wave/wave-geometry.ts`，交互与逐帧写样式在 `rail/wave/RailWave.tsx`；
- **数据收集器**：挂载在 `conversation.composer.dock` slot，该 slot 仅在存在活跃会话时渲染——天然的「会话存活」信号；
- **Host 半**：[`src/host/`](src/host/)——一个上游渠道一个模块（`opencode` / `commandcode` / `usage-daily` / `github` / `deepseek` / `sysinfo`）外加状态文件，共用 `host/http.ts`（memo 与请求体读取）、`host/exec.ts`（唯一的子进程缝）与 `host/context.ts`（ctx 契约）。[`src/index.ts`](src/index.ts) 是 31 行，只组装 `HOST_ROUTES`。路由：`/api/opencode-usage`、`/api/opencode-usage-multi`、`/api/commandcode-usage`、`/api/widgets-usage-daily`、`/api/github`、`/api/deepseek-balance`、`/api/widgets-state`（组件栏配置的权威副本，持久化到 `profiles/web/dsh-widgets-state.json`——浏览器换 origin、无痕模式、清除站点数据都丢不了）与 `/api/sysinfo`；
- **可逆清理**：所有注册都由 fiber 的 effect 生命周期管理；卸载即完全恢复；
- **Slot 接入**：`conversation.input.overlay`（组件栏抽屉、放大浮层与设置抽屉——刻意放进对话子树，它会画在官方右栏面板**之下**，面板因此能吞掉组件栏）、`conversation.session.header.utilities`（「组件」胶囊，注册在 `order: 5`，避免与 `dsh-better-sidebar` 的底部面板开关撞号而换位）、`conversation.composer.dock`（数据收集器）、`settings.section`（设置页）；
- **空间契约**：组件栏从不索取固定宽度。它的预算是「官方对话列宽 − 官方正文 measure − 74px 盒内缩」，两个数都读产品自己发布的变量并带几何回退；在预算内它是一个流式网格（`repeat(auto-fill, minmax(基准边长, 1fr))` 的语义：列数按基准边长自动填充、上限为用户设置；卡片边长 = 该列数下的等分宽度，按 10px 档位量化，夹在自动下限与「5 行可见」上限之间），因此正文始终保住产品自己的 measure。空间连单列都放不下时才让位。实测见 [CHANGELOG.md](CHANGELOG.md)。

## 安装

```sh
# 通过 npm（插件市场）
dsh plugin --profile web add dsh-widgets

# 本地开发（link 方式）
dsh plugin --profile web add link:D:/dsh-home/plugins/dsh-widgets
```

安装后**硬刷新浏览器**（Ctrl+Shift+R），在会话页头部点击「组件」图标即可展开组件栏。OpenCode Go 部件需先在 Models 设置中配置 `OPENCODE_GO_API_KEY`。

## 开发

```sh
pnpm install
pnpm run build       # gen-registry + tsdown 打包 + 发布用 .d.ts
pnpm run check       # 注册表最新性守卫 + tsc --noEmit（0 错）
pnpm run check:types # 用一个真实消费方验证两个入口的类型能解析
pnpm check:registry  # 仅注册表发现守卫
node scripts/validate-widget-unit.mjs [dir]   # 部件单元契约校验器（Worker 自检 / 评审）
```

> **本仓库类型检查是干净的**，所以 `pnpm check` 是硬门；另有八个离线闸门守着每一次改动：G1 注册表过期、G2 单元契约与文案完整性、G3 类型检查、G4 每个部件 × 每个尺寸 × 每个预览状态的 **110 份渲染输出**（纯数据，不需要浏览器、不需要 React）、G5 编译后 CSS 的拼接、G6 纯搬移的增删行审计、G7 用桩驱动**构建产物**跑完 14 个 host 路由案例（不需要起服务）、G8 用一个真实消费方解析发布出去的声明。哪里实现什么，看 [`docs/architecture/CODE_MAP.md`](docs/architecture/CODE_MAP.md)；每一步用什么证明「没改行为」，看 [`docs/architecture/ARCHITECTURE_REFACTOR_REPORT.md`](docs/architecture/ARCHITECTURE_REFACTOR_REPORT.md)。

- `peerDependencies`：`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-ui-slots`（由 DSH web profile 提供；`@deepseek-ai/dsh-client-runtime` 已在 DSH 0.1.5 退役）；
- `cordis.patch.yml` 插入一行 `widgets`；host 半与浏览器半分别由 loader 与 client-modules 加载。

## 兼容性

- DeepSeek Harness `0.1.0-rc.6` 起的 `0.1.x`，以及 `0.2.0-rc.1` 起的 `0.2.x`——浏览器与桌面端都可用；
- 通过 `conversation.input.overlay` / `conversation.session.header.utilities` / `conversation.composer.dock` / `settings.section` 接入；
- 与 `dsh-better-sidebar` 的右栏显式协调：组件栏读取官方右栏列（旧版 better-sidebar 的 `--dsh-sidebar-width` 保留为回退），页头胶囊注册在 `order: 5`，因此 bundle 重载不会让两个开关互换位置；卸载后无残留。

## 发布

每个版本的逐条记录——连同每条改动背后的实测数据——都在 **[`CHANGELOG.md`](CHANGELOG.md)**（[中文](CHANGELOG.zh-CN.md)）；每个版本同时以 [GitHub Release](https://github.com/Physicolor/dsh-widgets/releases) 发布，锚定到实际发布它的那个 commit。原始证据（CDP 探针、截图、JSON 回执、逐事件的修复记录）在 [`docs/`](docs/) 下。本 README 只保留当前版本的速览。

### 本次更新 — v1.8.5

**又多了六张卡，而且每张卡背后的数据现在由自己声明**——注册表从 55 个单元长到 **61**：

- **时间族**——时钟（指针或数字、六个时区，秒针真的在跳）、日历（有 DSH 定时任务的日子打点；2×2 月历，2×4 多一列临近日程）、任务计时器（本回合已跑多久、此刻在等什么）、秒表（开始 / 暂停 / 复位）、倒计时（时长胶囊，或最近的定时任务）；
- **DeepSeek 余额**——官方账户余额，旁边是账本推导出的当日花费；它读自己那条 host 路由（`/api/deepseek-balance`），折叠逻辑由一个新增的离线闸门守着；
- **数据源声明式契约**——单元自己说清楚读什么（`reads` / `source` / 骨架屏），一个通用轮询器替所有人应答；声明与单元漂移时**构建直接失败**；
- **四个缺陷消失**——同住者把组件栏压下去之后，悬浮波峰不再在指针下面一整行处激活（45 项：25 FAIL → 45/45 PASS）；shell 换节点后会话头部锚点不再冻结；整卡判定与滚轮终点开始读同一个实测 inset——而且 `paneH` 终于被量到了（此前从未）；nvidia-smi 的 MiB 不再被当成字节透传；
- **「设置」齿轮改为打开 设置 → 组件**——应用自己那一页，而不是我们自造的第二套面板（那套面板保留为代码里的本机备用，靠 `localStorage['dsx-addpanel-local'] = '1'` 重新启用）；
- **两份有牙齿的卡片标准**——`docs/design/CARD-BODY-POSTURE.md` 与 `CARD-TITLE-STANDARD.md`，都是「过了当时所有闸门的事故」催生的，现在都由 `scripts/probe-geometry.cjs` 机器断言；
- **CI 与可在 CI 里跑的离线闸门**——`node scripts/gates.mjs`（G1–G12），以及 `.github/workflows/ci.yml`：每次 push 与 PR 都跑 `pnpm run build` + `pnpm test`。

以上每一条连同背后的实测数据，都写在 [CHANGELOG](CHANGELOG.zh-CN.md) 里。

## 路线图

组件系统现在是为规模而建的：每个部件都是 `src/widgets/` 下独立、契约驱动的单元，并在构建期被发现——新增一个部件就是新增一个单元目录，无需改动任何共享文件（指南：`src/widgets-template/README.md`）。

- **热度图 token 口径的后续工作**：卡片的总量已经与 dsh-usage-center 一致（host 路由复用它的 `getActivity()`）。要在**未安装** usage-center 时也做到精确，就得在本 host 里折叠同一批会话日志——今天刻意不重复实现，以保持口径只在一处维护；
- **Command Code 月窗口，待上游字段**：月份由本地推出 `used = 套餐额度 − 剩余月额度`（额度取自公开套餐表），因为 `billing/credits` 不返回月窗口对象。若上游之后在 `windowLimits` 里补上 `monthly`（含 used / cap / resetAt），`monthlyWindow()` 应直接读它，推导只作为回退；
- **Command Code 池靠命名约定发现**：host 读 `COMMANDCODE_API_KEY` 以及 provider「账户轮换」卡片写下的 `_2 … _4` 备用 Key。若某把 Key 存在无关的 ref 名下，需要把该 ref 加进 `COMMANDCODE_POOL_ENVS`（credentials 服务只暴露 `resolve(ref)`，没有「列出所有 ref」的口子，无法自动枚举）；
- **Agent 生产部件**：机器可读契约（`manifest.json` + `defineWidget` 描述符 + 模板 + 共享 API）正是一个 worker agent 端到端造出一个部件所需要的一切；v1.3.0 的并行创建测试证明两个 agent 可以同时添加部件、零文件冲突；
- **更多硬件指标**：通过可选的 LibreHardwareMonitor 桥拿 CPU 温度（外部依赖、需显式开启——刻意不打包）、NVIDIA 之外的 AMD/Intel GPU 支持、按网卡的流量；
- **热度图范围/周期控制**：让 2×4 热度图与柱状图能选当前半年 / 7 天默认值之外的自定义范围（周/月等）；
- **更多用量提供方**：Z.ai 等，复用 host 同源代理 + credentials 模式（DeepSeek 官方余额卡已在 v1.8.5 上线）；
- **峰谷定价的节假日表，每年一次**：法定假期区间放在 `src/widgets/peak-pricing/holidays.ts`，逐条抄自国务院办公厅的年度通知（2026 年 = 国办发明电〔2025〕7号）。下一年的安排通常在前一年 11 月公布——届时补上区间并扩展 `HOLIDAY_YEARS` 即可；在此之前卡片退回「工作日 + 周末」判断、在悬停提示里说明这一点，并可用卡片级的**额外低谷日**字段补齐。同一张卡上仍待做的：工作日集合自定义（时段与时区已经可改）；
- **实用工具部件**：一键压缩（需要 DSH 官方 compaction 能力）等；
- **外部集成**：飞书 / 微信推送与交互，密钥严格走 DSH credentials；
- **部件市场**：开放第三方部件注册机制，让社区部件像插件一样入驻——单元 + 发现架构（v1.3.0）就是载体；未来的 `widgets-market` bundle 可以同样地把单元放进 `src/widgets/`；
- **更多语言**：字典层每个 key 都已有 zh/en——加 `ja`/`ko` 等纯属字典扩展；
- **跨设备同步**（可选）：今天每台 DSH 服务各存一份 `dsh-widgets-state.json`——云/账号同步层可以让多台机器共享一份配置，但「本地优先、设备独立」是刻意保留的默认行为。

## License

[MIT](LICENSE)
