@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
@@ -0,0 +1,220 @@
1
+ /**
2
+ * tool-name.ts — 工具名「出网前的唯一净化边界」(2026-09-26)
3
+ *
4
+ * 为什么要这个文件:
5
+ * OpenAI 兼容 API 对 `tools[i].function.name` 有硬约束 —— 必须匹配
6
+ * `^[a-zA-Z0-9_-]{1,64}$`. 只要**第 121 个**工具的名字里有一个非法字符 (`.` `:` `/`
7
+ * 空格 / 中文 ...), 整个请求就被 400 拒掉:
8
+ *
9
+ * Invalid 'tools[120].function.name': string does not match pattern.
10
+ * Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.
11
+ *
12
+ * 表现是「网页端明明配好了 API, 一跑就失败」—— 根因在**发包形状**, 不在凭据。
13
+ * 本仓曾注册 6 个带点的工具名 (`contact.list_authorized` 一族), 命中这个坑。
14
+ *
15
+ * 设计铁律:
16
+ * 1. **唯一边界**: 净化只在 `pi-ai.ts` 的 `generateText()` 里做一次 (那里是全仓唯一
17
+ * 产出 wire 形状 tools 数组的地方). 业务代码一个都不许自己做净化 —— 否则同一次改动
18
+ * 要有 N 个调用点跟着改, 迟早漏一个.
19
+ * 2. **确定性, 不是随机**: 非法字符 → `_`; 超长 → 前 55 字符 + `_` + 原名 sha256 前 8 位
20
+ * (同一次运行里同一原名永远得到同一个 API 名).
21
+ * 3. **派发不断**: 净化时把 `原名 → API 名` 同时反向登记 (原名 ↔ API 名双射), 回程
22
+ * (`resolveApiToolName`) 把 LLM 回吐的 API 名还原成原名, 再交给 `this.tools.get()`.
23
+ * LLM 也可能照 system prompt 里的原名回吐 —— 原名在表里就直接穿透.
24
+ * 4. **碰撞不许静默**: 两个**不同**原名净化后同名 → 抛 `ToolNameCollisionError`, 消息里
25
+ * 把两个原名都点名. 绝不悄悄合并或悄悄改名 (那会让 LLM 调到的工具和它以为的不是一个).
26
+ * 5. **不猜**: 没有名字 / 空名字的工具也是非法形状 → 抛错点名, 不静默丢掉.
27
+ *
28
+ * 真正「覆盖所有入口」的依据: CLI / Web / MCP / 技能 / 子 Agent / 长期任务全都走
29
+ * `getMinimax().chat(..., tools)` → `PiAIModel.generateText()` → `callOpenAI()`,
30
+ * 全仓只有这一条产出 tools 的路径 (见 `scripts/verify-tool-names.ts` 的源码级断言).
31
+ */
32
+ import * as crypto from 'crypto';
33
+ /** OpenAI 兼容 API 对 function.name 的硬约束 */
34
+ export const TOOL_NAME_MAX_LENGTH = 64;
35
+ /** 允许的字符集: 字母 / 数字 / 下划线 / 连字符 */
36
+ export const TOOL_NAME_CHAR_CLASS = 'a-zA-Z0-9_-';
37
+ /** 完整形状约束 (1..64, 只含安全字符) */
38
+ export const TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
39
+ /** 太长时的截断长度: 55 + '_' + 8 位 hash = 64, 正好贴着上限 */
40
+ const TRUNCATE_KEEP = 55;
41
+ /** 严格形状校验 (不做任何修补) */
42
+ export function isValidToolName(name) {
43
+ return typeof name === 'string' && TOOL_NAME_PATTERN.test(name);
44
+ }
45
+ /** 名字里的非法字符 (去重, 按出现顺序) —— 给探针/门报「违规字符是什么」用 */
46
+ export function illegalCharsOf(name) {
47
+ const out = [];
48
+ for (const ch of String(name ?? '')) {
49
+ if (!/[a-zA-Z0-9_-]/.test(ch) && !out.includes(ch))
50
+ out.push(ch);
51
+ }
52
+ return out;
53
+ }
54
+ /** 原名指纹 (sha256 前 8 位 hex) —— 超长截断的去碰撞后缀 */
55
+ export function toolNameFingerprint(name) {
56
+ return crypto.createHash('sha256').update(String(name), 'utf8').digest('hex').slice(0, 8);
57
+ }
58
+ /**
59
+ * 纯函数净化: 非法字符 → `_`; 超长 → 截断 + 稳定 hash 后缀.
60
+ * 已经是合法名的原样返回 (幂等, 再跑一次不变).
61
+ */
62
+ export function sanitizeToolName(name) {
63
+ const raw = String(name ?? '');
64
+ let s = raw.replace(new RegExp(`[^${TOOL_NAME_CHAR_CLASS}]`, 'g'), '_');
65
+ if (s.length > TOOL_NAME_MAX_LENGTH) {
66
+ s = `${s.slice(0, TRUNCATE_KEEP)}_${toolNameFingerprint(raw)}`;
67
+ }
68
+ // 空名 / 全非法字符且被清空 (理论上不会, 上面都是替换成 '_') —— 兜底成可派发的稳定名
69
+ if (s.length === 0)
70
+ s = `tool_${toolNameFingerprint(raw)}`;
71
+ return s;
72
+ }
73
+ /** 两个不同原名净化后撞名 —— 点名两方, 拒绝发包 */
74
+ export class ToolNameCollisionError extends Error {
75
+ apiName;
76
+ names;
77
+ constructor(apiName, names) {
78
+ super(`工具名净化后撞名: ${names.map((n) => `"${n}"`).join(' 与 ')} 都会净化为 "${apiName}" —— ` +
79
+ `拒绝发包 (两个不同工具共用一个 API 名会让 LLM 调到的实现不可知). ` +
80
+ `请给其中一个改名, 只剩安全字符 [${TOOL_NAME_CHAR_CLASS}] 且 ≤64 字符.`);
81
+ this.name = 'ToolNameCollisionError';
82
+ this.apiName = apiName;
83
+ this.names = names;
84
+ }
85
+ }
86
+ /**
87
+ * 原名 ↔ API 名 双射表.
88
+ * 正向: 出网时把原名登记进来拿 API 名 (碰撞 → 抛错, 不静默).
89
+ * 反向: 回程把 LLM 回吐的 API 名还原成原名 (未知名原样穿透, 由业务侧报「未知工具」).
90
+ */
91
+ export class ToolNameRouteTable {
92
+ byApi = new Map();
93
+ byOriginal = new Map();
94
+ /** 登记一个原名, 返回它的 API 名. 撞名 → ToolNameCollisionError. */
95
+ register(original) {
96
+ const orig = String(original ?? '');
97
+ if (isValidToolName(orig)) {
98
+ // 合法名不动: 但也要防「另一个原名净化成它」的撞名 (下面 byApi 会处理)
99
+ const api = orig;
100
+ const holder = this.byApi.get(api);
101
+ if (holder !== undefined && holder !== orig) {
102
+ throw new ToolNameCollisionError(api, [holder, orig]);
103
+ }
104
+ this.byApi.set(api, orig);
105
+ this.byOriginal.set(orig, api);
106
+ return api;
107
+ }
108
+ const api = sanitizeToolName(orig);
109
+ const holder = this.byApi.get(api);
110
+ if (holder !== undefined && holder !== orig) {
111
+ throw new ToolNameCollisionError(api, [holder, orig]);
112
+ }
113
+ this.byApi.set(api, orig);
114
+ this.byOriginal.set(orig, api);
115
+ return api;
116
+ }
117
+ /** 批量登记 */
118
+ registerAll(originals) {
119
+ return originals.map((n) => this.register(n));
120
+ }
121
+ /** 原名 → API 名 (没登记过就现算, 不登记 —— 给纯查询用) */
122
+ apiNameOf(original) {
123
+ const known = this.byOriginal.get(String(original ?? ''));
124
+ if (known !== undefined)
125
+ return known;
126
+ return isValidToolName(original) ? original : sanitizeToolName(original);
127
+ }
128
+ /**
129
+ * API 名 → 原名 (回程派发用).
130
+ * 不在表里 (LLM 照 system prompt 里的原名回吐 / 编了个名字) → 原样返回,
131
+ * 交给业务侧 `tools.get()` 走「未知工具」分支 — 不在这里吞掉.
132
+ */
133
+ resolveToOriginal(apiName) {
134
+ const hit = this.byApi.get(String(apiName ?? ''));
135
+ return hit !== undefined ? hit : String(apiName ?? '');
136
+ }
137
+ /** 是否知道这个 API 名 */
138
+ knows(apiName) {
139
+ return this.byApi.has(String(apiName ?? ''));
140
+ }
141
+ /** 全部映射 (原名 → API 名), 按登记顺序 */
142
+ get mappings() {
143
+ const seen = new Set();
144
+ const out = [];
145
+ for (const [api, original] of this.byApi.entries()) {
146
+ if (seen.has(original))
147
+ continue;
148
+ seen.add(original);
149
+ out.push({ original, api, changed: original !== api });
150
+ }
151
+ return out;
152
+ }
153
+ /** 被改写过的部分 (改名清单) */
154
+ get changed() {
155
+ return this.mappings.filter((m) => m.changed);
156
+ }
157
+ /** 表里已知的原名个数 */
158
+ get size() {
159
+ return this.byApi.size;
160
+ }
161
+ /** 清空 (测试用; 业务路径不该调) */
162
+ clear() {
163
+ this.byApi.clear();
164
+ this.byOriginal.clear();
165
+ }
166
+ }
167
+ /**
168
+ * 进程级单例: 出网净化与回程派发共用同一张表.
169
+ * (出网在 pi-ai.ts, 派发在 pi-sdk.ts / workflow-pivot-loop.ts —— 只有共享一份才保证还原得到真名)
170
+ */
171
+ export const globalToolNameRoutes = new ToolNameRouteTable();
172
+ /**
173
+ * **唯一**的出网净化入口: 把 wire 形状的 tools 数组里的 `function.name` 逐个净化,
174
+ * 顺便把映射登记进 `globalToolNameRoutes` 供回程派发还原.
175
+ *
176
+ * - 不改入参 (返回新数组 / 新对象), 调用方拿去发包即可;
177
+ * - 没名字 / 空名字 → 抛错 (说不出名字的工具本来就是坏形状, 不许静默丢);
178
+ * - 撞名 → ToolNameCollisionError (点名两个原名).
179
+ */
180
+ export function sanitizeToolsForApi(tools, table = globalToolNameRoutes) {
181
+ if (!Array.isArray(tools))
182
+ return tools;
183
+ const out = new Array(tools.length);
184
+ for (let i = 0; i < tools.length; i++) {
185
+ const tool = tools[i];
186
+ const original = tool?.function?.name;
187
+ if (typeof original !== 'string' || original.length === 0) {
188
+ throw new Error(`工具 #${i} 没有可用的 function.name (拿到 ${JSON.stringify(original)}) —— 拒绝发包: ` +
189
+ `OpenAI 兼容 API 要求每个工具都有 name 且匹配 /^[a-zA-Z0-9_-]{1,64}$/`);
190
+ }
191
+ const api = table.register(original);
192
+ out[i] = api === original ? tool : { ...tool, function: { ...tool.function, name: api } };
193
+ }
194
+ return out;
195
+ }
196
+ /** 回程: LLM 回吐的名字 → 注册表真名 (未知名原样穿透) */
197
+ export function resolveApiToolName(apiName, table = globalToolNameRoutes) {
198
+ return table.resolveToOriginal(apiName);
199
+ }
200
+ /**
201
+ * 给「文本解析」路径用的已知名集合: 原名 + 它们的 API 名.
202
+ * LLM 在文本里回吐的可能是净化后的名字 (prompt/工具面里给的是 API 名),
203
+ * 只用原名集合过滤会把这类调用直接丢掉.
204
+ */
205
+ export function expandKnownToolNames(names, table = globalToolNameRoutes) {
206
+ const out = new Set();
207
+ for (const n of names) {
208
+ out.add(n);
209
+ const api = table.apiNameOf(n);
210
+ out.add(api);
211
+ // 预登记, 保证回程还原得到
212
+ try {
213
+ table.register(n);
214
+ }
215
+ catch {
216
+ // 撞名: 交给出网时的 sanitizeToolsForApi 抛 (那里能给出「拒绝发包」的上下文)
217
+ }
218
+ }
219
+ return out;
220
+ }
@@ -23,9 +23,41 @@ import * as dagCbor from '@ipld/dag-cbor';
23
23
  import { sha256 } from 'multiformats/hashes/sha2';
24
24
  import { concat as uint8Concat } from 'uint8arrays/concat';
25
25
  import { createOrbitDB } from '@orbitdb/core';
26
+ import * as orbitdbCoreNs from '@orbitdb/core';
27
+ /**
28
+ * 运行时取 `IPFSAccessController` (2026-09-24)。
29
+ *
30
+ * 它在运行时是**真具名导出** (`@orbitdb/core` 4.0.0 `src/index.js:26`, 经
31
+ * `./access-controllers/index.js` 二级再导出), 但该包是**纯 JS 包**: `package.json`
32
+ * 既无 `types` 也无 `exports`、包内没有任何 `.d.ts` —— TS7 从 JS 推断导出集时看不见
33
+ * 这个二级再导出, `import { IPFSAccessController }` 直接 TS2305。
34
+ * 所以从命名空间按需取一次并显式收窄到已知形状; 不给整个模块上 `any`
35
+ * (createOrbitDB / OrbitDB / KeyValue 仍照旧受类型检查)。
36
+ */
37
+ const IPFSAccessController = orbitdbCoreNs.IPFSAccessController;
26
38
  import { createBolloonIpfs } from './ipfs-node.js';
27
39
  import * as path from 'path';
28
40
  import * as os from 'os';
41
+ /**
42
+ * store 打不开 (地址不可解析 / 区块不在本地且没有 block broker 能去网上取)。
43
+ *
44
+ * 2026-09-24: 原来 `openStoreByAddress` 失败返回 null, 上游 (gateway-group) 把它当"空群",
45
+ * 于是"读不到"被显示成"没有消息"。改成**抛**: 打不开 ≠ 没内容。
46
+ */
47
+ export class OrbitDBStoreUnreachableError extends Error {
48
+ address;
49
+ cause;
50
+ dataDir;
51
+ code = 'STORE_UNREACHABLE';
52
+ constructor(address, cause, dataDir) {
53
+ const why = String(cause?.message ?? cause ?? '未知原因').slice(0, 200);
54
+ super(`store 不可达 (${address}): ${why}`);
55
+ this.address = address;
56
+ this.cause = cause;
57
+ this.dataDir = dataDir;
58
+ this.name = 'OrbitDBStoreUnreachableError';
59
+ }
60
+ }
29
61
  /** 内容 → CID (dag-cbor, sha2-256, codec 0x71); 先 JSON 清洗 (dag-cbor 不支持 undefined) */
30
62
  export async function contentToCid(obj) {
31
63
  const cleaned = JSON.parse(JSON.stringify(obj)); // 丢弃 undefined 字段
@@ -34,6 +66,26 @@ export async function contentToCid(obj) {
34
66
  return CID.createV1(0x71, hash).toString();
35
67
  }
36
68
  const home = () => process.env.HOME || os.homedir() || '/tmp';
69
+ /**
70
+ * OrbitDB 身份在 keystore 里的槽名 (2026-09-24)。
71
+ * 固定槽名 = 同一 dataDir 下每个进程读到**同一对密钥 / 同一个身份**。
72
+ * 不固定 (@orbitdb/core 默认 createId() 随机 32 位) 会让每个进程变成新写入者,
73
+ * 于是 `canAppend` 拿新身份去比对老 manifest 的 write 列表 → 一律拒绝。
74
+ */
75
+ const ORBITDB_IDENTITY_ID = 'bolloon';
76
+ /**
77
+ * `opts.accessController.write` → OrbitDB v4 的大写 `AccessController` 工厂 (2026-09-24)。
78
+ *
79
+ * @orbitdb/core v4 的 `open(address, options)` **只认大写 `AccessController`** (一个构造函数),
80
+ * 小写 `accessController: { write: [...] }` 会被静默丢弃 → 新 store 落回默认策略
81
+ * (`IPFSAccessController`: `write = write || [创建者身份 id]`, 即创建者独占写)。
82
+ * 于是 gateway-group 一直传的 `write:['*']` (「群成员可广播」) **从来没生效过**。
83
+ * 这里把已成文的选项接上; 对已存在的地址, manifest 里的 AC 仍优先 (这里不改变既有群)。
84
+ */
85
+ function accessControllerOption(opts) {
86
+ const write = opts?.accessController?.write;
87
+ return Array.isArray(write) && write.length ? { AccessController: IPFSAccessController({ write }) } : {};
88
+ }
37
89
  /**
38
90
  * OrbitDB 后端实现。单例: 同一进程只建一个 (helia/OrbitDB 都是重量级节点)。
39
91
  */
@@ -46,15 +98,29 @@ export class OrbitDBAdapter {
46
98
  constructor(dataDir = path.join(home(), '.bolloon', 'orbitdb')) {
47
99
  this.dataDir = dataDir;
48
100
  }
101
+ /** 落盘位置 (未初始化时 null; 诊断/验收用) */
102
+ get ipfsPaths() {
103
+ return this.node?.paths ?? null;
104
+ }
49
105
  /** 懒初始化: 首次使用时启动 helia + OrbitDB + 打开 keyvalue store */
50
106
  async ensure() {
51
107
  if (this.db)
52
108
  return;
53
109
  this.node = await createBolloonIpfs(path.join(this.dataDir, 'ipfs'));
54
- this._orbitdb = await createOrbitDB({
110
+ // 2026-09-24: 身份必须**跨进程稳定**。不给 id 时 @orbitdb/core 用 createId()
111
+ // (32 位随机串) 当 keystore 槽名 (src/orbitdb.js:43 `id = id || await createId()`
112
+ // → :61 `createIdentity({ id })`) → 每个进程一对新密钥 → 新身份 → 写自己的 store
113
+ // 会被访问控制拒掉 ("Key … is not allowed to write to the log")。
114
+ // 给固定槽名后, 密钥从 <dataDir>/stores/keystore 读出复用 → 同 dataDir 同身份。
115
+ // 类型注: `id` 是 @orbitdb/core 的真选项 (src/orbitdb.js:33), 但 TS7 从 JS 推断出的
116
+ // 参数类型漏了它 → 显式取 Parameters<typeof createOrbitDB>[0] 再补 { id }
117
+ // (不裸 any 掉整个入参)。
118
+ const init = {
55
119
  ipfs: this.node.helia,
56
120
  directory: path.join(this.dataDir, 'stores'),
57
- });
121
+ id: ORBITDB_IDENTITY_ID,
122
+ };
123
+ this._orbitdb = await createOrbitDB(init);
58
124
  this.db = await this._orbitdb.open('bolloon-cid-store', { type: 'keyvalue' });
59
125
  // 共享底层实例 (只读暴露)
60
126
  this.orbitdb = this._orbitdb;
@@ -192,33 +258,32 @@ export class OrbitDBAdapter {
192
258
  */
193
259
  async openStore(name, type = 'keyvalue', opts) {
194
260
  await this.ensure();
195
- const options = { type };
196
- if (opts?.accessController)
197
- options.accessController = opts.accessController;
261
+ const options = { type, ...accessControllerOption(opts) };
198
262
  const raw = (await this._orbitdb.open(name, options));
199
263
  return this.wrapStore(raw);
200
264
  }
201
265
  /**
202
266
  * 2026-08-14: 按地址打开远端 store (Agent Gateway 共享网络 / 群组)。
203
- * replica=true → 只读副本: 拉取复制但不写回 (自动加入网络不污染他人 registry)。
204
- * replica=false → 可写 (群组: events store 配 write:'*' 时成员可广播)。
267
+ *
268
+ * 2026-09-24: 失败改为**抛 OrbitDBStoreUnreachableError** (含原始错误原因),
269
+ * 不再 `console.warn` + return null —— 那让"整个群读不到"在上层显示成"群里没有消息"。
270
+ *
271
+ * 2026-09-24 (修正既有错述): **不再往 OrbitDB 传 `replica`** ——
272
+ * `@orbitdb/core` 4.0.0 的 `open()` 参数表里没有它 (src/orbitdb.js:118), 传了也被丢,
273
+ * 之前的"replica=true 只读副本"是**不存在的语义**(等于什么都没做)。入参保留以兼容
274
+ * 既有调用点, 但不再冒充 OrbitDB 选项。可写与否由 manifest 里的 ACL + 调用方是否 `put` 决定。
275
+ * `opts.accessController` 在**打开既有地址**时同样不生效 (见接口注释: AC 由 manifest 决定)。
205
276
  */
206
277
  async openStoreByAddress(address, type = 'keyvalue', opts) {
207
278
  await this.ensure();
208
279
  try {
209
- const options = { type };
210
- if (opts?.replica === false)
211
- options.replica = false;
212
- else
213
- options.replica = true;
214
- if (opts?.accessController)
215
- options.accessController = opts.accessController;
280
+ const options = { type, ...accessControllerOption(opts) };
216
281
  const raw = (await this._orbitdb.open(address, options));
217
282
  return this.wrapStore(raw);
218
283
  }
219
284
  catch (err) {
220
- console.warn(`[orbitdb] openStoreByAddress 失败: ${String(err?.message || err).slice(0, 160)}`);
221
- return null;
285
+ // 大声失败: 带上地址 + 原始原因 (最常见: "No block brokers capable of retrieving blocks ...")
286
+ throw new OrbitDBStoreUnreachableError(address, err, this.dataDir);
222
287
  }
223
288
  }
224
289
  async close() {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06)
2
+ * ipfs-node.ts — Bolloon 的 OrbitDB 底层 IPFS 节点工厂 (2026-08-06; 落盘 2026-09-24)
3
3
  *
4
4
  * 基于 helia 7:
5
5
  * - createHelia() 内部已 withLibp2p 但**不传 opts** → 无法自定义 services
@@ -10,10 +10,26 @@
10
10
  * 必须显式列出要保留的默认服务 (dht/identify/keychain/...)
11
11
  * - withLibp2p().start() 之后 libp2p getter 才可用 (之前抛 NotStartedError)
12
12
  *
13
+ * 2026-09-24 **区块/datastore 落盘修复** (跨进程可重开的根因):
14
+ * 原来 createHeliaLight 只传 codecs/hashers → helia 用默认 MemoryBlockstore / MemoryDatastore,
15
+ * 而 OrbitDB 的 manifest 与 oplog 条目都走 IPFSBlockStorage(ipfs.blockstore) 存
16
+ * (@orbitdb/core/src/storage/ipfs-block.js)。于是 `~/.bolloon/orbitdb/` 下只有 stores/
17
+ * (keystore + log/_heads 的 LevelDB), 没有 ipfs/ 块存储 —— store 只活在创建它的那个进程里。
18
+ * 新进程用地址重开必挂在 manifestStore.get():
19
+ * "No block brokers capable of retrieving blocks are configured, the CID bafyrei… cannot be fetched"
20
+ * 现在: FsBlockstore(dataDir/blocks) + FsDatastore(dataDir/datastore) 真落盘。
21
+ * · datastore 顺带持久化 pin (helia pins) 与 libp2p 私钥
22
+ * (@helia/libp2p: loadOrCreateSelfKey(helia.datastore) + opts.datastore ??= helia.datastore)
23
+ * → 同一 dataDir 的 peerId 跨进程稳定。
24
+ * · 这两个包是 Node 专用 (node:fs / node:path); 本文件只被 Node 侧 import,
25
+ * 浏览器入口 (client.ts / mobile-core.ts / a2ui-client.tsx) 到不了这里 —— 不伤 Web 包。
26
+ *
13
27
  * 跑法: npx tsx scripts/smoke-orbitdb.ts
14
28
  */
15
29
  import { createHeliaLight } from 'helia';
16
30
  import { withLibp2p } from '@helia/libp2p';
31
+ import { FsBlockstore } from 'blockstore-fs';
32
+ import { FsDatastore } from 'datastore-fs';
17
33
  import * as dagCbor from '@ipld/dag-cbor';
18
34
  import * as dagJson from '@ipld/dag-json';
19
35
  import * as json from 'multiformats/codecs/json';
@@ -37,13 +53,25 @@ import * as os from 'os';
37
53
  /**
38
54
  * 创建 Bolloon 用的 helia 节点 (libp2p 完整默认服务 + gossipsub pubsub)。
39
55
  * dataDir 持久化节点身份/数据 (默认 ~/.bolloon/orbitdb-ipfs)。
56
+ *
57
+ * 2026-09-24: dataDir 下真落两个目录 —— `blocks/` (区块) 与 `datastore/` (pin + peerkey)。
58
+ * 目录不可写时**直接抛** (不退回内存: 静默退回正好是"新进程读不到群"的根因)。
40
59
  */
41
60
  export async function createBolloonIpfs(dataDir) {
42
- const dir = dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs');
61
+ const dir = path.resolve(dataDir ?? path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'orbitdb-ipfs'));
62
+ const blocksDir = path.join(dir, 'blocks');
63
+ const datastoreDir = path.join(dir, 'datastore');
64
+ // 落盘存储: 不传这两个, helia 默认 MemoryBlockstore/MemoryDatastore → 进程退出即丢区块
65
+ const blockstore = new FsBlockstore(blocksDir);
66
+ const datastore = new FsDatastore(datastoreDir);
67
+ await blockstore.open(); // 建目录 + F_OK|W_OK 探测, 失败抛 OpenFailedError (不静默)
68
+ await datastore.open();
43
69
  // createHeliaLight 无 libp2p → withLibp2p 手动装配 (可传 services)
44
70
  // codecs/hashers 照抄 createHelia 默认: OrbitDB 的 log entry 用 dag-cbor (codec 113),
45
71
  // 不注册会报 "Could not load codec for 113"
46
72
  const helia = withLibp2p(createHeliaLight({
73
+ blockstore,
74
+ datastore,
47
75
  codecs: [dagCbor, dagJson, json],
48
76
  hashers: [sha512],
49
77
  }), {
@@ -71,6 +99,7 @@ export async function createBolloonIpfs(dataDir) {
71
99
  return {
72
100
  helia,
73
101
  peerId,
102
+ paths: { dataDir: dir, blocksDir, datastoreDir },
74
103
  start: async () => { await helia.start(); },
75
104
  stop: async () => { await helia.stop(); },
76
105
  };
@@ -104,9 +104,9 @@ const stepProvider = {
104
104
  }
105
105
  try {
106
106
  await store.updateProvider(picked, { enabled: true });
107
- // setup 模式直接激活; reconfigure 要等测试通过才切换 active (失败不破坏旧配置)
108
- if (c.mode !== 'reconfigure')
109
- await store.setActiveProvider(picked);
107
+ // 2026-09-26 (P6): 这里**不再**直接 `setActiveProvider` —— 激活是"切换", 只能走统一入口,
108
+ // 而且必须等 key/模型齐了、连通实测过了才切 (见 stepConnectivity 的成功分支)。
109
+ // 早切的问题: 那时还没有 key, 切过去等于把 effective 配置指到一个用不了的供应商。
110
110
  }
111
111
  catch (err) {
112
112
  return { ok: false, errorClass: 'io', message: `写配置失败: ${String(err?.message || err).slice(0, 120)}` };
@@ -114,7 +114,7 @@ const stepProvider = {
114
114
  return {
115
115
  ok: true,
116
116
  patch: { inputs: { provider: picked }, checks: { providerSelected: true } },
117
- note: `${picked} 已 ${c.mode === 'reconfigure' ? '标记为候选 (测试通过后切换)' : '激活'}`,
117
+ note: `${picked} 已 ${c.mode === 'reconfigure' ? '标记为候选 (测试通过后切换)' : '标记为待启用 (连通实测通过后由统一入口切换)'}`,
118
118
  };
119
119
  },
120
120
  };
@@ -202,7 +202,30 @@ const stepConnectivity = {
202
202
  c.io.print(`用最终保存的 provider/key/baseUrl/model 真实测试 ${provider} …`);
203
203
  const res = await store.testProvider(provider);
204
204
  if (res?.success) {
205
- return { ok: true, patch: { checks: { connectivityOk: true, connectivityAt: new Date().toISOString(), connectivityErrorClass: undefined } }, note: `通过 (${res.latency ?? '?'}ms)` };
205
+ // 2026-09-26 (P6): 测试通过之后**才**真正切换 —— 而且只有一处实现: 统一入口 `selectModel()`。
206
+ // 此前 setup 模式在 stepProvider 里直接 `setActiveProvider` (那时还没有 key, 也没探测过),
207
+ // reconfigure 模式则根本不切换 —— 两个都不算"同一语义"。
208
+ // 这里 `verify:false`: 刚在上面真测过一次, 入口不用再打一次上游 (重复探测是浪费, 不是严谨)。
209
+ try {
210
+ const { selectModel } = await import('../llm/model-selection.js');
211
+ const sw = await selectModel({ provider, model: c.state.inputs.model, scope: 'global', verify: false });
212
+ if (!sw.ok) {
213
+ return {
214
+ ok: false,
215
+ errorClass: sw.failureClass === 'missing_api_key' ? 'auth' : 'config',
216
+ message: `连接是通的, 但切换没有落盘 [${sw.failureClass}]: ${sw.message}`,
217
+ retryable: false,
218
+ };
219
+ }
220
+ return {
221
+ ok: true,
222
+ patch: { checks: { connectivityOk: true, connectivityAt: new Date().toISOString(), connectivityErrorClass: undefined } },
223
+ note: `通过 (${res.latency ?? '?'}ms) · 已由统一入口切到 ${sw.effective.provider}/${sw.effective.model}`,
224
+ };
225
+ }
226
+ catch (err) {
227
+ return { ok: false, errorClass: 'runtime', message: `连通但切换失败: ${String(err?.message || err).slice(0, 160)}` };
228
+ }
206
229
  }
207
230
  const raw = String(res?.error || '未知错误');
208
231
  const cls = /401|403|key/i.test(raw) ? 'auth' : /429|限流/i.test(raw) ? 'network' : /404|端点|baseUrl/i.test(raw) ? 'config' : /timeout|超时/i.test(raw) ? 'timeout' : /fetch|network|ECONN/i.test(raw) ? 'network' : 'unknown';
@@ -220,11 +243,14 @@ const stepRuntime = {
220
243
  const details = [];
221
244
  try {
222
245
  const mod = await import('../llm/pi-ai.js');
223
- mod.initMinimax?.();
246
+ // 2026-09-26 (P6): 运行时**只由统一入口装配** —— `installRuntime` 是唯一那个 initMinimax 调用点,
247
+ // 这里不再自己 `initMinimax()` (那会让"按配置文件装配"和"按有效配置装配"分叉成两条)。
248
+ const { applyEffectiveToRuntime } = await import('../llm/model-selection.js');
249
+ const eff = await applyEffectiveToRuntime();
224
250
  const model = mod.getMinimax?.() || mod.getModel?.();
225
251
  if (!model)
226
- return { ok: false, errorClass: 'runtime', message: 'initMinimax 之后仍拿不到模型对象' };
227
- details.push('initMinimax ✓');
252
+ return { ok: false, errorClass: 'runtime', message: '统一入口装配之后仍拿不到模型对象' };
253
+ details.push(`统一入口装配 ✓ (${eff.provider}/${eff.model})`);
228
254
  const chat = model.chat || model.generate || model.complete || model.call;
229
255
  if (typeof chat !== 'function') {
230
256
  return { ok: false, errorClass: 'runtime', message: '运行时对象没有可调用的 chat/generate 接口 —— 只有 singleton 不算 ready' };