"use strict"; if (process.env.MNOTE_ALLOW_RETIRED_AI_AGENT_RUN_SMOKE !== "1") { console.log(JSON.stringify({ ok: true, retired: true, script: "task052-ai-tools-runtime-smoke.js", reason: "旧 /api/ai-agent/run AI tools runtime smoke 已退役;当前请运行 scripts/task-hermes-page-ai-retirement-guard.js 验证 legacy guard。", }, null, 2)); process.exit(0); } const { chromium } = require("playwright"); const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000"; const REQUEST_TIMEOUT_MS = 120_000; const UI_TIMEOUT_MS = 30_000; const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录"; async function requestJsonWithCookieHeader(path, init = {}, cookieHeader = "") { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { const response = await fetch(`${BASE_URL}${path}`, { ...init, headers: { ...(init.data !== undefined ? { "content-type": "application/json" } : {}), ...(init.headers || {}), ...(cookieHeader ? { cookie: cookieHeader } : {}), }, body: init.data !== undefined ? JSON.stringify(init.data) : init.body, signal: controller.signal, }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { payload = text; } if (!response.ok) { throw new Error( `${path} 请求失败: ${response.status} ${response.statusText} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`, ); } const contentType = response.headers.get("content-type") || ""; if (!contentType.includes("application/json")) { throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`); } return payload; } finally { clearTimeout(timer); } } function assert(condition, message) { if (!condition) { throw new Error(message); } } async function requestJson(requestContext, path, init = {}) { const response = await requestContext.fetch(`${BASE_URL}${path}`, { ...init, headers: init.data !== undefined ? { "content-type": "application/json", ...(init.headers || {}), } : { ...(init.headers || {}), }, timeout: REQUEST_TIMEOUT_MS, }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { payload = text; } if (!response.ok()) { throw new Error( `${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`, ); } const contentType = response.headers()["content-type"] || ""; if (!contentType.includes("application/json")) { throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`); } return payload; } async function ensureAuthenticated(page, requestContext) { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const tryWhoAmI = async () => { try { return await requestJson(requestContext, "/api/auth/whoami", { method: "GET" }); } catch { return null; } }; await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); let viewer = await tryWhoAmI(); if (viewer) return viewer; const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME }); await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await quickLoginButton.click({ timeout: UI_TIMEOUT_MS }); for (let i = 0; i < 20; i += 1) { await sleep(500); viewer = await tryWhoAmI(); if (viewer) return viewer; } throw new Error("测试账号快速登录后仍无法获取 whoami"); } async function createTempDocument(requestContext) { const payload = await requestJson(requestContext, "/api/documents/create", { method: "POST", data: { parentId: null }, }); assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id"); assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败:缺少 workspace_id"); return { documentId: payload.id, workspaceId: payload.workspace_id }; } async function renameDocument(requestContext, documentId, workspaceId, title) { return await requestJson(requestContext, "/api/documents/title", { method: "POST", data: { documentId, workspaceId, title, commandName: "page.head.updateTitle" }, }); } async function saveDocument(requestContext, documentId, workspaceId, content) { return await requestJson(requestContext, "/api/documents/save", { method: "POST", data: { documentId, workspaceId, content }, }); } async function purgeTempDocument(requestContext, documentId) { return await requestJson(requestContext, "/api/documents/purge", { method: "POST", data: { documentId }, }); } async function runAiAgentDocsSmoke(requestContext, uniqueTitle, needleText, cookieHeader) { const payload = await requestJsonWithCookieHeader("/api/ai-agent/run", { method: "POST", data: { stream: false, maxSteps: 4, scope: "document", messages: [ { role: "user", content: `{"query":"${uniqueTitle}","limit":5}`, }, ], toolChoice: { mode: "manual", toolSets: ["toolset.docs_read"], tools: ["docs_search", "docs_read"], }, context: { documentId: "smoke-doc-context", documentBlocks: [], }, options: { ai: { provider: "codex", sessionId: "", }, }, }, }, cookieHeader); assert(Array.isArray(payload.events), "AI Agent 返回缺少 events"); const toolResults = payload.events.filter((event) => event && event.type === "tool_result"); assert(toolResults.length >= 1, "AI Agent 未返回任何 tool_result"); const searchResultEvent = toolResults.find((event) => event.data && event.data.tool === "docs_search" && event.data.ok === true); assert(searchResultEvent, "docs_search 未成功执行"); const searchResults = searchResultEvent.data.result && Array.isArray(searchResultEvent.data.result.results) ? searchResultEvent.data.result.results : []; assert(searchResults.length >= 1, "docs_search 没有返回结果"); const top = searchResults[0]; assert(typeof top.id === "string" && top.id, "docs_search 首条结果缺少 documentId"); const readPayload = await requestJsonWithCookieHeader("/api/ai-agent/run", { method: "POST", data: { stream: false, maxSteps: 4, scope: "document", messages: [ { role: "user", content: `{"documentId":"${top.id}","maxChars":2500,"includeContent":false}`, }, ], toolChoice: { mode: "manual", toolSets: ["toolset.docs_read"], tools: ["docs_read"], }, context: { documentId: "smoke-doc-context", documentBlocks: [], }, options: { ai: { provider: "codex", sessionId: "", }, }, }, }, cookieHeader); assert(Array.isArray(readPayload.events), "docs_read 返回缺少 events"); const readEvent = readPayload.events.find((event) => event && event.type === "tool_result" && event.data && event.data.tool === "docs_read" && event.data.ok === true); assert(readEvent, "docs_read 未成功执行"); const rawText = String(readEvent.data.result?.rawText ?? ""); assert(rawText.includes(needleText), `docs_read 返回未命中预期正文片段:${needleText}`); return { searchDocumentId: top.id, searchResultsCount: searchResults.length, readRawTextLength: Number(readEvent.data.result?.rawTextLength ?? 0), }; } async function main() { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); const page = await context.newPage(); let tempDocument = null; let caughtError = null; try { await ensureAuthenticated(page, context.request); tempDocument = await createTempDocument(context.request); const uniqueSuffix = `${Date.now()}`; const uniqueTitle = `task052-ai-runtime-${uniqueSuffix}`; const needleText = `task052 Rust docs runtime smoke ${uniqueSuffix}`; await renameDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, uniqueTitle); await saveDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, [ { id: `task052_block_${uniqueSuffix}`, type: "paragraph", props: {}, content: [{ type: "text", text: needleText }], children: [], }, ]); const cookies = await context.cookies(BASE_URL); const cookieHeader = cookies.map((item) => `${item.name}=${item.value}`).join("; "); const runtime = await runAiAgentDocsSmoke(context.request, uniqueTitle, needleText, cookieHeader); console.log(JSON.stringify({ ok: true, workspaceId: tempDocument.workspaceId, documentId: tempDocument.documentId, title: uniqueTitle, needleText, ...runtime, }, null, 2)); } catch (error) { caughtError = error; } finally { if (tempDocument?.documentId) { try { await purgeTempDocument(context.request, tempDocument.documentId); } catch (cleanupError) { if (!caughtError) { caughtError = cleanupError; } else { console.error(`清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`); } } } await page.close().catch(() => undefined); await context.close().catch(() => undefined); await browser.close().catch(() => undefined); } if (caughtError) { throw caughtError; } } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });