@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +12 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +140 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +31 -7
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +145 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +410 -21
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +545 -146
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +267 -44
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +121 -19
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +129 -21
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +76 -17
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +56 -8
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +63 -13
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +34 -6
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +81 -18
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +202 -42
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +160 -27
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +217 -41
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-export:把 SurrealDB 故事图导出成项目根下的中文 Markdown。
|
|
3
|
+
*
|
|
4
|
+
* 这个插件只读数据库,不调用 LLM,也不写阶段 JSON。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { notify } from '#infra';
|
|
10
|
+
import { openStoryGraph } from '../fancy-story-builder/storydb/client.js';
|
|
11
|
+
import { exportStoryGraph } from './exporter.js';
|
|
12
|
+
import { resolveProject } from '../../lib/projects.js';
|
|
13
|
+
|
|
14
|
+
export const inject = ['commands'];
|
|
15
|
+
|
|
16
|
+
function helpText() {
|
|
17
|
+
return `📖 故事导出 用法
|
|
18
|
+
|
|
19
|
+
/fancy-export - 导出当前项目到 <项目目录>/导出
|
|
20
|
+
/fancy-export --project <名字|UUID> - 指定项目
|
|
21
|
+
/fancy-export help - 显示本帮助`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cwdOf(session) {
|
|
25
|
+
return session && session.header && session.header.cwd ? session.header.cwd : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function apply(ctx) {
|
|
29
|
+
ctx.commands.register({
|
|
30
|
+
name: 'fancy-export',
|
|
31
|
+
description: '📖 故事导出 ( usage: /fancy-export | help )',
|
|
32
|
+
input: { hint: '.' },
|
|
33
|
+
handler: async (invocation) => {
|
|
34
|
+
const session = invocation.agent.session;
|
|
35
|
+
const raw = String(invocation.rawInput || '').trim().toLowerCase();
|
|
36
|
+
if (raw === 'help') return notify(session, 'success', helpText());
|
|
37
|
+
if (raw && raw !== 'all') {
|
|
38
|
+
return notify(session, 'error', '不支持的参数:' + raw + '\n\n' + helpText());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let project = null;
|
|
42
|
+
try {
|
|
43
|
+
project = await resolveProject({ cwd: cwdOf(session) });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return notify(session, 'error', '❌ ' + (err && err.message ? err.message : String(err)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let sdb = null;
|
|
49
|
+
try {
|
|
50
|
+
sdb = await openStoryGraph(project.dir, { project });
|
|
51
|
+
const result = await exportStoryGraph(sdb, project.dir);
|
|
52
|
+
return notify(session, 'success', [
|
|
53
|
+
'✅ 已导出到 ' + result.dir,
|
|
54
|
+
'文件:' + result.files + ' 个 · 细纲:' + result.outlines + ' 章 · 正文:' + result.chapters + ' 章',
|
|
55
|
+
].join('\n'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return notify(session, 'error', '❌ 导出失败:' + (err && err.message ? err.message : String(err)));
|
|
58
|
+
} finally {
|
|
59
|
+
if (sdb) await sdb.close();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -210,10 +210,18 @@ export function flattenQuota(quota) {
|
|
|
210
210
|
export function buildRoundPrompt(o) {
|
|
211
211
|
const round = o.round;
|
|
212
212
|
const ctx = o.context || {};
|
|
213
|
+
// 项目根**绝对路径**:读文件、写文件都要用它。
|
|
214
|
+
//
|
|
215
|
+
// 为什么必须给绝对路径:LLM 的工作目录未必是项目根。
|
|
216
|
+
// 用相对路径(`设定/raw/sources/骨架/实体.json`)时,产物会落到
|
|
217
|
+
// LLM 当前目录下的 `设定/...` 下 —— 插件在项目根下找不到文件,
|
|
218
|
+
// 只会报一句"文件没有生成",模型完全不知道自己写歪了。
|
|
219
|
+
const projectRoot = ctx.projectRoot || '';
|
|
220
|
+
const abs = (rel) => (projectRoot ? projectRoot + '/' + rel : rel);
|
|
213
221
|
const parts = [];
|
|
214
222
|
|
|
215
223
|
if (o.contract) {
|
|
216
|
-
parts.push(o.contract.trim());
|
|
224
|
+
parts.push(o.contract.trim().replace(/\$\{projectRoot\}/g, projectRoot || '(项目根)'));
|
|
217
225
|
parts.push('');
|
|
218
226
|
parts.push('---');
|
|
219
227
|
parts.push('');
|
|
@@ -224,14 +232,16 @@ export function buildRoundPrompt(o) {
|
|
|
224
232
|
|
|
225
233
|
parts.push('## 旧稿在哪');
|
|
226
234
|
parts.push('');
|
|
227
|
-
parts.push('-
|
|
235
|
+
parts.push('- 正文:`' + abs('设定/raw/sources/写作/') + '`(第001章.md ~ 第' +
|
|
228
236
|
String(ctx.chapterCount || 0).padStart(3, '0') + '章.md,共 ' + (ctx.chapterCount || 0) + ' 章)');
|
|
229
237
|
if (ctx.settingCount) {
|
|
230
|
-
parts.push('-
|
|
238
|
+
parts.push('- 设定资料:`' + abs('设定/raw/sources/导入资料/') + '`(' + ctx.settingCount + ' 个文件)');
|
|
231
239
|
} else {
|
|
232
240
|
parts.push('- 设定资料:没有(旧稿只有正文)');
|
|
233
241
|
}
|
|
234
242
|
parts.push('- **先读这些文件再动笔**,不要凭想象填。');
|
|
243
|
+
parts.push('- **路径一律用上面给的绝对路径**:你的工作目录未必是项目根,');
|
|
244
|
+
parts.push(' 用相对路径会读不到旧稿、把产物写到别的地方去。');
|
|
235
245
|
parts.push('');
|
|
236
246
|
|
|
237
247
|
if (ctx.excerpts && ctx.excerpts.length) {
|
|
@@ -264,10 +274,11 @@ export function buildRoundPrompt(o) {
|
|
|
264
274
|
parts.push('## 本轮要写的文件');
|
|
265
275
|
parts.push('');
|
|
266
276
|
for (const f of round.files) {
|
|
267
|
-
parts.push('-
|
|
277
|
+
parts.push('- `' + abs('设定/raw/sources/' + round.dir + '/' + f.name) + '`');
|
|
268
278
|
}
|
|
269
279
|
parts.push('');
|
|
270
280
|
parts.push('**只写上面这几个文件。** 其余阶段的文件本轮不要动。');
|
|
281
|
+
parts.push('**用上面给的绝对路径写**(目录不存在就先建目录),不要用相对路径。');
|
|
271
282
|
parts.push('');
|
|
272
283
|
|
|
273
284
|
if (round.id === 'input') {
|
|
@@ -301,6 +312,18 @@ export function buildRoundPrompt(o) {
|
|
|
301
312
|
parts.push('');
|
|
302
313
|
}
|
|
303
314
|
|
|
315
|
+
parts.push('## 本轮怎么算完成');
|
|
316
|
+
parts.push('');
|
|
317
|
+
parts.push('- 你只管**把上面那几个文件写出来**;内容对不对由插件的校验器判,不用自己跑校验。');
|
|
318
|
+
parts.push('- 写完后插件会跑校验器。**下一轮派活里没有「上一轮的校验错误」这一节 = 这一轮通过了**。');
|
|
319
|
+
parts.push('- 下一轮派活里**有**「上一轮的校验错误」= 没通过:按那节里的错误码改');
|
|
320
|
+
parts.push(' (错误码的含义见上面契约的「四、会被拦下的东西」),**只改错误点名的字段**,');
|
|
321
|
+
parts.push(' 改完**重新写整个文件**(产物是整份覆盖,不是打补丁)。');
|
|
322
|
+
parts.push('- 派活标题里的「第 N 次尝试」是第几次。同一轮最多试 ' + IMPORT_MAX_ATTEMPTS + ' 次;');
|
|
323
|
+
parts.push(' 第 ' + IMPORT_MAX_ATTEMPTS + ' 次仍未通过,插件会停在这一轮,');
|
|
324
|
+
parts.push(' 并在导入报告里把它列成「未通过」——这时候不要继续自己往下写别的阶段。');
|
|
325
|
+
parts.push('');
|
|
326
|
+
|
|
304
327
|
parts.push('## 交活');
|
|
305
328
|
parts.push('');
|
|
306
329
|
parts.push('写完给一段话汇报:本轮写了哪些文件、各写了多少条、哪些内容旧稿里没有(已留空)。');
|
|
@@ -558,8 +581,8 @@ export function renderImportReport(o) {
|
|
|
558
581
|
lines.push('## 五、下一步');
|
|
559
582
|
lines.push('');
|
|
560
583
|
lines.push('1. 修掉上面的待补项(可以直接重新跑 `/fancy-import` 覆盖)。');
|
|
561
|
-
lines.push('2. 跑 `/fancy-
|
|
562
|
-
lines.push('3. 跑 `/fancy-
|
|
584
|
+
lines.push('2. 跑 `/fancy-build write` 让台账与章节对齐(导入只登记旧稿已有的事实)。');
|
|
585
|
+
lines.push('3. 跑 `/fancy-build deslop` 去掉旧稿里的 AI 味。');
|
|
563
586
|
lines.push('');
|
|
564
587
|
|
|
565
588
|
return lines.join('\n');
|
|
@@ -266,7 +266,7 @@ function importStamp() {
|
|
|
266
266
|
export async function apply(ctx) {
|
|
267
267
|
ctx.commands.register({
|
|
268
268
|
name: 'fancy-import',
|
|
269
|
-
description: '📥 旧稿导入 ( usage: /fancy-import <旧稿目录或文件> )',
|
|
269
|
+
description: '📥 旧稿导入 ( usage: /fancy-import <旧稿目录或文件> | help )',
|
|
270
270
|
input: {
|
|
271
271
|
hint: '旧稿目录或文件'
|
|
272
272
|
},
|
|
@@ -274,11 +274,15 @@ export async function apply(ctx) {
|
|
|
274
274
|
const session = invocation.agent.session;
|
|
275
275
|
const { signal } = invocation;
|
|
276
276
|
|
|
277
|
+
if ((invocation.rawInput || '').trim().toLowerCase() === 'help') {
|
|
278
|
+
return notify(session, 'success', importTemplates.notify({ type: 'help' }));
|
|
279
|
+
}
|
|
280
|
+
|
|
277
281
|
const projectRoot = getProjectRoot(session);
|
|
278
282
|
if (!projectRoot) {
|
|
279
283
|
return notify(session, 'error', importTemplates.notify({
|
|
280
284
|
type: 'no_project_root',
|
|
281
|
-
err: '
|
|
285
|
+
err: '会话未提供当前工作目录'
|
|
282
286
|
}));
|
|
283
287
|
}
|
|
284
288
|
|
|
@@ -290,7 +294,7 @@ export async function apply(ctx) {
|
|
|
290
294
|
if (!deployedRes.ok || !deployedRes.config) {
|
|
291
295
|
return notify(session, 'error', importTemplates.notify({
|
|
292
296
|
type: deployedRes.reason === 'not_found' ? 'not_initialized' : 'no_source',
|
|
293
|
-
err: '.fancy-deployed
|
|
297
|
+
err: '无法读取 .fancy-deployed(' + (deployedRes.reason || '未知原因') + ')'
|
|
294
298
|
}));
|
|
295
299
|
}
|
|
296
300
|
const deployedConfig = deployedRes.config;
|
|
@@ -371,7 +375,11 @@ export async function apply(ctx) {
|
|
|
371
375
|
chapterCount: scan.chapters.length,
|
|
372
376
|
settingCount: scan.settings.length,
|
|
373
377
|
excerpts: pickExcerpts(scan.chapters),
|
|
374
|
-
quota: null
|
|
378
|
+
quota: null,
|
|
379
|
+
// 项目根绝对路径:派活提示词里的产物路径、模板路径、词表路径都用它。
|
|
380
|
+
// 少了它,LLM 拿到的还是相对路径,产物会写到 LLM 自己的工作目录里,
|
|
381
|
+
// 插件在项目根下找不到文件,只会报"文件没有生成"。
|
|
382
|
+
projectRoot,
|
|
375
383
|
};
|
|
376
384
|
|
|
377
385
|
const report = await runImportStage({
|
|
@@ -726,5 +734,5 @@ export function rhythmInfeasibleReason(T, BC) {
|
|
|
726
734
|
return '旧稿只有 ' + T + ' 章,达不到骨架阶段的规模要求:' +
|
|
727
735
|
'支线配额 ' + BC + ' 要求区间 [' + lower + ', ' + upper + '] 无解' +
|
|
728
736
|
'(' + feasible + ')。' +
|
|
729
|
-
'建议把支线数改到上面的可行区间,或改用 /fancy-
|
|
737
|
+
'建议把支线数改到上面的可行区间,或改用 /fancy-build 从骨架重新起稿。';
|
|
730
738
|
}
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## 〇、路径口径
|
|
13
|
+
|
|
14
|
+
**项目根的绝对路径:`${projectRoot}`**
|
|
15
|
+
|
|
16
|
+
本文件与每轮派活里出现的路径都相对于项目根,但**读文件、写文件一律用绝对路径**(`<项目根>/...`)——你的工作目录未必是项目根:用相对路径会读不到旧稿,也会把产物写到别的地方去,插件在项目根下找不到文件,只会回一句"文件没有生成"。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
12
20
|
## 一、核心铁律
|
|
13
21
|
|
|
14
22
|
> 1. **只反推,不创作**。旧稿里没有的人物、势力、地点、伏笔**不许新增**。
|
|
@@ -30,16 +38,16 @@
|
|
|
30
38
|
|
|
31
39
|
## 二、产物清单
|
|
32
40
|
|
|
33
|
-
| 阶段 | 文件 |
|
|
41
|
+
| 阶段 | 文件 | 落盘路径(绝对路径) |
|
|
34
42
|
|---|---|---|
|
|
35
|
-
| 骨架 | 实体.json |
|
|
36
|
-
| 骨架 | 关系.json |
|
|
37
|
-
| 骨架 | 层级.json |
|
|
38
|
-
| 血肉 | 角色.json 等 8 个 |
|
|
39
|
-
| 节奏 | 章节规划.json 等 4 个 |
|
|
40
|
-
| 写作 | 4 个台账 |
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
| 骨架 | 实体.json | `${projectRoot}/设定/raw/sources/骨架/实体.json` |
|
|
44
|
+
| 骨架 | 关系.json | `${projectRoot}/设定/raw/sources/骨架/关系.json` |
|
|
45
|
+
| 骨架 | 层级.json | `${projectRoot}/设定/raw/sources/骨架/层级.json` |
|
|
46
|
+
| 血肉 | 角色.json 等 8 个 | `${projectRoot}/设定/raw/sources/血肉/<名字>.json` |
|
|
47
|
+
| 节奏 | 章节规划.json 等 4 个 | `${projectRoot}/设定/raw/sources/节奏/<名字>.json` |
|
|
48
|
+
| 写作 | 4 个台账 | `${projectRoot}/设定/raw/sources/写作/台账/<名字>.json` |
|
|
49
|
+
|
|
50
|
+
**`${projectRoot}/设定/raw/sources/骨架/框架.json` 由插件生成,不要写。**里面的 `quota` 是插件从旧稿实际规模
|
|
43
51
|
反推出来的(推不出来的按公式补齐),你写的实体条数必须和它对得上。
|
|
44
52
|
|
|
45
53
|
---
|
|
@@ -66,8 +74,8 @@
|
|
|
66
74
|
### 3.3 字段与枚举
|
|
67
75
|
|
|
68
76
|
- 字段结构、id 规则、枚举取值:**以 `模板/` 下对应模板为准**
|
|
69
|
-
|
|
70
|
-
- 枚举词表(角色类型、冲突类型、伏笔类型…):**以
|
|
77
|
+
(`${projectRoot}/模板/骨架/实体模板.json` 等,**用绝对路径读**)。
|
|
78
|
+
- 枚举词表(角色类型、冲突类型、伏笔类型…):**以 `${projectRoot}/references/glossary.md` 为准**。
|
|
71
79
|
写错枚举值会被校验器拦下。
|
|
72
80
|
|
|
73
81
|
---
|
|
@@ -81,8 +89,15 @@
|
|
|
81
89
|
| id 规则 | `ID_FORMAT` / `ID_DUP` | id 不符合 `C001` / `F001` 这类格式,或 id 重复 |
|
|
82
90
|
| 枚举 | `ENUM_INVALID` | 取值不在 glossary 的词表里 |
|
|
83
91
|
| 引用 | `REF_UNKNOWN` | 关系里引用了不存在的 id |
|
|
84
|
-
|
|
|
92
|
+
| 配额 | `QUOTA_MISMATCH` | 条数和 `框架.json` 里的 `quota` 对不上 |
|
|
93
|
+
| 结构 | `DERIVATION_MISSING` | `${projectRoot}/设定/raw/sources/骨架/框架.json` 缺推导记录(**这个由插件生成,你不用管**) |
|
|
85
94
|
| 内容 | `NAME_PLACEHOLDER` / `TEXT_PLACEHOLDER` / `TEXT_TOO_SHORT` / `TEXT_DUPLICATE` | 实体名是"主角1"这类标签、字段是"待补充"这类占位、字段太短、同质化 |
|
|
95
|
+
| 没落盘 | `NO_OUTPUT` | **插件没等到你写文件**:多半是路径写歪了(用了相对路径),也可能是这一轮超时或被中断 |
|
|
96
|
+
| 没落盘 | `READ` | 插件在项目根下没找到本轮要写的文件,或它不是合法 JSON |
|
|
97
|
+
| 校验器 | `VALIDATOR_CRASH` | 校验脚本自己崩了——**不是你的产物有问题**,照原样再交一次即可 |
|
|
98
|
+
|
|
99
|
+
**什么算这一轮通过**:插件跑完校验器,**没有回任何错误**就是通过。
|
|
100
|
+
下一轮派活里如果带着「上一轮的校验错误」,就是没通过,按第五节处理。
|
|
86
101
|
|
|
87
102
|
---
|
|
88
103
|
|
|
@@ -92,10 +107,20 @@
|
|
|
92
107
|
|
|
93
108
|
1. **只改错误指向的字段**。错误没提到的部分不要动。
|
|
94
109
|
2. **按 `hint` 的方向改**,不要绕过。
|
|
95
|
-
3. **不要为了通过校验而编造内容**。校验器拦下"实体名是占位名"
|
|
96
|
-
正确做法是**删掉这个编出来的实体**,不是给它起个像样的名字。
|
|
110
|
+
3. **不要为了通过校验而编造内容**。校验器拦下"实体名是占位名"时,正确做法是**删掉这个编出来的实体**,不是给它起个像样的名字。
|
|
97
111
|
4. 改完**重新写整个文件**(产物是整份覆盖,不是打补丁)。
|
|
98
112
|
|
|
113
|
+
**上面几类特殊错误码另说**:
|
|
114
|
+
|
|
115
|
+
- `NO_OUTPUT` / `READ`(文件没落盘)→ **先确认你用的是绝对路径**
|
|
116
|
+
(派活里给的那条 `<项目根>/设定/raw/sources/...`),目录不存在就先建目录,然后把整个文件**重新写一遍**。
|
|
117
|
+
不要因为"内容我已经想好了"就跳过写文件——
|
|
118
|
+
没落盘 = 这一轮没做,下一次还会重来。
|
|
119
|
+
- `VALIDATOR_CRASH`(校验器自己崩了)→ 不用改产物,原样重交一次。
|
|
120
|
+
|
|
121
|
+
**同一轮最多试 3 次**(派活标题里写着"第 N 次尝试")。
|
|
122
|
+
第 3 次还没通过,插件会停在这一轮并写进导入报告;这时候**不要自己接着往下写别的阶段**,等插件告诉你下一步。
|
|
123
|
+
|
|
99
124
|
**汇报格式**(每轮完成后给一段话):
|
|
100
125
|
|
|
101
126
|
```
|
|
@@ -10,26 +10,35 @@ export const importTemplates = {
|
|
|
10
10
|
*/
|
|
11
11
|
notify({ type, err, extra, chapters, files, rounds, failed }) {
|
|
12
12
|
switch (type) {
|
|
13
|
+
case 'help':
|
|
14
|
+
return `📥 旧稿导入 用法
|
|
15
|
+
|
|
16
|
+
/fancy-import <旧稿目录或文件> 导入旧稿并自动切章
|
|
17
|
+
/fancy-import 默认读取项目内的 旧稿/ 目录
|
|
18
|
+
/fancy-import help 显示本帮助
|
|
19
|
+
|
|
20
|
+
作用:把旧稿导入到 设定/raw/sources/,并纳入 llm_wiki 项目。`;
|
|
21
|
+
|
|
13
22
|
case 'no_project_root':
|
|
14
|
-
return '❌ 无法确定项目目录:' + (err || '
|
|
23
|
+
return '❌ 无法确定项目目录:' + (err || '会话未提供当前工作目录') + '。请在项目目录下运行该命令。';
|
|
15
24
|
|
|
16
25
|
case 'not_initialized':
|
|
17
|
-
return '⚠️
|
|
26
|
+
return '⚠️ 当前目录不属于已初始化的 fancy-webnovel 工作区。请先运行 /fancy-bootstrap 初始化工作区。';
|
|
18
27
|
|
|
19
28
|
case 'no_source':
|
|
20
|
-
return '❌
|
|
29
|
+
return '❌ 旧稿导入失败:' + (err || '未知错误');
|
|
21
30
|
|
|
22
31
|
case 'no_source_arg':
|
|
23
32
|
return '⚠️ 请指定要导入的旧稿路径:\n'
|
|
24
33
|
+ '/fancy-import <旧稿目录或文件>\n'
|
|
25
|
-
+ '
|
|
34
|
+
+ '(目录中的 .txt / .md 文件会被扫描,正文与设定资料将自动归类。)';
|
|
26
35
|
|
|
27
36
|
case 'deploy_failed':
|
|
28
|
-
return '❌ 导入阶段文件部署失败:' + (err || '未知错误') + '
|
|
37
|
+
return '❌ 导入阶段文件部署失败:' + (err || '未知错误') + '。请检查磁盘空间和目录权限后重试。';
|
|
29
38
|
|
|
30
39
|
case 'setup_id_unavailable':
|
|
31
40
|
return '❌ 未能建立 llm-wiki 设定项目:' + (err || '未知原因') +
|
|
32
|
-
'\n
|
|
41
|
+
'\n导入产物必须位于「设定」目录下才会被纳管。请先按提示完成 llm-wiki 配置。';
|
|
33
42
|
|
|
34
43
|
case 'import_done':
|
|
35
44
|
return '✅ 导入完成:' + chapters + ' 章旧稿已转成 story-builder 格式,' +
|
|
@@ -38,8 +47,8 @@ export const importTemplates = {
|
|
|
38
47
|
(extra ? '\n\n' + extra : '');
|
|
39
48
|
|
|
40
49
|
case 'import_incomplete':
|
|
41
|
-
return '⚠️
|
|
42
|
-
'
|
|
50
|
+
return '⚠️ 旧稿导入已完成,但有 ' + (failed || 0) + ' 轮反推未通过校验。' +
|
|
51
|
+
'**当前导入产物不完整**,待补清单见 `导入报告/导入报告.md`。' +
|
|
43
52
|
(extra ? '\n\n' + extra : '');
|
|
44
53
|
|
|
45
54
|
default:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Material library Markdown export for atomic blocks.
|
|
3
|
+
*
|
|
4
|
+
* Every record is one small block (name + tags + genres + optional note), so
|
|
5
|
+
* the export is deliberately terse:
|
|
6
|
+
*
|
|
7
|
+
* ## 命名
|
|
8
|
+
* ### 地名(place_name)
|
|
9
|
+
* #### 归墟
|
|
10
|
+
* - **tags**:地名、海、尽头、仙侠
|
|
11
|
+
* - **genres**:仙侠、通用
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { atomicWriteFile } from '#atomic-write';
|
|
16
|
+
import { localIsoString } from '#workspace';
|
|
17
|
+
import { connectMaterial } from '#material-db';
|
|
18
|
+
import { KIND_GROUPS, KIND_LABELS } from './kinds.js';
|
|
19
|
+
|
|
20
|
+
const KIND_ORDER = KIND_GROUPS.flatMap((group) => group.kinds);
|
|
21
|
+
const GROUP_OF = new Map();
|
|
22
|
+
for (const group of KIND_GROUPS) {
|
|
23
|
+
for (const kind of group.kinds) GROUP_OF.set(kind, group.title);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function text(value) {
|
|
27
|
+
return String(value == null ? '' : value).replace(/\r?\n/g, ' ').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function heading(value) {
|
|
31
|
+
return text(value).replace(/\s+/g, ' ').trim() || '(未命名)';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function valueText(value) {
|
|
35
|
+
if (value === undefined || value === null || value === '') return '—';
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.length ? value.map((item) => text(item)).join('、') : '—';
|
|
38
|
+
}
|
|
39
|
+
return text(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render atomic materials as Markdown.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array<object>} records
|
|
46
|
+
* @param {{workspace?: string, database?: string, exported_at?: string}} [meta]
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function renderMaterialMarkdown(records, meta = {}) {
|
|
50
|
+
const list = Array.isArray(records) ? records : [];
|
|
51
|
+
const byKind = new Map();
|
|
52
|
+
for (const record of list) {
|
|
53
|
+
const kind = String(record && record.kind || 'unknown');
|
|
54
|
+
if (!byKind.has(kind)) byKind.set(kind, []);
|
|
55
|
+
byKind.get(kind).push(record);
|
|
56
|
+
}
|
|
57
|
+
for (const rows of byKind.values()) {
|
|
58
|
+
rows.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push('# 网文创作素材库');
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push('> 导出时间:' + (meta.exported_at || localIsoString()));
|
|
65
|
+
if (meta.workspace) lines.push('> 工作区:' + meta.workspace);
|
|
66
|
+
if (meta.database) lines.push('> 数据库:' + meta.database);
|
|
67
|
+
lines.push('> 素材:' + list.length + ' 条 · 类型:' + byKind.size + ' 个');
|
|
68
|
+
lines.push('');
|
|
69
|
+
|
|
70
|
+
if (!list.length) {
|
|
71
|
+
lines.push('暂无素材。运行 `/fancy-material` 生成,或 `/fancy-material import <文件>` 导入。');
|
|
72
|
+
lines.push('');
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push('## 目录');
|
|
77
|
+
lines.push('');
|
|
78
|
+
for (const group of KIND_GROUPS) {
|
|
79
|
+
const rows = group.kinds.reduce((sum, kind) => sum + (byKind.get(kind)?.length || 0), 0);
|
|
80
|
+
if (!rows) continue;
|
|
81
|
+
lines.push(`- ${group.title}:${rows} 条`);
|
|
82
|
+
}
|
|
83
|
+
lines.push('');
|
|
84
|
+
|
|
85
|
+
for (const group of KIND_GROUPS) {
|
|
86
|
+
const groupRows = group.kinds.reduce((sum, kind) => sum + (byKind.get(kind)?.length || 0), 0);
|
|
87
|
+
if (!groupRows) continue;
|
|
88
|
+
lines.push('## ' + group.title);
|
|
89
|
+
lines.push('');
|
|
90
|
+
for (const kind of group.kinds) {
|
|
91
|
+
const rows = byKind.get(kind) || [];
|
|
92
|
+
if (!rows.length) continue;
|
|
93
|
+
lines.push(`### ${KIND_LABELS[kind] || kind}(${kind})`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
for (const record of rows) {
|
|
96
|
+
lines.push('#### ' + heading(record.name));
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push('- **tags**:' + valueText(record.tags));
|
|
99
|
+
lines.push('- **genres**:' + valueText(record.genres));
|
|
100
|
+
if (record.source) lines.push('- **source**:' + text(record.source));
|
|
101
|
+
if (record.note) lines.push('- **note**:' + text(record.note));
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Include any unknown kinds at the end rather than silently dropping them.
|
|
108
|
+
const unknown = [...byKind.keys()].filter((kind) => !KIND_ORDER.includes(kind));
|
|
109
|
+
if (unknown.length) {
|
|
110
|
+
lines.push('## 其他');
|
|
111
|
+
lines.push('');
|
|
112
|
+
for (const kind of unknown) {
|
|
113
|
+
lines.push(`### ${kind}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
for (const record of byKind.get(kind)) {
|
|
116
|
+
lines.push('#### ' + heading(record.name));
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('- **tags**:' + valueText(record.tags));
|
|
119
|
+
lines.push('- **genres**:' + valueText(record.genres));
|
|
120
|
+
if (record.source) lines.push('- **source**:' + text(record.source));
|
|
121
|
+
if (record.note) lines.push('- **note**:' + text(record.note));
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n*$/, '\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Query the material DB and write `<workspace>/素材库.md`.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} root workspace directory
|
|
134
|
+
* @returns {Promise<{file: string, count: number, kinds: number, database: string}>}
|
|
135
|
+
*/
|
|
136
|
+
export async function exportMaterials(root) {
|
|
137
|
+
const { db, database } = await connectMaterial(root);
|
|
138
|
+
try {
|
|
139
|
+
const rows = await db.query('SELECT kind, name, tags, genres, note, source FROM material ORDER BY kind, name;');
|
|
140
|
+
const records = Array.isArray(rows) && Array.isArray(rows[0]) ? rows[0] : [];
|
|
141
|
+
const file = path.join(path.resolve(root), '素材库.md');
|
|
142
|
+
const markdown = renderMaterialMarkdown(records, {
|
|
143
|
+
workspace: root,
|
|
144
|
+
database,
|
|
145
|
+
exported_at: localIsoString(),
|
|
146
|
+
});
|
|
147
|
+
atomicWriteFile(file, markdown, { mode: 0o644 });
|
|
148
|
+
const kinds = new Set(records.map((record) => record.kind).filter(Boolean));
|
|
149
|
+
return { file, count: records.length, kinds: kinds.size, database };
|
|
150
|
+
} finally {
|
|
151
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
152
|
+
}
|
|
153
|
+
}
|