@attson/atwebpilot-mcp 0.0.55 → 0.0.57
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/README.md +72 -7
- package/dist/index.js +883 -292
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -217,6 +217,14 @@ var StepSchema = z4.object({
|
|
|
217
217
|
tool: z4.string(),
|
|
218
218
|
args: z4.unknown()
|
|
219
219
|
});
|
|
220
|
+
var DerivedTabFields = {
|
|
221
|
+
/** true when the receiving connection owns this tab */
|
|
222
|
+
mine: z4.boolean().optional(),
|
|
223
|
+
/** true when some *other* connection owns it */
|
|
224
|
+
busy: z4.boolean().optional(),
|
|
225
|
+
/** the owning session's label, present only when busy */
|
|
226
|
+
busy_label: z4.string().optional()
|
|
227
|
+
};
|
|
220
228
|
var HelloSchema = z4.object({
|
|
221
229
|
...EnvelopeFields,
|
|
222
230
|
type: z4.literal("HELLO"),
|
|
@@ -227,12 +235,20 @@ var HelloSchema = z4.object({
|
|
|
227
235
|
chrome: z4.string()
|
|
228
236
|
}),
|
|
229
237
|
capabilities: z4.array(z4.string()),
|
|
238
|
+
/**
|
|
239
|
+
* Tool names this worker can actually execute. Optional for backward
|
|
240
|
+
* compatibility: an extension predating Plan 32 does not send it, and the
|
|
241
|
+
* server then falls back to the legacy 19-tool surface rather than
|
|
242
|
+
* advertising tools that would fail at call time with "unknown tool".
|
|
243
|
+
*/
|
|
244
|
+
supported_tools: z4.array(z4.string()).optional(),
|
|
230
245
|
attended: z4.boolean(),
|
|
231
246
|
available_tabs: z4.array(
|
|
232
247
|
z4.object({
|
|
233
248
|
tab_id: z4.string(),
|
|
234
249
|
url: z4.string(),
|
|
235
|
-
title: z4.string().optional()
|
|
250
|
+
title: z4.string().optional(),
|
|
251
|
+
...DerivedTabFields
|
|
236
252
|
})
|
|
237
253
|
),
|
|
238
254
|
saved_tools: z4.array(
|
|
@@ -383,6 +399,24 @@ var SidepanelStateReplySchema = z4.object({
|
|
|
383
399
|
lastSystemNote: z4.string().optional()
|
|
384
400
|
}).optional()
|
|
385
401
|
});
|
|
402
|
+
var SessionOpenedSchema = z4.object({
|
|
403
|
+
...EnvelopeFields,
|
|
404
|
+
type: z4.literal("SESSION_OPENED"),
|
|
405
|
+
session_id: z4.string(),
|
|
406
|
+
tab_id: z4.string()
|
|
407
|
+
});
|
|
408
|
+
var TabsUpdateSchema = z4.object({
|
|
409
|
+
...EnvelopeFields,
|
|
410
|
+
type: z4.literal("TABS_UPDATE"),
|
|
411
|
+
tabs: z4.array(
|
|
412
|
+
z4.object({
|
|
413
|
+
tab_id: z4.string(),
|
|
414
|
+
url: z4.string(),
|
|
415
|
+
title: z4.string().optional(),
|
|
416
|
+
...DerivedTabFields
|
|
417
|
+
})
|
|
418
|
+
)
|
|
419
|
+
});
|
|
386
420
|
var ClientToServerSchema = z4.discriminatedUnion("type", [
|
|
387
421
|
HelloSchema,
|
|
388
422
|
PingSchema,
|
|
@@ -392,7 +426,8 @@ var ClientToServerSchema = z4.discriminatedUnion("type", [
|
|
|
392
426
|
SessionEventSchema,
|
|
393
427
|
StateSnapshotSchema,
|
|
394
428
|
ChatEventSchema,
|
|
395
|
-
SidepanelStateReplySchema
|
|
429
|
+
SidepanelStateReplySchema,
|
|
430
|
+
TabsUpdateSchema
|
|
396
431
|
]);
|
|
397
432
|
var ServerToClientSchema = z4.discriminatedUnion("type", [
|
|
398
433
|
WelcomeSchema,
|
|
@@ -402,7 +437,8 @@ var ServerToClientSchema = z4.discriminatedUnion("type", [
|
|
|
402
437
|
CloseSessionSchema,
|
|
403
438
|
StartChatSessionSchema,
|
|
404
439
|
AbortSessionSchema,
|
|
405
|
-
ReadSidepanelStateSchema
|
|
440
|
+
ReadSidepanelStateSchema,
|
|
441
|
+
SessionOpenedSchema
|
|
406
442
|
]);
|
|
407
443
|
var ProtocolMessageSchema = z4.union([ClientToServerSchema, ServerToClientSchema]);
|
|
408
444
|
|
|
@@ -613,19 +649,26 @@ var CAPABILITIES = [
|
|
|
613
649
|
"httpRequest:cookied",
|
|
614
650
|
"runJS:scanned",
|
|
615
651
|
"runJS:unsafe",
|
|
616
|
-
"tab:open"
|
|
652
|
+
"tab:open",
|
|
653
|
+
// Plan 32 — page-event recorder
|
|
654
|
+
"read:console",
|
|
655
|
+
"read:network",
|
|
656
|
+
"read:network-body"
|
|
617
657
|
];
|
|
618
658
|
var IMPLICIT_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
619
659
|
"read:dom",
|
|
620
660
|
"read:image",
|
|
621
|
-
"nav:tab"
|
|
661
|
+
"nav:tab",
|
|
662
|
+
"read:console"
|
|
622
663
|
]);
|
|
623
664
|
var DANGEROUS_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
624
665
|
"read:storage",
|
|
625
666
|
"submit:form",
|
|
626
667
|
"upload:file",
|
|
627
668
|
"httpRequest:cookied",
|
|
628
|
-
"runJS:unsafe"
|
|
669
|
+
"runJS:unsafe",
|
|
670
|
+
// response headers routinely carry Authorization / Set-Cookie / bearer tokens
|
|
671
|
+
"read:network-body"
|
|
629
672
|
]);
|
|
630
673
|
function isCapability(s) {
|
|
631
674
|
return CAPABILITIES.includes(s);
|
|
@@ -703,6 +746,26 @@ function capabilityForTool(tool, opts) {
|
|
|
703
746
|
// can be added later if a finer distinction is needed.
|
|
704
747
|
case "writeStorage":
|
|
705
748
|
return "read:storage";
|
|
749
|
+
// Plan 32 — playwright parity
|
|
750
|
+
case "consoleMessages":
|
|
751
|
+
return "read:console";
|
|
752
|
+
case "networkRequests":
|
|
753
|
+
return "read:network";
|
|
754
|
+
case "networkRequestDetail":
|
|
755
|
+
return "read:network-body";
|
|
756
|
+
case "recorderConfig":
|
|
757
|
+
return opts?.recorderArmsBodies ? "read:network-body" : "read:network";
|
|
758
|
+
case "handleDialog":
|
|
759
|
+
case "drag":
|
|
760
|
+
return "interact:form";
|
|
761
|
+
case "drop":
|
|
762
|
+
return opts?.dropHasFiles ? "upload:file" : "interact:form";
|
|
763
|
+
case "navigateBack":
|
|
764
|
+
case "navigateForward":
|
|
765
|
+
case "resize":
|
|
766
|
+
return "nav:tab";
|
|
767
|
+
case "findElements":
|
|
768
|
+
return "read:dom";
|
|
706
769
|
default: {
|
|
707
770
|
const _exhaustive = tool;
|
|
708
771
|
throw new Error(`capabilityForTool: unknown tool ${_exhaustive}`);
|
|
@@ -739,7 +802,11 @@ var Dispatcher = class {
|
|
|
739
802
|
return fail("SessionExpired", `Session ${input.session_id} is expired`);
|
|
740
803
|
if (session.state !== "active")
|
|
741
804
|
return fail("InternalError", `Session ${input.session_id} state=${session.state}`);
|
|
742
|
-
const required = input.kind === "extension_tool" ? capabilityForTool(input.tool, {
|
|
805
|
+
const required = input.kind === "extension_tool" ? capabilityForTool(input.tool, {
|
|
806
|
+
httpCookied: input.httpCookied,
|
|
807
|
+
dropHasFiles: input.dropHasFiles,
|
|
808
|
+
recorderArmsBodies: input.recorderArmsBodies
|
|
809
|
+
}) : input.kind === "capability" ? input.capability : capabilityForRunJs(input.unsafe);
|
|
743
810
|
if (!scopeCovers(session.scope, required)) {
|
|
744
811
|
return fail("PermissionDenied", `Capability ${required} not in session scope`, {
|
|
745
812
|
denied_capability: required
|
|
@@ -780,8 +847,12 @@ var Coordinator = class {
|
|
|
780
847
|
catalog;
|
|
781
848
|
dispatcher;
|
|
782
849
|
hub;
|
|
850
|
+
clock;
|
|
851
|
+
idGen;
|
|
783
852
|
constructor(deps) {
|
|
784
853
|
this.hub = deps.hub;
|
|
854
|
+
this.clock = deps.clock;
|
|
855
|
+
this.idGen = deps.idGen;
|
|
785
856
|
this.sessions = new SessionManager(deps.clock, deps.idGen);
|
|
786
857
|
this.workers = new WorkerRegistry(deps.clock);
|
|
787
858
|
this.catalog = new Catalog(this.workers);
|
|
@@ -803,7 +874,17 @@ var Coordinator = class {
|
|
|
803
874
|
}
|
|
804
875
|
// === Session lifecycle ===
|
|
805
876
|
openSession(input) {
|
|
806
|
-
|
|
877
|
+
const session = this.sessions.open(input);
|
|
878
|
+
void this.hub.send(session.worker_id, {
|
|
879
|
+
type: "SESSION_OPENED",
|
|
880
|
+
nonce: this.idGen.next("nonce"),
|
|
881
|
+
ts: this.clock.now(),
|
|
882
|
+
protocol_version: PROTOCOL_VERSION,
|
|
883
|
+
session_id: session.id,
|
|
884
|
+
tab_id: session.tab_id
|
|
885
|
+
}).catch(() => {
|
|
886
|
+
});
|
|
887
|
+
return session;
|
|
807
888
|
}
|
|
808
889
|
closeSession(id) {
|
|
809
890
|
this.sessions.close(id);
|
|
@@ -835,232 +916,6 @@ var Coordinator = class {
|
|
|
835
916
|
}
|
|
836
917
|
};
|
|
837
918
|
|
|
838
|
-
// src/loopback-ws-hub.ts
|
|
839
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
840
|
-
var HEARTBEAT_INTERVAL_MS = 2e4;
|
|
841
|
-
var DEFAULT_KEEPALIVE_INTERVAL_MS = 15e3;
|
|
842
|
-
var LoopbackWSHub = class {
|
|
843
|
-
constructor(opts) {
|
|
844
|
-
this.opts = opts;
|
|
845
|
-
this.execTimeoutMs = opts.execTimeoutMs ?? 3e4;
|
|
846
|
-
this.keepaliveIntervalMs = opts.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
847
|
-
this.wss = new WebSocketServer({
|
|
848
|
-
port: opts.port,
|
|
849
|
-
path: "/worker",
|
|
850
|
-
handleProtocols: (protocols) => [...protocols][0] ?? false
|
|
851
|
-
});
|
|
852
|
-
this.wss.on("connection", (socket, req) => this.onConnection(socket, req));
|
|
853
|
-
}
|
|
854
|
-
opts;
|
|
855
|
-
wss;
|
|
856
|
-
byWorker = /* @__PURE__ */ new Map();
|
|
857
|
-
workerOf = /* @__PURE__ */ new Map();
|
|
858
|
-
pending = /* @__PURE__ */ new Map();
|
|
859
|
-
msgHandlers = [];
|
|
860
|
-
disconnectHandlers = [];
|
|
861
|
-
execTimeoutMs;
|
|
862
|
-
keepaliveIntervalMs;
|
|
863
|
-
keepaliveTimers = /* @__PURE__ */ new Map();
|
|
864
|
-
ready() {
|
|
865
|
-
return new Promise((resolve) => {
|
|
866
|
-
const addr = this.wss.address();
|
|
867
|
-
if (addr) return resolve(addr.port);
|
|
868
|
-
this.wss.on(
|
|
869
|
-
"listening",
|
|
870
|
-
() => resolve(this.wss.address().port)
|
|
871
|
-
);
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
close() {
|
|
875
|
-
for (const p of this.pending.values()) {
|
|
876
|
-
clearTimeout(p.timer);
|
|
877
|
-
p.reject(new Error("hub closing"));
|
|
878
|
-
}
|
|
879
|
-
this.pending.clear();
|
|
880
|
-
for (const timer of this.keepaliveTimers.values()) clearInterval(timer);
|
|
881
|
-
this.keepaliveTimers.clear();
|
|
882
|
-
for (const socket of this.wss.clients) {
|
|
883
|
-
socket.terminate();
|
|
884
|
-
}
|
|
885
|
-
return new Promise((resolve) => this.wss.close(() => resolve()));
|
|
886
|
-
}
|
|
887
|
-
tokenOk(req) {
|
|
888
|
-
if (!this.opts.token) return true;
|
|
889
|
-
const offered = String(req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim());
|
|
890
|
-
return offered.includes(`bearer.${this.opts.token}`);
|
|
891
|
-
}
|
|
892
|
-
onConnection(socket, req) {
|
|
893
|
-
if (!this.tokenOk(req)) {
|
|
894
|
-
socket.close(4401, "bad token");
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
socket.on("message", (raw) => this.onMessageRaw(socket, raw.toString()));
|
|
898
|
-
socket.on("close", () => this.onSocketClose(socket));
|
|
899
|
-
socket.on("error", () => {
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
onMessageRaw(socket, raw) {
|
|
903
|
-
let parsed;
|
|
904
|
-
try {
|
|
905
|
-
parsed = JSON.parse(raw);
|
|
906
|
-
} catch {
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const r = ClientToServerSchema.safeParse(parsed);
|
|
910
|
-
if (!r.success) return;
|
|
911
|
-
const msg = r.data;
|
|
912
|
-
if (msg.type === "HELLO") {
|
|
913
|
-
const existingSocket = this.byWorker.get(msg.worker_id);
|
|
914
|
-
if (existingSocket && existingSocket !== socket) {
|
|
915
|
-
this.workerOf.delete(existingSocket);
|
|
916
|
-
for (const [req_id, p] of [...this.pending]) {
|
|
917
|
-
if (p.worker_id === msg.worker_id) {
|
|
918
|
-
clearTimeout(p.timer);
|
|
919
|
-
this.pending.delete(req_id);
|
|
920
|
-
p.reject(new Error(`worker ${msg.worker_id} reconnected, exec cancelled`));
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
existingSocket.terminate();
|
|
924
|
-
}
|
|
925
|
-
this.byWorker.set(msg.worker_id, socket);
|
|
926
|
-
this.workerOf.set(socket, msg.worker_id);
|
|
927
|
-
this.rawSend(socket, {
|
|
928
|
-
type: "WELCOME",
|
|
929
|
-
nonce: this.opts.idGen.next("nonce"),
|
|
930
|
-
ts: this.opts.clock.now(),
|
|
931
|
-
protocol_version: PROTOCOL_VERSION,
|
|
932
|
-
server_time: this.opts.clock.now(),
|
|
933
|
-
heartbeat_interval_ms: HEARTBEAT_INTERVAL_MS
|
|
934
|
-
});
|
|
935
|
-
this.startKeepalive(socket);
|
|
936
|
-
for (const h of this.msgHandlers) h(msg.worker_id, msg);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
if (msg.type === "RESULT") {
|
|
940
|
-
const p = this.pending.get(msg.req_id);
|
|
941
|
-
if (p) {
|
|
942
|
-
clearTimeout(p.timer);
|
|
943
|
-
this.pending.delete(msg.req_id);
|
|
944
|
-
p.resolve(msg);
|
|
945
|
-
}
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
if (msg.type === "PING") {
|
|
949
|
-
this.rawSend(socket, {
|
|
950
|
-
type: "PONG",
|
|
951
|
-
nonce: this.opts.idGen.next("nonce"),
|
|
952
|
-
ts: this.opts.clock.now(),
|
|
953
|
-
protocol_version: PROTOCOL_VERSION,
|
|
954
|
-
echo_nonce: msg.nonce
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
const wid = this.workerOf.get(socket);
|
|
958
|
-
if (wid) {
|
|
959
|
-
for (const h of this.msgHandlers) h(wid, msg);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
onSocketClose(socket) {
|
|
963
|
-
this.stopKeepalive(socket);
|
|
964
|
-
const wid = this.workerOf.get(socket);
|
|
965
|
-
if (!wid) return;
|
|
966
|
-
this.workerOf.delete(socket);
|
|
967
|
-
this.byWorker.delete(wid);
|
|
968
|
-
for (const [req_id, p] of [...this.pending]) {
|
|
969
|
-
if (p.worker_id === wid) {
|
|
970
|
-
clearTimeout(p.timer);
|
|
971
|
-
this.pending.delete(req_id);
|
|
972
|
-
p.reject(new Error(`worker ${wid} disconnected`));
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
for (const h of this.disconnectHandlers) h(wid);
|
|
976
|
-
}
|
|
977
|
-
startKeepalive(socket) {
|
|
978
|
-
if (this.keepaliveIntervalMs <= 0) return;
|
|
979
|
-
if (this.keepaliveTimers.has(socket)) return;
|
|
980
|
-
const timer = setInterval(() => {
|
|
981
|
-
if (socket.readyState !== WebSocket.OPEN) return;
|
|
982
|
-
this.rawSend(socket, {
|
|
983
|
-
type: "PONG",
|
|
984
|
-
nonce: this.opts.idGen.next("nonce"),
|
|
985
|
-
ts: this.opts.clock.now(),
|
|
986
|
-
protocol_version: PROTOCOL_VERSION,
|
|
987
|
-
echo_nonce: "server-keepalive"
|
|
988
|
-
});
|
|
989
|
-
}, this.keepaliveIntervalMs);
|
|
990
|
-
if (typeof timer.unref === "function") {
|
|
991
|
-
timer.unref();
|
|
992
|
-
}
|
|
993
|
-
this.keepaliveTimers.set(socket, timer);
|
|
994
|
-
}
|
|
995
|
-
stopKeepalive(socket) {
|
|
996
|
-
const timer = this.keepaliveTimers.get(socket);
|
|
997
|
-
if (!timer) return;
|
|
998
|
-
clearInterval(timer);
|
|
999
|
-
this.keepaliveTimers.delete(socket);
|
|
1000
|
-
}
|
|
1001
|
-
rawSend(socket, msg) {
|
|
1002
|
-
const r = ServerToClientSchema.safeParse(msg);
|
|
1003
|
-
if (!r.success) {
|
|
1004
|
-
console.error("[hub] outgoing failed schema", r.error);
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
1008
|
-
socket.send(JSON.stringify(msg));
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
exec(worker_id, params) {
|
|
1012
|
-
const socket = this.byWorker.get(worker_id);
|
|
1013
|
-
if (!socket)
|
|
1014
|
-
return Promise.reject(new Error(`worker ${worker_id} not connected`));
|
|
1015
|
-
const req_id = this.opts.idGen.next("req");
|
|
1016
|
-
return new Promise((resolve, reject) => {
|
|
1017
|
-
const timer = setTimeout(() => {
|
|
1018
|
-
this.pending.delete(req_id);
|
|
1019
|
-
reject(
|
|
1020
|
-
new Error(`EXEC ${req_id} timeout after ${this.execTimeoutMs}ms`)
|
|
1021
|
-
);
|
|
1022
|
-
}, this.execTimeoutMs);
|
|
1023
|
-
this.pending.set(req_id, { resolve, reject, timer, worker_id });
|
|
1024
|
-
this.rawSend(socket, {
|
|
1025
|
-
type: "EXEC",
|
|
1026
|
-
nonce: this.opts.idGen.next("nonce"),
|
|
1027
|
-
ts: this.opts.clock.now(),
|
|
1028
|
-
protocol_version: PROTOCOL_VERSION,
|
|
1029
|
-
req_id,
|
|
1030
|
-
session_id: params.session_id,
|
|
1031
|
-
tab_id: params.tab_id,
|
|
1032
|
-
step: { kind: "tool", tool: params.step.tool, args: params.step.args }
|
|
1033
|
-
});
|
|
1034
|
-
if (this.byWorker.get(worker_id) !== socket) {
|
|
1035
|
-
clearTimeout(timer);
|
|
1036
|
-
this.pending.delete(req_id);
|
|
1037
|
-
reject(new Error(`worker ${worker_id} disconnected`));
|
|
1038
|
-
}
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
async send(worker_id, msg) {
|
|
1042
|
-
const socket = this.byWorker.get(worker_id);
|
|
1043
|
-
if (!socket) throw new Error(`worker ${worker_id} not connected`);
|
|
1044
|
-
this.rawSend(socket, msg);
|
|
1045
|
-
}
|
|
1046
|
-
onMessage(handler) {
|
|
1047
|
-
this.msgHandlers.push(handler);
|
|
1048
|
-
}
|
|
1049
|
-
onDisconnect(handler) {
|
|
1050
|
-
this.disconnectHandlers.push(handler);
|
|
1051
|
-
}
|
|
1052
|
-
connectedWorkers() {
|
|
1053
|
-
return [...this.byWorker.keys()];
|
|
1054
|
-
}
|
|
1055
|
-
// Intentionally fire-and-forget: actual cleanup (byWorker/workerOf removal and
|
|
1056
|
-
// pending-exec rejection) happens asynchronously via the socket's close event.
|
|
1057
|
-
// v1 callers do not await this method.
|
|
1058
|
-
async disconnect(worker_id, _reason) {
|
|
1059
|
-
const socket = this.byWorker.get(worker_id);
|
|
1060
|
-
if (socket) socket.close();
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
919
|
// src/mcp-server.ts
|
|
1065
920
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1066
921
|
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -1253,6 +1108,8 @@ var TOOL_DEFS = [
|
|
|
1253
1108
|
ms: { type: "integer" },
|
|
1254
1109
|
selector: { type: "string" },
|
|
1255
1110
|
timeoutMs: { type: "integer", default: 5e3 },
|
|
1111
|
+
text: { type: "string", description: "\u7B49\u5F85\u9875\u9762\u51FA\u73B0\u8BE5\u6587\u672C" },
|
|
1112
|
+
textGone: { type: "string", description: "\u7B49\u5F85\u8BE5\u6587\u672C\u6D88\u5931" },
|
|
1256
1113
|
tabId: TAB_ID_FIELD
|
|
1257
1114
|
}
|
|
1258
1115
|
}
|
|
@@ -1265,6 +1122,12 @@ var TOOL_DEFS = [
|
|
|
1265
1122
|
properties: {
|
|
1266
1123
|
selector: { type: "string" },
|
|
1267
1124
|
required: { type: "boolean", default: true },
|
|
1125
|
+
doubleClick: { type: "boolean", default: false },
|
|
1126
|
+
button: { type: "string", enum: ["left", "right", "middle"], default: "left" },
|
|
1127
|
+
modifiers: {
|
|
1128
|
+
type: "array",
|
|
1129
|
+
items: { type: "string", enum: ["Alt", "Control", "Meta", "Shift"] }
|
|
1130
|
+
},
|
|
1268
1131
|
tabId: TAB_ID_FIELD
|
|
1269
1132
|
},
|
|
1270
1133
|
required: ["selector"]
|
|
@@ -1308,6 +1171,12 @@ var TOOL_DEFS = [
|
|
|
1308
1171
|
selector: { type: "string" },
|
|
1309
1172
|
value: { type: "string" },
|
|
1310
1173
|
clear: { type: "boolean", default: true },
|
|
1174
|
+
slowly: {
|
|
1175
|
+
type: "boolean",
|
|
1176
|
+
default: false,
|
|
1177
|
+
description: "\u9010\u5B57\u7B26\u89E6\u53D1 keydown/keypress/input/keyup\uFF0C\u5BF9\u4ED8\u53D7\u63A7\u7EC4\u4EF6"
|
|
1178
|
+
},
|
|
1179
|
+
submit: { type: "boolean", default: false, description: "\u586B\u5B8C\u6309\u4E00\u6B21 Enter" },
|
|
1311
1180
|
tabId: TAB_ID_FIELD
|
|
1312
1181
|
},
|
|
1313
1182
|
required: ["selector", "value"]
|
|
@@ -1499,6 +1368,9 @@ var TOOL_DEFS = [
|
|
|
1499
1368
|
blockId: { type: "string", description: "\u53EF\u9009\uFF1Apage-index \u8FD4\u56DE\u7684 blockId\uFF0C\u7528\u4E8E\u5C40\u90E8\u89C6\u89C9\u8BC1\u636E" },
|
|
1500
1369
|
indexId: { type: "string", description: "\u53EF\u9009\uFF1A\u4EA7\u751F blockId \u7684 indexId\uFF0C\u907F\u514D refresh \u540E\u8BEF\u8BFB" },
|
|
1501
1370
|
highlightMs: { type: "integer", default: 1500, description: "\u622A\u56FE\u524D\u76EE\u6807\u9AD8\u4EAE\u6301\u7EED\u65F6\u95F4\uFF0C250-5000ms" },
|
|
1371
|
+
fullPage: { type: "boolean", default: false, description: "\u6EDA\u52A8\u5206\u6BB5\u622A\u56FE\u540E\u62FC\u63A5\u6574\u9875" },
|
|
1372
|
+
format: { type: "string", enum: ["png", "jpeg"], default: "png" },
|
|
1373
|
+
scale: { type: "number", default: 1, description: "0.1\u20131\uFF0C\u7F29\u5C0F\u53EF\u7701 token" },
|
|
1502
1374
|
tabId: TAB_ID_FIELD
|
|
1503
1375
|
}
|
|
1504
1376
|
}
|
|
@@ -1757,89 +1629,318 @@ var TOOL_DEFS = [
|
|
|
1757
1629
|
},
|
|
1758
1630
|
required: ["store", "key", "value"]
|
|
1759
1631
|
}
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1632
|
+
},
|
|
1633
|
+
// ── Plan 32 — playwright parity ──────────────────────────────────────────
|
|
1634
|
+
{
|
|
1635
|
+
name: "consoleMessages",
|
|
1636
|
+
description: "[OBSERVE] \u8BFB\u53D6\u672C\u9875 console \u65E5\u5FD7\u4E0E\u672A\u6355\u83B7\u9519\u8BEF\u3002\u8FD4\u56DE\u91CC\u5E26 backend \u5B57\u6BB5\uFF1Amain-world \u6863\u770B\u4E0D\u5230\u811A\u672C\u6CE8\u5165\u4E4B\u524D\u7684\u6D88\u606F\u548C\u6D4F\u89C8\u5668\u7EA7 CORS/CSP \u62A5\u9519\uFF0Ccdp \u6863\u80FD\u770B\u5230\u3002\n\u793A\u4F8B\uFF1A\u53EA\u770B\u62A5\u9519 { level: 'error', limit: 50 }\uFF1B\u589E\u91CF\u8F6E\u8BE2 { sinceId: \u4E0A\u6B21\u8FD4\u56DE\u7684\u6700\u5927 id }",
|
|
1637
|
+
input_schema: {
|
|
1638
|
+
type: "object",
|
|
1639
|
+
properties: {
|
|
1640
|
+
level: { type: "string", enum: ["log", "info", "warn", "error", "debug", "trace"] },
|
|
1641
|
+
limit: { type: "integer", default: 100 },
|
|
1642
|
+
sinceId: { type: "integer", description: "\u53EA\u8FD4\u56DE id \u5927\u4E8E\u6B64\u503C\u7684\u6D88\u606F\uFF0C\u7528\u4E8E\u589E\u91CF\u8BFB\u53D6" },
|
|
1643
|
+
tabId: TAB_ID_FIELD
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
name: "networkRequests",
|
|
1649
|
+
description: "[OBSERVE] \u5217\u51FA\u672C\u9875\u53D1\u51FA\u7684\u7F51\u7EDC\u8BF7\u6C42\u6458\u8981\uFF08method / url / status / \u8017\u65F6\uFF09\u3002\u9ED8\u8BA4\u9690\u85CF PerformanceObserver \u89C2\u6D4B\u5230\u7684\u9759\u6001\u8D44\u6E90\uFF0CincludeStatic:true \u624D\u5E26\u4E0A\u3002\n\u8981\u770B headers \u6216 body \u7528 networkRequestDetail\u3002",
|
|
1650
|
+
input_schema: {
|
|
1651
|
+
type: "object",
|
|
1652
|
+
properties: {
|
|
1653
|
+
urlPattern: { type: "string", description: "\u5B50\u4E32\u5339\u914D\uFF1B\u7528 /re/ \u6216 /re/i \u5305\u8D77\u6765\u5219\u6309\u6B63\u5219" },
|
|
1654
|
+
method: { type: "string" },
|
|
1655
|
+
status: { type: "integer" },
|
|
1656
|
+
includeStatic: { type: "boolean", default: false },
|
|
1657
|
+
limit: { type: "integer", default: 50 },
|
|
1658
|
+
sinceId: { type: "integer" },
|
|
1659
|
+
tabId: TAB_ID_FIELD
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
name: "networkRequestDetail",
|
|
1665
|
+
description: "[OBSERVE] \u8BFB\u53D6\u5355\u6761\u8BF7\u6C42\u7684 headers \u4E0E body\u3002**dangerous**\uFF1A\u54CD\u5E94\u5934\u91CC\u53EF\u80FD\u6709 Authorization / Set-Cookie / token\u3002\nmain-world \u6863\u9700\u5148 recorderConfig({bodies:true}) \u624D\u4F1A\u8BB0\u5F55 body\uFF0C\u4E14\u53EA\u8BB0 256KB \u4EE5\u5185\u7684\u6587\u672C\u7C7B\u54CD\u5E94\uFF1Bcdp \u6863\u76F4\u63A5\u53EF\u53D6\u3002",
|
|
1666
|
+
input_schema: {
|
|
1667
|
+
type: "object",
|
|
1668
|
+
properties: {
|
|
1669
|
+
id: { type: "integer", description: "networkRequests \u8FD4\u56DE\u7684\u6761\u76EE id" },
|
|
1670
|
+
part: {
|
|
1671
|
+
type: "string",
|
|
1672
|
+
enum: ["request-headers", "request-body", "response-headers", "response-body"],
|
|
1673
|
+
description: "\u53EA\u53D6\u5176\u4E2D\u4E00\u90E8\u5206\uFF1B\u7701\u7565\u5219\u5168\u7ED9"
|
|
1674
|
+
},
|
|
1675
|
+
tabId: TAB_ID_FIELD
|
|
1676
|
+
},
|
|
1677
|
+
required: ["id"]
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
name: "handleDialog",
|
|
1682
|
+
description: "[OBSERVE] \u8BBE\u5B9A alert / confirm / prompt \u7684\u5E94\u7B54\u7B56\u7565\uFF0C\u5E76\u8FD4\u56DE\u5DF2\u8BB0\u5F55\u7684\u5F39\u7A97\u3002\nmain-world \u6863\u4E0B\u5F39\u7A97\u662F\u540C\u6B65\u7684\uFF0C\u65E0\u6CD5\u6302\u8D77\u7B49\u4F60\u51B3\u5B9A\uFF0C\u6240\u4EE5\u8FD9\u91CC\u8BBE\u7684\u662F**\u9884\u5148\u7B56\u7565**\uFF1A\u8C03\u7528\u4E4B\u540E\u53D1\u751F\u7684\u5F39\u7A97\u6309\u6B64\u5904\u7406\u3002cdp \u6863\u4E0B\u5F39\u7A97\u771F\u6302\u8D77\uFF0C\u672C\u8C03\u7528\u4F1A\u7ACB\u5373\u5E94\u7B54\u5F53\u524D\u6302\u8D77\u7684\u5F39\u7A97\u3002\n\u672A\u8C03\u7528\u8FC7\u672C\u5DE5\u5177\u65F6\u5F39\u7A97\u4FDD\u6301\u539F\u751F\u884C\u4E3A\uFF08passthrough\uFF09\uFF0C\u9875\u9762\u8868\u73B0\u4E0E\u6CA1\u88C5\u6269\u5C55\u4E00\u81F4\u3002",
|
|
1683
|
+
input_schema: {
|
|
1684
|
+
type: "object",
|
|
1685
|
+
properties: {
|
|
1686
|
+
accept: { type: "boolean", description: "true=\u786E\u5B9A\uFF0Cfalse=\u53D6\u6D88" },
|
|
1687
|
+
promptText: { type: "string", description: "prompt \u5F39\u7A97\u586B\u5165\u7684\u6587\u672C" },
|
|
1688
|
+
scope: { type: "string", enum: ["next", "all"], default: "next" },
|
|
1689
|
+
tabId: TAB_ID_FIELD
|
|
1690
|
+
},
|
|
1691
|
+
required: ["accept"]
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
name: "recorderConfig",
|
|
1696
|
+
description: "[OBSERVE] \u5F00\u5173\u9875\u9762\u4E8B\u4EF6\u5F55\u5236\u3002bodies:true \u6253\u5F00\u8BF7\u6C42/\u54CD\u5E94 body \u6355\u83B7\uFF08\u9ED8\u8BA4\u5173\uFF0C\u6709\u5185\u5B58\u4EE3\u4EF7\uFF09\uFF1Bdialog:true \u8BA9\u5F39\u7A97\u8D70\u7B56\u7565\u800C\u4E0D\u662F\u539F\u751F\u884C\u4E3A\uFF1Bclear \u6E05\u7A7A\u7F13\u51B2\u3002\u7701\u7565\u7684\u5B57\u6BB5\u4FDD\u6301\u539F\u6837\u3002",
|
|
1697
|
+
input_schema: {
|
|
1698
|
+
type: "object",
|
|
1699
|
+
properties: {
|
|
1700
|
+
console: { type: "boolean" },
|
|
1701
|
+
network: { type: "boolean" },
|
|
1702
|
+
bodies: { type: "boolean" },
|
|
1703
|
+
dialog: { type: "boolean" },
|
|
1704
|
+
clear: {
|
|
1705
|
+
type: "array",
|
|
1706
|
+
items: { type: "string", enum: ["console", "network", "dialog"] }
|
|
1707
|
+
},
|
|
1708
|
+
tabId: TAB_ID_FIELD
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
name: "navigateBack",
|
|
1714
|
+
description: "[FLOW] \u540E\u9000\u4E00\u9875\u3002\u5DF2\u5728\u5386\u53F2\u8D77\u70B9\u65F6\u8FD4\u56DE { ok:false, reason }\uFF0C\u4E0D\u629B\u9519\u3002",
|
|
1715
|
+
input_schema: { type: "object", properties: { tabId: TAB_ID_FIELD } }
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: "navigateForward",
|
|
1719
|
+
description: "[FLOW] \u524D\u8FDB\u4E00\u9875\u3002\u5DF2\u5728\u5386\u53F2\u672B\u5C3E\u65F6\u8FD4\u56DE { ok:false, reason }\uFF0C\u4E0D\u629B\u9519\u3002",
|
|
1720
|
+
input_schema: { type: "object", properties: { tabId: TAB_ID_FIELD } }
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
name: "resize",
|
|
1724
|
+
description: "[FLOW] \u628A\u89C6\u53E3\u8C03\u6574\u5230\u6307\u5B9A\u5C3A\u5BF8\u3002main-world \u6863\u91CF\u53D6 outerWidth-innerWidth \u53CD\u63A8\u6D4F\u89C8\u5668\u8FB9\u6846\u540E\u6539\u7A97\u53E3\u5916\u6846\uFF0C\u89C6\u53E3\u7CBE\u786E\u4F46**\u7528\u6237\u7684\u7A97\u53E3\u4F1A\u771F\u7684\u53D8\u5927\u5C0F**\uFF1Bcdp \u6863\u7528 Emulation \u8986\u76D6\u8BBE\u5907\u5C3A\u5BF8\uFF0C\u4E0D\u52A8\u771F\u5B9E\u7A97\u53E3\u3002",
|
|
1725
|
+
input_schema: {
|
|
1726
|
+
type: "object",
|
|
1727
|
+
properties: {
|
|
1728
|
+
width: { type: "integer" },
|
|
1729
|
+
height: { type: "integer" },
|
|
1730
|
+
tabId: TAB_ID_FIELD
|
|
1731
|
+
},
|
|
1732
|
+
required: ["width", "height"]
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
name: "drag",
|
|
1737
|
+
description: "[ACT] \u628A\u4E00\u4E2A\u5143\u7D20\u62D6\u5230\u53E6\u4E00\u4E2A\u5143\u7D20\u4E0A\u3002\u540C\u65F6\u53D1 pointer \u5E8F\u5217\u548C HTML5 DragEvent\uFF08\u5171\u7528\u4E00\u4E2A DataTransfer\uFF09\uFF0C\u517C\u5BB9\u539F\u751F\u62D6\u653E\u548C\u81EA\u5B9A\u4E49 pointer \u62D6\u653E\u3002\n\u8FD4\u56DE { consumed: { pointer, html5 } } \u8BF4\u660E\u76EE\u6807\u5B9E\u9645\u6D88\u8D39\u4E86\u54EA\u4E00\u7C7B\u4E8B\u4EF6\u2014\u2014\u90FD\u4E3A false \u8BF4\u660E\u8FD9\u4E2A\u62D6\u653E\u5B9E\u73B0\u4E24\u6761\u8DEF\u90FD\u4E0D\u5403\u3002",
|
|
1738
|
+
input_schema: {
|
|
1739
|
+
type: "object",
|
|
1740
|
+
properties: {
|
|
1741
|
+
fromSelector: { type: "string" },
|
|
1742
|
+
fromUid: { type: "string", description: "takeSnapshot / findElements \u8FD4\u56DE\u7684 uid" },
|
|
1743
|
+
toSelector: { type: "string" },
|
|
1744
|
+
toUid: { type: "string" },
|
|
1745
|
+
tabId: TAB_ID_FIELD
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
name: "drop",
|
|
1751
|
+
description: "[ACT] \u6A21\u62DF\u4ECE\u6D4F\u89C8\u5668\u5916\u90E8\u628A\u6587\u4EF6\u6216\u6570\u636E\u62D6\u653E\u5230\u9875\u9762\u5143\u7D20\u4E0A\u3002\u5E26 files \u65F6\u7B49\u540C\u4E0A\u4F20\uFF08dangerous\uFF09\u3002\n\u793A\u4F8B\uFF1A{ selector:'#dropzone', files:[{ name:'a.csv', mimeType:'text/csv', base64:'...' }] }",
|
|
1752
|
+
input_schema: {
|
|
1753
|
+
type: "object",
|
|
1754
|
+
properties: {
|
|
1755
|
+
selector: { type: "string" },
|
|
1756
|
+
uid: { type: "string" },
|
|
1757
|
+
files: {
|
|
1758
|
+
type: "array",
|
|
1759
|
+
items: {
|
|
1760
|
+
type: "object",
|
|
1761
|
+
properties: {
|
|
1762
|
+
name: { type: "string" },
|
|
1763
|
+
mimeType: { type: "string" },
|
|
1764
|
+
base64: { type: "string" }
|
|
1765
|
+
},
|
|
1766
|
+
required: ["name", "base64"]
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
data: { type: "object", description: "MIME \u2192 \u5B57\u7B26\u4E32\uFF0C\u4F8B\u5982 { 'text/plain':'hi' }" },
|
|
1770
|
+
tabId: TAB_ID_FIELD
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
name: "findElements",
|
|
1776
|
+
description: "[READ] \u6309\u6587\u672C\u6216\u6B63\u5219\u5728\u53EF\u4EA4\u4E92\u5143\u7D20\u91CC\u627E\u76EE\u6807\uFF0C\u8FD4\u56DE uid / role / name / bounds\u3002\u4E0D\u9700\u8981\u5148 createPageIndex\u3002\n\u62FF\u5230 uid \u540E\u7528 clickByUid / fillByUid \u64CD\u4F5C\u6700\u7A33\u3002",
|
|
1777
|
+
input_schema: {
|
|
1778
|
+
type: "object",
|
|
1779
|
+
properties: {
|
|
1780
|
+
text: { type: "string", description: "\u5927\u5C0F\u5199\u4E0D\u654F\u611F\u5B50\u4E32" },
|
|
1781
|
+
regex: { type: "string", description: "\u4E0E text \u4E8C\u9009\u4E00" },
|
|
1782
|
+
limit: { type: "integer", default: 20 },
|
|
1783
|
+
tabId: TAB_ID_FIELD
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
];
|
|
1788
|
+
|
|
1789
|
+
// src/tool-gen.ts
|
|
1790
|
+
var BLOCKED_TOOLS = /* @__PURE__ */ new Set(["askUser", "attachTab", "detachTab"]);
|
|
1791
|
+
var PARITY_TOOLS = [
|
|
1792
|
+
// page state and search
|
|
1793
|
+
"takeSnapshot",
|
|
1794
|
+
"findElements",
|
|
1795
|
+
"getPageInfo",
|
|
1796
|
+
// interaction
|
|
1797
|
+
"clickByUid",
|
|
1776
1798
|
"click",
|
|
1777
1799
|
"fillInput",
|
|
1778
|
-
"
|
|
1800
|
+
"fillForm",
|
|
1779
1801
|
"selectOption",
|
|
1780
|
-
"
|
|
1781
|
-
"
|
|
1802
|
+
"setCheckbox",
|
|
1803
|
+
"hover",
|
|
1804
|
+
"pressKey",
|
|
1805
|
+
"drag",
|
|
1806
|
+
"drop",
|
|
1782
1807
|
"uploadFile",
|
|
1783
|
-
|
|
1808
|
+
// navigation and tabs
|
|
1809
|
+
"navigate",
|
|
1810
|
+
"navigateBack",
|
|
1811
|
+
"listTabs",
|
|
1812
|
+
"openTab",
|
|
1813
|
+
"resize",
|
|
1814
|
+
// observation
|
|
1815
|
+
"screenshot",
|
|
1816
|
+
"waitFor",
|
|
1817
|
+
"runJS",
|
|
1818
|
+
"consoleMessages",
|
|
1819
|
+
"networkRequests"
|
|
1784
1820
|
];
|
|
1821
|
+
var IMAGE_RESULT_TOOLS = /* @__PURE__ */ new Set(["screenshot"]);
|
|
1822
|
+
var JS_STEP_TOOLS = /* @__PURE__ */ new Set(["runJS"]);
|
|
1785
1823
|
function rebuildSchema(src) {
|
|
1786
1824
|
const s = src;
|
|
1787
1825
|
const properties = { ...s.properties ?? {} };
|
|
1788
1826
|
delete properties.tabId;
|
|
1789
|
-
properties.session_id = {
|
|
1827
|
+
properties.session_id = {
|
|
1828
|
+
type: "string",
|
|
1829
|
+
description: "open_session \u8FD4\u56DE\u7684\u4F1A\u8BDD id\uFF08\u51B3\u5B9A\u76EE\u6807 worker \u4E0E tab\uFF09"
|
|
1830
|
+
};
|
|
1790
1831
|
const required = [.../* @__PURE__ */ new Set([...(s.required ?? []).filter((r) => r !== "tabId"), "session_id"])];
|
|
1791
1832
|
return { type: "object", properties, required };
|
|
1792
1833
|
}
|
|
1793
|
-
function generateBrowserTools() {
|
|
1794
|
-
const
|
|
1795
|
-
return TOOL_DEFS.filter((t) =>
|
|
1834
|
+
function generateBrowserTools(mode = "full") {
|
|
1835
|
+
const parity = new Set(PARITY_TOOLS);
|
|
1836
|
+
return TOOL_DEFS.filter((t) => {
|
|
1837
|
+
if (BLOCKED_TOOLS.has(t.name)) return false;
|
|
1838
|
+
return mode === "full" || parity.has(t.name);
|
|
1839
|
+
}).map((t) => ({
|
|
1796
1840
|
name: `browser_${t.name}`,
|
|
1797
1841
|
builtinTool: t.name,
|
|
1798
1842
|
description: t.description,
|
|
1843
|
+
resultKind: IMAGE_RESULT_TOOLS.has(t.name) ? "image" : "json",
|
|
1844
|
+
stepKind: JS_STEP_TOOLS.has(t.name) ? "js" : "tool",
|
|
1799
1845
|
inputSchema: rebuildSchema(t.input_schema)
|
|
1800
1846
|
}));
|
|
1801
1847
|
}
|
|
1848
|
+
function readToolMode(env) {
|
|
1849
|
+
const raw = env.ATWEBPILOT_MCP_TOOLS;
|
|
1850
|
+
if (raw == null || raw === "") return "full";
|
|
1851
|
+
if (raw === "full" || raw === "parity") return raw;
|
|
1852
|
+
process.stderr.write(
|
|
1853
|
+
`[atwebpilot-mcp] ATWEBPILOT_MCP_TOOLS="${raw}" is not recognised; using "full"
|
|
1854
|
+
`
|
|
1855
|
+
);
|
|
1856
|
+
return "full";
|
|
1857
|
+
}
|
|
1802
1858
|
|
|
1803
1859
|
// src/handlers.ts
|
|
1804
|
-
function singleWorkerId(c) {
|
|
1860
|
+
function singleWorkerId(c, pairUrl) {
|
|
1805
1861
|
const workers = c.workers.list();
|
|
1806
|
-
if (workers.length === 0)
|
|
1862
|
+
if (workers.length === 0) {
|
|
1863
|
+
throw new Error(
|
|
1864
|
+
"\u6CA1\u6709\u6D4F\u89C8\u5668\u8FDE\u5165\u3002\u5DF2\u4E3A\u4F60\u6253\u5F00\u914D\u5BF9\u9875" + (pairUrl ? ` ${pairUrl}` : "") + "\uFF0C\u5728\u6D4F\u89C8\u5668\u91CC\u786E\u8BA4\u540E\u91CD\u8BD5\u672C\u6B21\u8C03\u7528\u3002\uFF08\u5982\u679C\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E0D\u662F\u88C5\u4E86\u6269\u5C55\u7684\u90A3\u4E2A\uFF0C\u8BF7\u628A\u8BE5\u5730\u5740\u7C98\u8D34\u8FC7\u53BB\u6253\u5F00\uFF09"
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1807
1867
|
if (workers.length > 1) throw new Error("\u68C0\u6D4B\u5230\u591A\u4E2A\u6D4F\u89C8\u5668\u8FDE\u5165\uFF1Bv1 \u4EC5\u652F\u6301\u5355 worker\uFF0C\u8BF7\u53EA\u4FDD\u7559\u4E00\u4E2A\u8FDE\u63A5");
|
|
1808
1868
|
return workers[0].id;
|
|
1809
1869
|
}
|
|
1810
|
-
function handleListTabs(deps) {
|
|
1811
|
-
const
|
|
1870
|
+
async function handleListTabs(deps) {
|
|
1871
|
+
const { coordinator } = await deps.ensure();
|
|
1872
|
+
const w = coordinator.workers.get(singleWorkerId(coordinator, deps.pairUrl()));
|
|
1812
1873
|
return { tabs: w.available_tabs };
|
|
1813
1874
|
}
|
|
1814
|
-
function handleOpenSession(deps, args) {
|
|
1815
|
-
const
|
|
1875
|
+
async function handleOpenSession(deps, args) {
|
|
1876
|
+
const { coordinator } = await deps.ensure();
|
|
1877
|
+
const worker_id = singleWorkerId(coordinator, deps.pairUrl());
|
|
1816
1878
|
const tab_id = String(args.tab_id);
|
|
1817
1879
|
const requested = Array.isArray(args.capabilities) ? args.capabilities.map(String).filter(isCapability) : [];
|
|
1818
1880
|
const scope = new Set(requested.length ? requested : CAPABILITIES);
|
|
1819
1881
|
const idle_timeout_ms = typeof args.idle_timeout_min === "number" ? args.idle_timeout_min * 6e4 : void 0;
|
|
1820
|
-
const s =
|
|
1882
|
+
const s = coordinator.openSession({ ai_client_fingerprint: "mcp-local", worker_id, tab_id, scope, idle_timeout_ms });
|
|
1821
1883
|
return { session_id: s.id };
|
|
1822
1884
|
}
|
|
1823
|
-
function handleCloseSession(deps, args) {
|
|
1824
|
-
deps.
|
|
1885
|
+
async function handleCloseSession(deps, args) {
|
|
1886
|
+
const { coordinator } = await deps.ensure();
|
|
1887
|
+
coordinator.closeSession(String(args.session_id));
|
|
1825
1888
|
return { ok: true };
|
|
1826
1889
|
}
|
|
1827
|
-
function handleGetQuota(deps, args) {
|
|
1828
|
-
const
|
|
1890
|
+
async function handleGetQuota(deps, args) {
|
|
1891
|
+
const { coordinator } = await deps.ensure();
|
|
1892
|
+
const q = coordinator.quotaFor(String(args.session_id));
|
|
1829
1893
|
if (!q) throw new Error(`session ${String(args.session_id)} not found`);
|
|
1830
1894
|
return q;
|
|
1831
1895
|
}
|
|
1896
|
+
var NON_BUILTIN_CAPABILITY = {
|
|
1897
|
+
listTabs: "tab:open",
|
|
1898
|
+
openTab: "tab:open"
|
|
1899
|
+
};
|
|
1832
1900
|
async function handleBrowserTool(deps, gen, args) {
|
|
1901
|
+
const { coordinator, hub } = await deps.ensure();
|
|
1833
1902
|
const session_id = String(args.session_id);
|
|
1834
|
-
const session =
|
|
1903
|
+
const session = coordinator.sessions.get(session_id);
|
|
1835
1904
|
if (!session) throw new Error(`session ${session_id} not found`);
|
|
1836
1905
|
const { session_id: _omit, ...toolArgs } = args;
|
|
1837
1906
|
const tool = gen.builtinTool;
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1907
|
+
if (gen.stepKind === "js") {
|
|
1908
|
+
const source = String(toolArgs.source ?? "");
|
|
1909
|
+
if (!source) throw new Error("browser_runJS: source required");
|
|
1910
|
+
const v = coordinator.validateCall({ session_id, kind: "runJS", unsafe: true });
|
|
1911
|
+
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
1912
|
+
coordinator.recordCall(session_id, v.dangerous);
|
|
1913
|
+
const jsResult = await hub.exec(session.worker_id, {
|
|
1914
|
+
session_id,
|
|
1915
|
+
tab_id: session.tab_id,
|
|
1916
|
+
step: { kind: "js", source }
|
|
1917
|
+
});
|
|
1918
|
+
if (!jsResult.ok) {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
jsResult.error ? `${jsResult.error.code}: ${jsResult.error.message}` : "EXEC failed"
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
return jsResult.return ?? null;
|
|
1924
|
+
}
|
|
1925
|
+
const override = NON_BUILTIN_CAPABILITY[gen.builtinTool];
|
|
1926
|
+
if (override) {
|
|
1927
|
+
const v = coordinator.validateCall({ session_id, kind: "capability", capability: override });
|
|
1928
|
+
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
1929
|
+
coordinator.recordCall(session_id, v.dangerous);
|
|
1930
|
+
} else {
|
|
1931
|
+
const raw = toolArgs;
|
|
1932
|
+
const v = coordinator.validateCall({
|
|
1933
|
+
session_id,
|
|
1934
|
+
kind: "extension_tool",
|
|
1935
|
+
tool,
|
|
1936
|
+
httpCookied: tool === "httpRequest" ? Boolean(raw.withCredentials) : void 0,
|
|
1937
|
+
dropHasFiles: tool === "drop" ? Array.isArray(raw.files) && raw.files.length > 0 : void 0,
|
|
1938
|
+
recorderArmsBodies: tool === "recorderConfig" ? raw.bodies === true : void 0
|
|
1939
|
+
});
|
|
1940
|
+
if (!v.ok) throw new Error(`${v.error.code}: ${v.error.message}`);
|
|
1941
|
+
coordinator.recordCall(session_id, v.dangerous);
|
|
1942
|
+
}
|
|
1943
|
+
const result = await hub.exec(session.worker_id, { session_id, tab_id: session.tab_id, step: { kind: "tool", tool, args: toolArgs } });
|
|
1843
1944
|
if (!result.ok) throw new Error(result.error ? `${result.error.code}: ${result.error.message}` : "EXEC failed");
|
|
1844
1945
|
return result.return ?? null;
|
|
1845
1946
|
}
|
|
@@ -1886,16 +1987,57 @@ var SKILL_TOOL = {
|
|
|
1886
1987
|
};
|
|
1887
1988
|
|
|
1888
1989
|
// src/mcp-server.ts
|
|
1889
|
-
var
|
|
1990
|
+
var TOOL_MODE = readToolMode(process.env);
|
|
1991
|
+
var BROWSER_TOOLS = generateBrowserTools(TOOL_MODE);
|
|
1890
1992
|
var BROWSER_BY_NAME = new Map(BROWSER_TOOLS.map((t) => [t.name, t]));
|
|
1891
|
-
|
|
1993
|
+
var LEGACY_TOOLS = [
|
|
1994
|
+
"snapshotDOM",
|
|
1995
|
+
"querySelector",
|
|
1996
|
+
"querySelectorAll",
|
|
1997
|
+
"extractText",
|
|
1998
|
+
"extractImages",
|
|
1999
|
+
"getValue",
|
|
2000
|
+
"extractFormState",
|
|
2001
|
+
"hover",
|
|
2002
|
+
"focus",
|
|
2003
|
+
"scroll",
|
|
2004
|
+
"waitFor",
|
|
2005
|
+
"click",
|
|
2006
|
+
"fillInput",
|
|
2007
|
+
"setCheckbox",
|
|
2008
|
+
"selectOption",
|
|
2009
|
+
"httpRequest",
|
|
2010
|
+
"submitForm",
|
|
2011
|
+
"uploadFile",
|
|
2012
|
+
"readStorage"
|
|
2013
|
+
];
|
|
2014
|
+
function workerToolSupport(deps) {
|
|
2015
|
+
const bundle = deps?.peek();
|
|
2016
|
+
if (!bundle) return void 0;
|
|
2017
|
+
const workers = bundle.coordinator.workers.list();
|
|
2018
|
+
if (workers.length === 0) return void 0;
|
|
2019
|
+
const supported = workers[0].supported_tools;
|
|
2020
|
+
return supported ?? new Set(LEGACY_TOOLS);
|
|
2021
|
+
}
|
|
2022
|
+
function buildToolList(deps) {
|
|
2023
|
+
const supported = workerToolSupport(deps);
|
|
2024
|
+
const browser = supported ? BROWSER_TOOLS.filter((t) => supported.has(t.builtinTool)) : BROWSER_TOOLS;
|
|
1892
2025
|
return [
|
|
1893
2026
|
{ name: SKILL_TOOL.name, description: SKILL_TOOL.description, inputSchema: SKILL_TOOL.inputSchema },
|
|
1894
2027
|
...CONTROL_TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
1895
|
-
...
|
|
2028
|
+
...browser.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
|
|
1896
2029
|
];
|
|
1897
2030
|
}
|
|
1898
|
-
var ok = (data) => ({
|
|
2031
|
+
var ok = (data) => ({
|
|
2032
|
+
content: [{ type: "text", text: JSON.stringify(data ?? null) }]
|
|
2033
|
+
});
|
|
2034
|
+
function toolResult(gen, data) {
|
|
2035
|
+
if (gen.resultKind !== "image") return ok(data);
|
|
2036
|
+
const d = data ?? {};
|
|
2037
|
+
if (typeof d.data !== "string") return ok(data);
|
|
2038
|
+
const mimeType = typeof d.media_type === "string" ? d.media_type : "image/png";
|
|
2039
|
+
return { content: [{ type: "image", data: d.data, mimeType }] };
|
|
2040
|
+
}
|
|
1899
2041
|
var fail2 = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
1900
2042
|
async function dispatchCall(deps, name, args) {
|
|
1901
2043
|
try {
|
|
@@ -1903,12 +2045,12 @@ async function dispatchCall(deps, name, args) {
|
|
|
1903
2045
|
const bundle = readSkillBundle();
|
|
1904
2046
|
return { content: [{ type: "text", text: bundle.content }] };
|
|
1905
2047
|
}
|
|
1906
|
-
if (name === "list_tabs") return ok(handleListTabs(deps));
|
|
1907
|
-
if (name === "open_session") return ok(handleOpenSession(deps, args));
|
|
1908
|
-
if (name === "close_session") return ok(handleCloseSession(deps, args));
|
|
1909
|
-
if (name === "get_quota") return ok(handleGetQuota(deps, args));
|
|
2048
|
+
if (name === "list_tabs") return ok(await handleListTabs(deps));
|
|
2049
|
+
if (name === "open_session") return ok(await handleOpenSession(deps, args));
|
|
2050
|
+
if (name === "close_session") return ok(await handleCloseSession(deps, args));
|
|
2051
|
+
if (name === "get_quota") return ok(await handleGetQuota(deps, args));
|
|
1910
2052
|
const gen = BROWSER_BY_NAME.get(name);
|
|
1911
|
-
if (gen) return
|
|
2053
|
+
if (gen) return toolResult(gen, await handleBrowserTool(deps, gen, args));
|
|
1912
2054
|
return fail2(`unknown tool: ${name}`);
|
|
1913
2055
|
} catch (e) {
|
|
1914
2056
|
return fail2(e instanceof Error ? e.message : String(e));
|
|
@@ -1916,7 +2058,7 @@ async function dispatchCall(deps, name, args) {
|
|
|
1916
2058
|
}
|
|
1917
2059
|
function createMcpServer(deps) {
|
|
1918
2060
|
const server = new Server({ name: "atwebpilot-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
1919
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList() }));
|
|
2061
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: buildToolList(deps) }));
|
|
1920
2062
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1921
2063
|
const args = req.params.arguments ?? {};
|
|
1922
2064
|
return dispatchCall(deps, req.params.name, args);
|
|
@@ -1924,12 +2066,325 @@ function createMcpServer(deps) {
|
|
|
1924
2066
|
return server;
|
|
1925
2067
|
}
|
|
1926
2068
|
|
|
2069
|
+
// src/loopback-ws-hub.ts
|
|
2070
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
2071
|
+
import { createServer } from "http";
|
|
2072
|
+
|
|
2073
|
+
// ../shared/src/pairing/types.ts
|
|
2074
|
+
var GRACEFUL_CLOSE_CODE = 4e3;
|
|
2075
|
+
var GRACEFUL_CLOSE_REASON = "server-shutting-down";
|
|
2076
|
+
|
|
2077
|
+
// src/pair-page.ts
|
|
2078
|
+
function renderPairPage(payload) {
|
|
2079
|
+
const json = JSON.stringify(payload).replace(/</g, "\\u003c");
|
|
2080
|
+
return `<!doctype html>
|
|
2081
|
+
<meta charset="utf-8">
|
|
2082
|
+
<title>AtWebPilot \u914D\u5BF9</title>
|
|
2083
|
+
<body style="font:14px/1.6 system-ui,sans-serif;padding:2rem;max-width:32rem;margin:auto;color:#222">
|
|
2084
|
+
<h1 style="font-size:1.1rem;margin:0 0 1rem">AtWebPilot \u914D\u5BF9</h1>
|
|
2085
|
+
<p id="atwebpilot-status">\u6B63\u5728\u8054\u7CFB\u6269\u5C55\u2026</p>
|
|
2086
|
+
<p style="color:#666;font-size:13px">\u4F1A\u8BDD\uFF1A${escapeHtml(payload.label)} \xB7 pid ${payload.pid} \xB7 \u7AEF\u53E3 ${payload.port}</p>
|
|
2087
|
+
<p style="color:#666;font-size:13px">\u6CA1\u6709\u53CD\u5E94\uFF1F\u8BF7\u786E\u8BA4\u8FD9\u4E2A\u6D4F\u89C8\u5668\u5DF2\u5B89\u88C5\u5E76\u542F\u7528 AtWebPilot \u6269\u5C55\uFF0C\u7136\u540E\u5237\u65B0\u672C\u9875\u3002</p>
|
|
2088
|
+
<script>
|
|
2089
|
+
(function () {
|
|
2090
|
+
var payload = ${json};
|
|
2091
|
+
window.postMessage({ source: "atwebpilot-pair", payload: payload }, "*");
|
|
2092
|
+
window.addEventListener("message", function (e) {
|
|
2093
|
+
if (!e.data || e.data.source !== "atwebpilot-pair-result") return;
|
|
2094
|
+
var el = document.getElementById("atwebpilot-status");
|
|
2095
|
+
if (e.data.ok) {
|
|
2096
|
+
el.textContent = e.data.trusted ? "\u5DF2\u4FE1\u4EFB\uFF0C\u8FDE\u63A5\u4E2D\u2026" : "\u5DF2\u8FDE\u63A5";
|
|
2097
|
+
setTimeout(function () { window.close(); }, 1200);
|
|
2098
|
+
} else {
|
|
2099
|
+
el.textContent = "\u5DF2\u62D2\u7EDD\u3002\u53EF\u4EE5\u5173\u95ED\u672C\u9875\u3002";
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
})();
|
|
2103
|
+
</script>
|
|
2104
|
+
`;
|
|
2105
|
+
}
|
|
2106
|
+
function escapeHtml(s) {
|
|
2107
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/loopback-ws-hub.ts
|
|
2111
|
+
var HEARTBEAT_INTERVAL_MS = 2e4;
|
|
2112
|
+
var DEFAULT_KEEPALIVE_INTERVAL_MS = 15e3;
|
|
2113
|
+
var LoopbackWSHub = class {
|
|
2114
|
+
constructor(opts) {
|
|
2115
|
+
this.opts = opts;
|
|
2116
|
+
this.execTimeoutMs = opts.execTimeoutMs ?? 3e4;
|
|
2117
|
+
this.keepaliveIntervalMs = opts.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
2118
|
+
this.http = createServer((req, res) => this.onHttpRequest(req, res));
|
|
2119
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
2120
|
+
this.http.once("listening", () => resolve(this.http.address().port));
|
|
2121
|
+
this.http.once("error", (err) => {
|
|
2122
|
+
reject(
|
|
2123
|
+
err.code === "EADDRINUSE" ? new Error(
|
|
2124
|
+
`port ${opts.port} is already in use \u2014 another atwebpilot-mcp may be running; set ATWEBPILOT_WS_PORT to choose another port`
|
|
2125
|
+
) : err
|
|
2126
|
+
);
|
|
2127
|
+
});
|
|
2128
|
+
});
|
|
2129
|
+
this.wss = new WebSocketServer({
|
|
2130
|
+
server: this.http,
|
|
2131
|
+
path: "/worker",
|
|
2132
|
+
handleProtocols: (protocols) => [...protocols][0] ?? false
|
|
2133
|
+
});
|
|
2134
|
+
this.wss.on("connection", (socket, req) => this.onConnection(socket, req));
|
|
2135
|
+
this.http.on("error", () => void 0);
|
|
2136
|
+
this.wss.on("error", () => void 0);
|
|
2137
|
+
this.http.listen(opts.port);
|
|
2138
|
+
}
|
|
2139
|
+
opts;
|
|
2140
|
+
http;
|
|
2141
|
+
wss;
|
|
2142
|
+
/** Settled by the constructor so ready() cannot race the listen outcome. */
|
|
2143
|
+
readyPromise;
|
|
2144
|
+
byWorker = /* @__PURE__ */ new Map();
|
|
2145
|
+
workerOf = /* @__PURE__ */ new Map();
|
|
2146
|
+
pending = /* @__PURE__ */ new Map();
|
|
2147
|
+
msgHandlers = [];
|
|
2148
|
+
disconnectHandlers = [];
|
|
2149
|
+
execTimeoutMs;
|
|
2150
|
+
keepaliveIntervalMs;
|
|
2151
|
+
keepaliveTimers = /* @__PURE__ */ new Map();
|
|
2152
|
+
ready() {
|
|
2153
|
+
return this.readyPromise;
|
|
2154
|
+
}
|
|
2155
|
+
/** Serves the pairing page; everything else is a 404. */
|
|
2156
|
+
onHttpRequest(req, res) {
|
|
2157
|
+
const path = (req.url ?? "").split("?")[0];
|
|
2158
|
+
if (path !== "/pair") {
|
|
2159
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
2160
|
+
res.end("not found");
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const payload = this.opts.pairPayload?.();
|
|
2164
|
+
if (!payload) {
|
|
2165
|
+
res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
|
|
2166
|
+
res.end("pairing unavailable");
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
res.writeHead(200, {
|
|
2170
|
+
"content-type": "text/html; charset=utf-8",
|
|
2171
|
+
"cache-control": "no-store"
|
|
2172
|
+
});
|
|
2173
|
+
res.end(renderPairPage(payload));
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Tells connected extensions this shutdown is deliberate, so they drop the
|
|
2177
|
+
* endpoint instead of retrying it for the rest of the browser session.
|
|
2178
|
+
*/
|
|
2179
|
+
async shutdown() {
|
|
2180
|
+
for (const socket of this.wss.clients) {
|
|
2181
|
+
try {
|
|
2182
|
+
socket.close(GRACEFUL_CLOSE_CODE, GRACEFUL_CLOSE_REASON);
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2187
|
+
await this.close();
|
|
2188
|
+
}
|
|
2189
|
+
close() {
|
|
2190
|
+
for (const p of this.pending.values()) {
|
|
2191
|
+
clearTimeout(p.timer);
|
|
2192
|
+
p.reject(new Error("hub closing"));
|
|
2193
|
+
}
|
|
2194
|
+
this.pending.clear();
|
|
2195
|
+
for (const timer of this.keepaliveTimers.values()) clearInterval(timer);
|
|
2196
|
+
this.keepaliveTimers.clear();
|
|
2197
|
+
for (const socket of this.wss.clients) {
|
|
2198
|
+
socket.terminate();
|
|
2199
|
+
}
|
|
2200
|
+
return new Promise(
|
|
2201
|
+
(resolve) => this.wss.close(() => this.http.close(() => resolve()))
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
tokenOk(req) {
|
|
2205
|
+
if (!this.opts.token) return true;
|
|
2206
|
+
const offered = String(req.headers["sec-websocket-protocol"] ?? "").split(",").map((s) => s.trim());
|
|
2207
|
+
return offered.includes(`bearer.${this.opts.token}`);
|
|
2208
|
+
}
|
|
2209
|
+
onConnection(socket, req) {
|
|
2210
|
+
if (!this.tokenOk(req)) {
|
|
2211
|
+
socket.close(4401, "bad token");
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
socket.on("message", (raw) => this.onMessageRaw(socket, raw.toString()));
|
|
2215
|
+
socket.on("close", () => this.onSocketClose(socket));
|
|
2216
|
+
socket.on("error", () => {
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
onMessageRaw(socket, raw) {
|
|
2220
|
+
let parsed;
|
|
2221
|
+
try {
|
|
2222
|
+
parsed = JSON.parse(raw);
|
|
2223
|
+
} catch {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const r = ClientToServerSchema.safeParse(parsed);
|
|
2227
|
+
if (!r.success) return;
|
|
2228
|
+
const msg = r.data;
|
|
2229
|
+
if (msg.type === "HELLO") {
|
|
2230
|
+
const existingSocket = this.byWorker.get(msg.worker_id);
|
|
2231
|
+
if (existingSocket && existingSocket !== socket) {
|
|
2232
|
+
this.workerOf.delete(existingSocket);
|
|
2233
|
+
for (const [req_id, p] of [...this.pending]) {
|
|
2234
|
+
if (p.worker_id === msg.worker_id) {
|
|
2235
|
+
clearTimeout(p.timer);
|
|
2236
|
+
this.pending.delete(req_id);
|
|
2237
|
+
p.reject(new Error(`worker ${msg.worker_id} reconnected, exec cancelled`));
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
existingSocket.terminate();
|
|
2241
|
+
}
|
|
2242
|
+
this.byWorker.set(msg.worker_id, socket);
|
|
2243
|
+
this.workerOf.set(socket, msg.worker_id);
|
|
2244
|
+
this.rawSend(socket, {
|
|
2245
|
+
type: "WELCOME",
|
|
2246
|
+
nonce: this.opts.idGen.next("nonce"),
|
|
2247
|
+
ts: this.opts.clock.now(),
|
|
2248
|
+
protocol_version: PROTOCOL_VERSION,
|
|
2249
|
+
server_time: this.opts.clock.now(),
|
|
2250
|
+
heartbeat_interval_ms: HEARTBEAT_INTERVAL_MS
|
|
2251
|
+
});
|
|
2252
|
+
this.startKeepalive(socket);
|
|
2253
|
+
for (const h of this.msgHandlers) h(msg.worker_id, msg);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (msg.type === "RESULT") {
|
|
2257
|
+
const p = this.pending.get(msg.req_id);
|
|
2258
|
+
if (p) {
|
|
2259
|
+
clearTimeout(p.timer);
|
|
2260
|
+
this.pending.delete(msg.req_id);
|
|
2261
|
+
p.resolve(msg);
|
|
2262
|
+
}
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (msg.type === "PING") {
|
|
2266
|
+
this.rawSend(socket, {
|
|
2267
|
+
type: "PONG",
|
|
2268
|
+
nonce: this.opts.idGen.next("nonce"),
|
|
2269
|
+
ts: this.opts.clock.now(),
|
|
2270
|
+
protocol_version: PROTOCOL_VERSION,
|
|
2271
|
+
echo_nonce: msg.nonce
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
const wid = this.workerOf.get(socket);
|
|
2275
|
+
if (wid) {
|
|
2276
|
+
for (const h of this.msgHandlers) h(wid, msg);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
onSocketClose(socket) {
|
|
2280
|
+
this.stopKeepalive(socket);
|
|
2281
|
+
const wid = this.workerOf.get(socket);
|
|
2282
|
+
if (!wid) return;
|
|
2283
|
+
this.workerOf.delete(socket);
|
|
2284
|
+
this.byWorker.delete(wid);
|
|
2285
|
+
for (const [req_id, p] of [...this.pending]) {
|
|
2286
|
+
if (p.worker_id === wid) {
|
|
2287
|
+
clearTimeout(p.timer);
|
|
2288
|
+
this.pending.delete(req_id);
|
|
2289
|
+
p.reject(new Error(`worker ${wid} disconnected`));
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
for (const h of this.disconnectHandlers) h(wid);
|
|
2293
|
+
}
|
|
2294
|
+
startKeepalive(socket) {
|
|
2295
|
+
if (this.keepaliveIntervalMs <= 0) return;
|
|
2296
|
+
if (this.keepaliveTimers.has(socket)) return;
|
|
2297
|
+
const timer = setInterval(() => {
|
|
2298
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
2299
|
+
this.rawSend(socket, {
|
|
2300
|
+
type: "PONG",
|
|
2301
|
+
nonce: this.opts.idGen.next("nonce"),
|
|
2302
|
+
ts: this.opts.clock.now(),
|
|
2303
|
+
protocol_version: PROTOCOL_VERSION,
|
|
2304
|
+
echo_nonce: "server-keepalive"
|
|
2305
|
+
});
|
|
2306
|
+
}, this.keepaliveIntervalMs);
|
|
2307
|
+
if (typeof timer.unref === "function") {
|
|
2308
|
+
timer.unref();
|
|
2309
|
+
}
|
|
2310
|
+
this.keepaliveTimers.set(socket, timer);
|
|
2311
|
+
}
|
|
2312
|
+
stopKeepalive(socket) {
|
|
2313
|
+
const timer = this.keepaliveTimers.get(socket);
|
|
2314
|
+
if (!timer) return;
|
|
2315
|
+
clearInterval(timer);
|
|
2316
|
+
this.keepaliveTimers.delete(socket);
|
|
2317
|
+
}
|
|
2318
|
+
rawSend(socket, msg) {
|
|
2319
|
+
const r = ServerToClientSchema.safeParse(msg);
|
|
2320
|
+
if (!r.success) {
|
|
2321
|
+
console.error("[hub] outgoing failed schema", r.error);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
2325
|
+
socket.send(JSON.stringify(msg));
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
exec(worker_id, params) {
|
|
2329
|
+
const socket = this.byWorker.get(worker_id);
|
|
2330
|
+
if (!socket)
|
|
2331
|
+
return Promise.reject(new Error(`worker ${worker_id} not connected`));
|
|
2332
|
+
const req_id = this.opts.idGen.next("req");
|
|
2333
|
+
return new Promise((resolve, reject) => {
|
|
2334
|
+
const timer = setTimeout(() => {
|
|
2335
|
+
this.pending.delete(req_id);
|
|
2336
|
+
reject(
|
|
2337
|
+
new Error(`EXEC ${req_id} timeout after ${this.execTimeoutMs}ms`)
|
|
2338
|
+
);
|
|
2339
|
+
}, this.execTimeoutMs);
|
|
2340
|
+
this.pending.set(req_id, { resolve, reject, timer, worker_id });
|
|
2341
|
+
this.rawSend(socket, {
|
|
2342
|
+
type: "EXEC",
|
|
2343
|
+
nonce: this.opts.idGen.next("nonce"),
|
|
2344
|
+
ts: this.opts.clock.now(),
|
|
2345
|
+
protocol_version: PROTOCOL_VERSION,
|
|
2346
|
+
req_id,
|
|
2347
|
+
session_id: params.session_id,
|
|
2348
|
+
tab_id: params.tab_id,
|
|
2349
|
+
step: { kind: "tool", tool: params.step.tool, args: params.step.args }
|
|
2350
|
+
});
|
|
2351
|
+
if (this.byWorker.get(worker_id) !== socket) {
|
|
2352
|
+
clearTimeout(timer);
|
|
2353
|
+
this.pending.delete(req_id);
|
|
2354
|
+
reject(new Error(`worker ${worker_id} disconnected`));
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
async send(worker_id, msg) {
|
|
2359
|
+
const socket = this.byWorker.get(worker_id);
|
|
2360
|
+
if (!socket) throw new Error(`worker ${worker_id} not connected`);
|
|
2361
|
+
this.rawSend(socket, msg);
|
|
2362
|
+
}
|
|
2363
|
+
onMessage(handler) {
|
|
2364
|
+
this.msgHandlers.push(handler);
|
|
2365
|
+
}
|
|
2366
|
+
onDisconnect(handler) {
|
|
2367
|
+
this.disconnectHandlers.push(handler);
|
|
2368
|
+
}
|
|
2369
|
+
connectedWorkers() {
|
|
2370
|
+
return [...this.byWorker.keys()];
|
|
2371
|
+
}
|
|
2372
|
+
// Intentionally fire-and-forget: actual cleanup (byWorker/workerOf removal and
|
|
2373
|
+
// pending-exec rejection) happens asynchronously via the socket's close event.
|
|
2374
|
+
// v1 callers do not await this method.
|
|
2375
|
+
async disconnect(worker_id, _reason) {
|
|
2376
|
+
const socket = this.byWorker.get(worker_id);
|
|
2377
|
+
if (socket) socket.close();
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
|
|
1927
2381
|
// src/wire.ts
|
|
1928
2382
|
function helloToWorker(h, now) {
|
|
1929
2383
|
return {
|
|
1930
2384
|
id: h.worker_id,
|
|
1931
2385
|
fingerprint: h.fingerprint,
|
|
1932
2386
|
capabilities: new Set(h.capabilities.filter(isCapability)),
|
|
2387
|
+
supported_tools: h.supported_tools ? new Set(h.supported_tools) : void 0,
|
|
1933
2388
|
attended: h.attended,
|
|
1934
2389
|
labels: new Set(h.labels),
|
|
1935
2390
|
available_tabs: h.available_tabs,
|
|
@@ -1949,6 +2404,11 @@ function installWire(hub, coordinator, clock) {
|
|
|
1949
2404
|
case "PING":
|
|
1950
2405
|
coordinator.heartbeatWorker(worker_id);
|
|
1951
2406
|
break;
|
|
2407
|
+
case "TABS_UPDATE": {
|
|
2408
|
+
const w = coordinator.workers.get(worker_id);
|
|
2409
|
+
if (w) w.available_tabs = msg.tabs.map((t) => ({ ...t }));
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
1952
2412
|
default:
|
|
1953
2413
|
break;
|
|
1954
2414
|
}
|
|
@@ -1956,19 +2416,150 @@ function installWire(hub, coordinator, clock) {
|
|
|
1956
2416
|
hub.onDisconnect((worker_id) => coordinator.unregisterWorker(worker_id));
|
|
1957
2417
|
}
|
|
1958
2418
|
|
|
2419
|
+
// src/identity.ts
|
|
2420
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2421
|
+
import { homedir } from "os";
|
|
2422
|
+
import { join as join2 } from "path";
|
|
2423
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
2424
|
+
var FILE = "identity.json";
|
|
2425
|
+
function defaultIdentityDir() {
|
|
2426
|
+
return join2(homedir(), ".atwebpilot");
|
|
2427
|
+
}
|
|
2428
|
+
function read(dir) {
|
|
2429
|
+
try {
|
|
2430
|
+
const raw = readFileSync2(join2(dir, FILE), "utf-8");
|
|
2431
|
+
const parsed = JSON.parse(raw);
|
|
2432
|
+
if (typeof parsed.installId !== "string" || typeof parsed.secret !== "string") return null;
|
|
2433
|
+
return parsed;
|
|
2434
|
+
} catch {
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
function write(dir, data) {
|
|
2439
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2440
|
+
writeFileSync(join2(dir, FILE), `${JSON.stringify(data, null, 2)}
|
|
2441
|
+
`, { mode: 384 });
|
|
2442
|
+
}
|
|
2443
|
+
function loadOrCreateIdentity(dir = defaultIdentityDir()) {
|
|
2444
|
+
const existing = read(dir);
|
|
2445
|
+
if (existing) return { installId: existing.installId, secret: existing.secret };
|
|
2446
|
+
const created = {
|
|
2447
|
+
installId: `inst_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
|
|
2448
|
+
secret: randomBytes(24).toString("base64url")
|
|
2449
|
+
};
|
|
2450
|
+
write(dir, created);
|
|
2451
|
+
return { installId: created.installId, secret: created.secret };
|
|
2452
|
+
}
|
|
2453
|
+
function loadLastPort(dir = defaultIdentityDir()) {
|
|
2454
|
+
const existing = read(dir);
|
|
2455
|
+
return typeof existing?.lastPort === "number" ? existing.lastPort : void 0;
|
|
2456
|
+
}
|
|
2457
|
+
function saveLastPort(port, dir = defaultIdentityDir()) {
|
|
2458
|
+
const existing = read(dir);
|
|
2459
|
+
const base = existing ?? { ...loadOrCreateIdentity(dir) };
|
|
2460
|
+
write(dir, { ...base, lastPort: port });
|
|
2461
|
+
}
|
|
2462
|
+
function processInfo(cwd = process.cwd()) {
|
|
2463
|
+
const home = homedir();
|
|
2464
|
+
const label = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
2465
|
+
return {
|
|
2466
|
+
sessionId: `sess_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
|
|
2467
|
+
label,
|
|
2468
|
+
pid: process.pid
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/ensure-hub.ts
|
|
2473
|
+
function createHubEnsurer(d) {
|
|
2474
|
+
let bundle = null;
|
|
2475
|
+
let inflight = null;
|
|
2476
|
+
let pairUrlValue = null;
|
|
2477
|
+
let opened = false;
|
|
2478
|
+
const info = d.processInfo ?? processInfo();
|
|
2479
|
+
async function bind() {
|
|
2480
|
+
const identity = loadOrCreateIdentity(d.identityDir);
|
|
2481
|
+
const preferred = d.explicitPort ?? loadLastPort(d.identityDir) ?? 0;
|
|
2482
|
+
let hub;
|
|
2483
|
+
let port;
|
|
2484
|
+
const pairPayload = () => ({
|
|
2485
|
+
v: 1,
|
|
2486
|
+
installId: identity.installId,
|
|
2487
|
+
secret: identity.secret,
|
|
2488
|
+
sessionId: info.sessionId,
|
|
2489
|
+
label: info.label,
|
|
2490
|
+
pid: info.pid,
|
|
2491
|
+
port
|
|
2492
|
+
});
|
|
2493
|
+
try {
|
|
2494
|
+
hub = new LoopbackWSHub({ ...common(), port: preferred, pairPayload });
|
|
2495
|
+
port = await hub.ready();
|
|
2496
|
+
} catch (e) {
|
|
2497
|
+
if (d.explicitPort != null) throw e;
|
|
2498
|
+
hub = new LoopbackWSHub({ ...common(), port: 0, pairPayload });
|
|
2499
|
+
port = await hub.ready();
|
|
2500
|
+
}
|
|
2501
|
+
saveLastPort(port, d.identityDir);
|
|
2502
|
+
const coordinator = new Coordinator({ hub, clock: d.clock, idGen: d.idGen });
|
|
2503
|
+
installWire(hub, coordinator, d.clock);
|
|
2504
|
+
pairUrlValue = `http://127.0.0.1:${port}/pair`;
|
|
2505
|
+
return { coordinator, hub, port };
|
|
2506
|
+
function common() {
|
|
2507
|
+
return { token: d.token, clock: d.clock, idGen: d.idGen };
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
async function ensure() {
|
|
2511
|
+
if (bundle) return bundle;
|
|
2512
|
+
inflight ??= bind().then((b2) => {
|
|
2513
|
+
bundle = b2;
|
|
2514
|
+
return b2;
|
|
2515
|
+
});
|
|
2516
|
+
const b = await inflight;
|
|
2517
|
+
if (!opened) {
|
|
2518
|
+
opened = true;
|
|
2519
|
+
if (pairUrlValue) d.openUrl?.(pairUrlValue);
|
|
2520
|
+
}
|
|
2521
|
+
return b;
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
ensure,
|
|
2525
|
+
peek: () => bundle,
|
|
2526
|
+
pairUrl: () => pairUrlValue,
|
|
2527
|
+
bound: () => bundle != null
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
function defaultOpenUrl(url) {
|
|
2531
|
+
void import("child_process").then(({ spawn }) => {
|
|
2532
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2533
|
+
try {
|
|
2534
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
2535
|
+
} catch {
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
|
|
1959
2540
|
// src/index.ts
|
|
1960
2541
|
async function main() {
|
|
1961
|
-
const
|
|
2542
|
+
const explicitPort = process.env.ATWEBPILOT_WS_PORT ? Number(process.env.ATWEBPILOT_WS_PORT) : void 0;
|
|
1962
2543
|
const token = process.env.ATWEBPILOT_WS_TOKEN || void 0;
|
|
1963
2544
|
const clock = new DefaultClock();
|
|
1964
2545
|
const idGen = new DefaultIdGen();
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2546
|
+
const deps = createHubEnsurer({
|
|
2547
|
+
clock,
|
|
2548
|
+
idGen,
|
|
2549
|
+
explicitPort,
|
|
2550
|
+
token,
|
|
2551
|
+
openUrl: defaultOpenUrl
|
|
2552
|
+
});
|
|
2553
|
+
const server = createMcpServer(deps);
|
|
1970
2554
|
await server.connect(new StdioServerTransport());
|
|
1971
|
-
console.error(
|
|
2555
|
+
console.error("[atwebpilot-mcp] stdio MCP connected; ws port binds on first browser use");
|
|
2556
|
+
const shutdown = () => {
|
|
2557
|
+
const bundle = deps.peek();
|
|
2558
|
+
void (bundle?.hub.shutdown?.() ?? Promise.resolve()).finally(() => process.exit(0));
|
|
2559
|
+
};
|
|
2560
|
+
process.on("SIGTERM", shutdown);
|
|
2561
|
+
process.on("SIGINT", shutdown);
|
|
2562
|
+
process.stdin.on("close", shutdown);
|
|
1972
2563
|
}
|
|
1973
2564
|
main().catch((e) => {
|
|
1974
2565
|
console.error("[atwebpilot-mcp] fatal", e);
|