dsh-paoding 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,7 @@ dsh-paoding(中文品牌「庖丁」,典出《庄子》——庖丁顺纹理
25
25
  - **HTTP 接口直连配置**:预设的列举、新增、更新、删除走 `/api/paoding/presets` 四条接口,随 DSH Web 服务同进程挂载、本机回环直调;与面板「保存并应用」同一条生成管线、同一把写盘互斥闸——脚本和自动化工具改班底,不必开浏览器。
26
26
  - **面板中英双语**:界面文案 zh/en 全覆盖,语言自动识别(显式选择 → 中文信号优先:浏览器语言、宿主页面语言、中文时区),「更多」菜单一键切换并记住。
27
27
  - **版本更新有提醒,面板一键升级**:面板会对比 npm registry(npmmirror 镜像、GitHub release 依次兜底)上的最新版本,发现新版就地提示并附上发布页链接;点「升级」就地换上新版(profile 自动探测:普通 profile 走 `dsh plugin update dsh-paoding`;desktop profile 被 dsh CLI 的 Electron 专属守卫挡住,改在 profile 目录直跑 `pnpm add dsh-paoding@<版本>`,pnpm 找不到时自动回落 `corepack pnpm`),重启 DSH 后生效,编排预设会在重启时按新版自动重生成。开发检出的 link 形态无法就地升级,面板会提示切回 registry 版的办法;检测失败时页头留一行「版本检测失败 · 重试」(完整原因与代理排查提示在悬浮提示里),不弹窗打扰。
28
- - **委派效果可量化**:面板「委派效果」卡按角色统计调用数、平均耗时与失败数(近 30 天),数据读宿主会话存档本地聚合——哪个角色用得多、哪个常掉链子,一眼可见。
28
+ - **委派效果可量化**:面板「委派效果」卡按角色统计调用数、平均耗时与失败数(近 30 天),数据读宿主会话存档本地聚合——哪个角色用得多、哪个常掉链子,一眼可见;标题行点按可折叠 / 展开(偏好记入浏览器 localStorage),收起时只留标题一行,页头「更多」/「×」恒钉右上角;缺省项目(宿主 cwd 换算)扫不到任何委派记录时自动回落为全项目聚合(卡片标注「全部项目」),冷读带日志 mtime 预过滤,窗口外的日志文件不再开读句柄。
29
29
  - **来去自由**:不改 DSH 一行源码;装完新会话即用,卸载即删目录。
30
30
  - **两代宿主都兼容**:DSH 0.1.5 – 0.2.1 全支持(0.1.5 / 0.1.6 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2+)——两代宿主发现预设的机制不同(目录扫描 vs patch 声明行),安装器自动检测宿主版本选对落点;编排组合里的 workflow worker 行也按宿主版本自动适配(0.1.5 用 workflow-worker-thread,0.1.6+ 换 workflow-ptc——旧包 0.1.6 起已删除,残留行正是 0.2.0 面板上「加载失败」的元凶);DSH 0.2.1 起角色委派行同步换代(0.2.1 的 dsh-tool-subagent 拒绝内联 persona/toolFilter——人设与工具面改挂各角色的 AgentPreset,spawn 子代理按 agentPreset 选择子预设;0.2.0 及以下保持内联形状),宿主升级、降级后的首次保存/自愈自动迁移(工作区专属预设 orchestrator-<slug> 随同轮自愈一并换代),全程无感;0.2.1 的 prerelease 再细分——tool-subagent 守卫自 0.2.1-rc.2 落地,rc.0/rc.1 仍按内联形状生成。
31
31
 
@@ -33,6 +33,7 @@ dsh-paoding 把「按角色分工的编排 preset」做成**配置驱动**:你
33
33
  | 顶层键 | 类型 | 语义 |
34
34
  |---|---|---|
35
35
  | `profile` | string | 检测时纳入哪个 profile 的 patch 层(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`);缺省 `web` |
36
+ | `patch_layer` | `'home' \| 'profile'` | 庖丁声明行写入哪个 patch 层的托管块:`home` = `$DSH_HOME/cordis.patch.yml`,`profile` = `$DSH_HOME/profiles/<profile>/cordis.patch.yml`。缺省(键缺失 / `null`)= 按环境自动判定:Android 宿主或 compat 壳(DSH 家目录在容器路径下)自动落 `profile` 层,其余环境落 `home` 层;非法值告警并忽略(按缺省自动判定)。判定细节与迁移行为见 1.4 |
36
37
  | `roles` | map | `toolName` → `{ persona?, name?, tools[], model?, provider?, start_mode?, background_mode? }`。对内置三角色(`search_external` / `design` / `implement`):覆盖 persona 与 allow 意图,并可配 `name` 显示名(只替换该角色默认 persona 首行身份句的主语,见 2.4)、专用模型(`model` / `provider`,见 2.5)、会话模式(`background_mode`,见 2.6)与启动方式(`start_mode`,见 2.7);其它键名 = **自定义角色**:生成全新 `delegation-<toolName>` 块并把 toolName 注入主 agent 的 `config.allow`(详见第 2、6 节) |
37
38
  | `roles_remove` | string[] | 整条删除的内置角色 toolName 列表(只认 `search_external` / `design` / `implement`,其它名字忽略并告警;缺省 = `[]`)。删除 = 委派行 / 工具面 / 角色 persona / 主 agent 侧委派指引全部不再生成(详见 2.3) |
38
39
  | `skills` | map | `skill` → 分配给该技能的角色 toolName 列表;安装时把 `Available skills: …` 引导句写进这些角色 persona 的尾部(详见第 7 节) |
@@ -124,6 +125,24 @@ skills: # skill -> 分配给的角色(写进角色 persona 的
124
125
  - `roles.code-reviewer` 安装后生成 `delegation-code-reviewer` 委派块,主 agent 白名单加入 `code-reviewer`,对话里直接调该 toolName 即可委派。
125
126
  - `main_agent_persona_extra` 里的 `{{cwd}}` / `{{model}}` 是 DSH 运行时替换的占位符,必须原样保留。
126
127
 
128
+ ### 1.4 patch 层判定与 Android compat 壳(dsh-maoying#1)
129
+
130
+ 庖丁的预设声明行写在 `$DSH_HOME` 下某个 `cordis.patch.yml` 的托管块里(`# --- dsh-paoding presets (auto-generated; do not edit) ---` 与结束标记之间)。patch 层有两处:
131
+
132
+ - **home 层**:`$DSH_HOME/cordis.patch.yml`(桌面 / Linux 直装环境的缺省)。
133
+ - **profile 层**:`$DSH_HOME/profiles/<profile>/cordis.patch.yml`(Android compat 壳的缺省,`<profile>` 取 `--profile` 参数 > 配置 `profile` 键 > `web`)。
134
+
135
+ **判定优先级**:配置 `patch_layer` 键 > 环境变量 `DSH_PAODING_PATCH_LAYER` > 自动判定(Android 平台或 DSH 家目录在 `/data/(user|data)/` 容器路径下 → `profile` 层;其余 → `home` 层)。
136
+
137
+ **为什么 Android 上要落 profile 层**:compat 壳(DeepSeekHarnessCompat)每次 boot 都会**重写 home 层** `cordis.patch.yml`,把 preset 注册表元数据压平成 `- id: 显示名 (英文名)` + 深缩进键的坏行——宿主在任何插件代码运行前就死于 YAML 解析(dsh-maoying#1 同款问题,跟本插件无关也会被牵连)。声明行搬进 profile 层后,compat 壳不再碰它。
138
+
139
+ 配套护栏(0.4.6 起):
140
+
141
+ - **写盘校验 + 逐字节回滚**:每次托管块写入(补行 / 撤行 / 整盘重生成)后整文件重新解析一遍,解析不过就回滚为写入前的精确字节并报错(错误信息含行号与 `dsh-paoding doctor` 指引)——文件已含坏行时,宁可不动也不把坏文件写得更坏。
142
+ - **可疑行探测**:启动自愈与 `dsh-paoding doctor` 会扫描托管块**外**的 `- id:` 坏行(id 含空白或非 ASCII,形如 compat 壳压平产物),只警告点名行号不自动删。
143
+ - **层迁移**:目标层是 `profile` 而目标层文件存在时,写入成功后会自动撤掉 home 层的庖丁托管块(用户内容逐字节保留);反方向同理(`reconcilePatchLayers`)。
144
+ - **doctor 双层体检**:托管块损坏 / 整文件解析失败为 error(后者 = 宿主启动必死),可疑行为 warn,托管块落在非目标层报「层漂移」warn(下次自愈 / 保存会迁移)。
145
+
127
146
  ## 2. 默认角色微调
128
147
 
129
148
  ### 2.1 persona 与 tools 的覆盖语义
@@ -33,6 +33,7 @@ The key names below match `normalizeConfig` in `tools/lib/config.mjs` — the si
33
33
  | Top-level key | Type | Semantics |
34
34
  |---|---|---|
35
35
  | `profile` | string | Which profile's patch layer (`$DSH_HOME/profiles/<profile>/cordis.patch.yml`) is scanned during detection; default `web` |
36
+ | `patch_layer` | `'home' \| 'profile'` | Which patch layer receives Paoding's managed preset rows: `home` = `$DSH_HOME/cordis.patch.yml`, `profile` = `$DSH_HOME/profiles/<profile>/cordis.patch.yml`. Default (key missing / `null`) = automatic per environment: Android hosts and compat shells (DSH home under a container path) land on the `profile` layer, everything else on the `home` layer; an invalid value is warned about and ignored (falls back to automatic). See 1.4 for the full decision chain and migration behavior |
36
37
  | `roles` | map | `toolName` → `{ persona?, name?, tools[], model?, provider?, start_mode?, background_mode? }`. For the three built-in roles (`search_external` / `design` / `implement`): overrides the persona and the allow intent, and may carry a `name` display name (rewrites the subject of that role's default persona identity sentence and prefixes its delegation labels — see 2.4), a dedicated model (`model` / `provider`, see 2.5), a session mode (`background_mode`, see 2.6) and a start mode (`start_mode`, see 2.7). Any other key is a **custom role**: the installer emits a fresh `delegation-<toolName>` block and injects the toolName into the main agent's `config.allow` (sections 2 and 6) |
37
38
  | `roles_remove` | string[] | toolNames of built-in roles deleted wholesale (only `search_external` / `design` / `implement` are accepted; other names are ignored with a warning; default `[]`). Deleting means the delegation block / tool surface / role persona / main-agent delegation guidance are no longer generated (see 2.3) |
38
39
  | `skills` | map | `skill` → the list of role toolNames the skill is assigned to; the installer appends an `Available skills: …` guidance sentence to those roles' personas (section 7) |
@@ -124,6 +125,24 @@ Notes:
124
125
  - After installation, `roles.code-reviewer` becomes a generated `delegation-code-reviewer` block, `code-reviewer` joins the main-agent allow list, and the main agent can delegate to it directly by calling that toolName.
125
126
  - `{{cwd}}` and `{{model}}` inside `main_agent_persona_extra` are persona placeholders that DSH substitutes at runtime — keep them verbatim.
126
127
 
128
+ ### 1.4 Patch layer selection and the Android compat shell (dsh-maoying#1)
129
+
130
+ Paoding writes its preset rows into the managed block (`# --- dsh-paoding presets (auto-generated; do not edit) ---` … end marker) of one `cordis.patch.yml` under `$DSH_HOME`. There are two patch layers:
131
+
132
+ - **home layer**: `$DSH_HOME/cordis.patch.yml` (the default on desktop / plain-Linux installs).
133
+ - **profile layer**: `$DSH_HOME/profiles/<profile>/cordis.patch.yml` (the default on the Android compat shell; `<profile>` resolves from the `--profile` argument > the config `profile` key > `web`).
134
+
135
+ **Decision order**: the config `patch_layer` key > the `DSH_PAODING_PATCH_LAYER` environment variable > automatic detection (Android platform, or the DSH home under a `/data/(user|data)/` container path → the `profile` layer; otherwise → the `home` layer).
136
+
137
+ **Why Android lands on the profile layer**: the compat shell (DeepSeekHarnessCompat) rewrites the home-layer `cordis.patch.yml` on every boot, flattening the preset-registry metadata into malformed rows like `- id: Display Name (English Name)` with deeply indented keys — the host dies at YAML parse time before any plugin code runs (the dsh-maoying#1 failure shape; it takes down the host even when Paoding is not involved). With the rows living in the profile layer, the compat shell no longer touches them.
138
+
139
+ Guard rails shipped alongside (since 0.4.6):
140
+
141
+ - **Write validation + exact-byte rollback**: every managed-block write (row backfill / row strip / full regen) re-parses the whole file afterwards; if it fails to parse, the file is rolled back to the exact pre-write bytes and an error is raised (with a line number and a `dsh-paoding doctor` hint). When a file already contains bad rows, doing nothing beats writing it into a worse state.
142
+ - **Suspect-row detection**: startup self-heal and `dsh-paoding doctor` scan for `- id:` rows **outside** the managed block whose id contains whitespace or non-ASCII characters (the compat-shell flattening shape); they warn with line numbers and never auto-delete.
143
+ - **Layer migration**: when the target layer is `profile` and that file exists, a successful write also strips Paoding's managed block from the home layer (user content preserved byte-for-byte); the reverse direction works the same way (`reconcilePatchLayers`).
144
+ - **Doctor checks both layers**: a damaged managed block and a whole-file parse failure are errors (the latter means the host cannot boot at all); suspect rows are warnings; a managed block sitting in a non-target layer reports a "layer drift" warning (the next self-heal / save migrates it).
145
+
127
146
  ## 2. Tuning the built-in roles
128
147
 
129
148
  ### 2.1 Override semantics of persona and tools
@@ -48,7 +48,7 @@ dsh plugin --profile web add dsh-paoding
48
48
 
49
49
  ### profile 语义
50
50
 
51
- `--profile web` 决定插件装进哪个 profile:「庖丁配置」面板只在以该 profile 启动的 DSH Web 里出现。`web` 是 `dsh web` 的默认 profile,多数机器无需改动;多 profile 用户对每个需要的 profile 各执行一次 `dsh plugin add`。编排预设本体落在 `$DSH_HOME/.agent-presets/`,全机共享、不随 profile 走——DSH ≥ 0.1.7 的声明行写在 home 层 `$DSH_HOME/cordis.patch.yml`,同样与 profile 无关——在任何 profile 的面板里「保存并应用」,写的都是同一份预设。
51
+ `--profile web` 决定插件装进哪个 profile:「庖丁配置」面板只在以该 profile 启动的 DSH Web 里出现。`web` 是 `dsh web` 的默认 profile,多数机器无需改动;多 profile 用户对每个需要的 profile 各执行一次 `dsh plugin add`。编排预设本体落在 `$DSH_HOME/.agent-presets/`,全机共享、不随 profile 走——DSH ≥ 0.1.7 的声明行缺省写在 home 层 `$DSH_HOME/cordis.patch.yml`(Android compat 壳或配置 `patch_layer: profile` 时改写 profile 层 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`,见下节「patch 层与写盘护栏」)——在任何 profile 的面板里「保存并应用」,写的都是同一份预设。
52
52
 
53
53
  ### 版本支持与预设落点双轨(DSH 0.1.5 – 0.2.1)
54
54
 
@@ -57,7 +57,7 @@ dsh plugin --profile web add dsh-paoding
57
57
  两代宿主发现「本地 authored preset」的机制不同,安装器据此分双轨落点,宿主检测全自动、用户无感:
58
58
 
59
59
  - **目录轨(DSH ≤ 0.1.6)**:宿主扫描 `$DSH_HOME/.agent-presets/` 下的目录来发现 preset,目录即注册。安装器照旧把产物写成 `$DSH_HOME/.agent-presets/orchestrator/`(`agent.cordis.yml` + `preset.yml` + `restrict.mjs` + `delegation-labels.mjs`);按工作区配置生成的预设落同目录下的 `orchestrator-<slug>`。
60
- - **声明行轨(DSH ≥ 0.1.7)**:宿主不再扫目录,改为读取 patch 里的 `@deepseek-ai/dsh-agent-preset` 声明行。安装器在 `$DSH_HOME/cordis.patch.yml` 里维护一个托管块——以 `# --- dsh-paoding presets (auto-generated; do not edit) ---` 起始、以配套的 end 标记收尾——块内用 `- insert:` 写声明行:全局预设 `config.id: orchestrator`,工作区预设 `config.id: orchestrator-<slug>`,各自的 `plugins` 内联全部插件行;`restrict.mjs` / `delegation-labels.mjs` 以绝对 `file:` URL 引用。产物四件套仍落在 `.agent-presets/<id>/`,只是从「目录即注册」变成「文件存放处」。托管块之外的用户内容一字节不动;块内带 do not edit 标记,手改块内内容会在下次保存时被覆盖。
60
+ - **声明行轨(DSH ≥ 0.1.7)**:宿主不再扫目录,改为读取 patch 里的 `@deepseek-ai/dsh-agent-preset` 声明行。安装器在目标 patch 层(缺省 home 层 `$DSH_HOME/cordis.patch.yml`;Android compat 壳 / `patch_layer: profile` 时为 profile 层 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`,见下节「patch 层与写盘护栏」)里维护一个托管块——以 `# --- dsh-paoding presets (auto-generated; do not edit) ---` 起始、以配套的 end 标记收尾——块内用 `- insert:` 写声明行:全局预设 `config.id: orchestrator`,工作区预设 `config.id: orchestrator-<slug>`,各自的 `plugins` 内联全部插件行;`restrict.mjs` / `delegation-labels.mjs` 以绝对 `file:` URL 引用。产物四件套仍落在 `.agent-presets/<id>/`,只是从「目录即注册」变成「文件存放处」。托管块之外的用户内容一字节不动;块内带 do not edit 标记,手改块内内容会在下次保存时被覆盖。
61
61
 
62
62
  安装器按下面的顺序判定走哪条轨:
63
63
 
@@ -94,6 +94,10 @@ dsh plugin --profile web add dsh-paoding
94
94
 
95
95
  面板不监视配置文件改动;想立刻按当前配置重新生成,点面板「保存并应用」。
96
96
 
97
+ ### patch 层与写盘护栏(Android compat 壳,dsh-maoying#1)
98
+
99
+ 声明行写在哪一层由判定链决定:配置 `patch_layer` 键 > 环境变量 `DSH_PAODING_PATCH_LAYER` > 自动判定(Android 平台或 DSH 家目录在 `/data/(user|data)/` 容器路径下 → profile 层;其余 → home 层)。为什么 Android 要落 profile 层:compat 壳(DeepSeekHarnessCompat)每次 boot 重写 home 层 `cordis.patch.yml`,把 preset 注册表元数据压平成 `- id: 显示名 (英文名)` + 深缩进键的坏行,宿主在任何插件代码运行前就死于 YAML 解析(dsh-maoying#1 同款问题)——声明行搬进 profile 层后 compat 壳不再碰它;层切换后的首次写入会自动撤掉另一层的庖丁托管块(用户内容逐字节保留)。写盘护栏:每次托管块写入后整文件重新解析一遍,解析不过立即回滚为写入前字节并报错(含行号与 `dsh-paoding doctor` 指引);启动自愈与 `dsh-paoding doctor` 会扫描托管块外的可疑坏行(id 含空白或非 ASCII,形如 compat 壳压平产物),只警告点名行号不自动删——`dsh-paoding doctor` 的双层体检把「整文件解析失败」报为 error(宿主启动必死),可疑行为 warn,托管块落在非目标层报「层漂移」warn。
100
+
97
101
  ### 基础模板与工具勾选
98
102
 
99
103
  全新安装(无配置文件)生成的基础模板只含 DSH 自带的基础工具——主 agent 21 项核心工具,加各委派角色的基础工具集;检测到的 host/MCP 工具**默认不写入**。检测管线照常运行:点开左侧栏「庖丁配置」,候选清单列出全部已识别工具,按需勾选后点「保存并应用」即可。
@@ -203,7 +207,7 @@ patch 与配置文件用 **`yaml` 包**解析——通过 `createRequire` 从 `$
203
207
  - **场景模板**:页头三档切换(轻量:只留实现角色;调研:只留检索角色;全开:全员到齐、主 agent 额外工具带满检测库存),一键换「班底」;只动 `roles_remove` 与 `main_agent_extra`,各角色精调过的工具面、persona、模型、启动方式分毫不动,切回即原样复原。
204
208
  - **检查提示卡**:每次「预览生成 / 保存并应用」自动过一遍体检(配置了未挂载的工具、零工具角色、查无此名的模型、预设落后于配置),分级提示以卡片形式贴在页头下方,处理完再保存不迟。
205
209
  - **备份与回滚**:保存、导入、恢复配置前都会自动把当前配置轮换备份三份(`config.yml.bak-1..3`,新压旧);「更多」→「恢复备份…」列时间与大小、二次确认后回退,恢复前当前配置还会再自动备份一份——误操作有后悔药。
206
- - **委派效果统计**:页头「委派效果」卡按角色统计被委派的调用数、平均耗时与失败数(近 30 天,`days` 参数可调)。数据来自宿主会话存档的冷读——经宿主 `sessionPersistence` 服务逐会话抽摘要、本地聚合,不出机、不带负载;失败记的是子会话零轮次或轮次中断(spawn 后即死、中途终止两种形态)。宿主缺会话服务时整卡不渲染。
210
+ - **委派效果统计**:页头「委派效果」卡按角色统计被委派的调用数、平均耗时与失败数(近 30 天,`days` 参数可调)。数据来自宿主会话存档的冷读——经宿主 `sessionPersistence` 服务逐会话抽摘要、本地聚合,不出机、不带负载;失败记的是子会话零轮次或轮次中断(spawn 后即死、中途终止两种形态)。缺省项目(宿主 cwd 换算)扫不到任何委派记录时自动回落为全项目聚合(响应 `project='all'`,卡片标注「全部项目」);冷读带日志 mtime 预过滤,窗口外的日志文件不再开读句柄。标题行点按可折叠 / 展开(偏好记入浏览器 localStorage,默认展开),收起时只留标题一行。宿主缺会话服务时整卡不渲染。
207
211
  - **「更多」菜单**:配置导出(下载 `dsh-paoding.config.yml` 原文)/ 导入(先验格式,整份覆盖含工作区段,需二次确认)/ 恢复备份(见上条)与面板语言切换(中英自动识别、手动选择优先并记住)。
208
212
 
209
213
  ## 7. 卸载
@@ -48,7 +48,7 @@ The orchestrator preset needs no separate install — the plugin fills it in aut
48
48
 
49
49
  ### Profile semantics
50
50
 
51
- `--profile web` decides which profile the plugin is installed into: the Paoding Config page appears only in a DSH Web launched with that profile. `web` is the default profile of `dsh web`, so most machines need no change; multi-profile users run `dsh plugin add` once per profile they want it in. The orchestrator preset itself lives under `$DSH_HOME/.agent-presets/` and is shared machine-wide, independent of profiles — under DSH ≥ 0.1.7 the declaration rows go into the home-layer `$DSH_HOME/cordis.patch.yml`, equally profile-independent — Save & Apply from any profile's panel writes the same preset.
51
+ `--profile web` decides which profile the plugin is installed into: the Paoding Config page appears only in a DSH Web launched with that profile. `web` is the default profile of `dsh web`, so most machines need no change; multi-profile users run `dsh plugin add` once per profile they want it in. The orchestrator preset itself lives under `$DSH_HOME/.agent-presets/` and is shared machine-wide, independent of profiles — under DSH ≥ 0.1.7 the declaration rows go into the home-layer `$DSH_HOME/cordis.patch.yml` by default (on the Android compat shell, or with `patch_layer: profile` in the config, they go into the profile layer at `$DSH_HOME/profiles/<profile>/cordis.patch.yml` instead — see "Patch layers and write guard rails" below) — Save & Apply from any profile's panel writes the same preset.
52
52
 
53
53
  ### Version support and the dual preset landing (DSH 0.1.5 – 0.2.1)
54
54
 
@@ -57,7 +57,7 @@ The plugin supports two host generations: **DSH 0.1.5 / 0.1.6 / 0.1.7-rc.1 / 0.1
57
57
  The two generations discover "local authored presets" differently, so the installer maintains a dual landing, detecting the host automatically — nothing for the user to do:
58
58
 
59
59
  - **Directory track (DSH ≤ 0.1.6)**: the host discovers presets by scanning directories under `$DSH_HOME/.agent-presets/` — the directory is the registration. The installer writes the artifacts to `$DSH_HOME/.agent-presets/orchestrator/` (`agent.cordis.yml` + `preset.yml` + `restrict.mjs` + `delegation-labels.mjs`) as before; workspace-specific presets land in sibling `orchestrator-<slug>` directories.
60
- - **Declaration track (DSH ≥ 0.1.7)**: the host no longer scans directories; it reads `@deepseek-ai/dsh-agent-preset` declaration rows from the patch instead. The installer maintains a managed block in `$DSH_HOME/cordis.patch.yml` — opening with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closing with the matching end marker — whose `- insert:` rows write the declarations: the global preset gets `config.id: orchestrator`, a workspace preset gets `config.id: orchestrator-<slug>`, each with all plugin rows inlined under `plugins`; `restrict.mjs` / `delegation-labels.mjs` are referenced by absolute `file:` URLs. The artifact quartet still lands in `.agent-presets/<id>/` — demoted from "the directory is the registration" to "a place where the files live". User content outside the block is left untouched, byte for byte; the block carries a do-not-edit marker, and anything hand-edited inside it is overwritten by the next save.
60
+ - **Declaration track (DSH ≥ 0.1.7)**: the host no longer scans directories; it reads `@deepseek-ai/dsh-agent-preset` declaration rows from the patch instead. The installer maintains a managed block in the target patch layer (home layer `$DSH_HOME/cordis.patch.yml` by default; on the Android compat shell or with `patch_layer: profile`, the profile layer at `$DSH_HOME/profiles/<profile>/cordis.patch.yml` — see "Patch layers and write guard rails" below) — opening with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closing with the matching end marker — whose `- insert:` rows write the declarations: the global preset gets `config.id: orchestrator`, a workspace preset gets `config.id: orchestrator-<slug>`, each with all plugin rows inlined under `plugins`; `restrict.mjs` / `delegation-labels.mjs` are referenced by absolute `file:` URLs. The artifact quartet still lands in `.agent-presets/<id>/` — demoted from "the directory is the registration" to "a place where the files live". User content outside the block is left untouched, byte for byte; the block carries a do-not-edit marker, and anything hand-edited inside it is overwritten by the next save.
61
61
 
62
62
  The installer decides which track to take in this order:
63
63
 
@@ -94,6 +94,10 @@ The `.generator-version` marker is written by the plugin's startup self-heal aft
94
94
 
95
95
  The panel does not watch the config file; to regenerate from the current config right away, hit Save & Apply.
96
96
 
97
+ ### Patch layers and write guard rails (Android compat shell, dsh-maoying#1)
98
+
99
+ Which layer receives the declaration rows follows a decision chain: the config `patch_layer` key > the `DSH_PAODING_PATCH_LAYER` environment variable > automatic detection (Android platform, or the DSH home under a `/data/(user|data)/` container path → the profile layer; otherwise → the home layer). Why Android lands on the profile layer: the compat shell (DeepSeekHarnessCompat) rewrites the home-layer `cordis.patch.yml` on every boot, flattening the preset-registry metadata into malformed rows like `- id: Display Name (English Name)` with deeply indented keys, and the host dies at YAML parse time before any plugin code runs (the dsh-maoying#1 failure shape) — with the rows living in the profile layer, the compat shell no longer touches them; the first write after a layer switch also strips Paoding's managed block from the other layer (user content preserved byte for byte). Write guard rails: every managed-block write re-parses the whole file afterwards, and on failure rolls back to the exact pre-write bytes and raises an error (with a line number and a `dsh-paoding doctor` hint); startup self-heal and `dsh-paoding doctor` scan for suspect rows outside the managed block (ids containing whitespace or non-ASCII characters — the compat-shell flattening shape) and warn with line numbers without auto-deleting — the doctor's dual-layer check reports a whole-file parse failure as an error (the host cannot boot at all), suspect rows as warnings, and a managed block sitting in a non-target layer as a "layer drift" warning.
100
+
97
101
  ### Base template and tool opt-in
98
102
 
99
103
  A fresh install (no config file) generates the base template: only DSH's built-in base tools — the main agent's 21 core tools plus each delegated role's base set. Detected host/MCP tools are **not written in** by default. The detection pipeline still runs: open 庖丁配置 in the sidebar, the candidate list shows every recognized tool, opt in as needed, and hit Save & Apply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-paoding",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "庖丁 — DSH (DeepSeek Harness) 编排模式安装器:host 感知的角色工具过滤 + Web 设置页可视化配置器",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@ import { assignSlugs, collectState, generateAndInstall, presetIdOf } from '../..
14
14
  import { autoAssignments, baseAssignments } from '../../tools/lib/alloc.mjs'
15
15
  import { computeRestrictAllow } from '../../tools/lib/compose.mjs'
16
16
  import { loadConfig, normalizeConfig, normalizeTargetFields, saveConfig, serializeConfig, stripNonTargetKeys } from '../../tools/lib/config.mjs'
17
- import { parseYamlSubset, preprocessPatchText } from '../../tools/lib/yaml.mjs'
17
+ import { loadYaml, parseYamlSubset, preprocessPatchText } from '../../tools/lib/yaml.mjs'
18
18
  import { captureWarnings, healthCheck } from '../../tools/lib/health.mjs'
19
19
  import { ROLES, warn } from '../../tools/lib/util.mjs'
20
20
  import {
@@ -22,6 +22,7 @@ import {
22
22
  detectAgentRolesFlavor,
23
23
  detectHostVersionInfo,
24
24
  detectPresetSystem,
25
+ findSuspectPresetRows,
25
26
  formatGeneratorMarker,
26
27
  isPtcWorkerHost,
27
28
  listOrchestratorPresetIds,
@@ -29,6 +30,7 @@ import {
29
30
  presetRolesFlavorOf,
30
31
  presetRowId,
31
32
  readHomePatchRows,
33
+ resolvePatchTargetLayer,
32
34
  stripHomePatchRows,
33
35
  ROLES_AGENT_PRESET,
34
36
  WORKER_PTC,
@@ -75,6 +77,37 @@ export function resolveConfigFile() {
75
77
  return process.env.DSH_PAODING_CONFIG || path.join(resolveDshHome(), 'dsh-paoding.config.yml')
76
78
  }
77
79
 
80
+ /**
81
+ * patch 层解析(自愈 / 删除工作区预设等动 patch 的路径入口各解析一次):
82
+ * 读一次配置(loadConfig 失败按 null 对待——层判定不被配置错误牵连,配置问题
83
+ * 由各写盘路径自己的护栏处理),经 resolvePatchTargetLayer 走完整判定链
84
+ * (patch_layer 键 > DSH_PAODING_PATCH_LAYER > compat 壳探测 > home)。
85
+ */
86
+ function resolveLayerTarget(dshHome, config = null) {
87
+ return resolvePatchTargetLayer(dshHome, { config })
88
+ }
89
+
90
+ /**
91
+ * 两层 patch 文件的可疑行扫描(maoying 坏行形状探测,WARN 级,只记不删——
92
+ * 自动删行会踩用户手写内容)。目标层与 legacy 层都扫:compat 壳把坏行留在
93
+ * home 层的场景里,启动自愈只重写 profile 层,home 层的坏行只有这里能被看见。
94
+ * 读失败静默跳过(该层没这文件很正常)。
95
+ */
96
+ function warnSuspectPatchRows(dshHome, layerTarget) {
97
+ for (const patchPath of [layerTarget.patchPath, layerTarget.legacyPatchPath]) {
98
+ let text = null
99
+ try {
100
+ text = readFileSync(patchPath, 'utf8')
101
+ } catch {
102
+ continue
103
+ }
104
+ for (const suspect of findSuspectPresetRows(text)) {
105
+ warn(`${patchPath} 第 ${suspect.lineNo} 行检出可疑声明行 "- id: ${suspect.id}"(id 含空白或非 ASCII,` +
106
+ '形如 compat 壳压平的坏行,会让宿主 YAML 解析失败)——运行 `dsh-paoding doctor` 查看与清理')
107
+ }
108
+ }
109
+ }
110
+
78
111
  // ── state cache ─────────────────────────────────────────────────────────────
79
112
 
80
113
  let stateCache = null
@@ -583,7 +616,11 @@ export function deleteWorkspacePreset(state, workspacePath) {
583
616
  rmSync(presetDir, { recursive: true, force: true })
584
617
  removedPresetDir = true
585
618
  }
586
- stripHomePatchRows(dshHome, [presetRowId(presetId)])
619
+ // 声明行撤在目标层(prev 配置在场:patch_layer / profile 键生效;compat 壳
620
+ // 环境自动落 profile 层)。撤哪层必须与当初写哪层对齐,否则删工作区会在
621
+ // home 层空撤一轮、profile 层残留孤儿声明行。
622
+ const layerTarget = resolveLayerTarget(dshHome, prev)
623
+ stripHomePatchRows(dshHome, [presetRowId(presetId)], { layer: layerTarget.layer, profileName: layerTarget.profileName })
587
624
 
588
625
  return { presetId, removedConfigEntry, removedPresetDir }
589
626
  }
@@ -695,6 +732,17 @@ export async function ensurePresetInstalled({ dshHome = resolveDshHome(), genera
695
732
  marker = parseGeneratorMarker(await readFile(markerFile, 'utf8'))
696
733
  } catch { /* 目录或标记缺失:按未生成处理 */ }
697
734
 
735
+ // 目标 patch 层解析一次(配置读取失败按 null 对待——层判定不被配置错误
736
+ // 牵连),后续补行 / 读行都落目标层(compat 壳环境即 profile 层);顺手扫
737
+ // 两层可疑坏行(WARN 级只留轨迹,绝不阻塞自愈——诊断归 doctor)。
738
+ let ensureConfig = null
739
+ try {
740
+ ensureConfig = loadConfig(resolveConfigFile(), loadYaml(dshHome))
741
+ } catch { /* 配置解析失败:层按自动判定走 */ }
742
+ const layerTarget = resolveLayerTarget(dshHome, ensureConfig)
743
+ const patchOpts = { layer: layerTarget.layer, profileName: layerTarget.profileName }
744
+ warnSuspectPatchRows(dshHome, layerTarget)
745
+
698
746
  if (marker !== null && marker.version === generatorVersion) {
699
747
  // 版本一致:内容原则上无需重生成,但宿主可能已随 dsh 升降级翻转轨道或
700
748
  // 跨过 workflow worker 形态 / 角色委派行形态边界(只升 dsh 不动插件是升级
@@ -709,7 +757,7 @@ export async function ensurePresetInstalled({ dshHome = resolveDshHome(), genera
709
757
  if (shouldAbort?.()) return { installed: false, reason: PRESET_ENSURE_ABORTED }
710
758
  const presetIds = listOrchestratorPresetIds(dshHome)
711
759
  if (system === 'declarative') {
712
- const backfilled = backfillHomePatchRows(dshHome, presetIds)
760
+ const backfilled = backfillHomePatchRows(dshHome, presetIds, patchOpts)
713
761
  if (marker.track === 'declarative' && backfilled.length === 0) {
714
762
  return { installed: false, reason: 'preset 已由当前版本生成器产出,无需重生成' }
715
763
  }
@@ -720,7 +768,8 @@ export async function ensurePresetInstalled({ dshHome = resolveDshHome(), genera
720
768
  }
721
769
  // directory 轨:声明行残留会让 0.1.6 profile 启动失败——有残留走整盘
722
770
  // 重生成(directory 分支撤块自愈);行清白时只升级标记格式,不动内容。
723
- if (readHomePatchRows(dshHome).length === 0) {
771
+ // 读的是目标层(compat 壳环境下 home 层残留由 reconcile 另行处理)。
772
+ if (readHomePatchRows(dshHome, patchOpts).length === 0) {
724
773
  if (marker.track === 'directory') {
725
774
  return { installed: false, reason: 'preset 已由当前版本生成器产出,无需重生成' }
726
775
  }
@@ -759,8 +808,11 @@ export async function ensurePresetInstalled({ dshHome = resolveDshHome(), genera
759
808
  const track = result.presetSystem === 'declarative' ? 'declarative' : 'directory'
760
809
  writeFileSync(markerFile, formatGeneratorMarker(generatorVersion, track, workerFlavorOf(state.hostVersionTriple), rolesFlavorOf(state.hostVersionTriple, state.hostPrerelease)), 'utf8')
761
810
  // 声明轨上整盘重生成只重建本 preset 的行:其余工作区 preset 目录若在而
762
- // 声明行缺(轨道翻转后首次触发本路径),一并补齐。
763
- const backfilled = track === 'declarative' ? backfillHomePatchRows(dshHome, listOrchestratorPresetIds(dshHome)) : []
811
+ // 声明行缺(轨道翻转后首次触发本路径),一并补齐(落目标层;
812
+ // generateAndInstall 内部同口径解析层并在 profile 层写成功后 reconcile 撤
813
+ // home 遗留块)。校验失败(如文件已含坏行)在此被外层 catch 收成 error,
814
+ // 自愈绝不带崩插件启动。
815
+ const backfilled = track === 'declarative' ? backfillHomePatchRows(dshHome, listOrchestratorPresetIds(dshHome), patchOpts) : []
764
816
  // 工作区预设角色行形态换代(宿主跨 0.2.1 边界后的存量 orchestrator-<slug>
765
817
  // 产物):基础重生成只换了全局预设,工作区产物不跟手就会继续被守卫拒绝。
766
818
  const migratedPresets = migrateWorkspacePresetRoles({ dshHome, state })
@@ -453,12 +453,15 @@ export async function apply(ctx) {
453
453
  if (route === 'stats' && req.method === 'GET') {
454
454
  // 委派效果统计(GET stats?days=30&project=<slug>):宿主
455
455
  // sessionPersistence 服务逐会话冷读抽摘要聚合(A 路,见
456
- // stats-core.mjs 文件头——服务面、slug 口径与失败判据的实测证据都在
457
- // 那里)。服务经 ctx.reflect 软探测(绝不进 inject 数组:硬注入会让
458
- // 缺服务的老宿主在挂载时就炸),拿不到回 { ok:false,
456
+ // stats-core.mjs 文件头——服务面、slug 口径、mtime 预过滤与失败判据
457
+ // 的实测证据都在那里)。服务经 ctx.reflect 软探测(绝不进 inject 数
458
+ // 组:硬注入会让缺服务的老宿主在挂载时就炸),拿不到回 { ok:false,
459
459
  // reason:'persistence-unavailable' },客户端整卡不渲染。project
460
- // 缺省 = 当前面板项目(cwd → projectKey 换算),days 缺省 30 上限
461
- // 365;project 非法在核心层抛 422(外层 catch 统一回 { error })。
460
+ // 缺省 = 当前面板项目(cwd → projectKey 换算),该 slug 零委派记录
461
+ // 时核心层自动回落全项目聚合(响应 project='all',面板标题标注
462
+ // 「全部项目」;desktop 宿主进程 cwd 停在 profile 目录、换算 slug
463
+ // 恒无会话,靠这一步救回卡片),显式传 project 不回落;days 缺省 30
464
+ // 上限 365;project 非法在核心层抛 422(外层 catch 统一回 { error })。
462
465
  // 路由注册在既有 /api/paoding 前缀下,上方信任围栏自动覆盖本路由。
463
466
  json(res, 200, await collectDelegationStats(ctx, {
464
467
  project: url.searchParams.get('project') ?? undefined,
@@ -1325,7 +1325,9 @@ html[data-dsh-paoding-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
1325
1325
  .pd-pageHeader {
1326
1326
  flex: none;
1327
1327
  display: flex;
1328
- align-items: center;
1328
+ /* 顶格对齐:标题列内含委派效果卡(懒加载后撑高整列)时,「更多」/「×」
1329
+ 仍钉在页头右上角,不随 align-items:center 的纵向居中漂到半腰。 */
1330
+ align-items: flex-start;
1329
1331
  gap: 12px;
1330
1332
  width: 100%;
1331
1333
  padding: 18px 28px 10px;
@@ -1402,6 +1404,52 @@ html[data-dsh-paoding-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
1402
1404
  color: var(--dsw-alias-label-primary);
1403
1405
  }
1404
1406
 
1407
+ /* 标题行整行按钮:点按切换展开 / 收起(pd-delegateHead 同族观感,缩一号:
1408
+ 卡内嵌在页头,hover 面积过大反而抢眼)。chevron 靠右(margin-left:auto),
1409
+ 展开态旋 90° 指下——与委派调用行的 pd-delegateChev 同一语义。 */
1410
+ .pd-delegHead {
1411
+ box-sizing: border-box;
1412
+ display: flex;
1413
+ align-items: center;
1414
+ gap: 4px;
1415
+ width: 100%;
1416
+ padding: 1px 4px;
1417
+ margin: -1px -4px 0;
1418
+ border: 0;
1419
+ border-radius: 6px;
1420
+ background: transparent;
1421
+ cursor: pointer;
1422
+ text-align: left;
1423
+ font: inherit;
1424
+ transition: background 0.12s ease;
1425
+ }
1426
+
1427
+ .pd-delegHead:hover,
1428
+ .pd-delegHead:focus-visible {
1429
+ background: var(--dsw-alias-interactive-bg-hover);
1430
+ outline: none;
1431
+ }
1432
+
1433
+ .pd-delegChev {
1434
+ margin-left: auto;
1435
+ flex: none;
1436
+ font-size: 12px;
1437
+ line-height: 18px;
1438
+ color: var(--dsw-alias-label-tertiary);
1439
+ transition: transform 0.12s ease;
1440
+ }
1441
+
1442
+ .pd-delegChevOpen { transform: rotate(90deg); }
1443
+
1444
+ /* 标题尾角标:缺省项目零记录回落全项目聚合(project='all')时点明口径——
1445
+ 弱化字号与三级文字色,紧跟标题不抢视觉。 */
1446
+ .pd-delegScope {
1447
+ margin-left: 6px;
1448
+ font-size: 0.85em;
1449
+ font-weight: 400;
1450
+ color: var(--dsw-alias-label-tertiary);
1451
+ }
1452
+
1405
1453
  .pd-delegTable {
1406
1454
  margin-top: 6px;
1407
1455
  font-size: 11px;
@@ -1885,6 +1933,7 @@ html[data-dsh-paoding-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
1885
1933
  @media (prefers-reduced-motion: reduce) {
1886
1934
  .pd-delegateRunning { animation: none; }
1887
1935
  .pd-delegateChev { transition: none; }
1936
+ .pd-delegChev { transition: none; }
1888
1937
  .pd-delegateHead { transition: none; }
1889
1938
  }
1890
1939
 
@@ -475,6 +475,10 @@ window.__ModuleLoader__.load({
475
475
  "del.colLastUsed": { zh: "最近使用", en: "Last used" },
476
476
  "del.totalRoles": { zh: "共 {n} 个角色", en: "{n} roles in total" },
477
477
  "del.empty": { zh: "暂无委派记录", en: "No delegation records yet" },
478
+ // 标题角标:响应 project='all'(缺省项目零记录,服务端回落全项目聚合)时点明统计口径
479
+ "del.scopeAll": { zh: "全部项目", en: "all projects" },
480
+ // 标题行按钮的悬浮提示(title 属性):展开 / 收起切换的 affordance
481
+ "del.toggleTip": { zh: "展开 / 收起委派效果", en: "Expand / collapse delegation stats" },
478
482
 
479
483
  // ── 预览「本次变更」区块(preview.diff 结构化差分的渲染文案)───────────
480
484
  // 服务端 diff 是纯结构零文案(tools/lib/config.mjs diffAssignments),
@@ -662,14 +666,27 @@ window.__ModuleLoader__.load({
662
666
  }
663
667
 
664
668
  // 卡片渲染:stats 缺失 / ok!==true / roles 非数组 → null(整卡零占位)。
665
- // delegatedSessions=0 渲染标题 + 一行空文案;有数据渲染紧凑表(表头 +
666
- // 最多 8 行,超出给「共 N 个角色」尾行),失败数 >0 标红提醒。
667
- function delegationCardOf(stats) {
669
+ // 标题行是整行按钮(pd-delegHead):点按经 onToggle 切换 open(PageShell
670
+ // 持态并写 localStorage 记忆);收起时表格 / 尾行 / 空文案一律不渲染,
671
+ // 只剩标题 + 指向 chevron。delegatedSessions=0 展开态渲染一行空文案;
672
+ // 有数据渲染紧凑表(表头 + 最多 8 行,超出给「共 N 个角色」尾行),
673
+ // 失败数 >0 标红提醒。
674
+ function delegationCardOf(stats, open, onToggle) {
668
675
  if (!stats || stats.ok !== true || !Array.isArray(stats.roles)) return null;
669
- var head = h("div", { className: "pd-delegTitle" }, t("del.cardTitle"));
676
+ // project==='all' = 缺省项目零记录后回落的全项目聚合,标题尾加弱化角标
677
+ // 点明口径,免得误读成当前项目自己的数据(服务端见 stats-core.mjs 回落)。
678
+ var head = h("button", {
679
+ type: "button",
680
+ className: "pd-delegHead",
681
+ "aria-expanded": open ? "true" : "false",
682
+ title: t("del.toggleTip"),
683
+ onClick: function () { if (typeof onToggle === "function") onToggle(); },
684
+ }, h("span", { className: "pd-delegTitle" }, t("del.cardTitle"),
685
+ stats.project === "all" ? h("span", { className: "pd-delegScope" }, t("del.scopeAll")) : null),
686
+ h("span", { className: open ? "pd-delegChev pd-delegChevOpen" : "pd-delegChev", "aria-hidden": "true" }, "▸"));
670
687
  if (!stats.delegatedSessions) {
671
688
  return h("div", { className: "pd-delegCard" }, head,
672
- h("div", { className: "pd-delegEmpty" }, t("del.empty")));
689
+ open ? h("div", { className: "pd-delegEmpty" }, t("del.empty")) : null);
673
690
  }
674
691
  var cols = [
675
692
  ["pd-delegRole", t("del.colRole")],
@@ -694,7 +711,8 @@ window.__ModuleLoader__.load({
694
711
  ? h("div", { className: "pd-delegMore" }, t("del.totalRoles", { n: stats.roles.length }))
695
712
  : null;
696
713
  return h("div", { className: "pd-delegCard" }, head,
697
- h.apply(null, ["div", { className: "pd-delegTable" }, headerRow].concat(rows)), tail);
714
+ open ? h.apply(null, ["div", { className: "pd-delegTable" }, headerRow].concat(rows)) : null,
715
+ open ? tail : null);
698
716
  }
699
717
 
700
718
  var DEFAULT_ROLES = ["search_external", "design", "implement"];
@@ -4096,6 +4114,20 @@ window.__ModuleLoader__.load({
4096
4114
  }).catch(function () { /* 静默降级:整卡不渲染 */ });
4097
4115
  return function () { alive = false; };
4098
4116
  }, []);
4117
+ // 委派卡折叠态:默认展开,点标题行切换(delegationCardOf 的 onToggle)。
4118
+ // localStorage 记忆偏好(键 paoding-deleg-collapsed,"1" = 收起),GUI
4119
+ // 重载后保持;存储不可用(隐私模式等)时只丢记忆不丢功能——与 paoding-lang
4120
+ // 同款 try/catch 围栏。
4121
+ var _dc = useState(function () {
4122
+ try { return window.localStorage.getItem("paoding-deleg-collapsed") !== "1"; }
4123
+ catch (err) { return true; }
4124
+ }), delOpen = _dc[0], setDelOpen = _dc[1];
4125
+ var toggleDelOpen = useCallback(function () {
4126
+ setDelOpen(function (o) {
4127
+ try { window.localStorage.setItem("paoding-deleg-collapsed", o ? "1" : "0"); } catch (err) { /* 只丢记忆 */ }
4128
+ return !o;
4129
+ });
4130
+ }, []);
4099
4131
  // 语言切换 tick:页头文案(标题 / 副标题 / 菜单 / 语言项)走 t(),切换后
4100
4132
  // 需整体重渲染一次;ConfigPanel 内部另有 registerReload 整页重拉。
4101
4133
  var _lg = useState(0), langTick = _lg[0], setLangTick = _lg[1];
@@ -4374,8 +4406,9 @@ window.__ModuleLoader__.load({
4374
4406
  // 回写点);数据未就绪 / 老服务端无该字段时零占位。
4375
4407
  slimInfo ? h("p", { className: "pd-slimLine" }, slimLineText(slimInfo)) : null,
4376
4408
  // 委派效果卡:紧贴瘦身行之下(delegationCardOf 自判,统计没拉到 /
4377
- // 服务缺失时返回 null 整卡不渲染)。
4378
- delegationCardOf(delStats)),
4409
+ // 服务缺失时返回 null 整卡不渲染);标题行点按切换展开 / 收起,
4410
+ // 折叠态由 delOpen 持久记忆。
4411
+ delegationCardOf(delStats, delOpen, toggleDelOpen)),
4379
4412
  // 页头「更多」菜单:整份配置文件的导出 / 导入(低频操作收进来,页头
4380
4413
  // 不添第二排按钮)。定位锚点用相对定位的薄包裹层,下拉复用工作区
4381
4414
  // 「更多」的 omd-tabMenu / omd-tabMenuItem 既有样式;靠右对齐(inline
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paoding-config-ui",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "main": "index.mjs",