@cloud411716/fancy-webnovel 0.1.18 → 0.1.20
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 +3 -4
- package/plugins/fancy-bootstrap/index.js +3 -122
- package/plugins/fancy-bootstrap/registry.js +294 -0
- package/plugins/fancy-scan/index.js +197 -0
- package/plugins/fancy-scan/scripts/run-scan.js +453 -0
- package/plugins/fancy-scan/scripts/scan-output-format.md +207 -0
- package/plugins/fancy-scan/scripts/scraper-registry.md +115 -0
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud411716/fancy-webnovel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "plugins/fancy-bootstrap/index.js",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./plugins/fancy-bootstrap/
|
|
8
|
-
"./fancy-bootstrap": "./plugins/fancy-bootstrap/index.js"
|
|
9
|
-
"./fancy-scan": "./plugins/fancy-scan/index.js"
|
|
7
|
+
".": "./plugins/fancy-bootstrap/registry.js",
|
|
8
|
+
"./fancy-bootstrap": "./plugins/fancy-bootstrap/index.js"
|
|
10
9
|
},
|
|
11
10
|
"files": ["plugins/", "cordis.patch.yml"],
|
|
12
11
|
"peerDependencies": {
|
|
@@ -1,122 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { dirname, join } from 'path';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = dirname(__filename);
|
|
10
|
-
|
|
11
|
-
export const name = 'fancy-bootstrap';
|
|
12
|
-
export const inject = ['commands', 'userQuestions'];
|
|
13
|
-
|
|
14
|
-
function notify(session, text) {
|
|
15
|
-
queueMicrotask(() => {
|
|
16
|
-
session.append(
|
|
17
|
-
'user/message',
|
|
18
|
-
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
19
|
-
{ surfaceOp: 'append' }
|
|
20
|
-
);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const ILLEGAL_PATH_RE = /[<>:"|?*[\x00-\x1f]/;
|
|
25
|
-
|
|
26
|
-
function isValidPath(p) {
|
|
27
|
-
return !p.includes('..') && !ILLEGAL_PATH_RE.test(p) && !/\s/.test(p);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function apply(ctx) {
|
|
31
|
-
ctx.commands.register({
|
|
32
|
-
name: 'fancy-bootstrap',
|
|
33
|
-
description: '📁 初始化项目根 ( usage: /fancy-bootstrap [项目根] )',
|
|
34
|
-
handler: async (invocation) => {
|
|
35
|
-
const session = invocation.agent.session;
|
|
36
|
-
const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
37
|
-
|
|
38
|
-
let projectRoot = rawInput;
|
|
39
|
-
if (!projectRoot) {
|
|
40
|
-
const result = await ctx.userQuestions.ask({
|
|
41
|
-
questions: [{ id: 'path', question: '📂 请输入项目根路径(输入 . 代表当前路径)' }],
|
|
42
|
-
});
|
|
43
|
-
projectRoot = result.answers[0]?.custom ? result.answers[0].custom.trim() : '';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!projectRoot) {
|
|
47
|
-
notify(session, '⚠️ 未提供路径,已取消');
|
|
48
|
-
return { kind: 'success', text: '' };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (projectRoot === '.') {
|
|
52
|
-
projectRoot = process.cwd();
|
|
53
|
-
notify(session, '📍 使用当前路径:' + projectRoot);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!isValidPath(projectRoot)) {
|
|
57
|
-
notify(session, '❌ 路径不合法(不能含空格、.. 或特殊字符)');
|
|
58
|
-
return { kind: 'success', text: '' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const confirm = await ctx.userQuestions.ask({
|
|
62
|
-
questions: [{
|
|
63
|
-
id: 'confirm',
|
|
64
|
-
question: '❓ 确认将以下路径作为项目根?',
|
|
65
|
-
detail: projectRoot,
|
|
66
|
-
hideCustomInput: true,
|
|
67
|
-
options: [
|
|
68
|
-
{ label: '确认创建', description: '开始在此目录初始化项目' },
|
|
69
|
-
{ label: '取消', description: '取消本次操作' },
|
|
70
|
-
],
|
|
71
|
-
}],
|
|
72
|
-
});
|
|
73
|
-
const ans = confirm.answers[0];
|
|
74
|
-
const chosen = ans?.selected?.[0] ?? ans?.custom;
|
|
75
|
-
if (chosen === '取消') {
|
|
76
|
-
notify(session, '🚫 已取消');
|
|
77
|
-
return { kind: 'success', text: '' };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
notify(session, '⏳ 正在初始化项目...');
|
|
81
|
-
|
|
82
|
-
const scriptPath = join(__dirname, 'scripts', 'init.js');
|
|
83
|
-
const { ok, output, error } = await runScript('node', [scriptPath, projectRoot]);
|
|
84
|
-
|
|
85
|
-
if (!ok) {
|
|
86
|
-
let detail = error || output || '未知错误';
|
|
87
|
-
try { detail = JSON.parse(output).error.message || detail; } catch (_) {}
|
|
88
|
-
notify(session, '❌ 初始化失败:' + detail);
|
|
89
|
-
} else {
|
|
90
|
-
notify(session, '✅ 项目初始化完成:' + projectRoot + '\n\n下一步:/fancy-scan 扫榜分析');
|
|
91
|
-
}
|
|
92
|
-
return { kind: 'success', text: '' };
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function runScript(cmd, args) {
|
|
98
|
-
return new Promise((resolve) => {
|
|
99
|
-
const proc = spawn(cmd, args, { timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
100
|
-
const outParts = [], errParts = [];
|
|
101
|
-
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
102
|
-
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
103
|
-
proc.on('close', (code) => {
|
|
104
|
-
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
105
|
-
try {
|
|
106
|
-
const parsed = JSON.parse(stdout);
|
|
107
|
-
if (parsed.ok === true) {
|
|
108
|
-
resolve({ ok: true, output: stdout });
|
|
109
|
-
} else {
|
|
110
|
-
const e = parsed.error;
|
|
111
|
-
const msg = (typeof e === 'object' && e !== null)
|
|
112
|
-
? (e.message || JSON.stringify(e))
|
|
113
|
-
: (e || stderr || ('exit ' + code));
|
|
114
|
-
resolve({ ok: false, output: stdout, error: msg });
|
|
115
|
-
}
|
|
116
|
-
} catch (_) {
|
|
117
|
-
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
121
|
-
});
|
|
122
|
-
}
|
|
1
|
+
// Combined entry for @cloud411716/fancy-webnovel
|
|
2
|
+
// Registers: fancy-bootstrap, fancy-scan
|
|
3
|
+
export { registerCommands } from './registry.js';
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry.js — 合并注册所有命令
|
|
3
|
+
* fancy-bootstrap + fancy-scan
|
|
4
|
+
*/
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// fancy-bootstrap
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const BOOTSTRAP_DIR = join(__dirname, '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
|
+
|
|
27
|
+
function isValidPath(p) {
|
|
28
|
+
return !p.includes('..') && !/[<>:"|?*[\x00-\x1f]/.test(p) && !/\s/.test(p);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runBootstrapScript(projectRoot) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const initScript = join(BOOTSTRAP_SCRIPTS_DIR, 'init.js');
|
|
34
|
+
const proc = require('child_process').spawn(
|
|
35
|
+
'node', [initScript, projectRoot],
|
|
36
|
+
{ timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
37
|
+
);
|
|
38
|
+
const outParts = [], errParts = [];
|
|
39
|
+
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
40
|
+
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
41
|
+
proc.on('close', (code) => {
|
|
42
|
+
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(stdout);
|
|
45
|
+
resolve({ ok: parsed.ok === true, output: stdout, error: parsed.error || stderr || undefined });
|
|
46
|
+
} catch (_) {
|
|
47
|
+
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function applyBootstrap(ctx) {
|
|
55
|
+
ctx.commands.register({
|
|
56
|
+
name: 'fancy-bootstrap',
|
|
57
|
+
description: '📁 初始化项目根 ( usage: /fancy-bootstrap [项目根] )',
|
|
58
|
+
handler: async (invocation) => {
|
|
59
|
+
const session = invocation.agent.session;
|
|
60
|
+
const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
61
|
+
|
|
62
|
+
let projectRoot = rawInput;
|
|
63
|
+
if (!projectRoot) {
|
|
64
|
+
const result = await ctx.userQuestions.ask({
|
|
65
|
+
questions: [{
|
|
66
|
+
id: 'path',
|
|
67
|
+
question: '📂 请输入项目根路径 ( 或者输入 . 代表当前路径 )',
|
|
68
|
+
}],
|
|
69
|
+
});
|
|
70
|
+
projectRoot = result.answers[0]?.custom ? result.answers[0].custom.trim() : '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!projectRoot) {
|
|
74
|
+
notifyBootstrap(session, '⚠️ 未提供路径,已取消');
|
|
75
|
+
return { kind: 'success', text: '' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (projectRoot === '.') {
|
|
79
|
+
projectRoot = process.cwd();
|
|
80
|
+
notifyBootstrap(session, `📂 使用当前目录:${projectRoot}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (projectRoot.includes('..') || /[<>:"'|?*[\x00-\x1f]/.test(projectRoot) || /\s/.test(projectRoot)) {
|
|
84
|
+
notifyBootstrap(session, '❌ 路径不合法(不能含空格、.. 或特殊字符)');
|
|
85
|
+
return { kind: 'success', text: '' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (existsSync(join(projectRoot, '.fancy-deployed'))) {
|
|
89
|
+
notifyBootstrap(session, '⚠️ 该目录已初始化(.fancy-deployed 已存在)');
|
|
90
|
+
return { kind: 'success', text: '' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const confirm = await ctx.userQuestions.ask({
|
|
94
|
+
questions: [{
|
|
95
|
+
id: 'confirm',
|
|
96
|
+
question: '❓ 确认将以下路径作为项目根?',
|
|
97
|
+
detail: projectRoot,
|
|
98
|
+
hideCustomInput: true,
|
|
99
|
+
options: [
|
|
100
|
+
{ label: '确认创建', description: '开始在此目录初始化项目' },
|
|
101
|
+
{ label: '取消', description: '取消本次操作' },
|
|
102
|
+
],
|
|
103
|
+
}],
|
|
104
|
+
});
|
|
105
|
+
const ans = confirm.answers[0];
|
|
106
|
+
const chosen = ans?.selected?.[0] ?? ans?.custom;
|
|
107
|
+
if (chosen !== '确认创建') {
|
|
108
|
+
notifyBootstrap(session, '🚫 已取消');
|
|
109
|
+
return { kind: 'success', text: '' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
notifyBootstrap(session, `⏳ 正在初始化 ${projectRoot}...`);
|
|
113
|
+
const result = await runBootstrapScript(projectRoot);
|
|
114
|
+
|
|
115
|
+
if (result.ok) {
|
|
116
|
+
notifyBootstrap(session, '✅ 项目初始化完成!\n\n📁 生成的目录结构:\n - 备份/\n - 图片/\n - 发布/\n - .快照/\n - RAG索引/\n - 调试/\n\n下一步:运行 /fancy-scan 扫榜,或 /fancy-topic 做选题决策');
|
|
117
|
+
} else {
|
|
118
|
+
const errMsg = (typeof result.error === 'object' && result.error !== null)
|
|
119
|
+
? (result.error.message || JSON.stringify(result.error))
|
|
120
|
+
: String(result.error || '未知错误');
|
|
121
|
+
notifyBootstrap(session, '❌ 初始化失败:' + errMsg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { kind: 'success', text: '' };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// fancy-scan
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
const SCAN_DIR = join(__dirname, 'fancy-scan');
|
|
133
|
+
const SCAN_SCRIPTS_DIR = join(SCAN_DIR, 'scripts');
|
|
134
|
+
|
|
135
|
+
const PLATFORM_TABLE = [
|
|
136
|
+
['1', 'qidian', '长篇', '起点'],
|
|
137
|
+
['2', 'fanqie', '长篇', '番茄'],
|
|
138
|
+
['3', 'jinjiang', '长篇', '晋江'],
|
|
139
|
+
['4', 'zhihu', '长/短','知乎'],
|
|
140
|
+
['5', 'dianzhong','短篇','点众'],
|
|
141
|
+
['6', 'qimao', '长/短','七猫'],
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function platformLabel(platform) {
|
|
145
|
+
const row = PLATFORM_TABLE.find(r => r[1] === platform);
|
|
146
|
+
return row ? row[3] : platform;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function notifyScan(session, text) {
|
|
150
|
+
queueMicrotask(() => {
|
|
151
|
+
session.append(
|
|
152
|
+
'user/message',
|
|
153
|
+
{ content: [{ type: 'text', text }], source: { kind: 'user' } },
|
|
154
|
+
{ surfaceOp: 'append' }
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runScanScript(projectRoot, platform, length) {
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const scriptPath = join(SCAN_SCRIPTS_DIR, 'run-scan.js');
|
|
162
|
+
const proc = require('child_process').spawn(
|
|
163
|
+
'node', [scriptPath, '--project-root', projectRoot, '--platform', platform, '--length', length],
|
|
164
|
+
{ timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
165
|
+
);
|
|
166
|
+
const outParts = [], errParts = [];
|
|
167
|
+
proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
|
|
168
|
+
proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
|
|
169
|
+
proc.on('close', (code) => {
|
|
170
|
+
const stdout = outParts.join(''), stderr = errParts.join('');
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(stdout);
|
|
173
|
+
resolve({ ok: parsed.ok === true, output: stdout, error: parsed.error ? (parsed.error.message || JSON.stringify(parsed.error)) : stderr || undefined });
|
|
174
|
+
} catch (_) {
|
|
175
|
+
resolve({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
proc.on('error', (err) => resolve({ ok: false, output: '', error: err.message }));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function applyScan(ctx) {
|
|
183
|
+
ctx.commands.register({
|
|
184
|
+
name: 'fancy-scan',
|
|
185
|
+
description: '📊 扫榜分析 ( usage: /fancy-scan [平台] [篇幅] )',
|
|
186
|
+
handler: async (invocation) => {
|
|
187
|
+
const session = invocation.agent.session;
|
|
188
|
+
const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
|
|
189
|
+
|
|
190
|
+
// 获取项目根
|
|
191
|
+
let projectRoot = rawInput ? rawInput.split(/\s+/)[0] : '';
|
|
192
|
+
if (!projectRoot) projectRoot = process.cwd();
|
|
193
|
+
|
|
194
|
+
if (!existsSync(join(projectRoot, '.fancy-deployed'))) {
|
|
195
|
+
notifyScan(session, '⚠️ 当前目录尚未初始化。请先运行 /fancy-bootstrap 进行初始化。');
|
|
196
|
+
return { kind: 'success', text: '' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 解析参数
|
|
200
|
+
let platform = '', length = '';
|
|
201
|
+
if (rawInput) {
|
|
202
|
+
const parts = rawInput.split(/\s+/);
|
|
203
|
+
platform = parts[0] || '';
|
|
204
|
+
length = parts[1] || '';
|
|
205
|
+
const numMatch = platform.match(/^(\d+)$/);
|
|
206
|
+
if (numMatch) {
|
|
207
|
+
const row = PLATFORM_TABLE.find(r => r[0] === numMatch[1]);
|
|
208
|
+
if (row) {
|
|
209
|
+
platform = row[1];
|
|
210
|
+
if (platform === 'zhihu' || platform === 'qimao') {
|
|
211
|
+
length = parts[1] || 'long';
|
|
212
|
+
} else {
|
|
213
|
+
length = row[2] === '长篇' ? 'long' : (row[2] === '短篇' ? 'short' : 'long');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!platform || !length || !PLATFORM_TABLE.find(r => r[1] === platform)) {
|
|
220
|
+
notifyScan(session, '[/fancy-scan] 调用方式:带参数(平台 篇幅)\n[/fancy-scan] 调用方式:无参数(弹窗选择)\n\n📋 支持的平台:\n' +
|
|
221
|
+
PLATFORM_TABLE.map(r => ` ${r[0]}. ${r[3]}(${r[2]})`).join('\n') +
|
|
222
|
+
'\n\n- 长篇:关注 追读、月票、订阅 等指标\n- 短篇:关注 传播、完读率 等指标\n- 知乎和七猫同时支持长篇和短篇');
|
|
223
|
+
|
|
224
|
+
const choice = await ctx.userQuestions.ask({
|
|
225
|
+
questions: [{ id: 'platform', question: '📊 请选择要扫的平台(输入编号 1-6):' }],
|
|
226
|
+
});
|
|
227
|
+
const answer = choice.answers[0]?.custom?.trim();
|
|
228
|
+
if (!answer) {
|
|
229
|
+
notifyScan(session, '⚠️ 已取消。请再次调用 /fancy-scan 继续。');
|
|
230
|
+
return { kind: 'success', text: '' };
|
|
231
|
+
}
|
|
232
|
+
const numMatch = answer.match(/^(\d+)$/);
|
|
233
|
+
const row = numMatch ? PLATFORM_TABLE.find(r => r[0] === numMatch[1]) : null;
|
|
234
|
+
if (!row) {
|
|
235
|
+
notifyScan(session, '❌ 无效选择,请输入 1-6 的编号。');
|
|
236
|
+
return { kind: 'success', text: '' };
|
|
237
|
+
}
|
|
238
|
+
platform = row[1];
|
|
239
|
+
length = (platform === 'zhihu' || platform === 'qimao') ? null : (row[2] === '长篇' ? 'long' : 'short');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 知乎/七猫二次选择篇幅
|
|
243
|
+
if (platform === 'zhihu' || platform === 'qimao') {
|
|
244
|
+
if (!length || !['long', 'short'].includes(length)) {
|
|
245
|
+
const lenResult = await ctx.userQuestions.ask({
|
|
246
|
+
questions: [{
|
|
247
|
+
id: 'length',
|
|
248
|
+
question: `${platformLabel(platform)} 同时支持长篇和短篇,请选择:`,
|
|
249
|
+
hideCustomInput: true,
|
|
250
|
+
options: [
|
|
251
|
+
{ label: '长篇', description: '' },
|
|
252
|
+
{ label: '短篇', description: '' },
|
|
253
|
+
],
|
|
254
|
+
}],
|
|
255
|
+
});
|
|
256
|
+
const lenAnswer = lenResult.answers[0]?.selected?.[0] ?? lenResult.answers[0]?.custom;
|
|
257
|
+
if (!lenAnswer || lenAnswer === '取消') {
|
|
258
|
+
notifyScan(session, '🚫 已取消');
|
|
259
|
+
return { kind: 'success', text: '' };
|
|
260
|
+
}
|
|
261
|
+
length = (lenAnswer === '长篇') ? 'long' : 'short';
|
|
262
|
+
}
|
|
263
|
+
} else if (!length) {
|
|
264
|
+
length = 'long';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
notifyScan(session, `⏳ 正在采集 ${platformLabel(platform)}(${length === 'long' ? '长篇' : '短篇'})...`);
|
|
268
|
+
|
|
269
|
+
const result = await runScanScript(projectRoot, platform, length);
|
|
270
|
+
|
|
271
|
+
if (!result.ok) {
|
|
272
|
+
const errMsg = (typeof result.error === 'object' && result.error !== null)
|
|
273
|
+
? (result.error.message || JSON.stringify(result.error))
|
|
274
|
+
: String(result.error || '未知错误');
|
|
275
|
+
notifyScan(session, '❌ 采集失败:' + errMsg);
|
|
276
|
+
} else {
|
|
277
|
+
let files = [];
|
|
278
|
+
try { files = JSON.parse(result.output).files || []; } catch (_) {}
|
|
279
|
+
const fileList = files.length > 0 ? '\n\n📁 生成文件:\n' + files.map(f => ' - ' + f).join('\n') : '';
|
|
280
|
+
notifyScan(session, `✅ ${platformLabel(platform)}(${length === 'long' ? '长篇' : '短篇'})采集完成${fileList}\n\n如需扫其他平台,请再次调用 /fancy-scan`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { kind: 'success', text: '' };
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// 导出合并的 apply
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
export async function registerCommands(ctx) {
|
|
292
|
+
await applyBootstrap(ctx);
|
|
293
|
+
await applyScan(ctx);
|
|
294
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
}
|