dsh-paoding 0.4.6 → 0.4.7
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 +1 -1
- package/docs/configuration.md +19 -0
- package/docs/configuration_en.md +19 -0
- package/docs/installation.md +7 -3
- package/docs/installation_en.md +6 -2
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +58 -6
- package/plugins/paoding-config-ui/index.mjs +8 -5
- package/plugins/paoding-config-ui/lib/base.css +9 -0
- package/plugins/paoding-config-ui/lib/client.js +6 -1
- package/plugins/paoding-config-ui/stats-core.mjs +94 -35
- package/tools/lib/config.mjs +23 -1
- package/tools/lib/doctor.mjs +79 -19
- package/tools/lib/lifecycle.mjs +105 -36
- package/tools/lib/preset-system.mjs +254 -38
- package/tools/lib/state.mjs +24 -9
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 天),数据读宿主会话存档本地聚合——哪个角色用得多、哪个常掉链子,一眼可见;缺省项目(宿主 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
|
|
package/docs/configuration.md
CHANGED
|
@@ -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 的覆盖语义
|
package/docs/configuration_en.md
CHANGED
|
@@ -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
|
package/docs/installation.md
CHANGED
|
@@ -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
|
|
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`
|
|
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 预过滤,窗口外的日志文件不再开读句柄。宿主缺会话服务时整卡不渲染。
|
|
207
211
|
- **「更多」菜单**:配置导出(下载 `dsh-paoding.config.yml` 原文)/ 导入(先验格式,整份覆盖含工作区段,需二次确认)/ 恢复备份(见上条)与面板语言切换(中英自动识别、手动选择优先并记住)。
|
|
208
212
|
|
|
209
213
|
## 7. 卸载
|
package/docs/installation_en.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
458
|
-
//
|
|
456
|
+
// stats-core.mjs 文件头——服务面、slug 口径、mtime 预过滤与失败判据
|
|
457
|
+
// 的实测证据都在那里)。服务经 ctx.reflect 软探测(绝不进 inject 数
|
|
458
|
+
// 组:硬注入会让缺服务的老宿主在挂载时就炸),拿不到回 { ok:false,
|
|
459
459
|
// reason:'persistence-unavailable' },客户端整卡不渲染。project
|
|
460
|
-
// 缺省 = 当前面板项目(cwd → projectKey
|
|
461
|
-
//
|
|
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,
|
|
@@ -1402,6 +1402,15 @@ html[data-dsh-paoding-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
|
|
|
1402
1402
|
color: var(--dsw-alias-label-primary);
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
|
+
/* 标题尾角标:缺省项目零记录回落全项目聚合(project='all')时点明口径——
|
|
1406
|
+
弱化字号与三级文字色,紧跟标题不抢视觉。 */
|
|
1407
|
+
.pd-delegScope {
|
|
1408
|
+
margin-left: 6px;
|
|
1409
|
+
font-size: 0.85em;
|
|
1410
|
+
font-weight: 400;
|
|
1411
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1405
1414
|
.pd-delegTable {
|
|
1406
1415
|
margin-top: 6px;
|
|
1407
1416
|
font-size: 11px;
|
|
@@ -475,6 +475,8 @@ 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" },
|
|
478
480
|
|
|
479
481
|
// ── 预览「本次变更」区块(preview.diff 结构化差分的渲染文案)───────────
|
|
480
482
|
// 服务端 diff 是纯结构零文案(tools/lib/config.mjs diffAssignments),
|
|
@@ -666,7 +668,10 @@ window.__ModuleLoader__.load({
|
|
|
666
668
|
// 最多 8 行,超出给「共 N 个角色」尾行),失败数 >0 标红提醒。
|
|
667
669
|
function delegationCardOf(stats) {
|
|
668
670
|
if (!stats || stats.ok !== true || !Array.isArray(stats.roles)) return null;
|
|
669
|
-
|
|
671
|
+
// project==='all' = 缺省项目零记录后回落的全项目聚合,标题尾加弱化角标
|
|
672
|
+
// 点明口径,免得误读成当前项目自己的数据(服务端见 stats-core.mjs 回落)。
|
|
673
|
+
var head = h("div", { className: "pd-delegTitle" }, t("del.cardTitle"),
|
|
674
|
+
stats.project === "all" ? h("span", { className: "pd-delegScope" }, t("del.scopeAll")) : null);
|
|
670
675
|
if (!stats.delegatedSessions) {
|
|
671
676
|
return h("div", { className: "pd-delegCard" }, head,
|
|
672
677
|
h("div", { className: "pd-delegEmpty" }, t("del.empty")));
|
|
@@ -29,6 +29,16 @@
|
|
|
29
29
|
* 志带上一轮化身的历史,那段 turn 天然平衡;继承切点打在轮次边界(实测 73 处
|
|
30
30
|
* mid 位 end-seed 前一事件以 turn/end 为主),owned 段计数不受污染。时长与
|
|
31
31
|
* 窗口过滤同样只用 owned 段,防跨化身膨胀。
|
|
32
|
+
*
|
|
33
|
+
* ── mtime 预过滤与缺省回落 ────────────────────────────────────────────────
|
|
34
|
+
* 开读句柄前先看日志文件 mtime:事件产生即落盘,文件末次写入(mtime)加松弛
|
|
35
|
+
* 量仍早于窗口起点 ⇒ 文件里不可能有窗口内事件,聚合窗口过滤本就会剔除,直接
|
|
36
|
+
* 跳过不开句柄——大库冷读从「全库逐个解压」降到「只读窗口内还活跃过的文件」。
|
|
37
|
+
* 预过滤条目不进摘要缓存(mtime 日后前移时本检查自然放行重开;缓存留 null 反
|
|
38
|
+
* 要额外对账)。project 缺省 = cwd 换算 slug,而 desktop 等宿主 profile 的
|
|
39
|
+
* 进程 cwd 停在 profile 目录、换算出的 slug 恒无会话,面板于是恒见「暂无委
|
|
40
|
+
* 派记录」——故缺省扫描零委派记录时自动回落一轮全项目聚合,响应 project 标
|
|
41
|
+
* 'all'(客户端标题加「全部项目」角标);显式传 project(含 'all')不回落。
|
|
32
42
|
*/
|
|
33
43
|
|
|
34
44
|
import { readdirSync, statSync } from 'node:fs'
|
|
@@ -41,6 +51,9 @@ import { ROLE_PRESET_ID_PREFIX } from '../../tools/lib/compose.mjs'
|
|
|
41
51
|
export const STATS_DEFAULT_DAYS = 30
|
|
42
52
|
export const STATS_MAX_DAYS = 365
|
|
43
53
|
|
|
54
|
+
/** mtime 预过滤松弛量(1h):挡时钟偏斜与落盘延迟——宁可多开几个句柄,不误跳。 */
|
|
55
|
+
const MTIME_SLACK_MS = 60 * 60 * 1000
|
|
56
|
+
|
|
44
57
|
// ── 参数清洗 ─────────────────────────────────────────────────────────────────
|
|
45
58
|
|
|
46
59
|
/** days 清洗:parseInt 夹取 [1, 365];NaN / 负数 / 0 回落缺省或夹到下限。 */
|
|
@@ -140,6 +153,15 @@ function listSessionIds(sessionsRoot, slug) {
|
|
|
140
153
|
}
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
/** 列会话根下全部项目目录名('all' 的枚举口径)。根缺失 / 不可读 → 空数组。 */
|
|
157
|
+
function allProjectSlugs(sessionsRoot) {
|
|
158
|
+
try {
|
|
159
|
+
return readdirSync(sessionsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
|
160
|
+
} catch {
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
// ── 冷读摘要 ─────────────────────────────────────────────────────────────────
|
|
144
166
|
|
|
145
167
|
/** 一个会话的抽摘要结果(JSON-safe 纯数据,可进缓存)。 */
|
|
@@ -279,35 +301,19 @@ async function mapLimited(items, limit, worker) {
|
|
|
279
301
|
|
|
280
302
|
const summaryCache = new Map()
|
|
281
303
|
|
|
304
|
+
// ── 扫描 + 聚合(一轮冷读,缺省 / 回落两路共用)─────────────────────────────
|
|
305
|
+
|
|
282
306
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
307
|
+
* 一轮「枚举 → 冷读 → 聚合」。slugs 是待扫的项目目录名列表(单 slug 或 'all'
|
|
308
|
+
* 枚举皆可);collectDelegationStats 的「缺省 slug 一轮」与「回落全项目一轮」
|
|
309
|
+
* 都走这里,两轮的 sessionsScanned / skipped 口径由此保持一致。worker 里:
|
|
310
|
+
* 先 mtime 预过滤(见文件头)——事件产生即落盘,文件末次写入时刻即 mtime,
|
|
311
|
+
* mtime+slack 仍早于窗口起点 ⇒ 文件内一切事件都在窗口外,聚合窗口过滤本就会
|
|
312
|
+
* 剔除,直接返回 null 摘要不开读句柄;预过滤条目不进缓存(mtime 日后前移时
|
|
313
|
+
* 本检查自然放行重开)。null 摘要不进聚合、不计 skipped(那是读错误的计数),
|
|
314
|
+
* 但照常计入 sessionsScanned(目录确实扫过)。
|
|
287
315
|
*/
|
|
288
|
-
|
|
289
|
-
const persistence = reflectService(ctx, 'sessionPersistence')
|
|
290
|
-
if (!persistence) return { ok: false, reason: 'persistence-unavailable' }
|
|
291
|
-
|
|
292
|
-
const windowDays = sanitizeStatsDays(days)
|
|
293
|
-
const sessionsRoot = path.join(resolveDshHome(), 'sessions')
|
|
294
|
-
const slug = sanitizeProjectSlug(project !== undefined && project !== null && project !== '' ? project : currentProjectSlug(), sessionsRoot)
|
|
295
|
-
if (slug === null) {
|
|
296
|
-
const err = new Error(`project 参数不合法(仅允许 [A-Za-z0-9._-],或 all): ${String(project).slice(0, 64)}`)
|
|
297
|
-
err.statusCode = 422
|
|
298
|
-
throw err
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 枚举:all = 列会话根下全部项目目录;单 slug 只列该目录。
|
|
302
|
-
const slugs = slug === 'all'
|
|
303
|
-
? (() => {
|
|
304
|
-
try {
|
|
305
|
-
return readdirSync(sessionsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
|
|
306
|
-
} catch {
|
|
307
|
-
return []
|
|
308
|
-
}
|
|
309
|
-
})()
|
|
310
|
-
: [slug]
|
|
316
|
+
async function scanAndAggregate(sessionsRoot, slugs, windowDays, nowMs, persistence) {
|
|
311
317
|
const targets = []
|
|
312
318
|
for (const dir of slugs) {
|
|
313
319
|
for (const id of listSessionIds(sessionsRoot, dir)) {
|
|
@@ -315,12 +321,14 @@ export async function collectDelegationStats(ctx, { project, days } = {}) {
|
|
|
315
321
|
}
|
|
316
322
|
}
|
|
317
323
|
|
|
318
|
-
const nowMs = Date.now()
|
|
319
324
|
const reads = await mapLimited(targets, 4, async ({ slug: dir, id }) => {
|
|
320
325
|
const sessionDir = path.join(sessionsRoot, dir, id)
|
|
321
326
|
const file = currentSessionFile(sessionDir)
|
|
322
327
|
if (file === null) throw new Error('no session log file')
|
|
323
328
|
const st = statSync(path.join(sessionDir, file)) // stat 失败 → 跳过计数
|
|
329
|
+
if (st.mtimeMs + MTIME_SLACK_MS < nowMs - windowDays * 86400000) {
|
|
330
|
+
return { id, summary: null }
|
|
331
|
+
}
|
|
324
332
|
const cacheKey = dir + '\u0000' + id
|
|
325
333
|
const hit = summaryCache.get(cacheKey)
|
|
326
334
|
if (hit && hit.file === file && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
|
|
@@ -334,20 +342,71 @@ export async function collectDelegationStats(ctx, { project, days } = {}) {
|
|
|
334
342
|
let skipped = 0
|
|
335
343
|
const summaries = []
|
|
336
344
|
for (const r of reads) {
|
|
337
|
-
if (r.ok)
|
|
338
|
-
|
|
345
|
+
if (!r.ok) {
|
|
346
|
+
skipped++ // 只有 open/read 真失败才计;预过滤出局不是错
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (r.value.summary !== null) summaries.push(r.value.summary)
|
|
339
350
|
}
|
|
340
351
|
const { roles, delegatedSessions } = aggregateRoles(summaries, windowDays, nowMs)
|
|
352
|
+
return { roles, delegatedSessions, sessionsScanned: targets.length, skipped }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 单一响应形状(ok:true 信封):两路扫描的结果都从这里出,字段恒齐。 */
|
|
356
|
+
function statsEnvelope(project, result, windowDays, nowMs) {
|
|
341
357
|
return {
|
|
342
358
|
ok: true,
|
|
343
|
-
project
|
|
359
|
+
project,
|
|
344
360
|
windowDays,
|
|
345
361
|
generatedAt: new Date(nowMs).toISOString(),
|
|
346
|
-
roles,
|
|
347
|
-
sessionsScanned:
|
|
348
|
-
delegatedSessions,
|
|
349
|
-
skipped,
|
|
362
|
+
roles: result.roles,
|
|
363
|
+
sessionsScanned: result.sessionsScanned,
|
|
364
|
+
delegatedSessions: result.delegatedSessions,
|
|
365
|
+
skipped: result.skipped,
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 委派效果统计主入口。ctx 缺 sessionPersistence(reflect 软探测,绝不进
|
|
371
|
+
* inject——硬注入会让缺服务的老宿主挂载即炸)→ { ok:false,
|
|
372
|
+
* reason:'persistence-unavailable' },面板据此整卡不渲染。project 非法抛
|
|
373
|
+
* 422(外层 catch 统一回 { error } 信封)。
|
|
374
|
+
*
|
|
375
|
+
* project 口径:显式传参(非空字符串,含 'all')按传参扫,绝不回落;缺省 =
|
|
376
|
+
* cwd 换算的当前面板项目,该 slug 一轮扫完 delegatedSessions=0 时自动回落
|
|
377
|
+
* 一轮全项目聚合(响应 project 标 'all';会话根缺失时枚举为空 → 空 roles
|
|
378
|
+
* 仍 ok:true,不 422 不 5xx)。
|
|
379
|
+
*/
|
|
380
|
+
export async function collectDelegationStats(ctx, { project, days } = {}) {
|
|
381
|
+
const persistence = reflectService(ctx, 'sessionPersistence')
|
|
382
|
+
if (!persistence) return { ok: false, reason: 'persistence-unavailable' }
|
|
383
|
+
|
|
384
|
+
const windowDays = sanitizeStatsDays(days)
|
|
385
|
+
const sessionsRoot = path.join(resolveDshHome(), 'sessions')
|
|
386
|
+
const explicit = project !== undefined && project !== null && project !== ''
|
|
387
|
+
const slug = sanitizeProjectSlug(explicit ? project : currentProjectSlug(), sessionsRoot)
|
|
388
|
+
if (slug === null) {
|
|
389
|
+
const err = new Error(`project 参数不合法(仅允许 [A-Za-z0-9._-],或 all): ${String(project).slice(0, 64)}`)
|
|
390
|
+
err.statusCode = 422
|
|
391
|
+
throw err
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const nowMs = Date.now()
|
|
395
|
+
const result = await scanAndAggregate(
|
|
396
|
+
sessionsRoot,
|
|
397
|
+
slug === 'all' ? allProjectSlugs(sessionsRoot) : [slug],
|
|
398
|
+
windowDays,
|
|
399
|
+
nowMs,
|
|
400
|
+
persistence,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
// 缺省回落:cwd slug 恒空的宿主 profile(desktop 进程 cwd 停在 profile 目录)
|
|
404
|
+
// 靠这一步救回面板——零委派记录才回落,显式 project 永不回落。
|
|
405
|
+
if (!explicit && slug !== 'all' && result.delegatedSessions === 0) {
|
|
406
|
+
const all = await scanAndAggregate(sessionsRoot, allProjectSlugs(sessionsRoot), windowDays, nowMs, persistence)
|
|
407
|
+
return statsEnvelope('all', all, windowDays, nowMs)
|
|
350
408
|
}
|
|
409
|
+
return statsEnvelope(slug, result, windowDays, nowMs)
|
|
351
410
|
}
|
|
352
411
|
|
|
353
412
|
/** reflect 软探测(与 persistenceAvailableOf 同款口径,任何异常按缺失处理)。 */
|
package/tools/lib/config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-paoding.config.yml 键规范化与读写:main_agent_display_name /
|
|
3
|
-
*
|
|
3
|
+
* patch_layer(声明行落盘层 'home' | 'profile',缺省自动判定,非法值 warn 回
|
|
4
|
+
* 未设置)/ roles.<toolName>.name /
|
|
4
5
|
* roles.<toolName>.model / roles.<toolName>.provider(角色专用模型,生成层映射到
|
|
5
6
|
* dsh-tool-subagent 的 agentOptions)/ roles.<toolName>.start_mode(角色启动方式,
|
|
6
7
|
* 生成层把内置委派块的块级 provider 行改写为 fork,'fresh' | 'fork')/
|
|
@@ -212,11 +213,26 @@ export function normalizeConfig(data) {
|
|
|
212
213
|
}
|
|
213
214
|
return {
|
|
214
215
|
profile: typeof data.profile === 'string' && data.profile !== '' ? data.profile : null,
|
|
216
|
+
patch_layer: normalizePatchLayer(data.patch_layer),
|
|
215
217
|
...normalizeTargetFields(data),
|
|
216
218
|
workspaces: normalizeWorkspaces(data.workspaces),
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 声明行落盘层(顶层键 patch_layer,仅 'home' | 'profile' 两个合法值):缺省 /
|
|
224
|
+
* null = 按环境自动判定(Android compat 壳 / 容器路径走 profile 层,其余 home,
|
|
225
|
+
* 判定在 preset-system.resolvePatchTargetLayer);非法值(类型或取值不对)warn
|
|
226
|
+
* 后当未设置——配置错了降级提醒而非中断加载,与 background_mode / start_mode
|
|
227
|
+
* 的非法值口径一致。normalize 后恒为 'home' | 'profile' | null。
|
|
228
|
+
*/
|
|
229
|
+
function normalizePatchLayer(value) {
|
|
230
|
+
if (value === null || value === undefined || value === '') return null
|
|
231
|
+
if (value === 'home' || value === 'profile') return value
|
|
232
|
+
warn(`patch_layer: 非法值已忽略(${JSON.stringify(value)}),按自动判定(可选: 'home' / 'profile')`)
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
220
236
|
/**
|
|
221
237
|
* 工作区条目段(顶层键 workspaces)规范化:只收 trim 后非空的字符串键(键 =
|
|
222
238
|
* 工作区目录绝对路径),值过 normalizeTargetFields;值不是映射、键为空串则
|
|
@@ -606,6 +622,12 @@ function writeWorkspaces(lines, workspaces) {
|
|
|
606
622
|
export function serializeConfig(config) {
|
|
607
623
|
const lines = ['# dsh-paoding(庖丁)安装配置 —— 可手编;改后在侧栏底部「庖丁配置」入口应用,或重跑 node tools/install.mjs --auto']
|
|
608
624
|
lines.push(`profile: ${config.profile ?? 'web'}`)
|
|
625
|
+
// patch_layer 显式钉层才落盘(null/缺省 = 自动判定,输出与旧版逐字节一致);
|
|
626
|
+
// 落盘它才能在 loadConfig → saveConfig 往返中保住用户手编的层钉(否则面板
|
|
627
|
+
// 一保存该键就被抹掉,层判定退回自动)。
|
|
628
|
+
if (config.patch_layer === 'home' || config.patch_layer === 'profile') {
|
|
629
|
+
lines.push(`patch_layer: ${config.patch_layer}`)
|
|
630
|
+
}
|
|
609
631
|
writeTargetFields(lines, '', config)
|
|
610
632
|
// workspaces 段可选:键缺失 / 空对象整段省略(不含 workspaces 的配置输出与
|
|
611
633
|
// 旧版逐字节一致)。
|