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.
Files changed (40) hide show
  1. package/README.md +77 -40
  2. package/README.zh.md +111 -74
  3. package/docs/screenshots/dispatch-card.png +0 -0
  4. package/docs/screenshots/settings-page1.png +0 -0
  5. package/docs/screenshots/settings-page2.png +0 -0
  6. package/index.mjs +276 -81
  7. package/lib/client.js +3218 -166
  8. package/lib/core/adoption-reminder.mjs +48 -0
  9. package/lib/core/adoption-tracker.mjs +430 -0
  10. package/lib/core/background-ledger.mjs +71 -0
  11. package/lib/core/catalog-cache.mjs +45 -7
  12. package/lib/core/catalog.mjs +6 -6
  13. package/lib/core/cost-evidence.mjs +145 -0
  14. package/lib/core/cost-guard.mjs +71 -44
  15. package/lib/core/decision-trace.mjs +413 -0
  16. package/lib/core/delegation.mjs +111 -50
  17. package/lib/core/dispatch-gates.mjs +153 -0
  18. package/lib/core/dispatch-guard.mjs +156 -0
  19. package/lib/core/dispatch-schema.mjs +103 -14
  20. package/lib/core/dispatch-tool.mjs +220 -204
  21. package/lib/core/draft-gates.mjs +45 -0
  22. package/lib/core/drafts-store.mjs +45 -0
  23. package/lib/core/escape.mjs +130 -0
  24. package/lib/core/evolution-advice.mjs +224 -0
  25. package/lib/core/evolution-ledger.mjs +300 -0
  26. package/lib/core/evolution-summary.mjs +255 -0
  27. package/lib/core/http-routes.mjs +256 -72
  28. package/lib/core/intersection.mjs +6 -9
  29. package/lib/core/presets-sync.mjs +161 -43
  30. package/lib/core/prices.mjs +46 -0
  31. package/lib/core/profile-directory.mjs +139 -0
  32. package/lib/core/profile-provider.mjs +42 -39
  33. package/lib/core/profiles-store.mjs +103 -76
  34. package/lib/core/pure.mjs +110 -66
  35. package/lib/core/reminder-store.mjs +172 -0
  36. package/lib/core/shims.mjs +67 -76
  37. package/lib/core/whitelist.mjs +23 -17
  38. package/package.json +82 -83
  39. package/presets/orchestrator/agent.cordis.yml +59 -87
  40. package/presets/orchestrator/NOTICE +0 -3
@@ -0,0 +1,172 @@
1
+ // lib/core/reminder-store.mjs — 提醒系统 host 侧 store + 审计同源。
2
+ // 一条提醒 = 一条治理审计记录(经 audit 钩子写入 governance-audit.jsonl,与 [6] 审计
3
+ // 同源);每个用户动作(采纳/忽略/拒绝/关闭)与「标记已读」都追加审计留痕。P0 三要件:
4
+ // severity 上色(P0 由 client 用错误色渲染)、常驻至已读(unread 标记,绝不自动消失)、
5
+ // 带 session 标识(sessionId 字段;全局异常显式 null,client 标注「全局」)。
6
+ // 隐私红线:只落结构字段(severity/kind/title/detail/sessionId/时间戳)与「未采纳」
7
+ // 条目的展示字段(子会话 id/模式、prompt 摘要 ≤200 字符、结算摘要结构、估算成本)。
8
+ // prompt 摘要不存原文,结算摘要无子输出正文;其余提醒类不携带这些字段(null)。
9
+ // 落盘 subagent-evolution/reminders.json(原子写 tmp+rename;损坏/形状不符
10
+ // fail-soft 从空起步)。import-free(仅 node 内置),可被裸 CI 单测。
11
+
12
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ const SEVERITIES = new Set(['P0', 'P1', 'P2']);
16
+ const MAX_REMINDERS = 200;
17
+
18
+ function isRecordShape(rec) {
19
+ return rec !== null && typeof rec === 'object' && typeof rec.id === 'string'
20
+ && typeof rec.severity === 'string' && typeof rec.kind === 'string'
21
+ && typeof rec.title === 'string' && typeof rec.detail === 'string';
22
+ }
23
+
24
+ // 结算摘要形状守卫(宽松):只接受非数组对象,具体字段逐个归一。
25
+ function isSettleShape(value) {
26
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
27
+ }
28
+
29
+ function normalizeSettled(value) {
30
+ if (!isSettleShape(value)) return null;
31
+ return {
32
+ stopReason: typeof value.stopReason === 'string' ? value.stopReason : '',
33
+ elapsedMs: Number.isFinite(value.elapsedMs) ? value.elapsedMs : null,
34
+ ...(Number.isFinite(value.childTotalTokens) ? { childTotalTokens: value.childTotalTokens } : {}),
35
+ ...(isSettleShape(value.childUsage) ? { childUsage: value.childUsage } : {}),
36
+ ...(typeof value.childSessionId === 'string' && value.childSessionId !== '' ? { childSessionId: value.childSessionId } : {}),
37
+ };
38
+ }
39
+
40
+ function loadState(stateFile, state, warn) {
41
+ if (stateFile === undefined) return;
42
+ try {
43
+ if (!existsSync(stateFile)) return;
44
+ const parsed = JSON.parse(readFileSync(stateFile, 'utf8'));
45
+ if (parsed === null || typeof parsed !== 'object' || parsed.v !== 1 || !Array.isArray(parsed.reminders)) {
46
+ warn('reminder store: 提醒文件版本或形状不符,从空状态起步');
47
+ return;
48
+ }
49
+ for (const raw of parsed.reminders) {
50
+ if (!isRecordShape(raw)) continue;
51
+ state.reminders.push({
52
+ id: raw.id,
53
+ severity: SEVERITIES.has(raw.severity) ? raw.severity : 'P2',
54
+ kind: raw.kind,
55
+ title: raw.title,
56
+ detail: raw.detail,
57
+ sessionId: typeof raw.sessionId === 'string' && raw.sessionId !== '' ? raw.sessionId : null,
58
+ childId: typeof raw.childId === 'string' && raw.childId !== '' ? raw.childId : null,
59
+ childSessionId: typeof raw.childSessionId === 'string' && raw.childSessionId !== '' ? raw.childSessionId : null,
60
+ childMode: typeof raw.childMode === 'string' && raw.childMode !== '' ? raw.childMode : null,
61
+ promptExcerpt: typeof raw.promptExcerpt === 'string' ? raw.promptExcerpt : '',
62
+ settled: normalizeSettled(raw.settled),
63
+ costEstimated: Number.isFinite(raw.costEstimated) ? raw.costEstimated : null,
64
+ unread: raw.unread !== false,
65
+ createdAt: Number.isFinite(raw.createdAt) ? raw.createdAt : 0,
66
+ action: typeof raw.action === 'string' ? raw.action : null,
67
+ actedAt: Number.isFinite(raw.actedAt) ? raw.actedAt : null,
68
+ });
69
+ }
70
+ } catch (error) {
71
+ warn('reminder store: 提醒文件读取失败,从空状态起步(' + (error instanceof Error ? error.message : String(error)) + ')');
72
+ }
73
+ }
74
+
75
+ function persistState(stateFile, state, warn) {
76
+ if (stateFile === undefined) return;
77
+ const tmp = stateFile + '.tmp';
78
+ try {
79
+ mkdirSync(dirname(stateFile), { recursive: true });
80
+ writeFileSync(tmp, JSON.stringify({ v: 1, reminders: state.reminders }, null, 2), 'utf8');
81
+ renameSync(tmp, stateFile);
82
+ } catch (error) {
83
+ try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
84
+ warn('reminder store: 提醒文件写入失败(' + (error instanceof Error ? error.message : String(error)) + ')');
85
+ }
86
+ }
87
+
88
+ function auditReminder(audit, kind, reminder, action) {
89
+ audit({
90
+ kind,
91
+ reminderId: reminder.id,
92
+ severity: reminder.severity,
93
+ reminderKind: reminder.kind,
94
+ ...(reminder.sessionId !== null ? { sessionId: reminder.sessionId } : {}),
95
+ ...(action !== undefined ? { action } : {}),
96
+ });
97
+ }
98
+
99
+ // 产生一条提醒:severity 非法回退 P2;空 sessionId 归一 null(全局)。「未采纳」
100
+ // 条目额外携带子会话 id/模式、prompt 摘要(≤200 字符再截一次防超)、结算摘要与估算
101
+ // 成本;其余提醒类这些字段为空/null。创建即审计。
102
+ function recordReminder(state, persist, audit, { severity, kind, title, detail, sessionId, childId, childSessionId, childMode, promptExcerpt, settled, costEstimated }) {
103
+ const reminder = {
104
+ id: 'r-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
105
+ severity: SEVERITIES.has(severity) ? severity : 'P2',
106
+ kind: typeof kind === 'string' && kind !== '' ? kind : 'event',
107
+ title: typeof title === 'string' ? title : '',
108
+ detail: typeof detail === 'string' ? detail : '',
109
+ sessionId: typeof sessionId === 'string' && sessionId !== '' ? sessionId : null,
110
+ childId: typeof childId === 'string' && childId !== '' ? childId : null,
111
+ childSessionId: typeof childSessionId === 'string' && childSessionId !== '' ? childSessionId : null,
112
+ childMode: typeof childMode === 'string' && childMode !== '' ? childMode : null,
113
+ promptExcerpt: typeof promptExcerpt === 'string' ? promptExcerpt.slice(0, 200) : '',
114
+ settled: normalizeSettled(settled),
115
+ costEstimated: Number.isFinite(costEstimated) ? costEstimated : null,
116
+ unread: true,
117
+ createdAt: Date.now(),
118
+ action: null,
119
+ actedAt: null,
120
+ };
121
+ state.reminders.push(reminder);
122
+ if (state.reminders.length > state.maxReminders) state.reminders.shift();
123
+ persist();
124
+ auditReminder(audit, 'reminder-created', reminder);
125
+ return reminder;
126
+ }
127
+
128
+ // 标记已读(unread=false):P0 角标常驻至已读的「已读」动作,逐条留痕。
129
+ function ackReminders(state, persist, audit, ids) {
130
+ const list = Array.isArray(ids) ? ids : [];
131
+ const acked = [];
132
+ for (const reminder of state.reminders) {
133
+ if (reminder.unread !== true || !list.includes(reminder.id)) continue;
134
+ reminder.unread = false;
135
+ acked.push(reminder.id);
136
+ auditReminder(audit, 'reminder-acked', reminder);
137
+ }
138
+ if (acked.length > 0) persist();
139
+ return acked;
140
+ }
141
+
142
+ // 用户动作(采纳/忽略/拒绝/关闭):互斥终态 + 审计留痕(关闭留痕要求)。
143
+ function actOnReminder(state, persist, audit, id, action) {
144
+ const reminder = state.reminders.find((r) => r.id === id);
145
+ if (reminder === undefined) return false;
146
+ if (action !== 'adopt' && action !== 'ignore' && action !== 'reject' && action !== 'dismiss') return false;
147
+ reminder.action = action;
148
+ reminder.actedAt = Date.now();
149
+ reminder.unread = false;
150
+ persist();
151
+ auditReminder(audit, 'reminder-action', reminder, action);
152
+ return true;
153
+ }
154
+
155
+ function listReminders(state, unreadOnly) {
156
+ const all = [...state.reminders].reverse();
157
+ return unreadOnly ? all.filter((r) => r.unread === true) : all;
158
+ }
159
+
160
+ export function createReminderStore({ dshHome, audit = () => {}, warn = () => {}, maxReminders = MAX_REMINDERS } = {}) {
161
+ const state = { reminders: [], maxReminders: Number.isFinite(maxReminders) && maxReminders > 0 ? maxReminders : MAX_REMINDERS };
162
+ const stateFile = dshHome !== undefined && dshHome !== '' ? join(dshHome, 'subagent-evolution', 'reminders.json') : undefined;
163
+ loadState(stateFile, state, warn);
164
+ const persist = () => persistState(stateFile, state, warn);
165
+ return {
166
+ record: (entry) => recordReminder(state, persist, audit, entry),
167
+ ack: (ids) => ackReminders(state, persist, audit, ids),
168
+ actOn: (id, action) => actOnReminder(state, persist, audit, id, action),
169
+ list: ({ unreadOnly = false } = {}) => listReminders(state, unreadOnly),
170
+ unreadCount: () => state.reminders.filter((r) => r.unread === true).length,
171
+ };
172
+ }
@@ -1,51 +1,42 @@
1
- // lib/core/shims.mjs — facade. The ONLY module that imports the
2
- // @deepseek-ai symbols index.mjs relies on, converging the previously
3
- // top-level-scattered import surface. Two failure classes:
1
+ // lib/core/shims.mjs — facade。唯一 import index.mjs 依赖的 @deepseek-ai
2
+ // 符号的模块,收敛此前散落在顶层的 import 面。两类失败:
4
3
  //
5
- // Guard-type (fail-loud, NO fallback): assertSubagentMaxDepth /
6
- // resolveChildDepth are STATICALLY imported and re-exported. A rename/removal
7
- // in the host rc makes this module and therefore index.mjs — fail at load
8
- // time with the ESM link error "The requested module ... does not provide an
9
- // export named '...'". That is exactly the loading-time isolation the spec
10
- // wants for the delegation-depth safety gates: no weakened reimplementation is
11
- // ever substituted, so a child can never be dispatched past MAX_DEPTH.
4
+ // 守卫型(fail-loud,无回退):assertSubagentMaxDepth / resolveChildDepth
5
+ // 是**静态** import 并再导出。宿主 rc 里改名/移除会让本模块——进而
6
+ // index.mjs——在加载时以 ESM 链接错误失败("The requested module ... does
7
+ // not provide an export named '...'")。这正是委派深度安全闸想要的加载时
8
+ // 隔离:绝不替换为弱化实现,子 Agent 永远不可能派发超过 MAX_DEPTH。
12
9
  //
13
- // Function-mapping (fail-soft + warn): foldConsumedWork / finalAssistantOutput
14
- // / createUserMessage / appendDelegatedPolicyOverrides /
15
- // captureDelegatedPolicyOverrides / resolveChildAgentOptions / defineTool are
16
- // loaded with a dynamic top-level-await import wrapped in try/catch. On an
17
- // import failure (or a renamed/absent named export) the module still loads, a
18
- // console.warn is emitted (module top level has no ctx, so no logger), and a
19
- // local functionally-equivalent (or safe-degraded) implementation is used.
10
+ // 函数映射型(fail-soft + warn):foldConsumedWork / finalAssistantOutput /
11
+ // createUserMessage / appendDelegatedPolicyOverrides /
12
+ // captureDelegatedPolicyOverrides / resolveChildAgentOptions / defineTool
13
+ // try/catch 包裹的动态顶层 await import 加载。导入失败(或命名导出被
14
+ // 改名/移除)时模块仍能加载,发出 console.warn(模块顶层没有 ctx,没有
15
+ // logger),并使用本地功能等价(或安全降级)实现。
20
16
  //
21
- // A dynamic import whose package loads but whose named export was renamed or
22
- // removed resolves the destructure to `undefined` WITHOUT throwing, so loadSoft
23
- // also verifies the value is a function before accepting it.
17
+ // 动态 import 的包能加载但命名导出被改名/移除时,解构结果为 `undefined`
18
+ // 且**不抛**,所以 loadSoft 还校验值是函数后才接受。
24
19
 
25
20
  import { randomUUID } from 'node:crypto';
26
21
  import { createRequire } from 'node:module';
27
- // Guard-type: static, fail-loud no fallback. Kept as the only two
28
- // static @deepseek-ai imports; a missing export aborts module load with a clear
29
- // error BEFORE apply can run, which is the isolation this class exists for.
22
+ // 守卫型:静态、fail-loud、无回退。只保留这两个静态 @deepseek-ai import;
23
+ // 导出缺失会在 apply 能运行之前就以明确错误中止模块加载——这正是本类存在的隔离。
30
24
  import { assertSubagentMaxDepth, resolveChildDepth } from '@deepseek-ai/dsh-subagent';
31
25
  import { toStopReason } from './pure.mjs';
32
26
 
33
- // warn: at module top level there is no ctx / logger, so degrade to console.warn.
34
- // The prefix keeps the source recognizable in a shared host log.
27
+ // warn:模块顶层没有 ctx / logger,降级用 console.warn
28
+ // 前缀让来源在共享宿主日志中可辨认。
35
29
  function warn(...parts) {
36
30
  console.warn('[dsh-subagent-profile]', ...parts);
37
31
  }
38
32
 
39
- // Load one function-mapping `@deepseek-ai` symbol, returning a local fallback when
40
- // the package or the named export is unavailable. `warnMessage` carries the
41
- // actionable user-facing text. `importer` is an injectable loader (defaults to
42
- // the dynamic `import` — see DYNAMIC_IMPORT) so tests / future loaders can
43
- // override the failure route (e.g. stub it to throw, or return a module object
44
- // missing the symbol). It is exported as a seam for that purpose.
33
+ // 加载一个函数映射型 `@deepseek-ai` 符号,包或命名导出不可用时返回本地回退。
34
+ // `warnMessage` 携带可操作的用户面向文案。`importer` 是可注入加载器(缺省为
35
+ // 动态 `import`——见 DYNAMIC_IMPORT),测试 / 未来加载器可覆盖失败路径
36
+ // (如 stub 成抛错,或返回缺该符号的模块对象)。为此导出为接缝。
45
37
  //
46
- // Note: `import` is a keyword and cannot be used as a value reference
47
- // (`importer = import` is a SyntaxError), so the default is a thin wrapper around
48
- // the dynamic import expression.
38
+ // 注意:`import` 是关键字,不能作值引用(`importer = import` SyntaxError),
39
+ // 故缺省值是动态 import 表达式的薄包装。
49
40
  const DYNAMIC_IMPORT = (specifier) => import(specifier);
50
41
  async function loadSoft(pkg, symbol, fallback, warnMessage, importer = DYNAMIC_IMPORT) {
51
42
  try {
@@ -60,11 +51,9 @@ async function loadSoft(pkg, symbol, fallback, warnMessage, importer = DYNAMIC_I
60
51
  }
61
52
  }
62
53
 
63
- // --- local fallbacks (import-free, duck-typed, functionally equivalent) ----
64
- // Each is the minimal local reimplementation that preserves the observable
65
- // contract of the shipped helper; these are exported via `__fallbacks` so tests
66
- // can exercise the degraded path even though the junction packages resolve
67
- // successfully here.
54
+ // --- 本地回退(import-freeduck-typed、功能等价)------------------------------
55
+ // 每个都是保留官方助手可观察契约的最小本地实现;经 `__fallbacks` 导出,测试在
56
+ // junction 包解析成功时也能走降级路径。
68
57
 
69
58
  // foldConsumedWork: **近似、非等价**——readResult 只读 `.end`(终止 turn/end 事件)来
70
59
  // 推导 stopReason。shipped fold 是精密的 stepped/claimed 状态机;本降级实现取最后一个
@@ -80,8 +69,8 @@ function foldConsumedWorkFallback(events) {
80
69
  return { ...(end === undefined ? {} : { end }), droppedUnrun: false };
81
70
  }
82
71
 
83
- // finalAssistantOutput: identical fold rule — last non-empty assistant/message,
84
- // else accumulated text-delta chunks, else undefined.
72
+ // finalAssistantOutput:相同的 fold 规则——最后一个非空 assistant/message
73
+ // 否则累加的 text-delta 分片,再否则 undefined
85
74
  function finalAssistantOutputFallback(events) {
86
75
  let message;
87
76
  const partial = [];
@@ -99,11 +88,10 @@ function finalAssistantOutputFallback(events) {
99
88
  return text.length > 0 ? [{ type: 'text', text }] : undefined;
100
89
  }
101
90
 
102
- // createUserMessage: build the user-role message the followup driver needs. The
103
- // shipped helper uses a branded MessageId + deepFreeze; the fallback provides the
104
- // same observable shape (role/content/id/source) with a fresh random id. Note:
105
- // the returned object is MUTABLE (no deepFreeze) — consumers must NOT rely on
106
- // the shipped deepFreeze immutability; treat it as a plain message object.
91
+ // createUserMessage:构造 followup 驱动需要的 user 角色消息。官方助手用品牌化
92
+ // MessageId + deepFreeze;回退提供相同可观察形状(role/content/id/source)加
93
+ // 新鲜随机 id。注意:返回对象是**可变**的(无 deepFreeze)——消费方不得依赖
94
+ // 官方 deepFreeze 的不可变性;把它当普通消息对象。
107
95
  function createUserMessageFallback(input) {
108
96
  return {
109
97
  ...input,
@@ -112,10 +100,9 @@ function createUserMessageFallback(input) {
112
100
  };
113
101
  }
114
102
 
115
- // appendDelegatedPolicyOverrides: the delegation policy is authored INTO the
116
- // child's log for reconstruction. This is also the security-relevant
117
- // "approval: never" pin, so it must NOT be a no-op — reproduce the append
118
- // faithfully to keep the child's own log reconstructable and the pin visible.
103
+ // appendDelegatedPolicyOverrides:委派策略写进子会话日志供重建。这也是安全
104
+ // 相关的「approval: never」钉,绝不能是 no-op——忠实复现追加,保持子会话自身
105
+ // 日志可重建、钉可见。
119
106
  function appendDelegatedPolicyOverridesFallback(childSession, overrides) {
120
107
  const o = overrides || {};
121
108
  if (o.sandboxMode !== undefined) {
@@ -126,9 +113,8 @@ function appendDelegatedPolicyOverridesFallback(childSession, overrides) {
126
113
  }
127
114
  }
128
115
 
129
- // captureDelegatedPolicyOverrides: the parent's explicit sandbox override (or
130
- // undefined when none) plus the approval pin 'never' when the parent has an
131
- // approval service. Optional chaining keeps a mock parent (tests) working.
116
+ // captureDelegatedPolicyOverrides:父的显式沙箱覆盖(无则 undefined)加父有
117
+ // approval 服务时的审批钉 'never'。可选链让 mock 父(测试)正常工作。
132
118
  function captureDelegatedPolicyOverridesFallback(parent) {
133
119
  return {
134
120
  sandboxMode: parent.ctx?.get?.('sandboxPolicy')?.overrideOf?.(parent.session),
@@ -136,10 +122,9 @@ function captureDelegatedPolicyOverridesFallback(parent) {
136
122
  };
137
123
  }
138
124
 
139
- // resolveChildAgentOptions: merge the parent's route (provider/model/maxTokens,
140
- // present-only) with the per-child overrides and stamp the child's own depth.
141
- // This must preserve the child-config semantics — a no-op here would silently
142
- // break delegation routing.
125
+ // resolveChildAgentOptions:合并父路由(provider/model/maxTokens,存在才带)
126
+ // per-child 覆盖,并盖上子自身的深度。必须保持子配置语义——no-op 会静默
127
+ // 破坏委派路由。
143
128
  function resolveChildAgentOptionsFallback(parent, requested, childDepth) {
144
129
  const parentProvider = parent.options?.provider;
145
130
  const parentModel = parent.options?.model;
@@ -153,19 +138,16 @@ function resolveChildAgentOptionsFallback(parent, requested, childDepth) {
153
138
  };
154
139
  }
155
140
 
156
- // defineTool: the dispatch tool cannot exist without dsh-tools. Fail-loud at the
157
- // point of use with a clear, actionable message — the module still LOADS, and
158
- // calling this during apply surfaces the exact missing-dependency story instead
159
- // of a cryptic module-not-found at import time(加载不崩溃,调用点才报错)。
141
+ // defineTool:没有 dsh-tools 就没有 dispatch 工具。在用途点 fail-loud 并给出
142
+ // 清晰可操作的文案——模块仍然加载,apply 期间调用它才暴露确切的缺失依赖
143
+ // (加载不崩溃,调用点才报错)。
160
144
  function defineToolFallback() {
161
145
  throw new Error('dsh-tools 缺失:dispatch 工具不可用');
162
146
  }
163
147
 
164
- // --- wire the function-mapping symbols (module top-level await) --------------
165
- // The seven loads are independent, so they run in parallel (Promise.all) to cut
166
- // module-load latency; each still degrades to its local fallback on failure. We
167
- // keep the real functions when the junction resolves, and the fallbacks when a
168
- // host rc renamed/removed a symbol.
148
+ // --- 接线函数映射型符号(模块顶层 await)---------------------------------------
149
+ // 七个加载相互独立,并行(Promise.all)以削减模块加载延迟;每个失败仍降级到
150
+ // 本地回退。junction 解析成功时保留真函数,宿主 rc 改名/移除符号时用回退。
169
151
  const [foldConsumedWork, finalAssistantOutput, createUserMessage, appendDelegatedPolicyOverrides, captureDelegatedPolicyOverrides, resolveChildAgentOptions, defineTool] = await Promise.all([
170
152
  loadSoft('@deepseek-ai/dsh-agent', 'foldConsumedWork', foldConsumedWorkFallback, 'dsh-agent 的 foldConsumedWork 不可用,结果裁切使用本地降级实现'),
171
153
  loadSoft('@deepseek-ai/dsh-subagent', 'finalAssistantOutput', finalAssistantOutputFallback, 'dsh-subagent 的 finalAssistantOutput 不可用,子结果选取使用本地降级实现'),
@@ -176,9 +158,9 @@ const [foldConsumedWork, finalAssistantOutput, createUserMessage, appendDelegate
176
158
  loadSoft('@deepseek-ai/dsh-tools', 'defineTool', defineToolFallback, 'dsh-tools 缺失:dispatch 工具不可用'),
177
159
  ]);
178
160
 
179
- // readResult: shipped shape. The terminal turn reason comes from foldConsumedWork;
180
- // the selected output comes from finalAssistantOutput (last non-empty assistant
181
- // message, else joined text-delta chunks, else undefined -> []).
161
+ // readResult:官方形状。终止 turn 的原因来自 foldConsumedWork;选取的输出来自
162
+ // finalAssistantOutput(最后一个非空 assistant/message,否则拼接的 text-delta
163
+ // 分片,再否则 undefined -> [])。
182
164
  function readResult(child, boundary, cancelled) {
183
165
  const own = child.session.events.slice(boundary);
184
166
  const end = foldConsumedWork(own).end;
@@ -187,7 +169,7 @@ function readResult(child, boundary, cancelled) {
187
169
  return { output: finalAssistantOutput(own) ?? [], stopReason };
188
170
  }
189
171
 
190
- // --- version detection (pure probe) ------------------------------------------
172
+ // --- 版本探测(纯探测)--------------------------------------------------------
191
173
  // 读取 @deepseek-ai 三包(subagent/agent/llm)的 package.json version,与
192
174
  // peerDependencies 范围(>=PEER_MIN <PEER_MAX)比对后产出中文 warnings。纯探测:
193
175
  // 只读 manifest、不 import 新符号、不触发副作用;每包独立 try/catch,失败记
@@ -209,6 +191,16 @@ function readPackageVersion(pkg) {
209
191
  }
210
192
  }
211
193
 
194
+ // 读本插件自身 package.json version(fail-soft 'unknown'),供维护区显示当前版本。
195
+ function readOwnPluginVersion() {
196
+ try {
197
+ const manifest = requirePkg('../../package.json');
198
+ return typeof manifest.version === 'string' && manifest.version !== '' ? manifest.version : 'unknown';
199
+ } catch {
200
+ return 'unknown';
201
+ }
202
+ }
203
+
212
204
  // 极简 semver 比较:major.minor.patch + 可选 `-预发布` 段(覆盖 peerDependencies
213
205
  // 范围判断所需)。预发布 < 正式版;预发布段逐段比较,纯数字段按数值、否则按字典序
214
206
  // (`0.1.0-rc.10` > `0.1.0-rc.6`)。
@@ -267,12 +259,11 @@ export function detectVersions(reader = readPackageVersion) {
267
259
  warnings.push(`@deepseek-ai/${pkg} 版本 ${version} 超出 peerDependencies 范围(>=${PEER_MIN} <${PEER_MAX}),派发行为可能与预期不符`);
268
260
  }
269
261
  }
270
- return { versions, warnings };
262
+ return { versions, warnings, pluginVersion: readOwnPluginVersion() };
271
263
  }
272
264
 
273
- // Test-only access to the local degraded implementations (package imports
274
- // resolve here, so the real functions win; __fallbacks lets a test exercise the
275
- // fail-soft path without deleting node_modules).
265
+ // 仅供测试访问本地降级实现(包导入在这里可解析,真函数胜出;`__fallbacks`
266
+ // 让测试无需删 node_modules 即可走 fail-soft 路径)。
276
267
  export const __fallbacks = {
277
268
  foldConsumedWork: foldConsumedWorkFallback,
278
269
  finalAssistantOutput: finalAssistantOutputFallback,
@@ -286,7 +277,7 @@ export const __fallbacks = {
286
277
  export {
287
278
  assertSubagentMaxDepth,
288
279
  resolveChildDepth,
289
- // ---- test seam / injectable loader (see loadSoft doc) ----
280
+ // ---- 测试接缝 / 可注入加载器(见 loadSoft 文档)----
290
281
  loadSoft,
291
282
  foldConsumedWork,
292
283
  finalAssistantOutput,
@@ -296,6 +287,6 @@ export {
296
287
  resolveChildAgentOptions,
297
288
  defineTool,
298
289
  readResult,
299
- // ---- version probe seam (see detectVersions doc) ----
290
+ // ---- 版本探测接缝(见 detectVersions 文档)----
300
291
  readPackageVersion,
301
292
  };
@@ -1,22 +1,28 @@
1
- // lib/core/whitelist.mjs — target-preset whitelist derived from the runtime
2
- // roster, not hard-coded: system-trust presets when agentPresets exists, else
3
- // the shipped fallback names. Moved verbatim from index.mjs;
4
- // import-free (no @deepseek-ai dependency).
1
+ // lib/core/whitelist.mjs — 目标预设白名单:从运行时名册推导,不硬编码——
2
+ // agentPresets 存在时取 system-trust 预设,否则用官方回退名单。从 index.mjs
3
+ // 逐字移出;import-free(无 @deepseek-ai 依赖)。
5
4
  //
6
- // Injection note: the original apply-closure version took `agentCtx` and read
7
- // `agentCtx.get('agentPresets')` itself. The caller now injects the service —
8
- // `resolveWhitelist(parent.ctx.get('agentPresets'))` so the module does not
9
- // depend on apply-closure state. `parent.ctx.get('agentPresets')` yields the
10
- // exact same value (service or undefined), so the fallback semantics are
11
- // unchanged. FALLBACK_WHITELIST is exported for the tests.
12
-
13
- // The target-preset whitelist is derived from the runtime roster, not
14
- // hard-coded: system-trust presets when agentPresets exists, else the
15
- // shipped fallback names.
5
+ // 注入说明:原 apply 闭包版本自带 `agentCtx` 并自己读
6
+ // `agentCtx.get('agentPresets')`。调用方现在注入服务——
7
+ // `resolveWhitelist(parent.ctx.get('agentPresets'))`——使模块不依赖 apply 闭包
8
+ // 状态。`parent.ctx.get('agentPresets')` 得到完全相同的结果(服务或 undefined),
9
+ // 回退语义不变。FALLBACK_WHITELIST 导出供测试用。
16
10
  export const FALLBACK_WHITELIST = ['standard', 'code', 'minimal'];
17
11
 
18
- export async function resolveWhitelist(agentPresets) {
19
- if (agentPresets === undefined) return FALLBACK_WHITELIST;
12
+ // 逃生舱叠加(可选):escapeSet 是非 system-trust 预设的显式放行集(Set 或数组,
13
+ // 由调用方在开关打开时传入)。只叠加——base 集原样保留、去重;逃生舱绝不替代或
14
+ // 缩减 base 集,也不升级 base 成员的语义(逃生舱成员仍属「非 system 放行」)。
15
+ // escapeSet 缺省/为空时零叠加(行为与旧单参调用完全一致)。
16
+ function withEscapeSet(base, escapeSet) {
17
+ if (escapeSet === undefined) return base;
18
+ const extras = [...escapeSet].filter((id) => typeof id === 'string' && id !== '');
19
+ if (extras.length === 0) return base;
20
+ return [...new Set([...base, ...extras])];
21
+ }
22
+
23
+ export async function resolveWhitelist(agentPresets, escapeSet) {
24
+ if (agentPresets === undefined) return withEscapeSet(FALLBACK_WHITELIST, escapeSet);
20
25
  const presets = await agentPresets.list();
21
- return (presets ?? []).filter((preset) => preset && preset.trust === 'system').map((preset) => preset.id);
26
+ const base = (presets ?? []).filter((preset) => preset && preset.trust === 'system').map((preset) => preset.id);
27
+ return withEscapeSet(base, escapeSet);
22
28
  }