新增普通文案优化接口
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user