@aiti-ai/pi-web-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/aiti-devtools-pi.js +3371 -0
- package/package.json +53 -0
- package/src/bridge-registry.ts +170 -0
- package/src/pi-web-bridge.ts +1007 -0
|
@@ -0,0 +1,3371 @@
|
|
|
1
|
+
/* aiti-devtools-pi · pi 扩展(内嵌桥 + 工具直连)· 构建产物,勿手改 */
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { join as join3, resolve, dirname as dirname3 } from "node:path";
|
|
5
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, openSync, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
|
|
6
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
7
|
+
import { homedir as homedir2 } from "node:os";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
// ../aiti-devtools-core/src/bridge.ts
|
|
12
|
+
import { randomBytes, randomInt } from "node:crypto";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { mkdirSync, appendFileSync, readdirSync, readFileSync, existsSync, statSync, rmSync, writeFileSync, renameSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
var ID_TTL = parseInt(process.env.AITI_BRIDGE_TTL || "600000", 10);
|
|
17
|
+
var AGENT_TTL = parseInt(process.env.AITI_AGENT_TTL || "3600000", 10);
|
|
18
|
+
var BROADCAST_TTL = parseInt(process.env.AITI_BROADCAST_TTL || "600000", 10);
|
|
19
|
+
var AGENT_GRACE_TTL = parseInt(process.env.AITI_AGENT_GRACE_TTL || "120000", 10);
|
|
20
|
+
var AGENT_HISTORY_TTL = parseInt(process.env.AITI_AGENT_HISTORY_TTL || String(7 * 24 * 3600 * 1e3), 10);
|
|
21
|
+
var SWEEP_MS = parseInt(process.env.AITI_SWEEP_MS || "60000", 10);
|
|
22
|
+
var LEGACY_TOKEN = "dev";
|
|
23
|
+
function createBridge(dataDir = "", opts = {}) {
|
|
24
|
+
const BRIDGE_NAME = opts.name || "aiti-mcp";
|
|
25
|
+
const DISPLAY_NAME = opts.name || "aiti-devtools";
|
|
26
|
+
const TAG = "[" + BRIDGE_NAME + "]";
|
|
27
|
+
const busPublish = opts.busPublish || null;
|
|
28
|
+
const BUS_CONN_TTL = parseInt(process.env.AITI_BUS_CONN_TTL || "150000", 10);
|
|
29
|
+
const idRegistry = /* @__PURE__ */ new Map();
|
|
30
|
+
const pages = /* @__PURE__ */ new Map();
|
|
31
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
32
|
+
const aiConns = /* @__PURE__ */ new Set();
|
|
33
|
+
const mcpReqMap = /* @__PURE__ */ new Map();
|
|
34
|
+
const cancelIdMap = /* @__PURE__ */ new Map();
|
|
35
|
+
const aiPageReg = /* @__PURE__ */ new Map();
|
|
36
|
+
const aiSelfReg = /* @__PURE__ */ new Map();
|
|
37
|
+
const sseMcpClients = /* @__PURE__ */ new Set();
|
|
38
|
+
const mcpSessions = /* @__PURE__ */ new Map();
|
|
39
|
+
const MCP_SESSION_TTL = parseInt(process.env.AITI_MCP_SESSION_TTL || "600000", 10);
|
|
40
|
+
const mcpSessionExpired = (s) => Date.now() - (s && s.lastSeen || 0) > MCP_SESSION_TTL;
|
|
41
|
+
const agents = /* @__PURE__ */ new Map();
|
|
42
|
+
const agentFallbackPageIds = /* @__PURE__ */ new Map();
|
|
43
|
+
const boundPageIdFor = (sess) => {
|
|
44
|
+
const key = sess && (sess.agentKey || sess.agentId);
|
|
45
|
+
const ag = key ? agents.get(String(key)) : null;
|
|
46
|
+
if (!ag) return null;
|
|
47
|
+
if (ag.activeBoundPageId && boundPageAlive(ag.activeBoundPageId)) return String(ag.activeBoundPageId);
|
|
48
|
+
const fbList = agentFallbackPageIds.get(String(key));
|
|
49
|
+
if (fbList && fbList.length > 0) {
|
|
50
|
+
const sorted = fbList.filter(function(e) {
|
|
51
|
+
return boundPageAlive(e.pageId);
|
|
52
|
+
}).sort(function(a, b) {
|
|
53
|
+
return b.lastSeen - a.lastSeen;
|
|
54
|
+
});
|
|
55
|
+
if (sorted.length > 0) return String(sorted[0].pageId);
|
|
56
|
+
}
|
|
57
|
+
if (ag.boundPageId && boundPageAlive(ag.boundPageId)) return String(ag.boundPageId);
|
|
58
|
+
const sid = sess && sess.id || key && String(key).split("/").slice(1).join("/") || null;
|
|
59
|
+
if (sid) {
|
|
60
|
+
let best = null;
|
|
61
|
+
let bestSeen = -1;
|
|
62
|
+
for (const [, rec] of aiSelfReg) {
|
|
63
|
+
const m = String(rec.url || "").match(/[?&]session=([^&#]+)/);
|
|
64
|
+
if (m && decodeURIComponent(m[1]) === sid && (rec.lastSeen || 0) > bestSeen) {
|
|
65
|
+
best = rec;
|
|
66
|
+
bestSeen = rec.lastSeen || 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (best) return String(best.instanceId) + "/ai";
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
const bindAgentPage = (key, pageId) => {
|
|
74
|
+
const a = agents.get(String(key || ""));
|
|
75
|
+
if (!a) return { ok: false, error: "unknown agent: " + key };
|
|
76
|
+
a.boundPageId = pageId ? String(pageId) : null;
|
|
77
|
+
return { ok: true, agentKey: String(key), boundPageId: a.boundPageId };
|
|
78
|
+
};
|
|
79
|
+
const bindAgentFromAiSelf = (rec) => {
|
|
80
|
+
const m = String(rec.url || "").match(/[?&]session=([^&#]+)/);
|
|
81
|
+
if (!m) return;
|
|
82
|
+
const sid = decodeURIComponent(m[1]);
|
|
83
|
+
const newPageId = String(rec.instanceId) + "/ai";
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
for (const [key, ag] of agents) {
|
|
86
|
+
const agSid = ag.sessionId || String(key).split("/").slice(1).join("/");
|
|
87
|
+
if (agSid === sid) {
|
|
88
|
+
if (!agentFallbackPageIds.has(key)) agentFallbackPageIds.set(key, []);
|
|
89
|
+
var list = agentFallbackPageIds.get(key);
|
|
90
|
+
var found = false;
|
|
91
|
+
for (var i = 0; i < list.length; i++) {
|
|
92
|
+
if (list[i].pageId === newPageId) {
|
|
93
|
+
list[i].lastSeen = now;
|
|
94
|
+
found = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!found) list.push({ pageId: newPageId, lastSeen: now });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const bindAgentFromActive = (instanceId, sessionId) => {
|
|
103
|
+
const pageId = instanceId + "/ai";
|
|
104
|
+
for (const [key, ag] of agents) {
|
|
105
|
+
const agSid = ag.sessionId || String(key).split("/").slice(1).join("/");
|
|
106
|
+
if (agSid === sessionId) {
|
|
107
|
+
ag.activeBoundPageId = pageId;
|
|
108
|
+
console.log(TAG + " AI 主窗口活跃绑定:" + key + " → " + pageId);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const boundPageAlive = (pageId) => {
|
|
114
|
+
const inst = String(pageId || "").split("/")[0];
|
|
115
|
+
if (!inst) return false;
|
|
116
|
+
for (const [, rec] of aiSelfReg) if (rec.instanceId === inst) return true;
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
const lastEventPush = /* @__PURE__ */ new Map();
|
|
120
|
+
const pushDeduped = (key, sig) => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const last = lastEventPush.get(key);
|
|
123
|
+
if (last && last.sig === sig && now - last.ts < 5e3) return true;
|
|
124
|
+
lastEventPush.set(key, { sig, ts: now });
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
const agentIdx = /* @__PURE__ */ new Map();
|
|
128
|
+
const agentOnline = (a) => !!(a && a.conn > 0);
|
|
129
|
+
const markOffline = (key) => {
|
|
130
|
+
const a = agents.get(key);
|
|
131
|
+
if (a && !agentOnline(a) && !a.offlineAt) a.offlineAt = Date.now();
|
|
132
|
+
};
|
|
133
|
+
const agentKey = (aid, sid) => safeId(aid) + "/" + safeId(sid || "s_" + randomBytes(3).toString("hex"));
|
|
134
|
+
const randomSessionId = () => "s_" + randomBytes(3).toString("hex");
|
|
135
|
+
const OUTBOX_MAX = 200;
|
|
136
|
+
const safeId = (s) => String(s || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 48);
|
|
137
|
+
const msgSessionDir = (agentId2) => join(dataDir, "aiti-bridge", "sessions", safeId(agentId2));
|
|
138
|
+
const msgSessionFile = (agentId2) => {
|
|
139
|
+
const dir = msgSessionDir(agentId2);
|
|
140
|
+
const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort() : [];
|
|
141
|
+
if (files.length) {
|
|
142
|
+
const latest = join(dir, files[files.length - 1]);
|
|
143
|
+
try {
|
|
144
|
+
if (statSync(latest).size > 2 * 1024 * 1024) return newMsgSessionFile(agentId2);
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
return latest;
|
|
148
|
+
}
|
|
149
|
+
return newMsgSessionFile(agentId2);
|
|
150
|
+
};
|
|
151
|
+
const newMsgSessionFile = (agentId2) => {
|
|
152
|
+
const dir = msgSessionDir(agentId2);
|
|
153
|
+
mkdirSync(dir, { recursive: true });
|
|
154
|
+
return join(dir, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-") + "_" + randomBytes(8).toString("hex") + ".jsonl");
|
|
155
|
+
};
|
|
156
|
+
const appendMsgEvent = (agentId2, evt) => {
|
|
157
|
+
try {
|
|
158
|
+
const file = msgSessionFile(agentId2);
|
|
159
|
+
if (!existsSync(file)) appendFileSync(file, JSON.stringify({ type: "session", version: 1, id: randomBytes(8).toString("hex"), timestamp: (/* @__PURE__ */ new Date()).toISOString(), kind: "aiti-bridge" }) + "\n");
|
|
160
|
+
appendFileSync(file, JSON.stringify(evt) + "\n");
|
|
161
|
+
} catch (e) {
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const readMsgSession = (agentId2) => {
|
|
165
|
+
const rows = [];
|
|
166
|
+
try {
|
|
167
|
+
const dir = msgSessionDir(agentId2);
|
|
168
|
+
if (!existsSync(dir)) return rows;
|
|
169
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith(".jsonl")).sort()) {
|
|
170
|
+
for (const line of readFileSync(join(dir, f), "utf8").split("\n")) {
|
|
171
|
+
if (!line.trim()) continue;
|
|
172
|
+
const evt = JSON.parse(line);
|
|
173
|
+
if (evt.type === "message") rows.push({ id: evt.id, ...evt.message || {}, at: evt.timestamp });
|
|
174
|
+
else if (evt.type === "status_change") {
|
|
175
|
+
const r = rows.find((x) => x.id === evt.msgId);
|
|
176
|
+
if (r) r.status = evt.status;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
}
|
|
182
|
+
return rows;
|
|
183
|
+
};
|
|
184
|
+
const restoreAgentOutbox = (agentId2, agent) => {
|
|
185
|
+
for (const row of readMsgSession(agentId2)) {
|
|
186
|
+
if (row.status !== "consumed") agent.outbox.push({ ...row, status: "pending" });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const ensureAgent = (agentId2, sessionId, session) => {
|
|
190
|
+
if (!agentId2) return { a: null, key: null };
|
|
191
|
+
const aid = safeId(agentId2);
|
|
192
|
+
const sid = sessionId ? safeId(String(sessionId)) : randomSessionId();
|
|
193
|
+
const key = aid + "/" + sid;
|
|
194
|
+
let a = agents.get(key);
|
|
195
|
+
if (!a) {
|
|
196
|
+
a = {
|
|
197
|
+
agentId: aid,
|
|
198
|
+
sessionId: sid,
|
|
199
|
+
registered: !!sessionId,
|
|
200
|
+
name: aid + (session && session.clientInfo && session.clientInfo.name ? " (" + session.clientInfo.name + ")" : ""),
|
|
201
|
+
cwd: null,
|
|
202
|
+
offlineAt: null,
|
|
203
|
+
conn: 0,
|
|
204
|
+
// cwd:工作目录(显示 CWD . name (id));offlineAt:掉线时间(显示/清理用);conn:持久连接数
|
|
205
|
+
transportKind: session && session.open ? "streamable" : "stateless",
|
|
206
|
+
source: session && session.source || "unknown",
|
|
207
|
+
// 调用来源:extension(pi 扩展直连)/ mcp / unknown
|
|
208
|
+
capabilities: session && session.capabilities || {},
|
|
209
|
+
stream: session && session.writer || null,
|
|
210
|
+
boundPageId: null,
|
|
211
|
+
subEvents: /* @__PURE__ */ new Set(),
|
|
212
|
+
outbox: [],
|
|
213
|
+
lastSeen: Date.now(),
|
|
214
|
+
agentStream: null,
|
|
215
|
+
inProcessSink: null
|
|
216
|
+
// 宿主进程内 agent(如 pi-web 方案 M):消息直推回调(注入会话,不走 outbox/SSE)——任何宿主可用
|
|
217
|
+
};
|
|
218
|
+
restoreAgentOutbox(key, a);
|
|
219
|
+
agents.set(key, a);
|
|
220
|
+
let idxSet = agentIdx.get(aid);
|
|
221
|
+
if (!idxSet) {
|
|
222
|
+
idxSet = /* @__PURE__ */ new Set();
|
|
223
|
+
agentIdx.set(aid, idxSet);
|
|
224
|
+
}
|
|
225
|
+
idxSet.add(key);
|
|
226
|
+
console.log(TAG + " agent 会话注册:" + key + (a.registered ? "(显式)" : "(随机 session,引导 aiti_hello 注册)") + " " + a.transportKind + " 恢复 outbox " + a.outbox.length + " 条");
|
|
227
|
+
}
|
|
228
|
+
a.lastSeen = Date.now();
|
|
229
|
+
if (session) {
|
|
230
|
+
a.capabilities = session.capabilities || a.capabilities;
|
|
231
|
+
if (session.writer) a.stream = session.writer;
|
|
232
|
+
if (session.open) a.transportKind = "streamable";
|
|
233
|
+
}
|
|
234
|
+
return { a, key };
|
|
235
|
+
};
|
|
236
|
+
const queueMsg = (key, msg) => {
|
|
237
|
+
const a = agents.get(key);
|
|
238
|
+
if (!a) return { ok: false, error: "unknown agent session: " + key };
|
|
239
|
+
let srcPageId = msg.pageId || msg.payload && msg.payload.pageId || null;
|
|
240
|
+
let srcUrl = msg.url || msg.payload && msg.payload.url || null;
|
|
241
|
+
if (typeof msg.text === "string") {
|
|
242
|
+
const fm = msg.text.match(/\[发自:\s*([^\s·\]((]+)/);
|
|
243
|
+
if (fm) srcPageId = srcPageId || fm[1].trim();
|
|
244
|
+
if (!srcUrl) {
|
|
245
|
+
const um = msg.text.match(/\[发自:\s*[^\s·\]((]+\s*·\s*([^\s((]+)/);
|
|
246
|
+
if (um) srcUrl = um[1].trim();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const m = { id: randomBytes(4).toString("hex"), ...msg, pageId: srcPageId, url: srcUrl, source: msg.source || msg.payload && msg.payload.source || null, sourceLabel: msg.sourceLabel || msg.payload && msg.payload.sourceLabel || null, agentId: key, at: (/* @__PURE__ */ new Date()).toISOString(), status: "pending" };
|
|
250
|
+
a.outbox.push(m);
|
|
251
|
+
if (a.outbox.length > OUTBOX_MAX) a.outbox.splice(0, a.outbox.length - OUTBOX_MAX);
|
|
252
|
+
appendMsgEvent(key, { type: "message", id: m.id, parentId: null, timestamp: m.at, message: m });
|
|
253
|
+
return { ok: true, msg: m };
|
|
254
|
+
};
|
|
255
|
+
const resolvePushTargets = (to) => {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const liveKeys = [...agents.entries()].filter(([, a]) => a.registered && (agentOnline(a) || now - (a.offlineAt || a.lastSeen || 0) <= BROADCAST_TTL)).map(([k]) => k);
|
|
258
|
+
if (!to || to === "all") return liveKeys.sort((a, b) => ((agents.get(b) || {}).lastSeen || 0) - ((agents.get(a) || {}).lastSeen || 0));
|
|
259
|
+
const s = String(to);
|
|
260
|
+
if (agents.has(s)) return [s];
|
|
261
|
+
if (agentIdx.has(s)) return [...agentIdx.get(s)].filter((k) => liveKeys.includes(k)).sort((a, b) => ((agents.get(b) || {}).lastSeen || 0) - ((agents.get(a) || {}).lastSeen || 0));
|
|
262
|
+
return [];
|
|
263
|
+
};
|
|
264
|
+
const listAgentSessions = () => {
|
|
265
|
+
return [...agents.entries()].map(([key, a]) => ({
|
|
266
|
+
agentKey: key,
|
|
267
|
+
agentId: a.agentId,
|
|
268
|
+
sessionId: a.sessionId,
|
|
269
|
+
registered: a.registered,
|
|
270
|
+
name: a.name || a.agentId,
|
|
271
|
+
transportKind: a.transportKind,
|
|
272
|
+
source: a.source || "unknown",
|
|
273
|
+
boundPageId: a.boundPageId || null,
|
|
274
|
+
subEvents: [...a.subEvents],
|
|
275
|
+
online: agentOnline(a),
|
|
276
|
+
agentStream: !!a.agentStream,
|
|
277
|
+
cwd: a.cwd || null,
|
|
278
|
+
offlineAt: a.offlineAt || null,
|
|
279
|
+
inboxPending: a.outbox.filter((m) => m.status !== "consumed").length,
|
|
280
|
+
lastSeen: a.lastSeen
|
|
281
|
+
})).sort((x, y) => (y.lastSeen || 0) - (x.lastSeen || 0));
|
|
282
|
+
};
|
|
283
|
+
const pruneSessionDirs = (now) => {
|
|
284
|
+
if (!dataDir) return;
|
|
285
|
+
const rootDir = join(dataDir, "aiti-bridge", "sessions");
|
|
286
|
+
let dirs = [];
|
|
287
|
+
try {
|
|
288
|
+
dirs = existsSync(rootDir) ? readdirSync(rootDir) : [];
|
|
289
|
+
} catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const d of dirs) {
|
|
293
|
+
try {
|
|
294
|
+
const p = join(rootDir, d);
|
|
295
|
+
const st = statSync(p);
|
|
296
|
+
if (!st.isDirectory()) continue;
|
|
297
|
+
const files = readdirSync(p).filter((f) => f.endsWith(".jsonl"));
|
|
298
|
+
let mtime = st.mtimeMs;
|
|
299
|
+
for (const f of files) {
|
|
300
|
+
try {
|
|
301
|
+
mtime = Math.max(mtime, statSync(join(p, f)).mtimeMs);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (now - mtime > AGENT_HISTORY_TTL) {
|
|
306
|
+
rmSync(p, { recursive: true, force: true });
|
|
307
|
+
console.log(TAG + " 清理历史 session(> " + Math.round(AGENT_HISTORY_TTL / 864e5) + " 天无活动):" + d);
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const agentSweep = () => {
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
for (const [key, a] of agents) {
|
|
316
|
+
if (agentOnline(a)) {
|
|
317
|
+
if (a.offlineAt) a.offlineAt = null;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (now - a.lastSeen <= AGENT_GRACE_TTL) {
|
|
321
|
+
if (a.offlineAt) a.offlineAt = null;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (!a.offlineAt) a.offlineAt = a.lastSeen;
|
|
325
|
+
if (now - a.offlineAt > AGENT_TTL) {
|
|
326
|
+
console.log(TAG + " agent 清理(掉线 > " + Math.round(AGENT_TTL / 6e4) + "min):" + key + " " + (a.name || "") + "(offlineAt " + new Date(a.offlineAt).toISOString() + ")");
|
|
327
|
+
agents.delete(key);
|
|
328
|
+
const idx = agentIdx.get(a.agentId);
|
|
329
|
+
if (idx) {
|
|
330
|
+
idx.delete(key);
|
|
331
|
+
if (!idx.size) agentIdx.delete(a.agentId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const [k, v] of lastEventPush) if (now - v.ts > 36e5) lastEventPush.delete(k);
|
|
336
|
+
for (const [iid, p] of [...pages]) {
|
|
337
|
+
if (p.writer?.viaBus && now - (p.writer.lastSeen || 0) > BUS_CONN_TTL) {
|
|
338
|
+
pages.delete(iid);
|
|
339
|
+
console.log(TAG + " 总线型页面连接超时摘除:" + iid);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const w of [...aiConns]) {
|
|
343
|
+
if (w?.viaBus && now - (w.lastSeen || 0) > BUS_CONN_TTL) {
|
|
344
|
+
aiConns.delete(w);
|
|
345
|
+
aiPageReg.delete(w);
|
|
346
|
+
aiSelfReg.delete(w);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
pruneSessionDirs(now);
|
|
350
|
+
};
|
|
351
|
+
const sweepTimer = setInterval(agentSweep, SWEEP_MS);
|
|
352
|
+
if (typeof sweepTimer.unref === "function") sweepTimer.unref();
|
|
353
|
+
const close = () => {
|
|
354
|
+
clearInterval(sweepTimer);
|
|
355
|
+
};
|
|
356
|
+
const pendingSampling = /* @__PURE__ */ new Map();
|
|
357
|
+
const SAMPLING_TIMEOUT_MS = 45e3;
|
|
358
|
+
const samplingTextOf = (result) => {
|
|
359
|
+
if (!result) return "";
|
|
360
|
+
const c = result.content;
|
|
361
|
+
if (typeof c === "string") return c;
|
|
362
|
+
if (Array.isArray(c)) return c.map((b) => b && b.text || "").join("\n").trim();
|
|
363
|
+
if (c && typeof c === "object") return c.text || "";
|
|
364
|
+
return JSON.stringify(result).slice(0, 300);
|
|
365
|
+
};
|
|
366
|
+
const pushToAgents = (to, text, kind = "user_message", payload = {}, ref = null) => {
|
|
367
|
+
const targets = resolvePushTargets(to);
|
|
368
|
+
if (!targets.length) return { ok: false, error: "无已注册 agent 会话(agent 连桥并注册后才有收件箱)" };
|
|
369
|
+
const delivered = [];
|
|
370
|
+
for (const t of targets) {
|
|
371
|
+
const { a } = ensureAgent(t.split("/")[0], t.split("/").slice(1).join("/"), null);
|
|
372
|
+
const srcLabel = a && a.source === "extension" ? "aiti-*" : DISPLAY_NAME + " MCP";
|
|
373
|
+
let srcPageId = payload && payload.pageId || null;
|
|
374
|
+
let srcUrl = payload && payload.url || null;
|
|
375
|
+
let srcTitle = payload && payload.title || null;
|
|
376
|
+
if (!srcPageId) {
|
|
377
|
+
const bind = a && a.boundPageId ? resolvePageId(a.boundPageId) : null;
|
|
378
|
+
const rows = listSessions().filter((r) => r.active && r.pageId);
|
|
379
|
+
const pick = bind && bind.pageId && rows.find((r) => r.pageId === bind.pageId) || (rows.length === 1 ? rows[0] : null);
|
|
380
|
+
if (pick) {
|
|
381
|
+
srcPageId = pick.pageId;
|
|
382
|
+
srcUrl = pick.url;
|
|
383
|
+
srcTitle = pick.title || null;
|
|
384
|
+
}
|
|
385
|
+
} else if (!srcUrl) {
|
|
386
|
+
const row = listSessions().find((r) => r.pageId === srcPageId);
|
|
387
|
+
if (row) {
|
|
388
|
+
srcUrl = row.url || null;
|
|
389
|
+
srcTitle = srcTitle || row.title || null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const clipMid = (s, max) => {
|
|
393
|
+
s = String(s || "");
|
|
394
|
+
if (s.length <= max) return s;
|
|
395
|
+
const half = Math.max(8, Math.floor((max - 1) / 2));
|
|
396
|
+
return s.slice(0, half) + "…" + s.slice(s.length - half);
|
|
397
|
+
};
|
|
398
|
+
let tText = String(text);
|
|
399
|
+
if (srcPageId && tText.indexOf("[发自: ") < 0) {
|
|
400
|
+
const parts = [srcPageId];
|
|
401
|
+
if (srcUrl) parts.push(clipMid(srcUrl, 64));
|
|
402
|
+
if (srcTitle) parts.push(clipMid(srcTitle, 40));
|
|
403
|
+
tText = tText + "\n[发自: " + parts.join(" · ") + "(" + srcLabel + ":pageId 参数用它)]";
|
|
404
|
+
}
|
|
405
|
+
payload = { ...payload || {}, source: a && a.source || null, sourceLabel: srcLabel, pageId: srcPageId, url: srcUrl, title: srcTitle };
|
|
406
|
+
const canSample = !!(a && a.stream && a.capabilities && a.capabilities.sampling && (kind === "user_message" || kind === "chat"));
|
|
407
|
+
if (canSample) {
|
|
408
|
+
const rid = "smp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 6);
|
|
409
|
+
const done = (ok, result) => {
|
|
410
|
+
if (!pendingSampling.delete(rid)) return;
|
|
411
|
+
const answer2 = ok ? samplingTextOf(result) : result && result.error || "sampling 失败";
|
|
412
|
+
console.log(TAG + " sampling 作答 → agent=" + t + " · " + String(answer2).slice(0, 80));
|
|
413
|
+
for (const w of aiConns) w.write({ type: "agent_answer", agentKey: t, agentId: a.agentId, ok, text: answer2, requestId: rid, at: Date.now() });
|
|
414
|
+
};
|
|
415
|
+
pendingSampling.set(rid, { agentKey: t, done, at: Date.now() });
|
|
416
|
+
a.stream.write({
|
|
417
|
+
jsonrpc: "2.0",
|
|
418
|
+
id: rid,
|
|
419
|
+
method: "sampling/createMessage",
|
|
420
|
+
params: {
|
|
421
|
+
messages: [{ role: "user", content: { type: "text", text: tText } }],
|
|
422
|
+
systemPrompt: "(调试现场的人通过 " + DISPLAY_NAME + " 发来的消息。目标页 pageId=" + (payload.pageId || "?") + "。直接回答即可,简短。",
|
|
423
|
+
maxTokens: 1024,
|
|
424
|
+
temperature: 0.3
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
setTimeout(() => done(false, { error: "sampling 超时(agent 未响应或需审批确认)" }), SAMPLING_TIMEOUT_MS);
|
|
428
|
+
delivered.push({ agentKey: t, agentId: a.agentId, sessionId: a.sessionId, status: "sampling" });
|
|
429
|
+
} else {
|
|
430
|
+
const q = queueMsg(t, { kind, from: "window", text: tText, payload, pageId: payload && payload.pageId || null, source: payload.source, sourceLabel: payload.sourceLabel, ref });
|
|
431
|
+
if (typeof a.inProcessSink === "function") {
|
|
432
|
+
try {
|
|
433
|
+
a.inProcessSink(tText, kind, payload);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
console.error(TAG + " inProcessSink 失败", e);
|
|
436
|
+
}
|
|
437
|
+
const m0 = q && q.msg;
|
|
438
|
+
if (m0 && m0.status === "pending") {
|
|
439
|
+
m0.status = "delivered";
|
|
440
|
+
appendMsgEvent(t, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: m0.id, status: "delivered", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
441
|
+
for (const w of aiConns) w.write({ type: "msg_status", agentKey: t, msgId: m0.id, status: "delivered" });
|
|
442
|
+
}
|
|
443
|
+
delivered.push({ agentKey: t, agentId: a.agentId, sessionId: a.sessionId, status: "inprocess" });
|
|
444
|
+
} else if (a.agentStream && !pushDeduped(t, "push:" + kind + tText)) {
|
|
445
|
+
a.agentStream.write({ type: "agent_push", kind, text: tText, payload, at: Date.now() });
|
|
446
|
+
const m = q && q.msg;
|
|
447
|
+
if (m && m.status === "pending") {
|
|
448
|
+
m.status = "delivered";
|
|
449
|
+
appendMsgEvent(t, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: m.id, status: "delivered", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
450
|
+
for (const w of aiConns) w.write({ type: "msg_status", agentKey: t, msgId: m.id, status: "delivered" });
|
|
451
|
+
}
|
|
452
|
+
delivered.push({ agentKey: t, agentId: a.agentId, sessionId: a.sessionId, status: "stream+delivered" });
|
|
453
|
+
} else {
|
|
454
|
+
delivered.push({ agentKey: t, agentId: a.agentId, sessionId: a.sessionId, status: "queue" });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const w of aiConns) w.write({ type: "event", from: "bridge", event: "inbox_status", payload: { agentKeys: targets } });
|
|
459
|
+
return { ok: true, delivered, to: targets };
|
|
460
|
+
};
|
|
461
|
+
const deliverUserAnswer = (agentKey2, result) => {
|
|
462
|
+
if (!agentKey2 || !result) return;
|
|
463
|
+
const text = JSON.stringify(result).slice(0, 500);
|
|
464
|
+
const a = agents.get(agentKey2);
|
|
465
|
+
if (a && a.agentStream) a.agentStream.write({ type: "agent_push", kind: "user_answer", text, payload: result, at: Date.now() });
|
|
466
|
+
if (a && typeof a.inProcessSink === "function") {
|
|
467
|
+
try {
|
|
468
|
+
a.inProcessSink(text, "user_answer", result);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.error(TAG + " inProcessSink 失败", e);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
queueMsg(agentKey2, { kind: "user_answer", from: "page", text, payload: result });
|
|
474
|
+
};
|
|
475
|
+
const formatPageEvent = (eventType, payload, pageInfo) => {
|
|
476
|
+
const where = pageInfo && pageInfo.url ? pageInfo.pageId ? `[${pageInfo.pageId} · ${pageInfo.url}]` : `[${pageInfo.url}]` : "";
|
|
477
|
+
if (eventType === "page_error") {
|
|
478
|
+
const pl = payload || {};
|
|
479
|
+
return `⚠ 页面报错${where} ${pl.kind || ""} ${String(pl.message || "").slice(0, 300)}` + (pl.stack ? `
|
|
480
|
+
${String(pl.stack).slice(0, 400)}` : "");
|
|
481
|
+
}
|
|
482
|
+
if (eventType === "page_log") {
|
|
483
|
+
const items = ((payload || {}).logs || (payload || {}).batch || []).slice(0, 5);
|
|
484
|
+
return `📋 页面日志${where}: ` + items.map((l) => `[${l.level}] ${String(l.text || "").slice(0, 120)}`).join(" | ");
|
|
485
|
+
}
|
|
486
|
+
if (eventType === "page_network") {
|
|
487
|
+
const items = ((payload || {}).requests || (payload || {}).batch || []).slice(0, 5);
|
|
488
|
+
return `🌐 页面网络${where}: ` + items.map((r) => `${r.method || "?"} ${String(r.url || "").slice(0, 80)} → ${r.status ?? "?"}${r.error ? " ERR" : ""}`).join(" | ");
|
|
489
|
+
}
|
|
490
|
+
return `📢 页面事件${where}: ${JSON.stringify(payload).slice(0, 200)}`;
|
|
491
|
+
};
|
|
492
|
+
const pushEventToSubscribers = (eventType, payload, pageInfo) => {
|
|
493
|
+
const sigBase = eventType + ":" + JSON.stringify(payload).slice(0, 120);
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
for (const [key, a] of agents) {
|
|
496
|
+
if (!a.subEvents || !a.subEvents.has(eventType)) continue;
|
|
497
|
+
if (!agentOnline(a) && now - (a.offlineAt || a.lastSeen || 0) > BROADCAST_TTL) continue;
|
|
498
|
+
if (pushDeduped(key, sigBase)) {
|
|
499
|
+
console.log(TAG + " push 去重(5s 内同事件)" + key + " " + eventType);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const text = formatPageEvent(eventType, payload, pageInfo);
|
|
503
|
+
if (a.agentStream) a.agentStream.write({ type: "page_event", event: eventType, text, pageId: pageInfo && pageInfo.pageId || null, url: pageInfo && pageInfo.url || null, at: Date.now() });
|
|
504
|
+
else {
|
|
505
|
+
queueMsg(key, { kind: "page_" + eventType, from: "page", text, payload });
|
|
506
|
+
console.log(TAG + " push 无流 → outbox " + key + " " + eventType);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const json = (body, status = 200, extraHeaders = {}) => new Response(JSON.stringify(body), {
|
|
511
|
+
status,
|
|
512
|
+
headers: { "content-type": "application/json", "cache-control": "no-store", ...cors, ...extraHeaders }
|
|
513
|
+
});
|
|
514
|
+
const cors = { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,OPTIONS", "access-control-allow-headers": "Content-Type" };
|
|
515
|
+
const pickedReports = /* @__PURE__ */ new Map();
|
|
516
|
+
const pickedFile = dataDir ? join(dataDir, "picked.json") : "";
|
|
517
|
+
try {
|
|
518
|
+
if (pickedFile && existsSync(pickedFile)) {
|
|
519
|
+
const saved = JSON.parse(readFileSync(pickedFile, "utf8"));
|
|
520
|
+
if (saved && typeof saved === "object") {
|
|
521
|
+
for (const [k, v] of Object.entries(saved)) if (Array.isArray(v)) pickedReports.set(k, v);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
const persistPicked = () => {
|
|
527
|
+
if (!pickedFile) return;
|
|
528
|
+
try {
|
|
529
|
+
mkdirSync(dirname(pickedFile), { recursive: true });
|
|
530
|
+
const tmp = pickedFile + ".tmp";
|
|
531
|
+
writeFileSync(tmp, JSON.stringify(Object.fromEntries(pickedReports)));
|
|
532
|
+
renameSync(tmp, pickedFile);
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const isExpired = (rec) => Date.now() - rec.lastSeen > ID_TTL;
|
|
537
|
+
function randomInstanceId() {
|
|
538
|
+
const letters = "abcdefghijklmnopqrstuvwxyz";
|
|
539
|
+
return letters[randomInt(26)] + letters[randomInt(26)] + String(randomInt(100, 1e3));
|
|
540
|
+
}
|
|
541
|
+
function genSecret() {
|
|
542
|
+
return randomBytes(12).toString("hex");
|
|
543
|
+
}
|
|
544
|
+
function portFromHostUrl(hostUrl) {
|
|
545
|
+
if (!hostUrl || typeof hostUrl !== "string") return null;
|
|
546
|
+
try {
|
|
547
|
+
const u = new URL(hostUrl);
|
|
548
|
+
if (u.port) {
|
|
549
|
+
const n = parseInt(u.port, 10);
|
|
550
|
+
if (n > 0 && n <= 65535) return n;
|
|
551
|
+
}
|
|
552
|
+
const host = (u.hostname || "").toLowerCase();
|
|
553
|
+
const m = host.match(/^(\d{1,5})(?:-port)?[-.]/);
|
|
554
|
+
if (m) {
|
|
555
|
+
const n = parseInt(m[1], 10);
|
|
556
|
+
if (n > 0 && n <= 65535) return n;
|
|
557
|
+
}
|
|
558
|
+
const m2 = host.match(/-?(\d{1,5})-port(?=\.|$)/);
|
|
559
|
+
if (m2) {
|
|
560
|
+
const n = parseInt(m2[1], 10);
|
|
561
|
+
if (n > 0 && n <= 65535) return n;
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const allocInstanceId = (preferred) => {
|
|
568
|
+
const free = (id) => !pages.has(id) && !idRegistry.has(id);
|
|
569
|
+
if (preferred && free(String(preferred))) return String(preferred);
|
|
570
|
+
for (let i = 0; i < 64; i++) {
|
|
571
|
+
const n = randomInstanceId();
|
|
572
|
+
if (free(n)) return n;
|
|
573
|
+
}
|
|
574
|
+
return randomInstanceId();
|
|
575
|
+
};
|
|
576
|
+
function claimInstanceId(preferred, secret, pageKey, hostUrl) {
|
|
577
|
+
const now = Date.now();
|
|
578
|
+
for (const [id2, rec] of idRegistry) {
|
|
579
|
+
if (!pages.has(id2) && now - rec.lastSeen > ID_TTL) idRegistry.delete(id2);
|
|
580
|
+
}
|
|
581
|
+
if (preferred && idRegistry.has(preferred) && !pages.has(preferred) && !isExpired(idRegistry.get(preferred))) {
|
|
582
|
+
const rec = idRegistry.get(preferred);
|
|
583
|
+
if (secret && rec.secret && secret === rec.secret) {
|
|
584
|
+
rec.lastSeen = now;
|
|
585
|
+
return { instanceId: preferred, secret: rec.secret };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const id = allocInstanceId(portFromHostUrl(hostUrl));
|
|
589
|
+
const sec = genSecret();
|
|
590
|
+
idRegistry.set(id, { pageKey: pageKey || null, secret: sec, lastSeen: now });
|
|
591
|
+
return { instanceId: id, secret: sec };
|
|
592
|
+
}
|
|
593
|
+
function checkSecret(instanceId, secret) {
|
|
594
|
+
if (!secret) return true;
|
|
595
|
+
const rec = idRegistry.get(instanceId);
|
|
596
|
+
return !!(rec && rec.secret && rec.secret === secret);
|
|
597
|
+
}
|
|
598
|
+
function parsePageId(pageId) {
|
|
599
|
+
if (typeof pageId !== "string") return { instanceId: LEGACY_TOKEN, frameId: null };
|
|
600
|
+
const i = pageId.lastIndexOf("/");
|
|
601
|
+
if (i < 0) return { instanceId: pageId, frameId: null };
|
|
602
|
+
return { instanceId: pageId.slice(0, i), frameId: pageId.slice(i + 1) };
|
|
603
|
+
}
|
|
604
|
+
const validInstance = (id) => id === LEGACY_TOKEN || idRegistry.has(id);
|
|
605
|
+
function resolvePageId(input) {
|
|
606
|
+
if (typeof input !== "string" || !input) return { notFound: true };
|
|
607
|
+
const rows = listSessions().filter((r) => r.active);
|
|
608
|
+
if (rows.some((r) => r.pageId === input)) return { pageId: input };
|
|
609
|
+
const hit = /* @__PURE__ */ new Map();
|
|
610
|
+
for (const r of rows) {
|
|
611
|
+
const u = r.url || "", t = r.title || "";
|
|
612
|
+
if (u && (u === input || u.includes(input)) || t && (t === input || t.includes(input))) hit.set(r.pageId, true);
|
|
613
|
+
}
|
|
614
|
+
if (hit.size === 1) return { pageId: [...hit.keys()][0] };
|
|
615
|
+
if (hit.size > 1) return { multiple: [...hit.keys()] };
|
|
616
|
+
return { notFound: true };
|
|
617
|
+
}
|
|
618
|
+
const withDevice = (row, dev, label) => {
|
|
619
|
+
if (dev && typeof dev === "object") {
|
|
620
|
+
row.device = dev;
|
|
621
|
+
row.deviceLabel = label || [dev.browser || "?", dev.os || "", dev.screen || ""].filter(Boolean).join(" · ");
|
|
622
|
+
}
|
|
623
|
+
return row;
|
|
624
|
+
};
|
|
625
|
+
function listSessions(boundPageId) {
|
|
626
|
+
const out = [];
|
|
627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
628
|
+
const addMine = (row) => {
|
|
629
|
+
row.mine = !!(boundPageId && row.pageId === boundPageId);
|
|
630
|
+
};
|
|
631
|
+
for (const [instanceId, page] of pages) {
|
|
632
|
+
for (const frameId of page.frames) {
|
|
633
|
+
const fi = page.frameInfo && page.frameInfo.get(frameId) || null;
|
|
634
|
+
const isHost = frameId === "host";
|
|
635
|
+
const fUrl = fi && fi.url || (isHost ? page.url : null);
|
|
636
|
+
const fTitle = fi && fi.title || (isHost ? page.title : null);
|
|
637
|
+
const row = {
|
|
638
|
+
instanceId,
|
|
639
|
+
frameId,
|
|
640
|
+
pageId: instanceId + "/" + frameId,
|
|
641
|
+
url: page.url,
|
|
642
|
+
title: page.title || null,
|
|
643
|
+
frameworks: page.frameworks,
|
|
644
|
+
active: true,
|
|
645
|
+
lastSeen: Date.now(),
|
|
646
|
+
frameUrl: fUrl || null,
|
|
647
|
+
frameTitle: fTitle || null,
|
|
648
|
+
frameName: fi && fi.name || "",
|
|
649
|
+
frameSrc: fi && fi.src || "",
|
|
650
|
+
frameParent: isHost ? null : fi && fi.parentId || null,
|
|
651
|
+
root: isHost || !(fi && fi.parentId)
|
|
652
|
+
};
|
|
653
|
+
withDevice(row, page.device, page.deviceLabel);
|
|
654
|
+
seen.add(row.pageId);
|
|
655
|
+
addMine(row);
|
|
656
|
+
out.push(row);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
for (const [instanceId, rec] of idRegistry) {
|
|
660
|
+
if (!pages.has(instanceId) && !isExpired(rec)) {
|
|
661
|
+
const row = { instanceId, frameId: null, pageId: null, url: null, frameworks: null, active: false, lastSeen: rec.lastSeen, pageKey: rec.pageKey };
|
|
662
|
+
addMine(row);
|
|
663
|
+
out.push(row);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const aiByBase = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const [w, rec] of aiPageReg) {
|
|
668
|
+
if (!rec.pageId) continue;
|
|
669
|
+
const baseId = String(rec.pageId).replace(/\/host$/, "");
|
|
670
|
+
const prev = aiByBase.get(baseId);
|
|
671
|
+
const prevScore = (prev && prev.frames && prev.frames.length ? prev.frames.length : 0) * 100 + (prev ? prev.lastSeen : 0);
|
|
672
|
+
const recScore = (rec.frames && rec.frames.length ? rec.frames.length : 0) * 100 + rec.lastSeen;
|
|
673
|
+
if (!prev || recScore > prevScore) aiByBase.set(baseId, rec);
|
|
674
|
+
}
|
|
675
|
+
for (const [baseId, rec] of aiByBase) {
|
|
676
|
+
const fr = rec.frames || null;
|
|
677
|
+
if (!fr || !fr.length) {
|
|
678
|
+
if (seen.has(rec.pageId)) continue;
|
|
679
|
+
seen.add(rec.pageId);
|
|
680
|
+
const aiRow1 = { instanceId: rec.instanceId, frameId: null, pageId: rec.pageId, url: rec.url || null, title: rec.title || null, frameworks: null, active: true, viaAI: true, lastSeen: rec.lastSeen };
|
|
681
|
+
withDevice(aiRow1, rec.device, rec.deviceLabel);
|
|
682
|
+
addMine(aiRow1);
|
|
683
|
+
out.push(aiRow1);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
for (const f of fr) {
|
|
687
|
+
const fid = String(f.id || "host");
|
|
688
|
+
const isHost = fid === "host";
|
|
689
|
+
const rowPageId = baseId + "/" + fid;
|
|
690
|
+
if (seen.has(rowPageId)) continue;
|
|
691
|
+
seen.add(rowPageId);
|
|
692
|
+
const frRow = {
|
|
693
|
+
instanceId: rec.instanceId,
|
|
694
|
+
frameId: fid,
|
|
695
|
+
pageId: rowPageId,
|
|
696
|
+
url: rec.url || null,
|
|
697
|
+
title: rec.title || null,
|
|
698
|
+
frameworks: null,
|
|
699
|
+
active: true,
|
|
700
|
+
viaAI: true,
|
|
701
|
+
lastSeen: rec.lastSeen,
|
|
702
|
+
frameUrl: f && f.url || (isHost ? rec.url : null) || null,
|
|
703
|
+
frameTitle: f && f.title || (isHost ? rec.title : null) || null,
|
|
704
|
+
frameName: f && f.name || "",
|
|
705
|
+
frameSrc: f && f.src || "",
|
|
706
|
+
frameParent: isHost ? null : f && f.parentId || null,
|
|
707
|
+
root: isHost || !(f && f.parentId)
|
|
708
|
+
};
|
|
709
|
+
withDevice(frRow, rec.device, rec.deviceLabel);
|
|
710
|
+
addMine(frRow);
|
|
711
|
+
out.push(frRow);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const [w, rec] of aiSelfReg) {
|
|
715
|
+
const selfPageId = String(rec.instanceId) + "/ai";
|
|
716
|
+
if (seen.has(selfPageId)) continue;
|
|
717
|
+
seen.add(selfPageId);
|
|
718
|
+
const selfRow = { instanceId: rec.instanceId, frameId: "ai", pageId: selfPageId, url: rec.url || null, title: rec.title ? rec.title + " · AI 主窗口" : "AI 主窗口", frameworks: null, active: true, viaAI: true, self: true, lastSeen: rec.lastSeen };
|
|
719
|
+
withDevice(selfRow, rec.device, rec.deviceLabel);
|
|
720
|
+
addMine(selfRow);
|
|
721
|
+
out.push(selfRow);
|
|
722
|
+
}
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
const TOOLS = [
|
|
726
|
+
{
|
|
727
|
+
name: "page_exec",
|
|
728
|
+
description: "在已连接页面执行类 CDP 命令(DOM/CSS/Input/Page/Runtime/Storage/Performance/Browser 域)。pageId 缺省=最新 AI 主窗口(self:true);显式 pageId 优先。危险命令/浏览器受限操作会自动弹用户确认;Page.captureScreenshot 截屏会先弹准备卡展示截取意图(args.reason,请务必用一句话说明想截什么),用户调整好页面才开始截屏,截完再弹预览确认(大图自动落盘返回文件路径);确认卡倒计时可用 args.timeout(ms)调整(默认 280000,服务端安全网自动同步 +30s 余量)。调试会话中从被调试页面(inject)发出的消息统一带 [发自: <pageId> · <url>] 来源标注——把 pageId 值传给 pageId 参数即操作该页面。\n\niframe 支持:Page.getFrameTree 查看 frame 树(iframe src/name/title,同域含 url/readyState);Runtime.evaluate 传 contextId(iframe 索引从 1 起,同域可 eval)进 iframe 上下文执行。",
|
|
729
|
+
inputSchema: { type: "object", properties: {
|
|
730
|
+
cmd: { type: "string" },
|
|
731
|
+
args: { type: "object" },
|
|
732
|
+
pageId: { type: "string", description: "目标页面完整 ID——来自 AI 消息的 [发自: xxx](如 3001/host);也可 /bridge/sessions 查询" }
|
|
733
|
+
}, required: ["cmd"] }
|
|
734
|
+
},
|
|
735
|
+
{ name: "page_snapshot", description: "拉取已连接页面基础现场(URL/选区/模拟条件)。pageId 缺省=最新 AI 主窗口(self:true);显式 pageId 优先。", inputSchema: { type: "object", properties: { pageId: { type: "string" } } } },
|
|
736
|
+
{ name: "self_ui", description: "扫描目标页面(缺省 AI 主窗口 xxx/ai)当前打开的弹窗/dialog/表单字段/按钮清单——ai-helper 回填定位用。返回 {dialogs:[{title, fields:[{label,selector,type}], buttons}]},无弹窗时 dialogs 为空数组。", inputSchema: { type: "object", properties: { pageId: { type: "string", description: "缺省用 AI 主窗口(xxx/ai);也可指定其他页面" } } } },
|
|
737
|
+
{ name: "form_fill", description: "在目标页面(缺省 AI 主窗口 xxx/ai)的弹窗/表单里回填字段值——AI 结果直接操作扩展弹窗表单(React 兼容:原生 value setter + dispatch input/change)。配合 self_ui 使用:先 self_ui 拿字段 label/selector,再 form_fill 填值。参数:dialog(弹窗标识:标题子串或序号 0 起,缺省取面积最大的可见弹窗)、fields[{selector|label, value}]、button(可选,回填后点击的按钮文本如『保存』)。返回 {foundDialog, title, filled, missed, clicked}。", inputSchema: { type: "object", properties: {
|
|
738
|
+
pageId: { type: "string", description: "缺省用 AI 主窗口(xxx/ai);也可指定其他页面" },
|
|
739
|
+
dialog: { type: "string", description: "弹窗标识:标题子串或序号(0 起);缺省取面积最大的可见弹窗" },
|
|
740
|
+
fields: { type: "array", items: { type: "object", properties: { selector: { type: "string", description: "CSS 选择器(优先)" }, label: { type: "string", description: "弹窗内文本匹配(label[for]/包裹/文本包含)" }, value: { type: "string", description: "要填的值" } }, required: ["value"] }, description: "要填的字段列表" },
|
|
741
|
+
button: { type: "string", description: "回填后点击的按钮文本(如『保存』『确定』)" }
|
|
742
|
+
} } },
|
|
743
|
+
{ name: "bridge_status", description: "查询已连接页面(sessions:含 pageId/url/title/frameworks + device/deviceLabel——浏览器·OS·屏幕·dpr·在线·语言·时区等设备信息,便于区分多台机器/标签页)与挂起请求。用户提到页面标题或 URL 时,按 title/url 匹配 pageId(如 refresh『某标题页面』或『某个 url 页面』)。", inputSchema: { type: "object", properties: {} } },
|
|
744
|
+
{
|
|
745
|
+
name: "page_logs",
|
|
746
|
+
description: "从目标页面拉取本地缓冲的 console 日志(log/debug/info/warn/error 全量,CDP Runtime.evaluate 读取 window.__dbg.__consoleRing)。pageId 缺省=最新 AI 主窗口(self:true);显式 pageId 优先。页面报错排查首选:先查 warn/error 再逐步放开。返回 {logs:[{ts,level,text,count}], bufferSize}。",
|
|
747
|
+
inputSchema: { type: "object", properties: {
|
|
748
|
+
pageId: { type: "string", description: "目标页面完整 ID([发自: xxx] 或 /bridge/sessions)" },
|
|
749
|
+
level: { type: "string", description: "过滤级别:debug/log/info/warn/error(默认全部)" },
|
|
750
|
+
since: { type: "string", description: "ISO 时间:只返回此时间之后的日志" },
|
|
751
|
+
limit: { type: "number", description: "条数上限(≤200,默认 50,取最新)" },
|
|
752
|
+
token: { type: "string" }
|
|
753
|
+
} }
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "page_network",
|
|
757
|
+
description: "从目标页面拉取本地缓冲的 ajax/fetch 请求完成态(CDP Runtime.evaluate 读取 window.__dbg.__netRing)。pageId 缺省=最新 AI 主窗口(self:true);显式 pageId 优先。失败排查:status 传 400 或 errorOnly:true。注意:不含静态资源加载(那是 CDP Network 的能力)。返回 {requests:[{kind,method,url,status,statusText,durationMs,size,error,ts}], bufferSize}。",
|
|
758
|
+
inputSchema: { type: "object", properties: {
|
|
759
|
+
pageId: { type: "string" },
|
|
760
|
+
url: { type: "string", description: "URL 子串过滤" },
|
|
761
|
+
status: { type: "number", description: "status >= 此值才返回(如 400 只看失败)" },
|
|
762
|
+
errorOnly: { type: "boolean", description: "只看失败(error 非空 或 status>=400)" },
|
|
763
|
+
limit: { type: "number", description: "条数上限(≤100,默认 30,取最新)" },
|
|
764
|
+
token: { type: "string" }
|
|
765
|
+
} }
|
|
766
|
+
},
|
|
767
|
+
// ---- 用户交互型工具:AI 与人在页面上协作(AI 提问 → 人在 AI 窗口/页面作答或拒绝 → 结果回 AI) ----
|
|
768
|
+
// 统一机制:prompt 显示给人看;timeout 为可选——不传=永久等待(面板常驻直到用户操作,不读秒不超时);
|
|
769
|
+
// 传了才显示读秒倒计时(可暂停)且服务器强制到点返回“用户无响应/不在线”;
|
|
770
|
+
// 交互消息到达时页面走浏览器 Notification(+标题闪烁/提示音)通知用户;人可拒绝(rejectable)。
|
|
771
|
+
// 响应信封:{ status:'answered'|'rejected'|'timeout'|'error', value?, reason?, error?, tookMs, timeoutMs }
|
|
772
|
+
{
|
|
773
|
+
name: "user_pick_element",
|
|
774
|
+
description: "请当前页面的人类用户点选一个页面元素(自动进入 Inspector 点选模式、收起 AI 窗口、弹点选面板;用户可确认选中或拒绝)。返回元素载荷 {tag,rect,xpath,text,attributes,framework,selector}。",
|
|
775
|
+
inputSchema: { type: "object", properties: {
|
|
776
|
+
prompt: { type: "string", description: "给用户看的说明(为什么需要点选这个元素)" },
|
|
777
|
+
timeout: { type: "number", description: "可选超时 ms(5000-600000)。不传=永久等待:面板常驻直到用户操作,无读秒;传了才显示读秒倒计时(可暂停),到点自动返回 timeout" },
|
|
778
|
+
rejectable: { type: "boolean", description: "是否允许用户拒绝(默认 true)" },
|
|
779
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送;缺省=同步等待" },
|
|
780
|
+
pageId: { type: "string", description: "目标页面完整 ID(instanceId/frameId)" }
|
|
781
|
+
}, required: ["prompt"] }
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "user_confirm",
|
|
785
|
+
description: "在 AI 窗口内向人类用户提一个是/否问题(如“确认执行此操作?”“是否继续?”),用户点 ✅/❌ 作答,并可附「补充说明」自由文本(value.comment);另有固定选项「都不适合」→ 自定义回答(approved:null, custom)。返回 {status, approved, comment?, custom?, reason?}。",
|
|
786
|
+
inputSchema: { type: "object", properties: {
|
|
787
|
+
prompt: { type: "string", description: "问题内容" },
|
|
788
|
+
timeout: { type: "number", description: "可选超时 ms(5000-600000)。不传=永久等待:面板常驻直到用户操作,无读秒;传了才显示读秒倒计时(可暂停),到点自动返回 timeout" },
|
|
789
|
+
rejectable: { type: "boolean", description: "是否允许用户以“暂时无法处理”拒绝(默认 true)" },
|
|
790
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送(不阻塞本调用);缺省=同步等待" },
|
|
791
|
+
pageId: { type: "string" }
|
|
792
|
+
}, required: ["prompt"] }
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "user_choose",
|
|
796
|
+
description: "在 AI 窗口内给人类用户一组选项让其选择(如修复方案 A/B/C)。单选(默认)→ value:{index,label};multiple:true 多选 → value:{indexes,labels}。用户可附「补充说明」自由文本(value.comment);另有固定选项「这些都不适合」→ 自定义答案(index:-1, custom:true)。返回 {status, value, reason?}。",
|
|
797
|
+
inputSchema: { type: "object", properties: {
|
|
798
|
+
prompt: { type: "string" },
|
|
799
|
+
choices: { type: "array", items: { type: "string" }, description: "选项列表(2-8 个)" },
|
|
800
|
+
multiple: { type: "boolean", description: "是否允许多选(默认 false;true 时返回 indexes/labels)" },
|
|
801
|
+
timeout: { type: "number", description: "可选超时 ms(5000-600000)。不传=永久等待:面板常驻直到用户操作,无读秒;传了才显示读秒倒计时(可暂停),到点自动返回 timeout" },
|
|
802
|
+
rejectable: { type: "boolean", description: "是否允许用户拒绝(默认 true)" },
|
|
803
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送;缺省=同步等待" },
|
|
804
|
+
pageId: { type: "string" }
|
|
805
|
+
}, required: ["prompt", "choices"] }
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: "user_input",
|
|
809
|
+
description: "在 AI 窗口内请人类用户输入一段文本(如期望文案、配置值、场景描述),回车/点击即发送。返回 {status, value:{text}, reason?}。",
|
|
810
|
+
inputSchema: { type: "object", properties: {
|
|
811
|
+
prompt: { type: "string" },
|
|
812
|
+
defaultValue: { type: "string", description: "预填默认值(可选)" },
|
|
813
|
+
timeout: { type: "number", description: "可选超时 ms(5000-600000)。不传=永久等待:面板常驻直到用户操作,无读秒;传了才显示读秒倒计时(可暂停),到点自动返回 timeout" },
|
|
814
|
+
rejectable: { type: "boolean", description: "是否允许用户拒绝(默认 true)" },
|
|
815
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送;缺省=同步等待" },
|
|
816
|
+
pageId: { type: "string" }
|
|
817
|
+
}, required: ["prompt"] }
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: "user_presence",
|
|
821
|
+
description: "探测人类用户是否在场/在线(AI 需要用户配合长任务前先确认),用户点「我在/稍后」,可附「补充说明」(value.comment)。返回 {status, present, comment?};超时即视为不在线。",
|
|
822
|
+
inputSchema: { type: "object", properties: {
|
|
823
|
+
prompt: { type: "string", description: "询问文案(默认“你在吗?需要你配合继续开发”)" },
|
|
824
|
+
timeout: { type: "number", description: "可选超时 ms(5000-600000)。不传=永久等待:面板常驻直到用户作答;传了才显示读秒倒计时(可暂停),到点返回 status:'timeout'(视为不在线)——需要快速探测在场/限时等待时才显式传" },
|
|
825
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送;缺省=同步等待" },
|
|
826
|
+
pageId: { type: "string" }
|
|
827
|
+
}, required: [] }
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "user_notify",
|
|
831
|
+
description: "非阻塞通知:通过浏览器 Notification 告诉用户一条消息(如“修复完成,请刷新验证”),不等待回答。返回 {notified:true}。",
|
|
832
|
+
inputSchema: { type: "object", properties: {
|
|
833
|
+
title: { type: "string", description: "通知标题" },
|
|
834
|
+
body: { type: "string", description: "通知内容" },
|
|
835
|
+
timeout: { type: "number", description: "可选:通知卡自动关闭时限 ms(5000-600000)。不传=常驻直到用户关闭" },
|
|
836
|
+
async: { type: "boolean", description: "true=异步:立即返回已异步发出,用户作答结果经 AI 消息窗发送;缺省=同步等待" },
|
|
837
|
+
pageId: { type: "string" }
|
|
838
|
+
}, required: ["body"] }
|
|
839
|
+
},
|
|
840
|
+
// ---- 方案 G · P2:agent 接入 + 消息主通道 ----
|
|
841
|
+
{
|
|
842
|
+
name: "aiti_hello",
|
|
843
|
+
description: "agent 自我介绍/注册:覆盖 agentId(默认 clientInfo.name)与 sessionId(默认 Mcp-Session-Id 或桥随机)、绑定默认目标页 pageId、订阅页面事件(page_error)。注意:page_log/page_network 不再实时推送,改用 page_logs/page_network 工具按需拉取。建议会话开始时调用一次——注册后消息可定向到本会话(agentKey=agentId/sessionId)。",
|
|
844
|
+
inputSchema: { type: "object", properties: {
|
|
845
|
+
agentId: { type: "string", description: "自定义 agent 标识(如 pi / cc / qoder;缺省用 clientInfo.name)" },
|
|
846
|
+
sessionId: { type: "string", description: "本实例会话标识(多实例/多会话时区分;缺省用 Mcp-Session-Id,无则桥生成随机)" },
|
|
847
|
+
name: { type: "string", description: "显示名(可选,如 'pi · 工作区A')" },
|
|
848
|
+
cwd: { type: "string", description: "工作目录(可选,显示 CWD . name (id),方便识别 agent 身份)" },
|
|
849
|
+
pageId: { type: "string", description: "绑定默认调试目标页(pageId/url/title 均可,留空不绑)" },
|
|
850
|
+
subEvents: { type: "array", items: { type: "string" }, description: "订阅实时事件:page_error(默认不投递,显式订阅才推;page_log/page_network 不再实时推送,改用 page_logs/page_network 工具按需拉取)" }
|
|
851
|
+
} }
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "file_stat",
|
|
855
|
+
description: "查询文件存在性/类型/大小/修改时间(绝对路径;组件源码/调用位置定位、AI 窗『后端打开/前端打开』前置校验用)。不存在或非法路径返回 {exists:false}。",
|
|
856
|
+
inputSchema: { type: "object", properties: {
|
|
857
|
+
path: { type: "string", description: "绝对路径(如 /home/user/src/App.vue)" }
|
|
858
|
+
}, required: ["path"] }
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
name: "aiti_inbox",
|
|
862
|
+
description: "消息主通道:拉取发给本 agent 会话的消息(人主动发话 user_message / 异步 user_* 作答 user_answer / 订阅的页面事件)。开始新一轮或需要最新信息时先调用;处理完把消息 id 放 ack 参数确认(标记 consumed,历史保留)。返回 agentKey(agentId/sessionId),未注册时提示调 aiti_hello 注册。",
|
|
863
|
+
inputSchema: { type: "object", properties: {
|
|
864
|
+
agentId: { type: "string", description: "agent 标识(缺省用 aiti_hello 注册值或 clientInfo.name)" },
|
|
865
|
+
sessionId: { type: "string", description: "本实例会话标识(缺省用 Mcp-Session-Id 或 aiti_hello 注册值)" },
|
|
866
|
+
ack: { description: "已处理消息 id 列表,或 { ids: [...], summary: '执行总结' }——summary(必带)会作为 agent 处理记录显示在操作台消息流", oneOf: [{ type: "array", items: { type: "string" } }, { type: "object", properties: { ids: { type: "array", items: { type: "string" } }, summary: { type: "string" } } }] }
|
|
867
|
+
} }
|
|
868
|
+
}
|
|
869
|
+
];
|
|
870
|
+
const settlePending = (requestId, ok, result) => {
|
|
871
|
+
const r = pendingRequests.get(requestId);
|
|
872
|
+
if (!r) return false;
|
|
873
|
+
pendingRequests.delete(requestId);
|
|
874
|
+
if (r.mcpReqId) mcpReqMap.delete(r.mcpReqId);
|
|
875
|
+
if (r.cancelId) cancelIdMap.delete(r.cancelId);
|
|
876
|
+
try {
|
|
877
|
+
if (r.signal && r.onAbort) r.signal.removeEventListener("abort", r.onAbort);
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
r.resolve({ ok, result });
|
|
881
|
+
return true;
|
|
882
|
+
};
|
|
883
|
+
const cancelPending = (requestId, reason) => {
|
|
884
|
+
const r = pendingRequests.get(requestId);
|
|
885
|
+
if (!r) return false;
|
|
886
|
+
const ist = interactionTimers.get(requestId);
|
|
887
|
+
if (ist) {
|
|
888
|
+
clearTimeout(ist.timer);
|
|
889
|
+
interactionTimers.delete(requestId);
|
|
890
|
+
}
|
|
891
|
+
const result = { status: "cancelled", error: reason, cancelled: true, tookMs: Date.now() - (r.t0 || Date.now()), ...ist ? { timeoutMs: ist.ms, pausedMs: ist.pausedMs } : {} };
|
|
892
|
+
settlePending(requestId, false, result);
|
|
893
|
+
for (const w of aiConns) w.write({ type: "result", requestId, ok: false, result });
|
|
894
|
+
console.log(TAG + " ✂ 请求已取消:" + requestId + " · " + reason);
|
|
895
|
+
return true;
|
|
896
|
+
};
|
|
897
|
+
const SNAPSHOT_FALLBACK = `(() => {
|
|
898
|
+
try {
|
|
899
|
+
var isVis = function (el) {
|
|
900
|
+
if (!el) return false;
|
|
901
|
+
if (el.getClientRects && el.getClientRects().length > 0) return true;
|
|
902
|
+
try { return el.offsetParent !== null; } catch (e) { return false; }
|
|
903
|
+
};
|
|
904
|
+
var label = function (el) {
|
|
905
|
+
return (el.innerText || el.value || el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name') || el.id || '').trim().slice(0, 60);
|
|
906
|
+
};
|
|
907
|
+
var pack = function (el) {
|
|
908
|
+
var r = el.getBoundingClientRect();
|
|
909
|
+
return { tag: el.tagName.toLowerCase(), text: label(el), id: el.id || undefined,
|
|
910
|
+
name: el.getAttribute('name') || undefined, type: el.getAttribute('type') || undefined,
|
|
911
|
+
href: (el.tagName === 'A' && el.getAttribute('href')) ? el.getAttribute('href').slice(0, 80) : undefined,
|
|
912
|
+
rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) } };
|
|
913
|
+
};
|
|
914
|
+
var els = [].slice.call(document.querySelectorAll('a,button,input,select,textarea,[role="button"],[role="tab"],[role="dialog"],[role="alertdialog"]')).filter(isVis).slice(0, 60);
|
|
915
|
+
return JSON.stringify({
|
|
916
|
+
url: location.href.slice(0, 300), title: document.title,
|
|
917
|
+
viewport: { w: window.innerWidth, h: window.innerHeight, scrollY: Math.round(window.scrollY) },
|
|
918
|
+
selected: String(window.getSelection ? window.getSelection().toString() : '').slice(0, 120) || undefined,
|
|
919
|
+
elements: els.map(pack),
|
|
920
|
+
textHead: (document.body && document.body.innerText ? document.body.innerText : '').replace(/\\s+/g, ' ').trim().slice(0, 600)
|
|
921
|
+
});
|
|
922
|
+
} catch (e) { return JSON.stringify({ error: 'snapshot fallback failed: ' + (e && e.message) }); }
|
|
923
|
+
})()`;
|
|
924
|
+
const SELF_UI_SCAN = `(() => {
|
|
925
|
+
try {
|
|
926
|
+
var out = { dialogs: [] };
|
|
927
|
+
var isVis = function (el) {
|
|
928
|
+
if (!el) return false;
|
|
929
|
+
if (el.getClientRects && el.getClientRects().length > 0) return true;
|
|
930
|
+
try { return el.offsetParent !== null; } catch (e) { return false; }
|
|
931
|
+
};
|
|
932
|
+
var hits = document.querySelectorAll('[role="dialog"], [role="alertdialog"], [data-dialog], [data-role="dialog"], .dialog, .modal, [class*="dialog"], [class*="modal"]');
|
|
933
|
+
var dialogs = [];
|
|
934
|
+
for (var i = 0; i < hits.length && dialogs.length < 8; i++) {
|
|
935
|
+
var d = hits[i];
|
|
936
|
+
if (!isVis(d)) continue;
|
|
937
|
+
if (d.closest && d.closest('#__dbg_launcher')) continue; // 排除 aiti 自身
|
|
938
|
+
var already = false;
|
|
939
|
+
for (var j = 0; j < dialogs.length; j++) if (dialogs[j].contains(d)) { already = true; break; }
|
|
940
|
+
if (already) continue;
|
|
941
|
+
if (d.parentElement && dialogs.indexOf(d.parentElement) >= 0) continue;
|
|
942
|
+
dialogs.push(d);
|
|
943
|
+
}
|
|
944
|
+
if (!dialogs.length) {
|
|
945
|
+
// 兜底:可见的 fixed/absolute 面板(宽度 > 40% 视口且 zIndex 高)
|
|
946
|
+
var all = document.querySelectorAll('div, section, aside');
|
|
947
|
+
for (var k = 0; k < all.length && dialogs.length < 4; k++) {
|
|
948
|
+
var e = all[k];
|
|
949
|
+
if (!isVis(e)) continue;
|
|
950
|
+
var st = getComputedStyle(e);
|
|
951
|
+
if (st.position !== 'fixed' && st.position !== 'absolute') continue;
|
|
952
|
+
var r = e.getBoundingClientRect();
|
|
953
|
+
if (r.width < window.innerWidth * 0.4 || r.height < 200) continue;
|
|
954
|
+
if (e.closest && e.closest('#__dbg_launcher')) continue;
|
|
955
|
+
dialogs.push(e);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
for (var m = 0; m < dialogs.length; m++) {
|
|
959
|
+
var dlg = dialogs[m];
|
|
960
|
+
var titleEl = dlg.querySelector('[class*="title"], h1, h2, h3, header, [role="heading"]');
|
|
961
|
+
var title = titleEl ? String((titleEl.innerText || titleEl.textContent || '').trim()).slice(0, 60) : '';
|
|
962
|
+
var fields = [];
|
|
963
|
+
var inputs = dlg.querySelectorAll('input, textarea, select, [contenteditable="true"], [contenteditable="plaintext-only"], [contenteditable=""]');
|
|
964
|
+
for (var n = 0; n < inputs.length && fields.length < 20; n++) {
|
|
965
|
+
var f = inputs[n];
|
|
966
|
+
if (f.type === 'hidden') continue;
|
|
967
|
+
var isCE = f.isContentEditable || (f.tagName !== 'INPUT' && f.tagName !== 'TEXTAREA' && f.tagName !== 'SELECT' && f.hasAttribute && f.hasAttribute('contenteditable'));
|
|
968
|
+
var label = '';
|
|
969
|
+
try { if (!isCE && f.labels && f.labels.length && f.labels[0].innerText) label = f.labels[0].innerText.trim(); } catch (e) {}
|
|
970
|
+
if (!label && f.placeholder) label = String(f.placeholder).trim();
|
|
971
|
+
if (!label && f.name) label = String(f.name);
|
|
972
|
+
if (!label && f.id) label = '#' + f.id;
|
|
973
|
+
if (!label && isCE) label = String((f.getAttribute && (f.getAttribute('data-placeholder') || f.getAttribute('aria-label'))) || (f.innerText || '').trim()).slice(0, 40) || ('可编辑区域' + (n + 1));
|
|
974
|
+
var sel = f.id ? ('#' + String(f.id).replace(/([^a-zA-Z0-9_-])/g, '\\$1')) : (f.name ? ('[name="' + String(f.name).replace(/"/g, '\\"') + '"]') : (isCE ? null : ''));
|
|
975
|
+
fields.push({ label: String(label).slice(0, 40), selector: sel, type: isCE ? 'contenteditable' : (f.type || 'text'), value: String(isCE ? ((f.innerText || f.textContent || '')) : (f.value || '')).slice(0, 60) });
|
|
976
|
+
}
|
|
977
|
+
var buttons = [];
|
|
978
|
+
var btns = dlg.querySelectorAll('button, [role="button"]');
|
|
979
|
+
for (var b = 0; b < btns.length && buttons.length < 10; b++) {
|
|
980
|
+
if (!isVis(btns[b])) continue;
|
|
981
|
+
var bt = String((btns[b].innerText || btns[b].getAttribute('aria-label') || '').trim()).slice(0, 30);
|
|
982
|
+
if (bt) buttons.push(bt);
|
|
983
|
+
}
|
|
984
|
+
out.dialogs.push({ title: title, fields: fields, buttons: buttons });
|
|
985
|
+
}
|
|
986
|
+
return out;
|
|
987
|
+
} catch (e) { return { error: String((e && e.message) || e) }; }
|
|
988
|
+
})()`;
|
|
989
|
+
function pickSelfPageId() {
|
|
990
|
+
let best = null, bestSeen = 0;
|
|
991
|
+
for (const r of listSessions()) {
|
|
992
|
+
if (r.self && (r.lastSeen || 0) > bestSeen) {
|
|
993
|
+
best = r.pageId;
|
|
994
|
+
bestSeen = r.lastSeen || 0;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return best || null;
|
|
998
|
+
}
|
|
999
|
+
const FORM_FILL_FN = `function (opts) {
|
|
1000
|
+
try {
|
|
1001
|
+
var isVis = function (el) { return !!(el && (el.getClientRects().length > 0 || el.offsetParent !== null)); };
|
|
1002
|
+
var cands = Array.prototype.filter.call(document.querySelectorAll('[role="dialog"], [role="alertdialog"], [data-dialog], .dialog, .modal, [class*="dialog"], [class*="modal"]'), isVis);
|
|
1003
|
+
var dialog = null;
|
|
1004
|
+
if (opts && opts.dialog !== undefined && opts.dialog !== null && String(opts.dialog) !== '') {
|
|
1005
|
+
var q = String(opts.dialog);
|
|
1006
|
+
var idx = /^\\d+$/.test(q) ? parseInt(q, 10) : -1;
|
|
1007
|
+
if (idx >= 0 && idx < cands.length) dialog = cands[idx];
|
|
1008
|
+
else dialog = cands.find(function (d) {
|
|
1009
|
+
var t = ((d.querySelector('h1,h2,h3,[class*="title"],header,[role="heading"]') || {}).textContent || '');
|
|
1010
|
+
return (d.textContent || '').indexOf(q) >= 0 || t.indexOf(q) >= 0;
|
|
1011
|
+
}) || null;
|
|
1012
|
+
} else {
|
|
1013
|
+
var best = null, bestArea = 0;
|
|
1014
|
+
for (var i = 0; i < cands.length; i++) {
|
|
1015
|
+
var r = cands[i].getBoundingClientRect();
|
|
1016
|
+
var area = r.width * r.height;
|
|
1017
|
+
if (area > bestArea) { bestArea = area; best = cands[i]; }
|
|
1018
|
+
}
|
|
1019
|
+
dialog = best;
|
|
1020
|
+
}
|
|
1021
|
+
if (!dialog) return { foundDialog: false, filled: 0, missed: 0, clicked: false, error: '未找到可见弹窗(先用 self_ui 确认弹窗已打开)' };
|
|
1022
|
+
var setNative = function (el, value) {
|
|
1023
|
+
var text = String(value == null ? '' : value);
|
|
1024
|
+
// contenteditable(富文本/<p> 可编辑块):浏览器原生输入路径(真实键入,等价 CDP Input.insertText)——
|
|
1025
|
+
// execCommand('insertText') 触发真实 beforeinput/input(inputType=insertText),React onChange 正确接收,值不被重置
|
|
1026
|
+
if (el.isContentEditable || (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA' && el.tagName !== 'SELECT' && el.hasAttribute && el.hasAttribute('contenteditable'))) {
|
|
1027
|
+
try { el.focus(); } catch (e) {}
|
|
1028
|
+
try {
|
|
1029
|
+
var sel = window.getSelection();
|
|
1030
|
+
if (sel) {
|
|
1031
|
+
var range = document.createRange();
|
|
1032
|
+
range.selectNodeContents(el);
|
|
1033
|
+
sel.removeAllRanges();
|
|
1034
|
+
sel.addRange(range);
|
|
1035
|
+
}
|
|
1036
|
+
} catch (e) {}
|
|
1037
|
+
var done = false;
|
|
1038
|
+
try { done = document.execCommand('insertText', false, text); } catch (e) {}
|
|
1039
|
+
if (!done) {
|
|
1040
|
+
try { el.textContent = text; } catch (e) {}
|
|
1041
|
+
try { el.dispatchEvent(new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' })); }
|
|
1042
|
+
catch (e) { try { el.dispatchEvent(new Event('input', { bubbles: true })); } catch (e2) {} }
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
var proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : el.tagName === 'SELECT' ? window.HTMLSelectElement.prototype : window.HTMLInputElement.prototype;
|
|
1047
|
+
var desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
1048
|
+
if (desc && desc.set) desc.set.call(el, text); else el.value = text;
|
|
1049
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1050
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1051
|
+
};
|
|
1052
|
+
var findField = function (f) {
|
|
1053
|
+
if (f.selector) { try { var el = dialog.querySelector(f.selector); if (el) return el; } catch (e) {} }
|
|
1054
|
+
if (f.label) {
|
|
1055
|
+
var l = String(f.label);
|
|
1056
|
+
var labels = Array.prototype.filter.call(dialog.querySelectorAll('label'), function (x) { return (x.textContent || '').trim() === l || (x.textContent || '').indexOf(l) >= 0; });
|
|
1057
|
+
for (var j = 0; j < labels.length; j++) {
|
|
1058
|
+
var lb = labels[j];
|
|
1059
|
+
if (lb.htmlFor) { try { var fe = dialog.querySelector('#' + String(lb.htmlFor).replace(/([^a-zA-Z0-9_\\-])/g, '\\\\$1')); if (fe) return fe; } catch (e) {} }
|
|
1060
|
+
var inl = lb.querySelector('input,textarea,select,[contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""]'); if (inl) return inl;
|
|
1061
|
+
}
|
|
1062
|
+
// 兜底:contenteditable 无 <label> 时按 aria-label/data-placeholder 匹配(self_ui 已列出)
|
|
1063
|
+
var ces = dialog.querySelectorAll('[contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""]');
|
|
1064
|
+
for (var c = 0; c < ces.length; c++) {
|
|
1065
|
+
var ce = ces[c];
|
|
1066
|
+
var al = (ce.getAttribute && (ce.getAttribute('aria-label') || ce.getAttribute('data-placeholder'))) || '';
|
|
1067
|
+
if (al && (al === l || al.indexOf(l) >= 0)) return ce;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return null;
|
|
1071
|
+
};
|
|
1072
|
+
var filled = 0, missed = 0;
|
|
1073
|
+
var fields = (opts && Array.isArray(opts.fields)) ? opts.fields : [];
|
|
1074
|
+
for (var k = 0; k < fields.length; k++) {
|
|
1075
|
+
var f = fields[k];
|
|
1076
|
+
if (!f || typeof f !== 'object' || f.value === undefined) continue;
|
|
1077
|
+
var tgt = findField(f);
|
|
1078
|
+
if (tgt) { setNative(tgt, f.value); filled++; } else missed++;
|
|
1079
|
+
}
|
|
1080
|
+
var clicked = false;
|
|
1081
|
+
if (opts && opts.button) {
|
|
1082
|
+
var btns = dialog.querySelectorAll('button, [role="button"]');
|
|
1083
|
+
var b = null;
|
|
1084
|
+
for (var m = 0; m < btns.length; m++) { if ((btns[m].textContent || '').trim().indexOf(String(opts.button)) >= 0) { b = btns[m]; break; } }
|
|
1085
|
+
if (b) { try { b.click(); clicked = true; } catch (e) {} }
|
|
1086
|
+
}
|
|
1087
|
+
return { foundDialog: true, title: ((dialog.querySelector('h1,h2,h3,[class*="title"],header') || {}).textContent || '').slice(0, 60), filled: filled, missed: missed, clicked: clicked };
|
|
1088
|
+
} catch (e) { return { error: String((e && e.message) || e) }; }
|
|
1089
|
+
}`;
|
|
1090
|
+
;
|
|
1091
|
+
function execOnPage(cmd, args, pageId, signal, mcpReqId, cancelId) {
|
|
1092
|
+
const { instanceId, frameId } = parsePageId(pageId || LEGACY_TOKEN);
|
|
1093
|
+
const page = pages.get(instanceId);
|
|
1094
|
+
return new Promise((resolve2) => {
|
|
1095
|
+
const requestId = "mcp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 6);
|
|
1096
|
+
const rec = { resolve: resolve2, agentId: null, t0: Date.now(), signal: null, onAbort: null, mcpReqId: mcpReqId || null, cancelId: cancelId || null };
|
|
1097
|
+
pendingRequests.set(requestId, rec);
|
|
1098
|
+
if (rec.mcpReqId) mcpReqMap.set(rec.mcpReqId, requestId);
|
|
1099
|
+
if (rec.cancelId) cancelIdMap.set(rec.cancelId, requestId);
|
|
1100
|
+
rec.onAbort = () => cancelPending(requestId, "已取消(agent 停止)");
|
|
1101
|
+
if (signal) {
|
|
1102
|
+
if (signal.aborted) {
|
|
1103
|
+
cancelPending(requestId, "已取消(agent 停止)");
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
rec.signal = signal;
|
|
1107
|
+
signal.addEventListener("abort", rec.onAbort, { once: true });
|
|
1108
|
+
}
|
|
1109
|
+
console.log(TAG + " exec →", cmd, JSON.stringify(args || {}), "→ pageId=" + pageId);
|
|
1110
|
+
if (page) {
|
|
1111
|
+
if (!page.writer.write({ type: "exec", requestId, cmd, args: args || {}, frameId })) {
|
|
1112
|
+
const irec = idRegistry.get(instanceId);
|
|
1113
|
+
if (irec) irec.lastSeen = Date.now();
|
|
1114
|
+
pages.delete(instanceId);
|
|
1115
|
+
settlePending(requestId, false, { error: "页面连接已断开(重连后重试)" });
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
} else if (frameId === "ai" && aiSelfReg.size > 0) {
|
|
1119
|
+
let target = null, bestSeen = 0;
|
|
1120
|
+
for (const [w, rec2] of aiSelfReg) {
|
|
1121
|
+
if (String(rec2.instanceId) === String(instanceId) && (rec2.lastSeen || 0) > bestSeen) {
|
|
1122
|
+
target = w;
|
|
1123
|
+
bestSeen = rec2.lastSeen || 0;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (target) {
|
|
1127
|
+
if (!target.write({ type: "exec", requestId, cmd, args: args || {}, pageId, via: "self" })) {
|
|
1128
|
+
aiConns.delete(target);
|
|
1129
|
+
aiSelfReg.delete(target);
|
|
1130
|
+
settlePending(requestId, false, { error: "目标 AI 主窗口连接已断开(刷新后自动重连)" });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
} else if (aiConns.size > 0) {
|
|
1134
|
+
for (const w of aiConns) w.write({ type: "exec", requestId, cmd, args: args || {}, pageId, via: "self" });
|
|
1135
|
+
} else {
|
|
1136
|
+
setTimeout(() => settlePending(requestId, false, { error: "无执行端(AI 主窗口未连接),先连接桥" }), 100);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
} else if (aiPageReg.size > 0) {
|
|
1140
|
+
let target = null, targetRec = null, viaFrameId = null, bestSeen = 0;
|
|
1141
|
+
for (const [w, rec2] of aiPageReg) {
|
|
1142
|
+
const baseId = String(rec2.pageId || "").replace(/\/host$/, "");
|
|
1143
|
+
if (rec2.pageId === pageId || baseId && pageId && String(pageId).indexOf(baseId + "/") === 0) {
|
|
1144
|
+
if (!target || (rec2.lastSeen || 0) > bestSeen) {
|
|
1145
|
+
target = w;
|
|
1146
|
+
targetRec = rec2;
|
|
1147
|
+
bestSeen = rec2.lastSeen || 0;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (target && targetRec) {
|
|
1152
|
+
const baseId = String(targetRec.pageId || "").replace(/\/host$/, "");
|
|
1153
|
+
if (pageId && baseId && String(pageId).indexOf(baseId + "/") === 0) viaFrameId = String(pageId).slice(baseId.length + 1) || "host";
|
|
1154
|
+
if (!target.write({ type: "exec", requestId, cmd, args: args || {}, pageId, via: "ai", frameId: viaFrameId })) {
|
|
1155
|
+
aiConns.delete(target);
|
|
1156
|
+
aiPageReg.delete(target);
|
|
1157
|
+
settlePending(requestId, false, { error: "目标 AI 窗连接已断开(页面刷新后自动重连,稍后重试)" });
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
} else if (aiConns.size > 0) {
|
|
1161
|
+
for (const w of aiConns) w.write({ type: "exec", requestId, cmd, args: args || {}, pageId, via: "ai" });
|
|
1162
|
+
} else {
|
|
1163
|
+
setTimeout(() => settlePending(requestId, false, { error: "无执行端(页面/AI 通道均未连接),先连接桥" }), 100);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
} else if (aiConns.size > 0) {
|
|
1167
|
+
for (const w of aiConns) w.write({ type: "exec", requestId, cmd, args: args || {}, pageId, via: "ai" });
|
|
1168
|
+
} else {
|
|
1169
|
+
setTimeout(() => settlePending(requestId, false, { error: "无执行端(页面/AI 通道均未连接),先连接桥" }), 100);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const SLOW_EXEC_CMDS = /* @__PURE__ */ new Set([
|
|
1173
|
+
"Page.captureScreenshot",
|
|
1174
|
+
"Browser.paste",
|
|
1175
|
+
"Browser.pickFile",
|
|
1176
|
+
"Browser.share",
|
|
1177
|
+
"DOM.removeNode",
|
|
1178
|
+
"Page.reload",
|
|
1179
|
+
"Page.navigate",
|
|
1180
|
+
"Storage.localStorageClear",
|
|
1181
|
+
"Storage.localStorageSet"
|
|
1182
|
+
]);
|
|
1183
|
+
const slowNet = (function() {
|
|
1184
|
+
if (!SLOW_EXEC_CMDS.has(cmd)) return 15e3;
|
|
1185
|
+
if (cmd === "Page.captureScreenshot") {
|
|
1186
|
+
const n = parseInt(args && args.timeout, 10);
|
|
1187
|
+
if (n > 0) return Math.min(Math.max(n + 3e4, 6e4), 36e5);
|
|
1188
|
+
}
|
|
1189
|
+
return 3e5;
|
|
1190
|
+
})();
|
|
1191
|
+
setTimeout(() => settlePending(requestId, false, { error: "页面执行超时" + (SLOW_EXEC_CMDS.has(cmd) ? "(用户未完成确认/选择)" : "") }), slowNet);
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
const INTERACTION_MAX_TIMEOUT_MS = 6e5;
|
|
1195
|
+
const INTERACTION_MIN_TIMEOUT_MS = 5e3;
|
|
1196
|
+
const clampTimeout = (ms) => {
|
|
1197
|
+
const n = parseInt(ms, 10);
|
|
1198
|
+
if (!n || n <= 0) return 0;
|
|
1199
|
+
return Math.min(Math.max(n, INTERACTION_MIN_TIMEOUT_MS), INTERACTION_MAX_TIMEOUT_MS);
|
|
1200
|
+
};
|
|
1201
|
+
const interactionTimers = /* @__PURE__ */ new Map();
|
|
1202
|
+
const pauseInteraction = (requestId) => {
|
|
1203
|
+
const st = interactionTimers.get(requestId);
|
|
1204
|
+
if (!st) return { ok: false, error: "unknown interaction" };
|
|
1205
|
+
if (st.paused) return { ok: true, paused: true, remainingMs: st.remaining };
|
|
1206
|
+
clearTimeout(st.timer);
|
|
1207
|
+
st.remaining = Math.max(0, st.deadline - Date.now());
|
|
1208
|
+
st.paused = true;
|
|
1209
|
+
st.pausedAt = Date.now();
|
|
1210
|
+
console.log(TAG + " interaction ⏸ 暂停 requestId=" + requestId + " remaining=" + st.remaining + "ms");
|
|
1211
|
+
return { ok: true, paused: true, remainingMs: st.remaining };
|
|
1212
|
+
};
|
|
1213
|
+
const resumeInteraction = (requestId) => {
|
|
1214
|
+
const st = interactionTimers.get(requestId);
|
|
1215
|
+
if (!st) return { ok: false, error: "unknown interaction" };
|
|
1216
|
+
if (!st.paused) return { ok: true, paused: false, remainingMs: Math.max(0, st.deadline - Date.now()) };
|
|
1217
|
+
if (st.pausedAt) st.pausedMs += Date.now() - st.pausedAt;
|
|
1218
|
+
st.pausedAt = null;
|
|
1219
|
+
st.paused = false;
|
|
1220
|
+
st.remaining = Math.max(0, st.remaining);
|
|
1221
|
+
st.deadline = Date.now() + st.remaining;
|
|
1222
|
+
const fire = () => {
|
|
1223
|
+
if (!interactionTimers.delete(requestId)) return;
|
|
1224
|
+
const result = { status: "timeout", error: "用户无响应(不在线或未处理)", tookMs: Date.now() - st.t0, timeoutMs: st.ms, pausedMs: st.pausedMs };
|
|
1225
|
+
if (settlePending(requestId, false, result)) {
|
|
1226
|
+
for (const w of aiConns) w.write({ type: "result", requestId, ok: false, result });
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
if (st.remaining <= 0) {
|
|
1230
|
+
fire();
|
|
1231
|
+
return { ok: true, paused: false, remainingMs: 0, timedOut: true };
|
|
1232
|
+
}
|
|
1233
|
+
st.timer = setTimeout(fire, st.remaining);
|
|
1234
|
+
console.log(TAG + " interaction ▶ 继续 requestId=" + requestId + " remaining=" + st.remaining + "ms");
|
|
1235
|
+
return { ok: true, paused: false, remainingMs: st.remaining };
|
|
1236
|
+
};
|
|
1237
|
+
const clearInteractionTimer = (requestId) => {
|
|
1238
|
+
const st = interactionTimers.get(requestId);
|
|
1239
|
+
if (st) {
|
|
1240
|
+
clearTimeout(st.timer);
|
|
1241
|
+
interactionTimers.delete(requestId);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
function interactOnPage(interaction, pageId, timeoutMs, signal, mcpReqId, cancelId) {
|
|
1245
|
+
const { instanceId, frameId } = parsePageId(pageId || LEGACY_TOKEN);
|
|
1246
|
+
const page = pages.get(instanceId);
|
|
1247
|
+
let aiTarget = null;
|
|
1248
|
+
if (frameId === "ai") {
|
|
1249
|
+
for (const [w, rec] of aiSelfReg) if (rec.instanceId === instanceId) {
|
|
1250
|
+
aiTarget = w;
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const ms = clampTimeout(timeoutMs);
|
|
1255
|
+
const t0 = Date.now();
|
|
1256
|
+
const payload = { ...interaction, timeoutMs: ms };
|
|
1257
|
+
const isAsync = !!payload.async;
|
|
1258
|
+
return new Promise((resolve2) => {
|
|
1259
|
+
const requestId = "ui_" + Date.now() + "_" + Math.random().toString(36).slice(2, 6);
|
|
1260
|
+
if (isAsync) {
|
|
1261
|
+
console.log(TAG + " interaction(异步)→ kind=" + payload.kind + " · " + String(payload.prompt || "").slice(0, 60) + " → pageId=" + pageId + (payload.agentId ? " · agent=" + payload.agentId : ""));
|
|
1262
|
+
resolve2({ ok: true, result: { status: "async", message: "已异步发出:用户作答后结果将进发起 agent(" + (payload.agentId || "?") + ")的 aiti_inbox" } });
|
|
1263
|
+
} else {
|
|
1264
|
+
const rec = { resolve: resolve2, agentId: payload.agentId || null, t0: Date.now(), signal: null, onAbort: null, mcpReqId: mcpReqId || null, cancelId: cancelId || null };
|
|
1265
|
+
pendingRequests.set(requestId, rec);
|
|
1266
|
+
if (rec.mcpReqId) mcpReqMap.set(rec.mcpReqId, requestId);
|
|
1267
|
+
if (rec.cancelId) cancelIdMap.set(rec.cancelId, requestId);
|
|
1268
|
+
rec.onAbort = () => cancelPending(requestId, "已取消(agent 停止)");
|
|
1269
|
+
if (signal) {
|
|
1270
|
+
if (signal.aborted) {
|
|
1271
|
+
cancelPending(requestId, "已取消(agent 停止)");
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
rec.signal = signal;
|
|
1275
|
+
signal.addEventListener("abort", rec.onAbort, { once: true });
|
|
1276
|
+
}
|
|
1277
|
+
console.log(TAG + " interaction → kind=" + payload.kind + " · " + String(payload.prompt || "").slice(0, 60) + " → pageId=" + pageId + " · timeout=" + (ms > 0 ? ms + "ms" : "∞(永久等待)") + (payload.agentId ? " · agent=" + payload.agentId : ""));
|
|
1278
|
+
}
|
|
1279
|
+
const noExecutor = () => {
|
|
1280
|
+
clearInteractionTimer(requestId);
|
|
1281
|
+
cancelPending(requestId, "无执行端(页面/AI 通道均未连接),先连接桥");
|
|
1282
|
+
};
|
|
1283
|
+
if (aiTarget) {
|
|
1284
|
+
if (!aiTarget.write({ type: "interaction", requestId, interaction: payload, frameId: "ai", via: "self" })) {
|
|
1285
|
+
aiConns.delete(aiTarget);
|
|
1286
|
+
aiSelfReg.delete(aiTarget);
|
|
1287
|
+
if (aiConns.size > 0) {
|
|
1288
|
+
for (const w of aiConns) w.write({ type: "interaction", requestId, interaction: payload, pageId, via: "ai" });
|
|
1289
|
+
} else return noExecutor();
|
|
1290
|
+
}
|
|
1291
|
+
} else if (payload.kind === "pick_element") {
|
|
1292
|
+
if (page) {
|
|
1293
|
+
if (!page.writer.write({ type: "interaction", requestId, interaction: payload, frameId })) {
|
|
1294
|
+
pages.delete(instanceId);
|
|
1295
|
+
return noExecutor();
|
|
1296
|
+
}
|
|
1297
|
+
} else if (aiConns.size > 0) {
|
|
1298
|
+
for (const w of aiConns) w.write({ type: "interaction", requestId, interaction: payload, pageId, via: "ai" });
|
|
1299
|
+
} else return noExecutor();
|
|
1300
|
+
} else {
|
|
1301
|
+
if (page) {
|
|
1302
|
+
if (!page.writer.write({ type: "interaction", requestId, interaction: payload, frameId })) {
|
|
1303
|
+
pages.delete(instanceId);
|
|
1304
|
+
return noExecutor();
|
|
1305
|
+
}
|
|
1306
|
+
} else if (aiConns.size > 0) {
|
|
1307
|
+
for (const w of aiConns) w.write({ type: "interaction", requestId, interaction: payload, pageId, via: "ai" });
|
|
1308
|
+
} else return noExecutor();
|
|
1309
|
+
}
|
|
1310
|
+
if (!isAsync && ms > 0) {
|
|
1311
|
+
const st = { timer: null, deadline: Date.now() + ms, remaining: ms, paused: false, pausedAt: null, pausedMs: 0, ms, t0, resolve: resolve2 };
|
|
1312
|
+
interactionTimers.set(requestId, st);
|
|
1313
|
+
st.timer = setTimeout(() => {
|
|
1314
|
+
if (!interactionTimers.delete(requestId)) return;
|
|
1315
|
+
const result = { status: "timeout", error: "用户无响应(不在线或未处理)", tookMs: Date.now() - t0, timeoutMs: ms, pausedMs: st.pausedMs };
|
|
1316
|
+
if (settlePending(requestId, false, result)) {
|
|
1317
|
+
for (const w of aiConns) w.write({ type: "result", requestId, ok: false, result });
|
|
1318
|
+
}
|
|
1319
|
+
}, ms);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
function handleMcpRequest(req, session = null, signal = null, cancelId = null) {
|
|
1324
|
+
const params = req.params || {};
|
|
1325
|
+
switch (req.method) {
|
|
1326
|
+
case "initialize":
|
|
1327
|
+
return {
|
|
1328
|
+
protocolVersion: params.protocolVersion || "2024-11-05",
|
|
1329
|
+
capabilities: { tools: {} },
|
|
1330
|
+
serverInfo: { name: DISPLAY_NAME, version: "0.2" },
|
|
1331
|
+
instructions: [
|
|
1332
|
+
"aiti-devtools:页面内 AI 调试助手(浏览器桥)。",
|
|
1333
|
+
"工作流:页面发来的消息通常带 pageId。没有也可以先用 bridge_status 发现已连接页面(返回 sessions,含 pageId/url/title/frameworks/device——deviceLabel 一行的浏览器·屏幕·在线·语言可快速识别是哪台机器哪个标签)→ 把 pageId 传给目标工具。",
|
|
1334
|
+
"pageId 格式为 instanceId/frameId(如 3001/host);也可直接传 url 或页面标题;命中多个时需具体 pageId 指定。",
|
|
1335
|
+
"page_exec:在已连接页面执行类 CDP 命令(DOM/CSS/Input/Page/Runtime/Storage/Performance/Browser 域)。args 为命令参数对象。危险命令(导航/下载/对话框等)会自动弹用户确认。截屏(Page.captureScreenshot)务必在 args 带 reason(一句话向用户说明想截什么,页面会先弹准备卡展示该说明、用户调整好页面才开始截屏)。",
|
|
1336
|
+
"page_snapshot:拉取目标页面基础现场(URL/选区/模拟条件)。",
|
|
1337
|
+
"page_logs / page_network:排查页面报错/请求失败首选——page_logs 查 console(可 level/since/limit 过滤,先看 warn/error);page_network 查 ajax/fetch 完成态(status≥400 或 errorOnly:true 只看失败)。两者均读页面本地缓冲(CDP Runtime.evaluate 按需拉取,不再实时上报)。",
|
|
1338
|
+
"user_confirm / user_choose / user_input / user_pick_element / user_presence:AI 与人类用户在页面上协作——prompt 给用户看,默认同步等待且永久等待(不传 timeout:面板常驻直到用户作答,无读秒;仅在确需限时催促时传 timeout ms,面板显示读秒倒计时可暂停,到点返回 status:'timeout'),响应信封 {status:'answered'|'rejected'|'timeout'|'error', value?, reason?, tookMs, timeoutMs}。传 async:true 可立即返回(结果经 AI 消息窗送达)。",
|
|
1339
|
+
"user_notify:非阻塞浏览器通知,不等待回答。",
|
|
1340
|
+
"常见用法:页面报错→page_logs(level:warn/error) 或 page_network(errorOnly:true);操作页面→page_exec(cmd:'Runtime.evaluate', args:{expression:'...'});定位元素→page_snapshot 后再 page_exec 或请用户 user_pick_element。"
|
|
1341
|
+
].join("\n")
|
|
1342
|
+
};
|
|
1343
|
+
case "notifications/initialized":
|
|
1344
|
+
return null;
|
|
1345
|
+
case "notifications/cancelled": {
|
|
1346
|
+
const rid = params && params.requestId !== void 0 ? String(params.requestId) : "";
|
|
1347
|
+
if (rid) {
|
|
1348
|
+
const internalId = mcpReqMap.get(rid);
|
|
1349
|
+
if (internalId) cancelPending(internalId, "已取消(MCP notifications/cancelled)");
|
|
1350
|
+
}
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
case "ping":
|
|
1354
|
+
return {};
|
|
1355
|
+
case "tools/list":
|
|
1356
|
+
return { tools: TOOLS };
|
|
1357
|
+
case "resources/list":
|
|
1358
|
+
return { resources: [] };
|
|
1359
|
+
case "tools/call": {
|
|
1360
|
+
const a = params.arguments || {};
|
|
1361
|
+
const rawArgs = (() => {
|
|
1362
|
+
let x = a.args;
|
|
1363
|
+
if (typeof x === "string") {
|
|
1364
|
+
try {
|
|
1365
|
+
x = JSON.parse(x);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return x && typeof x === "object" ? x : {};
|
|
1370
|
+
})();
|
|
1371
|
+
const cmdRaw = a.cmd || rawArgs.cmd || a.command || rawArgs.command;
|
|
1372
|
+
const cmd = typeof cmdRaw === "string" && cmdRaw.includes(".") ? cmdRaw.replace(/^./, (c) => c.toUpperCase()) : cmdRaw;
|
|
1373
|
+
const pageId = a.pageId || rawArgs.pageId || a.target || rawArgs.target;
|
|
1374
|
+
const cmdArgs = { ...rawArgs };
|
|
1375
|
+
delete cmdArgs.cmd;
|
|
1376
|
+
delete cmdArgs.command;
|
|
1377
|
+
delete cmdArgs.pageId;
|
|
1378
|
+
delete cmdArgs.target;
|
|
1379
|
+
delete cmdArgs.token;
|
|
1380
|
+
const recAgentKey = session && session.agentKey || (session && session.agentId ? session.agentId + (session.id ? "/" + session.id : "") : null) || null;
|
|
1381
|
+
const recToolCall = (text, ok, detail) => {
|
|
1382
|
+
if (!recAgentKey) return;
|
|
1383
|
+
const m = { id: randomBytes(4).toString("hex"), kind: "agent_tool", from: "agent", agentKey: recAgentKey, agentId: recAgentKey.split("/")[0], sessionId: recAgentKey.split("/").slice(1).join("/") || null, source: (agents.get(recAgentKey) || {}).source || "unknown", text: String(text).slice(0, 2e3), detail: detail === void 0 ? null : typeof detail === "string" ? detail : JSON.stringify(detail).slice(0, 4e3), at: (/* @__PURE__ */ new Date()).toISOString(), status: ok ? "ok" : "error" };
|
|
1384
|
+
appendMsgEvent(recAgentKey, { type: "message", id: m.id, parentId: null, timestamp: m.at, message: m });
|
|
1385
|
+
for (const w of aiConns) w.write({ type: "agent_answer", agentKey: recAgentKey, agentId: recAgentKey.split("/")[0], ok, text: m.text, at: m.at, detail: m.detail });
|
|
1386
|
+
};
|
|
1387
|
+
if (params.name === "bridge_status") {
|
|
1388
|
+
const curAgentKey = session && session.agentKey || (session && session.agentId ? session.agentId + (session.id ? "/" + session.id : "") : null) || null;
|
|
1389
|
+
const boundPageId = curAgentKey && agents.get(curAgentKey) && agents.get(curAgentKey).boundPageId || null;
|
|
1390
|
+
const sess = listSessions(boundPageId);
|
|
1391
|
+
const resp = { sessions: sess, activePageId: boundPageId || null };
|
|
1392
|
+
recToolCall("🛠 bridge_status → " + sess.length + " 个已连接页面" + (boundPageId ? " · 当前绑定: " + boundPageId : ""), true, resp);
|
|
1393
|
+
return { content: [{ type: "text", text: JSON.stringify(resp) }], isError: false };
|
|
1394
|
+
}
|
|
1395
|
+
if (params.name === "file_stat") {
|
|
1396
|
+
const fp = String(a.path || "").trim();
|
|
1397
|
+
const out = { path: fp || null, exists: false, isFile: false, isDirectory: false, size: null, mtime: null };
|
|
1398
|
+
if (fp) {
|
|
1399
|
+
try {
|
|
1400
|
+
const st = statSync(fp);
|
|
1401
|
+
out.exists = true;
|
|
1402
|
+
out.isFile = st.isFile();
|
|
1403
|
+
out.isDirectory = st.isDirectory();
|
|
1404
|
+
out.size = st.isFile() ? st.size : null;
|
|
1405
|
+
out.mtime = st.mtimeMs;
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
recToolCall("🛠 file_stat(" + (fp || "") + ") → " + (out.exists ? out.isFile ? "文件 " + out.size + "B" : "目录" : "不存在"), out.exists);
|
|
1410
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }], isError: false };
|
|
1411
|
+
}
|
|
1412
|
+
if (params.name === "page_logs" || params.name === "page_network") {
|
|
1413
|
+
const isLog = params.name === "page_logs";
|
|
1414
|
+
const ringExpr = isLog ? "(() => { try { var r = window.__dbg && window.__dbg.__consoleRing; if (!r) return []; return r.slice(-" + (parseInt(a.limit, 10) || 200) + "); } catch(e) { return []; } })()" : "(() => { try { var r = window.__dbg && window.__dbg.__netRing; if (!r) return []; return r.slice(-" + (parseInt(a.limit, 10) || 100) + "); } catch(e) { return []; } })()";
|
|
1415
|
+
const target = pageId && String(pageId) || pickSelfPageId();
|
|
1416
|
+
const resolved = target ? resolvePageId(target) : { notFound: true };
|
|
1417
|
+
if (resolved.multiple) {
|
|
1418
|
+
recToolCall("🛠 " + params.name + " → ⚠ 页面匹配多个", false);
|
|
1419
|
+
return { content: [{ type: "text", text: "页面匹配多个(按 url/title):" + resolved.multiple.join(", ") + "——请用具体 pageId 指定" }], isError: true };
|
|
1420
|
+
}
|
|
1421
|
+
if (resolved.notFound) {
|
|
1422
|
+
recToolCall("🛠 " + params.name + " → ⚠ 未找到匹配页面", false);
|
|
1423
|
+
return { content: [{ type: "text", text: "未找到匹配页面(pageId/url/title 均未命中)——用 bridge_status 查看可用 pageId" }], isError: true };
|
|
1424
|
+
}
|
|
1425
|
+
const mcpReqId = req && req.id !== void 0 ? String(req.id) : null;
|
|
1426
|
+
return execOnPage("Runtime.evaluate", { expression: ringExpr }, resolved.pageId, signal, mcpReqId, cancelId).then((r) => {
|
|
1427
|
+
const raw = r.ok && r.result && r.result.value !== void 0 ? r.result.value : [];
|
|
1428
|
+
let rows = Array.isArray(raw) ? raw : [];
|
|
1429
|
+
if (isLog) {
|
|
1430
|
+
if (a.level) rows = rows.filter((l) => l.level === a.level);
|
|
1431
|
+
if (a.since) {
|
|
1432
|
+
const t = Date.parse(a.since);
|
|
1433
|
+
if (!isNaN(t)) rows = rows.filter((l) => l.ts >= t);
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
if (a.url) rows = rows.filter((rq) => String(rq.url || "").includes(String(a.url)));
|
|
1437
|
+
if (a.status) rows = rows.filter((rq) => (rq.status || 0) >= Number(a.status));
|
|
1438
|
+
if (a.errorOnly) rows = rows.filter((rq) => rq.error || (rq.status || 0) >= 400);
|
|
1439
|
+
}
|
|
1440
|
+
const limit = Math.min(parseInt(a.limit, 10) || (isLog ? 50 : 30), isLog ? 200 : 100);
|
|
1441
|
+
rows = rows.slice(-limit);
|
|
1442
|
+
const out = isLog ? { logs: rows, bufferSize: Array.isArray(raw) ? raw.length : 0 } : { requests: rows, bufferSize: Array.isArray(raw) ? raw.length : 0 };
|
|
1443
|
+
recToolCall("🛠 " + params.name + "(" + resolved.pageId + ") → " + rows.length + " 条", true, out);
|
|
1444
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }], isError: false };
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
if (params.name === "page_exec" || params.name === "page_snapshot") {
|
|
1448
|
+
const target = pageId && String(pageId) || pickSelfPageId();
|
|
1449
|
+
const resolved = target ? resolvePageId(target) : { notFound: true };
|
|
1450
|
+
if (resolved.multiple) {
|
|
1451
|
+
recToolCall("🛠 " + params.name + " → ⚠ 页面匹配多个:" + resolved.multiple.join(", "), false);
|
|
1452
|
+
return { content: [{ type: "text", text: "页面匹配多个(按 url/title):" + resolved.multiple.join(", ") + "——请用具体 pageId 指定(bridge_status 可查)" }], isError: true };
|
|
1453
|
+
}
|
|
1454
|
+
if (resolved.notFound) {
|
|
1455
|
+
recToolCall("🛠 " + params.name + (pageId ? "(" + pageId + ")" : "") + " → ⚠ 未找到匹配页面", false);
|
|
1456
|
+
return { content: [{ type: "text", text: "未找到匹配页面(pageId/url/title 均未命中)——用 bridge_status 查看可用 pageId" }], isError: true };
|
|
1457
|
+
}
|
|
1458
|
+
const mcpReqId = req && req.id !== void 0 ? String(req.id) : null;
|
|
1459
|
+
const runPageTool = params.name === "page_exec" ? execOnPage(cmd, cmdArgs, resolved.pageId, signal, mcpReqId, cancelId) : execOnPage("__snapshot__", {}, resolved.pageId, signal, mcpReqId, cancelId).then((r) => {
|
|
1460
|
+
const txt = JSON.stringify(r.result || r);
|
|
1461
|
+
if (r.ok || txt.indexOf("unknown command: __snapshot__") === -1) return r;
|
|
1462
|
+
return execOnPage("Runtime.evaluate", { expression: SNAPSHOT_FALLBACK, returnByValue: true }, resolved.pageId, signal, mcpReqId, cancelId).then((r2) => {
|
|
1463
|
+
if (!r2.ok) return r2;
|
|
1464
|
+
const ro = r2.result && r2.result.result ? r2.result.result : r2.result;
|
|
1465
|
+
let val = ro && ro.value !== void 0 ? ro.value : ro;
|
|
1466
|
+
if (typeof val === "string") {
|
|
1467
|
+
try {
|
|
1468
|
+
val = JSON.parse(val);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return { ok: true, result: val };
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
return runPageTool.then((r) => {
|
|
1476
|
+
recToolCall("🛠 " + params.name + "(" + resolved.pageId + ")" + (params.name === "page_exec" && cmd ? " · " + String(cmd).slice(0, 80) : "") + " → " + (r.ok ? "ok" : "失败"), !!r.ok, { tool: params.name, request: params.name === "page_exec" ? { cmd, args: cmdArgs } : { pageId: resolved.pageId }, response: r.result || r, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1477
|
+
return { content: [{ type: "text", text: JSON.stringify(r.result || r) }], isError: !r.ok };
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
if (params.name === "self_ui") {
|
|
1481
|
+
const target = pageId && String(pageId) || pickSelfPageId();
|
|
1482
|
+
const resolved = target ? resolvePageId(target) : { notFound: true };
|
|
1483
|
+
if (!resolved.pageId) {
|
|
1484
|
+
recToolCall("🛠 self_ui → 未找到目标页面(无 AI 主窗口连接或 pageId 未命中)", false);
|
|
1485
|
+
return { content: [{ type: "text", text: "未找到目标页面——需 AI 主窗口已连接(bridge_status 可见 xxx/ai),或指定 pageId" }], isError: true };
|
|
1486
|
+
}
|
|
1487
|
+
const mcpReqId = req && req.id !== void 0 ? String(req.id) : null;
|
|
1488
|
+
return execOnPage("Runtime.evaluate", { expression: SELF_UI_SCAN }, resolved.pageId, signal, mcpReqId, cancelId).then((r) => {
|
|
1489
|
+
const val = r.ok && r.result && r.result.value !== void 0 ? r.result.value : r.result || r;
|
|
1490
|
+
recToolCall("🛠 self_ui(" + resolved.pageId + ") → " + (r.ok ? val && val.dialogs ? val.dialogs.length + " 个弹窗" : "无弹窗" : "失败"), !!r.ok, { tool: "self_ui", pageId: resolved.pageId, response: val });
|
|
1491
|
+
return { content: [{ type: "text", text: JSON.stringify(val) }], isError: !r.ok };
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (params.name === "form_fill") {
|
|
1495
|
+
const target = pageId && String(pageId) || pickSelfPageId();
|
|
1496
|
+
const resolved = target ? resolvePageId(target) : { notFound: true };
|
|
1497
|
+
if (!resolved.pageId) {
|
|
1498
|
+
recToolCall("🛠 form_fill → 未找到目标页面", false);
|
|
1499
|
+
return { content: [{ type: "text", text: "未找到目标页面——需 AI 主窗口已连接,或指定 pageId" }], isError: true };
|
|
1500
|
+
}
|
|
1501
|
+
const mcpReqId = req && req.id !== void 0 ? String(req.id) : null;
|
|
1502
|
+
const opts2 = { dialog: a.dialog || null, fields: a.fields || [], button: a.button || null };
|
|
1503
|
+
const expression = "(" + FORM_FILL_FN + ")(" + JSON.stringify(opts2) + ")";
|
|
1504
|
+
return execOnPage("Runtime.evaluate", { expression }, resolved.pageId, signal, mcpReqId, cancelId).then((r) => {
|
|
1505
|
+
const val = r.ok && r.result && r.result.value !== void 0 ? r.result.value : r.result || r;
|
|
1506
|
+
recToolCall("🛠 form_fill(" + resolved.pageId + ") → " + (r.ok ? val && val.foundDialog ? "填 " + val.filled + " 字段" + (val.clicked ? ",点按钮" : "") : "未找到弹窗" : "失败"), !!r.ok, { tool: "form_fill", pageId: resolved.pageId, response: val });
|
|
1507
|
+
return { content: [{ type: "text", text: JSON.stringify(val) }], isError: !r.ok };
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
const interact = (interaction, timeout) => {
|
|
1511
|
+
const mcpReqId = req && req.id !== void 0 ? String(req.id) : null;
|
|
1512
|
+
const targetPageId = pageId || boundPageIdFor(session) || void 0;
|
|
1513
|
+
return interactOnPage({ ...interaction, agentId: session && session.agentKey || session && session.agentId || a.agentId || null }, targetPageId, timeout, signal, mcpReqId, cancelId).then((r) => {
|
|
1514
|
+
recToolCall("🛠 " + interaction.kind + " → " + (r.ok ? "已作答" : r.result && r.result.status || "失败/拒绝"), !!r.ok);
|
|
1515
|
+
return { content: [{ type: "text", text: JSON.stringify(r.result || r) }], isError: !r.ok };
|
|
1516
|
+
});
|
|
1517
|
+
};
|
|
1518
|
+
if (params.name === "user_pick_element") {
|
|
1519
|
+
return interact({ kind: "pick_element", prompt: String(a.prompt || "请点击页面上的一个元素(AI 需要你帮忙定位)"), rejectable: a.rejectable !== false, async: a.async === true }, a.timeout);
|
|
1520
|
+
}
|
|
1521
|
+
if (params.name === "user_confirm") {
|
|
1522
|
+
return interact({ kind: "confirm", prompt: String(a.prompt || "请确认"), rejectable: a.rejectable !== false, async: a.async === true }, a.timeout);
|
|
1523
|
+
}
|
|
1524
|
+
if (params.name === "user_choose") {
|
|
1525
|
+
const choices = Array.isArray(a.choices) && a.choices.length ? a.choices.map(String).slice(0, 8) : ["选项 A", "选项 B"];
|
|
1526
|
+
return interact({ kind: "choose", prompt: String(a.prompt || "请选择一项"), choices, multiple: !!a.multiple, rejectable: a.rejectable !== false, async: a.async === true }, a.timeout);
|
|
1527
|
+
}
|
|
1528
|
+
if (params.name === "user_input") {
|
|
1529
|
+
return interact({ kind: "input", prompt: String(a.prompt || "请输入"), defaultValue: String(a.defaultValue || ""), rejectable: a.rejectable !== false, async: a.async === true }, a.timeout);
|
|
1530
|
+
}
|
|
1531
|
+
if (params.name === "user_presence") {
|
|
1532
|
+
return interact({ kind: "presence", prompt: String(a.prompt || "你在吗?需要你配合继续开发"), rejectable: false, async: a.async === true }, a.timeout);
|
|
1533
|
+
}
|
|
1534
|
+
if (params.name === "user_notify") {
|
|
1535
|
+
const notif = { kind: "notify", title: String(a.title || "🤖 AI 有一条消息"), body: String(a.body || a.prompt || ""), timeoutMs: clampTimeout(a.timeout) };
|
|
1536
|
+
const rid = "notify_" + Date.now();
|
|
1537
|
+
for (const w of aiConns) w.write({ type: "interaction", requestId: rid, interaction: notif, pageId });
|
|
1538
|
+
const pg = pages.get(parsePageId(pageId || LEGACY_TOKEN).instanceId);
|
|
1539
|
+
if (pg) pg.writer.write({ type: "interaction", requestId: rid, interaction: notif, frameId: parsePageId(pageId || LEGACY_TOKEN).frameId, render: "notify-only" });
|
|
1540
|
+
recToolCall("🔔 user_notify → " + String(notif.body).slice(0, 60), true);
|
|
1541
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "sent", notified: true, title: notif.title }) }], isError: false };
|
|
1542
|
+
}
|
|
1543
|
+
if (params.name === "aiti_hello") {
|
|
1544
|
+
const agentId2 = String(a.agentId || session && session.agentId || "pi");
|
|
1545
|
+
const sessionId = a.sessionId || session && session.id || null;
|
|
1546
|
+
const { a: agent, key } = ensureAgent(agentId2, sessionId, session);
|
|
1547
|
+
if (!agent) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "agent 注册失败" }) }], isError: true };
|
|
1548
|
+
agent.registered = true;
|
|
1549
|
+
agent.name = a.name || agent.name;
|
|
1550
|
+
if (typeof a.cwd === "string" && a.cwd) agent.cwd = a.cwd;
|
|
1551
|
+
if (session) {
|
|
1552
|
+
session.agentId = agentId2;
|
|
1553
|
+
session.agentKey = key;
|
|
1554
|
+
}
|
|
1555
|
+
if (a.pageId) {
|
|
1556
|
+
const resolved2 = resolvePageId(a.pageId);
|
|
1557
|
+
agent.boundPageId = resolved2.pageId || a.pageId;
|
|
1558
|
+
}
|
|
1559
|
+
if (Array.isArray(a.subEvents)) agent.subEvents = new Set(a.subEvents.map(String));
|
|
1560
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, agentKey: key, agentId: agentId2, sessionId: agent.sessionId, boundPageId: agent.boundPageId || null, subEvents: [...agent.subEvents], inboxPending: agent.outbox.filter((m) => m.status !== "consumed").length }) }], isError: false };
|
|
1561
|
+
}
|
|
1562
|
+
if (params.name === "aiti_inbox") {
|
|
1563
|
+
const agentId2 = String(a.agentId || session && session.agentId || "pi");
|
|
1564
|
+
const sessionId = a.sessionId || session && session.id || (session && session.agentKey ? session.agentKey.split("/").slice(1).join("/") : null) || null;
|
|
1565
|
+
const { a: agent, key } = ensureAgent(agentId2, sessionId, session);
|
|
1566
|
+
if (!agent) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "unknown agent:先 aiti_hello 或带 agentId/sessionId" }) }], isError: true };
|
|
1567
|
+
if (session && !session.agentKey) session.agentKey = key;
|
|
1568
|
+
if (Array.isArray(a.ack) || a.ack && typeof a.ack === "object") {
|
|
1569
|
+
const ackIds = Array.isArray(a.ack) ? a.ack.map(String) : (a.ack && a.ack.ids || []).map(String);
|
|
1570
|
+
const summary = !Array.isArray(a.ack) && a.ack && typeof a.ack.summary === "string" && a.ack.summary.trim() || "";
|
|
1571
|
+
let ackedAny = false;
|
|
1572
|
+
for (const id of ackIds) {
|
|
1573
|
+
const m = agent.outbox.find((x) => x.id === id && x.status !== "consumed");
|
|
1574
|
+
if (m) {
|
|
1575
|
+
m.status = "consumed";
|
|
1576
|
+
m.ackedAt = Date.now();
|
|
1577
|
+
m.summary = summary || null;
|
|
1578
|
+
ackedAny = true;
|
|
1579
|
+
appendMsgEvent(key, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: id, status: "consumed", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (ackedAny) {
|
|
1583
|
+
const text = summary || `✅ 已处理 ${ackIds.length} 条消息(未附总结——建议 ack 时带 summary)`;
|
|
1584
|
+
const rid = randomBytes(4).toString("hex");
|
|
1585
|
+
const rec = { id: rid, kind: "agent_summary", from: "agent", agentKey: key, agentId: agentId2, sessionId: agent.sessionId, text, ackedIds: ackIds, at: (/* @__PURE__ */ new Date()).toISOString(), status: "ok" };
|
|
1586
|
+
agent.outbox.push(rec);
|
|
1587
|
+
appendMsgEvent(key, { type: "message", id: rid, parentId: null, timestamp: rec.at, message: rec });
|
|
1588
|
+
for (const w of aiConns) w.write({ type: "agent_answer", agentKey: key, agentId: agentId2, ok: true, text, at: Date.now(), summary: true });
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const out = [];
|
|
1592
|
+
for (const m of agent.outbox) {
|
|
1593
|
+
if (m.status === "consumed") continue;
|
|
1594
|
+
if (m.status === "pending") {
|
|
1595
|
+
m.status = "delivered";
|
|
1596
|
+
appendMsgEvent(key, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: m.id, status: "delivered", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1597
|
+
}
|
|
1598
|
+
out.push({ id: m.id, kind: m.kind, from: m.from, agentKey: key, agentId: agentId2, sessionId: agent.sessionId, pageId: m.pageId || m.payload && m.payload.pageId || null, url: m.url || m.payload && m.payload.url || null, text: m.text ?? null, payload: m.payload || null, at: m.at, status: m.status });
|
|
1599
|
+
}
|
|
1600
|
+
for (const w of aiConns) w.write({ type: "event", from: "agent", event: "inbox_status", payload: { agentKey: key, queueSize: agent.outbox.filter((x) => x.status !== "consumed").length } });
|
|
1601
|
+
const guide = agent.registered ? "" : " ⚠ 未显式注册(随机 session)——建议调 aiti_hello 传 agentId/sessionId 注册,消息将绑定到该会话";
|
|
1602
|
+
return { content: [{ type: "text", text: JSON.stringify({ agentKey: key, registered: agent.registered, messages: out, queueSize: agent.outbox.filter((x) => x.status !== "consumed").length, guide }) }], isError: false };
|
|
1603
|
+
}
|
|
1604
|
+
return { content: [{ type: "text", text: "unknown tool: " + params.name }], isError: true };
|
|
1605
|
+
}
|
|
1606
|
+
default:
|
|
1607
|
+
return { error: "unknown method " + req.method };
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function parseCmdStr(cmdStr) {
|
|
1611
|
+
const parts = [];
|
|
1612
|
+
const re = /"([^"]*)"|(\S+)/g;
|
|
1613
|
+
let m;
|
|
1614
|
+
while (m = re.exec(cmdStr)) parts.push(m[1] !== void 0 ? m[1] : m[2]);
|
|
1615
|
+
return { exe: parts[0] || "", args: parts.slice(1) };
|
|
1616
|
+
}
|
|
1617
|
+
function isCodeLikeEditor(name) {
|
|
1618
|
+
return /(^|[\\/])(code|code-insiders|cursor|windsurf|codium|vscodium|zed)(\.exe|\.cmd|\.bat)?$/i.test(name);
|
|
1619
|
+
}
|
|
1620
|
+
function hasDesktopSession() {
|
|
1621
|
+
if (process.platform !== "linux") return true;
|
|
1622
|
+
return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY || process.env.XDG_SESSION_TYPE || Object.keys(desktopEnv()).length);
|
|
1623
|
+
}
|
|
1624
|
+
function desktopEnv() {
|
|
1625
|
+
const out = {};
|
|
1626
|
+
try {
|
|
1627
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
1628
|
+
const home = process.env.HOME || "";
|
|
1629
|
+
try {
|
|
1630
|
+
const xs = readdirSync("/tmp/.X11-unix").filter((f) => /^X\d+$/.test(f)).sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
|
|
1631
|
+
if (xs.length && !out.DISPLAY) out.DISPLAY = ":" + xs[0].slice(1);
|
|
1632
|
+
} catch {
|
|
1633
|
+
}
|
|
1634
|
+
const authCands = [];
|
|
1635
|
+
if (uid != null) authCands.push(`/run/user/${uid}/gdm/Xauthority`);
|
|
1636
|
+
authCands.push(`${home}/.Xauthority`);
|
|
1637
|
+
for (const a of authCands) {
|
|
1638
|
+
try {
|
|
1639
|
+
if (a && existsSync(a)) {
|
|
1640
|
+
out.XAUTHORITY = a;
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (uid != null) {
|
|
1647
|
+
try {
|
|
1648
|
+
const ws = readdirSync(`/run/user/${uid}`).filter((f) => /^wayland-\d+$/.test(f)).sort();
|
|
1649
|
+
if (ws.length) out.WAYLAND_DISPLAY = ws[0];
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
if (existsSync(`/run/user/${uid}/bus`)) {
|
|
1654
|
+
out.XDG_RUNTIME_DIR = `/run/user/${uid}`;
|
|
1655
|
+
out.DBUS_SESSION_BUS_ADDRESS = `unix:path=/run/user/${uid}/bus`;
|
|
1656
|
+
}
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
return out;
|
|
1663
|
+
}
|
|
1664
|
+
function openFileInEditor(file, line) {
|
|
1665
|
+
return new Promise((resolve2) => {
|
|
1666
|
+
if (!file) return resolve2({ ok: false, error: "缺少 file" });
|
|
1667
|
+
const platform = process.platform;
|
|
1668
|
+
const isWin = platform === "win32";
|
|
1669
|
+
const target = file + (line ? ":" + line : "");
|
|
1670
|
+
const cmdRaw = process.env.AITI_OPEN_CMD || process.env.EDITOR || process.env.VISUAL || "";
|
|
1671
|
+
const { exe, args: cmdArgs } = cmdRaw ? parseCmdStr(cmdRaw) : { exe: "", args: [] };
|
|
1672
|
+
const defaultOpen = (f) => {
|
|
1673
|
+
if (isWin) return spawn("cmd", ["/c", "start", "", f], { detached: true, stdio: "ignore", windowsHide: true, env: { ...process.env, ...desktopEnv() } });
|
|
1674
|
+
return spawn(platform === "darwin" ? "open" : "xdg-open", [f], { detached: true, stdio: "ignore", env: { ...process.env, ...desktopEnv() } });
|
|
1675
|
+
};
|
|
1676
|
+
let child;
|
|
1677
|
+
if (exe) {
|
|
1678
|
+
const codeLike = isCodeLikeEditor(exe);
|
|
1679
|
+
const args = [...cmdArgs];
|
|
1680
|
+
if (codeLike && !args.includes("--goto")) args.push("--goto");
|
|
1681
|
+
args.push(codeLike ? target : file);
|
|
1682
|
+
child = spawn(exe, args, { detached: true, stdio: "ignore", windowsHide: true, env: { ...process.env, ...desktopEnv() }, ...isWin ? { shell: true } : {} });
|
|
1683
|
+
} else {
|
|
1684
|
+
if (!hasDesktopSession()) {
|
|
1685
|
+
return resolve2({ ok: false, error: "无桌面会话(systemd/无头服务)——后端打开不可用:请用「前端打开」(pi-web 文件标签页在线查看)或设置 AITI_OPEN_CMD 指定打开命令(如 ssh 回本地编辑器)" });
|
|
1686
|
+
}
|
|
1687
|
+
child = defaultOpen(file);
|
|
1688
|
+
}
|
|
1689
|
+
let done = false;
|
|
1690
|
+
const finish = (r) => {
|
|
1691
|
+
if (!done) {
|
|
1692
|
+
done = true;
|
|
1693
|
+
resolve2(r);
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
child.on("error", () => {
|
|
1697
|
+
if (!hasDesktopSession()) {
|
|
1698
|
+
return finish({ ok: false, error: "编辑器与系统默认打开器均不可用(无桌面会话)——请配置 AITI_OPEN_CMD 或使用前端打开" });
|
|
1699
|
+
}
|
|
1700
|
+
const c2 = defaultOpen(file);
|
|
1701
|
+
c2.on("error", (e2) => finish({ ok: false, error: String(e2 && e2.message || e2) }));
|
|
1702
|
+
c2.unref();
|
|
1703
|
+
setTimeout(() => finish({ ok: true, opened: file, via: "default" }), 300);
|
|
1704
|
+
});
|
|
1705
|
+
child.on("spawn", () => setTimeout(() => finish({ ok: true, opened: exe ? target : file, via: exe || "default" }), 300));
|
|
1706
|
+
child.unref();
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
function sseResponse(onClose, opts2 = {}) {
|
|
1710
|
+
let controllerRef = null;
|
|
1711
|
+
let closed = false;
|
|
1712
|
+
const stream = new ReadableStream({
|
|
1713
|
+
start(controller) {
|
|
1714
|
+
controllerRef = controller;
|
|
1715
|
+
},
|
|
1716
|
+
cancel() {
|
|
1717
|
+
closed = true;
|
|
1718
|
+
try {
|
|
1719
|
+
if (onClose) onClose();
|
|
1720
|
+
} catch {
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
const eventLine = opts2.event ? "event: " + opts2.event + "\n" : "";
|
|
1725
|
+
const writer = {
|
|
1726
|
+
// 返回是否写成功:流已关闭/写入异常(浏览器端断连)→ 触发 onClose 清理注册 + 返回 false,调用方(exec/interact 路由)据此快速失败而非白等超时
|
|
1727
|
+
write(obj) {
|
|
1728
|
+
try {
|
|
1729
|
+
if (controllerRef && !closed) {
|
|
1730
|
+
controllerRef.enqueue(new TextEncoder().encode(eventLine + "data: " + JSON.stringify(obj) + "\n\n"));
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
}
|
|
1735
|
+
if (!closed) {
|
|
1736
|
+
closed = true;
|
|
1737
|
+
try {
|
|
1738
|
+
if (onClose) onClose();
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
const ping = setInterval(() => {
|
|
1746
|
+
try {
|
|
1747
|
+
if (controllerRef && !closed) controllerRef.enqueue(new TextEncoder().encode(": ping\n\n"));
|
|
1748
|
+
} catch {
|
|
1749
|
+
}
|
|
1750
|
+
}, 15e3);
|
|
1751
|
+
const cleanup = () => {
|
|
1752
|
+
clearInterval(ping);
|
|
1753
|
+
};
|
|
1754
|
+
return { response: new Response(stream, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-accel-buffering": "no", ...cors } }), writer, cleanup };
|
|
1755
|
+
}
|
|
1756
|
+
async function handle(req, path, searchParams) {
|
|
1757
|
+
if (path[0] === "aiti") path = path.slice(1);
|
|
1758
|
+
const endpoint = path[0];
|
|
1759
|
+
const sub = path.slice(1)[0] || "";
|
|
1760
|
+
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
|
|
1761
|
+
if (endpoint === "bridge" && sub === "claim" && req.method === "POST") {
|
|
1762
|
+
const msg = await req.json().catch(() => null);
|
|
1763
|
+
const r = claimInstanceId(msg && msg.instanceId, msg && msg.secret, msg && msg.pageKey, msg && msg.hostUrl);
|
|
1764
|
+
const reused = !!(msg && msg.instanceId && r.instanceId === msg.instanceId && msg.secret);
|
|
1765
|
+
const portPref = portFromHostUrl(msg && msg.hostUrl);
|
|
1766
|
+
console.log(TAG + " claim → instanceId=" + r.instanceId + (reused ? "(复用)" : "(新" + (portPref ? " 端口偏好=" + portPref : "") + ")"));
|
|
1767
|
+
return json({ ok: true, instanceId: r.instanceId, secret: r.secret });
|
|
1768
|
+
}
|
|
1769
|
+
if (endpoint === "bridge" && sub === "sessions" && req.method === "GET") return json(listSessions());
|
|
1770
|
+
if (endpoint === "bridge" && sub === "ai" && path[2] === "page" && req.method === "POST") {
|
|
1771
|
+
const msg = await req.json().catch(() => null);
|
|
1772
|
+
if (!msg || !msg.instanceId || !msg.pageId) return json({ ok: false, error: "缺少 instanceId/pageId" }, 400);
|
|
1773
|
+
let hit = null;
|
|
1774
|
+
for (const [w, rec] of aiPageReg) if (rec.instanceId === String(msg.instanceId)) {
|
|
1775
|
+
hit = w;
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
if (!hit) return json({ ok: false, error: "no such ai conn:" + msg.instanceId }, 404);
|
|
1779
|
+
const baseId = String(msg.pageId).replace(/\/host$/, "");
|
|
1780
|
+
for (const [w, rec] of aiPageReg) {
|
|
1781
|
+
if (w !== hit && rec.pageId && String(rec.pageId).replace(/\/host$/, "") === baseId) aiPageReg.delete(w);
|
|
1782
|
+
}
|
|
1783
|
+
const prev = aiPageReg.get(hit) || {};
|
|
1784
|
+
const fr = Array.isArray(msg.frames) ? msg.frames : prev.frames || null;
|
|
1785
|
+
aiPageReg.set(hit, { instanceId: String(msg.instanceId), pageId: String(msg.pageId), title: msg.title || prev.title || null, url: msg.url || prev.url || null, frames: fr, device: msg.device && typeof msg.device === "object" ? msg.device : prev.device || null, deviceLabel: msg.deviceLabel || prev.deviceLabel || null, lastSeen: Date.now() });
|
|
1786
|
+
console.log(TAG + " AI 宿主页注册:" + msg.pageId + " · " + (msg.title || "") + (fr && fr.length ? " · frames=" + fr.length : "") + (msg.device ? " · " + (msg.deviceLabel || msg.device.browser) : ""));
|
|
1787
|
+
return json({ ok: true, pageId: msg.pageId });
|
|
1788
|
+
}
|
|
1789
|
+
if (endpoint === "bridge" && sub === "self" && path[2] === "page" && req.method === "POST") {
|
|
1790
|
+
const msg = await req.json().catch(() => null);
|
|
1791
|
+
if (!msg || !msg.instanceId) return json({ ok: false, error: "缺少 instanceId" }, 400);
|
|
1792
|
+
let hit = null;
|
|
1793
|
+
for (const [w, rec] of aiSelfReg) if (rec.instanceId === String(msg.instanceId)) {
|
|
1794
|
+
hit = w;
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
if (!hit) return json({ ok: false, error: "no such ai conn:" + msg.instanceId }, 404);
|
|
1798
|
+
const prev = aiSelfReg.get(hit) || {};
|
|
1799
|
+
aiSelfReg.set(hit, { instanceId: String(msg.instanceId), url: msg.url || prev.url || null, title: msg.title || prev.title || null, device: msg.device && typeof msg.device === "object" ? msg.device : prev.device || null, deviceLabel: msg.deviceLabel || prev.deviceLabel || null, lastSeen: Date.now() });
|
|
1800
|
+
bindAgentFromAiSelf(aiSelfReg.get(hit));
|
|
1801
|
+
console.log(TAG + " AI 主窗口 self 注册:" + msg.instanceId + "/ai · " + (msg.title || "") + (msg.device ? " · " + (msg.deviceLabel || msg.device.browser) : ""));
|
|
1802
|
+
return json({ ok: true, pageId: String(msg.instanceId) + "/ai" });
|
|
1803
|
+
}
|
|
1804
|
+
if (endpoint === "bridge" && sub === "self" && path[2] === "active" && req.method === "POST") {
|
|
1805
|
+
const msg = await req.json().catch(() => null);
|
|
1806
|
+
if (!msg || !msg.instanceId || !msg.sessionId) return json({ ok: false, error: "缺少 instanceId/sessionId" }, 400);
|
|
1807
|
+
bindAgentFromActive(String(msg.instanceId), String(msg.sessionId));
|
|
1808
|
+
return json({ ok: true, pageId: String(msg.instanceId) + "/ai" });
|
|
1809
|
+
}
|
|
1810
|
+
if (endpoint === "bridge" && sub === "ai" && path[2] === "frames" && req.method === "POST") {
|
|
1811
|
+
const msg = await req.json().catch(() => null);
|
|
1812
|
+
if (!msg || !msg.pageId || !Array.isArray(msg.frames)) return json({ ok: false, error: "bad request(需要 pageId/frames)" }, 400);
|
|
1813
|
+
let hit = null;
|
|
1814
|
+
for (const [w, rec2] of aiPageReg) if (rec2.pageId === String(msg.pageId)) {
|
|
1815
|
+
hit = w;
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
if (!hit) return json({ ok: false, error: "no such ai host page:" + msg.pageId }, 404);
|
|
1819
|
+
const rec = aiPageReg.get(hit);
|
|
1820
|
+
rec.frames = msg.frames;
|
|
1821
|
+
rec.lastSeen = Date.now();
|
|
1822
|
+
console.log(TAG + " AI 宿主帧树更新:" + msg.pageId + " frames=" + msg.frames.length);
|
|
1823
|
+
return json({ ok: true, frames: msg.frames.length });
|
|
1824
|
+
}
|
|
1825
|
+
if (endpoint === "bridge" && sub === "register" && req.method === "POST") {
|
|
1826
|
+
const msg = await req.json().catch(() => null);
|
|
1827
|
+
if (!msg || !validInstance(msg.instanceId)) return json({ ok: false, error: "bad instanceId" }, 401);
|
|
1828
|
+
const page = pages.get(msg.instanceId);
|
|
1829
|
+
if (!page) return json({ ok: false, error: "no active page" }, 404);
|
|
1830
|
+
const list = Array.isArray(msg.frames) ? msg.frames : [msg.frameId].filter(Boolean);
|
|
1831
|
+
page.frames.clear();
|
|
1832
|
+
page.frameInfo.clear();
|
|
1833
|
+
for (const f of list) {
|
|
1834
|
+
if (typeof f === "string" && f) {
|
|
1835
|
+
page.frames.add(f);
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (f && f.id) {
|
|
1839
|
+
page.frames.add(String(f.id));
|
|
1840
|
+
if (f.title || f.url || f.name || f.src || f.parentId) {
|
|
1841
|
+
page.frameInfo.set(String(f.id), { title: f.title || null, url: f.url || null, name: f.name || null, src: f.src || null, parentId: f.parentId || null });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
console.log(TAG + " register frames →", msg.instanceId, [...page.frames].join(","));
|
|
1846
|
+
return json({ ok: true, frames: [...page.frames] });
|
|
1847
|
+
}
|
|
1848
|
+
if (endpoint === "bridge" && sub === "agent" && path[2] === "tools" && path[3] === "call" && req.method === "POST") {
|
|
1849
|
+
const msg = await req.json().catch(() => null);
|
|
1850
|
+
if (!msg || !msg.tool) return json({ ok: false, error: "bad request(需要 tool)" }, 400);
|
|
1851
|
+
const agentId2 = msg.agentId ? String(msg.agentId) : "pi";
|
|
1852
|
+
const sessionId = msg.sessionId ? String(msg.sessionId) : null;
|
|
1853
|
+
const { a: ag, key } = ensureAgent(agentId2, sessionId, null);
|
|
1854
|
+
if (sessionId) ag.registered = true;
|
|
1855
|
+
ag.source = msg.source === "mcp" ? "mcp" : "extension";
|
|
1856
|
+
if (msg.cwd) ag.cwd = String(msg.cwd);
|
|
1857
|
+
const session = { agentId: agentId2, agentKey: key, id: sessionId || key.split("/").slice(1).join("/"), source: ag.source };
|
|
1858
|
+
try {
|
|
1859
|
+
const r = await callTool(String(msg.tool), msg.args && typeof msg.args === "object" ? msg.args : {}, session, null, msg.cancelId ? String(msg.cancelId) : null);
|
|
1860
|
+
if (r && r.error) return json({ ok: false, error: r.error });
|
|
1861
|
+
const text = r && r.content && r.content[0] && r.content[0].text || JSON.stringify(r);
|
|
1862
|
+
return json({ ok: true, agentKey: key, result: text, isError: !!(r && r.isError) });
|
|
1863
|
+
} catch (e) {
|
|
1864
|
+
return json({ ok: false, error: String(e && e.message || e) });
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (endpoint === "bridge" && sub === "agent" && path[2] === "register" && req.method === "POST") {
|
|
1868
|
+
const msg = await req.json().catch(() => null);
|
|
1869
|
+
if (!msg || !msg.agentId) return json({ ok: false, error: "bad request(需要 agentId)" }, 400);
|
|
1870
|
+
const { a: ag, key } = ensureAgent(String(msg.agentId), msg.sessionId ? String(msg.sessionId) : null, null);
|
|
1871
|
+
ag.registered = true;
|
|
1872
|
+
if (msg.name) ag.name = String(msg.name);
|
|
1873
|
+
if (msg.cwd) ag.cwd = String(msg.cwd);
|
|
1874
|
+
if (msg.source) ag.source = String(msg.source);
|
|
1875
|
+
if (msg.pageId) ag.boundPageId = String(msg.pageId);
|
|
1876
|
+
if (Array.isArray(msg.subEvents)) ag.subEvents = new Set(msg.subEvents.map(String));
|
|
1877
|
+
return json({ ok: true, agentKey: key, agentId: ag.agentId, sessionId: ag.sessionId, registered: true, subEvents: [...ag.subEvents], inboxPending: ag.outbox.filter((m) => m.status !== "consumed").length });
|
|
1878
|
+
}
|
|
1879
|
+
if (endpoint === "bridge" && sub === "agent" && path[2] === "sse" && req.method === "GET") {
|
|
1880
|
+
const key = searchParams.get("agentKey") || "";
|
|
1881
|
+
const parts = key.split("/");
|
|
1882
|
+
const { a, key: resolvedKey } = ensureAgent(parts[0], parts.slice(1).join("/") || null, null);
|
|
1883
|
+
if (!a) return json({ ok: false, error: "unknown agent session" }, 400);
|
|
1884
|
+
const { response, writer, cleanup } = sseResponse(() => {
|
|
1885
|
+
if (a.agentStream === writer) {
|
|
1886
|
+
a.agentStream = null;
|
|
1887
|
+
a.conn = Math.max(0, (a.conn || 1) - 1);
|
|
1888
|
+
markOffline(resolvedKey);
|
|
1889
|
+
}
|
|
1890
|
+
cleanup();
|
|
1891
|
+
});
|
|
1892
|
+
a.agentStream = writer;
|
|
1893
|
+
a.conn = (a.conn || 0) + 1;
|
|
1894
|
+
console.log(TAG + " agent SSE 已建立:" + resolvedKey + "(P3 推送通道)");
|
|
1895
|
+
return response;
|
|
1896
|
+
}
|
|
1897
|
+
if (endpoint === "bridge" && sub === "agent" && path[2] === "answer" && req.method === "POST") {
|
|
1898
|
+
const msg = await req.json().catch(() => null);
|
|
1899
|
+
if (!msg || !msg.agentKey || !msg.text) return json({ ok: false, error: "bad request(需要 agentKey/text)" }, 400);
|
|
1900
|
+
const parts = String(msg.agentKey).split("/");
|
|
1901
|
+
const { a, key: resolvedKey } = ensureAgent(parts[0], parts.slice(1).join("/") || null, null);
|
|
1902
|
+
for (const om of a.outbox) {
|
|
1903
|
+
if (om.status !== "consumed" && (om.kind === "user_message" || om.kind === "user_answer" || om.kind === "chat")) {
|
|
1904
|
+
om.status = "consumed";
|
|
1905
|
+
om.ackedAt = Date.now();
|
|
1906
|
+
appendMsgEvent(resolvedKey, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: om.id, status: "consumed", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1907
|
+
for (const w of aiConns) w.write({ type: "msg_status", agentKey: resolvedKey, msgId: om.id, status: "consumed" });
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const m = { id: randomBytes(4).toString("hex"), kind: msg.kind || "agent_answer", from: "agent", agentId: parts[0], pageId: null, text: String(msg.text).slice(0, 4e3), payload: msg.payload || null, at: (/* @__PURE__ */ new Date()).toISOString(), status: "consumed" };
|
|
1911
|
+
appendMsgEvent(resolvedKey, { type: "message", id: m.id, parentId: null, timestamp: m.at, message: m });
|
|
1912
|
+
for (const w of aiConns) w.write({ type: "agent_answer", agentKey: resolvedKey, agentId: parts[0], text: m.text, at: m.at });
|
|
1913
|
+
console.log(TAG + " agent 回答 → " + resolvedKey + " · " + String(m.text).slice(0, 80) + "(已消费待处理消息 " + a.outbox.filter((x) => x.status === "consumed").length + " 条)");
|
|
1914
|
+
return json({ ok: true, message: m });
|
|
1915
|
+
}
|
|
1916
|
+
if (endpoint === "bridge" && sub === "agents" && req.method === "GET") return json({ ok: true, agents: listAgentSessions() });
|
|
1917
|
+
if (endpoint === "bridge" && sub === "sse") {
|
|
1918
|
+
const instanceId = searchParams.get("instanceId") || searchParams.get("token") || LEGACY_TOKEN;
|
|
1919
|
+
const role = searchParams.get("role") || "page";
|
|
1920
|
+
const secret = searchParams.get("secret") || null;
|
|
1921
|
+
if (role !== "ai" && !validInstance(instanceId)) return json({ ok: false, error: "bad instanceId" }, 401);
|
|
1922
|
+
if (!checkSecret(instanceId, secret)) return json({ ok: false, error: "bad secret" }, 401);
|
|
1923
|
+
const { response, writer, cleanup: sseCleanup } = sseResponse(() => cleanup());
|
|
1924
|
+
const cleanup = () => {
|
|
1925
|
+
if (role === "ai") {
|
|
1926
|
+
aiConns.delete(writer);
|
|
1927
|
+
aiPageReg.delete(writer);
|
|
1928
|
+
aiSelfReg.delete(writer);
|
|
1929
|
+
var deadPageId = instanceId + "/ai";
|
|
1930
|
+
for (var [, fbList] of agentFallbackPageIds) {
|
|
1931
|
+
for (var fi = fbList.length - 1; fi >= 0; fi--) {
|
|
1932
|
+
if (fbList[fi].pageId === deadPageId) fbList.splice(fi, 1);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
for (var [, ag] of agents) {
|
|
1936
|
+
if (ag.activeBoundPageId === deadPageId) ag.activeBoundPageId = null;
|
|
1937
|
+
}
|
|
1938
|
+
sseCleanup();
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (pages.get(instanceId)?.writer === writer) {
|
|
1942
|
+
const rec = idRegistry.get(instanceId);
|
|
1943
|
+
if (rec) rec.lastSeen = Date.now();
|
|
1944
|
+
pages.delete(instanceId);
|
|
1945
|
+
}
|
|
1946
|
+
sseCleanup();
|
|
1947
|
+
};
|
|
1948
|
+
if (role === "ai") {
|
|
1949
|
+
aiConns.add(writer);
|
|
1950
|
+
aiPageReg.set(writer, { instanceId, pageId: null, title: null, url: null, frames: null, lastSeen: Date.now() });
|
|
1951
|
+
aiSelfReg.set(writer, { instanceId, url: searchParams.get("url") || null, title: searchParams.get("title") || null, lastSeen: Date.now() });
|
|
1952
|
+
bindAgentFromAiSelf(aiSelfReg.get(writer));
|
|
1953
|
+
console.log(TAG + " AI 控制台已连接:instanceId=" + instanceId);
|
|
1954
|
+
} else {
|
|
1955
|
+
const existing = pages.get(instanceId);
|
|
1956
|
+
if (existing) {
|
|
1957
|
+
try {
|
|
1958
|
+
existing.writer.end();
|
|
1959
|
+
} catch {
|
|
1960
|
+
}
|
|
1961
|
+
pages.delete(instanceId);
|
|
1962
|
+
}
|
|
1963
|
+
const initialFrame = searchParams.get("frameId") || "host";
|
|
1964
|
+
let pageDevice = null, pageDeviceLabel = null;
|
|
1965
|
+
try {
|
|
1966
|
+
const d = searchParams.get("device");
|
|
1967
|
+
if (d) pageDevice = JSON.parse(d);
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
}
|
|
1970
|
+
pageDeviceLabel = searchParams.get("deviceLabel") || null;
|
|
1971
|
+
pages.set(instanceId, {
|
|
1972
|
+
writer,
|
|
1973
|
+
url: searchParams.get("url") || null,
|
|
1974
|
+
title: searchParams.get("title") || null,
|
|
1975
|
+
frameworks: searchParams.get("frameworks") || null,
|
|
1976
|
+
frames: /* @__PURE__ */ new Set([initialFrame]),
|
|
1977
|
+
frameInfo: /* @__PURE__ */ new Map(),
|
|
1978
|
+
device: pageDevice,
|
|
1979
|
+
deviceLabel: pageDeviceLabel
|
|
1980
|
+
});
|
|
1981
|
+
const rec = idRegistry.get(instanceId);
|
|
1982
|
+
if (rec) rec.lastSeen = Date.now();
|
|
1983
|
+
console.log(TAG + " 页面已连接:instanceId=" + instanceId + " frame=" + initialFrame);
|
|
1984
|
+
}
|
|
1985
|
+
writer.write({ type: "ready", bridge: "/api/extensions/aiti-devtools/mcp", instanceId, token: LEGACY_TOKEN, role });
|
|
1986
|
+
return response;
|
|
1987
|
+
}
|
|
1988
|
+
if (endpoint === "bridge" && sub === "attach-bus" && req.method === "POST") {
|
|
1989
|
+
if (!busPublish) return json({ ok: false, error: "bus not wired" }, 501);
|
|
1990
|
+
const msg = await req.json().catch(() => null);
|
|
1991
|
+
const instanceId = msg && (msg.instanceId || msg.token) || LEGACY_TOKEN;
|
|
1992
|
+
const role = msg && msg.role || "page";
|
|
1993
|
+
if (role !== "ai" && !validInstance(instanceId)) return json({ ok: false, error: "bad instanceId" }, 401);
|
|
1994
|
+
if (!checkSecret(instanceId, msg && msg.secret || null)) return json({ ok: false, error: "bad secret" }, 401);
|
|
1995
|
+
const topic = "bridge-" + instanceId;
|
|
1996
|
+
const writer = {
|
|
1997
|
+
viaBus: true,
|
|
1998
|
+
lastSeen: Date.now(),
|
|
1999
|
+
// 帧契约:{ type: "bridge-<instanceId>", msg: <桥原消息> }——帧 type 即 topic(与
|
|
2000
|
+
// 客户端订阅白名单一致,publish(type, data) 的 data 展开会覆盖同名键,故桥消息只能放 msg);
|
|
2001
|
+
// 永远返回 true:投递由总线管理(白名单过滤 + SSE 转发);断线重连由客户端订阅状态回调驱动
|
|
2002
|
+
write(obj) {
|
|
2003
|
+
try {
|
|
2004
|
+
busPublish(topic, { msg: obj });
|
|
2005
|
+
return true;
|
|
2006
|
+
} catch {
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
end() {
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
if (role === "ai") {
|
|
2014
|
+
aiConns.add(writer);
|
|
2015
|
+
aiPageReg.set(writer, { instanceId, pageId: null, title: msg && msg.title || null, url: msg && msg.url || null, frames: null, lastSeen: Date.now() });
|
|
2016
|
+
aiSelfReg.set(writer, { instanceId, url: msg && msg.url || null, title: msg && msg.title || null, lastSeen: Date.now() });
|
|
2017
|
+
bindAgentFromAiSelf(aiSelfReg.get(writer));
|
|
2018
|
+
console.log(TAG + " AI 控制台已连接(总线):instanceId=" + instanceId);
|
|
2019
|
+
} else {
|
|
2020
|
+
const existing = pages.get(instanceId);
|
|
2021
|
+
if (existing) {
|
|
2022
|
+
try {
|
|
2023
|
+
existing.writer.end();
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
pages.delete(instanceId);
|
|
2027
|
+
}
|
|
2028
|
+
let pageDevice = null;
|
|
2029
|
+
try {
|
|
2030
|
+
if (msg && msg.device) pageDevice = typeof msg.device === "string" ? JSON.parse(msg.device) : msg.device;
|
|
2031
|
+
} catch (e) {
|
|
2032
|
+
}
|
|
2033
|
+
pages.set(instanceId, {
|
|
2034
|
+
writer,
|
|
2035
|
+
url: msg && msg.url || null,
|
|
2036
|
+
title: msg && msg.title || null,
|
|
2037
|
+
frameworks: msg && msg.frameworks || null,
|
|
2038
|
+
frames: /* @__PURE__ */ new Set([msg && msg.frameId || "host"]),
|
|
2039
|
+
frameInfo: /* @__PURE__ */ new Map(),
|
|
2040
|
+
device: pageDevice,
|
|
2041
|
+
deviceLabel: msg && msg.deviceLabel || null
|
|
2042
|
+
});
|
|
2043
|
+
const rec = idRegistry.get(instanceId);
|
|
2044
|
+
if (rec) rec.lastSeen = Date.now();
|
|
2045
|
+
console.log(TAG + " 页面已连接(总线):instanceId=" + instanceId);
|
|
2046
|
+
}
|
|
2047
|
+
writer.write({ type: "ready", bridge: "/api/extensions/aiti-devtools/mcp", instanceId, token: LEGACY_TOKEN, role, via: "bus" });
|
|
2048
|
+
return json({ ok: true, topic, via: "bus" });
|
|
2049
|
+
}
|
|
2050
|
+
if (endpoint === "bridge" && sub === "bus-heartbeat" && req.method === "POST") {
|
|
2051
|
+
const msg = await req.json().catch(() => null);
|
|
2052
|
+
const instanceId = msg && (msg.instanceId || msg.token) || LEGACY_TOKEN;
|
|
2053
|
+
const role = msg && msg.role || "page";
|
|
2054
|
+
if (!checkSecret(instanceId, msg && msg.secret || null)) return json({ ok: false, error: "bad secret" }, 401);
|
|
2055
|
+
if (role === "ai") {
|
|
2056
|
+
for (const w of aiConns) {
|
|
2057
|
+
if (w.viaBus && aiSelfReg.get(w)?.instanceId === instanceId) {
|
|
2058
|
+
w.lastSeen = Date.now();
|
|
2059
|
+
return json({ ok: true });
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return json({ ok: false, error: "unknown conn" }, 404);
|
|
2063
|
+
}
|
|
2064
|
+
const p = pages.get(instanceId);
|
|
2065
|
+
if (!p || !p.writer.viaBus) return json({ ok: false, error: "unknown conn" }, 404);
|
|
2066
|
+
p.writer.lastSeen = Date.now();
|
|
2067
|
+
const rec = idRegistry.get(instanceId);
|
|
2068
|
+
if (rec) rec.lastSeen = Date.now();
|
|
2069
|
+
return json({ ok: true });
|
|
2070
|
+
}
|
|
2071
|
+
if (endpoint === "bridge" && sub === "interaction" && req.method === "POST") {
|
|
2072
|
+
const msg = await req.json().catch(() => null);
|
|
2073
|
+
if (!msg || !msg.requestId || msg.action !== "pause" && msg.action !== "resume") return json({ ok: false, error: "bad request" }, 400);
|
|
2074
|
+
const { instanceId } = parsePageId(msg.pageId);
|
|
2075
|
+
if (!validInstance(instanceId || msg.token)) return json({ ok: false }, 401);
|
|
2076
|
+
if (!checkSecret(instanceId || msg.token, msg.secret)) return json({ ok: false, error: "bad secret" }, 401);
|
|
2077
|
+
const r = msg.action === "pause" ? pauseInteraction(msg.requestId) : resumeInteraction(msg.requestId);
|
|
2078
|
+
for (const w of aiConns) w.write({ type: "event", from: "page", event: "interaction_" + msg.action, payload: { requestId: msg.requestId, remainingMs: r.remainingMs } });
|
|
2079
|
+
return json({ ok: r.ok, ...r });
|
|
2080
|
+
}
|
|
2081
|
+
if (endpoint === "bridge" && sub === "result" && req.method === "POST") {
|
|
2082
|
+
const msg = await req.json().catch(() => null);
|
|
2083
|
+
if (!msg) return json({ ok: false }, 400);
|
|
2084
|
+
const { instanceId } = parsePageId(msg.pageId);
|
|
2085
|
+
if (!validInstance(instanceId || msg.token)) return json({ ok: false }, 401);
|
|
2086
|
+
if (!checkSecret(instanceId || msg.token, msg.secret)) return json({ ok: false, error: "bad secret" }, 401);
|
|
2087
|
+
try {
|
|
2088
|
+
const r0 = msg.result;
|
|
2089
|
+
if (r0 && typeof r0 === "object" && typeof r0.dataUrl === "string" && r0.dataUrl.length > 12e4) {
|
|
2090
|
+
const ext = r0.format === "png" ? "png" : "jpg";
|
|
2091
|
+
const dir = join(dataDir, "aiti-bridge", "shots");
|
|
2092
|
+
mkdirSync(dir, { recursive: true });
|
|
2093
|
+
const file = join(dir, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-") + "-" + String(msg.requestId).replace(/[^\w-]/g, "").slice(0, 12) + "." + ext);
|
|
2094
|
+
writeFileSync(file, Buffer.from(r0.dataUrl.slice(r0.dataUrl.indexOf(",") + 1), "base64"));
|
|
2095
|
+
msg.result = { file, width: r0.width, height: r0.height, format: r0.format, bytes: r0.bytes || 0, note: "截屏已存文件(dataUrl 过大不内联);前端文件标签页可直接打开查看" };
|
|
2096
|
+
console.log(TAG + " 截屏落盘:" + file + "(" + Math.round((r0.bytes || 0) / 1024) + "KB)");
|
|
2097
|
+
}
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
console.warn(TAG + " 截屏落盘失败(退回内联):", e.message);
|
|
2100
|
+
}
|
|
2101
|
+
const r = pendingRequests.get(msg.requestId);
|
|
2102
|
+
if (r) {
|
|
2103
|
+
pendingRequests.delete(msg.requestId);
|
|
2104
|
+
try {
|
|
2105
|
+
if (r.signal && r.onAbort) r.signal.removeEventListener("abort", r.onAbort);
|
|
2106
|
+
} catch {
|
|
2107
|
+
}
|
|
2108
|
+
if (r.mcpReqId) mcpReqMap.delete(r.mcpReqId);
|
|
2109
|
+
if (r.cancelId) cancelIdMap.delete(r.cancelId);
|
|
2110
|
+
const ist = interactionTimers.get(msg.requestId);
|
|
2111
|
+
if (ist) {
|
|
2112
|
+
clearTimeout(ist.timer);
|
|
2113
|
+
interactionTimers.delete(msg.requestId);
|
|
2114
|
+
if (msg.result && typeof msg.result === "object" && !("pausedMs" in msg.result)) msg.result.pausedMs = ist.pausedMs;
|
|
2115
|
+
}
|
|
2116
|
+
r.resolve({ ok: msg.ok, result: msg.result });
|
|
2117
|
+
console.log(TAG + " ← 结果回传 ok=" + msg.ok + (r.agentId ? " → agent=" + r.agentId : ""));
|
|
2118
|
+
if (r.agentId && msg.ok) deliverUserAnswer(r.agentId, msg.result);
|
|
2119
|
+
} else {
|
|
2120
|
+
console.log(TAG + " ⚠ 结果回传但无 pending:" + msg.requestId);
|
|
2121
|
+
return new Response("ok", { headers: cors });
|
|
2122
|
+
}
|
|
2123
|
+
for (const w of aiConns) w.write({ type: "result", requestId: msg.requestId, ok: msg.ok, result: msg.result, pageId: msg.pageId });
|
|
2124
|
+
return new Response("ok", { headers: cors });
|
|
2125
|
+
}
|
|
2126
|
+
if (endpoint === "bridge" && sub === "cancel" && req.method === "POST") {
|
|
2127
|
+
const msg = await req.json().catch(() => null);
|
|
2128
|
+
if (!msg) return json({ ok: false, error: "bad request" }, 400);
|
|
2129
|
+
const internalId = msg.requestId ? String(msg.requestId) : msg.cancelId ? cancelIdMap.get(String(msg.cancelId)) : null;
|
|
2130
|
+
if (!internalId) return json({ ok: false, error: "unknown request", cancelled: false });
|
|
2131
|
+
return json({ ok: cancelPending(internalId, "已取消(客户端请求)"), cancelled: true });
|
|
2132
|
+
}
|
|
2133
|
+
if (endpoint === "bridge" && sub === "push" && req.method === "POST") {
|
|
2134
|
+
const msg = await req.json().catch(() => null);
|
|
2135
|
+
if (!msg || !msg.text) return json({ ok: false, error: "bad request(需要 text)" }, 400);
|
|
2136
|
+
return json(pushToAgents(msg.to, String(msg.text), msg.kind || "user_message", msg.payload || {}, msg.ref || null));
|
|
2137
|
+
}
|
|
2138
|
+
if (endpoint === "bridge" && sub === "picked" && req.method === "POST") {
|
|
2139
|
+
const msg = await req.json().catch(() => null);
|
|
2140
|
+
const list = msg && Array.isArray(msg.candidates) ? msg.candidates : null;
|
|
2141
|
+
if (!list) return json({ ok: false, error: "bad request(需要 candidates 数组)" }, 400);
|
|
2142
|
+
const key = String(msg && msg.to || "__all__");
|
|
2143
|
+
pickedReports.set(key, list.filter((c) => c && c.tag));
|
|
2144
|
+
persistPicked();
|
|
2145
|
+
return json({ ok: true, count: pickedReports.get(key).length, key });
|
|
2146
|
+
}
|
|
2147
|
+
if (endpoint === "bridge" && sub === "picked" && req.method === "GET") {
|
|
2148
|
+
const key = searchParams.get("agentKey") || "__all__";
|
|
2149
|
+
const list = pickedReports.get(key) ?? pickedReports.get("__all__") ?? [];
|
|
2150
|
+
return json({ ok: true, key, count: list.length, candidates: list });
|
|
2151
|
+
}
|
|
2152
|
+
if (endpoint === "bridge" && sub === "ai" && path[2] === "event" && req.method === "POST") {
|
|
2153
|
+
const msg = await req.json().catch(() => null);
|
|
2154
|
+
if (!msg || !msg.event) return json({ ok: false, error: "bad request(需要 event)" }, 400);
|
|
2155
|
+
const ev = String(msg.event);
|
|
2156
|
+
if (ev === "aiti_consumed") {
|
|
2157
|
+
const tags = Array.isArray(msg.payload?.tags) ? msg.payload.tags.map(String) : null;
|
|
2158
|
+
for (const [k, list] of pickedReports) {
|
|
2159
|
+
const next = tags ? list.filter((c) => !tags.includes(String(c.tag))) : [];
|
|
2160
|
+
if (next.length !== list.length) {
|
|
2161
|
+
pickedReports.set(k, next);
|
|
2162
|
+
persistPicked();
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return json(pushAiEvent(ev, msg.payload || {}, "host-rest"));
|
|
2167
|
+
}
|
|
2168
|
+
if (endpoint === "bridge" && sub === "inbox-status" && req.method === "GET") {
|
|
2169
|
+
const input = searchParams.get("agentId") || "";
|
|
2170
|
+
const key = input.includes("/") ? input : resolvePushTargets(input)[0] || "";
|
|
2171
|
+
const a = key ? agents.get(key) : null;
|
|
2172
|
+
if (!a) return json({ ok: false, error: "unknown agent session" }, 404);
|
|
2173
|
+
return json({ ok: true, agentKey: key, agentId: a.agentId, sessionId: a.sessionId, queueSize: a.outbox.filter((x) => x.status !== "consumed").length, messages: a.outbox.slice(-50).map((m) => ({ id: m.id, kind: m.kind, from: m.from, text: m.text ?? null, at: m.at, status: m.status })) });
|
|
2174
|
+
}
|
|
2175
|
+
if (endpoint === "bridge" && sub === "messages" && req.method === "GET") {
|
|
2176
|
+
const input = searchParams.get("agentId") || "";
|
|
2177
|
+
const key = input.includes("/") ? input : resolvePushTargets(input)[0] || "";
|
|
2178
|
+
const limit = Math.min(parseInt(searchParams.get("limit"), 10) || 50, 200);
|
|
2179
|
+
const before = searchParams.get("before") || "";
|
|
2180
|
+
const since = searchParams.get("since") || "";
|
|
2181
|
+
const rows = key ? readMsgSession(key).sort((x, y) => String(x.at).localeCompare(String(y.at))) : [];
|
|
2182
|
+
let filtered = rows;
|
|
2183
|
+
if (since) {
|
|
2184
|
+
const t = Date.parse(since);
|
|
2185
|
+
if (!isNaN(t)) filtered = filtered.filter((r) => Date.parse(r.at) >= t);
|
|
2186
|
+
} else if (before) filtered = filtered.filter((r) => String(r.at) < before);
|
|
2187
|
+
const msgs = filtered.slice(-limit).map((m) => {
|
|
2188
|
+
const ak = m.agentKey || key || (m.agentId ? m.agentId + (m.sessionId ? "/" + m.sessionId : "") : "");
|
|
2189
|
+
return { id: m.id, kind: m.kind, from: m.from, agentKey: ak, agentId: m.agentId || (ak ? ak.split("/")[0] : null), sessionId: m.sessionId || (ak && ak.includes("/") ? ak.split("/").slice(1).join("/") : null) || null, pageId: m.pageId || m.payload && m.payload.pageId || null, ref: m.ref ?? null, text: m.text ?? null, detail: m.detail ?? null, at: m.at, status: m.status || "pending" };
|
|
2190
|
+
});
|
|
2191
|
+
return json({ ok: true, agentKey: key, total: rows.length, since, messages: msgs });
|
|
2192
|
+
}
|
|
2193
|
+
if (endpoint === "bridge" && sub === "event" && req.method === "POST") {
|
|
2194
|
+
const msg = await req.json().catch(() => null);
|
|
2195
|
+
if (!msg) return json({ ok: false }, 400);
|
|
2196
|
+
const { instanceId } = parsePageId(msg.pageId);
|
|
2197
|
+
const aiRegistered = typeof msg.pageId === "string" && [...aiPageReg.values()].some((r) => r.pageId === msg.pageId);
|
|
2198
|
+
if (!aiRegistered && !validInstance(instanceId || msg.token)) return json({ ok: false }, 401);
|
|
2199
|
+
if (!checkSecret(aiRegistered ? instanceId : instanceId || msg.token, msg.secret)) return json({ ok: false, error: "bad secret" }, 401);
|
|
2200
|
+
for (const w of aiConns) w.write({ type: "event", from: "page", ...msg });
|
|
2201
|
+
const pageInfo = { pageId: msg.pageId || null, url: msg.payload && msg.payload.url || null };
|
|
2202
|
+
if (msg.type === "page_error") {
|
|
2203
|
+
pushEventToSubscribers("page_error", msg.payload || {}, pageInfo);
|
|
2204
|
+
console.log(TAG + " page_error 事件 → 订阅分发(type=" + msg.type + " payload=" + JSON.stringify(msg.payload || {}).slice(0, 120) + ")");
|
|
2205
|
+
}
|
|
2206
|
+
if (msg.type === "page_log" || msg.type === "page_network") {
|
|
2207
|
+
return json({ ok: false, error: "page_log/page_network 不再实时上报;请用 page_logs/page_network 工具按需拉取" }, 410);
|
|
2208
|
+
}
|
|
2209
|
+
if (msg.type === "open_file") {
|
|
2210
|
+
const payload = msg.payload || {};
|
|
2211
|
+
console.log(TAG + " open_file →", payload.file + (payload.line ? ":" + payload.line : ""));
|
|
2212
|
+
return json(await openFileInEditor(payload.file, payload.line));
|
|
2213
|
+
}
|
|
2214
|
+
return json({ ok: true, type: msg.type });
|
|
2215
|
+
}
|
|
2216
|
+
if (endpoint === "mcp") {
|
|
2217
|
+
for (const [sid2, s] of mcpSessions) if (!s.open && mcpSessionExpired(s)) mcpSessions.delete(sid2);
|
|
2218
|
+
if (req.method === "GET") {
|
|
2219
|
+
const sid2 = req.headers.get("mcp-session-id") || req.headers.get("Mcp-Session-Id") || "";
|
|
2220
|
+
const session2 = sid2 ? mcpSessions.get(sid2) : null;
|
|
2221
|
+
if (!session2) return json({ ok: false, error: "bad or missing Mcp-Session-Id" }, 400);
|
|
2222
|
+
if (session2.open) return json({ ok: false, error: "stream already open" }, 409);
|
|
2223
|
+
session2.open = true;
|
|
2224
|
+
session2.lastSeen = Date.now();
|
|
2225
|
+
const { response, writer, cleanup } = sseResponse(() => {
|
|
2226
|
+
session2.open = false;
|
|
2227
|
+
session2.lastSeen = Date.now();
|
|
2228
|
+
session2.writer = null;
|
|
2229
|
+
const ak = session2.agentKey || (session2.agentId ? session2.agentId + "/" + session2.id : "");
|
|
2230
|
+
if (ak) {
|
|
2231
|
+
const { a: ag } = ensureAgent(ak.split("/")[0], ak.split("/").slice(1).join("/") || null, null);
|
|
2232
|
+
if (ag) {
|
|
2233
|
+
ag.conn = Math.max(0, (ag.conn || 1) - 1);
|
|
2234
|
+
if (ag.stream === writer) ag.stream = null;
|
|
2235
|
+
markOffline(ak);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
cleanup();
|
|
2239
|
+
}, { event: "message" });
|
|
2240
|
+
session2.writer = writer;
|
|
2241
|
+
if (session2.agentId) {
|
|
2242
|
+
const { a: ag } = ensureAgent(session2.agentId, session2.id, session2);
|
|
2243
|
+
if (ag) {
|
|
2244
|
+
ag.transportKind = "streamable";
|
|
2245
|
+
ag.conn = (ag.conn || 0) + 1;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
console.log(TAG + " streamable GET 流已建立 session=" + sid2.slice(0, 8) + " agent=" + (session2.agentId || "?"));
|
|
2249
|
+
writer.write({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
2250
|
+
return response;
|
|
2251
|
+
}
|
|
2252
|
+
const msg = await req.json().catch(() => null);
|
|
2253
|
+
if (!msg) return json({ ok: false }, 400);
|
|
2254
|
+
if (msg.id !== void 0 && !msg.method && pendingSampling.has(String(msg.id))) {
|
|
2255
|
+
const p = pendingSampling.get(String(msg.id));
|
|
2256
|
+
const ok = !(msg.error || msg.result && msg.result.isError);
|
|
2257
|
+
p.done(ok, msg.error ? { error: String(msg.error.message || msg.error.code || "sampling 拒绝") } : msg.result || {});
|
|
2258
|
+
console.log(TAG + " sampling 响应回传 id=" + String(msg.id).slice(0, 12) + " agent=" + p.agentId + " ok=" + ok);
|
|
2259
|
+
return new Response(null, { status: 202 });
|
|
2260
|
+
}
|
|
2261
|
+
const sid = req.headers.get("mcp-session-id") || req.headers.get("Mcp-Session-Id") || "";
|
|
2262
|
+
let session = sid ? mcpSessions.get(sid) || null : null;
|
|
2263
|
+
const isInit = msg.method === "initialize";
|
|
2264
|
+
if (isInit) {
|
|
2265
|
+
if (!session) {
|
|
2266
|
+
const newId = randomBytes(16).toString("hex");
|
|
2267
|
+
session = { id: newId, writer: null, agentId: null, clientInfo: null, capabilities: null, lastSeen: Date.now(), open: false, source: "mcp" };
|
|
2268
|
+
mcpSessions.set(newId, session);
|
|
2269
|
+
}
|
|
2270
|
+
const p = msg.params || {};
|
|
2271
|
+
session.clientInfo = p.clientInfo || null;
|
|
2272
|
+
session.capabilities = p.capabilities || null;
|
|
2273
|
+
session.agentId = p.clientInfo && p.clientInfo.name || session.agentId || null;
|
|
2274
|
+
session.clientInfo = p.clientInfo || session.clientInfo;
|
|
2275
|
+
session.lastSeen = Date.now();
|
|
2276
|
+
console.log(TAG + " initialize → session=" + session.id.slice(0, 8) + " client=" + (p.clientInfo ? p.clientInfo.name + " " + (p.clientInfo.version || "") : "?") + " transport=" + (sid ? "streamable" : "session-new") + " caps=" + JSON.stringify(p.capabilities || {}).slice(0, 120));
|
|
2277
|
+
} else if (session) {
|
|
2278
|
+
session.lastSeen = Date.now();
|
|
2279
|
+
}
|
|
2280
|
+
if (isInit && session && session.agentId) {
|
|
2281
|
+
const { a: ag, key } = ensureAgent(session.agentId, session.id, session);
|
|
2282
|
+
session.agentKey = key;
|
|
2283
|
+
if (ag) ag.transportKind = sid ? "streamable" : "stateless";
|
|
2284
|
+
}
|
|
2285
|
+
const extraHeaders = isInit && session ? { "mcp-session-id": session.id } : {};
|
|
2286
|
+
const respond = async (result) => {
|
|
2287
|
+
if (msg.id !== void 0) return json({ jsonrpc: "2.0", id: msg.id, result }, 200, extraHeaders);
|
|
2288
|
+
return new Response(null, { status: 202, headers: extraHeaders });
|
|
2289
|
+
};
|
|
2290
|
+
const r = handleMcpRequest(msg, session);
|
|
2291
|
+
if (r && typeof r.then === "function") return respond(await r);
|
|
2292
|
+
return respond(r);
|
|
2293
|
+
}
|
|
2294
|
+
return new Response("not found: " + endpoint + "/" + sub, { status: 404 });
|
|
2295
|
+
}
|
|
2296
|
+
function callTool(name, args = {}, session = null, signal = null, cancelId = null) {
|
|
2297
|
+
const r = handleMcpRequest({ method: "tools/call", params: { name, arguments: args } }, session, signal, cancelId);
|
|
2298
|
+
if (r && typeof r.then === "function") return r;
|
|
2299
|
+
return Promise.resolve(r);
|
|
2300
|
+
}
|
|
2301
|
+
const bindAgentFromAiSelfReverse = (agentKey2, agent) => {
|
|
2302
|
+
const agSid = agent.sessionId || String(agentKey2).split("/").slice(1).join("/");
|
|
2303
|
+
if (!agSid || agent.activeBoundPageId) return;
|
|
2304
|
+
for (const [, rec] of aiSelfReg) {
|
|
2305
|
+
const m = String(rec.url || "").match(/[?&]session=([^&#]+)/);
|
|
2306
|
+
if (!m) continue;
|
|
2307
|
+
const sid = decodeURIComponent(m[1]);
|
|
2308
|
+
if (sid === agSid) {
|
|
2309
|
+
agent.boundPageId = String(rec.instanceId) + "/ai";
|
|
2310
|
+
console.log(TAG + " agent 注册时反向绑定 AI 主窗口:" + agentKey2 + " → " + agent.boundPageId);
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
const registerAgent2 = (key, opts2 = {}) => {
|
|
2316
|
+
const k = String(key || "");
|
|
2317
|
+
if (!k) return { ok: false, error: "bad key" };
|
|
2318
|
+
if (!agents.has(k)) {
|
|
2319
|
+
agents.set(k, { agentId: k, sessionId: opts2.id || k, registered: true, name: opts2.name || k, cwd: opts2.cwd || null, offlineAt: null, conn: 0, transportKind: "inprocess", source: opts2.source || "extension", capabilities: opts2.capabilities || {}, stream: null, boundPageId: opts2.pageId || null, activeBoundPageId: null, subEvents: new Set(opts2.subEvents || []), outbox: [], lastSeen: Date.now(), agentStream: null, inProcessSink: null });
|
|
2320
|
+
}
|
|
2321
|
+
const a = agents.get(k);
|
|
2322
|
+
a.lastSeen = Date.now();
|
|
2323
|
+
if (opts2.pageId) a.boundPageId = String(opts2.pageId);
|
|
2324
|
+
if (Array.isArray(opts2.subEvents)) a.subEvents = new Set(opts2.subEvents.map(String));
|
|
2325
|
+
if (!a.boundPageId) bindAgentFromAiSelfReverse(k, a);
|
|
2326
|
+
return { ok: true, agentKey: k, agentId: k };
|
|
2327
|
+
};
|
|
2328
|
+
const setAgentSink = (key, fn) => {
|
|
2329
|
+
const a = agents.get(String(key || ""));
|
|
2330
|
+
if (!a) return { ok: false, error: "unknown agent: " + key };
|
|
2331
|
+
a.inProcessSink = typeof fn === "function" ? fn : null;
|
|
2332
|
+
if (a.transportKind === "inprocess") {
|
|
2333
|
+
if (a.inProcessSink) {
|
|
2334
|
+
a.conn = Math.max(a.conn || 0, 1);
|
|
2335
|
+
a.offlineAt = null;
|
|
2336
|
+
a.lastSeen = Date.now();
|
|
2337
|
+
} else {
|
|
2338
|
+
a.conn = 0;
|
|
2339
|
+
if (!a.offlineAt) a.offlineAt = a.lastSeen;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return { ok: true, agentKey: String(key) };
|
|
2343
|
+
};
|
|
2344
|
+
const answer = (key, text) => {
|
|
2345
|
+
const k = String(key || "");
|
|
2346
|
+
const t = String(text || "");
|
|
2347
|
+
if (!k || !t) return { ok: false, error: "bad request" };
|
|
2348
|
+
const a = agents.get(k);
|
|
2349
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2350
|
+
if (a) {
|
|
2351
|
+
for (const om of a.outbox) {
|
|
2352
|
+
if (om.status !== "consumed" && (om.kind === "user_message" || om.kind === "user_answer" || om.kind === "chat")) {
|
|
2353
|
+
om.status = "consumed";
|
|
2354
|
+
om.ackedAt = Date.now();
|
|
2355
|
+
appendMsgEvent(k, { type: "status_change", id: randomBytes(4).toString("hex"), msgId: om.id, status: "consumed", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2356
|
+
for (const w of aiConns) w.write({ type: "msg_status", agentKey: k, msgId: om.id, status: "consumed" });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const m = { id: randomBytes(4).toString("hex"), kind: "agent_summary", from: "agent", agentKey: k, agentId: k, sessionId: k.split("/").slice(1).join("/") || null, text: t.slice(0, 4e3), payload: null, at, status: "consumed" };
|
|
2361
|
+
appendMsgEvent(k, { type: "message", id: m.id, parentId: null, timestamp: at, message: m });
|
|
2362
|
+
for (const w of aiConns) w.write({ type: "agent_answer", agentKey: k, agentId: k, ok: true, text: m.text, at });
|
|
2363
|
+
return { ok: true, message: m };
|
|
2364
|
+
};
|
|
2365
|
+
const clearAgent = (key) => {
|
|
2366
|
+
const k = String(key || "");
|
|
2367
|
+
if (!k) return { ok: false, error: "bad key" };
|
|
2368
|
+
agents.delete(k);
|
|
2369
|
+
agentFallbackPageIds.delete(k);
|
|
2370
|
+
return { ok: true };
|
|
2371
|
+
};
|
|
2372
|
+
const pushAiEvent = (event, payload = {}, from = "host") => {
|
|
2373
|
+
let n = 0;
|
|
2374
|
+
for (const w of aiConns) {
|
|
2375
|
+
try {
|
|
2376
|
+
w.write({ type: "event", from, event: String(event || ""), payload });
|
|
2377
|
+
n++;
|
|
2378
|
+
} catch {
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return { ok: true, delivered: n };
|
|
2382
|
+
};
|
|
2383
|
+
return { handle, sessions: () => listSessions(), tools: () => TOOLS, callTool, registerAgent: registerAgent2, bindAgentPage, setAgentSink, answer, clearAgent, pushAiEvent, close };
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// ../aiti-devtools-core/src/mcp-config.ts
|
|
2387
|
+
import { homedir } from "node:os";
|
|
2388
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
2389
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2, rmSync as rmSync2 } from "node:fs";
|
|
2390
|
+
var homeDir = () => homedir();
|
|
2391
|
+
var MCP_TARGETS = {
|
|
2392
|
+
// pi 实际读取 getAgentDir()/mcp.json(getAgentDir = $PI_CODING_AGENT_DIR,默认 ~/.pi/agent);
|
|
2393
|
+
// ~/.config/mcp/mcp.json 是历史误记——默认 pi 不读它(探针实证 2026-09-10,见执行记录 P17)
|
|
2394
|
+
pi: [{ path: () => join2(process.env.PI_CODING_AGENT_DIR || join2(homeDir(), ".pi", "agent"), "mcp.json"), key: "mcpServers" }],
|
|
2395
|
+
zcode: [{ path: () => join2(homeDir(), ".zcode", "cli", "config.json"), key: "mcpServers", keyPath: ["mcp", "servers"] }],
|
|
2396
|
+
opencode: [{
|
|
2397
|
+
path: () => join2(homeDir(), ".config", "opencode", "opencode.json"),
|
|
2398
|
+
key: "mcpServers",
|
|
2399
|
+
keyPath: ["mcp"],
|
|
2400
|
+
shimEntry: (shimPath) => ({ type: "local", command: ["node", shimPath], enabled: true }),
|
|
2401
|
+
envKey: "environment",
|
|
2402
|
+
// opencode 的 env map 字段名异构;条目零顶层私有键(schema 严格拒未知键)
|
|
2403
|
+
httpEntry: (url) => ({ type: "remote", url, enabled: true })
|
|
2404
|
+
// D23:http 类型对 opencode 非法
|
|
2405
|
+
}],
|
|
2406
|
+
claude: [
|
|
2407
|
+
{ path: () => join2(homeDir(), ".claude.json"), key: "mcpServers" },
|
|
2408
|
+
{ path: (p) => join2(p, ".mcp.json"), key: "mcpServers", project: true }
|
|
2409
|
+
],
|
|
2410
|
+
// CodeBuddy Code(腾讯 cbc):用户级 ~/.codebuddy/.mcp.json(官方 mcp.md:.mcp.json 推荐 /
|
|
2411
|
+
// mcp.json 已废弃;条目形状 mcpServers+stdio 与 claude 同构,2026-09-11 cbc mcp add 实测钉死)。
|
|
2412
|
+
// 项目级 .mcp.json 与 claude 同文件(claude 目标已覆盖),不重复注册。
|
|
2413
|
+
codebuddy: [{ path: () => join2(homeDir(), ".codebuddy", ".mcp.json"), key: "mcpServers" }],
|
|
2414
|
+
cursor: [{ path: () => join2(homeDir(), ".cursor", "mcp.json"), key: "mcpServers" }],
|
|
2415
|
+
windsurf: [{ path: () => join2(homeDir(), ".codeium", "windsurf", "mcp_config.json"), key: "mcpServers" }],
|
|
2416
|
+
vscode: [{ path: (p) => join2(p, ".vscode", "mcp.json"), key: "servers", project: true }],
|
|
2417
|
+
zed: [
|
|
2418
|
+
{ path: () => join2(homeDir(), ".config", "zed", "settings.json"), key: "mcp" },
|
|
2419
|
+
{ path: (p) => join2(p, ".zed", "settings.json"), key: "mcp", project: true }
|
|
2420
|
+
]
|
|
2421
|
+
};
|
|
2422
|
+
function detectStructureKey(cfg) {
|
|
2423
|
+
if (cfg && typeof cfg === "object") {
|
|
2424
|
+
const c = cfg;
|
|
2425
|
+
if (c.mcpServers && typeof c.mcpServers === "object") return { key: "mcpServers", inferred: false };
|
|
2426
|
+
if (c.servers && typeof c.servers === "object") return { key: "servers", inferred: false };
|
|
2427
|
+
if (c.mcp && typeof c.mcp === "object") return { key: "mcp", inferred: false };
|
|
2428
|
+
}
|
|
2429
|
+
return { key: "mcpServers", inferred: true };
|
|
2430
|
+
}
|
|
2431
|
+
function readJson(path) {
|
|
2432
|
+
try {
|
|
2433
|
+
return { cfg: JSON.parse(stripBom(readFileSync2(path, "utf8"))) };
|
|
2434
|
+
} catch {
|
|
2435
|
+
try {
|
|
2436
|
+
const clean = stripJsonComments(stripBom(readFileSync2(path, "utf8"))).replace(/,\s*([}\]])/g, "$1");
|
|
2437
|
+
return { cfg: JSON.parse(clean) };
|
|
2438
|
+
} catch (e) {
|
|
2439
|
+
return { cfg: null, error: "JSON 解析失败:" + String(e?.message || e) };
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function stripBom(text) {
|
|
2444
|
+
return text.replace(/^\uFEFF/, "");
|
|
2445
|
+
}
|
|
2446
|
+
function isPlainContainer(v) {
|
|
2447
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2448
|
+
}
|
|
2449
|
+
function stripJsonComments(text) {
|
|
2450
|
+
let out = "";
|
|
2451
|
+
let inStr = false, esc = false;
|
|
2452
|
+
for (let i = 0; i < text.length; i++) {
|
|
2453
|
+
const c = text[i], n = text[i + 1];
|
|
2454
|
+
if (inStr) {
|
|
2455
|
+
out += c;
|
|
2456
|
+
if (esc) esc = false;
|
|
2457
|
+
else if (c === "\\") esc = true;
|
|
2458
|
+
else if (c === '"') inStr = false;
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if (c === '"') {
|
|
2462
|
+
inStr = true;
|
|
2463
|
+
out += c;
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
if (c === "/" && n === "/") {
|
|
2467
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
2468
|
+
out += "\n";
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
if (c === "/" && n === "*") {
|
|
2472
|
+
i += 2;
|
|
2473
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
2474
|
+
i++;
|
|
2475
|
+
out += " ";
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
out += c;
|
|
2479
|
+
}
|
|
2480
|
+
return out;
|
|
2481
|
+
}
|
|
2482
|
+
function resolveContainer(cfg, keyPath) {
|
|
2483
|
+
let cur = cfg;
|
|
2484
|
+
for (const k of keyPath) {
|
|
2485
|
+
if (!isPlainContainer(cur[k])) cur[k] = {};
|
|
2486
|
+
cur = cur[k];
|
|
2487
|
+
}
|
|
2488
|
+
return cur;
|
|
2489
|
+
}
|
|
2490
|
+
function writeAtomic(path, cfg) {
|
|
2491
|
+
const tmp = path + ".tmp";
|
|
2492
|
+
const bak = path + ".bak";
|
|
2493
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2494
|
+
writeFileSync2(tmp, JSON.stringify(cfg, null, 2) + "\n");
|
|
2495
|
+
if (existsSync2(path)) {
|
|
2496
|
+
try {
|
|
2497
|
+
renameSync2(path, bak);
|
|
2498
|
+
} catch {
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
try {
|
|
2502
|
+
renameSync2(tmp, path);
|
|
2503
|
+
} catch (e) {
|
|
2504
|
+
if (existsSync2(bak) && !existsSync2(path)) {
|
|
2505
|
+
try {
|
|
2506
|
+
renameSync2(bak, path);
|
|
2507
|
+
} catch {
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
try {
|
|
2511
|
+
rmSync2(tmp, { force: true });
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
throw e;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function listPlannedWrites(opts) {
|
|
2518
|
+
const projectDir = opts.projectDir || process.cwd();
|
|
2519
|
+
const out = [];
|
|
2520
|
+
const hasConfigPaths = !!(opts.configPaths && opts.configPaths.length);
|
|
2521
|
+
const auto = (opts.targets === "auto" || !opts.targets) && !hasConfigPaths;
|
|
2522
|
+
if (!hasConfigPaths) {
|
|
2523
|
+
const targets = auto ? Object.keys(MCP_TARGETS) : opts.targets;
|
|
2524
|
+
for (const t of targets) {
|
|
2525
|
+
const files = MCP_TARGETS[t];
|
|
2526
|
+
if (!files) continue;
|
|
2527
|
+
for (const f of files) {
|
|
2528
|
+
const p = f.path(projectDir);
|
|
2529
|
+
if (auto && !existsSync2(p)) continue;
|
|
2530
|
+
out.push({ target: t, path: p, key: f.key, exists: existsSync2(p), keyPath: f.keyPath, shimEntry: f.shimEntry, envKey: f.envKey, httpEntry: f.httpEntry });
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
for (const p of opts.configPaths || []) {
|
|
2535
|
+
const abs = p.replace(/^~(?=\/|$)/, homeDir());
|
|
2536
|
+
let matched = null;
|
|
2537
|
+
for (const [t, files] of Object.entries(MCP_TARGETS)) {
|
|
2538
|
+
for (const f of files) {
|
|
2539
|
+
if (f.path(projectDir) === abs) {
|
|
2540
|
+
matched = { target: t, path: abs, key: f.key, exists: existsSync2(abs), keyPath: f.keyPath, shimEntry: f.shimEntry, envKey: f.envKey, httpEntry: f.httpEntry };
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
if (matched) break;
|
|
2545
|
+
}
|
|
2546
|
+
out.push(matched ?? { target: abs, path: abs, key: "mcpServers", exists: existsSync2(abs) });
|
|
2547
|
+
}
|
|
2548
|
+
return out;
|
|
2549
|
+
}
|
|
2550
|
+
function assertKnownTargets(targets) {
|
|
2551
|
+
if (!targets || targets === "auto") return null;
|
|
2552
|
+
const unknown = targets.filter((t) => !MCP_TARGETS[t]);
|
|
2553
|
+
return unknown.length ? `未知注册目标:${unknown.join("、")}(可用:${Object.keys(MCP_TARGETS).join("、")})` : null;
|
|
2554
|
+
}
|
|
2555
|
+
var LEGACY_VERSION_FIELD = "_aitiShimVersion";
|
|
2556
|
+
var SHIM_VERSION_ENV = "AITI_SHIM_VERSION";
|
|
2557
|
+
function readShimEntryVersion(entry, envKey) {
|
|
2558
|
+
const env = entry[envKey];
|
|
2559
|
+
if (!isPlainContainer(env)) return 0;
|
|
2560
|
+
return parseInt(String(env[SHIM_VERSION_ENV] ?? ""), 10) || 0;
|
|
2561
|
+
}
|
|
2562
|
+
function entryReferencesShimPath(entry, shimPath) {
|
|
2563
|
+
return JSON.stringify(entry).includes(JSON.stringify(shimPath));
|
|
2564
|
+
}
|
|
2565
|
+
function registerAitiShim(opts) {
|
|
2566
|
+
const unknownErr = assertKnownTargets(opts.targets);
|
|
2567
|
+
if (unknownErr) return { ok: false, changed: false, results: [], summary: unknownErr };
|
|
2568
|
+
const serverName = opts.serverName || "aiti";
|
|
2569
|
+
const dryRun = !!opts.dryRun;
|
|
2570
|
+
const log = opts.log || ((l) => console.log(l));
|
|
2571
|
+
const results = [];
|
|
2572
|
+
let changed = false;
|
|
2573
|
+
for (const w of listPlannedWrites(opts)) {
|
|
2574
|
+
const outcome = (() => {
|
|
2575
|
+
let cfg;
|
|
2576
|
+
if (!existsSync2(w.path)) {
|
|
2577
|
+
cfg = {};
|
|
2578
|
+
} else {
|
|
2579
|
+
const r = readJson(w.path);
|
|
2580
|
+
if (r.error) return { changed: false, error: "JSON 解析失败:" + r.error };
|
|
2581
|
+
cfg = r.cfg;
|
|
2582
|
+
}
|
|
2583
|
+
if (!isPlainContainer(cfg)) return { changed: false, error: "配置不是对象" };
|
|
2584
|
+
const map = w.keyPath ? resolveContainer(cfg, w.keyPath) : (() => {
|
|
2585
|
+
const det = detectStructureKey(cfg);
|
|
2586
|
+
if (!isPlainContainer(cfg[det.key])) cfg[det.key] = {};
|
|
2587
|
+
return cfg[det.key];
|
|
2588
|
+
})();
|
|
2589
|
+
const existing = map[serverName] && typeof map[serverName] === "object" && !Array.isArray(map[serverName]) ? map[serverName] : null;
|
|
2590
|
+
const envKey = w.envKey || "env";
|
|
2591
|
+
const existingVer = existing ? readShimEntryVersion(existing, envKey) : 0;
|
|
2592
|
+
const hasLegacyField = !!existing && LEGACY_VERSION_FIELD in existing;
|
|
2593
|
+
const shimShapeBroken = !!w.shimEntry && !!existing && ("args" in existing || "env" in existing && envKey !== "env");
|
|
2594
|
+
if (existing && !hasLegacyField && !shimShapeBroken && existingVer >= opts.version && !opts.force && entryReferencesShimPath(existing, opts.shimPath)) {
|
|
2595
|
+
return { changed: false, note: `已是最新(v${existingVer} >= v${opts.version})` };
|
|
2596
|
+
}
|
|
2597
|
+
if (!dryRun) {
|
|
2598
|
+
const entry = { ...existing || {}, ...w.shimEntry ? w.shimEntry(opts.shimPath) : { type: "stdio", command: "node", args: [opts.shimPath] } };
|
|
2599
|
+
delete entry.url;
|
|
2600
|
+
delete entry[LEGACY_VERSION_FIELD];
|
|
2601
|
+
if (w.shimEntry) {
|
|
2602
|
+
delete entry.args;
|
|
2603
|
+
delete entry.env;
|
|
2604
|
+
}
|
|
2605
|
+
const prevEnv = isPlainContainer(entry[envKey]) ? entry[envKey] : {};
|
|
2606
|
+
entry[envKey] = { ...prevEnv, [SHIM_VERSION_ENV]: String(opts.version) };
|
|
2607
|
+
map[serverName] = entry;
|
|
2608
|
+
try {
|
|
2609
|
+
writeAtomic(w.path, cfg);
|
|
2610
|
+
} catch (e) {
|
|
2611
|
+
return { changed: false, error: "写入失败:" + String(e?.message || e) };
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return { changed: true, note: hasLegacyField ? `迁移(顶层版本键 → ${envKey}.${SHIM_VERSION_ENV})` : existing ? `升级 v${existingVer} → v${opts.version}` : "新建" };
|
|
2615
|
+
})();
|
|
2616
|
+
results.push({ target: w.target, path: w.path, changed: outcome.changed, error: outcome.error });
|
|
2617
|
+
if (outcome.error) log(` ✗ ${w.target} → ${w.path}:${outcome.error}`);
|
|
2618
|
+
else {
|
|
2619
|
+
if (outcome.changed) changed = true;
|
|
2620
|
+
log(` ${outcome.changed ? "✓" : "·"} ${w.target} → ${w.path}${outcome.note ? ":" + outcome.note : ""}`);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
const ok = results.every((r) => !r.error);
|
|
2624
|
+
const summary = `${dryRun ? "[dry-run] " : ""}shim ${serverName} v${opts.version}(${opts.shimPath})${results.length ? ";" + results.map((r) => r.error ? r.target + " ✗" : r.changed ? r.target + " ✓" : r.target + " ·").join(" ") : "(无目标文件)"}`;
|
|
2625
|
+
return { ok, changed, results, summary };
|
|
2626
|
+
}
|
|
2627
|
+
function resolveShimPathAuto(cwdCandidates) {
|
|
2628
|
+
const installedCandidates = [
|
|
2629
|
+
process.env.AITI_WEB_ROOT && join2(process.env.AITI_WEB_ROOT, "node_modules"),
|
|
2630
|
+
join2(homeDir(), ".pi", "web", "node_modules"),
|
|
2631
|
+
join2(homeDir(), ".pi", "web-dev", "node_modules")
|
|
2632
|
+
].filter((p) => !!p).map((nm) => join2(nm, "@aiti-ai", "aiti-bridge", "bin", "aiti-mcp-shim.cjs"));
|
|
2633
|
+
const candidates = [
|
|
2634
|
+
process.env.AITI_SHIM_PATH,
|
|
2635
|
+
...installedCandidates,
|
|
2636
|
+
...cwdCandidates || [],
|
|
2637
|
+
join2(homeDir(), "workspace", "pi-work", "packages", "aiti-bridge", "bin", "aiti-mcp-shim.cjs")
|
|
2638
|
+
].filter((p) => !!p);
|
|
2639
|
+
for (const p of candidates) {
|
|
2640
|
+
try {
|
|
2641
|
+
const m = readFileSync2(p, "utf8").match(/SHIM_VERSION\s*=\s*(\d+)/);
|
|
2642
|
+
if (m) return { shimPath: p, version: parseInt(m[1], 10) };
|
|
2643
|
+
} catch {
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2648
|
+
function autoRegisterAitiShim(opts) {
|
|
2649
|
+
const log = opts.log || (() => {
|
|
2650
|
+
});
|
|
2651
|
+
try {
|
|
2652
|
+
const found = resolveShimPathAuto();
|
|
2653
|
+
if (!found) {
|
|
2654
|
+
log("[aiti] stdio shim 未找到(可设 env AITI_SHIM_PATH 指向 aiti-mcp-shim.cjs),跳过 MCP 自注册");
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
const r = registerAitiShim({
|
|
2658
|
+
shimPath: found.shimPath,
|
|
2659
|
+
version: found.version,
|
|
2660
|
+
configPaths: opts.configPaths,
|
|
2661
|
+
targets: opts.configPaths && opts.configPaths.length ? void 0 : opts.targets,
|
|
2662
|
+
projectDir: opts.projectDir,
|
|
2663
|
+
force: opts.force,
|
|
2664
|
+
log
|
|
2665
|
+
});
|
|
2666
|
+
return r;
|
|
2667
|
+
} catch (e) {
|
|
2668
|
+
log("[aiti] shim 自注册失败(忽略,不影响 agent 启动):" + String(e?.message || e));
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// ../aiti-devtools-agent/src/bridge-client.ts
|
|
2674
|
+
var AgentBridgeClient = class {
|
|
2675
|
+
_bridgeUp = false;
|
|
2676
|
+
/** 桥地址(已剥尾部斜杠,含 /aiti 路径前缀等) */
|
|
2677
|
+
base;
|
|
2678
|
+
constructor(base) {
|
|
2679
|
+
this.base = base.replace(/\/+$/, "");
|
|
2680
|
+
}
|
|
2681
|
+
/** 桥当前连通状态(probe/register 成功置 true,失败置 false) */
|
|
2682
|
+
get isUp() {
|
|
2683
|
+
return this._bridgeUp;
|
|
2684
|
+
}
|
|
2685
|
+
/** 桥连通性预检(GET /bridge/sessions;本地拒绝连接即时返回) */
|
|
2686
|
+
async probe(timeoutMs = 2500) {
|
|
2687
|
+
try {
|
|
2688
|
+
const res = await fetch(this.base + "/bridge/sessions", { signal: AbortSignal.timeout(timeoutMs) });
|
|
2689
|
+
this._bridgeUp = res.ok;
|
|
2690
|
+
return res.ok;
|
|
2691
|
+
} catch {
|
|
2692
|
+
this._bridgeUp = false;
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
/** agent 启动注册(POST /bridge/agent/register)→ agentKey;失败返回 null */
|
|
2697
|
+
async register(opts) {
|
|
2698
|
+
try {
|
|
2699
|
+
const res = await fetch(this.base + "/bridge/agent/register", {
|
|
2700
|
+
method: "POST",
|
|
2701
|
+
headers: { "Content-Type": "application/json" },
|
|
2702
|
+
signal: AbortSignal.timeout(5e3),
|
|
2703
|
+
body: JSON.stringify({
|
|
2704
|
+
agentId: opts.agentId,
|
|
2705
|
+
sessionId: opts.sessionId,
|
|
2706
|
+
name: opts.name,
|
|
2707
|
+
cwd: opts.cwd || void 0,
|
|
2708
|
+
source: opts.source || "extension",
|
|
2709
|
+
subEvents: opts.subEvents ?? ["page_error"]
|
|
2710
|
+
})
|
|
2711
|
+
});
|
|
2712
|
+
const b = await res.json().catch(() => null);
|
|
2713
|
+
return b && b.ok ? b : null;
|
|
2714
|
+
} catch {
|
|
2715
|
+
return null;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
/** 工具执行(POST /bridge/agent/tools/call)——支持宿主 signal 取消 + cancelId 通知桥中断挂起请求 */
|
|
2719
|
+
async callTool(tool, args, opts, signal) {
|
|
2720
|
+
const ctrl = new AbortController();
|
|
2721
|
+
const t = setTimeout(() => ctrl.abort(), 65e3);
|
|
2722
|
+
const onAbort = () => {
|
|
2723
|
+
clearTimeout(t);
|
|
2724
|
+
try {
|
|
2725
|
+
ctrl.abort();
|
|
2726
|
+
} catch {
|
|
2727
|
+
}
|
|
2728
|
+
if (opts.cancelId) {
|
|
2729
|
+
void fetch(this.base + "/bridge/cancel", {
|
|
2730
|
+
method: "POST",
|
|
2731
|
+
headers: { "Content-Type": "application/json" },
|
|
2732
|
+
body: JSON.stringify({ cancelId: opts.cancelId }),
|
|
2733
|
+
signal: AbortSignal.timeout(3e3)
|
|
2734
|
+
}).catch(() => {
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
if (signal) {
|
|
2739
|
+
if (signal.aborted) {
|
|
2740
|
+
onAbort();
|
|
2741
|
+
return { ok: false, error: "已取消(agent 停止)" };
|
|
2742
|
+
}
|
|
2743
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2744
|
+
}
|
|
2745
|
+
const res = await fetch(this.base + "/bridge/agent/tools/call", {
|
|
2746
|
+
method: "POST",
|
|
2747
|
+
headers: { "Content-Type": "application/json" },
|
|
2748
|
+
signal: ctrl.signal,
|
|
2749
|
+
body: JSON.stringify({
|
|
2750
|
+
tool,
|
|
2751
|
+
args: args || {},
|
|
2752
|
+
agentId: opts.agentId,
|
|
2753
|
+
sessionId: opts.sessionId,
|
|
2754
|
+
cwd: opts.cwd || void 0,
|
|
2755
|
+
source: "extension",
|
|
2756
|
+
cancelId: opts.cancelId
|
|
2757
|
+
})
|
|
2758
|
+
}).catch(() => null);
|
|
2759
|
+
if (signal) {
|
|
2760
|
+
try {
|
|
2761
|
+
signal.removeEventListener("abort", onAbort);
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
clearTimeout(t);
|
|
2766
|
+
if (!res) return { ok: false, error: "桥不可达:" + this.base };
|
|
2767
|
+
const body = await res.json().catch(() => null);
|
|
2768
|
+
if (!body) return { ok: false, error: "桥响应异常(HTTP " + res.status + ")" };
|
|
2769
|
+
return body;
|
|
2770
|
+
}
|
|
2771
|
+
/** agent 完成 → 回答回桥(POST /bridge/agent/answer)——消息流落盘供窗口展示 */
|
|
2772
|
+
async answer(agentKey, text) {
|
|
2773
|
+
try {
|
|
2774
|
+
await fetch(this.base + "/bridge/agent/answer", {
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
headers: { "Content-Type": "application/json" },
|
|
2777
|
+
signal: AbortSignal.timeout(5e3),
|
|
2778
|
+
body: JSON.stringify({ agentKey, text })
|
|
2779
|
+
});
|
|
2780
|
+
} catch {
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* agent SSE 持久流(桥 → 宿主注入模型)。断线指数退避重连 + 空闲兜底。
|
|
2785
|
+
*
|
|
2786
|
+
* @param registerFn 每次(重)连前调用以(重新)注册并取 agentKey——桥重启后 agents 表会丢,
|
|
2787
|
+
* 必须在 connect 前重新 register;返回 null 表示本轮不连(宿主要求,不重试)。
|
|
2788
|
+
* @param onMessage 每条 SSE 消息回调(含 type/text/payload)
|
|
2789
|
+
* @param opts idleMs(空闲超时,默认 60000);retryBase(退避基数,默认 2000)
|
|
2790
|
+
* @returns 停止函数(置停 + abort 当前流;不再重连)
|
|
2791
|
+
*/
|
|
2792
|
+
connectAgentStream(registerFn, onMessage, opts = {}) {
|
|
2793
|
+
const idleMs = opts.idleMs ?? 6e4;
|
|
2794
|
+
let retry = opts.retryBase ?? 2e3;
|
|
2795
|
+
let ctrl = null;
|
|
2796
|
+
let stopped = false;
|
|
2797
|
+
const run = async () => {
|
|
2798
|
+
if (stopped) return;
|
|
2799
|
+
const c = new AbortController();
|
|
2800
|
+
ctrl = c;
|
|
2801
|
+
const agentKey = await registerFn();
|
|
2802
|
+
if (!agentKey || stopped) {
|
|
2803
|
+
if (ctrl === c) ctrl = null;
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
this._bridgeUp = true;
|
|
2807
|
+
let idleTimer = null;
|
|
2808
|
+
const poke = () => {
|
|
2809
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2810
|
+
idleTimer = setTimeout(() => {
|
|
2811
|
+
try {
|
|
2812
|
+
c.abort();
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
}, idleMs);
|
|
2816
|
+
};
|
|
2817
|
+
try {
|
|
2818
|
+
const resp = await fetch(this.base + "/bridge/agent/sse?agentKey=" + encodeURIComponent(agentKey), {
|
|
2819
|
+
signal: c.signal,
|
|
2820
|
+
headers: { Accept: "text/event-stream" }
|
|
2821
|
+
});
|
|
2822
|
+
if (!resp.ok || !resp.body) throw new Error("SSE HTTP " + resp.status);
|
|
2823
|
+
retry = opts.retryBase ?? 2e3;
|
|
2824
|
+
const reader = resp.body.getReader();
|
|
2825
|
+
const decoder = new TextDecoder();
|
|
2826
|
+
let buf = "";
|
|
2827
|
+
poke();
|
|
2828
|
+
while (true) {
|
|
2829
|
+
const { done, value } = await reader.read();
|
|
2830
|
+
if (done) break;
|
|
2831
|
+
poke();
|
|
2832
|
+
buf += decoder.decode(value, { stream: true });
|
|
2833
|
+
let idx;
|
|
2834
|
+
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
2835
|
+
const chunk = buf.slice(0, idx);
|
|
2836
|
+
buf = buf.slice(idx + 2);
|
|
2837
|
+
const line = chunk.split("\n").find((l) => l.startsWith("data:"));
|
|
2838
|
+
if (line) {
|
|
2839
|
+
try {
|
|
2840
|
+
onMessage(JSON.parse(line.slice(5).trim()));
|
|
2841
|
+
} catch {
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
} catch {
|
|
2847
|
+
}
|
|
2848
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2849
|
+
if (stopped) return;
|
|
2850
|
+
if (ctrl === c) {
|
|
2851
|
+
setTimeout(() => {
|
|
2852
|
+
if (ctrl === c && !stopped) run();
|
|
2853
|
+
}, retry);
|
|
2854
|
+
retry = Math.min(3e4, retry * 2);
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
run();
|
|
2858
|
+
return () => {
|
|
2859
|
+
stopped = true;
|
|
2860
|
+
try {
|
|
2861
|
+
ctrl?.abort();
|
|
2862
|
+
} catch {
|
|
2863
|
+
}
|
|
2864
|
+
ctrl = null;
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
|
|
2869
|
+
// ../aiti-devtools-agent/src/agent-host.ts
|
|
2870
|
+
function wrapToolResult(r) {
|
|
2871
|
+
if (!r.ok) {
|
|
2872
|
+
return { content: [{ type: "text", text: r.error || "未知错误" }], details: {}, isError: true };
|
|
2873
|
+
}
|
|
2874
|
+
return {
|
|
2875
|
+
content: [{ type: "text", text: r.result ?? "" }],
|
|
2876
|
+
details: {},
|
|
2877
|
+
...r.isError ? { isError: true } : {}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// ../aiti-devtools-agent/src/schema.ts
|
|
2882
|
+
import { Type } from "typebox";
|
|
2883
|
+
function jsonSchemaToTypeBox(schema) {
|
|
2884
|
+
if (!schema || typeof schema !== "object") return Type.Object({});
|
|
2885
|
+
const opts = (d) => d ? { description: d } : {};
|
|
2886
|
+
switch (schema.type) {
|
|
2887
|
+
case "string":
|
|
2888
|
+
return Type.String(opts(schema.description));
|
|
2889
|
+
case "number":
|
|
2890
|
+
return Type.Number(opts(schema.description));
|
|
2891
|
+
case "integer":
|
|
2892
|
+
return Type.Integer(opts(schema.description));
|
|
2893
|
+
case "boolean":
|
|
2894
|
+
return Type.Boolean(opts(schema.description));
|
|
2895
|
+
case "array": {
|
|
2896
|
+
const item = schema.items ? jsonSchemaToTypeBox(schema.items) : Type.Any();
|
|
2897
|
+
return Type.Array(item, opts(schema.description));
|
|
2898
|
+
}
|
|
2899
|
+
case "object": {
|
|
2900
|
+
const props = {};
|
|
2901
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
2902
|
+
for (const [k, v] of Object.entries(schema.properties || {})) {
|
|
2903
|
+
const t = jsonSchemaToTypeBox(v);
|
|
2904
|
+
props[k] = required.includes(k) ? t : Type.Optional(t);
|
|
2905
|
+
}
|
|
2906
|
+
return Type.Object(props, opts(schema.description));
|
|
2907
|
+
}
|
|
2908
|
+
default:
|
|
2909
|
+
return Type.Object({}, opts(schema.description));
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// src/bridge-url.ts
|
|
2914
|
+
var PI_WEB_EXT_PATH_SIG = "/api/extensions/aiti-devtools";
|
|
2915
|
+
function normalizeBridgeBase(url) {
|
|
2916
|
+
try {
|
|
2917
|
+
const u = new URL(url);
|
|
2918
|
+
const idx = u.pathname.indexOf(PI_WEB_EXT_PATH_SIG);
|
|
2919
|
+
if (idx < 0) return url;
|
|
2920
|
+
const extBase = u.origin + u.pathname.slice(0, idx + PI_WEB_EXT_PATH_SIG.length);
|
|
2921
|
+
return extBase + "/mcp";
|
|
2922
|
+
} catch {
|
|
2923
|
+
return url;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// src/index.ts
|
|
2928
|
+
function logFile(msg) {
|
|
2929
|
+
try {
|
|
2930
|
+
const logDir = join3(getAgentDir(), "logs");
|
|
2931
|
+
mkdirSync3(logDir, { recursive: true });
|
|
2932
|
+
appendFileSync2(join3(logDir, "aiti-devtools-pi.log"), `[aiti-devtools-pi] ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${msg}
|
|
2933
|
+
`);
|
|
2934
|
+
} catch {
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
var DEFAULT_BRIDGE_URL = "http://127.0.0.1:8777/aiti";
|
|
2938
|
+
function resolveBridgeUrl() {
|
|
2939
|
+
if (process.env.AITI_BRIDGE_URL) {
|
|
2940
|
+
const url = process.env.AITI_BRIDGE_URL.replace(/\/+$/, "");
|
|
2941
|
+
if (process.env.AITI_BRIDGE_PORT) {
|
|
2942
|
+
try {
|
|
2943
|
+
const uPort = new URL(url).port || (url.startsWith("https:") ? "443" : "80");
|
|
2944
|
+
if (uPort !== process.env.AITI_BRIDGE_PORT) {
|
|
2945
|
+
logFile("[aiti-devtools-pi] AITI_BRIDGE_URL(端口 " + uPort + ")与 AITI_BRIDGE_PORT=" + process.env.AITI_BRIDGE_PORT + " 不一致 → 以 URL 为准(PORT 被忽略)");
|
|
2946
|
+
}
|
|
2947
|
+
} catch {
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
return normalizeBridgeBase(url);
|
|
2951
|
+
}
|
|
2952
|
+
const port = process.env.AITI_BRIDGE_PORT;
|
|
2953
|
+
if (port) {
|
|
2954
|
+
const host = process.env.AITI_BRIDGE_HOST || "127.0.0.1";
|
|
2955
|
+
return normalizeBridgeBase("http://" + host + ":" + port + "/aiti");
|
|
2956
|
+
}
|
|
2957
|
+
try {
|
|
2958
|
+
const settingsPath = join3(getAgentDir(), "settings.json");
|
|
2959
|
+
if (existsSync3(settingsPath)) {
|
|
2960
|
+
const cfg = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
2961
|
+
const u = cfg?.settings?.aitiDevtools?.bridgeUrl;
|
|
2962
|
+
if (typeof u === "string" && u) return normalizeBridgeBase(u.replace(/\/+$/, ""));
|
|
2963
|
+
}
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2966
|
+
return DEFAULT_BRIDGE_URL;
|
|
2967
|
+
}
|
|
2968
|
+
function bridgeScriptPath() {
|
|
2969
|
+
if (process.env.AITI_BRIDGE_SCRIPT && existsSync3(process.env.AITI_BRIDGE_SCRIPT)) return process.env.AITI_BRIDGE_SCRIPT;
|
|
2970
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
2971
|
+
const candidates = [
|
|
2972
|
+
resolve(here, "../../aiti-bridge/bin/aiti-bridge.mjs"),
|
|
2973
|
+
// 源码/产物目录 → packages/aiti-bridge/bin
|
|
2974
|
+
resolve(homedir2(), "workspace/pi-work/packages/aiti-bridge/bin/aiti-bridge.mjs")
|
|
2975
|
+
// 开发环境绝对兜底
|
|
2976
|
+
];
|
|
2977
|
+
for (const p of candidates) if (existsSync3(p)) return p;
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
function ensureBridgeSetting(url) {
|
|
2981
|
+
try {
|
|
2982
|
+
const p = join3(getAgentDir(), "settings.json");
|
|
2983
|
+
let cfg = null;
|
|
2984
|
+
if (existsSync3(p)) {
|
|
2985
|
+
try {
|
|
2986
|
+
cfg = JSON.parse(readFileSync3(p, "utf8"));
|
|
2987
|
+
} catch {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
2992
|
+
const cur = cfg?.settings?.aitiDevtools?.bridgeUrl;
|
|
2993
|
+
if (typeof cur === "string" && cur.replace(/\/+$/, "") === url) return false;
|
|
2994
|
+
cfg = { ...cfg, settings: { ...cfg.settings || {}, aitiDevtools: { ...cfg.settings?.aitiDevtools || {}, bridgeUrl: url } } };
|
|
2995
|
+
writeFileSync3(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
2996
|
+
return true;
|
|
2997
|
+
} catch (e) {
|
|
2998
|
+
return false;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
function ensureMcpConfig(url) {
|
|
3002
|
+
try {
|
|
3003
|
+
const p = join3(homedir2(), ".config", "mcp", "mcp.json");
|
|
3004
|
+
let cfg = null;
|
|
3005
|
+
if (existsSync3(p)) {
|
|
3006
|
+
try {
|
|
3007
|
+
cfg = JSON.parse(readFileSync3(p, "utf8"));
|
|
3008
|
+
} catch {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
3013
|
+
if (!cfg.mcpServers) cfg.mcpServers = {};
|
|
3014
|
+
const uu = new URL(url);
|
|
3015
|
+
const sigIdx = uu.pathname.indexOf(PI_WEB_EXT_PATH_SIG);
|
|
3016
|
+
const target = sigIdx >= 0 ? uu.origin + uu.pathname.slice(0, sigIdx + PI_WEB_EXT_PATH_SIG.length) + "/mcp/mcp" : uu.origin + "/aiti/mcp";
|
|
3017
|
+
const cur = cfg.mcpServers?.["aiti-devtools"];
|
|
3018
|
+
if (cur && cur.type === "http" && cur.url === target) return false;
|
|
3019
|
+
cfg.mcpServers = { ...cfg.mcpServers || {}, "aiti-devtools": { type: "http", url: target } };
|
|
3020
|
+
writeFileSync3(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
3021
|
+
return true;
|
|
3022
|
+
} catch (e) {
|
|
3023
|
+
return false;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
function spawnBridge(script, host, port) {
|
|
3027
|
+
try {
|
|
3028
|
+
const log = openSync(join3(getAgentDir(), "aiti-bridge.log"), "a");
|
|
3029
|
+
const child = spawn2(process.execPath, [script, "--host", host, "--port", String(port)], {
|
|
3030
|
+
detached: true,
|
|
3031
|
+
stdio: ["ignore", log, log],
|
|
3032
|
+
env: { ...process.env, AITI_BRIDGE_HOST: host, AITI_BRIDGE_PORT: String(port) }
|
|
3033
|
+
});
|
|
3034
|
+
child.unref();
|
|
3035
|
+
return true;
|
|
3036
|
+
} catch (e) {
|
|
3037
|
+
return false;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
async function bootstrapBridge(base) {
|
|
3041
|
+
ensureBridgeSetting(base);
|
|
3042
|
+
ensureMcpConfig(base);
|
|
3043
|
+
if (await probeBridge2(base, 1500)) return true;
|
|
3044
|
+
if (process.env.AITI_PI_EXT_NO_BOOTSTRAP) {
|
|
3045
|
+
logFile("[aiti-devtools-pi] 桥不可达且已跳过自动拉起(AITI_PI_EXT_NO_BOOTSTRAP=1)");
|
|
3046
|
+
return false;
|
|
3047
|
+
}
|
|
3048
|
+
const script = bridgeScriptPath();
|
|
3049
|
+
if (!script) {
|
|
3050
|
+
logFile("[aiti-devtools-pi] 桥不可达且找不到桥脚本,无法自动拉起(" + base + ")——可设 AITI_BRIDGE_SCRIPT 指定");
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
const u = new URL(base);
|
|
3054
|
+
if (!u.port) {
|
|
3055
|
+
logFile("[aiti-devtools-pi] 托管桥(URL 无端口)不可达 → 不自动拉起,等待宿主恢复:" + base);
|
|
3056
|
+
return false;
|
|
3057
|
+
}
|
|
3058
|
+
const port = parseInt(u.port || "8777", 10) || 8777;
|
|
3059
|
+
const host = u.hostname || "127.0.0.1";
|
|
3060
|
+
logFile("[aiti-devtools-pi] 桥不可达 → 按端口 " + port + " 拉起独立桥:" + script);
|
|
3061
|
+
spawnBridge(script, host, port);
|
|
3062
|
+
for (let i = 0; i < 15; i++) {
|
|
3063
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3064
|
+
if (await probeBridge2(base, 500)) {
|
|
3065
|
+
logFile("[aiti-devtools-pi] 桥已拉起并就绪(" + base + ")");
|
|
3066
|
+
return true;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
logFile("[aiti-devtools-pi] 桥拉起超时(" + base + ")——查看日志 " + join3(getAgentDir(), "aiti-bridge.log"));
|
|
3070
|
+
return false;
|
|
3071
|
+
}
|
|
3072
|
+
var agentId = () => process.env.AITI_AGENT_ID || "pi";
|
|
3073
|
+
function agentDisplayName() {
|
|
3074
|
+
if (process.env.AITI_AGENT_NAME) return process.env.AITI_AGENT_NAME;
|
|
3075
|
+
try {
|
|
3076
|
+
const settingsPath = join3(getAgentDir(), "settings.json");
|
|
3077
|
+
if (existsSync3(settingsPath)) {
|
|
3078
|
+
const cfg = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
3079
|
+
const n = cfg?.settings?.aitiDevtools?.agentName;
|
|
3080
|
+
if (typeof n === "string" && n) return n;
|
|
3081
|
+
}
|
|
3082
|
+
} catch {
|
|
3083
|
+
}
|
|
3084
|
+
return "pi";
|
|
3085
|
+
}
|
|
3086
|
+
var bridge = createBridge(join3(getAgentDir(), "aiti-bridge"));
|
|
3087
|
+
var _bridgeClient = null;
|
|
3088
|
+
function getBridgeClient(base) {
|
|
3089
|
+
if (!_bridgeClient || _bridgeClient.base !== base) _bridgeClient = new AgentBridgeClient(base);
|
|
3090
|
+
return _bridgeClient;
|
|
3091
|
+
}
|
|
3092
|
+
var currentSessionId = null;
|
|
3093
|
+
var currentAgentKey = null;
|
|
3094
|
+
var currentCwd = "";
|
|
3095
|
+
var streamCtrl = null;
|
|
3096
|
+
var FALLBACK_SESSION = "s_p" + process.pid.toString(36);
|
|
3097
|
+
var bridgeUp = false;
|
|
3098
|
+
var toolsRegistered = false;
|
|
3099
|
+
async function callBridgeTool(tool, args, signal, cancelId) {
|
|
3100
|
+
const relay = globalThis.__aitiTuiRelay;
|
|
3101
|
+
if (relay && relay.active()) {
|
|
3102
|
+
try {
|
|
3103
|
+
logFile("[aiti-devtools-pi] transport=socket →", tool);
|
|
3104
|
+
const r = await relay.call(tool, args || {}, cancelId, signal);
|
|
3105
|
+
return r;
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
logFile("[aiti-devtools-pi] socket 转发失败 → 回退直连桥:", e instanceof Error ? e.message : e);
|
|
3108
|
+
}
|
|
3109
|
+
} else {
|
|
3110
|
+
logFile("[aiti-devtools-pi] transport=http(relay未激活) →", tool);
|
|
3111
|
+
}
|
|
3112
|
+
return getBridgeClient(resolveBridgeUrl()).callTool(
|
|
3113
|
+
tool,
|
|
3114
|
+
args || {},
|
|
3115
|
+
{ agentId: agentId(), sessionId: currentSessionId || FALLBACK_SESSION, cwd: currentCwd || void 0, cancelId },
|
|
3116
|
+
signal
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
async function registerAgent(base, sessionId, name, cwd) {
|
|
3120
|
+
return getBridgeClient(base).register({ agentId: agentId(), sessionId, name, cwd: cwd || void 0, source: "extension", subEvents: ["page_error"] });
|
|
3121
|
+
}
|
|
3122
|
+
async function probeBridge2(base, timeoutMs = 2500) {
|
|
3123
|
+
const ok = await getBridgeClient(base).probe(timeoutMs);
|
|
3124
|
+
bridgeUp = ok;
|
|
3125
|
+
return ok;
|
|
3126
|
+
}
|
|
3127
|
+
var STREAM_IDLE_MS = 6e4;
|
|
3128
|
+
async function connectAgentStream(pi, base, sessionId, name, cwd, onMessage) {
|
|
3129
|
+
if (streamCtrl) {
|
|
3130
|
+
try {
|
|
3131
|
+
streamCtrl.abort();
|
|
3132
|
+
} catch {
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
let retry = 2e3;
|
|
3136
|
+
let respawned = false;
|
|
3137
|
+
const run = async () => {
|
|
3138
|
+
const ctrl = new AbortController();
|
|
3139
|
+
streamCtrl = ctrl;
|
|
3140
|
+
const reg = await registerAgent(base, sessionId, name, cwd).catch(() => null);
|
|
3141
|
+
if (!reg || !reg.agentKey) {
|
|
3142
|
+
bridgeUp = false;
|
|
3143
|
+
if (!respawned && !process.env.AITI_PI_EXT_NO_BOOTSTRAP) {
|
|
3144
|
+
const script = bridgeScriptPath();
|
|
3145
|
+
const u = (() => {
|
|
3146
|
+
try {
|
|
3147
|
+
return new URL(base);
|
|
3148
|
+
} catch {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
})();
|
|
3152
|
+
if (script && u?.port) {
|
|
3153
|
+
try {
|
|
3154
|
+
respawned = true;
|
|
3155
|
+
spawnBridge(script, u.hostname || "127.0.0.1", parseInt(u.port, 10) || 8777);
|
|
3156
|
+
logFile("[aiti-devtools-pi] 桥会话内失联 → 尝试重新拉起:" + script + "(" + base + ")");
|
|
3157
|
+
} catch (e) {
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
if (streamCtrl === ctrl && !ctrl.signal.aborted) {
|
|
3162
|
+
setTimeout(() => {
|
|
3163
|
+
if (streamCtrl === ctrl && !ctrl.signal.aborted) run();
|
|
3164
|
+
}, retry);
|
|
3165
|
+
retry = Math.min(3e4, retry * 2);
|
|
3166
|
+
}
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
bridgeUp = true;
|
|
3170
|
+
currentAgentKey = reg.agentKey;
|
|
3171
|
+
registerBridgeTools(pi);
|
|
3172
|
+
let idleTimer = null;
|
|
3173
|
+
const poke = () => {
|
|
3174
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
3175
|
+
idleTimer = setTimeout(() => {
|
|
3176
|
+
try {
|
|
3177
|
+
ctrl.abort();
|
|
3178
|
+
} catch {
|
|
3179
|
+
}
|
|
3180
|
+
}, STREAM_IDLE_MS);
|
|
3181
|
+
};
|
|
3182
|
+
try {
|
|
3183
|
+
const resp = await fetch(base + "/bridge/agent/sse?agentKey=" + encodeURIComponent(reg.agentKey), {
|
|
3184
|
+
signal: ctrl.signal,
|
|
3185
|
+
headers: { Accept: "text/event-stream" }
|
|
3186
|
+
});
|
|
3187
|
+
if (!resp.ok || !resp.body) throw new Error("SSE HTTP " + resp.status);
|
|
3188
|
+
retry = 2e3;
|
|
3189
|
+
const reader = resp.body.getReader();
|
|
3190
|
+
const decoder = new TextDecoder();
|
|
3191
|
+
let buf = "";
|
|
3192
|
+
poke();
|
|
3193
|
+
while (true) {
|
|
3194
|
+
const { done, value } = await reader.read();
|
|
3195
|
+
if (done) break;
|
|
3196
|
+
poke();
|
|
3197
|
+
buf += decoder.decode(value, { stream: true });
|
|
3198
|
+
let idx;
|
|
3199
|
+
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
3200
|
+
const chunk = buf.slice(0, idx);
|
|
3201
|
+
buf = buf.slice(idx + 2);
|
|
3202
|
+
const line = chunk.split("\n").find((l) => l.startsWith("data:"));
|
|
3203
|
+
if (line) {
|
|
3204
|
+
try {
|
|
3205
|
+
onMessage(JSON.parse(line.slice(5).trim()));
|
|
3206
|
+
} catch {
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
} catch (e) {
|
|
3212
|
+
if (streamCtrl !== ctrl) return;
|
|
3213
|
+
}
|
|
3214
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
3215
|
+
setTimeout(() => {
|
|
3216
|
+
if (streamCtrl === ctrl) run();
|
|
3217
|
+
}, retry);
|
|
3218
|
+
retry = Math.min(3e4, retry * 2);
|
|
3219
|
+
};
|
|
3220
|
+
run();
|
|
3221
|
+
}
|
|
3222
|
+
var lastUserSrc = { pageId: null, url: null, sourceLabel: null };
|
|
3223
|
+
function injectAgentEvent(pi, m) {
|
|
3224
|
+
if (!m || typeof m !== "object") return;
|
|
3225
|
+
let text = "";
|
|
3226
|
+
if (m.type === "page_event") {
|
|
3227
|
+
text = m.text || "";
|
|
3228
|
+
} else if (m.type === "agent_push") {
|
|
3229
|
+
if (m.kind === "user_answer") text = "💬 你的交互请求有人作答(来自 aiti-devtools):" + (m.text || "");
|
|
3230
|
+
else text = "💬 人发来消息(来自 aiti-devtools):" + (m.text || "");
|
|
3231
|
+
const p = m.payload || {};
|
|
3232
|
+
if (p.pageId || p.sourceLabel) lastUserSrc = { pageId: p.pageId || null, url: p.url || null, sourceLabel: p.sourceLabel || null };
|
|
3233
|
+
}
|
|
3234
|
+
if (!text) return;
|
|
3235
|
+
logFile("[aiti-devtools-pi] 收到推送 → 注入会话:" + text.slice(0, 120));
|
|
3236
|
+
try {
|
|
3237
|
+
pi.sendUserMessage(text, { deliverAs: "steer" });
|
|
3238
|
+
} catch (e) {
|
|
3239
|
+
console.error("[aiti-devtools-pi] 注入失败", e);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
var lastAssistantText = "";
|
|
3243
|
+
function extractAssistantText(content) {
|
|
3244
|
+
if (!content) return "";
|
|
3245
|
+
if (typeof content === "string") return content.trim();
|
|
3246
|
+
if (Array.isArray(content)) {
|
|
3247
|
+
return content.map((p) => p && p.type === "text" ? String(p.text || "") : "").join("\n").trim();
|
|
3248
|
+
}
|
|
3249
|
+
if (content && content.type === "text") return String(content.text || "").trim();
|
|
3250
|
+
return "";
|
|
3251
|
+
}
|
|
3252
|
+
async function postAgentAnswer(text) {
|
|
3253
|
+
if (!text || !currentAgentKey || !bridgeUp) return;
|
|
3254
|
+
await getBridgeClient(resolveBridgeUrl()).answer(currentAgentKey, text);
|
|
3255
|
+
}
|
|
3256
|
+
function registerBridgeTools(pi) {
|
|
3257
|
+
if (toolsRegistered) return;
|
|
3258
|
+
toolsRegistered = true;
|
|
3259
|
+
for (const t of bridge.tools()) {
|
|
3260
|
+
const prefixed = "aiti_" + t.name;
|
|
3261
|
+
pi.registerTool({
|
|
3262
|
+
name: prefixed,
|
|
3263
|
+
description: t.description + "(aiti-devtools-pi 扩展直连独立桥——如与 MCP 同名工具冲突,用本前缀版)",
|
|
3264
|
+
parameters: jsonSchemaToTypeBox(t.inputSchema || { type: "object", properties: {} }),
|
|
3265
|
+
async execute(toolCallId, params, signal) {
|
|
3266
|
+
const r = await callBridgeTool(t.name, params || {}, signal, toolCallId);
|
|
3267
|
+
return wrapToolResult(r);
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
async function index_default(pi) {
|
|
3273
|
+
const BRIDGE_CFG_ENVS = /* @__PURE__ */ new Set([
|
|
3274
|
+
"PI_WEB_BRIDGE_AUTO",
|
|
3275
|
+
"PI_WEB_BRIDGE_BASE",
|
|
3276
|
+
"PI_WEB_BRIDGE_PORT",
|
|
3277
|
+
"PI_WEB_BRIDGE_LANG",
|
|
3278
|
+
// TUI 自动桥空闲超时(与 pi-web inprocess 会话同名的用户级配置;设了它不得被误判为宿主)
|
|
3279
|
+
"PI_WEB_IDLE_TIMEOUT_MS"
|
|
3280
|
+
]);
|
|
3281
|
+
if (!process.env.AITI_PI_EXT_FORCE) {
|
|
3282
|
+
let dup = false;
|
|
3283
|
+
try {
|
|
3284
|
+
const all = pi.getAllTools?.();
|
|
3285
|
+
if (Array.isArray(all)) dup = all.some((t) => t && typeof t.name === "string" && t.name.startsWith("aiti_"));
|
|
3286
|
+
} catch {
|
|
3287
|
+
}
|
|
3288
|
+
const piWebEnv = Object.keys(process.env).some((k) => k.startsWith("PI_WEB_") && !BRIDGE_CFG_ENVS.has(k));
|
|
3289
|
+
if (dup || piWebEnv) {
|
|
3290
|
+
logFile(`[aiti-devtools-pi] 跳过注册(${dup ? "工具表已有 aiti_*" : "pi-web 进程(PI_WEB_* env)——已由 pi-web 扩展注册"})`);
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
const bridgeBase = resolveBridgeUrl();
|
|
3295
|
+
await bootstrapBridge(bridgeBase);
|
|
3296
|
+
if (bridgeUp) {
|
|
3297
|
+
registerBridgeTools(pi);
|
|
3298
|
+
} else {
|
|
3299
|
+
logFile(`[aiti-devtools-pi] 桥不可达(${bridgeBase})→ 暂不注册 aiti_* 工具(会话中持续尝试连接桥,恢复后自动补注册 + 建 SSE;先起独立桥:node packages/aiti-bridge/bin/aiti-bridge.mjs)`);
|
|
3300
|
+
}
|
|
3301
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
3302
|
+
const base = resolveBridgeUrl();
|
|
3303
|
+
let piMcpCfg = null;
|
|
3304
|
+
try {
|
|
3305
|
+
piMcpCfg = join3(getAgentDir(), "mcp.json");
|
|
3306
|
+
} catch {
|
|
3307
|
+
}
|
|
3308
|
+
autoRegisterAitiShim({ targets: ["pi"], configPaths: piMcpCfg ? [piMcpCfg] : void 0, log: logFile });
|
|
3309
|
+
let sid = null;
|
|
3310
|
+
try {
|
|
3311
|
+
sid = ctx.sessionManager.getSessionId() || null;
|
|
3312
|
+
} catch {
|
|
3313
|
+
}
|
|
3314
|
+
if (!sid) {
|
|
3315
|
+
try {
|
|
3316
|
+
const f = ctx.sessionManager.getSessionFile();
|
|
3317
|
+
if (f) sid = f.split(/[/\\]/).pop()?.replace(/\.jsonl$/, "") || null;
|
|
3318
|
+
} catch {
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
if (!sid) sid = "s_" + Date.now().toString(36);
|
|
3322
|
+
currentSessionId = sid;
|
|
3323
|
+
currentCwd = ctx.cwd || "";
|
|
3324
|
+
const name = agentDisplayName();
|
|
3325
|
+
await probeBridge2(base, 800);
|
|
3326
|
+
if (bridgeUp) registerBridgeTools(pi);
|
|
3327
|
+
connectAgentStream(pi, base, sid, name, currentCwd, (m) => injectAgentEvent(pi, m));
|
|
3328
|
+
logFile(`[aiti-devtools-pi] session_start · 桥${bridgeUp ? "已连接" : "不可达(后台重试中)"} · session=${sid}`);
|
|
3329
|
+
ctx.ui.notify(
|
|
3330
|
+
bridgeUp ? `🧰 aiti-devtools 已注册:${currentAgentKey || "…"}(桥 ${base};页面注入 <script src="${base}/aiti-launcher.js">)` : `🧰 aiti-devtools 桥不可达:${base}(后台持续尝试连接,桥恢复后页面消息自动注入;先起独立桥:node packages/aiti-bridge/bin/aiti-bridge.mjs)`,
|
|
3331
|
+
bridgeUp ? "info" : "warning"
|
|
3332
|
+
);
|
|
3333
|
+
});
|
|
3334
|
+
pi.on("session_shutdown", () => {
|
|
3335
|
+
if (lastAssistantText) {
|
|
3336
|
+
const t = lastAssistantText;
|
|
3337
|
+
lastAssistantText = "";
|
|
3338
|
+
logFile("[aiti-devtools-pi] session_shutdown 补发回答 → ", t.slice(0, 60));
|
|
3339
|
+
void postAgentAnswer(t);
|
|
3340
|
+
}
|
|
3341
|
+
currentSessionId = null;
|
|
3342
|
+
currentAgentKey = null;
|
|
3343
|
+
currentCwd = "";
|
|
3344
|
+
lastAssistantText = "";
|
|
3345
|
+
if (streamCtrl) {
|
|
3346
|
+
try {
|
|
3347
|
+
streamCtrl.abort();
|
|
3348
|
+
} catch {
|
|
3349
|
+
}
|
|
3350
|
+
streamCtrl = null;
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
pi.on("message_end", async (event) => {
|
|
3354
|
+
if (!event || event.message?.role !== "assistant") return;
|
|
3355
|
+
const t = extractAssistantText(event.message.content);
|
|
3356
|
+
if (t) lastAssistantText = t;
|
|
3357
|
+
});
|
|
3358
|
+
pi.on("agent_settled", async () => {
|
|
3359
|
+
if (!lastAssistantText) return;
|
|
3360
|
+
const t = lastAssistantText;
|
|
3361
|
+
lastAssistantText = "";
|
|
3362
|
+
if (!bridgeUp) {
|
|
3363
|
+
logFile("[aiti-devtools-pi] 桥未联通 → 跳过回答回桥(不发总结)");
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
await postAgentAnswer(t);
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
export {
|
|
3370
|
+
index_default as default
|
|
3371
|
+
};
|