@xiaoqiong0v0/opencode-ssh-tool 0.6.14 → 0.6.16
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/dist/agent.d.ts +4 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +17 -2
- package/dist/agent.js.map +1 -1
- package/dist/base-session.d.ts +68 -32
- package/dist/base-session.d.ts.map +1 -1
- package/dist/base-session.js +253 -130
- package/dist/base-session.js.map +1 -1
- package/dist/constants.d.ts +14 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -1
- package/dist/constants.js.map +1 -1
- package/dist/i18n.d.ts +36 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +33 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/dist/local-session.d.ts +3 -1
- package/dist/local-session.d.ts.map +1 -1
- package/dist/local-session.js +82 -4
- package/dist/local-session.js.map +1 -1
- package/dist/log.d.ts +8 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +28 -1
- package/dist/log.js.map +1 -1
- package/dist/parse-check.d.ts +20 -0
- package/dist/parse-check.d.ts.map +1 -0
- package/dist/parse-check.js +505 -0
- package/dist/parse-check.js.map +1 -0
- package/dist/precheck.d.ts +32 -0
- package/dist/precheck.d.ts.map +1 -0
- package/dist/precheck.js +105 -0
- package/dist/precheck.js.map +1 -0
- package/dist/server-entry.js +1 -1
- package/dist/server-entry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -35
- package/dist/server.js.map +1 -1
- package/dist/session-store.d.ts +56 -2
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +100 -3
- package/dist/session-store.js.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/shell-adapter.d.ts +14 -0
- package/dist/shell-adapter.d.ts.map +1 -1
- package/dist/shell-adapter.js +58 -10
- package/dist/shell-adapter.js.map +1 -1
- package/dist/utils.d.ts +33 -20
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +97 -43
- package/dist/utils.js.map +1 -1
- package/dist/web/app.css +4 -0
- package/dist/web/app.js +10 -10
- package/dist/web/index.html +1 -0
- package/package.json +1 -1
package/dist/base-session.js
CHANGED
|
@@ -1,48 +1,98 @@
|
|
|
1
1
|
// BaseSession:SSH(SshSession) 与本地(LocalSession) 终端的公共基类
|
|
2
|
-
//
|
|
2
|
+
// 统一完成标记法:每条命令后追加一条标记命令,完成后输出 <SSH_DONE:nonce:seq:退出码>(不依赖 PROMPT_COMMAND/PS1 钩子)
|
|
3
3
|
// 子类仅需实现:连接建立(connect)、数据写入(_write)、传输层清理(_closeTransport)、状态字段(getStatus)
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { ANIMATION_WINDOW_MS, MAX_OUTPUT_LEN, RAW_LOG_MAX, DONE_TAG, SYNTAX_QUIET_MS, SYNTAX_MAX_WAIT_MS, FALLBACK_WATCH_INTERVAL_MS, INTERRUPT_PROBE_INTERVAL_DEFAULT_SEC, } from "./constants.js";
|
|
6
|
-
import log, { debug, describeSend, isDebugEnabled, tailLines, tailSummary } from "./log.js";
|
|
5
|
+
import { ANIMATION_WINDOW_MS, MAX_OUTPUT_LEN, RAW_LOG_MAX, DONE_TAG, PROBE_TAG, SYNTAX_QUIET_MS, SYNTAX_MAX_WAIT_MS, FALLBACK_WATCH_INTERVAL_MS, INTERRUPT_PROBE_INTERVAL_DEFAULT_SEC, } from "./constants.js";
|
|
6
|
+
import log, { debug, describeSend, infoDedup, isDebugEnabled, tailLines, tailSummary } from "./log.js";
|
|
7
7
|
import { decodeTerminalEscapes } from "./escapes.js";
|
|
8
8
|
import { SessionHistory } from "./history.js";
|
|
9
|
-
import { toModelText, extractOutputStart,
|
|
10
|
-
import { detectLastDoneMarker, stripMarkers, detectInterrupt, detectSyntaxError, adapters } from "./shell-adapter.js";
|
|
9
|
+
import { toModelText, extractOutputStart, removeProbeEchoLines, ECHO_REGEX_MAX_CMD_CHARS, echoRegexSourceLength } from "./utils.js";
|
|
10
|
+
import { detectLastDoneMarker, detectLastProbeMarker, stripMarkers, detectInterrupt, detectSyntaxError, adapters } from "./shell-adapter.js";
|
|
11
11
|
import { tr } from "./i18n.js";
|
|
12
12
|
/** buffer 最大长度(未消费输出超限时截断头部,防内存膨胀) */
|
|
13
13
|
const MAX_BUFFER_LEN = 2 * 1024 * 1024;
|
|
14
14
|
/** Shell 探测等待超时 */
|
|
15
15
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
16
|
+
/** 探针回显 seq 保留上限:只保留最近 N 条,防长会话 Set 无界增长与剔除成本 O(N×行) */
|
|
17
|
+
const PROBE_SEQ_KEEP = 8;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* 剥离命令尾部注释:只在**最后一行**、`#` 位于引号外且处于词首时视为注释。
|
|
20
|
+
* 扫描整条命令以维护跨行引号状态(bash/pwsh 字符串都可跨行),避免 `"line2 # x"` 被误截断。
|
|
21
|
+
* - pwsh:反引号是转义符;单引号内 `''` 表示字面单引号。
|
|
22
|
+
* - bash/zsh:反斜杠转义;反引号是命令替换,内部 `#` 不剥(保守处理)。
|
|
23
|
+
* - cmd:不支持 `#` 注释,跳过。
|
|
24
|
+
* @param s 完整命令文本
|
|
25
|
+
* @param shell 适配器名(bash/zsh/pwsh/cmd;空串按 POSIX 处理)
|
|
20
26
|
* @returns 剥离尾注释(若有)后的文本
|
|
21
27
|
*/
|
|
22
|
-
function stripTrailingComment(s) {
|
|
28
|
+
function stripTrailingComment(s, shell) {
|
|
29
|
+
if (shell === "cmd")
|
|
30
|
+
return s.trimEnd();
|
|
31
|
+
const pwsh = shell === "pwsh" || shell === "powershell";
|
|
32
|
+
const lastLineStart = s.lastIndexOf("\n") + 1;
|
|
23
33
|
let inSingle = false;
|
|
24
34
|
let inDouble = false;
|
|
25
|
-
let
|
|
35
|
+
let inBacktick = false;
|
|
26
36
|
for (let i = 0; i < s.length; i++) {
|
|
27
37
|
const c = s[i];
|
|
28
|
-
if (
|
|
29
|
-
|
|
38
|
+
if (c === "\n")
|
|
39
|
+
continue;
|
|
40
|
+
if (inSingle) {
|
|
41
|
+
if (pwsh && c === "'" && s[i + 1] === "'") {
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === "'")
|
|
46
|
+
inSingle = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (inDouble) {
|
|
50
|
+
if (pwsh && c === "`") {
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!pwsh && c === "\\") {
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === '"')
|
|
59
|
+
inDouble = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inBacktick) {
|
|
63
|
+
if (c === "`")
|
|
64
|
+
inBacktick = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (pwsh && c === "`") {
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!pwsh && c === "\\") {
|
|
72
|
+
i++;
|
|
30
73
|
continue;
|
|
31
74
|
}
|
|
32
|
-
if (c === "
|
|
33
|
-
|
|
75
|
+
if (c === "'") {
|
|
76
|
+
inSingle = true;
|
|
34
77
|
continue;
|
|
35
78
|
}
|
|
36
|
-
if (c === "'
|
|
37
|
-
|
|
79
|
+
if (c === '"') {
|
|
80
|
+
inDouble = true;
|
|
38
81
|
continue;
|
|
39
82
|
}
|
|
40
|
-
if (c ===
|
|
41
|
-
|
|
83
|
+
if (!pwsh && c === "`") {
|
|
84
|
+
inBacktick = true;
|
|
42
85
|
continue;
|
|
43
86
|
}
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
// 注释起始:行首/空白/语句分隔符/括号之后(`;#`、`&#`、`)#` 都能正确识别)
|
|
88
|
+
if (c === "#" && (i === 0 || /[\s;&|(){}]/.test(s[i - 1]))) {
|
|
89
|
+
if (i >= lastLineStart)
|
|
90
|
+
return s.slice(0, i).trimEnd();
|
|
91
|
+
// 前面的行上的注释:跳过本行剩余内容,继续扫描后续行
|
|
92
|
+
const nl = s.indexOf("\n", i);
|
|
93
|
+
if (nl < 0)
|
|
94
|
+
break;
|
|
95
|
+
i = nl;
|
|
46
96
|
}
|
|
47
97
|
}
|
|
48
98
|
return s.trimEnd();
|
|
@@ -52,16 +102,17 @@ function stripTrailingComment(s) {
|
|
|
52
102
|
* - 尾注释 `# xxx` 把 marker 整行吞掉 → 检测不到完成
|
|
53
103
|
* - 尾运算符 `&`、`;`、`&&`、`||` 与拼接的 `;` 形成 `&;`/`;;` 等语法错误
|
|
54
104
|
* @param command 原始命令
|
|
105
|
+
* @param shell 适配器名(用于正确的引号/转义语义;空串按 POSIX 处理)
|
|
55
106
|
* @returns 可安全拼装 marker 的命令体
|
|
56
107
|
*/
|
|
57
|
-
function stripCommandTail(command) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return (head +
|
|
108
|
+
export function stripCommandTail(command, shell = "") {
|
|
109
|
+
const withoutComment = stripTrailingComment(command, shell);
|
|
110
|
+
// 仅处理最后一行:多行命令(如 heredoc 正文)中间的 `#`/运算符是内容,不能在这里截断
|
|
111
|
+
const nl = withoutComment.lastIndexOf("\n");
|
|
112
|
+
const head = nl >= 0 ? withoutComment.slice(0, nl + 1) : "";
|
|
113
|
+
const last = nl >= 0 ? withoutComment.slice(nl + 1) : withoutComment;
|
|
114
|
+
const cleaned = last.replace(/(?:&&|\|\||[;&|])[\s]*$/, "");
|
|
115
|
+
return (head + cleaned).trimEnd();
|
|
65
116
|
}
|
|
66
117
|
/**
|
|
67
118
|
* 诊断辅助:转义控制字符,使日志单行可读(\r\n\t 转义、ESC 写成 \x1b)。
|
|
@@ -150,11 +201,6 @@ export class BaseSession {
|
|
|
150
201
|
* 只认自己 nonce:重放标记不再冒充本会话标记。
|
|
151
202
|
*/
|
|
152
203
|
_nonce = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
153
|
-
/**
|
|
154
|
-
* 命令发起前缓冲的末行(旧提示符行):语法错误收尾时 shell 会重印同形提示符,
|
|
155
|
-
* 据此把它从输出尾部剥掉(exec 的输出窗口自命令回显起,旧提示符在窗口之外,需单独记录)。
|
|
156
|
-
*/
|
|
157
|
-
_promptRef = "";
|
|
158
204
|
/** 下次捕获窗口起点(相对当前 buffer):前一条命令结束后,标记/残留从此处开始 */
|
|
159
205
|
_cursor = 0;
|
|
160
206
|
/** WS 实时流已发送位置(相对 buffer 偏移);null = 尚未定位命令回显结束点 */
|
|
@@ -163,6 +209,12 @@ export class BaseSession {
|
|
|
163
209
|
_adapter = null;
|
|
164
210
|
/** 多行命令写入 PTY 的行分隔符:Unix PTY 用 \n;Windows ConPTY(本地会话)用 \r(裸 \n 不提交行) */
|
|
165
211
|
_lineSep = "\n";
|
|
212
|
+
/**
|
|
213
|
+
* 当前会话终端是否为 Windows ConPTY:决定提交分隔符。
|
|
214
|
+
* 本地会话按宿主平台设置;SSH 会话在 shell 探测后按 pwsh 的 `$PSHOME` 路径判断
|
|
215
|
+
*(Windows `C:\...` → true,Unix `/...` → false),不能只看 adapter 类型。
|
|
216
|
+
*/
|
|
217
|
+
_windowsPTY = process.platform === "win32";
|
|
166
218
|
_closed = false;
|
|
167
219
|
/** 当前运行命令的输入时刻(history 记录展示命令发起时间用) */
|
|
168
220
|
_runningStartTs = 0;
|
|
@@ -262,7 +314,7 @@ export class BaseSession {
|
|
|
262
314
|
const completion = this._awaitCompletion();
|
|
263
315
|
this._write(this._composeCommand(command));
|
|
264
316
|
const outcome = await completion;
|
|
265
|
-
|
|
317
|
+
debug(`exec ${this._logTag} seq=${this._runningSeq} cmdLen=${command.length} -> ${outcome.kind}${outcome.kind === "running" ? "" : " markerPos=" + outcome.markerPos + " exitCode=" + outcome.exitCode + " duration=" + (Date.now() - startTs) + "ms"} (busy=${this._remoteBusy})`);
|
|
266
318
|
debug(`exec ${this._logTag} seq=${this._runningSeq} cmd=${tailLines(command, 3)}`);
|
|
267
319
|
switch (outcome.kind) {
|
|
268
320
|
case "done": {
|
|
@@ -351,6 +403,14 @@ export class BaseSession {
|
|
|
351
403
|
* @returns 8 位十六进制会话串
|
|
352
404
|
*/
|
|
353
405
|
get nonce() { return this._nonce; }
|
|
406
|
+
/**
|
|
407
|
+
* 当前 shell 适配器名("zsh" / "bash" / "pwsh" / "cmd"):供发送前语法预检选校验器。
|
|
408
|
+
* 只读暴露探测结果,不触发探测(未连接时为 null,调用方按"不校验"处理)。
|
|
409
|
+
* @returns 适配器名;未连接/未探测到适配器时为 null
|
|
410
|
+
*/
|
|
411
|
+
get shellName() { return this._adapter?.name ?? null; }
|
|
412
|
+
/** 是否本地会话:发送前语法预检只接本地(SSH 远程需会话内往返,另开一轮) */
|
|
413
|
+
get isLocal() { return false; }
|
|
354
414
|
getRunningOutput() {
|
|
355
415
|
if (this._runningStartPos === null || !this._connected || !this._runningCommand)
|
|
356
416
|
return "";
|
|
@@ -389,12 +449,18 @@ export class BaseSession {
|
|
|
389
449
|
* 供事后对齐状态时间线(busy 每次翻转都能对应到具体原因,不再出现"忙了却查不出为何")。
|
|
390
450
|
* @param next 目标 busy 值
|
|
391
451
|
* @param reason 翻转原因(cmdStart / cmdRunning / cmdDone / 会话关闭 / 断连补 done 等)
|
|
452
|
+
* @param quiet true 时该行降为 debug:用于"释放"与紧随其后的收尾行(如 [run] 结束)重复的场景,
|
|
453
|
+
* 避免同一事件打两条 info(busy 时间线仍可由该收尾行还原)
|
|
392
454
|
*/
|
|
393
|
-
_setBusy(next, reason) {
|
|
455
|
+
_setBusy(next, reason, quiet = false) {
|
|
394
456
|
if (this._remoteBusy === next)
|
|
395
457
|
return;
|
|
396
458
|
this._remoteBusy = next;
|
|
397
|
-
|
|
459
|
+
const line = `[busy] ${this._logTag} seq=${this._runningSeq} busy=${next} reason=${reason}`;
|
|
460
|
+
if (quiet)
|
|
461
|
+
debug(line);
|
|
462
|
+
else
|
|
463
|
+
log.info(line);
|
|
398
464
|
// 通知插件同步状态文件(busy 已更新,getStatus 读到新值);监听器异常不打断执行
|
|
399
465
|
try {
|
|
400
466
|
this._busyListener?.(next, reason);
|
|
@@ -459,10 +525,7 @@ export class BaseSession {
|
|
|
459
525
|
}
|
|
460
526
|
const raw = this._buffer.slice(this._streamPos, windowEnd);
|
|
461
527
|
this._streamPos = windowEnd;
|
|
462
|
-
|
|
463
|
-
if (this._adapter)
|
|
464
|
-
data = data.replace(this._markerCmd(this._runningSeq), "");
|
|
465
|
-
data = this._stripProbeEchoes(data);
|
|
528
|
+
const data = this._stripProbeEchoes(stripMarkers(raw));
|
|
466
529
|
return { data, done: marker.done };
|
|
467
530
|
}
|
|
468
531
|
/** 关闭终端,清理会话态(幂等) */
|
|
@@ -472,7 +535,10 @@ export class BaseSession {
|
|
|
472
535
|
this._closed = true;
|
|
473
536
|
if (this._watchTimer)
|
|
474
537
|
clearInterval(this._watchTimer);
|
|
475
|
-
this.
|
|
538
|
+
this._watchTimer = null;
|
|
539
|
+
// 先清运行上下文(含语法/中断定时器),再释放 history:
|
|
540
|
+
// 否则 close 后定时器仍可能触发写已关闭终端,并在 history 已 dispose 时 append 抛未捕获 ENOENT
|
|
541
|
+
this._clearRunningContext();
|
|
476
542
|
this._closeTransport();
|
|
477
543
|
this._connected = false;
|
|
478
544
|
this._setBusy(false, "会话关闭");
|
|
@@ -493,28 +559,39 @@ export class BaseSession {
|
|
|
493
559
|
_markerCmd(seq) {
|
|
494
560
|
return this._adapter ? this._adapter.markerCmd(this._nonce, seq) : `printf '\\n${DONE_TAG}${this._nonce}:${seq}:%s>' $?`;
|
|
495
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* 补发探针命令:与完成标记命令**同构**,仅把 tag 换成 PROBE_TAG。
|
|
564
|
+
* 复用各 adapter 的 markerCmd,避免四套 shell 各维护一份退出码表达式;
|
|
565
|
+
* 独立 tag 让「探针标记」与「原命令真实完成标记」在字节流里可严格区分。
|
|
566
|
+
* @param seq 命令序号
|
|
567
|
+
* @returns 探针标记命令文本
|
|
568
|
+
*/
|
|
569
|
+
_probeCmd(seq) {
|
|
570
|
+
const marker = this._markerCmd(seq);
|
|
571
|
+
// cmd 的 marker 用 ^< 转义,DONE_TAG 不再是连续子串;此时只替换 tag 主体
|
|
572
|
+
return marker.includes(DONE_TAG) ? marker.replace(DONE_TAG, PROBE_TAG) : marker.replace("SSH_DONE:", "SSH_PROBE:");
|
|
573
|
+
}
|
|
496
574
|
/**
|
|
497
575
|
* 组合实际写入 PTY 的命令:在规范形式(`_composeEchoText`)基础上把换行改写为行分隔符并补尾部回车。
|
|
498
|
-
*
|
|
499
|
-
* - Windows
|
|
500
|
-
* -
|
|
576
|
+
* 行分隔符按**探测到的终端平台**决定,而非按传输类型(本地/SSH):
|
|
577
|
+
* - Windows ConPTY(本机 Windows,或 SSH 到 Windows pwsh/cmd)→ `\r`;
|
|
578
|
+
* - Unix PTY(bash/zsh,含 Linux/macOS 上的 pwsh)→ `_lineSep`(通常 `\n`)。
|
|
579
|
+
* SSH 的 pwsh 平台由探测输出里的 `$PSHOME` 路径区分(`C:\...` vs `/...`)。
|
|
501
580
|
* 原因:ConPTY 下裸 `\n` 只下移不复位、不提交行 → 多行命令逆序执行并卡在 `>>` 续行,须用 `\r`;
|
|
502
|
-
*
|
|
503
|
-
* Unix PTY 下 `\n` 可正常提交行,保持不变。
|
|
581
|
+
* Unix PTY 下 `\n` 可正常提交行。
|
|
504
582
|
* @param command 原始命令(history 存干净版本)
|
|
505
|
-
* @returns 实际写入 PTY
|
|
583
|
+
* @returns 实际写入 PTY 的文本(尾部带提交分隔符,行分隔已按平台规范化)
|
|
506
584
|
*/
|
|
507
585
|
_composeCommand(command) {
|
|
508
|
-
//
|
|
509
|
-
return this._composeEchoText(command).replace(/\n/g, this._ptyLineSep()) +
|
|
586
|
+
// 行分隔符按终端平台判定(见 _ptyLineSep)
|
|
587
|
+
return this._composeEchoText(command).replace(/\n/g, this._ptyLineSep()) + this._ptyLineSep();
|
|
510
588
|
}
|
|
511
589
|
/**
|
|
512
|
-
*
|
|
513
|
-
* (裸 \n 不提交行);其余沿用会话 `_lineSep`(Unix PTY 为 \n,本地会话为 \r)。
|
|
590
|
+
* 当前终端的 PTY **提交分隔符**:Windows ConPTY → `\r`;Unix PTY → `_lineSep`(通常 `\n`)。
|
|
514
591
|
* @returns 提交分隔符字符串
|
|
515
592
|
*/
|
|
516
593
|
_ptyLineSep() {
|
|
517
|
-
return this.
|
|
594
|
+
return this._windowsPTY ? "\r" : this._lineSep;
|
|
518
595
|
}
|
|
519
596
|
/**
|
|
520
597
|
* 组合中断探针写入文本:**不加任何前缀换行**,直接写一条独立的当前 seq 完成标记命令,
|
|
@@ -529,26 +606,39 @@ export class BaseSession {
|
|
|
529
606
|
* @returns 写入 PTY 的探针文本
|
|
530
607
|
*/
|
|
531
608
|
_composeProbe(seq) {
|
|
532
|
-
return this.
|
|
609
|
+
return this._probeCmd(seq) + this._ptyLineSep();
|
|
610
|
+
}
|
|
611
|
+
/** 记录探针 seq,只保留最近 PROBE_SEQ_KEEP 条(迟到回显跨命令有限,避免 Set 无界增长) */
|
|
612
|
+
_rememberProbeSeq(seq) {
|
|
613
|
+
this._probeSeqs.add(seq);
|
|
614
|
+
while (this._probeSeqs.size > PROBE_SEQ_KEEP) {
|
|
615
|
+
const first = this._probeSeqs.values().next().value;
|
|
616
|
+
if (first === undefined)
|
|
617
|
+
break;
|
|
618
|
+
this._probeSeqs.delete(first);
|
|
619
|
+
}
|
|
533
620
|
}
|
|
534
621
|
/**
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
622
|
+
* 补发一次当前命令的探针标记(仅中断收尾使用):
|
|
623
|
+
* 写入一条独立的本 seq 标记命令,并记录 seq,使其回显迟到落入后续窗口时仍能被剔除。
|
|
624
|
+
* - 中断场景:用户显式 Ctrl+C 后前台命令可能已死、原命令的完成标记永不执行 → busy 卡死;
|
|
625
|
+
* 补写探针后 shell 若已回到提示符会读它并打印当前 seq 标记使命令收尾;若前台程序未死,
|
|
626
|
+
* 字节留在 tty 输入缓冲待 shell 拿回控制权再读(两种情况均符合预期,绝不误判完成)。
|
|
627
|
+
* 由中断周期调用:写完 \x03 时立即补发一次,之后定时器每间隔补发一次(见 _startInterruptCycle)。
|
|
628
|
+
* 幂等:同一条命令多次补发时,先到的当前 seq 标记胜出。仅在确实追踪着命令时补发(空闲补发只污染缓冲)。
|
|
629
|
+
* 注意:语法错误路径**不再补发探针**(会写进仍在运行程序的 stdin),改为安静窗口到点直接收尾。
|
|
630
|
+
* @param reason 补发原因(仅用于日志:中断探针)
|
|
542
631
|
* @returns 无
|
|
543
632
|
*/
|
|
544
|
-
|
|
545
|
-
|
|
633
|
+
_sendProbe(reason) {
|
|
634
|
+
if (!this._connected)
|
|
635
|
+
return;
|
|
546
636
|
if (this._runningStartPos === null || this._runningCommand === "")
|
|
547
637
|
return;
|
|
548
638
|
const seq = this._runningSeq;
|
|
549
|
-
this.
|
|
639
|
+
this._rememberProbeSeq(seq);
|
|
550
640
|
const probe = this._composeProbe(seq);
|
|
551
|
-
debug(
|
|
641
|
+
debug(`${reason} ${this._logTag} seq=${seq} ${JSON.stringify(probe)} (busy=${this._remoteBusy})`);
|
|
552
642
|
this._write(probe);
|
|
553
643
|
}
|
|
554
644
|
/**
|
|
@@ -570,7 +660,7 @@ export class BaseSession {
|
|
|
570
660
|
const intervalSec = this._interruptProbeIntervalMs / 1000;
|
|
571
661
|
log.info(`中断周期开始 ${this._logTag} seq=${seq},已立即补发首条探针,之后每 ${intervalSec}s 一条`);
|
|
572
662
|
// 写完 \x03 后立即补发首条探针(t0),后续 setInterval 自此刻起按间隔补发(t0+N、t0+2N…)
|
|
573
|
-
this.
|
|
663
|
+
this._sendProbe("中断探针");
|
|
574
664
|
this._interruptTimer = setInterval(() => { this._onInterruptTick(); }, this._interruptProbeIntervalMs);
|
|
575
665
|
}
|
|
576
666
|
/**
|
|
@@ -583,7 +673,7 @@ export class BaseSession {
|
|
|
583
673
|
this._stopInterruptCycle("命令已结束或会话关闭");
|
|
584
674
|
return;
|
|
585
675
|
}
|
|
586
|
-
this.
|
|
676
|
+
this._sendProbe("中断探针");
|
|
587
677
|
}
|
|
588
678
|
/**
|
|
589
679
|
* 停止中断周期并清周期状态:清定时器,日志记录结束原因(幂等,未在周期内时不做任何事)。
|
|
@@ -599,23 +689,24 @@ export class BaseSession {
|
|
|
599
689
|
log.info(`中断周期结束 ${this._logTag} seq=${this._runningSeq},原因=${reason}`);
|
|
600
690
|
}
|
|
601
691
|
/**
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
692
|
+
* 从可见输出中剔除探针留下的痕迹(探针命令的回显)。
|
|
693
|
+
* 探针命令已改为独立 PROBE tag,但其回显仍是"一条标记命令";同时保留对当前命令 DONE 标记回显的剔除。
|
|
694
|
+
* 对每个已记录探针 seq(以及当前命令自身)做 **ANSI 容忍定位**:即便 pwsh 语法高亮在 token 间插入
|
|
695
|
+
* SGR、行首带提示符与 `\x1b[?25l`,也能命中并整段剔除(见 removeProbeEchoLines)。
|
|
696
|
+
* 三处调用点(本函数、getRunningStream、readBuffer)统一收敛至此;Raw 视图保留原始字节。
|
|
606
697
|
* @param text 可见输出文本
|
|
607
698
|
* @returns 剔除探针回显后的文本
|
|
608
699
|
*/
|
|
609
700
|
_stripProbeEchoes(text) {
|
|
610
|
-
|
|
611
|
-
return text;
|
|
612
|
-
let out = text;
|
|
701
|
+
const needles = new Set();
|
|
613
702
|
for (const seq of this._probeSeqs) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
out = out.split(echo).join("");
|
|
703
|
+
needles.add(this._markerCmd(seq));
|
|
704
|
+
needles.add(this._probeCmd(seq));
|
|
617
705
|
}
|
|
618
|
-
|
|
706
|
+
// 当前命令的标记命令回显:与上面可能重复,Set 去重避免同一 needle 反复扫描
|
|
707
|
+
needles.add(this._markerCmd(this._runningSeq));
|
|
708
|
+
needles.add(this._probeCmd(this._runningSeq));
|
|
709
|
+
return removeProbeEchoLines(text, [...needles]);
|
|
619
710
|
}
|
|
620
711
|
/**
|
|
621
712
|
* 组合命令文本(规范/匹配用形式,始终以 `\n` 分隔行、无尾部回车):命令 + 完成标记。
|
|
@@ -636,8 +727,11 @@ export class BaseSession {
|
|
|
636
727
|
*/
|
|
637
728
|
_composeEchoText(command) {
|
|
638
729
|
const marker = this._markerCmd(this._runningSeq);
|
|
639
|
-
const body = stripCommandTail(command);
|
|
730
|
+
const body = stripCommandTail(command, this._adapter?.name ?? "");
|
|
640
731
|
const sep = this._adapter?.stmtSep ?? ";";
|
|
732
|
+
// cmd:marker 必须独占下一行——%errorlevel% 是整行解析期展开,同行拼接会取到旧值
|
|
733
|
+
if (this._adapter?.name === "cmd")
|
|
734
|
+
return `${body}\n${marker}`;
|
|
641
735
|
// 多行命令:尝试包组(cmd 返回 null → 回退原样拼接),使整段作为一条命令执行
|
|
642
736
|
if (body.includes("\n")) {
|
|
643
737
|
const grouped = this._adapter?.groupWrap(body);
|
|
@@ -654,7 +748,7 @@ export class BaseSession {
|
|
|
654
748
|
* 语法错误路径(syntaxMode)另作两处特殊处理:
|
|
655
749
|
* - 回显定位取**首次**匹配:pwsh 错误块的 ` 1 | <命令>` 行会重印含标记的整行命令,
|
|
656
750
|
* 末次匹配会落到该拷贝上,导致 `ParserError:`/`Line |` 等错误块前半被裁掉;
|
|
657
|
-
* -
|
|
751
|
+
* - 输出窗口右界取缓冲区末尾(不再补发语法探针),尾部最多多 1 行提示符噪音。
|
|
658
752
|
* @param raw 本次执行窗口原始字节流
|
|
659
753
|
* @param command 原始命令(历史用)
|
|
660
754
|
* @param syntaxMode true 表示语法/解析错误收尾(无完成标记,整行命令作废)
|
|
@@ -663,33 +757,29 @@ export class BaseSession {
|
|
|
663
757
|
_extractOutput(raw, command, syntaxMode = false) {
|
|
664
758
|
// 完成标记先定位:marker 自带唯一 seq,从窗口起点搜索即可(echoEnd 可能被连续输入的后序回显带偏)
|
|
665
759
|
const marker = detectLastDoneMarker(raw, 0, this._nonce, this._runningSeq);
|
|
760
|
+
// 中断探针路径没有真实完成标记,右界取 PROBE 标记结束位置,避免把探针之后的提示符污染带进输出;
|
|
761
|
+
// 语法错误路径不补发探针,boundary 退到 raw.length(尾部最多多 1 行提示符噪音)
|
|
762
|
+
const probe = marker.done ? null : detectLastProbeMarker(raw, 0, this._nonce, this._runningSeq);
|
|
763
|
+
const boundary = marker.done ? marker.pos : (probe?.done ? probe.pos : raw.length);
|
|
666
764
|
const echoText = this._composeEchoText(command);
|
|
667
765
|
const echoLen = echoText.trim().length;
|
|
668
766
|
// 超长回显:extractOutputStart 会降级为"首/尾锚点"匹配(否则模式源约 265×长度,JSC 下会抛 too large)
|
|
669
767
|
if (echoLen > ECHO_REGEX_MAX_CMD_CHARS) {
|
|
670
|
-
|
|
768
|
+
debug(`[extract] ${this._logTag} seq=${this._runningSeq} 回显定位降级为首/尾锚点: echoLen=${echoLen} regexSrcLen=${echoRegexSourceLength(echoText)} (>阈值 ${ECHO_REGEX_MAX_CMD_CHARS})`);
|
|
671
769
|
}
|
|
672
770
|
let end;
|
|
673
|
-
const extractStart = Date.now();
|
|
674
771
|
try {
|
|
675
|
-
end =
|
|
676
|
-
? extractOutputStart(raw, echoText, marker.pos)
|
|
677
|
-
: extractOutputStart(raw, echoText, raw.length, syntaxMode);
|
|
772
|
+
end = extractOutputStart(raw, echoText, boundary, syntaxMode);
|
|
678
773
|
}
|
|
679
774
|
catch (e) {
|
|
680
775
|
// 回显定位异常绝不上抛(否则会中断完成收尾):记一行后退回整窗输出
|
|
681
776
|
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 回显定位失败, 退回整窗: echoLen=${echoLen} err=${e instanceof Error ? e.message : String(e)}`);
|
|
682
777
|
end = -1;
|
|
683
778
|
}
|
|
684
|
-
// 诊断:定位结果与耗时(ret<=0 显眼标注"定位失败")
|
|
685
|
-
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 定位: inLen=${raw.length} patternLen=${echoRegexSourceLength(echoText)} ret=${end <= 0 ? `<${end}> 定位失败` : end} elapsedMs=${Date.now() - extractStart}`);
|
|
686
779
|
// 诊断:输出窗口坐标 + 是否退回整窗(startPos=回显结束偏移,endPos=窗口末尾;窗口长=endPos-startPos)
|
|
687
780
|
const winStart = end <= 0 ? 0 : end;
|
|
688
|
-
const winEnd =
|
|
689
|
-
|
|
690
|
-
// 诊断:只扫本命令窗口 [winStart, winEnd) 内的完成标记(最多列 5 个,位置为相对窗口起点的偏移)——定位"输出里混进的标记来自哪条命令"
|
|
691
|
-
const hits = scanDoneMarkers(raw.slice(winStart, winEnd));
|
|
692
|
-
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 窗口内含标记: ${hits.length === 0 ? "无" : hits.slice(0, 5).map((h) => `@${h.pos}=${h.seq ?? "?"}`).join(" ")}`);
|
|
781
|
+
const winEnd = boundary;
|
|
782
|
+
debug(`[extract] ${this._logTag} seq=${this._runningSeq} 窗口: startPos=${winStart} endPos=${winEnd} 窗口长=${winEnd - winStart} markerSeq=${this._runningSeq} 是否退回整窗=${end <= 0 ? `是(定位失败, syntaxMode=${syntaxMode})` : "否"}`);
|
|
693
783
|
// 诊断:窗口前 120 字符(仅 SSH_TOOL_DEBUG=1 时落盘)
|
|
694
784
|
debug(`[extract] ${this._logTag} seq=${this._runningSeq} 窗口前120: ${escapeCtl(raw.slice(0, 120))}`);
|
|
695
785
|
if (end <= 0) {
|
|
@@ -697,16 +787,16 @@ export class BaseSession {
|
|
|
697
787
|
// **不得退回整窗**——整窗会把重放的别会话命令行/标记当输出落盘(旧 bug:outLen=14019 全是杂物)。
|
|
698
788
|
// 改为按窗口内已有标记边界裁剪:取"本命令自己的标记之前、所有非本 seq 标记之后"的边界,
|
|
699
789
|
// 先把重放段整段切掉,再剥行首残余控制码噪音;定位仍失败也只剩边界之后的少量内容。
|
|
700
|
-
const ownEnd =
|
|
701
|
-
const
|
|
702
|
-
let fallback =
|
|
790
|
+
const ownEnd = boundary;
|
|
791
|
+
const fbBoundary = this._fallbackBoundary(raw, ownEnd);
|
|
792
|
+
let fallback = fbBoundary > 0 ? raw.slice(fbBoundary) : raw;
|
|
703
793
|
// 边界裁剪后"再试一次定位":此时窗口已剔除重放段,回显残片可能恢复成可匹配的连续文本
|
|
704
794
|
const retry = extractOutputStart(fallback, echoText, fallback.length, false);
|
|
705
795
|
if (retry > 0)
|
|
706
796
|
fallback = fallback.slice(retry);
|
|
707
797
|
else
|
|
708
798
|
fallback = this._stripLeadingControlNoise(fallback);
|
|
709
|
-
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 定位失败改用标记边界: boundary=${
|
|
799
|
+
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 定位失败改用标记边界: boundary=${fbBoundary} retry=${retry} 裁剪后长=${fallback.length} 原窗长=${raw.length}`);
|
|
710
800
|
// 中断命令再截到首个 ^C 回显,避免输出残留 ^C 与探针后 shell 重印的提示符(与旧版中断收尾语义一致)
|
|
711
801
|
if (this._interruptSent) {
|
|
712
802
|
const intr = detectInterrupt(fallback, 0);
|
|
@@ -716,10 +806,15 @@ export class BaseSession {
|
|
|
716
806
|
return this._stripProbeEchoes(stripMarkers(fallback));
|
|
717
807
|
}
|
|
718
808
|
let out;
|
|
719
|
-
if (
|
|
720
|
-
//
|
|
809
|
+
if (syntaxMode) {
|
|
810
|
+
// 语法错误:命令整行作废、本条标记未执行 → 输出 = 回显结束 → 缓冲区末尾(含整段错误块)
|
|
811
|
+
//(不补发探针;尾部最多多 1 行提示符噪音,按设计接受)
|
|
812
|
+
out = raw.slice(end, boundary);
|
|
813
|
+
}
|
|
814
|
+
else if (marker.done || probe?.done) {
|
|
815
|
+
// 中断命令:完成由补发探针标记(SSH_PROBE)触发,输出窗口会多出 ^C 回显、重印提示符与探针回显;
|
|
721
816
|
// 输出在首个 ^C 回显处截断,保留中断前程序输出(探针回显另由 _stripProbeEchoes 剔除)
|
|
722
|
-
let endPos =
|
|
817
|
+
let endPos = boundary;
|
|
723
818
|
if (this._interruptSent) {
|
|
724
819
|
const intr = detectInterrupt(raw, end);
|
|
725
820
|
if (intr.interrupted)
|
|
@@ -727,16 +822,18 @@ export class BaseSession {
|
|
|
727
822
|
}
|
|
728
823
|
out = raw.slice(end, endPos);
|
|
729
824
|
}
|
|
730
|
-
else if (syntaxMode) {
|
|
731
|
-
// 语法错误:命令整行作废、无 marker → 输出 = 回显结束 → 窗口末尾(含整段错误块),再剥尾部重印提示符
|
|
732
|
-
out = stripTrailingPromptLine(raw.slice(end), raw, this._promptRef);
|
|
733
|
-
}
|
|
734
825
|
else {
|
|
735
|
-
|
|
736
|
-
|
|
826
|
+
// 无完成标记:仅当插件确实发过中断(_interruptSent)才把首个 ^C 当中断回显截断;
|
|
827
|
+
// 否则真实输出里的字面量 ^C(cat/echo 含 ^C 的文本、工具日志、记录过 Ctrl+C 的 transcript)
|
|
828
|
+
// 会被误当中断回显,导致输出在那里被切甚至全空
|
|
829
|
+
if (this._interruptSent) {
|
|
830
|
+
const intr = detectInterrupt(raw, end);
|
|
831
|
+
out = intr.interrupted ? raw.slice(end, intr.pos) : raw.slice(end);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
out = raw.slice(end);
|
|
835
|
+
}
|
|
737
836
|
}
|
|
738
|
-
if (this._adapter)
|
|
739
|
-
out = out.replace(this._markerCmd(this._runningSeq), "");
|
|
740
837
|
return this._stripProbeEchoes(stripMarkers(out.replace(/^[\r\n]+/, "")));
|
|
741
838
|
}
|
|
742
839
|
/**
|
|
@@ -794,9 +891,7 @@ export class BaseSession {
|
|
|
794
891
|
// 诊断:本次窗口起点取自"自身找到的 lastMarkerPos"(无标记时为 0,即缓冲起点);
|
|
795
892
|
// captureStart=裁剪后残留长度(exec 的实际捕获起点,与缓冲内容相关,仅供参考)
|
|
796
893
|
const winStartPos = marker.done ? marker.pos : 0;
|
|
797
|
-
|
|
798
|
-
// 记录命令发起前缓冲末行(旧提示符行):语法错误收尾时 shell 会重印同形提示符,供输出尾部剥离
|
|
799
|
-
this._promptRef = this._buffer.split("\n").pop() ?? "";
|
|
894
|
+
debug(`[capture] ${this._logTag} seq=${this._cmdSeq + 1} 起点=${winStartPos} captureStart=${this._buffer.length}(参考) entryCursor=${entryCursor} bufLen=${entryBufLen} lastMarkerPos=${marker.done ? marker.pos : -1}`);
|
|
800
895
|
this._runningStartPos = 0;
|
|
801
896
|
this._runningCommand = command;
|
|
802
897
|
this._runningStartTs = Date.now();
|
|
@@ -805,7 +900,7 @@ export class BaseSession {
|
|
|
805
900
|
this._runningSeq = this._cmdSeq;
|
|
806
901
|
this._setBusy(true, "cmdStart");
|
|
807
902
|
this._lastActive = Date.now();
|
|
808
|
-
|
|
903
|
+
debug(`[run] 开始 ${this._logTag} seq=${this._runningSeq} startPos=0 cmdLen=${command.length}`);
|
|
809
904
|
debug(`[run] 开始 ${this._logTag} seq=${this._runningSeq} cmd=${JSON.stringify(command.slice(0, 120))}`);
|
|
810
905
|
this._emitStart(command, this._runningStartTs);
|
|
811
906
|
}
|
|
@@ -832,7 +927,7 @@ export class BaseSession {
|
|
|
832
927
|
if (text.includes(DONE_TAG)) {
|
|
833
928
|
const found = scanDoneMarkers(text).map((h) => h.seq ?? "?").join(",");
|
|
834
929
|
// 非内容信息保持 info(排障主力);命令/输出片段前缀(prefix)降级为 debug,避免泄漏内容
|
|
835
|
-
|
|
930
|
+
debug(`[chunk] ${this._logTag} seq=${this._runningSeq} runningStart=${this._runningStartPos} chunkLen=${text.length} found=[${found}]`);
|
|
836
931
|
debug(`[chunk] ${this._logTag} seq=${this._runningSeq} prefix=${escapeCtl(text.slice(0, 60))}`);
|
|
837
932
|
}
|
|
838
933
|
// 数据已落缓冲:通知数据到达监听器(agent 据此做节流合并推流;异常不打断数据写入)
|
|
@@ -854,8 +949,8 @@ export class BaseSession {
|
|
|
854
949
|
* @returns 无
|
|
855
950
|
*/
|
|
856
951
|
_sendBaselineMarker() {
|
|
857
|
-
const baseline = this._markerCmd(0) +
|
|
858
|
-
|
|
952
|
+
const baseline = this._markerCmd(0) + this._ptyLineSep();
|
|
953
|
+
debug(`[baseline] ${this._logTag} 初始化完成,发送基线标记 nonce=${this._nonce} seq=0`);
|
|
859
954
|
this._write(baseline);
|
|
860
955
|
}
|
|
861
956
|
/** 探测 shell 类型:按各适配器 probeCommand 逐个探测(POSIX 系优先,pwsh/cmd 用各自独有命令),
|
|
@@ -866,8 +961,16 @@ export class BaseSession {
|
|
|
866
961
|
this._write(candidate.probeCommand + "\r");
|
|
867
962
|
const output = await this._waitProbeOutput(Math.min(PROBE_TIMEOUT_MS, deadline - Date.now()));
|
|
868
963
|
this._buffer = "";
|
|
869
|
-
if (output !== null && candidate.parseProbe(output))
|
|
964
|
+
if (output !== null && candidate.parseProbe(output)) {
|
|
965
|
+
// 终端平台按真实 shell 判断:cmd 必为 Windows;pwsh 看 $PSHOME 是 Windows 路径还是 Unix 路径;
|
|
966
|
+
// bash/zsh 走 Unix PTY。这样 SSH 到 Linux 上的 pwsh 也会用 \n(不依赖 ICRNL)。
|
|
967
|
+
this._windowsPTY = candidate.name === "cmd"
|
|
968
|
+
? true
|
|
969
|
+
: candidate.name === "pwsh"
|
|
970
|
+
? /__SHELL_ID__pwsh_[A-Za-z]:[\\/]/.test(output)
|
|
971
|
+
: false;
|
|
870
972
|
return candidate;
|
|
973
|
+
}
|
|
871
974
|
}
|
|
872
975
|
}
|
|
873
976
|
return null;
|
|
@@ -938,13 +1041,21 @@ export class BaseSession {
|
|
|
938
1041
|
return;
|
|
939
1042
|
}
|
|
940
1043
|
try {
|
|
941
|
-
//
|
|
1044
|
+
// 真实完成(权威且唯一):只认本会话 nonce + 本 seq 的 SSH_DONE 标记。
|
|
1045
|
+
// 探针已改用独立 SSH_PROBE tag,二者不再混淆——语法误判时真实退出码不会被覆盖。
|
|
942
1046
|
const marker = detectLastDoneMarker(this._buffer, this._runningStartPos, this._nonce, this._runningSeq);
|
|
943
1047
|
if (marker.done) {
|
|
944
1048
|
this._settleCompletion({ kind: "done", markerPos: marker.pos, exitCode: marker.exitCode });
|
|
945
1049
|
return;
|
|
946
1050
|
}
|
|
947
|
-
//
|
|
1051
|
+
// 中断探针(SSH_PROBE):Ctrl+C 后 shell 回到提示符执行补发的标记命令。
|
|
1052
|
+
// 语法错误路径不再补发探针(见 _armSyntaxQuiet):避免把命令写进仍在运行的程序 stdin。
|
|
1053
|
+
const probe = detectLastProbeMarker(this._buffer, this._runningStartPos, this._nonce, this._runningSeq);
|
|
1054
|
+
if (probe.done) {
|
|
1055
|
+
this._settleCompletion({ kind: "done", markerPos: probe.pos, exitCode: probe.exitCode });
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// 语法/解析错误:整行命令作废(含同行拼装的完成标记也不会执行)→ 输出安静后补发探针按完成收尾
|
|
948
1059
|
if (detectSyntaxError(this._adapter, this._buffer, this._watchStartPos)) {
|
|
949
1060
|
// 兜底 tick 不重置已挂的静默计时(否则每周期顺延、永不收尾);数据到达时才推进
|
|
950
1061
|
if (fromData || this._syntaxTimer === null)
|
|
@@ -988,9 +1099,11 @@ export class BaseSession {
|
|
|
988
1099
|
this._completeRunning(this._watchStartPos, o.markerPos ?? this._buffer.length, o.exitCode ?? null, this._watchCommand);
|
|
989
1100
|
}
|
|
990
1101
|
/**
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
1102
|
+
* 推进语法错误安静窗口(一次性定时器,非轮询):
|
|
1103
|
+
* 识别到语法错误后等输出停止增长 SYNTAX_QUIET_MS,确保 pwsh 异步/批量渲染的多行错误块完整收进输出窗口;
|
|
1104
|
+
* 到点**直接按语法错误收尾,不再补发探针**——探针会被写进仍在运行的程序 stdin(如 `cat >file` / REPL),
|
|
1105
|
+
* 且语法误判时会提前收尾、丢弃迟到的真实完成标记;尾部最多多 1 行提示符噪音,按设计接受。
|
|
1106
|
+
* 自首次识别起硬上限 SYNTAX_MAX_WAIT_MS;数据到达(输出变化)时重置安静窗口;兜底 tick 不重置(见 _checkCompletion)。
|
|
994
1107
|
* @returns 无
|
|
995
1108
|
*/
|
|
996
1109
|
_armSyntaxQuiet() {
|
|
@@ -1007,7 +1120,7 @@ export class BaseSession {
|
|
|
1007
1120
|
}
|
|
1008
1121
|
if (now - this._syntaxSeenAt >= SYNTAX_MAX_WAIT_MS) {
|
|
1009
1122
|
this._clearSyntaxTimer();
|
|
1010
|
-
this.
|
|
1123
|
+
this._settleSyntaxError();
|
|
1011
1124
|
return;
|
|
1012
1125
|
}
|
|
1013
1126
|
const remain = Math.max(0, SYNTAX_QUIET_MS - (now - this._syntaxQuietAt));
|
|
@@ -1016,14 +1129,18 @@ export class BaseSession {
|
|
|
1016
1129
|
this._syntaxTimer = null;
|
|
1017
1130
|
if (this._runningStartPos === null)
|
|
1018
1131
|
return;
|
|
1019
|
-
|
|
1020
|
-
this._settleCompletion({ kind: "done", exitCode: null, syntaxError: true });
|
|
1021
|
-
}
|
|
1022
|
-
catch (e) {
|
|
1023
|
-
log.info(`[watch] ${this._logTag} seq=${this._runningSeq} 语法静默收尾异常: err=${e instanceof Error ? e.message : String(e)}`);
|
|
1024
|
-
}
|
|
1132
|
+
this._settleSyntaxError();
|
|
1025
1133
|
}, remain);
|
|
1026
1134
|
}
|
|
1135
|
+
/** 按语法错误收尾(含异常兜底:收尾本身再抛也不能变成未捕获异常) */
|
|
1136
|
+
_settleSyntaxError() {
|
|
1137
|
+
try {
|
|
1138
|
+
this._settleCompletion({ kind: "done", exitCode: null, syntaxError: true });
|
|
1139
|
+
}
|
|
1140
|
+
catch (e) {
|
|
1141
|
+
log.info(`[watch] ${this._logTag} seq=${this._runningSeq} 语法错误收尾异常: err=${e instanceof Error ? e.message : String(e)}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1027
1144
|
/**
|
|
1028
1145
|
* 断连收尾(由后台兜底 tick 判定 _connected=false 触发):命令视为结束(可能从未完成),
|
|
1029
1146
|
* 补发 done 防 server busy 卡死;不写 history(输出窗口从未定案,无可信尾部可摘)。
|
|
@@ -1043,7 +1160,7 @@ export class BaseSession {
|
|
|
1043
1160
|
}
|
|
1044
1161
|
}
|
|
1045
1162
|
/**
|
|
1046
|
-
*
|
|
1163
|
+
* 开始一次命令的后台追踪:记录兜底判定所需的捕获窗口起点/命令,并复位语法错误状态。
|
|
1047
1164
|
* 不做任何轮询——完成判定由数据到达事件驱动(见 `_onDataArrived`)。
|
|
1048
1165
|
* @param startPos 捕获窗口起点(相对当前 buffer;submit 为 0,exec 转后台为 captureStart)
|
|
1049
1166
|
* @param command 命令(history 记录用)
|
|
@@ -1082,7 +1199,7 @@ export class BaseSession {
|
|
|
1082
1199
|
}
|
|
1083
1200
|
catch (e) {
|
|
1084
1201
|
// tick 内异常:记一行后继续下一轮监听(完成判定仍只认当前 seq 标记)
|
|
1085
|
-
|
|
1202
|
+
infoDedup(`tick-exc:${this._logTag}:${this._runningSeq}`, `[watch] ${this._logTag} seq=${this._runningSeq} tick 异常, 保持监听: err=${e instanceof Error ? e.message : String(e)}`);
|
|
1086
1203
|
}
|
|
1087
1204
|
}
|
|
1088
1205
|
/** 清除 exec 同步等待的 animation 一次性定时器(幂等) */
|
|
@@ -1121,10 +1238,16 @@ export class BaseSession {
|
|
|
1121
1238
|
log.info(`[extract] ${this._logTag} seq=${this._runningSeq} 输出提取异常, 使用整窗输出: cmdLen=${command.length} err=${e instanceof Error ? e.message : String(e)}`);
|
|
1122
1239
|
output = raw;
|
|
1123
1240
|
}
|
|
1124
|
-
|
|
1241
|
+
try {
|
|
1242
|
+
this._history.append(command, output, this._runningStartTs, undefined, exitCode ?? undefined);
|
|
1243
|
+
}
|
|
1244
|
+
catch (e) {
|
|
1245
|
+
// history 落盘失败(目录被删/权限/磁盘)不得阻断 done/busy 收尾,否则会话永久 busy
|
|
1246
|
+
log.info(`[history] ${this._logTag} seq=${this._runningSeq} 追加失败: err=${e instanceof Error ? e.message : String(e)}`);
|
|
1247
|
+
}
|
|
1125
1248
|
this._emitDone(exitCode, Date.now(), output);
|
|
1126
1249
|
this._cursor = Math.max(startPos, endPos);
|
|
1127
|
-
this._setBusy(false, "cmdDone"); // 命令完成即清 busy("保持 busy 到排队序列结束"
|
|
1250
|
+
this._setBusy(false, "cmdDone", true); // 命令完成即清 busy("保持 busy 到排队序列结束"的机制已移除);释放行紧随 [run] 结束,降 debug 免重复
|
|
1128
1251
|
// 结束日志:非内容信息(长度/耗时/退出码/原因)保持 info;有界输出尾部摘要降级为 debug,避免泄漏输出内容
|
|
1129
1252
|
log.info(`[run] 结束 ${this._logTag} seq=${this._runningSeq} endPos=${endPos} exitCode=${exitCode} outLen=${output.length} duration=${Date.now() - this._runningStartTs}ms reason=${reason}`);
|
|
1130
1253
|
if (isDebugEnabled()) {
|