Просмотр исходного кода

✨ feat(ai-agent): add AI agent management feature

- add AI agent menu item in admin panel
- create AI agent management page with CRUD operations
- implement AI agent API endpoints (create, read, update, delete)
- add form validation and error handling for AI agent operations
- add data table with sorting and pagination for AI agent list
- implement permissions for AI agent management access
yourname 7 месяцев назад
Родитель
Сommit
b0c81c3ea2

+ 8 - 0
src/client/admin/menu.tsx

@@ -15,6 +15,7 @@ import {
   BookOutlined,
   FolderOutlined,
   FundViewOutlined,
+  RobotOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -149,6 +150,13 @@ export const useMenu = () => {
       path: '/admin/big-screen',
       permission: 'dashboard:view'
     },
+    {
+      key: 'ai-agents',
+      label: 'AI智能体管理',
+      icon: <RobotOutlined />,
+      path: '/admin/ai-agents',
+      permission: 'ai-agent:manage'
+    },
   ];
 
   // 用户菜单项

+ 324 - 0
src/client/admin/pages/AIAgents.tsx

@@ -0,0 +1,324 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, Switch, InputNumber, message, Space, Popconfirm, Card, Descriptions } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import { aiAgentClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+
+// 类型定义
+type AIAgent = InferResponseType<typeof aiAgentClient.$get, 200>['data'][0];
+type CreateAIAgentDto = InferRequestType<typeof aiAgentClient.$post>['json'];
+type UpdateAIAgentDto = InferRequestType<typeof aiAgentClient[':id']['$put']>['json'];
+
+const AIAgents: React.FC = () => {
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalType, setModalType] = useState<'create' | 'edit'>('create');
+  const [editingRecord, setEditingRecord] = useState<AIAgent | null>(null);
+  const [data, setData] = useState<AIAgent[]>([]);
+
+  // 获取智能体列表
+  const fetchAgents = async () => {
+    setLoading(true);
+    try {
+      const response = await aiAgentClient.$get();
+      const result = await response.json();
+      setData(result.data);
+    } catch (error) {
+      message.error('获取智能体列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchAgents();
+  }, []);
+
+  // 创建或更新智能体
+  const handleSubmit = async (values: any) => {
+    try {
+      if (modalType === 'create') {
+        const response = await aiAgentClient.$post({
+          json: values as CreateAIAgentDto,
+        });
+        if (response.ok) {
+          message.success('创建成功');
+        } else {
+          throw new Error('创建失败');
+        }
+      } else if (editingRecord) {
+        const response = await aiAgentClient[':id'].$put({
+          param: { id: editingRecord.id.toString() },
+          json: values as UpdateAIAgentDto,
+        });
+        if (response.ok) {
+          message.success('更新成功');
+        } else {
+          throw new Error('更新失败');
+        }
+      }
+      setModalVisible(false);
+      form.resetFields();
+      fetchAgents();
+    } catch (error) {
+      message.error(modalType === 'create' ? '创建失败' : '更新失败');
+    }
+  };
+
+  // 删除智能体
+  const handleDelete = async (record: AIAgent) => {
+    try {
+      const response = await aiAgentClient[':id'].$delete({
+        param: { id: record.id.toString() },
+      });
+      if (response.ok) {
+        message.success('删除成功');
+        fetchAgents();
+      } else {
+        throw new Error('删除失败');
+      }
+    } catch (error) {
+      message.error('删除失败');
+    }
+  };
+
+  // 打开创建/编辑模态框
+  const openModal = (type: 'create' | 'edit', record?: AIAgent) => {
+    setModalType(type);
+    setEditingRecord(record || null);
+    setModalVisible(true);
+    
+    if (record) {
+      form.setFieldsValue({
+        ...record,
+        isActive: record.isActive === 1,
+      });
+    } else {
+      form.resetFields();
+    }
+  };
+
+  // 表格列配置
+  const columns: ColumnsType<AIAgent> = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+      ellipsis: true,
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+      render: (text) => text || '-',
+    },
+    {
+      title: '模型',
+      dataIndex: 'model',
+      key: 'model',
+      width: 120,
+    },
+    {
+      title: '温度',
+      dataIndex: 'temperature',
+      key: 'temperature',
+      width: 80,
+      render: (value) => value.toFixed(2),
+    },
+    {
+      title: '最大令牌',
+      dataIndex: 'maxTokens',
+      key: 'maxTokens',
+      width: 100,
+    },
+    {
+      title: '状态',
+      dataIndex: 'isActive',
+      key: 'isActive',
+      width: 100,
+      render: (isActive) => (
+        <Switch 
+          checked={isActive === 1} 
+          disabled
+          checkedChildren="启用"
+          unCheckedChildren="禁用"
+        />
+      ),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 180,
+      render: (text) => new Date(text).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 150,
+      fixed: 'right',
+      render: (_, record) => (
+        <Space>
+          <Button
+            type="text"
+            icon={<EditOutlined />}
+            onClick={() => openModal('edit', record)}
+          />
+          <Popconfirm
+            title="确定要删除这个智能体吗?"
+            onConfirm={() => handleDelete(record)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="text" danger icon={<DeleteOutlined />} />
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <Card>
+        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
+          <h1 style={{ margin: 0 }}>AI智能体管理</h1>
+          <Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('create')}>
+            创建智能体
+          </Button>
+        </div>
+
+        <Table
+          columns={columns}
+          dataSource={data}
+          rowKey="id"
+          loading={loading}
+          scroll={{ x: 1200 }}
+          pagination={{
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total) => `共 ${total} 条记录`,
+          }}
+        />
+      </Card>
+
+      <Modal
+        title={modalType === 'create' ? '创建智能体' : '编辑智能体'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+        width={800}
+        destroyOnClose
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSubmit}
+          initialValues={{
+            model: 'gpt-3.5-turbo',
+            temperature: 0.7,
+            maxTokens: 1000,
+            isActive: true,
+          }}
+        >
+          <Form.Item
+            label="智能体名称"
+            name="name"
+            rules={[{ required: true, message: '请输入智能体名称' }]}
+          >
+            <Input placeholder="例如:银龄助手" maxLength={100} />
+          </Form.Item>
+
+          <Form.Item
+            label="描述"
+            name="description"
+          >
+            <Input.TextArea 
+              placeholder="智能体的功能描述" 
+              rows={3}
+              maxLength={500}
+              showCount
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="系统提示词"
+            name="systemPrompt"
+            tooltip="定义AI助手的角色和行为方式"
+          >
+            <Input.TextArea 
+              placeholder="你是一个友好的银龄助手,专门帮助老年用户..."
+              rows={4}
+              maxLength={2000}
+              showCount
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="AI模型"
+            name="model"
+            rules={[{ required: true, message: '请选择AI模型' }]}
+          >
+            <Input placeholder="例如:gpt-3.5-turbo" />
+          </Form.Item>
+
+          <Form.Item
+            label="温度参数"
+            name="temperature"
+            tooltip="控制AI回复的随机性,0-1之间,值越大回复越多样"
+            rules={[{ required: true, message: '请输入温度参数' }]}
+          >
+            <InputNumber
+              min={0}
+              max={2}
+              step={0.1}
+              style={{ width: '100%' }}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="最大令牌数"
+            name="maxTokens"
+            tooltip="控制AI回复的最大长度"
+            rules={[{ required: true, message: '请输入最大令牌数' }]}
+          >
+            <InputNumber
+              min={1}
+              max={4000}
+              style={{ width: '100%' }}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="启用状态"
+            name="isActive"
+            valuePropName="checked"
+          >
+            <Switch checkedChildren="启用" unCheckedChildren="禁用" />
+          </Form.Item>
+
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                {modalType === 'create' ? '创建' : '更新'}
+              </Button>
+              <Button onClick={() => setModalVisible(false)}>
+                取消
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default AIAgents;

+ 6 - 0
src/client/admin/routes.tsx

@@ -16,6 +16,7 @@ import SilverKnowledgesPage from './pages/SilverKnowledges';
 import SilverKnowledgeDetailPage from './pages/SilverKnowledgeDetailPage';
 import KnowledgeCategories from './pages/KnowledgeCategories';
 import BigScreenDashboard from './pages/BigScreenDashboard';
+import AIAgents from './pages/AIAgents';
 
 export const router = createBrowserRouter([
   {
@@ -93,6 +94,11 @@ export const router = createBrowserRouter([
         element: <BigScreenDashboard />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'ai-agents',
+        element: <AIAgents />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 59 - 0
src/server/api/ai-agents/[id]/delete.ts

@@ -0,0 +1,59 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AIAgentService } from '@/server/modules/ai-agents/ai-agent.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const DeleteParams = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '智能体ID'
+  })
+});
+
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: DeleteParams
+  },
+  responses: {
+    200: {
+      description: '删除智能体成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({ example: true, description: '删除成功' })
+          })
+        }
+      }
+    },
+    404: {
+      description: '智能体不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const service = new AIAgentService(AppDataSource);
+    
+    await service.delete(parseInt(id));
+    
+    return c.json({ success: true }, 200);
+  } catch (error) {
+    const { code = 500, message = '删除智能体失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 68 - 0
src/server/api/ai-agents/[id]/put.ts

@@ -0,0 +1,68 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { AIAgentSchema, UpdateAIAgentDto } from '@/server/modules/ai-agents/ai-agent.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AIAgentService } from '@/server/modules/ai-agents/ai-agent.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const UpdateParams = z.object({
+  id: z.string().openapi({
+    param: { name: 'id', in: 'path' },
+    example: '1',
+    description: '智能体ID'
+  })
+});
+
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: UpdateParams,
+    body: {
+      content: {
+        'application/json': { schema: UpdateAIAgentDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '更新智能体成功',
+      content: { 'application/json': { schema: AIAgentSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '智能体不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const data = await c.req.json();
+    const user = c.get('user');
+    const service = new AIAgentService(AppDataSource);
+    
+    const agent = await service.update(parseInt(id), {
+      ...data,
+      updatedBy: user.id
+    });
+    
+    return c.json(agent, 200);
+  } catch (error) {
+    const { code = 500, message = '更新智能体失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 6 - 0
src/server/api/ai-agents/index.ts

@@ -1,11 +1,17 @@
 import { OpenAPIHono } from '@hono/zod-openapi';
 import listRoute from './get';
 import getByIdRoute from './[id]/get';
+import createRoute from './post';
+import updateRoute from './[id]/put';
+import deleteRoute from './[id]/delete';
 import chatRoute from './chat/post';
 
 const app = new OpenAPIHono()
   .route('/', listRoute)
+  .route('/', createRoute)
   .route('/', getByIdRoute)
+  .route('/', updateRoute)
+  .route('/', deleteRoute)
   .route('/', chatRoute);
 
 export default app;

+ 55 - 0
src/server/api/ai-agents/post.ts

@@ -0,0 +1,55 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { AIAgentSchema, CreateAIAgentDto } from '@/server/modules/ai-agents/ai-agent.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AIAgentService } from '@/server/modules/ai-agents/ai-agent.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateAIAgentDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '创建智能体成功',
+      content: { 'application/json': { schema: AIAgentSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const data = await c.req.json();
+    const user = c.get('user');
+    const service = new AIAgentService(AppDataSource);
+    
+    const agent = await service.create({
+      ...data,
+      createdBy: user.id,
+      updatedBy: user.id
+    });
+    
+    return c.json(agent, 200);
+  } catch (error) {
+    const { code = 500, message = '创建智能体失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 22 - 1
src/server/utils/errorHandler.ts

@@ -10,9 +10,30 @@ export const ErrorSchema = z.object({
   }),
 })
 
+export class AppError extends Error {
+  constructor(
+    message: string,
+    public code: number = 500,
+    public status?: number
+  ) {
+    super(message)
+    this.name = 'AppError'
+  }
+}
+
 export const errorHandler = async (err: Error, c: Context) => {
+  if (err instanceof AppError) {
+    return c.json(
+      {
+        code: err.code,
+        message: err.message
+      },
+      err.status || err.code
+    )
+  }
+  
   return c.json(
-    { 
+    {
       code: 500,
       message: err.message || 'Internal Server Error'
     },