dsh-paoding 0.3.9 → 0.4.0
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 +6 -1
- package/README_en.md +6 -1
- package/bin/dsh-paoding.mjs +23 -0
- package/docs/architecture.md +4 -3
- package/docs/architecture_en.md +6 -4
- package/docs/configuration.md +85 -12
- package/docs/configuration_en.md +88 -13
- package/docs/installation.md +18 -6
- package/docs/installation_en.md +18 -6
- package/docs/orchestration.md +4 -0
- package/docs/orchestration_en.md +4 -0
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +107 -4
- package/plugins/paoding-config-ui/index.mjs +93 -28
- package/plugins/paoding-config-ui/lib/client.js +941 -237
- package/plugins/paoding-config-ui/package.json +1 -1
- package/presets/orchestrator/agent.cordis.yml +13 -0
- package/presets/orchestrator/delegation-labels.mjs +80 -0
- package/tools/lib/cli.mjs +91 -13
- package/tools/lib/compose.mjs +103 -16
- package/tools/lib/doctor.mjs +245 -0
- package/tools/lib/glob.mjs +76 -0
- package/tools/lib/health.mjs +298 -0
- package/tools/lib/preset-system.mjs +29 -11
- package/tools/lib/state.mjs +24 -6
package/docs/installation_en.md
CHANGED
|
@@ -19,13 +19,13 @@ Check the following before installing:
|
|
|
19
19
|
|---|---|---|
|
|
20
20
|
| `$DSH_HOME` | `~/.dsh` | The DSH home directory (the `DSH_HOME` environment variable is honored, defaulting to `$HOME/.dsh`). The home-layer `cordis.patch.yml`, `node_modules`, `profiles/<name>/cordis.patch.yml`, and plugin profile directories live under it |
|
|
21
21
|
| preset roster root | `$DSH_HOME/.agent-presets/` | Under DSH ≤ 0.1.6, `dsh-agent-presets` scans this directory for locally authored presets; under DSH ≥ 0.1.7 the host no longer scans it and the directory is just where the preset files live (see §2 "Version support and the dual preset landing") |
|
|
22
|
-
| Install target of this preset | `$DSH_HOME/.agent-presets/orchestrator` | The generated artifact: `agent.cordis.yml` (rewritten role config), `preset.yml`, `restrict.mjs`, and the `.generator-version` version-and-track marker — a static directory; per-workspace presets land in sibling `orchestrator-<slug>` directories |
|
|
22
|
+
| Install target of this preset | `$DSH_HOME/.agent-presets/orchestrator` | The generated artifact: `agent.cordis.yml` (rewritten role config), `preset.yml`, `restrict.mjs`, `delegation-labels.mjs`, and the `.generator-version` version-and-track marker — a static directory; per-workspace presets land in sibling `orchestrator-<slug>` directories |
|
|
23
23
|
| Declaration managed block (DSH ≥ 0.1.7 only) | a managed block inside `$DSH_HOME/cordis.patch.yml` | Opens with `# --- dsh-paoding presets (auto-generated; do not edit) ---` and closes with the matching end marker; the `- insert:` rows inside carry `@deepseek-ai/dsh-agent-preset` declarations, maintained by the installer — see §2 |
|
|
24
24
|
| Config file | `$DSH_HOME/dsh-paoding.config.yml` | Role/tool assignments written by the Paoding Config panel's Save & Apply (`0o600`, hand-editable); first-install automation reads and writes it too |
|
|
25
25
|
|
|
26
26
|
- **Environment variables**: `DSH_HOME` overrides the DSH home directory (default `~/.dsh`); `DSH_PAODING_CONFIG` overrides the config-file path (honored by the panel; the in-repo fallback CLI uses `--config <file>` instead).
|
|
27
27
|
|
|
28
|
-
Inside the repository, `presets/orchestrator/` is the static preset source (the pipeline reads and rewrites `agent.cordis.yml`; `preset.yml` and `
|
|
28
|
+
Inside the repository, `presets/orchestrator/` is the static preset source (the pipeline reads and rewrites `agent.cordis.yml`; `preset.yml`, `restrict.mjs` and `delegation-labels.mjs` are copied as-is — the one exception: when `main_agent_display_name` (or a workspace-derived display name) is configured, the `name:` line of `preset.yml` is rewritten), `tools/` is the generator itself (shared by the panel and the fallback CLI), and `plugins/paoding-config-ui/` is the Paoding Config panel plugin. For plain usage you never need to touch any of these — see the next section.
|
|
29
29
|
|
|
30
30
|
## 2. Install: the single official channel
|
|
31
31
|
|
|
@@ -56,8 +56,8 @@ The plugin supports two host generations: **DSH 0.1.5 / 0.1.6 / 0.1.7-rc.1 / 0.1
|
|
|
56
56
|
|
|
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
|
-
- **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`) 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`
|
|
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.
|
|
61
61
|
|
|
62
62
|
The installer decides which track to take in this order:
|
|
63
63
|
|
|
@@ -135,7 +135,7 @@ dsh plugin --profile web add link:"$PWD"
|
|
|
135
135
|
|
|
136
136
|
The package sits directly on the repository directory; changes go live via HMR or a restart, with no reinstalls. This form cannot be upgraded in place (see §3); to switch back to the registry version, run `dsh plugin --profile web add dsh-paoding@latest`.
|
|
137
137
|
|
|
138
|
-
An **in-repo fallback CLI** also exists (not advertised; for developers who cloned the repo): `node tools/install.mjs --auto` (apply the config if present, else write the base template), `--dry-run` (print the detection report
|
|
138
|
+
An **in-repo fallback CLI** also exists (not advertised; for developers who cloned the repo): `node tools/install.mjs --auto` (apply the config if present, else write the base template), `--dry-run` (print the detection report, the would-be allow lists and the config health check without writing anything), `--config <file>` (select a config file), `--suggest` (on a fresh install without a config file, seed smart defaults that assign host tools; the default base template leaves host tools out), `--profile <name>` (which profile's patch layer to scan, default `web`), `doctor` (read-only install health check — see the start of §8). Without `--auto` (and without the read-only `--dry-run` / `doctor`) it refuses to run — interactive install has been removed; visual configuration belongs to the Web panel that ships with the plugin channel.
|
|
139
139
|
|
|
140
140
|
## 5. Host patch detection
|
|
141
141
|
|
|
@@ -155,7 +155,7 @@ All layers are **included** (detection unions the enabled entries across layers)
|
|
|
155
155
|
|
|
156
156
|
- **Enabled test**: a patch entry counts as enabled unless disabled (`enabled !== false` and no `disabled`); plugins additionally check `config.enabled`.
|
|
157
157
|
- **MCP server recognition**: the entry's package-name segment **exactly equals** `dsh-mcp-client` (scoped/sub-path forms compare the last `/` segment; a name like `not-dsh-mcp-client-foo` does not match), or the config carries a `serverName` with transport `stdio` / `streamable-http` / `sse` (or has a `command`).
|
|
158
|
-
- **Exact tool-name resolution** (
|
|
158
|
+
- **Exact tool-name resolution** (detection and artifacts reconcile exact names; config entries may use `*`/`?` wildcards, expanded to concrete names at generation time — see the configuration guide): the detection order is that **every `stdio` / `streamable-http` server gets a live JSON-RPC handshake first** (stdio spawns the process and sends `initialize` + `tools/list`; streamable-http POSTs; **15-second timeout**), and the returned tools become `mcp__<server>__<tool>` names; the static known table (`tavily` → 5 tools `tavily_search` / `tavily_crawl` / `tavily_extract` / `tavily_map` / `tavily_research`, `codegraph` → `codegraph_explore`) is only the degraded fallback when a **known** server's handshake fails — tools are kept per the table, the report flags a static table, and no tools are lost; only an unknown server that fails the handshake is skipped entirely.
|
|
159
159
|
- **Local tool plugins**: matched against a whitelist of patch entry names, registering their tools, e.g. `magic-memory` → `memory_search` (new plugins extend the whitelist — `KNOWN_HOST_PLUGINS` in `tools/lib/host.mjs`).
|
|
160
160
|
- **Skipped, not fatal**: "servers whose handshake fails, times out, or uses a transport without live discovery (`sse`) are skipped" applies to **unknown servers** only — such a server is skipped entirely, the role simply lacks those tools, and generation raises no error (the report flags `handshake failed` with the reason; URLs and keys are masked); known servers fall back to the static table on a failed handshake and keep their tools, so they never land here.
|
|
161
161
|
|
|
@@ -218,6 +218,18 @@ Uninstalling touches nothing else on the host (MCP, plugins, sessions), and stay
|
|
|
218
218
|
|
|
219
219
|
## 8. Troubleshooting
|
|
220
220
|
|
|
221
|
+
**Run the doctor first**
|
|
222
|
+
|
|
223
|
+
Most "won't install / won't show up / behaves oddly" reports can be pinned down by the read-only health check:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npx dsh-paoding@latest doctor # or from a checkout: node tools/install.mjs doctor
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The doctor writes nothing. It walks through each check and marks it `✔ ok / ⚠ warning / ✘ error / – skipped`: is the config file parseable, does the preset track match the host version, are there orphan or corrupted managed declaration rows, are there `orchestrator-*` preset directories no longer referenced by the config, does the installed preset drift from what the current config would regenerate, and does the `.generator-version` marker line up. A summary closes the report; any ✘ makes the exit code 1, while ⚠ and – are safe to live with (skips are normal on a fresh machine). `--config` / `--profile` mean the same as during installation.
|
|
230
|
+
|
|
231
|
+
The panel runs the same checks on every save: each Preview / Save & Apply response carries a `warnings` field (configured-but-unmounted tools, zero-tool roles, role models missing from the model directory, presets behind the config, and so on) — follow the per-item hints instead of waiting for a runtime error.
|
|
232
|
+
|
|
221
233
|
**pnpm missing**
|
|
222
234
|
|
|
223
235
|
`dsh plugin add` fails with a pnpm error: `dsh plugin` relies on pnpm to install packages. Install it (`npm i -g pnpm` or `corepack enable`) and retry.
|
package/docs/orchestration.md
CHANGED
|
@@ -26,6 +26,10 @@ dsh-paoding 把 DSH 从「单体 agent 全量加载工具」改造成「主 agen
|
|
|
26
26
|
|
|
27
27
|
每请求工具开销(估算,随注册面浮动):主 agent 全量 ~16.2k tokens → 白名单后 ~5.5k;`search_external` ~3.0k / `design` ~3.3k / `implement` ~3.8k,仅在真正委派时发生(数值来源:`agent.cordis.yml` 头部注释)。
|
|
28
28
|
|
|
29
|
+
### 子代理标签带角色名
|
|
30
|
+
|
|
31
|
+
委派时主 agent 写下的任务短语(委派工具的 `description` 参数)会随子会话持久化,成为三处展示面的标签:侧栏任务管理页、会话头的子代理目录树、后台任务列表。给角色配了显示名(`roles.<toolName>.name`,见[配置](configuration.md) 2.4)后,安装器在预设里带上 `delegation-labels` 插件行:该角色派出的每个子会话,标签统一为 `角色名 · 原任务短语`(如「民工码农 · 修复标签显示」)——多角色并行时扫一眼就知道谁在干活,不用点进会话核对。改写在工具执行的瀑布上原地完成,主 agent 与子 agent 都无感知;只对配置后新发起的委派生效,已有子会话不追溯。
|
|
32
|
+
|
|
29
33
|
### 主 agent persona(编排人设)原文摘录
|
|
30
34
|
|
|
31
35
|
来自 `presets/orchestrator/agent.cordis.yml` 的 `- id: persona` 行(YAML `prefix: |-` 块标量,内容行缩进 6 空格;v0.3.0 起 persona 配置键为 `prefix`,`text` 是生成器兼容的旧名)。编排与委派的总指引(persona 原文为英文,照录如下):
|
package/docs/orchestration_en.md
CHANGED
|
@@ -26,6 +26,10 @@ The main agent's model-facing surface is narrowed by `restrict.mjs` on the `syst
|
|
|
26
26
|
|
|
27
27
|
Per-request tool tax (estimates; they float with the registered surface): full monolithic main agent ~16.2k tokens → ~5.5k after the allow list; `search_external` ~3.0k / `design` ~3.3k / `implement` ~3.8k, incurred only when actually delegated (figures from the header comment of `agent.cordis.yml`).
|
|
28
28
|
|
|
29
|
+
### Sub-agent labels carry the role name
|
|
30
|
+
|
|
31
|
+
The task phrase the main agent writes when delegating (the delegation tool's `description` argument) persists with the sub-session and becomes the label shown in three places: the sidebar task-management page, the sub-agent tree in the session header, and the background-job list. Once a role has a display name configured (`roles.<toolName>.name`, see [Configuration](configuration_en.md) 2.4), the installer ships a `delegation-labels` plugin row with the preset: every sub-session that role spawns gets a uniform label of `role name · original task phrase` (e.g.「民工码农 · 修复标签显示」) — with several roles in flight, one glance tells you who is doing what, no session-digging required. The rewrite happens in-place on the tools waterfall; neither the main agent nor the sub-agent needs to cooperate or even know. It applies only to delegations spawned after the configuration change — existing sub-sessions are not relabeled.
|
|
32
|
+
|
|
29
33
|
### Main-agent persona (excerpt, verbatim)
|
|
30
34
|
|
|
31
35
|
Taken from the `- id: persona` row of `presets/orchestrator/agent.cordis.yml` (a YAML `prefix: |-` block scalar whose content lines are indented 6 spaces; as of v0.3.0 the persona config key is `prefix`, with `text` kept as a legacy alias the generator still accepts). The overall orchestration-and-delegation guidance:
|
package/package.json
CHANGED
|
@@ -11,7 +11,10 @@ import path from 'node:path'
|
|
|
11
11
|
import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { readFile } from 'node:fs/promises'
|
|
13
13
|
import { assignSlugs, collectState, generateAndInstall, presetIdOf } from '../../tools/install.mjs'
|
|
14
|
-
import { autoAssignments } from '../../tools/lib/alloc.mjs'
|
|
14
|
+
import { autoAssignments, baseAssignments } from '../../tools/lib/alloc.mjs'
|
|
15
|
+
import { normalizeConfig, normalizeTargetFields, saveConfig, serializeConfig } from '../../tools/lib/config.mjs'
|
|
16
|
+
import { parseYamlSubset, preprocessPatchText } from '../../tools/lib/yaml.mjs'
|
|
17
|
+
import { captureWarnings, healthCheck } from '../../tools/lib/health.mjs'
|
|
15
18
|
import {
|
|
16
19
|
backfillHomePatchRows,
|
|
17
20
|
detectHostVersionTriple,
|
|
@@ -159,12 +162,112 @@ export function serializeState(s) {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
/** Compose + (optionally) install from a UI-supplied assignments object.
|
|
162
|
-
* workspacePath 非 null 时为该工作区生成独立 preset(见 generateAndInstall)。
|
|
163
|
-
|
|
165
|
+
* workspacePath 非 null 时为该工作区生成独立 preset(见 generateAndInstall)。
|
|
166
|
+
*
|
|
167
|
+
* 0.3.10 起响应在既有字段之外纯增量附带 `warnings`(tools/lib/health.mjs 的
|
|
168
|
+
* 结构化体检结果,见 healthCheck):生成成功时把本次生成的产物直接喂体检
|
|
169
|
+
* (零重跑),生成失败(如零工具角色 throw)时体检照跑、items 挂在
|
|
170
|
+
* error.warnings 上随原错误上抛——路由层 422 响应可将其一并透传给面板。
|
|
171
|
+
* models(可选):面板 models 路由的目录(groups 数组),体检的 unknown_model
|
|
172
|
+
* 项据此核对角色专用模型名;调用方拿不到目录时传 null(缺省),该项自动跳过。 */
|
|
173
|
+
export function installAssignments(state, assignments, { dryRun = false, saveConfigOnWrite = false, workspacePath = null, models = null } = {}) {
|
|
164
174
|
if (!assignments || typeof assignments !== 'object') {
|
|
165
175
|
throw new Error('missing assignments object')
|
|
166
176
|
}
|
|
167
|
-
|
|
177
|
+
let result = null
|
|
178
|
+
let generationWarnings = []
|
|
179
|
+
let generateError = null
|
|
180
|
+
try {
|
|
181
|
+
;[result, generationWarnings] = captureWarnings(() =>
|
|
182
|
+
generateAndInstall(state, assignments, { dryRun, saveConfigOnWrite, workspacePath }),
|
|
183
|
+
)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
generateError = err
|
|
186
|
+
}
|
|
187
|
+
const { items } = healthCheck({ state, assignments, models, dryRunResult: result, generationWarnings })
|
|
188
|
+
if (generateError !== null) {
|
|
189
|
+
generateError.warnings = items
|
|
190
|
+
throw generateError
|
|
191
|
+
}
|
|
192
|
+
return { ...result, warnings: items }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── 配置文件导出 / 导入(面板页头「更多」菜单的服务端核心)────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 基础模板配置文本:配置文件不存在时导出的回落内容。与 fresh 首装自愈
|
|
199
|
+
* (ensurePresetInstalled → generateAndInstall 的 saveConfig 分支)完全同源:
|
|
200
|
+
* baseAssignments + normalizeTargetFields + serializeConfig 单一序列化路径,
|
|
201
|
+
* 保证「导出的模板 → 再导入」往返出来的也是合法、可再解析的配置。
|
|
202
|
+
*/
|
|
203
|
+
function baseTemplateConfigText(state) {
|
|
204
|
+
return serializeConfig({
|
|
205
|
+
profile: state.effectiveProfile,
|
|
206
|
+
...normalizeTargetFields(baseAssignments(state.staticBase)),
|
|
207
|
+
workspaces: {},
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 导出配置:返回配置文件原文(字节不动,用户手编的注释 / 排版原样带走);
|
|
213
|
+
* 文件不存在(ENOENT)时回落基础模板文本——与首装自愈写的内容一致,导出
|
|
214
|
+
* 即是「当前生效配置」的忠实快照。其余读失败(权限 / EIO 等)原样上抛:
|
|
215
|
+
* 静默回落模板会把「读不了」伪装成「没有」,误导用户拿模板覆盖回去。
|
|
216
|
+
* 只读不写,不触碰 state 缓存。
|
|
217
|
+
*/
|
|
218
|
+
export async function exportConfigPayload() {
|
|
219
|
+
const file = resolveConfigFile()
|
|
220
|
+
let content = null
|
|
221
|
+
try {
|
|
222
|
+
content = await readFile(file, 'utf8')
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err.code !== 'ENOENT') throw err
|
|
225
|
+
content = baseTemplateConfigText(await getState())
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, filename: path.basename(file), content }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 导入配置:content(配置文件全文)过与 loadConfig 完全同口径的解析校验
|
|
232
|
+
*(preprocessPatchText → 宿主 yaml 模块优先、内置 parseYamlSubset 兜底 →
|
|
233
|
+
* 顶层映射检查 → normalizeConfig),解析失败 / 顶层非映射以 statusCode=422
|
|
234
|
+
* 上抛(路由层回 { error },不写盘);通过后经 saveConfig 单一写盘路径落盘
|
|
235
|
+
*(0o600 + 头注释 + 规范化,与 CLI / 面板任何一次保存同一口径)。写盘即改
|
|
236
|
+
* mtime,state 缓存靠既有对账自动失效,无需手动清理。
|
|
237
|
+
*
|
|
238
|
+
* 往返取舍(有意为之):load→normalize→serialize 会丢注释、未知键与排版,
|
|
239
|
+
* schema 字段(含 has_main_agent_extra 显式空表等「缺省 vs 显式」语义)全保留
|
|
240
|
+
* ——与面板 / CLI 每次保存的行为一致,换来落盘配置恒合法、可再解析。
|
|
241
|
+
*/
|
|
242
|
+
export async function importConfigContent(content) {
|
|
243
|
+
const processed = preprocessPatchText(String(content ?? ''))
|
|
244
|
+
// 宿主 yaml 模块从 state 顺路取(collectState 已探测好);state 通道异常时
|
|
245
|
+
// 退化为仅内置解析器校验——内置解析器覆盖不了的长文法此时可能误报 422,
|
|
246
|
+
// 但绝不放过坏配置写盘,宁严勿漏。
|
|
247
|
+
let yamlMod = null
|
|
248
|
+
try {
|
|
249
|
+
yamlMod = (await getState())?.yamlMod ?? null
|
|
250
|
+
} catch { /* 校验口径降级,见上 */ }
|
|
251
|
+
let data = null
|
|
252
|
+
if (yamlMod) {
|
|
253
|
+
try { data = yamlMod.parse(processed) } catch { data = null }
|
|
254
|
+
}
|
|
255
|
+
if (data === null) {
|
|
256
|
+
try {
|
|
257
|
+
data = parseYamlSubset(processed)
|
|
258
|
+
} catch (err) {
|
|
259
|
+
const error = new Error(`配置解析失败:${err.message}`)
|
|
260
|
+
error.statusCode = 422
|
|
261
|
+
throw error
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
265
|
+
const error = new Error('配置解析失败:顶层必须是键值映射(mapping),不能是空文件、列表或标量')
|
|
266
|
+
error.statusCode = 422
|
|
267
|
+
throw error
|
|
268
|
+
}
|
|
269
|
+
saveConfig(resolveConfigFile(), normalizeConfig(data))
|
|
270
|
+
return { ok: true }
|
|
168
271
|
}
|
|
169
272
|
|
|
170
273
|
// ── 首装 / 升级后 preset 自动重生成 ─────────────────────────────────────────
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 把 dsh-paoding 可视化配置器整合进 DSH Web GUI(左侧栏全页配置,无独立服务端):
|
|
5
5
|
* - Node 侧:注册 /api/paoding/* 前缀路由(state/models/workspaces/version/
|
|
6
|
-
* upgrade/preview/apply/rescan + client.css
|
|
6
|
+
* upgrade/preview/apply/rescan/config/export/config/import + client.css
|
|
7
|
+
* 静态样式),经
|
|
7
8
|
* ./api-core.mjs 复用 tools/install.mjs 的检测与生成管线;workspaces 直连
|
|
8
9
|
* 宿主 workspaceRegistry 服务(list / create,幂等);version
|
|
9
10
|
* 复用 tools/lib/version.mjs 的版本检测(npm registry 优先,npmmirror
|
|
@@ -36,7 +37,8 @@ import { readFile } from 'node:fs/promises'
|
|
|
36
37
|
import os from 'node:os'
|
|
37
38
|
import path from 'node:path'
|
|
38
39
|
import { fileURLToPath } from 'node:url'
|
|
39
|
-
import { getState, refreshState, isStateCacheStale, serializeState, installAssignments, resolveDshHome, discoverWorkspaceCandidates, ensurePresetInstalled, pluginVersion } from './api-core.mjs'
|
|
40
|
+
import { getState, refreshState, isStateCacheStale, serializeState, installAssignments, resolveDshHome, discoverWorkspaceCandidates, ensurePresetInstalled, pluginVersion, exportConfigPayload, importConfigContent } from './api-core.mjs'
|
|
41
|
+
import { collectModelRefs } from '../../tools/lib/health.mjs'
|
|
40
42
|
import { detectSkills } from '../../tools/lib/skills.mjs'
|
|
41
43
|
import { collectRuntimeFacts } from './runtime-inventory.mjs'
|
|
42
44
|
import { getVersionInfo } from '../../tools/lib/version.mjs'
|
|
@@ -255,6 +257,38 @@ async function stateWithRuntimeFacts(ctx, { force = false } = {}) {
|
|
|
255
257
|
// apply in-flight 闸(模块级):apply 落盘期间再来的 apply 直接 409,防并发写配置。
|
|
256
258
|
let applyInFlight = false
|
|
257
259
|
|
|
260
|
+
/**
|
|
261
|
+
* 模型目录(面板 models 路由与 preview/apply 的模型体检共用同一份映射,防两处
|
|
262
|
+
* 漂移):ctx.llm(DSH ≥ 0.1.6 的宿主模型运行时)listProviders() 给已注册
|
|
263
|
+
* 适配器的 provider 路由({ id, ... }),listModels(provider) 给该路由的目录
|
|
264
|
+
*({ provider, id, name, ... })。逐 provider 并发实调映射成客户端的 groups
|
|
265
|
+
* 形状(group.id = provider 路由 id);单路由失败隔离记入 failures 不抛错,
|
|
266
|
+
* 与旧 apiProxy 时代的容错语义一致。ctx.llm 缺失(老宿主 / 测试假 ctx)时
|
|
267
|
+
* listProviders 直接 TypeError,由调用方自捕降级。
|
|
268
|
+
*/
|
|
269
|
+
async function modelDirectoryOf(ctx) {
|
|
270
|
+
const providers = ctx.llm.listProviders()
|
|
271
|
+
const failures = []
|
|
272
|
+
const results = await Promise.all((Array.isArray(providers) ? providers : []).map(async (provider) => {
|
|
273
|
+
const id = provider && typeof provider.id === 'string' ? provider.id : null
|
|
274
|
+
if (!id) return null
|
|
275
|
+
try {
|
|
276
|
+
const models = await ctx.llm.listModels(id)
|
|
277
|
+
return {
|
|
278
|
+
id,
|
|
279
|
+
name: provider.name || provider.displayName || id,
|
|
280
|
+
models: (Array.isArray(models) ? models : [])
|
|
281
|
+
.filter((m) => m && typeof m.id === 'string' && m.id !== '')
|
|
282
|
+
.map((m) => ({ id: m.id, name: m.name || m.id })),
|
|
283
|
+
}
|
|
284
|
+
} catch (err) {
|
|
285
|
+
failures.push(`${id}: ${err.message}`)
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
}))
|
|
289
|
+
return { groups: results.filter(Boolean), failures }
|
|
290
|
+
}
|
|
291
|
+
|
|
258
292
|
// —— 宿主热切换竞态兜底 ————————————————————————————————————————————
|
|
259
293
|
// dsh 0.1.7-rc.2 / 0.2.0-rc.x 插件列表「禁用→启用」会重建条目纤维:新 apply()
|
|
260
294
|
// 先执行,旧 incarnation 的路由注销走异步结算,注册瞬间旧路由常仍在表里,
|
|
@@ -339,31 +373,8 @@ export async function apply(ctx) {
|
|
|
339
373
|
// 目录发现可能稍慢,属正常。失败组在 failures 里,不抛错;这里自捕
|
|
340
374
|
// 一层,保证错误也走 { ok: false, error } 形状(外层 catch 是 { error })。
|
|
341
375
|
try {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// 路由的目录({ provider, id, name, ... })。逐 provider 并发实调
|
|
345
|
-
// 映射成客户端的 groups 形状(group.id = provider 路由 id);单路由
|
|
346
|
-
// 失败隔离记入 failures 不抛错,与旧 apiProxy 时代的容错语义一致。
|
|
347
|
-
const providers = ctx.llm.listProviders()
|
|
348
|
-
const failures = []
|
|
349
|
-
const results = await Promise.all((Array.isArray(providers) ? providers : []).map(async (provider) => {
|
|
350
|
-
const id = provider && typeof provider.id === 'string' ? provider.id : null
|
|
351
|
-
if (!id) return null
|
|
352
|
-
try {
|
|
353
|
-
const models = await ctx.llm.listModels(id)
|
|
354
|
-
return {
|
|
355
|
-
id,
|
|
356
|
-
name: provider.name || provider.displayName || id,
|
|
357
|
-
models: (Array.isArray(models) ? models : [])
|
|
358
|
-
.filter((m) => m && typeof m.id === 'string' && m.id !== '')
|
|
359
|
-
.map((m) => ({ id: m.id, name: m.name || m.id })),
|
|
360
|
-
}
|
|
361
|
-
} catch (err) {
|
|
362
|
-
failures.push(`${id}: ${err.message}`)
|
|
363
|
-
return null
|
|
364
|
-
}
|
|
365
|
-
}))
|
|
366
|
-
json(res, 200, { ok: true, groups: results.filter(Boolean), failures })
|
|
376
|
+
const { groups, failures } = await modelDirectoryOf(ctx)
|
|
377
|
+
json(res, 200, { ok: true, groups, failures })
|
|
367
378
|
} catch (err) {
|
|
368
379
|
json(res, 500, { ok: false, error: err.message })
|
|
369
380
|
}
|
|
@@ -516,11 +527,23 @@ export async function apply(ctx) {
|
|
|
516
527
|
// 必须走原始 state 通道(Set/Map/dstDir/yamlMod 齐全)——序列化视图
|
|
517
528
|
// 喂生成管线会炸 TypeError(契约由 tests/api-apply.test.mjs 钉死)。
|
|
518
529
|
const s = await rawStateWithRuntimeFacts(ctx)
|
|
530
|
+
// 模型目录按需取(unknown_model 体检的比对名单):配置里没有任何
|
|
531
|
+
// 角色专用模型时不做目录发现(发现可能稍慢,别拖慢每次预览);
|
|
532
|
+
// 拿不到(老宿主无 ctx.llm / 全路由失败)传 null,体检自动跳过该项。
|
|
533
|
+
let models = null
|
|
534
|
+
if (collectModelRefs(body.assignments).length > 0) {
|
|
535
|
+
try {
|
|
536
|
+
models = (await modelDirectoryOf(ctx)).groups
|
|
537
|
+
} catch {
|
|
538
|
+
models = null
|
|
539
|
+
}
|
|
540
|
+
}
|
|
519
541
|
try {
|
|
520
542
|
const result = installAssignments(s, body.assignments, {
|
|
521
543
|
dryRun,
|
|
522
544
|
saveConfigOnWrite: !dryRun,
|
|
523
545
|
workspacePath,
|
|
546
|
+
models,
|
|
524
547
|
})
|
|
525
548
|
// apply 落盘后必须重刷状态缓存:stateCache 仍是应用前的快照(existing
|
|
526
549
|
// 还是旧 config 的 roles),浏览器刷新读到的就是它——角色专用模型 /
|
|
@@ -544,9 +567,17 @@ export async function apply(ctx) {
|
|
|
544
567
|
// 本次落点:全局 orchestrator 或工作区专属 orchestrator-<slug>。
|
|
545
568
|
presetId: result.presetId,
|
|
546
569
|
configFile: s.configFile,
|
|
570
|
+
// 结构化体检结果(tools/lib/health.mjs):未挂载工具 / 零工具角色 /
|
|
571
|
+
// 未知模型 / 预设漂移 / 生成期告警。纯增量字段,客户端批次负责渲染。
|
|
572
|
+
warnings: result.warnings,
|
|
547
573
|
})
|
|
548
574
|
} catch (err) {
|
|
549
|
-
|
|
575
|
+
// 422 保持既有 { error } 形状;体检结果(如零工具角色的可读说明)
|
|
576
|
+
// 挂在 error.warnings 上时一并透传(同样纯增量)。
|
|
577
|
+
json(res, 422, {
|
|
578
|
+
error: err.message,
|
|
579
|
+
...(Array.isArray(err?.warnings) ? { warnings: err.warnings } : {}),
|
|
580
|
+
})
|
|
550
581
|
}
|
|
551
582
|
} finally {
|
|
552
583
|
if (!dryRun) applyInFlight = false
|
|
@@ -554,6 +585,40 @@ export async function apply(ctx) {
|
|
|
554
585
|
return
|
|
555
586
|
}
|
|
556
587
|
|
|
588
|
+
if (route === 'config/export' && req.method === 'GET') {
|
|
589
|
+
// 导出配置:配置文件原文(不存在时回落基础模板文本,同首装自愈写入
|
|
590
|
+
// 的内容,见 exportConfigPayload)。只读不写、不碰 state 缓存;失败
|
|
591
|
+
// 走外层 catch(500 + 脱敏 message)。
|
|
592
|
+
json(res, 200, await exportConfigPayload())
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (route === 'config/import' && req.method === 'POST') {
|
|
597
|
+
// 导入配置 = 用提交文本整体覆盖配置文件(含 workspaces 段——工作区
|
|
598
|
+
// 条目随整份文件一起被替换,客户端确认文案里已明说)。与 apply 共用
|
|
599
|
+
// 同一把 in-flight 闸:apply 落盘期间导入 409,导入落盘期间 apply 409,
|
|
600
|
+
// 防两条写盘路径并发交错。空文本先拦(语义是「没选内容」,不该报成
|
|
601
|
+
// 「顶层不是映射」);解析不合法由 importConfigContent 抛 422(外层
|
|
602
|
+
// catch 按 statusCode 回 { error },不写盘)。
|
|
603
|
+
const body = await readBody(req)
|
|
604
|
+
const content = typeof body.content === 'string' ? body.content : ''
|
|
605
|
+
if (content.trim() === '') {
|
|
606
|
+
json(res, 422, { error: '缺少配置文本(body.content)' })
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
if (applyInFlight) {
|
|
610
|
+
json(res, 409, { error: '已有一次应用(apply)正在进行,请稍后重试' })
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
applyInFlight = true
|
|
614
|
+
try {
|
|
615
|
+
json(res, 200, await importConfigContent(content))
|
|
616
|
+
} finally {
|
|
617
|
+
applyInFlight = false
|
|
618
|
+
}
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
|
|
557
622
|
if (route === 'version' && req.method === 'GET') {
|
|
558
623
|
// 版本提示:npm registry 优先,npmmirror / GitHub 依次兜底(version.mjs,
|
|
559
624
|
// 带 1 小时内存缓存,命中不发外网请求)。绝不 5xx——检测失败也回
|