diff --git a/backend/python/app/api/chat_media.py b/backend/python/app/api/chat_media.py index feaa7d7..b444fa7 100644 --- a/backend/python/app/api/chat_media.py +++ b/backend/python/app/api/chat_media.py @@ -1430,7 +1430,7 @@ async def _generate_media_task(ai_msg_id: str, ai_model_id: str, prompt: str, us logger.info(f"[_generate_media_task] 启动 media 生成 msg={ai_msg_id} model={ai_model_id} prompt={prompt[:60]!r}") caps = [c.lower() for c in (ai_model.capabilities or [])] is_video = "video" in caps - timeout_seconds = 300 if is_video else 90 + timeout_seconds = 300 if is_video else 180 try: result = await asyncio.wait_for( _generate_media(db, ai_model, prompt, user_id), diff --git a/backend/python/app/api/chat_service.py b/backend/python/app/api/chat_service.py index c06e352..bda1f8b 100644 --- a/backend/python/app/api/chat_service.py +++ b/backend/python/app/api/chat_service.py @@ -1657,6 +1657,16 @@ async def _send_message_core( + # 媒体分支直接返回,不会进入 LLM 工具调用;提前初始化避免未绑定 + + called_skill_names = [] + + + + called_mcp_names = [] + + + if effective_skill_ids: diff --git a/frontend/src/pages/front/ChatPage.css b/frontend/src/pages/front/ChatPage.css index a8db290..afd0eb5 100644 --- a/frontend/src/pages/front/ChatPage.css +++ b/frontend/src/pages/front/ChatPage.css @@ -471,10 +471,15 @@ } .md-table td { - padding: 10px 14px; + padding: 8px 12px; border-bottom: 1px solid #EEF0F4; color: var(--color-text-primary); vertical-align: top; + line-height: 1.5; +} + +.md-table td br { + line-height: 0.8; } .md-table tbody tr:last-child td { diff --git a/frontend/src/pages/front/ChatPage.tsx b/frontend/src/pages/front/ChatPage.tsx index 4b5f2e4..edc3b06 100644 --- a/frontend/src/pages/front/ChatPage.tsx +++ b/frontend/src/pages/front/ChatPage.tsx @@ -189,41 +189,8 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { return '\x00MEDIA' + (mediaBlocks.length - 1) + '\x00'; }); - // 2.7 Extract Markdown tables: | h1 | h2 |\n|---|---| ... | - // 把整段表格替换为占位符,表格内容(已转义)放入 tableBlocks 数组 - const tableBlocks: string[] = []; - text = text.replace( - /((?:^\|.+\|\r?\n)+^\|[\s\-:|]+\|\r?\n(?:^\|.+\|\r?\n?)+)/gm, - (_m: string, block: string) => { - const lines = block.trim().split(/\r?\n/).filter((l: string) => l.trim()); - if (lines.length < 2) return block; - const headerCells = lines[0].split('|').slice(1, -1).map((c: string) => c.trim()); - const alignCells = lines[1].split('|').slice(1, -1).map((c: string) => { - const t = c.trim(); - const left = t.startsWith(':'); - const right = t.endsWith(':'); - if (left && right) return 'center'; - if (right) return 'right'; - if (left) return 'left'; - return 'left'; - }); - const bodyRows = lines.slice(2).map((row: string) => - row.split('|').slice(1, -1).map((c: string) => c.trim()) - ); - const thead = headerCells.map((c: string, i: number) => - `${c}` - ).join(''); - const tbody = bodyRows.map((r: string[]) => - '' + r.map((c: string, i: number) => - `${c}` - ).join('') + '' - ).join(''); - const html = `${thead}${tbody}
`; - const idx = tableBlocks.length; - tableBlocks.push(html); - return '\x00TABLE' + idx + '\x00'; - } - ); + // 2.6 Normalize excessive blank lines in prose (code blocks already extracted) + text = text.replace(/\n{3,}/g, '\n\n').trim(); // 2.8 LaTeX 数学公式:$$...$$(块级)和 $...$(行内) // 提取为占位符,渲染后用 KaTeX 处理 @@ -273,15 +240,58 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => { text = text.replace(/\*\*(.+?)\*\*/g, '$1'); text = text.replace(/(?$1'); + // 7.5 Extract Markdown tables after inline formatting so cells support bold/italic/links. + // 表格提取放在加粗/链接之后,避免单元格内 **xxx** 原样显示;同时压缩单元格内多余
。 + const tableBlocks: string[] = []; + text = text.replace( + /((?:^\|.+\|\r?\n)+^\|[\s\-:|]+\|\r?\n(?:^\|.+\|\r?\n?)+)/gm, + (_m: string, block: string) => { + const lines = block.trim().split(/\r?\n/).filter((l: string) => l.trim()); + if (lines.length < 2) return block; + const headerCells = lines[0].split('|').slice(1, -1).map((c: string) => c.trim()); + const alignCells = lines[1].split('|').slice(1, -1).map((c: string) => { + const t = c.trim(); + const left = t.startsWith(':'); + const right = t.endsWith(':'); + if (left && right) return 'center'; + if (right) return 'right'; + if (left) return 'left'; + return 'left'; + }); + const normalizeCell = (cell: string) => { + return cell + .replace(/\s*/gi, '
') + .replace(/\n/g, '
') + .replace(/(){2,}/g, '
') + .replace(/^()+|()+$/g, ''); + }; + const bodyRows = lines.slice(2).map((row: string) => + row.split('|').slice(1, -1).map((c: string) => normalizeCell(c.trim())) + ); + const thead = headerCells.map((c: string, i: number) => + `${normalizeCell(c)}` + ).join(''); + const tbody = bodyRows.map((r: string[]) => + '' + r.map((c: string, i: number) => + `${c}` + ).join('') + '' + ).join(''); + const html = `${thead}${tbody}
`; + const idx = tableBlocks.length; + tableBlocks.push(html); + return '\x00TABLE' + idx + '\x00'; + } + ); + // 8. Lists text = text.replace(/^\s*[-*] (.+)$/gm, '
  • $1
  • '); text = text.replace(/^\s*\d+\. (.+)$/gm, '
  • $1
  • '); // Wrap consecutive
  • in
      text = text.replace(/((?:
    • [\s\S]*?<\/li>\n?)+)/g, '
        $1
      '); - // 9. Convert double newlines to paragraph breaks, single to
      + // 9. Convert double newlines to paragraph breaks, single newlines to spaces text = text.replace(/\n\n/g, '

      '); - text = text.replace(/\n/g, '
      '); + text = text.replace(/\n/g, ' '); // 10. Restore code blocks and inline codes text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m: string, idx: string) => codeBlocks[parseInt(idx)] || '');