@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.
@@ -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.command !== 'string')
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
- await new Promise((r) => setTimeout(r, 400));
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 —
@@ -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
- const hasSession = existsSync(`${sessionsDir}/${cid}:default.json`) || existsSync(`${sessionsDir}/${cid}.json`);
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
- async function loadAccounts() {
2851
- try {
2852
- const { readFile } = await import('fs/promises');
2853
- const parsed = JSON.parse(await readFile(ACCOUNTS_FILE, 'utf-8'));
2854
- return Array.isArray(parsed) ? parsed : [];
2855
- }
2856
- catch {
2857
- return [];
2858
- }
2859
- }
2860
- async function saveAccounts(accs) {
2861
- const { mkdir, writeFile } = await import('fs/promises');
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 accs = await loadAccounts();
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: accs.map((a) => ({
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 existing = accs.find((a) => a.provider === prov && (!idStr || a.identifier === idStr || a.email === idStr));
2905
- const now = new Date().toISOString();
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 accs = await loadAccounts();
2946
- const before = accs.length;
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",