import { NextRequest } from "next/server"; import { invokeAgent } from "@/lib/agents/invoke"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; type Body = { agent: string; /** User's natural-language request, e.g. "write a tweet about X". */ instruction: string; /** Existing markdown in the editor — given as context so the agent can * continue the user's voice instead of writing in isolation. */ context?: string; model?: string; /** Optional absolute path to the agent binary; see /api/convert. */ binOverride?: string; }; function buildDraftPrompt(args: { instruction: string; context: string }): string { const ctx = args.context.trim(); return `你正在为用户起草一段 **markdown** 内容(不是 HTML,不是 JSON,不是代码)。 【硬性规则】 1. 只输出 markdown 正文,不要任何前后解释、不要 \`\`\`md 围栏、不要"以下是…"开头。 2. 第一个字符就是正文。最后一个字符是正文末尾。 3. 不要捏造数据、不要凭空添加引用链接。 4. 标题、列表、强调、引用、代码块按 markdown 语法书写。 5. 如果用户没有指定语言,使用与"已有内容"一致的语言;都没有就用中文。 6. 长度控制:除非用户明确要求长文,控制在 300 字以内。 【用户当前编辑器里的内容(可能为空)】 ${ctx ? ctx : "(空)"} 【用户的需求】 ${args.instruction} `; } export async function POST(req: NextRequest) { let body: Body; try { body = (await req.json()) as Body; } catch { return new Response("invalid JSON body", { status: 400 }); } const { agent, instruction, context = "", model, binOverride } = body; if (!agent || !instruction?.trim()) { return new Response("missing required fields: agent, instruction", { status: 400, }); } const prompt = buildDraftPrompt({ instruction, context }); const abortCtl = new AbortController(); req.signal?.addEventListener("abort", () => abortCtl.abort(), { once: true }); const stream = invokeAgent({ agent, prompt, model, binOverride, signal: abortCtl.signal, }); const sse = new ReadableStream({ async start(controller) { const enc = new TextEncoder(); let outClosed = false; const send = (event: string, data: unknown) => { if (outClosed) return; try { controller.enqueue( enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`), ); } catch { outClosed = true; } }; const reader = stream.getReader(); try { while (true) { const { value, done } = await reader.read(); if (done) break; if (!value) continue; send(value.type, value); } } catch (err) { send("error", { message: err instanceof Error ? err.message : String(err), }); } finally { outClosed = true; try { controller.close(); } catch {} } }, cancel() { abortCtl.abort(); }, }); return new Response(sse, { headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no", }, }); }