From dd854479eb4d8b38a4a71b4c11b875ad88270090 Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 14 Apr 2026 17:48:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=89=E7=AB=AF=E8=81=94=E8=B0=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=202=20P1=20+=202=20P2=20+=204=20P3=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-07: billing get_or_create_usage 同步 max_* 列到当前计划限额 P1-08: relay handler 增加直接配额检查 (relay_requests/input/output_tokens) P2-09: relay failover 成功后记录 tokens 并标记 completed P2-10: Tauri agentStore saas-relay 模式下从 SaaS API 获取真实用量 P2-14: super_admin 合成 subscription + check_quota 放行 P3-19: 新建 ApiKeys.tsx 页面替代 ModelServices 路由 P3-15: antd destroyOnClose → destroyOnHidden (3处) P3-16: ProTable onSearch → onSubmit (2处) --- admin-v2/src/pages/ApiKeys.tsx | 169 ++++++++++++++++++++++ admin-v2/src/pages/Industries.tsx | 6 +- admin-v2/src/pages/Knowledge.tsx | 2 +- admin-v2/src/pages/ScheduledTasks.tsx | 2 +- admin-v2/src/router/index.tsx | 2 +- crates/zclaw-saas/src/billing/handlers.rs | 16 +- crates/zclaw-saas/src/billing/service.rs | 26 +++- crates/zclaw-saas/src/middleware.rs | 8 +- crates/zclaw-saas/src/relay/handlers.rs | 12 ++ crates/zclaw-saas/src/relay/service.rs | 11 ++ desktop/src/store/agentStore.ts | 17 +++ 11 files changed, 259 insertions(+), 12 deletions(-) create mode 100644 admin-v2/src/pages/ApiKeys.tsx diff --git a/admin-v2/src/pages/ApiKeys.tsx b/admin-v2/src/pages/ApiKeys.tsx new file mode 100644 index 0000000..1761231 --- /dev/null +++ b/admin-v2/src/pages/ApiKeys.tsx @@ -0,0 +1,169 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Button, message, Tag, Modal, Form, Input, InputNumber, Select, Space, Popconfirm, Typography } from 'antd' +import { PlusOutlined, CopyOutlined } from '@ant-design/icons' +import { ProTable } from '@ant-design/pro-components' +import type { ProColumns } from '@ant-design/pro-components' +import { apiKeyService } from '@/services/api-keys' +import type { TokenInfo } from '@/types' + +const { Text, Paragraph } = Typography + +const PERMISSION_OPTIONS = [ + { label: 'Relay Chat', value: 'relay:use' }, + { label: 'Knowledge Read', value: 'knowledge:read' }, + { label: 'Knowledge Write', value: 'knowledge:write' }, + { label: 'Agent Read', value: 'agent:read' }, + { label: 'Agent Write', value: 'agent:write' }, +] + +export default function ApiKeys() { + const queryClient = useQueryClient() + const [form] = Form.useForm() + const [createOpen, setCreateOpen] = useState(false) + const [newToken, setNewToken] = useState(null) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + + const { data, isLoading } = useQuery({ + queryKey: ['api-keys', page, pageSize], + queryFn: ({ signal }) => apiKeyService.list({ page, page_size: pageSize }, signal), + }) + + const createMutation = useMutation({ + mutationFn: (values: { name: string; expires_days?: number; permissions: string[] }) => + apiKeyService.create(values), + onSuccess: (result: TokenInfo) => { + message.success('API 密钥创建成功') + if (result.token) { + setNewToken(result.token) + } + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) + form.resetFields() + }, + onError: (err: Error) => message.error(err.message || '创建失败'), + }) + + const revokeMutation = useMutation({ + mutationFn: (id: string) => apiKeyService.revoke(id), + onSuccess: () => { + message.success('密钥已吊销') + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) + }, + onError: (err: Error) => message.error(err.message || '吊销失败'), + }) + + const handleCreate = async () => { + const values = await form.validateFields() + createMutation.mutate(values) + } + + const columns: ProColumns[] = [ + { title: '名称', dataIndex: 'name', width: 180 }, + { + title: '前缀', + dataIndex: 'token_prefix', + width: 120, + render: (val: string) => {val}..., + }, + { + title: '权限', + dataIndex: 'permissions', + width: 240, + render: (perms: string[]) => + perms?.map((p) => {p}) || '-', + }, + { + title: '最后使用', + dataIndex: 'last_used_at', + width: 180, + render: (val: string) => (val ? new Date(val).toLocaleString() : 从未使用), + }, + { + title: '过期时间', + dataIndex: 'expires_at', + width: 180, + render: (val: string) => + val ? new Date(val).toLocaleString() : 永不过期, + }, + { + title: '创建时间', + dataIndex: 'created_at', + width: 180, + render: (val: string) => new Date(val).toLocaleString(), + }, + { + title: '操作', + width: 100, + render: (_: unknown, record: TokenInfo) => ( + revokeMutation.mutate(record.id)} + > + + + ), + }, + ] + + return ( +
+ + columns={columns} + dataSource={data?.items || []} + loading={isLoading} + rowKey="id" + search={false} + pagination={{ + current: page, + pageSize, + total: data?.total || 0, + onChange: (p, ps) => { setPage(p); setPageSize(ps) }, + }} + toolBarRender={() => [ + , + ]} + /> + + { setCreateOpen(false); setNewToken(null); form.resetFields() }} + confirmLoading={createMutation.isPending} + destroyOnHidden + > + {newToken ? ( +
+ + 请立即复制密钥,关闭后将无法再次查看。 + + + {newToken} +
+ ) : ( +
+ + + + + + + +