koishi-plugin-spawn-modified 1.2.0 → 1.2.2
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.js +21 -23
- package/package.json +4 -1
- package/src/index.ts +28 -27
package/lib/index.js
CHANGED
|
@@ -54,7 +54,7 @@ var child_process_1 = require("child_process");
|
|
|
54
54
|
var koishi_1 = require("koishi");
|
|
55
55
|
var path_1 = __importDefault(require("path"));
|
|
56
56
|
var url_1 = require("url");
|
|
57
|
-
var
|
|
57
|
+
var ansi_to_html_1 = __importDefault(require("ansi-to-html"));
|
|
58
58
|
var encodings = ['utf8', 'utf16le', 'latin1', 'ucs2'];
|
|
59
59
|
exports.Config = koishi_1.Schema.object({
|
|
60
60
|
root: koishi_1.Schema.string().description('工作路径。').default(''),
|
|
@@ -101,24 +101,36 @@ function validateCdCommand(command, currentDir, rootDir, restrictDirectory) {
|
|
|
101
101
|
// 渲染终端输出为图片
|
|
102
102
|
function renderTerminalImage(ctx, workingDir, command, output) {
|
|
103
103
|
return __awaiter(this, void 0, void 0, function () {
|
|
104
|
-
var displayOutput, lines, commandLineLength, maxLineLength, charWidth, horizontalBuffer, containerWidth, fontPath, html, page, element, screenshot;
|
|
104
|
+
var ansiStrip, normalizeTabs, displayOutputRaw, displayOutput, lines, commandLineLength, visibleLineLengths, maxLineLength, charWidth, horizontalBuffer, containerWidth, ansi, coloredOutputHtml, fontPath, html, page, element, screenshot;
|
|
105
105
|
return __generator(this, function (_a) {
|
|
106
106
|
switch (_a.label) {
|
|
107
107
|
case 0:
|
|
108
108
|
if (!ctx.puppeteer) {
|
|
109
109
|
throw new Error('Puppeteer plugin is not available');
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
ansiStrip = function (text) { return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, ''); };
|
|
112
|
+
normalizeTabs = function (text) { return text.replace(/\t/g, ' '); };
|
|
113
|
+
displayOutputRaw = normalizeTabs(output || '(no output)');
|
|
114
|
+
displayOutput = displayOutputRaw.replace(/^\s+/, '');
|
|
112
115
|
lines = displayOutput.split(/\r?\n/);
|
|
113
|
-
commandLineLength = "".concat(workingDir, "$ ").concat(command).length;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
commandLineLength = ansiStrip("".concat(workingDir, "$ ").concat(command)).length;
|
|
117
|
+
visibleLineLengths = lines.map(function (line) { return ansiStrip(line).length; });
|
|
118
|
+
maxLineLength = Math.max.apply(Math, __spreadArray([commandLineLength], visibleLineLengths, false)) || commandLineLength;
|
|
119
|
+
charWidth = 7.1 // refined average width for JetBrains Mono 13px
|
|
116
120
|
;
|
|
117
|
-
horizontalBuffer =
|
|
121
|
+
horizontalBuffer = 56 // padding + borders + margin buffer
|
|
118
122
|
;
|
|
119
|
-
containerWidth = Math.max(600, Math.min(
|
|
123
|
+
containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)));
|
|
124
|
+
ansi = new ansi_to_html_1.default({
|
|
125
|
+
fg: '#cccccc',
|
|
126
|
+
bg: '#1e1e1e',
|
|
127
|
+
newline: true,
|
|
128
|
+
escapeXML: true,
|
|
129
|
+
stream: false,
|
|
130
|
+
});
|
|
131
|
+
coloredOutputHtml = ansi.toHtml(displayOutput);
|
|
120
132
|
fontPath = (0, url_1.pathToFileURL)(path_1.default.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href;
|
|
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:
|
|
133
|
+
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 max-width: 1600px;\n min-width: 600px;\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: 8px 12px;\n white-space: pre;\n word-break: normal;\n line-height: 1.18;\n overflow-x: auto;\n }\n \n .command-line {\n display: flex;\n gap: 3px;\n align-items: baseline;\n margin-bottom: 2px;\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.12;\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(coloredOutputHtml, "</div>\n </div>\n </div>\n</body>\n</html>\n ");
|
|
122
134
|
return [4 /*yield*/, ctx.puppeteer.page()];
|
|
123
135
|
case 1:
|
|
124
136
|
page = _a.sent();
|
|
@@ -168,32 +180,25 @@ function apply(ctx, config) {
|
|
|
168
180
|
return __generator(this, function (_d) {
|
|
169
181
|
switch (_d.label) {
|
|
170
182
|
case 0:
|
|
171
|
-
(0, debug_log_1.debugLog)(ctx, 'input', { command: command });
|
|
172
183
|
if (!command) {
|
|
173
|
-
(0, debug_log_1.debugLog)(ctx, 'expect-text', { text: session.text('.expect-text') });
|
|
174
184
|
return [2 /*return*/, session.text('.expect-text')];
|
|
175
185
|
}
|
|
176
186
|
command = (0, koishi_1.h)('', koishi_1.h.parse(command)).toString(true);
|
|
177
|
-
(0, debug_log_1.debugLog)(ctx, 'parsed-command', { command: command });
|
|
178
187
|
filterList = (((_c = config.commandList) === null || _c === void 0 ? void 0 : _c.length) ? config.commandList : config.blockedCommands) || [];
|
|
179
188
|
filterMode = config.commandFilterMode || 'blacklist';
|
|
180
189
|
if (isCommandBlocked(command, filterMode, filterList)) {
|
|
181
|
-
(0, debug_log_1.debugLog)(ctx, 'blocked-command', { command: command, filterMode: filterMode, filterList: filterList });
|
|
182
190
|
return [2 /*return*/, session.text('.blocked-command')];
|
|
183
191
|
}
|
|
184
192
|
sessionId = session.uid || session.channelId;
|
|
185
193
|
rootDir = path_1.default.resolve(ctx.baseDir, config.root);
|
|
186
194
|
currentDir = sessionDirs.get(sessionId) || rootDir;
|
|
187
195
|
cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory);
|
|
188
|
-
(0, debug_log_1.debugLog)(ctx, 'cd-validation', { command: command, cdValidation: cdValidation });
|
|
189
196
|
if (!cdValidation.valid) {
|
|
190
|
-
(0, debug_log_1.debugLog)(ctx, 'restricted-directory', { text: session.text('.restricted-directory') });
|
|
191
197
|
return [2 /*return*/, session.text('.restricted-directory')];
|
|
192
198
|
}
|
|
193
199
|
timeout = config.timeout;
|
|
194
200
|
state = { command: command, timeout: timeout, output: '' };
|
|
195
201
|
if (!!config.renderImage) return [3 /*break*/, 2];
|
|
196
|
-
(0, debug_log_1.debugLog)(ctx, 'send-started', { text: session.text('.started', state) });
|
|
197
202
|
return [4 /*yield*/, session.send(session.text('.started', state))];
|
|
198
203
|
case 1:
|
|
199
204
|
_d.sent();
|
|
@@ -208,11 +213,9 @@ function apply(ctx, config) {
|
|
|
208
213
|
windowsHide: true,
|
|
209
214
|
});
|
|
210
215
|
child.stdout.on('data', function (data) {
|
|
211
|
-
(0, debug_log_1.debugLog)(ctx, 'stdout', { data: data.toString() });
|
|
212
216
|
state.output += data.toString();
|
|
213
217
|
});
|
|
214
218
|
child.stderr.on('data', function (data) {
|
|
215
|
-
(0, debug_log_1.debugLog)(ctx, 'stderr', { data: data.toString() });
|
|
216
219
|
state.output += data.toString();
|
|
217
220
|
});
|
|
218
221
|
child.on('close', function (code, signal) { return __awaiter(_this, void 0, void 0, function () {
|
|
@@ -224,11 +227,9 @@ function apply(ctx, config) {
|
|
|
224
227
|
state.signal = signal;
|
|
225
228
|
state.timeUsed = Date.now() - start;
|
|
226
229
|
state.output = state.output.trim();
|
|
227
|
-
(0, debug_log_1.debugLog)(ctx, 'close', { code: code, signal: signal, timeUsed: state.timeUsed, output: state.output });
|
|
228
230
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
229
231
|
if (cdValidation.newDir && code === 0) {
|
|
230
232
|
sessionDirs.set(sessionId, cdValidation.newDir);
|
|
231
|
-
(0, debug_log_1.debugLog)(ctx, 'cd-updated', { sessionId: sessionId, newDir: cdValidation.newDir });
|
|
232
233
|
}
|
|
233
234
|
if (!(config.renderImage && ctx.puppeteer)) return [3 /*break*/, 5];
|
|
234
235
|
_a.label = 1;
|
|
@@ -237,18 +238,15 @@ function apply(ctx, config) {
|
|
|
237
238
|
return [4 /*yield*/, renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')];
|
|
238
239
|
case 2:
|
|
239
240
|
image = _a.sent();
|
|
240
|
-
(0, debug_log_1.debugLog)(ctx, 'render-image-success');
|
|
241
241
|
resolve(image);
|
|
242
242
|
return [3 /*break*/, 4];
|
|
243
243
|
case 3:
|
|
244
244
|
error_1 = _a.sent();
|
|
245
245
|
ctx.logger.error('Failed to render terminal image:', error_1);
|
|
246
|
-
(0, debug_log_1.debugLog)(ctx, 'render-image-fail', { error: error_1 });
|
|
247
246
|
resolve(session.text('.finished', state));
|
|
248
247
|
return [3 /*break*/, 4];
|
|
249
248
|
case 4: return [3 /*break*/, 6];
|
|
250
249
|
case 5:
|
|
251
|
-
(0, debug_log_1.debugLog)(ctx, 'send-finished', { text: session.text('.finished', state) });
|
|
252
250
|
resolve(session.text('.finished', state));
|
|
253
251
|
_a.label = 6;
|
|
254
252
|
case 6: return [2 /*return*/];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-spawn-modified",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Run shell commands with Koishi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bot",
|
|
@@ -83,5 +83,8 @@
|
|
|
83
83
|
"puppeteer"
|
|
84
84
|
]
|
|
85
85
|
}
|
|
86
|
+
},
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"ansi-to-html": "^0.7.2"
|
|
86
89
|
}
|
|
87
90
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { exec } from 'child_process'
|
|
|
2
2
|
import { Context, h, Schema, Time } from 'koishi'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { pathToFileURL } from 'url'
|
|
5
|
-
import
|
|
5
|
+
import AnsiToHtml from 'ansi-to-html'
|
|
6
6
|
|
|
7
7
|
declare module 'koishi' {
|
|
8
8
|
interface Context {
|
|
@@ -91,13 +91,26 @@ 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
|
|
94
|
+
const ansiStrip = (text: string) => text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
|
|
95
|
+
const normalizeTabs = (text: string) => text.replace(/\t/g, ' ')
|
|
96
|
+
const displayOutputRaw = normalizeTabs(output || '(no output)')
|
|
97
|
+
const displayOutput = displayOutputRaw.replace(/^\s+/, '')
|
|
95
98
|
const lines = displayOutput.split(/\r?\n/)
|
|
96
|
-
const commandLineLength = `${workingDir}$ ${command}
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
99
|
+
const commandLineLength = ansiStrip(`${workingDir}$ ${command}`).length
|
|
100
|
+
const visibleLineLengths = lines.map(line => ansiStrip(line).length)
|
|
101
|
+
const maxLineLength = Math.max(commandLineLength, ...visibleLineLengths) || commandLineLength
|
|
102
|
+
const charWidth = 7.1 // refined average width for JetBrains Mono 13px
|
|
103
|
+
const horizontalBuffer = 56 // padding + borders + margin buffer
|
|
104
|
+
const containerWidth = Math.max(600, Math.min(1600, Math.ceil(maxLineLength * charWidth + horizontalBuffer)))
|
|
105
|
+
|
|
106
|
+
const ansi = new AnsiToHtml({
|
|
107
|
+
fg: '#cccccc',
|
|
108
|
+
bg: '#1e1e1e',
|
|
109
|
+
newline: true,
|
|
110
|
+
escapeXML: true,
|
|
111
|
+
stream: false,
|
|
112
|
+
})
|
|
113
|
+
const coloredOutputHtml = ansi.toHtml(displayOutput)
|
|
101
114
|
|
|
102
115
|
const fontPath = pathToFileURL(path.resolve(__dirname, '../fonts/JetBrainsMono-Regular.ttf')).href
|
|
103
116
|
|
|
@@ -127,6 +140,8 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
|
|
|
127
140
|
padding: 0;
|
|
128
141
|
display: inline-block;
|
|
129
142
|
width: ${containerWidth}px;
|
|
143
|
+
max-width: 1600px;
|
|
144
|
+
min-width: 600px;
|
|
130
145
|
}
|
|
131
146
|
|
|
132
147
|
.terminal {
|
|
@@ -169,18 +184,18 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
|
|
|
169
184
|
.button.close { background: #ff5f56; }
|
|
170
185
|
|
|
171
186
|
.content {
|
|
172
|
-
padding:
|
|
187
|
+
padding: 8px 12px;
|
|
173
188
|
white-space: pre;
|
|
174
189
|
word-break: normal;
|
|
175
|
-
line-height: 1.
|
|
190
|
+
line-height: 1.18;
|
|
176
191
|
overflow-x: auto;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
.command-line {
|
|
180
195
|
display: flex;
|
|
181
|
-
gap:
|
|
196
|
+
gap: 3px;
|
|
182
197
|
align-items: baseline;
|
|
183
|
-
margin-bottom:
|
|
198
|
+
margin-bottom: 2px;
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
.prompt {
|
|
@@ -198,7 +213,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
|
|
|
198
213
|
|
|
199
214
|
.output {
|
|
200
215
|
color: #cccccc;
|
|
201
|
-
line-height: 1.
|
|
216
|
+
line-height: 1.12;
|
|
202
217
|
white-space: pre;
|
|
203
218
|
word-break: normal;
|
|
204
219
|
overflow-x: auto;
|
|
@@ -220,7 +235,7 @@ async function renderTerminalImage(ctx: Context, workingDir: string, command: st
|
|
|
220
235
|
<div class="prompt">${escapeHtml(workingDir)}$</div>
|
|
221
236
|
<div class="command">${escapeHtml(command)}</div>
|
|
222
237
|
</div>
|
|
223
|
-
<div class="output">${
|
|
238
|
+
<div class="output">${coloredOutputHtml}</div>
|
|
224
239
|
</div>
|
|
225
240
|
</div>
|
|
226
241
|
</body>
|
|
@@ -255,19 +270,15 @@ export function apply(ctx: Context, config: Config) {
|
|
|
255
270
|
|
|
256
271
|
ctx.command('exec <command:text>', { authority: config.authority ?? 4 })
|
|
257
272
|
.action(async ({ session }, command) => {
|
|
258
|
-
debugLog(ctx, 'input', { command })
|
|
259
273
|
if (!command) {
|
|
260
|
-
debugLog(ctx, 'expect-text', { text: session.text('.expect-text') })
|
|
261
274
|
return session.text('.expect-text')
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
command = h('', h.parse(command)).toString(true)
|
|
265
|
-
debugLog(ctx, 'parsed-command', { command })
|
|
266
278
|
// 检查命令过滤(黑/白名单)
|
|
267
279
|
const filterList = (config.commandList?.length ? config.commandList : config.blockedCommands) || []
|
|
268
280
|
const filterMode = config.commandFilterMode || 'blacklist'
|
|
269
281
|
if (isCommandBlocked(command, filterMode, filterList)) {
|
|
270
|
-
debugLog(ctx, 'blocked-command', { command, filterMode, filterList })
|
|
271
282
|
return session.text('.blocked-command')
|
|
272
283
|
}
|
|
273
284
|
const sessionId = session.uid || session.channelId
|
|
@@ -275,15 +286,12 @@ export function apply(ctx: Context, config: Config) {
|
|
|
275
286
|
const currentDir = sessionDirs.get(sessionId) || rootDir
|
|
276
287
|
// 验证 cd 命令
|
|
277
288
|
const cdValidation = validateCdCommand(command, currentDir, rootDir, config.restrictDirectory)
|
|
278
|
-
debugLog(ctx, 'cd-validation', { command, cdValidation })
|
|
279
289
|
if (!cdValidation.valid) {
|
|
280
|
-
debugLog(ctx, 'restricted-directory', { text: session.text('.restricted-directory') })
|
|
281
290
|
return session.text('.restricted-directory')
|
|
282
291
|
}
|
|
283
292
|
const { timeout } = config
|
|
284
293
|
const state: State = { command, timeout, output: '' }
|
|
285
294
|
if (!config.renderImage) {
|
|
286
|
-
debugLog(ctx, 'send-started', { text: session.text('.started', state) })
|
|
287
295
|
await session.send(session.text('.started', state))
|
|
288
296
|
}
|
|
289
297
|
return new Promise((resolve) => {
|
|
@@ -296,11 +304,9 @@ export function apply(ctx: Context, config: Config) {
|
|
|
296
304
|
windowsHide: true,
|
|
297
305
|
})
|
|
298
306
|
child.stdout.on('data', (data) => {
|
|
299
|
-
debugLog(ctx, 'stdout', { data: data.toString() })
|
|
300
307
|
state.output += data.toString()
|
|
301
308
|
})
|
|
302
309
|
child.stderr.on('data', (data) => {
|
|
303
|
-
debugLog(ctx, 'stderr', { data: data.toString() })
|
|
304
310
|
state.output += data.toString()
|
|
305
311
|
})
|
|
306
312
|
child.on('close', async (code, signal) => {
|
|
@@ -308,25 +314,20 @@ export function apply(ctx: Context, config: Config) {
|
|
|
308
314
|
state.signal = signal
|
|
309
315
|
state.timeUsed = Date.now() - start
|
|
310
316
|
state.output = state.output.trim()
|
|
311
|
-
debugLog(ctx, 'close', { code, signal, timeUsed: state.timeUsed, output: state.output })
|
|
312
317
|
// 更新当前目录(如果是 cd 命令且执行成功)
|
|
313
318
|
if (cdValidation.newDir && code === 0) {
|
|
314
319
|
sessionDirs.set(sessionId, cdValidation.newDir)
|
|
315
|
-
debugLog(ctx, 'cd-updated', { sessionId, newDir: cdValidation.newDir })
|
|
316
320
|
}
|
|
317
321
|
// 渲染为图片或返回文本
|
|
318
322
|
if (config.renderImage && ctx.puppeteer) {
|
|
319
323
|
try {
|
|
320
324
|
const image = await renderTerminalImage(ctx, currentDir, command, state.output || '(no output)')
|
|
321
|
-
debugLog(ctx, 'render-image-success')
|
|
322
325
|
resolve(image)
|
|
323
326
|
} catch (error) {
|
|
324
327
|
ctx.logger.error('Failed to render terminal image:', error)
|
|
325
|
-
debugLog(ctx, 'render-image-fail', { error })
|
|
326
328
|
resolve(session.text('.finished', state))
|
|
327
329
|
}
|
|
328
330
|
} else {
|
|
329
|
-
debugLog(ctx, 'send-finished', { text: session.text('.finished', state) })
|
|
330
331
|
resolve(session.text('.finished', state))
|
|
331
332
|
}
|
|
332
333
|
})
|