@twinsearth/dsh-master 3.3.3 → 3.3.5

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 (57) hide show
  1. package/README.md +17 -0
  2. package/client.js +126 -1
  3. package/cordis.patch.yml +51 -0
  4. package/dist-host/agents/pjm-agent.js +62 -0
  5. package/dist-host/agents/pm-agent.js +57 -0
  6. package/dist-host/agents/prompts.js +71 -0
  7. package/dist-host/agents/toolkit.js +64 -0
  8. package/dist-host/channel/abort-registry.js +93 -0
  9. package/dist-host/channel/protocol.js +114 -0
  10. package/dist-host/channel/server.js +349 -0
  11. package/dist-host/channel/stream-state.js +343 -0
  12. package/dist-host/channel/ws.js +199 -0
  13. package/dist-host/dual/documents.js +143 -0
  14. package/dist-host/dual/orchestrator.js +214 -0
  15. package/dist-host/dual/schema.js +367 -0
  16. package/dist-host/engine/atomic-switch.js +159 -0
  17. package/dist-host/engine/health-probe.js +86 -0
  18. package/dist-host/engine/index.js +18 -0
  19. package/dist-host/engine/llm-stream.js +215 -0
  20. package/dist-host/engine/plugin-updater.js +392 -0
  21. package/dist-host/engine/safe-update.js +150 -0
  22. package/dist-host/index.js +154 -3
  23. package/dist-host/kernel.js +27 -1
  24. package/dist-host/plugins/cost-optimizer.js +3 -3
  25. package/dist-host/plugins/dual-manager.js +374 -77
  26. package/docs/AGENTS-PM-PJM.md +207 -0
  27. package/docs/ARCHITECTURE.md +2 -0
  28. package/docs/CHANGELOG.md +74 -0
  29. package/docs/CHANNEL-WEBSOCKET.md +169 -0
  30. package/docs/DESIGN-command-center.md +147 -0
  31. package/docs/ENGINE-PRIMITIVES.md +215 -0
  32. package/docs/GAP-ANALYSIS-fang-session.md +75 -0
  33. package/docs/REVIEW-PM-PJM-3.3.4.md +179 -0
  34. package/docs/examples/command-center/README.md +167 -0
  35. package/package.json +5 -1
  36. package/src/agents/pjm-agent.ts +62 -0
  37. package/src/agents/pm-agent.ts +58 -0
  38. package/src/agents/prompts.ts +74 -0
  39. package/src/agents/toolkit.ts +64 -0
  40. package/src/channel/abort-registry.ts +85 -0
  41. package/src/channel/protocol.ts +110 -0
  42. package/src/channel/server.ts +289 -0
  43. package/src/channel/stream-state.ts +349 -0
  44. package/src/channel/ws.ts +182 -0
  45. package/src/dual/documents.ts +142 -0
  46. package/src/dual/orchestrator.ts +223 -0
  47. package/src/dual/schema.ts +355 -0
  48. package/src/engine/atomic-switch.ts +146 -0
  49. package/src/engine/health-probe.ts +84 -0
  50. package/src/engine/index.ts +20 -0
  51. package/src/engine/llm-stream.ts +189 -0
  52. package/src/engine/plugin-updater.ts +359 -0
  53. package/src/engine/safe-update.ts +148 -0
  54. package/src/index.ts +143 -4
  55. package/src/kernel.ts +28 -1
  56. package/src/plugins/cost-optimizer.ts +3 -3
  57. package/src/plugins/dual-manager.ts +337 -76
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 dsh-Master contributors — see LICENSE-CORE.
3
+ // @ts-nocheck
4
+
5
+ /**
6
+ * 快照 → 更新 → 冒烟 → 失败回滚(PluginSafeUpdater 的可编译实现)。
7
+ *
8
+ * 关键设计:**不假装有 VM**。执行能力由调用方注入一个 `executor`:
9
+ *
10
+ * executor.snapshot(name) → 建快照
11
+ * executor.restore(name) → 回滚到快照
12
+ * executor.run(argv, options) → 跑一条命令,返回 { ok, stdout, stderr, code }
13
+ *
14
+ * 房主本身没有 vm-sandbox;真实接入要么由瓮城的 VM 层提供 executor,
15
+ * 要么由宿主进程管理器提供。这里只保证**顺序、门禁与回滚语义**正确。
16
+ */
17
+
18
+ function asString(value) { return String(value ?? '').trim() }
19
+
20
+ function executorOf(input) {
21
+ const executor = input && typeof input === 'object' ? input.executor : null
22
+ if (!executor || typeof executor.snapshot !== 'function' || typeof executor.restore !== 'function' || typeof executor.run !== 'function') {
23
+ return null
24
+ }
25
+ return executor
26
+ }
27
+
28
+ /**
29
+ * @param {{
30
+ * executor?: {snapshot:Function, restore:Function, run:Function},
31
+ * snapshotName?: string,
32
+ * update?: string[], // 更新命令 argv
33
+ * smoke?: string[], // 冒烟命令 argv(可选)
34
+ * restart?: string[], // 回滚后重启命令 argv(可选)
35
+ * now?: () => number,
36
+ * }} input
37
+ */
38
+ export async function runSafeUpdate(input = {}) {
39
+ const executor = executorOf(input)
40
+ if (!executor) {
41
+ return { ok: false, code: 'executor-required', error: 'an executor with snapshot/restore/run is required; no VM is assumed' }
42
+ }
43
+ const now = typeof input.now === 'function' ? input.now : () => Date.now()
44
+ const snapshotName = asString(input.snapshotName) || `pre-update-${now()}`
45
+ const steps = []
46
+ const record = (step, detail) => { steps.push({ step, at: now(), ...detail }) }
47
+
48
+ // 1. 快照是硬前置:拿不到快照就不允许更新,否则失败时无法回滚。
49
+ try {
50
+ const snapshot = await executor.snapshot(snapshotName)
51
+ if (snapshot && snapshot.ok === false) {
52
+ record('snapshot', { ok: false, detail: snapshot })
53
+ return { ok: false, code: 'snapshot-failed', error: 'snapshot failed; refusing to update', snapshotName, steps }
54
+ }
55
+ record('snapshot', { ok: true })
56
+ } catch (error) {
57
+ record('snapshot', { ok: false, error: String(error?.message ?? error) })
58
+ return { ok: false, code: 'snapshot-threw', error: String(error?.message ?? error), snapshotName, steps }
59
+ }
60
+
61
+ const rollback = async (reason, detail) => {
62
+ record('rollback-start', { reason })
63
+ let restored = null
64
+ try { restored = await executor.restore(snapshotName) } catch (error) { restored = { ok: false, error: String(error?.message ?? error) } }
65
+ record('restore', { ok: restored?.ok !== false, detail: restored })
66
+ if (Array.isArray(input.restart) && input.restart.length > 0) {
67
+ try {
68
+ const restarted = await executor.run(input.restart, { phase: 'restart' })
69
+ record('restart', { ok: restarted?.ok !== false, detail: restarted })
70
+ } catch (error) {
71
+ record('restart', { ok: false, error: String(error?.message ?? error) })
72
+ }
73
+ }
74
+ return { ok: false, code: reason, error: detail, snapshotName, steps, rolledBack: restored?.ok !== false }
75
+ }
76
+
77
+ // 2. 更新
78
+ const update = Array.isArray(input.update) && input.update.length > 0 ? input.update : null
79
+ if (!update) return await rollback('update-command-required', 'update argv is required')
80
+ try {
81
+ const updated = await executor.run(update, { phase: 'update' })
82
+ if (updated && updated.ok === false) {
83
+ record('update', { ok: false, detail: updated })
84
+ return await rollback('update-failed', 'update command failed')
85
+ }
86
+ record('update', { ok: true })
87
+ } catch (error) {
88
+ record('update', { ok: false, error: String(error?.message ?? error) })
89
+ return await rollback('update-threw', String(error?.message ?? error))
90
+ }
91
+
92
+ // 3. 冒烟(可选但推荐):不通过同样回滚,绝不“更新成功但起不来”。
93
+ if (Array.isArray(input.smoke) && input.smoke.length > 0) {
94
+ try {
95
+ const smoke = await executor.run(input.smoke, { phase: 'smoke' })
96
+ if (smoke && smoke.ok === false) {
97
+ record('smoke', { ok: false, detail: smoke })
98
+ return await rollback('smoke-failed', 'smoke test failed after update')
99
+ }
100
+ record('smoke', { ok: true })
101
+ } catch (error) {
102
+ record('smoke', { ok: false, error: String(error?.message ?? error) })
103
+ return await rollback('smoke-threw', String(error?.message ?? error))
104
+ }
105
+ }
106
+
107
+ return { ok: true, code: 'updated', snapshotName, steps, rolledBack: false }
108
+ }
109
+
110
+ /**
111
+ * 组合使用:切指针 + 健康检查 + 失败回滚。适合蓝绿切换后的验证。
112
+ * 指针切换成功但健康检查失败时,把指针切回旧目标。
113
+ */
114
+ export async function switchAndVerify(input = {}) {
115
+ const { readPointer, switchWithRollback } = await import('./atomic-switch.ts')
116
+ const { probeHealth } = await import('./health-probe.ts')
117
+ // 先记住旧目标:健康检查失败时要切回它,而不是切到“刚切过去的新目标”。
118
+ const previous = await readPointer({ linkPath: input.linkPath, fs: input.fs })
119
+ const switched = await switchWithRollback({
120
+ linkPath: input.linkPath,
121
+ targetDir: input.targetDir,
122
+ fs: input.fs,
123
+ platform: input.platform,
124
+ now: input.now,
125
+ })
126
+ if (!switched.ok) return { ok: false, code: switched.code, error: switched.error, switch: switched, health: null, previous }
127
+ const health = await probeHealth({
128
+ url: input.healthUrl,
129
+ fetchImpl: input.fetchImpl,
130
+ timeoutMs: input.timeoutMs,
131
+ intervalMs: input.intervalMs,
132
+ maxAttempts: input.maxAttempts,
133
+ now: input.now,
134
+ sleepImpl: input.sleepImpl,
135
+ })
136
+ if (health.ok) return { ok: true, code: 'switched-and-healthy', switch: switched, health, previous }
137
+ if (!previous.ok || !previous.resolved) {
138
+ return { ok: false, code: 'health-failed-after-switch', switch: switched, health, previous, rollback: null }
139
+ }
140
+ const back = await switchWithRollback({
141
+ linkPath: input.linkPath,
142
+ targetDir: previous.resolved,
143
+ fs: input.fs,
144
+ platform: input.platform,
145
+ now: input.now,
146
+ }).catch((error) => ({ ok: false, error: String(error?.message ?? error) }))
147
+ return { ok: false, code: 'health-failed-after-switch', switch: switched, health, previous, rollback: back }
148
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import {
23
+ AGENT_CATALOG,
23
24
  FANG_CONTRACT_MIN,
24
25
  FANG_SERVICE,
25
26
  MASTER_VERSION,
@@ -37,14 +38,19 @@ import {
37
38
  safeGet,
38
39
  tokenEquals,
39
40
  } from './kernel.ts'
41
+ import { randomBytes } from 'node:crypto'
42
+
40
43
  import { BRIDGE_PATH, filterEnv } from './superagent/dispatch.ts'
41
44
  import { createBridgeTokenManager } from './superagent/bridge-tokens.ts'
45
+ import { attachCommandCenterChannel, CHANNEL_PATH } from './channel/server.ts'
42
46
 
43
47
  export const name = 'dsh-master'
44
48
  export const inject = []
45
49
 
46
50
  /** 状态路由(Client UI 读取;只读、非敏感、loopback-only 由宿主 webServer 保证)。 */
47
51
  export const STATUS_ROUTE = '/dsh-master/api/status'
52
+ /** 3.3.5:智囊团只读快照(交接/计划/反馈/复核)。 */
53
+ export const DUAL_ROUTE = '/dsh-master/api/dual'
48
54
 
49
55
  /**
50
56
  * 房主核心 apply。
@@ -87,8 +93,7 @@ export function apply(ctx, config = {}) {
87
93
  }
88
94
  }
89
95
 
90
- const catalog = () => SUBPLUGIN_CATALOG.map((entry) => {
91
- const active = subplugins.get(entry.id) ?? null
96
+ const catalog = () => SUBPLUGIN_CATALOG.map((entry) => { const active = subplugins.get(entry.id) ?? null
92
97
  const allowed = roleAllowed(entry.row, role)
93
98
  return {
94
99
  id: entry.id,
@@ -106,8 +111,97 @@ export function apply(ctx, config = {}) {
106
111
  }
107
112
  })
108
113
 
109
- const service = {
110
- id: SERVICE.master,
114
+ /**
115
+ * 3.3.4:两个智能体的**可选择性**(不是“已激活”)。
116
+ * preset 的激活由 agentPresets 服务在会话创建时决定,这里只如实报告宿主是否有该服务。
117
+ */
118
+ const agentCards = () => {
119
+ const presets = safeGet(ctx, 'agentPresets')
120
+ const available = Boolean(presets) && typeof presets?.list === 'function'
121
+ return AGENT_CATALOG.map((entry) => ({
122
+ ...entry,
123
+ presetServiceAvailable: available,
124
+ selectable: available,
125
+ hint: available
126
+ ? '新会话创建时选择该 preset;已有会话不会被改变。'
127
+ : '宿主缺少 agentPresets 服务(DSH web bundle 才有),本 Host 无法选择该 preset。',
128
+ }))
129
+ }
130
+
131
+ /**
132
+ * 3.3.5:智囊团只读快照(面板数据源)。
133
+ * 只暴露“谁在等什么”,不暴露 token、不提供写入口;dual 未装配时给 available:false。
134
+ */
135
+ const dualSnapshot = () => {
136
+ const dual = safeGet(ctx, SERVICE.dual)
137
+ if (!dual || typeof dual.status !== 'function') {
138
+ return { ok: true, available: false, reason: 'dual-service-unavailable', updatedAt: Date.now(), handoffs: [], plans: [], feedbacks: [], reviews: [], loop: null }
139
+ }
140
+ const safe = (fn, fallback) => { try { return fn() } catch { return fallback } }
141
+ const handoffs = safe(() => dual.pm.listHandoffs().items, [])
142
+ const plans = safe(() => dual.pjm.listPlans().items, [])
143
+ const feedbacks = safe(() => dual.pjm.listFeedback().items, [])
144
+ const reviews = safe(() => dual.pm.listReviews().items, [])
145
+ return {
146
+ ok: true,
147
+ available: true,
148
+ updatedAt: Date.now(),
149
+ agents: { pm: 'master-pm', pjm: 'master-pjm' },
150
+ loop: safe(() => dual.status().loop, null),
151
+ handoffs: handoffs.map((item) => ({
152
+ id: item.id,
153
+ prdId: item.prdId,
154
+ title: item.title,
155
+ status: item.status,
156
+ reason: item.reason ?? null,
157
+ openQuestions: (item.openQuestions ?? []).slice(0, 10),
158
+ acceptance: (item.acceptance ?? []).slice(0, 10),
159
+ planId: item.planId ?? null,
160
+ preparedAt: item.preparedAt ?? null,
161
+ })),
162
+ plans: plans.map((plan) => {
163
+ const progress = safe(() => dual.pjm.progress(plan.id).progress, null)
164
+ const lastReport = Array.isArray(plan.reports) && plan.reports.length > 0 ? plan.reports.at(-1) : null
165
+ return {
166
+ id: plan.id,
167
+ prdId: plan.prdId,
168
+ title: plan.title,
169
+ status: plan.status,
170
+ handoffId: plan.handoffId ?? null,
171
+ progress,
172
+ lastReport: lastReport ? { status: lastReport.status, summary: lastReport.summary, blocker: lastReport.blocker ?? null, at: lastReport.at } : null,
173
+ pmReview: plan.pmReview ?? null,
174
+ feedbackId: plan.feedbackId ?? null,
175
+ }
176
+ }),
177
+ feedbacks: feedbacks.map((item) => ({
178
+ id: item.id,
179
+ planId: item.planId,
180
+ prdId: item.prdId ?? null,
181
+ createdAt: item.createdAt ?? null,
182
+ summary: String(item.summary ?? '').slice(0, 500),
183
+ metrics: item.metrics ?? null,
184
+ openIssues: (item.openIssues ?? []).slice(0, 10),
185
+ })),
186
+ reviews: reviews.map((review) => ({
187
+ id: review.id,
188
+ feedbackId: review.feedbackId,
189
+ planId: review.planId,
190
+ verdict: review.verdict,
191
+ createdAt: review.createdAt ?? null,
192
+ failed: (review.acceptanceChecks ?? []).filter((check) => check.met !== true).map((check) => check.criterion),
193
+ })),
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 智能体通道 token(3.3.5):每次加载随机生成;只读 status 暴露给同源面板。
199
+ * 跨站页面由 Origin 校验挡住;token 只是纵深防御,不是唯一门槛。
200
+ */
201
+ const channelToken = String(config.channelToken ?? '').trim() || randomBytes(24).toString('hex')
202
+ const safeOrigin = (fn) => { try { return String(fn() ?? '') } catch { return '' } }
203
+
204
+ const service = { id: SERVICE.master,
111
205
  name: 'dsh-master',
112
206
  version: MASTER_VERSION,
113
207
  role,
@@ -151,6 +245,27 @@ export function apply(ctx, config = {}) {
151
245
  uptimeMs: Date.now() - startedAt,
152
246
  cooperation: cooperation(),
153
247
  subplugins: catalog(),
248
+ /** 3.3.4:PM / PJM 智能体(Agent preset)清单;与子插件行分开报告。 */
249
+ agents: agentCards(),
250
+ /**
251
+ * 3.3.4:智囊团只读摘要。dual-manager 未装配时给 null —— 不构造空壳假装成功。
252
+ */
253
+ dual: (() => {
254
+ const dual = safeGet(ctx, SERVICE.dual)
255
+ if (!dual || typeof dual.status !== 'function') return null
256
+ try {
257
+ const snapshot = dual.status()
258
+ return {
259
+ ok: snapshot.ok === true,
260
+ agents: snapshot.agents ?? null,
261
+ pm: snapshot.pm ?? null,
262
+ pjm: snapshot.pjm ?? null,
263
+ cooperation: snapshot.cooperation ?? null,
264
+ }
265
+ } catch { return null }
266
+ })(),
267
+ /** 3.3.5:智囊团 WebSocket 通道(同源面板用;chat 目前返回具名缺口)。 */
268
+ channel: { path: CHANNEL_PATH, token: channelToken },
154
269
  roleMatrix: ROLE_MATRIX,
155
270
  bridge: {
156
271
  enabled: Boolean(String(config.bridgeToken ?? '').trim()) || bridgeTokens.describe().tokens.length > 0,
@@ -198,6 +313,30 @@ export function apply(ctx, config = {}) {
198
313
  } catch (error) {
199
314
  logWarn(`[dsh-master] status route registration failed: ${String(error?.message ?? error)}`)
200
315
  }
316
+ // v3.3.5:智囊团只读数据源(交接 / 计划 / 反馈 / 复核)。无写口、无 token。
317
+ try {
318
+ webServer.register({
319
+ kind: 'exact',
320
+ path: DUAL_ROUTE,
321
+ method: 'GET',
322
+ handler: async (req, res) => jsonResponse(res, 200, dualSnapshot()),
323
+ })
324
+ } catch (error) {
325
+ logWarn(`[dsh-master] dual route registration failed: ${String(error?.message ?? error)}`)
326
+ }
327
+ // v3.3.5:智囊团 WebSocket 通道(双向;当前只推快照,chat 返回具名缺口)。
328
+ try {
329
+ const channel = attachCommandCenterChannel({
330
+ webServer,
331
+ token: channelToken,
332
+ dualSnapshot,
333
+ allowedOrigin: safeOrigin(() => webServer.getOrigin?.()),
334
+ logger: { info: logWarn, warn: logWarn, error: logWarn },
335
+ })
336
+ logWarn(`[dsh-master] command-center channel: ${channel.attached ? channel.path : `not attached (${channel.reason})`}`)
337
+ } catch (error) {
338
+ logWarn(`[dsh-master] command-center channel failed: ${String(error?.message ?? error)}`)
339
+ }
201
340
  // v3.2.3:原身 Router → 母体 forker 的跨进程派发桥。
202
341
  try {
203
342
  webServer.register({
package/src/kernel.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import crypto from 'node:crypto'
12
12
 
13
- export const MASTER_VERSION = '3.3.3'
13
+ export const MASTER_VERSION = '3.3.5'
14
14
  export const FANG_CONTRACT_MIN = '3.1.14'
15
15
 
16
16
  export const SERVICE = Object.freeze({
@@ -63,6 +63,33 @@ export const ROW = Object.freeze({
63
63
  dual: 'master-dual-manager',
64
64
  })
65
65
 
66
+ /**
67
+ * 3.3.4:两个可被新会话选择的智能体(Agent preset,不是子插件行)。
68
+ *
69
+ * 与 ROLE_MATRIX 的区别:preset 决定“这个会话是谁”,角色矩阵决定“这一行在哪个角色里装配”。
70
+ * 两者都不改变对方;智能体自身不授予任何执行权限,工具边界写在 cordis.patch.yml 的 preset 里。
71
+ */
72
+ export const AGENT_CATALOG = Object.freeze([
73
+ Object.freeze({
74
+ id: 'master-pm',
75
+ preset: 'master-pm',
76
+ name: '产品经理(PM)',
77
+ nameEn: 'Product Manager',
78
+ responsibilities: ['需求澄清与选择题请示', 'WHY/WHAT 与 PRD', '六类现状盘点(证据驱动)', '交接给 PJM', '复核/复盘/总结'],
79
+ tools: ['pm_create_prd', 'pm_create_assessment', 'pm_write_prd', 'pm_list_prds', 'pm_handoff_to_pjm', 'pm_review_feedback', 'pm_pipeline', 'pm_next_action'],
80
+ boundary: '只读代码与文档 + 向用户提问 + 写 PRD/盘点/复核;不执行、不派发、不发布。',
81
+ }),
82
+ Object.freeze({
83
+ id: 'master-pjm',
84
+ preset: 'master-pjm',
85
+ name: '项目经理(PJM)',
86
+ nameEn: 'Project Manager',
87
+ responsibilities: ['WBS 与七类项目管理', '接单与隔离派发', '节点自反馈', '测试/交付/验收与结案'],
88
+ tools: ['pjm_create_plan', 'pjm_take_handoff', 'pjm_dispatch_step', 'pjm_complete_step', 'pjm_project_status', 'pjm_check_step', 'pjm_report', 'pjm_submit_feedback', 'pjm_write_project', 'pjm_close_project', 'pjm_next_action'],
89
+ boundary: '只读代码 + 计划/派发/反馈;派发必须经 fangForker.spawn,无 forker 即 fail-closed;不替 PM 决定需求。',
90
+ }),
91
+ ])
92
+
66
93
  export const ROLE = Object.freeze({ mother: 'mother', essence: 'essence', fork: 'fork' })
67
94
 
68
95
  /**
@@ -117,7 +117,7 @@ export function apply(ctx, config = {}) {
117
117
 
118
118
  const service = {
119
119
  id: SERVICE.cost,
120
- version: '3.3.3',
120
+ version: '3.3.5',
121
121
  role,
122
122
  get mode() {
123
123
  return system.policy.describe().mode
@@ -354,7 +354,7 @@ export function apply(ctx, config = {}) {
354
354
  return {
355
355
  ok: true,
356
356
  id: 'cost-optimizer',
357
- version: '3.3.3',
357
+ version: '3.3.5',
358
358
  role,
359
359
  mode: current.mode,
360
360
  requestedMode: current.requestedMode,
@@ -396,7 +396,7 @@ export function apply(ctx, config = {}) {
396
396
  doubleCountGuard: report.doubleCountGuard,
397
397
  }
398
398
  })(),
399
- note: '3.3.3:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1.1 真实投递,回执计数修复);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认);3.3.3 起角色矩阵母体全开、分身挂双主管。',
399
+ note: '3.3.5:系统层记账/预算/灰度/审计 + A1 工具输出紧凑化(默认 shadow)+ 瓮城回流出口(contract 1.1.1 真实投递,回执计数修复);工具面 A2–A3 与决策复用 B1–B4 已接入(shadow 默认);3.3.3 起角色矩阵母体全开、分身挂双主管;3.3.4 起内置 PM/PJM 两个智能体 preset;3.3.5 起补齐引擎原语、可中断 LLM 流、WS 通道与流式管线。',
400
400
  }
401
401
  },
402
402
  }