修复完整会话历史与长气泡交互并更新 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

@@ -26,13 +26,20 @@ function extractFunction(name) {
// 仅模拟浏览器节点操作,消息身份、对账、缓存合并及重连分支均执行 app.js 原函数。
class Element {
constructor() {
constructor(tagName = '') {
this.tagName = tagName;
this.dataset = {};
this.children = [];
this.parentNode = null;
this.scrollTop = 0;
this.id = '';
this.classList = { contains: (name) => this.message?.role === name };
this.classes = new Set();
this.listeners = new Map();
this.classList = {
contains: (name) => this.classes.has(name) || this.message?.role === name,
add: (name) => this.classes.add(name),
remove: (name) => this.classes.delete(name),
};
}
get scrollHeight() { return this.children.length * 50; }
get nextSibling() {
@@ -40,9 +47,11 @@ class Element {
return this.parentNode.children[this.parentNode.children.indexOf(this) + 1] || null;
}
set innerHTML(value) {
this.html = value;
this.children.forEach((child) => { child.parentNode = null; });
this.children = [];
}
get innerHTML() { return this.html || ''; }
insertBefore(node, anchor) {
if (node.fragment) {
[...node.children].forEach((child) => this.insertBefore(child, anchor));
@@ -57,6 +66,8 @@ class Element {
return node;
}
appendChild(node) { return this.insertBefore(node, null); }
addEventListener(type, listener) { this.listeners.set(type, listener); }
click() { if (!this.disabled) this.listeners.get('click')?.(); }
removeAttribute(name) { if (name === 'id') this.id = ''; }
remove() {
if (!this.parentNode) return;
@@ -71,6 +82,7 @@ class Element {
}
querySelectorAll(selector) {
return this.children.filter((child) => {
if (selector.includes('.user') && child.message?.role !== 'user') return false;
if (selector.includes('[data-session-message="true"]') && child.dataset.sessionMessage !== 'true') return false;
if (selector.includes('[data-message-key]') && !child.dataset.messageKey) return false;
return true;
@@ -190,6 +202,241 @@ function message(id, role = 'assistant', content = id) {
return { id, role, content, timestamp: '2026-09-21T00:00:00.000Z' };
}
function createPaginationHarness() {
const rendering = createHarness();
const names = [
'createOutlineHistoryState', 'resetOutlineHistoryState', 'buildOutlineHistoryEntry',
'mergeOutlineHistoryMessages', 'syncOutlineHistoryIndex', 'normalizeOutlineTitleHistory',
'formatUserOutlineDate', 'buildUserOutlineItems', 'buildUserOutlineTimelineItems',
'updateUserOutlinePanel', 'resetHistoryLoadState', 'renderHistoryLoadControl',
'syncHistoryLoadControl', 'requestOlderHistory', 'requestOutlineHistoryPage',
];
const historyStart = source.indexOf("case 'session_history_chunk':");
const historyEnd = source.indexOf("case 'session_message':", historyStart);
const errorStart = source.indexOf("case 'error':", historyStart);
const errorEnd = source.indexOf('const matchesActiveLoadError =', errorStart);
assert(historyStart >= 0 && historyEnd > historyStart && errorStart >= 0 && errorEnd > errorStart);
return new Function('Element', 'rendering', `
let currentSessionId = 'running-session';
const loadedHistorySessionId = currentSessionId;
let currentHistoryState = {};
let currentOutlineHistoryState = createOutlineHistoryState(currentSessionId);
let activeHistoryPageRequest = null;
let activeOutlineHistoryRequest = null;
let activeOutlineTargetRequest = null;
let historyPageRequestSeq = 0;
let outlineHistoryRequestSeq = 0;
const activeSessionLoad = null;
let ws = { readyState: 1 };
let wsAuthenticated = true;
const historyLoadMore = new Element();
const userOutlinePanel = new Element();
const userOutlineBtn = new Element();
const messagesDiv = rendering.messagesDiv;
const userMessageIndex = new Map();
const currentOutlineTitleHistory = [];
const requests = [];
const document = { createElement(tagName) { return new Element(tagName); } };
function send(request) { requests.push(request); }
function shortMessagePreview(value, length) { return String(value).slice(0, length); }
function escapeHtml(value) { return String(value); }
function isBlockingSessionLoad() { return false; }
function scheduleAdvancedSearchJump() {}
function prependHistoryMessages(...args) { rendering.prependHistoryMessages(...args); }
function mergeHistoryChunkIntoCachedSnapshot(...args) { rendering.mergeHistoryChunkIntoCachedSnapshot(...args); }
${names.map(extractFunction).join('\n')}
return {
requests, historyLoadMore, userOutlinePanel, userOutlineBtn,
requestOlderHistory, requestOutlineHistoryPage,
reset() {
resetHistoryLoadState(currentSessionId);
resetOutlineHistoryState(currentSessionId);
updateUserOutlinePanel();
},
ids: rendering.ids,
outlineItems: buildUserOutlineItems,
state() { return { history: currentHistoryState, outline: currentOutlineHistoryState,
pageRequest: activeHistoryPageRequest, outlineRequest: activeOutlineHistoryRequest }; },
connect(authenticated = true, readyState = 1, connected = true) {
wsAuthenticated = authenticated;
ws = connected ? { readyState } : null;
},
select(sessionId) { currentSessionId = sessionId; },
init(snapshot) {
resetHistoryLoadState();
resetOutlineHistoryState();
rendering.buffers.set(snapshot.sessionId, { ...snapshot, messages: [...snapshot.messages] });
rendering.renderMessages(snapshot.messages, { immediate: true, baseIndex: snapshot.historyBaseIndex });
syncHistoryLoadControl(snapshot);
syncOutlineHistoryIndex(snapshot);
updateUserOutlinePanel();
},
receive(msg) {
switch (msg.type) {
${source.slice(historyStart, historyEnd)}
${source.slice(errorStart, errorEnd)}
break;
}
},
};
`)(Element, rendering);
}
const savedMessages = Array.from({ length: 57 }, (_, index) =>
message(`saved-${index}`, index < 45 && index % 2 === 0 ? 'user' : 'assistant'));
function savedSnapshot(available = false, cursor = 45) {
return {
sessionId: 'running-session', messages: savedMessages.slice(cursor),
historyBaseIndex: cursor, historyCursor: cursor, historyTotal: savedMessages.length,
historyAvailable: available,
};
}
function historyChunk(request, cursor) {
return {
type: 'session_history_chunk', sessionId: request.sessionId, requestId: request.requestId,
messages: savedMessages.slice(cursor, request.before), historyBaseIndex: cursor,
historyCursor: cursor, historyTotal: savedMessages.length, historyAvailable: false, remaining: 0,
};
}
function historyButtons(api) {
return api.historyLoadMore.children.filter((node) => node.tagName === 'button');
}
function historyCopy(api) {
return api.historyLoadMore.children.filter((node) => node.tagName === 'span')
.map((node) => node.textContent).join(' ');
}
{
const api = createPaginationHarness();
api.init(savedSnapshot());
api.requestOlderHistory();
assert.equal(api.requests.length, 1, '原始来源不可用时仍须请求已保存的45条正文');
assert.match(historyCopy(api), /45 条已保存的更早消息/);
assert.equal(historyButtons(api)[0].disabled, true, '请求期间保留 loading 和按钮禁用');
assert.equal(historyButtons(api)[0].textContent, '正在加载…');
api.requestOlderHistory();
assert.equal(api.requests.length, 1, '重复点击不能重复请求');
const first = api.requests[0];
api.receive({ ...historyChunk(first, 33), requestId: '过期请求' });
api.receive({ ...historyChunk(first, 33), sessionId: '其他会话' });
assert.equal(api.ids().length, 12, '错误会话和过期请求不能插入正文');
while (api.state().pageRequest) {
const request = api.state().pageRequest;
const response = historyChunk(request, Math.max(0, request.before - 12));
api.receive(response);
api.receive(response);
if (api.state().history.cursor > 0) historyButtons(api)[0].click();
}
assert.deepEqual(api.requests.map((request) => request.before), [45, 33, 21, 9]);
assert.deepEqual(api.ids(), savedMessages.map((item) => item.id), '57条已保存正文保持顺序且不重复');
assert.equal(api.state().history.available, false, '来源状态不能伪装成完整恢复');
assert.equal(api.historyLoadMore.hidden, false, '读完已保存消息仍须保留更早记录待核实提示');
assert.equal(historyButtons(api).length, 0, 'cursor=0时不能保留加载按钮');
assert.doesNotMatch(historyCopy(api), /无法恢复/);
}
{
const api = createPaginationHarness();
api.init(savedSnapshot());
assert.equal(api.outlineItems().length, 0, '首屏没有用户提问,用于验证目录入口');
assert.equal(api.userOutlineBtn.disabled, false, '有已保存旧消息时目录按钮不能因来源状态而禁用');
api.requestOutlineHistoryPage();
assert.equal(api.requests.length, 1, '原始来源不可用时仍须索引已保存的45条');
assert.match(api.userOutlinePanel.innerHTML, /正在加载/);
api.requestOutlineHistoryPage();
assert.equal(api.requests.length, 1, '目录索引请求必须去重');
const first = api.requests[0];
api.receive({ ...historyChunk(first, 33), requestId: '过期目录请求' });
api.receive({ ...historyChunk(first, 33), sessionId: '其他会话' });
assert.equal(api.outlineItems().length, 0, '错误会话和过期响应不能污染目录');
while (api.state().outlineRequest) {
const request = api.state().outlineRequest;
const response = historyChunk(request, Math.max(0, request.before - 12));
api.receive(response);
api.receive(response);
}
assert.deepEqual(api.requests.map((request) => request.before), [45, 33, 21, 9]);
assert(api.requests.every((request) => request.purpose === 'outline'));
assert.deepEqual(api.outlineItems().map((item) => item.id),
savedMessages.filter((item) => item.role === 'user').map((item) => item.id),
'目录自动完成全部用户提问索引,顺序正确且无重复');
assert.equal(api.state().outline.complete, true);
assert.equal(api.state().outline.available, false);
assert.match(api.userOutlinePanel.innerHTML, /更早的记录尚待核实/);
assert.doesNotMatch(api.userOutlinePanel.innerHTML, /无法恢复/);
}
for (const outline of [false, true]) {
const api = createPaginationHarness();
api.init(savedSnapshot());
const requestPage = () => outline ? api.requestOutlineHistoryPage() : api.requestOlderHistory();
const state = () => outline ? api.state().outline : api.state().history;
const active = () => outline ? api.state().outlineRequest : api.state().pageRequest;
api.connect(false);
requestPage();
api.connect(true, 0);
requestPage();
api.connect(true, 1, false);
requestPage();
api.connect();
api.select('其他会话');
requestPage();
assert.equal(api.requests.length, 0, '保留认证、连接状态和会话隔离门禁');
api.select('running-session');
requestPage();
assert.equal(api.requests.length, 1);
const failedRequest = active();
api.receive({ type: 'error', sessionId: '其他会话', requestId: failedRequest.requestId, message: '不得生效' });
assert.equal(state().loading, true, '其他会话错误不能取消本次加载');
api.receive({ type: 'error', sessionId: failedRequest.sessionId, requestId: failedRequest.requestId, message: '读取失败,请重试' });
assert.equal(active(), null);
assert.equal(state().loading, false);
assert.equal(state().cursor, 45, '失败后保留游标供重试');
if (outline) {
assert.match(api.userOutlinePanel.innerHTML, /data-outline-retry/);
assert.equal(api.userOutlineBtn.disabled, false);
requestPage();
} else {
assert.equal(historyButtons(api)[0].disabled, false);
assert.equal(historyButtons(api)[0].textContent, '重试');
historyButtons(api)[0].click();
}
assert.equal(api.requests.length, 2, '失败后能够重试已有记录');
assert.notEqual(active().requestId, failedRequest.requestId);
assert.equal(state().error, '');
assert.equal(state().loading, true);
}
for (const available of [false, true]) {
const api = createPaginationHarness();
api.init(savedSnapshot(available, 0));
api.requestOlderHistory();
api.requestOutlineHistoryPage();
assert.equal(api.requests.length, 0, 'cursor=0时两类请求都必须停止');
assert.equal(historyButtons(api).length, 0);
assert.equal(api.historyLoadMore.hidden, available, '完整历史读完隐藏控件;来源待核实则保留提示');
if (available) {
assert.doesNotMatch(api.userOutlinePanel.innerHTML, /待核实/);
} else {
assert.match(historyCopy(api), /更早的记录尚待核实/);
assert.match(api.userOutlinePanel.innerHTML, /更早的记录尚待核实/);
assert.doesNotMatch(historyCopy(api), /不完整|未保存|无法恢复/);
assert.doesNotMatch(api.userOutlinePanel.innerHTML, /不完整|未保存|无法恢复/);
}
}
{
const api = createPaginationHarness();
api.reset();
assert.equal(api.historyLoadMore.hidden, true, '收到历史元数据前不能误报更早记录待核实');
assert.doesNotMatch(api.userOutlinePanel.innerHTML, /待核实/);
assert.equal(api.userOutlineBtn.disabled, true, '没有已保存消息或待加载记录时保持空目录禁用');
api.init(savedSnapshot(true));
assert.equal(historyButtons(api)[0].disabled, false);
assert.doesNotMatch(historyCopy(api), /待核实/);
assert.doesNotMatch(api.userOutlinePanel.innerHTML, /待核实/);
}
{
const api = createHarness();
assert.equal(api.getStableMessageKey({ role: 'assistant', turnId: 'turn-1', content: '初稿' }),
@@ -343,4 +590,4 @@ for (const cached of [false, true]) {
assert.equal(api.count(), 4);
}
console.log('前端历史单测通过:稳定 ID、分页、重连、流式身份绑定与完成去重');
console.log('前端历史单测通过:已保存历史分页、目录索引、来源提示、认证隔离与重试、稳定 ID、重连和流式去重');