Files
zclaw_openfang/admin-v2/src/pages/Prompts.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

229 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// 提示词管理
// ============================================================
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, message, Tag, Modal, Form, Input, Select, Space, Popconfirm, Descriptions, Tabs, Typography } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { promptService } from '@/services/prompts'
import type { PromptTemplate, PromptVersion } from '@/types'
const { TextArea } = Input
const { Text } = Typography
const sourceLabels: Record<string, string> = { builtin: '内置', custom: '自定义' }
const statusLabels: Record<string, string> = { active: '活跃', deprecated: '已废弃', archived: '已归档' }
const statusColors: Record<string, string> = { active: 'green', deprecated: 'orange', archived: 'default' }
export default function Prompts() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [createOpen, setCreateOpen] = useState(false)
const [detailName, setDetailName] = useState<string | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['prompts'],
queryFn: ({ signal }) => promptService.list(signal),
})
const { data: detailData } = useQuery({
queryKey: ['prompt-detail', detailName],
queryFn: ({ signal }) => promptService.get(detailName!, signal),
enabled: !!detailName,
})
const { data: versionsData } = useQuery({
queryKey: ['prompt-versions', detailName],
queryFn: ({ signal }) => promptService.listVersions(detailName!, signal),
enabled: !!detailName,
})
const createMutation = useMutation({
mutationFn: (data: Parameters<typeof promptService.create>[0]) => promptService.create(data),
onSuccess: () => {
message.success('创建成功')
queryClient.invalidateQueries({ queryKey: ['prompts'] })
setCreateOpen(false)
form.resetFields()
},
onError: (err: Error) => message.error(err.message || '创建失败'),
})
const archiveMutation = useMutation({
mutationFn: (name: string) => promptService.archive(name),
onSuccess: () => {
message.success('已归档')
queryClient.invalidateQueries({ queryKey: ['prompts'] })
},
onError: (err: Error) => message.error(err.message || '归档失败'),
})
const rollbackMutation = useMutation({
mutationFn: ({ name, version }: { name: string; version: number }) =>
promptService.rollback(name, version),
onSuccess: () => {
message.success('回滚成功')
queryClient.invalidateQueries({ queryKey: ['prompts'] })
queryClient.invalidateQueries({ queryKey: ['prompt-detail', detailName] })
queryClient.invalidateQueries({ queryKey: ['prompt-versions', detailName] })
},
onError: (err: Error) => message.error(err.message || '回滚失败'),
})
const columns: ProColumns<PromptTemplate>[] = [
{ title: '名称', dataIndex: 'name', width: 200, render: (_, r) => <Text code>{r.name}</Text> },
{ title: '分类', dataIndex: 'category', width: 100 },
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true },
{
title: '来源',
dataIndex: 'source',
width: 80,
render: (_, r) => <Tag>{sourceLabels[r.source]}</Tag>,
},
{ title: '版本', dataIndex: 'current_version', width: 70 },
{
title: '状态',
dataIndex: 'status',
width: 90,
render: (_, r) => <Tag color={statusColors[r.status]}>{statusLabels[r.status]}</Tag>,
},
{
title: '操作',
width: 180,
render: (_, record) => (
<Space>
<Button size="small" onClick={() => setDetailName(record.name)}></Button>
{record.status === 'active' && (
<Popconfirm title="确定归档此提示词?" onConfirm={() => archiveMutation.mutate(record.name)}>
<Button size="small" danger></Button>
</Popconfirm>
)}
</Space>
),
},
]
const handleCreate = async () => {
const values = await form.validateFields()
createMutation.mutate(values)
}
const versionColumns: ProColumns<PromptVersion>[] = [
{ title: '版本', dataIndex: 'version', width: 60 },
{ title: '更新说明', dataIndex: 'changelog', width: 200, ellipsis: true },
{ title: '最低版本', dataIndex: 'min_app_version', width: 100, render: (_, r) => r.min_app_version || '-' },
{
title: '创建时间',
dataIndex: 'created_at',
width: 180,
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
},
{
title: '操作',
width: 80,
render: (_, record) => (
<Popconfirm
title={`确定回滚到版本 ${record.version}`}
onConfirm={() => detailName && rollbackMutation.mutate({ name: detailName, version: record.version })}
>
<Button size="small"></Button>
</Popconfirm>
),
},
]
return (
<div>
<ProTable<PromptTemplate>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setCreateOpen(true) }}>
</Button>,
]}
pagination={{
total: data?.total ?? 0,
pageSize: data?.page_size ?? 20,
current: data?.page ?? 1,
showSizeChanger: false,
}}
/>
<Modal
title="新建提示词"
open={createOpen}
onOk={handleCreate}
onCancel={() => { setCreateOpen(false); form.resetFields() }}
confirmLoading={createMutation.isPending}
width={640}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="唯一标识" />
</Form.Item>
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
<Input placeholder="如 system, tool" />
</Form.Item>
<Form.Item name="description" label="描述">
<TextArea rows={2} />
</Form.Item>
<Form.Item name="system_prompt" label="系统提示词" rules={[{ required: true }]}>
<TextArea rows={6} />
</Form.Item>
<Form.Item name="user_prompt_template" label="用户提示词模板">
<TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title={`提示词详情: ${detailName || ''}`}
open={!!detailName}
onCancel={() => setDetailName(null)}
footer={null}
width={800}
>
<Tabs items={[
{
key: 'info',
label: '基本信息',
children: detailData ? (
<Descriptions column={2} bordered size="small">
<Descriptions.Item label="名称">{detailData.name}</Descriptions.Item>
<Descriptions.Item label="分类">{detailData.category}</Descriptions.Item>
<Descriptions.Item label="来源">{sourceLabels[detailData.source]}</Descriptions.Item>
<Descriptions.Item label="状态">{statusLabels[detailData.status]}</Descriptions.Item>
<Descriptions.Item label="当前版本">{detailData.current_version}</Descriptions.Item>
<Descriptions.Item label="描述" span={2}>{detailData.description || '-'}</Descriptions.Item>
</Descriptions>
) : null,
},
{
key: 'versions',
label: '版本历史',
children: (
<ProTable<PromptVersion>
columns={versionColumns}
dataSource={versionsData ?? []}
rowKey="id"
search={false}
toolBarRender={false}
pagination={false}
size="small"
loading={!versionsData}
/>
),
},
]} />
</Modal>
</div>
)
}