@bolloon/bolloon-agent 0.4.25 → 0.4.27
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/execution-supervisor.js +87 -2
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +79 -4
- package/dist/agents/p2p-info.js +175 -0
- package/dist/agents/pi-sdk.js +39 -0
- package/dist/agents/run-store.js +15 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +282 -0
- package/dist/agents/trace-export.js +125 -0
- package/dist/agents/write-staging.js +12 -4
- package/dist/agents/x402/goal-run-bridge.js +105 -0
- package/dist/agents/x402/milestone-settlement.js +150 -0
- package/dist/agents/x402/paid-info-store.js +66 -12
- package/dist/agents/x402/payment-recovery.js +290 -0
- package/dist/agents/x402/resource-contract.js +473 -0
- package/dist/agents/x402/settlement-state.js +378 -0
- package/dist/agents/x402/trade.js +257 -0
- package/dist/agents/x402/transaction-protocol.js +99 -0
- package/dist/agents/x402/transaction-store.js +350 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/cli-entry.js +74 -0
- package/dist/electron/first-run.js +33 -2
- package/dist/electron-build/electron/first-run.js +35 -2
- package/dist/electron-build/electron/first-run.js.map +1 -1
- package/dist/index.js +224 -4
- package/dist/llm/config-store.js +35 -4
- package/dist/network/agent-network.js +10 -0
- package/dist/network/goal-event-bridge.js +57 -0
- package/dist/setup/onboard.js +549 -0
- package/dist/setup/setup-store.js +592 -0
- package/dist/web/routes-x402-info.js +1 -0
- package/dist/web/server.js +330 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1449,6 +1449,149 @@ async function processInputInner(input, comm) {
|
|
|
1449
1449
|
}
|
|
1450
1450
|
return;
|
|
1451
1451
|
}
|
|
1452
|
+
// 2026-09-18 (Phase 4): /tx [transactionId] —— 交易审计 (里程碑/争议/责任/结算事实)
|
|
1453
|
+
if (cmd === '/tx' || cmd.startsWith('/tx ')) {
|
|
1454
|
+
const id = trimmed.slice('/tx'.length).trim().split(/\s+/).filter(Boolean)[0];
|
|
1455
|
+
try {
|
|
1456
|
+
const { listTransactions, readTransaction, replayTransaction } = await import('./agents/x402/transaction-store.js');
|
|
1457
|
+
const MILE = await import('./agents/x402/milestone-settlement.js');
|
|
1458
|
+
if (!id) {
|
|
1459
|
+
const txs = await listTransactions();
|
|
1460
|
+
if (!txs.length) {
|
|
1461
|
+
appendLine(`${C_DIM}还没有交易记录 (~/.bolloon/transactions/)${RESET}`);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
appendLine(`${C_DIM}最近 ${txs.length} 笔交易 (生命周期 + 结算事实 两层):${RESET}`);
|
|
1465
|
+
for (const t of txs.slice(-12)) {
|
|
1466
|
+
const agg = t.milestones?.length ? MILE.aggregateMilestones(t.milestones) : null;
|
|
1467
|
+
appendLine(` ${C_DIM}${t.transactionId} ${RESET}${String(t.status).padEnd(18)} 结算=${String(t.settlementFact || '?').padEnd(18)} ${t.amount || '?'} ${t.currency || ''}${agg ? ` · 里程碑 ${agg.verified}/${agg.total}` : ''}${t.dispute ? ' · ⚠争议' : ''}`);
|
|
1468
|
+
}
|
|
1469
|
+
appendLine(`${C_DIM}/tx <transactionId> 看完整审计 (含证据链回放)${RESET}`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const rec = await readTransaction(id);
|
|
1473
|
+
if (!rec) {
|
|
1474
|
+
appendLine(`${C_ERROR}没有这笔交易: ${id}${RESET}`);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
appendLine(` ${C_DIM}交易 ${rec.transactionId} · ${rec.status} · 结算 ${rec.settlementFact} · 链上=${rec.chainSettled === true} txHash=${rec.txHash || '(无)'}${RESET}`);
|
|
1478
|
+
appendLine(` ${C_DIM}资源 ${rec.itemId} · ${rec.amount} ${rec.currency} · ${rec.network} · 付款方式 ${rec.paymentMode}${RESET}`);
|
|
1479
|
+
if (rec.milestones?.length) {
|
|
1480
|
+
const agg = MILE.aggregateMilestones(rec.milestones);
|
|
1481
|
+
appendLine(` ${C_DIM}里程碑 ${agg.verified}/${agg.total} 完成 → ${agg.settlementFact}: ${agg.reason}${RESET}`);
|
|
1482
|
+
for (const m of rec.milestones)
|
|
1483
|
+
appendLine(` ${C_DIM}${m.milestoneId} ${m.title} ${m.amount} [付:${m.paymentStatus} 交:${m.deliveryStatus} 验:${m.verificationStatus}]${RESET}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (rec.dispute)
|
|
1486
|
+
appendLine(` ${C_ERROR}争议: ${rec.dispute.reason} (缺证据 ${rec.dispute.missingEvidence.length} 项, ${rec.dispute.resolution ? `已收尾: ${rec.dispute.resolution.decision}` : '未收尾'})${RESET}`);
|
|
1487
|
+
if (rec.responsibility)
|
|
1488
|
+
appendLine(` ${C_DIM}责任候选: ${rec.responsibility.type} — ${rec.responsibility.reason}${RESET}`);
|
|
1489
|
+
const elig = MILE.milestoneGoalEligibility(rec, { executionOk: rec.execution?.ok === true, goalCriteriaHit: rec.goalCriteriaMet === true });
|
|
1490
|
+
appendLine(` ${C_DIM}Goal 成功证据资格: ${elig.eligible ? '✓' : '✗'} ${elig.reason}${RESET}`);
|
|
1491
|
+
const lines = await replayTransaction(id);
|
|
1492
|
+
appendLine(` ${C_DIM}证据链 (${lines.length} 条):${RESET}`);
|
|
1493
|
+
for (const l of lines)
|
|
1494
|
+
appendLine(` ${C_DIM}${l}${RESET}`);
|
|
1495
|
+
}
|
|
1496
|
+
catch (e) {
|
|
1497
|
+
appendLine(`${C_ERROR}/tx 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1498
|
+
}
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
// 2026-09-18: /trace [runId] [--json] —— 智能体工具执行轨迹 (真跑过什么工具/结果/耗时), 可复制交换
|
|
1502
|
+
if (cmd === '/trace' || cmd.startsWith('/trace ')) {
|
|
1503
|
+
const rest = trimmed.slice('/trace'.length).trim();
|
|
1504
|
+
try {
|
|
1505
|
+
const { listRuns, readRun } = await import('./agents/run-store.js');
|
|
1506
|
+
const { runToTraceText, runToTraceJson, summarizeTrace } = await import('./agents/trace-export.js');
|
|
1507
|
+
const wantJson = rest.includes('--json');
|
|
1508
|
+
const runId = rest.split(/\s+/).find((a) => a && !a.startsWith('--'));
|
|
1509
|
+
if (!runId) {
|
|
1510
|
+
const runs = await listRuns({ limit: 12 });
|
|
1511
|
+
if (!runs.length) {
|
|
1512
|
+
appendLine(`${C_DIM}还没有运行记录 (每次智能体运行都落盘 ~/.bolloon/runs/)${RESET}`);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
appendLine(`${C_DIM}最近 ${runs.length} 次运行的工具执行轨迹:${RESET}`);
|
|
1516
|
+
for (const r of runs)
|
|
1517
|
+
appendLine(` ${C_DIM}${r.runId} [${r.status}] ${RESET}${summarizeTrace(r)}`);
|
|
1518
|
+
appendLine(`${C_DIM}/trace <runId> 看完整轨迹 (文本可复制交换) · /trace <runId> --json 机器可读${RESET}`);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const run = await readRun(runId);
|
|
1522
|
+
if (!run) {
|
|
1523
|
+
appendLine(`${C_ERROR}没有这个运行: ${runId}${RESET}`);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (wantJson) {
|
|
1527
|
+
appendLine(JSON.stringify(runToTraceJson(run), null, 2));
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
for (const line of runToTraceText(run).split('\n'))
|
|
1531
|
+
appendLine(` ${C_DIM}${line}${RESET}`);
|
|
1532
|
+
}
|
|
1533
|
+
catch (e) {
|
|
1534
|
+
appendLine(`${C_ERROR}/trace 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// 2026-09-18: /p2p [--json] —— 本机 P2P 连接信息 (peerId + 可拨入地址), 抄进名片/小工具递给对方
|
|
1539
|
+
if (cmd === '/p2p' || cmd.startsWith('/p2p ')) {
|
|
1540
|
+
try {
|
|
1541
|
+
const { getLocalP2pInfo, formatP2pInfoText, formatP2pInfoJson } = await import('./agents/p2p-info.js');
|
|
1542
|
+
const info = await getLocalP2pInfo();
|
|
1543
|
+
if (trimmed.includes('--json')) {
|
|
1544
|
+
appendLine(formatP2pInfoJson(info));
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
for (const line of formatP2pInfoText(info).split('\n'))
|
|
1548
|
+
appendLine(` ${C_DIM}${line}${RESET}`);
|
|
1549
|
+
}
|
|
1550
|
+
catch (e) {
|
|
1551
|
+
appendLine(`${C_ERROR}/p2p 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1552
|
+
}
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
// 2026-09-16 (2-F): /criteria <goalId> [confirm|propose <text...>] —— 判据:看/确认/改/让 agent 提候选
|
|
1556
|
+
if (cmd === '/criteria' || cmd.startsWith('/criteria ')) {
|
|
1557
|
+
const rest = trimmed.slice('/criteria'.length).trim();
|
|
1558
|
+
try {
|
|
1559
|
+
const { longTermStatus, confirmCriteria, proposeForGoal } = await import('./agents/goal-criteria.js');
|
|
1560
|
+
const { readGoal } = await import('./agents/goal-store.js');
|
|
1561
|
+
const [goalId, action, ...words] = rest.split(/\s+/).filter(Boolean);
|
|
1562
|
+
if (!goalId) {
|
|
1563
|
+
appendLine(`${C_DIM}用法: /criteria <goalId> [confirm | propose | set <判据用;分>]${RESET}`);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const g = await readGoal(goalId);
|
|
1567
|
+
if (!g) {
|
|
1568
|
+
appendLine(`${C_ERROR}没有这个目标: ${goalId}${RESET}`);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (action === 'propose') {
|
|
1572
|
+
const p = await proposeForGoal(goalId);
|
|
1573
|
+
appendLine(p.ok ? `${C_DIM}候选判据 (待确认):\n${p.criteria.map((c, i) => ` [${i}] ${c}`).join('\n')}${RESET}` : `${C_ERROR}无法生成判据: ${p.reason}${RESET}`);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const list = action === 'set' && words.length ? words.join(' ').split(';').map((x) => x.trim()).filter(Boolean) : undefined;
|
|
1577
|
+
if (action === 'confirm' || list) {
|
|
1578
|
+
const r = await confirmCriteria(goalId, { criteria: list, by: 'cli' });
|
|
1579
|
+
appendLine(r.ok ? `${C_DIM}判据已确认 (v${r.goal?.criteriaVersion}): ${(r.goal?.successCriteria || []).join(' | ')}${RESET}` : `${C_ERROR}${r.reason}${RESET}`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const st = await longTermStatus(goalId);
|
|
1583
|
+
appendLine(`${C_DIM}goal ${g.goalId} [${g.status}] 判据来源=${g.criteriaSource || 'unknown'} 已确认=${g.criteriaConfirmed === true} v${g.criteriaVersion || 1}${RESET}`);
|
|
1584
|
+
g.successCriteria.forEach((c, i) => appendLine(` ${g.completedCriteria.includes(i) ? '✓' : '·'} [${i}] ${c}`));
|
|
1585
|
+
if (g.proposedCriteria?.length)
|
|
1586
|
+
appendLine(` ${C_DIM}候选 (未确认): ${g.proposedCriteria.join(' | ')}${RESET}`);
|
|
1587
|
+
appendLine(` 长期完成: ${st.canComplete ? '✅ 可判完成' : `❌ ${st.reason}`}`);
|
|
1588
|
+
appendLine(` ${C_DIM}检查: ${Object.entries(st.checks).map(([k, v]) => `${k}=${v ? '✓' : '✗'}`).join(' ')}${RESET}`);
|
|
1589
|
+
}
|
|
1590
|
+
catch (e) {
|
|
1591
|
+
appendLine(`${C_ERROR}/criteria 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1592
|
+
}
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1452
1595
|
// 2026-09-16 (M2-B): /supervise — 长期执行层 (状态/唤醒原因/手动推进一个周期) · /wake <goalId> — 外部事件唤醒
|
|
1453
1596
|
if (cmd === '/supervise' || cmd.startsWith('/supervise ')) {
|
|
1454
1597
|
const arg = trimmed.slice('/supervise'.length).trim();
|
|
@@ -3901,6 +4044,21 @@ function parseArgs() {
|
|
|
3901
4044
|
case '--supervise':
|
|
3902
4045
|
result.supervise = true;
|
|
3903
4046
|
break;
|
|
4047
|
+
case '--setup-status':
|
|
4048
|
+
result.setupStatus = true;
|
|
4049
|
+
break;
|
|
4050
|
+
case '--setup-resume':
|
|
4051
|
+
result.setupResume = true;
|
|
4052
|
+
break;
|
|
4053
|
+
case '--setup-repair':
|
|
4054
|
+
result.setupRepair = true;
|
|
4055
|
+
break;
|
|
4056
|
+
case '--setup-reconfigure':
|
|
4057
|
+
result.setupReconfigure = true;
|
|
4058
|
+
break;
|
|
4059
|
+
case '--setup-test':
|
|
4060
|
+
result.setupTest = true;
|
|
4061
|
+
break;
|
|
3904
4062
|
case '--supervise-once':
|
|
3905
4063
|
result.supervise = true;
|
|
3906
4064
|
result.superviseOnce = true;
|
|
@@ -4333,6 +4491,45 @@ async function main() {
|
|
|
4333
4491
|
const mode = args.web ? 'web' : 'cli';
|
|
4334
4492
|
// 2026-09-16 (2-C.1): 独立 Supervisor 宿主 —— `bolloon --supervise [--supervise-once|--supervise-dry-run]`
|
|
4335
4493
|
// 长期执行不再依附 web 进程 (页面关掉/CLI 没开也能继续推进 Goal)。
|
|
4494
|
+
// 2026-09-16 (Phase 4): Onboard 统一入口 —— CLI / Web 共用同一条执行器 (src/setup/onboard.ts)
|
|
4495
|
+
// --setup-status 只读; --setup-resume 从失败阶段继续; --setup-repair 迁移/备份坏文件后就地修;
|
|
4496
|
+
// --setup-reconfigure 只改选中项 (新配置测通才切 active); --setup-test 重跑连通性 + 运行时。
|
|
4497
|
+
if (args.setupStatus || args.setupResume || args.setupRepair || args.setupReconfigure || args.setupTest) {
|
|
4498
|
+
const mode = args.setupStatus ? 'status' : args.setupRepair ? 'repair' : args.setupReconfigure ? 'reconfigure' : args.setupTest ? 'test' : 'resume';
|
|
4499
|
+
if (mode === 'status') {
|
|
4500
|
+
const { evaluateSetup, describeSetup } = await import('./setup/setup-store.js');
|
|
4501
|
+
const ev = await evaluateSetup();
|
|
4502
|
+
process.stdout.write(describeSetup(ev) + '\n');
|
|
4503
|
+
process.exit(ev.gate === 'ready' ? 0 : 1);
|
|
4504
|
+
}
|
|
4505
|
+
const { defaultWizardIO } = await import('./cli/setup-wizard.js');
|
|
4506
|
+
const { runOnboard } = await import('./setup/onboard.js');
|
|
4507
|
+
const wizIO = defaultWizardIO();
|
|
4508
|
+
const res = await runOnboard({
|
|
4509
|
+
mode: mode,
|
|
4510
|
+
io: {
|
|
4511
|
+
print: (m) => process.stdout.write(m + '\n'),
|
|
4512
|
+
ask: async (q, o) => wizIO.ask(q, o?.defaultValue ? { default: o.defaultValue } : undefined),
|
|
4513
|
+
askHidden: async (q) => wizIO.ask(q, { hidden: true }),
|
|
4514
|
+
confirm: async (q, d = true) => {
|
|
4515
|
+
const a = String(await wizIO.ask(`${q} (y/n)`, { default: d ? 'y' : 'n' })).trim().toLowerCase();
|
|
4516
|
+
return a === '' ? d : /^(y|yes|1|true|是)$/.test(a);
|
|
4517
|
+
},
|
|
4518
|
+
select: async (q, choices) => {
|
|
4519
|
+
process.stdout.write(q + '\n');
|
|
4520
|
+
choices.forEach((c, i) => process.stdout.write(` ${i + 1}) ${c.label}${c.hint ? ` — ${c.hint}` : ''}\n`));
|
|
4521
|
+
const a = String(await wizIO.ask('选择 (序号或名称)', { default: '1' })).trim();
|
|
4522
|
+
if (!a)
|
|
4523
|
+
return choices[0]?.value || '';
|
|
4524
|
+
if (/^\d+$/.test(a) && choices[Number(a) - 1])
|
|
4525
|
+
return choices[Number(a) - 1].value;
|
|
4526
|
+
const hit = choices.find((c) => c.value === a) || choices.find((c) => a && c.value.startsWith(a));
|
|
4527
|
+
return hit?.value || a;
|
|
4528
|
+
},
|
|
4529
|
+
},
|
|
4530
|
+
});
|
|
4531
|
+
process.exit(res.ok ? 0 : 1);
|
|
4532
|
+
}
|
|
4336
4533
|
if (args.supervise) {
|
|
4337
4534
|
const { runStandaloneSupervisorHost } = await import('./agents/supervisor-host.js');
|
|
4338
4535
|
const res = await runStandaloneSupervisorHost({
|
|
@@ -4381,14 +4578,37 @@ async function main() {
|
|
|
4381
4578
|
// 2026-09-13: 首次运行引导 — 没有可用模型供应商 / 还没有用户身份时, 先走初始化向导
|
|
4382
4579
|
// (放在 CLI 启动前: 用户先回答"你是谁 / 用哪个模型", 再进 TUI 面板)
|
|
4383
4580
|
if (isCLIInteractive) {
|
|
4581
|
+
// 2026-09-16 (M4 启动硬门禁): 先把初始化事实读出来 —— 未就绪就**先修**, 修不好就非零退出。
|
|
4582
|
+
// 旧行为是"向导失败也只 warn, 照常进对话", 结果是半成品配置能进运行态 (看起来启动成功、实际不可执行)。
|
|
4583
|
+
const w = (m) => process.stderr.write(m.endsWith('\n') ? m : m + '\n');
|
|
4584
|
+
let gateEv = null;
|
|
4384
4585
|
try {
|
|
4385
|
-
const {
|
|
4386
|
-
|
|
4387
|
-
|
|
4586
|
+
const { refreshSetupState, describeSetup } = await import('./setup/setup-store.js');
|
|
4587
|
+
gateEv = await refreshSetupState({ light: true });
|
|
4588
|
+
if (gateEv.gate !== 'ready') {
|
|
4589
|
+
w(describeSetup(gateEv));
|
|
4590
|
+
if (process.env.BOLLOON_SKIP_SETUP === '1') {
|
|
4591
|
+
w('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
|
|
4592
|
+
}
|
|
4593
|
+
else {
|
|
4594
|
+
const { runSetupWizard } = await import('./cli/setup-wizard.js');
|
|
4595
|
+
await runSetupWizard({ interactive: true });
|
|
4596
|
+
const after = await refreshSetupState({});
|
|
4597
|
+
if (after.gate !== 'ready') {
|
|
4598
|
+
w(describeSetup(after));
|
|
4599
|
+
w('⛔ 初始化未完成 → 退出 (不会以"看起来能跑"的状态进入对话)');
|
|
4600
|
+
process.exit(1);
|
|
4601
|
+
}
|
|
4602
|
+
w('✅ 初始化完成, 进入正常模式');
|
|
4603
|
+
}
|
|
4388
4604
|
}
|
|
4389
4605
|
}
|
|
4390
4606
|
catch (e) {
|
|
4391
|
-
|
|
4607
|
+
// 评估/向导自身异常: fail-closed —— 不假装就绪
|
|
4608
|
+
w(`⛔ 初始化流程失败 (fail-closed): ${String(e?.message || e).slice(0, 200)}`);
|
|
4609
|
+
w(` 当前状态评估: ${gateEv ? gateEv.gate : '未知 (评估都没跑通)'}`);
|
|
4610
|
+
w(' 排查: `bolloon setup --status` (或删掉 ~/.bolloon/setup-state.json 后重跑 setup)');
|
|
4611
|
+
process.exit(1);
|
|
4392
4612
|
}
|
|
4393
4613
|
}
|
|
4394
4614
|
if (isNonInteractive) {
|
package/dist/llm/config-store.js
CHANGED
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as fsSync from 'fs';
|
|
9
|
+
/** 2026-09-16 (M1): 不再在模块加载时固定 HOME —— 独立宿主/测试注入/长驻进程都走同一个解析 */
|
|
10
|
+
function configDir() {
|
|
11
|
+
const env = process.env.BOLLOON_HOME?.trim();
|
|
12
|
+
if (env)
|
|
13
|
+
return env;
|
|
14
|
+
return path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon');
|
|
15
|
+
}
|
|
8
16
|
// 2026-08-07: llm-config.json → bolloon-config.json (统一配置文件名, Bolloon 自己有写权限)
|
|
9
17
|
// initialize() 会做一次迁移: 旧文件存在且新文件不存在 → 复制旧内容, 之后统一读写新文件
|
|
10
|
-
const CONFIG_PATH = path.join(
|
|
11
|
-
const LEGACY_CONFIG_PATH = path.join(
|
|
18
|
+
const CONFIG_PATH = path.join(configDir(), 'bolloon-config.json');
|
|
19
|
+
const LEGACY_CONFIG_PATH = path.join(configDir(), 'llm-config.json');
|
|
12
20
|
export const DEFAULT_PROVIDER_CONFIGS = {
|
|
13
21
|
openai: {
|
|
14
22
|
enabled: false,
|
|
@@ -231,6 +239,10 @@ function getDefaultConfig() {
|
|
|
231
239
|
class LLMConfigStore {
|
|
232
240
|
config = null;
|
|
233
241
|
initialized = false;
|
|
242
|
+
/** 2026-09-16: 记住加载时用的配置目录 —— 目录变了必须丢弃缓存, 否则会把 A 目录的配置写到 B 目录 */
|
|
243
|
+
loadedDir = null;
|
|
244
|
+
/** 2026-09-16: 记住加载时的文件签名 —— 外部改了配置 (repair/agent 工具/用户手改) 必须重新读, 不能一直用旧值 */
|
|
245
|
+
loadedSig = null;
|
|
234
246
|
// v0.2.15: single-flight lock around read-modify-write of `~/.bolloon/llm-config.json`.
|
|
235
247
|
// Prevents concurrent save() calls from clobbering each other when the user
|
|
236
248
|
// configures two providers back-to-back (e.g. saving gemini, then anthropic, in
|
|
@@ -244,10 +256,19 @@ class LLMConfigStore {
|
|
|
244
256
|
return next;
|
|
245
257
|
}
|
|
246
258
|
async initialize() {
|
|
259
|
+
// 目录变了 (换 HOME / 独立宿主 / 测试注入): 内存缓存作废, 重新读盘
|
|
260
|
+
const dir = configDir();
|
|
261
|
+
const sig = this.fileSignature();
|
|
262
|
+
if (this.initialized && (this.loadedDir !== dir || this.loadedSig !== sig)) {
|
|
263
|
+
this.initialized = false;
|
|
264
|
+
this.config = null;
|
|
265
|
+
}
|
|
247
266
|
if (this.initialized)
|
|
248
267
|
return;
|
|
268
|
+
this.loadedDir = dir;
|
|
269
|
+
this.loadedSig = sig;
|
|
249
270
|
try {
|
|
250
|
-
await fs.mkdir(
|
|
271
|
+
await fs.mkdir(configDir(), { recursive: true });
|
|
251
272
|
// 2026-08-07: 迁移 — 旧 llm-config.json 存在且新 bolloon-config.json 不存在时复制旧内容
|
|
252
273
|
try {
|
|
253
274
|
await fs.access(CONFIG_PATH);
|
|
@@ -289,6 +310,16 @@ class LLMConfigStore {
|
|
|
289
310
|
// 2026-08-07: mode 0o600 — 仅当前用户可读写 (含 API key 的敏感配置); Bolloon 自身进程可写
|
|
290
311
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2), { mode: 0o600 });
|
|
291
312
|
}
|
|
313
|
+
/** 配置文件签名 (mtime+size); 不存在时返回 'missing' */
|
|
314
|
+
fileSignature() {
|
|
315
|
+
try {
|
|
316
|
+
const st = fsSync.statSync(configDir() + '/bolloon-config.json');
|
|
317
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return 'missing';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
292
323
|
async getConfig() {
|
|
293
324
|
await this.initialize();
|
|
294
325
|
return { ...this.config };
|
|
@@ -619,6 +619,16 @@ export class AgentMessaging {
|
|
|
619
619
|
console.warn(`[Messaging] Signature verification failed for ${signedMsg.from.substring(0, 20)}`);
|
|
620
620
|
return false;
|
|
621
621
|
}
|
|
622
|
+
// 2026-09-16 (2-C.4): 目标关联的真实外部事件 —— 签名已通过校验, 这里再按
|
|
623
|
+
// 来源 / correlation / 过期 / eventId 去重做一次严格匹配, 只唤醒"正在等这个事件"的 Goal。
|
|
624
|
+
// **不在这里启动 agent**: 只写事实 + 唤醒, 由 Supervisor 下一轮继续。
|
|
625
|
+
try {
|
|
626
|
+
const { tryDeliverGoalEvent } = await import('./goal-event-bridge.js');
|
|
627
|
+
await tryDeliverGoalEvent(signedMsg, fromPeerId);
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
console.warn(`[Messaging] goal event bridge 失败 (不影响普通消息):`, e?.message);
|
|
631
|
+
}
|
|
622
632
|
const handler = this.messageHandlers.get(signedMsg.type);
|
|
623
633
|
if (handler) {
|
|
624
634
|
handler(data, fromPeerId, signedMsg.from);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-event-bridge.ts — 把"真实入站的签名消息"翻译成 Goal 外部事件 (批次 2-C.4)
|
|
3
|
+
*
|
|
4
|
+
* 只有签名已通过校验的消息才进到这里 (来源 = 对端 DID)。这里只做解析 + 投递,
|
|
5
|
+
* 启动执行由 Supervisor 决定 —— 事件处理器不碰 agent。
|
|
6
|
+
*
|
|
7
|
+
* 识别的两种真实来源:
|
|
8
|
+
* · delegate 回包: type 含 `delegate` 或 payload 里有 delegateId/resultCid → source='delegate'
|
|
9
|
+
* · P2P 协作回复: 其它带 goalId/requestId/continuationId 的消息 → source='p2p'
|
|
10
|
+
*/
|
|
11
|
+
import { deliverExternalEvent } from '../agents/external-events.js';
|
|
12
|
+
function parsePayload(raw) {
|
|
13
|
+
if (!raw)
|
|
14
|
+
return null;
|
|
15
|
+
if (typeof raw === 'object')
|
|
16
|
+
return raw;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(String(raw));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** 从 payload 里提取目标关联字段 (支持 goalEvent 包裹或平铺两种写法) */
|
|
25
|
+
export function extractGoalEvent(signed, fromDid) {
|
|
26
|
+
const body = parsePayload(signed.payload);
|
|
27
|
+
if (!body || typeof body !== 'object')
|
|
28
|
+
return null;
|
|
29
|
+
const env = (body.goalEvent && typeof body.goalEvent === 'object') ? body.goalEvent : body;
|
|
30
|
+
const goalId = env.goalId || env.goal_id;
|
|
31
|
+
const requestId = env.requestId || env.request_id || env.correlationId;
|
|
32
|
+
const continuationId = env.continuationId || env.continuation_id;
|
|
33
|
+
const eventId = env.eventId || env.event_id;
|
|
34
|
+
if (!goalId && !requestId && !continuationId)
|
|
35
|
+
return null; // 与 Goal 无关 → 普通消息
|
|
36
|
+
if (!eventId)
|
|
37
|
+
return null; // 没有 eventId 无法去重 → 不当事件处理
|
|
38
|
+
const isDelegate = /delegate/i.test(String(signed.type)) || !!(env.delegateId || env.resultCid);
|
|
39
|
+
return {
|
|
40
|
+
source: isDelegate ? 'delegate' : 'p2p',
|
|
41
|
+
eventId: String(eventId),
|
|
42
|
+
requestId: requestId ? String(requestId) : undefined,
|
|
43
|
+
continuationId: continuationId ? String(continuationId) : undefined,
|
|
44
|
+
goalId: goalId ? String(goalId) : undefined,
|
|
45
|
+
fromDid: fromDid || signed.from,
|
|
46
|
+
eventName: env.event || env.eventName || env.kind,
|
|
47
|
+
payload: env.result ?? env.payload ?? body,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** 投递 (未匹配/不合法 → 返回 matched:false, 调用方继续走普通消息处理) */
|
|
51
|
+
export async function tryDeliverGoalEvent(signed, fromDid) {
|
|
52
|
+
const event = extractGoalEvent(signed, fromDid);
|
|
53
|
+
if (!event)
|
|
54
|
+
return { matched: false };
|
|
55
|
+
const res = await deliverExternalEvent(event);
|
|
56
|
+
return { matched: res.ok, reason: res.reason, goalId: res.goalId };
|
|
57
|
+
}
|