@bolloon/bolloon-agent 0.1.23 → 0.1.24
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/agent-manifest-protocol.js +81 -0
- package/dist/agents/iroh-secret.js +32 -0
- package/dist/index.js +5 -8
- package/dist/network/iroh-transport.js +14 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +40 -0
- package/dist/utils/auto-update.js +11 -2
- package/dist/web/agent-delegate-server.js +123 -0
- package/dist/web/client.js +437 -8
- package/dist/web/index.html +63 -0
- package/dist/web/iroh-delegate-transport.js +125 -0
- package/dist/web/server.js +304 -1
- package/dist/web/style.css +7 -0
- package/package.json +1 -1
- package/src/agents/agent-manifest-protocol.ts +117 -0
- package/src/agents/iroh-secret.ts +32 -0
- package/src/index.ts +6 -8
- package/src/network/iroh-transport.ts +14 -0
- package/src/utils/auto-update.ts +12 -2
- package/src/web/agent-delegate-server.ts +148 -0
- package/src/web/client.js +437 -8
- package/src/web/index.html +63 -0
- package/src/web/iroh-delegate-transport.ts +139 -0
- package/src/web/server.ts +312 -1
- package/src/web/style.css +7 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iroh-delegate-transport — 把 irohTransport 适配成 DelegateTransport 抽象
|
|
3
|
+
*
|
|
4
|
+
* 目的: 让 agent-delegate-server 可以挂到 iroh transport 上, 而不是只在 Hyperswarm 测试里跑.
|
|
5
|
+
*
|
|
6
|
+
* 注意命名坑:
|
|
7
|
+
* - DelegateTransport.sendToNode 用的是 "publicKey" (Hyperswarm 测试用 Hyperswarm 公钥)
|
|
8
|
+
* - iroh 路径里我们没有 "publicKey", 只有 nodeId (string)
|
|
9
|
+
* - 这里我们把 iroh nodeId 直接当作 publicKey 字段传入. 调用方 (/api/agent/delegate) 的
|
|
10
|
+
* toPublicKey 参数实际上对应的就是 iroh 目标 nodeId.
|
|
11
|
+
* - DIAP 真实 did:key 与 iroh nodeId 的映射关系另由 agent-manifest 协议承担
|
|
12
|
+
* (manifest_payload.ownerPublicKey 字段), 后续在 manifest cache 里维护.
|
|
13
|
+
*/
|
|
14
|
+
import { irohTransport } from '../network/iroh-transport.js';
|
|
15
|
+
import { parseFrame } from '../agents/agent-manifest-protocol.js';
|
|
16
|
+
/**
|
|
17
|
+
* 构造一个基于 irohTransport 的 DelegateTransport 实现.
|
|
18
|
+
*
|
|
19
|
+
* sendToNode: 用 irohTransport.sendMessage 发送 frame, 然后用 requestResponse 等待
|
|
20
|
+
* 对端 via onIncomingFrame 注册的 handler 写回.
|
|
21
|
+
* 由于 iroh 1-shot 消息没有 req/resp 关联, 这里用 'agent_request' 类型
|
|
22
|
+
* 包一层, 在 onMessage('agent_request') 内部复用现有 handler, 拿到 reply
|
|
23
|
+
* 再用 'agent_response' 单播回原 node.
|
|
24
|
+
*
|
|
25
|
+
* onIncomingFrame: 把对方发的 manifest_request / manifest_payload / agent_delegate
|
|
26
|
+
* 类型消息路由到 agent-delegate-server 注册的 handler.
|
|
27
|
+
*/
|
|
28
|
+
export function createIrohDelegateTransport(opts = {}) {
|
|
29
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
30
|
+
const verbose = opts.verbose ?? false;
|
|
31
|
+
let incomingHandler = null;
|
|
32
|
+
// 单播回包映射: requestId -> resolver
|
|
33
|
+
const pendingReplies = new Map();
|
|
34
|
+
// 启动时挂一次 onMessage 监听 (重复挂也只会换 handler, 不重复触发)
|
|
35
|
+
irohTransport.onMessage('agent_request', async (msg) => {
|
|
36
|
+
if (!incomingHandler)
|
|
37
|
+
return;
|
|
38
|
+
const f = parseFrame(new TextDecoder().decode(msg.payload));
|
|
39
|
+
if (!f)
|
|
40
|
+
return;
|
|
41
|
+
const fromKey = msg.from;
|
|
42
|
+
const reply = await incomingHandler(fromKey, new TextDecoder().decode(msg.payload));
|
|
43
|
+
if (reply) {
|
|
44
|
+
try {
|
|
45
|
+
// 把 reply 也用 agent_response 类型发回去, requestId 透传
|
|
46
|
+
const replyFrame = JSON.parse(reply);
|
|
47
|
+
const reqId = f._reqId || replyFrame._reqId;
|
|
48
|
+
const tagged = reqId ? JSON.stringify({ ...replyFrame, _reqId: reqId }) : reply;
|
|
49
|
+
await irohTransport.sendMessage(fromKey, 'agent_response', new TextEncoder().encode(tagged));
|
|
50
|
+
if (verbose)
|
|
51
|
+
console.log(`[iroh-delegate] 已回包给 ${fromKey.substring(0, 12)}... (${replyFrame.type})`);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
if (verbose)
|
|
55
|
+
console.warn('[iroh-delegate] 回包失败:', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
irohTransport.onMessage('agent_response', (msg) => {
|
|
60
|
+
const f = parseFrame(new TextDecoder().decode(msg.payload));
|
|
61
|
+
if (!f)
|
|
62
|
+
return;
|
|
63
|
+
const reqId = f._reqId;
|
|
64
|
+
if (!reqId)
|
|
65
|
+
return;
|
|
66
|
+
const pending = pendingReplies.get(reqId);
|
|
67
|
+
if (pending) {
|
|
68
|
+
clearTimeout(pending.timer);
|
|
69
|
+
pendingReplies.delete(reqId);
|
|
70
|
+
pending.resolve(JSON.stringify(f));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
sendToNode: async (publicKey, frame, timeoutOverrideMs) => {
|
|
75
|
+
const t = timeoutOverrideMs ?? timeoutMs;
|
|
76
|
+
const reqId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
77
|
+
const tagged = JSON.stringify({ ...JSON.parse(frame), _reqId: reqId });
|
|
78
|
+
const payload = new TextEncoder().encode(tagged);
|
|
79
|
+
return new Promise(async (resolve) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
pendingReplies.delete(reqId);
|
|
82
|
+
resolve(null);
|
|
83
|
+
}, t);
|
|
84
|
+
pendingReplies.set(reqId, { resolve, timer });
|
|
85
|
+
try {
|
|
86
|
+
const ok = await irohTransport.sendMessage(publicKey, 'agent_request', payload);
|
|
87
|
+
if (!ok) {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
pendingReplies.delete(reqId);
|
|
90
|
+
resolve(null);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (verbose)
|
|
95
|
+
console.warn('[iroh-delegate] 发送失败:', e);
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
pendingReplies.delete(reqId);
|
|
98
|
+
resolve(null);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
onIncomingFrame: (handler) => {
|
|
103
|
+
incomingHandler = handler;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 把本地节点 manifest 注册到 agent-manifest 协议, 并写入本地缓存.
|
|
109
|
+
* 在 iroh 初始化完成时调用一次.
|
|
110
|
+
*/
|
|
111
|
+
export function registerLocalAgents(ownerName, ownerPublicKey, agents) {
|
|
112
|
+
// 动态导入避免循环引用
|
|
113
|
+
import('../agents/agent-manifest-protocol.js').then((mod) => {
|
|
114
|
+
mod.setLocalManifest({
|
|
115
|
+
ownerName,
|
|
116
|
+
ownerPublicKey,
|
|
117
|
+
agents: agents.map((a) => ({
|
|
118
|
+
id: a.id,
|
|
119
|
+
name: a.name,
|
|
120
|
+
capabilities: a.capabilities,
|
|
121
|
+
status: a.status || 'active',
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
package/dist/web/server.js
CHANGED
|
@@ -11,6 +11,8 @@ import { initMinimax, getMinimax } from '../constraints/index.js';
|
|
|
11
11
|
import { createAgentSession } from '../agents/pi-sdk.js';
|
|
12
12
|
import { llmConfigStore } from '../llm/config-store.js';
|
|
13
13
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
14
|
+
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
15
|
+
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
14
16
|
import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
15
17
|
// 前端资源路径:在打包后会通过 CommonJS require 加载,使用 import.meta.url
|
|
16
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -310,7 +312,9 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
310
312
|
}
|
|
311
313
|
return session;
|
|
312
314
|
}
|
|
313
|
-
|
|
315
|
+
let selfImproveEnabled = false;
|
|
316
|
+
export async function createWebServer(port = 3000, options = {}) {
|
|
317
|
+
selfImproveEnabled = options.selfImprove ?? false;
|
|
314
318
|
// 防止 P2P DHT 超时等错误导致进程崩溃
|
|
315
319
|
process.on('unhandledRejection', (reason, promise) => {
|
|
316
320
|
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
@@ -1489,6 +1493,17 @@ export async function createWebServer(port = 3000) {
|
|
|
1489
1493
|
initialized: true
|
|
1490
1494
|
};
|
|
1491
1495
|
irohInitialized = true;
|
|
1496
|
+
// 挂载 agent-delegate app (manifest 协议 + agent_delegate)
|
|
1497
|
+
// 必须在 irohInitialized 之后挂, 因为适配器要监听 irohTransport.onMessage
|
|
1498
|
+
try {
|
|
1499
|
+
const delegateTransport = createIrohDelegateTransport({ verbose: true });
|
|
1500
|
+
const delegateApp = createAgentDelegateApp(delegateTransport);
|
|
1501
|
+
app.use('/api/agent', delegateApp);
|
|
1502
|
+
console.log('[iroh API] agent-delegate app 已挂载到 /api/agent');
|
|
1503
|
+
}
|
|
1504
|
+
catch (e) {
|
|
1505
|
+
console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
|
|
1506
|
+
}
|
|
1492
1507
|
// 设置消息处理
|
|
1493
1508
|
irohTransport.onMessage('chat', (msg) => {
|
|
1494
1509
|
const content = new TextDecoder().decode(msg.payload);
|
|
@@ -2177,6 +2192,294 @@ export async function createWebServer(port = 3000) {
|
|
|
2177
2192
|
res.status(500).json({ error: err.message });
|
|
2178
2193
|
}
|
|
2179
2194
|
});
|
|
2195
|
+
// ==================== Judgments (v1 核心: 让我能记录判断) ====================
|
|
2196
|
+
// POST /api/judgments — 记录一个判断
|
|
2197
|
+
// GET /api/judgments — 列出所有判断 (新→旧)
|
|
2198
|
+
// 存储: ~/.bolloon/human-values/judgments.json (human-value-store)
|
|
2199
|
+
// 极简版: 只记录 decision + reason; 其它字段可选
|
|
2200
|
+
app.post('/api/judgments', async (req, res) => {
|
|
2201
|
+
try {
|
|
2202
|
+
const { decision, reason, context } = req.body;
|
|
2203
|
+
if (!decision || typeof decision !== 'string' || !decision.trim()) {
|
|
2204
|
+
return res.status(400).json({ error: 'decision required' });
|
|
2205
|
+
}
|
|
2206
|
+
const { storeHumanJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2207
|
+
await initializeValueStore();
|
|
2208
|
+
const j = await storeHumanJudgment({
|
|
2209
|
+
decision: decision.trim(),
|
|
2210
|
+
decision_type: 'approve',
|
|
2211
|
+
reasons: reason ? [reason.trim()] : [],
|
|
2212
|
+
values_derived: [],
|
|
2213
|
+
context: {
|
|
2214
|
+
domain: context?.domain || 'general',
|
|
2215
|
+
complexity: 'moderate',
|
|
2216
|
+
stakes: context?.stakes || 'medium',
|
|
2217
|
+
time_pressure: 'low',
|
|
2218
|
+
},
|
|
2219
|
+
metadata: {
|
|
2220
|
+
source: 'explicit',
|
|
2221
|
+
confidence: 0.8,
|
|
2222
|
+
revisable: true,
|
|
2223
|
+
},
|
|
2224
|
+
});
|
|
2225
|
+
res.json({ ok: true, judgment: j });
|
|
2226
|
+
}
|
|
2227
|
+
catch (err) {
|
|
2228
|
+
console.error('[judgments] POST failed:', err);
|
|
2229
|
+
res.status(500).json({ error: err.message });
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
app.get('/api/judgments', async (_req, res) => {
|
|
2233
|
+
try {
|
|
2234
|
+
const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2235
|
+
await initializeValueStore();
|
|
2236
|
+
const all = await loadAllJudgments();
|
|
2237
|
+
// 新的在前
|
|
2238
|
+
all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
2239
|
+
res.json({ count: all.length, judgments: all });
|
|
2240
|
+
}
|
|
2241
|
+
catch (err) {
|
|
2242
|
+
console.error('[judgments] GET failed:', err);
|
|
2243
|
+
res.status(500).json({ error: err.message });
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
// 导入判断: 接受 { filename, content (base64), context }.
|
|
2247
|
+
// 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
|
|
2248
|
+
// 解析规则:
|
|
2249
|
+
// - .json: 顶层数组 [{decision, reason?, context?}, ...] 或 {judgments: [...]} 或 {items: [...]}
|
|
2250
|
+
// - .yaml/.yml: 期望顶层数组 (用 js-yaml); 不支持复杂结构
|
|
2251
|
+
// - .md/.txt/.html: 每一段 (按空行分隔) 算一条判断, 首行非空 = decision, 整段 = content
|
|
2252
|
+
// 如果首行是 markdown 标题 (# ...) 则去掉 #, 整段去掉首行后作 reason
|
|
2253
|
+
app.post('/api/judgments/import', async (req, res) => {
|
|
2254
|
+
try {
|
|
2255
|
+
const { filename, content, context } = req.body;
|
|
2256
|
+
if (!filename || !content) {
|
|
2257
|
+
return res.status(400).json({ error: 'filename and content (base64) required' });
|
|
2258
|
+
}
|
|
2259
|
+
let raw;
|
|
2260
|
+
try {
|
|
2261
|
+
raw = Buffer.from(content, 'base64').toString('utf-8');
|
|
2262
|
+
}
|
|
2263
|
+
catch {
|
|
2264
|
+
return res.status(400).json({ error: 'content is not valid base64' });
|
|
2265
|
+
}
|
|
2266
|
+
const lower = filename.toLowerCase();
|
|
2267
|
+
let items = [];
|
|
2268
|
+
if (lower.endsWith('.json')) {
|
|
2269
|
+
try {
|
|
2270
|
+
const parsed = JSON.parse(raw);
|
|
2271
|
+
const arr = Array.isArray(parsed) ? parsed
|
|
2272
|
+
: Array.isArray(parsed?.judgments) ? parsed.judgments
|
|
2273
|
+
: Array.isArray(parsed?.items) ? parsed.items
|
|
2274
|
+
: null;
|
|
2275
|
+
if (!arr)
|
|
2276
|
+
return res.status(400).json({ error: 'JSON must be an array, or {judgments:[]}/{items:[]}' });
|
|
2277
|
+
for (const it of arr) {
|
|
2278
|
+
if (it && typeof it.decision === 'string' && it.decision.trim()) {
|
|
2279
|
+
items.push({ decision: it.decision.trim(), reason: it.reason, context: it.context });
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
catch (e) {
|
|
2284
|
+
return res.status(400).json({ error: 'JSON parse failed: ' + e.message });
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
else if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
|
|
2288
|
+
try {
|
|
2289
|
+
const yaml = (await import('js-yaml')).default;
|
|
2290
|
+
const parsed = yaml.load(raw);
|
|
2291
|
+
if (!Array.isArray(parsed))
|
|
2292
|
+
return res.status(400).json({ error: 'YAML must be a top-level array' });
|
|
2293
|
+
for (const it of parsed) {
|
|
2294
|
+
if (it && typeof it.decision === 'string' && it.decision.trim()) {
|
|
2295
|
+
items.push({ decision: it.decision.trim(), reason: it.reason, context: it.context });
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
catch (e) {
|
|
2300
|
+
return res.status(400).json({ error: 'YAML parse failed: ' + e.message });
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
else if (lower.endsWith('.md') || lower.endsWith('.txt') || lower.endsWith('.html') || lower.endsWith('.htm')) {
|
|
2304
|
+
// 通用纯文本: 按空行分段, 每段是一条判断
|
|
2305
|
+
// 对 .html 先剥掉标签, 但保留段落分隔
|
|
2306
|
+
let text = raw;
|
|
2307
|
+
if (lower.endsWith('.html') || lower.endsWith('.htm')) {
|
|
2308
|
+
text = text.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
2309
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
2310
|
+
// 块级标签 -> 双换行 (保留段落分隔)
|
|
2311
|
+
.replace(/<\/?(p|div|h[1-6]|li|tr|br)[^>]*>/gi, '\n\n')
|
|
2312
|
+
.replace(/<[^>]+>/g, ' ')
|
|
2313
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2314
|
+
}
|
|
2315
|
+
const blocks = text.split(/\n\s*\n/).map(b => b.trim()).filter(b => b.length > 0);
|
|
2316
|
+
for (const block of blocks) {
|
|
2317
|
+
const lines = block.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
2318
|
+
if (lines.length === 0)
|
|
2319
|
+
continue;
|
|
2320
|
+
let decision = lines[0];
|
|
2321
|
+
// 如果首行是 markdown 标题, 去掉 # 前缀
|
|
2322
|
+
decision = decision.replace(/^#+\s*/, '');
|
|
2323
|
+
// 如果整段就是一个短句 (没有换行), 直接当 decision
|
|
2324
|
+
const reason = lines.length > 1 ? lines.slice(1).join(' ').trim() || undefined : undefined;
|
|
2325
|
+
if (decision)
|
|
2326
|
+
items.push({ decision, reason });
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
else {
|
|
2330
|
+
return res.status(400).json({ error: 'unsupported file type (use .json .yaml .yml .md .txt .html)' });
|
|
2331
|
+
}
|
|
2332
|
+
if (items.length === 0) {
|
|
2333
|
+
return res.status(400).json({ error: 'no parseable judgments found in file' });
|
|
2334
|
+
}
|
|
2335
|
+
const { storeHumanJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2336
|
+
await initializeValueStore();
|
|
2337
|
+
const imported = [];
|
|
2338
|
+
const errors = [];
|
|
2339
|
+
for (let i = 0; i < items.length; i++) {
|
|
2340
|
+
try {
|
|
2341
|
+
const it = items[i];
|
|
2342
|
+
const j = await storeHumanJudgment({
|
|
2343
|
+
decision: it.decision,
|
|
2344
|
+
decision_type: 'approve',
|
|
2345
|
+
reasons: it.reason ? [String(it.reason)] : [],
|
|
2346
|
+
values_derived: [],
|
|
2347
|
+
context: {
|
|
2348
|
+
domain: it.context?.domain || context?.domain || 'general',
|
|
2349
|
+
complexity: 'moderate',
|
|
2350
|
+
stakes: it.context?.stakes || context?.stakes || 'medium',
|
|
2351
|
+
time_pressure: 'low',
|
|
2352
|
+
},
|
|
2353
|
+
metadata: {
|
|
2354
|
+
source: 'explicit',
|
|
2355
|
+
confidence: 0.8,
|
|
2356
|
+
revisable: true,
|
|
2357
|
+
},
|
|
2358
|
+
});
|
|
2359
|
+
imported.push(j);
|
|
2360
|
+
}
|
|
2361
|
+
catch (e) {
|
|
2362
|
+
errors.push(`#${i + 1} (${items[i].decision.substring(0, 30)}): ${e.message}`);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
res.json({ ok: true, imported: imported.length, failed: errors.length, errors: errors.slice(0, 5), judgments: imported });
|
|
2366
|
+
}
|
|
2367
|
+
catch (err) {
|
|
2368
|
+
console.error('[judgments] import failed:', err);
|
|
2369
|
+
res.status(500).json({ error: err.message });
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
// 修改判断 (手动编辑 decision / reasons / context / values_derived)
|
|
2373
|
+
app.patch('/api/judgments/:id', async (req, res) => {
|
|
2374
|
+
try {
|
|
2375
|
+
const { id } = req.params;
|
|
2376
|
+
const { updateJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2377
|
+
await initializeValueStore();
|
|
2378
|
+
const updated = await updateJudgment(id, req.body || {});
|
|
2379
|
+
if (!updated)
|
|
2380
|
+
return res.status(404).json({ error: 'judgment not found' });
|
|
2381
|
+
res.json({ ok: true, judgment: updated });
|
|
2382
|
+
}
|
|
2383
|
+
catch (err) {
|
|
2384
|
+
console.error('[judgments] PATCH failed:', err);
|
|
2385
|
+
res.status(500).json({ error: err.message });
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
// 删除判断
|
|
2389
|
+
app.delete('/api/judgments/:id', async (req, res) => {
|
|
2390
|
+
try {
|
|
2391
|
+
const { id } = req.params;
|
|
2392
|
+
const { deleteJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2393
|
+
await initializeValueStore();
|
|
2394
|
+
const ok = await deleteJudgment(id);
|
|
2395
|
+
if (!ok)
|
|
2396
|
+
return res.status(404).json({ error: 'judgment not found' });
|
|
2397
|
+
res.json({ ok: true });
|
|
2398
|
+
}
|
|
2399
|
+
catch (err) {
|
|
2400
|
+
console.error('[judgments] DELETE failed:', err);
|
|
2401
|
+
res.status(500).json({ error: err.message });
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
// 批量删除: { ids: ['hv-xxx', ...] } → { ok, deleted, notFound }
|
|
2405
|
+
app.post('/api/judgments/batch-delete', async (req, res) => {
|
|
2406
|
+
try {
|
|
2407
|
+
const ids = (req.body && Array.isArray(req.body.ids)) ? req.body.ids : null;
|
|
2408
|
+
if (!ids)
|
|
2409
|
+
return res.status(400).json({ error: 'ids array required' });
|
|
2410
|
+
const { deleteJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
2411
|
+
await initializeValueStore();
|
|
2412
|
+
const idStrs = ids.filter((x) => typeof x === 'string' && x.length > 0);
|
|
2413
|
+
let deleted = 0;
|
|
2414
|
+
const notFound = [];
|
|
2415
|
+
for (const id of idStrs) {
|
|
2416
|
+
const ok = await deleteJudgment(id);
|
|
2417
|
+
if (ok)
|
|
2418
|
+
deleted++;
|
|
2419
|
+
else
|
|
2420
|
+
notFound.push(id);
|
|
2421
|
+
}
|
|
2422
|
+
res.json({ ok: true, deleted, notFound });
|
|
2423
|
+
}
|
|
2424
|
+
catch (err) {
|
|
2425
|
+
console.error('[judgments] batch-delete failed:', err);
|
|
2426
|
+
res.status(500).json({ error: err.message });
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
// AI 自动委派: 根据新判断的 capability / context 找最匹配的远端 agent, 委派任务
|
|
2430
|
+
// 由前端在 POST /api/judgments 成功后调用 (fire-and-forget)
|
|
2431
|
+
// 出参: { matched, targetAgent, response | skipped, reason }
|
|
2432
|
+
app.post('/api/judgments/auto-delegate', async (req, res) => {
|
|
2433
|
+
try {
|
|
2434
|
+
const { judgmentId, capability, instruction } = req.body;
|
|
2435
|
+
if (!judgmentId && !capability) {
|
|
2436
|
+
return res.status(400).json({ error: 'judgmentId or capability required' });
|
|
2437
|
+
}
|
|
2438
|
+
const cap = capability || 'general';
|
|
2439
|
+
// 用 agent-manifest-protocol 里的 pickAgent (内存) — 走本节点已经缓存的远端 manifest
|
|
2440
|
+
const manifestMod = await import('../agents/agent-manifest-protocol.js');
|
|
2441
|
+
const picked = manifestMod.pickAgent(cap);
|
|
2442
|
+
if (!picked) {
|
|
2443
|
+
return res.json({ ok: true, matched: false, reason: 'no remote agent matches capability' });
|
|
2444
|
+
}
|
|
2445
|
+
// 命中后, 用 iroh delegate transport 真正发过去
|
|
2446
|
+
// 注: irohDelegateTransport.sendToNode 走的是 sendToNode(publicKey, frame, timeoutMs)
|
|
2447
|
+
// irohTransport 的 sendMessage 不等回包, 所以委托是 fire-and-forget
|
|
2448
|
+
// 想等回包需要新接口. 这里先把 "找得到目标 + 发送成功" 作为成功.
|
|
2449
|
+
// TODO: 接入 requestResponse 等待远端 agent_response
|
|
2450
|
+
try {
|
|
2451
|
+
const idMod = await import('../network/iroh-integration.js');
|
|
2452
|
+
const integ = idMod.getIrohIntegration();
|
|
2453
|
+
if (!integ || !integ.getNodeId()) {
|
|
2454
|
+
return res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, reason: 'iroh not initialized' });
|
|
2455
|
+
}
|
|
2456
|
+
// 用 pickAgent 选出来的 agent 关联的 irohNodeId (有的话), 没有就跳到本地自处理
|
|
2457
|
+
const targetIrohNodeId = picked.agent.irohNodeId;
|
|
2458
|
+
if (!targetIrohNodeId) {
|
|
2459
|
+
return res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, reason: 'target agent has no irohNodeId (peer identity not bound)' });
|
|
2460
|
+
}
|
|
2461
|
+
const ok = await integ.sendTo(targetIrohNodeId, 'agent_delegate', new TextEncoder().encode(JSON.stringify({
|
|
2462
|
+
type: 'agent_delegate',
|
|
2463
|
+
payload: {
|
|
2464
|
+
capability: cap,
|
|
2465
|
+
instruction: instruction || `请执行我的判断: ${judgmentId}`,
|
|
2466
|
+
fromAgentId: 'local-judgment',
|
|
2467
|
+
},
|
|
2468
|
+
ts: Date.now(),
|
|
2469
|
+
fromDid: '',
|
|
2470
|
+
})));
|
|
2471
|
+
res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: ok });
|
|
2472
|
+
}
|
|
2473
|
+
catch (e) {
|
|
2474
|
+
res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, error: e.message });
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
catch (err) {
|
|
2478
|
+
console.error('[judgments] auto-delegate failed:', err);
|
|
2479
|
+
res.status(500).json({ error: err.message });
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
// (判断的 UI 已合并到主页面 header 的盾牌按钮 + modal, 不再走独立路由)
|
|
2180
2483
|
// 启动看门狗监控
|
|
2181
2484
|
if (watchdog) {
|
|
2182
2485
|
// level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
|
package/dist/web/style.css
CHANGED
|
@@ -575,6 +575,13 @@ body {
|
|
|
575
575
|
transform: rotate(90deg);
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/* ⚡ 自动工具徽章 + 当前会话名: 折叠时不显示, 只在展开时可见.
|
|
579
|
+
配置 (钱包/工具) 按钮保留在行上, 用户随时能进 modal. */
|
|
580
|
+
.agent-group:not(.expanded) .agent-tools-badge,
|
|
581
|
+
.agent-group:not(.expanded) .agent-current-session {
|
|
582
|
+
display: none !important;
|
|
583
|
+
}
|
|
584
|
+
|
|
578
585
|
.agent-new-session {
|
|
579
586
|
/* 已废弃: 新建会话按钮迁移到 session 列表顶部, 见 .session-new-item */
|
|
580
587
|
display: none;
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manifest-protocol - 节点握手后立刻互换"我有哪些 agent + capabilities"
|
|
3
|
+
*
|
|
4
|
+
* 核心目的:实现 "建联一次,访问对方所有智能体"。
|
|
5
|
+
* - 节点连上 (Hyperswarm 主题 / iroh) 后立刻发 'manifest_request'
|
|
6
|
+
* - 对端回 'manifest_payload',写入本地 agentRegistry
|
|
7
|
+
* - 之后任何指令可以 pickAgent(capability, ownerDid) → 直接委派
|
|
8
|
+
*
|
|
9
|
+
* 协议消息 (Hyperswarm 字符串帧):
|
|
10
|
+
* manifest_request: { }
|
|
11
|
+
* manifest_payload: { ownerName, ownerPublicKey, agents:[{id,name,capabilities,status}], publishedAt }
|
|
12
|
+
*
|
|
13
|
+
* 本文件不绑定 transport — 只提供 build/parse/dispatch 帮助函数。
|
|
14
|
+
* 调用方在自己 transport 上挂 onMessage('manifest_request', ...) 和 onMessage('manifest_payload', ...)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface AgentManifestEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
capabilities: string[];
|
|
21
|
+
status: 'active' | 'idle' | 'busy' | 'creating' | 'terminated';
|
|
22
|
+
// 可选 — 若对方把这个 agent 关联到具体子 P2P 端点
|
|
23
|
+
peerId?: string;
|
|
24
|
+
irohNodeId?: string;
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
cid?: string;
|
|
27
|
+
ipnsName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentManifest {
|
|
31
|
+
ownerName: string;
|
|
32
|
+
ownerPublicKey: string;
|
|
33
|
+
agents: AgentManifestEntry[];
|
|
34
|
+
publishedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============== 帧构造 ==============
|
|
38
|
+
export function buildManifestRequest(): string {
|
|
39
|
+
return JSON.stringify({ type: 'manifest_request', payload: {}, ts: Date.now(), fromDid: '' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildManifestPayload(manifest: AgentManifest): string {
|
|
43
|
+
return JSON.stringify({ type: 'manifest_payload', payload: manifest, ts: Date.now(), fromDid: '' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildAgentDelegateRequest(opts: {
|
|
47
|
+
capability: string;
|
|
48
|
+
docPath?: string;
|
|
49
|
+
docContent?: string;
|
|
50
|
+
instruction: string;
|
|
51
|
+
fromAgentId: string;
|
|
52
|
+
}): string {
|
|
53
|
+
return JSON.stringify({ type: 'agent_delegate', payload: opts, ts: Date.now(), fromDid: '' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildAgentResponse(opts: {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
delegatedTo: string;
|
|
59
|
+
resultCid?: string;
|
|
60
|
+
summary: string;
|
|
61
|
+
error?: string;
|
|
62
|
+
}): string {
|
|
63
|
+
return JSON.stringify({ type: 'agent_response', payload: opts, ts: Date.now(), fromDid: '' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============== 帧解析 ==============
|
|
67
|
+
export function parseFrame(text: string): { type: string; payload: any; ts: number; fromDid: string } | null {
|
|
68
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============== 本地 manifest registry ==============
|
|
72
|
+
const localManifest: AgentManifest = {
|
|
73
|
+
ownerName: '',
|
|
74
|
+
ownerPublicKey: '',
|
|
75
|
+
agents: [],
|
|
76
|
+
publishedAt: 0,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function setLocalManifest(m: Partial<AgentManifest>) {
|
|
80
|
+
Object.assign(localManifest, m, { publishedAt: Date.now() });
|
|
81
|
+
return localManifest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getLocalManifest(): AgentManifest {
|
|
85
|
+
return localManifest;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function addLocalAgent(agent: AgentManifestEntry) {
|
|
89
|
+
const idx = localManifest.agents.findIndex((a) => a.id === agent.id);
|
|
90
|
+
if (idx >= 0) localManifest.agents[idx] = agent;
|
|
91
|
+
else localManifest.agents.push(agent);
|
|
92
|
+
localManifest.publishedAt = Date.now();
|
|
93
|
+
return localManifest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============== 远端 manifest 缓存 ==============
|
|
97
|
+
const remoteManifests: Map<string, AgentManifest> = new Map(); // key = ownerPublicKey
|
|
98
|
+
|
|
99
|
+
export function cacheRemoteManifest(m: AgentManifest) {
|
|
100
|
+
if (m.ownerPublicKey) remoteManifests.set(m.ownerPublicKey, m);
|
|
101
|
+
return m;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getRemoteManifests(): AgentManifest[] {
|
|
105
|
+
return Array.from(remoteManifests.values());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function pickAgent(capability: string, ownerPublicKey?: string): { agent: AgentManifestEntry; owner: AgentManifest } | null {
|
|
109
|
+
const owners = ownerPublicKey
|
|
110
|
+
? [remoteManifests.get(ownerPublicKey)].filter(Boolean) as AgentManifest[]
|
|
111
|
+
: getRemoteManifests();
|
|
112
|
+
for (const owner of owners) {
|
|
113
|
+
const a = owner.agents.find((x) => x.capabilities.includes(capability) && x.status === 'active');
|
|
114
|
+
if (a) return { agent: a, owner };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iroh-secret - 把 iroh secretKey 落 ~/.bolloon/iroh-secret.json
|
|
3
|
+
*
|
|
4
|
+
* iroh 默认每次启动生成新节点 ID;落盘后可实现"跨重启稳定 nodeId"。
|
|
5
|
+
* 对应 Issue: "建联一次访问所有智能体"需要稳定标识, 不然对方缓存的 nodeId 失效
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
|
|
13
|
+
const SECRET_DIR = path.join(os.homedir(), '.bolloon');
|
|
14
|
+
|
|
15
|
+
export function loadOrCreateIrohSecret(role: string = 'default'): { secretKey: Uint8Array; createdAt: string; reused: boolean } {
|
|
16
|
+
if (!fs.existsSync(SECRET_DIR)) fs.mkdirSync(SECRET_DIR, { recursive: true });
|
|
17
|
+
const fp = path.join(SECRET_DIR, `iroh-secret-${role}.json`);
|
|
18
|
+
if (fs.existsSync(fp)) {
|
|
19
|
+
const j = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
20
|
+
return { secretKey: Buffer.from(j.secretKey, 'hex'), createdAt: j.createdAt, reused: true };
|
|
21
|
+
}
|
|
22
|
+
const sk = crypto.randomBytes(32);
|
|
23
|
+
const createdAt = new Date().toISOString();
|
|
24
|
+
fs.writeFileSync(fp, JSON.stringify({ secretKey: sk.toString('hex'), createdAt }, null, 2), { mode: 0o600 });
|
|
25
|
+
// chmod in case the file already existed and umask produced looser perms
|
|
26
|
+
try { fs.chmodSync(fp, 0o600); } catch {}
|
|
27
|
+
return { secretKey: sk, createdAt, reused: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function irohSecretFilePath(role: string = 'default'): string {
|
|
31
|
+
return path.join(SECRET_DIR, `iroh-secret-${role}.json`);
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
|
|
|
22
22
|
import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
23
23
|
import { BollharnessIntegration, createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
24
24
|
import * as readline from 'readline';
|
|
25
|
-
|
|
25
|
+
// 启动时自动检查更新已禁用 (改用 --update-check / --update-now 显式触发)
|
|
26
|
+
|
|
26
27
|
|
|
27
28
|
const RESET = '\x1b[0m';
|
|
28
29
|
const BOLD = '\x1b[1m';
|
|
@@ -1516,13 +1517,10 @@ async function main() {
|
|
|
1516
1517
|
process.exit(0);
|
|
1517
1518
|
}
|
|
1518
1519
|
|
|
1519
|
-
//
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
// 更新完成后退出,让用户重新启动
|
|
1524
|
-
process.exit(0);
|
|
1525
|
-
}
|
|
1520
|
+
// 自动更新已禁用: 启动时不再自动检查/安装更新.
|
|
1521
|
+
// 想手动检查: bolloon --update-check
|
|
1522
|
+
// 想手动更新: bolloon --update-now [package]
|
|
1523
|
+
// 想完全屏蔽 (CI / sandbox): BOLLOON_SKIP_UPDATE=true
|
|
1526
1524
|
|
|
1527
1525
|
const mode = args.web ? 'web' : 'cli';
|
|
1528
1526
|
const isNonInteractive = !!(args.tool || args.prompt);
|