@bolloon/bolloon-agent 0.1.23 → 0.1.25
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 +570 -10
- package/dist/web/index.html +74 -0
- package/dist/web/iroh-delegate-transport.js +125 -0
- package/dist/web/server.js +385 -7
- 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 +570 -10
- package/src/web/index.html +74 -0
- package/src/web/iroh-delegate-transport.ts +139 -0
- package/src/web/server.ts +410 -7
- package/src/web/style.css +7 -0
package/src/web/server.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { initMinimax, getMinimax } from '../constraints/index.js';
|
|
|
19
19
|
import { createAgentSession, type AgentSession, type StreamCallback, type StreamEvent } from '../agents/pi-sdk.js';
|
|
20
20
|
import { llmConfigStore, type ModelProvider, PROVIDER_INFO } from '../llm/config-store.js';
|
|
21
21
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
22
|
+
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
23
|
+
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
22
24
|
import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
23
25
|
|
|
24
26
|
// 前端资源路径:在打包后会通过 CommonJS require 加载,使用 import.meta.url
|
|
@@ -70,6 +72,8 @@ interface Channel {
|
|
|
70
72
|
updatedAt: string;
|
|
71
73
|
currentSessionId?: string;
|
|
72
74
|
sessions?: SessionSummary[];
|
|
75
|
+
/** 用户在盾牌里手动绑定的判断力 (LLM 跑 channel 时会注入). 默认 []. */
|
|
76
|
+
bound_judgment_ids?: string[];
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
interface SessionSummary {
|
|
@@ -381,6 +385,67 @@ let sseClients: Set<SSEClient> = new Set();
|
|
|
381
385
|
let channelSessions: Map<string, AgentSession> = new Map(); // key: channelId
|
|
382
386
|
let sessionMessages: Map<string, any[]> = new Map(); // key: channelId + sessionId
|
|
383
387
|
|
|
388
|
+
/**
|
|
389
|
+
* v3 重做: 构造 channel 的两路 judgment prompt 片段
|
|
390
|
+
* 路 1: 用户在盾牌里手动绑定的 judgment (channel.bound_judgment_ids)
|
|
391
|
+
* 路 2: 全局 judgment 列表 (供 LLM 在主调用中按需挑选, 写入回复)
|
|
392
|
+
* 返回 "" 表示完全没数据; 否则返回完整 "[系统上下文] ..." 块 (含尾部换行)
|
|
393
|
+
* 失败非致命 — 任何异常都返回空串, 保证 LLM 调用不被阻塞
|
|
394
|
+
*/
|
|
395
|
+
async function buildJudgmentHint(
|
|
396
|
+
channel: Channel | undefined | null,
|
|
397
|
+
channelIdForLog: string
|
|
398
|
+
): Promise<string> {
|
|
399
|
+
try {
|
|
400
|
+
const { loadAllJudgments, initializeValueStore } = await import(
|
|
401
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
402
|
+
);
|
|
403
|
+
await initializeValueStore();
|
|
404
|
+
const allJudgments = await loadAllJudgments();
|
|
405
|
+
if (allJudgments.length === 0) return '';
|
|
406
|
+
|
|
407
|
+
const boundIds = new Set(
|
|
408
|
+
channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []
|
|
409
|
+
);
|
|
410
|
+
const bound = allJudgments.filter(j => j.id !== undefined && boundIds.has(j.id));
|
|
411
|
+
const others = allJudgments.filter(j => j.id !== undefined && !boundIds.has(j.id));
|
|
412
|
+
|
|
413
|
+
let hint = '';
|
|
414
|
+
|
|
415
|
+
// 路 1: 用户手动绑定的 judgment — 硬约束, 必须遵循
|
|
416
|
+
if (bound.length > 0) {
|
|
417
|
+
hint += `[系统上下文] 此 channel 用户绑定了 ${bound.length} 条判断力, 必须严格遵循:\n`;
|
|
418
|
+
for (const j of bound) {
|
|
419
|
+
const decision = (j.decision || '').toString().slice(0, 200);
|
|
420
|
+
const reasonList = Array.isArray(j.reasons) ? j.reasons : [];
|
|
421
|
+
const reasonText = reasonList.length > 0
|
|
422
|
+
? ` (理由: ${reasonList.join('; ').slice(0, 100)})`
|
|
423
|
+
: '';
|
|
424
|
+
hint += `- ${decision}${reasonText}\n`;
|
|
425
|
+
}
|
|
426
|
+
hint += '\n';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 路 2: 全局 judgment 候选池 — 软参考, LLM 自己挑
|
|
430
|
+
if (others.length > 0) {
|
|
431
|
+
hint += `[系统上下文] 候选判断力 (用户未明确绑定, 你可以按相关性自主选择参考):\n`;
|
|
432
|
+
for (const j of others) {
|
|
433
|
+
const decision = (j.decision || '').toString().slice(0, 120);
|
|
434
|
+
hint += `- [id=${j.id}] ${decision}\n`;
|
|
435
|
+
}
|
|
436
|
+
hint += `\n[系统上下文] 如果你的回复参考了某条候选判断力, 请在回复中自然提及 "我参考了你的判断: <decision 简述>" 即可, 无需复述 id.\n\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log(
|
|
440
|
+
`[v3] channel ${channelIdForLog} 注入: 绑定 ${bound.length} 条, 候选 ${others.length} 条`
|
|
441
|
+
);
|
|
442
|
+
return hint;
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.error(`[v3] 加载判断力失败 (非致命):`, (err as Error).message);
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
384
449
|
async function getAgentForChannel(
|
|
385
450
|
channelId: string,
|
|
386
451
|
channelDid?: string,
|
|
@@ -452,7 +517,14 @@ async function getAgentForChannel(
|
|
|
452
517
|
return session;
|
|
453
518
|
}
|
|
454
519
|
|
|
455
|
-
export
|
|
520
|
+
export interface CreateWebServerOptions {
|
|
521
|
+
selfImprove?: boolean;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let selfImproveEnabled = false;
|
|
525
|
+
|
|
526
|
+
export async function createWebServer(port: number = 3000, options: CreateWebServerOptions = {}) {
|
|
527
|
+
selfImproveEnabled = options.selfImprove ?? false;
|
|
456
528
|
// 防止 P2P DHT 超时等错误导致进程崩溃
|
|
457
529
|
process.on('unhandledRejection', (reason, promise) => {
|
|
458
530
|
console.error('[警告] 未处理的 Promise 拒绝:', reason);
|
|
@@ -631,6 +703,8 @@ export async function createWebServer(port: number = 3000) {
|
|
|
631
703
|
// (line ~638) 与这里外层的 const channel 形成 shadowing 让 TS 误报"使用前未声明"
|
|
632
704
|
const boundWalletAddress = channel?.walletAddress;
|
|
633
705
|
const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
|
|
706
|
+
// 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
|
|
707
|
+
const channelForJudgment = channel;
|
|
634
708
|
|
|
635
709
|
try {
|
|
636
710
|
const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
@@ -666,6 +740,12 @@ export async function createWebServer(port: number = 3000) {
|
|
|
666
740
|
} else {
|
|
667
741
|
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
668
742
|
}
|
|
743
|
+
|
|
744
|
+
// v3: 注入 channel 绑定的判断力 (judgment_ids)
|
|
745
|
+
// 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
|
|
746
|
+
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
747
|
+
if (judgmentHint) contextHint += judgmentHint;
|
|
748
|
+
|
|
669
749
|
if (contextHint) contextHint += '\n';
|
|
670
750
|
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
671
751
|
|
|
@@ -826,8 +906,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
826
906
|
|
|
827
907
|
app.post('/channels', async (req, res) => {
|
|
828
908
|
try {
|
|
829
|
-
const { name, agentId, walletAddress, autoInvokeTools } = req.body;
|
|
830
|
-
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}`);
|
|
909
|
+
const { name, agentId, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
910
|
+
console.log(`[创建频道] 收到请求: name=${name}, agentId=${agentId}, wallet=${walletAddress ? 'yes' : 'no'}, boundJudgments=${Array.isArray(bound_judgment_ids) ? bound_judgment_ids.length : 0}`);
|
|
831
911
|
if (!name || !agentId) {
|
|
832
912
|
return res.status(400).json({ error: 'name and agentId required' });
|
|
833
913
|
}
|
|
@@ -837,6 +917,11 @@ app.get('/channels', async (_req, res) => {
|
|
|
837
917
|
// 校验钱包地址格式 (粗校验: 0x + 40 hex / Solana base58 / Sui 0x+64)
|
|
838
918
|
const validWallet = isValidWalletAddress(walletAddress);
|
|
839
919
|
|
|
920
|
+
// 过滤 bound_judgment_ids: 只保留 string
|
|
921
|
+
const safeBoundIds = Array.isArray(bound_judgment_ids)
|
|
922
|
+
? bound_judgment_ids.filter((x: unknown) => typeof x === 'string' && (x as string).length > 0)
|
|
923
|
+
: [];
|
|
924
|
+
|
|
840
925
|
// 先创建频道(不阻塞等待 DID 生成)
|
|
841
926
|
const channel: Channel = {
|
|
842
927
|
id,
|
|
@@ -848,6 +933,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
848
933
|
walletAddress: validWallet || undefined,
|
|
849
934
|
walletRegisteredAt: validWallet ? new Date().toISOString() : undefined,
|
|
850
935
|
autoInvokeTools: autoInvokeTools !== false, // 默认 true
|
|
936
|
+
bound_judgment_ids: safeBoundIds,
|
|
851
937
|
sessions: [{
|
|
852
938
|
id: `sess_${Date.now()}`,
|
|
853
939
|
createdAt: new Date().toISOString(),
|
|
@@ -1026,7 +1112,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
1026
1112
|
app.patch('/channels/:channelId', async (req, res) => {
|
|
1027
1113
|
try {
|
|
1028
1114
|
const { channelId } = req.params;
|
|
1029
|
-
const { name, walletAddress, autoInvokeTools } = req.body;
|
|
1115
|
+
const { name, walletAddress, autoInvokeTools, bound_judgment_ids } = req.body;
|
|
1030
1116
|
const channels = await loadChannels();
|
|
1031
1117
|
const channel = channels.find(c => c.id === channelId);
|
|
1032
1118
|
if (!channel) {
|
|
@@ -1052,6 +1138,19 @@ app.get('/channels', async (_req, res) => {
|
|
|
1052
1138
|
if (typeof autoInvokeTools === 'boolean') {
|
|
1053
1139
|
channel.autoInvokeTools = autoInvokeTools;
|
|
1054
1140
|
}
|
|
1141
|
+
// bound_judgment_ids: 允许数组(替换)/null(清空)/undefined(不改)
|
|
1142
|
+
if (bound_judgment_ids !== undefined) {
|
|
1143
|
+
if (bound_judgment_ids === null) {
|
|
1144
|
+
channel.bound_judgment_ids = [];
|
|
1145
|
+
} else if (Array.isArray(bound_judgment_ids)) {
|
|
1146
|
+
channel.bound_judgment_ids = bound_judgment_ids.filter(
|
|
1147
|
+
(x: unknown) => typeof x === 'string' && (x as string).length > 0
|
|
1148
|
+
);
|
|
1149
|
+
} else {
|
|
1150
|
+
return res.status(400).json({ error: 'bound_judgment_ids must be array or null' });
|
|
1151
|
+
}
|
|
1152
|
+
console.log(`[Channel ${channelId}] 绑定判断力: ${channel.bound_judgment_ids.length} 条`);
|
|
1153
|
+
}
|
|
1055
1154
|
channel.updatedAt = new Date().toISOString();
|
|
1056
1155
|
await saveChannels(channels);
|
|
1057
1156
|
res.json(channel);
|
|
@@ -1216,8 +1315,9 @@ app.get('/channels', async (_req, res) => {
|
|
|
1216
1315
|
}
|
|
1217
1316
|
};
|
|
1218
1317
|
|
|
1219
|
-
// 重新生成时只发送用户消息
|
|
1220
|
-
|
|
1318
|
+
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
1319
|
+
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
1320
|
+
fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
|
|
1221
1321
|
|
|
1222
1322
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
1223
1323
|
|
|
@@ -1733,6 +1833,17 @@ app.get('/channels', async (_req, res) => {
|
|
|
1733
1833
|
};
|
|
1734
1834
|
irohInitialized = true;
|
|
1735
1835
|
|
|
1836
|
+
// 挂载 agent-delegate app (manifest 协议 + agent_delegate)
|
|
1837
|
+
// 必须在 irohInitialized 之后挂, 因为适配器要监听 irohTransport.onMessage
|
|
1838
|
+
try {
|
|
1839
|
+
const delegateTransport = createIrohDelegateTransport({ verbose: true });
|
|
1840
|
+
const delegateApp = createAgentDelegateApp(delegateTransport);
|
|
1841
|
+
app.use('/api/agent', delegateApp);
|
|
1842
|
+
console.log('[iroh API] agent-delegate app 已挂载到 /api/agent');
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
console.error('[iroh API] 挂载 agent-delegate app 失败:', e);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1736
1847
|
// 设置消息处理
|
|
1737
1848
|
irohTransport.onMessage('chat', (msg) => {
|
|
1738
1849
|
const content = new TextDecoder().decode(msg.payload);
|
|
@@ -2431,7 +2542,8 @@ app.get('/channels', async (_req, res) => {
|
|
|
2431
2542
|
try {
|
|
2432
2543
|
const { createHealthMonitor, createWatchdog } = await import('../heartbeat/index.js');
|
|
2433
2544
|
healthMonitor = createHealthMonitor();
|
|
2434
|
-
watchdog
|
|
2545
|
+
// 把 watchdog 静默阈值拉到 30 分钟, 避免开发期 / 用户空闲时被误杀
|
|
2546
|
+
watchdog = createWatchdog({ silentThresholdMs: 30 * 60 * 1000 });
|
|
2435
2547
|
|
|
2436
2548
|
console.log('[24h] Heartbeat modules loaded');
|
|
2437
2549
|
} catch (err) {
|
|
@@ -2489,6 +2601,297 @@ app.get('/channels', async (_req, res) => {
|
|
|
2489
2601
|
}
|
|
2490
2602
|
});
|
|
2491
2603
|
|
|
2604
|
+
// ==================== Judgments (v1 核心: 让我能记录判断) ====================
|
|
2605
|
+
// POST /api/judgments — 记录一个判断
|
|
2606
|
+
// GET /api/judgments — 列出所有判断 (新→旧)
|
|
2607
|
+
// 存储: ~/.bolloon/human-values/judgments.json (human-value-store)
|
|
2608
|
+
// 极简版: 只记录 decision + reason; 其它字段可选
|
|
2609
|
+
app.post('/api/judgments', async (req, res) => {
|
|
2610
|
+
try {
|
|
2611
|
+
const { decision, reason, context } = req.body as {
|
|
2612
|
+
decision?: string; reason?: string; context?: { domain?: string; stakes?: string };
|
|
2613
|
+
};
|
|
2614
|
+
if (!decision || typeof decision !== 'string' || !decision.trim()) {
|
|
2615
|
+
return res.status(400).json({ error: 'decision required' });
|
|
2616
|
+
}
|
|
2617
|
+
const { storeHumanJudgment, initializeValueStore } = await import(
|
|
2618
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2619
|
+
);
|
|
2620
|
+
await initializeValueStore();
|
|
2621
|
+
const j = await storeHumanJudgment({
|
|
2622
|
+
decision: decision.trim(),
|
|
2623
|
+
decision_type: 'approve',
|
|
2624
|
+
reasons: reason ? [reason.trim()] : [],
|
|
2625
|
+
values_derived: [],
|
|
2626
|
+
context: {
|
|
2627
|
+
domain: context?.domain || 'general',
|
|
2628
|
+
complexity: 'moderate',
|
|
2629
|
+
stakes: (context?.stakes as 'low' | 'medium' | 'high' | 'critical') || 'medium',
|
|
2630
|
+
time_pressure: 'low',
|
|
2631
|
+
},
|
|
2632
|
+
metadata: {
|
|
2633
|
+
source: 'explicit',
|
|
2634
|
+
confidence: 0.8,
|
|
2635
|
+
revisable: true,
|
|
2636
|
+
},
|
|
2637
|
+
});
|
|
2638
|
+
res.json({ ok: true, judgment: j });
|
|
2639
|
+
} catch (err: any) {
|
|
2640
|
+
console.error('[judgments] POST failed:', err);
|
|
2641
|
+
res.status(500).json({ error: err.message });
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
app.get('/api/judgments', async (_req, res) => {
|
|
2646
|
+
try {
|
|
2647
|
+
const { loadAllJudgments, initializeValueStore } = await import(
|
|
2648
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2649
|
+
);
|
|
2650
|
+
await initializeValueStore();
|
|
2651
|
+
const all = await loadAllJudgments();
|
|
2652
|
+
// 新的在前
|
|
2653
|
+
all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
|
2654
|
+
res.json({ count: all.length, judgments: all });
|
|
2655
|
+
} catch (err: any) {
|
|
2656
|
+
console.error('[judgments] GET failed:', err);
|
|
2657
|
+
res.status(500).json({ error: err.message });
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
|
|
2661
|
+
// 导入判断: 接受 { filename, content (base64), context }.
|
|
2662
|
+
// 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
|
|
2663
|
+
// 解析规则:
|
|
2664
|
+
// - .json: 顶层数组 [{decision, reason?, context?}, ...] 或 {judgments: [...]} 或 {items: [...]}
|
|
2665
|
+
// - .yaml/.yml: 期望顶层数组 (用 js-yaml); 不支持复杂结构
|
|
2666
|
+
// - .md/.txt/.html: 每一段 (按空行分隔) 算一条判断, 首行非空 = decision, 整段 = content
|
|
2667
|
+
// 如果首行是 markdown 标题 (# ...) 则去掉 #, 整段去掉首行后作 reason
|
|
2668
|
+
app.post('/api/judgments/import', async (req, res) => {
|
|
2669
|
+
try {
|
|
2670
|
+
const { filename, content, context } = req.body as {
|
|
2671
|
+
filename?: string; content?: string; context?: { domain?: string; stakes?: string };
|
|
2672
|
+
};
|
|
2673
|
+
if (!filename || !content) {
|
|
2674
|
+
return res.status(400).json({ error: 'filename and content (base64) required' });
|
|
2675
|
+
}
|
|
2676
|
+
let raw: string;
|
|
2677
|
+
try { raw = Buffer.from(content, 'base64').toString('utf-8'); }
|
|
2678
|
+
catch { return res.status(400).json({ error: 'content is not valid base64' }); }
|
|
2679
|
+
|
|
2680
|
+
const lower = filename.toLowerCase();
|
|
2681
|
+
let items: Array<{ decision: string; reason?: string; context?: any }> = [];
|
|
2682
|
+
if (lower.endsWith('.json')) {
|
|
2683
|
+
try {
|
|
2684
|
+
const parsed = JSON.parse(raw);
|
|
2685
|
+
const arr = Array.isArray(parsed) ? parsed
|
|
2686
|
+
: Array.isArray(parsed?.judgments) ? parsed.judgments
|
|
2687
|
+
: Array.isArray(parsed?.items) ? parsed.items
|
|
2688
|
+
: null;
|
|
2689
|
+
if (!arr) return res.status(400).json({ error: 'JSON must be an array, or {judgments:[]}/{items:[]}' });
|
|
2690
|
+
for (const it of arr) {
|
|
2691
|
+
if (it && typeof it.decision === 'string' && it.decision.trim()) {
|
|
2692
|
+
items.push({ decision: it.decision.trim(), reason: it.reason, context: it.context });
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
} catch (e: any) {
|
|
2696
|
+
return res.status(400).json({ error: 'JSON parse failed: ' + e.message });
|
|
2697
|
+
}
|
|
2698
|
+
} else if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
|
|
2699
|
+
try {
|
|
2700
|
+
const yaml = (await import('js-yaml')).default;
|
|
2701
|
+
const parsed = yaml.load(raw);
|
|
2702
|
+
if (!Array.isArray(parsed)) return res.status(400).json({ error: 'YAML must be a top-level array' });
|
|
2703
|
+
for (const it of parsed) {
|
|
2704
|
+
if (it && typeof it.decision === 'string' && it.decision.trim()) {
|
|
2705
|
+
items.push({ decision: it.decision.trim(), reason: it.reason, context: it.context });
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
} catch (e: any) {
|
|
2709
|
+
return res.status(400).json({ error: 'YAML parse failed: ' + e.message });
|
|
2710
|
+
}
|
|
2711
|
+
} else if (lower.endsWith('.md') || lower.endsWith('.txt') || lower.endsWith('.html') || lower.endsWith('.htm')) {
|
|
2712
|
+
// 通用纯文本: 按空行分段, 每段是一条判断
|
|
2713
|
+
// 对 .html 先剥掉标签, 但保留段落分隔
|
|
2714
|
+
let text = raw;
|
|
2715
|
+
if (lower.endsWith('.html') || lower.endsWith('.htm')) {
|
|
2716
|
+
text = text.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
2717
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
2718
|
+
// 块级标签 -> 双换行 (保留段落分隔)
|
|
2719
|
+
.replace(/<\/?(p|div|h[1-6]|li|tr|br)[^>]*>/gi, '\n\n')
|
|
2720
|
+
.replace(/<[^>]+>/g, ' ')
|
|
2721
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2722
|
+
}
|
|
2723
|
+
const blocks = text.split(/\n\s*\n/).map(b => b.trim()).filter(b => b.length > 0);
|
|
2724
|
+
for (const block of blocks) {
|
|
2725
|
+
const lines = block.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
2726
|
+
if (lines.length === 0) continue;
|
|
2727
|
+
let decision = lines[0];
|
|
2728
|
+
// 如果首行是 markdown 标题, 去掉 # 前缀
|
|
2729
|
+
decision = decision.replace(/^#+\s*/, '');
|
|
2730
|
+
// 如果整段就是一个短句 (没有换行), 直接当 decision
|
|
2731
|
+
const reason = lines.length > 1 ? lines.slice(1).join(' ').trim() || undefined : undefined;
|
|
2732
|
+
if (decision) items.push({ decision, reason });
|
|
2733
|
+
}
|
|
2734
|
+
} else {
|
|
2735
|
+
return res.status(400).json({ error: 'unsupported file type (use .json .yaml .yml .md .txt .html)' });
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (items.length === 0) {
|
|
2739
|
+
return res.status(400).json({ error: 'no parseable judgments found in file' });
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
const { storeHumanJudgment, initializeValueStore } = await import(
|
|
2743
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2744
|
+
);
|
|
2745
|
+
await initializeValueStore();
|
|
2746
|
+
|
|
2747
|
+
const imported: any[] = [];
|
|
2748
|
+
const errors: string[] = [];
|
|
2749
|
+
for (let i = 0; i < items.length; i++) {
|
|
2750
|
+
try {
|
|
2751
|
+
const it = items[i];
|
|
2752
|
+
const j = await storeHumanJudgment({
|
|
2753
|
+
decision: it.decision,
|
|
2754
|
+
decision_type: 'approve',
|
|
2755
|
+
reasons: it.reason ? [String(it.reason)] : [],
|
|
2756
|
+
values_derived: [],
|
|
2757
|
+
context: {
|
|
2758
|
+
domain: it.context?.domain || context?.domain || 'general',
|
|
2759
|
+
complexity: 'moderate',
|
|
2760
|
+
stakes: (it.context?.stakes as any) || context?.stakes || 'medium',
|
|
2761
|
+
time_pressure: 'low',
|
|
2762
|
+
},
|
|
2763
|
+
metadata: {
|
|
2764
|
+
source: 'explicit',
|
|
2765
|
+
confidence: 0.8,
|
|
2766
|
+
revisable: true,
|
|
2767
|
+
},
|
|
2768
|
+
});
|
|
2769
|
+
imported.push(j);
|
|
2770
|
+
} catch (e: any) {
|
|
2771
|
+
errors.push(`#${i + 1} (${items[i].decision.substring(0, 30)}): ${e.message}`);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
res.json({ ok: true, imported: imported.length, failed: errors.length, errors: errors.slice(0, 5), judgments: imported });
|
|
2776
|
+
} catch (err: any) {
|
|
2777
|
+
console.error('[judgments] import failed:', err);
|
|
2778
|
+
res.status(500).json({ error: err.message });
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
// 修改判断 (手动编辑 decision / reasons / context / values_derived)
|
|
2783
|
+
app.patch('/api/judgments/:id', async (req, res) => {
|
|
2784
|
+
try {
|
|
2785
|
+
const { id } = req.params;
|
|
2786
|
+
const { updateJudgment, initializeValueStore } = await import(
|
|
2787
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2788
|
+
);
|
|
2789
|
+
await initializeValueStore();
|
|
2790
|
+
const updated = await updateJudgment(id, req.body || {});
|
|
2791
|
+
if (!updated) return res.status(404).json({ error: 'judgment not found' });
|
|
2792
|
+
res.json({ ok: true, judgment: updated });
|
|
2793
|
+
} catch (err: any) {
|
|
2794
|
+
console.error('[judgments] PATCH failed:', err);
|
|
2795
|
+
res.status(500).json({ error: err.message });
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
// 删除判断
|
|
2800
|
+
app.delete('/api/judgments/:id', async (req, res) => {
|
|
2801
|
+
try {
|
|
2802
|
+
const { id } = req.params;
|
|
2803
|
+
const { deleteJudgment, initializeValueStore } = await import(
|
|
2804
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2805
|
+
);
|
|
2806
|
+
await initializeValueStore();
|
|
2807
|
+
const ok = await deleteJudgment(id);
|
|
2808
|
+
if (!ok) return res.status(404).json({ error: 'judgment not found' });
|
|
2809
|
+
res.json({ ok: true });
|
|
2810
|
+
} catch (err: any) {
|
|
2811
|
+
console.error('[judgments] DELETE failed:', err);
|
|
2812
|
+
res.status(500).json({ error: err.message });
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
|
|
2816
|
+
// 批量删除: { ids: ['hv-xxx', ...] } → { ok, deleted, notFound }
|
|
2817
|
+
app.post('/api/judgments/batch-delete', async (req, res) => {
|
|
2818
|
+
try {
|
|
2819
|
+
const ids = (req.body && Array.isArray(req.body.ids)) ? (req.body.ids as unknown[]) : null;
|
|
2820
|
+
if (!ids) return res.status(400).json({ error: 'ids array required' });
|
|
2821
|
+
const { deleteJudgment, initializeValueStore } = await import(
|
|
2822
|
+
'../pi-ecosystem-judgment/human-value-store.js'
|
|
2823
|
+
);
|
|
2824
|
+
await initializeValueStore();
|
|
2825
|
+
const idStrs = ids.filter((x): x is string => typeof x === 'string' && x.length > 0);
|
|
2826
|
+
let deleted = 0;
|
|
2827
|
+
const notFound: string[] = [];
|
|
2828
|
+
for (const id of idStrs) {
|
|
2829
|
+
const ok = await deleteJudgment(id);
|
|
2830
|
+
if (ok) deleted++; else notFound.push(id);
|
|
2831
|
+
}
|
|
2832
|
+
res.json({ ok: true, deleted, notFound });
|
|
2833
|
+
} catch (err: any) {
|
|
2834
|
+
console.error('[judgments] batch-delete failed:', err);
|
|
2835
|
+
res.status(500).json({ error: err.message });
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
// AI 自动委派: 根据新判断的 capability / context 找最匹配的远端 agent, 委派任务
|
|
2840
|
+
// 由前端在 POST /api/judgments 成功后调用 (fire-and-forget)
|
|
2841
|
+
// 出参: { matched, targetAgent, response | skipped, reason }
|
|
2842
|
+
app.post('/api/judgments/auto-delegate', async (req, res) => {
|
|
2843
|
+
try {
|
|
2844
|
+
const { judgmentId, capability, instruction } = req.body as {
|
|
2845
|
+
judgmentId?: string; capability?: string; instruction?: string;
|
|
2846
|
+
};
|
|
2847
|
+
if (!judgmentId && !capability) {
|
|
2848
|
+
return res.status(400).json({ error: 'judgmentId or capability required' });
|
|
2849
|
+
}
|
|
2850
|
+
const cap = capability || 'general';
|
|
2851
|
+
// 用 agent-manifest-protocol 里的 pickAgent (内存) — 走本节点已经缓存的远端 manifest
|
|
2852
|
+
const manifestMod = await import('../agents/agent-manifest-protocol.js');
|
|
2853
|
+
const picked = manifestMod.pickAgent(cap);
|
|
2854
|
+
if (!picked) {
|
|
2855
|
+
return res.json({ ok: true, matched: false, reason: 'no remote agent matches capability' });
|
|
2856
|
+
}
|
|
2857
|
+
// 命中后, 用 iroh delegate transport 真正发过去
|
|
2858
|
+
// 注: irohDelegateTransport.sendToNode 走的是 sendToNode(publicKey, frame, timeoutMs)
|
|
2859
|
+
// irohTransport 的 sendMessage 不等回包, 所以委托是 fire-and-forget
|
|
2860
|
+
// 想等回包需要新接口. 这里先把 "找得到目标 + 发送成功" 作为成功.
|
|
2861
|
+
// TODO: 接入 requestResponse 等待远端 agent_response
|
|
2862
|
+
try {
|
|
2863
|
+
const idMod = await import('../network/iroh-integration.js');
|
|
2864
|
+
const integ = idMod.getIrohIntegration();
|
|
2865
|
+
if (!integ || !integ.getNodeId()) {
|
|
2866
|
+
return res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, reason: 'iroh not initialized' });
|
|
2867
|
+
}
|
|
2868
|
+
// 用 pickAgent 选出来的 agent 关联的 irohNodeId (有的话), 没有就跳到本地自处理
|
|
2869
|
+
const targetIrohNodeId = picked.agent.irohNodeId;
|
|
2870
|
+
if (!targetIrohNodeId) {
|
|
2871
|
+
return res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, reason: 'target agent has no irohNodeId (peer identity not bound)' });
|
|
2872
|
+
}
|
|
2873
|
+
const ok = await integ.sendTo(targetIrohNodeId, 'agent_delegate', new TextEncoder().encode(JSON.stringify({
|
|
2874
|
+
type: 'agent_delegate',
|
|
2875
|
+
payload: {
|
|
2876
|
+
capability: cap,
|
|
2877
|
+
instruction: instruction || `请执行我的判断: ${judgmentId}`,
|
|
2878
|
+
fromAgentId: 'local-judgment',
|
|
2879
|
+
},
|
|
2880
|
+
ts: Date.now(),
|
|
2881
|
+
fromDid: '',
|
|
2882
|
+
})));
|
|
2883
|
+
res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: ok });
|
|
2884
|
+
} catch (e: any) {
|
|
2885
|
+
res.json({ ok: true, matched: true, targetAgent: picked.agent, sent: false, error: e.message });
|
|
2886
|
+
}
|
|
2887
|
+
} catch (err: any) {
|
|
2888
|
+
console.error('[judgments] auto-delegate failed:', err);
|
|
2889
|
+
res.status(500).json({ error: err.message });
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
|
|
2893
|
+
// (判断的 UI 已合并到主页面 header 的盾牌按钮 + modal, 不再走独立路由)
|
|
2894
|
+
|
|
2492
2895
|
// 启动看门狗监控
|
|
2493
2896
|
if (watchdog) {
|
|
2494
2897
|
// level 1 (内存爆) → 进程自杀, 依赖外层 supervisor / 用户重启 (Windows 任务计划/手动)
|
package/src/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;
|