@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
@@ -0,0 +1,592 @@
1
+ /**
2
+ * setup-store.ts — 初始化的**唯一事实来源** (M0/M1 + Phase 1 事实来源统一, 2026-09-16)
3
+ *
4
+ * 职责边界 (冻结):
5
+ * ① **只汇总不新增配置库**: 身份看 `user.json`; LLM 看 `bolloon-config.json`
6
+ * (**旧 `llm-config.json` 只作迁移输入**, 2026-08-07 已改名); 技能看 `skills-registry.json`;
7
+ * 长期执行看 `supervisor.json` + runs/goals 目录; `setup-state.json` 只存引导进度/校验结果/readiness,
8
+ * 永不存 apiKey 明文。
9
+ * ② 冻结初始化状态机 + 启动门禁结论 (ready/setup/repair/blocked)。
10
+ * ③ readiness 四层是**真实检查结果**:
11
+ * basic = identity + provider + credential + model + connectivity + runtime
12
+ * agent = basic + PiAgentHarness + 技能健康 (skillsOk 未知 **不算通过**)
13
+ * durable = agent + RunStore/GoalStore + Supervisor runner 可解析 + lease 可写
14
+ * network = P2P/Kubo (optional, 不阻塞基础对话)
15
+ *
16
+ * 不可混淆 (Phase 1): Provider 已选择 ≠ 凭证已可用 · 凭证存在 ≠ 模型可用 ·
17
+ * 模型可用 ≠ 运行时已初始化 · runtime ready ≠ 长期执行 ready
18
+ */
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import * as fs from 'fs/promises';
22
+ import * as crypto from 'crypto';
23
+ // ── 路径统一 (所有入口走这一个解析, 不在模块顶层永久缓存 HOME) ──────────────
24
+ export function resolveBolloonHome(env = process.env, home = os.homedir()) {
25
+ const explicit = env.BOLLOON_HOME?.trim();
26
+ if (explicit)
27
+ return explicit;
28
+ return path.join(env.HOME || home, '.bolloon');
29
+ }
30
+ export function setupStatePath(bolloonHome = resolveBolloonHome()) {
31
+ return path.join(bolloonHome, 'setup-state.json');
32
+ }
33
+ /** 正式配置文件 (config-store 写这个) */
34
+ export const CANONICAL_CONFIG_FILE = 'bolloon-config.json';
35
+ /** 迁移期旧文件: **只作输入** */
36
+ export const LEGACY_CONFIG_FILE = 'llm-config.json';
37
+ // ── 状态机 ──────────────────────────────────────────────────────────────────
38
+ export const SETUP_STAGES = [
39
+ 'uninitialized', 'identity_pending', 'provider_pending', 'credential_pending',
40
+ 'model_pending', 'connectivity_pending', 'runtime_pending', 'ready',
41
+ ];
42
+ export function emptySetupState(now = new Date()) {
43
+ return {
44
+ schema: 'bolloon-setup/1', stage: 'uninitialized', completed: [], inputs: {}, checks: {},
45
+ readiness: { basic: false, agent: false, durable: false, network: false },
46
+ readinessWhy: { basic: ['还没有开始初始化'], agent: ['基础层未就绪'], durable: ['agent 层未就绪'], network: ['P2P/Kubo 未检测 (optional)'] },
47
+ allow: { cli: true, web: false, supervisor: false, agent: false },
48
+ actions: ['运行 `bolloon setup` 完成初始化 (身份 → 供应商 → 密钥 → 模型 → 连通性 → 运行时)'],
49
+ updatedAt: now.toISOString(),
50
+ };
51
+ }
52
+ export async function readSetupState(bolloonHome = resolveBolloonHome()) {
53
+ try {
54
+ const raw = JSON.parse(await fs.readFile(setupStatePath(bolloonHome), 'utf8'));
55
+ if (!raw || raw.schema !== 'bolloon-setup/1')
56
+ return null;
57
+ const base = emptySetupState();
58
+ return {
59
+ ...base, ...raw,
60
+ readiness: { ...base.readiness, ...(raw.readiness || {}) },
61
+ readinessWhy: { ...base.readinessWhy, ...(raw.readinessWhy || {}) },
62
+ checks: { ...(raw.checks || {}) },
63
+ inputs: { ...(raw.inputs || {}) },
64
+ allow: { ...base.allow, ...(raw.allow || {}) },
65
+ };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /** 原子写 (tmp + rename) —— 半份状态文件比没有状态文件更危险 */
72
+ export async function writeSetupState(state, bolloonHome = resolveBolloonHome()) {
73
+ await fs.mkdir(bolloonHome, { recursive: true });
74
+ const p = setupStatePath(bolloonHome);
75
+ const tmp = `${p}.tmp`;
76
+ const next = { ...state, updatedAt: new Date().toISOString() };
77
+ await fs.writeFile(tmp, JSON.stringify(next, null, 2), 'utf8');
78
+ await fs.rename(tmp, p);
79
+ }
80
+ /** 配置指纹 (缺文件本身也是状态, 不碰密钥值) */
81
+ export async function hashConfigFiles(bolloonHome = resolveBolloonHome()) {
82
+ const files = [CANONICAL_CONFIG_FILE, LEGACY_CONFIG_FILE, path.join('identity', 'user.json'), 'user.json'];
83
+ const h = crypto.createHash('sha256');
84
+ let version;
85
+ for (const f of files) {
86
+ try {
87
+ const buf = await fs.readFile(path.join(bolloonHome, f));
88
+ h.update(f).update('\0').update(buf);
89
+ if (f === CANONICAL_CONFIG_FILE) {
90
+ try {
91
+ version = Number(JSON.parse(buf.toString('utf8'))?.version) || undefined;
92
+ }
93
+ catch { /* 无版本字段 */ }
94
+ }
95
+ }
96
+ catch {
97
+ h.update(f).update('\0').update('<missing>');
98
+ }
99
+ }
100
+ return { hash: h.digest('hex').slice(0, 32), version };
101
+ }
102
+ export async function readConfigFacts(bolloonHome = resolveBolloonHome()) {
103
+ const canonical = path.join(bolloonHome, CANONICAL_CONFIG_FILE);
104
+ const legacy = path.join(bolloonHome, LEGACY_CONFIG_FILE);
105
+ let canonicalRaw = null;
106
+ let canonicalError = null;
107
+ try {
108
+ canonicalRaw = JSON.parse(await fs.readFile(canonical, 'utf8'));
109
+ }
110
+ catch (err) {
111
+ if (err?.code !== 'ENOENT')
112
+ canonicalError = err;
113
+ }
114
+ let legacyRaw = null;
115
+ try {
116
+ legacyRaw = JSON.parse(await fs.readFile(legacy, 'utf8'));
117
+ }
118
+ catch { /* 旧文件不存在/坏 = 没有 */ }
119
+ if (canonicalError)
120
+ throw canonicalError; // 存在但坏了 → 上层分类 config → needs_repair
121
+ const chosen = canonicalRaw || legacyRaw;
122
+ return {
123
+ source: canonicalRaw ? 'canonical' : (legacyRaw ? 'legacy' : 'missing'),
124
+ activeProvider: chosen?.activeProvider || chosen?.provider,
125
+ providers: chosen?.providers || {},
126
+ legacyPresent: !!legacyRaw,
127
+ raw: chosen || undefined,
128
+ };
129
+ }
130
+ export function providerUsable(p) {
131
+ if (!p)
132
+ return false;
133
+ if (p.enabled === false)
134
+ return false;
135
+ if (p.apiKey)
136
+ return true;
137
+ return p.requiresApiKey === false;
138
+ }
139
+ /** 该 provider 是否还需要 key (缺省视为需要) */
140
+ export function providerNeedsKey(p) {
141
+ if (!p)
142
+ return true;
143
+ if (p.apiKey)
144
+ return false;
145
+ return p.requiresApiKey !== false;
146
+ }
147
+ export function deriveStage(c) {
148
+ if (!c.identity)
149
+ return 'identity_pending';
150
+ if (!c.providerSelected)
151
+ return 'provider_pending';
152
+ if (!c.credentialOk)
153
+ return 'credential_pending';
154
+ if (!c.modelOk)
155
+ return 'model_pending';
156
+ if (!c.connectivity)
157
+ return 'connectivity_pending';
158
+ if (!c.runtime)
159
+ return 'runtime_pending';
160
+ return 'ready';
161
+ }
162
+ function stageIndex(s) {
163
+ if (s === 'needs_repair' || s === 'blocked')
164
+ return -1;
165
+ return SETUP_STAGES.indexOf(s);
166
+ }
167
+ function setupHasPriorState(prev) {
168
+ return prev.completed.length > 0 || !!prev.inputs.provider || !!prev.inputs.identityDid || prev.stage !== 'uninitialized';
169
+ }
170
+ export function nextActionsFor(stage, c) {
171
+ const migrate = c.source === 'legacy' ? ['检测到旧 `llm-config.json`: `bolloon setup --repair` 会迁移到 `bolloon-config.json` (旧文件保留)'] : [];
172
+ switch (stage) {
173
+ case 'identity_pending': return [...migrate, '生成/填写身份 (姓名) — 已有 DID 会复用, 不会重复生成'];
174
+ case 'provider_pending': return [...migrate, c.configError === 'config'
175
+ ? '`bolloon-config.json` 损坏或缺失 → `bolloon setup --repair` (不重置身份, 不清 key)'
176
+ : '选择模型供应商 — 会显示是否需要 key / 默认模型 / 配置来源'];
177
+ case 'credential_pending': return ['填该 provider 的 apiKey (隐藏输入, 不落日志/不写状态文件)', 'Provider 已选择 ≠ 凭证已可用'];
178
+ case 'model_pending': return ['指定模型名 (留空用默认模型)', '自定义模型会标"未经过供应商模型列表验证"'];
179
+ case 'connectivity_pending': return ['跑真实连通性测试 (`bolloon setup --test`) — 用最终保存的 provider/key/baseUrl/model', '超时/401/404/限流/网络错误分别显示; 超时可重试, 不当成功'];
180
+ case 'runtime_pending': return ['真实初始化运行时 (initMinimax + 建 session + 最小模型调用) — 只检查 singleton 不算通过'];
181
+ case 'needs_repair': return ['配置损坏 → `bolloon setup --repair` (就地修, 不回退成默认配置假装正常)', '修完再启动; 期间只允许诊断'];
182
+ case 'blocked': return ['配置根目录不可写 → 检查磁盘/权限后 `bolloon setup --status`'];
183
+ default: return ['已就绪'];
184
+ }
185
+ }
186
+ export function gateFor(stage, readiness) {
187
+ if (stage === 'ready' && readiness.basic)
188
+ return 'ready';
189
+ if (stage === 'needs_repair')
190
+ return 'repair';
191
+ if (stage === 'blocked')
192
+ return 'blocked';
193
+ return 'setup';
194
+ }
195
+ export async function evaluateSetup(opts = {}) {
196
+ const bolloonHome = opts.bolloonHome ?? resolveBolloonHome();
197
+ const now = opts.now ? opts.now() : Date.now();
198
+ const ttl = opts.connectivityTtlMs ?? 24 * 60 * 60 * 1000;
199
+ const prev = (await readSetupState(bolloonHome)) || emptySetupState();
200
+ const reasons = [];
201
+ const why = { basic: [], agent: [], durable: [], network: [] };
202
+ const checks = { ...prev.checks };
203
+ // 1) 身份
204
+ try {
205
+ const identity = opts.readIdentity ? await opts.readIdentity() : await defaultReadIdentity(bolloonHome);
206
+ checks.identity = !!(identity && (identity.did || identity.name));
207
+ }
208
+ catch (err) {
209
+ checks.identity = false;
210
+ reasons.push(`身份读取失败: ${msg(err)}`);
211
+ }
212
+ if (!checks.identity)
213
+ why.basic.push('缺身份 (user.json 无 did/name)');
214
+ // 2) LLM 配置: 供应商 / 凭证 / 模型
215
+ let cfgErr;
216
+ let provider;
217
+ let model;
218
+ let hasKey = false;
219
+ let providerSelected = false;
220
+ let credentialOk = false;
221
+ let modelOk = false;
222
+ try {
223
+ const facts = opts.readConfig ? await opts.readConfig() : await readConfigFacts(bolloonHome);
224
+ checks.configSource = facts.source;
225
+ provider = facts.activeProvider;
226
+ providerSelected = !!provider;
227
+ const p = provider ? facts.providers?.[provider] : undefined;
228
+ hasKey = !!p?.apiKey;
229
+ credentialOk = providerSelected && (!providerNeedsKey(p) || hasKey);
230
+ const defaults = opts.providerDefaults ?? await defaultProviderDefaults();
231
+ model = p?.model || (provider ? defaults?.[provider]?.model : undefined);
232
+ modelOk = !!model;
233
+ checks.modelVerified = !!p?.model;
234
+ if (!providerSelected) {
235
+ reasons.push(facts.source === 'missing'
236
+ ? `还没有 LLM 配置 (${CANONICAL_CONFIG_FILE} 不存在) → 未选择模型供应商`
237
+ : `配置里没有 activeProvider → 未选择模型供应商`);
238
+ why.basic.push('未选择模型供应商');
239
+ }
240
+ else if (!credentialOk) {
241
+ reasons.push(`供应商 ${provider} 需要 apiKey 但没配 (Provider 已选择 ≠ 凭证已可用)`);
242
+ why.basic.push(`供应商 ${provider} 缺 apiKey`);
243
+ }
244
+ else if (!modelOk) {
245
+ reasons.push(`供应商 ${provider} 没有可用模型名`);
246
+ why.basic.push(`供应商 ${provider} 缺模型名`);
247
+ }
248
+ if (facts.source === 'legacy')
249
+ reasons.push(`当前配置来自旧文件 ${LEGACY_CONFIG_FILE} (未迁移) → 建议 \`bolloon setup --repair\``);
250
+ }
251
+ catch (err) {
252
+ cfgErr = 'config';
253
+ reasons.push(`LLM 配置不可读: ${msg(err)}`);
254
+ why.basic.push('bolloon-config.json 损坏或不可读 (→ --repair)');
255
+ }
256
+ checks.providerSelected = providerSelected;
257
+ checks.credentialPresent = hasKey;
258
+ checks.providerUsable = providerSelected && credentialOk;
259
+ checks.modelPresent = modelOk;
260
+ // 3) 连通性 (24h 有效期)
261
+ const connAt = checks.connectivityAt ? Date.parse(checks.connectivityAt) : 0;
262
+ const connFresh = !!connAt && now - connAt < ttl;
263
+ let connectivityOk = false;
264
+ if (checks.connectivityOk && connFresh)
265
+ connectivityOk = true;
266
+ else if (checks.connectivityOk && !connFresh) {
267
+ reasons.push('连通性结果已过期 (>24h) → 需要重新测试');
268
+ checks.connectivityOk = false;
269
+ }
270
+ if (!connectivityOk)
271
+ why.basic.push(connAt && !connFresh ? '连通性结果过期 (需重测)' : '还没做过连通性测试');
272
+ // 4) 运行时 (真实: initMinimax + session; 不是"singleton 存在")
273
+ let runtimeOk = checks.runtimeInitialized === true;
274
+ if (!opts.light) {
275
+ try {
276
+ runtimeOk = opts.runtimeReady ? await opts.runtimeReady() : await defaultRuntimeReady();
277
+ }
278
+ catch {
279
+ runtimeOk = false;
280
+ }
281
+ }
282
+ checks.runtimeInitialized = runtimeOk;
283
+ if (!runtimeOk)
284
+ why.basic.push('运行时未初始化 (initMinimax/session 不可用)');
285
+ const basic = !!(checks.identity && providerSelected && credentialOk && modelOk && connectivityOk && runtimeOk);
286
+ if (!basic && why.basic.length === 0)
287
+ why.basic.push('基础层未就绪');
288
+ // 5) agent 层
289
+ let harnessOk = checks.harnessOk;
290
+ if (!opts.light) {
291
+ try {
292
+ harnessOk = await defaultHarnessReady();
293
+ }
294
+ catch {
295
+ harnessOk = false;
296
+ }
297
+ }
298
+ checks.harnessOk = harnessOk;
299
+ if (!harnessOk)
300
+ why.agent.push('PiAgentHarness 不可用 (约束层加载失败)');
301
+ let skillsOk = checks.skillsOk;
302
+ if (!opts.light) {
303
+ try {
304
+ const s = opts.readSkills ? await opts.readSkills() : await defaultReadSkills(bolloonHome);
305
+ skillsOk = s ? s.invalid === 0 : undefined;
306
+ checks.skillsInvalid = s?.invalid;
307
+ if (s && s.invalid > 0)
308
+ why.agent.push(`有 ${s.invalid} 个技能不合格 → \`bolloon skills\` 看具体技能`);
309
+ }
310
+ catch {
311
+ skillsOk = undefined;
312
+ }
313
+ }
314
+ if (skillsOk === undefined)
315
+ why.agent.push('技能健康度未知 (registry 不可读, 不能当作通过)');
316
+ checks.skillsOk = skillsOk;
317
+ const agentReady = !!(basic && harnessOk && skillsOk === true);
318
+ if (!agentReady && why.agent.length === 0)
319
+ why.agent.push('agent 层未就绪');
320
+ // 6) durable 层
321
+ let runStoreOk = checks.runStoreOk;
322
+ let goalStoreOk = checks.goalStoreOk;
323
+ let leaseOk = checks.leaseOk;
324
+ let supervisorResolvable = checks.supervisorResolvable;
325
+ if (!opts.light) {
326
+ const p = await probeDurable(bolloonHome, opts);
327
+ runStoreOk = p.runStoreOk;
328
+ goalStoreOk = p.goalStoreOk;
329
+ leaseOk = p.leaseOk;
330
+ supervisorResolvable = p.supervisorResolvable;
331
+ if (!runStoreOk)
332
+ why.durable.push('runs 目录不可读写');
333
+ if (!goalStoreOk)
334
+ why.durable.push('goals 目录不可读写');
335
+ if (!leaseOk)
336
+ why.durable.push('goal lease 文件不可写 (无法跨进程排他)');
337
+ if (!supervisorResolvable)
338
+ why.durable.push(p.supervisorReason || 'Supervisor runner 无法解析');
339
+ }
340
+ checks.runStoreOk = runStoreOk;
341
+ checks.goalStoreOk = goalStoreOk;
342
+ checks.leaseOk = leaseOk;
343
+ checks.supervisorResolvable = supervisorResolvable;
344
+ const durableReady = !!(agentReady && runStoreOk && goalStoreOk && leaseOk && supervisorResolvable);
345
+ if (!durableReady && why.durable.length === 0)
346
+ why.durable.push('durable 层未就绪');
347
+ // 7) network 层 (optional)
348
+ const networkOk = checks.networkOk === true;
349
+ if (!networkOk)
350
+ why.network.push('P2P/Kubo 未确认可用 (optional, 不阻塞基础对话)');
351
+ const readiness = { basic, agent: agentReady, durable: durableReady, network: networkOk };
352
+ const stage = deriveStage({ identity: !!checks.identity, providerSelected, credentialOk, modelOk, connectivity: connectivityOk, runtime: runtimeOk });
353
+ const completed = [];
354
+ for (const s of SETUP_STAGES) {
355
+ if (s !== 'uninitialized' && stageIndex(stage) > stageIndex(s))
356
+ completed.push(s);
357
+ }
358
+ let homeWritable = true;
359
+ try {
360
+ homeWritable = opts.homeWritableProbe ? await opts.homeWritableProbe() : await probeHomeWritable(bolloonHome);
361
+ }
362
+ catch {
363
+ homeWritable = false;
364
+ }
365
+ if (!homeWritable)
366
+ reasons.push(`配置根目录不可写: ${bolloonHome}`);
367
+ let gate;
368
+ if (readiness.basic && stage === 'ready')
369
+ gate = 'ready';
370
+ else if (!homeWritable)
371
+ gate = 'blocked';
372
+ else if (cfgErr === 'config' && (setupHasPriorState(prev) || !!checks.identity))
373
+ gate = 'repair'; // 盘上已有身份 = 有历史 → 就地修, 不要从零再来
374
+ else
375
+ gate = 'setup';
376
+ const state = {
377
+ ...prev,
378
+ schema: 'bolloon-setup/1',
379
+ stage, completed,
380
+ inputs: { ...prev.inputs, provider: provider ?? prev.inputs.provider, model: model ?? prev.inputs.model, hasApiKey: hasKey || prev.inputs.hasApiKey },
381
+ checks, readiness, readinessWhy: why,
382
+ allow: { cli: true, web: gate === 'ready', supervisor: gate === 'ready' && readiness.durable, agent: gate === 'ready' && readiness.basic },
383
+ actions: nextActionsFor(stage, { identity: !!checks.identity, providerSelected, credentialOk, modelOk, connectivity: connectivityOk, runtime: runtimeOk, configError: cfgErr, source: checks.configSource }),
384
+ updatedAt: new Date().toISOString(),
385
+ };
386
+ const fp = await hashConfigFiles(bolloonHome);
387
+ state.configHash = fp.hash;
388
+ state.configVersion = fp.version;
389
+ if (gate !== 'ready' && cfgErr)
390
+ state.lastError = { at: new Date().toISOString(), stage, errorClass: cfgErr, message: reasons[0] || '配置不完整' };
391
+ return { state, gate, reasons, nextActions: state.actions };
392
+ }
393
+ // ── 默认探测 ────────────────────────────────────────────────────────────────
394
+ /**
395
+ * 身份事实来源: **`~/.bolloon/identity/user.json`** (setup-wizard 的真实写入路径) —— 2026-09-16 修:
396
+ * 之前这里读 `user.json` / `identity.json`, 与真实路径不一致 → 明明配好了身份却被判未配置
397
+ * (与 bolloon-config.json / llm-config.json 同类问题, Phase 1 一并修掉)。
398
+ */
399
+ async function defaultReadIdentity(bolloonHome) {
400
+ const candidates = [
401
+ path.join('identity', 'user.json'),
402
+ 'user.json',
403
+ 'identity.json',
404
+ ];
405
+ for (const f of candidates) {
406
+ try {
407
+ const j = JSON.parse(await fs.readFile(path.join(bolloonHome, f), 'utf8'));
408
+ if (j?.did || j?.name || j?.identity?.did || j?.identity?.name) {
409
+ return { did: j.did || j.identity?.did, name: j.name || j.identity?.name };
410
+ }
411
+ }
412
+ catch { /* 下一个 */ }
413
+ }
414
+ return null;
415
+ }
416
+ async function defaultProviderDefaults() {
417
+ try {
418
+ const mod = await import('../llm/config-store.js');
419
+ const info = mod.PROVIDER_INFO || {};
420
+ const out = {};
421
+ for (const [k, v] of Object.entries(info))
422
+ out[k] = { model: v?.models?.[0] };
423
+ return out;
424
+ }
425
+ catch {
426
+ return {};
427
+ }
428
+ }
429
+ async function defaultRuntimeReady() {
430
+ try {
431
+ const mod = await import('../constraints/index.js');
432
+ if (typeof mod.initMinimax === 'function')
433
+ mod.initMinimax();
434
+ if (typeof mod.getMinimax === 'function')
435
+ mod.getMinimax();
436
+ return true;
437
+ }
438
+ catch {
439
+ return false;
440
+ }
441
+ }
442
+ async function defaultHarnessReady() {
443
+ try {
444
+ const mod = await import('../agents/pi-harness.js');
445
+ return typeof mod.PiAgentHarness === 'function';
446
+ }
447
+ catch {
448
+ return false;
449
+ }
450
+ }
451
+ async function defaultReadSkills(bolloonHome) {
452
+ try {
453
+ const { SkillsManager } = await import('../agents/skills-manager.js');
454
+ const sm = new SkillsManager({ home: path.dirname(bolloonHome), cwd: process.cwd() });
455
+ const h = await sm.health({ home: path.dirname(bolloonHome) });
456
+ return { total: h.total, invalid: h.invalid.length };
457
+ }
458
+ catch {
459
+ return null;
460
+ }
461
+ }
462
+ async function probeDirWritable(dir) {
463
+ try {
464
+ await fs.mkdir(dir, { recursive: true });
465
+ const probe = path.join(dir, '.write-probe');
466
+ await fs.writeFile(probe, 'ok', 'utf8');
467
+ await fs.rm(probe, { force: true });
468
+ return true;
469
+ }
470
+ catch {
471
+ return false;
472
+ }
473
+ }
474
+ async function probeHomeWritable(bolloonHome) { return probeDirWritable(bolloonHome); }
475
+ async function probeDurable(bolloonHome, opts) {
476
+ const runStoreOk = await probeDirWritable(path.join(bolloonHome, 'runs'));
477
+ const goalStoreOk = await probeDirWritable(path.join(bolloonHome, 'goals'));
478
+ let leaseOk = false;
479
+ try {
480
+ const leasePath = path.join(bolloonHome, 'goals', '.lease-probe');
481
+ await fs.mkdir(path.dirname(leasePath), { recursive: true });
482
+ const fh = await fs.open(leasePath, 'wx');
483
+ await fh.close();
484
+ await fs.rm(leasePath, { force: true });
485
+ leaseOk = true;
486
+ }
487
+ catch {
488
+ leaseOk = false;
489
+ }
490
+ let supervisorResolvable = false;
491
+ let supervisorReason;
492
+ try {
493
+ if (opts.supervisorStatus) {
494
+ const s = await opts.supervisorStatus();
495
+ supervisorResolvable = !!s?.resolvable;
496
+ supervisorReason = s?.reason;
497
+ }
498
+ else {
499
+ const js = JSON.parse(await fs.readFile(path.join(bolloonHome, 'supervisor.json'), 'utf8'));
500
+ const res = js?.lastResolution;
501
+ if (res && typeof res.ok === 'boolean') {
502
+ supervisorResolvable = res.ok === true;
503
+ supervisorReason = res.ok ? undefined : `上次执行器解析失败: ${res.reason || res.failedStage || '未知原因'}`;
504
+ }
505
+ else {
506
+ supervisorResolvable = opts.sessionStoreOk ? await opts.sessionStoreOk() : true;
507
+ supervisorReason = supervisorResolvable ? undefined : '缺少可用的 session store';
508
+ }
509
+ }
510
+ }
511
+ catch {
512
+ supervisorResolvable = opts.sessionStoreOk ? await opts.sessionStoreOk() : true;
513
+ }
514
+ return { runStoreOk, goalStoreOk, leaseOk, supervisorResolvable, supervisorReason };
515
+ }
516
+ function msg(err) { return String(err?.message || err).slice(0, 160); }
517
+ // ── 落盘/门禁 ───────────────────────────────────────────────────────────────
518
+ export async function refreshSetupState(opts = {}) {
519
+ const ev = await evaluateSetup(opts);
520
+ await writeSetupState(ev.state, opts.bolloonHome ?? resolveBolloonHome());
521
+ return ev;
522
+ }
523
+ export async function recordSetupFailure(stage, errorClass, message, bolloonHome = resolveBolloonHome()) {
524
+ const prev = (await readSetupState(bolloonHome)) || emptySetupState();
525
+ const state = {
526
+ ...prev,
527
+ stage: errorClass === 'config' || errorClass === 'io' ? 'needs_repair' : prev.stage,
528
+ lastError: { at: new Date().toISOString(), stage, errorClass, message: String(message).slice(0, 300) },
529
+ lastAttemptAt: new Date().toISOString(),
530
+ actions: [`${stage} 阶段失败 (${errorClass}) → 可以 重试 / 修改 / 返回上一步 / \`bolloon setup --repair\``, '已完成步骤保留; 再次运行 `bolloon setup` 从失败阶段继续'],
531
+ allow: { ...prev.allow, agent: false, supervisor: false, web: false },
532
+ updatedAt: new Date().toISOString(),
533
+ };
534
+ await writeSetupState(state, bolloonHome);
535
+ return state;
536
+ }
537
+ export async function recordSetupStage(stage, patch = {}, bolloonHome = resolveBolloonHome()) {
538
+ const prev = (await readSetupState(bolloonHome)) || emptySetupState();
539
+ const completed = Array.from(new Set([...prev.completed, ...(stage === 'uninitialized' ? [] : [stage])]));
540
+ const state = {
541
+ ...prev, stage, completed,
542
+ inputs: { ...prev.inputs, ...(patch.inputs || {}) },
543
+ checks: { ...prev.checks, ...(patch.checks || {}) },
544
+ lastAttemptAt: new Date().toISOString(),
545
+ lastError: undefined,
546
+ updatedAt: new Date().toISOString(),
547
+ };
548
+ await writeSetupState(state, bolloonHome);
549
+ return state;
550
+ }
551
+ export function describeSetup(ev) {
552
+ const s = ev.state;
553
+ const lines = [
554
+ `初始化状态: ${s.stage} 门禁: ${ev.gate}`,
555
+ `就绪度: basic=${s.readiness.basic ? '✓' : '✗'} agent=${s.readiness.agent ? '✓' : '✗'} durable=${s.readiness.durable ? '✓' : '✗'} network=${s.readiness.network ? '✓' : '✗'}`,
556
+ `已完成: ${s.completed.join(' → ') || '(无)'}`,
557
+ ];
558
+ if (s.checks.configSource)
559
+ lines.push(`配置来源: ${s.checks.configSource}${s.checks.configSource === 'legacy' ? ' (旧文件, 建议 --repair 迁移)' : ''}`);
560
+ if (s.inputs.provider)
561
+ lines.push(`已存输入: provider=${s.inputs.provider}${s.inputs.model ? ` model=${s.inputs.model}` : ''} key=${s.inputs.hasApiKey ? '有' : '无'}`);
562
+ for (const x of s.readinessWhy.basic)
563
+ lines.push(`缺 (basic): ${x}`);
564
+ if (!s.readiness.agent)
565
+ for (const x of s.readinessWhy.agent)
566
+ lines.push(`缺 (agent): ${x}`);
567
+ if (!s.readiness.durable)
568
+ for (const x of s.readinessWhy.durable)
569
+ lines.push(`缺 (durable): ${x}`);
570
+ if (s.lastError)
571
+ lines.push(`上次错误 [${s.lastError.errorClass}] ${s.lastError.stage}: ${s.lastError.message}`);
572
+ for (const r of ev.reasons)
573
+ lines.push(`· ${r}`);
574
+ lines.push(`下一步: ${ev.nextActions[0]}`);
575
+ return lines.join('\n');
576
+ }
577
+ let gateCache = null;
578
+ const GATE_TTL_MS = 30_000;
579
+ export async function getSetupGateCached(now = Date.now()) {
580
+ if (gateCache && now - gateCache.at < GATE_TTL_MS)
581
+ return { gate: gateCache.gate, state: gateCache.state };
582
+ try {
583
+ const ev = await evaluateSetup({ light: true });
584
+ gateCache = { gate: ev.gate, at: now, state: ev.state };
585
+ return { gate: ev.gate, state: ev.state };
586
+ }
587
+ catch {
588
+ gateCache = { gate: 'blocked', at: now, state: emptySetupState() };
589
+ return { gate: 'blocked', state: gateCache.state };
590
+ }
591
+ }
592
+ export function resetSetupGateCache() { gateCache = null; }
@@ -107,6 +107,7 @@ export function registerX402InfoRoutes(app) {
107
107
  const pay = await checkAndSettlePayment({
108
108
  paymentHeader: String(paymentHeader),
109
109
  requirements,
110
+ expectedItemId: stored.item.id, // 凭据必须绑定这条资源 (防跨资源复用)
110
111
  });
111
112
  if (!pay.ok) {
112
113
  return res.status(402).json({ ...requirements, error: pay.error });