// @ts-nocheck import { render as renderMarkdown } from '../reports/markdown.js'; import { stripReportMarkers } from '../reports/report-clean.js'; import { getCurrentModelInfo } from '../utils/config.js'; import { getState as getSessionState } from '../core/session-context.js'; import { resolveAppNamesFromToolNames } from './sidebar.js'; const MIN_CHARS = 200; const TABLE_RE = /\|[^\n]+\|[\r\n]+\|[-:\s|]+\|/; const PAGE_WIDTH_DXA = 11906; const PAGE_MARGIN_X_DXA = 540; const CONTENT_WIDTH_DXA = PAGE_WIDTH_DXA - PAGE_MARGIN_X_DXA * 2; function formatExportModelLabel() { try { const info = getCurrentModelInfo(); const source = String(info?.sourceLabel || '').trim() || '模型'; const model = String(info?.model || '').trim(); if (!model) return source; return `${source}-${model}`; } catch (_e) { return ''; } } export function collectToolNamesFromMessages(messages) { const list = Array.isArray(messages) ? messages : []; const names = []; for (let i = list.length - 1; i >= 0; i--) { const m = list[i]; if (m?.role === 'user') break; if (Array.isArray(m?.toolCalls)) { for (const tc of m.toolCalls) { if (tc?.name) names.push(tc.name); } } } return names.reverse(); } function collectToolNamesFromDom(anchorEl) { const names = []; const msg = anchorEl?.closest?.('.msg'); if (!msg?.parentElement) return names; const siblings = [...msg.parentElement.children]; const idx = siblings.indexOf(msg); for (let i = idx; i >= 0; i--) { const el = siblings[i]; if (i < idx && el.classList?.contains('msg') && el.classList.contains('user')) break; for (const n of el.querySelectorAll?.('.tool-name[title]') || []) { const t = n.getAttribute('title'); if (t) names.push(t); } } return names; } function inferAppNamesFromMarkdown(markdown) { const raw = String(markdown || ''); if (/个人.*年度报告|班组.*年度报告|profile_/i.test(raw)) return ['Profile']; if (/运营图|轮乘|交接|排班|metro_/i.test(raw)) return ['Assignment']; return []; } function resolveExportAppNames(markdown, opts = {}) { if (Array.isArray(opts.appNames) && opts.appNames.length) { return opts.appNames.map((s) => String(s).trim()).filter(Boolean); } const fromTools = resolveAppNamesFromToolNames(opts.toolNames || []); if (fromTools.length) return fromTools; const fromDom = resolveAppNamesFromToolNames(collectToolNamesFromDom(opts.anchorEl)); if (fromDom.length) return fromDom; return inferAppNamesFromMarkdown(markdown); } function footerCreditLine(markdown, opts = {}) { const model = formatExportModelLabel(); const apps = resolveExportAppNames(markdown, opts); const appPart = apps.length ? `根据${apps.join('、')}业务数据生成` : '根据业务数据生成'; const operator = getProfileUser(); const operatorPart = operator ? ` - 操作员:${operator}` : ''; return model ? `由 ccSparkle Agent ${appPart} - 大模型:${model}${operatorPart}。` : `由 ccSparkle Agent ${appPart}${operatorPart}。`; } let _profileUser = null; export function setProfileUser(user) { if (!user) return; if (typeof user === 'string') { _profileUser = user; return; } if (user.name) _profileUser = user.name; } export function getProfileUser() { return _profileUser; } let _libsPromise = null; function loadScript(src) { return new Promise((resolve, reject) => { const abs = new URL(src, import.meta.url).href; const existed = [...document.scripts].find(s => s.src === abs); if (existed) { if (existed.dataset.loaded === '1') return resolve(); existed.addEventListener('load', () => resolve(), { once: true }); existed.addEventListener('error', () => reject(new Error(`加载失败: ${src}`)), { once: true }); return; } const el = document.createElement('script'); el.src = abs; el.async = true; el.onload = () => { el.dataset.loaded = '1'; resolve(); }; el.onerror = () => reject(new Error(`加载失败: ${src}`)); document.head.appendChild(el); }); } async function ensureLibs() { if (!_libsPromise) { _libsPromise = (async () => { await loadScript('./vendor/docx.umd.cjs'); if (!window.docx?.Document || !window.docx?.Packer) { throw new Error('docx 库未就绪'); } })().catch(e => { _libsPromise = null; throw e; }); } return _libsPromise; } export function shouldShowExport(markdown) { const text = String(markdown || '').trim(); if (!text) return false; if (TABLE_RE.test(text)) return true; const compact = text.replace(/\s+/g, ''); return compact.length >= MIN_CHARS; } export function extractFilename(markdown) { const raw = String(markdown || ''); const cell = (label) => { const re = new RegExp(`\\|\\s*${label}\\s*\\|\\s*([^|\\n]+)\\s*\\|`); const m = raw.match(re); if (!m) return ''; const v = m[1].replace(/[*_`]/g, '').trim(); return (!v || v === '—') ? '' : v; }; const yearOnly = (y) => { const s = String(y || '').trim(); if (!s) return ''; const m = s.match(/(\d{4})/); return m ? m[1] : s.replace(/年$/, ''); }; const teamTitleSubject = (name) => { const s = String(name || '').trim(); if (!s) return ''; return /班组$/.test(s) ? s : `${s}班组`; }; let title = (raw.match(/^#\s+([^#\n]+)$/m) || [])[1]?.trim() || ''; if (!title || /^(班组概况|基本信息|AI分析报告|暂无数据|个人画像报告|班组画像报告)$/.test(title)) { let subject = ''; let kind = ''; const team = cell('班组'); if (team) { subject = team; kind = 'team'; } else { const name = cell('姓名'); if (name) { subject = name; kind = 'person'; } } if (!subject) { try { const st = getSessionState() || {}; if (st.currentTeam) { subject = String(st.currentTeam).trim(); kind = 'team'; } else if (st.currentEmployeeName) { subject = String(st.currentEmployeeName).trim(); kind = 'person'; } } catch (_e) { } } let year = yearOnly(cell('年份')); if (!year) { try { const y = getSessionState()?.currentYear; if (y != null && y !== '') year = yearOnly(y); } catch (_e) { } } if (subject && kind === 'person') { title = `${subject}个人${year || ''}年度报告`; } else if (subject && kind === 'team') { title = `${teamTitleSubject(subject)}${year || ''}年度报告`; } } if (!title) title = 'AI分析报告'; title = title .replace(/[*_`#\[\]()]/g, '') .replace(/[\\/:*?"<>|]+/g, '_') .replace(/\s+/g, '_') .replace(/·/g, '_') .replace(/_+/g, '_') .replace(/^_|_$/g, '') .slice(0, 80) || 'AI分析报告'; const d = new Date(); const stamp = [ d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0'), ].join(''); return `${title}_${stamp}`; } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 2000); } function cellBorders() { const { BorderStyle } = window.docx; const b = { style: BorderStyle.SINGLE, size: 4, color: '8FA8C8' }; return { top: b, bottom: b, left: b, right: b }; } function cellWeight(text) { const s = String(text || ''); let w = 0; for (const ch of s) { w += /[\u3400-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]/.test(ch) ? 2 : 1; } return Math.max(w, 2); } function estimateColumnWidths(trs, colCount, tableWidthDxa) { const weights = Array.from({ length: colCount }, () => 2); for (const tr of trs) { const cells = [...tr.children].filter(c => /^(td|th)$/i.test(c.tagName)); for (let i = 0; i < colCount; i++) { const t = cells[i] ? textOf(cells[i]) : ''; weights[i] = Math.max(weights[i], cellWeight(t)); } } const minW = Math.max(720, Math.floor(tableWidthDxa / (colCount * 4))); const maxW = Math.floor(tableWidthDxa * (colCount <= 2 ? 0.72 : 0.58)); const sum = weights.reduce((a, b) => a + b, 0) || colCount; let widths = weights.map((w) => { const raw = Math.floor((tableWidthDxa * w) / sum); return Math.min(maxW, Math.max(minW, raw)); }); let total = widths.reduce((a, b) => a + b, 0); if (total !== tableWidthDxa && total > 0) { const scale = tableWidthDxa / total; widths = widths.map((w, i) => ( i === colCount - 1 ? 0 : Math.max(minW, Math.floor(w * scale)) )); const used = widths.slice(0, -1).reduce((a, b) => a + b, 0); widths[colCount - 1] = Math.max(minW, tableWidthDxa - used); } return widths; } function columnWidthsToPct(widthsDxA) { const total = widthsDxA.reduce((a, b) => a + b, 0) || 1; const pcts = widthsDxA.map((w) => Math.max(1, Math.floor((100 * w) / total))); const used = pcts.slice(0, -1).reduce((a, b) => a + b, 0); pcts[pcts.length - 1] = Math.max(1, 100 - used); return pcts; } function pctFiftieths(percent0to100) { const n = Math.max(1, Math.min(5000, Math.round(Number(percent0to100) * 50))); return String(n); } function isAnalysisKicker(text) { return /^(总体表现|主要问题|改进建议)\s*[::]?$/.test(String(text || '').trim()); } function inlineToRuns(node, base = {}) { const { TextRun } = window.docx; const runs = []; const walk = (n, style) => { if (!n) return; if (n.nodeType === Node.TEXT_NODE) { const t = n.textContent || ''; if (!t) return; const parts = t.split('\n'); parts.forEach((part, i) => { if (part) runs.push(new TextRun({ text: part, ...style })); if (i < parts.length - 1) runs.push(new TextRun({ break: 1 })); }); return; } if (n.nodeType !== Node.ELEMENT_NODE) return; const tag = n.tagName.toLowerCase(); if (tag === 'br') { runs.push(new TextRun({ break: 1 })); return; } const next = { ...style }; if (tag === 'strong' || tag === 'b') next.bold = true; if (tag === 'em' || tag === 'i') next.italics = true; if (tag === 'code') { next.font = 'Consolas'; next.size = 18; } if (tag === 'a') { next.color = '2c5aa0'; next.underline = {}; } for (const child of n.childNodes) walk(child, next); }; walk(node, { font: 'Microsoft YaHei', size: 22, ...base }); if (!runs.length) runs.push(new TextRun({ text: '', font: 'Microsoft YaHei', size: 22 })); return runs; } function textOf(el) { return (el?.textContent || '').replace(/\s+/g, ' ').trim(); } function pushParagraph(children, opts) { const { Paragraph } = window.docx; children.push(new Paragraph(opts)); } function htmlElementToBlocks(el, out, listCtx = { ordered: false, index: 0 }) { const { Paragraph, TextRun, Table, TableRow, TableCell, WidthType, AlignmentType, } = window.docx; if (!el || el.nodeType !== Node.ELEMENT_NODE) return; const tag = el.tagName.toLowerCase(); if (tag === 'h1' || tag === 'h2' || tag === 'h3') { const size = tag === 'h1' ? 32 : tag === 'h2' ? 26 : 24; const color = tag === 'h1' ? '1a4a8a' : tag === 'h2' ? '2c5aa0' : '3d6fa8'; const border = tag === 'h2' ? { bottom: { style: window.docx.BorderStyle.SINGLE, size: 8, color: '2c5aa0', space: 4 } } : tag === 'h3' ? { bottom: { style: window.docx.BorderStyle.SINGLE, size: 4, color: 'A8C4E0', space: 2 } } : undefined; out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { before: tag === 'h1' ? 280 : tag === 'h2' ? 220 : 160, after: 100 }, border, children: inlineToRuns(el, { bold: true, size, color }), })); return; } if (tag === 'p') { const plain = textOf(el); const kicker = isAnalysisKicker(plain) || el.classList?.contains('analysis-kicker'); if (kicker) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { before: 140, after: 40 }, children: [new TextRun({ text: plain.replace(/[::]\s*$/, ''), bold: true, size: 22, color: '2c5aa0', font: 'Microsoft YaHei', })], })); return; } const split = plain.match(/^(总体表现|主要问题|改进建议)\s*[::]\s*(.+)$/); if (split) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { before: 140, after: 40 }, children: [new TextRun({ text: split[1], bold: true, size: 22, color: '2c5aa0', font: 'Microsoft YaHei', })], })); out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 100 }, children: [new TextRun({ text: split[2], font: 'Microsoft YaHei', size: 22, })], })); return; } out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 100 }, children: inlineToRuns(el), })); return; } if (tag === 'blockquote') { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 120 }, indent: { left: 420 }, border: { left: { style: window.docx.BorderStyle.SINGLE, size: 12, color: '2c5aa0', space: 8 }, }, children: inlineToRuns(el, { italics: true, color: '444444' }), })); return; } if (tag === 'ul' || tag === 'ol') { const ordered = tag === 'ol'; let i = 0; for (const li of el.children) { if (li.tagName?.toLowerCase() !== 'li') continue; i++; const prefix = ordered ? `${i}. ` : '• '; const runs = [ new TextRun({ text: prefix, font: 'Microsoft YaHei', size: 22 }), ...inlineToRuns(li), ]; out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 60 }, indent: { left: 360 }, children: runs, })); } return; } if (tag === 'pre') { const code = el.textContent || ''; for (const line of code.split('\n')) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 0 }, children: [new TextRun({ text: line || ' ', font: 'Consolas', size: 18, })], })); } out.push(new Paragraph({ children: [] })); return; } if (tag === 'table') { const rows = []; const trs = [...el.querySelectorAll('tr')]; let colCount = 1; for (const tr of trs) { const cells = [...tr.children].filter(c => /^(td|th)$/i.test(c.tagName)); colCount = Math.max(colCount, cells.length || 1); } const columnWidths = estimateColumnWidths(trs, colCount, CONTENT_WIDTH_DXA); const columnPcts = columnWidthsToPct(columnWidths); const borders = cellBorders(); const { TableLayoutType, ShadingType } = window.docx; for (const tr of trs) { const cells = [...tr.children].filter(c => /^(td|th)$/i.test(c.tagName)); const isHeader = cells.some(c => c.tagName.toLowerCase() === 'th'); const rowCells = []; for (let i = 0; i < colCount; i++) { const cell = cells[i]; const headerCell = isHeader || cell?.tagName?.toLowerCase() === 'th'; const runs = cell ? inlineToRuns(cell, headerCell ? { bold: true, size: 18, color: '1a3a5c' } : { size: 18, color: '222222' }) : [new TextRun({ text: ' ', size: 18 })]; const bodyFill = (rows.length % 2 === 1) ? 'F3F7FC' : 'FFFFFF'; rowCells.push(new TableCell({ borders, width: { size: pctFiftieths(columnPcts[i]), type: WidthType.PERCENTAGE }, margins: { top: 50, bottom: 50, left: 70, right: 70 }, shading: headerCell ? { type: ShadingType.CLEAR, fill: 'D6E4F5' } : { type: ShadingType.CLEAR, fill: bodyFill }, children: [new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 0 }, children: runs, })], })); } rows.push(new TableRow({ children: rowCells })); } if (rows.length) { out.push(new Table({ width: { size: pctFiftieths(100), type: WidthType.PERCENTAGE }, columnWidths, layout: TableLayoutType.FIXED, rows, })); out.push(new Paragraph({ children: [] })); } return; } if (tag === 'div' || tag === 'section' || tag === 'article' || tag === 'body' || tag === 'span') { if (tag === 'span' && ![...el.children].some(c => /^(h\d|p|ul|ol|table|div|pre|blockquote)$/i.test(c.tagName))) { if (textOf(el)) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 120 }, children: inlineToRuns(el), })); } return; } for (const child of el.childNodes) { if (child.nodeType === Node.ELEMENT_NODE) { htmlElementToBlocks(child, out, listCtx); } else if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 80 }, children: [new TextRun({ text: child.textContent.trim(), font: 'Microsoft YaHei', size: 22, })], })); } } return; } if (textOf(el)) { out.push(new Paragraph({ alignment: AlignmentType.LEFT, spacing: { after: 100 }, children: inlineToRuns(el), })); } } function buildDocChildren(markdown, opts = {}) { const { Paragraph, TextRun, AlignmentType } = window.docx; const titleMatch = String(markdown || '').match(/^#{1,6}\s+(.+)$/m); const title = titleMatch ? titleMatch[1].trim() : 'AI 分析报告'; const now = new Date().toLocaleString('zh-CN', { hour12: false }); const bodyHtml = renderMarkdown(markdown); const credit = footerCreditLine(markdown, opts); const wrap = document.createElement('div'); wrap.innerHTML = `

ccSparkle Agent · 智能体中台

导出时间: ${now.replace(/

${bodyHtml}
`; const children = []; children.push(new Paragraph({ spacing: { after: 60 }, children: [new TextRun({ text: 'ccSparkle Agent · 智能体中台', bold: true, color: '2c5aa0', font: 'Microsoft YaHei', size: 24, })], })); children.push(new Paragraph({ spacing: { after: 200 }, border: { bottom: { style: window.docx.BorderStyle.SINGLE, size: 12, color: '2c5aa0', space: 4 }, }, children: [new TextRun({ text: `导出时间: ${now} · ${title}`, color: '666666', font: 'Microsoft YaHei', size: 18, })], })); const body = wrap.querySelector('.doc-body'); if (body) htmlElementToBlocks(body, children); children.push(new Paragraph({ spacing: { before: 300 }, border: { top: { style: window.docx.BorderStyle.SINGLE, size: 6, color: 'CCCCCC', space: 8 }, }, children: [new TextRun({ text: credit, color: '888888', font: 'Microsoft YaHei', size: 16, })], })); if (children.length === 0) { children.push(new Paragraph({ alignment: AlignmentType.LEFT, children: [new TextRun({ text: '(无内容)', font: 'Microsoft YaHei' })], })); } return children; } export async function exportWord(markdown, opts = {}) { await ensureLibs(); const { Document, Packer } = window.docx; const cleanMd = stripReportMarkers(markdown); const baseName = extractFilename(cleanMd || markdown); const suffix = opts.filenameSuffix ? `-${opts.filenameSuffix}` : ''; const filename = `${baseName}${suffix}.docx`; const doc = new Document({ creator: 'ccSparkle Agent', title: baseName + suffix, sections: [{ properties: { page: { size: { width: PAGE_WIDTH_DXA, height: 16838 }, margin: { top: 720, right: PAGE_MARGIN_X_DXA, bottom: 720, left: PAGE_MARGIN_X_DXA, }, }, }, children: buildDocChildren(cleanMd || markdown, opts), }], }); const blob = await Packer.toBlob(doc); downloadBlob(blob, filename); } export function mountExportBar(assistantTextEl, markdown, opts = {}) { if (!assistantTextEl) return; const old = assistantTextEl.querySelector('.export-actions'); if (old) old.remove(); const cleanMd = stripReportMarkers(markdown); if (!shouldShowExport(cleanMd || markdown)) return; const toolNames = Array.isArray(opts.toolNames) && opts.toolNames.length ? opts.toolNames : collectToolNamesFromMessages(opts.messages); const appNames = Array.isArray(opts.appNames) && opts.appNames.length ? opts.appNames : resolveAppNamesFromToolNames(toolNames); const bar = document.createElement('div'); bar.className = 'export-actions'; bar.innerHTML = ` `; assistantTextEl.appendChild(bar); const statusEl = bar.querySelector('.export-status'); const setStatus = (text, isError = false) => { if (!statusEl) return; if (!text) { statusEl.hidden = true; statusEl.textContent = ''; return; } statusEl.hidden = false; statusEl.textContent = text; statusEl.classList.toggle('error', !!isError); }; bar.addEventListener('click', async (e) => { const btn = e.target.closest('[data-export]'); if (!btn || btn.disabled) return; const exportType = btn.dataset.export; const buttons = [...bar.querySelectorAll('.export-btn')]; buttons.forEach(b => { b.disabled = true; }); try { if (exportType === 'raw-thinking') { setStatus('正在生成原文+思考...'); let raw = opts.rawMarkdown; if (!raw && opts.messages) { raw = opts.messages .filter(m => m?.role === 'tool' && m?.content) .map(m => String(m.content)) .join('\n\n---\n\n'); } if (!raw) raw = cleanMd || markdown; raw = raw.replace(//gi, '').replace(/\n{3,}/g, '\n\n').trim(); let thinking = opts.thinkingMd; if (!thinking && opts.messages) { const parts = opts.messages .filter(m => m?.role === 'assistant' && m?.thinking) .map(m => String(m.thinking)); thinking = parts.length ? parts.join('\n\n---\n\n') : ''; } const thinkingSection = thinking ? `\n\n---\n\n# AI 思考过程\n\n${thinking}` : ''; await exportWord(`${raw}${thinkingSection}`, { appNames, toolNames, anchorEl: assistantTextEl, filenameSuffix: '原文+思考', }); setStatus(thinking ? '已导出(原文 + 思考)' : '已导出(原文,本次无思考)'); } else { setStatus('正在生成 Word...'); await exportWord(cleanMd || markdown, { appNames, toolNames, anchorEl: assistantTextEl, }); } setTimeout(() => setStatus(''), 2500); } catch (err) { console.error('[export]', err); setStatus(`导出失败: ${err.message || err}`, true); } finally { buttons.forEach(b => { b.disabled = false; }); } }); }