repro-new-session-send.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. #!/usr/bin/env node
  2. /**
  3. * 复现「新建会话后立刻发消息,消息不显示」(2026-11-11)
  4. *
  5. * 不读代码推断,在真实浏览器里按用户操作顺序跑:
  6. * 1. 打开聊天页 → 点「新建对话」→ 弹窗里选容器
  7. * 2. **立刻**在输入框里输入并回车
  8. * 3. 每 200ms 采样消息区,看消息何时出现、输入框何时清空
  9. *
  10. * 用法:node scripts/repro-new-session-send.mjs
  11. */
  12. import { chromium } from 'playwright';
  13. import { readFileSync } from 'node:fs';
  14. import { dirname, resolve } from 'node:path';
  15. import { fileURLToPath } from 'node:url';
  16. const here = dirname(fileURLToPath(import.meta.url));
  17. const login = JSON.parse(readFileSync(resolve(here, '../.claude/dev-login.json'), 'utf8'));
  18. const BASE = 'http://localhost:4200';
  19. const AGENT = process.argv[2] ?? 'lj-test';
  20. const b = await chromium.launch({ channel: 'chrome' });
  21. const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
  22. const page = await ctx.newPage();
  23. await page.addInitScript(
  24. ([token, me]) => {
  25. localStorage.setItem('fmode_session_token', token);
  26. localStorage.setItem('fmode_me', me);
  27. },
  28. [login.token, JSON.stringify(login.me)],
  29. );
  30. const errors = [];
  31. page.on('pageerror', (e) => errors.push(String(e).slice(0, 200)));
  32. page.on('console', (m) => {
  33. if (m.type() === 'error') errors.push('console: ' + m.text().slice(0, 200));
  34. });
  35. console.log('=== 1. 打开聊天页 ===');
  36. await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  37. await page.waitForTimeout(6000);
  38. console.log('=== 2. 点「新建对话」 ===');
  39. await page.locator('text=新建对话').first().click();
  40. await page.waitForTimeout(2000);
  41. console.log(`=== 3. 选容器 ${AGENT} ===`);
  42. const opt = page.locator(`text=${AGENT}`).last();
  43. await opt.click();
  44. await page.waitForTimeout(4000);
  45. console.log(' URL:', page.url());
  46. /* 确认输入框就绪 */
  47. const ta = page.locator('textarea').first();
  48. await ta.waitFor({ state: 'visible', timeout: 20_000 });
  49. console.log('\n=== 4. 立刻输入并回车(模拟用户"新建完就发")===');
  50. const TEXT = '测试' + Date.now().toString().slice(-4);
  51. await ta.fill(TEXT);
  52. const t0 = Date.now();
  53. await ta.press('Enter');
  54. console.log('\n=== 5. 采样(每 200ms,最多 20 秒)===');
  55. let firstSeenAt = null;
  56. let inputClearedAt = null;
  57. for (let i = 0; i < 100; i++) {
  58. const snap = await page.evaluate(() => {
  59. const ta = document.querySelector('textarea');
  60. return {
  61. inputVal: ta ? ta.value : '(无)',
  62. bodyText: (document.body.innerText || '').slice(0, 6000),
  63. };
  64. });
  65. const ms = Date.now() - t0;
  66. if (snap.inputVal === '' && inputClearedAt === null) {
  67. inputClearedAt = ms;
  68. console.log(` ${ms}ms 输入框已清空`);
  69. }
  70. /* 消息区出现该文本(排除输入框里的那份) */
  71. if (snap.bodyText.includes(TEXT) && firstSeenAt === null) {
  72. firstSeenAt = ms;
  73. console.log(` ${ms}ms ✅ 消息出现在页面上`);
  74. }
  75. if (i % 10 === 0 && firstSeenAt === null) {
  76. console.log(` ${ms}ms 还没出现 | 输入框="${snap.inputVal.slice(0, 20)}"`);
  77. }
  78. if (firstSeenAt !== null && inputClearedAt !== null) break;
  79. await page.waitForTimeout(200);
  80. }
  81. console.log('\n=== 结果 ===');
  82. console.log(`文本: "${TEXT}"`);
  83. console.log(`输入框清空: ${inputClearedAt === null ? '❌ 从未清空' : inputClearedAt + 'ms'}`);
  84. console.log(`消息出现: ${firstSeenAt === null ? '❌ 20 秒内从未出现' : firstSeenAt + 'ms'}`);
  85. if (firstSeenAt !== null && firstSeenAt > 1500) {
  86. console.log('⚠️ 超过 1.5 秒 —— 用户能感知到卡顿');
  87. }
  88. console.log('页面错误:', errors.length ? errors.slice(0, 5) : '无');
  89. await page.screenshot({ path: 'test-results/repro-new-session.png' });
  90. await b.close();