diff --git a/app/[locale]/docs/[...slug]/page.tsx b/app/[locale]/docs/[...slug]/page.tsx index f8c79764..99668ff8 100644 --- a/app/[locale]/docs/[...slug]/page.tsx +++ b/app/[locale]/docs/[...slug]/page.tsx @@ -214,6 +214,7 @@ export default async function DocPage({ params }: Param) { title: page.data.title, description: page.data.description, slug: slug?.join("/"), + locale, }} /> diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index fca7d3e3..97dba3a2 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -2,6 +2,8 @@ import { streamText, UIMessage, convertToModelMessages } from "ai"; import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models"; import { buildSystemMessage } from "@/lib/ai/prompt"; import { source } from "@/lib/source"; +import { routing } from "@/i18n/routing"; +import { hasLocale } from "next-intl"; import { limitChat, rateLimitResponse } from "@/lib/rate-limit"; import fs from "fs/promises"; import path from "path"; @@ -20,6 +22,7 @@ interface ChatRequest { description?: string; content?: string; slug?: string; + locale?: string; }; provider?: AIProvider; apiKey?: string; @@ -140,7 +143,11 @@ export async function POST(req: Request) { if (pageContext?.slug && !pageContext.content) { try { const slugArray = pageContext.slug.split("/"); - const page = source.getPage(slugArray); + // 不传 locale 会固定拿到 zh 原文,/en 页的助手就读中文;非法值交给 fallback(zh) + const locale = hasLocale(routing.locales, pageContext.locale) + ? pageContext.locale + : undefined; + const page = source.getPage(slugArray, locale); if (page) { const cachedContent = mdxContentCache.get(page.path); diff --git a/app/api/suggestions/route.ts b/app/api/suggestions/route.ts index 2fa23003..5c045d79 100644 --- a/app/api/suggestions/route.ts +++ b/app/api/suggestions/route.ts @@ -120,7 +120,7 @@ export async function POST(req: Request) { let text = ""; // 判断是否可以使用缓存(如果使用了自定义 apiKey,为了防止数据交叉,不采用公用缓存) - if (provider !== "intern" && !process.env.ZHIPU_API_KEY) { + if (provider !== "intern") { const { text: directText } = await generateText({ model, prompt, @@ -128,12 +128,7 @@ export async function POST(req: Request) { text = directText; } else { // 确定内部用于缓存匹配的模型标识 - const internalModelId = - provider === "intern" - ? process.env.ZHIPU_API_KEY - ? "glm" - : "intern" - : "intern"; + const internalModelId = process.env.ZHIPU_API_KEY ? "glm" : "intern"; // 将缓存粒度关联到请求本身的内容上 const suggestionKey = isWelcomeRequest ? `welcome-${pageContext?.slug || "default"}` diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index d6f4e4ab..18c4a012 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -19,6 +19,7 @@ interface PageContext { title?: string; description?: string; slug?: string; + locale?: string; } export interface WelcomeSuggestion { diff --git a/tests/suggestions-route.test.ts b/tests/suggestions-route.test.ts new file mode 100644 index 00000000..f2fc3b5c --- /dev/null +++ b/tests/suggestions-route.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const generateText = vi.fn(async () => ({ text: '["q1","q2","q3"]' })); +const unstableCache = vi.fn( + (fn: (...args: unknown[]) => unknown, _keys?: string[], _opts?: object) => + (...args: unknown[]) => + fn(...args), +); + +vi.mock("ai", () => ({ generateText })); +vi.mock("next/cache", () => ({ unstable_cache: unstableCache })); +vi.mock("@/lib/rate-limit", () => ({ + limitChat: async () => ({ success: true }), + rateLimitResponse: () => new Response(null, { status: 429 }), +})); +vi.mock("@/lib/ai/models", () => ({ + getModel: (provider: string, apiKey?: string) => ({ provider, apiKey }), + requiresApiKey: (provider: string) => provider !== "intern", +})); +vi.mock("@/lib/ai/providers/glm", () => ({ + createGlmFlashModel: () => ({ provider: "glm" }), +})); + +const { POST } = await import("@/app/api/suggestions/route"); + +// 公共缓存按请求建一层 unstable_cache,key 形如 suggestion-key-<...> +const usedSharedCache = () => + unstableCache.mock.calls.some(([, keys]) => + (keys as string[]).some((k) => k.startsWith("suggestion-key-")), + ); + +function post(body: object) { + return POST( + new Request("http://localhost/api/suggestions", { + method: "POST", + body: JSON.stringify(body), + }), + ); +} + +describe("/api/suggestions 模型选择", () => { + beforeEach(() => vi.stubEnv("ZHIPU_API_KEY", "server-key")); + afterEach(() => { + vi.unstubAllEnvs(); + vi.clearAllMocks(); + }); + + it("自带 key 的 provider 用用户自己的模型,不进公共缓存(即使服务端配了 ZHIPU)", async () => { + await post({ messages: [], provider: "openai", apiKey: "user-key" }); + + expect(usedSharedCache()).toBe(false); + expect(generateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: { provider: "openai", apiKey: "user-key" }, + }), + ); + }); + + it("默认 intern 走公共缓存,配了 ZHIPU 时用 GLM", async () => { + await post({ messages: [] }); + + expect(usedSharedCache()).toBe(true); + expect(generateText).toHaveBeenCalledWith( + expect.objectContaining({ model: { provider: "glm" } }), + ); + }); +});