Files
zclaw_openfang/admin-v2/src/pages/AgentTemplates.tsx
iven ba2c6a6105 fix(saas): P1 审计修复 — 连接池断路器 + Worker重试 + XSS防护 + 状态机SQL解析器
P1 修复内容:
- F7: health handler 连接池容量检查 (80%阈值返回503 degraded)
- F9: SSE spawned task 并发限制 (Semaphore 16 permits)
- F10: Key Pool 单次 JOIN 查询优化 (消除 N+1)
- F12: CORS panic → 配置错误
- F14: 连接池使用率计算修正 (ratio = used*100/total)
- F15: SQL 迁移解析器替换为状态机 (支持 $$, DO $body$, 存储过程)
- Worker 重试机制: 失败任务通过 mpsc channel 重新入队
- DOMPurify XSS 防护 (PipelineResultPreview)
- Admin V2: ErrorBoundary + SWR全局配置 + 请求优化
2026-03-30 14:21:39 +08:00

191 lines
7.2 KiB
TypeScript

// ============================================================
// Agent 模板管理
// ============================================================
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, message, Tag, Modal, Form, Input, Select, InputNumber, Space, Popconfirm, Descriptions } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { agentTemplateService } from '@/services/agent-templates'
import type { AgentTemplate } from '@/types'
const { TextArea } = Input
const sourceLabels: Record<string, string> = { builtin: '内置', custom: '自定义' }
const visibilityLabels: Record<string, string> = { public: '公开', team: '团队', private: '私有' }
const statusLabels: Record<string, string> = { active: '活跃', archived: '已归档' }
const statusColors: Record<string, string> = { active: 'green', archived: 'default' }
export default function AgentTemplates() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const [detailRecord, setDetailRecord] = useState<AgentTemplate | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['agent-templates'],
queryFn: ({ signal }) => agentTemplateService.list(signal),
})
const createMutation = useMutation({
mutationFn: (data: Parameters<typeof agentTemplateService.create>[0]) =>
agentTemplateService.create(data),
onSuccess: () => {
message.success('创建成功')
queryClient.invalidateQueries({ queryKey: ['agent-templates'] })
setModalOpen(false)
form.resetFields()
},
onError: (err: Error) => message.error(err.message || '创建失败'),
})
const archiveMutation = useMutation({
mutationFn: (id: string) => agentTemplateService.archive(id),
onSuccess: () => {
message.success('已归档')
queryClient.invalidateQueries({ queryKey: ['agent-templates'] })
},
onError: (err: Error) => message.error(err.message || '归档失败'),
})
const columns: ProColumns<AgentTemplate>[] = [
{ title: '名称', dataIndex: 'name', width: 160 },
{ title: '分类', dataIndex: 'category', width: 100 },
{ title: '模型', dataIndex: 'model', width: 140, render: (_, r) => r.model || '-' },
{
title: '来源',
dataIndex: 'source',
width: 80,
render: (_, r) => <Tag>{sourceLabels[r.source] || r.source}</Tag>,
},
{
title: '可见性',
dataIndex: 'visibility',
width: 80,
render: (_, r) => <Tag color="blue">{visibilityLabels[r.visibility] || r.visibility}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status] || r.status}</Tag>,
},
{ title: '版本', dataIndex: 'current_version', width: 70 },
{
title: '操作',
width: 180,
render: (_, record) => (
<Space>
<Button size="small" onClick={() => setDetailRecord(record)}></Button>
{record.status === 'active' && (
<Popconfirm title="确定归档此模板?" onConfirm={() => archiveMutation.mutate(record.id)}>
<Button size="small" danger></Button>
</Popconfirm>
)}
</Space>
),
},
]
const handleCreate = async () => {
const values = await form.validateFields()
createMutation.mutate(values)
}
return (
<div>
<ProTable<AgentTemplate>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true) }}>
</Button>,
]}
pagination={{
total: data?.total ?? 0,
pageSize: data?.page_size ?? 20,
current: data?.page ?? 1,
showSizeChanger: false,
}}
/>
<Modal
title="新建 Agent 模板"
open={modalOpen}
onOk={handleCreate}
onCancel={() => { setModalOpen(false); form.resetFields() }}
confirmLoading={createMutation.isPending}
width={640}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="描述">
<TextArea rows={2} />
</Form.Item>
<Form.Item name="category" label="分类">
<Input placeholder="如 assistant, tool" />
</Form.Item>
<Form.Item name="model" label="默认模型">
<Input placeholder="如 gpt-4o" />
</Form.Item>
<Form.Item name="system_prompt" label="系统提示词">
<TextArea rows={4} />
</Form.Item>
<Form.Item name="temperature" label="Temperature">
<InputNumber min={0} max={2} step={0.1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="max_tokens" label="最大 Token">
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="visibility" label="可见性">
<Select options={[
{ value: 'public', label: '公开' },
{ value: 'team', label: '团队' },
{ value: 'private', label: '私有' },
]} />
</Form.Item>
</Form>
</Modal>
<Modal
title="模板详情"
open={!!detailRecord}
onCancel={() => setDetailRecord(null)}
footer={null}
width={640}
>
{detailRecord && (
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="名称">{detailRecord.name}</Descriptions.Item>
<Descriptions.Item label="分类">{detailRecord.category}</Descriptions.Item>
<Descriptions.Item label="模型">{detailRecord.model || '-'}</Descriptions.Item>
<Descriptions.Item label="来源">{sourceLabels[detailRecord.source]}</Descriptions.Item>
<Descriptions.Item label="可见性">{visibilityLabels[detailRecord.visibility]}</Descriptions.Item>
<Descriptions.Item label="状态">{statusLabels[detailRecord.status]}</Descriptions.Item>
<Descriptions.Item label="描述" span={2}>{detailRecord.description || '-'}</Descriptions.Item>
<Descriptions.Item label="系统提示词" span={2}>
<div style={{ whiteSpace: 'pre-wrap', maxHeight: 200, overflow: 'auto' }}>
{detailRecord.system_prompt || '-'}
</div>
</Descriptions.Item>
<Descriptions.Item label="工具" span={2}>
{detailRecord.tools?.map((t) => <Tag key={t}>{t}</Tag>) || '-'}
</Descriptions.Item>
<Descriptions.Item label="能力" span={2}>
{detailRecord.capabilities?.map((c) => <Tag key={c} color="blue">{c}</Tag>) || '-'}
</Descriptions.Item>
</Descriptions>
)}
</Modal>
</div>
)
}