对话、重新生成、评价完成

This commit is contained in:
2025-11-05 16:55:58 +08:00
parent 8850a06fea
commit d9d62e22de
34 changed files with 1658 additions and 965 deletions

View File

@@ -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;
},

View File

@@ -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;

View File

@@ -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);
};
});
},
/**

View File

@@ -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
});

View File

@@ -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;
}
};

View File

@@ -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) => {

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -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 {

View File

@@ -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);
});
}
};

View File

@@ -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 文档分段相关类型 ====================

View File

@@ -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