调整文档路径
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user