feat: 添加工具市场前端页面
- 新增 Tools.vue 页面:工具列表、分类筛选、搜索、新建/编辑/删除/测试 - 新增路由 /tools - 导航菜单添加"工具市场"入口 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,10 @@
|
|||||||
<el-icon><Connection /></el-icon>
|
<el-icon><Connection /></el-icon>
|
||||||
<span>数据源管理</span>
|
<span>数据源管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="tools" @click="router.push('/tools')">
|
||||||
|
<el-icon><Tools /></el-icon>
|
||||||
|
<span>工具市场</span>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item index="model-configs" @click="router.push('/model-configs')">
|
<el-menu-item index="model-configs" @click="router.push('/model-configs')">
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
<span>模型配置管理</span>
|
<span>模型配置管理</span>
|
||||||
@@ -90,7 +94,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid, DataAnalysis } from '@element-plus/icons-vue'
|
import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid, DataAnalysis, Tools } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -104,6 +108,7 @@ const activeMenu = computed(() => {
|
|||||||
if (route.path === '/agents' || route.path.startsWith('/agents/')) return 'agents'
|
if (route.path === '/agents' || route.path.startsWith('/agents/')) return 'agents'
|
||||||
if (route.path === '/executions' || route.path.startsWith('/executions/')) return 'executions'
|
if (route.path === '/executions' || route.path.startsWith('/executions/')) return 'executions'
|
||||||
if (route.path === '/data-sources') return 'data-sources'
|
if (route.path === '/data-sources') return 'data-sources'
|
||||||
|
if (route.path === '/tools') return 'tools'
|
||||||
if (route.path === '/model-configs') return 'model-configs'
|
if (route.path === '/model-configs') return 'model-configs'
|
||||||
if (route.path === '/node-templates') return 'node-templates'
|
if (route.path === '/node-templates') return 'node-templates'
|
||||||
if (route.path === '/permissions') return 'permissions'
|
if (route.path === '/permissions') return 'permissions'
|
||||||
@@ -127,6 +132,8 @@ const handleMenuSelect = (key: string) => {
|
|||||||
router.push('/executions')
|
router.push('/executions')
|
||||||
} else if (key === 'data-sources') {
|
} else if (key === 'data-sources') {
|
||||||
router.push('/data-sources')
|
router.push('/data-sources')
|
||||||
|
} else if (key === 'tools') {
|
||||||
|
router.push('/tools')
|
||||||
} else if (key === 'model-configs') {
|
} else if (key === 'model-configs') {
|
||||||
router.push('/model-configs')
|
router.push('/model-configs')
|
||||||
} else if (key === 'node-templates') {
|
} else if (key === 'node-templates') {
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/NodeTemplates.vue'),
|
component: () => import('@/views/NodeTemplates.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tools',
|
||||||
|
name: 'tools',
|
||||||
|
component: () => import('@/views/Tools.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/agent-chat',
|
path: '/agent-chat',
|
||||||
name: 'agent-chat',
|
name: 'agent-chat',
|
||||||
|
|||||||
604
frontend/src/views/Tools.vue
Normal file
604
frontend/src/views/Tools.vue
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<div class="tools-page">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<el-input
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="搜索工具名称或描述..."
|
||||||
|
clearable
|
||||||
|
style="width: 280px"
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="categoryFilter" placeholder="全部分类" clearable style="width: 160px" @change="fetchTools">
|
||||||
|
<el-option
|
||||||
|
v-for="cat in categories"
|
||||||
|
:key="cat"
|
||||||
|
:label="cat"
|
||||||
|
:value="cat"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-radio-group v-model="scopeFilter" @change="fetchTools">
|
||||||
|
<el-radio-button value="public">公开工具</el-radio-button>
|
||||||
|
<el-radio-button value="mine">我的工具</el-radio-button>
|
||||||
|
<el-radio-button value="all">全部</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新建工具
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具列表 -->
|
||||||
|
<el-table :data="tools" v-loading="loading" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="名称" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="tool-name">
|
||||||
|
<span class="name-text">{{ row.name }}</span>
|
||||||
|
<el-tag v-if="row.implementation_type === 'builtin'" size="small" type="info">内置</el-tag>
|
||||||
|
<el-tag v-else-if="row.implementation_type === 'code'" size="small" type="warning">代码</el-tag>
|
||||||
|
<el-tag v-else-if="row.implementation_type === 'http'" size="small" type="success">HTTP</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="category" label="分类" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.category" size="small" effect="plain">{{ row.category }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="use_count" label="使用次数" width="100" align="center" />
|
||||||
|
<el-table-column label="操作" width="240" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="showTestDialog(row)">测试</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.implementation_type !== 'builtin'"
|
||||||
|
size="small"
|
||||||
|
@click="showEditDialog(row)"
|
||||||
|
>编辑</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
v-if="row.implementation_type !== 'builtin'"
|
||||||
|
title="确定删除此工具?"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<el-empty v-if="!loading && tools.length === 0" description="暂无工具">
|
||||||
|
<el-button type="primary" @click="showCreateDialog">创建第一个工具</el-button>
|
||||||
|
</el-empty>
|
||||||
|
|
||||||
|
<!-- 创建/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="formDialogVisible"
|
||||||
|
:title="isEditing ? '编辑工具' : '新建工具'"
|
||||||
|
width="700px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="form" label-position="top" :rules="formRules" ref="formRef">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="工具名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="唯一名称,如 translate_text" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="分类" prop="category">
|
||||||
|
<el-select v-model="form.category" placeholder="选择分类" clearable filterable allow-create style="width:100%">
|
||||||
|
<el-option v-for="cat in categories" :key="cat" :label="cat" :value="cat" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="描述" prop="description">
|
||||||
|
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="工具功能描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="实现类型" prop="implementation_type">
|
||||||
|
<el-select v-model="form.implementation_type" placeholder="选择类型" @change="onTypeChange">
|
||||||
|
<el-option label="代码 (Python)" value="code" />
|
||||||
|
<el-option label="HTTP 请求" value="http" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="是否公开">
|
||||||
|
<el-switch v-model="form.is_public" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 代码类型配置 -->
|
||||||
|
<template v-if="form.implementation_type === 'code'">
|
||||||
|
<el-form-item label="Python 代码" prop="implementation_config.source">
|
||||||
|
<el-input
|
||||||
|
v-model="form.implementation_config.source"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="def run(args): ''' args: dict, 包含 function_schema.parameters 中定义的参数 returns: Any (将被 JSON 序列化) ''' x = args.get('x', 0) y = args.get('y', 0) return x + y"
|
||||||
|
font-family="monospace"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- HTTP 类型配置 -->
|
||||||
|
<template v-if="form.implementation_type === 'http'">
|
||||||
|
<el-form-item label="请求 URL" prop="implementation_config.url">
|
||||||
|
<el-input v-model="form.implementation_config.url" placeholder="https://api.example.com/endpoint" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="请求方法" prop="implementation_config.method">
|
||||||
|
<el-select v-model="form.implementation_config.method" style="width:100%">
|
||||||
|
<el-option label="GET" value="GET" />
|
||||||
|
<el-option label="POST" value="POST" />
|
||||||
|
<el-option label="PUT" value="PUT" />
|
||||||
|
<el-option label="DELETE" value="DELETE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="超时(秒)">
|
||||||
|
<el-input-number v-model="form.implementation_config.timeout" :min="1" :max="120" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="请求头 (JSON)">
|
||||||
|
<el-input
|
||||||
|
v-model="formHeadersStr"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder='{"Authorization": "Bearer xxx", "Content-Type": "application/json"}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Parameters Schema -->
|
||||||
|
<el-divider>参数定义 (function_schema.parameters)</el-divider>
|
||||||
|
<el-form-item label="参数 JSON Schema">
|
||||||
|
<el-input
|
||||||
|
v-model="formSchemaStr"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder='{"type": "object", "properties": {"x": {"type": "integer", "description": "第一个数"}, "y": {"type": "integer", "description": "第二个数"}}, "required": ["x", "y"]}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="formDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveForm" :loading="saving">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 测试对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="testDialogVisible"
|
||||||
|
:title="'测试工具: ' + (testingTool?.name || '')"
|
||||||
|
width="650px"
|
||||||
|
>
|
||||||
|
<template v-if="testingTool">
|
||||||
|
<!-- 代码工具测试 -->
|
||||||
|
<template v-if="testingTool.implementation_type === 'code'">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="测试参数 (JSON)">
|
||||||
|
<el-input
|
||||||
|
v-model="testArgsStr"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder='{"x": 1, "y": 2}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
<!-- HTTP 工具测试 -->
|
||||||
|
<template v-else-if="testingTool.implementation_type === 'http'">
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-form-item label="URL">
|
||||||
|
<el-input v-model="testHttpUrl" :placeholder="testingTool.implementation_config?.url || ''" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="方法">
|
||||||
|
<el-select v-model="testHttpMethod" style="width:100%">
|
||||||
|
<el-option label="GET" value="GET" />
|
||||||
|
<el-option label="POST" value="POST" />
|
||||||
|
<el-option label="PUT" value="PUT" />
|
||||||
|
<el-option label="DELETE" value="DELETE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="请求头 (JSON)">
|
||||||
|
<el-input v-model="testHttpHeadersStr" type="textarea" :rows="3" placeholder='{"Content-Type": "application/json"}' />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求体 (JSON)">
|
||||||
|
<el-input v-model="testHttpBodyStr" type="textarea" :rows="4" placeholder='{"key": "value"}' />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="参数 (将被替换到 URL) (JSON)">
|
||||||
|
<el-input v-model="testArgsStr" type="textarea" :rows="3" placeholder='{"query": "hello"}' />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
<!-- 通用参数 -->
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="参数 (function_schema args)">
|
||||||
|
<el-input
|
||||||
|
v-model="testArgsStr"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='{"arg1": "value1"}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 测试结果 -->
|
||||||
|
<div v-if="testResult" class="test-result">
|
||||||
|
<el-divider>测试结果</el-divider>
|
||||||
|
<el-tag :type="testResult.success ? 'success' : 'danger'" effect="dark" style="margin-bottom: 10px">
|
||||||
|
{{ testResult.success ? '成功' : '失败' }}
|
||||||
|
<span v-if="testResult.elapsed_ms !== undefined"> ({{ testResult.elapsed_ms }}ms)</span>
|
||||||
|
</el-tag>
|
||||||
|
<el-input
|
||||||
|
v-model="testResultOutput"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="testDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="handleTest" :loading="testing">测试运行</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Search, Plus } from '@element-plus/icons-vue'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category?: string
|
||||||
|
function_schema: Record<string, any>
|
||||||
|
implementation_type: string
|
||||||
|
implementation_config?: Record<string, any>
|
||||||
|
is_public: boolean
|
||||||
|
use_count: number
|
||||||
|
user_id?: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
|
const tools = ref<Tool[]>([])
|
||||||
|
const categories = ref<string[]>([])
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const categoryFilter = ref('')
|
||||||
|
const scopeFilter = ref('public')
|
||||||
|
|
||||||
|
// 表单
|
||||||
|
const formDialogVisible = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const editingId = ref('')
|
||||||
|
const formRef = ref<any>(null)
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
category: '',
|
||||||
|
implementation_type: 'code',
|
||||||
|
implementation_config: { source: '', url: '', method: 'GET', timeout: 30, headers: {} },
|
||||||
|
is_public: false,
|
||||||
|
})
|
||||||
|
const formHeadersStr = ref('{}')
|
||||||
|
const formSchemaStr = ref('{"type": "object", "properties": {}, "required": []}')
|
||||||
|
|
||||||
|
// 测试
|
||||||
|
const testDialogVisible = ref(false)
|
||||||
|
const testingTool = ref<Tool | null>(null)
|
||||||
|
const testArgsStr = ref('{}')
|
||||||
|
const testHttpUrl = ref('')
|
||||||
|
const testHttpMethod = ref('GET')
|
||||||
|
const testHttpHeadersStr = ref('{}')
|
||||||
|
const testHttpBodyStr = ref('')
|
||||||
|
const testResult = ref<any>(null)
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
name: [{ required: true, message: '请输入工具名称', trigger: 'blur' }],
|
||||||
|
description: [{ required: true, message: '请输入工具描述', trigger: 'blur' }],
|
||||||
|
implementation_type: [{ required: true, message: '请选择实现类型', trigger: 'change' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const testResultOutput = computed(() => {
|
||||||
|
if (!testResult.value) return ''
|
||||||
|
const parts: string[] = []
|
||||||
|
if (testResult.value.error) parts.push(`错误: ${testResult.value.error}`)
|
||||||
|
if (testResult.value.status_code) parts.push(`状态码: ${testResult.value.status_code}`)
|
||||||
|
if (testResult.value.result !== undefined && testResult.value.result !== null) {
|
||||||
|
parts.push(`结果: ${JSON.stringify(testResult.value.result, null, 2)}`)
|
||||||
|
}
|
||||||
|
if (testResult.value.body) parts.push(`响应体: ${testResult.value.body}`)
|
||||||
|
return parts.join('\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换类型时重置配置
|
||||||
|
function onTypeChange() {
|
||||||
|
if (form.value.implementation_type === 'code') {
|
||||||
|
form.value.implementation_config = { source: '' }
|
||||||
|
} else if (form.value.implementation_type === 'http') {
|
||||||
|
form.value.implementation_config = { url: '', method: 'GET', timeout: 30 }
|
||||||
|
formHeadersStr.value = '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工具列表
|
||||||
|
async function fetchTools() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: Record<string, any> = { scope: scopeFilter.value }
|
||||||
|
if (categoryFilter.value) params.category = categoryFilter.value
|
||||||
|
if (searchQuery.value) params.search = searchQuery.value
|
||||||
|
const res = await api.get('/api/v1/tools', { params })
|
||||||
|
tools.value = res.data || []
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('获取工具列表失败', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类列表
|
||||||
|
async function fetchCategories() {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/v1/tools/categories')
|
||||||
|
categories.value = res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取分类列表失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索防抖
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function handleSearch() {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(fetchTools, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建对话框
|
||||||
|
function showCreateDialog() {
|
||||||
|
isEditing.value = false
|
||||||
|
editingId.value = ''
|
||||||
|
form.value = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
category: '',
|
||||||
|
implementation_type: 'code',
|
||||||
|
implementation_config: { source: '' },
|
||||||
|
is_public: false,
|
||||||
|
}
|
||||||
|
formHeadersStr.value = '{}'
|
||||||
|
formSchemaStr.value = '{"type": "object", "properties": {}, "required": []}'
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
function showEditDialog(row: Tool) {
|
||||||
|
isEditing.value = true
|
||||||
|
editingId.value = row.id
|
||||||
|
form.value = {
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
category: row.category || '',
|
||||||
|
implementation_type: row.implementation_type,
|
||||||
|
implementation_config: row.implementation_config ? { ...row.implementation_config } : {},
|
||||||
|
is_public: row.is_public,
|
||||||
|
}
|
||||||
|
formSchemaStr.value = JSON.stringify(row.function_schema?.parameters || { type: 'object', properties: {} }, null, 2)
|
||||||
|
formHeadersStr.value = JSON.stringify(row.implementation_config?.headers || {}, null, 2)
|
||||||
|
formDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存表单
|
||||||
|
async function handleSaveForm() {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
let headers: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
headers = JSON.parse(formHeadersStr.value || '{}')
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('请求头 JSON 格式不正确')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let params: Record<string, any> = { type: 'object', properties: {} }
|
||||||
|
try {
|
||||||
|
params = JSON.parse(formSchemaStr.value || '{}')
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('参数 Schema JSON 格式不正确')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = { ...form.value.implementation_config }
|
||||||
|
if (form.value.implementation_type === 'http') {
|
||||||
|
config.headers = headers
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: form.value.name,
|
||||||
|
description: form.value.description,
|
||||||
|
category: form.value.category || null,
|
||||||
|
implementation_type: form.value.implementation_type,
|
||||||
|
implementation_config: config,
|
||||||
|
is_public: form.value.is_public,
|
||||||
|
function_schema: {
|
||||||
|
name: form.value.name,
|
||||||
|
description: form.value.description,
|
||||||
|
parameters: params,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (isEditing.value) {
|
||||||
|
await api.put(`/api/v1/tools/${editingId.value}`, payload)
|
||||||
|
ElMessage.success('工具已更新')
|
||||||
|
} else {
|
||||||
|
await api.post('/api/v1/tools', payload)
|
||||||
|
ElMessage.success('工具已创建')
|
||||||
|
}
|
||||||
|
formDialogVisible.value = false
|
||||||
|
fetchTools()
|
||||||
|
fetchCategories()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('保存工具失败', e)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除工具
|
||||||
|
async function handleDelete(row: Tool) {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/v1/tools/${row.id}`)
|
||||||
|
ElMessage.success('工具已删除')
|
||||||
|
fetchTools()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('删除工具失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示测试对话框
|
||||||
|
function showTestDialog(row: Tool) {
|
||||||
|
testingTool.value = row
|
||||||
|
testArgsStr.value = '{}'
|
||||||
|
testResult.value = null
|
||||||
|
testHttpUrl.value = row.implementation_config?.url || ''
|
||||||
|
testHttpMethod.value = row.implementation_config?.method || 'GET'
|
||||||
|
testHttpHeadersStr.value = JSON.stringify(row.implementation_config?.headers || {}, null, 2)
|
||||||
|
testHttpBodyStr.value = ''
|
||||||
|
testDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行测试
|
||||||
|
async function handleTest() {
|
||||||
|
if (!testingTool.value) return
|
||||||
|
|
||||||
|
let args: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
args = JSON.parse(testArgsStr.value || '{}')
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('参数 JSON 格式不正确')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.value = true
|
||||||
|
testResult.value = null
|
||||||
|
try {
|
||||||
|
if (testingTool.value.implementation_type === 'code') {
|
||||||
|
const config = testingTool.value.implementation_config
|
||||||
|
const source = config?.source || ''
|
||||||
|
const res = await api.post('/api/v1/tools/test/code', { source, args })
|
||||||
|
testResult.value = res.data
|
||||||
|
} else if (testingTool.value.implementation_type === 'http') {
|
||||||
|
let headers: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
headers = JSON.parse(testHttpHeadersStr.value || '{}')
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
let body: Record<string, any> | undefined
|
||||||
|
try {
|
||||||
|
body = testHttpBodyStr.value ? JSON.parse(testHttpBodyStr.value) : undefined
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
const res = await api.post('/api/v1/tools/test/http', {
|
||||||
|
url: testHttpUrl.value,
|
||||||
|
method: testHttpMethod.value,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
args,
|
||||||
|
timeout: 30,
|
||||||
|
})
|
||||||
|
testResult.value = res.data
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
testResult.value = { success: false, error: e.message || '测试执行失败' }
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(scopeFilter, fetchTools)
|
||||||
|
watch(categoryFilter, fetchTools)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTools()
|
||||||
|
fetchCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tools-page {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-text {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user