@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,103 @@
1
+ /**
2
+ * Shell 工具: 给 Bolloon agent 跑受限的 shell 命令
3
+ *
4
+ * 这个工具**只做两件事**:
5
+ * 1. 把命令交给硬护栏检查
6
+ * 2. 在沙箱 cwd 下用 child_process 执行
7
+ *
8
+ * AI 完全自主触发自改, 但 shell 工具本身**只接受白名单内命令**.
9
+ * 禁区列表在 shell-guard.ts, AI 改不了那个文件.
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import * as fs from 'fs';
14
+ import { checkCommand, checkWritePath, getSandboxCwd } from './shell-guard.js';
15
+
16
+ export interface ShellExecResult {
17
+ success: boolean;
18
+ output?: string;
19
+ error?: string;
20
+ exitCode?: number;
21
+ /** true 表示被护栏拒绝, AI 不应该重试 */
22
+ deniedByGuard?: boolean;
23
+ }
24
+
25
+ /**
26
+ * 在沙箱里跑一条命令
27
+ * @param cmd 可执行文件名, 必须命中白名单
28
+ * @param args 参数列表
29
+ * @param opts.timeoutMs 超时毫秒, 默认 30s
30
+ * @param opts.allowedWriteTargets 允许的写入路径, 命中禁区列表的路径会拒
31
+ */
32
+ export async function shellExec(
33
+ cmd: string,
34
+ args: string[] = [],
35
+ opts: { timeoutMs?: number; allowedWriteTargets?: string[] } = {}
36
+ ): Promise<ShellExecResult> {
37
+ // 1. 护栏检查
38
+ const cmdCheck = checkCommand(cmd, args);
39
+ if (!cmdCheck.allowed) {
40
+ return {
41
+ success: false,
42
+ error: `[shell-guard] ${cmdCheck.reason}`,
43
+ deniedByGuard: true
44
+ };
45
+ }
46
+
47
+ // 2. 写入目标检查
48
+ if (opts.allowedWriteTargets) {
49
+ for (const target of opts.allowedWriteTargets) {
50
+ const pathCheck = checkWritePath(target);
51
+ if (!pathCheck.allowed) {
52
+ return {
53
+ success: false,
54
+ error: `[shell-guard] ${pathCheck.reason}`,
55
+ deniedByGuard: true
56
+ };
57
+ }
58
+ }
59
+ }
60
+
61
+ // 3. 确保沙箱存在
62
+ const sandboxCwd = getSandboxCwd();
63
+ try {
64
+ fs.mkdirSync(sandboxCwd, { recursive: true });
65
+ } catch {
66
+ // 已经存在则忽略
67
+ }
68
+
69
+ // 4. 跑命令
70
+ return new Promise((resolve) => {
71
+ const proc = spawn(cmd, args, {
72
+ cwd: sandboxCwd,
73
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // 禁止 git 弹交互
74
+ shell: false, // **关键**: 禁用 shell, 防止元字符注入
75
+ windowsHide: true
76
+ });
77
+
78
+ let stdout = '';
79
+ let stderr = '';
80
+ const timeout = setTimeout(() => {
81
+ proc.kill('SIGKILL');
82
+ resolve({ success: false, error: `命令超时 (>${opts.timeoutMs || 30000}ms)`, exitCode: -1 });
83
+ }, opts.timeoutMs || 30000);
84
+
85
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
86
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
87
+
88
+ proc.on('error', (err) => {
89
+ clearTimeout(timeout);
90
+ resolve({ success: false, error: `启动失败: ${err.message}` });
91
+ });
92
+
93
+ proc.on('close', (code) => {
94
+ clearTimeout(timeout);
95
+ const output = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
96
+ if (code === 0) {
97
+ resolve({ success: true, output, exitCode: 0 });
98
+ } else {
99
+ resolve({ success: false, output, error: `exit code ${code}`, exitCode: code ?? -1 });
100
+ }
101
+ });
102
+ });
103
+ }
@@ -0,0 +1,202 @@
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
+
20
+ import * as fs from 'fs/promises';
21
+ import * as os from 'os';
22
+ import * as path from 'path';
23
+ import type { Skill } from '@bolloon/constraint-runtime';
24
+
25
+ /** 解析后的 SKILL.md 内部表示 */
26
+ export interface SkillMeta {
27
+ /** 唯一名, 通常 = 目录名 = frontmatter.name */
28
+ name: string;
29
+ /** SKILL.md 绝对路径 */
30
+ sourcePath: string;
31
+ /** 去掉 frontmatter 后的 Markdown body */
32
+ body: string;
33
+ /** 原始 frontmatter 解析结果, 保留以备调用方取 license/compatibility 等 */
34
+ frontmatter: Record<string, unknown>;
35
+ /** 统一后的 description */
36
+ description: string;
37
+ /** 状态: active / archived / draft, 缺省 active */
38
+ status: 'active' | 'archived' | 'draft';
39
+ /** tier (bollharness 概念, 缺省 'utility') */
40
+ tier: string;
41
+ /** 触发条件 (合并 keywords + triggers) */
42
+ triggers: string[];
43
+ }
44
+
45
+ /** YAML frontmatter 最小解析器 — 避免引入额外依赖, 支持双格式 */
46
+ function parseFrontmatter(raw: string): { frontmatter: Record<string, unknown>; body: string } {
47
+ // frontmatter 必须以 --- 开头, 紧跟换行, 再以 --- 闭合
48
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
49
+ if (!match) {
50
+ return { frontmatter: {}, body: raw };
51
+ }
52
+ const [, yamlBlock, body] = match;
53
+ const frontmatter: Record<string, unknown> = {};
54
+ const lines = yamlBlock.split(/\r?\n/);
55
+ let currentKey: string | null = null;
56
+ let currentArray: string[] | null = null;
57
+
58
+ for (const rawLine of lines) {
59
+ const line = rawLine.replace(/\s+$/, '');
60
+ if (!line.trim()) continue;
61
+ // 数组项: " - value"
62
+ const arrItem = line.match(/^\s+-\s+(.*)$/);
63
+ if (arrItem && currentKey && currentArray) {
64
+ currentArray.push(stripQuotes(arrItem[1]));
65
+ continue;
66
+ }
67
+ // 键值对: "key: value" 或 "key:"
68
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
69
+ if (kv) {
70
+ const [, key, value] = kv;
71
+ if (value === '') {
72
+ // 可能是数组开始 (下一行 " - xxx")
73
+ currentKey = key;
74
+ currentArray = [];
75
+ frontmatter[key] = currentArray;
76
+ } else {
77
+ currentKey = key;
78
+ currentArray = null;
79
+ frontmatter[key] = stripQuotes(value);
80
+ }
81
+ }
82
+ }
83
+
84
+ return { frontmatter, body: body.replace(/^\r?\n/, '') };
85
+ }
86
+
87
+ function stripQuotes(s: string): string {
88
+ const t = s.trim();
89
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
90
+ return t.slice(1, -1);
91
+ }
92
+ return t;
93
+ }
94
+
95
+ /** 把 frontmatter 统一到 SkillMeta */
96
+ function normalize(name: string, sourcePath: string, raw: string): SkillMeta | null {
97
+ const { frontmatter, body } = parseFrontmatter(raw);
98
+
99
+ // name 至少要有一个来源: frontmatter.name 或目录名
100
+ const fmName = typeof frontmatter.name === 'string' ? frontmatter.name : name;
101
+ const finalName = fmName || name;
102
+ if (!finalName) return null;
103
+
104
+ const description =
105
+ typeof frontmatter.description === 'string'
106
+ ? frontmatter.description
107
+ : '';
108
+
109
+ const statusRaw = typeof frontmatter.status === 'string' ? frontmatter.status.toLowerCase() : 'active';
110
+ const status: SkillMeta['status'] =
111
+ statusRaw === 'archived' || statusRaw === 'draft' ? statusRaw : 'active';
112
+
113
+ const tier = typeof frontmatter.tier === 'string' ? frontmatter.tier : 'utility';
114
+
115
+ // 合并两套触发字段
116
+ const triggers: string[] = [];
117
+ if (Array.isArray(frontmatter.triggers)) {
118
+ for (const t of frontmatter.triggers) if (typeof t === 'string') triggers.push(t);
119
+ }
120
+ if (Array.isArray(frontmatter.keywords)) {
121
+ for (const k of frontmatter.keywords) if (typeof k === 'string') triggers.push(k);
122
+ }
123
+
124
+ return { name: finalName, sourcePath, body, frontmatter, description, status, tier, triggers };
125
+ }
126
+
127
+ /** 解析单个 SKILL.md 文件 */
128
+ export async function parseSkillFile(filePath: string): Promise<SkillMeta | null> {
129
+ let raw: string;
130
+ try {
131
+ raw = await fs.readFile(filePath, 'utf-8');
132
+ } catch {
133
+ return null;
134
+ }
135
+ // 从路径推目录名作为 name 兜底
136
+ const dirName = path.basename(path.dirname(filePath));
137
+ return normalize(dirName, filePath, raw);
138
+ }
139
+
140
+ /** 扫描一个目录, 找所有 {name}/SKILL.md (一层嵌套结构) */
141
+ export async function loadSkillsDir(dir: string): Promise<SkillMeta[]> {
142
+ const out: SkillMeta[] = [];
143
+ let entries: import('fs').Dirent[];
144
+ try {
145
+ entries = await fs.readdir(dir, { withFileTypes: true });
146
+ } catch {
147
+ return out;
148
+ }
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory()) continue;
151
+ if (entry.name.startsWith('.')) continue;
152
+ const skillFile = path.join(dir, entry.name, 'SKILL.md');
153
+ const meta = await parseSkillFile(skillFile);
154
+ if (meta) out.push(meta);
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /** 默认 skill 路径优先级 (后者覆盖前者同名 skill) */
160
+ export function defaultSkillPaths(home: string = os.homedir(), cwd: string = process.cwd()): string[] {
161
+ return [
162
+ path.join(home, '.bolloon', 'skills'), // 全局用户级
163
+ path.join(cwd, '.bolloon', 'skills'), // 项目级
164
+ path.join(home, '.boll', 'skills'), // 全局 (兼容 bollharness 旧用户)
165
+ ];
166
+ }
167
+
168
+ /** 把 SkillMeta 包成 @bolloon/constraint-runtime 期望的 Skill 对象 */
169
+ export function skillFromMeta(meta: SkillMeta): Skill {
170
+ return {
171
+ name: meta.name,
172
+ description: meta.description || meta.tier,
173
+ execute: async (_params: Record<string, unknown>): Promise<string> => {
174
+ // Skills 协议: 把 body 当 Markdown 文档返回, 由调用方注入 LLM context
175
+ // 调用方 (use_skill tool) 拿到后会把 body 放到 tool result,
176
+ // LLM 下一轮对话看到这份指南, 按它执行
177
+ const header = `## Skill: ${meta.name}\n\n${meta.description ? `> ${meta.description}\n\n` : ''}`;
178
+ const triggersBlock = meta.triggers.length
179
+ ? `**触发条件**: ${meta.triggers.join('; ')}\n\n`
180
+ : '';
181
+ return `${header}${triggersBlock}${meta.body}`;
182
+ },
183
+ };
184
+ }
185
+
186
+ /** 加载多个目录, 同名 skill 后者覆盖前者 */
187
+ export async function loadSkillsFromPaths(paths: string[]): Promise<Skill[]> {
188
+ const seen = new Map<string, Skill>();
189
+ for (const p of paths) {
190
+ const metas = await loadSkillsDir(p);
191
+ for (const m of metas) {
192
+ if (m.status === 'archived') continue; // 归档的跳过
193
+ seen.set(m.name, skillFromMeta(m));
194
+ }
195
+ }
196
+ return Array.from(seen.values());
197
+ }
198
+
199
+ /** 列出已加载的 skills (调试/UI 用) */
200
+ export function describeSkill(s: Skill): string {
201
+ return `${s.name}: ${s.description}`;
202
+ }
@@ -38,6 +38,7 @@ export interface ToolDefinition {
38
38
  name: string;
39
39
  description: string;
40
40
  parameters: Record<string, string>;
41
+ args?: Record<string, string>;
41
42
  }
42
43
 
43
44
  export interface LoopResult {
@@ -137,7 +138,7 @@ export class WorkflowPivotLoop {
137
138
  private config: Required<PivotLoopConfig>;
138
139
  private state: PivotLoopState;
139
140
  private tools: Map<string, Tool>;
140
- private messageHistory: Array<{ role: string; content: string; toolCall?: { name: string; args: Record<string, string> }; toolResult?: ToolResult }>;
141
+ private messageHistory: Array<{ role: string; content: string; toolCall?: ToolDefinition; toolResult?: ToolResult }>;
141
142
  private streamCallback?: StreamCallback;
142
143
 
143
144
  constructor(config: PivotLoopConfig) {
@@ -318,7 +319,7 @@ export class WorkflowPivotLoop {
318
319
  });
319
320
 
320
321
  try {
321
- const result = await tool.execute(toolCall.args);
322
+ const result = await tool.execute(toolCall.args ?? {});
322
323
 
323
324
  this.emit({
324
325
  type: result.success ? 'status' : 'error',
@@ -416,9 +417,9 @@ export class WorkflowPivotLoop {
416
417
  /**
417
418
  * Extract pending tool uses from LLM response
418
419
  */
419
- private extractPendingToolUses(content: string): Array<{ name: string; args: Record<string, string> }> {
420
- const pending: Array<{ name: string; args: Record<string, string> }> = [];
421
-
420
+ private extractPendingToolUses(content: string): ToolDefinition[] {
421
+ const pending: ToolDefinition[] = [];
422
+
422
423
  // Pattern 1: Chinese format "调用工具: tool_name(args)"
423
424
  const pattern1 = /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/g;
424
425
  let match;
@@ -427,10 +428,10 @@ export class WorkflowPivotLoop {
427
428
  const argsStr = match[2];
428
429
  const args = this.parseArgs(argsStr);
429
430
  if (this.tools.has(name)) {
430
- pending.push({ name, args });
431
+ pending.push({ name, args, description: '', parameters: {} });
431
432
  }
432
433
  }
433
-
434
+
434
435
  // Pattern 2: tool_name(args) format
435
436
  const pattern2 = /(\w+)\s*\(\s*([^)]*)\s*\)/g;
436
437
  while ((match = pattern2.exec(content)) !== null) {
@@ -439,11 +440,11 @@ export class WorkflowPivotLoop {
439
440
  // Skip if already matched or doesn't look like a tool call
440
441
  if (pending.some(p => p.name === name)) continue;
441
442
  if (!this.tools.has(name)) continue;
442
-
443
+
443
444
  const args = this.parseArgs(argsStr);
444
- pending.push({ name, args });
445
+ pending.push({ name, args, description: '', parameters: {} });
445
446
  }
446
-
447
+
447
448
  // Pattern 3: JSON format tool calls
448
449
  try {
449
450
  const jsonMatch = content.match(/\{[\s\S]*"tool_calls"[\s\S]*\}/);
@@ -452,7 +453,7 @@ export class WorkflowPivotLoop {
452
453
  if (Array.isArray(parsed.tool_calls)) {
453
454
  for (const tc of parsed.tool_calls) {
454
455
  if (this.tools.has(tc.name)) {
455
- pending.push({ name: tc.name, args: tc.args || {} });
456
+ pending.push({ name: tc.name, args: tc.args || {}, description: '', parameters: {} });
456
457
  }
457
458
  }
458
459
  }
@@ -460,7 +461,7 @@ export class WorkflowPivotLoop {
460
461
  } catch {
461
462
  // JSON parsing failed, ignore
462
463
  }
463
-
464
+
464
465
  return pending;
465
466
  }
466
467
 
@@ -505,7 +505,7 @@ export class ChannelJudgmentEngine {
505
505
  * 确定 Skills(基于 Gate 和上下文)
506
506
  */
507
507
  private determineSkills(gate: Gate, context: JudgmentContext): string[] {
508
- const baseSkills = GATE_PROMPTS[gate]?.skills || ['arch'];
508
+ const baseSkills = (GATE_PROMPTS as Record<number, { skills: string[] }>)[gate]?.skills || ['arch'];
509
509
 
510
510
  // 根据上下文调整 Skills
511
511
  const { currentMessage } = context;
@@ -4,20 +4,20 @@
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
 
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
19
19
 
20
- export const CONTEXT_CHAINS_DIR = path.join('.boll', 'state', 'context-chains');
20
+ export const CONTEXT_CHAINS_DIR = path.join('.bolloon', 'state', 'context-chains');
21
21
 
22
22
  export type WorkType = 'code_change' | 'review' | 'design' | 'question' | 'planning' | 'debugging';
23
23
 
@@ -119,7 +119,7 @@ export class ContextRouter {
119
119
 
120
120
  constructor(fragmentsDir?: string) {
121
121
  this.fragmentsDir = fragmentsDir || FRAGMENTS_DIR;
122
- this.injectedFile = path.join('.boll', 'guard', 'injected.json');
122
+ this.injectedFile = path.join('.bolloon', 'guard', 'injected.json');
123
123
  }
124
124
 
125
125
  /**
@@ -23,6 +23,11 @@ export class DocumentReader {
23
23
  switch (ext) {
24
24
  case '.txt':
25
25
  case '.md':
26
+ case '.html':
27
+ case '.htm':
28
+ case '.yaml':
29
+ case '.yml':
30
+ case '.json':
26
31
  text = await fs.readFile(filePath, 'utf-8');
27
32
  break;
28
33
  case '.pdf':
@@ -205,11 +205,11 @@ export class DocumentStore {
205
205
  async readDocument(docId: string): Promise<{ content: string; metadata: ReceivedDocument } | null> {
206
206
  const docDir = path.join(this.baseDir, docId);
207
207
  const manifestPath = path.join(docDir, 'manifest.json');
208
- const filePath = path.join(docDir);
209
208
 
210
209
  try {
211
210
  const manifestData = await fs.readFile(manifestPath, 'utf-8');
212
211
  const manifest = JSON.parse(manifestData);
212
+ const filePath = path.join(docDir, manifest.fileName);
213
213
  const fileContent = await fs.readFile(filePath, 'utf-8');
214
214
 
215
215
  return {
@@ -126,12 +126,14 @@ export class Watchdog {
126
126
  return;
127
127
  }
128
128
 
129
- // 检查内存使用
129
+ // 检查内存使用: 用绝对阈值, 不看 heapUsed/heapTotal 比例 (V8 内部比例不可靠, 经常 80-95% 误报)
130
130
  const usage = process.memoryUsage();
131
- const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
132
- if (heapUsedPercent > 90) {
133
- console.warn(`[Watchdog] Memory usage critical: ${heapUsedPercent.toFixed(1)}%`);
134
- this.triggerRestart(1, `Memory usage ${heapUsedPercent.toFixed(1)}%`);
131
+ const heapUsedMB = usage.heapUsed / 1024 / 1024;
132
+ const rssMB = usage.rss / 1024 / 1024;
133
+ // 1.2GB heap 1.5GB RSS 才是真危险
134
+ if (heapUsedMB > 1224 || rssMB > 1536) {
135
+ console.warn(`[Watchdog] Memory usage critical: heapUsed=${heapUsedMB.toFixed(0)}MB, rss=${rssMB.toFixed(0)}MB`);
136
+ this.triggerRestart(1, `Memory usage heap=${heapUsedMB.toFixed(0)}MB rss=${rssMB.toFixed(0)}MB`);
135
137
  }
136
138
  }
137
139
 
@@ -8,6 +8,7 @@ export * from './HealthMonitor.js';
8
8
  export * from './Watchdog.js';
9
9
  export * from './DaemonManager.js';
10
10
  export * from './StartupVerifier.js';
11
+ export * from './self-improve-bus.js';
11
12
 
12
13
  import { createHealthMonitor, getHealthMonitor } from './HealthMonitor.js';
13
14
  import { createWatchdog, getWatchdog } from './Watchdog.js';
@@ -0,0 +1,110 @@
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
+
19
+ import { SELF_IMPROVE_COOLDOWN_MS } from '../agents/shell-guard.js';
20
+
21
+ export type SelfImproveEvent =
22
+ | { kind: 'ci-failed'; details: string }
23
+ | { kind: 'task-failures'; details: string }
24
+ | { kind: 'silent-timeout'; details: string }
25
+ | { kind: 'user-requested'; details: string };
26
+
27
+ interface EventRecord {
28
+ at: number;
29
+ count: number;
30
+ }
31
+
32
+ const EVENT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 小时内同类型只触发 1 次
33
+
34
+ const eventHistory: Map<SelfImproveEvent['kind'], EventRecord> = new Map();
35
+ let lastTriggerAt: number | null = null;
36
+
37
+ type Listener = (event: SelfImproveEvent, goal: string) => void | Promise<void>;
38
+ const listeners: Set<Listener> = new Set();
39
+
40
+ /**
41
+ * 订阅自改触发事件
42
+ */
43
+ export function onSelfImproveTrigger(fn: Listener): () => void {
44
+ listeners.add(fn);
45
+ return () => { listeners.delete(fn); };
46
+ }
47
+
48
+ /**
49
+ * 心跳事件 → 自改总线
50
+ *
51
+ * @returns { triggered: boolean, reason?: string }
52
+ */
53
+ export function reportSelfImproveEvent(event: SelfImproveEvent): { triggered: boolean; reason?: string } {
54
+ // 1. 24 小时同类事件冷却
55
+ const prev = eventHistory.get(event.kind);
56
+ if (prev && Date.now() - prev.at < EVENT_COOLDOWN_MS) {
57
+ return { triggered: false, reason: `同类事件 ${event.kind} 在 24h 内已记录过, 跳过` };
58
+ }
59
+
60
+ // 2. 累加计数
61
+ eventHistory.set(event.kind, {
62
+ at: Date.now(),
63
+ count: (prev?.count || 0) + 1
64
+ });
65
+
66
+ // 3. 自改循环冷却
67
+ if (lastTriggerAt && Date.now() - lastTriggerAt < SELF_IMPROVE_COOLDOWN_MS) {
68
+ const waitHrs = Math.ceil((SELF_IMPROVE_COOLDOWN_MS - (Date.now() - lastTriggerAt)) / 3600000);
69
+ return { triggered: false, reason: `自改冷却中, 还需要约 ${waitHrs} 小时` };
70
+ }
71
+
72
+ // 4. 触发
73
+ lastTriggerAt = Date.now();
74
+ const goal = `信号事件: ${event.kind} - ${event.details}`;
75
+
76
+ console.log(`[self-improve-bus] 🚀 触发自改循环: ${goal}`);
77
+
78
+ // 异步触发所有 listener, 不阻塞调用方
79
+ Promise.resolve().then(async () => {
80
+ for (const listener of listeners) {
81
+ try {
82
+ await listener(event, goal);
83
+ } catch (err) {
84
+ console.error(`[self-improve-bus] listener 失败:`, err);
85
+ }
86
+ }
87
+ });
88
+
89
+ return { triggered: true };
90
+ }
91
+
92
+ /**
93
+ * 获取当前事件历史 (供调试 / UI 显示)
94
+ */
95
+ export function getEventHistory(): Array<{ kind: string; at: string; count: number }> {
96
+ return Array.from(eventHistory.entries()).map(([kind, rec]) => ({
97
+ kind,
98
+ at: new Date(rec.at).toISOString(),
99
+ count: rec.count
100
+ }));
101
+ }
102
+
103
+ /**
104
+ * 强制重置 (仅供调试)
105
+ */
106
+ export function resetSelfImproveBus(): void {
107
+ eventHistory.clear();
108
+ lastTriggerAt = null;
109
+ console.log('[self-improve-bus] 已重置');
110
+ }
package/src/llm/pi-ai.ts CHANGED
@@ -143,13 +143,14 @@ export class PiAIModel {
143
143
  return this.config.baseUrl;
144
144
  }
145
145
 
146
+ // 允许通过 OPENAI_BASE_URL 等环境变量覆盖默认 base URL
146
147
  const baseUrls: Record<ModelProvider, string> = {
147
- openai: 'https://api.openai.com/v1',
148
+ openai: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
148
149
  anthropic: 'https://api.anthropic.com/v1',
149
150
  ollama: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
150
- openrouter: 'https://openrouter.ai/api/v1',
151
+ openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
151
152
  gemini: 'https://generativelanguage.googleapis.com/v1beta',
152
- minimax: 'https://api.minimaxi.com/v1',
153
+ minimax: process.env.MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1',
153
154
  local: 'http://localhost:11434'
154
155
  };
155
156
 
@@ -158,12 +159,12 @@ export class PiAIModel {
158
159
 
159
160
  private mapModel(): string {
160
161
  const modelMap: Record<ModelProvider, string> = {
161
- openai: this.config.model || 'gpt-4',
162
+ openai: this.config.model || process.env.OPENAI_MODEL || 'gpt-4',
162
163
  anthropic: this.config.model || 'claude-3-5-sonnet-20241022',
163
164
  ollama: this.config.model || 'llama3.2',
164
165
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
165
166
  gemini: this.config.model || 'gemini-2.0-flash',
166
- minimax: this.config.model || 'MiniMax-M2.7',
167
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
167
168
  local: this.config.model || 'llama3.2'
168
169
  };
169
170
  return modelMap[this.provider];
@@ -89,9 +89,10 @@ export class IrohDiscoveryService {
89
89
  }
90
90
 
91
91
  private startDiscoveryLoop(): void {
92
+ const interval = this.config.discoveryIntervalMs ?? 30000;
92
93
  this.discoveryTimer = setInterval(async () => {
93
94
  await this.discoverPeers();
94
- }, this.discoveryIntervalMs!);
95
+ }, interval);
95
96
 
96
97
  setTimeout(() => this.discoverPeers(), 2000);
97
98
  }