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.
472 lines
11 KiB
472 lines
11 KiB
<script setup lang="ts">
|
|
import type { AgentRow } from "#server/service/agent/agent";
|
|
|
|
interface AgentListItem extends AgentRow {
|
|
toolCount: number;
|
|
defaultModel?: {
|
|
modelName: string;
|
|
modelId: string;
|
|
providerName: string;
|
|
providerStatus: string;
|
|
apiKeySet: boolean;
|
|
} | null;
|
|
titleModel?: {
|
|
modelName: string;
|
|
modelId: string;
|
|
providerName: string;
|
|
providerStatus: string;
|
|
apiKeySet: boolean;
|
|
} | null;
|
|
}
|
|
|
|
const { $toast } = useNuxtApp() as any;
|
|
const { data, refresh } = await useHttpFetch("/api/admin/agents");
|
|
|
|
const agentList = computed<AgentListItem[]>(() => {
|
|
const raw = data.value as any;
|
|
return raw?.data?.list ?? raw?.list ?? [];
|
|
});
|
|
|
|
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 handleToggle(agent: AgentListItem) {
|
|
try {
|
|
await $fetch(`/api/agents/${agent.slug}`, {
|
|
method: "PUT",
|
|
body: { enabled: agent.enabled ? 0 : 1 },
|
|
});
|
|
$toast?.success?.(agent.enabled ? "已禁用" : "已启用");
|
|
refresh();
|
|
} catch (e: any) {
|
|
$toast?.error?.(e?.data?.message ?? "操作失败");
|
|
}
|
|
}
|
|
|
|
async function handleSetDefault(agent: AgentListItem) {
|
|
if (agent.isDefault === 1) return;
|
|
try {
|
|
await $fetch(`/api/agents/${agent.slug}`, {
|
|
method: "PUT",
|
|
body: { isDefault: 1 },
|
|
});
|
|
$toast?.success?.("已设为默认");
|
|
refresh();
|
|
} catch (e: any) {
|
|
$toast?.error?.(e?.data?.message ?? "操作失败");
|
|
}
|
|
}
|
|
|
|
function handleDelete(agent: AgentListItem) {
|
|
if (agent.isDefault === 1) {
|
|
$toast?.error?.("不可删除默认 agent");
|
|
return;
|
|
}
|
|
showConfirm(`确定删除 agent「${agent.name}」?此操作不可恢复。`, async () => {
|
|
try {
|
|
await $fetch(`/api/agents/${agent.slug}`, { method: "DELETE" });
|
|
$toast?.success?.("删除成功");
|
|
refresh();
|
|
} catch (e: any) {
|
|
$toast?.error?.(e?.data?.message ?? "删除失败");
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-agents-page">
|
|
<header class="page-header">
|
|
<div class="page-header-content">
|
|
<h1 class="page-title">Agent 管理</h1>
|
|
<p class="page-subtitle">管理所有 Agent,包括已禁用的 Agent</p>
|
|
</div>
|
|
<NuxtLink to="/admin/agents/new" class="btn-primary">
|
|
<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>
|
|
创建 Agent
|
|
</NuxtLink>
|
|
</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-left">可调用</th>
|
|
<th class="text-left">排序</th>
|
|
<th class="text-left">Tools</th>
|
|
<th class="text-right">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="agentList.length === 0">
|
|
<td colspan="9" class="text-center empty-cell">暂无 Agent</td>
|
|
</tr>
|
|
<tr v-for="a in agentList" :key="a.id" class="table-row">
|
|
<td class="name-cell">
|
|
<div class="name-title">{{ a.name }}</div>
|
|
<div class="name-desc">{{ a.description || '—' }}</div>
|
|
</td>
|
|
<td class="slug-cell">{{ a.slug }}</td>
|
|
<td class="model-cell">
|
|
<template v-if="a.defaultModel">
|
|
<div class="model-name">{{ a.defaultModel.modelName }}</div>
|
|
<div class="model-meta">
|
|
<span class="model-provider">{{ a.defaultModel.providerName }}</span>
|
|
<span
|
|
:class="['model-badge', a.defaultModel.providerStatus === 'active' && a.defaultModel.apiKeySet ? 'model-ok' : 'model-warn']"
|
|
>
|
|
{{ !a.defaultModel.apiKeySet ? '无Key' : a.defaultModel.providerStatus !== 'active' ? '已停用' : '正常' }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<span v-else class="model-none">— 未设置 —</span>
|
|
</td>
|
|
<td class="status-cell">
|
|
<span :class="['status-badge', statusBadge(a.enabled).class]">
|
|
{{ statusBadge(a.enabled).label }}
|
|
</span>
|
|
</td>
|
|
<td class="default-cell">
|
|
<span v-if="a.isDefault === 1" class="status-badge badge-default">默认</span>
|
|
<span v-else class="status-badge badge-muted">—</span>
|
|
</td>
|
|
<td class="callable-cell">
|
|
<span :class="['status-badge', a.isCallable ? 'badge-callable' : 'badge-muted']">
|
|
{{ a.isCallable ? '可调用' : '—' }}
|
|
</span>
|
|
</td>
|
|
<td class="sort-cell">{{ a.sortOrder }}</td>
|
|
<td class="tools-cell">{{ a.toolCount }}</td>
|
|
<td class="actions-cell">
|
|
<div class="action-buttons">
|
|
<NuxtLink :to="`/admin/agents/${a.slug}`" class="action-btn">编辑</NuxtLink>
|
|
<button class="action-btn" @click="handleToggle(a)">
|
|
{{ a.enabled ? '禁用' : '启用' }}
|
|
</button>
|
|
<button
|
|
v-if="a.isDefault !== 1"
|
|
class="action-btn"
|
|
@click="handleSetDefault(a)"
|
|
>
|
|
设为默认
|
|
</button>
|
|
<button class="action-btn action-btn-danger" @click="handleDelete(a)">删除</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>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-agents-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;
|
|
text-decoration: none;
|
|
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);
|
|
}
|
|
|
|
.model-cell {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.model-name {
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.model-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.model-provider {
|
|
font-size: 12px;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.model-badge {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.model-ok {
|
|
background: #e6f4ea;
|
|
color: #137333;
|
|
}
|
|
|
|
.model-warn {
|
|
background: #fef0f0;
|
|
color: #c53030;
|
|
}
|
|
|
|
.model-none {
|
|
font-size: 12px;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.sort-cell,
|
|
.tools-cell {
|
|
color: var(--color-muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.badge-default {
|
|
background: #e8f0fe;
|
|
color: #1a56db;
|
|
}
|
|
|
|
.badge-callable {
|
|
background: #f3e8ff;
|
|
color: #7c3aed;
|
|
}
|
|
|
|
.badge-muted {
|
|
background: #f0f0f0;
|
|
color: #999;
|
|
}
|
|
|
|
.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;
|
|
text-decoration: none;
|
|
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;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.admin-agents-page {
|
|
padding: 16px;
|
|
padding-top: 56px;
|
|
}
|
|
|
|
.page-header {
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.btn-primary {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.table-card {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.data-table {
|
|
min-width: 800px;
|
|
}
|
|
|
|
.confirm-dialog {
|
|
margin: 16px;
|
|
}
|
|
}
|
|
</style>
|
|
|