customer-waiting.mjs 6.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. import assert from 'node:assert/strict';
  2. import { mkdir, readFile, writeFile } from 'node:fs/promises';
  3. import { chromium } from 'playwright';
  4. // 准备资料必须来自真实 request_clarification 工具运行;此脚本不替换 HTTP 响应。
  5. const fixture = JSON.parse(await readFile(new URL('../../.work/frontend-waiting.json', import.meta.url), 'utf8'));
  6. const started = Date.now();
  7. const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
  8. const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE });
  9. const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
  10. page.setDefaultTimeout(20000);
  11. const errors = [], checks = [], resumed = [];
  12. let token, report, browserRequests = 0, serviceRequests = 0;
  13. page.on('pageerror', error => errors.push(error.message));
  14. page.on('request', request => { if (/\/(openapi|admin)\//.test(request.url())) browserRequests++; if (request.method() === 'POST' && request.url().endsWith('/inputs')) resumed.push(request); });
  15. const content = page.locator('.mobile .content');
  16. const timeout = setTimeout(() => { void browser.close(); }, 90000);
  17. async function navigate(target) {
  18. await page.evaluate(value => { location.hash = `/customer/${value}`; }, target);
  19. await page.locator(`.mobile[data-page="${target}"]`).waitFor();
  20. await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]'));
  21. }
  22. try {
  23. await page.goto(`${base}/#/customer/login`);
  24. await content.getByLabel('账户', { exact: true }).fill(fixture.username);
  25. await content.getByLabel('密码', { exact: true }).fill(fixture.password);
  26. await content.getByRole('checkbox').check();
  27. const login = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/auth/sessions'));
  28. await content.getByRole('button', { name: '登录并继续', exact: true }).click();
  29. const loginResponse = await login;
  30. assert.equal(loginResponse.status(), 200);
  31. token = (await loginResponse.json()).access_token;
  32. await content.locator('.message').filter({ hasText: fixture.question }).waitFor();
  33. await content.getByText('请补充助手询问的信息,继续本次咨询。', { exact: false }).waitFor();
  34. await page.waitForFunction(() => [...document.querySelectorAll('.mobile button')].some(button => button.textContent === '发送' && !button.disabled));
  35. assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isEnabled(), true);
  36. checks.push('重新登录读取真实等待运行,显示助手问题并开放原输入控件');
  37. await navigate('profile');
  38. await navigate('chat');
  39. await content.getByText('请补充助手询问的信息,继续本次咨询。', { exact: false }).waitFor();
  40. const answer = `键盘空格键无法使用,申请数量为 1。浏览器验证 ${Date.now()}`;
  41. await content.getByLabel('补充问题', { exact: true }).fill(answer);
  42. const receiving = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/agent/runs/${fixture.run_id}/inputs`));
  43. await content.getByRole('button', { name: '发送', exact: true }).click();
  44. const response = await receiving;
  45. assert.equal(response.status(), 202, await response.text());
  46. const result = await response.json();
  47. assert.equal(result.run.run_id, fixture.run_id);
  48. assert.equal(result.run.status, 'QUEUED');
  49. assert.equal(result.run.model_calls, 0);
  50. assert.equal(resumed.length, 1);
  51. assert.equal(resumed[0].postDataJSON().answer, answer);
  52. assert.ok(resumed[0].postDataJSON().interrupt_id);
  53. assert.ok(resumed[0].postDataJSON().expected_run_version >= 1);
  54. assert.ok(resumed[0].headers()['idempotency-key']);
  55. await content.locator('.message.customer').filter({ hasText: answer }).waitFor();
  56. assert.equal(await content.getByRole('button', { name: '发送', exact: true }).isDisabled(), true);
  57. checks.push('补充资料提交原 run_id、等待标识和版本,真实接口返回 202,SSE 显示本人消息');
  58. const cancelling = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/agent/runs/${fixture.run_id}/cancel`));
  59. await content.getByRole('button', { name: '停止本次回复', exact: true }).click();
  60. assert.equal((await cancelling).status(), 200);
  61. await content.getByRole('button', { name: '停止本次回复', exact: true }).waitFor({ state: 'hidden' });
  62. serviceRequests++;
  63. const current = await page.request.get(`${base}/openapi/v1/agent/runs/${fixture.run_id}`, { headers: { Authorization: `Bearer ${token}` } });
  64. assert.equal(current.status(), 200);
  65. const run = await current.json();
  66. assert.equal(run.status, 'CANCELLED');
  67. assert.equal(run.model_calls, 0);
  68. checks.push('恢复后停止原运行,取消事件实时释放输入,生成模型调用为零');
  69. assert.deepEqual(errors, []);
  70. report = { date: new Date().toISOString(), checks, browserErrors: errors, inputRequests: resumed.length, generatedModelCalls: run.model_calls };
  71. console.log(checks.join('\n'));
  72. } finally {
  73. clearTimeout(timeout);
  74. if (token) {
  75. serviceRequests++;
  76. const current = await page.request.get(`${base}/openapi/v1/agent/runs/${fixture.run_id}`, { headers: { Authorization: `Bearer ${token}` } });
  77. if (current.ok() && ['WAITING_USER', 'QUEUED', 'RUNNING'].includes((await current.json()).status)) { serviceRequests++; await page.request.post(`${base}/openapi/v1/agent/runs/${fixture.run_id}/cancel`, { headers: { Authorization: `Bearer ${token}` } }); }
  78. serviceRequests++;
  79. await page.request.delete(`${base}/openapi/v1/auth/sessions/current`, { headers: { Authorization: `Bearer ${token}` } });
  80. }
  81. await browser.close();
  82. if (report) {
  83. await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
  84. await writeFile(new URL('../../.work/frontend-verification/customer-waiting.json', import.meta.url), JSON.stringify({ ...report, seconds: (Date.now() - started) / 1000, browserRequests, serviceRequests, requestCount: browserRequests + serviceRequests, durationLimitSeconds: 90 }, null, 2));
  85. }
  86. }