koishi-plugin-spawn-modified 1.2.5 → 1.2.7

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 CHANGED
@@ -13,6 +13,7 @@ export interface Config {
13
13
  encoding?: typeof encodings[number];
14
14
  timeout?: number;
15
15
  renderImage?: boolean;
16
+ exemptUsers?: string[];
16
17
  blockedCommands?: string[];
17
18
  restrictDirectory?: boolean;
18
19
  authority?: number;
package/lib/index.js CHANGED
@@ -63,6 +63,7 @@ exports.Config = koishi_1.Schema.object({
63
63
  encoding: koishi_1.Schema.union(encodings).description('输出内容编码。').default('utf8'),
64
64
  timeout: koishi_1.Schema.number().description('最长运行时间。').default(koishi_1.Time.minute),
65
65
  renderImage: koishi_1.Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
66
+ exemptUsers: koishi_1.Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
66
67
  blockedCommands: koishi_1.Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
67
68
  restrictDirectory: koishi_1.Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
68
69
  authority: koishi_1.Schema.number().description('exec 命令所需权限等级。').default(4),
@@ -226,6 +227,33 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
226
227
  }
227
228
  return { valid: true };
228
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
+ }
229
257
  // 渲染终端输出为图片
230
258
  function renderTerminalImage(ctx, workingDir, command, output) {
231
259
  return __awaiter(this, void 0, void 0, function () {
@@ -301,30 +329,34 @@ function apply(ctx, config) {
301
329
  ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
302
330
  ctx.command('exec <command:text>', { authority: (_a = config.authority) !== null && _a !== void 0 ? _a : 4 })
303
331
  .action(function (_a, command_1) { return __awaiter(_this, [_a, command_1], void 0, function (_b, command) {
304
- var filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
332
+ var guildId, userId, userKey, isExempt, filterList, filterMode, sessionId, rootDir, currentDir, cdValidation, pathValidation, timeout, state;
305
333
  var _this = this;
306
- var _c;
334
+ var _c, _d, _e;
307
335
  var session = _b.session;
308
- return __generator(this, function (_d) {
309
- switch (_d.label) {
336
+ return __generator(this, function (_f) {
337
+ switch (_f.label) {
310
338
  case 0:
311
339
  if (!command) {
312
340
  return [2 /*return*/, session.text('.expect-text')];
313
341
  }
314
342
  command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
315
- filterList = (((_c = config.commandList) === null || _c === void 0 ? void 0 : _c.length) ? config.commandList : config.blockedCommands) || [];
343
+ guildId = session.guildId || '0';
344
+ userId = session.userId || '';
345
+ userKey = "".concat(guildId, ":").concat(userId);
346
+ isExempt = (_d = (_c = config.exemptUsers) === null || _c === void 0 ? void 0 : _c.some(function (entry) { return entry === userKey; })) !== null && _d !== void 0 ? _d : false;
347
+ filterList = (((_e = config.commandList) === null || _e === void 0 ? void 0 : _e.length) ? config.commandList : config.blockedCommands) || [];
316
348
  filterMode = config.commandFilterMode || 'blacklist';
317
- if (isCommandBlocked(command, filterMode, filterList)) {
349
+ if (!isExempt && isCommandBlocked(command, filterMode, filterList)) {
318
350
  return [2 /*return*/, session.text('.blocked-command')];
319
351
  }
320
352
  sessionId = session.uid || session.channelId;
321
353
  rootDir = path_1.default.resolve(ctx.baseDir, config.root);
322
354
  currentDir = sessionDirs.get(sessionId) || rootDir;
323
- cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
355
+ cdValidation = validateCdCommand(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
324
356
  if (!cdValidation.valid) {
325
357
  return [2 /*return*/, session.text('.restricted-directory')];
326
358
  }
327
- pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory);
359
+ pathValidation = validatePathAccess(command, currentDir, rootDir, !isExempt && config.restrictDirectory);
328
360
  if (!pathValidation.valid) {
329
361
  return [2 /*return*/, session.text('.restricted-path')];
330
362
  }
@@ -333,8 +365,8 @@ function apply(ctx, config) {
333
365
  if (!!config.renderImage) return [3 /*break*/, 2];
334
366
  return [4 /*yield*/, session.send(session.text('.started', state))];
335
367
  case 1:
336
- _d.sent();
337
- _d.label = 2;
368
+ _f.sent();
369
+ _f.label = 2;
338
370
  case 2: return [2 /*return*/, new Promise(function (resolve) {
339
371
  var start = Date.now();
340
372
  var child = (0, child_process_1.exec)(command, {
@@ -358,7 +390,7 @@ function apply(ctx, config) {
358
390
  state.code = code;
359
391
  state.signal = signal;
360
392
  state.timeUsed = Date.now() - start;
361
- state.output = state.output.trim();
393
+ state.output = maskCurlOutput(command, state.output.trim());
362
394
  // 更新当前目录(如果是 cd 命令且执行成功)
363
395
  if (cdValidation.newDir && code === 0) {
364
396
  sessionDirs.set(sessionId, cdValidation.newDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export interface Config {
21
21
  encoding?: typeof encodings[number]
22
22
  timeout?: number
23
23
  renderImage?: boolean
24
+ exemptUsers?: string[]
24
25
  blockedCommands?: string[]
25
26
  restrictDirectory?: boolean
26
27
  authority?: number
@@ -34,6 +35,7 @@ export const Config: Schema<Config> = Schema.object({
34
35
  encoding: Schema.union(encodings).description('输出内容编码。').default('utf8'),
35
36
  timeout: Schema.number().description('最长运行时间。').default(Time.minute),
36
37
  renderImage: Schema.boolean().description('是否将命令执行结果渲染为图片(需要安装 puppeteer 插件)。').default(false),
38
+ exemptUsers: Schema.array(String).description('例外用户列表,格式为 "群组ID:用户ID"。私聊时群组ID为0。匹配的用户将无视一切过滤器。').default([]),
37
39
  blockedCommands: Schema.array(String).description('违禁命令列表(命令的开头部分)。').default([]),
38
40
  restrictDirectory: Schema.boolean().description('是否限制在当前目录及子目录内执行命令(禁止 cd 到上级或其他目录)。').default(false),
39
41
  authority: Schema.number().description('exec 命令所需权限等级。').default(4),
@@ -227,6 +229,30 @@ function validateCdCommand(command: string, currentDir: string, rootDir: string,
227
229
  return { valid: true }
228
230
  }
229
231
 
232
+ function maskCurlOutput(command: string, output: string): string {
233
+ if (!output) return output
234
+ if (!/\bcurl\b/i.test(command)) return output
235
+
236
+ const 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
237
+ return output.replace(ipv4Regex, (ip) => (isPrivateIpv4(ip) ? ip : '*.*.*.*'))
238
+ }
239
+
240
+ function isPrivateIpv4(ip: string): boolean {
241
+ const octets = ip.split('.').map(Number)
242
+ if (octets.length !== 4) return false
243
+ if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255)) return false
244
+
245
+ const [a, b] = octets
246
+
247
+ if (a === 10) return true
248
+ if (a === 172 && b >= 16 && b <= 31) return true
249
+ if (a === 192 && b === 168) return true
250
+ if (a === 127) return true
251
+ if (a === 169 && b === 254) return true
252
+
253
+ return false
254
+ }
255
+
230
256
  // 渲染终端输出为图片
231
257
  async function renderTerminalImage(ctx: Context, workingDir: string, command: string, output: string): Promise<h> {
232
258
  if (!ctx.puppeteer) {
@@ -417,21 +443,28 @@ export function apply(ctx: Context, config: Config) {
417
443
  }
418
444
 
419
445
  command = h('', h.parse(command)).toString(true)
446
+
447
+ // 检查是否为例外用户(无视一切过滤器)
448
+ const guildId = session.guildId || '0'
449
+ const userId = session.userId || ''
450
+ const userKey = `${guildId}:${userId}`
451
+ const isExempt = config.exemptUsers?.some(entry => entry === userKey) ?? false
452
+
420
453
  // 检查命令过滤(黑/白名单);仅使用配置提供的正则
421
454
  const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
422
455
  const filterMode = config.commandFilterMode || 'blacklist'
423
- if (isCommandBlocked(command, filterMode, filterList)) {
456
+ if (!isExempt && isCommandBlocked(command, filterMode, filterList)) {
424
457
  return session.text('.blocked-command')
425
458
  }
426
459
  const sessionId = session.uid || session.channelId
427
460
  const rootDir = path.resolve(ctx.baseDir, config.root)
428
461
  const currentDir = sessionDirs.get(sessionId) || rootDir
429
462
  // 验证 cd 命令
430
- const cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory)
463
+ const cdValidation = validateCdCommand(command, currentDir, rootDir, !isExempt && config.restrictDirectory)
431
464
  if (!cdValidation.valid) {
432
465
  return session.text('.restricted-directory')
433
466
  }
434
- const pathValidation = validatePathAccess(command, currentDir, rootDir, config.restrictDirectory)
467
+ const pathValidation = validatePathAccess(command, currentDir, rootDir, !isExempt && config.restrictDirectory)
435
468
  if (!pathValidation.valid) {
436
469
  return session.text('.restricted-path')
437
470
  }
@@ -459,7 +492,7 @@ export function apply(ctx: Context, config: Config) {
459
492
  state.code = code
460
493
  state.signal = signal
461
494
  state.timeUsed = Date.now() - start
462
- state.output = state.output.trim()
495
+ state.output = maskCurlOutput(command, state.output.trim())
463
496
  // 更新当前目录(如果是 cd 命令且执行成功)
464
497
  if (cdValidation.newDir && code === 0) {
465
498
  sessionDirs.set(sessionId, cdValidation.newDir)