feat: 新增管理后台前端项目及安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流

fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型

feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入

chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点

docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明

test: 完善配置解析单元测试
- 新增环境变量插值测试用例
This commit is contained in:
iven
2026-03-31 00:11:33 +08:00
parent 6821df5f44
commit eb956d0dce
129 changed files with 11913 additions and 863 deletions

View File

@@ -0,0 +1,228 @@
// ============================================================
// 提示词管理
// ============================================================
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>
)
}