@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
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
* fancy-scan plugin entry
|
|
3
3
|
*/
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname, join } from 'path';
|
|
5
|
+
import { dirname, join, resolve } from 'path';
|
|
6
6
|
import { askUserQuestion, llmFollowup, notify, spawnScript } from '#infra';
|
|
7
7
|
import { readDeployedState, deployedStateProblemText } from '#deployed-state';
|
|
8
|
+
import { isSensitiveProjectPath, isValidProjectPath } from '#path-guard';
|
|
9
|
+
import { resolveWorkspace } from '#workspace';
|
|
8
10
|
import { localDateStamp, DEFAULT_SCAN_MODE } from '#scraper-utils';
|
|
11
|
+
import {
|
|
12
|
+
backupTopicDecision,
|
|
13
|
+
listMarkerBlocks,
|
|
14
|
+
repairTopicDecision,
|
|
15
|
+
inspectTopicDecision,
|
|
16
|
+
dedupeMarkerBlocks,
|
|
17
|
+
ensureTrailingNewline,
|
|
18
|
+
} from '#topic-decision';
|
|
19
|
+
import { readFileSync, existsSync, lstatSync, realpathSync, unlinkSync, mkdirSync } from 'fs';
|
|
9
20
|
import { scan, PLATFORM_TABLE, RANK_LISTS, parseScanMode } from './templates.js';
|
|
21
|
+
import { captureFileState, verifyBlockWritten } from './scripts/report-validate.js';
|
|
22
|
+
import { inspectExistingBlock, EXISTING_BLOCK_STATUS } from './existing-block.js';
|
|
23
|
+
import { waitForAnalysisTurn } from './wait-turn.js';
|
|
24
|
+
import { deployScanTools, writeScanManifest } from './tool-deploy.js';
|
|
10
25
|
|
|
11
26
|
export const inject = ['commands', 'userQuestions'];
|
|
12
27
|
|
|
@@ -59,7 +74,7 @@ function getRankLabels(rankIds, platform) {
|
|
|
59
74
|
export async function apply(ctx) {
|
|
60
75
|
ctx.commands.register({
|
|
61
76
|
name: 'fancy-scan',
|
|
62
|
-
description: '📊 扫榜分析 ( usage: /fancy-scan )',
|
|
77
|
+
description: '📊 扫榜分析 ( usage: /fancy-scan [help] )',
|
|
63
78
|
input: {
|
|
64
79
|
hint: '.',
|
|
65
80
|
},
|
|
@@ -67,17 +82,66 @@ export async function apply(ctx) {
|
|
|
67
82
|
const session = invocation.agent.session;
|
|
68
83
|
const { signal } = invocation;
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
if ((invocation.rawInput || '').trim().toLowerCase() === 'help') {
|
|
86
|
+
return notify(session, 'success', scan.notify({ type: 'help' }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 获取工作区目录(忽略所有命令行参数,统一通过交互选择)
|
|
90
|
+
// 不能回退到 process.cwd():那是宿主进程的工作目录,不是用户工作区目录。
|
|
72
91
|
// 宿主若恰好在某个含 .fancy-deployed 的目录里启动,会对错误的项目执行部署与写盘。
|
|
73
|
-
const
|
|
92
|
+
const cwd = session.header?.cwd ?? null;
|
|
93
|
+
let projectRoot;
|
|
94
|
+
try {
|
|
95
|
+
projectRoot = resolveWorkspace({ cwd, required: true });
|
|
96
|
+
} catch {
|
|
97
|
+
projectRoot = cwd ? resolve(cwd) : null;
|
|
98
|
+
}
|
|
74
99
|
if (!projectRoot) {
|
|
75
100
|
return notify(session, 'error', scan.notify({
|
|
76
101
|
type: 'no_project_root',
|
|
77
|
-
err: '
|
|
102
|
+
err: '会话未提供当前工作目录'
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// 工作区目录来自会话 cwd;写盘前仍要过一遍路径守卫。
|
|
106
|
+
// 一个手工放置过 .fancy-deployed 的敏感目录(/etc 等)不能成为扫榜目标。
|
|
107
|
+
if (isSensitiveProjectPath(projectRoot) || !isValidProjectPath(projectRoot)) {
|
|
108
|
+
return notify(session, 'error', scan.notify({
|
|
109
|
+
type: 'no_project_root',
|
|
110
|
+
err: '工作区目录路径不合法或属于受保护目录'
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// 解开工作区目录的符号链接后再查一次:`/home/link -> /etc` 这种字符串
|
|
114
|
+
// 检查看不出来,但 run-scan 会跟着链接把 扫榜结果/ 写到 /etc。
|
|
115
|
+
try {
|
|
116
|
+
projectRoot = realpathSync(projectRoot);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return notify(session, 'error', scan.notify({
|
|
119
|
+
type: 'no_project_root',
|
|
120
|
+
err: `无法读取工作区目录:${e.message}`
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
if (isSensitiveProjectPath(projectRoot) || !isValidProjectPath(projectRoot)) {
|
|
124
|
+
return notify(session, 'error', scan.notify({
|
|
125
|
+
type: 'no_project_root',
|
|
126
|
+
err: '工作区目录的真实路径属于受保护目录'
|
|
78
127
|
}));
|
|
79
128
|
}
|
|
80
129
|
|
|
130
|
+
// 扫榜结果/ 是插件自己的产物目录;如果它是指向工作区外的符号链接,
|
|
131
|
+
// 备份/修复/写入都会落到外面。run-scan 会拒绝,这里有旧项目时也要拦。
|
|
132
|
+
try {
|
|
133
|
+
const scanDirPath = join(projectRoot, '扫榜结果');
|
|
134
|
+
if (existsSync(scanDirPath)) {
|
|
135
|
+
const st = lstatSync(scanDirPath);
|
|
136
|
+
if (st.isSymbolicLink() || !st.isDirectory()) {
|
|
137
|
+
return notify(session, 'error', scan.notify({
|
|
138
|
+
type: 'no_project_root',
|
|
139
|
+
err: '扫榜结果/ 不是真实目录,拒绝写入;请换成真实目录'
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch { /* 目录不存在或检查失败交给后续流程 */ }
|
|
144
|
+
|
|
81
145
|
// 体检 .fancy-deployed —— 必须在**扫榜之前**查。
|
|
82
146
|
//
|
|
83
147
|
// 之前只检查文件存不存在,于是文件损坏时:
|
|
@@ -159,6 +223,42 @@ export async function apply(ctx) {
|
|
|
159
223
|
}
|
|
160
224
|
const rankLabels = getRankLabels(rankIds, platform);
|
|
161
225
|
|
|
226
|
+
// ── 采集前先查:今天这个平台 + 榜单是不是已经分析过 ──────────
|
|
227
|
+
// 平台、榜单、日期在采集前就确定了,块 ID 可以提前算出来。
|
|
228
|
+
// 如果目标文件里已经有本次块,再跑几十分钟采集没有任何意义;
|
|
229
|
+
// 直接发一条通知,不启动采集脚本、不改动任何文件。
|
|
230
|
+
const scanDateForCheck = localDateStamp();
|
|
231
|
+
const topicFileForCheck = join(
|
|
232
|
+
projectRoot,
|
|
233
|
+
'扫榜结果',
|
|
234
|
+
`topic_decision_${scanDateForCheck}.md`
|
|
235
|
+
);
|
|
236
|
+
const blockIdForCheck = scan.makeBlockId(platform, rankLabels, scanDateForCheck);
|
|
237
|
+
const existing = inspectExistingBlock(topicFileForCheck, blockIdForCheck);
|
|
238
|
+
if (existing.status === EXISTING_BLOCK_STATUS.FOUND) {
|
|
239
|
+
// 这不是失败:插件已经正确判断"今天这个块分析过了",
|
|
240
|
+
// 没有启动采集、也没有改文件。用 success,避免命令面板把它
|
|
241
|
+
// 渲染成红色错误(用户会以为扫榜坏了)。
|
|
242
|
+
return notify(session, 'success', scan.notify({
|
|
243
|
+
type: 'already_scanned',
|
|
244
|
+
platform: scan.platformLabel(platform),
|
|
245
|
+
rankLabels,
|
|
246
|
+
date: scanDateForCheck,
|
|
247
|
+
topicFile: topicFileForCheck,
|
|
248
|
+
projectRoot,
|
|
249
|
+
blockId: blockIdForCheck,
|
|
250
|
+
closed: existing.blocks.length > 0 ? existing.blocks[0].closed : true,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
if (existing.status === EXISTING_BLOCK_STATUS.UNREADABLE) {
|
|
254
|
+
return notify(session, 'error', scan.notify({
|
|
255
|
+
type: 'existing_block_unreadable',
|
|
256
|
+
topicFile: topicFileForCheck,
|
|
257
|
+
projectRoot,
|
|
258
|
+
err: existing.error || '目标文件无法读取',
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
162
262
|
// 扫描方式选择(榜单选定之后)。
|
|
163
263
|
// 只有番茄有两条取数路径(明文接口 / 截图 OCR),其他平台 askScanMode
|
|
164
264
|
// 返回 null —— 此时不打扰用户,scanMode 留空表示"该平台无此选项"。
|
|
@@ -191,12 +291,31 @@ export async function apply(ctx) {
|
|
|
191
291
|
scanMode = parsedMode;
|
|
192
292
|
}
|
|
193
293
|
|
|
294
|
+
// ── 部署工作区内工具脚本 ──────────────────────────────────────
|
|
295
|
+
// 提示词里的预检/追加命令只能引用工作区内路径。脚本放在插件目录下时,
|
|
296
|
+
// 模型会去 node_modules 里执行:路径长、随安装位置变化、且那里是
|
|
297
|
+
// 包管理器管理的只读目录。部署失败就直接停 —— 没有脚本就没有
|
|
298
|
+
// 可信的校验与写入路径,继续采集只会产出一份没人能兜底的报告。
|
|
299
|
+
const deployed = deployScanTools(projectRoot);
|
|
300
|
+
if (!deployed.ok) {
|
|
301
|
+
return notify(session, 'error', scan.notify({
|
|
302
|
+
type: 'tool_deploy_failed',
|
|
303
|
+
err: deployed.problems.join(';'),
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
|
|
194
307
|
// 依次采集每个榜单(单个榜单失败不中断,失败清单在末尾汇总)
|
|
195
308
|
const allFiles = [];
|
|
196
309
|
let phaseBlocked = null;
|
|
197
310
|
const failedRanks = [];
|
|
311
|
+
const succeededRankIds = [];
|
|
312
|
+
const receiptDates = new Set();
|
|
198
313
|
let lastDate = '';
|
|
199
314
|
for (const rankId of rankIds) {
|
|
315
|
+
// 用户按停止后不要再启动下一个榜单的采集(每个榜单可能跑几十分钟)。
|
|
316
|
+
if (signal?.aborted) {
|
|
317
|
+
return notify(session, 'error', scan.notify({ type: 'cancelled' }));
|
|
318
|
+
}
|
|
200
319
|
const rankRow = RANK_LISTS[platform]?.find(r => r.id === rankId);
|
|
201
320
|
const rankLabel = rankRow ? rankRow.label : rankId;
|
|
202
321
|
|
|
@@ -232,8 +351,15 @@ export async function apply(ctx) {
|
|
|
232
351
|
}
|
|
233
352
|
|
|
234
353
|
lastDate = receipt.date || lastDate;
|
|
235
|
-
|
|
354
|
+
if (/^\d{8}$/.test(receipt.date || '')) receiptDates.add(receipt.date);
|
|
355
|
+
// receipt 来自子进程,不能假设字段一定是数组:字符串会被 spread 成字符。
|
|
356
|
+
const files = Array.isArray(receipt.scan_files) ? receipt.scan_files : [];
|
|
357
|
+
if (!files.length) {
|
|
358
|
+
failedRanks.push({ label: rankLabel, error: '采集回执里没有 scan_files 数组' });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
236
361
|
allFiles.push(...files);
|
|
362
|
+
if (files.length > 0) succeededRankIds.push(rankId);
|
|
237
363
|
// scraper 内部的部分失败(番茄某个题材采失败)必须透出:
|
|
238
364
|
// run-scan.js 对这种情况仍报 ok:true(只要有文件),
|
|
239
365
|
// 若这里不读 partialReasons,失败信息会在插件层被整个吞掉,
|
|
@@ -255,6 +381,13 @@ export async function apply(ctx) {
|
|
|
255
381
|
}
|
|
256
382
|
}
|
|
257
383
|
|
|
384
|
+
// 只把**真正采成功**的榜单写进提示词/块 ID。
|
|
385
|
+
// 之前把所有选中的 label 都传进去,某个榜失败时提示词会声称
|
|
386
|
+
// “它们都在本次数据文件里”,模型于是分析一个不存在的榜单。
|
|
387
|
+
const effectiveRankLabels = succeededRankIds.length
|
|
388
|
+
? getRankLabels(succeededRankIds, platform)
|
|
389
|
+
: rankLabels;
|
|
390
|
+
|
|
258
391
|
// 一个榜单都没采成功 → 硬失败
|
|
259
392
|
if (allFiles.length === 0) {
|
|
260
393
|
const detail = failedRanks.length
|
|
@@ -263,21 +396,149 @@ export async function apply(ctx) {
|
|
|
263
396
|
return notify(session, 'error', scan.notify({ type: 'scraper_failed', err: detail }));
|
|
264
397
|
}
|
|
265
398
|
|
|
399
|
+
// 跨午夜采集:每个榜单的 run-scan 在各自启动时取日期,可能出现
|
|
400
|
+
// 畅销榜 20261029、其它榜 20261030 的混合。此时不能拿最后一个
|
|
401
|
+
// receipt 的日期当整份日报的日期 —— 会把两天数据写进 10/30。
|
|
402
|
+
if (receiptDates.size > 1) {
|
|
403
|
+
return notify(session, 'error', scan.notify({
|
|
404
|
+
type: 'scan_date_mixed',
|
|
405
|
+
dates: [...receiptDates].sort(),
|
|
406
|
+
files: allFiles,
|
|
407
|
+
projectRoot,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (signal?.aborted) {
|
|
412
|
+
return notify(session, 'error', scan.notify({ type: 'cancelled' }));
|
|
413
|
+
}
|
|
414
|
+
|
|
266
415
|
// 日期口径必须与 run-scan.js 一致(本地时区)。此前 fallback 用
|
|
267
416
|
// toISOString()(UTC),凌晨 0-8 点会算出比采集时点早一天的文件名。
|
|
417
|
+
// receipt 里的日期必须再验一次格式;坏日期不能拼进路径/块 ID。
|
|
418
|
+
const scanDate = /^\d{8}$/.test(lastDate) ? lastDate : localDateStamp();
|
|
268
419
|
const topicFile = allFiles.length > 0
|
|
269
|
-
? join(projectRoot, '扫榜结果', `topic_decision_${
|
|
420
|
+
? join(projectRoot, '扫榜结果', `topic_decision_${scanDate}.md`)
|
|
270
421
|
: '';
|
|
271
422
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
423
|
+
// 2026-10-27:投递提示词之前,先把 topic_decision 的状态收拾干净。
|
|
424
|
+
//
|
|
425
|
+
// 写这个文件的是 LLM,而 LLM 的 write 工具是**覆盖**语义 ——
|
|
426
|
+
// 实测(tt5 项目)第二个平台分析时,模型先 write("") 把文件清空,
|
|
427
|
+
// 再 cat >> 追加自己的内容,第一个平台的分析就此消失,全程无报错。
|
|
428
|
+
// 提示词已经改成"只许 cat >> 追加、不许用 write",但提示词只是建议,
|
|
429
|
+
// 真正的兜底是备份 + 校验恢复。
|
|
430
|
+
//
|
|
431
|
+
// 顺序不能乱:**先修复 → 再备份 → 最后才把已有内容告诉模型**。
|
|
432
|
+
// 修复 —— 上一轮可能被覆盖过,先把丢掉的段落补回来;
|
|
433
|
+
// 备份 —— 存的必须是补全后的内容,不然备份也是残缺的;
|
|
434
|
+
// 告知 —— 模型知道文件里已经有什么,才知道自己只该追加。
|
|
435
|
+
let repairedKeys = [];
|
|
436
|
+
try {
|
|
437
|
+
const repair = repairTopicDecision(topicFile);
|
|
438
|
+
if (repair.repaired.length) {
|
|
439
|
+
repairedKeys = repair.repaired;
|
|
440
|
+
console.error(`[fancy-scan] 上一次分析把 topic_decision 覆盖了,已从备份补回 ${repairedKeys.length} 个内容块:${repairedKeys.join('、')}`);
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error('[fancy-scan] 校验 topic_decision 失败:' + (e && e.message ? e.message : String(e)));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const backup = backupTopicDecision(topicFile);
|
|
447
|
+
if (backup.backupPath) {
|
|
448
|
+
console.error(`[fancy-scan] 已备份 topic_decision → ${backup.backupPath}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 文件末尾没有换行时,新追加的 start 标记会粘在旧行尾 ——
|
|
452
|
+
// grep 的 '^# …-start$' 就匹配不到了,模型会误判"没写进去"而重写,
|
|
453
|
+
// 于是同一个平台出现两个 start(写重了)。
|
|
454
|
+
// 这件事模型看不见,插件自己能看出来,所以在派发前补上。
|
|
455
|
+
try {
|
|
456
|
+
const nl = ensureTrailingNewline(topicFile);
|
|
457
|
+
if (nl.fixed) {
|
|
458
|
+
console.error('[fancy-scan] topic_decision 末尾缺换行,已补上(否则新块会粘在旧行尾)');
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
console.error('[fancy-scan] 补末尾换行失败:' + (e && e.message ? e.message : String(e)));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 查文件里"上次写歪"的痕迹:同一个平台写了两遍、或把校验命令写进了正文。
|
|
465
|
+
//
|
|
466
|
+
// 模型自己看不见自己写进去的东西(写完就交差了),指望它"下次注意"没用。
|
|
467
|
+
// 插件查出来,写进提示词(让模型知道现状、别再重蹈覆辙),
|
|
468
|
+
// 同时通知用户。
|
|
469
|
+
//
|
|
470
|
+
// **重复的块直接收敛**(保留最后一个,与 topic 侧"后覆盖前"同向):
|
|
471
|
+
// 重复内容对 topic 是有害的(同一平台两份,用户看到的是混乱),
|
|
472
|
+
// 而收敛前一定先备份,删错了能从 .revisions/ 找回来。
|
|
473
|
+
// 命令痕迹只报告不删 —— 删正文行有误伤风险,交给用户自己看。
|
|
474
|
+
let dirt = null;
|
|
475
|
+
let fileWarnings = [];
|
|
476
|
+
let deduped = null;
|
|
477
|
+
try {
|
|
478
|
+
if (existsSync(topicFile)) {
|
|
479
|
+
let found = inspectTopicDecision(readFileSync(topicFile, 'utf-8'));
|
|
480
|
+
if (found.duplicateKeys.length) {
|
|
481
|
+
const dd = backupTopicDecision(topicFile);
|
|
482
|
+
// 备份失败就不删任何重复块:删了找不回来,宁可把重复留给用户处理。
|
|
483
|
+
if (!dd.backupPath) {
|
|
484
|
+
fileWarnings.push('检测到重复块,但备份失败;为避免丢数据,本次不自动收敛,请人工检查');
|
|
485
|
+
} else {
|
|
486
|
+
const res = dedupeMarkerBlocks(topicFile);
|
|
487
|
+
if (res.ok && res.removed.length) {
|
|
488
|
+
deduped = {
|
|
489
|
+
removed: res.removed,
|
|
490
|
+
backupPath: dd.backupPath,
|
|
491
|
+
kept: res.kept,
|
|
492
|
+
};
|
|
493
|
+
console.error(`[fancy-scan] topic_decision 收敛了 ${res.removed.length} 个重复块`);
|
|
494
|
+
// 收敛后重新查一遍,剩下的才是真正要提醒的
|
|
495
|
+
found = inspectTopicDecision(readFileSync(topicFile, 'utf-8'));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (found.duplicateKeys.length || found.commandLines.length || found.unclosedBlocks.length) {
|
|
500
|
+
const lines = [];
|
|
501
|
+
for (const d of found.duplicateKeys) {
|
|
502
|
+
const msg = `「${d.key}」这个平台在文件里有 **${d.count} 个块**(上次追加写重了)`;
|
|
503
|
+
lines.push('- ' + msg);
|
|
504
|
+
fileWarnings.push(msg);
|
|
505
|
+
}
|
|
506
|
+
for (const c of found.commandLines) {
|
|
507
|
+
const msg = `「${c.key}」的块里混进了命令:\`${c.text}\``;
|
|
508
|
+
lines.push('- ' + msg);
|
|
509
|
+
fileWarnings.push(msg);
|
|
510
|
+
}
|
|
511
|
+
for (const u of found.unclosedBlocks) {
|
|
512
|
+
const msg = `「${u.key}」这个块**只有 start 标记、没有 end 标记**(上次没写完,内容被截断了)`;
|
|
513
|
+
lines.push('- ' + msg);
|
|
514
|
+
fileWarnings.push(msg);
|
|
515
|
+
}
|
|
516
|
+
dirt = { lines, deduped };
|
|
517
|
+
console.error(`[fancy-scan] topic_decision 有写歪痕迹:${lines.length} 条`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
dirt = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 把文件里已有的标记块告诉模型,让它知道"里面已经有什么",
|
|
525
|
+
// 自己只该追加、不该重写。
|
|
526
|
+
//
|
|
527
|
+
// **必须在收敛之后读**(2026-10-29 审计):之前这段在 dedupe 之前,
|
|
528
|
+
// 于是刚被删掉的重复块还留在列表里 ——
|
|
529
|
+
// 提示词告诉模型"文件里已经有这个块,一个字都不许动",
|
|
530
|
+
// 而那个块刚被插件删了。
|
|
531
|
+
let existingBlocks = [];
|
|
532
|
+
try {
|
|
533
|
+
if (existsSync(topicFile)) {
|
|
534
|
+
existingBlocks = listMarkerBlocks(readFileSync(topicFile, 'utf-8'));
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
existingBlocks = [];
|
|
538
|
+
}
|
|
539
|
+
if (existingBlocks.length) {
|
|
540
|
+
console.error(`[fancy-scan] topic_decision 已有 ${existingBlocks.length} 个内容块,提示词已告知模型只追加`);
|
|
541
|
+
}
|
|
281
542
|
|
|
282
543
|
// phase 推不动就别再触发 LLM 分析了 —— 分析产物写进 topic_decision 后,
|
|
283
544
|
// /fancy-topic 依然会因为 phase 不对而跑不了,纯属浪费。
|
|
@@ -294,14 +555,61 @@ export async function apply(ctx) {
|
|
|
294
555
|
}
|
|
295
556
|
|
|
296
557
|
// 触发 LLM 分析
|
|
297
|
-
if (allFiles.length > 0 &&
|
|
558
|
+
if (allFiles.length > 0 && effectiveRankLabels) {
|
|
559
|
+
// 本次块的固定参数只算一次:块 ID、正文路径、指标与开关。
|
|
560
|
+
// 这些值同时进 manifest、提示词与回合结束后的复核 —— 三处必须完全一致。
|
|
561
|
+
const ds = scan.dataShape(platform);
|
|
562
|
+
const multiList = effectiveRankLabels === '全选'
|
|
563
|
+
|| effectiveRankLabels.split('、').filter(Boolean).length > 1;
|
|
564
|
+
const metricLabel = scan.heatMetric(platform).label;
|
|
565
|
+
const blockId = scan.makeBlockId(platform, effectiveRankLabels, scanDate);
|
|
566
|
+
mkdirSync(join(projectRoot, '.tmp'), { recursive: true });
|
|
567
|
+
const bodyFile = scan.bodyFilePath(projectRoot, blockId, topicFile || projectRoot || '');
|
|
568
|
+
// 上一次运行可能留下同名的临时正文(同一块 ID)。先删掉:
|
|
569
|
+
// 模型如果跳过 5.1 直接跑 5.2,脚本会因为正文文件不存在而拒绝,
|
|
570
|
+
// 不会把上一轮的旧正文再追加一遍。
|
|
571
|
+
if (bodyFile.includes('fancy_scan_body_')) {
|
|
572
|
+
try { unlinkSync(bodyFile); } catch { /* 不存在 */ }
|
|
573
|
+
}
|
|
574
|
+
const manifest = writeScanManifest(projectRoot, {
|
|
575
|
+
topicFile,
|
|
576
|
+
blockId,
|
|
577
|
+
dataFiles: allFiles,
|
|
578
|
+
bodyFile,
|
|
579
|
+
metricLabel,
|
|
580
|
+
grouped: ds.grouped,
|
|
581
|
+
multiList,
|
|
582
|
+
rankLabels: (RANK_LISTS[platform] || []).map(r => r.label),
|
|
583
|
+
});
|
|
584
|
+
if (!manifest.ok) {
|
|
585
|
+
return notify(session, 'error', scan.notify({
|
|
586
|
+
type: 'manifest_failed',
|
|
587
|
+
err: manifest.problems.join(';'),
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 写之前把文件状态记下来 —— 模型回合结束后要核对
|
|
592
|
+
// "旧内容有没有被动过"。模型看不见自己写进去的东西,
|
|
593
|
+
// 这一层只有插件能查。
|
|
594
|
+
let preWrite = null;
|
|
595
|
+
try {
|
|
596
|
+
preWrite = captureFileState(topicFile);
|
|
597
|
+
} catch {
|
|
598
|
+
preWrite = null;
|
|
599
|
+
}
|
|
600
|
+
|
|
298
601
|
const follow = await llmFollowup(invocation, scan.llmAnalysisPrompt({
|
|
299
602
|
platform,
|
|
300
|
-
rankLists:
|
|
301
|
-
date:
|
|
603
|
+
rankLists: effectiveRankLabels ? effectiveRankLabels.split('、') : ['全选'],
|
|
604
|
+
date: scanDate,
|
|
302
605
|
files: allFiles,
|
|
303
606
|
topicFile,
|
|
304
607
|
projectRoot,
|
|
608
|
+
existingBlocks,
|
|
609
|
+
fileWarnings,
|
|
610
|
+
failedRanks,
|
|
611
|
+
appendScript: deployed.appendScript,
|
|
612
|
+
manifestPath: manifest.path,
|
|
305
613
|
}));
|
|
306
614
|
// llmFollowup 内部已捕获异常并返回 { ok, error },必须检查返回值,
|
|
307
615
|
// 否则投递失败也会报"采集完成,正在将数据交给 LLM 进行分析"
|
|
@@ -311,9 +619,90 @@ export async function apply(ctx) {
|
|
|
311
619
|
err: (follow && follow.error) || '提示词投递失败'
|
|
312
620
|
}));
|
|
313
621
|
}
|
|
622
|
+
|
|
623
|
+
// llmFollowup 只代表“消息已入队”,模型是异步跑的。
|
|
624
|
+
// 必须等这一轮真正结束再复核;否则会在模型还没开始写的时候
|
|
625
|
+
// 立刻报“目标文件不存在”,而几秒后模型又正常写进去了。
|
|
626
|
+
let waited = await waitForAnalysisTurn(invocation, { signal });
|
|
627
|
+
if (waited && waited.code === 'ABORTED') {
|
|
628
|
+
return notify(session, 'error', scan.notify({ type: 'cancelled' }));
|
|
629
|
+
}
|
|
630
|
+
// DSH 在 TRANSPORT 错误后会自动给 agent 发“继续”,模型往往在
|
|
631
|
+
// 下一轮才真正写完。第一轮 wait 已经返回错误时,不要立刻判定
|
|
632
|
+
// 失败;只对明确的传输错误再等一轮,给自动重试一个机会。
|
|
633
|
+
if (waited && waited.code === 'TURN_ERROR'
|
|
634
|
+
&& (waited.providerCode === 'TRANSPORT'
|
|
635
|
+
|| /TRANSPORT|Connection error/i.test(waited.reason || ''))) {
|
|
636
|
+
waited = await waitForAnalysisTurn(invocation, {
|
|
637
|
+
signal,
|
|
638
|
+
timeoutMs: 10 * 60 * 1000,
|
|
639
|
+
// 实测 DSH 的自动“继续”在本轮报错后约 2 分钟才启动;
|
|
640
|
+
// 默认 120 秒的 idle 宽限只差 3 秒就会误判超时。
|
|
641
|
+
idleGraceMs: 5 * 60 * 1000,
|
|
642
|
+
});
|
|
643
|
+
if (waited && waited.code === 'ABORTED') {
|
|
644
|
+
return notify(session, 'error', scan.notify({ type: 'cancelled' }));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── 复核:这一次到底写进去没有、写对没有 ──────────────────
|
|
649
|
+
// 提示词里唯一的写入路径是 append-block.js(它自己会校验),
|
|
650
|
+
// 但模型**可能绕过脚本**。所以插件自己再查一遍:
|
|
651
|
+
// 块在不在、闭合没闭合、正文过不过机械校验、旧内容有没有被动过。
|
|
652
|
+
// 查出来不对就告诉用户 —— 不能让"没写进去"被当成"写成功了"。
|
|
653
|
+
try {
|
|
654
|
+
const vres = verifyBlockWritten(
|
|
655
|
+
topicFile,
|
|
656
|
+
blockId,
|
|
657
|
+
{
|
|
658
|
+
metricLabel,
|
|
659
|
+
grouped: ds.grouped,
|
|
660
|
+
multiList,
|
|
661
|
+
dataFiles: allFiles,
|
|
662
|
+
knownLists: (RANK_LISTS[platform] || []).map(r => r.label),
|
|
663
|
+
},
|
|
664
|
+
preWrite
|
|
665
|
+
);
|
|
666
|
+
if (!vres.ok) {
|
|
667
|
+
console.error('[fancy-scan] 写入复核不通过:' + vres.problems.join(';'));
|
|
668
|
+
const waitNote = (waited && waited.ok !== true && waited.code !== 'TURN_ENDED')
|
|
669
|
+
? `(等待 LLM 回合:${waited.reason || waited.code})`
|
|
670
|
+
: '';
|
|
671
|
+
return notify(session, 'error', scan.notify({
|
|
672
|
+
type: 'write_verify_failed',
|
|
673
|
+
err: `本次分析没有正确写入 ${topicFile}:${vres.problems.join(';')}${waitNote}`,
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
} catch (e) {
|
|
677
|
+
// 复核本身出错(文件读不了 / 解析异常)不能静默放过:
|
|
678
|
+
// 否则用户会收到“成功”,但没人知道报告到底写没写对。
|
|
679
|
+
const msg = e && e.message ? e.message : String(e);
|
|
680
|
+
console.error('[fancy-scan] 写入复核出错:' + msg);
|
|
681
|
+
return notify(session, 'error', scan.notify({
|
|
682
|
+
type: 'write_verify_failed',
|
|
683
|
+
err: `写入复核本身失败:${msg}`,
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
314
686
|
}
|
|
315
687
|
|
|
316
|
-
|
|
688
|
+
// 只有走到这里才是真的完成:采集成功 + LLM 回合结束 + 写入复核通过。
|
|
689
|
+
// 不能返回投递前的 handover 文案 —— 那会显示“正在将数据交给 LLM
|
|
690
|
+
// 进行分析”,而实际上分析已经做完了(真实日志 91f7ae4e 的第三条
|
|
691
|
+
// 命令就是这样:turn/end 之后仍显示“正在分析”)。
|
|
692
|
+
const doneText = scan.notify({
|
|
693
|
+
type: 'analysis_done',
|
|
694
|
+
platform: scan.platformLabel(platform),
|
|
695
|
+
rankLabels: effectiveRankLabels,
|
|
696
|
+
date: scanDate,
|
|
697
|
+
files: allFiles,
|
|
698
|
+
topicFile,
|
|
699
|
+
projectRoot,
|
|
700
|
+
failedRanks,
|
|
701
|
+
scanMode,
|
|
702
|
+
repaired: repairedKeys,
|
|
703
|
+
});
|
|
704
|
+
const dirtText = dirt ? '\n\n' + scan.notify({ type: 'dirty_file', dirt }) : '';
|
|
705
|
+
return notify(session, 'success', doneText + dirtText);
|
|
317
706
|
}
|
|
318
707
|
});
|
|
319
708
|
}
|