koishi-plugin-spawn-modified 1.2.8 → 1.3.0
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 +1 -0
- package/lib/config.js +14 -14
- package/lib/index.js +112 -11
- package/lib/locales/zh-CN.json +8 -0
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/index.ts +92 -0
- package/src/locales/zh-CN.yml +8 -0
package/lib/config.d.ts
CHANGED
package/lib/config.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
14
|
exports.Config = void 0;
|
|
4
15
|
var koishi_1 = require("koishi");
|
|
5
16
|
var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
|
|
6
|
-
exports.Config = koishi_1.Schema.object({
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
});
|
|
17
|
+
exports.Config = koishi_1.Schema.object(__assign({ root: koishi_1.Schema.string().description('工作路径。').default(''), shell: koishi_1.Schema.string().description('运行命令的程序。'), encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'), timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute), debug: koishi_1.Schema.boolean().description('开启调试模式,将群组ID、用户ID等信息输出到日志。').default(false), renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false), exemptUsers: koishi_1.Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]), blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]), restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false), authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4), commandFilterMode: koishi_1.Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'), commandList: koishi_1.Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]) }, (process.platform !== 'win32' ? {
|
|
18
|
+
sudoPassword: koishi_1.Schema.string().role('secret').description('管理员密码,用于 sudoexec 指令以最高权限执行命令。').default(''),
|
|
19
|
+
} : {})));
|
package/lib/index.js
CHANGED
|
@@ -138,25 +138,31 @@ function apply(ctx, config) {
|
|
|
138
138
|
if (cdValidation.newDir && code === 0) {
|
|
139
139
|
sessionDirs.set(sessionId, cdValidation.newDir);
|
|
140
140
|
}
|
|
141
|
-
if (!
|
|
142
|
-
|
|
141
|
+
if (!isExempt) return [3 /*break*/, 2];
|
|
142
|
+
return [4 /*yield*/, session.send('命令成功执行')];
|
|
143
143
|
case 1:
|
|
144
|
-
_a.
|
|
145
|
-
|
|
144
|
+
_a.sent();
|
|
145
|
+
_a.label = 2;
|
|
146
146
|
case 2:
|
|
147
|
+
if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 7];
|
|
148
|
+
_a.label = 3;
|
|
149
|
+
case 3:
|
|
150
|
+
_a.trys.push([3, 5, , 6]);
|
|
151
|
+
return [4 /*yield*/, (0, render_1.renderTerminalImage)(ctx, currentDir, command, state.output || '(no output)')];
|
|
152
|
+
case 4:
|
|
147
153
|
image = _a.sent();
|
|
148
154
|
resolve(image);
|
|
149
|
-
return [3 /*break*/,
|
|
150
|
-
case
|
|
155
|
+
return [3 /*break*/, 6];
|
|
156
|
+
case 5:
|
|
151
157
|
error_1 = _a.sent();
|
|
152
158
|
ctx.logger.error('Failed to render terminal image:', error_1);
|
|
153
159
|
resolve(session.text('.finished', state));
|
|
154
|
-
return [3 /*break*/,
|
|
155
|
-
case
|
|
156
|
-
case
|
|
160
|
+
return [3 /*break*/, 6];
|
|
161
|
+
case 6: return [3 /*break*/, 8];
|
|
162
|
+
case 7:
|
|
157
163
|
resolve(session.text('.finished', state));
|
|
158
|
-
_a.label =
|
|
159
|
-
case
|
|
164
|
+
_a.label = 8;
|
|
165
|
+
case 8: return [2 /*return*/];
|
|
160
166
|
}
|
|
161
167
|
});
|
|
162
168
|
}); });
|
|
@@ -164,4 +170,99 @@ function apply(ctx, config) {
|
|
|
164
170
|
}
|
|
165
171
|
});
|
|
166
172
|
}); });
|
|
173
|
+
// sudoexec 指令:仅在非 Windows 平台注册
|
|
174
|
+
if (process.platform !== 'win32') {
|
|
175
|
+
ctx.command('sudoexec <command:text>')
|
|
176
|
+
.action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
|
|
177
|
+
var guildId, userId, userKey, isExempt, sudoPassword, sessionId, rootDir, currentDir, sudoCommand, timeout, state;
|
|
178
|
+
var _this = this;
|
|
179
|
+
var _c, _d;
|
|
180
|
+
var session = _b.session;
|
|
181
|
+
return __generator(this, function (_e) {
|
|
182
|
+
guildId = session.guildId || '0';
|
|
183
|
+
userId = session.userId || '';
|
|
184
|
+
userKey = "".concat(guildId, ":").concat(userId);
|
|
185
|
+
isExempt = (_d = (_c = config.exemptUsers) === null || _c === void 0 ? void 0 : _c.some(function (entry) { return entry === userKey; })) !== null && _d !== void 0 ? _d : false;
|
|
186
|
+
// 非例外用户直接忽略,不返回任何内容
|
|
187
|
+
if (!isExempt)
|
|
188
|
+
return [2 /*return*/];
|
|
189
|
+
if (!command) {
|
|
190
|
+
return [2 /*return*/, session.text('.expect-text')];
|
|
191
|
+
}
|
|
192
|
+
command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
|
|
193
|
+
sudoPassword = config.sudoPassword || '';
|
|
194
|
+
if (!sudoPassword) {
|
|
195
|
+
return [2 /*return*/, session.text('.no-password')];
|
|
196
|
+
}
|
|
197
|
+
sessionId = session.uid || session.channelId;
|
|
198
|
+
rootDir = path_1.default.resolve(ctx.baseDir, config.root);
|
|
199
|
+
currentDir = sessionDirs.get(sessionId) || rootDir;
|
|
200
|
+
(0, logger_1.debugLog)(ctx, config, {
|
|
201
|
+
guildId: guildId,
|
|
202
|
+
userId: userId,
|
|
203
|
+
command: "[sudoexec] ".concat(command),
|
|
204
|
+
isExempt: true,
|
|
205
|
+
currentDir: currentDir,
|
|
206
|
+
});
|
|
207
|
+
sudoCommand = "echo '".concat(sudoPassword.replace(/'/g, "'\\''"), "' | sudo -S ").concat(command);
|
|
208
|
+
timeout = config.timeout;
|
|
209
|
+
state = { command: command, timeout: timeout, output: '' };
|
|
210
|
+
return [2 /*return*/, new Promise(function (resolve) {
|
|
211
|
+
var start = Date.now();
|
|
212
|
+
var child = (0, child_process_1.exec)(sudoCommand, {
|
|
213
|
+
timeout: timeout,
|
|
214
|
+
cwd: currentDir,
|
|
215
|
+
encoding: config.encoding,
|
|
216
|
+
shell: config.shell || '/bin/bash',
|
|
217
|
+
windowsHide: true,
|
|
218
|
+
});
|
|
219
|
+
child.stdout.on('data', function (data) {
|
|
220
|
+
state.output += data.toString();
|
|
221
|
+
});
|
|
222
|
+
child.stderr.on('data', function (data) {
|
|
223
|
+
// 过滤掉 sudo 的密码提示信息
|
|
224
|
+
var text = data.toString();
|
|
225
|
+
if (!text.includes('[sudo] password for') && !text.includes('Password:')) {
|
|
226
|
+
state.output += text;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
child.on('close', function (code, signal) { return __awaiter(_this, void 0, void 0, function () {
|
|
230
|
+
var image, error_2;
|
|
231
|
+
return __generator(this, function (_a) {
|
|
232
|
+
switch (_a.label) {
|
|
233
|
+
case 0:
|
|
234
|
+
state.code = code;
|
|
235
|
+
state.signal = signal;
|
|
236
|
+
state.timeUsed = Date.now() - start;
|
|
237
|
+
state.output = state.output.trim();
|
|
238
|
+
(0, logger_1.debugLogResult)(ctx, config, code, state.timeUsed);
|
|
239
|
+
return [4 /*yield*/, session.send('命令成功执行')];
|
|
240
|
+
case 1:
|
|
241
|
+
_a.sent();
|
|
242
|
+
if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 6];
|
|
243
|
+
_a.label = 2;
|
|
244
|
+
case 2:
|
|
245
|
+
_a.trys.push([2, 4, , 5]);
|
|
246
|
+
return [4 /*yield*/, (0, render_1.renderTerminalImage)(ctx, currentDir, "[sudo] ".concat(command), state.output || '(no output)')];
|
|
247
|
+
case 3:
|
|
248
|
+
image = _a.sent();
|
|
249
|
+
resolve(image);
|
|
250
|
+
return [3 /*break*/, 5];
|
|
251
|
+
case 4:
|
|
252
|
+
error_2 = _a.sent();
|
|
253
|
+
ctx.logger.error('Failed to render terminal image:', error_2);
|
|
254
|
+
resolve(session.text('.finished', state));
|
|
255
|
+
return [3 /*break*/, 5];
|
|
256
|
+
case 5: return [3 /*break*/, 7];
|
|
257
|
+
case 6:
|
|
258
|
+
resolve(session.text('.finished', state));
|
|
259
|
+
_a.label = 7;
|
|
260
|
+
case 7: return [2 /*return*/];
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}); });
|
|
264
|
+
})];
|
|
265
|
+
});
|
|
266
|
+
}); });
|
|
267
|
+
}
|
|
167
268
|
}
|
package/lib/locales/zh-CN.json
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
"restricted-directory": "不允许切换到上级或其他目录。",
|
|
11
11
|
"restricted-path": "不允许访问配置目录以外的文件或目录。"
|
|
12
12
|
}
|
|
13
|
+
},
|
|
14
|
+
"sudoexec": {
|
|
15
|
+
"description": "以最高权限执行命令(仅限例外用户)",
|
|
16
|
+
"messages": {
|
|
17
|
+
"expect-text": "请输入要运行的命令。",
|
|
18
|
+
"no-password": "未配置管理员密码,无法执行 sudo 命令。",
|
|
19
|
+
"finished": "[sudo 运行完毕] {command}\n{output}"
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
}
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Config {
|
|
|
15
15
|
authority?: number
|
|
16
16
|
commandFilterMode?: 'blacklist' | 'whitelist'
|
|
17
17
|
commandList?: string[]
|
|
18
|
+
sudoPassword?: string
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const Config: Schema<Config> = Schema.object({
|
|
@@ -30,4 +31,7 @@ export const Config: Schema<Config> = Schema.object({
|
|
|
30
31
|
authority: Schema.number().description('exec 命令所需权限等级。').default(4),
|
|
31
32
|
commandFilterMode: Schema.union(['blacklist', 'whitelist']).description('命令过滤模式:blacklist/whitelist').default('blacklist'),
|
|
32
33
|
commandList: Schema.array(String).description('命令过滤列表,配合过滤模式使用(为空则不限制)。').default([]),
|
|
34
|
+
...(process.platform !== 'win32' ? {
|
|
35
|
+
sudoPassword: Schema.string().role('secret').description('管理员密码,用于 sudoexec 指令以最高权限执行命令。').default(''),
|
|
36
|
+
} : {}),
|
|
33
37
|
})
|
package/src/index.ts
CHANGED
|
@@ -118,6 +118,11 @@ export function apply(ctx: Context, config: Config) {
|
|
|
118
118
|
sessionDirs.set(sessionId, cdValidation.newDir)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// 例外用户先回复"命令成功执行",再尝试渲染结果
|
|
122
|
+
if (isExempt) {
|
|
123
|
+
await session.send('命令成功执行')
|
|
124
|
+
}
|
|
125
|
+
|
|
121
126
|
// 渲染为图片或返回文本
|
|
122
127
|
if (config.renderImage && ctx.puppeteer) {
|
|
123
128
|
try {
|
|
@@ -133,4 +138,91 @@ export function apply(ctx: Context, config: Config) {
|
|
|
133
138
|
})
|
|
134
139
|
})
|
|
135
140
|
})
|
|
141
|
+
|
|
142
|
+
// sudoexec 指令:仅在非 Windows 平台注册
|
|
143
|
+
if (process.platform !== 'win32') {
|
|
144
|
+
ctx.command('sudoexec <command:text>')
|
|
145
|
+
.action(async ({ session }, command) => {
|
|
146
|
+
// 检查是否为例外用户
|
|
147
|
+
const guildId = session.guildId || '0'
|
|
148
|
+
const userId = session.userId || ''
|
|
149
|
+
const userKey = `${guildId}:${userId}`
|
|
150
|
+
const isExempt = config.exemptUsers?.some(entry => entry === userKey) ?? false
|
|
151
|
+
|
|
152
|
+
// 非例外用户直接忽略,不返回任何内容
|
|
153
|
+
if (!isExempt) return
|
|
154
|
+
|
|
155
|
+
if (!command) {
|
|
156
|
+
return session.text('.expect-text')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
command = h('', h.parse(command)).toString(true)
|
|
160
|
+
|
|
161
|
+
const sudoPassword = config.sudoPassword || ''
|
|
162
|
+
if (!sudoPassword) {
|
|
163
|
+
return session.text('.no-password')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const sessionId = session.uid || session.channelId
|
|
167
|
+
const rootDir = path.resolve(ctx.baseDir, config.root)
|
|
168
|
+
const currentDir = sessionDirs.get(sessionId) || rootDir
|
|
169
|
+
|
|
170
|
+
debugLog(ctx, config, {
|
|
171
|
+
guildId,
|
|
172
|
+
userId,
|
|
173
|
+
command: `[sudoexec] ${command}`,
|
|
174
|
+
isExempt: true,
|
|
175
|
+
currentDir,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// 使用 sudo -S 通过 stdin 传递密码
|
|
179
|
+
const sudoCommand = `echo '${sudoPassword.replace(/'/g, "'\\''")}' | sudo -S ${command}`
|
|
180
|
+
|
|
181
|
+
const { timeout } = config
|
|
182
|
+
const state: State = { command, timeout, output: '' }
|
|
183
|
+
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const start = Date.now()
|
|
186
|
+
const child = exec(sudoCommand, {
|
|
187
|
+
timeout,
|
|
188
|
+
cwd: currentDir,
|
|
189
|
+
encoding: config.encoding,
|
|
190
|
+
shell: config.shell || '/bin/bash',
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
})
|
|
193
|
+
child.stdout.on('data', (data) => {
|
|
194
|
+
state.output += data.toString()
|
|
195
|
+
})
|
|
196
|
+
child.stderr.on('data', (data) => {
|
|
197
|
+
// 过滤掉 sudo 的密码提示信息
|
|
198
|
+
const text = data.toString()
|
|
199
|
+
if (!text.includes('[sudo] password for') && !text.includes('Password:')) {
|
|
200
|
+
state.output += text
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
child.on('close', async (code, signal) => {
|
|
204
|
+
state.code = code
|
|
205
|
+
state.signal = signal
|
|
206
|
+
state.timeUsed = Date.now() - start
|
|
207
|
+
state.output = state.output.trim()
|
|
208
|
+
|
|
209
|
+
debugLogResult(ctx, config, code, state.timeUsed)
|
|
210
|
+
|
|
211
|
+
await session.send('命令成功执行')
|
|
212
|
+
|
|
213
|
+
if (config.renderImage && ctx.puppeteer) {
|
|
214
|
+
try {
|
|
215
|
+
const image = await renderTerminalImage(ctx, currentDir, `[sudo] ${command}`, state.output || '(no output)')
|
|
216
|
+
resolve(image)
|
|
217
|
+
} catch (error) {
|
|
218
|
+
ctx.logger.error('Failed to render terminal image:', error)
|
|
219
|
+
resolve(session.text('.finished', state))
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
resolve(session.text('.finished', state))
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
}
|
|
136
228
|
}
|
package/src/locales/zh-CN.yml
CHANGED
|
@@ -10,3 +10,11 @@ commands:
|
|
|
10
10
|
blocked-command: 该命令已被禁止执行。
|
|
11
11
|
restricted-directory: 不允许切换到上级或其他目录。
|
|
12
12
|
restricted-path: 不允许访问配置目录以外的文件或目录。
|
|
13
|
+
sudoexec:
|
|
14
|
+
description: 以最高权限执行命令(仅限例外用户)
|
|
15
|
+
messages:
|
|
16
|
+
expect-text: 请输入要运行的命令。
|
|
17
|
+
no-password: 未配置管理员密码,无法执行 sudo 命令。
|
|
18
|
+
finished: |-
|
|
19
|
+
[sudo 运行完毕] {command}
|
|
20
|
+
{output}
|