@bolloon/bolloon-agent 0.4.24 → 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 +446 -0
- package/dist/agents/external-events.js +162 -0
- package/dist/agents/goal-criteria.js +124 -0
- package/dist/agents/goal-store.js +526 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +607 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skill-readiness.js +133 -0
- package/dist/agents/skill-supervisor-link.js +70 -0
- package/dist/agents/skills-manager.js +717 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cli/setup-wizard.js +96 -127
- package/dist/cron/tick-lock.js +1 -1
- 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 +549 -26
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +21 -1
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +195 -1
- package/dist/ios/mobile-core.js +24876 -24723
- package/dist/ios/mobile.css +15 -0
- package/dist/ios/mobile.html +21 -1
- package/dist/ios/mobile.js +143 -0
- package/dist/ios/server.js +51 -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/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +2 -2
- package/dist/web/mobile-core.js +24884 -24726
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +633 -0
- package/package.json +2 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* supervisor-host.ts — Supervisor 的「宿主分离」层 (2026-09-16, 批次 2-C.1)
|
|
3
|
+
*
|
|
4
|
+
* 之前的问题: Supervisor 的**启动点**长在 web server 里, 执行器也直接引用 web 的 channel agent
|
|
5
|
+
* → 长期执行能力事实上依附于「web 进程还活着」。
|
|
6
|
+
*
|
|
7
|
+
* 这一层把两件事彻底分开:
|
|
8
|
+
* 执行器 (runner) ← 由 RunnerResolver 在**每次执行前**解析 (web agent / CLI agent / 独立 agent / fake)
|
|
9
|
+
* 启动宿主 (host) ← web server / CLI `/supervise` / 独立 `bolloon supervise` / 测试进程, 都只是 host
|
|
10
|
+
*
|
|
11
|
+
* 冻结的接口:
|
|
12
|
+
* Supervisor { scheduler · lease · reducer · wake } + runnerResolver
|
|
13
|
+
* resolver 解析不出来 → **只诊断不执行, 且不写任何 Goal 状态** (绝不假装跑过, 也绝不误判完成)
|
|
14
|
+
*
|
|
15
|
+
* 宿主身份与状态落盘 (~/.bolloon/supervisor.json): owner / workerId / pid / host / 启动时间 /
|
|
16
|
+
* 最近 tick / tick 次数 / runner 种类 / 优雅停止时间 —— 让"谁在跑、跑到哪、为什么停"可查。
|
|
17
|
+
*/
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as fsp from 'fs/promises';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as crypto from 'crypto';
|
|
22
|
+
import { acquireTickLock } from '../cron/tick-lock.js';
|
|
23
|
+
export function supervisorDir(home = os.homedir()) {
|
|
24
|
+
return path.join(home, '.bolloon', 'supervisor');
|
|
25
|
+
}
|
|
26
|
+
export function supervisorStatePath(home = os.homedir()) {
|
|
27
|
+
return path.join(home, '.bolloon', 'supervisor.json');
|
|
28
|
+
}
|
|
29
|
+
/** Supervisor tick 的跨进程锁 (与 cron 的 tick 锁同语义, 但不共用文件) */
|
|
30
|
+
export function supervisorTickLockPath(home = os.homedir()) {
|
|
31
|
+
return path.join(supervisorDir(home), '.tick.lock');
|
|
32
|
+
}
|
|
33
|
+
export async function readSupervisorState(home = os.homedir()) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(await fsp.readFile(supervisorStatePath(home), 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function writeSupervisorState(state, home = os.homedir()) {
|
|
42
|
+
const p = supervisorStatePath(home);
|
|
43
|
+
await fsp.mkdir(path.dirname(p), { recursive: true });
|
|
44
|
+
const tmp = `${p}.tmp`;
|
|
45
|
+
await fsp.writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
|
|
46
|
+
await fsp.rename(tmp, p); // 原子替换, 避免读到半截状态
|
|
47
|
+
}
|
|
48
|
+
export function newWorkerId() {
|
|
49
|
+
return `${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 启动一个 Supervisor 宿主。
|
|
53
|
+
* 语义要点:
|
|
54
|
+
* - **跨进程单 tick 互斥**: 拿不到 tick 锁的宿主本轮直接让路 (不阻塞不排队), 并如实记原因;
|
|
55
|
+
* - **优雅停止**: stop() 会等当前 tick 跑完再退出 (不半路丢状态), 并落 stoppedAt/stopReason;
|
|
56
|
+
* - 宿主身份与心跳落 `~/.bolloon/supervisor.json`, 谁在跑/跑到哪可查。
|
|
57
|
+
*/
|
|
58
|
+
export async function runSupervisorHost(opts) {
|
|
59
|
+
const home = opts.home ?? os.homedir();
|
|
60
|
+
const log = opts.log ?? (() => { });
|
|
61
|
+
const workerId = newWorkerId();
|
|
62
|
+
const state = {
|
|
63
|
+
owner: opts.supervisor.owner,
|
|
64
|
+
workerId,
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
host: os.hostname(),
|
|
67
|
+
startedAt: new Date().toISOString(),
|
|
68
|
+
runnerKind: opts.runnerKind ?? 'injected',
|
|
69
|
+
dryRun: !!opts.dryRun,
|
|
70
|
+
tickIntervalMs: opts.tickIntervalMs ?? 30_000,
|
|
71
|
+
leaseTtlMs: opts.leaseTtlMs ?? 90_000,
|
|
72
|
+
ticks: 0,
|
|
73
|
+
v: 1,
|
|
74
|
+
};
|
|
75
|
+
await writeSupervisorState(state, home);
|
|
76
|
+
const persist = async () => {
|
|
77
|
+
if (opts.extraState)
|
|
78
|
+
Object.assign(state, opts.extraState());
|
|
79
|
+
await writeSupervisorState(state, home);
|
|
80
|
+
};
|
|
81
|
+
// 让调用方随时能把状态写下去 (解析阶段报告一确定就可查, 不必等 tick 结束)
|
|
82
|
+
opts.onStateReady?.(persist);
|
|
83
|
+
const acquire = opts.acquireLock ?? acquireTickLock;
|
|
84
|
+
const useLock = opts.crossProcessLock !== false;
|
|
85
|
+
let stopping = false;
|
|
86
|
+
let ticking = false;
|
|
87
|
+
let timer = null;
|
|
88
|
+
const tickWithLock = async () => {
|
|
89
|
+
if (stopping || ticking)
|
|
90
|
+
return; // 进程内互斥
|
|
91
|
+
ticking = true;
|
|
92
|
+
const tickNo = (state.ticks ?? 0) + 1;
|
|
93
|
+
const startedMs = Date.now();
|
|
94
|
+
// 卡住的 tick 必须可观测 (长期运行时"没日志"和"卡死"必须能区分)
|
|
95
|
+
const watchdog = setInterval(() => {
|
|
96
|
+
log(`[supervisor-host] ⚠ tick #${tickNo} 已运行 ${Math.round((Date.now() - startedMs) / 1000)}s 仍未结束 (可能卡在 runner/session 创建)`);
|
|
97
|
+
}, 30_000);
|
|
98
|
+
watchdog.unref?.();
|
|
99
|
+
let lockPath;
|
|
100
|
+
let release;
|
|
101
|
+
try {
|
|
102
|
+
if (useLock) {
|
|
103
|
+
const r = await acquire({ lockPath: supervisorTickLockPath(home), staleMs: 300_000, log: () => { } });
|
|
104
|
+
if (!r.acquired) {
|
|
105
|
+
// 别的宿主在 tick → 本轮让路 (不阻塞), 原因写进状态, 可观测
|
|
106
|
+
state.lastTickAt = new Date().toISOString();
|
|
107
|
+
state.lastSummary = `本轮让路: tick 锁被 ${r.holder?.pid ?? '?'}@${r.holder?.host ?? '?'} 持有`;
|
|
108
|
+
await persist();
|
|
109
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
release = r.release;
|
|
113
|
+
lockPath = supervisorTickLockPath(home);
|
|
114
|
+
}
|
|
115
|
+
const rep = await opts.supervisor.tickOnce();
|
|
116
|
+
state.ticks = (state.ticks ?? 0) + 1;
|
|
117
|
+
state.lastTickAt = rep.at;
|
|
118
|
+
state.lastSummary = `#${rep.tick} 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`;
|
|
119
|
+
await persist();
|
|
120
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
state.lastTickAt = new Date().toISOString();
|
|
124
|
+
state.lastSummary = `tick 异常: ${String(err?.message || err).slice(0, 160)}`;
|
|
125
|
+
await persist().catch(() => { });
|
|
126
|
+
log(`[supervisor-host] ${state.lastSummary}`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
clearInterval(watchdog);
|
|
130
|
+
try {
|
|
131
|
+
await release?.();
|
|
132
|
+
}
|
|
133
|
+
catch { /* 锁释放失败: 陈旧回收兜底 */ }
|
|
134
|
+
void lockPath;
|
|
135
|
+
ticking = false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
if ((opts.mode ?? 'interval') === 'once') {
|
|
139
|
+
await tickWithLock();
|
|
140
|
+
state.stoppedAt = new Date().toISOString();
|
|
141
|
+
state.stopReason = 'once';
|
|
142
|
+
await writeSupervisorState(state, home);
|
|
143
|
+
return { stop: async () => { }, state };
|
|
144
|
+
}
|
|
145
|
+
// 宿主驱动 tick (不用 supervisor 自己的 interval, 避免两条 tick 循环重复推进同一个 Goal)
|
|
146
|
+
timer = setInterval(() => { void tickWithLock(); }, state.tickIntervalMs);
|
|
147
|
+
if (!opts.keepAlive)
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
log(`[supervisor-host] 启动 owner=${state.owner} worker=${workerId} tick=${state.tickIntervalMs}ms`);
|
|
150
|
+
// 启动后**立刻**跑一轮: 重启/接管时不该白等一个 tick 间隔才开始推进 (ticking 标志同步置位, stop() 会等它收尾)
|
|
151
|
+
void tickWithLock();
|
|
152
|
+
const stop = async (reason = 'requested') => {
|
|
153
|
+
if (stopping)
|
|
154
|
+
return;
|
|
155
|
+
stopping = true;
|
|
156
|
+
if (timer) {
|
|
157
|
+
clearInterval(timer);
|
|
158
|
+
timer = null;
|
|
159
|
+
}
|
|
160
|
+
opts.supervisor.stop();
|
|
161
|
+
// 等当前 tick 收尾 (最多 30s), 保证不半路丢状态
|
|
162
|
+
for (let i = 0; i < 300 && ticking; i++)
|
|
163
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
164
|
+
state.stoppedAt = new Date().toISOString();
|
|
165
|
+
state.stopReason = reason;
|
|
166
|
+
await persist().catch(() => { });
|
|
167
|
+
log(`[supervisor-host] 已优雅停止 (${reason})`);
|
|
168
|
+
};
|
|
169
|
+
return { stop, state };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 给独立宿主用的解析器: 按 Goal 的 channelId 建/复用**专用 agent session** 执行。
|
|
173
|
+
* 解析不出来 (没有 channelId / agent 不可用 / 显式关闭) → { ok:false }, Goal 只被诊断不被执行。
|
|
174
|
+
*/
|
|
175
|
+
/**
|
|
176
|
+
* 给独立宿主用的解析器: 委托给**分阶段** resolver (`runner-resolver.ts`)。
|
|
177
|
+
* 阶段报告通过 `onResolution` 交给宿主落盘 (lastResolution), 让 wakeReport/API/CLI 能回答
|
|
178
|
+
* "这个 Goal 为什么没被执行、卡在哪一阶段、耗时多少"。
|
|
179
|
+
*/
|
|
180
|
+
export function createLocalAgentResolver(opts) {
|
|
181
|
+
const cache = new Map();
|
|
182
|
+
return async (req) => {
|
|
183
|
+
const { resolveGoalRunner } = await import('./runner-resolver.js');
|
|
184
|
+
const res = await resolveGoalRunner(req, opts);
|
|
185
|
+
opts.onResolution?.(req.goal.goalId, res);
|
|
186
|
+
if (res.ok && res.runner)
|
|
187
|
+
cache.set(req.goal.goalId, res);
|
|
188
|
+
return res;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 起一个**独立**的 Supervisor 宿主。
|
|
193
|
+
* - 执行器由 `createLocalAgentResolver` 按 Goal 的 channelId 解析专用 agent session (懒建 + 复用);
|
|
194
|
+
* - `BOLLOON_SUPERVISE_AGENT=0` 或 dryRun → 解析器恒 ok:false → 只诊断不执行 (Goal 状态不动);
|
|
195
|
+
* - 常驻模式下进程保持存活, 收到 SIGINT/SIGTERM 优雅停止并落 stoppedAt。
|
|
196
|
+
*/
|
|
197
|
+
export async function runStandaloneSupervisorHost(opts = {}) {
|
|
198
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
199
|
+
const { ExecutionSupervisor } = await import('./execution-supervisor.js');
|
|
200
|
+
let lastResolution = null;
|
|
201
|
+
let persistNow = async () => { };
|
|
202
|
+
const resolver = opts.dryRun
|
|
203
|
+
? async (req) => ({ ok: false, kind: 'none', reason: `dry-run: 不执行 (goal=${req.goal.goalId})` })
|
|
204
|
+
: createLocalAgentResolver({
|
|
205
|
+
allow: process.env.BOLLOON_SUPERVISE_AGENT !== '0',
|
|
206
|
+
log,
|
|
207
|
+
home: opts.home,
|
|
208
|
+
onResolution: (goalId, res) => {
|
|
209
|
+
lastResolution = {
|
|
210
|
+
goalId, at: new Date().toISOString(), ok: !!res.ok,
|
|
211
|
+
failedStage: res.failedStage, reason: res.reason,
|
|
212
|
+
stages: (res.stages || []).map((x) => `${x.stage}${x.ok ? '✓' : '✗'}${x.ms}ms`).join(' → '),
|
|
213
|
+
detail: (res.stages || []).map((x) => ({ stage: x.stage, ok: x.ok, ms: x.ms, note: x.note, error: x.error, errorClass: x.errorClass })),
|
|
214
|
+
};
|
|
215
|
+
log(`[supervisor-host] 解析 ${res.ok ? '✅ 可执行' : `⛔ 卡在 ${res.failedStage}`} — ${res.reason || ''}`);
|
|
216
|
+
void persistNow().catch(() => { }); // 阶段报告立刻落盘
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const sup = new ExecutionSupervisor({
|
|
220
|
+
resolver: resolver,
|
|
221
|
+
maxPerTick: opts.maxPerTick ?? (Number(process.env.BOLLOON_SUPERVISOR_MAX_PER_TICK) || 1),
|
|
222
|
+
tickIntervalMs: opts.tickIntervalMs ?? (Number(process.env.BOLLOON_SUPERVISOR_TICK_MS) || 30_000),
|
|
223
|
+
leaseTtlMs: opts.leaseTtlMs ?? (Number(process.env.BOLLOON_SUPERVISOR_LEASE_MS) || 90_000),
|
|
224
|
+
log,
|
|
225
|
+
});
|
|
226
|
+
const host = await runSupervisorHost({
|
|
227
|
+
supervisor: sup,
|
|
228
|
+
mode: opts.once ? 'once' : 'interval',
|
|
229
|
+
keepAlive: !opts.once,
|
|
230
|
+
tickIntervalMs: sup.status().tickIntervalMs,
|
|
231
|
+
leaseTtlMs: sup.status().leaseTtlMs,
|
|
232
|
+
runnerKind: opts.dryRun ? 'none' : 'standalone',
|
|
233
|
+
dryRun: !!opts.dryRun,
|
|
234
|
+
home: opts.home,
|
|
235
|
+
log,
|
|
236
|
+
extraState: () => (lastResolution ? { lastResolution } : {}),
|
|
237
|
+
onStateReady: (write) => { persistNow = write; },
|
|
238
|
+
});
|
|
239
|
+
if (opts.once) {
|
|
240
|
+
log(`[supervisor-host] once 完成: ${host.state.lastSummary || '(空)'}`);
|
|
241
|
+
return { ticks: host.state.ticks ?? 0, state: host.state, lastReport: sup.status().lastReport };
|
|
242
|
+
}
|
|
243
|
+
const onSignal = (sig) => {
|
|
244
|
+
void host.stop(`signal:${sig}`).then(() => process.exit(0));
|
|
245
|
+
};
|
|
246
|
+
process.once('SIGINT', () => onSignal('SIGINT'));
|
|
247
|
+
process.once('SIGTERM', () => onSignal('SIGTERM'));
|
|
248
|
+
return { ticks: host.state.ticks ?? 0, state: host.state, lastReport: null };
|
|
249
|
+
}
|
package/dist/cli/setup-wizard.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
244
|
-
userName:
|
|
245
|
-
provider,
|
|
246
|
-
model,
|
|
247
|
-
identityFile:
|
|
248
|
-
identityCreated:
|
|
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();
|
package/dist/cron/tick-lock.js
CHANGED
|
@@ -125,7 +125,7 @@ async function reclaim(lockPath, log, holder, reason) {
|
|
|
125
125
|
*/
|
|
126
126
|
export async function acquireTickLock(opts = {}) {
|
|
127
127
|
const home = opts.home ?? os.homedir();
|
|
128
|
-
const lockPath = tickLockPath(home);
|
|
128
|
+
const lockPath = opts.lockPath ?? tickLockPath(home);
|
|
129
129
|
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
130
130
|
const now = (opts.now ?? (() => new Date()))();
|
|
131
131
|
const log = opts.log ?? noopLogger;
|
|
@@ -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
|
-
|
|
149
|
+
const fact = readSetupFact();
|
|
150
|
+
if (fact.ready && hasSeenFirstRun())
|
|
125
151
|
return;
|
|
126
|
-
|
|
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
|
-
|
|
191
|
+
const fact = readSetupFact();
|
|
192
|
+
if (fact.ready && hasSeenFirstRun())
|
|
165
193
|
return;
|
|
166
|
-
(
|
|
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":"
|
|
1
|
+
{"version":3,"file":"first-run.js","sourceRoot":"","sources":["../../../src/electron/first-run.ts"],"names":[],"mappings":"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"}
|