koishi-plugin-spawn-modified 1.2.6 → 1.2.8

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.
@@ -0,0 +1,18 @@
1
+ import { Schema } from 'koishi';
2
+ declare const encodings: readonly ["utf8", "utf16le", "latin1", "ucs2"];
3
+ export interface Config {
4
+ root?: string;
5
+ shell?: string;
6
+ encoding?: typeof encodings[number];
7
+ timeout?: number;
8
+ debug?: boolean;
9
+ renderImage?: boolean;
10
+ exemptUsers?: string[];
11
+ blockedCommands?: string[];
12
+ restrictDirectory?: boolean;
13
+ authority?: number;
14
+ commandFilterMode?: 'blacklist' | 'whitelist';
15
+ commandList?: string[];
16
+ }
17
+ export declare const Config: Schema<Config>;
18
+ export {};
package/lib/config.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Config = void 0;
4
+ var koishi_1 = require("koishi");
5
+ var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
6
+ exports.Config = koishi_1.Schema.object({
7
+ root: koishi_1.Schema.string().description('工作路径。').default(''),
8
+ shell: koishi_1.Schema.string().description('运行命令的程序。'),
9
+ encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'),
10
+ timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute),
11
+ debug: koishi_1.Schema.boolean().description('开启调试模式,将群组ID、用户ID等信息输出到日志。').default(false),
12
+ renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
13
+ exemptUsers: koishi_1.Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
14
+ blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
15
+ restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
16
+ authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
17
+ commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
18
+ commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
19
+ });
package/lib/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { Context, Schema } from 'koishi';
1
+ import { Context } from 'koishi';
2
+ import { Config } from './config';
3
+ export { Config } from './config';
2
4
  declare module 'koishi' {
3
5
  interface Context {
4
6
  puppeteer?: {
@@ -6,20 +8,6 @@ declare module 'koishi' {
6
8
  };
7
9
  }
8
10
  }
9
- declare const encodings: readonly ["utf8", "utf16le", "latin1", "ucs2"];
10
- export interface Config {
11
- root?: string;
12
- shell?: string;
13
- encoding?: typeof encodings[number];
14
- timeout?: number;
15
- renderImage?: boolean;
16
- blockedCommands?: string[];
17
- restrictDirectory?: boolean;
18
- authority?: number;
19
- commandFilterMode?: 'blacklist' | 'whitelist';
20
- commandList?: string[];
21
- }
22
- export declare const Config: Schema<Config>;
23
11
  export interface State {
24
12
  command: string;
25
13
  timeout: number;
@@ -33,4 +21,3 @@ export declare const inject: {
33
21
  optional: string[];
34
22
  };
35
23
  export declare function apply(ctx: Context, config: Config): void;
36
- export {};
package/lib/index.js CHANGED
@@ -35,15 +35,6 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
36
  }
37
37
  };
38
- var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
- if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
- if (ar || !(i in from)) {
41
- if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
- ar[i] = from[i];
43
- }
44
- }
45
- return to.concat(ar || Array.prototype.slice.call(from));
46
- };
47
38
  var __importDefault = (this && this.__importDefault) || function (mod) {
48
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
49
40
  };
@@ -52,306 +43,61 @@ exports.inject = exports.name = exports.Config = void 0;
52
43
  exports.apply = apply;
53
44
  var child_process_1 = require("child_process");
54
45
  var koishi_1 = require("koishi");
55
- var os_1 = __importDefault(require("os"));
56
46
  var path_1 = __importDefault(require("path"));
57
- var url_1 = require("url");
58
- var ansi_to_html_1 = __importDefault(require("ansi-to-html"));
59
- var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
60
- exports.Config = koishi_1.Schema.object({
61
- root: koishi_1.Schema.string().description('工作路径。').default(''),
62
- shell: koishi_1.Schema.string().description('运行命令的程序。'),
63
- encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'),
64
- timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute),
65
- renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
66
- blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
67
- restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
68
- authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
69
- commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
70
- commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
71
- });
47
+ var utils_1 = require("./utils");
48
+ var render_1 = require("./render");
49
+ var logger_1 = require("./logger");
50
+ // Re-export config for plugin registration
51
+ var config_1 = require("./config");
52
+ Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return config_1.Config; } });
72
53
  exports.name = 'spawn';
73
54
  exports.inject = {
74
55
  optional: ['puppeteer'],
75
56
  };
76
57
  // 当前工作目录状态管理
77
58
  var sessionDirs = new Map();
78
- // 命令过滤:支持黑名单/白名单模式
79
- function buildRegex(entry) {
80
- try {
81
- return new RegExp(entry, 'i');
82
- }
83
- catch (_) {
84
- // 回退为逐字匹配,防止用户写了非法正则
85
- var escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
- try {
87
- return new RegExp(escaped, 'i');
88
- }
89
- catch (_) {
90
- return null;
91
- }
92
- }
93
- }
94
- function isCommandBlocked(command, mode, list) {
95
- if (!(list === null || list === void 0 ? void 0 : list.length))
96
- return false;
97
- var trimmedCommand = command.trim();
98
- var hit = list.some(function (entry) {
99
- var regex = buildRegex(entry);
100
- return regex ? regex.test(trimmedCommand) : false;
101
- });
102
- return mode === 'blacklist' ? hit : !hit;
103
- }
104
- function stripQuotes(text) {
105
- return text.replace(/^['"]|['"]$/g, '');
106
- }
107
- function tokenizeCommand(command) {
108
- var tokens = [];
109
- var current = '';
110
- var quote = null;
111
- for (var i = 0; i < command.length; i++) {
112
- var char = command[i];
113
- if ((char === '"' || char === "'") && (quote === null || quote === char)) {
114
- quote = quote ? null : char;
115
- continue;
116
- }
117
- if (!quote && /\s/.test(char)) {
118
- if (current) {
119
- tokens.push(current);
120
- current = '';
121
- }
122
- continue;
123
- }
124
- current += char;
125
- }
126
- if (current)
127
- tokens.push(current);
128
- return tokens;
129
- }
130
- function isPathLike(token) {
131
- var trimmed = token.trim();
132
- if (!trimmed)
133
- return false;
134
- if (/^[|&><]+$/.test(trimmed))
135
- return false;
136
- if (/^-{1,2}[a-zA-Z0-9][\w-]*$/.test(trimmed))
137
- return false;
138
- if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed))
139
- return false;
140
- var normalized = stripQuotes(trimmed);
141
- return (/^[A-Za-z]:[\\/]/.test(normalized) ||
142
- normalized.startsWith('/') ||
143
- normalized.startsWith('~') ||
144
- normalized.startsWith('..') ||
145
- normalized.startsWith('./') ||
146
- normalized.includes('/') ||
147
- normalized.includes('\\'));
148
- }
149
- function resolveCandidatePath(candidate, currentDir) {
150
- var _a;
151
- var cleaned = stripQuotes(candidate.trim());
152
- var homeDir = ((_a = os_1.default.homedir) === null || _a === void 0 ? void 0 : _a.call(os_1.default)) || '';
153
- if (cleaned.startsWith('~')) {
154
- var withoutTilde = cleaned.slice(1).replace(/^[/\\]/, '');
155
- var homeResolved = homeDir ? path_1.default.join(homeDir, withoutTilde) : cleaned;
156
- return path_1.default.resolve(homeResolved);
157
- }
158
- return path_1.default.resolve(currentDir, cleaned);
159
- }
160
- function extractPathCandidates(command) {
161
- var tokens = tokenizeCommand(command);
162
- var candidates = [];
163
- for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
164
- var token = tokens_1[_i];
165
- var normalized = stripQuotes(token);
166
- if (isPathLike(normalized)) {
167
- candidates.push(normalized);
168
- continue;
169
- }
170
- var eqIndex = normalized.indexOf('=');
171
- if (eqIndex > 0) {
172
- var value = normalized.slice(eqIndex + 1);
173
- if (isPathLike(value)) {
174
- candidates.push(value);
175
- }
176
- }
177
- }
178
- return candidates;
179
- }
180
- // 解析 cd 命令并验证路径
181
- function isWithinRoot(rootDir, targetPath) {
182
- var relative = path_1.default.relative(rootDir, targetPath);
183
- return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
184
- }
185
- function validatePathAccess(command, currentDir, rootDir, restrictDirectory) {
186
- if (!restrictDirectory)
187
- return { valid: true };
188
- var normalizedRoot = path_1.default.resolve(rootDir);
189
- var candidates = extractPathCandidates(command);
190
- for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
191
- var candidate = candidates_1[_i];
192
- var resolved = resolveCandidatePath(candidate, currentDir);
193
- if (!isWithinRoot(normalizedRoot, resolved)) {
194
- return { valid: false, error: 'restricted-path' };
195
- }
196
- }
197
- return { valid: true };
198
- }
199
- function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
200
- if (!restrictDirectory)
201
- return { valid: true };
202
- var normalizedRoot = path_1.default.resolve(rootDir);
203
- var cdMatches = [];
204
- var cdRegex = /\bcd\s+([^;&|\n]+)/gi;
205
- var m;
206
- while ((m = cdRegex.exec(command)) !== null) {
207
- cdMatches.push(m);
208
- }
209
- if (!cdMatches.length)
210
- return { valid: true };
211
- // 若命令被链式运算符分隔且包含 cd,则要求所有 cd 目标都在指定 root 下,否则拒绝
212
- for (var _i = 0, cdMatches_1 = cdMatches; _i < cdMatches_1.length; _i++) {
213
- var match = cdMatches_1[_i];
214
- var target = match[1].trim().replace(/['"]/g, '');
215
- var absolutePath = path_1.default.resolve(currentDir, target);
216
- if (!isWithinRoot(normalizedRoot, absolutePath)) {
217
- return { valid: false, error: 'restricted-directory' };
218
- }
219
- }
220
- // 仅当命令是单独的 cd 时才更新会话目录,避免链式命令切换目录后执行其他操作
221
- var singleCdOnly = /^\s*cd\s+[^;&|\n]+\s*$/i.test(command);
222
- if (singleCdOnly) {
223
- var target = cdMatches[0][1].trim().replace(/['"]/g, '');
224
- var absolutePath = path_1.default.resolve(currentDir, target);
225
- return { valid: true, newDir: absolutePath };
226
- }
227
- return { valid: true };
228
- }
229
- function maskCurlOutput(command, output) {
230
- if (!output)
231
- return output;
232
- if (!/\bcurl\b/i.test(command))
233
- return output;
234
- var ipv4Regex = /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g;
235
- return output.replace(ipv4Regex, function (ip) { return (isPrivateIpv4(ip) ? ip : '*.*.*.*'); });
236
- }
237
- function isPrivateIpv4(ip) {
238
- var octets = ip.split('.').map(Number);
239
- if (octets.length !== 4)
240
- return false;
241
- if (octets.some(function (octet) { return Number.isNaN(octet) || octet < 0 || octet > 255; }))
242
- return false;
243
- var a = octets[0], b = octets[1];
244
- if (a === 10)
245
- return true;
246
- if (a === 172 && b >= 16 && b <= 31)
247
- return true;
248
- if (a === 192 && b === 168)
249
- return true;
250
- if (a === 127)
251
- return true;
252
- if (a === 169 && b === 254)
253
- return true;
254
- return false;
255
- }
256
- // 渲染终端输出为图片
257
- function renderTerminalImage(ctx, workingDir, command, output) {
258
- return __awaiter(this, void 0, void 0, function () {
259
- var ansiStrip, normalizeTabs, displayOutputRaw, displayOutput, lines, commandLineLength, visibleLineLengths, maxLineLength, charWidth, horizontalBuffer, containerWidth, ansi, coloredOutputHtml, fontPath, html, page, element, screenshot;
260
- return __generator(this, function (_a) {
261
- switch (_a.label) {
262
- case 0:
263
- if (!ctx.puppeteer) {
264
- throw new Error('Puppeteer plugin is not available');
265
- }
266
- ansiStrip = function (text) { return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, ''); };
267
- normalizeTabs = function (text) { return text.replace(/\t/g, ' '); };
268
- displayOutputRaw = normalizeTabs(output || '(no output)');
269
- displayOutput = displayOutputRaw.replace(/^\s+/, '');
270
- lines = displayOutput.split(/\r?\n/);
271
- commandLineLength = ansiStrip("".concat(workingDir, "$ ").concat(command)).length;
272
- visibleLineLengths = lines.map(function (line) { return ansiStrip(line).length; });
273
- maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], visibleLineLengths, false)) || commandLineLength;
274
- charWidth = 7.1 // refined average width for JetBrains Mono 13px
275
- ;
276
- horizontalBuffer = 56 // padding + borders + margin buffer
277
- ;
278
- containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
279
- ansi = new ansi_to_html_1.default({
280
- fg: '#cccccc',
281
- bg: '#1e1e1e',
282
- newline: true,
283
- escapeXML: true,
284
- stream: false,
285
- });
286
- coloredOutputHtml = ansi.toHtml(displayOutput);
287
- fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
288
- 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 ");
289
- return [4 /*yield*/, ctx.puppeteer.page()];
290
- case 1:
291
- page = _a.sent();
292
- _a.label = 2;
293
- case 2:
294
- _a.trys.push([2, , 7, 9]);
295
- return [4 /*yield*/, page.setContent(html)];
296
- case 3:
297
- _a.sent();
298
- return [4 /*yield*/, page.waitForNetworkIdle({ timeout: 5000 })];
299
- case 4:
300
- _a.sent();
301
- return [4 /*yield*/, page.$('.terminal')];
302
- case 5:
303
- element = _a.sent();
304
- return [4 /*yield*/, element.screenshot({ type: 'png' })];
305
- case 6:
306
- screenshot = _a.sent();
307
- return [2 /*return*/, koishi_1.h.image(screenshot, 'image/png')];
308
- case 7: return [4 /*yield*/, page.close()];
309
- case 8:
310
- _a.sent();
311
- return [7 /*endfinally*/];
312
- case 9: return [2 /*return*/];
313
- }
314
- });
315
- });
316
- }
317
- function escapeHtml(text) {
318
- return text
319
- .replace(/&/g, '&amp;')
320
- .replace(/</g, '&lt;')
321
- .replace(/>/g, '&gt;')
322
- .replace(/"/g, '&quot;')
323
- .replace(/'/g, '&#039;');
324
- }
325
59
  function apply(ctx, config) {
326
60
  var _this = this;
327
61
  var _a;
328
62
  ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
329
63
  ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
330
64
  .action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
331
- var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
65
+ var guildId, userId, userKey, isExempt, sessionId, rootDir, currentDir, filterList, filterMode, cdValidation, pathValidation, timeout, state;
332
66
  var _this = this;
333
- var _c;
67
+ var _c, _d, _e;
334
68
  var session = _b.session;
335
- return __generator(this, function (_d) {
336
- switch (_d.label) {
69
+ return __generator(this, function (_f) {
70
+ switch (_f.label) {
337
71
  case 0:
338
72
  if (!command) {
339
73
  return [2 /*return*/, session.text('.expect-text')];
340
74
  }
341
75
  command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
342
- filterList = (((_c = config.commandList) === null || _c === void 0 ? void 0 : _c.length) ? config.commandList : config.blockedCommands) || [];
343
- filterMode = config.commandFilterMode || 'blacklist';
344
- if (isCommandBlocked(command, filterMode, filterList)) {
345
- return [2 /*return*/, session.text('.blocked-command')];
346
- }
76
+ guildId = session.guildId || '0';
77
+ userId = session.userId || '';
78
+ userKey = "".concat(guildId, ":").concat(userId);
79
+ isExempt = (_d = (_c = config.exemptUsers) === null || _c === void 0 ? void 0 : _c.some(function (entry) { return entry === userKey; })) !== null && _d !== void 0 ? _d : false;
347
80
  sessionId = session.uid || session.channelId;
348
81
  rootDir = path_1.default.resolve(ctx.baseDir, config.root);
349
82
  currentDir = sessionDirs.get(sessionId) || rootDir;
350
- cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
83
+ // 输出调试信息
84
+ (0, logger_1.debugLog)(ctx, config, {
85
+ guildId: guildId,
86
+ userId: userId,
87
+ command: command,
88
+ isExempt: isExempt,
89
+ currentDir: currentDir,
90
+ });
91
+ filterList = (((_e = config.commandList) === null || _e === void 0 ? void 0 : _e.length) ? config.commandList : config.blockedCommands) || [];
92
+ filterMode = config.commandFilterMode || 'blacklist';
93
+ if (!isExempt && (0, utils_1.isCommandBlocked)(command, filterMode, filterList)) {
94
+ return [2 /*return*/, session.text('.blocked-command')];
95
+ }
96
+ cdValidation = (0, utils_1.validateCdCommand)(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
351
97
  if (!cdValidation.valid) {
352
98
  return [2 /*return*/, session.text('.restricted-directory')];
353
99
  }
354
- pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory);
100
+ pathValidation = (0, utils_1.validatePathAccess)(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
355
101
  if (!pathValidation.valid) {
356
102
  return [2 /*return*/, session.text('.restricted-path')];
357
103
  }
@@ -360,8 +106,8 @@ function apply(ctx, config) {
360
106
  if (!!config.renderImage) return [3 /*break*/, 2];
361
107
  return [4 /*yield*/, session.send(session.text('.started', state))];
362
108
  case 1:
363
- _d.sent();
364
- _d.label = 2;
109
+ _f.sent();
110
+ _f.label = 2;
365
111
  case 2: return [2 /*return*/, new Promise(function (resolve) {
366
112
  var start = Date.now();
367
113
  var child = (0, child_process_1.exec)(command, {
@@ -385,7 +131,9 @@ function apply(ctx, config) {
385
131
  state.code = code;
386
132
  state.signal = signal;
387
133
  state.timeUsed = Date.now() - start;
388
- state.output = maskCurlOutput(command, state.output.trim());
134
+ state.output = (0, utils_1.maskCurlOutput)(command, state.output.trim());
135
+ // 输出执行结果调试信息
136
+ (0, logger_1.debugLogResult)(ctx, config, code, state.timeUsed);
389
137
  // 更新当前目录(如果是 cd 命令且执行成功)
390
138
  if (cdValidation.newDir && code === 0) {
391
139
  sessionDirs.set(sessionId, cdValidation.newDir);
@@ -394,7 +142,7 @@ function apply(ctx, config) {
394
142
  _a.label = 1;
395
143
  case 1:
396
144
  _a.trys.push([1, 3, , 4]);
397
- return [4 /*yield*/, renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')];
145
+ return [4 /*yield*/, (0, render_1.renderTerminalImage)(ctx, currentDir, command, state.output || '(no output)')];
398
146
  case 2:
399
147
  image = _a.sent();
400
148
  resolve(image);
@@ -0,0 +1,11 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './config';
3
+ export interface DebugInfo {
4
+ guildId: string;
5
+ userId: string;
6
+ command: string;
7
+ isExempt: boolean;
8
+ currentDir: string;
9
+ }
10
+ export declare function debugLog(ctx: Context, config: Config, info: DebugInfo): void;
11
+ export declare function debugLogResult(ctx: Context, config: Config, code: number | undefined, timeUsed: number): void;
package/lib/logger.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.debugLog = debugLog;
4
+ exports.debugLogResult = debugLogResult;
5
+ var koishi_1 = require("koishi");
6
+ var logger = new koishi_1.Logger('spawn');
7
+ function debugLog(ctx, config, info) {
8
+ if (!config.debug)
9
+ return;
10
+ logger.info("[DEBUG] \u7FA4\u7EC4ID: ".concat(info.guildId, ", \u7528\u6237ID: ").concat(info.userId));
11
+ logger.info("[DEBUG] \u547D\u4EE4: ".concat(info.command));
12
+ logger.info("[DEBUG] \u4F8B\u5916\u7528\u6237: ".concat(info.isExempt ? '是' : '否'));
13
+ logger.info("[DEBUG] \u5DE5\u4F5C\u76EE\u5F55: ".concat(info.currentDir));
14
+ }
15
+ function debugLogResult(ctx, config, code, timeUsed) {
16
+ if (!config.debug)
17
+ return;
18
+ logger.info("[DEBUG] \u6267\u884C\u7ED3\u679C: \u9000\u51FA\u7801=".concat(code, ", \u8017\u65F6=").concat(timeUsed, "ms"));
19
+ }
@@ -0,0 +1,2 @@
1
+ import { Context, h } from 'koishi';
2
+ export declare function renderTerminalImage(ctx: Context, workingDir: string, command: string, output: string): Promise<h>;
package/lib/render.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.renderTerminalImage = renderTerminalImage;
52
+ var koishi_1 = require("koishi");
53
+ var path_1 = __importDefault(require("path"));
54
+ var url_1 = require("url");
55
+ var ansi_to_html_1 = __importDefault(require("ansi-to-html"));
56
+ var utils_1 = require("./utils");
57
+ // 渲染终端输出为图片
58
+ function renderTerminalImage(ctx, workingDir, command, output) {
59
+ return __awaiter(this, void 0, void 0, function () {
60
+ var ansiStrip, normalizeTabs, displayOutputRaw, displayOutput, lines, commandLineLength, visibleLineLengths, maxLineLength, charWidth, horizontalBuffer, containerWidth, ansi, coloredOutputHtml, fontPath, html, page, element, screenshot;
61
+ return __generator(this, function (_a) {
62
+ switch (_a.label) {
63
+ case 0:
64
+ if (!ctx.puppeteer) {
65
+ throw new Error('Puppeteer plugin is not available');
66
+ }
67
+ ansiStrip = function (text) { return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, ''); };
68
+ normalizeTabs = function (text) { return text.replace(/\t/g, ' '); };
69
+ displayOutputRaw = normalizeTabs(output || '(no output)');
70
+ displayOutput = displayOutputRaw.replace(/^\s+/, '');
71
+ lines = displayOutput.split(/\r?\n/);
72
+ commandLineLength = ansiStrip("".concat(workingDir, "$ ").concat(command)).length;
73
+ visibleLineLengths = lines.map(function (line) { return ansiStrip(line).length; });
74
+ maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], visibleLineLengths, false)) || commandLineLength;
75
+ charWidth = 7.1 // refined average width for JetBrains Mono 13px
76
+ ;
77
+ horizontalBuffer = 56 // padding + borders + margin buffer
78
+ ;
79
+ containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
80
+ ansi = new ansi_to_html_1.default({
81
+ fg: '#cccccc',
82
+ bg: '#1e1e1e',
83
+ newline: true,
84
+ escapeXML: true,
85
+ stream: false,
86
+ });
87
+ coloredOutputHtml = ansi.toHtml(displayOutput);
88
+ fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
89
+ 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((0, utils_1.escapeHtml)(workingDir), "$</div>\n <div class=\"command\">").concat((0, utils_1.escapeHtml)(command), "</div>\n </div>\n <div class=\"output\">").concat(coloredOutputHtml, "</div>\n </div>\n </div>\n</body>\n</html>\n ");
90
+ return [4 /*yield*/, ctx.puppeteer.page()];
91
+ case 1:
92
+ page = _a.sent();
93
+ _a.label = 2;
94
+ case 2:
95
+ _a.trys.push([2, , 7, 9]);
96
+ return [4 /*yield*/, page.setContent(html)];
97
+ case 3:
98
+ _a.sent();
99
+ return [4 /*yield*/, page.waitForNetworkIdle({ timeout: 5000 })];
100
+ case 4:
101
+ _a.sent();
102
+ return [4 /*yield*/, page.$('.terminal')];
103
+ case 5:
104
+ element = _a.sent();
105
+ return [4 /*yield*/, element.screenshot({ type: 'png' })];
106
+ case 6:
107
+ screenshot = _a.sent();
108
+ return [2 /*return*/, koishi_1.h.image(screenshot, 'image/png')];
109
+ case 7: return [4 /*yield*/, page.close()];
110
+ case 8:
111
+ _a.sent();
112
+ return [7 /*endfinally*/];
113
+ case 9: return [2 /*return*/];
114
+ }
115
+ });
116
+ });
117
+ }
package/lib/utils.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export declare function buildRegex(entry: string): RegExp | null;
2
+ export declare function isCommandBlocked(command: string, mode: 'blacklist' | 'whitelist', list: string[]): boolean;
3
+ export declare function stripQuotes(text: string): string;
4
+ export declare function tokenizeCommand(command: string): string[];
5
+ export declare function isPathLike(token: string): boolean;
6
+ export declare function resolveCandidatePath(candidate: string, currentDir: string): string;
7
+ export declare function extractPathCandidates(command: string): string[];
8
+ export declare function isWithinRoot(rootDir: string, targetPath: string): boolean;
9
+ export declare function validatePathAccess(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): {
10
+ valid: boolean;
11
+ error?: string;
12
+ };
13
+ export declare function validateCdCommand(command: string, currentDir: string, rootDir: string, restrictDirectory: boolean): {
14
+ valid: boolean;
15
+ newDir?: string;
16
+ error?: string;
17
+ };
18
+ export declare function maskCurlOutput(command: string, output: string): string;
19
+ export declare function isPrivateIpv4(ip: string): boolean;
20
+ export declare function escapeHtml(text: string): string;