@bolloon/bolloon-agent 0.4.25 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/execution-supervisor.js +57 -2
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +79 -4
- package/dist/agents/pi-sdk.js +39 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +282 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/electron/first-run.js +33 -2
- package/dist/electron-build/electron/first-run.js +35 -2
- package/dist/electron-build/electron/first-run.js.map +1 -1
- package/dist/index.js +121 -4
- package/dist/llm/config-store.js +35 -4
- package/dist/network/agent-network.js +10 -0
- package/dist/network/goal-event-bridge.js +57 -0
- package/dist/setup/onboard.js +549 -0
- package/dist/setup/setup-store.js +592 -0
- package/dist/web/server.js +248 -1
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -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,217 @@ ${goalDesc}
|
|
|
2937
2949
|
}
|
|
2938
2950
|
});
|
|
2939
2951
|
// 2026-09-16: 运行记录 (持久化 harness) — 当前 + 历史 agent 运行。跨重载可读。
|
|
2952
|
+
// 2026-09-16 (2-F/2-H): 判据 (criteria) + 长期执行面板 API —— CLI/Web 读同一份 Goal 事实
|
|
2953
|
+
app.get('/api/goals/:id/criteria', async (req, res) => {
|
|
2954
|
+
try {
|
|
2955
|
+
const { longTermStatus } = await import('../agents/goal-criteria.js');
|
|
2956
|
+
const { readGoal } = await import('../agents/goal-store.js');
|
|
2957
|
+
const g = await readGoal(req.params.id);
|
|
2958
|
+
if (!g)
|
|
2959
|
+
return res.status(404).json({ error: 'goal 不存在' });
|
|
2960
|
+
const st = await longTermStatus(req.params.id);
|
|
2961
|
+
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 });
|
|
2962
|
+
}
|
|
2963
|
+
catch (err) {
|
|
2964
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
app.post('/api/goals/:id/criteria', async (req, res) => {
|
|
2968
|
+
try {
|
|
2969
|
+
const { confirmCriteria, proposeForGoal } = await import('../agents/goal-criteria.js');
|
|
2970
|
+
const { readGoal } = await import('../agents/goal-store.js');
|
|
2971
|
+
const g = await readGoal(req.params.id);
|
|
2972
|
+
if (!g)
|
|
2973
|
+
return res.status(404).json({ error: 'goal 不存在' });
|
|
2974
|
+
const criteria = Array.isArray(req.body?.criteria) ? req.body.criteria.map((c) => String(c)) : undefined;
|
|
2975
|
+
if (req.body?.propose === true) {
|
|
2976
|
+
const p = await proposeForGoal(req.params.id);
|
|
2977
|
+
return res.json({ ok: p.ok, proposed: p.criteria, needsHuman: !!p.needsHuman, reason: p.reason || null });
|
|
2978
|
+
}
|
|
2979
|
+
// 默认: 确认 (可同时改内容)
|
|
2980
|
+
const r = await confirmCriteria(req.params.id, { criteria, by: 'web' });
|
|
2981
|
+
res.status(r.ok ? 200 : 409).json({ ok: r.ok, reason: r.reason || null, criteria: r.goal?.successCriteria, criteriaVersion: r.goal?.criteriaVersion });
|
|
2982
|
+
}
|
|
2983
|
+
catch (err) {
|
|
2984
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200) });
|
|
2985
|
+
}
|
|
2986
|
+
});
|
|
2987
|
+
// 长期执行面板 (纯静态页, 直接打已有 API; 未 ready 时 chat 路由本来就 503)
|
|
2988
|
+
app.get('/goals', (_req, res) => {
|
|
2989
|
+
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 长期执行</title>
|
|
2990
|
+
<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}
|
|
2991
|
+
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}
|
|
2992
|
+
.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>
|
|
2993
|
+
<h1>长期执行面板</h1>
|
|
2994
|
+
<p class="dim">数据 = <code>/api/goals</code> · <code>/api/runs</code> · <code>/api/goals/:id/criteria</code> · <code>/api/supervisor</code> (与 CLI 同一份事实)</p>
|
|
2995
|
+
<div id="sup"></div>
|
|
2996
|
+
<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>
|
|
2997
|
+
<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>
|
|
2998
|
+
<pre id="log" class="dim"></pre>
|
|
2999
|
+
<script>
|
|
3000
|
+
const log = (m) => { document.getElementById('log').textContent = typeof m === 'string' ? m : JSON.stringify(m, null, 1); };
|
|
3001
|
+
async function j(u){ const r = await fetch(u); return await r.json(); }
|
|
3002
|
+
async function view(id){
|
|
3003
|
+
const c = await j('/api/goals/'+id+'/criteria');
|
|
3004
|
+
log(c);
|
|
3005
|
+
}
|
|
3006
|
+
async function act(id, what){
|
|
3007
|
+
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()); }
|
|
3008
|
+
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()); }
|
|
3009
|
+
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()); }
|
|
3010
|
+
else if (what === 'resume') { const r = await fetch('/api/runs/'+id+'/resume',{method:'POST'}); log(await r.json()); }
|
|
3011
|
+
else if (what === 'pause') { const r = await fetch('/api/runs/'+id+'/pause',{method:'POST'}); log(await r.json()); }
|
|
3012
|
+
else if (what === 'abort') { const r = await fetch('/api/runs/'+id+'/abort',{method:'POST'}); log(await r.json()); }
|
|
3013
|
+
refresh();
|
|
3014
|
+
}
|
|
3015
|
+
async function refresh(){
|
|
3016
|
+
try {
|
|
3017
|
+
const sup = await j('/api/supervisor');
|
|
3018
|
+
document.getElementById('sup').innerHTML = '<b>Supervisor</b> owner='+(sup.state?.owner||'-')+' worker='+(sup.state?.workerId||'-')+' ticks='+(sup.ticks||0)+
|
|
3019
|
+
' <span class="dim">lease='+(sup.leaseTtlMs||'-')+'ms tick='+(sup.tickIntervalMs||'-')+'ms</span>';
|
|
3020
|
+
} catch(e){ document.getElementById('sup').textContent = 'supervisor 不可用: '+e; }
|
|
3021
|
+
const gs = await j('/api/goals');
|
|
3022
|
+
const gtb = document.querySelector('#goals tbody'); gtb.innerHTML = '';
|
|
3023
|
+
for (const g of (gs.goals || [])) {
|
|
3024
|
+
let crit = g.successCriteria?.length ? (g.completedCriteria?.length||0)+'/'+g.successCriteria.length : '<span class="bad">无判据</span>';
|
|
3025
|
+
const src = g.criteriaSource ? ' <span class="dim">('+g.criteriaSource+(g.criteriaConfirmed?'✓':'?')+')</span>' : '';
|
|
3026
|
+
const tr = document.createElement('tr');
|
|
3027
|
+
tr.innerHTML = '<td><code>'+g.goalId+'</code></td><td>'+g.status+'</td><td>'+crit+src+'</td><td class="dim">'+
|
|
3028
|
+
((g.continuation&&(g.continuation.nextAction||g.continuation.wakeReason))||'-')+'</td><td>'+((g.runs||[]).length)+'</td>'+
|
|
3029
|
+
'<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>';
|
|
3030
|
+
gtb.appendChild(tr);
|
|
3031
|
+
}
|
|
3032
|
+
const rs = await j('/api/runs');
|
|
3033
|
+
const rtb = document.querySelector('#runs tbody'); rtb.innerHTML = '';
|
|
3034
|
+
for (const r of (rs.runs || []).slice(-20).reverse()) {
|
|
3035
|
+
const tr = document.createElement('tr');
|
|
3036
|
+
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>'+
|
|
3037
|
+
'<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>';
|
|
3038
|
+
rtb.appendChild(tr);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
refresh(); setInterval(refresh, 5000);
|
|
3042
|
+
</script>`);
|
|
3043
|
+
});
|
|
3044
|
+
// 2026-09-16 (Phase 4): Onboard API —— 与 CLI 共用同一条执行器 (src/setup/onboard.ts)
|
|
3045
|
+
// 页面只负责"提交本步输入", 阶段判定/校验/真实验证/原子提交/门禁全在服务端同一份事实里。
|
|
3046
|
+
const setupState = async () => {
|
|
3047
|
+
const { evaluateSetup } = await import('../setup/setup-store.js');
|
|
3048
|
+
const { nextStepInfo } = await import('../setup/onboard.js');
|
|
3049
|
+
const ev = await evaluateSetup();
|
|
3050
|
+
const next = ev.gate === 'ready' ? null : await nextStepInfo(ev.state).catch(() => null);
|
|
3051
|
+
return { gate: ev.gate, stage: ev.state.stage, readiness: ev.state.readiness, readinessWhy: ev.state.readinessWhy,
|
|
3052
|
+
allow: ev.state.allow, completed: ev.state.completed, inputs: ev.state.inputs, checks: ev.state.checks,
|
|
3053
|
+
lastError: ev.state.lastError || null, actions: ev.state.actions, reasons: ev.reasons, nextStep: next };
|
|
3054
|
+
};
|
|
3055
|
+
app.get('/api/setup', async (_req, res) => {
|
|
3056
|
+
try {
|
|
3057
|
+
res.json(await setupState());
|
|
3058
|
+
}
|
|
3059
|
+
catch (err) {
|
|
3060
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
// 跑一步 (answers 按顺序喂给执行器; oneShot: 一步失败就返回, 不阻塞)
|
|
3064
|
+
const runSetupStep = async (req, res, mode) => {
|
|
3065
|
+
try {
|
|
3066
|
+
const { runOnboard, ScriptedIO } = await import('../setup/onboard.js');
|
|
3067
|
+
const answers = Array.isArray(req.body?.answers) ? req.body.answers.map((x) => String(x ?? '')) : [];
|
|
3068
|
+
const io = new ScriptedIO(answers);
|
|
3069
|
+
const r = await runOnboard({ mode, io, oneShot: true, targets: req.body?.targets });
|
|
3070
|
+
res.json({ ...(await setupState()), ok: r.ok, failedStage: r.failedStage || null, errorClass: r.errorClass || null,
|
|
3071
|
+
message: r.message || null, steps: r.steps, stepLog: io.log.slice(-40), summary: r.summary });
|
|
3072
|
+
}
|
|
3073
|
+
catch (err) {
|
|
3074
|
+
res.status(500).json({ ok: false, error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
app.post('/api/setup/start', async (_req, res) => { try {
|
|
3078
|
+
res.json(await setupState());
|
|
3079
|
+
}
|
|
3080
|
+
catch (err) {
|
|
3081
|
+
res.status(500).json({ error: String(err) });
|
|
3082
|
+
} });
|
|
3083
|
+
app.post('/api/setup/step', async (req, res) => runSetupStep(req, res, 'resume'));
|
|
3084
|
+
app.post('/api/setup/resume', async (req, res) => runSetupStep(req, res, 'resume'));
|
|
3085
|
+
app.post('/api/setup/test', async (req, res) => runSetupStep(req, res, 'test'));
|
|
3086
|
+
app.post('/api/setup/repair', async (req, res) => runSetupStep(req, res, 'repair'));
|
|
3087
|
+
app.post('/api/setup/reconfigure', async (req, res) => runSetupStep(req, res, 'reconfigure'));
|
|
3088
|
+
app.post('/api/setup/commit', async (_req, res) => {
|
|
3089
|
+
try {
|
|
3090
|
+
const { refreshSetupState } = await import('../setup/setup-store.js');
|
|
3091
|
+
const ev = await refreshSetupState({});
|
|
3092
|
+
const { describeSetup } = await import('../setup/setup-store.js');
|
|
3093
|
+
res.json({ ...(await setupState()), ok: ev.gate === 'ready', summary: describeSetup(ev) });
|
|
3094
|
+
}
|
|
3095
|
+
catch (err) {
|
|
3096
|
+
res.status(500).json({ ok: false, error: String(err) });
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
app.post('/api/setup/identity', async (req, res) => runSetupStep({ body: { answers: [req.body?.name] } }, res, 'resume'));
|
|
3100
|
+
app.post('/api/setup/provider', async (req, res) => runSetupStep({ body: { answers: [req.body?.provider] } }, res, 'resume'));
|
|
3101
|
+
// 首启页面 (零依赖, 直接打这些 API; 未 ready 时所有对话路由都是 503)
|
|
3102
|
+
app.get('/setup', (_req, res) => {
|
|
3103
|
+
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Bolloon 初始化</title>
|
|
3104
|
+
<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}
|
|
3105
|
+
h1{font-size:20px}pre{background:#f6f6f6;padding:12px;border-radius:8px;white-space:pre-wrap}
|
|
3106
|
+
.ok{color:#0a7d32}.bad{color:#b00020}input,select,button{font-size:15px;padding:8px;margin:4px 0}
|
|
3107
|
+
.row{margin:10px 0}small{color:#666}</style>
|
|
3108
|
+
<h1>Bolloon 初始化</h1>
|
|
3109
|
+
<div id="state">加载中…</div>
|
|
3110
|
+
<div class="row" id="form"></div>
|
|
3111
|
+
<div class="row"><button onclick="run('step')">提交这一步</button>
|
|
3112
|
+
<button onclick="run('test')">重新测试连通性/运行时</button>
|
|
3113
|
+
<button onclick="run('repair')">修复/迁移配置</button>
|
|
3114
|
+
<button onclick="fetchState()">刷新</button></div>
|
|
3115
|
+
<pre id="log"></pre>
|
|
3116
|
+
<script>
|
|
3117
|
+
async function fetchState(){
|
|
3118
|
+
const r = await fetch('/api/setup'); const s = await r.json();
|
|
3119
|
+
const ready = s.gate==='ready';
|
|
3120
|
+
document.getElementById('state').innerHTML =
|
|
3121
|
+
'<b>阶段</b>: '+s.stage+' <b>门禁</b>: <span class="'+(ready?'ok':'bad')+'">'+s.gate+'</span><br>'+
|
|
3122
|
+
'<b>就绪度</b>: basic '+s.readiness.basic+' · agent '+s.readiness.agent+' · durable '+s.readiness.durable+' · network '+s.readiness.network+'<br>'+
|
|
3123
|
+
'<small>已完成: '+(s.completed.join(' → ')||'(无)')+'</small><br>'+
|
|
3124
|
+
'<small>下一步: '+(s.actions[0]||'')+'</small>'+
|
|
3125
|
+
(s.lastError?'<br><small class="bad">上次错误 ['+s.lastError.errorClass+'] '+s.lastError.message+'</small>':'');
|
|
3126
|
+
const f = document.getElementById('form');
|
|
3127
|
+
const n = s.nextStep;
|
|
3128
|
+
if(!n){ f.innerHTML='<b class="ok">✅ 已就绪, 可以进入对话</b>'; window.currentNeeds=null; return; }
|
|
3129
|
+
window.currentNeeds=n.needs;
|
|
3130
|
+
if(n.needs==='provider'){
|
|
3131
|
+
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>';
|
|
3132
|
+
} else if(n.needs==='credential'){
|
|
3133
|
+
f.innerHTML='<div>'+n.question+'</div><input id="v" type="password" placeholder="API key (不会回显)" autocomplete="off">';
|
|
3134
|
+
} else if(n.needs==='none'){
|
|
3135
|
+
f.innerHTML='<div>'+n.question+' (点上面按钮执行)</div>';
|
|
3136
|
+
} else {
|
|
3137
|
+
f.innerHTML='<div>'+n.question+'</div><input id="v" value="'+(n.defaultValue||'')+'">';
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
async function run(ep){
|
|
3141
|
+
const v=document.getElementById('v'); const n=window.currentNeeds;
|
|
3142
|
+
const answers = (n==='credential'||n==='name'||n==='model'||n==='provider') && v ? [v.value] : [];
|
|
3143
|
+
if(v) v.value=''; // key 立即从 DOM 里清掉
|
|
3144
|
+
const r = await fetch('/api/setup/'+ep,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({answers})});
|
|
3145
|
+
const j = await r.json();
|
|
3146
|
+
document.getElementById('log').textContent = (j.log||[]).join('\n') + '\n' + (j.summary||'');
|
|
3147
|
+
fetchState();
|
|
3148
|
+
}
|
|
3149
|
+
fetchState();
|
|
3150
|
+
</script>`);
|
|
3151
|
+
});
|
|
3152
|
+
// 2026-09-16 (M1/M3): 初始化状态 —— CLI 与 Web 读同一份事实 (setup-state.json + 各领域 store)
|
|
3153
|
+
app.get('/api/setup', async (_req, res) => {
|
|
3154
|
+
try {
|
|
3155
|
+
const { evaluateSetup, describeSetup, resolveBolloonHome } = await import('../setup/setup-store.js');
|
|
3156
|
+
const ev = await evaluateSetup({ light: false });
|
|
3157
|
+
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() });
|
|
3158
|
+
}
|
|
3159
|
+
catch (err) {
|
|
3160
|
+
res.status(500).json({ error: String(err?.message || err).slice(0, 200), gate: 'blocked' });
|
|
3161
|
+
}
|
|
3162
|
+
});
|
|
2940
3163
|
// 2026-09-16 (2-G.1): Skills Manager —— 与 CLI `/skills` `/skill` 读同一份事实 (skills-registry.json + 技能目录)
|
|
2941
3164
|
app.get('/api/skills', async (_req, res) => {
|
|
2942
3165
|
try {
|
|
@@ -3037,6 +3260,8 @@ ${goalDesc}
|
|
|
3037
3260
|
});
|
|
3038
3261
|
// 手动踢一个调度周期 (调试/验收用; 不改变任何"事实来源")
|
|
3039
3262
|
app.post('/api/supervisor/tick', async (_req, res) => {
|
|
3263
|
+
if (!(await setupGate(res)))
|
|
3264
|
+
return;
|
|
3040
3265
|
try {
|
|
3041
3266
|
const { getSupervisor } = await import('../agents/execution-supervisor.js');
|
|
3042
3267
|
const report = await getSupervisor().tickOnce();
|
|
@@ -4275,7 +4500,29 @@ ${goalDesc}
|
|
|
4275
4500
|
console.log(`[SSE] 客户端断开 channelId=${channelId || '(broadcast)'}, 剩余=${sseClients.size}`);
|
|
4276
4501
|
});
|
|
4277
4502
|
});
|
|
4503
|
+
// 2026-09-16 (M4): agent 执行硬门禁 —— 未 ready 时这些路由 503 + 结构化初始化状态 (Web 只显示初始化页)
|
|
4504
|
+
const setupGate = async (res) => {
|
|
4505
|
+
try {
|
|
4506
|
+
const { getSetupGateCached } = await import('../setup/setup-store.js');
|
|
4507
|
+
const { gate, state } = await getSetupGateCached();
|
|
4508
|
+
if (gate === 'ready')
|
|
4509
|
+
return true;
|
|
4510
|
+
res.status(503).json({
|
|
4511
|
+
error: `初始化未就绪 (${gate}, 阶段 ${state.stage}) — 请先完成初始化`,
|
|
4512
|
+
gate, stage: state.stage, readiness: state.readiness,
|
|
4513
|
+
nextActions: state.actions, lastError: state.lastError || null,
|
|
4514
|
+
});
|
|
4515
|
+
return false;
|
|
4516
|
+
}
|
|
4517
|
+
catch (err) {
|
|
4518
|
+
// fail-closed: 连门禁都读不出来 → 不放行
|
|
4519
|
+
res.status(503).json({ error: `初始化状态不可读 (fail-closed): ${String(err?.message || err).slice(0, 160)}`, gate: 'blocked' });
|
|
4520
|
+
return false;
|
|
4521
|
+
}
|
|
4522
|
+
};
|
|
4278
4523
|
app.post('/message', async (req, res) => {
|
|
4524
|
+
if (!(await setupGate(res)))
|
|
4525
|
+
return;
|
|
4279
4526
|
const { text, channelId, channelDid, attachments } = req.body;
|
|
4280
4527
|
if (!text) {
|
|
4281
4528
|
return res.status(400).json({ error: 'No text provided' });
|