@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.
@@ -12,10 +12,12 @@
12
12
  * - io 可注入 → 单测不碰真实 stdin/stdout
13
13
  */
14
14
  import * as readline from 'readline';
15
+ import { evaluateSetup, providerUsable, readConfigFacts, resolveBolloonHome, } from '../setup/setup-store.js';
16
+ import { runOnboard } from '../setup/onboard.js';
15
17
  import * as fs from 'fs/promises';
16
18
  import * as path from 'path';
17
19
  import * as os from 'os';
18
- import { llmConfigStore, PROVIDER_INFO, DEFAULT_PROVIDER_CONFIGS } from '../llm/config-store.js';
20
+ import { llmConfigStore, PROVIDER_INFO } from '../llm/config-store.js';
19
21
  /** 向导推荐的供应商顺序 (第一个是最省事的国内直连) */
20
22
  export const RECOMMENDED_PROVIDERS = [
21
23
  'deepseek', 'minimax', 'openai', 'anthropic', 'openrouter', 'gemini',
@@ -108,149 +110,116 @@ export async function writeUserIdentity(name, home = os.homedir()) {
108
110
  return { identity, created, file };
109
111
  }
110
112
  // ---------------------------------------------------------------- 首次运行判断
111
- function providerUsable(p) {
112
- if (!p?.enabled)
113
- return false;
114
- if (p.apiKey)
115
- return true;
116
- return p.requiresApiKey === false; // 本地模型 (ollama/local) 不需要 key
117
- }
113
+ // providerUsable 只有一份实现 (setup-store)
118
114
  /** 首次运行: 没有可用供应商, 或还没有用户身份 */
119
115
  export async function isFirstRun(home = os.homedir()) {
116
+ // 2026-09-16 (Phase 1/3): 不再有独立判断 —— 是否"需要引导"由 setup-store 唯一决定。
117
+ // fail-closed: 评估不出来 → 视为"需要引导"。
120
118
  try {
121
- await llmConfigStore.initialize();
122
- const cfg = await llmConfigStore.getConfig();
123
- const usable = Object.values(cfg.providers || {}).some((p) => providerUsable(p));
124
- if (!usable)
125
- return true;
126
- const user = await readUserIdentity(home);
127
- return !user;
119
+ const ev = await evaluateSetup({ bolloonHome: resolveBolloonHome(process.env, home), light: true });
120
+ return ev.gate !== 'ready';
128
121
  }
129
- catch {
130
- return false; // 判断失败不阻塞启动
122
+ catch (e) {
123
+ console.warn('[setup] 初始化状态评估失败, 按"需要引导"处理 (fail-closed):', String(e?.message || e).slice(0, 160));
124
+ return true;
131
125
  }
132
126
  }
133
- /** 运行初始化向导 (交互式或参数式) */
127
+ /** WizardIO → OnboardIO 适配 (select/confirm 用编号或值回答) */
128
+ function onboardIO(io) {
129
+ return {
130
+ print: (m) => io.print(m),
131
+ ask: async (q, opts) => {
132
+ for (let i = 0; i < 3; i++) {
133
+ const v = await io.ask(q, opts?.defaultValue ? { default: opts.defaultValue } : undefined);
134
+ const val = String(v ?? '').trim() || String(opts?.defaultValue ?? '');
135
+ if (!opts?.validate)
136
+ return val;
137
+ const r = opts.validate(val);
138
+ if (r.ok)
139
+ return val;
140
+ io.print(` ✗ ${r.error || '输入无效'}`);
141
+ }
142
+ return String(opts?.defaultValue ?? '');
143
+ },
144
+ askHidden: async (q) => {
145
+ const { askHiddenLine } = await import('./setup-wizard.js').catch(() => ({ askHiddenLine: null }));
146
+ return io.ask(q, { hidden: true }); // WizardIO 已支持 hidden (不回显)
147
+ },
148
+ confirm: async (q, d = true) => {
149
+ const a = String(await io.ask(`${q} (y/n)`, { default: d ? 'y' : 'n' })).trim().toLowerCase();
150
+ return a === '' ? d : /^(y|yes|1|true|是)$/.test(a);
151
+ },
152
+ select: async (q, choices) => {
153
+ io.print(q);
154
+ choices.forEach((c, i) => io.print(` ${i + 1}) ${c.label}${c.hint ? ` — ${c.hint}` : ''}`));
155
+ const a = String(await io.ask(`选择 (序号或名称)`, { default: '1' })).trim();
156
+ if (!a)
157
+ return choices[0]?.value || '';
158
+ if (/^\d+$/.test(a) && choices[Number(a) - 1])
159
+ return choices[Number(a) - 1].value;
160
+ const hit = choices.find((c) => c.value === a) || choices.find((c) => a && c.value.startsWith(a)) || choices.find((c) => a && c.label.includes(a));
161
+ return hit?.value || a;
162
+ },
163
+ };
164
+ }
165
+ /**
166
+ * 运行初始化向导 —— 现在是**可恢复阶段执行器**的薄包装 (Phase 2):
167
+ * load state → 显示已有 → 收集修改 → 校验 → 真实验证 → 原子提交阶段 → 推进
168
+ * 失败: 保留已完成步骤 · 不清配置 · 标失败分类 · 给重试/修改/回退入口 · **不显示配置完成**
169
+ */
134
170
  export async function runSetupWizard(opts = {}) {
135
171
  const io = opts.io ?? defaultWizardIO();
136
172
  const home = opts.home ?? os.homedir();
137
- const interactive = opts.interactive !== false;
173
+ const bolloonHome = resolveBolloonHome(process.env, home);
138
174
  const P = (s) => io.print(s);
175
+ const mode = opts.mode || 'setup';
176
+ P('');
177
+ P('╭─ Bolloon 初始化 (可中断, 下次从失败阶段继续) ─────╮');
178
+ P('│ 身份 → 供应商 → 凭证 → 模型 → 连通性 → 运行时 │');
179
+ P('╰──────────────────────────────────────────────────╯');
180
+ // 参数式输入 (非交互/脚本): 先落盘再跑阶段, 保证"输入已保存"
139
181
  try {
140
- await llmConfigStore.initialize();
182
+ if (opts.name)
183
+ await writeUserIdentity(opts.name, home);
184
+ const prov = opts.provider;
185
+ if (prov) {
186
+ const store = llmConfigStore;
187
+ await store.initialize();
188
+ await store.updateProvider(prov, { enabled: true });
189
+ if (opts.apiKey)
190
+ await store.updateProvider(prov, { apiKey: opts.apiKey });
191
+ if (opts.model)
192
+ await store.updateProvider(prov, { model: opts.model });
193
+ if (mode !== 'reconfigure')
194
+ await store.setActiveProvider(prov);
195
+ }
141
196
  }
142
197
  catch (e) {
143
- return { ok: false, error: `读取配置失败: ${String(e?.message || e).slice(0, 200)}` };
144
- }
145
- P('');
146
- P('╭─ Bolloon 初始化 ─────────────────────────────╮');
147
- P('│ 三步: 你的称呼 → 模型供应商 → API key + 模型 │');
148
- P('╰──────────────────────────────────────────────╯');
149
- // ---- 1) 用户称呼 ----
150
- const existingUser = await readUserIdentity(home);
151
- let name = String(opts.name || '').trim();
152
- if (!name && interactive) {
153
- name = await io.ask('① 我该怎么称呼你?', { default: existingUser?.name || os.userInfo().username });
198
+ return { ok: false, error: `参数落盘失败: ${String(e?.message || e).slice(0, 160)}` };
154
199
  }
155
- if (!name)
156
- name = existingUser?.name || os.userInfo().username;
157
- const idWrite = await writeUserIdentity(name, home);
158
- P(` 身份: ${idWrite.identity.name}${idWrite.created ? ' (已生成新 DID)' : ' (复用已有 DID)'}`);
159
- P(` DID: ${idWrite.identity.did.slice(0, 42)}...`);
160
- P(` 文件: ${idWrite.file}`);
161
- // ---- 2) 供应商 ----
162
- const cfg = await llmConfigStore.getConfig();
163
- const providers = cfg.providers;
164
- let provider = String(opts.provider || '').toLowerCase().trim();
165
- if (!provider && interactive) {
166
- P('');
167
- P('② 选一个模型供应商:');
168
- RECOMMENDED_PROVIDERS.forEach((p, i) => {
169
- const info = PROVIDER_INFO[p] || {};
170
- const st = providers[p];
171
- const mark = providerUsable(st) ? '🔑 已配置' : (st?.requiresApiKey === false ? '免 key' : '');
172
- P(` ${String(i + 1).padStart(2)}. ${p.padEnd(11)} ${String(info.name || '').padEnd(16)} ${mark}`);
173
- });
174
- const ans = await io.ask(' 序号或名字', { default: cfg.activeProvider });
175
- const idx = Number(ans);
176
- provider = Number.isFinite(idx) && idx >= 1 && idx <= RECOMMENDED_PROVIDERS.length
177
- ? RECOMMENDED_PROVIDERS[idx - 1]
178
- : ans.trim().toLowerCase();
179
- }
180
- if (!provider)
181
- provider = cfg.activeProvider;
182
- if (!providers[provider]) {
183
- return { ok: false, error: `未知供应商 '${provider}'. 可用: ${Object.keys(providers).join(', ')}` };
184
- }
185
- const info = PROVIDER_INFO[provider] || {};
186
- const current = providers[provider];
187
- const needsKey = current?.requiresApiKey !== false;
188
- // ---- 3) API key ----
189
- let apiKey = String(opts.apiKey || '').trim();
190
- if (!apiKey)
191
- apiKey = String(current?.apiKey || '').trim(); // 已配置的沿用
192
- if (!apiKey && needsKey && interactive) {
193
- P('');
194
- P(`③ ${info.name || provider} 需要 API key (输入不回显, 只写本地 ~/.bolloon/bolloon-config.json)`);
195
- apiKey = await io.ask(` 粘贴 ${provider} API key`, { hidden: true });
196
- if (!apiKey)
197
- P(' ⚠ 未输入 key — 该供应商会保持不可用 (可用 bolloon model key <provider> 之后再补)');
198
- }
199
- // ---- 4) 模型 ----
200
- const models = Array.isArray(info.models) ? info.models : [];
201
- let model = String(opts.model || '').trim();
202
- if (!model && interactive) {
203
- if (models.length > 0)
204
- P(` 可选模型: ${models.slice(0, 8).join(' / ')}`);
205
- model = await io.ask('④ 用哪个模型?', { default: current?.model || models[0] || '' });
206
- }
207
- if (!model)
208
- model = current?.model || models[0] || '';
209
- // ---- 5) 落盘 ----
210
- const patch = { enabled: true };
211
- if (apiKey)
212
- patch.apiKey = apiKey;
213
- if (model)
214
- patch.model = model;
215
- if (!current?.baseUrl)
216
- patch.baseUrl = DEFAULT_PROVIDER_CONFIGS[provider]?.baseUrl;
217
- await llmConfigStore.updateProvider(provider, patch);
218
- const usableNow = apiKey || !needsKey;
219
- if (usableNow) {
220
- await llmConfigStore.setActiveProvider(provider);
221
- }
222
- // ---- 6) 连通性测试 ----
223
- let test;
224
- if (!opts.skipTest && usableNow) {
225
- P('');
226
- P('⑤ 测试连通性...');
227
- try {
228
- const r = await llmConfigStore.testProvider(provider);
229
- test = { success: !!r.success, latency: r.latency, error: r.error };
230
- P(r.success ? ` ✅ 连通 (${r.latency ?? '?'} ms)` : ` ⚠ 未通过: ${String(r.error || '').slice(0, 160)}`);
231
- }
232
- catch (e) {
233
- test = { success: false, error: String(e?.message || e).slice(0, 200) };
234
- P(` ⚠ 测试失败: ${test.error}`);
235
- }
236
- }
237
- P('');
238
- P(`✅ 配置完成 — 当前供应商: ${provider}${model ? `, 模型: ${model}` : ''}`);
239
- P(` 配置文件: ${path.join(home, '.bolloon', 'bolloon-config.json')}`);
240
- P(' 开始对话: bolloon --cli');
241
- P('');
200
+ const res = await runOnboard({
201
+ mode,
202
+ io: onboardIO(io),
203
+ home,
204
+ bolloonHome,
205
+ targets: opts.targets,
206
+ skipSteps: opts.skipTest ? ['connectivity'] : undefined,
207
+ oneShot: opts.interactive === false,
208
+ });
209
+ const cfg = await readConfigFacts(bolloonHome).catch(() => null);
242
210
  return {
243
- ok: true,
244
- userName: idWrite.identity.name,
245
- provider,
246
- model,
247
- identityFile: idWrite.file,
248
- identityCreated: idWrite.created,
249
- test,
211
+ ok: res.ok,
212
+ userName: res.state.inputs.name,
213
+ provider: res.state.inputs.provider,
214
+ model: res.state.inputs.model,
215
+ identityFile: getUserIdentityFile(home),
216
+ identityCreated: !!res.state.inputs.identityDid,
217
+ test: res.state.checks.connectivityOk ? { success: true } : { success: false, error: res.state.lastError?.message },
218
+ error: res.ok ? undefined : (res.message || res.actions[0]),
219
+ // 新增: 结构化状态 (旧调用方看不到也不影响)
220
+ ...{ gate: res.gate, stage: res.state.stage, summary: res.summary },
250
221
  };
251
222
  }
252
- // ---------------------------------------------------------------- /model 命令
253
- /** 配置状态一览 (供 CLI 会话内 /model 与 bolloon model 共用) */
254
223
  export async function formatProviderStatus() {
255
224
  await llmConfigStore.initialize();
256
225
  const cfg = await llmConfigStore.getConfig();
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { BrowserWindow, app, ipcMain } from 'electron';
8
8
  import * as fs from 'fs';
9
+ import * as os from 'os';
9
10
  import * as path from 'path';
10
11
  import { firstRunFlagPath, dataDir, logsDir } from './paths';
11
12
  import { log } from './logger';
@@ -64,6 +65,26 @@ const OVERLAY_HTML = `
64
65
  </body>
65
66
  </html>
66
67
  `;
68
+ /**
69
+ * 2026-09-16 (Phase 3): 首启**事实**来自 SetupStore 的状态文件 (setup-state.json),
70
+ * first-run flag 只决定"要不要自动弹窗"。Electron 不能用 flag 当初始化事实。
71
+ */
72
+ export function readSetupFact() {
73
+ try {
74
+ const p = path.join(os.homedir(), '.bolloon', 'setup-state.json');
75
+ const js = JSON.parse(fs.readFileSync(p, 'utf8'));
76
+ const stage = String(js?.stage || 'uninitialized');
77
+ const ready = stage === 'ready';
78
+ return { ready, stage, gate: ready ? 'ready' : (stage === 'needs_repair' ? 'repair' : stage === 'blocked' ? 'blocked' : 'setup') };
79
+ }
80
+ catch {
81
+ return { ready: false, stage: 'uninitialized', gate: 'setup' };
82
+ }
83
+ }
84
+ /** 未 ready → 显示 Onboard (无论 flag 是否已看过) */
85
+ export function shouldShowOnboard() {
86
+ return !readSetupFact().ready;
87
+ }
67
88
  export function hasSeenFirstRun() {
68
89
  try {
69
90
  return fs.existsSync(firstRunFlagPath());
@@ -120,10 +141,20 @@ export function registerFirstRunIpc() {
120
141
  log('first-run IPC handlers registered');
121
142
  }
122
143
  /** 包装 — 决定要不要弹 overlay */
144
+ /**
145
+ * 2026-09-16 (Phase 3): 是否弹引导 —— **事实来自 SetupStore** (setup-state.json), flag 只控制"已 ready 后是否还提示"。
146
+ * 未 ready 时无论 flag 有没有都弹 (初始化没完成的机器不能装作已配置)。
147
+ */
123
148
  export async function maybeShowFirstRun(parent) {
124
- if (hasSeenFirstRun())
149
+ const fact = readSetupFact();
150
+ if (fact.ready && hasSeenFirstRun())
125
151
  return;
126
- log('首启 — 弹出引导');
152
+ if (!fact.ready) {
153
+ log(`初始化未就绪 (stage=${fact.stage}, gate=${fact.gate}) — 弹出引导 (flag=${hasSeenFirstRun() ? '已看过' : '未看过'})`);
154
+ }
155
+ else {
156
+ log('首启 — 弹出引导');
157
+ }
127
158
  await showFirstRunOverlay(parent);
128
159
  app.addRecentDocument(firstRunFlagPath()); // 跟踪最近文档, 让 user 知道有这文件
129
160
  }
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readSetupFact = readSetupFact;
37
+ exports.shouldShowOnboard = shouldShowOnboard;
36
38
  exports.hasSeenFirstRun = hasSeenFirstRun;
37
39
  exports.markFirstRunSeen = markFirstRunSeen;
38
40
  exports.showFirstRunOverlay = showFirstRunOverlay;
@@ -46,6 +48,7 @@ exports.maybeShowFirstRun = maybeShowFirstRun;
46
48
  */
47
49
  const electron_1 = require("electron");
48
50
  const fs = __importStar(require("fs"));
51
+ const os = __importStar(require("os"));
49
52
  const path = __importStar(require("path"));
50
53
  const paths_1 = require("./paths");
51
54
  const logger_1 = require("./logger");
@@ -104,6 +107,26 @@ const OVERLAY_HTML = `
104
107
  </body>
105
108
  </html>
106
109
  `;
110
+ /**
111
+ * 2026-09-16 (Phase 3): 首启**事实**来自 SetupStore 的状态文件 (setup-state.json),
112
+ * first-run flag 只决定"要不要自动弹窗"。Electron 不能用 flag 当初始化事实。
113
+ */
114
+ function readSetupFact() {
115
+ try {
116
+ const p = path.join(os.homedir(), '.bolloon', 'setup-state.json');
117
+ const js = JSON.parse(fs.readFileSync(p, 'utf8'));
118
+ const stage = String(js?.stage || 'uninitialized');
119
+ const ready = stage === 'ready';
120
+ return { ready, stage, gate: ready ? 'ready' : (stage === 'needs_repair' ? 'repair' : stage === 'blocked' ? 'blocked' : 'setup') };
121
+ }
122
+ catch {
123
+ return { ready: false, stage: 'uninitialized', gate: 'setup' };
124
+ }
125
+ }
126
+ /** 未 ready → 显示 Onboard (无论 flag 是否已看过) */
127
+ function shouldShowOnboard() {
128
+ return !readSetupFact().ready;
129
+ }
107
130
  function hasSeenFirstRun() {
108
131
  try {
109
132
  return fs.existsSync((0, paths_1.firstRunFlagPath)());
@@ -160,10 +183,20 @@ function registerFirstRunIpc() {
160
183
  (0, logger_1.log)('first-run IPC handlers registered');
161
184
  }
162
185
  /** 包装 — 决定要不要弹 overlay */
186
+ /**
187
+ * 2026-09-16 (Phase 3): 是否弹引导 —— **事实来自 SetupStore** (setup-state.json), flag 只控制"已 ready 后是否还提示"。
188
+ * 未 ready 时无论 flag 有没有都弹 (初始化没完成的机器不能装作已配置)。
189
+ */
163
190
  async function maybeShowFirstRun(parent) {
164
- if (hasSeenFirstRun())
191
+ const fact = readSetupFact();
192
+ if (fact.ready && hasSeenFirstRun())
165
193
  return;
166
- (0, logger_1.log)('首启 — 弹出引导');
194
+ if (!fact.ready) {
195
+ (0, logger_1.log)(`初始化未就绪 (stage=${fact.stage}, gate=${fact.gate}) — 弹出引导 (flag=${hasSeenFirstRun() ? '已看过' : '未看过'})`);
196
+ }
197
+ else {
198
+ (0, logger_1.log)('首启 — 弹出引导');
199
+ }
167
200
  await showFirstRunOverlay(parent);
168
201
  electron_1.app.addRecentDocument((0, paths_1.firstRunFlagPath)()); // 跟踪最近文档, 让 user 知道有这文件
169
202
  }
@@ -1 +1 @@
1
- {"version":3,"file":"first-run.js","sourceRoot":"","sources":["../../../src/electron/first-run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"first-run.js","sourceRoot":"","sources":["../../../src/electron/first-run.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1449,6 +1449,46 @@ async function processInputInner(input, comm) {
1449
1449
  }
1450
1450
  return;
1451
1451
  }
1452
+ // 2026-09-16 (2-F): /criteria <goalId> [confirm|propose <text...>] —— 判据:看/确认/改/让 agent 提候选
1453
+ if (cmd === '/criteria' || cmd.startsWith('/criteria ')) {
1454
+ const rest = trimmed.slice('/criteria'.length).trim();
1455
+ try {
1456
+ const { longTermStatus, confirmCriteria, proposeForGoal } = await import('./agents/goal-criteria.js');
1457
+ const { readGoal } = await import('./agents/goal-store.js');
1458
+ const [goalId, action, ...words] = rest.split(/\s+/).filter(Boolean);
1459
+ if (!goalId) {
1460
+ appendLine(`${C_DIM}用法: /criteria <goalId> [confirm | propose | set <判据用;分>]${RESET}`);
1461
+ return;
1462
+ }
1463
+ const g = await readGoal(goalId);
1464
+ if (!g) {
1465
+ appendLine(`${C_ERROR}没有这个目标: ${goalId}${RESET}`);
1466
+ return;
1467
+ }
1468
+ if (action === 'propose') {
1469
+ const p = await proposeForGoal(goalId);
1470
+ appendLine(p.ok ? `${C_DIM}候选判据 (待确认):\n${p.criteria.map((c, i) => ` [${i}] ${c}`).join('\n')}${RESET}` : `${C_ERROR}无法生成判据: ${p.reason}${RESET}`);
1471
+ return;
1472
+ }
1473
+ const list = action === 'set' && words.length ? words.join(' ').split(';').map((x) => x.trim()).filter(Boolean) : undefined;
1474
+ if (action === 'confirm' || list) {
1475
+ const r = await confirmCriteria(goalId, { criteria: list, by: 'cli' });
1476
+ appendLine(r.ok ? `${C_DIM}判据已确认 (v${r.goal?.criteriaVersion}): ${(r.goal?.successCriteria || []).join(' | ')}${RESET}` : `${C_ERROR}${r.reason}${RESET}`);
1477
+ return;
1478
+ }
1479
+ const st = await longTermStatus(goalId);
1480
+ appendLine(`${C_DIM}goal ${g.goalId} [${g.status}] 判据来源=${g.criteriaSource || 'unknown'} 已确认=${g.criteriaConfirmed === true} v${g.criteriaVersion || 1}${RESET}`);
1481
+ g.successCriteria.forEach((c, i) => appendLine(` ${g.completedCriteria.includes(i) ? '✓' : '·'} [${i}] ${c}`));
1482
+ if (g.proposedCriteria?.length)
1483
+ appendLine(` ${C_DIM}候选 (未确认): ${g.proposedCriteria.join(' | ')}${RESET}`);
1484
+ appendLine(` 长期完成: ${st.canComplete ? '✅ 可判完成' : `❌ ${st.reason}`}`);
1485
+ appendLine(` ${C_DIM}检查: ${Object.entries(st.checks).map(([k, v]) => `${k}=${v ? '✓' : '✗'}`).join(' ')}${RESET}`);
1486
+ }
1487
+ catch (e) {
1488
+ appendLine(`${C_ERROR}/criteria 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
1489
+ }
1490
+ return;
1491
+ }
1452
1492
  // 2026-09-16 (M2-B): /supervise — 长期执行层 (状态/唤醒原因/手动推进一个周期) · /wake <goalId> — 外部事件唤醒
1453
1493
  if (cmd === '/supervise' || cmd.startsWith('/supervise ')) {
1454
1494
  const arg = trimmed.slice('/supervise'.length).trim();
@@ -3901,6 +3941,21 @@ function parseArgs() {
3901
3941
  case '--supervise':
3902
3942
  result.supervise = true;
3903
3943
  break;
3944
+ case '--setup-status':
3945
+ result.setupStatus = true;
3946
+ break;
3947
+ case '--setup-resume':
3948
+ result.setupResume = true;
3949
+ break;
3950
+ case '--setup-repair':
3951
+ result.setupRepair = true;
3952
+ break;
3953
+ case '--setup-reconfigure':
3954
+ result.setupReconfigure = true;
3955
+ break;
3956
+ case '--setup-test':
3957
+ result.setupTest = true;
3958
+ break;
3904
3959
  case '--supervise-once':
3905
3960
  result.supervise = true;
3906
3961
  result.superviseOnce = true;
@@ -4333,6 +4388,45 @@ async function main() {
4333
4388
  const mode = args.web ? 'web' : 'cli';
4334
4389
  // 2026-09-16 (2-C.1): 独立 Supervisor 宿主 —— `bolloon --supervise [--supervise-once|--supervise-dry-run]`
4335
4390
  // 长期执行不再依附 web 进程 (页面关掉/CLI 没开也能继续推进 Goal)。
4391
+ // 2026-09-16 (Phase 4): Onboard 统一入口 —— CLI / Web 共用同一条执行器 (src/setup/onboard.ts)
4392
+ // --setup-status 只读; --setup-resume 从失败阶段继续; --setup-repair 迁移/备份坏文件后就地修;
4393
+ // --setup-reconfigure 只改选中项 (新配置测通才切 active); --setup-test 重跑连通性 + 运行时。
4394
+ if (args.setupStatus || args.setupResume || args.setupRepair || args.setupReconfigure || args.setupTest) {
4395
+ const mode = args.setupStatus ? 'status' : args.setupRepair ? 'repair' : args.setupReconfigure ? 'reconfigure' : args.setupTest ? 'test' : 'resume';
4396
+ if (mode === 'status') {
4397
+ const { evaluateSetup, describeSetup } = await import('./setup/setup-store.js');
4398
+ const ev = await evaluateSetup();
4399
+ process.stdout.write(describeSetup(ev) + '\n');
4400
+ process.exit(ev.gate === 'ready' ? 0 : 1);
4401
+ }
4402
+ const { defaultWizardIO } = await import('./cli/setup-wizard.js');
4403
+ const { runOnboard } = await import('./setup/onboard.js');
4404
+ const wizIO = defaultWizardIO();
4405
+ const res = await runOnboard({
4406
+ mode: mode,
4407
+ io: {
4408
+ print: (m) => process.stdout.write(m + '\n'),
4409
+ ask: async (q, o) => wizIO.ask(q, o?.defaultValue ? { default: o.defaultValue } : undefined),
4410
+ askHidden: async (q) => wizIO.ask(q, { hidden: true }),
4411
+ confirm: async (q, d = true) => {
4412
+ const a = String(await wizIO.ask(`${q} (y/n)`, { default: d ? 'y' : 'n' })).trim().toLowerCase();
4413
+ return a === '' ? d : /^(y|yes|1|true|是)$/.test(a);
4414
+ },
4415
+ select: async (q, choices) => {
4416
+ process.stdout.write(q + '\n');
4417
+ choices.forEach((c, i) => process.stdout.write(` ${i + 1}) ${c.label}${c.hint ? ` — ${c.hint}` : ''}\n`));
4418
+ const a = String(await wizIO.ask('选择 (序号或名称)', { default: '1' })).trim();
4419
+ if (!a)
4420
+ return choices[0]?.value || '';
4421
+ if (/^\d+$/.test(a) && choices[Number(a) - 1])
4422
+ return choices[Number(a) - 1].value;
4423
+ const hit = choices.find((c) => c.value === a) || choices.find((c) => a && c.value.startsWith(a));
4424
+ return hit?.value || a;
4425
+ },
4426
+ },
4427
+ });
4428
+ process.exit(res.ok ? 0 : 1);
4429
+ }
4336
4430
  if (args.supervise) {
4337
4431
  const { runStandaloneSupervisorHost } = await import('./agents/supervisor-host.js');
4338
4432
  const res = await runStandaloneSupervisorHost({
@@ -4381,14 +4475,37 @@ async function main() {
4381
4475
  // 2026-09-13: 首次运行引导 — 没有可用模型供应商 / 还没有用户身份时, 先走初始化向导
4382
4476
  // (放在 CLI 启动前: 用户先回答"你是谁 / 用哪个模型", 再进 TUI 面板)
4383
4477
  if (isCLIInteractive) {
4478
+ // 2026-09-16 (M4 启动硬门禁): 先把初始化事实读出来 —— 未就绪就**先修**, 修不好就非零退出。
4479
+ // 旧行为是"向导失败也只 warn, 照常进对话", 结果是半成品配置能进运行态 (看起来启动成功、实际不可执行)。
4480
+ const w = (m) => process.stderr.write(m.endsWith('\n') ? m : m + '\n');
4481
+ let gateEv = null;
4384
4482
  try {
4385
- const { isFirstRun, runSetupWizard } = await import('./cli/setup-wizard.js');
4386
- if (await isFirstRun()) {
4387
- await runSetupWizard({ interactive: true });
4483
+ const { refreshSetupState, describeSetup } = await import('./setup/setup-store.js');
4484
+ gateEv = await refreshSetupState({ light: true });
4485
+ if (gateEv.gate !== 'ready') {
4486
+ w(describeSetup(gateEv));
4487
+ if (process.env.BOLLOON_SKIP_SETUP === '1') {
4488
+ w('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
4489
+ }
4490
+ else {
4491
+ const { runSetupWizard } = await import('./cli/setup-wizard.js');
4492
+ await runSetupWizard({ interactive: true });
4493
+ const after = await refreshSetupState({});
4494
+ if (after.gate !== 'ready') {
4495
+ w(describeSetup(after));
4496
+ w('⛔ 初始化未完成 → 退出 (不会以"看起来能跑"的状态进入对话)');
4497
+ process.exit(1);
4498
+ }
4499
+ w('✅ 初始化完成, 进入正常模式');
4500
+ }
4388
4501
  }
4389
4502
  }
4390
4503
  catch (e) {
4391
- console.warn('[setup] 初始化向导失败 (不阻塞启动):', String(e?.message || e).slice(0, 200));
4504
+ // 评估/向导自身异常: fail-closed —— 不假装就绪
4505
+ w(`⛔ 初始化流程失败 (fail-closed): ${String(e?.message || e).slice(0, 200)}`);
4506
+ w(` 当前状态评估: ${gateEv ? gateEv.gate : '未知 (评估都没跑通)'}`);
4507
+ w(' 排查: `bolloon setup --status` (或删掉 ~/.bolloon/setup-state.json 后重跑 setup)');
4508
+ process.exit(1);
4392
4509
  }
4393
4510
  }
4394
4511
  if (isNonInteractive) {
@@ -4,11 +4,19 @@
4
4
  */
5
5
  import * as fs from 'fs/promises';
6
6
  import * as path from 'path';
7
- const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
7
+ import * as os from 'os';
8
+ import * as fsSync from 'fs';
9
+ /** 2026-09-16 (M1): 不再在模块加载时固定 HOME —— 独立宿主/测试注入/长驻进程都走同一个解析 */
10
+ function configDir() {
11
+ const env = process.env.BOLLOON_HOME?.trim();
12
+ if (env)
13
+ return env;
14
+ return path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon');
15
+ }
8
16
  // 2026-08-07: llm-config.json → bolloon-config.json (统一配置文件名, Bolloon 自己有写权限)
9
17
  // initialize() 会做一次迁移: 旧文件存在且新文件不存在 → 复制旧内容, 之后统一读写新文件
10
- const CONFIG_PATH = path.join(CONFIG_DIR, 'bolloon-config.json');
11
- const LEGACY_CONFIG_PATH = path.join(CONFIG_DIR, 'llm-config.json');
18
+ const CONFIG_PATH = path.join(configDir(), 'bolloon-config.json');
19
+ const LEGACY_CONFIG_PATH = path.join(configDir(), 'llm-config.json');
12
20
  export const DEFAULT_PROVIDER_CONFIGS = {
13
21
  openai: {
14
22
  enabled: false,
@@ -231,6 +239,10 @@ function getDefaultConfig() {
231
239
  class LLMConfigStore {
232
240
  config = null;
233
241
  initialized = false;
242
+ /** 2026-09-16: 记住加载时用的配置目录 —— 目录变了必须丢弃缓存, 否则会把 A 目录的配置写到 B 目录 */
243
+ loadedDir = null;
244
+ /** 2026-09-16: 记住加载时的文件签名 —— 外部改了配置 (repair/agent 工具/用户手改) 必须重新读, 不能一直用旧值 */
245
+ loadedSig = null;
234
246
  // v0.2.15: single-flight lock around read-modify-write of `~/.bolloon/llm-config.json`.
235
247
  // Prevents concurrent save() calls from clobbering each other when the user
236
248
  // configures two providers back-to-back (e.g. saving gemini, then anthropic, in
@@ -244,10 +256,19 @@ class LLMConfigStore {
244
256
  return next;
245
257
  }
246
258
  async initialize() {
259
+ // 目录变了 (换 HOME / 独立宿主 / 测试注入): 内存缓存作废, 重新读盘
260
+ const dir = configDir();
261
+ const sig = this.fileSignature();
262
+ if (this.initialized && (this.loadedDir !== dir || this.loadedSig !== sig)) {
263
+ this.initialized = false;
264
+ this.config = null;
265
+ }
247
266
  if (this.initialized)
248
267
  return;
268
+ this.loadedDir = dir;
269
+ this.loadedSig = sig;
249
270
  try {
250
- await fs.mkdir(CONFIG_DIR, { recursive: true });
271
+ await fs.mkdir(configDir(), { recursive: true });
251
272
  // 2026-08-07: 迁移 — 旧 llm-config.json 存在且新 bolloon-config.json 不存在时复制旧内容
252
273
  try {
253
274
  await fs.access(CONFIG_PATH);
@@ -289,6 +310,16 @@ class LLMConfigStore {
289
310
  // 2026-08-07: mode 0o600 — 仅当前用户可读写 (含 API key 的敏感配置); Bolloon 自身进程可写
290
311
  await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2), { mode: 0o600 });
291
312
  }
313
+ /** 配置文件签名 (mtime+size); 不存在时返回 'missing' */
314
+ fileSignature() {
315
+ try {
316
+ const st = fsSync.statSync(configDir() + '/bolloon-config.json');
317
+ return `${st.mtimeMs}:${st.size}`;
318
+ }
319
+ catch {
320
+ return 'missing';
321
+ }
322
+ }
292
323
  async getConfig() {
293
324
  await this.initialize();
294
325
  return { ...this.config };
@@ -619,6 +619,16 @@ export class AgentMessaging {
619
619
  console.warn(`[Messaging] Signature verification failed for ${signedMsg.from.substring(0, 20)}`);
620
620
  return false;
621
621
  }
622
+ // 2026-09-16 (2-C.4): 目标关联的真实外部事件 —— 签名已通过校验, 这里再按
623
+ // 来源 / correlation / 过期 / eventId 去重做一次严格匹配, 只唤醒"正在等这个事件"的 Goal。
624
+ // **不在这里启动 agent**: 只写事实 + 唤醒, 由 Supervisor 下一轮继续。
625
+ try {
626
+ const { tryDeliverGoalEvent } = await import('./goal-event-bridge.js');
627
+ await tryDeliverGoalEvent(signedMsg, fromPeerId);
628
+ }
629
+ catch (e) {
630
+ console.warn(`[Messaging] goal event bridge 失败 (不影响普通消息):`, e?.message);
631
+ }
622
632
  const handler = this.messageHandlers.get(signedMsg.type);
623
633
  if (handler) {
624
634
  handler(data, fromPeerId, signedMsg.from);