dsh-novel-write-engine 0.2.7 → 0.2.8
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/CHANGELOG.md +27 -0
- package/README.md +22 -20
- package/lib/client.js +15 -10
- package/lib/index.d.ts +69 -3
- package/lib/index.js +268 -53
- package/lib/types/client/dto.d.ts +20 -0
- package/lib/types/client/viewmodel.d.ts +2 -0
- package/lib/types/types.d.ts +37 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.8
|
|
4
|
+
|
|
5
|
+
0.2.8 是边界与可信性版本,不改工具、阶段、门控、schema、导出或配置键。
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **章节边界读模型**:归档第 N 章后 `pipeline.chapter` 指向 N+1、`stage` 仍为 `done`,旧读面把它拼成“当前第 N+1 章,阶段 done”。新增 `ChapterProgress` 单一投影,`/novel status`、`/novel resume`、`novel_status`、`novel_pipeline`、`novel_guide`、`novel_doctor`、面板摘要与复制续写指令统一显示“第 N 章已归档;下一章第 N+1 章从 P1 开始”。全书达到目标后 `finished=true`,引擎续写文本返回 `null`;`/novel resume` 改为显示“全书已完成”,不再把它当成故障态。
|
|
10
|
+
- **备份路径安全**:`backups/` 改用 `FsStore.ensureDir` + `safePath` 创建和解析,符号链接目录返回 `ENGINE_PATH_ESCAPE`,项目外零写入;备份 staging 写入前重复检查项目根,避免删除根后被 `mkdir` 重建;备份列表/retention 跳过 `backups/` 下的符号链接子项,不再因一个用户文件整体失败。
|
|
11
|
+
- **叙述者别名**:新增 `src/domain/narrator.ts`,PG-05、`state_delta` 的存在性与 locks 取证共用 `isNarratorAlias`;`narrator: "林砚(第一人称)"` 不再因为正文只写“我”而卡在 PG-05 或归档;只接受第一人称“我”,排除第三人称与无关括号引号,locks 优先非对白段落。
|
|
12
|
+
- **面板健康缓存**:`panelCacheKey` 纳入 journal 行数、末尾行与 `journalBlocking` 摘要,外部 ledger 改动或 journal 追加后不再返回旧 `health.ok`/旧 activity。
|
|
13
|
+
- **归档卡字段**:归档卡的 `goal/conflict/hook/notes` 与 `title/scene_type/target_words/cast` 一样不可改写;省略字段仍保留原值。`novel_amend` 在 `chapters.yaml` 不可读时也以旧大纲卡为证据,不能借损坏窗口改写。
|
|
14
|
+
- **created growth**:`state_delta` 的 `created` 角色不再丢弃已声明的 `growth`,按当前章写入 ledger。
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `stats()` 与 `/novel stats` 的 `totalWords` 统一为“归档 + 活动草稿”,并新增 `archivedWords`/`draftWords` 拆分;面板同步新增 `archivedWords`/`draftWords`,头部总字数与命令口径一致。
|
|
19
|
+
- 面板九阶段名称改为与 README/DESIGN 一致:P7 状态同步、P8 终审、P9 归档;教程注明首次章卡提交默认 strict、纯文本 `novel_word_count`/`novel_diff` 不绑定会话。
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- 91 个测试文件 / 923 项测试全绿;覆盖率 93.55 / 89.23 / 98.14 / 93.55,逐文件门禁通过。新增归档边界、完结续写与命令文案、P8 brief 回归、面板缓存、字数拆分、备份符号链接、第一人称归档、created growth、归档字段保护、损坏 chapters 下的 amend 保护、空 legacy 卡重交、locks 对白取证、提交叙述者优先级回归。
|
|
24
|
+
|
|
25
|
+
### Migration
|
|
26
|
+
|
|
27
|
+
- `progress`、`archivedWords`、`draftWords` 均为 additive 字段;`REMOTE_VERSION` 保持 6,旧标签页刷新即可。
|
|
28
|
+
- 无 schema、工具、阶段、门控、导出或配置键变更;项目文件不需要迁移。
|
|
29
|
+
|
|
3
30
|
## 0.2.7
|
|
4
31
|
|
|
5
32
|
0.2.7 是通用性与发布边界版本,不改工具、阶段、门控、schema 或规则阈值。
|
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
21
|
> [!IMPORTANT]
|
|
22
|
-
> 当前版本 **0.2.
|
|
22
|
+
> 当前版本 **0.2.8**。宿主范围:DSH `>=0.2.1-rc.2 <0.3.0-0`;Node `>=24.9.0`。
|
|
23
23
|
>
|
|
24
|
-
> 0.2.
|
|
24
|
+
> 0.2.8 是边界与可信性版本:修正归档后“下一章被误标为 done”的读模型、备份路径符号链接逃逸、PG-05 与归档叙述者别名不一致、面板健康缓存假绿、完结书仍生成续写指令;同时保护归档章卡字段并修复 created growth 丢失。没有改工具、阶段、门控、schema 或配置键;改变的是人类读法、路径安全和少量状态契约,不迁移项目数据。历史版本的内部方案与验收稿留在对应发布 tag,不再复制进 `main`。
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -119,6 +119,8 @@ flowchart TB
|
|
|
119
119
|
|
|
120
120
|
P7、P8、P9 在 `novel_archive` 一次事务里完成。单章归档后状态是 `done`;下一章仍从 P1 开始。只有项目配置的 `total_chapters` 全部归档,面板才显示“全书完成”。
|
|
121
121
|
|
|
122
|
+
`pipeline.chapter` + `pipeline.stage` 是协议对,不是人类读法:归档第 N 章后,管线指向 N+1,`stage` 仍是 `done`,因为 `done` 描述的是**刚刚归档的第 N 章**。所有读面统一使用 `progress` 投影:`/novel status`、`novel_status`、`novel_pipeline`、`novel_guide`、`novel_doctor`、面板和“复制续写指令”都会显示“第 N 章已归档;下一章第 N+1 章从 P1 开始”。全书完成时 `progress.finished=true`,引擎续写文本为 `null`,`/novel resume` 显示“全书已完成”;要继续写,先用 `novel_amend` 提高 `targets.total_chapters`。
|
|
123
|
+
|
|
122
124
|
### 十三道门控
|
|
123
125
|
|
|
124
126
|
门控按固定顺序求值,并且**收集全部失败项**,不是遇到第一个就返回。这样模型可以一次改完,而不是来回撞墙。
|
|
@@ -201,7 +203,7 @@ pnpm --version # CLI 安装插件时需要
|
|
|
201
203
|
corepack enable
|
|
202
204
|
corepack prepare pnpm@11.28.9 --activate
|
|
203
205
|
|
|
204
|
-
dsh plugin --profile web add dsh-novel-write-engine@0.2.
|
|
206
|
+
dsh plugin --profile web add dsh-novel-write-engine@0.2.8
|
|
205
207
|
```
|
|
206
208
|
|
|
207
209
|
安装后重启 DSH,或刷新 Web 页面。
|
|
@@ -249,10 +251,10 @@ corepack pnpm build
|
|
|
249
251
|
corepack pnpm pack
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
得到 `dsh-novel-write-engine-0.2.
|
|
254
|
+
得到 `dsh-novel-write-engine-0.2.8.tgz`,复制到目标机器后:
|
|
253
255
|
|
|
254
256
|
```bash
|
|
255
|
-
dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.
|
|
257
|
+
dsh plugin --profile web add /absolute/path/to/dsh-novel-write-engine-0.2.8.tgz
|
|
256
258
|
```
|
|
257
259
|
|
|
258
260
|
tarball 已包含 `lib/`、`rulepacks/`、`briefs/`、`skills/`、`presets/` 和两层 patch,不需要在目标机器重新构建。
|
|
@@ -264,7 +266,7 @@ tarball 已包含 `lib/`、`rulepacks/`、`briefs/`、`skills/`、`presets/` 和
|
|
|
264
266
|
|
|
265
267
|
1. 左侧栏进入 **插件**;
|
|
266
268
|
2. 点 **添加插件**;
|
|
267
|
-
3. 填 npm 包名 `dsh-novel-write-engine`(或精确版本 `dsh-novel-write-engine@0.2.
|
|
269
|
+
3. 填 npm 包名 `dsh-novel-write-engine`(或精确版本 `dsh-novel-write-engine@0.2.8`);
|
|
268
270
|
4. 安装后启用,刷新页面,新开会话。
|
|
269
271
|
|
|
270
272
|
插件页安装的是当前 profile 的依赖;Desktop 的插件页管理的是 `desktop` profile。
|
|
@@ -332,9 +334,9 @@ tarball 已包含 `lib/`、`rulepacks/`、`briefs/`、`skills/`、`presets/` 和
|
|
|
332
334
|
|
|
333
335
|
归档后打开左侧 **网文工坊**:
|
|
334
336
|
|
|
335
|
-
-
|
|
336
|
-
- 首屏“当前章 /
|
|
337
|
-
-
|
|
337
|
+
- 紧凑顶栏:书名、类型、平台、当前章边界(例如“第 1 章已归档 · 下一章第 2 章待开始”);
|
|
338
|
+
- 首屏“当前章 / 下一步”卡与“复制续写指令”;归档后显示下一章 P1,全书完成时隐藏复制按钮;
|
|
339
|
+
- 统计条:已归档章节、总字数(归档 + 活动草稿)、平均分、待处理门控;
|
|
338
340
|
- 双栏工作台:阶段轨、门控、提示、旧数据、全书规划、质量趋势、健康度、最近活动;
|
|
339
341
|
- 底部章节卡。
|
|
340
342
|
|
|
@@ -439,9 +441,9 @@ novel_write_draft, novel_review, novel_revise, novel_check, novel_archive
|
|
|
439
441
|
| `novel_outline` | 按 `num` upsert 章卡、按 `title` upsert 卷;省略不删,显式 `remove`;`dry_run` 预览 |
|
|
440
442
|
| `novel_outline_import` | 从 Markdown / YAML / JSON 导入大纲;`dry_run` 先预览 |
|
|
441
443
|
| `novel_worldbook` | 增量更新世界观设定集(设定/规则/势力/地点/角色/术语/前史) |
|
|
442
|
-
| `novel_status` |
|
|
444
|
+
| `novel_status` | `progress` 章节边界、轮次、阻塞项、`state_errors`、`manuscript_hash`、下一步 |
|
|
443
445
|
| `novel_guide` | 上手引导:当前状态、已完成步骤、下一步工具;无项目也能用 |
|
|
444
|
-
| `novel_pipeline` |
|
|
446
|
+
| `novel_pipeline` | “下一步该做什么”的建议,带同一份 `progress` |
|
|
445
447
|
|
|
446
448
|
### 单章流水线(8)
|
|
447
449
|
|
|
@@ -620,18 +622,18 @@ corepack pnpm test
|
|
|
620
622
|
|
|
621
623
|
## 升级与卸载
|
|
622
624
|
|
|
623
|
-
### 升级到 0.2.
|
|
625
|
+
### 升级到 0.2.8
|
|
624
626
|
|
|
625
|
-
0.2.
|
|
627
|
+
0.2.8 没有 schema、工具、阶段、门控、导出或配置键变更,不需要迁移项目文件。
|
|
626
628
|
|
|
627
629
|
```bash
|
|
628
630
|
dsh plugin --profile web remove dsh-novel-write-engine
|
|
629
|
-
dsh plugin --profile web add dsh-novel-write-engine@0.2.
|
|
631
|
+
dsh plugin --profile web add dsh-novel-write-engine@0.2.8
|
|
630
632
|
```
|
|
631
633
|
|
|
632
|
-
升级后刷新页面、新开会话。面板教程版本从 0.2.
|
|
634
|
+
升级后刷新页面、新开会话。面板教程版本从 0.2.7 升到 0.2.8;旧标签页显示“需要刷新”时刷新即可。
|
|
633
635
|
|
|
634
|
-
历史版本的升级说明见 [CHANGELOG.md](CHANGELOG.md)。0.2.
|
|
636
|
+
历史版本的升级说明见 [CHANGELOG.md](CHANGELOG.md)。0.2.8 不再在 `main` 保留旧版方案与验收稿;需要溯源时使用对应发布 tag 或 Release 快照。
|
|
635
637
|
|
|
636
638
|
### 从源码目录升级
|
|
637
639
|
|
|
@@ -786,14 +788,14 @@ presets/ # 内置 Agent 预设 patch
|
|
|
786
788
|
tests/ # 单测、集成、falsify、e2e
|
|
787
789
|
```
|
|
788
790
|
|
|
789
|
-
### 本次 0.2.
|
|
791
|
+
### 本次 0.2.8 验证
|
|
790
792
|
|
|
791
793
|
以本次发布实际跑出的结果为准:
|
|
792
794
|
|
|
793
|
-
- `corepack pnpm test`:**91 个测试文件 /
|
|
794
|
-
- 覆盖率:**语句 93.
|
|
795
|
+
- `corepack pnpm test`:**91 个测试文件 / 923 项全部通过**;
|
|
796
|
+
- 覆盖率:**语句 93.55% / 分支 89.23% / 函数 98.14% / 行 93.55%**;逐文件门禁通过;
|
|
795
797
|
- `typecheck`、`lint`、`build`:通过;
|
|
796
|
-
- `check:hygiene`(
|
|
798
|
+
- `check:hygiene`(157 files,0 v8 ignore)、`test:isolated`(63 源文件)、`validate:rulepack`、`check:pack`(26 files)、`check:artifacts`、`check:install`(26 packages)、`check:preset`(13 packages)、`check:host`、`check:crash`:通过;
|
|
797
799
|
- 三题材矩阵:玄幻修仙、都市现代、西方奇幻各跑一遍 `init → archive`;
|
|
798
800
|
- 隐私守卫:`src / briefs / skills / presets / rulepacks / docs` 与根文档均无具体项目身份。
|
|
799
801
|
|
package/lib/client.js
CHANGED
|
@@ -50,9 +50,9 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
P4: "评审",
|
|
51
51
|
P5: "修订",
|
|
52
52
|
P6: "一致性",
|
|
53
|
-
P7: "
|
|
54
|
-
P8: "
|
|
55
|
-
P9: "
|
|
53
|
+
P7: "状态同步",
|
|
54
|
+
P8: "终审",
|
|
55
|
+
P9: "归档"
|
|
56
56
|
};
|
|
57
57
|
const STAGE_LABELS = {
|
|
58
58
|
idle: "待开始",
|
|
@@ -332,8 +332,10 @@ window.__ModuleLoader__.load({
|
|
|
332
332
|
const rowAverage = scored.length === 0 ? 0 : scored.reduce((sum, row) => sum + row.scoreValue, 0) / scored.length;
|
|
333
333
|
const average = summary.averageScore !== void 0 && Number.isFinite(summary.averageScore) ? summary.averageScore : rowAverage;
|
|
334
334
|
const next = summary.next;
|
|
335
|
-
const
|
|
335
|
+
const progress = summary.progress;
|
|
336
|
+
const finished = progress?.finished ?? summary.finished === true;
|
|
336
337
|
const doneLabel = summary.stage === "done" ? finished ? "全书完成" : "本章已归档" : stageLabel(summary.stage);
|
|
338
|
+
const progressLine = progress?.label ?? (summary.stage === "done" ? finished ? `共 ${summary.totalChapters} 章` : `下一章 第 ${summary.chapter} 章` : `第 ${summary.chapter} 章`);
|
|
337
339
|
const selected = base.projects.find((project) => project.selected);
|
|
338
340
|
return {
|
|
339
341
|
...base,
|
|
@@ -343,7 +345,7 @@ window.__ModuleLoader__.load({
|
|
|
343
345
|
damaged: selected?.state === "damaged",
|
|
344
346
|
resumeText: payload.resume?.text ?? "",
|
|
345
347
|
volumeNumber: summary.volume,
|
|
346
|
-
subtitle:
|
|
348
|
+
subtitle: `${summary.genre} · ${platformLabel(summary.platform)} · ${progressLine}${volumeLabel(summary)}`,
|
|
347
349
|
stageLabel: doneLabel,
|
|
348
350
|
finished,
|
|
349
351
|
revisionLabel: `第 ${summary.bookData?.current.rounds.revise ?? summary.revision} 次修订`,
|
|
@@ -357,6 +359,8 @@ window.__ModuleLoader__.load({
|
|
|
357
359
|
stats: {
|
|
358
360
|
chapters: summary.totalChapters,
|
|
359
361
|
words: summary.totalWords,
|
|
362
|
+
...summary.archivedWords === void 0 ? {} : { archivedWords: summary.archivedWords },
|
|
363
|
+
...summary.draftWords === void 0 ? {} : { draftWords: summary.draftWords },
|
|
360
364
|
averageScore: Math.round(average * 10) / 10,
|
|
361
365
|
blocking: blocking.length
|
|
362
366
|
},
|
|
@@ -1146,7 +1150,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
1150
|
title: "写书级蓝图与卷结构",
|
|
1147
1151
|
tool: "novel_blueprint",
|
|
1148
1152
|
body: "让模型先定 premise(故事前提)、logline(一句话卖点)、themes(主题)、tone(基调)、pov(视角)、audience(读者)、selling_points(卖点)、ending(结局方向)和 volumes(每卷起止章、目标、弧线、高潮)。",
|
|
1149
|
-
note: "
|
|
1153
|
+
note: "蓝图不是正文,可以后补。首次提交章卡时 strict 默认开启;要宽松写作可在蓝图里显式 strict:false 关闭。strict 开启后每章必须先有章卡;章卡一旦存在,标题、场景、目标字数和 cast 仍会校验。"
|
|
1150
1154
|
},
|
|
1151
1155
|
{
|
|
1152
1156
|
title: "排章卡,或导入现成大纲",
|
|
@@ -1219,7 +1223,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1219
1223
|
每次提交前先领取当前阶段任务书,失败时按返回的 code 和 blocking 一次改完。`;
|
|
1220
1224
|
const TUTORIAL_NEXT_PROMPT = `继续写当前项目的下一章。先调用 novel_pipeline(或 novel_status)确认当前阶段、项目根目录和下一步工具,不要猜阶段。
|
|
1221
1225
|
如果当前会话还没绑定或不确定是哪本书:先用 /novel current 看会话绑定,再调用 novel_status 看 selection;需要切换时用 /novel use <路径|书名|.>,不要靠 active 猜。
|
|
1222
|
-
|
|
1226
|
+
第一次带 project_root 的项目调用会成为当前会话的持久绑定;后续无 root 调用会继续这本书。纯文本的 novel_word_count 和 novel_diff 不读取项目,也不会建立绑定。
|
|
1223
1227
|
|
|
1224
1228
|
如果下一章还没有章卡:用 novel_outline 只提交下一章的卡(按 num upsert),省略的已有卡会保持不动;不要重交全量,也不要用空数组清空。要删卡必须显式传 remove。
|
|
1225
1229
|
如果 targets.total_chapters 已经写满:先用 novel_amend 把 total_chapters 调到至少包含下一章,不能低于已归档章数。
|
|
@@ -1515,7 +1519,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1515
1519
|
ref: closeRef,
|
|
1516
1520
|
onClick: onClose,
|
|
1517
1521
|
"aria-label": "关闭使用教程"
|
|
1518
|
-
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.
|
|
1522
|
+
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.8 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
|
|
1519
1523
|
const NovelHelp = () => {
|
|
1520
1524
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1521
1525
|
const triggerRef = (0, react.useRef)(null);
|
|
@@ -1592,6 +1596,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1592
1596
|
const bookDataSection = (view) => {
|
|
1593
1597
|
const data = view.bookData;
|
|
1594
1598
|
if (data === null) return null;
|
|
1599
|
+
const currentLabel = data.current.stage === "done" ? data.progress.target !== null && data.progress.archived >= data.progress.target ? "全书完成" : `下一章 ${data.current.chapter} · 未开始` : `${data.current.chapter} · ${data.current.stage}`;
|
|
1595
1600
|
const dims = data.quality.dimensions.map((dimension) => (0, react.createElement)("div", {
|
|
1596
1601
|
className: "nwe-v6-bar",
|
|
1597
1602
|
key: dimension.id
|
|
@@ -1613,7 +1618,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1613
1618
|
["已归档", `${data.progress.archived} / ${data.progress.target ?? "不限"} 章`],
|
|
1614
1619
|
["字数", `归档 ${data.progress.words_archived} · 含草稿 ${data.progress.words_with_draft}`],
|
|
1615
1620
|
["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${data.progress.remaining_words_basis})`],
|
|
1616
|
-
["当前章", `${
|
|
1621
|
+
["当前章", `${currentLabel} · ${data.current.card?.title ?? "无章卡"}`],
|
|
1617
1622
|
["轮次", `评 ${data.current.rounds.review} / 修 ${data.current.rounds.revise} / 检 ${data.current.rounds.check}`],
|
|
1618
1623
|
["正文 hash", data.current.manuscript_hash ?? "—"],
|
|
1619
1624
|
["当前字数", data.current.metrics === null ? "—" : String(data.current.metrics.total_word_count)]
|
|
@@ -1685,7 +1690,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
|
|
|
1685
1690
|
className: "nwe-stats",
|
|
1686
1691
|
role: "group",
|
|
1687
1692
|
"aria-label": "项目统计"
|
|
1688
|
-
}, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready ? bookDataSection(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? (0, react.createElement)("div", { className: "nwe-desk" }, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
|
|
1693
|
+
}, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), (view.stats.draftWords ?? 0) > 0 ? `字(归档 ${(view.stats.archivedWords ?? 0).toLocaleString("zh-CN")} + 草稿 ${view.stats.draftWords.toLocaleString("zh-CN")})` : "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, ready ? bookDataSection(view) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? (0, react.createElement)("div", { className: "nwe-desk" }, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
|
|
1689
1694
|
className: "nwe-desk-side",
|
|
1690
1695
|
"aria-label": "全书与活动"
|
|
1691
1696
|
}, bookSection(view), activitySection(view))) : null, footer());
|
package/lib/index.d.ts
CHANGED
|
@@ -301,8 +301,8 @@ interface CharacterDelta {
|
|
|
301
301
|
role?: string;
|
|
302
302
|
profile?: Record<string, unknown>;
|
|
303
303
|
current_state?: Record<string, unknown>;
|
|
304
|
+
/** Chapter is engine-assigned from the archive transaction, not caller-supplied. */
|
|
304
305
|
growth?: {
|
|
305
|
-
chapter: number;
|
|
306
306
|
change: string;
|
|
307
307
|
status_change: string;
|
|
308
308
|
};
|
|
@@ -972,6 +972,8 @@ interface GateInput {
|
|
|
972
972
|
briefStage: BriefStage | null;
|
|
973
973
|
draftTitle: string | null;
|
|
974
974
|
sceneType: SceneType | null;
|
|
975
|
+
/** The submitted/active plan narrator; PG-05 uses it before a draft exists. */
|
|
976
|
+
narrator?: string | null;
|
|
975
977
|
summary: ChapterSummary | null;
|
|
976
978
|
/** P5 may omit `summary`; when omitted, the previous plan summary is reused. */
|
|
977
979
|
summaryProvided?: boolean;
|
|
@@ -1034,6 +1036,36 @@ interface Selection {
|
|
|
1034
1036
|
title?: string;
|
|
1035
1037
|
created_at?: string;
|
|
1036
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* One human-readable projection of the pipeline's chapter boundary.
|
|
1041
|
+
*
|
|
1042
|
+
* `pipeline.chapter`/`stage` are protocol values: after archiving chapter N the
|
|
1043
|
+
* pipeline points at N+1 and still says `done` because `done` describes the
|
|
1044
|
+
* chapter that was just archived. This projection is the only place that turns
|
|
1045
|
+
* those values into "chapter N archived; chapter N+1 starts at P1".
|
|
1046
|
+
*/
|
|
1047
|
+
interface ChapterProgress {
|
|
1048
|
+
active_chapter: number;
|
|
1049
|
+
stage: Stage;
|
|
1050
|
+
/** The chapter currently being written; null while `stage === 'done'`. */
|
|
1051
|
+
current: {
|
|
1052
|
+
chapter: number;
|
|
1053
|
+
stage: Stage;
|
|
1054
|
+
} | null;
|
|
1055
|
+
/** Highest archived chapter, or null before the first archive. */
|
|
1056
|
+
last_archived: number | null;
|
|
1057
|
+
/** Next chapter to start at P1; null while writing or after the book is finished. */
|
|
1058
|
+
next: {
|
|
1059
|
+
chapter: number;
|
|
1060
|
+
stage: 'P1';
|
|
1061
|
+
} | null;
|
|
1062
|
+
/** True only when the configured target has been reached at `stage === 'done'`. */
|
|
1063
|
+
finished: boolean;
|
|
1064
|
+
/** One line for status cards, commands and the panel. */
|
|
1065
|
+
label: string;
|
|
1066
|
+
/** Sentence used by the resume prompt instead of "当前第 N 章,阶段 done". */
|
|
1067
|
+
resume: string;
|
|
1068
|
+
}
|
|
1037
1069
|
/** The `getStatus` payload. */
|
|
1038
1070
|
interface StatusReport {
|
|
1039
1071
|
project: {
|
|
@@ -1054,6 +1086,8 @@ interface StatusReport {
|
|
|
1054
1086
|
/** SHA-256 of the active manuscript, or null when the stage has no draft. */
|
|
1055
1087
|
manuscript_hash: string | null;
|
|
1056
1088
|
stage: Stage;
|
|
1089
|
+
/** Chapter-boundary projection shared by every human-readable surface. */
|
|
1090
|
+
progress?: ChapterProgress;
|
|
1057
1091
|
rounds: RoundCounters;
|
|
1058
1092
|
blocking: BlockingItem[];
|
|
1059
1093
|
/** Non-blocking recovery notes, e.g. a recoverable pending transaction. */
|
|
@@ -1106,6 +1140,8 @@ interface StatusReport {
|
|
|
1106
1140
|
interface AdviceReport {
|
|
1107
1141
|
stage: Stage;
|
|
1108
1142
|
chapter: number;
|
|
1143
|
+
/** Same chapter-boundary projection as getStatus. */
|
|
1144
|
+
progress?: ChapterProgress;
|
|
1109
1145
|
tool: string;
|
|
1110
1146
|
args: Record<string, unknown>;
|
|
1111
1147
|
reason: string;
|
|
@@ -1577,6 +1613,8 @@ interface PanelSnapshot {
|
|
|
1577
1613
|
platform: string;
|
|
1578
1614
|
chapter: number;
|
|
1579
1615
|
stage: Stage;
|
|
1616
|
+
/** Chapter-boundary projection; `chapter`/`stage` stay as the protocol pair. */
|
|
1617
|
+
progress?: ChapterProgress;
|
|
1580
1618
|
revision: number;
|
|
1581
1619
|
rounds: {
|
|
1582
1620
|
review: number;
|
|
@@ -1595,8 +1633,12 @@ interface PanelSnapshot {
|
|
|
1595
1633
|
chapters: ChapterRecord[];
|
|
1596
1634
|
/** The active, not-yet-archived chapter; archived rows come from `chapters`. */
|
|
1597
1635
|
draft?: PanelDraft | null;
|
|
1598
|
-
/**
|
|
1636
|
+
/** Archived words plus the active draft; the header shows this total. */
|
|
1599
1637
|
totalWords: number;
|
|
1638
|
+
/** Words in archived chapters only. */
|
|
1639
|
+
archivedWords?: number;
|
|
1640
|
+
/** Active draft words, zero when no draft exists. */
|
|
1641
|
+
draftWords?: number;
|
|
1600
1642
|
/** Book-wide archived average, not just the panel's capped rows. */
|
|
1601
1643
|
averageScore?: number;
|
|
1602
1644
|
/** The nine-phase strip state, derived from the pipeline stage. */
|
|
@@ -1694,8 +1736,14 @@ interface PanelSummaryDto {
|
|
|
1694
1736
|
platform: string;
|
|
1695
1737
|
chapter: number;
|
|
1696
1738
|
stage: Stage;
|
|
1739
|
+
/** Chapter-boundary projection; absent on older payloads. */
|
|
1740
|
+
progress?: ChapterProgress;
|
|
1697
1741
|
revision: number;
|
|
1698
1742
|
totalWords: number;
|
|
1743
|
+
/** Words in archived chapters only; `totalWords` also counts the active draft. */
|
|
1744
|
+
archivedWords?: number;
|
|
1745
|
+
/** Active draft words, zero when no draft exists. */
|
|
1746
|
+
draftWords?: number;
|
|
1699
1747
|
totalChapters: number;
|
|
1700
1748
|
/** Book-wide archived average; absent on older payloads. */
|
|
1701
1749
|
averageScore?: number;
|
|
@@ -1825,12 +1873,27 @@ interface PanelBundle {
|
|
|
1825
1873
|
empty?: true;
|
|
1826
1874
|
error?: string;
|
|
1827
1875
|
}
|
|
1876
|
+
interface ChapterProgressInput {
|
|
1877
|
+
activeChapter: number;
|
|
1878
|
+
stage: Stage;
|
|
1879
|
+
archived: readonly number[];
|
|
1880
|
+
target: number | null;
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Turn the protocol pair (`chapter` + `stage`) into the only human-readable
|
|
1884
|
+
* chapter boundary. After archiving chapter N the protocol intentionally keeps
|
|
1885
|
+
* `stage: 'done'` and advances `chapter` to N+1; `done` describes chapter N,
|
|
1886
|
+
* not the chapter the pipeline now points at.
|
|
1887
|
+
*/
|
|
1888
|
+
declare function projectChapterProgress(input: ChapterProgressInput): ChapterProgress;
|
|
1828
1889
|
interface ResumePromptInput {
|
|
1829
1890
|
title: string;
|
|
1830
1891
|
root: string;
|
|
1831
1892
|
chapter: number | string;
|
|
1832
1893
|
stage: string;
|
|
1833
1894
|
nextTool: string;
|
|
1895
|
+
/** When supplied, replaces the protocol pair with the chapter boundary text. */
|
|
1896
|
+
progress?: ChapterProgress;
|
|
1834
1897
|
}
|
|
1835
1898
|
/** One shared prompt template for `/novel resume` and the panel copy button. */
|
|
1836
1899
|
declare function formatResumePrompt(input: ResumePromptInput): string;
|
|
@@ -2298,6 +2361,7 @@ declare class EngineService {
|
|
|
2298
2361
|
}): Promise<OpResult<{
|
|
2299
2362
|
stage: Stage;
|
|
2300
2363
|
chapter: number;
|
|
2364
|
+
progress: ChapterProgress;
|
|
2301
2365
|
tool: string;
|
|
2302
2366
|
args: Record<string, unknown>;
|
|
2303
2367
|
reason: string;
|
|
@@ -2419,6 +2483,8 @@ declare class EngineService {
|
|
|
2419
2483
|
root?: string;
|
|
2420
2484
|
}): Promise<{
|
|
2421
2485
|
totalWords: number;
|
|
2486
|
+
archivedWords: number;
|
|
2487
|
+
draftWords: number;
|
|
2422
2488
|
chapters: number;
|
|
2423
2489
|
revision: number;
|
|
2424
2490
|
rulepack: string;
|
|
@@ -2805,4 +2871,4 @@ declare const Config: Schema<Config>;
|
|
|
2805
2871
|
* created lazily per project root.
|
|
2806
2872
|
*/
|
|
2807
2873
|
declare function apply(ctx: Context, config: Config): void;
|
|
2808
|
-
export { ARCHIVE_DIGESTS_FILE, AdviceReport, ArchiveHandoff, AuditRow, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BlockingItem, BookPlan, BookReport, BriefDocument, BriefLibrary, BriefReceipt, BriefReceiptsDocument, BriefStage, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, ChapterCastMember, ChapterPlanInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, Deduction, DeltaAction, DiffReport, DoctorCheck, DoctorReport, DraftInput, DraftState, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, GuideReport, GuideStep, HISTORY_DIR, HISTORY_KINDS, HandoffDocument, HistoryKind, INITIAL_STAGE, LIMITS, LOCK_FILE, LocksDocument, METADATA_LOCK_FILE, MachineMetrics, NovelProject, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, OpResult, Operation, OutlineChapter, OutlineDocument, OutlineProgress, OutlineVolume, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, PanelBook, PanelBundle, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelMissing, PanelProjectOption, PanelSelection, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ResumePromptInput, ReviewDocument, ReviewInput, ReviewIssue, RevisionEdit, RevisionInput, RevisionPatch, RoundCounters, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, SceneType, ScoreDimension, SearchReport, SearchResultItem, Selection, SelectionSource, SemanticItem, Severity, Stage, StateDelta, StateError, StateErrorCode, StatusReport, TOOL_NAMES, TimelineDelta, TimelineDocument, ToolName, Violation, WORLDBOOK_FILE, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, WorldbookBackstory, WorldbookCharacter, WorldbookDocument, WorldbookFact, WorldbookFaction, WorldbookGlossary, WorldbookLocation, WorldbookRule, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
|
2874
|
+
export { ARCHIVE_DIGESTS_FILE, AdviceReport, ArchiveHandoff, AuditRow, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BlockingItem, BookPlan, BookReport, BriefDocument, BriefLibrary, BriefReceipt, BriefReceiptsDocument, BriefStage, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, ChapterCastMember, ChapterPlanInput, ChapterProgress, ChapterProgressInput, ChapterRecord, ChapterStatusRow, ChapterSummary, ChaptersDocument, CharacterDelta, CharactersDocument, CheckDocument, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, Deduction, DeltaAction, DiffReport, DoctorCheck, DoctorReport, DraftInput, DraftState, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, ErrorCode, ExportChapter, ExportKind, ForeshadowDelta, ForeshadowsDocument, GENRES, GateInput, GateOptions, GateResult, Genre, GuideReport, GuideStep, HISTORY_DIR, HISTORY_KINDS, HandoffDocument, HistoryKind, INITIAL_STAGE, LIMITS, LOCK_FILE, LocksDocument, METADATA_LOCK_FILE, MachineMetrics, NovelProject, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, OpResult, Operation, OutlineChapter, OutlineDocument, OutlineProgress, OutlineVolume, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, PanelBook, PanelBundle, PanelChapterRow, PanelChaptersDto, PanelDraft, PanelGatesDto, PanelMissing, PanelProjectOption, PanelSelection, PanelSnapshot, PanelSummaryDto, PipelineState, Platform, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, ReadingProof, ReadingProofReference, RelationshipDelta, RelationshipsDocument, ResumePromptInput, ReviewDocument, ReviewInput, ReviewIssue, RevisionEdit, RevisionInput, RevisionPatch, RoundCounters, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, SceneType, ScoreDimension, SearchReport, SearchResultItem, Selection, SelectionSource, SemanticItem, Severity, Stage, StateDelta, StateError, StateErrorCode, StatusReport, TOOL_NAMES, TimelineDelta, TimelineDocument, ToolName, Violation, WORLDBOOK_FILE, Warning, WordMetrics, WorldDelta, WorldDocument, WorldPlanInput, WorldbookBackstory, WorldbookCharacter, WorldbookDocument, WorldbookFact, WorldbookFaction, WorldbookGlossary, WorldbookLocation, WorldbookRule, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapterProgress, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
package/lib/index.js
CHANGED
|
@@ -308,6 +308,33 @@ function allocateIds(prefix, nextSeq, count) {
|
|
|
308
308
|
nextSeq: nextSeq + count
|
|
309
309
|
};
|
|
310
310
|
}
|
|
311
|
+
// src/domain/narrator.ts
|
|
312
|
+
/**
|
|
313
|
+
* One narrator-alias contract shared by PG-05, state_delta and lock capture.
|
|
314
|
+
*
|
|
315
|
+
* The plan format is `角色名(第一人称)` or `角色名(第一人称「我」)`. A
|
|
316
|
+
* first-person manuscript says 「我」 instead of repeating the character name;
|
|
317
|
+
* every consumer must accept that alias, and none may infer it from a bare
|
|
318
|
+
* substring such as "林" inside "林砚".
|
|
319
|
+
*/
|
|
320
|
+
const narratorName = (narrator) => narrator.split(/[((]/, 1)[0].trim();
|
|
321
|
+
/** The only documented first-person alias is the pronoun itself. */
|
|
322
|
+
const narratorAliases = (narrator) => isFirstPersonNarrator(narrator) ? ["我"] : [];
|
|
323
|
+
/** Whether the plan declares first-person narration. */
|
|
324
|
+
const isFirstPersonNarrator = (narrator) => narrator.includes("第一人称") && !narrator.includes("第三人称");
|
|
325
|
+
/**
|
|
326
|
+
* True when `name` is the declared narrator and the manuscript uses the
|
|
327
|
+
* declared first-person alias. A name that merely contains the narrator name
|
|
328
|
+
* is not an alias.
|
|
329
|
+
*/
|
|
330
|
+
const isNarratorAlias = (name, narrator, text) => {
|
|
331
|
+
if (text.includes(name)) return true;
|
|
332
|
+
if (narratorName(narrator) !== name) return false;
|
|
333
|
+
if (!isFirstPersonNarrator(narrator)) return false;
|
|
334
|
+
return narratorAliases(narrator).some((alias) => text.includes(alias));
|
|
335
|
+
};
|
|
336
|
+
/** Paragraph used for a newly locked character; prefers the named/aliased one. */
|
|
337
|
+
const narratorAliasesFor = (name, narrator) => name === narratorName(narrator) ? narratorAliases(narrator) : [];
|
|
311
338
|
// src/domain/scoring.ts
|
|
312
339
|
/** The fifteen dimensions, grouped as the commercial weights demand. */
|
|
313
340
|
const SCORE_DIMENSIONS = [
|
|
@@ -2528,7 +2555,13 @@ var FsStore = class {
|
|
|
2528
2555
|
/** Absolute path of an internal file outside the project root (locks live at root). */
|
|
2529
2556
|
async ensureDir(relativePath) {
|
|
2530
2557
|
const absolute = await this.safePath(relativePath);
|
|
2531
|
-
await
|
|
2558
|
+
await this.ensureParent(absolute);
|
|
2559
|
+
try {
|
|
2560
|
+
await promises.mkdir(absolute);
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
if (error.code !== "EEXIST") throw ioFailure(error);
|
|
2563
|
+
if (!(await promises.lstat(absolute)).isDirectory()) throw new EngineFaultError(ERROR_CODES.IO_PERMISSION, `refusing to use "${relativePath}": it exists but is not a directory`, "remove or rename the conflicting file, then retry");
|
|
2564
|
+
}
|
|
2532
2565
|
return absolute;
|
|
2533
2566
|
}
|
|
2534
2567
|
async exists(relativePath) {
|
|
@@ -3218,7 +3251,9 @@ const BACKUP_MANIFEST = "manifest.json";
|
|
|
3218
3251
|
/** Backup transaction directories, newest first by filesystem mtime. */
|
|
3219
3252
|
async function listBackups(store) {
|
|
3220
3253
|
const entries = [];
|
|
3221
|
-
for (const
|
|
3254
|
+
for (const entry of await store.entries(BACKUP_DIR)) {
|
|
3255
|
+
if (!entry.directory || entry.symbolicLink) continue;
|
|
3256
|
+
const name = entry.name;
|
|
3222
3257
|
if (name === "migrations") continue;
|
|
3223
3258
|
const info = await store.stat(joinRel(BACKUP_DIR, name));
|
|
3224
3259
|
if (info === null || !info.isDirectory) continue;
|
|
@@ -3379,6 +3414,7 @@ async function runTransaction(store, input) {
|
|
|
3379
3414
|
for (const file of input.files) {
|
|
3380
3415
|
const existing = before.get(file.path);
|
|
3381
3416
|
if (existing === null || existing === void 0) continue;
|
|
3417
|
+
await assertRootPresent();
|
|
3382
3418
|
await store.writeBytes(joinRel(staging, "before", file.path), existing);
|
|
3383
3419
|
}
|
|
3384
3420
|
if (input.backup) {
|
|
@@ -3386,8 +3422,10 @@ async function runTransaction(store, input) {
|
|
|
3386
3422
|
for (const file of input.files) {
|
|
3387
3423
|
const existing = before.get(file.path);
|
|
3388
3424
|
if (existing === null || existing === void 0) continue;
|
|
3425
|
+
await assertRootPresent();
|
|
3389
3426
|
await store.writeBytes(joinRel(stagingBackup, file.path), existing);
|
|
3390
3427
|
}
|
|
3428
|
+
await assertRootPresent();
|
|
3391
3429
|
await store.writeText(joinRel(stagingBackup, BACKUP_MARKER), "1");
|
|
3392
3430
|
await store.writeText(joinRel(stagingBackup, BACKUP_MANIFEST), `${JSON.stringify({
|
|
3393
3431
|
schema_version: 1,
|
|
@@ -3399,9 +3437,11 @@ async function runTransaction(store, input) {
|
|
|
3399
3437
|
...input.reason === void 0 ? {} : { reason: input.reason },
|
|
3400
3438
|
files: input.files.filter((file) => before.get(file.path) !== null && before.get(file.path) !== void 0).map((file) => file.path)
|
|
3401
3439
|
}, null, 2)}\n`);
|
|
3402
|
-
|
|
3403
|
-
await
|
|
3404
|
-
await
|
|
3440
|
+
await assertRootPresent();
|
|
3441
|
+
await store.ensureDir(BACKUP_DIR);
|
|
3442
|
+
await assertRootPresent();
|
|
3443
|
+
const targetBackup = await store.safePath(backupDir(input.txid));
|
|
3444
|
+
await promises.rename(await store.safePath(stagingBackup), targetBackup);
|
|
3405
3445
|
await syncDirectory(dirname(targetBackup));
|
|
3406
3446
|
backupMarked = true;
|
|
3407
3447
|
backupComplete = true;
|
|
@@ -4305,12 +4345,38 @@ function normalizeOutlineChapters(input, options) {
|
|
|
4305
4345
|
if (target[0] !== base.target_words[0] || target[1] !== base.target_words[1]) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its target_words cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4306
4346
|
const cast = raw.cast === void 0 ? base.cast : stringArray$1(raw.cast, `chapters[${index}].cast`, 8);
|
|
4307
4347
|
if (cast.length !== base.cast.length || cast.some((name) => !base.cast.includes(name))) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its cast cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4348
|
+
const goal = raw.goal === void 0 || raw.goal === base.goal ? base.goal : text$2(raw.goal, `chapter ${num} goal`);
|
|
4349
|
+
const conflict = raw.conflict === void 0 || raw.conflict === base.conflict ? base.conflict : text$2(raw.conflict, `chapter ${num} conflict`);
|
|
4350
|
+
const hook = raw.hook === void 0 || raw.hook === base.hook ? base.hook : text$2(raw.hook, `chapter ${num} hook`);
|
|
4351
|
+
const notes = raw.notes === void 0 || raw.notes === base.notes ? base.notes : optionalText(raw.notes, `chapter ${num} notes`, LIMITS.maxOutlineNotesChars);
|
|
4352
|
+
for (const [field, value, previous] of [
|
|
4353
|
+
[
|
|
4354
|
+
"goal",
|
|
4355
|
+
goal,
|
|
4356
|
+
base.goal
|
|
4357
|
+
],
|
|
4358
|
+
[
|
|
4359
|
+
"conflict",
|
|
4360
|
+
conflict,
|
|
4361
|
+
base.conflict
|
|
4362
|
+
],
|
|
4363
|
+
[
|
|
4364
|
+
"hook",
|
|
4365
|
+
hook,
|
|
4366
|
+
base.hook
|
|
4367
|
+
],
|
|
4368
|
+
[
|
|
4369
|
+
"notes",
|
|
4370
|
+
notes,
|
|
4371
|
+
base.notes
|
|
4372
|
+
]
|
|
4373
|
+
]) if (value !== previous) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its ${field} cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4308
4374
|
normalized.set(num, {
|
|
4309
4375
|
...base,
|
|
4310
|
-
goal
|
|
4311
|
-
conflict
|
|
4312
|
-
hook
|
|
4313
|
-
notes
|
|
4376
|
+
goal,
|
|
4377
|
+
conflict,
|
|
4378
|
+
hook,
|
|
4379
|
+
notes,
|
|
4314
4380
|
volume: volumeForChapter(options.volumes, num, raw.volume === void 0 ? base.volume : raw.volume),
|
|
4315
4381
|
status: "archived",
|
|
4316
4382
|
archived_at: base.archived_at ?? record.archived_at ?? null,
|
|
@@ -5485,9 +5551,10 @@ const requireForeshadowFields = (value, where) => {
|
|
|
5485
5551
|
if (value.note !== void 0 && typeof value.note !== "string") fail$1(`${where}.note must be a string`);
|
|
5486
5552
|
if (value.new_info !== void 0 && typeof value.new_info !== "boolean") fail$1(`${where}.new_info must be a boolean`);
|
|
5487
5553
|
};
|
|
5554
|
+
const withoutQuotedSpeech = (text) => text.replace(/"[^"]*"|“[^”]*”|「[^」]*」|『[^』]*』/g, "");
|
|
5488
5555
|
const appearanceOf = (text, name, aliases = []) => {
|
|
5489
5556
|
const paragraphs = splitParagraphs(text);
|
|
5490
|
-
const source = paragraphs.find((paragraph) => paragraph.includes(name)
|
|
5557
|
+
const source = paragraphs.find((paragraph) => withoutQuotedSpeech(paragraph).includes(name)) ?? paragraphs.find((paragraph) => paragraph.includes(name)) ?? paragraphs.find((paragraph) => aliases.some((alias) => alias !== "" && withoutQuotedSpeech(paragraph).includes(alias))) ?? paragraphs[0] ?? "";
|
|
5491
5558
|
return Array.from(source).slice(0, 120).join("");
|
|
5492
5559
|
};
|
|
5493
5560
|
const named = (value) => isRecord(value) && typeof value.name === "string";
|
|
@@ -5578,12 +5645,8 @@ function applyStateDelta(input) {
|
|
|
5578
5645
|
const appeared = [...new Set(summary.characters_appeared)];
|
|
5579
5646
|
const byName = new Map(characters.characters.map((character) => [character.name, character]));
|
|
5580
5647
|
const narrator = input.narrator ?? "";
|
|
5581
|
-
const narratorName = narrator
|
|
5582
|
-
const
|
|
5583
|
-
const appearsInManuscript = (name) => {
|
|
5584
|
-
if (input.text.includes(name)) return true;
|
|
5585
|
-
return name === narratorName && narratorAliases.some((alias) => alias !== "" && input.text.includes(alias));
|
|
5586
|
-
};
|
|
5648
|
+
const narratorName$1 = narratorName(narrator);
|
|
5649
|
+
const appearsInManuscript = (name) => isNarratorAlias(name, narrator, input.text);
|
|
5587
5650
|
for (const name of appeared) {
|
|
5588
5651
|
if (!appearsInManuscript(name)) fail$1(`character "${name}" is listed in characters_appeared but does not appear in the manuscript`);
|
|
5589
5652
|
if (!Object.hasOwn(delta.characters, name)) fail$1(`character "${name}" appeared in the chapter but is not declared in characters`);
|
|
@@ -5605,16 +5668,20 @@ function applyStateDelta(input) {
|
|
|
5605
5668
|
if (existing !== void 0) fail$1(`character "${name}" already exists; use updated or no_change`);
|
|
5606
5669
|
if (!appeared.includes(name)) fail$1(`character "${name}" is created but does not appear in characters_appeared`);
|
|
5607
5670
|
const rawHint = input.roleHints?.[name];
|
|
5608
|
-
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName || name === narrator) ? "protagonist" : "extra");
|
|
5671
|
+
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName$1 || name === narrator) ? "protagonist" : "extra");
|
|
5609
5672
|
const currentState = declared.current_state ?? {};
|
|
5610
5673
|
validateResources(currentState, `characters.${name}`);
|
|
5674
|
+
const growth = declared.growth === void 0 ? [] : [{
|
|
5675
|
+
...declared.growth,
|
|
5676
|
+
chapter
|
|
5677
|
+
}];
|
|
5611
5678
|
const created = {
|
|
5612
5679
|
name,
|
|
5613
5680
|
role: normalizeRole(declared.role, hinted),
|
|
5614
5681
|
first_appearance: chapter,
|
|
5615
5682
|
profile: declared.profile ?? {},
|
|
5616
5683
|
current_state: currentState,
|
|
5617
|
-
growth
|
|
5684
|
+
growth,
|
|
5618
5685
|
updated_at: chapter
|
|
5619
5686
|
};
|
|
5620
5687
|
characters.characters.push(created);
|
|
@@ -5626,7 +5693,7 @@ function applyStateDelta(input) {
|
|
|
5626
5693
|
const rawHint = input.roleHints?.[name];
|
|
5627
5694
|
const hinted = canonicalRole(rawHint);
|
|
5628
5695
|
if (hinted !== null && hinted !== "extra") existing.role = hinted;
|
|
5629
|
-
else if (rawHint === void 0 && (name === narratorName || name === narrator)) existing.role = "protagonist";
|
|
5696
|
+
else if (rawHint === void 0 && (name === narratorName$1 || name === narrator)) existing.role = "protagonist";
|
|
5630
5697
|
}
|
|
5631
5698
|
if (declared.role !== void 0) existing.role = normalizeRole(declared.role, existing.role);
|
|
5632
5699
|
if (declared.action === "updated") {
|
|
@@ -5809,7 +5876,7 @@ function applyStateDelta(input) {
|
|
|
5809
5876
|
setOwn(locks.characters, name, {
|
|
5810
5877
|
first_appearance: character.first_appearance,
|
|
5811
5878
|
locked_at: chapter,
|
|
5812
|
-
appearance: appearanceOf(input.text, name, name
|
|
5879
|
+
appearance: appearanceOf(input.text, name, narratorAliasesFor(name, narrator))
|
|
5813
5880
|
});
|
|
5814
5881
|
}
|
|
5815
5882
|
for (const location of world.locations) if (!Object.hasOwn(locks.locations, location.name)) setOwn(locks.locations, location.name, {
|
|
@@ -6330,8 +6397,8 @@ function buildGuide(input) {
|
|
|
6330
6397
|
},
|
|
6331
6398
|
{
|
|
6332
6399
|
id: "write",
|
|
6333
|
-
title: input.finished ? "全书章节已写完" : `写第 ${input.chapter} 章`,
|
|
6334
|
-
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `当前阶段 ${input.stage};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6400
|
+
title: input.finished ? "全书章节已写完" : input.progress?.current !== null && input.progress?.current !== void 0 ? `写第 ${input.progress.current.chapter} 章` : input.progress?.next !== null && input.progress?.next !== void 0 ? `准备第 ${input.progress.next.chapter} 章(P1)` : `写第 ${input.chapter} 章`,
|
|
6401
|
+
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `${input.progress?.label ?? `当前阶段 ${input.stage}`};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6335
6402
|
done: input.finished,
|
|
6336
6403
|
tool: input.next.tool
|
|
6337
6404
|
},
|
|
@@ -6345,7 +6412,7 @@ function buildGuide(input) {
|
|
|
6345
6412
|
];
|
|
6346
6413
|
return {
|
|
6347
6414
|
state: input.finished ? "finished" : input.stateErrors > 0 || input.blocking > 0 ? "blocked" : input.stage === "idle" && input.archived === 0 && !outlineDone && !blueprintDone && !worldbookDone ? "new" : input.stage === "idle" && input.archived === 0 ? "planning" : "writing",
|
|
6348
|
-
title: input.finished ? "全书已完成" : `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6415
|
+
title: input.finished ? "全书已完成" : input.progress?.label ?? `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6349
6416
|
steps,
|
|
6350
6417
|
next: input.next,
|
|
6351
6418
|
commands: [
|
|
@@ -8015,11 +8082,8 @@ const summarySchema = (input) => {
|
|
|
8015
8082
|
}
|
|
8016
8083
|
}
|
|
8017
8084
|
if (textArray(summary.characters_appeared)) {
|
|
8018
|
-
const narrator = input.pipeline.draft?.narrator ?? "";
|
|
8019
|
-
for (const name of summary.characters_appeared) {
|
|
8020
|
-
const alias = narrator.includes(name) && (narrator.includes("第一人称") || narrator.includes("「我」")) && input.text.includes("我");
|
|
8021
|
-
if (!input.text.includes(name) && !alias) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8022
|
-
}
|
|
8085
|
+
const narrator = input.narrator ?? input.pipeline.draft?.narrator ?? "";
|
|
8086
|
+
for (const name of summary.characters_appeared) if (!isNarratorAlias(name, narrator, input.text)) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8023
8087
|
}
|
|
8024
8088
|
const resources = summary.resource_changes;
|
|
8025
8089
|
if (!Array.isArray(resources) || !resources.every((item) => typeof item === "object" && item !== null && isText(item.character) && isText(item.resource) && Number.isInteger(item.delta) && item.delta !== 0 && isText(item.reason))) problems.push("resource_changes must contain character/resource/non-zero delta/reason");
|
|
@@ -8363,12 +8427,67 @@ function briefStageFor(operation, stage) {
|
|
|
8363
8427
|
}
|
|
8364
8428
|
}
|
|
8365
8429
|
// src/engine/projections.ts
|
|
8430
|
+
/**
|
|
8431
|
+
* Turn the protocol pair (`chapter` + `stage`) into the only human-readable
|
|
8432
|
+
* chapter boundary. After archiving chapter N the protocol intentionally keeps
|
|
8433
|
+
* `stage: 'done'` and advances `chapter` to N+1; `done` describes chapter N,
|
|
8434
|
+
* not the chapter the pipeline now points at.
|
|
8435
|
+
*/
|
|
8436
|
+
function projectChapterProgress(input) {
|
|
8437
|
+
const archived = [...new Set(input.archived)].filter((num) => Number.isInteger(num) && num > 0).sort((left, right) => left - right);
|
|
8438
|
+
const lastArchived = archived.length === 0 ? null : archived[archived.length - 1];
|
|
8439
|
+
const finished = input.stage === "done" && input.target !== null && archived.length >= input.target;
|
|
8440
|
+
const base = {
|
|
8441
|
+
active_chapter: input.activeChapter,
|
|
8442
|
+
stage: input.stage,
|
|
8443
|
+
last_archived: lastArchived,
|
|
8444
|
+
finished
|
|
8445
|
+
};
|
|
8446
|
+
if (finished) return {
|
|
8447
|
+
...base,
|
|
8448
|
+
current: null,
|
|
8449
|
+
next: null,
|
|
8450
|
+
label: `全书完成:已归档 ${archived.length} 章`,
|
|
8451
|
+
resume: `全书已完成(已归档 ${archived.length} 章);如需续写,先用 novel_amend 提高 targets.total_chapters。`
|
|
8452
|
+
};
|
|
8453
|
+
if (input.stage === "done") {
|
|
8454
|
+
if (lastArchived === null) return {
|
|
8455
|
+
...base,
|
|
8456
|
+
current: null,
|
|
8457
|
+
next: null,
|
|
8458
|
+
label: `当前第 ${input.activeChapter} 章 · 阶段 done(尚无归档章)`,
|
|
8459
|
+
resume: `引擎处于 done,但章节索引里还没有已归档章;先核对项目状态。`
|
|
8460
|
+
};
|
|
8461
|
+
const nextChapter = input.activeChapter > lastArchived ? input.activeChapter : lastArchived + 1;
|
|
8462
|
+
return {
|
|
8463
|
+
...base,
|
|
8464
|
+
current: null,
|
|
8465
|
+
next: {
|
|
8466
|
+
chapter: nextChapter,
|
|
8467
|
+
stage: "P1"
|
|
8468
|
+
},
|
|
8469
|
+
label: `第 ${lastArchived} 章已归档 · 下一章第 ${nextChapter} 章待开始(P1)`,
|
|
8470
|
+
resume: `第 ${lastArchived} 章已归档;当前不处于写作阶段,下一章第 ${nextChapter} 章从 P1 开始。`
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
return {
|
|
8474
|
+
...base,
|
|
8475
|
+
current: {
|
|
8476
|
+
chapter: input.activeChapter,
|
|
8477
|
+
stage: input.stage
|
|
8478
|
+
},
|
|
8479
|
+
next: null,
|
|
8480
|
+
label: `第 ${input.activeChapter} 章 · 阶段 ${input.stage}`,
|
|
8481
|
+
resume: `当前第 ${input.activeChapter} 章,阶段 ${input.stage}。`
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8366
8484
|
/** One shared prompt template for `/novel resume` and the panel copy button. */
|
|
8367
8485
|
function formatResumePrompt(input) {
|
|
8486
|
+
if (input.progress?.finished === true) return "";
|
|
8368
8487
|
return [
|
|
8369
8488
|
`请继续《${input.title}》。`,
|
|
8370
8489
|
`项目根:${input.root}(第一次工具调用必须显式传 project_root=${input.root})。`,
|
|
8371
|
-
`当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8490
|
+
input.progress?.resume ?? `当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8372
8491
|
`下一步:${input.nextTool}。`,
|
|
8373
8492
|
"先调用 novel_status 核对项目根与书名,再按 novel_pipeline 的下一步执行;",
|
|
8374
8493
|
"只操作这个项目,不要读取或写入其他书。"
|
|
@@ -8434,8 +8553,11 @@ function projectSummary(snapshot) {
|
|
|
8434
8553
|
platform: snapshot.platform,
|
|
8435
8554
|
chapter: snapshot.chapter,
|
|
8436
8555
|
stage: snapshot.stage,
|
|
8556
|
+
...snapshot.progress === void 0 ? {} : { progress: snapshot.progress },
|
|
8437
8557
|
revision: snapshot.revision,
|
|
8438
8558
|
totalWords: snapshot.totalWords,
|
|
8559
|
+
...snapshot.archivedWords === void 0 ? {} : { archivedWords: snapshot.archivedWords },
|
|
8560
|
+
...snapshot.draftWords === void 0 ? {} : { draftWords: snapshot.draftWords },
|
|
8439
8561
|
totalChapters: snapshot.chapters.length,
|
|
8440
8562
|
averageScore: snapshot.averageScore ?? (snapshot.chapters.length === 0 ? 0 : snapshot.chapters.reduce((sum, chapter) => sum + chapter.score, 0) / snapshot.chapters.length),
|
|
8441
8563
|
...snapshot.volume === void 0 ? {} : { volume: snapshot.volume },
|
|
@@ -10206,6 +10328,12 @@ var EngineService = class {
|
|
|
10206
10328
|
*/
|
|
10207
10329
|
async statusFor(snapshot, chapter) {
|
|
10208
10330
|
const metrics = await this.metricsFor(snapshot, chapter);
|
|
10331
|
+
const progress = projectChapterProgress({
|
|
10332
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10333
|
+
stage: snapshot.pipeline.stage,
|
|
10334
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10335
|
+
target: snapshot.project.targets.total_chapters
|
|
10336
|
+
});
|
|
10209
10337
|
const journal = await this.journalBlocking(snapshot);
|
|
10210
10338
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
10211
10339
|
const pendingWarnings = pending.state === "complete" || pending.state === "untouched" ? [{
|
|
@@ -10229,6 +10357,7 @@ var EngineService = class {
|
|
|
10229
10357
|
active_chapter: snapshot.pipeline.chapter,
|
|
10230
10358
|
manuscript_hash: snapshot.pipeline.draft !== void 0 && snapshot.text !== null ? sha256(snapshot.text) : null,
|
|
10231
10359
|
stage: snapshot.pipeline.stage,
|
|
10360
|
+
progress,
|
|
10232
10361
|
rounds: { ...snapshot.pipeline.rounds },
|
|
10233
10362
|
blocking: [...this.effectiveBlocking(snapshot), ...journal],
|
|
10234
10363
|
...pendingWarnings.length === 0 ? {} : { warnings: pendingWarnings },
|
|
@@ -10540,6 +10669,12 @@ var EngineService = class {
|
|
|
10540
10669
|
}
|
|
10541
10670
|
async advise(input = {}) {
|
|
10542
10671
|
const snapshot = await this.load(input.root);
|
|
10672
|
+
const progress = projectChapterProgress({
|
|
10673
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10674
|
+
stage: snapshot.pipeline.stage,
|
|
10675
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10676
|
+
target: snapshot.project.targets.total_chapters
|
|
10677
|
+
});
|
|
10543
10678
|
const journal = await this.journalBlocking(snapshot);
|
|
10544
10679
|
const next = journal.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
10545
10680
|
tool: "novel_audit",
|
|
@@ -10550,6 +10685,7 @@ var EngineService = class {
|
|
|
10550
10685
|
data: {
|
|
10551
10686
|
stage: snapshot.pipeline.stage,
|
|
10552
10687
|
chapter: snapshot.pipeline.chapter,
|
|
10688
|
+
progress,
|
|
10553
10689
|
tool: next.tool,
|
|
10554
10690
|
args: next.args,
|
|
10555
10691
|
reason: next.reason,
|
|
@@ -10751,7 +10887,12 @@ var EngineService = class {
|
|
|
10751
10887
|
id: "pipeline",
|
|
10752
10888
|
severity: "ok",
|
|
10753
10889
|
title: "流水线",
|
|
10754
|
-
detail: `revision ${snapshot.pipeline.revision} ·
|
|
10890
|
+
detail: `revision ${snapshot.pipeline.revision} · ${projectChapterProgress({
|
|
10891
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10892
|
+
stage: snapshot.pipeline.stage,
|
|
10893
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10894
|
+
target: snapshot.project.targets.total_chapters
|
|
10895
|
+
}).label}`
|
|
10755
10896
|
});
|
|
10756
10897
|
for (const error of this.stateErrors(snapshot)) add({
|
|
10757
10898
|
id: `state:${error.path}`,
|
|
@@ -11124,7 +11265,13 @@ var EngineService = class {
|
|
|
11124
11265
|
const snapshot = await this.loadAt(selection);
|
|
11125
11266
|
await this.recordSelectionBestEffort(selection, snapshot);
|
|
11126
11267
|
const target = snapshot.project.targets.total_chapters;
|
|
11127
|
-
const
|
|
11268
|
+
const progress = projectChapterProgress({
|
|
11269
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
11270
|
+
stage: snapshot.pipeline.stage,
|
|
11271
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
11272
|
+
target
|
|
11273
|
+
});
|
|
11274
|
+
const finished = progress.finished;
|
|
11128
11275
|
const journalBlocking = await this.journalBlocking(snapshot);
|
|
11129
11276
|
const next = journalBlocking.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
11130
11277
|
tool: "novel_audit",
|
|
@@ -11136,6 +11283,7 @@ var EngineService = class {
|
|
|
11136
11283
|
projectPresent: true,
|
|
11137
11284
|
chapter: snapshot.pipeline.chapter,
|
|
11138
11285
|
stage: snapshot.pipeline.stage,
|
|
11286
|
+
progress,
|
|
11139
11287
|
archived: snapshot.chapters.chapters.length,
|
|
11140
11288
|
target,
|
|
11141
11289
|
finished,
|
|
@@ -11535,6 +11683,7 @@ var EngineService = class {
|
|
|
11535
11683
|
summary: input.summary,
|
|
11536
11684
|
draftTitle: input.chapter_title,
|
|
11537
11685
|
sceneType: input.scene_type,
|
|
11686
|
+
narrator: input.narrator,
|
|
11538
11687
|
planTargetWords: snapshot.plan?.target_words ?? null,
|
|
11539
11688
|
planCast: snapshot.plan?.cast === void 0 ? null : snapshot.plan.cast.map((member) => member.name)
|
|
11540
11689
|
});
|
|
@@ -12863,12 +13012,13 @@ var EngineService = class {
|
|
|
12863
13012
|
}
|
|
12864
13013
|
const summary = projectSummary(snapshot);
|
|
12865
13014
|
if (option.state !== "ok" && option.title !== "") summary.title = option.title;
|
|
12866
|
-
const resume = summary.next === void 0 ? void 0 : { text: formatResumePrompt({
|
|
13015
|
+
const resume = summary.next === void 0 || summary.progress?.finished === true ? void 0 : { text: formatResumePrompt({
|
|
12867
13016
|
title: summary.title,
|
|
12868
13017
|
root: summary.root,
|
|
12869
13018
|
chapter: summary.chapter,
|
|
12870
13019
|
stage: summary.stage,
|
|
12871
|
-
nextTool: summary.next.tool
|
|
13020
|
+
nextTool: summary.next.tool,
|
|
13021
|
+
...summary.progress === void 0 ? {} : { progress: summary.progress }
|
|
12872
13022
|
}) };
|
|
12873
13023
|
return {
|
|
12874
13024
|
projects,
|
|
@@ -12888,12 +13038,13 @@ var EngineService = class {
|
|
|
12888
13038
|
const nextTool = status.data.next?.tool;
|
|
12889
13039
|
return {
|
|
12890
13040
|
data: {
|
|
12891
|
-
text: nextTool === void 0 ? null : formatResumePrompt({
|
|
13041
|
+
text: nextTool === void 0 || status.data.progress?.finished === true ? null : formatResumePrompt({
|
|
12892
13042
|
title: status.data.project.title,
|
|
12893
13043
|
root: status.data.project.root,
|
|
12894
13044
|
chapter: status.data.chapter,
|
|
12895
13045
|
stage: status.data.stage,
|
|
12896
|
-
nextTool
|
|
13046
|
+
nextTool,
|
|
13047
|
+
...status.data.progress === void 0 ? {} : { progress: status.data.progress }
|
|
12897
13048
|
}),
|
|
12898
13049
|
selection: status.data.selection
|
|
12899
13050
|
},
|
|
@@ -12903,12 +13054,17 @@ var EngineService = class {
|
|
|
12903
13054
|
}
|
|
12904
13055
|
async stats(input = {}) {
|
|
12905
13056
|
const snapshot = await this.load(input.root);
|
|
12906
|
-
const
|
|
13057
|
+
const archivedWords = snapshot.chapters.chapters.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13058
|
+
const activeIndexed = snapshot.chapters.chapters.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13059
|
+
const draftWords = snapshot.text === null || activeIndexed || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13060
|
+
const totalWords = archivedWords + draftWords;
|
|
12907
13061
|
const scores = snapshot.chapters.chapters.map((chapter) => chapter.score);
|
|
12908
13062
|
const outline = outlineSummary(snapshot.outline, snapshot.chapters.chapters.length, snapshot.project.targets.total_chapters, snapshot.chapters.chapters.map((record) => record.num));
|
|
12909
13063
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
12910
13064
|
return {
|
|
12911
13065
|
totalWords,
|
|
13066
|
+
archivedWords,
|
|
13067
|
+
draftWords,
|
|
12912
13068
|
chapters: snapshot.chapters.chapters.length,
|
|
12913
13069
|
revision: snapshot.pipeline.revision,
|
|
12914
13070
|
rulepack: `${this.rulepack.name}@${this.rulepack.version}`,
|
|
@@ -13406,15 +13562,32 @@ var EngineService = class {
|
|
|
13406
13562
|
const archived = snapshot.chapters.chapters;
|
|
13407
13563
|
const archivedCurrent = archived.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13408
13564
|
const target = snapshot.project.targets.total_chapters;
|
|
13409
|
-
const
|
|
13410
|
-
|
|
13565
|
+
const progress = projectChapterProgress({
|
|
13566
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
13567
|
+
stage: snapshot.pipeline.stage,
|
|
13568
|
+
archived: archived.map((record) => record.num),
|
|
13569
|
+
target
|
|
13570
|
+
});
|
|
13571
|
+
const finished = progress.finished;
|
|
13572
|
+
const draftWords = snapshot.text === null || archivedCurrent || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13573
|
+
const archivedWords = archived.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13411
13574
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
13412
13575
|
const stateErrorCount = this.stateErrors(snapshot).length;
|
|
13413
|
-
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}`;
|
|
13414
|
-
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13415
|
-
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13416
13576
|
const journal = await readJournal(this.storeFor(snapshot.root));
|
|
13417
13577
|
const journalBlocking = await this.journalBlocking(snapshot);
|
|
13578
|
+
const journalTail = journal.rows[journal.rows.length - 1];
|
|
13579
|
+
const journalKey = sha256(stableJson({
|
|
13580
|
+
rows: journal.rows.length,
|
|
13581
|
+
tail: journalTail === void 0 ? null : [
|
|
13582
|
+
journalTail.seq,
|
|
13583
|
+
journalTail.at,
|
|
13584
|
+
journalTail.status
|
|
13585
|
+
],
|
|
13586
|
+
blocking: journalBlocking
|
|
13587
|
+
}));
|
|
13588
|
+
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}:${journalKey}`;
|
|
13589
|
+
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13590
|
+
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13418
13591
|
const outline = outlineSummary(snapshot.outline, archived.length, target, archived.map((record) => record.num));
|
|
13419
13592
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
13420
13593
|
const scores = archived.slice(-10).map((chapter) => chapter.score);
|
|
@@ -13569,6 +13742,7 @@ var EngineService = class {
|
|
|
13569
13742
|
platform: snapshot.project.platform,
|
|
13570
13743
|
chapter: snapshot.pipeline.chapter,
|
|
13571
13744
|
stage: snapshot.pipeline.stage,
|
|
13745
|
+
progress,
|
|
13572
13746
|
revision: snapshot.pipeline.revision,
|
|
13573
13747
|
rounds: snapshot.pipeline.rounds,
|
|
13574
13748
|
blocking,
|
|
@@ -13589,7 +13763,9 @@ var EngineService = class {
|
|
|
13589
13763
|
finished,
|
|
13590
13764
|
chapters: archived,
|
|
13591
13765
|
draft,
|
|
13592
|
-
totalWords:
|
|
13766
|
+
totalWords: archivedWords + draftWords,
|
|
13767
|
+
archivedWords,
|
|
13768
|
+
draftWords,
|
|
13593
13769
|
averageScore: archived.length === 0 ? 0 : archived.reduce((total, chapter) => total + chapter.score, 0) / archived.length,
|
|
13594
13770
|
phases: phaseStrip(snapshot.pipeline.stage, finished),
|
|
13595
13771
|
book,
|
|
@@ -13697,6 +13873,12 @@ var EngineService = class {
|
|
|
13697
13873
|
nextStep(snapshot) {
|
|
13698
13874
|
const stage = snapshot.pipeline.stage;
|
|
13699
13875
|
const chapter = snapshot.pipeline.chapter;
|
|
13876
|
+
const progress = projectChapterProgress({
|
|
13877
|
+
activeChapter: chapter,
|
|
13878
|
+
stage,
|
|
13879
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
13880
|
+
target: snapshot.project.targets.total_chapters
|
|
13881
|
+
});
|
|
13700
13882
|
if (snapshot.chapters.chapters.some((record) => record.num === chapter)) return {
|
|
13701
13883
|
tool: "novel_amend",
|
|
13702
13884
|
args: { path: joinRel(STATE_DIR, STATE_FILES.chapters) },
|
|
@@ -13747,8 +13929,7 @@ var EngineService = class {
|
|
|
13747
13929
|
};
|
|
13748
13930
|
}
|
|
13749
13931
|
const step = STAGE_NEXT[stage];
|
|
13750
|
-
|
|
13751
|
-
if (stage === "done" && target !== null && snapshot.chapters.chapters.length >= target) return {
|
|
13932
|
+
if (progress.finished) return {
|
|
13752
13933
|
tool: "novel_export",
|
|
13753
13934
|
args: { kind: "epub" },
|
|
13754
13935
|
reason: "全书已完成;可导出成书,或结束本书"
|
|
@@ -14356,6 +14537,7 @@ var EngineService = class {
|
|
|
14356
14537
|
briefStage: extras.briefStage ?? briefStageFor(operation, snapshot.pipeline.stage),
|
|
14357
14538
|
draftTitle: composeTitle(snapshot.pipeline.chapter, extras.draftTitle ?? snapshot.pipeline.draft?.title ?? snapshot.plan?.title ?? null, this.rulepack.format.title.pattern),
|
|
14358
14539
|
sceneType: extras.sceneType ?? snapshot.pipeline.draft?.scene_type ?? snapshot.plan?.scene_type ?? null,
|
|
14540
|
+
narrator: extras.narrator ?? snapshot.pipeline.draft?.narrator ?? snapshot.plan?.narrator ?? null,
|
|
14359
14541
|
summary: extras.summary ?? snapshot.pipeline.draft?.summary ?? snapshot.plan?.summary ?? null,
|
|
14360
14542
|
summaryProvided: extras.summaryProvided,
|
|
14361
14543
|
summaryOnly: extras.summaryOnly,
|
|
@@ -15446,6 +15628,12 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15446
15628
|
problems.push(`archived chapter ${record.num} has no previous outline card; restore its archive backup before repairing target_words/cast/archived_at`);
|
|
15447
15629
|
continue;
|
|
15448
15630
|
}
|
|
15631
|
+
for (const field of [
|
|
15632
|
+
"goal",
|
|
15633
|
+
"conflict",
|
|
15634
|
+
"hook",
|
|
15635
|
+
"notes"
|
|
15636
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${record.num} cannot change its ${field}`);
|
|
15449
15637
|
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${record.num} cannot change its target_words`);
|
|
15450
15638
|
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
15451
15639
|
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
@@ -15457,6 +15645,27 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15457
15645
|
for (const [num, card] of oldCards) if (card.status === "archived") expected.add(num);
|
|
15458
15646
|
const missing = [...expected].filter((num) => byNum.get(num)?.status !== "archived");
|
|
15459
15647
|
if (missing.length > 0) problems.push(`cannot verify archived cards while chapters.yaml is unreadable; keep archived cards for ${missing.join(", ")}`);
|
|
15648
|
+
for (const [num, before] of oldCards) {
|
|
15649
|
+
if (before.status !== "archived") continue;
|
|
15650
|
+
const card = byNum.get(num);
|
|
15651
|
+
if (card === void 0) continue;
|
|
15652
|
+
if (card.status !== "archived") problems.push(`archived chapter ${num} cannot be changed back to planned`);
|
|
15653
|
+
for (const field of [
|
|
15654
|
+
"title",
|
|
15655
|
+
"scene_type",
|
|
15656
|
+
"word_count",
|
|
15657
|
+
"score",
|
|
15658
|
+
"goal",
|
|
15659
|
+
"conflict",
|
|
15660
|
+
"hook",
|
|
15661
|
+
"notes"
|
|
15662
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${num} cannot change its ${field}`);
|
|
15663
|
+
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${num} cannot change its target_words`);
|
|
15664
|
+
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
15665
|
+
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
15666
|
+
if (beforeCast.length !== afterCast.length || afterCast.some((name) => !beforeCast.includes(name))) problems.push(`archived chapter ${num} cannot change its cast`);
|
|
15667
|
+
if (before.archived_at !== card.archived_at && !(before.archived_at === null && typeof card.archived_at === "string")) problems.push(`archived chapter ${num} cannot change archived_at`);
|
|
15668
|
+
}
|
|
15460
15669
|
}
|
|
15461
15670
|
return problems;
|
|
15462
15671
|
}
|
|
@@ -15847,9 +16056,11 @@ function createNovelTools(engine, options = {}) {
|
|
|
15847
16056
|
render: (_args, raw) => {
|
|
15848
16057
|
const value = payload(raw);
|
|
15849
16058
|
const active = value.active_chapter ?? value.chapter;
|
|
15850
|
-
const
|
|
16059
|
+
const progressLabel = value.progress?.label ?? `第 ${value.chapter} 章 · 阶段 ${value.stage}`;
|
|
15851
16060
|
return text([
|
|
15852
|
-
`《${value.project.title}
|
|
16061
|
+
`《${value.project.title}》`,
|
|
16062
|
+
`进度:${progressLabel}`,
|
|
16063
|
+
...active === value.chapter ? [] : [`字数查询:第 ${value.chapter} 章(活动章第 ${active} 章)`],
|
|
15853
16064
|
...value.selection === void 0 ? [] : [`项目根 ${value.selection.root} · 选择来源 ${value.selection.source}`],
|
|
15854
16065
|
...value.rounds === void 0 ? [] : [`轮次:评审 ${value.rounds.review} / 修订 ${value.rounds.revise} / 校验 ${value.rounds.check}`],
|
|
15855
16066
|
value.blocking.length === 0 ? "门控:全部通过" : `门控:${value.blocking.length} 条阻塞`,
|
|
@@ -16803,9 +17014,11 @@ const failure = (error) => {
|
|
|
16803
17014
|
const formatPercent = (value) => value === null || value === void 0 ? "—" : `${Math.round(value * 1e3) / 10}%`;
|
|
16804
17015
|
const statusText = async (engine) => {
|
|
16805
17016
|
const { data } = await engine.getStatus({});
|
|
17017
|
+
const progressLabel = data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`;
|
|
16806
17018
|
return [
|
|
16807
17019
|
`《${data.project.title}》 ${data.project.genre} · ${data.project.platform}`,
|
|
16808
|
-
|
|
17020
|
+
`进度:${progressLabel}`,
|
|
17021
|
+
...data.chapter === data.active_chapter ? [] : [`字数查询:第 ${data.chapter} 章(活动章第 ${data.active_chapter} 章)`],
|
|
16809
17022
|
...data.selection === void 0 ? [] : [`项目根 ${data.selection.root} · 选择来源 ${data.selection.source}`],
|
|
16810
17023
|
`轮次 评审 ${data.rounds.review} / 修订 ${data.rounds.revise} / 校验 ${data.rounds.check}`,
|
|
16811
17024
|
data.metrics === null ? "字数 —" : `字数 ${data.metrics.total_word_count}(汉字 ${data.metrics.cjk_chars} + 标点 ${data.metrics.punctuation_chars},含标点)`,
|
|
@@ -16852,11 +17065,11 @@ function createCommands(engine, host = {}) {
|
|
|
16852
17065
|
case "next": {
|
|
16853
17066
|
const { data } = await engine.advise({});
|
|
16854
17067
|
const reason = NEXT_DETAILS[data.tool] ?? data.reason;
|
|
16855
|
-
return ok(
|
|
17068
|
+
return ok(`进度:${data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`}\n下一步:${data.tool}\n理由:${reason}${data.blocking.length === 0 ? "" : `\n阻塞 ${data.blocking.length} 条`}`);
|
|
16856
17069
|
}
|
|
16857
17070
|
case "stats": {
|
|
16858
17071
|
const stats = await engine.stats({});
|
|
16859
|
-
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords} · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17072
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
16860
17073
|
}
|
|
16861
17074
|
case "outline": {
|
|
16862
17075
|
const status = await engine.getStatus({});
|
|
@@ -17000,8 +17213,10 @@ function createCommands(engine, host = {}) {
|
|
|
17000
17213
|
}
|
|
17001
17214
|
case "resume": {
|
|
17002
17215
|
const { data } = await engine.resumePrompt({});
|
|
17003
|
-
if (data.text
|
|
17004
|
-
|
|
17216
|
+
if (data.text !== null) return ok(data.text);
|
|
17217
|
+
const status = await engine.getStatus({});
|
|
17218
|
+
if (status.data.progress?.finished === true) return ok(status.data.progress.resume);
|
|
17219
|
+
return bad("当前项目没有可续写的下一步;先用 /novel status 查看项目状态。");
|
|
17005
17220
|
}
|
|
17006
17221
|
case "admin": {
|
|
17007
17222
|
if (rest.length === 0) return ok(adminHelp());
|
|
@@ -17061,7 +17276,7 @@ function createCommands(engine, host = {}) {
|
|
|
17061
17276
|
if (invocation.signal.aborted) return bad("已取消");
|
|
17062
17277
|
try {
|
|
17063
17278
|
const stats = await engine.stats({});
|
|
17064
|
-
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords} · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17279
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17065
17280
|
} catch (error) {
|
|
17066
17281
|
return failure(error);
|
|
17067
17282
|
}
|
|
@@ -17837,4 +18052,4 @@ function connectionFence(scope) {
|
|
|
17837
18052
|
}
|
|
17838
18053
|
if (Object.prototype.hasOwnProperty.call(candidate, "connection")) return candidate.connection;
|
|
17839
18054
|
}
|
|
17840
|
-
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
|
18055
|
+
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapterProgress, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
|
@@ -11,6 +11,23 @@ export interface BlockingDto {
|
|
|
11
11
|
detail: string;
|
|
12
12
|
hint?: string;
|
|
13
13
|
}
|
|
14
|
+
/** Chapter-boundary projection; absent on older payloads. */
|
|
15
|
+
export interface ProgressDto {
|
|
16
|
+
active_chapter: number;
|
|
17
|
+
stage: string;
|
|
18
|
+
current: {
|
|
19
|
+
chapter: number;
|
|
20
|
+
stage: string;
|
|
21
|
+
} | null;
|
|
22
|
+
last_archived: number | null;
|
|
23
|
+
next: {
|
|
24
|
+
chapter: number;
|
|
25
|
+
stage: 'P1';
|
|
26
|
+
} | null;
|
|
27
|
+
finished: boolean;
|
|
28
|
+
label: string;
|
|
29
|
+
resume: string;
|
|
30
|
+
}
|
|
14
31
|
export interface SummaryDto {
|
|
15
32
|
schemaVersion: number;
|
|
16
33
|
panelId: string;
|
|
@@ -21,8 +38,11 @@ export interface SummaryDto {
|
|
|
21
38
|
platform: string;
|
|
22
39
|
chapter: number;
|
|
23
40
|
stage: string;
|
|
41
|
+
progress?: ProgressDto;
|
|
24
42
|
revision: number;
|
|
25
43
|
totalWords: number;
|
|
44
|
+
archivedWords?: number;
|
|
45
|
+
draftWords?: number;
|
|
26
46
|
totalChapters: number;
|
|
27
47
|
/** Book-wide archived average; absent on older payloads. */
|
|
28
48
|
averageScore?: number;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -301,8 +301,8 @@ export interface CharacterDelta {
|
|
|
301
301
|
role?: string;
|
|
302
302
|
profile?: Record<string, unknown>;
|
|
303
303
|
current_state?: Record<string, unknown>;
|
|
304
|
+
/** Chapter is engine-assigned from the archive transaction, not caller-supplied. */
|
|
304
305
|
growth?: {
|
|
305
|
-
chapter: number;
|
|
306
306
|
change: string;
|
|
307
307
|
status_change: string;
|
|
308
308
|
};
|
|
@@ -972,6 +972,8 @@ export interface GateInput {
|
|
|
972
972
|
briefStage: BriefStage | null;
|
|
973
973
|
draftTitle: string | null;
|
|
974
974
|
sceneType: SceneType | null;
|
|
975
|
+
/** The submitted/active plan narrator; PG-05 uses it before a draft exists. */
|
|
976
|
+
narrator?: string | null;
|
|
975
977
|
summary: ChapterSummary | null;
|
|
976
978
|
/** P5 may omit `summary`; when omitted, the previous plan summary is reused. */
|
|
977
979
|
summaryProvided?: boolean;
|
|
@@ -1034,6 +1036,36 @@ export interface Selection {
|
|
|
1034
1036
|
title?: string;
|
|
1035
1037
|
created_at?: string;
|
|
1036
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* One human-readable projection of the pipeline's chapter boundary.
|
|
1041
|
+
*
|
|
1042
|
+
* `pipeline.chapter`/`stage` are protocol values: after archiving chapter N the
|
|
1043
|
+
* pipeline points at N+1 and still says `done` because `done` describes the
|
|
1044
|
+
* chapter that was just archived. This projection is the only place that turns
|
|
1045
|
+
* those values into "chapter N archived; chapter N+1 starts at P1".
|
|
1046
|
+
*/
|
|
1047
|
+
export interface ChapterProgress {
|
|
1048
|
+
active_chapter: number;
|
|
1049
|
+
stage: Stage;
|
|
1050
|
+
/** The chapter currently being written; null while `stage === 'done'`. */
|
|
1051
|
+
current: {
|
|
1052
|
+
chapter: number;
|
|
1053
|
+
stage: Stage;
|
|
1054
|
+
} | null;
|
|
1055
|
+
/** Highest archived chapter, or null before the first archive. */
|
|
1056
|
+
last_archived: number | null;
|
|
1057
|
+
/** Next chapter to start at P1; null while writing or after the book is finished. */
|
|
1058
|
+
next: {
|
|
1059
|
+
chapter: number;
|
|
1060
|
+
stage: 'P1';
|
|
1061
|
+
} | null;
|
|
1062
|
+
/** True only when the configured target has been reached at `stage === 'done'`. */
|
|
1063
|
+
finished: boolean;
|
|
1064
|
+
/** One line for status cards, commands and the panel. */
|
|
1065
|
+
label: string;
|
|
1066
|
+
/** Sentence used by the resume prompt instead of "当前第 N 章,阶段 done". */
|
|
1067
|
+
resume: string;
|
|
1068
|
+
}
|
|
1037
1069
|
/** The `getStatus` payload. */
|
|
1038
1070
|
export interface StatusReport {
|
|
1039
1071
|
project: {
|
|
@@ -1054,6 +1086,8 @@ export interface StatusReport {
|
|
|
1054
1086
|
/** SHA-256 of the active manuscript, or null when the stage has no draft. */
|
|
1055
1087
|
manuscript_hash: string | null;
|
|
1056
1088
|
stage: Stage;
|
|
1089
|
+
/** Chapter-boundary projection shared by every human-readable surface. */
|
|
1090
|
+
progress?: ChapterProgress;
|
|
1057
1091
|
rounds: RoundCounters;
|
|
1058
1092
|
blocking: BlockingItem[];
|
|
1059
1093
|
/** Non-blocking recovery notes, e.g. a recoverable pending transaction. */
|
|
@@ -1106,6 +1140,8 @@ export interface StatusReport {
|
|
|
1106
1140
|
export interface AdviceReport {
|
|
1107
1141
|
stage: Stage;
|
|
1108
1142
|
chapter: number;
|
|
1143
|
+
/** Same chapter-boundary projection as getStatus. */
|
|
1144
|
+
progress?: ChapterProgress;
|
|
1109
1145
|
tool: string;
|
|
1110
1146
|
args: Record<string, unknown>;
|
|
1111
1147
|
reason: string;
|
package/package.json
CHANGED