AgentEvalTool/frontend/web/src/pages/Targets.tsx
sinohqb 12c1732617 v0.4-t3t4: OpenClaw 通道 + 前端 bundle 优化
## 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>
2026-07-17 15:37:14 +08:00

234 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}