@bolloon/bolloon-agent 0.4.33 → 0.5.1
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/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +580 -40
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +157 -42
- package/dist/agents/run-store.js +71 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +17 -4
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/agents/workflow-pivot-loop.js +19 -12
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +757 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +137 -20
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +578 -38
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +156 -41
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +75 -6
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +16 -3
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +70 -11
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +347 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +328 -104
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +9 -11
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +57 -31
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +385 -108
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +362 -0
- package/dist/ios/mobile-core.js +3102 -310
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +1013 -41
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +921 -0
- package/dist/ios/sw.js +1 -1
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +70 -11
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +297 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +325 -96
- package/dist/utils/update-state.js +13 -10
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +27 -22
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +2901 -580
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +838 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/dist/web/sw.js +1 -1
- package/package.json +3 -1
- package/scripts/build-web.ts +36 -0
package/dist/ios/server.js
CHANGED
|
@@ -13,10 +13,17 @@ import { registerJudgmentsRoutes } from './routes-judgments.js';
|
|
|
13
13
|
import { registerLlmConfigRoutes } from './routes-llm-config.js';
|
|
14
14
|
import { registerExternalEngineRoutes } from './routes-external-engines.js';
|
|
15
15
|
import { registerTaskRoutes } from './routes-tasks.js';
|
|
16
|
+
// 2026-09-19: 联系方式 / 人工批准 / 手机—桌面配对 路由
|
|
17
|
+
import { registerContactRoutes } from './routes-contacts.js';
|
|
18
|
+
// 2026-09-25: 手机端「任务协作」(入群 / 发公告 / 看飞轮进度) 路由 —— 高风险动作由设备签名把关
|
|
19
|
+
import { registerMobileTaskRoutes } from './routes-mobile-tasks.js';
|
|
16
20
|
// 2026-09-13: 微支付信息服务 (x402) 路由
|
|
17
21
|
import { registerX402InfoRoutes } from './routes-x402-info.js';
|
|
22
|
+
// 2026-09-22: P5 链上索引 (只读: status/stats/timeline/events 增量) 路由
|
|
23
|
+
import { registerChainIndexRoutes } from './routes-chain-index.js';
|
|
18
24
|
import { loadPeerTier, recordInteraction, tierLabel } from '../social/dunbar-tier.js';
|
|
19
25
|
import { registerHearthRoutes } from './routes-hearth.js';
|
|
26
|
+
import { registerOnchainTradeRoutes } from './routes-onchain-trade.js';
|
|
20
27
|
import { loadOrCreateAgentIdentity } from '../agents/agent-identity.js';
|
|
21
28
|
// 2026-07-06: 类型抽到 ./server-types.ts (channel / session / task / sse client / iroh info / paths)
|
|
22
29
|
import { SESSION_CACHE_PATH, SHARED_SESSION_PATH, IPFS_ENDPOINT, } from './server-types.js';
|
|
@@ -1530,6 +1537,11 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
1530
1537
|
usePivotLoop: true,
|
|
1531
1538
|
}, true); // forceNew: true 强制创建新实例
|
|
1532
1539
|
channelSessions.set(sessionKey, session);
|
|
1540
|
+
// 2026-09-16: 标记运行表面 — 落盘 run 记录里能区分 web 运行 (CLI 默认 cli)
|
|
1541
|
+
try {
|
|
1542
|
+
session.setRunSurface?.('web');
|
|
1543
|
+
}
|
|
1544
|
+
catch { /* 老 session 无此方法则忽略 */ }
|
|
1533
1545
|
if (channelDid) {
|
|
1534
1546
|
console.log(`[Agent] 新建频道 ${channelId} session, DID = ${channelDid}, sessionId = ${currentSessionId}`);
|
|
1535
1547
|
}
|
|
@@ -1626,6 +1638,104 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1626
1638
|
catch (err) {
|
|
1627
1639
|
console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
|
|
1628
1640
|
}
|
|
1641
|
+
// 2026-09-16: 孤儿运行对账 — 上次进程留下的 running 记录改判 interrupted。
|
|
1642
|
+
// 不做这步的话, 刷新/重启后 UI 会显示"还在跑"的幽灵运行 (最典型的假状态)。
|
|
1643
|
+
try {
|
|
1644
|
+
const { reconcileOrphans } = await import('../agents/run-store.js');
|
|
1645
|
+
const rec = await reconcileOrphans();
|
|
1646
|
+
if (rec.interrupted.length) {
|
|
1647
|
+
console.log(`[runs] 对账: ${rec.interrupted.length} 条上次中断的运行已标 interrupted (${rec.interrupted.slice(0, 3).join(', ')}${rec.interrupted.length > 3 ? '…' : ''})`);
|
|
1648
|
+
}
|
|
1649
|
+
if (rec.stillRunning.length)
|
|
1650
|
+
console.log(`[runs] 存活运行: ${rec.stillRunning.length} 条`);
|
|
1651
|
+
}
|
|
1652
|
+
catch (err) {
|
|
1653
|
+
console.warn('[runs] 对账失败 (非致命):', err?.message);
|
|
1654
|
+
}
|
|
1655
|
+
// 2026-09-16 (M2-B): ExecutionSupervisor —— 长期执行层。
|
|
1656
|
+
// 页面关掉、进程重启后由它把 Goal 继续下去; 跨进程排他靠 Goal lease (不是内存状态)。
|
|
1657
|
+
// Harness 管"这一段能不能安全执行", Supervisor 管"这个目标还要不要继续执行"。
|
|
1658
|
+
if (process.env.BOLLOON_SUPERVISOR !== '0') {
|
|
1659
|
+
try {
|
|
1660
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
1661
|
+
const { runSupervisorHost } = await import('../agents/supervisor-host.js');
|
|
1662
|
+
const tickMs = Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000;
|
|
1663
|
+
const leaseMs = Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000;
|
|
1664
|
+
// 执行器**在每次执行前解析** (不写死): web 宿主按 Goal 的 channelId 找 channel agent。
|
|
1665
|
+
// 解析不出来 → 只诊断不执行, 且不写任何 Goal 状态。
|
|
1666
|
+
const resolver = async (req) => {
|
|
1667
|
+
const goal = req.goal;
|
|
1668
|
+
if (!goal?.channelId) {
|
|
1669
|
+
return { ok: false, kind: 'none', reason: 'Goal 没有 channelId: 无法在 web 进程内解析执行器' };
|
|
1670
|
+
}
|
|
1671
|
+
const agent = await getAgentForChannel(goal.channelId, goal.agentId || '', undefined, undefined);
|
|
1672
|
+
if (!agent)
|
|
1673
|
+
return { ok: false, kind: 'none', reason: 'channel agent 不可用' };
|
|
1674
|
+
const runner = async (r) => {
|
|
1675
|
+
if (r.kind === 'resume' && r.prevRunId && typeof agent.resumeRun === 'function') {
|
|
1676
|
+
const res = await agent.resumeRun(r.prevRunId);
|
|
1677
|
+
return { runId: r.prevRunId, status: res?.ok ? 'done' : 'failed', error: res?.ok ? undefined : res?.reason };
|
|
1678
|
+
}
|
|
1679
|
+
// 跨预算继续: 新 Run 必须挂在同一个 Goal 下 (setGoalId), 并带上上一个 Run 的非幂等守卫
|
|
1680
|
+
agent.setGoalId?.(goal.goalId);
|
|
1681
|
+
agent.setContinuationGuards?.(r.guards || []);
|
|
1682
|
+
const reply = await agent.prompt(r.instruction);
|
|
1683
|
+
// 注意: prompt 收尾会清空 currentRunId → 必须读 lastRunId (否则会拿上一条 run 做决策)
|
|
1684
|
+
const runId = agent.getLastRunId?.() || agent.getRunId?.();
|
|
1685
|
+
return { runId, status: 'done', reply: typeof reply === 'string' ? reply.slice(0, 500) : undefined };
|
|
1686
|
+
};
|
|
1687
|
+
return { ok: true, runner, kind: 'web' };
|
|
1688
|
+
};
|
|
1689
|
+
// 2026-09-16 (M4): 初始化未就绪 → Supervisor 只诊断 (不解析执行器, 不执行任何 Goal)
|
|
1690
|
+
let setupReadyForSupervisor = false;
|
|
1691
|
+
try {
|
|
1692
|
+
const { getSetupGateCached } = await import('../setup/setup-store.js');
|
|
1693
|
+
const g = await getSetupGateCached();
|
|
1694
|
+
setupReadyForSupervisor = g.gate === 'ready';
|
|
1695
|
+
if (!setupReadyForSupervisor)
|
|
1696
|
+
console.warn(`[supervisor] 初始化未就绪 (${g.gate}, 阶段 ${g.state.stage}) → 只诊断, 不执行 Goal`);
|
|
1697
|
+
}
|
|
1698
|
+
catch {
|
|
1699
|
+
console.warn('[supervisor] 初始化状态不可读 → 只诊断, 不执行 Goal');
|
|
1700
|
+
}
|
|
1701
|
+
const sup = getSupervisor({
|
|
1702
|
+
tickIntervalMs: tickMs,
|
|
1703
|
+
leaseTtlMs: leaseMs,
|
|
1704
|
+
maxPerTick: Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1,
|
|
1705
|
+
log: (m) => console.log(m),
|
|
1706
|
+
onEvent: (e) => { try {
|
|
1707
|
+
broadcast({ type: 'supervisor', ...e });
|
|
1708
|
+
}
|
|
1709
|
+
catch { /* UI 广播失败不影响调度 */ } },
|
|
1710
|
+
resolver: setupReadyForSupervisor ? resolver : undefined,
|
|
1711
|
+
});
|
|
1712
|
+
// 宿主层: 跨进程单 tick 互斥 + 宿主身份/心跳落盘 + 优雅停止
|
|
1713
|
+
const host = await runSupervisorHost({
|
|
1714
|
+
supervisor: sup,
|
|
1715
|
+
tickIntervalMs: tickMs,
|
|
1716
|
+
leaseTtlMs: leaseMs,
|
|
1717
|
+
runnerKind: 'web',
|
|
1718
|
+
log: (m) => console.log(m),
|
|
1719
|
+
});
|
|
1720
|
+
global.__bolloonSupervisorHost = host;
|
|
1721
|
+
console.log(`[supervisor] 长期执行层已启动 (owner=${host.state.owner}, worker=${host.state.workerId}, tick=${tickMs}ms, lease=${leaseMs}ms)`);
|
|
1722
|
+
const stopHost = (sig) => { void host.stop(`signal:${sig}`).finally(() => process.exit(0)); };
|
|
1723
|
+
process.once('SIGINT', () => stopHost('SIGINT'));
|
|
1724
|
+
process.once('SIGTERM', () => stopHost('SIGTERM'));
|
|
1725
|
+
}
|
|
1726
|
+
catch (err) {
|
|
1727
|
+
// 启动失败必须可观测 (不许静默降级成长时间不执行)
|
|
1728
|
+
const msg = `[supervisor] 启动失败: ${err?.message || err}`;
|
|
1729
|
+
console.error(msg);
|
|
1730
|
+
try {
|
|
1731
|
+
broadcast({ type: 'supervisor', kind: 'startup_failed', message: msg });
|
|
1732
|
+
}
|
|
1733
|
+
catch { /* ignore */ }
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
console.log('[supervisor] 长期执行层已关闭 (BOLLOON_SUPERVISOR=0)');
|
|
1738
|
+
}
|
|
1629
1739
|
// 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
|
|
1630
1740
|
try {
|
|
1631
1741
|
const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
|
|
@@ -2439,6 +2549,19 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2439
2549
|
console.log(`[cron] 调度器已启动 (每 ${Math.round((Number(process.env.BOLLOON_CRON_HEARTBEAT_MS) || 60_000) / 1000)}s 一轮, tick 带锁 + DND)`);
|
|
2440
2550
|
// 暴露给 /message 使用 (主任务闸门)
|
|
2441
2551
|
global.__bolloonMainTask = { enterMainTask, exitMainTask };
|
|
2552
|
+
// 2026-09-16: 运行失速巡检 (每 60s) — running 但长时间没新进展的运行标 stalled,
|
|
2553
|
+
// 让 UI/CLI 能说"这个运行卡住了", 而不是永远转圈 (crash 由启动时对账兜底)。
|
|
2554
|
+
setInterval(() => {
|
|
2555
|
+
void (async () => {
|
|
2556
|
+
try {
|
|
2557
|
+
const { superviseRuns } = await import('../agents/run-store.js');
|
|
2558
|
+
const r = await superviseRuns();
|
|
2559
|
+
if (r.stalled.length)
|
|
2560
|
+
console.warn(`[runs] 失速巡检: ${r.stalled.length} 条运行标 stalled (${r.stalled.slice(0, 3).join(', ')})`);
|
|
2561
|
+
}
|
|
2562
|
+
catch { /* 巡检失败不影响主流程 */ }
|
|
2563
|
+
})();
|
|
2564
|
+
}, 60_000);
|
|
2442
2565
|
}
|
|
2443
2566
|
catch (cronErr) {
|
|
2444
2567
|
console.warn('[cron] 调度器启动失败 (non-fatal):', cronErr?.message);
|
|
@@ -2805,6 +2928,15 @@ ${goalDesc}
|
|
|
2805
2928
|
}
|
|
2806
2929
|
app.get('/', serveStaticHtml('index.html', 'index.html not found; please run `npm run build:web`', 'index'));
|
|
2807
2930
|
app.get('/api-config', serveStaticHtml('api-config.html', 'api-config.html not found; please run `npm run build:web`', 'api-config'));
|
|
2931
|
+
// 2026-09-22 (P7): 链上浏览器页面 (只读索引的网页入口)
|
|
2932
|
+
// /explorer 与 /chain 两个别名都指向同一个静态页 (explorer.html + chain-explorer.js);
|
|
2933
|
+
// 页面只调 /api/chain/index/*, 不碰任何写接口。
|
|
2934
|
+
{
|
|
2935
|
+
const serveExplorer = serveStaticHtml('explorer.html', 'explorer.html not found; please run `npm run build:web`', 'explorer');
|
|
2936
|
+
app.get('/explorer', serveExplorer);
|
|
2937
|
+
app.get('/chain', serveExplorer);
|
|
2938
|
+
registerChainIndexRoutes(app);
|
|
2939
|
+
}
|
|
2808
2940
|
// 2026-07-01 (v0.2.5): 输入验证 + 健康检查 API
|
|
2809
2941
|
// - POST /api/validate-input: 前端发送前预校验, 避免后端 reject
|
|
2810
2942
|
// - GET /api/health: liveness probe + validators 清单
|
|
@@ -2832,6 +2964,737 @@ ${goalDesc}
|
|
|
2832
2964
|
res.status(400).json({ ok: false, severity: 'block', reason: e?.message ?? 'parse error' });
|
|
2833
2965
|
}
|
|
2834
2966
|
});
|
|
2967
|
+
// 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
|
|
2968
|
+
// 2026-09-18: 智能体工具执行轨迹 + 本机 P2P 信息 (与小工具/名片交换用, 结构化)
|
|
2969
|
+
// 2026-09-18 (Phase 4): 交易审计 — 里程碑 / 争议 / 责任 / 事件链 一眼可查
|
|
2970
|
+
app.get('/api/x402/transactions', async (_req, res) => {
|
|
2971
|
+
try {
|
|
2972
|
+
const { listTransactions } = await import('../agents/x402/transaction-store.js');
|
|
2973
|
+
const { aggregateMilestones } = await import('../agents/x402/milestone-settlement.js');
|
|
2974
|
+
const txs = await listTransactions();
|
|
2975
|
+
res.json({
|
|
2976
|
+
count: txs.length,
|
|
2977
|
+
transactions: txs.map((t) => ({
|
|
2978
|
+
transactionId: t.transactionId, status: t.status, settlementFact: t.settlementFact,
|
|
2979
|
+
itemId: t.itemId, amount: t.amount, currency: t.currency, network: t.network,
|
|
2980
|
+
paymentMode: t.paymentMode, chainSettled: t.chainSettled === true, txHash: t.txHash,
|
|
2981
|
+
milestones: t.milestones?.length ? aggregateMilestones(t.milestones) : null,
|
|
2982
|
+
disputed: !!t.dispute, disputeResolved: t.dispute?.resolution?.decision || null,
|
|
2983
|
+
responsibility: t.responsibility?.type || null,
|
|
2984
|
+
partial: t.settlementFact === 'partially_settled',
|
|
2985
|
+
})),
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
catch (err) {
|
|
2989
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
app.get('/api/x402/transactions/:id', async (req, res) => {
|
|
2993
|
+
try {
|
|
2994
|
+
const { readTransaction, replayTransaction } = await import('../agents/x402/transaction-store.js');
|
|
2995
|
+
const { aggregateMilestones, milestoneGoalEligibility } = await import('../agents/x402/milestone-settlement.js');
|
|
2996
|
+
const rec = await readTransaction(req.params.id);
|
|
2997
|
+
if (!rec)
|
|
2998
|
+
return res.status(404).json({ error: '交易不存在' });
|
|
2999
|
+
res.json({
|
|
3000
|
+
transaction: rec,
|
|
3001
|
+
milestones: rec.milestones?.length ? { list: rec.milestones, aggregate: aggregateMilestones(rec.milestones) } : null,
|
|
3002
|
+
dispute: rec.dispute || null,
|
|
3003
|
+
responsibility: rec.responsibility || null,
|
|
3004
|
+
goalEligibility: milestoneGoalEligibility(rec, { executionOk: rec.execution?.ok === true, goalCriteriaHit: rec.goalCriteriaMet === true }),
|
|
3005
|
+
replay: await replayTransaction(req.params.id),
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
catch (err) {
|
|
3009
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
app.get('/api/trace', async (req, res) => {
|
|
3013
|
+
try {
|
|
3014
|
+
const { listRuns } = await import('../agents/run-store.js');
|
|
3015
|
+
const { runToTraceJson } = await import('../agents/trace-export.js');
|
|
3016
|
+
const runs = await listRuns({ limit: Number(req.query.limit) || 20 });
|
|
3017
|
+
res.json({ runs: runs.map((r) => runToTraceJson(r)) });
|
|
3018
|
+
}
|
|
3019
|
+
catch (err) {
|
|
3020
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
app.get('/api/trace/:runId', async (req, res) => {
|
|
3024
|
+
try {
|
|
3025
|
+
const { readRun } = await import('../agents/run-store.js');
|
|
3026
|
+
const { runToTraceText, runToTraceJson } = await import('../agents/trace-export.js');
|
|
3027
|
+
const run = await readRun(req.params.runId);
|
|
3028
|
+
if (!run)
|
|
3029
|
+
return res.status(404).json({ error: 'run 不存在' });
|
|
3030
|
+
if (String(req.query.format || 'json') === 'text') {
|
|
3031
|
+
res.type('text/plain; charset=utf-8').send(runToTraceText(run));
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
res.json(runToTraceJson(run));
|
|
3035
|
+
}
|
|
3036
|
+
catch (err) {
|
|
3037
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
app.get('/api/p2p/info', async (_req, res) => {
|
|
3041
|
+
try {
|
|
3042
|
+
const { getLocalP2pInfo, formatP2pInfoJson } = await import('../agents/p2p-info.js');
|
|
3043
|
+
const info = await getLocalP2pInfo();
|
|
3044
|
+
res.type('application/json').send(formatP2pInfoJson(info));
|
|
3045
|
+
}
|
|
3046
|
+
catch (err) {
|
|
3047
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
// 2026-09-16 (2-F/2-H): 判据 (criteria) + 长期执行面板 API —— CLI/Web 读同一份 Goal 事实
|
|
3051
|
+
app.get('/api/goals/:id/criteria', async (req, res) => {
|
|
3052
|
+
try {
|
|
3053
|
+
const { longTermStatus } = await import('../agents/goal-criteria.js');
|
|
3054
|
+
const { readGoal } = await import('../agents/goal-store.js');
|
|
3055
|
+
const g = await readGoal(req.params.id);
|
|
3056
|
+
if (!g)
|
|
3057
|
+
return res.status(404).json({ error: 'goal 不存在' });
|
|
3058
|
+
const st = await longTermStatus(req.params.id);
|
|
3059
|
+
res.json({ goalId: g.goalId, objective: g.objective, status: g.status, successCriteria: g.successCriteria, completedCriteria: g.completedCriteria, criteriaSource: g.criteriaSource, criteriaConfirmed: g.criteriaConfirmed === true, criteriaVersion: g.criteriaVersion, proposedCriteria: g.proposedCriteria || null, unresolvedItems: g.unresolvedItems, evidence: (g.evidence || []).slice(-10), longTerm: st });
|
|
3060
|
+
}
|
|
3061
|
+
catch (err) {
|
|
3062
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3063
|
+
}
|
|
3064
|
+
});
|
|
3065
|
+
app.post('/api/goals/:id/criteria', async (req, res) => {
|
|
3066
|
+
try {
|
|
3067
|
+
const { confirmCriteria, proposeForGoal } = await import('../agents/goal-criteria.js');
|
|
3068
|
+
const { readGoal } = await import('../agents/goal-store.js');
|
|
3069
|
+
const g = await readGoal(req.params.id);
|
|
3070
|
+
if (!g)
|
|
3071
|
+
return res.status(404).json({ error: 'goal 不存在' });
|
|
3072
|
+
const criteria = Array.isArray(req.body?.criteria) ? req.body.criteria.map((c) => String(c)) : undefined;
|
|
3073
|
+
if (req.body?.propose === true) {
|
|
3074
|
+
const p = await proposeForGoal(req.params.id);
|
|
3075
|
+
return res.json({ ok: p.ok, proposed: p.criteria, needsHuman: !!p.needsHuman, reason: p.reason || null });
|
|
3076
|
+
}
|
|
3077
|
+
// 默认: 确认 (可同时改内容)
|
|
3078
|
+
const r = await confirmCriteria(req.params.id, { criteria, by: 'web' });
|
|
3079
|
+
res.status(r.ok ? 200 : 409).json({ ok: r.ok, reason: r.reason || null, criteria: r.goal?.successCriteria, criteriaVersion: r.goal?.criteriaVersion });
|
|
3080
|
+
}
|
|
3081
|
+
catch (err) {
|
|
3082
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
// 长期执行面板 (纯静态页, 直接打已有 API; 未 ready 时 chat 路由本来就 503)
|
|
3086
|
+
app.get('/goals', (_req, res) => {
|
|
3087
|
+
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 长期执行</title>
|
|
3088
|
+
<style>body{font-family:system-ui,-apple-system,"PingFang SC",sans-serif;max-width:1000px;margin:32px auto;padding:0 16px;line-height:1.55;color:#181818}
|
|
3089
|
+
h1{font-size:20px}h2{font-size:15px;margin-top:22px}table{border-collapse:collapse;width:100%;font-size:13px}th,td{border-bottom:1px solid #eee;padding:6px 8px;text-align:left;vertical-align:top}
|
|
3090
|
+
.ok{color:#0a7d32}.bad{color:#b00020}.dim{color:#666}button{font-size:12px;padding:4px 8px;margin-right:4px}code{background:#f6f6f6;padding:1px 4px;border-radius:4px}</style>
|
|
3091
|
+
<h1>长期执行面板</h1>
|
|
3092
|
+
<p class="dim">数据 = <code>/api/goals</code> · <code>/api/runs</code> · <code>/api/goals/:id/criteria</code> · <code>/api/supervisor</code> (与 CLI 同一份事实)</p>
|
|
3093
|
+
<div id="sup"></div>
|
|
3094
|
+
<h2>目标 (Goals)</h2><table id="goals"><thead><tr><th>goalId</th><th>状态</th><th>判据</th><th>下一动作</th><th>Runs</th><th>操作</th></tr></thead><tbody></tbody></table>
|
|
3095
|
+
<h2>运行 (Runs, 最近 20)</h2><table id="runs"><thead><tr><th>runId</th><th>goalId</th><th>状态</th><th>步数</th><th>恢复</th><th>操作</th></tr></thead><tbody></tbody></table>
|
|
3096
|
+
<pre id="log" class="dim"></pre>
|
|
3097
|
+
<script>
|
|
3098
|
+
const log = (m) => { document.getElementById('log').textContent = typeof m === 'string' ? m : JSON.stringify(m, null, 1); };
|
|
3099
|
+
async function j(u){ const r = await fetch(u); return await r.json(); }
|
|
3100
|
+
async function view(id){
|
|
3101
|
+
const c = await j('/api/goals/'+id+'/criteria');
|
|
3102
|
+
log(c);
|
|
3103
|
+
}
|
|
3104
|
+
async function act(id, what){
|
|
3105
|
+
if (what === 'confirm') { const r = await fetch('/api/goals/'+id+'/criteria',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({})}); log(await r.json()); }
|
|
3106
|
+
else if (what === 'propose') { const r = await fetch('/api/goals/'+id+'/criteria',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({propose:true})}); log(await r.json()); }
|
|
3107
|
+
else if (what === 'wake') { const r = await fetch('/api/goals/'+id+'/wake',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({force:true})}); log(await r.json()); }
|
|
3108
|
+
else if (what === 'resume') { const r = await fetch('/api/runs/'+id+'/resume',{method:'POST'}); log(await r.json()); }
|
|
3109
|
+
else if (what === 'pause') { const r = await fetch('/api/runs/'+id+'/pause',{method:'POST'}); log(await r.json()); }
|
|
3110
|
+
else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
|
|
3111
|
+
refresh();
|
|
3112
|
+
}
|
|
3113
|
+
async function req(id){
|
|
3114
|
+
const text = prompt('新要求 (原话逐字入档; 只影响后续 Run, 不改写已发生的历史。若说"撤销/别继续做了", 在跑的这一轮会在下一次循环停下):');
|
|
3115
|
+
if (!text) return;
|
|
3116
|
+
const r = await fetch('/api/goals/'+id+'/requirement',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text})});
|
|
3117
|
+
log(await r.json()); refresh();
|
|
3118
|
+
}
|
|
3119
|
+
async function refresh(){
|
|
3120
|
+
try {
|
|
3121
|
+
const sup = await j('/api/supervisor');
|
|
3122
|
+
document.getElementById('sup').innerHTML = '<b>Supervisor</b> owner='+(sup.state?.owner||'-')+' worker='+(sup.state?.workerId||'-')+' ticks='+(sup.ticks||0)+
|
|
3123
|
+
' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
|
|
3124
|
+
} catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
|
|
3125
|
+
const gs = await j('/api/goals');
|
|
3126
|
+
const labels = gs.visibleLabels || {};
|
|
3127
|
+
const visMap = {}; (gs.goals || []).forEach((g, i) => { visMap[g.goalId] = (gs.visible || [])[i]; });
|
|
3128
|
+
const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
|
|
3129
|
+
for (const g of (gs.goals || [])) {
|
|
3130
|
+
let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
|
|
3131
|
+
const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
|
|
3132
|
+
const tr = document.createElement('tr');
|
|
3133
|
+
const vis = (visMap[g.goalId]) || (g.continuation ? 'executing' : 'executing');
|
|
3134
|
+
const visZh = (labels[vis] && labels[vis].zh) || vis;
|
|
3135
|
+
const pend = (gs.pendingChanges||[])[(gs.goals||[]).indexOf(g)];
|
|
3136
|
+
const pendZh = pend ? ' <span class="bad">('+pend+' 条变更等你决定)</span>' : '';
|
|
3137
|
+
tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+visZh+pendZh+' <span class="dim">('+vis+')</span></td><td>'+crit+src+'</td><td class="dim">'+
|
|
3138
|
+
((g.continuation&&(g.continuation.nextAction||g.continuation.wakeReason))||'-')+'</td><td>'+((g.runs||[]).length)+'</td>'+
|
|
3139
|
+
'<td><button onclick="view(\\''+g.goalId+'\\')">详情</button><button onclick="act(\\''+g.goalId+'\\',\\'confirm\\')">确认判据</button><button onclick="act(\\''+g.goalId+'\\',\\'propose\\')">提候选</button><button onclick="act(\\''+g.goalId+'\\',\\'wake\\')">唤醒</button><button onclick="req(\\''+g.goalId+'\\')">新要求</button></td>';
|
|
3140
|
+
gtb.appendChild(tr);
|
|
3141
|
+
}
|
|
3142
|
+
const rs = await j('/api/runs');
|
|
3143
|
+
const rtb = document.querySelector('#runs tbody'); rtb.innerHTML = '';
|
|
3144
|
+
for (const r of (rs.runs || []).slice(-20).reverse()) {
|
|
3145
|
+
const tr = document.createElement('tr');
|
|
3146
|
+
tr.innerHTML = '<td><code>'+String(r.runId).slice(0,14)+'</code></td><td class="dim">'+(r.goalId||'-')+'</td><td>'+r.status+'</td><td>'+((r.steps||[]).length)+'</td><td>'+((r.recovery||[]).length)+'</td>'+
|
|
3147
|
+
'<td><button onclick="act(\\''+r.runId+'\\',\\'resume\\')">resume</button><button onclick="act(\\''+r.runId+'\\',\\'pause\\')">pause</button><button onclick="act(\\''+r.runId+'\\',\\'abort\\')">abort</button></td>';
|
|
3148
|
+
rtb.appendChild(tr);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
refresh(); setInterval(refresh, 5000);
|
|
3152
|
+
</script>`);
|
|
3153
|
+
});
|
|
3154
|
+
// 2026-09-16 (Phase 4): Onboard API —— 与 CLI 共用同一条执行器 (src/setup/onboard.ts)
|
|
3155
|
+
// 页面只负责"提交本步输入", 阶段判定/校验/真实验证/原子提交/门禁全在服务端同一份事实里。
|
|
3156
|
+
const setupState = async () => {
|
|
3157
|
+
const { evaluateSetup } = await import('../setup/setup-store.js');
|
|
3158
|
+
const { nextStepInfo } = await import('../setup/onboard.js');
|
|
3159
|
+
const ev = await evaluateSetup();
|
|
3160
|
+
const next = ev.gate === 'ready' ? null : await nextStepInfo(ev.state).catch(() => null);
|
|
3161
|
+
return { gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, readinessWhy: ev.state.readinessWhy,
|
|
3162
|
+
allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, checks: ev.state.checks,
|
|
3163
|
+
lastError: ev.state.lastError || null, actions: ev.state.actions, reasons: ev.reasons, nextStep: next };
|
|
3164
|
+
};
|
|
3165
|
+
app.get('/api/setup', async (_req, res) => {
|
|
3166
|
+
try {
|
|
3167
|
+
res.json(await setupState());
|
|
3168
|
+
}
|
|
3169
|
+
catch (err) {
|
|
3170
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
// 跑一步 (answers 按顺序喂给执行器; oneShot: 一步失败就返回, 不阻塞)
|
|
3174
|
+
const runSetupStep = async (req, res, mode) => {
|
|
3175
|
+
try {
|
|
3176
|
+
const { runOnboard, ScriptedIO } = await import('../setup/onboard.js');
|
|
3177
|
+
const answers = Array.isArray(req.body?.answers) ? req.body.answers.map((x) => String(x ?? '')) : [];
|
|
3178
|
+
const io = new ScriptedIO(answers);
|
|
3179
|
+
const r = await runOnboard({ mode, io, oneShot: true, targets: req.body?.targets });
|
|
3180
|
+
res.json({ ...(await setupState()), ok: r.ok, failedStage: r.failedStage || null, errorClass: r.errorClass || null,
|
|
3181
|
+
message: r.message || null, steps: r.steps, stepLog: io.log.slice(-40), summary: r.summary });
|
|
3182
|
+
}
|
|
3183
|
+
catch (err) {
|
|
3184
|
+
res.status(500).json({ ok: false, error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
3187
|
+
app.post('/api/setup/start', async (_req, res) => { try {
|
|
3188
|
+
res.json(await setupState());
|
|
3189
|
+
}
|
|
3190
|
+
catch (err) {
|
|
3191
|
+
res.status(500).json({ error: String(err) });
|
|
3192
|
+
} });
|
|
3193
|
+
app.post('/api/setup/step', async (req, res) => runSetupStep(req, res, 'resume'));
|
|
3194
|
+
app.post('/api/setup/resume', async (req, res) => runSetupStep(req, res, 'resume'));
|
|
3195
|
+
app.post('/api/setup/test', async (req, res) => runSetupStep(req, res, 'test'));
|
|
3196
|
+
app.post('/api/setup/repair', async (req, res) => runSetupStep(req, res, 'repair'));
|
|
3197
|
+
app.post('/api/setup/reconfigure', async (req, res) => runSetupStep(req, res, 'reconfigure'));
|
|
3198
|
+
app.post('/api/setup/commit', async (_req, res) => {
|
|
3199
|
+
try {
|
|
3200
|
+
const { refreshSetupState } = await import('../setup/setup-store.js');
|
|
3201
|
+
const ev = await refreshSetupState({});
|
|
3202
|
+
const { describeSetup } = await import('../setup/setup-store.js');
|
|
3203
|
+
res.json({ ...(await setupState()), ok: ev.gate === 'ready', summary: describeSetup(ev) });
|
|
3204
|
+
}
|
|
3205
|
+
catch (err) {
|
|
3206
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
app.post('/api/setup/identity', async (req, res) => runSetupStep({ body: { answers: [req.body?.name] } }, res, 'resume'));
|
|
3210
|
+
app.post('/api/setup/provider', async (req, res) => runSetupStep({ body: { answers: [req.body?.provider] } }, res, 'resume'));
|
|
3211
|
+
// 首启页面 (零依赖, 直接打这些 API; 未 ready 时所有对话路由都是 503)
|
|
3212
|
+
app.get('/setup', (_req, res) => {
|
|
3213
|
+
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 初始化</title>
|
|
3214
|
+
<style>body{font-family:system-ui,-apple-system,"PingFang SC",sans-serif;max-width:760px;margin:40px auto;padding:0 16px;line-height:1.6}
|
|
3215
|
+
h1{font-size:20px}pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
|
|
3216
|
+
.ok{color:#0a7d32}.bad{color:#b00020}input,select,button{font-size:15px;padding:8px;margin:4px 0}
|
|
3217
|
+
.row{margin:10px 0}small{color:#666}</style>
|
|
3218
|
+
<h1>Bolloon 初始化</h1>
|
|
3219
|
+
<div id="state">加载中…</div>
|
|
3220
|
+
<div class="row" id="form"></div>
|
|
3221
|
+
<div class="row"><button onclick="run('step')">提交这一步</button>
|
|
3222
|
+
<button onclick="run('test')">重新测试连通性/运行时</button>
|
|
3223
|
+
<button onclick="run('repair')">修复/迁移配置</button>
|
|
3224
|
+
<button onclick="fetchState()">刷新</button></div>
|
|
3225
|
+
<pre id="log"></pre>
|
|
3226
|
+
<script>
|
|
3227
|
+
async function fetchState(){
|
|
3228
|
+
const r = await fetch('/api/setup'); const s = await r.json();
|
|
3229
|
+
const ready = s.gate==='ready';
|
|
3230
|
+
document.getElementById('state').innerHTML =
|
|
3231
|
+
'<b>阶段</b>: '+s.stage+' <b>门禁</b>: <span class="'+(ready?'ok':'bad')+'">'+s.gate+'</span><br>'+
|
|
3232
|
+
'<b>就绪度</b>: basic '+s.readiness.basic+' · agent '+s.readiness.agent+' · durable '+s.readiness.durable+' · network '+s.readiness.network+'<br>'+
|
|
3233
|
+
'<small>已完成: '+(s.completed.join(' → ')||'(无)')+'</small><br>'+
|
|
3234
|
+
'<small>下一步: '+(s.actions[0]||'')+'</small>'+
|
|
3235
|
+
(s.lastError?'<br><small class="bad">上次错误 ['+s.lastError.errorClass+'] '+s.lastError.message+'</small>':'');
|
|
3236
|
+
const f = document.getElementById('form');
|
|
3237
|
+
const n = s.nextStep;
|
|
3238
|
+
if(!n){ f.innerHTML='<b class="ok">✅ 已就绪, 可以进入对话</b>'; window.currentNeeds=null; return; }
|
|
3239
|
+
window.currentNeeds=n.needs;
|
|
3240
|
+
if(n.needs==='provider'){
|
|
3241
|
+
f.innerHTML='<div>'+n.question+'</div><select id="v">'+ (n.choices||[]).map(c=>'<option value="'+c.value+'">'+c.label+(c.hint?' — '+c.hint:'')+'</option>').join('') +'</select>';
|
|
3242
|
+
} else if(n.needs==='credential'){
|
|
3243
|
+
f.innerHTML='<div>'+n.question+'</div><input id="v" type="password" placeholder="API key (不会回显)" autocomplete="off">';
|
|
3244
|
+
} else if(n.needs==='none'){
|
|
3245
|
+
f.innerHTML='<div>'+n.question+' (点上面按钮执行)</div>';
|
|
3246
|
+
} else {
|
|
3247
|
+
f.innerHTML='<div>'+n.question+'</div><input id="v" value="'+(n.defaultValue||'')+'">';
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
async function run(ep){
|
|
3251
|
+
const v=document.getElementById('v'); const n=window.currentNeeds;
|
|
3252
|
+
const answers = (n==='credential'||n==='name'||n==='model'||n==='provider') && v ? [v.value] : [];
|
|
3253
|
+
if(v) v.value=''; // key 立即从 DOM 里清掉
|
|
3254
|
+
const r = await fetch('/api/setup/'+ep,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({answers})});
|
|
3255
|
+
const j = await r.json();
|
|
3256
|
+
document.getElementById('log').textContent = (j.log||[]).join('\n') + '\n' + (j.summary||'');
|
|
3257
|
+
fetchState();
|
|
3258
|
+
}
|
|
3259
|
+
fetchState();
|
|
3260
|
+
</script>`);
|
|
3261
|
+
});
|
|
3262
|
+
// 2026-09-16 (M1/M3): 初始化状态 —— CLI 与 Web 读同一份事实 (setup-state.json + 各领域 store)
|
|
3263
|
+
app.get('/api/setup', async (_req, res) => {
|
|
3264
|
+
try {
|
|
3265
|
+
const { evaluateSetup, describeSetup, resolveBolloonHome } = await import('../setup/setup-store.js');
|
|
3266
|
+
const ev = await evaluateSetup({ light: false });
|
|
3267
|
+
res.json({ gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, lastError: ev.state.lastError, reasons: ev.reasons, nextActions: ev.nextActions, summary: describeSetup(ev), bolloonHome: resolveBolloonHome() });
|
|
3268
|
+
}
|
|
3269
|
+
catch (err) {
|
|
3270
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3273
|
+
// 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
|
|
3274
|
+
app.get('/api/skills', async (_req, res) => {
|
|
3275
|
+
try {
|
|
3276
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
3277
|
+
const sm = getSkillsManager();
|
|
3278
|
+
const skills = await sm.view();
|
|
3279
|
+
res.json({ count: skills.length, registryPath: (await import('../agents/skills-manager.js')).registryPath(), skills, health: await sm.health() });
|
|
3280
|
+
}
|
|
3281
|
+
catch (err) {
|
|
3282
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
app.get('/api/skills/health', async (_req, res) => {
|
|
3286
|
+
try {
|
|
3287
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
3288
|
+
res.json(await getSkillsManager().health());
|
|
3289
|
+
}
|
|
3290
|
+
catch (err) {
|
|
3291
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
app.get('/api/skills/:name', async (req, res) => {
|
|
3295
|
+
try {
|
|
3296
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
3297
|
+
const s = await getSkillsManager().inspect(String(req.params.name));
|
|
3298
|
+
if (!s) {
|
|
3299
|
+
res.status(404).json({ error: `没有这个技能: ${req.params.name}` });
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
res.json({ skill: s });
|
|
3303
|
+
}
|
|
3304
|
+
catch (err) {
|
|
3305
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
app.post('/api/skills/import', async (req, res) => {
|
|
3309
|
+
try {
|
|
3310
|
+
const ref = String(req.body?.ref || '').trim();
|
|
3311
|
+
if (!ref) {
|
|
3312
|
+
res.status(400).json({ error: '要 body {ref: "bolloon://skill/<cid>" | "ipfs://<cid>" | "<cid>"}' });
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
3316
|
+
const r = await getSkillsManager().import(ref, { force: !!req.body?.force });
|
|
3317
|
+
if (!r.ok) {
|
|
3318
|
+
res.status(409).json({ error: r.error });
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
res.json({ ok: true, name: r.name, version: r.version, skill: r.skill });
|
|
3322
|
+
}
|
|
3323
|
+
catch (err) {
|
|
3324
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3325
|
+
}
|
|
3326
|
+
});
|
|
3327
|
+
// enable / disable / approve / validate / quarantine (Express 5 不支持内联正则, 显式写开)
|
|
3328
|
+
for (const action of ['enable', 'disable', 'approve', 'validate', 'quarantine']) {
|
|
3329
|
+
app.post(`/api/skills/:name/${action}`, async (req, res) => {
|
|
3330
|
+
try {
|
|
3331
|
+
const { getSkillsManager } = await import('../agents/skills-manager.js');
|
|
3332
|
+
const sm = getSkillsManager();
|
|
3333
|
+
const name = String(req.params.name);
|
|
3334
|
+
const r = action === 'approve' ? await sm.approve(name, 'web')
|
|
3335
|
+
: action === 'quarantine' ? await sm.quarantine(name, String(req.body?.reason || 'web 手动隔离'))
|
|
3336
|
+
: action === 'enable' ? await sm.enable(name)
|
|
3337
|
+
: action === 'disable' ? await sm.disable(name)
|
|
3338
|
+
: await sm.validate(name);
|
|
3339
|
+
if (!r.ok && !r.skill) {
|
|
3340
|
+
res.status(409).json({ error: r.reason || r.issues || '操作未完成' });
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
res.json({ ok: r.ok, name, action, skill: r.skill, issues: r.issues });
|
|
3344
|
+
}
|
|
3345
|
+
catch (err) {
|
|
3346
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3347
|
+
}
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3350
|
+
// 2026-09-16 (M2-B): 长期执行层诊断 + 手动推进 —— 与 CLI /supervise 读同一份持久化记录
|
|
3351
|
+
app.get('/api/supervisor', async (_req, res) => {
|
|
3352
|
+
try {
|
|
3353
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3354
|
+
const { readSupervisorState, supervisorStatePath } = await import('../agents/supervisor-host.js');
|
|
3355
|
+
const { wakeReport, listRunnableGoals } = await import('../agents/goal-store.js');
|
|
3356
|
+
const { goalVisibleState } = await import('../agents/goal-flywheel-wiring.js');
|
|
3357
|
+
const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
|
|
3358
|
+
res.json({
|
|
3359
|
+
supervisor: getSupervisor().status(),
|
|
3360
|
+
/** 宿主身份/心跳 (谁在跑、跑到哪、上次有没有好好停) */
|
|
3361
|
+
host: await readSupervisorState(),
|
|
3362
|
+
hostStatePath: supervisorStatePath(),
|
|
3363
|
+
wake: await wakeReport(),
|
|
3364
|
+
// 2026-09-25 (飞轮接线 §7): 用户可见态 (六类) —— 界面不得自己造第二套映射
|
|
3365
|
+
wakeVisible: await Promise.all((await wakeReport()).map(async (r) => {
|
|
3366
|
+
try {
|
|
3367
|
+
return { goalId: r.goalId, visible: await goalVisibleState({ goalId: r.goalId }) };
|
|
3368
|
+
}
|
|
3369
|
+
catch {
|
|
3370
|
+
return { goalId: r.goalId, visible: r.visible };
|
|
3371
|
+
}
|
|
3372
|
+
})),
|
|
3373
|
+
runnable: runnable.map((g) => ({ goalId: g.goalId, objective: g.objective, status: g.status })),
|
|
3374
|
+
skipped,
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
catch (err) {
|
|
3378
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3381
|
+
// 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
|
|
3382
|
+
app.post('/api/supervisor/tick', async (_req, res) => {
|
|
3383
|
+
if (!(await setupGate(res)))
|
|
3384
|
+
return;
|
|
3385
|
+
try {
|
|
3386
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3387
|
+
const report = await getSupervisor().tickOnce();
|
|
3388
|
+
res.json({ ok: true, report });
|
|
3389
|
+
}
|
|
3390
|
+
catch (err) {
|
|
3391
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3394
|
+
// 2026-09-16 (2-E 第 4 类): 外部事件到达 → 唤醒在等它的 Goal (不重发已成功的请求)
|
|
3395
|
+
app.post('/api/goals/:goalId/wake', async (req, res) => {
|
|
3396
|
+
const goalId = String(req.params.goalId);
|
|
3397
|
+
try {
|
|
3398
|
+
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3399
|
+
const { readGoal, setContinuation } = await import('../agents/goal-store.js');
|
|
3400
|
+
const g = await readGoal(goalId);
|
|
3401
|
+
if (!g) {
|
|
3402
|
+
res.status(404).json({ error: `goal 不存在: ${goalId}` });
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
const woke = await getSupervisor().notifyExternal(goalId);
|
|
3406
|
+
if (!woke) {
|
|
3407
|
+
// 没在等外部事件也要如实回答 (可能它其实在等人/在跑), 但允许显式加急
|
|
3408
|
+
if (req.body?.force) {
|
|
3409
|
+
await setContinuation(goalId, { wakeReason: 'active', autoContinue: true, wakeAt: undefined, needsExternal: undefined });
|
|
3410
|
+
res.json({ ok: true, goalId, woke: false, forced: true, note: `原状态 ${g.status} (不在等外部事件), 已按 force 加急` });
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
res.status(409).json({ error: `Goal 不在等外部事件 (status=${g.status}, wakeReason=${g.continuation?.wakeReason || '无'})`, force_hint: '加 body {"force":true} 可强制加急' });
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
res.json({ ok: true, goalId, woke: true, note: '已唤醒: 下一次 Supervisor tick 会推进它' });
|
|
3417
|
+
}
|
|
3418
|
+
catch (err) {
|
|
3419
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
// 2026-09-25 (飞轮接线 P4 / M4 深化): 新要求注入 —— 原话逐字入档 + 分诊 + 版本; 只影响**后续** Run。
|
|
3423
|
+
// 当前 Run 不被历史改写 (规则 4): 本路由只写 Goal 上的变更记录与下一 Run 的指令。
|
|
3424
|
+
// ★ M4: 这里还要回答"**正在跑的 Goal** 会发生什么" —— 撤销 → 在跑的这一轮必须停
|
|
3425
|
+
// (走既有外部控制面原语 `setRunStatus`, 与 /api/runs/:id/abort 同一条路; 执行器只在需要时才注入,
|
|
3426
|
+
// 判定本身在 change 接缝里, 本路由不自己判)。事实 (在跑的 Run / 在跑的子 Agent) 由本路由读出来喂进去。
|
|
3427
|
+
app.post('/api/goals/:goalId/requirement', async (req, res) => {
|
|
3428
|
+
const goalId = String(req.params.goalId);
|
|
3429
|
+
try {
|
|
3430
|
+
const { readGoal } = await import('../agents/goal-store.js');
|
|
3431
|
+
const { readRun } = await import('../agents/run-store.js');
|
|
3432
|
+
const { flywheelSeams } = await import('../agents/goal-flywheel-wiring.js');
|
|
3433
|
+
const { isRefusal } = await import('../agents/goal-flywheel/wiring/index.js');
|
|
3434
|
+
const { USER_VISIBLE_STATE_LABELS } = await import('../agents/goal-flywheel/types.js');
|
|
3435
|
+
const text = String(req.body?.text ?? '').trim();
|
|
3436
|
+
if (!text) {
|
|
3437
|
+
res.status(400).json({ error: '缺少 text (新要求的原话)' });
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3440
|
+
const goal = await readGoal(goalId);
|
|
3441
|
+
if (!goal) {
|
|
3442
|
+
res.status(404).json({ error: `goal 不存在: ${goalId}` });
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
// 在跑的 Run 的事实 (读不到记录 = 没有可停的对象; 绝不因为"读不到"去停一个不存在的 Run)
|
|
3446
|
+
const runRec = goal.currentRunId ? await readRun(goal.currentRunId).catch(() => null) : null;
|
|
3447
|
+
// 在跑的子 Agent = 已签合同还没回报的那些 (规则 5: 它们必须先收到变更版本)
|
|
3448
|
+
const liveWorkIds = (goal.continuation?.pendingReports ?? []).map((p) => p.workId).filter(Boolean);
|
|
3449
|
+
const now = new Date().toISOString();
|
|
3450
|
+
const seam = flywheelSeams().change;
|
|
3451
|
+
const view = await seam.ingestChange({
|
|
3452
|
+
goalId,
|
|
3453
|
+
instruction: text,
|
|
3454
|
+
source: 'user',
|
|
3455
|
+
recordedBy: 'web',
|
|
3456
|
+
now,
|
|
3457
|
+
runningRun: runRec ? { runId: runRec.runId, status: String(runRec.status) } : null,
|
|
3458
|
+
liveWorkIds,
|
|
3459
|
+
caller: 'human',
|
|
3460
|
+
});
|
|
3461
|
+
if (isRefusal(view)) {
|
|
3462
|
+
res.status(409).json({ ok: false, refused: true, rule: view.rule, reason: view.reason, goalId });
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
// 计划说要停, 而接缝自己没执行器 (M0 尚未注入 stopRunningRun) → 入口按计划把停落到真 Run 上:
|
|
3466
|
+
// 执行器 = 既有的 setRunStatus (与 /api/runs/:id/abort 同一个原语, 不新造停止通道)。
|
|
3467
|
+
let boundary = view.runBoundary;
|
|
3468
|
+
if (boundary.action === 'stop_running_run' && !boundary.stopped) {
|
|
3469
|
+
const { setRunStatus } = await import('../agents/run-store.js');
|
|
3470
|
+
boundary = await seam.applyRunBoundary({
|
|
3471
|
+
plan: view.plan,
|
|
3472
|
+
now,
|
|
3473
|
+
stop: async (i) => {
|
|
3474
|
+
const r = await setRunStatus(i.runId, i.runStatus, { error: `变更注入 (${i.goalId}): ${String(i.reason).slice(0, 120)}` });
|
|
3475
|
+
return { ok: !!r.ok, reason: r.reason || '' };
|
|
3476
|
+
},
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
const bumped = view.plan.outcome === 'next_run' && view.request.kind === 'success_criteria_change'
|
|
3480
|
+
? view.plan.criteriaVersion
|
|
3481
|
+
: null;
|
|
3482
|
+
res.json({
|
|
3483
|
+
ok: true,
|
|
3484
|
+
goalId,
|
|
3485
|
+
changeId: view.request.changeId,
|
|
3486
|
+
kind: view.request.kind,
|
|
3487
|
+
outcome: view.plan.outcome,
|
|
3488
|
+
status: view.request.status,
|
|
3489
|
+
/** 只有"用户 + 改判据"才 +1 (规则 3); 其余一律 null */
|
|
3490
|
+
criteriaVersionBumpedTo: bumped,
|
|
3491
|
+
criteriaVersion: view.plan.criteriaVersion,
|
|
3492
|
+
/** 下一 Run 真的会读到的指令 (写进 continuation.nextAction) */
|
|
3493
|
+
nextRunDirective: view.plan.nextRunDirective,
|
|
3494
|
+
/** ★ 正在运行的 Goal: 在跑的这一轮停不停 (停不了也会如实说) */
|
|
3495
|
+
runBoundary: boundary,
|
|
3496
|
+
appliesToFutureRunsOnly: view.plan.appliesToFutureRunsOnly,
|
|
3497
|
+
/** 规则 5: 在跑的子 Agent 各拿一份下发内容, 且必须先 ack */
|
|
3498
|
+
childDelivery: {
|
|
3499
|
+
count: view.childDelivery.directives.length,
|
|
3500
|
+
workIds: view.childDelivery.workIds,
|
|
3501
|
+
mustAck: view.childDelivery.mustAck,
|
|
3502
|
+
scopeMissing: view.childDelivery.scopeMissing,
|
|
3503
|
+
blocked: view.childDelivery.blocked,
|
|
3504
|
+
note: view.childDelivery.note,
|
|
3505
|
+
},
|
|
3506
|
+
/** 用户可见态 (六类之一): 有待拍板的变更 → "需要你决定"; null = 基础态读不到 (不编) */
|
|
3507
|
+
visibleState: view.visibleState,
|
|
3508
|
+
visibleZh: view.visibleState ? (USER_VISIBLE_STATE_LABELS[view.visibleState]?.zh || view.visibleState) : null,
|
|
3509
|
+
visibleReason: view.visibleReason,
|
|
3510
|
+
/** 一个 kind 装不下多个意图 → 已入档但不生效, 需拆开重提 */
|
|
3511
|
+
needsDisambiguation: view.needsDisambiguation,
|
|
3512
|
+
/** 流程留痕 (接收→记录→判影响→摘要→评估当前Run→必要时停→重规划→写入下一Run) */
|
|
3513
|
+
steps: view.plan.steps,
|
|
3514
|
+
changeArchive: {
|
|
3515
|
+
authoritative: 'goal.goalChanges (Goal 记录 = 权威; 界面/下一次 Run 读它)',
|
|
3516
|
+
mirror: '~/.bolloon/goal-changes/<goalId>.json (镜像, 由接线层写)',
|
|
3517
|
+
},
|
|
3518
|
+
note: view.plan.outcome === 'rejected'
|
|
3519
|
+
? '未生效 (见 outcome): 已如实入档, 不会偷偷生效'
|
|
3520
|
+
: '已生效: 只影响后续 Run, 已发生的 Run 记录不被改写',
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
catch (err) {
|
|
3524
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
// 2026-09-16 (M5): 运行列表 —— Web 与 CLI 读同一份 run 事实 (~/.bolloon/runs)
|
|
3528
|
+
app.get('/api/runs', async (req, res) => {
|
|
3529
|
+
try {
|
|
3530
|
+
const { listRuns, formatRunLine } = await import('../agents/run-store.js');
|
|
3531
|
+
const status = String(req.query?.status || '').trim();
|
|
3532
|
+
const limit = Number(req.query?.limit || 20);
|
|
3533
|
+
const runs = await listRuns({ status: status ? status : undefined, limit });
|
|
3534
|
+
res.json({
|
|
3535
|
+
count: runs.length,
|
|
3536
|
+
runs,
|
|
3537
|
+
lines: runs.map(formatRunLine),
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
catch (err) {
|
|
3541
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3542
|
+
}
|
|
3543
|
+
});
|
|
3544
|
+
// 2026-09-16 (M5): 单个 run + 停止原因/checkpoint/最近步骤/recovery/harness 事件 (两端同一份事实)
|
|
3545
|
+
app.get('/api/runs/:runId', async (req, res) => {
|
|
3546
|
+
try {
|
|
3547
|
+
const { readRun } = await import('../agents/run-store.js');
|
|
3548
|
+
const run = await readRun(String(req.params.runId));
|
|
3549
|
+
if (!run) {
|
|
3550
|
+
res.status(404).json({ error: `run 不存在: ${req.params.runId}` });
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
const goal = run.goalId ? await (await import('../agents/goal-store.js')).readGoal(run.goalId) : null;
|
|
3554
|
+
res.json({ run, goal, checkpoint: run.checkpoint, steps: run.steps, recovery: run.recovery, harness: run.harness || [] });
|
|
3555
|
+
}
|
|
3556
|
+
catch (err) {
|
|
3557
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3558
|
+
}
|
|
3559
|
+
});
|
|
3560
|
+
// 2026-09-16 (M5): 继续 interrupted/stalled/paused/needs_human/awaiting_external 的运行 —— 真从 checkpoint 继续
|
|
3561
|
+
app.post('/api/runs/:runId/resume', async (req, res) => {
|
|
3562
|
+
const runId = String(req.params.runId);
|
|
3563
|
+
try {
|
|
3564
|
+
const { readRun, RESUMABLE_STATUSES } = await import('../agents/run-store.js');
|
|
3565
|
+
const run = await readRun(runId);
|
|
3566
|
+
if (!run) {
|
|
3567
|
+
res.status(404).json({ error: `run 不存在: ${runId}` });
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
// 先做只读校验: 不可恢复的状态明确拒绝 (不假装"已开始"), 避免前端以为在跑
|
|
3571
|
+
if (!RESUMABLE_STATUSES.includes(run.status)) {
|
|
3572
|
+
res.status(409).json({ error: `状态 ${run.status} 不可恢复 (可恢复: ${RESUMABLE_STATUSES.join('/')})` });
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
if (!run.channelId) {
|
|
3576
|
+
res.status(400).json({ error: '该 run 没有 channelId, 无法在 web 端恢复 (请在 CLI 用 /resume)' });
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const agent = await getAgentForChannel(run.channelId);
|
|
3580
|
+
if (!agent?.resumeRun) {
|
|
3581
|
+
res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
// 异步执行: resume 可能跑几分钟; 状态从 /api/runs/:runId 读 (recovering → running → done/failed)
|
|
3585
|
+
void agent.resumeRun(runId).then((r) => {
|
|
3586
|
+
if (!r?.ok)
|
|
3587
|
+
console.warn(`[runs] resume ${runId} 失败:`, r?.reason);
|
|
3588
|
+
}).catch((err) => console.warn(`[runs] resume ${runId} 异常:`, err?.message));
|
|
3589
|
+
res.json({ ok: true, accepted: true, runId, note: '已开始恢复; 状态请读 GET /api/runs/:runId' });
|
|
3590
|
+
}
|
|
3591
|
+
catch (err) {
|
|
3592
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
// 2026-09-16 (M5): 人工批准 —— needs_human 的运行经人确认后继续 (= 带批准的 resume)
|
|
3596
|
+
app.post('/api/runs/:runId/approve', async (req, res) => {
|
|
3597
|
+
const runId = String(req.params.runId);
|
|
3598
|
+
try {
|
|
3599
|
+
const { readRun, recordRecovery } = await import('../agents/run-store.js');
|
|
3600
|
+
const run = await readRun(runId);
|
|
3601
|
+
if (!run) {
|
|
3602
|
+
res.status(404).json({ error: `run 不存在: ${runId}` });
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
if (run.status !== 'needs_human') {
|
|
3606
|
+
res.status(409).json({ error: `只有 needs_human 的运行需要批准 (当前 ${run.status})` });
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
await recordRecovery(runId, { errorClass: run.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' });
|
|
3610
|
+
if (!run.channelId) {
|
|
3611
|
+
res.status(400).json({ error: '该 run 没有 channelId, 请在 CLI 用 /approve' });
|
|
3612
|
+
return;
|
|
3613
|
+
}
|
|
3614
|
+
const agent = await getAgentForChannel(run.channelId, run.channelId, run.agentId, {});
|
|
3615
|
+
if (!agent?.resumeRun) {
|
|
3616
|
+
res.status(409).json({ error: '该 channel 的 agent 不支持 resumeRun' });
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
void agent.resumeRun(runId).catch((err) => console.warn(`[runs] approve ${runId} 异常:`, err?.message));
|
|
3620
|
+
res.json({ ok: true, accepted: true, runId, approved: true });
|
|
3621
|
+
}
|
|
3622
|
+
catch (err) {
|
|
3623
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
// 2026-09-16 (M5): 暂停 / 中止 —— 只改 run 状态; 运行中的 agent 会在下一次循环检查时如实停下
|
|
3627
|
+
// (Express 5 的 path-to-regexp 不再支持 `:action(a|b)` 内联正则, 所以两条写开)
|
|
3628
|
+
for (const [suffix, to] of [['pause', 'paused'], ['abort', 'aborted']]) {
|
|
3629
|
+
app.post(`/api/runs/:runId/${suffix}`, async (req, res) => {
|
|
3630
|
+
const runId = String(req.params.runId);
|
|
3631
|
+
try {
|
|
3632
|
+
const { setRunStatus } = await import('../agents/run-store.js');
|
|
3633
|
+
const r = await setRunStatus(runId, to, { error: `外部请求 (${suffix})` });
|
|
3634
|
+
if (!r.ok) {
|
|
3635
|
+
res.status(409).json({ error: r.reason || '状态迁移被拒绝' });
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
res.json({ ok: true, runId, status: to, note: '运行中的 agent 会在下一轮循环检查时停下 (轮内不打断)' });
|
|
3639
|
+
}
|
|
3640
|
+
catch (err) {
|
|
3641
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
// 2026-09-16 (M2): 目标 (Goal) —— 目标事实来源; runId → goalId → objective/successCriteria 反查链
|
|
3646
|
+
app.get('/api/goals', async (req, res) => {
|
|
3647
|
+
try {
|
|
3648
|
+
const { listGoals, formatGoalLine } = await import('../agents/goal-store.js');
|
|
3649
|
+
const status = String(req.query?.status || '').trim();
|
|
3650
|
+
const goals = await listGoals({ status: status ? status : undefined, limit: Number(req.query?.limit || 30) });
|
|
3651
|
+
// 2026-09-25 (飞轮接线): 界面只认 `toUserVisibleState` 映射的**六类**用户可见态 ——
|
|
3652
|
+
// 内部状态 (retry_wait / stalled / recovering) 不再直接上屏。
|
|
3653
|
+
// ★ M4: 再过一遍**变更驱动**的覆盖 —— 有待拍板的新要求 (`needs_approval`) 时必须是
|
|
3654
|
+
// "需要你决定", 否则界面一直显示"正在执行", 没人知道有东西卡在等人 (只升不降)。
|
|
3655
|
+
const { toUserVisibleState } = await import('../agents/goal-flywheel/work-monitor.js');
|
|
3656
|
+
const { changeVisibleState } = await import('../agents/goal-flywheel/wiring/change.js');
|
|
3657
|
+
const { collectWorkBlocks } = await import('../agents/goal-flywheel-wiring.js');
|
|
3658
|
+
const { USER_VISIBLE_STATE_LABELS } = await import('../agents/goal-flywheel/types.js');
|
|
3659
|
+
const visible = await Promise.all(goals.map(async (g) => {
|
|
3660
|
+
try {
|
|
3661
|
+
const base = toUserVisibleState(g.continuation ? { ...g.continuation } : null, await collectWorkBlocks({ goalId: g.goalId }), null,
|
|
3662
|
+
// ★ M5-⑥ (2026-09-25): 界面认 Goal **本体**的终态 —— 完不成这一步的话, 一份落后的
|
|
3663
|
+
// continuation 就能把"已完成"的目标显示成"正在执行" (界面比系统更乐观)。
|
|
3664
|
+
String(g.status ?? '') || null);
|
|
3665
|
+
// 这里 base 一定读得到 (读不到就是上面的 catch) → 覆盖结果为 null 时回落 base 本身
|
|
3666
|
+
// (不是编一个态: base 是这一步刚刚读出来的真态)
|
|
3667
|
+
return changeVisibleState({ base, changes: g.goalChanges ?? [] }).state ?? base;
|
|
3668
|
+
}
|
|
3669
|
+
catch {
|
|
3670
|
+
return 'executing';
|
|
3671
|
+
}
|
|
3672
|
+
}));
|
|
3673
|
+
res.json({
|
|
3674
|
+
count: goals.length, goals, lines: goals.map(formatGoalLine),
|
|
3675
|
+
visible, visibleZh: visible.map((v) => USER_VISIBLE_STATE_LABELS[v]?.zh || v), visibleLabels: USER_VISIBLE_STATE_LABELS,
|
|
3676
|
+
/** 待拍板的变更 (界面/CLI 用同一份事实说清"你在等什么") */
|
|
3677
|
+
pendingChanges: goals.map((g) => (g.goalChanges ?? []).filter((c) => c.status === 'needs_approval' || c.status === 'triaged').length),
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
catch (err) {
|
|
3681
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3684
|
+
app.get('/api/goals/:goalId', async (req, res) => {
|
|
3685
|
+
try {
|
|
3686
|
+
const { readGoal, evaluateGoalCompletion } = await import('../agents/goal-store.js');
|
|
3687
|
+
const goal = await readGoal(String(req.params.goalId));
|
|
3688
|
+
if (!goal) {
|
|
3689
|
+
res.status(404).json({ error: `goal 不存在: ${req.params.goalId}` });
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
res.json({ goal, completion: evaluateGoalCompletion(goal) });
|
|
3693
|
+
}
|
|
3694
|
+
catch (err) {
|
|
3695
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
3696
|
+
}
|
|
3697
|
+
});
|
|
2835
3698
|
// 2026-08-13 (Phase E1): Agent 服务 Registry — 发现层 (OrbitDB 去中心化 + 本地 fallback)
|
|
2836
3699
|
app.get('/api/registry', async (_req, res) => {
|
|
2837
3700
|
try {
|
|
@@ -3889,7 +4752,29 @@ ${goalDesc}
|
|
|
3889
4752
|
console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
|
|
3890
4753
|
});
|
|
3891
4754
|
});
|
|
4755
|
+
// 2026-09-16 (M4): agent 执行硬门禁 —— 未 ready 时这些路由 503 + 结构化初始化状态 (Web 只显示初始化页)
|
|
4756
|
+
const setupGate = async (res) => {
|
|
4757
|
+
try {
|
|
4758
|
+
const { getSetupGateCached } = await import('../setup/setup-store.js');
|
|
4759
|
+
const { gate, state } = await getSetupGateCached();
|
|
4760
|
+
if (gate === 'ready')
|
|
4761
|
+
return true;
|
|
4762
|
+
res.status(503).json({
|
|
4763
|
+
error: `初始化未就绪 (${gate}, 阶段 ${state.stage}) — 请先完成初始化`,
|
|
4764
|
+
gate, stage: state.stage, readiness: state.readiness,
|
|
4765
|
+
nextActions: state.actions, lastError: state.lastError || null,
|
|
4766
|
+
});
|
|
4767
|
+
return false;
|
|
4768
|
+
}
|
|
4769
|
+
catch (err) {
|
|
4770
|
+
// fail-closed: 连门禁都读不出来 → 不放行
|
|
4771
|
+
res.status(503).json({ error: `初始化状态不可读 (fail-closed): ${String(err?.message || err).slice(0, 160)}`, gate: 'blocked' });
|
|
4772
|
+
return false;
|
|
4773
|
+
}
|
|
4774
|
+
};
|
|
3892
4775
|
app.post('/message', async (req, res) => {
|
|
4776
|
+
if (!(await setupGate(res)))
|
|
4777
|
+
return;
|
|
3893
4778
|
const { text, channelId, channelDid, attachments } = req.body;
|
|
3894
4779
|
if (!text) {
|
|
3895
4780
|
return res.status(400).json({ error: 'No text provided' });
|
|
@@ -6082,8 +6967,15 @@ ${goalDesc}
|
|
|
6082
6967
|
});
|
|
6083
6968
|
// 2026-07-06: Task Queue API 抽到 ./routes-tasks.ts
|
|
6084
6969
|
registerTaskRoutes(app, { broadcast, getAgentForChannel });
|
|
6970
|
+
// 2026-09-19: 社交身份 API (DID + 已验证联系方式 + 联系能力 + 权限 + 证据)
|
|
6971
|
+
// 只返回脱敏值; 明文联系方式不进 Run/prompt/Git
|
|
6972
|
+
registerContactRoutes(app, {});
|
|
6973
|
+
// 2026-09-25: 手机端「任务协作」API (入群 / 发公告 / 看飞轮进度)
|
|
6974
|
+
// 手机只做 输入·展示待发内容·设备签名; 执行回到桌面, 调用与 CLI 同一批函数 (同一份 store 与协议)
|
|
6975
|
+
registerMobileTaskRoutes(app, {});
|
|
6085
6976
|
// 2026-09-13: 微支付信息服务 (x402) — 发布 / 402 收款 / 买方代付 / 验真
|
|
6086
6977
|
registerX402InfoRoutes(app);
|
|
6978
|
+
registerOnchainTradeRoutes(app);
|
|
6087
6979
|
// 2026-07-06: LLM/Video/Audio 配置路由抽到 ./routes-llm-config.ts
|
|
6088
6980
|
registerLlmConfigRoutes(app);
|
|
6089
6981
|
// 2026-07-22: 外部编码智能体 (codex/claude-code/opencode/openclaw/hermes/实验 API)
|
|
@@ -7501,6 +8393,35 @@ ${goalDesc}
|
|
|
7501
8393
|
console.warn('[24h] Failed to load heartbeat modules:', err);
|
|
7502
8394
|
}
|
|
7503
8395
|
// 健康检查端点
|
|
8396
|
+
// 2026-09-18: 网络脉冲公开只读投影 (给 bolloon-UI 网关页; 无认证, 短缓存, 只有聚合)
|
|
8397
|
+
// 永远不返回原始事件 / registry / DID / peerId / IP / 钱包 / 任务正文。
|
|
8398
|
+
app.get('/api/public/network/progress', async (req, res) => {
|
|
8399
|
+
try {
|
|
8400
|
+
const np = await import('../agents/network-pulse.js');
|
|
8401
|
+
const snap = await np.getNetworkPulse({});
|
|
8402
|
+
// agent_sites = 本节点**显式发布**的公开指针 (IPNS); 没发布就是空数组, 不猜
|
|
8403
|
+
const body = JSON.stringify({ ...snap, status: np.snapshotStatus(snap), agent_sites: np.readAgentSites() });
|
|
8404
|
+
const { createHash } = await import('crypto');
|
|
8405
|
+
const etag = '"' + createHash('sha256').update(body).digest('hex').slice(0, 32) + '"';
|
|
8406
|
+
res.setHeader('Cache-Control', 'public, max-age=15, stale-while-revalidate=15');
|
|
8407
|
+
res.setHeader('ETag', etag);
|
|
8408
|
+
if (req.headers['if-none-match'] === etag) {
|
|
8409
|
+
res.status(304).end();
|
|
8410
|
+
return;
|
|
8411
|
+
}
|
|
8412
|
+
res.type('application/json').send(body);
|
|
8413
|
+
}
|
|
8414
|
+
catch (e) {
|
|
8415
|
+
// 观察层不可用 → 如实 unavailable (不是 500 空体, 也不是"网络为空")
|
|
8416
|
+
try {
|
|
8417
|
+
const np = await import('../agents/network-pulse.js');
|
|
8418
|
+
res.type('application/json').send(JSON.stringify(await np.getNetworkPulse({ unavailable: true })));
|
|
8419
|
+
}
|
|
8420
|
+
catch {
|
|
8421
|
+
res.status(503).json({ status: 'unavailable', error: 'network pulse unavailable' });
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
});
|
|
7504
8425
|
app.get('/api/health', async (req, res) => {
|
|
7505
8426
|
try {
|
|
7506
8427
|
if (!healthMonitor) {
|