@waterwx/dsh-novel-forge 1.4.1 → 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 (43) 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 +1455 -301
  23. package/lib/client.js.map +1 -1
  24. package/lib/index.js +589 -6
  25. package/lib/index.js.map +1 -1
  26. package/lib/types/client/api.d.ts +8 -0
  27. package/lib/types/client/panel/MangaWorkspace.d.ts +9 -0
  28. package/lib/types/client/panel/NovelPanel.d.ts +1 -1
  29. package/lib/types/client/panel/StoryboardTab.d.ts +13 -0
  30. package/lib/types/client/panel/StyleGallery.d.ts +1 -0
  31. package/lib/types/engine.d.ts +19 -3
  32. package/lib/types/protocol.d.ts +149 -0
  33. package/lib/types/style-library.d.ts +33 -0
  34. package/package.json +2 -1
  35. package/src/client/api.ts +20 -0
  36. package/src/client/panel/MangaWorkspace.tsx +185 -0
  37. package/src/client/panel/NovelPanel.tsx +18 -3
  38. package/src/client/panel/StoryboardTab.tsx +287 -0
  39. package/src/client/panel/StyleGallery.tsx +109 -0
  40. package/src/engine.ts +247 -0
  41. package/src/protocol.ts +164 -0
  42. package/src/routes.ts +192 -2
  43. package/src/style-library.ts +78 -0
@@ -18,6 +18,14 @@ export declare class NovelApi {
18
18
  }>;
19
19
  /** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流)。 */
20
20
  outlineReverse(onFrame: (frame: JobFrame) => void): Promise<void>;
21
+ /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
22
+ storyboardTable(chapterNo: number, skeleton: import('../protocol.ts').StoryboardSkeleton, styleId?: string, filterId?: string): Promise<import('../protocol.ts').StoryboardTableResponse>;
23
+ /** 漫剧方案管理:create / remove / activate。 */
24
+ manhuaPlans(req: import('../protocol.ts').MangaPlansRequest): Promise<import('../protocol.ts').MangaPlansResponse>;
25
+ /** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
26
+ storyboardPrompts(chapterNo: number, table: import('../protocol.ts').StoryboardTable, styleId?: string, filterId?: string): Promise<import('../protocol.ts').StoryboardPromptsResponse>;
27
+ /** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
28
+ storyboardSkeleton(chapterNo: number): Promise<import('../protocol.ts').StoryboardSkeletonResponse>;
21
29
  /** 开书想法 → AI 大纲:生成 count 个方案(换批时传 exclude 避开已暂留方向)。 */
22
30
  outlineSuggest(idea: string, count?: number, exclude?: string[]): Promise<import('../protocol.ts').OutlineSuggestResponse>;
23
31
  /** 拆书分析:对已写章节做结构/人物/文风/卖点体检。 */
@@ -0,0 +1,9 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ import type { ChapterPlan, ProjectState } from '../../protocol.ts';
3
+ export declare function MangaWorkspace({ api, project, chapters, onProjectChanged, }: {
4
+ api: NovelApi;
5
+ project: ProjectState | null;
6
+ chapters: ChapterPlan[];
7
+ /** 方案变更已持久化后触发(刷新项目)。 */
8
+ onProjectChanged?: () => void | Promise<void>;
9
+ }): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import type { NovelApi } from '../api.ts';
2
2
  import type { PanelController } from './controller.ts';
3
3
  /** The panel's tab identifiers. */
4
- export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'roles' | 'facts' | 'plotlines' | 'reviews' | 'progress' | 'breakdown' | 'roleImage' | 'scenes' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run' | 'book' | 'assets';
4
+ export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'roles' | 'facts' | 'plotlines' | 'reviews' | 'progress' | 'breakdown' | 'roleImage' | 'scenes' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run' | 'manhua' | 'styleLib' | 'book' | 'assets';
5
5
  /** Panel shell props. */
6
6
  export interface NovelPanelProps {
7
7
  /** The panel state owner (open/close/toggle). */
@@ -0,0 +1,13 @@
1
+ import type { NovelApi } from '../api.ts';
2
+ import type { ChapterPlan, ProjectState } from '../../protocol.ts';
3
+ export declare function StoryboardTab({ api, project, chapters, onProjectChanged, styleId, filterId, }: {
4
+ api: NovelApi;
5
+ project: ProjectState | null;
6
+ chapters: ChapterPlan[];
7
+ /** 生成成功且已持久化后触发(刷新项目,切章/重进可恢复)。 */
8
+ onProjectChanged?: () => void | Promise<void>;
9
+ /** 漫剧基底风格 id(画面措辞随风格)。 */
10
+ styleId?: string;
11
+ /** 可选滤镜风格 id。 */
12
+ filterId?: string;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function StyleGallery(): import("react").JSX.Element;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export declare const COMPLIANCE_REDLINES: ReadonlyArray<string>;
14
14
  import type { Context } from '@deepseek-ai/cordis';
15
- import type { AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, Volume, WorldState } from './protocol.ts';
15
+ import type { AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, StoryboardSkeleton, StoryboardTable, StoryboardPrompt, Volume, WorldState } from './protocol.ts';
16
16
  /** Project state file name inside the output dir. */
17
17
  export declare const PROJECT_FILE = "novel-project.json";
18
18
  /** Chapter output file name, e.g. 第001章_开篇.md */
@@ -113,12 +113,12 @@ export interface RoleVisualPrompt {
113
113
  * 为单个角色提炼「动漫形象描述词」:扫描该角色出场的已写章节正文,
114
114
  * 截取含外貌描写的段落,交给 LLM 提炼中文描述 + 英文绘图标签。
115
115
  */
116
- export declare function extractRoleVisual(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, roleName: string): Promise<RoleVisualPrompt>;
116
+ export declare function extractRoleVisual(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, roleName: string, styleId?: string, filterId?: string): Promise<RoleVisualPrompt>;
117
117
  /**
118
118
  * 角色四类生图提示词精修包(promptKit):以锚点 + 表情清单 + 视觉规则为输入,
119
119
  * LLM 产出 立绘/四视图/表情×N/细节 四套 zh+en 提示词(比前端拼装更精炼)。
120
120
  */
121
- export declare function generateRolePromptKit(ctx: Context, config: NovelConfig, project: ProjectState, roleName: string): Promise<RoleRecord['promptKit']>;
121
+ export declare function generateRolePromptKit(ctx: Context, config: NovelConfig, project: ProjectState, roleName: string, styleId?: string, filterId?: string): Promise<RoleRecord['promptKit']>;
122
122
  /** ✨ 从道藏/红线提炼「视觉世界观规则」:生图/生视频必须遵守的设定纠偏(如"商品=人,禁止常规超市商品")。 */
123
123
  export declare function extractVisualRules(ctx: Context, config: NovelConfig, project: ProjectState): Promise<string[]>;
124
124
  /**
@@ -182,6 +182,22 @@ export declare function generateChapterStream(ctx: Context, config: NovelConfig,
182
182
  }, void, unknown>;
183
183
  /** Generate a chapter summary (narrative memory). */
184
184
  export declare function summarizeChapter(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): Promise<string>;
185
+ /**
186
+ * 分镜·导演级:剧情骨架 → 分镜表(镜头级)。
187
+ * 只做画面层:景别/机位运镜/时长/画面/台词/音效/光效 + 状态连续;禁止改剧情(骨架只读)。
188
+ */
189
+ export declare function generateStoryboardTable(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, skeleton: StoryboardSkeleton, styleId?: string, filterId?: string): Promise<StoryboardTable>;
190
+ /**
191
+ * 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。
192
+ * 每镜头一段:风格词块(基底+滤镜)+ 画面内容(角色动作/服装标志物)+ 机位运镜 + 光效。
193
+ * 提示词聚焦画面与镜头(视频模型无音频,台词/音效不注入)。
194
+ */
195
+ export declare function generateStoryboardPrompts(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number, table: StoryboardTable, styleId?: string, filterId?: string): Promise<StoryboardPrompt[]>;
196
+ /**
197
+ * 分镜·编剧级:单章 → 剧情骨架(节拍链)。
198
+ * 只做故事层(事件/情绪/功能/因果),不做画面;导演级分镜在其上展开。
199
+ */
200
+ export declare function generateStoryboardSkeleton(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): Promise<StoryboardSkeleton>;
185
201
  /**
186
202
  * 反向推大纲:从已写章节正文反推出全书总纲(分卷 + 章节要点 + 主线/人物弧线/伏笔清单)。
187
203
  * 两阶段:分批提取章节事件摘要 → 汇总生成大纲。不修改章节/设定,只返回大纲文本。
@@ -41,6 +41,14 @@ export declare const NOVEL_API: {
41
41
  readonly bookshelfImportText: "/api/dsh-novel-forge/bookshelf/import-text";
42
42
  /** 导入 txt/md 全本:拆章预览(不落盘)。 */
43
43
  readonly bookshelfImportTextPreview: "/api/dsh-novel-forge/bookshelf/import-text/preview";
44
+ /** 漫剧方案管理:create/remove/activate。 */
45
+ readonly manhuaPlans: "/api/dsh-novel-forge/manhua/plans";
46
+ /** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
47
+ readonly storyboardSkeleton: "/api/dsh-novel-forge/storyboard/skeleton";
48
+ /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
49
+ readonly storyboardTable: "/api/dsh-novel-forge/storyboard/table";
50
+ /** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
51
+ readonly storyboardPrompts: "/api/dsh-novel-forge/storyboard/prompts";
44
52
  /** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
45
53
  readonly reset: "/api/dsh-novel-forge/reset";
46
54
  /** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
@@ -190,6 +198,139 @@ export interface BookImportTextPreviewResponse {
190
198
  /** 因内容过短被跳过的章节标题列表。 */
191
199
  skipped: string[];
192
200
  }
201
+ /** 分镜·编剧级:剧情骨架中的一节拍。 */
202
+ export interface StoryboardBeat {
203
+ /** 节拍 id(如 b1、b2)。 */
204
+ id: string;
205
+ /** 事件一句话(发生了什么)。 */
206
+ event: string;
207
+ /** 情绪走向(如 压抑→隐忍→惊惧)。 */
208
+ emotion: string;
209
+ /** 叙事功能:铺垫/冲突/转折/高潮/收束/伏笔/人物塑造。 */
210
+ function: string;
211
+ /** 因果:承接上一节拍的原因(可选)。 */
212
+ cause?: string;
213
+ }
214
+ /** 分镜·编剧级:单章剧情骨架。 */
215
+ export interface StoryboardSkeleton {
216
+ chapterNo: number;
217
+ /** 本章弧线一句话(起承转合)。 */
218
+ arc: string;
219
+ /** 剧情节拍链(时间顺序,因果连贯)。 */
220
+ beats: StoryboardBeat[];
221
+ }
222
+ /** POST /storyboard/skeleton 请求:生成单章剧情骨架。 */
223
+ export interface StoryboardSkeletonRequest {
224
+ chapterNo: number;
225
+ }
226
+ /** POST /storyboard/skeleton 响应。 */
227
+ export interface StoryboardSkeletonResponse {
228
+ skeleton: StoryboardSkeleton;
229
+ }
230
+ /** 分镜·导演级:一个镜头。 */
231
+ export interface StoryboardShot {
232
+ /** 镜头 id(如 s1、s2)。 */
233
+ id: string;
234
+ /** 挂载的节拍 id(骨架中的 b1…)。 */
235
+ beatId: string;
236
+ /** 景别:远景/全景/中景/近景/特写。 */
237
+ shot: string;
238
+ /** 机位与运镜(如 低机位仰拍 + 缓慢推近)。 */
239
+ camera: string;
240
+ /** 时长(秒,1-12)。 */
241
+ duration: number;
242
+ /** 画面内容:角色动作 + 表情 + 服装/标志物。 */
243
+ visual: string;
244
+ /** 台词/旁白(无则空字符串)。 */
245
+ line: string;
246
+ /** 音效(无则空字符串)。 */
247
+ sound: string;
248
+ /** 光效(如 雨夜顶光 + 手机屏幕冷光)。 */
249
+ light: string;
250
+ /** 承接上一镜头结尾状态(位置/动作/情绪/服装)。 */
251
+ prevState: string;
252
+ /** 本镜头结束状态。 */
253
+ nextState: string;
254
+ }
255
+ /** 分镜·导演级:单章分镜表。 */
256
+ export interface StoryboardTable {
257
+ chapterNo: number;
258
+ shots: StoryboardShot[];
259
+ }
260
+ /** POST /storyboard/table 请求:骨架 → 分镜表。 */
261
+ export interface StoryboardTableRequest {
262
+ chapterNo: number;
263
+ /** 编剧级骨架(前端已生成/编辑后的版本)。 */
264
+ skeleton: StoryboardSkeleton;
265
+ /** 漫剧方案基底风格 id(style-library);提供时按风格措辞画面描述。 */
266
+ styleId?: string;
267
+ /** 可选滤镜风格 id(stackable)。 */
268
+ filterId?: string;
269
+ }
270
+ /** POST /storyboard/table 响应。 */
271
+ export interface StoryboardTableResponse {
272
+ table: StoryboardTable;
273
+ }
274
+ /** 分镜·提示词级:一个镜头的即梦视频提示词。 */
275
+ export interface StoryboardPrompt {
276
+ /** 对应镜头 id(s1…)。 */
277
+ shotId: string;
278
+ /** 即梦/视频模型可粘贴的中文提示词(画面+运镜+光效+风格词块)。 */
279
+ text: string;
280
+ }
281
+ /** 分镜持久化:单章的分镜产出(骨架 + 分镜表 + 视频提示词,可分别存在)。 */
282
+ export interface ChapterStoryboard {
283
+ chapterNo: number;
284
+ skeleton?: StoryboardSkeleton;
285
+ table?: StoryboardTable;
286
+ prompts?: StoryboardPrompt[];
287
+ updatedAt: string;
288
+ }
289
+ /** POST /storyboard/prompts 请求:分镜表 → 视频提示词。 */
290
+ export interface StoryboardPromptsRequest {
291
+ chapterNo: number;
292
+ /** 分镜表(前端已生成的镜头列表)。 */
293
+ table: StoryboardTable;
294
+ /** 基底风格 id。 */
295
+ styleId?: string;
296
+ /** 可选滤镜风格 id。 */
297
+ filterId?: string;
298
+ }
299
+ /** POST /storyboard/prompts 响应。 */
300
+ export interface StoryboardPromptsResponse {
301
+ prompts: StoryboardPrompt[];
302
+ }
303
+ /** 漫剧方案:同一本书的多套视觉演绎(基底风格 + 可选滤镜)。 */
304
+ export interface MangaPlan {
305
+ /** 方案 id。 */
306
+ id: string;
307
+ /** 方案名(如《保质期》3D 皮克斯版)。 */
308
+ name: string;
309
+ /** 基底风格 id(style-library 中非 stackable 风格)。 */
310
+ styleId: string;
311
+ /** 可选滤镜风格 id(style-library 中 stackable 风格)。 */
312
+ filterId?: string;
313
+ /** 是否激活(分镜/角色图生成时使用)。 */
314
+ active: boolean;
315
+ createdAt: string;
316
+ updatedAt: string;
317
+ }
318
+ /** POST /manhua/plans 请求:漫剧方案管理。 */
319
+ export interface MangaPlansRequest {
320
+ op: 'create' | 'remove' | 'activate';
321
+ /** create:方案名。 */
322
+ name?: string;
323
+ /** create:基底风格 id。 */
324
+ styleId?: string;
325
+ /** create:可选滤镜风格 id。 */
326
+ filterId?: string;
327
+ /** remove/activate:方案 id。 */
328
+ id?: string;
329
+ }
330
+ /** POST /manhua/plans 响应。 */
331
+ export interface MangaPlansResponse {
332
+ plans: MangaPlan[];
333
+ }
193
334
  /** Chapter lifecycle states (the writing pipeline's state machine). */
194
335
  export type ChapterStatus = 'pending' | 'generating' | 'written' | 'reviewing' | 'approved' | 'rejected' | 'error';
195
336
  /** One chapter in the plan. */
@@ -674,6 +815,10 @@ export interface RolesRequest {
674
815
  label?: string;
675
816
  /** op='imageGenerate' 时的漫画风格预设 id(用于统一角色图与漫画页风格)。 */
676
817
  style?: string;
818
+ /** op='visual' / op='promptKit' 时的漫剧基底风格 id(角色图按方案风格重出)。 */
819
+ styleId?: string;
820
+ /** op='visual' / op='promptKit' 时的可选滤镜风格 id。 */
821
+ filterId?: string;
677
822
  }
678
823
  /** POST /roles 响应。 */
679
824
  export interface RolesResponse {
@@ -805,6 +950,10 @@ export interface ProjectState {
805
950
  scenes?: SceneCard[];
806
951
  /** 视觉世界观规则(生图/生视频必须遵守,从道藏提炼,注入所有提示词)。 */
807
952
  visualRules?: string[];
953
+ /** 分镜工作台产出(骨架/分镜表,按章持久化,刷新/切换不丢)。 */
954
+ storyboards?: ChapterStoryboard[];
955
+ /** 漫剧方案列表(同一本书的多套视觉演绎:基底风格 + 可选滤镜)。 */
956
+ mangaPlans?: MangaPlan[];
808
957
  /** 人物志:角色当前状态聚合结果(从编年录刷新后存档,打开页面直接显示)。 */
809
958
  roleStatus?: RoleStatusCard[];
810
959
  /** 漫剧分镜结果缓存:章号 → 分镜(持久化,刷新不丢)。 */
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 风格库(内置模板):漫剧方案的视觉基底 + 可叠加滤镜。
3
+ * 字段来自通用风格模板(豆包整理),keywords 即风格前缀词块,生成时直接套用。
4
+ */
5
+ export type StyleCategory = '3d' | 'game' | '2d' | 'film' | 'craft';
6
+ export interface ArtStyle {
7
+ id: string;
8
+ /** 风格名(用户可见)。 */
9
+ name: string;
10
+ category: StyleCategory;
11
+ /** 特点一句话(卡片展示)。 */
12
+ traits: string;
13
+ /** 风格关键词/前缀(生成角色图/视频提示词时套用)。 */
14
+ keywords: string;
15
+ /** 是否可作为滤镜叠加在基底风格之上(影视实验类;缺省 false)。 */
16
+ stackable?: boolean;
17
+ /** 默认推荐排序权重(0-1,推荐池排序参考)。 */
18
+ weight?: number;
19
+ }
20
+ export declare const STYLE_CATEGORIES: ReadonlyArray<{
21
+ id: StyleCategory;
22
+ label: string;
23
+ icon: string;
24
+ desc: string;
25
+ }>;
26
+ /** 内置风格模板(18 个)。 */
27
+ export declare const STYLE_LIBRARY: ArtStyle[];
28
+ /** 按分类取风格。 */
29
+ export declare function stylesByCategory(category: StyleCategory): ArtStyle[];
30
+ /** 取单个风格(未找到返回 undefined)。 */
31
+ export declare function findStyle(id: string): ArtStyle | undefined;
32
+ /** 按基底 + 滤镜拼接风格关键词(角色图/分镜/视频提示词共用;缺省回退 3D 动漫)。 */
33
+ export declare function styleKeywords(styleId?: string, filterId?: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@waterwx/dsh-novel-forge",
3
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": "1.4.1",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -81,6 +81,7 @@
81
81
  "lib/**/*.d.ts",
82
82
  "lib/**/*.d.ts.map",
83
83
  "src",
84
+ "assets/styles",
84
85
  "scripts/patch-schemastery.mjs",
85
86
  "cordis.patch.yml",
86
87
  "README.md"
package/src/client/api.ts CHANGED
@@ -80,6 +80,26 @@ export class NovelApi {
80
80
  await this.streamJob(NOVEL_API.outlineReverse, {}, onFrame)
81
81
  }
82
82
 
83
+ /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
84
+ async storyboardTable(chapterNo: number, skeleton: import('../protocol.ts').StoryboardSkeleton, styleId?: string, filterId?: string): Promise<import('../protocol.ts').StoryboardTableResponse> {
85
+ return postJson<import('../protocol.ts').StoryboardTableResponse>(NOVEL_API.storyboardTable, { chapterNo, skeleton, styleId, filterId })
86
+ }
87
+
88
+ /** 漫剧方案管理:create / remove / activate。 */
89
+ async manhuaPlans(req: import('../protocol.ts').MangaPlansRequest): Promise<import('../protocol.ts').MangaPlansResponse> {
90
+ return postJson<import('../protocol.ts').MangaPlansResponse>(NOVEL_API.manhuaPlans, req)
91
+ }
92
+
93
+ /** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
94
+ async storyboardPrompts(chapterNo: number, table: import('../protocol.ts').StoryboardTable, styleId?: string, filterId?: string): Promise<import('../protocol.ts').StoryboardPromptsResponse> {
95
+ return postJson<import('../protocol.ts').StoryboardPromptsResponse>(NOVEL_API.storyboardPrompts, { chapterNo, table, styleId, filterId })
96
+ }
97
+
98
+ /** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
99
+ async storyboardSkeleton(chapterNo: number): Promise<import('../protocol.ts').StoryboardSkeletonResponse> {
100
+ return postJson<import('../protocol.ts').StoryboardSkeletonResponse>(NOVEL_API.storyboardSkeleton, { chapterNo })
101
+ }
102
+
83
103
  /** 开书想法 → AI 大纲:生成 count 个方案(换批时传 exclude 避开已暂留方向)。 */
84
104
  async outlineSuggest(idea: string, count?: number, exclude?: string[]): Promise<import('../protocol.ts').OutlineSuggestResponse> {
85
105
  return postJson<import('../protocol.ts').OutlineSuggestResponse>(NOVEL_API.outlineSuggest, { idea, count, exclude })
@@ -0,0 +1,185 @@
1
+ /**
2
+ * 漫剧工作台:方案管理(基底风格 + 可选滤镜)→ 分镜(骨架/分镜表,按方案风格生成)。
3
+ * 骨架为故事层(所有方案共享);分镜表画面措辞随方案风格。
4
+ */
5
+ import { useMemo, useState } from 'react'
6
+ import type { NovelApi } from '../api.ts'
7
+ import type { ChapterPlan, MangaPlan, ProjectState } from '../../protocol.ts'
8
+ import { STYLE_LIBRARY, findStyle } from '../../style-library.ts'
9
+ import { StoryboardTab } from './StoryboardTab.tsx'
10
+ import css from './panel.module.css'
11
+
12
+ export function MangaWorkspace({
13
+ api,
14
+ project,
15
+ chapters,
16
+ onProjectChanged,
17
+ }: {
18
+ api: NovelApi
19
+ project: ProjectState | null
20
+ chapters: ChapterPlan[]
21
+ /** 方案变更已持久化后触发(刷新项目)。 */
22
+ onProjectChanged?: () => void | Promise<void>
23
+ }) {
24
+ const plans = useMemo(() => project?.mangaPlans ?? [], [project?.mangaPlans])
25
+ const activePlan = plans.find(p => p.active) ?? null
26
+ const [showCreate, setShowCreate] = useState(false)
27
+ const [name, setName] = useState('')
28
+ const [styleId, setStyleId] = useState('')
29
+ const [filterId, setFilterId] = useState('')
30
+ const [busy, setBusy] = useState(false)
31
+ const [error, setError] = useState('')
32
+
33
+ // 基底候选:非 stackable 风格按 weight 降序(推荐池),+ 全部基底风格
34
+ const baseStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable !== true).sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0)), [])
35
+ const filterStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable === true), [])
36
+
37
+ const mutate = async (req: import('../../protocol.ts').MangaPlansRequest): Promise<boolean> => {
38
+ setBusy(true)
39
+ setError('')
40
+ try {
41
+ await api.manhuaPlans(req)
42
+ return true
43
+ } catch (err) {
44
+ setError((err as Error).message)
45
+ return false
46
+ } finally {
47
+ setBusy(false)
48
+ }
49
+ }
50
+
51
+ const createPlan = async (): Promise<void> => {
52
+ const n = name.trim()
53
+ if (n === '' || styleId === '') {
54
+ setError('请填写方案名并选择基底风格')
55
+ return
56
+ }
57
+ const ok = await mutate({ op: 'create', name: n, styleId, filterId: filterId !== '' ? filterId : undefined })
58
+ if (ok) {
59
+ setShowCreate(false)
60
+ setName('')
61
+ setStyleId('')
62
+ setFilterId('')
63
+ await refreshProject()
64
+ }
65
+ }
66
+
67
+ const refreshProject = async (): Promise<void> => {
68
+ await onProjectChanged?.()
69
+ }
70
+
71
+ if (project === null) {
72
+ return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入漫剧工作台。</span></div>
73
+ }
74
+
75
+ return (
76
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
77
+ {/* ① 方案管理 */}
78
+ <div className={css.card}>
79
+ <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
80
+ <span className={css.cardTitle}>🎬 漫剧方案</span>
81
+ <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { setShowCreate(!showCreate) }}>
82
+ {showCreate ? '收起' : '+ 新建方案'}
83
+ </button>
84
+ </div>
85
+ <span className={css.meta}>同一本书可建多套视觉演绎(基底风格 + 可选滤镜);分镜画面措辞随激活方案。</span>
86
+
87
+ {showCreate && (
88
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, border: '1px solid var(--nf-info)', borderRadius: 12, padding: 10, marginTop: 8 }}>
89
+ <div className={css.field}>
90
+ <label className={css.fieldLabel}>方案名</label>
91
+ <input className={css.input} value={name} placeholder="例如:《保质期》3D 皮克斯版" onChange={e => { setName(e.target.value) }} />
92
+ </div>
93
+ <div className={css.field}>
94
+ <label className={css.fieldLabel}>基底风格(按推荐度排序)</label>
95
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
96
+ {baseStyles.map(s => (
97
+ <button
98
+ key={s.id}
99
+ type="button"
100
+ className={`${css.button} ${css.buttonSmall} ${styleId === s.id ? css.buttonPrimary : ''}`}
101
+ title={s.traits}
102
+ onClick={() => { setStyleId(s.id) }}
103
+ >
104
+ {s.name}
105
+ </button>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ {filterStyles.length > 0 && (
110
+ <div className={css.field}>
111
+ <label className={css.fieldLabel}>叠加滤镜(可选)</label>
112
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
113
+ <button type="button" className={`${css.button} ${css.buttonSmall} ${filterId === '' ? css.buttonPrimary : ''}`} onClick={() => { setFilterId('') }}>无</button>
114
+ {filterStyles.map(s => (
115
+ <button
116
+ key={s.id}
117
+ type="button"
118
+ className={`${css.button} ${css.buttonSmall} ${filterId === s.id ? css.buttonPrimary : ''}`}
119
+ title={s.traits}
120
+ onClick={() => { setFilterId(s.id) }}
121
+ >
122
+ {s.name}
123
+ </button>
124
+ ))}
125
+ </div>
126
+ </div>
127
+ )}
128
+ {error !== '' && <div className={css.importError}>{error}</div>}
129
+ <div className={css.row}>
130
+ <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy || name.trim() === '' || styleId === ''} onClick={() => { void createPlan() }}>
131
+ 创建方案
132
+ </button>
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {plans.length === 0 ? (
138
+ <div className={css.meta} style={{ marginTop: 8 }}>尚无漫剧方案——新建一个方案,选择基底风格开始。</div>
139
+ ) : (
140
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }}>
141
+ {plans.map(p => {
142
+ const style = findStyle(p.styleId)
143
+ const filter = p.filterId !== undefined ? findStyle(p.filterId) : undefined
144
+ return (
145
+ <div
146
+ key={p.id}
147
+ style={{
148
+ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
149
+ border: `1px solid ${p.active ? 'var(--nf-accent)' : 'var(--nf-border)'}`, borderRadius: 10, padding: '8px 10px',
150
+ }}
151
+ >
152
+ <b>{p.name}</b>
153
+ <span className={css.badge}>{style?.name ?? p.styleId}</span>
154
+ {filter !== undefined && <span className={css.badge}>+{filter.name}</span>}
155
+ {p.active && <span className={`${css.badge} ${css.badgeDone}`}>激活中</span>}
156
+ <div style={{ flex: 1 }} />
157
+ {!p.active && (
158
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={async () => { if (await mutate({ op: 'activate', id: p.id })) await refreshProject() }}>
159
+ 激活
160
+ </button>
161
+ )}
162
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={async () => { if (await mutate({ op: 'remove', id: p.id })) await refreshProject() }}>
163
+ 删除
164
+ </button>
165
+ </div>
166
+ )
167
+ })}
168
+ </div>
169
+ )}
170
+ </div>
171
+
172
+ {/* ② 分镜(按激活方案风格) */}
173
+ {plans.length === 0 ? null : (
174
+ <StoryboardTab
175
+ api={api}
176
+ project={project}
177
+ chapters={chapters}
178
+ onProjectChanged={onProjectChanged}
179
+ styleId={activePlan?.styleId}
180
+ filterId={activePlan?.filterId}
181
+ />
182
+ )}
183
+ </div>
184
+ )
185
+ }
@@ -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
  },
@@ -1174,7 +1179,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
1174
1179
  setBusyLabel(`生成「${name}」形象锚点…`)
1175
1180
  setError('')
1176
1181
  try {
1177
- 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 })
1178
1184
  await refresh(false)
1179
1185
  pushProgress(`已生成「${name}」形象锚点`, 'done')
1180
1186
  } catch (err) {
@@ -4384,6 +4390,14 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4384
4390
  <RunPanel api={api} totalChapters={chapters.length} />
4385
4391
  )}
4386
4392
 
4393
+ {activeTab === 'styleLib' && (
4394
+ <StyleGallery />
4395
+ )}
4396
+
4397
+ {activeTab === 'manhua' && (
4398
+ <MangaWorkspace api={api} project={project} chapters={chapters} onProjectChanged={() => refresh(false)} />
4399
+ )}
4400
+
4387
4401
  {activeTab === 'breakdown' && (
4388
4402
  <div className={css.card}>
4389
4403
  <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
@@ -4693,7 +4707,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
4693
4707
  setBusyLabel(`精修「${detailRole.name}」提示词…`)
4694
4708
  setError('')
4695
4709
  try {
4696
- 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 })
4697
4712
  await refresh(false)
4698
4713
  pushProgress(`已生成「${detailRole.name}」四类精修提示词`, 'done')
4699
4714
  } catch (err) {