@cloud411716/fancy-webnovel 2.2.0 → 3.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/client.js +201 -15
- package/cordis.patch.yml +5 -0
- 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 +11 -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 +29 -4
- 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 +143 -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,612 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* append-summary-chapter.js —— 往总结文件里追加技法内容 + 登记章节
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么需要这个脚本
|
|
6
|
+
*
|
|
7
|
+
* 「已总结章节」清单是插件判断"哪些章节已经总结过"的**唯一事实源**
|
|
8
|
+
* (summary-progress.js:374 —— 头部权威;头部非空时正文反推不参与)。
|
|
9
|
+
*
|
|
10
|
+
* 让模型手工编辑 YAML 头部来"追加一行"非常脆:模型习惯用 write 重写整个
|
|
11
|
+
* 文件,一旦把头部写坏了(只留最新登记的那一章),之前登记的章节**全丢** ——
|
|
12
|
+
* 下次 summary 会把它们全部重做一遍,文件越滚越胖,而且没有任何报错。
|
|
13
|
+
*
|
|
14
|
+
* 这个脚本把"追加"做成**幂等且原子**的操作:
|
|
15
|
+
* · 章节已在清单里 → 跳过,不重复登记;
|
|
16
|
+
* · 文件有正文但没有 front-matter → 补一个;
|
|
17
|
+
* · front-matter 里没有「已总结章节」键 → 补上;
|
|
18
|
+
* · 写盘用临时文件 + rename,中途失败不会留下半截文件。
|
|
19
|
+
*
|
|
20
|
+
* ## 用法
|
|
21
|
+
*
|
|
22
|
+
* node append-summary-chapter.js --file=<绝对路径> --chapter=<章节名>
|
|
23
|
+
*
|
|
24
|
+
* 追加正文(从 stdin 读,heredoc 最方便):
|
|
25
|
+
*
|
|
26
|
+
* node append-summary-chapter.js --file=<绝对路径> --chapter=<章节名> <<'EOF'
|
|
27
|
+
* ### 技法名称
|
|
28
|
+
* - 来源章节:某书-某技法-第1章
|
|
29
|
+
* - 核心要点:……
|
|
30
|
+
* EOF
|
|
31
|
+
*
|
|
32
|
+
* 正文也可以用 --body-file=<路径> 给。
|
|
33
|
+
*
|
|
34
|
+
* 输出:一行 JSON({ ok, file, added, alreadyRegistered, bodyAdded, total })
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, statSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, realpathSync } from 'fs';
|
|
37
|
+
import { dirname, resolve, relative, isAbsolute } from 'path';
|
|
38
|
+
|
|
39
|
+
const KEY = '已总结章节';
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const out = { file: '', chapter: '', bodyFile: '', projectRoot: '', verify: false, section: '核心技法' };
|
|
43
|
+
for (const arg of argv) {
|
|
44
|
+
if (arg === '--verify') { out.verify = true; continue; }
|
|
45
|
+
const m = /^--([a-zA-Z-]+)=(.*)$/.exec(arg);
|
|
46
|
+
if (!m) continue;
|
|
47
|
+
const key = m[1];
|
|
48
|
+
if (key === 'file') out.file = m[2];
|
|
49
|
+
else if (key === 'chapter') out.chapter = m[2];
|
|
50
|
+
else if (key === 'body-file') out.bodyFile = m[2];
|
|
51
|
+
else if (key === 'project-root') out.projectRoot = m[2];
|
|
52
|
+
else if (key === 'section') out.section = m[2].trim() || '核心技法';
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** front-matter 里可能出现的键(用于识别坏头部的残留块)。 */
|
|
58
|
+
const FM_KEY = /^(已总结章节|标题|标签)\s*[::]/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 头部有开头没结尾时,把开头的 YAML 残块从正文里剥掉。
|
|
62
|
+
*
|
|
63
|
+
* 只认已知的几个键和它们下面的列表项 —— 不能用"看起来像 key: value"
|
|
64
|
+
* 这种宽泛的判断,否则正文里的 URL(https://...)会被误当成头部。
|
|
65
|
+
*
|
|
66
|
+
* @param {string} text - 全文
|
|
67
|
+
* @returns {string} 剥掉头部残块后的正文
|
|
68
|
+
*/
|
|
69
|
+
function stripBrokenHeader(text) {
|
|
70
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
71
|
+
if (!/^---\s*$/.test(lines[0] || '')) return text;
|
|
72
|
+
let i = 1;
|
|
73
|
+
let inList = false;
|
|
74
|
+
for (; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
if (line.trim() === '') continue;
|
|
77
|
+
if (FM_KEY.test(line)) { inList = true; continue; }
|
|
78
|
+
if (inList && /^\s*-\s+\S/.test(line)) continue;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return lines.slice(i).join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 取开头两段 --- 之间的 front-matter 原文;没有则 null。
|
|
86
|
+
*
|
|
87
|
+
* 头��**有开头没结尾**(被 write 冲掉了结尾的 ---)时 fm 也是 null,
|
|
88
|
+
* 但要跟"根本没有头部"区分开:前者要抢救清单,不能当成新文件重建。
|
|
89
|
+
*
|
|
90
|
+
* @param {string} text
|
|
91
|
+
* @returns {{fm: string|null, body: string, broken: boolean, partialFm: string}}
|
|
92
|
+
*/
|
|
93
|
+
function splitFrontmatter(text) {
|
|
94
|
+
const normalized = text.replace(/^/, '');
|
|
95
|
+
if (!/^---\r?\n/.test(normalized)) {
|
|
96
|
+
return { fm: null, body: normalized, broken: false, partialFm: '' };
|
|
97
|
+
}
|
|
98
|
+
const rest = normalized.slice(normalized.indexOf('\n') + 1);
|
|
99
|
+
const end = rest.search(/^---[ \t]*(\r?\n|$)/m);
|
|
100
|
+
if (end === -1) {
|
|
101
|
+
// 有开头没结尾 → 头部坏了。残块留着,等下从里面抢救清单。
|
|
102
|
+
return {
|
|
103
|
+
fm: null,
|
|
104
|
+
body: stripBrokenHeader(normalized),
|
|
105
|
+
broken: true,
|
|
106
|
+
partialFm: rest,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// fm 不含 --- 标记,body 也不含 —— 写盘时再统一包回去
|
|
110
|
+
return {
|
|
111
|
+
fm: rest.slice(0, end),
|
|
112
|
+
body: rest.slice(end).replace(/^---[ \t]*\r?\n?/, ''),
|
|
113
|
+
broken: false,
|
|
114
|
+
partialFm: '',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 从 front-matter 里读已登记章节(块列表 + 行内数组都认)。 */
|
|
119
|
+
function parseChapters(fm) {
|
|
120
|
+
if (!fm) return [];
|
|
121
|
+
const lines = fm.split(/\r?\n/);
|
|
122
|
+
const out = [];
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const km = lines[i].match(/^\s*已总结章节\s*[::]\s*(.*)$/);
|
|
125
|
+
if (!km) continue;
|
|
126
|
+
const inline = km[1].trim();
|
|
127
|
+
if (inline.startsWith('[') && inline.endsWith(']')) {
|
|
128
|
+
for (const part of inline.slice(1, -1).split(',')) {
|
|
129
|
+
const v = part.trim().replace(/^["']|["']$/g, '').trim();
|
|
130
|
+
if (v) out.push(v);
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
135
|
+
const item = lines[j].match(/^\s*-\s+(.*\S)\s*$/);
|
|
136
|
+
if (!item) {
|
|
137
|
+
if (lines[j].trim() === '') continue;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
out.push(item[1].trim().replace(/^["']|["']$/g, '').trim());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 从正文里**反推**已总结的章节名。
|
|
148
|
+
*
|
|
149
|
+
* ## 为什么需要
|
|
150
|
+
*
|
|
151
|
+
* 头部损坏时(front-matter 被 write 冲掉、或者结尾的 --- 没了),
|
|
152
|
+
* 直接重建头部会**把已登记的章节全冲掉** —— 实测:
|
|
153
|
+
* 头部里已有甲-第1/2/3章,坏掉后追加甲-第4章,
|
|
154
|
+
* 新头部里只剩甲-第4章,而脚本还报 ok:true。
|
|
155
|
+
* 下次 summary 会把甲-第1/2/3章全部重做一遍。
|
|
156
|
+
*
|
|
157
|
+
* 但每条技法条目本身就记着它来自哪些文件:
|
|
158
|
+
*
|
|
159
|
+
* ### 轻叙、爆料与心理停顿交替
|
|
160
|
+
* - 来源章节:全球高武-节奏韵律-第1章
|
|
161
|
+
*
|
|
162
|
+
* 所以坏掉时可以从正文反推,把清单补回来。
|
|
163
|
+
* 这跟 summary-progress.js 的 inferSummarizedChapters 同一套思路 ——
|
|
164
|
+
* **反推不如头部权威**(可能漏写),但比"全部推倒重来"好得多。
|
|
165
|
+
*
|
|
166
|
+
* @param {string} body - 正文(不含 front-matter)
|
|
167
|
+
* @returns {string[]} 反推出的章节名(去重、保序)
|
|
168
|
+
*/
|
|
169
|
+
function inferChapters(body) {
|
|
170
|
+
const out = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const match of String(body || '').matchAll(/^\s*-\s*来源(?:章节|实体|页面)\s*[::]\s*(.+)$/gm)) {
|
|
173
|
+
// "来源章节:甲-第1章、甲-第2章" → 拆开
|
|
174
|
+
for (const part of String(match[1]).split(/[、,,;;]/)) {
|
|
175
|
+
const v = part.trim();
|
|
176
|
+
if (!v || seen.has(v)) continue;
|
|
177
|
+
seen.add(v);
|
|
178
|
+
out.push(v);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 正文里是否已经有这一章的「来源章节」行。
|
|
186
|
+
*
|
|
187
|
+
* 为什么需要:头部说"已登记"不代表正文一定写过(比如上一次只登记了清单,
|
|
188
|
+
* 或者模型重试时又调用了一次)。如果正文已有这一章,就不能再把同一段
|
|
189
|
+
* 技法追加一遍 —— 否则重试会产出重复条目,文件越滚越胖。
|
|
190
|
+
*
|
|
191
|
+
* @param {string} body
|
|
192
|
+
* @param {string} chapter
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function bodyHasChapter(body, chapter) {
|
|
196
|
+
const target = String(chapter || '').trim().replace(/\.md$/i, '');
|
|
197
|
+
if (!target) return false;
|
|
198
|
+
for (const match of String(body || '').matchAll(/^\s*-\s*来源(?:章节|实体|页面)\s*[::]\s*(.+)$/gm)) {
|
|
199
|
+
for (const part of String(match[1]).split(/[、,,;;]/)) {
|
|
200
|
+
if (part.trim().replace(/\.md$/i, '') === target) return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 把章节名写进 front-matter 的「已总结章节」清单。
|
|
208
|
+
*
|
|
209
|
+
* - 已有该键(块列表)→ 插到列表末尾
|
|
210
|
+
* - 已有该键(行内数组)→ 转成块列表再插
|
|
211
|
+
* - 没有该键 → 在 front-matter 末尾补一个块列表
|
|
212
|
+
* - front-matter 整个不存在 → 建一份只含「已总结章节」的
|
|
213
|
+
*
|
|
214
|
+
* ## 为什么头部只有这一个键
|
|
215
|
+
*
|
|
216
|
+
* 早先还会写「标题:」和「标签:」两行,但整个插件里**没有任何代码读它们**
|
|
217
|
+
* (summary-progress.js 只认「已总结章节」这一个键)—— 它们只是看着完整,
|
|
218
|
+
* 实际是死字段。模型看到这个模板还容易误以为"头部该由我手写",
|
|
219
|
+
* 而手写头部正是 write 冲掉清单的诱因。
|
|
220
|
+
*
|
|
221
|
+
* @param {string|null} fm - front-matter 原文(不含 --- 标记)
|
|
222
|
+
* @param {string} chapter - 章节名
|
|
223
|
+
* @param {string[]} recovered - 头部损坏时从正文反推出来的章节名
|
|
224
|
+
* @returns {string} 新的 front-matter 原文
|
|
225
|
+
*/
|
|
226
|
+
function registerChapter(fm, chapter, recovered) {
|
|
227
|
+
if (fm === null) {
|
|
228
|
+
// 文件没有可用的 front-matter —— 建一份新的。
|
|
229
|
+
//
|
|
230
|
+
// 关键:把从正文反推出来的章节**一起**补回去。
|
|
231
|
+
// 头部损坏时如果只放当前这一章,之前登记的章节就被冲掉了
|
|
232
|
+
// (实测:已有甲-第1/2/3章,坏掉后追加甲-第4章 → 新头部只剩甲-第4章,
|
|
233
|
+
// 而脚本还报 ok:true),下次 summary 会把那 3 章全部重做一遍。
|
|
234
|
+
const list = [];
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
for (const c of (recovered || [])) {
|
|
237
|
+
if (!c || seen.has(c)) continue;
|
|
238
|
+
seen.add(c);
|
|
239
|
+
list.push(c);
|
|
240
|
+
}
|
|
241
|
+
if (!seen.has(chapter)) list.push(chapter);
|
|
242
|
+
return [
|
|
243
|
+
`${KEY}:`,
|
|
244
|
+
...list.map(c => ` - ${c}`),
|
|
245
|
+
].join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lines = fm.split(/\r?\n/);
|
|
249
|
+
// 去掉末尾的空行
|
|
250
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
251
|
+
|
|
252
|
+
let keyIndex = -1;
|
|
253
|
+
let inline = '';
|
|
254
|
+
for (let i = 0; i < lines.length; i++) {
|
|
255
|
+
const km = lines[i].match(/^(\s*已总结章节\s*[::]\s*)(.*)$/);
|
|
256
|
+
if (km) { keyIndex = i; inline = km[2].trim(); break; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (keyIndex === -1) {
|
|
260
|
+
// 补键
|
|
261
|
+
lines.push(`${KEY}:`);
|
|
262
|
+
lines.push(` - ${chapter}`);
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (inline.startsWith('[') && inline.endsWith(']')) {
|
|
267
|
+
// 行内数组 → 转块列表
|
|
268
|
+
const items = [];
|
|
269
|
+
for (const part of inline.slice(1, -1).split(',')) {
|
|
270
|
+
const v = part.trim().replace(/^["']|["']$/g, '').trim();
|
|
271
|
+
if (v) items.push(v);
|
|
272
|
+
}
|
|
273
|
+
items.push(chapter);
|
|
274
|
+
lines.splice(keyIndex, 1, `${KEY}:`, ...items.map(x => ` - ${x}`));
|
|
275
|
+
return lines.join('\n');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 块列表:插到该列表末尾
|
|
279
|
+
let insertAt = keyIndex + 1;
|
|
280
|
+
while (insertAt < lines.length) {
|
|
281
|
+
const item = lines[insertAt].match(/^\s*-\s+(.*\S)\s*$/);
|
|
282
|
+
if (!item) {
|
|
283
|
+
if (lines[insertAt].trim() === '') { insertAt++; continue; }
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
insertAt++;
|
|
287
|
+
}
|
|
288
|
+
lines.splice(insertAt, 0, ` - ${chapter}`);
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 把正文块追加到指定章节末尾(默认「## 核心技法」);没有该章节就新建。 */
|
|
293
|
+
function appendBody(body, addition, sectionName = '核心技法') {
|
|
294
|
+
const text = addition.trim();
|
|
295
|
+
if (!text) return body;
|
|
296
|
+
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
297
|
+
const marker = new RegExp(`^##\\s+${escaped}\\s*$`, 'm');
|
|
298
|
+
const m = marker.exec(body);
|
|
299
|
+
if (!m) {
|
|
300
|
+
const sep = body.endsWith('\n') ? '' : '\n';
|
|
301
|
+
return `${body}${sep}\n## ${sectionName}\n\n${text}\n`;
|
|
302
|
+
}
|
|
303
|
+
const start = m.index + m[0].length;
|
|
304
|
+
const rest = body.slice(start);
|
|
305
|
+
const nextHeading = /^##\s+/m.exec(rest);
|
|
306
|
+
const end = nextHeading ? start + nextHeading.index : body.length;
|
|
307
|
+
const head = body.slice(0, end).replace(/\s*$/, '\n');
|
|
308
|
+
const tail = body.slice(end);
|
|
309
|
+
return `${head}\n${text}\n${tail}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 统计「## 核心技法」段落下的技法条目。
|
|
314
|
+
*
|
|
315
|
+
* 条目 = `###` 三级标题(与 summary-progress.js 的 entries 口径一致)。
|
|
316
|
+
* 顺带看每条有没有「来源章节」行 —— 那是头部缺失时反推已总结章节的
|
|
317
|
+
* 兜底依据(summary-progress.js:222),缺了就会连兜底一起失效。
|
|
318
|
+
*
|
|
319
|
+
* @param {string} body - 正文(不含 front-matter)
|
|
320
|
+
* @returns {{entries: number, withSource: number, missingSource: number}}
|
|
321
|
+
*/
|
|
322
|
+
function inspectEntries(body, sectionName = '核心技法') {
|
|
323
|
+
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
324
|
+
const marker = new RegExp(`^##\\s+${escaped}\\s*$`, 'm').exec(body);
|
|
325
|
+
const scope = marker ? body.slice(marker.index) : '';
|
|
326
|
+
// 只数「核心技法」这一节里的 ###,别把后面别的 ## 段落算进来
|
|
327
|
+
const section = scope.replace(new RegExp(`^##\\s+${escaped}\\s*$`, 'm'), '');
|
|
328
|
+
const nextSection = /^##\s+/m.exec(section);
|
|
329
|
+
const text = nextSection ? section.slice(0, nextSection.index) : section;
|
|
330
|
+
|
|
331
|
+
const heads = [...text.matchAll(/^###\s+(.+?)\s*$/gm)];
|
|
332
|
+
let withSource = 0;
|
|
333
|
+
for (const h of heads) {
|
|
334
|
+
const start = h.index + h[0].length;
|
|
335
|
+
const nextHead = text.slice(start).search(/^###\s+/m);
|
|
336
|
+
const chunk = nextHead === -1 ? text.slice(start) : text.slice(start, start + nextHead);
|
|
337
|
+
if (/^\s*-\s*来源(?:章节|实体|页面)\s*[::]/m.test(chunk)) withSource++;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
entries: heads.length,
|
|
341
|
+
withSource,
|
|
342
|
+
missingSource: heads.length - withSource,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 只读校验:报告登记状态,**不修改任何文件**。
|
|
348
|
+
*
|
|
349
|
+
* 为什么不用 `head -20`:章节一多,front-matter 里的清单就超过 20 行,
|
|
350
|
+
* head 根本看不到最新登记的那一条 —— 越到后期越验不出来。清单条数和
|
|
351
|
+
* 最后一条在这里直接算出来,跟章节数多少无关。
|
|
352
|
+
*
|
|
353
|
+
* @param {string} file - 总结文件绝对路径
|
|
354
|
+
* @returns {number} 退出码
|
|
355
|
+
*/
|
|
356
|
+
function verifyMain(file, sectionName = '核心技法') {
|
|
357
|
+
if (!existsSync(file)) {
|
|
358
|
+
process.stdout.write(JSON.stringify({
|
|
359
|
+
ok: false, verify: true, file, exists: false,
|
|
360
|
+
}) + '\n');
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
let text = '';
|
|
364
|
+
try {
|
|
365
|
+
text = readFileSync(file, 'utf-8');
|
|
366
|
+
} catch (err) {
|
|
367
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
368
|
+
process.stdout.write(JSON.stringify({
|
|
369
|
+
ok: false, verify: true, file, error: msg,
|
|
370
|
+
}) + '\n');
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const { fm, body, broken, partialFm } = splitFrontmatter(text);
|
|
375
|
+
const chapters = parseChapters(fm);
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
const duplicates = [];
|
|
378
|
+
for (const c of chapters) {
|
|
379
|
+
if (seen.has(c)) duplicates.push(c);
|
|
380
|
+
seen.add(c);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const entryInfo = inspectEntries(body, sectionName);
|
|
384
|
+
const frontmatterOk = fm !== null;
|
|
385
|
+
const hasCoreSection = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(body);
|
|
386
|
+
|
|
387
|
+
// 头部坏了 → 清单读不出来,但残留块/正文里可能还捞得回一些。
|
|
388
|
+
// 报告出来,让模型知道"登记清单可能不完整"。
|
|
389
|
+
const recovered = fm === null
|
|
390
|
+
? [...new Set([...parseChapters(partialFm), ...inferChapters(body)])]
|
|
391
|
+
: [];
|
|
392
|
+
|
|
393
|
+
// ---- 判定"内容是否正常" ----
|
|
394
|
+
//
|
|
395
|
+
// ok 只表示"文件读到了"是不够的:模型拿到 ok:true 会以为一切正常,
|
|
396
|
+
// 于是带着坏掉的登记状态继续往下写,把问题越滚越大。
|
|
397
|
+
// 所以这里把"什么算正常"直接算出来,模型看 ok 和 problems 就行,
|
|
398
|
+
// 不用自己推断字段之间的关系。
|
|
399
|
+
//
|
|
400
|
+
// entries 与 total 的关系(**两者不要求相等**):
|
|
401
|
+
// · 一章提炼出多条技法 → entries > total;
|
|
402
|
+
// · 多章合并成同一条技法(提示词里的"去重合并"原则)→ entries < total;
|
|
403
|
+
// · 一章一条 → entries == total。
|
|
404
|
+
// 所以 entries 与 total 谁大谁小都正常,**只有下面几条是不正常的**。
|
|
405
|
+
const problems = [];
|
|
406
|
+
if (broken) {
|
|
407
|
+
problems.push('文件头有开头没结尾(被覆盖过):frontmatterOk=false。'
|
|
408
|
+
+ `已登记章节清单读不出来,但从残留内容里反推出 ${recovered.length} 章`
|
|
409
|
+
+ (recovered.length ? `(${recovered.join('、')})` : '')
|
|
410
|
+
+ '—— 下次写入会把这些补回头部,但清单可能仍不完整,建议检查该文件');
|
|
411
|
+
} else if (!frontmatterOk) {
|
|
412
|
+
problems.push('文件头缺失或损坏:frontmatterOk=false(已登记章节清单读不出来,'
|
|
413
|
+
+ '下次执行会把这批章节全部重做)');
|
|
414
|
+
}
|
|
415
|
+
if (duplicates.length > 0) {
|
|
416
|
+
problems.push(`有重复登记的章节(${duplicates.length} 个):${duplicates.join('、')}`);
|
|
417
|
+
}
|
|
418
|
+
if (!hasCoreSection) {
|
|
419
|
+
problems.push(`正文中未找到「## ${sectionName}」段(hasCoreSection=false,` +
|
|
420
|
+
'总结内容没落到正确的位置)');
|
|
421
|
+
}
|
|
422
|
+
if (entryInfo.entries === 0 && chapters.length > 0) {
|
|
423
|
+
problems.push(`已登记了章节,但「## ${sectionName}」下没有任何条目`);
|
|
424
|
+
}
|
|
425
|
+
if (entryInfo.missingSource > 0) {
|
|
426
|
+
problems.push(`有 ${entryInfo.missingSource} 个条目缺「来源章节/来源实体」行`
|
|
427
|
+
+ '(头部一旦损坏,这些条目就没法反推出章节名)');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
process.stdout.write(JSON.stringify({
|
|
431
|
+
// ok 现在是**内容判定结果**:false 说明有问题,看 problems 找原因
|
|
432
|
+
ok: problems.length === 0,
|
|
433
|
+
verify: true,
|
|
434
|
+
file,
|
|
435
|
+
exists: true,
|
|
436
|
+
problems,
|
|
437
|
+
frontmatterOk,
|
|
438
|
+
brokenHeader: broken,
|
|
439
|
+
recovered,
|
|
440
|
+
total: chapters.length,
|
|
441
|
+
last: chapters.length ? chapters[chapters.length - 1] : null,
|
|
442
|
+
duplicates,
|
|
443
|
+
hasCoreSection,
|
|
444
|
+
// 技法条目(内容)的校验结果
|
|
445
|
+
entries: entryInfo.entries,
|
|
446
|
+
entriesWithSource: entryInfo.withSource,
|
|
447
|
+
entriesMissingSource: entryInfo.missingSource,
|
|
448
|
+
bytes: Buffer.byteLength(text, 'utf-8'),
|
|
449
|
+
}) + '\n');
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* 校验 --file 是否在项目内的 对标/wiki/<技法>/<技法>-总结.md。
|
|
455
|
+
*
|
|
456
|
+
* 只有调用方显式给了 --project-root 才校验;老测试/直接调用不带这个参数,
|
|
457
|
+
* 保持兼容。生产提示词会带上它,避免模型被参考文本里的注入指令带偏,
|
|
458
|
+
* 把内容写到项目外。
|
|
459
|
+
*
|
|
460
|
+
* @param {{projectRoot: string, file: string}} args
|
|
461
|
+
* @returns {string|null} 错误信息;合法时返回 null
|
|
462
|
+
*/
|
|
463
|
+
function validateTargetPath(args) {
|
|
464
|
+
if (!args.projectRoot) return null;
|
|
465
|
+
let root;
|
|
466
|
+
let rootReal;
|
|
467
|
+
try {
|
|
468
|
+
root = resolve(args.projectRoot);
|
|
469
|
+
if (!statSync(root).isDirectory()) return `项目根不是目录: ${root}`;
|
|
470
|
+
rootReal = realpathSync(root);
|
|
471
|
+
} catch (e) {
|
|
472
|
+
return `项目根不可用: ${e instanceof Error ? e.message : String(e)}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const target = resolve(args.file);
|
|
476
|
+
// 先用词法路径判断(项目根本身可能是符号链接,不能拿 realpath 去比原始路径)
|
|
477
|
+
const rel = relative(root, target);
|
|
478
|
+
const normalized = rel.replace(/\\/g, '/');
|
|
479
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
|
|
480
|
+
return `总结文件必须位于项目根内: ${target}`;
|
|
481
|
+
}
|
|
482
|
+
const parts = normalized.split('/');
|
|
483
|
+
if (parts.length !== 4 || parts[0] !== '对标' || parts[1] !== 'wiki'
|
|
484
|
+
|| !parts[3].endsWith('-总结.md')) {
|
|
485
|
+
return `总结文件必须位于 对标/wiki/<技法>/<技法>-总结.md: ${target}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 父目录若已存在,解析真实路径,防止通过项目内符号链接写到外面。
|
|
489
|
+
try {
|
|
490
|
+
const parentReal = realpathSync(dirname(target));
|
|
491
|
+
const parentRel = relative(rootReal, parentReal);
|
|
492
|
+
if (parentRel.startsWith('..') || isAbsolute(parentRel)) {
|
|
493
|
+
return `总结文件父目录不在项目根内: ${target}`;
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
// 父目录不存在:后面写入时会创建,词法检查已覆盖
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function main() {
|
|
502
|
+
const args = parseArgs(process.argv.slice(2));
|
|
503
|
+
if (!args.file) {
|
|
504
|
+
process.stdout.write(JSON.stringify({
|
|
505
|
+
ok: false,
|
|
506
|
+
error: '必须给 --file=<绝对路径>',
|
|
507
|
+
}) + '\n');
|
|
508
|
+
process.exit(2);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const pathError = validateTargetPath(args);
|
|
512
|
+
if (pathError) {
|
|
513
|
+
process.stdout.write(JSON.stringify({
|
|
514
|
+
ok: false,
|
|
515
|
+
error: pathError,
|
|
516
|
+
}) + '\n');
|
|
517
|
+
process.exit(2);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (args.verify) {
|
|
521
|
+
process.exit(verifyMain(args.file, args.section));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!args.chapter) {
|
|
525
|
+
process.stdout.write(JSON.stringify({
|
|
526
|
+
ok: false,
|
|
527
|
+
error: '必须给 --chapter=<章节名>(或用 --verify 只做校验)',
|
|
528
|
+
}) + '\n');
|
|
529
|
+
process.exit(2);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let bodyText = '';
|
|
533
|
+
if (args.bodyFile) {
|
|
534
|
+
if (!existsSync(args.bodyFile)) {
|
|
535
|
+
process.stdout.write(JSON.stringify({
|
|
536
|
+
ok: false,
|
|
537
|
+
error: `--body-file 不存在: ${args.bodyFile}`,
|
|
538
|
+
}) + '\n');
|
|
539
|
+
process.exit(2);
|
|
540
|
+
}
|
|
541
|
+
bodyText = readFileSync(args.bodyFile, 'utf-8');
|
|
542
|
+
} else if (!process.stdin.isTTY) {
|
|
543
|
+
try {
|
|
544
|
+
bodyText = readFileSync(0, 'utf-8');
|
|
545
|
+
} catch {
|
|
546
|
+
bodyText = '';
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const existed = existsSync(args.file);
|
|
551
|
+
const raw = existed ? readFileSync(args.file, 'utf-8') : '';
|
|
552
|
+
const { fm, body, broken, partialFm } = splitFrontmatter(raw);
|
|
553
|
+
|
|
554
|
+
// 头部坏了就抢救清单 ——
|
|
555
|
+
//
|
|
556
|
+
// 头部是唯一事实源(summary-progress.js 只认它),它一坏,
|
|
557
|
+
// "这章总结过没有"就无从判断,下次 summary 会把整本重做一遍。
|
|
558
|
+
// 但清单还在两处可能的地方:残留的头部块里,或正文的「来源章节」行里。
|
|
559
|
+
// 两者都捞一遍,捞到多少算多少(反推不如头部权威,但远好过推倒重来)。
|
|
560
|
+
let recovered = [];
|
|
561
|
+
if (fm === null && existed) {
|
|
562
|
+
recovered = parseChapters(partialFm);
|
|
563
|
+
for (const c of inferChapters(body)) {
|
|
564
|
+
if (!recovered.includes(c)) recovered.push(c);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const already = parseChapters(fm).includes(args.chapter)
|
|
569
|
+
|| recovered.includes(args.chapter);
|
|
570
|
+
// 头部没登记、但正文已经有这一章的「来源章节」行 → 只补头部,不重复追加正文。
|
|
571
|
+
const bodyAlready = bodyHasChapter(body, args.chapter);
|
|
572
|
+
// fm 为 null(头部坏/缺)时一定要重建头部 —— 即使这一章"已经登记过",
|
|
573
|
+
// 也得把抢救回来的清单写回去,否则清单就永远丢了。
|
|
574
|
+
const newFm = (fm !== null && already)
|
|
575
|
+
? fm
|
|
576
|
+
: registerChapter(fm, args.chapter, recovered);
|
|
577
|
+
const newBody = bodyAlready ? body : appendBody(body, bodyText, args.section);
|
|
578
|
+
|
|
579
|
+
// front-matter 必须用 --- 包起来 —— 少了它,summary-progress.js
|
|
580
|
+
// 的 extractFrontmatter 会认不出来,整份清单就废了。
|
|
581
|
+
const out = (newFm === null ? '' : '---\n' + newFm + '\n---\n') + newBody;
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const dir = dirname(args.file);
|
|
585
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
586
|
+
const tmp = `${args.file}.tmp-${Date.now()}-${process.pid}`;
|
|
587
|
+
writeFileSync(tmp, out, 'utf-8');
|
|
588
|
+
renameSync(tmp, args.file);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
591
|
+
process.stdout.write(JSON.stringify({ ok: false, file: args.file, error: msg }) + '\n');
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
process.stdout.write(JSON.stringify({
|
|
596
|
+
// ok=true 表示**这次写入确实落盘了**(写盘异常时前面已 exit(1))
|
|
597
|
+
ok: true,
|
|
598
|
+
file: args.file,
|
|
599
|
+
added: !already,
|
|
600
|
+
alreadyRegistered: already,
|
|
601
|
+
bodyAdded: Boolean(bodyText.trim()) && !bodyAlready,
|
|
602
|
+
bodyAlreadyPresent: bodyAlready,
|
|
603
|
+
// 写完后文件里的实际状态,方便模型直接确认,不用再 cat 一遍
|
|
604
|
+
total: parseChapters(newFm).length,
|
|
605
|
+
hasCoreSection: /^##\s+核心技法\s*$/m.test(newBody),
|
|
606
|
+
// 头部原来是坏的 → 已从残留头部/正文反推抢救回这些章节
|
|
607
|
+
repaired: fm === null && existed,
|
|
608
|
+
recovered: recovered.length,
|
|
609
|
+
}) + '\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
main();
|