@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.
- package/dist/agents/p2p-chat-tools.js +321 -0
- package/dist/agents/p2p-document-tools.js +121 -1
- package/dist/agents/pi-sdk.js +185 -0
- package/dist/agents/shell-guard.js +354 -0
- package/dist/agents/shell-tool.js +83 -0
- package/dist/agents/skill-loader.js +174 -0
- package/dist/agents/workflow-pivot-loop.js +4 -4
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/cli-entry.js +1 -1
- package/dist/documents/reader.js +5 -0
- package/dist/documents/store.js +1 -1
- package/dist/heartbeat/Watchdog.js +7 -5
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/self-improve-bus.js +85 -0
- package/dist/llm/pi-ai.js +6 -5
- package/dist/network/iroh-discovery.js +2 -1
- package/dist/network/iroh-transport.js +15 -2
- package/dist/network/p2p.js +9 -8
- package/dist/network/storage/adapters/json-adapter.js +16 -1
- package/dist/network/storage/index.js +2 -1
- package/dist/pi-ecosystem-judgment/index.js +42 -115
- package/dist/social/channels/channel-heartbeat-agent.js +1 -1
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +839 -103
- package/dist/web/index.html +100 -8
- package/dist/web/server.js +568 -98
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/scripts/build-web.ts +1 -1
- package/src/agents/p2p-chat-tools.ts +383 -0
- package/src/agents/p2p-document-tools.ts +151 -1
- package/src/agents/pi-sdk.ts +196 -0
- package/src/agents/shell-guard.ts +417 -0
- package/src/agents/shell-tool.ts +103 -0
- package/src/agents/skill-loader.ts +202 -0
- package/src/agents/workflow-pivot-loop.ts +13 -12
- package/src/bollharness-integration/channel-judgment-engine.ts +1 -1
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/documents/reader.ts +5 -0
- package/src/documents/store.ts +1 -1
- package/src/heartbeat/Watchdog.ts +7 -5
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/self-improve-bus.ts +110 -0
- package/src/llm/pi-ai.ts +6 -5
- package/src/network/iroh-discovery.ts +2 -1
- package/src/network/iroh-transport.ts +15 -2
- package/src/network/p2p.ts +9 -8
- package/src/network/storage/adapters/json-adapter.ts +17 -2
- package/src/network/storage/index.ts +19 -3
- package/src/social/channels/channel-heartbeat-agent.ts +1 -1
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +839 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +577 -102
- package/src/web/style.css +506 -9
- package/tsconfig.electron.json +1 -1
- package/tsconfig.json +1 -1
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
- package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
- package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
- package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
- package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
- package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
- package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
- package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
- package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
- 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 .
|
|
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 .
|
|
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('.
|
|
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('.
|
|
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
package/dist/documents/reader.js
CHANGED
package/dist/documents/store.js
CHANGED
|
@@ -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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
/**
|
package/dist/heartbeat/index.js
CHANGED
|
@@ -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
|
-
},
|
|
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
|
-
|
|
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();
|
package/dist/network/p2p.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// 默认存储配置
|