Files
Agent-System/web/src/engine/replyDocument/serializeMarkdown.ts
T
WeiCHEN 1fc7d0d01f feat(export): 实现单次智能体回复导出、私密文档保存与隐私防泄露全套闭环 (Phase 0 - Phase 2)
- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API
- Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制
- Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动
- 专项修复:
  1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表
  2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply)
  3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
2026-09-13 02:50:21 +00:00

86 lines
2.4 KiB
TypeScript

import { ReplyDocumentIR, ReplyNode, ReplyInline } from '../../types/replyExport';
/**
* 将 Reply Document IR 序列化为规范化 GFM Markdown
*/
export function serializeMarkdown(ir: ReplyDocumentIR): string {
return ir.nodes.map(renderNodeToMarkdown).join('\n\n');
}
function renderNodeToMarkdown(node: ReplyNode): string {
switch (node.type) {
case 'heading': {
const hashes = '#'.repeat(node.level);
return `${hashes} ${node.children.map(renderInlineToMarkdown).join('')}`;
}
case 'paragraph': {
return node.children.map(renderInlineToMarkdown).join('');
}
case 'list': {
return node.items
.map((item, idx) => {
const prefix = node.ordered ? `${idx + 1}. ` : '- ';
return `${prefix}${item.map(renderInlineToMarkdown).join('')}`;
})
.join('\n');
}
case 'table': {
const headerLine = `| ${node.header.map((c) => c.map(renderInlineToMarkdown).join('')).join(' | ')} |`;
const sepLine = `| ${node.header.map(() => '---').join(' | ')} |`;
const rowLines = node.rows.map(
(row) => `| ${row.map((c) => c.map(renderInlineToMarkdown).join('')).join(' | ')} |`
);
return [headerLine, sepLine, ...rowLines].join('\n');
}
case 'blockquote': {
const inner = node.children.map(renderNodeToMarkdown).join('\n\n');
return inner
.split('\n')
.map((l) => `> ${l}`)
.join('\n');
}
case 'code': {
const lang = node.language || '';
return `\`\`\`${lang}\n${node.value}\n\`\`\``;
}
case 'math': {
if (node.display) {
return `$$\n${node.latex}\n$$`;
}
return `$${node.latex}$`;
}
case 'mermaid': {
return `\`\`\`mermaid\n${node.source}\n\`\`\``;
}
case 'divider': {
return '---';
}
default:
return '';
}
}
function renderInlineToMarkdown(inline: ReplyInline): string {
switch (inline.type) {
case 'text': {
let val = inline.value;
if (inline.bold) val = `**${val}**`;
if (inline.italic) val = `*${val}*`;
if (inline.strike) val = `~~${val}~~`;
return val;
}
case 'code':
return `\`${inline.value}\``;
case 'math':
return `$${inline.latex}$`;
case 'link': {
const text = inline.children.map(renderInlineToMarkdown).join('');
return `[${text}](${inline.href})`;
}
case 'hardBreak':
return '\n';
default:
return '';
}
}