dsh-remote-agent 0.6.0 → 0.7.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/CHANGELOG.md +30 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/handoff-2026-10-17.md +78 -0
- package/lib/hub/node-registry.js +4 -0
- package/lib/hub/registry-ingest.js +13 -0
- package/lib/hub/tools.js +50 -0
- package/lib/node/index.js +37 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,36 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## [0.7.0] - 2026-10-17
|
|
13
|
+
|
|
14
|
+
### 功能:模型自述 + headless 指定模型派发前拦截(提案 #27)
|
|
15
|
+
|
|
16
|
+
副机此前"只有它自己知道有哪些模型"—— 面板让用户自由填 provider/model,
|
|
17
|
+
填错要等任务跑到副机才失败。本版先解决可靠可达的两半:
|
|
18
|
+
|
|
19
|
+
- **副机自述**:`RESOURCE` 帧随车上报 `runnerKind`(in-process/headless)
|
|
20
|
+
与 `defaultModel`(来自 DSH `agentDefaultModel.currentSelection()`,
|
|
21
|
+
**只报 provider 键名 + model id,不碰任何凭证**;取不到一律 null,
|
|
22
|
+
尽力而为不影响遥测帧)。hub 摄入清洗后 `remote_nodes` 透出并在节点行
|
|
23
|
+
渲染("后端 in-process · 默认模型 official/deepseek-chat")。
|
|
24
|
+
- **派发前拦截**:`remote_task` 在 `mode=agent` + 显式 `model` + 目标节点
|
|
25
|
+
为 headless 后端时**立即报错**(旧链路要等任务跑到副机才失败,浪费
|
|
26
|
+
一轮往返 + 烧一个 taskId)。dryRun 与真实派发同一条校验路径。
|
|
27
|
+
- **dryRun 回显模型**:预览里给解析后的 `{provider, model}` 与
|
|
28
|
+
`modelSource`(用户指定 / 副机默认 / 未指定)—— 派之前就知道用
|
|
29
|
+
什么模型。
|
|
30
|
+
|
|
31
|
+
**范围收敛(如实说明)**:完整的"可用模型清单 + 面板下拉"本版不做 ——
|
|
32
|
+
in-process 后端没有稳定的清单 API(DSH 内部的 `model/list` 走
|
|
33
|
+
app-server 协议面,fiber 服务摸不到;探测 llm-pi-ai 内部结构太脆弱)。
|
|
34
|
+
等 DSH 把清单暴露成稳定服务 API 后补(面板届时渲染下拉,第一项
|
|
35
|
+
"跟随副机默认",数据链路本版已铺好)。
|
|
36
|
+
|
|
37
|
+
测试:`test/model-selfreport.test.js` 3 条 + e2e 2 条(自述贯通 /
|
|
38
|
+
headless 拦截 / dryRun 回显)+ 修一处脆弱断言(applySync 窗口锚定义处)。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
12
42
|
## [0.6.0] - 2026-10-17
|
|
13
43
|
|
|
14
44
|
### 修复:单条慢命令锁死整台副机(提案 #26,实测事故防线)
|
package/README.en.md
CHANGED
|
@@ -268,7 +268,7 @@ changes, change diffs, final artifacts, log tail.
|
|
|
268
268
|
| `dryRun` | Preview only: returns which node/workspace/mode would be used, computed by the exact same routing + validation as a real dispatch |
|
|
269
269
|
| `workspace` | Absolute path on the worker, must be inside the node's `roots`. Defaults when omitted |
|
|
270
270
|
| `mode` | `agent` (worker starts an independent agent session, default) / `exec` (run a command directly) |
|
|
271
|
-
| `model` | Optional. Model the worker should use (`mode=agent` only): `{model:"deepseek-chat"}` or `{provider:"openai",model:"gpt-4o"}`. Selection only, no credentials
|
|
271
|
+
| `model` | Optional. Model the worker should use (`mode=agent` only): `{model:"deepseek-chat"}` or `{provider:"openai",model:"gpt-4o"}`. Selection only, no credentials. **Not supported on headless backends** (rejected before dispatch; the worker then uses its default model). `dryRun` echoes the resolved model and its source; the worker's default model is shown by `remote_nodes` |
|
|
272
272
|
| `outputs` | Glob of expected outputs (relative to the workspace), used to filter the artifact manifest |
|
|
273
273
|
| `timeoutMs` | Worker-side timeout. Defaults by mode: **exec 10 minutes, agent 1 hour** (one slow exec should not pin the worker for an hour) |
|
|
274
274
|
| `denyPaths` | Extra absolute paths to deny |
|
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ ack 自动按明文兼容旧中继继续),安全边界与已知局限见
|
|
|
238
238
|
| `dryRun` | 只出预览不派发:返回会派到哪个节点/工作区/模式,路由与校验和正式派发同一条路径 |
|
|
239
239
|
| `workspace` | 副机上的绝对路径,必须在节点 `roots` 内。省略用默认 |
|
|
240
240
|
| `mode` | `agent`(副机起独立 agent 会话,默认)/ `exec`(直接跑命令)|
|
|
241
|
-
| `model` | 可选。副机用的模型(仅 `mode=agent` 生效):`{model:"deepseek-chat"}` 或 `{provider:"openai",model:"gpt-4o"}`。只传选择不传凭证 —— 副机用自己的 provider
|
|
241
|
+
| `model` | 可选。副机用的模型(仅 `mode=agent` 生效):`{model:"deepseek-chat"}` 或 `{provider:"openai",model:"gpt-4o"}`。只传选择不传凭证 —— 副机用自己的 provider 配置跑。**headless 后端不支持指定模型**(派发前就会被拦,副机固定用默认模型);`dryRun` 会回显解析后的模型与来源,副机的默认模型见 `remote_nodes` |
|
|
242
242
|
| `outputs` | 期望产出物的 glob(相对工作区),用于产物清单筛选 |
|
|
243
243
|
| `timeoutMs` | 副机侧超时。缺省按模式:**exec 10 分钟、agent 1 小时**(一条慢 exec 不该锁死副机一小时) |
|
|
244
244
|
| `denyPaths` | 额外禁止访问的绝对路径 |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 交接:dsh-remote-agent 移交 DSH 接手(2026-10-17)
|
|
2
|
+
|
|
3
|
+
**移交方**:WorkBuddy 侧会话(用户积分耗尽,项目全权交 DSH)
|
|
4
|
+
**移交时状态**:`main` = `ae2ffe5`(即本文档提交自身;v0.7.0 定版,829 tests + lint 全绿,CI 八 job 全绿,未发布。local == origin == `ae2ffe5`)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. DSH 接手第一件事:发 v0.7.0
|
|
9
|
+
|
|
10
|
+
代码与文档已在 main,**发布三通道还没做**。按顺序:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd "D:\Deepseek Harness file\dsh-remote-agent"
|
|
14
|
+
# 1) CI 先绿(八 job:lint / packaging / check×4 / windows+macos 抖动 rerun)
|
|
15
|
+
gh run list --limit 1
|
|
16
|
+
# 2) annotated tag(gh release create 直接 --target 会 422,必须先建 tag)
|
|
17
|
+
TAGSHA=$(gh api repos/zbzbhs/dsh-remote-agent/git/tags -f tag=v0.7.0 \
|
|
18
|
+
-f message="v0.7.0 模型自述 + headless 派发前拦截(提案 #27)" \
|
|
19
|
+
-f object=$(git rev-parse HEAD) -f type=commit --jq '.sha')
|
|
20
|
+
gh api repos/zbzbhs/dsh-remote-agent/git/refs -f ref=refs/tags/v0.7.0 -f sha=$TAGSHA
|
|
21
|
+
gh release create v0.7.0 --title "v0.7.0 — ..." --notes-file <notes>
|
|
22
|
+
# 3) npm(granular bypass-2FA token 在 DSH 运行机的 ~/.npmrc;必须显式官方源,本机默认 registry 是 npmmirror)
|
|
23
|
+
npm publish --registry=https://registry.npmjs.org --access public
|
|
24
|
+
sleep 60 && npm view dsh-remote-agent version --registry=https://registry.npmjs.org
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Release notes 素材直接用 CHANGELOG 的 [0.7.0] 段。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. 提案池与批次状态
|
|
32
|
+
|
|
33
|
+
| # | 内容 | 状态 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| #25 | 远程文件工具集(remote_ls/read/grep/tree) | ✅ 已发布 v0.5.0 |
|
|
36
|
+
| #26 | 队列堵死防线(超时分模式/并发4/饥饿告警) | ✅ 主体已发布 v0.6.0;**剩余 #26-4 面板"运行中"分组、#26-5 agent 取消真打断+快照中断** |
|
|
37
|
+
| #27 | 模型下拉 | ✅ 代码+文档完成(v0.7.0 待发);**剩余面板下拉**(等 DSH 把 `model/list` 暴露成稳定的 fiber 服务 API;hub 侧数据链路已铺好:RESOURCE 帧 `defaultModel` → `remote_nodes` → 面板) |
|
|
38
|
+
|
|
39
|
+
提案原文与评估结论:工作区 `dsh-feature-proposals-2026-10-17-code-sauce.md`、
|
|
40
|
+
`dsh-feature-proposals-2026-10-16.md`(#25 节末有完成记录)。
|
|
41
|
+
|
|
42
|
+
### 建议接手顺序
|
|
43
|
+
1. 发 v0.7.0(上面第 0 节)
|
|
44
|
+
2. **#26-4/#26-5**(队列残余风险,副机侧为主,不依赖 DSH 新 API)
|
|
45
|
+
3. #27 面板下拉(等 `model/list` 进 fiber 服务面;届时副机加 models 清单上报,hub/面板只做消费)
|
|
46
|
+
4. EPERM 原子写 CI 抖动**立项修**(templates.js 的 rename EPERM,Windows runner 上已犯四次,每次 rerun 绿——是真实缺陷不是幻觉)
|
|
47
|
+
5. `launch/` 目录的发帖文案已备好(等用户发话再动)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. 工程纪律(踩过坑固化的,务必遵守)
|
|
52
|
+
|
|
53
|
+
1. **`npm run check` 不含 lint**(= packaging + doc-drift + tests)。推送前必须单独 `npm run lint`。
|
|
54
|
+
2. **git push 先试常规**(`timeout 25 git push`);不通走 API 复刻:`node "C:\Users\MR\AppData\Local\Temp\apipush\push-p2.mjs"`(⚠ 传 Windows 路径,Git Bash 的 `/c/...` node 不认)。API 推送后本地 tracking ref 会被沙箱**即时回滚**(`git update-ref` 成功后同一条 `&&` 链里 `rev-parse` 已是旧值,status 显示假 `[ahead N]`——远端其实是新的,先用 `gh api repos/<o>/<r>/commits/main` 核实)。修复:`sed -i` 直改 `.git/packed-refs` 单行(**sha 在前 ref 在后**)+ `rm -f .git/refs/remotes/origin/main` 清掉会压住 packed 的松散 ref,且完整 sha 必须 `git rev-parse` 取值、禁止手打。
|
|
55
|
+
3. **只 `git add` 自己的文件**——并行会话可能同仓库工作,`git add -A` 会扫走别人 WIP。
|
|
56
|
+
4. **测试跨平台双坑**(Windows 本地假绿、CI 才挂):
|
|
57
|
+
- 树结构子串断言用 `"name":"xxx"` 精确匹配(Linux symlink 创建成功会让 `link-to-secret` 命中裸 `secret`)
|
|
58
|
+
- macOS `os.tmpdir()` 是符号链接(/var → /private/var),realpath 归一后绝对路径前缀会变——路径断言用 `endsWith` 相对段
|
|
59
|
+
- 文本断言锚**定义处**不锚调用点(`indexOf('applySync(')` 命中 `this.applySync(...)` 调用点,固定长度窗口会随文件增删漂移——本次交接前刚修一例)
|
|
60
|
+
5. **每个 doc-drift 的 read() 新目标**要同步 `test/doc-drift-selfcheck.test.js` 的复制清单(lib/node/ 整目录已覆盖,lib/ 根下新文件要单列)。
|
|
61
|
+
6. CI 已知抖动:Windows templates.js EPERM 原子写(见上)、偶发 diag:inject runner 失败——rerun 即绿,别当代码问题查。
|
|
62
|
+
|
|
63
|
+
## 3. 架构速览(关键决策,勿动摇)
|
|
64
|
+
|
|
65
|
+
- 副机是任务状态权威;hub 崩溃靠 task/sync 对账,reconcile 不重跑 completed。
|
|
66
|
+
- 中继只见密文(E2E 默认开,`relayE2E:false` 可关;已知局限:主动降级 + enrollToken 冒充,SECURITY.md 如实钉住)。
|
|
67
|
+
- 凭证不离开副机;#27 起副机只自述 provider 键名 + model id。
|
|
68
|
+
- roots 是安全边界(realpath 展开 8.3/符号链接后前缀比对);denyPaths 枚举层生效。
|
|
69
|
+
- 产物语义:outputs 是扫描过滤器,不是 final 提升器(final 由内置扩展名表 + artifactRules + dist/ 决定)。
|
|
70
|
+
- #25 文件工具走独立 `fs/req`↔`fs/res` 帧,**不进任务队列不占并发槽**。
|
|
71
|
+
- 跨版本兼容模式:新字段(queue/runnerKind/defaultModel)缺失时消费方按 null 处理,旧副机/旧 hub 混跑不炸。
|
|
72
|
+
|
|
73
|
+
## 4. 关键文件
|
|
74
|
+
|
|
75
|
+
- 副机:`lib/node/index.js`(装配/帧分发/readDefaultModel)、`task-engine.js`(队列/超时常量/queueStats)、`fs-service.js`(#25)、`runner.js`(in-process/headless)
|
|
76
|
+
- hub:`lib/hub/tools.js`(12 工具)、`node-registry.js`(fsPending/列表投影)、`registry-ingest.js`(RESOURCE 摄入 + queue/defaultModel 清洗)
|
|
77
|
+
- 测试:`test/fs-service.test.js`、`test/model-selfreport.test.js`、`test/engine-e2e.test.js`、`test/hub-worker-e2e.test.js`(真通道 e2e 主战场)
|
|
78
|
+
- 记忆/历史:工作区 `.workbuddy/memory/2026-10-15.md` ~ `2026-10-17.md`(含全部踩坑链)
|
package/lib/hub/node-registry.js
CHANGED
|
@@ -865,6 +865,10 @@ class NodeRegistryCore {
|
|
|
865
865
|
// 队列健康快照(提案 #26-3):null = 副机未上报(旧版本)。starved=true
|
|
866
866
|
// 表示"有排队且并发占满且最久排队超 2 分钟"——派发前值得先看一眼。
|
|
867
867
|
queue: r.queue ?? null,
|
|
868
|
+
// 执行后端 + 默认模型(提案 #27):面板显示与派发前校验的依据。
|
|
869
|
+
// null = 副机未上报或未配置默认模型。
|
|
870
|
+
runnerKind: typeof r.runnerKind === 'string' ? r.runnerKind : null,
|
|
871
|
+
defaultModel: r.defaultModel && typeof r.defaultModel === 'object' ? r.defaultModel : null,
|
|
868
872
|
// 仓库清单(提案 #13):路由提示。remote_nodes 给名字列表供选型,
|
|
869
873
|
// repo 路由细节在 pickNodeByRepo。
|
|
870
874
|
repos: Array.isArray(r.repos) ? r.repos.map((x) => x.name) : [],
|
|
@@ -175,6 +175,19 @@ export function withIngest(Base) {
|
|
|
175
175
|
starved: q.starved === true,
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
+
// 执行后端 + 默认模型(提案 #27):面板显示与派发前校验的依据。
|
|
179
|
+
// 每帧整体替换(副机是这一事实的权威);旧副机不上报则保留原值。
|
|
180
|
+
if (typeof d.runnerKind === 'string' && d.runnerKind !== '') rec.runnerKind = d.runnerKind.slice(0, 32)
|
|
181
|
+
const dm = d.defaultModel && typeof d.defaultModel === 'object' && !Array.isArray(d.defaultModel) ? d.defaultModel : null
|
|
182
|
+
if (dm && typeof dm.model === 'string' && dm.model !== '') {
|
|
183
|
+
rec.defaultModel = {
|
|
184
|
+
provider: typeof dm.provider === 'string' ? dm.provider.slice(0, 64) : '',
|
|
185
|
+
model: dm.model.slice(0, 128),
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// 显式 null 是合法上报(副机没配默认模型)—— 清掉旧值避免误导
|
|
189
|
+
if (d.defaultModel === null) rec.defaultModel = null
|
|
190
|
+
}
|
|
178
191
|
return
|
|
179
192
|
}
|
|
180
193
|
|
package/lib/hub/tools.js
CHANGED
|
@@ -434,6 +434,15 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
434
434
|
starved: { type: 'boolean' },
|
|
435
435
|
},
|
|
436
436
|
},
|
|
437
|
+
// 执行后端 + 默认模型(提案 #27):副机未上报时为 null。
|
|
438
|
+
runnerKind: { type: 'string' },
|
|
439
|
+
defaultModel: {
|
|
440
|
+
type: 'object',
|
|
441
|
+
properties: {
|
|
442
|
+
provider: { type: 'string' },
|
|
443
|
+
model: { type: 'string' },
|
|
444
|
+
},
|
|
445
|
+
},
|
|
437
446
|
},
|
|
438
447
|
required: ['nodeId', 'online'],
|
|
439
448
|
},
|
|
@@ -465,6 +474,11 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
465
474
|
lines.push(` 队列: ${n.queue.running}/${n.queue.maxConcurrent} 在跑,${n.queue.queued} 条排队`)
|
|
466
475
|
}
|
|
467
476
|
}
|
|
477
|
+
// 后端与默认模型(提案 #27):派模型单前先看这台副机有什么
|
|
478
|
+
const bits = []
|
|
479
|
+
if (n.runnerKind) bits.push(`后端 ${n.runnerKind}`)
|
|
480
|
+
if (n.defaultModel?.model) bits.push(`默认模型 ${n.defaultModel.provider ? `${n.defaultModel.provider}/` : ''}${n.defaultModel.model}`)
|
|
481
|
+
if (bits.length > 0) lines.push(` ${bits.join(' · ')}`)
|
|
468
482
|
}
|
|
469
483
|
return lines.join('\n')
|
|
470
484
|
}),
|
|
@@ -536,6 +550,16 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
536
550
|
mode: { type: 'string' },
|
|
537
551
|
approval: { type: 'string' },
|
|
538
552
|
repo: { type: 'string' },
|
|
553
|
+
// 模型回显(提案 #27):dryRun 返回解析后的选择与来源;
|
|
554
|
+
// 正式派发路径不填(模型生效情况看 handoff)。
|
|
555
|
+
model: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
properties: {
|
|
558
|
+
provider: { type: 'string' },
|
|
559
|
+
model: { type: 'string' },
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
modelSource: { type: 'string' },
|
|
539
563
|
error: { type: 'object', properties: { code: { type: 'string' }, message: { type: 'string' } } },
|
|
540
564
|
hint: { type: 'string' },
|
|
541
565
|
},
|
|
@@ -544,10 +568,14 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
544
568
|
render: renderText((_args, value) => {
|
|
545
569
|
if (!value) return 'remote_task: 无返回'
|
|
546
570
|
if (value.dryRun === true) {
|
|
571
|
+
const modelLine = value.model?.model
|
|
572
|
+
? `model: ${value.model.provider ? `${value.model.provider}/` : ''}${value.model.model}(${value.modelSource ?? ''})`
|
|
573
|
+
: 'model: 未指定'
|
|
547
574
|
return [
|
|
548
575
|
'[dry-run 预览] 未派发、未落库 —— 确认无误后去掉 dryRun 正式派发',
|
|
549
576
|
`node: ${value.node}${value.repo ? `(repo 命中:${value.repo})` : ''}`,
|
|
550
577
|
`mode: ${value.mode ?? 'agent'}${value.approval === 'manual' ? '(将挂起等待审批)' : ''}`,
|
|
578
|
+
modelLine,
|
|
551
579
|
`workspace: ${value.workspace ?? '(节点默认)'}`,
|
|
552
580
|
value.hint ?? '',
|
|
553
581
|
].filter((l) => l !== '').join('\n')
|
|
@@ -623,6 +651,18 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
623
651
|
})
|
|
624
652
|
if (!v.ok) throw new RemoteAgentError(`invalid task: ${v.error}`, ERROR_CODE.INTERNAL)
|
|
625
653
|
|
|
654
|
+
// 提案 #27:模型选择的派发前校验。headless 后端不支持指定模型 ——
|
|
655
|
+
// 旧链路要等任务跑到副机才失败(浪费一轮往返 + 烧一个 taskId),
|
|
656
|
+
// 现在派发前就拦。runnerKind 是副机随资源帧自述的(#27),旧副机
|
|
657
|
+
// 未上报时为 null → 不拦,保留副机侧兜底报错(向后兼容)。
|
|
658
|
+
const recForModel = registry.getNode(nodeId)
|
|
659
|
+
if (v.spec.mode === TASK_MODE.AGENT && v.spec.model && recForModel?.runnerKind === 'headless') {
|
|
660
|
+
throw new RemoteAgentError(
|
|
661
|
+
`节点 ${nodeId} 是 headless 后端,不支持指定模型(副机固定用默认模型)。去掉 model 参数重试,或让副机以 in-process 后端接入。`,
|
|
662
|
+
ERROR_CODE.INTERNAL,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
626
666
|
const str = (x) => (typeof x === 'string' ? x : '')
|
|
627
667
|
// 提案 #23:dry-run —— 只算"会怎么派",不落库不派帧、不烧 taskId。
|
|
628
668
|
// 路由与校验和真实派发走同一条代码路径(同一处 repo 解析、同一个
|
|
@@ -631,6 +671,14 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
631
671
|
// workspace 省略时副机会用 defaultWorkspace(= hello.roots[0],见
|
|
632
672
|
// node/index.js)—— 预览必须镜像同一回填,否则预览和实派不一致。
|
|
633
673
|
const rec0 = registry.getNode(nodeId)
|
|
674
|
+
// 提案 #27:回显解析后的模型与来源 —— "派之前就知道用什么模型",
|
|
675
|
+
// 而不是派完看 handoff。用户指定优先;否则借副机默认(若已上报)。
|
|
676
|
+
const selModel = v.spec.model
|
|
677
|
+
? { provider: str(v.spec.model.provider), model: str(v.spec.model.model) }
|
|
678
|
+
: (rec0?.defaultModel && typeof rec0.defaultModel === 'object' ? { provider: str(rec0.defaultModel.provider), model: str(rec0.defaultModel.model) } : null)
|
|
679
|
+
const modelSource = v.spec.model
|
|
680
|
+
? '用户指定'
|
|
681
|
+
: (rec0?.defaultModel ? '副机默认(用户未指定)' : '未指定(副机未上报默认模型)')
|
|
634
682
|
return {
|
|
635
683
|
taskId: '',
|
|
636
684
|
node: nodeId,
|
|
@@ -640,6 +688,8 @@ export function registerRemoteTools({ ctx, registry, pulls, log: _log, defaults
|
|
|
640
688
|
approval: v.spec.approval,
|
|
641
689
|
workspace: str(v.spec.workspace) || str(rec0?.hello?.roots?.[0]),
|
|
642
690
|
repo: str(repoHit?.name),
|
|
691
|
+
...(selModel ? { model: selModel } : {}),
|
|
692
|
+
modelSource: str(modelSource),
|
|
643
693
|
hint: '预览即真实(与正式派发同一套路由+校验);确认无误后去掉 dryRun 正式派发。',
|
|
644
694
|
}
|
|
645
695
|
}
|
package/lib/node/index.js
CHANGED
|
@@ -301,9 +301,34 @@ export function resolveToken(config, stateDir = '') {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 读副机当前默认模型选择(提案 #27)。
|
|
306
|
+
*
|
|
307
|
+
* 来源是 DSH 的 `agentDefaultModel` 服务(`currentSelection()` 返回 detached
|
|
308
|
+
* 的 provider/model/reasoning 选择)。**只报 provider 键名与 model id,
|
|
309
|
+
* 不碰任何凭证**。取不到(服务缺失 / 抛错 / 字段形态不对)一律 null ——
|
|
310
|
+
* 上报是尽力而为的遥测,绝不能影响资源帧本身。
|
|
311
|
+
*
|
|
312
|
+
* 完整的"可用模型清单"第一版不做:in-process 后端没有稳定的清单 API
|
|
313
|
+
* (DSH 内部的 model/list 走 app-server 协议面,fiber 服务摸不到),
|
|
314
|
+
* 探测 llm-pi-ai 内部结构太脆弱。等 DSH 把清单暴露成稳定 API 再补。
|
|
315
|
+
*/
|
|
316
|
+
export function readDefaultModel(ctx) {
|
|
317
|
+
try {
|
|
318
|
+
const dm = typeof ctx?.get === 'function' ? ctx.get('agentDefaultModel') : undefined
|
|
319
|
+
const sel = dm && typeof dm.currentSelection === 'function' ? dm.currentSelection() : undefined
|
|
320
|
+
if (!sel || typeof sel !== 'object') return null
|
|
321
|
+
const provider = typeof sel.provider === 'string' && sel.provider !== '' ? sel.provider.slice(0, 64) : ''
|
|
322
|
+
const model = typeof sel.model === 'string' && sel.model !== '' ? sel.model.slice(0, 128) : ''
|
|
323
|
+
if (model === '') return null
|
|
324
|
+
return { provider, model }
|
|
325
|
+
} catch {
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
304
330
|
/** 组 hello:hub 与 relay 都靠它认这台机器。 */
|
|
305
|
-
export function buildNodeHello(config, { roots, nodeId } = {}) {
|
|
306
|
-
const c = asConfig(config)
|
|
331
|
+
export function buildNodeHello(config, { roots, nodeId } = {}) { const c = asConfig(config)
|
|
307
332
|
// 不变式:产出的 hello **必须**能过 protocol.validateHello。所以显式 override 也
|
|
308
333
|
// 要过一遍 sanitizeNodeId —— 调用方塞一个未清洗的 id(`''` / `'中文节点'` / `'X Y'`)
|
|
309
334
|
// 会让 buildNodeHello 直接产出 hub 必拒的 hello(`hello-nodeId-invalid`),而
|
|
@@ -565,7 +590,16 @@ function startWorker(ctx, config) {
|
|
|
565
590
|
// 装配,但 interval 回调最早也在装配完成后才触发,引用安全。
|
|
566
591
|
// engine 尚未就绪(极端:interval 在装配中途先触发)时给空快照。
|
|
567
592
|
const queue = engine ? engine.queueStats() : { queued: 0, running: 0, maxConcurrent: 0, oldestQueuedMs: 0, starved: false }
|
|
568
|
-
|
|
593
|
+
// 提案 #27:执行后端与默认模型随帧自述 —— hub 派发前要知道
|
|
594
|
+
// "这台副机用哪个后端、默认模型是什么"(面板显示 + 派发前校验)。
|
|
595
|
+
// currentSelection 每帧重算(用户可在副机改默认),取不到就 null。
|
|
596
|
+
send(NODE_TO_HUB.RESOURCE, {
|
|
597
|
+
nodeId,
|
|
598
|
+
...sampler.sample(),
|
|
599
|
+
queue,
|
|
600
|
+
runnerKind: runner.kind,
|
|
601
|
+
defaultModel: readDefaultModel(ctx),
|
|
602
|
+
})
|
|
569
603
|
}, resourceIntervalMs)
|
|
570
604
|
: undefined
|
|
571
605
|
if (resourceTimer?.unref) resourceTimer.unref()
|