#!/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 = '
10:20:30
'; 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 ['
10:20:30']) { 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 }));