This commit is contained in:
2025-11-04 18:49:37 +08:00
parent b95fff224b
commit 8850a06fea
103 changed files with 15337 additions and 771 deletions

View File

@@ -1,32 +1,125 @@
/**
* @description 智能体配置相关API
* @author yslg
* @since 2025-10-15
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type { AiAgentConfig, ResultDomain } from '@/types';
import type { AiAgentConfig, ResultDomain, PageDomain, PageParam } from '@/types';
/**
* 智能体配置API服务
*/
export const aiAgentConfigApi = {
/**
* 获取智能体配置
* 创建智能体
* @param agentConfig 智能体配置
* @returns Promise<ResultDomain<AiAgentConfig>>
*/
async getAgentConfig(): Promise<ResultDomain<AiAgentConfig>> {
const response = await api.get<AiAgentConfig>('/ai/agent-config');
async createAgent(agentConfig: AiAgentConfig): Promise<ResultDomain<AiAgentConfig>> {
const response = await api.post<AiAgentConfig>('/ai/agent', agentConfig);
return response.data;
},
/**
* 更新智能体配置
* @param config 配置数据
* 更新智能体
* @param agentConfig 智能体配置
* @returns Promise<ResultDomain<AiAgentConfig>>
*/
async updateAgentConfig(config: AiAgentConfig): Promise<ResultDomain<AiAgentConfig>> {
const response = await api.put<AiAgentConfig>('/ai/agent-config', config);
async updateAgent(agentConfig: AiAgentConfig): Promise<ResultDomain<AiAgentConfig>> {
const response = await api.put<AiAgentConfig>('/ai/agent', agentConfig);
return response.data;
},
/**
* 删除智能体
* @param agentId 智能体ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteAgent(agentId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/agent/${agentId}`);
return response.data;
},
/**
* 获取智能体详情
* @param agentId 智能体ID
* @returns Promise<ResultDomain<AiAgentConfig>>
*/
async getAgentById(agentId: string): Promise<ResultDomain<AiAgentConfig>> {
const response = await api.get<AiAgentConfig>(`/ai/agent/${agentId}`);
return response.data;
},
/**
* 获取启用的智能体列表
* @returns Promise<ResultDomain<AiAgentConfig[]>>
*/
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig[]>> {
const response = await api.get<AiAgentConfig[]>('/ai/agent/enabled');
return response.data;
},
/**
* 获取智能体列表(支持过滤)
* @param filter 过滤条件
* @returns Promise<ResultDomain<AiAgentConfig[]>>
*/
async listAgents(filter?: Partial<AiAgentConfig>): Promise<ResultDomain<AiAgentConfig[]>> {
const response = await api.post<AiAgentConfig[]>('/ai/agent/list', filter || {});
return response.data;
},
/**
* 分页查询智能体
* @param filter 过滤条件
* @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', {
filter,
pageParam
});
return response.data;
},
/**
* 更新智能体状态
* @param agentId 智能体ID
* @param status 状态0禁用 1启用
* @returns Promise<ResultDomain<boolean>>
*/
async updateAgentStatus(agentId: string, status: number): Promise<ResultDomain<boolean>> {
const response = await api.put<boolean>(`/ai/agent/${agentId}/status`, { status });
return response.data;
},
/**
* 更新Dify配置
* @param agentId 智能体ID
* @param difyAppId Dify应用ID
* @param difyApiKey Dify API Key
* @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
});
return response.data;
},
/**
* 检查智能体名称是否存在
* @param name 名称
* @param excludeId 排除的ID用于更新时
* @returns Promise<ResultDomain<boolean>>
*/
async checkNameExists(name: string, excludeId?: string): Promise<ResultDomain<boolean>> {
const response = await api.get<boolean>('/ai/agent/check-name', {
params: { name, excludeId }
});
return response.data;
}
};

View File

@@ -0,0 +1,198 @@
/**
* @description AI对话历史相关API
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type {
AiConversation,
AiMessage,
ConversationSearchParams,
MessageSearchParams,
UserChatStatistics,
ConversationStatistics,
BatchExportParams,
ResultDomain,
PageDomain
} from '@/types';
/**
* 对话历史API服务
*/
export const chatHistoryApi = {
/**
* 分页查询用户的会话列表
* @param params 搜索参数
* @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;
},
/**
* 搜索会话(全文搜索标题和摘要)
* @param params 搜索参数
* @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;
},
/**
* 收藏/取消收藏会话
* @param conversationId 会话ID
* @param isFavorite 是否收藏
* @returns Promise<ResultDomain<boolean>>
*/
async toggleFavorite(conversationId: string, isFavorite: boolean): Promise<ResultDomain<boolean>> {
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/favorite`, {
isFavorite
});
return response.data;
},
/**
* 置顶/取消置顶会话
* @param conversationId 会话ID
* @param isPinned 是否置顶
* @returns Promise<ResultDomain<boolean>>
*/
async togglePin(conversationId: string, isPinned: boolean): Promise<ResultDomain<boolean>> {
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/pin`, {
isPinned
});
return response.data;
},
/**
* 批量删除会话
* @param conversationIds 会话ID列表
* @returns Promise<ResultDomain<number>>
*/
async batchDeleteConversations(conversationIds: string[]): Promise<ResultDomain<number>> {
const response = await api.post<number>('/ai/history/conversations/batch-delete', {
conversationIds
});
return response.data;
},
/**
* 获取用户的对话统计信息
* @param userId 用户ID可选默认当前用户
* @returns Promise<ResultDomain<UserChatStatistics>>
*/
async getUserChatStatistics(userId?: string): Promise<ResultDomain<UserChatStatistics>> {
const response = await api.get<UserChatStatistics>('/ai/history/statistics/user', {
params: { userId }
});
return response.data;
},
/**
* 获取会话的详细统计
* @param conversationId 会话ID
* @returns Promise<ResultDomain<ConversationStatistics>>
*/
async getConversationStatistics(conversationId: string): Promise<ResultDomain<ConversationStatistics>> {
const response = await api.get<ConversationStatistics>(`/ai/history/statistics/conversation/${conversationId}`);
return response.data;
},
/**
* 导出会话记录Markdown格式
* @param conversationId 会话ID
* @returns Promise<ResultDomain<string>>
*/
async exportConversationAsMarkdown(conversationId: string): Promise<ResultDomain<string>> {
const response = await api.get<string>(`/ai/history/export/markdown/${conversationId}`);
return response.data;
},
/**
* 导出会话记录JSON格式
* @param conversationId 会话ID
* @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 });
return response.data;
},
/**
* 获取用户最近的对话
* @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', {
params: { limit }
});
return response.data;
}
};

View File

@@ -0,0 +1,238 @@
/**
* @description AI对话相关API
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type {
AiConversation,
AiMessage,
ChatRequest,
ChatResponse,
ResultDomain,
StreamCallback
} from '@/types';
/**
* AI对话API服务
*/
export const chatApi = {
/**
* 流式对话SSE
* @param request 对话请求
* @param callback 流式回调
* @returns Promise<ResultDomain<AiMessage>>
*/
async streamChat(request: ChatRequest, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
return new Promise((resolve, reject) => {
const eventSource = new EventSource(
`${api.defaults.baseURL}/ai/chat/stream?` +
new URLSearchParams({
agentId: request.agentId,
conversationId: request.conversationId || '',
query: request.query,
knowledgeIds: request.knowledgeIds?.join(',') || ''
})
);
let fullMessage = '';
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({
success: true,
data: metadata as AiMessage,
message: '对话成功'
});
});
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 Error);
eventSource.close();
reject(error);
};
});
},
/**
* 阻塞式对话(非流式)
* @param request 对话请求
* @returns Promise<ResultDomain<AiMessage>>
*/
async blockingChat(request: ChatRequest): Promise<ResultDomain<AiMessage>> {
const response = await api.post<AiMessage>('/ai/chat/blocking', request);
return response.data;
},
/**
* 停止对话生成
* @param messageId 消息ID
* @returns Promise<ResultDomain<boolean>>
*/
async stopChat(messageId: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`/ai/chat/stop/${messageId}`);
return response.data;
},
/**
* 创建新会话
* @param agentId 智能体ID
* @param title 会话标题(可选)
* @returns Promise<ResultDomain<AiConversation>>
*/
async createConversation(agentId: string, title?: string): Promise<ResultDomain<AiConversation>> {
const response = await api.post<AiConversation>('/ai/chat/conversation', {
agentId,
title
});
return response.data;
},
/**
* 获取会话信息
* @param conversationId 会话ID
* @returns Promise<ResultDomain<AiConversation>>
*/
async getConversation(conversationId: string): Promise<ResultDomain<AiConversation>> {
const response = await api.get<AiConversation>(`/ai/chat/conversation/${conversationId}`);
return response.data;
},
/**
* 更新会话
* @param conversation 会话信息
* @returns Promise<ResultDomain<AiConversation>>
*/
async updateConversation(conversation: AiConversation): Promise<ResultDomain<AiConversation>> {
const response = await api.put<AiConversation>('/ai/chat/conversation', conversation);
return response.data;
},
/**
* 删除会话
* @param conversationId 会话ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteConversation(conversationId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/chat/conversation/${conversationId}`);
return response.data;
},
/**
* 获取用户的会话列表
* @param agentId 智能体ID可选
* @returns Promise<ResultDomain<AiConversation[]>>
*/
async listUserConversations(agentId?: string): Promise<ResultDomain<AiConversation[]>> {
const response = await api.get<AiConversation[]>('/ai/chat/conversations', {
params: { agentId }
});
return response.data;
},
/**
* 获取会话的消息列表
* @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`);
return response.data;
},
/**
* 获取单条消息
* @param messageId 消息ID
* @returns Promise<ResultDomain<AiMessage>>
*/
async getMessage(messageId: string): Promise<ResultDomain<AiMessage>> {
const response = await api.get<AiMessage>(`/ai/chat/message/${messageId}`);
return response.data;
},
/**
* 重新生成回答
* @param messageId 原消息ID
* @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`
);
eventSource.addEventListener('message', (event) => {
callback.onMessage?.(event.data);
});
eventSource.addEventListener('end', (event) => {
const metadata = JSON.parse(event.data);
callback.onMessageEnd?.(metadata);
eventSource.close();
resolve({
success: true,
data: metadata as AiMessage,
message: '重新生成成功'
});
});
eventSource.addEventListener('error', (event: any) => {
const error = new Error(event.data || '重新生成失败');
callback.onError?.(error);
eventSource.close();
reject(error);
});
});
} else {
// 使用阻塞方式重新生成
const response = await api.post<AiMessage>(`/ai/chat/regenerate/${messageId}`);
return response.data;
}
},
/**
* 异步生成会话摘要
* @param conversationId 会话ID
* @returns Promise<ResultDomain<boolean>>
*/
async generateSummary(conversationId: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`/ai/chat/conversation/${conversationId}/summary`);
return response.data;
},
/**
* 评价消息
* @param messageId 消息ID
* @param rating 评分1=好评,-1=差评0=取消评价)
* @param feedback 反馈内容(可选)
* @returns Promise<ResultDomain<boolean>>
*/
async rateMessage(messageId: string, rating: number, feedback?: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`/ai/chat/message/${messageId}/rate`, {
rating,
feedback
});
return response.data;
}
};

View File

@@ -1,53 +0,0 @@
/**
* @description 对话相关API
* @author yslg
* @since 2025-10-15
*/
import { api } from '@/apis/index';
import type { AiConversation, ResultDomain } from '@/types';
/**
* 对话API服务
*/
export const conversationApi = {
/**
* 获取用户对话列表
* @param userID 用户ID
* @returns Promise<ResultDomain<AiConversation>>
*/
async getConversationList(userID: string): Promise<ResultDomain<AiConversation>> {
const response = await api.get<AiConversation>('/ai/conversation/list', { userID });
return response.data;
},
/**
* 创建对话
* @param conversation 对话数据
* @returns Promise<ResultDomain<AiConversation>>
*/
async createConversation(conversation: AiConversation): Promise<ResultDomain<AiConversation>> {
const response = await api.post<AiConversation>('/ai/conversation/create', conversation);
return response.data;
},
/**
* 删除对话
* @param conversationID 对话ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteConversation(conversationID: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/conversation/${conversationID}`);
return response.data;
},
/**
* 清空对话记录
* @param conversationID 对话ID
* @returns Promise<ResultDomain<boolean>>
*/
async clearConversation(conversationID: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`/ai/conversation/${conversationID}/clear`);
return response.data;
}
};

View File

@@ -0,0 +1,168 @@
/**
* @description Dify 文档分段管理 API
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type { ResultDomain } from '@/types';
import type {
DifySegmentListResponse,
DifyChildChunkListResponse,
DifyChildChunkResponse,
SegmentUpdateRequest,
SegmentCreateRequest
} from '@/types/ai';
/**
* 文档分段管理 API
*/
export const documentSegmentApi = {
/**
* 获取文档的所有分段(父级)
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @returns Promise<ResultDomain<DifySegmentListResponse>>
*/
async getDocumentSegments(
datasetId: string,
documentId: string
): Promise<ResultDomain<DifySegmentListResponse>> {
const response = await api.get<DifySegmentListResponse>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments`
);
return response.data;
},
/**
* 获取分段的子块列表
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @returns Promise<ResultDomain<DifyChildChunkListResponse>>
*/
async getChildChunks(
datasetId: string,
documentId: string,
segmentId: string
): Promise<ResultDomain<DifyChildChunkListResponse>> {
const response = await api.get<DifyChildChunkListResponse>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`
);
return response.data;
},
/**
* 更新子块内容
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @param childChunkId 子块ID
* @param content 新内容
* @returns Promise<ResultDomain<DifyChildChunkResponse>>
*/
async updateChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string,
content: string
): Promise<ResultDomain<DifyChildChunkResponse>> {
const requestBody: SegmentUpdateRequest = { content };
const response = await api.patch<DifyChildChunkResponse>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`,
requestBody
);
return response.data;
},
/**
* 创建新的子块
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @param content 分段内容
* @returns Promise<ResultDomain<DifyChildChunkResponse>>
*/
async createChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
content: string
): Promise<ResultDomain<DifyChildChunkResponse>> {
const requestBody: SegmentCreateRequest = { content };
const response = await api.post<DifyChildChunkResponse>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`,
requestBody
);
return response.data;
},
/**
* 删除子块
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @param segmentId 分段ID
* @param childChunkId 子块ID
* @returns Promise<ResultDomain<void>>
*/
async deleteChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string
): Promise<ResultDomain<void>> {
const response = await api.delete<void>(
`/ai/dify/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`
);
return response.data;
},
/**
* 批量获取所有分段和子块
* @param datasetId Dify数据集ID
* @param documentId Dify文档ID
* @returns Promise<DifyChildChunk[]> 所有子块的扁平列表
*/
async getAllSegmentsWithChunks(
datasetId: string,
documentId: string
): Promise<any[]> {
// 1. 获取所有父级分段
const segmentsResult = await this.getDocumentSegments(datasetId, documentId);
if (!segmentsResult.success || !segmentsResult.data?.data) {
throw new Error('获取分段列表失败');
}
// 2. 对每个父级分段,获取其子块
const allChunks: any[] = [];
for (const segment of segmentsResult.data.data) {
try {
const chunksResult = await this.getChildChunks(
datasetId,
documentId,
segment.id
);
if (chunksResult.success && chunksResult.data?.data) {
// 为每个子块添加父级分段信息(用于显示)
const chunksWithSegmentInfo = chunksResult.data.data.map(chunk => ({
...chunk,
parentSegmentId: segment.id,
parentPosition: segment.position,
parentKeywords: segment.keywords
}));
allChunks.push(...chunksWithSegmentInfo);
}
} catch (error) {
console.error(`获取分段 ${segment.id} 的子块失败:`, error);
// 继续处理其他分段
}
}
return allChunks;
}
};

View File

@@ -1,48 +1,117 @@
/**
* @description 文件上传相关API
* @author yslg
* @since 2025-10-15
* @description AI文件上传相关API
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type { AiUploadFile, FileUploadResponse, ResultDomain } from '@/types';
import type { AiUploadFile, ResultDomain, FileUploadResponse, PageDomain, PageParam } from '@/types';
/**
* 文件上传API服务
*/
export const fileUploadApi = {
/**
* 上传文件
* @param file 文件
* @param userID 用户ID
* 上传单个文件到知识库
* @param knowledgeId 知识库ID
* @param file 文件对象
* @returns Promise<ResultDomain<FileUploadResponse>>
*/
async uploadFile(file: File, userID: string): Promise<ResultDomain<FileUploadResponse>> {
async uploadFile(knowledgeId: string, file: File): Promise<ResultDomain<FileUploadResponse>> {
const formData = new FormData();
formData.append('file', file);
formData.append('userID', userID);
const response = await api.upload<FileUploadResponse>('/ai/file/upload', formData);
formData.append('knowledgeId', knowledgeId);
const response = await api.post<FileUploadResponse>('/ai/file/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
},
/**
* 获取上传文件列表
* @param userID 用户ID
* @returns Promise<ResultDomain<AiUploadFile>>
* 批量上传文件
* @param knowledgeId 知识库ID
* @param files 文件列表
* @returns Promise<ResultDomain<FileUploadResponse[]>>
*/
async getUploadFileList(userID: string): Promise<ResultDomain<AiUploadFile>> {
const response = await api.get<AiUploadFile>('/ai/file/list', { userID });
async batchUploadFiles(knowledgeId: string, files: File[]): Promise<ResultDomain<FileUploadResponse[]>> {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
formData.append('knowledgeId', knowledgeId);
const response = await api.post<FileUploadResponse[]>('/ai/file/batch-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
},
/**
* 删除上传文件
* @param fileID 文件ID
* 删除文件
* @param fileId 文件ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteUploadFile(fileID: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/file/${fileID}`);
async deleteFile(fileId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/file/${fileId}`);
return response.data;
},
/**
* 获取文件详情
* @param fileId 文件ID
* @returns Promise<ResultDomain<AiUploadFile>>
*/
async getFileById(fileId: string): Promise<ResultDomain<AiUploadFile>> {
const response = await api.get<AiUploadFile>(`/ai/file/${fileId}`);
return response.data;
},
/**
* 获取知识库的文件列表
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<AiUploadFile[]>>
*/
async listFilesByKnowledge(knowledgeId: string): Promise<ResultDomain<AiUploadFile[]>> {
const response = await api.get<AiUploadFile[]>(`/ai/file/knowledge/${knowledgeId}`);
return response.data;
},
/**
* 分页查询文件
* @param filter 过滤条件
* @param pageParam 分页参数
* @returns Promise<PageDomain<AiUploadFile>>
*/
async pageFiles(filter: Partial<AiUploadFile>, pageParam: PageParam): Promise<PageDomain<AiUploadFile>> {
const response = await api.post<PageDomain<AiUploadFile>>('/ai/file/page', {
filter,
pageParam
});
return response.data;
},
/**
* 同步文件状态从Dify
* @param fileId 文件ID
* @returns Promise<ResultDomain<AiUploadFile>>
*/
async syncFileStatus(fileId: string): Promise<ResultDomain<AiUploadFile>> {
const response = await api.post<AiUploadFile>(`/ai/file/${fileId}/sync`);
return response.data;
},
/**
* 批量同步文件状态
* @param fileIds 文件ID列表
* @returns Promise<ResultDomain<number>>
*/
async batchSyncFileStatus(fileIds: string[]): Promise<ResultDomain<number>> {
const response = await api.post<number>('/ai/file/batch-sync', { fileIds });
return response.data;
}
};

View File

@@ -1,12 +1,17 @@
/**
* @description 智能体相关API
* @author yslg
* @since 2025-10-15
* @description AI模块API导出
* @author AI Assistant
* @since 2025-11-04
*/
// 重新导出各个子模块
export { aiAgentConfigApi } from './agent-config';
export { conversationApi } from './conversation';
export { messageApi } from './message';
export { knowledgeApi } from './knowledge';
export { fileUploadApi } from './file-upload';
export { chatApi } from './chat';
export { chatHistoryApi } from './chat-history';
export { documentSegmentApi } from './document-segment';
// 为了向后兼容,保留旧的导出名称
export { chatApi as conversationApi } from './chat';
export { chatApi as messageApi } from './chat';

View File

@@ -1,52 +1,146 @@
/**
* @description 知识库相关API
* @author yslg
* @since 2025-10-15
* @description AI知识库相关API
* @author AI Assistant
* @since 2025-11-04
*/
import { api } from '@/apis/index';
import type { AiKnowledge, ResultDomain } from '@/types';
import type { AiKnowledge, ResultDomain, PageDomain, PageParam, KnowledgePermissionParams } from '@/types';
/**
* 知识库API服务
*/
export const knowledgeApi = {
/**
* 获取知识库列表
* @returns Promise<ResultDomain<AiKnowledge>>
*/
async getKnowledgeList(): Promise<ResultDomain<AiKnowledge>> {
const response = await api.get<AiKnowledge>('/ai/knowledge/list');
return response.data;
},
/**
* 创建知识库条目
* 创建知识库
* @param knowledge 知识库数据
* @returns Promise<ResultDomain<AiKnowledge>>
*/
async createKnowledge(knowledge: AiKnowledge): Promise<ResultDomain<AiKnowledge>> {
const response = await api.post<AiKnowledge>('/ai/knowledge/create', knowledge);
const response = await api.post<AiKnowledge>('/ai/knowledge', knowledge);
return response.data;
},
/**
* 更新知识库条目
* 更新知识库
* @param knowledge 知识库数据
* @returns Promise<ResultDomain<AiKnowledge>>
*/
async updateKnowledge(knowledge: AiKnowledge): Promise<ResultDomain<AiKnowledge>> {
const response = await api.put<AiKnowledge>('/ai/knowledge/update', knowledge);
const response = await api.put<AiKnowledge>('/ai/knowledge', knowledge);
return response.data;
},
/**
* 删除知识库条目
* @param knowledgeID 知识库ID
* 删除知识库
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteKnowledge(knowledgeID: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/knowledge/${knowledgeID}`);
async deleteKnowledge(knowledgeId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/ai/knowledge/${knowledgeId}`);
return response.data;
},
/**
* 获取知识库详情
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<AiKnowledge>>
*/
async getKnowledgeById(knowledgeId: string): Promise<ResultDomain<AiKnowledge>> {
const response = await api.get<AiKnowledge>(`/ai/knowledge/${knowledgeId}`);
return response.data;
},
/**
* 获取用户可见的知识库列表
* @returns Promise<ResultDomain<AiKnowledge[]>>
*/
async listUserKnowledges(): Promise<ResultDomain<AiKnowledge[]>> {
const response = await api.get<AiKnowledge[]>('/ai/knowledge/user');
return response.data;
},
/**
* 获取知识库列表(支持过滤)
* @param filter 过滤条件
* @returns Promise<ResultDomain<AiKnowledge[]>>
*/
async listKnowledges(filter?: Partial<AiKnowledge>): Promise<ResultDomain<AiKnowledge[]>> {
const response = await api.post<AiKnowledge[]>('/ai/knowledge/list', filter || {});
return response.data;
},
/**
* 分页查询知识库
* @param filter 过滤条件
* @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', {
filter,
pageParam
});
return response.data;
},
/**
* 同步知识库到Dify
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<boolean>>
*/
async syncToDify(knowledgeId: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`/ai/knowledge/${knowledgeId}/sync`);
return response.data;
},
/**
* 从Dify同步知识库状态
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<AiKnowledge>>
*/
async syncFromDify(knowledgeId: string): Promise<ResultDomain<AiKnowledge>> {
const response = await api.get<AiKnowledge>(`/ai/knowledge/${knowledgeId}/sync`);
return response.data;
},
/**
* 设置知识库权限
* @param params 权限参数
* @returns Promise<ResultDomain<boolean>>
*/
async setPermissions(params: KnowledgePermissionParams): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>('/ai/knowledge/permissions', params);
return response.data;
},
/**
* 获取知识库权限
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<any>>
*/
async getPermissions(knowledgeId: string): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/permissions`);
return response.data;
},
/**
* 检查用户是否有权限访问知识库
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<boolean>>
*/
async checkPermission(knowledgeId: string): Promise<ResultDomain<boolean>> {
const response = await api.get<boolean>(`/ai/knowledge/${knowledgeId}/check-permission`);
return response.data;
},
/**
* 获取知识库统计信息
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<any>>
*/
async getStats(knowledgeId: string): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/stats`);
return response.data;
}
};

View File

@@ -1,73 +0,0 @@
/**
* @description 消息相关API
* @author yslg
* @since 2025-10-15
*/
import { api } from '@/apis/index';
import type { AiMessage, ChatRequest, ChatResponse, ResultDomain } from '@/types';
/**
* 消息API服务
*/
export const messageApi = {
/**
* 获取对话消息列表
* @param conversationID 对话ID
* @returns Promise<ResultDomain<AiMessage>>
*/
async getMessageList(conversationID: string): Promise<ResultDomain<AiMessage>> {
const response = await api.get<AiMessage>(`/ai/message/list`, { conversationID });
return response.data;
},
/**
* 发送消息
* @param request 消息请求
* @returns Promise<ResultDomain<ChatResponse>>
*/
async sendMessage(request: ChatRequest): Promise<ResultDomain<ChatResponse>> {
const response = await api.post<ChatResponse>('/ai/message/send', request);
return response.data;
}
/**
* 流式发送消息
* @param request 消息请求
* @param onMessage 消息回调
* @returns Promise<void>
*/
// async sendMessageStream(request: ChatRequest, onMessage: (message: string) => void): Promise<void> {
// const response = await api.post('/ai/message/stream', { ...request, stream: true }, {
// responseType: 'stream'
// });
// // 处理流式响应
// const reader = response.data.getReader();
// const decoder = new TextDecoder();
// let done = false;
// while (!done) {
// const readResult = await reader.read();
// done = readResult.done;
// if (done) break;
// const chunk = decoder.decode(readResult.value);
// const lines = chunk.split('\n');
// for (const line of lines) {
// if (line.startsWith('data: ')) {
// const data = line.slice(6);
// if (data === '[DONE]') return;
// try {
// const parsed = JSON.parse(data);
// onMessage(parsed.content || '');
// } catch (e) {
// console.error('解析流式数据失败:', e);
// }
// }
// }
// }
// }
};

View File

@@ -0,0 +1,10 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10C0 4.47715 4.47715 0 10 0H38C43.5229 0 48 4.47715 48 10V38C48 43.5229 43.5228 48 38 48H10C4.47715 48 0 43.5228 0 38V10Z" fill="#E7000B"/>
<path d="M24 20V16H20" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M30 20H18C16.8954 20 16 20.8954 16 22V30C16 31.1046 16.8954 32 18 32H30C31.1046 32 32 31.1046 32 30V22C32 20.8954 31.1046 20 30 20Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 26H16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32 26H34" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 25V27" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 25V27" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -0,0 +1,5 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="80" height="80" rx="40" fill="#C62828"/>
<path d="M44.3335 20.4997C44.5794 20.4997 44.8244 20.5052 45.0679 20.5153C44.5924 21.8662 44.3335 23.3192 44.3335 24.8327C44.3335 32.0124 50.1538 37.8327 57.3335 37.8327C58.8471 37.8327 60.301 37.5739 61.6519 37.0983C61.662 37.3417 61.6665 37.5869 61.6665 37.8327C61.6665 47.4056 53.9064 55.1666 44.3335 55.1667V62.7497C33.5002 58.4163 18.3335 51.916 18.3335 37.8327C18.3337 28.26 26.0938 20.4998 35.6665 20.4997H44.3335ZM24.8335 41.444H29.8892V33.4997H24.8335V41.444ZM32.7778 33.4997V41.444H37.8335V33.4997H32.7778ZM56.314 16.859C56.6964 15.936 57.9708 15.936 58.353 16.859L58.9019 18.1833C59.8379 20.4428 61.5836 22.247 63.7788 23.2233L65.3325 23.9147C66.2225 24.3106 66.2226 25.6058 65.3325 26.0016L63.687 26.7331C61.5468 27.685 59.8316 29.4256 58.8794 31.611L58.3452 32.8366C57.9543 33.7336 56.713 33.7335 56.3218 32.8366L55.7876 31.611C54.8353 29.4256 53.1202 27.685 50.98 26.7331L49.3345 26.0016C48.4446 25.6058 48.4446 24.3105 49.3345 23.9147L50.8882 23.2233C53.0833 22.2469 54.8294 20.4428 55.7651 18.1833L56.314 16.859Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7137 12.3572L13.5352 11.1787L14.7137 10.0002C16.0154 8.6985 16.0154 6.58795 14.7137 5.2862C13.4119 3.98445 11.3013 3.98445 9.99959 5.2862L8.82109 6.46471L7.64258 5.2862L8.82109 4.10769C10.7737 2.15506 13.9395 2.15506 15.8922 4.10769C17.8448 6.06031 17.8448 9.22616 15.8922 11.1787L14.7137 12.3572ZM12.3566 14.7143L11.1781 15.8928C9.2255 17.8454 6.05966 17.8454 4.10705 15.8928C2.15442 13.9402 2.15442 10.7743 4.10705 8.82175L5.28555 7.64322L6.46406 8.82175L5.28555 10.0002C3.9838 11.302 3.9838 13.4126 5.28555 14.7143C6.5873 16.016 8.69784 16.016 9.99959 14.7143L11.1781 13.5357L12.3566 14.7143ZM12.3566 6.46471L13.5352 7.64322L7.64258 13.5357L6.46406 12.3572L12.3566 6.46471Z" fill="#334155"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

View File

@@ -0,0 +1,5 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="30" rx="8" fill="#C62828"/>
<path d="M9 15.6667H13V14.3333H9V8.23046C9 8.04637 9.14924 7.89713 9.33333 7.89713C9.3895 7.89713 9.44476 7.91132 9.49397 7.93839L21.8023 14.7079C21.9636 14.7967 22.0225 14.9993 21.9337 15.1607C21.9033 15.2161 21.8577 15.2617 21.8023 15.2921L9.49397 22.0617C9.33267 22.1504 9.12998 22.0915 9.04126 21.9303C9.01419 21.881 9 21.8258 9 21.7696V15.6667Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.1665 5.00002H18.3332V6.66669H16.6665V17.5C16.6665 17.9603 16.2934 18.3334 15.8332 18.3334H4.1665C3.70627 18.3334 3.33317 17.9603 3.33317 17.5V6.66669H1.6665V5.00002H5.83317V2.50002C5.83317 2.03979 6.20627 1.66669 6.6665 1.66669H13.3332C13.7934 1.66669 14.1665 2.03979 14.1665 2.50002V5.00002ZM14.9998 6.66669H4.99984V16.6667H14.9998V6.66669ZM7.49984 3.33335V5.00002H12.4998V3.33335H7.49984Z" fill="#334155"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,10 +1,11 @@
/**
* @description 智能体相关类型定义
* @author yslg
* @since 2025-10-15
* @description AI模块类型定义
* @author AI Assistant
* @since 2025-11-04
*/
import { BaseDTO } from '../base';
import { PageParam } from '../base';
/**
* 智能体配置实体
@@ -14,18 +15,24 @@ export interface AiAgentConfig extends BaseDTO {
name?: string;
/** 智能体头像 */
avatar?: string;
/** 智能体描述 */
description?: string;
/** 系统提示词 */
systemPrompt?: string;
/** 模型名称 */
modelName?: string;
/** 模型提供商 */
modelProvider?: string;
/** 温度值 */
/** 温度值0.0-1.0 */
temperature?: number;
/** 最大tokens */
maxTokens?: number;
/** Top P值 */
topP?: number;
/** Dify应用ID */
difyAppId?: string;
/** Dify API Key */
difyApiKey?: string;
/** 状态0禁用 1启用 */
status?: number;
/** 创建者 */
@@ -34,18 +41,84 @@ export interface AiAgentConfig extends BaseDTO {
updater?: string;
}
/**
* AI知识库实体
*/
export interface AiKnowledge extends BaseDTO {
/** 知识库名称 */
name?: string;
/** 知识库描述 */
description?: string;
/** 索引方式high_quality=高质量, economy=经济) */
indexingTechnique?: string;
/** Embedding模型 */
embeddingModel?: string;
/** Dify数据集ID */
difyDatasetId?: string;
/** 同步状态0未同步 1已同步 2同步失败 */
syncStatus?: number;
/** 文档数量 */
documentCount?: number;
/** 字符数 */
characterCount?: number;
/** 创建者部门 */
creatorDept?: string;
/** 状态0禁用 1启用 */
status?: number;
}
/**
* AI上传文件实体
*/
export interface AiUploadFile extends BaseDTO {
/** 知识库ID */
knowledgeId?: string;
/** 文件名 */
fileName?: string;
/** 文件路径 */
filePath?: string;
/** 文件大小(字节) */
fileSize?: number;
/** 文件类型MIME类型 */
fileType?: string;
/** Dify文档ID */
difyDocumentId?: string;
/** Dify批次ID */
difyBatchId?: string;
/** 上传状态0处理中 1成功 2失败 */
uploadStatus?: number;
/** 向量化状态0待处理 1处理中 2已完成 3失败 */
vectorStatus?: number;
/** 分片数 */
segmentCount?: number;
/** 错误信息 */
errorMessage?: string;
}
/**
* 对话会话实体
*/
export interface AiConversation extends BaseDTO {
/** 用户ID */
userID?: string;
/** 智能体ID */
agentID?: string;
/** 会话标题 */
title?: string;
/** 会话摘要 */
summary?: string;
/** Dify会话ID */
difyConversationId?: string;
/** 状态0已结束 1进行中 */
status?: number;
/** 是否收藏 */
isFavorite?: boolean;
/** 是否置顶 */
isPinned?: boolean;
/** 消息数量 */
messageCount?: number;
/** Token总数 */
totalTokens?: number;
/** 最后消息时间 */
lastMessageTime?: string;
}
@@ -58,6 +131,8 @@ export interface AiMessage extends BaseDTO {
conversationID?: string;
/** 用户ID */
userID?: string;
/** 智能体ID */
agentID?: string;
/** 角色user用户 assistant助手 system系统 */
role?: string;
/** 消息内容 */
@@ -66,54 +141,26 @@ export interface AiMessage extends BaseDTO {
fileIDs?: string;
/** 引用知识IDJSON数组 */
knowledgeIDs?: string;
/** 知识库引用详情JSON数组 */
knowledgeRefs?: string;
/** Token数量 */
tokenCount?: number;
/** Dify消息ID */
difyMessageId?: string;
/** 评分1=好评,-1=差评) */
rating?: number;
/** 反馈内容 */
feedback?: string;
}
/**
* 知识库实体
*/
export interface AiKnowledge extends BaseDTO {
/** 知识标题 */
title?: string;
/** 知识内容 */
content?: string;
/** 知识类型1文本 2文件 */
type?: number;
/** 文件ID */
fileID?: string;
/** 状态0禁用 1启用 */
status?: number;
/** 创建者 */
creator?: string;
}
/**
* 上传文件实体
*/
export interface AiUploadFile extends BaseDTO {
/** 用户ID */
userID?: string;
/** 文件名 */
fileName?: string;
/** 文件路径 */
filePath?: string;
/** 文件大小 */
fileSize?: number;
/** 文件类型 */
fileType?: string;
/** 状态0处理中 1成功 2失败 */
status?: number;
/** 错误信息 */
errorMessage?: string;
}
/**
* 使用统计实体
* AI使用统计实体
*/
export interface AiUsageStatistics extends BaseDTO {
/** 用户ID */
userID?: string;
/** 智能体ID */
agentID?: string;
/** 统计日期 */
statisticsDate?: string;
/** 对话次数 */
@@ -122,6 +169,8 @@ export interface AiUsageStatistics extends BaseDTO {
messageCount?: number;
/** Token使用量 */
tokenUsage?: number;
/** 知识库查询次数 */
knowledgeQueryCount?: number;
/** 文件上传次数 */
fileUploadCount?: number;
}
@@ -130,12 +179,14 @@ export interface AiUsageStatistics extends BaseDTO {
* 对话请求参数
*/
export interface ChatRequest {
/** 会话ID */
conversationID?: string;
/** 消息内容 */
message?: string;
/** 文件ID列表 */
fileIDs?: string[];
/** 智能体ID */
agentId: string;
/** 会话ID可选为空则创建新会话 */
conversationId?: string;
/** 用户问题 */
query: string;
/** 指定的知识库ID列表可选 */
knowledgeIds?: string[];
/** 是否流式返回 */
stream?: boolean;
}
@@ -145,7 +196,9 @@ export interface ChatRequest {
*/
export interface ChatResponse {
/** 消息ID */
messageID?: string;
messageId?: string;
/** 会话ID */
conversationId?: string;
/** 消息内容 */
content?: string;
/** Token使用量 */
@@ -159,11 +212,243 @@ export interface ChatResponse {
*/
export interface FileUploadResponse {
/** 文件ID */
fileID?: string;
fileId?: string;
/** 文件名 */
fileName?: string;
/** 文件大小 */
fileSize?: number;
/** 处理状态 */
status?: number;
/** Dify文档ID */
difyDocumentId?: string;
}
/**
* 会话搜索参数
*/
export interface ConversationSearchParams {
/** 智能体ID可选 */
agentId?: string;
/** 关键词 */
keyword?: string;
/** 是否收藏(可选) */
isFavorite?: boolean;
/** 开始日期 */
startDate?: string;
/** 结束日期 */
endDate?: string;
/** 分页参数 */
pageParam?: PageParam;
}
/**
* 消息搜索参数
*/
export interface MessageSearchParams {
/** 关键词 */
keyword: string;
/** 会话ID可选限定范围 */
conversationId?: string;
/** 分页参数 */
pageParam?: PageParam;
}
/**
* 用户对话统计
*/
export interface UserChatStatistics {
/** 会话总数 */
totalConversations?: number;
/** 消息总数 */
totalMessages?: number;
/** Token总数 */
totalTokens?: number;
/** 收藏会话数 */
favoriteConversations?: number;
/** 最近活跃会话数7天 */
recentActiveConversations?: number;
}
/**
* 会话统计详情
*/
export interface ConversationStatistics {
/** 会话ID */
conversationId?: string;
/** 标题 */
title?: string;
/** 消息数量 */
messageCount?: number;
/** Token总数 */
totalTokens?: number;
/** 创建时间 */
createTime?: string;
/** 最后消息时间 */
lastMessageTime?: string;
/** 用户消息数 */
userMessageCount?: number;
/** AI回复数 */
assistantMessageCount?: number;
/** 评分分布 */
ratingDistribution?: Array<{ rating: number; count: number }>;
/** 反馈数量 */
feedbackCount?: number;
}
/**
* 批量导出参数
*/
export interface BatchExportParams {
/** 会话ID列表 */
conversationIds: string[];
/** 格式markdown/json */
format: 'markdown' | 'json';
}
/**
* 知识库权限参数
*/
export interface KnowledgePermissionParams {
/** 知识库ID */
knowledgeId: string;
/** 部门ID列表 */
deptIds: string[];
/** 角色ID列表 */
roleIds: string[];
}
/**
* Streaming 回调接口
*/
export interface StreamCallback {
/** 接收到消息片段 */
onMessage?: (message: string) => void;
/** 消息结束 */
onMessageEnd?: (metadata: string) => void;
/** 完成 */
onComplete?: () => void;
/** 错误 */
onError?: (error: Error) => void;
}
// ==================== Dify 文档分段相关类型 ====================
/**
* Dify 分段(父级)
*/
export interface DifySegment {
/** 分段ID */
id: string;
/** 位置序号 */
position: number;
/** 文档ID */
document_id: string;
/** 分段内容 */
content: string;
/** 字数 */
word_count: number;
/** Token数 */
tokens: number;
/** 关键词列表 */
keywords: string[];
/** 索引节点ID */
index_node_id: string;
/** 索引节点哈希 */
index_node_hash: string;
/** 命中次数 */
hit_count: number;
/** 是否启用 */
enabled: boolean;
/** 禁用时间 */
disabled_at?: number;
/** 禁用者 */
disabled_by?: string;
/** 状态 */
status: string;
/** 创建者ID */
created_by: string;
/** 创建时间(时间戳) */
created_at: number;
/** 索引开始时间 */
indexing_at?: number;
/** 完成时间 */
completed_at?: number;
/** 错误信息 */
error?: string;
/** 停止时间 */
stopped_at?: number;
}
/**
* Dify 子块(分段内容块)
*/
export interface DifyChildChunk {
/** 子块ID */
id: string;
/** 父分段ID */
segment_id: string;
/** 分段内容 */
content: string;
/** 字数 */
word_count: number;
/** Token数 */
tokens: number;
/** 索引节点ID */
index_node_id: string;
/** 索引节点哈希 */
index_node_hash: string;
/** 状态 */
status: string;
/** 创建者ID */
created_by: string;
/** 创建时间(时间戳) */
created_at: number;
/** 索引开始时间 */
indexing_at?: number;
/** 完成时间 */
completed_at?: number;
/** 错误信息 */
error?: string;
/** 停止时间 */
stopped_at?: number;
}
/**
* Dify 分段列表响应
*/
export interface DifySegmentListResponse {
/** 分段数据列表 */
data: DifySegment[];
}
/**
* Dify 子块列表响应
*/
export interface DifyChildChunkListResponse {
/** 子块数据列表 */
data: DifyChildChunk[];
}
/**
* Dify 子块单个响应
*/
export interface DifyChildChunkResponse {
/** 子块数据 */
data: DifyChildChunk;
}
/**
* 分段更新请求
*/
export interface SegmentUpdateRequest {
/** 更新的内容 */
content: string;
}
/**
* 分段创建请求
*/
export interface SegmentCreateRequest {
/** 分段内容 */
content: string;
}

View File

@@ -1,127 +1,428 @@
<template>
<AdminLayout title="AI配置" subtitle="AI配置">
<div class="ai-config">
<el-form :model="configForm" label-width="150px" class="config-form">
<el-divider content-position="left">模型配置</el-divider>
<el-form-item label="AI模型">
<el-select v-model="configForm.model" placeholder="选择AI模型">
<el-option label="GPT-3.5" value="gpt-3.5" />
<el-option label="GPT-4" value="gpt-4" />
<el-option label="Claude" value="claude" />
</el-select>
</el-form-item>
<AdminLayout title="AI配置" subtitle="AI助手配置管理">
<div class="ai-config-container">
<!-- 智能体信息卡片 -->
<div class="agent-info-card">
<div class="agent-header">
<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="助手头像" />
</div>
</div>
<div class="agent-info">
<h2 class="agent-name">{{ configForm.name || '未配置助手' }}</h2>
</div>
<div class="agent-status" :class="statusClass">
{{ statusText }}
</div>
</div>
</div>
<el-form-item label="API Key">
<el-input v-model="configForm.apiKey" type="password" show-password />
</el-form-item>
<!-- 配置表单 -->
<div class="config-form-container">
<el-form :model="configForm" label-position="top" class="config-form">
<!-- 基本信息 -->
<div class="form-section">
<el-form-item label="助手名称" required>
<el-input
v-model="configForm.name"
placeholder="请输入助手名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="configForm.apiUrl" />
</el-form-item>
<el-form-item label="助手头像">
<FileUpload
v-model:cover-url="configForm.avatar"
:as-dialog="false"
list-type="cover"
accept="image/*"
:max-size="2"
module="ai-agent"
tip="点击上传助手头像"
/>
</el-form-item>
<el-divider content-position="left">对话配置</el-divider>
<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-slider v-model="configForm.temperature" :min="0" :max="2" :step="0.1" show-input />
<span class="help-text">控制回答的随机性值越大回答越随机</span>
</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="最大token数">
<el-input-number v-model="configForm.maxTokens" :min="100" :max="4000" />
</el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="configForm.systemPrompt"
type="textarea"
:rows="8"
placeholder="请输入系统提示词定义AI助手的角色、行为和回答风格..."
maxlength="2000"
show-word-limit
/>
</el-form-item>
</div>
<el-form-item label="历史对话轮数">
<el-input-number v-model="configForm.historyTurns" :min="1" :max="20" />
</el-form-item>
<el-divider content-position="left">功能配置</el-divider>
<el-form-item label="启用流式输出">
<el-switch v-model="configForm.enableStreaming" />
</el-form-item>
<el-form-item label="启用文件解读">
<el-switch v-model="configForm.enableFileInterpretation" />
</el-form-item>
<el-form-item label="启用知识库检索">
<el-switch v-model="configForm.enableKnowledgeRetrieval" />
</el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="configForm.systemPrompt"
type="textarea"
:rows="6"
placeholder="设置AI助手的角色和行为..."
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleTest">测试连接</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button type="primary" size="large" @click="handleSave" :loading="saving">
保存配置
</el-button>
<el-button size="large" @click="handleReset">
重置
</el-button>
</div>
</el-form>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElSlider, ElInputNumber, ElSwitch, ElButton, ElDivider, ElMessage } from 'element-plus';
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { AdminLayout } from '@/views/admin';
import { FileUpload } from '@/components/file';
import { aiAgentConfigApi } from '@/apis/ai';
import { FILE_DOWNLOAD_URL } from '@/config';
import type { AiAgentConfig } from '@/types';
defineOptions({
name: 'AIConfigView'
});
const configForm = ref({
model: 'gpt-3.5',
apiKey: '',
apiUrl: '',
temperature: 0.7,
maxTokens: 2000,
historyTurns: 5,
enableStreaming: true,
enableFileInterpretation: true,
enableKnowledgeRetrieval: true,
systemPrompt: ''
// 表单数据
const configForm = ref<AiAgentConfig>({
name: '',
avatar: '',
systemPrompt: '',
modelName: '',
modelProvider: 'dify',
status: 1
});
onMounted(() => {
loadConfig();
// 状态
const saving = ref(false);
const loading = ref(false);
// 状态文本
const statusText = computed(() => {
return configForm.value.status === 1 ? '运行中' : '已停用';
});
function loadConfig() {
// TODO: 加载AI配置
// 状态样式类
const statusClass = computed(() => {
return configForm.value.status === 1 ? 'status-active' : 'status-inactive';
});
// 加载配置
onMounted(async () => {
await loadConfig();
});
async function loadConfig() {
try {
loading.value = true;
// 获取启用的智能体列表
const result = await aiAgentConfigApi.listEnabledAgents();
if (result.success && result.data && result.data.length > 0) {
// 使用第一个启用的智能体
Object.assign(configForm.value, result.data[0]);
}
} catch (error) {
console.error('加载配置失败:', error);
ElMessage.warning('暂无配置信息,请填写配置');
} finally {
loading.value = false;
}
}
function handleSave() {
// TODO: 保存配置
ElMessage.success('配置保存成功');
// 保存配置
async function handleSave() {
// 验证必填项
if (!configForm.value.name) {
ElMessage.warning('请输入助手名称');
return;
}
if (!configForm.value.modelProvider) {
ElMessage.warning('请选择模式');
return;
}
try {
saving.value = true;
// 判断是更新还是创建
if (configForm.value.id) {
await aiAgentConfigApi.updateAgent(configForm.value);
ElMessage.success('配置更新成功');
} else {
const result = await aiAgentConfigApi.createAgent(configForm.value);
if (result.success && result.data) {
configForm.value.id = result.data.id;
}
ElMessage.success('配置创建成功');
}
// 重新加载配置
await loadConfig();
} catch (error: any) {
console.error('保存配置失败:', error);
ElMessage.error(error.message || '保存配置失败');
} finally {
saving.value = false;
}
}
function handleTest() {
// TODO: 测试API连接
ElMessage.info('正在测试连接...');
}
function handleReset() {
// TODO: 重置配置
// 重置配置
async function handleReset() {
try {
await ElMessageBox.confirm(
'确定要重置配置吗?此操作将清空当前未保存的修改。',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await loadConfig();
ElMessage.success('配置已重置');
} catch {
// 用户取消
}
}
</script>
<style lang="scss" scoped>
.ai-config {
padding: 20px;
max-width: 800px;
.ai-config-container {
padding: 24px;
max-width: 1600px;
margin: 0 auto;
}
.agent-info-card {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 24px;
margin-bottom: 24px;
.agent-header {
display: flex;
align-items: center;
gap: 12px;
.agent-icon {
width: 48px;
height: 48px;
border-radius: 10px;
background: #E7000B;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.default-icon {
display: flex;
align-items: center;
justify-content: center;
}
}
.agent-info {
flex: 1;
min-width: 0;
.agent-name {
font-size: 16px;
font-weight: 400;
line-height: 1.5;
color: #101828;
margin: 0;
letter-spacing: -0.02em;
}
}
.agent-status {
padding: 2px 8px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
line-height: 1.33;
white-space: nowrap;
&.status-active {
background: #DCFCE7;
color: #008236;
border: 1px solid transparent;
}
&.status-inactive {
background: #FEF2F2;
color: #DC2626;
border: 1px solid transparent;
}
}
}
}
.config-form-container {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 24px;
}
.config-form {
.help-text {
font-size: 12px;
color: #999;
margin-left: 12px;
max-width: 672px;
.form-section {
margin-bottom: 32px;
&:last-child {
margin-bottom: 0;
}
}
.section-title {
font-size: 16px;
font-weight: 500;
color: #101828;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #F3F3F5;
}
:deep(.el-form-item) {
margin-bottom: 16px;
.el-form-item__label {
font-size: 14px;
font-weight: 500;
color: #0A0A0A;
line-height: 1;
margin-bottom: 8px;
padding: 0;
letter-spacing: -0.01em;
}
.el-input__wrapper {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
padding: 4px 12px;
box-shadow: none;
transition: all 0.2s;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&.is-focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
.el-textarea__inner {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
padding: 8px 12px;
box-shadow: none;
transition: all 0.2s;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&:focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
.el-input-number {
width: 100%;
.el-input__wrapper {
width: 100%;
}
}
}
.form-actions {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #F3F3F5;
display: flex;
gap: 12px;
.el-button {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
color: #FFFFFF;
&:hover {
background: #C90009;
border-color: #C90009;
}
&:active {
background: #A30008;
border-color: #A30008;
}
}
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
}
}
}
}
}
:deep(.el-switch) {
--el-switch-on-color: #E7000B;
.el-switch__label {
font-size: 14px;
color: #0A0A0A;
}
}
</style>

View File

@@ -0,0 +1,742 @@
<template>
<el-dialog
v-model="visible"
title="文档分段管理"
width="1200px"
:close-on-click-modal="false"
class="segment-dialog"
>
<!-- 统计信息卡片 -->
<div class="segment-stats">
<div class="stat-item">
<div class="stat-label">总分段数</div>
<div class="stat-value">{{ totalSegments }}</div>
</div>
<div class="stat-item">
<div class="stat-label">总字数</div>
<div class="stat-value">{{ totalWords }}</div>
</div>
<div class="stat-item">
<div class="stat-label"> Tokens</div>
<div class="stat-value">{{ totalTokens }}</div>
</div>
</div>
<!-- 分段列表 -->
<div class="segment-list" v-loading="loading">
<div
v-for="(segment, index) in segments"
:key="segment.id"
class="segment-item"
>
<div class="segment-header">
<span class="segment-index">分段 {{ index + 1 }}</span>
<span class="segment-info">
{{ segment.word_count }} · {{ segment.tokens }} tokens
</span>
<div class="segment-actions">
<el-button
size="small"
@click="editSegment(segment)"
:icon="Edit"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="deleteSegment(segment)"
:icon="Delete"
>
删除
</el-button>
</div>
</div>
<!-- 分段内容显示/编辑 -->
<div class="segment-content">
<div v-if="editingSegmentId === segment.id" class="segment-editor">
<el-input
v-model="editingContent"
type="textarea"
:rows="6"
placeholder="编辑分段内容"
/>
<div class="editor-actions">
<el-button size="small" @click="cancelEdit">取消</el-button>
<el-button
size="small"
type="primary"
@click="saveSegment(segment)"
:loading="saving"
>
保存
</el-button>
</div>
</div>
<div v-else class="segment-text">
{{ segment.content }}
</div>
</div>
<!-- 关键词标签 -->
<div class="segment-keywords" v-if="segment.parentKeywords?.length">
<el-tag
v-for="keyword in segment.parentKeywords"
:key="keyword"
size="small"
style="margin-right: 8px;"
>
{{ keyword }}
</el-tag>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && segments.length === 0" class="empty-state">
<p>暂无分段数据</p>
</div>
</div>
<!-- 底部操作栏 -->
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" @click="showAddSegment" :icon="Plus">
添加分段
</el-button>
</div>
</template>
<!-- 添加分段对话框 -->
<el-dialog
v-model="addDialogVisible"
title="添加新分段"
width="600px"
append-to-body
>
<el-form label-position="top">
<el-form-item label="选择父分段(可选)">
<el-select
v-model="selectedParentSegment"
placeholder="选择一个分段作为父级"
style="width: 100%"
clearable
>
<el-option
v-for="(seg, idx) in segments"
:key="seg.id"
:label="`分段 ${idx + 1}: ${seg.content.substring(0, 30)}...`"
:value="seg.parentSegmentId"
/>
</el-select>
</el-form-item>
<el-form-item label="分段内容" required>
<el-input
v-model="newSegmentContent"
type="textarea"
:rows="8"
placeholder="请输入分段内容"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="createNewSegment"
:loading="creating"
>
创建
</el-button>
</template>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
import { documentSegmentApi } from '../../../../../apis/ai';
defineOptions({
name: 'DocumentSegmentDialog'
});
interface Props {
/** 是否显示对话框 */
modelValue: boolean;
/** Dify数据集ID */
datasetId: string;
/** Dify文档ID */
documentId: string;
}
const props = defineProps<Props>();
const emit = defineEmits(['update:modelValue']);
// 对话框显示状态
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
});
// 数据状态
const loading = ref(false);
const saving = ref(false);
const creating = ref(false);
const segments = ref<any[]>([]);
// 编辑状态
const editingSegmentId = ref<string | null>(null);
const editingContent = ref('');
// 添加分段状态
const addDialogVisible = ref(false);
const selectedParentSegment = ref<string>('');
const newSegmentContent = ref('');
// 统计信息
const totalSegments = computed(() => segments.value.length);
const totalWords = computed(() =>
segments.value.reduce((sum, seg) => sum + (seg.word_count || 0), 0)
);
const totalTokens = computed(() =>
segments.value.reduce((sum, seg) => sum + (seg.tokens || 0), 0)
);
// 监听对话框显示状态,加载数据
watch(visible, async (val) => {
if (val) {
await loadSegments();
}
});
/**
* 加载分段数据
*/
async function loadSegments() {
try {
loading.value = true;
// 使用辅助方法获取所有分段和子块
const allChunks = await documentSegmentApi.getAllSegmentsWithChunks(
props.datasetId,
props.documentId
);
segments.value = allChunks;
} catch (error: any) {
console.error('加载分段失败:', error);
ElMessage.error(error.message || '加载分段失败');
} finally {
loading.value = false;
}
}
/**
* 编辑分段
*/
function editSegment(segment: any) {
editingSegmentId.value = segment.id;
editingContent.value = segment.content;
}
/**
* 取消编辑
*/
function cancelEdit() {
editingSegmentId.value = null;
editingContent.value = '';
}
/**
* 保存分段
*/
async function saveSegment(segment: any) {
if (!editingContent.value.trim()) {
ElMessage.warning('分段内容不能为空');
return;
}
try {
saving.value = true;
const result = await documentSegmentApi.updateChildChunk(
props.datasetId,
props.documentId,
segment.segment_id,
segment.id,
editingContent.value
);
if (result.success && result.data?.data) {
// 更新本地数据
const index = segments.value.findIndex(s => s.id === segment.id);
if (index !== -1) {
segments.value[index] = {
...segments.value[index],
...result.data.data
};
}
ElMessage.success('保存成功');
cancelEdit();
}
} catch (error: any) {
console.error('保存分段失败:', error);
ElMessage.error(error.message || '保存失败');
} finally {
saving.value = false;
}
}
/**
* 删除分段
*/
async function deleteSegment(segment: any) {
try {
await ElMessageBox.confirm(
'确定要删除这个分段吗?此操作不可恢复。',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const result = await documentSegmentApi.deleteChildChunk(
props.datasetId,
props.documentId,
segment.segment_id,
segment.id
);
if (result.success) {
segments.value = segments.value.filter(s => s.id !== segment.id);
ElMessage.success('删除成功');
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除分段失败:', error);
ElMessage.error(error.message || '删除失败');
}
}
}
/**
* 显示添加分段对话框
*/
function showAddSegment() {
// 如果有分段,默认选择第一个作为父级
if (segments.value.length > 0) {
selectedParentSegment.value = segments.value[0].parentSegmentId;
}
addDialogVisible.value = true;
}
/**
* 创建新分段
*/
async function createNewSegment() {
if (!newSegmentContent.value.trim()) {
ElMessage.warning('请输入分段内容');
return;
}
if (!selectedParentSegment.value) {
ElMessage.warning('请选择一个父分段');
return;
}
try {
creating.value = true;
const result = await documentSegmentApi.createChildChunk(
props.datasetId,
props.documentId,
selectedParentSegment.value,
newSegmentContent.value
);
if (result.success && result.data?.data) {
ElMessage.success('创建成功');
addDialogVisible.value = false;
newSegmentContent.value = '';
selectedParentSegment.value = '';
// 重新加载列表
await loadSegments();
}
} catch (error: any) {
console.error('创建分段失败:', error);
ElMessage.error(error.message || '创建失败');
} finally {
creating.value = false;
}
}
</script>
<style lang="scss" scoped>
.segment-dialog {
:deep(.el-dialog) {
border-radius: 14px;
.el-dialog__header {
padding: 24px 24px 16px;
border-bottom: 1px solid #F3F3F5;
.el-dialog__title {
font-size: 18px;
font-weight: 500;
color: #101828;
letter-spacing: -0.02em;
}
}
.el-dialog__body {
padding: 24px;
background: #FAFAFA;
}
.el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid #F3F3F5;
}
}
}
.segment-stats {
display: flex;
gap: 12px;
margin-bottom: 20px;
.stat-item {
flex: 1;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 20px 16px;
text-align: center;
transition: all 0.2s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.stat-label {
font-size: 14px;
font-weight: 400;
color: #4A5565;
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.stat-value {
font-size: 28px;
font-weight: 400;
color: #101828;
letter-spacing: -0.02em;
}
}
}
.segment-list {
max-height: 520px;
overflow-y: auto;
padding-right: 4px;
/* 自定义滚动条 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #D1D5DB;
border-radius: 3px;
&:hover {
background: #9CA3AF;
}
}
.segment-item {
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s;
&:hover {
border-color: #E7000B;
box-shadow: 0 4px 12px rgba(231, 0, 11, 0.12);
}
&:last-child {
margin-bottom: 0;
}
}
.segment-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
gap: 12px;
.segment-index {
font-weight: 500;
color: #101828;
font-size: 14px;
letter-spacing: -0.01em;
white-space: nowrap;
}
.segment-info {
flex: 1;
font-size: 12px;
color: #6A7282;
letter-spacing: -0.01em;
white-space: nowrap;
}
.segment-actions {
display: flex;
gap: 8px;
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
padding: 5px 12px;
&.el-button--small {
font-size: 14px;
}
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
border-color: transparent;
}
}
&.el-button--danger {
background: #FEF2F2;
border-color: transparent;
color: #DC2626;
&:hover {
background: #FEE2E2;
border-color: transparent;
}
}
}
}
}
.segment-content {
.segment-text {
padding: 12px;
background: #F9FAFB;
border: 1px solid transparent;
border-radius: 8px;
line-height: 1.6;
color: #4A5565;
font-size: 14px;
white-space: pre-wrap;
word-break: break-word;
letter-spacing: -0.01em;
}
.segment-editor {
:deep(.el-textarea) {
.el-textarea__inner {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
padding: 12px;
font-size: 14px;
line-height: 1.6;
color: #0A0A0A;
letter-spacing: -0.01em;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&:focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
}
.editor-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-end;
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
}
}
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
border-color: #C90009;
}
}
}
}
}
}
.segment-keywords {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-tag) {
background: #EFF6FF;
border: 1px solid transparent;
border-radius: 8px;
color: #1447E6;
font-size: 12px;
padding: 2px 8px;
height: 24px;
line-height: 20px;
}
}
.empty-state {
text-align: center;
padding: 80px 20px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
p {
font-size: 14px;
color: #6A7282;
margin: 0;
letter-spacing: -0.01em;
}
}
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
}
}
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
border-color: #C90009;
}
}
}
}
/* 添加分段对话框样式 */
:deep(.el-dialog__wrapper) {
.el-dialog {
border-radius: 14px;
.el-form-item__label {
font-size: 14px;
font-weight: 500;
color: #0A0A0A;
letter-spacing: -0.01em;
}
.el-input__wrapper {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
box-shadow: none;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&.is-focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
.el-textarea__inner {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
box-shadow: none;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&:focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
.el-select {
.el-input__wrapper {
background: #F3F3F5;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export { default as AIAgent} from './AIAgent.vue';