# 更新日志 本文件记录每个已发布版本的对外变化。版本号遵循语义化版本(主.次.修订)。 > **脱敏声明(2026-10-08)**:为公开发布做准备,**本文件历史条目中的个人标识已做脱敏** —— > 人名昵称、人格卡示例中的姓名/别名、内部实例名与容器名、本机绝对路径,一律换成中性示例 > (如 `本名:「示例」;别名:「例子」`)。**"改了什么、为什么改"的技术内容逐字保留**,只换标识。 > ### 版本号读法(2026-10-04 用户要求) > **对外称呼用两段式:`/<插件版本>`**,例如 **`0.2.0-rc.2/0.1.6`**。 > - 第一段 = 适配的 DSH 版本(本部署是 `0.2.0-rc.2`) > - 第二段 = 插件自身版本(本版是 `0.1.6`) > - ⚠️ `/` 不能出现在 `package.json` 的 `version` 与文件名里(前者要合法 semver,后者是路径分隔符)⇒ > 这两处落地为 **`0.1.6+dsh.0.2.0-rc.2`**(`+` 是合法 semver 的 build metadata)。**两处指同一件事。** > - ⚠️ **发到 npm 时只用第二段(干净号)**(2026-10-08 补充):`npm publish` 会用 `semver.clean()` > **剥掉 `+…`** 再上传(npm 自带 `libnpmpublish` 的行为),所以"发 `0.1.9+dsh.0.2.0-rc.2`"实际发上去的 > 就是 `0.1.9` —— 与其让工具**静默改名**,不如**发布前就把 `version` 写成 `0.1.9`**。 > **`0.1.9` 该号一旦发出永久占用、不可覆盖**,所以这一步必须在发布前定死。 ## 0.2.0-rc.2/0.1.17 — 2026-10-11(**未发布**,界面文案清理版) > **为什么又换一次号**:`0.1.15` 与 `0.1.16` 先后被**两次并行的发布**占用(registry 上同版本不可覆盖、不可重发),而那两次的产物都**没有走本仓库的脱敏与逐字节验收**(其中一次还带着 2 个已无源文件的陈旧产物)。本版用**通过全部隐私检查**的内容重发。 **修掉的内容(面向用户的界面文案)** - 技能页的删除提示里出现了一个**内部运维侧的称呼** → 改为中性表述(中英文成对):「真要删文件,**跟运维通道说一声**,我帮你删(会先备份)。」 - 测试注释里指向同一内部称呼的措辞 → 一并中性化。 **为什么之前没被发现(一条值得记住的教训)** - 隐私检查的规则表里**只收录了"带前缀的实例全名",没有收录"裸的罗马化短名"**; 而"验规则表能不能抓到已知串"这类对照实验,**永远测不出"规则表根本没覆盖"这件事**。 - 现已补上:罗马化形式进规则表(**一律带词边界**,否则会淹没在 `channel`/`naming` 这类词的噪音里), 并在对照实验里增设一步 ——「**故意放一个规则表里没有的真阳性,预期它应当被漏过**」,用来验证对照本身有没有盲区。 - 另加一道**死文件校验**:包内每个文件都应能在 `src/` 找到源文件(`0.1.15` 的产物里混进了 2 个 0 引用的陈旧文件)。 **与 0.1.15 的关系** 功能与逻辑**完全一致** —— 逐字节比对显示两者的差异 **100% 只是「主人→用户」这类称呼替换**,零逻辑差异。本版只是把**还没清干净的文字**清掉,并补齐产物完整性校验。 ## 0.2.0-rc.2/0.1.15 — 2026-10-10(**未发布**,换号重发版;内容与 0.1.14 一致) > **这是一次"换号重发",不是一次功能变更。** > > **为什么要换号**:npm 规定**版本号一旦用过就永不复用**(不可覆盖、不可重复发布同一号)。 > 本插件此前在 npm 上可见的版本(`0.1.0`–`0.1.14`)已按**隐私处置**整体删除; > 删除后重发**必须启用一个从未用过的新号**,因此本版取号 `0.1.15`。 > > **内容**:与本轮已经过两轮脱敏的 `0.1.14` 发布包**逐字节一致**,差异只有 > `package.json` 的 `version` 字段与本文这一条说明 —— **没有任何功能、文案、数据或依赖变化**。 > 发布包指纹与逐文件差异对照见 `docs/reports/humanity-0.1.15-publish.md`。 > > **状态**:本轮只**备包**,未发布。发布需等整包删除完成并跨过 npm 的 24 小时名字封禁期后另行执行。 ## 0.2.0-rc.2/0.1.14 — 2026-10-09(**未发布**,人格卡只剩 4 个原生项 + 去掉「用户定义」区块) > **npm 发布为 `0.1.14`;两段式称 `0.2.0-rc.2/0.1.14`**(见上方版本号读法)。 > 本地构建的 tgz 用**带版次的新文件名** `0.1.14+dsh.0.2.0-rc.2-r1.tgz`(同名同版本换内容的坑已在流程里修掉)。 > **未部署**:本轮只做到"打好包 + 独立验收",装包与重启由用户单独拍板。 **用户 2026-10-09 的两条产品决定(逐字)** > ① "只有 8 个原生项,角色卡 4 个,人格卡 4 个,其余都是属于自定一项,**没有标准这一说**, > 除了原生的 8 个外,**其余都一样待遇**,包括 dsh 新装一个 Humanity,也是显示 8 个原生项。" > ② "**用户自定义可以删了,它跟添加小节重复了。**" **A 人格卡原生项从 8 个减到 4 个(其余一律自定义、待遇相同)** - **唯一真源改一处、全局跟随**:`src/host/persona/constants.ts` 的 `STANDARD_SECTION_TITLES` 截到 4 项 (对用户称呼 / 说话风格 / 交付格式 / 行为边界),`LOCKED_SECTION_TITLES` 仍是它的前 4 项(派生,不另抄)。 宿主 7 处与客户端 4 处消费者(默认文档、规范化、校验、补齐、排序、界面)全部跟随,没有第二份字面量。 - **新装只有 8 个原生项**:身份卡 4 字段 + 人格卡 4 小节;**不再预置**那 4 个空节。 - **现存那 4 个节降级为普通自定义项**(内容与标题一字不动):可删、可改名,**删掉之后不再被补回** (只有原生 4 个会被"补齐")。列表顺序会变(原生在前),这是预期。 - **「非标准小节」提示取消**:`mutable.section-non-standard` 这个**问题码本身删除** (连同客户端中英文案与派生判断 `isStandardMutableLayer`)—— 与 `mutable.address-changed` 同一种退役方式: 永远不会再产生的东西不留着。**删的是提示,不是校验**:空标题、重复标题、冒牌固定层字段名、超长条目照旧拦。 - **原生 4 项仍锁定**:界面没有删除/改名入口,绕过界面直删直改被宿主以 `mutable.section-locked-missing` 拒绝(`checkLockedSections` 对所有来源生效)——工作台不是后门。 **B 去掉 UI 的「用户定义」区块(数据保留)** - 渲染与编辑路径整体下线:`PersonaFields` 的区块、`persona-edits` 的三个入口、 控制器的 `addUserDefined` / `setUserDefined` / `removeUserDefined`、以及中英文案。 - **数据一个字节都不动**:`mutableLayer.userDefined` 仍在 schema 里、仍随保存原样写回、仍会进注入; 用户的那张卡里它是空数组 ⇒ 零影响。**非空的老卡**:界面不再显示它(报告里已点名这一残留)。 **验证读数(施工方,2026-10-09)**:`pnpm typecheck` → **RC=0**; `pnpm test` → **基线 697/697/0 → 710/710/0**(净增 13;新增 `test/persona-native-four.test.mjs` 的 15 条 V3–V8 用例, 其中 V4/V5/V6 都做了正反两面);`pnpm build` → RC=0;tgz 的 **sha256 / 字节数 / 文件数**见施工报告。 ## 0.2.0-rc.2/0.1.11 — 2026-10-09(**未发布**,人格卡护栏改「接近预算就刹 + 写入前预演」) > **npm 发布为 `0.1.11`;两段式称 `0.2.0-rc.2/0.1.11`**(见上方版本号读法)。 > 本地构建的 tgz 文件名沿用旧口径 `0.1.11+dsh.0.2.0-rc.2`。 > **未部署**:本轮只做到"打好包 + 独立验收",装包与重启由用户单独拍板。 > 本条目**只覆盖 A(人格卡护栏)**;B(技能落盘)见本条目末尾的追加标记。 **A 护栏从「已经超了才拦」改成「接近预算就刹 + 写入前预演」** - **现象 / 根因**(真机实测,2026-10-09T03:20:47Z 那轮):自进化台账逐字写着 `人格注入预算:已用 1880/2065 字符,预算内(注入 33 条);本轮合并同义条目 0 条` —— 0.1.10 的判据只看 `truncated`(**已经**装不下),它读到"预算内"就放行,**追加 3 条之后就超了**; 随后注入即报超预算。"此刻没超"不等于"写完不超",只看前者永远晚一步。 - **修复(两道闸,都在 `src/host/evolution/`)**: 1. **护栏**:轮首读一次预算读数(仍是 `persona/render.ts` 的 `FitStats`,**唯一真源**), 判据从 `truncated` 扩成 `truncated || used/budget ≥ 0.85` (新常量 `PERSONA_BUDGET_GUARD_RATIO`,见下"为什么是常量")。触发时: 分析提示词末尾追加"**只做整合(合并/精简)、禁止追加新条目**"(读数文案分两档: 「已超」与「**接近上限**」,运行记录里能分清是哪种);写回层同样一条新条目都不写, **但合并照常**(护栏只停追加,不停"腾地方")。 2. **写入前预演**(本版核心):护栏**没**触发时也不是照单全收 —— 把拟新增条目先合进候选文档、 用**与真实注入同一套拟合**重新算一遍;这一批会让注入变成"已截断"就**整批丢弃**、只保留合并。 ⇒ 这就是"空当"被堵住的地方:读数 0.83 的卡再追加一批,照样在写盘前被拦下。 - **可观测**:运行记录注记扩成 `人格注入预算:已用 N/M 字符,占 xx%(接近上限) ⇒ 本轮只做整合;本轮合并同义条目 K 条,护栏/预演拦下拟新增 J 条` —— "自进化这轮什么都没干"与"它守住了预算"终于能分开看。 - **为什么阈值是常量而不是配置项**:这是护栏的**松紧**,不是用户的功能选择;做成配置项会让 提示词与写回层各读一个阈值,护栏形同虚设(且它的含义随 `PROMPT_CHAR_BUDGET` 一起变,本质与预算常量成对)。 - **已知取舍(提请用户拍板)**:`truncated` 也包含"单条超过软上限 `VALUE_SOFT_LIMIT = 120`、 注入时会被裁短"这一档 ⇒ 这类**超长新条目**也会被拦下(宁可这轮不写并如实记账,也不静默收下半条)。 若希望"长条目照收、注入时再裁",把预演判据收窄成 `dropped > 0` 即可(一处改动)。 - **阴性对照**:预算充足(如占 34%)时那段约束**一个字都不出现**,新条目**照常写入**;预演能过的追加照常写入。 - **红线未动**:`PROMPT_CHAR_BUDGET` 仍 **2400**;不新增存储表、旧数据零迁移;不做删除。 **B 技能落盘:立即止血 + 根因调研**(本版另一条线,由 B 的施工方完成;此处补记) - **止血(数据面)**:把 `evolution_proposals` 里两条 `kind: skill`、`status: applied` 的提案正文, 用已装包里真正的 `renderSkillFile`(不自己实现格式)渲染后写进 `/skills/dsh-duplicate-plugin-mount-fix/SKILL.md` 与 `/skills/restart-effect-verification/SKILL.md` —— 落盘用**先 staging 再落目标**,并做了独立校验(比对正文 sha256,不用写入脚本的自述)。 两个文件的正文与提案 payload **逐字一致**(唯一一处不完全逐字之处已在报告中如实说明)。 - **根因(源码级)**:插件在「批准提案 / 启动恢复」时落盘,那一刻**没有 agent 会话** ⇒ 官方文件后端按**部署默认模式**判(本部署 `workspace-write`,根 `/workspace`), 而 `$DSH_HOME/skills` 不在其下 ⇒ `FS_SANDBOX_DENIED`。 **权限预设是会话级的**(`setSandboxMode(session, mode)` 写的是 `sandbox/mode` 会话事件), 对无会话的插件调用**不生效** —— 所以"把预设放宽到 danger-full-access 就能落盘"是错的 (README 该句已在 0.1.12 订正)。 - **DSH 不给"插件侧自签通道"**:`ctx.fs.writeText` 的 per-call `sandboxPolicy` 形参确实存在 (`dsh-fs/lib/types/index.d.ts:221`),但合法取值只有"该会话的策略"或"已批准的升级", 两者都要求会话对象;插件自己手搓 `{mode:'danger-full-access'}` 传进去属于**绕过围栏**,不是通道。 - **当时的落地方式**:由有会话身份的一方(会话侧 bash)直接写文件;插件侧保持"如实上报失败" (技能页 `落盘写入 0 条 … 失败 2 条`)。→ **0.1.12 把这条路产品化**(见下一条)。 **验证读数(施工方,2026-10-09)**:`pnpm typecheck` → **RC=0**; `pnpm test` → **tests 628 / pass 628 / fail 0**(改动前基线 **622 / 622 / 0**,净增 6 条新用例, 逐条对应 `docs/plans/humanity-0.1.11-guard-and-skill-disk.md` §4 的 V3–V6,含 V4 阴性对照与 V5 边界用例); `pnpm build` → RC=0;tgz 的 **sha256 / 字节数 / 文件数**见施工报告。 ## 0.2.0-rc.2/0.1.13 — 2026-10-09(**未发布**,人格页交互改造:8 项锁定 / 整项删除 / 名称只读·内容可编 / 底部添加 + 历史保留期) > **npm 发布为 `0.1.13`;两段式称 `0.2.0-rc.2/0.1.13`**(见上方版本号读法)。 > 本地构建的 tgz 文件名沿用旧口径 `0.1.13+dsh.0.2.0-rc.2`。 > **未部署**:本轮只做到"打好包 + 独立验收",装包与重启由用户单独拍板(可**与 0.1.12 一起装**,省一次重启)。 > 判据真源:`docs/plans/humanity-0.1.13-persona-ui-lock-delete.md` §0 / §4(V1–V8)。 **A 默认 8 项锁死:固定层 4 项 + 人格卡前 4 个小节** - **锁的是哪 8 项**:身份卡 姓名 / 性别 / 人格偏好 / 主语言(`CORE_IDENTITY_KEYS`,**原有**约束) \+ 人格卡 对用户称呼 / 说话风格 / 交付格式 / 行为边界(新增 `LOCKED_SECTION_TITLES`, 派生自 `STANDARD_SECTION_TITLES` 的前 4 项,不另抄字面量)。 - **两面都拦**:① 界面上这 8 项**没有删除入口**(`canDeleteItem` 是唯一判据,连二次确认态都进不去); ② **绕过界面直接请求删除被拒** —— 宿主新增 `mutable.section-locked-missing`(rejected), 对**所有写入来源**生效(工作台不是后门)。固定层那半边仍由既有的 `identity.core-missing` 管。 - **只拦"整项删除",不拦编辑**:清空条目、改写内容照常放行(P-010 的高风险是"内容没了", 且只对 Agent 生效;"这一节空着"不是删除)。 - **祖父化**:当前文档里本来就没有这一节时(旧数据 / 外部写入留下的畸形文档)不拦 —— 否则会变成"一个字都存不进去"的死结。`prev` 缺席(首次写入)时从严,与 `checkCoreFields` 同一口径。 **B 其余项可删,且是「真删」(删除粒度 = 整项)** - 可删:习惯与偏好 / 经验教训 / 情绪 / 未结修正 / 自定义小节 / 自定义字段 / 「用户定义」条目。 **不做条目级删除**(用户明确否掉)。 - **真删**:整项从 `mutableLayer.sections`(或 `fields` / `userDefined`)里摘掉 ⇒ 文档里没有、 注入内容里也没有。客户端 `orderedSections` **不再补齐缺失的标准小标题** —— 旧实现会给每个标准标题兜一个空壳,于是"删了还在界面上、还打不进字"(假删除)。 - **二次确认**:删除是**不可逆**的(本步没有撤销栈),点删除先进确认态(确认带点名删的是哪一项), 确认后才落进草稿;落盘仍走卡片上的「保存」这一条路。 - **可回溯**:被删内容进 `persona_history` —— 沿用宿主既有行为(`persistPersona` → `supersededBy`, `reason: superseded`)。**保留期**见下方「返工 2」。 **C 形态:名称是纯文字、内容一直是输入框 + 删除按钮在项名称下方 + 添加走底部入口** - **名称区没有输入框**(用户原话:"只说每项的名称不要输入框了"):字段名 / 小节名 /「用户定义」的键 一律纯文字;**用户自定义的项**可以点「改名」把这一格临时换成输入框(见「追加 3」), 锁定 8 项与标准小节的名字只读。 - **内容区一直是可编辑输入框**:单行(身份卡字段 /「用户定义」的值)或多行(小节), **不需要**"点一下才出现"这一步;只读部署下渲染为 `disabled`。 - **删除按钮位于项名称下方**(用户原话):项名那一列纵向排「名字 → 删除」, 行的列定义从 `名称 | 内容 | 操作` 收敛成 `名称 | 内容`(内容区更宽)。 - **底部统一添加入口**:默认只有一个按钮,点开才出现输入区 + 保存/取消;保存顺带走一次写回 (填出来的一项不该停在草稿里)。名字或内容为空时按钮**禁用**、提交是**空操作**(两处说的同一件事)。 **追加 3(同日,用户选定"加一个改名按钮"):给用户自定义的项加「改名」入口** - **给**:自定义身份卡字段、**自定义**小节、「用户定义」的条目 —— 名称处多一个「改名」按钮, 点了才把名称那一格换成输入框 + 确定/取消;确认后原地换名字,落盘仍走卡片上的「保存」 (旧名字那一节按"被取代"进 `persona_history`)。 - **不给**:锁定 8 项 + **全部标准小节**(含可删的习惯与偏好 / 经验教训 / 情绪 / 未结修正)。 理由有机械依据(不是"觉得不该给"):标准名是插件认的标识,改掉之后 `completeSections`(Agent 整理工具)下一次就会把原标准名**补回一个空壳**、 而新名字变成非标准小节(实测:`经验教训`→`复盘` 之后,小节列表多出一个空的 `经验教训`, `复盘` 拿 `mutable.section-non-standard` 警告)。 - **边界**(V13):空名 / 没改动 / 撞标准名(保留名)/ 重名 **一律不落草稿**,确定键禁用并在旁边写明原因; 重名判据是**整张人格卡内名字唯一**(字段名 / 小节标题 / 用户定义键共用一个名字空间)—— 与宿主逐向对齐,其中"小节或用户定义键撞身份卡字段名"这一向**比宿主严**(宿主不拦这一向, 这里在源头拦,避免用户之后加不回同名字段)。**绝不静默覆盖**:改名只改这一项自己的键 / 标题。 - **没动宿主**:这条只改客户端(宿主对自定义项的改名本来就放行),锁定拦截等约束原样。 **追加 4(同日,**真机反馈**修复):按钮并排 + 身份卡输入框不再被截断** > 0.1.13 装到真机、用户在浏览器里用眼睛看出来的两个问题 —— 前几轮的渲染断言**覆盖不到** > "按钮排布好不好看、框够不够宽"。修复都是纯客户端布局,**判据也改成可机械复核的形态**(含阴性对照)。 - **按钮并排**(用户:改名与删除原来上下两行堆着):项名下方新增一层**横向动作行** (`editor-styles.itemActions`:`display:flex; flex-direction:row; flex-wrap:nowrap`), 「改名」「删除」同属这一层;锁定项两个动作都没有时**整层不渲染**(不留空行)。 - **身份卡输入框被截断**(根因是布局,不是宽度策略):原语 `Input` 的真实结构是 `` + 内层 `` (`dsh-client-ui-primitives/lib/Input.module.css`)——外层是 **inline-flex(收缩到内容宽)**。 0.1.12 时它是 grid 的**直接子项**,grid 的 `justify-self: stretch` 会拉满那一列; 0.1.13 给它加了 `data-persona-content` 包装层(为了可机械复核),而包装层是 `display:block` —— **block 不拉伸子元素** ⇒ 输入框退回收缩宽(≈ 输入框默认 20 字符 ≈ 135px),长值被框截住。 `