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.
- package/lib/index.js +41 -25
- package/package.json +4 -1
- 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
|
|
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()
|
|
82
|
-
var hit = list.some(function (entry) {
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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 =
|
|
139
|
+
horizontalBuffer = 56 // padding + borders + margin buffer
|
|
118
140
|
;
|
|
119
|
-
containerWidth = Math.max(600, Math.min(
|
|
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:
|
|
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.
|
|
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
|
|
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()
|
|
65
|
-
const hit = list.some(entry =>
|
|
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
|
|
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}
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
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:
|
|
204
|
+
padding: 8px 12px;
|
|
173
205
|
white-space: pre;
|
|
174
206
|
word-break: normal;
|
|
175
|
-
line-height: 1.
|
|
207
|
+
line-height: 1.18;
|
|
176
208
|
overflow-x: auto;
|
|
177
209
|
}
|
|
178
210
|
|
|
179
211
|
.command-line {
|
|
180
212
|
display: flex;
|
|
181
|
-
gap:
|
|
213
|
+
gap: 3px;
|
|
182
214
|
align-items: baseline;
|
|
183
|
-
margin-bottom:
|
|
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.
|
|
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">${
|
|
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
|
})
|