@waterwx/dsh-novel-forge 1.4.0 → 1.5.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 (49) hide show
  1. package/assets/styles/arcane-thick-3d.webp +0 -0
  2. package/assets/styles/chinese-ink.webp +0 -0
  3. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  4. package/assets/styles/dark-ghibli.webp +0 -0
  5. package/assets/styles/dark-gothic-3d.webp +0 -0
  6. package/assets/styles/film-photo.webp +0 -0
  7. package/assets/styles/guofeng-heavy.webp +0 -0
  8. package/assets/styles/horror-game-3d.webp +0 -0
  9. package/assets/styles/japan-cel-dark.webp +0 -0
  10. package/assets/styles/korean-webtoon.webp +0 -0
  11. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  12. package/assets/styles/nextgen-cg.webp +0 -0
  13. package/assets/styles/noir-film.webp +0 -0
  14. package/assets/styles/openworld-survival.webp +0 -0
  15. package/assets/styles/paper-cut-3d.webp +0 -0
  16. package/assets/styles/pixar-adult-3d.webp +0 -0
  17. package/assets/styles/pop-marvel-2d.webp +0 -0
  18. package/assets/styles/shinkai-anime.webp +0 -0
  19. package/assets/styles/tactical-stealth.webp +0 -0
  20. package/assets/styles/vaporwave-3d.webp +0 -0
  21. package/assets/styles/vhs90.webp +0 -0
  22. package/lib/client.js +2073 -658
  23. package/lib/client.js.map +1 -1
  24. package/lib/index.js +919 -64
  25. package/lib/index.js.map +1 -1
  26. package/lib/types/client/api.d.ts +15 -1
  27. package/lib/types/client/locales.d.ts +4 -2
  28. package/lib/types/client/panel/MangaWorkspace.d.ts +9 -0
  29. package/lib/types/client/panel/NovelPanel.d.ts +1 -1
  30. package/lib/types/client/panel/StoryboardTab.d.ts +13 -0
  31. package/lib/types/client/panel/StyleGallery.d.ts +1 -0
  32. package/lib/types/engine.d.ts +46 -4
  33. package/lib/types/index.d.ts +2 -0
  34. package/lib/types/protocol.d.ts +194 -3
  35. package/lib/types/style-library.d.ts +33 -0
  36. package/package.json +2 -1
  37. package/src/client/api.ts +36 -1
  38. package/src/client/locales.ts +7 -3
  39. package/src/client/panel/ImportModal.tsx +123 -21
  40. package/src/client/panel/MangaWorkspace.tsx +185 -0
  41. package/src/client/panel/NovelPanel.tsx +239 -150
  42. package/src/client/panel/StoryboardTab.tsx +287 -0
  43. package/src/client/panel/StyleGallery.tsx +109 -0
  44. package/src/client/panel/panel.module.css +77 -0
  45. package/src/engine.ts +457 -39
  46. package/src/index.ts +6 -0
  47. package/src/protocol.ts +200 -3
  48. package/src/routes.ts +280 -13
  49. package/src/style-library.ts +78 -0
@@ -17,6 +17,9 @@ import { RunPanel } from './RunPanel.tsx'
17
17
  import { CreateBookView } from './CreateBookView.tsx'
18
18
  import { ImportModal } from './ImportModal.tsx'
19
19
  import { WorldTab } from './WorldTab.tsx'
20
+ import { StoryboardTab } from './StoryboardTab.tsx'
21
+ import { MangaWorkspace } from './MangaWorkspace.tsx'
22
+ import { StyleGallery } from './StyleGallery.tsx'
20
23
  import { AuditIssueRow, PlotlineCard, PlotlineHealthPanel, PlotlinePlanPanel, PlotlineSuggestionPanel, RoleCandidateRow, RoleCard, StatCell, TodoRow } from './views.tsx'
21
24
  import { extractDocxTextFromBuffer } from '../docx.ts'
22
25
  import type {
@@ -43,7 +46,7 @@ export type NovelTab =
43
46
  | 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings'
44
47
  | 'characters' | 'roles' | 'facts' | 'plotlines' | 'reviews' | 'progress' | 'breakdown'
45
48
  | 'roleImage' | 'scenes'
46
- | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run'
49
+ | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run' | 'manhua' | 'styleLib'
47
50
  | 'book' | 'assets'
48
51
 
49
52
  /** Panel shell props. */
@@ -88,6 +91,8 @@ const NAV_GROUPS: ReadonlyArray<{ id: string; label: string; items: ReadonlyArra
88
91
  { id: 'breakdown', label: '拆书分析', icon: '🔍' },
89
92
  { id: 'roleImage', label: '角色形象', icon: '🖼️' },
90
93
  { id: 'scenes', label: '场景库', icon: '🏞️' },
94
+ { id: 'styleLib', label: '风格库', icon: '🎨' },
95
+ { id: 'manhua', label: '漫剧', icon: '🎬' },
91
96
  { id: 'run', label: '生产单', icon: '🏭' },
92
97
  ],
93
98
  },
@@ -791,6 +796,30 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
791
796
  }
792
797
  }
793
798
 
799
+ /** 反推大纲:从已写章节正文反向生成全书总纲(流式进度)。 */
800
+ const handleReverseOutline = async (): Promise<void> => {
801
+ if (project === null) return
802
+ if (!window.confirm('将从已写章节正文反推出全书总纲(覆盖当前大纲文本,不影响章节/设定/进度)。确定继续?')) return
803
+ setBusy(true)
804
+ setError('')
805
+ try {
806
+ await api.outlineReverse(frame => {
807
+ if (frame.type === 'outline-progress') {
808
+ pushProgress(`反推大纲:${frame.phase}(${frame.done}/${frame.total})`, 'info')
809
+ } else if (frame.type === 'outline-done') {
810
+ setOutlineText(frame.outline)
811
+ pushProgress(`反推大纲完成(${frame.chars} 字),已保存为当前总纲`, 'done')
812
+ }
813
+ })
814
+ await refresh(false)
815
+ } catch (err) {
816
+ setError((err as Error).message)
817
+ pushProgress(`反推大纲失败:${(err as Error).message}`, 'error')
818
+ } finally {
819
+ setBusy(false)
820
+ }
821
+ }
822
+
794
823
  /** 大纲页「更新大纲」展开/收起(展开时预填当前大纲文本)。 */
795
824
  const handleToggleUpdateOutline = (): void => {
796
825
  if (!updatingOutline && project !== null) setOutlineText(project.outline)
@@ -1150,7 +1179,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
1150
1179
  setBusyLabel(`生成「${name}」形象锚点…`)
1151
1180
  setError('')
1152
1181
  try {
1153
- await api.roles({ op: 'visual', name })
1182
+ const activePlan = (project?.mangaPlans ?? []).find(pl => pl.active)
1183
+ await api.roles({ op: 'visual', name, styleId: activePlan?.styleId, filterId: activePlan?.filterId })
1154
1184
  await refresh(false)
1155
1185
  pushProgress(`已生成「${name}」形象锚点`, 'done')
1156
1186
  } catch (err) {
@@ -3001,6 +3031,15 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3001
3031
  {tt('overview.bookName')}: {project.bookName} · {project.outline.length} 字
3002
3032
  {project.outlinePath !== undefined && <span> · {project.outlinePath}</span>}
3003
3033
  </span>
3034
+ <button
3035
+ type="button"
3036
+ className={`${css.button} ${css.buttonPrimary}`}
3037
+ disabled={busy || !(project.chapters ?? []).some(c => c.status === 'written' || c.status === 'approved' || c.status === 'rejected')}
3038
+ onClick={() => { void handleReverseOutline() }}
3039
+ title="从已写章节正文反向生成全书总纲(覆盖当前大纲文本)"
3040
+ >
3041
+ 🔄 反推大纲
3042
+ </button>
3004
3043
  <button type="button" className={css.button} disabled={busy} onClick={() => { handleToggleUpdateOutline() }}>
3005
3044
  {updatingOutline ? '收起' : '更新大纲'}
3006
3045
  </button>
@@ -3076,7 +3115,12 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3076
3115
  </>
3077
3116
  ) : (
3078
3117
  /* 只读展示:大纲是开书时的出生证明 */
3118
+ <>
3119
+ <div className={css.meta} style={{ marginBottom: 6 }}>
3120
+ 大纲是本书的「出生证明」;已写章节可点右上角「🔄 反推大纲」从正文反推,或「更新大纲」手动修订。
3121
+ </div>
3079
3122
  <pre className={css.outlineReadonly}>{project.outline}</pre>
3123
+ </>
3080
3124
  )}
3081
3125
  </div>
3082
3126
  <div className={css.card}>
@@ -3729,170 +3773,206 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3729
3773
  {activeTab === 'assets' && <AssetsTab api={api} />}
3730
3774
 
3731
3775
  {activeTab === 'settings' && configDraft !== null && (
3732
- <div className={css.card}>
3733
- <span className={css.cardTitle}>{tt('settings.title')}</span>
3734
- <div className={css.field}>
3735
- <label className={css.fieldLabel}>{tt('settings.outlinePath')}</label>
3736
- <input className={css.input} value={configDraft.outlinePath} onChange={e => { setConfigDraft({ ...configDraft, outlinePath: e.target.value }) }} />
3737
- </div>
3738
- <div className={css.field}>
3739
- <label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
3740
- <input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
3741
- </div>
3742
- <div className={css.row} style={{ alignItems: 'flex-start' }}>
3743
- <div className={css.field} style={{ flex: 1 }}>
3744
- <label className={css.fieldLabel}>{tt('settings.provider')}</label>
3745
- <input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
3776
+ <>
3777
+ {/* ① 路径与输出 */}
3778
+ <div className={`${css.card} ${css.settingsCard}`}>
3779
+ <span className={css.cardTitle}>📁 路径与输出</span>
3780
+ <div className={css.field}>
3781
+ <label className={css.fieldLabel}>{tt('settings.outlinePath')}</label>
3782
+ <input className={css.input} value={configDraft.outlinePath} onChange={e => { setConfigDraft({ ...configDraft, outlinePath: e.target.value }) }} />
3746
3783
  </div>
3747
- <div className={css.field} style={{ flex: 1.4 }}>
3748
- <label className={css.fieldLabel}>{tt('settings.model')}</label>
3749
- <select
3750
- className={css.input}
3751
- value={modelCustomMode ? '__custom__' : configDraft.model}
3752
- onChange={e => {
3753
- const v = e.target.value
3754
- if (v === '__custom__') {
3755
- setModelCustomMode(true)
3756
- } else {
3757
- setModelCustomMode(false)
3758
- setConfigDraft({ ...configDraft, model: v })
3759
- }
3760
- }}
3761
- >
3762
- {MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
3763
- <option value="__custom__">{tt('settings.modelCustom')}</option>
3764
- </select>
3765
- {modelCustomMode && (
3766
- <input
3767
- className={css.input}
3768
- style={{ marginTop: 6 }}
3769
- value={configDraft.model}
3770
- placeholder={tt('settings.modelCustomPlaceholder')}
3771
- onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
3772
- />
3773
- )}
3784
+ <div className={css.field}>
3785
+ <label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
3786
+ <input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
3774
3787
  </div>
3775
- <div className={css.field} style={{ flex: 1 }}>
3776
- <label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label>
3777
- <select
3778
- className={css.input}
3779
- value={configDraft.reasoningEffort ?? 'off'}
3780
- onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
3781
- >
3782
- {REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
3783
- </select>
3784
- <span className={css.meta}>{tt('settings.reasoningHint')}</span>
3788
+ <div className={css.row}>
3789
+ <button type="button" className={css.button} onClick={() => { void api.openFolder() }}>{tt('settings.openFolder')}</button>
3785
3790
  </div>
3786
3791
  </div>
3787
- <div className={css.row}>
3788
- <div className={css.field} style={{ flex: 1 }}>
3789
- <label className={css.fieldLabel}>豆包 API Key(生图)</label>
3790
- <input className={css.input} type="password" placeholder="ark-..." value={configDraft.imageApiKey ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiKey: e.target.value }) }} />
3791
- </div>
3792
- <div className={css.field} style={{ flex: 1 }}>
3793
- <label className={css.fieldLabel}>豆包生图模型</label>
3794
- <input className={css.input} placeholder="doubao-seedream-5-0-pro-260628" value={configDraft.imageApiModel ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiModel: e.target.value }) }} />
3792
+
3793
+ {/* ② 模型与推理 */}
3794
+ <div className={`${css.card} ${css.settingsCard}`}>
3795
+ <span className={css.cardTitle}>🤖 模型与推理</span>
3796
+ <div className={css.row} style={{ alignItems: 'flex-start' }}>
3797
+ <div className={css.field} style={{ flex: 1 }}>
3798
+ <label className={css.fieldLabel}>{tt('settings.provider')}</label>
3799
+ <input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
3800
+ </div>
3801
+ <div className={css.field} style={{ flex: 1.4 }}>
3802
+ <label className={css.fieldLabel}>{tt('settings.model')}</label>
3803
+ <select
3804
+ className={css.input}
3805
+ value={modelCustomMode ? '__custom__' : configDraft.model}
3806
+ onChange={e => {
3807
+ const v = e.target.value
3808
+ if (v === '__custom__') {
3809
+ setModelCustomMode(true)
3810
+ } else {
3811
+ setModelCustomMode(false)
3812
+ setConfigDraft({ ...configDraft, model: v })
3813
+ }
3814
+ }}
3815
+ >
3816
+ {MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
3817
+ <option value="__custom__">{tt('settings.modelCustom')}</option>
3818
+ </select>
3819
+ {modelCustomMode && (
3820
+ <input
3821
+ className={css.input}
3822
+ style={{ marginTop: 6 }}
3823
+ value={configDraft.model}
3824
+ placeholder={tt('settings.modelCustomPlaceholder')}
3825
+ onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
3826
+ />
3827
+ )}
3828
+ </div>
3795
3829
  </div>
3796
- <div className={css.field} style={{ flex: 1 }}>
3797
- <label className={css.fieldLabel}>启用豆包生图</label>
3798
- <select
3799
- className={css.input}
3800
- value={configDraft.imageApiEnabled ? '1' : '0'}
3801
- onChange={e => { setConfigDraft({ ...configDraft, imageApiEnabled: e.target.value === '1' }) }}
3802
- >
3803
- <option value="0">关(默认)</option>
3804
- <option value="1">开</option>
3805
- </select>
3806
- <span className={css.meta}>开启后角色详情显示「豆包生成」参考图按钮</span>
3830
+ <div className={css.row} style={{ alignItems: 'flex-start' }}>
3831
+ <div className={css.field} style={{ flex: 1 }}>
3832
+ <label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label>
3833
+ <select
3834
+ className={css.input}
3835
+ value={configDraft.reasoningEffort ?? 'off'}
3836
+ onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
3837
+ >
3838
+ {REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
3839
+ </select>
3840
+ <span className={css.meta}>{tt('settings.reasoningHint')}</span>
3841
+ </div>
3842
+ <div className={css.field} style={{ flex: 1 }}>
3843
+ <label className={css.fieldLabel}>{tt('settings.analysisReasoning')}</label>
3844
+ <select
3845
+ className={css.input}
3846
+ value={configDraft.analysisReasoning ?? 'low'}
3847
+ onChange={e => { setConfigDraft({ ...configDraft, analysisReasoning: e.target.value as NovelConfig['analysisReasoning'] }) }}
3848
+ >
3849
+ {REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
3850
+ </select>
3851
+ <span className={css.meta}>{tt('settings.analysisReasoningHint')}</span>
3852
+ </div>
3807
3853
  </div>
3808
3854
  </div>
3809
- <div className={css.row}>
3810
- <div className={css.field} style={{ flex: 1 }}>
3811
- <label className={css.fieldLabel}>{tt('settings.chapterChars')}</label>
3812
- <input className={css.input} type="number" min={1000} max={20000} value={configDraft.chapterChars} onChange={e => { setConfigDraft({ ...configDraft, chapterChars: Number(e.target.value) }) }} />
3813
- </div>
3814
- <div className={css.field} style={{ flex: 1 }}>
3815
- <label className={css.fieldLabel}>{tt('settings.maxTokens')}</label>
3816
- <input className={css.input} type="number" min={2000} max={64000} value={configDraft.maxTokens} onChange={e => { setConfigDraft({ ...configDraft, maxTokens: Number(e.target.value) }) }} />
3855
+
3856
+ {/* ③ 生图(豆包) */}
3857
+ <div className={`${css.card} ${css.settingsCard}`}>
3858
+ <span className={css.cardTitle}>🎨 生图(豆包)</span>
3859
+ <div className={css.row}>
3860
+ <div className={css.field} style={{ flex: 1 }}>
3861
+ <label className={css.fieldLabel}>豆包 API Key(生图)</label>
3862
+ <input className={css.input} type="password" placeholder="ark-..." value={configDraft.imageApiKey ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiKey: e.target.value }) }} />
3863
+ </div>
3864
+ <div className={css.field} style={{ flex: 1 }}>
3865
+ <label className={css.fieldLabel}>豆包生图模型</label>
3866
+ <input className={css.input} placeholder="doubao-seedream-5-0-pro-260628" value={configDraft.imageApiModel ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiModel: e.target.value }) }} />
3867
+ </div>
3868
+ <div className={css.field} style={{ flex: 1 }}>
3869
+ <label className={css.fieldLabel}>启用豆包生图</label>
3870
+ <select
3871
+ className={css.input}
3872
+ value={configDraft.imageApiEnabled ? '1' : '0'}
3873
+ onChange={e => { setConfigDraft({ ...configDraft, imageApiEnabled: e.target.value === '1' }) }}
3874
+ >
3875
+ <option value="0">关(默认)</option>
3876
+ <option value="1">开</option>
3877
+ </select>
3878
+ <span className={css.meta}>开启后角色详情显示「豆包生成」参考图按钮</span>
3879
+ </div>
3817
3880
  </div>
3818
3881
  </div>
3819
- <div className={css.row}>
3820
- <div className={css.field} style={{ flex: 1 }}>
3821
- <label className={css.fieldLabel}>{tt('settings.reviewPassScore')}</label>
3822
- <input className={css.input} type="number" min={0} max={100} value={configDraft.reviewPassScore} onChange={e => { setConfigDraft({ ...configDraft, reviewPassScore: Number(e.target.value) }) }} />
3823
- </div>
3824
- <div className={css.field} style={{ flex: 1 }}>
3825
- <label className={css.fieldLabel}>{tt('settings.editorFontSize')}</label>
3826
- <select
3827
- className={css.input}
3828
- value={editorFontSize}
3829
- onChange={e => { changeEditorFontSize(Number(e.target.value)) }}
3830
- >
3831
- {[12, 13, 14, 15, 16, 18, 20, 22, 24].map(v => (
3832
- <option key={v} value={v}>{v}px</option>
3833
- ))}
3834
- </select>
3835
- <span className={css.meta}>{tt('settings.editorFontSizeHint')}</span>
3882
+
3883
+ {/* ④ 写作与审稿参数 */}
3884
+ <div className={`${css.card} ${css.settingsCard}`}>
3885
+ <span className={css.cardTitle}>✍️ 写作与审稿参数</span>
3886
+ <div className={css.row}>
3887
+ <div className={css.field} style={{ flex: 1 }}>
3888
+ <label className={css.fieldLabel}>{tt('settings.chapterChars')}</label>
3889
+ <input className={css.input} type="number" min={1000} max={20000} value={configDraft.chapterChars} onChange={e => { setConfigDraft({ ...configDraft, chapterChars: Number(e.target.value) }) }} />
3890
+ </div>
3891
+ <div className={css.field} style={{ flex: 1 }}>
3892
+ <label className={css.fieldLabel}>{tt('settings.maxTokens')}</label>
3893
+ <input className={css.input} type="number" min={2000} max={64000} value={configDraft.maxTokens} onChange={e => { setConfigDraft({ ...configDraft, maxTokens: Number(e.target.value) }) }} />
3894
+ </div>
3895
+ <div className={css.field} style={{ flex: 1 }}>
3896
+ <label className={css.fieldLabel}>{tt('settings.editorFontSize')}</label>
3897
+ <select
3898
+ className={css.input}
3899
+ value={editorFontSize}
3900
+ onChange={e => { changeEditorFontSize(Number(e.target.value)) }}
3901
+ >
3902
+ {[12, 13, 14, 15, 16, 18, 20, 22, 24].map(v => (
3903
+ <option key={v} value={v}>{v}px</option>
3904
+ ))}
3905
+ </select>
3906
+ <span className={css.meta}>{tt('settings.editorFontSizeHint')}</span>
3907
+ </div>
3836
3908
  </div>
3837
- <div className={css.field} style={{ flex: 1 }}>
3838
- <label className={css.fieldLabel}>{tt('settings.autoReview')}</label>
3839
- <select
3840
- className={css.input}
3841
- value={configDraft.autoReview ? '1' : '0'}
3842
- onChange={e => { setConfigDraft({ ...configDraft, autoReview: e.target.value === '1' }) }}
3843
- >
3844
- <option value="1">✓ 是</option>
3845
- <option value="0">✗ 否</option>
3846
- </select>
3909
+ <div className={css.row}>
3910
+ <div className={css.field} style={{ flex: 1 }}>
3911
+ <label className={css.fieldLabel}>{tt('settings.reviewPassScore')}</label>
3912
+ <input className={css.input} type="number" min={0} max={100} value={configDraft.reviewPassScore} onChange={e => { setConfigDraft({ ...configDraft, reviewPassScore: Number(e.target.value) }) }} />
3913
+ <span className={css.meta}>通过判定:综合评分 ≥ 此分数(默认 70),或无 high 级问题且评分 ≥ 60;建议设 60-80。</span>
3914
+ </div>
3915
+ <div className={css.field} style={{ flex: 1 }}>
3916
+ <label className={css.fieldLabel}>{tt('settings.autoReview')}</label>
3917
+ <select
3918
+ className={css.input}
3919
+ value={configDraft.autoReview ? '1' : '0'}
3920
+ onChange={e => { setConfigDraft({ ...configDraft, autoReview: e.target.value === '1' }) }}
3921
+ >
3922
+ <option value="1">✓ 是</option>
3923
+ <option value="0">✗ 否</option>
3924
+ </select>
3925
+ </div>
3926
+ <div className={css.field} style={{ flex: 1 }}>
3927
+ <label className={css.fieldLabel}>{tt('settings.autoAuthorReview')}</label>
3928
+ <select
3929
+ className={css.input}
3930
+ value={configDraft.autoAuthorReview ? '1' : '0'}
3931
+ onChange={e => { setConfigDraft({ ...configDraft, autoAuthorReview: e.target.value === '1' }) }}
3932
+ >
3933
+ <option value="1">✓ 是</option>
3934
+ <option value="0">✗ 否</option>
3935
+ </select>
3936
+ <span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
3937
+ </div>
3938
+ <div className={css.field} style={{ flex: 1 }}>
3939
+ <label className={css.fieldLabel}>{tt('settings.autoReviewAfterRevise')}</label>
3940
+ <select
3941
+ className={css.input}
3942
+ value={configDraft.autoReviewAfterRevise ? '1' : '0'}
3943
+ onChange={e => { setConfigDraft({ ...configDraft, autoReviewAfterRevise: e.target.value === '1' }) }}
3944
+ >
3945
+ <option value="1">✓ 是</option>
3946
+ <option value="0">✗ 否</option>
3947
+ </select>
3948
+ <span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
3949
+ </div>
3847
3950
  </div>
3848
- <div className={css.field} style={{ flex: 1 }}>
3849
- <label className={css.fieldLabel}>{tt('settings.autoAuthorReview')}</label>
3850
- <select
3851
- className={css.input}
3852
- value={configDraft.autoAuthorReview ? '1' : '0'}
3853
- onChange={e => { setConfigDraft({ ...configDraft, autoAuthorReview: e.target.value === '1' }) }}
3854
- >
3855
- <option value="1">✓ 是</option>
3856
- <option value="0">✗ 否</option>
3857
- </select>
3858
- <span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
3951
+ </div>
3952
+
3953
+ {/* ⑤ 保存与导出 */}
3954
+ <div className={`${css.card} ${css.settingsCard}`}>
3955
+ <div className={css.row}>
3956
+ <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
3957
+ {tt('settings.save')}
3958
+ </button>
3959
+ <span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
3859
3960
  </div>
3860
- <div className={css.field} style={{ flex: 1 }}>
3861
- <label className={css.fieldLabel}>{tt('settings.autoReviewAfterRevise')}</label>
3862
- <select
3863
- className={css.input}
3864
- value={configDraft.autoReviewAfterRevise ? '1' : '0'}
3865
- onChange={e => { setConfigDraft({ ...configDraft, autoReviewAfterRevise: e.target.value === '1' }) }}
3866
- >
3867
- <option value="1">✓ 是</option>
3868
- <option value="0">✗ 否</option>
3869
- </select>
3870
- <span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
3961
+ <div className={css.row}>
3962
+ <span className={css.cardTitle}>{tt('settings.export')}</span>
3963
+ <button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
3964
+ {tt('settings.exportTxt')}
3965
+ </button>
3966
+ <button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
3967
+ {tt('settings.exportMd')}
3968
+ </button>
3871
3969
  </div>
3872
3970
  </div>
3873
- <div className={css.row}>
3874
- <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
3875
- {tt('settings.save')}
3876
- </button>
3877
- <button type="button" className={css.button} onClick={() => { void api.openFolder() }}>
3878
- {tt('settings.openFolder')}
3879
- </button>
3880
- <span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
3881
- </div>
3882
- <div className={css.row}>
3883
- <span className={css.cardTitle}>{tt('settings.export')}</span>
3884
- <button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
3885
- {tt('settings.exportTxt')}
3886
- </button>
3887
- <button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
3888
- {tt('settings.exportMd')}
3889
- </button>
3890
- </div>
3891
- </div>
3971
+ </>
3892
3972
  )}
3893
3973
 
3894
3974
  {activeTab === 'settings' && (
3895
- <div className={css.card}>
3975
+ <div className={`${css.card} ${css.settingsCard}`}>
3896
3976
  <span className={css.cardTitle}>{tt('settings.theme')}</span>
3897
3977
  <div className={css.row} style={{ gap: 8, flexWrap: 'wrap' }}>
3898
3978
  <button
@@ -4310,6 +4390,14 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4310
4390
  <RunPanel api={api} totalChapters={chapters.length} />
4311
4391
  )}
4312
4392
 
4393
+ {activeTab === 'styleLib' && (
4394
+ <StyleGallery />
4395
+ )}
4396
+
4397
+ {activeTab === 'manhua' && (
4398
+ <MangaWorkspace api={api} project={project} chapters={chapters} onProjectChanged={() => refresh(false)} />
4399
+ )}
4400
+
4313
4401
  {activeTab === 'breakdown' && (
4314
4402
  <div className={css.card}>
4315
4403
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
@@ -4619,7 +4707,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4619
4707
  setBusyLabel(`精修「${detailRole.name}」提示词…`)
4620
4708
  setError('')
4621
4709
  try {
4622
- await api.roles({ op: 'promptKit', name: detailRole.name })
4710
+ const activePlan = (project?.mangaPlans ?? []).find(pl => pl.active)
4711
+ await api.roles({ op: 'promptKit', name: detailRole.name, styleId: activePlan?.styleId, filterId: activePlan?.filterId })
4623
4712
  await refresh(false)
4624
4713
  pushProgress(`已生成「${detailRole.name}」四类精修提示词`, 'done')
4625
4714
  } catch (err) {