@bolloon/bolloon-agent 0.3.45 → 0.3.46

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.
@@ -17,6 +17,19 @@ import * as path from 'path';
17
17
  import * as os from 'os';
18
18
  import { KeyManager } from '@diap/sdk';
19
19
  const AGENT_KEYS_DIR = path.join(os.homedir(), '.bolloon', 'agent-keys');
20
+ /** 读取用户唯一 DID (与 server loadOrCreateUserIdentity 同源, 归属字段用) */
21
+ export function getUserOwnerDid() {
22
+ try {
23
+ const f = path.join(os.homedir(), '.bolloon', 'identity', 'user.json');
24
+ if (fs.existsSync(f)) {
25
+ const j = JSON.parse(fs.readFileSync(f, 'utf-8'));
26
+ if (j.did)
27
+ return j.did;
28
+ }
29
+ }
30
+ catch { /* 无 user.json → 无归属 */ }
31
+ return '';
32
+ }
20
33
  /**
21
34
  * 加载或创建 agent 的持久化 diap 身份
22
35
  * - 文件存在 → 从私钥重建 keyPair
@@ -44,6 +57,7 @@ export function loadOrCreateAgentIdentity(agentId) {
44
57
  did: kp.did,
45
58
  createdAt: j.createdAt || 'unknown',
46
59
  reused: true,
60
+ ownerDid: j.ownerDid || getUserOwnerDid(),
47
61
  };
48
62
  }
49
63
  }
@@ -70,6 +84,8 @@ export function loadOrCreateAgentIdentity(agentId) {
70
84
  lastUsedAt: createdAt,
71
85
  agentId,
72
86
  version: '1.0',
87
+ // 2026-08-09: 归属用户唯一 DID — DIAP 智能体身份全部归到用户身份下
88
+ ownerDid: getUserOwnerDid(),
73
89
  };
74
90
  fs.writeFileSync(fp, JSON.stringify(data, null, 2), { mode: 0o600 });
75
91
  try {
@@ -83,6 +99,7 @@ export function loadOrCreateAgentIdentity(agentId) {
83
99
  did: kp.did,
84
100
  createdAt,
85
101
  reused: false,
102
+ ownerDid: data.ownerDid,
86
103
  };
87
104
  }
88
105
  /**
@@ -46,11 +46,27 @@ export function decideAfterReview(state, maxReviews = DEFAULT_MAX_REVIEWS) {
46
46
  export function buildReviewHint(state, maxReviews = DEFAULT_MAX_REVIEWS) {
47
47
  const intent = (state.userIntent || '').trim() || '(无)';
48
48
  const tools = state.completedTools.length ? state.completedTools.join(', ') : '(尚未执行工具)';
49
- return (`[目标对齐 review ${state.reviewsDone + 1}/${maxReviews}]\n` +
50
- `用户需求: ${intent.slice(0, 300)}\n` +
51
- `本轮已完成工具: ${tools}\n` +
52
- `请对照需求自查: 是否还有未完成/可深挖的子目标? 若有 继续调用工具推进; ` +
53
- `若已满足用户原始需求 直接输出 <final gen> 结束. 不要再重复已完成的动作.`);
49
+ // 2026-08-09: 行动日志 逐条列出已完成动作 + 结果摘要, 让 LLM 对照目标核查
50
+ // (旧版只有工具名, LLM 容易潦草自查; 带结果才能判断"这一步到底做完了没")
51
+ let actionLines = '';
52
+ if (state.actionLog && state.actionLog.length > 0) {
53
+ actionLines = '\n本轮已执行动作 (逐条):\n' + state.actionLog
54
+ .map((a, i) => {
55
+ const args = a.argsPreview ? `(${a.argsPreview.slice(0, 80)})` : '';
56
+ const res = a.success
57
+ ? `✓ ${(a.resultPreview || 'ok').slice(0, 120)}`
58
+ : `✗ ${(a.resultPreview || 'failed').slice(0, 120)}`;
59
+ return ` ${i + 1}. ${a.tool}${args} → ${res}`;
60
+ })
61
+ .join('\n');
62
+ }
63
+ return (`[目标对齐 review ${state.reviewsDone + 1}/${maxReviews}]` +
64
+ `\n用户需求: ${intent.slice(0, 300)}` +
65
+ actionLines +
66
+ `\n已完成工具: ${tools}` +
67
+ `\n请对照需求逐条自查: 上面每一条动作是否真正完成了对应的子目标? 还有未完成/可深挖的子目标 → 继续调用工具推进 (注意: 同一动作已完成就不要重复执行, 直接基于已有结果推进下一步); ` +
68
+ `若已满足用户原始需求 → 直接输出 <final gen> 结束.` +
69
+ `\n[重要] 如果你要结束, 请先逐条对照「用户需求」确认每一项都已完成, 不要因为做了一部分就潦草收尾.`);
54
70
  }
55
71
  /** 布尔门: 是否该继续 review (测试/消融快速断言) */
56
72
  export function shouldReviewAgain(state, maxReviews = DEFAULT_MAX_REVIEWS) {
@@ -1129,6 +1129,10 @@ ${this.getToolDefinitions()}
1129
1129
  // 上限=2 次 (用户要求"运行一两次"), 结束后按用户需求为准.
1130
1130
  let loopReviewCount = 0;
1131
1131
  const loopReviewCompletedTools = new Set();
1132
+ // 2026-08-09: 本轮行动日志 — 每轮工具执行都记录 (args + 结果摘要),
1133
+ // final 前 review 用逐条核查目标; 也注入 system prompt 让 LLM 看到连续进度
1134
+ // (防"每轮都像重启" — 之前 LLM 看不到自己已完成什么, 容易重复 react)
1135
+ const loopActionLog = [];
1132
1136
  // 发送循环开始的事件
1133
1137
  if (onStream) {
1134
1138
  onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
@@ -1252,11 +1256,26 @@ ${this.getToolDefinitions()}
1252
1256
  `;
1253
1257
  }
1254
1258
  const personaSection = this.cachedPersonaSection;
1259
+ // 2026-08-09: 循环进度段 — 让 LLM 看到本轮已完成的动作 (连续进度, 不重启)
1260
+ // Hermes 式 Agent Runtime: 循环是状态机, LLM 每次看到的是"第 N 步 + 已完成 X"
1261
+ // (之前每轮都是全新上下文, LLM 不知道做过什么 → 重复 react / 衔接差)
1262
+ let loopProgressSection = '';
1263
+ if (loopActionLog.length > 0) {
1264
+ const actionLines = loopActionLog
1265
+ .map((a, i) => {
1266
+ const args = a.argsPreview ? `(${a.argsPreview.slice(0, 60)})` : '';
1267
+ const res = a.success ? '✓' : '✗';
1268
+ return ` ${i + 1}. ${res} ${a.tool}${args}`;
1269
+ })
1270
+ .join('\n');
1271
+ loopProgressSection = `\n【本轮循环进度】你已完成以下 ${loopActionLog.length} 个动作, 这是连续执行的同一轮任务:\n${actionLines}\n请基于已有结果继续推进, 不要重复执行上面已成功的动作. 全部完成后用 <final gen> 结束.\n`;
1272
+ }
1255
1273
  const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1256
1274
  当前工作目录: ${this.cwd}
1257
1275
  当前身份: ${this.identity.name} (${this.identity.did})
1258
1276
  ${refineContext}
1259
1277
  ${this.currentIntentHint}
1278
+ ${loopProgressSection}
1260
1279
 
1261
1280
  ${toolDefs}
1262
1281
 
@@ -1361,9 +1380,10 @@ ${toolDefs}
1361
1380
  }
1362
1381
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
1363
1382
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
1364
- // 通知前端:收到 LLM 回复
1383
+ // 通知前端:收到 LLM 回复 (2026-08-09: 不再截断 100 字符 — 前端流式渲染完整内容,
1384
+ // 配合 Hermes 式回复框: 加载中显示完整文本, 完成后封闭底框)
1365
1385
  if (onStream) {
1366
- onStream({ type: 'token', content: reply.substring(0, 100) });
1386
+ onStream({ type: 'token', content: reply });
1367
1387
  }
1368
1388
  // 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
1369
1389
  // 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
@@ -1415,9 +1435,10 @@ ${toolDefs}
1415
1435
  content: reply,
1416
1436
  toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
1417
1437
  });
1418
- // 顺序执行每个工具
1419
- for (let ti = 0; ti < toolCalls.length; ti++) {
1420
- const toolCall = toolCalls[ti];
1438
+ // 2026-08-09: 并发执行本轮所有工具 (Hermes 式 Agent Runtime: 一轮内多工具并行,
1439
+ // 一轮没跑完之前不中断 工具执行不检查 abort, 全部完成才 continue 下一轮)
1440
+ // 旧实现顺序 for 循环, 一个工具等一个, 慢; 且多工具时 LLM 要等全部串完才能看到结果.
1441
+ await Promise.all(toolCalls.map(async (toolCall, ti) => {
1421
1442
  const isMulti = toolCalls.length > 1;
1422
1443
  // 通知前端
1423
1444
  if (onStream) {
@@ -1450,7 +1471,7 @@ ${toolDefs}
1450
1471
  const denyResultMsg = { success: false, error: `拒绝: [${denyResult.source}] ${denyResult.reason}` };
1451
1472
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
1452
1473
  this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
1453
- continue;
1474
+ return;
1454
1475
  }
1455
1476
  if (denyResult.systemAddition) {
1456
1477
  this.contextHintAddition += '\n' + denyResult.systemAddition;
@@ -1469,7 +1490,7 @@ ${toolDefs}
1469
1490
  if (onStream)
1470
1491
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
1471
1492
  console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
1472
- continue;
1493
+ return;
1473
1494
  }
1474
1495
  // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1475
1496
  // 失败静默 — hook 自身挂掉 = 放行
@@ -1513,7 +1534,7 @@ ${toolDefs}
1513
1534
  this.messageHistory.push({ role: 'system', content: `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.` });
1514
1535
  consecutiveErrors = 0;
1515
1536
  }
1516
- continue;
1537
+ return;
1517
1538
  }
1518
1539
  }
1519
1540
  catch (err) {
@@ -1550,7 +1571,7 @@ ${toolDefs}
1550
1571
  this.messageHistory.push({ role: 'system', content: `[注意] 连续 ${consecutiveErrors} 次工具调用被 Harness 拒绝. 请换其他工具或直接回答.` });
1551
1572
  consecutiveErrors = 0;
1552
1573
  }
1553
- continue;
1574
+ return;
1554
1575
  }
1555
1576
  }
1556
1577
  catch (err) {
@@ -1587,6 +1608,20 @@ ${toolDefs}
1587
1608
  }
1588
1609
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
1589
1610
  this.logToHarness(toolCall.name, toolCall.args, result);
1611
+ // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
1612
+ // 去重: 同一工具同 args 连续成功只记一次 (防 LLM 重复 react 刷屏)
1613
+ const argsPreview = JSON.stringify(toolCall.args || {}).slice(0, 120);
1614
+ const isDup = loopActionLog.some((a) => a.tool === toolCall.name && a.argsPreview === argsPreview && a.success === !!result.success);
1615
+ if (!isDup) {
1616
+ loopActionLog.push({
1617
+ tool: toolCall.name,
1618
+ argsPreview,
1619
+ resultPreview: result.success
1620
+ ? String(result.output || '(无输出)').slice(0, 200)
1621
+ : String(result.error || 'failed').slice(0, 200),
1622
+ success: !!result.success,
1623
+ });
1624
+ }
1590
1625
  if (onStream) {
1591
1626
  if (result.success) {
1592
1627
  onStream({ type: 'status', content: `✅ ${toolCall.name} 执行成功`, tool: toolCall.name });
@@ -1644,7 +1679,7 @@ ${toolDefs}
1644
1679
  lastFailedTool = '';
1645
1680
  lastFailedToolCount = 0;
1646
1681
  consecutiveErrors = 0;
1647
- continue;
1682
+ return;
1648
1683
  }
1649
1684
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
1650
1685
  this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
@@ -1665,7 +1700,7 @@ ${toolDefs}
1665
1700
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
1666
1701
  console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
1667
1702
  }
1668
- } // end for (ti)
1703
+ })); // end Promise.all(toolCalls.map(async ...))
1669
1704
  // 所有工具执行完毕后, continue while 循环, 让 LLM 看到结果
1670
1705
  continue;
1671
1706
  }
@@ -1675,9 +1710,9 @@ ${toolDefs}
1675
1710
  role: 'assistant',
1676
1711
  content: reply
1677
1712
  });
1678
- // 通知前端收到非工具调用回复
1713
+ // 通知前端收到非工具调用回复 (2026-08-09: 完整内容, 不再截断 150)
1679
1714
  if (onStream) {
1680
- onStream({ type: 'token', content: reply.substring(0, 150) });
1715
+ onStream({ type: 'token', content: reply });
1681
1716
  }
1682
1717
  // 2026-06-19 架构 fix: 只有 strip <think> 后才检查 isFinalResponse
1683
1718
  // (parseToolCall 已先尝试, 既然没解析出 tool_call, 现在检查 final gen 是否真的在最终回答区)
@@ -1720,6 +1755,7 @@ ${toolDefs}
1720
1755
  reviewsDone: loopReviewCount,
1721
1756
  userIntent: this.currentIntentHint,
1722
1757
  completedTools: Array.from(loopReviewCompletedTools),
1758
+ actionLog: loopActionLog,
1723
1759
  }, DEFAULT_MAX_REVIEWS);
1724
1760
  if (reviewDecision.kind === 'continue-review') {
1725
1761
  loopReviewCount++;
@@ -260,7 +260,7 @@ export class WorkflowPivotLoop {
260
260
  const llmResponse = await llm.chat(context, headerForThisIter, signal, openAITools);
261
261
  const reply = (llmResponse.reply || '').trim();
262
262
  this.vlog(`[pivot] iter=${this.state.iteration} LLM took=${Date.now() - t0}ms reply=${reply.length} nativeToolCalls=${llmResponse.toolCalls?.length ?? 0} head=${reply.substring(0, 80).replace(/\n/g, ' ')}`);
263
- this.emit({ type: 'token', content: reply.substring(0, 100) });
263
+ this.emit({ type: 'token', content: reply });
264
264
  // 2026-07-06: 把完整 reply 推给前端 — 前端按需更新临时气泡
265
265
  // 之前只 emit token(100B 截断), 前端拿到 100B 看不清. 现在 emit preview 带完整 content.
266
266
  // 折叠 / 清洗交给前端的 message-renderer.addMessage (类型 ai 入口统一 strip).
package/dist/cli-entry.js CHANGED
@@ -111,11 +111,13 @@ function getMainScript() {
111
111
  }
112
112
  function getElectronPath() {
113
113
  // ESM 兼容: use _require (createRequire) instead of raw require
114
+ // 2026-08-09: electron 在 devDependencies, 全局安装不装 → require 失败返回 null
115
+ // (上层 startElectron 收到 null 后降级为 Web 模式, 不再返回裸字符串 'electron' 导致 ENOENT)
114
116
  try {
115
117
  return _require('electron');
116
118
  }
117
119
  catch {
118
- return 'electron';
120
+ return null;
119
121
  }
120
122
  }
121
123
  // 解析命令行参数
@@ -466,6 +468,14 @@ async function runNodeScript(scriptPath, additionalArgs) {
466
468
  // 启动 Electron
467
469
  async function startElectron(additionalArgs) {
468
470
  const electronPath = getElectronPath();
471
+ // 2026-08-09: electron 是 devDependency, 全局安装不包含 → 降级为 Web 模式
472
+ // (浏览器打开 Web UI, 功能等价, 避免 spawn electron ENOENT 直接退出)
473
+ if (!electronPath) {
474
+ log('未检测到 Electron 桌面运行时 (全局安装不含 devDependencies)', YELLOW);
475
+ log('自动降级为 Web 模式 — 浏览器打开 Web UI...', CYAN);
476
+ await startWebServer(additionalArgs);
477
+ return;
478
+ }
469
479
  const distDir = getDistDir();
470
480
  // 确定主进程入口
471
481
  let mainPath = path.join(distDir, 'electron.js');
@@ -3791,6 +3791,13 @@ ${data.error || "channel not found"}`, "error");
3791
3791
  if (nameEl) nameEl.textContent = identity.name || "\u533F\u540D";
3792
3792
  if (didEl) didEl.textContent = identity.didShort ? `did:key:${identity.didShort}` : "";
3793
3793
  if (letterEl) letterEl.textContent = letter;
3794
+ const avatarEl = document.getElementById("user-avatar");
3795
+ if (avatarEl) {
3796
+ avatarEl.style.cursor = "pointer";
3797
+ avatarEl.onclick = () => {
3798
+ openAuthModal();
3799
+ };
3800
+ }
3794
3801
  if (nameEl) {
3795
3802
  nameEl.onclick = () => {
3796
3803
  const current = nameEl.textContent || "";
@@ -3841,6 +3848,140 @@ ${data.error || "channel not found"}`, "error");
3841
3848
  } catch (e) {
3842
3849
  }
3843
3850
  }
3851
+ function getEl(id) {
3852
+ return document.getElementById(id);
3853
+ }
3854
+ function showAuthMsg(text, isError = false) {
3855
+ const msg = getEl("auth-msg");
3856
+ if (!msg) return;
3857
+ msg.textContent = text;
3858
+ msg.style.display = "block";
3859
+ msg.style.color = isError ? "#ef4444" : "#22c55e";
3860
+ }
3861
+ async function refreshAuthAccounts() {
3862
+ try {
3863
+ const res = await fetch("/api/auth/status");
3864
+ if (!res.ok) return;
3865
+ const data = await res.json();
3866
+ const statusLine = getEl("auth-status-line");
3867
+ if (statusLine) {
3868
+ statusLine.innerHTML = `\u5F53\u524D\u7528\u6237 DID: <code style="font-size:11px;">${escapeHtml2(data.did || "")}</code><br><span style="color:var(--text-muted);">\u6240\u6709\u767B\u5F55\u8D26\u53F7\u4E0E DIAP \u667A\u80FD\u4F53\u8EAB\u4EFD\u5747\u5F52\u5C5E\u6B64 DID\u3002</span>`;
3869
+ }
3870
+ const listEl = getEl("auth-accounts-list");
3871
+ if (listEl) {
3872
+ const accs = data.accounts || [];
3873
+ if (accs.length === 0) {
3874
+ listEl.innerHTML = '<div class="form-info" style="margin-top:4px;color:var(--text-muted);">\u5C1A\u672A\u7ED1\u5B9A\u4EFB\u4F55\u8D26\u53F7</div>';
3875
+ } else {
3876
+ listEl.innerHTML = '<div class="form-info" style="margin-bottom:4px;"><b>\u5DF2\u7ED1\u5B9A\u8D26\u53F7</b></div>' + accs.map((a) => `<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border:1px solid var(--border);border-radius:6px;margin-bottom:4px;font-size:12px;">
3877
+ <span>${escapeHtml2(a.provider)}${a.identifier ? " \xB7 " + escapeHtml2(a.identifier) : ""}${a.skeleton ? ' <span style="color:#f59e0b;">(\u9AA8\u67B6)</span>' : ""}</span>
3878
+ <button class="btn-secondary btn-sm auth-unbind-btn" data-provider="${escapeHtml2(a.provider)}" style="padding:1px 8px;font-size:11px;">\u89E3\u7ED1</button>
3879
+ </div>`).join("");
3880
+ listEl.querySelectorAll(".auth-unbind-btn").forEach((btn) => {
3881
+ btn.addEventListener("click", async () => {
3882
+ const provider = btn.dataset.provider || "";
3883
+ try {
3884
+ const r = await fetch("/api/auth/logout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider }) });
3885
+ if (r.ok) {
3886
+ showAuthMsg(`\u2713 \u5DF2\u89E3\u7ED1 ${provider}`);
3887
+ refreshAuthAccounts();
3888
+ } else {
3889
+ showAuthMsg(`\u89E3\u7ED1\u5931\u8D25: ${(await r.json()).error || ""}`, true);
3890
+ }
3891
+ } catch {
3892
+ showAuthMsg("\u89E3\u7ED1\u5931\u8D25", true);
3893
+ }
3894
+ });
3895
+ });
3896
+ }
3897
+ }
3898
+ } catch {
3899
+ }
3900
+ }
3901
+ async function openAuthModal() {
3902
+ const modal = getEl("auth-modal");
3903
+ if (!modal) return;
3904
+ modal.style.display = "flex";
3905
+ const msg = getEl("auth-msg");
3906
+ if (msg) msg.style.display = "none";
3907
+ const emailEl = getEl("auth-email");
3908
+ const phoneEl = getEl("auth-phone");
3909
+ if (emailEl) emailEl.value = "";
3910
+ if (phoneEl) phoneEl.value = "";
3911
+ await refreshAuthAccounts();
3912
+ }
3913
+ function closeAuthModal() {
3914
+ const modal = getEl("auth-modal");
3915
+ if (modal) modal.style.display = "none";
3916
+ }
3917
+ async function authLogin(provider, identifier) {
3918
+ try {
3919
+ const res = await fetch("/api/auth/login", {
3920
+ method: "POST",
3921
+ headers: { "Content-Type": "application/json" },
3922
+ body: JSON.stringify({ provider, identifier })
3923
+ });
3924
+ const data = await res.json();
3925
+ if (!res.ok) {
3926
+ showAuthMsg(data.error || "\u767B\u5F55\u5931\u8D25", true);
3927
+ return;
3928
+ }
3929
+ showAuthMsg(`\u2713 ${data.message || "\u5DF2\u7ED1\u5B9A"}`);
3930
+ await refreshAuthAccounts();
3931
+ loadUserIdentity();
3932
+ } catch {
3933
+ showAuthMsg("\u767B\u5F55\u8BF7\u6C42\u5931\u8D25", true);
3934
+ }
3935
+ }
3936
+ function bindAuthModalEvents() {
3937
+ const closeBtn = getEl("auth-modal-close");
3938
+ if (closeBtn) closeBtn.onclick = closeAuthModal;
3939
+ document.querySelectorAll(".auth-oauth-btn").forEach((btn) => {
3940
+ btn.addEventListener("click", () => {
3941
+ const provider = btn.dataset.provider || "";
3942
+ authLogin(provider);
3943
+ });
3944
+ });
3945
+ const emailBtn = getEl("auth-email-btn");
3946
+ if (emailBtn) emailBtn.onclick = () => {
3947
+ const emailEl = getEl("auth-email");
3948
+ const val = emailEl?.value?.trim() || "";
3949
+ if (!val) {
3950
+ showAuthMsg("\u8BF7\u586B\u5199\u90AE\u7BB1", true);
3951
+ return;
3952
+ }
3953
+ authLogin("email", val);
3954
+ };
3955
+ const phoneBtn = getEl("auth-phone-btn");
3956
+ if (phoneBtn) phoneBtn.onclick = () => {
3957
+ const phoneEl = getEl("auth-phone");
3958
+ const val = phoneEl?.value?.trim() || "";
3959
+ if (!val) {
3960
+ showAuthMsg("\u8BF7\u586B\u5199\u624B\u673A\u53F7", true);
3961
+ return;
3962
+ }
3963
+ authLogin("phone", val);
3964
+ };
3965
+ const emailInput = getEl("auth-email");
3966
+ if (emailInput) emailInput.addEventListener("keydown", (e) => {
3967
+ if (e.key === "Enter") getEl("auth-email-btn")?.click();
3968
+ });
3969
+ const phoneInput = getEl("auth-phone");
3970
+ if (phoneInput) phoneInput.addEventListener("keydown", (e) => {
3971
+ if (e.key === "Enter") getEl("auth-phone-btn")?.click();
3972
+ });
3973
+ const modal = getEl("auth-modal");
3974
+ if (modal) modal.addEventListener("click", (e) => {
3975
+ if (e.target === modal) closeAuthModal();
3976
+ });
3977
+ }
3978
+ if (typeof document !== "undefined") {
3979
+ if (document.readyState === "loading") {
3980
+ document.addEventListener("DOMContentLoaded", bindAuthModalEvents);
3981
+ } else {
3982
+ bindAuthModalEvents();
3983
+ }
3984
+ }
3844
3985
  async function init() {
3845
3986
  const themeData = await loadTheme();
3846
3987
  currentAgentId = themeData.agentId || `agent_${generateId().substring(0, 8)}`;
@@ -414,6 +414,54 @@
414
414
  </main>
415
415
  </div>
416
416
 
417
+ <!-- ========== 登录 Modal (2026-08-09) — GitHub/Google/邮箱/手机号, 仅骨架 ========== -->
418
+ <div id="auth-modal" class="modal" style="display:none;">
419
+ <div class="modal-content">
420
+ <div class="modal-header">
421
+ <h2>登录 · 账号绑定</h2>
422
+ <button id="auth-modal-close" class="modal-close">&times;</button>
423
+ </div>
424
+ <div class="modal-body">
425
+ <div id="auth-status-line" class="form-info" style="margin-bottom:12px;font-size:12px;"></div>
426
+
427
+ <!-- OAuth 骨架按钮 -->
428
+ <div class="form-group">
429
+ <label>第三方账号</label>
430
+ <div style="display:flex;gap:8px;flex-wrap:wrap;">
431
+ <button type="button" class="btn-secondary auth-oauth-btn" data-provider="github" style="flex:1;min-width:120px;">GitHub</button>
432
+ <button type="button" class="btn-secondary auth-oauth-btn" data-provider="google" style="flex:1;min-width:120px;">Google</button>
433
+ </div>
434
+ <small class="form-hint" id="auth-oauth-hint">骨架: 点击后记录账号归属当前 DID, 真实 OAuth 授权页后续接入。</small>
435
+ </div>
436
+
437
+ <!-- 邮箱登录骨架 -->
438
+ <div class="form-group">
439
+ <label for="auth-email">邮箱登录</label>
440
+ <div style="display:flex;gap:8px;">
441
+ <input type="email" id="auth-email" placeholder="you@example.com" style="flex:1;">
442
+ <button type="button" class="btn-secondary" id="auth-email-btn">绑定</button>
443
+ </div>
444
+ <small class="form-hint">骨架: 记录邮箱归属当前 DID, 验证码发送后续接入。</small>
445
+ </div>
446
+
447
+ <!-- 手机号登录骨架 -->
448
+ <div class="form-group">
449
+ <label for="auth-phone">手机号登录</label>
450
+ <div style="display:flex;gap:8px;">
451
+ <input type="tel" id="auth-phone" placeholder="+86 138... " style="flex:1;">
452
+ <button type="button" class="btn-secondary" id="auth-phone-btn">绑定</button>
453
+ </div>
454
+ <small class="form-hint">骨架: 记录手机号归属当前 DID, 短信验证码后续接入。</small>
455
+ </div>
456
+
457
+ <!-- 已绑定列表 -->
458
+ <div id="auth-accounts-list" style="margin-top:8px;"></div>
459
+
460
+ <div id="auth-msg" class="form-info" style="display:none;margin-top:8px;"></div>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
417
465
  <script type="module" src="./components/wallet-viem.mjs"></script>
418
466
  <script type="module" src="./components/p2p/index.js"></script>
419
467
  <script type="module" src="./ui/step-timeline.js"></script>
@@ -1873,6 +1873,13 @@ export async function createWebServer(port = 3000, options = {}) {
1873
1873
  // 尝试发布 DID 到 IPFS
1874
1874
  try {
1875
1875
  const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
1876
+ // 2026-08-09: 归属用户 DID — 智能体身份归属用户唯一身份 (DID 文档带 controller+alsoKnownAs)
1877
+ try {
1878
+ const owner = await loadOrCreateUserIdentity();
1879
+ if (owner?.did)
1880
+ auth.setOwnerDid(owner.did);
1881
+ }
1882
+ catch { /* 归属设置失败不阻塞 */ }
1876
1883
  await auth.registerAgent({ name, services: [] }, kp, '');
1877
1884
  console.log('P2P DID 已发布到 IPFS');
1878
1885
  }
@@ -2793,6 +2800,117 @@ ${goalDesc}
2793
2800
  res.status(500).json({ error: err.message });
2794
2801
  }
2795
2802
  });
2803
+ // ========== 登录框架 (2026-08-09) — GitHub/Google/邮箱/手机号, 仅骨架 ==========
2804
+ // 所有登录方式最终都归属到用户 DID (右下角唯一身份).
2805
+ // 真实 OAuth / 验证码后续接入, 这里先做: 记录账号 + 绑定用户 DID + 提供状态查询.
2806
+ const ACCOUNTS_FILE = `${process.env.HOME || '/tmp'}/.bolloon/accounts.json`;
2807
+ async function loadAccounts() {
2808
+ try {
2809
+ const { readFile } = await import('fs/promises');
2810
+ const parsed = JSON.parse(await readFile(ACCOUNTS_FILE, 'utf-8'));
2811
+ return Array.isArray(parsed) ? parsed : [];
2812
+ }
2813
+ catch {
2814
+ return [];
2815
+ }
2816
+ }
2817
+ async function saveAccounts(accs) {
2818
+ const { mkdir, writeFile } = await import('fs/promises');
2819
+ await mkdir(`${process.env.HOME || '/tmp'}/.bolloon`, { recursive: true });
2820
+ await writeFile(ACCOUNTS_FILE, JSON.stringify(accs, null, 2), { mode: 0o600 });
2821
+ }
2822
+ // GET /api/auth/status — 当前用户 DID + 已绑定账号列表
2823
+ app.get('/api/auth/status', async (_req, res) => {
2824
+ try {
2825
+ const identity = await loadOrCreateUserIdentity();
2826
+ const accs = await loadAccounts();
2827
+ res.json({
2828
+ did: identity.did,
2829
+ didShort: identity.didShort,
2830
+ name: identity.name,
2831
+ // 只返回脱敏视图 (不含 token)
2832
+ accounts: accs.map((a) => ({
2833
+ provider: a.provider,
2834
+ identifier: a.identifier || a.email || a.username || '',
2835
+ loggedAt: a.loggedAt,
2836
+ skeleton: !!a.skeleton,
2837
+ })),
2838
+ });
2839
+ }
2840
+ catch (err) {
2841
+ res.status(500).json({ error: err.message });
2842
+ }
2843
+ });
2844
+ // POST /api/auth/login — 登录骨架 (记录账号 + 绑定用户 DID)
2845
+ // body: { provider: 'github'|'google'|'email'|'phone', identifier?: string }
2846
+ app.post('/api/auth/login', async (req, res) => {
2847
+ try {
2848
+ const { provider, identifier } = req.body || {};
2849
+ const prov = String(provider || '').trim().toLowerCase();
2850
+ const VALID = ['github', 'google', 'email', 'phone'];
2851
+ if (!VALID.includes(prov)) {
2852
+ return res.status(400).json({ error: `provider 必须是 ${VALID.join('/')}` });
2853
+ }
2854
+ // 邮箱/手机号必填 identifier
2855
+ if ((prov === 'email' || prov === 'phone') && !String(identifier || '').trim()) {
2856
+ return res.status(400).json({ error: `${prov === 'email' ? '邮箱' : '手机号'}必填` });
2857
+ }
2858
+ const identity = await loadOrCreateUserIdentity();
2859
+ const accs = await loadAccounts();
2860
+ const idStr = String(identifier || '').trim();
2861
+ const existing = accs.find((a) => a.provider === prov && (!idStr || a.identifier === idStr || a.email === idStr));
2862
+ const now = new Date().toISOString();
2863
+ if (existing) {
2864
+ // 已绑定 → 更新归属 DID + 时间
2865
+ existing.ownerDid = identity.did;
2866
+ existing.loggedAt = now;
2867
+ existing.skeleton = true;
2868
+ }
2869
+ else {
2870
+ accs.push({
2871
+ provider: prov,
2872
+ identifier: idStr || '',
2873
+ email: prov === 'email' ? idStr : '',
2874
+ phone: prov === 'phone' ? idStr : '',
2875
+ username: idStr || '',
2876
+ token: '', // 真实 OAuth 后填
2877
+ ownerDid: identity.did, // 归属用户唯一 DID
2878
+ loggedAt: now,
2879
+ skeleton: true, // 骨架标记: 未做真实 OAuth/验证码
2880
+ });
2881
+ }
2882
+ await saveAccounts(accs);
2883
+ console.log(`[auth] 登录骨架: ${prov}${idStr ? ' ' + idStr : ''} → 归属 DID ${identity.did.substring(0, 20)}...`);
2884
+ res.json({
2885
+ ok: true,
2886
+ provider: prov,
2887
+ identifier: idStr,
2888
+ ownerDid: identity.did,
2889
+ skeleton: true,
2890
+ message: `${prov} 登录骨架已记录 (归属用户 DID), 真实 OAuth/验证码后续接入`,
2891
+ });
2892
+ }
2893
+ catch (err) {
2894
+ res.status(500).json({ error: err.message });
2895
+ }
2896
+ });
2897
+ // POST /api/auth/logout — 解除某个 provider 的绑定 (骨架)
2898
+ app.post('/api/auth/logout', async (req, res) => {
2899
+ try {
2900
+ const { provider } = req.body || {};
2901
+ const prov = String(provider || '').trim().toLowerCase();
2902
+ const accs = await loadAccounts();
2903
+ const before = accs.length;
2904
+ const remaining = accs.filter((a) => a.provider !== prov);
2905
+ if (remaining.length === before)
2906
+ return res.status(404).json({ error: `未绑定 ${prov} 账号` });
2907
+ await saveAccounts(remaining);
2908
+ res.json({ ok: true, provider: prov });
2909
+ }
2910
+ catch (err) {
2911
+ res.status(500).json({ error: err.message });
2912
+ }
2913
+ });
2796
2914
  // 2026-08-08: DID 目录 (Postgres 式) — 以用户 DID 为主键, 绑定 memory/persona/skills/on_policy/context_os 等表
2797
2915
  app.get('/api/did-catalog/:table', async (req, res) => {
2798
2916
  try {
@@ -4176,6 +4294,13 @@ ${goalDesc}
4176
4294
  const pkBytes = Buffer.from(channel.publicKey, 'hex');
4177
4295
  const kp = { privateKey: new Uint8Array(32), publicKey: pkBytes, did: channel.did };
4178
4296
  const auth = await AgentAuthManager.newWithRemoteIpfs('http://127.0.0.1:5001', 'http://127.0.0.1:8080');
4297
+ // 2026-08-09: 归属用户 DID — 子智能体身份归属用户唯一身份
4298
+ try {
4299
+ const owner = await loadOrCreateUserIdentity();
4300
+ if (owner?.did)
4301
+ auth.setOwnerDid(owner.did);
4302
+ }
4303
+ catch { /* 归属设置失败不阻塞 */ }
4179
4304
  const result = await auth.registerAgent({ name: channel.name, services: [] }, kp, '');
4180
4305
  channel.cid = result.cid || channel.cid;
4181
4306
  // 关键: 不再保存整份 didDocument, 只留 cid/ipnsName 两个引用字段
@@ -1446,6 +1446,38 @@ body {
1446
1446
  gap: 8px;
1447
1447
  }
1448
1448
 
1449
+ /* 2026-08-09: Hermes 式流式回复框 — 加载中底部开放 (虚线底边 + 脉动),
1450
+ 回复完成后 finalizeTimelineAsMessage → addMessage 生成完整封闭气泡 (.bubble-ai).
1451
+ 加载框即"未封闭"状态: 顶部/左右边框实线, 底部虚线示意还在写. */
1452
+ .message-streaming {
1453
+ background: var(--ai-bg);
1454
+ color: var(--ai-text);
1455
+ border: 1px solid var(--border);
1456
+ border-top-left-radius: var(--radius);
1457
+ border-top-right-radius: var(--radius);
1458
+ border-bottom-left-radius: 4px;
1459
+ border-bottom-right-radius: 4px;
1460
+ padding: 14px 18px;
1461
+ line-height: 1.7;
1462
+ white-space: pre-line;
1463
+ word-break: break-word;
1464
+ font-size: 15px;
1465
+ /* 底部开放: 虚线 + 强调色, 视觉上"还没写完" */
1466
+ border-bottom: 2px dashed var(--accent);
1467
+ animation: streamBoxPulse 1.4s ease-in-out infinite;
1468
+ }
1469
+
1470
+ @keyframes streamBoxPulse {
1471
+ 0%, 100% { border-bottom-color: var(--accent); }
1472
+ 50% { border-bottom-color: var(--accent-dark); }
1473
+ }
1474
+
1475
+ /* 完成态: 底部封闭成实线 (由 addMessage 生成 .bubble-ai, 这里兜底旧 streaming 元素) */
1476
+ .message-streaming.closed {
1477
+ border-bottom: 1px solid var(--border);
1478
+ animation: none;
1479
+ }
1480
+
1449
1481
  .streaming-content::before {
1450
1482
  content: '';
1451
1483
  width: 6px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.45",
3
+ "version": "0.3.46",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -59,7 +59,7 @@
59
59
  "@capacitor/ios": "^8.4.2",
60
60
  "@chainsafe/libp2p-noise": "^17.0.0",
61
61
  "@chainsafe/libp2p-yamux": "^8.0.1",
62
- "@diap/sdk": "^0.2.2",
62
+ "@diap/sdk": "^0.2.4",
63
63
  "@libp2p/autonat": "^3.0.25",
64
64
  "@libp2p/circuit-relay-v2": "^4.2.9",
65
65
  "@libp2p/dcutr": "^3.0.20",