ソースを参照

fix(studio): make RightCode custom chat work end-to-end

Ma 6 ヶ月 前
親
コミット
396002cb43

+ 3 - 2
packages/core/src/llm/provider.ts

@@ -144,11 +144,13 @@ export function createLLMClient(config: LLMConfig): LLMClient {
   const baseUrl = config.baseUrl || preset?.baseUrl || "";
   const baseUrl = config.baseUrl || preset?.baseUrl || "";
   const extraHeaders = config.headers ?? parseEnvHeaders();
   const extraHeaders = config.headers ?? parseEnvHeaders();
 
 
+  const provider = config.provider === "anthropic" ? "anthropic" : "openai";
+
   const piModel: PiModel<PiApi> = {
   const piModel: PiModel<PiApi> = {
     id: config.model,
     id: config.model,
     name: config.model,
     name: config.model,
     api: piApi,
     api: piApi,
-    provider: serviceName,
+    provider,
     baseUrl,
     baseUrl,
     reasoning: (config.thinkingBudget ?? 0) > 0,
     reasoning: (config.thinkingBudget ?? 0) > 0,
     input: ["text"] as ("text" | "image")[],
     input: ["text"] as ("text" | "image")[],
@@ -158,7 +160,6 @@ export function createLLMClient(config: LLMConfig): LLMClient {
     ...(extraHeaders ? { headers: extraHeaders } : {}),
     ...(extraHeaders ? { headers: extraHeaders } : {}),
   };
   };
 
 
-  const provider = config.provider === "anthropic" ? "anthropic" : "openai";
   return {
   return {
     provider,
     provider,
     apiFormat,
     apiFormat,

+ 28 - 1
packages/studio/src/api/server.test.ts

@@ -122,6 +122,7 @@ vi.mock("@actalk/inkos-core", () => {
     loadProjectSession: loadProjectSessionMock,
     loadProjectSession: loadProjectSessionMock,
     resolveSessionActiveBook: resolveSessionActiveBookMock,
     resolveSessionActiveBook: resolveSessionActiveBookMock,
     runAgentSession: runAgentSessionMock,
     runAgentSession: runAgentSessionMock,
+    buildAgentSystemPrompt: vi.fn(() => "You are helpful."),
     findOrCreateBookSession: findOrCreateBookSessionMock,
     findOrCreateBookSession: findOrCreateBookSessionMock,
     loadBookSession: loadBookSessionMock,
     loadBookSession: loadBookSessionMock,
     persistBookSession: persistBookSessionMock,
     persistBookSession: persistBookSessionMock,
@@ -989,7 +990,7 @@ describe("createStudioServer daemon lifecycle", () => {
       responseText: "",
       responseText: "",
       messages: [{ role: "user", content: "nihao" }],
       messages: [{ role: "user", content: "nihao" }],
     });
     });
-    chatCompletionMock.mockRejectedValueOnce(new Error("quota exhausted"));
+    chatCompletionMock.mockRejectedValue(new Error("quota exhausted"));
 
 
     const { createStudioServer } = await import("./server.js");
     const { createStudioServer } = await import("./server.js");
     const app = createStudioServer(cloneProjectConfig() as never, root);
     const app = createStudioServer(cloneProjectConfig() as never, root);
@@ -1010,6 +1011,32 @@ describe("createStudioServer daemon lifecycle", () => {
     });
     });
   });
   });
 
 
+  it("falls back to plain chat when the tool-agent returns empty text", async () => {
+    runAgentSessionMock.mockResolvedValueOnce({
+      responseText: "",
+      messages: [{ role: "user", content: "nihao" }],
+    });
+    chatCompletionMock.mockResolvedValueOnce({
+      content: "你好!",
+      usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
+    });
+
+    const { createStudioServer } = await import("./server.js");
+    const app = createStudioServer(cloneProjectConfig() as never, root);
+
+    const response = await app.request("http://localhost/api/v1/agent", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ instruction: "nihao", activeBookId: "demo-book" }),
+    });
+
+    expect(response.status).toBe(200);
+    await expect(response.json()).resolves.toEqual({
+      response: "你好!",
+      session: { sessionId: "agent-session-1" },
+    });
+  });
+
   it("returns the shared interaction session state", async () => {
   it("returns the shared interaction session state", async () => {
     loadProjectSessionMock.mockResolvedValue({
     loadProjectSessionMock.mockResolvedValue({
       sessionId: "session-2",
       sessionId: "session-2",

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

@@ -19,6 +19,7 @@ import {
   persistBookSession,
   persistBookSession,
   appendBookSessionMessage,
   appendBookSessionMessage,
   runAgentSession,
   runAgentSession,
+  buildAgentSystemPrompt,
   resolveServicePreset,
   resolveServicePreset,
   resolveServiceModel,
   resolveServiceModel,
   loadSecrets,
   loadSecrets,
@@ -812,7 +813,13 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
         return c.json({ ok: false, error: "连接成功但未返回可用模型" }, 400);
         return c.json({ ok: false, error: "连接成功但未返回可用模型" }, 400);
       }
       }
 
 
-      const sampleModel = models[0]?.id;
+      const rawConfig = await loadRawConfig(root).catch(() => ({} as Record<string, unknown>));
+      const preferredModel = typeof (rawConfig.llm as Record<string, unknown> | undefined)?.defaultModel === "string"
+        ? String((rawConfig.llm as Record<string, unknown>).defaultModel)
+        : undefined;
+      const sampleModel = models.find((m) => m.id === preferredModel)?.id
+        ?? models.find((m) => m.id === "gpt-5.4")?.id
+        ?? models[0]?.id;
       if (sampleModel) {
       if (sampleModel) {
         const client = createLLMClient({
         const client = createLLMClient({
           provider: service === "anthropic" ? "anthropic" : "openai",
           provider: service === "anthropic" ? "anthropic" : "openai",
@@ -1310,6 +1317,41 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
         });
         });
       }
       }
       if (!result.responseText) {
       if (!result.responseText) {
+        try {
+          const fallbackClient = createLLMClient({
+            ...config.llm,
+            service: configuredEntry?.service ?? reqService ?? config.llm.service,
+            model: reqModel ?? config.llm.model,
+            apiKey: agentApiKey ?? config.llm.apiKey,
+            baseUrl: configuredEntry?.baseUrl ?? config.llm.baseUrl,
+            ...(configuredEntry?.apiFormat ? { apiFormat: configuredEntry.apiFormat } : {}),
+            ...(configuredEntry?.stream !== undefined ? { stream: configuredEntry.stream } : {}),
+          } as ProjectConfig["llm"]);
+          const fallback = await chatCompletion(
+            fallbackClient,
+            reqModel ?? config.llm.model,
+            [
+              { role: "system", content: buildAgentSystemPrompt(activeBookId ?? null, config.language ?? "zh") },
+              { role: "user", content: instruction },
+            ],
+            { maxTokens: 256 },
+          );
+          if (fallback.content?.trim()) {
+            bookSession = appendBookSessionMessage(bookSession, {
+              role: "assistant",
+              content: fallback.content,
+              timestamp: Date.now() + 1,
+            });
+            await persistBookSession(root, bookSession);
+            return c.json({
+              response: fallback.content,
+              session: { sessionId: bookSession.sessionId },
+            });
+          }
+        } catch {
+          // fall through to probe-based diagnosis below
+        }
+
         try {
         try {
           const probeClient = createLLMClient({
           const probeClient = createLLMClient({
             ...config.llm,
             ...config.llm,