koishi-plugin-spawn-modified 1.2.9 → 1.3.1

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 CHANGED
@@ -13,6 +13,7 @@ export interface Config {
13
13
  authority?: number;
14
14
  commandFilterMode?: 'blacklist' | 'whitelist';
15
15
  commandList?: string[];
16
+ sudoPassword?: string;
16
17
  }
17
18
  export declare const Config: Schema<Config>;
18
19
  export {};
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
- 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
- });
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
@@ -62,7 +62,7 @@ function apply(ctx, config) {
62
62
  ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
63
63
  ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
64
64
  .action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
65
- var guildId, userId, userKey, isExempt, sessionId, rootDir, currentDir, filterList, filterMode, cdValidation, pathValidation, timeout, state;
65
+ var guildId, userId, userKey, isExempt, sessionId, rootDir, currentDir, fs, filterList, filterMode, cdValidation, pathValidation, timeout, state;
66
66
  var _this = this;
67
67
  var _c, _d, _e;
68
68
  var session = _b.session;
@@ -80,6 +80,10 @@ function apply(ctx, config) {
80
80
  sessionId = session.uid || session.channelId;
81
81
  rootDir = path_1.default.resolve(ctx.baseDir, config.root);
82
82
  currentDir = sessionDirs.get(sessionId) || rootDir;
83
+ fs = require('fs');
84
+ if (!fs.existsSync(currentDir)) {
85
+ return [2 /*return*/, session.text('执行目录不存在')];
86
+ }
83
87
  // 输出调试信息
84
88
  (0, logger_1.debugLog)(ctx, config, {
85
89
  guildId: guildId,
@@ -170,4 +174,103 @@ function apply(ctx, config) {
170
174
  }
171
175
  });
172
176
  }); });
177
+ // sudoexec 指令:仅在非 Windows 平台注册
178
+ if (process.platform !== 'win32') {
179
+ ctx.command('sudoexec <command:text>')
180
+ .action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
181
+ var guildId, userId, userKey, isExempt, sudoPassword, sessionId, rootDir, currentDir, fs, sudoCommand, timeout, state;
182
+ var _this = this;
183
+ var _c, _d;
184
+ var session = _b.session;
185
+ return __generator(this, function (_e) {
186
+ guildId = session.guildId || '0';
187
+ userId = session.userId || '';
188
+ userKey = "".concat(guildId, ":").concat(userId);
189
+ isExempt = (_d = (_c = config.exemptUsers) === null || _c === void 0 ? void 0 : _c.some(function (entry) { return entry === userKey; })) !== null && _d !== void 0 ? _d : false;
190
+ // 非例外用户直接忽略,不返回任何内容
191
+ if (!isExempt)
192
+ return [2 /*return*/];
193
+ if (!command) {
194
+ return [2 /*return*/, session.text('.expect-text')];
195
+ }
196
+ command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
197
+ sudoPassword = config.sudoPassword || '';
198
+ if (!sudoPassword) {
199
+ return [2 /*return*/, session.text('.no-password')];
200
+ }
201
+ sessionId = session.uid || session.channelId;
202
+ rootDir = path_1.default.resolve(ctx.baseDir, config.root);
203
+ currentDir = sessionDirs.get(sessionId) || rootDir;
204
+ fs = require('fs');
205
+ if (!fs.existsSync(currentDir)) {
206
+ return [2 /*return*/, session.text('执行目录不存在')];
207
+ }
208
+ (0, logger_1.debugLog)(ctx, config, {
209
+ guildId: guildId,
210
+ userId: userId,
211
+ command: "[sudoexec] ".concat(command),
212
+ isExempt: true,
213
+ currentDir: currentDir,
214
+ });
215
+ sudoCommand = "echo '".concat(sudoPassword.replace(/'/g, "'\\''"), "' | sudo -S ").concat(command);
216
+ timeout = config.timeout;
217
+ state = { command: command, timeout: timeout, output: '' };
218
+ return [2 /*return*/, new Promise(function (resolve) {
219
+ var start = Date.now();
220
+ var child = (0, child_process_1.exec)(sudoCommand, {
221
+ timeout: timeout,
222
+ cwd: currentDir,
223
+ encoding: config.encoding,
224
+ shell: config.shell || '/bin/bash',
225
+ windowsHide: true,
226
+ });
227
+ child.stdout.on('data', function (data) {
228
+ state.output += data.toString();
229
+ });
230
+ child.stderr.on('data', function (data) {
231
+ // 过滤掉 sudo 的密码提示信息
232
+ var text = data.toString();
233
+ if (!text.includes('[sudo] password for') && !text.includes('Password:')) {
234
+ state.output += text;
235
+ }
236
+ });
237
+ child.on('close', function (code, signal) { return __awaiter(_this, void 0, void 0, function () {
238
+ var image, error_2;
239
+ return __generator(this, function (_a) {
240
+ switch (_a.label) {
241
+ case 0:
242
+ state.code = code;
243
+ state.signal = signal;
244
+ state.timeUsed = Date.now() - start;
245
+ state.output = state.output.trim();
246
+ (0, logger_1.debugLogResult)(ctx, config, code, state.timeUsed);
247
+ return [4 /*yield*/, session.send('命令成功执行')];
248
+ case 1:
249
+ _a.sent();
250
+ if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 6];
251
+ _a.label = 2;
252
+ case 2:
253
+ _a.trys.push([2, 4, , 5]);
254
+ return [4 /*yield*/, (0, render_1.renderTerminalImage)(ctx, currentDir, "[sudo] ".concat(command), state.output || '(no output)')];
255
+ case 3:
256
+ image = _a.sent();
257
+ resolve(image);
258
+ return [3 /*break*/, 5];
259
+ case 4:
260
+ error_2 = _a.sent();
261
+ ctx.logger.error('Failed to render terminal image:', error_2);
262
+ resolve(session.text('.finished', state));
263
+ return [3 /*break*/, 5];
264
+ case 5: return [3 /*break*/, 7];
265
+ case 6:
266
+ resolve(session.text('.finished', state));
267
+ _a.label = 7;
268
+ case 7: return [2 /*return*/];
269
+ }
270
+ });
271
+ }); });
272
+ })];
273
+ });
274
+ }); });
275
+ }
173
276
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.2.9",
3
+ "version": "1.3.1",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
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
@@ -56,6 +56,11 @@ export function apply(ctx: Context, config: Config) {
56
56
  const sessionId = session.uid || session.channelId
57
57
  const rootDir = path.resolve(ctx.baseDir, config.root)
58
58
  const currentDir = sessionDirs.get(sessionId) || rootDir
59
+ // 检查执行目录是否存在
60
+ const fs = require('fs')
61
+ if (!fs.existsSync(currentDir)) {
62
+ return session.text('执行目录不存在')
63
+ }
59
64
 
60
65
  // 输出调试信息
61
66
  debugLog(ctx, config, {
@@ -138,4 +143,96 @@ export function apply(ctx: Context, config: Config) {
138
143
  })
139
144
  })
140
145
  })
146
+
147
+ // sudoexec 指令:仅在非 Windows 平台注册
148
+ if (process.platform !== 'win32') {
149
+ ctx.command('sudoexec <command:text>')
150
+ .action(async ({ session }, command) => {
151
+ // 检查是否为例外用户
152
+ const guildId = session.guildId || '0'
153
+ const userId = session.userId || ''
154
+ const userKey = `${guildId}:${userId}`
155
+ const isExempt = config.exemptUsers?.some(entry => entry === userKey) ?? false
156
+
157
+ // 非例外用户直接忽略,不返回任何内容
158
+ if (!isExempt) return
159
+
160
+ if (!command) {
161
+ return session.text('.expect-text')
162
+ }
163
+
164
+ command = h('', h.parse(command)).toString(true)
165
+
166
+ const sudoPassword = config.sudoPassword || ''
167
+ if (!sudoPassword) {
168
+ return session.text('.no-password')
169
+ }
170
+
171
+ const sessionId = session.uid || session.channelId
172
+ const rootDir = path.resolve(ctx.baseDir, config.root)
173
+ const currentDir = sessionDirs.get(sessionId) || rootDir
174
+ // 检查执行目录是否存在
175
+ const fs = require('fs')
176
+ if (!fs.existsSync(currentDir)) {
177
+ return session.text('执行目录不存在')
178
+ }
179
+
180
+ debugLog(ctx, config, {
181
+ guildId,
182
+ userId,
183
+ command: `[sudoexec] ${command}`,
184
+ isExempt: true,
185
+ currentDir,
186
+ })
187
+
188
+ // 使用 sudo -S 通过 stdin 传递密码
189
+ const sudoCommand = `echo '${sudoPassword.replace(/'/g, "'\\''")}' | sudo -S ${command}`
190
+
191
+ const { timeout } = config
192
+ const state: State = { command, timeout, output: '' }
193
+
194
+ return new Promise((resolve) => {
195
+ const start = Date.now()
196
+ const child = exec(sudoCommand, {
197
+ timeout,
198
+ cwd: currentDir,
199
+ encoding: config.encoding,
200
+ shell: config.shell || '/bin/bash',
201
+ windowsHide: true,
202
+ })
203
+ child.stdout.on('data', (data) => {
204
+ state.output += data.toString()
205
+ })
206
+ child.stderr.on('data', (data) => {
207
+ // 过滤掉 sudo 的密码提示信息
208
+ const text = data.toString()
209
+ if (!text.includes('[sudo] password for') && !text.includes('Password:')) {
210
+ state.output += text
211
+ }
212
+ })
213
+ child.on('close', async (code, signal) => {
214
+ state.code = code
215
+ state.signal = signal
216
+ state.timeUsed = Date.now() - start
217
+ state.output = state.output.trim()
218
+
219
+ debugLogResult(ctx, config, code, state.timeUsed)
220
+
221
+ await session.send('命令成功执行')
222
+
223
+ if (config.renderImage && ctx.puppeteer) {
224
+ try {
225
+ const image = await renderTerminalImage(ctx, currentDir, `[sudo] ${command}`, state.output || '(no output)')
226
+ resolve(image)
227
+ } catch (error) {
228
+ ctx.logger.error('Failed to render terminal image:', error)
229
+ resolve(session.text('.finished', state))
230
+ }
231
+ } else {
232
+ resolve(session.text('.finished', state))
233
+ }
234
+ })
235
+ })
236
+ })
237
+ }
141
238
  }
@@ -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}