| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- import assert from 'node:assert/strict';
- import { spawn } from 'node:child_process';
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
- import { fileURLToPath } from 'node:url';
- import { chromium } from 'playwright';
- // 整项最多运行 100 秒;浏览器发送一个问候,单次工作进程共享最多三次模型尝试。
- const root = fileURLToPath(new URL('../../', import.meta.url));
- const base = (process.env.FRONTEND_URL || 'http://127.0.0.1:18763').replace(/\/$/, '');
- const python = fileURLToPath(new URL('../../.venv/Scripts/python.exe', import.meta.url));
- const reportUrl = new URL('../../.work/frontend-verification/customer-model-worker.json', import.meta.url);
- const started = Date.now();
- let worker, browser, page, runId, customerToken, prepared = false;
- const errors = [];
- const report = { maximumDurationSeconds: 100, maximumProviderAttempts: 3, requestedMessages: 1 };
- // 执行阶段最多 85 秒,另外保留 15 秒关闭专项账户与保存检查记录。
- const remaining = () => Math.max(1, 85000 - (Date.now() - started));
- const deadline = setTimeout(() => {
- worker?.kill();
- browser?.close();
- }, 85000);
- async function helper(args) {
- return new Promise((resolve, reject) => {
- worker = spawn(python, ['scripts/check_browser_model.py', ...args], {
- cwd: root, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env: process.env,
- });
- let stderr = '';
- worker.stdout.on('data', () => {});
- worker.stderr.on('data', chunk => { stderr += chunk.toString(); });
- worker.on('error', reject);
- worker.on('close', code => {
- worker = null;
- if (code === 0) resolve();
- else reject(new Error(`单次模型工作进程退出:${code}\n${stderr}`));
- });
- });
- }
- try {
- assert.ok(process.env.DEEPSEEK_API_KEY, '进程环境必须提供 DEEPSEEK_API_KEY');
- await helper(['--preflight', '--prepare-account', '--timeout-seconds', '10']);
- prepared = true;
- const accounts = JSON.parse(await readFile(new URL('../../.work/browser-model-account.json', import.meta.url), 'utf8'));
- browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || 'C:/Users/Administrator/AppData/Local/ms-playwright/chromium-1228/chrome-win64/chrome.exe' });
- const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
- page = await context.newPage();
- page.setDefaultTimeout(12000);
- page.on('pageerror', error => errors.push(error.message));
- const auth = await context.request.post(`${base}/openapi/v1/auth/sessions`, {
- data: { tenant_id: accounts.tenant_id, username: accounts.username, password: accounts.password },
- });
- assert.equal(auth.status(), 200);
- const preparationToken = (await auth.json()).access_token;
- const created = await context.request.post(`${base}/openapi/v1/conversations`, {
- data: { store_id: accounts.store_id },
- headers: { Authorization: `Bearer ${preparationToken}`, 'Idempotency-Key': crypto.randomUUID() },
- });
- assert.equal(created.status(), 201);
- const conversation = await created.json();
- assert.equal(conversation.control_owner, 'AGENT');
- const preparatoryLogout = await context.request.delete(`${base}/openapi/v1/auth/sessions/current`, {
- headers: { Authorization: `Bearer ${preparationToken}` },
- });
- assert.equal(preparatoryLogout.status(), 204);
- report.conversationId = conversation.conversation_id;
- await page.goto(`${base}/#/customer/login`);
- await page.getByLabel('账户', { exact: true }).fill(accounts.username);
- await page.getByLabel('密码', { exact: true }).fill(accounts.password);
- await page.getByRole('checkbox', { name: '确认进入当前商家的客户服务' }).click();
- const loggedIn = page.waitForResponse(response => response.url().endsWith('/openapi/v1/auth/sessions') && response.request().method() === 'POST');
- const selectedConversation = page.waitForResponse(response => response.request().method() === 'GET' && new URL(response.url()).pathname === `/openapi/v1/conversations/${conversation.conversation_id}`);
- await page.getByRole('button', { name: '登录并继续', exact: true }).click();
- const loginResponse = await loggedIn;
- assert.equal(loginResponse.status(), 200);
- customerToken = (await loginResponse.json()).access_token;
- assert.equal((await selectedConversation).status(), 200, '页面读取本次新建会话');
- await page.getByLabel('补充问题', { exact: true }).fill('你好,请仅回复一句简短的中文问候。');
- const accepted = page.waitForResponse(response => response.request().method() === 'POST' && new URL(response.url()).pathname === `/openapi/v1/conversations/${conversation.conversation_id}/messages`);
- await page.getByRole('button', { name: '发送', exact: true }).click();
- const submission = await accepted;
- assert.equal(submission.status(), 201);
- const event = await submission.json();
- runId = event.payload.run_id;
- assert.ok(runId, '浏览器消息受理返回真实 Agent 运行');
- report.runId = runId;
- await page.getByText('助手正在查询,请稍候。', { exact: false }).waitFor();
- assert.equal(await page.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
- // 真实前端轮询必须取得本次运行的 assistant 事件,固定欢迎文字不能通过检查。
- const answer = page.waitForResponse(async response => {
- if (response.request().method() !== 'GET' || new URL(response.url()).pathname !== `/openapi/v1/conversations/${conversation.conversation_id}/messages`) return false;
- if (!response.ok()) return false;
- return (await response.json()).items.some(item => item.type === 'message.assistant' && item.payload.run_id === runId);
- }, { timeout: remaining() });
- const duration = Math.min(85, Math.floor((remaining() - 5000) / 1000));
- assert.ok(duration > 0, '仍有单次模型执行时间');
- const [, received] = await Promise.all([
- helper(['--run-id', runId, '--conversation-id', conversation.conversation_id, '--report', fileURLToPath(reportUrl), '--timeout-seconds', String(duration)]),
- answer,
- ]);
- const assistant = (await received.json()).items.find(item => item.type === 'message.assistant' && item.payload.run_id === runId);
- assert.equal(typeof assistant.payload.text, 'string');
- assert.ok(assistant.payload.text.trim());
- await page.locator('.message').filter({ hasText: assistant.payload.text }).filter({ hasText: 'AI 购物助手' }).waitFor();
- await page.getByText('助手正在查询,请稍候。', { exact: false }).waitFor({ state: 'hidden' });
- await page.getByLabel('补充问题', { exact: true }).fill('下一条消息保持未提交');
- assert.equal(await page.getByRole('button', { name: '发送', exact: true }).isEnabled(), true);
- const model = JSON.parse(await readFile(reportUrl, 'utf8'));
- assert.equal(model.run_status, 'COMPLETED');
- assert.ok(model.provider_attempts_started >= 1 && model.provider_attempts_started <= 3);
- assert.ok(model.effective_max_output_tokens <= 1024);
- assert.equal(model.token_entries.length, model.provider_attempts_started);
- assert.ok(model.model_attempts.every(item => item.status === 'RECEIVED' && item.usage.total_tokens > 0));
- assert.ok(model.token_entries.every(item => item.status === 'SETTLED' && item.usage.total_tokens > 0));
- assert.deepEqual(errors, []);
- report.passed = true;
- report.assistantAnswer = assistant.payload.text;
- report.model = model;
- report.browserErrors = errors;
- } finally {
- clearTimeout(deadline);
- worker?.kill();
- try {
- try {
- if (runId && customerToken && !report.passed) {
- // 浏览器关闭后仍通过真实 HTTP 取消本次运行,保留已发生用量。
- await fetch(`${base}/openapi/v1/agent/runs/${runId}/cancel`, {
- method: 'POST', headers: { Authorization: `Bearer ${customerToken}` }, signal: AbortSignal.timeout(3000),
- });
- }
- } finally {
- if (prepared) {
- await helper(['--cleanup-account', '--timeout-seconds', '5']);
- report.accountCleanup = '已停用专项账户、撤销全部会话并删除本地密码文件;实际用量与业务事件保留';
- }
- report.elapsedSeconds = (Date.now() - started) / 1000;
- report.browserErrors = errors;
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
- await writeFile(new URL('../../.work/frontend-verification/customer-model.json', import.meta.url), JSON.stringify(report, null, 2));
- }
- } finally {
- await browser?.close();
- }
- }
- console.log(JSON.stringify({ passed: true, providerAttempts: report.model.provider_attempts_started, maxOutputTokens: report.model.effective_max_output_tokens, generatedReplyVisible: true }));
|