修复完整会话历史与长气泡交互并更新 CentOS 7 发布包

提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
shiyue
2026-10-10 23:40:16 +08:00
parent 747771a048
commit 69b6db0f5b
71 changed files with 9536 additions and 503 deletions

View File

@@ -48,9 +48,13 @@ async function waitFor(predicate, description, timeout = 12000) {
}
async function stopChild(child) {
if (!child || child.exitCode !== null || child.signalCode) return;
const exited = new Promise((resolve) => child.once('exit', resolve));
child.kill('SIGTERM');
await Promise.race([new Promise((resolve) => child.once('exit', resolve)), sleep(1000)]);
if (child.exitCode === null && !child.signalCode) child.kill('SIGKILL');
await Promise.race([exited, sleep(1000)]);
if (child.exitCode === null && !child.signalCode) {
child.kill('SIGKILL');
await exited;
}
}
class FirefoxDriver {
@@ -168,7 +172,11 @@ Module._extensions['.js'] = (module, filename) => {
}, 80);
`).replace("path.join(__dirname, 'mock-codex.js')", JSON.stringify(path.join(REPO, 'scripts/mock-codex.js')));
fs.writeFileSync(mock, browserMock, { mode: 0o755 });
return { home, sessions, rollout, preload, mock, completeFlag, snapshotCount: snapshot.length, nativeCount: store.parseCodexRolloutLines(rows.map((row) => JSON.stringify(row))).messages.length };
return {
home, sessions, rollout, preload, mock, completeFlag, snapshotCount: snapshot.length,
historyIds: [...prefix, ...snapshot.slice(-4)].map((message) => message.id),
nativeCount: store.parseCodexRolloutLines(rows.map((row) => JSON.stringify(row))).messages.length,
};
}
async function startRelay(port, targetPort, evidence) {
@@ -192,8 +200,13 @@ async function startRelay(port, targetPort, evidence) {
try {
const msg = JSON.parse(String(data));
if (['session_info', 'resume_session_result', 'session_history_chunk'].includes(msg.type)) evidence.protocol.push({
type: msg.type, historySource: msg.historySource, historyTotal: msg.historyTotal, isRunning: msg.isRunning,
type: msg.type, historySource: msg.historySource, historyTotal: msg.historyTotal,
historyAvailable: msg.historyAvailable, isRunning: msg.isRunning,
ids: msg.messages?.map((message) => message.id),
messages: msg.messages?.map((message, index) => ({
id: message.id,
index: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) + index : null,
})),
});
} catch {}
if (client.readyState === WebSocket.OPEN) client.send(data, { binary: false });
@@ -265,16 +278,23 @@ async function main() {
const evidence = { root, fixture, protocol: [], stages: {}, kind: '真实 server.js / Firefox / 隔离持久化与 native rollout / mock Codex App' };
const [serverPort, relayPort, driverPort] = await Promise.all([freePort(), freePort(), freePort()]);
let server, gecko, relay, driver;
let rolloutHidden = false;
try {
server = spawn(process.execPath, ['-r', fixture.preload, path.join(REPO, 'server.js')], {
cwd: REPO, env: { ...process.env, PORT: String(serverPort), CC_WEB_PASSWORD: PASSWORD,
CC_WEB_INTERNAL_MCP_TOKEN: MCP_TOKEN,
CC_WEB_CONFIG_DIR: path.join(root, 'config'), CC_WEB_SESSIONS_DIR: fixture.sessions, CC_WEB_LOGS_DIR: path.join(root, 'logs'),
CODEX_HOME: path.join(fixture.home, '.codex'),
CODEX_PATH: fixture.mock, CLAUDE_PATH: path.join(REPO, 'scripts/mock-claude.js'), CC_WEB_TASK_BOARD_ENABLED: '0',
}, stdio: ['ignore', 'pipe', 'pipe'],
});
const serverLog = fs.createWriteStream(path.join(root, 'server.log')); server.stdout.pipe(serverLog); server.stderr.pipe(serverLog);
const launchServer = (logName = 'server.log') => {
const child = spawn(process.execPath, ['-r', fixture.preload, path.join(REPO, 'server.js')], {
cwd: REPO, env: { ...process.env, PORT: String(serverPort), CC_WEB_PASSWORD: PASSWORD,
CC_WEB_INTERNAL_MCP_TOKEN: MCP_TOKEN,
CC_WEB_CONFIG_DIR: path.join(root, 'config'), CC_WEB_SESSIONS_DIR: fixture.sessions, CC_WEB_LOGS_DIR: path.join(root, 'logs'),
CODEX_HOME: path.join(fixture.home, '.codex'),
CODEX_PATH: fixture.mock, CLAUDE_PATH: path.join(REPO, 'scripts/mock-claude.js'), CC_WEB_TASK_BOARD_ENABLED: '0',
}, stdio: ['ignore', 'pipe', 'pipe'],
});
const log = fs.createWriteStream(path.join(root, logName));
child.stdout.pipe(log); child.stderr.pipe(log);
child.once('exit', () => log.end());
return child;
};
server = launchServer();
await waitFor(async () => (await fetch(`http://127.0.0.1:${serverPort}/`)).ok, '隔离服务启动失败');
const mcpResponse = await fetch(`http://127.0.0.1:${serverPort}/api/internal/mcp/stream?sourceSessionId=${SESSION_ID}`, {
method: 'POST',
@@ -301,6 +321,106 @@ async function main() {
}, '登录或会话列表未就绪');
await driver.execute(`document.querySelector('.session-item').click();`);
evidence.stages.loaded = await capture(driver, 'loaded', root, false);
const archiveStart = evidence.protocol.length;
fs.renameSync(fixture.rollout, `${fixture.rollout}.isolated-hidden`);
rolloutHidden = true;
await stopChild(server);
server = launchServer('server-restarted.log');
await waitFor(async () => {
assert.equal(server.exitCode, null, '隔离服务重启后已退出');
return (await fetch(`http://127.0.0.1:${serverPort}/`)).ok;
}, '归档验证服务重启失败');
await driver.refresh();
await waitFor(async () => {
const ready = await driver.execute('return !document.querySelector("#app").hidden && !!document.querySelector(".session-item")');
if (ready) return true;
await driver.execute('document.querySelector("#login-password").value=arguments[0];document.querySelector("#login-form").requestSubmit();', [PASSWORD]);
return false;
}, '归档验证重启后登录或会话列表未就绪');
await driver.execute('document.querySelector(".session-item").click();');
evidence.stages.archivedAfterRestart = await capture(driver, 'archived-after-restart', root, false);
const archivedBeforePaging = await driver.execute(`return {
ids: [...document.querySelectorAll('#messages > .msg')].map((node) => node.dataset.messageKey).filter(Boolean),
indices: [...document.querySelectorAll('#messages > .msg')]
.map((node) => Number(node.dataset.messageIndex))
.filter((value) => Number.isFinite(value)),
notice: document.querySelector('#history-load-more')?.textContent || '',
};`);
await driver.execute(`const box=document.querySelector('#messages'); box.scrollTop=0;`);
await waitFor(async () => {
const state = await driver.execute(`return {
earliest: [...document.querySelectorAll('#messages .msg')].some((node) => node.textContent.includes('早期问题 0')),
canLoad: !!document.querySelector('.history-load-more-button:not([disabled])'),
};`);
if (state.earliest) return true;
if (state.canLoad) {
await driver.execute(`document.querySelector('.history-load-more-button:not([disabled])').click();`);
}
return false;
}, '原始记录移开并重启后,最早消息无法从自身归档读回', 30000);
const archived = await driver.execute(`return {
ids: [...document.querySelectorAll('#messages > .msg')].map((node) => node.dataset.messageKey).filter(Boolean),
indices: [...document.querySelectorAll('#messages > .msg')]
.map((node) => Number(node.dataset.messageIndex))
.filter((value) => Number.isFinite(value)),
notice: document.querySelector('#history-load-more')?.textContent || '',
};`);
const archiveProtocol = evidence.protocol.slice(archiveStart);
const archiveInfo = archiveProtocol.filter((message) => message.type === 'session_info');
const archiveChunks = archiveProtocol.filter((message) => message.type === 'session_history_chunk');
const archiveMessages = archiveProtocol.flatMap((message) => message.messages || []);
const archiveIds = new Set(archiveMessages.map((message) => message.id).filter(Boolean));
const archiveIndices = new Set(archiveMessages
.map((message) => message.index)
.filter((index) => Number.isFinite(index)));
assert(archiveInfo.length > 0 && archiveInfo.every((message) => message.historySource === 'archive'),
'归档刷新后的 session_info 必须全部来自 archive');
assert(archiveInfo.every((message) => message.historyTotal === 148 && message.historyAvailable === true),
'归档响应必须提供完整总数,且 historyAvailable=true');
assert(archiveChunks.length > 0 && archiveChunks.every((message) => message.historySource === 'archive'),
'归档分页必须全部来自 archive');
assert(archiveChunks.every((message) => message.historyTotal === 148 && message.historyAvailable === true),
'归档分页总数或可用状态不一致');
assert.deepEqual(
[...archiveIndices].sort((left, right) => left - right),
Array.from({ length: 148 }, (_, index) => index),
'归档协议消息下标必须连续覆盖 0 到 147',
);
assert.equal(archiveIds.size, 148, `归档协议应覆盖 148 个稳定 ID,实际 ${archiveIds.size}`);
assert.equal(archived.ids.length, 148, `归档消息总数应为 148,实际 ${archived.ids.length}`);
assert.deepEqual(archived.ids, fixture.historyIds, '归档分页必须保留夹具全部消息稳定 ID 及顺序');
assert.deepEqual(
archived.indices.slice().sort((left, right) => left - right),
Array.from({ length: archived.indices.length }, (_, index) => index),
'归档可见消息下标必须从 0 连续覆盖到 147',
);
assert.deepEqual(
archivedBeforePaging.indices.slice().sort((left, right) => left - right),
Array.from({ length: archivedBeforePaging.indices.length }, (_, index) => index + 136),
'归档首屏必须从消息下标 136 开始',
);
assert.equal(new Set(archived.ids).size, 148, '归档必须保留 148 个唯一稳定 ID');
assert(!archived.notice.includes('无法恢复') && !archived.notice.includes('待核实'),
'完整归档读完后不应保留来源不可用或待核实提示');
await driver.execute(`document.querySelector('#messages').scrollTop=0;`);
await waitFor(() => driver.execute(`const node=[...document.querySelectorAll('#messages > .msg')]
.find((message) => message.dataset.messageKey === arguments[0]);
if (!node) return false;
const box=node.getBoundingClientRect(), style=getComputedStyle(node);
return node.textContent.includes('早期问题 0') && box.top >= 0
&& box.bottom <= innerHeight && box.height > 0 && Number(style.opacity) > 0.9;`,
[fixture.historyIds[0]]), '归档最早问题没有实际显示在视口');
await driver.screenshot(path.join(root, 'archived-paged.png'));
evidence.stages.archivedPaged = archived;
evidence.archived = {
beforePaging: archivedBeforePaging,
afterPaging: archived,
archiveInfoCount: archiveInfo.length,
archiveChunkCount: archiveChunks.length,
protocolStableIdCount: archiveIds.size,
protocolIndexCount: archiveIndices.size,
pagedScreenshot: path.join(root, 'archived-paged.png'),
};
await driver.execute(`const input=document.querySelector('#msg-input');input.value='继续核对页面职责与流程边界';input.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('#send-btn').click();`);
evidence.stages.running = await capture(driver, 'running', root);
await driver.refresh();
@@ -323,10 +443,14 @@ async function main() {
if (driver?.sessionId) await driver.screenshot(path.join(root, 'failure.png')).catch(() => {});
throw new Error(`${error.message}\n证据:${root}`, { cause: error });
} finally {
fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2));
if (driver) await driver.close().catch(() => {});
if (relay) await relay.close();
await stopChild(gecko); await stopChild(server);
await stopChild(gecko);
await stopChild(server);
if (rolloutHidden && fs.existsSync(`${fixture.rollout}.isolated-hidden`)) {
fs.renameSync(`${fixture.rollout}.isolated-hidden`, fixture.rollout);
}
fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2));
}
}