Преглед изворни кода

fix(studio): surface create config errors

Ma пре 6 месеци
родитељ
комит
d597ce4a99

+ 29 - 0
packages/studio/src/api/server.test.ts

@@ -1642,6 +1642,35 @@ describe("createStudioServer daemon lifecycle", () => {
     });
   });
 
+  it("surfaces LLM config errors during create instead of masking them as internal errors", async () => {
+    loadProjectConfigMock.mockRejectedValueOnce(
+      new Error("Studio LLM API key not set. Open Studio services and save an API key for the selected service."),
+    );
+
+    const { createStudioServer } = await import("./server.js");
+    const app = createStudioServer(cloneProjectConfig() as never, root);
+
+    const response = await app.request("http://localhost/api/v1/books/create", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        title: "Needs Key",
+        genre: "urban",
+        platform: "qidian",
+        language: "zh",
+      }),
+    });
+
+    expect(response.status).toBe(400);
+    await expect(response.json()).resolves.toEqual({
+      error: {
+        code: "LLM_CONFIG_ERROR",
+        message: "Studio LLM API key not set. Open Studio services and save an API key for the selected service.",
+      },
+    });
+    expect(processProjectInteractionRequestMock).not.toHaveBeenCalled();
+  });
+
   it("uses rollback semantics for chapter rejection instead of only flipping status", async () => {
     loadChapterIndexMock.mockResolvedValue([
       {

+ 5 - 0
packages/studio/src/api/server.ts

@@ -769,6 +769,11 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
     if (error instanceof ApiError) {
       return c.json({ error: { code: error.code, message: error.message } }, error.status as 400);
     }
+    const message = error instanceof Error ? error.message : String(error);
+    if (message.includes("LLM API key not set") || message.includes("INKOS_LLM_API_KEY not set")) {
+      return c.json({ error: { code: "LLM_CONFIG_ERROR", message } }, 400);
+    }
+    console.error("[studio] Unexpected server error", error);
     return c.json(
       { error: { code: "INTERNAL_ERROR", message: "Unexpected server error." } },
       500,

+ 9 - 0
packages/studio/src/lib/error-copy.test.ts

@@ -17,4 +17,13 @@ describe("localizeKnownRuntimeMessage", () => {
     )).toBe("只能安全修复最新的状态降级(state-degraded)章节;当前最新章是第 5 章。");
     expect(localizeKnownRuntimeMessage("Bad request")).toBe("Bad request");
   });
+
+  it("localizes common LLM configuration errors", () => {
+    expect(localizeKnownRuntimeMessage(
+      "Studio LLM API key not set. Open Studio services and save an API key for the selected service.",
+    )).toBe("Studio 模型 API Key 未设置。请打开“模型配置”,为当前服务保存 API Key。");
+    expect(localizeKnownRuntimeMessage(
+      "INKOS_LLM_API_KEY not set. Run 'inkos config set-global' or add it to project .env file.",
+    )).toBe("INKOS_LLM_API_KEY 未设置。请运行 `inkos config set-global`,或在项目 .env 文件中添加它。");
+  });
 });

+ 8 - 0
packages/studio/src/lib/error-copy.ts

@@ -18,6 +18,14 @@ const KNOWN_RUNTIME_REPLACEMENTS: ReadonlyArray<{
     pattern: /State repair still failed for chapter (\d+)\./g,
     replacement: "第 $1 章状态修复仍然失败。",
   },
+  {
+    pattern: /Studio LLM API key not set\. Open Studio services and save an API key for the selected service\./g,
+    replacement: "Studio 模型 API Key 未设置。请打开“模型配置”,为当前服务保存 API Key。",
+  },
+  {
+    pattern: /INKOS_LLM_API_KEY not set\. Run 'inkos config set-global' or add it to project \.env file\./g,
+    replacement: "INKOS_LLM_API_KEY 未设置。请运行 `inkos config set-global`,或在项目 .env 文件中添加它。",
+  },
 ];
 
 export function localizeKnownRuntimeMessage(message: string): string {