dsh-subagent-profile 0.3.2 → 0.3.4
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 +77 -40
- package/README.zh.md +111 -74
- package/docs/screenshots/dispatch-card.png +0 -0
- package/docs/screenshots/settings-page1.png +0 -0
- package/docs/screenshots/settings-page2.png +0 -0
- package/index.mjs +276 -81
- package/lib/client.js +3218 -166
- package/lib/core/adoption-reminder.mjs +48 -0
- package/lib/core/adoption-tracker.mjs +430 -0
- package/lib/core/background-ledger.mjs +71 -0
- package/lib/core/catalog-cache.mjs +45 -7
- package/lib/core/catalog.mjs +6 -6
- package/lib/core/cost-evidence.mjs +145 -0
- package/lib/core/cost-guard.mjs +71 -44
- package/lib/core/decision-trace.mjs +413 -0
- package/lib/core/delegation.mjs +111 -50
- package/lib/core/dispatch-gates.mjs +153 -0
- package/lib/core/dispatch-guard.mjs +156 -0
- package/lib/core/dispatch-schema.mjs +103 -14
- package/lib/core/dispatch-tool.mjs +220 -204
- package/lib/core/draft-gates.mjs +45 -0
- package/lib/core/drafts-store.mjs +45 -0
- package/lib/core/escape.mjs +130 -0
- package/lib/core/evolution-advice.mjs +224 -0
- package/lib/core/evolution-ledger.mjs +300 -0
- package/lib/core/evolution-summary.mjs +255 -0
- package/lib/core/http-routes.mjs +256 -72
- package/lib/core/intersection.mjs +6 -9
- package/lib/core/presets-sync.mjs +161 -43
- package/lib/core/prices.mjs +46 -0
- package/lib/core/profile-directory.mjs +139 -0
- package/lib/core/profile-provider.mjs +42 -39
- package/lib/core/profiles-store.mjs +103 -76
- package/lib/core/pure.mjs +110 -66
- package/lib/core/reminder-store.mjs +172 -0
- package/lib/core/shims.mjs +67 -76
- package/lib/core/whitelist.mjs +23 -17
- package/package.json +82 -83
- package/presets/orchestrator/agent.cordis.yml +59 -87
- package/presets/orchestrator/NOTICE +0 -3
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
1
|
+
// lib/core/presets-sync.mjs — bundled agent 预设自安装(从 index.mjs 逐字移出;
|
|
2
|
+
// 仅 node 内置,无 @deepseek-ai 依赖)。
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// Windows
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, utimesSync } from 'node:fs';
|
|
16
|
-
import { basename, dirname, join, relative } from 'node:path';
|
|
4
|
+
// 宿主启动时插件把 bundled `presets/` 树同步进 DSH agent-presets 发现根
|
|
5
|
+
// (~/.dsh/.agent-presets),使「编排者模式」无需手动复制就出现在新建会话
|
|
6
|
+
// 选择器里——与官方 dsh-liangshen bundle 相同的自安装模式。同步按目录、
|
|
7
|
+
// 经哈希门控:per-target sidecar(.dsh-sync.json)记录 bundled 内容哈希与派生树
|
|
8
|
+
// 快照哈希,下次运行跳过未变化的树、跳过(绝不覆盖)用户改过的树;两侧都变时
|
|
9
|
+
// 先把用户侧归档到 .user-modified-<timestamp>/ 再写入升级后的 bundled 内容。
|
|
10
|
+
// bundle 不再提供的目标文件会被剪除;插件不拥有的目录永不触碰。
|
|
11
|
+
// 刻意不用 node:fs cpSync:Node 22 的 Windows 上,fs.cpSync({ recursive: true })
|
|
12
|
+
// 在源路径含非 ASCII(CJK 主目录,nodejs/node#54476)时可能崩溃进程,故逐条
|
|
13
|
+
// 复制并保留源 mtime。
|
|
14
|
+
|
|
15
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// (same resolved value as when this function lived in index.mjs at the package
|
|
23
|
-
// root; Task 8.6 moved the module from lib/ to lib/core/ and the climb must be
|
|
24
|
-
// `../../`, not `../` — preflight's preset-tree reconciliation caught the stale
|
|
25
|
-
// one returning lib/presets, which silently disabled the startup self-install).
|
|
20
|
+
// 本包内 bundled 预设树的绝对路径。本模块在 lib/core/,比包根低两层,URL 必须
|
|
21
|
+
// 上爬两段才能指向仓库根 `presets/` 目录(与它还在包根的 index.mjs 时解析到的
|
|
22
|
+
// 值相同;写错成 `../` 会指向不存在的 lib/presets,从而静默禁用启动自安装)。
|
|
26
23
|
export function bundledPresetsRoot() {
|
|
27
24
|
return fileURLToPath(new URL('../../presets', import.meta.url));
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export const MTIME_TOLERANCE_MS = 1000;
|
|
31
28
|
|
|
29
|
+
// 同步元数据:sidecar 记录最近一次 bundled/目标内容哈希,归档前缀命名三方
|
|
30
|
+
// 用户编辑备份。两者都是插件本地记账、非 preset 内容——hashTree 排除它们、
|
|
31
|
+
// prune 保留它们(否则 sidecar 自身写入会让目标哈希自指漂移,归档会被剪掉)。
|
|
32
|
+
export const SIDECAR_NAME = '.dsh-sync.json';
|
|
33
|
+
export const ARCHIVE_PREFIX = '.user-modified-';
|
|
34
|
+
|
|
32
35
|
export function filesUnder(root) {
|
|
33
36
|
const out = [];
|
|
34
37
|
const walk = (dir) => {
|
|
@@ -42,7 +45,7 @@ export function filesUnder(root) {
|
|
|
42
45
|
return out;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
//
|
|
48
|
+
// 文件同一性按字节;size/mtime 只作快速否定检查。
|
|
46
49
|
export function sameFile(a, b) {
|
|
47
50
|
const sa = statSync(a);
|
|
48
51
|
const sb = statSync(b);
|
|
@@ -65,7 +68,7 @@ export function copyTreeSync(sourceDir, targetDir) {
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
//
|
|
71
|
+
// 删除 `keep` 之外的 target 文件,再只删因此清空的目录。
|
|
69
72
|
export function pruneExtras(root, keep) {
|
|
70
73
|
const parents = new Set();
|
|
71
74
|
for (const file of filesUnder(root)) {
|
|
@@ -85,38 +88,151 @@ export function pruneExtras(root, keep) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
//
|
|
91
|
+
// preset 树内容哈希:每文件的相对路径 + 字节喂进 sha1。同步元数据(sidecar 与
|
|
92
|
+
// 任何 `.user-modified-*` 归档)被排除——它是插件记账、非 preset 内容,纳入会使
|
|
93
|
+
// 派生树哈希在 sidecar 被重写(自指)或归档创建时立即漂移。
|
|
94
|
+
export function hashTree(root) {
|
|
95
|
+
const hash = createHash('sha1');
|
|
96
|
+
for (const file of filesUnder(root).sort()) {
|
|
97
|
+
const rel = relative(root, file);
|
|
98
|
+
if (isSyncMetadata(rel)) continue;
|
|
99
|
+
hash.update(rel);
|
|
100
|
+
hash.update('\0');
|
|
101
|
+
hash.update(readFileSync(file));
|
|
102
|
+
hash.update('\0');
|
|
103
|
+
}
|
|
104
|
+
return hash.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 相对 preset 根的路径是否为同步元数据(sidecar 文件,或 `.user-modified-*`
|
|
108
|
+
// 归档目录下的任何内容)。
|
|
109
|
+
export function isSyncMetadata(rel) {
|
|
110
|
+
const head = rel.split(sep)[0];
|
|
111
|
+
return head === SIDECAR_NAME || head.startsWith(ARCHIVE_PREFIX);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function sidecarPath(targetDir) {
|
|
115
|
+
return join(targetDir, SIDECAR_NAME);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 读 sidecar;缺失或畸形(版本错 / 哈希字段缺 / 解析失败)按缺失处理,使下次
|
|
119
|
+
// 同步走保守的首跑路径,而不是信任损坏的记账。
|
|
120
|
+
function readSidecar(targetDir) {
|
|
121
|
+
const path = sidecarPath(targetDir);
|
|
122
|
+
if (!existsSync(path)) return undefined;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
125
|
+
if (parsed && parsed.version === 1 && typeof parsed.bundledHash === 'string' && typeof parsed.targetHash === 'string') {
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeSidecar(targetDir, bundledHash, targetHash) {
|
|
135
|
+
writeFileSync(sidecarPath(targetDir), `${JSON.stringify({ version: 1, bundledHash, targetHash }, null, 2)}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 复制一个文件系统条目(文件或目录),保留逐条语义与源 mtime——与 copyTreeSync
|
|
139
|
+
// 自身文件分支同形。
|
|
140
|
+
function copyEntry(source, dest) {
|
|
141
|
+
const st = statSync(source);
|
|
142
|
+
if (st.isDirectory()) copyTreeSync(source, dest);
|
|
143
|
+
else {
|
|
144
|
+
copyFileSync(source, dest);
|
|
145
|
+
utimesSync(dest, st.atime, st.mtime);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 覆盖前把当前(用户可见的)preset 树归档到
|
|
150
|
+
// <targetDir>/.user-modified-<timestamp>/。sidecar 与既有归档被排除,归档不会
|
|
151
|
+
// 嵌套。写失败返回 false——调用方必须跳过本次同步、保留用户编辑原样
|
|
152
|
+
// (fail-safe:备份失败绝不允许成为销毁用户内容的许可)。
|
|
153
|
+
function archiveUserTree(targetDir) {
|
|
154
|
+
const archiveDir = join(targetDir, `${ARCHIVE_PREFIX}${Date.now()}`);
|
|
155
|
+
// 先拍下要归档的条目再创建 archiveDir:归档位于 targetDir 之下,mkdirSync 后
|
|
156
|
+
// 再读目录会把归档本身读进来,无限递归进它自己。
|
|
157
|
+
const entries = [];
|
|
158
|
+
for (const entry of readdirSync(targetDir)) {
|
|
159
|
+
if (entry === SIDECAR_NAME || entry.startsWith(ARCHIVE_PREFIX)) continue;
|
|
160
|
+
entries.push(entry);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
copyEntry(join(targetDir, entry), join(archiveDir, entry));
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
try { rmSync(archiveDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// pruneExtras 的 keep 集:同步后树必须保留的一切——bundled 文件、sidecar、
|
|
175
|
+
// 既有 `.user-modified-*` 归档下的每个文件。keep 里没有归档条目的话,
|
|
176
|
+
// pruneExtras 会在同步当下删掉归档,静默丢弃它本该保留的用户编辑。
|
|
177
|
+
function computeKeep(sourceDir, targetDir) {
|
|
178
|
+
const keep = new Set(filesUnder(sourceDir).map((f) => relative(sourceDir, f)));
|
|
179
|
+
keep.add(SIDECAR_NAME);
|
|
180
|
+
if (existsSync(targetDir)) {
|
|
181
|
+
for (const entry of readdirSync(targetDir)) {
|
|
182
|
+
if (!entry.startsWith(ARCHIVE_PREFIX)) continue;
|
|
183
|
+
const dir = join(targetDir, entry);
|
|
184
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
185
|
+
for (const f of filesUnder(dir)) keep.add(relative(targetDir, f));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return keep;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 同步一个 preset 目录。返回 'synced'(写入或升级)、'current'(未变)或
|
|
192
|
+
// 'user-modified'(target 偏离上次同步快照而跳过——绝不覆盖用户编辑)。判定表:
|
|
193
|
+
// - target 不存在 → 复制 + 写 sidecar → 'synced'
|
|
194
|
+
// - 无 sidecar、target 存在 → 一次性归档 + 同步 → 'synced'
|
|
195
|
+
// - bundled 未变、target 相同 → 'current'
|
|
196
|
+
// - bundled 未变、target 漂移 → 'user-modified'(跳过)
|
|
197
|
+
// - bundled 变了 → 漂移则先归档,再同步 → 'synced'
|
|
89
198
|
export function syncOnePreset(sourceDir, targetDir) {
|
|
90
|
-
const
|
|
91
|
-
const sourceSet = new Set(sourceFiles.map((f) => relative(sourceDir, f)));
|
|
199
|
+
const bundledHash = hashTree(sourceDir);
|
|
92
200
|
if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
|
|
93
|
-
|
|
201
|
+
// target 意外是文件(同名冲突)时先改名备份再清,避免直接 rmSync 丢失用户内容。
|
|
202
|
+
try { renameSync(targetDir, `${targetDir}.removed-${Date.now()}`); } catch { rmSync(targetDir, { recursive: true, force: true }); }
|
|
94
203
|
}
|
|
95
204
|
if (!existsSync(targetDir)) {
|
|
96
205
|
copyTreeSync(sourceDir, targetDir);
|
|
97
|
-
|
|
206
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
98
207
|
return 'synced';
|
|
99
208
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
209
|
+
const sidecar = readSidecar(targetDir);
|
|
210
|
+
// 无 sidecar + 既有 target = 门控前状态:无法判断用户是否编辑过,按「可能编辑
|
|
211
|
+
// 过」处理,在首次门控同步前归档一次(一次性迁移)。
|
|
212
|
+
if (sidecar === undefined) {
|
|
213
|
+
if (!archiveUserTree(targetDir)) return 'user-modified';
|
|
214
|
+
copyTreeSync(sourceDir, targetDir);
|
|
215
|
+
pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
|
|
216
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
217
|
+
return 'synced';
|
|
104
218
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
219
|
+
const currentTargetHash = hashTree(targetDir);
|
|
220
|
+
if (sidecar.bundledHash === bundledHash) {
|
|
221
|
+
return sidecar.targetHash === currentTargetHash ? 'current' : 'user-modified';
|
|
222
|
+
}
|
|
223
|
+
// bundled 升级。用户也改过 target(快照漂移)时先归档其树,编辑不会静默丢失。
|
|
224
|
+
if (sidecar.targetHash !== currentTargetHash) {
|
|
225
|
+
if (!archiveUserTree(targetDir)) return 'user-modified';
|
|
109
226
|
}
|
|
110
|
-
if (!dirty) return 'current';
|
|
111
|
-
pruneExtras(targetDir, sourceSet);
|
|
112
227
|
copyTreeSync(sourceDir, targetDir);
|
|
113
|
-
pruneExtras(targetDir,
|
|
228
|
+
pruneExtras(targetDir, computeKeep(sourceDir, targetDir));
|
|
229
|
+
writeSidecar(targetDir, bundledHash, hashTree(targetDir));
|
|
114
230
|
return 'synced';
|
|
115
231
|
}
|
|
116
232
|
|
|
117
|
-
//
|
|
233
|
+
// 把 `presets/` 下每个 preset 目录同步进目标发现根。
|
|
118
234
|
export function syncBundledPresets(targetRoot) {
|
|
119
|
-
const result = { synced: [], current: [], failed: [] };
|
|
235
|
+
const result = { synced: [], current: [], userModified: [], failed: [] };
|
|
120
236
|
const sourceRoot = bundledPresetsRoot();
|
|
121
237
|
mkdirSync(targetRoot, { recursive: true });
|
|
122
238
|
if (existsSync(sourceRoot)) {
|
|
@@ -126,7 +242,9 @@ export function syncBundledPresets(targetRoot) {
|
|
|
126
242
|
const id = basename(source);
|
|
127
243
|
try {
|
|
128
244
|
const outcome = syncOnePreset(source, join(targetRoot, id));
|
|
129
|
-
(outcome === 'synced'
|
|
245
|
+
if (outcome === 'synced') result.synced.push(id);
|
|
246
|
+
else if (outcome === 'current') result.current.push(id);
|
|
247
|
+
else result.userModified.push(id);
|
|
130
248
|
} catch (error) {
|
|
131
249
|
result.failed.push({ id, error: error instanceof Error ? error.message : String(error) });
|
|
132
250
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// lib/core/prices.mjs — 模型单价表(纯数据)。
|
|
2
|
+
// 来源:docs/measured-params.md / docs/cost-closed-loop-design.md(2026-08-24 实测)。
|
|
3
|
+
// 口径:估算、非精确计费;A 级为「单次均价」、B 级为「五段单价」(元/千 token)。
|
|
4
|
+
// 可被 A″ / F 数据层复用(同源同表,不散落魔法数)。
|
|
5
|
+
|
|
6
|
+
// A 级:模型 → 单次均价(元/次)。同规模派发实测:flash 0.0039、继承父(pro)0.0104。
|
|
7
|
+
export const MODEL_AVG_COST = Object.freeze({
|
|
8
|
+
'deepseek-v4-flash': 0.0039,
|
|
9
|
+
'deepseek-v4-pro': 0.0104,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// B 级:模型 → 五段单价(元/千 token)。当前仅有单次均价实测,五段单价先按
|
|
13
|
+
// 均价占位(input/output 同价),保留键结构供未来实测回填;cache 段缺数据记 null。
|
|
14
|
+
export const MODEL_USAGE_PRICES = Object.freeze({
|
|
15
|
+
'deepseek-v4-flash': Object.freeze({
|
|
16
|
+
inputTokens: null,
|
|
17
|
+
outputTokens: null,
|
|
18
|
+
cacheReadTokens: null,
|
|
19
|
+
cacheWriteTokens: null,
|
|
20
|
+
reasoningTokens: null,
|
|
21
|
+
avgCost: MODEL_AVG_COST['deepseek-v4-flash'],
|
|
22
|
+
}),
|
|
23
|
+
'deepseek-v4-pro': Object.freeze({
|
|
24
|
+
inputTokens: null,
|
|
25
|
+
outputTokens: null,
|
|
26
|
+
cacheReadTokens: null,
|
|
27
|
+
cacheWriteTokens: null,
|
|
28
|
+
reasoningTokens: null,
|
|
29
|
+
avgCost: MODEL_AVG_COST['deepseek-v4-pro'],
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 模型名归一:常见别名/前缀统一到价格键;未知返回 undefined(fail-soft 不猜价)。
|
|
34
|
+
export function priceKeyFor(model) {
|
|
35
|
+
if (typeof model !== 'string' || model === '') return undefined;
|
|
36
|
+
const m = model.trim().toLowerCase();
|
|
37
|
+
if (MODEL_AVG_COST[m] !== undefined) return m;
|
|
38
|
+
if (m.includes('flash')) return 'deepseek-v4-flash';
|
|
39
|
+
if (m.includes('pro')) return 'deepseek-v4-pro';
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function avgCostFor(model) {
|
|
44
|
+
const key = priceKeyFor(model);
|
|
45
|
+
return key === undefined ? undefined : MODEL_AVG_COST[key];
|
|
46
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// lib/core/profile-directory.mjs — 派发方案目录的单一数据源。
|
|
2
|
+
// profileSectionText(模型可见段)与 profileSnapshotOf(决策轨迹记录面)都从
|
|
3
|
+
// 本模块的 profileDirectoryRows 派生,构造保证「模型看到 ≡ 记录面」的 id、顺序与
|
|
4
|
+
// 字段集。未来 F 数据层注入 avg_cost / success_rate 只改这里一处。
|
|
5
|
+
//
|
|
6
|
+
// 仅依赖 lib/core/pure.mjs(tierSortKey);无 @deepseek-ai 依赖,可被 bare-CI
|
|
7
|
+
// 单测直接 import。
|
|
8
|
+
|
|
9
|
+
import { tierSortKey } from './pure.mjs';
|
|
10
|
+
|
|
11
|
+
// 决策输入快照阈值:profiles_snapshot 条目数与 description 截断长度。
|
|
12
|
+
const MAX_PROFILE_SNAPSHOT_ENTRIES = 6;
|
|
13
|
+
const PROFILE_DESC_CHARS = 80;
|
|
14
|
+
|
|
15
|
+
function snapshotEntry(row, maxDesc) {
|
|
16
|
+
const description = typeof row.description === 'string' ? row.description : '';
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
...(description !== '' ? { description: description.length > maxDesc ? `${description.slice(0, maxDesc)}…` : description } : {}),
|
|
20
|
+
...(typeof row.preset === 'string' && row.preset !== '' ? { preset: row.preset } : {}),
|
|
21
|
+
...(typeof row.model === 'string' && row.model !== '' ? { model: row.model } : {}),
|
|
22
|
+
...(typeof row.provider === 'string' && row.provider !== '' ? { provider: row.provider } : {}),
|
|
23
|
+
...(typeof row.tokenTier === 'string' && row.tokenTier !== '' ? { tokenTier: row.tokenTier } : {}),
|
|
24
|
+
...(typeof row.avgCost === 'number' && Number.isFinite(row.avgCost) ? { avgCost: row.avgCost } : {}),
|
|
25
|
+
...(typeof row.successRate === 'number' && Number.isFinite(row.successRate) ? { successRate: row.successRate } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 模型可见 profile 目录快照:输入为 profileDirectoryRows 的结构化行。description
|
|
30
|
+
// 截断、条目数截到 maxEntries;chosenId 超出 maxEntries 时并入该条并打
|
|
31
|
+
// chosenOutsideSnapshot(37b:截断不得制造审计空洞)。
|
|
32
|
+
export function profileSnapshotOf(profiles, { maxEntries = MAX_PROFILE_SNAPSHOT_ENTRIES, maxDesc = PROFILE_DESC_CHARS, chosenId } = {}) {
|
|
33
|
+
const list = Array.isArray(profiles) ? profiles : [];
|
|
34
|
+
const full = [];
|
|
35
|
+
for (const row of list) {
|
|
36
|
+
if (row === null || typeof row !== 'object' || typeof row.id !== 'string' || row.id === '') continue;
|
|
37
|
+
if (row.enabled === false) continue;
|
|
38
|
+
full.push(snapshotEntry(row, maxDesc));
|
|
39
|
+
}
|
|
40
|
+
const entries = full.slice(0, maxEntries);
|
|
41
|
+
let truncated = full.length > maxEntries;
|
|
42
|
+
if (typeof chosenId === 'string' && chosenId !== '' && chosenId !== '(inline)' && !entries.some((e) => e.id === chosenId)) {
|
|
43
|
+
const chosen = full.find((e) => e.id === chosenId);
|
|
44
|
+
if (chosen !== undefined) {
|
|
45
|
+
entries.push({ ...chosen, chosenOutsideSnapshot: true });
|
|
46
|
+
truncated = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { entries, truncated, total: full.length };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// store 兼容 createProfileStore 返回形态(含 profiles Map);也接受数组(测试与
|
|
53
|
+
// 非标准调用方)。只保留启用且 id 合法的方案;按 tokenTier cheap-first 稳定排序,
|
|
54
|
+
// tier 相同按 id 升序(确定性,不依赖 Map 插入序的跨运行差异)。
|
|
55
|
+
export function profileDirectoryRows(store) {
|
|
56
|
+
const source = store !== null && typeof store === 'object' && store.profiles !== undefined
|
|
57
|
+
? [...store.profiles.values()]
|
|
58
|
+
: (Array.isArray(store) ? store : []);
|
|
59
|
+
return source
|
|
60
|
+
.filter((p) => p !== null && typeof p === 'object' && typeof p.id === 'string' && p.id !== '' && p.enabled !== false)
|
|
61
|
+
.sort((a, b) => tierSortKey(a.tokenTier) - tierSortKey(b.tokenTier) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
62
|
+
.map((p) => ({
|
|
63
|
+
id: p.id,
|
|
64
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
65
|
+
...(typeof p.preset === 'string' && p.preset !== '' ? { preset: p.preset } : {}),
|
|
66
|
+
...(typeof p.model === 'string' && p.model !== '' ? { model: p.model } : {}),
|
|
67
|
+
...(typeof p.provider === 'string' && p.provider !== '' ? { provider: p.provider } : {}),
|
|
68
|
+
...(typeof p.tokenTier === 'string' && p.tokenTier !== '' ? { tokenTier: p.tokenTier } : {}),
|
|
69
|
+
...(typeof p.avgCost === 'number' && Number.isFinite(p.avgCost) ? { avgCost: p.avgCost } : {}),
|
|
70
|
+
...(typeof p.successRate === 'number' && Number.isFinite(p.successRate) ? { successRate: p.successRate } : {}),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// 从 summaries.json 的 l1/l2 聚合为每个 profile 提取 { avgCost, successRate, n }。
|
|
76
|
+
// 匹配优先级:profile.model → L2 model 轴;profile.preset → L2 preset 轴。多个 L1
|
|
77
|
+
// 组共享同一轴值时按成本与部署量合并(avg_cost = Σestimated_cost / Σpriced;
|
|
78
|
+
// successRate = Σcompleted / Σdeployments)。无数据返回 null,调用方省略显示。
|
|
79
|
+
export function profileStatsFromSummaries(store, summaries) {
|
|
80
|
+
const rows = profileDirectoryRows(store);
|
|
81
|
+
const l2 = summaries !== null && typeof summaries === 'object' && summaries.l2 !== null && typeof summaries.l2 === 'object' ? summaries.l2 : {};
|
|
82
|
+
const byModel = new Map();
|
|
83
|
+
const byPreset = new Map();
|
|
84
|
+
for (const [key, group] of Object.entries(l2)) {
|
|
85
|
+
if (group === null || typeof group !== 'object') continue;
|
|
86
|
+
const modelMatch = /:model:([^|]+)$/.exec(key);
|
|
87
|
+
if (modelMatch) {
|
|
88
|
+
const arr = byModel.get(modelMatch[1]) ?? [];
|
|
89
|
+
arr.push(group);
|
|
90
|
+
byModel.set(modelMatch[1], arr);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const presetMatch = /:preset:([^|]+)$/.exec(key);
|
|
94
|
+
if (presetMatch) {
|
|
95
|
+
const arr = byPreset.get(presetMatch[1]) ?? [];
|
|
96
|
+
arr.push(group);
|
|
97
|
+
byPreset.set(presetMatch[1], arr);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const merge = (groups) => {
|
|
101
|
+
let deployments = 0;
|
|
102
|
+
let completed = 0;
|
|
103
|
+
let priced = 0;
|
|
104
|
+
let cost = 0;
|
|
105
|
+
for (const g of groups) {
|
|
106
|
+
deployments += Number.isFinite(g.deployments_total) ? g.deployments_total : 0;
|
|
107
|
+
completed += Number.isFinite(g.outcome?.completed) ? g.outcome.completed : 0;
|
|
108
|
+
priced += Number.isFinite(g.cost?.priced) ? g.cost.priced : 0;
|
|
109
|
+
cost += Number.isFinite(g.cost?.estimated_cost) ? g.cost.estimated_cost : 0;
|
|
110
|
+
}
|
|
111
|
+
if (deployments <= 0 && priced <= 0) return null;
|
|
112
|
+
return {
|
|
113
|
+
avgCost: priced > 0 ? Number((cost / priced).toFixed(4)) : null,
|
|
114
|
+
successRate: deployments > 0 ? Number((completed / deployments).toFixed(3)) : null,
|
|
115
|
+
n: deployments,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const out = new Map();
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
let stats = null;
|
|
121
|
+
if (typeof row.model === 'string' && row.model !== '') stats = merge(byModel.get(row.model) ?? []);
|
|
122
|
+
if (stats === null && typeof row.preset === 'string' && row.preset !== '') stats = merge(byPreset.get(row.preset) ?? []);
|
|
123
|
+
if (stats !== null) out.set(row.id, stats);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 用 stats map 为目录行补 avgCost/successRate(F 数据层 join)。stats 为
|
|
129
|
+
// profileStatsFromSummaries 的返回值;行上已有 avgCost/successRate 时优先保留
|
|
130
|
+
// 显式值(store 侧/测试注入),stats 只补缺失。
|
|
131
|
+
export function applyProfileStats(rows, stats) {
|
|
132
|
+
const map = stats instanceof Map ? stats : new Map(Object.entries(stats ?? {}));
|
|
133
|
+
return rows.map((row) => ({
|
|
134
|
+
...row,
|
|
135
|
+
...(row.avgCost === undefined && map.get(row.id)?.avgCost !== null && map.get(row.id)?.avgCost !== undefined ? { avgCost: map.get(row.id).avgCost } : {}),
|
|
136
|
+
...(row.successRate === undefined && map.get(row.id)?.successRate !== null && map.get(row.id)?.successRate !== undefined ? { successRate: map.get(row.id).successRate } : {}),
|
|
137
|
+
...(map.get(row.id)?.n !== undefined ? { n: map.get(row.id).n } : {}),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
// `ctx.subagents.registerProvider({...})` 块逐字拆出。仅引用 lib + shims;
|
|
4
4
|
// 无 @deepseek-ai 依赖(shims 是唯一入口)。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// subagents
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// The factory returns whatever registerProvider returns (the caller keeps the
|
|
14
|
-
// original `if (typeof disposeProvider === 'function') ctx.effect(...)` shape).
|
|
6
|
+
// 注入:所有 apply 闭包 / ctx 依赖都是显式参数——
|
|
7
|
+
// subagents 本 provider 注册进的注册表(工厂内调用 subagents.registerProvider),
|
|
8
|
+
// store profile store(getAllowFailOpen 供 cost guard),
|
|
9
|
+
// getEnabled 读取 apply 闭包 `enabled` 标志(关时 start fail-loud),
|
|
10
|
+
// logger ctx.logger(决策级子会话日志)。
|
|
11
|
+
// 工厂返回 registerProvider 的返回值(调用方保持原有
|
|
12
|
+
// `if (typeof disposeProvider === 'function') ctx.effect(...)` 形状)。
|
|
15
13
|
//
|
|
16
14
|
// start 按预检段(runStartPreflight)/ 结算+取消接线段(wireChildLifecycle)
|
|
17
15
|
// 拆为模块级私有函数;start 内嵌 setup 按 ①-⑦ 步拆 setupChild +
|
|
@@ -40,26 +38,32 @@ import { DELEGATION_CONTEXT, buildDispatchMeta } from './delegation.mjs';
|
|
|
40
38
|
// agentOptions,一次性返回 start 后续段所需的全部状态。
|
|
41
39
|
async function runStartPreflight(request, deps) {
|
|
42
40
|
if (!deps.getEnabled()) {
|
|
43
|
-
throw new Error('dispatch:
|
|
41
|
+
throw new Error('dispatch: 插件已禁用(可在设置 → 子 Agent 方案 中重新启用)');
|
|
44
42
|
}
|
|
45
43
|
const profile = request.profile;
|
|
46
44
|
if (profile === undefined) {
|
|
47
|
-
throw new Error('dispatch: request.profile
|
|
45
|
+
throw new Error('dispatch: request.profile 缺失(dispatch 工具在启动子 Agent 前必须解析出 profile,请检查调用参数)');
|
|
48
46
|
}
|
|
49
47
|
const parent = request.parent;
|
|
50
48
|
// 同步捕获委派策略,在首个 await 之前——之后的父会话切换属于父的未来,
|
|
51
49
|
// 不属于本 child(shipped captureDelegatedPolicyOverrides)。
|
|
52
50
|
const delegated = captureDelegatedPolicyOverrides(parent);
|
|
53
|
-
// 权威 preset whitelist
|
|
54
|
-
|
|
51
|
+
// 权威 preset whitelist 检查(对照运行时名册)。逃生舱只叠加:开关开时
|
|
52
|
+
// getEscapeSet 返回放行集,否则空数组(零叠加)。
|
|
53
|
+
const whitelist = new Set(await resolveWhitelist(parent.ctx.get('agentPresets'), deps.getEscapeSet()));
|
|
55
54
|
if (typeof profile.preset === 'string' && profile.preset !== 'inherit' && !whitelist.has(profile.preset)) {
|
|
56
|
-
throw new Error(`dispatch:
|
|
55
|
+
throw new Error(`dispatch: 预设 "${profile.preset}" 不在 system-trust 白名单(可在设置页改用受信任预设)`);
|
|
56
|
+
}
|
|
57
|
+
// 放行审计(provider 渠道):成员经逃生舱放行时同样写高可见治理审计(「每个放行
|
|
58
|
+
// 都留痕」的绝对语义覆盖非 dispatch 渠道)。成本闸在本函数下方、交集在子会话
|
|
59
|
+
// setup 时执行——审计快照按 provider 渠道口径标注。
|
|
60
|
+
if (typeof deps.recordEscapeAllowProvider === 'function' && deps.getEscapeSet().includes(profile.preset)) {
|
|
61
|
+
deps.recordEscapeAllowProvider(parent, profile.preset);
|
|
57
62
|
}
|
|
58
63
|
// 权威 cost guard(运行时推导;硬上限始终生效,llm 能力核验由 allowFailOpen 门控;
|
|
59
64
|
// llm 目录读取走共享 catalog 快照)。
|
|
60
65
|
await assertCostGuard(parent, profile, deps.store.getAllowFailOpen(), deps.logger, deps.catalog);
|
|
61
|
-
//
|
|
62
|
-
// the child depth (parent floor + 1) and enforce the cap.
|
|
66
|
+
// 委派深度:官方助手——先断言上限值,再解析子深度(父底 + 1)并执行上限。
|
|
63
67
|
assertSubagentMaxDepth(profile.maxDepth);
|
|
64
68
|
const childDepth = resolveChildDepth(parent, profile.maxDepth);
|
|
65
69
|
const childId = randomUUID();
|
|
@@ -88,9 +92,8 @@ function buildProviderMeta(parent, profile, swapPreset, childDepth) {
|
|
|
88
92
|
});
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
// agentOptions
|
|
92
|
-
//
|
|
93
|
-
// child's own delegation depth.
|
|
95
|
+
// agentOptions:官方 resolveChildAgentOptions——父路由继承,除非 profile 覆盖
|
|
96
|
+
// provider/model/maxTokens,并盖上子自身的委派深度。
|
|
94
97
|
function buildProviderAgentOptions(parent, profile, childDepth) {
|
|
95
98
|
return resolveChildAgentOptions(parent, {
|
|
96
99
|
...(profile.provider !== undefined ? { provider: profile.provider } : {}),
|
|
@@ -103,7 +106,7 @@ function buildProviderAgentOptions(parent, profile, childDepth) {
|
|
|
103
106
|
async function startProvider(request, deps) {
|
|
104
107
|
const { parent, profile, delegated, childId, swapPreset, meta, agentOptions } = await runStartPreflight(request, deps);
|
|
105
108
|
if (request.signal !== undefined && request.signal.aborted) {
|
|
106
|
-
throw new Error('dispatch:
|
|
109
|
+
throw new Error('dispatch: 子 Agent 请求在发布前已被取消(可重试一次派发)');
|
|
107
110
|
}
|
|
108
111
|
const handle = await parent.ctx.agents.create({
|
|
109
112
|
sessionId: childId,
|
|
@@ -118,12 +121,12 @@ async function startProvider(request, deps) {
|
|
|
118
121
|
// --- start 内嵌 setup(childCtx):按 ①-⑦ 步装配子 Agent 会话(从 start 拆出)-------
|
|
119
122
|
|
|
120
123
|
async function setupChild(childCtx, { parent, profile, swapPreset, delegated, request }) {
|
|
121
|
-
// ①
|
|
122
|
-
//
|
|
124
|
+
// ① 预设组合:显式 swap 挂载目标预设;否则从父组合。无名册 + 显式 swap
|
|
125
|
+
// fail-loud。
|
|
123
126
|
const childPresets = childCtx.get('agentPresets');
|
|
124
127
|
if (swapPreset) {
|
|
125
128
|
if (childPresets === undefined) {
|
|
126
|
-
throw new Error('dispatch:
|
|
129
|
+
throw new Error('dispatch: 无法在无预设名册的部署中切换预设(该部署不支持自定义预设挂载,可省略 preset 以继承父预设)');
|
|
127
130
|
}
|
|
128
131
|
await childPresets.mount(childCtx, profile.preset);
|
|
129
132
|
} else if (childPresets !== undefined) {
|
|
@@ -131,12 +134,12 @@ async function setupChild(childCtx, { parent, profile, swapPreset, delegated, re
|
|
|
131
134
|
}
|
|
132
135
|
// ② Tool intersection(safety gate 1,实现见 restrictChildTools)。
|
|
133
136
|
restrictChildTools(childCtx, parent, profile);
|
|
134
|
-
// ③
|
|
137
|
+
// ③ 委派范围声明(systemPrompt 可用时)。
|
|
135
138
|
const systemPrompt = childCtx.get('systemPrompt');
|
|
136
139
|
if (systemPrompt !== undefined) {
|
|
137
140
|
systemPrompt.context({ name: 'subagent:delegation', order: 120, text: DELEGATION_CONTEXT });
|
|
138
141
|
}
|
|
139
|
-
// ④ Persona
|
|
142
|
+
// ④ Persona 影子段(覆盖 order 0 的 deployment:persona)。双防线 prefix。
|
|
140
143
|
if (profile.persona !== undefined && systemPrompt !== undefined) {
|
|
141
144
|
systemPrompt.section({
|
|
142
145
|
name: 'deployment:persona',
|
|
@@ -144,30 +147,30 @@ async function setupChild(childCtx, { parent, profile, swapPreset, delegated, re
|
|
|
144
147
|
text: profile.persona.length > 0 ? `${GUIDANCE_PREFIX}${profile.persona}` : profile.persona,
|
|
145
148
|
});
|
|
146
149
|
}
|
|
147
|
-
// ⑤
|
|
150
|
+
// ⑤ 推理档位注入每个子请求。
|
|
148
151
|
if (profile.reasoningEffort !== undefined) {
|
|
149
152
|
childCtx.on('agent/request', async (_payload, next) => {
|
|
150
153
|
const resolved = await next();
|
|
151
154
|
return { ...resolved, reasoningEffort: profile.reasoningEffort };
|
|
152
155
|
});
|
|
153
156
|
}
|
|
154
|
-
// ⑥ Descriptor append inside the child's first turn
|
|
157
|
+
// ⑥ Descriptor append inside the child's first turn(descriptor 存在才 append;
|
|
158
|
+
// 核心注入的是对象(snapshotJsonValue 快照),早期版本为字符串,故只判存在不判类型)。
|
|
155
159
|
let appended = false;
|
|
156
160
|
childCtx.on('agent/pre-step', async ({ agent }, next) => {
|
|
157
161
|
const decision = await next();
|
|
158
162
|
if (!appended && decision.kind === 'enter') {
|
|
159
163
|
appended = true;
|
|
160
|
-
agent.session.append('subagent/descriptor', request.descriptor);
|
|
164
|
+
if (request.descriptor !== undefined && request.descriptor !== null) agent.session.append('subagent/descriptor', request.descriptor);
|
|
161
165
|
}
|
|
162
166
|
return decision;
|
|
163
167
|
});
|
|
164
|
-
// ⑦
|
|
168
|
+
// ⑦ 委派策略追加(官方助手)。
|
|
165
169
|
appendDelegatedPolicyOverrides(childCtx.agent.session, delegated);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
// ②
|
|
169
|
-
//
|
|
170
|
-
// lib/core/intersection.mjs: computeEffectiveAllow).
|
|
172
|
+
// ② 工具交集(安全门 1):父集 ∩ 子集,减去 run_code、减去 deny,存在 allow
|
|
173
|
+
// 时再收窄(纯核心在 lib/core/intersection.mjs:computeEffectiveAllow)。
|
|
171
174
|
function restrictChildTools(childCtx, parent, profile) {
|
|
172
175
|
const parentNames = new Set(parent.ctx.tools.schemas(parent).map((schema) => schema.name));
|
|
173
176
|
const childNames = childCtx.tools.schemas(childCtx.agent).map((schema) => schema.name);
|
|
@@ -175,14 +178,14 @@ function restrictChildTools(childCtx, parent, profile) {
|
|
|
175
178
|
// shipped restrict 不对 allow:[] throw——在此 fail-loud,让空交集显式化
|
|
176
179
|
// (throw ⇒ setupAndPublish 回滚创建)。
|
|
177
180
|
if (effective.length === 0) {
|
|
178
|
-
throw new Error('dispatch:
|
|
181
|
+
throw new Error('dispatch: 子工具交集为空(父工具集与子工具集无交集,或 toolFilter 过滤后为空,可放宽过滤器后重试)');
|
|
179
182
|
}
|
|
180
|
-
// restrict
|
|
181
|
-
//
|
|
183
|
+
// restrict 对未知/scope-local/保留 allow 集抛错:包成干净错误再 rethrow,
|
|
184
|
+
// 触发创建回滚。
|
|
182
185
|
try {
|
|
183
186
|
childCtx.tools.restrict({ allow: effective });
|
|
184
187
|
} catch (error) {
|
|
185
|
-
throw new Error(`dispatch:
|
|
188
|
+
throw new Error(`dispatch: 子工具限制失败(未能按交集限制子工具集:${error instanceof Error ? error.message : String(error)}),可检查 toolFilter 是否含未知工具`, { cause: error });
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -210,7 +213,7 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
|
|
|
210
213
|
await child.whenIdle();
|
|
211
214
|
}
|
|
212
215
|
const settled = readResult(child, boundary, flags.cancelled);
|
|
213
|
-
//
|
|
216
|
+
// 决策级日志在结果结算时(readResult,返回前)。
|
|
214
217
|
logger.info('[dsh-subagent-profile] child:', JSON.stringify({ childId, preset: profile.preset ?? 'inherit', swapPreset, stopReason: settled.stopReason }));
|
|
215
218
|
return settled;
|
|
216
219
|
} finally {
|
|
@@ -230,8 +233,8 @@ function wireChildLifecycle(handle, request, childId, swapPreset, profile, logge
|
|
|
230
233
|
};
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
export function createProfileProvider({ subagents, store, getEnabled, logger, catalog }) {
|
|
234
|
-
const deps = { store, getEnabled, logger, catalog };
|
|
236
|
+
export function createProfileProvider({ subagents, store, getEnabled, logger, catalog, getEscapeSet }) {
|
|
237
|
+
const deps = { store, getEnabled, logger, catalog, getEscapeSet };
|
|
235
238
|
return subagents.registerProvider({
|
|
236
239
|
name: 'profile',
|
|
237
240
|
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
|