feat(admin-v2): add LLM routing to accounts, upgrade Key Pool CRUD, extend agent template fields

- Add llm_routing field (relay/local) to AccountPublic type and Accounts page table + edit modal
- Upgrade Providers Key Pool from read-only to full CRUD with add/toggle/delete mutations
- Extend AgentTemplate type with soul_content, scenarios, welcome_message, quick_commands,
  personality, communication_style, emoji, version, source_id fields
- Add AgentTemplateAvailable lightweight interface
- Add emoji column and extended form fields (emoji, personality, soul_content, welcome_message,
  communication_style, source_id) to Agent Templates page
- Add getFull method to agent-templates service
- Fix misplaced useState import in Accounts.tsx
This commit is contained in:
iven
2026-03-31 03:07:40 +08:00
parent 3e57fadfc9
commit 9fb9c3204c
5 changed files with 168 additions and 4 deletions

View File

@@ -2,6 +2,7 @@
// 账号管理
// ============================================================
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
@@ -88,6 +89,16 @@ export default function Accounts() {
width: 80,
render: (_, record) => record.totp_enabled ? <Tag color="green"></Tag> : <Tag></Tag>,
},
{
title: 'LLM 路由',
dataIndex: 'llm_routing',
width: 120,
valueType: 'select',
valueEnum: {
relay: { text: 'SaaS 中转', status: 'Success' },
local: { text: '本地直连', status: 'Default' },
},
},
{
title: '最后登录',
dataIndex: 'last_login_at',
@@ -161,10 +172,14 @@ export default function Accounts() {
{ value: 'user', label: '用户' },
]} />
</Form.Item>
<Form.Item name="llm_routing" label="LLM 路由模式">
<Select options={[
{ value: 'local', label: '本地直连' },
{ value: 'relay', label: 'SaaS 中转 (Token 池)' },
]} />
</Form.Item>
</Form>
</Modal>
</div>
)
}
import { useState } from 'react'

View File

@@ -51,6 +51,7 @@ export default function AgentTemplates() {
})
const columns: ProColumns<AgentTemplate>[] = [
{ title: '图标', dataIndex: 'emoji', width: 60 },
{ title: '名称', dataIndex: 'name', width: 160 },
{ title: '分类', dataIndex: 'category', width: 100 },
{ title: '模型', dataIndex: 'model', width: 140, render: (_, r) => r.model || '-' },
@@ -152,6 +153,29 @@ export default function AgentTemplates() {
{ value: 'private', label: '私有' },
]} />
</Form.Item>
<Form.Item name="emoji" label="图标">
<Input placeholder="如 🏥" />
</Form.Item>
<Form.Item name="personality" label="人格预设">
<Select options={[
{ value: 'professional', label: '专业' },
{ value: 'friendly', label: '友好' },
{ value: 'creative', label: '创意' },
{ value: 'concise', label: '简洁' },
]} allowClear placeholder="选择人格预设" />
</Form.Item>
<Form.Item name="soul_content" label="SOUL.md 人格配置">
<TextArea rows={8} />
</Form.Item>
<Form.Item name="welcome_message" label="欢迎语">
<TextArea rows={2} />
</Form.Item>
<Form.Item name="communication_style" label="沟通风格">
<TextArea rows={2} />
</Form.Item>
<Form.Item name="source_id" label="模板标识">
<Input placeholder="如 medical-assistant-v1" />
</Form.Item>
</Form>
</Modal>

View File

@@ -19,6 +19,8 @@ export default function Providers() {
const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [keyModalProviderId, setKeyModalProviderId] = useState<string | null>(null)
const [addKeyOpen, setAddKeyOpen] = useState(false)
const [addKeyForm] = Form.useForm()
const { data, isLoading } = useQuery({
queryKey: ['providers'],
@@ -63,6 +65,38 @@ export default function Providers() {
onError: (err: Error) => message.error(err.message || '删除失败'),
})
const addKeyMutation = useMutation({
mutationFn: ({ providerId, data }: { providerId: string; data: any }) =>
providerService.addKey(providerId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['provider-keys', keyModalProviderId] })
message.success('密钥已添加')
setAddKeyOpen(false)
addKeyForm.resetFields()
},
onError: () => message.error('添加失败'),
})
const toggleKeyMutation = useMutation({
mutationFn: ({ providerId, keyId, active }: { providerId: string; keyId: string; active: boolean }) =>
providerService.toggleKey(providerId, keyId, active),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['provider-keys', keyModalProviderId] })
message.success('状态已切换')
},
onError: () => message.error('切换失败'),
})
const deleteKeyMutation = useMutation({
mutationFn: ({ providerId, keyId }: { providerId: string; keyId: string }) =>
providerService.deleteKey(providerId, keyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['provider-keys', keyModalProviderId] })
message.success('密钥已删除')
},
onError: () => message.error('删除失败'),
})
const columns: ProColumns<Provider>[] = [
{ title: '名称', dataIndex: 'display_name', width: 140 },
{ title: '标识', dataIndex: 'name', width: 120, render: (_, r) => <Text code>{r.name}</Text> },
@@ -104,6 +138,35 @@ export default function Providers() {
width: 80,
render: (_, r) => r.is_active ? <Tag color="green"></Tag> : <Tag></Tag>,
},
{
title: '操作',
width: 160,
render: (_, record) => (
<Space>
<Popconfirm
title={record.is_active ? '确定禁用此密钥?' : '确定启用此密钥?'}
onConfirm={() => toggleKeyMutation.mutate({
providerId: keyModalProviderId!,
keyId: record.id,
active: !record.is_active,
})}
>
<Button size="small" type={record.is_active ? 'default' : 'primary'}>
{record.is_active ? '禁用' : '启用'}
</Button>
</Popconfirm>
<Popconfirm
title="确定删除此密钥?此操作不可恢复。"
onConfirm={() => deleteKeyMutation.mutate({
providerId: keyModalProviderId!,
keyId: record.id,
})}
>
<Button size="small" danger></Button>
</Popconfirm>
</Space>
),
},
]
const handleSave = async () => {
@@ -169,7 +232,14 @@ export default function Providers() {
title="Key Pool"
open={!!keyModalProviderId}
onCancel={() => setKeyModalProviderId(null)}
footer={null}
footer={(_, { OkBtn, CancelBtn }) => (
<Space>
<CancelBtn />
<Button type="primary" onClick={() => { addKeyForm.resetFields(); setAddKeyOpen(true) }}>
</Button>
</Space>
)}
width={700}
>
<ProTable<ProviderKey>
@@ -183,6 +253,36 @@ export default function Providers() {
size="small"
/>
</Modal>
<Modal
title="添加密钥"
open={addKeyOpen}
onOk={() => {
addKeyForm.validateFields().then((v) =>
addKeyMutation.mutate({ providerId: keyModalProviderId!, data: v })
)
}}
onCancel={() => setAddKeyOpen(false)}
confirmLoading={addKeyMutation.isPending}
>
<Form form={addKeyForm} layout="vertical">
<Form.Item name="key_label" label="标签" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="key_value" label="API Key" rules={[{ required: true }]}>
<Input.Password />
</Form.Item>
<Form.Item name="priority" label="优先级" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="max_rpm" label="最大 RPM (可选)">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="max_tpm" label="最大 TPM (可选)">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
)
}

View File

@@ -8,11 +8,16 @@ export const agentTemplateService = {
get: (id: string, signal?: AbortSignal) =>
request.get<AgentTemplate>(`/agent-templates/${id}`, withSignal({}, signal)).then((r) => r.data),
getFull: (id: string, signal?: AbortSignal) =>
request.get<AgentTemplate>(`/agent-templates/${id}/full`, withSignal({}, signal)).then((r) => r.data),
create: (data: {
name: string; description?: string; category?: string; source?: string
model?: string; system_prompt?: string; tools?: string[]
capabilities?: string[]; temperature?: number; max_tokens?: number
visibility?: string
visibility?: string; emoji?: string; personality?: string
soul_content?: string; welcome_message?: string
communication_style?: string; source_id?: string
}, signal?: AbortSignal) =>
request.post<AgentTemplate>('/agent-templates', data, withSignal({}, signal)).then((r) => r.data),

View File

@@ -13,6 +13,7 @@ export interface AccountPublic {
totp_enabled: boolean
last_login_at: string | null
created_at: string
llm_routing: 'relay' | 'local'
}
/** 登录请求 */
@@ -227,6 +228,25 @@ export interface AgentTemplate {
current_version: number
created_at: string
updated_at: string
soul_content?: string
scenarios: string[]
welcome_message?: string
quick_commands: Array<{ label: string; command: string }>
personality?: string
communication_style?: string
emoji?: string
version: number
source_id?: string
}
/** Agent 模板可用列表(轻量) */
export interface AgentTemplateAvailable {
id: string
name: string
category: string
emoji?: string
description?: string
source_id?: string
}
/** Provider Key */