@cloud411716/fancy-webnovel 0.1.31 → 0.1.33
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/package.json +1 -1
- package/plugins/fancy-bootstrap/registry.js +66 -164
- package/plugins/fancy-bootstrap/scripts/infra.js +65 -0
- package/plugins/fancy-bootstrap/scripts/templates.js +185 -0
- package/plugins/fancy-scan/scripts/run-scan.js +1 -7
- package/plugins/fancy-bootstrap/cordis.patch.yml +0 -3
- package/plugins/fancy-bootstrap/scripts/path-utils.js +0 -17
- package/plugins/fancy-scan/index.js +0 -197
package/package.json
CHANGED
|
@@ -1,56 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* registry.js — 合并注册所有命令
|
|
3
3
|
* fancy-bootstrap + fancy-scan
|
|
4
|
+
*
|
|
5
|
+
* 所有消息模板 → scripts/templates.js
|
|
6
|
+
* 所有基础设施 → scripts/infra.js
|
|
4
7
|
*/
|
|
5
8
|
import { fileURLToPath } from 'url';
|
|
6
9
|
import { dirname, join } from 'path';
|
|
7
10
|
import { existsSync } from 'fs';
|
|
11
|
+
import { notify, llmFollowup, spawnScript } from './scripts/infra.js';
|
|
12
|
+
import { scan, bootstrap, PLATFORM_TABLE, platformLabel } from './scripts/templates.js';
|
|
8
13
|
|
|
9
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const SCRIPTS_DIR = join(__dirname, 'scripts');
|
|
10
16
|
|
|
11
17
|
// ---------------------------------------------------------------------------
|
|
12
|
-
//
|
|
18
|
+
// 路径工具
|
|
13
19
|
// ---------------------------------------------------------------------------
|
|
14
|
-
const BOOTSTRAP_DIR = __dirname; // registry.js is inside plugins/fancy-bootstrap/
|
|
15
|
-
const BOOTSTRAP_SCRIPTS_DIR = join(BOOTSTRAP_DIR, 'scripts');
|
|
16
|
-
|
|
17
|
-
function notifyBootstrap(session, text) {
|
|
18
|
-
queueMicrotask(() => {
|
|
19
|
-
session.append(
|
|
20
|
-
'user/message',
|
|
21
|
-
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
22
|
-
{ surfaceOp: 'append' }
|
|
23
|
-
);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
20
|
|
|
27
21
|
function isValidPath(p) {
|
|
28
|
-
return !p.includes('..') && !/[<>:"|?*[\x00-\x1f]/.test(p) && !/\s/.test(p);
|
|
22
|
+
return !p.includes('..') && !/[<>:"'|?*[\x00-\x1f]/.test(p) && !/\s/.test(p);
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const { spawn } = await import('child_process');
|
|
35
|
-
const proc = spawn(
|
|
36
|
-
'node', [initScript, projectRoot],
|
|
37
|
-
{ timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
38
|
-
);
|
|
39
|
-
const outParts = [], errParts = [];
|
|
40
|
-
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
41
|
-
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
42
|
-
proc.on('close', (code) => {
|
|
43
|
-
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(stdout);
|
|
46
|
-
resolve({ ok: parsed.ok === true, output: stdout, error: parsed.error || stderr || undefined });
|
|
47
|
-
} catch (_) {
|
|
48
|
-
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
52
|
-
});
|
|
53
|
-
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// fancy-bootstrap
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
54
28
|
|
|
55
29
|
async function applyBootstrap(ctx) {
|
|
56
30
|
ctx.commands.register({
|
|
@@ -61,65 +35,61 @@ async function applyBootstrap(ctx) {
|
|
|
61
35
|
const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
62
36
|
|
|
63
37
|
let projectRoot = rawInput;
|
|
38
|
+
|
|
39
|
+
// 无参数 → 弹窗
|
|
64
40
|
if (!projectRoot) {
|
|
65
41
|
const result = await ctx.userQuestions.ask({
|
|
66
|
-
questions: [
|
|
67
|
-
id: 'path',
|
|
68
|
-
question: '📂 请输入项目根路径 ( 或者输入 . 代表当前路径 )',
|
|
69
|
-
}],
|
|
42
|
+
questions: [bootstrap.askPath()],
|
|
70
43
|
});
|
|
71
|
-
projectRoot = result.answers[0]?.custom
|
|
44
|
+
projectRoot = result.answers[0]?.custom?.trim() || '';
|
|
72
45
|
}
|
|
73
46
|
|
|
74
47
|
if (!projectRoot) {
|
|
75
|
-
|
|
48
|
+
notify(session, bootstrap.notify({ type: 'no_path' }));
|
|
76
49
|
return { kind: 'success', text: '' };
|
|
77
50
|
}
|
|
78
51
|
|
|
79
52
|
if (projectRoot === '.') {
|
|
80
53
|
projectRoot = process.cwd();
|
|
81
|
-
|
|
54
|
+
notify(session, bootstrap.notify({ type: 'using_cwd', projectRoot }));
|
|
82
55
|
}
|
|
83
56
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
57
|
+
if (!isValidPath(projectRoot)) {
|
|
58
|
+
notify(session, bootstrap.notify({ type: 'invalid_path' }));
|
|
86
59
|
return { kind: 'success', text: '' };
|
|
87
60
|
}
|
|
88
61
|
|
|
89
62
|
if (existsSync(join(projectRoot, '.fancy-deployed'))) {
|
|
90
|
-
|
|
63
|
+
notify(session, bootstrap.notify({ type: 'already_initialized' }));
|
|
91
64
|
return { kind: 'success', text: '' };
|
|
92
65
|
}
|
|
93
66
|
|
|
94
67
|
const confirm = await ctx.userQuestions.ask({
|
|
95
68
|
questions: [{
|
|
96
69
|
id: 'confirm',
|
|
97
|
-
question:
|
|
70
|
+
question: bootstrap.notify({ type: 'confirm', projectRoot }),
|
|
98
71
|
detail: projectRoot,
|
|
99
72
|
hideCustomInput: true,
|
|
100
|
-
options:
|
|
101
|
-
{ label: '确认创建', description: '开始在此目录初始化项目' },
|
|
102
|
-
{ label: '取消', description: '取消本次操作' },
|
|
103
|
-
],
|
|
73
|
+
options: bootstrap.confirmOptions(),
|
|
104
74
|
}],
|
|
105
75
|
});
|
|
106
76
|
const ans = confirm.answers[0];
|
|
107
77
|
const chosen = ans?.selected?.[0] ?? ans?.custom;
|
|
108
78
|
if (chosen !== '确认创建') {
|
|
109
|
-
|
|
79
|
+
notify(session, bootstrap.notify({ type: 'cancelled' }));
|
|
110
80
|
return { kind: 'success', text: '' };
|
|
111
81
|
}
|
|
112
82
|
|
|
113
|
-
|
|
114
|
-
const result = await
|
|
83
|
+
notify(session, bootstrap.notify({ type: 'initializing', projectRoot }));
|
|
84
|
+
const result = await spawnScript('node', [join(SCRIPTS_DIR, 'init.js'), projectRoot], { timeout: 30000 });
|
|
115
85
|
|
|
116
86
|
if (result.ok) {
|
|
117
|
-
|
|
87
|
+
notify(session, bootstrap.notify({ type: 'success' }));
|
|
118
88
|
} else {
|
|
119
|
-
const
|
|
89
|
+
const err = (typeof result.error === 'object' && result.error !== null)
|
|
120
90
|
? (result.error.message || JSON.stringify(result.error))
|
|
121
91
|
: String(result.error || '未知错误');
|
|
122
|
-
|
|
92
|
+
notify(session, bootstrap.notify({ type: 'error', err }));
|
|
123
93
|
}
|
|
124
94
|
|
|
125
95
|
return { kind: 'success', text: '' };
|
|
@@ -130,56 +100,6 @@ async function applyBootstrap(ctx) {
|
|
|
130
100
|
// ---------------------------------------------------------------------------
|
|
131
101
|
// fancy-scan
|
|
132
102
|
// ---------------------------------------------------------------------------
|
|
133
|
-
const SCAN_DIR = join(dirname(__dirname), 'fancy-scan'); // from plugins/fancy-bootstrap/ up to plugins/, then fancy-scan
|
|
134
|
-
const SCAN_SCRIPTS_DIR = join(SCAN_DIR, 'scripts');
|
|
135
|
-
|
|
136
|
-
const PLATFORM_TABLE = [
|
|
137
|
-
['1', 'qidian', '长篇', '起点'],
|
|
138
|
-
['2', 'fanqie', '长篇', '番茄'],
|
|
139
|
-
['3', 'jinjiang', '长篇', '晋江'],
|
|
140
|
-
['4', 'zhihu', '长/短','知乎'],
|
|
141
|
-
['5', 'dianzhong','短篇','点众'],
|
|
142
|
-
['6', 'qimao', '长/短','七猫'],
|
|
143
|
-
];
|
|
144
|
-
|
|
145
|
-
function platformLabel(platform) {
|
|
146
|
-
const row = PLATFORM_TABLE.find(r => r[1] === platform);
|
|
147
|
-
return row ? row[3] : platform;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function notifyScan(session, text) {
|
|
151
|
-
queueMicrotask(() => {
|
|
152
|
-
session.append(
|
|
153
|
-
'user/message',
|
|
154
|
-
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
155
|
-
{ surfaceOp: 'append' }
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function runScanScript(projectRoot, platform, length) {
|
|
161
|
-
return new Promise(async (resolve) => {
|
|
162
|
-
const scriptPath = join(SCAN_SCRIPTS_DIR, 'run-scan.js');
|
|
163
|
-
const { spawn } = await import('child_process');
|
|
164
|
-
const proc = spawn(
|
|
165
|
-
'node', [scriptPath, '--project-root', projectRoot, '--platform', platform, '--length', length],
|
|
166
|
-
{ timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
167
|
-
);
|
|
168
|
-
const outParts = [], errParts = [];
|
|
169
|
-
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
170
|
-
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
171
|
-
proc.on('close', (code) => {
|
|
172
|
-
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
173
|
-
try {
|
|
174
|
-
const parsed = JSON.parse(stdout);
|
|
175
|
-
resolve({ ok: parsed.ok === true, output: stdout, error: parsed.error ? (parsed.error.message || JSON.stringify(parsed.error)) : stderr || undefined });
|
|
176
|
-
} catch (_) {
|
|
177
|
-
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
103
|
|
|
184
104
|
async function applyScan(ctx) {
|
|
185
105
|
ctx.commands.register({
|
|
@@ -194,7 +114,7 @@ async function applyScan(ctx) {
|
|
|
194
114
|
if (!projectRoot) projectRoot = process.cwd();
|
|
195
115
|
|
|
196
116
|
if (!existsSync(join(projectRoot, '.fancy-deployed'))) {
|
|
197
|
-
|
|
117
|
+
notify(session, scan.notify({ type: 'not_initialized' }));
|
|
198
118
|
return { kind: 'success', text: '' };
|
|
199
119
|
}
|
|
200
120
|
|
|
@@ -218,46 +138,39 @@ async function applyScan(ctx) {
|
|
|
218
138
|
}
|
|
219
139
|
}
|
|
220
140
|
|
|
141
|
+
// 参数不全 → 弹窗选择
|
|
221
142
|
if (!platform || !length || !PLATFORM_TABLE.find(r => r[1] === platform)) {
|
|
222
|
-
|
|
223
|
-
PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[3]}(${r[2]})`).join('\n') +
|
|
224
|
-
'\n\n- 长篇:关注 追读、月票、订阅 等指标\n- 短篇:关注 传播、完读率 等指标\n- 知乎和七猫同时支持长篇和短篇');
|
|
143
|
+
notify(session, scan.platformList());
|
|
225
144
|
|
|
226
145
|
const choice = await ctx.userQuestions.ask({
|
|
227
|
-
questions: [
|
|
146
|
+
questions: [scan.askPlatform()],
|
|
228
147
|
});
|
|
229
148
|
const answer = choice.answers[0]?.custom?.trim();
|
|
230
149
|
if (!answer) {
|
|
231
|
-
|
|
150
|
+
notify(session, scan.notify({ type: 'cancelled_with_hint' }));
|
|
232
151
|
return { kind: 'success', text: '' };
|
|
233
152
|
}
|
|
234
153
|
const numMatch = answer.match(/^(\d+)$/);
|
|
235
154
|
const row = numMatch ? PLATFORM_TABLE.find(r => r[0] === numMatch[1]) : null;
|
|
236
155
|
if (!row) {
|
|
237
|
-
|
|
156
|
+
notify(session, scan.notify({ type: 'invalid_choice' }));
|
|
238
157
|
return { kind: 'success', text: '' };
|
|
239
158
|
}
|
|
240
159
|
platform = row[1];
|
|
241
|
-
length = (platform === 'zhihu' || platform === 'qimao')
|
|
160
|
+
length = (platform === 'zhihu' || platform === 'qimao')
|
|
161
|
+
? null
|
|
162
|
+
: (row[2] === '长篇' ? 'long' : 'short');
|
|
242
163
|
}
|
|
243
164
|
|
|
244
165
|
// 知乎/七猫二次选择篇幅
|
|
245
166
|
if (platform === 'zhihu' || platform === 'qimao') {
|
|
246
167
|
if (!length || !['long', 'short'].includes(length)) {
|
|
247
168
|
const lenResult = await ctx.userQuestions.ask({
|
|
248
|
-
questions: [
|
|
249
|
-
id: 'length',
|
|
250
|
-
question: `${platformLabel(platform)} 同时支持长篇和短篇,请选择:`,
|
|
251
|
-
hideCustomInput: true,
|
|
252
|
-
options: [
|
|
253
|
-
{ label: '长篇', description: '' },
|
|
254
|
-
{ label: '短篇', description: '' },
|
|
255
|
-
],
|
|
256
|
-
}],
|
|
169
|
+
questions: [scan.askLength(platform)],
|
|
257
170
|
});
|
|
258
171
|
const lenAnswer = lenResult.answers[0]?.selected?.[0] ?? lenResult.answers[0]?.custom;
|
|
259
172
|
if (!lenAnswer || lenAnswer === '取消') {
|
|
260
|
-
|
|
173
|
+
notify(session, scan.notify({ type: 'cancelled' }));
|
|
261
174
|
return { kind: 'success', text: '' };
|
|
262
175
|
}
|
|
263
176
|
length = (lenAnswer === '长篇') ? 'long' : 'short';
|
|
@@ -266,56 +179,44 @@ async function applyScan(ctx) {
|
|
|
266
179
|
length = 'long';
|
|
267
180
|
}
|
|
268
181
|
|
|
269
|
-
|
|
182
|
+
notify(session, scan.notify({ type: 'collecting', platform, length }));
|
|
270
183
|
|
|
271
|
-
const
|
|
184
|
+
const scriptResult = await spawnScript(
|
|
185
|
+
'node',
|
|
186
|
+
[join(SCRIPTS_DIR, '..', 'fancy-scan', 'scripts', 'run-scan.js'),
|
|
187
|
+
'--project-root', projectRoot, '--platform', platform, '--length', length],
|
|
188
|
+
{ timeout: 120000 }
|
|
189
|
+
);
|
|
272
190
|
|
|
273
|
-
if (!
|
|
274
|
-
const
|
|
275
|
-
? (
|
|
276
|
-
: String(
|
|
277
|
-
|
|
191
|
+
if (!scriptResult.ok) {
|
|
192
|
+
const err = (typeof scriptResult.error === 'object' && scriptResult.error !== null)
|
|
193
|
+
? (scriptResult.error.message || JSON.stringify(scriptResult.error))
|
|
194
|
+
: String(scriptResult.error || '未知错误');
|
|
195
|
+
notify(session, scan.notify({ type: 'failed', err }));
|
|
278
196
|
return { kind: 'success', text: '' };
|
|
279
197
|
}
|
|
280
198
|
|
|
281
|
-
//
|
|
199
|
+
// 解析 receipt
|
|
282
200
|
let receipt;
|
|
283
|
-
try { receipt = JSON.parse(
|
|
284
|
-
|
|
201
|
+
try { receipt = JSON.parse(scriptResult.output); } catch (_) {
|
|
202
|
+
notify(session, scan.notify({ type: 'parse_error' }));
|
|
285
203
|
return { kind: 'success', text: '' };
|
|
286
204
|
}
|
|
287
205
|
|
|
288
206
|
const files = receipt.scan_files || [];
|
|
289
|
-
const reportFile = receipt.report_file || '';
|
|
290
207
|
const topicFile = receipt.topic_decision || '';
|
|
291
|
-
const fileList = files.map(f => '- ' + f.replace(projectRoot + '/', '')).join('\n');
|
|
292
|
-
|
|
293
|
-
// Step 8-9: 通知 LLM 执行分析
|
|
294
|
-
notifyScan(session,
|
|
295
|
-
`📊 采集完成,正在将数据交给 LLM 进行分析...\n` +
|
|
296
|
-
`📁 生成文件:\n${fileList}\n` +
|
|
297
|
-
`📄 报告:${reportFile.replace(projectRoot + '/', '')}\n` +
|
|
298
|
-
`💡 分析报告将写入:${topicFile.replace(projectRoot + '/', '')}`
|
|
299
|
-
);
|
|
300
208
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`3. 生成扫榜报告(包含:市场趋势、热门题材分析、用户画像、竞争度评估)\n` +
|
|
311
|
-
`4. 给出 3-5 个可行的选题方向建议\n` +
|
|
312
|
-
`5. 将完整分析报告追加写入:${topicFile}\n\n` +
|
|
313
|
-
`报告要求:\n- 客观分析数据,不臆测\n- 选题建议要有差异化竞争力\n- 报告语言:中文`;
|
|
314
|
-
|
|
315
|
-
invocation.agent.followup({
|
|
316
|
-
content: [{ type: 'text', text: analysisPrompt }],
|
|
317
|
-
source: { kind: 'user' }
|
|
209
|
+
// 通知用户 + 触发 LLM 分析
|
|
210
|
+
notify(session, scan.notify({ type: 'handover', platform, length, files, topicFile, projectRoot }));
|
|
211
|
+
|
|
212
|
+
const prompt = scan.llmAnalysisPrompt({
|
|
213
|
+
platform, length,
|
|
214
|
+
date: receipt.date || '',
|
|
215
|
+
files,
|
|
216
|
+
topicFile,
|
|
217
|
+
projectRoot,
|
|
318
218
|
});
|
|
219
|
+
llmFollowup(invocation, prompt);
|
|
319
220
|
|
|
320
221
|
return { kind: 'success', text: '' };
|
|
321
222
|
}
|
|
@@ -323,8 +224,9 @@ async function applyScan(ctx) {
|
|
|
323
224
|
}
|
|
324
225
|
|
|
325
226
|
// ---------------------------------------------------------------------------
|
|
326
|
-
//
|
|
227
|
+
// 导出
|
|
327
228
|
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
328
230
|
export const inject = ['commands', 'userQuestions'];
|
|
329
231
|
|
|
330
232
|
export async function apply(ctx) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infra.js — DSH 插件核心基础设施
|
|
3
|
+
*
|
|
4
|
+
* 提供:
|
|
5
|
+
* notify(session, text) — 打印消息给用户(不触发 LLM)
|
|
6
|
+
* llmFollowup(invocation, prompt) — 发送消息给 LLM,让 LLM 继续处理
|
|
7
|
+
* spawnScript(cmd, args, opts) — spawn 子进程,解析 stdout JSON
|
|
8
|
+
*
|
|
9
|
+
* 所有 fancy-* 插件均直接 import 此文件。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// notify — 打印消息给用户(queueMicrotask 避免打断 LLM 输出顺序)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export function notify(session, text) {
|
|
17
|
+
queueMicrotask(() => {
|
|
18
|
+
session.append(
|
|
19
|
+
'user/message',
|
|
20
|
+
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
21
|
+
{ surfaceOp: 'append' }
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// llmFollowup — 发送消息给 LLM,让 LLM 在同一 session 继续处理
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export function llmFollowup(invocation, content) {
|
|
31
|
+
const message = typeof content === 'string'
|
|
32
|
+
? { content: [{ type: 'text', text: content }], source: { kind: 'user' } }
|
|
33
|
+
: content; // 允许传入完整消息对象
|
|
34
|
+
invocation.agent.followup(message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// spawnScript — spawn 子进程,stdout 只收集 JSON,stderr 单独收集
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export function spawnScript(cmd, args, { timeout = 30000, cwd } = {}) {
|
|
42
|
+
return new Promise(async (resolve) => {
|
|
43
|
+
const { spawn } = await import('child_process');
|
|
44
|
+
const proc = spawn(cmd, args, { timeout, cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
45
|
+
const outParts = [], errParts = [];
|
|
46
|
+
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
47
|
+
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
48
|
+
proc.on('close', (code) => {
|
|
49
|
+
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(stdout);
|
|
52
|
+
resolve({
|
|
53
|
+
ok: parsed.ok === true,
|
|
54
|
+
output: stdout,
|
|
55
|
+
error: parsed.error
|
|
56
|
+
? (parsed.error.message || JSON.stringify(parsed.error))
|
|
57
|
+
: stderr || undefined,
|
|
58
|
+
});
|
|
59
|
+
} catch (_) {
|
|
60
|
+
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* templates.js — 所有插件模板字符串
|
|
3
|
+
*
|
|
4
|
+
* 包括:
|
|
5
|
+
* bootstrap 提示消息
|
|
6
|
+
* scan 提示消息
|
|
7
|
+
* LLM 分析 prompt 模板
|
|
8
|
+
*
|
|
9
|
+
* 原则:所有展示给用户的文字、发送给 LLM 的 prompt
|
|
10
|
+
* 均从此文件导出,插件主代码只做逻辑和拼装。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// 共享常量
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const PLATFORM_TABLE = [
|
|
18
|
+
['1', 'qidian', '长篇', '起点'],
|
|
19
|
+
['2', 'fanqie', '长篇', '番茄'],
|
|
20
|
+
['3', 'jinjiang', '长篇', '晋江'],
|
|
21
|
+
['4', 'zhihu', '长/短','知乎'],
|
|
22
|
+
['5', 'dianzhong','短篇', '点众'],
|
|
23
|
+
['6', 'qimao', '长/短','七猫'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function platformLabel(platform) {
|
|
27
|
+
const row = PLATFORM_TABLE.find(r => r[1] === platform);
|
|
28
|
+
return row ? row[3] : platform;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// fancy-bootstrap 模板
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export const bootstrap = {
|
|
36
|
+
askPath(question = '📂 请输入项目根路径 ( 或者输入 . 代表当前路径 )') {
|
|
37
|
+
return { question };
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
notify({ type, projectRoot }) {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'no_path':
|
|
43
|
+
return '⚠️ 未提供路径,已取消';
|
|
44
|
+
case 'using_cwd':
|
|
45
|
+
return `📂 使用当前目录:${projectRoot}`;
|
|
46
|
+
case 'invalid_path':
|
|
47
|
+
return '❌ 路径不合法(不能含空格、.. 或特殊字符)';
|
|
48
|
+
case 'already_initialized':
|
|
49
|
+
return '⚠️ 该目录已初始化(.fancy-deployed 已存在)';
|
|
50
|
+
case 'confirm':
|
|
51
|
+
return `❓ 确认将以下路径作为项目根?\n${projectRoot}`;
|
|
52
|
+
case 'cancelled':
|
|
53
|
+
return '🚫 已取消';
|
|
54
|
+
case 'initializing':
|
|
55
|
+
return `⏳ 正在初始化 ${projectRoot}...`;
|
|
56
|
+
case 'success':
|
|
57
|
+
return '✅ 项目初始化完成!\n下一步:运行 /fancy-scan 扫榜';
|
|
58
|
+
case 'error':
|
|
59
|
+
return (err) => '❌ 初始化失败:' + err;
|
|
60
|
+
default:
|
|
61
|
+
return String(type);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
confirmOptions() {
|
|
66
|
+
return [
|
|
67
|
+
{ label: '确认创建', description: '开始在此目录初始化项目' },
|
|
68
|
+
{ label: '取消', description: '取消本次操作' },
|
|
69
|
+
];
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// fancy-scan 模板
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export const scan = {
|
|
78
|
+
platformList() {
|
|
79
|
+
return (
|
|
80
|
+
'📋 支持的平台:\n' +
|
|
81
|
+
PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[3]}(${r[2]})`).join('\n') +
|
|
82
|
+
'\n\n- 长篇:关注 追读、月票、订阅 等指标\n' +
|
|
83
|
+
'- 短篇:关注 传播、完读率 等指标\n' +
|
|
84
|
+
'- 知乎和七猫同时支持长篇和短篇'
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
askPlatform() {
|
|
89
|
+
return { question: '📊 请选择要扫的平台(输入编号 1-6):' };
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
askLength(platform) {
|
|
93
|
+
return {
|
|
94
|
+
question: `${platformLabel(platform)} 同时支持长篇和短篇,请选择:` ,
|
|
95
|
+
hideCustomInput: true,
|
|
96
|
+
options: [
|
|
97
|
+
{ label: '长篇', description: '' },
|
|
98
|
+
{ label: '短篇', description: '' },
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
notify({ type, platform, length, files, receipt, topicFile, projectRoot, err }) {
|
|
104
|
+
const lenStr = length === 'long' ? '长篇' : '短篇';
|
|
105
|
+
const pLabel = platformLabel(platform);
|
|
106
|
+
|
|
107
|
+
switch (type) {
|
|
108
|
+
case 'not_initialized':
|
|
109
|
+
return '⚠️ 当前目录尚未初始化。请先运行 /fancy-bootstrap 进行初始化。';
|
|
110
|
+
|
|
111
|
+
case 'invalid_choice':
|
|
112
|
+
return '❌ 无效选择,请输入 1-6 的编号。';
|
|
113
|
+
|
|
114
|
+
case 'cancelled':
|
|
115
|
+
return '🚫 已取消';
|
|
116
|
+
|
|
117
|
+
case 'cancelled_with_hint':
|
|
118
|
+
return '⚠️ 已取消。请再次调用 /fancy-scan 继续。';
|
|
119
|
+
|
|
120
|
+
case 'collecting':
|
|
121
|
+
return `⏳ 正在采集 ${pLabel}(${lenStr})...`;
|
|
122
|
+
|
|
123
|
+
case 'scan_complete':
|
|
124
|
+
if (!files || files.length === 0) return `✅ ${pLabel}(${lenStr})采集完成`;
|
|
125
|
+
const fileList = files.map(f => ' - ' + f.replace(projectRoot + '/', '')).join('\n');
|
|
126
|
+
return `✅ ${pLabel}(${lenStr})采集完成\n\n📁 生成文件:\n${fileList}`;
|
|
127
|
+
|
|
128
|
+
case 'failed':
|
|
129
|
+
return '❌ 采集失败:' + err;
|
|
130
|
+
|
|
131
|
+
case 'parse_error':
|
|
132
|
+
return '❌ 采集结果解析失败';
|
|
133
|
+
|
|
134
|
+
case 'handover':
|
|
135
|
+
return (
|
|
136
|
+
`📊 采集完成,正在将数据交给 LLM 进行分析...\n` +
|
|
137
|
+
`📁 生成文件:\n${(files || []).map(f => '- ' + f.replace(projectRoot + '/', '')).join('\n')}\n` +
|
|
138
|
+
`💡 分析报告将写入:${(topicFile || '').replace(projectRoot + '/', '')}`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
return String(type);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 发送给 LLM 的分析 prompt 模板
|
|
148
|
+
* @param {object} params
|
|
149
|
+
* @param params.platform
|
|
150
|
+
* @param params.length
|
|
151
|
+
* @param params.date
|
|
152
|
+
* @param params.files string[] — 完整绝对路径列表
|
|
153
|
+
* @param params.topicFile string — topic_decision 文件完整路径
|
|
154
|
+
* @param params.projectRoot string
|
|
155
|
+
*/
|
|
156
|
+
llmAnalysisPrompt({ platform, length, date, files, topicFile, projectRoot }) {
|
|
157
|
+
const pLabel = platformLabel(platform);
|
|
158
|
+
const lenStr = length === 'long' ? '长篇' : '短篇';
|
|
159
|
+
const fileList = (files || [])
|
|
160
|
+
.map(f => '- ' + f.replace(projectRoot + '/', ''))
|
|
161
|
+
.join('\n');
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
`## 扫榜数据分析任务`,
|
|
165
|
+
``,
|
|
166
|
+
`平台:${pLabel}(${lenStr})`,
|
|
167
|
+
`时间:${date || ''}`,
|
|
168
|
+
``,
|
|
169
|
+
`已生成的文件:`,
|
|
170
|
+
fileList,
|
|
171
|
+
``,
|
|
172
|
+
`请执行以下步骤:`,
|
|
173
|
+
`1. 读取上述生成的原始数据文件(扫榜结果目录下的 .md 文件)`,
|
|
174
|
+
`2. 分析爆款书籍的题材、卖点、节奏、人设等特征`,
|
|
175
|
+
`3. 生成扫榜报告(包含:市场趋势、热门题材分析、用户画像、竞争度评估)`,
|
|
176
|
+
`4. 给出 3-5 个可行的选题方向建议`,
|
|
177
|
+
`5. 将完整分析报告追加写入:${topicFile}`,
|
|
178
|
+
``,
|
|
179
|
+
`报告要求:`,
|
|
180
|
+
`- 客观分析数据,不臆测`,
|
|
181
|
+
`- 选题建议要有差异化竞争力`,
|
|
182
|
+
`- 报告语言:中文`,
|
|
183
|
+
].join('\n');
|
|
184
|
+
},
|
|
185
|
+
};
|
|
@@ -426,11 +426,6 @@ async function main() {
|
|
|
426
426
|
console.error('=== 浏览器采集指令 END ===\n');
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
// 生成扫榜报告
|
|
430
|
-
const mdFiles = scanFiles.filter(f => f.endsWith('.md') && !f.includes('扫榜报告') && !f.includes('topic_decision'));
|
|
431
|
-
const reportFile = generateReport(platform, length, mdFiles, scanDir);
|
|
432
|
-
appendTopicDecision(platform, length, reportFile, scanDir);
|
|
433
|
-
|
|
434
429
|
// Step 6: 收集原始数据
|
|
435
430
|
const rawData = {};
|
|
436
431
|
for (const f of scanFiles) {
|
|
@@ -449,10 +444,9 @@ async function main() {
|
|
|
449
444
|
length,
|
|
450
445
|
date: today,
|
|
451
446
|
scan_files: scanFiles,
|
|
452
|
-
report_file: reportFile,
|
|
453
447
|
topic_decision: join(scanDir, `topic_decision_${today}.md`),
|
|
454
448
|
raw_data: rawData,
|
|
455
|
-
summary: `${PLATFORM_CN[platform]}(${length})采集完成,共 ${scanFiles.length} 个文件`,
|
|
449
|
+
summary: `${PLATFORM_CN[platform]}(${length === 'long' ? '长篇' : '短篇'})采集完成,共 ${scanFiles.length} 个文件`,
|
|
456
450
|
};
|
|
457
451
|
|
|
458
452
|
console.log(JSON.stringify(receipt));
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 路径工具
|
|
3
|
-
*/
|
|
4
|
-
export function expandHome(pathStr) {
|
|
5
|
-
if (pathStr.startsWith('~/') || pathStr === '~') {
|
|
6
|
-
return pathStr === '~' ? (process.env.HOME ?? '/root') : pathStr.replace('~', process.env.HOME ?? '/root');
|
|
7
|
-
}
|
|
8
|
-
return pathStr;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function resolvePath(pathStr) {
|
|
12
|
-
return expandHome(pathStr).replace(/\\+$/, '');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function joinPath(...parts) {
|
|
16
|
-
return parts.join('/').replace(/\/+/g, '/');
|
|
17
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fancy-scan DSH plugin
|
|
3
|
-
* 扫榜:平台数据采集 + 报告生成
|
|
4
|
-
*/
|
|
5
|
-
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { dirname, join } from 'path';
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
|
|
12
|
-
export const name = 'fancy-scan';
|
|
13
|
-
export const inject = ['commands', 'userQuestions'];
|
|
14
|
-
|
|
15
|
-
const PLATFORM_TABLE = [
|
|
16
|
-
['1', 'qidian', '长篇', '起点'],
|
|
17
|
-
['2', 'fanqie', '长篇', '番茄'],
|
|
18
|
-
['3', 'jinjiang','长篇', '晋江'],
|
|
19
|
-
['4', 'zhihu', '长/短', '知乎'],
|
|
20
|
-
['5', 'dianzhong','短篇','点众'],
|
|
21
|
-
['6', 'qimao', '长/短', '七猫'],
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function platformLabel(platform) {
|
|
25
|
-
const row = PLATFORM_TABLE.find(r => r[1] === platform);
|
|
26
|
-
return row ? row[3] : platform;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function notify(session, text) {
|
|
30
|
-
queueMicrotask(() => {
|
|
31
|
-
session.append(
|
|
32
|
-
'user/message',
|
|
33
|
-
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
34
|
-
{ surfaceOp: 'append' }
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getProjectRoot(invocation) {
|
|
40
|
-
// 从 rawInput 提取项目根(fancy-bootstrap 初始化后的 .fancy-deployed 所在目录)
|
|
41
|
-
// 优先取命令后的路径参数,否则用当前目录
|
|
42
|
-
const raw = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
43
|
-
return raw || process.cwd();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function checkInitialized(projectRoot) {
|
|
47
|
-
return existsSync(join(projectRoot, '.fancy-deployed'));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function buildPlatformChoices() {
|
|
51
|
-
return PLATFORM_TABLE.map(r => ({
|
|
52
|
-
label: `${r[0]}. ${r[3]}(${r[2]})`,
|
|
53
|
-
description: '',
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function buildLengthChoices(platform) {
|
|
58
|
-
if (platform === 'zhihu' || platform === 'qimao') {
|
|
59
|
-
return [
|
|
60
|
-
{ label: '长篇', description: '' },
|
|
61
|
-
{ label: '短篇', description: '' },
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
return null; // 无需二次选择
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function apply(ctx) {
|
|
68
|
-
ctx.commands.register({
|
|
69
|
-
name: 'fancy-scan',
|
|
70
|
-
description: '📊 扫榜分析 ( usage: /fancy-scan [平台] [篇幅] )',
|
|
71
|
-
handler: async (invocation) => {
|
|
72
|
-
const session = invocation.agent.session;
|
|
73
|
-
const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
74
|
-
|
|
75
|
-
// Step 1: 检查 .fancy-deployed
|
|
76
|
-
const projectRoot = getProjectRoot(invocation);
|
|
77
|
-
if (!checkInitialized(projectRoot)) {
|
|
78
|
-
notify(session, '⚠️ 当前目录尚未初始化。请先运行 /fancy-bootstrap 进行初始化。');
|
|
79
|
-
return { kind: 'success', text: '' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Step 2: 解析参数
|
|
83
|
-
let platform = '', length = '';
|
|
84
|
-
|
|
85
|
-
if (rawInput) {
|
|
86
|
-
// 从 rawInput 解析平台+篇幅
|
|
87
|
-
const parts = rawInput.split(/\s+/);
|
|
88
|
-
platform = parts[0] || '';
|
|
89
|
-
length = parts[1] || '';
|
|
90
|
-
// 尝试数字映射
|
|
91
|
-
const numMatch = platform.match(/^(\d+)$/);
|
|
92
|
-
if (numMatch) {
|
|
93
|
-
const row = PLATFORM_TABLE.find(r => r[0] === numMatch[1]);
|
|
94
|
-
if (row) {
|
|
95
|
-
platform = row[1];
|
|
96
|
-
if (!length && (platform === 'zhihu' || platform === 'qimao')) {
|
|
97
|
-
// 需要二次选择,默认长篇
|
|
98
|
-
length = 'long';
|
|
99
|
-
} else {
|
|
100
|
-
length = row[2] === '长篇' ? 'long' : (row[2] === '短篇' ? 'short' : 'long');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 无参数或参数不全 → 弹窗
|
|
107
|
-
if (!platform || !length || !PLATFORM_TABLE.find(r => r[1] === platform)) {
|
|
108
|
-
// 打印调用方式
|
|
109
|
-
notify(session, '[/fancy-scan] 调用方式:带参数(平台 篇幅)\n' +
|
|
110
|
-
'[/fancy-scan] 调用方式:无参数(弹窗选择)');
|
|
111
|
-
notify(session, '📋 支持的平台:\n' +
|
|
112
|
-
PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[3]}(${r[2]})`).join('\n') +
|
|
113
|
-
'\n\n- 长篇:关注 追读、月票、订阅 等指标\n- 短篇:关注 传播、完读率 等指标\n- 知乎和七猫同时支持长篇和短篇');
|
|
114
|
-
|
|
115
|
-
const choice = await ctx.userQuestions.ask({
|
|
116
|
-
questions: [{
|
|
117
|
-
id: 'platform',
|
|
118
|
-
question: '📊 请选择要扫的平台(输入编号 1-6):',
|
|
119
|
-
}],
|
|
120
|
-
});
|
|
121
|
-
const answer = choice.answers[0]?.custom?.trim();
|
|
122
|
-
if (!answer) {
|
|
123
|
-
notify(session, '⚠️ 已取消。请再次调用 /fancy-scan 继续。');
|
|
124
|
-
return { kind: 'success', text: '' };
|
|
125
|
-
}
|
|
126
|
-
const numMatch = answer.match(/^(\d+)$/);
|
|
127
|
-
const row = numMatch ? PLATFORM_TABLE.find(r => r[0] === numMatch[1]) : null;
|
|
128
|
-
if (!row) {
|
|
129
|
-
notify(session, '❌ 无效选择,请输入 1-6 的编号。');
|
|
130
|
-
return { kind: 'success', text: '' };
|
|
131
|
-
}
|
|
132
|
-
platform = row[1];
|
|
133
|
-
length = row[2] === '长/短' ? null : (row[2] === '长篇' ? 'long' : 'short');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 知乎/七猫需要二次选择篇幅
|
|
137
|
-
if (!length || length === 'long/short') {
|
|
138
|
-
const lengthChoices = buildLengthChoices(platform);
|
|
139
|
-
if (lengthChoices) {
|
|
140
|
-
const lenResult = await ctx.userQuestions.ask({
|
|
141
|
-
questions: [{
|
|
142
|
-
id: 'length',
|
|
143
|
-
question: `${platformLabel(platform)} 同时支持长篇和短篇,请选择:`,
|
|
144
|
-
hideCustomInput: true,
|
|
145
|
-
options: lengthChoices,
|
|
146
|
-
}],
|
|
147
|
-
});
|
|
148
|
-
const lenAnswer = lenResult.answers[0]?.selected?.[0] ?? lenResult.answers[0]?.custom;
|
|
149
|
-
if (!lenAnswer || lenAnswer === '取消') {
|
|
150
|
-
notify(session, '🚫 已取消');
|
|
151
|
-
return { kind: 'success', text: '' };
|
|
152
|
-
}
|
|
153
|
-
length = (lenAnswer === '长篇') ? 'long' : 'short';
|
|
154
|
-
} else {
|
|
155
|
-
length = 'long';
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
notify(session, `⏳ 正在采集 ${platformLabel(platform)}(${length === 'long' ? '长篇' : '短篇'})...`);
|
|
160
|
-
|
|
161
|
-
// Step 3: 调用采集脚本
|
|
162
|
-
const scriptPath = join(__dirname, 'scripts', 'run-scan.js');
|
|
163
|
-
const { ok, output, error } = await runScript('node', [scriptPath, '--project-root', projectRoot, '--platform', platform, '--length', length]);
|
|
164
|
-
|
|
165
|
-
if (!ok) {
|
|
166
|
-
notify(session, '❌ 采集失败:' + (error || '未知错误'));
|
|
167
|
-
} else {
|
|
168
|
-
// 解析输出中的文件列表
|
|
169
|
-
let files = [];
|
|
170
|
-
try { files = JSON.parse(output).files || []; } catch (_) {}
|
|
171
|
-
const fileList = files.length > 0 ? '\n\n📁 生成文件:\n' + files.map(f => ' - ' + f).join('\n') : '';
|
|
172
|
-
notify(session, `✅ ${platformLabel(platform)}(${length === 'long' ? '长篇' : '短篇'})采集完成${fileList}\n\n如需扫其他平台,请再次调用 /fancy-scan`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { kind: 'success', text: '' };
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function runScript(cmd, args) {
|
|
181
|
-
return new Promise((resolve) => {
|
|
182
|
-
const proc = require('child_process').spawn(cmd, args, { timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
183
|
-
const outParts = [], errParts = [];
|
|
184
|
-
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
185
|
-
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
186
|
-
proc.on('close', (code) => {
|
|
187
|
-
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
188
|
-
try {
|
|
189
|
-
const parsed = JSON.parse(stdout);
|
|
190
|
-
resolve({ ok: parsed.ok === true, output: stdout, error: parsed.error ? (parsed.error.message || JSON.stringify(parsed.error)) : stderr || undefined });
|
|
191
|
-
} catch (_) {
|
|
192
|
-
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
196
|
-
});
|
|
197
|
-
}
|