@bolloon/bolloon-agent 0.1.12 → 0.1.14

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 (77) hide show
  1. package/dist/agents/p2p-chat-tools.js +321 -0
  2. package/dist/agents/p2p-document-tools.js +121 -1
  3. package/dist/agents/pi-sdk.js +185 -0
  4. package/dist/agents/shell-guard.js +354 -0
  5. package/dist/agents/shell-tool.js +83 -0
  6. package/dist/agents/skill-loader.js +174 -0
  7. package/dist/agents/workflow-pivot-loop.js +4 -4
  8. package/dist/bollharness-integration/context-chain-router.js +3 -3
  9. package/dist/bollharness-integration/context-router.js +1 -1
  10. package/dist/cli-entry.js +1 -1
  11. package/dist/documents/reader.js +5 -0
  12. package/dist/documents/store.js +1 -1
  13. package/dist/heartbeat/Watchdog.js +7 -5
  14. package/dist/heartbeat/index.js +1 -0
  15. package/dist/heartbeat/self-improve-bus.js +85 -0
  16. package/dist/llm/pi-ai.js +6 -5
  17. package/dist/network/iroh-discovery.js +2 -1
  18. package/dist/network/iroh-transport.js +15 -2
  19. package/dist/network/p2p.js +9 -8
  20. package/dist/network/storage/adapters/json-adapter.js +16 -1
  21. package/dist/network/storage/index.js +2 -1
  22. package/dist/pi-ecosystem-judgment/index.js +42 -115
  23. package/dist/social/channels/channel-heartbeat-agent.js +1 -1
  24. package/dist/utils/auto-update.js +44 -12
  25. package/dist/web/client.js +839 -103
  26. package/dist/web/index.html +100 -8
  27. package/dist/web/server.js +568 -98
  28. package/dist/web/style.css +506 -9
  29. package/package.json +2 -2
  30. package/scripts/build-cli.js +11 -1
  31. package/scripts/build-web.ts +1 -1
  32. package/src/agents/p2p-chat-tools.ts +383 -0
  33. package/src/agents/p2p-document-tools.ts +151 -1
  34. package/src/agents/pi-sdk.ts +196 -0
  35. package/src/agents/shell-guard.ts +417 -0
  36. package/src/agents/shell-tool.ts +103 -0
  37. package/src/agents/skill-loader.ts +202 -0
  38. package/src/agents/workflow-pivot-loop.ts +13 -12
  39. package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
  40. package/src/bollharness-integration/context-chain-router.ts +3 -3
  41. package/src/bollharness-integration/context-router.ts +1 -1
  42. package/src/documents/reader.ts +5 -0
  43. package/src/documents/store.ts +1 -1
  44. package/src/heartbeat/Watchdog.ts +7 -5
  45. package/src/heartbeat/index.ts +1 -0
  46. package/src/heartbeat/self-improve-bus.ts +110 -0
  47. package/src/llm/pi-ai.ts +6 -5
  48. package/src/network/iroh-discovery.ts +2 -1
  49. package/src/network/iroh-transport.ts +15 -2
  50. package/src/network/p2p.ts +9 -8
  51. package/src/network/storage/adapters/json-adapter.ts +17 -2
  52. package/src/network/storage/index.ts +19 -3
  53. package/src/social/channels/channel-heartbeat-agent.ts +1 -1
  54. package/src/types.d.ts +12 -0
  55. package/src/utils/auto-update.ts +45 -14
  56. package/src/web/client.js +839 -103
  57. package/src/web/index.html +88 -8
  58. package/src/web/server.ts +577 -102
  59. package/src/web/style.css +506 -9
  60. package/tsconfig.electron.json +1 -1
  61. package/tsconfig.json +1 -1
  62. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  63. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  64. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  65. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  66. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  67. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  68. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  69. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  70. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  71. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  72. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  73. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  74. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  75. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  76. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  77. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
@@ -0,0 +1,174 @@
1
+ /**
2
+ * skill-loader.ts — 双 frontmatter 兼容的 SKILL.md 加载器
3
+ *
4
+ * 兼容两套 SKILL.md frontmatter:
5
+ * A. Anthropic Agent Skills 标准 (2025-12): name / description / license / compatibility / keywords
6
+ * B. bollharness 现有 frontmatter: name / description / status / tier / triggers / outputs / truth_policy
7
+ *
8
+ * 字段映射规则(统一到内部 SkillMeta):
9
+ * description ← 直接取
10
+ * license ← 取 A,没有则空
11
+ * status ← 取 B,没有则默认 'active'
12
+ * tier ← 取 B("tier" 是 bollharness 概念)
13
+ * triggers / keywords ← 合并 A.keywords 和 B.triggers 数组
14
+ * body ← 去掉 frontmatter 后的 Markdown 正文
15
+ *
16
+ * Skill 的 execute() 把 body 作为 Markdown 文档注入到 LLM context。
17
+ * 这是 Skills 协议的核心 — "告诉 agent 怎么做",与 MCP "能调什么"互补。
18
+ */
19
+ import * as fs from 'fs/promises';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ /** YAML frontmatter 最小解析器 — 避免引入额外依赖, 支持双格式 */
23
+ function parseFrontmatter(raw) {
24
+ // frontmatter 必须以 --- 开头, 紧跟换行, 再以 --- 闭合
25
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
26
+ if (!match) {
27
+ return { frontmatter: {}, body: raw };
28
+ }
29
+ const [, yamlBlock, body] = match;
30
+ const frontmatter = {};
31
+ const lines = yamlBlock.split(/\r?\n/);
32
+ let currentKey = null;
33
+ let currentArray = null;
34
+ for (const rawLine of lines) {
35
+ const line = rawLine.replace(/\s+$/, '');
36
+ if (!line.trim())
37
+ continue;
38
+ // 数组项: " - value"
39
+ const arrItem = line.match(/^\s+-\s+(.*)$/);
40
+ if (arrItem && currentKey && currentArray) {
41
+ currentArray.push(stripQuotes(arrItem[1]));
42
+ continue;
43
+ }
44
+ // 键值对: "key: value" 或 "key:"
45
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
46
+ if (kv) {
47
+ const [, key, value] = kv;
48
+ if (value === '') {
49
+ // 可能是数组开始 (下一行 " - xxx")
50
+ currentKey = key;
51
+ currentArray = [];
52
+ frontmatter[key] = currentArray;
53
+ }
54
+ else {
55
+ currentKey = key;
56
+ currentArray = null;
57
+ frontmatter[key] = stripQuotes(value);
58
+ }
59
+ }
60
+ }
61
+ return { frontmatter, body: body.replace(/^\r?\n/, '') };
62
+ }
63
+ function stripQuotes(s) {
64
+ const t = s.trim();
65
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
66
+ return t.slice(1, -1);
67
+ }
68
+ return t;
69
+ }
70
+ /** 把 frontmatter 统一到 SkillMeta */
71
+ function normalize(name, sourcePath, raw) {
72
+ const { frontmatter, body } = parseFrontmatter(raw);
73
+ // name 至少要有一个来源: frontmatter.name 或目录名
74
+ const fmName = typeof frontmatter.name === 'string' ? frontmatter.name : name;
75
+ const finalName = fmName || name;
76
+ if (!finalName)
77
+ return null;
78
+ const description = typeof frontmatter.description === 'string'
79
+ ? frontmatter.description
80
+ : '';
81
+ const statusRaw = typeof frontmatter.status === 'string' ? frontmatter.status.toLowerCase() : 'active';
82
+ const status = statusRaw === 'archived' || statusRaw === 'draft' ? statusRaw : 'active';
83
+ const tier = typeof frontmatter.tier === 'string' ? frontmatter.tier : 'utility';
84
+ // 合并两套触发字段
85
+ const triggers = [];
86
+ if (Array.isArray(frontmatter.triggers)) {
87
+ for (const t of frontmatter.triggers)
88
+ if (typeof t === 'string')
89
+ triggers.push(t);
90
+ }
91
+ if (Array.isArray(frontmatter.keywords)) {
92
+ for (const k of frontmatter.keywords)
93
+ if (typeof k === 'string')
94
+ triggers.push(k);
95
+ }
96
+ return { name: finalName, sourcePath, body, frontmatter, description, status, tier, triggers };
97
+ }
98
+ /** 解析单个 SKILL.md 文件 */
99
+ export async function parseSkillFile(filePath) {
100
+ let raw;
101
+ try {
102
+ raw = await fs.readFile(filePath, 'utf-8');
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ // 从路径推目录名作为 name 兜底
108
+ const dirName = path.basename(path.dirname(filePath));
109
+ return normalize(dirName, filePath, raw);
110
+ }
111
+ /** 扫描一个目录, 找所有 {name}/SKILL.md (一层嵌套结构) */
112
+ export async function loadSkillsDir(dir) {
113
+ const out = [];
114
+ let entries;
115
+ try {
116
+ entries = await fs.readdir(dir, { withFileTypes: true });
117
+ }
118
+ catch {
119
+ return out;
120
+ }
121
+ for (const entry of entries) {
122
+ if (!entry.isDirectory())
123
+ continue;
124
+ if (entry.name.startsWith('.'))
125
+ continue;
126
+ const skillFile = path.join(dir, entry.name, 'SKILL.md');
127
+ const meta = await parseSkillFile(skillFile);
128
+ if (meta)
129
+ out.push(meta);
130
+ }
131
+ return out;
132
+ }
133
+ /** 默认 skill 路径优先级 (后者覆盖前者同名 skill) */
134
+ export function defaultSkillPaths(home = os.homedir(), cwd = process.cwd()) {
135
+ return [
136
+ path.join(home, '.bolloon', 'skills'), // 全局用户级
137
+ path.join(cwd, '.bolloon', 'skills'), // 项目级
138
+ path.join(home, '.boll', 'skills'), // 全局 (兼容 bollharness 旧用户)
139
+ ];
140
+ }
141
+ /** 把 SkillMeta 包成 @bolloon/constraint-runtime 期望的 Skill 对象 */
142
+ export function skillFromMeta(meta) {
143
+ return {
144
+ name: meta.name,
145
+ description: meta.description || meta.tier,
146
+ execute: async (_params) => {
147
+ // Skills 协议: 把 body 当 Markdown 文档返回, 由调用方注入 LLM context
148
+ // 调用方 (use_skill tool) 拿到后会把 body 放到 tool result,
149
+ // LLM 下一轮对话看到这份指南, 按它执行
150
+ const header = `## Skill: ${meta.name}\n\n${meta.description ? `> ${meta.description}\n\n` : ''}`;
151
+ const triggersBlock = meta.triggers.length
152
+ ? `**触发条件**: ${meta.triggers.join('; ')}\n\n`
153
+ : '';
154
+ return `${header}${triggersBlock}${meta.body}`;
155
+ },
156
+ };
157
+ }
158
+ /** 加载多个目录, 同名 skill 后者覆盖前者 */
159
+ export async function loadSkillsFromPaths(paths) {
160
+ const seen = new Map();
161
+ for (const p of paths) {
162
+ const metas = await loadSkillsDir(p);
163
+ for (const m of metas) {
164
+ if (m.status === 'archived')
165
+ continue; // 归档的跳过
166
+ seen.set(m.name, skillFromMeta(m));
167
+ }
168
+ }
169
+ return Array.from(seen.values());
170
+ }
171
+ /** 列出已加载的 skills (调试/UI 用) */
172
+ export function describeSkill(s) {
173
+ return `${s.name}: ${s.description}`;
174
+ }
@@ -220,7 +220,7 @@ export class WorkflowPivotLoop {
220
220
  tool: toolCall.name
221
221
  });
222
222
  try {
223
- const result = await tool.execute(toolCall.args);
223
+ const result = await tool.execute(toolCall.args ?? {});
224
224
  this.emit({
225
225
  type: result.success ? 'status' : 'error',
226
226
  content: result.success
@@ -313,7 +313,7 @@ export class WorkflowPivotLoop {
313
313
  const argsStr = match[2];
314
314
  const args = this.parseArgs(argsStr);
315
315
  if (this.tools.has(name)) {
316
- pending.push({ name, args });
316
+ pending.push({ name, args, description: '', parameters: {} });
317
317
  }
318
318
  }
319
319
  // Pattern 2: tool_name(args) format
@@ -327,7 +327,7 @@ export class WorkflowPivotLoop {
327
327
  if (!this.tools.has(name))
328
328
  continue;
329
329
  const args = this.parseArgs(argsStr);
330
- pending.push({ name, args });
330
+ pending.push({ name, args, description: '', parameters: {} });
331
331
  }
332
332
  // Pattern 3: JSON format tool calls
333
333
  try {
@@ -337,7 +337,7 @@ export class WorkflowPivotLoop {
337
337
  if (Array.isArray(parsed.tool_calls)) {
338
338
  for (const tc of parsed.tool_calls) {
339
339
  if (this.tools.has(tc.name)) {
340
- pending.push({ name: tc.name, args: tc.args || {} });
340
+ pending.push({ name: tc.name, args: tc.args || {}, description: '', parameters: {} });
341
341
  }
342
342
  }
343
343
  }
@@ -4,18 +4,18 @@
4
4
  * Integrates with existing ContextRouter and Judgment systems.
5
5
  *
6
6
  * Architecture:
7
- * - Session end → extract summary by work_type → store in .boll/state/context-chains/
7
+ * - Session end → extract summary by work_type → store in .bolloon/state/context-chains/
8
8
  * - Session start (Gate 0/3) → lookup related chains → inject summaries
9
9
  * - Work type: code_change | review | design | question | planning | debugging
10
10
  *
11
11
  * Integration points:
12
12
  * - Uses existing context-router-judgment.ts pattern (extends, not replaces)
13
13
  * - Gate injection via gate-judgment-inject.ts
14
- * - Storage in .boll/state/context-chains/
14
+ * - Storage in .bolloon/state/context-chains/
15
15
  */
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
- export const CONTEXT_CHAINS_DIR = path.join('.boll', 'state', 'context-chains');
18
+ export const CONTEXT_CHAINS_DIR = path.join('.bolloon', 'state', 'context-chains');
19
19
  /**
20
20
  * Ensure context-chains directory exists
21
21
  */
@@ -105,7 +105,7 @@ export class ContextRouter {
105
105
  injectedTTL = 3600; // 1 hour
106
106
  constructor(fragmentsDir) {
107
107
  this.fragmentsDir = fragmentsDir || FRAGMENTS_DIR;
108
- this.injectedFile = path.join('.boll', 'guard', 'injected.json');
108
+ this.injectedFile = path.join('.bolloon', 'guard', 'injected.json');
109
109
  }
110
110
  /**
111
111
  * Get fragments for a file path
package/dist/cli-entry.js CHANGED
@@ -22,7 +22,7 @@ const YELLOW = '\x1b[33m';
22
22
  const GREEN = '\x1b[32m';
23
23
  const MAGENTA = '\x1b[35m';
24
24
  // 版本信息
25
- const VERSION = '0.1.11';
25
+ const VERSION = '0.1.12';
26
26
  function log(msg, color = RESET) {
27
27
  console.log(`${color}${msg}${RESET}`);
28
28
  }
@@ -11,6 +11,11 @@ export class DocumentReader {
11
11
  switch (ext) {
12
12
  case '.txt':
13
13
  case '.md':
14
+ case '.html':
15
+ case '.htm':
16
+ case '.yaml':
17
+ case '.yml':
18
+ case '.json':
14
19
  text = await fs.readFile(filePath, 'utf-8');
15
20
  break;
16
21
  case '.pdf':
@@ -139,10 +139,10 @@ export class DocumentStore {
139
139
  async readDocument(docId) {
140
140
  const docDir = path.join(this.baseDir, docId);
141
141
  const manifestPath = path.join(docDir, 'manifest.json');
142
- const filePath = path.join(docDir);
143
142
  try {
144
143
  const manifestData = await fs.readFile(manifestPath, 'utf-8');
145
144
  const manifest = JSON.parse(manifestData);
145
+ const filePath = path.join(docDir, manifest.fileName);
146
146
  const fileContent = await fs.readFile(filePath, 'utf-8');
147
147
  return {
148
148
  content: fileContent,
@@ -105,12 +105,14 @@ export class Watchdog {
105
105
  this.triggerRestart(1, `No activity for ${Math.round(silentTime / 1000)}s`);
106
106
  return;
107
107
  }
108
- // 检查内存使用
108
+ // 检查内存使用: 用绝对阈值, 不看 heapUsed/heapTotal 比例 (V8 内部比例不可靠, 经常 80-95% 误报)
109
109
  const usage = process.memoryUsage();
110
- const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
111
- if (heapUsedPercent > 90) {
112
- console.warn(`[Watchdog] Memory usage critical: ${heapUsedPercent.toFixed(1)}%`);
113
- this.triggerRestart(1, `Memory usage ${heapUsedPercent.toFixed(1)}%`);
110
+ const heapUsedMB = usage.heapUsed / 1024 / 1024;
111
+ const rssMB = usage.rss / 1024 / 1024;
112
+ // 1.2GB heap 1.5GB RSS 才是真危险
113
+ if (heapUsedMB > 1224 || rssMB > 1536) {
114
+ console.warn(`[Watchdog] Memory usage critical: heapUsed=${heapUsedMB.toFixed(0)}MB, rss=${rssMB.toFixed(0)}MB`);
115
+ this.triggerRestart(1, `Memory usage heap=${heapUsedMB.toFixed(0)}MB rss=${rssMB.toFixed(0)}MB`);
114
116
  }
115
117
  }
116
118
  /**
@@ -7,6 +7,7 @@ export * from './HealthMonitor.js';
7
7
  export * from './Watchdog.js';
8
8
  export * from './DaemonManager.js';
9
9
  export * from './StartupVerifier.js';
10
+ export * from './self-improve-bus.js';
10
11
  import { createHealthMonitor, getHealthMonitor } from './HealthMonitor.js';
11
12
  import { createWatchdog, getWatchdog } from './Watchdog.js';
12
13
  import { createDaemonManager, getDaemonManager } from './DaemonManager.js';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Self-Improve Event Bus
3
+ *
4
+ * 心跳事件 → 自改触发器 (解耦 watchdog)
5
+ *
6
+ * 设计原则:
7
+ * - Watchdog 负责保活 (重启进程), 不知道"自改"是什么
8
+ * - Self-Improve Bus 监听"信号事件" (CI 失败, 任务连续失败, 静默超时)
9
+ * - 信号达到阈值 + 通过冷却期 → 触发 runSelfImproveLoop
10
+ * - 触发时通过 SSE 广播给前端, 用户能在 UI 里看到
11
+ *
12
+ * 关键不变量:
13
+ * 1. 心跳**不**直接调自改 - 通过 emit() 异步触发
14
+ * 2. 触发频率受 SELF_IMPROVE_COOLDOWN_MS 限制
15
+ * 3. 同类事件 24 小时内只触发 1 次
16
+ * 4. 触发后不阻塞健康检查
17
+ */
18
+ import { SELF_IMPROVE_COOLDOWN_MS } from '../agents/shell-guard.js';
19
+ const EVENT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 小时内同类型只触发 1 次
20
+ const eventHistory = new Map();
21
+ let lastTriggerAt = null;
22
+ const listeners = new Set();
23
+ /**
24
+ * 订阅自改触发事件
25
+ */
26
+ export function onSelfImproveTrigger(fn) {
27
+ listeners.add(fn);
28
+ return () => { listeners.delete(fn); };
29
+ }
30
+ /**
31
+ * 心跳事件 → 自改总线
32
+ *
33
+ * @returns { triggered: boolean, reason?: string }
34
+ */
35
+ export function reportSelfImproveEvent(event) {
36
+ // 1. 24 小时同类事件冷却
37
+ const prev = eventHistory.get(event.kind);
38
+ if (prev && Date.now() - prev.at < EVENT_COOLDOWN_MS) {
39
+ return { triggered: false, reason: `同类事件 ${event.kind} 在 24h 内已记录过, 跳过` };
40
+ }
41
+ // 2. 累加计数
42
+ eventHistory.set(event.kind, {
43
+ at: Date.now(),
44
+ count: (prev?.count || 0) + 1
45
+ });
46
+ // 3. 自改循环冷却
47
+ if (lastTriggerAt && Date.now() - lastTriggerAt < SELF_IMPROVE_COOLDOWN_MS) {
48
+ const waitHrs = Math.ceil((SELF_IMPROVE_COOLDOWN_MS - (Date.now() - lastTriggerAt)) / 3600000);
49
+ return { triggered: false, reason: `自改冷却中, 还需要约 ${waitHrs} 小时` };
50
+ }
51
+ // 4. 触发
52
+ lastTriggerAt = Date.now();
53
+ const goal = `信号事件: ${event.kind} - ${event.details}`;
54
+ console.log(`[self-improve-bus] 🚀 触发自改循环: ${goal}`);
55
+ // 异步触发所有 listener, 不阻塞调用方
56
+ Promise.resolve().then(async () => {
57
+ for (const listener of listeners) {
58
+ try {
59
+ await listener(event, goal);
60
+ }
61
+ catch (err) {
62
+ console.error(`[self-improve-bus] listener 失败:`, err);
63
+ }
64
+ }
65
+ });
66
+ return { triggered: true };
67
+ }
68
+ /**
69
+ * 获取当前事件历史 (供调试 / UI 显示)
70
+ */
71
+ export function getEventHistory() {
72
+ return Array.from(eventHistory.entries()).map(([kind, rec]) => ({
73
+ kind,
74
+ at: new Date(rec.at).toISOString(),
75
+ count: rec.count
76
+ }));
77
+ }
78
+ /**
79
+ * 强制重置 (仅供调试)
80
+ */
81
+ export function resetSelfImproveBus() {
82
+ eventHistory.clear();
83
+ lastTriggerAt = null;
84
+ console.log('[self-improve-bus] 已重置');
85
+ }
package/dist/llm/pi-ai.js CHANGED
@@ -102,25 +102,26 @@ export class PiAIModel {
102
102
  if (this.config.baseUrl) {
103
103
  return this.config.baseUrl;
104
104
  }
105
+ // 允许通过 OPENAI_BASE_URL 等环境变量覆盖默认 base URL
105
106
  const baseUrls = {
106
- openai: 'https://api.openai.com/v1',
107
+ openai: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
107
108
  anthropic: 'https://api.anthropic.com/v1',
108
109
  ollama: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
109
- openrouter: 'https://openrouter.ai/api/v1',
110
+ openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
110
111
  gemini: 'https://generativelanguage.googleapis.com/v1beta',
111
- minimax: 'https://api.minimaxi.com/v1',
112
+ minimax: process.env.MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1',
112
113
  local: 'http://localhost:11434'
113
114
  };
114
115
  return baseUrls[this.provider];
115
116
  }
116
117
  mapModel() {
117
118
  const modelMap = {
118
- openai: this.config.model || 'gpt-4',
119
+ openai: this.config.model || process.env.OPENAI_MODEL || 'gpt-4',
119
120
  anthropic: this.config.model || 'claude-3-5-sonnet-20241022',
120
121
  ollama: this.config.model || 'llama3.2',
121
122
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
122
123
  gemini: this.config.model || 'gemini-2.0-flash',
123
- minimax: this.config.model || 'MiniMax-M2.7',
124
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
124
125
  local: this.config.model || 'llama3.2'
125
126
  };
126
127
  return modelMap[this.provider];
@@ -54,9 +54,10 @@ export class IrohDiscoveryService {
54
54
  }, this.config.refreshIntervalMs);
55
55
  }
56
56
  startDiscoveryLoop() {
57
+ const interval = this.config.discoveryIntervalMs ?? 30000;
57
58
  this.discoveryTimer = setInterval(async () => {
58
59
  await this.discoverPeers();
59
- }, this.discoveryIntervalMs);
60
+ }, interval);
60
61
  setTimeout(() => this.discoverPeers(), 2000);
61
62
  }
62
63
  async discoverPeers() {
@@ -100,6 +100,9 @@ export class IrohTransport {
100
100
  count += queue.length;
101
101
  return count;
102
102
  },
103
+ async getAllOfflineTargets() {
104
+ return Array.from(offlineQueues.keys());
105
+ },
103
106
  async savePendingResponse(req) {
104
107
  const id = crypto.randomUUID();
105
108
  const pending = { ...req, id };
@@ -123,8 +126,13 @@ export class IrohTransport {
123
126
  if (!this.messageStore)
124
127
  return;
125
128
  this.offlineDeliveryInterval = setInterval(async () => {
129
+ // 遍历所有有离线消息的目标节点(不仅是"已连接"的)
130
+ // 这样目标节点一旦在线(accept 连接)就能拿到离线消息
131
+ const allTargets = await this.messageStore.getAllOfflineTargets();
126
132
  const connectedPeers = this.getConnectedPeers();
127
- for (const peerId of connectedPeers) {
133
+ // 合并:已连接 + 有离线消息但未连接(也会去尝试 connect)
134
+ const targets = new Set([...connectedPeers, ...allTargets]);
135
+ for (const peerId of targets) {
128
136
  const offlineMsgs = await this.messageStore.getOfflineMessages(peerId);
129
137
  for (const msg of offlineMsgs) {
130
138
  if (msg.retryCount >= 10) {
@@ -140,6 +148,9 @@ export class IrohTransport {
140
148
  await this.messageStore.dequeueOfflineMessage(msg.id);
141
149
  console.log(`[IrohTransport] Delivered offline message to ${peerId.substring(0, 12)}...`);
142
150
  }
151
+ else {
152
+ await this.messageStore.incrementOfflineRetry(msg.id);
153
+ }
143
154
  }
144
155
  catch {
145
156
  await this.messageStore.incrementOfflineRetry(msg.id);
@@ -356,8 +367,10 @@ export class IrohTransport {
356
367
  await send.writeAll(Buffer.from(requestMsg));
357
368
  await send.finish();
358
369
  // 等待响应,带超时
370
+ // 注意: server sendResponse 后会关闭连接,导致 readToEnd 以 "connection lost" 错误 reject
371
+ // 这里我们把 readToEnd 的错误吞掉(视为流结束),只有超时才视为失败
359
372
  const response = await Promise.race([
360
- recv.readToEnd(64 * 1024),
373
+ recv.readToEnd(64 * 1024).catch(() => new Uint8Array(0)),
361
374
  new Promise((_, rejectTimeout) => setTimeout(() => rejectTimeout(new Error('timeout')), timeout)),
362
375
  ]);
363
376
  conn.close();
@@ -437,8 +437,8 @@ export class P2PNetwork {
437
437
  const colonIdx = messageStr.indexOf(':');
438
438
  const didMarker = 'DID:';
439
439
  let did;
440
- let type;
441
- let payload;
440
+ let type = 'message';
441
+ let payload = '';
442
442
  let requestId = undefined;
443
443
  if (messageStr.startsWith(didMarker)) {
444
444
  const didEndIdx = messageStr.indexOf('|');
@@ -651,7 +651,11 @@ export class P2PNetwork {
651
651
  * Register a handler for responses (used by the receiving side)
652
652
  */
653
653
  onResponse(type, handler) {
654
- this.messageHandlers.set(type, handler);
654
+ // Store as pendingResponseHandlers-shaped wrapper. Extra args (did, requestId) are not
655
+ // available in pendingResponseHandlers signature, so ignore them when invoked.
656
+ this.pendingResponseHandlers.set(type, (responseData, from) => {
657
+ handler(responseData, from, undefined, undefined);
658
+ });
655
659
  }
656
660
  /**
657
661
  * Send a response back to a peer
@@ -675,11 +679,8 @@ export class P2PNetwork {
675
679
  handleRequest(type, payload, requestId, fromPeerId, did) {
676
680
  const handler = this.messageHandlers.get(type);
677
681
  if (handler) {
678
- // Create a wrapper that sends the response
679
- const originalHandler = handler;
680
- handler = (msg, from, didParam) => {
681
- originalHandler(msg, from, didParam);
682
- };
682
+ // Forward raw payload; callers register with onMessage() and adapt as needed.
683
+ handler(new TextEncoder().encode(payload), fromPeerId, did);
683
684
  }
684
685
  // Check if there's a response handler registered
685
686
  const responseHandler = this.pendingResponseHandlers.get(type);
@@ -43,7 +43,7 @@ export class JsonMessageStore {
43
43
  const stored = { ...msg, id };
44
44
  const filePath = this.getMessageFilePath(new Date(msg.timestamp));
45
45
  await this.withLock(filePath, async () => {
46
- const messages = await this.readJsonFile(filePath) || [];
46
+ let messages = await this.readJsonFile(filePath) || [];
47
47
  messages.push(stored);
48
48
  // 如果文件过大,拆分
49
49
  if (messages.length > this.config.maxMessagesPerFile) {
@@ -189,6 +189,21 @@ export class JsonMessageStore {
189
189
  }
190
190
  return count;
191
191
  }
192
+ async getAllOfflineTargets() {
193
+ // 重新从磁盘加载最新状态(避免内存 vs 磁盘不一致)
194
+ const baseDir = path.join(this.config.baseDir, 'offline');
195
+ let files = [];
196
+ try {
197
+ files = await fs.readdir(baseDir);
198
+ }
199
+ catch {
200
+ return Array.from(this.offlineMessages.keys());
201
+ }
202
+ return files
203
+ .filter((f) => f.endsWith('.json'))
204
+ .map((f) => f.replace(/\.json$/, ''))
205
+ .filter((id) => id.length > 0);
206
+ }
192
207
  // ============================================================================
193
208
  // 待响应请求
194
209
  // ============================================================================
@@ -2,7 +2,8 @@
2
2
  * Storage Layer Entry Point
3
3
  * 导出消息存储工厂函数和类型
4
4
  */
5
- export { DEFAULT_STORAGE_CONFIG } from './types.js';
5
+ import { DEFAULT_STORAGE_CONFIG } from './types.js';
6
+ export { DEFAULT_STORAGE_CONFIG };
6
7
  import { JsonMessageStore } from './adapters/json-adapter.js';
7
8
  import * as path from 'path';
8
9
  // 默认存储配置