Эх сурвалжийг харах

fix(studio): 优化 Google Gemini 测试连接错误提示

fanghanjun 6 сар өмнө
parent
commit
6e1c1bcbca

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

@@ -1214,6 +1214,49 @@ describe("createStudioServer daemon lifecycle", () => {
     expect(new Set(chatCompletionMock.mock.calls.map((call) => call[1]))).toEqual(new Set(["gemini-2.5-flash"]));
     expect(new Set(chatCompletionMock.mock.calls.map((call) => call[1]))).toEqual(new Set(["gemini-2.5-flash"]));
   });
   });
 
 
+  it("returns a Google-specific diagnostic when Gemini probe returns 400", async () => {
+    await writeFile(join(root, "inkos.json"), JSON.stringify({
+      ...projectConfig,
+      llm: {
+        services: [
+          { service: "google", apiFormat: "chat", stream: false },
+        ],
+      },
+    }, null, 2), "utf-8");
+
+    const fetchMock = vi.fn().mockResolvedValue({
+      ok: false,
+      status: 404,
+      text: async () => "Not Found",
+    });
+    vi.stubGlobal("fetch", fetchMock as typeof fetch);
+    createLLMClientMock.mockImplementation(((cfg: unknown) => cfg) as any);
+    chatCompletionMock.mockRejectedValue(
+      new Error("API 返回 400(请求参数错误)。常见原因:\n  1. temperature / max_tokens 超出模型约束(如 Moonshot kimi-k2.X 强制 temperature=1)\n  (baseUrl: https://generativelanguage.googleapis.com/v1beta/openai, model: gemini-2.5-flash)"),
+    );
+
+    const { createStudioServer } = await import("./server.js");
+    const app = createStudioServer(cloneProjectConfig() as never, root);
+
+    const response = await app.request("http://localhost/api/v1/services/google/test", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        apiKey: "google-key",
+        apiFormat: "chat",
+        stream: false,
+      }),
+    });
+
+    expect(response.status).toBe(400);
+    const json = await response.json() as { error?: string };
+    expect(json.error).toContain("Google Gemini 测试连接失败");
+    expect(json.error).toContain("测试模型:gemini-2.5-flash");
+    expect(json.error).toContain("API Key 是否来自 Google AI Studio");
+    expect(json.error).toContain("Gemini API");
+    expect(json.error).not.toContain("Moonshot");
+  });
+
   it("uses the preset models baseUrl when listing Bailian models", async () => {
   it("uses the preset models baseUrl when listing Bailian models", async () => {
     await writeFile(join(root, "inkos.json"), JSON.stringify({
     await writeFile(join(root, "inkos.json"), JSON.stringify({
       ...projectConfig,
       ...projectConfig,

+ 64 - 1
packages/studio/src/api/server.ts

@@ -385,6 +385,61 @@ function buildModelCandidates(args: {
   return candidates;
   return candidates;
 }
 }
 
 
+function formatServiceProbeError(args: {
+  readonly service: string;
+  readonly label?: string;
+  readonly baseUrl: string;
+  readonly model?: string;
+  readonly apiFormat?: "chat" | "responses";
+  readonly stream?: boolean;
+  readonly error: string;
+}): string {
+  const rawDetail = args.error
+    .replace(/\n\s*\(baseUrl:[\s\S]*?\)$/m, "")
+    .trim();
+  const upstreamDetail = rawDetail.includes("上游详情:")
+    ? rawDetail
+    : "";
+  const context = [
+    `服务商:${args.label ?? args.service}`,
+    `测试模型:${args.model ?? "未确定"}`,
+    `协议:${args.apiFormat === "responses" ? "Responses" : "Chat / Completions"}${typeof args.stream === "boolean" ? `,${args.stream ? "流式" : "非流式"}` : ""}`,
+    `Base URL:${args.baseUrl}`,
+  ].join("\n");
+
+  if (args.service === "google") {
+    return [
+      "Google Gemini 测试连接失败。",
+      context,
+      "",
+      "请优先检查:",
+      "1. API Key 是否来自 Google AI Studio 的 Gemini API key,而不是 OAuth、Vertex AI 或其它 Google 服务凭据。",
+      "2. 该 key 所属项目是否已启用 Gemini API,并且没有被限制到其它 API、来源或服务。",
+      "3. 当前地区/账号是否允许访问 Gemini API。",
+      "4. 如果 key 曾经泄露,请在 AI Studio 重新生成后再保存。",
+      upstreamDetail ? `\n上游返回:${upstreamDetail}` : "",
+    ].filter(Boolean).join("\n");
+  }
+
+  if (args.service === "moonshot" || args.service === "kimiCodingPlan") {
+    return [
+      `${args.label ?? args.service} 测试连接失败。`,
+      context,
+      "",
+      "请优先检查模型是否可用,以及 kimi-k2.x 这类模型是否需要 temperature=1。",
+      rawDetail ? `\n上游返回:${rawDetail}` : "",
+    ].filter(Boolean).join("\n");
+  }
+
+  return [
+    `${args.label ?? args.service} 测试连接失败。`,
+    context,
+    "",
+    "请检查 API Key、模型可用性、账号额度,以及协议类型是否匹配该服务商。",
+    rawDetail ? `\n上游返回:${rawDetail}` : "",
+  ].filter(Boolean).join("\n");
+}
+
 async function fetchModelsFromServiceBaseUrl(
 async function fetchModelsFromServiceBaseUrl(
   serviceId: string,
   serviceId: string,
   baseUrl: string,
   baseUrl: string,
@@ -513,7 +568,15 @@ async function probeServiceCapabilities(args: {
           modelsSource: discoveredModels.length > 0 ? "api" : "fallback",
           modelsSource: discoveredModels.length > 0 ? "api" : "fallback",
         };
         };
       } catch (error) {
       } catch (error) {
-        lastError = error instanceof Error ? error.message : String(error);
+        lastError = formatServiceProbeError({
+          service: baseService,
+          label: endpoint?.label ?? preset?.label,
+          baseUrl: args.baseUrl,
+          model,
+          apiFormat: plan.apiFormat,
+          stream: plan.stream,
+          error: error instanceof Error ? error.message : String(error),
+        });
       }
       }
     }
     }
   }
   }