🐛 fix(ai): 修复 WebView2 中 AI 面板动态模块加载失败 (#772)

## 背景

在 Wails WebView2 开发环境首次打开 AI 面板时,出现动态模块加载失败:

`Failed to fetch dynamically imported module: AIChatPanel.tsx`

同时 DevTools Network 中出现大量 Vite 502 请求,导致 AI 面板无法使用。

## 变更点

- 修复 AI 面板首次打开时动态模块加载失败的问题
- 调整代码高亮和 Mermaid 的加载方式,避免触发 Vite 依赖请求洪峰
- 增加依赖边界回归测试,确保普通代码块和 Mermaid 代码块正常渲染

## 影响范围

仅影响 AI 面板 Markdown、代码高亮和 Mermaid 渲染路径,不改变数据库连接、SQL 执行和普通工作台功能。

## 验证方式

- AI 依赖边界测试和 Markdown 测试共 8 项全部通过
- Wails WebView2 冷启动后 AI 面板正常显示“你好,我是 GoNavi AI”
- 修复后相关 Network 502 请求为 0

## 截图

<img width="2150" height="1168" alt="ai-panel-loaded-after"
src="https://github.com/user-attachments/assets/3151451f-2d23-4353-b45a-3e7a429ea6cb"
/>
<img width="2150" height="1168" alt="ai-panel-load-failure-before"
src="https://github.com/user-attachments/assets/00a6ef33-d536-4896-8de6-db847d3aafce"
/>
<img width="628" height="634" alt="ai-panel-network-502-before"
src="https://github.com/user-attachments/assets/db3b9974-3de6-4157-87b1-dfb17a45dcb1"
/>



## 备注

该 PR 目标分支为 `dev`,仅包含 AI 面板功能修复。
This commit is contained in:
Syngnat
2026-07-29 11:02:18 +08:00
committed by GitHub
4 changed files with 123 additions and 16 deletions

View File

@@ -0,0 +1,27 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
const source = readFileSync(new URL('./AIMessageCodeBlock.tsx', import.meta.url), 'utf8');
const viteConfigSource = readFileSync(new URL('../../../../vite.config.ts', import.meta.url), 'utf8');
describe('AIMessageCodeBlock dependency boundary', () => {
it('does not pull the complete syntax-highlighter language registry into the AI panel chunk', () => {
expect(source).not.toMatch(/from\s+['"]react-syntax-highlighter['"]/);
expect(source).toMatch(/react-syntax-highlighter\/dist\/esm\/prism-light/);
});
it('loads Mermaid only when a Mermaid fenced block is rendered', () => {
expect(source).not.toMatch(/^import\s+mermaid\s+from\s+['"]mermaid['"];?$/m);
expect(source).toMatch(/await\s+import\(['"]mermaid['"]\)/);
});
it('pre-bundles every static syntax-highlighter entry before Wails opens the panel', () => {
const syntaxHighlighterImports = Array.from(source.matchAll(
/from\s+['"](react-syntax-highlighter\/dist\/esm\/[^'"]+)['"]/g,
)).map((match) => match[1]);
for (const dependency of syntaxHighlighterImports) {
expect(viteConfigSource).toContain(`'${dependency}'`);
}
});
});

View File

@@ -1,9 +1,29 @@
import React, { useState } from 'react';
import { Tooltip, message } from 'antd';
import { CheckOutlined, CopyOutlined, PlayCircleOutlined } from '@ant-design/icons';
import mermaid from 'mermaid';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
import diff from 'react-syntax-highlighter/dist/esm/languages/prism/diff';
import go from 'react-syntax-highlighter/dist/esm/languages/prism/go';
import ini from 'react-syntax-highlighter/dist/esm/languages/prism/ini';
import java from 'react-syntax-highlighter/dist/esm/languages/prism/java';
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
import markup from 'react-syntax-highlighter/dist/esm/languages/prism/markup';
import php from 'react-syntax-highlighter/dist/esm/languages/prism/php';
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
import ruby from 'react-syntax-highlighter/dist/esm/languages/prism/ruby';
import rust from 'react-syntax-highlighter/dist/esm/languages/prism/rust';
import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql';
import toml from 'react-syntax-highlighter/dist/esm/languages/prism/toml';
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
import vscDarkPlus from 'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus';
import vs from 'react-syntax-highlighter/dist/esm/styles/prism/vs';
import { t as catalogTranslate } from '../../../i18n/catalog';
import type { I18nParams } from '../../../i18n/types';
@@ -12,6 +32,29 @@ import Modal from '../../common/ResizableDraggableModal';
import type { OverlayWorkbenchTheme } from '../../../utils/overlayWorkbenchTheme';
import { buildAIReadonlyPreviewSQL } from '../../../utils/aiSqlLimit';
[
bash,
css,
diff,
go,
ini,
java,
javascript,
json,
jsx,
markdown,
markup,
php,
python,
ruby,
rust,
sql,
toml,
tsx,
typescript,
yaml,
].forEach((language) => SyntaxHighlighter.registerLanguage('', language));
interface AIMessageCodeBlockProps {
className?: string;
inline?: boolean;
@@ -56,24 +99,31 @@ const MermaidRenderer: React.FC<{ chart: string; darkMode: boolean }> = ({ chart
if (!containerRef.current) {
return;
}
try {
mermaid.initialize({ startOnLoad: false, theme: darkMode ? 'dark' : 'default' });
const id = `mermaid-${Math.random().toString(36).slice(2)}`;
(async () => {
let cancelled = false;
(async () => {
try {
const { default: mermaid } = await import('mermaid');
if (cancelled || !containerRef.current) {
return;
}
mermaid.initialize({ startOnLoad: false, theme: darkMode ? 'dark' : 'default' });
const id = `mermaid-${Math.random().toString(36).slice(2)}`;
const result: any = await mermaid.render(id, chart);
if (containerRef.current) {
if (!cancelled && containerRef.current) {
containerRef.current.innerHTML = result.svg || result;
}
})().catch((error: any) => {
if (containerRef.current) {
containerRef.current.innerHTML = `<div style="color:#ef4444; padding:12px; background:rgba(239,68,68,0.1); border-radius:6px; font-size:12px">${escapeHtml(copy('ai_chat.message.mermaid.parse_failed', { detail: error?.message || '' }))}</div>`;
} catch (error: any) {
if (!cancelled && containerRef.current) {
containerRef.current.innerHTML = `<div style="color:#ef4444; padding:12px; background:rgba(239,68,68,0.1); border-radius:6px; font-size:12px">${escapeHtml(copy('ai_chat.message.mermaid.render_failed', { detail: error?.message || '' }))}</div>`;
}
});
} catch (error: any) {
if (containerRef.current) {
containerRef.current.innerHTML = `<div style="color:#ef4444; padding:12px; background:rgba(239,68,68,0.1); border-radius:6px; font-size:12px">${escapeHtml(copy('ai_chat.message.mermaid.render_failed', { detail: error?.message || '' }))}</div>`;
}
}
})();
return () => {
cancelled = true;
};
}, [chart, copy, darkMode]);
return <div ref={containerRef} className="ai-mermaid-container" style={{ margin: '16px 0', display: 'flex', justifyContent: 'flex-start', overflowX: 'auto' }} />;

View File

@@ -49,6 +49,7 @@ describe('AIMessageMarkdown', () => {
expect(markup).toContain('Insert');
expect(markup).toContain('Execute');
expect(markup).toContain('Preview');
expect(markup).toMatch(/<span class="token"[^>]*>SELECT<\/span>/);
});
it('renders SQL code block actions in Chinese when an i18n provider is available', () => {

View File

@@ -1,6 +1,32 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
const aiCodeHighlightDeps = [
'react-syntax-highlighter/dist/esm/prism-light',
'react-syntax-highlighter/dist/esm/languages/prism/bash',
'react-syntax-highlighter/dist/esm/languages/prism/css',
'react-syntax-highlighter/dist/esm/languages/prism/diff',
'react-syntax-highlighter/dist/esm/languages/prism/go',
'react-syntax-highlighter/dist/esm/languages/prism/ini',
'react-syntax-highlighter/dist/esm/languages/prism/java',
'react-syntax-highlighter/dist/esm/languages/prism/javascript',
'react-syntax-highlighter/dist/esm/languages/prism/json',
'react-syntax-highlighter/dist/esm/languages/prism/jsx',
'react-syntax-highlighter/dist/esm/languages/prism/markdown',
'react-syntax-highlighter/dist/esm/languages/prism/markup',
'react-syntax-highlighter/dist/esm/languages/prism/php',
'react-syntax-highlighter/dist/esm/languages/prism/python',
'react-syntax-highlighter/dist/esm/languages/prism/ruby',
'react-syntax-highlighter/dist/esm/languages/prism/rust',
'react-syntax-highlighter/dist/esm/languages/prism/sql',
'react-syntax-highlighter/dist/esm/languages/prism/toml',
'react-syntax-highlighter/dist/esm/languages/prism/tsx',
'react-syntax-highlighter/dist/esm/languages/prism/typescript',
'react-syntax-highlighter/dist/esm/languages/prism/yaml',
'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus',
'react-syntax-highlighter/dist/esm/styles/prism/vs',
]
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
@@ -21,6 +47,9 @@ export default defineConfig({
'dayjs/locale/ru',
'dayjs/locale/zh-cn',
'dayjs/locale/zh-tw',
// Keep lazy AI panel imports out of Vite's mid-session dependency discovery.
// A discovery reload invalidates React.lazy inside Wails and used to leave the panel blank.
...aiCodeHighlightDeps,
],
},
server: {