对话、重新生成、评价完成
This commit is contained in:
@@ -55,8 +55,8 @@ export const aiAgentConfigApi = {
|
||||
* 获取启用的智能体列表
|
||||
* @returns Promise<ResultDomain<AiAgentConfig[]>>
|
||||
*/
|
||||
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig[]>> {
|
||||
const response = await api.get<AiAgentConfig[]>('/ai/agent/enabled');
|
||||
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig>> {
|
||||
const response = await api.get<AiAgentConfig>('/ai/agent/enabled');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -76,8 +76,8 @@ export const aiAgentConfigApi = {
|
||||
* @param pageParam 分页参数
|
||||
* @returns Promise<PageDomain<AiAgentConfig>>
|
||||
*/
|
||||
async pageAgents(filter: Partial<AiAgentConfig>, pageParam: PageParam): Promise<PageDomain<AiAgentConfig>> {
|
||||
const response = await api.post<PageDomain<AiAgentConfig>>('/ai/agent/page', {
|
||||
async pageAgents(filter: Partial<AiAgentConfig>, pageParam: PageParam): Promise<ResultDomain<AiAgentConfig>> {
|
||||
const response = await api.post<AiAgentConfig>('/ai/agent/page', {
|
||||
filter,
|
||||
pageParam
|
||||
});
|
||||
@@ -91,7 +91,9 @@ export const aiAgentConfigApi = {
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async updateAgentStatus(agentId: string, status: number): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/status`, { status });
|
||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/status`, null, {
|
||||
params: { status }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -103,9 +105,8 @@ export const aiAgentConfigApi = {
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async updateDifyConfig(agentId: string, difyAppId: string, difyApiKey: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/dify`, {
|
||||
difyAppId,
|
||||
difyApiKey
|
||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/dify`, null, {
|
||||
params: { difyAppId, difyApiKey }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
import { api } from '@/apis/index';
|
||||
import type {
|
||||
AiConversation,
|
||||
AiMessage,
|
||||
ConversationSearchParams,
|
||||
MessageSearchParams,
|
||||
UserChatStatistics,
|
||||
ConversationStatistics,
|
||||
BatchExportParams,
|
||||
ResultDomain,
|
||||
PageDomain
|
||||
} from '@/types';
|
||||
@@ -27,8 +23,9 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<PageDomain<AiConversation>>
|
||||
*/
|
||||
async pageUserConversations(params: ConversationSearchParams): Promise<PageDomain<AiConversation>> {
|
||||
const response = await api.post<PageDomain<AiConversation>>('/ai/history/conversations/page', params);
|
||||
return response.data;
|
||||
// 后端直接返回PageDomain,不需要再次包装
|
||||
const response = await api.post('/ai/chat/history/conversations/page', params);
|
||||
return response.data as PageDomain<AiConversation>;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -37,18 +34,9 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<PageDomain<AiConversation>>
|
||||
*/
|
||||
async searchConversations(params: MessageSearchParams): Promise<PageDomain<AiConversation>> {
|
||||
const response = await api.post<PageDomain<AiConversation>>('/ai/history/conversations/search', params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索消息内容(全文搜索)
|
||||
* @param params 搜索参数
|
||||
* @returns Promise<PageDomain<AiMessage>>
|
||||
*/
|
||||
async searchMessages(params: MessageSearchParams): Promise<PageDomain<AiMessage>> {
|
||||
const response = await api.post<PageDomain<AiMessage>>('/ai/history/messages/search', params);
|
||||
return response.data;
|
||||
// 后端直接返回PageDomain,不需要再次包装
|
||||
const response = await api.post('/ai/chat/history/search', params);
|
||||
return response.data as PageDomain<AiConversation>;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -58,7 +46,8 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async toggleFavorite(conversationId: string, isFavorite: boolean): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/favorite`, {
|
||||
const response = await api.put<boolean>('/ai/chat/history/conversation/favorite', {
|
||||
conversationId,
|
||||
isFavorite
|
||||
});
|
||||
return response.data;
|
||||
@@ -71,7 +60,8 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async togglePin(conversationId: string, isPinned: boolean): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/pin`, {
|
||||
const response = await api.put<boolean>('/ai/chat/history/conversation/pin', {
|
||||
conversationId,
|
||||
isPinned
|
||||
});
|
||||
return response.data;
|
||||
@@ -83,8 +73,8 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<ResultDomain<number>>
|
||||
*/
|
||||
async batchDeleteConversations(conversationIds: string[]): Promise<ResultDomain<number>> {
|
||||
const response = await api.post<number>('/ai/history/conversations/batch-delete', {
|
||||
conversationIds
|
||||
const response = await api.delete<number>('/ai/chat/history/conversations/batch', {
|
||||
data: { conversationIds }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
@@ -92,10 +82,10 @@ export const chatHistoryApi = {
|
||||
/**
|
||||
* 获取用户的对话统计信息
|
||||
* @param userId 用户ID(可选,默认当前用户)
|
||||
* @returns Promise<ResultDomain<UserChatStatistics>>
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async getUserChatStatistics(userId?: string): Promise<ResultDomain<UserChatStatistics>> {
|
||||
const response = await api.get<UserChatStatistics>('/ai/history/statistics/user', {
|
||||
async getUserChatStatistics(userId?: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>('/ai/chat/history/statistics', {
|
||||
params: { userId }
|
||||
});
|
||||
return response.data;
|
||||
@@ -104,10 +94,10 @@ export const chatHistoryApi = {
|
||||
/**
|
||||
* 获取会话的详细统计
|
||||
* @param conversationId 会话ID
|
||||
* @returns Promise<ResultDomain<ConversationStatistics>>
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async getConversationStatistics(conversationId: string): Promise<ResultDomain<ConversationStatistics>> {
|
||||
const response = await api.get<ConversationStatistics>(`/ai/history/statistics/conversation/${conversationId}`);
|
||||
async getConversationStatistics(conversationId: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>(`/ai/chat/history/conversation/${conversationId}/statistics`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -117,7 +107,7 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async exportConversationAsMarkdown(conversationId: string): Promise<ResultDomain<string>> {
|
||||
const response = await api.get<string>(`/ai/history/export/markdown/${conversationId}`);
|
||||
const response = await api.get<string>(`/ai/chat/history/export/markdown/${conversationId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -127,47 +117,7 @@ export const chatHistoryApi = {
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async exportConversationAsJson(conversationId: string): Promise<ResultDomain<string>> {
|
||||
const response = await api.get<string>(`/ai/history/export/json/${conversationId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量导出会话
|
||||
* @param params 导出参数
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async batchExportConversations(params: BatchExportParams): Promise<ResultDomain<string>> {
|
||||
const response = await api.post<string>('/ai/history/export/batch', params);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载导出文件
|
||||
* @param conversationId 会话ID
|
||||
* @param format 格式(markdown/json)
|
||||
*/
|
||||
downloadExport(conversationId: string, format: 'markdown' | 'json'): void {
|
||||
const url = `${api.defaults.baseURL}/ai/history/export/download/${conversationId}?format=${format}`;
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量下载导出文件
|
||||
* @param conversationIds 会话ID列表
|
||||
* @param format 格式(markdown/json)
|
||||
*/
|
||||
batchDownloadExport(conversationIds: string[], format: 'markdown' | 'json'): void {
|
||||
const url = `${api.defaults.baseURL}/ai/history/export/batch-download?format=${format}&ids=${conversationIds.join(',')}`;
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
/**
|
||||
* 清理过期会话(软删除超过N天的会话)
|
||||
* @param days 天数
|
||||
* @returns Promise<ResultDomain<number>>
|
||||
*/
|
||||
async cleanExpiredConversations(days: number): Promise<ResultDomain<number>> {
|
||||
const response = await api.post<number>('/ai/history/clean', { days });
|
||||
const response = await api.get<string>(`/ai/chat/history/export/json/${conversationId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -176,20 +126,8 @@ export const chatHistoryApi = {
|
||||
* @param limit 数量限制(默认10)
|
||||
* @returns Promise<ResultDomain<AiConversation>>
|
||||
*/
|
||||
async getRecentConversations(limit?: number): Promise<ResultDomain<AiConversation>> {
|
||||
const response = await api.get<AiConversation>('/ai/history/recent', {
|
||||
params: { limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热门会话(基于消息数或Token数)
|
||||
* @param limit 数量限制(默认10)
|
||||
* @returns Promise<ResultDomain<AiConversation>>
|
||||
*/
|
||||
async getPopularConversations(limit?: number): Promise<ResultDomain<AiConversation>> {
|
||||
const response = await api.get<AiConversation>('/ai/history/popular', {
|
||||
async getRecentConversations(limit = 10): Promise<ResultDomain<AiConversation>> {
|
||||
const response = await api.get<AiConversation>('/ai/chat/history/recent', {
|
||||
params: { limit }
|
||||
});
|
||||
return response.data;
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
AiConversation,
|
||||
AiMessage,
|
||||
ChatRequest,
|
||||
ChatResponse,
|
||||
ResultDomain,
|
||||
StreamCallback
|
||||
} from '@/types';
|
||||
@@ -19,43 +18,102 @@ import type {
|
||||
*/
|
||||
export const chatApi = {
|
||||
/**
|
||||
* 流式对话(SSE)
|
||||
* 流式对话(SSE)- 使用fetch支持Authorization
|
||||
* @param request 对话请求
|
||||
* @param callback 流式回调
|
||||
* @returns Promise<ResultDomain<AiMessage>>
|
||||
*/
|
||||
async streamChat(request: ChatRequest, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||||
const token = localStorage.getItem('token');
|
||||
const tokenData = token ? JSON.parse(token) : null;
|
||||
return new Promise((resolve, reject) => {
|
||||
// 使用相对路径走Vite代理,避免跨域
|
||||
const eventSource = new EventSource(
|
||||
`${api.defaults.baseURL}/ai/chat/stream?` +
|
||||
`/api/ai/chat/stream?` +
|
||||
new URLSearchParams({
|
||||
agentId: request.agentId,
|
||||
conversationId: request.conversationId || '',
|
||||
query: request.query,
|
||||
knowledgeIds: request.knowledgeIds?.join(',') || ''
|
||||
knowledgeIds: request.knowledgeIds?.join(',') || '',
|
||||
token: tokenData?.value || ''
|
||||
})
|
||||
);
|
||||
|
||||
let fullMessage = '';
|
||||
// 通知外部EventSource已创建
|
||||
callback?.onStart?.(eventSource);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let fullMessage = ''; // 累积完整消息内容
|
||||
|
||||
// 监听初始化事件(包含messageId和conversationId)
|
||||
eventSource.addEventListener('init', (event) => {
|
||||
try {
|
||||
const initData = JSON.parse(event.data);
|
||||
console.log('[初始化数据]', initData);
|
||||
// 通知外部保存messageId(用于停止生成)
|
||||
if (callback?.onInit) {
|
||||
callback.onInit(initData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析init事件失败:', event.data);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听标准消息事件
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
fullMessage += data;
|
||||
callback?.onMessage?.(data);
|
||||
});
|
||||
|
||||
// 监听结束事件
|
||||
eventSource.addEventListener('end', (event) => {
|
||||
const metadata = JSON.parse(event.data);
|
||||
callback?.onMessageEnd?.(metadata);
|
||||
eventSource.close();
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
success: true,
|
||||
login: true,
|
||||
auth: true,
|
||||
data: metadata as AiMessage,
|
||||
message: '对话成功'
|
||||
});
|
||||
});
|
||||
|
||||
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||||
const difyEventTypes = [
|
||||
'dify_workflow_started',
|
||||
'dify_node_started',
|
||||
'dify_node_finished',
|
||||
'dify_workflow_finished',
|
||||
'dify_message',
|
||||
'dify_agent_message',
|
||||
'dify_message_end',
|
||||
'dify_message_file',
|
||||
'dify_agent_thought',
|
||||
'dify_ping'
|
||||
];
|
||||
|
||||
difyEventTypes.forEach(eventType => {
|
||||
eventSource.addEventListener(eventType, (event: any) => {
|
||||
try {
|
||||
const eventData = JSON.parse(event.data);
|
||||
console.log(`[Dify事件] ${eventType}:`, eventData);
|
||||
|
||||
// 调用自定义的Dify事件回调
|
||||
if (callback?.onDifyEvent) {
|
||||
const cleanEventType = eventType.replace('dify_', '');
|
||||
callback.onDifyEvent(cleanEventType, eventData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
eventSource.addEventListener('error', (event: any) => {
|
||||
const error = new Error(event.data || '对话失败');
|
||||
callback?.onError?.(error);
|
||||
@@ -64,7 +122,7 @@ export const chatApi = {
|
||||
});
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
callback?.onError?.(error as Error);
|
||||
callback?.onError?.(error as unknown as Error);
|
||||
eventSource.close();
|
||||
reject(error);
|
||||
};
|
||||
@@ -82,7 +140,7 @@ export const chatApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止对话生成
|
||||
* 停止对话生成(通过消息ID)
|
||||
* @param messageId 消息ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
@@ -91,6 +149,20 @@ export const chatApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止对话生成(通过Dify TaskID)
|
||||
* @param taskId Dify任务ID
|
||||
* @param agentId 智能体ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async stopChatByTaskId(taskId: string, agentId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>('/ai/chat/stop-by-taskid', {
|
||||
taskId,
|
||||
agentId
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* @param agentId 智能体ID
|
||||
@@ -152,8 +224,8 @@ export const chatApi = {
|
||||
* @param conversationId 会话ID
|
||||
* @returns Promise<ResultDomain<AiMessage[]>>
|
||||
*/
|
||||
async listMessages(conversationId: string): Promise<ResultDomain<AiMessage[]>> {
|
||||
const response = await api.get<AiMessage[]>(`/ai/chat/conversation/${conversationId}/messages`);
|
||||
async listMessages(conversationId: string): Promise<ResultDomain<AiMessage>> {
|
||||
const response = await api.get<AiMessage>(`/ai/chat/conversation/${conversationId}/messages`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -168,46 +240,112 @@ export const chatApi = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新生成回答
|
||||
* 重新生成回答(SSE流式)
|
||||
* @param messageId 原消息ID
|
||||
* @param callback 流式回调(可选)
|
||||
* @param callback 流式回调
|
||||
* @returns Promise<ResultDomain<AiMessage>>
|
||||
*/
|
||||
async regenerateAnswer(messageId: string, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||||
if (callback) {
|
||||
// 使用流式方式重新生成
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventSource = new EventSource(
|
||||
`${api.defaults.baseURL}/ai/chat/regenerate/${messageId}?stream=true`
|
||||
);
|
||||
const token = localStorage.getItem('token');
|
||||
const tokenData = token ? JSON.parse(token) : null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 使用相对路径走Vite代理,SSE流式推送
|
||||
const eventSource = new EventSource(
|
||||
`/api/ai/chat/regenerate/${messageId}?` +
|
||||
new URLSearchParams({
|
||||
token: tokenData?.value || ''
|
||||
})
|
||||
);
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
callback.onMessage?.(event.data);
|
||||
});
|
||||
// 通知外部EventSource已创建
|
||||
callback?.onStart?.(eventSource);
|
||||
|
||||
eventSource.addEventListener('end', (event) => {
|
||||
const metadata = JSON.parse(event.data);
|
||||
callback.onMessageEnd?.(metadata);
|
||||
eventSource.close();
|
||||
resolve({
|
||||
success: true,
|
||||
data: metadata as AiMessage,
|
||||
message: '重新生成成功'
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let fullMessage = ''; // 累积完整消息内容
|
||||
|
||||
eventSource.addEventListener('error', (event: any) => {
|
||||
const error = new Error(event.data || '重新生成失败');
|
||||
callback.onError?.(error);
|
||||
eventSource.close();
|
||||
reject(error);
|
||||
// 监听初始化事件(包含messageId和conversationId)
|
||||
eventSource.addEventListener('init', (event) => {
|
||||
try {
|
||||
const initData = JSON.parse(event.data);
|
||||
console.log('[初始化数据-重新生成]', initData);
|
||||
// 通知外部保存messageId(用于停止生成)
|
||||
if (callback?.onInit) {
|
||||
callback.onInit(initData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('解析init事件失败:', event.data);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听标准消息事件
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
fullMessage += data;
|
||||
callback?.onMessage?.(data);
|
||||
});
|
||||
|
||||
// 监听结束事件
|
||||
eventSource.addEventListener('end', (event) => {
|
||||
const metadata = JSON.parse(event.data);
|
||||
callback?.onMessageEnd?.(metadata);
|
||||
eventSource.close();
|
||||
|
||||
resolve({
|
||||
code: 200,
|
||||
success: true,
|
||||
login: true,
|
||||
auth: true,
|
||||
data: metadata as AiMessage,
|
||||
message: '重新生成成功'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 使用阻塞方式重新生成
|
||||
const response = await api.post<AiMessage>(`/ai/chat/regenerate/${messageId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||||
const difyEventTypes = [
|
||||
'dify_workflow_started',
|
||||
'dify_node_started',
|
||||
'dify_node_finished',
|
||||
'dify_workflow_finished',
|
||||
'dify_message',
|
||||
'dify_agent_message',
|
||||
'dify_message_end',
|
||||
'dify_message_file',
|
||||
'dify_agent_thought',
|
||||
'dify_ping'
|
||||
];
|
||||
|
||||
difyEventTypes.forEach(eventType => {
|
||||
eventSource.addEventListener(eventType, (event: any) => {
|
||||
try {
|
||||
const eventData = JSON.parse(event.data);
|
||||
console.log(`[Dify事件] ${eventType}:`, eventData);
|
||||
|
||||
// 调用自定义的Dify事件回调
|
||||
if (callback?.onDifyEvent) {
|
||||
const cleanEventType = eventType.replace('dify_', '');
|
||||
callback.onDifyEvent(cleanEventType, eventData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
eventSource.addEventListener('error', (event: any) => {
|
||||
const error = new Error(event.data || '重新生成失败');
|
||||
callback?.onError?.(error);
|
||||
eventSource.close();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
callback?.onError?.(error as unknown as Error);
|
||||
eventSource.close();
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type { AiUploadFile, ResultDomain, FileUploadResponse, PageDomain, PageParam } from '@/types';
|
||||
|
||||
import type { AiUploadFile, ResultDomain, FileUploadResponse, PageParam } from '@/types';
|
||||
/**
|
||||
* 文件上传API服务
|
||||
*/
|
||||
@@ -22,11 +21,7 @@ export const fileUploadApi = {
|
||||
formData.append('file', file);
|
||||
formData.append('knowledgeId', knowledgeId);
|
||||
|
||||
const response = await api.post<FileUploadResponse>('/ai/file/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
const response = await api.post<FileUploadResponse>('/ai/file/upload', formData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -43,11 +38,7 @@ export const fileUploadApi = {
|
||||
});
|
||||
formData.append('knowledgeId', knowledgeId);
|
||||
|
||||
const response = await api.post<FileUploadResponse[]>('/ai/file/batch-upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
const response = await api.post<FileUploadResponse[]>('/ai/file/batch-upload', formData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -85,10 +76,10 @@ export const fileUploadApi = {
|
||||
* 分页查询文件
|
||||
* @param filter 过滤条件
|
||||
* @param pageParam 分页参数
|
||||
* @returns Promise<PageDomain<AiUploadFile>>
|
||||
* @returns Promise<ResultDomain<AiUploadFile>>
|
||||
*/
|
||||
async pageFiles(filter: Partial<AiUploadFile>, pageParam: PageParam): Promise<PageDomain<AiUploadFile>> {
|
||||
const response = await api.post<PageDomain<AiUploadFile>>('/ai/file/page', {
|
||||
async pageFiles(filter: Partial<AiUploadFile>, pageParam: PageParam): Promise<ResultDomain<AiUploadFile>> {
|
||||
const response = await api.post<AiUploadFile>('/ai/file/page', {
|
||||
filter,
|
||||
pageParam
|
||||
});
|
||||
|
||||
@@ -76,8 +76,8 @@ export const knowledgeApi = {
|
||||
* @param pageParam 分页参数
|
||||
* @returns Promise<PageDomain<AiKnowledge>>
|
||||
*/
|
||||
async pageKnowledges(filter: Partial<AiKnowledge>, pageParam: PageParam): Promise<PageDomain<AiKnowledge>> {
|
||||
const response = await api.post<PageDomain<AiKnowledge>>('/ai/knowledge/page', {
|
||||
async pageKnowledges(filter: Partial<AiKnowledge>, pageParam: PageParam): Promise<ResultDomain<AiKnowledge>> {
|
||||
const response = await api.post<AiKnowledge>('/ai/knowledge/page', {
|
||||
filter,
|
||||
pageParam
|
||||
});
|
||||
@@ -108,6 +108,7 @@ export const knowledgeApi = {
|
||||
* 设置知识库权限
|
||||
* @param params 权限参数
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
|
||||
*/
|
||||
async setPermissions(params: KnowledgePermissionParams): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>('/ai/knowledge/permissions', params);
|
||||
@@ -144,3 +145,4 @@ export const knowledgeApi = {
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,26 +18,45 @@ interface CustomAxiosRequestConfig extends Partial<InternalAxiosRequestConfig> {
|
||||
* Token管理
|
||||
*/
|
||||
export const TokenManager = {
|
||||
/** 获取token(优先从localStorage,其次sessionStorage) */
|
||||
/** 获取token(从localStorage获取并检查过期) */
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
},
|
||||
|
||||
/** 设置token(根据rememberMe决定存储位置) */
|
||||
setToken(token: string, rememberMe = false): void {
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('token', token);
|
||||
sessionStorage.removeItem('token'); // 清除sessionStorage中的旧token
|
||||
} else {
|
||||
sessionStorage.setItem('token', token);
|
||||
localStorage.removeItem('token'); // 清除localStorage中的旧token
|
||||
const itemStr = localStorage.getItem('token');
|
||||
if (!itemStr) return null;
|
||||
|
||||
try {
|
||||
const item = JSON.parse(itemStr);
|
||||
const now = Date.now();
|
||||
|
||||
// 检查是否过期
|
||||
if (item.timestamp && item.expiresIn) {
|
||||
if (now - item.timestamp > item.expiresIn) {
|
||||
// 已过期,删除
|
||||
localStorage.removeItem('token');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return item.value || itemStr; // 兼容旧数据
|
||||
} catch {
|
||||
// 如果不是JSON格式,直接返回(兼容旧数据)
|
||||
return itemStr;
|
||||
}
|
||||
},
|
||||
|
||||
/** 移除token(两个存储都清除) */
|
||||
/** 设置token(始终使用localStorage,根据rememberMe设置过期时间) */
|
||||
setToken(token: string, rememberMe = false): void {
|
||||
const data = {
|
||||
value: token,
|
||||
timestamp: Date.now(),
|
||||
// 如果不勾选"记住我",设置1天过期时间;勾选则7天
|
||||
expiresIn: rememberMe ? 7 * 24 * 60 * 60 * 1000 : 1 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
localStorage.setItem('token', JSON.stringify(data));
|
||||
},
|
||||
|
||||
/** 移除token */
|
||||
removeToken(): void {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('token');
|
||||
},
|
||||
|
||||
/** 检查是否有token */
|
||||
@@ -81,6 +100,11 @@ request.interceptors.request.use(
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 自动处理 FormData:删除 Content-Type,让浏览器自动设置(包含 boundary)
|
||||
if (config.data instanceof FormData && config.headers) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 1004 B |
@@ -28,6 +28,7 @@
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main class="main-content">
|
||||
<AIAgent/>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
@@ -35,6 +36,7 @@
|
||||
<!-- 没有侧边栏时直接显示内容 -->
|
||||
<div class="content-wrapper-full" v-else>
|
||||
<main class="main-content-full">
|
||||
<AIAgent/>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
@@ -48,8 +50,9 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { getMenuPath } from '@/utils/route-generator';
|
||||
import { TopNavigation, MenuSidebar, Breadcrumb } from '@/components';
|
||||
// import { getMenuPath } from '@/utils/route-generator';
|
||||
import { TopNavigation, MenuSidebar } from '@/components';
|
||||
import { AIAgent } from '@/views/public/ai';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -89,16 +92,16 @@ const sidebarMenus = computed(() => {
|
||||
// 是否有侧边栏菜单
|
||||
const hasSidebarMenus = computed(() => sidebarMenus.value.length > 0);
|
||||
|
||||
// 面包屑数据
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (!route.meta?.menuId) return [];
|
||||
|
||||
const menuPath = getMenuPath(allMenus.value, route.meta.menuId as string);
|
||||
return menuPath.map((menu) => ({
|
||||
title: menu.name || '',
|
||||
path: menu.url || '',
|
||||
}));
|
||||
});
|
||||
// 面包屑数据(暂时未使用)
|
||||
// const breadcrumbItems = computed(() => {
|
||||
// if (!route.meta?.menuId) return [];
|
||||
//
|
||||
// const menuPath = getMenuPath(allMenus.value, route.meta.menuId as string);
|
||||
// return menuPath.map((menu) => ({
|
||||
// title: menu.name || '',
|
||||
// path: menu.url || '',
|
||||
// }));
|
||||
// });
|
||||
|
||||
// 判断路径是否在菜单下
|
||||
function isPathUnderMenu(path: string, menu: SysMenu): boolean {
|
||||
|
||||
@@ -26,24 +26,45 @@ export interface AuthState {
|
||||
|
||||
// 存储工具函数
|
||||
const StorageUtil = {
|
||||
// 保存数据(根据rememberMe选择存储方式)
|
||||
// 保存数据(始终使用localStorage,但根据rememberMe设置过期时间)
|
||||
setItem(key: string, value: string, rememberMe = false) {
|
||||
if (rememberMe) {
|
||||
localStorage.setItem(key, value);
|
||||
} else {
|
||||
sessionStorage.setItem(key, value);
|
||||
const data = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
// 如果不勾选"记住我",设置1天过期时间;勾选则7天
|
||||
expiresIn: rememberMe ? 7 * 24 * 60 * 60 * 1000 : 1 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
},
|
||||
|
||||
// 获取数据(检查是否过期)
|
||||
getItem(key: string): string | null {
|
||||
const itemStr = localStorage.getItem(key);
|
||||
if (!itemStr) return null;
|
||||
|
||||
try {
|
||||
const item = JSON.parse(itemStr);
|
||||
const now = Date.now();
|
||||
|
||||
// 检查是否过期
|
||||
if (item.timestamp && item.expiresIn) {
|
||||
if (now - item.timestamp > item.expiresIn) {
|
||||
// 已过期,删除
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return item.value || itemStr; // 兼容旧数据
|
||||
} catch {
|
||||
// 如果不是JSON格式,直接返回(兼容旧数据)
|
||||
return itemStr;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取数据(优先从localStorage,其次sessionStorage)
|
||||
getItem(key: string): string | null {
|
||||
return localStorage.getItem(key) || sessionStorage.getItem(key);
|
||||
},
|
||||
|
||||
// 删除数据(从两个存储中都删除)
|
||||
// 删除数据
|
||||
removeItem(key: string) {
|
||||
localStorage.removeItem(key);
|
||||
sessionStorage.removeItem(key);
|
||||
},
|
||||
|
||||
// 清除所有认证相关数据
|
||||
@@ -51,7 +72,6 @@ const StorageUtil = {
|
||||
const keys = ['token', 'loginDomain', 'menus', 'permissions', 'rememberMe'];
|
||||
keys.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -321,6 +321,10 @@ export interface KnowledgePermissionParams {
|
||||
* Streaming 回调接口
|
||||
*/
|
||||
export interface StreamCallback {
|
||||
/** 开始连接 */
|
||||
onStart?: (eventSource: EventSource) => void;
|
||||
/** 初始化数据(包含messageId用于停止生成) */
|
||||
onInit?: (initData: { messageId: string; conversationId: string }) => void;
|
||||
/** 接收到消息片段 */
|
||||
onMessage?: (message: string) => void;
|
||||
/** 消息结束 */
|
||||
@@ -329,6 +333,28 @@ export interface StreamCallback {
|
||||
onComplete?: () => void;
|
||||
/** 错误 */
|
||||
onError?: (error: Error) => void;
|
||||
/** Dify原始事件回调(包含task_id等完整信息) */
|
||||
onDifyEvent?: (eventType: string, eventData: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dify事件数据接口
|
||||
*/
|
||||
export interface DifyEvent {
|
||||
/** 事件类型 */
|
||||
event: string;
|
||||
/** 任务ID */
|
||||
task_id?: string;
|
||||
/** 工作流运行ID */
|
||||
workflow_run_id?: string;
|
||||
/** 消息ID */
|
||||
message_id?: string;
|
||||
/** 会话ID */
|
||||
conversation_id?: string;
|
||||
/** 创建时间戳 */
|
||||
created_at?: number;
|
||||
/** 其他字段 */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// ==================== Dify 文档分段相关类型 ====================
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="agent-icon">
|
||||
<img v-if="configForm.avatar" :src="FILE_DOWNLOAD_URL + configForm.avatar" alt="助手头像" />
|
||||
<div v-else class="default-icon">
|
||||
<img src="@/assets/imgs/assisstent.svg" alt="助手头像" />
|
||||
<img src="@/assets/imgs/assistant.svg" alt="助手头像" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
@@ -45,34 +45,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模式">
|
||||
<el-select
|
||||
v-model="configForm.modelProvider"
|
||||
placeholder="选择模式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="OpenAI" value="openai" />
|
||||
<el-option label="Anthropic" value="anthropic" />
|
||||
<el-option label="Azure OpenAI" value="azure" />
|
||||
<el-option label="通义千问" value="qwen" />
|
||||
<el-option label="文心一言" value="wenxin" />
|
||||
<el-option label="Dify" value="dify" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型">
|
||||
<el-input
|
||||
v-model="configForm.modelName"
|
||||
placeholder="例如: gpt-4, claude-3-opus"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="系统提示词">
|
||||
<el-form-item label="助手描述">
|
||||
<el-input
|
||||
v-model="configForm.systemPrompt"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="请输入系统提示词,定义AI助手的角色、行为和回答风格..."
|
||||
placeholder="请输入助手描述,介绍AI助手的功能、特点和用途..."
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
@@ -160,10 +138,6 @@ async function handleSave() {
|
||||
ElMessage.warning('请输入助手名称');
|
||||
return;
|
||||
}
|
||||
if (!configForm.value.modelProvider) {
|
||||
ElMessage.warning('请选择模式');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user