# 更新日志 本文件记录 **DSH 插件包 `@dsh-external/persona-driven`** 的版本。 技能内容本身(8 旋钮、16 型、参考文件、实验工具)另有自己的版本线,见 `skills/persona-driven/SKILL.md` 的 `metadata.version`。 版本号约定:**插件大版本对应技能版本**(`1.6.x` = 技能 `1.5.37` + DSH 原生化第一版)。 --- ## 1.7.0 — 2026-10(DSH 专属埋点与看板维度) 版本号与代码注释对齐:1.6.1 之后新增的埋点与看板维度在注释里已写作「1.7.0 起」, 这里把包版本真正升到 1.7.0。 ### 新埋点(只有原生插件会写,钩子桥没有) 机制先查实再写:树内约 30 个内置插件都用**插件顶层** `ctx.on('session/event', …)` 订阅会话事件(含 `dsh-session-telemetry`),不需要按 agent 订阅。照它抄了两条纪律: **逐事件兜住异常**(一个坏事件不能让后面的全丢)、只读 `event.type` / `event.data`。 | 埋点 | 字段 | 干什么用 | |---|---|---| | 审批结局 | `ev:"approval"` `{outcome, tool}` | **拒绝率** —— 只能从会话事件拿,`tools/result` 里看不到用户的选择 | | 每轮边界 | `ev:"turn"` `{turn}` | 每轮调用数的网格 | | 工具结局 | `ok`(**只落布尔**) | 错误率 | | 技能名 | `skill` | 技能调用排行 | | 层级 | `layer` | 修掉「层级分布」全归「未归属」 | **故意不落错误原文**:错误消息可能夹带命令片段或路径,与本项目「埋点不落原文」的约定冲突, 而错误率本身不需要原文就有意义。 ### 新看板维度(全部只用已落盘数据算,不新增采集) - **工具族**:MCP / Blender / 技能 / 子代理 / 网络 / 后台作业 / **待办 / 计划模式** / 内置 / 其他。 后两项是「结构约束」旋钮的行为代理指标。 - **审批结局与拒绝率**:分母只算用户真的做了选择的两种结局(`allowed-once` / `rejected`), `cancelled` 与 `unavailable` 不计入——把「环境没提示」算成「用户满意」是错的。 - **每轮调用数**:判定线关心的相对量。 - **档位切换前后对照**(判定线真正关心的那块):同一会话首条 turn 之后、切档前后 **各至少 2 轮**才纳入;两侧不足 2 轮就不显示,且**不按方向排序**,避免诱导只看想要的结果。 - **技能调用排行**。 ### 三个必须记下的坑(都是实测踩到的) 1. **会话事件与工具事件必须分开加载。** 审批记录带 `tool` 字段,混进工具事件会被 「工具分布」与「探查画像」算成一次真实调用——那是污染不是统计。 2. **每轮调用数必须按「该会话首条 turn」截断。** turn 埋点是本版才上线的,老会话有 332 次历史调用却只有 1 条 turn 记录;不截断就会算出 **332 次/轮** 这种荒谬值。 3. **时间戳必须真解析,不能比字符串。** 档位记录由 `persona.py` 写成 `+08:00`,插件写 `Z`, 逐字符比较会得到错误的大小关系。新增 `parse_ts()` 统一容错。 另:工具族归类我自己连错两次——先把已归类的记录扔进「其他」(190 条), 又把字符串 `"other"` 当成「已归类」(`"other"` 也是真值),让 `blender_rt_*` / `skill` 全算成内置。两处都已修,原因写进了函数注释。 ## 1.6.1 — 2026-10(看板数据修复) 修的是「看板看着没数据、其实数据一直在写」这一类,两处独立缺陷。 ### 一、工具词表跨语言不一致(根因) `lib/persona-core.js` 的 `TOOL_ALIASES` 在移植时被**单方面改了名**:`shell`→`bash`、 `grep`+`glob` 合并成 `search`、`task`→`subagent`。而读取端(`persona_dashboard.py`、 `persona.py report`)按 Python 侧的规范类别名判定,于是「探查画像」「终端调用」 「验证回收」**全部恒为 0**。 叠加第二个原因:DSH 的工具名是**小写**(`read`/`write`/`edit`/`grep`/`glob`/`pwsh`), 而两套词表都停留在 Claude Code 的 `Read`/`Edit`/`Grep`/`Glob`/`Bash` —— 读/改调用整批 落成 `other`(实测 94 行)。 | 面板 | 修前 | 修后(同一份埋点) | |---|---|---| | 探查画像 | 读 0 / 改 1 / 探查 0 | 读 59 / 改 62 / 探查 69 | | 验证遵守 | 0/0 | 1/1 | | 终端调用 | 恒 0 | 有值 | 修法: - JS 词表改为与 Python **同构同义**(类别名回到 `read/write/edit/shell/grep/glob/task/web`), 两侧的类别集合、成员集合、顺序逐项一致。 - 两侧都补 DSH 小写工具名,并加**同一套前缀兜底**(20 条,顺序敏感)。理由:DSH 的工具名 由插件贡献(`mcp__*`、`blender_rt_*`、内置的 `skill`/`todo_write`/`job_*`…),枚举追不上; 未识别时仍写 `raw_tool`,所以「不认识」是**可诊断**的,不是静默丢失。 - 插件补写工具事件的 **`layer`** 字段(原先只有注入记录写,导致「层级分布」全归「未归属」)。 - 看板读取端加**历史兼容**:`tool == "other"` 但带 `raw_tool` 的记录用原名重新归类,并把 `bash` / `search` / `subagent` 一并认作 shell / 搜索类 / 任务类 —— **修复前的数据不必丢弃**。 (`search` 是被合并过的搜索类,无法再区分 grep 与 glob,按搜索类计入,不猜。) ### 二、看板把「L3 生效」判断成了「装没装钩子」 `persona_dashboard.py` 的 `liteMode()` 用 `P.hook`,只看 `$DSH_HOME/persona/hooks.json`。 DSH 上 L3 的载体是**原生插件**,没装钩子时它判 false 并**强制锁进简洁版、禁用复选框**, 把已经在写的工具数据藏起来。 改为 `P.l3 = l3_active()`:**原生插件**(复用 `check_environment.dsh_plugin_installed()`, 同一件事不写第二份实现)**或**桥接钩子,任一条在跑即算生效;相关文案与提示同步改正。 ### 验证 - `npm test`:6 个套件 **224 条断言全绿**,安装自检「就绪(阻塞项 0)」。 - 跨语言词表一致性:比对 8 个类别、20 条前缀规则、47 个探针工具名、4 个埋点字段 → **0 失败**;变异检验(把 `'read'` 从 JS 成员里删掉)→ **类别成员不一致,被捕获**。 注意:只看归类**行为**会被前缀兜底掩盖(变异后 `normalize('read')` 两侧仍相同), **必须同时比对词表本身**。 - **固化程度说明**:原计划的 `tests/tool-vocab-sync.mjs` **本轮未落地**(新增测试文件未获准), 故以「命令 + 期望输出」作为证据,契约本身**已知但未自动强制**。命令见 `docs/架构与移植说明.md` 的「跨语言词表契约」一节。 ### 三、安全底线误伤 env 属性访问(连带发现「规则无法被修改」) `密钥与凭据` 里那条用反斜杠转义匹配 dotenv 文件名的正则,会命中**代码里对 env 属性的访问**。 后果不是「危险操作被拦」,而是**正常操作被误判**:任何写了该属性访问的脚本或测试都被转去审批, 而审批通道不可用时(例如本会话禁用了审批提示)`ask` 会直接变成 deny。实测一次会话里 9 次命中 有 5 次属于此类误报,对应 3 次写文件、2 次跑命令被拒。 **更糟的是它会阻止自己被修复**:守卫扫描的字段里含编辑内容 `new_string`,所以任何包含该字面量的 编辑都会被自己的守卫拦下——修这一行的第一次尝试正是被它拒绝的。**规则表因此成了不可编辑的文件。** 新正则改用方括号形式(语义等价,字面量不再是命中串),注释里写明理由,避免后人「改回」转义写法后复发。 改法:要求点号前是路径或引号边界。JS 与 Python 两份规则表同步改,导出**逐字节一致**(各 1306 字符)。 `tests/guard-rules.mjs` 由 94 条增至 **101 条**断言(3 条正例 + 4 条负例), 变异检验(去掉边界条件)→ 4 条负例全红。 > 附带说明:**守卫本身只会 `ask`,不会 deny**(`lib/` 全库只有一个 `{kind:'ask'}`, > `tools/pre-execute` 的默认值是 `{kind:'allow'}`)。看到的「直接拒绝」来自下游 —— > `ask` → DSH 审批 → 无通道时 `serviceAsk` 返回 deny。审批提示打开后它就是一次真实询问。 ## 1.6.0 — 2026-10(首个 DSH 版本) 从 WorkBuddy 技能 `persona-driven v1.5.37` 完整迁移,并原生化。 ### 新增:DSH 插件能力 - **内置技能**:`lib/index.js` 通过 `ctx.skills.registerProvider()` 注册 `persona-driven`, 模型在会话的 `` 目录里直接看到它。等价于 `@deepseek-ai/dsh-skill-badge` 的形态(不可变单候选 + 目录资源基)。 - **每轮注入(L3)**:`agent/pre-step` 瀑布按回合注入档位块。DSH 没有 Claude Code 的 `UserPromptSubmit` 钩子,`agent/pre-step` 是等价拦截点,且在**第一次模型请求之前**执行, 所以 turn 1 也覆盖得到。同回合多步只注入一次(按 agent + turn 去重)。 - **安全底线强制确认(L3)**:`tools/pre-execute` 命中危险操作时返回 `{kind:'ask'}`, 由 DSH 的 Approval 服务转成真实的人工确认。**不再依赖 Git Bash**,也不再每轮起 Python 子进程。 - **埋点**:以与 Python 侧完全相同的字段名写入 `$DSH_HOME/persona/metrics/*.jsonl`, `persona.py report` 与人格看板照旧可读。 - **网页面板**:只读路由 `/host/persona-driven/state` + `lib/client.js` 的状态药丸, 显示当前生效的人格与档位串。**故意不提供写操作**:从 UI 静默改全局档位会波及用户 毫无察觉的其他对话,本技能明确禁止。 ### 迁移:平台耦合点全部改到 DSH - 状态根目录 `~/.workbuddy/persona` → `$DSH_HOME/persona`(`$DSH_HOME` 缺省 `~/.dsh`)。 - 项目层 `/.workbuddy/persona/state.json` → `/.dsh/persona/state.json`。 - 会话标识 `CODEBUDDY_SESSION_ID` → `DSH_SESSION_ID`(保留旧名作为回退)。 - 钩子配置目标 `~/.workbuddy/settings.json` → `$DSH_HOME/persona/hooks.json`, 并改为 **Claude Code 事件映射格式**(DSH 的 `@deepseek-ai/dsh-hooks-claude-code` 桥接吃这个格式)。 - **删除 Git Bash 依赖**:DSH 的钩子桥接走 `ctx.shell`,原生插件完全不需要 shell。 - `doctor` 的判定从「L3 钩子是否注册」改为「DSH 插件是否装入 profile」。 - 安全底线里「本机全局配置」的命中规则改为 `.dsh/cordis.patch.yml` 与 `.dsh/persona/hooks.json`。 ### 移除:PreCompact 事件 WorkBuddy 版注册了 5 个钩子事件,DSH 的 Claude Code 桥接支持其中 4 个: `SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PostToolUse`。 **`PreCompact` 在 DSH 侧不受支持**(在桥接的不支持清单里,配置会被静默忽略), 因此已从生成器、事件分组与文档中删除。 **这是一处真实但轻微的能力回退**,如实说明:走**钩子桥接**这条路径时,上下文压缩之后 档位块不再被重新注入。走**原生插件**路径不受影响(`agent/pre-step` 每次都在)。 ### 修复 - `persona_dashboard.py`:埋点目录为空时 `min()` 抛 `ValueError`,导致「刚装好、还没用过」的 用户打开看板直接报错。改为空数据单独分支,如实显示「暂无埋点数据」。 (这是迁移前就存在的缺陷,不是本次引入的。) ### 新增:可复跑的验证 - `tests/floor-text-sync.mjs`:三条底线的**文案在 JS 与 Python 两侧逐字节比对**。 起因是本项目反复踩过的失效模式——同一段文案存两份副本、改一边漏改另一边。 - `tests/plugin-wiring.mjs`:用真实 DSH 包 + 假 Cordis 上下文真的调用 `apply()`, 驱动 `skills.registerProvider` / `tools/pre-execute` / `agent/pre-step` / 只读路由四条接线。 - `tests/guard-rules.mjs`、`tests/render-block.mjs`、`tests/state-layers.mjs`、 `tests/skill-contract.mjs`、`tests/doctor.mjs`,以及 `.github/workflows/ci.yml` (Ubuntu + Windows × Node 20/22 × Python 3.10/3.12)。 ### 文档 - **技能描述与 README 写明权限前提**:本技能需要**完全权限(DSH 的 Full access)**—— 它要执行 shell 命令跑 `python scripts/persona.py …`,并写**会话工作区之外**的 `$DSH_HOME/persona/`。缺权限时 `set` / `off` / 看板 / `doctor` 会因写入被拒而失败, 档位落不下去。`SKILL.md` 新增「权限前提」一节列出各操作在受限模式下的实际表现, 并要求**先告知用户、不要先切档再声称已生效**。 (注:装了插件后每轮注入与安全底线不经过 shell,但这些 CLI 操作仍然需要权限。) - **文档不再写死本机路径**:README 与 docs 里 8 处本地绝对路径(`cd <本机路径>`、 `link:<本机路径>` 形式的示例,以及一处分析用的解包目录)改为 `<本仓库目录>` / `` 占位符。这些示例会随 git 分发,写死作者机器上的路径对读者是错的。 (`~/.dsh` 下 DSH profile 里的 `link:` 属于安装配置,**必须**含绝对路径,且不随仓库分发——两者性质不同。) ### 修复 - `tests/plugin-wiring.mjs`:不再探测任何机器私有路径,只认 `PERSONA_DSH_NODE_MODULES` 与本仓库自己的 `node_modules`。 - `tests/plugin-wiring.mjs`:**重启条件写错了**——原先用「用户有没有设 `PERSONA_DSH_NODE_MODULES`」判断是否要自己重启装解析钩子,导致**照文档设了环境变量的人 永远不会触发重启、钩子永远装不上、用例恒跳过**,文档指的那条路根本走不通。 改为按「钩子装没装」(哨兵变量)判断。 - `tests/run-all.mjs`:**跳过不再被算作通过**。此前汇总只看退出码,有用例跳过时仍报 「全部用例通过(6 个套件)」;现在区分「通过 / 跳过(未验证)/ 失败」,并显式声明 跳过的套件没有被真正跑过。 ### 文档:补上社区分发路径 - README(中英)新增置顶安装方式:**`dsh plugin --profile add github:yxdotnet/dsh-persona-driven`**,一条命令装包并自动写入 `dsh.profile.bundles`。 实测 pnpm 9.1 秒完成、组合无警告、技能确实注册进会话目录。 - 修正同一处**过时说明**:原文让用户手工编辑 `dsh.profile.bundles`——`dsh plugin add` 本来就会自己写,这一步是多余的。 - 顶部加 topic / license 徽章。 - 背景:DSH **产品本身没有插件市场**(源码树 `marketplace` 零命中),社区生态围绕 GitHub topic **`dsh-plugin`** 建立(注册表 `vbarter/dsh-plugin-registry` 按该 topic 每 2 小时自动发现;另有若干 awesome 目录)。本仓库的清单已满足收录契约 (根 `package.json` + `dsh.bundle.patch` 为安全相对路径),**只差声明该 topic**。 ### 修复:网页面板默认关闭(它在默认档位下取不到数据) 实测发现 `lib/client.js` 的状态药丸**永远不会显示**,不是显示得晚: | 调用 | 返回 | |---|---| | 不带 `?session=`(客户端就是这么调的) | `{"layer":"global","persona":null}` | | `?session=<本会话>` | `{"layer":"session","persona":"ENTJ","dialsText":"P5/V3/R4/S2/D4/E2/ST4/W1"}` | 根因三条,都查证过:宿主渲染 `conversation.composer.dock` 时传的是**空 props** (源码里就是 `renderSlot("conversation.composer.dock", {})`);会话标识不在页面 URL 里; 客户端也没有可注入的 session 服务。于是它只能读全局层,而档位默认写在会话层, `persona` 为 null,组件 `return null`。 改法:`webPanel` 默认 `false`,并在 `lib/index.js` / `lib/client.js` / `cordis.patch.yml` / 两份 README / `docs/安装与验收.md` 里把实测数据与原因写清楚。只有显式设了**全局**档位 (`--scope 全局`)时,打开 `webPanel: true` 才有意义。 顺带修了测试里一个**真实的构造错误**:`plugin-wiring` 的 `DEFAULTS` 把 `...parsed` 写在 字面量之后,于是真实默认值会覆盖字面量——`webPanel` 默认一改,那条路由用例就静默改用 `false` 跑并红了 2 条。已调整次序(字面量只作兜底),把路由用例改为**显式** `webPanel: true`,并**新增一条反向断言**「默认配置下不注册路由」。 该断言做过变异检验:把默认改回 `true` 时它确实红(67 通过 / 1 失败),不是摆设。