@bolloon/bolloon-agent 0.1.41 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.comm/README.md +21 -0
  2. package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +7 -0
  3. package/CLAUDE.md +3 -0
  4. package/build/icon.icns +0 -0
  5. package/build/icon.ico +0 -0
  6. package/build/icon.png +0 -0
  7. package/build/tray.png +0 -0
  8. package/build/trayTemplate.png +0 -0
  9. package/dist/agents/intent-classifier.js +99 -0
  10. package/dist/agents/pi-sdk.js +1418 -67
  11. package/dist/agents/pre-tool-validator.js +2 -1
  12. package/dist/agents/shell-guard.js +9 -3
  13. package/dist/agents/shell-tool.js +28 -13
  14. package/dist/agents/task-state.js +224 -0
  15. package/dist/agents/workflow-pivot-loop.js +30 -0
  16. package/dist/bollharness-integration/gate-state-machine.js +13 -0
  17. package/dist/bollharness-integration/integration.js +71 -0
  18. package/dist/bollharness-integration/skill-adapter.js +52 -127
  19. package/dist/bootstrap/context-hierarchy.js +6 -4
  20. package/dist/cli/interface.js +28 -0
  21. package/dist/documents/reader.js +14 -0
  22. package/dist/electron/config.js +21 -0
  23. package/dist/electron/dialogs.js +108 -0
  24. package/dist/electron/first-run.js +170 -0
  25. package/dist/electron/ipc.js +20 -0
  26. package/dist/electron/logger.js +114 -0
  27. package/dist/electron/main.js +63 -0
  28. package/dist/electron/menu.js +145 -0
  29. package/dist/electron/paths.js +75 -0
  30. package/dist/electron/server.js +112 -0
  31. package/dist/electron/tray.js +111 -0
  32. package/dist/electron/window.js +108 -0
  33. package/dist/git-transport/chat-render.js +155 -0
  34. package/dist/git-transport/chat-repo.js +370 -0
  35. package/dist/git-transport/chat-types.js +23 -0
  36. package/dist/git-transport/chat-watch.js +102 -0
  37. package/dist/index.js +477 -28
  38. package/dist/llm/pi-ai.js +103 -27
  39. package/dist/network/local-inbox-bus.js +73 -0
  40. package/dist/network/p2p-direct.js +3 -4
  41. package/dist/pi-ecosystem-judgment/adaptive-scan.js +6 -2
  42. package/dist/pi-ecosystem-judgment/causal-judge.js +3 -3
  43. package/dist/pi-ecosystem-judgment/index.js +1 -1
  44. package/dist/security/tool-gate.js +11 -0
  45. package/dist/web/server.js +68 -4
  46. package/package.json +8 -3
  47. package/scripts/lefthook-helper.sh +69 -0
package/dist/llm/pi-ai.js CHANGED
@@ -20,21 +20,78 @@ export function getSystemPrependProvider() {
20
20
  export class PiAIModel {
21
21
  config;
22
22
  provider;
23
+ /** 单次 LLM HTTP 请求硬上限 (ms), 防止上游卡住挂死整个 loop. 可通过 BOLLOON_LLM_TIMEOUT 覆盖. */
24
+ requestTimeoutMs;
23
25
  constructor(config) {
24
26
  this.config = config;
25
27
  this.provider = config.provider;
28
+ const envTimeout = Number(process.env.BOLLOON_LLM_TIMEOUT);
29
+ this.requestTimeoutMs = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120_000;
26
30
  }
27
- async chat(message, context, signal) {
28
- const systemPrompt = await this.buildSystemPromptAsync(context);
29
- const messages = [
30
- { role: 'system', content: systemPrompt },
31
- { role: 'user', content: message }
32
- ];
31
+ /**
32
+ * 把外部 signal 和内部 timeout 合并: 任一触发都 abort.
33
+ * - 外部 signal 优先 (用户主动 abort)
34
+ * - 否则套 120s timeout
35
+ * - 任一不合法 (非 AbortSignal 实例) 时退到无 signal
36
+ */
37
+ combinedSignal(external) {
38
+ const valid = external instanceof AbortSignal ? external : undefined;
39
+ // Node 18+ 支持 AbortSignal.timeout, 旧版本兜底用 setTimeout 构造
40
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
41
+ const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
42
+ if (!valid)
43
+ return timeoutSignal;
44
+ // 合并两个 signal
45
+ const ctrl = new AbortController();
46
+ const onAbort = () => ctrl.abort();
47
+ valid.addEventListener('abort', onAbort, { once: true });
48
+ timeoutSignal.addEventListener('abort', onAbort, { once: true });
49
+ if (valid.aborted || timeoutSignal.aborted)
50
+ ctrl.abort();
51
+ return ctrl.signal;
52
+ }
53
+ return valid;
54
+ }
55
+ /**
56
+ * 与 LLM 对话.
57
+ *
58
+ * 支持三种调用形式:
59
+ * - 旧: chat(message: string, context?: string, signal?)
60
+ * 单一 user message + 附加 system context. 简单场景用.
61
+ * - 新: chat(messages: ChatMessage[], context?: string, signal?)
62
+ * 完整 messages 数组, 含 user/assistant/tool/system role.
63
+ * 工具调用场景必用 — 否则 LLM 看不到工具结果.
64
+ * - pivot loop 兼容: chat(context: string, systemPrompt: string, signal?)
65
+ * 第二参超过 2K 时视为 system prompt 覆盖, 避免把 46K context 当 user message 发送.
66
+ *
67
+ * 2026-06-17 (M3.5 调试): buildContext() 之前把所有 history 序列化成单字符串,
68
+ * LLM 看不到 tool 调用的真实结果,导致 CLI loop 卡死.
69
+ * 现在 messages 数组版本保留 role 语义, LLM 能正确看到工具返回.
70
+ */
71
+ async chat(messageOrMessages, contextOrSystemPrompt, signal) {
72
+ const systemPrompt = await this.buildSystemPromptAsync(contextOrSystemPrompt);
73
+ let messages;
74
+ if (Array.isArray(messageOrMessages)) {
75
+ messages = [{ role: 'system', content: systemPrompt }, ...messageOrMessages];
76
+ }
77
+ else if (contextOrSystemPrompt && contextOrSystemPrompt.length > 2000) {
78
+ const baseSystem = await this.buildSystemPromptAsync(undefined);
79
+ messages = [
80
+ { role: 'system', content: baseSystem + '\n\n' + contextOrSystemPrompt },
81
+ { role: 'user', content: messageOrMessages }
82
+ ];
83
+ }
84
+ else {
85
+ messages = [
86
+ { role: 'system', content: systemPrompt },
87
+ { role: 'user', content: messageOrMessages }
88
+ ];
89
+ }
33
90
  try {
34
91
  const response = await this.generateText({
35
92
  messages,
36
93
  temperature: 0.8,
37
- maxTokens: 8192, // 2026-06-15: 提到 8192, 避免长回答 (含表格/代码块) 被截断
94
+ maxTokens: 16384, // 2026-06-17: 提到 16384 agent 注入 16K+ system prompt + 8K tool defs 时, 8K 撞上限返回空 content (见 memory: bolloon-llm-empty-large-prompt)
38
95
  signal,
39
96
  });
40
97
  return { reply: response };
@@ -215,23 +272,42 @@ export class PiAIModel {
215
272
  temperature,
216
273
  max_tokens: maxTokens
217
274
  };
218
- const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {
219
- method: 'POST',
220
- headers: {
221
- 'Content-Type': 'application/json',
222
- 'Authorization': `Bearer ${apiKey}`
223
- },
224
- body: JSON.stringify(requestBody),
225
- // 2026-06-15: signal 字段防御 Node 22+ undici 强类型 AbortSignal, 非 AbortSignal 实例
226
- // (e.g. 误传的 { maxTokens: 1 } 对象) 会 throw "Expected signal to be AbortSignal".
227
- // 若不是 AbortSignal, 退到无 signal 调用, fetch 自然支持 timeout 由外层控制.
228
- signal: signal instanceof AbortSignal ? signal : undefined,
229
- });
230
- if (!response.ok) {
231
- throw new Error(`OpenAI API error: ${response.status}`);
275
+ // 2026-06-19: 一次命中优化 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
276
+ // 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
277
+ // 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
278
+ // 2026-06-19: 一次命中优化 — 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
279
+ // 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
280
+ // 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
281
+ let lastFinishReason = '';
282
+ for (let attempt = 0; attempt < 3; attempt++) {
283
+ const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Content-Type': 'application/json',
287
+ 'Authorization': `Bearer ${apiKey}`
288
+ },
289
+ body: JSON.stringify(requestBody),
290
+ signal: this.combinedSignal(signal),
291
+ });
292
+ if (!response.ok) {
293
+ throw new Error(`OpenAI API error: ${response.status}`);
294
+ }
295
+ const data = await response.json();
296
+ const choice = data.choices?.[0];
297
+ const content = choice?.message?.content || '';
298
+ lastFinishReason = choice?.finish_reason || '';
299
+ if (content) {
300
+ if (lastFinishReason === 'length') {
301
+ console.warn(`[pi-ai] hit max_tokens ceiling (model=${this.mapModel()}, max_tokens=${maxTokens}) — caller should trim prompt or raise cap`);
302
+ }
303
+ return content;
304
+ }
305
+ // 空 content: 200 但 content="" → minimax 上游偶发, 退避后重试
306
+ console.warn(`[pi-ai] attempt ${attempt + 1}/3: 空 content (finish_reason=${lastFinishReason}), 退避 1.5s 重试`);
307
+ await new Promise(resolve => setTimeout(resolve, 1500));
232
308
  }
233
- const data = await response.json();
234
- return data.choices?.[0]?.message?.content || '';
309
+ console.warn(`[pi-ai] 3 次重试都返回空 content (finish_reason=${lastFinishReason})`);
310
+ return ''; // 返回空让上层看到 [AI 服务调用失败]
235
311
  }
236
312
  async callAnthropic(messages, temperature, maxTokens, signal) {
237
313
  const apiKey = this.getApiKey();
@@ -255,7 +331,7 @@ export class PiAIModel {
255
331
  temperature,
256
332
  max_tokens: maxTokens
257
333
  }),
258
- signal,
334
+ signal: this.combinedSignal(signal),
259
335
  });
260
336
  if (!response.ok) {
261
337
  throw new Error(`Anthropic API error: ${response.status}`);
@@ -275,7 +351,7 @@ export class PiAIModel {
275
351
  temperature,
276
352
  stream: false
277
353
  }),
278
- signal,
354
+ signal: this.combinedSignal(signal),
279
355
  });
280
356
  if (!response.ok) {
281
357
  throw new Error(`Ollama API error: ${response.status}`);
@@ -302,7 +378,7 @@ export class PiAIModel {
302
378
  temperature,
303
379
  max_tokens: maxTokens
304
380
  }),
305
- signal,
381
+ signal: this.combinedSignal(signal),
306
382
  });
307
383
  if (!response.ok) {
308
384
  throw new Error(`OpenRouter API error: ${response.status}`);
@@ -335,7 +411,7 @@ export class PiAIModel {
335
411
  maxOutputTokens: maxTokens
336
412
  }
337
413
  }),
338
- signal,
414
+ signal: this.combinedSignal(signal),
339
415
  });
340
416
  if (!response.ok) {
341
417
  throw new Error(`Gemini API error: ${response.status}`);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * local-inbox-bus.ts — 进程内 Agent 通信总线 (2026-06-19)
3
+ *
4
+ * 解决 bolloon "和另一个智能体发消息" 的本地场景:
5
+ * - 同进程内多个 PiAgentSession 实例 (例如 nodeA / nodeB 测试) 互发消息
6
+ * - 走 in-process EventEmitter, 不依赖 P2P 网络/Hyperswarm
7
+ * - 远端通信仍走 p2pNetwork.sendMessage + onMessage
8
+ *
9
+ * 用法:
10
+ * const bus = LocalInboxBus.getInstance();
11
+ * bus.subscribe('nodeA', (msg) => console.log('nodeA got:', msg));
12
+ * bus.deliver('nodeB', { from: 'nodeA', type: 'agent-message', payload: 'hi' });
13
+ *
14
+ * 角色标识: 默认从 process.env.BOLLOON_ROLE 读, 也可显式传
15
+ */
16
+ import { EventEmitter } from 'events';
17
+ class LocalInboxBus {
18
+ static _instance = null;
19
+ emitter;
20
+ /** 记录已注册的角色, deliver 时找不到会返回 false (避免 silent drop) */
21
+ registeredRoles = new Set();
22
+ constructor() {
23
+ this.emitter = new EventEmitter();
24
+ // 防止 listener 太多导致 memory leak
25
+ this.emitter.setMaxListeners(1000);
26
+ }
27
+ static getInstance() {
28
+ if (!LocalInboxBus._instance) {
29
+ LocalInboxBus._instance = new LocalInboxBus();
30
+ }
31
+ return LocalInboxBus._instance;
32
+ }
33
+ /**
34
+ * 订阅角色 inbox — 投递到该角色的消息会回调 fn
35
+ * 返回 unsubscribe 函数
36
+ */
37
+ subscribe(role, fn) {
38
+ this.registeredRoles.add(role);
39
+ this.emitter.on(`role:${role}`, fn);
40
+ return () => {
41
+ this.emitter.off(`role:${role}`, fn);
42
+ // 如果没人订阅, 移除 role (避免泄漏)
43
+ if (this.emitter.listenerCount(`role:${role}`) === 0) {
44
+ this.registeredRoles.delete(role);
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * 投递消息到指定角色
50
+ * 返回 true=已投递, false=角色未注册
51
+ */
52
+ deliver(role, msg) {
53
+ if (!this.registeredRoles.has(role)) {
54
+ return false;
55
+ }
56
+ this.emitter.emit(`role:${role}`, msg);
57
+ return true;
58
+ }
59
+ /**
60
+ * 列出当前进程所有已注册的角色
61
+ */
62
+ listRoles() {
63
+ return Array.from(this.registeredRoles);
64
+ }
65
+ /**
66
+ * 检查某角色是否已订阅
67
+ */
68
+ hasRole(role) {
69
+ return this.registeredRoles.has(role);
70
+ }
71
+ }
72
+ export { LocalInboxBus };
73
+ export default LocalInboxBus;
@@ -18,6 +18,8 @@ import { EventEmitter } from 'events';
18
18
  // @ts-ignore — b4a 没官方 .d.ts
19
19
  import b4a from 'b4a';
20
20
  import { loadOrCreateKeyPair, writebackPublicKey } from './p2p-secret.js';
21
+ // 静态 import input-scanner (ESM, 替代 require 避免 "require is not defined" 报错)
22
+ import * as inputScanner from '../security/input-scanner.js';
21
23
  export class P2PDirect extends EventEmitter {
22
24
  swarm = null;
23
25
  name;
@@ -67,10 +69,7 @@ export class P2PDirect extends EventEmitter {
67
69
  // P-Action 3: untrusted-input scanner (默认 silence-on-fail + log-only)
68
70
  // 包装层 跨机器 @-mention 代发 (已 WORKING) 不能 block-on-suspicion
69
71
  try {
70
- // 动态 import 避免循环依赖 + 启动时加载
71
- // (lazy require to keep cold-start fast)
72
- // eslint-disable-next-line @typescript-eslint/no-var-requires
73
- const { scanInput, writeScanAudit, shouldHardBlock } = require('../security/input-scanner.js');
72
+ const { scanInput, writeScanAudit, shouldHardBlock } = inputScanner;
74
73
  const result = scanInput(buf, { source: 'p2p' });
75
74
  if (shouldHardBlock(result)) {
76
75
  console.warn(`[P2PDirect:${this.name}] 阻断恶意消息 from ${remotePubKeyHex.substring(0, 12)}... (verdict=${result.verdict}, threats=${result.threats.length})`);
@@ -21,11 +21,15 @@ import * as path from 'path';
21
21
  import { loadAllJudgments } from './human-value-store.js';
22
22
  // 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
23
23
  // 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
24
+ // 跨平台: 优先 process.env.HOME (测试可控), 然后 os.homedir() (Windows 上读 USERPROFILE).
25
+ // 用 path.join 而不是字符串拼接, 保证 Windows 上用 \ 分隔.
24
26
  function getUsageLogPath() {
25
- return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/usage.jsonl';
27
+ const home = process.env.HOME || os.homedir() || '/tmp';
28
+ return path.join(home, '.bolloon', 'human-values', 'usage.jsonl');
26
29
  }
27
30
  function getEvolutionLogPath() {
28
- return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
31
+ const home = process.env.HOME || os.homedir() || '/tmp';
32
+ return path.join(home, '.bolloon', 'human-values', 'evolution.jsonl');
29
33
  }
30
34
  /**
31
35
  * P-Action 1: next-action hint (4 仓库都强调的错误信息 = 修复指令 思想)
@@ -22,9 +22,9 @@ import * as os from 'os';
22
22
  import * as path from 'path';
23
23
  import { getModel } from '../llm/pi-ai.js';
24
24
  import { loadAllJudgments } from './human-value-store.js';
25
- const EVOLUTION_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
26
- const COUNTERFACTUAL_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/counterfactual-audit.jsonl';
27
- const USAGE_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/usage.jsonl';
25
+ const EVOLUTION_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'evolution.jsonl');
26
+ const COUNTERFACTUAL_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'counterfactual-audit.jsonl');
27
+ const USAGE_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'usage.jsonl');
28
28
  async function readUsageLog() {
29
29
  try {
30
30
  const content = await fs.readFile(USAGE_LOG, 'utf-8');
@@ -266,7 +266,7 @@ export async function extractFromFragment(fragmentPath) {
266
266
  type: 'rule',
267
267
  content: ruleContent,
268
268
  source: 'human',
269
- confidence: 0.8,
269
+ confidence: 0.9,
270
270
  context,
271
271
  createdAt: new Date().toISOString(),
272
272
  updatedAt: new Date().toISOString(),
@@ -21,6 +21,17 @@ const TOOL_WHITELIST = new Set([
21
21
  'read', 'write', 'edit_file', 'list_files',
22
22
  'list_sessions', 'get_session_state', 'list_messages',
23
23
  'send_to_channel', 'create_channel',
24
+ // M2.1 (2026-06-17): 新增的 10 个 agent 工具 (跟 pi-sdk registerTools 同步)
25
+ 'write_file', 'git_diff', 'git_commit', 'git_push', 'git_branch',
26
+ 'create_task', 'update_task', 'get_task', 'list_tasks',
27
+ 'use_skill', 'self_improve',
28
+ // 2026-06-19: M4 新增 (跟 pi-sdk.ts registerTools 同步)
29
+ 'read_file', 'delete_file', 'mkdir', 'move_file',
30
+ 'grep_files', 'glob_files',
31
+ 'git_log', 'git_show', 'git_stash',
32
+ 'vitest_run', 'tsc_check',
33
+ // 2026-06-19: Agent Mesh 通信工具 (跟 pi-sdk.ts registerTools 同步)
34
+ 'check_inbox', 'send_to_peer', 'p2p_broadcast', 'send_to_local_agent', 'list_local_agents', 'agent_call',
24
35
  // MCP 注册的工具
25
36
  'mcp_tool',
26
37
  ]);
@@ -769,6 +769,50 @@ async function handleV3P2PMessage(parsed, conn, comm) {
769
769
  }
770
770
  return;
771
771
  }
772
+ // v3 新增: --collab CLI 派任务过来, 对方 web 收到后跑 LLM 干活, 回结果
773
+ if (op === 'agent.collab.run') {
774
+ const { requestId, task, fromPublicKey, fromRole, timeoutMs } = parsed.payload || {};
775
+ if (!requestId || !task) {
776
+ console.warn(`[v3-collab] agent.collab.run 缺少 requestId/task`);
777
+ return;
778
+ }
779
+ const senderKey = fromPublicKey || peerKey;
780
+ console.log(`[v3-collab] 收到 ${senderKey.substring(0, 12)}... (${fromRole}) 的协作任务: "${task.substring(0, 60)}..."`);
781
+ const startTs = Date.now();
782
+ try {
783
+ // 调 LLM: 走 getAgentForChannel 拿 agent, 用 prompt() 拿 string 结果
784
+ const visitorHint = `[系统上下文] 协作任务来源: 远端 peer (P2P, publicKey=${senderKey.substring(0, 12)}..., role=${fromRole || 'unknown'}). 这是通过 P2P 派过来的协作任务, 回答后会自动回到对方那里. 简洁完成, 不需要反问.\n\n`;
785
+ const fullPrompt = `【本轮协作任务】\n${task}\n【任务结束】\n\n${visitorHint}`;
786
+ // 临时用 channelId = "__collab__" 拿个一次性 agent
787
+ const collabChannelId = `__collab_${senderKey.substring(0, 8)}__`;
788
+ const agent = await getAgentForChannel(collabChannelId, '', `collab-${senderKey.substring(0, 8)}`, undefined);
789
+ const resultText = await agent.prompt(fullPrompt);
790
+ const reply = JSON.stringify({
791
+ v: 3,
792
+ op: 'agent.collab.reply',
793
+ payload: {
794
+ requestId,
795
+ fromPublicKey: v3P2PRef?.getPublicKey() || '',
796
+ result: resultText || '(empty)',
797
+ durationMs: Date.now() - startTs,
798
+ },
799
+ });
800
+ await comm.sendToConnection(conn.id, reply);
801
+ console.log(`[v3-collab] 已回 reply (${Date.now() - startTs}ms, ${(resultText || '').length} chars)`);
802
+ }
803
+ catch (e) {
804
+ console.error(`[v3-collab] 处理失败:`, e.message);
805
+ try {
806
+ const errReply = JSON.stringify({
807
+ v: 3, op: 'agent.collab.reply',
808
+ payload: { requestId, fromPublicKey: v3P2PRef?.getPublicKey() || '', error: e.message, result: '' }
809
+ });
810
+ await comm.sendToConnection(conn.id, errReply);
811
+ }
812
+ catch { }
813
+ }
814
+ return;
815
+ }
772
816
  // v3 新增: 收到远端发来的 @-mention 跨渠道消息, 存到本地 target channel
773
817
  if (op === 'agent.cross.post') {
774
818
  const { targetChannelId, targetChannelName, originChannelId, originChannelName, text, fromPublicKey } = parsed.payload || {};
@@ -936,7 +980,14 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
936
980
  const session = await createAgentSession({
937
981
  cwd: process.cwd(),
938
982
  peerId: `channel-${channelId}:${currentSessionId}`,
939
- identityDoc
983
+ identityDoc,
984
+ // M2.3 (2026-06-17): 构造时从 session JSON 回灌历史 — 服务重启后 LLM 仍记得前面对话
985
+ // key 跟 server.ts:240 写入路径保持一致: ~/.bolloon/sessions/cache/<key>.json
986
+ loadSessionKey: sessionKey,
987
+ // M3.1 (2026-06-17): 默认启用 WorkflowPivotLoop — web 路径以前死代码, 现在用
988
+ // pivot loop 自带 quality scoring + task complexity analysis + 30 iters cap
989
+ // (老 ReAct 10000 iter cap 容易跑飞)
990
+ usePivotLoop: true,
940
991
  }, true); // forceNew: true 强制创建新实例
941
992
  channelSessions.set(sessionKey, session);
942
993
  if (channelDid) {
@@ -1665,13 +1716,19 @@ export async function createWebServer(port = 3000, options = {}) {
1665
1716
  console.log(`[chat] aborted channel=${channelId}`);
1666
1717
  }
1667
1718
  else {
1668
- throw err;
1719
+ // M1.2 (2026-06-17): 不再 rethrow — 把错误塞到 fullResponse 让 broadcast 出来, 后续 session 仍保存
1720
+ // 旧行为: 抛到外层 catch, 触发 500 + 用户消息丢失 (session 不保存)
1721
+ console.error(`[chat] LLM 调用失败 channel=${channelId}:`, err);
1722
+ fullResponse = `[错误: LLM 调用失败] ${String(err?.message || err).slice(0, 300)}`;
1723
+ broadcast({ type: 'error', content: fullResponse }, channelId);
1669
1724
  }
1670
1725
  }
1671
1726
  // abort 模式: 给 partial 拼后缀
1672
1727
  if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
1673
1728
  fullResponse = fullResponse + '\n\n_[生成已中断]_';
1674
1729
  }
1730
+ // 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
1731
+ runState.lastFinalReply = fullResponse;
1675
1732
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
1676
1733
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
1677
1734
  broadcast({ type: 'ai', content: fullResponse }, channelId);
@@ -5152,16 +5209,23 @@ export async function createWebServer(port = 3000, options = {}) {
5152
5209
  const startPort = port;
5153
5210
  let currentPort = startPort;
5154
5211
  let attempt = 0;
5212
+ // 2026-06-24: 默认 loopback bind,避免在 LAN 上暴露 (CORS 已经 * 了)。
5213
+ // Electron 包装里强制 127.0.0.1; CLI 用户想 LAN 访问可显式 BOLLOON_HOST=0.0.0.0
5214
+ const bindHost = options.host ?? process.env.BOLLOON_HOST ?? '127.0.0.1';
5155
5215
  // 局部可变 server 引用 — listen 失败后必须重新 createServer 再 listen
5156
5216
  let currentServer = server;
5157
5217
  const tryListen = () => {
5158
5218
  currentServer.removeAllListeners('error');
5159
5219
  currentServer.once('error', onError);
5160
- currentServer.listen(currentPort, () => {
5220
+ currentServer.listen(currentPort, bindHost, () => {
5161
5221
  if (currentPort !== startPort) {
5162
5222
  console.warn(`⚠ 端口 ${startPort} 被占用,已自动切换到 ${currentPort}`);
5163
5223
  }
5164
- console.log(`Web 服务器启动完成: http://localhost:${currentPort}`);
5224
+ // 2026-06-24: BOLLOON_PORT=NNNN 是 Electron 主进程解析的契约行 (parseable),
5225
+ // 旧 marker ('服务器已监听') 也保留给日志/调试看。
5226
+ console.log(`BOLLOON_PORT=${currentPort}`);
5227
+ console.log(`BOLLOON_HOST=${bindHost}`);
5228
+ console.log(`Web 服务器启动完成: http://${bindHost}:${currentPort}`);
5165
5229
  console.log('服务器已监听');
5166
5230
  // 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
5167
5231
  void installChatBusHook();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.41",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -17,7 +17,7 @@
17
17
  "build:all": "npm run build:main && npm run build:web && npm run build:electron && npm run build:cli",
18
18
  "start": "node dist/index.js",
19
19
  "dev": "tsx -r dotenv/config src/index.ts",
20
- "dev:web": "npm run build:web && npm run dev",
20
+ "dev:web": "npm run build:web && tsx -r dotenv/config src/index.ts --web",
21
21
  "dev:electron": "npm run build:electron && npm run electron:start",
22
22
  "electron:start": "NODE_ENV=development electron dist/electron.js",
23
23
  "electron:dev": "concurrently \"npm run dev:web\" \"npm run electron:start\"",
@@ -87,6 +87,11 @@
87
87
  "bin/**/*",
88
88
  "package.json"
89
89
  ],
90
+ "npmRebuild": false,
91
+ "buildDependenciesFromSource": false,
92
+ "extraResources": [
93
+ { "from": "build", "to": "build", "filter": ["**/*"] }
94
+ ],
90
95
  "extraMetadata": {
91
96
  "main": "dist/electron.js",
92
97
  "type": "commonjs"
@@ -121,7 +126,7 @@
121
126
  "createDesktopShortcut": true,
122
127
  "createStartMenuShortcut": true
123
128
  },
124
- "asar": false,
129
+ "asar": true,
125
130
  "asarUnpack": [
126
131
  "node_modules/@diap/**/*",
127
132
  "node_modules/@rayhanadev/**/*",
@@ -0,0 +1,69 @@
1
+ #!/bin/sh
2
+ # 2026-06-17: lefthook helper — agent auto-evolve branch detection
3
+ # 写在独立文件里避免 lefthook 嵌套引号 + Windows bash 解析问题
4
+ # 用法: lefthook.yml 里 run: sh scripts/lefthook-helper.sh <command>
5
+ # <command> = commit | vitest-full | build-check | tag-baseline
6
+
7
+ CMD="$1"
8
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo none)
9
+ AUTO_EVOLVE_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | grep -c ^auto-evolve: || echo 0)
10
+
11
+ is_master_main() {
12
+ [ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ]
13
+ }
14
+
15
+ is_auto_evolve() {
16
+ if [ "$BOLLOON_AUTO_EVOLVE" = "1" ]; then return 0; fi
17
+ if [ "$AUTO_EVOLVE_MSG" -gt 0 ] 2>/dev/null; then return 0; fi
18
+ return 1
19
+ }
20
+
21
+ case "$CMD" in
22
+ commit-vitest-bail)
23
+ if is_auto_evolve; then
24
+ echo "[skip] vitest-bail (auto-evolve mode, branch=$BRANCH)"
25
+ exit 0
26
+ fi
27
+ npx vitest run --bail=1 --reporter=dot
28
+ ;;
29
+ commit-tsc-check)
30
+ if is_auto_evolve; then
31
+ echo "[skip] tsc-check (auto-evolve mode, branch=$BRANCH)"
32
+ exit 0
33
+ fi
34
+ npx tsc --noEmit
35
+ ;;
36
+ push-vitest-full)
37
+ if is_master_main; then
38
+ npx vitest run --reporter=dot
39
+ else
40
+ echo "[skip] vitest-full (branch=$BRANCH, not master/main)"
41
+ exit 0
42
+ fi
43
+ ;;
44
+ push-build-check)
45
+ if is_master_main; then
46
+ npm run build:main
47
+ else
48
+ echo "[skip] build-check (branch=$BRANCH, not master/main)"
49
+ exit 0
50
+ fi
51
+ ;;
52
+ push-tag-baseline)
53
+ if is_master_main; then
54
+ if git tag -l 'auto-evolve-baseline-*' | grep -q .; then
55
+ exit 0
56
+ else
57
+ echo "[ERROR] push to master/main requires auto-evolve-baseline-* tag. Run: bash scripts/auto-evolve-snapshot.sh"
58
+ exit 1
59
+ fi
60
+ else
61
+ echo "[skip] tag-baseline (branch=$BRANCH, not master/main)"
62
+ exit 0
63
+ fi
64
+ ;;
65
+ *)
66
+ echo "Unknown command: $CMD"
67
+ exit 1
68
+ ;;
69
+ esac