Compare commits

...

23 Commits

Author SHA1 Message Date
OrionMark
eb6cc79796 perf(AiChat): 优化输入框以及模型回答展示 2025-06-07 11:19:51 +08:00
OrionMark
68cf5bddca Merge branch 'master' into feat/AIChat 2025-06-07 11:13:13 +08:00
OrionMark
d8a24fc106 Merge branch 'master' into feat/AIChat 2025-06-06 21:45:13 +08:00
OrionMark
1a3868b2ea perf(AiChat): 对接新的AI接口、加载列表切换虚拟列表 2025-06-06 21:44:33 +08:00
OrionMark
d1e949aa87 fix(request): 🐛 修复DELETE请求传参问题 2025-06-06 21:41:53 +08:00
OrionMark
a24f3d0511 fix(router): 🐛 修复SSE请求断开问题 2025-06-06 21:40:30 +08:00
OrionMark
277d598a69 perf(AiChat): 优化聊天记录虚拟列表加载 2025-05-24 10:35:30 +08:00
OrionMark
09337131b5 Merge branch 'master' into feat/AIChat 2025-05-24 09:44:53 +08:00
OrionMark
06384bb204 Merge branch 'master' into feat/AIChat 2025-05-22 11:48:36 +08:00
OrionMark
86335c917d perf(AiChat): 优化模型切换选中、滚动加载逻辑 2025-05-18 21:16:45 +08:00
OrionMark
ea738270a8 Merge branch 'master' into feat/AIChat 2025-05-18 19:52:39 +08:00
Dawn
78565bacc8 Merge branch 'master' into feat/AIChat 2025-05-14 11:10:37 +08:00
Dawn
b50c386432 Merge branch 'master' into feat/AIChat 2025-05-13 19:08:20 +08:00
OrionMark
43d85b10cc Merge branch 'master' into feat/AIChat 2025-05-13 10:17:43 +08:00
OrionMark
f17f07cc2b perf(AiChat): 优化左侧会话加载,删除会话,以及滚动逻辑 2025-05-13 10:02:25 +08:00
Dawn
c078a0be1d Merge branch 'master' into feat/AIChat 2025-05-12 22:06:03 +08:00
Dawn
72740e7d2d Merge branch 'master' into feat/AIChat 2025-05-12 19:37:37 +08:00
Dawn
0bc93f14da Merge remote-tracking branch 'origin/master' into feat/AIChat 2025-05-11 19:56:53 +08:00
OrionMark
9c33073f75 Merge branch 'master' into feat/AIChat 2025-05-09 15:18:48 +08:00
OrionMark
3c608a8875 perf(AiChat): 优化回答加载,以及滚动逻辑 2025-05-08 22:25:59 +08:00
OrionMark
5cfbac209d Merge branch 'master' into feat/AIChat 2025-05-08 22:20:15 +08:00
OrionMark
706021871a feat(view): AI chat对接后端接口 2025-05-07 22:49:38 +08:00
OrionMark
026c3d35a0 feat(view): AI chat对接后端接口 2025-05-07 22:14:35 +08:00
18 changed files with 2015 additions and 75 deletions

View File

@@ -58,6 +58,7 @@
"dependencies": {
"@actions/github": "^6.0.0",
"@fingerprintjs/fingerprintjs": "^4.6.0",
"@microsoft/fetch-event-source": "^2.0.1",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-autostart": "2.3.0",
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
@@ -74,6 +75,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"dompurify": "^3.2.4",
"event-source-polyfill": "^1.0.31",
"grapheme-splitter": "^1.0.4",
"hula-emojis": "^1.2.11",
"lodash-es": "^4.17.21",
@@ -97,6 +99,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@tauri-apps/cli": "2.5.0",
"@types/crypto-js": "^4.2.2",
"@types/event-source-polyfill": "^1.0.5",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.7",
"@typescript-eslint/eslint-plugin": "7.1.0",

24
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@fingerprintjs/fingerprintjs':
specifier: ^4.6.0
version: 4.6.0
'@microsoft/fetch-event-source':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/api':
specifier: 2.5.0
version: 2.5.0
@@ -62,6 +65,9 @@ importers:
dompurify:
specifier: ^3.2.4
version: 3.2.4
event-source-polyfill:
specifier: ^1.0.31
version: 1.0.31
grapheme-splitter:
specifier: ^1.0.4
version: 1.0.4
@@ -126,6 +132,9 @@ importers:
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/event-source-polyfill':
specifier: ^1.0.5
version: 1.0.5
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -1020,6 +1029,9 @@ packages:
'@lokesh.dhakar/quantize@1.4.0':
resolution: {integrity: sha512-+//cqVWKis//t0YH62EDtwaFSPG/CDtYNg4CZmzNmG2d5W17Iu3fuDAdpQXCDHUDrrU9q0veze4A7tPZXlR/mg==}
'@microsoft/fetch-event-source@2.0.1':
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
@@ -1543,6 +1555,9 @@ packages:
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/event-source-polyfill@1.0.5':
resolution: {integrity: sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -2743,6 +2758,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-source-polyfill@1.0.31:
resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@@ -5818,6 +5836,8 @@ snapshots:
'@lokesh.dhakar/quantize@1.4.0': {}
'@microsoft/fetch-event-source@2.0.1': {}
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
dependencies:
eslint-scope: 5.1.1
@@ -6255,6 +6275,8 @@ snapshots:
'@types/estree@1.0.7': {}
'@types/event-source-polyfill@1.0.5': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
@@ -7759,6 +7781,8 @@ snapshots:
esutils@2.0.3: {}
event-source-polyfill@1.0.31: {}
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {}

View File

@@ -149,7 +149,9 @@ export enum StoresEnum {
/** 缓存 */
CACHED = 'cached',
/** 配置 */
CONFIG = 'config'
CONFIG = 'config',
/** AI Chat */
AICHAT = 'aiChat'
}
/**

View File

@@ -0,0 +1,71 @@
import request from '@/services/request'
const { VITE_SERVICE_URL } = import.meta.env
const prefix = VITE_SERVICE_URL
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// AI 聊天对话 VO
export interface ChatConversationVO {
id: number // ID 编号
userId: number // 用户编号
title: string // 对话标题
pinned: boolean // 是否置顶
roleId: number // 角色编号
modelId: number // 模型编号
model: string // 模型标志
temperature: number // 温度参数
maxTokens: number // 单条回复的最大 Token 数量
maxContexts: number // 上下文的最大 Message 数量
createTime?: Date // 创建时间
// 额外字段
systemMessage?: string // 角色设定
modelName?: string // 模型名字
roleAvatar?: string // 角色头像
modelMaxTokens?: string // 模型的单条回复的最大 Token 数量
modelMaxContexts?: string // 模型的上下文的最大 Message 数量
}
// AI 聊天对话 API
export const ChatConversationApi = {
// 获得【我的】聊天对话
getChatConversationMy: async (id: number) => {
return await GET(`${prefix}/ai/chat/conversation/get-my?id=${id}`)
},
// 新增【我的】聊天对话
createChatConversationMy: async (data?: ChatConversationVO) => {
return await POST(`${prefix}/ai/chat/conversation/create-my`, data)
},
// 更新【我的】聊天对话
updateChatConversationMy: async (data: ChatConversationVO) => {
return await PUT(`${prefix}/ai/chat/conversation/update-my`, data)
},
// 删除【我的】聊天对话
deleteChatConversationMy: async (id: string) => {
return await DELETE(`${prefix}/ai/chat/conversation/delete-my`, { id })
},
// 删除【我的】所有对话,置顶除外
deleteChatConversationMyByUnpinned: async () => {
return await DELETE(`${prefix}/ai/chat/conversation/delete-by-unpinned`)
},
// 获得【我的】聊天对话列表
getChatConversationMyList: async () => {
return await GET(`${prefix}/ai/chat/conversation/my-list`)
},
// 获得对话分页
getChatConversationPage: async (params: any) => {
return await GET(`${prefix}/ai/chat/conversation/page`, params)
},
// 管理员删除消息
deleteChatConversationByAdmin: async (id: number) => {
return await DELETE(`${prefix}/ai/chat/conversation/delete-by-admin`, { id })
}
}

View File

@@ -0,0 +1,130 @@
import request from '@/services/request'
import { fetchEventSource } from '@microsoft/fetch-event-source'
const { VITE_SERVICE_URL } = import.meta.env
const prefix = VITE_SERVICE_URL
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// 聊天VO
export interface ChatMessageVO {
id: number // 编号
conversationId: number // 对话编号
type: string // 消息类型
userId: string // 用户编号
roleId: string // 角色编号
model: number // 模型标志
modelId: number // 模型编号
content: string // 聊天内容
tokens: number // 消耗 Token 数量
segmentIds?: number[] // 段落编号
segments?: {
id: number // 段落编号
content: string // 段落内容
documentId: number // 文档编号
documentName: string // 文档名称
}[]
createTime: Date // 创建时间
roleAvatar: string // 角色头像
userAvatar: string // 用户头像
}
// AI chat 聊天
export const ChatMessageApi = {
// 消息列表
getChatMessageListByConversationId: async (conversationId: number | null) => {
return await GET(`${prefix}/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`)
},
// 发送 Stream 消息
// 为什么不用 axios 呢?因为它不支持 SSE 调用
sendChatMessageStream: async (
conversationId: number,
content: string,
ctrl: AbortController,
enableContext: boolean,
onMessage: any,
onError: any,
onClose: any
) => {
const token = localStorage.getItem('TOKEN')
// 使用代理模式避免CORS问题
const apiUrl = '/api/ai/chat/message/send-stream'
return fetchEventSource(apiUrl, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
'Cache-Control': 'no-cache',
Accept: 'text/event-stream'
},
openWhenHidden: true,
body: JSON.stringify({
conversationId,
content,
useContext: enableContext
}),
onopen: async (response) => {
console.log('SSE连接响应:', {
status: response.status,
statusText: response.statusText,
contentType: response.headers.get('content-type'),
url: response.url
})
if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {
console.log('SSE连接已建立')
return // 连接成功
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
console.error('SSE连接失败状态码', response.status)
throw new Error(`连接失败: ${response.status}`)
} else {
console.warn('SSE连接异常尝试重试', response.status)
throw new Error(`临时错误: ${response.status}`)
}
},
onmessage: (event) => {
console.log('收到SSE消息:', event.data.substring(0, 100) + (event.data.length > 100 ? '...' : ''))
onMessage(event)
},
onerror: (err) => {
console.error('❌ SSE连接错误:', err)
// 检查是否是主动中止
if (ctrl.signal.aborted) {
console.log('🛑 连接被主动中止')
throw err // 不重试,直接抛出错误
}
onError(err)
// 对于网络错误,让库自动重试
// 抛出错误以触发重试机制
throw err
},
onclose: () => {
console.log('🔌 SSE连接已关闭')
onClose()
},
signal: ctrl.signal
})
},
// 删除消息
deleteChatMessage: async (id: string) => {
return await DELETE(`${prefix}/ai/chat/message/delete`, { id })
},
// 删除指定对话的消息
deleteByConversationId: async (conversationId: number) => {
return await DELETE(`${prefix}/ai/chat/message/delete-by-conversation-id`, { conversationId })
},
// 获得消息分页
getChatMessagePage: async (params: any) => {
return await GET(`${prefix}/ai/chat/message/page`, params)
},
// 管理员删除消息
deleteChatMessageByAdmin: async (id: number) => {
return await DELETE(`${prefix}/ai/chat/message/delete-by-admin`, { id })
}
}

View File

@@ -0,0 +1,91 @@
import request from '@/services/request'
import { fetch } from '@tauri-apps/plugin-http'
const { VITE_SERVICE_URL } = import.meta.env
const prefix = VITE_SERVICE_URL
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
// 获取会话内容列表
export function listChatMessage<T>(params: object) {
return GET<T>(`${prefix}/chat/message`, params)
}
// 获取用户可用模型信息
export function fetchModel<T>(params: any) {
return GET<T>(`${prefix}/gpt/model/userModel`, params)
}
// 创建对话
export function fetchChatAPI<T = any>(data: any) {
return POST<T>(`${prefix}/chat`, data)
}
// 发送消息
export function fetchChatMessageAPI<T = any>(data: any) {
return POST<T>(`${prefix}/chat/message`, data)
}
// 根据消息id获取当前内容
export function fetchChatMessageById<T = any>(messageId: string) {
return GET<T>(`${prefix}/chat/message/${messageId}`)
}
// 获取会话列表
export function listChat<T>(data: any) {
return POST<T>(`${prefix}/chat/list`, data)
}
// 删除会话列表
export function removeChat<T>(chatNumber: string) {
return POST<T>(`${prefix}/chat/del/${chatNumber}`, {})
}
// 获取文件列表
export function fileListApi<T>(data: string) {
return GET<T>(`${prefix}/file/fileList?model=${data}`, data)
}
// 删除文件
export function fileDeleteApi<T>(data: { fileIds: [any]; model: string }) {
return POST<T>(`${prefix}/file/delete`, data)
}
// 流式响应聊天
export function fetchChatAPIProcess(data: { conversationId: string; fileIds: string[] }) {
// return POST<T>(`${prefix}/chat/completions`, data, new AbortController())
const myHeaders = new Headers()
myHeaders.append('Content-Type', 'application/json')
myHeaders.append('Authorization', `Bearer ${localStorage.getItem('TOKEN')}`)
const raw = JSON.stringify({
conversationId: data.conversationId
})
const requestOptions: any = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
}
return fetch(`${prefix}/chat/completions`, requestOptions)
}
// 获取配置信息
export function fetchAgreement<T>(type: number) {
return GET<T>(`/app/api/content/agreement/${type}`)
}
// 获取助手分类
export function listAssistantType<T>() {
return GET<T>('/app/api/assistant/type')
}
// 根据分类获取助手
export function listAssistantByType<T>(data: { current: number; size: number; typeId?: number }) {
return POST<T>(`${prefix}/gpt/assistant/list`, data)
}
// 随机获取助手
export function listAssistantRandom<T>(params: any) {
return GET<T>(`${prefix}/app/api/assistant/random?`, params)
}

View File

@@ -0,0 +1,48 @@
import request from '@/services/request'
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// AI API 密钥 VO
export interface ApiKeyVO {
id: number // 编号
name: string // 名称
apiKey: string // 密钥
platform: string // 平台
url: string // 自定义 API 地址
status: number // 状态
}
// AI API 密钥 API
export const ApiKeyApi = {
// 查询 API 密钥分页
getApiKeyPage: async (params: any) => {
return await GET('/ai/api-key/page', params)
},
// 获得 API 密钥列表
getApiKeySimpleList: async () => {
return await GET('/ai/api-key/simple-list')
},
// 查询 API 密钥详情
getApiKey: async (id: number) => {
return await GET('/ai/api-key/get?id=' + id)
},
// 新增 API 密钥
createApiKey: async (data: ApiKeyVO) => {
return await POST('/ai/api-key/create', data)
},
// 修改 API 密钥
updateApiKey: async (data: ApiKeyVO) => {
return await PUT('/ai/api-key/update', data)
},
// 删除 API 密钥
deleteApiKey: async (id: number) => {
return await DELETE('/ai/api-key/delete?id=' + id)
}
}

View File

@@ -0,0 +1,86 @@
import request from '@/services/request'
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// AI 聊天角色 VO
export interface ChatRoleVO {
id: number // 角色编号
modelId: number // 模型编号
name: string // 角色名称
avatar: string // 角色头像
category: string // 角色类别
sort: number // 角色排序
description: string // 角色描述
systemMessage: string // 角色设定
welcomeMessage: string // 角色设定
publicStatus: boolean // 是否公开
status: number // 状态
knowledgeIds?: number[] // 引用的知识库 ID 列表
toolIds?: number[] // 引用的工具 ID 列表
}
// AI 聊天角色 分页请求 vo
export interface ChatRolePageReqVO {
name?: string // 角色名称
category?: string // 角色类别
publicStatus: boolean // 是否公开
pageNo: number // 是否公开
pageSize: number // 是否公开
}
// AI 聊天角色 API
export const ChatRoleApi = {
// 查询聊天角色分页
getChatRolePage: async (params: any) => {
return await GET('/ai/chat-role/page', params)
},
// 查询聊天角色详情
getChatRole: async (id: number) => {
return await GET('/ai/chat-role/get?id=' + id)
},
// 新增聊天角色
createChatRole: async (data: ChatRoleVO) => {
return await POST('/ai/chat-role/create', data)
},
// 修改聊天角色
updateChatRole: async (data: ChatRoleVO) => {
return await PUT('/ai/chat-role/update', data)
},
// 删除聊天角色
deleteChatRole: async (id: number) => {
return await DELETE('/ai/chat-role/delete', { id })
},
// ======= chat 聊天
// 获取 my role
getMyPage: async (params: ChatRolePageReqVO) => {
return await GET('/ai/chat-role/my-page', params)
},
// 获取角色分类
getCategoryList: async () => {
return await GET('/ai/chat-role/category-list')
},
// 创建角色
createMy: async (data: ChatRoleVO) => {
return await POST('/ai/chat-role/create-my', data)
},
// 更新角色
updateMy: async (data: ChatRoleVO) => {
return await PUT('/ai/chat-role/update-my', data)
},
// 删除角色 my
deleteMy: async (id: number) => {
return await DELETE('/ai/chat-role/delete-my', { id })
}
}

View File

@@ -0,0 +1,55 @@
import request from '@/services/request'
const { VITE_SERVICE_URL } = import.meta.env
const prefix = VITE_SERVICE_URL
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// AI 模型 VO
export interface ModelVO {
id: number // 编号
keyId: number // API 秘钥编号
name: string // 模型名字
model: string // 模型标识
platform: string // 模型平台
type: number // 模型类型
sort: number // 排序
status: number // 状态
temperature?: number // 温度参数
maxTokens?: number // 单条回复的最大 Token 数量
maxContexts?: number // 上下文的最大 Message 数量
}
// AI 模型 API
export const ModelApi = {
// 查询模型分页
getModelPage: async (params: any) => {
return await GET(`${prefix}/ai/model/page`, params)
},
// 获得模型列表
getModelSimpleList: async (type?: number) => {
return await GET(`${prefix}/ai/model/simple-list?type=${type}`)
},
// 查询模型详情
getModel: async (id: number) => {
return await GET(`${prefix}/ai/model/get?id=` + id)
},
// 新增模型
createModel: async (data: ModelVO) => {
return await POST(`${prefix}/ai/model/create`, data)
},
// 修改模型
updateModel: async (data: ModelVO) => {
return await PUT('/ai/model/update', data)
},
// 删除模型
deleteModel: async (id: number) => {
return await DELETE(`${prefix}/ai/model/delete?id=` + id)
}
}

View File

@@ -0,0 +1,46 @@
import request from '@/services/request'
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
// AI 工具 VO
export interface ToolVO {
id: number // 工具编号
name: string // 工具名称
description: string // 工具描述
status: number // 状态
}
// AI 工具 API
export const ToolApi = {
// 查询工具分页
getToolPage: async (params: any) => {
return await GET('/ai/tool/page', params)
},
// 查询工具详情
getTool: async (id: number) => {
return await GET('/ai/tool/get?id=' + id)
},
// 新增工具
createTool: async (data: ToolVO) => {
return await POST('/ai/tool/create', data)
},
// 修改工具
updateTool: async (data: ToolVO) => {
return await PUT('/ai/tool/update', data)
},
// 删除工具
deleteTool: async (id: number) => {
return await DELETE('/ai/tool/delete?id=' + id)
},
// 获取工具简单列表
getToolSimpleList: async () => {
return await GET('/ai/tool/simple-list')
}
}

View File

@@ -0,0 +1,60 @@
import { ref, onUnmounted } from 'vue'
import { EventSourcePolyfill } from 'event-source-polyfill'
export function useSSE(url: string) {
const message = ref<string | null>(null)
const error = ref<Error | null>(null)
const isConnected = ref<boolean>(false)
let eventSource: EventSource | null = null
const connect = () => {
if (eventSource) {
eventSource.close()
}
eventSource = new EventSourcePolyfill(url, {
withCredentials: false,
headers: {
Authorization: `Bearer ${localStorage.getItem('TOKEN')}`
}
})
eventSource.onopen = () => {
isConnected.value = true
console.log('SSE connected')
}
eventSource.onmessage = (event) => {
message.value = event.data
console.log('SSE message.value :', event.data)
// 你可以在这里处理不同类型的消息
}
eventSource.onerror = (err) => {
error.value = new Error('SSE connection error')
console.error('SSE error:', err)
eventSource?.close()
isConnected.value = false
}
}
const disconnect = () => {
if (eventSource) {
eventSource.close()
eventSource = null
isConnected.value = false
}
}
// 在组件卸载时关闭 SSE
onUnmounted(() => {
disconnect()
})
return {
message,
error,
isConnected,
connect,
disconnect
}
}

View File

@@ -12,8 +12,15 @@
import Left from './layout/Left.vue'
import Right from './layout/Right.vue'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useAiChatStore } from '@/stores/aiChat.ts'
const aiChatStore = useAiChatStore()
onMounted(async () => {
await getCurrentWebviewWindow().show()
handleInitModels()
})
const handleInitModels = () => {
aiChatStore.InitModels()
}
</script>

View File

@@ -17,7 +17,9 @@
</n-flex>
<p class="text-(12px #909090)">建立一个属于自己AI</p>
</n-flex>
<svg class="size-44px color-#13987f opacity-20"><use href="#GPT"></use></svg>
<svg class="size-44px color-#13987f opacity-20">
<use href="#GPT"></use>
</svg>
</n-flex>
<!-- 头像和插件 -->
@@ -26,12 +28,14 @@
<n-avatar bordered round :src="AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)" :size="48" />
<n-flex vertical>
<p class="text-(14px [--chat-text-color]) font-500">{{ userStore.userInfo.name }}</p>
<p class="text-(12px #909090)">剩余28天过期</p>
<p class="text-(12px #909090)">剩余{{ userStore.userInfo.num }}</p>
</n-flex>
</n-flex>
<div class="plugins">
<svg class="size-22px"><use href="#plugins"></use></svg>
<svg class="size-22px">
<use href="#plugins"></use>
</svg>
<p>插件</p>
</div>
</n-flex>
@@ -100,14 +104,18 @@
<div
@click="jump"
class="bg-[--chat-bt-color] border-(1px solid [--line-color]) color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px custom-shadow cursor-pointer">
<svg class="size-18px"><use href="#settings"></use></svg>
<svg class="size-18px">
<use href="#settings"></use>
</svg>
</div>
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/HuLaSpark/HuLa"
class="bg-[--chat-bt-color] border-(1px solid [--line-color]) color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px custom-shadow cursor-pointer">
<svg class="size-18px"><use href="#github"></use></svg>
<svg class="size-18px">
<use href="#github"></use>
</svg>
</a>
</n-flex>
@@ -115,12 +123,16 @@
<div
@click="add"
class="flex items-center justify-center gap-4px bg-[--chat-bt-color] border-(1px solid [--line-color]) select-none text-(12px [--chat-text-color]) size-fit w-80px h-32px rounded-8px custom-shadow cursor-pointer">
<svg class="size-15px pb-2px"><use href="#plus"></use></svg>
<svg class="size-15px pb-2px">
<use href="#plus"></use>
</svg>
<p>新的聊天</p>
</div>
<n-popconfirm v-model:show="showDeleteConfirm">
<template #icon>
<svg class="size-22px"><use href="#explosion"></use></svg>
<svg class="size-22px">
<use href="#explosion"></use>
</svg>
</template>
<template #action>
<n-button size="small" tertiary @click.stop="showDeleteConfirm = false">取消</n-button>
@@ -129,7 +141,9 @@
<template #trigger>
<div
class="flex items-center justify-center gap-4px bg-[--chat-bt-color] border-(1px solid [--line-color]) select-none text-(12px [--chat-text-color]) size-fit w-80px h-32px rounded-8px custom-shadow cursor-pointer">
<svg class="size-15px pb-2px"><use href="#delete"></use></svg>
<svg class="size-15px pb-2px">
<use href="#delete"></use>
</svg>
<p>全部删除</p>
</div>
</template>
@@ -146,22 +160,24 @@ import { VueDraggable } from 'vue-draggable-plus'
import router from '@/router'
import { useUserStore } from '@/stores/user.ts'
import { AvatarUtils } from '@/utils/AvatarUtils'
import { ChatConversationApi, ChatConversationVO } from '../api/chat/conversation'
const userStore = useUserStore()
const activeItem = ref(0)
const activeItem = ref('')
const scrollbar = ref<VirtualListInst>()
const inputInstRef = ref<InputInst | null>(null)
const editingItemId = ref<number | null>()
const editingItemId = ref<string | null>()
/** 原始标题 */
const originalTitle = ref('')
const showDeleteConfirm = ref(false)
const chatList = ref(
Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: `新的聊天${index + 1}`,
time: '2022-01-01 12:00:00'
}))
)
interface chatItem {
id: string
title: string
time: string
}
const chatList = ref<Array<chatItem>>([])
const menuList = ref<OPT.RightMenu[]>([
{
label: '置顶',
@@ -202,10 +218,26 @@ const specialMenuList = ref<OPT.RightMenu[]>([
}
])
/** 获取会话列表 */
const handleGetChatList = () => {
chatList.value = []
// 获取会话列表
ChatConversationApi.getChatConversationMyList().then((res: any) => {
console.log(res)
if (res.length === 0) {
add()
} else {
res.forEach((item: any) => {
chatList.value.push(item)
})
}
})
}
/** 跳转到设置 */
const jump = () => {
router.push('/chatSettings')
activeItem.value = 0
activeItem.value = ''
}
/** 选中会话 */
@@ -220,11 +252,14 @@ const handleActive = (item: any) => {
/** 添加会话 */
const add = () => {
const id = chatList.value.length + 1
chatList.value.push({ id: id, title: `新的聊天${id}`, time: '2022-01-01 12:00:00' })
// 滚动到最底部
nextTick(() => {
scrollbar.value?.scrollTo({ position: 'bottom', debounce: true })
ChatConversationApi.createChatConversationMy({} as unknown as ChatConversationVO).then((id: any) => {
console.log(id)
const length = chatList.value.length + 1
chatList.value.push({ id: id, title: `新的聊天${length}`, time: '2022-01-01 12:00:00' })
// 滚动到最底部
nextTick(() => {
scrollbar.value?.scrollTo({ position: 'bottom', debounce: true })
})
})
}
@@ -285,9 +320,17 @@ const deleteChat = (item?: any) => {
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
removeChat(item.id)
}
}
const removeChat = (id: string) => {
console.log(id)
ChatConversationApi.deleteChatConversationMy(id).then((res: any) => {
console.log(res)
})
}
/** 重命名 */
const renameChat = (item: any) => {
originalTitle.value = item.title
@@ -314,6 +357,8 @@ const handleBlur = (item: any, index: number) => {
}
onMounted(() => {
// 获取会话列表
handleGetChatList()
// /** 默认选择第一个聊天内容 */
// handleActive(chatList.value[0])
/** 刚加载的时候默认跳转到欢迎页面 */
@@ -328,32 +373,39 @@ onMounted(() => {
useMitt.on('return-chat', () => {
handleActive(chatList.value[0])
})
useMitt.on('get-chat-list', () => {
handleGetChatList()
})
})
</script>
<style scoped lang="scss">
.gpt-subtitle {
@apply bg-clip-text text-transparent bg-gradient-to-r from-#38BDF8 to-#13987F text-20px font-800;
}
.plugins {
@apply size-fit bg-[--chat-bt-color] rounded-8px custom-shadow p-[8px_14px]
flex items-center gap-10px select-none cursor-pointer
text-14px color-[--chat-text-color] border-(1px solid [--line-color]);
@apply size-fit bg-[--chat-bt-color] rounded-8px custom-shadow p-[8px_14px] flex items-center gap-10px select-none cursor-pointer text-14px color-[--chat-text-color] border-(1px solid [--line-color]);
}
.chat-item {
@apply relative bg-[--chat-bt-color] border-(1px solid [--line-color]) cursor-pointer custom-shadow rounded-8px w-full h-65px;
&:hover {
@apply bg-[--chat-hover-color];
svg {
@apply opacity-100 -translate-x-1 transition-all duration-800 ease-in-out;
}
}
}
.list-move, /* 对移动中的元素应用的过渡 */
.list-move,
/* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;

File diff suppressed because it is too large Load Diff

View File

@@ -80,8 +80,8 @@ const put = async <T>(url: string, params: any, abort?: AbortController, noRetry
return responseInterceptor(url, 'PUT', {}, params, abort, noRetry)
}
const del = async <T>(url: string, params: any, abort?: AbortController, noRetry?: boolean): Promise<T> => {
return responseInterceptor(url, 'DELETE', {}, params, abort, noRetry)
const del = async <T>(url: string, params: any, body: any, abort?: AbortController, noRetry?: boolean): Promise<T> => {
return responseInterceptor(url, 'DELETE', params, body, abort, noRetry)
}
export default {

View File

@@ -251,6 +251,8 @@ export type UserInfoType = {
avatarUpdateTime: number
/** 客户端 */
client: string
/** 剩余使用次数 */
num: number
}
export type BadgeType = {

30
src/stores/aiChat.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineStore } from 'pinia'
import { StoresEnum } from '@/enums'
import { ModelApi } from '@/plugins/robot/api/model/model'
export const useAiChatStore = defineStore(StoresEnum.AICHAT, () => {
const aiModels: any = ref([])
const InitModels = () => {
ModelApi.getModelPage({
pageNo: 1,
pageSize: -1
}).then((res: any) => {
console.log(res)
aiModels.value = []
res.list.forEach((item: any) => {
item.label = item.name
item.key = item.model
item.value = item.model
aiModels.value.push({
label: item.platform + ':' + item.name,
value: item.id,
model: item.model,
disabled: false
})
})
console.log('aiModels.value:', aiModels.value)
})
}
return { InitModels, aiModels }
})

View File

@@ -12,7 +12,7 @@ import VueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/
/**! 不需要优化前端打包(如开启gzip) */
export default defineConfig(({ mode }: ConfigEnv) => {
// 获取当前环境的配置,如何设置第三个参数则加载所有变量,而不是以VITE_前缀的变量
// 获取当前环境的配置,如何设置第三个参数则加载所有变量,而不是以"VITE_"前缀的变量
const config = loadEnv(mode, process.cwd())
return {
resolve: {
@@ -97,10 +97,38 @@ export default defineConfig(({ mode }: ConfigEnv) => {
//配置跨域
proxy: {
'/api': {
// /api 以及前置字符串会被替换为真正域名
// "/api" 以及前置字符串会被替换为真正域名
target: config.VITE_SERVICE_URL, // 请求域名
changeOrigin: true, // 是否跨域
rewrite: (path) => path.replace(/^\/api/, '')
rewrite: (path) => path.replace(/^\/api/, ''),
// 支持SSE流式连接的配置
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
// 保持连接活跃
proxyReq.setHeader('Connection', 'keep-alive')
// 禁用缓存
proxyReq.setHeader('Cache-Control', 'no-cache')
// 设置SSE相关头部
if (req.url?.includes('send-stream')) {
proxyReq.setHeader('Accept', 'text/event-stream')
}
})
proxy.on('proxyRes', (proxyRes, req) => {
// 处理SSE响应
if (req.url?.includes('send-stream')) {
// 确保SSE响应头正确
proxyRes.headers['cache-control'] = 'no-cache'
proxyRes.headers['connection'] = 'keep-alive'
// 禁用缓冲,立即传输数据
delete proxyRes.headers['content-length']
}
})
proxy.on('error', (err) => {
console.error('代理错误:', err.message)
})
},
// 增加超时时间以支持长连接
timeout: 0 // 无限超时适用于SSE
}
},
hmr: true, // 热更新