修复完整会话历史与长气泡交互并更新 CentOS 7 发布包
提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
@@ -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、重连和流式去重');
|
||||
|
||||
Reference in New Issue
Block a user