chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
290
admin/src/app/(dashboard)/agent-templates/page.tsx
Normal file
290
admin/src/app/(dashboard)/agent-templates/page.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { AgentTemplate } from '@/lib/types'
|
||||
import { ErrorBanner, EmptyState } from '@/components/ui/state'
|
||||
import { TableSkeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export default function AgentTemplatesPage() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [error, setError] = useState('')
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading, mutate } = useSWR(
|
||||
['agentTemplates.list', page],
|
||||
() => api.agentTemplates.list({ page, page_size: 50 }),
|
||||
)
|
||||
|
||||
const templates = data?.items ?? []
|
||||
const total = data?.total ?? 0
|
||||
|
||||
const handleCreate = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const fd = new FormData(e.currentTarget)
|
||||
try {
|
||||
const tools = (fd.get('tools') as string || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
const capabilities = (fd.get('capabilities') as string || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
await api.agentTemplates.create({
|
||||
name: fd.get('name') as string,
|
||||
description: (fd.get('description') as string) || undefined,
|
||||
category: (fd.get('category') as string) || 'general',
|
||||
model: (fd.get('model') as string) || undefined,
|
||||
system_prompt: (fd.get('system_prompt') as string) || undefined,
|
||||
tools: tools.length > 0 ? tools : undefined,
|
||||
capabilities: capabilities.length > 0 ? capabilities : undefined,
|
||||
temperature: (fd.get('temperature') as string) ? parseFloat(fd.get('temperature') as string) : undefined,
|
||||
max_tokens: (fd.get('max_tokens') as string) ? parseInt(fd.get('max_tokens') as string, 10) : undefined,
|
||||
visibility: (fd.get('visibility') as string) || 'public',
|
||||
})
|
||||
setShowCreate(false)
|
||||
mutate()
|
||||
} catch {
|
||||
setError('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleArchive = async (id: string, name: string) => {
|
||||
if (!confirm(`确认归档模板 "${name}"?`)) return
|
||||
try {
|
||||
await api.agentTemplates.archive(id)
|
||||
mutate()
|
||||
} catch {
|
||||
setError('归档失败')
|
||||
}
|
||||
}
|
||||
|
||||
const statusBadge = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
active: 'bg-emerald-500/20 text-emerald-400',
|
||||
archived: 'bg-zinc-500/20 text-zinc-400',
|
||||
}
|
||||
return <span className={`px-2 py-0.5 text-xs rounded-full ${colors[status] || colors.archived}`}>{status}</span>
|
||||
}
|
||||
|
||||
const sourceBadge = (source: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
builtin: 'bg-blue-500/20 text-blue-400',
|
||||
custom: 'bg-purple-500/20 text-purple-400',
|
||||
}
|
||||
return (
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${colors[source] || ''}`}>
|
||||
{source === 'builtin' ? '内置' : '自定义'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Agent 配置模板</h1>
|
||||
<p className="text-sm text-zinc-400 mt-1">管理 Agent 配置模板,支持团队共享和一键复用</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
+ 新建模板
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <ErrorBanner message={error} onDismiss={() => setError('')} />}
|
||||
|
||||
<div className="bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800">
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">名称</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">分类</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">来源</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">模型</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">工具数</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">可见性</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">状态</th>
|
||||
<th className="text-left px-4 py-3 text-zinc-400 font-medium">更新时间</th>
|
||||
<th className="text-right px-4 py-3 text-zinc-400 font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={9}>
|
||||
<TableSkeleton rows={5} cols={9} hasToolbar={false} />
|
||||
</td>
|
||||
</tr>
|
||||
) : templates.length === 0 ? (
|
||||
<tr><td colSpan={9}><EmptyState message="暂无 Agent 模板" /></td></tr>
|
||||
) : (
|
||||
templates.map(t => (
|
||||
<tr key={t.id} className="border-b border-zinc-800/50 hover:bg-zinc-800/30">
|
||||
<td className="px-4 py-3">
|
||||
<div>
|
||||
<span className="text-white font-medium">{t.name}</span>
|
||||
{t.description && (
|
||||
<p className="text-xs text-zinc-500 mt-0.5 truncate max-w-[200px]">{t.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-zinc-400">{t.category}</td>
|
||||
<td className="px-4 py-3">{sourceBadge(t.source)}</td>
|
||||
<td className="px-4 py-3 text-zinc-300 font-mono text-xs">{t.model || '-'}</td>
|
||||
<td className="px-4 py-3 text-zinc-400">{t.tools.length}</td>
|
||||
<td className="px-4 py-3 text-zinc-400">{t.visibility}</td>
|
||||
<td className="px-4 py-3">{statusBadge(t.status)}</td>
|
||||
<td className="px-4 py-3 text-zinc-500 text-xs">
|
||||
{new Date(t.updated_at).toLocaleString('zh-CN')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => setEditingId(editingId === t.id ? null : t.id)}
|
||||
className="text-zinc-400 hover:text-white mr-2"
|
||||
>
|
||||
详情
|
||||
</button>
|
||||
{t.source === 'custom' && (
|
||||
<button
|
||||
onClick={() => handleArchive(t.id, t.name)}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
归档
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="px-4 py-2 text-xs text-zinc-500 border-t border-zinc-800">
|
||||
共 {total} 个模板
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 展开详情 */}
|
||||
{editingId && (() => {
|
||||
const t = templates.find(t => t.id === editingId)
|
||||
if (!t) return null
|
||||
return (
|
||||
<div className="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-white">{t.name} — 详情</h2>
|
||||
<button onClick={() => setEditingId(null)} className="text-zinc-400 hover:text-white text-sm">关闭</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-zinc-500">分类:</span>
|
||||
<span className="text-zinc-300">{t.category}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500">模型:</span>
|
||||
<span className="text-zinc-300 font-mono">{t.model || '未指定'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500">温度:</span>
|
||||
<span className="text-zinc-300">{t.temperature?.toFixed(2) || '默认'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500">最大 Token:</span>
|
||||
<span className="text-zinc-300">{t.max_tokens || '未限制'}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-zinc-500">工具:</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{t.tools.length > 0 ? t.tools.map(tool => (
|
||||
<span key={tool} className="px-2 py-0.5 bg-zinc-800 rounded text-xs text-zinc-300">{tool}</span>
|
||||
)) : <span className="text-zinc-600">无</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-zinc-500">能力:</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{t.capabilities.length > 0 ? t.capabilities.map(cap => (
|
||||
<span key={cap} className="px-2 py-0.5 bg-blue-500/10 rounded text-xs text-blue-400">{cap}</span>
|
||||
)) : <span className="text-zinc-600">无</span>}
|
||||
</div>
|
||||
</div>
|
||||
{t.system_prompt && (
|
||||
<div className="col-span-2">
|
||||
<span className="text-zinc-500">系统提示词:</span>
|
||||
<pre className="text-xs text-zinc-400 bg-zinc-800/50 rounded p-2 mt-1 overflow-x-auto max-h-32">
|
||||
{t.system_prompt.substring(0, 500)}{t.system_prompt.length > 500 ? '...' : ''}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Create Modal */}
|
||||
{showCreate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<form onSubmit={handleCreate} className="bg-zinc-900 rounded-xl border border-zinc-700 p-6 w-full max-w-lg space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
<h2 className="text-lg font-semibold text-white">新建 Agent 模板</h2>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">名称 *</label>
|
||||
<input name="name" required className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="my_agent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">描述</label>
|
||||
<input name="description" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="可选" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">分类</label>
|
||||
<select name="category" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm">
|
||||
<option value="general">通用</option>
|
||||
<option value="coding">编程</option>
|
||||
<option value="research">研究</option>
|
||||
<option value="creative">创意</option>
|
||||
<option value="assistant">助手</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">模型</label>
|
||||
<input name="model" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="如 glm-4-plus" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">系统提示词</label>
|
||||
<textarea name="system_prompt" rows={4} className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm font-mono" placeholder="Agent 系统提示词" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">工具(逗号分隔)</label>
|
||||
<input name="tools" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="browser, file_system, code_execute" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">能力(逗号分隔)</label>
|
||||
<input name="capabilities" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="streaming, vision, function_calling" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">温度</label>
|
||||
<input name="temperature" type="number" step="0.1" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="默认" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">最大 Token</label>
|
||||
<input name="max_tokens" type="number" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm" placeholder="不限" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-zinc-400 mb-1">可见性</label>
|
||||
<select name="visibility" className="w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white text-sm">
|
||||
<option value="public">公开</option>
|
||||
<option value="team">团队</option>
|
||||
<option value="private">私有</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button type="button" onClick={() => setShowCreate(false)} className="px-4 py-2 bg-zinc-700 text-white rounded-lg hover:bg-zinc-600 text-sm">取消</button>
|
||||
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user