Skip to content

RobotFactory API设计介绍

RobotFactory通过低代码配置的方式将Robot逐步构建出来。

前端设计目标:

  1. 封装一个前端组件,可以接收一个任意的JsonSchema,如果是一个SettingID,则由有个ReactFlow独立的Node表达。如果是字段,在Node内进行配置,并且可以结合Zod进行格式校验。JsonSchemaToZod也可以派上用场。
  2. 所有的一切,注意抽象一个合理的原子单元的实现,通过少量的逻辑性强的原子单元组成千变万化的配置。
  3. 如果是嵌套的List或者Dict,则使用弹窗+面包屑进行递归配置与表达。
  4. 如果子字段引用其它SettingID,则通过Node之间的连线来表达。

后端需要在当前RobotFactory封装的基础上为前端提供满足其需求的接口。

一、整体可行性与可能的难点

  1. 可行性
  2. 目前大多数配置型系统都会利用 JsonSchemaPydantic(或其他 Schema 工具)来自动生成前端所需的表单或配置项。
  3. 对于特殊“SettingID”类型(可视为独立配置),可以使用 React Flow 中的 Node 来单独表示;若是引用到另外一个 SettingID,也可以将其通过 Edge 进行连接。
  4. 对于嵌套的 ListDict,可以在 Node 内部用弹窗、面包屑等来进行深层次的配置。

  5. 可能的难点

  6. 如何高效地把后端的 Python 类型与前端的 JsonSchema 对接:如果直接使用 pydanticmodel.model_json_schema(),大部分字段可以自动转换成 JSON Schema,但需要注意自定义字段(如 “SettingID”)如何在 Schema 中标识。
  7. 前端如何管理 Node 与普通字段之间的关系
    • 如果是普通字段,可能直接在 Node 的内部一组表单控件中进行编辑。
    • 如果是引用了另一个独立配置(SettingID),则需要新建一个 Node,并通过连接线(Edge)表达二者的引用关系。

只要我们在后端与前端之间约定好 JsonSchema 的数据格式和关键字(例如定义一个 type: "SettingID" 或者 "x-setting-id": true"),就可以在前端根据不同类型自动渲染不同的组件或 Node。


二、抽象的最小逻辑单元

结合上述思路,可以把“最小逻辑单元”抽象为以下三类 前端组件

  1. 基础数据类型组件
  2. 用于展示/编辑 string, number, boolean 等最基础类型的字段(输入框、单选框、开关等)。
  3. 可结合 Zodjson-schema-to-zod 来进行数据校验。
  4. 容器组件(面包屑/弹窗/折叠面板),用于展示:
  5. Dict(对象),内部含有若干子字段
  6. List(数组),内部含有多条对象或基础类型
    这些容器组件可以递归调用自己或其他子组件来渲染更深层次的字段结构。
  7. SettingID 组件(Node + Edge):
  8. 通过 React Flow 的一个 Node 来承载该“独立配置”的内容。
  9. Node 内部依旧有若干字段需要配置(也可能嵌套一堆 Dict/List 字段),但关键是它是“可视化节点”而非在已有 Node 内部简单显示。
  10. 如果 Node 内的字段又引用其他独立配置,便可以在 React Flow 中动态添加新的 Node 并与之连线,表达引用关系。

通过这三类组件,基本上可以涵盖所有的配置场景。你只需要在前端写一个“递归渲染入口”来遍历 JSON Schema:
- 如果 type: SettingID -> 创建一个 Node
- 如果是 type: object -> 使用容器组件进行递归
- 如果是 type: array -> 使用容器组件(数组)进行递归
- 如果是 type: string/number/boolean -> 使用基础组件编辑


三、后端(FastAPI)如何设计

3.1. 数据模型与 JSON Schema 生成

在后端,你已经有了大量的 BaseRobotDraftSettingRobotDraftConfig 等类,并且这些类中可能都有对应的 Pydantic 模型(或者你可以将它们本身做成 Pydantic 模型)。关键就在于:
1. 需要为“SettingID”这种字段提供一个特殊的标识,告诉前端它是一个可独立拆分的配置。
2. 当调用 model.json_schema()(或 Pydantic 提供的 model.schema() 方法)时,要让这个字段在返回的 JSON 中带上它的特殊“类型”或“标识”。

假设我们有这样一个 Pydantic 模型(简化说明):

from pydantic import BaseModel, Field
from typing import Union, List, Dict


class SettingID(BaseModel):
    # 仅用一个 id 字段表示这个配置ID
    id: str = Field(..., description="Setting ID 用于引用其他配置的唯一标识")

    class Config:
        # 让json_schema里带上自己的自定义keyword,比如 x-setting-id
        schema_extra = {
            "x-setting-id": True
        }


class RobotDraftConfig(BaseModel):
    name: str = Field(..., title="机器人名称")
    # 例如嵌套一个SettingID
    memory_config: SettingID = Field(..., title="Memory 配置")

    # 也可能嵌套Dict / List
    thresholds: Dict[str, int] = Field({}, title="阈值设置")
    tags: List[str] = Field([], title="标签集合")

要点SettingIDschema_extra 中可添加自定义字段(如 "x-setting-id": True)。前端根据这个“标识”来决定渲染为 Node。

3.2. FastAPI Endpoint 设计示例

1. 获取指定 Draft 类型的 Schema
- 为了让前端渲染配置界面,需要根据后端返回的 JSON Schema 来动态构造界面。
- 例如:GET /api/drafts/{draft_name}/schema

from fastapi import FastAPI, HTTPException
from pydantic import ValidationError
from typing import Any

app = FastAPI()

# 假设我们把 Schema 的生成做一个统一接口
@app.get("/v1/factory/drafts/{draft_name}/schema")
def get_draft_schema(draft_name: str) -> dict[str, Any]:
    """
    根据draft_name获取对应的Pydantic模型,然后把它的JsonSchema返回给前端。
    """
    # 你可以根据 draft_name 在 draft_factories 或者某个映射里取到对应的Pydantic模型
    # 这里只是伪代码
    draft_model = draft_factories.get(draft_name)
    if not draft_model:
        raise HTTPException(status_code=404, detail="Draft not found")
    # 假设draft_model就是一个Pydantic模型
    return draft_model.model_json_schema()  # Pydantic v2 里可以用 .model_json_schema()

2. 提交某个 Draft 的配置并进行校验
- 前端会把编辑完毕的 JSON(或一系列字段)提交给后端进行持久化、二次校验或生成实际的“机器人实例”配置等。
- 例如:POST /api/drafts/{draft_name}/validate

@app.post("/api/drafts/{draft_name}/validate")
def validate_draft_config(draft_name: str, payload: dict[str, Any]) -> dict[str, Any]:
    """
    接收前端提交的草稿配置,使用对应的Pydantic模型进行校验。
    """
    draft_model = draft_factories.get(draft_name)
    if not draft_model:
        raise HTTPException(status_code=404, detail="Draft not found")

    try:
        validated_config = draft_model(**payload)  # 用Pydantic进行反序列化校验
        # 这里可以做更多业务逻辑,例如写入数据库
        return {"success": True, "data": validated_config.model_dump()}
    except ValidationError as e:
        return {"success": False, "errors": e.errors()}

上述仅是最简要的示例。根据实际业务,还可以加上 PUT /api/drafts/{draft_id}, GET /api/drafts/{draft_id} 等接口。


四、前端(Next.js + React Flow)如何设计

4.1. 目录结构与依赖

一个最小的 Next.js 项目可能有以下结构(仅示例):

.
├── pages
│   ├── index.tsx
│   └── drafts
│       └── [draftName].tsx
├── components
│   ├── FormField.tsx           # 基础数据类型组件
│   ├── ObjectEditor.tsx         # 用于Dict/嵌套的递归处理
│   ├── ArrayEditor.tsx          # 用于List/数组的递归处理
│   ├── SettingIDNode.tsx        # SettingID 对应的 Node
│   ├── ReactFlowCanvas.tsx      # ReactFlow 画布组件,用来管理 Node / Edge
│   └── ...
├── lib
│   ├── schemaUtils.ts           # 封装 json-schema-to-zod 或其他Schema解析
│   └── api.ts                   # 封装 axios/fetch
└── ...

需要安装的依赖可能有:
- reactflow(或 react-flow-renderer),实现可视化 Node/Edge。
- zod + @anatine/zod-openapi 或者 json-schema-to-zod 等,用来把后端返回的 JSON Schema 转换成前端可校验的 Zod schema。
- 或者你也可以完全只依赖 JSON Schema 解析,配合 ajv 等进行校验。

4.2. Node 与普通字段组件示例

  1. SettingIDNode.tsx(节点组件示例)

```tsx import React from 'react'; import { Handle, Position } from 'reactflow';

// 这里的 props 可以根据你的实际需要调整 interface SettingIDNodeProps { id: string; data: { schema: any; // 这里存放本节点的JsonSchema value: any; // 这里存放本节点的当前值 onChange: (newValue: any) => void; }; }

const SettingIDNode: React.FC = ({ id, data }) => { const { schema, value, onChange } = data;

 // 你可以在这里渲染节点内部字段,比如 name, desc, etc...
 // 对于嵌套字段,可以引入另外的 ObjectEditor / ArrayEditor
 // 这里只是一个最简示例

 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   onChange({ ...value, [e.target.name]: e.target.value });
 };

 return (
   <div style={{ border: '1px solid #ddd', padding: '8px', background: '#fff' }}>
     <div>SettingID Node: {id}</div>
     {/* 假设要编辑 value.id */}
     <label>ID: 
       <input
         type="text"
         name="id"
         value={value.id ?? ''}
         onChange={handleInputChange}
       />
     </label>

     {/* 画布的连线进出口 */}
     <Handle type="source" position={Position.Bottom} />
     <Handle type="target" position={Position.Top} />
   </div>
 );

};

export default SettingIDNode; ```

  1. FormField.tsx(基础字段组件示例)

```tsx import React from 'react';

interface FormFieldProps { schema: any; // json schema for the field value: any; onChange: (newValue: any) => void; fieldKey: string; // 字段名 }

const FormField: React.FC = ({ schema, value, onChange, fieldKey }) => { const handleChange = (e: React.ChangeEvent) => { onChange(e.target.value); };

 switch (schema.type) {
   case 'string':
     return (
       <div>
         <label>{fieldKey}</label>
         <input type="text" value={value || ''} onChange={handleChange} />
       </div>
     );
   case 'number':
     return (
       <div>
         <label>{fieldKey}</label>
         <input
           type="number"
           value={value ?? 0}
           onChange={(e) => onChange(Number(e.target.value))}
         />
       </div>
     );
   case 'boolean':
     return (
       <div>
         <label>{fieldKey}</label>
         <input
           type="checkbox"
           checked={!!value}
           onChange={(e) => onChange(e.target.checked)}
         />
       </div>
     );
   default:
     return <div>Unsupported field type: {schema.type}</div>;
 }

};

export default FormField; ```

  1. ObjectEditor.tsx(示例:当检测到 type: "object",可以用面包屑或递归方式呈现)

```tsx import React from 'react'; import FormField from './FormField';

interface ObjectEditorProps { schema: any; // { type: 'object', properties: {...} } value: any; onChange: (newValue: any) => void; }

const ObjectEditor: React.FC = ({ schema, value = {}, onChange }) => { const handleFieldChange = (fieldKey: string, newValue: any) => { onChange({ ...value, [fieldKey]: newValue }); };

 return (
   <div style={{ border: '1px solid #ccc', margin: '8px', padding: '8px' }}>
     {Object.keys(schema.properties || {}).map((fieldKey) => {
       const fieldSchema = schema.properties[fieldKey];
       const fieldValue = value[fieldKey];

       // 如果这个 fieldSchema 有 x-setting-id,就说明需要使用 SettingIDNode (或跳转到 Node)
       if (fieldSchema['x-setting-id'] === true) {
         return (
           <div key={fieldKey} style={{ marginBottom: '8px' }}>
             <span>{fieldKey} 是一个 SettingID,需要用 Node 表示</span>
             {/* 这里的逻辑可以是:在画布上新建一个 Node,也可以直接引导用户去连接 */}
           </div>
         );
       }

       // 如果是普通对象/数组/基础类型
       if (fieldSchema.type === 'object') {
         // 递归渲染ObjectEditor
         return (
           <ObjectEditor
             key={fieldKey}
             schema={fieldSchema}
             value={fieldValue}
             onChange={(nv) => handleFieldChange(fieldKey, nv)}
           />
         );
       } else if (fieldSchema.type === 'array') {
         // 这里可以写一个 ArrayEditor 去管理数组项目
         return <div key={fieldKey}>[ArrayEditor Placeholder]</div>;
       } else {
         // 基础类型
         return (
           <FormField
             key={fieldKey}
             schema={fieldSchema}
             fieldKey={fieldKey}
             value={fieldValue}
             onChange={(nv) => handleFieldChange(fieldKey, nv)}
           />
         );
       }
     })}
   </div>
 );

};

export default ObjectEditor; ```

4.3. 在 Next.js 页面中整合

假设有一页:pages/drafts/[draftName].tsx,路由形如 http://localhost:3000/drafts/RobotDraftSetting。我们在其中做以下事情:

  1. 根据 draftName 从后端拿到对应的 JSON Schema。
  2. 用 React Flow 初始化一个画布,用来表示所有“SettingID”类型的节点。
  3. 把普通字段放在一个侧边栏或者 Node 内部表单中进行配置。
  4. 最后提交给后端进行校验或保存。

大致代码示例(示意性):

import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import ReactFlow, { ReactFlowProvider, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
import { fetchDraftSchema } from '../../lib/api'; // 封装的后端请求函数
import SettingIDNode from '../../components/SettingIDNode';

const nodeTypes = {
  settingIDNode: SettingIDNode,
};

export default function DraftPage() {
  const router = useRouter();
  const { draftName } = router.query;

  const [schema, setSchema] = useState<any | null>(null);
  const [nodes, setNodes] = useState([]);
  const [edges, setEdges] = useState([]);

  useEffect(() => {
    if (draftName) {
      fetchDraftSchema(draftName as string).then((data) => {
        setSchema(data);
      });
    }
  }, [draftName]);

  const onNodesChange = (changes: any) => {
    // reactflow 中对 nodes 的改变
    setNodes((nds) => nds.map((node) => node));
  };

  const onEdgesChange = (changes: any) => {
    setEdges((eds) => eds.map((edge) => edge));
  };

  const onConnect = (params: any) => {
    setEdges((eds) => addEdge(params, eds));
  };

  // 这里可以根据拿到的 schema 做一些初始化 nodes 的操作
  // 也可以在点击按钮时新增节点

  if (!schema) {
    return <div>Loading...</div>;
  }

  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ReactFlowProvider>
        <ReactFlow
          nodeTypes={nodeTypes}
          nodes={nodes}
          edges={edges}
          onNodesChange={onNodesChange}
          onEdgesChange={onEdgesChange}
          onConnect={onConnect}
        >
          {/* 可以添加 React Flow 内部的 Control / Background / MiniMap 等组件 */}
        </ReactFlow>
      </ReactFlowProvider>
    </div>
  );
}

五、总结

  1. 抽象的原子单元
  2. 基础字段组件(针对 string/number/boolean
  3. 容器组件(针对 object/array
  4. Node 组件(针对自定义标识,如 SettingID

  5. 前后端交互

  6. 后端:需要提供获取 JSON Schema、提交配置校验/保存的 API Endpoint。
  7. 前端:使用 React Flow + 递归表单组件 + Zod/JsonSchema 解析 进行渲染与校验。
  8. 对于 SettingID,前端创建一个 Node;若有引用其他“独立配置”,则在画布上再添加对应 Node 并用连线表示关系。

  9. 可行性

  10. 该方案在目前常见的前后端技术栈里是非常可行的。
  11. 难点更多在于:
    • 如何为“SettingID”实现可视化的拖拽、连线逻辑;
    • 如何在前端递归渲染复杂的嵌套结构;
    • 如何管理“多个 Node 之间的依赖关系”并最终序列化成一个可提交的整体配置。

只要把 JsonSchema 的定义、Node 的可视化及 递归渲染 的逻辑三者对应起来,就能完成一个功能较为强大的可视化配置系统。而且,你提到的“用较少却逻辑性强的原子单元组成千变万化的配置”这一点,正是通过递归Node 拆分来实现的核心理念。

以上就是一个整体的思路与示例,希望对你有所启发。祝项目开发顺利!