@bolloon/bolloon-agent 0.6.5 → 0.6.7

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 (188) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/ha-daemon.js +85 -0
  9. package/dist/agents/ha-supervisor.js +146 -0
  10. package/dist/agents/intent-classifier.js +1 -1
  11. package/dist/agents/iteration-budget.js +12 -0
  12. package/dist/agents/knowledge-organizer.js +0 -2
  13. package/dist/agents/lesson-to-skill.js +0 -1
  14. package/dist/agents/loop-review.js +2 -2
  15. package/dist/agents/memory-store.js +384 -0
  16. package/dist/agents/opportunity-match.js +15 -3
  17. package/dist/agents/opportunity-web-search.js +39 -0
  18. package/dist/agents/p2p-chat-tools.js +1 -2
  19. package/dist/agents/parse-tool-call.js +9 -22
  20. package/dist/agents/pi-harness.js +1 -2
  21. package/dist/agents/pi-sdk-session-manager.js +8 -0
  22. package/dist/agents/pi-sdk-tools.js +180 -5
  23. package/dist/agents/pi-sdk.js +210 -503
  24. package/dist/agents/process-runner.js +28 -9
  25. package/dist/agents/react-loop.js +9 -17
  26. package/dist/agents/runner-resolver.js +4 -3
  27. package/dist/agents/skill-loader.js +22 -15
  28. package/dist/agents/skills-manager.js +1 -25
  29. package/dist/agents/workflow-pivot-loop.js +152 -61
  30. package/dist/bootstrap/context-collector.js +67 -7
  31. package/dist/bootstrap/context-hierarchy.js +13 -1
  32. package/dist/bootstrap/persona-loader.js +26 -0
  33. package/dist/cli/ink-app.js +44 -17
  34. package/dist/cli/intent-command.js +22 -12
  35. package/dist/cli/loading-tui.js +8 -2
  36. package/dist/cli/log-gate.js +40 -8
  37. package/dist/cli/mention-data.js +1 -0
  38. package/dist/cli/reply-hygiene.js +22 -0
  39. package/dist/cli/trace-line.js +61 -2
  40. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  41. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  42. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  43. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  44. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  45. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  46. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  47. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  48. package/dist/electron-build/agents/contacts/tools.js +112 -0
  49. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  50. package/dist/electron-build/agents/experience-review.js +0 -3
  51. package/dist/electron-build/agents/experience-review.js.map +1 -1
  52. package/dist/electron-build/agents/external-events.js.map +1 -1
  53. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  55. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  56. package/dist/electron-build/agents/goal-resume.js +0 -4
  57. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  58. package/dist/electron-build/agents/intent-classifier.js +1 -1
  59. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  60. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  61. package/dist/electron-build/agents/loop-review.js +2 -2
  62. package/dist/electron-build/agents/loop-review.js.map +1 -1
  63. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  64. package/dist/electron-build/agents/memory-store.js +430 -0
  65. package/dist/electron-build/agents/memory-store.js.map +1 -0
  66. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  67. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  68. package/dist/electron-build/agents/pi-harness.js +1 -2
  69. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  70. package/dist/electron-build/agents/pi-sdk-session-manager.js +8 -0
  71. package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
  72. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  73. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  74. package/dist/electron-build/agents/pi-sdk.js +208 -501
  75. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  76. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  77. package/dist/electron-build/agents/process-runner.js +28 -9
  78. package/dist/electron-build/agents/process-runner.js.map +1 -1
  79. package/dist/electron-build/agents/runner-resolver.js +4 -3
  80. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  81. package/dist/electron-build/agents/skill-loader.js +22 -16
  82. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  83. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  84. package/dist/electron-build/agents/skills-manager.js +0 -24
  85. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  86. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  87. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  88. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  89. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  90. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  91. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  92. package/dist/electron-build/bootstrap/context-collector.js +68 -7
  93. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  94. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  95. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  96. package/dist/electron-build/bootstrap/persona-loader.js +27 -0
  97. package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
  98. package/dist/electron-build/electron-preload.js.map +1 -1
  99. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  100. package/dist/electron-build/kernel/channel-actor.js +31 -2
  101. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  102. package/dist/electron-build/kernel/model-runtime.js +8 -1
  103. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  104. package/dist/electron-build/llm/config-store.js +20 -0
  105. package/dist/electron-build/llm/config-store.js.map +1 -1
  106. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  107. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  108. package/dist/electron-build/llm/pi-ai.js +112 -38
  109. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  110. package/dist/electron-build/llm/system-prompt/registry.js +8 -6
  111. package/dist/electron-build/llm/system-prompt/registry.js.map +1 -1
  112. package/dist/electron-build/network/agent-network.js.map +1 -1
  113. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  114. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  115. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  116. package/dist/electron-build/security/tool-gate.js +2 -0
  117. package/dist/electron-build/security/tool-gate.js.map +1 -1
  118. package/dist/electron-build/setup/setup-store.js +34 -6
  119. package/dist/electron-build/setup/setup-store.js.map +1 -1
  120. package/dist/electron-build/social/heartbeat.js +18 -5
  121. package/dist/electron-build/social/heartbeat.js.map +1 -1
  122. package/dist/electron-build/utils/update-manager.js.map +1 -1
  123. package/dist/index.js +250 -12
  124. package/dist/ios/bolloon-web.json +3 -3
  125. package/dist/ios/client.js +74 -35
  126. package/dist/ios/icons/icon-1400x1400.png +0 -0
  127. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  128. package/dist/ios/index.html +122 -108
  129. package/dist/ios/mobile-agent.js +572 -45
  130. package/dist/ios/mobile-contacts.js +0 -4
  131. package/dist/ios/mobile-core.js +8079 -4590
  132. package/dist/ios/mobile-harness.js +1004 -0
  133. package/dist/ios/mobile-memory.js +317 -0
  134. package/dist/ios/mobile-orbitdb.js +194 -0
  135. package/dist/ios/mobile-persona.js +80 -0
  136. package/dist/ios/mobile-privacy.js +2 -3
  137. package/dist/ios/mobile.css +286 -21
  138. package/dist/ios/mobile.html +122 -108
  139. package/dist/ios/mobile.js +434 -152
  140. package/dist/ios/routes-contacts.js +94 -1
  141. package/dist/ios/routes-world.js +13 -2
  142. package/dist/ios/server.js +54 -41
  143. package/dist/ios/style.css +13 -5
  144. package/dist/ios/sw.js +1 -1
  145. package/dist/ios/ui/message-renderer.js +4 -18
  146. package/dist/ios/ui/step-timeline.js +14 -1
  147. package/dist/kernel/channel-actor.js +30 -2
  148. package/dist/kernel/gate-scan.js +2 -2
  149. package/dist/kernel/ha/ha-arbiter.js +177 -0
  150. package/dist/kernel/ha/ha-guard.js +151 -0
  151. package/dist/kernel/index.js +36 -0
  152. package/dist/kernel/model-runtime.js +8 -1
  153. package/dist/kernel/plan.js +5 -5
  154. package/dist/kernel/roster.js +3 -3
  155. package/dist/llm/config-store.js +20 -0
  156. package/dist/llm/embedding-adapter.js +69 -0
  157. package/dist/llm/pi-ai.js +110 -37
  158. package/dist/llm/system-prompt/registry.js +8 -6
  159. package/dist/orbitdb/memory-sync.js +70 -0
  160. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  161. package/dist/security/tool-gate.js +2 -0
  162. package/dist/setup/setup-store.js +34 -6
  163. package/dist/social/heartbeat.js +18 -5
  164. package/dist/web/bolloon-web.json +3 -3
  165. package/dist/web/client.js +74 -35
  166. package/dist/web/icons/icon-1400x1400.png +0 -0
  167. package/dist/web/icons/icon-1400x1400.webp +0 -0
  168. package/dist/web/index.html +37 -25
  169. package/dist/web/mobile-agent.js +572 -45
  170. package/dist/web/mobile-contacts.js +0 -4
  171. package/dist/web/mobile-core.js +8079 -4590
  172. package/dist/web/mobile-harness.js +1004 -0
  173. package/dist/web/mobile-memory.js +317 -0
  174. package/dist/web/mobile-orbitdb.js +194 -0
  175. package/dist/web/mobile-persona.js +80 -0
  176. package/dist/web/mobile-privacy.js +2 -3
  177. package/dist/web/mobile.css +286 -21
  178. package/dist/web/mobile.html +122 -108
  179. package/dist/web/mobile.js +434 -152
  180. package/dist/web/routes-contacts.js +94 -1
  181. package/dist/web/routes-world.js +13 -2
  182. package/dist/web/server.js +54 -41
  183. package/dist/web/style.css +13 -5
  184. package/dist/web/sw.js +1 -1
  185. package/dist/web/ui/message-renderer.js +4 -18
  186. package/dist/web/ui/step-timeline.js +14 -1
  187. package/package.json +3 -2
  188. package/scripts/build-web.ts +16 -1
@@ -0,0 +1,177 @@
1
+ /**
2
+ * ha-arbiter.ts — Kernel 双活仲裁器 (2026-10-16, leo: 双内核冗余第一刀)
3
+ *
4
+ * 职责: 同机两个 bolloon 进程各自加载 kernel, 通过**心跳文件**互相探测,
5
+ * 主进程死亡时备进程接管其 orphan run (复用 reconcileOrphans + prepareResume)。
6
+ *
7
+ * 设计 (对齐 tick-lock 跨进程先例):
8
+ * - 心跳文件 `~/.bolloon/kernel-ha/<role>.heartbeat` = JSON {pid, host, role, startedAt, lastBeat, runIds[], kernelVersion}
9
+ * - 主进程每 BEAT_MS (10s) 写自己的心跳 (含当前 runIds); 备进程每 BEAT_MS 探测主心跳
10
+ * - 角色仲裁: 抢 `~/.bolloon/kernel-ha/.primary.lock` (O_EXCL 原子创建) — 防双主
11
+ * - 主心跳过期 (lastBeat 超过 STALE_MS=30s 或 pid 已死) → 备**接管**:
12
+ * ① 标记本机 role=primary (抢锁)
13
+ * ② reconcileOrphans() 找主进程留下的 orphan run
14
+ * ③ 逐个 prepareResume(runId) 确认可恢复 → 记入本机心跳 runIds
15
+ * ④ 心跳文件切 role=primary
16
+ * - 纯函数可单测: parseHeartbeat / isHeartbeatStale / decideTakeover / serializeHeartbeat
17
+ */
18
+ import * as fs from 'fs/promises';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ export const BEAT_MS = 10_000; // 心跳间隔
22
+ export const STALE_MS = 30_000; // 心跳过期阈值 (3 倍间隔)
23
+ export const PRIMARY_LOCK_STALE_MS = 60_000;
24
+ /** HA 数据目录 ~/.bolloon/kernel-ha */
25
+ export function haDir(home = os.homedir()) {
26
+ return path.join(home, '.bolloon', 'kernel-ha');
27
+ }
28
+ export function heartbeatPath(role, home = os.homedir()) {
29
+ return path.join(haDir(home), `${role}.heartbeat`);
30
+ }
31
+ export function primaryLockPath(home = os.homedir()) {
32
+ return path.join(haDir(home), '.primary.lock');
33
+ }
34
+ // ---------------------------------------------------------------- 纯函数
35
+ /** 序列化心跳 (原子写用) */
36
+ export function serializeHeartbeat(rec) {
37
+ return JSON.stringify(rec, null, 2);
38
+ }
39
+ /** 解析心跳文件内容 — 坏行/畸形一律 ok:false (fail-closed) */
40
+ export function parseHeartbeat(raw) {
41
+ try {
42
+ const p = JSON.parse(raw);
43
+ if (!p || typeof p !== 'object')
44
+ return { ok: false, error: 'not-object' };
45
+ if (typeof p.pid !== 'number' || typeof p.host !== 'string' || typeof p.role !== 'string') {
46
+ return { ok: false, error: 'missing-fields' };
47
+ }
48
+ if (p.role !== 'primary' && p.role !== 'standby')
49
+ return { ok: false, error: 'bad-role' };
50
+ const record = {
51
+ pid: p.pid,
52
+ host: p.host,
53
+ role: p.role,
54
+ startedAt: typeof p.startedAt === 'string' ? p.startedAt : new Date().toISOString(),
55
+ lastBeat: typeof p.lastBeat === 'string' ? p.lastBeat : new Date().toISOString(),
56
+ runIds: Array.isArray(p.runIds) ? p.runIds.filter((x) => typeof x === 'string') : [],
57
+ kernelVersion: typeof p.kernelVersion === 'string' ? p.kernelVersion : '',
58
+ };
59
+ return { ok: true, record };
60
+ }
61
+ catch (e) {
62
+ return { ok: false, error: String(e?.message || e).slice(0, 80) };
63
+ }
64
+ }
65
+ /** 心跳是否过期: lastBeat 距今 > staleMs, 或 pid 已不存在 */
66
+ export function isHeartbeatStale(rec, now = Date.now(), staleMs = STALE_MS) {
67
+ const last = Date.parse(rec.lastBeat);
68
+ if (!Number.isFinite(last))
69
+ return true; // 坏时间 → 判过期 (fail-closed)
70
+ if (now - last > staleMs)
71
+ return true;
72
+ // pid 不存在 = 进程已死 → 判过期
73
+ try {
74
+ process.kill(rec.pid, 0);
75
+ return false;
76
+ }
77
+ catch {
78
+ return true;
79
+ }
80
+ }
81
+ /**
82
+ * 是否接管: 主心跳缺失 / 解析失败 / 过期 → true。
83
+ * 纯函数 (不含抢锁副作用), 调用方拿到 true 再走 acquirePrimaryLock。
84
+ */
85
+ export function decideTakeover(primaryBeat, now = Date.now(), staleMs = STALE_MS) {
86
+ if (!primaryBeat || !primaryBeat.ok)
87
+ return { takeover: true, reason: 'primary-heartbeat-missing' };
88
+ const rec = primaryBeat.record;
89
+ if (isHeartbeatStale(rec, now, staleMs))
90
+ return { takeover: true, reason: 'primary-heartbeat-stale' };
91
+ return { takeover: false, reason: 'primary-alive' };
92
+ }
93
+ // ---------------------------------------------------------------- 副作用 (原子写 / 锁)
94
+ /** 原子写心跳文件 (tmp + rename) */
95
+ export async function writeHeartbeat(rec, home = os.homedir()) {
96
+ await fs.mkdir(haDir(home), { recursive: true });
97
+ const target = heartbeatPath(rec.role, home);
98
+ const tmp = target + '.tmp';
99
+ await fs.writeFile(tmp, serializeHeartbeat(rec), { mode: 0o600 });
100
+ await fs.rename(tmp, target);
101
+ }
102
+ /** 抢主锁 (O_EXCL 原子创建; 已存在且持锁者存活 → 未抢到; 陈旧 → 回收重试一次) */
103
+ export async function acquirePrimaryLock(pid, host, home = os.homedir(), staleMs = PRIMARY_LOCK_STALE_MS) {
104
+ const lockPath = primaryLockPath(home);
105
+ await fs.mkdir(haDir(home), { recursive: true });
106
+ const content = JSON.stringify({ pid, host, at: new Date().toISOString() });
107
+ try {
108
+ await fs.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
109
+ return { acquired: true, reason: 'created' };
110
+ }
111
+ catch (e) {
112
+ if (e?.code !== 'EEXIST')
113
+ return { acquired: false, reason: String(e?.message || e).slice(0, 80) };
114
+ // 已存在 → 判陈旧
115
+ let holder = {};
116
+ try {
117
+ holder = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
118
+ }
119
+ catch { /* 坏锁文件 → 视为陈旧 */ }
120
+ const holderAlive = holder && typeof holder.pid === 'number' && (() => { try {
121
+ process.kill(holder.pid, 0);
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ } })();
127
+ const fresh = holderAlive;
128
+ if (fresh)
129
+ return { acquired: false, reason: 'held-by-alive-primary' };
130
+ // 陈旧 → 回收重试一次
131
+ try {
132
+ await fs.unlink(lockPath);
133
+ }
134
+ catch { /* 忽略 */ }
135
+ try {
136
+ await fs.writeFile(lockPath, content, { flag: 'wx', mode: 0o600 });
137
+ return { acquired: true, reason: 'reclaimed-stale' };
138
+ }
139
+ catch {
140
+ return { acquired: false, reason: 'reclaim-race-lost' };
141
+ }
142
+ }
143
+ }
144
+ /** 读主心跳 (不存在 → null; 坏内容 → ok:false) */
145
+ export async function readPrimaryHeartbeat(home = os.homedir()) {
146
+ try {
147
+ const raw = await fs.readFile(heartbeatPath('primary', home), 'utf-8');
148
+ return parseHeartbeat(raw);
149
+ }
150
+ catch (e) {
151
+ if (e?.code === 'ENOENT')
152
+ return null; // 主心跳还没写过 = 缺失
153
+ return { ok: false, error: String(e?.message || e).slice(0, 80) };
154
+ }
155
+ }
156
+ /** 当前活跃 runIds: 扫 runs 目录里 status ∈ running/recovering/interrupted 的 run */
157
+ export async function collectActiveRunIds(runsDir, now = Date.now()) {
158
+ try {
159
+ const names = await fs.readdir(runsDir);
160
+ const out = [];
161
+ for (const n of names) {
162
+ if (!n.endsWith('.json') || n.endsWith('.bak'))
163
+ continue;
164
+ try {
165
+ const rec = JSON.parse(await fs.readFile(path.join(runsDir, n), 'utf-8'));
166
+ if (rec && typeof rec.status === 'string' && ['running', 'recovering', 'interrupted'].includes(rec.status)) {
167
+ out.push(n.replace(/\.json$/, ''));
168
+ }
169
+ }
170
+ catch { /* 坏 run 文件跳过 */ }
171
+ }
172
+ return out;
173
+ }
174
+ catch {
175
+ return [];
176
+ }
177
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ha-guard.ts — Kernel 双活守护循环 (2026-10-16, leo: 双内核冗余 — 独立 kernel 守护进程)
3
+ *
4
+ * 职责: 心跳循环 + 对等探测 + 接管 orphan run。
5
+ *
6
+ * 边界 (K1 目录边界: kernel 只做机制, 不碰业务):
7
+ * - 本文件**不 import** 任何业务模块 (run-store / supervisor 等);
8
+ * - reconcileOrphans / prepareResume / collectActiveRunIds 全部**端口注入**,
9
+ * 由调用方 (主进程) 把业务实现传进来 —— 守护循环只负责「什么时候探测/什么时候接管/接管几步」。
10
+ *
11
+ * 循环语义:
12
+ * - 每 BEAT_MS 写自己的心跳 (含当前活跃 runIds, 由注入的 collectActiveRunIds 提供)
13
+ * - 每 BEAT_MS 读对端 (primary) 心跳 → decideTakeover
14
+ * - 判定接管 → acquirePrimaryLock (防双主) → 接管步骤 (注入的 takeoverRuns):
15
+ * ① reconcileOrphans() 找主进程留下的 orphan run
16
+ * ② 逐个 prepareResume(runId) 确认可恢复
17
+ * ③ 记入自己的心跳 runIds
18
+ * ④ 自己的心跳 role 切 primary
19
+ * - 一次接管只做一轮; 下轮心跳若 primary 复活 (心跳新鲜) → 备进程让位 (不主动降级, 由仲裁决定)
20
+ *
21
+ * 退出: stop() 幂等; 进程退出时 best-effort 清心跳 (防止陈旧心跳误导对端判接管)。
22
+ */
23
+ import { BEAT_MS, STALE_MS, writeHeartbeat, readPrimaryHeartbeat, decideTakeover, acquirePrimaryLock, } from './ha-arbiter.js';
24
+ import * as fsSync from 'fs';
25
+ import * as os from 'os';
26
+ import * as path from 'path';
27
+ const noop = () => { };
28
+ /** 构造一条心跳记录 */
29
+ export function makeHeartbeat(role, runIds, kernelVersion = '') {
30
+ const now = new Date().toISOString();
31
+ return {
32
+ pid: process.pid,
33
+ host: os.hostname(),
34
+ role,
35
+ startedAt: now,
36
+ lastBeat: now,
37
+ runIds: runIds || [],
38
+ kernelVersion: String(kernelVersion || '').slice(0, 40),
39
+ };
40
+ }
41
+ /**
42
+ * 一次探测+可能的接管 (纯编排, 可单测)。
43
+ * 返回 { action: 'beat' | 'takeover' | 'noop' } — noop = 本进程是 primary, 只写心跳。
44
+ */
45
+ export async function guardTick(ports, opts, state) {
46
+ const log = opts.log || ((_l, _m) => { });
47
+ const home = opts.home || os.homedir();
48
+ const beatMs = opts.beatMs ?? BEAT_MS;
49
+ const staleMs = opts.staleMs ?? STALE_MS;
50
+ const collect = ports.collectActiveRunIds || (async () => []);
51
+ // ① 收集当前活跃 runIds (本机)
52
+ const runIds = await collect();
53
+ if (opts.role === 'primary') {
54
+ // 主进程: 写心跳, 什么都不用探测 (备进程会探测我)
55
+ const rec = makeHeartbeat('primary', runIds, opts.kernelVersion);
56
+ await writeHeartbeat(rec, home);
57
+ state.lastBeatAt = rec.lastBeat;
58
+ state.beats++;
59
+ if (opts.onBeat)
60
+ opts.onBeat(rec);
61
+ return { action: 'noop' };
62
+ }
63
+ // ② 备进程: 探测主进程心跳
64
+ const primaryBeat = await readPrimaryHeartbeat(home);
65
+ const decision = decideTakeover(primaryBeat, Date.now(), staleMs);
66
+ if (!decision.takeover) {
67
+ // 主还活着 → 备待命 (写自己的 standby 心跳, 供观察; 不接管)
68
+ const rec = makeHeartbeat('standby', runIds, opts.kernelVersion);
69
+ await writeHeartbeat(rec, home);
70
+ state.lastBeatAt = rec.lastBeat;
71
+ state.beats++;
72
+ if (opts.onBeat)
73
+ opts.onBeat(rec);
74
+ // 2026-10-16: 待命 tick 打一条 (verify/诊断用; 生产可忽略)
75
+ if (process.env.BOLLOON_HA_DEBUG === '1')
76
+ log('info', `standby 待命 beat#${state.beats} (primary alive)`);
77
+ return { action: 'beat' };
78
+ }
79
+ // ③ 判定接管: 抢主锁 (防双主), 抢到才接
80
+ const lock = await acquirePrimaryLock(process.pid, os.hostname(), home);
81
+ if (!lock.acquired) {
82
+ log('warn', `接管被拒: ${lock.reason} (另一进程已抢到主锁)`);
83
+ return { action: 'noop' };
84
+ }
85
+ // ④ 接管步骤: reconcileOrphans → prepareResume 逐个 → 记入心跳 runIds
86
+ let adopted = [];
87
+ try {
88
+ const orphan = await ports.reconcileOrphans();
89
+ adopted = [...orphan.interrupted];
90
+ const resumed = [];
91
+ for (const runId of orphan.interrupted) {
92
+ const r = await ports.prepareResume(runId);
93
+ if (r.ok)
94
+ resumed.push(runId);
95
+ else
96
+ log('warn', `接管 ${runId} 未恢复: ${r.reason || ''}`);
97
+ }
98
+ adopted = resumed;
99
+ // 接管后本机心跳提升为 primary (包含接管的 run)
100
+ const rec = makeHeartbeat('primary', [...runIds, ...resumed], opts.kernelVersion);
101
+ await writeHeartbeat(rec, home);
102
+ state.lastBeatAt = rec.lastBeat;
103
+ state.adoptedRunIds = resumed;
104
+ state.role = 'primary';
105
+ state.takeovers++;
106
+ if (opts.onTakeover)
107
+ opts.onTakeover({ adoptedRunIds: resumed, reason: decision.reason });
108
+ return { action: 'takeover', adoptedRunIds: resumed, reason: decision.reason };
109
+ }
110
+ catch (e) {
111
+ log('error', `接管失败: ${String(e?.message || e).slice(0, 200)}`);
112
+ return { action: 'noop' };
113
+ }
114
+ }
115
+ /** 持续守护循环 (每 beatMs 一次 guardTick); stop() 幂等停止 */
116
+ export function startHaGuard(ports, opts) {
117
+ const state = { running: true, role: opts.role, beats: 0, takeovers: 0, lastBeatAt: null, adoptedRunIds: [] };
118
+ const beatMs = opts.beatMs ?? BEAT_MS;
119
+ let timer = null;
120
+ const tick = async () => {
121
+ if (!state.running)
122
+ return;
123
+ try {
124
+ await guardTick(ports, opts, state);
125
+ }
126
+ catch (e) {
127
+ (opts.log || noop)('error', `guardTick 异常: ${String(e?.message || e).slice(0, 160)}`);
128
+ }
129
+ };
130
+ // 首跳立即执行 (不等一个 beatMs)
131
+ void tick();
132
+ timer = setInterval(() => { void tick(); }, beatMs);
133
+ // 2026-10-16 修复: **不 unref** — daemon 是 spawn 出来的独立守护进程, 必须持续跑。
134
+ // 旧代码 unref() 导致事件循环只剩 interval 时 (stdio pipe 不算活跃句柄),
135
+ // daemon 首跳写完心跳后进程直接退出 → 永远等不到接管 (真跑验证撞出的硬坑)。
136
+ // 只有「不拖住宿主退出」的场景才该 unref (如 CLI 内嵌的心跳), daemon 不属于那种。
137
+ const stop = () => {
138
+ state.running = false;
139
+ if (timer) {
140
+ clearInterval(timer);
141
+ timer = null;
142
+ }
143
+ // best-effort 清自己的心跳 (防陈旧误导对端判接管) — 静态 fsSync (打包面禁 require)
144
+ try {
145
+ const f = path.join(opts.home || os.homedir(), '.bolloon', 'kernel-ha', `${opts.role}.heartbeat`);
146
+ fsSync.unlinkSync(f);
147
+ }
148
+ catch { /* 清理失败不影响退出 */ }
149
+ };
150
+ return { stop, state: () => ({ ...state }) };
151
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @bolloon/bolloon-kernel — Bolloon Native Macro-Kernel SDK 出口
3
+ *
4
+ * 进程内 Agent 内核机制层(统一调度 · 共享状态受控 · 端口注入 · 零业务依赖)。
5
+ *
6
+ * 设计页: docs/wiki/bolloon-native-macro-kernel.md
7
+ * 出口纪律:
8
+ * · 只导出「机制」—— 不导出任何业务判定(Kernel 判据: 不判"该不该继续"、不判"算不算完成")。
9
+ * · 只导出「运行时机制」—— gate-scan / roster / plan-* 是 Bolloon 仓库治理件(判据与台账),
10
+ * 面向本仓的门(kernel-boundary / kernel-authority / kernel-gate-scan 等测试)直接吃源码,
11
+ * 不进 SDK 出口。
12
+ * · 依赖面: 除 node: 内置外零依赖 —— 每个模块的 I/O 走端口注入,调用方给什么就用什么。
13
+ *
14
+ * 模块速览:
15
+ * channel-actor K5 ChannelActor + SerialMailbox(同 channel 串行、异 channel 并发、ALS 重入)
16
+ * model-runtime K6 ModelRuntime.acquire(snapshot) 只读租约(连接复用/超时/取消/退避/熔断)
17
+ * control 命令式控制面(pause / resume / abort / recover)
18
+ * run-lifecycle Run 生命周期写口(start-run / record-step / finish-run / save-checkpoint)
19
+ * session-lifecycle 会话规则(持久化映射 / 过滤水合 / 运行种子合并)
20
+ * run-resume 恢复编排(漂移上报 / 核对失败不阻塞 / 单次语义)
21
+ * turn-selfcheck 回合末自检编排(fail-closed: 被拒/门抛错 ⇒ 零执行且可见)
22
+ * code-write-gate 写代码工具判定(哪个写操作产出 TS · 收尾要不要跑 tsc)
23
+ * transport 通信端口(send / broadcast / peers,未注入即拒)
24
+ */
25
+ export * from './channel-actor.js';
26
+ export * from './model-runtime.js';
27
+ export * from './control.js';
28
+ export * from './run-lifecycle.js';
29
+ export * from './session-lifecycle.js';
30
+ export * from './run-resume.js';
31
+ export * from './turn-selfcheck.js';
32
+ export * from './code-write-gate.js';
33
+ export * from './transport.js';
34
+ // 2026-10-16 (leo: 双内核冗余): Kernel 双活 — 心跳仲裁 (纯机制) + 守护循环 (端口注入)
35
+ export * from './ha/ha-arbiter.js';
36
+ export * from './ha/ha-guard.js';
@@ -179,7 +179,14 @@ export class ModelRuntime {
179
179
  call: async (req, opts) => {
180
180
  if (released)
181
181
  return { ok: false, error: '租约已归还' };
182
- const budget = snapshotReadonly.timeoutMs ?? this.defaultTimeoutMs;
182
+ // 2026-10-13 (leo: 「多步执行的任务会返回空」根因链的②): 30s 硬超时与上下文长度脱钩 ——
183
+ // 多步任务工具多轮后 prompt 膨胀到 40KB+, 模型生成要更久, 30s 一到就被杀。
184
+ // 预算 = max(配置/默认, 上下文长度自适应): 每 1KB 上下文给 4s, 再加 10s 生成余量。
185
+ // 长上下文不被误杀; 短上下文仍是 30s 底线 (供应商真慢照样暴露, 不掩盖问题)。
186
+ const baseBudget = snapshotReadonly.timeoutMs ?? this.defaultTimeoutMs;
187
+ const ctxChars = String(req?.context ?? '').length + String(req?.systemPrompt ?? '').length;
188
+ const adaptive = Math.ceil(ctxChars / 1024) * 4_000 + 10_000;
189
+ const budget = Math.max(baseBudget, adaptive);
183
190
  const external = opts?.signal;
184
191
  if (external?.aborted) {
185
192
  self.stats.aborts += 1;
@@ -79,7 +79,7 @@ export const ENTRY_GRAPH = [
79
79
  { file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K4', note: "runExecution 的流式派发 (K5 步骤⑦ 新增)" },
80
80
  { file: 'agents/pi-sdk.ts', kind: 'adapter-internal', method: 'promptWithPivotLoop', count: 2, path: 'direct', targetPhase: 'K4', note: "适配器内部 pivot 分派 (不是外部入口)" },
81
81
  { file: 'cli/interface.ts', kind: 'readline-tui', method: 'prompt', count: 3, path: 'queued', targetPhase: 'K3', note: "readline 输入提示, 非 agent 入口" },
82
- { file: 'index.ts', kind: 'agent-entry', method: 'prompt', count: 7, path: 'direct', targetPhase: 'K3', note: "CLI 装配点" },
82
+ { file: 'index.ts', kind: 'agent-entry', method: 'prompt', count: 6, path: 'direct', targetPhase: 'K3', note: "CLI 装配点 (2026-10-16 并行线: prompt 调用点 7→6)" },
83
83
  { file: 'index.ts', kind: 'agent-entry', method: 'promptStream', count: 1, path: 'direct', targetPhase: 'K3', note: "CLI 装配点" },
84
84
  ];
85
85
  /**
@@ -87,7 +87,7 @@ export const ENTRY_GRAPH = [
87
87
  * ⚠️ 24 → **25** (2026-10-02, K5 步骤⑦): `runExecution` 内部新增 `prompt`/`promptStream` 派发各 1 处 (+2),
88
88
  * web/server.ts 用户消息路径改成请求式 (-1)。**这是"形态变化"不是"旁路复活"** —— 见 ENTRY_GRAPH 的两行 adapter-internal。
89
89
  */
90
- export const ENTRY_DIRECT_CALLS_FROZEN_AT = 13; // 2026-10-02: 20 → 16 → 13 (K8 第四/五步: server.ts 7 处全迁; 只剩 CLI 侧 index.ts 的 8 处) // 2026-10-02: 25 → 20 (K8: 5 处改走 runExecution; 棘轮只许降)
90
+ export const ENTRY_DIRECT_CALLS_FROZEN_AT = 12; // 2026-10-16: 13 → 12 (并行线 src/index.ts prompt 调用点 7→6) // 2026-10-02: 20 → 16 → 13 (K8 第四/五步: server.ts 7 处全迁; 只剩 CLI 侧 index.ts 的 8 处) // 2026-10-02: 25 → 20 (K8: 5 处改走 runExecution; 棘轮只许降)
91
91
  export const DELETION_LEDGER = [
92
92
  {
93
93
  target: 'src/constraint-runtime/src/migrations/',
@@ -144,6 +144,6 @@ export const DELETION_LEDGER = [
144
144
  },
145
145
  ];
146
146
  /** 第一批删除候选 = 产品码里 **0 入边引用** 且非入口形态 (机械派生, 不手写) */
147
- export const DELETION_CANDIDATE_COUNT = 94;
148
- export const DELETION_CANDIDATE_SHA256 = '8ec437279513ddb82e0d909338c84d003486c3905143615b2b86f76d0d070112';
149
- export const DELETION_CANDIDATE_DIRS = { "agents": 5, "bollharness-integration": 1, "bollharness": 61, "cli": 1, "(根)": 1, "judgeness": 3, "llm": 1, "network": 3, "orbitdb": 2, "scripts": 1, "social": 2, "utils": 3, "web": 10 };
147
+ export const DELETION_CANDIDATE_COUNT = 100;
148
+ export const DELETION_CANDIDATE_SHA256 = '8f70bd1370cdb1c37aabdb19feb9d46d55fb51b8072b0ecf4f77f51b618a9478';
149
+ export const DELETION_CANDIDATE_DIRS = { "agents": 10, "bollharness-integration": 1, "bollharness": 61, "cli": 1, "(根)": 1, "judgeness": 3, "llm": 1, "network": 3, "orbitdb": 2, "scripts": 1, "social": 2, "utils": 3, "web": 11 };
@@ -100,7 +100,7 @@ export const KERNEL_ALLOWED_IMPORT_PREFIXES = ['kernel/'];
100
100
  * 判据是**双向相等**: 盘上多一个未登记文件 ⇒ 红; 名册有而盘上没有 ⇒ 红。
101
101
  * (先例: SEAM_ROSTER 的「名册外无人越界」)
102
102
  */
103
- export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/run-lifecycle.ts', 'kernel/transport.ts', 'kernel/session-lifecycle.ts', 'kernel/code-write-gate.ts', 'kernel/turn-selfcheck.ts', 'kernel/run-resume.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/plan-harness.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan-communication.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
103
+ export const KERNEL_FILES = ['kernel/channel-actor.ts', 'kernel/control.ts', 'kernel/run-lifecycle.ts', 'kernel/transport.ts', 'kernel/session-lifecycle.ts', 'kernel/code-write-gate.ts', 'kernel/turn-selfcheck.ts', 'kernel/run-resume.ts', 'kernel/gate-scan.ts', 'kernel/model-runtime.ts', 'kernel/index.ts', 'kernel/ha/ha-arbiter.ts', 'kernel/ha/ha-guard.ts', 'kernel/plan-harness.ts', 'kernel/plan-modelruntime.ts', 'kernel/plan-constraint.ts', 'kernel/plan-channel-actor.ts', 'kernel/plan-deletion.ts', 'kernel/plan-runcontext.ts', 'kernel/plan-communication.ts', 'kernel/plan.ts', 'kernel/roster.ts'];
104
104
  /** Goal 事实的写入口 (goal-store.ts 的导出中带副作用的那些) */
105
105
  export const GOAL_WRITES = [
106
106
  'createGoal',
@@ -230,9 +230,9 @@ export const AUTHORITY_DEBT_FROZEN_AT = 0; // 3 → 0 (K4 用内核控制面还
230
230
  * 目的只有一个: **不许所有逻辑回流到 kernel.ts**。要加就得显式抬这个数字, 留下痕迹。
231
231
  * 数值 = 当前 kernel 目录真实行数, 不留余量。
232
232
  */
233
- export const KERNEL_LINE_BUDGET = 3852; // 2026-10-02: 收尾自检规则 `kernel/code-write-gate.ts` 从 agents 侧整条搬入 (+49, 减法: agent 侧少一个模块) // 2026-10-02: 会话生命周期端口 `kernel/session-lifecycle.ts` (278 行) + `gate-scan` 播种锚点跟形状 (+4) —— **显式抬档 +282** (K10 余项; pi-sdk 本批 -18 行); // 2026-10-02 **显式抬档 +22** (K8 收尾: channel-actor 加 `abortActorsOfChannel` + 台账/门同步) // 2026-10-02 **显式抬档 +16** (K10 ④ 后半: transport.ts 加同步读入口 `transportPeersSync` + `peersSync` 端口) // 2026-10-02 **显式抬档 +145** (K10 ④: 新增内核模块 kernel/transport.ts —— 通信入口端口) // 2026-10-02 **显式抬档 +184** (K10 ①: 新增内核模块 kernel/run-lifecycle.ts —— 运行生命周期写口, 与 control.ts 的控制面分开; 预算 == 真实值) // 2026-10-02: +1 (K4-B: gate-scan 复位阈值注释) // 2026-10-02: +5 (K8 正刀: gate-scan 加"收口字段数 3→1"+ queue 反回归) // 2026-10-02: +54 (K8: gate-scan 加 K8 判据) // 2026-10-02: +2 (STAGE_STATUS 拆 K4-A/K4-B + K6/K7 如实修正)
233
+ export const KERNEL_LINE_BUDGET = 4302; // 2026-10-16: **显式抬档 +379** (重构: 删重复 re-export + fsSync 泄漏, 4304→4302) (Kernel 双活: 新增 kernel/ha/ha-arbiter.ts 心跳仲裁 + kernel/ha/ha-guard.ts 守护循环, 纯机制零业务 import; 守护进程宿主在 src/agents/ha-daemon.ts 不占 kernel 预算) // 2026-10-16: **显式抬档 +38** (10-13/14 两笔 loop 机制: model-runtime 超时预算按上下文长度自适应 +8 / channel-actor Ctrl+C 取消信号 ALS 注入 +33, 均无可删的真实功能) // 2026-10-12: **显式抬档 +33** (kernel SDK 独立 npm 包: 新增 `kernel/index.ts` 出口, 零业务 import 纯 `export *`) // 2026-10-02: 收尾自检规则 `kernel/code-write-gate.ts` 从 agents 侧整条搬入 (+49, 减法: agent 侧少一个模块) // 2026-10-02: 会话生命周期端口 `kernel/session-lifecycle.ts` (278 行) + `gate-scan` 播种锚点跟形状 (+4) —— **显式抬档 +282** (K10 余项; pi-sdk 本批 -18 行); // 2026-10-02 **显式抬档 +22** (K8 收尾: channel-actor 加 `abortActorsOfChannel` + 台账/门同步) // 2026-10-02 **显式抬档 +16** (K10 ④ 后半: transport.ts 加同步读入口 `transportPeersSync` + `peersSync` 端口) // 2026-10-02 **显式抬档 +145** (K10 ④: 新增内核模块 kernel/transport.ts —— 通信入口端口) // 2026-10-02 **显式抬档 +184** (K10 ①: 新增内核模块 kernel/run-lifecycle.ts —— 运行生命周期写口, 与 control.ts 的控制面分开; 预算 == 真实值) // 2026-10-02: +1 (K4-B: gate-scan 复位阈值注释) // 2026-10-02: +5 (K8 正刀: gate-scan 加"收口字段数 3→1"+ queue 反回归) // 2026-10-02: +54 (K8: gate-scan 加 K8 判据) // 2026-10-02: +2 (STAGE_STATUS 拆 K4-A/K4-B + K6/K7 如实修正)
234
234
  /** 预算冻结值 (棘轮: 只许减; 想抬预算必须同时改上面那个数字 ⇒ 一次显式动作, diff 里看得见) */
235
- export const KERNEL_LINE_BUDGET_FROZEN_AT = 3852; // 同步至 2026-10-02 真实值 (K10 ④ 新增 transport.ts 后)
235
+ export const KERNEL_LINE_BUDGET_FROZEN_AT = 4302; // 2026-10-16: 随抬档同步 (Kernel 双活 +376) // 2026-10-16: 随抬档同步 (10-13/14 loop 机制增量)
236
236
  /**
237
237
  * K3b —— **台账数据**单独一档预算 (`src/kernel/plan.ts`)。
238
238
  *
@@ -225,6 +225,26 @@ function getDefaultConfig() {
225
225
  activeProvider = 'mimo';
226
226
  else if (process.env.OLLAMA_BASE_URL)
227
227
  activeProvider = 'ollama';
228
+ // ── 2026-10-07: 开箱即用 — 无任何私有 key 时落到内置公共网关 ──────────────
229
+ // 手机端/网页端下载安装后**不需要配置任何 API key** 就能直接对话:
230
+ // Cloudflare Worker 托管 (api.bolloon.cn) 转发到 GLM 上游, 公共通道每天 ≤500 次/人,
231
+ // 配了自己的 key 的机器不受限 (上面 env 检测先命中)。
232
+ const hasAnyKey = !!(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY
233
+ || process.env.OPENROUTER_API_KEY || process.env.GEMINI_API_KEY || process.env.MINIMAX_API_KEY
234
+ || process.env.DEEPSEEK_API_KEY || process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY
235
+ || process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || process.env.QWEN_API_KEY
236
+ || process.env.DASHSCOPE_API_KEY || process.env.MIMO_API_KEY || process.env.OLLAMA_BASE_URL);
237
+ if (!hasAnyKey) {
238
+ envConfigs.glm = {
239
+ ...DEFAULT_PROVIDER_CONFIGS.glm,
240
+ enabled: true,
241
+ baseUrl: 'https://api.bolloon.cn/v1', // CF Worker 公共网关 (免配置)
242
+ apiKey: 'bolloon-free', // 公共通道标识 (每天 ≤500 次/人)
243
+ model: 'glm-5.3',
244
+ maxTokens: 16384,
245
+ };
246
+ activeProvider = 'glm';
247
+ }
228
248
  const providers = { ...DEFAULT_PROVIDER_CONFIGS };
229
249
  for (const [provider, config] of Object.entries(envConfigs)) {
230
250
  if (config) {
@@ -0,0 +1,69 @@
1
+ /** none 实现: 无向量能力, 调用即抛 (由调用方按 provider 判走关键词回退) */
2
+ const noneAdapter = {
3
+ provider: 'none',
4
+ dim: 0,
5
+ async embed() {
6
+ throw new Error('embedding adapter 未配置 (provider=none); 调用方应先查 provider !== "none"');
7
+ },
8
+ async embedBatch(texts) {
9
+ throw new Error('embedding adapter 未配置 (provider=none); 调用方应先查 provider !== "none"');
10
+ },
11
+ };
12
+ /**
13
+ * 工厂: 从配置构造 adapter。
14
+ * 规则:
15
+ * - 无 provider / provider='none' → none adapter (关键词回退)
16
+ * - provider 有值但没有实现 → none + 说明 (不假装能用)
17
+ * - 新增供应商实现在此登记 (每实现一个, 加一个 case)
18
+ */
19
+ export function createEmbeddingAdapter(cfg = {}) {
20
+ const provider = String(cfg.provider || 'none').toLowerCase();
21
+ switch (provider) {
22
+ case 'none':
23
+ case '':
24
+ return noneAdapter;
25
+ // 预留登记位: 供应商 key 就绪后在此补实现 (见文件头诚实边界)
26
+ // case 'glm': return createGlmAdapter(cfg);
27
+ // case 'minimax': return createMiniMaxAdapter(cfg);
28
+ // case 'openai': return createOpenAIAdapter(cfg);
29
+ default:
30
+ return noneAdapter;
31
+ }
32
+ }
33
+ /** 测试辅助: 造一个确定性伪 adapter (cosine 检索测试用, 不碰网络) */
34
+ export function createFakeAdapter(dim = 8, seedText = 'seed') {
35
+ // 确定性伪向量: 文本长度 + 字符和 → 稳定可复现 (仅测试用)
36
+ const vec = (t) => {
37
+ const out = [];
38
+ const s = String(t || '');
39
+ for (let i = 0; i < dim; i++) {
40
+ const v = (s.length * (i + 1) + s.charCodeAt(i % Math.max(1, s.length)) * 0.01) % 1;
41
+ out.push(Number(v.toFixed(6)));
42
+ }
43
+ // 归一化 (cosine 需要)
44
+ const norm = Math.sqrt(out.reduce((a, b) => a + b * b, 0)) || 1;
45
+ return out.map((x) => x / norm);
46
+ };
47
+ return {
48
+ provider: 'fake',
49
+ dim,
50
+ embed: async (t) => vec(t),
51
+ embedBatch: async (ts) => ts.map(vec),
52
+ };
53
+ }
54
+ /** cosine 相似度 (纯函数, 供检索用; 维度不一致 → 0) */
55
+ export function cosineSimilarity(a, b) {
56
+ if (!a || !b || a.length === 0 || a.length !== b.length)
57
+ return 0;
58
+ let dot = 0;
59
+ let na = 0;
60
+ let nb = 0;
61
+ for (let i = 0; i < a.length; i++) {
62
+ dot += a[i] * b[i];
63
+ na += a[i] * a[i];
64
+ nb += b[i] * b[i];
65
+ }
66
+ if (na === 0 || nb === 0)
67
+ return 0;
68
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
69
+ }