@bolloon/bolloon-agent 0.4.33 → 0.5.1

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 (214) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +580 -40
  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/model-policy.js +651 -0
  31. package/dist/agents/network-pulse.js +304 -18
  32. package/dist/agents/pi-sdk-tools.js +84 -33
  33. package/dist/agents/pi-sdk.js +157 -42
  34. package/dist/agents/run-store.js +71 -5
  35. package/dist/agents/skill-readiness.js +52 -10
  36. package/dist/agents/subagent-manager.js +209 -3
  37. package/dist/agents/task/task-runner.js +17 -4
  38. package/dist/agents/task-group.js +8 -51
  39. package/dist/agents/task-public-text.js +89 -0
  40. package/dist/agents/workflow-pivot-loop.js +19 -12
  41. package/dist/cli/clipboard.js +65 -0
  42. package/dist/cli/commands/index.js +1 -0
  43. package/dist/cli/commands/tasks.js +350 -2
  44. package/dist/cli/identity-command.js +177 -0
  45. package/dist/cli/ink-app.js +145 -17
  46. package/dist/cli/log-gate.js +386 -0
  47. package/dist/cli/mention-data.js +2 -0
  48. package/dist/cli/model-selector.js +538 -0
  49. package/dist/cli/reply-hygiene.js +54 -0
  50. package/dist/cli/setup-wizard.js +757 -69
  51. package/dist/cli/startup-notice.js +160 -0
  52. package/dist/cli/stores.js +22 -1
  53. package/dist/cli/theme.js +49 -0
  54. package/dist/cli/tui-select.js +970 -0
  55. package/dist/cli/update-commands.js +150 -24
  56. package/dist/cli-entry.js +137 -20
  57. package/dist/electron-build/agents/contacts/chain.js +26 -6
  58. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  59. package/dist/electron-build/agents/execution-supervisor.js +578 -38
  60. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  61. package/dist/electron-build/agents/external-events.js +34 -5
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/goal-criteria.js +10 -3
  64. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  65. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  66. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  67. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  68. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  69. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  70. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  71. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  72. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  73. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  74. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  75. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  76. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  86. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  87. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  88. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  89. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  90. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  91. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  92. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  93. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  94. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  95. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  96. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  97. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  98. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  99. package/dist/electron-build/agents/goal-store.js +35 -1
  100. package/dist/electron-build/agents/goal-store.js.map +1 -1
  101. package/dist/electron-build/agents/model-policy.js +706 -0
  102. package/dist/electron-build/agents/model-policy.js.map +1 -0
  103. package/dist/electron-build/agents/network-pulse.js +307 -19
  104. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  105. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  106. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  107. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  108. package/dist/electron-build/agents/pi-sdk.js +156 -41
  109. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  110. package/dist/electron-build/agents/run-store.js +75 -6
  111. package/dist/electron-build/agents/run-store.js.map +1 -1
  112. package/dist/electron-build/agents/skill-readiness.js +52 -9
  113. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  114. package/dist/electron-build/agents/task/task-runner.js +16 -3
  115. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  116. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  117. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  118. package/dist/electron-build/llm/config-store.js +146 -6
  119. package/dist/electron-build/llm/config-store.js.map +1 -1
  120. package/dist/electron-build/llm/connection-probe.js +764 -0
  121. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  122. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  123. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  124. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  125. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  126. package/dist/electron-build/llm/model-catalog.js +778 -0
  127. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  128. package/dist/electron-build/llm/model-discovery.js +1202 -0
  129. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  130. package/dist/electron-build/llm/model-selection.js +1315 -0
  131. package/dist/electron-build/llm/model-selection.js.map +1 -0
  132. package/dist/electron-build/llm/pi-ai.js +70 -11
  133. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  134. package/dist/electron-build/llm/provider-catalog.js +969 -0
  135. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  136. package/dist/electron-build/llm/provider-registry.js +950 -0
  137. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  138. package/dist/electron-build/llm/tool-name.js +266 -0
  139. package/dist/electron-build/llm/tool-name.js.map +1 -0
  140. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  141. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  142. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  143. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  144. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  145. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  146. package/dist/electron-build/utils/dual-source.js +347 -0
  147. package/dist/electron-build/utils/dual-source.js.map +1 -0
  148. package/dist/electron-build/utils/update-manager.js +328 -104
  149. package/dist/electron-build/utils/update-manager.js.map +1 -1
  150. package/dist/electron-build/utils/update-state.js +9 -11
  151. package/dist/electron-build/utils/update-state.js.map +1 -1
  152. package/dist/electron-build/utils/version-identity.js +193 -0
  153. package/dist/electron-build/utils/version-identity.js.map +1 -0
  154. package/dist/electron-build/utils/version-info.js +57 -31
  155. package/dist/electron-build/utils/version-info.js.map +1 -1
  156. package/dist/index.js +385 -108
  157. package/dist/ios/bolloon-web.json +10 -0
  158. package/dist/ios/chain-explorer.js +617 -0
  159. package/dist/ios/explorer.html +163 -0
  160. package/dist/ios/index.html +96 -0
  161. package/dist/ios/mobile-agent.js +2 -2
  162. package/dist/ios/mobile-contacts.js +362 -0
  163. package/dist/ios/mobile-core.js +3102 -310
  164. package/dist/ios/mobile-privacy.js +185 -0
  165. package/dist/ios/mobile-tasks.js +339 -0
  166. package/dist/ios/mobile-update.js +1442 -0
  167. package/dist/ios/mobile.css +19 -0
  168. package/dist/ios/mobile.html +96 -0
  169. package/dist/ios/mobile.js +1013 -41
  170. package/dist/ios/routes-chain-index.js +108 -0
  171. package/dist/ios/routes-contacts.js +420 -0
  172. package/dist/ios/routes-llm-config.js +344 -52
  173. package/dist/ios/routes-mobile-tasks.js +353 -0
  174. package/dist/ios/routes-onchain-trade.js +99 -0
  175. package/dist/ios/routes-x402-info.js +1 -0
  176. package/dist/ios/server.js +921 -0
  177. package/dist/ios/sw.js +1 -1
  178. package/dist/ios/task-frame-server.js +211 -0
  179. package/dist/llm/config-store.js +146 -6
  180. package/dist/llm/connection-probe.js +744 -0
  181. package/dist/llm/custom-provider-store.js +224 -0
  182. package/dist/llm/data/provider-catalog-baked.js +19 -0
  183. package/dist/llm/model-catalog.js +717 -0
  184. package/dist/llm/model-discovery.js +1137 -0
  185. package/dist/llm/model-selection.js +1241 -0
  186. package/dist/llm/pi-ai.js +70 -11
  187. package/dist/llm/provider-catalog.js +898 -0
  188. package/dist/llm/provider-registry.js +917 -0
  189. package/dist/llm/tool-name.js +220 -0
  190. package/dist/orbitdb/cid-database.js +81 -16
  191. package/dist/orbitdb/ipfs-node.js +31 -2
  192. package/dist/setup/onboard.js +34 -8
  193. package/dist/utils/dual-source-facts.js +223 -0
  194. package/dist/utils/dual-source.js +297 -0
  195. package/dist/utils/update-cli.js +16 -21
  196. package/dist/utils/update-health.js +35 -5
  197. package/dist/utils/update-manager.js +325 -96
  198. package/dist/utils/update-state.js +13 -10
  199. package/dist/utils/version-identity.js +176 -0
  200. package/dist/utils/version-info.js +27 -22
  201. package/dist/web/bolloon-web.json +10 -0
  202. package/dist/web/mobile-contacts.js +44 -11
  203. package/dist/web/mobile-core.js +2901 -580
  204. package/dist/web/mobile-tasks.js +339 -0
  205. package/dist/web/mobile-update.js +1442 -0
  206. package/dist/web/mobile.css +19 -0
  207. package/dist/web/mobile.html +64 -0
  208. package/dist/web/mobile.js +838 -35
  209. package/dist/web/routes-llm-config.js +344 -52
  210. package/dist/web/routes-mobile-tasks.js +353 -0
  211. package/dist/web/server.js +163 -4
  212. package/dist/web/sw.js +1 -1
  213. package/package.json +3 -1
  214. package/scripts/build-web.ts +36 -0
@@ -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
@@ -66,6 +66,18 @@ ${title('bolloon task')}
66
66
  交付只贴哈希 · 初筛逐条结果 · 终审结论; 群消息一律无钱包地址/DID/peerId/multiaddr/IP/私钥形态
67
67
  (命中任一条 → 拒发并报出命中的规则)
68
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
+
69
81
  选项: --json · --quiet · --request-id <id> · --timeout <ms> · --resume <goalId> (同 bolloon task)
70
82
  说明: 收件箱落盘 ~/.bolloon/tasks/inbox/; 本机发出的任务台账 ~/.bolloon/tasks/local/;
71
83
  结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout);
@@ -169,6 +181,8 @@ export async function taskCommand(flags) {
169
181
  case 'announce': return taskAnnounce(flags);
170
182
  case 'trail': return taskTrail(flags);
171
183
  case 'post': return taskPost(flags);
184
+ // 2026-09-24: 群管理 (create/join/list/link/leave) —— 外部接单者的自助入群入口
185
+ case 'group': return taskGroup(flags);
172
186
  default:
173
187
  return {
174
188
  envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 task 子命令: ${sub}` : '缺少 task 子命令', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
@@ -675,7 +689,7 @@ function parseInput(raw) {
675
689
  }
676
690
  // ── bolloon-task/1 真收发: send / inbox / accept / reject ────────────────────
677
691
  /** 人类单位 → 原子单位正整数串 (拒绝 0 / 负 / 超精度; 浮点模糊一律拒) */
678
- function toAtomic(human, decimals) {
692
+ export function toAtomic(human, decimals) {
679
693
  const s = String(human ?? '').trim();
680
694
  if (!/^[0-9]+(\.[0-9]+)?$/.test(s))
681
695
  return null;
@@ -686,7 +700,7 @@ function toAtomic(human, decimals) {
686
700
  return n > 0n ? n.toString() : null;
687
701
  }
688
702
  /** `--deadline +2h` / `+30m` / `+45s` / `+1d`, 或未来毫秒时间戳 */
689
- function parseDeadline(v, now = Date.now()) {
703
+ export function parseDeadline(v, now = Date.now()) {
690
704
  if (v === undefined)
691
705
  return now + 24 * 60 * 60 * 1000;
692
706
  const m = /^\+(\d+)(s|m|h|d)$/.exec(String(v).trim());
@@ -1837,6 +1851,340 @@ async function taskPost(flags) {
1837
1851
  ].join('\n'),
1838
1852
  };
1839
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
+ }
1840
2188
  // ── 仍未实现的两个 (任务书没点名; 如实报, 不假装) ────────────────────────────
1841
2189
  async function taskComplete(_flags) {
1842
2190
  const env = notImplemented('task complete 还没实现: 没有任务状态存储可写 (交易层有非法迁移保护, 不许绕过)', {
@@ -0,0 +1,177 @@
1
+ /**
2
+ * identity-command.ts — `bolloon identity init|show` (2026-09-24)
3
+ *
4
+ * 补的真缺口: 建本机身份以前**只有** `bolloon setup` —— 一个 readline 交互向导。
5
+ * 没有 TTY 的环境 (CI / 容器 / ssh 管道 / 第二实例的自动化) 跑它会
6
+ * `readline was closed` (ERR_USE_AFTER_CLOSE), 于是"新机器/第二个身份"根本建不出来,
7
+ * 而群/任务/签名这条链全靠 `~/.bolloon/identity.json`。
8
+ *
9
+ * 本模块只做薄包装 (不重实现任何密钥学):
10
+ * · `init` — 幂等建 `~/.bolloon/identity.json` (复用 `setup-wizard.initLocalIdentity`,
11
+ * 它内部走 `KeyManager.generate/saveToFile`, 与 `src/index.ts:bootstrapIdentity` 同一条路径);
12
+ * 已存在 → **一个字不改** + `action:'reused'` + exit 0; 损坏 → **拒绝覆盖** (要 --force)。
13
+ * · `show` — 只出 did / 公钥指纹 / 文件权限; **绝不打印私钥**。
14
+ *
15
+ * 红线: 本模块任何路径都不把 `privateKey` 写进 stdout / data / human。
16
+ * 兜底还有 `finalizeEnvelope` 的 `redactSecrets` (AUDIT_FORBIDDEN_KEYS 含 `privateKey`)。
17
+ */
18
+ import * as fs from 'fs/promises';
19
+ import * as os from 'os';
20
+ import * as crypto from 'crypto';
21
+ import { okEnvelope, failEnvelope, line, title, hint, plain, has, } from './protocol-envelope.js';
22
+ import { getLocalIdentityFile, initLocalIdentity } from './setup-wizard.js';
23
+ import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
24
+ export const IDENTITY_USAGE = `
25
+ ${title('bolloon identity')}
26
+ bolloon identity init [--force] [--json]
27
+ 非交互建本机身份 → ~/.bolloon/identity.json (文件模式 0600)
28
+ 字段与既有完全一致: keyType='Ed25519' · privateKey · publicKey · did · createdAt · version
29
+ 幂等: 已存在 → 一个字不改 (action=reused), 仍然 exit 0
30
+ 损坏 (读不出 did) → 拒绝覆盖 (exit 1); 确认重来加 --force (会先备份成 .bak-<时间戳>)
31
+ **绝不打印私钥** (输出只有 did / 文件路径 / 文件权限)
32
+ bolloon identity show [--json]
33
+ 看本机身份: did · 公钥指纹(sha256 前 16 位) · 文件权限 · 创建时间 (不含私钥)
34
+
35
+ 选项: --json · --quiet · --force (只对 init 有效)
36
+ 说明: 只建 identity.json; 供应商/API key 仍走 bolloon setup (或 bolloon setup --provider … --api-key …);
37
+ 用户称呼/归属身份在 ~/.bolloon/identity/user.json (bolloon setup --name 写它)
38
+ `;
39
+ /** 公钥指纹 (sha256 前 16 位 hex) —— 不可逆, 不含密钥材料 */
40
+ function fingerprint(pub) {
41
+ return `sha256:${crypto.createHash('sha256').update(String(pub || '')).digest('hex').slice(0, 16)}`;
42
+ }
43
+ /** 读现有身份文件 (只取**非私密**字段; privateKey 连读都不读出来) */
44
+ async function readIdentityPublicFacts(home) {
45
+ const file = getLocalIdentityFile(home);
46
+ try {
47
+ const j = JSON.parse(await fs.readFile(file, 'utf-8'));
48
+ const pub = typeof j?.publicKey === 'string' ? j.publicKey : null;
49
+ return {
50
+ exists: true,
51
+ did: typeof j?.did === 'string' && j.did ? j.did : null,
52
+ keyType: typeof j?.keyType === 'string' ? j.keyType : null,
53
+ createdAt: typeof j?.createdAt === 'string' ? j.createdAt : null,
54
+ version: typeof j?.version === 'string' ? j.version : null,
55
+ fingerprint: pub ? fingerprint(pub) : null,
56
+ // 只报**非私密**字段名 (schema 一致性的证据落在测试里: 那边直接读文件比对 6 个键)。
57
+ // 私钥类字段名 (AUDIT_FORBIDDEN_KEYS: privateKey/…) 连名字都不出现在输出里 ——
58
+ // 免得"输出里有个 privateKey 字样"被当成可疑, 也让 `grep privateKey` 这种粗检查能直接过。
59
+ keys: Object.keys(j ?? {}).filter((k) => !AUDIT_FORBIDDEN_KEYS.includes(k)).sort(),
60
+ };
61
+ }
62
+ catch {
63
+ return { exists: false };
64
+ }
65
+ }
66
+ /** `bolloon identity init` —— 幂等建本机身份 */
67
+ async function identityInit(flags) {
68
+ const head = 'bolloon identity init';
69
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
70
+ const force = has(flags, '--force');
71
+ let r;
72
+ try {
73
+ r = await initLocalIdentity(home, { force });
74
+ }
75
+ catch (e) {
76
+ return {
77
+ envelope: failEnvelope('INTERNAL_ERROR', `建身份失败: ${String(e?.message || e).slice(0, 200)}`, { file: getLocalIdentityFile(home), created: false }, [], 'needs_human'),
78
+ human: `${title(head)}\n 建身份失败: ${String(e?.message || e).slice(0, 200)}`,
79
+ };
80
+ }
81
+ if (!r.ok) {
82
+ // 拒绝覆盖 (损坏 / force 下备份失败): 如实失败, 不静默重建
83
+ return {
84
+ envelope: failEnvelope('POLICY_DENIED', r.reason || '拒绝覆盖既有身份文件', { file: r.file, action: r.action, created: false, overwritten: false }, [], 'needs_human'),
85
+ human: `${title(head)}\n ${r.reason || '拒绝覆盖'}\n\n${hint('静默盖掉一个可能还能救的身份 = 丢钥匙: 本命令宁可失败')}`,
86
+ };
87
+ }
88
+ const reused = r.action === 'reused';
89
+ const facts = await readIdentityPublicFacts(home);
90
+ const payload = {
91
+ action: r.action,
92
+ did: r.did ?? null,
93
+ file: r.file,
94
+ mode: r.mode ?? null,
95
+ created: !reused,
96
+ changed: false,
97
+ keyMaterialInOutput: false,
98
+ publicFields: facts.exists ? facts.keys : null,
99
+ fingerprint: facts.exists ? facts.fingerprint : null,
100
+ keyType: facts.exists ? facts.keyType : null,
101
+ createdAt: facts.exists ? facts.createdAt : null,
102
+ version: facts.exists ? facts.version : null,
103
+ nextSteps: reused
104
+ ? ['身份已存在, 未改动 (幂等); 直接用即可']
105
+ : ['跑任务/建群/签名现在有身份可用: bolloon task group create --name <群名>', '要配模型供应商/API key: bolloon setup'],
106
+ };
107
+ return {
108
+ envelope: okEnvelope('OK', reused
109
+ ? `本机身份已存在, 未改动 (${String(r.did).slice(0, 24)}…)`
110
+ : `已建本机身份 (${String(r.did).slice(0, 24)}…), 文件模式 ${r.mode}`, payload, [], null),
111
+ human: [
112
+ title(head),
113
+ line('动作', reused ? 'reused (已存在, 一个字没改 — 幂等)' : 'created (新建)'),
114
+ line('DID', r.did ?? '(无)'),
115
+ line('身份文件', r.file),
116
+ line('文件模式', r.mode ?? '(读不到)'),
117
+ line('字段', facts.exists ? facts.keys.join(' · ') : '(无)'),
118
+ line('公钥指纹', facts.exists ? facts.fingerprint : '(无)'),
119
+ '',
120
+ ` ${hint(reused ? '幂等: 第二个身份请换一个 HOME (HOME=/tmp/x bolloon identity init)' : '下一步')}`,
121
+ ...(reused
122
+ ? [' 要用第二个身份: `HOME=/tmp/second bolloon identity init` (每个 HOME 一对自己的密钥)']
123
+ : [' 建群: bolloon task group create --name <群名>', ' 模型/API key: bolloon setup (交互向导)']),
124
+ ].join('\n'),
125
+ };
126
+ }
127
+ /** `bolloon identity show` —— 看本机身份 (不含私钥) */
128
+ async function identityShow(flags) {
129
+ const head = 'bolloon identity show';
130
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
131
+ const facts = await readIdentityPublicFacts(home);
132
+ const file = getLocalIdentityFile(home);
133
+ if (!facts.exists) {
134
+ return {
135
+ envelope: failEnvelope('NOT_FOUND', `本机还没有身份文件: ${file}`, { file, exists: false, howTo: 'bolloon identity init', keyMaterialInOutput: false }, [], 'needs_human'),
136
+ human: `${title(head)}\n 本机还没有身份文件: ${file}\n\n${hint('建一个: bolloon identity init (非交互, 0600, 幂等)')}`,
137
+ };
138
+ }
139
+ const payload = {
140
+ file,
141
+ exists: true,
142
+ did: facts.did,
143
+ keyType: facts.keyType,
144
+ createdAt: facts.createdAt,
145
+ version: facts.version,
146
+ fingerprint: facts.fingerprint,
147
+ publicFields: facts.keys,
148
+ keyMaterialInOutput: false,
149
+ };
150
+ return {
151
+ envelope: okEnvelope('OK', `本机身份: ${String(facts.did).slice(0, 24)}…`, payload, [], null),
152
+ human: [
153
+ title(head),
154
+ line('DID', facts.did ?? '(缺 did — 文件可能损坏)'),
155
+ line('身份文件', file),
156
+ line('keyType', facts.keyType ?? '(无)'),
157
+ line('version', facts.version ?? '(无)'),
158
+ line('createdAt', facts.createdAt ?? '(无)'),
159
+ line('公钥指纹', facts.fingerprint ?? '(无)'),
160
+ line('非私密字段', facts.keys.join(' · ')),
161
+ '',
162
+ ' 私钥只在本机文件里 (0600), 不进任何输出/日志',
163
+ ].join('\n'),
164
+ };
165
+ }
166
+ /** 命令入口 (cli-entry 用 runCommand 包它: 统一 --json/--quiet/--timeout/异常兜底) */
167
+ export async function identityCommand(flags) {
168
+ const sub = String(flags.positionals[0] ?? '').trim().toLowerCase();
169
+ if (sub === 'init')
170
+ return identityInit(flags);
171
+ if (sub === 'show' || sub === 'status')
172
+ return identityShow(flags);
173
+ return {
174
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show'] }, [], 'needs_human'),
175
+ human: IDENTITY_USAGE,
176
+ };
177
+ }