@bolloon/bolloon-agent 0.4.33 → 0.5.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.
Files changed (143) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +493 -35
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/network-pulse.js +304 -18
  31. package/dist/agents/pi-sdk.js +75 -20
  32. package/dist/agents/run-store.js +18 -5
  33. package/dist/agents/skill-readiness.js +52 -10
  34. package/dist/agents/subagent-manager.js +209 -3
  35. package/dist/agents/task/task-runner.js +9 -3
  36. package/dist/agents/task-group.js +8 -51
  37. package/dist/agents/task-public-text.js +89 -0
  38. package/dist/cli/commands/index.js +1 -0
  39. package/dist/cli/commands/tasks.js +350 -2
  40. package/dist/cli/identity-command.js +177 -0
  41. package/dist/cli/setup-wizard.js +71 -0
  42. package/dist/cli/update-commands.js +150 -24
  43. package/dist/cli-entry.js +12 -2
  44. package/dist/electron-build/agents/contacts/chain.js +26 -6
  45. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  46. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  47. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  48. package/dist/electron-build/agents/external-events.js +34 -5
  49. package/dist/electron-build/agents/external-events.js.map +1 -1
  50. package/dist/electron-build/agents/goal-criteria.js +10 -3
  51. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  52. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  53. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  55. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  56. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  57. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  58. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  59. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  60. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  61. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  63. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  65. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  67. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  69. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  71. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  79. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  81. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  83. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  84. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  85. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  86. package/dist/electron-build/agents/goal-store.js +35 -1
  87. package/dist/electron-build/agents/goal-store.js.map +1 -1
  88. package/dist/electron-build/agents/network-pulse.js +307 -19
  89. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  90. package/dist/electron-build/agents/pi-sdk.js +74 -19
  91. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  92. package/dist/electron-build/agents/run-store.js +20 -5
  93. package/dist/electron-build/agents/run-store.js.map +1 -1
  94. package/dist/electron-build/agents/skill-readiness.js +52 -9
  95. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  96. package/dist/electron-build/agents/task/task-runner.js +8 -2
  97. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  98. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  99. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  100. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  101. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  102. package/dist/electron-build/utils/dual-source.js +471 -0
  103. package/dist/electron-build/utils/dual-source.js.map +1 -0
  104. package/dist/electron-build/utils/update-manager.js +331 -57
  105. package/dist/electron-build/utils/update-manager.js.map +1 -1
  106. package/dist/electron-build/utils/update-state.js +16 -1
  107. package/dist/electron-build/utils/update-state.js.map +1 -1
  108. package/dist/electron-build/utils/version-info.js +82 -2
  109. package/dist/electron-build/utils/version-info.js.map +1 -1
  110. package/dist/index.js +103 -12
  111. package/dist/ios/chain-explorer.js +617 -0
  112. package/dist/ios/explorer.html +163 -0
  113. package/dist/ios/index.html +96 -0
  114. package/dist/ios/mobile-agent.js +2 -2
  115. package/dist/ios/mobile-contacts.js +329 -0
  116. package/dist/ios/mobile-core.js +1379 -226
  117. package/dist/ios/mobile-privacy.js +185 -0
  118. package/dist/ios/mobile.css +19 -0
  119. package/dist/ios/mobile.html +96 -0
  120. package/dist/ios/mobile.js +706 -7
  121. package/dist/ios/routes-chain-index.js +108 -0
  122. package/dist/ios/routes-contacts.js +420 -0
  123. package/dist/ios/routes-onchain-trade.js +99 -0
  124. package/dist/ios/routes-x402-info.js +1 -0
  125. package/dist/ios/server.js +762 -0
  126. package/dist/ios/task-frame-server.js +211 -0
  127. package/dist/orbitdb/cid-database.js +81 -16
  128. package/dist/orbitdb/ipfs-node.js +31 -2
  129. package/dist/utils/dual-source.js +426 -0
  130. package/dist/utils/update-cli.js +16 -21
  131. package/dist/utils/update-health.js +35 -5
  132. package/dist/utils/update-manager.js +329 -57
  133. package/dist/utils/update-state.js +16 -1
  134. package/dist/utils/version-info.js +75 -1
  135. package/dist/web/mobile-contacts.js +44 -11
  136. package/dist/web/mobile-core.js +1197 -513
  137. package/dist/web/mobile-tasks.js +339 -0
  138. package/dist/web/mobile.css +19 -0
  139. package/dist/web/mobile.html +64 -0
  140. package/dist/web/mobile.js +531 -1
  141. package/dist/web/routes-mobile-tasks.js +353 -0
  142. package/dist/web/server.js +163 -4
  143. package/package.json +3 -1
@@ -12,7 +12,9 @@
12
12
  */
13
13
  import * as os from 'os';
14
14
  import { SkillsManager } from './skills-manager.js';
15
- import { readGoal, setContinuation, updateGoal, addEvidence, } from './goal-store.js';
15
+ import { readGoal, updateGoal, addEvidence, } from './goal-store.js';
16
+ // 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态与 continuation 变更只有一个漏斗
17
+ import { reduceGoalState } from './goal-state-reducer.js';
16
18
  import { recordDegradation } from './run-store.js';
17
19
  export function parseSkillSpecs(list = []) {
18
20
  const required = [];
@@ -109,15 +111,45 @@ export async function ensureGoalSkillsReady(goal, opts = {}) {
109
111
  /** 门禁不过 → 写清事实并把 Goal 交给人 (不启动 Run, 不伪造失败) */
110
112
  export async function blockGoalOnSkills(goalId, res) {
111
113
  const reason = res.reason || '技能未就绪';
112
- await setContinuation(goalId, {
113
- wakeReason: 'needs_human', autoContinue: false, needsExternal: undefined,
114
- skillReadiness: { ok: false, at: new Date().toISOString(), reason, missing: res.missing, drift: res.drift, degradations: res.degradations },
115
- });
116
- await updateGoal(goalId, { status: 'needs_human' }).catch(() => { });
117
- await addEvidence(goalId, [`技能门禁拦截: ${reason}`]).catch(() => { });
114
+ // 规则 ②: 状态与 continuation 都经 Goal reducer 写 (单一漏斗)
115
+ await reduceGoalState({
116
+ goalId,
117
+ intent: 'skill_gate_block',
118
+ now: new Date().toISOString(),
119
+ by: 'skill-readiness',
120
+ reason,
121
+ skillReadiness: {
122
+ ok: false, at: new Date().toISOString(), reason,
123
+ missing: res.missing, drift: res.drift, degradations: res.degradations,
124
+ },
125
+ }).catch(() => null);
118
126
  for (const d of res.degradations)
119
127
  await recordDegradation({ kind: 'observational', op: 'skill-readiness', message: d }).catch(() => { });
120
128
  }
129
+ /**
130
+ * 只**记事实**、不改状态: 把本地技能就绪检查的结论写进 `continuation.skillReadiness`。
131
+ *
132
+ * ★ 2026-09-25 (P5 验收修复): 飞轮裁决 = `delegate` 时本地技能门禁**不适用** (能力由子 Agent 凭合同
133
+ * 交付), 所以不能调 `blockGoalOnSkills` (那会把 Goal 打成 needs_human)。但"本地缺什么"必须仍然
134
+ * 可查 —— 原来只有 `blockGoalOnSkills` 写这个字段, 门禁一放行就没有任何记录了。
135
+ */
136
+ export async function recordSkillReadiness(goalId, res) {
137
+ // 规则 ②: continuation 也经 Goal reducer 写 (只记事实, 不改状态)
138
+ await reduceGoalState({
139
+ goalId,
140
+ intent: 'skill_gate_record',
141
+ now: new Date().toISOString(),
142
+ by: 'skill-readiness',
143
+ skillReadiness: {
144
+ ok: res.ok,
145
+ at: new Date().toISOString(),
146
+ reason: res.reason,
147
+ missing: res.missing,
148
+ drift: res.drift,
149
+ degradations: res.degradations,
150
+ },
151
+ }).catch(() => null);
152
+ }
121
153
  /** 人工批准技能升级: 重新冻结快照 (显式动作, 不隐式切换) */
122
154
  export async function approveSkillUpgrade(goalId, opts = {}) {
123
155
  const goal = await readGoal(goalId);
@@ -126,8 +158,18 @@ export async function approveSkillUpgrade(goalId, opts = {}) {
126
158
  const frozen = await freezeGoalSkills(goal, { ...opts, force: true });
127
159
  if (!frozen.ok)
128
160
  return { ok: false, reason: frozen.reason };
129
- await setContinuation(goalId, { skillReadiness: { ok: true, at: new Date().toISOString(), reason: '人工批准技能升级' }, wakeReason: 'active', autoContinue: true });
130
- await updateGoal(goalId, { status: 'active' }).catch(() => { });
131
- await addEvidence(goalId, [`技能升级已被人工批准: ${(frozen.snapshot || []).map((s) => `${s.name}@${s.version}`).join(', ')}`]).catch(() => { });
161
+ // ★ 规则 ⑥ (Skill 不许绕过 验证+快照+可回退 通道): 批准是**人**的动作, 但必须留下通道证据 ——
162
+ // 快照 (skillSnapshot, 上面 freezeGoalSkills 已写) + 版本 + 变更原因 + 可回退 (fromVersion 仍在
163
+ // Goal 上)。这里把"谁批的、为什么"连同新版本一起记进 Goal (走唯一漏斗, 规则 ②)。
164
+ const versions = (frozen.snapshot || []).map((s) => `${s.name}@${s.version}`);
165
+ const fromVersions = (goal.skillSnapshot || []).map((s) => `${s.name}@${s.version}`);
166
+ await reduceGoalState({
167
+ goalId,
168
+ intent: 'skill_upgrade_approved',
169
+ now: new Date().toISOString(),
170
+ by: 'human',
171
+ skillReadiness: { ok: true, at: new Date().toISOString(), reason: '人工批准技能升级' },
172
+ reason: `技能升级已被人工批准 (by=human; from=[${fromVersions.join(', ')}] → to=[${versions.join(', ')}]; 可回退: Goal.skillSnapshot 保留新版本快照, 内容哈希在 skills-manager 侧可核验)`,
173
+ }).catch(() => null);
132
174
  return { ok: true, snapshot: frozen.snapshot };
133
175
  }
@@ -1,5 +1,7 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
+ // 2026-09-25 (M3 接缝): 换人必须重签合同的判据只有一份 (纯函数, 零 I/O)
4
+ import { childMatchesContract } from './goal-flywheel/wiring/contract.js';
3
5
  const DEFAULT_STORAGE_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'agents');
4
6
  const DEFAULT_TASK_TIMEOUT_MS = 5 * 60 * 1000;
5
7
  const DEFAULT_MAX_CONCURRENT_TASKS = 10;
@@ -160,10 +162,36 @@ export class SubAgentManager {
160
162
  async assignTask(taskId, toAgentId) {
161
163
  const task = this.tasks.get(taskId);
162
164
  if (task) {
165
+ // 2026-09-25 (M3): 挂着工作合同的任务 **不许换人而不换合同**。
166
+ // 合同把 `childAgentId` 钉死了 (回报核验要求 report.childAgentId === contract.childAgentId),
167
+ // 把同一份合同指给另一个子 Agent ⇒ 新子的回报必然对不上合同, 合规判定整个失效。
168
+ // 换人的正确做法是**重新派遣** (父侧 signed 新合同) —— 本方法只维护指派关系, 无权签合同,
169
+ // 所以这里如实拒绝并留痕, 而不是"改了字段就当换成了"。
170
+ if (task.goalId && task.workId) {
171
+ const contract = (task.metadata?.workContract ?? null);
172
+ if (!contract) {
173
+ task.reassignRefused = {
174
+ code: 'contract_unreadable',
175
+ reason: `任务 ${taskId} 挂着 workId=${task.workId} 却读不到合同 → 无法判断新执行者是否与原合同一致, 拒绝换人 (先重签合同)`,
176
+ };
177
+ await this.saveTasks();
178
+ this.notifyTaskListeners(task);
179
+ return;
180
+ }
181
+ const same = childMatchesContract(contract, toAgentId);
182
+ if (!same.ok) {
183
+ task.reassignRefused = { code: 'requires_new_contract', reason: same.reason };
184
+ await this.saveTasks();
185
+ this.notifyTaskListeners(task);
186
+ return;
187
+ }
188
+ task.reassignRefused = null;
189
+ }
163
190
  task.assignedAgentId = toAgentId;
164
191
  task.toAgentId = toAgentId;
165
192
  task.status = 'assigned';
166
193
  task.assignedAt = new Date().toISOString();
194
+ await this.heartbeatContractedTask(task, task.assignedAt);
167
195
  await this.saveTasks();
168
196
  this.notifyTaskListeners(task);
169
197
  }
@@ -171,6 +199,37 @@ export class SubAgentManager {
171
199
  async updateTaskStatus(taskId, status, result, error) {
172
200
  const task = this.tasks.get(taskId);
173
201
  if (task) {
202
+ // 2026-09-25 (M3 接缝): 挂了合同的子任务, 任何状态更新都是"子还活着"的证据 → 记一次心跳。
203
+ // 这是"执行中统一监控"的输入面: 没有心跳, 阻塞巡检只能看见"卡住", 看不见"在动"。
204
+ await this.heartbeatContractedTask(task);
205
+ // 2026-09-25 (飞轮接线 P2): 挂着工作合同的任务, "完成" 必须过回报核验 ——
206
+ // 回一段漂亮话 (没有逐条判据证据) **不算完成**: 任务留在 in_progress, 原因写清楚,
207
+ // 而 Goal 那边的 pendingReports 保持 (父仍然在等这份回报)。
208
+ //
209
+ // 2026-09-25 (M3): 判据从 `task.workId && task.goalId` 收紧成 `task.goalId` ——
210
+ // "派前必须有合同"的**反面**: 目标语境下的任务若**没有**合同 (签发失败 / 旧数据),
211
+ // 它同样不许自称完成 —— 没有合同的回报不核验也不接受 (handleChildReport 的 no_contract)。
212
+ if (status === 'completed' && task.goalId) {
213
+ if (!task.workId) {
214
+ task.status = 'in_progress';
215
+ task.error = '回报不接受为完成: 这条任务在目标语境下**没有工作合同** '
216
+ + '(签合同失败或任务早于合同接线) —— 没有合同就没有判据, 无法核验, 不许当完成记账';
217
+ await this.saveTasks();
218
+ this.notifyTaskListeners(task);
219
+ return;
220
+ }
221
+ const verdict = await this.validateTaskReport(task, result ?? '');
222
+ if (!verdict.accepted) {
223
+ task.status = 'in_progress';
224
+ if (result)
225
+ task.result = result;
226
+ task.error = `回报核验不过, 不接受为完成: ${verdict.reason}`
227
+ + (verdict.missingEvidence.length ? ` [缺证据: ${verdict.missingEvidence.join('; ')}]` : '');
228
+ await this.saveTasks();
229
+ this.notifyTaskListeners(task);
230
+ return;
231
+ }
232
+ }
174
233
  task.status = status;
175
234
  if (result)
176
235
  task.result = result;
@@ -183,6 +242,31 @@ export class SubAgentManager {
183
242
  this.notifyTaskListeners(task);
184
243
  }
185
244
  }
245
+ /**
246
+ * 给挂着合同的任务记一次心跳 (P3 的输入面; 只有真写成盘才算数)。
247
+ *
248
+ * 为什么放在**真实派遣过程**里而不是让子 Agent 自己记得报: 心跳是"任务还活着"的事实,
249
+ * 而子 Agent 是最没有动力报心跳的一方 (它卡住了就不会报)。所以由派遣侧在每一次
250
+ * 真实状态变化 (派出去 / 换人 / 子更新状态) 时落一次 —— 没有这些落点, 阻塞巡检
251
+ * 只能看到"从来没心跳过", 分不清"卡住"与"刚派出"。
252
+ *
253
+ * 失败**不覆盖** `task.error` (那是子 Agent 的事实), 而是记进 `heartbeatError`:
254
+ * 心跳写不进去 = 监控会看到"无进展", 这件事必须留在任务上, 不许静默。
255
+ */
256
+ async heartbeatContractedTask(task, at) {
257
+ if (!task.goalId || !task.workId)
258
+ return;
259
+ const now = at ?? new Date().toISOString();
260
+ try {
261
+ const { recordWorkHeartbeat } = await import('./goal-flywheel-wiring.js');
262
+ await recordWorkHeartbeat(task.goalId, task.workId, now);
263
+ task.lastHeartbeatAt = now;
264
+ task.heartbeatError = null;
265
+ }
266
+ catch (e) {
267
+ task.heartbeatError = `心跳写入失败 (监控会看到"无进展"): ${String(e?.message || e).slice(0, 160)}`;
268
+ }
269
+ }
186
270
  async getTask(taskId) {
187
271
  return this.tasks.get(taskId);
188
272
  }
@@ -300,14 +384,136 @@ export class SubAgentManager {
300
384
  }
301
385
  }
302
386
  }
303
- async delegateTask(fromAgentId, taskDescription, requiredCapabilities, priority = 'normal', input) {
387
+ async delegateTask(fromAgentId, taskDescription, requiredCapabilities, priority = 'normal', input, contractOptions) {
304
388
  const agent = await this.findBestAgentForTask(requiredCapabilities, fromAgentId);
305
389
  if (!agent) {
306
390
  const task = await this.createTask('delegate', taskDescription.substring(0, 50), taskDescription, fromAgentId, undefined, priority, input);
307
- return { task, agent: undefined };
391
+ const workContract = await this.issueWorkContractFor(fromAgentId, task, requiredCapabilities, contractOptions);
392
+ // 派前必须有工作合同 (M3): 目标语境下签不出合同 → **(没有收件人时本来也派不出去)**, 但要如实标注
393
+ if (!workContract && contractOptions?.goalId) {
394
+ this.refuseDispatchWithoutContract(task, contractOptions?.goalId);
395
+ await this.saveTasks();
396
+ return { task, agent: undefined, workContract: undefined };
397
+ }
398
+ if (workContract)
399
+ await this.heartbeatContractedTask(task);
400
+ await this.saveTasks();
401
+ return { task, agent: undefined, workContract };
308
402
  }
309
403
  const task = await this.createTask('delegate', taskDescription.substring(0, 50), taskDescription, fromAgentId, agent.id, priority, input);
310
- return { task, agent };
404
+ // 2026-09-25 (P2): 派遣这件事**本身**就包含签合同 (不是事后再补一份文档);
405
+ // 有没有空闲子 Agent 不影响合同签发 (合同锁的是能力与判据, 不是某个人)。
406
+ //
407
+ // 2026-09-25 (M3): 签不出合同 ⇒ **不派出**。旧形状是"记个 error 照样把任务交出去",
408
+ // 那等于"派前必须有的合同"在失败路径上直接不存在 (子拿到一段任务描述就开始干,
409
+ // 回来只有一段文本, 父连判据都没有)。现在: 任务退回 `pending`、不绑执行者、
410
+ // 返回 `agent: undefined`, 让调用方**没有**可派遣的对象。
411
+ const workContract = await this.issueWorkContractFor(fromAgentId, task, requiredCapabilities, contractOptions);
412
+ if (!workContract && contractOptions?.goalId) {
413
+ this.refuseDispatchWithoutContract(task, contractOptions.goalId);
414
+ await this.saveTasks();
415
+ return { task, agent: undefined, workContract: undefined };
416
+ }
417
+ if (workContract)
418
+ await this.heartbeatContractedTask(task);
419
+ await this.saveTasks();
420
+ return { task, agent, workContract };
421
+ }
422
+ /**
423
+ * 目标语境下没有合同 ⇒ **不派出** (M3 的"派前必须有合同")。
424
+ *
425
+ * 落成事实: 任务退回 `pending` 且**不绑执行者** (没有 `assignedAt`),
426
+ * `contractRefused` 记下原因。于是:
427
+ * · 调用方拿到的 `agent` 是 `undefined` → 没有派遣对象;
428
+ * · 任务不会出现在 `getActiveTasks()` 里 (它没被派出去);
429
+ * · 原因留在任务上, 父能据此重签合同或交人 (不许静默当"没派过")。
430
+ */
431
+ refuseDispatchWithoutContract(task, goalId) {
432
+ const reason = task.error
433
+ ? `派前必须有工作合同: 合同没签成 → 不派出 (${task.error})`
434
+ : '派前必须有工作合同: 合同没签成 → 不派出';
435
+ task.status = 'pending';
436
+ task.assignedAgentId = undefined;
437
+ task.toAgentId = undefined;
438
+ task.assignedAt = undefined;
439
+ // Goal 上下文要留在任务上: ① 它说明这次派遣是**为哪个目标**尝试的;
440
+ // ② 于是完成门 (`updateTaskStatus` 的 `task.goalId` 判据) 也管得到这条没有合同的任务
441
+ // —— 没有合同就不许自称完成, 不留 goalId 的话它就绕过了那道门。
442
+ if (goalId)
443
+ task.goalId = goalId;
444
+ task.contractRefused = { code: 'contract_not_issued', reason };
445
+ }
446
+ /**
447
+ * 给这次派遣签工作合同 (P2)。只有调用方给了 `goalId` 才签 —— 没有长期目标就没有合同上下文,
448
+ * 这时行为与接线前完全一致 (只记任务)。
449
+ */
450
+ async issueWorkContractFor(fromAgentId, task, requiredCapabilities, opts) {
451
+ if (!opts?.goalId)
452
+ return undefined;
453
+ try {
454
+ const { dispatchChildWork } = await import('./goal-flywheel-wiring.js');
455
+ const capability = (requiredCapabilities[0] || 'general').trim();
456
+ const issued = await dispatchChildWork({
457
+ goalId: opts.goalId,
458
+ parentRunId: opts.parentRunId ?? `subagent-task:${task.id}`,
459
+ childAgentId: task.assignedAgentId || task.toAgentId || `(unassigned:${fromAgentId})`,
460
+ capability,
461
+ objective: task.description,
462
+ inputs: { taskId: task.id, input: task.input ?? null },
463
+ allowedTools: opts.allowedTools ?? [],
464
+ budget: opts.budget ?? { maxSteps: null, maxDurationMs: this.config.taskTimeoutMs, maxAmount: null, currency: null },
465
+ deadline: opts.deadline ?? null,
466
+ successCriteria: (opts.successCriteria?.length ? opts.successCriteria : [task.description]).slice(0, 20),
467
+ issuedBy: `subagent-manager:${fromAgentId}`,
468
+ });
469
+ task.goalId = opts.goalId;
470
+ task.workId = issued.workId;
471
+ task.metadata = { ...(task.metadata ?? {}), workContract: issued };
472
+ await this.saveTasks();
473
+ return issued;
474
+ }
475
+ catch (err) {
476
+ // 签合同失败 → 如实记在任务上 (不许悄悄降级成"没合同也能派")
477
+ task.error = `工作合同签发失败: ${String(err?.message || err).slice(0, 200)}`;
478
+ await this.saveTasks();
479
+ return undefined;
480
+ }
481
+ }
482
+ /**
483
+ * 核验挂着合同的子任务回报 (P2)。
484
+ * 报告可以是**结构化 JSON 文本** (推荐: 子按 `bolloon-work-report/1` 回), 也可以是纯文本 ——
485
+ * 纯文本没有逐条证据 → **不接受为完成** (这正是要堵的"回一段漂亮话就算完成")。
486
+ */
487
+ async validateTaskReport(task, result) {
488
+ const { handleChildReport } = await import('./goal-flywheel-wiring.js');
489
+ const contract = (task.metadata?.workContract ?? null);
490
+ const raw = String(result ?? '');
491
+ let report = null;
492
+ try {
493
+ const parsed = JSON.parse(raw);
494
+ if (parsed && typeof parsed === 'object' && typeof parsed.status === 'string') {
495
+ report = parsed;
496
+ }
497
+ }
498
+ catch { /* 不是 JSON → 当纯文本处理 */ }
499
+ if (!report) {
500
+ report = {
501
+ workId: task.workId,
502
+ childAgentId: contract?.childAgentId ?? task.assignedAgentId ?? task.toAgentId ?? '(unknown)',
503
+ status: 'completed',
504
+ summary: raw.slice(0, 400),
505
+ evidence: [],
506
+ artifacts: [],
507
+ checks: [],
508
+ unresolvedItems: [],
509
+ blockReason: null,
510
+ nextRecommendation: '(未提供)',
511
+ durationMs: 0,
512
+ reportedAt: new Date().toISOString(),
513
+ };
514
+ }
515
+ const out = await handleChildReport({ goalId: task.goalId, workId: task.workId, report });
516
+ return { accepted: out.accepted, reason: out.reason, missingEvidence: out.missingEvidence };
311
517
  }
312
518
  async consultAgent(fromAgentId, toAgentId, query, taskId) {
313
519
  return this.sendMessage(fromAgentId, toAgentId, query, 'query', taskId);
@@ -17,13 +17,15 @@ import * as os from 'os';
17
17
  import * as path from 'path';
18
18
  import * as fs from 'fs';
19
19
  import * as crypto from 'crypto';
20
- import { createGoal, attachRun, markCriterion, completeGoalIfEligible, readGoal, setCriteria, } from '../goal-store.js';
20
+ import { createGoal, attachRun, markCriterion, readGoal, setCriteria, } from '../goal-store.js';
21
21
  import { startRun, recordStep, finishRun, readRun } from '../run-store.js';
22
22
  import { listTransactions, updateTransaction, setTransactionStatus } from '../x402/transaction-store.js';
23
23
  import { bridgeTransactionToRunGoal } from '../x402/goal-run-bridge.js';
24
24
  import { buyInfoAsTransaction } from '../x402/trade.js';
25
25
  import { publishInfo, listInfo } from '../x402/paid-info-store.js';
26
26
  import { planTransactionRecovery } from '../x402/payment-recovery.js';
27
+ // 2026-09-25 (M0 接线冻结): CLI 的 Run 终止也必须过唯一责任链 (规则 ③④)
28
+ import * as wiring from '../goal-flywheel-wiring.js';
27
29
  import { executeContractSkill, validateResourceOutput, verifyInstallFidelity, loadResourceContract } from '../x402/resource-contract.js';
28
30
  import { adviseResource } from './resource-advisor.js';
29
31
  import { resolveTaskBudget, checkPurchaseAllowed, previewPurchaseImpact } from './task-budget.js';
@@ -318,6 +320,9 @@ export async function runTask(opts) {
318
320
  const fail = async (stage, note, cardOpts, runStatus = 'needs_human') => {
319
321
  mark(stage, note);
320
322
  await finishRun(run.runId, { status: runStatus, summary: note.slice(0, 200) });
323
+ // ★ 2026-09-25 (M0 接线冻结, 规则 ④): CLI 任务的每条失败出口也是 Run 的终止 →
324
+ // 同样过收尾漏斗 (旧写法只 finishRun, 于是这条路径上没有 Memory / Skill 候选 / 权威 continuation)。
325
+ await wiring.closeTaskRun(run.runId, goal.goalId, note, 'cli');
321
326
  const card = buildReportCard({ ...cardOpts, evidenceRef: { goalId: goal.goalId, runId: run.runId, transactionId: cardOpts.evidenceRef?.transactionId }, budgetLines: [...budget.why, ...(cardOpts.budgetLines || [])] });
322
327
  return { ok: false, card, text: renderReportCard(card), goalId: goal.goalId, runId: run.runId, transactionId: card.evidenceRef.transactionId, stages, budget, outputIssues: cardOpts.blocker ? [cardOpts.blocker] : [] };
323
328
  };
@@ -554,8 +559,9 @@ export async function runTask(opts) {
554
559
  summary: evidenceOk ? `任务完成: ${concl.conclusion || '已产出'}` : '未达标 (证据/契约不完整)',
555
560
  evidence: [`bridge: step=${bridge.stepWritten} evidence=${bridge.evidenceWritten} goal=${bridge.goalEvidenceWritten}`],
556
561
  });
557
- if (evidenceOk)
558
- await completeGoalIfEligible(goal.goalId);
562
+ // ★ M0 (规则 ③④): 成功出口也走同一个收尾漏斗 —— 完不完成由飞轮决策 + 完成门判,
563
+ // 不再在这里直接调 completeGoalIfEligible (那是一条绕过 closeRun 的旧收尾路径)。
564
+ await wiring.closeTaskRun(run.runId, goal.goalId, card.conclusion || card.status, 'cli');
559
565
  mark('report', card.status);
560
566
  return {
561
567
  ok: card.status === '已完成',
@@ -40,6 +40,10 @@ import * as os from 'os';
40
40
  import * as path from 'path';
41
41
  import * as crypto from 'crypto';
42
42
  import { parseGroupLink, listGroups, groupSend, groupMessages } from './gateway-group.js';
43
+ // 2026-09-25: 隐私红线规则表抽到 task-public-text.ts (手机端与桌面端**同一份**)
44
+ import { PRIVACY_RULES, NODE_IDENTITY_RULES, scanPublicText, requirePublicText, scanNodeIdentity, mask } from './task-public-text.js';
45
+ // 保持对外 API 不变 (老调用方/测试继续 from './task-group.js' 拿这些)
46
+ export { PRIVACY_RULES, NODE_IDENTITY_RULES, scanPublicText, requirePublicText, scanNodeIdentity, mask };
43
47
  // ── 常量 ────────────────────────────────────────────────────────────────────
44
48
  /** 消息标记 (一行一条; 解析只认这一行) */
45
49
  export const TRAIL_TAG = '[bolloon-task]';
@@ -55,56 +59,9 @@ export const TRAIL_KIND_LABEL = {
55
59
  final: '终审结论',
56
60
  };
57
61
  export const VERDICTS = ['accept', 'reject', 'unknown'];
58
- /**
59
- * 群消息**禁止出现**的形态 (顺序即报告顺序)。宁可多拦 (拒发 + 说清哪条规则), 也不放行一条带标识符的消息。
60
- *
61
- * 实现纪律 (2026-09-23 踩过的坑, 不可回退):
62
- * · **不要用 `\b` 当边界** —— 消息里的 token 会把空白折成 `_`, 而 `_` 也是 \w, 于是 `判据见_0x1111…`
63
- * 这种"粘在 `_`/汉字后面"的标识符 `\b` 判不出来, 会直接漏进群。一律改成对标识符自身字母表的
64
- * lookahead/lookbehind (`(?<![0-9a-fA-FxX])`, `(?![0-9a-fA-F])`), 粘在 `_`/中文/符号后面也照样命中。
65
- * · ipv6 规则要**避开 ISO 时间戳** (`09:50:12.345Z` 只有两个冒号): 只认 3 组以上 `x:y:z:w` 或含 `::` 的压缩形态。
66
- * · 内容哈希 (sha256 64 位裸 hex) **不在**禁列 —— 交付就是要贴哈希 (它不是密钥, 也没有 0x 前缀)。
67
- */
68
- export const PRIVACY_RULES = [
69
- { rule: 'wallet-address', why: '以太坊/EVM 钱包地址', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/ },
70
- { rule: 'private-key-hex', why: '32 字节私钥形态 (0x + 64 hex)', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{64}(?![0-9a-fA-F])/ },
71
- { rule: 'hex-0x-blob', why: '0x 开头的十六进制块 (签名/哈希/密钥都长这样, 群里一律不放原文)', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{8,}(?![0-9a-fA-F])/ },
72
- { rule: 'did', why: '去中心化身份 DID', re: /did:[a-z0-9]+:[A-Za-z0-9._:%-]{3,}/i },
73
- { rule: 'peer-id', why: 'libp2p peerId (12D3Koo…/Qm…)', re: /12D3Koo[A-Za-z0-9]{8,}|Qm[1-9A-HJ-NP-Za-km-z]{30,}/ },
74
- { rule: 'multiaddr', why: 'libp2p multiaddr 或 store 地址 (/ip4 /tcp /p2p /orbitdb …)', re: /\/(ip4|ip6|dns|dns4|dns6|tcp|udp|ws|wss|quic|p2p|p2p-circuit|ipfs|orbitdb)(?![A-Za-z0-9])/i },
75
- { rule: 'orbitdb-link', why: 'OrbitDB 链接 (群邀请链接本身也不进群消息)', re: /orbitdb:\/\//i },
76
- { rule: 'ipv4', why: 'IPv4 地址', re: /(?<!\d)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?!\d)/ },
77
- { rule: 'ipv6', why: 'IPv6 地址 (不含 ISO 时间戳)', re: /(?<![0-9A-Za-z])(?:[0-9a-fA-F]{1,4})?::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}(?![0-9A-Za-z])|(?<![0-9A-Za-z:])(?:[0-9a-fA-F]{1,4}:){3,}[0-9a-fA-F]{1,4}(?![0-9A-Za-z])/ },
78
- { rule: 'pem', why: 'PEM 材料 (私钥/证书)', re: /-----BEGIN [A-Z ]*(PRIVATE KEY|CERTIFICATE)/ },
79
- { rule: 'secret-words', why: '私钥/助记词/密钥字样 (正文里提到即停, 人不该把密钥写进群)', re: /(private\s*key|seed\s*phrase|mnemonic|私钥|助记词|密钥材料)/i },
80
- { rule: 'url', why: 'http(s) 端点 (端点会带 host/IP 与用途指纹)', re: /https?:\/\/\S+/i },
81
- { rule: 'email', why: '邮箱地址', re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
82
- ];
83
- function mask(s) {
84
- const t = String(s || '');
85
- return t.length <= 4 ? '****' : `${t.slice(0, 4)}…`;
86
- }
87
- /** 扫一遍文本, 返回命中的隐私规则 (空数组 = 可以发) */
88
- export function scanPublicText(text) {
89
- const t = String(text ?? '');
90
- const out = [];
91
- const seen = new Set();
92
- for (const r of PRIVACY_RULES) {
93
- const m = r.re.exec(t);
94
- if (!m)
95
- continue;
96
- if (seen.has(r.rule))
97
- continue;
98
- seen.add(r.rule);
99
- out.push({ rule: r.rule, why: r.why, masked: mask(String(m[0])), chars: String(m[0]).length });
100
- }
101
- return out;
102
- }
103
- /** 发送前唯一闸门: 命中任何规则 → 拒绝发送 (返回 violations 供 CLI 如实报出) */
104
- export function requirePublicText(text) {
105
- const v = scanPublicText(text);
106
- return v.length === 0 ? { ok: true } : { ok: false, violations: v };
107
- }
62
+ // ── 隐私守卫 ────────────────────────────────────────────────────────────────
63
+ // 规则表与扫描函数本身在 `task-public-text.ts` (单一来源, 手机/桌面共用);
64
+ // 本文件只负责**发送前调用它** (见 sendTrailMessage) 与在群管理输出上再过一遍 (scanNodeIdentity)。
108
65
  // ── 发送者标记 (稳定假名, 原始 DID 不入群) ──────────────────────────────────
109
66
  const sha256Hex = (s) => crypto.createHash('sha256').update(s, 'utf8').digest('hex');
110
67
  /** DID → 稳定假名 `agent-<8位>` (同一个人每次都是同一个标记, 但看不出是谁) */
@@ -147,7 +104,7 @@ export async function resolveSenderTag(explicit) {
147
104
  return {
148
105
  ok: false, code: 'INVALID_ARGUMENT',
149
106
  message: '没有本机身份 (~/.bolloon/identity.json) 也没有 --from → 给不出诚实的发送者标记 (拒, 不假造一个)',
150
- detail: { identityFile: '~/.bolloon/identity.json', accepted: ['--from 委托方', 'bolloon setup'] },
107
+ detail: { identityFile: '~/.bolloon/identity.json', accepted: ['--from 委托方', 'bolloon identity init', 'bolloon setup'] },
151
108
  };
152
109
  }
153
110
  /** 与 gateway-group 内部 `groupIdOf` 同款派生 (它没导出; 这里只作地址比不中时的兜底) */
@@ -0,0 +1,89 @@
1
+ /**
2
+ * task-public-text.ts — **公开文本的隐私红线 (单一来源)** (2026-09-25)
3
+ *
4
+ * 为什么要单独一个文件: 同一条红线要同时把住**两个入口** ——
5
+ * · 桌面/CLI: 群消息发送前 (`task-group.ts` → `requirePublicText`)
6
+ * · 手机 WebView: 手机侧发公告/接单/入群前 (mobile-tasks.ts → 同一函数)
7
+ * 两边各抄一份规则 = 迟早漂移 (一边加了规则另一边没加, 于是手机成了绕过红线的后门)。
8
+ * 所以规则表放在这里, 双方都 import 这一份; 本文件**不 import 任何 node: 模块**,
9
+ * 因此既能进 Node, 也能进浏览器 bundle。
10
+ *
11
+ * 群消息**禁止出现**的形态 (顺序即报告顺序)。宁可多拦 (拒发 + 说清哪条规则),
12
+ * 也不放行一条带标识符的消息。
13
+ *
14
+ * 实现纪律 (2026-09-23 踩过的坑, 不可回退):
15
+ * · **不要用 `\b` 当边界** —— 消息里的 token 会把空白折成 `_`, 而 `_` 也是 \w, 于是 `判据见_0x1111…`
16
+ * 这种"粘在 `_`/汉字后面"的标识符 `\b` 判不出来, 会直接漏进群。一律改成对标识符自身字母表的
17
+ * lookahead/lookbehind (`(?<![0-9a-fA-FxX])`, `(?![0-9a-fA-F])`), 粘在 `_`/中文/符号后面也照样命中。
18
+ * · ipv6 规则要**避开 ISO 时间戳** (`09:50:12.345Z` 只有两个冒号): 只认 3 组以上 `x:y:z:w` 或含 `::` 的压缩形态。
19
+ * · 内容哈希 (sha256 64 位裸 hex) **不在**禁列 —— 交付就是要贴哈希 (它不是密钥, 也没有 0x 前缀)。
20
+ */
21
+ export const PRIVACY_RULES = [
22
+ { rule: 'wallet-address', why: '以太坊/EVM 钱包地址', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/ },
23
+ { rule: 'private-key-hex', why: '32 字节私钥形态 (0x + 64 hex)', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{64}(?![0-9a-fA-F])/ },
24
+ { rule: 'hex-0x-blob', why: '0x 开头的十六进制块 (签名/哈希/密钥都长这样, 群里一律不放原文)', re: /(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{8,}(?![0-9a-fA-F])/ },
25
+ { rule: 'did', why: '去中心化身份 DID', re: /did:[a-z0-9]+:[A-Za-z0-9._:%-]{3,}/i },
26
+ { rule: 'peer-id', why: 'libp2p peerId (12D3Koo…/Qm…)', re: /12D3Koo[A-Za-z0-9]{8,}|Qm[1-9A-HJ-NP-Za-km-z]{30,}/ },
27
+ { rule: 'multiaddr', why: 'libp2p multiaddr 或 store 地址 (/ip4 /tcp /p2p /orbitdb …)', re: /\/(ip4|ip6|dns|dns4|dns6|tcp|udp|ws|wss|quic|p2p|p2p-circuit|ipfs|orbitdb)(?![A-Za-z0-9])/i },
28
+ { rule: 'orbitdb-link', why: 'OrbitDB 链接 (群邀请链接本身也不进群消息)', re: /orbitdb:\/\//i },
29
+ { rule: 'ipv4', why: 'IPv4 地址', re: /(?<!\d)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?!\d)/ },
30
+ { rule: 'ipv6', why: 'IPv6 地址 (不含 ISO 时间戳)', re: /(?<![0-9A-Za-z])(?:[0-9a-fA-F]{1,4})?::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}(?![0-9A-Za-z])|(?<![0-9A-Za-z:])(?:[0-9a-fA-F]{1,4}:){3,}[0-9a-fA-F]{1,4}(?![0-9A-Za-z])/ },
31
+ { rule: 'pem', why: 'PEM 材料 (私钥/证书)', re: /-----BEGIN [A-Z ]*(PRIVATE KEY|CERTIFICATE)/ },
32
+ { rule: 'secret-words', why: '私钥/助记词/密钥字样 (正文里提到即停, 人不该把密钥写进群)', re: /(private\s*key|seed\s*phrase|mnemonic|私钥|助记词|密钥材料)/i },
33
+ { rule: 'url', why: 'http(s) 端点 (端点会带 host/IP 与用途指纹)', re: /https?:\/\/\S+/i },
34
+ { rule: 'email', why: '邮箱地址', re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
35
+ ];
36
+ /** 标识符打码 (只留前 4 字符) */
37
+ export function mask(s) {
38
+ const t = String(s || '');
39
+ return t.length <= 4 ? '****' : `${t.slice(0, 4)}…`;
40
+ }
41
+ /** 扫一遍文本, 返回命中的隐私规则 (空数组 = 可以发) */
42
+ export function scanPublicText(text) {
43
+ const t = String(text ?? '');
44
+ const out = [];
45
+ const seen = new Set();
46
+ for (const r of PRIVACY_RULES) {
47
+ const m = r.re.exec(t);
48
+ if (!m)
49
+ continue;
50
+ if (seen.has(r.rule))
51
+ continue;
52
+ seen.add(r.rule);
53
+ out.push({ rule: r.rule, why: r.why, masked: mask(String(m[0])), chars: String(m[0]).length });
54
+ }
55
+ return out;
56
+ }
57
+ /** 发送前唯一闸门: 命中任何规则 → 拒绝发送 (返回 violations 供 CLI/手机如实报出) */
58
+ export function requirePublicText(text) {
59
+ const v = scanPublicText(text);
60
+ return v.length === 0 ? { ok: true } : { ok: false, violations: v };
61
+ }
62
+ /**
63
+ * **节点身份**口径 (群管理命令 `task group create|join|list|link|leave` 的输出用, 2026-09-24):
64
+ * 与群消息同一张规则表, 但**去掉 orbitdb 那一档** ——
65
+ * · `orbitdb-link` 整条去掉: 邀请链接是群唯一能让别人入群的东西, 必须能打印出来;
66
+ * · `multiaddr` 规则里的 `orbitdb` 选项去掉 (只留节点地址 /ip4 /ip6 /dns /tcp /p2p …):
67
+ * 群 store 地址 (`/orbitdb/zdpu…`) 是**群自己的公开标识**, 不是节点地址。
68
+ * 仍然全拦: 原始 DID · 钱包地址 · peerId · 节点 multiaddr · IPv4/IPv6 · 私钥形态 · PEM · URL · 邮箱。
69
+ *
70
+ * 用法: 群管理**输出**再过一遍这道闸 (`scanNodeIdentity`)。命中 = 输出里真有节点身份/联系方式泄漏,
71
+ * 此时**拒输出并如实报**, 不静默脱敏掉再当成没事 (那样人看到的就不是事实了)。
72
+ */
73
+ export const NODE_IDENTITY_RULES = PRIVACY_RULES
74
+ .filter((r) => r.rule !== 'orbitdb-link')
75
+ .map((r) => (r.rule === 'multiaddr'
76
+ ? { ...r, re: /\/(ip4|ip6|dns|dns4|dns6|tcp|udp|ws|wss|quic|p2p|p2p-circuit|ipfs)(?![A-Za-z0-9])/i }
77
+ : r));
78
+ /** 扫节点身份泄漏 (空数组 = 输出干净) */
79
+ export function scanNodeIdentity(text) {
80
+ const t = String(text ?? '');
81
+ const out = [];
82
+ for (const r of NODE_IDENTITY_RULES) {
83
+ const m = r.re.exec(t);
84
+ if (!m)
85
+ continue;
86
+ out.push({ rule: r.rule, why: r.why, masked: mask(String(m[0])), chars: String(m[0]).length });
87
+ }
88
+ return out;
89
+ }
@@ -35,6 +35,7 @@ export const GROUPS_HELP = `
35
35
  bolloon task list | status | result | retry | run | send|inbox|accept|reject|complete|cancel(未实现)
36
36
  publish|board|claim (任务对外发布 + 接单: 公告板)
37
37
  announce|trail|post (群聊通道 C7: 公告入群 / 过程留痕回看 / 交付·初筛·终审痕迹)
38
+ group create|join|list|link|leave (群管理: 建群/自助入群/看群/取链接/退群)
38
39
  bolloon wallet status | policy | set-policy
39
40
  bolloon payment pending | approve | reject
40
41
  bolloon trade list | show | events | reconcile