chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View 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>
)
}