terminal-bridge-setup 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/files/proxy/client-example.mjs +22 -10
- package/files/proxy/server.js +144 -27
- package/files/skill/SKILL.md +6 -0
- package/files/skill/references/protocol.md +14 -3
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ const BRIDGE = process.env.BRIDGE || "ws://127.0.0.1:8787/ssh";
|
|
|
24
24
|
* @param {number} timeoutMs - 超时(默认 10s)
|
|
25
25
|
* @returns {Promise<{ok: boolean, output?: string, error?: string, suggest?: string, message?: string}>}
|
|
26
26
|
*/
|
|
27
|
-
function run(cmd, timeoutMs = 10000) {
|
|
27
|
+
function run(cmd, timeoutMs = 10000, opts = {}) {
|
|
28
28
|
return new Promise((resolve) => {
|
|
29
29
|
const ws = new WebSocket(BRIDGE);
|
|
30
30
|
const reqId = randomBytes(4).toString("hex");
|
|
@@ -47,7 +47,12 @@ function run(cmd, timeoutMs = 10000) {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
ws.on("open", () => {
|
|
50
|
-
|
|
50
|
+
// base64 通道:命令 base64 编码后由代理包装成 `echo <b64> | base64 -d | sh`。
|
|
51
|
+
// 用于多层引号场景(Bash→mjs→SSH→kubectl exec→sh -c),彻底规避引号剥离。
|
|
52
|
+
const payload = { type: "run", reqId, timeoutMs };
|
|
53
|
+
if (opts.b64) payload.cmdB64 = Buffer.from(cmd, "utf8").toString("base64");
|
|
54
|
+
else payload.cmd = cmd;
|
|
55
|
+
ws.send(JSON.stringify(payload));
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
ws.on("message", (raw) => {
|
|
@@ -85,10 +90,11 @@ function run(cmd, timeoutMs = 10000) {
|
|
|
85
90
|
* @param {function} ask - 可选的自定义询问函数,默认用 readline 控制台提问。
|
|
86
91
|
* Agent 接入时应传入自己的 AskUserQuestion 逻辑。
|
|
87
92
|
* 签名: async (message) => boolean
|
|
93
|
+
* @param {object} opts - 可选,透传给 run():{ b64: true } 走 base64 通道
|
|
88
94
|
* @returns {Promise<{ok: boolean, output?: string, error?: string}>}
|
|
89
95
|
*/
|
|
90
|
-
async function runWithSudoRetry(cmd, timeoutMs = 10000, ask = defaultAsk) {
|
|
91
|
-
let result = await run(cmd, timeoutMs);
|
|
96
|
+
async function runWithSudoRetry(cmd, timeoutMs = 10000, ask = defaultAsk, opts = {}) {
|
|
97
|
+
let result = await run(cmd, timeoutMs, opts);
|
|
92
98
|
|
|
93
99
|
if (result.error === "sudo-required") {
|
|
94
100
|
const approved = await ask(result.message || "检测到需要 sudo 权限,是否切换到 root 后重试?");
|
|
@@ -103,7 +109,7 @@ async function runWithSudoRetry(cmd, timeoutMs = 10000, ask = defaultAsk) {
|
|
|
103
109
|
}
|
|
104
110
|
// 切成功后重发原命令
|
|
105
111
|
console.log("→ 重新执行原命令");
|
|
106
|
-
result = await run(cmd, timeoutMs);
|
|
112
|
+
result = await run(cmd, timeoutMs, opts);
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
return result;
|
|
@@ -178,20 +184,26 @@ async function runWithArthasGuard(cmd, timeoutMs = 10000, ask = defaultAsk) {
|
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
// --- CLI ---
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
// 用法:node client-example.mjs [--b64] "<cmd>" [timeoutMs]
|
|
188
|
+
// --b64 命令经 base64 通道下发(多层引号场景防剥离,kubectl exec 刚需)
|
|
189
|
+
const cliArgs = process.argv.slice(2).filter(a => a !== "--b64");
|
|
190
|
+
const useB64 = process.argv.slice(2).includes("--b64");
|
|
191
|
+
const cmd = cliArgs[0] || "uname -a";
|
|
192
|
+
const timeoutMs = Number(cliArgs[1] || 10000);
|
|
193
|
+
const runOpts = { b64: useB64 };
|
|
194
|
+
|
|
195
|
+
console.log(`→ run${useB64 ? " [b64]" : ""}: ${cmd}`);
|
|
185
196
|
// 自动判断用哪种 wrapper:Arthas 命令走 guard,其他走 sudo 重试
|
|
186
197
|
const { isArthasCommand } = await import("./arthas-guard.js");
|
|
187
198
|
const result = isArthasCommand(cmd)
|
|
188
199
|
? await runWithArthasGuard(cmd, timeoutMs)
|
|
189
|
-
: await runWithSudoRetry(cmd, timeoutMs);
|
|
200
|
+
: await runWithSudoRetry(cmd, timeoutMs, undefined, runOpts);
|
|
190
201
|
if (result.ok) {
|
|
191
202
|
console.log(`✓ ok (${result.elapsedMs}ms)`);
|
|
192
203
|
console.log(result.output);
|
|
193
204
|
} else {
|
|
194
205
|
console.error(`✗ failed: ${result.error || "unknown"}`);
|
|
206
|
+
if (result.message) console.error(result.message);
|
|
195
207
|
if (result.output) console.log("--- partial output ---\n" + result.output);
|
|
196
208
|
process.exit(1);
|
|
197
209
|
}
|
package/files/proxy/server.js
CHANGED
|
@@ -70,11 +70,42 @@ function genReqId() {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Agent 发来的 run 请求
|
|
73
|
+
// 支持两种命令下发方式:
|
|
74
|
+
// { cmd: "..." } —— 明文命令(原有)
|
|
75
|
+
// { cmdB64: "<base64>" } —— base64 编码命令(多层引号场景的安全传参通道)
|
|
76
|
+
// 代理解码后包装成 `echo <b64> | base64 -d | sh` 下发:
|
|
77
|
+
// base64 字符集(A-Za-z0-9+/=)不含引号/空格/元字符,四层引号嵌套
|
|
78
|
+
// (Bash→mjs→SSH→kubectl exec→sh -c)下也不会被任何一层剥离篡改。
|
|
73
79
|
function handleRun(ws, msg) {
|
|
74
80
|
const reqId = msg.reqId || genReqId();
|
|
75
|
-
const cmd = (msg.cmd || "").toString();
|
|
76
81
|
const timeoutMs = Number(msg.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
77
82
|
|
|
83
|
+
let cmd;
|
|
84
|
+
let wrapped = null; // 实际注入终端的完整文本(base64 通道时为 wrapper 命令)
|
|
85
|
+
|
|
86
|
+
if (msg.cmdB64) {
|
|
87
|
+
// base64 通道:解码 → 校验 → 包装
|
|
88
|
+
let decoded;
|
|
89
|
+
try {
|
|
90
|
+
decoded = Buffer.from(String(msg.cmdB64), "base64").toString("utf8");
|
|
91
|
+
} catch {
|
|
92
|
+
ws.send(JSON.stringify({ type: "result", reqId, ok: false, error: "invalid cmdB64" }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!decoded.trim()) {
|
|
96
|
+
ws.send(JSON.stringify({ type: "result", reqId, ok: false, error: "empty cmd" }));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (decoded.length > 16 * 1024) {
|
|
100
|
+
ws.send(JSON.stringify({ type: "result", reqId, ok: false, error: "cmd too long (max 16KB after decode)" }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
cmd = decoded;
|
|
104
|
+
wrapped = `echo ${Buffer.from(cmd, "utf8").toString("base64")} | base64 -d | sh`;
|
|
105
|
+
} else {
|
|
106
|
+
cmd = (msg.cmd || "").toString();
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
if (!cmd) {
|
|
79
110
|
ws.send(JSON.stringify({ type: "result", reqId, ok: false, error: "empty cmd" }));
|
|
80
111
|
return;
|
|
@@ -109,7 +140,7 @@ function handleRun(ws, msg) {
|
|
|
109
140
|
}
|
|
110
141
|
}
|
|
111
142
|
|
|
112
|
-
const job = { ws, reqId, cmd: finalCmd, timeoutMs, sentAt: Date.now() };
|
|
143
|
+
const job = { ws, reqId, cmd: finalCmd, wrapped, timeoutMs, sentAt: Date.now() };
|
|
113
144
|
queue.push(job);
|
|
114
145
|
maybeRunNext();
|
|
115
146
|
}
|
|
@@ -129,12 +160,14 @@ function maybeRunNext() {
|
|
|
129
160
|
// 关键:prompt 是服务端 shell 在命令完成后输出的,不受 kitty 输入重绘影响。
|
|
130
161
|
//
|
|
131
162
|
// 流程:
|
|
132
|
-
// 1. 发 cmd + \r
|
|
163
|
+
// 1. 发 cmd + \r(base64 通道时发 `echo <b64> | base64 -d | sh` + \r)
|
|
133
164
|
// 2. 在 ws-recv 流里累积,找 prompt 正则匹配(]\s*[#$]\s*$ 在行尾)
|
|
134
165
|
// 3. 第一次匹配到 prompt:说明之前的 buffer 含"上一条命令的尾部 prompt + cmd 回显",
|
|
135
166
|
// 从这个 prompt 之后开始才是本命令的输出区域
|
|
136
167
|
// 4. 第二次匹配到 prompt:命令执行完毕,两个 prompt 之间就是输出
|
|
137
|
-
|
|
168
|
+
// 注:wrapped 优先用 handleRun 传入的 base64 wrapper(此时终端回显的是 wrapper
|
|
169
|
+
// 命令而非原始 cmd,回显清理必须按 wrapper 比对才能删掉)。
|
|
170
|
+
const wrapped = `${job.wrapped != null ? job.wrapped : job.cmd}\r`;
|
|
138
171
|
|
|
139
172
|
// 状态机:
|
|
140
173
|
// phase 0 (wait_prompt_1) : 等 prompt 第 1 次出现(命令回显前的 prompt)
|
|
@@ -144,6 +177,7 @@ function maybeRunNext() {
|
|
|
144
177
|
if (pending.has(job.reqId)) {
|
|
145
178
|
clearTimeout(entry.timer);
|
|
146
179
|
if (entry.weakTimer != null) { clearTimeout(entry.weakTimer); entry.weakTimer = null; }
|
|
180
|
+
if (entry.ps2Timer != null) { clearTimeout(entry.ps2Timer); entry.ps2Timer = null; }
|
|
147
181
|
pending.delete(job.reqId);
|
|
148
182
|
try {
|
|
149
183
|
job.ws.send(JSON.stringify({ type: "result", reqId: job.reqId, ...result }));
|
|
@@ -156,6 +190,8 @@ function maybeRunNext() {
|
|
|
156
190
|
phase: 0,
|
|
157
191
|
buffer: "",
|
|
158
192
|
cmd: job.cmd,
|
|
193
|
+
// 回显清理用的比对串:base64 通道下终端实际回显的是 wrapper 命令
|
|
194
|
+
echoCmd: job.wrapped != null ? job.wrapped : job.cmd,
|
|
159
195
|
promptCount: 0
|
|
160
196
|
};
|
|
161
197
|
pending.set(job.reqId, entry);
|
|
@@ -168,7 +204,7 @@ function maybeRunNext() {
|
|
|
168
204
|
entry.resolve({
|
|
169
205
|
ok: false,
|
|
170
206
|
error: "timeout",
|
|
171
|
-
output:
|
|
207
|
+
output: finalizeOutput(entry),
|
|
172
208
|
elapsedMs: Date.now() - job.sentAt
|
|
173
209
|
});
|
|
174
210
|
}, job.timeoutMs);
|
|
@@ -362,6 +398,40 @@ function handleWsRecv(payload) {
|
|
|
362
398
|
continue; // 已 resolve,跳过后续 prompt 检测
|
|
363
399
|
}
|
|
364
400
|
|
|
401
|
+
// ====== PS2 续行快速失败(问题 3,必须在 prompt 匹配之前)======
|
|
402
|
+
// 多层引号在某层被剥离/篡改后,远端 shell 因引号未闭合进入续行等待,
|
|
403
|
+
// 终端显示行首孤立的 >(PS2 提示符)。此时 prompt 锚点永远不会命中 →
|
|
404
|
+
// 死等超时。检测到孤立 > 且 400ms 无新数据时立即失败并 Ctrl+C 退出续行
|
|
405
|
+
// (终端回到正常 prompt,可继续执行后续命令),返回明确的错误类型。
|
|
406
|
+
// 顺序关键:unknown 类型的宽松兜底正则含裸 >,PS2 的 > 若先落到那里
|
|
407
|
+
// 会被误判成"命令完成"(实测:ok + 空输出),所以 PS2 必须先检查。
|
|
408
|
+
if (entry.phase === 0 && /^\s*>$/.test(lastNonEmptyLine(entry.buffer))) {
|
|
409
|
+
if (entry.ps2Timer == null) {
|
|
410
|
+
const ps2SnapLen = entry.buffer.length;
|
|
411
|
+
entry.ps2Timer = setTimeout(() => {
|
|
412
|
+
entry.ps2Timer = null;
|
|
413
|
+
if (!pending.has(reqId) || entry.buffer.length !== ps2SnapLen) return;
|
|
414
|
+
if (!/^\s*>$/.test(lastNonEmptyLine(entry.buffer))) return;
|
|
415
|
+
console.warn(TAG, `[${reqId}] PS2 续行命中(未闭合引号),快速失败 + Ctrl+C 退出续行`);
|
|
416
|
+
sendCtrlC();
|
|
417
|
+
entry.resolve({
|
|
418
|
+
ok: false,
|
|
419
|
+
error: "unterminated-quote",
|
|
420
|
+
message: "命令疑似包含未闭合的引号/括号,远端 shell 进入续行等待(PS2 >)。已自动 Ctrl+C 退出续行。多层引号场景请改用 base64 通道下发命令。",
|
|
421
|
+
suggest: "用 client 的 --b64 参数(或 run 帧的 cmdB64 字段)下发,规避多层引号剥离",
|
|
422
|
+
output: finalizeOutput(entry),
|
|
423
|
+
elapsedMs: Date.now() - entry.sentAt
|
|
424
|
+
});
|
|
425
|
+
}, 400);
|
|
426
|
+
}
|
|
427
|
+
// PS2 等待期间不做 prompt 匹配(下一帧数据到达会重新进入本循环)
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (entry.ps2Timer != null) {
|
|
431
|
+
clearTimeout(entry.ps2Timer);
|
|
432
|
+
entry.ps2Timer = null;
|
|
433
|
+
}
|
|
434
|
+
|
|
365
435
|
// 检查清理后 buffer 的尾部是否以 prompt 结尾
|
|
366
436
|
// 用按终端类型选出的正则(截断根治:jumpserver 不再被裸 > 误触发)
|
|
367
437
|
const tail = cleanFull.slice(-200);
|
|
@@ -375,8 +445,8 @@ function handleWsRecv(payload) {
|
|
|
375
445
|
// 早已流过(attach 之前)。我们注入 cmd 后,第一个出现的 prompt 就是
|
|
376
446
|
// 命令完成后的 prompt。所以只需等 1 个 prompt。
|
|
377
447
|
// buffer 里 = kitty 重绘碎片 + cmd 回显 + 真实输出 + 最终 prompt
|
|
378
|
-
// 切掉末尾 prompt
|
|
379
|
-
const output =
|
|
448
|
+
// 切掉末尾 prompt,前面的内容交给清理器(finalizeOutput 含空兜底)
|
|
449
|
+
const output = finalizeOutput(entry);
|
|
380
450
|
entry.resolve({
|
|
381
451
|
ok: true,
|
|
382
452
|
output,
|
|
@@ -403,7 +473,7 @@ function handleWsRecv(payload) {
|
|
|
403
473
|
entry.lastScanLen = entry.buffer.length;
|
|
404
474
|
entry.resolve({
|
|
405
475
|
ok: true,
|
|
406
|
-
output:
|
|
476
|
+
output: finalizeOutput(entry),
|
|
407
477
|
elapsedMs: Date.now() - entry.sentAt
|
|
408
478
|
});
|
|
409
479
|
}
|
|
@@ -446,11 +516,13 @@ function probeLog(data, opcode) {
|
|
|
446
516
|
|
|
447
517
|
// ===================== 输出清理 =====================
|
|
448
518
|
// prompt 锚点方案:buffer = kitty 重绘碎片 + cmd 回显 + 真实输出 + 最终 prompt
|
|
449
|
-
//
|
|
519
|
+
// 清理策略(收紧:只删确定属于终端噪音的内容,其余原样保留):
|
|
450
520
|
// 1. 去 ANSI(颜色、OSC 标题)
|
|
451
|
-
// 2.
|
|
452
|
-
//
|
|
453
|
-
//
|
|
521
|
+
// 2. 控制字符替换为空格(NUL/x00 等,/proc/*/cmdline 场景;绝不因含控制字符丢整段)
|
|
522
|
+
// 3. 删 prompt 行(要求 prompt 出现在行首附近,且 #$ 紧跟 prompt——
|
|
523
|
+
// 旧行为 "]/…任意内容…/#$" 会把含 ] 和 $ 的长 JSON 输出整行误删,即问题 1 根因)
|
|
524
|
+
// 4. 删 koko 控制消息行
|
|
525
|
+
// 5. 删命令回显行(折行感知:终端宽度折行会把长命令拆成多行,先做软换行 join 再比对)
|
|
454
526
|
function cleanOutput(text, cmd) {
|
|
455
527
|
if (!text) return "";
|
|
456
528
|
let out = text
|
|
@@ -458,18 +530,21 @@ function cleanOutput(text, cmd) {
|
|
|
458
530
|
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
459
531
|
// ANSI OSC 序列:ESC ] ... BEL 或 ESC \(标题设置等)
|
|
460
532
|
.replace(/\x1b\][^\x07\x1b]*(\x07|\x1b\\)/g, "")
|
|
533
|
+
// 控制字符 → 空格:NUL 等 C0 控制符(保留 \t\n\r,它们在下一步处理)。
|
|
534
|
+
// /proc/*/cmdline、environ 都是 NUL 分隔,替换而非丢弃(问题 2)
|
|
535
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, " ")
|
|
461
536
|
// \r\n 和孤立 \r → \n
|
|
462
537
|
.replace(/\r\n?/g, "\n");
|
|
463
538
|
|
|
464
|
-
// 删除 prompt
|
|
465
|
-
// -
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// - Arthas
|
|
539
|
+
// 删除 prompt 行(命令回显行 = prompt + 命令文本,可能粘有 kitty 重绘碎片):
|
|
540
|
+
// - 红帽系:[user@host /cwd]# —— 要求 ] 后紧跟 #$(不再允许中间隔任意内容)
|
|
541
|
+
// - Ubuntu/Debian:user@host:~/path$ —— 要求出现在行首附近(≤40 字符前缀内,
|
|
542
|
+
// 覆盖 kitty 碎片),且 prompt 段内无空格
|
|
543
|
+
// - Arthas:行尾 > 且行短(≤60 字符)
|
|
544
|
+
// 前缀窗口 40 字符:正常 prompt 行 prompt 就在行首;碎片通常几到几十字符。
|
|
469
545
|
out = out.split("\n").filter(line => {
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
if (/[\w.-]+@[\w.-]+:\S*[$#]\s*$/.test(line)) return false;
|
|
546
|
+
if (/^.{0,40}\[[^\]\n]+@[^\]\n]+\]\s*[#$]/.test(line)) return false;
|
|
547
|
+
if (/^.{0,40}[\w.-]+@[\w.-]+:[^\s]*[$#]/.test(line)) return false;
|
|
473
548
|
// Arthas / 通用 REPL prompt:行尾 > (允许前面有 arthas@xxx 等前缀)
|
|
474
549
|
if (/>\s*$/.test(line) && line.trim().length <= 60) return false;
|
|
475
550
|
return true;
|
|
@@ -483,14 +558,33 @@ function cleanOutput(text, cmd) {
|
|
|
483
558
|
return true;
|
|
484
559
|
}).join("\n");
|
|
485
560
|
|
|
486
|
-
// 删除命令回显行:整行(去空白后)等于 cmd
|
|
487
|
-
//
|
|
488
|
-
|
|
561
|
+
// 删除命令回显行:整行(去空白后)等于 cmd 的行。
|
|
562
|
+
// 折行感知(问题 4):长命令在终端宽度处折行会拆开 token(--com\nmand=),
|
|
563
|
+
// 单行比对失败 → 残留回显碎片。这里允许把连续 1-4 行 join 后再与 cmd 比对,
|
|
564
|
+
// 匹配则整组删除。只做"合并后完全相等"的精确匹配,不做子串匹配(防误删真实输出)。
|
|
565
|
+
if (cmd && cmd.trim()) {
|
|
489
566
|
const cmdNoWs = cmd.replace(/\s+/g, "");
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
567
|
+
const lines = out.split("\n");
|
|
568
|
+
const kept = [];
|
|
569
|
+
let i = 0;
|
|
570
|
+
while (i < lines.length) {
|
|
571
|
+
let matched = false;
|
|
572
|
+
let joinedNoWs = "";
|
|
573
|
+
for (let k = 0; i + k < lines.length; k++) {
|
|
574
|
+
joinedNoWs += lines[i + k].replace(/\s+/g, "");
|
|
575
|
+
if (joinedNoWs.length > cmdNoWs.length) break;
|
|
576
|
+
if (joinedNoWs === cmdNoWs) {
|
|
577
|
+
i += k + 1;
|
|
578
|
+
matched = true;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (!matched) {
|
|
583
|
+
kept.push(lines[i]);
|
|
584
|
+
i++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
out = kept.join("\n");
|
|
494
588
|
}
|
|
495
589
|
|
|
496
590
|
return out
|
|
@@ -498,6 +592,29 @@ function cleanOutput(text, cmd) {
|
|
|
498
592
|
.trim();
|
|
499
593
|
}
|
|
500
594
|
|
|
595
|
+
// ===================== 输出兜底(问题 1 的安全网)=====================
|
|
596
|
+
// 清理后为空、但原始 WS 帧里有实际内容时,绝不能返回 ok:true + 空输出——
|
|
597
|
+
// 那会让 Agent 误判"命令无输出/文件为空"。这里返回去 ANSI 的原始内容
|
|
598
|
+
// (截断到 4KB)+ raw 字节数提示,把"被过滤"的事实显式暴露给调用方。
|
|
599
|
+
const RAW_FALLBACK_THRESHOLD = 512; // 原始内容低于此值视为真无输出
|
|
600
|
+
const RAW_FALLBACK_LIMIT = 4000; // 兜底输出截断长度
|
|
601
|
+
|
|
602
|
+
function finalizeOutput(entry) {
|
|
603
|
+
const cleaned = cleanOutput(entry.buffer, entry.echoCmd != null ? entry.echoCmd : entry.cmd);
|
|
604
|
+
if (cleaned.length > 0) return cleaned;
|
|
605
|
+
|
|
606
|
+
const raw = stripAnsi(entry.buffer)
|
|
607
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, " ")
|
|
608
|
+
.replace(/\r\n?/g, "\n")
|
|
609
|
+
.trim();
|
|
610
|
+
if (raw.length < RAW_FALLBACK_THRESHOLD) return "";
|
|
611
|
+
|
|
612
|
+
const note = `[warning] output filtered to empty by cleaner; raw ${raw.length} chars shown (truncated)]`;
|
|
613
|
+
return raw.length > RAW_FALLBACK_LIMIT
|
|
614
|
+
? raw.slice(0, RAW_FALLBACK_LIMIT) + "\n" + note
|
|
615
|
+
: raw + "\n" + note;
|
|
616
|
+
}
|
|
617
|
+
|
|
501
618
|
// ===================== WebSocket 服务 =====================
|
|
502
619
|
const wss = new WebSocketServer({ host: HOST, port: PORT });
|
|
503
620
|
|
package/files/skill/SKILL.md
CHANGED
|
@@ -120,6 +120,10 @@ node client-example.mjs "uname -a"
|
|
|
120
120
|
node client-example.mjs "df -h" 15000 # 第二参数是超时 ms
|
|
121
121
|
node client-example.mjs "ps aux | grep java" 20000
|
|
122
122
|
|
|
123
|
+
# 多层引号场景(kubectl exec / 嵌套引号):加 --b64 走 base64 通道,
|
|
124
|
+
# 命令经 base64 编码下发(echo <b64> | base64 -d | sh),任何一层都不会剥离引号
|
|
125
|
+
node client-example.mjs --b64 "kubectl exec -n ns pod -- sh -c 'ps aux | grep java'"
|
|
126
|
+
|
|
123
127
|
# Arthas 场景
|
|
124
128
|
node client-example.mjs "help"
|
|
125
129
|
node client-example.mjs "thread" # 查看线程概况
|
|
@@ -259,6 +263,8 @@ Agent → 代理:
|
|
|
259
263
|
| `error: arthas-forbidden` | Arthas 高风险命令(retransform/profiler/stop/reset)被禁用 | 告知用户去浏览器 Arthas 终端手动执行,不要重试或绕过 |
|
|
260
264
|
| `error: arthas-needs-limit` | 中风险命令(trace/watch/stack/monitor)缺 -n/#cost(严格模式) | 按 suggest 补参数重发,或加 `-n 1` |
|
|
261
265
|
| `error: arthas-quota-exceeded` | 中风险命令会话内超限(默认 20 次) | 告知用户已达上限,不要重试;如需继续重启代理 |
|
|
266
|
+
| `error: unterminated-quote` | 命令含未闭合引号,远端 shell 卡在 PS2 续行(已自动 Ctrl+C 退出) | 检查引号配对;多层引号场景改用 `--b64` 通道重发 |
|
|
267
|
+
| 输出末尾带 `[warning] output filtered to empty...` | 清理器把输出过滤为空,兜底返回了原始内容(截断) | 输出可用但含终端噪音;原命令输出被判定为 prompt 噪音时可调整命令(如拆行) |
|
|
262
268
|
| `error: timeout` + output 含 `[sudo] password` | 旧版代理未实现 sudo 检测 | 升级 proxy/server.js;临时用绝对路径 `/bin/cat` 绕过 |
|
|
263
269
|
| `error: timeout` + output 为空 | 命令是交互式/持续刷新(vim/dashboard/monitor) | 改用非交互等价命令,或 Arthas 用一次性命令(thread/jad) |
|
|
264
270
|
| 命令发出去但 Arthas/JumpServer 没反应 | content script 没识别到 xterm | 让用户 F5 刷新终端页,或在 popup 点"捕捉 xterm" |
|
|
@@ -27,9 +27,14 @@
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
- `reqId`:可选。不传则代理生成(8 位 hex)。用于配对 result。
|
|
30
|
-
- `cmd
|
|
30
|
+
- `cmd`:必填(与 `cmdB64` 二选一)。命令本身,**不要自己加哨兵/换行**——代理会自动包末尾 `\r`。
|
|
31
31
|
- JumpServer:linux 命令,如 `ps aux | grep java`
|
|
32
32
|
- Arthas:Arthas 命令,如 `jad com.foo.Bar`
|
|
33
|
+
- `cmdB64`:可选,与 `cmd` 二选一。命令的 base64 编码(UTF-8)。
|
|
34
|
+
代理解码后包装成 `echo <b64> | base64 -d | sh` 下发——base64 字符集不含
|
|
35
|
+
引号/空格/元字符,Bash→mjs→SSH→kubectl exec→sh -c 多层引号嵌套下也不会
|
|
36
|
+
被任何一层剥离篡改。kubectl exec / 嵌套引号场景**必须**用这个通道。
|
|
37
|
+
限制:解码后 ≤16KB。client 侧对应 `--b64` 参数。
|
|
33
38
|
- `timeoutMs`:可选,默认 10000。超时则返回 `ok:false, error:timeout`。
|
|
34
39
|
|
|
35
40
|
### 代理 → Agent
|
|
@@ -50,14 +55,20 @@
|
|
|
50
55
|
"type": "result",
|
|
51
56
|
"reqId": "abc123",
|
|
52
57
|
"ok": false,
|
|
53
|
-
"error": "timeout | inject failed | extension disconnected | sudo-required | arthas-forbidden | arthas-needs-limit | arthas-quota-exceeded",
|
|
58
|
+
"error": "timeout | inject failed | extension disconnected | sudo-required | arthas-forbidden | arthas-needs-limit | arthas-quota-exceeded | unterminated-quote",
|
|
54
59
|
"output": "部分输出(可能为空)",
|
|
55
60
|
"suggest": "更安全的替代命令(部分错误才有)",
|
|
56
61
|
"message": "给用户看的说明文字(部分错误才有)",
|
|
57
62
|
"elapsedMs": 10000
|
|
58
63
|
}
|
|
59
64
|
```
|
|
60
|
-
- `output`:prompt 锚点出现前的所有 recv 帧拼接,已去 ANSI、`\r\n`→`\n
|
|
65
|
+
- `output`:prompt 锚点出现前的所有 recv 帧拼接,已去 ANSI、`\r\n`→`\n`、
|
|
66
|
+
控制字符(NUL 等)替换为空格、删 prompt 行和命令回显行(折行感知)、首尾 trim。
|
|
67
|
+
**空输出兜底**:若清理后为空但原始帧有内容(≥512 字符),返回截断的原始内容
|
|
68
|
+
并附 `[warning] output filtered to empty...` 提示——绝不返回 ok:true + 空输出。
|
|
69
|
+
- `unterminated-quote`:命令含未闭合引号,远端 shell 卡在 PS2 续行(`>` 提示)。
|
|
70
|
+
代理检测到后 ~400ms 快速失败并自动 Ctrl+C 退出续行(终端可继续用),
|
|
71
|
+
`suggest` 会指向 base64 通道。
|
|
61
72
|
- `error` 枚举:
|
|
62
73
|
- `timeout` — 命令超时(交互式/持续命令会触发)
|
|
63
74
|
- `inject failed` — content script 注入失败(xterm 没捕捉到)
|
package/package.json
CHANGED