Files
cc-web/scripts/assistant-bubble-controls-unit.js
shiyue 69b6db0f5b 修复完整会话历史与长气泡交互并更新 CentOS 7 发布包
提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
2026-10-10 23:40:16 +08:00

115 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const source = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
function extractFunction(name) {
const start = source.indexOf(`function ${name}(`);
assert(start >= 0, `缺少前端函数 ${name}`);
let depth = 0, parentheses = 0, signatureEnd = start;
for (let i = source.indexOf('(', start); i < source.length; i += 1) {
if (source[i] === '(') parentheses += 1;
if (source[i] === ')' && --parentheses === 0) { signatureEnd = i; break; }
}
for (let i = source.indexOf('{', signatureEnd); i < source.length; i += 1) {
if (source[i] === '{') depth += 1;
if (source[i] === '}' && --depth === 0) return source.slice(start, i + 1);
}
throw new Error(`无法提取 ${name}`);
}
const names = ['getAssistantReplySections', 'getAssistantReplySource', 'getAssistantLastReplyText', 'shouldCollapseAssistantReplies'];
const api = new Function(`
const ASSISTANT_REPLY_COLLAPSE_CHARS = 6000;
const ASSISTANT_REPLY_COLLAPSE_BLOCKS = 24;
${names.map(extractFunction).join('\n')}
return { ${names.join(',')} };
`)();
const DIVIDER = '<div class="agent-message-divider" data-divider-time="10:20:30"><span>10:20:30</span></div>';
const FIRST = '# 第一条回复\n\n' + '前文'.repeat(4000);
const LAST = '# 最后一条回复\n\n第一自然段。\n\n第二自然段。\n\n```js\nconst value = 1;\n```';
let passed = 0;
function test(description, fn) { fn(); passed += 1; console.log('通过:' + description); }
test('末段按原生回复分界,保留 Markdown、多自然段与代码', () => {
const content = FIRST + '\n\n' + DIVIDER + '\n\n' + LAST;
const message = { __assistantContent: content };
assert.deepEqual(api.getAssistantReplySections(content), [FIRST, LAST]);
assert.equal(api.getAssistantLastReplyText(message), LAST);
assert(!api.getAssistantLastReplyText(message).includes('10:20:30'));
assert(!api.getAssistantLastReplyText(message).includes('前文'));
});
test('无原生分界的单条回复复制全文,长正文也不会按最后 p 截断', () => {
assert.equal(api.getAssistantLastReplyText({ __assistantContent: LAST }), LAST);
assert.equal(api.shouldCollapseAssistantReplies([FIRST], 30), true);
});
test('复制保留 Markdown 首行缩进和行末空格,不改变代码及换行语义', () => {
const indented = ' const value = 1;\n return value; ';
assert.equal(api.getAssistantLastReplyText({ __assistantContent: indented }), indented);
assert.equal(api.getAssistantLastReplyText({ __assistantContent: FIRST + '\n\n' + DIVIDER + '\n\n' + indented + '\n\n' }), indented);
});
test('代码围栏内展示分界 HTML 不应误判成原生回复边界', () => {
const code = '```html\n' + DIVIDER + '\n```\n\n说明仍属于同一回复。';
assert.deepEqual(api.getAssistantReplySections(code), [code]);
assert.deepEqual(api.getAssistantReplySections(code + '\n' + DIVIDER + '\n' + LAST), [code, LAST]);
const tilde = '~~~~html\n' + DIVIDER + '\n~~~~\n段落';
assert.deepEqual(api.getAssistantReplySections(tilde), [tilde]);
});
test('分界 HTML 尚未传完时复制上一条正文,不夹带时间或半截标签', () => {
for (const partial of ['<div class="agent-message-divider', '<div class="agent-message-divider" data-divider-time="10:20',
'<div class="agent-message-divider" data-divider-time="10:20:30"><span>10:20:30</span>']) {
assert.equal(api.getAssistantLastReplyText({ __assistantContent: LAST + '\n\n' + partial }), LAST);
}
});
test('历史、流式及文本块采用完整原始内容,复制不掺工具或按钮', () => {
assert.equal(api.getAssistantLastReplyText({ __sessionMessage: { content: FIRST + '\n' + DIVIDER + '\n' + LAST } }), LAST);
assert.equal(api.getAssistantLastReplyText({ __assistantContent: [
{ type: 'text', text: FIRST + '\n' + DIVIDER + '\n' + LAST },
{ type: 'tool_result', text: '不该复制的工具结果' },
{ type: 'image', alt: '不该复制的图片元数据' },
] }), LAST);
const stream = { __assistantContent: FIRST + '\n' + DIVIDER + '\n' + LAST };
stream.__assistantRepliesExpanded = true;
assert.equal(api.getAssistantLastReplyText(stream), LAST);
stream.__assistantContent += '\n\n最新流式追加';
assert.equal(api.getAssistantLastReplyText(stream), LAST + '\n\n最新流式追加');
});
test('短消息无折叠入口;达到长度或块数后仅折较早原生回复', () => {
assert.equal(api.shouldCollapseAssistantReplies(['短前文', '短末段'], 2), false);
assert.equal(api.shouldCollapseAssistantReplies([FIRST, LAST], 2), true);
assert.equal(api.shouldCollapseAssistantReplies(['短前文', '短末段'], 24), true);
assert.equal(api.shouldCollapseAssistantReplies([], 50), false);
});
test('定位先展开裁剪的单条长回复,再读取末段坐标;多回复收起状态保持', () => {
const locate = new Function('initiallyClipped', `
let clipped = initiallyClipped;
const events = [];
const message = { classList: { contains: (name) => name === 'assistant-single-collapsed' && clipped } };
const bubble = { closest: () => message, querySelector: () => ({ click() { clipped = false; events.push('展开'); } }) };
const scope = { classList: { remove() {} }, getBoundingClientRect: () => ({ top: 100 }) };
const getAssistantLastSectionTarget = () => { events.push(clipped ? '读取隐藏末段' : '读取可见末段'); return { node: {}, offset: 0, scope }; };
const getRangeRectFromTextPosition = () => ({ top: 100 });
const messagesDiv = { scrollTop: 0, getBoundingClientRect: () => ({ top: 0 }), scrollTo: (value) => events.push(value.top) };
const ASSISTANT_LAST_SECTION_SCROLL_OFFSET = 12;
const ASSISTANT_LAST_SECTION_FOCUS_CLASS = 'focus';
const requestAnimationFrame = () => {};
const updateScrollbar = () => {};
${extractFunction('scrollAssistantBubbleToLastSection')}
return { result: scrollAssistantBubbleToLastSection(bubble), clipped, events };
`);
assert.deepEqual(locate(true), { result: true, clipped: false, events: ['展开', '读取可见末段', 88] });
assert.deepEqual(locate(false), { result: true, clipped: false, events: ['读取可见末段', 88] });
});
console.log(JSON.stringify({ passed, source: 'public/app.js 原函数', collapseChars: 6000, collapseBlocks: 24 }));