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.
 
 
 
 

396 lines
8.8 KiB

<script setup lang="ts">
interface AgentToolRow {
id: string
name: string
slug: string
description: string
type: string
config: string
enabled: number
sortOrder: number
createdAt: string
updatedAt: string
}
const { $toast } = useNuxtApp()
const { data, refresh } = await useHttpFetch("/api/agent-tools")
const toolList = computed<AgentToolRow[]>(() => (data.value as any)?.data ?? (data.value as any) ?? [])
const showFormModal = ref(false)
const editingTool = ref<AgentToolRow | null>(null)
const showExecuteModal = ref(false)
const executingTool = ref<AgentToolRow | null>(null)
const confirmShow = ref(false)
const confirmMsg = ref('')
let confirmAction: (() => void) | null = null
function showConfirm(msg: string, action: () => void) {
confirmMsg.value = msg
confirmAction = action
confirmShow.value = true
}
function onConfirm() {
confirmAction?.()
confirmShow.value = false
}
function statusBadge(enabled: number) {
return enabled
? { label: "启用", class: "badge-active" }
: { label: "禁用", class: "badge-paused" }
}
async function handleDelete(id: string) {
showConfirm("确定删除此工具?", async () => {
try {
await $fetch(`/api/agent-tools/${id}`, { method: "DELETE" })
$toast.success("删除成功")
refresh()
} catch (e: any) {
$toast.error(e?.data?.message ?? "删除失败")
}
})
}
async function handleToggle(tool: AgentToolRow) {
try {
await $fetch(`/api/agent-tools/${tool.id}`, {
method: "PUT",
body: { enabled: !tool.enabled },
})
$toast.success(tool.enabled ? "已禁用" : "已启用")
refresh()
} catch (e: any) {
$toast.error(e?.data?.message ?? "操作失败")
}
}
function openEdit(tool: AgentToolRow) {
editingTool.value = tool
showFormModal.value = true
}
function openCreate() {
editingTool.value = null
showFormModal.value = true
}
function openExecute(tool: AgentToolRow) {
executingTool.value = tool
showExecuteModal.value = true
}
function onFormClose() {
showFormModal.value = false
editingTool.value = null
refresh()
}
function onExecuteClose() {
showExecuteModal.value = false
executingTool.value = null
}
</script>
<template>
<div class="agent-tools-page">
<header class="page-header">
<div class="page-header-content">
<h1 class="page-title">Agent 工具</h1>
<p class="page-subtitle">管理 LLM 工具调用框架中的工具定义</p>
</div>
<button class="btn-primary" @click="openCreate">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19" stroke-linecap="round" />
<line x1="5" y1="12" x2="19" y2="12" stroke-linecap="round" />
</svg>
创建工具
</button>
</header>
<div class="table-card">
<table class="data-table">
<thead>
<tr>
<th class="text-left">名称</th>
<th class="text-left">Slug</th>
<th class="text-left">类型</th>
<th class="text-left">状态</th>
<th class="text-left">排序</th>
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="toolList.length === 0">
<td colspan="6" class="text-center empty-cell">暂无工具</td>
</tr>
<tr v-for="t in toolList" :key="t.id" class="table-row">
<td class="name-cell">
<div class="name-title">{{ t.name }}</div>
<div class="name-desc">{{ t.description }}</div>
</td>
<td class="slug-cell">{{ t.slug }}</td>
<td class="type-cell">
<span :class="['type-badge', `type-${t.type}`]">{{ t.type }}</span>
</td>
<td class="status-cell">
<span :class="['status-badge', statusBadge(t.enabled).class]">
{{ statusBadge(t.enabled).label }}
</span>
</td>
<td class="sort-cell">{{ t.sortOrder }}</td>
<td class="actions-cell">
<div class="action-buttons">
<button class="action-btn" @click="openExecute(t)">测试</button>
<button class="action-btn" @click="handleToggle(t)">
{{ t.enabled ? '禁用' : '启用' }}
</button>
<button class="action-btn" @click="openEdit(t)">编辑</button>
<button class="action-btn action-btn-danger" @click="handleDelete(t.id)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<BoDialog v-model:show="confirmShow" :mask-can-close="true">
<div class="rounded-lg bg-white p-6 shadow-xl border border-[#e6dfd8] confirm-dialog">
<h3 class="text-[16px] font-medium text-[#141413]">确认操作</h3>
<p class="mt-3 text-[14px] text-[#3d3d3a]">{{ confirmMsg }}</p>
<div class="mt-6 flex justify-end gap-2">
<button class="px-4 py-2 bg-[#faf9f5] border border-[#e6dfd8] rounded-lg text-[14px] hover:bg-white transition-colors" @click="confirmShow = false">取消</button>
<button class="px-4 py-2 bg-[#cc785c] text-white rounded-lg text-[14px] hover:bg-[#a9583e] transition-colors" @click="onConfirm">确定</button>
</div>
</div>
</BoDialog>
<AgentToolFormModal
v-if="showFormModal"
:tool="editingTool"
@close="onFormClose"
/>
<AgentToolExecuteModal
v-if="showExecuteModal"
:tool="executingTool"
@close="onExecuteClose"
/>
</div>
</template>
<style scoped>
.agent-tools-page {
padding: 40px;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 32px;
}
.page-title {
font-family: var(--font-display);
font-size: 32px;
font-weight: 400;
color: var(--color-ink);
margin: 0;
}
.page-subtitle {
font-size: 15px;
color: var(--color-muted);
margin: 4px 0 0;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.btn-primary:hover {
background: var(--color-primary-active);
}
.btn-primary svg {
width: 16px;
height: 16px;
}
.table-card {
background: var(--color-surface-card);
border-radius: 12px;
overflow: hidden;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
padding: 14px 16px;
font-size: 12px;
font-weight: 600;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--color-surface-soft);
border-bottom: 1px solid var(--color-hairline);
text-align: left;
}
.data-table td {
padding: 14px 16px;
font-size: 14px;
color: var(--color-body-strong);
border-bottom: 1px solid var(--color-hairline);
}
.table-row:last-child td {
border-bottom: none;
}
.table-row:hover td {
background: var(--color-surface-soft);
}
.empty-cell {
text-align: center;
color: var(--color-muted);
padding: 40px 16px;
}
.name-cell {
font-weight: 500;
color: var(--color-ink);
}
.name-desc {
font-size: 12px;
color: var(--color-muted);
font-weight: 400;
margin-top: 2px;
}
.slug-cell {
font-family: var(--font-mono, monospace);
font-size: 13px;
color: var(--color-muted);
}
.type-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: var(--color-surface-soft);
color: var(--color-muted);
}
.type-fetch {
background: #e8f0fe;
color: #1a56db;
}
.type-calculator {
background: #f3e8ff;
color: #7c3aed;
}
.type-datetime {
background: #fef3c7;
color: #b45309;
}
.type-uuid {
background: #d1fae5;
color: #059669;
}
.type-base64 {
background: #ffe4e6;
color: #e11d48;
}
.type-json-formatter {
background: #e0f2fe;
color: #0369a1;
}
.type-regex-tester {
background: #fce7f3;
color: #be185d;
}
.status-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-active {
background: #e6f4ea;
color: #137333;
}
.badge-paused {
background: #fef7e0;
color: #b06000;
}
.sort-cell {
color: var(--color-muted);
font-size: 13px;
}
.actions-cell {
text-align: right;
}
.action-buttons {
display: inline-flex;
gap: 6px;
}
.action-btn {
padding: 4px 12px;
font-size: 13px;
border: 1px solid var(--color-hairline);
border-radius: 6px;
background: var(--color-surface-card);
color: var(--color-body-strong);
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: var(--color-surface-soft);
border-color: var(--color-primary);
color: var(--color-primary);
}
.action-btn-danger:hover {
background: #fef0f0;
border-color: #e53e3e;
color: #e53e3e;
}
</style>