From d376bd0df78c86e224663bc46c876a13f6bf4861 Mon Sep 17 00:00:00 2001 From: czj <13261895355@163.com> Date: Thu, 16 Jul 2026 13:21:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E8=BF=BD=E8=B8=AA=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E7=AB=9E=E6=80=81=E4=B8=8E=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/jobs/optimize-stream/route.ts | 17 ++++--- src/lib/db/__tests__/repositories.test.ts | 2 + tests/e2e/mvp.spec.ts | 59 +++++++++++++++++++++-- tests/e2e/sample-flow.spec.ts | 7 ++- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/app/api/jobs/optimize-stream/route.ts b/src/app/api/jobs/optimize-stream/route.ts index aebebfc..e08751e 100644 --- a/src/app/api/jobs/optimize-stream/route.ts +++ b/src/app/api/jobs/optimize-stream/route.ts @@ -106,12 +106,7 @@ export async function POST(request: Request) { user_instructions: normalized.articleInput.user_instructions, }, }); - send({ - type: "job_created", - job: { id: job.id }, - case: { id: optimizationCase.id, case_type: "article" }, - }); - + let traceSetupError: unknown; try { traceRecorder = await createLlmTraceRecorder({ jobId: job.id, @@ -121,11 +116,19 @@ export async function POST(request: Request) { publish: (event) => send(event), }); } catch (error) { + traceSetupError = error; + } + send({ + type: "job_created", + job: { id: job.id }, + case: { id: optimizationCase.id, case_type: "article" }, + }); + if (traceSetupError) { send({ type: "trace_warning", job_id: job.id, trace_completeness: "incomplete", - error_summary: safeTraceError(error), + error_summary: safeTraceError(traceSetupError), }); } diff --git a/src/lib/db/__tests__/repositories.test.ts b/src/lib/db/__tests__/repositories.test.ts index 02c34bc..08fca6c 100644 --- a/src/lib/db/__tests__/repositories.test.ts +++ b/src/lib/db/__tests__/repositories.test.ts @@ -52,6 +52,8 @@ describe("sqlite repositories", () => { "calibration_events", "case_inputs", "fact_cards", + "llm_trace_calls", + "llm_trace_runs", "optimization_cases", "optimization_result_versions", "optimized_articles", diff --git a/tests/e2e/mvp.spec.ts b/tests/e2e/mvp.spec.ts index 7966e0a..4cd9926 100644 --- a/tests/e2e/mvp.spec.ts +++ b/tests/e2e/mvp.spec.ts @@ -1,10 +1,61 @@ import { expect, test } from "@playwright/test"; test("中文界面可以生成优化文章和导出链接", async ({ page }) => { - test.setTimeout(180_000); + const jobId = "job_mvp"; + const factCard = { + company_full_name: "Example Technology Co., Ltd.", + company_short_names: ["Example Tech"], + brand_names: ["Example"], + product_names: ["Example GEO"], + target_industry: "GEO optimization", + target_audience: "市场团队", + experience_years: 8, + core_claims: ["拥有 8 年 GEO 优化经验"], + forbidden_claims: [], + image_topics: ["产品仪表盘"], + uncertain_items: [], + is_ready_for_optimization: true, + confirmed_by_user: false, + }; + const article = { + job_id: jobId, + revision: 1, + title: "Example Technology Co., Ltd. GEO 指南", + summary: "面向市场团队的 GEO 优化说明。", + body_markdown: "## 服务能力\nExample GEO 帮助团队优化内容结构。", + image_suggestions: [], + changed_sections: ["title", "body"], + requires_user_confirmation: [], + }; + const qaReport = { + job_id: jobId, + revision: 1, + overall_status: "pass", + checks: [], + }; + + await page.route("**/api/jobs/optimize-stream", async (route) => { + const events = [ + { type: "job_created", job: { id: jobId } }, + { type: "fact_card_ready", job_id: jobId, fact_card: factCard }, + { type: "draft_ready", job_id: jobId, article }, + { type: "qa_ready", job_id: jobId, qa_report: qaReport }, + { + type: "final_ready", + job_id: jobId, + optimized_article: article, + qa_report: qaReport, + export_paths: {}, + }, + ]; + await route.fulfill({ + status: 200, + contentType: "application/x-ndjson; charset=utf-8", + body: `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, + }); + }); await page.goto("/"); - await page.getByLabel("访问密钥").fill("local-dev-key"); await page .getByLabel("文章内容") @@ -15,9 +66,7 @@ test("中文界面可以生成优化文章和导出链接", async ({ page }) => await page.getByLabel("用户要求").fill("保持事实准确,语气自然。"); await page.getByRole("button", { name: "开始优化" }).click(); - await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible({ - timeout: 120_000, - }); + await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible(); await expect(page.getByText("质量报告")).toBeVisible(); await expect(page.getByRole("link", { name: "optimized.docx" })).toBeVisible(); await expect(page.getByRole("link", { name: "qa_report.json" })).toBeVisible(); diff --git a/tests/e2e/sample-flow.spec.ts b/tests/e2e/sample-flow.spec.ts index b8cb057..c0c02e7 100644 --- a/tests/e2e/sample-flow.spec.ts +++ b/tests/e2e/sample-flow.spec.ts @@ -18,7 +18,7 @@ const samplesDir = const reportDir = process.env.GEO_SAMPLE_REPORT_DIR ?? join(repoRoot, "test-results", "geo-sample-flow", "manual"); -const baseURL = process.env.GEO_SAMPLE_BASE_URL ?? "http://127.0.0.1:3000"; +const baseURL = process.env.GEO_SAMPLE_BASE_URL ?? "http://localhost:3000"; const apiAccessKey = process.env.API_ACCESS_KEY ?? "local-dev-key"; const timeoutMs = Number(process.env.GEO_SAMPLE_TIMEOUT_MS ?? "600000"); const filter = process.env.GEO_SAMPLE_FILTER; @@ -26,8 +26,13 @@ const limit = Number(process.env.GEO_SAMPLE_LIMIT ?? "0"); const loaded = loadArticleSamples(samplesDir); const selectedSamples = selectSamples(loaded.valid, filter, limit); +const provider = process.env.LLM_PROVIDER ?? "deepseek"; +const hasLiveLlmCredentials = provider === "openai" + ? Boolean(process.env.OPENAI_API_KEY) + : Boolean(process.env.DEEPSEEK_API_KEY); test.describe("GEO sample article live E2E flow", () => { + test.skip(!hasLiveLlmCredentials, "需要真实 LLM 提供商密钥"); test.beforeAll(() => { mkdirSync(reportDir, { recursive: true }); writeFileSync(