# 普通文案人味儿优化 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add an independent `普通文案优化` tab that optimizes pasted copy with renwei-writing-inspired rules, returning optimized text, change notes, and AI-taste checks. **Architecture:** Add shared copy-optimization domain types and zod schemas, a prompt/workflow module that calls the existing validated LLM JSON path, a protected API route, and a focused React panel rendered behind a new home-page tab. The existing GEO article workflow stays the default tab and keeps its current state and behavior. **Tech Stack:** Next.js App Router, React client components, TypeScript, Zod, existing `generateValidatedJson` LLM client, Vitest, Playwright. --- ## File Structure - Modify `src/lib/domain/types.ts`: add copy optimization request/result interfaces and enums. - Modify `src/lib/domain/validation.ts`: add zod schemas for copy optimization request and result. - Modify `src/lib/domain/__tests__/validation.test.ts`: add schema tests. - Modify `src/lib/llm/client.ts`: add the `renwei_copy_optimizer` task name. - Modify `src/lib/llm/prompts.ts`: add renwei system prompt and prompt builder. - Modify `src/lib/llm/__tests__/prompts.test.ts`: add prompt coverage. - Create `src/lib/workflow/renwei-copy-optimizer.ts`: call the LLM with the prompt and result schema. - Create `src/app/api/copy/renwei-optimize/route.ts`: protected POST endpoint. - Create `src/app/api/__tests__/copy-renwei.test.ts`: API tests with mocked LLM. - Create `src/components/renwei-copy-optimizer-panel.tsx`: client UI for the new tool. - Modify `src/app/page.tsx`: add app-level tabs and render the new panel. - Modify `src/app/globals.css`: add small tab/result layout styles. - Create `tests/e2e/renwei-copy.spec.ts`: browser test using route interception for the new API. ## Task 1: Domain Types And Validation **Files:** - Modify: `src/lib/domain/types.ts` - Modify: `src/lib/domain/validation.ts` - Test: `src/lib/domain/__tests__/validation.test.ts` - [ ] **Step 1: Write the failing validation tests** Append these tests inside the existing `describe` block in `src/lib/domain/__tests__/validation.test.ts`: ```ts it("accepts a trimmed copy optimization request", () => { const parsed = copyOptimizationRequestSchema.parse({ source_text: " 我写了一段有点卡的文案 ", goal: "", intensity: "light", user_instructions: " 保留口语感 ", }); expect(parsed).toEqual({ source_text: "我写了一段有点卡的文案", goal: "保留原意,减少 AI 味", intensity: "light", user_instructions: "保留口语感", }); }); it("rejects empty copy optimization source text", () => { expect(() => copyOptimizationRequestSchema.parse({ source_text: " ", intensity: "light", }), ).toThrow(); }); it("accepts structured copy optimization results", () => { const parsed = copyOptimizationResultSchema.parse({ optimized_text: "我把句子顺了一下。", change_notes: [ { original: "我把句子顺顺。", revised: "我把句子顺了一下。", reason: "修正口语里不顺的重复。", confidence: "confident", revertible: false, }, ], ai_taste_checks: [ { rule_id: "promotion_tone", status: "pass", evidence: "没有新增宣传词。", suggestion: "", }, ], warnings: [], }); expect(parsed.optimized_text).toBe("我把句子顺了一下。"); expect(parsed.change_notes[0].confidence).toBe("confident"); expect(parsed.ai_taste_checks[0].rule_id).toBe("promotion_tone"); }); ``` Also update the import at the top of the test file: ```ts import { articleInputSchema, candidateFactCardSchema, confirmedFactCardSchema, copyOptimizationRequestSchema, copyOptimizationResultSchema, optimizedArticleSchema, qaReportSchema, } from "../validation"; ``` - [ ] **Step 2: Run the validation tests to verify RED** Run: ```bash npm test -- src/lib/domain/__tests__/validation.test.ts ``` Expected: FAIL with TypeScript or runtime errors saying `copyOptimizationRequestSchema` and `copyOptimizationResultSchema` are not exported. - [ ] **Step 3: Add the domain types** Append to `src/lib/domain/types.ts`: ```ts export type CopyOptimizationIntensity = "light" | "medium" | "conversational"; export type CopyChangeConfidence = "confident" | "uncertain"; export type CopyAiTasteRuleId = | "meaning_inflation" | "promotion_tone" | "formulaic_sentence" | "format_trace" | "chat_trace" | "filler_hedging"; export type CopyAiTasteStatus = "pass" | "warn"; export interface CopyOptimizationRequest { source_text: string; goal: string; intensity: CopyOptimizationIntensity; user_instructions: string; } export interface CopyChangeNote { original: string; revised: string; reason: string; confidence: CopyChangeConfidence; revertible: boolean; } export interface CopyAiTasteCheck { rule_id: CopyAiTasteRuleId; status: CopyAiTasteStatus; evidence: string; suggestion: string; } export interface CopyOptimizationResult { optimized_text: string; change_notes: CopyChangeNote[]; ai_taste_checks: CopyAiTasteCheck[]; warnings: string[]; } ``` - [ ] **Step 4: Add the zod schemas** In `src/lib/domain/validation.ts`, add the new imported type names: ```ts CopyAiTasteCheck, CopyChangeNote, CopyOptimizationRequest, CopyOptimizationResult, ``` Then append these schemas near the other exported schemas: ```ts export const copyOptimizationIntensitySchema = z.enum([ "light", "medium", "conversational", ]); export const copyOptimizationRequestSchema = z.object({ source_text: z.string().trim().min(1), goal: z .preprocess((value) => { if (typeof value !== "string") return value; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : "保留原意,减少 AI 味"; }, z.string().trim().min(1)) .default("保留原意,减少 AI 味"), intensity: copyOptimizationIntensitySchema.default("light"), user_instructions: z.string().trim().default(""), }) satisfies z.ZodType; const copyChangeNoteSchema = z.object({ original: requiredLlmStringSchema, revised: requiredLlmStringSchema, reason: requiredLlmStringSchema, confidence: z.enum(["confident", "uncertain"]), revertible: z.boolean(), }) satisfies z.ZodType; const copyAiTasteCheckSchema = z.object({ rule_id: z.enum([ "meaning_inflation", "promotion_tone", "formulaic_sentence", "format_trace", "chat_trace", "filler_hedging", ]), status: z.enum(["pass", "warn"]), evidence: requiredLlmStringSchema, suggestion: optionalLlmStringSchema, }) satisfies z.ZodType; export const copyOptimizationResultSchema = z.object({ optimized_text: requiredLlmStringSchema, change_notes: z.array(copyChangeNoteSchema).default([]), ai_taste_checks: z.array(copyAiTasteCheckSchema).default([]), warnings: stringListSchema, }) satisfies z.ZodType; ``` - [ ] **Step 5: Run the validation tests to verify GREEN** Run: ```bash npm test -- src/lib/domain/__tests__/validation.test.ts ``` Expected: PASS. - [ ] **Step 6: Commit Task 1** ```bash git add src/lib/domain/types.ts src/lib/domain/validation.ts src/lib/domain/__tests__/validation.test.ts git commit -m "新增普通文案优化数据校验" ``` ## Task 2: Prompt Builder And Workflow **Files:** - Modify: `src/lib/llm/client.ts` - Modify: `src/lib/llm/prompts.ts` - Modify: `src/lib/llm/__tests__/prompts.test.ts` - Create: `src/lib/workflow/renwei-copy-optimizer.ts` - Test: `src/lib/workflow/__tests__/llm-integration.test.ts` - [ ] **Step 1: Write the failing prompt tests** Update imports in `src/lib/llm/__tests__/prompts.test.ts`: ```ts RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, buildRenweiCopyOptimizationPrompt, ``` Append these tests: ```ts it("renwei copy prompt keeps the author present and defaults to small edits", () => { const prompt = `${RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT}\n${buildRenweiCopyOptimizationPrompt({ source_text: "我观察到大家越来越难进入心流了。", goal: "保留原意,减少 AI 味", intensity: "light", user_instructions: "保留作者的口语感。", })}`; expect(prompt).toContain("少动"); expect(prompt).toContain("保留作者"); expect(prompt).toContain("手迹"); expect(prompt).toContain("不凭空新增时间、地点、数字、案例"); expect(prompt).toContain("不写过度金句"); expect(prompt).toContain("逐处说明"); expect(prompt).toContain("只检查被改动句子"); expect(prompt).toContain("source_text"); }); it("renwei copy prompt describes all result fields and intensity modes", () => { const prompt = buildRenweiCopyOptimizationPrompt({ source_text: "这是一段文案。", goal: "更自然", intensity: "conversational", user_instructions: "", }); expect(prompt).toContain("optimized_text"); expect(prompt).toContain("change_notes"); expect(prompt).toContain("ai_taste_checks"); expect(prompt).toContain("warnings"); expect(prompt).toContain("light"); expect(prompt).toContain("medium"); expect(prompt).toContain("conversational"); }); ``` - [ ] **Step 2: Run prompt tests to verify RED** Run: ```bash npm test -- src/lib/llm/__tests__/prompts.test.ts ``` Expected: FAIL because `RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT` and `buildRenweiCopyOptimizationPrompt` are not exported. - [ ] **Step 3: Write the failing workflow test** In `src/lib/workflow/__tests__/llm-integration.test.ts`, update the hoisted LLM mock tests by importing: ```ts import { optimizeRenweiCopy } from "../renwei-copy-optimizer"; ``` Append this test: ```ts it("uses validated LLM output for renwei copy optimization", 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 result = await optimizeRenweiCopy({ source_text: "我观察到大家越来越难进入心流", goal: "保留原意,减少 AI 味", intensity: "light", user_instructions: "", }); expect(result.optimized_text).toBe("我观察到大家越来越难进入心流了。"); expect(llmMocks.generateValidatedJson).toHaveBeenCalledWith( expect.objectContaining({ task: "renwei_copy_optimizer", temperature: 0.2, }), ); }); ``` - [ ] **Step 4: Run workflow test to verify RED** Run: ```bash npm test -- src/lib/workflow/__tests__/llm-integration.test.ts -t "renwei copy" ``` Expected: FAIL because `../renwei-copy-optimizer` does not exist or task type does not accept `renwei_copy_optimizer`. - [ ] **Step 5: Add the LLM task name** Modify `src/lib/llm/client.ts`: ```ts export type LlmTaskName = | "unknown" | "fact_extractor" | "article_optimizer" | "quality_inspector" | "targeted_rewriter" | "renwei_copy_optimizer"; ``` - [ ] **Step 6: Add prompt constants and builder** Append to `src/lib/llm/prompts.ts`: ```ts import type { CopyOptimizationRequest } from "../domain/types"; ``` If the file already has a type import from `../domain/types`, merge `CopyOptimizationRequest` into that existing import. Then add: ```ts const RENWEI_OUTPUT_CONTRACT = [ "Output type contract:", "- optimized_text must be a string.", "- change_notes must be an array of { original, revised, reason, confidence, revertible }.", "- confidence must be confident or uncertain.", "- revertible must be boolean.", "- ai_taste_checks must be an array of { rule_id, status, evidence, suggestion }.", "- rule_id must be one of meaning_inflation, promotion_tone, formulaic_sentence, format_trace, chat_trace, filler_hedging.", "- status must be pass or warn.", "- warnings must be string[].", "Example:", JSON.stringify( { optimized_text: "我把句子顺了一下。", change_notes: [ { original: "我把句子顺顺。", revised: "我把句子顺了一下。", reason: "修掉重复,让句子自然收住。", confidence: "confident", revertible: false, }, ], ai_taste_checks: [ { rule_id: "promotion_tone", status: "pass", evidence: "没有新增宣传腔。", suggestion: "", }, ], warnings: [], }, null, 2, ), ].join("\n"); export const RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT = [ "你是中文普通文案编辑,不是营销代笔工具。", "目标是让文字更顺,但改完后作者还在。", "硬性规则:少动,只改真正打绊的地方;保留作者的位置、语气、口头习惯和可识别手迹。", "不得凭空新增时间、地点、数字、案例、情绪、场景、资质、效果承诺或没有来源的事实。", "不要把普通句子改成宣传腔、排比、格言、万能展望、意义拔高或过度金句。", "拿不准时白描,不拔高;拿不准的改动必须标记为 uncertain 且 revertible 为 true。", "改后只检查被改动句子是否引入 AI 味,并逐处说明改了什么、为什么改。", JSON_ONLY_PROMPT, ].join(" "); function describeRenweiIntensity(intensity: CopyOptimizationRequest["intensity"]) { const descriptions = { light: "light / 轻微整理:尽量只修顺病句、错别字、明显卡顿。", medium: "medium / 适度润色:允许调整句序和连接,但不改变作者表达的粗糙感。", conversational: "conversational / 更口语自然:让口吻更像真人说话,但不凭空增加表演性口语。", } satisfies Record; return descriptions[intensity]; } export function buildRenweiCopyOptimizationPrompt(input: CopyOptimizationRequest) { return [ "Return a CopyOptimizationResult JSON object with these exact keys:", "optimized_text, change_notes, ai_taste_checks, warnings.", "", RENWEI_OUTPUT_CONTRACT, "", "强度选项:", "- light:轻微整理。", "- medium:适度润色。", "- conversational:更口语自然。", `当前修改强度:${describeRenweiIntensity(input.intensity)}`, "", "改稿要求:", "- 默认少动,不追求更漂亮,只清掉绊脚处。", "- 毛边先假设是作者手迹,不要机械删口语、停顿或重复。", "- 不写“不是X,而是Y”、排比三连、万能展望、宣传腔或假深刻句式。", "- 不使用破折号作为装饰性解释。", "- 必须逐处说明改动;未改动的句子不要编造改动说明。", "- AI 味检查只检查被改动句子。", "", "User request:", JSON.stringify( { goal: input.goal, intensity: input.intensity, user_instructions: input.user_instructions, }, null, 2, ), "", "source_text:", input.source_text, ].join("\n"); } ``` - [ ] **Step 7: Add the workflow module** Create `src/lib/workflow/renwei-copy-optimizer.ts`: ```ts import type { CopyOptimizationRequest, CopyOptimizationResult, } from "../domain/types"; import { copyOptimizationResultSchema } from "../domain/validation"; import { generateValidatedJson } from "../llm/client"; import { RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, buildRenweiCopyOptimizationPrompt, } from "../llm/prompts"; export async function optimizeRenweiCopy( input: CopyOptimizationRequest, ): Promise { return generateValidatedJson({ schema: copyOptimizationResultSchema, system: RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, prompt: buildRenweiCopyOptimizationPrompt(input), temperature: 0.2, task: "renwei_copy_optimizer", }); } ``` - [ ] **Step 8: Run prompt and workflow tests to verify GREEN** Run: ```bash npm test -- src/lib/llm/__tests__/prompts.test.ts src/lib/workflow/__tests__/llm-integration.test.ts ``` Expected: PASS. - [ ] **Step 9: Commit Task 2** ```bash git add src/lib/llm/client.ts src/lib/llm/prompts.ts src/lib/llm/__tests__/prompts.test.ts src/lib/workflow/renwei-copy-optimizer.ts src/lib/workflow/__tests__/llm-integration.test.ts git commit -m "新增人味儿文案优化提示词" ``` ## Task 3: Protected API Route **Files:** - Create: `src/app/api/copy/renwei-optimize/route.ts` - Create: `src/app/api/__tests__/copy-renwei.test.ts` - [ ] **Step 1: Write the failing API route tests** Create `src/app/api/__tests__/copy-renwei.test.ts`: ```ts 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( "../../../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 = { "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, }); } ``` - [ ] **Step 2: Run API tests to verify RED** Run: ```bash npm test -- src/app/api/__tests__/copy-renwei.test.ts ``` Expected: FAIL because `../copy/renwei-optimize/route` does not exist. - [ ] **Step 3: Add the route implementation** Create `src/app/api/copy/renwei-optimize/route.ts`: ```ts 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; } ``` - [ ] **Step 4: Run API tests to verify GREEN** Run: ```bash npm test -- src/app/api/__tests__/copy-renwei.test.ts ``` Expected: PASS. - [ ] **Step 5: Commit Task 3** ```bash git add src/app/api/copy/renwei-optimize/route.ts src/app/api/__tests__/copy-renwei.test.ts git commit -m "新增普通文案优化接口" ``` ## Task 4: UI Tab And Panel **Files:** - Create: `src/components/renwei-copy-optimizer-panel.tsx` - Modify: `src/app/page.tsx` - Modify: `src/app/globals.css` - Create: `tests/e2e/renwei-copy.spec.ts` - [ ] **Step 1: Write the failing E2E test** Create `tests/e2e/renwei-copy.spec.ts`: ```ts import { expect, test } from "@playwright/test"; test("普通文案优化标签页可以生成文案结果", async ({ page }) => { await page.route("**/api/copy/renwei-optimize", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ result: { optimized_text: "我观察到大家越来越难进入心流了。", change_notes: [ { original: "我观察到大家越来越难进入心流", revised: "我观察到大家越来越难进入心流了。", reason: "补足句尾语气,让句子自然收住。", confidence: "confident", revertible: false, }, ], ai_taste_checks: [ { rule_id: "promotion_tone", status: "pass", evidence: "没有新增宣传腔。", suggestion: "", }, ], warnings: [], }, }), }); }); await page.goto("/"); await expect(page.getByRole("heading", { name: "GEO 智能文章优化器" })).toBeVisible(); await expect(page.getByText("文章输入")).toBeVisible(); await page.getByRole("button", { name: "普通文案优化" }).click(); await expect(page.getByLabel("原始文案")).toBeVisible(); await page.getByLabel("访问密钥").fill("local-dev-key"); await page .getByLabel("原始文案") .fill("我观察到大家越来越难进入心流"); await page.getByRole("button", { name: "优化文案" }).click(); await expect(page.getByText("优化后文案")).toBeVisible(); await expect(page.getByText("我观察到大家越来越难进入心流了。")).toBeVisible(); await expect(page.getByText("改动说明")).toBeVisible(); await expect(page.getByText("AI 味检查")).toBeVisible(); }); ``` - [ ] **Step 2: Run E2E test to verify RED** Run: ```bash npx playwright test tests/e2e/renwei-copy.spec.ts ``` Expected: FAIL because the `普通文案优化` tab does not exist. - [ ] **Step 3: Create the client panel** Create `src/components/renwei-copy-optimizer-panel.tsx`: ```tsx "use client"; import { useState, type FormEvent } from "react"; import type { CopyOptimizationIntensity, CopyOptimizationResult, } from "../lib/domain/types"; interface RenweiCopyOptimizerPanelProps { apiAccessKey: string; } interface CopyOptimizeResponse { result?: CopyOptimizationResult; error?: string; } const intensityOptions: Array<{ value: CopyOptimizationIntensity; label: string; }> = [ { value: "light", label: "轻微整理" }, { value: "medium", label: "适度润色" }, { value: "conversational", label: "更口语自然" }, ]; export function RenweiCopyOptimizerPanel({ apiAccessKey, }: RenweiCopyOptimizerPanelProps) { const [sourceText, setSourceText] = useState(""); const [goal, setGoal] = useState("保留原意,减少 AI 味"); const [intensity, setIntensity] = useState("light"); const [userInstructions, setUserInstructions] = useState(""); const [result, setResult] = useState(null); const [message, setMessage] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); setIsSubmitting(true); setMessage(""); setResult(null); try { const response = await fetch("/api/copy/renwei-optimize", { method: "POST", headers: apiHeaders(apiAccessKey), body: JSON.stringify({ source_text: sourceText, goal, intensity, user_instructions: userInstructions, }), }); const body = (await response.json()) as CopyOptimizeResponse; if (!response.ok || !body.result) { throw new Error(body.error ?? "文案优化失败"); } setResult(body.result); setMessage("文案优化完成。"); } catch (error) { setMessage(error instanceof Error ? error.message : "文案优化失败"); } finally { setIsSubmitting(false); } } async function copyResult() { if (!result?.optimized_text) return; await navigator.clipboard.writeText(result.optimized_text); setMessage("已复制优化结果。"); } return (
普通文案优化