@cloud411716/fancy-webnovel 1.0.46 → 2.2.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 +183 -0
- package/cordis.patch.yml +0 -14
- package/index.js +21 -37
- package/lib/chinese-num.js +111 -0
- package/lib/deployed-state.js +141 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +569 -0
- package/lib/llm-wiki.js +109 -0
- package/lib/safe-rm.js +94 -0
- package/lib/scraper-utils.js +165 -0
- package/package.json +37 -5
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +973 -0
- package/plugins/fancy-analyze/schema-analyze.md +379 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +431 -0
- package/plugins/fancy-analyze/templates.js +881 -0
- package/plugins/fancy-bootstrap/index.js +168 -0
- package/plugins/fancy-bootstrap/scripts/init.js +143 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +54 -0
- package/plugins/fancy-import/import-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +581 -0
- package/plugins/fancy-import/index.js +730 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +106 -0
- package/plugins/fancy-import/templates.js +49 -0
- package/plugins/fancy-scan/index.js +319 -0
- package/plugins/fancy-scan/scripts/run-scan.js +520 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +794 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +339 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +611 -0
- package/plugins/fancy-scan/templates.js +295 -0
- package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +155 -0
- package/plugins/fancy-story-builder/index.js +3152 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +553 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +540 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +504 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +188 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +107 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -0
- package/plugins/fancy-story-builder/references/glossary.md +585 -0
- package/plugins/fancy-story-builder/references/index.md +490 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +163 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +239 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +177 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +208 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +142 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.md +324 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +413 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1189 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1385 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1216 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +377 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1832 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1371 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +417 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -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 +204 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
- 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 +66 -0
- 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 +76 -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 +72 -0
- 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 +37 -0
- 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 +53 -0
- 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 +40 -0
- 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 +79 -0
- 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 +132 -0
- 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 +133 -0
- 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 +160 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
- 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 +134 -0
- package/plugins/fancy-story-builder/templates.js +1248 -0
- package/plugins/fancy-story-builder/wiki-references.js +204 -0
- package/plugins/fancy-story-builder/writing-driver.js +1061 -0
- package/plugins/fancy-topic/index.js +704 -0
- package/plugins/fancy-topic/scripts/select-topic.js +274 -0
- package/plugins/fancy-topic/templates.js +373 -0
- package/commands/bootstrap/index.js +0 -164
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -283
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
package/client.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fancy-webnovel 浏览器半边。
|
|
3
|
+
*
|
|
4
|
+
* 目标:新会话里执行 /fancy-* 时,
|
|
5
|
+
* 1) **看得见**初始化成功/失败的结果;
|
|
6
|
+
* 2) **LLM 完全不被触发**(不发 provider 请求、零额度、零噪音);
|
|
7
|
+
* 3) **会话日志保持干净**(插件一条都不写),agent 之后照常能跑。
|
|
8
|
+
*
|
|
9
|
+
* ── 为什么必须放在浏览器半边 ──────────────────────────────────────────────
|
|
10
|
+
* DSH 的聊天区有一个硬门槛(dsh-client-ui-chat/lib/client.js:5385):
|
|
11
|
+
*
|
|
12
|
+
* isActive: (snapshot) => snapshot.order.some((key) => snapshot.nodes.get(key)?.kind !== "command")
|
|
13
|
+
*
|
|
14
|
+
* 只有 kind !== "command" 的节点才会点亮聊天区。而命令结果在 DSH 里**就是**
|
|
15
|
+
* 一个 kind === "command" 的 flow 节点(dsh-client-ui-chat/lib/client.js:5514),
|
|
16
|
+
* 所以「插件不碰会话」= 聊天区永远不亮 = 用户什么都看不到。这就是「彻底本地」
|
|
17
|
+
* 方案让人眼前一空的真正原因,不是渲染坏了,是**门就没开**。
|
|
18
|
+
*
|
|
19
|
+
* 而宿主侧点亮只有一条路:在会话里写一条 user/message 占住 surface 节点 0。
|
|
20
|
+
* DSH 规定 surface 第一个节点必须是 agent 的 system/message(protected first
|
|
21
|
+
* surface head),插件一占,agent 之后一跑整个会话就读不回来 —— 磁盘上已经
|
|
22
|
+
* 有 160 个这样的坏会话。所以宿主侧的路是死的。
|
|
23
|
+
*
|
|
24
|
+
* ── 浏览器半边怎么开这扇门 ────────────────────────────────────────────────
|
|
25
|
+
* 1) package.json 声明 dsh.client + exports["./client"]
|
|
26
|
+
* → dsh-client-modules/lib/index.js:479 扫 host Loader entries 找 dsh.client
|
|
27
|
+
* → dsh-client-modules/lib/index.js:630-636 要求 platform === "web" 且有
|
|
28
|
+
* exports["./client"];扫到的包进 window.__DSH_BOOT__,浏览器加载它的 client bundle。
|
|
29
|
+
* 这是 DSH 自带的「双面包」机制(官方示范:dsh-client-ui-goal 的 host 半边
|
|
30
|
+
* 就是空的 `function apply() {}`),**不需要改 harness 一行代码**。
|
|
31
|
+
*
|
|
32
|
+
* 2) 注册一个自己的 conversation-node Definition(kind = "fancy-notice"),
|
|
33
|
+
* 匹配 command/run + command/done,只对 fancy-* 命令产出节点。
|
|
34
|
+
* 它的 kind 不是 "command" → 聊天区被点亮 → 结果可见。
|
|
35
|
+
* 节点本身**不渲染任何内容**(display:none):结果由 DSH 原生的命令卡片
|
|
36
|
+
* (GenericCommandCard)渲染,本来就完整,不需要我们再画一遍。
|
|
37
|
+
*
|
|
38
|
+
* 3) 空白会话里聊天区不挂载 → chat target 永远不会自动激活 →
|
|
39
|
+
* conversationPhase(dsh-client-ui-conversation/lib/client.js:14011)
|
|
40
|
+
* 恒为 "blank" → 死循环。所以这里主动订阅当前会话的 chat target,
|
|
41
|
+
* 强制 assembler 激活它(dsh-client-ui-conversation/lib/client.js:794)。
|
|
42
|
+
*
|
|
43
|
+
* ── 铁律 ──────────────────────────────────────────────────────────────────
|
|
44
|
+
* 浏览器半边**只读**会话事件,一个字都不往会话里写。
|
|
45
|
+
* 会话的写入权仍然只有宿主插件,插件也仍然只写命令自己的结果(command/done)。
|
|
46
|
+
*/
|
|
47
|
+
window.__ModuleLoader__.load({
|
|
48
|
+
id: "@cloud411716/fancy-webnovel",
|
|
49
|
+
factory: (require) => {
|
|
50
|
+
var module = { exports: {} };
|
|
51
|
+
var exports = module.exports;
|
|
52
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
53
|
+
|
|
54
|
+
var react = require("react");
|
|
55
|
+
|
|
56
|
+
/** 我们自己的 conversation-node kind(非 "command",专用于点亮聊天区)。 */
|
|
57
|
+
var NOTICE_KIND = "fancy-notice";
|
|
58
|
+
/** 只对这些命令产出节点,别人的命令一概不碰。 */
|
|
59
|
+
var FANCY_COMMANDS = [
|
|
60
|
+
"fancy-bootstrap",
|
|
61
|
+
"fancy-analyze",
|
|
62
|
+
"fancy-import",
|
|
63
|
+
"fancy-scan",
|
|
64
|
+
"fancy-story-builder",
|
|
65
|
+
"fancy-topic"
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** 复刻 dsh-client-ui-chat 的 contextLocation(那边是包内私有函数)。 */
|
|
69
|
+
function contextLocation(context) {
|
|
70
|
+
return context.start?.location ?? context.matches[0]?.location ?? { kind: "unresolved" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 把 command/run + command/done 折算成一条 fancy 通知。 */
|
|
74
|
+
var noticeDefinition = {
|
|
75
|
+
kind: NOTICE_KIND,
|
|
76
|
+
target: "chat",
|
|
77
|
+
match: function (event) {
|
|
78
|
+
if (event.type === "command/run") return { id: String(event.data.commandId), role: "start" };
|
|
79
|
+
if (event.type === "command/done") return { id: String(event.data.commandId), role: "update" };
|
|
80
|
+
return null;
|
|
81
|
+
},
|
|
82
|
+
start: function (_context, match) {
|
|
83
|
+
return {
|
|
84
|
+
name: match.event.data.name ?? "",
|
|
85
|
+
args: match.event.data.args ?? null,
|
|
86
|
+
outcome: null
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
update: function (context, match) {
|
|
90
|
+
if (match.event.type !== "command/done") return context.state;
|
|
91
|
+
return {
|
|
92
|
+
name: context.state?.name ?? "",
|
|
93
|
+
args: context.state?.args ?? null,
|
|
94
|
+
outcome: {
|
|
95
|
+
kind: match.event.data.kind,
|
|
96
|
+
text: match.event.data.text ?? ""
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
buildViewNode: function (context) {
|
|
101
|
+
var state = context.state;
|
|
102
|
+
if (state === undefined) return null;
|
|
103
|
+
if (FANCY_COMMANDS.indexOf(state.name) === -1) return null;
|
|
104
|
+
return {
|
|
105
|
+
key: context.key,
|
|
106
|
+
kind: NOTICE_KIND,
|
|
107
|
+
id: context.id,
|
|
108
|
+
target: "chat",
|
|
109
|
+
anchorSeq: context.startSeq ?? context.matches[0].event.seq,
|
|
110
|
+
location: contextLocation(context),
|
|
111
|
+
visibility: "visible",
|
|
112
|
+
data: state
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 点亮用的隐形节点。真正的可见内容由 DSH 原生命令卡片渲染,
|
|
119
|
+
* 这里只留一个 data 钩子,方便自动化测试断言。
|
|
120
|
+
*/
|
|
121
|
+
function FancyNoticeNodeView(props) {
|
|
122
|
+
var data = props?.node?.data ?? {};
|
|
123
|
+
return react.createElement("div", {
|
|
124
|
+
"data-fancy-notice": data.name ?? "",
|
|
125
|
+
"data-fancy-notice-kind": data.outcome?.kind ?? "",
|
|
126
|
+
style: { display: "none" }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 客户端插件体。
|
|
132
|
+
* @param ctx - client root context.
|
|
133
|
+
*/
|
|
134
|
+
function apply(ctx) {
|
|
135
|
+
ctx.uiConversation.events.register(noticeDefinition);
|
|
136
|
+
|
|
137
|
+
// 注册我们这个 kind 的渲染器。不注册的话 DSH 会走 JsonBlock 兜底,
|
|
138
|
+
// 聊天区里会冒出一行「未知 surface 事件:fancy-notice」。
|
|
139
|
+
ctx.slots.inject("conversation.chat.node", function () {
|
|
140
|
+
return ctx.slots.register({
|
|
141
|
+
name: "conversation.chat.node",
|
|
142
|
+
key: NOTICE_KIND
|
|
143
|
+
}, FancyNoticeNodeView);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* 强制激活当前会话的 chat target。
|
|
148
|
+
*
|
|
149
|
+
* 空白会话里聊天区不挂载 → chat target 不会被激活 →
|
|
150
|
+
* conversation.activeTargets 为空 → conversationPhase 恒为 "blank" →
|
|
151
|
+
* ConversationSession 直接 return null → 聊天区永远不出现。
|
|
152
|
+
* 这里订阅一次 chat target,逼 assembler 把它激活
|
|
153
|
+
* (dsh-client-ui-conversation/lib/index.js:794 activateTarget)。
|
|
154
|
+
*/
|
|
155
|
+
var disposeCurrent = null;
|
|
156
|
+
ctx.effect(function () {
|
|
157
|
+
var sessions = ctx.sessions;
|
|
158
|
+
return sessions.list.subscribe(function () {
|
|
159
|
+
var current = sessions.list.getSnapshot().current;
|
|
160
|
+
if (typeof current !== "string" || current.length === 0) return;
|
|
161
|
+
if (disposeCurrent !== null) {
|
|
162
|
+
try {
|
|
163
|
+
disposeCurrent();
|
|
164
|
+
} catch {}
|
|
165
|
+
disposeCurrent = null;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
var binding = ctx.uiConversation.binding(current);
|
|
169
|
+
if (binding === undefined) return;
|
|
170
|
+
disposeCurrent = binding.target("chat").subscribe(function () {});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
disposeCurrent = null;
|
|
173
|
+
ctx.logger.warn("fancy-webnovel: chat target activation failed: %s", String(error));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
exports.apply = apply;
|
|
180
|
+
exports.inject = ["uiConversation", "sessions", "slots"];
|
|
181
|
+
return module.exports;
|
|
182
|
+
}
|
|
183
|
+
});
|
package/cordis.patch.yml
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
# @cloud411716/fancy-webnovel — DSH Cordis Plugin Patch
|
|
2
|
-
#
|
|
3
|
-
# IMPORTANT: This plugin creates project directories at user-specified paths
|
|
4
|
-
# (which may be OUTSIDE the DSH workspace). Therefore it REQUIRES
|
|
5
|
-
# `danger-full-access` sandbox mode. Add this to your profile's patch, e.g.:
|
|
6
|
-
#
|
|
7
|
-
# - insert:
|
|
8
|
-
# - id: sandbox-policy
|
|
9
|
-
# config:
|
|
10
|
-
# mode: danger-full-access
|
|
11
|
-
#
|
|
12
|
-
# All other services used (commands, userQuestions, timer, events, session)
|
|
13
|
-
# are part of dsh-base and available in every DSH UI (tui, web, desktop).
|
|
14
|
-
|
|
15
1
|
- insert:
|
|
16
2
|
- id: fancy-webnovel
|
|
17
3
|
name: "@cloud411716/fancy-webnovel"
|
package/index.js
CHANGED
|
@@ -1,41 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* - All scrapers run IN-PROCESS (no subprocess).
|
|
10
|
-
* DSH manages the lifecycle via ctx.effect() and AbortSignal.
|
|
11
|
-
* - HTTP fetching uses Node.js native fetch (portable across all DSH UIs).
|
|
12
|
-
* - File operations use Node.js fs (project roots may be outside workspace).
|
|
13
|
-
* - ctx.timer (via ctx.timeout()) handles all delays/timeouts.
|
|
14
|
-
*
|
|
15
|
-
* Required Cordis services: commands, userQuestions, timer
|
|
16
|
-
* (all are part of dsh-base, so this plugin works on dsh-tui, dsh-web, dsh-desktop).
|
|
17
|
-
*
|
|
18
|
-
* IMPORTANT: This plugin needs `danger-full-access` sandbox mode because it
|
|
19
|
-
* creates project directories outside the DSH workspace (at paths the user
|
|
20
|
-
* specifies). Declare this in the profile's cordis.patch.yml.
|
|
21
|
-
*/
|
|
1
|
+
// @cloud411716/fancy-webnovel unified entry point
|
|
2
|
+
import { apply as applyBootstrap } from './plugins/fancy-bootstrap/index.js';
|
|
3
|
+
import { apply as applyScan } from './plugins/fancy-scan/index.js';
|
|
4
|
+
import { apply as applyTopic } from './plugins/fancy-topic/index.js';
|
|
5
|
+
import { apply as applyAnalyze } from './plugins/fancy-analyze/index.js';
|
|
6
|
+
import { apply as applyStoryBuilder } from './plugins/fancy-story-builder/index.js';
|
|
7
|
+
import { apply as applyImport } from './plugins/fancy-import/index.js';
|
|
8
|
+
import { registerSafeRmPrompt } from './lib/safe-rm.js';
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
import { apply as applyScan } from './commands/scan/index.js';
|
|
10
|
+
export const inject = ['commands', 'userQuestions'];
|
|
25
11
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
12
|
+
export async function apply(ctx) {
|
|
13
|
+
// bundle 级提示:沙箱下 rm 会挂死(见 lib/safe-rm.js)。
|
|
14
|
+
// 放在这里而不是各子插件里,是因为每个子插件都可能让模型删临时文件,
|
|
15
|
+
// 逐个加必然漏 —— 一次注册对整个 bundle 的所有会话生效。
|
|
16
|
+
registerSafeRmPrompt(ctx);
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
applyBootstrap(ctx);
|
|
40
|
-
applyScan(ctx);
|
|
18
|
+
await applyBootstrap(ctx);
|
|
19
|
+
await applyScan(ctx);
|
|
20
|
+
await applyTopic(ctx);
|
|
21
|
+
await applyAnalyze(ctx);
|
|
22
|
+
await applyStoryBuilder(ctx);
|
|
23
|
+
// 导入放在最后:它要用 story-builder 的模板与校验器
|
|
24
|
+
await applyImport(ctx);
|
|
41
25
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chinese-num.js —— 中文数字 ↔ 阿拉伯数字(插件共用)
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块:
|
|
5
|
+
* 导入阶段要读旧稿里的"第十二章",去味阶段要判断"# 第十二章"与
|
|
6
|
+
* "# 第12章"是不是同一章。两边各写一份转换函数的话,
|
|
7
|
+
* 遇到"二十三""两百"这类写法就会一边认一边不认。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 中文数字字符 → 值 */
|
|
11
|
+
const CN_DIGITS = {
|
|
12
|
+
零: 0, 〇: 0, 一: 1, 二: 2, 两: 2, 三: 3, 四: 4,
|
|
13
|
+
五: 5, 六: 6, 七: 7, 八: 8, 九: 9
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 中文数字转阿拉伯数字。
|
|
18
|
+
*
|
|
19
|
+
* 支持:一 ~ 九十九、一百、一百零八、**两千、三千、一千零一**、
|
|
20
|
+
* 一万、十二万、**一亿**。
|
|
21
|
+
*
|
|
22
|
+
* 2026-10-27 扩展到千/万/亿:之前只认到"百",
|
|
23
|
+
* 于是"第三千章""第十二万章"这类写法一律返回 null ——
|
|
24
|
+
* 而 split_source.js 还**自己**写了一份更弱的转换(只到千,且没有"两"),
|
|
25
|
+
* 同一本书在不同插件里被切成了不同的章号。
|
|
26
|
+
* 现在全仓库统一走这一个函数。
|
|
27
|
+
*
|
|
28
|
+
* @param {string} s 中文数字串(纯数字直接转)
|
|
29
|
+
* @returns {number|null} 认不出来返回 null
|
|
30
|
+
*/
|
|
31
|
+
export function cnToNumber(s) {
|
|
32
|
+
const str = String(s || '').trim();
|
|
33
|
+
if (!str) return null;
|
|
34
|
+
if (/^\d+$/.test(str)) return Number(str);
|
|
35
|
+
|
|
36
|
+
let total = 0; // 已完成的万/亿级部分
|
|
37
|
+
let section = 0; // 当前小节(万以下)
|
|
38
|
+
let num = 0; // 当前数字
|
|
39
|
+
let seen = false;
|
|
40
|
+
let pendingZero = false;
|
|
41
|
+
|
|
42
|
+
for (const ch of str) {
|
|
43
|
+
if (ch in CN_DIGITS) {
|
|
44
|
+
if (ch === '零' || ch === '〇') {
|
|
45
|
+
pendingZero = true;
|
|
46
|
+
num = 0;
|
|
47
|
+
} else {
|
|
48
|
+
num = CN_DIGITS[ch];
|
|
49
|
+
pendingZero = false;
|
|
50
|
+
}
|
|
51
|
+
seen = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === '十' || ch === '百' || ch === '千') {
|
|
55
|
+
const unit = ch === '十' ? 10 : (ch === '百' ? 100 : 1000);
|
|
56
|
+
// 前面没有数字时按 1 处理(十二 = 12、两千 = 2000)
|
|
57
|
+
section += (num === 0 && !pendingZero ? 1 : num) * unit;
|
|
58
|
+
num = 0;
|
|
59
|
+
pendingZero = false;
|
|
60
|
+
seen = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === '万' || ch === '亿') {
|
|
64
|
+
const unit = ch === '万' ? 10000 : 100000000;
|
|
65
|
+
// 先收拢当前小节,再整体乘单位(十二万 = 12 * 10000)
|
|
66
|
+
section += num;
|
|
67
|
+
total = (total + section) * unit;
|
|
68
|
+
section = 0;
|
|
69
|
+
num = 0;
|
|
70
|
+
pendingZero = false;
|
|
71
|
+
seen = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
section += num;
|
|
77
|
+
const result = total + section;
|
|
78
|
+
return seen && result > 0 ? result : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 从一行文本里抠出"第 N 章"里的 N(阿拉伯或中文都行)。
|
|
83
|
+
*
|
|
84
|
+
* @param {string} line
|
|
85
|
+
* @param {string} [units] 允许的量词(默认 章回节)
|
|
86
|
+
* @returns {{no:number, unit:string, rest:string}|null}
|
|
87
|
+
*/
|
|
88
|
+
export function parseChapterNo(line, units = '章回节') {
|
|
89
|
+
const re = new RegExp('第\\s*([0-9]+|[〇零一二三四五六七八九十百千万两]+)\\s*([' + units + '])\\s*(.*)$');
|
|
90
|
+
const m = re.exec(String(line || '').trim());
|
|
91
|
+
if (!m) return null;
|
|
92
|
+
const no = cnToNumber(m[1]);
|
|
93
|
+
if (no === null) return null;
|
|
94
|
+
return { no, unit: m[2], rest: (m[3] || '').trim() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 归一化章节标题行:把章号统一成阿拉伯数字、去掉前导零。
|
|
99
|
+
* "# 第012章 夜巡" 与 "# 第十二章 夜巡" 归一后相同。
|
|
100
|
+
*
|
|
101
|
+
* @param {string} line
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeChapterHeading(line) {
|
|
105
|
+
const raw = String(line || '').trim();
|
|
106
|
+
if (!raw) return '';
|
|
107
|
+
return raw.replace(/第\s*([0-9]+|[〇零一二三四五六七八九十百千万两]+)\s*([章回节])/, (m, no, unit) => {
|
|
108
|
+
const n = cnToNumber(no);
|
|
109
|
+
return n === null ? m : '第' + n + unit;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .fancy-deployed 状态文件的统一体检。
|
|
3
|
+
*
|
|
4
|
+
* 这个文件是整个项目的状态源:phase(当前阶段)、llm_wiki_url/token、
|
|
5
|
+
* reference_id、setup_id 都在里面。五个插件都要读它,但原先各写各的:
|
|
6
|
+
* - fancy-topic readPhaseSafe() → missing / corrupt / no_phase
|
|
7
|
+
* - fancy-analyze readDeployedConfig() → missing / corrupt
|
|
8
|
+
* - fancy-scan advancePhase() → missing / unknown_phase / error
|
|
9
|
+
* 三套口径不一致 → 同一个坏文件在三个插件里表现不同,
|
|
10
|
+
* 而且**没有一个插件告诉用户该怎么修** —— 最糟的情况下形成死循环:
|
|
11
|
+
* /fancy-scan 扫完、报成功,但 phase 没推进(失败只写进 stderr)
|
|
12
|
+
* /fancy-topic 报"状态文件读不了,请重新运行 /fancy-bootstrap"
|
|
13
|
+
* /fancy-bootstrap 报"该目录已初始化"(只看文件存在,坏文件也拒绝)
|
|
14
|
+
* 用户被困在中间,没有任何命令能推进那个 phase。
|
|
15
|
+
*
|
|
16
|
+
* 所以这里统一两件事:**怎么判断**(readDeployedState)与
|
|
17
|
+
* **怎么修**(deployedStateProblemText)。
|
|
18
|
+
*
|
|
19
|
+
* @module lib/deployed-state
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
|
|
24
|
+
/** 阶段推进顺序(只能向前) */
|
|
25
|
+
export const PHASE_ORDER = ['initialized', 'scanned', 'topic_done', 'bible_done', 'outline_done'];
|
|
26
|
+
|
|
27
|
+
/** 体检结果的状态枚举 */
|
|
28
|
+
export const DEPLOYED_STATUS = {
|
|
29
|
+
OK: 'ok',
|
|
30
|
+
/** 文件不存在 */
|
|
31
|
+
MISSING: 'missing',
|
|
32
|
+
/** 文件不是合法 JSON */
|
|
33
|
+
CORRUPT: 'corrupt',
|
|
34
|
+
/** 文件是合法 JSON,但没有 phase 字段(或不是字符串) */
|
|
35
|
+
NO_PHASE: 'no_phase',
|
|
36
|
+
/** 有 phase 字段,但值不在 PHASE_ORDER 里 */
|
|
37
|
+
UNKNOWN_PHASE: 'unknown_phase',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 体检 .fancy-deployed。
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectRoot - 项目根目录
|
|
44
|
+
* @returns {{status: string, config: object|null, phase: string|null, error: string|null}}
|
|
45
|
+
*/
|
|
46
|
+
export function readDeployedState(projectRoot) {
|
|
47
|
+
const deployed = join(projectRoot, '.fancy-deployed');
|
|
48
|
+
if (!existsSync(deployed)) {
|
|
49
|
+
return { status: DEPLOYED_STATUS.MISSING, config: null, phase: null, error: null };
|
|
50
|
+
}
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(readFileSync(deployed, 'utf-8'));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return {
|
|
56
|
+
status: DEPLOYED_STATUS.CORRUPT,
|
|
57
|
+
config: null,
|
|
58
|
+
phase: null,
|
|
59
|
+
error: e && e.message ? e.message : String(e),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
63
|
+
return {
|
|
64
|
+
status: DEPLOYED_STATUS.CORRUPT,
|
|
65
|
+
config: null,
|
|
66
|
+
phase: null,
|
|
67
|
+
error: '不是合法的 JSON 对象',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (typeof data.phase !== 'string' || data.phase === '') {
|
|
71
|
+
return { status: DEPLOYED_STATUS.NO_PHASE, config: data, phase: null, error: null };
|
|
72
|
+
}
|
|
73
|
+
if (!PHASE_ORDER.includes(data.phase)) {
|
|
74
|
+
return {
|
|
75
|
+
status: DEPLOYED_STATUS.UNKNOWN_PHASE,
|
|
76
|
+
config: data,
|
|
77
|
+
phase: data.phase,
|
|
78
|
+
error: `phase 的值 "${data.phase}" 不认识`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { status: DEPLOYED_STATUS.OK, config: data, phase: data.phase, error: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 把体检结果渲染成用户可读的说明(含**可执行的修复指引**)。
|
|
86
|
+
*
|
|
87
|
+
* 关键点:不能只说"文件坏了"。用户需要知道改哪一行、改成什么,
|
|
88
|
+
* 或者干脆怎么重建 —— 否则就只能反复重跑命令直到放弃。
|
|
89
|
+
*
|
|
90
|
+
* @param {object} state - readDeployedState 的返回值
|
|
91
|
+
* @param {object} [opts]
|
|
92
|
+
* @param {string} [opts.projectRoot] - 项目根(用于生成模板里的路径)
|
|
93
|
+
* @returns {string|null} status 为 ok 时返回 null
|
|
94
|
+
*/
|
|
95
|
+
export function deployedStateProblemText(state, opts = {}) {
|
|
96
|
+
if (!state || state.status === DEPLOYED_STATUS.OK) return null;
|
|
97
|
+
const projectRoot = opts.projectRoot || '';
|
|
98
|
+
|
|
99
|
+
if (state.status === DEPLOYED_STATUS.MISSING) {
|
|
100
|
+
return '❌ 当前目录尚未初始化,请先运行 /fancy-bootstrap 进行初始化。';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const head = state.status === DEPLOYED_STATUS.CORRUPT
|
|
104
|
+
? '❌ 状态文件 .fancy-deployed 损坏(不是合法 JSON),无法确定项目状态。'
|
|
105
|
+
: state.status === DEPLOYED_STATUS.NO_PHASE
|
|
106
|
+
? '❌ 状态文件 .fancy-deployed 缺少 phase 字段,无法确定项目当前阶段。'
|
|
107
|
+
: `❌ 状态文件 .fancy-deployed 的 phase 值不认识("${state.phase}"),无法确定项目当前阶段。`;
|
|
108
|
+
|
|
109
|
+
const phaseHelp = state.status === DEPLOYED_STATUS.CORRUPT
|
|
110
|
+
? `3. 如果整个文件不是合法 JSON,用下面的模板重建 .fancy-deployed,
|
|
111
|
+
并把原来的 llm_wiki_token / reference_id 等字段值填回去:
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
"deployed_at": "<ISO 时间>",
|
|
115
|
+
"project_root_abs": "${projectRoot}",
|
|
116
|
+
"phase": "initialized",
|
|
117
|
+
"llm_wiki_url": "http://localhost:19828/api/v1",
|
|
118
|
+
"llm_wiki_token": "",
|
|
119
|
+
"reference_id": "",
|
|
120
|
+
"novel_id": "",
|
|
121
|
+
"provider": "custom"
|
|
122
|
+
}
|
|
123
|
+
`
|
|
124
|
+
: `3. 把 phase 改成合法值之一:${PHASE_ORDER.join(' / ')}。
|
|
125
|
+
拿不准就填 "initialized" —— 它允许流程从头再走一遍。`;
|
|
126
|
+
|
|
127
|
+
return `${head}${state.error ? `\n原因:${state.error}` : ''}
|
|
128
|
+
|
|
129
|
+
这条消息不能靠重跑命令解决(重跑只会再撞一次同样的问题),需要手工修复:
|
|
130
|
+
|
|
131
|
+
1. 打开项目根目录下的 .fancy-deployed
|
|
132
|
+
2. ${state.status === DEPLOYED_STATUS.CORRUPT
|
|
133
|
+
? '确认文件内容是不是被截断了(写入过程中断电/被编辑器改坏都会这样)'
|
|
134
|
+
: `找到 "phase" 那一行${state.phase ? `(当前值 "${state.phase}")` : ''},改成下面任一合法值`}
|
|
135
|
+
${phaseHelp}
|
|
136
|
+
4. 修完重新执行刚才的命令
|
|
137
|
+
|
|
138
|
+
另一条路(会丢配置,慎用):
|
|
139
|
+
5. 删除 .fancy-deployed 后重新运行 /fancy-bootstrap 重建。
|
|
140
|
+
注意:这会丢掉 llm_wiki_token / reference_id / setup_id 等字段,需要重新填。`;
|
|
141
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/error-codes.js — 全插件统一错误码定义
|
|
3
|
+
*
|
|
4
|
+
* 所有 fancy-* 插件共享此文件,保证错误码命名一致。
|
|
5
|
+
*
|
|
6
|
+
* 使用:
|
|
7
|
+
* import { EC, makeError } from '#error-codes';
|
|
8
|
+
* console.log(JSON.stringify({ ok: false, error: makeError(EC.EC001_INVALID_PATH) }));
|
|
9
|
+
*
|
|
10
|
+
* 命名规则:
|
|
11
|
+
* - ECxxx_NAME 全大写,下划线分隔
|
|
12
|
+
* - code 数字(1-999)
|
|
13
|
+
* - code_name 字符串 `ECxxx`,由 makeError 自动从 key 生成
|
|
14
|
+
* - message 默认描述;调用方可用 makeError(EC.XXX, { message: '...' }) 覆盖
|
|
15
|
+
*
|
|
16
|
+
* 码段分配:
|
|
17
|
+
* 001-019 bootstrap 路径校验、初始化
|
|
18
|
+
* 020-039 scan 浏览器、采集、超时
|
|
19
|
+
* 040-059 topic 参数、phase、状态
|
|
20
|
+
* 060-079 bible (预留)
|
|
21
|
+
* 080-099 outline (预留)
|
|
22
|
+
* 100-119 write (预留)
|
|
23
|
+
* 120-139 shared 通用(atomic-write、permission 等)
|
|
24
|
+
* 200+ system 致命错误
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const EC = {
|
|
28
|
+
// ─── 001-019: bootstrap ───────────────────────────────────────
|
|
29
|
+
EC001_INVALID_PATH: { code: 1, message: '路径不合法(不能含空格、.. 或特殊字符)' },
|
|
30
|
+
EC002_ALREADY_INITIALIZED: { code: 2, message: '项目已存在,不能重复 init' },
|
|
31
|
+
EC003_INVALID_PROJECT_ROOT:{ code: 3, message: '项目根路径不是绝对路径或不存在' },
|
|
32
|
+
EC017_INIT_FAILED: { code: 17, message: '初始化失败' },
|
|
33
|
+
EC018_DIR_NOT_FOUND: { code: 18, message: '目录不存在' },
|
|
34
|
+
EC019_SENSITIVE_PATH: { code: 19, message: '拒绝访问敏感系统路径' },
|
|
35
|
+
|
|
36
|
+
// ─── 020-039: scan ────────────────────────────────────────────
|
|
37
|
+
EC020_BROWSER_LAUNCH: { code: 20, message: '浏览器启动失败' },
|
|
38
|
+
EC021_BROWSER_DISCONNECTED: { code: 21, message: '浏览器在采集过程中断开' },
|
|
39
|
+
EC022_PAGE_GOTO_TIMEOUT: { code: 22, message: '页面导航超时' },
|
|
40
|
+
EC023_WAIT_RENDER_TIMEOUT: { code: 23, message: '等待榜单渲染超时' },
|
|
41
|
+
EC024_SCRAPER_TIMEOUT: { code: 24, message: 'scraper 子进程超时' },
|
|
42
|
+
EC025_SCRAPER_NON_JSON: { code: 25, message: 'scraper 输出非 JSON' },
|
|
43
|
+
EC026_OCR_FAILED: { code: 26, message: 'OCR 识别失败' },
|
|
44
|
+
EC027_NO_OUTPUT_FILES: { code: 27, message: '未找到采集输出文件' },
|
|
45
|
+
EC028_INVALID_RANK: { code: 28, message: '未知或非法的榜单 ID' },
|
|
46
|
+
EC029_INVALID_PLATFORM_SCAN:{ code: 29, message: '不支持的平台' },
|
|
47
|
+
|
|
48
|
+
// ─── 040-059: topic ───────────────────────────────────────────
|
|
49
|
+
EC040_INVALID_PLATFORM: { code: 40, message: 'platform 参数非法' },
|
|
50
|
+
EC041_INVALID_GENRE: { code: 41, message: 'genre 参数非法' },
|
|
51
|
+
EC042_MISSING_REQUIRED: { code: 42, message: '必填参数缺失' },
|
|
52
|
+
EC043_PHASE_NOT_TOPIC: { code: 43, message: '当前 phase 不允许 fancy-topic' },
|
|
53
|
+
EC044_TOPIC_STATE_MISSING: { code: 44, message: '_story_state.json 不存在' },
|
|
54
|
+
EC045_TOPIC_WRITE_FAILED: { code: 45, message: 'fancy-topic 写盘失败' },
|
|
55
|
+
EC046_INVALID_WORDS: { code: 46, message: '目标字数不合法(必须为正整数)' },
|
|
56
|
+
EC047_INVALID_CHAPTERS: { code: 47, message: '目标章数不合法(必须为正整数)' },
|
|
57
|
+
EC048_PLATFORM_NOT_FOUND: { code: 48, message: 'topic_decision 中找不到该平台的内容块' },
|
|
58
|
+
|
|
59
|
+
// ─── 120-139: shared ──────────────────────────────────────────
|
|
60
|
+
EC120_ATOMIC_WRITE_FAIL: { code: 120, message: '原子写文件失败' },
|
|
61
|
+
EC121_PHASE_ADVANCE_FAIL: { code: 121, message: 'phase 推进失败' },
|
|
62
|
+
EC122_PERMISSION_DENIED: { code: 122, message: '权限不足' },
|
|
63
|
+
EC123_JSON_PARSE_FAIL: { code: 123, message: 'JSON 解析失败' },
|
|
64
|
+
// "项目未初始化"与 EC002(项目已存在,不能重复 init)语义相反,
|
|
65
|
+
// 不能复用。此前 run-scan.js / select-topic.js 都把"未初始化"报成 EC002,
|
|
66
|
+
// 导致 code_name 与 message 自相矛盾(退出码 2 实际表示"未初始化")。
|
|
67
|
+
EC124_PROJECT_NOT_INITIALIZED: { code: 124, message: '项目未初始化(.fancy-deployed 不存在)' },
|
|
68
|
+
|
|
69
|
+
// ─── 200+: system ─────────────────────────────────────────────
|
|
70
|
+
EC200_SPAWN_FAIL: { code: 200, message: '子进程启动失败' },
|
|
71
|
+
EC201_SCRIPT_TIMEOUT: { code: 201, message: '脚本执行超时' },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 把 EC.XXX 引用转成标准 error 对象。
|
|
76
|
+
*
|
|
77
|
+
* @param {object} ec - EC.XXX 之一
|
|
78
|
+
* @param {object} [extra] - 额外字段:
|
|
79
|
+
* - message: 覆盖默认 message
|
|
80
|
+
* - 其他字段会合并进 error 对象
|
|
81
|
+
* @returns {{ code: number, code_name: string, message: string }}
|
|
82
|
+
*/
|
|
83
|
+
export function makeError(ec, extra = {}) {
|
|
84
|
+
if (!ec || typeof ec !== 'object' || typeof ec.code !== 'number') {
|
|
85
|
+
throw new Error('makeError: invalid EC reference');
|
|
86
|
+
}
|
|
87
|
+
// 自动从 EC 找到 key 名作为 code_name(如 'EC001_INVALID_PATH')
|
|
88
|
+
const codeName = Object.keys(EC).find(k => EC[k] === ec) || `EC${String(ec.code).padStart(3, '0')}`;
|
|
89
|
+
return {
|
|
90
|
+
code: ec.code,
|
|
91
|
+
code_name: codeName,
|
|
92
|
+
message: extra.message || ec.message,
|
|
93
|
+
...(extra && typeof extra === 'object' ? Object.fromEntries(
|
|
94
|
+
Object.entries(extra).filter(([k]) => k !== 'message')
|
|
95
|
+
) : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 便捷函数:直接拼出 ok:false 错误响应
|
|
101
|
+
*/
|
|
102
|
+
export function errorResponse(ec, extra = {}) {
|
|
103
|
+
return { ok: false, error: makeError(ec, extra) };
|
|
104
|
+
}
|