@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.
- package/dist/agents/agent-identity.js +17 -0
- package/dist/agents/loop-review.js +21 -5
- package/dist/agents/pi-sdk.js +49 -13
- package/dist/agents/workflow-pivot-loop.js +1 -1
- package/dist/cli-entry.js +11 -1
- package/dist/web/client.js +141 -0
- package/dist/web/index.html +48 -0
- package/dist/web/server.js +125 -0
- package/dist/web/style.css +32 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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');
|
package/dist/web/client.js
CHANGED
|
@@ -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)}`;
|
package/dist/web/index.html
CHANGED
|
@@ -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">×</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>
|
package/dist/web/server.js
CHANGED
|
@@ -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 两个引用字段
|
package/dist/web/style.css
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|