调整文档路径

This commit is contained in:
2026-09-02 17:31:52 +08:00
parent 01b3798e02
commit 674ff09db1
43 changed files with 0 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
import { useEffect, useState } from 'react';
import {
Button,
Form,
Input,
Modal,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import { PlusOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import type { ColumnsType } from 'antd/es/table';
import {
listServices,
registerService,
revokeService,
enableService,
deleteService,
type ServiceCreateResult,
type ServiceRow,
} from '../../api';
const STATUS_COLOR: Record<string, string> = {
active: 'green',
revoked: 'red',
};
export default function Services() {
const [rows, setRows] = useState<ServiceRow[]>([]);
const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [created, setCreated] = useState<ServiceCreateResult | null>(null);
const [form] = Form.useForm();
const load = async () => {
setLoading(true);
try {
const res = await listServices();
setRows(res.services);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const onRevoke = async (id: number) => {
Modal.confirm({
title: '吊销此服务的 API Key?',
content: '吊销后该 MCP 服务将无法调用鉴权 API。吊销后可执行删除操作。',
okText: '吊销',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await revokeService(id);
message.success('已吊销');
} catch (e: any) {
message.error(e?.response?.data?.detail || '吊销失败');
} finally {
load();
}
},
});
};
const onEnable = async (id: number) => {
await enableService(id);
message.success('已启用');
load();
};
const onDelete = async (id: number) => {
Modal.confirm({
title: '删除此服务?',
content: '删除后记录将永久移除,无法恢复。',
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await deleteService(id);
message.success('已删除');
} catch (e: any) {
message.error(e?.response?.data?.detail || '删除失败');
} finally {
load();
}
},
});
};
const onCreate = async () => {
try {
const values = await form.validateFields();
const res = await registerService({
service_name: values.service_name,
service_url: values.service_url,
description: values.description,
});
setCreated(res);
setCreateOpen(false);
form.resetFields();
load();
} catch (e: any) {
if (e?.response?.data?.detail) {
message.error(e.response.data.detail);
} else if (!e?.errorFields) {
message.error('注册失败');
}
}
};
const onCopy = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success('已复制');
});
};
const columns: ColumnsType<ServiceRow> = [
{
title: 'API Key',
dataIndex: 'api_key',
key: 'api_key',
width: 400,
render: (key: string) => (
<Space size="small">
<Typography.Text style={{ fontFamily: 'monospace', fontSize: 13 }} ellipsis>
{key}
</Typography.Text>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => onCopy(key)} />
</Space>
),
},
{ title: '服务名', dataIndex: 'service_name', key: 'service_name', width: 150, ellipsis: true },
{
title: 'MCP服务地址',
dataIndex: 'service_url',
key: 'service_url',
width: 280,
ellipsis: true,
render: (u: string) => (
<Typography.Text style={{ fontFamily: 'monospace', fontSize: 13 }} ellipsis>
{u}
</Typography.Text>
),
},
{ title: '说明', dataIndex: 'description', key: 'description', width: 200, ellipsis: true },
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90,
render: (s: string) => <Tag color={STATUS_COLOR[s] || 'default'}>{s}</Tag>,
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 160,
render: (t: string) => (t ? dayjs(t).format('YYYY-MM-DD HH:mm') : '-'),
},
{
title: '最近调用',
dataIndex: 'last_used_at',
key: 'last_used_at',
width: 160,
ellipsis: true,
render: (t: string) => (t ? dayjs(t).format('MM-DD HH:mm') : '-'),
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
render: (_, row: ServiceRow) =>
row.status === 'active' ? (
<Space size="small">
<Button type="link" danger size="small" onClick={() => onRevoke(row.service_id)}>
吊销
</Button>
<Button type="link" size="small" disabled>
删除
</Button>
</Space>
) : row.status === 'revoked' ? (
<Space size="small">
<Button type="link" size="small" onClick={() => onEnable(row.service_id)}>
启用
</Button>
<Button type="link" danger size="small" onClick={() => onDelete(row.service_id)}>
删除
</Button>
</Space>
) : (
<Typography.Text type="secondary">-</Typography.Text>
),
},
];
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
注册服务
</Button>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>
刷新
</Button>
</Space>
<Table
columns={columns}
dataSource={rows}
rowKey="service_id"
loading={loading}
pagination={{ pageSize: 15 }}
tableLayout="fixed"
scroll={{ x: 1540 }}
/>
{/* 注册服务表单 */}
<Modal
title="注册 MCP 服务"
open={createOpen}
onOk={onCreate}
onCancel={() => setCreateOpen(false)}
okText="注册"
cancelText="取消"
>
<Form form={form} layout="vertical">
<Form.Item
name="service_name"
label="服务标识"
rules={[{ required: true, message: '请输入服务标识' }]}
>
<Input placeholder="如 erp / crm" />
</Form.Item>
<Form.Item
name="service_url"
label="MCP 服务地址"
rules={[{ required: true, message: '请输入 MCP 服务地址' }]}
>
<Input placeholder="如 http://10.100.154.100:8001/mcp" />
</Form.Item>
<Form.Item name="description" label="说明">
<Input.TextArea rows={2} placeholder="服务用途说明" />
</Form.Item>
</Form>
</Modal>
{/* 注册结果:API Key */}
<Modal
title="API Key 已生成"
open={created !== null}
onCancel={() => setCreated(null)}
footer={<Button type="primary" onClick={() => setCreated(null)}>关闭</Button>}
>
<Typography.Paragraph type="secondary">
配置到 MCP 服务的 <code>MCP_AUTH_API_KEY</code> 环境变量:
</Typography.Paragraph>
<Space.Compact style={{ width: '100%' }}>
<Input.TextArea
rows={2}
value={created?.api_key || ''}
readOnly
style={{ fontFamily: 'monospace' }}
/>
<Button
type="primary"
icon={<CopyOutlined />}
onClick={() => created?.api_key && onCopy(created.api_key)}
>
复制
</Button>
</Space.Compact>
</Modal>
</div>
);
}