@opentiny/webmcp-cli 0.1.0 → 0.1.1
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 +2 -0
- package/dist/bin.cjs +822 -150
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +794 -124
- package/dist/bin.js.map +1 -1
- package/dist/inject-bundle.js +108 -48
- package/dist/webmcp-tools/juejin.cn.js +86 -30
- package/package.json +17 -3
- package/scripts/ensure-pty-helper.mjs +24 -0
package/dist/bin.js
CHANGED
|
@@ -526,9 +526,17 @@ var init_package = __esm({
|
|
|
526
526
|
"package.json"() {
|
|
527
527
|
package_default = {
|
|
528
528
|
name: "@opentiny/webmcp-cli",
|
|
529
|
-
version: "0.1.
|
|
529
|
+
version: "0.1.1",
|
|
530
530
|
type: "module",
|
|
531
531
|
description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
|
|
532
|
+
repository: {
|
|
533
|
+
type: "git",
|
|
534
|
+
url: "git+https://github.com/opentiny/webmcp-sdk.git",
|
|
535
|
+
directory: "packages/webmcp-cli"
|
|
536
|
+
},
|
|
537
|
+
bugs: {
|
|
538
|
+
url: "https://github.com/opentiny/webmcp-sdk/issues"
|
|
539
|
+
},
|
|
532
540
|
main: "dist/index.js",
|
|
533
541
|
types: "dist/index.d.ts",
|
|
534
542
|
bin: {
|
|
@@ -536,7 +544,8 @@ var init_package = __esm({
|
|
|
536
544
|
},
|
|
537
545
|
files: [
|
|
538
546
|
"dist",
|
|
539
|
-
"README.md"
|
|
547
|
+
"README.md",
|
|
548
|
+
"scripts/ensure-pty-helper.mjs"
|
|
540
549
|
],
|
|
541
550
|
scripts: {
|
|
542
551
|
dev: "tsup --watch",
|
|
@@ -545,6 +554,7 @@ var init_package = __esm({
|
|
|
545
554
|
prepublishOnly: "pnpm run build",
|
|
546
555
|
"link:global": "npm install -g .",
|
|
547
556
|
start: "node dist/bin.js",
|
|
557
|
+
postinstall: "node scripts/ensure-pty-helper.mjs",
|
|
548
558
|
test: "vitest run",
|
|
549
559
|
"test:watch": "vitest",
|
|
550
560
|
"test:browser": "vitest run --config vitest.e2e.config.ts"
|
|
@@ -557,7 +567,7 @@ var init_package = __esm({
|
|
|
557
567
|
license: "MIT",
|
|
558
568
|
author: "OpenTiny Team",
|
|
559
569
|
engines: {
|
|
560
|
-
node: ">=
|
|
570
|
+
node: ">=18.19.0"
|
|
561
571
|
},
|
|
562
572
|
publishConfig: {
|
|
563
573
|
access: "public"
|
|
@@ -569,6 +579,7 @@ var init_package = __esm({
|
|
|
569
579
|
"@page-agent/page-controller": "catalog:",
|
|
570
580
|
clipboardy: "^5.3.1",
|
|
571
581
|
commander: "^11.1.0",
|
|
582
|
+
execa: "^9.6.1",
|
|
572
583
|
json5: "^2.2.3",
|
|
573
584
|
marked: "^15.0.7",
|
|
574
585
|
picocolors: "^1.0.0",
|
|
@@ -576,6 +587,9 @@ var init_package = __esm({
|
|
|
576
587
|
ws: "^8.18.0",
|
|
577
588
|
zod: "^3.23.0"
|
|
578
589
|
},
|
|
590
|
+
optionalDependencies: {
|
|
591
|
+
"node-pty": "^1.1.0"
|
|
592
|
+
},
|
|
579
593
|
devDependencies: {
|
|
580
594
|
"@types/node": "^20.0.0",
|
|
581
595
|
"@types/ws": "^8.5.10",
|
|
@@ -797,12 +811,15 @@ var init_cdp_adapter = __esm({
|
|
|
797
811
|
});
|
|
798
812
|
|
|
799
813
|
// src/bridge/protocol.ts
|
|
800
|
-
|
|
814
|
+
function isTerminalMethod(method) {
|
|
815
|
+
return TERMINAL_METHOD_SET.has(method);
|
|
816
|
+
}
|
|
817
|
+
var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS, TERMINAL_EVENTS, BASH_EVENTS, TERMINAL_METHOD_SET;
|
|
801
818
|
var init_protocol = __esm({
|
|
802
819
|
"src/bridge/protocol.ts"() {
|
|
803
820
|
"use strict";
|
|
804
821
|
DEFAULT_WS_PORT = 18999;
|
|
805
|
-
PROTOCOL_VERSION = "1.
|
|
822
|
+
PROTOCOL_VERSION = "1.2.0";
|
|
806
823
|
WS_CLOSE_CODE = {
|
|
807
824
|
/** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
|
|
808
825
|
REPLACED: 4e3,
|
|
@@ -836,8 +853,25 @@ var init_protocol = __esm({
|
|
|
836
853
|
SKILLS_GET: "skills/get",
|
|
837
854
|
SKILLS_READ_RESOURCE: "skills/read_resource",
|
|
838
855
|
SYSTEM_BASH: "system/bash",
|
|
839
|
-
SYSTEM_PICK_DIRECTORY: "system/pick_directory"
|
|
856
|
+
SYSTEM_PICK_DIRECTORY: "system/pick_directory",
|
|
857
|
+
TERMINAL_OPEN: "terminal/open",
|
|
858
|
+
TERMINAL_INPUT: "terminal/input",
|
|
859
|
+
TERMINAL_RESIZE: "terminal/resize",
|
|
860
|
+
TERMINAL_CLOSE: "terminal/close"
|
|
861
|
+
};
|
|
862
|
+
TERMINAL_EVENTS = {
|
|
863
|
+
OUTPUT: "notification/terminal_output",
|
|
864
|
+
EXIT: "notification/terminal_exit"
|
|
840
865
|
};
|
|
866
|
+
BASH_EVENTS = {
|
|
867
|
+
OUTPUT: "notification/bash_output"
|
|
868
|
+
};
|
|
869
|
+
TERMINAL_METHOD_SET = /* @__PURE__ */ new Set([
|
|
870
|
+
BRIDGE_METHODS.TERMINAL_OPEN,
|
|
871
|
+
BRIDGE_METHODS.TERMINAL_INPUT,
|
|
872
|
+
BRIDGE_METHODS.TERMINAL_RESIZE,
|
|
873
|
+
BRIDGE_METHODS.TERMINAL_CLOSE
|
|
874
|
+
]);
|
|
841
875
|
}
|
|
842
876
|
});
|
|
843
877
|
|
|
@@ -896,9 +930,11 @@ var init_workspace_manager = __esm({
|
|
|
896
930
|
});
|
|
897
931
|
|
|
898
932
|
// src/bridge/bash-executor.ts
|
|
933
|
+
import { spawnSync } from "child_process";
|
|
899
934
|
import fs3 from "fs";
|
|
900
935
|
import path3 from "path";
|
|
901
|
-
import {
|
|
936
|
+
import { StringDecoder } from "string_decoder";
|
|
937
|
+
import { execa } from "execa";
|
|
902
938
|
function findWindowsBash() {
|
|
903
939
|
const pathDirs = (process.env.PATH || "").split(path3.delimiter);
|
|
904
940
|
for (const dir of pathDirs) {
|
|
@@ -949,25 +985,136 @@ function resolveShell(forcePlatform) {
|
|
|
949
985
|
if (!forcePlatform) cachedShell = fallback;
|
|
950
986
|
return fallback;
|
|
951
987
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
988
|
+
function stripAnsi(text) {
|
|
989
|
+
return text.replace(ANSI_PATTERN, "");
|
|
990
|
+
}
|
|
991
|
+
function createAnsiStripper() {
|
|
992
|
+
let pending = "";
|
|
993
|
+
const take = (final) => {
|
|
994
|
+
let out = "";
|
|
995
|
+
let index = 0;
|
|
996
|
+
while (index < pending.length) {
|
|
997
|
+
if (pending[index] !== "\x1B") {
|
|
998
|
+
out += pending[index];
|
|
999
|
+
index += 1;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const rest = pending.slice(index);
|
|
1003
|
+
ANSI_PATTERN.lastIndex = 0;
|
|
1004
|
+
const match = ANSI_PATTERN.exec(rest);
|
|
1005
|
+
if (match && match.index === 0) {
|
|
1006
|
+
index += match[0].length;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (!final && rest.length < 128 && isOpenAnsi(rest)) {
|
|
1010
|
+
pending = rest;
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
out += pending[index];
|
|
1014
|
+
index += 1;
|
|
1015
|
+
}
|
|
1016
|
+
pending = "";
|
|
1017
|
+
return out;
|
|
1018
|
+
};
|
|
1019
|
+
return {
|
|
1020
|
+
push(text) {
|
|
1021
|
+
pending += text;
|
|
1022
|
+
return take(false);
|
|
1023
|
+
},
|
|
1024
|
+
end() {
|
|
1025
|
+
return take(true);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function isOpenAnsi(text) {
|
|
1030
|
+
if (!text.startsWith("\x1B")) return false;
|
|
1031
|
+
if (text.length === 1) return true;
|
|
1032
|
+
if (text[1] === "[") {
|
|
1033
|
+
let index = 2;
|
|
1034
|
+
while (index < text.length && /[0-?]/.test(text[index])) index += 1;
|
|
1035
|
+
while (index < text.length && /[ -/]/.test(text[index])) index += 1;
|
|
1036
|
+
return index >= text.length;
|
|
1037
|
+
}
|
|
1038
|
+
if (text[1] === "]") return !text.includes("\x07") && !text.includes("\x1B\\");
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
function withoutCarriageReturn(buf) {
|
|
1042
|
+
if (!buf.includes(13)) return buf;
|
|
1043
|
+
const out = Buffer.allocUnsafe(buf.length);
|
|
1044
|
+
let size = 0;
|
|
1045
|
+
for (let index = 0; index < buf.length; index += 1) {
|
|
1046
|
+
const byte = buf[index];
|
|
1047
|
+
if (byte === 13) continue;
|
|
1048
|
+
out[size] = byte;
|
|
1049
|
+
size += 1;
|
|
1050
|
+
}
|
|
1051
|
+
return out.subarray(0, size);
|
|
1052
|
+
}
|
|
1053
|
+
function collectLimited(stream, maxBuffer, onText, stripCarriageReturn = false, ansi) {
|
|
1054
|
+
const state = { chunks: [], truncated: false };
|
|
1055
|
+
if (!stream) return state;
|
|
1056
|
+
const decoder = new StringDecoder("utf8");
|
|
1057
|
+
let bytes = 0;
|
|
1058
|
+
const emitText = (text) => {
|
|
1059
|
+
const cleaned = ansi ? ansi.push(text) : text;
|
|
1060
|
+
if (cleaned) onText?.(cleaned);
|
|
1061
|
+
};
|
|
1062
|
+
const emit = (buf) => {
|
|
1063
|
+
emitText(decoder.write(buf));
|
|
1064
|
+
};
|
|
1065
|
+
stream.on("data", (chunk) => {
|
|
1066
|
+
const raw = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1067
|
+
const buf = stripCarriageReturn ? withoutCarriageReturn(raw) : raw;
|
|
1068
|
+
if (buf.length === 0) return;
|
|
1069
|
+
emit(buf);
|
|
1070
|
+
if (bytes >= maxBuffer) {
|
|
1071
|
+
state.truncated = true;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const remaining = maxBuffer - bytes;
|
|
1075
|
+
if (buf.length <= remaining) {
|
|
1076
|
+
state.chunks.push(buf);
|
|
1077
|
+
bytes += buf.length;
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
state.chunks.push(buf.subarray(0, remaining));
|
|
1081
|
+
bytes += remaining;
|
|
1082
|
+
state.truncated = true;
|
|
1083
|
+
});
|
|
1084
|
+
stream.on("end", () => {
|
|
1085
|
+
const rest = ansi ? `${ansi.push(decoder.end())}${ansi.end()}` : decoder.end();
|
|
1086
|
+
if (rest) onText?.(rest);
|
|
1087
|
+
});
|
|
1088
|
+
return state;
|
|
1089
|
+
}
|
|
1090
|
+
function decodeLimited(state, removeAnsi = false) {
|
|
1091
|
+
const decoder = new StringDecoder("utf8");
|
|
1092
|
+
const text = decoder.write(Buffer.concat(state.chunks));
|
|
1093
|
+
const flushed = state.truncated ? "" : decoder.end();
|
|
1094
|
+
const decoded = text + flushed;
|
|
1095
|
+
return (removeAnsi ? stripAnsi(decoded) : decoded).trim();
|
|
1096
|
+
}
|
|
1097
|
+
function signalProcessTree(pid, signal) {
|
|
1098
|
+
if (!pid || pid <= 0) return;
|
|
1099
|
+
if (process.platform === "win32") {
|
|
954
1100
|
try {
|
|
955
|
-
|
|
956
|
-
windowsHide: true,
|
|
957
|
-
stdio: "ignore"
|
|
958
|
-
});
|
|
1101
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, timeout: 5e3 });
|
|
959
1102
|
} catch {
|
|
960
|
-
child.kill("SIGKILL");
|
|
961
1103
|
}
|
|
962
|
-
|
|
963
|
-
child.kill("SIGTERM");
|
|
964
|
-
setTimeout(() => {
|
|
965
|
-
try {
|
|
966
|
-
child.kill("SIGKILL");
|
|
967
|
-
} catch {
|
|
968
|
-
}
|
|
969
|
-
}, 2e3);
|
|
1104
|
+
return;
|
|
970
1105
|
}
|
|
1106
|
+
try {
|
|
1107
|
+
process.kill(-pid, signal);
|
|
1108
|
+
} catch {
|
|
1109
|
+
try {
|
|
1110
|
+
process.kill(pid, signal);
|
|
1111
|
+
} catch {
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function ignoreStreamError(stream) {
|
|
1116
|
+
stream?.on("error", () => {
|
|
1117
|
+
});
|
|
971
1118
|
}
|
|
972
1119
|
async function executeBash(params) {
|
|
973
1120
|
const trimmedCommand = params.command?.trim();
|
|
@@ -986,77 +1133,83 @@ async function executeBash(params) {
|
|
|
986
1133
|
});
|
|
987
1134
|
const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
988
1135
|
const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
|
|
989
|
-
const startTime = Date.now();
|
|
990
1136
|
const shell = resolveShell();
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
} else {
|
|
1021
|
-
truncated = true;
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
child.stderr.on("data", (chunk) => {
|
|
1025
|
-
if (stderrBytes < maxBuffer) {
|
|
1026
|
-
const remaining = maxBuffer - stderrBytes;
|
|
1027
|
-
if (chunk.length <= remaining) {
|
|
1028
|
-
stderrChunks.push(chunk);
|
|
1029
|
-
stderrBytes += chunk.length;
|
|
1030
|
-
} else {
|
|
1031
|
-
stderrChunks.push(chunk.subarray(0, remaining));
|
|
1032
|
-
stderrBytes += remaining;
|
|
1033
|
-
truncated = true;
|
|
1034
|
-
}
|
|
1035
|
-
} else {
|
|
1036
|
-
truncated = true;
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
child.on("error", (err) => {
|
|
1040
|
-
clearTimeout(killTimeoutTimer);
|
|
1041
|
-
reject(err);
|
|
1042
|
-
});
|
|
1043
|
-
child.on("close", (exitCode, signal) => {
|
|
1044
|
-
clearTimeout(killTimeoutTimer);
|
|
1045
|
-
if (isKilled) return;
|
|
1046
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
1047
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
1048
|
-
resolve3({
|
|
1049
|
-
stdout,
|
|
1050
|
-
stderr,
|
|
1051
|
-
exitCode,
|
|
1052
|
-
signal,
|
|
1053
|
-
durationMs: Date.now() - startTime,
|
|
1054
|
-
...truncated ? { truncated: true } : {}
|
|
1055
|
-
});
|
|
1056
|
-
});
|
|
1137
|
+
const useTty = params.tty === true && process.platform !== "win32";
|
|
1138
|
+
const detached = process.platform !== "win32";
|
|
1139
|
+
const subprocess = useTty ? execa("python3", ["-u", "-c", TTY_HELPER], {
|
|
1140
|
+
cwd,
|
|
1141
|
+
env: {
|
|
1142
|
+
...env,
|
|
1143
|
+
PAGER: "cat",
|
|
1144
|
+
GIT_PAGER: "cat",
|
|
1145
|
+
BASH_TTY_COMMAND: trimmedCommand,
|
|
1146
|
+
BASH_TTY_SHELL: shell
|
|
1147
|
+
},
|
|
1148
|
+
stdin: "ignore",
|
|
1149
|
+
buffer: false,
|
|
1150
|
+
encoding: "buffer",
|
|
1151
|
+
reject: false,
|
|
1152
|
+
windowsHide: true,
|
|
1153
|
+
stripFinalNewline: false,
|
|
1154
|
+
detached
|
|
1155
|
+
}) : execa(trimmedCommand, {
|
|
1156
|
+
shell,
|
|
1157
|
+
cwd,
|
|
1158
|
+
env,
|
|
1159
|
+
stdin: "ignore",
|
|
1160
|
+
buffer: false,
|
|
1161
|
+
encoding: "buffer",
|
|
1162
|
+
reject: false,
|
|
1163
|
+
windowsHide: true,
|
|
1164
|
+
stripFinalNewline: false,
|
|
1165
|
+
detached
|
|
1057
1166
|
});
|
|
1167
|
+
ignoreStreamError(subprocess.stdout);
|
|
1168
|
+
ignoreStreamError(subprocess.stderr);
|
|
1169
|
+
let timedOut = false;
|
|
1170
|
+
let forceTimer;
|
|
1171
|
+
const timer = setTimeout(() => {
|
|
1172
|
+
timedOut = true;
|
|
1173
|
+
signalProcessTree(subprocess.pid, "SIGTERM");
|
|
1174
|
+
forceTimer = setTimeout(() => {
|
|
1175
|
+
signalProcessTree(subprocess.pid, "SIGKILL");
|
|
1176
|
+
subprocess.stdout?.destroy();
|
|
1177
|
+
subprocess.stderr?.destroy();
|
|
1178
|
+
}, FORCE_KILL_AFTER_DELAY_MS);
|
|
1179
|
+
}, normalizedTimeoutMs);
|
|
1180
|
+
const emit = (stream) => (text) => {
|
|
1181
|
+
params.onOutput?.({ stream, text });
|
|
1182
|
+
};
|
|
1183
|
+
const ansi = useTty ? createAnsiStripper() : void 0;
|
|
1184
|
+
const stdoutState = collectLimited(subprocess.stdout, maxBuffer, emit("stdout"), useTty, ansi);
|
|
1185
|
+
const stderrState = collectLimited(subprocess.stderr, maxBuffer, emit("stderr"));
|
|
1186
|
+
let result;
|
|
1187
|
+
try {
|
|
1188
|
+
result = await subprocess;
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
if (timedOut) throw new BashTimeoutError(normalizedTimeoutMs);
|
|
1191
|
+
throw err;
|
|
1192
|
+
} finally {
|
|
1193
|
+
clearTimeout(timer);
|
|
1194
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
1195
|
+
}
|
|
1196
|
+
if (timedOut) {
|
|
1197
|
+
throw new BashTimeoutError(normalizedTimeoutMs);
|
|
1198
|
+
}
|
|
1199
|
+
if (result.failed && result.exitCode == null && result.signal == null && !result.isTerminated) {
|
|
1200
|
+
throw new Error(result.shortMessage || result.message || "\u547D\u4EE4\u542F\u52A8\u5931\u8D25");
|
|
1201
|
+
}
|
|
1202
|
+
const truncated = stdoutState.truncated || stderrState.truncated;
|
|
1203
|
+
return {
|
|
1204
|
+
stdout: decodeLimited(stdoutState, useTty),
|
|
1205
|
+
stderr: decodeLimited(stderrState),
|
|
1206
|
+
exitCode: result.exitCode ?? null,
|
|
1207
|
+
signal: result.signal ?? null,
|
|
1208
|
+
durationMs: result.durationMs ?? 0,
|
|
1209
|
+
...truncated ? { truncated: true } : {}
|
|
1210
|
+
};
|
|
1058
1211
|
}
|
|
1059
|
-
var BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell;
|
|
1212
|
+
var BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell, FORCE_KILL_AFTER_DELAY_MS, TTY_HELPER, ANSI_PATTERN;
|
|
1060
1213
|
var init_bash_executor = __esm({
|
|
1061
1214
|
"src/bridge/bash-executor.ts"() {
|
|
1062
1215
|
"use strict";
|
|
@@ -1074,11 +1227,56 @@ var init_bash_executor = __esm({
|
|
|
1074
1227
|
MAX_TIMEOUT_MS = 6e5;
|
|
1075
1228
|
DEFAULT_MAX_BUFFER = 512 * 1024;
|
|
1076
1229
|
cachedShell = null;
|
|
1230
|
+
FORCE_KILL_AFTER_DELAY_MS = 2e3;
|
|
1231
|
+
TTY_HELPER = `
|
|
1232
|
+
import errno, os, sys
|
|
1233
|
+
cmd = os.environ.get("BASH_TTY_COMMAND") or ""
|
|
1234
|
+
shell = os.environ.get("BASH_TTY_SHELL") or "/bin/bash"
|
|
1235
|
+
if not cmd:
|
|
1236
|
+
sys.exit(127)
|
|
1237
|
+
import pty
|
|
1238
|
+
pid, fd = pty.fork()
|
|
1239
|
+
if pid == 0:
|
|
1240
|
+
os.environ.pop("BASH_TTY_COMMAND", None)
|
|
1241
|
+
os.environ.pop("BASH_TTY_SHELL", None)
|
|
1242
|
+
os.environ["TERM"] = "xterm-256color"
|
|
1243
|
+
try:
|
|
1244
|
+
# stdout/stderr \u4ECD\u662F\u7EC8\u7AEF\uFF1Bstdin \u6362\u6210 /dev/null\uFF0C\u8BFB\u53D6\u8F93\u5165\u7684\u547D\u4EE4\u4F1A\u7ACB\u523B\u9047\u5230 EOF
|
|
1245
|
+
null_fd = os.open(os.devnull, os.O_RDONLY)
|
|
1246
|
+
os.dup2(null_fd, 0)
|
|
1247
|
+
os.close(null_fd)
|
|
1248
|
+
os.execvp(shell, [shell, "-c", cmd])
|
|
1249
|
+
except OSError:
|
|
1250
|
+
os._exit(127)
|
|
1251
|
+
while True:
|
|
1252
|
+
try:
|
|
1253
|
+
data = os.read(fd, 65536)
|
|
1254
|
+
except OSError as exc:
|
|
1255
|
+
if exc.errno == errno.EINTR:
|
|
1256
|
+
continue
|
|
1257
|
+
break
|
|
1258
|
+
if not data:
|
|
1259
|
+
break
|
|
1260
|
+
try:
|
|
1261
|
+
os.write(1, data)
|
|
1262
|
+
except OSError:
|
|
1263
|
+
break
|
|
1264
|
+
try:
|
|
1265
|
+
_, status = os.waitpid(pid, 0)
|
|
1266
|
+
except OSError:
|
|
1267
|
+
sys.exit(1)
|
|
1268
|
+
if os.WIFEXITED(status):
|
|
1269
|
+
sys.exit(os.WEXITSTATUS(status))
|
|
1270
|
+
if os.WIFSIGNALED(status):
|
|
1271
|
+
sys.exit(128 + os.WTERMSIG(status))
|
|
1272
|
+
sys.exit(1)
|
|
1273
|
+
`;
|
|
1274
|
+
ANSI_PATTERN = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
|
|
1077
1275
|
}
|
|
1078
1276
|
});
|
|
1079
1277
|
|
|
1080
1278
|
// src/bridge/directory-picker.ts
|
|
1081
|
-
import { spawn as
|
|
1279
|
+
import { spawn as spawn2 } from "child_process";
|
|
1082
1280
|
async function pickDirectory(optionsOrTitle) {
|
|
1083
1281
|
const options = typeof optionsOrTitle === "string" ? { title: optionsOrTitle } : optionsOrTitle || {};
|
|
1084
1282
|
const title = options.title || "\u8BF7\u9009\u62E9\u5DE5\u4F5C\u7A7A\u95F4\u6587\u4EF6\u5939";
|
|
@@ -1097,7 +1295,7 @@ function pickDirectoryMac(title, defaultPath) {
|
|
|
1097
1295
|
const defaultClause = defaultPath ? ` default location POSIX file "${defaultPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : "";
|
|
1098
1296
|
const appleScript = `POSIX path of (choose folder with prompt "${safeTitle}"${defaultClause})`;
|
|
1099
1297
|
return new Promise((resolve3, reject) => {
|
|
1100
|
-
const child =
|
|
1298
|
+
const child = spawn2("osascript", ["-e", appleScript], {
|
|
1101
1299
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1102
1300
|
});
|
|
1103
1301
|
let stdout = "";
|
|
@@ -1215,7 +1413,7 @@ $result = [FolderPicker]::Pick("${safeTitle}", "${safeDefaultPath}")
|
|
|
1215
1413
|
if ($result) { Write-Output $result }
|
|
1216
1414
|
`;
|
|
1217
1415
|
return new Promise((resolve3, reject) => {
|
|
1218
|
-
const child =
|
|
1416
|
+
const child = spawn2("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
|
|
1219
1417
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1220
1418
|
windowsHide: true
|
|
1221
1419
|
});
|
|
@@ -1246,7 +1444,7 @@ function pickDirectoryLinux(title, defaultPath) {
|
|
|
1246
1444
|
args.push(`--filename=${defaultPath}`);
|
|
1247
1445
|
}
|
|
1248
1446
|
return new Promise((resolve3, reject) => {
|
|
1249
|
-
const child =
|
|
1447
|
+
const child = spawn2("zenity", args, {
|
|
1250
1448
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1251
1449
|
});
|
|
1252
1450
|
let stdout = "";
|
|
@@ -1284,6 +1482,385 @@ var init_directory_picker = __esm({
|
|
|
1284
1482
|
}
|
|
1285
1483
|
});
|
|
1286
1484
|
|
|
1485
|
+
// src/bridge/pty-backend.ts
|
|
1486
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1487
|
+
import { chmodSync, existsSync } from "fs";
|
|
1488
|
+
import { createRequire } from "module";
|
|
1489
|
+
import path4 from "path";
|
|
1490
|
+
import { constants } from "os";
|
|
1491
|
+
function interactiveArgs(shell) {
|
|
1492
|
+
if (process.platform !== "win32") return ["-i"];
|
|
1493
|
+
const base = path4.basename(shell).toLowerCase();
|
|
1494
|
+
if (base === "powershell.exe" || base === "pwsh.exe") return ["-NoLogo"];
|
|
1495
|
+
if (base === "cmd.exe") return [];
|
|
1496
|
+
return ["-i"];
|
|
1497
|
+
}
|
|
1498
|
+
function ptyEnvironment() {
|
|
1499
|
+
const env = {};
|
|
1500
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1501
|
+
if (value !== void 0) env[key] = value;
|
|
1502
|
+
}
|
|
1503
|
+
env.TERM = "xterm-256color";
|
|
1504
|
+
return env;
|
|
1505
|
+
}
|
|
1506
|
+
function resolveSpawnFile(shell, pathEnv = process.env.PATH || "") {
|
|
1507
|
+
if (!shell || path4.isAbsolute(shell)) return shell;
|
|
1508
|
+
const names = process.platform === "win32" && !shell.toLowerCase().endsWith(".exe") ? [shell, `${shell}.exe`] : [shell];
|
|
1509
|
+
for (const dir of pathEnv.split(path4.delimiter)) {
|
|
1510
|
+
if (!dir) continue;
|
|
1511
|
+
for (const name of names) {
|
|
1512
|
+
const candidate = path4.join(dir, name);
|
|
1513
|
+
try {
|
|
1514
|
+
if (existsSync(candidate)) return candidate;
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (process.platform === "win32") {
|
|
1520
|
+
const systemRoot = process.env.SystemRoot || "C:\\Windows";
|
|
1521
|
+
const fallbacks = [
|
|
1522
|
+
path4.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
1523
|
+
path4.join(systemRoot, "Sysnative", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
1524
|
+
];
|
|
1525
|
+
for (const candidate of fallbacks) {
|
|
1526
|
+
try {
|
|
1527
|
+
if (existsSync(candidate)) return candidate;
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return shell;
|
|
1533
|
+
}
|
|
1534
|
+
function describeSpawnHelperChmodError(helper, code) {
|
|
1535
|
+
if (code !== "EPERM" && code !== "EACCES") return null;
|
|
1536
|
+
return `spawn-helper \u65E0\u6CD5\u8BBE\u7F6E\u4E3A\u53EF\u6267\u884C\uFF08${helper}\uFF09\u3002\u5F53\u524D\u7528\u6237\u6CA1\u6709\u6743\u9650\u4FEE\u6539\u8BE5\u6587\u4EF6\u3002\u8BF7\u7528\u5B89\u88C5\u65F6\u7684\u7528\u6237\u91CD\u65B0\u5B89\u88C5 @opentiny/webmcp-cli\uFF0C\u6216\u5BF9\u8BE5\u6587\u4EF6\u6267\u884C chmod 755\u3002`;
|
|
1537
|
+
}
|
|
1538
|
+
function loadNodePty() {
|
|
1539
|
+
try {
|
|
1540
|
+
return createRequire(import.meta.url)("node-pty");
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1543
|
+
throw new Error(`\u4EA4\u4E92\u7EC8\u7AEF\u4E0D\u53EF\u7528\uFF1A\u672A\u80FD\u52A0\u8F7D node-pty\uFF08${detail}\uFF09\u3002\u5176\u4F59\u6865\u63A5\u547D\u4EE4\u4ECD\u53EF\u4F7F\u7528\u3002`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function spawnHelperPath() {
|
|
1547
|
+
try {
|
|
1548
|
+
const pkgJson = createRequire(import.meta.url).resolve("node-pty/package.json");
|
|
1549
|
+
return path4.join(path4.dirname(pkgJson), "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function ensureSpawnHelper() {
|
|
1555
|
+
if (process.platform === "win32") return null;
|
|
1556
|
+
const helper = spawnHelperPath();
|
|
1557
|
+
if (!helper || !existsSync(helper)) return null;
|
|
1558
|
+
try {
|
|
1559
|
+
chmodSync(helper, 493);
|
|
1560
|
+
return null;
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
|
|
1563
|
+
return describeSpawnHelperChmodError(helper, code);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
function signalName(code) {
|
|
1567
|
+
if (!code) return null;
|
|
1568
|
+
for (const [name, value] of Object.entries(constants.signals)) {
|
|
1569
|
+
if (value === code) return name;
|
|
1570
|
+
}
|
|
1571
|
+
return String(code);
|
|
1572
|
+
}
|
|
1573
|
+
function descendantPids(rootPid) {
|
|
1574
|
+
const result = spawnSync2("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 3e3 });
|
|
1575
|
+
if (result.status !== 0 || !result.stdout) return [];
|
|
1576
|
+
const children = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const line of result.stdout.split("\n")) {
|
|
1578
|
+
const [pidText, ppidText] = line.trim().split(/\s+/);
|
|
1579
|
+
const pid = Number(pidText);
|
|
1580
|
+
const ppid = Number(ppidText);
|
|
1581
|
+
if (!pid || !ppid) continue;
|
|
1582
|
+
const list = children.get(ppid);
|
|
1583
|
+
if (list) list.push(pid);
|
|
1584
|
+
else children.set(ppid, [pid]);
|
|
1585
|
+
}
|
|
1586
|
+
const out = [];
|
|
1587
|
+
const walk = (pid) => {
|
|
1588
|
+
for (const child of children.get(pid) || []) {
|
|
1589
|
+
walk(child);
|
|
1590
|
+
out.push(child);
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
walk(rootPid);
|
|
1594
|
+
return out;
|
|
1595
|
+
}
|
|
1596
|
+
function killPtySession(term) {
|
|
1597
|
+
const pid = term.pid;
|
|
1598
|
+
if (pid && pid > 0) {
|
|
1599
|
+
if (process.platform === "win32") {
|
|
1600
|
+
try {
|
|
1601
|
+
spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, timeout: 3e3 });
|
|
1602
|
+
} catch {
|
|
1603
|
+
}
|
|
1604
|
+
} else {
|
|
1605
|
+
for (const victim of [...descendantPids(pid), pid]) {
|
|
1606
|
+
try {
|
|
1607
|
+
process.kill(victim, "SIGKILL");
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
term.kill("SIGHUP");
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function spawnHostPty(options) {
|
|
1619
|
+
const pty = loadNodePty();
|
|
1620
|
+
const helperError = ensureSpawnHelper();
|
|
1621
|
+
const shell = resolveSpawnFile(options.shell || resolveShell());
|
|
1622
|
+
const pending = [];
|
|
1623
|
+
let dataCb = null;
|
|
1624
|
+
let exitCb = null;
|
|
1625
|
+
let pendingExit = null;
|
|
1626
|
+
let settled = false;
|
|
1627
|
+
let term = null;
|
|
1628
|
+
const emitData = (data) => {
|
|
1629
|
+
if (!data) return;
|
|
1630
|
+
if (dataCb) dataCb(data);
|
|
1631
|
+
else pending.push(data);
|
|
1632
|
+
};
|
|
1633
|
+
const finish = (info) => {
|
|
1634
|
+
if (settled) return;
|
|
1635
|
+
settled = true;
|
|
1636
|
+
if (exitCb) exitCb(info);
|
|
1637
|
+
else pendingExit = info;
|
|
1638
|
+
};
|
|
1639
|
+
try {
|
|
1640
|
+
term = pty.spawn(shell, interactiveArgs(shell), {
|
|
1641
|
+
name: "xterm-256color",
|
|
1642
|
+
cols: options.cols,
|
|
1643
|
+
rows: options.rows,
|
|
1644
|
+
cwd: options.cwd,
|
|
1645
|
+
env: ptyEnvironment()
|
|
1646
|
+
});
|
|
1647
|
+
term.onData((data) => emitData(data));
|
|
1648
|
+
term.onExit((event) => {
|
|
1649
|
+
finish({ exitCode: event.exitCode, signal: signalName(event.signal) });
|
|
1650
|
+
});
|
|
1651
|
+
} catch (err) {
|
|
1652
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1653
|
+
const detail = helperError ? `${helperError} ${message}` : message;
|
|
1654
|
+
setImmediate(() => {
|
|
1655
|
+
emitData(`\r
|
|
1656
|
+
[\u7EC8\u7AEF\u542F\u52A8\u5931\u8D25] ${detail}\r
|
|
1657
|
+
`);
|
|
1658
|
+
finish({ exitCode: null, signal: null });
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
write(data) {
|
|
1663
|
+
try {
|
|
1664
|
+
term?.write(data);
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1668
|
+
resize(cols, rows) {
|
|
1669
|
+
try {
|
|
1670
|
+
term?.resize(cols, rows);
|
|
1671
|
+
} catch {
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
kill() {
|
|
1675
|
+
if (term) killPtySession(term);
|
|
1676
|
+
},
|
|
1677
|
+
pause() {
|
|
1678
|
+
try {
|
|
1679
|
+
term?.pause();
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
resume() {
|
|
1684
|
+
try {
|
|
1685
|
+
term?.resume();
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
onData(cb) {
|
|
1690
|
+
dataCb = cb;
|
|
1691
|
+
for (const chunk of pending) cb(chunk);
|
|
1692
|
+
pending.length = 0;
|
|
1693
|
+
},
|
|
1694
|
+
onExit(cb) {
|
|
1695
|
+
exitCb = cb;
|
|
1696
|
+
if (pendingExit) cb(pendingExit);
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
var init_pty_backend = __esm({
|
|
1701
|
+
"src/bridge/pty-backend.ts"() {
|
|
1702
|
+
"use strict";
|
|
1703
|
+
init_bash_executor();
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
// src/bridge/terminal-session.ts
|
|
1708
|
+
import { randomUUID } from "crypto";
|
|
1709
|
+
function applyTerminalBackpressure(bufferedAmount, flow) {
|
|
1710
|
+
if (bufferedAmount > TERMINAL_OUTPUT_HIGH_WATER) flow.pauseOutput();
|
|
1711
|
+
else if (bufferedAmount <= TERMINAL_OUTPUT_HIGH_WATER / 2) flow.resumeOutput();
|
|
1712
|
+
}
|
|
1713
|
+
function clampDimension(value, fallback) {
|
|
1714
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1715
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1716
|
+
return Math.max(2, Math.min(500, Math.floor(parsed)));
|
|
1717
|
+
}
|
|
1718
|
+
var OUTPUT_CHUNK, MAX_INPUT, DEFAULT_COALESCE_MS, TERMINAL_OUTPUT_HIGH_WATER, TerminalSessionManager;
|
|
1719
|
+
var init_terminal_session = __esm({
|
|
1720
|
+
"src/bridge/terminal-session.ts"() {
|
|
1721
|
+
"use strict";
|
|
1722
|
+
init_workspace_manager();
|
|
1723
|
+
init_pty_backend();
|
|
1724
|
+
init_protocol();
|
|
1725
|
+
OUTPUT_CHUNK = 32 * 1024;
|
|
1726
|
+
MAX_INPUT = 64 * 1024;
|
|
1727
|
+
DEFAULT_COALESCE_MS = 16;
|
|
1728
|
+
TERMINAL_OUTPUT_HIGH_WATER = 1024 * 1024;
|
|
1729
|
+
TerminalSessionManager = class {
|
|
1730
|
+
session = null;
|
|
1731
|
+
buffer = "";
|
|
1732
|
+
timer = null;
|
|
1733
|
+
send;
|
|
1734
|
+
spawnPty;
|
|
1735
|
+
coalesceMs;
|
|
1736
|
+
constructor(options) {
|
|
1737
|
+
this.send = options.send;
|
|
1738
|
+
this.spawnPty = options.spawnPty ?? ((opts) => spawnHostPty(opts));
|
|
1739
|
+
this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
1740
|
+
}
|
|
1741
|
+
open(params = {}) {
|
|
1742
|
+
const cwd = resolveWorkspaceDir(params);
|
|
1743
|
+
const cols = clampDimension(params.cols, 80);
|
|
1744
|
+
const rows = clampDimension(params.rows, 24);
|
|
1745
|
+
this.closeActive();
|
|
1746
|
+
const id = randomUUID();
|
|
1747
|
+
const pty = this.spawnPty({ cwd, cols, rows });
|
|
1748
|
+
this.session = { id, pty, cwd };
|
|
1749
|
+
pty.onData((data) => this.pushOutput(id, data));
|
|
1750
|
+
pty.onExit((info) => {
|
|
1751
|
+
if (this.session?.id !== id) return;
|
|
1752
|
+
this.flush(id);
|
|
1753
|
+
this.session = null;
|
|
1754
|
+
this.send({
|
|
1755
|
+
jsonrpc: "2.0",
|
|
1756
|
+
method: TERMINAL_EVENTS.EXIT,
|
|
1757
|
+
params: {
|
|
1758
|
+
sessionId: id,
|
|
1759
|
+
exitCode: info.exitCode,
|
|
1760
|
+
signal: info.signal
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1764
|
+
return { sessionId: id, cwd };
|
|
1765
|
+
}
|
|
1766
|
+
input(params) {
|
|
1767
|
+
const session = this.requireSession(params.sessionId);
|
|
1768
|
+
if (typeof params.data !== "string") {
|
|
1769
|
+
throw new BashInvalidParamsError("data \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
1770
|
+
}
|
|
1771
|
+
if (params.data.length > MAX_INPUT) {
|
|
1772
|
+
throw new BashInvalidParamsError("\u5355\u6B21\u8F93\u5165\u8FC7\u957F");
|
|
1773
|
+
}
|
|
1774
|
+
session.pty.write(params.data);
|
|
1775
|
+
return { ok: true };
|
|
1776
|
+
}
|
|
1777
|
+
resize(params) {
|
|
1778
|
+
const session = this.requireSession(params.sessionId);
|
|
1779
|
+
session.pty.resize(clampDimension(params.cols, 80), clampDimension(params.rows, 24));
|
|
1780
|
+
return { ok: true };
|
|
1781
|
+
}
|
|
1782
|
+
close(params = {}) {
|
|
1783
|
+
if (!this.session || params.sessionId && params.sessionId !== this.session.id) {
|
|
1784
|
+
return { ok: true };
|
|
1785
|
+
}
|
|
1786
|
+
this.closeActive();
|
|
1787
|
+
return { ok: true };
|
|
1788
|
+
}
|
|
1789
|
+
disposeAll() {
|
|
1790
|
+
this.closeActive();
|
|
1791
|
+
}
|
|
1792
|
+
pauseOutput() {
|
|
1793
|
+
try {
|
|
1794
|
+
this.session?.pty.pause();
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
resumeOutput() {
|
|
1799
|
+
try {
|
|
1800
|
+
this.session?.pty.resume();
|
|
1801
|
+
} catch {
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
requireSession(sessionId) {
|
|
1805
|
+
if (!this.session || !sessionId || sessionId !== this.session.id) {
|
|
1806
|
+
throw new Error("\u7EC8\u7AEF\u4F1A\u8BDD\u4E0D\u5B58\u5728");
|
|
1807
|
+
}
|
|
1808
|
+
return this.session;
|
|
1809
|
+
}
|
|
1810
|
+
closeActive() {
|
|
1811
|
+
const current = this.session;
|
|
1812
|
+
this.session = null;
|
|
1813
|
+
this.buffer = "";
|
|
1814
|
+
if (this.timer) {
|
|
1815
|
+
clearTimeout(this.timer);
|
|
1816
|
+
this.timer = null;
|
|
1817
|
+
}
|
|
1818
|
+
current?.pty.kill();
|
|
1819
|
+
}
|
|
1820
|
+
pushOutput(sessionId, chunk) {
|
|
1821
|
+
if (this.session?.id !== sessionId || !chunk) return;
|
|
1822
|
+
this.buffer += chunk;
|
|
1823
|
+
this.flushFullChunks(sessionId);
|
|
1824
|
+
if (!this.buffer) return;
|
|
1825
|
+
if (this.coalesceMs <= 0) {
|
|
1826
|
+
this.flush(sessionId);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (!this.timer) {
|
|
1830
|
+
this.timer = setTimeout(() => this.flush(sessionId), this.coalesceMs);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
flushFullChunks(sessionId) {
|
|
1834
|
+
while (this.buffer.length >= OUTPUT_CHUNK) {
|
|
1835
|
+
const data = this.buffer.slice(0, OUTPUT_CHUNK);
|
|
1836
|
+
this.buffer = this.buffer.slice(OUTPUT_CHUNK);
|
|
1837
|
+
this.emitOutput(sessionId, data);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
flush(sessionId) {
|
|
1841
|
+
if (this.timer) {
|
|
1842
|
+
clearTimeout(this.timer);
|
|
1843
|
+
this.timer = null;
|
|
1844
|
+
}
|
|
1845
|
+
if (!this.buffer || this.session?.id !== sessionId) {
|
|
1846
|
+
this.buffer = "";
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const data = this.buffer;
|
|
1850
|
+
this.buffer = "";
|
|
1851
|
+
this.emitOutput(sessionId, data);
|
|
1852
|
+
}
|
|
1853
|
+
emitOutput(sessionId, data) {
|
|
1854
|
+
this.send({
|
|
1855
|
+
jsonrpc: "2.0",
|
|
1856
|
+
method: TERMINAL_EVENTS.OUTPUT,
|
|
1857
|
+
params: { sessionId, data }
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1287
1864
|
// src/bridge/bridge-client.ts
|
|
1288
1865
|
var bridge_client_exports = {};
|
|
1289
1866
|
__export(bridge_client_exports, {
|
|
@@ -1298,10 +1875,14 @@ __export(bridge_client_exports, {
|
|
|
1298
1875
|
});
|
|
1299
1876
|
import { WebSocketServer, WebSocket as WsClient } from "ws";
|
|
1300
1877
|
import { EventEmitter } from "events";
|
|
1301
|
-
import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
|
|
1878
|
+
import { randomUUID as randomUUID2, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
|
|
1302
1879
|
import fs4 from "fs";
|
|
1303
|
-
import
|
|
1880
|
+
import path5 from "path";
|
|
1304
1881
|
import os3 from "os";
|
|
1882
|
+
function isJsonRpcId(id) {
|
|
1883
|
+
if (id === null || typeof id === "string") return true;
|
|
1884
|
+
return typeof id === "number" && Number.isFinite(id);
|
|
1885
|
+
}
|
|
1305
1886
|
function resolveOrCreateAuthToken(explicitToken) {
|
|
1306
1887
|
if (explicitToken && explicitToken.trim()) {
|
|
1307
1888
|
const token = explicitToken.trim();
|
|
@@ -1367,12 +1948,13 @@ var init_bridge_client = __esm({
|
|
|
1367
1948
|
init_protocol();
|
|
1368
1949
|
init_bash_executor();
|
|
1369
1950
|
init_directory_picker();
|
|
1951
|
+
init_terminal_session();
|
|
1370
1952
|
WS_PORT = DEFAULT_WS_PORT;
|
|
1371
1953
|
REQUEST_TIMEOUT_MS = 3e4;
|
|
1372
1954
|
HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
1373
1955
|
WS_OPEN = 1;
|
|
1374
|
-
TOKEN_FILE_DIR =
|
|
1375
|
-
TOKEN_FILE_PATH =
|
|
1956
|
+
TOKEN_FILE_DIR = path5.join(os3.homedir(), ".robot-wxt");
|
|
1957
|
+
TOKEN_FILE_PATH = path5.join(TOKEN_FILE_DIR, "bridge-token");
|
|
1376
1958
|
ExtensionBridgeClient = class _ExtensionBridgeClient extends EventEmitter {
|
|
1377
1959
|
constructor(port = WS_PORT, authToken) {
|
|
1378
1960
|
super();
|
|
@@ -1388,6 +1970,7 @@ var init_bridge_client = __esm({
|
|
|
1388
1970
|
pending = /* @__PURE__ */ new Map();
|
|
1389
1971
|
socketAuthStates = /* @__PURE__ */ new Map();
|
|
1390
1972
|
destroyed = false;
|
|
1973
|
+
terminalSessions = null;
|
|
1391
1974
|
idCounter = 0;
|
|
1392
1975
|
isClientMode = false;
|
|
1393
1976
|
explicitAuthToken;
|
|
@@ -1642,7 +2225,7 @@ var init_bridge_client = __esm({
|
|
|
1642
2225
|
this.emit("error", err);
|
|
1643
2226
|
});
|
|
1644
2227
|
this.wss.on("connection", (socket) => {
|
|
1645
|
-
const challenge =
|
|
2228
|
+
const challenge = randomUUID2();
|
|
1646
2229
|
const hmacSalt = randomBytes(16).toString("hex");
|
|
1647
2230
|
const authState = {
|
|
1648
2231
|
authenticated: false,
|
|
@@ -1684,6 +2267,7 @@ var init_bridge_client = __esm({
|
|
|
1684
2267
|
}
|
|
1685
2268
|
if (socket === this.extensionSocket) {
|
|
1686
2269
|
this.extensionSocket = null;
|
|
2270
|
+
this.terminalSessions?.disposeAll();
|
|
1687
2271
|
this.emit("disconnected");
|
|
1688
2272
|
for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
|
|
1689
2273
|
if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
|
|
@@ -1730,6 +2314,16 @@ var init_bridge_client = __esm({
|
|
|
1730
2314
|
return;
|
|
1731
2315
|
}
|
|
1732
2316
|
if (msg.method && msg.id) {
|
|
2317
|
+
if (typeof msg.method === "string" && isTerminalMethod(msg.method) && socket !== this.extensionSocket) {
|
|
2318
|
+
this.sendJsonRpcResponse(socket, {
|
|
2319
|
+
id: msg.id,
|
|
2320
|
+
error: {
|
|
2321
|
+
code: BRIDGE_ERROR_CODE.METHOD_NOT_FOUND,
|
|
2322
|
+
message: "\u7EC8\u7AEF\u4F1A\u8BDD\u4EC5\u5141\u8BB8\u5DF2\u8BA4\u8BC1\u7684\u6269\u5C55\u8FDE\u63A5"
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
1733
2327
|
if (socket === this.extensionSocket) {
|
|
1734
2328
|
await this.handleExtensionSystemRpc(socket, msg);
|
|
1735
2329
|
return;
|
|
@@ -1802,6 +2396,7 @@ var init_bridge_client = __esm({
|
|
|
1802
2396
|
currentAuth.clientType = msg.clientType;
|
|
1803
2397
|
if (msg.clientType === "extension") {
|
|
1804
2398
|
if (this.extensionSocket && this.extensionSocket !== socket) {
|
|
2399
|
+
this.terminalSessions?.disposeAll();
|
|
1805
2400
|
try {
|
|
1806
2401
|
this.extensionSocket.close(WS_CLOSE_CODE.REPLACED, "Replaced by new connection");
|
|
1807
2402
|
} catch {
|
|
@@ -1893,9 +2488,6 @@ var init_bridge_client = __esm({
|
|
|
1893
2488
|
systemRpcHandlers = Object.assign(
|
|
1894
2489
|
/* @__PURE__ */ Object.create(null),
|
|
1895
2490
|
{
|
|
1896
|
-
[BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
|
|
1897
|
-
return executeBash(params || {});
|
|
1898
|
-
},
|
|
1899
2491
|
[BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
|
|
1900
2492
|
const selectedPath = await pickDirectory({
|
|
1901
2493
|
title: params?.title || void 0,
|
|
@@ -1906,11 +2498,61 @@ var init_bridge_client = __esm({
|
|
|
1906
2498
|
selectedPath,
|
|
1907
2499
|
canceled: selectedPath === null
|
|
1908
2500
|
};
|
|
1909
|
-
}
|
|
2501
|
+
},
|
|
2502
|
+
[BRIDGE_METHODS.TERMINAL_OPEN]: async (params) => this.getTerminalSessions().open(params || {}),
|
|
2503
|
+
[BRIDGE_METHODS.TERMINAL_INPUT]: async (params) => this.getTerminalSessions().input(params || {}),
|
|
2504
|
+
[BRIDGE_METHODS.TERMINAL_RESIZE]: async (params) => this.getTerminalSessions().resize(params || {}),
|
|
2505
|
+
[BRIDGE_METHODS.TERMINAL_CLOSE]: async (params) => this.getTerminalSessions().close(params || {})
|
|
1910
2506
|
}
|
|
1911
2507
|
);
|
|
2508
|
+
sendJsonRpcNotification(socket, method, params) {
|
|
2509
|
+
if (socket.readyState !== WS_OPEN) return;
|
|
2510
|
+
let payload;
|
|
2511
|
+
try {
|
|
2512
|
+
payload = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
2513
|
+
} catch {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
try {
|
|
2517
|
+
socket.send(payload);
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
async executeHostBash(socket, id, params) {
|
|
2522
|
+
try {
|
|
2523
|
+
const requestParams = params && typeof params === "object" ? params : {};
|
|
2524
|
+
const result = await executeBash({
|
|
2525
|
+
...requestParams,
|
|
2526
|
+
onOutput: (chunk) => {
|
|
2527
|
+
if (!chunk.text) return;
|
|
2528
|
+
this.sendJsonRpcNotification(socket, BASH_EVENTS.OUTPUT, {
|
|
2529
|
+
id,
|
|
2530
|
+
stream: chunk.stream,
|
|
2531
|
+
text: chunk.text
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
this.sendJsonRpcResponse(socket, { id, result });
|
|
2536
|
+
} catch (err) {
|
|
2537
|
+
this.sendJsonRpcResponse(socket, { id, error: this.formatSystemRpcError(err) });
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
1912
2540
|
async handleExtensionSystemRpc(socket, msg) {
|
|
1913
2541
|
const { id, method, params } = msg;
|
|
2542
|
+
if (!isJsonRpcId(id)) {
|
|
2543
|
+
this.sendJsonRpcResponse(socket, {
|
|
2544
|
+
id: null,
|
|
2545
|
+
error: {
|
|
2546
|
+
code: BRIDGE_ERROR_CODE.INVALID_PARAMS,
|
|
2547
|
+
message: "\u65E0\u6548\u7684\u8BF7\u6C42 id"
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (method === BRIDGE_METHODS.SYSTEM_BASH) {
|
|
2553
|
+
await this.executeHostBash(socket, id, params);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
1914
2556
|
const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
|
|
1915
2557
|
if (!handler || typeof handler !== "function") {
|
|
1916
2558
|
this.sendJsonRpcResponse(socket, {
|
|
@@ -2006,9 +2648,37 @@ var init_bridge_client = __esm({
|
|
|
2006
2648
|
}
|
|
2007
2649
|
});
|
|
2008
2650
|
}
|
|
2651
|
+
getTerminalSessions() {
|
|
2652
|
+
if (!this.terminalSessions) {
|
|
2653
|
+
this.terminalSessions = new TerminalSessionManager({
|
|
2654
|
+
send: (frame) => {
|
|
2655
|
+
const socket = this.extensionSocket;
|
|
2656
|
+
if (!socket || socket.readyState !== WS_OPEN) return;
|
|
2657
|
+
const sessions = this.terminalSessions;
|
|
2658
|
+
if (sessions) applyTerminalBackpressure(socket.bufferedAmount ?? 0, sessions);
|
|
2659
|
+
let payload;
|
|
2660
|
+
try {
|
|
2661
|
+
payload = JSON.stringify(frame);
|
|
2662
|
+
} catch {
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
try {
|
|
2666
|
+
socket.send(payload, () => {
|
|
2667
|
+
if (this.terminalSessions && socket.readyState === WS_OPEN) {
|
|
2668
|
+
applyTerminalBackpressure(socket.bufferedAmount ?? 0, this.terminalSessions);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
} catch {
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
return this.terminalSessions;
|
|
2677
|
+
}
|
|
2009
2678
|
/** 销毁服务,断开所有连接 */
|
|
2010
2679
|
destroy() {
|
|
2011
2680
|
this.destroyed = true;
|
|
2681
|
+
this.terminalSessions?.disposeAll();
|
|
2012
2682
|
this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
|
|
2013
2683
|
this.forwardedRequests.clear();
|
|
2014
2684
|
if (this.clientSocket) {
|
|
@@ -2153,22 +2823,22 @@ __export(daemon_exports, {
|
|
|
2153
2823
|
isDaemonProcessAlive: () => isDaemonProcessAlive,
|
|
2154
2824
|
isProcessAlive: () => isProcessAlive,
|
|
2155
2825
|
killProcessOnPort: () => killProcessOnPort,
|
|
2156
|
-
killProcessTree: () =>
|
|
2826
|
+
killProcessTree: () => killProcessTree,
|
|
2157
2827
|
probeServerReady: () => probeServerReady,
|
|
2158
2828
|
runDaemonProcess: () => runDaemonProcess,
|
|
2159
2829
|
stopBridgeDaemon: () => stopBridgeDaemon
|
|
2160
2830
|
});
|
|
2161
|
-
import { spawn as
|
|
2831
|
+
import { spawn as spawn3, exec } from "child_process";
|
|
2162
2832
|
import { promisify } from "util";
|
|
2163
2833
|
import fs5 from "fs";
|
|
2164
|
-
import
|
|
2834
|
+
import path6 from "path";
|
|
2165
2835
|
import os4 from "os";
|
|
2166
2836
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2167
2837
|
function getDaemonPidFile(port = WS_PORT2) {
|
|
2168
2838
|
if (port === WS_PORT2) {
|
|
2169
|
-
return
|
|
2839
|
+
return path6.join(DAEMON_DIR, "daemon.pid");
|
|
2170
2840
|
}
|
|
2171
|
-
return
|
|
2841
|
+
return path6.join(DAEMON_DIR, `daemon-${port}.pid`);
|
|
2172
2842
|
}
|
|
2173
2843
|
function isProcessAlive(pid) {
|
|
2174
2844
|
if (!pid || pid <= 0) return false;
|
|
@@ -2212,7 +2882,7 @@ async function findPidByPort(port, excludePid) {
|
|
|
2212
2882
|
return null;
|
|
2213
2883
|
}
|
|
2214
2884
|
}
|
|
2215
|
-
async function
|
|
2885
|
+
async function killProcessTree(pid) {
|
|
2216
2886
|
if (!pid || pid <= 0 || pid === process.pid) return false;
|
|
2217
2887
|
if (process.platform === "win32") {
|
|
2218
2888
|
try {
|
|
@@ -2250,7 +2920,7 @@ async function killProcessTree2(pid) {
|
|
|
2250
2920
|
async function killProcessOnPort(port) {
|
|
2251
2921
|
const pid = await findPidByPort(port, process.pid);
|
|
2252
2922
|
if (pid) {
|
|
2253
|
-
await
|
|
2923
|
+
await killProcessTree(pid);
|
|
2254
2924
|
for (let i = 0; i < 20; i++) {
|
|
2255
2925
|
await new Promise((r) => setTimeout(r, 100));
|
|
2256
2926
|
const remainingPid = await findPidByPort(port, process.pid);
|
|
@@ -2314,10 +2984,10 @@ async function ensureBridgeDaemon(options = {}) {
|
|
|
2314
2984
|
throw new Error(`\u542F\u52A8\u540E\u53F0 WebSocket \u6865\u63A5\u670D\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7AEF\u53E3 ${port} \u662F\u5426\u53EF\u7528\u3002`);
|
|
2315
2985
|
}
|
|
2316
2986
|
function resolveCliEntry() {
|
|
2317
|
-
const currentDir =
|
|
2318
|
-
const distCandidate =
|
|
2987
|
+
const currentDir = path6.dirname(fileURLToPath3(import.meta.url));
|
|
2988
|
+
const distCandidate = path6.join(currentDir, "bin.js");
|
|
2319
2989
|
if (fs5.existsSync(distCandidate)) return distCandidate;
|
|
2320
|
-
const projectDistCandidate =
|
|
2990
|
+
const projectDistCandidate = path6.resolve(currentDir, "../dist/bin.js");
|
|
2321
2991
|
if (fs5.existsSync(projectDistCandidate)) return projectDistCandidate;
|
|
2322
2992
|
return distCandidate;
|
|
2323
2993
|
}
|
|
@@ -2326,7 +2996,7 @@ function startDaemonProcessBackground(token, port) {
|
|
|
2326
2996
|
if (!fs5.existsSync(DAEMON_DIR)) {
|
|
2327
2997
|
fs5.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
|
|
2328
2998
|
}
|
|
2329
|
-
const child =
|
|
2999
|
+
const child = spawn3(process.execPath, [cliEntry, "--mode", "wxt", "daemon", "--port", String(port)], {
|
|
2330
3000
|
detached: true,
|
|
2331
3001
|
stdio: "ignore",
|
|
2332
3002
|
windowsHide: true,
|
|
@@ -2432,7 +3102,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
|
|
|
2432
3102
|
if (portPid && portPid !== daemonPid) {
|
|
2433
3103
|
return false;
|
|
2434
3104
|
}
|
|
2435
|
-
const killed = await
|
|
3105
|
+
const killed = await killProcessTree(daemonPid);
|
|
2436
3106
|
if (killed) {
|
|
2437
3107
|
killedAny = true;
|
|
2438
3108
|
try {
|
|
@@ -2443,7 +3113,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
|
|
|
2443
3113
|
} else {
|
|
2444
3114
|
if (daemonPid) {
|
|
2445
3115
|
try {
|
|
2446
|
-
const killed = await
|
|
3116
|
+
const killed = await killProcessTree(daemonPid);
|
|
2447
3117
|
if (killed) {
|
|
2448
3118
|
killedAny = true;
|
|
2449
3119
|
try {
|
|
@@ -2483,7 +3153,7 @@ var init_daemon = __esm({
|
|
|
2483
3153
|
init_bridge_client();
|
|
2484
3154
|
execAsync = promisify(exec);
|
|
2485
3155
|
WS_PORT2 = 18999;
|
|
2486
|
-
DAEMON_DIR =
|
|
3156
|
+
DAEMON_DIR = path6.join(os4.homedir(), ".robot-wxt");
|
|
2487
3157
|
PID_FILE = getDaemonPidFile(WS_PORT2);
|
|
2488
3158
|
}
|
|
2489
3159
|
});
|
|
@@ -2945,7 +3615,7 @@ var init_skills = __esm({
|
|
|
2945
3615
|
|
|
2946
3616
|
// src/bin.ts
|
|
2947
3617
|
import fs6 from "fs";
|
|
2948
|
-
import
|
|
3618
|
+
import path7 from "path";
|
|
2949
3619
|
import os5 from "os";
|
|
2950
3620
|
import { Command } from "commander";
|
|
2951
3621
|
import pc2 from "picocolors";
|
|
@@ -3434,7 +4104,7 @@ function tryParseBrowserStateText(text) {
|
|
|
3434
4104
|
}
|
|
3435
4105
|
function cleanOldLogs(baseDir, logDir) {
|
|
3436
4106
|
try {
|
|
3437
|
-
const oldLogFile =
|
|
4107
|
+
const oldLogFile = path7.join(baseDir, "webmcp-cli.log");
|
|
3438
4108
|
if (fs6.existsSync(oldLogFile)) {
|
|
3439
4109
|
try {
|
|
3440
4110
|
fs6.unlinkSync(oldLogFile);
|
|
@@ -3446,7 +4116,7 @@ function cleanOldLogs(baseDir, logDir) {
|
|
|
3446
4116
|
for (const file of baseFiles) {
|
|
3447
4117
|
if (/^webmcp-cli-\d{4}-\d{2}-\d{2}\.log$/.test(file)) {
|
|
3448
4118
|
try {
|
|
3449
|
-
fs6.unlinkSync(
|
|
4119
|
+
fs6.unlinkSync(path7.join(baseDir, file));
|
|
3450
4120
|
} catch {
|
|
3451
4121
|
}
|
|
3452
4122
|
}
|
|
@@ -3465,7 +4135,7 @@ function cleanOldLogs(baseDir, logDir) {
|
|
|
3465
4135
|
if (isNaN(fileDate)) continue;
|
|
3466
4136
|
if (now - fileDate > maxAgeMs) {
|
|
3467
4137
|
try {
|
|
3468
|
-
fs6.unlinkSync(
|
|
4138
|
+
fs6.unlinkSync(path7.join(logDir, file));
|
|
3469
4139
|
} catch {
|
|
3470
4140
|
}
|
|
3471
4141
|
}
|
|
@@ -3521,13 +4191,13 @@ ${searchResultStr}`;
|
|
|
3521
4191
|
}
|
|
3522
4192
|
function writeLog(commandName, args, result, error) {
|
|
3523
4193
|
try {
|
|
3524
|
-
const baseDir = process.env.WEBMCP_WORKSPACE ||
|
|
3525
|
-
const logDir =
|
|
4194
|
+
const baseDir = process.env.WEBMCP_WORKSPACE || path7.join(os5.homedir(), ".webmcp_chrome_profile");
|
|
4195
|
+
const logDir = path7.join(baseDir, "logs");
|
|
3526
4196
|
if (!fs6.existsSync(logDir)) {
|
|
3527
4197
|
fs6.mkdirSync(logDir, { recursive: true });
|
|
3528
4198
|
}
|
|
3529
4199
|
const todayStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3530
|
-
const logFile =
|
|
4200
|
+
const logFile = path7.join(logDir, `webmcp-cli-${todayStr}.log`);
|
|
3531
4201
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3532
4202
|
let logText = `========================================
|
|
3533
4203
|
`;
|