Merge remote-tracking branch 'origin/ai-hub' into ai-hub

This commit is contained in:
ccnetcore
2025-12-23 00:49:25 +08:00
6 changed files with 431 additions and 182 deletions

View File

@@ -19,3 +19,8 @@ export function getChatList(params: GetChatListParams) {
// return get<ChatMessageVo[]>('/system/message/list', params);
return get<ChatMessageVo[]>('/message', params).json();
}
// 新增对应会话聊天记录
export function aiChatTool() {
return post('/ai-chat/tool').json();
}

View File

@@ -0,0 +1,416 @@
<script setup lang="ts">
import { ChromeFilled, ElementPlus, Loading, MagicStick, Search } from '@element-plus/icons-vue';
import { computed, onMounted, ref } from 'vue';
import { aiChatTool } from '@/api';
// API返回的工具接口
interface ApiToolItem {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
};
}
// 前端工具按钮的数据
interface ToolItem {
id: number;
name: string;
apiName: string;
icon: any;
tip: string;
enabled: boolean;
}
// 定义组件事件
const emit = defineEmits<{
'tools-update': [payload: {
selectedToolIds: number[];
selectedApiTools: string[];
}];
}>();
// 图标映射配置 - 根据API名称映射图标
const iconMap: Record<string, any> = {
online_search: ChromeFilled, // 在线搜索
deep_think: ElementPlus, // 深度思考
search: Search, // 搜索
web_search: ChromeFilled, // 网页搜索
thinking: MagicStick, // 思考
default: ElementPlus, // 默认图标
};
// 提示信息映射
const tipMap: Record<string, string> = {
online_search: '实时搜索最新信息,获取网络资料',
deep_think: '深度推理分析,解决复杂问题',
web_search: '联网搜索网页内容',
thinking: '深入思考和分析问题',
default: '点击启用此功能',
};
// 响应式工具列表 - 初始为空,等待接口加载
const tools = ref<ToolItem[]>([]);
// 当前选中的工具ID数组支持多选
const activeToolIds = ref<number[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
// 从API数据转换为前端格式
function transformApiTools(apiTools: ApiToolItem[]): ToolItem[] {
return apiTools.map((tool, index) => {
const apiName = tool.name;
return {
id: index + 1,
name: tool.description, // 使用中文描述
apiName,
icon: iconMap[apiName] || iconMap.default,
tip: tipMap[apiName] || tipMap.default,
enabled: true,
};
});
}
// 获取所有选中的工具对象
const selectedTools = computed(() => {
return tools.value.filter(tool => activeToolIds.value.includes(tool.id));
});
// 获取所有选中的API工具名称
const selectedApiTools = computed(() => {
return selectedTools.value.map(tool => tool.apiName);
});
// 点击事件处理
function handleToolClick(tool: ToolItem) {
if (!tool.enabled)
return;
const index = activeToolIds.value.indexOf(tool.id);
if (index > -1) {
activeToolIds.value.splice(index, 1);
}
else {
activeToolIds.value.push(tool.id);
}
console.log('当前选中的工具:', selectedTools.value.map(t => t.name));
console.log('对应的API名称:', selectedApiTools.value);
emitToolsUpdate();
}
// 辅助函数:检查某个工具是否被选中
function isActive(toolId: number) {
return activeToolIds.value.includes(toolId);
}
// 清空所有选中
function clearSelection() {
activeToolIds.value = [];
emitToolsUpdate();
}
// 全选已启用的工具
function selectAll() {
activeToolIds.value = tools.value
.filter(tool => tool.enabled)
.map(tool => tool.id);
emitToolsUpdate();
}
// 启用/禁用工具
function setToolEnabled(toolId: number, enabled: boolean) {
const tool = tools.value.find(t => t.id === toolId);
if (tool) {
tool.enabled = enabled;
if (!enabled && isActive(toolId)) {
const index = activeToolIds.value.indexOf(toolId);
if (index > -1) {
activeToolIds.value.splice(index, 1);
emitToolsUpdate();
}
}
}
}
// 根据API名称启用/禁用工具
function setToolEnabledByApiName(apiName: string, enabled: boolean) {
const tool = tools.value.find(t => t.apiName === apiName);
if (tool) {
setToolEnabled(tool.id, enabled);
}
}
// 通知父组件工具状态变化
function emitToolsUpdate() {
console.log('工具状态已更新:', {
selectedToolIds: activeToolIds.value,
selectedTools: selectedTools.value,
selectedApiTools: selectedApiTools.value,
});
// 触发自定义事件
emit('tools-update', {
selectedToolIds: activeToolIds.value,
selectedApiTools: selectedApiTools.value,
});
}
// 从API获取工具列表
async function getAiChatToolList() {
loading.value = true;
error.value = null;
try {
const res = await aiChatTool();
if (res.data && Array.isArray(res.data)) {
console.log('API返回的工具列表:', res.data);
// 转换API数据
const apiTools = transformApiTools(res.data);
// 更新工具列表
tools.value = apiTools;
// 默认选中第一个可用的工具
const firstEnabledTool = apiTools.find(tool => tool.enabled);
if (firstEnabledTool && activeToolIds.value.length === 0) {
activeToolIds.value = [firstEnabledTool.id];
}
console.log('加载的工具列表:', tools.value);
emitToolsUpdate();
}
else {
error.value = '接口返回数据格式不正确';
console.error('接口返回数据格式错误:', res);
}
}
catch (err) {
error.value = '获取工具列表失败,请检查网络连接';
console.error('获取工具列表失败:', err);
}
finally {
loading.value = false;
}
}
// 刷新工具列表
async function refreshTools() {
await getAiChatToolList();
}
// 组件挂载时获取工具列表
onMounted(() => {
getAiChatToolList();
});
// 暴露方法给父组件
defineExpose({
selectedTools,
selectedApiTools,
clearSelection,
selectAll,
setToolEnabled,
setToolEnabledByApiName,
refreshTools,
getAiChatToolList,
});
</script>
<template>
<div class="tools-container">
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<el-icon class="loading-icon">
<Loading />
</el-icon>
<span>加载工具中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<el-icon class="error-icon">
<ElementPlus />
</el-icon>
<span>{{ error }}</span>
<el-button type="text" class="retry-btn" @click="refreshTools">
重试
</el-button>
</div>
<!-- 空状态 -->
<div v-else-if="tools.length === 0" class="empty-state">
<span>暂无可用工具</span>
</div>
<!-- 工具按钮 -->
<div
v-for="tool in tools"
v-else
:key="tool.id"
class="tool-item"
:class="{
active: isActive(tool.id),
disabled: !tool.enabled,
}"
:title="tool.tip"
@click="handleToolClick(tool)"
>
<el-icon class="tool-icon">
<component :is="tool.icon" />
</el-icon>
<span class="tool-text">{{ tool.name }}</span>
<!-- 选中指示器 -->
<!-- <span v-if="isActive(tool.id)" class="check-indicator"></span> -->
</div>
</div>
<!-- 调试信息 -->
<div v-if="false" class="debug-info">
<div>工具数量: {{ tools.length }}</div>
<div>当前选中ID: {{ activeToolIds }}</div>
<div>选中的API工具: {{ selectedApiTools }}</div>
</div>
</template>
<style scoped lang="scss">
.tools-container {
display: flex;
flex-direction: row;
gap: 8px;
padding: 4px;
flex-wrap: wrap;
align-items: center;
min-height: 44px;
}
.loading-state,
.error-state,
.empty-state {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 14px;
color: #909399;
}
.loading-state {
.loading-icon {
animation: rotate 1s linear infinite;
}
}
.error-state {
color: #f56c6c;
.error-icon {
color: #f56c6c;
}
.retry-btn {
margin-left: 8px;
padding: 0;
height: auto;
color: #409EFF;
}
}
.empty-state {
color: #c0c4cc;
font-style: italic;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.tool-item {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 6px;
cursor: pointer;
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
background: white;
position: relative;
border-radius: 10px;
user-select: none;
&:hover {
background: rgba(0, 0, 0, 0.04);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&.active {
background: rgba(64, 158, 255, 0.1);
border-color: #409EFF;
color: #409EFF;
.tool-icon {
color: #409EFF;
}
&:hover {
background: rgba(64, 158, 255, 0.15);
}
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: white;
transform: none;
box-shadow: none;
}
}
&:active:not(.disabled) {
transform: translateY(0);
}
}
.tool-icon {
font-size: 16px;
color: #606266;
transition: color 0.2s ease;
}
.tool-text {
white-space: nowrap;
font-size: 13px;
}
.check-indicator {
margin-left: 4px;
font-weight: bold;
font-size: 12px;
color: #409EFF;
}
.debug-info {
margin-top: 10px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
font-size: 12px;
color: #666;
div {
margin-bottom: 4px;
}
}
</style>

View File

@@ -13,13 +13,13 @@ import { Sender } from 'vue-element-plus-x';
import { useRoute } from 'vue-router';
import { send } from '@/api';
import ModelSelect from '@/components/ModelSelect/index.vue';
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
import { useGuideTourStore } from '@/stores';
import { useChatStore } from '@/stores/modules/chat';
import { useFilesStore } from '@/stores/modules/files';
import { useModelStore } from '@/stores/modules/model';
import { useUserStore } from '@/stores/modules/user';
import { getUserProfilePicture, systemProfilePicture } from '@/utils/user.ts';
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
import '@/styles/github-markdown.css';
import '@/styles/yixin-markdown.scss';
@@ -284,10 +284,12 @@ async function startSSE(chatContent: string) {
// 如果有图片或文件,使用数组格式
if (contentArray.length > 1 || imageFiles.length > 0 || textFiles.length > 0) {
baseMessage.content = contentArray;
} else {
}
else {
baseMessage.content = item.content;
}
} else {
}
else {
// 其他消息保持原样
baseMessage.content = (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
@@ -391,7 +393,7 @@ function addMessage(message: string, isUser: boolean, images?: Array<{ url: stri
/**
* 处理思考链展开/收起状态变化
* @param {Object} payload - 状态变化的载荷
* @param {object} payload - 状态变化的载荷
* @param {boolean} payload.value - 展开/收起状态
* @param {ThinkingStatus} payload.status - 思考状态
*/
@@ -477,9 +479,9 @@ function handleImagePreview(url: string) {
:key="index"
class="user-file-item"
>
<el-icon class="file-icon">
<ElIcon class="file-icon">
<Document />
</el-icon>
</ElIcon>
<span class="file-name">{{ file.name }}</span>
</div>
</div>
@@ -543,6 +545,7 @@ function handleImagePreview(url: string) {
<template #prefix>
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
<FilesSelect />
<!-- < ToolList/> -->
<ModelSelect />
</div>
</template>

File diff suppressed because one or more lines are too long

View File

@@ -79,6 +79,7 @@ declare module 'vue' {
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default']
TokenFormDialog: typeof import('./../src/components/userPersonalCenter/components/TokenFormDialog.vue')['default']
ToolList: typeof import('./../src/components/ToolList/index.vue')['default']
UsageStatistics: typeof import('./../src/components/userPersonalCenter/components/UsageStatistics.vue')['default']
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']

View File

@@ -7,7 +7,6 @@ interface ImportMetaEnv {
readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string;
readonly VITE_FILE_UPLOAD_API: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string;
}