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 +37 -5
- package/lib/index.js +25 -10
- package/package.json +1 -1
- package/src/index.ts +25 -14
package/README.md
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
[](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
|
-
|
|
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,
|
|
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:
|
|
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, ,
|
|
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.
|
|
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
|
|
127
|
-
case
|
|
141
|
+
case 7: return [4 /*yield*/, page.close()];
|
|
142
|
+
case 8:
|
|
128
143
|
_a.sent();
|
|
129
144
|
return [7 /*endfinally*/];
|
|
130
|
-
case
|
|
145
|
+
case 9: return [2 /*return*/];
|
|
131
146
|
}
|
|
132
147
|
});
|
|
133
148
|
});
|
package/package.json
CHANGED
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:
|
|
126
|
+
font-size: 13px;
|
|
119
127
|
padding: 0;
|
|
120
128
|
display: inline-block;
|
|
121
|
-
|
|
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:
|
|
165
|
-
white-space: pre
|
|
166
|
-
word-break:
|
|
167
|
-
line-height: 1.
|
|
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:
|
|
181
|
+
gap: 4px;
|
|
173
182
|
align-items: baseline;
|
|
174
|
-
margin-bottom:
|
|
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:
|
|
195
|
+
word-break: normal;
|
|
187
196
|
flex: 1;
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
.output {
|
|
191
200
|
color: #cccccc;
|
|
192
|
-
line-height: 1.
|
|
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(
|
|
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
|
|
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 {
|