koishi-plugin-spawn-modified 1.2.1 → 1.2.3

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.
Files changed (3) hide show
  1. package/lib/index.js +41 -25
  2. package/package.json +4 -1
  3. package/src/index.ts +47 -29
package/lib/index.js CHANGED
@@ -54,7 +54,7 @@ var child_process_1 = require("child_process");
54
54
  var koishi_1 = require("koishi");
55
55
  var path_1 = __importDefault(require("path"));
56
56
  var url_1 = require("url");
57
- var debug_log_1 = require("./debug-log");
57
+ var ansi_to_html_1 = __importDefault(require("ansi-to-html"));
58
58
  var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
59
59
  exports.Config = koishi_1.Schema.object({
60
60
  root: koishi_1.Schema.string().description('工作路径。').default(''),
@@ -75,11 +75,29 @@ exports.inject = {
75
75
  // 当前工作目录状态管理
76
76
  var sessionDirs = new Map();
77
77
  // 命令过滤:支持黑名单/白名单模式
78
+ function buildRegex(entry) {
79
+ try {
80
+ return new RegExp(entry, 'i');
81
+ }
82
+ catch (_) {
83
+ // 回退为逐字匹配,防止用户写了非法正则
84
+ var escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
85
+ try {
86
+ return new RegExp(escaped, 'i');
87
+ }
88
+ catch (_) {
89
+ return null;
90
+ }
91
+ }
92
+ }
78
93
  function isCommandBlocked(command, mode, list) {
79
94
  if (!(list === null || list === void 0 ? void 0 : list.length))
80
95
  return false;
81
- var trimmedCommand = command.trim().toLowerCase();
82
- var hit = list.some(function (entry) { return trimmedCommand.startsWith(entry.toLowerCase()); });
96
+ var trimmedCommand = command.trim();
97
+ var hit = list.some(function (entry) {
98
+ var regex = buildRegex(entry);
99
+ return regex ? regex.test(trimmedCommand) : false;
100
+ });
83
101
  return mode === 'blacklist' ? hit : !hit;
84
102
  }
85
103
  // 解析 cd 命令并验证路径
@@ -101,24 +119,36 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
101
119
  // 渲染终端输出为图片
102
120
  function renderTerminalImage(ctx, workingDir, command, output) {
103
121
  return __awaiter(this, void 0, void 0, function () {
104
- var displayOutput, lines, commandLineLength, maxLineLength, charWidth, horizontalBuffer, containerWidth, fontPath, html, page, element, screenshot;
122
+ var ansiStrip, normalizeTabs, displayOutputRaw, displayOutput, lines, commandLineLength, visibleLineLengths, maxLineLength, charWidth, horizontalBuffer, containerWidth, ansi, coloredOutputHtml, fontPath, html, page, element, screenshot;
105
123
  return __generator(this, function (_a) {
106
124
  switch (_a.label) {
107
125
  case 0:
108
126
  if (!ctx.puppeteer) {
109
127
  throw new Error('Puppeteer plugin is not available');
110
128
  }
111
- displayOutput = output || '(no output)';
129
+ ansiStrip = function (text) { return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, ''); };
130
+ normalizeTabs = function (text) { return text.replace(/\t/g, ' '); };
131
+ displayOutputRaw = normalizeTabs(output || '(no output)');
132
+ displayOutput = displayOutputRaw.replace(/^\s+/, '');
112
133
  lines = displayOutput.split(/\r?\n/);
113
- commandLineLength = "".concat(workingDir, "$ ").concat(command).length;
114
- maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], lines.map(function (line) { return line.length; }), false)) || commandLineLength;
115
- charWidth = 7.4 // approximate width of JetBrains Mono at 13px
134
+ commandLineLength = ansiStrip("".concat(workingDir, "$ ").concat(command)).length;
135
+ visibleLineLengths = lines.map(function (line) { return ansiStrip(line).length; });
136
+ maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], visibleLineLengths, false)) || commandLineLength;
137
+ charWidth = 7.1 // refined average width for JetBrains Mono 13px
116
138
  ;
117
- horizontalBuffer = 48 // padding + borders + margin buffer
139
+ horizontalBuffer = 56 // padding + borders + margin buffer
118
140
  ;
119
- containerWidth = Math.max(600, Math.min(1400, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
141
+ containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
142
+ ansi = new ansi_to_html_1.default({
143
+ fg: '#cccccc',
144
+ bg: '#1e1e1e',
145
+ newline: true,
146
+ escapeXML: true,
147
+ stream: false,
148
+ });
149
+ coloredOutputHtml = ansi.toHtml(displayOutput);
120
150
  fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
121
- html = "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <style>\n @font-face {\n font-family: 'JetBrains Mono';\n src: url('".concat(fontPath, "') format('truetype');\n }\n \n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n background: #1e1e1e;\n color: #cccccc;\n font-family: 'JetBrains Mono', 'Courier New', monospace;\n font-weight: 400;\n font-size: 13px;\n padding: 0;\n display: inline-block;\n width: ").concat(containerWidth, "px;\n }\n \n .terminal {\n background: #1e1e1e;\n border: 1px solid #3c3c3c;\n border-radius: 8px;\n overflow: hidden;\n width: 100%;\n }\n \n .title-bar {\n background: #2d2d2d;\n height: 35px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 12px;\n border-bottom: 1px solid #3c3c3c;\n }\n \n .title {\n color: #cccccc;\n font-size: 13px;\n font-weight: 500;\n }\n \n .buttons {\n display: flex;\n gap: 8px;\n }\n \n .button {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n }\n \n .button.minimize { background: #ffbd2e; }\n .button.maximize { background: #28c940; }\n .button.close { background: #ff5f56; }\n \n .content {\n padding: 10px 14px;\n white-space: pre;\n word-break: normal;\n line-height: 1.25;\n overflow-x: auto;\n }\n \n .command-line {\n display: flex;\n gap: 4px;\n align-items: baseline;\n margin-bottom: 6px;\n }\n \n .prompt {\n color: #4ec9b0;\n margin: 0;\n flex-shrink: 0;\n }\n \n .command {\n color: #dcdcaa;\n margin: 0;\n word-break: normal;\n flex: 1;\n }\n \n .output {\n color: #cccccc;\n line-height: 1.22;\n white-space: pre;\n word-break: normal;\n overflow-x: auto;\n }\n </style>\n</head>\n<body>\n <div class=\"terminal\">\n <div class=\"title-bar\">\n <div class=\"title\">Terminal</div>\n <div class=\"buttons\">\n <div class=\"button minimize\"></div>\n <div class=\"button maximize\"></div>\n <div class=\"button close\"></div>\n </div>\n </div>\n <div class=\"content\">\n <div class=\"command-line\">\n <div class=\"prompt\">").concat(escapeHtml(workingDir), "$</div>\n <div class=\"command\">").concat(escapeHtml(command), "</div>\n </div>\n <div class=\"output\">").concat(escapeHtml(displayOutput), "</div>\n </div>\n </div>\n</body>\n</html>\n ");
151
+ html = "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <style>\n @font-face {\n font-family: 'JetBrains Mono';\n src: url('".concat(fontPath, "') format('truetype');\n }\n \n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n background: #1e1e1e;\n color: #cccccc;\n font-family: 'JetBrains Mono', 'Courier New', monospace;\n font-weight: 400;\n font-size: 13px;\n padding: 0;\n display: inline-block;\n width: ").concat(containerWidth, "px;\n max-width: 1600px;\n min-width: 600px;\n }\n \n .terminal {\n background: #1e1e1e;\n border: 1px solid #3c3c3c;\n border-radius: 8px;\n overflow: hidden;\n width: 100%;\n }\n \n .title-bar {\n background: #2d2d2d;\n height: 35px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 12px;\n border-bottom: 1px solid #3c3c3c;\n }\n \n .title {\n color: #cccccc;\n font-size: 13px;\n font-weight: 500;\n }\n \n .buttons {\n display: flex;\n gap: 8px;\n }\n \n .button {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n }\n \n .button.minimize { background: #ffbd2e; }\n .button.maximize { background: #28c940; }\n .button.close { background: #ff5f56; }\n \n .content {\n padding: 8px 12px;\n white-space: pre;\n word-break: normal;\n line-height: 1.18;\n overflow-x: auto;\n }\n \n .command-line {\n display: flex;\n gap: 3px;\n align-items: baseline;\n margin-bottom: 2px;\n }\n \n .prompt {\n color: #4ec9b0;\n margin: 0;\n flex-shrink: 0;\n }\n \n .command {\n color: #dcdcaa;\n margin: 0;\n word-break: normal;\n flex: 1;\n }\n \n .output {\n color: #cccccc;\n line-height: 1.12;\n white-space: pre;\n word-break: normal;\n overflow-x: auto;\n }\n </style>\n</head>\n<body>\n <div class=\"terminal\">\n <div class=\"title-bar\">\n <div class=\"title\">Terminal</div>\n <div class=\"buttons\">\n <div class=\"button minimize\"></div>\n <div class=\"button maximize\"></div>\n <div class=\"button close\"></div>\n </div>\n </div>\n <div class=\"content\">\n <div class=\"command-line\">\n <div class=\"prompt\">").concat(escapeHtml(workingDir), "$</div>\n <div class=\"command\">").concat(escapeHtml(command), "</div>\n </div>\n <div class=\"output\">").concat(coloredOutputHtml, "</div>\n </div>\n </div>\n</body>\n</html>\n ");
122
152
  return [4 /*yield*/, ctx.puppeteer.page()];
123
153
  case 1:
124
154
  page = _a.sent();
@@ -168,32 +198,25 @@ function apply(ctx, config) {
168
198
  return __generator(this, function (_d) {
169
199
  switch (_d.label) {
170
200
  case 0:
171
- (0, debug_log_1.debugLog)(ctx, 'input', { command: command });
172
201
  if (!command) {
173
- (0, debug_log_1.debugLog)(ctx, 'expect-text', { text: session.text('.expect-text') });
174
202
  return [2 /*return*/, session.text('.expect-text')];
175
203
  }
176
204
  command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
177
- (0, debug_log_1.debugLog)(ctx, 'parsed-command', { command: command });
178
205
  filterList = (((_c = config.commandList) === null || _c === void 0 ? void 0 : _c.length) ? config.commandList : config.blockedCommands) || [];
179
206
  filterMode = config.commandFilterMode || 'blacklist';
180
207
  if (isCommandBlocked(command, filterMode, filterList)) {
181
- (0, debug_log_1.debugLog)(ctx, 'blocked-command', { command: command, filterMode: filterMode, filterList: filterList });
182
208
  return [2 /*return*/, session.text('.blocked-command')];
183
209
  }
184
210
  sessionId = session.uid || session.channelId;
185
211
  rootDir = path_1.default.resolve(ctx.baseDir, config.root);
186
212
  currentDir = sessionDirs.get(sessionId) || rootDir;
187
213
  cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
188
- (0, debug_log_1.debugLog)(ctx, 'cd-validation', { command: command, cdValidation: cdValidation });
189
214
  if (!cdValidation.valid) {
190
- (0, debug_log_1.debugLog)(ctx, 'restricted-directory', { text: session.text('.restricted-directory') });
191
215
  return [2 /*return*/, session.text('.restricted-directory')];
192
216
  }
193
217
  timeout = config.timeout;
194
218
  state = { command: command, timeout: timeout, output: '' };
195
219
  if (!!config.renderImage) return [3 /*break*/, 2];
196
- (0, debug_log_1.debugLog)(ctx, 'send-started', { text: session.text('.started', state) });
197
220
  return [4 /*yield*/, session.send(session.text('.started', state))];
198
221
  case 1:
199
222
  _d.sent();
@@ -208,11 +231,9 @@ function apply(ctx, config) {
208
231
  windowsHide: true,
209
232
  });
210
233
  child.stdout.on('data', function (data) {
211
- (0, debug_log_1.debugLog)(ctx, 'stdout', { data: data.toString() });
212
234
  state.output += data.toString();
213
235
  });
214
236
  child.stderr.on('data', function (data) {
215
- (0, debug_log_1.debugLog)(ctx, 'stderr', { data: data.toString() });
216
237
  state.output += data.toString();
217
238
  });
218
239
  child.on('close', function (code, signal) { return __awaiter(_this, void 0, void 0, function () {
@@ -224,11 +245,9 @@ function apply(ctx, config) {
224
245
  state.signal = signal;
225
246
  state.timeUsed = Date.now() - start;
226
247
  state.output = state.output.trim();
227
- (0, debug_log_1.debugLog)(ctx, 'close', { code: code, signal: signal, timeUsed: state.timeUsed, output: state.output });
228
248
  // 更新当前目录(如果是 cd 命令且执行成功)
229
249
  if (cdValidation.newDir && code === 0) {
230
250
  sessionDirs.set(sessionId, cdValidation.newDir);
231
- (0, debug_log_1.debugLog)(ctx, 'cd-updated', { sessionId: sessionId, newDir: cdValidation.newDir });
232
251
  }
233
252
  if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 5];
234
253
  _a.label = 1;
@@ -237,18 +256,15 @@ function apply(ctx, config) {
237
256
  return [4 /*yield*/, renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')];
238
257
  case 2:
239
258
  image = _a.sent();
240
- (0, debug_log_1.debugLog)(ctx, 'render-image-success');
241
259
  resolve(image);
242
260
  return [3 /*break*/, 4];
243
261
  case 3:
244
262
  error_1 = _a.sent();
245
263
  ctx.logger.error('Failed to render terminal image:', error_1);
246
- (0, debug_log_1.debugLog)(ctx, 'render-image-fail', { error: error_1 });
247
264
  resolve(session.text('.finished', state));
248
265
  return [3 /*break*/, 4];
249
266
  case 4: return [3 /*break*/, 6];
250
267
  case 5:
251
- (0, debug_log_1.debugLog)(ctx, 'send-finished', { text: session.text('.finished', state) });
252
268
  resolve(session.text('.finished', state));
253
269
  _a.label = 6;
254
270
  case 6: return [2 /*return*/];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
@@ -83,5 +83,8 @@
83
83
  "puppeteer"
84
84
  ]
85
85
  }
86
+ },
87
+ "dependencies": {
88
+ "ansi-to-html": "^0.7.2"
86
89
  }
87
90
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { exec } from 'child_process'
2
2
  import { Context, h, Schema, Time } from 'koishi'
3
3
  import path from 'path'
4
4
  import { pathToFileURL } from 'url'
5
- import { debugLog } from './debug-log'
5
+ import AnsiToHtml from 'ansi-to-html'
6
6
 
7
7
  declare module 'koishi' {
8
8
  interface Context {
@@ -59,10 +59,27 @@ export const inject = {
59
59
  const sessionDirs = new Map<string, string>()
60
60
 
61
61
  // 命令过滤:支持黑名单/白名单模式
62
+ function buildRegex(entry: string): RegExp | null {
63
+ try {
64
+ return new RegExp(entry, 'i')
65
+ } catch (_) {
66
+ // 回退为逐字匹配,防止用户写了非法正则
67
+ const escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
68
+ try {
69
+ return new RegExp(escaped, 'i')
70
+ } catch (_) {
71
+ return null
72
+ }
73
+ }
74
+ }
75
+
62
76
  function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list: string[]): boolean {
63
77
  if (!list?.length) return false
64
- const trimmedCommand = command.trim().toLowerCase()
65
- const hit = list.some(entry => trimmedCommand.startsWith(entry.toLowerCase()))
78
+ const trimmedCommand = command.trim()
79
+ const hit = list.some(entry => {
80
+ const regex = buildRegex(entry)
81
+ return regex ? regex.test(trimmedCommand) : false
82
+ })
66
83
  return mode === 'blacklist' ? hit : !hit
67
84
  }
68
85
 
@@ -91,13 +108,26 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
91
108
  throw new Error('Puppeteer plugin is not available')
92
109
  }
93
110
 
94
- const displayOutput = output || '(no output)'
111
+ const ansiStrip = (text: string) => text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
112
+ const normalizeTabs = (text: string) => text.replace(/\t/g, ' ')
113
+ const displayOutputRaw = normalizeTabs(output || '(no output)')
114
+ const displayOutput = displayOutputRaw.replace(/^\s+/, '')
95
115
  const lines = displayOutput.split(/\r?\n/)
96
- const commandLineLength = `${workingDir}$ ${command}`.length
97
- const maxLineLength = Math.max(commandLineLength, ...lines.map(line => line.length)) || commandLineLength
98
- const charWidth = 7.4 // approximate width of JetBrains Mono at 13px
99
- const horizontalBuffer = 48 // padding + borders + margin buffer
100
- const containerWidth = Math.max(600, Math.min(1400, Math.ceil(maxLineLength * charWidth + horizontalBuffer)))
116
+ const commandLineLength = ansiStrip(`${workingDir}$ ${command}`).length
117
+ const visibleLineLengths = lines.map(line => ansiStrip(line).length)
118
+ const maxLineLength = Math.max(commandLineLength, ...visibleLineLengths) || commandLineLength
119
+ const charWidth = 7.1 // refined average width for JetBrains Mono 13px
120
+ const horizontalBuffer = 56 // padding + borders + margin buffer
121
+ const containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)))
122
+
123
+ const ansi = new AnsiToHtml({
124
+ fg: '#cccccc',
125
+ bg: '#1e1e1e',
126
+ newline: true,
127
+ escapeXML: true,
128
+ stream: false,
129
+ })
130
+ const coloredOutputHtml = ansi.toHtml(displayOutput)
101
131
 
102
132
  const fontPath = pathToFileURL(path.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href
103
133
 
@@ -127,6 +157,8 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
127
157
  padding: 0;
128
158
  display: inline-block;
129
159
  width: ${containerWidth}px;
160
+ max-width: 1600px;
161
+ min-width: 600px;
130
162
  }
131
163
 
132
164
  .terminal {
@@ -169,18 +201,18 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
169
201
  .button.close { background: #ff5f56; }
170
202
 
171
203
  .content {
172
- padding: 10px 14px;
204
+ padding: 8px 12px;
173
205
  white-space: pre;
174
206
  word-break: normal;
175
- line-height: 1.25;
207
+ line-height: 1.18;
176
208
  overflow-x: auto;
177
209
  }
178
210
 
179
211
  .command-line {
180
212
  display: flex;
181
- gap: 4px;
213
+ gap: 3px;
182
214
  align-items: baseline;
183
- margin-bottom: 6px;
215
+ margin-bottom: 2px;
184
216
  }
185
217
 
186
218
  .prompt {
@@ -198,7 +230,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
198
230
 
199
231
  .output {
200
232
  color: #cccccc;
201
- line-height: 1.22;
233
+ line-height: 1.12;
202
234
  white-space: pre;
203
235
  word-break: normal;
204
236
  overflow-x: auto;
@@ -220,7 +252,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
220
252
  <div class="prompt">${escapeHtml(workingDir)}$</div>
221
253
  <div class="command">${escapeHtml(command)}</div>
222
254
  </div>
223
- <div class="output">${escapeHtml(displayOutput)}</div>
255
+ <div class="output">${coloredOutputHtml}</div>
224
256
  </div>
225
257
  </div>
226
258
  </body>
@@ -255,19 +287,15 @@ export function apply(ctx: Context, config: Config) {
255
287
 
256
288
  ctx.command('exec <command:text>', { authority: config.authority ?? 4 })
257
289
  .action(async ({ session }, command) => {
258
- debugLog(ctx, 'input', { command })
259
290
  if (!command) {
260
- debugLog(ctx, 'expect-text', { text: session.text('.expect-text') })
261
291
  return session.text('.expect-text')
262
292
  }
263
293
 
264
294
  command = h('', h.parse(command)).toString(true)
265
- debugLog(ctx, 'parsed-command', { command })
266
295
  // 检查命令过滤(黑/白名单)
267
296
  const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
268
297
  const filterMode = config.commandFilterMode || 'blacklist'
269
298
  if (isCommandBlocked(command, filterMode, filterList)) {
270
- debugLog(ctx, 'blocked-command', { command, filterMode, filterList })
271
299
  return session.text('.blocked-command')
272
300
  }
273
301
  const sessionId = session.uid || session.channelId
@@ -275,15 +303,12 @@ export function apply(ctx: Context, config: Config) {
275
303
  const currentDir = sessionDirs.get(sessionId) || rootDir
276
304
  // 验证 cd 命令
277
305
  const cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory)
278
- debugLog(ctx, 'cd-validation', { command, cdValidation })
279
306
  if (!cdValidation.valid) {
280
- debugLog(ctx, 'restricted-directory', { text: session.text('.restricted-directory') })
281
307
  return session.text('.restricted-directory')
282
308
  }
283
309
  const { timeout } = config
284
310
  const state: State = { command, timeout, output: '' }
285
311
  if (!config.renderImage) {
286
- debugLog(ctx, 'send-started', { text: session.text('.started', state) })
287
312
  await session.send(session.text('.started', state))
288
313
  }
289
314
  return new Promise((resolve) => {
@@ -296,11 +321,9 @@ export function apply(ctx: Context, config: Config) {
296
321
  windowsHide: true,
297
322
  })
298
323
  child.stdout.on('data', (data) => {
299
- debugLog(ctx, 'stdout', { data: data.toString() })
300
324
  state.output += data.toString()
301
325
  })
302
326
  child.stderr.on('data', (data) => {
303
- debugLog(ctx, 'stderr', { data: data.toString() })
304
327
  state.output += data.toString()
305
328
  })
306
329
  child.on('close', async (code, signal) => {
@@ -308,25 +331,20 @@ export function apply(ctx: Context, config: Config) {
308
331
  state.signal = signal
309
332
  state.timeUsed = Date.now() - start
310
333
  state.output = state.output.trim()
311
- debugLog(ctx, 'close', { code, signal, timeUsed: state.timeUsed, output: state.output })
312
334
  // 更新当前目录(如果是 cd 命令且执行成功)
313
335
  if (cdValidation.newDir && code === 0) {
314
336
  sessionDirs.set(sessionId, cdValidation.newDir)
315
- debugLog(ctx, 'cd-updated', { sessionId, newDir: cdValidation.newDir })
316
337
  }
317
338
  // 渲染为图片或返回文本
318
339
  if (config.renderImage && ctx.puppeteer) {
319
340
  try {
320
341
  const image = await renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')
321
- debugLog(ctx, 'render-image-success')
322
342
  resolve(image)
323
343
  } catch (error) {
324
344
  ctx.logger.error('Failed to render terminal image:', error)
325
- debugLog(ctx, 'render-image-fail', { error })
326
345
  resolve(session.text('.finished', state))
327
346
  }
328
347
  } else {
329
- debugLog(ctx, 'send-finished', { text: session.text('.finished', state) })
330
348
  resolve(session.text('.finished', state))
331
349
  }
332
350
  })