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:
@@ -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'
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user