@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.
Files changed (35) hide show
  1. package/dist/agents/execution-supervisor.js +87 -2
  2. package/dist/agents/external-events.js +162 -0
  3. package/dist/agents/goal-criteria.js +124 -0
  4. package/dist/agents/goal-store.js +79 -4
  5. package/dist/agents/p2p-info.js +175 -0
  6. package/dist/agents/pi-sdk.js +39 -0
  7. package/dist/agents/run-store.js +15 -0
  8. package/dist/agents/skill-readiness.js +133 -0
  9. package/dist/agents/skill-supervisor-link.js +70 -0
  10. package/dist/agents/skills-manager.js +282 -0
  11. package/dist/agents/trace-export.js +125 -0
  12. package/dist/agents/write-staging.js +12 -4
  13. package/dist/agents/x402/goal-run-bridge.js +105 -0
  14. package/dist/agents/x402/milestone-settlement.js +150 -0
  15. package/dist/agents/x402/paid-info-store.js +66 -12
  16. package/dist/agents/x402/payment-recovery.js +290 -0
  17. package/dist/agents/x402/resource-contract.js +473 -0
  18. package/dist/agents/x402/settlement-state.js +378 -0
  19. package/dist/agents/x402/trade.js +257 -0
  20. package/dist/agents/x402/transaction-protocol.js +99 -0
  21. package/dist/agents/x402/transaction-store.js +350 -0
  22. package/dist/cli/setup-wizard.js +96 -127
  23. package/dist/cli-entry.js +74 -0
  24. package/dist/electron/first-run.js +33 -2
  25. package/dist/electron-build/electron/first-run.js +35 -2
  26. package/dist/electron-build/electron/first-run.js.map +1 -1
  27. package/dist/index.js +224 -4
  28. package/dist/llm/config-store.js +35 -4
  29. package/dist/network/agent-network.js +10 -0
  30. package/dist/network/goal-event-bridge.js +57 -0
  31. package/dist/setup/onboard.js +549 -0
  32. package/dist/setup/setup-store.js +592 -0
  33. package/dist/web/routes-x402-info.js +1 -0
  34. package/dist/web/server.js +330 -1
  35. package/package.json +1 -1
@@ -1679,6 +1679,18 @@ export async function createWebServer(port = 3000, options = {}) {
1679
1679
  };
1680
1680
  return { ok: true, runner, kind: 'web' };
1681
1681
  };
1682
+ // 2026-09-16 (M4): 初始化未就绪 → Supervisor 只诊断 (不解析执行器, 不执行任何 Goal)
1683
+ let setupReadyForSupervisor = false;
1684
+ try {
1685
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
1686
+ const g = await getSetupGateCached();
1687
+ setupReadyForSupervisor = g.gate === 'ready';
1688
+ if (!setupReadyForSupervisor)
1689
+ console.warn(`[supervisor] 初始化未就绪 (${g.gate}, 阶段 ${g.state.stage}) → 只诊断, 不执行 Goal`);
1690
+ }
1691
+ catch {
1692
+ console.warn('[supervisor] 初始化状态不可读 → 只诊断, 不执行 Goal');
1693
+ }
1682
1694
  const sup = getSupervisor({
1683
1695
  tickIntervalMs: tickMs,
1684
1696
  leaseTtlMs: leaseMs,
@@ -1688,7 +1700,7 @@ export async function createWebServer(port = 3000, options = {}) {
1688
1700
  broadcast({ type: 'supervisor', ...e });
1689
1701
  }
1690
1702
  catch { /* UI 广播失败不影响调度 */ } },
1691
- resolver: resolver,
1703
+ resolver: setupReadyForSupervisor ? resolver : undefined,
1692
1704
  });
1693
1705
  // 宿主层: 跨进程单 tick 互斥 + 宿主身份/心跳落盘 + 优雅停止
1694
1706
  const host = await runSupervisorHost({
@@ -2937,6 +2949,299 @@ ${goalDesc}
2937
2949
  }
2938
2950
  });
2939
2951
  // 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
2952
+ // 2026-09-18: 智能体工具执行轨迹 + 本机 P2P 信息 (与小工具/名片交换用, 结构化)
2953
+ // 2026-09-18 (Phase 4): 交易审计 — 里程碑 / 争议 / 责任 / 事件链 一眼可查
2954
+ app.get('/api/x402/transactions', async (_req, res) => {
2955
+ try {
2956
+ const { listTransactions } = await import('../agents/x402/transaction-store.js');
2957
+ const { aggregateMilestones } = await import('../agents/x402/milestone-settlement.js');
2958
+ const txs = await listTransactions();
2959
+ res.json({
2960
+ count: txs.length,
2961
+ transactions: txs.map((t) => ({
2962
+ transactionId: t.transactionId, status: t.status, settlementFact: t.settlementFact,
2963
+ itemId: t.itemId, amount: t.amount, currency: t.currency, network: t.network,
2964
+ paymentMode: t.paymentMode, chainSettled: t.chainSettled === true, txHash: t.txHash,
2965
+ milestones: t.milestones?.length ? aggregateMilestones(t.milestones) : null,
2966
+ disputed: !!t.dispute, disputeResolved: t.dispute?.resolution?.decision || null,
2967
+ responsibility: t.responsibility?.type || null,
2968
+ partial: t.settlementFact === 'partially_settled',
2969
+ })),
2970
+ });
2971
+ }
2972
+ catch (err) {
2973
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
2974
+ }
2975
+ });
2976
+ app.get('/api/x402/transactions/:id', async (req, res) => {
2977
+ try {
2978
+ const { readTransaction, replayTransaction } = await import('../agents/x402/transaction-store.js');
2979
+ const { aggregateMilestones, milestoneGoalEligibility } = await import('../agents/x402/milestone-settlement.js');
2980
+ const rec = await readTransaction(req.params.id);
2981
+ if (!rec)
2982
+ return res.status(404).json({ error: '交易不存在' });
2983
+ res.json({
2984
+ transaction: rec,
2985
+ milestones: rec.milestones?.length ? { list: rec.milestones, aggregate: aggregateMilestones(rec.milestones) } : null,
2986
+ dispute: rec.dispute || null,
2987
+ responsibility: rec.responsibility || null,
2988
+ goalEligibility: milestoneGoalEligibility(rec, { executionOk: rec.execution?.ok === true, goalCriteriaHit: rec.goalCriteriaMet === true }),
2989
+ replay: await replayTransaction(req.params.id),
2990
+ });
2991
+ }
2992
+ catch (err) {
2993
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
2994
+ }
2995
+ });
2996
+ app.get('/api/trace', async (req, res) => {
2997
+ try {
2998
+ const { listRuns } = await import('../agents/run-store.js');
2999
+ const { runToTraceJson } = await import('../agents/trace-export.js');
3000
+ const runs = await listRuns({ limit: Number(req.query.limit) || 20 });
3001
+ res.json({ runs: runs.map((r) => runToTraceJson(r)) });
3002
+ }
3003
+ catch (err) {
3004
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3005
+ }
3006
+ });
3007
+ app.get('/api/trace/:runId', async (req, res) => {
3008
+ try {
3009
+ const { readRun } = await import('../agents/run-store.js');
3010
+ const { runToTraceText, runToTraceJson } = await import('../agents/trace-export.js');
3011
+ const run = await readRun(req.params.runId);
3012
+ if (!run)
3013
+ return res.status(404).json({ error: 'run 不存在' });
3014
+ if (String(req.query.format || 'json') === 'text') {
3015
+ res.type('text/plain; charset=utf-8').send(runToTraceText(run));
3016
+ return;
3017
+ }
3018
+ res.json(runToTraceJson(run));
3019
+ }
3020
+ catch (err) {
3021
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3022
+ }
3023
+ });
3024
+ app.get('/api/p2p/info', async (_req, res) => {
3025
+ try {
3026
+ const { getLocalP2pInfo, formatP2pInfoJson } = await import('../agents/p2p-info.js');
3027
+ const info = await getLocalP2pInfo();
3028
+ res.type('application/json').send(formatP2pInfoJson(info));
3029
+ }
3030
+ catch (err) {
3031
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3032
+ }
3033
+ });
3034
+ // 2026-09-16 (2-F/2-H): 判据 (criteria) + 长期执行面板 API —— CLI/Web 读同一份 Goal 事实
3035
+ app.get('/api/goals/:id/criteria', async (req, res) => {
3036
+ try {
3037
+ const { longTermStatus } = await import('../agents/goal-criteria.js');
3038
+ const { readGoal } = await import('../agents/goal-store.js');
3039
+ const g = await readGoal(req.params.id);
3040
+ if (!g)
3041
+ return res.status(404).json({ error: 'goal 不存在' });
3042
+ const st = await longTermStatus(req.params.id);
3043
+ 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 });
3044
+ }
3045
+ catch (err) {
3046
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3047
+ }
3048
+ });
3049
+ app.post('/api/goals/:id/criteria', async (req, res) => {
3050
+ try {
3051
+ const { confirmCriteria, proposeForGoal } = await import('../agents/goal-criteria.js');
3052
+ const { readGoal } = await import('../agents/goal-store.js');
3053
+ const g = await readGoal(req.params.id);
3054
+ if (!g)
3055
+ return res.status(404).json({ error: 'goal 不存在' });
3056
+ const criteria = Array.isArray(req.body?.criteria) ? req.body.criteria.map((c) => String(c)) : undefined;
3057
+ if (req.body?.propose === true) {
3058
+ const p = await proposeForGoal(req.params.id);
3059
+ return res.json({ ok: p.ok, proposed: p.criteria, needsHuman: !!p.needsHuman, reason: p.reason || null });
3060
+ }
3061
+ // 默认: 确认 (可同时改内容)
3062
+ const r = await confirmCriteria(req.params.id, { criteria, by: 'web' });
3063
+ res.status(r.ok ? 200 : 409).json({ ok: r.ok, reason: r.reason || null, criteria: r.goal?.successCriteria, criteriaVersion: r.goal?.criteriaVersion });
3064
+ }
3065
+ catch (err) {
3066
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
3067
+ }
3068
+ });
3069
+ // 长期执行面板 (纯静态页, 直接打已有 API; 未 ready 时 chat 路由本来就 503)
3070
+ app.get('/goals', (_req, res) => {
3071
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 长期执行</title>
3072
+ <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}
3073
+ 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}
3074
+ .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>
3075
+ <h1>长期执行面板</h1>
3076
+ <p class="dim">数据 = <code>/api/goals</code> · <code>/api/runs</code> · <code>/api/goals/:id/criteria</code> · <code>/api/supervisor</code> (与 CLI 同一份事实)</p>
3077
+ <div id="sup"></div>
3078
+ <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>
3079
+ <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>
3080
+ <pre id="log" class="dim"></pre>
3081
+ <script>
3082
+ const log = (m) => { document.getElementById('log').textContent = typeof m === 'string' ? m : JSON.stringify(m, null, 1); };
3083
+ async function j(u){ const r = await fetch(u); return await r.json(); }
3084
+ async function view(id){
3085
+ const c = await j('/api/goals/'+id+'/criteria');
3086
+ log(c);
3087
+ }
3088
+ async function act(id, what){
3089
+ 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()); }
3090
+ 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()); }
3091
+ 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()); }
3092
+ else if (what === 'resume') { const r = await fetch('/api/runs/'+id+'/resume',{method:'POST'}); log(await r.json()); }
3093
+ else if (what === 'pause') { const r = await fetch('/api/runs/'+id+'/pause',{method:'POST'}); log(await r.json()); }
3094
+ else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
3095
+ refresh();
3096
+ }
3097
+ async function refresh(){
3098
+ try {
3099
+ const sup = await j('/api/supervisor');
3100
+ document.getElementById('sup').innerHTML = '<b>Supervisor</b> owner='+(sup.state?.owner||'-')+' worker='+(sup.state?.workerId||'-')+' ticks='+(sup.ticks||0)+
3101
+ ' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
3102
+ } catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
3103
+ const gs = await j('/api/goals');
3104
+ const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
3105
+ for (const g of (gs.goals || [])) {
3106
+ let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
3107
+ const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
3108
+ const tr = document.createElement('tr');
3109
+ tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+g.status+'</td><td>'+crit+src+'</td><td class="dim">'+
3110
+ ((g.continuation&&(g.continuation.nextAction||g.continuation.wakeReason))||'-')+'</td><td>'+((g.runs||[]).length)+'</td>'+
3111
+ '<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></td>';
3112
+ gtb.appendChild(tr);
3113
+ }
3114
+ const rs = await j('/api/runs');
3115
+ const rtb = document.querySelector('#runs tbody'); rtb.innerHTML = '';
3116
+ for (const r of (rs.runs || []).slice(-20).reverse()) {
3117
+ const tr = document.createElement('tr');
3118
+ 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>'+
3119
+ '<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>';
3120
+ rtb.appendChild(tr);
3121
+ }
3122
+ }
3123
+ refresh(); setInterval(refresh, 5000);
3124
+ </script>`);
3125
+ });
3126
+ // 2026-09-16 (Phase 4): Onboard API —— 与 CLI 共用同一条执行器 (src/setup/onboard.ts)
3127
+ // 页面只负责"提交本步输入", 阶段判定/校验/真实验证/原子提交/门禁全在服务端同一份事实里。
3128
+ const setupState = async () => {
3129
+ const { evaluateSetup } = await import('../setup/setup-store.js');
3130
+ const { nextStepInfo } = await import('../setup/onboard.js');
3131
+ const ev = await evaluateSetup();
3132
+ const next = ev.gate === 'ready' ? null : await nextStepInfo(ev.state).catch(() => null);
3133
+ return { gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, readinessWhy: ev.state.readinessWhy,
3134
+ allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, checks: ev.state.checks,
3135
+ lastError: ev.state.lastError || null, actions: ev.state.actions, reasons: ev.reasons, nextStep: next };
3136
+ };
3137
+ app.get('/api/setup', async (_req, res) => {
3138
+ try {
3139
+ res.json(await setupState());
3140
+ }
3141
+ catch (err) {
3142
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3143
+ }
3144
+ });
3145
+ // 跑一步 (answers 按顺序喂给执行器; oneShot: 一步失败就返回, 不阻塞)
3146
+ const runSetupStep = async (req, res, mode) => {
3147
+ try {
3148
+ const { runOnboard, ScriptedIO } = await import('../setup/onboard.js');
3149
+ const answers = Array.isArray(req.body?.answers) ? req.body.answers.map((x) => String(x ?? '')) : [];
3150
+ const io = new ScriptedIO(answers);
3151
+ const r = await runOnboard({ mode, io, oneShot: true, targets: req.body?.targets });
3152
+ res.json({ ...(await setupState()), ok: r.ok, failedStage: r.failedStage || null, errorClass: r.errorClass || null,
3153
+ message: r.message || null, steps: r.steps, stepLog: io.log.slice(-40), summary: r.summary });
3154
+ }
3155
+ catch (err) {
3156
+ res.status(500).json({ ok: false, error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3157
+ }
3158
+ };
3159
+ app.post('/api/setup/start', async (_req, res) => { try {
3160
+ res.json(await setupState());
3161
+ }
3162
+ catch (err) {
3163
+ res.status(500).json({ error: String(err) });
3164
+ } });
3165
+ app.post('/api/setup/step', async (req, res) => runSetupStep(req, res, 'resume'));
3166
+ app.post('/api/setup/resume', async (req, res) => runSetupStep(req, res, 'resume'));
3167
+ app.post('/api/setup/test', async (req, res) => runSetupStep(req, res, 'test'));
3168
+ app.post('/api/setup/repair', async (req, res) => runSetupStep(req, res, 'repair'));
3169
+ app.post('/api/setup/reconfigure', async (req, res) => runSetupStep(req, res, 'reconfigure'));
3170
+ app.post('/api/setup/commit', async (_req, res) => {
3171
+ try {
3172
+ const { refreshSetupState } = await import('../setup/setup-store.js');
3173
+ const ev = await refreshSetupState({});
3174
+ const { describeSetup } = await import('../setup/setup-store.js');
3175
+ res.json({ ...(await setupState()), ok: ev.gate === 'ready', summary: describeSetup(ev) });
3176
+ }
3177
+ catch (err) {
3178
+ res.status(500).json({ ok: false, error: String(err) });
3179
+ }
3180
+ });
3181
+ app.post('/api/setup/identity', async (req, res) => runSetupStep({ body: { answers: [req.body?.name] } }, res, 'resume'));
3182
+ app.post('/api/setup/provider', async (req, res) => runSetupStep({ body: { answers: [req.body?.provider] } }, res, 'resume'));
3183
+ // 首启页面 (零依赖, 直接打这些 API; 未 ready 时所有对话路由都是 503)
3184
+ app.get('/setup', (_req, res) => {
3185
+ res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 初始化</title>
3186
+ <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}
3187
+ h1{font-size:20px}pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
3188
+ .ok{color:#0a7d32}.bad{color:#b00020}input,select,button{font-size:15px;padding:8px;margin:4px 0}
3189
+ .row{margin:10px 0}small{color:#666}</style>
3190
+ <h1>Bolloon 初始化</h1>
3191
+ <div id="state">加载中…</div>
3192
+ <div class="row" id="form"></div>
3193
+ <div class="row"><button onclick="run('step')">提交这一步</button>
3194
+ <button onclick="run('test')">重新测试连通性/运行时</button>
3195
+ <button onclick="run('repair')">修复/迁移配置</button>
3196
+ <button onclick="fetchState()">刷新</button></div>
3197
+ <pre id="log"></pre>
3198
+ <script>
3199
+ async function fetchState(){
3200
+ const r = await fetch('/api/setup'); const s = await r.json();
3201
+ const ready = s.gate==='ready';
3202
+ document.getElementById('state').innerHTML =
3203
+ '<b>阶段</b>: '+s.stage+' &nbsp; <b>门禁</b>: <span class="'+(ready?'ok':'bad')+'">'+s.gate+'</span><br>'+
3204
+ '<b>就绪度</b>: basic '+s.readiness.basic+' · agent '+s.readiness.agent+' · durable '+s.readiness.durable+' · network '+s.readiness.network+'<br>'+
3205
+ '<small>已完成: '+(s.completed.join(' → ')||'(无)')+'</small><br>'+
3206
+ '<small>下一步: '+(s.actions[0]||'')+'</small>'+
3207
+ (s.lastError?'<br><small class="bad">上次错误 ['+s.lastError.errorClass+'] '+s.lastError.message+'</small>':'');
3208
+ const f = document.getElementById('form');
3209
+ const n = s.nextStep;
3210
+ if(!n){ f.innerHTML='<b class="ok">✅ 已就绪, 可以进入对话</b>'; window.currentNeeds=null; return; }
3211
+ window.currentNeeds=n.needs;
3212
+ if(n.needs==='provider'){
3213
+ 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>';
3214
+ } else if(n.needs==='credential'){
3215
+ f.innerHTML='<div>'+n.question+'</div><input id="v" type="password" placeholder="API key (不会回显)" autocomplete="off">';
3216
+ } else if(n.needs==='none'){
3217
+ f.innerHTML='<div>'+n.question+' (点上面按钮执行)</div>';
3218
+ } else {
3219
+ f.innerHTML='<div>'+n.question+'</div><input id="v" value="'+(n.defaultValue||'')+'">';
3220
+ }
3221
+ }
3222
+ async function run(ep){
3223
+ const v=document.getElementById('v'); const n=window.currentNeeds;
3224
+ const answers = (n==='credential'||n==='name'||n==='model'||n==='provider') && v ? [v.value] : [];
3225
+ if(v) v.value=''; // key 立即从 DOM 里清掉
3226
+ const r = await fetch('/api/setup/'+ep,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({answers})});
3227
+ const j = await r.json();
3228
+ document.getElementById('log').textContent = (j.log||[]).join('\n') + '\n' + (j.summary||'');
3229
+ fetchState();
3230
+ }
3231
+ fetchState();
3232
+ </script>`);
3233
+ });
3234
+ // 2026-09-16 (M1/M3): 初始化状态 —— CLI 与 Web 读同一份事实 (setup-state.json + 各领域 store)
3235
+ app.get('/api/setup', async (_req, res) => {
3236
+ try {
3237
+ const { evaluateSetup, describeSetup, resolveBolloonHome } = await import('../setup/setup-store.js');
3238
+ const ev = await evaluateSetup({ light: false });
3239
+ 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() });
3240
+ }
3241
+ catch (err) {
3242
+ res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
3243
+ }
3244
+ });
2940
3245
  // 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
2941
3246
  app.get('/api/skills', async (_req, res) => {
2942
3247
  try {
@@ -3037,6 +3342,8 @@ ${goalDesc}
3037
3342
  });
3038
3343
  // 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
3039
3344
  app.post('/api/supervisor/tick', async (_req, res) => {
3345
+ if (!(await setupGate(res)))
3346
+ return;
3040
3347
  try {
3041
3348
  const { getSupervisor } = await import('../agents/execution-supervisor.js');
3042
3349
  const report = await getSupervisor().tickOnce();
@@ -4275,7 +4582,29 @@ ${goalDesc}
4275
4582
  console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
4276
4583
  });
4277
4584
  });
4585
+ // 2026-09-16 (M4): agent 执行硬门禁 —— 未 ready 时这些路由 503 + 结构化初始化状态 (Web 只显示初始化页)
4586
+ const setupGate = async (res) => {
4587
+ try {
4588
+ const { getSetupGateCached } = await import('../setup/setup-store.js');
4589
+ const { gate, state } = await getSetupGateCached();
4590
+ if (gate === 'ready')
4591
+ return true;
4592
+ res.status(503).json({
4593
+ error: `初始化未就绪 (${gate}, 阶段 ${state.stage}) — 请先完成初始化`,
4594
+ gate, stage: state.stage, readiness: state.readiness,
4595
+ nextActions: state.actions, lastError: state.lastError || null,
4596
+ });
4597
+ return false;
4598
+ }
4599
+ catch (err) {
4600
+ // fail-closed: 连门禁都读不出来 → 不放行
4601
+ res.status(503).json({ error: `初始化状态不可读 (fail-closed): ${String(err?.message || err).slice(0, 160)}`, gate: 'blocked' });
4602
+ return false;
4603
+ }
4604
+ };
4278
4605
  app.post('/message', async (req, res) => {
4606
+ if (!(await setupGate(res)))
4607
+ return;
4279
4608
  const { text, channelId, channelDid, attachments } = req.body;
4280
4609
  if (!text) {
4281
4610
  return res.status(400).json({ error: 'No text provided' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.4.25",
3
+ "version": "0.4.27",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",