You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

344 lines
12 KiB

<template>
<div class="custom-tool-manager">
<el-page-header @back="$router.back()" content="自定义API工具管理" />
<el-card style="margin-top: 20px">
<div style="display: flex; gap: 12px; margin-bottom: 16px">
<el-button type="primary" @click="showImportDialog = true">
<el-icon><Upload /></el-icon> OpenAPI
</el-button>
<el-button type="success" @click="showCreateDialog = true">
<el-icon><Plus /></el-icon>
</el-button>
</div>
<el-table :data="tools" v-loading="loading" stripe>
<el-table-column prop="name" label="工具名称" min-width="160" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="method" label="方法" width="80">
<template #default="{ row }">
<el-tag :type="methodTagType(row.method)" size="small">{{ row.method }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip />
<el-table-column prop="auth_type" label="认证" width="100">
<template #default="{ row }">
<el-tag :type="row.auth_type === 'none' ? 'info' : 'warning'" size="small">
{{ row.auth_type === 'api_key' ? 'API Key' : row.auth_type === 'bearer' ? 'Bearer' : '无' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="170">
<template #default="{ row }">
{{ formatTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openTestDialog(row)">测试</el-button>
<el-button size="small" link @click="openEditDialog(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 导入OpenAPI对话框 -->
<el-dialog v-model="showImportDialog" title="导入 OpenAPI 工具" width="500px">
<el-form :model="importForm" label-width="120px">
<el-form-item label="OpenAPI URL">
<el-input v-model="importForm.openapi_url" placeholder="https://petstore.swagger.io/v2/swagger.json" />
</el-form-item>
<el-form-item label="Base URL 覆盖">
<el-input v-model="importForm.base_url_override" placeholder="可选,覆盖API基础URL" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showImportDialog = false">取消</el-button>
<el-button type="primary" @click="handleImport" :loading="importing">导入</el-button>
</template>
</el-dialog>
<!-- 创建/编辑对话框 -->
<el-dialog v-model="showCreateDialog" :title="editingTool ? '编辑工具' : '创建自定义工具'" width="600px">
<el-form :model="createForm" label-width="120px">
<el-form-item label="工具名称" required>
<el-input v-model="createForm.name" placeholder="如: get_weather" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="createForm.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="请求方法">
<el-select v-model="createForm.method">
<el-option label="GET" value="GET" />
<el-option label="POST" value="POST" />
<el-option label="PUT" value="PUT" />
<el-option label="PATCH" value="PATCH" />
<el-option label="DELETE" value="DELETE" />
</el-select>
</el-form-item>
<el-form-item label="API端点URL" required>
<el-input v-model="createForm.endpoint_url" placeholder="https://api.example.com" />
</el-form-item>
<el-form-item label="路径">
<el-input v-model="createForm.path" placeholder="/v1/weather" />
</el-form-item>
<el-form-item label="认证方式">
<el-select v-model="createForm.auth_type">
<el-option label="无" value="none" />
<el-option label="API Key" value="api_key" />
<el-option label="Bearer Token" value="bearer" />
</el-select>
</el-form-item>
<template v-if="createForm.auth_type === 'api_key'">
<el-form-item label="Key名称">
<el-input v-model="createForm.auth_config.name" placeholder="X-API-Key" />
</el-form-item>
<el-form-item label="Key值">
<el-input v-model="createForm.auth_config.key" type="password" placeholder="your-api-key" show-password />
</el-form-item>
<el-form-item label="位置">
<el-select v-model="createForm.auth_config.location">
<el-option label="Header" value="header" />
<el-option label="Query" value="query" />
</el-select>
</el-form-item>
</template>
<template v-if="createForm.auth_type === 'bearer'">
<el-form-item label="Token">
<el-input v-model="createForm.auth_config.token" type="password" placeholder="bearer-token" show-password />
</el-form-item>
</template>
<el-form-item label="参数 Schema (JSON)">
<el-input v-model="createForm.schema_json_str" type="textarea" :rows="5" placeholder='{"type":"object","properties":{"city":{"type":"string","description":"城市名称"}}}' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saving">保存</el-button>
</template>
</el-dialog>
<!-- 测试对话框 -->
<el-dialog v-model="showTestDialog" title="测试工具" width="700px">
<template v-if="testTool">
<el-descriptions :column="2" border style="margin-bottom: 16px">
<el-descriptions-item label="名称">{{ testTool.name }}</el-descriptions-item>
<el-descriptions-item label="方法">
<el-tag :type="methodTagType(testTool.method)">{{ testTool.method }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="URL" :span="2">{{ testTool.endpoint_url }}{{ testTool.path }}</el-descriptions-item>
</el-descriptions>
<el-form label-width="100px">
<el-form-item label="请求参数 (JSON)">
<el-input v-model="testParamsStr" type="textarea" :rows="4" placeholder='{"city":"beijing"}' />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleTest" :loading="testing">
<el-icon><Connection /></el-icon>
</el-button>
</el-form-item>
</el-form>
<div v-if="testResult !== null" style="margin-top: 16px">
<el-divider />
<h4>测试结果:</h4>
<el-input v-model="testResult" type="textarea" :rows="10" readonly />
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Upload, Plus, Connection } from '@element-plus/icons-vue'
import api from '@/api'
const tools = ref<any[]>([])
const loading = ref(false)
const importing = ref(false)
const saving = ref(false)
const testing = ref(false)
const showImportDialog = ref(false)
const showCreateDialog = ref(false)
const showTestDialog = ref(false)
const editingTool = ref<any>(null)
const testTool = ref<any>(null)
const testParamsStr = ref('{}')
const testResult = ref<string | null>(null)
const importForm = reactive({
openapi_url: '',
base_url_override: '',
})
const createForm = reactive<any>({
name: '',
description: '',
method: 'GET',
endpoint_url: '',
path: '',
auth_type: 'none',
auth_config: { name: 'X-API-Key', key: '', location: 'header', token: '' },
schema_json_str: '{}',
})
function methodTagType(method: string) {
const map: Record<string, string> = { GET: '', POST: 'primary', PUT: 'warning', PATCH: 'info', DELETE: 'danger' }
return map[method] || ''
}
function formatTime(t: string) {
if (!t) return ''
return new Date(t).toLocaleString('zh-CN')
}
async function fetchTools() {
loading.value = true
try {
const res = await api.get('/custom-tools/')
tools.value = (res as any)?.data || res
} catch {
ElMessage.error('获取工具列表失败')
}
loading.value = false
}
async function handleImport() {
if (!importForm.openapi_url.trim()) {
ElMessage.warning('请输入 OpenAPI URL')
return
}
importing.value = true
try {
await api.post('/custom-tools/import-openapi', {
openapi_url: importForm.openapi_url,
base_url_override: importForm.base_url_override || undefined,
})
ElMessage.success('导入成功')
showImportDialog.value = false
importForm.openapi_url = ''
importForm.base_url_override = ''
await fetchTools()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '导入失败')
}
importing.value = false
}
function openEditDialog(tool: any) {
editingTool.value = tool
createForm.name = tool.name
createForm.description = tool.description || ''
createForm.method = tool.method
createForm.endpoint_url = tool.endpoint_url
createForm.path = tool.path || ''
createForm.auth_type = tool.auth_type || 'none'
createForm.auth_config = { ...tool.auth_config } || {}
createForm.schema_json_str = JSON.stringify(tool.schema_json || {}, null, 2)
showCreateDialog.value = true
}
async function handleSave() {
let schemaJson = {}
try {
schemaJson = JSON.parse(createForm.schema_json_str)
} catch {
ElMessage.warning('参数 Schema 不是有效的 JSON')
return
}
saving.value = true
try {
const payload = {
name: createForm.name,
description: createForm.description,
method: createForm.method,
endpoint_url: createForm.endpoint_url,
path: createForm.path,
auth_type: createForm.auth_type,
auth_config: createForm.auth_config,
schema_json: schemaJson,
headers: {},
}
if (editingTool.value) {
await api.put(`/custom-tools/${editingTool.value.id}`, payload)
ElMessage.success('更新成功')
} else {
await api.post('/custom-tools/', payload)
ElMessage.success('创建成功')
}
showCreateDialog.value = false
editingTool.value = null
resetCreateForm()
await fetchTools()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '保存失败')
}
saving.value = false
}
function resetCreateForm() {
createForm.name = ''
createForm.description = ''
createForm.method = 'GET'
createForm.endpoint_url = ''
createForm.path = ''
createForm.auth_type = 'none'
createForm.auth_config = { name: 'X-API-Key', key: '', location: 'header', token: '' }
createForm.schema_json_str = '{}'
}
async function handleDelete(tool: any) {
try {
await ElMessageBox.confirm(`确认停用工具 "${tool.name}"?`, '提示', { type: 'warning' })
} catch {
return
}
try {
await api.delete(`/custom-tools/${tool.id}`)
ElMessage.success('已停用')
await fetchTools()
} catch {
ElMessage.error('操作失败')
}
}
function openTestDialog(tool: any) {
testTool.value = tool
testResult.value = null
testParamsStr.value = '{}'
showTestDialog.value = true
}
async function handleTest() {
let params = {}
try {
params = JSON.parse(testParamsStr.value)
} catch {
ElMessage.warning('参数不是有效的 JSON')
return
}
testing.value = true
try {
const res = await api.post(`/custom-tools/${testTool.value.id}/test`, params)
testResult.value = (res as any)?.data?.result || JSON.stringify(res)
} catch (e: any) {
testResult.value = e?.response?.data?.detail || '测试失败'
}
testing.value = false
}
onMounted(() => {
fetchTools()
})
</script>
<style scoped>
.custom-tool-manager {
padding: 0;
}
</style>