mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
🐛 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:
@@ -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}'`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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' }} />;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user