Compare commits
23 Commits
master
...
feat/AICha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb6cc79796 | ||
|
|
68cf5bddca | ||
|
|
d8a24fc106 | ||
|
|
1a3868b2ea | ||
|
|
d1e949aa87 | ||
|
|
a24f3d0511 | ||
|
|
277d598a69 | ||
|
|
09337131b5 | ||
|
|
06384bb204 | ||
|
|
86335c917d | ||
|
|
ea738270a8 | ||
|
|
78565bacc8 | ||
|
|
b50c386432 | ||
|
|
43d85b10cc | ||
|
|
f17f07cc2b | ||
|
|
c078a0be1d | ||
|
|
72740e7d2d | ||
|
|
0bc93f14da | ||
|
|
9c33073f75 | ||
|
|
3c608a8875 | ||
|
|
5cfbac209d | ||
|
|
706021871a | ||
|
|
026c3d35a0 |
@@ -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
24
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -149,7 +149,9 @@ export enum StoresEnum {
|
||||
/** 缓存 */
|
||||
CACHED = 'cached',
|
||||
/** 配置 */
|
||||
CONFIG = 'config'
|
||||
CONFIG = 'config',
|
||||
/** AI Chat */
|
||||
AICHAT = 'aiChat'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
71
src/plugins/robot/api/chat/conversation/index.ts
Normal file
71
src/plugins/robot/api/chat/conversation/index.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
130
src/plugins/robot/api/chat/message/index.ts
Normal file
130
src/plugins/robot/api/chat/message/index.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
91
src/plugins/robot/api/index.ts
Normal file
91
src/plugins/robot/api/index.ts
Normal 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)
|
||||
}
|
||||
48
src/plugins/robot/api/model/apiKey/index.ts
Normal file
48
src/plugins/robot/api/model/apiKey/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
86
src/plugins/robot/api/model/chatRole/index.ts
Normal file
86
src/plugins/robot/api/model/chatRole/index.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
55
src/plugins/robot/api/model/model/index.ts
Normal file
55
src/plugins/robot/api/model/model/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
46
src/plugins/robot/api/model/tool/index.ts
Normal file
46
src/plugins/robot/api/model/tool/index.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
60
src/plugins/robot/hook/useSSE.ts
Normal file
60
src/plugins/robot/hook/useSSE.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -251,6 +251,8 @@ export type UserInfoType = {
|
||||
avatarUpdateTime: number
|
||||
/** 客户端 */
|
||||
client: string
|
||||
/** 剩余使用次数 */
|
||||
num: number
|
||||
}
|
||||
|
||||
export type BadgeType = {
|
||||
|
||||
30
src/stores/aiChat.ts
Normal file
30
src/stores/aiChat.ts
Normal 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 }
|
||||
})
|
||||
@@ -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, // 热更新
|
||||
|
||||
Reference in New Issue
Block a user