Commit 75526240 by User

添加聊天模块和数学公式展示功能

parent 3faf04c4
......@@ -61,6 +61,10 @@
"dayjs": "1.11.12",
"echarts": "5.5.1",
"font-awesome": "4.7.0",
"highlight.js": "^11.11.1",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"naive-ui": "2.39.0",
"nprogress": "0.2.0",
"pinia": "2.2.0",
......
......@@ -155,19 +155,20 @@ const local: App.I18n.Schema = {
500: 'Server Error',
'iframe-page': 'Iframe',
home: 'Home',
exception: '异常页',
chat: 'Chat',
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500',
document: '文档',
document_project: '项目文档',
'document_project-link': '项目文档(外链)',
document_vue: 'Vue文档',
document_localhost:'本地测试',
document_vite: 'Vite文档',
document_unocss: 'UnoCSS文档',
document_naive: 'Naive UI文档',
document_antd: 'Ant Design Vue文档'
document: 'Document',
document_project: 'Project Document',
'document_project-link': 'Project Document(Link)',
document_vue: 'Vue Document',
document_localhost: 'Local Test',
document_vite: 'Vite Document',
document_unocss: 'UnoCSS Document',
document_naive: 'Naive UI Document',
document_antd: 'Ant Design Vue Document'
},
page: {
login: {
......
......@@ -155,6 +155,7 @@ const local: App.I18n.Schema = {
500: '服务器错误',
'iframe-page': '外链页面',
home: '首页',
chat: 'AI助手',
exception: '异常页',
exception_403: '403',
exception_404: '404',
......
......@@ -20,5 +20,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
chat: () => import("@/views/chat/index.vue"),
home: () => import("@/views/home/index.vue"),
};
......@@ -39,6 +39,17 @@ export const generatedRoutes: GeneratedRoute[] = [
}
},
{
name: 'chat',
path: '/chat',
component: 'layout.base$view.chat',
meta: {
title: 'chat',
i18nKey: 'route.chat',
icon: 'mdi:chat',
order: 1
}
},
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
......
......@@ -179,6 +179,7 @@ const routeMap: RouteMap = {
"403": "/403",
"404": "/404",
"500": "/500",
"chat": "/chat",
"home": "/home",
"iframe-page": "/iframe-page/:url",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
......
......@@ -13,49 +13,52 @@ import { transformElegantRoutesToVueRoutes } from '../elegant/transform';
* @link https://github.com/soybeanjs/elegant-router?tab=readme-ov-file#custom-route
*/
const customRoutes: CustomRoute[] = [
// {
// name: 'exception',
// path: '/exception',
// component: 'layout.base',
// meta: {
// title: 'exception',
// i18nKey: 'route.exception',
// icon: 'ant-design:exception-outlined',
// order: 7
// },
// children: [
// {
// name: 'exception_403',
// path: '/exception/403',
// component: 'view.403',
// meta: {
// title: 'exception_403',
// i18nKey: 'route.exception_403',
// icon: 'ic:baseline-block'
// }
// },
// {
// name: 'exception_404',
// path: '/exception/404',
// component: 'view.404',
// meta: {
// title: 'exception_404',
// i18nKey: 'route.exception_404',
// icon: 'ic:baseline-web-asset-off'
// }
// },
// {
// name: 'exception_500',
// path: '/exception/500',
// component: 'view.500',
// meta: {
// title: 'exception_500',
// i18nKey: 'route.exception_500',
// icon: 'ic:baseline-wifi-off'
// }
// },
// ]
// }
{
name: 'exception',
path: '/exception',
component: 'layout.base',
meta: {
title: 'exception',
i18nKey: 'route.exception',
icon: 'ant-design:exception-outlined',
order: 7
},
children: [
{
name: 'exception_403',
path: '/exception/403',
component: 'view.403',
meta: {
title: 'exception_403',
i18nKey: 'route.exception_403',
icon: 'ic:baseline-block'
}
},
{
name: 'exception_404',
path: '/exception/404',
component: 'view.404',
meta: {
title: 'exception_404',
i18nKey: 'route.exception_404',
icon: 'ic:baseline-web-asset-off'
}
},
{
name: 'exception_500',
path: '/exception/500',
component: 'view.500',
meta: {
title: 'exception_500',
i18nKey: 'route.exception_500',
icon: 'ic:baseline-wifi-off'
}
}
]
},
// 以下是iframe-page的示例
// {
// name: 'document',
// path: '/document',
......@@ -72,7 +75,9 @@ const customRoutes: CustomRoute[] = [
// path: '/document/antd',
// component: 'view.iframe-page',
// props: {
// url: 'https://antdv.com/components/overview-cn'
// url: 'https://antdv.com/components/overview-cn',
// kvid: 'A0B25952-82B6-4A1E-B567-4BCDF9463Fs9A1',
// type: 'iframe'
// },
// meta: {
// title: 'document_antd',
......@@ -178,7 +183,7 @@ const customRoutes: CustomRoute[] = [
// icon: 'logos:vue'
// }
// }
// ]
// ]
// }
];
/**
......@@ -186,86 +191,85 @@ const customRoutes: CustomRoute[] = [
*
* @param MenuThree MenuThree
*/
// 以下是获取菜单的示例
// ${window.uiGlobalConfig.InternalCode}
// const { data: menus } = await getRootMenu(`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=dashboard`);
const { data: menus } = await getRootMenu(
`/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}`
);
// const rootMenu =getRootMenu('/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=Dashboard');
// // const { data: menus } = await getRootMenu(
// // `/Restful/Kivii.Basic.Entities.Menu/Show.json?RootInternalCode=${window.uiGlobalConfig.InternalCode}`
// // );
const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
const MenuRoot = menus?.MenuRoot;
// console.log(MenuRoot);
// 存储 MenuRoot 到 store
// const MenuThree = await getMenuThree(menus?.MenusMain?.Results);
// const MenuRoot = menus?.MenuRoot;
// // console.log(MenuRoot);
// // 存储 MenuRoot 到 store
if (MenuRoot) {
setTimeout(() => {
const routeStore = useRouteStore();
routeStore.setMenuRoot(MenuRoot);
}, 1000);
}
// if (MenuRoot) {
// setTimeout(() => {
// const routeStore = useRouteStore();
// routeStore.setMenuRoot(MenuRoot);
// }, 1000);
// }
const MenuThree2 = generateRoutes(MenuThree);
if (MenuThree2.length > 0) {
for (let i = 0; i < MenuThree2.length; i++) {
customRoutes.push(MenuThree2[i]);
}
}
function getMenuThree(data: any) {
const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
// eslint-disable-next-line eqeqeq
const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid == child.ParentKvid);
// eslint-disable-next-line no-unused-expressions
branchArr.length > 0 ? (father.children = branchArr) : '';
// eslint-disable-next-line eqeqeq
return father.ParentKvid == undefined;
});
}
function generateRoutes(data: any[]) {
return data.map(item => {
const route: any = {
name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
component: 'layout.base',
meta: {
title: item.DisplayName,
icon: 'mdi:file-document-multiple-outline',
order: item.SortId,
keepAlive: true
},
children: []
};
// const MenuThree2 = generateRoutes(MenuThree);
// if (MenuThree2.length > 0) {
// for (let i = 0; i < MenuThree2.length; i++) {
// customRoutes.push(MenuThree2[i]);
// }
// }
// function getMenuThree(data: any) {
// const cloneData = JSON.parse(JSON.stringify(data)); // 对源数据深度克隆
// return cloneData.filter((father: { Kvid: any; children: any; ParentKvid: undefined }) => {
// const branchArr = cloneData.filter((child: { ParentKvid: any }) => father.Kvid === child.ParentKvid);
// // eslint-disable-next-line no-unused-expressions
// branchArr.length > 0 ? (father.children = branchArr) : '';
// // eslint-disable-next-line eqeqeq
// return father.ParentKvid == undefined;
// });
// }
// function generateRoutes(data: any[]) {
// return data.map(item => {
// const route: any = {
// name: item.Type === 'System' ? `${item.Type}` : item.Kvid,
// path: item.Type === 'System' ? `/${item.Type}` : `/${item.Kvid}`,
// component: 'layout.base',
// meta: {
// title: item.DisplayName,
// icon: 'mdi:file-document-multiple-outline',
// order: item.SortId,
// keepAlive: true
// },
// children: []
// };
if (item.children && item.children.length > 0) {
route.children = item.children.map(
(child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
const sanitizedRemark =
child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
return {
name: child.Type === 'System' ? `${item.Type}_${sanitizedRemark}` : `${item.Kvid}_${child.Kvid}`,
path: child.Type === 'System' ? `/${item.Type}/${sanitizedRemark}` : `/${item.Kvid}/${child.Kvid}`,
component: 'view.iframe-page',
props: {
url: child.Type === 'System' ? sanitizedRemark : '',
kvid: child.Kvid,
type: child.Type
},
meta: {
title: child.DisplayName,
icon: child.Icon,
order: child.SortId,
keepAlive: true,
type: 'iframe'
}
};
}
);
}
// if (item.children && item.children.length > 0) {
// route.children = item.children.map(
// (child: { Remark: string; Type: string; Kvid: any; DisplayName: any; Icon: any; SortId: any }) => {
// const sanitizedRemark =
// child.Remark && child.Remark.startsWith('/') ? child.Remark.replace(/^\//, '') : child.Remark;
// return {
// name: child.Type === 'System' ? `${item.Type}_${sanitizedRemark}` : `${item.Kvid}_${child.Kvid}`,
// path: child.Type === 'System' ? `/${item.Type}/${sanitizedRemark}` : `/${item.Kvid}/${child.Kvid}`,
// component: 'view.iframe-page',
// props: {
// url: child.Type === 'System' ? sanitizedRemark : '',
// kvid: child.Kvid,
// type: child.Type
// },
// meta: {
// title: child.DisplayName,
// icon: child.Icon,
// order: child.SortId,
// keepAlive: true,
// type: 'iframe'
// }
// };
// }
// );
// }
return route;
});
}
// return route;
// });
// }
export function createStaticRoutes() {
const constantRoutes: ElegantRoute[] = [];
......
interface DeepSeekConfig {
apiKey: string;
baseURL: string;
model: string;
}
interface ModelOption {
id: string;
name: string;
description: string;
config: DeepSeekConfig;
}
interface DeepSeekMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface DeepSeekRequest {
model: string;
messages: DeepSeekMessage[];
temperature?: number;
max_tokens?: number;
stream?: boolean;
}
interface DeepSeekResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: {
role: string;
content: string;
};
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
class DeepSeekService {
private config: DeepSeekConfig;
private readonly modelOptions: ModelOption[] = [
{
id: 'deepseek',
name: 'DeepSeek Chat',
description: 'DeepSeek 深度思考模型,适合复杂问题分析',
config: {
apiKey: 'sk-71f583d8c182420d9d2001f01aa2b643', // 预配置的DeepSeek API Key
baseURL: 'https://api.deepseek.com',
model: 'deepseek-chat'
}
},
{
id: 'openai-gpt4o-mini',
name: 'GPT-4o Mini',
description: 'OpenAI GPT-4o Mini 模型,快速响应',
config: {
apiKey: 'sk-m5oOhoMo9l3QboachCkbIjRAetfc0c1DCcBPvHUs8U3cMzS2', // 预配置的OpenAI API Key
baseURL: 'https://aicvw.com',
model: 'gpt-4o-mini'
}
}
];
constructor() {
// 默认使用第一个模型
this.config = { ...this.modelOptions[0].config };
}
// 获取可用模型列表
getModelOptions(): ModelOption[] {
return this.modelOptions;
}
// 根据模型ID切换模型
switchModel(modelId: string) {
const selectedModel = this.modelOptions.find(model => model.id === modelId);
if (selectedModel) {
this.config = { ...selectedModel.config };
return true;
}
return false;
}
// 获取当前模型信息
getCurrentModel(): ModelOption | undefined {
return this.modelOptions.find(
model => model.config.model === this.config.model && model.config.baseURL === this.config.baseURL
);
}
// 设置 API Key (保留兼容性)
setApiKey(apiKey: string) {
this.config.apiKey = apiKey;
}
// 设置模型 (保留兼容性)
setModel(model: string) {
this.config.model = model;
}
// 检查配置是否完整
private validateConfig(): boolean {
if (!this.config.apiKey) {
console.error('API Key 未设置');
return false;
}
return true;
}
// 发送聊天请求
async chat(
messages: DeepSeekMessage[],
options?: {
temperature?: number;
maxTokens?: number;
stream?: boolean;
}
): Promise<DeepSeekResponse> {
if (!this.validateConfig()) {
throw new Error('API 配置不完整');
}
const requestBody: DeepSeekRequest = {
model: this.config.model,
messages,
temperature: options?.temperature || 0.7,
max_tokens: options?.maxTokens || 2000,
stream: options?.stream || false
};
try {
const response = await fetch(`${this.config.baseURL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API 请求失败: ${response.status} ${response.statusText} - ${errorData.error?.message || ''}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('API 调用错误:', error);
throw error;
}
}
// 流式聊天请求
async streamChat(
messages: DeepSeekMessage[],
onChunk: (chunk: string) => void,
onComplete: () => void,
onError: (error: Error) => void,
options?: {
temperature?: number;
maxTokens?: number;
}
): Promise<void> {
if (!this.validateConfig()) {
onError(new Error('API 配置不完整'));
return;
}
const requestBody: DeepSeekRequest = {
model: this.config.model,
messages,
temperature: options?.temperature || 0.7,
max_tokens: options?.maxTokens || 2000,
stream: true
};
try {
const response = await fetch(`${this.config.baseURL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API 请求失败: ${response.status} ${response.statusText} - ${errorData.error?.message || ''}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('无法读取响应流');
}
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
onComplete();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
onChunk(content);
}
} catch (parseError) {
// 忽略解析错误,继续处理下一行
}
}
}
}
} catch (error) {
console.error('流式请求错误:', error);
onError(error as Error);
}
}
// 生成最终回答
async generateAnswer(question: string, thinkingProcess: string): Promise<string> {
const answerPrompt: DeepSeekMessage[] = [
{
role: 'system',
content:
'你是一个专业的AI助手,善于给出详细、准确的回答。请基于之前的思考过程,给出最终的完整答案。如果涉及代码,请提供完整的、可运行的代码示例。使用Markdown格式回复。'
},
{
role: 'user',
content: `问题:${question}\n\n我的思考过程:\n${thinkingProcess}\n\n请基于以上思考,给出详细的最终答案。`
}
];
try {
const response = await this.chat(answerPrompt, { temperature: 0.7, maxTokens: 2000 });
return response.choices[0]?.message?.content || '抱歉,我无法生成回答。';
} catch (error) {
console.error('生成最终答案失败:', error);
throw error;
}
}
}
// 导出单例实例
export const deepSeekService = new DeepSeekService();
// 导出类型
export type { DeepSeekMessage, DeepSeekResponse, ModelOption };
......@@ -33,6 +33,7 @@ declare module "@elegant-router/types" {
"403": "/403";
"404": "/404";
"500": "/500";
"chat": "/chat";
"home": "/home";
"iframe-page": "/iframe-page/:url";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
......@@ -83,6 +84,7 @@ declare module "@elegant-router/types" {
| "403"
| "404"
| "500"
| "chat"
| "home"
| "iframe-page"
| "login"
......@@ -109,6 +111,7 @@ declare module "@elegant-router/types" {
| "500"
| "iframe-page"
| "login"
| "chat"
| "home"
>;
......
import katex from 'katex';
// 简单的数学公式渲染器
export const renderMathFormulas = (content: string): string => {
console.log('开始处理数学公式,内容长度:', content.length);
// 渲染块级数学公式 $$...$$ 和 \[...\](支持多行)
content = content.replace(/\$\$([\s\S]*?)\$\$/g, (match, formula) => {
console.log('发现$$块级公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: true,
throwOnError: false
});
console.log('$$块级公式渲染成功');
return `<div class="math-display">${rendered}</div>`;
} catch (error) {
console.error('KaTeX $$块级公式渲染错误:', error, '公式内容:', formula);
return `<div class="math-error">数学公式渲染错误: ${formula.trim()}</div>`;
}
});
// 渲染 \[...\] 块级数学公式
content = content.replace(/\\\[([\s\S]*?)\\\]/g, (match, formula) => {
console.log('发现\\[\\]块级公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: true,
throwOnError: false
});
console.log('\\[\\]块级公式渲染成功');
return `<div class="math-display">${rendered}</div>`;
} catch (error) {
console.error('KaTeX \\[\\]块级公式渲染错误:', error, '公式内容:', formula);
return `<div class="math-error">数学公式渲染错误: ${formula.trim()}</div>`;
}
});
// 渲染行内数学公式 $...$
content = content.replace(/\$([^$]+?)\$/g, (match, formula) => {
console.log('发现$行内公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: false,
throwOnError: false
});
console.log('$行内公式渲染成功');
return rendered;
} catch (error) {
console.error('KaTeX $行内公式渲染错误:', error, '公式内容:', formula);
return `<span class="math-error">公式错误: ${formula.trim()}</span>`;
}
});
// 渲染 \(...\) 行内数学公式
content = content.replace(/\\\((.*?)\\\)/g, (match, formula) => {
console.log('发现\\(\\)行内公式:', formula.trim());
try {
const rendered = katex.renderToString(formula.trim(), {
displayMode: false,
throwOnError: false
});
console.log('\\(\\)行内公式渲染成功');
return rendered;
} catch (error) {
console.error('KaTeX \\(\\)行内公式渲染错误:', error, '公式内容:', formula);
return `<span class="math-error">公式错误: ${formula.trim()}</span>`;
}
});
return content;
};
// 处理 markdown + 数学公式
export const renderMarkdownWithMath = (content: string): string => {
console.log('原始内容:', content);
// 首先处理数学公式
const withMath = renderMathFormulas(content);
console.log('处理数学公式后:', withMath);
// 简单的 markdown 处理,但保持数学公式完整
let processed = withMath;
// 处理标题(只在行首)
processed = processed.replace(/^### (.*)$/gm, '<h3>$1</h3>');
processed = processed.replace(/^## (.*)$/gm, '<h2>$1</h2>');
processed = processed.replace(/^# (.*)$/gm, '<h1>$1</h1>');
// 处理粗体
processed = processed.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 处理有序列表
processed = processed.replace(/^\d+\.\s+(.*)$/gm, '<li>$1</li>');
// 处理无序列表
processed = processed.replace(/^[-*]\s+(.*)$/gm, '<li>$1</li>');
// 将连续的 li 标签包装在 ol 或 ul 中
processed = processed.replace(/(<li>.*?<\/li>(?:\s*<li>.*?<\/li>)*)/gs, '<ul>$1</ul>');
// 处理段落(双换行表示段落分隔)
const paragraphs = processed.split(/\n\s*\n/);
processed = paragraphs.map(paragraph => {
const trimmed = paragraph.trim();
if (!trimmed) return '';
// 如果已经是HTML标签,不要包装
if (trimmed.startsWith('<')) {
return trimmed;
}
// 处理单行换行
const withBreaks = trimmed.replace(/\n/g, '<br>');
return `<p>${withBreaks}</p>`;
}).join('\n');
console.log('最终处理结果:', processed);
return processed;
};
import MarkdownIt from 'markdown-it';
// @ts-ignore
import markdownItKatex from 'markdown-it-katex';
// 创建 markdown-it 实例并配置 KaTeX 支持
export const createMarkdownRenderer = () => {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
// 使用 markdown-it-katex 插件
md.use(markdownItKatex, {
blockClass: 'math-display',
errorColor: '#cc0000',
macros: {
"\\RR": "\\mathbb{R}"
}
});
return md;
};
// 渲染 markdown 内容
export const renderMarkdown = (content: string): string => {
const md = createMarkdownRenderer();
return md.render(content);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment