fix: 消除会话列表解析阻塞并按需加载工具详情

This commit is contained in:
shiyue
2026-10-19 10:58:22 +08:00
parent fb2c3360c9
commit 99cc807f4b
35 changed files with 1468 additions and 214 deletions

View File

@@ -25,7 +25,7 @@ class FirefoxDriver {
}
async start() {
const value = await this.request('POST', '/session', { capabilities: { alwaysMatch: {
browserName: 'firefox', 'moz:firefoxOptions': { binary: FIREFOX, args: ['-headless'] },
browserName: 'firefox', pageLoadStrategy: 'eager', 'moz:firefoxOptions': { binary: FIREFOX, args: ['-headless'] },
} } });
this.id = value.sessionId;
await this.request('POST', `/session/${this.id}/window/rect`, { width: 1280, height: 1000 });
@@ -38,7 +38,7 @@ class FirefoxDriver {
async function startRelay(port, targetPort, evidence) {
const sockets = new Set();
const state = { dropWs: false, blockUpgrades: false, holdId: '', held: [] };
const state = { dropWs: false, blockUpgrades: false, holdId: '', held: [], bytesPerSecond: 0 };
const relay = http.createServer((request, response) => {
if (CDN_FIXTURE_DIR && request.url.startsWith('/fixture-cdn/')) {
const name = request.url.slice('/fixture-cdn/'.length);
@@ -63,6 +63,23 @@ async function startRelay(port, targetPort, evidence) {
});
return;
}
if (!hold && isQuery && state.bytesPerSecond) {
const chunks = [];
result.on('data', chunk => chunks.push(chunk));
result.on('end', () => {
const body = Buffer.concat(chunks);
const bytesPerTick = Math.max(1, Math.floor(state.bytesPerSecond / 20));
let offset = 0;
response.writeHead(result.statusCode, result.headers);
const timer = setInterval(() => {
const end = Math.min(body.length, offset + bytesPerTick);
response.write(body.subarray(offset, end)); offset = end;
if (offset === body.length) { clearInterval(timer); response.end(); }
}, 50);
response.on('close', () => clearInterval(timer));
});
return;
}
if (!hold) { response.writeHead(result.statusCode, result.headers); result.pipe(response); return; }
const chunks = [];
result.on('data', chunk => chunks.push(chunk));
@@ -161,6 +178,36 @@ async function runBrowser(options) {
await click(A);
await loaded(A, 12, true);
evidence.stages.fakeOpen = { durationMs: Date.now() - started, count: 12, runtimeVisible: true };
await driver.execute(`window.__switchErrors=[];window.addEventListener('error',e=>__switchErrors.push(e.message));
window.addEventListener('unhandledrejection',e=>__switchErrors.push(String(e.reason)));`);
relay.state.bytesPerSecond = 32768;
started = Date.now();
await click('http-large-tools');
await loaded('http-large-tools', 12);
const compact = await driver.execute(`return {
tools:document.querySelectorAll('#messages .tool-call').length,
contents:document.querySelectorAll('#messages .tool-call-content').length,
bytes:performance.getEntriesByType('resource').filter(x=>x.name.includes('/api/sessions/http-large-tools?')).at(-1)?.decodedBodySize,
};`);
assert.equal(compact.tools, 100);
assert.equal(compact.contents, 0, '折叠工具不能提前构建详情DOM');
assert(Date.now() - started < 8000, '32KB/s 下工具大结果不应卡住首屏');
evidence.stages.largeTools = { ...compact, durationMs: Date.now() - started, bytesPerSecond: 32768 };
relay.state.bytesPerSecond = 0;
await driver.execute(`const tool=document.querySelector('#messages details.tool-call');
const group=tool.closest('.tool-group');if(group)group.open=true;tool.querySelector('summary').click();`);
await waitFor(() => driver.execute(`return document.querySelector('#messages details.tool-call .tool-call-content')?.textContent.includes('结果终点');`), '完整工具详情未显示');
evidence.stages.toolDetail = { complete: true };
relay.state.holdId = 'http-large-tools';
await driver.execute(`document.querySelectorAll('#messages details.tool-call')[1].querySelector('summary').click();`);
await waitFor(() => relay.state.held.length, '工具详情延迟响应未到达');
await click(B);
await loaded(B, 12);
relay.release();
await sleep(100);
await loaded(B, 12);
evidence.stages.detailCancellation = { previousDetailCannotOverwriteNewView: true };
assert.deepEqual(await driver.execute('return window.__switchErrors;'), [], '切换及展开不能产生浏览器异常');
await click(B);
await loaded(B, 12);
for (const count of [36, 58]) {
@@ -201,6 +248,7 @@ async function runBrowser(options) {
await click(A);
await waitFor(() => relay.state.held.length, '完成间隙的 HTTP 快照未到达');
assert.equal(JSON.parse(relay.state.held[0].body).isRunning, true);
const runtimeDetailRef = JSON.parse(relay.state.held[0].body).runtime.toolCalls.find(tool => tool.kind === 'command_execution').detailRef;
fs.writeFileSync(fixture.completeFlag, 'complete');
await waitFor(async () => {
const result = (await query(A)).payload;
@@ -215,6 +263,9 @@ async function runBrowser(options) {
&& value.text.includes('Codex App mock handled:') ? value : null;
}, '读取与订阅之间完成的消息未补齐', 12000);
assert(evidence.ws.some(msg => msg.type === 'session_subscribed' && msg.historyChanged));
const archivedTool = await query(A, '?toolDetail=' + encodeURIComponent(JSON.stringify(runtimeDetailRef)));
assert.equal(archivedTool.response.status, 200, '运行工具归档后,原详情引用仍须可读取');
assert(archivedTool.payload.tool.result.includes('运行结果终点'));
evidence.stages.completionGap = { count: completed.ids.length, streaming: false, finalVisible: true };
await click('http-corrupt-chunk');
await waitFor(async () => {
@@ -224,7 +275,7 @@ async function runBrowser(options) {
evidence.stages.readFailure = { previousViewPreserved: true, loading: false };
assert(!evidence.ws.some(msg => msg.direction === 'client' && ['load_session', 'load_history_page', 'resume_session'].includes(msg.type)));
fs.writeFileSync(path.join(root, 'browser-evidence.json'), JSON.stringify(evidence, null, 2));
console.log('Firefox 通过:假 OPEN/断 WS 首屏与运行输出、HTTP 分页顺序、迟到响应隔离、切换间隙完成对账;浏览器未通过 WS 查询历史');
console.log('Firefox 通过:32KB/s 大工具会话首屏、折叠懒建、完整详情、假 OPEN/断 WS 运行恢复、分页、迟到隔离与完成交错');
} catch (error) {
evidence.failure = error.message;
evidence.state = await snapshot().catch(() => null);