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 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
@@ -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 (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 5];
142
- _a.label = 1;
141
+ if (!isExempt) return [3 /*break*/, 2];
142
+ return [4 /*yield*/, session.send('命令成功执行')];
143
143
  case 1:
144
- _a.trys.push([1, 3, , 4]);
145
- return [4 /*yield*/, (0, render_1.renderTerminalImage)(ctx, currentDir, command, state.output || '(no output)')];
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*/, 4];
150
- case 3:
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*/, 4];
155
- case 4: return [3 /*break*/, 6];
156
- case 5:
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 = 6;
159
- case 6: return [2 /*return*/];
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
  }
@@ -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.8",
3
+ "version": "1.3.0",
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
@@ -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
  }
@@ -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}