@hyzyn/dsh-tty 0.20.0 → 0.20.2
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/client.js +34 -3
- package/lib/index.d.ts +133 -2
- package/lib/index.js +324 -70
- package/lib/index.js.map +1 -1
- package/package.json +13 -6
- package/scripts/integration.mjs +46 -7
- package/scripts/preview.mjs +16 -1
- package/scripts/probe-route-smoke.mjs +3 -1
- package/scripts/screen-crash-repro.mjs +62 -0
- package/scripts/sftplimits-smoke.mjs +3 -1
- package/scripts/windows-smoke.mjs +3 -1
package/lib/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
* 子进程),必须 best-effort:失败降级为对顶层 shell 直接 SIGKILL。
|
|
68
68
|
*/
|
|
69
69
|
import type { Context } from '@deepseek-ai/cordis';
|
|
70
|
+
import z from '@deepseek-ai/schemastery';
|
|
70
71
|
import { StringDecoder } from 'node:string_decoder';
|
|
71
72
|
import WebSocket from 'ws';
|
|
72
73
|
import xtermHeadless from '@xterm/headless';
|
|
@@ -125,6 +126,15 @@ export interface SftpLimits {
|
|
|
125
126
|
/** 一次批量/拖拽上传的文件数上限。默认 1000。 */
|
|
126
127
|
maxUploadFiles: number;
|
|
127
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 运行时 Config schema——DSH ≥0.1.7 起同时就是本插件的 settings 存储。
|
|
131
|
+
*
|
|
132
|
+
* 全部字段都标 `.volatile()`:它们都是「插件配置 → 终端面板」卡片可改项,而
|
|
133
|
+
* `settings.update(entryId, patch)` 只接受 volatile 路径;loader 对 volatile-only
|
|
134
|
+
* 变更原地更新引用并发 `loader/volatile-update`,不重挂插件——插件订阅后走
|
|
135
|
+
* `applyPatch` 热应用(见 @hyzyn/dsh-kit 的 settingsEntryScope)。
|
|
136
|
+
*/
|
|
137
|
+
export declare const Config: z;
|
|
128
138
|
/**
|
|
129
139
|
* 本地 PTY 顶层 shell 的 best-effort 强杀(D48)。
|
|
130
140
|
*
|
|
@@ -188,6 +198,10 @@ interface TtySession {
|
|
|
188
198
|
decoder: StringDecoder;
|
|
189
199
|
/** 虚拟屏(xterm-headless):tty_screen 的数据源;创建失败为 null。 */
|
|
190
200
|
screen: HeadlessTerminal | null;
|
|
201
|
+
/** 虚拟屏心跳(D57 停摆检测):在途批次 + 看门狗。 */
|
|
202
|
+
screenHeartbeat: ScreenHeartbeat;
|
|
203
|
+
/** 虚拟屏被退役的原因(停摆 / 写队列满);null = 正常。tty_screen 据此如实报错。 */
|
|
204
|
+
screenDownReason: string | null;
|
|
191
205
|
/** 转入孤儿状态的时间戳;null 表示已连接(客户端在线)。 */
|
|
192
206
|
orphanedAt: number | null;
|
|
193
207
|
/** shell 集成状态(OSC 133/7 解析;本地与 SSH 会话都喂)。 */
|
|
@@ -221,7 +235,7 @@ interface ReqLike {
|
|
|
221
235
|
interface SocketLike {
|
|
222
236
|
destroy(): void;
|
|
223
237
|
}
|
|
224
|
-
/** 可热更新的运行时配置(
|
|
238
|
+
/** 可热更新的运行时配置(loader 的 volatile 更新事件动态应用)。 */
|
|
225
239
|
declare class LiveConfig {
|
|
226
240
|
shell: string;
|
|
227
241
|
term: string;
|
|
@@ -319,6 +333,104 @@ declare class HostKeyStore {
|
|
|
319
333
|
/** 记录指纹:同 host:port 已有记录则并入集合(一机多把钥匙),否则新建。 */
|
|
320
334
|
record(host: string, port: number, fingerprint: string): void;
|
|
321
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* 虚拟屏的 scrollback 余量(D57)。
|
|
338
|
+
*
|
|
339
|
+
* **不能是 0。** xterm 的 `Buffer` 在 `scrollback: 0` 时把 `lines.maxLength` 压成
|
|
340
|
+
* `rows`,但 normal buffer 的 `_hasScrollback` 仍是 true(reflow 照常开启):输出与
|
|
341
|
+
* resize(列宽变化触发 reflow)交错时,`lines` 会短于 `ybase + y`,于是
|
|
342
|
+
* `lineFeed()` 里 `lines.get(ybase + y).isWrapped = false` 命中 `undefined` →
|
|
343
|
+
* 未捕获 `TypeError: Cannot set properties of undefined (setting 'isWrapped')`。
|
|
344
|
+
*
|
|
345
|
+
* 该异常抛在 `WriteBuffer._innerWrite` 的 `setTimeout` 回调里,写入路径的同步
|
|
346
|
+
* try/catch 结构性拦不住,会直接打死整个宿主进程(线上 `last-failure-web.log`
|
|
347
|
+
* 的堆栈即此)。
|
|
348
|
+
*
|
|
349
|
+
* 关键在 `lines.maxLength`(= rows + scrollback):`BufferService.scroll` 只在「没满」时
|
|
350
|
+
* 才 `lines.push` + `ybase++`(成对)。`scrollback: 0` 把 maxLength 钉死成 rows,
|
|
351
|
+
* 一旦有别的路径把 `ybase` 顶上去(resize 收缩 / reflow),`lines` 长度就再也追不上,
|
|
352
|
+
* `ybase + y + 1` 越界只是时间问题。留 1 行余量(maxLength = rows + 1)即维持住成对增长:
|
|
353
|
+
* 同一最小序列 `scrollback: 0` 崩 5/5,`scrollback: 1` 崩 0/5;700 块随机屏压测里
|
|
354
|
+
* `ybase` 涨到 16 也没再出现越界(见 test/screen-crash.test.ts)。
|
|
355
|
+
*
|
|
356
|
+
* 余量不影响 `tty_screen` 读数——它走 `buffer.getLine(row)`(内部 `ybase + row`,
|
|
357
|
+
* 即视口),多出来的行只在回滚区,不进读数。
|
|
358
|
+
*/
|
|
359
|
+
export declare const SCREEN_SCROLLBACK = 1;
|
|
360
|
+
/**
|
|
361
|
+
* 建一块虚拟屏(`tty_screen` 的数据源);失败降级为 null。
|
|
362
|
+
*
|
|
363
|
+
* 导出仅供单测(test/screen-crash.test.ts)钉住构造参数——生产路径是
|
|
364
|
+
* `SessionManager.createScreen`,它必须与这里同源(就一行委托)。
|
|
365
|
+
*/
|
|
366
|
+
export declare function createHeadlessScreen(cols: number, rows: number): HeadlessTerminal | null;
|
|
367
|
+
/** 读累计吞掉的虚拟屏异常数。 */
|
|
368
|
+
export declare function xtermScreenCrashCount(): number;
|
|
369
|
+
/** 判定未捕获异常是否来自虚拟屏(xterm-headless)。导出仅供单测。 */
|
|
370
|
+
export declare function isXtermScreenCrash(err: unknown): boolean;
|
|
371
|
+
/**
|
|
372
|
+
* 记账并吞掉一个虚拟屏异常;返回 true 表示已吞(非虚拟屏异常返回 false,交回调用方)。
|
|
373
|
+
* 导出仅供单测。
|
|
374
|
+
*/
|
|
375
|
+
export declare function swallowXtermScreenCrash(err: unknown): boolean;
|
|
376
|
+
/**
|
|
377
|
+
* 注册进程级虚拟屏异常兜底(D57):把来自 xterm-headless 的未捕获异常 / 未处理 rejection
|
|
378
|
+
* 吞掉并记账,让插件自己的 bug 不再拖垮整个 harness。幂等 + 引用计数,返回解绑函数。
|
|
379
|
+
*
|
|
380
|
+
* 覆盖两个入口:
|
|
381
|
+
* - `uncaughtException`:同步路径(`_innerWrite` 的定时器回调里抛出,见上);
|
|
382
|
+
* - `unhandledRejection`:xterm 的异步 handler(DCS/OSC)rejection 走这里,宿主实测
|
|
383
|
+
* **0 处**监听,Node 15+ 下未处理 rejection 直接杀进程。
|
|
384
|
+
*
|
|
385
|
+
* 三条边界(刻意如此,不是随手 `process.on`):
|
|
386
|
+
* 1. **只吞虚拟屏异常**——`isXtermScreenCrash` 按堆栈判定;其余异常照旧。
|
|
387
|
+
* 2. **其余异常只在「我们是唯一的监听者」时抛回**:没有本兜底时未捕获异常会让宿主退出,
|
|
388
|
+
* 抛回保住这个语义;已经有别的监听者(宿主/其它插件)时保持沉默,由它们决定——
|
|
389
|
+
* 此时抛回反而会抢在别人前面把进程杀掉。
|
|
390
|
+
* 3. `unhandledRejection` 的「抛回」还有一层必要性:**只要挂了监听器,Node 就不再走
|
|
391
|
+
* 默认的致命处理**,所以非虚拟屏的 rejection 必须由我们抛出来还原默认行为
|
|
392
|
+
* (已实测:抛回后进程照旧 exit 1)。
|
|
393
|
+
*/
|
|
394
|
+
export declare function installXtermScreenCrashGuard(): () => void;
|
|
395
|
+
/**
|
|
396
|
+
* 停摆判定窗口:写出去的数据超过这么久还没解析完,就认定那块屏的解析器已停摆。
|
|
397
|
+
* 正常屏的解析是毫秒级(回调随 `_innerWrite` 逐批回来),5s 不会误伤。
|
|
398
|
+
*/
|
|
399
|
+
export declare const SCREEN_STALL_MS = 5000;
|
|
400
|
+
/** 退役原因①:写队列超限 / 尺寸非法导致的同步抛出。 */
|
|
401
|
+
export declare const SCREEN_DOWN_WRITE_REJECTED = "\u5199\u5165\u88AB\u62D2\uFF08\u5199\u961F\u5217\u8D85\u9650\u6216\u5C3A\u5BF8\u975E\u6CD5\uFF09";
|
|
402
|
+
/** 退役原因②:解析器停摆(超时窗口内没有任何一批数据被解析完)。 */
|
|
403
|
+
export declare const SCREEN_DOWN_STALLED = "\u89E3\u6790\u505C\u6446\uFF08xterm \u5728\u8D85\u65F6\u7A97\u53E3\u5185\u672A\u56DE\u8C03\uFF09";
|
|
404
|
+
/** 虚拟屏心跳:在途批次 + 看门狗(D57)。 */
|
|
405
|
+
export interface ScreenHeartbeat {
|
|
406
|
+
/** 已写出、尚未被 xterm 解析完的批次(`write(data, cb)` 的回调未回来即 >0)。 */
|
|
407
|
+
inflight: number;
|
|
408
|
+
/** 看门狗;null = 当前没有挂着的窗口。 */
|
|
409
|
+
watchdog: NodeJS.Timeout | null;
|
|
410
|
+
/** 最近一次解析完成的时间戳(0 = 从未)。看门狗靠它区分「解析在途」与「真停摆」。 */
|
|
411
|
+
lastParseAt: number;
|
|
412
|
+
}
|
|
413
|
+
/** 建一份空心跳。 */
|
|
414
|
+
export declare function newScreenHeartbeat(): ScreenHeartbeat;
|
|
415
|
+
/** 摘掉看门狗(会话结束 / 屏退役时调用,避免定时器在会话死后误报)。 */
|
|
416
|
+
export declare function clearScreenWatchdog(heartbeat: ScreenHeartbeat): void;
|
|
417
|
+
/**
|
|
418
|
+
* 写一帧到虚拟屏,并维护停摆看门狗(D57)。
|
|
419
|
+
*
|
|
420
|
+
* **为什么需要心跳**:xterm 的解析在 `WriteBuffer._innerWrite` 的 setTimeout 回调里跑,
|
|
421
|
+
* 异常被进程级兜底吞掉之后,那块屏的解析器**永久停摆**——出错的那批数据留在写队列里、
|
|
422
|
+
* `_bufferOffset` 不前进,而 `write()` 只在队列**空**时才重新调度解析。后果:`tty_screen`
|
|
423
|
+
* 一直返回**冻结的旧画面**(agent 会据此行事),写队列还会一路堆到 5e7 字符上限。
|
|
424
|
+
* 心跳把这种屏识别出来退役,`tty_screen` 改为如实报「虚拟屏不可用」。
|
|
425
|
+
*
|
|
426
|
+
* 信号用 `write(data, cb)` 的回调(xterm 解析完这批数据才回调):停摆时回调永远不来 →
|
|
427
|
+
* `inflight` 不归零 → 看门狗判定。**不能用 `onWriteParsed` 事件**——它在 5.5.0 不是
|
|
428
|
+
* 公开 API(`Terminal` 只暴露 onBell/onBinary/onCursorMove/onData/onLineFeed/onResize/
|
|
429
|
+
* onScroll/onTitleChange)。
|
|
430
|
+
*/
|
|
431
|
+
export declare function writeToScreen(screen: {
|
|
432
|
+
write(data: string, callback?: () => void): void;
|
|
433
|
+
}, heartbeat: ScreenHeartbeat, text: string, onStall: (reason: string) => void, stallMs?: number): void;
|
|
322
434
|
/** 导出仅供单测(test/host-frames.test.ts):上限 / 孤儿回收 / grace 热改的行为护栏。 */
|
|
323
435
|
export declare class SessionManager {
|
|
324
436
|
private readonly sessions;
|
|
@@ -395,6 +507,8 @@ export declare class TtyServer {
|
|
|
395
507
|
private readonly pendingTmux;
|
|
396
508
|
/** 已接线的面板连接(sessions 帧广播用;比 wss.clients 更贴合「面板」语义,单测也可驱动)。 */
|
|
397
509
|
private readonly panels;
|
|
510
|
+
/** 会话 → 它所属连接的 sid 映射(kill 兜底结案时要从本地表里摘除)。 */
|
|
511
|
+
private readonly sessionLocals;
|
|
398
512
|
/** WS 闸门(插件禁用时关闭):拒绝新升级 + 断开存量连接。 */
|
|
399
513
|
private wsGateOpen;
|
|
400
514
|
/** 服务器状态条总开关(配置热生效;关闭时停掉全部采集,重开按订阅恢复)。 */
|
|
@@ -515,8 +629,17 @@ export declare class TtyServer {
|
|
|
515
629
|
* 会话会留在 tmux server 上);2.5s 兜底 forceKill 防收尾悬挂。
|
|
516
630
|
*/
|
|
517
631
|
private killSessionNow;
|
|
518
|
-
/** 每会话一块虚拟屏(xterm-headless):tty_screen 的数据源;失败降级为 null。
|
|
632
|
+
/** 每会话一块虚拟屏(xterm-headless):tty_screen 的数据源;失败降级为 null。
|
|
633
|
+
* 构造参数在 createHeadlessScreen(D57:scrollback 不能是 0),这里只做委托。 */
|
|
519
634
|
private createScreen;
|
|
635
|
+
/**
|
|
636
|
+
* 退役一块**不可用**的虚拟屏(D57):解析停摆或写队列满时调用。
|
|
637
|
+
*
|
|
638
|
+
* 只摘虚拟屏,**不动会话**——PTY 还活着、浏览器面板照常收发(虚拟屏只是 `tty_screen`
|
|
639
|
+
* 的数据源)。退役后 `tty_screen` 会如实报「虚拟屏不可用(原因)」,而不是返回冻结的
|
|
640
|
+
* 旧画面让 agent 据此行事。
|
|
641
|
+
*/
|
|
642
|
+
private dropScreen;
|
|
520
643
|
private handleMessage;
|
|
521
644
|
/**
|
|
522
645
|
* 服务器状态条订阅(0.17.0):按「标签可见性」驱动——只有可见标签才发
|
|
@@ -526,6 +649,14 @@ export declare class TtyServer {
|
|
|
526
649
|
private handleStatsFrame;
|
|
527
650
|
/** 会话退出事实 → exit 帧(恰好一次;本地 PTY 与 SSH 共用)。 */
|
|
528
651
|
private watchDone;
|
|
652
|
+
/**
|
|
653
|
+
* 会话终局的**唯一出口**:退役 + 清理 + 给所有绑定连接发 exit 帧(恰好一次)。
|
|
654
|
+
*
|
|
655
|
+
* `outcome` 正常来自 PTY 句柄的 done;显式 kill 的兜底(KILL_EXIT_FALLBACK_MS)
|
|
656
|
+
* 也走这里,带 code=null / signal=SIGKILL。exit 广播到所有绑定连接(跨窗口共享),
|
|
657
|
+
* 各客户端按自己的 sid 收址。
|
|
658
|
+
*/
|
|
659
|
+
private finishSession;
|
|
529
660
|
/** 输出下行 + 基于 ws.bufferedAmount 的背压(暂停/恢复 PassThrough)。 */
|
|
530
661
|
private attachOutput;
|
|
531
662
|
/** 立即冲刷待发的合并输出(exit/kill 前调用,保证 exit 帧永远在最后一帧 data 之后)。 */
|