Merge remote-tracking branch 'origin/ai-hub' into ai-hub
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
416
Yi.Ai.Vue3/src/components/ToolList/index.vue
Normal file
416
Yi.Ai.Vue3/src/components/ToolList/index.vue
Normal 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>
|
||||
@@ -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
1
Yi.Ai.Vue3/types/components.d.ts
vendored
1
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -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']
|
||||
|
||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user