@waterwx/dsh-novel-forge 1.1.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -178,18 +178,19 @@ export function ShelfView({
178
178
  value={query}
179
179
  onChange={e => { setQuery(e.target.value) }}
180
180
  />
181
- <div className={css.shelfFilters}>
182
- {filters.map(f => (
183
- <button
184
- key={f.id}
185
- type="button"
186
- className={`${css.button} ${css.buttonSmall} ${filter === f.id ? css.buttonPrimary : ''}`}
187
- onClick={() => { setFilter(f.id) }}
188
- >
189
- {f.label}{f.count > 0 ? `(${f.count})` : ''}
190
- </button>
191
- ))}
192
- </div>
181
+ </div>
182
+ <div className={css.shelfFilters} style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
183
+ {filters.map(f => (
184
+ <button
185
+ key={f.id}
186
+ type="button"
187
+ className={`${css.button} ${filter === f.id ? css.buttonPrimary : ''}`}
188
+ style={{ fontSize: 14, flex: 1 }}
189
+ onClick={() => { setFilter(f.id) }}
190
+ >
191
+ {f.label}{f.count > 0 ? `(${f.count})` : ''}
192
+ </button>
193
+ ))}
193
194
  </div>
194
195
  </div>
195
196
 
@@ -96,8 +96,8 @@ body[data-ds-dark-theme] .entry {
96
96
  --nf-glass-sheen: rgba(255, 255, 255, 0.55);
97
97
  --nf-glass-border: rgba(255, 255, 255, 0.65);
98
98
  --nf-glass-inset: rgba(255, 255, 255, 0.55);
99
- --nf-glass-orb-a: rgba(0, 184, 92, 0.14);
100
- --nf-glass-orb-b: rgba(120, 160, 255, 0.10);
99
+ --nf-glass-orb-a: rgba(0, 184, 92, 0.18);
100
+ --nf-glass-orb-b: rgba(120, 160, 255, 0.13);
101
101
  --nf-glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
102
102
  background: var(--nf-bg);
103
103
  /* Frosted glass: blur whatever is behind the panel (the chat dialog),
@@ -138,8 +138,8 @@ body[data-ds-dark-theme] .view {
138
138
  --nf-glass-sheen: rgba(255, 255, 255, 0.26);
139
139
  --nf-glass-border: rgba(255, 255, 255, 0.14);
140
140
  --nf-glass-inset: rgba(255, 255, 255, 0.07);
141
- --nf-glass-orb-a: rgba(0, 202, 82, 0.20);
142
- --nf-glass-orb-b: rgba(190, 200, 215, 0.07);
141
+ --nf-glass-orb-a: rgba(0, 202, 82, 0.28);
142
+ --nf-glass-orb-b: rgba(190, 200, 215, 0.10);
143
143
  --nf-glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
144
144
  }
145
145
 
@@ -334,23 +334,6 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
334
334
  radial-gradient(120% 95% at 50% 42%, transparent 55%, rgba(2, 2, 3, 0.10) 100%);
335
335
  }
336
336
 
337
- .panelHeader {
338
- display: flex;
339
- align-items: center;
340
- justify-content: space-between;
341
- margin: 10px 12px 0;
342
- padding: 10px 16px;
343
- border: 1px solid var(--nf-glass-border);
344
- border-radius: 18px;
345
- flex-shrink: 0;
346
- background: var(--nf-glass-face);
347
- box-shadow: var(--nf-shadow), inset 0 1px 0 var(--nf-glass-sheen);
348
- -webkit-backdrop-filter: blur(28px) saturate(175%);
349
- backdrop-filter: blur(28px) saturate(175%);
350
- /* 固定在面板顶部(panel 为 flex 列布局,header 正常占位),
351
- 下方内容在 panelBody 内独立滚动,header 永不遮挡内容。 */
352
- }
353
-
354
337
  .panelTitle {
355
338
  margin: 0;
356
339
  font-size: 16px;
@@ -456,15 +439,15 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
456
439
 
457
440
  .panelNav {
458
441
  flex: none;
459
- align-self: flex-start;
460
- width: 142px;
442
+ align-self: stretch;
443
+ width: 200px;
461
444
  display: flex;
462
445
  flex-direction: column;
463
446
  gap: 4px;
464
447
  margin: 10px 0 10px 12px;
465
- padding: 10px 8px;
448
+ padding: 14px 10px 12px;
466
449
  border: 1px solid var(--nf-glass-border);
467
- border-radius: 16px;
450
+ border-radius: 18px;
468
451
  background: var(--nf-glass-face);
469
452
  box-shadow: var(--nf-shadow), inset 0 1px 0 var(--nf-glass-sheen);
470
453
  -webkit-backdrop-filter: blur(28px) saturate(175%);
@@ -490,7 +473,7 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
490
473
  letter-spacing: 0.6px;
491
474
  text-transform: uppercase;
492
475
  color: var(--nf-text-3);
493
- padding: 8px 10px 3px;
476
+ padding: 10px 10px 4px;
494
477
  user-select: none;
495
478
  }
496
479
 
@@ -622,15 +605,15 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
622
605
  align-items: center;
623
606
  gap: 8px;
624
607
  width: 100%;
625
- border: none;
608
+ border: 1px solid transparent;
626
609
  background: transparent;
627
610
  cursor: pointer;
628
- border-radius: 12px;
629
- padding: 8px 10px;
611
+ border-radius: 10px;
612
+ padding: 9px 10px;
630
613
  font-size: 13px;
631
614
  color: var(--nf-text-2-alpha, var(--nf-text-2));
632
615
  font-weight: 500;
633
- transition: color 0.15s ease, background 0.15s ease;
616
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
634
617
  position: relative;
635
618
  text-align: left;
636
619
  white-space: nowrap;
@@ -649,27 +632,20 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
649
632
  .navTab[data-active] {
650
633
  color: var(--nf-accent);
651
634
  font-weight: 600;
652
- background: var(--nf-accent-soft);
653
- }
654
-
655
- .navTab[data-active]::before {
656
- content: '';
657
- position: absolute;
658
- left: -8px;
659
- top: 20%;
660
- bottom: 20%;
661
- width: 3px;
662
- border-radius: 999px;
663
- background: var(--nf-accent);
635
+ background: color-mix(in srgb, var(--nf-text) 8%, transparent);
636
+ border-color: color-mix(in srgb, var(--nf-text) 16%, transparent);
637
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--nf-text) 6%, transparent);
664
638
  }
665
639
 
666
- .panelNavCollapsed .navTab[data-active]::before {
667
- left: -6px;
640
+ .navTab[data-active] .navTabIcon {
641
+ color: var(--nf-accent);
668
642
  }
669
643
 
670
644
  .navTabIcon {
671
645
  flex: none;
672
- font-size: 15px;
646
+ width: 20px;
647
+ text-align: center;
648
+ font-size: 16px;
673
649
  line-height: 1;
674
650
  }
675
651
 
@@ -678,43 +654,183 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
678
654
  text-overflow: ellipsis;
679
655
  }
680
656
 
681
- /* 顶栏副标题行:书名 + 进度摘要(iOS 大标题下方副标题) */
682
- .panelSubtitle {
657
+ /* demo 风格:侧栏标题区(Logo + 小说工坊 + 当前书名) */
658
+ .navTitle {
683
659
  display: flex;
684
660
  align-items: center;
685
- gap: 8px;
686
- font-size: 12px;
687
- color: var(--nf-text-2);
688
- padding: 0 4px 2px;
661
+ gap: 10px;
662
+ padding: 2px 6px 12px;
663
+ border-bottom: 1px solid var(--nf-border);
664
+ margin-bottom: 6px;
665
+ flex: none;
689
666
  min-width: 0;
690
- flex-wrap: wrap;
667
+ }
668
+
669
+ .navTitleLogo {
670
+ width: 34px;
671
+ height: 34px;
672
+ border-radius: 10px;
673
+ background: linear-gradient(145deg, var(--nf-accent), var(--nf-accent-hover));
674
+ color: var(--nf-accent-fg);
675
+ font-weight: 800;
676
+ font-size: 15px;
677
+ display: flex;
678
+ align-items: center;
679
+ justify-content: center;
680
+ flex: none;
681
+ box-shadow: 0 6px 16px var(--nf-accent-soft);
682
+ }
683
+
684
+ .navTitleName {
685
+ font-weight: 700;
686
+ font-size: 15px;
687
+ letter-spacing: 0.2px;
688
+ white-space: nowrap;
689
+ overflow: hidden;
690
+ text-overflow: ellipsis;
691
+ }
692
+
693
+ .navTitleBook {
694
+ font-size: 11px;
695
+ color: var(--nf-text-3);
691
696
  white-space: nowrap;
697
+ overflow: hidden;
698
+ text-overflow: ellipsis;
692
699
  }
693
700
 
694
- .headerProgress {
695
- font-size: 12px;
696
- font-weight: 400;
701
+ .panelNavCollapsed .navTitle {
702
+ justify-content: center;
703
+ padding: 2px 0 10px;
704
+ }
705
+
706
+ .panelNavCollapsed .navTitleLogo {
707
+ width: 32px;
708
+ height: 32px;
709
+ }
710
+
711
+ /* 操作行:收起导航 / 关闭面板 */
712
+ .navActions {
713
+ flex: none;
714
+ display: flex;
715
+ gap: 6px;
716
+ margin-top: 8px;
717
+ padding-top: 8px;
718
+ border-top: 1px solid var(--nf-border);
719
+ }
720
+
721
+ .navActionBtn {
722
+ flex: 1;
723
+ border: 1px solid var(--nf-border);
724
+ background: rgba(0, 0, 0, 0);
697
725
  color: var(--nf-text-2);
726
+ cursor: pointer;
727
+ border-radius: 10px;
728
+ height: 32px;
729
+ font-size: 14px;
730
+ line-height: 1;
698
731
  display: inline-flex;
699
732
  align-items: center;
700
- gap: 6px;
701
- margin-left: 0;
702
- white-space: nowrap;
733
+ justify-content: center;
734
+ transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
703
735
  }
704
736
 
705
- .headerProgress b {
706
- color: var(--nf-accent);
737
+ .navActionBtn:hover {
738
+ background: var(--nf-hover);
739
+ color: var(--nf-text);
740
+ border-color: var(--nf-border-strong);
741
+ }
742
+
743
+ .panelNavCollapsed .navActions {
744
+ flex-direction: column;
745
+ align-items: stretch;
746
+ }
747
+
748
+ /* demo 风格:底部当前书切换卡(封面首字 + 书名/进度 + 箭头) */
749
+ .bookSwitch {
750
+ flex: none;
751
+ display: flex;
752
+ align-items: center;
753
+ gap: 8px;
754
+ margin-top: 4px;
755
+ padding: 9px;
756
+ border-radius: 12px;
757
+ background: var(--nf-bg-inset);
758
+ border: 1px solid var(--nf-border);
759
+ cursor: pointer;
760
+ text-align: left;
761
+ font: inherit;
762
+ color: var(--nf-text);
763
+ min-width: 0;
764
+ transition: background 0.15s ease, border-color 0.15s ease;
765
+ }
766
+
767
+ .bookSwitch:hover {
768
+ background: var(--nf-hover);
769
+ border-color: var(--nf-border-strong);
770
+ }
771
+
772
+ .bookSwitchCover {
773
+ width: 26px;
774
+ height: 34px;
775
+ border-radius: 5px;
776
+ background: linear-gradient(160deg, var(--nf-accent), color-mix(in srgb, var(--nf-accent) 45%, #000));
777
+ color: var(--nf-accent-fg);
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ font-size: 11px;
707
782
  font-weight: 700;
783
+ flex: none;
708
784
  }
709
785
 
710
- .headerProgressDot {
711
- width: 3px;
712
- height: 3px;
713
- border-radius: 50%;
714
- background: var(--nf-border-strong, var(--nf-text-3));
715
- display: inline-block;
786
+ .bookSwitchName {
787
+ font-size: 12.5px;
788
+ font-weight: 600;
789
+ overflow: hidden;
790
+ text-overflow: ellipsis;
791
+ white-space: nowrap;
716
792
  }
717
793
 
794
+ .bookSwitchMeta {
795
+ font-size: 11px;
796
+ color: var(--nf-text-3);
797
+ overflow: hidden;
798
+ text-overflow: ellipsis;
799
+ white-space: nowrap;
800
+ }
801
+
802
+ .bookSwitchArrow {
803
+ margin-left: auto;
804
+ color: var(--nf-text-3);
805
+ font-size: 12px;
806
+ flex: none;
807
+ }
808
+
809
+ /* demo 风格统计卡(今日待写 / 写作中 / 已通过 / 待审稿) */
810
+ .statRowD { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
811
+ .statCardD { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; padding: 14px 16px; border-radius: 16px; background: var(--nf-bg-inset); border: 1px solid var(--nf-border); min-width: 0; }
812
+ .statCardDLabel { font-size: 12px; color: var(--nf-text-3); }
813
+ .statCardDValue { font-size: 22px; font-weight: 700; margin-top: 2px; color: var(--nf-text); }
814
+ .statCardDUnit { font-size: 12px; color: var(--nf-text-3); font-weight: 400; }
815
+ .statCardDDetail { font-size: 11.5px; color: var(--nf-text-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
816
+ .statCardDDown { color: var(--nf-error); }
817
+ .statCardDIcon { width: 38px; height: 38px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex: none; background: var(--nf-accent-soft); }
818
+ .statCardDIconRed { background: color-mix(in srgb, var(--nf-error) 13%, transparent); }
819
+
820
+ /* demo 风格创作时间线(6 阶段分段条,点击跳转对应 tab) */
821
+ .tlBar { display: flex; gap: 6px; margin-top: 8px; }
822
+ .tlSeg { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; border: none; background: transparent; padding: 0; cursor: pointer; font: inherit; }
823
+ .tlSegTrack { height: 8px; border-radius: 999px; background: var(--nf-hover); border: 1px solid var(--nf-border); overflow: hidden; }
824
+ .tlSegFill { display: block; height: 100%; width: 100%; border-radius: 999px; background: var(--nf-accent); }
825
+ .tlSegTodo .tlSegFill { opacity: 0; }
826
+ .tlSegTodo .tlSegTrack { background: transparent; }
827
+ .tlSegCurrent .tlSegTrack { border-color: var(--nf-accent); box-shadow: 0 0 0 2px var(--nf-accent-soft); }
828
+ .tlSegCurrent .tlSegFill { opacity: 0.45; }
829
+ .tlSegLabel { font-size: 10.5px; color: var(--nf-text-3); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
830
+ .tlSegDone .tlSegLabel { color: var(--nf-accent); font-weight: 600; }
831
+ .tlSeg:hover .tlSegTrack { border-color: var(--nf-border-strong); }
832
+
833
+
718
834
  /* ------------------------------------------------------------ cards */
719
835
 
720
836
  .card {
@@ -1592,8 +1708,16 @@ body[data-ds-dark-theme] .panel[data-nf-theme='neumorph'] .buttonPrimary {
1592
1708
  display: flex;
1593
1709
  flex-direction: column;
1594
1710
  gap: 4px;
1595
- background: var(--nf-glass-inset);
1596
- box-shadow: inset 0 1px 0 var(--nf-glass-sheen);
1711
+ background: var(--nf-glass-face);
1712
+ box-shadow: var(--nf-card-shadow), inset 0 1px 0 var(--nf-glass-sheen);
1713
+ -webkit-backdrop-filter: blur(30px) saturate(180%);
1714
+ backdrop-filter: blur(30px) saturate(180%);
1715
+ transition: transform 0.22s ease, box-shadow 0.22s ease;
1716
+ }
1717
+
1718
+ .assetStat:hover {
1719
+ transform: translateY(-3px);
1720
+ box-shadow: var(--nf-card-shadow-hover), inset 0 1px 0 var(--nf-glass-sheen);
1597
1721
  }
1598
1722
 
1599
1723
  .assetStatLabel {
package/src/engine.ts CHANGED
@@ -279,6 +279,7 @@ async function complete(
279
279
  system: options.system,
280
280
  maxTokens: options.maxTokens ?? config.maxTokens,
281
281
  temperature: options.temperature ?? 0.7,
282
+ reasoningEffort: ReasoningEffortId(config.reasoningEffort ?? 'off'),
282
283
  }
283
284
  const assembler = new BlockAssembler()
284
285
  for await (const chunk of ctx.llm.stream(request)) {
@@ -2299,16 +2300,23 @@ export async function auditBook(
2299
2300
  config: NovelConfig,
2300
2301
  project: ProjectState,
2301
2302
  outputDir: string,
2303
+ onProgress?: (completedBatches: number, totalBatches: number) => void,
2302
2304
  ): Promise<AuditIssue[]> {
2303
2305
  const written = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating')
2304
- if (written.length === 0) return []
2306
+ if (written.length === 0) {
2307
+ onProgress?.(0, 0)
2308
+ return []
2309
+ }
2305
2310
  // 分批:每批 AUDIT_BATCH_SIZE 章,避免超长后单次爆上下文。
2311
+ const totalBatches = Math.ceil(written.length / AUDIT_BATCH_SIZE)
2306
2312
  const all: AuditIssue[] = []
2313
+ onProgress?.(0, totalBatches)
2307
2314
  for (let i = 0; i < written.length; i += AUDIT_BATCH_SIZE) {
2308
2315
  const batch = written.slice(i, i + AUDIT_BATCH_SIZE)
2309
2316
  try {
2310
2317
  all.push(...await auditBatch(ctx, config, project, outputDir, batch))
2311
2318
  } catch { /* 单批失败不阻断其余批次 */ }
2319
+ onProgress?.(Math.min(Math.ceil((i + AUDIT_BATCH_SIZE) / AUDIT_BATCH_SIZE), totalBatches), totalBatches)
2312
2320
  }
2313
2321
  return all.slice(0, 50)
2314
2322
  }
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export interface Config {
44
44
  provider?: string
45
45
  /** LLM model id. */
46
46
  model?: string
47
+ /** LLM reasoning effort (off/low/high/max). */
48
+ reasoningEffort?: 'off' | 'low' | 'high' | 'max'
47
49
  /** Target characters per chapter. */
48
50
  chapterChars?: number
49
51
  /** Max output tokens per chapter call. */
@@ -65,6 +67,7 @@ export const Config: z<Config> = z.object({
65
67
  outputDir: z.string().default('D:\\ryan work\\harness\\novels\\归墟玉主'),
66
68
  provider: z.string().default('deepseek-official'),
67
69
  model: z.string().default('deepseek-v4-flash'),
70
+ reasoningEffort: z.union(['off', 'low', 'high', 'max']).default('off'),
68
71
  chapterChars: z.number().default(3500),
69
72
  maxTokens: z.number().default(12000),
70
73
  reviewPassScore: z.number().default(70),
@@ -79,6 +82,7 @@ const DEFAULT_OUTLINE_PATH = 'D:\\ryan work\\harness\\novels\\归墟玉主\\《
79
82
  const DEFAULT_OUTPUT_DIR = 'D:\\ryan work\\harness\\novels\\归墟玉主'
80
83
  const DEFAULT_PROVIDER = 'deepseek-official'
81
84
  const DEFAULT_MODEL = 'deepseek-v4-flash'
85
+ const DEFAULT_REASONING_EFFORT = 'off' as const
82
86
  const DEFAULT_CHAPTER_CHARS = 3500
83
87
  const DEFAULT_MAX_TOKENS = 12000
84
88
  const DEFAULT_REVIEW_PASS_SCORE = 70
@@ -99,6 +103,7 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
99
103
  outputDir: value?.outputDir ?? DEFAULT_OUTPUT_DIR,
100
104
  provider: value?.provider ?? DEFAULT_PROVIDER,
101
105
  model: value?.model ?? DEFAULT_MODEL,
106
+ reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
102
107
  chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
103
108
  maxTokens: value?.maxTokens ?? DEFAULT_MAX_TOKENS,
104
109
  reviewPassScore: value?.reviewPassScore ?? DEFAULT_REVIEW_PASS_SCORE,
@@ -133,6 +138,7 @@ export function apply(ctx: Context, config?: Config): void {
133
138
  if (patch.outputDir !== undefined) next.outputDir = patch.outputDir
134
139
  if (patch.provider !== undefined) next.provider = patch.provider
135
140
  if (patch.model !== undefined) next.model = patch.model
141
+ if (patch.reasoningEffort !== undefined) next.reasoningEffort = patch.reasoningEffort
136
142
  if (patch.chapterChars !== undefined) next.chapterChars = patch.chapterChars
137
143
  if (patch.maxTokens !== undefined) next.maxTokens = patch.maxTokens
138
144
  if (patch.reviewPassScore !== undefined) next.reviewPassScore = patch.reviewPassScore
package/src/protocol.ts CHANGED
@@ -306,6 +306,27 @@ export interface AuditResponse {
306
306
  auditedAt: string
307
307
  }
308
308
 
309
+ /** 全书质检的实时状态(通过 /status 暴露给面板/外部读取)。 */
310
+ export interface AuditStatus {
311
+ status: 'idle' | 'running' | 'done' | 'error'
312
+ /** 开始时间(ISO)。 */
313
+ startedAt?: string
314
+ /** 结束时间(ISO)。 */
315
+ finishedAt?: string
316
+ /** 总批次数(0 = 尚未开始/无章节)。 */
317
+ totalBatches: number
318
+ /** 已完成批次数。 */
319
+ completedBatches: number
320
+ /** 参与质检的章节数。 */
321
+ auditedChapters?: number
322
+ /** 发现的问题数(done 后有效)。 */
323
+ issuesCount?: number
324
+ /** 最近一次质检的问题清单(done 后有效;error 时为空)。 */
325
+ issues?: AuditIssue[]
326
+ /** 失败信息(error 时有效)。 */
327
+ error?: string
328
+ }
329
+
309
330
  /** 一条剧情线(主线/支线/人物线/悬念线)。 */
310
331
  export interface Plotline {
311
332
  /** 稳定 id。 */
@@ -772,6 +793,8 @@ export interface NovelConfig {
772
793
  provider: string
773
794
  /** LLM model id (e.g. deepseek-v4-flash). */
774
795
  model: string
796
+ /** LLM reasoning effort: off = no thinking; low/high/max = thinking intensity. */
797
+ reasoningEffort: 'off' | 'low' | 'high' | 'max'
775
798
  /** Target characters per chapter. */
776
799
  chapterChars: number
777
800
  /** Max output tokens per chapter call. */
@@ -793,6 +816,8 @@ export interface StatusResponse {
793
816
  project?: ProjectState
794
817
  /** Chapter files already on disk (basenames, sorted). */
795
818
  generatedFiles: string[]
819
+ /** 全书质检实时状态(用于面板/外部读取进度)。 */
820
+ audit?: AuditStatus
796
821
  }
797
822
 
798
823
  /** 生产单状态(生产单 = 区间批量生产执行器:计划补足 + 逐章生成 + 被拒分级处理)。 */
@@ -985,6 +1010,7 @@ export interface ConfigPatch {
985
1010
  outputDir?: string
986
1011
  provider?: string
987
1012
  model?: string
1013
+ reasoningEffort?: 'off' | 'low' | 'high' | 'max'
988
1014
  chapterChars?: number
989
1015
  maxTokens?: number
990
1016
  reviewPassScore?: number
package/src/routes.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type AssistantHistoryResponse,
24
24
  type AssistantRequest,
25
25
  type AuditResponse,
26
+ type AuditStatus,
26
27
  type AuthorReview,
27
28
  type BiblePatchRequest,
28
29
  type BibleRequest,
@@ -199,6 +200,9 @@ const DEFAULT_PLAN_COUNT = 30
199
200
  export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
200
201
  const { ctx, getConfig, patchConfig } = deps
201
202
 
203
+ /** 全书质检实时状态(内存态,重启后回到 idle;用于 /status 暴露进度)。 */
204
+ let auditState: AuditStatus = { status: 'idle', totalBatches: 0, completedBatches: 0 }
205
+
202
206
  /** Guard helper: fence + method check. */
203
207
  const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
204
208
  if (!isLoopbackRequest(req)) {
@@ -260,6 +264,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
260
264
  ? { ...project, facts: (project.facts ?? []).slice(-80) }
261
265
  : undefined,
262
266
  generatedFiles: listChapterFiles(config.outputDir),
267
+ audit: auditState,
263
268
  }
264
269
  writeJson(res, 200, response)
265
270
  },
@@ -1210,15 +1215,37 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1210
1215
  const config = getConfig()
1211
1216
  const project = requireProject(res)
1212
1217
  if (project === undefined) return
1218
+ const startedAt = new Date().toISOString()
1219
+ auditState = { status: 'running', startedAt, totalBatches: 0, completedBatches: 0 }
1213
1220
  try {
1214
- const issues = await auditBook(ctx, config, project, config.outputDir)
1221
+ const issues = await auditBook(ctx, config, project, config.outputDir, (completed, total) => {
1222
+ auditState.totalBatches = total
1223
+ auditState.completedBatches = completed
1224
+ })
1225
+ const auditedChapters = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating').length
1226
+ auditState = {
1227
+ status: 'done',
1228
+ startedAt,
1229
+ finishedAt: new Date().toISOString(),
1230
+ totalBatches: auditState.totalBatches,
1231
+ completedBatches: auditState.totalBatches,
1232
+ auditedChapters,
1233
+ issuesCount: issues.length,
1234
+ issues,
1235
+ }
1215
1236
  const response: AuditResponse = {
1216
1237
  issues,
1217
- auditedChapters: project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating').length,
1218
- auditedAt: new Date().toISOString(),
1238
+ auditedChapters,
1239
+ auditedAt: auditState.finishedAt!,
1219
1240
  }
1220
1241
  writeJson(res, 200, response)
1221
1242
  } catch (error) {
1243
+ auditState = {
1244
+ ...auditState,
1245
+ status: 'error',
1246
+ finishedAt: new Date().toISOString(),
1247
+ error: (error as Error).message,
1248
+ }
1222
1249
  writeJson(res, 500, { error: (error as Error).message })
1223
1250
  }
1224
1251
  },
@@ -1798,7 +1825,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1798
1825
  }
1799
1826
 
1800
1827
  // 全书:NDJSON 流式补跑缺失复盘的已写章节。
1801
- const missing = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.authorReview === undefined)
1828
+ const missing = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error' && c.authorReview === undefined)
1802
1829
  if (missing.length === 0) {
1803
1830
  writeJson(res, 200, { count: 0 })
1804
1831
  return