# 更新日志 本文件记录 dsh-stash 的版本变化。版本号遵循[语义化版本](https://semver.org/lang/zh-CN/), 结构参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 > **关于 0.5.1 之前的版本** > 本仓库的首个提交即 0.5.1,更早版本(0.1.0–0.4.0)的提交记录未随仓库保留。 > 下方「0.4.0 及更早」一节据 `README.md` 的踩坑记录与 `DESIGN-vault.md`(决策记录)整理, > **只保留有据可查的部分**;这部分没有日期,因为它们的具体发布日已不可考。 --- ## [0.11.2] — 2026-10-01 ### 修复 - **去掉 11 个文件里的 UTF-8 BOM。** Windows PowerShell 的 `Set-Content -Encoding utf8` 会给文件加 BOM, 而 DSH 的插件管理器用 `JSON.parse` 读 `package.json`——带 BOM 直接抛 `Unexpected token ''`, 于是**任何 `dsh plugin` 操作都失败**:装这个插件时报的是 `cannot resolve profile bundle "dsh-stash"`,看着像依赖没装,其实是文件头那三个字节。 影响 0.11.0 / 0.11.1 的已发布内容,涉及 `package.json`、`README.md`、`CHANGELOG.md`、 `cordis.patch.yml`、`sources.example.mjs`、`client/client.js`、`lib/command.js`、 `lib/library-view.js`、`lib/handlers/http.js`、`test/client-runtime.mjs`、`test/host-assembly.mjs`。 - **上传前自检加一条 BOM 检查**(`scripts/preflight-upload.mjs`,CI 也跑这一步): 带 BOM 的文件推不上去了。已用"人为加 BOM → 非零退出、撤掉 → 通过"双向验证。 ### 说明 - 代码逻辑一字未改,606 项断言不变。 --- ## [0.11.1] — 2026-09-30 ### 变更 - **README 重写成作者本人的声部。** 525 行 / 17,459 字符 → 225 行 / 7,044 字符(字符降 60%)。 开篇用故事入场,第一节先立常解再推翻("再包一层,是不是重复建设?"),小节标题统一为 「如何安装 / 如何登记 / 如何迁移 / 工作面板 / 框架目录 / 开发测试」。 - **面板的逐条说明搬进新增的 [`DESIGN-panel.md`](DESIGN-panel.md)。** 含"排版与字号"一节: `TYPE` 常量、输入框与按钮的 `!important` 钉死、以及**对象里重复键会覆盖同名样式**这个坑 (曾把 14px 覆盖成 13px,表现是标签折行、方框起点参差)。 - **用词定案:** 首页数「数据源」(三类合计,读作"N 个数据源"),类层列「这一类里的资源」; 界面上不再出现"资源总数",同一个词只在一个地方出现。 - 安装规格串改指 `#v0.11.1`。 - **tag 规则:** 已发布的 tag 不再移动。本版起,每次改动走新 tag。 ### 修复(文档) - 0.11.0 一节里 6 处与最终代码不符的表述改正:两级下拉(实为单页三级下拉)、 「全部账号 →」入口(已删)、资源卡详情按钮(实为整行点开)、面板只读(可写了)、 522 项断言(实为 606)。 - 测试与注释里出现的真实上游(SEC、the-odds-api)换成中性例子;文档示例名一律中性。 ### 说明 - **本版只改文档**:代码与 0.11.0 逐字相同,606 项断言未变。 --- ## [0.11.0] — 2026-09-30 ### 新增 - **面板重做成「数字优先」的首页 + 三类下钻。** 面板名就是插件名:`设置 → stash`。 - **首页**:`52px` 的大数字与就绪/有阻塞(有阻塞时那个数字**可点**,直达筛选),标签写「个数据源」; 三类各 `34px`(**0 条的那一类也显示**,空本身是信息);每格一行小字(账号数与未配置钥匙数 / 依赖本机 / 路径数);台账行(取数 / 经验 / 失败未记经验);迁移仍是折叠块。 - **用词分两层**:首页数的是**数据源**(三类合计),类层列的是**这一类里的资源** ——同一屏两个"资源"曾经让人问"资源到底指什么",所以首页那个块改叫「数据源」。 - **三类是唯一一级分类**:`远端接口` / `本机服务` / `本机文件`。技术细节只在登记时出现。 - **归类自动派生**:`files` 与 SQLite → 本机文件;MCP 走 stdio、以及回环地址(`127.0.0.1` / `localhost` / `::1`)→ 本机服务;其余 → 远端接口。新注册表字段 `bucket` 可覆盖它—— 本地反向代理把远端伪装成 `127.0.0.1` 是真实存在的反例。 - **面板能新建 / 编辑 / 删除资源了。** 在这之前只能由模型工具或手写文件登记,删更是哪都没有入口。 - 新增写端点 `POST /stash/sources`(新建 / 覆盖)与 `DELETE /stash/sources?id=`(删除, 顺带把它从账号的 `usedBy` 里摘掉)。两条都**只对代写条目生效**,手写 `sources.mjs` 永远拒绝。 - **写端点直接调用 `stash_source_add` 本体**:校验、密钥特征扫描、写入是一套代码, 不会出现"面板放行、模型侧失败"。13 项断言钉住这条约束。 - 「+ 新建条目」进的是**一个页面 + 三级下拉**:**新建什么**(就是首页那三类:远端接口 / 本机服务 / 本机文件)→ **形态**(按类自动收窄,如「本机文件」只列文档与数据文件、本地数据库) → 字段。必填只 4 项,其余折进「高级设置」;报错落到字段上(顶部汇总 + 就地标红); 表单里**永远没有"值"的输入框**,只登记引用名。 - **账号不是资源的一类**,所以不占「新建什么」的位置:它是页面底部的次要入口 ("要新建的不是数据源,而是账号?新建账号 →")。 - **字段按类给,不多问**:本机文件那一类(文档与数据文件 / 本地数据库)不上网取数、也不需要凭据, 所以**不问「使用边界」与「钥匙引用名」**,提交时也不带 `access`;host 侧同步不再把它们 计入"未声明取数边界"。 - 字段补全:对象存储补**协议下拉**(此前 WebDAV 无法从面板登记);HTTP 高级补 **查询参数 / 取哪一段 / 请求体**;通用高级补**覆盖范围 / 注意事项**。内置处理器通用化: 不带任何本机专有名字,只填 `handler id`。 - **类层:账号在前,资源在后。** 钥匙**跟着它服务的资源走**——账号块只列这一类资源用到的账号 (按引用名交叉认领),带自己的状态下拉与展开/收起。两个块标题分别是「账号」与「资源」, 各带自己的数字与下拉(`账号 2 个 · 3 把钥匙 · 1 把未配置` / `资源 6 条 · 6 就绪`)。 整行点开就是详情(没有单独的「详情」按钮),里面有「编辑」「删除」(仅代写条目) 与"程序不改写它"的说明(手写条目)。 - **失败却没记经验的库在卡上点出来。** `stash_doctor` 早就有这条判定,现在把它带到面板上。 - **上传前自检 `scripts/preflight-upload.mjs`**(也进 CI):扫全仓,命中就非零退出——不该进仓库的 数据文件(`sources*.mjs|json`、`ledger.ndjson`、`lessons.json`、`corpus/`、`.credentials*`、`.env`)、 本机绝对路径、疑似密钥值、私钥正文。`.gitignore` 同步补齐这几类运行时数据。 ### 变更 - 面板与面包屑根统一叫 **stash**;`外部资源` 这个面板名不再使用(它仍是对这一类东西的普通说法)。 - 资源层从"按大类 + 形态分组"改为**三挡状态筛选 + 搜索**,形态写在卡片标签上。 ### 新增(分类与 handler) - **资源分类成文:先把"能登记什么"定下来。** 面板里「钥匙」与「文库」两个名字曾经撞车 (`sources.mjs` 自称"文库注册表",而"文库"又被当成层名),也没有任何一处能回答 "stash 一共能登记多少类资源"。新增 [`DESIGN-taxonomy.md`](DESIGN-taxonomy.md): - 穷尽性不靠行业举例,靠**通道枚举**:程序触达外部世界只有四条物理通道 (网络套接字 / 文件系统 / 子进程 / 人的输入),其下展开 A–H 八种形态; - **「能不能登记」与「能不能经 stash 取数」是两个属性**——MCP 服务正是靠这条被重新纳入; - 附两条前提、一个可被反驳的检验法("它走哪条通道")、以及 D 跨承载的例外说明; - 术语表:**"文库"不再作为层名或独立名词**,它等于"库/资源"。 - 第四节末给出**四条通道 ↔ 面板三类**的映射与 `bucket` 覆盖字段。 - **B 类形态:本地数据库(`handler: "db"`)。** 零依赖实现,走 Node 内置 `node:sqlite`: - 一条源 = **一条只读查询**(不是"任意 SQL"),参数用 `:name` 占位,`limit` 限行数; - **只读打开**(`readOnly: true`,写操作在引擎层就失败)+ **只放行单条 SELECT/WITH** (登记与取数两处都拦)+ 单语句(挡 `SELECT 1; DROP TABLE x`); - 动作 `query`(注册表里那条)与 `tables`(列表与视图); - 服务端数据库**不内置驱动**(零依赖的代价),那类走 A 类的只读 HTTP 门面—— 这条边界写进了错误提示与文档,而不是等人踩。 - **C 类形态:对象存储 / 文件传输(`handler: "objstore"`)。** 两种协议、零依赖: - `s3`:S3 兼容(AWS / MinIO / OSS 的 S3 端点),路径式寻址;私有桶用 **SigV4 签名**, 公开桶不声明凭据即匿名读取; - `webdav`:PROPFIND 列表 + GET 取文件,可选 Basic 认证; - 动作 `list` / `get`;`get` 只回**文本类**且小于 256 KB 的正文,二进制只回元信息; - 回报的 URL 会抹掉签名类查询参数。 - **`lib/handlers/sigv4.js`:自己写的 SigV4 签名器,并用 AWS 官方测试向量核对。** 没有引依赖,所以拿 `aws-sig-v4-test-suite` 的 `get-vanilla` 逐字段比对—— canonical request / string to sign / signature 三项**逐字节相同**才算过 (见 `test/handlers-bc.mjs`)。没验证过的加密代码不进这个包。 - **D 类形态:MCP 服务(`kind: "mcp"`)——只登记,不经 stash 取数。** - 字段:`server`(DSH profile 里那条 MCP 行的 serverName)、`transport` (`stdio` / `streamable-http` / `sse`)、`tools`(工具前缀)、外加常规的 `credentials` / `access` / `notes` / `boundary`; - `stash_fetch` 对它**明确拒绝并指路** `mcp____`——拒绝本身是边界表达; - 顺手点出那个坑:MCP 行读 `process.env`,值必须写 `$DSH_HOME/.env`, 粘在「设置 → 钥匙」面板对该消费者无效; - **明写缺口**:MCP 调用不经过 stash,所以**没有 `ledgerId` / `contentHash`**, 溯源只能看会话日志。卡片与 catalog 都写着这句。 - **面板改成"一页总览 + 两个下钻",并按分类学统计。** - 面板名从「钥匙」改为「**外部资源**」;落地页四段:① 资源 ② 钥匙 ③ 记录 ④ 迁移。 **资源排在第一位**——"我登记了哪些库、哪条是坏的"才是打开这个面板的第一个问题; - 一级按**大类**(网络套接字 / 文件系统 / 子进程),二级按**形态**(声明式 HTTP、 本地语料、本地数据库、对象存储、MCP 服务…),点任一行带筛选进资源层; - 「钥匙」降为总览里的一段,原来那三层一行未改; - 「记录」段给出取数台账(条数 / 失败 / 最近)与经验库(总数 / 覆盖库数); - 资源层:卡片按大类分组,筛选器改按**形态**列(不再按 kind——那会把"本地数据库" 和"本地语料"混成一类),MCP 卡片多一行"引用型"说明; - host 侧新增 `lib/library-view.js`:就绪 / 阻塞的判定口径与 `stash_catalog` 共用一处, 形态与大类也派生在一处;`GET /stash/credentials` 的 `libraries` 升级为富形状并新增 `records` 汇总,组装失败时降级回 `{id,name}`,**不把「钥匙」那三层一起带走**。 - **模型侧快照改为按形态汇总。** 原来罗列前 12 个库 id + "等 N 条"(`prompt.js`), 现在只说形态与条数,例如"远程接口 12(声明式 HTTP 12)· 本地语料 1 · MCP 服务 2", 再点出只能人工导出的条数与台账数字。库 id 是每台机器自己的数据,列进提示只会过期与占 token ——要看具体有哪些库,`stash_catalog` 现取。 ### 修复 - **筛选到 0 条时,资源块的表头与下拉不再消失。** 原来表头写在"有命中"的分支里, 选完一个状态(如「有阻塞」而该类全部就绪)整个块连同下拉一起消失——要改回来只能退出重进。 现在表头先渲染,空态写成"没有命中当前筛选的资源。把上面的下拉改回「全部」,或清空搜索框。" - **字号收进一处。** 面板里原有 33 处写死的字号,且按钮从未钉过字号(`取消` / `概览` / `刷新` 比 `账号` / `资源` 大一号)。现在定成 `TYPE` 常量:标签 14 → 值 13 → 占位与说明 11 → 元信息 12(首页展示级 52 / 34 / 28 / 20 也登记在册),输入框与按钮用 `!important` 钉死。 - **表单纯净排版。** 标签列固定 `104px` 且右对齐,所有控件占满控件列——方框左右各成一条线; 方框下的注释一律与方框同一左边缘(此前「形态」那条在控件列里、别条挂在行外,起点不一)。 曾有一个重复的 `rowLabel` 键覆盖了新样式(同名键取后者),这就是标签折行、 方框起点参差的原因——已删,并加断言防复发。 - **删掉三处名不副实或重复的入口**:首页「全部资源 →」(写着全部、跳的却是"远端接口"一类)、 账号块「全部账号 →」(下拉里已有「全部」,账号块自身可展开)、新建页的中间菜单。 - **术语清理**:界面文案里指"数据源"的「库」改为「数据源」;`本地语料` → `文档与数据文件` (语料是行话);提示里的 `stash_fetch` 改为「取数工具」;`ID` 大写。 - 删掉已成死代码的 `CREATE_OBJECTS`(上一版"资源/账号"对象轴的残留,里面还挂着旧词)。 ### 测试 - 新增 `test/handlers-bc.mjs`(42 项):SigV4 官方向量、SQLite 只读与 SQL 守卫、 S3 / WebDAV 打本机 mock 服务器(验的是发出去什么:签名头、Basic 头、列表解析、二进制不回正文)。 - `sources.example.mjs` 补齐三种新形态的示例(MCP / SQLite / 对象存储),照抄即可。 - 全套 **606 项断言**:host 223 · client 254 · portability 87 · handlers-bc 42。 CI 增加第 4 个测试文件;Node 22.5–23.3 的 `node:sqlite` 需 `--experimental-sqlite`, 那几条真库断言会跳过并说明,其余照跑。 ### 说明 - 版本仍是 0.11.0(写这一节时尚未发布,所以以上都并进同一个版本,没有另开 0.12)。 - 面板可写了:新建 / 编辑 / 删除走 `POST|DELETE /stash/sources`(只对代写条目生效); 手写 `sources.mjs` 里的条目仍然只读,程序不改写它。 --- ## [0.10.0] — 2026-09-25 ### 新增 - **导出 / 导入 / 清空:让积累跟着人走。** 个人常常同时用几台机器(家里的、工位的、新买的), 而 README 原来的「换一台机器」只是一张手工拷文件的表——它只回答了「换」(旧机退役、单向一次), 没回答「并」(多机并存、双向持续)。多台机器都在长的时候,导出再导入会覆盖或丢。 - `/stash export [--out <目录>] [--with-values] [--corpus]`:导出一个**目录包**(不是压缩包—— 零依赖且可 diff)。默认**不带值**;带值则为明文。另产出 `credentials-guide.md`, 记下每把钥匙**从哪来、该放到哪**,换机器照办即可,不必凭记忆重找来源。 `--corpus` 时语料一并进校验清单(否则包被动过时只有载荷看得见,语料是一道暗门); `.last-export.json` 保留**最近 5 次**导出的校验码——人常常导出好几遍,只留最后一次会让早先那个包对不上。 - `/stash import <目录> [--dry-run]`:**值覆盖、积累并集**。库条目与账号按 id 并集, 经验按(来源库 + 标题)去重,台账只拼接,语料文件级合并,`sources.mjs` 永不改写。 冲突一律报出且**保留本机**,不自动决定。值的写入按每个字段自己的 `inject` 落点分流: `env:NAME` → `$DSH_HOME/.env`(**需重启**),其余 → 凭据服务。 - `/stash wipe [--corpus] [--confirm <校验码>]`:清空本机 stash 痕迹,并按名删除 stash 认得的凭据值。 **顺序铁律**:先导出 → 在另一台机器导入成功 → 再回来清空,确认码即导入方回显的那一枚。 `corpus/` 默认不删;清单外的引用名一律不碰;只清数据,不卸插件。 清 `.env` 时按两类键找:声明了 `env:` 落点的,以及**名字与引用名同名**的 (用户可能把值放进了 `.env` 却没在台账里记 `inject`——只按 `inject` 找会漏掉它)。 三件事都走 `/stash` 人类命令,**不做成 Model Tool**——它们要搬运凭据的值, 而「值不进 tool 参数、不进上下文」是整套设计的前提。 决策记录见 `DESIGN-portability.md`。 - **面板写值之后自动对齐落点:一处录入,两处一致。** 宿主的凭据有两个存储、两套消费协议—— `.credentials.yaml`(只有读 seam 的看得到,热重载)与 `.env`(seam 同 `process.env` 消费者都看得到,要重启)。 面板写值走 `credentials.set()`,只落前者,于是"读 `process.env` 的组件"(harness 的 MCP 行之类) 永远拿不到,用户只能手工去编辑 `.env`——这条手工步骤现在由插件接上: - 写完值 / 移除值之后,客户端**只把引用名**通知宿主(新增 `POST /stash/credentials?mirror=sync|clear`)。 值由宿主从凭据服务自己读,**不经过这条通知**,测试里专门断言了请求体里不含值。 - 新增 `lib/credential-store.js`:`inject` 是 `env:NAME` 就写进 `.env` 并回一句**需重启**; 其余落点不写 `.env`,但会清掉**同名的旧键**——`.env` 物化后排在 `.credentials.yaml` 之前,不清就是 "面板改了却不生效"。 - 移除值、`/stash wipe` 一律**两个存储一起清**,不留幽灵值。 - `.env` 的读写从 `portability.js` 搬进 `credential-store.js`:那里现在是"值该放哪"的唯一权威, 导入与清空共用同一套规则,不再各写一份。 - **`stash_catalog` 现在一次说清「能不能用」**,不用先试错: - **必填参数**(来自 `request.required`)——调用前就知道要带什么,不必发出去等上游 400; - **`ready` / `blockers`**——现在就绪,还是被哪一条卡住(越界、handler 未实现、凭据未配置、路径不存在); - **按库的台账汇总**——调过几次、失败几次、最近一次什么时候、最近一次错在哪。 为此在 `lib/ledger.js` 增加 `ledgerStatsBySource()`(原先只有全局统计,答不了"这条库以前通不通")。 渲染时同一件事只说一遍:越界的后果交给 ⛔ 那行,边界行只报模式。 - **按需递线索:挂在 `agent/pre-step` 上,只在相关时才注入。** 位置本身就是设计。原先写在 `systemPrompt.section` 里,但 `AssembleContext` 只有 `scope` / `signal`—— **看不到用户这一轮说了什么**,只能每步常驻报状态,一直占 token。 `agent/pre-step` 是每步之前的 waterfall,payload 里带着 `messages`,返回值可以往这一步追加一条消息; **harness 自己注入 `AGENTS.md`(`packages/context/agent-instructions`)用的就是这条接缝。** 三条判据,从严到宽: - 这句话点到某条已登记的库(id 或中文名)→ 递上那几条的要点:取数边界、凭据引用名与落点、能不能直接取; - 这句话有取数意图(外部数据 / 接口 / 凭据 / 数据库 / 语料…)→ 递上全景:登记了几条、哪几条只能人工导出; - 都不是 → **一个 token 都不加**。 实现要点:同一句用户消息只递一次(一回合多步不重复);快照异步刷新,读取不在步进路径上 `await`; 读不到清单时**干脆不注入**;注入失败一律退回默认决定,绝不挡住这一步。 注入的消息自带 `source.kind = 'dsh-stash'`,下次读 claims 时会跳过它,不会拿自己注入的话当用户意图。 - **「设置 → 钥匙」这一页重做视觉。** 原先是写死的灰度与状态色、层层嵌套描边、按钮不分主次, 且没有任何交互反馈。现在: - **配色全部走宿主主题令牌**(`Theme.listTokens` 的 14 个 `--dsw-alias-*`,均自适应亮/暗)。 原来那套「同一个灰度套不同透明度」在亮/暗里必有一边发虚。 - **交互态写进一张作用域样式表**(`.dshs-root` 之下,只注入一次、幂等)。 内联 `style` 表达不了 `:hover` / `:focus-visible` / `transition`——这正是旧版点了没反应的原因。 - **层级改用背景与间距,不靠层层描边**:账号卡用 `bg-layer-2`,字段之间一条细分隔线; 统计条改成「大数字 + 小标签」,去掉外框。 - **状态用色点 + 文字**,不再「彩色胶囊 + 重复文字」说两遍。 - **按钮分主次**:保存/创建 = `dshs-primary`(品牌填充),移除值/删除条目 = `dshs-danger`, 展开收起/取消/编辑信息 = `dshs-ghost`。 - 宽度交给设置面板的内容列,不再写死 `860px`。 - **可选参数有了显式写法 `{?参数名}`。** 只允许出现在 `request.query` 的值里:参数缺失(或空串)时 **整个查询键被丢弃**,参数存在时与 `{参数名}` 完全相同。此前没有表达「可选过滤器」的办法—— `'eq.{category}'` 缺参数时代入成 `eq.`,看着合法、实际必被上游拒(本机实测:那一串换来一次 HTTP 400), 而 `required` 只能把参数变成必填。写在 `url` / `headers` / `body` 里会在登记时就被拒—— 那几处没有「整键丢弃」的语义,与其等取数时才怪,不如当场拦下。 ### 修复 - **取数失败的输出被宿主整份拒收,失败原因全部丢失。** `stash_fetch` 失败时 `summaryText` 被写成 `undefined`,而 `undefined` 不是 lossless JSON, 宿主在输出边界拒收**整份结果**,调用方只拿到一句 `value is not lossless JSON`—— `kind` / `error` / `hint` / `body` / `ledgerId` 一个都到不了。 影响面是**所有** handler 返回 `ok:false` 的调用(HTTP 失败、凭据缺失、必填参数缺失……), 而工具描述里写着「结果里的 ok=false 表示失败,error 与 hint 会说明原因」——这句话此前是空头支票。 (实测:本机四次调用全部命中——1 次凭据缺失、3 次 HTTP 400。台账里只有指纹,看得见失败、看不见原因。) 改成 `null`。 - **`/stash import` 与面板贴值的行为不一致:`env:` 落点的值只写 `.env`,不写凭据服务。** 后果很具体——导入后那几把钥匙**要重启才生效**,重启前面板显示"未配置",而值其实已经躺在 `.env` 里了,**看起来像导入失败**。面板贴值的路径是两处都写(浏览器写凭据服务 + 端点镜像 `.env`), 导入没有浏览器那一跳,于是漏了前半截。 现在抽出 `installCredentialValue()`(`lib/credential-store.js`)当唯一入口:**先写凭据服务, 再按 `inject` 落点镜像 `.env`**,两条路落到同样的状态。 (这条是逐项核对"导入/导出/清空覆盖了哪些数据"时发现的,不是猜的。) - **主按钮白底白字,标签整个看不见**(实机截图发现,用户报的"按键是空白")。 样式表把主按钮写成 `background: var(--dsw-alias-brand-primary); color:#fff` —— **那个令牌一旦解析不出来,`background` 就失效回到初始值(透明),而文字还是写死的白**, 结果是一块白板:按钮在、边框在、字没有。全仓 5 颗主按钮都中招: 导出 / 导入 / 保存 / 创建 / 保存修改。 同一类问题还有一处:概览页**进度条的填充** `background: C.brand`,令牌失败时看着像一条空槽。 两处一起修: - **每个令牌都加系统色兜底**(`Canvas` / `CanvasText` / `GrayText` / `AccentColor` / `Red` …), 令牌解析不出来最多回退到系统色,不会再出现"东西在、颜色没有"; - **主按钮不再把可见性押在品牌令牌上**:底色用 `bg-layer-1`、文字用 `label-primary` (这两个在实机上已确认可见),品牌色只当边框与加粗的强调。 记一条教训:**凡是"令牌色做背景 + 前景写死"的组合,都是一颗会在特定主题下静默消失的按钮。** 测试现在会拦住它——源码里不许出现 `color:#fff`,不许有绕过 `C` 对象的裸令牌写法。 (同时修掉自查时写错的正则:`[a-z-]+` 不含数字,漏算了 `border-l1` / `bg-layer-2` 这类名字。) ### 变更 - **失败渲染带出上游回包正文。** `handlers/http.js` 早已把响应体截断到 400 字符存进失败对象, 但渲染层从不打印它。现在失败时多一行「上游正文:」——`{"code":"42703","message":"column cases.slug does not exist"}` 这种根因才说得出口。`hint` 仍保留,两者不互斥。 - **新增迁移端点**(`lib/portability-route.js`),给面板用:`GET /stash/portability` 只报状态 (默认导出目录、有没有导出记录、最近几次导出),`POST` 按 `?action=export|import|plan-wipe|wipe` 分发。与 `/stash` 命令调**同一批** portability 函数,区别只是入口——命令给人用,这条给面板用。 两条安全约束写在端点里: - **值永远不经过浏览器。** 客户端只送动作、目录、开关、校验码;导出带值时值由宿主从凭据服务 读出后直接落盘到 `values.json`。 - **校验码只收不回。** 执行清空时才收,`GET` 与 `plan-wipe` 的返回里都刻意省略—— 否则界面可以直接把码填进输入框,清空的门槛形同虚设。 - **收紧按需线索的触发词:去掉裸名词。** 原 `INTENT` 含 `接口` / `API` / `凭据` / `密钥` / `token` / `数据库` / `语料` / `台账`,这些在日常对话里太常见。**实测误报**:讨论界面分类时写了 一句"其中:网站账号 X、API 密钥 Y",就命中通用分支,把一段资源线索塞进了上下文。 现在只认**带动作的短语**(`外部数据` / `取数` / `查一下…数据` 等),兜底仍靠更准的那条路—— **点名提到某条已登记的库**。这是「宁可漏,不可处处命中」:每命中一次都要花 token,误命中等于常驻噪音。 - **「钥匙」页从单页改成三层。** 原先是一页长滚动:统计条 + 表单 + 按分类分箱的账号卡(卡内展开、卡内贴值)。 账号一多就得一路翻,而最该一眼看到的"我总共多少、配得怎么样"反而埋在中间。现在: - **L1 概览 = 一张可点的统计索引。** 顶上是**总数**(引用名把数,28px),下面**其中**按账号类别逐行, 再下面**注意**只有"未完成配置 N 把"一行——**每一个数字都能点,点了带对应的筛选进 L2**。 「注意」块**只在真有未配置时出现**:大多数时候钥匙是配好的,常态不该在页面上留一行噪音。 - 口径统一用**「把」(引用名)**计数,账号数括注在括号里,所以分类各行相加 = 总数,未配置也落在同一口径。 - **L2 清单**保留按分类分箱,加四挡筛选器(全部 / 未配置 / 已配置 / 未登记)。筛选**作用于字段**: 一个字段都不命中的账号卡整张不显示,命中的自动展开。卡片是**摘要 + 缺什么**, 未配置的字段直接给 `[贴值]` 就地录入——否则补 5 把钥匙要点 15 次。 - **L3 账号详情**:字段列表(已配置的给「更换值」,未配置的直接给输入框)、移除值二次确认、 编辑信息(表单态与「新建」复用同一张表)、删除条目。手写来源整页只读。 - **面包屑**贯通三层。⚠️ 面板插槽不提供路由,所以三层是客户端内部状态——**不能深链、浏览器返回键无效**, 面包屑是唯一的回退路径。 - **迁移区块进了 L1**(默认折叠)。导出/导入/清空本来是"整库操作",原先只以 `/stash` 命令存在, 现在面板里也有,与命令调**同一批**函数。折叠是因为它低频且含不可逆动作。 清空走三步:看删除清单 → 手输校验码 → 确认。结果就地出**结果卡**。 **没有导出记录时,清空按钮照常渲染但禁用**,旁边写明"先做一次导出,并在另一台机器导入成功"—— 早先是整节换成一行文字、按钮根本不出现,页面是干净了,但人会以为这功能没做。 - **`stash_catalog` 现在带出每个凭据的 `inject` 落点。** 值依然绝不出现,但「值该放哪」必须出现—— 它决定这把钥匙写 `$DSH_HOME/.env` 还是写面板,正是「面板一片绿、功能却是断的」那类故障的分界线。 `env:` 落点会当场点明「要写 `.env`、改完要重启、面板对它无效」。导入也靠它决定值的去向, 所以这条信息从「说明」升格为**迁移必需字段**。 - **`lib/http.js` 的 400 提示改成通用文案。** 原文写死成「该接口的 product 必填」——那是给某个 贸易接口写的,对别的库是反向误导(真实原因可能是字段名不存在)。现在指向结果的 `body`: 「上游的报错正文在结果的 body 里,比这句话精确」。401/403、429 的分支不变,并补上 422。 ### 测试 - host 128 → **171 项断言**(+8 输出边界、+5 可选占位符 `{?name}`、+15 凭据落点镜像、+15 按需线索,另有若干改写)。全部离线、不联网: - 复刻宿主的 lossless JSON 规则(`undefined` / 非有限数 / 类实例都会让整份输出被拒收),并自检该规则本身可靠; - 复刻失败路径:登记一条声明必填参数的库、调用时不传,断言失败结果过边界、`summaryText` 为 `null`、`ledgerId` 不丢; - 断言失败渲染带出上游正文,且没有正文时不多出一行。 - 可选占位符 `{?name}`:缺参数 / 空串时整键丢弃,给参数时正常代入——用本机回环上的临时服务, 直接断言**真正发出去的查询串**;写在 `query` 之外时登记即拒收。 - 按需线索:注册在 `agent/pre-step` 上;从 claims 里只认用户真正说的话(跳过 `AGENTS.md` 与本插件自己注入的); 命中的库 id / 中文名;无意图时返回 `null` 不注入;点到越界的库时点明取不了; **快照没就绪时绝不注入**;监听器层断言不相关的一轮不追加、相关的一轮追加一条、 同一条用户消息不重复注入、`reject` 的决定原样返回。 - 凭据落点镜像:`envTargetOf` 只认合法 `env:NAME`;`env:` 落点写进 `.env` 且提示需重启; 非 `env:` 落点不写 `.env` 但清掉同名旧键;落点名变了写新名清旧名;清除时两个名字都清; **全程不动 `.env` 里无关的键与注释**。客户端侧断言:写完值 / 移除值都通知了镜像端点, 且**请求体里只有引用名、没有值**。 - 视觉体系:渲染时**注入一次**作用域样式表(幂等);样式表与组件都走主题令牌; 交互态(hover / focus)在样式表里;**源码里不再有硬编码灰**;危险操作有 `dshs-danger`、 主次操作分开。为此给测试的假 `document` 补了可写的 `head`,用来验证样式表真的被注入。 - 新增 `test/portability.mjs`:**77 项断言**,同样跑在临时 `DSH_HOME` 下。锁的是语义而非实现: 值覆盖 / 经验并集 / 台账只拼接;去重键不能用含时间戳的 id;三类冲突都报出且保留本机; 包被改过(含包内语料被改过)则拒绝导入且本机一字未动;值的落点分流(`env:` → `.env` 并要重启, 其余 → 凭据服务);清空的前置铁律,以及「清单外的引用名一律不动」「`corpus/` 默认不删」 「名字与引用名同名的 `.env` 键也要清掉」;catalog 带出 `inject` 却绝不带出值。 - client 58 → **137 项断言**(+79:样式表注入幂等、令牌与交互态、三层导航、L1 统计索引与点击映射、 迁移折叠与三节、清空引导态、筛选作用于字段、L2 摘要语义、L3 更换值/取消/只读, **迁移区块的点击级回归**——导出→校验码、计划清空→空的确认框、执行清空→结果卡, 并断言三个请求体里**都没有值**、确认框**不回显**校验码, 以及**按钮可见性**——不再写死白字、每个令牌都带系统色兜底、不留绕过 `C` 对象的裸令牌写法)。 - 合计 185 + 137 + 87 = **409 项断言**。 ### 文档 - `README.md` **重写**(241 行 → 233 行): - 标题从机制描述(「外接数据源的门禁、钥匙与台账」)改为对象本身(**「个人在 DSH 里的外部资源库」**); - 开篇先说清**它是什么**,而不是原来那句讲效果的「让 agent 能取数,但看不到钥匙」; - 新增**「为什么需要它」**:把「写进笔记或 `AGENTS.md` 就够了」这个常见答案摆出来,逐条指出它补不上的四处缺口(取数、钥匙、来源、换机器),再点明个人用 DSH 的场景最经不起「重新交代」; - 六个 `##` 部分取代原来的 16 个平铺章节——**定位 / 安装与登记 / 取数与记录 / 钥匙与安全 / 迁移 / 已知限制**,开篇一句话亮出这个框架;每部分下的 `###` 覆盖得住各自内容,「已知限制」内部再按数据、台账、检索、凭据、迁移分组(原来是一张 11 条的平表)。 - `README.md` 的断言计数纠正为实测值:host 128 / client 58 / 合计 186(原文写 127 / 57 / 184, 两处各少 1,与 HEAD 版实测不符)。 - `README.md`「安全边界」的「值只有一条路径」改写为**两条受控路径**(钥匙面板 + `/stash import`)—— 加上导入这条路径之后,原话就成了假话。 - `README.md`「换一台机器」升级为「**在多台机器间使用**」:换机与并存两种情形、合并语义表、 值不能随行的两条硬边界(凭据的值、语料的绝对路径)。 - `README.md` 数据布局补 `.last-export.json`;入口一节补三条子命令。 - `README.md`「加库的三种方式」补可选占位符 `{?参数名}` 的写法与它为什么必须是显式的。 - `DESIGN-lessons.md` 第五节「留给下一轮:经验的导出/导入」一条划掉——本轮做掉了。 ## [0.9.0] — 2026-09-21 ### 变更(破坏性) - **两个内置 handler 的标识符改用能力命名**:原先用的是各自数据源的商业名,现改为 `trade_stats` 与 `policy_alerts`。 理由:handler 是**能力抽象**(贸易统计 / 政策通报),不是对某个具体产品的绑定。注册表条目自己写 `name` / `summary` 已经足够说明取的是什么;把产品名塞进 handler 标识符,等于让契约与某一家数据源耦合。 `VALID_HANDLERS` 只认新名,**注册表里写旧值的条目需要改名**。 - 示例注册表 `sources.example.mjs` 里的两条样例条目 id 同步改名,展示名改为能力描述。 - 其中一个 handler 的缓存文件名同步改名(旧缓存文件不会被读到,会重取一次)。 - 实现里的 endpoint URL 不变——那是功能所必需的。 ### 文档 - **README 重写:468 行 → 199 行(物理行;非空行 333 → 146)。** 去掉开发过程叙述("为什么是 `/stash` 而不是……"的三张表、三条踩坑记录、 端点清单)与重复解释,只留必要内容:安装 / 工具 / 数据布局 / 使用边界 / 取数台账 / 经验库 / 钥匙台账 / 安全边界 / 加库的三种方式 / 换一台机器 / 开发与测试 / 不做的事 / 已知限制。 - 源码注释、示例、测试里指向具体数据源的名称一并改为能力描述,保持全仓一致。 ### 测试 - host 127 + client 57 = **184 项断言**,全通过(断言数未变,只是夹具与示例改了名)。 ## [0.8.1] — 2026-09-21 ### 修复 - **手写条目删不掉。** 人往 `lessons.json` 里加经验时通常只写 `title` 与 `body`,不会去算 `id`; 而 `stash_lesson_remove` 按 id 定位,于是这条经验**能被读出、却删不掉**, 在 `stash_lesson_list` 里也显示不出标识。 (实测:`readLessons` 给出 `id: null`,删除返回「没有 id 为 "null" 的经验」,文件里那条纹丝不动。) 现在读取时会为没有 `id` 的条目**派生一个稳定 id**(按内容哈希), 于是**手写条目与工具写的条目在 `list` / `remove` 面前完全等价**;派生 id 会在工具下次回写时落盘。 ### 文档 - README 的「经验库」小节补「两条写入路径,地位相同」,并写明两处**不对称**—— 假装对称会比写清楚更糟:手动录入**不过**密钥特征扫描(扫描只挂在 `stash_lesson_add` 那条路上); 工具回写会**规范化排版**(排序 + 缩进 + 落盘派生 id),这与手写 `sources.mjs` 的「程序永不改写」不是一回事。 - 新增 `DESIGN-lessons.md` 第 3.5 节,记录这次为什么改,以及上面的取舍。 - `lib/lessons.js` 模块注释同步(含"两条写入路径"一节)。 ### 测试 - host 半边 120 → **127 项断言**(新增 7 项覆盖手写路径:手写条目可读、派生 id 稳定、 可被工具删除、删完为空、畸形条目被跳过并计数、顶层形状不对时按空处理)。 - client 半边 57 项不变。 ## [0.8.0] — 2026-09-21 ### 新增 - **经验库**:`$DSH_HOME/stash/lessons.json`,按库 id 分组,回答「这条库踩过什么坑、正确写法是什么」。 台账记「取过什么」,注册表记「允许怎么取」,经验库补上第三件事——而坑通常是在**失败之后**才被认识的。 - 三个工具:`stash_lesson_add` / `stash_lesson_list` / `stash_lesson_remove` - 除 `title` 外全是可选(`body` / `action` / `tags` / `evidence`):各库的经验形态本就不同 (贸易数据是口径陷阱,文献库是检索语法,MCP 服务是鉴权流程),所以**不设分类、不设枚举**—— stash 只提供存放、校验、按库带出的便利。 - `evidence` 通常填台账 `ledgerId`,把「这次踩的坑」与「这一次取数的留痕」接起来。 - 正文与标签都过一遍密钥特征扫描,命中即拒绝(经验库是**明文文件**,而经验叙述最容易顺手把口令抄进去)。 - 每个库上限 200 条,**到顶时拒绝写入而不是静默淘汰**——悄悄丢掉别人写下的教训,比报错糟糕得多。 - `stash_catalog` 带出经验:**全量清单**给「条数 + 最近 3 条标题」,**单库查询**才给全文(免得把经验库整个灌进上下文)。 - `stash_doctor` 报告经验库总量,并点出**有失败台账却没记经验**的库。失败是证据、经验是结论;有证据没结论的库,下次还会以同样方式失败一遍。此项是提示级,**不参与 `healthy` 判定**。 ### 变更 - `stash_fetch` 失败时(含被使用边界拒绝)提示可以记一条经验,并给出这次的 `ledgerId`。 只在 render 层加提示,返回结构不变;它只提示,不替使用者决定该不该记。 - `stash_catalog` 的文本渲染现在会显示库条目上的 `notes`(此前只在 JSON 返回值里,文本里看不见)。 ### 为什么是独立文件,而不是挂在库条目的 `notes` 上 **手写的 `sources.mjs` 永不被程序改写**,而现实里的库常常就住在手写文件里(本机就有这样的库)。 经验若只能挂在库条目上,这批库就永远记不了经验——而它们恰恰是最需要记的。 独立文件让**手写库与代写库一视同仁**,也恰好落在「各库经验各不相同」上:文件的第一层键就是库 id。 ### 测试 - host 半边 92 → **120 项断言**(新增 28 项:给手写库记经验、拒绝未登记的库、拒绝无 `title`、 拒绝疑似密钥正文、子串过滤、`catalog` 两种口径、`doctor` 的 gap 提示、删除与各类边界)。 - client 半边 57 项不变。 ### 兼容 - 纯增量:`lessons.json` 不存在时按空经验库处理,旧行为不变;不写经验就与 0.7.1 完全一致。 ## [0.7.1] — 2026-09-12 ### 新增 - `handler: 'http'` 支持 `request.cacheTtlMs`:缓存寿命,过期自动重取;**不写则保持「永不过期」的旧行为**(向后兼容 0.6.0)。 - `handler: 'http'` 支持 `request.captureHeaders`:点名带出的响应头,用于配额计数器一类的用途。**不点名的一律不带出**,所以 `set-cookie` 之类不会被顺手捞进结果。响应头随缓存落盘,命中缓存时同样能拿到配额数。 ## [0.7.0] — 2026-09-12 ### 变更 - 定位明确为「外接数据源的门禁 / 钥匙 / 台账」。 ### 新增 - `access` 使用边界:`public-api` / `official-api` / `export-import` / `unsupported`。声明为 `export-import` 或 `unsupported` 的库会被 `stash_fetch` **拒绝并只留痕**,而不是尽力而为地绕过。 - 取数台账 `$DSH_HOME/stash/ledger.ndjson` 与查询工具 `stash_ledger`。每条记录带 `ledgerId` 与 `contentHash`,**只记指纹不记内容**;失败与被边界拒绝同样留痕。 - `stash_doctor` 增加**条目深度校验**:把「能加载但注定取不到数的写法」提前报出来(`request.url` 写错、`{credential:REF}` 的引用名没在 `credentials` 里声明、`required` 的参数没出现在请求里、`paths` 不是绝对路径、没声明 `access`)。 - `stash_source_add` 在登记前**当场拒收**会导致取数失败的条目。 ### 兼容 - 全部为增量:旧注册表不用改;不写 `access` 只是会在体检里被提示补上。 ## [0.6.0] — 2026-09-12 ### 变更 - 钥匙台账从手写 `sources.mjs` 的 `export const credentials = [...]` 迁到代写分片 `sources.local.json` 的 `accounts`。手写文件界面只读,**迁走才能真正编辑**。 - 数据模型升级为**两层:账号 + 字段**。「同一个网站只占一条目录」在目录层做;值这一层不合并,否则「缺账号还是缺密码」看不出来。 - 类别枚举补 `mcp`(MCP / 智能体服务)。 - 面板行为:已配置条目默认折叠、「更换值」才出现输入框、「移除值」必须二次确认、**值不再 trim**(口令首尾空格有意义)、多行值走 textarea。 - 「编辑信息」与「新建」复用同一张字段表,字段带 `secret` / `inject` / `multiline` / `notes`。 ### 兼容 - 旧形状(`export const credentials = [...]`)仍可读;迁移前的原文件留在 `$DSH_HOME/stash/backup/`。 ## [0.5.1] — 2026-09-11 - 首个进入本仓库的版本,当时的定位是「个人资源堆放处(Profile Bundle)」。 ## 0.4.0 及更早 - **0.4.0**:钥匙台账成为**一等公民**——钥匙可独立于库存在、带人类可读元数据(中文名 / 类别 / 网址 / 备注)、面板按类别分组、模型可建条目但**拿不到值**。 - **0.3.2**:修复 host 半边在 `apply` 时同步 `ctx.get('webServer')` 拿到 `undefined` → 静默跳过路由注册,表现为**界面 HTTP 404 而工具与页面一切正常**。改为 `ctx.inject(['webServer'], (hostCtx) => ...)`。 - **0.3.1**:修复客户端把 URL helper 命名为 `api`、与 `createSection(api)` 的形参同名造成**遮蔽** → async `load()` 抛 `TypeError` → effect 未 await → 只剩 unhandled rejection、界面永远停在「正在读取」。为此新增 `test/client-runtime.mjs`(`node --check` 抓不到这类运行时错误)。 - **0.2.1**:修复 `exports.inject` 里填了**包名**(而非 Cordis 服务键)→ 被当作服务名死等 → 客户端插件永远 `pending`,开机报 `did not activate`。 - **0.1.0–0.2.0**:早期开发阶段,记录未保留。 --- ## 版本对照 | 版本 | 提交 | 日期 | |---|---|---| | 0.9.0 | 见 `git log`(tag `v0.9.0`) | 2026-09-21 | | 0.8.1 | 见 `git log`(tag `v0.8.1`) | 2026-09-21 | | 0.8.0 | 见 `git log`(tag `v0.8.0`) | 2026-09-21 | | 0.7.1 | `fcbd29b` + 发布前整理 | 2026-09-12 | | 0.7.0 | `9cd3cbb` | 2026-09-12 | | 0.5.1 | `219dd8d` | 2026-09-11 | > 0.6.0 未单独留提交(与 0.7.0 合并于 `9cd3cbb`),其内容见上文。