115 lines
6.3 KiB
JavaScript
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 }));
|