/** * 真实场景验证:在真实会话里上传文件 → 右侧栏直接显示 * * 这是用户要求的核心体验验证: * 「用户上传文件后,右侧栏应该能看到,且**不依赖加载聊天历史**」 * * 用法:`node scripts/verify-real-upload.mjs` * 前置:`npm start` 在 4200 运行。 */ import { chromium } from 'playwright'; import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { execSync } from 'node:child_process'; import { loadDevLogin } from './dev-login.mjs'; const OUT = resolve('test-results/real-upload-20261110'); mkdirSync(OUT, { recursive: true }); const login = loadDevLogin(); if (!login.enabled) throw new Error(`开发态登录不可用:${login.reason}`); const TOKEN = login.payload.token; const FN = 'THXAOGKiPK'; const results = []; function check(name, ok, detail = '') { results.push({ name, ok }); console.log(`${ok ? '✅' : '❌'} ${name}${detail ? ` (${detail})` : ''}`); } async function dbQuery(threadRoot) { const where = encodeURIComponent(JSON.stringify({ threadRoot })); const r = await fetch( `https://server.fmode.cn/parse/classes/AgentDeliverable?where=${where}&limit=50`, { headers: { 'X-Parse-Application-Id': 'ncloudmaster', 'X-Parse-Master-Key': 'SnkK12*&sunq2#@20!', }, }, ); return (await r.json()).results ?? []; } const FILE_PATH = resolve('/tmp', `real-upload-${Date.now()}.md`); writeFileSync(FILE_PATH, `# 真实上传验证\n\n时间:${new Date().toISOString()}\n`); async function main() { const b = await chromium.launch({ headless: true, channel: 'chromium' }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const page = await ctx.newPage(); await page.addInitScript((d) => { localStorage.setItem('fmode_session_token', d.token); localStorage.setItem('fmode_me', JSON.stringify(d.me)); }, login.payload); /* 记录 listBySession 的调用与返回值,用来判定「右侧栏真的从库里拿」 */ const listCalls = []; page.on('response', async (res) => { if (!res.url().includes('/api/functions')) return; try { const req = res.request(); const body = JSON.parse(req.postData() ?? '{}'); if (body.id !== FN) return; if (body.params?.action !== 'listBySession') return; const json = await res.json(); listCalls.push({ threadRoot: body.params.threadRoot, files: json.files ?? [] }); } catch { /* 忽略 */ } }); await page.goto('http://localhost:4200/desktop/chat', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(6000); /* 进入第一个真实会话 */ const first = page.locator('.session-item, .rail-session, [data-session-id]').first(); if ((await first.count()) === 0) { const byText = page.getByText(/写一份团队周报/).first(); await byText.click({ timeout: 8000 }).catch(() => undefined); } else { await first.click({ timeout: 8000 }).catch(() => undefined); } await page.waitForTimeout(5000); const url = page.url(); const threadRoot = decodeURIComponent(url.split('/').pop() ?? ''); console.log(`\n当前会话: ${threadRoot}`); await page.screenshot({ path: `${OUT}/01-session-opened.png` }); check('① 进入了具体会话', !!threadRoot && threadRoot.includes('fmode.cn'), threadRoot); /* 走文件选择器上传 */ const input = page.locator('input[type="file"]').first(); if ((await input.count()) === 0) { check('② 找到上传入口', false, '页面上没有 file input'); return finish(b, []); } check('② 找到上传入口', true); await input.setInputFiles(FILE_PATH); await page.waitForTimeout(1500); await page.screenshot({ path: `${OUT}/02-file-staged.png` }); /* 点发送 */ const sendBtn = page .locator('button:has(i.fa-paper-plane), button[aria-label*="发送"], .composer button') .last(); await sendBtn.click({ timeout: 8000 }).catch(() => undefined); await page.waitForTimeout(12000); await page.screenshot({ path: `${OUT}/03-after-send.png` }); /* 直接查库:文件是否进了台账,归属是否正确 */ const rows = await dbQuery(threadRoot); check( '③ 文件已入库且归属当前会话', rows.length > 0 && rows.every((r) => r.threadRoot === threadRoot), `${rows.length} 条`, ); if (rows.length > 0) { const r = rows[0]; check( '④ 记录信息完整(上传者/文件名/链接)', r.uploadedBy === 'user' && !!r.fileName && /^https:\/\/s3\.fmode\.cn\//.test(r.artifacts?.[0]?.url ?? ''), `name=${r.fileName}`, ); /* 链接可访问 */ const head = await fetch(r.artifacts[0].url).then((x) => x.status); check('⑤ 云盘链接可访问', head === 200, `HTTP ${head}`); } /* 刷新页面,确认「不依赖聊天历史」也能显示 */ await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForTimeout(8000); await page.screenshot({ path: `${OUT}/04-after-reload.png`, fullPage: false }); const lastCall = listCalls.filter((c) => c.threadRoot === threadRoot).pop(); check( '⑥ 刷新后右侧栏从数据库拿到了这个文件', !!lastCall && lastCall.files.length > 0, lastCall ? `${lastCall.files.length} 个文件` : '未捕获', ); /* 页面上真的显示出来了吗 */ const panelText = await page .locator('.rail, .session-rail, aside') .first() .innerText() .catch(() => ''); const shown = /real-upload|真实上传验证/.test(panelText); check('⑦ 右侧栏界面上能看到这个文件', shown, shown ? '已显示' : `面板文本: ${panelText.slice(0, 60)}`); return finish(b, rows); } function finish(browser, rows) { for (const r of rows ?? []) { try { execSync( `curl -s -X DELETE "https://server.fmode.cn/parse/classes/AgentDeliverable/${r.objectId}" ` + `-H "X-Parse-Application-Id: ncloudmaster" -H "X-Parse-Master-Key: SnkK12*&sunq2#@20!" -o /dev/null`, { stdio: 'ignore' }, ); } catch { /* 忽略 */ } } const passed = results.filter((x) => x.ok).length; console.log(`\n=== 结果:${passed}/${results.length} 通过 ===`); console.log(`截图:${OUT}`); browser?.close(); process.exit(passed === results.length ? 0 : 1); } main().catch((e) => { console.error('失败:', e instanceof Error ? e.message : e); finish(null, []); });