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