@cloud411716/fancy-webnovel 0.3.16 → 1.0.0

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/infra.js CHANGED
@@ -1,34 +1,39 @@
1
1
  /**
2
- * infra.js — DSH 插件核心基础设施
2
+ * infra.js — DSH Plugin Core Infrastructure
3
3
  *
4
- * 提供:
5
- * notify(session, text) 打印消息给用户(不触发 LLM)
6
- * llmFollowup(invocation, prompt) 发送消息给 LLM,让 LLM 继续处理
7
- * startActivity(session) 开始 Activity Line 计时,返回 stop 函数
8
- * onUserCancel(session, onCancel) 监听 CTRL+C / ESC 取消信号,返回 stop 函数
9
- * spawnScript(cmd, args, opts) — spawn 子进程,解析 stdout JSON
4
+ * Provides:
5
+ * notify(session, text) append a user-facing message to the session log
6
+ * llmFollowup(invocation,prompt)— hand a prompt to the LLM as a user message
7
+ * scanProgress(ctx, phase, data)— emit structured scan progress events
8
+ * sleep(ctx, ms) DSH-native promise delay via ctx.timeout()
10
9
  *
11
- * 所有 fancy-* 插件均直接 import 此文件。
10
+ * All fancy-* plugins import from this file.
12
11
  */
13
12
 
14
13
  // --------------------------------------------------------------------------
15
- // notify — 打印消息给用户(queueMicrotask 避免打断 LLM 输出顺序)
14
+ // notify — append a text message as a session event (no LLM round-trip)
16
15
  // --------------------------------------------------------------------------
17
16
 
17
+ /**
18
+ * @param {object} session - DSH Session object
19
+ * @param {string} text
20
+ */
18
21
  export function notify(session, text) {
19
- queueMicrotask(() => {
20
- session.append(
21
- 'user/message',
22
- { content: [{ type: 'text', text: String(text) }], source: { kind: 'user' } },
23
- { surfaceOp: 'append' }
24
- );
25
- });
22
+ session.append(
23
+ 'user/message',
24
+ { content: [{ type: 'text', text: String(text) }], source: { kind: 'user' } },
25
+ { surfaceOp: 'append' }
26
+ );
26
27
  }
27
28
 
28
29
  // --------------------------------------------------------------------------
29
- // llmFollowup
30
+ // llmFollowup — hand a prompt to the LLM as a user message
30
31
  // --------------------------------------------------------------------------
31
32
 
33
+ /**
34
+ * @param {object} invocation - CommandInvocation from ctx.commands handler
35
+ * @param {string|object} content
36
+ */
32
37
  export function llmFollowup(invocation, content) {
33
38
  const prompt = typeof content === 'string' ? content : JSON.stringify(content);
34
39
  invocation.agent.followup({
@@ -38,103 +43,79 @@ export function llmFollowup(invocation, content) {
38
43
  }
39
44
 
40
45
  // --------------------------------------------------------------------------
41
- // startActivityActivity Line 计时器(显示 Running… + 实时计时)
42
- // 调用 startActivity() 返回 stop 函数,进入新阶段时先 stop 再 start
46
+ // sleepDSH-native promise delay using ctx.timeout()
43
47
  // --------------------------------------------------------------------------
44
48
 
45
- export function startActivity(session) {
46
- session.append('turn/start', { source: 'fancy-plugin' });
47
- return () => session.append('turn/end', { source: 'fancy-plugin' });
49
+ /**
50
+ * Return a promise that resolves after `ms` milliseconds.
51
+ * Uses ctx.timeout() (Cordis timer service) instead of native setTimeout.
52
+ *
53
+ * @param {object} ctx - Cordis plugin context
54
+ * @param {number} ms - milliseconds to wait
55
+ * @returns {Promise<void>}
56
+ */
57
+ export function sleep(ctx, ms) {
58
+ return new Promise((resolve) => {
59
+ ctx.timeout(resolve, ms);
60
+ });
48
61
  }
49
62
 
50
63
  // --------------------------------------------------------------------------
51
- // onUserCancel监听用户按 CTRL+C ESC 取消信号
52
- //
53
- // 调用时传入 session + onCancel 回调(清理函数,如 kill 子进程、停止计时等)
54
- // 返回 stop 函数(调用以移除监听器,流程正常结束时调用)
55
- // 注意:不在弹窗场景下使用(弹窗自身有取消逻辑)
64
+ // scanProgressemit structured scan progress via DSH events
56
65
  // --------------------------------------------------------------------------
57
- const _cancelHandlers = new Map(); // session → { stopFn, onCancel }
58
66
 
59
- export function onUserCancel(session, onCancel) {
60
- if (_cancelHandlers.has(session)) {
61
- const existing = _cancelHandlers.get(session);
62
- const orig = existing.onCancel;
63
- existing.onCancel = () => { orig?.(); onCancel?.(); };
64
- return existing.stopFn;
67
+ /**
68
+ * Emit a scan progress event. These flow into the session log and can be
69
+ * observed by the running agent for real-time progress feedback.
70
+ *
71
+ * @param {object} ctx
72
+ * @param {'start'|'book'|'rank-done'|'done'} phase
73
+ * @param {object} data
74
+ */
75
+ export function scanProgress(ctx, phase, data) {
76
+ switch (phase) {
77
+ case 'start':
78
+ ctx.events.emit('fancy/scan:start', data.platform, data.channel, data.type, data.label);
79
+ break
80
+ case 'book':
81
+ ctx.events.emit('fancy/scan:book', data.platform, data.channel, data.type,
82
+ data.index, data.total, data.title, data.rank);
83
+ break
84
+ case 'rank-done':
85
+ ctx.events.emit('fancy/scan:rank-done', data.platform, data.channel, data.type,
86
+ data.ok, data.written, data.failed, data.errorMsg);
87
+ break
88
+ case 'done':
89
+ ctx.events.emit('fancy/scan:done', data.platform, data.totalBooks, data.totalFiles, data.ok);
90
+ break
65
91
  }
66
-
67
- let cancelled = false;
68
-
69
- const handler = () => {
70
- if (cancelled) return;
71
- cancelled = true;
72
- for (const fn of stopFns) { try { fn(); } catch (_) {} }
73
- cleanup();
74
- notify(session, '🚫 操作已取消。');
75
- };
76
-
77
- const cleanup = () => {
78
- process.removeListener('SIGINT', handler);
79
- _cancelHandlers.delete(session);
80
- };
81
-
82
- const stopFns = [onCancel].filter(Boolean);
83
- process.on('SIGINT', handler);
84
-
85
- const stopFn = () => {
86
- if (!cancelled) {
87
- for (const fn of stopFns) { try { fn(); } catch (_) {} }
88
- cleanup();
89
- }
90
- };
91
-
92
- _cancelHandlers.set(session, { stopFn });
93
- return stopFn;
94
92
  }
95
93
 
96
94
  // --------------------------------------------------------------------------
97
- // spawnScriptspawn 子进程,stdout 只收集 JSON,stderr 单独收集
98
- // opts.stop() 会在进程终止时自动调用(用于清理 turn/start)
95
+ // writeFile / mkdir Node.js fs, used for project dirs outside workspace
99
96
  // --------------------------------------------------------------------------
100
97
 
101
- export function spawnScript(cmd, args, { timeout = 30000, cwd, stop } = {}) {
102
- return new Promise(async (resolve) => {
103
- const { spawn } = await import('child_process');
104
- const proc = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
105
- const outParts = [], errParts = [];
106
- let settled = false;
107
- const settle = (v) => { if (!settled) { settled = true; resolve(v); } };
108
-
109
- const timer = timeout > 0 ? setTimeout(() => {
110
- proc.kill('SIGTERM');
111
- settle({ ok: false, output: outParts.join(''), error: 'timeout after ' + timeout + 'ms' });
112
- }, timeout) : null;
113
-
114
- proc.stdout.on('data', (d) => { outParts.push(d.toString()); });
115
- proc.stderr.on('data', (d) => { errParts.push(d.toString()); });
98
+ /**
99
+ * Write text to a file, creating directories as needed.
100
+ * Uses Node.js fs directly (not ctx.fs) since project roots may be
101
+ * outside the DSH sandbox workspace.
102
+ *
103
+ * @param {string} filepath
104
+ * @param {string} content
105
+ * @param {string} [encoding='utf-8']
106
+ */
107
+ export async function writeFile(filepath, content, encoding = 'utf-8') {
108
+ const { writeFileSync, mkdirSync } = await import('fs');
109
+ const { dirname } = await import('path');
110
+ mkdirSync(dirname(filepath), { recursive: true });
111
+ writeFileSync(filepath, content, encoding);
112
+ }
116
113
 
117
- const cleanup = () => { try { stop?.(); } catch (_) {} };
118
- proc.on('close', (code) => {
119
- clearTimeout(timer);
120
- cleanup();
121
- if (settled) return;
122
- const stdout = outParts.join(''), stderr = errParts.join('');
123
- try {
124
- const parsed = JSON.parse(stdout);
125
- settle({
126
- ok: parsed.ok === true,
127
- output: stdout,
128
- error: parsed.error
129
- ? (typeof parsed.error === 'object'
130
- ? (parsed.error.message || JSON.stringify(parsed.error))
131
- : String(parsed.error))
132
- : stderr || undefined,
133
- });
134
- } catch (_) {
135
- settle({ ok: false, output: stdout, error: stderr || ('exit ' + code) });
136
- }
137
- });
138
- proc.on('error', (err) => { clearTimeout(timer); cleanup(); settle({ ok: false, output: '', error: err.message }); });
139
- });
114
+ /**
115
+ * Create a directory, creating parents as needed.
116
+ * @param {string} dirpath
117
+ */
118
+ export async function mkdirp(dirpath) {
119
+ const { mkdirSync } = await import('fs');
120
+ mkdirSync(dirpath, { recursive: true });
140
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud411716/fancy-webnovel",
3
- "version": "0.3.16",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -9,21 +9,13 @@
9
9
  "files": [
10
10
  "index.js",
11
11
  "infra.js",
12
- "templates.js",
12
+ "events.js",
13
13
  "cordis.patch.yml",
14
- "plugins/"
14
+ "commands/"
15
15
  ],
16
16
  "dependencies": {
17
17
  "playwright-core": "1.48.0"
18
18
  },
19
- "peerDependencies": {
20
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2"
21
- },
22
- "peerDependenciesMeta": {
23
- "@deepseek-ai/dsh-llm": {
24
- "optional": true
25
- }
26
- },
27
19
  "dsh": {
28
20
  "bundle": {
29
21
  "patch": "./cordis.patch.yml"
@@ -1,100 +0,0 @@
1
- /**
2
- * fancy-bootstrap plugin entry
3
- */
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join } from 'path';
6
- import { existsSync } from 'fs';
7
- import { notify, spawnScript, startActivity } from '../../infra.js';
8
- import { bootstrap } from './templates.js';
9
-
10
- export const inject = ['commands', 'userQuestions'];
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const SCRIPTS_DIR = join(__dirname, 'scripts');
14
-
15
- // ---------------------------------------------------------------------------
16
- // 路径工具
17
- // ---------------------------------------------------------------------------
18
-
19
- function isValidPath(p) {
20
- return !p.includes('..') && !/[<>:"'|?*[\x00-\x1f]/.test(p) && !/\s/.test(p);
21
- }
22
-
23
- // ---------------------------------------------------------------------------
24
- // 注册
25
- // ---------------------------------------------------------------------------
26
-
27
- export async function apply(ctx) {
28
- ctx.commands.register({
29
- name: 'fancy-bootstrap',
30
- description: '📁 初始化项目根 ( usage: /fancy-bootstrap [项目根] )',
31
- handler: async (invocation) => {
32
- const session = invocation.agent.session;
33
- const rawInput = invocation.rawInput ? invocation.rawInput.trim() : '';
34
-
35
- let projectRoot = rawInput;
36
-
37
- // 无参数 → 弹窗
38
- if (!projectRoot) {
39
- const result = await ctx.userQuestions.ask({
40
- questions: [bootstrap.askPath()],
41
- });
42
- projectRoot = result.answers[0]?.custom?.trim() || '';
43
- }
44
-
45
- if (!projectRoot) {
46
- notify(session, bootstrap.notify({ type: 'no_path' }));
47
- return { kind: 'success', text: '' };
48
- }
49
-
50
- if (projectRoot === '.') {
51
- projectRoot = process.cwd();
52
- notify(session, bootstrap.notify({ type: 'using_cwd', projectRoot }));
53
- }
54
-
55
- if (!isValidPath(projectRoot)) {
56
- notify(session, bootstrap.notify({ type: 'invalid_path' }));
57
- return { kind: 'success', text: '' };
58
- }
59
-
60
- if (existsSync(join(projectRoot, '.fancy-deployed'))) {
61
- notify(session, bootstrap.notify({ type: 'already_initialized' }));
62
- return { kind: 'success', text: '' };
63
- }
64
-
65
- const confirm = await ctx.userQuestions.ask({
66
- questions: [{
67
- id: 'confirm',
68
- question: bootstrap.notify({ type: 'confirm', projectRoot }),
69
- detail: projectRoot,
70
- hideCustomInput: true,
71
- options: bootstrap.confirmOptions(),
72
- }],
73
- });
74
- const ans = confirm.answers[0];
75
- const chosen = ans?.selected?.[0] ?? ans?.custom;
76
- if (chosen !== '确认创建') {
77
- notify(session, bootstrap.notify({ type: 'cancelled' }));
78
- return { kind: 'success', text: '' };
79
- }
80
-
81
- notify(session, bootstrap.notify({ type: 'initializing', projectRoot }));
82
- const stopActivity = startActivity(session);
83
- const result = await spawnScript('node', [join(SCRIPTS_DIR, 'init.js'), projectRoot], {
84
- timeout: 30000,
85
- stop: stopActivity,
86
- });
87
-
88
- if (result.ok) {
89
- notify(session, bootstrap.notify({ type: 'success' }));
90
- } else {
91
- const err = (typeof result.error === 'object' && result.error !== null)
92
- ? (result.error.message || JSON.stringify(result.error))
93
- : String(result.error || '未知错误');
94
- notify(session, bootstrap.notify({ type: 'error', err }));
95
- }
96
-
97
- return { kind: 'success', text: '' };
98
- }
99
- });
100
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * fancy-bootstrap init — 项目初始化
3
- */
4
- import { writeFileSync, mkdirSync, existsSync } from 'fs';
5
- import { expandHome, resolvePath, joinPath } from './path-utils.js';
6
-
7
- const DIRS = [
8
- '设定/角色', '设定/势力', '设定/物品', '设定/地点',
9
- '大纲', '正文', '故事档案', '对标', '拆文库',
10
- '审查报告', '图片', '发布',
11
- '故事档案/备份', '故事档案/.快照', '故事档案/派生',
12
- '调试', 'RAG索引',
13
- ];
14
-
15
- function mkdirp(dir) {
16
- mkdirSync(dir, { recursive: true });
17
- }
18
-
19
- function writeJson(filePath, data) {
20
- writeFileSync(filePath, JSON.stringify(data), 'utf-8');
21
- }
22
-
23
- function nowIso() {
24
- return new Date().toISOString().replace('.000Z', 'Z');
25
- }
26
-
27
- function main() {
28
- const projectRootRaw = process.argv[2] ?? '';
29
-
30
- if (projectRootRaw.includes('..') || /[<>:\"'|?*]/.test(projectRootRaw) || /[\x00-\x1f]/.test(projectRootRaw)) {
31
- console.log(JSON.stringify({ ok: false, error: { code: 1, code_name: 'EC001', message: '路径不合法' } }));
32
- process.exit(1);
33
- }
34
-
35
- const projectRoot = resolvePath(expandHome(projectRootRaw));
36
- const deployed = joinPath(projectRoot, '.fancy-deployed');
37
-
38
- if (existsSync(deployed)) {
39
- console.log(JSON.stringify({ ok: false, error: { code: 2, code_name: 'EC002', message: '项目已存在,不能重复 init' } }));
40
- process.exit(2);
41
- }
42
- if (!existsSync(projectRoot)) {
43
- console.log(JSON.stringify({ ok: false, error: { code: 18, code_name: 'EC018', message: `目录不存在: ${projectRoot}` } }));
44
- process.exit(18);
45
- }
46
-
47
- const now = nowIso();
48
-
49
- try {
50
- for (const d of DIRS) mkdirp(joinPath(projectRoot, d));
51
-
52
- writeJson(deployed, {
53
- schema_version: '2.0.0', fancy_skills_version: '2.0.0', deployed_at: now,
54
- project_root_abs: projectRoot, project_root_rel: '.', current_book: '', books: [], active_book_index: 0,
55
- });
56
-
57
- const archive = joinPath(projectRoot, '故事档案');
58
- writeJson(joinPath(archive, '_story_state.json'), {
59
- schema_version: '2.0.0', last_modified: now,
60
- project: { name: '', created_at: now, last_modified: now, genre: '', target_words: 0, target_chapters: 0, platform: '', platform_other: '' },
61
- phase: 'uninitialized', phase_history: [{ phase: 'uninitialized', at: now }],
62
- state_machine: { current_volume: 1, last_chapter_committed: 0, next_chapter_to_write: 1, chapters_total_words: 0, committed_chapters: [], pause_reason: null, paused_at: null },
63
- author_intent: { style: 'xiaobai', pacing: 'medium', pov: 'third', target_audience: 'general', preferences: [], hard_constraints: [], anti_tropes: [], core_summary: '', core_conflict: '', reader_promise: '' },
64
- active_book: { project_root: '.', set_at: now },
65
- });
66
- writeJson(joinPath(archive, '_timeline.json'), {
67
- schema_version: '2.0.0', last_modified: now,
68
- world_clock: { start_date: now, current_date: now, calendar_unit: 'day' },
69
- events: [], character_status_at_chapter: {}, dead_characters: [], fact_log: [], discoveries: [],
70
- });
71
- writeJson(joinPath(archive, '_character_state.json'), { schema_version: '2.0.0', last_modified: now, characters: {}, info_boundary_log: [] });
72
- writeJson(joinPath(archive, '_foreshadows.json'), { schema_version: '2.0.0', last_modified: now, foreshadows: [], type_distribution: {}, open_count: 0 });
73
- writeJson(joinPath(archive, '_chapter_index.json'), { schema_version: '2.0.0', last_modified: now, chapters: [] });
74
-
75
- const intentMd = [
76
- '# 作者长期意图(Author Intent)',
77
- '',
78
- '> 由 fancy-bootstrap 自动生成。',
79
- '> **注意**:书名/题材/平台/字数等信息在 fancy-topic 阶段填入。',
80
- '',
81
- '## 项目信息',
82
- '> ⚠️ 以下由 fancy-topic 填入:书名 / 题材 / 平台 / 目标字数 / 目标章数',
83
- '',
84
- '## 故事核心',
85
- '> ⚠️ 由 fancy-topic 填入:一句话 / 核心冲突 / 读者承诺',
86
- '',
87
- '## 风格基调',
88
- '- 文风:xiaobai(待 fancy-bible 细化)',
89
- '- 节奏:medium(待 fancy-bible 细化)',
90
- '- POV:third(待 fancy-bible 细化)',
91
- '',
92
- `## 用户已声明的偏好`,
93
- `- ${now}:项目目录创建(fancy-bootstrap)`,
94
- ].join('\n');
95
- writeFileSync(joinPath(archive, '_author_intent.md'), intentMd, 'utf-8');
96
-
97
- } catch (e) {
98
- const errMsg = e instanceof Error ? e.message : String(e);
99
- console.log(JSON.stringify({ ok: false, error: { code: 17, code_name: 'EC017', message: `初始化失败: ${errMsg}` } }));
100
- process.exit(17);
101
- }
102
-
103
- console.log(JSON.stringify({
104
- ok: true, operation: 'init', project_root: projectRoot, phase: 'uninitialized',
105
- directories_created: DIRS.length,
106
- files_created: ['.fancy-deployed', '故事档案/_story_state.json', '故事档案/_timeline.json', '故事档案/_character_state.json', '故事档案/_foreshadows.json', '故事档案/_chapter_index.json', '故事档案/_author_intent.md'],
107
- note: '项目信息由 fancy-topic 填入',
108
- }));
109
- }
110
-
111
- main();
@@ -1,22 +0,0 @@
1
- /**
2
- * path-utils.js — init.js 专用路径工具
3
- * restore from git
4
- */
5
- import { existsSync } from 'fs';
6
- import { homedir } from 'os';
7
- import { resolve, join } from 'path';
8
-
9
- export function expandHome(p) {
10
- if (p === '~' || p.startsWith('~/')) {
11
- return join(homedir(), p.slice(1));
12
- }
13
- return p;
14
- }
15
-
16
- export function resolvePath(p) {
17
- return resolve(p);
18
- }
19
-
20
- export function joinPath(base, ...parts) {
21
- return join(base, ...parts);
22
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * bootstrap 模板 — 仅 fancy-bootstrap 使用
3
- */
4
-
5
- export const bootstrap = {
6
- askPath(question = '📂 请输入项目根路径 ( 或者输入 . 代表当前路径 )') {
7
- return { question };
8
- },
9
-
10
- notify({ type, projectRoot, err }) {
11
- switch (type) {
12
- case 'no_path':
13
- return '⚠️ 未提供路径,已取消';
14
- case 'using_cwd':
15
- return `📂 使用当前目录:${projectRoot}`;
16
- case 'invalid_path':
17
- return '❌ 路径不合法(不能含空格、.. 或特殊字符)';
18
- case 'already_initialized':
19
- return '⚠️ 该目录已初始化(.fancy-deployed 已存在)';
20
- case 'confirm':
21
- return `❓ 确认将以下路径作为项目根?\n${projectRoot}`;
22
- case 'cancelled':
23
- return '🚫 已取消';
24
- case 'initializing':
25
- return `⏳ 正在初始化 ${projectRoot}...`;
26
- case 'success':
27
- return '✅ 项目初始化完成!\n下一步:运行 /fancy-scan 扫榜';
28
- case 'error':
29
- return '❌ 初始化失败:' + (typeof err === 'function' ? err() : err);
30
- default:
31
- return String(type);
32
- }
33
- },
34
-
35
- confirmOptions() {
36
- return [
37
- { label: '确认创建' },
38
- { label: '取消' },
39
- ];
40
- },
41
- };