# 星愿 Dsh 插件开发文档 > 本文档描述独立项目 `@starwish-ai/xingyuan-dsh` 的架构、核心机制与开发规范,面向后续在本仓库上迭代的开发者。 > 接口基线:DeepSeek Harness(下称 dsh)**两条版本线并存** —— peer 一律写 > `^0.1.7-rc.1 || ^0.2.0-rc.1`(16 个 dsh-* peer 同形)。dsh 处于技术预览期,API 可能变动; > 升级依赖版本前先核对官方 release notes 与 §12 参考文档。**加一条新版本线必须先逐包核对, > 再往 `test/host-baseline.test.ts` 的 `SUPPORTED_DSH_VERSIONS` 里加**——该表同时把没核对过的 > 下一代(现为 `0.3.0-rc.1`)拦在门外,放开范围=无声承诺。 > (0.2.0-rc.1 增量要点(**本仓库已跟进**,2026-09-29 逐包按字节比对 + `typecheck/build/test` 全绿, > 业务代码零改动): > ① **本次唯一的破坏点在声明而不在 API**——0.6.5 的 peer 写 `^0.1.7-rc.1`,而闸门用 > `semver.satisfies('0.2.0-rc.1', '^0.1.7-rc.1', {includePrerelease:true})` 判定为 false, > 实测 `dsh --profile web --dump-config` 报 `skipping profile bundle "@starwish-ai/xingyuan-dsh"` > → 预设/卡片/标签页/设置页/直开页**整体消失**(数据无损:库在 `~/.dsh/xingyuan/`,§4 硬约束 2)。 > ② **`dsh-schedule` 被宿主重写**:从「会话内一次性定时器」变成 **Host-wide 持久提醒**——自有 > `schedule` 存储域与交付历史、新工具 `schedule_update`、新规则 **daily / weekly / cron**、 > `MIN_EVERY_INTERVAL_SECONDS` 300→60、插件形态由 `name`+`apply` 函数改为 `ScheduleService` 类 > (default 导出,Config 两项全可选)→ 本包补丁里那条裸 `schedule` 行照旧装载,它要的 > `agents/sessions/tools/storageDomain/sessionController/sessionPersistence` 在 web profile 全部具备。 > §10 决策 7 与 §11 首条的「周期提醒做不到 / 仅 session-local」据此当场作废,两节已按新事实改写。 > ③ 但本包 peer 同时声明两条线而**提醒能力在两版宿主上不同**,所以提示词一侧不得断言能力边界: > `REMINDER_GUIDE` 改为「先看 schedule_create 本次实际接受的参数」(`test/prompts.test.ts` 锁死)。 > ④ 其余相关事实全部核实未变:会话格式仍 **v4**、v3→v4 边仍把 ignorable 未知事件保留成 > `plugin:`(§5.6 的补标/去毒分界与「代次+模式」标记判定不变)、宿主**仍未**开放插件事件注册接缝 > (`dsh-session` 只多了 `ToolCallRecovery`)、`retainedBy.mainView` 与 `projectionValues.agentPreset` > 取法不变(§5.11,`mainView` 仍由 `dsh-client-ui-session` 经 `SessionReferenceSourceMap` 贡献)、 > 显示元数据契约(`readPluginMeta`/`iconOf`/`LANGUAGE_ID`)逐字不变、补丁层叠前提不变 > (`dsh-base` 的 `storage-domain` 行仍只有 `backend` 一键、base/web-app 仍无 `schedule` 行、 > 内置预设仍为 `preset-standard/-ptc/-minimal/-cordis`、`!!js` 作用域仍有 `dshHomePath`)。 > ⑤ 语义变化一处:**transcriptView 在 Web 端默认由 `standard` 改 `detailed`**(Desktop 才 standard, > 字段同时变可选/`plain`,缺省走客户端默认,legacy `normal`/`expanded` 读作 detailed), > 折叠策略没变——仍**只有 `verbose` 不折叠**(见 §11)。 > ⑥ 官方随包实践规则升版:`cordis-plugin-development/SKILL.md` 39→50 行,新增「禁止手改 profile 的 > `package.json`/`cordis.patch.yml`、不在 profile 目录跑 pnpm(走 `install_bundle`)」、设计/评审 > 章节与「Enable a shipped plugin」章节;新参考 `references/user-actions.md`(一个操作两个调用方) > 与新技能 `agent-experience`(工具描述纪律)——星愿自查结论见 §5.10 末段。) > (0.1.7-rc.1 增量要点(**本仓库已跟进**,2026-09-24 逐包按字节比对 + 真机核对):宿主新增 > **DSH peer 兼容闸门**——`dsh-app-boot` 的 `evaluatePluginCompatibility` 对包**声明出来的** > `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer 跑 `semver.satisfies(宿主版本, 范围, {includePrerelease:true})`, > 不符者**整包静默跳过**(真机可见第三方包因此消失),豁免走 profile 的 `compatibility.json` + > `dsh plugin allow-version --dsh-version --accept-risk`。这使「peer 不是闸门、 > 不兼容只能靠真机发现」的旧结论作废(见 §11 已改写);也正因为闸门只认声明, > **client 半侧 6 个宿主包已补进 peerDependencies**(连同补丁点名的 dsh-agent-preset / dsh-schedule, > 共 16 个 dsh-* peer;`test/host-baseline.test.ts` 三面互锁)。其余面为零变化: > 会话格式仍 v4(`SESSION_FORMAT_VERSION = 4`、v3→v4 边仍保留 ignorable)、settings 子系统、 > preset 声明行、tools/system-prompt/user-questions/storage/webserver 的 lib **逐字节相同**; > 真正改了的只有 client 侧三包,其中语义变化两处:**「每 (kind,id) 仅一条 start」不变量被宿主撤除** > (重复 start 不再抛错,最早的 start 建 State、后来的走 update),以及 transcriptView 变 > 四档 `compact/standard/detailed/verbose` 且默认由 compact 改 **standard**——但**只有 verbose 才 > 不折叠**(`foldCompletedTurns:false` 仅 verbose 一档),见 §11 已改写的措辞。 > **client 半侧的宿主包在 0.1.7-rc.1 之前连 peer 范围都没有**:`@deepseek-ai/dsh-api-session-controller` > 之类只作为 **devDependencies 精确锁基线版本**,运行时那一版完全由用户装的 dsh CLI 决定—— > 于是 typecheck 校验的未必是用户跑的那一版,也没有任何版本约束拦下不兼容升级 > (2026-09-22 的标签页静默失效即此,见 §5.11)。**现已补进 peerDependencies**(optional, > 免终端型 profile 报无谓的 unmet peer;rc.1 的闸门不看 optional 标记,保护不因此减弱), > 声明面/钉版/实装三者由 `test/host-baseline.test.ts` 互锁。但**peer 闸门只挡版本不匹配, > 挡不住「同版本号不同行为」**,字段撤除类故障仍只能靠真机验证发现(§5.10 的部署回路)。 > (0.1.7-alpha.2 迁移要点(**破坏性**,本仓库已跟进):**settings 子系统整体重写**—— > `ctx.settings.installSection`(host)与 `ctx.settingsScope`(client)**全部撤除、无兼容层**; > 可编辑项改为「profile 行 Config 里标了 `.volatile()` 的字段」,host 侧 `ctx.settings` 变成 > `SettingsForms`(describe/update/replace/mutate/configure),client 侧改为 > `ctx.configForms.get(行 id)`;**用户偏好不再存 `~/.dsh/settings.yaml`,改存 > `~/.dsh/profiles//cordis.patch.yml`**(旧 settings.yaml 被宿主改名 > `settings.yaml.imported`,其中星愿两节因无对应 Loader 行而未被导入 → 偏好重置为默认,见 §5.8); > `settings.plugin.item` 槽没了,改 `plugins.item`;新增 `configForms.whileServed` 注册接缝; > **Agent Preset 注册机制换形**——宿主不再扫描任何 preset 目录(旧 > `$DSH_HOME/.agent-presets//`(`preset.yml` + `agent.cordis.yml` 两文件)**无人再读**), > 一个 preset 就是 bundle 补丁里的**一行 `@deepseek-ai/dsh-agent-preset` 声明**,激活期由该插件 > 提交给 `agentPresets` 注册表(详见 §4「Agent Preset 声明行」)。本仓库在 0.1.7 适配时漏核了 > 这一项:`preset-root.ts` 继续往那个死目录复制,于是「Agent 预设」选择器里静默不出现星愿、 > typecheck 全绿、运行零报错(2026-09-23 真机发现,与 §5.11 那次标签页失效同一类坑)。 > 同版本**会话格式升 v4**(`session.v4.jsonl[.zstd]`):v3 从此算历史代;**v3→v4 迁移边接受 > ignorable 未知事件**(重命名为 `plugin:`、数据保留),而 v0→v1/v1→v2/v2→v3 仍拒绝一切 > 未知历史事件——去毒模式对 ≤v2 仍然必需,见 §5.6。`retainedBy.mainView`、`slots`/ > `uiConversation`/`locale` 契约、storage-domain 与自带 sqlite 后端、tools/system-prompt/ > user-questions 声明合并位未变(2026-09-23 逐项核对 + 真机验证;**同批核对清单不含 preset 发布 > 机制**,故有上一条的漏网——升级核对见 §12 排障索引的「preset 不出现」行)。 > 0.1.6-alpha.2 迁移要点(用户侧曾运行,基线已越过):**client 侧会话列表快照 > `SessionListState` 撤除 `current`/`currentAddress`**,「当前会话」改由 `SessionSummary` > 上必填的 `retainedBy.mainView` 引用计数表达(官方读法 > `Object.values(byId).find(s => (s.retainedBy.mainView ?? 0) > 0)`);`SessionSummary.completed` > 一并撤除。`agentPreset` 投影键未变(0.1.7 仍挂在 `SessionSummary.projectionValues` 上)。 > 0.1.5-rc.2 迁移要点(历史):**会话格式升 v3**,工件改名 `session.v3.jsonl[.zstd]`(v0 仍是 > `session.jsonl`),读取端对当前格式认 `ignorable`;系统提示词 section 号重排(宿主内容移到 > 10000+,插件固有 5–115 号现在自然排在宿主说明之前,无需改动);`ctx.agent` 移除(插件只用 > `exec.agent`,不受影响);PTC 词汇 `tool/code-dispatch` → `tool/ptc-dispatch`、预设 > `code` → `ptc`(插件未用)。 > 0.1.2-rc.1 迁移要点(历史):settings 收进 `ctx.settings.installSection`;`dsh-client-runtime` > 包消失,`ChatNodeDataMap` 移入 ui-chat、`ctx.conversationEvents` → `ctx.uiConversation.events`。) --- ## 1. 项目概览 星愿是一个愿望与习惯养成助手,以 dsh 插件组合包(bundle)+ Agent Preset 形式交付: 用户安装后执行 `dsh plugin --profile web add @starwish-ai/xingyuan-dsh`, agent 选择器出现「星愿」即安装成功。 | 能力 | 实现位置 | |---|---| | 对话创建愿望/任务(含查重、推荐任务) | `src/preset/tools.ts`(49 个模型工具,见 §6) | | 机会日打卡(周期计算、补卡、取消、未来预勾) | `src/opportunity.ts` + `src/store.ts` | | 微行动拆解(3–7 步逐步完成/跳过/重开) | `src/micro.ts` | | 记忆(增删改查 + 上下文自动注入) | `memories` 表 + `src/preset/prompts.ts` 动态上下文 | | 成长体系(Lv.1–Lv.10、经验、连续加成) | `src/growth.ts` | | 图表(15 种 chartKey,会话内卡片渲染) | `src/preset/charts.ts` + `xingyuan/chart` 事件 | | 会话视图页(今日/愿望/任务/日历/成长/记忆六标签,显隐可控) | `src/client/pages/*` + `src/client/tab-visibility.ts`(client 半侧) | | 安全确认(写操作二次确认、删除始终确认) | `src/preset/hitl.ts`(userQuestions) | | 数据持久化 | 自带 sqlite 后端,`~/.dsh/xingyuan/xingyuan.sqlite` | 技术栈:TypeScript(Node ≥22.5)+ Cordis 插件框架 + React 18(client 半侧) + `node:sqlite` + zod;peer 依赖 `@deepseek-ai/dsh-*` 同时 admit 两条宿主线 `^0.1.7-rc.1 || ^0.2.0-rc.1`(host 侧 8 + client 半侧 6 + 补丁点名的 dsh-agent-preset / dsh-schedule,共 16 个;rc.1 起的兼容闸门只看声明出来的 peer,多版本承诺由 `test/host-baseline.test.ts` 逐条放行判定锁死,见 §11 与文档头增量要点); `@deepseek-ai/cordis` 跟随宿主 4.x(独立版本线);`@deepseek-ai/schemastery` 为 `*`,跟随宿主。 ### 运行形态 ``` 用户浏览器 ── dsh Web GUI(官方壳) ├─ Chat 流:会话事件卡(愿望/任务/打卡/图表/微行动) ├─ 会话视图标签页:今日/愿望/任务/日历/成长/记忆(client 模块注入 slots) ├─ 设置 → 星愿 设置页(教练风格/画像/确认开关/注入上限) └─ 浏览器直开页面 /xingyuan/today | /calendar | /growth(webServer 路由) dsh 进程(Node) ├─【bundle 层 · 常驻】自带 sqlite 后端行 + storage-domain 领域路由 ├─【bundle 层 · 常驻】/xingyuan/* 页面与数据 API(src/routes/) ├─【bundle 层 · 常驻】preset-xingyuan 声明行 → 注册表里的「星愿」预设 └─【preset 层 · 选「星愿」才挂载】声明行的 plugins → @starwish-ai/xingyuan-dsh/preset/side (工具 + 11 段提示词 + HITL 确认 + 动态上下文) ``` 分层铁律:**工具/提示词只注册在 preset 层**——未选择星愿的会话完全看不到它们, 不污染其他类型的会话。 ## 2. dsh 术语速查 | dsh 概念 | 含义 | 星愿用法 | |---|---|---| | **Bundle 组合包** | npm 包,声明 `dsh.bundle.patch` 指向 `cordis.patch.yml`,`dsh plugin add` 安装 | `@starwish-ai/xingyuan-dsh` 本体 | | **Cordis 插件** | `apply(ctx, config)` + `inject` 依赖声明 + effect 自动清理副作用 | 一切代码的组织单元 | | **Agent Preset** | 会话级能力集合;0.1.7 起 = bundle 补丁里一行 `@deepseek-ai/dsh-agent-preset` 声明(`config.id` 是会话落盘的预设身份,`config.plugins` 是子插件行列表) | 行 `preset-xingyuan`、id `xingyuan`;绑定星愿工具与提示词 | | **Session Event** | 持久会话事件(`SessionEventMap`),模型可见即已记录,刷新可回放 | 承载 wish/task/checkin/chart/micro 业务事实 | | **Conversation Node** | client 半侧按事件渲染聊天卡片的扩展点 | 五类业务卡片 | | **storage domain** | `defineDomain` 声明表结构(zod),由存储后端落库 | 四张表 + global 单例 | | **userQuestions** | 阻塞式人机问答 seam | 写操作二次确认 | ## 3. 目录结构与构建产物 ``` XingYuan-Dsh/ ├── package.json # exports 子路径导出 + dsh.bundle/client 声明 + peer 锁版本 ├── cordis.patch.yml # bundle 层组合补丁:sqlite 后端行 + 主行 + schedule + preset 声明行(见 §4) ├── src/ │ ├── index.ts # bundle 常驻入口:开领域 → provide('xingyuan') → 注册路由 → 日志自愈 │ ├── tab-policy.ts # 标签页显隐纯策略与常量 + 「当前会话」取法(host/client 共用,见 §5.11) │ ├── pref-policy.ts # 对话偏好纯策略与常量 + 表单行 id 绑定(host/client 共用,见 §5.8) │ ├── pref-settings.ts # 对话偏好字段表(volatile)+ 配置→偏好读取映射(二次确认/注入上限/确认卡语言) │ ├── ui-settings.ts # 界面偏好字段表(volatile):标签页显隐三态 + 单标签勾选 │ ├── domain.ts # defineDomain 四张表 + global schema + XingyuanStore 服务 │ ├── sqlite.ts # 自带 sqlite 存储后端(StorageBackend 契约实现) │ ├── store.ts # 业务层(愿望/任务/打卡用例,工具面与路由面共用收口) │ ├── opportunity.ts # 机会日计算器(once/daily/weekly/monthly + 月末钳制) │ ├── growth.ts # 等级经验与连续统计(从 checkins 重放重算) │ ├── micro.ts # 微行动状态机 │ ├── cascade.ts # 删除级联(愿望→任务→打卡→微行动→颜色覆盖) │ ├── category-color.ts # 分类颜色解析(覆盖 > 显式 > 哈希兜底,22 键) │ ├── events.ts # SessionEventMap 声明合并(纯类型导出,host/client 共用) │ ├── session-log-repair.ts # 会话日志自愈:为历史 xingyuan/* 事件补 ignorable 标记(激活期,见 §5.6) │ ├── consistency-sweep.ts # 启动一致性清扫:孤儿打卡/任务/悬挂微行动的级联补救(integrity.test.ts 锁定) │ ├── types.ts # 包根类型再导出(domain 记录 + events 事件类型;./types 子路径单一产物) │ ├── routes/ # /xingyuan/* HTTP 面(index/api/config/errors/pages-html) │ └── preset/ # ↓ 只挂在 preset 层 ↓ │ ├── side.ts # preset 侧入口:工具/提示词注册 + 偏好读取(偏好不在此层) │ ├── tools.ts # 49 个模型工具 │ ├── prompts.ts # 11 段系统提示词 + 动态上下文 │ ├── hitl.ts # userQuestions 确认封装(文案语言随 confirmLang 偏好) │ └── charts.ts # 15 种 chartKey 数据计算 ├── src/client/ # 浏览器半侧(React 18) │ ├── index.ts # 注册 5 个卡片 Definition + 6 个视图标签页 + 设置整页 │ ├── cards.ts / types.ts # keyed 渲染器与卡片状态 │ ├── chart-labels.ts # 图表内建中文词的客户端本地化映射(test/chart-labels.test.ts 锁定) │ ├── view-state.ts # 视图页跨标签切换的状态快照(搜索词/月份偏移/展开集合) │ ├── tab-visibility.ts # 标签页显隐控制器(设置 × 会话预设动态注册,见 §5.11) │ ├── tab-hint.ts # 今日页非星愿提示行快照 store(控制器写、今日页订阅) │ ├── pages/ # today/wishes/tasks/calendar/growth/memory/detail/quick-create/... │ ├── ui.ts / styles.ts # 应用内对话框、toast、主题样式 │ └── i18n.ts # 中英双语字典 ├── debug/ # 视觉验证 mock 生成器(gen-mock.ts,用真实 STYLE_TEXT 产深/浅两主题静态页;见 §5.10) └── test/ # vitest:loader 级集成测试 + 各层单测(见 §9) ``` 构建产物为 `lib/`(tsdown 打包 + tsc 类型)。集成测试依赖 `lib/` 产物, 因此本地跑测试前必须先 `pnpm build`(CI 已保证顺序)。 ## 4. 分层与组合 两层职责(第三层「preset 发布」在 0.1.7 没了——预设改由补丁里的声明行注册): | 层 | 组合方式 | 内容 | 卸载行为 | |---|---|---|---| | bundle 常驻层 | `cordis.patch.yml` 补丁宿主组装 | sqlite 后端行、主插件行、schedule 行、**preset 声明行**、storage-domain 领域路由 | `dsh plugin remove` 连依赖带层一起拔除 | | preset 层 | 选「星愿」agent 时才挂载 | 工具/提示词/HITL | 未选则整层不存在 | ### cordis.patch.yml(现状) ```yaml # 语义:`- insert:` 插入新行;裸行按 id 整行替换既有配置(不深合并)。 - insert: - id: xingyuan-sqlite # 自带 sqlite 后端,DB 固定 ~/.dsh/xingyuan/ name: '@starwish-ai/xingyuan-dsh/sqlite' config: path: !!js "dshHomePath('xingyuan', 'xingyuan.sqlite')" - id: xy-bundle # 主插件行:开领域 + 提供 xingyuan 服务 + 注册路由 name: '@starwish-ai/xingyuan-dsh' - id: schedule # 宿主默认组装不含 schedule 行,需补插 name: '@deepseek-ai/dsh-schedule' - id: preset-xingyuan # Agent 预设「星愿」声明行(见下节) name: '@deepseek-ai/dsh-agent-preset' config: id: xingyuan name: 星愿 description: 愿望与任务管理助手:…… order: 100 plugins: - name: '@starwish-ai/xingyuan-dsh/preset/side' # 领域路由:星愿领域走 sqlite,其余领域维持 json(整行替换须重述 backend) - id: storage-domain name: '@deepseek-ai/dsh-storage-domain' config: backend: json routes: xingyuan: sqlite ``` 两个硬约束(踩过的坑,勿改回去): > **升级检查单**(patch DSL 无条件插入/整行替换,升级 dsh 前逐条核对官方 > `dsh-base`/`web-app` patch 原文):1) 本补丁以 `insert:` 插入 `id: schedule` 行, > 若上游默认组装新增同名行,装配器对重复 id **响亮抛错**(整个 profile 起不来); > 同一条也适用于 `preset-xingyuan`(官方内置预设是 `preset-standard`/`-ptc`/`-minimal`/ > `-cordis`,改名才会撞);2) `storage-domain` 裸行整行替换——上游给该行 config 新增键会被本补丁静默丢弃, > 且若其他 bundle 也路由 domain,后装者整值覆盖会使星愿数据落回 json 后端 > (`~/.dsh/storages/`),备份口径随之变化;3) `schedule` 行的**目标插件形态**也要核—— > 0.2.0-rc.1 把它从 `name`+`apply` 函数插件改成 `ScheduleService` 类(default 导出、 > Config 两项全可选、自有 `schedule` 存储域),裸行照旧装载;它新注入 `ctx.schedule` 服务并 > 要求 `sessionPersistence`/`sessionController` 等六项依赖,换宿主版本时先确认这些在目标 > profile 里具备,否则本包插入的那行会无声挂不上。 1. **主行 id 不能与 preset 身份同名**。`xy-bundle` 若也叫 `xingyuan`,会话挂载预设时 两条组装行互踩(死锁)。0.1.7 起预设身份的落点从目录名变成 `preset-xingyuan` 行的 `config.id`,避让口径不变:主行 `xy-bundle`、Loader 行 `preset-xingyuan`、 预设身份 `xingyuan` 三者互不相等(`test/pref-settings.test.ts` + `test/preset-declaration.test.ts` 对拍)。 2. **DB 路径禁止放进包安装目录**——卸载/升级均可能清掉包目录;固定在 `~/.dsh/xingyuan/` 才能保证数据存活。备份只需拷贝该目录。 ### Agent Preset 声明行(0.1.7 起的唯一注册途径) 宿主 0.1.7 起**没有任何目录扫描**:`dsh-agent-preset-registry` 的定义集是内存 Map, 只有 `@deepseek-ai/dsh-agent-preset` 那行插件在激活期 `ctx.agentPresets.register(config)` 才写入。旧形态(激活期把 `presets/xingyuan/` 复制到 `$DSH_HOME/.agent-presets/xingyuan`, 以文件指纹做幂等)**整体作废**——全树 grep `.agent-presets` 在宿主代码里零命中, 官方 `dsh-agent-preset/skills/editing-cordis-compositions` 明写"Nothing reads that directory any more"。 声明行的字段口径(`@deepseek-ai/dsh-agent-preset` 的 Config): | 字段 | 语义 | 星愿取值 | |---|---|---| | `id` | 会话落盘的预设身份(`agent-preset/selected` 事件里那个串),小写字母/数字/连字符 | `xingyuan`——与 `src/tab-policy.ts` 的 `XINGYUAN_PRESET_ID` 逐字同源 | | `plugins` | 该预设的**子插件行列表**(工具/提示词就挂在这里) | 单行 `@starwish-ai/xingyuan-dsh/preset/side` | | `name` / `description` | 选择器显示名与说明;**不写 `name` 会被当成内置预设** | `星愿` / 一句中文说明 | | `order` | roster 位置 | 100 | Loader 行 id 用官方约定 `preset-`(`plugin_manager` 的 `list_plugins` 按此行 id 显示激活态)。子插件行写**裸包名子路径**,模块解析跟随宿主组装基准,与补丁来自哪个包无关。 三条踩坑要点: - **静默失效**:这一行写错或没写,选择器就是不出现星愿,typecheck 全绿、运行零报错。 故 `test/preset-declaration.test.ts` 双面对拍——按 YAML 解析真实补丁(`!!js` 节点 读成宿主同形状的 `{__jsExpr}` 表达式包,不求值),并把取到的 config 交给**真实宿主插件** `@deepseek-ai/dsh-agent-preset`(devDependency 精确锁基线)装载,断言注册表收到 `xingyuan` 且本行 dispose 后收到注销。 - **升级核对**:宿主若再换这套机制(例如重开目录扫描或换包名),先按 §12 排障索引「preset 不出现」行核对,别默认本节仍然成立。 - **用户根残留**:升级到本机制后,老用户机器上 `~/.dsh/.agent-presets/xingyuan/` 是死目录(无人读、不影响任何部署)。官方口径为手动删除,本插件**不写删除用户目录的代码**。 ## 5. 核心机制 ### 5.1 存储领域(domain.ts) ```ts export const DOMAIN_VERSION = 1 export const xingyuanDomainSpec = defineDomain({ name: 'xingyuan', version: DOMAIN_VERSION, global: { // 全局单例槽:教练风格 + 用户画像 + 微行动状态 + 分类色覆盖 schema: GlobalSchema, initial: { coachStyle: 'gentle', profile: {} }, }, tables: { wishes: domainTable(z.object({ wishId, title(≤50), categoryName(2..6), colorKey?, description?, estimatedCompletionDate?, progress, totalRequiredDays, totalCompletedDays, archived, createdAt })), tasks: domainTable(z.object({ taskId, wishId?, name(≤100), hint?, dueDate?, checkInCycle: enum(['once','daily','weekly','monthly']), source: enum(['user','ai']), status, claimDate?, requiredDays, completedDays, closedReason?, createdAt })), checkins: domainTable(z.object({ checkinId, taskId, date, checkedAt })), memories: domainTable(z.object({ key(2..50), value(≤1000), category: enum(['personal','preference','habit','event','other']), importance: enum(['high','medium','low']), createdAt:number })), }, }) ``` 要点: - **无迁移机制**:介质版本不符直接拒绝打开。`DOMAIN_VERSION` v1 一次定死; 演进策略 = 版本号递增 + 重导出工具(从 v1 规划)。global 里新增字段一律声明 `optional`——zod 对旧数据缺键解析为 undefined 即向后兼容,无需动版本号。 - **规避「无二级索引」限制**:每 key 一文档一物理行,跨表无事务、无索引。 打卡表以 `` `${taskId}|${date}` `` 复合键存取,O(1) 判存在;「某任务的机会日序列」 不逐日展开落库,由 `opportunity.ts` 按需现算。 - **自带 sqlite 后端**(`sqlite.ts`):实现 dsh-storage `StorageBackend` 契约。 物理布局为一个 DB 文件:`xingyuan_meta(unit, version)` 戳介质版本并承载 global 槽; 每张声明表一张物理表 `u__`(key TEXT PRIMARY KEY, value TEXT JSON)。 `node:sqlite` 同步写 + WAL,写入即持久。 - **写路径 schema 闸门**(`makeXingyuanStore` 单点拦截):dsh-storage-domain 只在 冷启动 open 时校验存量记录,写句柄明确「不 re-check」——业务层字段校验只覆盖 各自路径,一条漏网越界记录会落库成功、下次启动 `open` 整体失败(插件永久无法 激活)。因此 store 组装时把全部 `put/update/global.set` 包一层声明 schema 校验 (`RecordInvalidError`,code `invalid_record` 供路由透传,客户端未知 code 回落原文);合法记录 parse 为恒等,读侧零开销。`test/domain-guard.test.ts` 锁定。 - **生命周期**:bundle 入口先 `ensurePresetRoot()` 再 `storageDomain.open(spec)`, 就绪后 `ctx.provide('xingyuan', store)`;`ctx.effect(() => async () => domain.close())` 保证卸载/HMR 自动关闭。注意:异步 await 间隙访问 `ctx.*` 会命中 inactive context, 服务引用必须在 apply 同步段取出。 ### 5.2 打卡与机会日语义(领域口径,改前必读) 机会日 = 任务应打卡的日期序列,是打卡/进度/日历/完成判定的唯一事实口径 (`src/opportunity.ts`,配套 `test/opportunity.test.ts` 覆盖边界用例): - **锚点日** = 领取日(claimDate),未领取则为创建日;延迟领取锚点随领取日后移, requiredDays 同口径重算。 - **周期**:`once` 仅截止日一天;`daily` 锚点日起每天;`weekly` 每 7 天(非自然周); `monthly` 逐自然月推进并做日期钳制(1/31 → 2/28 → 3/31)。 - 全部日期为本地时区 `yyyy-MM-dd` 字符串;内部换算与终止判定一律用 UTC 天数序号 (数值比较),规避夏令时漂移——**禁止用 ISO 字典序做终止判定**:序列越过 9999-12-31 后 `'10000-xx' < '9999-xx'` 恒真('1' < '9'),daily/weekly/monthly 三条循环全部死循环挂死进程(`calculateOpportunityDates` 的 monthly 终止还必须用 `Date.UTC` 数值而非字符串解析——'10000-…' 经 `Date.parse` 得 NaN,同样恒假)。 - 无截止日的任务没有机会日约束(不限次数)。 - **weekly/monthly 锚点语义**:机会日从领取日起每 7 天/逐自然月推进,**不绑定自然周 星期几**(monthly 按领取日的日期逐月推进,月末钳制)。用户要求固定星期几时,模型 按提示词口径如实说明(建议 daily 或在目标星期几领取),不得默认能满足。 - **截止日 10 年地平线**(`DUE_DATE_HORIZON_DAYS = 3650`,store.ts):序列按截止日 逐期物化,远期截止(9999 年)会让今日页/日历/图表每次读取都重建数百万格的数组; createTask/updateTask 超界一律拒绝(`due_too_far`),页面日期输入同步封顶。 写路径闸门只挡住插件自身的写入;手改 sqlite 塞入的远期截止属脏数据——终止性已由 数值比较兜住(有限但重),无需进程级防护。 打卡与领取规则(`check_in_task`/`claim_task` 工具描述与页面文案同源): 1. 不传日期 = 自动勾选今天(含)起**最早未勾选**的机会日; 2. 过去的日期不会自动补,补卡须指定日期(日历页入口); 3. 今天不是机会日时勾选的是未来日期——**提前打卡 = 承诺当天完成**,回复须如实告知; 4. 取消打卡可指定日期撤销;一次打卡对应一个机会日;页面撤销入口一律显式带日期 (详情页=最近一条打卡含预勾、今日页=当天),不依赖「不传日期=撤最近一次」。 5. **过期关闭的任务不能打卡**(写路径与页面共用 syncTaskValue 同口径新鲜化校验,不得以 库内陈旧 status 绕过);如需补历史,先延长截止日使任务重新开始。 6. **截止日已过的待领取任务拒绝领取**(`claim_expired`):锚点=领取日(晚于截止日)时 机会日序列为空,领取即当场过期关闭——与其回包虚假的「进入进行中」/页面 toast 「去打卡吧」,不如就地拒绝并引导先延长截止日复活(详情面板的复活行对「已过期仍 待领取」同样可用;页面任务行对该状态给指引而非必失败按钮)。 7. **承诺口径(2026-08,见 §10 决策 11)**:未领取(pending)任务处于候选池——未承诺 不算义务。**「今日」尺度只统计已领取任务**:今日页进度条/全部完成表扬、日历格子 due/checked 与完成判定、成长页近 30 天区间图(未领取不构成缺口,与月历同口径)、 开场「今日待打卡」概览、`get_today_unchecked_tasks` 输出 一律过滤 pending(共享 `isClaimed` 判定,勿各自写不等价过滤;client 展示层消费 day API 的 `claimed` 布尔,禁止重复写 status 谓词);「今日」尺度的统计对象是 **已领取且未完结**的义务——已完结(closed,达标关闭 achieved)任务不进入日程面 (once 无截止日任务打卡次日即退出,打卡当天保留完成区撤销行;撤销另可走任务详情页与 当天日历面板);**过期关闭(closedReason=expired)是失败记录,不在此列**——它留在其 截止日所在日程面如实呈现错过(月历格 c1/c2 与区间图缺口正是该记录),但已不可打卡, 复活走任务详情页延长截止日(2026-08 补充:规则 7 的「已完结」字面只指达标关闭); 未领取任务单列 「可领取」区(任务页待领取组、开场概览、工具附带行),并约束模型**不得主动领取** ——领取是用户决定,误领取同误打卡。 **今日页与日历详情面板(含今天)只呈现已领取任务**:候选池不进今日页、不进日历面板 (2026-08 用户裁决两轮——「今日/日历」= 承诺面,待领取属计划面;领取入口收归任务页 待领取组与对话开场概览,独立页 /xingyuan/today 与 /xingyuan/calendar 详情同口径)。 **「长期」比率为承诺口径(2026-09 修订,见 §10 决策 11)**:愿望进度(页面卡片三态、 wishProgress/taskCompletionRate 图、get_wish_* 工具输出)分母只计已领取任务——候选 不进比率、不拖累完成率,删除候选也不抬高进度;**达成 = 进度 100% 且无候选** (`store.ts` `wishProgressFromAgg` 单一公式,写路径与读侧共用,消费侧禁止重复判定)—— 满进度而有候选 = 「待结算」(进度条满、不归档):愿望卡附候选计数与收尾指引行、 工具输出注明,引导用户领取继续或删除候选收尾,模型不得为促成达成主动领取或删除; **展示派生位单一来源**:`freshWishes` 视图随公式产出 `pendingCount`/`settled`, 随 `/api/wishes` 与会话 `xingyuan/wish` 事件下发,页面/聊天卡/摘要一律消费下发位 (禁止展示层重建 `progress>=100 && 有候选` 闸门);**达成闸门翻转必须触达模型**: 打卡/领取/更新/取消/删除(含批量)与**创建**(含批量——向已达成愿望追加候选会退档)等 任务写操作完成后经 `wishSettleNote` 比对写入前后视图(批量入口前后视图各走 `wishViewsBefore`/`wishSettleNotes` 单遍索引,守批量读契约),达成/进待结算/回退 (**含待结算→进行中的分母扩大回退**,如延长截止日、领取多日候选)时回包注明并补发 wish 事件 (卡面与回包、愿望页三方同源,收尾路径「删除候选→达成」「领取 0 天候选→直接达成」 「追加候选→退档待结算」不静默);非翻转但待领取数变化同样补发 wish 事件(不产回包文案)—— 卡上「N 个任务待领取」计数与页面同源,增删候选后卡不定格旧数; 三态展示位 planning(无已领取任务)亦由 `wishProgressFromAgg` 同公式产出, 页面/聊天卡/列表回包一律消费,禁任务状态谓词重建;分区折叠唯一收口 `foldTaskIntoAgg`。 **用语规范(2026-09 用户裁决)**:用户可见文案与模型逐字转述面(i18n / 工具回包 / 提示词指南 / 独立备用页 pages-html)禁内部词——「候选(任务)」→「待领取的任务」; 「待结算」→「待收尾」;「口径」「锚点」「分母」「账本」不得出现在面向用户句子里 (文档/注释/工具 description 的模型指令面可用)。达成庆祝 emoji「🎉」统一置行首 (避免「…候选。🎉成长:」粘连,读侧 statusNote 行尾「已达成 🎉」为句末自然收尾可保留); 收尾句固定句式「领了继续,或删掉就达成」;加新状态先定口语名再落文案,禁词约束见愿望指南。 开场概览(`xingyuan:today`)读 `freshWishes` 而非库存 `archived`(库存位只在写路径刷新,闸门变更/跨日新鲜化下可与派生面分叉); 图表副标题恒标注「已领取任务」口径(无候选时的条件分支文案已取消——一个指标一个含义); 未来安排查询工具(`get_tasks_for_next_days` / `get_tasks_for_date_range`)维持计划口径并在描述/计数中标注未领取。 月历图例 c0 文案为「无打卡安排」以对齐该 口径(未领取机会日隐身是承诺面的有意取舍,预览由任务详情 ≤5 个未来机会日兜底)。 改这些口径必须先改 §10 决策 11 与本条,再同步单测对拍(test/routes.test.ts、 test/integrity.test.ts、test/charts.test.ts;闸门翻转回包/事件与用语规范另见 test/tools.test.ts、test/prompts.test.ts)。 进度口径:愿望进度 = 应打天数完成率(**floor 而非 round**——round 曾使 249/250 显示 100% 并触发提前归档);**今日页进度条/完成率同用 floor**(round 会显示「249/250 · 100%」却 无「全部完成」横幅,构成可见矛盾);任务的 requiredDays/completedDays 由机会日序列与 checkins 表推导,跨天恒为最新(不冗余存储历史状态)。 once 且无截止日的任务没有机会日序列:进行中阶段只在**今天**常驻于今日页/日历(唯一 打卡触点,计入完成率分母,故必须始终有打卡按钮);打卡即完成,完成当天保留在完成区 可撤销,**次日起退出日程面**——已完结不是待打卡义务,不占今日进度分母/日历格(撤销 入口=任务详情页与打卡当天日历面板)。 ### 5.3 成长体系(growth.ts) - 全部指标从 checkins 表**重放重算**(单一事实源):取消中间一天、乱序补卡、同日多条、 未来预勾均精确无漂移。 - 经验 = 每条打卡记录发 10 点基础分 × 该记录所在日期的连续加成(≥3 天 ×1.2、≥7 天 ×1.5, 四舍五入);同日多条共享同一连续值。 - 当前连续 = 从最后一条 ≤ today 的记录倒推的自然日连续,断更后**冻结不衰减**; 未来预勾只计入累计/最长/经验,不劫持当前连续。 - 等级 Lv.1 初心者 → Lv.10 星愿大师,累计经验阈值 0/100/300/600/1000/1500/2200/3000/4000/5200。 ### 5.4 模型工具(preset/tools.ts) 注册模式(全部 49 个工具一致): ```ts ctx.tools.register(defineTool({ name: 'check_in_task', description: '任务打卡。……', // 何时使用/语义边界写全,模型靠它决策 parameters: { // ValueSchemaSpec DSL;enum 表达枚举,不裸写 JSON Schema taskId: { type: 'string', required: true }, checkInDate: { type: 'string', description: 'yyyy-MM-dd,可选' }, }, output: TEXT_OUTPUT, // output.render 只产模型文本;UI 状态走会话事件 timeoutMs: 600_000, // HITL 等待类工具声明超时即承诺协作取消 async execute(args, exec) { // 写库 → agent?.session.append('xingyuan/*', 事件) → 回纯文本结论 }, })) ``` 规范要点: - 并发安全缺省 = 互斥;仅纯读工具显式标 `isConcurrencySafe: true`(generate_chart 这类「只追加相互独立的会话事件」的内部 recorder 同样适用——事件互不覆盖、可交换)。 - `execute` 必须观察 `exec.signal` 并转发给可中断的底层调用。 - ID 引用纪律写进每个工具描述:ID 必须取列表返回的真实值,模糊指代先查列表定位。 - 部分更新原则:update 类工具只接受用户明确提及的字段,未提及不传、禁止编造; 可选字段传**空字符串**表示清除(任务 hint/dueDate,愿望 描述/颜色键/预计完成日)。 - 业务写入统一走 `store.ts` 用例(createWish/updateWish/createTask/updateTask/打卡族/cascade), 工具面与 `/xingyuan/*` 路由动作共用同一收口——校验、级联、进度联动只有一份实现; 读侧(freshTask/freshWish/planForDay/图表)一律新鲜化,按钮态与写路径校验同口径。 - 内部工具约定:`generate_chart`、`batch_query_user_data` 为模型辅助查询, 成功结果经图表事件/汇总回包呈现;描述中注明回复口径约束。 ### 5.5 HITL 写确认(preset/hitl.ts) 语义矩阵(分层模型的完整决策记录见 §10 决策 8):总开关 `confirmWrites` **关闭**时,除锁定的删除类目外一律不弹卡(一键静音);**开启**时 按六个可配类目的明细开关 `confirmOps` 逐类决定: | 类目 | 覆盖操作 | 明细默认 | |---|---|---| | 创建 | 创建愿望/任务(含批量)+ 微行动拆解 | 确认 | | 打卡 / 取消打卡 | check_in_task / cancel_check_in_task | 确认 | | 领取 | claim_task(不可逆:锚点重算、误领取恢复=删除重建) | 免确认(用户指令即授权) | | 修改 | update_wish / update_task / rename_wish_category / set_wish_category_color | 免确认 | | 记忆保存 | save_memory / update_memory | 免确认 | | **删除(锁定)** | 删除愿望/任务/记忆(含批量)+ **清空全部记忆** + 微行动重开 | **始终确认,无开关** | | 不设门闩 | 微行动步进(口头即授权)、教练风格 / 用户画像 / 偏好设置(`update_xingyuan_settings`)——均属配置偏好;**安全策略本身(confirmWrites/confirmOps)不对 agent 开放** | — | 实现契约(dsh-user-questions 校验过的事实): - ask() 的 `plan-review` 意图要求 `detail` 携带被审阅的计划文本,缺 detail 抛 BAD_INTENT; 星愿的动作确认不是计划审批,**不挂意图标签**,走通用选项列表(answer 协议一致)。 - headless 无 live agent 时直接放行(一次指令跑完的语义);调用方没有可阻塞的确认 UI 时同样放行——确认卡是 Web GUI 交互面,无界面场景下任务文本本身即用户指令。 放行/拒绝口径(rc.2 `ask()` 校验顺序:CALLER_NOT_LIVE / DELEGATED_CALLER 先于 NO_PROVIDER 抛出):`NO_PROVIDER`(环境未注册 UI provider)与 `CALLER_NOT_LIVE` (调用方非注册表内精确 live 实例)**放行**——该调用方没有任何可阻塞的确认 UI, 拒绝也无法换路径执行;`DELEGATED_CALLER`(被其他 agent 拥有的委派实例,如会话 派生的 subagent 调用 preset 层工具)**fail-closed 并给出指引**(回主会话确认后由 主会话执行)——父 agent 有完整 UI,平台口径是「把未决问题写进子 agent 最终结果」, 拒绝可执行,且保住「删除始终确认」不因委派被静默绕过。 - `confirmWrites`/`confirmOps` 开关读取走 getter(设置热改后下一次 execute 立即生效, HMR 安全);受门控类目的工具描述保持「开与关都成立」的中性措辞(§5.8 静态描述约束)。 - **确认卡语言(confirmLang,默认 zh)**:平台事实(rc.2 实测)——宿主不向 host 侧 插件暴露用户界面语言(locale 服务是 client 半侧浏览器专属 seam,工具执行期读不到; ask() 载荷也无 i18n 字段)。因此确认卡卡头/按钮/问题文案的语言由对话偏好 `confirmLang` 显式选择(设置页提供 zh/en 两档,默认中文),不能自动 跟随界面语言。label 是 ask() 答案协议的匹配键:确认判定必须与渲染用同一份 label (hitl.ts CONFIRM_LABELS 成对维护)。上游若开放 locale seam 应回归自动跟随。 ### 5.6 会话事件与 UI 卡片(events.ts / client/) 星愿采用 **whole-value 单事件模式**:每条事件携带完整展示状态,一张事件一张卡片, 内部 id 直接用 `event.seq`,天然满足「每 (kind,id) 仅一条 start」的不变量, 无需 start/delta 配对与确定性拼接逻辑。(0.1.7-rc.1 起宿主已**不再强制**该不变量, 见 §11;本模式仍是首选,理由变成「一张事件一张卡」的简单性而非合规性。) 五个事件 kind(生产方 `events.ts` 声明合并,host 侧经 `exec.agent.session.append(kind, data)` 发出;卡片在同进程内存的会话里实时渲染,跨重启的回放依赖下述自愈机制): | kind | 载荷要点 | |---|---| | `xingyuan/wish` | op(created/updated/deleted) + 愿望快照(含 optional `pendingCount`/`settled` 派生位,旧事件回放如实降级) | | `xingyuan/task` | op + 任务快照 + 未来机会日预览(≤5 个) | | `xingyuan/checkin` | checked/cancelled + 任务名 + 日期 + 进度计数 | | `xingyuan/chart` | chartKey + title + chartType(line/column/bar/pie/arcbars/heatmap/radar) + 数据点 + generatedAt(快照时点,回放标注「生成于」) | | `xingyuan/micro` | started/stepped/restarted/finished + 步骤数组 + currentStepNumber | client 半侧注册三处声明合并位 + 一个 Definition 工厂(0.1.2-rc.1 起重排:`dsh-client-runtime` 包消失,`ClientContext` 即 cordis `Context`;`ctx.slots` 由 ui-renderer、`ctx.configForms` 由 ui-settings(0.1.7 起取代 `ctx.settingsScope`)、`ctx.sessions` 由 api-session-controller、 `ctx.locale` 由 dsh-client-locale 的类型声明合并位提供,client 源码以纯类型 import 引入): ```ts // src/client/index.ts —— 三个官方类型合并点缺一不可 declare module '@deepseek-ai/dsh-client-ui-chat/client' { // 0.1.2 起 ChatNodeDataMap 归 ui-chat interface ChatNodeDataMap { 'xy-wish': XyState; /* …共 5 个 */ } } declare module '@deepseek-ai/dsh-client-ui-conversation/client' { // ConversationStepDataMap 留在 ui-conversation interface ConversationStepDataMap { 'xy-wish': XyState; /* …共 5 个 */ } } // 第三处:LocaleNamespaceMap(i18n.ts 并入 '@deepseek-ai/dsh-client-ui-slots',位置不变) ``` 事件 Definition 注册面同步改名:旧 `ctx.conversationEvents.register(def)` → `ctx.uiConversation.events.register(def)`(`uiConversation` 服务由 ui-conversation 提供, `conversationEvents` 服务名已不存在)。Definition 按 `KIND_BY_EVENT` 把 `xingyuan/*` 映射到 `xy-*` kind,`match/start/update` 渲染器只读 `node.data`(whole-value),不扫描事件窗口。 注意:dsh 工具内建的 UI 卡片是固定五词汇(generic/terminal/diff/search/web), 不支持任意自定义 React 卡——业务卡片一律走 ConversationNode,人机确认走 userQuestions, 这是官方分工,不要试图扩展前者。 **client 行的声明语义(0.1.2-rc.1 迁移)**:`dsh.client.inject` 现为「依赖包行先到 + 条目组成边」(不再充当模块解析清单);基座模块(react / react-dom / cordis / ui-slots / ui-primitives / dsh-client-store)由壳 staticModules 表播种,client 源码里的**纯类型** import 会被 tsdown 擦除、不产生模块请求,无需声明 `external`——只有运行时 value import 非基座包才需要 `dsh.client.external`。业务插件行无需 `immediately`(entry compose 会 import 全部行、apply 正常执行);官方 ui-schedule 同款形态可作对照。 **冷读拒绝与会话日志自愈(session-log-repair.ts,必读)**:dsh 会话持久化读取端按 「本仓库生成的官方事件类型白名单」拒绝未知事件,仓库外插件事件按构造不在名单内; 写入端 `Session.append` 至今没有 ignorable 标记通道。规则按代际分三档(**当前格式 = v4**, 0.1.7 起;「哪一代是当前格式」由 `SESSION_FORMAT_VERSION` 驱动,而「ignorable 从哪一代起 被接受」是本模块另立的 `IGNORABLE_AWARE_FORMAT_VERSION = 3`——**两个数不是一回事, 升级时都要核对**,见下文分界): **当前格式工件**读取端认 `"ignorable": true`(带标事件安全跳过、数据原样保留); **v3 → v4 迁移边**已改为**接受** ignorable 未知事件——重命名为 `plugin:` 并保留 载荷(`dsh-session-format-v3-to-v4` 的 `namespaceV3OpaqueEvent` 在严格拒绝之前先放行); **v0→v1 / v1→v2 / v2→v3 三条边**仍拒绝一切未知历史事件,**ignorable 也不例外** (官方 alpha-historical-unknown-event-refusal 决策:迁移边无法证明不透明载荷里的 seq/生命周期引用仍然安全;拒绝时不产出后继、源文件原样保留)。bundle 层激活期 按工件版本执行**会话日志自愈**(工件名 `session.v{N}.jsonl[.zstd]`,v0 无版本段; 只处理目录内最高代,旧代被遮蔽时零写入)。**分界不是「是否当前代」,而是「该代迁移边 是否接受 ignorable」**(`IGNORABLE_AWARE_FORMAT_VERSION = 3`): - **v3 及以上的工件(含当前代)→ 补标模式**:给 `xingyuan/*` 事件行补 `"ignorable": true` 后原子写回,**事件载荷与坐标原封保留**——v3→v4 边会把带标未知事件重命名为 `plugin:xingyuan/*` 带进后继代,所以**升代后的 v3 旧工件仍属补标面**而不是去毒面。 但**别把「保留数据」读成「卡片还能回放」**:客户端 `KIND_BY_EVENT` 按裸 `xingyuan/*` 匹配,迁移后的 `plugin:` 前缀不命中,故跨代旧会话的卡片当下不再渲染 (停留在当前代、不再被迁移的工件才逐条回放)。补标的实义是**载荷仍在日志里**, 上游开放插件事件映射、或本插件登记前缀类型后即可即行恢复;去毒则是把载荷整个 换成惰性事件,**不可恢复**。 - **v2 及更早的工件 → 去毒模式**:把 `xingyuan/*` 事件行替换为官方惰性事件 `hook/invoked`(seq/time 不变,payload 满足各迁移边的语义校验),官方迁移链即可 安全穿过、会话照常打开;代价是该会话历史卡片事件不再回放(业务数据在 sqlite, 不丢失;备份保留原件)。 > **踩坑实录(2026-09-23)**:初版分界写成 `version === SESSION_FORMAT_VERSION`, > 宿主升到 v4 后所有 v3 工件当场改判为去毒目标——**下次启动即永久销毁**本可保留的 > 卡片事件。`patchPlaintext` 现同时校验「模式 × 工件版本」自洽,两侧走错一侧即跳过, > 并由 `test/session-log-repair.test.ts` 的 v3 组用例(含「官方链保留为 plugin: 前缀」 > 与「夹具合法性对照」)锁死。 安全边界:不含星愿事件的文件零写入;文件内非星愿行逐字节不变;改写前备份至 `~/.dsh/xingyuan/session-backups/`(每会话留 3 份);撕裂尾/坏帧/版本与工件名不符/ 解析失败/活会话一律整文件跳过。**增量跳过**:每个已处理会话在目录内写 `.xingyuan-repaired` 标记(记工件字节数 + 事件总数 + **工件格式代 + 当次处置模式**),下次启动 stat + 读 标记即可跳过,避免全量解压的启动拖慢(实际 <10ms)。**判定身份必须一起记**:处置方式由 「该代迁移边是否接受 ignorable」决定,而升宿主时按要求上下移 `IGNORABLE_AWARE_FORMAT_VERSION`—— 只比字节数时,边界移动后(或目录里换成另一代同名同尺寸工件)旧标记会把本该改判的文件 静默跳过;补标↔去毒判错的代价不对称(去毒不可恢复),故代次或模式不符即重扫, 旧格式标记(缺这两字段)一律视为过期重扫,重扫幂等,多花一次解压换判定正确。 工件字节数变化(新事件落盘) 或标记损坏时自动重扫并刷新。**压缩工件布局硬约束**:zstd 容器首帧必须恰好一行 header(dsh 的 `assertZstdHeaderFrame`/`listArtifacts` 启动期强制)——写回时按 「帧 0 = header、其余事件行第二帧」重建并自检,读入的容器首帧非单行(历史整文件 单帧的错误产物)即使无需补标也重写为合法布局。 升级 dsh 前先核对该模块头注的前提是否仍成立,尤其是**新迁移边是否接受 ignorable**: 接受则把 `IGNORABLE_AWARE_FORMAT_VERSION` 上移、去毒面收窄;拒绝则下移。判错方向代价 不对称——多去毒一次即永久销毁该会话卡片回放,少去毒一次则会话冷加载失败。若上游开放 ignorable 写入通道或插件事件注册面,整套自愈应回归官方机制并撤下补标/去毒。 ### 5.7 系统提示词与动态上下文(preset/prompts.ts) 静态分节 11 段(`ctx.systemPrompt.section({ name, order, text })`): identity(order 5)、capabilities/patterns/examples/constraints(101–104)、 wish-guide/task-guide/memory-guide/config-guide/chart-guide/reminder-guide(110–115)。 动态上下文三处(`ctx.systemPrompt.context({ name, order, text })`,text 为 provider 函数, 每次轮次求值): | name | order | 内容 | |---|---|---| | xingyuan:coach-tone | 20 | 按当前教练风格切换语气模板 | | xingyuan:memories | 21 | `` 块:画像 + 高/中重要性记忆(按 memoryInjectLimit 截断,超量标注可用 search_memory 检索) | | xingyuan:today | 22 | 开场「今日应打卡 N 条」概览(承诺口径:只统计已领取任务;未领取单列「可领取」并约束不得主动领取,见 §5.2 规则 7) | 关键行为准则已写入 identity/constraints:执行操作类工具后必须一句话明确告知结果; 预工具叙述在工具回合结束丢弃属于 dsh 轮次流程天然行为,无需额外处理。 ### 5.8 设置页(设置 → 星愿)与主行 volatile 配置 > **dsh 0.1.7 把 settings 子系统整体重写了**,本节按新模型写。旧模型(插件向宿主 > `installSection` 注册命名空间 + client 侧 `ctx.settingsScope.bind`)在 0.1.7 里 > **已彻底撤除、无兼容层**(全树 grep `installSection`/`settingsScope` 零命中)。 > 网上能找到的 0.1.5/0.1.6 时期资料一律不再适用。 新模型一句话:**「用户可改的设置」= 某条 profile 行的 `Config` schema 里标了 `.volatile()` 的字段**。宿主 `settings.describe()` 按**行 id** 把它们投影成表单 (非 volatile 字段根本不进表单),client 侧经 `ctx.configForms.get(行 id)` 读写。 设置整页由 client 半侧注册,页内五张面板卡的数据来源分两类: | 分节 | 数据源 | 位置 | |---|---|---| | 教练风格 / 昵称与画像 | 星愿数据库 global 单例,经 `/xingyuan/api/profile` | — | | 写操作确认 / 对话偏好(记忆注入上限 / 确认卡语言) | 主行 `xy-bundle` 的 volatile 字段 | 字段表 `src/pref-settings.ts` | | 标签页显示 | 同上(同一张表单的另两个字段) | 字段表 `src/ui-settings.ts` | - **偏好为什么挂 bundle 主行而不是 preset 层(结论未变,理由换了形态)**:设置整页常驻 可见,而 preset 挂载是**懒加载**的——重启后、开过星愿会话之前那一层并不存在。旧模型靠 「命名空间注册在 bundle 层」达成常驻;新模型按行 id 索引表单,主行天然常驻,效果相同。 判定口径不变:作用域属「单次会话的能力」→ preset 层;属「用户的全局偏好」→ bundle 常驻行。 **preset 层的 Config 不得出现任何 volatile 字段**(`test/pref-settings.test.ts` 机械锁定)。 - **`SETTINGS_ENTRY_ID`(`src/pref-policy.ts`,值 `'xy-bundle'`)与 `cordis.patch.yml` 的主行 `id:` 是一处纯字符串耦合**:宿主按行 id 索引表单,对不上时整页经 `configForms.whileServed([行 id])` **根本不注册**——设置里连「星愿」这一项都不出现 (含教练风格/画像两张走数据库的卡也一起消失),且无任何报错。这比 0.1.7 前「页面在、 显示未就绪」更安静,故与 §5.11 那次静默失效归为同一类坑,由测试对拍锁死。 主行 id 另受 §4 硬约束 1(不得等于预设身份 `xingyuan`)限制。 - **volatile 的语义收益**:改这些字段**不触发整行重启**(宿主走 `loader/volatile-update` 原地换引用),host 侧读当前值须 `.get()`;`readPrefSettings(config)` 每次调用现取, 工具与提示词下一次执行即生效。反过来,非 volatile 的技术参数(rangeDefaultDays 等 4 项) 改了会重启本行,且**不会出现在设置页里**。 - **漏标 `.volatile()` 即字段从表单静默消失**(宿主 `volatileForm()` 只挑 volatile 子节点)。 新增偏好必须同时进字段表与主行 Config——`test/pref-settings.test.ts` 逐字段断言 `meta.volatile === true`,忘了标就红。 - **整页注册改用 `configForms.whileServed([行 id], ...)`**(0.1.7 新增接缝):该行没被 组装出来时整页不留痕迹。旧模型没有这层保护、失败呈现全靠注册方自己判(当年 `settings.plugin.item` 卡片才有自动显隐),现在官方给了正规接缝,外层仍套 `ctx.effect`。 - **写后校验的绕路已撤**:`ConfigForm.set()` 现在返回 `Promise`(true = 宿主接受; false = 被拒或被跳过;传输失败才 reject),取代旧 scope「失败也是 resolve」的坑。 写队列与取代栅栏(`writeGeneration`/`pendingRevision`)归**宿主表单实例**所有 (同一行 id 只一个实例),组件里不再自写 `verifyWritten`/写序号,只留各控件的 pending 乐观态。见 `src/client/pages/settings.ts` 的 `writePref`。 - **用户偏好的存储位置变了**:不再写 `~/.dsh/settings.yaml`(宿主已弃用该文件并改名 `settings.yaml.imported`),改写 **`~/.dsh/profiles//cordis.patch.yml`** 里 对应行的 `config:` 节。后果一:**备份口径**——业务数据仍在 `~/.dsh/xingyuan/`, 但「偏好」现在属于 profile 文档;后果二:升级到 0.1.7 时旧星愿两节因无对应 Loader 行 而**未被导入**,用户此前自定义的偏好重置为 schema 默认(2026-09-23 实测确认)。 - `ctx.settings.configure({ auto: false }, ctx.fiber)`:本插件自带整页,关掉宿主对这一行的 自动生成页(官方惯例,`dsh-client-locale` 等同款)。经 `ctx.inject(['settings'], inv => …)` 等 settings 服务就绪后调用。 - **`ctx.inject` 即使依赖已就绪也在后续微任务才回调**(实测,仍然成立):注册类断言 须 await 一拍,不能写同步断言。 - 官方限制:设置卡本质是 schemastery 表单,无自定义按钮——引导闭环必须 chat-first, 复杂交互(应用内确认对话框/toast)做在 client 半侧 `ui.ts`。 - **工具描述是注册时烘焙的静态字符串**:dsh-tools 校验 `description` 必须是 string (不支持 getter),改设置也不会重建描述。故凡受设置开关门控的行为,描述里**一律 不可断言**其发生——只能写成"开与关都成立"的措辞(如 `CREATE_NOTE`),否则模型会 向用户宣称做了实际没做的事。删除类始终确认,不受门控,可以断言(见 tools.ts 组注释)。 ### 5.9 HTTP 路由(routes/) `webServer.register({ kind: 'prefix', path: '/xingyuan' })` 统一分发: - GET 页面(自包含 HTML,无 GUI 场景 URL 直开):`/`、`/today`、`/calendar`、`/growth` - GET 数据:overview / day / range / calendar / growth / wishes / tasks / profile / memories / task-detail / categories - POST 动作:checkin / cancel-checkin / claim / profile / memory-add / memory-delete / memory-clear / create-wish / create-task / delete-task / delete-wish / category-rename / category-color 错误协议:`HttpError` 按状态码直出;`ActionError` 返回 400 携带 `error/code/params` (code 供客户端本地化);其余 Error 按 400 返回领域校验消息,带稳定 code 的领域错误 原样透传。请求体上限 64KB。 **跨站写闸门(0.6.5 加固)**:所有 `POST /xingyuan/api/action/*` 必须带自定义头 `x-xingyuan-write: 1`,缺失即 403 + code `write_gate_required`(**先于请求体解析**, 未授权者连 413/400 都探不到)。理由:dsh 的 web 服务监听本机端口,而浏览器发跨源 「简单请求」(form / text-plain)**不触发 CORS 预检**即可打到写动作——宿主不给这类 响应发 CORS 头,攻击者读不到回包但副作用已发生;`/api/action/memory-clear` 是 零必填字段的破坏性动作,因此不能只靠「id 是随机 UUID 猜不到」兜底。自定义头强制 预检、宿主不放行即挡在门外;两个客户端面(GUI 的 `src/client/api.ts` 与直开备用页 `pages-html.ts` 的内联 `post()`)都显式带头,头名三处字面量由 `test/routes-shell.test.ts` 对拍防漂移。不校验 Referer(可伪造,代理/隐私模式失真)。 ### 5.10 客户端半侧开发纪律(视觉与验证踩坑沉淀) **activeLocale 依赖快照 `active` 字段**:语言判定经 `ctx.locale.getSnapshot().active`。 0.1.7-rc.1 复核更正本仓库旧记录:该字段**在 `@deepseek-ai/dsh-client-locale` 的发布类型里 就有**(`LocaleSnapshot.active: LocaleId`,`lib/types/client/index.d.ts:49-51`), 「不在类型面内」只对 `@deepseek-ai/dsh-client-ui-slots` 那份窄 `LocaleFace` 成立—— `i18n.ts` 的 `as { active?: unknown }` 兜底因此是多余的保守(保留无害,可在下次触碰该文件时 收窄为直接读类型)。en 格式化链路(Intl 日期/复数/标点分隔符)全部经 `activeLocale()`, 该字段缺席时静默回落 zh——升级 dsh 后若发现「英文正文配中文日期」即此处前提失效。 **API 请求 URL 拼接**:带 query 参数的请求一律 `URLSearchParams` 统一构造, 禁止手工字符串拼接;同资源的多条取数路径(首屏 / 加载更多)必须共用同一个 URL 构造函数。背景:记忆页搜索曾把 URL 拼成 `?q=词?offset=0`(第二个分隔符 误用 `?`),服务端把「词?offset=0」整体当关键词,用户症状是「搜索永远为空」; 现由 `memoryListUrl()` / `dayUrl(日期)` / `calendarUrl(月份)`(client 侧 `src/client/api.ts`)收口同资源多路径取数 + `test/client-pages.test.ts` 回归锁死; 单路径取数(task-detail / range 等)一律内联 `URLSearchParams` 构造;备用页 `pages-html.ts` 内联页各自持一份 `apiUrl(path, params)` 等价构造。 **任务详情操作区**(`detail.ts`,愿望页/任务页共用):按钮单行成组,顺序 = 主操作(打卡/领取)→ 条件动作(取消打卡)→ 辅助(让 AI 总结)→ 危险(删除); 危险删除不用 `margin-left:auto` 右漂——窄面板里是一段突兀空白,愿望卡内还会与 卡头的愿望级删除同侧对齐造成语义混淆,靠 danger 描边 + 确认弹窗区分即可; 不重复 TaskLine 行已展示的元信息(cycle/duration 等);详情底部恒留 8px 呼吸 (`xy-taskrow` 场景覆盖为 2px,分组卡行自带 11px 内边距),操作按钮不得与 下一条任务的分割线贴死;详情每段「标签 → 内容」固定纵向节奏 (`.xy-detail>div` gap 5px,段间 10px)。 **打卡记录周对齐网格**(`detail.ts`):窗口=**截至今日的机会日末 28 格 + 全部已打卡 日期并入**(预勾未来日、once 无截止日/无截止日周期任务的机会日序列外打卡都是 既成事实,序列为空时网格即打卡历史、详情页撤销入口依赖它;未勾选的未来日归 「接下来的机会日」预览;整条序列直出曾把客户端 `slice(-28)` 推向未来尾部、 今天的打卡反而不可见),按周一始 7 列 CSS Grid 排布 (与日历页表头同构,GitHub/Streaks 惯例)——首格前按星期序补 `.xy-dcell-blank` 隐形占位(真实数据按机会日稀疏返回,错位是常态),今日格加 `xy-dcell-today` accent 环(与日历圆章 today 环同一语法);禁改回流式 flex-wrap(换行参差、 连续性不可读)。读屏口径不变:格子 aria-hidden + 容器 role=img 一句汇总。 撤销打卡的日期必须**显式随请求携带**:详情页=网格内最近一条打卡(含预勾, `latestCheckedDate`),今日页=当天;弹框文案与实际撤销对象同源(服务端 「不传日期=撤最近一次」的隐式口径不得再被页面依赖)。 **行内低频动作图标化**:长列表里重复出现的编辑/删除用 `.xy-btn-icon` 图标幽灵键 (26px 方形命中目标,线稿 SVG 走 `ui.ts` 的 `IconEdit`/`IconTrash`——勿再加 emoji 或新引图标库;新增功能图标优先复用线稿语言扩展 ui.ts);文字语义全部 由 aria-label「动作 · 条目名」承担。适用:愿望卡头删除、记忆行编辑/删除。 详情操作区的「删除」保持文字按钮不变(与主操作并排成组,非列表重复场景)。 **元信息冗余裁剪**:TaskLine 的状态词仅在无旁证时保留(已完结)——进行中/ 待领取的状态由同行的打卡/领取按钮自解释,任务页还叠加状态分组头;同理 成长页统计仅「当前连续」用强调卡突出(streak 是习惯类 App 的英雄指标), 其余统计数字用主文字色;缺省值显示本地化文案(`growth.stat.none`)且走 `xy-statcard-muted` 弱化,不用「—」占位。 **展示性日期一律 Intl 本地化**(`format.ts` 三档,禁在界面裸奔 ISO): 短格式 `formatShortDate`(按钮/密集元信息/机会日列表)、友好全称 `formatFriendlyDate`(今日页标题、日历面板头,含星期)、中格式 `formatMediumDate`(愿望预计完成日等远期目标,带年份);ISO 仅保留给 确认弹窗文案与读屏 aria(精度优先)。en 语序差异由 helper 内部处理。 **日历视觉语法**:无边框连续网格 + 日期圆章(`.xy-daynum`)承载状态—— 待打卡=中性计划底(高频状态不作警示色)、部分完成=柔和琥珀、全部完成=柔和绿; today=圆章蓝环(不覆盖状态底色,「今天该打卡」的提示不能被吃掉)、选中=圆章 实底(短暂即时反馈);邻月补位=低透明度数字;月份导航成组居中,「回到本月」 贴右缘(≤520px 媒体查询改随流避免相撞)。改日历先读 `styles.ts`「日历」段注释。 **视觉改版验证回路**:客户端 React 页面无法脱离 dsh 壳运行,视觉改动不接受 「改完即完事」——用 `npx vite-node debug/gen-mock.ts` 生成深/浅两主题静态 mock (真实 `STYLE_TEXT` + 手写镜像 DOM,含日历/愿望卡/任务卡三个场景),本地静态 服务 + 浏览器截图核对后才算完成。mock 的 markup 是 tsx 输出的手写镜像,仅用于 视觉核对;对应组件结构变更时必须同步 `debug/gen-mock.ts`。兼容铁律由 `style-contract.test.ts` 机械锁定(见 §9),mock 源码同受检查。 **交互闭环沉淀(2026-08 评审批)**:装饰性 hover 一律门控在 `(hover:hover) and (pointer:fine)`(触屏粘滞);过渡只用 transform/opacity (width/height/left 会逐帧布局);视图页挂载统一 `useScrollTopOnMount()` 回顶, 跨标签切换要保留的交互态(记忆搜索词/日历月份偏移/展开集合)写 `view-state.ts` 快照;弹窗打开锁 body 滚动、错误 toast 走独立 assertive 容器; 删除/撤销成功后行 DOM 随列表刷新销毁——焦点交给 `focusPageTitle()` 兜底; 「动作成功但刷新失败」降级为 `StaleBanner`(旧数据 + 就地重试),不整页翻错屏。 **日历页默认展开今天**(2026-08 登记):首载/「回到本月」(offset=0)时自动 `pick(today, false)` 展开今天详情(不滚动);任何手动拾取后置位 autoPickRef, 同一次挂载内不再自动覆写;跨标签切换的视图状态只保留月份偏移(选中日不保留), 回来时仍默认展开今天——属既定交互行为,勿当缺陷报。 **设置页行语法(2026-09 改版沉淀)**:开关/枚举类设置用 row 语法(`.xy-setrow`: 标签+说明在左、控件在右、`.xy-setrows` hairline 分隔,与业界设置页同构),表单类 字段保持 label 在上纵排——两类惯例不混用;游离于卡片外的说明行经 `.xy-settings>.xy-hint` 补 panel 同款横向内边距,不得贴容器左缘悬在列外。 禁用必须可见:自定义 `xy-toggle`/`xy-seg-btn` 不吃原生禁用变灰,`:disabled` 置灰需 显式写(只读命名空间/写入在途的控件不得看起来仍可交互);锁定类目用「始终开启」 徽章而非禁用开关(禁用看着像坏了,徽章即状态)。 **CSS 兼容铁律**:客户端样式禁用 `color-mix()` 等新式取色函数——dsh 壳的浏览器 矩阵里存在不支持的环境,整条声明按无效处理(空态 SVG 线稿曾因此整体隐形、只剩 accent 点缀孤点,形似渲染事故)。半透明衍生色一律在 styles.ts 令牌区按主题写 显式 rgba(`--xyd-*-border/ring/hatch/hover` 对);JS 内联渐变需要变暗档时用 `darkenHex` 预混(growth.ts)。空态/错误态是纯文字版式(PageEmpty/PageError 无插画),重新引入装饰性元素前先确认目标环境支持面。 **本地部署验证**:profile 内安装副本是**实体拷贝**(pnpm 装的 npm 包,非软链), 仓库改动不会自动生效。开发自验回路:`pnpm build` → 把 `lib/`(动了包声明再加 `package.json`)覆盖到 `~/.dsh/profiles/web/node_modules/@starwish-ai/xingyuan-dsh/` → 重启 `dsh web` → 浏览器强刷(Ctrl+F5,client JS 有缓存)。注意: `dsh plugin add/update` 会用 npm 版覆盖该副本;数据无虞,库固定在 `~/.dsh/xingyuan/`(§4 硬约束 2)。 > **踩坑实录(2026-09-29)**:发新版后用户执行 `dsh plugin --profile web update > @starwish-ai/xingyuan-dsh` **不报错但版本没变**,「Agent 预设」里依然没有星愿。三层原因叠加: > ① profile 的 `package.json` 把依赖**精确钉死**(当时是 `"0.6.5-alpha.1"`),pnpm 对精确 pin > 的 `update` 就是不动 → 命令成功、副本仍是旧版、旧 peer 仍被闸门跳过(§11 首条症状); > ② 本机 npm registry 默认是 `registry.npmmirror.com`,**新发布版本会滞后**(当时镜像仍停在 > latest 0.6.5);③ 装对了也要**重启 `dsh web`**——在跑的进程用的是旧组合结果,不热更新。 > 正确写法:`dsh plugin --profile web add @starwish-ai/xingyuan-dsh@ > --registry=https://registry.npmjs.org`。取证口径:`node -p` 读安装副本的 `version` 与 > `peerDependencies`,再 `dsh --profile web --dump-config` 看 `xingyuan-sqlite`/`xy-bundle`/ > `preset-xingyuan` 三行在不在(stderr 出现 `skipping profile bundle "@starwish-ai/xingyuan-dsh"` > 即仍被拦)。**别把 `dsh plugin update` 当升级手段**——精确 pin 下它是 no-op。 **宿主官方实践规则(0.1.7-rc.1 起随包发布,星愿按此自查)**: `@deepseek-ai/dsh-agent-preset/skills/cordis-plugin-development/references/practices.md` (rc.1 新增)+ 同目录 `ui-plugin.md` / `verification.md` / `SKILL.md` 的验收清单。 星愿**已合规**的硬项:client 产物运行时只 `require('react')`,**不以任何模块方式加载宿主 Client 包**(practices.md 的头号禁令;`dsh.client.inject` 条目是官方允许的激活顺序声明); `styles.ts` 引用宿主 `--dsw-alias-*` 令牌(用到的令牌在宿主全部存在,浅/深成对由 `style-contract.test.ts` 锁);`pages-html.ts` 的直开页**不是**被 iframe 嵌进 GUI 的宿主 HTML (全仓 `iframe` 零命中),是另一条无 GUI 场景的备用出口。 **有意偏离、留痕待议**的两处:① `src/client/ui.ts` 的 toast 与确认框是命令式 `document.body.append`,而 practices.md 要求「不在组件外写 DOM / 不 append 到 document.body」, 官方落点是 `shell.overlay` 槽(宿主自己的重命名对话框、终端清理提示都渲染在那里)—— 现方案能跑且经 `ctx.effect` 清理,风险是 z-index/焦点与宿主 overlay 抢层,改造需把两者 重写成 React 组件;② `src/client/hooks.ts` 沿祖先链最多 16 层写行内 `scrollbarGutter`、 并用 `querySelector('.xy-page')` 找滚动容器(治滚动条 15px 抖动与跨标签残留滚动位), 同属「在组件外写 DOM」,宿主结构一变即失效。**验证口径**:官方 `verification.md` 明写「mock 页截图不是对运行插件的验证」,故 §5.10 的 `debug/gen-mock.ts` 回路只能当离线第一道核对(且其替身令牌值与宿主真值有出入),改版的最终结论必须在 真连的 dsh 页面里按 SKILL.md 清单核对(浅/深两主题、并排宿主同类页、控制台无 `slot entry crashed`)。 **0.2.0-rc.1 升版后的自查(2026-09-29,随包 skills 增量)**: - 新参考 `references/user-actions.md`(「一个操作两个调用方」):同一操作只实现一次、 UI 与工具共用同一个宿主服务方法;「把插件 UI 对应用数据与配置的操作暴露给 agent」; 「授予/放宽权限的动作保持用户专属」。星愿 §5.4 的 `store.ts` 共用收口正是要求。 **13 个页面动作现已全部有等价工具**(0.6.7 补最后三处): ① `category-color` → `set_wish_category_color`(顺带把原先只写在 `routes/api.ts` 里的 覆盖写路径上移为 `store.setCategoryColor`——上移前路由只截断不校验,`name:''` 会写出 键为空串的幽灵覆盖、从此甩不掉);② `memory-clear` → `clear_all_memories`(锁定删除 类目,始终确认;空库不弹卡);③ 四项非权限偏好(记忆注入上限 / 确认卡语言 / 标签页显隐 / 单标签勾选)→ `get_xingyuan_settings` + `update_xingyuan_settings`, 经 bundle 主行的宿主 `settings.update` 落到 profile 补丁,**写后读回比对**: 宿主没接受就报 `pref_write_rejected`,绝不允许「说改好了而实际没改」。 `confirmWrites`/`confirmOps` **刻意不进工具参数**——同一文档明写「授予或确认权限的 动作保持用户专属」,放宽安全策略只能由本人在设置页改(该边界由测试锁住)。 - 新技能 `agent-experience/SKILL.md`(模型面内容纪律:描述行为而非实现、每条事实只说一次、 参数规则挂在该参数上、改完量首轮 prompt token)。本包 49 个工具描述里重复的「ID 取列表 真实值」纪律与内部机制注记属被点名对象——属可选瘦身,不影响兼容性,记此备查。 - SKILL.md 另加硬约束:**不得手改 profile 的 `package.json`/`cordis.patch.yml`、不得在 profile 目录跑 pnpm**(官方落点是 `install_bundle`);验收以 installation 结果的 `application`/`warnings` 字段为准(而非日志、终端输出、进程列表、页面 boot payload), 新行用 `cordis_inspect_query` 确认,**测试中改过的用户设置要恢复**。 与本节「本地部署验证」的手拷 `lib/` 回路存在张力:那是仓库自验手段、只覆盖安装副本内容, 不动 profile 依赖与补丁文档(偏好那行是宿主写的),正式交付仍走 `dsh plugin`——明知偏离, 留此说明。 ### 5.11 标签页显隐(设置 × 会话预设动态注册) 六个会话视图标签不再无条件常驻:默认**跟随会话预设**(仅星愿预设的会话显示; 读取口径 = 会话列表快照里「当前会话」那行的 `projectionValues.agentPreset === 'xingyuan'`,官方 AgentPresetLabel 组件同口径),设置可切「始终显示 / 始终隐藏」并按标签勾选。 - **「当前会话」在宿主换过两次位置**(`src/tab-policy.ts` 的 `currentSessionIsXingyuan` 是唯一收口):0.1.2-rc.1 撤除 `SessionSummary.agentPreset` 顶层字段 → 改读 `projectionValues.agentPreset`;**0.1.6 撤除列表快照的 `current` (选中路 id)** → 改读行上的 `retainedBy.mainView` 引用计数(>0 = 主视图正在看它, 多栏可命中多行,取任一)。基线越过 0.1.6 后 `current` 那一支已无宿主会产生, 故代码里**只留计数一支**(不留兼容分支——client 半侧已硬依赖 0.1.7 的 `configForms`, 旧宿主整半侧本就起不来,留分支等于养死代码)。 > **踩坑实录(2026-09-22)**:`current` 被撤除后本插件**编译全绿、运行静默失效**—— > 判定恒为「没有当前会话」,`follow` 模式下六个标签一个都不注册,且没有任何报错。 > 三层原因叠加:①仓库类型基线仍锁旧版,编译面里 `current` 依然存在; > ②星愿读的是自己手写的结构收窄视图(可选属性),字段缺席合法; > ③取不到会话与取到但不是星愿在代码里是同一个 `false`。 > 结论:**宿主换代只能靠真机验证,不能靠 typecheck**(§5.10 本地部署验证回路); > 结构收窄视图必须照宿主真实形状写(`retainedBy` 在宿主类型里是必填)并配对应用例。 - **判定唯一口径**:`src/tab-policy.ts` 的 `visibleTabIds(mode, hiddenTabs, isXingyuanSession)` 纯函数——注册控制器、设置页回显、单测三方共用,禁止另写判定。 三态语义:`follow` 星愿会话才显示 / `show` 任何会话都显示 / `hide` 任何会话不显示; `hiddenTabs` 在上述「显示」前提下剔除单标签(默认 `[]` = 全显示;脏值容错忽略)。 - **`ctx.sessions` 撞名,只能按事实面断言**:`dsh-session` 把 host 侧 `SessionStore` 合并进 `Context.sessions`,`dsh-api-session-controller` 又把 client 侧 `ISessions` 合并进同一个键;两半侧类型同在本仓库编译面时解析结果是 **host 那个**, `ctx.sessions.list` 的类型因此是错的(`() => Session[]`)。故 tab-visibility 经 `ctx.get('sessions') as unknown as { list }` 收窄到 `SessionListFacts`。 服务依赖已在 `src/client/index.ts` 的 `inject` 显式声明(含 `sessions`), 不再依赖 ui-conversation 传递带来的组成边;`dsh.client.inject` 同步补上 `@deepseek-ai/dsh-api-session-controller` 行。控制器内不再写逐级可选链—— 声明了即必已就绪,可选链只会把故障变成沉默。 - **显隐设置的落点**:主行 `xy-bundle` 的两个 volatile 字段 `tabVisibilityMode` + `hiddenTabs`(字段表 `src/ui-settings.ts`,默认值与 `tab-policy` 的 `TAB_VISIBILITY_DEFAULTS` 同源),client 侧经 `ctx.configForms.get(SETTINGS_ENTRY_ID)` 读写——与设置页其它偏好同一张表单、 同一个宿主表单实例(0.1.7 前是独立命名空间 `xingyuan-ui` + `settingsScope.bind`, 该 API 已撤除,见 §5.8)。挂在常驻主行而非 preset 层的理由不变:未选星愿也能调, 且「全部隐藏」状态下开关仍可达。 - **注册机制**(`src/client/tab-visibility.ts`):`conversation.view` 标签环按 「全部已注册 entries」投影标签、无 per-session 过滤,故按会话显隐只能动态维护 注册表——控制器订阅「设置快照 × sessions 列表快照」,任一变化时 dispose 旧组、 按策略 register 应显示的组(标签环对槽版本号订阅自动重投影;切换瞬间至多一帧 旧标签,壳 `resolveActiveView` 对被注销的活跃视图回落 Chat,不渲染空白)。 必须在 `slots.inject('conversation.view')` 回调内安装:保证首次 sync 时槽已声明。 六个 entry 的 id/order(21–26)/label 与旧静态注册一致。 - **今日页轻提示**(`src/client/tab-hint.ts` + `today.ts`):仅「始终显示 × 非星愿 会话」时,今日页概览卡下一行提示(页面可浏览/操作,对话能力受限);控制器写值、 页面订阅,值未变化零通知。 - **不受门控**:5 类对话卡片(非星愿会话无 xingyuan/* 事件天然惰性)、设置页本体 (控制中心永远可达)、直开 URL `/xingyuan/*`(独立页面)。 - 测试:`test/tab-policy.test.ts` 对拍策略全分支(三态 × 会话 × 勾选 × 脏值)。 ## 6. 工具清单(49 个) 改动工具面时对照此表增删(新增务必同步 prompts.ts 能力段落与 README 功能列表)。 **愿望(14)** | 工具 | 说明 | |---|---| | check_similar_wishes | 创建前相似愿望查重(防重复核心体验) | | create_wish_with_tasks / create_wish | 创建愿望;前者附推荐任务(默认 3 个,确认卡可删减) | | get_wish_list / search_wishes / get_latest_wish / get_wish_detail | 读操作,并发安全 | | list_wish_categories / list_wish_category_color_keys | 分类与 22 色键枚举 | | update_wish / rename_wish_category | 部分更新;改名联动迁移颜色覆盖键 | | set_wish_category_color | 分类默认色(写 global 覆盖,与页面「分类管理」同一收口);空串清除 | | delete_wish / batch_delete_wishes | 删除(始终确认;级联清理下属任务/打卡/微行动/颜色覆盖) | **任务(16)** | 工具 | 说明 | |---|---| | check_similar_tasks | 相似任务查重 | | create_task / batch_create_tasks | 创建;批量联动愿望进度 | | get_task_list / get_today_unchecked_tasks / get_today_checked_tasks / get_tasks_for_date / get_tasks_for_next_days / get_tasks_for_date_range / get_recommended_tasks | 各时间窗查询与推荐;get_today_unchecked_tasks 为承诺口径(只列已领取待打卡,未领取单列可领取,见 §5.2 规则 7) | | claim_task | 领取(锚点日变为领取日,联动愿望进度) | | update_task | 部分更新 | | check_in_task / cancel_check_in_task | 打卡/撤卡(§5.2 语义) | | delete_task / batch_delete_tasks | 删除(始终确认,级联清理打卡与微行动) | **微行动(3)** | 工具 | 说明 | |---|---| | start_micro_action | 拆解 3–7 小步 | | complete_micro_step | 完成/跳过当前步 | | restart_micro_action | 重开(需确认) | **记忆(7)** save_memory / update_memory / search_memory / get_memory / get_all_memories / delete_memory / clear_all_memories(清空全部,**始终确认**;空库直接回不弹卡) **用户配置(6)** get_coach_config / update_coach_style / get_profile / update_profile(免确认,即时入 global 槽) + get_xingyuan_settings / update_xingyuan_settings(读全部偏好;写只放开 memoryInjectLimit / confirmLang / tabVisibilityMode / hiddenTabs 四项, **confirmWrites 与 confirmOps 不进参数**——放宽安全策略只能用户本人在设置页改,见 §5.10) **统计(3,其中两个为内部工具)** generate_chart(chartKey 15 选 1)、batch_query_user_data(全量汇总 + truncated 标注)、 get_growth_stats(等级/经验/连续/达成) ## 7. 图表 chartKey 枚举(15 种,勿随意改名) `checkinTrend / checkinCalendar / taskCompletionRate / checkinRateTrend / weekComparison / taskStatus / checkinByCategory / wishCategory / checkinRanking / taskDistribution / wishProgress / wishAchievement / continuousCheckin / checkinTimeDistribution / weeklyActivity` 数据全部由 storageDomain 现算(`charts.ts`),卡片经 `xingyuan/chart` 事件渲染; 趋势类默认 14 天窗、分布类 30 天、上限 90,均可配(§8 配置面)。 统计口径(改图表前必读,`test/charts.test.ts` 锁定): - **分桶类**图表只统计今天(含)以前的打卡,**未来预勾不进任何统计桶**(weekComparison 曾把 下周预勾按其星期几错算进本周柱);例外是日历热力图(逐日记录而非聚合统计); - **比率类图表(wishProgress / taskCompletionRate / wishAchievement)不在此列**:它们经 `claimedDaysSum` / `freshWishes` 取 `completedDays`,而该计数**含未来预勾**(承诺口径, §5.2 规则 3「预勾 = 承诺当天完成」)。这是**有意的**——比率与愿望页、`xingyuan/wish` 事件卡、工具回包共用 `wishProgressFromAgg` 一份公式(§5.2「展示派生位单一来源」), 只在图表侧加「今日上界」会让同一愿望在三处显示三个数。要收窄就上移改公式并同步 §10 决策 11 与 `test/charts.test.ts`、`test/store.test.ts`,不得在图表侧另写一遍; - checkinRateTrend 的**无安排日产出 inactive 空槽而非 0%**(缺失≠零惯例; `XingyuanChartDatum.inactive` 为 optional 字段,渲染器跳过画柱、悬停/读屏报「无安排」); - taskCompletionRate 与 wishProgress 均为承诺口径:分母只计已领取任务的应打天数(与愿望 进度同一公式族,2026-09 修订),候选不计入也不拖累;副标题恒标注「已领取任务」, 达成判定 = 进度 100% 且无候选(候选拦达成,`wishProgressFromAgg` 单一公式); - 图表事件携带 `generatedAt`:图表卡是生成时刻的冻结快照(whole-value),回放时卡片标注 「生成于」,避免历史会话旧图被误读为当前数据。 ## 8. 可配置项一览 配置哲学:无硬编码可调参数,默认值全部写进同名 schemastery schema(加载期校验失败响亮报错)。 | 来源 | 字段(默认值) | |---|---| | bundle 主行 Config | rangeDefaultDays(7)、rangeMaxDays(31)、memoryListLimit(500)、repairSessionLogs(true) | | preset side Config(无 Web 设置界面,仅组合层可调) | batchWishLimit(50)、batchTaskLimit(100)、chartTrendDays(14)、chartDistributionDays(30)、chartMaxDays(90)、chartRankLimit(10)、chartRankMax(20) | | bundle 主行 `xy-bundle` 的 volatile 字段(Web 设置页「写操作确认」「对话偏好」「标签页显示」三卡) | confirmWrites(true)、confirmOps(六类目明细,默认=分层化前矩阵:创建/打卡/取消确认,领取/修改/记忆保存不确认)、memoryInjectLimit(40,5-200 整数,`step(1)` 让服务端也拒绝小数)、confirmLang('zh',可选 zh/en)、tabVisibilityMode(follow)、hiddenTabs([])——字段表在 pref-settings.ts / ui-settings.ts,行 id 绑定见 §5.8,显隐语义见 §5.11。**对话侧可改的只有后四项**(`update_xingyuan_settings`,写后读回比对);confirmWrites/confirmOps 属放宽安全策略,保持设置页用户专属 | 配置变更的生效路径分两条:**volatile 字段**(上表第三行)走宿主 `loader/volatile-update` 原地换引用,不重启本行,读取端每次 `.get()` 现取;**非 volatile 字段**改了触发整行 重启(HMR 热替换)。两条路径都不做跨重载的模块级单例状态。 ## 9. 构建、测试与发布 ```sh pnpm install pnpm build # tsc + tsdown,产出 lib/(集成测试依赖它,先 build 后 test) pnpm typecheck pnpm test # vitest run ``` 测试分层(`test/`): - **loader 级组合启动**:真实 cordis Loader 加载完整组合,断言工具注册、落库与回包 (产品可见插件的门禁要求,手动 ctx.plugin() 不算数);每个注册贡献附带 dispose 断言。 - **机会日对拍**:daily/weekly/monthly/deadline、月末钳制、乱序补卡等边界用例。 - **包装完整性门禁**:package.json exports 子路径与 dsh.bundle.patch 声明的每个目标 文件必须存在于 lib/ 产物(`package-exports.test.ts`;./routes 曾指向不存在的 lib/routes.js,外部子路径导入会失败而宿主运行时不走该子路径,故静默)。 通配目标(`./locale/*.json`)按「目录内每个实文件都被模式覆盖」判定,不按字面路径查存在。 - **宿主版本闸门对拍**(`host-baseline.test.ts`):声明面 ↔ 钉版 ↔ 实装三面互锁——每个 `@deepseek-ai/dsh*` peer 的范围必须被 devDependencies 钉住的那一版满足、node_modules 实装版本必须等于钉版(否则 typecheck 校的不是用户跑的那一版)、补丁 `name:` 与 `dsh.client.inject` 点名的宿主包必须全部进了 peer(漏一个就不受 rc.1 闸门保护)、 且不得虚标(每个 peer 需有源码 import / 补丁点名 / 对应服务键三选一的证据)。 0.1.7-rc.1 的闸门只认**声明出来的** peer,这张表就是「覆盖面 = 真实依赖面」的证明。 另两面锁「**多版本适配**是声明不是口头」:`SUPPORTED_DSH_VERSIONS`(现为 `0.1.7-rc.1 / 0.2.0-rc.1`)里**每一条**宿主线都必须被全部 peer 范围以宿主同判定 (`semver.satisfies(线, 范围, {includePrerelease:true})`)放行——0.6.5 只写一条线, 用户升 0.2.0-rc.1 后整包被跳过、真机表现为星愿彻底消失;同时钉版必须落在该列表内 (防列表与实装脱钩),且**未核对的下一代(`0.3.0-rc.1`)必须被拦住** (放开范围=无声承诺,加线要先核对再改这张表)。 - **提示词对宿主能力保持中立**(`prompts.test.ts`):本包 peer 同时声明两条宿主线,而宿主 schedule 的提醒能力在两版上不同(0.1.7 仅一次性/session-local,0.2.0 起有 daily/weekly/cron 且 Host-wide 持久),故锁「`REMINDER_GUIDE` 与能力段都不得出现只在一版成立的固定断言」, 并要求判定口径写全(先看 `schedule_create` 本次实际接受的参数 + 保留兜底话术)。 与 §5.8「受开关门控的工具描述一律不断言」同族,只是载体换成了宿主工具。 - **显示元数据门禁**(`plugin-display-meta.test.ts`):按宿主 `package-meta` 的实现参数 (语言 id 正则、icon 256 KiB/扩展名白名单/不得越出包目录、`${包名}/locale/en.json` 解析方式、语言文件须同目录)逐条对拍,并在临时 consumer 目录里**用 Node 真实解析**一次 (`node_modules` 链接指向本包)——宿主读不到时静默回落包名 + 默认图,故必须机械证明 exports/files/形状三者都对。 - **写路径守卫门禁**(`write-guards.test.ts`):宿主 `update` 缺键先抛 `DomainError('missing-key')` 且不调回调 → 本包必须换成带 code 的 `ToolError`;派生量同步遇缺键静默跳过;月视图 年份/月份越界即 `bad_date`(极端年份逐日物化数十万格会挂死进程);任务名与分类改名 走 store 单一口径且**不留半改状态**。配套地,四个内存域桩(`memory-store.ts` 等)的 `update` 已改成缺键抛 `missing-key`——桩与宿主语义分裂曾让「记录不存在」分支 在测试里可达而真机不可达(§5.11 同一类坑)。 - **HTTP 壳跨站写闸门**(`routes-shell.test.ts`):POST 缺 `x-xingyuan-write` 头即 403 且**先于请求体解析**;头名三处字面量一致(服务端常量 ↔ GUI `api.ts` ↔ 备用页 `pages-html.ts`)。 - **页面/对话同权门禁**(`tools.test.ts` 的「页面动作的工具面补齐」组 + `write-guards.test.ts` 的「分类颜色覆盖收口」组):官方 `user-actions.md` 要求 UI 能做的 数据与配置操作对 agent 同样开放——13 个页面动作逐个要么有等价工具,要么有「用户专属」的 明确理由(`update_xingyuan_settings` 的参数里出现 `confirmWrites`/`confirmOps` 即红); 同时锁住「校验先于确认」「拒绝即不落盘」「写后读回比对」三条,防止工具说改好了而实际没改。 - **preset 注册门禁**(`preset-declaration.test.ts`):按 YAML 解析真实 `cordis.patch.yml` 取 `preset-xingyuan` 声明行(`!!js` 读成宿主同形状的表达式包、不求值),再把它的 config 交给**真实宿主插件** `@deepseek-ai/dsh-agent-preset`(devDependency 精确锁基线)装载, 断言注册表收到预设身份 `xingyuan`、本行 dispose 后收到注销。声明行缺席时选择器里 就是不出现星愿且零报错(§4「Agent Preset 声明行」),所以这一门禁是唯一的防线。 - 客户端页面纯函数回归:跨取数路径共用的构造函数(如记忆列表 URL)以 「构造 → 服务端往返命中」闭环锁定(`client-pages.test.ts`)。 - **样式兼容契约门禁**(`style-contract.test.ts`):STYLE_TEXT 与 gen-mock 源码禁 `color-mix(`;半透明衍生色只许出现在令牌区(正文区一律引用令牌);关键语义令牌 必须浅/深成对——视觉铁律由测试机械锁定,改样式先看它。 - **图表词表覆盖对拍**(`chart-labels.test.ts`):charts.ts 内建标题/枚举标签/固定 副标题逐一对照客户端映射表(标题双向逐字同源、标签∈映射∪用户数据∪日期轴、 副标题∈映射∪数字模板)——服务端加词不同步映射即红。 - **sqlite 后端门禁**(`sqlite-backend.test.ts`):介质版本不符拒绝打开、global 槽 损坏拒绝(malformed-medium)、写后冷读持久化——无迁移策略的安全底座。 - **会话日志自愈回归**(`session-log-repair.test.ts`):补标/去毒双模式、补标面按 「迁移边是否接受 ignorable」分界(v3 组用例含官方 v3→v4 链实跑,验证卡片事件以 `plugin:` 前缀存活;另有「夹具合法性对照」用例防止该组断言因夹具本身不合法而假绿)、 多代工件只处理最新代、布局/撕裂/坏帧放弃矩阵,以及去毒产物真实穿过官方迁移链 (经 `@deepseek-ai/dsh-session-format-catalog`,devDependency)。 - **重挂载回归**(loader.test.ts 末例):webServer 桩按宿主契约「重复 (kind,path) 抛错 + 返回 disposer」,拔除 bundle 行再重建——路由注册必须经 ctx.effect(HMR/升级路径)。 - 业务层/工具层/路由层用例:创建→领取→打卡链路、删除级联不留孤儿、写确认门闩、 延迟领取锚点重算、分类改名迁移颜色覆盖键、微行动状态机、成长聚合。 CI(`.github/workflows/ci.yml`):typecheck → build → test(Node 22 + pnpm 10)。 发布(`release.yml`):推送 `v*` tag 自动 build + test + npm publish(预发布版本挂 alpha tag, npm provenance 开启)。 新增第三方构建依赖时遵守 purity 门禁:不允许跨插件值导入(peer 依赖除外)。 ## 10. 设计决策摘要 延续项目立项时的定向结论,后续演进不得无声推翻: 1. **平行形态**:本仓库是独立 npm 包,其他形态的星愿版本各自演进;数据完全独立,不做互通。 2. **数据边界**:一切业务数据本地 SQLite(`~/.dsh/xingyuan/`),备份 = 拷贝目录; 卸载/升级数据存活。 3. **多会话解耦**:业务状态全在存储层,会话只是访问入口;新开会话无缝续接, 「今日应打卡」概览兜底提醒触达。 4. **Preset 可选不默认**:工具只挂 preset 层,agent 选择器手动选择;空白会话才能切换 preset。 5. **BYOK 复用 harness**:模型凭据使用自带「设置 → 模型」,不自建引导流。 6. **chat-first**:引导与教学在对话里完成;设置卡只承载偏好表单。 7. **提醒能力不在本包写死(2026-09-29 修订)**:宿主 schedule 的能力**随版本不同**—— `0.1.7` 只有一次性规则(at / after / every_seconds ≥300s,session-local),`0.2.0` 起是 **Host-wide 持久提醒 + daily/weekly/cron**(`MIN_EVERY_INTERVAL_SECONDS` 降到 60,另有 `schedule_update`)。本包 peer 同时声明这两条线,所以任何一侧的固定断言都会在另一侧失真: 星愿**不自造包装工具、也不在提示词里断言能力边界**,`REMINDER_GUIDE` 一律要求模型 「先看 schedule_create 本次实际接受的参数」再作答,不支持周期时按一次性口径如实说明并用 今日页 + 开场概览兜底(`test/prompts.test.ts` 的「提醒指南对宿主能力保持中立」锁死)。 打卡与宿主提醒的**联动(如按机会日自动设 daily 提醒)属未立项的产品决策**,见 §11 末条。 (另注:schedule 工具只对装载之后新建的 live 根 agent 可见。) 8. **写确认分层(2026-09,ADR-0001)**:总开关 + 六类目明细(创建/打卡/取消/领取/修改/记忆保存), 默认=分层化前矩阵;删除始终确认、不设开关(破坏性操作不设开关);总开关关闭=一键静音 (除删除外);作用域仅对话侧 HITL,页面侧确认对话框不跟随设置(§5.5 矩阵)。 9. **标签页显隐默认跟随会话预设**:六个会话视图标签默认仅在星愿预设的会话显示, 设置可切「始终显示/始终隐藏」并按标签勾选;界面偏好命名空间常驻 bundle 层 (未选星愿也可调,避免「全部隐藏后开关不可达」死锁,见 §5.11)。 10. **用户偏好一律常驻 bundle 层**:设置整页常驻可见,故任何有 Web 设置界面的偏好 (安全策略、界面、资源上限)都必须落在 **bundle 常驻组装行的 `.volatile()` 配置字段** 上,不得挂 preset 层——preset 挂载懒加载,会导致「重启后未开过星愿会话时偏好不可改 且写入静默失败」。0.1.7 前的形态是「bundle 层常驻命名空间」,现已收进主行 Config (`SETTINGS_ENTRY_ID` ↔ `cordis.patch.yml` 行 id,见 §5.8),不变量本身不变。 判定口径:作用域属「单次会话的能力」→ preset 层;属「用户的全局偏好」→ bundle 层 (见 §5.8)。preset 层按需经服务(如 `ctx.xingyuan.prefs()`)读取常驻偏好。 11. **承诺口径分层(2026-08 定;2026-09 修订——比率面并入承诺口径)**:未领取(pending)任务处于候选池, 未承诺不算行动义务——「今日」尺度的完成率/待打卡被未承诺任务稀释会让「今天全部 完成」的正反馈永不触发、且制造无谓负罪感,更会让模型把未领取任务当今天义务去打卡 而撞 `not_claimed`。故:**今日尺度(今日页进度/全部完成判定、日历格子与完成判定、 成长页近 30 天区间图、开场概览、get_today_unchecked_tasks)只统计已领取且未完结的 任务(已完结=达标关闭 achieved,不进入 日程面——once 无截止日任务打卡次日即退出,防「今天全部完成」被已完成任务永久 挡住;过期关闭属失败记录,留在截止日日程面如实呈现,见 §5.2 规则 7),未领取单列「可领取」 并以约束词防模型主动领取——单列位置在任务页待领取组与对话开场概览;今日页与日历详情 面板(含今天)不展示候选池(2026-08 用户裁决两轮:今日页→日历面板,承诺面不混候选池); 长期比率(愿望进度——页面卡片与 wishProgress/taskCompletionRate 图)2026-09 起同为 **承诺口径**:分母只计已领取任务(候选不进比率、不拖累完成率;业界对照 GitHub Projects draft 不计进度、Scrum velocity 只计 committed;把未承诺工作计入完成率是「生产力内疚」 反模式),**达成 = 进度 100% 且无候选**(候选拦达成:满进度而有候选 = 「待结算」, 收尾是用户决定,模型不得代结;此闸门同时保证口径切换的迁移安全——切换只会抬高 进度,新达标者必含候选、被闸门拦住而非意外归档)。未来安排查询工具维持计划口径。 **实现约束:过滤统一走 `store.ts` 的 `isClaimed` 判定,进度/达成公式单一收口 `wishProgressFromAgg`(写路径与读侧共用,读侧视图携 `pendingCount`/`settled` 派生位下发给 API/事件/回包,taskCompletionRate 图求和走 `claimedDaysSum`, 消费侧零重建);任务写操作的闸门翻转经 `wishSettleNote` 回包 + 补发 wish 事件;机会日序列 与写路径不随口径分层(未领取任务仍按创建日锚点产序列,日历只在其消费侧隐身); 已完结排除在计划层(`planFromPrepared`)单一实现,消费侧不重复判定。 口径的完整范围见 §5.2 规则 7。 ## 11. 已知限制 - 提醒的存活范围随宿主版本:`0.1.7` 系 session-local(只在承载它的会话存活期内触达); `0.2.0` 起为 Host-wide 持久(自有 `schedule` 存储域、交付回原会话、另有宿主自己的任务管理页)。 星愿一侧**不承诺任何一种**:能力判定交给工具参数(§10 决策 7)。到期呈现仍是 `[SCHEDULE REMINDER]` 用户角色 follow-up,本包无专属提醒 UI;把打卡机会日自动接成宿主 daily/weekly 提醒属未立项的产品决策。 - 设置卡是 schemastery 表单,无自定义按钮/复杂控件。 - weekly/monthly 不绑定自然周星期几:机会日从领取日起每 7 天/逐自然月推进, 「每周三健身」类固定星期几需求无法直接表达(§5.2 锚点语义);提示词层已要求模型 如实说明并引导用 daily 或调整领取日,属有意取舍,勿当缺陷报。 - 领取不可逆:claim 无「退回待领取」路径(锚点日/应打天数随领取重算,回退语义复杂); 误领取的恢复路径是删除重建。有意取舍,勿当缺陷报。 - 无任务的愿望没有「手动标记达成」路径:达成判定 ≡ 进度 100% 且无候选任务 (`wishProgressFromAgg` 派生,2026-09 起候选拦达成), 裸愿望(create_wish 不附任务)进度恒 0,对话与页面均无达成出口;用户想完结时 模型的引导口径是补一个任务来承载完成过程(或删除重建)。已评估过为 update_wish 增加 achieved 标记的方案,涉及领域字段/工具/路由/卡片/成长统计多处联动,留待 真实需求出现再立项,勿当缺陷报。 - 页面不承载通用编辑:愿望/任务的改名、改周期、改画像走对话(chat-first);页面侧 唯一的编辑动作是过期任务详情内的「延长截止日」复活闭环(/api/action/update-task)。 扩页面编辑面前先推翻这条记录。 - 确认卡语言不能自动跟随界面语言:宿主不向 host 侧暴露 locale(§5.5),由 confirmLang 偏好显式选择;独立备用页 /xingyuan/* 维持中文单语(pages-html 头注)。 - 存储无迁移机制:领域 schema 只能用 optional 字段做向后兼容增量;破坏性演进必须升 DOMAIN_VERSION 并提供数据重导出方案。 - 会话事件沿用 whole-value 单事件模式最省心(一条事件一张卡、id=`event.seq` 天然唯一)。 **0.1.7-rc.1 起宿主不再强制「每 (kind,id) 仅一条 start」**:assembler 删掉了 `received more than one start Match` / `received a transient start Match` 两处抛错, 最早的 start(durable 或 transient)建 State、后来被标 start 的事件走 `update()`; 仍抛的只有「start 不是该 Context 的首个 match」。星愿设计不受影响(每事件一 id, `update` 幂等),但**新事件类型不必再为「怕重复 start」而绕路**,可考虑增量更新卡。 - 外部插件会话事件在 dsh 读取白名单之外且无 ignorable 写入通道:依赖激活期 会话日志自愈兜底(§5.6)。补标面 = 迁移边接受 ignorable 的那一侧(**v3 起,含当前代 v4**),靠补 `ignorable` 保留卡片数据;**v2 及更早的迁移链拒绝一切未知事件,只能去毒** ——被去毒会话的历史卡片不再回放(README 对话面正常,业务数据在 sqlite)。插件未运行期间写入的事件在下次启动前不可冷读; 极端竞态(会话未被识别为活会话且恰在自愈写回窗口内落盘)最坏可丢失窗口内新 写入的事件(备份只有改写前状态),一般情形是多报一次错、下次启动自愈。上游若开放 ignorable 写入通道、事件注册面,或迁移链改为接受 ignorable 历史事件, 回归官方机制并撤下补标/去毒。 - **v2 及更早**会话的**历史卡片不可回放**:那条迁移链拒绝未知事件(官方设计,v3 起已 接受 ignorable,见 §5.6),去毒是保「会话可打开」的代价;期望上游开放迁移白名单或 插件事件注册面后恢复。勿当缺陷报。去毒只处理 `xingyuan/*` 命名空间——其他插件 写入的历史事件需其作者自行处理。 - **对话卡片会被「紧凑/标准」转录模式折叠**:ui-chat 的 Turn Process 披露把已完成轮次 里落在过程窗口 `[processStartSeq, answerAnchorSeq)` 的**所有非白名单 kind 节点** (含 xingyuan 五类卡片)收进「N 次工具调用」条,默认收起;白名单 (`TURN_PROCESS_INDEPENDENT_KINDS`:system-prompt/user/steering/turn-process/ turn-error/turn-max-tokens/turn-tail)为 ui-chat 写死的内置集,**插件无豁免接缝**。 用户侧开关:设置 → 通用设置 → 「对话显示」——0.1.7-rc.1 起档位是 `compact / standard / detailed / verbose` 四档,**默认随宿主版本与端别变** (0.1.7-rc.1 全端 `standard`;0.2.0-rc.1 起 Web 端 `detailed`、仅 Desktop `standard`, 且该字段改为可选——缺省走客户端默认值,legacy `normal`/`expanded` 读作 detailed), 但策略表里**只有 `verbose` 才 `foldCompletedTurns:false`**(两版宿主的 `POLICIES` 表都如此, `dsh-client-ui-chat/lib/client.js`),所以「标准/详细/紧凑」三档**一律**折叠卡片,只有「详尽」不折; 命名空间 `ui-chat` 字段 `transcriptView` 属宿主设置,插件不得代改; 单轮可点披露条展开, 隐藏节点是 `hidden="until-found"`,浏览器页内查找会自动展开。勿当插件缺陷报。 - 客户端样式禁用 color-mix() 等新式取色函数:dsh 壳的浏览器矩阵里存在不支持的 环境,凡用它的属性按无效处理(空态插画曾因此整体隐形只剩孤立色点,已移除插画 并全站改显式 rgba 令牌,见 styles.ts 头注)。 - 主行 id / Loader 行 id / 预设身份三者避让(§4 硬约束 1);预设只能靠补丁里的声明行注册, 宿主不提供任何目录扫描(0.1.7 前那套 `$DSH_HOME/.agent-presets/` 文件复制已作废)。 - 设置整页的可见性保护依赖 `configForms.whileServed`(0.1.7 新增):宿主若再换这一接缝, 注册方须自行呈现不可用态。`ConfigForm.set()` 现在会如实回报接受与否(false = 被拒或被 跳过),但 **false 不区分「校验拒绝」与「被后继写取代」**,两者都给同一条提示。 - **用户偏好不在 `~/.dsh/xingyuan/` 里**:0.1.7 起偏好存 profile 补丁文档 (`~/.dsh/profiles//cordis.patch.yml` 的 `xy-bundle` 行 config), `~/.dsh/settings.yaml` 已被宿主弃用并改名 `settings.yaml.imported`。故「备份即拷贝 `~/.dsh/xingyuan/`」只覆盖**业务数据**,不含偏好;换机要恢复偏好需另拷 profile。 - **0.1.7 升级重置了所有星愿偏好**:宿主的 legacy import 只认「有对应 Loader 行」的节, 旧的 `xingyuan-pref` / `xingyuan-ui` 两节因无行而被拒(`No configurable plugin entry`), 用户自定义值留在 `settings.yaml.imported` 里成为死数据。有意接受重置(2026-09-23 决策), 不做迁移。 - **基线越过 0.1.6 后不再兼容旧宿主**:client 半侧硬依赖 0.1.7 的 `configForms` 服务, 旧宿主上整半侧不激活。peer 范围现为 `^0.1.7-rc.1 || ^0.2.0-rc.1`(含 client 半侧 6 包, 两条宿主线同时放行——0.6.5 那次只写一条线,用户升 0.2.0-rc.1 后整包被闸门跳过,见文档头), 且 **0.1.7-rc.1 起它真的是闸门了**:dsh 在装配前用 `semver.satisfies(宿主版本, peer范围, {includePrerelease:true})` 校验包声明的每个 `@deepseek-ai/dsh*` peer,不符者**整包静默跳过**(豁免:profile 的 `compatibility.json` + `dsh plugin allow-version --dsh-version --accept-risk`)。 旧结论「装包时既不安装也不校验这些 peer」只对 pnpm 那一层成立(profile 的 `pnpm-workspace.yaml` 仍写 `autoInstallPeers: false`,宿主包由全局 dsh CLI 提供), 宿主自己的兼容闸门与 pnpm 无关。两条边界要记住:① 闸门**只认声明出来的 peer**, 没声明的面(例如将来新引入的宿主包)不受保护;② 闸门只挡版本区间, 挡不住「同版本号不同行为」,字段撤除类故障仍需 §5.10 的真机回路发现(§5.11 的教训不变)。 - 非回环连接(浏览器地址不是 `localhost` / `127.0.0.1/8` / `::1`)下,settings 写入被 宿主降级为 memory 模式(`ConfigFormSnapshot.mode === 'memory'`、`writable: false`, `set()` 直接返回 false 不上 wire)。这是 dsh 的安全约束(settings RPCs are loopback-only),设置页只能提示,无法绕过。 - dsh 技术预览期,peer 同时声明 `0.1.7-rc.1` 与 `0.2.0-rc.1` 两条线(**rc**:settings 与会话格式 曾在两周内各换过一次,schedule 子系统在 0.2.0 又被整体重写,见文档头两条注); devDependencies 只钉其中最新的一版做 typecheck/测试,另一版靠「范围必须放行 + 逐包字节比对」 兜住——**同版本号不同行为仍然只能靠真机回路发现**(§5.11 教训不变)。 跨版本升级前核对 release notes 与排障索引,并按 §5.10 回路做真机验证。 ## 12. 官方参考文档 文档站入口:(SPA,栏目内导航浏览)。 以下深链指向源码仓库 `docs/`(2026-08-27 逐一核验存在;`.zh.md` 为中文版): **入门 / BYOK** - 快速开始:`https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/index.zh.md` - 配置模型(providers):`…/docs/user/guide/providers.zh.md` **基础开发(日常迭代必读)** - 第一个插件:`…/docs/user/develop/basic/index.zh.md` - 开发一个 Tool:`…/docs/user/develop/basic/tool.zh.md` - 插件配置:`…/docs/user/develop/basic/config.zh.md` - 打包与安装(bundle/profile/层顺序):`…/docs/user/develop/basic/publish.zh.md` - 生命周期与 effect 清理:`…/docs/user/develop/framework/index.zh.md` - 服务与依赖:`…/docs/user/develop/framework/service.zh.md` - 事件系统:`…/docs/user/develop/framework/events.zh.md` **Cordis 教程(七篇)**:`…/docs/cordis-tutorial/01-first-plugin.zh.md` 起 **概念参考** - 架构总览:`…/docs/architecture.zh.md` - Cordis 入门:`…/docs/cordis-primer.zh.md` - 能力服务 seam:`…/docs/capability-seams.zh.md` - Agent 生命周期:`…/docs/agent-lifecycle.zh.md` - Tool 执行流水线:`…/docs/tool-execution-pipeline.zh.md` - 会话格式状态(含当前代与迁移链):`…/docs/session-format-status.zh.md` - 测试规范:`…/docs/testing.zh.md` - 术语表:`…/docs/glossary.zh.md` - 迁移拒绝外部历史事件(§5.6 依据):`…/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md` - 保留 ignorable 外部事件(当前格式依据):`…/.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md` **子系统(本项目用到的高频篇目,均在 `…/docs/subsystems/` 下)** tools · storage · persistence · system-prompt · user-questions · approval · schedule · web-server · client-modules · settings · credentials · session · scope · invariants · conversation (对应 `.zh.md`;总目录见 `subsystems/README.zh.md`) **Cookbook**(均在 `…/docs/cookbook/` 下) adding-a-package · adding-a-tool · adding-a-settings-card · adding-a-vendored-package · adding-an-llm-adapter · extension-cookbook (会话节点/事件卡契约在 `docs/subsystems/conversation.zh.md`——官方 cookbook 无 conversation-node 篇目,旧文档引用为失实) **生成式参考** - 配置目录(含 dsh-agent-presets 等):`…/docs/config-catalog.zh.md` - Tool Schema 目录:`…/docs/tool-catalog.zh.md` - 持久化事件目录:`…/docs/persistence-catalog.zh.md` **插件实践与验收规则(0.1.7-rc.1 起随包发布,不在文档站而在安装副本里)** `/node_modules/@deepseek-ai/dsh-agent-preset/skills/cordis-plugin-development/` 下的 `SKILL.md`(第 4 步的设计一致性验收清单)、`references/practices.md` (**rc.1 新增**:扩展点选择、会话状态、性能与 UI 的官方规则——「不要以模块方式加载宿主 Client 包」「只用 `--dsw-alias-*` 令牌」「不在组件外写 DOM / 不 append document.body」 「不要用新事件 type」都在这份里)、`references/ui-plugin.md`、`references/host-plugin.md` (bundle 清单与**显示元数据/图标**约定)、`references/verification.md` (**无浏览器控制时的验证边界**:mock 页截图不算对运行插件的验证)。 星愿自查结论见 §5.10 末段(已合规项 + 两处有意偏离 + 验证口径)。 **Agent Preset 格式权威出处** `https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/preset/agent-preset/README.zh.md` (配套 `packages/preset/agent-preset-registry/README.zh.md`;声明行写法与旧目录迁移口径在 该包随带的 skill `skills/editing-cordis-compositions/SKILL.md` 里——安装后可在 `/node_modules/@deepseek-ai/dsh-agent-preset/skills/` 读到。 旧路径 `packages/preset/agent-presets/README.zh.md`(目录扫描那套)已随机制作废。) (以上表格语境中 `…/` 代指 `https://github.com/deepseek-ai/deepseek-harness/blob/master/`。默认分支实测为 `master`,`main` 不存在——2026-08-29 经 GitHub API 核对。) ### 排障索引 | 症状 | 先查 | |---|---| | 装不上 / git 安装缺 lib / prepare 授权失败 | docs/user/develop/basic/publish | | **装/启阶段整包被跳过**(stderr:`skipping profile bundle "": Plugin @ is incompatible with dsh : peerDependencies {...}`;星愿实测见 0.6.5 遇 0.2.0-rc.1,预设/卡片/标签页/设置页/直开页整体消失而数据无损) | 0.1.7-rc.1 新增的 DSH peer 兼容闸门(`dsh-app-boot` 的 `evaluatePluginCompatibility`,semver 含 prerelease;**`peerDependenciesMeta.optional` 不参与判定**,声明即受约)。要么升级插件、要么按提示 `dsh plugin allow-version --dsh-version --accept-risk` 显式豁免(写进 profile 的 `compatibility.json`,下次组合生效);`dsh plugin add` 也在预检处被拒(`nothing was installed`)。星愿侧防漂移见 `test/host-baseline.test.ts`(§9:每条支持线必须放行、未核对的下一代必须拦住) | | 插件管理卡 / 设置页插件列表显示包名而非中文名、没有图标 | 显示元数据未导出:宿主**不激活插件**去读 `locale/en.json` 的 `meta.title`/`meta.description` 与 `package.json` 顶层 `icon`,解析不到即静默回落(`ERR_PACKAGE_PATH_NOT_EXPORTED` 也算「没有」)。检查 `exports` 是否暴露 `./locale/*.json` 与 `./package.json`、`files` 是否收录、locale 文件名是否语言 id——门禁 `test/plugin-display-meta.test.ts`(§9) | | **client 半侧整半侧不激活**(boot 报 `pending (waiting for service: settingsScope)`) | 宿主是 0.1.7 而插件按 ≤0.1.6 写:`settingsScope`/`installSection` 已彻底撤除,须迁到 `configForms`(§5.8)。第三方插件在 0.1.7 上普遍踩此坑(同机 `modsearch` 报 `scope.settings.register is not a function`) | | 设置里没有「星愿」这一项(整页缺席) | `SETTINGS_ENTRY_ID` 与 `cordis.patch.yml` 主行 `id:` 不一致 → `configForms.whileServed` 永不满足、整页不注册(§5.8,无报错);`dsh --dump-config` 确认该行存在 | | 设置页显示「未就绪/只读」但宿主正常 | 该行存在但表单不可写:`status: loading/unavailable` 或 `mode: memory`(非回环连接);个别字段不出现则是漏标 `.volatile()`(宿主只投影 volatile 字段) | | 设置改了不生效 / 弹回原样 | 0.1.7 起 `ConfigForm.set()` 返回 `Promise`:false = 宿主拒绝或跳过(非回环连接 = memory 模式,永不落盘);检查是否只处理了 reject | | 配置不生效 / 行被覆盖 | publish 的层顺序与「整行替换」语义;cordis.patch.yml | | 实际组合与预期不符 | `dsh --dump-config` 看最终层叠结果 | | 工具没出现在模型请求里 | subsystems/tools;preset 是否真的挂载(选了「星愿」吗;空白会话才能切) | | INVALID_ARGS / 参数被拒 | ValueSchemaSpec DSL 的 additionalProperties/enum 要求;tool-catalog | | 卡片不渲染 / 刷新后丢失 | subsystems/conversation(start 唯一、确定性回放);三个声明合并位是否齐全 | | 会话事件没落盘 | exec.agent 是否存在(headless 无 agent 时 append 是 no-op) | | 旧会话打不开(`SessionFormatUnsupportedError` / unknown historical event) | **v2 及更早**的迁移链拒绝外部历史事件(v3 起接受 ignorable):确认 bundle 已升级到含自愈模式的模块(§5.6);源文件未动,备份在 `~/.dsh/xingyuan/session-backups/` | | 提醒没触发 / 提醒不重复 | 先看 `schedule_create` 本次实际接受的参数(0.1.7 系只有 at/after/every_seconds 且 session-local;0.2.0 起才有 daily/weekly/cron 与 Host-wide 持久)→ subsystems/schedule;工具只对装载之后新建的 live 根 agent 可见 | | 数据库打开报版本不符 | subsystems/storage;DOMAIN_VERSION 策略 | | preset 不出现 / mount 拒绝 | packages/preset/agent-preset/README.zh.md + 该包 skills/editing-cordis-compositions(**宿主不扫任何目录**,见 §4「Agent Preset 声明行」)。先查 `cordis.patch.yml` 里 `preset-xingyuan` 行在不在(`dsh --dump-config` / `plugin_manager list_plugins` 看该行激活态);本地 `pnpm test` 的 `preset-declaration.test.ts` 会当场对拍这一行 | | HMR 后状态丢失 / 注册残留 | lifecycle effect 清理;是否存在跨重载模块级单例 | | client 卡片/标签页没加载 | subsystems/client-modules(dsh.client 声明:inject 组成边 / external 非基座请求)+ 星愿行在 `window.__DSH_BOOT__` 的 entries 里有则 factory 已注册;**只有六个视图标签缺席、卡片与设置页都正常** → 「当前会话」判定取空,核对宿主代际的 `current` vs `retainedBy.mainView` 口径(§5.11,切「始终显示」可当场反证) | | `cannot get property "X" without inject` | cordis 4.0.2 服务属性守卫:只能在声明过 X 的 fiber 上以 `ctx.X` 访问——经 `ctx.inject(['X'], (inv) => inv.X...)` 的回调形参读(见 §5.11 设置宿主) | | 设置卡不显示 | 0.1.7 起设置整页经 `configForms.whileServed([行 id])` 注册:先确认该行被组装出来(`dsh --dump-config`)、行 id 与 `SETTINGS_ENTRY_ID` 逐字一致(§5.8),再核对字段全部 `.volatile()`(宿主只投影 volatile 字段)。cookbook/adding-a-settings-card 是 0.1.6 前的 namespace 口径,已不适用 | | Key/模型问题 | user/guide/providers;subsystems/credentials |