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

View File

@@ -51,6 +51,7 @@ export default function AgentTemplates() {
}) })
const columns: ProColumns<AgentTemplate>[] = [ const columns: ProColumns<AgentTemplate>[] = [
{ title: '图标', dataIndex: 'emoji', width: 60 },
{ title: '名称', dataIndex: 'name', width: 160 }, { title: '名称', dataIndex: 'name', width: 160 },
{ title: '分类', dataIndex: 'category', width: 100 }, { title: '分类', dataIndex: 'category', width: 100 },
{ title: '模型', dataIndex: 'model', width: 140, render: (_, r) => r.model || '-' }, { title: '模型', dataIndex: 'model', width: 140, render: (_, r) => r.model || '-' },
@@ -152,6 +153,29 @@ export default function AgentTemplates() {
{ value: 'private', label: '私有' }, { value: 'private', label: '私有' },
]} /> ]} />
</Form.Item> </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> </Form>
</Modal> </Modal>

View File

@@ -19,6 +19,8 @@ export default function Providers() {
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [keyModalProviderId, setKeyModalProviderId] = useState<string | null>(null) const [keyModalProviderId, setKeyModalProviderId] = useState<string | null>(null)
const [addKeyOpen, setAddKeyOpen] = useState(false)
const [addKeyForm] = Form.useForm()
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['providers'], queryKey: ['providers'],
@@ -63,6 +65,38 @@ export default function Providers() {
onError: (err: Error) => message.error(err.message || '删除失败'), 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>[] = [ const columns: ProColumns<Provider>[] = [
{ title: '名称', dataIndex: 'display_name', width: 140 }, { title: '名称', dataIndex: 'display_name', width: 140 },
{ title: '标识', dataIndex: 'name', width: 120, render: (_, r) => <Text code>{r.name}</Text> }, { title: '标识', dataIndex: 'name', width: 120, render: (_, r) => <Text code>{r.name}</Text> },
@@ -104,6 +138,35 @@ export default function Providers() {
width: 80, width: 80,
render: (_, r) => r.is_active ? <Tag color="green"></Tag> : <Tag></Tag>, 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 () => { const handleSave = async () => {
@@ -169,7 +232,14 @@ export default function Providers() {
title="Key Pool" title="Key Pool"
open={!!keyModalProviderId} open={!!keyModalProviderId}
onCancel={() => setKeyModalProviderId(null)} onCancel={() => setKeyModalProviderId(null)}
footer={null} footer={(_, { OkBtn, CancelBtn }) => (
<Space>
<CancelBtn />
<Button type="primary" onClick={() => { addKeyForm.resetFields(); setAddKeyOpen(true) }}>
</Button>
</Space>
)}
width={700} width={700}
> >
<ProTable<ProviderKey> <ProTable<ProviderKey>
@@ -183,6 +253,36 @@ export default function Providers() {
size="small" size="small"
/> />
</Modal> </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> </div>
) )
} }

View File

@@ -8,11 +8,16 @@ export const agentTemplateService = {
get: (id: string, signal?: AbortSignal) => get: (id: string, signal?: AbortSignal) =>
request.get<AgentTemplate>(`/agent-templates/${id}`, withSignal({}, signal)).then((r) => r.data), 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: { create: (data: {
name: string; description?: string; category?: string; source?: string name: string; description?: string; category?: string; source?: string
model?: string; system_prompt?: string; tools?: string[] model?: string; system_prompt?: string; tools?: string[]
capabilities?: string[]; temperature?: number; max_tokens?: number 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) => }, signal?: AbortSignal) =>
request.post<AgentTemplate>('/agent-templates', data, withSignal({}, signal)).then((r) => r.data), request.post<AgentTemplate>('/agent-templates', data, withSignal({}, signal)).then((r) => r.data),

View File

@@ -13,6 +13,7 @@ export interface AccountPublic {
totp_enabled: boolean totp_enabled: boolean
last_login_at: string | null last_login_at: string | null
created_at: string created_at: string
llm_routing: 'relay' | 'local'
} }
/** 登录请求 */ /** 登录请求 */
@@ -227,6 +228,25 @@ export interface AgentTemplate {
current_version: number current_version: number
created_at: string created_at: string
updated_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 */ /** Provider Key */