koishi-plugin-spawn-modified 1.1.17 → 1.2.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/README.md CHANGED
@@ -1,9 +1,41 @@
1
- # [koishi-plugin-spawn](https://common.koishi.chat/plugins/spawn.html)
2
-
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-spawn?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-spawn)
1
+ # koishi-plugin-spawn-modified
4
2
 
5
3
  Run shell commands with Koishi. | 使用 Koishi 运行终端命令。
6
4
 
7
- ## 文档
5
+ > 在原插件基础上增加:命令过滤黑/白名单、渲染图片(Puppeteer)、动态宽度终端截图、可选调试日志。
8
6
 
9
- <https://common.koishi.chat/plugins/spawn.html>
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm i koishi-plugin-spawn-modified
11
+ ```
12
+
13
+ ## 配置
14
+
15
+ ```yaml
16
+ plugins:
17
+ spawn-modified:
18
+ root: "" # 工作目录,留空为 Koishi 根目录
19
+ shell: "" # 自定义 shell,可留空使用默认
20
+ encoding: utf8 # 输出编码
21
+ timeout: 60000 # 超时(毫秒)
22
+ renderImage: false # 启用截图需安装 koishi-plugin-puppeteer
23
+ restrictDirectory: false# 是否禁止 cd 到根目录之外
24
+ commandFilterMode: blacklist # blacklist | whitelist
25
+ commandList: [] # 与过滤模式配合使用
26
+ blockedCommands: [] # 兼容字段,过滤模式为 blacklist 时生效
27
+ ```
28
+
29
+ ## 使用
30
+
31
+ 在聊天中输入:
32
+
33
+ ```
34
+ exec <command>
35
+ ```
36
+
37
+ 如果开启 `renderImage`,输出会渲染为终端风格图片,并根据最长行自动加宽(600–1400px 区间)。
38
+
39
+ ## 调试
40
+
41
+ 启用 Koishi 日志后可查看 `spawn-debug` 通道,包含命令解析、过滤、输出等调试信息,便于排查文本发送或截图问题。
package/lib/index.js CHANGED
@@ -35,6 +35,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
36
  }
37
37
  };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
38
47
  var __importDefault = (this && this.__importDefault) || function (mod) {
39
48
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
49
  };
@@ -92,21 +101,30 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
92
101
  // 渲染终端输出为图片
93
102
  function renderTerminalImage(ctx, workingDir, command, output) {
94
103
  return __awaiter(this, void 0, void 0, function () {
95
- var fontPath, html, page, element, clip, screenshot;
104
+ var displayOutput, lines, commandLineLength, maxLineLength, charWidth, horizontalBuffer, containerWidth, fontPath, html, page, element, screenshot;
96
105
  return __generator(this, function (_a) {
97
106
  switch (_a.label) {
98
107
  case 0:
99
108
  if (!ctx.puppeteer) {
100
109
  throw new Error('Puppeteer plugin is not available');
101
110
  }
111
+ displayOutput = output || '(no output)';
112
+ lines = displayOutput.split(/\r?\n/);
113
+ commandLineLength = "".concat(workingDir, "$ ").concat(command).length;
114
+ maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], lines.map(function (line) { return line.length; }), false)) || commandLineLength;
115
+ charWidth = 7.4 // approximate width of JetBrains Mono at 13px
116
+ ;
117
+ horizontalBuffer = 48 // padding + borders + margin buffer
118
+ ;
119
+ containerWidth = Math.max(600, Math.min(1400, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
102
120
  fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
103
- 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-weight: 400;\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: 12px 16px;\n white-space: pre-wrap;\n word-break: break-word;\n line-height: 1.35;\n }\n \n .command-line {\n display: flex;\n gap: 6px;\n align-items: baseline;\n margin-bottom: 10px;\n }\n \n .prompt {\n color: #4ec9b0;\n margin: 0;\n flex-shrink: 0;\n }\n \n .command {\n color: #dcdcaa;\n margin: 0;\n word-break: break-word;\n flex: 1;\n }\n \n .output {\n color: #cccccc;\n line-height: 1.4;\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=\"command-line\">\n <div class=\"prompt\">").concat(escapeHtml(workingDir), "$</div>\n <div class=\"command\">").concat(escapeHtml(command), "</div>\n </div>\n <div class=\"output\">").concat(escapeHtml(output), "</div>\n </div>\n </div>\n</body>\n</html>\n ");
121
+ 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-weight: 400;\n font-size: 13px;\n padding: 0;\n display: inline-block;\n width: ").concat(containerWidth, "px;\n }\n \n .terminal {\n background: #1e1e1e;\n border: 1px solid #3c3c3c;\n border-radius: 8px;\n overflow: hidden;\n width: 100%;\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: 10px 14px;\n white-space: pre;\n word-break: normal;\n line-height: 1.25;\n overflow-x: auto;\n }\n \n .command-line {\n display: flex;\n gap: 4px;\n align-items: baseline;\n margin-bottom: 6px;\n }\n \n .prompt {\n color: #4ec9b0;\n margin: 0;\n flex-shrink: 0;\n }\n \n .command {\n color: #dcdcaa;\n margin: 0;\n word-break: normal;\n flex: 1;\n }\n \n .output {\n color: #cccccc;\n line-height: 1.22;\n white-space: pre;\n word-break: normal;\n overflow-x: auto;\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=\"command-line\">\n <div class=\"prompt\">").concat(escapeHtml(workingDir), "$</div>\n <div class=\"command\">").concat(escapeHtml(command), "</div>\n </div>\n <div class=\"output\">").concat(escapeHtml(displayOutput), "</div>\n </div>\n </div>\n</body>\n</html>\n ");
104
122
  return [4 /*yield*/, ctx.puppeteer.page()];
105
123
  case 1:
106
124
  page = _a.sent();
107
125
  _a.label = 2;
108
126
  case 2:
109
- _a.trys.push([2, , 8, 10]);
127
+ _a.trys.push([2, , 7, 9]);
110
128
  return [4 /*yield*/, page.setContent(html)];
111
129
  case 3:
112
130
  _a.sent();
@@ -116,18 +134,15 @@ function renderTerminalImage(ctx, workingDir, command, output) {
116
134
  return [4 /*yield*/, page.$('.terminal')];
117
135
  case 5:
118
136
  element = _a.sent();
119
- return [4 /*yield*/, element.boundingBox()];
137
+ return [4 /*yield*/, element.screenshot({ type: 'png' })];
120
138
  case 6:
121
- clip = _a.sent();
122
- return [4 /*yield*/, page.screenshot({ clip: clip })];
123
- case 7:
124
139
  screenshot = _a.sent();
125
140
  return [2 /*return*/, koishi_1.h.image(screenshot, 'image/png')];
126
- case 8: return [4 /*yield*/, page.close()];
127
- case 9:
141
+ case 7: return [4 /*yield*/, page.close()];
142
+ case 8:
128
143
  _a.sent();
129
144
  return [7 /*endfinally*/];
130
- case 10: return [2 /*return*/];
145
+ case 9: return [2 /*return*/];
131
146
  }
132
147
  });
133
148
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-spawn-modified",
3
- "version": "1.1.17",
3
+ "version": "1.2.1",
4
4
  "description": "Run shell commands with Koishi",
5
5
  "keywords": [
6
6
  "bot",
package/src/index.ts CHANGED
@@ -91,6 +91,14 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
91
91
  throw new Error('Puppeteer plugin is not available')
92
92
  }
93
93
 
94
+ const displayOutput = output || '(no output)'
95
+ const lines = displayOutput.split(/\r?\n/)
96
+ const commandLineLength = `${workingDir}$ ${command}`.length
97
+ const maxLineLength = Math.max(commandLineLength, ...lines.map(line => line.length)) || commandLineLength
98
+ const charWidth = 7.4 // approximate width of JetBrains Mono at 13px
99
+ const horizontalBuffer = 48 // padding + borders + margin buffer
100
+ const containerWidth = Math.max(600, Math.min(1400, Math.ceil(maxLineLength * charWidth + horizontalBuffer)))
101
+
94
102
  const fontPath = pathToFileURL(path.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href
95
103
 
96
104
  const html = `
@@ -115,11 +123,10 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
115
123
  color: #cccccc;
116
124
  font-family: 'JetBrains Mono', 'Courier New', monospace;
117
125
  font-weight: 400;
118
- font-size: 14px;
126
+ font-size: 13px;
119
127
  padding: 0;
120
128
  display: inline-block;
121
- min-width: 600px;
122
- max-width: 1200px;
129
+ width: ${containerWidth}px;
123
130
  }
124
131
 
125
132
  .terminal {
@@ -127,6 +134,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
127
134
  border: 1px solid #3c3c3c;
128
135
  border-radius: 8px;
129
136
  overflow: hidden;
137
+ width: 100%;
130
138
  }
131
139
 
132
140
  .title-bar {
@@ -161,17 +169,18 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
161
169
  .button.close { background: #ff5f56; }
162
170
 
163
171
  .content {
164
- padding: 12px 16px;
165
- white-space: pre-wrap;
166
- word-break: break-word;
167
- line-height: 1.35;
172
+ padding: 10px 14px;
173
+ white-space: pre;
174
+ word-break: normal;
175
+ line-height: 1.25;
176
+ overflow-x: auto;
168
177
  }
169
178
 
170
179
  .command-line {
171
180
  display: flex;
172
- gap: 6px;
181
+ gap: 4px;
173
182
  align-items: baseline;
174
- margin-bottom: 10px;
183
+ margin-bottom: 6px;
175
184
  }
176
185
 
177
186
  .prompt {
@@ -183,13 +192,16 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
183
192
  .command {
184
193
  color: #dcdcaa;
185
194
  margin: 0;
186
- word-break: break-word;
195
+ word-break: normal;
187
196
  flex: 1;
188
197
  }
189
198
 
190
199
  .output {
191
200
  color: #cccccc;
192
- line-height: 1.4;
201
+ line-height: 1.22;
202
+ white-space: pre;
203
+ word-break: normal;
204
+ overflow-x: auto;
193
205
  }
194
206
  </style>
195
207
  </head>
@@ -208,7 +220,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
208
220
  <div class="prompt">${escapeHtml(workingDir)}$</div>
209
221
  <div class="command">${escapeHtml(command)}</div>
210
222
  </div>
211
- <div class="output">${escapeHtml(output)}</div>
223
+ <div class="output">${escapeHtml(displayOutput)}</div>
212
224
  </div>
213
225
  </div>
214
226
  </body>
@@ -221,8 +233,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
221
233
  await page.waitForNetworkIdle({ timeout: 5000 })
222
234
 
223
235
  const element = await page.$('.terminal')
224
- const clip = await element.boundingBox()
225
- const screenshot = await page.screenshot({ clip }) as Buffer
236
+ const screenshot = await element.screenshot({ type: 'png' }) as Buffer
226
237
 
227
238
  return h.image(screenshot, 'image/png')
228
239
  } finally {