DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-design-ledger

U

dsh-design-ledger

Manifest valid

Turn design documents into a live development progress tree and inject it into DSH session context, so new sessions can pick up where the last one left off.

UI (client)hasBundlePatch

dsh-design-ledger

中文 · English

把设计文档变成一棵可实时更新的开发进度树,并让 DSH 代理在每次会话中自动接上进度。

面向场景:大型项目(游戏 / 软件)在同一个工作区长期开发,单个会话的上下文会被耗尽。本插件把"设计文档 → 进度清单 → 代码索引"变成可持续、可查询、可注入的结构化数据,使新会话能接着干。

30 秒理解

设计文档(.md / 文件夹)          开发进度树(本插件生成与维护)
┌──────────────────────┐          ┌──────────────────────────────────┐
│ 03_systems/          │  解析    │ index.json   索引(自动注入)      │
│   S01_core_gameplay/ │ ───────► │ systems/     每个系统的完整子树    │
│     design.md        │          │   S01_....json                   │
│   S02_base_building/ │          │ state/       易变进度(可 gitignore)│
│     design.md        │          └──────────────────────────────────┘
└──────────────────────┘                        │
                                                │ 注入
                                    ┌───────────▼───────────┐
                                    │ DSH 会话上下文          │
                                    │ · 任务提示词           │
                                    │ · 索引(系统 + 完成度) │
                                    │ · 当前聚焦子树         │
                                    └───────────────────────┘

为什么不是"把设计文档全塞进上下文"

实测一份真实设计文档:19 个 .md / 588 KB / 226,885 字符 ≈ 133,000 tokens。

内容体量能否每次注入
任务开启时的提示词几百字符✅ 可以
整个设计文档≈133k tokens❌ 不可行(单份系统文档就 ≈65k)
进度索引数 KB✅ 可以
当前聚焦子树数 KB✅ 可以
其余子树 / 代码索引 / bug 明细大❌ 按需读取

所以注入策略是分层的:索引常驻、聚焦子树常驻、其余按需。这不是妥协,而是唯一可持续的做法。

注入策略(用户已确认)

层内容时机
L0任务开启提示词(完整)会话首次装配时注入一次
L1索引:系统清单 + 完成度 + 文件指针每次会话注入
L2当前聚焦子树(优先 marked 标记的节点)每次会话注入
L3其余子树、代码索引、bug 明细不注入,由 design_ledger_read 按需取
L4设计文档原文分片不注入,由 design_doc_read 按需取

注入走 system-prompt/assemble(waterfall)——与 dsh-agent-instructions 注入 AGENTS.md 同一接缝。

进度树的数据模型

每个节点是一行"表项",字段固定,便于工具解析与你手改:

字段含义
id稳定标识(按层级派生,如 S02/模块A/功能1)
name节点名 = 设计文档里的名称(无名称时按功能/用处命名)
kindsystem / subsystem / feature / subfeature / task
statustodo / doing / done / blocked / dropped
parentId / children父子关系(构成树)
designRefs设计文档索引:{ file, heading?, anchor? }
codeRefs代码索引:{ file, symbol }(只记文件 + 符号名,不记行号)
interfaces对外接口/契约
bugs{ id, summary, status, codeRef?, designRef? }
notes备注
marked是否被标记为"当前聚焦"(注入优先)

为什么代码索引只记"文件 + 符号名":行号会随编辑漂移,符号名稳定。"几到几行"这类信息由编辑器/IDE 提供,不写进台账。

⚠️ 开发这个插件时踩过的坑(必读)

这些真的把 DSH 弄到起不来、或让功能静默失效/显示错乱,而且原因都很隐蔽。改本插件时请先读这一节。

崩溃 1:客户端 apply() 里调了不存在的 API → 宿主侧栏一起挂

Uncaught TypeError: ctx.locale.get is not a function
Error: web boot: 1 entry did not activate
@deepseek-ai/dsh-client-ui-sidebar: failed      ← 失败的是宿主自己的条目

机制:客户端半区的 apply() 里任何未捕获异常都会让宿主把该条目判为"未激活",并且连带把依赖它的宿主条目(这里是 dsh-client-ui-sidebar)一起判失败,于是整个 Web 启动失败、应用起不来。

根因:ctx.locale 没有 get 方法。真实 API 只有:

API签名说明
ctx.locale.register(ns, dict)(ns: string, dict: LocaleDict) => () => void注册字典(dict 是扁平 Record<string,string>)
ctx.locale.bind(ns)(ns: string) => Translate取翻译函数,调用时读当前语言

修复与纪律:翻译走 bind + 本地兜底;并且

  • apply() 内每一个可能失败的动作都各自 try/catch;
  • 最外层再包一层兜底 apply(),保证永远不向外抛(client/client.js 的 apply / applyInner 分工就是为此)。

崩溃 2:导出没有 schema 的 Config → cordis 调用 .validate 抛错

TypeError: Cannot read properties of undefined (reading 'validate')
  at resolveConfig (cordis/lib/index.js:958)

机制:cordis 的 resolveConfig 是这样的:

function resolveConfig(runtime, config) {
  if (!runtime.Config) return config          // ← 没有 Config 就直接返回
  const result = runtime.Config['~standard'].validate(config)   // ← 否则要求 schema
  ...
}

所以要么导出带 ~standard 的 schemastery schema,要么完全不导出 Config。导出一个普通对象(我为了去掉 schemastery 依赖而这么做过)就会命中 .validate 抛错、bundle 激活失败。

当前做法:不导出 Config,默认值放在内部 DEFAULTS,用防御式 readConfig(ctx.config) 取值。若将来要暴露可配置项,请引入 @deepseek-ai/schemastery 并导出 Schema.object({...})。

补充:这一条是在热重载时暴露的(运行中的宿主缓存了旧模块对象,ESM 按 URL 缓存、改文件不清缓存)。所以改完插件代码要整应用重启再判断,不要依赖 HMR 的即时结果。

崩溃 3:webServer.register 的路由对象缺 kind → 路由静默不生效

症状:插件 fiberPhase: active(加载成功),但 /design-ledger/* 一律 404(对照:dsh-whale 的路由返回 401,因为它在信任栅栏之后)。

机制:WebRoute 的真实契约是

interface WebRoute {
  kind: 'exact' | 'prefix'   // ← 必需
  path: string
  handler: (req, res) => void | Promise<void>
}

没有 method 字段。我曾写成 { method: 'GET', path, handler } → register() 抛错 → 被我自己包在注册外面的 try/catch 吞掉,于是路由根本没注册,而插件看起来一切正常。

注册方式的对照(重要):可跑通的 dsh-whale-widget 是这样做的 ——

root.inject(['webServer', 'credentials', 'connection'], (ctx) => {
  ctx.webServer.register({ kind: 'exact', path, handler })   // 直接注册,不套 ctx.effect
})

即:用注入作用域上的 ctx.webServer 直接注册,不要包 ctx.effect。本插件最初写成 ctx.effect(() => webServer.register(...), label),且解析顺序写成了 ws?.webServer ?? ctx.get('webServer') ?? ctx.webServer;现已改为与前者一致(先 ctx.webServer、直接注册)。

教训:防御式 try/catch 会把契约错误变成静默失败。所以:

  • probe/route-test.mjs 加了契约自检(断言每条路由 kind 合法),缺 kind 时以非零码退出;
  • 注册的三种结局都会打一条 [design-ledger] 日志:panel routes registered: 3 (…) / ROUTE REGISTRATION FAILED: <stack> / webServer unavailable; panel routes NOT registered。宿主不把 console.warn 落盘,所以排障时要看应用终端/控制台。

崩溃 4:面板配色自创 token → 黑底看不清 / hover 白底白字

症状(两次,同一个根因):

  1. 弹窗"太黑、看不清上面的字"(浅色主题下);
  2. 鼠标移到「选择设计文档」「载入为设计文档」上时,背景变白、字也是白的,字直接消失。

机制:颜色 token 必须照抄官方组件,不能凭名字猜。两次都栽在这上面:

我写的真实情况
--dsw-alias-bg-1 / --dsw-alias-text-1DSH 里不存在 → 永远走 fallback 的硬编码深色
--dsw-alias-bg-layer-2 当 hover 底色存在,但浅色主题下就是纯白(实测 var(--dsw-static-neutral-bluish-00) = #fff)

第二个还叠加了一个 CSS 层叠问题:.dlg-btn:hover 与 .dlg-primary 特异性相同 (0,2,0), 主按钮的底色/文字又是硬编码的(brand 底 + color:#fff);hover 规则一覆盖底色, 就成了"白色背景 + 白色文字",字彻底看不见。

正确来源:不要去猜,直接读官方组件的 CSS —— @deepseek-ai/dsh-client-ui-primitives 的 Button.module.css 就是权威:

.primary            { background: var(--dsw-alias-button-primary-fill);   color: var(--dsw-alias-label-primary-foreground); }
.primary:hover      { background: var(--dsw-alias-button-primary-hover); }
.ghost:hover        { background: var(--dsw-alias-interactive-bg-hover); }
.outline            { border: 0.5px solid var(--dsw-alias-border-l3); }
.outline:hover      { background: var(--dsw-alias-interactive-bg-hover); }

关键取值(从主题 CSS 里读出来的真实定义):

token浅色深色用途
--dsw-alias-interactive-bg-hover#2631480f#ffffff14交互底色首选(带透明度,两种主题都安全)
--dsw-alias-button-primary-fillvar(--dsw-alias-brand-primary)同主按钮底色
--dsw-alias-button-primary-hovervar(--dsw-static-neutral-bluish-750)…-100主按钮 hover(有独立规则)
--dsw-alias-label-primary-foreground#fff#0f1115主按钮文字
--dsw-alias-bg-layer-2#fff#2b2b2b抬升面 —— 不要拿它当 hover 底色

做法:交互底色一律用 --dsw-alias-interactive-bg-hover;主按钮用 button-primary 三件套; hover 一律写 :hover:not(:disabled)(避免禁用态变色、也顺手规避层叠打架)。 npm run check:client 会断言这些约定,防止再犯。

Theme.listTokens 只列出 14 个 token,是子集,不含上面用到的 button/interactive 系列 —— 所以"用什么 token"要以官方组件 CSS 为准,别只看 inspect 列表。

崩溃 5(最严重):systemPrompt 段落文本返回非字符串 → 整个对话一按发送就失败

本轮运行失败
text.indexOf is not a function          UNKNOWN

症状:只要插件处于启用状态,任何消息都发不出去:一发就红字「本轮运行失败」,整个会话瘫痪。 这比面板 404 严重得多 —— 面板坏了还能用对话,这一条把对话本身打掉了。

机制(@deepseek-ai/dsh-system-prompt 源码):宿主在每一轮装配时对每个已注册段落做

// assemble()
text: typeof section.text === 'function' ? section.text(context) : section.text
// 之后 renderPrompt(assembly) → interpolate()
for (let open = text.indexOf('{{'); …)   // ← 第一行就是这里

即 PromptSection.text 的契约是 string | ((context) => string),必须同步返回字符串。 一旦返回 Promise / object / undefined,interpolate() 立刻抛 TypeError: text.indexOf is not a function,而这是回合开始处的调用 —— 于是整个对话不可用。

修复做法(lib/index.js 注入段):

  1. text() 内只读内存缓存,绝不读盘、绝不 await、绝不返回 Promise;
  2. 注入文本由后台任务(refreshInjectionCache)预先算好;apply() 时预热一次, 台账变动(design_ledger_init / design_ledger_update / bind 路由)后 invalidateInjection() 重算;
  3. 过期判断只做一次 statSync(index.json).mtimeMs 比较,不用每次读文件;
  4. 出口再兜一层 typeof !== 'string' → '',并写 INJECT-NONSTRING 诊断日志。

纪律:这一段永远不要写成 async text()。渲染失败最多让注入内容缺失,绝不允许影响对话。

崩溃 6:defineTool 的 parameters 传了"预编译好的根 schema" → 六个工具全部静默失效

症状:插件 fiberPhase: active,注入正常,面板正常,但 design_ledger_* / design_doc_* 一个都不存在(模型调用会得到"未知工具")。而插件自己的日志里只有一行 warn,很容易被当成"没问题"。

机制:官方 defineTool(options) 的 options.parameters 要的是隐式属性映射:

// ✅ 正确:属性表 + 属性上的 required 标注;根对象由宿主编译生成
parameters: { rel: { type: 'string', description: '…', required: true } }

// ❌ 错误:手工预编译根 schema
parameters: { type: 'object', properties: { rel: {...} } }
// → defineTool 构造期抛:
//   unsupported JSON schema: parameters.type must be a value schema object
//   (parameterSchemaSpecToJsonSchema 把它当成一个"值 schema"节点,
//     而 "object" 值节点要求显式 additionalProperties,于是报 type 不合法)

因为本插件用 try/catch 把构造期异常吞成了"跳过这一个工具",结果是六个全跳过、 插件却看起来一切正常。防御式 try/catch 又一次把契约错误变成了静默失败。

纪律:lib/index.js 的 defineTool() 适配器只做两件事 —— 把顶层 required: [] 摊到属性上、 给缺 type 的标量补 string;根 schema 绝不自己拼。另外:对象属性必须显式 additionalProperties: true,且不要给 type: 'array' 补 items(会让参数校验去匹配元素形状)。

崩溃 7:面板把部署默认根当成了工作区 → 弹窗空白,且「工作区根」切不回去

症状:侧栏「设计文档包含」能打开,点「选择设计文档」弹窗显示 「(此目录下没有子目录或 .md 文件)」;面包屑点「工作区根」也回不到工作区 —— 因为那个根已经被当成工作区锁死了。

机制:宿主路由没有 session 上下文,sandboxPolicy.resolve() 给的是部署默认根 (本机实测 D:\AppData\.dsh\profiles\desktop),不是用户的工作区。 「工作区」这个事实只存在于会话侧:工具调用的 exec.agent.session、agent 事件。 面板拿不到它,宿主路由也猜不到,于是列了 profile 目录。

修复(四层,缺一不可):

  1. 会话工作区旁路记录(rememberSessionWorkspace):任何一次带 session 的解析 (工具调用、agent/created、agent/pre-step)都把工作区记下来,无 session 的路由缺省就用它。 这是关键 —— 它让旧版面板也能落在正确的工作区上。
  2. 主动发现(discoverSessionWorkspace):插件可能在会话诞生之后才加载 (bundle 刚登记、热重载、宿主重启时序),那时事件永远不会来。所以 apply 时以及 路由兜底时都主动去问 ctx.agents.list(),从活着的 agent 反查工作区。
  3. 部署根一律不认(isDeploymentRoot):命中 /.dsh/profiles/<名字>、/node_modules、 用户主目录的根,既不做缺省值、也不允许被"确认",直接 400,面板显示「工作区未知」。 这条专门防"锁死在 profile 里、再也切不回来"。
  4. 面板显式回传 workspace(取 status.json 的 workspaceRoot),宿主侧 resolveWs() 只认第一个被确认的工作区,之后必须一致。

同一个坑的第二个版本:事件名是编的。 第一版修复去监听 agent/session-start —— DSH 里根本没有这个事件(真实的只有 agent/created / agent/pre-step / agent/status …), 所以除了一次时序巧合外从未触发,面板又退回「工作区未知」。 教训:接缝名(事件名、服务名、字段名)必须对着源码或类型定义核实,别按"听起来该叫这个"写; 并且同一个能力要留一条不依赖事件的路径(这里是 agents.list() 主动发现)。

教训:无 session 的端点上,sandboxPolicy.resolve() 的返回值不是用户的工作区, 必须先想办法从会话侧把工作区"学"过来。另外:别把"面板能打开"当成"根是对的" —— 本机就是先修好客户端(面板能显示出路径了),才看清根依然是错的。

改这个插件时的验证顺序(本节供改代码的人看,使用者可跳过)

只想用插件的话,看到这里就够了 —— 直接跳到下面的「界面语言」或「设计文档的解析规则」。 下面这些命令在本仓库开发时用,需要 Node;其中带 ⭐ 的探针需要本机装了 DSH(见目录结构后的说明)。

npm run verify                                                                                # 语法 + 单测 + 文档体检 + 客户端体检 + 契约自检(推荐,一条命令跑完)
node --import ./probe/register.mjs probe/import-probe.mjs                                     # 能 import 吗
node --test test/core.test.js test/agents-md.test.js                                          # 13 个单测
node probe/client-ui-test.mjs                                                                 # ⭐ 客户端:按钮对比度 / 字典完整性 / 语言接线(不需要 DSH)
node --import ./probe/register.mjs probe/contract-test.mjs                                    # ⭐ 契约自检(崩溃 5/6 + 路由注册)
node --import ./probe/register.mjs probe/panel-list-test.mjs <工作区>                          # ⭐ 面板取数(崩溃 7 + 越界拒绝)
node --import ./probe/register.mjs probe/table-tree.mjs <设计文档>                             # ⭐ 方案 A:表格行 → 功能节点(只读)
node --import ./probe/register.mjs probe/host-smoke.mjs <设计文档> <工作区>                    # 宿主端到端
node --import ./probe/register.mjs probe/route-test.mjs <设计文档>                             # 路由与路径安全

probe/client-ui-test.mjs 与 probe/readme-check.mjs 零依赖(只用 Node),所以它们同时跑在 CI 上; 其余探针需要本机 DSH 依赖层,只在本地跑。

probe/contract-test.mjs 是回归护栏,把三个"静默毁掉功能"的契约钉死了:

断言守的是哪个崩溃
section.text() 同步返回 string,且宿主式 interpolate 不抛崩溃 5(对话瘫痪)
6 个工具全部注册,且 parameters 是 object-rooted schema崩溃 6(工具静默失效)
ctx.inject(['webServer']) 注册 3 条路由;服务未就绪时不误判、就绪后由重试兜底补上面板 Unexpected end of JSON input
会话工作区被记录后 list/status 都用它;部署根/越界/node_modules 一律 400崩溃 7(弹窗空白 + 根切不回来)
无台账 / 台账 JSON 损坏时,注入退化为 '' 且不抛崩溃 5 的边界情况

probe/client-ui-test.mjs 则守住界面这一侧:

断言防的是什么
交互底色不使用 --dsw-alias-bg-layer-2(浅色主题下 = 纯白)hover 白底白字
普通/列表 hover 用 --dsw-alias-interactive-bg-hover同上
主按钮用 button-primary 三件套,且 hover 有独立规则主按钮变色后文字消失
主按钮文字不再是硬编码 #fff深色/浅色主题下对比度失配
zh / en 键集一致,且代码用到的每个 key 都有翻译界面出现原始 key
槽位声明了 locale 命名空间、且不再自注入 t语言不跟随 DSH + 重复 prop 导致槽位装配抛错

界面语言(i18n)

面板文案跟随 DSH 的语言设置(DSH 内置 locale id 只有 zh 与 en):

// 1) 注册两套字典(一次调用覆盖全部内置语言)
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'design-ledger: dictionaries')

// 2) 槽位声明 locale 命名空间 → 渲染器注入**响应式**的 t 席位
ctx.slots.register({ name: 'main', key: PANEL_ID, locale: LOCALE_NS }, LedgerPanel)

关键机制(都已在源码里核实,不是猜的):

  • 渲染器的 standardKit() 在 entry.locale !== undefined 时执行 kit["t"] = localeSeat(face, entry.locale),并用 useLocaleRevision() 订阅 revision —— 所以组件里的 props.t 天生响应式,切换语言立即重渲染,插件不需要自己订阅。
  • 因此不能自己 inject: () => ({ t }):渲染器的 assertNoPropOverlap() 见到重复 prop 会抛 SlotAssemblyError,那是装配期错误,会让整个条目激活失败。
  • 侧栏按钮文字(label)由侧栏读取:它在每次语言变化时用 ctx.locale.subscribe(syncPanels) 重新解析所有面板标签,所以 label 只要返回"当前语言下的文案", 就会自动跟随 —— 无需额外通知。
  • 字典查找失败时框架的 t 返回 key 本身(不是 undefined),所以本地兜底必须先判"是否等于 key"。

设计文档的解析规则

支持两种输入:

1. 单个 .md 文件

按 Markdown 标题层级(# / ## / ### …)建树。

2. 文件夹(递归)

按目录结构推断层级,这是"严格项目管理"结构下最可靠的方式:

目录模式推断结果
NN_<名称>/(数字前缀)顶层分组 / 系统,名称取 <名称>
S0N_<名称>/系统(system)
SNN_<名称>/子系统(subsystem)
design.md该层级的设计正文
analysis.md该层级的分析(作为附加 designRef,不建节点;体量记在节点的 analysisTokens)
其他 *.md归入最近的祖先层级,作为补充材料

节点名称优先取设计文档中的名称(标题),无标题时按目录名/功能命名。

3. 功能表行 → 功能节点(方案 A,expandTableRows)

为什么需要这一条:很多项目的目录只到 S0X_<名称>/ 一层,而真正的内容写在 design.md 里的表格里(每行一个 XXX_001 条目,表格自带「当前状态」列)。 只按目录建树的话,每个系统下面是空的,代理没有可推进的功能节点。

规则说明
什么表算"功能表"首列即 ID 列,且每一行的首列都形如 PREFIX_数字(BFT_001 / TEC_009 / DEV_001)
什么表不算"白天预报与建设 | S01、S02" 这类普通表格 —— 首列不是 ID,不建节点(实测 2 张被正确忽略)
节点 id<父节点 id>/<表标题>#<行 ID>,例如 03_systems/S03_progression_economy/局外通用科技树表#TEC_001
节点名取「名称」列;是"待定"时用同行的类别/作用对象补一个可读名(如 TEC_002(通用科技))
节点 notes原样保留表格的「当前状态」列(如"机制已明确,名称待定"),渲染时显示为「备注: …」
designRefs{ file, heading: 表标题, anchor: 行 ID } —— anchor 就是那行的 ID,便于对回文档
docTokens取该表所在小节的体量(parseStructure 的分节统计)

策略(expandTableRows,写在行配置 config 下):

取值行为
'all'(默认)展开每张 ID 表的所有行;若某文档的表都只有 1 行,至少展开第一张,避免系统节点空着
'multi-row'只展开行数 > 1 的表(示例性的单行表不建节点,更干净)
false不自动建功能节点,回到"只按目录结构建树"

已知边界:如果某份 design.md 里没有带 ID 列的表(功能清单写在正文段落里), 该系统的功能节点仍需人工用 design_ledger_update {parent_id, node_name} 补, 或给那张表加一列 ID —— 这一条在探针里会如实报告是哪些系统,不会假装通过。

4. 台账记的是"实际开发",不是"设计清单"

自动生成只给骨架 + 候选条目。要不要为某个功能建节点,由实际开发决定:

情况做法
这一轮要做的功能,设计文档里有条目但还没节点design_ledger_update {parent_id, node_name, status:"doing", mark:true} 新建
已经在推进mark:true + status:"doing"
做完了status:"done" + 补 code_refs / interfaces
设计文档里有、但这轮不做先不建,等真要做时再建

为什么定这条:如果按设计文档把条目一次性全建成空节点,进度树就变成了设计文档的镜像, 完成度 会被大量"从未动过"的条目稀释,看不出真实进度。按需建节点能让 "树 = 已经动过的部分",也更泛用 —— 换项目、换文档风格(正文型/表格型/目录型)都不影响, 自动生成的多与少只是起点。

这条原则同时写在三处,确保任何接手会话都会遵守: lib/inject.js 的注入正文(每个会话都看得到)、design_ledger_update 的工具描述、 以及配套技能 skills/design-ledger/SKILL.md 第 2.1 节。

与 AGENTS.md 的关系

AGENTS.md          ← DSH 原生注入(dsh-agent-instructions):项目简介 + 指向台账 + 当前里程碑
DEVPLAN/
  index.json       ← 本插件注入:系统级索引 + 完成度
  systems/*.json   ← 按需读取:节点表详情

AGENTS.md 有字节预算(超限会被截断),所以它只放摘要与指针;台账明细放 DEVPLAN/。两者职责不重叠:AGENTS.md 给人读,台账给机器与注入用。

安装

装到桌面端 profile(必须用 plugin_manager——桌面端 profile 由 Electron 应用独占,dsh plugin --profile desktop 会被 CLI 拒绝):

plugin_manager  install_bundle  target = link:E:\program\dsh\dsh-design-ledger
  • link 安装时每个 harness import 都必须被 launcher 路由,因此本包的 peerDependencies 必须声明所有被 import 的 harness 包 —— 当前只有 @deepseek-ai/dsh-tools。反过来也不要多声明:cordis、dsh-system-prompt、会话等 harness 包都由 DSH 运行时提供,不经 npm 解析;而 DSH 的兼容性判定会逐个检查 @deepseek-ai/dsh* peer,多写只会增加误判风险(例如 cordis 在 npm 上根本没有 4.x 正式版,写了就是一条永远无法满足的约束)。同理本插件刻意不用 schemastery——两个标量配置不值得引入一个 harness 依赖。
  • 安装后必须重开桌面客户端,bundle 层才会激活(fiberPhase 从 null 变为 active)。
  • plugin_manager 可能报 application: failed + ambiguous-install;实测这是假报(pnpm 退出码 0、磁盘状态正确)。判断是否成功请用 plugin_manager list_plugins 看 fiberPhase,或看工具是否出现。
  • profile 的 package.json 里还有一份 dsh.profile.bundles 列表,bundle 必须同时登记在那里才能激活。任何 plugin_manager 操作都可能重写该文件并丢掉不是它登记的条目——插件"自己消失"时先看这一行。

从源码安装(给别人用)

git clone https://github.com/<owner>/dsh-design-ledger.git
# 然后在 DSH 里:
#   plugin_manager  install_bundle  target = link:<克隆下来的绝对路径>
#   并把 "dsh-design-ledger" 加进 profile 的 dsh.profile.bundles,然后重启客户端

无需构建步骤:插件是纯 ESM 的 .js,客户端半区也是直接加载的经典脚本。

要发新版本(打 tag、建 Release、附件 tarball)请见 RELEASING.md —— 那是维护者流程,与使用者无关,所以不放在这里。

验证状态(本机实测)

本表在项目被真正跑起来之后重新核对过一遍:旧表里"⏳ 待重开客户端"这类推测性状态已换成实测结论, 并补上了那轮排查中暴露出来、现已修好并加了回归护栏的契约。

项状态证据
核心逻辑单测✅ 13/13 通过node --test test/*.test.js
契约自检(回归护栏)✅probe/contract-test.mjs:注入文本必须是同步字符串、6 工具注册、3 路由注册、无台账/台账损坏都不抛
面板取数✅probe/panel-list-test.mjs:会话工作区解析正确;部署根/越界/node_modules 一律 400
功能表行建树✅probe/table-tree.mjs:真实文档识别 5 张功能表 / 16 行,2 张普通表被正确忽略
插件可导入✅node --check + probe/import-probe.mjs(name/inject/apply 齐全,default === undefined)
6 个工具注册✅宿主日志 tools: built=6 ctx.tools=obj register=function → tools: registered=6/6
systemPrompt 注入段落✅段落注册成功并产出文本;且不会打断对话(见「崩溃 5」)
3 条 JSON 路由✅GET status.json、GET list.json、POST bind.json;宿主日志 inject([webServer]) fired: register=function → routes registered=3
路径安全(工作区限制)✅../../Windows、..%2F..%2FWindows、C:\Windows、/etc 全部 403;resolveWs 只认第一个被确认的工作区
真实设计文档端到端✅14 个 .md / 153,957 字符 / ≈92.7k tokens;识别 4 个系统 + 16 个功能节点(表格行展开),名称取自文档 H1
多层树(系统→子系统→功能→子功能)✅合成结构验证:核心玩法系统 → 战斗子系统 → 命中判定/伤害计算;S01_01_01_x 归类为 feature
更新节点(状态/代码索引/接口/bug/mark)✅实测 acted = created … / status=doing / marked / codeRefs+=1 / interfaces+=1 / bug added …
聚焦注入跨回合刷新✅上一轮 mark:true → 下一轮上下文出现 聚焦:基地构筑与建筑运行 › 机枪塔(含代码索引与接口)
重复 bind 保护✅已存在台账时 409 ledger-exists(不覆盖进度)
AGENTS.md 联动✅托管块幂等:重复写入逐字节一致;绝不覆盖块外用户内容;行内引用标记不干扰边界定位
插件在 DSH 中激活✅plugin_manager list_plugins → include:design-ledger / fiberPhase: active
客户端侧栏入口 + 图形选择器✅侧栏「设计文档包含」可用;选择器能列出会话工作区目录并完成绑定

图形化设计文档选择器

面板(侧栏「设计文档包含」)里点「选择设计文档」打开模态浏览器:

  • 面包屑导航 + 上一级 + 工作区根
  • 目录行显示子树内 .md 数量(便于识别哪个目录是设计文档集),可点进去或直接选出
  • .md 文件行显示大小,可直接选中
  • 选中后「载入为设计文档」→ POST bind.json 建台账;已存在台账时提示可「重建」

安全边界:所有路径都在宿主侧做工作区内校验(safeJoin + isInside),并明确拒绝绝对路径与 ..;面板无法读取工作区外的任何内容。

工具

工具作用
design_ledger_status当前绑定状态:设计文档路径、台账位置、统计(节点数/完成度/待办 bug)
design_ledger_init绑定设计文档并生成初始进度树(可指定路径或让其列出候选)
design_ledger_read按需读取:索引 / 某系统子树 / 某节点详情 / bug 列表
design_ledger_update更新节点:状态、代码索引(文件+符号)、接口、bug、备注、mark 标记
design_doc_read按需读取设计文档原文(单文件或某层级)
design_doc_list列出设计文档树与各文件体量,便于按需选择

目录结构

dsh-design-ledger/
├── package.json                # DSH bundle 清单 + dsh.client 声明 + dsh.bundle.patch
├── cordis.patch.yml            # 挂载行(把本插件插入 profile 配置树)
├── LICENSE                     # MIT
├── CHANGELOG.md                # 更新日志(0.1.0 起)
├── .gitattributes              # 文本一律 LF 入库(Windows 开发,避免整文件换行 diff)
├── .gitignore                  # 忽略 .design-ledger/(插件落盘诊断)与 DEVPLAN/state.json
├── README.md / README.en.md    # 中文 / 英文文档(逐节对照)
├── RELEASING.md                # 发版流程(只给维护者看,使用者可忽略)
├── lib/
│   ├── index.js                # 宿主侧插件:6 个工具 + 3 条面板路由 + 注入段落
│   ├── design-doc.js           # 设计文档扫描、标题分节、表格行抽取(纯逻辑,可单测)
│   ├── ledger.js               # 进度树的建/读/改 + JSON 分片存储(纯逻辑)
│   ├── inject.js               # 分层注入文本装配
│   └── agents-md.js            # AGENTS.md 托管块(幂等写入,不覆盖块外内容)
├── client/
│   └── client.js               # 浏览器半区:侧栏「设计文档包含」入口 + 面板 + 选择器
├── skills/
│   └── design-ledger/
│       └── SKILL.md            # 配套技能:如何建台账、如何随开发更新
├── .github/workflows/ci.yml    # CI:语法检查 + 单测 + 文档体检(零依赖,刻意不跑探针)
├── test/                       # 零依赖断言测试(node --test,13 个用例)
└── probe/                      # 自检探针(可重复运行的契约/取数/建树验证)
    ├── register.mjs            # 注册解析钩子(配合 resolve-hook.mjs)
    ├── contract-test.mjs       # ⭐ 契约自检:注入文本必须是字符串 / 6 工具注册 / 路由注册
    ├── panel-list-test.mjs     # ⭐ 面板取数:会话工作区解析 + 越界拒绝
    ├── table-tree.mjs          # ⭐ 功能表行 → 功能节点(真实设计文档,只读)
    ├── readme-check.mjs        # 两版 README 体检:结构对齐 / 围栏成对 / 链接可解析(npm run check:readme)
    ├── client-ui-test.mjs      # 客户端体检:按钮对比度 token / 字典完整性 / 语言接线(npm run check:client)
    ├── publish-audit.mjs       # 发布审计:与 GitHub 逐文件比对(Git blob 哈希)+ 隐私体检(npm run audit)
    ├── reorder-crashes.mjs     # 维护脚本:把「崩溃 N」小节重排为升序(发布前跑一次)
    ├── host-smoke.mjs          # 宿主端到端(假 ctx 跑完整流程)
    ├── route-test.mjs          # 路由行为与路径安全
    ├── deep-tree.mjs           # 多层目录树(系统→子系统→功能→子功能)
    ├── import-probe.mjs        # 能否 import、导出形状
    └── resolve-hook.mjs        # 解析实现:把 @deepseek-ai/* 指向 DSH 的依赖目录

开发时验证顺序见上文「改这个插件时的验证顺序」。

跑探针前要让它找到 DSH 的依赖目录(探针要在 Node 里复现 DSH 的模块解析, 需要 @deepseek-ai/* 的真实安装位置)。probe/resolve-hook.mjs 不写死任何本机路径:

  1. 若设了 DSH_SHARED_NODE_MODULES,以它为准(指错了会立刻报错,不会偷偷回退);
  2. 否则用 DSH_HOME(默认 ~/.dsh)与 DSH_PROFILE 推导,依次尝试 <home>/profiles/node_modules、<home>/profiles/<profile>/node_modules、 <home>/profiles/desktop/node_modules;
  3. 判定标准是"这个目录里真的有 @deepseek-ai/dsh-tools",而不是"目录存在"—— 本机 profiles/desktop/node_modules 与 profiles/node_modules 都存在,但 harness 包只在后者里。
# 需要显式指定时(PowerShell)
$env:DSH_SHARED_NODE_MODULES = 'D:\AppData\.dsh\profiles\node_modules'

自动探测与显式指定都会先自检,指错了会直接给出可操作的报错,不会静默退化成弱校验。 另外:lib/、client/ 与 test/ 都不含任何本机路径,npm test 只要 Node、不需要 DSH, 所以 CI 只跑 npm run check / npm test / npm run check:readme。

许可

MIT

Comments

Loading…