## T3: OpenClaw 直连通道
- channels/openclaw.py: OpenClawChannel
- send: POST /api/v1/chat/completions
- poll_reply: GET /api/v1/chat/completions/{msg_id}
- health_check: GET /api/health
- 默认从 settings 读取 upstream/auth_token,channel_config 可覆盖
- channels/factory.py: 注册 ChannelType.OPENCLAW → OpenClawChannel
- Targets.tsx: 通道类型下拉新增「HTTP 通用」和「OpenClaw」选项
- 8 个 OpenClawChannel 单元测试(发送/轮询/超时/健康检查/默认配置)
## T4: 前端 Bundle 优化
- App.tsx: 7 个页面改为 React.lazy 懒加载 + Suspense fallback(Spin)
- vite.config.ts: 精细化 manualChunks
- vendor-antd / vendor-monaco / vendor-charts 独立拆分
- 主 index 68KB → 7KB,页面按需加载
- 无循环依赖警告
## 测试
- 178/178 全绿,覆盖率维持 77%
Co-Authored-By: Claude <noreply@anthropic.com>
234 lines
7.9 KiB
TypeScript
234 lines
7.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import {
|
||
Button, Drawer, Form, Input, message, Popconfirm,
|
||
Space, Table, Tag, Select, Tooltip,
|
||
} from 'antd'
|
||
import { PlusOutlined, ApiOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
|
||
import { targetsApi, type Target } from '../api'
|
||
import PageWrapper from '../components/PageWrapper'
|
||
import { colors } from '../tokens'
|
||
import { formatDateTime } from '../utils/date'
|
||
|
||
export default function TargetsPage() {
|
||
const [targets, setTargets] = useState<Target[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||
const [editingTarget, setEditingTarget] = useState<Target | null>(null)
|
||
const [form] = Form.useForm()
|
||
|
||
const load = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await targetsApi.list()
|
||
setTargets(res.data)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => { load() }, [])
|
||
|
||
const openCreate = () => {
|
||
setEditingTarget(null)
|
||
form.resetFields()
|
||
form.setFieldsValue({
|
||
platform: 'ai_digital_employee',
|
||
channel_type: 'tutu-api',
|
||
})
|
||
setDrawerOpen(true)
|
||
}
|
||
|
||
const openEdit = (target: Target) => {
|
||
setEditingTarget(target)
|
||
form.setFieldsValue({
|
||
name: target.name,
|
||
description: target.description,
|
||
platform: target.platform,
|
||
channel_type: target.channel_type,
|
||
base_url: (target.channel_config as any)?.base_url || '',
|
||
token: (target.channel_config as any)?.token || '',
|
||
tenant: (target.channel_config as any)?.tenant || '',
|
||
chat_channel_id: (target.channel_config as any)?.chat_channel_id || '',
|
||
chat_contact_id: (target.channel_config as any)?.chat_contact_id || '',
|
||
})
|
||
setDrawerOpen(true)
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
const values = await form.validateFields()
|
||
const channel_config = {
|
||
base_url: values.base_url,
|
||
token: values.token,
|
||
tenant: values.tenant,
|
||
chat_channel_id: values.chat_channel_id,
|
||
chat_contact_id: values.chat_contact_id,
|
||
}
|
||
const payload = {
|
||
name: values.name,
|
||
description: values.description || '',
|
||
platform: values.platform,
|
||
channel_type: values.channel_type,
|
||
channel_config,
|
||
status: 'active',
|
||
}
|
||
|
||
if (editingTarget) {
|
||
await targetsApi.update(editingTarget.id, payload)
|
||
message.success('更新成功')
|
||
} else {
|
||
await targetsApi.create(payload)
|
||
message.success('创建成功')
|
||
}
|
||
setDrawerOpen(false)
|
||
load()
|
||
}
|
||
|
||
const handleTest = async (id: string) => {
|
||
try {
|
||
const res = await targetsApi.test(id)
|
||
if (res.data.ok) {
|
||
message.success('通道连通正常')
|
||
} else {
|
||
message.warning(`通道异常: ${res.data.message}`)
|
||
}
|
||
} catch {
|
||
message.error('连通性测试失败')
|
||
}
|
||
}
|
||
|
||
const handleDelete = async (id: string) => {
|
||
await targetsApi.delete(id)
|
||
message.success('已删除')
|
||
load()
|
||
}
|
||
|
||
const statusColor: Record<string, string> = {
|
||
active: 'success',
|
||
inactive: 'default',
|
||
error: 'error',
|
||
}
|
||
|
||
const columns = [
|
||
{ title: '名称', dataIndex: 'name', key: 'name', width: 180,
|
||
render: (v: string) => <span style={{ fontWeight: 500 }}>{v}</span>,
|
||
},
|
||
{ title: '平台', dataIndex: 'platform', key: 'platform', width: 160,
|
||
render: (p: string) => p === 'ai_digital_employee' ? 'AI 数字员工' : 'AI 助手',
|
||
},
|
||
{ title: '通道类型', dataIndex: 'channel_type', key: 'channel_type', width: 120 },
|
||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
||
render: (s: string) => <Tag color={statusColor[s] || 'default'}>{s}</Tag>,
|
||
},
|
||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180,
|
||
render: (t: string) => formatDateTime(t),
|
||
},
|
||
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const,
|
||
render: (_: any, record: Target) => (
|
||
<Space size={4}>
|
||
<Tooltip title="连通测试">
|
||
<Button size="small" type="text" icon={<ApiOutlined />} onClick={() => handleTest(record.id)} />
|
||
</Tooltip>
|
||
<Tooltip title="编辑">
|
||
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
||
</Tooltip>
|
||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||
<Tooltip title="删除">
|
||
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
||
</Tooltip>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<PageWrapper
|
||
title="评测对象"
|
||
description="管理被评测的 AI 智能体及其通信通道"
|
||
inline
|
||
fullHeight
|
||
extra={
|
||
<Space>
|
||
<Tooltip title="刷新">
|
||
<Button size="middle" icon={<ReloadOutlined />} onClick={load} />
|
||
</Tooltip>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||
新增
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
{/* 表格区 — 撑满剩余高度 */}
|
||
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||
<Table
|
||
dataSource={targets}
|
||
columns={columns}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (t) => `共 ${t} 个` }}
|
||
scroll={{ y: 'calc(100vh - 200px)' }}
|
||
style={{ height: '100%' }}
|
||
/>
|
||
</div>
|
||
|
||
<Drawer
|
||
title={editingTarget ? '编辑评测对象' : '新增评测对象'}
|
||
open={drawerOpen}
|
||
onClose={() => setDrawerOpen(false)}
|
||
width={480}
|
||
extra={
|
||
<Button type="primary" onClick={handleSubmit}>
|
||
{editingTarget ? '更新' : '创建'}
|
||
</Button>
|
||
}
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||
<Input placeholder="例如:社区医院AI客服" />
|
||
</Form.Item>
|
||
<Form.Item name="description" label="描述">
|
||
<Input.TextArea rows={2} placeholder="对象描述" />
|
||
</Form.Item>
|
||
<Form.Item name="platform" label="平台类型" rules={[{ required: true }]}>
|
||
<Select options={[
|
||
{ value: 'ai_digital_employee', label: 'AI 数字员工' },
|
||
{ value: 'ai_assistant', label: 'AI 助手' },
|
||
]} />
|
||
</Form.Item>
|
||
<Form.Item name="channel_type" label="通道类型" rules={[{ required: true }]}>
|
||
<Select options={[
|
||
{ value: 'tutu-api', label: 'Tutu API' },
|
||
{ value: 'http', label: 'HTTP 通用' },
|
||
{ value: 'openclaw', label: 'OpenClaw' },
|
||
]} />
|
||
</Form.Item>
|
||
|
||
<div style={{
|
||
border: `1px solid ${colors.border}`, borderRadius: 8,
|
||
padding: '12px 16px', marginBottom: 16,
|
||
}}>
|
||
<div style={{ fontWeight: 600, fontSize: 13, marginBottom: 12, color: colors.text }}>
|
||
通道配置
|
||
</div>
|
||
<Form.Item name="base_url" label="Base URL" rules={[{ required: true, message: '请输入 API 地址' }]}>
|
||
<Input placeholder="https://api.example.com" />
|
||
</Form.Item>
|
||
<Form.Item name="token" label="Token" rules={[{ required: true, message: '请输入 Token' }]}>
|
||
<Input.Password placeholder="Bearer Token" />
|
||
</Form.Item>
|
||
<Form.Item name="tenant" label="Tenant">
|
||
<Input placeholder="租户 ID" />
|
||
</Form.Item>
|
||
<Form.Item name="chat_channel_id" label="Chat Channel ID" rules={[{ required: true }]}>
|
||
<Input placeholder="聊天频道 ID" />
|
||
</Form.Item>
|
||
<Form.Item name="chat_contact_id" label="Chat Contact ID">
|
||
<Input placeholder="联系人 ID" />
|
||
</Form.Item>
|
||
</div>
|
||
</Form>
|
||
</Drawer>
|
||
</PageWrapper>
|
||
)
|
||
}
|