@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.
- package/lib/client.js +1874 -1267
- package/lib/client.js.map +1 -1
- package/lib/index.js +59 -8
- package/lib/index.js.map +1 -1
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/engine.d.ts +1 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +25 -0
- package/package.json +1 -1
- package/src/client/locales.ts +16 -0
- package/src/client/panel/AssetsTab.tsx +19 -0
- package/src/client/panel/NovelPanel.tsx +522 -225
- package/src/client/panel/ShelfView.tsx +13 -12
- package/src/client/panel/panel.module.css +192 -68
- package/src/engine.ts +9 -1
- package/src/index.ts +6 -0
- package/src/protocol.ts +26 -0
- package/src/routes.ts +31 -4
|
@@ -178,18 +178,19 @@ export function ShelfView({
|
|
|
178
178
|
value={query}
|
|
179
179
|
onChange={e => { setQuery(e.target.value) }}
|
|
180
180
|
/>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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.
|
|
100
|
-
--nf-glass-orb-b: rgba(120, 160, 255, 0.
|
|
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.
|
|
142
|
-
--nf-glass-orb-b: rgba(190, 200, 215, 0.
|
|
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:
|
|
460
|
-
width:
|
|
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
|
|
448
|
+
padding: 14px 10px 12px;
|
|
466
449
|
border: 1px solid var(--nf-glass-border);
|
|
467
|
-
border-radius:
|
|
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:
|
|
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:
|
|
608
|
+
border: 1px solid transparent;
|
|
626
609
|
background: transparent;
|
|
627
610
|
cursor: pointer;
|
|
628
|
-
border-radius:
|
|
629
|
-
padding:
|
|
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-
|
|
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
|
-
.
|
|
667
|
-
|
|
640
|
+
.navTab[data-active] .navTabIcon {
|
|
641
|
+
color: var(--nf-accent);
|
|
668
642
|
}
|
|
669
643
|
|
|
670
644
|
.navTabIcon {
|
|
671
645
|
flex: none;
|
|
672
|
-
|
|
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
|
-
/*
|
|
682
|
-
.
|
|
657
|
+
/* demo 风格:侧栏标题区(Logo + 小说工坊 + 当前书名) */
|
|
658
|
+
.navTitle {
|
|
683
659
|
display: flex;
|
|
684
660
|
align-items: center;
|
|
685
|
-
gap:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
701
|
-
|
|
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
|
-
.
|
|
706
|
-
|
|
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
|
-
.
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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-
|
|
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)
|
|
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
|
|
1218
|
-
auditedAt:
|
|
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
|