@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/lib/index.js CHANGED
@@ -2948,14 +2948,66 @@ async function breakdownBook(ctx, config, project, outputDir, scope = "recent",
2948
2948
  usedTokens
2949
2949
  };
2950
2950
  }
2951
- async function generateRoleReferenceImage(ctx, config, project, outputDir, name, style = "") {
2951
+ /** 生图接口连通性测试(设置页模型条目用):GET {baseURL}/models,计时返回延迟。 */
2952
+ async function testImageEndpoint(baseURL, apiKey, model) {
2953
+ const trimmed = baseURL.trim();
2954
+ if (trimmed === "") return {
2955
+ ok: false,
2956
+ ms: 0,
2957
+ message: "接口地址不能为空"
2958
+ };
2959
+ const url = trimmed.replace(/\/+$/, "") + "/models";
2960
+ const start = Date.now();
2961
+ try {
2962
+ const res = await fetch(url, {
2963
+ method: "GET",
2964
+ headers: {
2965
+ Authorization: "Bearer " + apiKey,
2966
+ "Content-Type": "application/json"
2967
+ }
2968
+ });
2969
+ const ms = Date.now() - start;
2970
+ if (!res.ok) {
2971
+ const text = (await res.text().catch(() => "")).slice(0, 200);
2972
+ return {
2973
+ ok: false,
2974
+ ms,
2975
+ message: "HTTP " + res.status + (text !== "" ? ":" + text : "")
2976
+ };
2977
+ }
2978
+ let modelFound;
2979
+ if (model !== void 0 && model !== "") {
2980
+ const list = (await res.json().catch(() => null))?.data;
2981
+ if (Array.isArray(list)) modelFound = list.some((m) => {
2982
+ if (typeof m !== "object" || m === null) return false;
2983
+ const o = m;
2984
+ return o.id === model || o.model === model;
2985
+ });
2986
+ }
2987
+ return {
2988
+ ok: true,
2989
+ ms,
2990
+ modelFound
2991
+ };
2992
+ } catch (err) {
2993
+ return {
2994
+ ok: false,
2995
+ ms: Date.now() - start,
2996
+ message: err.message
2997
+ };
2998
+ }
2999
+ }
3000
+ async function generateRoleReferenceImage(ctx, config, project, outputDir, name, style = "", modelId) {
2952
3001
  const role = project.roles?.find((r) => r.name === name);
2953
3002
  if (role === void 0) throw new Error(`角色 ${name} 不存在`);
2954
3003
  if (role.imagePrompt === void 0) throw new Error(`角色 ${name} 还没有形象锚点,请先生成锚点`);
2955
- if (config.imageApiEnabled !== true) throw new Error("豆包生图未启用,请在设置中开启");
2956
- const apiKey = config.imageApiKey;
2957
- const model = config.imageApiModel;
2958
- if (apiKey === void 0 || apiKey === "" || model === void 0 || model === "") throw new Error("请先在设置中配置豆包 API Key 和生图模型");
3004
+ if (config.imageApiEnabled !== true) throw new Error("生图未启用,请在设置中启用一个生图模型");
3005
+ const activeImage = (modelId !== void 0 && modelId !== "" ? (config.imageModels ?? []).find((m) => m.id === modelId) : void 0) ?? (config.imageModels ?? []).find((m) => m.enabled) ?? (config.imageModels ?? [])[0];
3006
+ const apiKey = activeImage?.apiKey ?? config.imageApiKey;
3007
+ const model = activeImage?.model ?? config.imageApiModel;
3008
+ const baseURL = (activeImage?.baseURL ?? config.imageBaseUrl ?? "").replace(/\/+$/, "");
3009
+ if (baseURL === "") throw new Error("请先在设置中填写生图接口地址");
3010
+ if (apiKey === void 0 || apiKey === "" || model === void 0 || model === "") throw new Error("请先在设置中配置生图模型的 API Key 和模型 id");
2959
3011
  const stylePrompt = getComicStylePrompt(style);
2960
3012
  const prompt = [
2961
3013
  role.imagePrompt.zh,
@@ -2964,7 +3016,7 @@ async function generateRoleReferenceImage(ctx, config, project, outputDir, name,
2964
3016
  stylePrompt,
2965
3017
  "character reference sheet, front view, clean background, consistent design"
2966
3018
  ].filter(Boolean).join(", ");
2967
- const res = await fetch("https://ark.cn-beijing.volces.com/api/v3/images/generations", {
3019
+ const res = await fetch(baseURL + "/images/generations", {
2968
3020
  method: "POST",
2969
3021
  headers: {
2970
3022
  "Content-Type": "application/json",
@@ -2981,12 +3033,12 @@ async function generateRoleReferenceImage(ctx, config, project, outputDir, name,
2981
3033
  });
2982
3034
  if (!res.ok) {
2983
3035
  const text = await res.text();
2984
- throw new Error(`豆包生图失败(${res.status}):${text}`);
3036
+ throw new Error(`生图失败(${res.status}):${text}`);
2985
3037
  }
2986
3038
  const url = (await res.json()).data?.[0]?.url;
2987
- if (url === void 0 || url === "") throw new Error("豆包生图未返回图片 URL");
3039
+ if (url === void 0 || url === "") throw new Error("生图接口未返回图片 URL");
2988
3040
  const imgRes = await fetch(url);
2989
- if (!imgRes.ok) throw new Error("下载豆包图片失败");
3041
+ if (!imgRes.ok) throw new Error("下载生图结果失败");
2990
3042
  const buf = Buffer.from(await imgRes.arrayBuffer());
2991
3043
  const dataUrl = `data:${imgRes.headers.get("content-type") || "image/jpeg"};base64,${buf.toString("base64")}`;
2992
3044
  role.imageUrl = dataUrl;
@@ -4729,6 +4781,8 @@ const NOVEL_API = {
4729
4781
  storyboardTable: "/api/dsh-novel-forge/storyboard/table",
4730
4782
  /** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
4731
4783
  storyboardPrompts: "/api/dsh-novel-forge/storyboard/prompts",
4784
+ /** 生图接口连通性测试(设置页每个模型条目用)。 */
4785
+ imageTest: "/api/dsh-novel-forge/image-test",
4732
4786
  /** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
4733
4787
  reset: "/api/dsh-novel-forge/reset",
4734
4788
  /** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
@@ -7186,7 +7240,7 @@ function makeRoutes(deps) {
7186
7240
  return;
7187
7241
  }
7188
7242
  try {
7189
- const imageUrl = await generateRoleReferenceImage(ctx, config, project, config.outputDir, name, body?.style ?? "");
7243
+ const imageUrl = await generateRoleReferenceImage(ctx, config, project, config.outputDir, name, body?.style ?? "", body?.modelId);
7190
7244
  project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7191
7245
  saveProject(config.outputDir, project);
7192
7246
  writeJson(res, 200, {
@@ -7521,6 +7575,24 @@ function makeRoutes(deps) {
7521
7575
  }
7522
7576
  }
7523
7577
  };
7578
+ /** 生图接口连通性测试:GET {baseURL}/models + 计时(设置页模型条目用)。 */
7579
+ const imageTestRoute = {
7580
+ kind: "exact",
7581
+ path: NOVEL_API.imageTest,
7582
+ handler: async (req, res) => {
7583
+ if (!guard(req, res, "POST")) return;
7584
+ const body = await readJsonBody(req);
7585
+ if (body?.baseURL === void 0 || body?.apiKey === void 0) {
7586
+ writeJson(res, 400, { error: "baseURL 与 apiKey 必填" });
7587
+ return;
7588
+ }
7589
+ try {
7590
+ writeJson(res, 200, await testImageEndpoint(body.baseURL, body.apiKey, body.model));
7591
+ } catch (error) {
7592
+ writeJson(res, 500, { error: `测试失败:${error.message}` });
7593
+ }
7594
+ }
7595
+ };
7524
7596
  /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
7525
7597
  const storyboardTableRoute = {
7526
7598
  kind: "exact",
@@ -7825,6 +7897,7 @@ function makeRoutes(deps) {
7825
7897
  manhuaPlansRoute,
7826
7898
  styleImageRoute,
7827
7899
  storyboardSkeletonRoute,
7900
+ imageTestRoute,
7828
7901
  storyboardTableRoute,
7829
7902
  storyboardPromptsRoute,
7830
7903
  breakdownRoute,
@@ -7933,6 +8006,14 @@ const Config = z.object({
7933
8006
  autoReview: z.boolean().default(true),
7934
8007
  autoAuthorReview: z.boolean().default(true),
7935
8008
  autoReviewAfterRevise: z.boolean().default(true),
8009
+ imageModels: z.array(z.object({
8010
+ id: z.string().default(""),
8011
+ name: z.string().default(""),
8012
+ baseURL: z.string().default(""),
8013
+ apiKey: z.string().default(""),
8014
+ model: z.string().default(""),
8015
+ enabled: z.boolean().default(false)
8016
+ })).default([]),
7936
8017
  imageApiKey: z.string().default(""),
7937
8018
  imageApiModel: z.string().default(""),
7938
8019
  imageApiEnabled: z.boolean().default(false)
@@ -7970,11 +8051,29 @@ function resolveConfig(value) {
7970
8051
  autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
7971
8052
  autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
7972
8053
  autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
7973
- imageApiKey: value?.imageApiKey,
7974
- imageApiModel: value?.imageApiModel,
8054
+ imageModels: value?.imageModels !== void 0 ? value.imageModels : value?.imageApiKey !== void 0 && value.imageApiKey !== "" ? [{
8055
+ id: "img-legacy",
8056
+ name: "豆包(旧配置)",
8057
+ baseURL: DEFAULT_IMAGE_BASE,
8058
+ apiKey: value.imageApiKey,
8059
+ model: value.imageApiModel ?? "",
8060
+ enabled: true
8061
+ }] : [],
8062
+ imageBaseUrl: (() => {
8063
+ const active = (value?.imageModels ?? []).find((m) => m.enabled) ?? (value?.imageModels ?? [])[0];
8064
+ return active?.baseURL !== void 0 && active.baseURL !== "" ? active.baseURL : value?.imageApiKey !== void 0 && value.imageApiKey !== "" ? DEFAULT_IMAGE_BASE : void 0;
8065
+ })(),
8066
+ imageApiKey: (() => {
8067
+ return ((value?.imageModels ?? []).find((m) => m.enabled) ?? (value?.imageModels ?? [])[0])?.apiKey ?? value?.imageApiKey;
8068
+ })(),
8069
+ imageApiModel: (() => {
8070
+ return ((value?.imageModels ?? []).find((m) => m.enabled) ?? (value?.imageModels ?? [])[0])?.model ?? value?.imageApiModel;
8071
+ })(),
7975
8072
  imageApiEnabled: value?.imageApiEnabled ?? false
7976
8073
  };
7977
8074
  }
8075
+ /** 生图默认接口地址(豆包 ark,OpenAI 兼容)。 */
8076
+ const DEFAULT_IMAGE_BASE = "https://ark.cn-beijing.volces.com/api/v3";
7978
8077
  /**
7979
8078
  * Mount the routes and announcement.
7980
8079
  * @param ctx - host plugin context carrying webServer/llm/systemPrompt.