新增普通文案优化接口

This commit is contained in:
czj
2026-07-02 14:35:35 +08:00
parent 27d7a44ad0
commit 3919887a8d
2 changed files with 177 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const llmMocks = vi.hoisted(() => ({
generateValidatedJson: vi.fn(),
}));
vi.mock("../../../lib/llm/client", async () => {
const actual = await vi.importActual<typeof import("../../../lib/llm/client")>(
"../../../lib/llm/client",
);
return {
...actual,
generateValidatedJson: llmMocks.generateValidatedJson,
};
});
import { POST as optimizeCopy } from "../copy/renwei-optimize/route";
describe("renwei copy optimization API route", () => {
const originalApiKey = process.env.API_ACCESS_KEY;
const originalAuthDisabled = process.env.API_AUTH_DISABLED;
beforeEach(() => {
process.env.API_ACCESS_KEY = "test-key";
process.env.API_AUTH_DISABLED = "false";
});
afterEach(() => {
process.env.API_ACCESS_KEY = originalApiKey;
process.env.API_AUTH_DISABLED = originalAuthDisabled;
llmMocks.generateValidatedJson.mockReset();
});
it("rejects requests without the access key", async () => {
const response = await optimizeCopy(
request(
{
source_text: "这是一段普通文案。",
intensity: "light",
},
{ apiKey: null },
),
);
expect(response.status).toBe(401);
});
it("returns 400 for empty source text", async () => {
const response = await optimizeCopy(
request({
source_text: " ",
intensity: "light",
}),
);
const body = (await response.json()) as { error: string };
expect(response.status).toBe(400);
expect(body.error).toBe("请输入需要优化的文案");
});
it("returns structured copy optimization results", async () => {
llmMocks.generateValidatedJson.mockResolvedValueOnce({
optimized_text: "我把这段文案顺了一下。",
change_notes: [
{
original: "我把这段文案顺顺。",
revised: "我把这段文案顺了一下。",
reason: "修正重复表达。",
confidence: "confident",
revertible: false,
},
],
ai_taste_checks: [
{
rule_id: "promotion_tone",
status: "pass",
evidence: "没有新增宣传腔。",
suggestion: "",
},
],
warnings: [],
});
const response = await optimizeCopy(
request({
source_text: "我把这段文案顺顺。",
goal: "",
intensity: "light",
user_instructions: "保留口语。",
}),
);
const body = (await response.json()) as {
result: { optimized_text: string; change_notes: unknown[] };
};
expect(response.status).toBe(200);
expect(body.result.optimized_text).toBe("我把这段文案顺了一下。");
expect(body.result.change_notes).toHaveLength(1);
expect(llmMocks.generateValidatedJson).toHaveBeenCalledWith(
expect.objectContaining({
task: "renwei_copy_optimizer",
}),
);
});
it("surfaces LLM failures as a 502", async () => {
llmMocks.generateValidatedJson.mockRejectedValueOnce(
new Error("LLM response failed schema validation: optimized_text"),
);
const response = await optimizeCopy(
request({
source_text: "这是一段普通文案。",
intensity: "light",
}),
);
const body = (await response.json()) as { error: string };
expect(response.status).toBe(502);
expect(body.error).toBe(
"LLM response failed schema validation: optimized_text",
);
});
});
function request(body: unknown, options: { apiKey?: string | null } = {}) {
const headers: Record<string, string> = { "content-type": "application/json" };
const apiKey = options.apiKey === undefined ? "test-key" : options.apiKey;
if (apiKey) {
headers["x-api-key"] = apiKey;
}
return new Request("http://localhost/api/copy/renwei-optimize", {
method: "POST",
body: JSON.stringify(body),
headers,
});
}
+39
View File
@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { requireApiAccess } from "../../../../lib/api/auth";
import { copyOptimizationRequestSchema } from "../../../../lib/domain/validation";
import { LlmValidationError } from "../../../../lib/llm/client";
import { optimizeRenweiCopy } from "../../../../lib/workflow/renwei-copy-optimizer";
export async function POST(request: Request) {
const access = requireApiAccess(request);
if (!access.ok) {
return access.response;
}
try {
const payload = copyOptimizationRequestSchema.parse(await request.json());
const result = await optimizeRenweiCopy(payload);
return NextResponse.json({ result });
} catch (error) {
return jsonError(error, getErrorStatus(error));
}
}
function jsonError(error: unknown, status: number) {
const message =
error instanceof z.ZodError
? "请输入需要优化的文案"
: error instanceof Error
? error.message
: "文案优化失败";
return NextResponse.json({ error: message }, { status });
}
function getErrorStatus(error: unknown) {
if (error instanceof LlmValidationError) return 502;
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) return 502;
return 400;
}