@bolloon/bolloon-agent 0.6.1 → 0.6.3
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/README.md +128 -111
- package/dist/agents/gateway-group.js +231 -6
- package/dist/agents/group-autopilot.js +256 -0
- package/dist/agents/pi-sdk-tools.js +302 -0
- package/dist/agents/pi-sdk.js +89 -140
- package/dist/agents/task-group.js +21 -4
- package/dist/cli/log-gate.js +68 -4
- package/dist/cli/mention-data.js +1 -1
- package/dist/cli/model-selector.js +84 -104
- package/dist/cli/setup-wizard.js +2 -10
- package/dist/cli/tui-select.js +99 -10
- package/dist/cli-entry.js +1 -9
- package/dist/electron-build/agents/gateway-group.js +814 -0
- package/dist/electron-build/agents/gateway-group.js.map +1 -0
- package/dist/electron-build/agents/group-autopilot.js +302 -0
- package/dist/electron-build/agents/group-autopilot.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +302 -0
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +88 -139
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/task-group.js +601 -0
- package/dist/electron-build/agents/task-group.js.map +1 -0
- package/dist/electron-build/agents/task-public-text.js +97 -0
- package/dist/electron-build/agents/task-public-text.js.map +1 -0
- package/dist/electron-build/efficode/compress.js +86 -0
- package/dist/electron-build/efficode/compress.js.map +1 -0
- package/dist/electron-build/efficode/crc32.js +44 -0
- package/dist/electron-build/efficode/crc32.js.map +1 -0
- package/dist/electron-build/efficode/did.js +74 -0
- package/dist/electron-build/efficode/did.js.map +1 -0
- package/dist/electron-build/efficode/lz77.js +175 -0
- package/dist/electron-build/efficode/lz77.js.map +1 -0
- package/dist/electron-build/efficode/negotiate.js +233 -0
- package/dist/electron-build/efficode/negotiate.js.map +1 -0
- package/dist/electron-build/efficode/ops.js +96 -0
- package/dist/electron-build/efficode/ops.js.map +1 -0
- package/dist/electron-build/efficode/packet.js +279 -0
- package/dist/electron-build/efficode/packet.js.map +1 -0
- package/dist/electron-build/efficode/types.js +50 -0
- package/dist/electron-build/efficode/types.js.map +1 -0
- package/dist/electron-build/kernel/code-write-gate.js +55 -0
- package/dist/electron-build/kernel/code-write-gate.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +23 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -1
- package/dist/electron-build/kernel/run-resume.js +62 -0
- package/dist/electron-build/kernel/run-resume.js.map +1 -0
- package/dist/electron-build/kernel/session-lifecycle.js +223 -0
- package/dist/electron-build/kernel/session-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/turn-selfcheck.js +65 -0
- package/dist/electron-build/kernel/turn-selfcheck.js.map +1 -0
- package/dist/electron-build/llm/config-store.js +2 -2
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/model-selection.js +16 -2
- package/dist/electron-build/llm/model-selection.js.map +1 -1
- package/dist/electron-build/llm/pi-ai.js +2 -2
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/orbitdb/cid-database.js +98 -19
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/group-access.js +399 -0
- package/dist/electron-build/orbitdb/group-access.js.map +1 -0
- package/dist/index.js +143 -10
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/sw.js +1 -1
- package/dist/kernel/code-write-gate.js +47 -0
- package/dist/kernel/gate-scan.js +5 -1
- package/dist/kernel/plan-harness.js +3 -2
- package/dist/kernel/roster.js +6 -6
- package/dist/kernel/run-lifecycle.js +21 -0
- package/dist/kernel/run-resume.js +58 -0
- package/dist/kernel/session-lifecycle.js +210 -0
- package/dist/kernel/turn-selfcheck.js +60 -0
- package/dist/llm/config-store.js +2 -2
- package/dist/llm/model-selection.js +16 -2
- package/dist/llm/pi-ai.js +2 -2
- package/dist/orbitdb/cid-database.js +95 -19
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-lifecycle.ts — K10 余项: **会话生命周期端口** (把 pi-sdk 的运行/通道接线继续薄化)
|
|
3
|
+
*
|
|
4
|
+
* 搬走的是**规则**, 不是 I/O:
|
|
5
|
+
* · `save-session` : 消息 → 落盘形态的映射 (role/content/toolCall/toolResult/toolCallId/timestamp/source) + key 校验
|
|
6
|
+
* · `peek-history` : 读回后的**过滤/水合规则** (合法 role · 剔污染消息 · **保留"只有 tool call 没有正文"的合法消息**)
|
|
7
|
+
* · `resume-session` : 同上 + 把水合结果**应用**回历史 (应用由端口做), 返回新增条数
|
|
8
|
+
* · `seed-run` : 新一次运行的上下文种子 —— runId 来自**内核的活跃运行** (单一来源), extra 覆盖它 (**合并顺序是规则**)
|
|
9
|
+
*
|
|
10
|
+
* I/O (store 读写 · actor 快照 · 上下文工厂) 全部**端口注入** ⇒ 换存储 / 换 actor 实现只改注入点一处。
|
|
11
|
+
*
|
|
12
|
+
* 形状照抄 `run-lifecycle.ts` / `transport.ts` / `control.ts` (同一个仓库里只该有一种端口写法):
|
|
13
|
+
* 端口注入 · **未注入即拒** (不静默降级) · 一律**返回结果对象不抛** · 端口用 `{ok:false}` 表达拒绝要被归一化 · 审计流水可读。
|
|
14
|
+
*
|
|
15
|
+
* 哪些**留在** pi-sdk: 谁在什么时候调 (入口/时机) + 端口实现 (真 store / 真 actor)。本文件不 import 任何 agent 侧模块。
|
|
16
|
+
*/
|
|
17
|
+
export const SESSION_OPS = ['save-session', 'peek-history', 'resume-session', 'seed-run'];
|
|
18
|
+
/** 哪些操作必须有会话 key (seed-run 例外: 它要的是**活跃运行**, 不是会话 key) */
|
|
19
|
+
export const SESSION_NEEDS_KEY = {
|
|
20
|
+
'save-session': true,
|
|
21
|
+
'peek-history': true,
|
|
22
|
+
'resume-session': true,
|
|
23
|
+
'seed-run': false,
|
|
24
|
+
};
|
|
25
|
+
/** 端口**不抛异常但明确拒绝** (`{ ok: false, reason }`) 的归一化 —— 与 `control.ts` / `run-lifecycle.ts` 同口径 */
|
|
26
|
+
function portRefusal(res) {
|
|
27
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
28
|
+
const r = res;
|
|
29
|
+
if (r.ok === false)
|
|
30
|
+
return `端口拒绝: ${String(r.reason ?? '(未给原因)')}`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const audit = [];
|
|
35
|
+
const AUDIT_MAX = 200;
|
|
36
|
+
/** 审计流水 (最近 N 条; 测试与运维可读) */
|
|
37
|
+
export function sessionLifecycleAudit() {
|
|
38
|
+
return audit;
|
|
39
|
+
}
|
|
40
|
+
export function resetSessionLifecycleAudit() {
|
|
41
|
+
audit.length = 0;
|
|
42
|
+
}
|
|
43
|
+
function record(op, origin, target, ok, detail) {
|
|
44
|
+
audit.push({ at: Date.now(), op, origin, target, ok, detail });
|
|
45
|
+
if (audit.length > AUDIT_MAX)
|
|
46
|
+
audit.splice(0, audit.length - AUDIT_MAX);
|
|
47
|
+
}
|
|
48
|
+
// ── 纯规则 (可单独判: 不碰 I/O) ──────────────────────────────────────────────
|
|
49
|
+
/** 会话 key 必须是非空字符串 —— 空 key 会让写入落到一个"共享黑洞"里, 必须当场拒 */
|
|
50
|
+
export function isValidSessionKey(key) {
|
|
51
|
+
return typeof key === 'string' && key.trim().length > 0;
|
|
52
|
+
}
|
|
53
|
+
/** 合法角色 (拒绝旧 schema `{type:'user'}` 那种没有 role 字段的) */
|
|
54
|
+
export const SESSION_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
|
|
55
|
+
/**
|
|
56
|
+
* 落盘映射: 实时消息 → 持久形态。
|
|
57
|
+
* `timestamp` 取**写盘那一刻** (不是消息里的旧值) —— 迁移前就是这个口径, 明确保留。
|
|
58
|
+
* `source` 标记写入方 (默认 'pi-session')。
|
|
59
|
+
*/
|
|
60
|
+
export function toPersistedMessages(source, sourceTag = 'pi-session', now = Date.now) {
|
|
61
|
+
const ts = now();
|
|
62
|
+
return (source ?? []).map((raw) => {
|
|
63
|
+
const m = (raw ?? {});
|
|
64
|
+
return {
|
|
65
|
+
role: m.role,
|
|
66
|
+
content: m.content,
|
|
67
|
+
toolCall: m.toolCall,
|
|
68
|
+
toolResult: m.toolResult,
|
|
69
|
+
toolCallId: m.toolCallId,
|
|
70
|
+
timestamp: ts,
|
|
71
|
+
source: sourceTag,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 读回水合 + 过滤规则 (逐字搬自 pi-sdk, 只去掉 instance 依赖)。
|
|
77
|
+
* 三条"必须保留"的坑都在这:
|
|
78
|
+
* ① `content === ''` 但带 toolCall 的 assistant 消息**合法** (LLM 只输出工具调用没有正文) ⇒ 不许按"空内容"丢掉;
|
|
79
|
+
* ② 「[AI 服务调用失败]」/「[错误:」开头的污染消息要剔;
|
|
80
|
+
* ③ tool role 但没有 toolResult 的占位消息要剔。
|
|
81
|
+
*/
|
|
82
|
+
export function filterSessionMessages(loaded) {
|
|
83
|
+
if (!loaded)
|
|
84
|
+
return [];
|
|
85
|
+
const hydrated = [];
|
|
86
|
+
for (const m of loaded) {
|
|
87
|
+
if (!m || !SESSION_ROLES.has(String(m.role)))
|
|
88
|
+
continue;
|
|
89
|
+
if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
|
|
90
|
+
continue;
|
|
91
|
+
if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
|
|
92
|
+
continue;
|
|
93
|
+
if (!m.content && !m.toolCall && !m.toolResult)
|
|
94
|
+
continue;
|
|
95
|
+
if (m.role === 'tool' && !m.toolResult)
|
|
96
|
+
continue;
|
|
97
|
+
hydrated.push({
|
|
98
|
+
role: m.role,
|
|
99
|
+
content: m.content ?? '',
|
|
100
|
+
toolCall: m.toolCall,
|
|
101
|
+
toolResult: m.toolResult,
|
|
102
|
+
toolCallId: m.toolCallId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return hydrated;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 过滤 + 截断 (取**最近** maxMessages 条) —— 给"自己不做截断"的调用方用。
|
|
109
|
+
* actor 的 `hydrateHistory(spec)` 管道自带截断 ⇒ 那边只该用 `filterSessionMessages` (别截两次)。
|
|
110
|
+
*/
|
|
111
|
+
export function hydrateSessionMessages(loaded, maxMessages = 30) {
|
|
112
|
+
const filtered = filterSessionMessages(loaded);
|
|
113
|
+
const max = Number.isFinite(maxMessages) && maxMessages > 0 ? Math.floor(maxMessages) : 0;
|
|
114
|
+
return max > 0 ? filtered.slice(-max) : [];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 运行种子 = 活跃运行的 runId 打底 + `extra` 覆盖它 (**合并顺序是规则, 所以它归内核**)。
|
|
118
|
+
* 与 `seed-run` 分支共用这一条规则; 同步调用点 (pi-sdk 的 `seedRunContext` 是同步签名) 直接调它, 不必为了合并一次规则绕异步端口。
|
|
119
|
+
*/
|
|
120
|
+
export function composeRunSeed(activeRunId, extra = {}) {
|
|
121
|
+
return { runId: activeRunId, ...extra };
|
|
122
|
+
}
|
|
123
|
+
// ── 派发 (唯一入口) ───────────────────────────────────────────────────────────
|
|
124
|
+
const missing = (what) => `端口未注入: ${what} (拒绝静默降级)`;
|
|
125
|
+
/**
|
|
126
|
+
* 会话生命周期的**唯一系统调用门**。
|
|
127
|
+
* 未注入端口 / 缺 key ⇒ `{ ok:false }` (不抛); 端口抛错 ⇒ 也归一化成 `{ ok:false, detail }` (调用方按自己的响亮口径处理)。
|
|
128
|
+
*/
|
|
129
|
+
export async function submitSessionOp(request, ports) {
|
|
130
|
+
const op = request?.op;
|
|
131
|
+
const origin = String(request?.origin ?? '').trim() || 'unknown';
|
|
132
|
+
const target = String(request?.key ?? '').trim() || '-';
|
|
133
|
+
const fail = (detail) => {
|
|
134
|
+
record(op, origin, target, false, detail);
|
|
135
|
+
return { ok: false, op, via: 'kernel-session-lifecycle', detail };
|
|
136
|
+
};
|
|
137
|
+
if (!SESSION_OPS.includes(op))
|
|
138
|
+
return fail(`未知会话操作: ${String(op)}`);
|
|
139
|
+
if (!origin || origin === 'unknown')
|
|
140
|
+
return fail('缺少 origin (审计要求, 不许匿名)');
|
|
141
|
+
if (SESSION_NEEDS_KEY[op] && !isValidSessionKey(request.key))
|
|
142
|
+
return fail(`缺少合法会话 key (op=${op})`);
|
|
143
|
+
try {
|
|
144
|
+
if (op === 'save-session') {
|
|
145
|
+
const key = request.key;
|
|
146
|
+
if (!ports.saveMessages)
|
|
147
|
+
return fail(missing('saveMessages'));
|
|
148
|
+
if (!ports.historySnapshot)
|
|
149
|
+
return fail(missing('historySnapshot'));
|
|
150
|
+
const source = (await ports.historySnapshot()) ?? [];
|
|
151
|
+
const persisted = toPersistedMessages(source);
|
|
152
|
+
const res = await ports.saveMessages(key, persisted);
|
|
153
|
+
const refusal = portRefusal(res);
|
|
154
|
+
if (refusal)
|
|
155
|
+
return fail(refusal);
|
|
156
|
+
record(op, origin, target, true);
|
|
157
|
+
return { ok: true, op, via: 'kernel-session-lifecycle', port: 'saveMessages', result: { count: persisted.length } };
|
|
158
|
+
}
|
|
159
|
+
if (op === 'peek-history') {
|
|
160
|
+
const key = request.key;
|
|
161
|
+
if (!ports.loadMessages)
|
|
162
|
+
return fail(missing('loadMessages'));
|
|
163
|
+
const loaded = await ports.loadMessages(key);
|
|
164
|
+
const hydrated = hydrateSessionMessages(loaded, request.maxMessages ?? 30);
|
|
165
|
+
record(op, origin, target, true);
|
|
166
|
+
return { ok: true, op, via: 'kernel-session-lifecycle', port: 'loadMessages', result: hydrated };
|
|
167
|
+
}
|
|
168
|
+
if (op === 'resume-session') {
|
|
169
|
+
const key = request.key;
|
|
170
|
+
if (!ports.loadMessages)
|
|
171
|
+
return fail(missing('loadMessages'));
|
|
172
|
+
if (!ports.applyHistory)
|
|
173
|
+
return fail(missing('applyHistory'));
|
|
174
|
+
const loaded = await ports.loadMessages(key);
|
|
175
|
+
const hydrated = hydrateSessionMessages(loaded, request.maxMessages ?? 30);
|
|
176
|
+
const applied = (await ports.applyHistory(hydrated)) ?? 0;
|
|
177
|
+
const refusal = portRefusal(applied);
|
|
178
|
+
if (refusal)
|
|
179
|
+
return fail(refusal);
|
|
180
|
+
record(op, origin, target, true, `应用 ${hydrated.length} 条`);
|
|
181
|
+
return { ok: true, op, via: 'kernel-session-lifecycle', port: 'applyHistory', result: Number(applied) || 0 };
|
|
182
|
+
}
|
|
183
|
+
// seed-run
|
|
184
|
+
if (!ports.activeRunId)
|
|
185
|
+
return fail(missing('activeRunId'));
|
|
186
|
+
if (!ports.newRunContext)
|
|
187
|
+
return fail(missing('newRunContext'));
|
|
188
|
+
const activeRun = ports.activeRunId();
|
|
189
|
+
const extra = (request.extra ?? {});
|
|
190
|
+
// 合并顺序是规则: 先放活跃运行得到的 runId, 再让 extra 覆盖 (调用方显式传的优先)
|
|
191
|
+
const seed = composeRunSeed(activeRun, extra);
|
|
192
|
+
const ctx = ports.newRunContext(seed.runId, extra);
|
|
193
|
+
const refusal = portRefusal(ctx);
|
|
194
|
+
if (refusal)
|
|
195
|
+
return fail(refusal);
|
|
196
|
+
record(op, origin, target, true, `runId=${String(seed.runId ?? '(无)')}`);
|
|
197
|
+
return { ok: true, op, via: 'kernel-session-lifecycle', port: 'newRunContext', result: ctx };
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
return fail(`端口抛错: ${String(err?.message ?? err).slice(0, 200)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 需要**响亮失败**的调用方 (写不进 = 事实层面没发生) 在派发后调它。
|
|
205
|
+
* 与 `assertLifecycleOk` 同口径: 把 `{ok:false}` 变成抛, 而不是让调用方忘掉检查。
|
|
206
|
+
*/
|
|
207
|
+
export function assertSessionOk(out) {
|
|
208
|
+
if (!out.ok)
|
|
209
|
+
throw new Error(`[kernel-session-lifecycle] ${out.op} 失败: ${out.detail ?? '(无原因)'}`);
|
|
210
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn-selfcheck.ts — K10 余项: **回合收尾自检的编排与政策** (从 pi-sdk 的方法体搬进内核)
|
|
3
|
+
*
|
|
4
|
+
* 搬走的是**政策** (不是 I/O):
|
|
5
|
+
* ① 该不该跑 —— 由纯规则 `decideTypecheck` 决定 (改过 TS 且本回合没跑过), 规则本体在 `code-write-gate.ts`;
|
|
6
|
+
* ② **系统自检也过门** —— 它不是模型发起的工具调用 (args 恒空, 是本回合改过 TS 后的收尾自检),
|
|
7
|
+
* 但**更不能例外**: 例外一旦靠"没人知道它绕过"活着, 门就不再是唯一的执行咽喉;
|
|
8
|
+
* ③ **fail-closed**: 门被拒 **或门本身抛错** ⇒ **不执行**, 且要**可见地报出来** (拒绝不许静默);
|
|
9
|
+
* ④ 执行抛错 ⇒ 报一行"没能跑起来" (附"改动已落盘, 记得自己跑一次"), **绝不让自检失败影响回合结果**。
|
|
10
|
+
*
|
|
11
|
+
* 为什么归内核: 这三条都是**政策**, 不是某个入口的私事 —— 换入口 (CLI/Web/cron) 时不许各写一份。
|
|
12
|
+
* I/O 全部端口注入: 取工具 · 问门 · 上报 (emit)。本模块不 import 任何 agent 侧模块。
|
|
13
|
+
*/
|
|
14
|
+
import { decideTypecheck, formatTypecheckResult } from './code-write-gate.js';
|
|
15
|
+
/** 自检用的工具名 (与工具注册表里的名字一致) */
|
|
16
|
+
export const TURN_SELFCHECK_TOOL = 'tsc_check';
|
|
17
|
+
/**
|
|
18
|
+
* 回合收尾自检 —— **唯一入口**。语义与迁移前逐条一致:
|
|
19
|
+
* 不需要跑 ⇒ `{decided:false}`; 工具缺失 ⇒ 安静地不做 (迁移前也是静默);
|
|
20
|
+
* 门拒/门抛 ⇒ 不执行 + **上屏一行"被拒, 未执行: 原因"**; 执行成功 ⇒ 上屏一行结果;
|
|
21
|
+
* 执行抛错 ⇒ 上屏一行"没能跑起来"。**本函数从不抛**。
|
|
22
|
+
*/
|
|
23
|
+
export async function runTurnEndSelfCheck(spec, ports) {
|
|
24
|
+
if (!decideTypecheck([...spec.touched], spec.ranThisTurn)) {
|
|
25
|
+
return { ran: false, decided: false, line: '' };
|
|
26
|
+
}
|
|
27
|
+
const toolName = spec.toolName ?? TURN_SELFCHECK_TOOL;
|
|
28
|
+
const tool = ports.getTool(toolName);
|
|
29
|
+
if (!tool?.execute) {
|
|
30
|
+
return { ran: false, decided: true, line: '', reason: `工具缺失: ${toolName}` };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
let allowed = true;
|
|
34
|
+
let why = '';
|
|
35
|
+
try {
|
|
36
|
+
const d = await ports.askGate({ tool: toolName, args: {} });
|
|
37
|
+
allowed = d?.allow === true;
|
|
38
|
+
why = String(d?.reason || d?.rejectedBy || d?.source || '');
|
|
39
|
+
}
|
|
40
|
+
catch (gateErr) {
|
|
41
|
+
allowed = false;
|
|
42
|
+
why = `harness-error: ${String(gateErr?.message || gateErr)}`;
|
|
43
|
+
}
|
|
44
|
+
if (!allowed) {
|
|
45
|
+
const line = `🔎 类型检查被门拒绝, 未执行: ${why.slice(0, 120)}`;
|
|
46
|
+
ports.emit({ type: 'status', content: line, tool: 'system' });
|
|
47
|
+
return { ran: false, decided: true, rejected: true, line, reason: why };
|
|
48
|
+
}
|
|
49
|
+
const r = (await tool.execute({}));
|
|
50
|
+
const passed = r?.success !== false && !/error TS\d+/.test(String(r?.output || ''));
|
|
51
|
+
const line = `🔎 ${formatTypecheckResult(passed, String(r?.output || ''))}`;
|
|
52
|
+
ports.emit({ type: 'status', content: line, tool: 'system' });
|
|
53
|
+
return { ran: true, decided: true, passed, line };
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
const line = `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`;
|
|
57
|
+
ports.emit({ type: 'status', content: line, tool: 'system' });
|
|
58
|
+
return { ran: false, decided: true, line, reason: String(e?.message || e).slice(0, 120) };
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/llm/config-store.js
CHANGED
|
@@ -161,8 +161,8 @@ export const PROVIDER_INFO = {
|
|
|
161
161
|
// 2026-07-17: V3 系列 (deepseek-chat / deepseek-reasoner) 官方已下线, 改 V4
|
|
162
162
|
deepseek: { name: 'DeepSeek', description: '深度求索大模型 (V4)', requiresApiKey: true, models: ['deepseek-v4-flash', 'deepseek-v4-pro'] },
|
|
163
163
|
kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot Kimi K 系列模型', requiresApiKey: true, models: ['kimi-k3', 'kimi-k2-0905', 'moonshot-v1-128k', 'moonshot-v1-32k'] },
|
|
164
|
-
glm: { name: 'GLM (智谱)', description: '智谱 GLM 系列模型', requiresApiKey: true, models: ['glm-5.2', 'glm-4.6', 'glm-4.5', 'glm-4-flash', 'glm-4'] },
|
|
165
|
-
qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen3-max', 'qwen-max', 'qwen-plus', 'qwen-turbo'] },
|
|
164
|
+
glm: { name: 'GLM (智谱)', description: '智谱 GLM 系列模型', requiresApiKey: true, models: ['glm-5.3-flash', 'glm-5.3', 'glm-5.2', 'glm-5', 'glm-4.6', 'glm-4.5', 'glm-4.7', 'glm-4-flash', 'glm-4'] },
|
|
165
|
+
qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen3.8-max', 'qwen3-max', 'qwen-max', 'qwen-plus', 'qwen-turbo'] },
|
|
166
166
|
mimo: { name: 'MiMo (小米)', description: '小米 MiMo V2 系列 (openai 兼容)', requiresApiKey: true, models: ['mimo-v2.5-pro', 'mimo-v2-pro', 'mimo-v2-omni', 'mimo-v2-flash', 'mimo-v2.5-pro-ultraspeed'] },
|
|
167
167
|
grok: { name: 'Grok (xAI)', description: 'xAI Grok 系列模型 (openai 兼容)', requiresApiKey: true, models: ['grok-4.5', 'grok-4', 'grok-4-fast'] },
|
|
168
168
|
local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
|
|
@@ -763,7 +763,21 @@ export function validateSelection(req, ctx) {
|
|
|
763
763
|
return { ok: false, failureClass: 'invalid_model', message: `${provider} 没有可用模型 (配置为空且默认模型为空)` };
|
|
764
764
|
}
|
|
765
765
|
let baseUrl = base.baseUrl;
|
|
766
|
-
|
|
766
|
+
const urlAction = req.baseUrlAction ?? (req.baseUrl !== undefined ? 'replace' : 'keep');
|
|
767
|
+
if (urlAction === 'clear') {
|
|
768
|
+
// 清除 URL 覆盖 → 回落**官方默认** (内置表/注册表声明的那个, 不含配置里覆盖过的值)。
|
|
769
|
+
// leo 2026-10-05: 「展示的 URL 默认需要官方的来源」—— 清除 = 回到官方, 不是回到上一个手填值。
|
|
770
|
+
const official = baseDefaultsOf(provider, null);
|
|
771
|
+
baseUrl = normalizeBaseUrl(official?.baseUrl || '');
|
|
772
|
+
if (!baseUrl && regEntry && regEntry.origin === 'catalog') {
|
|
773
|
+
return {
|
|
774
|
+
ok: false,
|
|
775
|
+
failureClass: 'invalid_url',
|
|
776
|
+
message: `${provider} 在目录里没有官方 api 基址 → 无法回落官方默认, 必须自定义 baseUrl (加 --base-url <地址> 或写进配置)`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
else if (req.baseUrl !== undefined) {
|
|
767
781
|
const shape = validateBaseUrlShape(req.baseUrl);
|
|
768
782
|
if (!shape.ok) {
|
|
769
783
|
return { ok: false, failureClass: 'invalid_url', message: `base URL 非法 — ${shape.reason}` };
|
|
@@ -974,7 +988,7 @@ export async function selectModel(req) {
|
|
|
974
988
|
const p = await runConnectionProbe({
|
|
975
989
|
provider: target.provider,
|
|
976
990
|
model: target.model,
|
|
977
|
-
baseUrl:
|
|
991
|
+
baseUrl: target.baseUrl, // 校验已按 URL 意图算好 (keep=当前 / replace=新值 / clear=官方默认)
|
|
978
992
|
configuredBaseUrl: stored?.baseUrl, // 配置里这一家现在写的地址
|
|
979
993
|
apiKey: target.apiKey,
|
|
980
994
|
});
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -752,7 +752,7 @@ export class PiAIModel {
|
|
|
752
752
|
// 2026-07-17: deepseek-chat (V3) 官方已下线, 迁 deepseek-v4-flash
|
|
753
753
|
deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
|
|
754
754
|
kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'kimi-k3',
|
|
755
|
-
glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-5.
|
|
755
|
+
glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-5.3-flash',
|
|
756
756
|
qwen: this.config.model || process.env.QWEN_MODEL || process.env.DASHSCOPE_MODEL || 'qwen3-max',
|
|
757
757
|
// 小米 MiMo (openai 兼容) — env override 优先, 默认 mimo-v2.5-pro
|
|
758
758
|
mimo: this.config.model || process.env.MIMO_MODEL || 'mimo-v2.5-pro',
|
|
@@ -1375,7 +1375,7 @@ function detectModel(provider) {
|
|
|
1375
1375
|
// 2026-07-17: V3 官方下线, 迁 V4
|
|
1376
1376
|
deepseek: 'deepseek-v4-flash',
|
|
1377
1377
|
kimi: 'kimi-k3',
|
|
1378
|
-
glm: 'glm-5.
|
|
1378
|
+
glm: 'glm-5.3-flash',
|
|
1379
1379
|
qwen: 'qwen3-max',
|
|
1380
1380
|
// 小米 MiMo 默认走最新旗舰版 (v2.5-Pro); 2026-06 当前公开版
|
|
1381
1381
|
mimo: 'mimo-v2.5-pro',
|
|
@@ -44,6 +44,42 @@ import * as os from 'os';
|
|
|
44
44
|
* 2026-09-24: 原来 `openStoreByAddress` 失败返回 null, 上游 (gateway-group) 把它当"空群",
|
|
45
45
|
* 于是"读不到"被显示成"没有消息"。改成**抛**: 打不开 ≠ 没内容。
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* **有界等待** (2026-10-02): 没有它时"打不开"会变成**永远卡住** ——
|
|
49
|
+
* 实测直接调 `createGroup` 的探针 300s 都没返回 (进程被强杀)。
|
|
50
|
+
* 超时抛出的错误**带卡点** (哪一步 · 等了多久), 上游才能给出可行动的话术。
|
|
51
|
+
*/
|
|
52
|
+
export async function withStageTimeout(stage, ms, p) {
|
|
53
|
+
let timer;
|
|
54
|
+
try {
|
|
55
|
+
return await Promise.race([
|
|
56
|
+
p,
|
|
57
|
+
new Promise((_, reject) => {
|
|
58
|
+
timer = setTimeout(() => reject(new Error(`[orbitdb] ${stage} 超时 (${ms}ms) —— 这一步没能在预算内完成`)), ms);
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (timer)
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 把底层错误翻成**可行动的原因** (2026-10-02)。
|
|
69
|
+
* 最常见的两种原文都毫无提示: `Database failed to open` 与裸的 `ELOCKED` ——
|
|
70
|
+
* 真身几乎总是"**另一个 bolloon 实例正握着 keystore 单写锁**"(同机同 dataDir 只允许一个进程写)。
|
|
71
|
+
*/
|
|
72
|
+
export function describeOrbitDbFailure(err) {
|
|
73
|
+
// 有些底层库抛的是**没有 message 的对象** (只有 code, 如 `{ code: 'ELOCKED' }`) ⇒ 别让 String() 把它变成 `[object Object]`
|
|
74
|
+
const raw = String(err?.message ?? err?.code ?? err ?? '');
|
|
75
|
+
if (/Resource temporarily unavailable|ELOCKED|lock .*LOCK|Database failed to open/i.test(raw)) {
|
|
76
|
+
return 'OrbitDB 库被另一个进程占着 (同机同一 dataDir 只允许一个进程写; 常见原因: 另有一个 bolloon 实例在跑)。'
|
|
77
|
+
+ ' 处理: 退出其它实例, 或设 `BOLLOON_HOME=<独立目录>` 跑一个隔离实例。原始错误: ' + raw.slice(0, 160);
|
|
78
|
+
}
|
|
79
|
+
if (/超时 \(\d+ms\)/.test(raw))
|
|
80
|
+
return raw.slice(0, 220);
|
|
81
|
+
return raw.slice(0, 200) || '未知原因';
|
|
82
|
+
}
|
|
47
83
|
export class OrbitDBStoreUnreachableError extends Error {
|
|
48
84
|
address;
|
|
49
85
|
cause;
|
|
@@ -65,6 +101,11 @@ export async function contentToCid(obj) {
|
|
|
65
101
|
const hash = await sha256.digest(bytes);
|
|
66
102
|
return CID.createV1(0x71, hash).toString();
|
|
67
103
|
}
|
|
104
|
+
/** 2026-10-02: 原始字节 → CID (raw 0x55 + sha256, 与 `contentToCid` 同一套内容寻址口径) */
|
|
105
|
+
export async function bytesToCid(bytes) {
|
|
106
|
+
const hash = await sha256.digest(bytes);
|
|
107
|
+
return CID.createV1(0x55, hash).toString();
|
|
108
|
+
}
|
|
68
109
|
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
69
110
|
/**
|
|
70
111
|
* OrbitDB 身份在 keystore 里的槽名 (2026-09-24)。
|
|
@@ -95,7 +136,13 @@ export class OrbitDBAdapter {
|
|
|
95
136
|
db = null;
|
|
96
137
|
_orbitdb = null;
|
|
97
138
|
orbitdb;
|
|
98
|
-
|
|
139
|
+
/**
|
|
140
|
+
* 2026-10-02: **必须认 `BOLLOON_HOME`** —— 原来只走 `home()` (≈ `$HOME`),于是
|
|
141
|
+
* `BOLLOON_HOME=/tmp/x` 的"隔离运行"其实还在开**真实** `~/.bolloon/orbitdb/…`
|
|
142
|
+
* (实测: 隔离 HOME 下报的锁路径是 `/Users/apple/.bolloon/…`) ⇒ 隔离失效、测试会写真实库。
|
|
143
|
+
* 与仓里其它模块同一口径: `${BOLLOON_HOME:-~/.bolloon}`。
|
|
144
|
+
*/
|
|
145
|
+
constructor(dataDir = path.join(process.env.BOLLOON_HOME || path.join(home(), '.bolloon'), 'orbitdb')) {
|
|
99
146
|
this.dataDir = dataDir;
|
|
100
147
|
}
|
|
101
148
|
/** 落盘位置 (未初始化时 null; 诊断/验收用) */
|
|
@@ -161,24 +208,32 @@ export class OrbitDBAdapter {
|
|
|
161
208
|
async ensure() {
|
|
162
209
|
if (this.db)
|
|
163
210
|
return;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
// 2026-10-02: 默认 20s/步 —— 可调; 目的是"卡住"必须变成**可判定的失败**, 不许无声挂死
|
|
212
|
+
const stageMs = Number(process.env.BOLLOON_ORBITDB_STAGE_TIMEOUT_MS || 20_000);
|
|
213
|
+
try {
|
|
214
|
+
this.node = await withStageTimeout('启动 IPFS/helia 节点', stageMs, createBolloonIpfs(path.join(this.dataDir, 'ipfs')));
|
|
215
|
+
// 2026-09-24: 身份必须**跨进程稳定**。不给 id 时 @orbitdb/core 用 createId()
|
|
216
|
+
// (32 位随机串) 当 keystore 槽名 (src/orbitdb.js:43 `id = id || await createId()`
|
|
217
|
+
// → :61 `createIdentity({ id })`) → 每个进程一对新密钥 → 新身份 → 写自己的 store
|
|
218
|
+
// 会被访问控制拒掉 ("Key … is not allowed to write to the log")。
|
|
219
|
+
// 给固定槽名后, 密钥从 <dataDir>/stores/keystore 读出复用 → 同 dataDir 同身份。
|
|
220
|
+
// 类型注: `id` 是 @orbitdb/core 的真选项 (src/orbitdb.js:33), 但 TS7 从 JS 推断出的
|
|
221
|
+
// 参数类型漏了它 → 显式取 Parameters<typeof createOrbitDB>[0] 再补 { id }
|
|
222
|
+
// (不裸 any 掉整个入参)。
|
|
223
|
+
const init = {
|
|
224
|
+
ipfs: this.node.helia,
|
|
225
|
+
directory: path.join(this.dataDir, 'stores'),
|
|
226
|
+
id: ORBITDB_IDENTITY_ID,
|
|
227
|
+
};
|
|
228
|
+
this._orbitdb = await withStageTimeout('创建 OrbitDB 实例 (含 keystore)', stageMs, createOrbitDB(init));
|
|
229
|
+
this.db = await withStageTimeout('打开 CID 库', stageMs, this._orbitdb.open('bolloon-cid-store', { type: 'keyvalue' }));
|
|
230
|
+
// 共享底层实例 (只读暴露)
|
|
231
|
+
this.orbitdb = this._orbitdb;
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
// 打不开 ⇒ 一句**可行动**的话 (锁被谁占着 / 卡在哪一步), 不再让 "Database failed to open" 误导人
|
|
235
|
+
throw new Error(describeOrbitDbFailure(e), { cause: e });
|
|
236
|
+
}
|
|
182
237
|
}
|
|
183
238
|
async save(data) {
|
|
184
239
|
await this.ensure();
|
|
@@ -263,6 +318,25 @@ export class OrbitDBAdapter {
|
|
|
263
318
|
})
|
|
264
319
|
.sort((a, b) => a.timestamp - b.timestamp);
|
|
265
320
|
}
|
|
321
|
+
async putBytes(bytes) {
|
|
322
|
+
await this.ensure();
|
|
323
|
+
const cid = await bytesToCid(bytes);
|
|
324
|
+
await this.node.helia.blockstore.put(CID.parse(cid), bytes);
|
|
325
|
+
return { cid, bytes: bytes.length };
|
|
326
|
+
}
|
|
327
|
+
async getBytes(cid) {
|
|
328
|
+
await this.ensure();
|
|
329
|
+
try {
|
|
330
|
+
const stream = this.node.helia.blockstore.get(CID.parse(cid));
|
|
331
|
+
let out = new Uint8Array(0);
|
|
332
|
+
for await (const chunk of stream)
|
|
333
|
+
out = uint8Concat([out, chunk]);
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
266
340
|
async share(cid) {
|
|
267
341
|
await this.ensure();
|
|
268
342
|
const rec = await this.load(cid);
|
|
@@ -283,6 +357,8 @@ export class OrbitDBAdapter {
|
|
|
283
357
|
// events store 的 all() 返回 { hash, payload } — 统一成 { key, value }
|
|
284
358
|
return entries.map(e => ({
|
|
285
359
|
key: String(e.key ?? e.hash ?? ''),
|
|
360
|
+
// 2026-10-02: 条目自带的 hash/id **透出来** —— 回复某条 / 建分支要的锚点 (拿不到才留空, 不假造)
|
|
361
|
+
id: String((e.hash ?? e.id ?? e.payload?.key ?? '') || ''),
|
|
286
362
|
value: e.payload !== undefined ? e.payload : e.value,
|
|
287
363
|
}));
|
|
288
364
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "bolloon-mobile-web/1",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"channel": "stable",
|
|
5
5
|
"build": "stable",
|
|
6
|
-
"commit": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
6
|
+
"commit": "eb3ebe6fed23cb70e901ac8da92e6420bc0673ad",
|
|
7
|
+
"builtAt": "2026-10-05T06:24:49.679Z",
|
|
8
8
|
"layer": "web",
|
|
9
9
|
"note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
|
|
10
10
|
}
|
package/dist/web/sw.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 现在: 代码类资源 (html/js/css/json) 一律 **network-first** (拿不到网才回退缓存),
|
|
7
7
|
* 缓存名带版本号, activate 时清掉所有旧缓存; 图标等静态资源仍 cache-first。
|
|
8
8
|
*/
|
|
9
|
-
const CACHE = 'bolloon-mobile-v0.6.
|
|
9
|
+
const CACHE = 'bolloon-mobile-v0.6.3';
|
|
10
10
|
const SHELL = [
|
|
11
11
|
'./mobile.html',
|
|
12
12
|
'./mobile.css',
|