@bolloon/bolloon-agent 0.4.22 → 0.4.24

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.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * gateway-join.ts — 文档驱动的「加入全球智能体网络」编排 (2026-09-15)
3
+ *
4
+ * 背景: 手机端 / PC 端的人类入口只有一句话 —— `read https://bolloon.cn/bolloon-gateway-join.md`
5
+ * (默认 prompt, 见 src/web/mobile.js DEFAULT_JOIN_PROMPT)。文档本身是 SKILL.md 形状的入网说明,
6
+ * 但文档第 1-8 节写的是 SDK 伪码 (KeyManager / p2pNetwork.createNode / buildManifestRequest),
7
+ * 智能体手里只有工具, 照抄不了 → 需要一个工具把整条链路真正串起来。
8
+ *
9
+ * 本模块把「读文档 → 入网」收成一个可验证的闭环:
10
+ * ① 读入网说明 (http(s), 校验 frontmatter 是 bolloon-gateway-join)
11
+ * ② 确保 DID 身份
12
+ * ③ 确保 P2P 节点 (拿 peerId)
13
+ * ④ 注册本地 manifest (文档第 3 节 /api/agent/register 的等效物: setLocalManifest)
14
+ * ⑤ 建成可分享的网络 (orbitdb registry 链接) + 登记到本机 Agent 服务注册表
15
+ * ⑥ 落盘入网态 (幂等: 同 url 重复入网 → already)
16
+ *
17
+ * 诚实原则: 每一步都记 step.note; 拿不到的 (文档不可达 / 非入网说明 / 节点未就绪 / registry 离线)
18
+ * 一律如实报 ok=false 或 step.ok=false, 不假装入网成功。
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as os from 'os';
22
+ import * as path from 'path';
23
+ export const DEFAULT_GATEWAY_JOIN_DOC = 'https://bolloon.cn/bolloon-gateway-join.md';
24
+ /** 入网说明文档的识别名 (frontmatter name) / 能力标记 */
25
+ export const GATEWAY_JOIN_DOC_NAME = 'bolloon-gateway-join';
26
+ export const GATEWAY_JOIN_CAPABILITY = 'gateway-join';
27
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
28
+ const STATE_FILE = () => path.join(home(), '.bolloon', 'gateway-join.json');
29
+ /** 极简 frontmatter 解析 (只认 name/version/capabilities 三个键, 够用且不引 yaml 依赖) */
30
+ export function parseSkillFrontmatter(text) {
31
+ const raw = String(text || '');
32
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
33
+ if (!m)
34
+ return { body: raw };
35
+ const head = m[1];
36
+ const body = raw.slice(m[0].length);
37
+ const out = {};
38
+ for (const line of head.split(/\r?\n/)) {
39
+ const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line.trim());
40
+ if (!kv)
41
+ continue;
42
+ const key = kv[1].toLowerCase();
43
+ const val = kv[2].trim();
44
+ if (key === 'name')
45
+ out.name = val.replace(/^["']|["']$/g, '');
46
+ else if (key === 'version')
47
+ out.version = val.replace(/^["']|["']$/g, '');
48
+ else if (key === 'capabilities') {
49
+ out.capabilities = val.replace(/^\[|\]$/g, '').split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
50
+ }
51
+ }
52
+ return { ...out, body };
53
+ }
54
+ /** 从文档文本判定「这是不是入网说明」 */
55
+ export function isGatewayJoinDoc(text) {
56
+ const fm = parseSkillFrontmatter(text);
57
+ if (fm.name === GATEWAY_JOIN_DOC_NAME)
58
+ return true;
59
+ if (fm.capabilities?.includes(GATEWAY_JOIN_CAPABILITY))
60
+ return true;
61
+ // 没有 frontmatter 也允许 (纯正文版本): 标题 + 关键步骤同时出现才算
62
+ return /加入网关|加入 Bolloon|bolloon-gateway-join/.test(text) && /api\/agent\/register/.test(text);
63
+ }
64
+ export async function readGatewaySkillDoc(url, opts = {}) {
65
+ const u = String(url || '').trim();
66
+ if (!/^https?:\/\//i.test(u))
67
+ return { ok: false, url: u, error: `url 必须以 http(s):// 开头 (收到: ${u.slice(0, 60)})` };
68
+ let text = opts.text;
69
+ if (text === undefined) {
70
+ const f = opts.fetchImpl || fetch;
71
+ try {
72
+ const r = await f(u, { signal: AbortSignal.timeout(opts.timeoutMs ?? 15000) });
73
+ if (!r.ok)
74
+ return { ok: false, url: u, error: `文档不可达 (HTTP ${r.status})` };
75
+ text = await r.text();
76
+ }
77
+ catch (e) {
78
+ return { ok: false, url: u, error: `文档不可达: ${String(e?.message || e).slice(0, 140)}` };
79
+ }
80
+ }
81
+ const raw = String(text || '');
82
+ if (!raw.trim())
83
+ return { ok: false, url: u, error: '文档为空' };
84
+ const fm = parseSkillFrontmatter(raw);
85
+ if (!isGatewayJoinDoc(raw)) {
86
+ return { ok: false, url: u, name: fm.name, error: '这不是 Bolloon 网关入网说明 (缺 name: bolloon-gateway-join), 拒绝据此入网' };
87
+ }
88
+ return { ok: true, url: u, name: fm.name, version: fm.version, capabilities: fm.capabilities, body: fm.body };
89
+ }
90
+ export async function getGatewayJoinState() {
91
+ try {
92
+ const raw = fs.readFileSync(STATE_FILE(), 'utf-8');
93
+ const parsed = JSON.parse(raw);
94
+ return parsed && typeof parsed === 'object' ? parsed : null;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ export function writeGatewayJoinState(state) {
101
+ try {
102
+ const f = STATE_FILE();
103
+ fs.mkdirSync(path.dirname(f), { recursive: true });
104
+ fs.writeFileSync(f, JSON.stringify(state, null, 2), { mode: 0o600 });
105
+ }
106
+ catch { /* 落盘失败不影响本次入网, 只影响幂等/重启恢复 */ }
107
+ }
108
+ /** 节点初始化并发去重 (同一进程内只起一次 libp2p 节点) */
109
+ let nodeInitPromise = null;
110
+ /**
111
+ * 确保本机 libp2p 节点在跑 (文档第 2 节 p2pNetwork.createNode 的等价物)。
112
+ * p2pNetwork.createNode 不是幂等的, 所以先用 getNodePeerId() 探测 + 单飞 promise 兜住并发。
113
+ */
114
+ export async function ensureGatewayNode(deps = {}) {
115
+ if (deps.peerId)
116
+ return { ok: true, peerId: deps.peerId, started: false };
117
+ try {
118
+ const { p2pNetwork } = await import('../network/p2p.js');
119
+ const existing = String(p2pNetwork.getNodePeerId?.() || '');
120
+ if (existing)
121
+ return { ok: true, peerId: existing, multiaddrs: p2pNetwork.getWsMultiaddrs?.() || [], started: false };
122
+ if (!nodeInitPromise) {
123
+ const timeoutMs = deps.nodeTimeoutMs ?? 25_000;
124
+ nodeInitPromise = (async () => {
125
+ try {
126
+ const info = await Promise.race([
127
+ p2pNetwork.createNode({
128
+ enableRelay: true,
129
+ enableRelayServer: true,
130
+ enableAutoNat: deps.enableAutoNat ?? false,
131
+ enableUPnP: deps.enableUPnP ?? false,
132
+ }),
133
+ new Promise((_, rej) => setTimeout(() => rej(new Error(`节点初始化超时 (${timeoutMs}ms)`)), timeoutMs)),
134
+ ]);
135
+ const peerId = String(info?.peerId || p2pNetwork.getNodePeerId?.() || '');
136
+ return peerId
137
+ ? { ok: true, peerId, multiaddrs: (info?.multiaddrs || []) }
138
+ : { ok: false, error: '节点起来了但拿不到 peerId' };
139
+ }
140
+ catch (e) {
141
+ return { ok: false, error: String(e?.message || e).slice(0, 160) };
142
+ }
143
+ finally {
144
+ // 失败也允许下次重试 (成功时后续调用被 getNodePeerId 分支短路)
145
+ setTimeout(() => { nodeInitPromise = null; }, 1000);
146
+ }
147
+ })();
148
+ }
149
+ const r = await nodeInitPromise;
150
+ return { ...r, started: !!r.ok };
151
+ }
152
+ catch (e) {
153
+ return { ok: false, error: String(e?.message || e).slice(0, 160) };
154
+ }
155
+ }
156
+ function readUserDid() {
157
+ try {
158
+ const f = path.join(home(), '.bolloon', 'identity', 'user.json');
159
+ const parsed = JSON.parse(fs.readFileSync(f, 'utf-8'));
160
+ return String(parsed?.did || '');
161
+ }
162
+ catch {
163
+ return '';
164
+ }
165
+ }
166
+ /**
167
+ * 文档驱动入网: 读说明 → DID → 节点 → manifest → 建网 → 落盘。
168
+ * 核心三步 (文档/DID/manifest) 任一失败 → ok=false; 网络/服务登记失败只记 note (部分成功不谎报)。
169
+ */
170
+ export async function joinGlobalGateway(opts = {}) {
171
+ const url = String(opts.url || DEFAULT_GATEWAY_JOIN_DOC).trim();
172
+ const deps = opts.deps || {};
173
+ const steps = [];
174
+ // 幂等: 已经按同一文档入过网 → already (不重复建, 但仍返回既有信息)
175
+ const prev = await getGatewayJoinState();
176
+ if (prev && prev.url === url && prev.did && !opts.force) {
177
+ return {
178
+ ok: true, already: true, url, did: prev.did, peerId: prev.peerId,
179
+ networkLink: prev.networkLink, networkId: prev.networkId,
180
+ steps: [{ step: '幂等', ok: true, note: `已于 ${prev.joinedAt} 入网 (${prev.did.slice(0, 24)}…)` }],
181
+ };
182
+ }
183
+ // ① 读入网说明
184
+ const doc = await readGatewaySkillDoc(url, { fetchImpl: deps.fetchImpl, text: deps.docText });
185
+ steps.push({ step: '读入网说明', ok: doc.ok, note: doc.ok ? `${doc.name || 'doc'} v${doc.version || '?'} (${(doc.body || '').length} 字符)` : (doc.error || '') });
186
+ if (!doc.ok)
187
+ return { ok: false, url, error: doc.error, steps };
188
+ // ② DID 身份
189
+ const did = String(opts.did || deps.did || readUserDid() || '');
190
+ steps.push({ step: 'DID 身份', ok: !!did, note: did ? `${did.slice(0, 30)}…` : '本机无 DID (先运行 bolloon 让 server 生成 ~/.bolloon/identity/user.json)' });
191
+ if (!did)
192
+ return { ok: false, url, error: '缺少 DID 身份, 无法入网', steps };
193
+ const name = String(opts.name || 'bolloon-agent');
194
+ // ③ P2P 节点 (文档第 2 节: p2pNetwork.createNode) — 起不来的话如实在 note 里说
195
+ const node = await ensureGatewayNode(deps);
196
+ const peerId = String(node.peerId || '');
197
+ const multiaddrs = node.multiaddrs || [];
198
+ steps.push({
199
+ step: 'P2P 节点',
200
+ ok: !!peerId,
201
+ note: peerId ? `peerId=${peerId.slice(0, 16)}…${node.started ? ' (本次启动)' : ' (已在跑)'}` : `节点未就绪: ${node.error || '未知原因'}`,
202
+ });
203
+ // ③b 登记节点端点 + 广播地址 (文档第 2/7 节) — 非致命
204
+ if (peerId) {
205
+ try {
206
+ const { initializeAgentNetwork, broadcastOwnAddress } = await import('../network/agent-network.js');
207
+ await initializeAgentNetwork(did, name, peerId, multiaddrs);
208
+ await broadcastOwnAddress().catch(() => { });
209
+ steps.push({ step: '登记节点端点 + 广播地址', ok: true, note: `multiaddrs=${multiaddrs.length} 条` });
210
+ }
211
+ catch (e) {
212
+ steps.push({ step: '登记节点端点 + 广播地址', ok: false, note: `失败: ${String(e?.message || e).slice(0, 120)}` });
213
+ }
214
+ }
215
+ // ④ 注册本地 manifest (文档第 3 节)
216
+ const capabilities = (opts.capabilities && opts.capabilities.length ? opts.capabilities : ['chat', 'gateway-join']).map(String);
217
+ const entry = { id: `${name}-main`.replace(/\s+/g, '-'), name, capabilities, status: 'active', ...(peerId ? { peerId } : {}) };
218
+ let registered = false;
219
+ try {
220
+ if (deps.registerManifest) {
221
+ deps.registerManifest({ ownerName: name, ownerPublicKey: did, agents: [entry] });
222
+ registered = true;
223
+ }
224
+ else {
225
+ const { setLocalManifest, getLocalManifest } = await import('./agent-manifest-protocol.js');
226
+ setLocalManifest({ ownerName: name, ownerPublicKey: did, agents: [entry] });
227
+ registered = (getLocalManifest().agents || []).some((a) => a.id === entry.id);
228
+ }
229
+ }
230
+ catch (e) {
231
+ steps.push({ step: '注册 manifest', ok: false, note: `注册失败: ${String(e?.message || e).slice(0, 120)}` });
232
+ return { ok: false, url, did, error: 'manifest 注册失败', steps };
233
+ }
234
+ steps.push({ step: '注册 manifest', ok: registered, note: registered ? `agents=[${entry.id}] capabilities=[${capabilities.join(',')}]` : 'register 后读回为空' });
235
+ if (!registered)
236
+ return { ok: false, url, did, error: 'manifest 注册后读回为空', steps };
237
+ // ⑤ 建网 (可分享的 orbitdb 网络链接) — 离线/registry 未就绪时如实记 note
238
+ let networkLink;
239
+ let networkId;
240
+ try {
241
+ const share = deps.shareLink
242
+ ? await deps.shareLink({ name })
243
+ : await (await import('./gateway-network.js')).shareNetworkLink({ name });
244
+ if (share.ok && share.link) {
245
+ networkLink = share.link;
246
+ networkId = new URLSearchParams(share.link.split('?')[1] || '').get('name') || undefined;
247
+ steps.push({ step: '建成网络 (可分享链接)', ok: true, note: share.link.slice(0, 72) });
248
+ }
249
+ else {
250
+ steps.push({ step: '建成网络 (可分享链接)', ok: false, note: `未建成: ${share.error || 'registry 未就绪 (离线模式)'}` });
251
+ }
252
+ }
253
+ catch (e) {
254
+ steps.push({ step: '建成网络 (可分享链接)', ok: false, note: `异常: ${String(e?.message || e).slice(0, 120)}` });
255
+ }
256
+ // ⑥ 登记到本机 Agent 服务注册表 (让他方按能力发现/委派)
257
+ try {
258
+ const r = deps.registerService
259
+ ? await deps.registerService({ capability: capabilities[0], did, name })
260
+ : await (await import('./agent-gateway.js')).gatewayRegisterAgent({ capability: capabilities[0], description: `${name} 入网节点`, price: '0', per: 'task' }, { did, name, wallet: '' });
261
+ steps.push({ step: '服务登记 (可被发现)', ok: !!r?.ok, note: r?.ok ? '已写入本机 registry' : (r?.error || '未写入') });
262
+ }
263
+ catch (e) {
264
+ steps.push({ step: '服务登记 (可被发现)', ok: false, note: `异常: ${String(e?.message || e).slice(0, 120)}` });
265
+ }
266
+ // ⑦ 落盘入网态 (幂等 + 重启恢复)
267
+ writeGatewayJoinState({ url, did, name, capabilities, peerId: peerId || undefined, networkLink, networkId, joinedAt: new Date().toISOString() });
268
+ steps.push({ step: '落盘入网态', ok: (await getGatewayJoinState())?.url === url, note: STATE_FILE() });
269
+ return { ok: true, url, did, peerId: peerId || undefined, networkLink, networkId, docVersion: doc.version, steps };
270
+ }
271
+ export default { DEFAULT_GATEWAY_JOIN_DOC, readGatewaySkillDoc, isGatewayJoinDoc, parseSkillFrontmatter, joinGlobalGateway, getGatewayJoinState, writeGatewayJoinState };
@@ -1031,12 +1031,23 @@ export function registerBuiltinTools(ctx) {
1031
1031
  // 通用文件读取 (M4)
1032
1032
  ctx.tools.set('read_file', {
1033
1033
  name: 'read_file',
1034
- description: '读取任意文件内容 (相对 cwd). 只读操作, 无白名单限制.',
1035
- parameters: { path: '相对路径 (必填)', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
1034
+ description: '读取任意文件内容 (相对 cwd); 也支持 http(s) URL (如 "read https://…/doc.md" 这种口令 → 直接给 URL 即可). 只读操作, 无白名单限制.',
1035
+ parameters: { path: '相对路径或 http(s) URL (必填)', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
1036
1036
  execute: async (args) => {
1037
1037
  const relPath = String(args.path || '').trim();
1038
1038
  if (!relPath)
1039
1039
  return { success: false, error: 'path 必填' };
1040
+ // 2026-09-15: URL 直读 — 人类口令 "read <url>" 落到的就是本工具, 之前会 ENOENT (当成相对路径)
1041
+ if (/^https?:\/\//i.test(relPath)) {
1042
+ const r = await readUrlAsText(relPath);
1043
+ if (!r.ok)
1044
+ return { success: false, error: `读取 URL 失败: ${r.error}` };
1045
+ const start = Math.max(0, parseInt(String(args.startLine || '0')) || 0);
1046
+ const max = parseInt(String(args.maxLines || '500')) || 500;
1047
+ const lines = String(r.text || '').split('\n');
1048
+ const slice = lines.slice(start, start + max);
1049
+ return { success: true, output: `🌐 ${relPath} (第 ${start + 1}-${start + slice.length} 行, 共 ${lines.length} 行):\n${slice.map((l, i) => `${String(start + i + 1).padStart(4)} | ${l}`).join('\n')}` };
1050
+ }
1040
1051
  try {
1041
1052
  const absPath = path.resolve(ctx.cwd, relPath);
1042
1053
  const content = fsSync.readFileSync(absPath, 'utf-8');
@@ -1175,6 +1186,25 @@ export function registerBuiltinTools(ctx) {
1175
1186
  // Web 上网工具 (2026-08-04) — fetch_url + web_search
1176
1187
  // 用 undici request (独立连接池, 与 pi-ai 一致, 避开全局 fetch 僵尸连接问题)
1177
1188
  // ============================================================
1189
+ /** 读一个 http(s) URL 的正文 (curl, 兼容 TLS 指纹风控). read_file 的 URL 分支复用. 函数声明提升, 上面可调用. */
1190
+ async function readUrlAsText(url, maxChars = 60_000) {
1191
+ try {
1192
+ const { execFile } = await import('child_process');
1193
+ const { promisify } = await import('util');
1194
+ const pExecFile = promisify(execFile);
1195
+ const { stdout, stderr } = await pExecFile('curl', [
1196
+ '-sL', '--max-time', '25',
1197
+ '-A', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
1198
+ url,
1199
+ ], { maxBuffer: 4 * 1024 * 1024, timeout: 30_000 });
1200
+ if (!stdout)
1201
+ return { ok: false, error: String(stderr || '空响应').slice(0, 200) };
1202
+ return { ok: true, text: String(stdout).slice(0, maxChars) };
1203
+ }
1204
+ catch (e) {
1205
+ return { ok: false, error: String(e?.message || e).slice(0, 200) };
1206
+ }
1207
+ }
1178
1208
  ctx.tools.set('fetch_url', {
1179
1209
  name: 'fetch_url',
1180
1210
  description: '抓取一个 URL 的网页内容并转成纯文本. 适合查文档/新闻/API 页面. 返回前 4000 字符. HTML 自动去标签, JSON/文本原样返回. (走 curl, 兼容 TLS 指纹风控)',
@@ -2835,6 +2865,10 @@ export function registerBuiltinTools(ctx) {
2835
2865
  const link = String(args.link || '').trim();
2836
2866
  if (!link)
2837
2867
  return { success: false, error: 'link 必填 (ipns:// / orbitdb:// / https://)' };
2868
+ // 2026-09-15: 入网说明文档 (.md) 不是 registry 链接 — 别静默当 registry 拉, 指到正确工具
2869
+ if (/^https?:\/\//i.test(link) && (/\.md(\?|#|$)/i.test(link) || /gateway-join/i.test(link))) {
2870
+ return { success: false, error: `这是一个「入网说明文档」而不是网络链接。请改用 join_global_gateway (url=${link}) 走文档驱动入网。` };
2871
+ }
2838
2872
  try {
2839
2873
  const { joinNetwork } = await import('./gateway-network.js');
2840
2874
  const r = await joinNetwork(link);
@@ -2847,6 +2881,45 @@ export function registerBuiltinTools(ctx) {
2847
2881
  }
2848
2882
  },
2849
2883
  });
2884
+ // ============================================================
2885
+ // join_global_gateway (2026-09-15) — 文档驱动的「加入全球智能体网络」
2886
+ // 人类口令只有一句: `read https://bolloon.cn/bolloon-gateway-join.md`
2887
+ // 文档写的是 SDK 伪码, agent 手里只有工具 → 本工具把整条链路串成闭环:
2888
+ // 读说明 → DID → P2P 节点 → 注册 manifest → 建成可分享网络 → 服务登记 → 落盘(幂等)
2889
+ // ============================================================
2890
+ ctx.tools.set('join_global_gateway', {
2891
+ name: 'join_global_gateway',
2892
+ description: '按「入网说明文档」加入全球智能体网络 (完整闭环: 读文档 → DID 身份 → P2P 节点 → 注册本地 manifest → 建成可分享的网络 → 服务登记 → 落盘). 默认文档即 https://bolloon.cn/bolloon-gateway-join.md (人类口令 "read <该文档>" 时用本工具)。幂等: 重复入网返回 already。文档不可达或不是入网说明 → 如实失败, 不假装入网。',
2893
+ parameters: {
2894
+ url: '入网说明文档 URL (可选, 默认 https://bolloon.cn/bolloon-gateway-join.md)',
2895
+ name: '本智能体名字 (可选, 默认取当前身份名)',
2896
+ capabilities: '声明能力, 逗号分隔 (可选, 默认 chat,gateway-join)',
2897
+ force: 'true = 忽略幂等重新入网 (可选)',
2898
+ },
2899
+ execute: async (args) => {
2900
+ try {
2901
+ const { joinGlobalGateway } = await import('./gateway-join.js');
2902
+ const caps = String(args.capabilities || '').split(',').map((s) => s.trim()).filter(Boolean);
2903
+ const r = await joinGlobalGateway({
2904
+ url: String(args.url || '').trim() || undefined,
2905
+ did: ctx.identity?.did || undefined,
2906
+ name: String(args.name || '').trim() || ctx.identity?.name || undefined,
2907
+ capabilities: caps.length ? caps : undefined,
2908
+ force: String(args.force || '') === 'true',
2909
+ });
2910
+ const lines = r.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.step}: ${s.note}`);
2911
+ if (!r.ok)
2912
+ return { success: false, error: `${r.error}\n${lines.join('\n')}` };
2913
+ return {
2914
+ success: true,
2915
+ output: `🌐 已加入全球智能体网络${r.already ? ' (已在网, 幂等)' : ''}\nDID: ${r.did}\npeerId: ${r.peerId || '(节点后台启动中)'}${r.networkLink ? `\n可分享网络链接: ${r.networkLink}` : ''}\n${lines.join('\n')}`,
2916
+ };
2917
+ }
2918
+ catch (e) {
2919
+ return { success: false, error: `join_global_gateway 失败: ${String(e?.message || e).slice(0, 200)}` };
2920
+ }
2921
+ },
2922
+ });
2850
2923
  ctx.tools.set('gateway_status', {
2851
2924
  name: 'gateway_status',
2852
2925
  description: '查看 Agent 网络状态 (已注册服务 + 信誉 + 已加入的网络).',
@@ -1473,6 +1473,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1473
1473
  role: 'assistant',
1474
1474
  content: reply,
1475
1475
  toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
1476
+ // 2026-09-15: 思考模式思维链原样存回 (下一轮带 tools 的请求必须回带, 否则 deepseek 400)
1477
+ reasoningContent: response?.reasoningContent,
1476
1478
  });
1477
1479
  // 2026-08-09: 并发执行本轮所有工具 (Hermes 式 Agent Runtime: 一轮内多工具并行,
1478
1480
  // 一轮没跑完之前不中断 — 工具执行不检查 abort, 全部完成才 continue 下一轮)
@@ -1747,7 +1749,8 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1747
1749
  // LLM 返回的不是 tool call 格式
1748
1750
  this.messageHistory.push({
1749
1751
  role: 'assistant',
1750
- content: reply
1752
+ content: reply,
1753
+ reasoningContent: response?.reasoningContent,
1751
1754
  });
1752
1755
  // 通知前端收到非工具调用回复 (2026-08-09: 完整内容, 不再截断 150)
1753
1756
  if (onStream) {
@@ -1979,7 +1982,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
1979
1982
  continue;
1980
1983
  }
1981
1984
  if (r === 'assistant') {
1982
- out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000) });
1985
+ out.push({ role: 'assistant', content: (m.content || '').slice(0, 4000), reasoningContent: m.reasoningContent });
1983
1986
  continue;
1984
1987
  }
1985
1988
  if (r === 'user') {
@@ -2001,7 +2004,7 @@ ${PiAgentSession.TOOL_SELECTION_GUIDE}
2001
2004
  continue;
2002
2005
  }
2003
2006
  if (r === 'assistant') {
2004
- out.push({ role: 'assistant', content: m.content || '' });
2007
+ out.push({ role: 'assistant', content: m.content || '', reasoningContent: m.reasoningContent });
2005
2008
  continue;
2006
2009
  }
2007
2010
  if (r === 'user') {
@@ -34,9 +34,10 @@
34
34
  <div id="app" class="app">
35
35
  <!-- 顶部导航 -->
36
36
  <header class="topbar">
37
+ <button class="icon-btn" id="btn-index" title="索引"><svg class="ico" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
37
38
  <div class="topbar-title" id="topbar-title">首页</div>
38
39
  <div class="topbar-actions" id="topbar-actions">
39
- <button class="icon-btn" id="btn-refresh" title="刷新"><svg class="ico" viewBox="0 0 24 24"><path d="M20 12a8 8 0 1 1-2.3-5.6"/><path d="M20 4v4.5h-4.5"/></svg></button>
40
+ <button class="icon-btn" id="btn-search" title="搜索"><svg class="ico" viewBox="0 0 24 24"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/></svg></button>
40
41
  <button class="icon-btn" id="btn-add" title="添加会话"><svg class="ico" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>
41
42
  </div>
42
43
  </header>
@@ -60,31 +61,43 @@
60
61
  <div class="detail-body" id="detail-body"></div>
61
62
  </div>
62
63
  </main>
63
-
64
- <!-- 网络 tab -->
65
- <section class="page" id="page-network" data-tab="network" hidden>
64
+ <!-- 好友 tab: 好友与 P2P (原来散在网络页, 现已归拢; 网络页不再重复这些) -->
65
+ <section class="page" id="page-friends" data-tab="friends" hidden>
66
66
  <div class="list">
67
- <div class="list-item" id="item-join-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M3.5 9.5a13 13 0 0 1 17 0M6.5 13a8.5 8.5 0 0 1 11 0M9.6 16.3a4 4 0 0 1 4.8 0"/><circle cx="12" cy="19.5" r="1.1" fill="currentColor" stroke="none"/></svg></span><span>加入网络</span></div>
68
- <div class="list-item" id="item-scan-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg></span><span>扫码入网</span></div>
67
+ <div class="list-item" id="item-add-friend"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>连接好友</span><span class="list-arrow">›</span></div>
68
+ <div class="list-item" id="item-nearby"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg></span><span>附近设备</span><span class="list-arrow">›</span></div>
69
+ <div class="list-item" id="item-scan-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg></span><span>扫码入网 / 加好友</span></div>
69
70
  </div>
70
71
  <input type="file" id="qr-scan-input" accept="image/*" capture="environment" style="display:none">
71
- <div class="section-label">P2P 连接</div>
72
- <div class="list" id="p2p-status"></div>
73
- <div class="section-label">Agent 网络</div>
74
- <div class="list" id="net-members"></div>
75
72
  <div class="list">
76
73
  <div class="list-item" id="item-p2p"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.3 3.5 8.5s-1.2 6.2-3.5 8.5c-2.3-2.3-3.5-5.3-3.5-8.5S9.7 5.8 12 3.5z"/></svg></span><span>P2P 好友</span></div>
77
74
  <div class="list-item" id="item-p2p-id"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3" y="5.5" width="18" height="13" rx="2.5"/><circle cx="8.5" cy="11" r="2"/><path d="M5.6 15.8a3.2 3.2 0 0 1 5.8 0M13.5 10h5M13.5 13.5h5"/></svg></span><span>我的 P2P ID</span></div>
78
75
  </div>
76
+ <div class="section-label">P2P 连接</div>
77
+ <div class="list" id="p2p-status"></div>
78
+ <div class="section-label">好友列表</div>
79
+ <div class="list" id="contacts-list"></div>
80
+ </section>
81
+
82
+ </main>
83
+
84
+ <!-- 网络 tab -->
85
+ <section class="page" id="page-network" data-tab="network" hidden>
86
+ <div class="list">
87
+ <div class="list-item" id="item-join-global"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.7 2.6 2.7 14.4 0 17M12 3.5c-2.7 2.6-2.7 14.4 0 17"/><circle cx="12" cy="12" r="1.2" fill="currentColor" stroke="none"/></svg></span><span>一键入网 · 全球智能体网络</span><span class="list-arrow">›</span></div>
88
+ <div class="list-item" id="item-join-net"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M3.5 9.5a13 13 0 0 1 17 0M6.5 13a8.5 8.5 0 0 1 11 0M9.6 16.3a4 4 0 0 1 4.8 0"/><circle cx="12" cy="19.5" r="1.1" fill="currentColor" stroke="none"/></svg></span><span>加入网络</span><span class="list-arrow">›</span></div>
89
+ </div>
90
+ <div class="section-label">Agent 网络</div>
91
+ <div class="list" id="net-members"></div>
79
92
  <div class="section-label">Agent 服务 (协议自动发现)</div>
80
93
  <div class="list" id="agent-services"></div>
81
94
  <div class="list">
82
95
  <div class="list-item" id="item-trade"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7h16M4 12h10M4 17h7"/><path d="M17.5 14.5l2.5 2.5-2.5 2.5"/></svg></span><span style="flex:1">资源交易</span><span class="list-arrow">›</span></div>
83
96
  </div>
84
- <div class="section-label">P2P 好友列表</div>
85
- <div class="list" id="contacts-list"></div>
86
- <div class="section-label">MCP 工具 (触控调用)</div>
87
- <div class="list" id="mcp-tools"></div>
97
+ <div class="section-label">微信息 (微支付)</div>
98
+ <div class="list" id="x402-info-list"></div>
99
+ <div class="section-label">智能体控制 (MCP / Skills)</div>
100
+ <div class="list" id="agent-control"></div>
88
101
  <div class="section-label">待人工审批支付</div>
89
102
  <div class="list" id="approval-list"></div>
90
103
  <div class="section-label">A2UI 动态面板 (智能体生成)</div>
@@ -117,6 +130,7 @@
117
130
  <!-- 底部 tab -->
118
131
  <nav class="tabbar" id="tabbar">
119
132
  <button class="tab active" data-tab="main"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg></span><span>首页</span></button>
133
+ <button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>好友</span></button>
120
134
  <button class="tab" data-tab="network"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.3 3.5 8.5s-1.2 6.2-3.5 8.5c-2.3-2.3-3.5-5.3-3.5-8.5S9.7 5.8 12 3.5z"/></svg></span><span>网络</span></button>
121
135
  <button class="tab" data-tab="me"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="4"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/></svg></span><span>我</span></button>
122
136
  </nav>
@@ -132,15 +146,58 @@
132
146
  </div>
133
147
  </div>
134
148
 
135
- <!-- 添加好友: 底部滑出选择 sheet -->
149
+ <!-- 添加好友: 底部滑出选择 sheet (点按式: 扫码 / 附近 / 待处理申请 / 手动) -->
136
150
  <div class="sheet" id="addfriend-sheet" hidden>
137
151
  <div class="sheet-inner sheet-inner-choices">
138
- <div class="sheet-title">添加 P2P 好友</div>
152
+ <div class="sheet-title">连接好友</div>
153
+ <button class="sheet-choice" id="choice-nearby"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg> 附近的设备</button>
139
154
  <button class="sheet-choice" id="choice-scan"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg> 扫码添加</button>
140
- <button class="sheet-choice" id="choice-manual"><svg class="ico" viewBox="0 0 24 24"><path d="M10.5 13.5a3.5 3.5 0 0 1 0-5l2-2a3.5 3.5 0 0 1 5 5l-1 1"/><path d="M13.5 10.5a3.5 3.5 0 0 1 0 5l-2 2a3.5 3.5 0 0 1-5-5l1-1"/></svg> 手动输入地址</button>
155
+ <button class="sheet-choice" id="choice-requests"><svg class="ico" viewBox="0 0 24 24"><path d="M6 4h9l4 4v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><path d="M9 12h6M9 16h4"/></svg> 待处理申请</button>
156
+ <button class="sheet-choice sheet-cancel" id="choice-manual">⌨️ 手动输入地址 (兜底)</button>
141
157
  <button class="sheet-choice sheet-cancel" id="choice-cancel">取消</button>
142
158
  </div>
143
159
  </div>
160
+
161
+ <!-- 加入网络: 点按式 sheet (扫一扫 / 附近设备 / 手动粘贴兜底) -->
162
+ <div class="sheet" id="network-sheet" hidden>
163
+ <div class="sheet-inner sheet-inner-choices">
164
+ <div class="sheet-title">加入网络</div>
165
+ <button class="sheet-choice" id="choice-join-nearby"><svg class="ico" viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 16 0"/><path d="M7.5 12a4.5 4.5 0 0 1 9 0"/><circle cx="12" cy="12" r="1.4"/><path d="M12 13.6V19"/></svg> 附近的电脑 / 设备</button>
166
+ <button class="sheet-choice" id="choice-join-scan"><svg class="ico" viewBox="0 0 24 24"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg> 扫电脑上的二维码</button>
167
+ <button class="sheet-choice sheet-cancel" id="choice-join-manual">🔗 粘贴入网链接 (兜底)</button>
168
+ <button class="sheet-choice sheet-cancel" id="choice-join-cancel">取消</button>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- 附近设备: 点按列表 (点一条即连接 / 建立好友) -->
173
+ <div class="sheet" id="nearby-sheet" hidden>
174
+ <div class="sheet-inner">
175
+ <div class="sheet-title" id="nearby-title">附近设备</div>
176
+ <div class="sheet-text" id="nearby-hint">正在查找...</div>
177
+ <div class="list" id="nearby-list" style="width:100%;max-height:46vh;overflow-y:auto"></div>
178
+ <button class="sheet-choice" id="nearby-refresh">↻ 刷新</button>
179
+ <button class="sheet-choice sheet-cancel" id="nearby-close">关闭</button>
180
+ </div>
181
+ </div>
182
+ <!-- 微信息 (x402 付费信息): 点一条看详情 → 购买并验真 / 只看元数据 -->
183
+ <div class="sheet" id="x402-sheet" hidden>
184
+ <div class="sheet-inner">
185
+ <div class="sheet-title" id="x402-title">微信息</div>
186
+ <div class="sheet-text" id="x402-body" style="width:100%;max-height:46vh;overflow-y:auto;text-align:left;white-space:pre-wrap;word-break:break-word"></div>
187
+ <button class="sheet-choice" id="x402-buy">购买并验真</button>
188
+ <button class="sheet-choice" id="x402-verify">只看元数据 (离线验真)</button>
189
+ <button class="sheet-choice sheet-cancel" id="x402-close">关闭</button>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- 微信息结果: 内容 + 验真分档 (verified / self-attested / content-only / unverified) -->
194
+ <div class="sheet" id="x402-result-sheet" hidden>
195
+ <div class="sheet-inner">
196
+ <div class="sheet-title" id="x402-result-title">验真结果</div>
197
+ <div class="sheet-text" id="x402-result-body" style="width:100%;max-height:56vh;overflow-y:auto;text-align:left;white-space:pre-wrap;word-break:break-word"></div>
198
+ <button class="sheet-choice sheet-cancel" id="x402-result-close">关闭</button>
199
+ </div>
200
+ </div>
144
201
  </div>
145
202
 
146
203
  <!-- 2026-08-14: 手机端内化内核 (IndexedDB 数据 + 本地身份 + 支付审批 + Agent). 必须最先加载 -->
@@ -52,6 +52,45 @@ export async function resetAgentDb() {
52
52
  }
53
53
  });
54
54
  }
55
+ async function _kvGet(key) {
56
+ try {
57
+ const db = _identityDb || (await openIdentityDb());
58
+ _identityDb = db;
59
+ return await new Promise((res) => { const t = db.transaction('kv', 'readonly'); const r = t.objectStore('kv').get(key); r.onsuccess = () => res(r.result ?? null); r.onerror = () => res(null); });
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function _kvPut(key, val) {
66
+ try {
67
+ const db = _identityDb || (await openIdentityDb());
68
+ _identityDb = db;
69
+ await new Promise((res) => { const t = db.transaction('kv', 'readwrite'); t.objectStore('kv').put(val, key); t.oncomplete = () => res(); t.onerror = () => res(); });
70
+ }
71
+ catch { /* IDB 不可用则仅内存态 */ }
72
+ }
73
+ /** 登录: 设置本机身份昵称 (无身份则新建) + 标记已登录 */
74
+ export async function loginIdentity(name) {
75
+ const id = await ensureIdentity();
76
+ const nm = String(name || '').trim() || 'blln-mobile';
77
+ const next = { ...id, name: nm };
78
+ await _kvPut('identity', next);
79
+ await _kvPut('loggedIn', true);
80
+ _identity = next;
81
+ return { ...next, loggedIn: true };
82
+ }
83
+ /** 注销: 清除登录态 (保留设备 DID, 不影响 P2P/频道) */
84
+ export async function logoutIdentity() {
85
+ await _kvPut('loggedIn', false);
86
+ return { ok: true };
87
+ }
88
+ /** 身份状态 (含登录态; 未登录时 name 置空) */
89
+ export async function identityStatus() {
90
+ const id = await ensureIdentity();
91
+ const loggedIn = (await _kvGet('loggedIn')) === true;
92
+ return { did: id.did, didShort: id.did ? id.did.slice(0, 12) : '', name: loggedIn ? id.name : '', createdAt: id.createdAt, loggedIn };
93
+ }
55
94
  /** 获取本机 DID (首次生成并持久化) */
56
95
  export async function ensureIdentity() {
57
96
  if (_identity)
@@ -318,12 +357,18 @@ export async function runPhoneAgent(goal) {
318
357
  stepCount: r?.stepCount,
319
358
  };
320
359
  }
321
- // 离线 fallback: 内置规则 (不依赖 LLM/无障碍, 手机仍自治可用)
360
+ // 离线 fallback: 内置规则 (不依赖 LLM/原生执行能力, 手机仍自治可用)
322
361
  const reply = await runLocalAgent(goal);
323
- return { ok: true, goal, result: reply, did: id.did, mode: 'fallback' };
362
+ return {
363
+ ok: true, goal, result: reply, did: id.did, mode: 'fallback',
364
+ hint: '当前是「手机本地规则」模式:这台手机上还没接原生执行能力 (iOS 暂不支持原生操控 App; Android 需开无障碍服务),所以只能用内置规则回复。连上电脑端后,任务可以交给电脑端 Agent 真正执行。',
365
+ };
324
366
  }
325
367
  catch (e) {
326
- return { ok: false, goal, error: String(e?.message || e).slice(0, 100), did: id.did, mode: native ? 'native' : 'fallback' };
368
+ return {
369
+ ok: false, goal, error: String(e?.message || e).slice(0, 100), did: id.did, mode: native ? 'native' : 'fallback',
370
+ hint: '任务没跑起来。常见原因:① 电脑端没在运行或不同网段 (设置 → 电脑端同步 里配置/测试) ② 没配 LLM API (设置 → API 配置) ③ 这台手机没接原生执行能力 (iOS 不支持原生操控, Android 需无障碍服务)。',
371
+ };
327
372
  }
328
373
  }
329
374
  /** 手机端 Agent 状态 (phone.agent.status) */
@@ -400,4 +445,4 @@ export async function handleIncomingPhoneMessage(type, payload, fromPeer) {
400
445
  }
401
446
  catch { /* 控制消息处理失败静默 */ }
402
447
  }
403
- export default { ensureIdentity, runLocalAgent, setAgentTransport, onAgentReply, callRemoteAgent, handleIncomingAgentMessage, notifyAgentReply, onInboundChat, setLlmConfig, getLlmConfig, runPhoneAgent, phoneStatus, cancelPhoneAgent, handleIncomingPhoneMessage };
448
+ export default { ensureIdentity, loginIdentity, logoutIdentity, identityStatus, runLocalAgent, setAgentTransport, onAgentReply, callRemoteAgent, handleIncomingAgentMessage, notifyAgentReply, onInboundChat, setLlmConfig, getLlmConfig, runPhoneAgent, phoneStatus, cancelPhoneAgent, handleIncomingPhoneMessage };