Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/[locale]/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export default async function DocPage({ params }: Param) {
title: page.data.title,
description: page.data.description,
slug: slug?.join("/"),
locale,
}}
/>
</>
Expand Down
9 changes: 8 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,6 +22,7 @@ interface ChatRequest {
description?: string;
content?: string;
slug?: string;
locale?: string;
};
provider?: AIProvider;
apiKey?: string;
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 2 additions & 7 deletions app/api/suggestions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,15 @@ 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,
});
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"}`
Expand Down
1 change: 1 addition & 0 deletions app/components/DocsAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface PageContext {
title?: string;
description?: string;
slug?: string;
locale?: string;
}

export interface WelcomeSuggestion {
Expand Down
67 changes: 67 additions & 0 deletions tests/suggestions-route.test.ts
Original file line number Diff line number Diff line change
@@ -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" } }),
);
});
});
Loading