@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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* p2p-chat-tools — 异步 chat 通道 + 持久化 inbox + 判断力外包 (draft)
|
|
3
|
+
*
|
|
4
|
+
* 解决痛点: 两个节点的智能体代替各自人类主人做异步判断
|
|
5
|
+
*
|
|
6
|
+
* 流程:
|
|
7
|
+
* 1. A 节点 (人类在线) 通过 sendChat(peerDID, text) 发消息 → iroh 'agent_chat'
|
|
8
|
+
* 2. B 节点 (人类可能离线):
|
|
9
|
+
* - onMessage('agent_chat') → 落 ~/.bolloon/inbox/<peerDID>.jsonl
|
|
10
|
+
* - 状态 = 'received' (未处理)
|
|
11
|
+
* 3. B 节点 wake-up (processPendingInbox) → 扫描 status='received' 的消息
|
|
12
|
+
* - 调 LLM 生成 draft (注入主人历史判断 + ValueProfile)
|
|
13
|
+
* - 写 status='drafted', draft 落盘
|
|
14
|
+
* - 通过 'agent_chat' 消息类型回送 draft (前缀 [DRAFT] 表明是代回)
|
|
15
|
+
* 4. A 节点收到 draft → 写到 A 的 outbox (inbox 中 from=B 的那条)
|
|
16
|
+
* 5. B 人类上线 → getInbox() 看到 'drafted' 状态的条目, approveAndSend / dismissDraft
|
|
17
|
+
*
|
|
18
|
+
* 镜像 p2p-document-tools.ts 风格: iroh 消息 + 可注入 transport (支持多实例测试)
|
|
19
|
+
*/
|
|
20
|
+
import * as crypto from 'crypto';
|
|
21
|
+
import * as fs from 'fs/promises';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
import { EventEmitter } from 'events';
|
|
25
|
+
import { irohTransport as defaultIrohTransport } from '../network/iroh-transport.js';
|
|
26
|
+
import { getRelevantValues, getValueProfile, loadAllJudgments } from '../pi-ecosystem-judgment/human-value-store.js';
|
|
27
|
+
function homeDir() {
|
|
28
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
29
|
+
}
|
|
30
|
+
class ChatEventBus extends EventEmitter {
|
|
31
|
+
}
|
|
32
|
+
export const chatEventBus = new ChatEventBus();
|
|
33
|
+
const states = new WeakMap();
|
|
34
|
+
function getState(transport) {
|
|
35
|
+
let s = states.get(transport);
|
|
36
|
+
if (!s) {
|
|
37
|
+
s = { handlers: new Set(), listenerInstalled: false };
|
|
38
|
+
states.set(transport, s);
|
|
39
|
+
}
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Inbox 存储
|
|
44
|
+
// ============================================================================
|
|
45
|
+
function inboxDir() {
|
|
46
|
+
return path.join(homeDir(), '.bolloon', 'inbox');
|
|
47
|
+
}
|
|
48
|
+
function inboxPath(peerDID) {
|
|
49
|
+
const safe = peerDID.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
50
|
+
return path.join(inboxDir(), `${safe}.jsonl`);
|
|
51
|
+
}
|
|
52
|
+
function outboxPath() {
|
|
53
|
+
return path.join(inboxDir(), '_outbox.jsonl');
|
|
54
|
+
}
|
|
55
|
+
async function appendInbox(peerDID, entry) {
|
|
56
|
+
await fs.mkdir(inboxDir(), { recursive: true });
|
|
57
|
+
await fs.appendFile(inboxPath(peerDID), JSON.stringify(entry) + '\n', 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
async function readInbox(peerDID) {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(inboxPath(peerDID), 'utf-8');
|
|
62
|
+
return content.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function rewriteInbox(peerDID, entries) {
|
|
69
|
+
await fs.mkdir(inboxDir(), { recursive: true });
|
|
70
|
+
await fs.writeFile(inboxPath(peerDID), entries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
async function updateInboxEntry(peerDID, id, patch) {
|
|
73
|
+
const entries = await readInbox(peerDID);
|
|
74
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
75
|
+
if (idx < 0)
|
|
76
|
+
return null;
|
|
77
|
+
entries[idx] = { ...entries[idx], ...patch };
|
|
78
|
+
await rewriteInbox(peerDID, entries);
|
|
79
|
+
return entries[idx];
|
|
80
|
+
}
|
|
81
|
+
async function markOutboundDraft(originalId, draft, at) {
|
|
82
|
+
try {
|
|
83
|
+
const content = await fs.readFile(outboxPath(), 'utf-8');
|
|
84
|
+
const entries = content.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
85
|
+
const idx = entries.findIndex((e) => e.id === originalId);
|
|
86
|
+
if (idx >= 0) {
|
|
87
|
+
entries[idx].inboundDraft = draft;
|
|
88
|
+
entries[idx].inboundDraftAt = at;
|
|
89
|
+
await fs.writeFile(outboxPath(), entries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch { /* no outbox */ }
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Listener 安装 (每个 transport 各装一次)
|
|
96
|
+
// ============================================================================
|
|
97
|
+
function ensureListener(transport) {
|
|
98
|
+
const s = getState(transport);
|
|
99
|
+
if (s.listenerInstalled)
|
|
100
|
+
return;
|
|
101
|
+
s.listenerInstalled = true;
|
|
102
|
+
transport.onMessage('agent_chat', (msg) => {
|
|
103
|
+
try {
|
|
104
|
+
const data = JSON.parse(new TextDecoder().decode(msg.payload));
|
|
105
|
+
void handleIncomingChat(transport, msg.from, data);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error('[ChatReceiver] parse err:', e.message);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function handleIncomingChat(transport, fromNodeId, data) {
|
|
113
|
+
const peerDID = fromNodeId;
|
|
114
|
+
if (data.kind === 'user') {
|
|
115
|
+
const entry = {
|
|
116
|
+
id: data.id, peerDID, fromNodeId, text: data.text,
|
|
117
|
+
timestamp: Date.now(), receivedAt: Date.now(), status: 'received',
|
|
118
|
+
};
|
|
119
|
+
await appendInbox(peerDID, entry);
|
|
120
|
+
const preview = data.text.slice(0, 60);
|
|
121
|
+
const peerShort = peerDID.slice(0, 12);
|
|
122
|
+
console.log('[ChatReceiver] received from ' + peerShort + ': ' + preview);
|
|
123
|
+
chatEventBus.emit('chat', { kind: 'inbox_new', peerDID, entry });
|
|
124
|
+
for (const h of getState(transport).handlers) {
|
|
125
|
+
try {
|
|
126
|
+
h(entry, fromNodeId);
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (data.kind === 'draft' && data.originalId) {
|
|
132
|
+
await markOutboundDraft(data.originalId, data.text, Date.now());
|
|
133
|
+
console.log('[ChatReceiver] inbound draft for ' + data.originalId);
|
|
134
|
+
chatEventBus.emit('chat', { kind: 'outbox_draft_in', peerDID, originalId: data.originalId, draft: data.text });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// 公共 API (可注入 transport)
|
|
139
|
+
// ============================================================================
|
|
140
|
+
export function onChatMessage(handler, transport = defaultIrohTransport) {
|
|
141
|
+
getState(transport).handlers.add(handler);
|
|
142
|
+
ensureListener(transport);
|
|
143
|
+
}
|
|
144
|
+
export async function initChatReceiver(transport = defaultIrohTransport) {
|
|
145
|
+
ensureListener(transport);
|
|
146
|
+
await fs.mkdir(inboxDir(), { recursive: true });
|
|
147
|
+
console.log('[ChatReceiver] Initialized at', inboxDir(), 'transport=', transport === defaultIrohTransport ? 'singleton' : 'instance');
|
|
148
|
+
}
|
|
149
|
+
export async function sendChat(peerDID, text, transport = defaultIrohTransport) {
|
|
150
|
+
const id = crypto.randomUUID();
|
|
151
|
+
const ok = await transport.sendMessage(peerDID, 'agent_chat', new TextEncoder().encode(JSON.stringify({ kind: 'user', id, text })));
|
|
152
|
+
if (ok) {
|
|
153
|
+
const entry = {
|
|
154
|
+
id, peerDID,
|
|
155
|
+
fromNodeId: transport.getNodeId() || '',
|
|
156
|
+
text, timestamp: Date.now(), receivedAt: Date.now(),
|
|
157
|
+
status: 'sent', sentText: text, sentAt: Date.now(),
|
|
158
|
+
};
|
|
159
|
+
await fs.mkdir(inboxDir(), { recursive: true });
|
|
160
|
+
await fs.appendFile(outboxPath(), JSON.stringify(entry) + '\n', 'utf-8');
|
|
161
|
+
chatEventBus.emit('chat', { kind: 'outbox_updated', peerDID, entry });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.warn(`[ChatReceiver] send to ${peerDID.slice(0, 12)}... failed`);
|
|
165
|
+
}
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
168
|
+
export async function getInbox(peerDID) {
|
|
169
|
+
if (peerDID)
|
|
170
|
+
return readInbox(peerDID);
|
|
171
|
+
try {
|
|
172
|
+
const files = await fs.readdir(inboxDir());
|
|
173
|
+
const all = [];
|
|
174
|
+
for (const f of files) {
|
|
175
|
+
if (!f.endsWith('.jsonl') || f.startsWith('_'))
|
|
176
|
+
continue;
|
|
177
|
+
const peerDID = f.replace('.jsonl', '').replace(/_/g, ':');
|
|
178
|
+
all.push(...await readInbox(peerDID));
|
|
179
|
+
}
|
|
180
|
+
return all.sort((a, b) => b.receivedAt - a.receivedAt);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Draft 引擎
|
|
188
|
+
// ============================================================================
|
|
189
|
+
async function buildValueHint(text) {
|
|
190
|
+
try {
|
|
191
|
+
const allJudgments = await loadAllJudgments();
|
|
192
|
+
const relevant = await getRelevantValues(text, undefined);
|
|
193
|
+
const profile = await getValueProfile('me');
|
|
194
|
+
const profileHint = [
|
|
195
|
+
`quality_focus=${profile.quality_focus.toFixed(2)}`,
|
|
196
|
+
`efficiency_focus=${profile.efficiency_focus.toFixed(2)}`,
|
|
197
|
+
`safety_focus=${profile.safety_focus.toFixed(2)}`,
|
|
198
|
+
`collaboration_focus=${profile.collaboration_focus.toFixed(2)}`,
|
|
199
|
+
`learning_focus=${profile.learning_focus.toFixed(2)}`,
|
|
200
|
+
].join(', ');
|
|
201
|
+
const reasons = allJudgments
|
|
202
|
+
.filter((j) => (j.reasons || []).length > 0)
|
|
203
|
+
.slice(-10)
|
|
204
|
+
.flatMap((j) => j.reasons || [])
|
|
205
|
+
.slice(0, 20);
|
|
206
|
+
return `\n[主人历史判断 (style 参考, 不可外泄)]\n关注维度: ${profileHint}\n关键理由示例: ${reasons.join(' | ').slice(0, 400) || '(无)'}\n`;
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export async function generateDraft(messageId, peerDID, transport = defaultIrohTransport) {
|
|
213
|
+
const entries = await readInbox(peerDID);
|
|
214
|
+
const entry = entries.find((e) => e.id === messageId);
|
|
215
|
+
if (!entry)
|
|
216
|
+
return null;
|
|
217
|
+
if (entry.status !== 'received')
|
|
218
|
+
return entry;
|
|
219
|
+
const valueHint = await buildValueHint(entry.text);
|
|
220
|
+
const promptForDraft = `你是主人的代理. 对方发来这条消息: "${entry.text.slice(0, 1500)}"\n${valueHint}\n请基于主人的历史判断, 用 1-2 句话代主人拟一个回复草案. 草案要保留主人的语气和立场, 开头标注 [DRAFT]. 直接给草案文本, 不要解释.`;
|
|
221
|
+
let draftText = '';
|
|
222
|
+
let confidence = 0.5;
|
|
223
|
+
try {
|
|
224
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
225
|
+
if (!openaiKey)
|
|
226
|
+
throw new Error('OPENAI_API_KEY not set');
|
|
227
|
+
const openaiBase = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
|
228
|
+
const openaiModel = process.env.OPENAI_MODEL || 'gpt-4';
|
|
229
|
+
const r = await fetch(`${openaiBase}/chat/completions`, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${openaiKey}` },
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
model: openaiModel,
|
|
234
|
+
messages: [
|
|
235
|
+
{ role: 'system', content: '你是主人的代理. 你的输出会被主人审阅后才发出. 请谨慎.' },
|
|
236
|
+
{ role: 'user', content: promptForDraft },
|
|
237
|
+
],
|
|
238
|
+
temperature: 0.4,
|
|
239
|
+
max_tokens: 300,
|
|
240
|
+
}),
|
|
241
|
+
signal: AbortSignal.timeout(15000),
|
|
242
|
+
});
|
|
243
|
+
if (r.ok) {
|
|
244
|
+
const data = await r.json();
|
|
245
|
+
draftText = (data.choices?.[0]?.message?.content || '').trim();
|
|
246
|
+
confidence = 0.7;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.warn(`[DraftEngine] LLM ${r.status}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
console.warn(`[DraftEngine] LLM call failed: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
if (!draftText) {
|
|
256
|
+
draftText = `[DRAFT] 已收到. (本地 draft 引擎未配置 LLM, 主人上线后请手写回复)`;
|
|
257
|
+
confidence = 0.1;
|
|
258
|
+
}
|
|
259
|
+
const updated = await updateInboxEntry(peerDID, messageId, {
|
|
260
|
+
status: 'drafted', draft: draftText, draftConfidence: confidence,
|
|
261
|
+
draftReasoning: valueHint.slice(0, 200), draftAt: Date.now(),
|
|
262
|
+
});
|
|
263
|
+
if (updated) {
|
|
264
|
+
const sent = await transport.sendMessage(peerDID, 'agent_chat', new TextEncoder().encode(JSON.stringify({ kind: 'draft', id: crypto.randomUUID(), text: draftText, originalId: messageId })));
|
|
265
|
+
if (sent)
|
|
266
|
+
console.log('[DraftEngine] draft sent to ' + peerDID.slice(0, 12) + ' for ' + messageId.slice(0, 8));
|
|
267
|
+
else
|
|
268
|
+
console.warn('[DraftEngine] failed to send draft');
|
|
269
|
+
chatEventBus.emit('chat', { kind: 'draft_ready', peerDID, entry: updated });
|
|
270
|
+
chatEventBus.emit('chat', { kind: 'outbox_updated', peerDID, entry: updated });
|
|
271
|
+
}
|
|
272
|
+
return updated;
|
|
273
|
+
}
|
|
274
|
+
export async function processPendingInbox(transport = defaultIrohTransport) {
|
|
275
|
+
await fs.mkdir(inboxDir(), { recursive: true });
|
|
276
|
+
const files = await fs.readdir(inboxDir());
|
|
277
|
+
let processed = 0, skipped = 0;
|
|
278
|
+
for (const f of files) {
|
|
279
|
+
if (!f.endsWith('.jsonl') || f.startsWith('_'))
|
|
280
|
+
continue;
|
|
281
|
+
const peerDID = f.replace('.jsonl', '').replace(/_/g, ':');
|
|
282
|
+
const entries = await readInbox(peerDID);
|
|
283
|
+
for (const e of entries) {
|
|
284
|
+
if (e.status === 'received') {
|
|
285
|
+
const r = await generateDraft(e.id, peerDID, transport);
|
|
286
|
+
if (r)
|
|
287
|
+
processed++;
|
|
288
|
+
else
|
|
289
|
+
skipped++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { processed, skipped };
|
|
294
|
+
}
|
|
295
|
+
export async function approveAndSend(messageId, peerDID, finalText, transport = defaultIrohTransport) {
|
|
296
|
+
const entries = await readInbox(peerDID);
|
|
297
|
+
const entry = entries.find((e) => e.id === messageId);
|
|
298
|
+
if (!entry || entry.status !== 'drafted')
|
|
299
|
+
return false;
|
|
300
|
+
const text = finalText || entry.draft || '';
|
|
301
|
+
const ok = await transport.sendMessage(peerDID, 'agent_chat', new TextEncoder().encode(JSON.stringify({ kind: 'user', id: crypto.randomUUID(), text })));
|
|
302
|
+
if (ok) {
|
|
303
|
+
const updated = await updateInboxEntry(peerDID, messageId, { status: 'sent', sentText: text, sentAt: Date.now() });
|
|
304
|
+
if (updated)
|
|
305
|
+
chatEventBus.emit('chat', { kind: 'inbox_sent', peerDID, entry: updated });
|
|
306
|
+
}
|
|
307
|
+
return ok;
|
|
308
|
+
}
|
|
309
|
+
export async function dismissDraft(messageId, peerDID) {
|
|
310
|
+
const entries = await readInbox(peerDID);
|
|
311
|
+
const entry = entries.find((e) => e.id === messageId);
|
|
312
|
+
if (!entry)
|
|
313
|
+
return false;
|
|
314
|
+
const updated = await updateInboxEntry(peerDID, messageId, { status: 'dismissed' });
|
|
315
|
+
if (updated)
|
|
316
|
+
chatEventBus.emit('chat', { kind: 'inbox_dismissed', peerDID, entry: updated });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
export const p2pChatTools = {
|
|
320
|
+
sendChat, getInbox, processPendingInbox, generateDraft, approveAndSend, dismissDraft, onChatMessage, initChatReceiver,
|
|
321
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as crypto from 'crypto';
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
6
7
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
7
8
|
import { documentReader } from '../documents/reader.js';
|
|
8
9
|
import { documentStore } from '../documents/store.js';
|
|
@@ -15,6 +16,11 @@ function getMimeType(filename) {
|
|
|
15
16
|
const mimeTypes = {
|
|
16
17
|
'txt': 'text/plain',
|
|
17
18
|
'md': 'text/markdown',
|
|
19
|
+
'html': 'text/html',
|
|
20
|
+
'htm': 'text/html',
|
|
21
|
+
'yaml': 'application/yaml',
|
|
22
|
+
'yml': 'application/yaml',
|
|
23
|
+
'json': 'application/json',
|
|
18
24
|
'pdf': 'application/pdf',
|
|
19
25
|
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
20
26
|
};
|
|
@@ -22,8 +28,10 @@ function getMimeType(filename) {
|
|
|
22
28
|
}
|
|
23
29
|
export async function initDocumentReceiver() {
|
|
24
30
|
await documentStore.initialize();
|
|
25
|
-
documentStore.onDocumentReceived((doc) => {
|
|
31
|
+
documentStore.onDocumentReceived(async (doc) => {
|
|
26
32
|
console.log(`[DocumentReceiver] Document received: ${doc.fileName} from ${doc.fromNodeIdShort}`);
|
|
33
|
+
// 异步调用 LLM 解析(不阻塞接收流程)
|
|
34
|
+
void parseDocumentWithLLM(doc);
|
|
27
35
|
});
|
|
28
36
|
irohTransport.onMessage('document_chunk', async (msg) => {
|
|
29
37
|
try {
|
|
@@ -39,6 +47,118 @@ export async function initDocumentReceiver() {
|
|
|
39
47
|
});
|
|
40
48
|
console.log('[DocumentReceiver] Initialized and listening for document chunks');
|
|
41
49
|
}
|
|
50
|
+
const feedbackHandlers = new Set();
|
|
51
|
+
/** 注册 AI 解析反馈监听器(发送方调用) */
|
|
52
|
+
export function onAIFeedback(handler) {
|
|
53
|
+
feedbackHandlers.add(handler);
|
|
54
|
+
// iroh listener 只挂一次
|
|
55
|
+
ensureFeedbackListenerInstalled();
|
|
56
|
+
}
|
|
57
|
+
let feedbackListenerInstalled = false;
|
|
58
|
+
function ensureFeedbackListenerInstalled() {
|
|
59
|
+
if (feedbackListenerInstalled)
|
|
60
|
+
return;
|
|
61
|
+
feedbackListenerInstalled = true;
|
|
62
|
+
irohTransport.onMessage('ai_feedback', (msg) => {
|
|
63
|
+
try {
|
|
64
|
+
const fb = JSON.parse(new TextDecoder().decode(msg.payload));
|
|
65
|
+
for (const h of feedbackHandlers) {
|
|
66
|
+
try {
|
|
67
|
+
h(fb, msg.from);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.error('[AIFeedback] handler error:', e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
console.error('[AIFeedback] failed to parse message:', e);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** 文档解析服务:优先走 web 统一入口 (含 judgment + harness), fallback 到本地 LLM */
|
|
80
|
+
async function callAIParseService(text, fileName, mimeType, fromNodeId) {
|
|
81
|
+
// 1) 优先: 调 web 端 POST /api/ai-parse (统一入口, 含 judgment + harness)
|
|
82
|
+
const webBase = process.env.BOLLOON_WEB_URL || process.env.PORTAL_URL || 'http://127.0.0.1:54188';
|
|
83
|
+
try {
|
|
84
|
+
const r = await fetch(`${webBase}/api/ai-parse`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ text, mimeType, fileName, fromNodeId, source: 'p2p-document' }),
|
|
88
|
+
// 短超时, fallback 友好
|
|
89
|
+
signal: AbortSignal.timeout(8000),
|
|
90
|
+
});
|
|
91
|
+
if (r.ok) {
|
|
92
|
+
const data = await r.json();
|
|
93
|
+
return {
|
|
94
|
+
summary: data.summary,
|
|
95
|
+
qualityScore: data.qualityScore,
|
|
96
|
+
source: 'web',
|
|
97
|
+
judgmentId: data.judgmentId,
|
|
98
|
+
gateArtifact: data.gateArtifact,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
console.warn(`[AIParse] web returned ${r.status}, fallback to local`);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
console.warn(`[AIParse] web ${webBase} unreachable (${e.message}), fallback to local`);
|
|
105
|
+
}
|
|
106
|
+
// 2) Fallback: 直接调本地 LLM (不调 judgment/harness, 仅在 web 不可用时使用)
|
|
107
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.MINIMAX_API_KEY || process.env.OPENROUTER_API_KEY || process.env.GEMINI_API_KEY;
|
|
108
|
+
if (!apiKey) {
|
|
109
|
+
throw new Error('No web endpoint and no LLM API key configured. Set BOLLOON_WEB_URL or OPENAI_API_KEY.');
|
|
110
|
+
}
|
|
111
|
+
const { getMinimax } = await import('../constraints/index.js');
|
|
112
|
+
const llm = getMinimax();
|
|
113
|
+
const truncated = text.length > 6000 ? text.substring(0, 6000) + '...[截断]' : text;
|
|
114
|
+
const prompt = `请分析以下 ${mimeType} 文档,并给出 (1) 一句话中文摘要 (2) 关键要点列表 (3) 文档质量评分(0-1)。\n\n文件名: ${fileName}\n\n内容:\n${truncated}`;
|
|
115
|
+
const r = await llm.summarize(prompt);
|
|
116
|
+
return { summary: r.summary, qualityScore: r.qualityScore, source: 'local' };
|
|
117
|
+
}
|
|
118
|
+
async function parseDocumentWithLLM(doc) {
|
|
119
|
+
try {
|
|
120
|
+
const filePath = path.join(process.env.HOME || '/tmp', '.bolloon', 'documents', 'received', doc.id, doc.fileName);
|
|
121
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
122
|
+
const r = await callAIParseService(content, doc.fileName, doc.mimeType, doc.fromNodeId);
|
|
123
|
+
const sidecar = {
|
|
124
|
+
filename: doc.fileName,
|
|
125
|
+
mimeType: doc.mimeType,
|
|
126
|
+
fromNodeId: doc.fromNodeId,
|
|
127
|
+
receivedAt: doc.receivedAt,
|
|
128
|
+
summary: r.summary,
|
|
129
|
+
qualityScore: r.qualityScore,
|
|
130
|
+
parseSource: r.source,
|
|
131
|
+
judgmentId: r.judgmentId,
|
|
132
|
+
gateArtifact: r.gateArtifact,
|
|
133
|
+
analyzedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
const sidecarPath = path.join(process.env.HOME || '/tmp', '.bolloon', 'documents', 'received', doc.id, 'ai-analysis.json');
|
|
136
|
+
await fs.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
137
|
+
console.log(`[DocumentReceiver] AI parsed ${doc.fileName} via ${r.source} (score=${r.qualityScore.toFixed(2)})${r.judgmentId ? ` judgment=${r.judgmentId}` : ''}${r.gateArtifact ? ` gate=${r.gateArtifact}` : ''}`);
|
|
138
|
+
// 把 AI 解析结果回送给发送方 (doc.fromNodeId),形成 "接收 → 解析 → 反馈" 闭环
|
|
139
|
+
if (doc.fromNodeId) {
|
|
140
|
+
const feedback = {
|
|
141
|
+
docId: doc.id,
|
|
142
|
+
fileName: doc.fileName,
|
|
143
|
+
mimeType: doc.mimeType,
|
|
144
|
+
summary: r.summary,
|
|
145
|
+
qualityScore: r.qualityScore,
|
|
146
|
+
feedbackAt: Date.now(),
|
|
147
|
+
fromNodeId: irohTransport.getNodeId() || '',
|
|
148
|
+
};
|
|
149
|
+
const sent = await irohTransport.sendMessage(doc.fromNodeId, 'ai_feedback', new TextEncoder().encode(JSON.stringify(feedback)));
|
|
150
|
+
if (sent) {
|
|
151
|
+
console.log(`[DocumentReceiver] Feedback sent to ${doc.fromNodeIdShort}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.warn(`[DocumentReceiver] Failed to send feedback to ${doc.fromNodeIdShort}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`[DocumentReceiver] AI parse failed for ${doc.fileName}:`, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
42
162
|
export const p2pDocumentTools = [
|
|
43
163
|
{
|
|
44
164
|
name: 'list_online_peers',
|