@waterwx/dsh-novel-forge 1.6.0 → 1.7.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/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import z from 'schemastery'
15
15
  import type {} from '@deepseek-ai/dsh-host-webserver'
16
16
  import type {} from '@deepseek-ai/dsh-llm'
17
17
  import type {} from '@deepseek-ai/dsh-system-prompt'
18
- import type { ConfigPatch, NovelConfig } from './protocol.ts'
18
+ import type { ConfigPatch, ImageModelConfig, NovelConfig } from './protocol.ts'
19
19
  import { makeRoutes } from './routes.ts'
20
20
  import { activeBookOutputDir } from './bookshelf.ts'
21
21
 
@@ -62,11 +62,13 @@ export interface Config {
62
62
  autoAuthorReview?: boolean
63
63
  /** 修订/润色产出草稿后自动附带一次 AI 审查(默认开,可在设置页关闭省 token)。 */
64
64
  autoReviewAfterRevise?: boolean
65
- /** 豆包/Seedream 生图 API Key。 */
65
+ /** 生图模型库(多套并存,启用一条生效)。 */
66
+ imageModels?: ImageModelConfig[]
67
+ /** 豆包/Seedream 生图 API Key(旧字段,兼容迁移用)。 */
66
68
  imageApiKey?: string
67
- /** 豆包/Seedream 生图模型 ID。 */
69
+ /** 豆包/Seedream 生图模型 ID(旧字段,兼容迁移用)。 */
68
70
  imageApiModel?: string
69
- /** 是否启用豆包生图(默认关)。 */
71
+ /** 是否启用豆包生图(旧字段,兼容迁移用)。 */
70
72
  imageApiEnabled?: boolean
71
73
  }
72
74
 
@@ -85,6 +87,14 @@ export const Config: z<Config> = z.object({
85
87
  autoReview: z.boolean().default(true),
86
88
  autoAuthorReview: z.boolean().default(true),
87
89
  autoReviewAfterRevise: z.boolean().default(true),
90
+ imageModels: z.array(z.object({
91
+ id: z.string().default(''),
92
+ name: z.string().default(''),
93
+ baseURL: z.string().default(''),
94
+ apiKey: z.string().default(''),
95
+ model: z.string().default(''),
96
+ enabled: z.boolean().default(false),
97
+ })).default([]),
88
98
  imageApiKey: z.string().default(''),
89
99
  imageApiModel: z.string().default(''),
90
100
  imageApiEnabled: z.boolean().default(false),
@@ -126,12 +136,33 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
126
136
  autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
127
137
  autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
128
138
  autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
129
- imageApiKey: value?.imageApiKey,
130
- imageApiModel: value?.imageApiModel,
139
+ // 生图模型库:只有「从未保存过 imageModels」时才用旧配置迁移一条(豆包默认地址);
140
+ // 已保存为空数组 = 用户主动删光,保持空,不再复活。
141
+ imageModels: value?.imageModels !== undefined
142
+ ? value.imageModels
143
+ : (value?.imageApiKey !== undefined && value.imageApiKey !== ''
144
+ ? [{ id: 'img-legacy', name: '豆包(旧配置)', baseURL: DEFAULT_IMAGE_BASE, apiKey: value.imageApiKey, model: value.imageApiModel ?? '', enabled: true }]
145
+ : []),
146
+ // 运行时生效值 = 启用条目(兼容旧代码读取 imageApiKey/imageApiModel/imageApiEnabled)。
147
+ imageBaseUrl: (() => {
148
+ const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
149
+ return active?.baseURL !== undefined && active.baseURL !== '' ? active.baseURL : (value?.imageApiKey !== undefined && value.imageApiKey !== '' ? DEFAULT_IMAGE_BASE : undefined)
150
+ })(),
151
+ imageApiKey: (() => {
152
+ const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
153
+ return active?.apiKey ?? value?.imageApiKey
154
+ })(),
155
+ imageApiModel: (() => {
156
+ const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
157
+ return active?.model ?? value?.imageApiModel
158
+ })(),
131
159
  imageApiEnabled: value?.imageApiEnabled ?? false,
132
160
  }
133
161
  }
134
162
 
163
+ /** 生图默认接口地址(豆包 ark,OpenAI 兼容)。 */
164
+ const DEFAULT_IMAGE_BASE = 'https://ark.cn-beijing.volces.com/api/v3'
165
+
135
166
  /**
136
167
  * Mount the routes and announcement.
137
168
  * @param ctx - host plugin context carrying webServer/llm/systemPrompt.
package/src/protocol.ts CHANGED
@@ -50,6 +50,8 @@ export const NOVEL_API = {
50
50
  storyboardTable: '/api/dsh-novel-forge/storyboard/table',
51
51
  /** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
52
52
  storyboardPrompts: '/api/dsh-novel-forge/storyboard/prompts',
53
+ /** 生图接口连通性测试(设置页每个模型条目用)。 */
54
+ imageTest: '/api/dsh-novel-forge/image-test',
53
55
  /** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
54
56
  reset: '/api/dsh-novel-forge/reset',
55
57
  /** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
@@ -875,12 +877,35 @@ export interface RolesRequest {
875
877
  label?: string
876
878
  /** op='imageGenerate' 时的漫画风格预设 id(用于统一角色图与漫画页风格)。 */
877
879
  style?: string
880
+ /** op='imageGenerate' 时指定生图模型库条目 id(缺省用启用条目)。 */
881
+ modelId?: string
878
882
  /** op='visual' / op='promptKit' 时的漫剧基底风格 id(角色图按方案风格重出)。 */
879
883
  styleId?: string
880
884
  /** op='visual' / op='promptKit' 时的可选滤镜风格 id。 */
881
885
  filterId?: string
882
886
  }
883
887
 
888
+ /** POST /image-test 请求:测一个生图接口的连通性与延迟(不落盘)。 */
889
+ export interface ImageTestRequest {
890
+ /** OpenAI 兼容生图接口地址。 */
891
+ baseURL: string
892
+ /** API Key。 */
893
+ apiKey: string
894
+ /** 模型 id(顺带校验是否在接口模型列表里,可选)。 */
895
+ model?: string
896
+ }
897
+
898
+ /** POST /image-test 响应。 */
899
+ export interface ImageTestResponse {
900
+ ok: boolean
901
+ /** 连通延迟(毫秒)。 */
902
+ ms?: number
903
+ /** 失败原因。 */
904
+ message?: string
905
+ /** 模型 id 是否出现在接口的模型列表(探测成功时才有)。 */
906
+ modelFound?: boolean
907
+ }
908
+
884
909
  /** POST /roles 响应。 */
885
910
  export interface RolesResponse {
886
911
  roles: RoleRecord[]
@@ -1038,6 +1063,22 @@ export interface ProjectState {
1038
1063
  updatedAt: string
1039
1064
  }
1040
1065
 
1066
+ /** 生图模型配置(模型库条目,可多套并存,启用一条生效)。 */
1067
+ export interface ImageModelConfig {
1068
+ /** 条目 id(本地唯一)。 */
1069
+ id: string
1070
+ /** 展示名(如「豆包 Seedream」「即梦」)。 */
1071
+ name: string
1072
+ /** OpenAI 兼容生图接口地址(如 https://ark.cn-beijing.volces.com/api/v3)。 */
1073
+ baseURL: string
1074
+ /** API Key。 */
1075
+ apiKey: string
1076
+ /** 生图模型 id(如 doubao-seedream-5-0-pro-260628)。 */
1077
+ model: string
1078
+ /** 是否启用(同时只启用一条)。 */
1079
+ enabled: boolean
1080
+ }
1081
+
1041
1082
  /** Runtime config surface exposed to the panel (subset of plugin Config). */
1042
1083
  export interface NovelConfig {
1043
1084
  /** Absolute path of the default docx outline to load. */
@@ -1064,11 +1105,15 @@ export interface NovelConfig {
1064
1105
  autoAuthorReview: boolean
1065
1106
  /** 修订/润色产出草稿后是否自动附带一次 AI 审查(工作区显示新稿评分与剩余问题)。 */
1066
1107
  autoReviewAfterRevise: boolean
1067
- /** 豆包/Seedream 生图 API Key(保存在服务端配置,不暴露给前端明文回显)。 */
1108
+ /** 生图模型库(多套并存,启用一条生效;旧 imageApiKey/imageApiModel 自动迁移为一条)。 */
1109
+ imageModels?: ImageModelConfig[]
1110
+ /** 运行时生效的生图接口地址(= 启用条目,兼容旧代码读取)。 */
1111
+ imageBaseUrl?: string
1112
+ /** 运行时生效的生图 API Key(= 启用条目,兼容旧代码读取)。 */
1068
1113
  imageApiKey?: string
1069
- /** 豆包/Seedream 生图模型 ID(如 doubao-seedream-5-0-pro-260628)。 */
1114
+ /** 运行时生效的生图模型 id(= 启用条目,兼容旧代码读取)。 */
1070
1115
  imageApiModel?: string
1071
- /** 是否启用豆包生图(默认关,避免误调用付费 API)。 */
1116
+ /** 是否启用生图(= 存在启用条目)。 */
1072
1117
  imageApiEnabled?: boolean
1073
1118
  }
1074
1119
 
@@ -1286,6 +1331,8 @@ export interface ConfigPatch {
1286
1331
  imageApiKey?: string
1287
1332
  imageApiModel?: string
1288
1333
  imageApiEnabled?: boolean
1334
+ /** 生图模型库(完整替换保存)。 */
1335
+ imageModels?: ImageModelConfig[]
1289
1336
  }
1290
1337
 
1291
1338
  // ------------------------------------------------------------ assistant
package/src/routes.ts CHANGED
@@ -95,6 +95,8 @@ import {
95
95
  type StoryboardTableRequest,
96
96
  type StoryboardTableResponse,
97
97
  type StoryboardPromptsRequest,
98
+ type ImageTestRequest,
99
+ type ImageTestResponse,
98
100
  type StoryboardPromptsResponse,
99
101
  type StyleEngineRequest,
100
102
  type SummaryRequest,
@@ -146,6 +148,7 @@ import {
146
148
  reverseOutlineFromChapters,
147
149
  breakdownBook,
148
150
  generateRoleReferenceImage,
151
+ testImageEndpoint,
149
152
  generateStoryboardSkeleton,
150
153
  generateStoryboardTable,
151
154
  generateStoryboardPrompts,
@@ -1854,7 +1857,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1854
1857
  return
1855
1858
  }
1856
1859
  try {
1857
- const imageUrl = await generateRoleReferenceImage(ctx, config, project, config.outputDir, name, body?.style ?? '')
1860
+ const imageUrl = await generateRoleReferenceImage(ctx, config, project, config.outputDir, name, body?.style ?? '', body?.modelId)
1858
1861
  project.updatedAt = new Date().toISOString()
1859
1862
  saveProject(config.outputDir, project)
1860
1863
  writeJson(res, 200, { roles: project.roles, imageUrl } satisfies RolesResponse)
@@ -2192,6 +2195,27 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2192
2195
  },
2193
2196
  }
2194
2197
 
2198
+ // --------------------------------------------------------- image-test
2199
+ /** 生图接口连通性测试:GET {baseURL}/models + 计时(设置页模型条目用)。 */
2200
+ const imageTestRoute: WebRoute = {
2201
+ kind: 'exact',
2202
+ path: NOVEL_API.imageTest,
2203
+ handler: async (req, res) => {
2204
+ if (!guard(req, res, 'POST')) return
2205
+ const body = await readJsonBody<ImageTestRequest>(req)
2206
+ if (body?.baseURL === undefined || body?.apiKey === undefined) {
2207
+ writeJson(res, 400, { error: 'baseURL 与 apiKey 必填' })
2208
+ return
2209
+ }
2210
+ try {
2211
+ const result = await testImageEndpoint(body.baseURL, body.apiKey, body.model)
2212
+ writeJson(res, 200, result satisfies ImageTestResponse)
2213
+ } catch (error) {
2214
+ writeJson(res, 500, { error: `测试失败:${(error as Error).message}` })
2215
+ }
2216
+ },
2217
+ }
2218
+
2195
2219
  // ---------------------------------------------------- storyboard table
2196
2220
  /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
2197
2221
  const storyboardTableRoute: WebRoute = {
@@ -2576,6 +2600,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2576
2600
  manhuaPlansRoute,
2577
2601
  styleImageRoute,
2578
2602
  storyboardSkeletonRoute,
2603
+ imageTestRoute,
2579
2604
  storyboardTableRoute,
2580
2605
  storyboardPromptsRoute,
2581
2606
  breakdownRoute,