@hyzyn/dsh-tty 0.22.2 → 0.24.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/lib/ansi.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 终端文本的转义序列清洗原语(0.24.0 从 index.ts 抽出)。
3
+ *
4
+ * 为什么单独立一个文件:`tty_capture` 的尾部清洗与「失败即解释」的上下文压缩
5
+ * 建立在同一套正则上。各留一份的下场是漂移——一边补了新的转义形态、另一边不知道,
6
+ * 表现就是「工具输出里看着干干净净,发给模型的是满屏 `\x1b[32m`」。
7
+ *
8
+ * 逐行近似,**不追求完整 VT 语义**:要完整画面用 `tty_screen` 的 xterm-headless 虚拟屏。
9
+ */
10
+ /** 剥离 OSC 序列(`\x1b]…BEL/ST`):命令标记 133、cwd 上报 7、超链接 8 都在这里。 */
11
+ export declare function stripOsc(text: string): string;
12
+ /** 剥离 CSI 与其余 ESC 序列(颜色、光标移动、清屏)。 */
13
+ export declare function stripAnsi(raw: string): string;
14
+ /**
15
+ * 同行内 `\r` 覆盖收敛为**最后一次覆盖的结果**(进度条不再刷屏)。
16
+ *
17
+ * 先把「行尾 \r\n」(zsh 行结束常为 \r\r\n)归一成 \n,再处理剩余孤立的 \r ——
18
+ * 否则回显/输出行会被误判为覆盖而整行抹掉。
19
+ */
20
+ export declare function collapseCarriageReturns(text: string): string;
21
+ /** 转义清洗的合成(原 `cleanAnsiTail` 的行为,逐字节等价)。 */
22
+ export declare function cleanAnsi(raw: string): string;
package/lib/ansi.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 终端文本的转义序列清洗原语(0.24.0 从 index.ts 抽出)。
3
+ *
4
+ * 为什么单独立一个文件:`tty_capture` 的尾部清洗与「失败即解释」的上下文压缩
5
+ * 建立在同一套正则上。各留一份的下场是漂移——一边补了新的转义形态、另一边不知道,
6
+ * 表现就是「工具输出里看着干干净净,发给模型的是满屏 `\x1b[32m`」。
7
+ *
8
+ * 逐行近似,**不追求完整 VT 语义**:要完整画面用 `tty_screen` 的 xterm-headless 虚拟屏。
9
+ */
10
+ /** 剥离 OSC 序列(`\x1b]…BEL/ST`):命令标记 133、cwd 上报 7、超链接 8 都在这里。 */
11
+ export function stripOsc(text) {
12
+ return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
13
+ }
14
+ /** 剥离 CSI 与其余 ESC 序列(颜色、光标移动、清屏)。 */
15
+ export function stripAnsi(raw) {
16
+ const withoutOsc = stripOsc(raw);
17
+ const withoutCsi = withoutOsc.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
18
+ return withoutCsi.replace(/\x1b[@-Z\\-_]/g, '');
19
+ }
20
+ /**
21
+ * 同行内 `\r` 覆盖收敛为**最后一次覆盖的结果**(进度条不再刷屏)。
22
+ *
23
+ * 先把「行尾 \r\n」(zsh 行结束常为 \r\r\n)归一成 \n,再处理剩余孤立的 \r ——
24
+ * 否则回显/输出行会被误判为覆盖而整行抹掉。
25
+ */
26
+ export function collapseCarriageReturns(text) {
27
+ const normalized = text.replace(/\r+\n/g, '\n');
28
+ return normalized.split('\n').map((line) => {
29
+ const idx = line.lastIndexOf('\r');
30
+ return idx === -1 ? line : line.slice(idx + 1);
31
+ }).join('\n');
32
+ }
33
+ /** 转义清洗的合成(原 `cleanAnsiTail` 的行为,逐字节等价)。 */
34
+ export function cleanAnsi(raw) {
35
+ return collapseCarriageReturns(stripAnsi(raw));
36
+ }
37
+ //# sourceMappingURL=ansi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi.js","sourceRoot":"","sources":["../src/ansi.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8DAA8D;AAC9D,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,OAAO,CAAC,oCAAoC,EAAE,EAAE,CAAC,CAAA;AAC/D,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IAChC,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAA;IACrE,OAAO,UAAU,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAA;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC/C,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAClC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;IAChD,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,uBAAuB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;AAChD,CAAC"}
@@ -0,0 +1,84 @@
1
+ /** 命令输出尾部保留行数。 */
2
+ export declare const CONTEXT_TAIL_LINES = 40;
3
+ /** 上下文正文的字符上限(超出从头部丢弃,并注明丢了多少行)。 */
4
+ export declare const CONTEXT_MAX_CHARS = 6000;
5
+ /** 屏幕文本的行数上限(只在「没有命令输出」时才用屏幕兜底)。 */
6
+ export declare const SCREEN_TAIL_LINES = 30;
7
+ /**
8
+ * 这个退出码值不值得摆徽标。
9
+ *
10
+ * 0 不弹;`null` / `undefined`(没拿到退出码 = 命令边界不可信)也不弹——
11
+ * 宁可少弹一次,也不要在正常的提示符下摆一张「它失败了」的脸。
12
+ *
13
+ * **130 / 141 是刻意豁免的**:前者是 Ctrl-C(用户自己按的),后者是 SIGPIPE
14
+ * (`… | head` 的常态)。这两个天天出现,为它们弹徽标会让用户干脆把整个功能关掉。
15
+ */
16
+ export declare function shouldExplainExit(code: number | null | undefined): boolean;
17
+ /** 规则表只读导出,供单测逐条枚举(新加一条规则必须同时加一条用例)。 */
18
+ export declare const SECRET_RULE_WHY: readonly string[];
19
+ export declare function maskSecrets(text: string): {
20
+ text: string;
21
+ count: number;
22
+ };
23
+ /**
24
+ * 把一段终端输出压成上下文正文。
25
+ *
26
+ * 四步,每一步都对应一种真实的噪声:
27
+ * 1. 剥转义(颜色/光标/进度条转义)——不剥的话模型看到的是满屏控制码;
28
+ * 2. \r 覆盖收敛——`npm install` 那一类会把同一行重画几百次;
29
+ * 3. 折掉连续空行与**连续重复行**——重复日志(同一句 warning 刷 50 遍)是纯浪费预算;
30
+ * 4. 取尾部并按字符上限硬切——报错在末尾,且开头半行要丢掉(半行命令比没有更误导)。
31
+ *
32
+ * 头部被丢弃时**显式注明行数**:否则模型会把「输出第一行就是报错」当成事实。
33
+ */
34
+ export declare function compressTerminalText(raw: string, options?: {
35
+ maxLines?: number;
36
+ maxChars?: number;
37
+ }): string;
38
+ /**
39
+ * 从模型答案里取出**一条**可直接填入的命令行。
40
+ *
41
+ * 只认**围栏代码块**:没有围栏就返回空串(客户端据此禁用「填入」)。刻意不去散文里
42
+ * 猜哪一行像命令——猜错的下场是往用户的 PTY 里写进一句解释文字,比不给按钮糟得多。
43
+ */
44
+ export declare function extractCommandFromAnswer(answer: string): string;
45
+ /**
46
+ * 把模型答案变成**界面正文用的纯文本**。
47
+ *
48
+ * 只做「去掉标记」,**不做「解释标记」**——模型输出是不可信文本,任何 Markdown 渲染都是
49
+ * 一条新攻击面;而原样显示又满屏是星号与围栏。所以折中成两条纯删减:
50
+ * - 丢掉围栏代码块:命令另有一行 `.tt_assistCmd` 专门展示(还带「填入」按钮),
51
+ * 正文里再来一遍是重复;
52
+ * - 去掉 `**` 强调标记。
53
+ * 顺带折掉连续空行(丢掉围栏后常留下整块空白)。
54
+ */
55
+ export declare function plainAnswerText(answer: string): string;
56
+ /** 上下文包输入(宿主侧从会话状态取材,见 index.ts 的 buildAssistContext)。 */
57
+ export interface AssistPromptInput {
58
+ shell: string;
59
+ cwd: string;
60
+ /** 上一条已完成命令的命令行(拿不到时省略)。 */
61
+ command?: string;
62
+ exitCode?: number | null;
63
+ /** 上一条命令的输出(B..D 捕获窗口,未压缩)。 */
64
+ output?: string;
65
+ /** 当前屏纯文本(tty_screen 产物);只在没有命令输出时兜底。 */
66
+ screen?: string;
67
+ /** 界面语言;模型据此决定用中文还是英文作答。 */
68
+ lang?: 'zh' | 'en';
69
+ }
70
+ export declare function systemPromptFor(lang: 'zh' | 'en' | undefined): string;
71
+ /**
72
+ * 组装一次解释请求。
73
+ *
74
+ * **命令输出为主,屏幕补它缺的东西**(见 screenAddsContext)。两段各有不可替代之处:
75
+ * 输出回答了「这条命令干了什么」,屏幕回答了「敲的是哪条命令」——后者恰恰是输出里
76
+ * 没有的(实测:`tty_capture{last}` 的正文只有 `ls: … No such file or directory`)。
77
+ *
78
+ * `input.command` 目前**没有调用方会填**:shell 集成桩只发 A/B/D/T,不带命令行,
79
+ * 所以这个字段留着等「桩改成 C 标记带命令」那一轮(见 ROADMAP)。
80
+ */
81
+ export declare function buildFailurePrompt(input: AssistPromptInput): {
82
+ system: string;
83
+ user: string;
84
+ };
package/lib/assist.js ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * 「失败即解释」的上下文包(0.24.0)。
3
+ *
4
+ * 这一层的职责只有一个:把终端现场压成**模型读得懂、又淹不掉它**的一小段文本。
5
+ * 它是这个功能真正的工程主体——`ctx.llm.stream` 那条路 rss 已经走通(连 finish 块的
6
+ * 形状、reasoning 模型的 max-tokens 拐点都有注释留档),难的是**喂什么进去**:
7
+ * 终端输出里全是 ANSI、\r 覆盖行、进度条刷屏与重复日志,直接丢给模型就是垃圾进垃圾出。
8
+ *
9
+ * 全部导出都是**纯函数**(不碰 ctx、不碰会话对象)——这是整个功能里唯一能被确定性
10
+ * 测住的部分,所以逻辑尽量往这里收。
11
+ */
12
+ import { cleanAnsi } from './ansi.js';
13
+ /** 命令输出尾部保留行数。 */
14
+ export const CONTEXT_TAIL_LINES = 40;
15
+ /** 上下文正文的字符上限(超出从头部丢弃,并注明丢了多少行)。 */
16
+ export const CONTEXT_MAX_CHARS = 6000;
17
+ /** 屏幕文本的行数上限(只在「没有命令输出」时才用屏幕兜底)。 */
18
+ export const SCREEN_TAIL_LINES = 30;
19
+ /**
20
+ * 这个退出码值不值得摆徽标。
21
+ *
22
+ * 0 不弹;`null` / `undefined`(没拿到退出码 = 命令边界不可信)也不弹——
23
+ * 宁可少弹一次,也不要在正常的提示符下摆一张「它失败了」的脸。
24
+ *
25
+ * **130 / 141 是刻意豁免的**:前者是 Ctrl-C(用户自己按的),后者是 SIGPIPE
26
+ * (`… | head` 的常态)。这两个天天出现,为它们弹徽标会让用户干脆把整个功能关掉。
27
+ */
28
+ export function shouldExplainExit(code) {
29
+ if (code === null || code === undefined)
30
+ return false;
31
+ if (code === 0)
32
+ return false;
33
+ return code !== 130 && code !== 141;
34
+ }
35
+ /**
36
+ * 外发前的轻量遮盖。
37
+ *
38
+ * 这不是「防住攻击者」,而是**兜住常识**:终端屏幕上出现密钥、token、连接串是家常便饭,
39
+ * 而这一段文本要离开本机。成本极低,但它决定了这个开关该怎么向用户交代。
40
+ *
41
+ * 只做**形态明确**的规则——宁可漏,不可错杀:把一段正常日志改成 [已遮盖] 比漏掉一个
42
+ * 罕见格式的密钥更伤信任(用户会以为功能坏了)。
43
+ */
44
+ const SECRET_RULES = [
45
+ { re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, to: '[已遮盖私钥]', why: 'PEM 私钥块' },
46
+ { re: /\bsk-[A-Za-z0-9_-]{8,}/g, to: 'sk-[已遮盖]', why: 'OpenAI 风格密钥' },
47
+ { re: /\bAKIA[0-9A-Z]{16}\b/g, to: 'AKIA[已遮盖]', why: 'AWS access key id' },
48
+ { re: /\bgh[pousr]_[A-Za-z0-9]{20,}/g, to: 'gh_[已遮盖]', why: 'GitHub token' },
49
+ { re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, to: '[已遮盖 JWT]', why: 'JWT' },
50
+ { re: /(\/\/[^/\s:@]+):[^/\s:@]+@/g, to: '$1:[已遮盖]@', why: 'URL 里的密码' },
51
+ {
52
+ re: /((?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|authorization|密码)\s*[:=]\s*)([^\s'"]{3,})/gi,
53
+ to: '$1[已遮盖]',
54
+ why: '键值对里的秘密',
55
+ },
56
+ ];
57
+ /** 规则表只读导出,供单测逐条枚举(新加一条规则必须同时加一条用例)。 */
58
+ export const SECRET_RULE_WHY = SECRET_RULES.map((rule) => rule.why);
59
+ export function maskSecrets(text) {
60
+ let out = text;
61
+ let count = 0;
62
+ for (const rule of SECRET_RULES) {
63
+ out = out.replace(rule.re, (...args) => {
64
+ count += 1;
65
+ // 手写 $1 展开:用了回调就不能再靠 String.replace 的替换串语法
66
+ return rule.to.replace(/\$(\d)/g, (_m, d) => String(args[Number(d)] ?? ''));
67
+ });
68
+ }
69
+ return { text: out, count };
70
+ }
71
+ /**
72
+ * 把一段终端输出压成上下文正文。
73
+ *
74
+ * 四步,每一步都对应一种真实的噪声:
75
+ * 1. 剥转义(颜色/光标/进度条转义)——不剥的话模型看到的是满屏控制码;
76
+ * 2. \r 覆盖收敛——`npm install` 那一类会把同一行重画几百次;
77
+ * 3. 折掉连续空行与**连续重复行**——重复日志(同一句 warning 刷 50 遍)是纯浪费预算;
78
+ * 4. 取尾部并按字符上限硬切——报错在末尾,且开头半行要丢掉(半行命令比没有更误导)。
79
+ *
80
+ * 头部被丢弃时**显式注明行数**:否则模型会把「输出第一行就是报错」当成事实。
81
+ */
82
+ export function compressTerminalText(raw, options) {
83
+ const maxLines = options?.maxLines ?? CONTEXT_TAIL_LINES;
84
+ const maxChars = options?.maxChars ?? CONTEXT_MAX_CHARS;
85
+ const lines = cleanAnsi(raw).split('\n').map((line) => line.replace(/[ \t]+$/, ''));
86
+ const kept = [];
87
+ for (const line of lines) {
88
+ const prev = kept[kept.length - 1];
89
+ if (line === '') {
90
+ if (prev === '' || prev === undefined)
91
+ continue;
92
+ }
93
+ else if (line === prev) {
94
+ continue;
95
+ }
96
+ kept.push(line);
97
+ }
98
+ while (kept.length > 0 && kept[kept.length - 1] === '')
99
+ kept.pop();
100
+ const joined = kept.join('\n');
101
+ let text = joined;
102
+ let byChars = false;
103
+ if (joined.length > maxChars) {
104
+ text = joined.slice(-maxChars);
105
+ const nl = text.indexOf('\n');
106
+ // 从一个整行开始,别把一行切成半截(半截命令会让模型编出后半段)
107
+ text = nl === -1 ? text : text.slice(nl + 1);
108
+ byChars = true;
109
+ }
110
+ if (!byChars && kept.length > maxLines) {
111
+ text = kept.slice(-maxLines).join('\n');
112
+ }
113
+ const shown = text === '' ? 0 : text.split('\n').length;
114
+ const omitted = kept.length - shown;
115
+ return omitted > 0 ? '…(前面 ' + String(omitted) + ' 行已省略)\n' + text : text;
116
+ }
117
+ /**
118
+ * 从模型答案里取出**一条**可直接填入的命令行。
119
+ *
120
+ * 只认**围栏代码块**:没有围栏就返回空串(客户端据此禁用「填入」)。刻意不去散文里
121
+ * 猜哪一行像命令——猜错的下场是往用户的 PTY 里写进一句解释文字,比不给按钮糟得多。
122
+ */
123
+ export function extractCommandFromAnswer(answer) {
124
+ const fence = /```[A-Za-z0-9_-]*\n([\s\S]*?)```/.exec(answer);
125
+ if (fence === null)
126
+ return '';
127
+ for (const raw of fence[1].split('\n')) {
128
+ const line = raw.trim();
129
+ /*
130
+ * 注释行**先判**。`#` 既是注释符、又是 root 提示符:反过来先剥提示符的话
131
+ * 「# 先看看包名」会被当成一条命令填进终端(这个坑是单测当场抓出来的——
132
+ * 见 test/assist-context.test.ts 的「围栏里只有空行/注释也是空串」)。
133
+ * 代价是 root 会话里 `# cmd` 形态的首行取不到,而系统提示本来就要求不要 $ 前缀。
134
+ */
135
+ if (line === '' || line.startsWith('#'))
136
+ continue;
137
+ return line.replace(/^[$>]\s+/, '');
138
+ }
139
+ return '';
140
+ }
141
+ /**
142
+ * 把模型答案变成**界面正文用的纯文本**。
143
+ *
144
+ * 只做「去掉标记」,**不做「解释标记」**——模型输出是不可信文本,任何 Markdown 渲染都是
145
+ * 一条新攻击面;而原样显示又满屏是星号与围栏。所以折中成两条纯删减:
146
+ * - 丢掉围栏代码块:命令另有一行 `.tt_assistCmd` 专门展示(还带「填入」按钮),
147
+ * 正文里再来一遍是重复;
148
+ * - 去掉 `**` 强调标记。
149
+ * 顺带折掉连续空行(丢掉围栏后常留下整块空白)。
150
+ */
151
+ export function plainAnswerText(answer) {
152
+ const withoutFence = answer.replace(/```[A-Za-z0-9_-]*\n[\s\S]*?```/g, '').replace(/```[A-Za-z0-9_-]*/g, '');
153
+ const kept = [];
154
+ for (const raw of withoutFence.split('\n')) {
155
+ const line = raw.replace(/\*\*/g, '').replace(/[ \t]+$/, '');
156
+ if (line === '' && (kept.length === 0 || kept[kept.length - 1] === ''))
157
+ continue;
158
+ kept.push(line);
159
+ }
160
+ while (kept.length > 0 && kept[kept.length - 1] === '')
161
+ kept.pop();
162
+ return kept.join('\n');
163
+ }
164
+ /**
165
+ * 系统提示。
166
+ *
167
+ * 三条硬约束写进去的原因:
168
+ * - **固定两节标题**:界面按节渲染,多出来的前言/总结只是噪声;
169
+ * - **命令放进围栏且每条一行**:客户端的「填入」按钮就是从围栏里取第一条
170
+ * (extractCommandFromAnswer),格式漂了这个按钮就哑了;
171
+ * - **不要编造路径/IP、拿不准就给一条查证命令**:终端里照着模型编的路径敲下去,
172
+ * 比不回答更糟。
173
+ */
174
+ const SYSTEM_ZH = [
175
+ '你是终端里的排错助手。用户刚在终端里跑了一条命令、它以非零状态结束(或屏幕上出现了报错)。',
176
+ '用最短的篇幅说清两件事:出了什么事、下一步该敲什么。',
177
+ '',
178
+ '输出格式(严格遵守,**纯文本**,界面不渲染 Markdown):',
179
+ '第一行就以「发生了什么:」开头,一到两句,说人话。不要复述输出、不要贴日志原文。',
180
+ '空一行后写「下一步:」,紧接一个 SH 围栏代码块(三个反引号 + sh),里面放一到两条可以直接粘贴执行的命令,每条命令单独一行,不要 $ 前缀、不要行内注释。',
181
+ '然后在代码块下方用一句话说明需要用户补什么(如果需要的话)。',
182
+ '',
183
+ '规则:',
184
+ '- **不要用 Markdown 加粗 / 标题 / 列表符号**(星号和井号会原样出现在界面上);只允许那个 SH 围栏代码块。',
185
+ '- 不要前言、不要总结、不要客套、不要问「需要我帮你做别的吗」。',
186
+ '- 需要用户填空的地方用 <尖括号> 标出;**不要编造具体的路径、文件名、IP、版本号**。',
187
+ '- 拿不准原因时直说拿不准,并给出**一条能查证它**的命令(看日志、看版本、看帮助)。',
188
+ '- 不要建议安装软件、不要建议改系统配置,除非错误信息明确指向它。',
189
+ '- 你没有执行能力:不要说自己运行了什么,也不要假设你能看到用户没给你的输出。',
190
+ '- 回答语言:中文。',
191
+ ].join('\n');
192
+ const SYSTEM_EN = [
193
+ 'You are a troubleshooting assistant inside a terminal panel. The user just ran a command that exited non-zero (or the screen shows an error).',
194
+ 'Explain, in as few words as possible: what went wrong, and what to type next.',
195
+ '',
196
+ 'Output format (strict, **plain text** — the panel does not render Markdown):',
197
+ 'Start with "What happened: " on the first line, one or two sentences, plain language. Do not restate the raw output.',
198
+ 'Then a blank line and "Next step: ", immediately followed by one SH fenced block (three backticks + sh) with one or two commands that can be pasted directly, one command per line, no $ prefix, no inline comments.',
199
+ 'Then one short line saying what the user still needs to fill in, if anything.',
200
+ '',
201
+ 'Rules:',
202
+ '- **No Markdown bold / headings / bullets** (asterisks and hashes show up literally in the panel); the single SH fenced block is the only exception.',
203
+ '- No preamble, no summary, no pleasantries, no "anything else I can help with".',
204
+ '- Mark blanks as <angle brackets>; never invent paths, file names, IPs or versions.',
205
+ '- If unsure, say so and give ONE command that would verify it (check logs, version, help).',
206
+ '- Do not suggest installing software or changing system config unless the error clearly points there.',
207
+ '- You cannot execute anything: never claim you ran something, and never assume output you were not given.',
208
+ '- Answer language: English.',
209
+ ].join('\n');
210
+ export function systemPromptFor(lang) {
211
+ return lang === 'en' ? SYSTEM_EN : SYSTEM_ZH;
212
+ }
213
+ /**
214
+ * 屏幕是否带来了输出里没有的东西。
215
+ *
216
+ * 判据是**尾段三行的包含关系**,不是「谁更有用」这种说不清的话:tmux 持久会话里
217
+ * `lastCommand.output` 本来就是 capture-pane 快照(OSC 133;T),与屏幕是同一份内容,
218
+ * 再发一遍纯属浪费 token;而普通会话里两者**互补**——输出是 B..D 精确捕获的(干净,
219
+ * 但**不含命令回显**:zsh 的 preexec 发出的 B 标记在用户输入回显之后,实测确认),
220
+ * 命令行只存在于屏幕上。
221
+ */
222
+ function screenAddsContext(output, screen) {
223
+ if (screen.trim() === '')
224
+ return false;
225
+ if (output.trim() === '')
226
+ return true;
227
+ const probe = screen.split('\n').filter((line) => line.trim() !== '').slice(-3);
228
+ if (probe.length === 0)
229
+ return false;
230
+ return !probe.every((line) => output.includes(line));
231
+ }
232
+ /**
233
+ * 组装一次解释请求。
234
+ *
235
+ * **命令输出为主,屏幕补它缺的东西**(见 screenAddsContext)。两段各有不可替代之处:
236
+ * 输出回答了「这条命令干了什么」,屏幕回答了「敲的是哪条命令」——后者恰恰是输出里
237
+ * 没有的(实测:`tty_capture{last}` 的正文只有 `ls: … No such file or directory`)。
238
+ *
239
+ * `input.command` 目前**没有调用方会填**:shell 集成桩只发 A/B/D/T,不带命令行,
240
+ * 所以这个字段留着等「桩改成 C 标记带命令」那一轮(见 ROADMAP)。
241
+ */
242
+ export function buildFailurePrompt(input) {
243
+ const out = compressTerminalText(input.output ?? '');
244
+ const screen = compressTerminalText(input.screen ?? '', { maxLines: SCREEN_TAIL_LINES });
245
+ const lines = [
246
+ '[终端现场]',
247
+ 'shell: ' + (input.shell !== '' ? input.shell : '(未知)'),
248
+ 'cwd: ' + (input.cwd !== '' ? input.cwd : '(未知)'),
249
+ ];
250
+ if (input.command !== undefined && input.command !== '')
251
+ lines.push('命令: ' + input.command);
252
+ if (input.exitCode !== undefined && input.exitCode !== null)
253
+ lines.push('退出码: ' + String(input.exitCode));
254
+ const hasOut = out.trim() !== '';
255
+ const hasScreen = screenAddsContext(out, screen);
256
+ if (hasOut)
257
+ lines.push('', '[命令输出]', out);
258
+ if (hasScreen)
259
+ lines.push('', '[当前屏幕(含你敲的命令行与提示符)]', screen);
260
+ if (!hasOut && !hasScreen)
261
+ lines.push('', '(没有拿到输出)');
262
+ const masked = maskSecrets(lines.join('\n'));
263
+ return { system: systemPromptFor(input.lang), user: masked.text };
264
+ }
265
+ //# sourceMappingURL=assist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assist.js","sourceRoot":"","sources":["../src/assist.ts"],"names":[],"mappings":"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"}