@bolloon/bolloon-agent 0.4.32 → 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 (157) hide show
  1. package/dist/agents/chain/explorer.js +52 -0
  2. package/dist/agents/chain/index.js +2 -0
  3. package/dist/agents/chain/onchain-trade.js +15 -1
  4. package/dist/agents/contacts/chain.js +27 -7
  5. package/dist/agents/execution-supervisor.js +493 -35
  6. package/dist/agents/external-events.js +35 -6
  7. package/dist/agents/gateway-group.js +100 -12
  8. package/dist/agents/goal-criteria.js +10 -3
  9. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  10. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  11. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  12. package/dist/agents/goal-flywheel/index.js +26 -0
  13. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  14. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  15. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  16. package/dist/agents/goal-flywheel/types.js +329 -0
  17. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  18. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  19. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  20. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  21. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  22. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  23. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  24. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  25. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  26. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  27. package/dist/agents/goal-state-reducer.js +406 -0
  28. package/dist/agents/goal-store.js +35 -1
  29. package/dist/agents/mobile-flywheel-view.js +144 -0
  30. package/dist/agents/mobile-task-actions-verify.js +74 -0
  31. package/dist/agents/mobile-task-actions.js +178 -0
  32. package/dist/agents/mobile-task-views.js +199 -0
  33. package/dist/agents/network-pulse.js +525 -27
  34. package/dist/agents/pi-sdk.js +75 -20
  35. package/dist/agents/run-store.js +18 -5
  36. package/dist/agents/skill-readiness.js +52 -10
  37. package/dist/agents/subagent-manager.js +209 -3
  38. package/dist/agents/task/task-onchain-runner.js +19 -1
  39. package/dist/agents/task/task-runner.js +36 -6
  40. package/dist/agents/task-board.js +702 -0
  41. package/dist/agents/task-group.js +524 -0
  42. package/dist/agents/task-public-text.js +89 -0
  43. package/dist/cli/commands/index.js +3 -0
  44. package/dist/cli/commands/tasks.js +977 -8
  45. package/dist/cli/identity-command.js +177 -0
  46. package/dist/cli/protocol-envelope.js +4 -0
  47. package/dist/cli/setup-wizard.js +71 -0
  48. package/dist/cli/update-commands.js +150 -24
  49. package/dist/cli-entry.js +12 -2
  50. package/dist/electron-build/agents/chain/explorer.js +59 -0
  51. package/dist/electron-build/agents/chain/explorer.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/chain.js +26 -6
  53. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  54. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  55. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  56. package/dist/electron-build/agents/external-events.js +34 -5
  57. package/dist/electron-build/agents/external-events.js.map +1 -1
  58. package/dist/electron-build/agents/goal-criteria.js +10 -3
  59. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  60. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  61. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  63. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  65. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  67. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  69. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  71. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  86. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  87. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  88. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  89. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  90. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  91. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  92. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  93. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  94. package/dist/electron-build/agents/goal-store.js +35 -1
  95. package/dist/electron-build/agents/goal-store.js.map +1 -1
  96. package/dist/electron-build/agents/local-signer.js +101 -0
  97. package/dist/electron-build/agents/local-signer.js.map +1 -0
  98. package/dist/electron-build/agents/network-pulse.js +531 -28
  99. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  100. package/dist/electron-build/agents/pi-sdk.js +74 -19
  101. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  102. package/dist/electron-build/agents/run-store.js +20 -5
  103. package/dist/electron-build/agents/run-store.js.map +1 -1
  104. package/dist/electron-build/agents/skill-readiness.js +52 -9
  105. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  106. package/dist/electron-build/agents/task/task-runner.js +35 -5
  107. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  108. package/dist/electron-build/agents/task-board.js +761 -0
  109. package/dist/electron-build/agents/task-board.js.map +1 -0
  110. package/dist/electron-build/agents/task-contract.js +382 -0
  111. package/dist/electron-build/agents/task-contract.js.map +1 -0
  112. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  113. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  114. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  115. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  116. package/dist/electron-build/utils/dual-source.js +471 -0
  117. package/dist/electron-build/utils/dual-source.js.map +1 -0
  118. package/dist/electron-build/utils/update-manager.js +331 -57
  119. package/dist/electron-build/utils/update-manager.js.map +1 -1
  120. package/dist/electron-build/utils/update-state.js +16 -1
  121. package/dist/electron-build/utils/update-state.js.map +1 -1
  122. package/dist/electron-build/utils/version-info.js +82 -2
  123. package/dist/electron-build/utils/version-info.js.map +1 -1
  124. package/dist/index.js +103 -12
  125. package/dist/ios/chain-explorer.js +617 -0
  126. package/dist/ios/explorer.html +163 -0
  127. package/dist/ios/index.html +96 -0
  128. package/dist/ios/mobile-agent.js +2 -2
  129. package/dist/ios/mobile-contacts.js +329 -0
  130. package/dist/ios/mobile-core.js +1379 -226
  131. package/dist/ios/mobile-privacy.js +185 -0
  132. package/dist/ios/mobile.css +19 -0
  133. package/dist/ios/mobile.html +96 -0
  134. package/dist/ios/mobile.js +706 -7
  135. package/dist/ios/routes-chain-index.js +108 -0
  136. package/dist/ios/routes-contacts.js +420 -0
  137. package/dist/ios/routes-onchain-trade.js +99 -0
  138. package/dist/ios/routes-x402-info.js +1 -0
  139. package/dist/ios/server.js +762 -0
  140. package/dist/ios/task-frame-server.js +211 -0
  141. package/dist/orbitdb/cid-database.js +81 -16
  142. package/dist/orbitdb/ipfs-node.js +31 -2
  143. package/dist/utils/dual-source.js +426 -0
  144. package/dist/utils/update-cli.js +16 -21
  145. package/dist/utils/update-health.js +35 -5
  146. package/dist/utils/update-manager.js +329 -57
  147. package/dist/utils/update-state.js +16 -1
  148. package/dist/utils/version-info.js +75 -1
  149. package/dist/web/mobile-contacts.js +44 -11
  150. package/dist/web/mobile-core.js +1197 -513
  151. package/dist/web/mobile-tasks.js +339 -0
  152. package/dist/web/mobile.css +19 -0
  153. package/dist/web/mobile.html +64 -0
  154. package/dist/web/mobile.js +531 -1
  155. package/dist/web/routes-mobile-tasks.js +353 -0
  156. package/dist/web/server.js +163 -4
  157. package/package.json +3 -1
@@ -40,9 +40,49 @@ ${title('bolloon task')}
40
40
  bolloon task reject <requestId|taskId> --reason "<原因>"
41
41
  bolloon task complete|cancel 仍未实现 → 如实报 C_NOT_IMPLEMENTED
42
42
 
43
+ ── 任务对外发布 + 接单 (C1/C2, 2026-09-23: 公告板) ──
44
+ bolloon task publish --capability research --instruction "调研 X" --budget 0.05 \\
45
+ [--currency USDC] [--network base-sepolia] [--deadline +24h] [--reply-to http://me:port]
46
+ 发布一条**待接单**任务: 落盘 ~/.bolloon/tasks/board/<announcementId>.json +
47
+ 向 agent-registry 公告 (service.name = task.announce) + 记脉冲事件 task_announced;
48
+ 输出稳定 announcementId (同一 能力+正文+买方+预算 → 同一个 id, 重发幂等)
49
+ bolloon task board [--json] [--capability <名>] [--open] [--local]
50
+ 板上可接单的任务: **本地公告 + 注册表发现的远端公告**, 按 announcementId 去重
51
+ bolloon task claim <announcementId> [--price 0.02] [--group <群链接|groupId>]
52
+ provider 接单: 记认领者 DID + 时间 + 声明价格 (不执行、不付款、不标 verified);
53
+ 重复认领 / 已取消 / 不存在的 id → 一律拒绝并给原因;
54
+ 带 --group 时: 往群里发一条极短接单声明 (群非法 → 整条命令拒绝, 不静默降级)
55
+
56
+ ── 群聊通道 (C7, 2026-09-23: 过程留痕) ──
57
+ bolloon task announce --group <群链接|groupId> [--announcement-id <id>] [--capability <名>]
58
+ [--round <期号>] [--criteria "<验收判据摘要>"] [--from <短显示名>] [--json]
59
+ 把板上**一条待接单公告**压成一行极短事实 (期号 · capability · 预算 · 判据摘要 · 公告 id) 发进群;
60
+ 正文不进群; 缺 --group / 群非法 / 本机没这个群 → 拒绝执行 (绝不降级为只写本地)
61
+ bolloon task trail --group <群链接|groupId> [--announcement-id <id>] [--limit 300] [--json]
62
+ 从群消息读回本期过程留痕 (公告/接单/交付/初筛/终审) → 按时间排序的时间线, 每条带时间 + 发送者标记
63
+ bolloon task post --kind deliver|screen|final --group <群链接|groupId> --announcement-id <id> \\
64
+ [--hash sha256:<hex>] [--bytes <n>] [--checks "渠道结构=pass,价格带=fail"] [--verdict accept|reject|unknown]
65
+ [--round <期号>] [--from <短显示名>] [--json]
66
+ 交付只贴哈希 · 初筛逐条结果 · 终审结论; 群消息一律无钱包地址/DID/peerId/multiaddr/IP/私钥形态
67
+ (命中任一条 → 拒发并报出命中的规则)
68
+
69
+ ── 群管理 (2026-09-24: 外部接单者的自助入群入口) ──
70
+ bolloon task group create --name <群名> [--from <短显示名>] [--json]
71
+ 建群 → 打印**邀请链接** (发给要入群的人) + groupId; 落 ~/.bolloon/gateway-groups.json
72
+ (ACL write:['*'] = 成员可广播, 建群时写进 store manifest)
73
+ bolloon task group join <群链接|groupId> [--json]
74
+ 用**链接**自助入群 (幂等: 已在群里 → already=true, 仍 exit 0);
75
+ store 拿不到区块 → 大声失败 (TRANSPORT_FAILED + storeCode=STORE_UNREACHABLE), 绝不显示成空群
76
+ bolloon task group list [--json] 本机已加入的群 (groupId · 群名 · 加入时间); 没有群 → 空列表
77
+ bolloon task group link <groupId|群名> 显式取回邀请链接 (list 里不放链接)
78
+ bolloon task group leave <groupId|群名> 退群 (只摘本机记录)
79
+ 群管理输出不含原始 DID/钱包地址/peerId/节点 multiaddr/IP (脱敏口径与群消息同源)
80
+
43
81
  选项: --json · --quiet · --request-id <id> · --timeout <ms> · --resume <goalId> (同 bolloon task)
44
82
  说明: 收件箱落盘 ~/.bolloon/tasks/inbox/; 本机发出的任务台账 ~/.bolloon/tasks/local/;
45
- 结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout)。
83
+ 结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout);
84
+ 任务公告 ~/.bolloon/tasks/board/ (正文只在本地, 注册表里只有 sha256 摘要 + 60 字预览);
85
+ 群里的过程痕迹**只在群里** (群消息就是留痕本体, 本机不留影子副本); 结算仍只在链上。
46
86
  `;
47
87
  /** 交易记录判定 (P1 §5.1 红线: local-dev 永不 verified; 成功判据见 §7.1) */
48
88
  function judge(rec) {
@@ -135,6 +175,14 @@ export async function taskCommand(flags) {
135
175
  case 'reject': return taskReject(flags);
136
176
  case 'complete': return taskComplete(flags);
137
177
  case 'cancel': return taskCancel(flags);
178
+ case 'publish': return taskPublish(flags);
179
+ case 'board': return taskBoard(flags);
180
+ case 'claim': return taskClaim(flags);
181
+ case 'announce': return taskAnnounce(flags);
182
+ case 'trail': return taskTrail(flags);
183
+ case 'post': return taskPost(flags);
184
+ // 2026-09-24: 群管理 (create/join/list/link/leave) —— 外部接单者的自助入群入口
185
+ case 'group': return taskGroup(flags);
138
186
  default:
139
187
  return {
140
188
  envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 task 子命令: ${sub}` : '缺少 task 子命令', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
@@ -641,7 +689,7 @@ function parseInput(raw) {
641
689
  }
642
690
  // ── bolloon-task/1 真收发: send / inbox / accept / reject ────────────────────
643
691
  /** 人类单位 → 原子单位正整数串 (拒绝 0 / 负 / 超精度; 浮点模糊一律拒) */
644
- function toAtomic(human, decimals) {
692
+ export function toAtomic(human, decimals) {
645
693
  const s = String(human ?? '').trim();
646
694
  if (!/^[0-9]+(\.[0-9]+)?$/.test(s))
647
695
  return null;
@@ -652,7 +700,7 @@ function toAtomic(human, decimals) {
652
700
  return n > 0n ? n.toString() : null;
653
701
  }
654
702
  /** `--deadline +2h` / `+30m` / `+45s` / `+1d`, 或未来毫秒时间戳 */
655
- function parseDeadline(v, now = Date.now()) {
703
+ export function parseDeadline(v, now = Date.now()) {
656
704
  if (v === undefined)
657
705
  return now + 24 * 60 * 60 * 1000;
658
706
  const m = /^\+(\d+)(s|m|h|d)$/.exec(String(v).trim());
@@ -735,6 +783,32 @@ function transferView(t) {
735
783
  return t ? { kind: t.kind, target: t.target, ok: t.ok, elapsedMs: t.elapsedMs, duplicate: t.duplicate === true, error: t.error || null, detail: t.detail || null } : null;
736
784
  }
737
785
  // ── send ────────────────────────────────────────────────────────────────────
786
+ /**
787
+ * 没有匹配的 provider 时: 板上若有**可接单**的公告 → 给可操作提示 (指向 board/claim)。
788
+ * 拿不到板上的事实 → `hint:null` + 如实带 `boardError` (不猜、不假装有公告)。
789
+ */
790
+ async function boardHintForCapability(capability) {
791
+ try {
792
+ const { findOpenAnnouncementsForCapability } = await import('../../agents/task-board.js');
793
+ const open = await findOpenAnnouncementsForCapability(capability);
794
+ if (!open.length)
795
+ return { count: 0, announcements: [], hint: null, boardError: null };
796
+ const first = open[0];
797
+ return {
798
+ count: open.length,
799
+ announcements: open.slice(0, 5).map((a) => ({
800
+ announcementId: a.announcementId, capability: a.capability, status: a.status,
801
+ budget: a.budget, deadline: a.deadline, buyer: a.buyerShort, remote: a.remote,
802
+ signatureVerified: a.signatureVerified,
803
+ })),
804
+ hint: `板上有 ${open.length} 条可接单的 '${capability}' 任务: 先 bolloon task board 看全, 或直接 bolloon task claim ${first.announcementId}`,
805
+ boardError: null,
806
+ };
807
+ }
808
+ catch (e) {
809
+ return { count: 0, announcements: [], hint: null, boardError: String(e?.message || e).slice(0, 160) };
810
+ }
811
+ }
738
812
  async function taskSend(flags) {
739
813
  const instruction = opt(flags, '--instruction') || flags.positionals.slice(1).join(' ').trim();
740
814
  const capability = opt(flags, '--capability');
@@ -810,18 +884,29 @@ async function taskSend(flags) {
810
884
  providerDid = providerDid || resolved.target.providerDid || '';
811
885
  }
812
886
  else if (via === 'gateway') {
887
+ const boardHint = await boardHintForCapability(capability);
813
888
  return {
814
- envelope: failEnvelope('CAPABILITY_NOT_FOUND', `注册表里没有能力 '${capability}' 的 provider (registry ${resolved.registryReady ? '就绪' : '未就绪'}, ${resolved.candidates} 条候选)`, { capability, registryReady: resolved.registryReady, candidates: resolved.candidates, via: 'gateway' }, [], 'redefine_capability'),
815
- human: `${title('bolloon task send')}\n 注册表里没有这个能力的 provider, gateway 传输没有目标`,
889
+ envelope: failEnvelope('CAPABILITY_NOT_FOUND', `注册表里没有能力 '${capability}' 的 provider (registry ${resolved.registryReady ? '就绪' : '未就绪'}, ${resolved.candidates} 条候选)${boardHint.hint ? `;${boardHint.hint}` : ''}`, { capability, registryReady: resolved.registryReady, candidates: resolved.candidates, via: 'gateway', board: boardHint }, [], 'redefine_capability'),
890
+ human: [
891
+ `${title('bolloon task send')}`,
892
+ ` 注册表里没有这个能力的 provider, gateway 传输没有目标`,
893
+ ...(boardHint.hint ? [`\n ${hint(boardHint.hint)}`] : []),
894
+ ].join('\n'),
816
895
  };
817
896
  }
818
897
  else {
819
898
  const joined = resolved.registryReady;
899
+ const boardHint = await boardHintForCapability(capability);
820
900
  return {
821
901
  envelope: failEnvelope(joined ? 'CAPABILITY_NOT_FOUND' : 'NETWORK_NOT_JOINED', joined
822
- ? `注册表里没有能力 '${capability}' 的 provider (${resolved.candidates} 条候选, 且都没有 endpoint)`
823
- : '本机 registry 未就绪 (没有入网/没拉过任何服务声明), 无法解析目标', { capability, registryReady: joined, candidates: resolved.candidates, howTo: '用 --endpoint http://host:port 显式给目标, 或先 bolloon network join / bolloon agent register' }, [], joined ? 'redefine_capability' : 'rejoin_network'),
824
- human: `${title('bolloon task send')}\n ${joined ? '注册表里没有这个能力' : '本机还没入网, 也没有显式 --endpoint'}\n\n${hint('示例: bolloon task send --capability research --instruction "…" --endpoint http://127.0.0.1:54901')}`,
902
+ ? `注册表里没有能力 '${capability}' 的 provider (${resolved.candidates} 条候选, 且都没有 endpoint)${boardHint.hint ? `;${boardHint.hint}` : ''}`
903
+ : `本机 registry 未就绪 (没有入网/没拉过任何服务声明), 无法解析目标`, { capability, registryReady: joined, candidates: resolved.candidates, board: boardHint, howTo: '用 --endpoint http://host:port 显式给目标, 或先 bolloon network join / bolloon agent register' }, [], joined ? 'redefine_capability' : 'rejoin_network'),
904
+ human: [
905
+ `${title('bolloon task send')}`,
906
+ ` ${joined ? '注册表里没有这个能力' : '本机还没入网, 也没有显式 --endpoint'}`,
907
+ ...(boardHint.hint ? [`\n ${hint(boardHint.hint)}`] : []),
908
+ `\n${hint('示例: bolloon task send --capability research --instruction "…" --endpoint http://127.0.0.1:54901')}`,
909
+ ].join('\n'),
825
910
  };
826
911
  }
827
912
  }
@@ -1216,6 +1301,890 @@ async function taskReject(flags) {
1216
1301
  ].join('\n'),
1217
1302
  };
1218
1303
  }
1304
+ // ── publish / board / claim (C1/C2 任务对外发布 + 接单, 2026-09-23) ──────────
1305
+ /** 板上一行的对外投影 (不含正文; 远端认领数的诚实标注) */
1306
+ function boardEntryView(e) {
1307
+ return {
1308
+ announcementId: e.announcementId,
1309
+ capability: e.capability,
1310
+ buyer: e.buyerShort,
1311
+ status: e.status,
1312
+ budget: e.budget,
1313
+ deadline: e.deadline,
1314
+ deadlineInMs: e.deadlineInMs,
1315
+ claimCount: e.claimCount,
1316
+ claimedBy: e.claimedBy,
1317
+ claimedAt: e.claimedAt,
1318
+ remote: e.remote,
1319
+ source: e.source,
1320
+ signatureVerified: e.signatureVerified,
1321
+ claimable: e.claimable,
1322
+ instructionDigest: e.instructionDigest,
1323
+ instructionPreview: e.instructionPreview,
1324
+ };
1325
+ }
1326
+ async function taskPublish(flags) {
1327
+ const capability = opt(flags, '--capability');
1328
+ const instruction = opt(flags, '--instruction') || flags.positionals.slice(1).join(' ').trim();
1329
+ const budgetHuman = opt(flags, '--budget');
1330
+ if (!capability || !instruction) {
1331
+ return {
1332
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --capability 或 --instruction (公告必须有能力和任务正文)', { usage: plain(TASK_USAGE.trim()), got: { capability: capability || null, instruction: !!instruction } }, [], 'needs_human'),
1333
+ human: `${TASK_USAGE}\n${hint('示例: bolloon task publish --capability research --instruction "调研某类厨房用品的日本市场" --budget 0.05')}`,
1334
+ };
1335
+ }
1336
+ const currency = String(opt(flags, '--currency') || 'USDC').toUpperCase();
1337
+ if (currency !== 'USDC' && currency !== 'ETH') {
1338
+ return { envelope: failEnvelope('INVALID_ARGUMENT', `--currency 只支持 USDC/ETH (收到 ${currency})`, { currency }, [], 'needs_human'), human: TASK_USAGE };
1339
+ }
1340
+ const network = opt(flags, '--network') || 'base-sepolia';
1341
+ if (budgetHuman === undefined) {
1342
+ return {
1343
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --budget (待接单的公告必须带预算: 没有预算就没有可核验的委托口径)', { usage: plain(TASK_USAGE.trim()), accepted: ['--budget 0.05'] }, [], 'needs_human'),
1344
+ human: `${TASK_USAGE}\n${hint('示例: bolloon task publish --capability research --instruction "…" --budget 0.05')}`,
1345
+ };
1346
+ }
1347
+ const atomic = toAtomic(budgetHuman, currency === 'USDC' ? 6 : 18);
1348
+ if (!atomic) {
1349
+ return {
1350
+ envelope: failEnvelope('INVALID_ARGUMENT', `--budget 必须是正数 (收到 ${budgetHuman}); 原子单位串由本命令换算, 不接受 0/负/超精度`, { budget: budgetHuman, currency, accepted: ['--budget 0.05'] }, [], 'needs_human'),
1351
+ human: TASK_USAGE,
1352
+ };
1353
+ }
1354
+ const modeRaw = opt(flags, '--mode') || 'policy';
1355
+ const { isPaymentMode } = await import('../../agents/task-contract.js');
1356
+ if (!isPaymentMode(modeRaw)) {
1357
+ return {
1358
+ envelope: failEnvelope('INVALID_ARGUMENT', `--mode 非法: ${modeRaw} (要 manual|policy|autonomous|agent-authorized)`, { mode: modeRaw }, [], 'needs_human'),
1359
+ human: TASK_USAGE,
1360
+ };
1361
+ }
1362
+ const dl = parseDeadline(opt(flags, '--deadline'));
1363
+ if (typeof dl === 'string') {
1364
+ return { envelope: failEnvelope('INVALID_ARGUMENT', dl, { deadline: opt(flags, '--deadline') }, [], 'needs_human'), human: TASK_USAGE };
1365
+ }
1366
+ const s = await requireSigner('bolloon task publish');
1367
+ if ('fail' in s)
1368
+ return s.fail;
1369
+ const signer = s.signer;
1370
+ const { publishAnnouncement } = await import('../../agents/task-board.js');
1371
+ const r = await publishAnnouncement({
1372
+ capability, instruction,
1373
+ buyerDid: signer.did,
1374
+ buyerPublicKeyHex: signer.publicKeyHex,
1375
+ budget: { maxAmount: atomic, currency, network },
1376
+ deadline: dl,
1377
+ paymentMode: modeRaw,
1378
+ replyTo: opt(flags, '--reply-to') || null,
1379
+ signerKeypair: signer.keypair,
1380
+ });
1381
+ if (!r.ok || !r.announcement) {
1382
+ return {
1383
+ envelope: failEnvelope('INTERNAL_ERROR', `公告没发布成功: ${r.error || '未知原因'}`, { capability, announcementId: null, paid: false, fundsMoved: false }, [], 'needs_human'),
1384
+ human: `${title('bolloon task publish')}\n 没发布成功: ${r.error || '未知原因'}`,
1385
+ };
1386
+ }
1387
+ const a = r.announcement;
1388
+ const data = {
1389
+ announcementId: a.announcementId,
1390
+ capability: a.capability,
1391
+ buyerDid: a.buyerDid,
1392
+ status: a.status,
1393
+ budget: a.budget,
1394
+ deadline: a.deadline,
1395
+ paymentMode: a.paymentMode,
1396
+ instructionDigest: a.instructionDigest,
1397
+ instructionPreview: a.instructionPreview,
1398
+ signed: r.signed,
1399
+ duplicate: r.dup,
1400
+ registry: r.registry,
1401
+ pulse: r.pulse,
1402
+ file: r.file || `~/.bolloon/tasks/board/${a.announcementId}.json`,
1403
+ registryService: 'task.announce (agent-registry; description 里只有摘要+预览, 没有正文)',
1404
+ paid: false,
1405
+ fundsMoved: false,
1406
+ note: '公告只是"把待接单的任务放出去": 不执行、不付款、不产生交易; 接单: bolloon task claim <announcementId>',
1407
+ };
1408
+ const human = [
1409
+ title('bolloon task publish'),
1410
+ line('公告号', a.announcementId),
1411
+ line('capability', a.capability),
1412
+ line('预算', a.budget ? `≤ ${a.budget.maxAmount} 原子 (${a.budget.currency} @ ${a.budget.network})` : '(无)'),
1413
+ line('截止时间', new Date(a.deadline).toISOString()),
1414
+ line('签名', r.signed ? '已签名 (覆盖公开载荷: 能力/预算/截止/正文摘要)' : '**没有签名** (没有本机身份 → 接单会被拒)'),
1415
+ line('注册表', r.registry.attempted ? (r.registry.announced ? `已公告 (${r.registry.entries} 条 capability)` : `没公告: ${r.registry.error || '未知'}`) : '没有尝试 (离线)'),
1416
+ line('脉冲事件', r.pulse.attempted ? (r.pulse.ok ? 'task_announced 已记' : `没记: ${r.pulse.reason || '未知'}`) : '没有尝试 (离线)'),
1417
+ r.dup ? line('重复发布', '同一 (能力+正文+买方+预算) → 复用既有公告 (幂等), 没覆盖既有认领') : '',
1418
+ line('本次付款', '没有 (发布公告不付款)'),
1419
+ `\n ${hint('下一步: 别人看板 bolloon task board · 接单 bolloon task claim ' + a.announcementId)}`,
1420
+ ].filter(Boolean).join('\n');
1421
+ return {
1422
+ envelope: okEnvelope('OK', `已发布待接单任务 ${a.announcementId} (${a.capability})${r.dup ? ' — 复用既有公告 (幂等)' : ''}`, data, [a.announcementId], null),
1423
+ human,
1424
+ };
1425
+ }
1426
+ async function taskBoard(flags) {
1427
+ const { listBoard } = await import('../../agents/task-board.js');
1428
+ const capFilter = opt(flags, '--capability');
1429
+ const openOnly = has(flags, '--open');
1430
+ const localOnly = has(flags, '--local');
1431
+ const view = await listBoard({ openOnly, localOnly });
1432
+ const capLower = String(capFilter || '').trim().toLowerCase();
1433
+ const entries = capLower ? view.entries.filter((e) => String(e.capability).trim().toLowerCase() === capLower) : view.entries;
1434
+ const data = {
1435
+ count: entries.length,
1436
+ local: view.localCount,
1437
+ remote: view.remoteCount,
1438
+ duplicatesDeduped: view.duplicates,
1439
+ filter: { capability: capFilter || null, openOnly, localOnly },
1440
+ registryReady: view.registryReady,
1441
+ registryError: view.registryError,
1442
+ dir: '~/.bolloon/tasks/board',
1443
+ entries: entries.map(boardEntryView),
1444
+ notes: view.notes,
1445
+ note: '板上只有摘要/预览: 任务正文只在买方本地; 接单: bolloon task claim <announcementId>',
1446
+ next: entries.some((e) => e.claimable) ? `bolloon task claim ${entries.find((e) => e.claimable).announcementId}` : null,
1447
+ };
1448
+ const human = entries.length
1449
+ ? entries.map((e) => [
1450
+ `\n ${e.announcementId}${e.remote ? ' (远端公告)' : ''}`,
1451
+ ` capability ${e.capability}`,
1452
+ ` 买方 ${e.buyerShort || '(无)'}`,
1453
+ ` 状态 ${e.status}${e.claimable ? ' · 可接单' : e.status === 'open' ? ' · 已过期' : ''}`,
1454
+ ` 预算 ${e.budget ? `≤ ${e.budget.maxAmount} 原子 ${e.budget.currency} @ ${e.budget.network}` : '(无)'}`,
1455
+ ` 截止时间 ${e.deadline ? `${new Date(e.deadline).toISOString()}${e.deadlineInMs !== null ? ` (${e.deadlineInMs > 0 ? `还剩 ${Math.round(e.deadlineInMs / 1000)}s` : '已过期'})` : ''}` : '(无)'}`,
1456
+ ` 认领 ${e.claimCount === 0 ? '还没有人接' : `${e.claimCount} 人 (最近: ${String(e.claimedBy).slice(0, 24)})`}${e.remote ? ' · 远端的认领数本机看不到' : ''}`,
1457
+ ` 签名 ${e.signatureVerified === true ? '验签通过' : e.signatureVerified === false ? '**验签不过** (接不了单)' : '未验签(如实)'}`,
1458
+ ` 预览 ${e.instructionPreview || '(无)'}`,
1459
+ ].join('\n')).join('')
1460
+ : '\n (板上没有可接单的任务)';
1461
+ return {
1462
+ envelope: okEnvelope('OK', `板上 ${entries.length} 条${openOnly ? '可接单' : ''}公告 (本地 ${view.localCount} · 远端 ${view.remoteCount})${capFilter ? ` [capability=${capFilter}]` : ''}`, data, entries.map((e) => e.announcementId), null),
1463
+ human: [
1464
+ `${title('bolloon task board')}${human}`,
1465
+ '',
1466
+ ` ${String(data.note)}`,
1467
+ view.registryError ? ` ${hint(`注册表读取失败 (远端公告可能不全): ${view.registryError}`)}` : '',
1468
+ data.next ? `\n ${hint('下一步: ' + data.next)}` : '',
1469
+ ].filter(Boolean).join('\n'),
1470
+ };
1471
+ }
1472
+ async function taskClaim(flags) {
1473
+ const id = flags.positionals[1] || opt(flags, '--announcement-id') || flags.requestId;
1474
+ if (!id) {
1475
+ return {
1476
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 announcementId (bolloon task claim <announcementId>)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
1477
+ human: `${TASK_USAGE}\n${hint('先看板: bolloon task board')}`,
1478
+ };
1479
+ }
1480
+ const s = await requireSigner('bolloon task claim');
1481
+ if ('fail' in s)
1482
+ return s.fail;
1483
+ const signer = s.signer;
1484
+ // ★ C7: 带 --group 时**在认领之前**把群定死 —— 群非法/没加入 → 整条命令拒绝 (绝不"认领落本地 + 悄悄不发群")
1485
+ const wantsGroup = opt(flags, '--group') !== undefined;
1486
+ const gctx = wantsGroup ? await requireGroup('bolloon task claim ' + id, flags) : null;
1487
+ if (gctx && !gctx.ok)
1488
+ return gctx.result;
1489
+ // 声明价格 (可选): 不声明 → null, 不替它猜
1490
+ const priceHuman = opt(flags, '--price');
1491
+ const currency = String(opt(flags, '--currency') || 'USDC').toUpperCase();
1492
+ let priceAtomic = null;
1493
+ if (priceHuman !== undefined) {
1494
+ priceAtomic = toAtomic(priceHuman, currency === 'USDC' ? 6 : 18);
1495
+ if (!priceAtomic) {
1496
+ return {
1497
+ envelope: failEnvelope('INVALID_ARGUMENT', `--price 必须是正数 (收到 ${priceHuman})`, { price: priceHuman, currency, accepted: ['--price 0.02'] }, [], 'needs_human'),
1498
+ human: TASK_USAGE,
1499
+ };
1500
+ }
1501
+ }
1502
+ const { claimAnnouncement } = await import('../../agents/task-board.js');
1503
+ const r = await claimAnnouncement(id, {
1504
+ providerDid: signer.did,
1505
+ providerPublicKeyHex: signer.publicKeyHex,
1506
+ priceAmountAtomic: priceAtomic,
1507
+ currency,
1508
+ network: opt(flags, '--network') || '',
1509
+ signerKeypair: signer.keypair,
1510
+ });
1511
+ const baseData = {
1512
+ announcementId: id,
1513
+ reason: r.reason,
1514
+ remote: r.remote,
1515
+ // ★ 本命令任何路径都不动钱、不执行、不标 verified
1516
+ executed: false,
1517
+ paid: r.paid,
1518
+ fundsMoved: r.fundsMoved,
1519
+ verified: r.verified,
1520
+ deliveredToBuyer: r.deliveredToBuyer,
1521
+ };
1522
+ if (!r.ok) {
1523
+ const code = r.reason === 'not_found' ? 'NOT_FOUND'
1524
+ : r.reason === 'already_claimed' ? 'DUPLICATE_REQUEST'
1525
+ : r.reason === 'cancelled' ? 'TASK_CANCELLED'
1526
+ : r.reason === 'signature_invalid' || r.reason === 'instruction_digest_mismatch' ? 'SIGNATURE_INVALID'
1527
+ : r.reason === 'deadline_expired' ? 'DEADLINE_EXPIRED'
1528
+ : 'INVALID_ARGUMENT';
1529
+ return {
1530
+ envelope: failEnvelope(code, `没接成单: ${r.message}`, { ...baseData, status: r.status ?? null, existingClaim: r.existingClaim ?? null }, [id], 'needs_human'),
1531
+ human: `${title('bolloon task claim ' + id)}\n ${r.message}\n${line('本次付款', '没有')}\n\n${hint('看板: bolloon task board')}`,
1532
+ };
1533
+ }
1534
+ const c = r.claim;
1535
+ // ★ C7: 认领成功 → 往群里发一条极短接单声明 (群已在认领**之前**定死; 这里最坏也只是"发失败")
1536
+ let groupPost = null;
1537
+ if (gctx && gctx.ok) {
1538
+ const TG = await import('../../agents/task-group.js');
1539
+ const priceTok = c.priceAmountAtomic ? `${c.priceAmountAtomic}${String(c.currency || '').toUpperCase()}` : null;
1540
+ const built = TG.buildPostMessage({ kind: 'claim', announcementId: id, price: priceTok });
1541
+ const sent = built.ok
1542
+ ? await TG.sendTrailMessage(gctx.group.groupId, built.text, gctx.fromTag)
1543
+ : { ok: false, sent: false, error: built.message, violations: null };
1544
+ groupPost = {
1545
+ requested: true,
1546
+ posted: sent.sent === true,
1547
+ error: sent.sent ? null : (sent.error || '未知原因'),
1548
+ message: built.ok ? built.text : null,
1549
+ group: gctx.group.groupId,
1550
+ senderTag: gctx.fromTag,
1551
+ localFallback: false,
1552
+ /** 群消息没发出去**不等于**认领没发生: 认领事实已写进本地账 (上面那步真写过了) */
1553
+ claimStillRecorded: true,
1554
+ };
1555
+ }
1556
+ else if (!wantsGroup) {
1557
+ groupPost = { requested: false, posted: false, reason: '没有 --group: 本命令原有语义不变 (只记本地认领账); 群里留痕要显式给 --group' };
1558
+ }
1559
+ const data = {
1560
+ ...baseData,
1561
+ claim: {
1562
+ providerDid: c.providerDid,
1563
+ providerPublicKeyHex: c.providerPublicKeyHex ? `${String(c.providerPublicKeyHex).slice(0, 16)}…` : null,
1564
+ claimedAt: c.claimedAt,
1565
+ priceAmountAtomic: c.priceAmountAtomic,
1566
+ currency: c.currency,
1567
+ network: c.network,
1568
+ signed: !!c.signature,
1569
+ },
1570
+ status: r.status ?? null,
1571
+ file: r.file,
1572
+ priceNote: r.priceNote ?? null,
1573
+ /** C7 群聊侧: 认领有没有在群里留下接单声明 (发失败也如实标; 认领事实不受影响) */
1574
+ group: groupPost,
1575
+ honestyNote: r.remote
1576
+ ? '远端公告的认领只落本机台账 (**没有投递给买方**); 真交接走 bolloon task send → 对方 task accept'
1577
+ : '认领只是"我来做"的本地事实: 不执行任务、不付款、不标 verified (释放/验真另走裁决函数)',
1578
+ };
1579
+ const groupNext = groupPost && groupPost.requested === true && groupPost.posted !== true ? 'needs_human' : null;
1580
+ return {
1581
+ envelope: okEnvelope('OK', `${r.message}${groupPost && groupPost.requested === true ? (groupPost.posted === true ? ' (接单声明已进群)' : ' (接单声明没发进群)') : ''}`, data, [id], groupNext ?? (r.remote ? 'needs_human' : null)),
1582
+ human: [
1583
+ title('bolloon task claim ' + id),
1584
+ line('认领者', c.providerDid),
1585
+ line('认领时间', new Date(c.claimedAt).toISOString()),
1586
+ line('声明价格', c.priceAmountAtomic ? `${c.priceAmountAtomic} 原子 ${c.currency}` : '(没声明价)'),
1587
+ line('公告来源', r.remote ? '注册表 (远端)' : '本地公告文件'),
1588
+ line('投递给买方', r.deliveredToBuyer ? '已投递' : '**没有** (本版没有远端认领投递通道; 真交接走 task send/accept)'),
1589
+ ...(groupPost ? [line('群聊留痕', groupPost.requested === true
1590
+ ? (groupPost.posted === true ? `接单声明已进群 (发送者标记 ${String(groupPost.senderTag)})` : `**没发进群**: ${String(groupPost.error)} — 认领事实已记账, 但群里没有这条痕迹`)
1591
+ : `没有 --group: 只记本地账 (${String(groupPost.reason)})`)] : []),
1592
+ line('本次执行', '没有 (接单不执行任务)'),
1593
+ line('本次付款', '没有 (接单不付款)'),
1594
+ r.remote ? `\n ${hint('下一步: 让买方把任务发过来 (bolloon task send); 对方 accept 后才有执行/交付')}` : `\n ${hint('下一步: 等买方的 task send/accept; 交付后用 decideAnnouncementRelease 判是否可释放 (未交付不得释放)')}`,
1595
+ ].join('\n'),
1596
+ };
1597
+ }
1598
+ // ── 群聊通道 (C7: announce / trail / post, 2026-09-23) ───────────────────────
1599
+ /** C7 群上下文: 群必须先定下来 (缺/非法/没加入 → 拒跑); 发送者标记只用短假名 */
1600
+ async function requireGroup(head, flags) {
1601
+ const TG = await import('../../agents/task-group.js');
1602
+ const ref = opt(flags, '--group');
1603
+ const g = await TG.resolveGroupRef(ref);
1604
+ if (!g.ok) {
1605
+ return {
1606
+ ok: false,
1607
+ result: {
1608
+ envelope: failEnvelope(g.code, `${g.message}`, { group: ref ?? null, ...g.detail, sent: false, localFallback: false, why: '缺群/群非法/群没加入一律拒绝执行: 本命令不做"只写本地"的降级' }, [], 'needs_human'),
1609
+ human: [
1610
+ title(head),
1611
+ ` 群没定下来: ${g.message}`,
1612
+ '',
1613
+ hint('群聊通道就是留痕的地方: 没有群就没有留痕 —— 这里不会静默降级成只写本地'),
1614
+ ...(Array.isArray(g.detail.joined) && g.detail.joined.length
1615
+ ? [line('本机已加入的群', g.detail.joined.map((x) => `${x.id}${x.name ? ` (${x.name})` : ''}`).join(' · '))]
1616
+ : [line('本机已加入的群', '(没有)')]),
1617
+ ].join('\n'),
1618
+ },
1619
+ };
1620
+ }
1621
+ const s = await TG.resolveSenderTag(opt(flags, '--from'));
1622
+ if (!s.ok) {
1623
+ return {
1624
+ ok: false,
1625
+ result: {
1626
+ envelope: failEnvelope(s.code, `${s.message}`, { ...s.detail, group: g.group, sent: false, localFallback: false }, [], 'needs_human'),
1627
+ human: `${title(head)}\n 发送者标记定不下来: ${s.message}\n\n${hint('群里只出现短假名 (agent-xxxxxxxx) 或 --from 给的短显示名; 原始 DID 永不进群')}`,
1628
+ },
1629
+ };
1630
+ }
1631
+ return { ok: true, group: g.group, fromTag: s.tag, fromSource: s.source };
1632
+ }
1633
+ /** 时间线条目 → 对外投影 (不含原消息文本; 字段已被遮蔽过) */
1634
+ function trailEntryView(e) {
1635
+ return { kind: e.kind, at: e.at, atIso: e.at ? new Date(e.at).toISOString() : null, sender: e.sender, announcementId: e.announcementId, fields: e.fields };
1636
+ }
1637
+ /**
1638
+ * `bolloon task announce --group <群链接|groupId>` —— 把板上一条待接单公告压成一行极短事实发进群。
1639
+ * 内容只有: 期号 · capability · 预算 · 验收判据摘要 · 公告 id (任务正文不进群)。
1640
+ */
1641
+ async function taskAnnounce(flags) {
1642
+ const head = 'bolloon task announce';
1643
+ const ctx = await requireGroup(head, flags);
1644
+ if (!ctx.ok)
1645
+ return ctx.result;
1646
+ const TG = await import('../../agents/task-group.js');
1647
+ const picked = TG.pickAnnouncement({ announcementId: opt(flags, '--announcement-id'), capability: opt(flags, '--capability') });
1648
+ if (!picked.ok) {
1649
+ return {
1650
+ envelope: failEnvelope(picked.code, picked.message, { ...picked.detail, group: ctx.group, sent: false, localFallback: false }, [], 'needs_human'),
1651
+ human: `${title(head)}\n 没得可发: ${picked.message}\n\n${hint('先看板: bolloon task board; 发公告: bolloon task publish --capability … --instruction "…" --budget 0.05')}`,
1652
+ };
1653
+ }
1654
+ const a = picked.announcement;
1655
+ const text = TG.buildAnnounceMessage(a, { round: opt(flags, '--round'), criteria: opt(flags, '--criteria') });
1656
+ const sent = await TG.sendTrailMessage(ctx.group.groupId, text, ctx.fromTag);
1657
+ if (!sent.ok) {
1658
+ const violations = sent.violations || null;
1659
+ return {
1660
+ envelope: failEnvelope(violations ? 'POLICY_DENIED' : 'TRANSPORT_FAILED', violations ? `公告命中隐私红线, 拒绝发进群: ${violations.map((v) => v.rule).join(', ')}` : `群消息没发出去: ${sent.error}`, { announcementId: a.announcementId, group: ctx.group.groupId, message: text, sent: false, violations, localFallback: false, paid: false }, [a.announcementId], 'needs_human'),
1661
+ human: [
1662
+ title(head),
1663
+ ` ${violations ? '命中隐私红线, 拒发' : '没发出去'}: ${sent.error}`,
1664
+ ...(violations ? violations.map((v) => ` · ${v.rule} — ${v.why} (${v.masked}, ${v.chars} 字符)`) : []),
1665
+ '',
1666
+ hint(violations ? '把公告的预览/判据摘要改成不含标识符的短事实后重发 (本命令不静默脱敏: 脱敏后的公告不是你要发的那条)' : '群 store 不可达时不会落本地: 就是没发出去'),
1667
+ ].join('\n'),
1668
+ };
1669
+ }
1670
+ const data = {
1671
+ announcementId: a.announcementId,
1672
+ capability: a.capability,
1673
+ budget: a.budget,
1674
+ deadline: a.deadline,
1675
+ round: opt(flags, '--round') || null,
1676
+ judgeStated: !!opt(flags, '--criteria'),
1677
+ message: sent.text,
1678
+ group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
1679
+ sender: { tag: ctx.fromTag, source: ctx.fromSource, didPrinted: false },
1680
+ /** 群里只有过程事实: 不执行、不付款、不标 verified */
1681
+ executed: false,
1682
+ paid: false,
1683
+ fundsMoved: false,
1684
+ localFallback: false,
1685
+ note: '公告入群只发极短事实 (期号/capability/预算/判据摘要/公告 id): 任务正文不进群; 群里的话不替代链上 releaseV2',
1686
+ };
1687
+ return {
1688
+ envelope: okEnvelope('OK', `已把公告 ${a.announcementId} 发进群 (${a.capability})`, data, [a.announcementId], null),
1689
+ human: [
1690
+ title(head),
1691
+ line('公告号', a.announcementId),
1692
+ line('capability', a.capability),
1693
+ line('预算', a.budget ? `≤ ${a.budget.maxAmount} 原子 (${a.budget.currency} @ ${a.budget.network})` : '(无)'),
1694
+ line('期号', opt(flags, '--round') || '(未标)'),
1695
+ line('判据摘要', opt(flags, '--criteria') || `未声明 (只放任务书摘要 sha256:${String(a.instructionDigest || '').slice(0, 16)}…)`),
1696
+ line('群', `${ctx.group.name || '(无名)'} · ${ctx.group.groupId}`),
1697
+ line('发送者标记', `${ctx.fromTag} (${ctx.fromSource === 'identity' ? '本机身份派生假名' : '--from 显式'}; 原始 DID 未进群)`),
1698
+ line('本次执行/付款', '没有 (公告入群只是过程留痕)'),
1699
+ '',
1700
+ hint(`下一步: 别人 bolloon task claim ${a.announcementId} --group <群链接>; 回看留痕: bolloon task trail --group <群链接>`),
1701
+ ].join('\n'),
1702
+ };
1703
+ }
1704
+ /** `bolloon task trail --group <群链接|groupId>` —— 从群消息读回本期过程痕迹并汇总成时间线 */
1705
+ async function taskTrail(flags) {
1706
+ const head = 'bolloon task trail';
1707
+ const ctx = await requireGroup(head, flags);
1708
+ if (!ctx.ok)
1709
+ return ctx.result;
1710
+ const TG = await import('../../agents/task-group.js');
1711
+ const limitRaw = opt(flags, '--limit');
1712
+ const limit = limitRaw === undefined ? 300 : Number(limitRaw);
1713
+ if (limitRaw !== undefined && (!Number.isFinite(limit) || limit < 1)) {
1714
+ return {
1715
+ envelope: failEnvelope('INVALID_ARGUMENT', `--limit 必须是正数 (收到 ${limitRaw})`, { limit: limitRaw }, [], 'needs_human'),
1716
+ human: TASK_USAGE,
1717
+ };
1718
+ }
1719
+ let summary;
1720
+ try {
1721
+ summary = await TG.readTrail(ctx.group.groupId, { limit, announcementId: opt(flags, '--announcement-id') });
1722
+ }
1723
+ catch (e) {
1724
+ return {
1725
+ envelope: failEnvelope('TRANSPORT_FAILED', `群消息读不出来: ${String(e?.message || e).slice(0, 160)}`, { group: ctx.group.groupId, read: false, localFallback: false }, [], 'needs_human'),
1726
+ human: `${title(head)}\n 群消息读不出来: ${String(e?.message || e).slice(0, 160)}\n\n${hint('群不可达时如实报错: 不用本地缓存假装读过群')}`,
1727
+ };
1728
+ }
1729
+ const data = {
1730
+ group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
1731
+ count: summary.count,
1732
+ byKind: summary.byKind,
1733
+ announcements: summary.announcements,
1734
+ flags: summary.flags,
1735
+ inconsistencies: summary.inconsistencies,
1736
+ redacted: summary.redacted,
1737
+ ignoredMessages: summary.ignoredMessages,
1738
+ timeline: summary.entries.map(trailEntryView),
1739
+ limit,
1740
+ announcementFilter: opt(flags, '--announcement-id') || null,
1741
+ readOnly: true,
1742
+ localFallback: false,
1743
+ note: '时间线只汇总群里真发过的事实 (公告/接单/交付/初筛/终审); 没发过的环节不会凭空出现; 发送者只显示短标记 (原始 DID 不在输出里)',
1744
+ };
1745
+ const label = (k) => `${TG.TRAIL_KIND_LABEL[k]}=${summary.byKind[k]}`;
1746
+ const human = [
1747
+ title(`${head} (${ctx.group.name || ctx.group.groupId})`),
1748
+ line('条目', `${summary.count} 条 (${TG.TRAIL_KINDS.map(label).join(' · ')})`),
1749
+ ...(summary.announcements.length ? [line('涉及公告', summary.announcements.join(' · '))] : []),
1750
+ ...(summary.inconsistencies.length ? [line('事实矛盾/遮蔽', summary.inconsistencies.join(' · '))] : []),
1751
+ ...(summary.redacted.length ? [line('被遮蔽的字段', summary.redacted.join(' · ') + ' (别人发的消息里含标识符形态, 读回时已遮蔽)')] : []),
1752
+ '',
1753
+ ...(summary.entries.length
1754
+ ? summary.entries.map((e) => ` ${TG.formatTrailLine(e)}`)
1755
+ : [' (群里还没有本期的过程痕迹: 先用 bolloon task announce --group … 发公告)']),
1756
+ '',
1757
+ ` ${String(data.note)}`,
1758
+ ].join('\n');
1759
+ return {
1760
+ envelope: okEnvelope('OK', summary.count
1761
+ ? `群里本期 ${summary.count} 条过程痕迹 (${summary.announcements.length} 个公告)`
1762
+ : '群里本期没有过程痕迹 (群里真没有, 不是读失败)', data, summary.announcements, null),
1763
+ human,
1764
+ };
1765
+ }
1766
+ /** `bolloon task post --kind deliver|screen|final --group <…> --announcement-id <id>` —— 交付/初筛/终审的群消息 */
1767
+ async function taskPost(flags) {
1768
+ const head = 'bolloon task post';
1769
+ const kind = String(opt(flags, '--kind') || '').trim().toLowerCase();
1770
+ if (!['deliver', 'screen', 'final'].includes(kind)) {
1771
+ return {
1772
+ envelope: failEnvelope('INVALID_ARGUMENT', `--kind 必须是 deliver|screen|final (收到 ${kind || '(空)'})`, {
1773
+ accepted: ['--kind deliver --hash sha256:<hex>', '--kind screen --checks "渠道结构=pass,价格带=fail"', '--kind final --verdict accept'],
1774
+ claimKindNote: kind === 'claim'
1775
+ ? '接单痕迹不用 task post 发: 用 `bolloon task claim <announcementId> --group <群>` —— 这样"接单痕迹"必定对应一笔真认领账, 不会出现没有认领者却挂在群里的接单声明 (反过来也不会有认领了却没有任何群痕迹的静默)'
1776
+ : null,
1777
+ why: '群里只认这四种过程事实: 公告 / 交付 / 初筛 / 终审 (接单走 task claim)',
1778
+ }, [], 'needs_human'),
1779
+ human: TASK_USAGE,
1780
+ };
1781
+ }
1782
+ const ctx = await requireGroup(head, flags);
1783
+ if (!ctx.ok)
1784
+ return ctx.result;
1785
+ const TG = await import('../../agents/task-group.js');
1786
+ const bytesRaw = opt(flags, '--bytes');
1787
+ const built = TG.buildPostMessage({
1788
+ kind,
1789
+ announcementId: opt(flags, '--announcement-id'),
1790
+ round: opt(flags, '--round'),
1791
+ price: opt(flags, '--price'),
1792
+ hash: opt(flags, '--hash'),
1793
+ bytes: bytesRaw === undefined ? null : Number(bytesRaw),
1794
+ checksRaw: opt(flags, '--checks'),
1795
+ verdict: opt(flags, '--verdict'),
1796
+ });
1797
+ if (!built.ok) {
1798
+ return {
1799
+ envelope: failEnvelope(built.code, built.message, { ...built.detail, kind, group: ctx.group, sent: false, localFallback: false }, [], 'needs_human'),
1800
+ human: `${title(head)}\n ${built.message}\n\n${TASK_USAGE.split('选项:')[0].trim().split('\n').slice(-6).join('\n')}`,
1801
+ };
1802
+ }
1803
+ const sent = await TG.sendTrailMessage(ctx.group.groupId, built.text, ctx.fromTag);
1804
+ if (!sent.ok) {
1805
+ const violations = sent.violations || null;
1806
+ return {
1807
+ envelope: failEnvelope(violations ? 'POLICY_DENIED' : 'TRANSPORT_FAILED', violations ? `过程痕迹命中隐私红线, 拒绝发进群: ${violations.map((v) => v.rule).join(', ')}` : `群消息没发出去: ${sent.error}`, { kind, group: ctx.group.groupId, message: built.text, sent: false, violations, localFallback: false }, [], 'needs_human'),
1808
+ human: [
1809
+ title(head),
1810
+ ` ${violations ? '命中隐私红线, 拒发' : '没发出去'}: ${sent.error}`,
1811
+ ...(violations ? violations.map((v) => ` · ${v.rule} — ${v.why} (${v.masked}, ${v.chars} 字符)`) : []),
1812
+ '',
1813
+ hint('群里不放钱包地址/DID/peerId/multiaddr/IP/私钥形态; 换个不含标识符的短事实重发'),
1814
+ ].join('\n'),
1815
+ };
1816
+ }
1817
+ const onBoard = !!TG.readBoardAnnouncement(String(opt(flags, '--announcement-id') || ''));
1818
+ const data = {
1819
+ kind,
1820
+ announcementId: opt(flags, '--announcement-id'),
1821
+ message: sent.text,
1822
+ checks: built.checks ? built.checks : null,
1823
+ group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
1824
+ sender: { tag: ctx.fromTag, source: ctx.fromSource, didPrinted: false },
1825
+ announcementOnLocalBoard: onBoard,
1826
+ /** 群里只是留痕: 不执行、不付款、也不标 verified (终审结论不替代链上 release) */
1827
+ executed: false,
1828
+ paid: false,
1829
+ fundsMoved: false,
1830
+ verified: false,
1831
+ localFallback: false,
1832
+ note: kind === 'deliver'
1833
+ ? '交付痕迹只贴内容哈希 (群里没有正文); 真交接仍走 bolloon task send → 对方 task accept'
1834
+ : kind === 'screen'
1835
+ ? '初筛通过 ≠ 已验收: 终审结论另发 (--kind final); 线上一分钱都没动'
1836
+ : '终审结论只是过程事实: 结算仍只在链上 (群里的话不替代 releaseV2; 争议期不自动重付/不标 verified)',
1837
+ };
1838
+ return {
1839
+ envelope: okEnvelope('OK', `已把${TG.TRAIL_KIND_LABEL[kind]}发进群 (${opt(flags, '--announcement-id')})`, data, [String(opt(flags, '--announcement-id'))], null),
1840
+ human: [
1841
+ title(`${head} --kind ${kind}`),
1842
+ line('公告号', String(opt(flags, '--announcement-id'))),
1843
+ line('群消息', sent.text),
1844
+ ...(built.checks ? [line('初筛逐条', built.checks.map((c) => `${c.name}=${c.result}`).join(' · '))] : []),
1845
+ line('群', `${ctx.group.name || '(无名)'} · ${ctx.group.groupId}`),
1846
+ line('发送者标记', `${ctx.fromTag} (原始 DID 未进群)`),
1847
+ line('本地板上有这条公告吗', onBoard ? '有' : '没有 (本机不是买方; 只发痕迹不假装认识这条公告)'),
1848
+ line('本次执行/付款/验真', '都没有 (群消息只是过程留痕)'),
1849
+ '',
1850
+ ` ${data.note}`,
1851
+ ].join('\n'),
1852
+ };
1853
+ }
1854
+ // ── 群管理 (group create|join|list|link|leave, 2026-09-24) ────────────────────
1855
+ //
1856
+ // 为什么补这一段 (真缺口): `createGroup` / `joinGroup` / `listGroups` 一直只存在于
1857
+ // `src/agents/gateway-group.ts`, **CLI 里没有任何入口** —— 外部接单者拿到群链接后
1858
+ // 无法自助入群, 只能等对方把话转达。这里把它们接出来 (薄包装, 不重实现密钥学/存储),
1859
+ // 并补 `link` (取回邀请链接) 与 `leave` (退群)。
1860
+ //
1861
+ // 脱敏口径 (与群消息同一张表, 只去掉 orbitdb 那一档 —— 见 `TG.NODE_IDENTITY_RULES`):
1862
+ // · 原始 DID · 钱包地址 · peerId · 节点 multiaddr (`/ip4` `/p2p` …) · IPv4/IPv6 · URL · 邮箱
1863
+ // —— 一律不出现在群管理输出里;
1864
+ // · 群 store 地址 (`/orbitdb/zdpu…`) 是**群自己的公开标识**, 邀请链接必须能打印
1865
+ // → `list` 只出短事实 (groupId/群名/时间), 要链接就显式 `bolloon task group link <groupId>`。
1866
+ // · `create` 的 `--name` 与 `list` 的**输出**都再过一遍 `scanNodeIdentity`: 命中 → 拒 (不静默脱敏)。
1867
+ const GROUP_ACTIONS = ['create', 'join', 'list', 'link', 'leave'];
1868
+ const GROUP_USAGE = `
1869
+ ${title('bolloon task group')}
1870
+ bolloon task group create --name <群名> [--from <短显示名>] [--json]
1871
+ 建一个可被邀请的群 (OrbitDB events store, ACL write:['*'] = 成员可广播);
1872
+ 打印**邀请链接** (把它发给要入群的人) 与 groupId; 落 ~/.bolloon/gateway-groups.json (重启不忘)
1873
+ bolloon task group join <群链接|groupId> [--json]
1874
+ 用链接自助入群 (幂等: 已在群里 → already=true, 仍然 exit 0);
1875
+ 拿不到区块时**大声失败** (TRANSPORT_FAILED + storeCode=STORE_UNREACHABLE), 绝不把"读不到"说成"群里没东西"
1876
+ bolloon task group list [--json]
1877
+ 本机已加入的群 (groupId · 群名 · 加入时间); 没有群 → 空列表 (不是错误)
1878
+ bolloon task group link <groupId|群名> [--json]
1879
+ 显式取回邀请链接 (list 里不放链接: 群 store 地址只在你要分享时打印)
1880
+ bolloon task group leave <groupId|群名> [--json]
1881
+ 退群 (只摘本机记录; 群 store 是公共 append-only, 别人那边不会因为你退出而改)
1882
+
1883
+ 选项: --json · --quiet; --name/--from 见上
1884
+ 说明: 群管理输出不含原始 DID / 钱包地址 / peerId / multiaddr / IP (脱敏口径与群消息同源);
1885
+ 跨机**互相看见消息**仍需区块/日志复制 (bitswap + block broker, 不在本命令范围);
1886
+ 本机跨进程重开同一群链接读回既有消息已支持。
1887
+ `;
1888
+ /**
1889
+ * 群管理输出过一遍节点身份闸 (命中 → 拒输出并如实报, 不静默脱敏后再当成没事)。
1890
+ *
1891
+ * `allow` = 本命令**故意打印**的群自身标识 (store 地址 / 邀请链接): 扫之前按**精确串**摘掉。
1892
+ * 为什么要摘 (2026-09-24 想清楚的一个真坑): 群 store 地址是 base58 的 CID (`zdpu…`), 而 base58
1893
+ * 字母表里**同时有 `Q` 和 `m`** —— 几十位里偶然出现 `Qm`, 后面又跟够 30 个 base58 字符时,
1894
+ * 就会撞上 `Qm…` 这个 peerId 形状 → 建群**随机**被判成"输出泄漏了 peerId"。
1895
+ * 摘掉的是"我们自己刚给出的那一个串", **规则一个字没放宽**: 别处再出现任何
1896
+ * DID / 钱包 / peerId / 节点 multiaddr / IP 形状, 照样命中。
1897
+ */
1898
+ async function guardGroupOutput(payload, allow = []) {
1899
+ const TG = await import('../../agents/task-group.js');
1900
+ let text = typeof payload === 'string' ? payload : JSON.stringify(payload);
1901
+ const tokens = allow.filter((t) => typeof t === 'string' && t.length > 0);
1902
+ for (const t of tokens)
1903
+ text = text.split(t).join('');
1904
+ const hits = TG.scanNodeIdentity(text);
1905
+ return hits.length ? { ok: false, hits } : { ok: true, exempted: tokens.length };
1906
+ }
1907
+ /** 群管理输出命中节点身份闸时的统一失败 (如实列出命中规则, 不回显被命中的原文) */
1908
+ function groupGuardFail(head, hits, extra = {}) {
1909
+ return {
1910
+ envelope: failEnvelope('POLICY_DENIED', `本机群列表里含节点身份形态, 拒绝输出 (${hits.map((h) => h.rule).join(', ')}) —— 已遮住原文, 不把标识符打出来`, { violations: hits, ...extra }, [], 'needs_human'),
1911
+ human: [
1912
+ title(head),
1913
+ ` 拒绝输出: 本机群记录里含 ${hits.map((h) => h.rule).join(', ')}`,
1914
+ ...hits.map((h) => ` · ${h.rule} — ${h.why} (${h.masked}, ${h.chars} 字符)`),
1915
+ '',
1916
+ hint('群名/记录里被写进了标识符 → 改成不含标识符的名字 (本命令不静默脱敏: 脱敏后的群名不是你要看的那个)'),
1917
+ ].join('\n'),
1918
+ };
1919
+ }
1920
+ /** 群上下文解析: groupId 或群名 → 本机已加入的群 (都没有 → NOT_FOUND, 不猜) */
1921
+ async function resolveLocalGroup(raw) {
1922
+ const GG = await import('../../agents/gateway-group.js');
1923
+ const groups = await GG.listGroups();
1924
+ const s = String(raw || '').trim();
1925
+ const hit = groups.find((g) => g.id === s) || groups.find((g) => g.name === s) || null;
1926
+ if (hit)
1927
+ return { ok: true, group: hit };
1928
+ return {
1929
+ ok: false,
1930
+ result: {
1931
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${s ? `${s.slice(0, 24)}${s.length > 24 ? '…' : ''}` : '(空)'} (既不是已加入群的 groupId, 也不是群名)`, {
1932
+ joinedCount: groups.length,
1933
+ howTo: '入群: bolloon task group join <群链接>; 看已加入的群: bolloon task group list',
1934
+ }, [], 'needs_human'),
1935
+ human: '',
1936
+ },
1937
+ };
1938
+ }
1939
+ async function taskGroup(flags) {
1940
+ const action = String(flags.positionals[1] ?? '').trim();
1941
+ // 用查表而不是 switch: `case 'create':` 这种字面量不属于 task 的**顶层**子命令,
1942
+ // 免得将来有人把这段挪到 taskCommand 前面时被源级一致性门 (task-subcommands.test.ts) 误认。
1943
+ const table = {
1944
+ create: taskGroupCreate,
1945
+ join: taskGroupJoin,
1946
+ list: taskGroupList,
1947
+ link: taskGroupLink,
1948
+ leave: taskGroupLeave,
1949
+ };
1950
+ const fn = table[action];
1951
+ if (fn)
1952
+ return fn(flags);
1953
+ return {
1954
+ envelope: failEnvelope('INVALID_ARGUMENT', action ? `未知 group 动作: ${action}` : '缺少 group 动作 (create | join | list | link | leave)', { usage: plain(GROUP_USAGE.trim()), accepted: [...GROUP_ACTIONS] }, [], 'needs_human'),
1955
+ human: GROUP_USAGE,
1956
+ };
1957
+ }
1958
+ /** `task group create --name <群名>` —— 建群 + 打印邀请链接 */
1959
+ async function taskGroupCreate(flags) {
1960
+ const head = 'bolloon task group create';
1961
+ const TG = await import('../../agents/task-group.js');
1962
+ const GG = await import('../../agents/gateway-group.js');
1963
+ const name = String(opt(flags, '--name') ?? '').trim();
1964
+ if (!name) {
1965
+ return {
1966
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --name (群名)', { accepted: ['--name 测试群'], usage: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
1967
+ human: GROUP_USAGE,
1968
+ };
1969
+ }
1970
+ // 群名会进群消息与 list 输出 → 先过节点身份闸 (标识符形状的名字一律拒, 不静默改名)
1971
+ const nameHits = TG.scanNodeIdentity(name);
1972
+ if (nameHits.length) {
1973
+ return {
1974
+ envelope: failEnvelope('POLICY_DENIED', `群名含节点身份形态, 拒绝建群 (${nameHits.map((h) => h.rule).join(', ')})`, { violations: nameHits, accepted: ['--name 测试群'] }, [], 'needs_human'),
1975
+ human: `${title(head)}\n 群名不能含 ${nameHits.map((h) => h.rule).join(', ')}\n\n${hint('群名是公开事实 (会进群消息与 list): 换个不含标识符的名字')}`,
1976
+ };
1977
+ }
1978
+ const s = await TG.resolveSenderTag(opt(flags, '--from'));
1979
+ if (!s.ok) {
1980
+ return {
1981
+ envelope: failEnvelope(s.code, s.message, { ...s.detail, group: { name }, created: false }, [], 'needs_human'),
1982
+ human: `${title(head)}\n 群主标记定不下来: ${s.message}\n\n${hint('建群会写一条欢迎消息 (要一个诚实的发送者标记): 先 bolloon identity init 或 --from <短显示名>')}`,
1983
+ };
1984
+ }
1985
+ const r = await GG.createGroup(name, { from: s.tag });
1986
+ if (!r.ok || !r.group) {
1987
+ return {
1988
+ envelope: failEnvelope('TRANSPORT_FAILED', `建群失败: ${r.error ?? '未知原因'}`, { group: { name }, created: false, storeReachable: false }, [], 'needs_human'),
1989
+ human: `${title(head)}\n 建群失败: ${r.error ?? '未知原因'}\n\n${hint('建群要写 OrbitDB store: 落盘目录不可写 / store 打不开都会在这里如实失败')}`,
1990
+ };
1991
+ }
1992
+ const data = {
1993
+ group: { id: r.group.id, name: r.group.name, link: r.group.link, createdAt: r.group.createdAt },
1994
+ sender: { tag: s.tag, source: s.source, didPrinted: false },
1995
+ file: '~/.bolloon/gateway-groups.json',
1996
+ acl: { write: ['*'], note: '成员可广播: 入群的人都能往群里发消息 (建群时写进 store manifest)' },
1997
+ created: true,
1998
+ executed: false,
1999
+ paid: false,
2000
+ localFallback: false,
2001
+ };
2002
+ const guard = await guardGroupOutput(data, [r.group.id, r.group.link]);
2003
+ if (!guard.ok)
2004
+ return groupGuardFail(head, guard.hits, { group: { name }, created: true });
2005
+ data.nodeIdentityGuard = { scanned: true, exemptedGroupTokens: guard.exempted };
2006
+ return {
2007
+ envelope: okEnvelope('OK', `已建群「${r.group.name}」(${r.group.id.slice(0, 12)}…)`, data, [r.group.id], null),
2008
+ human: [
2009
+ title(head),
2010
+ line('群名', r.group.name),
2011
+ line('groupId', r.group.id),
2012
+ line('邀请链接', r.group.link),
2013
+ line('群主标记', `${s.tag} (${s.source === 'identity' ? '本机身份派生假名' : '--from 显式'}; 原始 DID 未进群)`),
2014
+ line('本机记录', '~/.bolloon/gateway-groups.json (重启后仍记得这个群)'),
2015
+ '',
2016
+ hint(`把链接发给要入群的人: bolloon task group join "<链接>"`),
2017
+ hint(`回看过程痕迹: bolloon task trail --group <链接> · 取回链接: bolloon task group link ${r.group.id.slice(0, 12)}…`),
2018
+ '',
2019
+ ' 跨机**互相看见消息**仍需区块/日志复制 (bitswap + block broker, 不在本命令范围);',
2020
+ ' 本机跨进程用同一链接重开读回既有消息已支持。',
2021
+ ].join('\n'),
2022
+ };
2023
+ }
2024
+ /** `task group join <群链接|groupId>` —— 自助入群 (幂等) */
2025
+ async function taskGroupJoin(flags) {
2026
+ const head = 'bolloon task group join';
2027
+ const GG = await import('../../agents/gateway-group.js');
2028
+ const raw = String(flags.positionals[2] ?? '').trim() || String(opt(flags, '--group') ?? '').trim();
2029
+ if (!raw) {
2030
+ return {
2031
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少群链接或 groupId', { accepted: ['bolloon task group join orbitdb:///orbitdb/<store>?type=group&name=<群名>', 'bolloon task group join <groupId>'], usage: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2032
+ human: GROUP_USAGE,
2033
+ };
2034
+ }
2035
+ // 链接 → 真入群 (打开 store); groupId/群名 → 必须已经在本地列表里 (不能凭 id 凭空入群)
2036
+ if (!/^orbitdb:\/\//i.test(raw)) {
2037
+ // 别的 scheme (`https://…` / `ipfs://…`): 这是**不是群链接**, 不是"本机没有这个群" —— 分开报
2038
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
2039
+ return {
2040
+ envelope: failEnvelope('INVALID_ARGUMENT', `这不是群链接 (${raw.split(':')[0]}://…) —— 群链接形如 orbitdb:///orbitdb/<store>?type=group&name=<群名>`, { scheme: raw.split(':')[0], accepted: ['bolloon task group join orbitdb:///orbitdb/<store>?type=group&name=<群名>'] }, [], 'needs_human'),
2041
+ human: `${title(head)}\n 这不是群链接: ${raw.slice(0, 12)}…\n\n${hint('群邀请链接形如 orbitdb:///orbitdb/<store>?type=group&name=<群名> (bolloon task group create 会打印它)')}`,
2042
+ };
2043
+ }
2044
+ const loc = await resolveLocalGroup(raw);
2045
+ if (!loc.ok) {
2046
+ return {
2047
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw} —— 用 id/群名**不能**入群, 只有群链接才能入群`, { accepted: 'bolloon task group join <群链接>', howTo: '拿到群链接后再试; 看已加入的群: bolloon task group list' }, [], 'needs_human'),
2048
+ human: `${title(head)}\n 本机没有这个群: ${raw}\n\n${hint('id/群名只能引用**已加入**的群; 要入群请用邀请链接')}`,
2049
+ };
2050
+ }
2051
+ const data = { group: { id: loc.group.id, name: loc.group.name }, already: true, joined: true, storeOpened: false };
2052
+ return {
2053
+ envelope: okEnvelope('OK', `已在群里「${loc.group.name}」(${loc.group.id.slice(0, 12)}…)`, data, [loc.group.id], null),
2054
+ human: [title(head), line('群名', loc.group.name), line('groupId', loc.group.id), line('状态', 'already=true (早就加入过, 没有重复入群)')].join('\n'),
2055
+ };
2056
+ }
2057
+ const r = await GG.joinGroup(raw);
2058
+ if (!r.ok || !r.group) {
2059
+ const unreachable = r.code === 'STORE_UNREACHABLE';
2060
+ return {
2061
+ envelope: failEnvelope(unreachable ? 'TRANSPORT_FAILED' : 'INVALID_ARGUMENT', unreachable ? `群 store 不可达: ${r.error ?? '未知原因'}` : `不是有效的群链接: ${r.error ?? '未知原因'}`, {
2062
+ storeCode: r.code ?? null,
2063
+ joined: false,
2064
+ storeOpened: false,
2065
+ localFallback: false,
2066
+ why: unreachable
2067
+ ? '链接能解析但区块不在本机且没有 block broker → 如实失败 (绝不把"读不到"说成"群里没东西")'
2068
+ : '链接不是 orbitdb://…?type=group&name=… 形状 → 拒绝 (不猜你想进哪个群)',
2069
+ }, [], 'needs_human'),
2070
+ human: [
2071
+ title(head),
2072
+ ` ${unreachable ? '群 store 不可达' : '链接非法'}: ${r.error ?? ''}`,
2073
+ '',
2074
+ hint(unreachable
2075
+ ? '区块不在本机: 跨机同步 (bitswap/block broker) 还没接; 同机请确认对方的区块目录可读'
2076
+ : '群链接形如 orbitdb:///orbitdb/<store>?type=group&name=<群名>'),
2077
+ ].join('\n'),
2078
+ };
2079
+ }
2080
+ const data = {
2081
+ group: { id: r.group.id, name: r.group.name },
2082
+ already: !!r.already,
2083
+ joined: true,
2084
+ storeOpened: true,
2085
+ linkKeptLocally: true,
2086
+ note: '入群只改本机群列表 + 打开群 store: 不执行任务、不付款',
2087
+ };
2088
+ const guard = await guardGroupOutput(data, [r.group.id]);
2089
+ if (!guard.ok)
2090
+ return groupGuardFail(head, guard.hits, { joined: true });
2091
+ data.nodeIdentityGuard = { scanned: true, exemptedGroupTokens: guard.exempted };
2092
+ return {
2093
+ envelope: okEnvelope('OK', r.already ? `已在群里「${r.group.name}」(${r.group.id.slice(0, 12)}…)` : `已入群「${r.group.name}」(${r.group.id.slice(0, 12)}…)`, data, [r.group.id], null),
2094
+ human: [
2095
+ title(head),
2096
+ line('群名', r.group.name),
2097
+ line('groupId', r.group.id),
2098
+ line('状态', r.already ? 'already=true (幂等: 早就加入过)' : '新加入'),
2099
+ '',
2100
+ hint(`现在可以: bolloon task trail --group ${r.group.id.slice(0, 12)}… / bolloon task claim <announcementId> --group <链接>`),
2101
+ ].join('\n'),
2102
+ };
2103
+ }
2104
+ /** `task group list [--json]` —— 本机已加入的群 (脱敏: 不出链接/地址/DID/peerId/IP) */
2105
+ async function taskGroupList(flags) {
2106
+ const head = 'bolloon task group list';
2107
+ const GG = await import('../../agents/gateway-group.js');
2108
+ const groups = await GG.listGroups();
2109
+ const rows = groups.map((g) => ({ id: g.id, name: g.name, createdAt: g.createdAt ?? null }));
2110
+ const data = {
2111
+ count: rows.length,
2112
+ groups: rows,
2113
+ note: '只出群标识/群名/加入时间: 邀请链接与 store 地址不进 list (要链接用 bolloon task group link <groupId>); '
2114
+ + '本输出不含原始 DID/钱包地址/peerId/节点 multiaddr/IP',
2115
+ };
2116
+ const guard = await guardGroupOutput(data, rows.map((r) => r.id));
2117
+ if (!guard.ok)
2118
+ return groupGuardFail(head, guard.hits, { count: rows.length });
2119
+ data.nodeIdentityGuard = { scanned: true, exemptedGroupTokens: guard.exempted };
2120
+ return {
2121
+ envelope: okEnvelope('OK', rows.length ? `本机已加入 ${rows.length} 个群` : '本机还没有加入任何群 (空列表, 不是错误)', data, rows.map((r) => r.id), null),
2122
+ human: [
2123
+ title(head),
2124
+ ...(rows.length
2125
+ ? rows.map((r) => ` ${String(r.id).slice(0, 12)}… ${String(r.name ?? '(无名)').padEnd(20)} ${r.createdAt ?? '(无时间)'}`)
2126
+ : [' (本机还没有加入任何群)']),
2127
+ '',
2128
+ hint('建群: bolloon task group create --name <群名> · 入群: bolloon task group join <群链接> · 取链接: bolloon task group link <groupId>'),
2129
+ ].join('\n'),
2130
+ };
2131
+ }
2132
+ /** `task group link <groupId|群名>` —— 显式取回邀请链接 */
2133
+ async function taskGroupLink(flags) {
2134
+ const head = 'bolloon task group link';
2135
+ const raw = String(flags.positionals[2] ?? '').trim() || String(opt(flags, '--group') ?? '').trim();
2136
+ if (!raw) {
2137
+ return {
2138
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 groupId (或群名)', { accepted: ['bolloon task group link <groupId>'], usage: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2139
+ human: GROUP_USAGE,
2140
+ };
2141
+ }
2142
+ const loc = await resolveLocalGroup(raw);
2143
+ if (!loc.ok) {
2144
+ return {
2145
+ envelope: failEnvelope('NOT_FOUND', `本机没有这个群: ${raw} (取不到链接)`, { howTo: '看已加入的群: bolloon task group list' }, [], 'needs_human'),
2146
+ human: `${title(head)}\n 本机没有这个群: ${raw}\n\n${hint('先 bolloon task group list 看有哪些群 (或 join 入群)')}`,
2147
+ };
2148
+ }
2149
+ const data = { group: { id: loc.group.id, name: loc.group.name, link: loc.group.link } };
2150
+ const guard = await guardGroupOutput(data, [loc.group.id, loc.group.link]);
2151
+ if (!guard.ok)
2152
+ return groupGuardFail(head, guard.hits, { group: { name: loc.group.name } });
2153
+ data.nodeIdentityGuard = { scanned: true, exemptedGroupTokens: guard.exempted };
2154
+ return {
2155
+ envelope: okEnvelope('OK', `群「${loc.group.name}」的邀请链接已取出`, data, [loc.group.id], null),
2156
+ human: [title(head), line('群名', loc.group.name), line('groupId', loc.group.id), line('邀请链接', loc.group.link)].join('\n'),
2157
+ };
2158
+ }
2159
+ /** `task group leave <groupId|群名>` —— 退群 (只摘本机记录) */
2160
+ async function taskGroupLeave(flags) {
2161
+ const head = 'bolloon task group leave';
2162
+ const GG = await import('../../agents/gateway-group.js');
2163
+ const raw = String(flags.positionals[2] ?? '').trim() || String(opt(flags, '--group') ?? '').trim();
2164
+ if (!raw) {
2165
+ return {
2166
+ envelope: failEnvelope('INVALID_ARGUMENT', '缺少 groupId (或群名)', { accepted: ['bolloon task group leave <groupId>'], usage: plain(GROUP_USAGE.trim()) }, [], 'needs_human'),
2167
+ human: GROUP_USAGE,
2168
+ };
2169
+ }
2170
+ const r = await GG.leaveGroup(raw);
2171
+ if (!r.ok) {
2172
+ return {
2173
+ envelope: failEnvelope('NOT_FOUND', `退群失败: ${r.error ?? '未知原因'}`, { removed: false }, [], 'needs_human'),
2174
+ human: `${title(head)}\n ${r.error ?? '未知原因'}\n\n${hint('看已加入的群: bolloon task group list')}`,
2175
+ };
2176
+ }
2177
+ return {
2178
+ envelope: okEnvelope('OK', `已退出群 ${String(r.removed).slice(0, 12)}… (只摘本机记录)`, { removed: true, groupId: r.removed, localOnly: true, note: '群 store 是公共 append-only: 别人那边不会因为你退出而改 (本命令没有踢人/解散权限, 也不假装有)' }, [String(r.removed)], null),
2179
+ human: [
2180
+ title(head),
2181
+ line('已退出', `${String(r.removed).slice(0, 12)}…`),
2182
+ line('范围', '只摘本机记录 (~/.bolloon/gateway-groups.json)'),
2183
+ '',
2184
+ ` 群 store 是公共 append-only: 别人那边不会因为你退出而改 (本命令没有踢人/解散权限, 也不假装有)`,
2185
+ ].join('\n'),
2186
+ };
2187
+ }
1219
2188
  // ── 仍未实现的两个 (任务书没点名; 如实报, 不假装) ────────────────────────────
1220
2189
  async function taskComplete(_flags) {
1221
2190
  const env = notImplemented('task complete 还没实现: 没有任务状态存储可写 (交易层有非法迁移保护, 不许绕过)', {