koishi-plugin-spawn-modified 1.1.13 → 1.1.14
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.d.ts +36 -0
- package/lib/index.js +231 -0
- package/package.json +1 -4
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
declare module 'koishi' {
|
|
3
|
+
interface Context {
|
|
4
|
+
puppeteer?: {
|
|
5
|
+
page(): Promise<any>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
}
|
|
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
|
+
export interface State {
|
|
24
|
+
command: string;
|
|
25
|
+
timeout: number;
|
|
26
|
+
output: string;
|
|
27
|
+
code?: number;
|
|
28
|
+
signal?: NodeJS.Signals;
|
|
29
|
+
timeUsed?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const name = "spawn";
|
|
32
|
+
export declare const inject: {
|
|
33
|
+
optional: string[];
|
|
34
|
+
};
|
|
35
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
36
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.inject = exports.name = exports.Config = void 0;
|
|
43
|
+
exports.apply = apply;
|
|
44
|
+
var child_process_1 = require("child_process");
|
|
45
|
+
var koishi_1 = require("koishi");
|
|
46
|
+
var path_1 = __importDefault(require("path"));
|
|
47
|
+
var url_1 = require("url");
|
|
48
|
+
var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
|
|
49
|
+
exports.Config = koishi_1.Schema.object({
|
|
50
|
+
root: koishi_1.Schema.string().description('工作路径。').default(''),
|
|
51
|
+
shell: koishi_1.Schema.string().description('运行命令的程序。'),
|
|
52
|
+
encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'),
|
|
53
|
+
timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute),
|
|
54
|
+
renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
|
|
55
|
+
blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
|
|
56
|
+
restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
|
|
57
|
+
authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
58
|
+
commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
59
|
+
commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
|
|
60
|
+
});
|
|
61
|
+
exports.name = 'spawn';
|
|
62
|
+
exports.inject = {
|
|
63
|
+
optional: ['puppeteer'],
|
|
64
|
+
};
|
|
65
|
+
// 当前工作目录状态管理
|
|
66
|
+
var sessionDirs = new Map();
|
|
67
|
+
// 命令过滤:支持黑名单/白名单模式
|
|
68
|
+
function isCommandBlocked(command, mode, list) {
|
|
69
|
+
if (!(list === null || list === void 0 ? void 0 : list.length))
|
|
70
|
+
return false;
|
|
71
|
+
var trimmedCommand = command.trim().toLowerCase();
|
|
72
|
+
var hit = list.some(function (entry) { return trimmedCommand.startsWith(entry.toLowerCase()); });
|
|
73
|
+
return mode === 'blacklist' ? hit : !hit;
|
|
74
|
+
}
|
|
75
|
+
// 解析 cd 命令并验证路径
|
|
76
|
+
function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
77
|
+
var cdMatch = command.trim().match(/^cd\s+(.+)$/i);
|
|
78
|
+
if (!cdMatch)
|
|
79
|
+
return { valid: true };
|
|
80
|
+
if (!restrictDirectory)
|
|
81
|
+
return { valid: true };
|
|
82
|
+
var targetPath = cdMatch[1].trim().replace(/['"]/g, '');
|
|
83
|
+
var absolutePath = path_1.default.resolve(currentDir, targetPath);
|
|
84
|
+
var normalizedRoot = path_1.default.resolve(rootDir);
|
|
85
|
+
// 检查目标路径是否在根目录内
|
|
86
|
+
if (!absolutePath.startsWith(normalizedRoot)) {
|
|
87
|
+
return { valid: false, error: 'restricted-directory' };
|
|
88
|
+
}
|
|
89
|
+
return { valid: true, newDir: absolutePath };
|
|
90
|
+
}
|
|
91
|
+
// 渲染终端输出为图片
|
|
92
|
+
function renderTerminalImage(ctx, workingDir, command, output) {
|
|
93
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
94
|
+
var fontPath, html, page, element, clip, screenshot;
|
|
95
|
+
return __generator(this, function (_a) {
|
|
96
|
+
switch (_a.label) {
|
|
97
|
+
case 0:
|
|
98
|
+
if (!ctx.puppeteer) {
|
|
99
|
+
throw new Error('Puppeteer plugin is not available');
|
|
100
|
+
}
|
|
101
|
+
fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
|
|
102
|
+
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-size: 14px;\n padding: 0;\n display: inline-block;\n min-width: 600px;\n max-width: 1200px;\n }\n \n .terminal {\n background: #1e1e1e;\n border: 1px solid #3c3c3c;\n border-radius: 8px;\n overflow: hidden;\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: 16px;\n white-space: pre-wrap;\n word-break: break-all;\n line-height: 1.5;\n }\n \n .prompt {\n color: #4ec9b0;\n margin-bottom: 8px;\n }\n \n .command {\n color: #dcdcaa;\n margin-bottom: 12px;\n }\n \n .output {\n color: #cccccc;\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=\"prompt\">").concat(escapeHtml(workingDir), "$</div>\n <div class=\"command\">").concat(escapeHtml(command), "</div>\n <div class=\"output\">").concat(escapeHtml(output), "</div>\n </div>\n </div>\n</body>\n</html>\n ");
|
|
103
|
+
return [4 /*yield*/, ctx.puppeteer.page()];
|
|
104
|
+
case 1:
|
|
105
|
+
page = _a.sent();
|
|
106
|
+
_a.label = 2;
|
|
107
|
+
case 2:
|
|
108
|
+
_a.trys.push([2, , 8, 10]);
|
|
109
|
+
return [4 /*yield*/, page.setContent(html)];
|
|
110
|
+
case 3:
|
|
111
|
+
_a.sent();
|
|
112
|
+
return [4 /*yield*/, page.waitForNetworkIdle({ timeout: 5000 })];
|
|
113
|
+
case 4:
|
|
114
|
+
_a.sent();
|
|
115
|
+
return [4 /*yield*/, page.$('.terminal')];
|
|
116
|
+
case 5:
|
|
117
|
+
element = _a.sent();
|
|
118
|
+
return [4 /*yield*/, element.boundingBox()];
|
|
119
|
+
case 6:
|
|
120
|
+
clip = _a.sent();
|
|
121
|
+
return [4 /*yield*/, page.screenshot({ clip: clip })];
|
|
122
|
+
case 7:
|
|
123
|
+
screenshot = _a.sent();
|
|
124
|
+
return [2 /*return*/, koishi_1.h.image(screenshot, 'image/png')];
|
|
125
|
+
case 8: return [4 /*yield*/, page.close()];
|
|
126
|
+
case 9:
|
|
127
|
+
_a.sent();
|
|
128
|
+
return [7 /*endfinally*/];
|
|
129
|
+
case 10: return [2 /*return*/];
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function escapeHtml(text) {
|
|
135
|
+
return text
|
|
136
|
+
.replace(/&/g, '&')
|
|
137
|
+
.replace(/</g, '<')
|
|
138
|
+
.replace(/>/g, '>')
|
|
139
|
+
.replace(/"/g, '"')
|
|
140
|
+
.replace(/'/g, ''');
|
|
141
|
+
}
|
|
142
|
+
function apply(ctx, config) {
|
|
143
|
+
var _this = this;
|
|
144
|
+
var _a;
|
|
145
|
+
ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
|
|
146
|
+
ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
|
|
147
|
+
.action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
|
|
148
|
+
var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, timeout, state;
|
|
149
|
+
var _this = this;
|
|
150
|
+
var _c;
|
|
151
|
+
var session = _b.session;
|
|
152
|
+
return __generator(this, function (_d) {
|
|
153
|
+
switch (_d.label) {
|
|
154
|
+
case 0:
|
|
155
|
+
if (!command)
|
|
156
|
+
return [2 /*return*/, session.text('.expect-text')];
|
|
157
|
+
command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
|
|
158
|
+
filterList = (((_c = config.commandList) === null || _c === void 0 ? void 0 : _c.length) ? config.commandList : config.blockedCommands) || [];
|
|
159
|
+
filterMode = config.commandFilterMode || 'blacklist';
|
|
160
|
+
if (isCommandBlocked(command, filterMode, filterList)) {
|
|
161
|
+
return [2 /*return*/, session.text('.blocked-command')];
|
|
162
|
+
}
|
|
163
|
+
sessionId = session.uid || session.channelId;
|
|
164
|
+
rootDir = path_1.default.resolve(ctx.baseDir, config.root);
|
|
165
|
+
currentDir = sessionDirs.get(sessionId) || rootDir;
|
|
166
|
+
cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
|
|
167
|
+
if (!cdValidation.valid) {
|
|
168
|
+
return [2 /*return*/, session.text('.restricted-directory')];
|
|
169
|
+
}
|
|
170
|
+
timeout = config.timeout;
|
|
171
|
+
state = { command: command, timeout: timeout, output: '' };
|
|
172
|
+
if (!!config.renderImage) return [3 /*break*/, 2];
|
|
173
|
+
return [4 /*yield*/, session.send(session.text('.started', state))];
|
|
174
|
+
case 1:
|
|
175
|
+
_d.sent();
|
|
176
|
+
_d.label = 2;
|
|
177
|
+
case 2: return [2 /*return*/, new Promise(function (resolve) {
|
|
178
|
+
var start = Date.now();
|
|
179
|
+
var child = (0, child_process_1.exec)(command, {
|
|
180
|
+
timeout: timeout,
|
|
181
|
+
cwd: currentDir,
|
|
182
|
+
encoding: config.encoding,
|
|
183
|
+
shell: config.shell,
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
});
|
|
186
|
+
child.stdout.on('data', function (data) {
|
|
187
|
+
state.output += data.toString();
|
|
188
|
+
});
|
|
189
|
+
child.stderr.on('data', function (data) {
|
|
190
|
+
state.output += data.toString();
|
|
191
|
+
});
|
|
192
|
+
child.on('close', function (code, signal) { return __awaiter(_this, void 0, void 0, function () {
|
|
193
|
+
var image, error_1;
|
|
194
|
+
return __generator(this, function (_a) {
|
|
195
|
+
switch (_a.label) {
|
|
196
|
+
case 0:
|
|
197
|
+
state.code = code;
|
|
198
|
+
state.signal = signal;
|
|
199
|
+
state.timeUsed = Date.now() - start;
|
|
200
|
+
state.output = state.output.trim();
|
|
201
|
+
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
202
|
+
if (cdValidation.newDir && code === 0) {
|
|
203
|
+
sessionDirs.set(sessionId, cdValidation.newDir);
|
|
204
|
+
}
|
|
205
|
+
if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 5];
|
|
206
|
+
_a.label = 1;
|
|
207
|
+
case 1:
|
|
208
|
+
_a.trys.push([1, 3, , 4]);
|
|
209
|
+
return [4 /*yield*/, renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')];
|
|
210
|
+
case 2:
|
|
211
|
+
image = _a.sent();
|
|
212
|
+
resolve(image);
|
|
213
|
+
return [3 /*break*/, 4];
|
|
214
|
+
case 3:
|
|
215
|
+
error_1 = _a.sent();
|
|
216
|
+
ctx.logger.error('Failed to render terminal image:', error_1);
|
|
217
|
+
resolve(session.text('.finished', state));
|
|
218
|
+
return [3 /*break*/, 4];
|
|
219
|
+
case 4: return [3 /*break*/, 6];
|
|
220
|
+
case 5:
|
|
221
|
+
resolve(session.text('.finished', state));
|
|
222
|
+
_a.label = 6;
|
|
223
|
+
case 6: return [2 /*return*/];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}); });
|
|
227
|
+
})];
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}); });
|
|
231
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-spawn-modified",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Run shell commands with Koishi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bot",
|
|
@@ -82,8 +82,5 @@
|
|
|
82
82
|
"puppeteer"
|
|
83
83
|
]
|
|
84
84
|
}
|
|
85
|
-
},
|
|
86
|
-
"dependencies": {
|
|
87
|
-
"koishi-plugin-spawn-modified": "^1.1.9"
|
|
88
85
|
}
|
|
89
86
|
}
|