@waterwx/dsh-novel-forge 2.0.0-alpha.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +286 -285
  2. package/lib/client.js +19561 -19439
  3. package/lib/index.js +8159 -4456
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +0 -6
  6. package/lib/types/assets.d.ts +20 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +136 -27
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/llmLive.d.ts +31 -0
  14. package/lib/types/client/locales.d.ts +15 -70
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AiTag.d.ts +1 -0
  17. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  18. package/lib/types/client/panel/AuthorHome.d.ts +13 -11
  19. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  20. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  21. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  22. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  25. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  26. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  31. package/lib/types/client/panel/SettingsView.d.ts +12 -9
  32. package/lib/types/client/panel/ShelfView.d.ts +10 -1
  33. package/lib/types/client/panel/SubPage.d.ts +27 -0
  34. package/lib/types/client/panel/TensionView.d.ts +18 -0
  35. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  36. package/lib/types/client/panel/helpers.d.ts +5 -3
  37. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  38. package/lib/types/client/panel/toast.d.ts +2 -0
  39. package/lib/types/client/panel/views.d.ts +21 -35
  40. package/lib/types/client/run-status.d.ts +21 -0
  41. package/lib/types/client/sidebar-entry.d.ts +0 -2
  42. package/lib/types/docx-core.d.ts +16 -0
  43. package/lib/types/docx.d.ts +2 -3
  44. package/lib/types/engine.d.ts +78 -103
  45. package/lib/types/global-assets.d.ts +6 -0
  46. package/lib/types/home.d.ts +11 -0
  47. package/lib/types/index.d.ts +26 -12
  48. package/lib/types/llm-live.d.ts +70 -0
  49. package/lib/types/llm-retry.d.ts +38 -0
  50. package/lib/types/market-radar-scan.d.ts +18 -0
  51. package/lib/types/market-radar-sources.d.ts +31 -0
  52. package/lib/types/migrate-output.d.ts +25 -0
  53. package/lib/types/model-capability.d.ts +93 -0
  54. package/lib/types/output-budget.d.ts +86 -0
  55. package/lib/types/prompt-slots.d.ts +57 -0
  56. package/lib/types/prompting.d.ts +10 -0
  57. package/lib/types/protocol.d.ts +816 -601
  58. package/lib/types/revision.d.ts +76 -0
  59. package/lib/types/routes.d.ts +9 -3
  60. package/lib/types/run.d.ts +23 -1
  61. package/lib/types/settings-persist.d.ts +59 -0
  62. package/lib/types/snapshots.d.ts +46 -0
  63. package/lib/types/stage-contract.d.ts +58 -0
  64. package/lib/types/tension.d.ts +53 -0
  65. package/lib/types/timeline.d.ts +37 -0
  66. package/package.json +50 -25
  67. package/src/action-guard.ts +127 -0
  68. package/src/ai-scan.ts +0 -24
  69. package/src/assets.ts +1801 -879
  70. package/src/assistant.ts +1098 -770
  71. package/src/author-assets.ts +131 -147
  72. package/src/bookshelf.ts +193 -167
  73. package/src/client/api.ts +767 -617
  74. package/src/client/chip-prefs.ts +121 -0
  75. package/src/client/docx.ts +32 -69
  76. package/src/client/index.ts +2 -0
  77. package/src/client/llmLive.ts +111 -0
  78. package/src/client/locales.ts +15 -274
  79. package/src/client/mount.tsx +3 -2
  80. package/src/client/panel/AdaptModeView.tsx +535 -520
  81. package/src/client/panel/AiTag.tsx +7 -0
  82. package/src/client/panel/AssetsTab.tsx +448 -458
  83. package/src/client/panel/AssistantTab.tsx +355 -301
  84. package/src/client/panel/AuthorAssetsView.tsx +205 -207
  85. package/src/client/panel/AuthorHome.tsx +309 -204
  86. package/src/client/panel/BookAnalysisView.tsx +63 -0
  87. package/src/client/panel/CmdPalette.tsx +102 -0
  88. package/src/client/panel/CreateBookView.tsx +366 -352
  89. package/src/client/panel/DirectorView.tsx +200 -0
  90. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
  91. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  92. package/src/client/panel/ImportModal.tsx +302 -302
  93. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  94. package/src/client/panel/LiveFeedLog.tsx +204 -0
  95. package/src/client/panel/MarketRadarView.tsx +415 -0
  96. package/src/client/panel/ModelManager.tsx +205 -205
  97. package/src/client/panel/NovelPanel.tsx +3144 -2205
  98. package/src/client/panel/PageHeader.tsx +37 -0
  99. package/src/client/panel/ProgressConsole.tsx +63 -64
  100. package/src/client/panel/PromptSlotsView.tsx +97 -0
  101. package/src/client/panel/ReaderView.tsx +306 -306
  102. package/src/client/panel/RunPanel.tsx +56 -24
  103. package/src/client/panel/SettingsView.tsx +175 -106
  104. package/src/client/panel/ShelfView.tsx +63 -39
  105. package/src/client/panel/SubPage.tsx +42 -0
  106. package/src/client/panel/TensionView.tsx +212 -0
  107. package/src/client/panel/TimelineView.tsx +208 -0
  108. package/src/client/panel/WorldTab.tsx +202 -202
  109. package/src/client/panel/helpers.ts +7 -5
  110. package/src/client/panel/market-radar-state.ts +79 -0
  111. package/src/client/panel/panel.module.css +5767 -4360
  112. package/src/client/panel/toast.ts +51 -0
  113. package/src/client/panel/views.tsx +316 -314
  114. package/src/client/run-status.ts +171 -0
  115. package/src/client/sidebar-entry.ts +3 -3
  116. package/src/docx-core.ts +53 -0
  117. package/src/docx.ts +44 -83
  118. package/src/engine.ts +1197 -1638
  119. package/src/global-assets.ts +58 -0
  120. package/src/home.ts +33 -0
  121. package/src/index.ts +181 -101
  122. package/src/llm-live.ts +245 -0
  123. package/src/llm-retry.ts +77 -0
  124. package/src/market-radar-scan.ts +35 -0
  125. package/src/market-radar-sources.ts +173 -0
  126. package/src/migrate-output.ts +76 -0
  127. package/src/model-capability.ts +215 -0
  128. package/src/novel-context.ts +286 -287
  129. package/src/output-budget.ts +147 -0
  130. package/src/prompt-slots.ts +125 -0
  131. package/src/prompting.ts +26 -0
  132. package/src/protocol.ts +835 -614
  133. package/src/revision.ts +283 -0
  134. package/src/routes.ts +1286 -910
  135. package/src/run.ts +113 -17
  136. package/src/settings-persist.ts +114 -0
  137. package/src/snapshots.ts +174 -0
  138. package/src/stage-contract.ts +186 -0
  139. package/src/tension.ts +251 -0
  140. package/src/timeline.ts +159 -0
  141. package/assets/styles/arcane-thick-3d.webp +0 -0
  142. package/assets/styles/chinese-ink.webp +0 -0
  143. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  144. package/assets/styles/dark-ghibli.webp +0 -0
  145. package/assets/styles/dark-gothic-3d.webp +0 -0
  146. package/assets/styles/film-photo.webp +0 -0
  147. package/assets/styles/guofeng-heavy.webp +0 -0
  148. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  149. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  150. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  151. package/assets/styles/horror-game-3d.webp +0 -0
  152. package/assets/styles/hyperreal-3d.webp +0 -0
  153. package/assets/styles/japan-cel-dark.webp +0 -0
  154. package/assets/styles/korean-webtoon.webp +0 -0
  155. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  156. package/assets/styles/nextgen-cg.webp +0 -0
  157. package/assets/styles/noir-film.webp +0 -0
  158. package/assets/styles/openworld-survival.webp +0 -0
  159. package/assets/styles/paper-cut-3d.webp +0 -0
  160. package/assets/styles/pixar-adult-3d.webp +0 -0
  161. package/assets/styles/pop-marvel-2d.webp +0 -0
  162. package/assets/styles/sandiao-qban.webp +0 -0
  163. package/assets/styles/shinkai-anime.webp +0 -0
  164. package/assets/styles/tactical-stealth.webp +0 -0
  165. package/assets/styles/urban-lightreal-3d.webp +0 -0
  166. package/assets/styles/vaporwave-3d.webp +0 -0
  167. package/assets/styles/vhs90.webp +0 -0
  168. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  169. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  170. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  171. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  172. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  173. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  174. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  175. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  176. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  177. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  178. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  179. package/lib/types/comic-presets.d.ts +0 -12
  180. package/lib/types/manga-genre-rules.d.ts +0 -22
  181. package/lib/types/prompt-utils.d.ts +0 -35
  182. package/lib/types/review-policy.d.ts +0 -97
  183. package/lib/types/shot-language.d.ts +0 -46
  184. package/lib/types/story-beat-language.d.ts +0 -35
  185. package/lib/types/style-library.d.ts +0 -33
  186. package/skills/chapter-batch/SKILL.md +0 -81
  187. package/src/client/mount.tsx._new +0 -97
  188. package/src/client/mount.tsx._nf +0 -97
  189. package/src/client/mount.tsx._tmp +0 -97
  190. package/src/client/panel/BookshelfBar.tsx +0 -170
  191. package/src/client/panel/FlowGuide.tsx +0 -84
  192. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  193. package/src/client/panel/MangaWorkspace.tsx +0 -715
  194. package/src/client/panel/ProgressHomeView.tsx +0 -87
  195. package/src/client/panel/PropLibrary.tsx +0 -175
  196. package/src/client/panel/SceneLibrary.tsx +0 -453
  197. package/src/client/panel/StoryboardTab.tsx +0 -472
  198. package/src/client/panel/StyleCard.tsx +0 -90
  199. package/src/client/panel/StyleGallery.tsx +0 -58
  200. package/src/comic-presets.ts +0 -67
  201. package/src/manga-genre-rules.ts +0 -87
  202. package/src/prompt-utils.ts +0 -73
  203. package/src/review-policy.ts +0 -161
  204. package/src/shot-language.ts +0 -181
  205. package/src/story-beat-language.ts +0 -130
  206. package/src/style-library.ts +0 -84
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 修订指令合并层(arbitration)。
3
+ *
4
+ * 问题:审稿、时间线、张力三条通道各自给建议,如果各自改一轮,会出现「改完 A 又违反 B」
5
+ * 的互相打脸:同一章可能被改三遍,最后一轮的改动把第一轮的修正覆盖掉。
6
+ *
7
+ * 规则(与面板/生产单共享同一份实现,避免两头各写一套):
8
+ * 1. 同一章只交付**一份**修订指令(本模块负责汇总,调用方不得自行拼装)。
9
+ * 2. 固定优先级,不做权重打分(可解释、可复现):
10
+ * 0 审稿 high(必改,是硬门)
11
+ * 1 时间线 high(硬矛盾:时间倒流、地点瞬移)
12
+ * 2 张力 medium(曲线偏差,建议性最强)
13
+ * 3 其余(审稿 medium / 时间线 medium)
14
+ * 4 低优先(审稿 low)
15
+ * 3. 一次修订最多吃 REVISION_ITEM_CAP 条,超出按优先级截断;截断数量显式回传,不静默丢弃。
16
+ * 4. 每章修订轮次上限 MAX_REVISION_ROUNDS,超出转人工待办。
17
+ * 5. 验证基准 = 本轮实际下发的全部条目(审稿 + 时间线 + 张力),
18
+ * 这样「复核」核对的就是「我们要求改的东西」,而不是只核对审稿那部分。
19
+ */
20
+ import type { ProjectState, ReviewIssue, ReviewReport, RevisionItem, RevisionSource, Severity } from './protocol.js';
21
+ /** 每章修订轮次上限:两轮仍不过即转人工,不做「无限自动修改」。 */
22
+ export declare const MAX_REVISION_ROUNDS = 2;
23
+ /** 单次修订指令最多携带的条目数(防止指令过长把正文挤出上下文)。 */
24
+ export declare const REVISION_ITEM_CAP = 8;
25
+ export declare const REVISION_SOURCE_LABELS: Record<RevisionSource, string>;
26
+ /** 固定优先级:数值越小越先改。 */
27
+ export declare function revisionPriority(source: RevisionSource, severity: Severity): number;
28
+ /** 排序 + 去重 + 截断。去重时保留优先级更高的那条,并累加 count。 */
29
+ export declare function mergeRevisionItems(items: RevisionItem[], cap?: number): {
30
+ items: RevisionItem[];
31
+ omitted: number;
32
+ };
33
+ export interface RevisionCollectOptions {
34
+ /** 目标章;缺省表示全书(用于整本批量修订)。 */
35
+ chapterNo?: number;
36
+ /** 显式指定的审稿意见;缺省取该章已落盘的审稿报告。 */
37
+ reviewIssues?: ReviewIssue[];
38
+ /** 是否纳入时间线矛盾(默认纳入)。 */
39
+ includeTimeline?: boolean;
40
+ /** 是否纳入张力偏差(默认纳入)。 */
41
+ includeTension?: boolean;
42
+ }
43
+ /** 汇总一章(或全书)的三类建议。纯函数,不改项目状态。 */
44
+ export declare function collectRevisionItems(project: ProjectState, options?: RevisionCollectOptions): RevisionItem[];
45
+ export interface RevisionPlan {
46
+ /** 已排序、去重、截断后的条目。 */
47
+ items: RevisionItem[];
48
+ /** 下发给模型的修订指令(唯一入口,调用方不要自行拼装)。 */
49
+ instruction: string;
50
+ counts: Record<RevisionSource, number>;
51
+ /** 因超出上限被截断的条数(显式回传,便于提示作者)。 */
52
+ omitted: number;
53
+ /** 验证基准:把本轮下发的全部条目当作「上一轮意见」,复核时逐条核对。 */
54
+ baseline: ReviewReport;
55
+ }
56
+ export interface BuildRevisionPlanOptions extends RevisionCollectOptions {
57
+ /** 审稿基线报告(提供后 baseline 会保留其分数与时间)。 */
58
+ baseReport?: ReviewReport;
59
+ /** 指令抬头(默认「按合并建议修订」)。 */
60
+ title?: string;
61
+ cap?: number;
62
+ }
63
+ /** 渲染修订指令。格式与历史「按审稿意见修订」保持同族,模型无需重新学习。 */
64
+ export declare function renderRevisionInstruction(items: RevisionItem[], title?: string): string;
65
+ /**
66
+ * 构造验证基准:把时间线/张力条目转成「上一轮意见」的编号项,
67
+ * 与审稿意见合并成一份报告。复核模型据此逐条核对,避免
68
+ * 「审稿说已解决、时间线说没改」这类互相打架的判定。
69
+ */
70
+ export declare function buildRevisionBaseline(items: RevisionItem[], base?: ReviewReport): ReviewReport;
71
+ /** 汇总 → 排序 → 指令 → 基准。面板与生产单都走这里。 */
72
+ export declare function buildRevisionPlan(project: ProjectState, options?: BuildRevisionPlanOptions): RevisionPlan;
73
+ /** 一行摘要,用于日志与面板提示("审稿 3 · 时间线 1 · 张力 1,另截断 2 条")。 */
74
+ export declare function describeRevisionPlan(plan: RevisionPlan): string;
75
+ /** 待办文本(生产单只提示不阻塞时写入项目待办,供作者人工决定)。 */
76
+ export declare function revisionTodoText(item: RevisionItem): string;
@@ -9,15 +9,21 @@
9
9
  */
10
10
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
11
11
  import type { Context } from '@deepseek-ai/cordis';
12
- import { type BibleResponse, type ConfigPatch, type ExportResponse, type LoadOutlineResponse, type NovelConfig, type PlanResponse, type StatusResponse, type VolumesResponse } from './protocol.ts';
12
+ import { type BibleResponse, type ConfigPatch, type ConfigResponse, type ExportResponse, type LoadOutlineResponse, type NovelConfig, type PlanResponse, type StatusResponse, type VolumesResponse } from './protocol.ts';
13
13
  import { chapterFileName } from './engine.ts';
14
14
  /** Route deps. */
15
15
  export interface NovelRoutesDeps {
16
16
  ctx: Context;
17
17
  /** Resolve the live plugin config (settings-aware). */
18
18
  getConfig: () => NovelConfig;
19
- /** Persist a config patch through the settings seam. */
20
- patchConfig: (patch: ConfigPatch) => Promise<NovelConfig>;
19
+ /** Persist a config patch through the settings seam (memory fallback included). */
20
+ patchConfig: (patch: ConfigPatch) => Promise<ConfigResponse>;
21
+ /** Raw settings config(未与激活书合并)——目录迁移判断默认目录是否需同步用。 */
22
+ rawConfig?: () => {
23
+ outputDir?: string;
24
+ };
25
+ /** Loader entry id the settings section is addressed by (undefined when unknown). */
26
+ settingsNs?: string;
21
27
  }
22
28
  /**
23
29
  * Build every /api/dsh-novel-forge route.
@@ -1,5 +1,17 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { NovelConfig, RunState } from './protocol.ts';
3
+ /**
4
+ * 解析某个输出目录的书名(用于旧 run-state.json 的兜底)。
5
+ * 优先项目文件里的 bookName,其次目录名——两者都没有时返回空串。
6
+ * @param outputDir - 该书输出目录。
7
+ */
8
+ export declare function resolveRunBookName(outputDir: string): string;
9
+ /**
10
+ * 一句话描述"正在跑的是哪本书的哪一章"。
11
+ * 面板状态行与 409 报错共用,避免两处各写各的(也便于单测覆盖)。
12
+ * @param state - 运行中的生产单状态。
13
+ */
14
+ export declare function describeRunningBatch(state: RunState): string;
3
15
  export declare class ProductionRunner {
4
16
  private deps;
5
17
  private state;
@@ -17,13 +29,23 @@ export declare class ProductionRunner {
17
29
  private persist;
18
30
  private log;
19
31
  /** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
20
- start(startNo: number, endNo: number): Promise<RunState>;
32
+ start(startNo: number, endNo: number, runDir?: string): Promise<RunState>;
21
33
  pause(): void;
22
34
  resume(): void;
23
35
  stop(): void;
36
+ /** 是否有生产单正在执行(目录迁移等磁盘级操作前须为 false)。 */
37
+ isWorking(): boolean;
24
38
  /** 主循环:从 currentNo 扫描到 endNo,逐章处理;支持暂停/停止。 */
25
39
  private loop;
26
40
  private processChapter;
41
+ /**
42
+ * 出章即核对曲线(建议性,不阻塞):
43
+ * 规则初筛得到的时间线矛盾 + 张力曲线偏差写进运行日志;本章已完结时再落成项目待办。
44
+ * 默认**不**触发任何自动修订——要看/要改由作者在面板发起「一键按建议修订」;
45
+ * 只有当本章因审稿 high 本来就要修订时,这些问题才搭同一轮车(见 handleRejected),
46
+ * 因此不额外增加轮次,也不会出现「改完甲又违反乙」。
47
+ */
48
+ private auditChapterAdvisories;
27
49
  /** 完整质量门:生成 → 摘要+事实 → 伏笔标记 → 审稿 → 作者复盘。 */
28
50
  private produce;
29
51
  /** 被拒分级处理:无 high 豁免;有 high 按意见修订(最多 2 轮)+ 验证模式;仍不过 → 待人工。 */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Config-patch persistence for the novel-forge plugin.
3
+ *
4
+ * Historically the plugin only knew how to write through the host settings
5
+ * service (`ctx.settings.update`) and treated the section's name as a constant.
6
+ * From Harness 0.2 the section is inferred from this plugin's exported `Config`
7
+ * and addressed by the **entry id** the Loader assigned — which the plugin does
8
+ * not control. A wrong id makes every save fail, so this module keeps the split
9
+ * explicit: apply the patch to the live config, then try to persist it, and
10
+ * report (rather than throw) when the host cannot.
11
+ */
12
+ /** Minimal structural view of the host settings service this plugin touches. */
13
+ export interface SettingsLike {
14
+ /** Persist one section patch; absent on hosts that expose read APIs only. */
15
+ update?: (ns: string, patch: Record<string, unknown>) => Promise<unknown>;
16
+ /** Live section registry; absence means the host cannot enumerate sections. */
17
+ describe?: () => unknown;
18
+ }
19
+ /** Where a config patch ended up. */
20
+ export interface ConfigPersistResult {
21
+ /** The merged config (live in memory regardless of persistence). */
22
+ config: Record<string, unknown>;
23
+ /**
24
+ * Why persistence did not happen, when it did not. Callers surface this to the
25
+ * author: a save that silently evaporates on restart is worse than a loud one.
26
+ */
27
+ settingsWarning?: string;
28
+ }
29
+ /**
30
+ * The Loader entry id the settings section is addressed by.
31
+ * @param ctx - plugin context (structurally typed: only `fiber.entry.options.id` is read).
32
+ * @returns the entry id, or undefined when the host exposes none.
33
+ */
34
+ export declare function novelSettingsNamespace(ctx: unknown): string | undefined;
35
+ /**
36
+ * Pick the settings section to write through.
37
+ *
38
+ * The entry id is authoritative; the declared legacy name is used only when the
39
+ * host actually registered a section under it. Guessing an id is never allowed:
40
+ * a write to a wrong section would persist another plugin's config.
41
+ * @param settings - the settings service, when present.
42
+ * @param entryNs - entry id resolved from the context, when available.
43
+ * @param legacyNs - this plugin's declared section name (0.1.x wiring).
44
+ * @returns the section id to write to, or undefined when none is addressable.
45
+ */
46
+ export declare function resolveSettingsTarget(settings: SettingsLike | undefined, entryNs: string | undefined, legacyNs: string): string | undefined;
47
+ /**
48
+ * Merge one patch into the live config and persist it when the host allows.
49
+ *
50
+ * Never throws for a persistence failure: the edit is valid and already applied,
51
+ * so failing the request would lose it and misreport a working save as an error.
52
+ * @param current - the config as currently resolved.
53
+ * @param patch - the subset to merge (`undefined` values are ignored by callers).
54
+ * @param settings - the settings service, when present.
55
+ * @param entryNs - entry id resolved from the context, when available.
56
+ * @param legacyNs - this plugin's declared section name.
57
+ * @returns the merged config plus a warning when it could not be persisted.
58
+ */
59
+ export declare function persistConfigPatch(current: Record<string, unknown>, patch: Record<string, unknown>, settings: SettingsLike | undefined, entryNs: string | undefined, legacyNs: string): Promise<ConfigPersistResult>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 章节历史版本(快照):覆盖写入前自动留存正文,支持列表 / 回滚 / 清理。
3
+ *
4
+ * 为什么需要它:生成、按意见重写、去 AI 味润色、采纳草稿都会**覆盖** `第N章_*.md`,
5
+ * 一旦改差(尤其是自动修订把好稿改坏)就没有退路。这里在覆盖前把旧稿存到
6
+ * `<书目录>/snapshots/`,并维护一份 `snapshots/index.json` 索引。
7
+ *
8
+ * 纯 Node 文件操作,不依赖模型;路由层只做参数校验与响应。
9
+ */
10
+ import type { ChapterPlan, ChapterSnapshot } from './protocol.ts';
11
+ /** 快照目录名(书目录下)。 */
12
+ export declare const SNAPSHOT_DIR = "snapshots";
13
+ /** 每章默认保留的快照数(超出后按时间淘汰最旧的)。 */
14
+ export declare const DEFAULT_SNAPSHOT_KEEP = 20;
15
+ /** 快照目录绝对路径。 */
16
+ export declare function snapshotDir(outputDir: string): string;
17
+ /** 读取索引(缺失/损坏时返回空数组)。 */
18
+ export declare function loadSnapshots(outputDir: string): ChapterSnapshot[];
19
+ /** 快照占用字节数(索引 + 正文)。 */
20
+ export declare function snapshotsBytes(outputDir: string): number;
21
+ /**
22
+ * 为一个章节留存快照(覆盖写入前调用)。
23
+ *
24
+ * @param outputDir 书目录。
25
+ * @param chapter 章节(需要已存在正文文件)。
26
+ * @param reason 留存原因(生成覆盖 / 采纳草稿 / 修订 / 润色 / 手工)。
27
+ * @param keep 每章保留数量(超出淘汰最旧)。
28
+ * @returns 新建的快照;无正文文件时返回 undefined(不产生空快照)。
29
+ */
30
+ export declare function createSnapshot(outputDir: string, chapter: ChapterPlan, reason: string, keep?: number): ChapterSnapshot | undefined;
31
+ /**
32
+ * 回滚到某个快照:把快照内容写回章节正文文件。
33
+ *
34
+ * 回滚前会**先给当前正文留一份快照**(reason=回滚前自动留存),所以回滚本身也可撤销。
35
+ *
36
+ * @param outputDir 书目录。
37
+ * @param chapter 目标章节。
38
+ * @param snapshotId 快照 id。
39
+ * @returns 回滚后的字符数。
40
+ * @throws 快照不存在或章节无正文文件时抛错。
41
+ */
42
+ export declare function restoreSnapshot(outputDir: string, chapter: ChapterPlan, snapshotId: string): number;
43
+ /** 删除一个快照(连同文件)。 */
44
+ export declare function removeSnapshot(outputDir: string, snapshotId: string): boolean;
45
+ /** 按每章保留数量清理(连文件一起删)。 */
46
+ export declare function pruneSnapshots(outputDir: string, keep?: number): number;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 阶段契约(Stage Contract)——宿主计算的「当前阶段 → 本轮允许的动作」。
3
+ *
4
+ * 设计来源(「作者请求中的当前阶段决定唯一允许命令」这一约定):
5
+ * 允许的动作必须由宿主根据真实项目状态算出,而不是指望模型记住或自律。
6
+ * 宿主计算结果同时用于三处:
7
+ * 1) 注入助手系统提示词:模型知道自己在哪一步、下一步该做什么;
8
+ * 2) 通用续写指令(「继续 / 下一步」)下的写操作白名单强约束(阶段即唯一命令);
9
+ * 3) 拒绝写操作时给出可执行的、阶段相关的理由,而不是一句「不允许」。
10
+ *
11
+ * 注意:作者显式点名的写操作永远优先于阶段白名单(作者终审权);
12
+ * 阶段强约束只在作者没有指定具体对象(通用续写)时生效。
13
+ */
14
+ import type { ChapterPlan, ProjectState, StageInfo } from './protocol.ts';
15
+ /** 一个阶段契约(assistant 侧需要作为类型使用)。 */
16
+ export type BookStage = StageInfo;
17
+ /**
18
+ * 作者这一轮是否只说了「继续/下一步」这类没有具体对象的指令。
19
+ *
20
+ * @param message 本轮用户消息。
21
+ * @returns 是通用续写指令时为 true。
22
+ */
23
+ export declare function isGenericContinue(message: string): boolean;
24
+ /**
25
+ * 根据项目真实状态计算当前创作阶段。
26
+ *
27
+ * @param project 当前项目(未加载时为 undefined)。
28
+ * @returns 阶段契约(推荐动作 + 写操作白名单 + 判定理由)。
29
+ */
30
+ export declare function computeBookStage(project: ProjectState | undefined): BookStage;
31
+ /**
32
+ * 判断某写操作是否在当前阶段的允许白名单内。
33
+ *
34
+ * @param stage 阶段契约。
35
+ * @param tool 助手工具名。
36
+ * @returns 允许时为 true。
37
+ */
38
+ export declare function stageAllows(stage: BookStage, tool: string): boolean;
39
+ /**
40
+ * 按阶段把一组动作分成「本阶段可用」与「阶段外」。
41
+ *
42
+ * @param stage 阶段契约。
43
+ * @param tools 动作名列表(通常是写操作登记表)。
44
+ * @returns 两个保持输入顺序的子集。
45
+ */
46
+ export declare function splitByStage(stage: BookStage, tools: readonly string[]): {
47
+ available: string[];
48
+ outside: string[];
49
+ };
50
+ /**
51
+ * 渲染阶段契约为可注入提示词的一段文字。
52
+ *
53
+ * @param stage 阶段契约。
54
+ * @returns 多行文本。
55
+ */
56
+ export declare function renderStageContract(stage: BookStage): string;
57
+ /** 汇总章节状态计数(供面板/提示词展示,避免各处重复统计)。 */
58
+ export declare function summarizeChapters(chapters: ChapterPlan[]): Record<string, number>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 张力曲线:把「每章该有多紧」显式化,并在出章后核对实际走向。
3
+ *
4
+ * 为什么要有它:长篇最常见的两种崩法——**一直高能没有呼吸**(读者疲劳)与
5
+ * **长时间低位打转**(读者流失);这两种都不是单章问题,而是"曲线"问题,
6
+ * 单章审稿看不见。这里提供:
7
+ * 1) 参考曲线(本项目自行设计,仅作形状参照);
8
+ * 2) 把「本章目标张力」注入写作提示词;
9
+ * 3) 规则核对:实际 vs 目标偏差、连续同值、高潮后不回落、长期低位。
10
+ *
11
+ * 本模块只放纯逻辑(可离线单测);模型交互与落盘在 engine/routes。
12
+ */
13
+ import type { ChapterPlan, ProjectState, TensionCurvePreset, TensionIssue } from './protocol.ts';
14
+ /** 单章张力的取值范围。 */
15
+ export declare const TENSION_MIN = 0;
16
+ export declare const TENSION_MAX = 100;
17
+ /** 参考曲线的中文标签(面板展示)。 */
18
+ export declare const TENSION_PRESET_LABELS: Record<TensionCurvePreset, string>;
19
+ /** 全部可选预设(UI 下拉用)。 */
20
+ export declare const TENSION_PRESETS: readonly TensionCurvePreset[];
21
+ /** 把参考形状按章节数重采样(最近邻,保持形状不引入平滑失真)。 */
22
+ export declare function referenceCurve(preset: TensionCurvePreset, chapterCount: number): number[];
23
+ /** 读一章的目标张力(未设置时返回 undefined)。 */
24
+ export declare function targetTension(chapter: ChapterPlan): number | undefined;
25
+ /** 读一章的实际张力(审稿时模型打分,未设置时返回 undefined)。 */
26
+ export declare function actualTension(chapter: ChapterPlan): number | undefined;
27
+ /** 收敛到合法区间并取整。 */
28
+ export declare function clampTension(value: number): number;
29
+ /**
30
+ * 渲染注入写作提示词的「本章张力」块。
31
+ *
32
+ * @param project 项目状态。
33
+ * @param chapterNo 本章章号。
34
+ * @returns 提示词片段;没有张力信息时返回空串。
35
+ */
36
+ export declare function renderTensionBlock(project: ProjectState, chapterNo: number): string;
37
+ export declare function detectTensionIssues(project: ProjectState): TensionIssue[];
38
+ /**
39
+ * 组装面板需要的曲线数据(章节号 / 目标 / 实际 / 参考)。
40
+ *
41
+ * @param project 项目状态。
42
+ * @returns 逐章数据点与预设名。
43
+ */
44
+ export declare function buildCurveData(project: ProjectState): {
45
+ preset: TensionCurvePreset;
46
+ points: Array<{
47
+ no: number;
48
+ title: string;
49
+ target?: number;
50
+ actual?: number;
51
+ reference?: number;
52
+ }>;
53
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 故事时间线:把「章节正文」压成可排序的时间事件,用于后续章节的时间连续性。
3
+ *
4
+ * 与编年录的分工:编年录记「事实」(人物状态/资源/关系),时间线记「顺序」
5
+ * (什么时候、在哪里、谁在场、发生了什么),专门用来抓时间倒流、地点瞬移与
6
+ * 跨越章节的时序矛盾。
7
+ *
8
+ * 本模块只放纯逻辑(可离线单测):排序、注入渲染、规则初筛、事件归一化;
9
+ * 模型交互(抽取/复核)在 engine.ts,落盘与路由在 routes.ts。
10
+ */
11
+ import type { ProjectState, TimelineEvent, TimelineIssue } from './protocol.ts';
12
+ /** 注入后续章节的时间线锚点条数(超过则显式声明省略,遵循截断显式化规则)。 */
13
+ export declare const TIMELINE_CONTEXT_LIMIT = 12;
14
+ /** 归一化事件(清洗字符串、补 id/时间戳)。 */
15
+ export declare function normalizeTimelineEvent(raw: Partial<TimelineEvent>, chapterNo: number, index: number, now?: string): TimelineEvent;
16
+ /** 时间线排序:先章节、再 order(缺省排在该章末尾)、最后 createdAt。 */
17
+ export declare function sortTimeline(events: readonly TimelineEvent[]): TimelineEvent[];
18
+ /**
19
+ * 渲染注入到「写作/规划」提示词的时间线锚点块。
20
+ *
21
+ * @param project 项目状态。
22
+ * @param beforeChapter 只注入该章之前的锚点(含该章本身不注入,避免剧透)。
23
+ * @returns 提示词片段(无事件时返回空串)。带显式截断声明。
24
+ */
25
+ export declare function renderTimelineBlock(project: ProjectState, beforeChapter: number): string;
26
+ /**
27
+ * 规则初筛时间线矛盾(不调模型,瞬时计算)。
28
+ *
29
+ * 覆盖三类最容易犯又最容易用规则抓到的错:
30
+ * 1) 同一章内事件顺序号倒退;
31
+ * 2) 全书顺序号在章节推进后倒退(时间倒流);
32
+ * 3) 相邻两章之间地点瞬移(同一角色在未交代移动的情况下跨地点)。
33
+ *
34
+ * @param project 项目状态。
35
+ * @returns 问题清单(可能为空)。
36
+ */
37
+ export declare function detectTimelineIssues(project: ProjectState): TimelineIssue[];
package/package.json CHANGED
@@ -1,8 +1,32 @@
1
1
  {
2
2
  "name": "@waterwx/dsh-novel-forge",
3
- "description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
4
- "version": "2.0.0-alpha.1",
3
+ "description": "AI 编译纯文本小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan volumes & chapters with LLM, generate chapter by chapter (3000-4000 chars each), auto AI review & book audit, and export the full book as Markdown/TXT into your chosen output folder.",
4
+ "version": "2.0.0",
5
5
  "type": "module",
6
+ "author": "waterwx",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/watersxya/dsh-novel-forge.git"
10
+ },
11
+ "homepage": "https://github.com/watersxya/dsh-novel-forge",
12
+ "bugs": {
13
+ "url": "https://github.com/watersxya/dsh-novel-forge/issues"
14
+ },
15
+ "keywords": [
16
+ "dsh",
17
+ "dsh-plugin",
18
+ "deepseek-harness",
19
+ "小说工坊",
20
+ "novel-forge",
21
+ "novel-writing-workbench",
22
+ "novel-writing",
23
+ "写作助手",
24
+ "小说创作",
25
+ "网文写作",
26
+ "章节生成",
27
+ "AI审稿",
28
+ "story-writing"
29
+ ],
6
30
  "engines": {
7
31
  "node": "^22.19.0 || >=24.0.0"
8
32
  },
@@ -38,31 +62,31 @@
38
62
  "schemastery": "^3.18.0"
39
63
  },
40
64
  "peerDependencies": {
41
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.3",
42
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.3",
43
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
44
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
45
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.3",
46
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
47
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.3",
48
- "@deepseek-ai/dsh-skill": "^0.1.2-alpha.3",
49
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
50
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
65
+ "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
66
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
67
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
69
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
70
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
71
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
72
+ "@deepseek-ai/dsh-skill": "^0.2.0-rc.2",
73
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
74
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
51
75
  "react": "^18.2.0",
52
76
  "react-dom": "^18.2.0"
53
77
  },
54
78
  "devDependencies": {
55
79
  "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.3",
57
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.3",
58
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
60
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.3",
61
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
62
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.3",
63
- "@deepseek-ai/dsh-skill": "^0.1.2-alpha.3",
64
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
65
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
80
+ "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.3",
81
+ "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.3",
82
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.3",
83
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.3",
84
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.3",
85
+ "@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
86
+ "@deepseek-ai/dsh-settings": "0.1.2-alpha.3",
87
+ "@deepseek-ai/dsh-skill": "0.1.2-alpha.3",
88
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.3",
89
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
66
90
  "@types/node": "^22.20.0",
67
91
  "@types/react": "~18.3.1",
68
92
  "@types/react-dom": "^18.3.5",
@@ -77,8 +101,6 @@
77
101
  "lib/**/*.js",
78
102
  "lib/**/*.d.ts",
79
103
  "src",
80
- "assets/styles",
81
- "skills",
82
104
  "scripts/patch-schemastery.mjs",
83
105
  "cordis.patch.yml",
84
106
  "README.md"
@@ -86,11 +108,14 @@
86
108
  "license": "Apache-2.0",
87
109
  "scripts": {
88
110
  "postinstall": "node scripts/patch-schemastery.mjs",
89
- "build": "tsc -p tsconfig.build.json && tsdown",
111
+ "build": "node scripts/clean-types.mjs && tsc -p tsconfig.build.json && tsdown",
90
112
  "bundle": "tsdown",
91
113
  "watch": "tsdown --watch",
92
114
  "typecheck": "tsc --noEmit",
93
115
  "check:styles": "node scripts/check-theme-sizes.mjs",
116
+ "check:third-party": "node scripts/check-third-party.mjs",
117
+ "check:contrast": "node scripts/audit-contrast.mjs",
118
+ "check:third-party:list": "node scripts/check-third-party.mjs --list",
94
119
  "release": "node scripts/release.mjs",
95
120
  "release:dry": "node scripts/release.mjs --dry-run",
96
121
  "test": "vitest run"
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 动作失败分级(fail-stop 规则)——不依赖 LLM/宿主服务的纯逻辑模块,可离线单测。
3
+ *
4
+ * 规则来源(借鉴同类 DSH 小说插件的规则设计):
5
+ * - 参数/契约类失败**不得重复同一调用**,解释一次即停;
6
+ * - 瞬时故障允许有限重试,同样禁止无限重试。
7
+ * 规则必须可测试,否则只是文档。
8
+ */
9
+
10
+ /**
11
+ * 动作失败分级:
12
+ * - `contract` 参数/契约类(参数缺失、片段未找到、章节不存在、非法取值…):
13
+ * 同一调用只允许修正一次,禁止原样重复。
14
+ * - `transient` 瞬时故障(网络、模型中断、磁盘临时不可用…):允许重试一次。
15
+ */
16
+ export type ActionErrorKind = 'contract' | 'transient'
17
+
18
+ /** 带分级的动作失败(executeAction 抛出,供 fail-stop 判定)。 */
19
+ export class NovelActionError extends Error {
20
+ readonly kind: ActionErrorKind
21
+ constructor(kind: ActionErrorKind, message: string) {
22
+ super(message)
23
+ this.name = 'NovelActionError'
24
+ this.kind = kind
25
+ }
26
+ }
27
+
28
+ /** 瞬时故障特征(先判,避免被下面的契约词误伤)。 */
29
+ const TRANSIENT_PATTERNS: readonly RegExp[] = [
30
+ /助手调用失败/,
31
+ /生成失败|请求失败|调用失败/,
32
+ /network|fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|EAI_AGAIN|socket hang up|aborted|terminated/i,
33
+ /超时|timeout/i,
34
+ /频繁|限流|rate\s*limit|too many requests|\b429\b/i,
35
+ /ENOSPC|EACCES|EBUSY|EPERM|EMFILE/,
36
+ ]
37
+
38
+ /**
39
+ * 参数/契约类失败特征(兜底识别)。
40
+ *
41
+ * 宿主自己的前置条件失败已在抛出点标成 {@link NovelActionError}(引擎与助手动作层),
42
+ * 这里的正则只兜住漏标的历史消息与第三方错误。
43
+ */
44
+ const CONTRACT_PATTERNS: readonly RegExp[] = [
45
+ /动作参数不是合法 JSON/,
46
+ /未知工具/,
47
+ /需要/,
48
+ /不存在/,
49
+ /尚未生成/,
50
+ /未找到/,
51
+ /不在计划中/,
52
+ /正文文件不存在/,
53
+ /还没有已写章节/,
54
+ /没有可分析的已写章节/,
55
+ /非法/,
56
+ /无效/,
57
+ /必须是/,
58
+ /无法识别/,
59
+ /请先/,
60
+ ]
61
+
62
+ /**
63
+ * 判定一次动作失败的类别。
64
+ *
65
+ * @param error 捕获到的异常。
66
+ * @returns `contract`(参数/契约,不重试原样调用)或 `transient`(瞬时,可重试一次)。
67
+ */
68
+ export function classifyActionError(error: unknown): ActionErrorKind {
69
+ if (error instanceof NovelActionError) return error.kind
70
+ const message = error instanceof Error ? error.message : String(error)
71
+ if (TRANSIENT_PATTERNS.some(re => re.test(message))) return 'transient'
72
+ if (CONTRACT_PATTERNS.some(re => re.test(message))) return 'contract'
73
+ // 未知错误默认按瞬时处理:允许一次重试,但同一调用签名仍有次数上限(见 fail-stop)。
74
+ return 'transient'
75
+ }
76
+
77
+ /**
78
+ * 同一调用签名允许的总尝试次数(含首次)。
79
+ *
80
+ * @param kind 失败类别。
81
+ * @returns `contract` = 1(禁止原样重复),`transient` = 2(允许一次重试)。
82
+ */
83
+ export function actionRetryLimit(kind: ActionErrorKind): number {
84
+ return kind === 'contract' ? 1 : 2
85
+ }
86
+
87
+ /**
88
+ * 把动作参数规范化为稳定签名(键排序),用于识别「同一个调用」。
89
+ *
90
+ * @param args 动作参数。
91
+ * @returns 稳定字符串签名。
92
+ */
93
+ export function safeArgsKey(args: Record<string, unknown> | undefined): string {
94
+ if (args === undefined) return ''
95
+ try {
96
+ return JSON.stringify(Object.keys(args).sort().map(key => [key, args[key]]))
97
+ } catch {
98
+ return String(args)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * 已失败调用记账:同一签名达到重试上限即 fail-stop。
104
+ */
105
+ export class FailureLedger {
106
+ private readonly counts = new Map<string, number>()
107
+
108
+ /**
109
+ * 记录一次失败并判断是否应当停止本轮。
110
+ *
111
+ * @param tool 工具名。
112
+ * @param args 该次调用参数。
113
+ * @param kind 失败类别。
114
+ * @returns `attempts` 本次是第几次失败;`stop` 是否达到上限(true = 立即停止,不再喂回模型)。
115
+ */
116
+ record(tool: string, args: Record<string, unknown> | undefined, kind: ActionErrorKind): { attempts: number; stop: boolean } {
117
+ const signature = `${tool}:${safeArgsKey(args)}`
118
+ const attempts = (this.counts.get(signature) ?? 0) + 1
119
+ this.counts.set(signature, attempts)
120
+ return { attempts, stop: attempts >= actionRetryLimit(kind) }
121
+ }
122
+
123
+ /** 已登记的失败签名数量(测试/诊断用)。 */
124
+ get size(): number {
125
+ return this.counts.size
126
+ }
127
+ }