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
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>
|