@bolloon/bolloon-agent 0.4.4 → 0.4.6
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/pi-sdk-tools.js +62 -45
- package/dist/agents/pi-sdk.js +13 -0
- package/dist/agents/skill-writer.js +15 -3
- package/dist/agents/workflow-pivot-loop.js +11 -1
- package/dist/bootstrap/memory-compressor.js +1 -1
- package/dist/cli/ink-app.js +18 -0
- package/dist/index.js +110 -26
- package/dist/orbitdb/agent-tools.js +139 -0
- package/dist/orbitdb/kanban-store.js +259 -0
- package/dist/orbitdb/task-store.js +112 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +1 -1
- package/dist/pi-ecosystem-mcp/index.js +146 -5
- package/dist/security/tool-gate.js +2 -0
- package/dist/web/edge-auth-client.js +120 -0
- package/dist/web/server-storage.js +29 -0
- package/dist/web/server.js +38 -55
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ import * as path from 'path';
|
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import * as readline from 'readline';
|
|
17
|
+
/** streamable HTTP 传输的浏览器 UA — Cloudflare MCP 等端点有 1010 风控, node fetch 默认 UA 会被拒 */
|
|
18
|
+
const MCP_BROWSER_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
|
|
17
19
|
// MCP adapter state
|
|
18
20
|
let tools = new Map();
|
|
19
21
|
let servers = new Map();
|
|
@@ -45,7 +47,23 @@ export async function discoverMcpServers() {
|
|
|
45
47
|
if (serversConfig && typeof serversConfig === 'object') {
|
|
46
48
|
for (const [name, config] of Object.entries(serversConfig)) {
|
|
47
49
|
const serverConfig = config;
|
|
48
|
-
if (!serverConfig || typeof serverConfig
|
|
50
|
+
if (!serverConfig || typeof serverConfig !== 'object')
|
|
51
|
+
continue;
|
|
52
|
+
// 2026-08-12: 支持 HTTP transport (type: "http" + url + headers), 如 Cloudflare MCP
|
|
53
|
+
const isHttp = serverConfig.type === 'http' || (typeof serverConfig.url === 'string' && typeof serverConfig.command !== 'string');
|
|
54
|
+
if (isHttp) {
|
|
55
|
+
if (typeof serverConfig.url !== 'string' || !serverConfig.url)
|
|
56
|
+
continue;
|
|
57
|
+
configs.push({
|
|
58
|
+
name,
|
|
59
|
+
type: 'http',
|
|
60
|
+
url: serverConfig.url,
|
|
61
|
+
command: '',
|
|
62
|
+
headers: serverConfig.headers,
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (typeof serverConfig.command !== 'string')
|
|
49
67
|
continue;
|
|
50
68
|
configs.push({
|
|
51
69
|
name,
|
|
@@ -60,10 +78,10 @@ export async function discoverMcpServers() {
|
|
|
60
78
|
// File doesn't exist, skip
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
|
-
// 去重 (同 name 同 command)
|
|
81
|
+
// 去重 (同 name 同 command/url)
|
|
64
82
|
const seen = new Set();
|
|
65
83
|
return configs.filter((c) => {
|
|
66
|
-
const key = `${c.name}::${c.command}`;
|
|
84
|
+
const key = `${c.name}::${c.type === 'http' ? c.url : c.command}`;
|
|
67
85
|
if (seen.has(key))
|
|
68
86
|
return false;
|
|
69
87
|
seen.add(key);
|
|
@@ -100,8 +118,10 @@ export async function connectAndDiscover(serverName) {
|
|
|
100
118
|
const started = await startServer(serverName);
|
|
101
119
|
if (!started)
|
|
102
120
|
return [];
|
|
103
|
-
// 等 server
|
|
104
|
-
|
|
121
|
+
// http 传输无进程, 无需等待就绪; stdio 等 server 启动
|
|
122
|
+
if (server.config.type !== 'http') {
|
|
123
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
124
|
+
}
|
|
105
125
|
}
|
|
106
126
|
await sendMcpRequest(serverName, 'initialize', {
|
|
107
127
|
protocolVersion: '2024-11-05',
|
|
@@ -230,6 +250,10 @@ async function sendMcpRequest(serverName, method, params) {
|
|
|
230
250
|
if (!server) {
|
|
231
251
|
throw new Error(`Server not registered: ${serverName}`);
|
|
232
252
|
}
|
|
253
|
+
// 2026-08-12: http 传输走 fetch JSON-RPC (streamable HTTP)
|
|
254
|
+
if (server.config.type === 'http') {
|
|
255
|
+
return sendHttpMcpRequest(serverName, method, params);
|
|
256
|
+
}
|
|
233
257
|
// 确保 server 进程在跑
|
|
234
258
|
if (!server.running || !server.process || !server.process.stdin?.writable) {
|
|
235
259
|
const started = await startServer(serverName);
|
|
@@ -260,6 +284,118 @@ async function sendMcpRequest(serverName, method, params) {
|
|
|
260
284
|
child.stdin.write(line + '\n');
|
|
261
285
|
});
|
|
262
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Send MCP request to a remote server via streamable HTTP (2026-08-12).
|
|
289
|
+
* - POST JSON-RPC 到 url, 携带 Authorization 等配置 headers
|
|
290
|
+
* - 默认带浏览器 UA (Cloudflare MCP 1010 风控, node fetch 默认 UA 被拒)
|
|
291
|
+
* - 响应支持 application/json 与 text/event-stream (SSE) 两种格式
|
|
292
|
+
* - 服务器返回 Mcp-Session-Id 时记录, 后续请求自动携带
|
|
293
|
+
* - 通知 (notifications/*) fire-and-forget: 不等待响应体 (Cloudflare 返回 202)
|
|
294
|
+
*/
|
|
295
|
+
async function sendHttpMcpRequest(serverName, method, params) {
|
|
296
|
+
const server = servers.get(serverName);
|
|
297
|
+
if (!server)
|
|
298
|
+
throw new Error(`Server not registered: ${serverName}`);
|
|
299
|
+
if (!server.config.url)
|
|
300
|
+
throw new Error(`MCP http server 缺 url: ${serverName}`);
|
|
301
|
+
const id = mcpRequestSeq++;
|
|
302
|
+
const isNotification = method.startsWith('notifications/');
|
|
303
|
+
const payload = isNotification
|
|
304
|
+
? { jsonrpc: '2.0', method, params: params ?? {} }
|
|
305
|
+
: { jsonrpc: '2.0', id, method, params: params ?? {} };
|
|
306
|
+
const headers = {
|
|
307
|
+
'Content-Type': 'application/json',
|
|
308
|
+
Accept: 'application/json, text/event-stream',
|
|
309
|
+
...(server.config.headers ?? {}),
|
|
310
|
+
};
|
|
311
|
+
// 1010 风控: 默认浏览器 UA, 用户 headers 可覆盖
|
|
312
|
+
if (!headers['User-Agent'])
|
|
313
|
+
headers['User-Agent'] = MCP_BROWSER_UA;
|
|
314
|
+
if (server.sessionId)
|
|
315
|
+
headers['Mcp-Session-Id'] = server.sessionId;
|
|
316
|
+
const ctrl = new AbortController();
|
|
317
|
+
const timer = setTimeout(() => ctrl.abort(), MCP_REQUEST_TIMEOUT_MS);
|
|
318
|
+
try {
|
|
319
|
+
const res = await fetch(server.config.url, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers,
|
|
322
|
+
body: JSON.stringify(payload),
|
|
323
|
+
signal: ctrl.signal,
|
|
324
|
+
});
|
|
325
|
+
if (isNotification) {
|
|
326
|
+
// fire-and-forget: 不等 body (Cloudflare 返回 202 空体)
|
|
327
|
+
res.body?.cancel().catch(() => { });
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
const sessionId = res.headers.get('Mcp-Session-Id');
|
|
331
|
+
if (sessionId)
|
|
332
|
+
server.sessionId = sessionId;
|
|
333
|
+
const msg = await readHttpBodyUntilResponse(res);
|
|
334
|
+
if (msg.error) {
|
|
335
|
+
throw new Error(`MCP error ${msg.error.code}: ${msg.error.message}`);
|
|
336
|
+
}
|
|
337
|
+
return msg.result;
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 读取 HTTP 响应直到拿到完整 JSON-RPC 响应 (2026-08-12 修复).
|
|
345
|
+
* 坑: streamable HTTP 服务器 (如 mcp.cloudflare.com/mcp) 对 tools/call 返回 SSE 后
|
|
346
|
+
* **不关闭连接** — res.text() 会永远等 EOF 挂起 (initialize/tools/list 响应收尾, 掩盖了问题).
|
|
347
|
+
* 方案: 流式读 body, 按 SSE 事件块 (空行分隔) 解析, 找到 id 匹配的响应就 cancel 流返回.
|
|
348
|
+
* 非 SSE (application/json) 响应直接 text().
|
|
349
|
+
*/
|
|
350
|
+
async function readHttpBodyUntilResponse(res) {
|
|
351
|
+
const ct = res.headers.get('content-type') || '';
|
|
352
|
+
if (!ct.includes('text/event-stream')) {
|
|
353
|
+
const raw = await res.text();
|
|
354
|
+
return JSON.parse(raw);
|
|
355
|
+
}
|
|
356
|
+
const reader = res.body.getReader();
|
|
357
|
+
const decoder = new TextDecoder();
|
|
358
|
+
let buf = '';
|
|
359
|
+
try {
|
|
360
|
+
while (true) {
|
|
361
|
+
const { done, value } = await reader.read();
|
|
362
|
+
if (done)
|
|
363
|
+
break;
|
|
364
|
+
buf += decoder.decode(value, { stream: true });
|
|
365
|
+
const msg = extractSseResponse(buf);
|
|
366
|
+
if (msg)
|
|
367
|
+
return msg;
|
|
368
|
+
}
|
|
369
|
+
const msg = extractSseResponse(buf);
|
|
370
|
+
if (msg)
|
|
371
|
+
return msg;
|
|
372
|
+
throw new Error(`SSE 响应无有效 JSON-RPC data 块: ${buf.slice(0, 200)}`);
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
// 拿到响应后立即关闭流 — 不等服务器断开连接
|
|
376
|
+
reader.cancel().catch(() => { });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** 从累积的 SSE 文本提取完整 JSON-RPC 响应 (按空行分隔事件块, data: 行拼 JSON) */
|
|
380
|
+
function extractSseResponse(buf) {
|
|
381
|
+
const blocks = buf.split(/\n\n/);
|
|
382
|
+
for (const block of blocks) {
|
|
383
|
+
let data = '';
|
|
384
|
+
for (const line of block.split(/\r?\n/)) {
|
|
385
|
+
if (line.startsWith('data:'))
|
|
386
|
+
data += line.slice(5).trim();
|
|
387
|
+
}
|
|
388
|
+
if (!data)
|
|
389
|
+
continue;
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse(data);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// 块不完整 (截断) — 等更多数据
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
263
399
|
/** 挂上 stdout 行读取器 (按 id 分发响应) */
|
|
264
400
|
function attachStdoutReader(serverName, child) {
|
|
265
401
|
const server = servers.get(serverName);
|
|
@@ -306,6 +442,11 @@ export async function startServer(serverName) {
|
|
|
306
442
|
if (server.running && server.process) {
|
|
307
443
|
return true;
|
|
308
444
|
}
|
|
445
|
+
// 2026-08-12: http 传输无子进程 — 虚拟 running, 请求走 sendHttpMcpRequest
|
|
446
|
+
if (server.config.type === 'http') {
|
|
447
|
+
server.running = true;
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
309
450
|
try {
|
|
310
451
|
const child = spawn(server.config.command, server.config.args || [], {
|
|
311
452
|
env: { ...process.env, ...server.config.env },
|
|
@@ -64,6 +64,8 @@ const TOOL_WHITELIST = new Set([
|
|
|
64
64
|
'cid_save', 'cid_load', 'cid_update', 'cid_version', 'cid_list', 'cid_share',
|
|
65
65
|
'context_save_snapshot', 'context_restore',
|
|
66
66
|
'ui_save_component', 'ui_load_component',
|
|
67
|
+
// 2026-08-12: Kanban 看板工具 (Hermes kanban_db → OrbitDB, src/orbitdb/kanban-store.ts)
|
|
68
|
+
'kanban_create', 'kanban_list', 'kanban_get', 'kanban_claim', 'kanban_complete', 'kanban_status',
|
|
67
69
|
// 2026-08-07: Bolloon 自身配置读写 (bolloon-config.json, agent 有修改自身配置权限)
|
|
68
70
|
'bolloon_config_get', 'bolloon_config_set',
|
|
69
71
|
]);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// 边缘认证客户端 — 把 WebUI 登录配置托管到 Cloudflare 边缘 (Workers + KV)
|
|
2
|
+
// 2026-08-12: 优先请求边缘 Worker, worker 不可达时降级到本地 accounts.json fallback.
|
|
3
|
+
// Worker 实现见 src/web/workers/auth/.
|
|
4
|
+
/**
|
|
5
|
+
* 边缘认证客户端.
|
|
6
|
+
* 每个 auth 操作先打边缘 Worker; 失败/超时自动降级到本地 accounts.json.
|
|
7
|
+
*/
|
|
8
|
+
export class EdgeAuthClient {
|
|
9
|
+
baseUrl;
|
|
10
|
+
timeoutMs;
|
|
11
|
+
fallbackFile;
|
|
12
|
+
log;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
15
|
+
this.timeoutMs = opts.timeoutMs ?? 4000;
|
|
16
|
+
this.fallbackFile = opts.fallbackFile;
|
|
17
|
+
this.log = opts.log ?? (() => { });
|
|
18
|
+
}
|
|
19
|
+
// ---- 私有: 边缘 HTTP / 本地文件 ----
|
|
20
|
+
async edgeFetch(path, opts) {
|
|
21
|
+
const ctrl = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${this.baseUrl}/api/auth${path}`, {
|
|
25
|
+
method: opts?.method ?? 'GET',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
28
|
+
signal: ctrl.signal,
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.text().catch(() => '');
|
|
32
|
+
throw new Error(`边缘认证 ${path} HTTP ${res.status} ${err}`);
|
|
33
|
+
}
|
|
34
|
+
return await res.json();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async loadLocal() {
|
|
41
|
+
try {
|
|
42
|
+
const { readFile } = await import('fs/promises');
|
|
43
|
+
const parsed = JSON.parse(await readFile(this.fallbackFile, 'utf-8'));
|
|
44
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async saveLocal(accs) {
|
|
51
|
+
const { mkdir, writeFile } = await import('fs/promises');
|
|
52
|
+
const dir = this.fallbackFile.replace(/[\\/][^\\/]*$/, '');
|
|
53
|
+
await mkdir(dir, { recursive: true });
|
|
54
|
+
await writeFile(this.fallbackFile, JSON.stringify(accs, null, 2), { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
// ---- 公共 API (与 server.ts 原 loadAccounts/saveAccounts 语义一致) ----
|
|
57
|
+
/** 返回脱敏视图 (provider/identifier/loggedAt/skeleton), 供 /api/auth/status 直接用 */
|
|
58
|
+
async loadAccounts() {
|
|
59
|
+
try {
|
|
60
|
+
const data = await this.edgeFetch('/status');
|
|
61
|
+
return Array.isArray(data?.accounts) ? data.accounts : [];
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
this.log(`[edge-auth] 边缘不可达, 降级本地: ${err?.message ?? err}`);
|
|
65
|
+
return this.loadLocal().then((accs) => accs.map((a) => ({
|
|
66
|
+
provider: a.provider,
|
|
67
|
+
identifier: a.identifier || a.email || a.username || '',
|
|
68
|
+
loggedAt: a.loggedAt,
|
|
69
|
+
skeleton: !!a.skeleton,
|
|
70
|
+
})));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async login(payload) {
|
|
74
|
+
try {
|
|
75
|
+
return await this.edgeFetch('/login', { method: 'POST', body: payload });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.log(`[edge-auth] 登录降级本地: ${err?.message ?? err}`);
|
|
79
|
+
const accs = await this.loadLocal();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const exists = accs.find((a) => a.provider === payload.provider &&
|
|
82
|
+
(!payload.identifier || a.identifier === payload.identifier || a.email === payload.identifier));
|
|
83
|
+
if (exists) {
|
|
84
|
+
exists.ownerDid = payload.ownerDid;
|
|
85
|
+
exists.loggedAt = now;
|
|
86
|
+
exists.skeleton = true;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const id = payload.identifier || '';
|
|
90
|
+
accs.push({
|
|
91
|
+
provider: payload.provider,
|
|
92
|
+
identifier: id,
|
|
93
|
+
email: payload.provider === 'email' ? id : '',
|
|
94
|
+
phone: payload.provider === 'phone' ? id : '',
|
|
95
|
+
username: id || '',
|
|
96
|
+
token: '',
|
|
97
|
+
ownerDid: payload.ownerDid,
|
|
98
|
+
loggedAt: now,
|
|
99
|
+
skeleton: true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
await this.saveLocal(accs);
|
|
103
|
+
return { ok: true, provider: payload.provider, identifier: payload.identifier, ownerDid: payload.ownerDid, skeleton: true, degraded: true };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async logout(payload) {
|
|
107
|
+
try {
|
|
108
|
+
return await this.edgeFetch('/logout', { method: 'POST', body: payload });
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
this.log(`[edge-auth] 登出降级本地: ${err?.message ?? err}`);
|
|
112
|
+
const before = await this.loadLocal();
|
|
113
|
+
const remaining = before.filter((a) => a.provider !== payload.provider);
|
|
114
|
+
if (remaining.length === before.length)
|
|
115
|
+
throw new Error(`未绑定 ${payload.provider} 账号`);
|
|
116
|
+
await this.saveLocal(remaining);
|
|
117
|
+
return { ok: true, degraded: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -168,7 +168,27 @@ export function isTaskExecuting() {
|
|
|
168
168
|
export function getExecutingTaskId() {
|
|
169
169
|
return executionTaskId;
|
|
170
170
|
}
|
|
171
|
+
// ==================== 任务队列 ====================
|
|
172
|
+
// 2026-08-12 (Task6): 任务队列以 OrbitDB 为主存储 (去中心化/跨设备同步), 本地文件作 fallback.
|
|
173
|
+
// orbit 由 server 启动时 warmTaskOrbitStore() 预热成功后调 markTaskOrbitReady() 置位;
|
|
174
|
+
// 未置位 (测试/离线) 自动回退本地文件.
|
|
175
|
+
let _taskOrbitReady = false;
|
|
176
|
+
/** server 启动 warm 成功后调用, 启用 orbitdb 主存储 (测试可调用 disableTaskOrbitStore 关闭) */
|
|
177
|
+
export function markTaskOrbitReady() { _taskOrbitReady = true; }
|
|
178
|
+
/** 禁用 orbitdb 任务存储 (测试隔离用) */
|
|
179
|
+
export function disableTaskOrbitStore() { _taskOrbitReady = false; }
|
|
180
|
+
function taskOrbitReady() { return _taskOrbitReady; }
|
|
171
181
|
export async function loadTaskQueue() {
|
|
182
|
+
// OrbitDB 主存储优先
|
|
183
|
+
if (taskOrbitReady()) {
|
|
184
|
+
try {
|
|
185
|
+
const { getTaskOrbitStore } = await import('../orbitdb/task-store.js');
|
|
186
|
+
const tasks = await getTaskOrbitStore().loadTasks();
|
|
187
|
+
if (Array.isArray(tasks))
|
|
188
|
+
return tasks;
|
|
189
|
+
}
|
|
190
|
+
catch { /* orbit 读失败 → fallback 本地 */ }
|
|
191
|
+
}
|
|
172
192
|
try {
|
|
173
193
|
const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
|
|
174
194
|
return JSON.parse(data);
|
|
@@ -178,7 +198,16 @@ export async function loadTaskQueue() {
|
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
export async function saveTaskQueue(tasks) {
|
|
201
|
+
// 本地双写 (备份 + fallback 源)
|
|
181
202
|
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
203
|
+
// OrbitDB 主存储同步 (尽力而为)
|
|
204
|
+
if (taskOrbitReady()) {
|
|
205
|
+
try {
|
|
206
|
+
const { getTaskOrbitStore } = await import('../orbitdb/task-store.js');
|
|
207
|
+
await getTaskOrbitStore().saveTasks(tasks);
|
|
208
|
+
}
|
|
209
|
+
catch { /* orbit 写失败静默, 本地已备份 */ }
|
|
210
|
+
}
|
|
182
211
|
}
|
|
183
212
|
// ==================== 任务认领 CAS (2026-08-11, Hermes kanban 模式) ====================
|
|
184
213
|
// kanban_db.py: "WAL + BEGIN IMMEDIATE + compare-and-swap on tasks.status/claim_lock —
|
package/dist/web/server.js
CHANGED
|
@@ -1698,6 +1698,18 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1698
1698
|
const rep = await startDidCatalogReplication(identity.did, { intervalMs: 30_000 });
|
|
1699
1699
|
console.log(`[did-catalog] OrbitDB 自动复制已启动: store=${rep.storeName} address=${String(rep.storeAddress).slice(0, 48)}...`);
|
|
1700
1700
|
globalThis.__didCatalogReplication = rep;
|
|
1701
|
+
// 2026-08-12 (Task6): 预热任务 OrbitDB 存储 (task 队列去中心化主存储). warm 成功才启用.
|
|
1702
|
+
try {
|
|
1703
|
+
const { warmTaskOrbitStore, taskStoreName } = await import('../orbitdb/task-store.js');
|
|
1704
|
+
const { markTaskOrbitReady } = await import('./server-storage.js');
|
|
1705
|
+
const ok = await warmTaskOrbitStore(identity.did);
|
|
1706
|
+
if (ok)
|
|
1707
|
+
markTaskOrbitReady();
|
|
1708
|
+
console.log(`[task-store] OrbitDB 任务存储 ${ok ? '已启用' : '未启用 (回退本地)'} (store=${taskStoreName(identity.did)})`);
|
|
1709
|
+
}
|
|
1710
|
+
catch (e) {
|
|
1711
|
+
console.warn('[task-store] 任务 OrbitDB 预热失败 (非致命):', e?.message?.slice(0, 120));
|
|
1712
|
+
}
|
|
1701
1713
|
}
|
|
1702
1714
|
catch (e) {
|
|
1703
1715
|
console.warn('[did-catalog] OrbitDB 复制启动失败 (非致命, 稍后可用 API 重试):', e?.message?.slice(0, 120));
|
|
@@ -1733,7 +1745,11 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1733
1745
|
if (!cid || knownIds.has(cid))
|
|
1734
1746
|
continue;
|
|
1735
1747
|
// 有 session 文件才算可恢复 (说明确实创建过)
|
|
1736
|
-
|
|
1748
|
+
// 2026-08-12 fix: SessionStore 保存到 cache/<channelId>__default.json (冒号被 filenameEscape 成 __),
|
|
1749
|
+
// 之前用 `${sessionsDir}/${cid}:default.json` (未 escape + 硬拼冒号) 查不到 → 会话已存在也被判为
|
|
1750
|
+
// "无 session" 永不恢复, 即 session/channel 路径与创建智能体 channel 的路径不一致 (bug 修复)
|
|
1751
|
+
const { getSessionCacheFile } = await import('../bootstrap/memory-compressor.js');
|
|
1752
|
+
const hasSession = existsSync(getSessionCacheFile(cid, 'default')) || existsSync(`${sessionsDir}/${cid}.json`);
|
|
1737
1753
|
if (!hasSession)
|
|
1738
1754
|
continue;
|
|
1739
1755
|
const restored = {
|
|
@@ -2845,39 +2861,32 @@ ${goalDesc}
|
|
|
2845
2861
|
});
|
|
2846
2862
|
// ========== 登录框架 (2026-08-09) — GitHub/Google/邮箱/手机号, 仅骨架 ==========
|
|
2847
2863
|
// 所有登录方式最终都归属到用户 DID (右下角唯一身份).
|
|
2864
|
+
// 2026-08-12: 登录配置托管到 Cloudflare 边缘 (Workers+KV), 本地 accounts.json 仅作降级 fallback.
|
|
2848
2865
|
// 真实 OAuth / 验证码后续接入, 这里先做: 记录账号 + 绑定用户 DID + 提供状态查询.
|
|
2849
2866
|
const ACCOUNTS_FILE = `${process.env.HOME || '/tmp'}/.bolloon/accounts.json`;
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
await mkdir(`${process.env.HOME || '/tmp'}/.bolloon`, { recursive: true });
|
|
2863
|
-
await writeFile(ACCOUNTS_FILE, JSON.stringify(accs, null, 2), { mode: 0o600 });
|
|
2864
|
-
}
|
|
2867
|
+
const EDGE_AUTH_URL = process.env.BOLLOON_EDGE_AUTH_URL || 'http://127.0.0.1:8788';
|
|
2868
|
+
const { EdgeAuthClient } = await import('./edge-auth-client.js');
|
|
2869
|
+
const edgeAuth = new EdgeAuthClient({
|
|
2870
|
+
baseUrl: EDGE_AUTH_URL,
|
|
2871
|
+
timeoutMs: 4000,
|
|
2872
|
+
fallbackFile: ACCOUNTS_FILE,
|
|
2873
|
+
log: (m) => { try {
|
|
2874
|
+
console.log(m);
|
|
2875
|
+
}
|
|
2876
|
+
catch { /* noop */ } },
|
|
2877
|
+
});
|
|
2878
|
+
async function loadAccounts() { return edgeAuth.loadAccounts(); }
|
|
2865
2879
|
// GET /api/auth/status — 当前用户 DID + 已绑定账号列表
|
|
2866
2880
|
app.get('/api/auth/status', async (_req, res) => {
|
|
2867
2881
|
try {
|
|
2868
2882
|
const identity = await loadOrCreateUserIdentity();
|
|
2869
|
-
const
|
|
2883
|
+
const accounts = await loadAccounts();
|
|
2870
2884
|
res.json({
|
|
2871
2885
|
did: identity.did,
|
|
2872
2886
|
didShort: identity.didShort,
|
|
2873
2887
|
name: identity.name,
|
|
2874
|
-
// 只返回脱敏视图 (不含 token)
|
|
2875
|
-
accounts
|
|
2876
|
-
provider: a.provider,
|
|
2877
|
-
identifier: a.identifier || a.email || a.username || '',
|
|
2878
|
-
loggedAt: a.loggedAt,
|
|
2879
|
-
skeleton: !!a.skeleton,
|
|
2880
|
-
})),
|
|
2888
|
+
// 只返回脱敏视图 (不含 token), 来源: 边缘 KV (worker 不可达时本地 fallback)
|
|
2889
|
+
accounts,
|
|
2881
2890
|
});
|
|
2882
2891
|
}
|
|
2883
2892
|
catch (err) {
|
|
@@ -2899,31 +2908,9 @@ ${goalDesc}
|
|
|
2899
2908
|
return res.status(400).json({ error: `${prov === 'email' ? '邮箱' : '手机号'}必填` });
|
|
2900
2909
|
}
|
|
2901
2910
|
const identity = await loadOrCreateUserIdentity();
|
|
2902
|
-
const accs = await loadAccounts();
|
|
2903
2911
|
const idStr = String(identifier || '').trim();
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2906
|
-
if (existing) {
|
|
2907
|
-
// 已绑定 → 更新归属 DID + 时间
|
|
2908
|
-
existing.ownerDid = identity.did;
|
|
2909
|
-
existing.loggedAt = now;
|
|
2910
|
-
existing.skeleton = true;
|
|
2911
|
-
}
|
|
2912
|
-
else {
|
|
2913
|
-
accs.push({
|
|
2914
|
-
provider: prov,
|
|
2915
|
-
identifier: idStr || '',
|
|
2916
|
-
email: prov === 'email' ? idStr : '',
|
|
2917
|
-
phone: prov === 'phone' ? idStr : '',
|
|
2918
|
-
username: idStr || '',
|
|
2919
|
-
token: '', // 真实 OAuth 后填
|
|
2920
|
-
ownerDid: identity.did, // 归属用户唯一 DID
|
|
2921
|
-
loggedAt: now,
|
|
2922
|
-
skeleton: true, // 骨架标记: 未做真实 OAuth/验证码
|
|
2923
|
-
});
|
|
2924
|
-
}
|
|
2925
|
-
await saveAccounts(accs);
|
|
2926
|
-
console.log(`[auth] 登录骨架: ${prov}${idStr ? ' ' + idStr : ''} → 归属 DID ${identity.did.substring(0, 20)}...`);
|
|
2912
|
+
const result = await edgeAuth.login({ provider: prov, identifier: idStr, ownerDid: identity.did });
|
|
2913
|
+
console.log(`[auth] 登录骨架: ${prov}${idStr ? ' ' + idStr : ''} → 归属 DID ${identity.did.substring(0, 20)}...${result.degraded ? ' (本地 fallback)' : ' (边缘)'}`);
|
|
2927
2914
|
res.json({
|
|
2928
2915
|
ok: true,
|
|
2929
2916
|
provider: prov,
|
|
@@ -2931,6 +2918,7 @@ ${goalDesc}
|
|
|
2931
2918
|
ownerDid: identity.did,
|
|
2932
2919
|
skeleton: true,
|
|
2933
2920
|
message: `${prov} 登录骨架已记录 (归属用户 DID), 真实 OAuth/验证码后续接入`,
|
|
2921
|
+
storage: result.degraded ? 'local' : 'edge',
|
|
2934
2922
|
});
|
|
2935
2923
|
}
|
|
2936
2924
|
catch (err) {
|
|
@@ -2942,13 +2930,8 @@ ${goalDesc}
|
|
|
2942
2930
|
try {
|
|
2943
2931
|
const { provider } = req.body || {};
|
|
2944
2932
|
const prov = String(provider || '').trim().toLowerCase();
|
|
2945
|
-
const
|
|
2946
|
-
|
|
2947
|
-
const remaining = accs.filter((a) => a.provider !== prov);
|
|
2948
|
-
if (remaining.length === before)
|
|
2949
|
-
return res.status(404).json({ error: `未绑定 ${prov} 账号` });
|
|
2950
|
-
await saveAccounts(remaining);
|
|
2951
|
-
res.json({ ok: true, provider: prov });
|
|
2933
|
+
const result = await edgeAuth.logout({ provider: prov });
|
|
2934
|
+
res.json({ ok: true, provider: prov, storage: result.degraded ? 'local' : 'edge' });
|
|
2952
2935
|
}
|
|
2953
2936
|
catch (err) {
|
|
2954
2937
|
res.status(500).json({ error: err.message });
|