| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- /**
- * 真实场景验证:在真实会话里上传文件 → 右侧栏直接显示
- *
- * 这是用户要求的核心体验验证:
- * 「用户上传文件后,右侧栏应该能看到,且**不依赖加载聊天历史**」
- *
- * 用法:`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, []);
- });
|