update config

This commit is contained in:
靳中伟 2025-03-17 18:31:20 +08:00
parent a950b043c8
commit 6c70fe2109
65 changed files with 6321 additions and 1058 deletions

52
.dockerignore Normal file
View File

@ -0,0 +1,52 @@
# Git
.git
.gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# 虚拟环境
venv/
ENV/
env/
# IDE
.idea/
.vscode/
*.swp
*.swo
# 日志
logs/*
!logs/.gitkeep
# 测试
tests/
.pytest_cache/
# 其他
.DS_Store
Dockerfile
.dockerignore
docker-compose.yml
README.md
*.md

24
.env.example Normal file
View File

@ -0,0 +1,24 @@
# 应用配置
DEBUG=false
HOST=0.0.0.0
PORT=8000
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=password
DB_NAME=tianfeng_task
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=
# 环境配置
TIANFENG_ENV=default
# 日志配置
LOG_LEVEL=INFO
LOG_FILE=logs/tianfeng_task.log

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
# 使用Python 3.11作为基础镜像
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
TIANFENG_ENV=default \
HOST=0.0.0.0 \
PORT=8000 \
DEBUG=false
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
default-libmysqlclient-dev \
netcat-traditional \
curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 复制项目文件
COPY . /app/
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
# 创建日志目录
RUN mkdir -p /app/logs && chmod 777 /app/logs
# 设置启动脚本权限
RUN chmod +x /app/scripts/docker-entrypoint.sh
# 暴露端口
EXPOSE 8000
# 设置启动脚本为入口点
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
# 启动命令
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@ -369,7 +369,7 @@ tianfeng_task/
### 环境要求
- Python 3.8+
- Python 3.11+
- 数据库MySQL/PostgreSQL
- Node.js 14+(前端开发)
@ -428,4 +428,98 @@ tianfeng_task/
## 联系方式
如有问题或建议,请联系系统管理员或开发团队。
如有问题或建议,请联系系统管理员或开发团队。
# Docker部署说明
## 使用Docker部署天风任务模块
本项目支持使用Docker进行部署以下是部署步骤
### 前提条件
- 安装Docker和Docker Compose
- 确保端口8000、3306和6379未被占用
### 部署步骤
1. 克隆代码仓库
```bash
git clone <仓库地址>
cd tianfeng_task
```
2. 使用Docker Compose构建并启动服务
```bash
docker-compose up -d
```
这将启动三个容器:
- tianfeng-task: 应用服务
- tianfeng-mysql: MySQL数据库
- tianfeng-redis: Redis缓存
3. 验证服务是否正常运行
```bash
docker-compose ps
```
4. 访问API文档
打开浏览器,访问 http://localhost:8000/docs 查看API文档。
### 环境变量配置
可以通过修改`docker-compose.yml`文件中的环境变量来配置应用:
```yaml
environment:
- TIANFENG_ENV=default # 环境名称
- DEBUG=false # 是否开启调试模式
- DB_HOST=db # 数据库主机
- DB_PORT=3306 # 数据库端口
- DB_USER=root # 数据库用户名
- DB_PASSWORD=password # 数据库密码
- DB_NAME=tianfeng_task # 数据库名称
- REDIS_HOST=redis # Redis主机
- REDIS_PORT=6379 # Redis端口
- REDIS_DB=0 # Redis数据库索引
```
### 单独构建Docker镜像
如果需要单独构建Docker镜像可以使用以下命令
```bash
docker build -t tianfeng-task:latest .
```
### 停止服务
```bash
docker-compose down
```
如果需要同时删除数据卷(会删除所有数据):
```bash
docker-compose down -v
```
### 查看日志
```bash
# 查看所有服务的日志
docker-compose logs
# 查看特定服务的日志
docker-compose logs app
docker-compose logs db
docker-compose logs redis
# 实时查看日志
docker-compose logs -f app
```

Binary file not shown.

Binary file not shown.

96
api/common_params_api.py Normal file
View File

@ -0,0 +1,96 @@
"""
常用参数API模块
提供获取各种常用参数数据的接口
"""
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from services.common_params_service import CommonParamsService
from config.component_config import CommonParamsConfig, CommonParamType
from api.models import ApiResponse
# 创建路由器
router = APIRouter(prefix="/common-params", tags=["常用参数"])
# 创建服务实例
common_params_service = CommonParamsService()
@router.get("/types", response_model=ApiResponse)
async def get_param_types():
"""获取所有常用参数类型"""
try:
param_types = CommonParamsConfig.get_param_types()
return {
"code": ApiResponseCode.SUCCESS,
"message": "获取常用参数类型成功",
"data": {
"param_types": param_types
}
}
except Exception as e:
raise HTTPException(
status_code=ApiResponseCode.SERVER_ERROR,
detail=f"获取常用参数类型失败: {str(e)}"
)
@router.get("/data/{param_type}", response_model=ApiResponse)
async def get_param_data(param_type: str):
"""获取指定类型的常用参数数据"""
try:
# 检查参数类型是否有效
param_type_config = CommonParamsConfig.get_param_type_by_type(param_type)
if not param_type_config:
return {
"code": ApiResponseCode.NOT_FOUND,
"message": f"未找到参数类型: {param_type}",
"data": {"param_data": []}
}
# 获取参数数据
param_data = common_params_service.get_param_data(param_type)
return {
"code": ApiResponseCode.SUCCESS,
"message": f"获取{param_type_config['name']}数据成功",
"data": {
"param_type": param_type_config,
"param_data": param_data
}
}
except Exception as e:
raise HTTPException(
status_code=ApiResponseCode.SERVER_ERROR,
detail=f"获取常用参数数据失败: {str(e)}"
)
@router.get("/all", response_model=ApiResponse)
async def get_all_param_data():
"""获取所有常用参数数据"""
try:
# 获取所有参数类型
param_types = CommonParamsConfig.get_param_types()
# 获取所有参数数据
all_param_data = {}
for param_type_config in param_types:
param_type = param_type_config["type"]
param_data = common_params_service.get_param_data(param_type)
all_param_data[param_type] = {
"param_type": param_type_config,
"param_data": param_data
}
return {
"code": ApiResponseCode.SUCCESS,
"message": "获取所有常用参数数据成功",
"data": {
"param_types": param_types,
"all_param_data": all_param_data
}
}
except Exception as e:
raise HTTPException(
status_code=ApiResponseCode.SERVER_ERROR,
detail=f"获取所有常用参数数据失败: {str(e)}"
)

View File

@ -1,13 +1,9 @@
# api/component_api.py
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Depends, Query
from pydantic import BaseModel, Field
from services.component_service import ComponentService
from config.component_config import (
ComponentDiscovery,
ComponentCategory
)
from config.component_detail_config import ComponentDetailConfig
from config.component_config import ComponentDetailConfig, ComponentCategoryConfig
from config.api_config import (
ApiResponseCode,
ApiResponseMessage
@ -44,10 +40,10 @@ async def get_components():
component_categories = {}
for component_type in component_types:
# 根据组件类型推断类别
category = ComponentCategory.BASE # 默认类别
category = ComponentCategoryConfig.BASE # 默认类别
# 使用配置文件中的映射关系确定组件类别
mapping = ComponentCategory.get_mapping()
mapping = ComponentCategoryConfig.get_mapping()
for prefix, cat in mapping.items():
if component_type.startswith(prefix):
category = cat

View File

@ -1,93 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
API模型模块
包含API请求和响应的数据模型
"""
from typing import Dict, Any, List, Optional, Literal
from pydantic import BaseModel, Field, validator
from config.component_config import ComponentDiscovery
from config.task_config import TASK_TYPE_CONFIG, TaskType, TaskStatus
from enum import Enum
# 获取所有任务类型的key值
TASK_TYPE_KEYS = list(TASK_TYPE_CONFIG.keys())
# 排序字段枚举
class SortField(str, Enum):
CREATED_AT = "created_at"
UPDATED_AT = "updated_at"
NAME = "name"
# 排序方式枚举
class SortOrder(str, Enum):
ASC = "asc"
DESC = "desc"
# 通用响应模型
class ApiResponse(BaseModel):
code: int = Field(..., description="状态码")
message: str = Field(..., description="消息")
data: Optional[dict] = Field(None, description="数据")
# 任务相关模型
class TaskInput(BaseModel):
name: str = Field(..., description="任务名称")
task_type: TaskType = Field(
...,
description="任务类型,可选值为:" + ", ".join([t.value for t in TaskType]),
example=TaskType.NORMAL
)
class TaskBatchInput(BaseModel):
tasks: List[TaskInput]
class TaskIdList(BaseModel):
task_ids: List[str]
class TaskTypeInfo(BaseModel):
key: str
name: str
description: str
# 工作流相关模型
class WorkflowInput(BaseModel):
name: str
workflow_type: Optional[str] = "normal"
description: Optional[str] = ""
blocks: Optional[List[Dict[str, Any]]] = []
variables: Optional[Dict[str, Any]] = {}
schedule: Optional[Dict[str, Any]] = None
class WorkflowUpdateInput(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
blocks: Optional[List[Dict[str, Any]]] = None
variables: Optional[Dict[str, Any]] = None
schedule: Optional[Dict[str, Any]] = None
class WorkflowExecuteInput(BaseModel):
task_inputs: Optional[Dict[str, Any]] = None
class WorkflowImportInput(BaseModel):
workflow_json: str
# 组件相关模型
class ComponentDiscoverInput(BaseModel):
package_name: Optional[str] = ComponentDiscovery.DEFAULT_PACKAGE
# 任务编辑相关模型
class TaskUpdateInput(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
task_type: Optional[TaskType] = None
blocks: Optional[List[Dict[str, Any]]] = None
variables: Optional[Dict[str, Any]] = None
schedule: Optional[Dict[str, Any]] = None
class TaskEditInput(BaseModel):
task_id: str
blocks: List[Dict[str, Any]]
variables: Optional[Dict[str, Any]] = {}

22
api/models/__init__.py Normal file
View File

@ -0,0 +1,22 @@
"""
API模型包
包含API请求和响应的数据模型
"""
# 导出基础模型
from api.models.base import ApiResponse, SortField, SortOrder
# 导出任务相关模型
from api.models.task import (
TaskInput, TaskBatchInput, TaskIdList, TaskTypeInfo,
TaskUpdateInput, TaskEditInput
)
# 导出工作流相关模型
from api.models.workflow import (
WorkflowInput, WorkflowUpdateInput,
WorkflowExecuteInput, WorkflowImportInput
)
# 导出组件相关模型
from api.models.component import ComponentDiscoverInput

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

25
api/models/base.py Normal file
View File

@ -0,0 +1,25 @@
"""
基础API模型模块
包含通用的API请求和响应的数据模型
"""
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from enum import Enum
# 排序字段枚举
class SortField(str, Enum):
CREATED_AT = "created_at"
UPDATED_AT = "updated_at"
NAME = "name"
# 排序方式枚举
class SortOrder(str, Enum):
ASC = "asc"
DESC = "desc"
# 通用响应模型
class ApiResponse(BaseModel):
code: int = Field(..., description="状态码")
message: str = Field(..., description="消息")
data: Optional[dict] = Field(None, description="数据")

12
api/models/component.py Normal file
View File

@ -0,0 +1,12 @@
"""
组件API模型模块
包含组件相关的API请求和响应的数据模型
"""
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from config.component_config import ComponentDiscovery
# 组件相关模型
class ComponentDiscoverInput(BaseModel):
package_name: Optional[str] = ComponentDiscovery.DEFAULT_PACKAGE

46
api/models/task.py Normal file
View File

@ -0,0 +1,46 @@
"""
任务API模型模块
包含任务相关的API请求和响应的数据模型
"""
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from config.task_config import TaskType, TaskStatus
# 任务相关模型
class TaskInput(BaseModel):
name: str = Field(..., description="任务名称")
task_type: TaskType = Field(
...,
description="任务类型,可选值为:" + ", ".join([t.value for t in TaskType]),
example=TaskType.NORMAL
)
class TaskBatchInput(BaseModel):
tasks: List[TaskInput]
class TaskIdList(BaseModel):
task_ids: List[str]
class TaskTypeInfo(BaseModel):
key: str
name: str
description: str
# 任务编辑相关模型
class TaskUpdateInput(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
task_type: Optional[TaskType] = None
blocks: Optional[List[Dict[str, Any]]] = None
variables: Optional[Dict[str, Any]] = None
schedule: Optional[Dict[str, Any]] = None
class TaskEditInput(BaseModel):
task_id: str
blocks: List[Dict[str, Any]]
variables: Optional[Dict[str, Any]] = {}
priority: Optional[int] = Field(1, description="任务优先级")
input_params: Optional[Dict[str, Any]] = Field(None, description="任务输入参数")
block_outputs: Optional[Dict[str, Any]] = Field(None, description="块输出参数")
context_params: Optional[Dict[str, Any]] = Field(None, description="上下文参数")

29
api/models/workflow.py Normal file
View File

@ -0,0 +1,29 @@
"""
工作流API模型模块
包含工作流相关的API请求和响应的数据模型
"""
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
# 工作流相关模型
class WorkflowInput(BaseModel):
name: str
workflow_type: Optional[str] = "normal"
description: Optional[str] = ""
blocks: Optional[List[Dict[str, Any]]] = []
variables: Optional[Dict[str, Any]] = {}
schedule: Optional[Dict[str, Any]] = None
class WorkflowUpdateInput(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
blocks: Optional[List[Dict[str, Any]]] = None
variables: Optional[Dict[str, Any]] = None
schedule: Optional[Dict[str, Any]] = None
class WorkflowExecuteInput(BaseModel):
task_inputs: Optional[Dict[str, Any]] = None
class WorkflowImportInput(BaseModel):
workflow_json: str

View File

@ -3,6 +3,7 @@ from typing import Dict, Any, List, Optional
from fastapi import APIRouter, HTTPException, Depends, Query
from pydantic import BaseModel, Field
from services.task_service import TaskService
from services.task_instance_service import TaskInstanceService
from core.exceptions import TianfengTaskError
from config.task_config import (
get_all_task_types,
@ -17,15 +18,21 @@ from config.task_config import (
DEFAULT_TASK_DESCRIPTION,
DEFAULT_TEMPLATE_DESCRIPTION,
TaskType,
TaskStatus
TaskStatus,
TaskInputParamConfig,
SystemParamKey
)
from api.models import (
ApiResponse, TaskInput, TaskBatchInput, TaskIdList,
TaskTypeInfo, SortField, SortOrder, TaskUpdateInput, TaskEditInput
)
from api.models import ApiResponse, TaskInput, TaskBatchInput, TaskIdList, TaskTypeInfo, SortField, SortOrder, TaskUpdateInput, TaskEditInput
# 创建路由器
router = APIRouter(tags=["任务管理"])
# 创建服务实例
task_service = TaskService()
task_instance_service = TaskInstanceService()
@router.get("/tasks", response_model=ApiResponse)
async def get_tasks(
@ -329,10 +336,48 @@ async def edit_task(task_edit: TaskEditInput):
variables=task_edit.variables
)
# 获取编辑中的任务实例
editing_instance = task_instance_service.get_editing_instance_by_task_id(task_edit.task_id)
# 准备更新的参数
update_params = {
"variables": task_edit.variables,
"priority": task_edit.priority if hasattr(task_edit, "priority") else None
}
# 如果有自定义输入参数,添加到更新参数中
if hasattr(task_edit, "input_params") and task_edit.input_params is not None:
update_params["input_params"] = task_edit.input_params
# 如果有块输出参数,添加到更新参数中
if hasattr(task_edit, "block_outputs") and task_edit.block_outputs is not None:
update_params["block_outputs"] = task_edit.block_outputs
# 如果有上下文参数,添加到更新参数中
if hasattr(task_edit, "context_params") and task_edit.context_params is not None:
update_params["context_params"] = task_edit.context_params
if editing_instance:
# 更新编辑中的任务实例
instance = task_instance_service.update_instance(
instance_id=editing_instance["instance_id"],
**update_params
)
else:
# 创建新的任务实例
instance = task_instance_service.create_instance(
task_id=task_edit.task_id,
name=task.get("name"),
**update_params
)
return {
"code": 200,
"message": "编辑任务成功",
"data": {"task": updated_task}
"data": {
"task": updated_task,
"instance": instance
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"编辑任务失败: {str(e)}")
@ -350,6 +395,9 @@ async def get_task_edit_info(task_id: str):
"data": None
}
# 获取或创建编辑中的任务实例
instance = task_instance_service.get_or_create_editing_instance(task_id)
# 获取可用的子任务列表(排除当前任务自身)
available_subtasks = []
try:
@ -363,9 +411,11 @@ async def get_task_edit_info(task_id: str):
except Exception as e:
# 如果获取任务列表失败,记录错误但继续执行
print(f"获取可用子任务列表失败: {str(e)}")
# 获取组件详细信息
from config.component_detail_config import ComponentDetailConfig
from config.component_config import ComponentDetailConfig
component_details = ComponentDetailConfig.get_all_components()
# 获取组件类型中文名称映射
component_type_names = ComponentDetailConfig.get_component_type_names()
@ -387,17 +437,56 @@ async def get_task_edit_info(task_id: str):
# 添加组件到对应类型下
component_types[component_type]["components"].append(component)
# 不再将子任务列表嵌入到组件类型对象中
if "subtask" in component_types:
component_types["subtask"]["available_subtasks"] = available_subtasks
# 获取常用参数数据
from services.common_params_service import CommonParamsService
from config.component_config import CommonParamsConfig
common_params_service = CommonParamsService()
param_types = CommonParamsConfig.get_param_types()
# 获取所有常用参数数据
common_params = {}
for param_type_config in param_types:
param_type = param_type_config["type"]
param_data = common_params_service.get_param_data(param_type)
common_params[param_type] = {
"type_info": param_type_config,
"data": param_data
}
# 从配置文件中获取系统默认参数
task_input_params = TaskInputParamConfig.build_system_params_with_values(task_id, instance)
# 如果实例中有自定义的输入参数,添加到列表中
if instance.get("input_params"):
for key, value in instance["input_params"].items():
# 检查是否已存在同名参数
if not any(param["key"] == key for param in task_input_params):
task_input_params.append({
"key": key,
"name": key, # 可以根据需要设置更友好的名称
"value": value,
"type": "string", # 可以根据值的类型动态设置
"is_system": False,
"is_readonly": False
})
# 获取块输出参数和上下文参数
block_output_params = instance.get("block_outputs", {})
context_params = instance.get("context_params", {})
return {
"code": 200,
"message": "获取任务编辑信息成功",
"data": {
"task": task,
"instance": instance,
"component_types": component_types,
"available_subtasks": available_subtasks # 作为单独的字段返回
"available_subtasks": available_subtasks,
"common_params": common_params,
"task_input_params": task_input_params,
"block_output_params": block_output_params,
"context_params": context_params
}
}
except Exception as e:
@ -429,3 +518,42 @@ async def get_available_subtasks(current_task_id: Optional[str] = None):
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取可用子任务列表失败: {str(e)}")
@router.post("/task/{task_id}/publish", response_model=ApiResponse)
async def publish_task(task_id: str):
"""发布任务(将编辑中的任务实例状态更改为已发布)"""
try:
# 检查任务是否存在
task = task_service.get_task_by_id(task_id)
if not task:
return {
"code": 404,
"message": f"任务 {task_id} 不存在",
"data": None
}
# 获取编辑中的任务实例
editing_instance = task_instance_service.get_editing_instance_by_task_id(task_id)
if not editing_instance:
return {
"code": 404,
"message": f"任务 {task_id} 没有编辑中的实例",
"data": None
}
# 发布任务实例
published_instance = task_instance_service.publish_instance(editing_instance["instance_id"])
# 构建响应数据
response_data = {
"task": task,
"instance": published_instance
}
return {
"code": 200,
"message": "发布任务成功",
"data": response_data
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"发布任务失败: {str(e)}")

218
api/task_instance_api.py Normal file
View File

@ -0,0 +1,218 @@
"""
任务实例API模块
提供任务实例的增删改查接口
"""
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from services.task_instance_service import TaskInstanceService, TaskInstanceStatus
from api.models import ApiResponse
# 创建路由器
router = APIRouter(prefix="/task-instances", tags=["任务实例"])
# 创建服务实例
task_instance_service = TaskInstanceService()
# 请求模型
class TaskInstanceCreateInput(BaseModel):
"""任务实例创建输入模型"""
task_id: str = Field(..., description="任务ID")
name: Optional[str] = Field(None, description="任务名称")
variables: Optional[Dict[str, Any]] = Field(None, description="任务变量")
priority: int = Field(1, description="任务优先级")
input_params: Optional[Dict[str, Any]] = Field(None, description="任务输入参数")
block_outputs: Optional[Dict[str, Any]] = Field(None, description="块输出参数")
context_params: Optional[Dict[str, Any]] = Field(None, description="上下文参数")
class TaskInstanceUpdateInput(BaseModel):
"""任务实例更新输入模型"""
variables: Optional[Dict[str, Any]] = Field(None, description="任务变量")
priority: Optional[int] = Field(None, description="任务优先级")
input_params: Optional[Dict[str, Any]] = Field(None, description="任务输入参数")
block_outputs: Optional[Dict[str, Any]] = Field(None, description="块输出参数")
context_params: Optional[Dict[str, Any]] = Field(None, description="上下文参数")
# API接口
@router.post("/create", response_model=ApiResponse)
async def create_task_instance(instance_input: TaskInstanceCreateInput):
"""创建任务实例"""
try:
# 创建任务实例
instance = task_instance_service.create_instance(
task_id=instance_input.task_id,
name=instance_input.name,
variables=instance_input.variables,
priority=instance_input.priority,
input_params=instance_input.input_params,
block_outputs=instance_input.block_outputs,
context_params=instance_input.context_params
)
return {
"code": 200,
"message": "创建任务实例成功",
"data": instance
}
except ValueError as e:
return {
"code": 400,
"message": str(e),
"data": None
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"创建任务实例失败: {str(e)}")
@router.get("/{instance_id}", response_model=ApiResponse)
async def get_task_instance(instance_id: str):
"""获取任务实例详情"""
try:
# 获取任务实例
instance = task_instance_service.get_instance_by_id(instance_id)
if not instance:
return {
"code": 404,
"message": f"任务实例不存在: {instance_id}",
"data": None
}
return {
"code": 200,
"message": "获取任务实例成功",
"data": instance
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取任务实例失败: {str(e)}")
@router.get("/task/{task_id}", response_model=ApiResponse)
async def get_task_instances(task_id: str):
"""获取任务的所有实例"""
try:
# 获取任务实例
instances = task_instance_service.get_instances_by_task_id(task_id)
return {
"code": 200,
"message": "获取任务实例列表成功",
"data": {
"instances": instances,
"total": len(instances)
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取任务实例列表失败: {str(e)}")
@router.get("/task/{task_id}/latest", response_model=ApiResponse)
async def get_latest_task_instance(task_id: str):
"""获取任务的最新实例"""
try:
# 获取最新任务实例
instance = task_instance_service.get_latest_instance_by_task_id(task_id)
if not instance:
return {
"code": 404,
"message": f"任务 {task_id} 没有实例",
"data": None
}
return {
"code": 200,
"message": "获取最新任务实例成功",
"data": instance
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取最新任务实例失败: {str(e)}")
@router.put("/{instance_id}", response_model=ApiResponse)
async def update_task_instance(instance_id: str, instance_input: TaskInstanceUpdateInput):
"""更新任务实例"""
try:
# 更新任务实例
instance = task_instance_service.update_instance(
instance_id=instance_id,
variables=instance_input.variables,
priority=instance_input.priority,
input_params=instance_input.input_params,
block_outputs=instance_input.block_outputs,
context_params=instance_input.context_params
)
if not instance:
return {
"code": 404,
"message": f"任务实例不存在: {instance_id}",
"data": None
}
return {
"code": 200,
"message": "更新任务实例成功",
"data": instance
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"更新任务实例失败: {str(e)}")
@router.delete("/{instance_id}", response_model=ApiResponse)
async def delete_task_instance(instance_id: str):
"""删除任务实例"""
try:
# 删除任务实例
success = task_instance_service.delete_instance(instance_id)
if not success:
return {
"code": 404,
"message": f"任务实例不存在: {instance_id}",
"data": None
}
return {
"code": 200,
"message": "删除任务实例成功",
"data": None
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"删除任务实例失败: {str(e)}")
@router.post("/{instance_id}/publish", response_model=ApiResponse)
async def publish_task_instance(instance_id: str):
"""发布任务实例"""
try:
# 发布任务实例
instance = task_instance_service.publish_instance(instance_id)
if not instance:
return {
"code": 404,
"message": f"任务实例不存在: {instance_id}",
"data": None
}
return {
"code": 200,
"message": "发布任务实例成功",
"data": instance
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"发布任务实例失败: {str(e)}")
@router.get("/task/{task_id}/editing", response_model=ApiResponse)
async def get_or_create_editing_instance(task_id: str):
"""获取或创建编辑中的任务实例"""
try:
# 获取或创建编辑中的任务实例
instance = task_instance_service.get_or_create_editing_instance(task_id)
return {
"code": 200,
"message": "获取编辑中的任务实例成功",
"data": instance
}
except ValueError as e:
return {
"code": 404,
"message": str(e),
"data": None
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取编辑中的任务实例失败: {str(e)}")

View File

@ -4,9 +4,12 @@ from fastapi import APIRouter, HTTPException, Depends, Query
from pydantic import BaseModel, Field
from services.workflow_service import WorkflowService
from core.exceptions import TianfengTaskError
from api.models import (
ApiResponse, WorkflowInput, WorkflowUpdateInput,
WorkflowExecuteInput, WorkflowImportInput
)
from core.workflow import WorkflowDefinition
from config.api_config import ApiResponseCode, ApiResponseMessage
from api.models import ApiResponse, WorkflowInput, WorkflowUpdateInput, WorkflowExecuteInput, WorkflowImportInput
# 创建路由器
router = APIRouter(prefix="/workflow", tags=["工作流管理"])

4
app.py
View File

@ -5,13 +5,14 @@ from fastapi.middleware.cors import CORSMiddleware
import logging
import time
from utils.logger import setup_logger
from config.component_registry import register_all_components
from config.component_config import register_all_components
from config.settings import (
AppConfig, ServerConfig, ApiConfig, CorsConfig
)
from api.task_api import router as task_router
from api.workflow_api import router as workflow_router
from api.component_api import router as component_router
from api.common_params_api import router as common_params_router
from core.exceptions import TianfengTaskError
from config.api_config import ApiResponseCode, ApiResponseMessage
from config.component_config import ComponentCategoryConfig
@ -120,6 +121,7 @@ register_all_components()
app.include_router(task_router, prefix=ApiConfig.PREFIX)
app.include_router(workflow_router, prefix=ApiConfig.PREFIX)
app.include_router(component_router, prefix=ApiConfig.PREFIX)
app.include_router(common_params_router, prefix=ApiConfig.PREFIX)
# 请求中间件
@app.middleware("http")

View File

@ -1,15 +1,185 @@
"""
组件配置文件
包含组件发现注册和分类的相关配置
组件配置模块
包含组件类型详细配置注册和常用参数等相关配置信息
"""
from typing import Dict, List, Any
from data.models.component import ComponentCategoryEnum
from typing import Dict, List, Any, Optional
from enum import Enum
from core.component import ComponentFactory
import importlib
from config.settings import COMPONENT_PACKAGES
from utils.logger import get_logger
# 获取日志记录器
logger = get_logger(__name__)
#################################################
# 组件初始化配置
#################################################
# 组件类别配置
COMPONENT_CATEGORIES = [
{"name": "基础组件", "code": "basic", "description": "基础功能组件", "sort_order": 1},
{"name": "流程组件", "code": "flow", "description": "流程控制组件", "sort_order": 2},
{"name": "机器人组件", "code": "robot", "description": "机器人控制组件", "sort_order": 3},
{"name": "库位组件", "code": "storage", "description": "库位管理组件", "sort_order": 4},
{"name": "设备组件", "code": "device", "description": "设备控制组件", "sort_order": 5},
{"name": "HTTP组件", "code": "http", "description": "HTTP请求组件", "sort_order": 6},
{"name": "脚本组件", "code": "script", "description": "脚本执行组件", "sort_order": 7},
{"name": "子任务组件", "code": "subtask", "description": "子任务组件", "sort_order": 8},
{"name": "任务组件", "code": "task", "description": "任务管理组件", "sort_order": 9}
]
# 组件类型配置
def get_component_types(categories):
"""
获取组件类型配置
Args:
categories: 类别ID映射字典 {类别代码: 类别ID}
Returns:
list: 组件类型配置列表
"""
return [
# 基础组件
{"name": "变量赋值", "code": "variable_assign", "category_id": categories["basic"], "description": "为变量赋值", "icon": "variable", "sort_order": 1},
{"name": "条件判断", "code": "condition", "category_id": categories["basic"], "description": "条件判断", "icon": "condition", "sort_order": 2},
{"name": "延时等待", "code": "delay", "category_id": categories["basic"], "description": "延时等待", "icon": "delay", "sort_order": 3},
# 流程组件
{"name": "If条件", "code": "if", "category_id": categories["flow"], "description": "If条件判断", "icon": "if", "sort_order": 1},
{"name": "If-Else条件", "code": "if_else", "category_id": categories["flow"], "description": "If-Else条件判断", "icon": "if_else", "sort_order": 2},
{"name": "循环", "code": "loop", "category_id": categories["flow"], "description": "循环执行", "icon": "loop", "sort_order": 3},
{"name": "并行执行", "code": "parallel", "category_id": categories["flow"], "description": "并行执行多个分支", "icon": "parallel", "sort_order": 4},
# 机器人组件
{"name": "选择机器人", "code": "select_robot", "category_id": categories["robot"], "description": "选择执行机器人", "icon": "robot", "sort_order": 1},
{"name": "机器人移动", "code": "robot_move", "category_id": categories["robot"], "description": "控制机器人移动", "icon": "move", "sort_order": 2},
{"name": "获取机器人状态", "code": "robot_status", "category_id": categories["robot"], "description": "获取机器人状态", "icon": "status", "sort_order": 3},
# HTTP组件
{"name": "HTTP请求", "code": "http_request", "category_id": categories["http"], "description": "发送HTTP请求", "icon": "http", "sort_order": 1},
{"name": "API调用", "code": "api_call", "category_id": categories["http"], "description": "调用系统API", "icon": "api", "sort_order": 2},
# 脚本组件
{"name": "JavaScript脚本", "code": "javascript", "category_id": categories["script"], "description": "执行JavaScript脚本", "icon": "script", "sort_order": 1},
# 子任务组件
{"name": "执行子任务", "code": "execute_subtask", "category_id": categories["subtask"], "description": "执行子任务", "icon": "subtask", "sort_order": 1}
]
# 系统组件配置
def get_system_components(types):
"""
获取系统组件配置
Args:
types: 组件类型ID映射字典 {类型代码: 类型ID}
Returns:
list: 系统组件配置列表
"""
return [
{
"name": "变量赋值",
"code": "variable_assign",
"type_id": types["variable_assign"],
"description": "为变量赋值",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"variable_name": {"type": "string", "title": "变量名"},
"value_type": {"type": "string", "enum": ["string", "number", "boolean", "object"], "title": "值类型"},
"value": {"type": "string", "title": ""}
},
"required": ["variable_name", "value_type", "value"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "执行结果"}
}
}
},
{
"name": "条件判断",
"code": "condition",
"type_id": types["condition"],
"description": "条件判断",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"condition": {"type": "string", "title": "条件表达式"}
},
"required": ["condition"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "判断结果"}
}
}
},
{
"name": "If条件",
"code": "if",
"type_id": types["if"],
"description": "If条件判断",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"condition": {"type": "string", "title": "条件表达式"}
},
"required": ["condition"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "判断结果"}
}
}
}
]
# 缓存配置
CACHE_EXPIRE_TIME = 86400 # 缓存过期时间单位24小时
CACHE_KEYS = {
"COMPONENT_CATEGORIES": "component_categories",
"COMPONENT_TYPES": "component_types",
"SYSTEM_COMPONENTS": "system_components"
}
#################################################
# 组件自动发现与注册
#################################################
class ComponentDiscovery:
"""组件自动发现配置"""
DEFAULT_PACKAGE = "components" # 默认组件包名
AUTO_REGISTER = True # 是否在启动时自动注册所有组件
def register_all_components():
"""注册所有组件"""
for package_name in COMPONENT_PACKAGES:
try:
ComponentFactory.auto_discover(package_name)
logger.info(f"自动注册组件包: {package_name}")
except ImportError:
logger.error(f"导入组件包失败: {package_name}")
except Exception as e:
logger.error(f"注册组件包失败: {package_name}, 错误: {str(e)}")
#################################################
# 组件分类配置
#################################################
class ComponentCategory:
"""组件类别配置"""
# 组件类别定义
@ -81,6 +251,8 @@ class ComponentCategoryConfig:
@classmethod
def get_category_name(cls, category_enum):
"""获取分类名称"""
from data.models.component import ComponentCategoryEnum
names = {
ComponentCategoryEnum.SUBTASK: "子任务",
ComponentCategoryEnum.SCRIPT: "脚本",
@ -97,6 +269,8 @@ class ComponentCategoryConfig:
@classmethod
def get_category_description(cls, category_enum):
"""获取分类描述"""
from data.models.component import ComponentCategoryEnum
descriptions = {
ComponentCategoryEnum.SUBTASK: "可重用的子任务组件",
ComponentCategoryEnum.SCRIPT: "执行脚本代码的组件",
@ -113,6 +287,8 @@ class ComponentCategoryConfig:
@classmethod
def get_category_order(cls, category_enum):
"""获取分类排序"""
from data.models.component import ComponentCategoryEnum
orders = {
ComponentCategoryEnum.BASIC: 1,
ComponentCategoryEnum.FLOW: 2,
@ -124,4 +300,607 @@ class ComponentCategoryConfig:
ComponentCategoryEnum.STORAGE: 8,
ComponentCategoryEnum.DEVICE: 9
}
return orders.get(category_enum, 99)
return orders.get(category_enum, 99)
#################################################
# 组件详细配置
#################################################
class ScriptComponentConfig:
"""脚本组件配置"""
# 脚本组件类型
RUN_SCRIPT = "run_script" # 运行脚本
SET_VARIABLES = "set_variables" # 设置task.variables
# 脚本组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取脚本组件列表"""
return [
{
"type": "script",
"sub_type": cls.RUN_SCRIPT,
"name": "运行脚本",
"description": "执行JavaScript代码并返回结果",
"icon": "code", # 图标名称,前端可用
"params": [
{
"name": "function_name",
"label": "函数名",
"type": "string",
"required": False,
"description": "定义脚本中的主函数名称",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
},
{
"name": "params",
"label": "函数参数",
"type": "array",
"required": False,
"description": "传递给脚本的参数",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
}
]
},
{
"type": "script",
"sub_type": cls.SET_VARIABLES,
"name": "设置task.variables",
"description": "设置和管理任务变量",
"icon": "variable",
"params": [
{
"name": "function_name",
"label": "函数名",
"type": "string",
"required": False,
"description": "定义脚本中的主函数名称",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
},
{
"name": "params",
"label": "函数参数",
"type": "array",
"required": False,
"description": "传递给脚本的参数",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
}
]
}
]
class HttpComponentConfig:
"""HTTP请求组件配置"""
# HTTP请求组件类型
HTTP_REQUEST = "http_request" # HTTP请求
# HTTP请求组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取HTTP请求组件列表"""
return [
{
"type": "http",
"sub_type": cls.HTTP_REQUEST,
"name": "HTTP请求",
"description": "发送HTTP请求并处理响应",
"icon": "http",
"params": [
{
"name": "method",
"label": "请求方法",
"type": "select",
"options": ["GET", "POST", "PUT", "DELETE", "PATCH"],
"required": True,
"description": "HTTP请求方法"
},
{
"name": "url",
"label": "请求URL",
"type": "string",
"required": True,
"description": "请求的目标URL"
},
{
"name": "headers",
"label": "请求头",
"type": "object",
"required": False,
"description": "HTTP请求头"
},
{
"name": "body",
"label": "请求体",
"type": "object",
"required": False,
"description": "HTTP请求体"
},
{
"name": "timeout",
"label": "超时时间",
"type": "number",
"required": False,
"description": "请求超时时间(毫秒)"
}
]
}
]
class FlowComponentConfig:
"""流程控制组件配置"""
# 流程控制组件类型
IF = "if" # 条件判断
IF_ELSE = "if_else" # 条件分支
FOR_EACH = "for_each" # 循环遍历
WHILE = "while" # 条件循环
# 流程控制组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取流程控制组件列表"""
return [
{
"type": "flow",
"sub_type": cls.IF,
"name": "条件判断",
"description": "根据条件执行不同的操作",
"icon": "branch",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "条件判断表达式"
}
]
},
{
"type": "flow",
"sub_type": cls.IF_ELSE,
"name": "条件分支",
"description": "根据条件执行不同的分支",
"icon": "branch-multiple",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "条件判断表达式"
}
]
},
{
"type": "flow",
"sub_type": cls.FOR_EACH,
"name": "循环遍历",
"description": "遍历数组或对象的每个元素",
"icon": "loop",
"params": [
{
"name": "collection",
"label": "集合表达式",
"type": "expression",
"required": True,
"description": "要遍历的数组或对象"
},
{
"name": "item_name",
"label": "元素变量名",
"type": "string",
"required": True,
"description": "当前元素的变量名"
},
{
"name": "index_name",
"label": "索引变量名",
"type": "string",
"required": False,
"description": "当前索引的变量名"
}
]
},
{
"type": "flow",
"sub_type": cls.WHILE,
"name": "条件循环",
"description": "当条件为真时重复执行",
"icon": "loop-circular",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "循环条件表达式"
}
]
}
]
class SubtaskComponentConfig:
"""子任务组件配置"""
# 子任务组件类型
SUBTASK = "subtask" # 子任务
# 子任务组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取子任务组件列表"""
return [
{
"type": "subtask",
"sub_type": cls.SUBTASK,
"name": "子任务",
"description": "执行已定义的任务作为子任务",
"icon": "subtask",
"params": [
{
"name": "task_id",
"label": "选择子任务",
"type": "select",
"required": True,
"description": "选择要执行的子任务(从已创建的任务中选择)",
"data_source": "available_subtasks", # 指示前端从API返回的available_subtasks字段获取数据
"display_field": "name", # 显示任务名称
"value_field": "task_id" # 使用任务ID作为值
},
{
"name": "params",
"label": "任务参数",
"type": "object",
"required": False,
"description": "传递给子任务的参数"
},
{
"name": "wait_complete",
"label": "等待完成",
"type": "boolean",
"required": False,
"default": True,
"description": "是否等待子任务完成后再继续执行"
}
]
}
]
class ComponentDetailConfig:
"""组件详细配置管理"""
# 组件类型中文名称映射
@classmethod
def get_component_type_names(cls) -> Dict[str, str]:
"""获取组件类型的中文名称映射"""
return {
# 基础类型
"script": "脚本",
"http": "HTTP请求",
"flow": "流程",
"robot": "机器人调度",
"site": "库位",
"device": "设备",
"subtask": "子任务",
"task": "任务",
"basic": "基础",
# 脚本组件
"run_script": "运行脚本",
"set_task_variables": "设置任务变量",
"runscript": "运行脚本",
"settaskvariables": "设置任务变量",
# HTTP请求组件
"http_get_request": "GET请求",
"http_post_request": "POST请求",
"httpgetrequest": "GET请求",
"httppostrequest": "POST请求",
# 流程控制组件
"if": "条件判断",
"if_else": "条件分支",
"if_else_if": "多条件分支",
"for_each": "循环遍历",
"while": "条件循环",
"break": "跳出循环",
"return": "返回",
"delay": "延时",
"parallel_execute": "并行执行",
"serial_execute": "串行执行",
"throw_exception": "抛出异常",
"foreach": "循环遍历",
"ifelse": "条件分支",
"ifelseif": "多条件分支",
"parallelexecute": "并行执行",
"serialexecute": "串行执行",
"throwexception": "抛出异常",
# 机器人调度组件
"select_robot": "选择机器人",
"get_robot_position": "获取机器人位置",
"robot_action": "机器人动作",
"change_robot_destination": "更改机器人目的地",
"get_robot_battery": "获取机器人电量",
"get_robot_pgv_code": "获取机器人PGV码",
"changerobotdestination": "更改机器人目的地",
"getrobotbattery": "获取机器人电量",
"getrobotpgvcode": "获取机器人PGV码",
"getrobotposition": "获取机器人位置",
"robotaction": "机器人动作",
"selectrobot": "选择机器人",
# 库位组件
"batch_set_site": "批量设置库位",
"get_dense_site": "获取密集库位",
"query_site": "查询库位",
"lock_site": "锁定库位",
"unlock_site": "解锁库位",
"get_locked_sites_by_task": "获取任务锁定的库位",
"get_site_extension_property": "获取库位扩展属性",
"set_site_extension_property": "设置库位扩展属性",
"set_site_goods": "设置库位货物",
"set_site_empty": "设置库位为空",
"set_site_occupied": "设置库位为占用",
"set_site_tag": "设置库位标签",
"batchsetsite": "批量设置库位",
"getdensesite": "获取密集库位",
"getlockedsitesbytask": "获取任务锁定的库位",
"getsiteextensionproperty": "获取库位扩展属性",
"locksite": "锁定库位",
"querysite": "查询库位",
"setsiteempty": "设置库位为空",
"setsiteextensionproperty": "设置库位扩展属性",
"setsitegoods": "设置库位货物",
"setsiteoccupied": "设置库位为占用",
"setsitetag": "设置库位标签",
"unlocksite": "解锁库位",
# 任务组件
"cache_data": "缓存数据",
"clear_cache_data": "清除缓存数据",
"get_cache_data": "获取缓存数据",
"set_task_status": "设置任务状态",
"jump_to_block": "跳转到块",
"get_task_input_param": "获取任务输入参数",
"cachedata": "缓存数据",
"clearcachedata": "清除缓存数据",
"getcachedata": "获取缓存数据",
"gettaskinputparam": "获取任务输入参数",
"jumptoblock": "跳转到块",
"settaskstatus": "设置任务状态",
# 基础组件
"check_task_instance_id_exists": "检查任务实例ID是否存在",
"create_unique_id": "创建唯一ID",
"current_timestamp": "当前时间戳",
"current_time": "当前时间",
"execute_sql": "执行SQL",
"query_sql": "查询SQL",
"string_md5_encrypt": "字符串MD5加密",
"string_to_json_array": "字符串转JSON数组",
"string_to_json_object": "字符串转JSON对象",
"print": "打印",
"checktaskinstanceidexists": "检查任务实例ID是否存在",
"createuniqueid": "创建唯一ID",
"currenttime": "当前时间",
"currenttimestamp": "当前时间戳",
"executesql": "执行SQL",
"querysql": "查询SQL",
"stringmd5encrypt": "字符串MD5加密",
"stringtojsonarray": "字符串转JSON数组",
"stringtojsonobject": "字符串转JSON对象",
# 设备组件
"wait_modbus_value": "等待Modbus值",
"write_modbus_value": "写入Modbus值",
"waitmodbusvalue": "等待Modbus值",
"writemodbusvalue": "写入Modbus值"
}
@classmethod
def get_all_components(cls) -> List[Dict[str, Any]]:
"""获取所有组件详细配置"""
all_components = []
# 添加子任务组件(放在第一位)
all_components.extend(SubtaskComponentConfig.get_components())
# 添加脚本组件
all_components.extend(ScriptComponentConfig.get_components())
# 添加HTTP请求组件
all_components.extend(HttpComponentConfig.get_components())
# 添加流程控制组件
all_components.extend(FlowComponentConfig.get_components())
# 可以继续添加其他类型的组件...
return all_components
@classmethod
def get_components_by_type(cls, component_type: str) -> List[Dict[str, Any]]:
"""根据组件类型获取组件列表"""
all_components = cls.get_all_components()
return [comp for comp in all_components if comp["type"] == component_type]
#################################################
# 常用参数配置
#################################################
class CommonParamType(str, Enum):
"""常用参数类型枚举"""
ROBOT_ID = "robot_id" # 机器人ID
ROBOT_GROUP = "robot_group" # 机器人组
ROBOT_TAG = "robot_tag" # 机器人标签
STORAGE_AREA_ID = "storage_area_id" # 库区ID
STORAGE_AREA = "storage_area" # 库区
SITE = "site" # 站点
BIN_TASK = "bin_task" # binTask
WORKSTATION = "workstation" # 工位
POST = "post" # 岗位
USER = "user" # 用户
CACHE = "cache" # 缓存
BUILT_IN_FUNCTION = "built_in_function" # 内置函数
class CommonParamsConfig:
"""常用参数配置类"""
@classmethod
def get_param_types(cls) -> List[Dict[str, Any]]:
"""获取所有常用参数类型"""
return [
{
"type": CommonParamType.ROBOT_ID,
"name": "机器人ID",
"description": "选择机器人ID",
"api_path": "/api/robots/ids", # 获取数据的API路径
"value_field": "id", # 值字段
"display_field": "name" # 显示字段
},
{
"type": CommonParamType.ROBOT_GROUP,
"name": "机器人组",
"description": "选择机器人组",
"api_path": "/api/robots/groups",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.ROBOT_TAG,
"name": "机器人标签",
"description": "选择机器人标签",
"api_path": "/api/robots/tags",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.STORAGE_AREA_ID,
"name": "库区ID",
"description": "选择库区ID",
"api_path": "/api/storage/areas/ids",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.STORAGE_AREA,
"name": "库区",
"description": "选择库区",
"api_path": "/api/storage/areas",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.SITE,
"name": "站点",
"description": "选择站点",
"api_path": "/api/sites",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.BIN_TASK,
"name": "binTask",
"description": "选择binTask",
"api_path": "/api/bin-tasks",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.WORKSTATION,
"name": "工位",
"description": "选择工位",
"api_path": "/api/workstations",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.POST,
"name": "岗位",
"description": "选择岗位",
"api_path": "/api/posts",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.USER,
"name": "用户",
"description": "选择用户",
"api_path": "/api/users",
"value_field": "id",
"display_field": "name"
},
{
"type": CommonParamType.CACHE,
"name": "缓存",
"description": "选择缓存",
"api_path": "/api/caches",
"value_field": "key",
"display_field": "key"
},
{
"type": CommonParamType.BUILT_IN_FUNCTION,
"name": "内置函数",
"description": "选择内置函数",
"api_path": "/api/built-in-functions",
"value_field": "name",
"display_field": "name"
}
]
@classmethod
def get_param_type_by_type(cls, param_type: str) -> Optional[Dict[str, Any]]:
"""根据类型获取参数类型配置"""
param_types = cls.get_param_types()
for pt in param_types:
if pt["type"] == param_type:
return pt
return None

View File

@ -1,472 +0,0 @@
"""
组件详细配置文件
包含各种组件类型及其子组件的配置信息
"""
from typing import Dict, List, Any, Optional
class ScriptComponentConfig:
"""脚本组件配置"""
# 脚本组件类型
RUN_SCRIPT = "run_script" # 运行脚本
SET_VARIABLES = "set_variables" # 设置task.variables
# 脚本组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取脚本组件列表"""
return [
{
"type": "script",
"sub_type": cls.RUN_SCRIPT,
"name": "运行脚本",
"description": "执行JavaScript代码并返回结果",
"icon": "code", # 图标名称,前端可用
"params": [
{
"name": "function_name",
"label": "函数名",
"type": "string",
"required": False,
"description": "定义脚本中的主函数名称",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
},
{
"name": "params",
"label": "函数参数",
"type": "array",
"required": False,
"description": "传递给脚本的参数",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
}
]
},
{
"type": "script",
"sub_type": cls.SET_VARIABLES,
"name": "设置task.variables",
"description": "设置和管理任务变量",
"icon": "variable",
"params": [
{
"name": "function_name",
"label": "函数名",
"type": "string",
"required": False,
"description": "定义脚本中的主函数名称",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
},
{
"name": "params",
"label": "函数参数",
"type": "array",
"required": False,
"description": "传递给脚本的参数",
"value_types": [
{
"type": "simple",
"label": "简单值",
"default": True
},
{
"type": "expression",
"label": "表达式",
"default": False
}
]
}
]
}
]
class HttpComponentConfig:
"""HTTP请求组件配置"""
# HTTP请求组件类型
HTTP_REQUEST = "http_request" # HTTP请求
# HTTP请求组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取HTTP请求组件列表"""
return [
{
"type": "http",
"sub_type": cls.HTTP_REQUEST,
"name": "HTTP请求",
"description": "发送HTTP请求并处理响应",
"icon": "http",
"params": [
{
"name": "method",
"label": "请求方法",
"type": "select",
"options": ["GET", "POST", "PUT", "DELETE", "PATCH"],
"required": True,
"description": "HTTP请求方法"
},
{
"name": "url",
"label": "请求URL",
"type": "string",
"required": True,
"description": "请求的目标URL"
},
{
"name": "headers",
"label": "请求头",
"type": "object",
"required": False,
"description": "HTTP请求头"
},
{
"name": "body",
"label": "请求体",
"type": "object",
"required": False,
"description": "HTTP请求体"
},
{
"name": "timeout",
"label": "超时时间",
"type": "number",
"required": False,
"description": "请求超时时间(毫秒)"
}
]
}
]
class FlowComponentConfig:
"""流程控制组件配置"""
# 流程控制组件类型
IF = "if" # 条件判断
IF_ELSE = "if_else" # 条件分支
FOR_EACH = "for_each" # 循环遍历
WHILE = "while" # 条件循环
# 流程控制组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取流程控制组件列表"""
return [
{
"type": "flow",
"sub_type": cls.IF,
"name": "条件判断",
"description": "根据条件执行不同的操作",
"icon": "branch",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "条件判断表达式"
}
]
},
{
"type": "flow",
"sub_type": cls.IF_ELSE,
"name": "条件分支",
"description": "根据条件执行不同的分支",
"icon": "branch-multiple",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "条件判断表达式"
}
]
},
{
"type": "flow",
"sub_type": cls.FOR_EACH,
"name": "循环遍历",
"description": "遍历数组或对象的每个元素",
"icon": "loop",
"params": [
{
"name": "collection",
"label": "集合表达式",
"type": "expression",
"required": True,
"description": "要遍历的数组或对象"
},
{
"name": "item_name",
"label": "元素变量名",
"type": "string",
"required": True,
"description": "当前元素的变量名"
},
{
"name": "index_name",
"label": "索引变量名",
"type": "string",
"required": False,
"description": "当前索引的变量名"
}
]
},
{
"type": "flow",
"sub_type": cls.WHILE,
"name": "条件循环",
"description": "当条件为真时重复执行",
"icon": "loop-circular",
"params": [
{
"name": "condition",
"label": "条件表达式",
"type": "expression",
"required": True,
"description": "循环条件表达式"
}
]
}
]
class SubtaskComponentConfig:
"""子任务组件配置"""
# 子任务组件类型
SUBTASK = "subtask" # 子任务
# 子任务组件详细配置
@classmethod
def get_components(cls) -> List[Dict[str, Any]]:
"""获取子任务组件列表"""
return [
{
"type": "subtask",
"sub_type": cls.SUBTASK,
"name": "子任务",
"description": "执行已定义的任务作为子任务",
"icon": "subtask",
"params": [
{
"name": "task_id",
"label": "选择子任务",
"type": "select",
"required": True,
"description": "选择要执行的子任务(从已创建的任务中选择)",
"data_source": "available_subtasks", # 指示前端从API返回的available_subtasks字段获取数据
"display_field": "name", # 显示任务名称
"value_field": "task_id" # 使用任务ID作为值
},
{
"name": "params",
"label": "任务参数",
"type": "object",
"required": False,
"description": "传递给子任务的参数"
},
{
"name": "wait_complete",
"label": "等待完成",
"type": "boolean",
"required": False,
"default": True,
"description": "是否等待子任务完成后再继续执行"
}
]
}
]
# 组件配置管理类
class ComponentDetailConfig:
"""组件详细配置管理"""
# 组件类型中文名称映射
@classmethod
def get_component_type_names(cls) -> Dict[str, str]:
"""获取组件类型的中文名称映射"""
return {
# 基础类型
"script": "脚本",
"http": "HTTP请求",
"flow": "流程",
"robot": "机器人调度",
"site": "库位",
"device": "设备",
"subtask": "子任务",
"task": "任务",
"basic": "基础",
# 脚本组件
"run_script": "运行脚本",
"set_task_variables": "设置任务变量",
"runscript": "运行脚本",
"settaskvariables": "设置任务变量",
# HTTP请求组件
"http_get_request": "GET请求",
"http_post_request": "POST请求",
"httpgetrequest": "GET请求",
"httppostrequest": "POST请求",
# 流程控制组件
"if": "条件判断",
"if_else": "条件分支",
"if_else_if": "多条件分支",
"for_each": "循环遍历",
"while": "条件循环",
"break": "跳出循环",
"return": "返回",
"delay": "延时",
"parallel_execute": "并行执行",
"serial_execute": "串行执行",
"throw_exception": "抛出异常",
"foreach": "循环遍历",
"ifelse": "条件分支",
"ifelseif": "多条件分支",
"parallelexecute": "并行执行",
"serialexecute": "串行执行",
"throwexception": "抛出异常",
# 机器人调度组件
"select_robot": "选择机器人",
"get_robot_position": "获取机器人位置",
"robot_action": "机器人动作",
"change_robot_destination": "更改机器人目的地",
"get_robot_battery": "获取机器人电量",
"get_robot_pgv_code": "获取机器人PGV码",
"changerobotdestination": "更改机器人目的地",
"getrobotbattery": "获取机器人电量",
"getrobotpgvcode": "获取机器人PGV码",
"getrobotposition": "获取机器人位置",
"robotaction": "机器人动作",
"selectrobot": "选择机器人",
# 库位组件
"batch_set_site": "批量设置库位",
"get_dense_site": "获取密集库位",
"query_site": "查询库位",
"lock_site": "锁定库位",
"unlock_site": "解锁库位",
"get_locked_sites_by_task": "获取任务锁定的库位",
"get_site_extension_property": "获取库位扩展属性",
"set_site_extension_property": "设置库位扩展属性",
"set_site_goods": "设置库位货物",
"set_site_empty": "设置库位为空",
"set_site_occupied": "设置库位为占用",
"set_site_tag": "设置库位标签",
"batchsetsite": "批量设置库位",
"getdensesite": "获取密集库位",
"getlockedsitesbytask": "获取任务锁定的库位",
"getsiteextensionproperty": "获取库位扩展属性",
"locksite": "锁定库位",
"querysite": "查询库位",
"setsiteempty": "设置库位为空",
"setsiteextensionproperty": "设置库位扩展属性",
"setsitegoods": "设置库位货物",
"setsiteoccupied": "设置库位为占用",
"setsitetag": "设置库位标签",
"unlocksite": "解锁库位",
# 任务组件
"cache_data": "缓存数据",
"clear_cache_data": "清除缓存数据",
"get_cache_data": "获取缓存数据",
"set_task_status": "设置任务状态",
"jump_to_block": "跳转到块",
"get_task_input_param": "获取任务输入参数",
"cachedata": "缓存数据",
"clearcachedata": "清除缓存数据",
"getcachedata": "获取缓存数据",
"gettaskinputparam": "获取任务输入参数",
"jumptoblock": "跳转到块",
"settaskstatus": "设置任务状态",
# 基础组件
"check_task_instance_id_exists": "检查任务实例ID是否存在",
"create_unique_id": "创建唯一ID",
"current_timestamp": "当前时间戳",
"current_time": "当前时间",
"execute_sql": "执行SQL",
"query_sql": "查询SQL",
"string_md5_encrypt": "字符串MD5加密",
"string_to_json_array": "字符串转JSON数组",
"string_to_json_object": "字符串转JSON对象",
"print": "打印",
"checktaskinstanceidexists": "检查任务实例ID是否存在",
"createuniqueid": "创建唯一ID",
"currenttime": "当前时间",
"currenttimestamp": "当前时间戳",
"executesql": "执行SQL",
"querysql": "查询SQL",
"stringmd5encrypt": "字符串MD5加密",
"stringtojsonarray": "字符串转JSON数组",
"stringtojsonobject": "字符串转JSON对象",
# 设备组件
"wait_modbus_value": "等待Modbus值",
"write_modbus_value": "写入Modbus值",
"waitmodbusvalue": "等待Modbus值",
"writemodbusvalue": "写入Modbus值"
}
@classmethod
def get_all_components(cls) -> List[Dict[str, Any]]:
"""获取所有组件详细配置"""
all_components = []
# 添加子任务组件(放在第一位)
all_components.extend(SubtaskComponentConfig.get_components())
# 添加脚本组件
all_components.extend(ScriptComponentConfig.get_components())
# 添加HTTP请求组件
all_components.extend(HttpComponentConfig.get_components())
# 添加流程控制组件
all_components.extend(FlowComponentConfig.get_components())
# 可以继续添加其他类型的组件...
return all_components
@classmethod
def get_components_by_type(cls, component_type: str) -> List[Dict[str, Any]]:
"""根据组件类型获取组件列表"""
all_components = cls.get_all_components()
return [comp for comp in all_components if comp["type"] == component_type]

View File

@ -1,23 +0,0 @@
"""
组件注册表
"""
# config/component_registry.py
from core.component import ComponentFactory
import importlib
from config.settings import COMPONENT_PACKAGES
from utils.logger import get_logger
# 获取日志记录器
logger = get_logger(__name__)
def register_all_components():
"""注册所有组件"""
for package_name in COMPONENT_PACKAGES:
try:
ComponentFactory.auto_discover(package_name)
logger.info(f"自动注册组件包: {package_name}")
except ImportError:
logger.error(f"导入组件包失败: {package_name}")
except Exception as e:
logger.error(f"注册组件包失败: {package_name}, 错误: {str(e)}")

View File

@ -3,27 +3,21 @@
"""
任务配置模块
包含任务类型状态等配置信息
包含任务类型状态输入参数等配置信息
"""
from enum import Enum
from typing import Dict, Any, List, Optional
#################################################
# 任务类型相关配置
#################################################
# 任务类型枚举
class TaskType(str, Enum):
"""任务类型枚举"""
NORMAL = "NORMAL"
SCHEDULED = "SCHEDULED"
# 任务状态枚举
class TaskStatus(str, Enum):
PENDING = "PENDING"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
CANCELLED = "CANCELLED"
FAILED = "FAILED"
PAUSED = "PAUSED"
WAITING = "WAITING"
# 任务类型配置
class TaskTypeConfig:
"""任务类型配置"""
NORMAL = TaskType.NORMAL.value
@ -84,8 +78,20 @@ class TaskTypeConfig:
})
return task_types
#################################################
# 任务状态相关配置
#################################################
class TaskStatus(str, Enum):
"""任务状态枚举"""
PENDING = "PENDING"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
CANCELLED = "CANCELLED"
FAILED = "FAILED"
PAUSED = "PAUSED"
WAITING = "WAITING"
# 任务状态配置
class TaskStatusConfig:
"""任务状态配置"""
PENDING = TaskStatus.PENDING.value
@ -171,15 +177,148 @@ class TaskStatusConfig:
})
return task_statuses
#################################################
# 任务输入参数相关配置
#################################################
class TaskInputParamType(str, Enum):
"""任务输入参数类型枚举"""
STRING = "string"
INTEGER = "integer"
FLOAT = "float"
BOOLEAN = "boolean"
DATETIME = "datetime"
JSON = "json"
ARRAY = "array"
OBJECT = "object"
class SystemParamKey(str, Enum):
"""系统参数键名枚举"""
TASK_ID = "task_id"
INSTANCE_ID = "instance_id"
TASK_NAME = "task_name"
CREATED_AT = "created_at"
VARIABLES = "variables"
PRIORITY = "priority"
class TaskInputParamConfig:
"""任务输入参数配置类"""
# 系统默认参数配置
SYSTEM_PARAMS = [
{
"key": SystemParamKey.TASK_ID,
"name": "任务ID",
"type": TaskInputParamType.STRING,
"is_system": True,
"is_readonly": True,
"description": "任务的唯一标识符"
},
{
"key": SystemParamKey.INSTANCE_ID,
"name": "任务实例ID",
"type": TaskInputParamType.STRING,
"is_system": True,
"is_readonly": True,
"description": "任务实例的唯一标识符"
},
{
"key": SystemParamKey.TASK_NAME,
"name": "任务名称",
"type": TaskInputParamType.STRING,
"is_system": True,
"is_readonly": True,
"description": "任务的名称"
},
{
"key": SystemParamKey.CREATED_AT,
"name": "创建时间",
"type": TaskInputParamType.DATETIME,
"is_system": True,
"is_readonly": True,
"description": "任务实例的创建时间"
},
{
"key": SystemParamKey.VARIABLES,
"name": "任务变量",
"type": TaskInputParamType.JSON,
"is_system": True,
"is_readonly": False,
"description": "任务的变量集合"
},
{
"key": SystemParamKey.PRIORITY,
"name": "任务优先级",
"type": TaskInputParamType.INTEGER,
"is_system": True,
"is_readonly": False,
"description": "任务的优先级,数值越大优先级越高"
}
]
@classmethod
def get_system_params(cls) -> List[Dict[str, Any]]:
"""获取系统默认参数配置"""
return cls.SYSTEM_PARAMS
@classmethod
def get_system_param_by_key(cls, key: str) -> Optional[Dict[str, Any]]:
"""根据键名获取系统参数配置"""
for param in cls.SYSTEM_PARAMS:
if param["key"] == key:
return param
return None
@classmethod
def build_system_params_with_values(cls, task_id: str, instance: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
构建带有值的系统参数列表
Args:
task_id: 任务ID
instance: 任务实例数据
Returns:
带有值的系统参数列表
"""
params = []
# 复制系统参数配置并添加值
for param_config in cls.SYSTEM_PARAMS:
param = param_config.copy()
# 根据参数键名设置值
if param["key"] == SystemParamKey.TASK_ID:
param["value"] = task_id
elif param["key"] == SystemParamKey.INSTANCE_ID:
param["value"] = instance.get("instance_id")
elif param["key"] == SystemParamKey.TASK_NAME:
param["value"] = instance.get("name")
elif param["key"] == SystemParamKey.CREATED_AT:
param["value"] = instance.get("created_at")
elif param["key"] == SystemParamKey.VARIABLES:
param["value"] = instance.get("variables")
elif param["key"] == SystemParamKey.PRIORITY:
param["value"] = instance.get("priority", 1)
params.append(param)
return params
#################################################
# 默认值配置
#################################################
class DefaultConfig:
"""默认值配置"""
TASK_DESCRIPTION = "" # 默认任务描述(备注)
TEMPLATE_DESCRIPTION = "用户自有模板" # 默认模板描述
#################################################
# 向后兼容的常量和函数
#################################################
# 为了向后兼容,保留原有的函数和变量
# 任务类型和状态配置常
TASK_TYPE_CONFIG = TaskTypeConfig.DETAILS
TASK_STATUS_CONFIG = TaskStatusConfig.DETAILS

View File

@ -6,6 +6,20 @@
包含数据库模型和数据访问功能
"""
from data.session import initialize_database, get_session, session_scope
# 避免循环导入
# from data.session import initialize_database, get_session, session_scope
__all__ = ['initialize_database', 'get_session', 'session_scope']
# 延迟导入
def initialize_database():
from data.session import initialize_database as init_db
return init_db()
def get_session():
from data.session import get_session as get_db_session
return get_db_session()
def session_scope():
from data.session import session_scope as db_session_scope
return db_session_scope()

View File

@ -21,6 +21,7 @@ from data.models.component_parameter import (
from data.models.task_variable import TaskVariableDefinition
from data.models.task_backup import TaskBackup
from data.models.task_edit_history import TaskEditHistory
from data.models.task_instance import TaskInstance, TaskInstanceStatus
__all__ = [
'BaseModel',
@ -38,5 +39,7 @@ __all__ = [
'ParameterValueFormat',
'TaskVariableDefinition',
'TaskBackup',
'TaskEditHistory'
'TaskEditHistory',
'TaskInstance',
'TaskInstanceStatus'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -69,6 +69,7 @@ class Task(BaseModel):
versions = relationship('TaskVersion', back_populates='task', cascade='all, delete-orphan')
records = relationship('TaskRecord', back_populates='task')
flow_blocks = relationship('TaskFlowBlock', back_populates='task')
instances = relationship("TaskInstance", back_populates="task", cascade="all, delete-orphan")
def __repr__(self):
return f"<Task(id={self.id}, uuid='{self.task_id}', name='{self.name}', type='{self.task_type}')>"

View File

@ -0,0 +1,60 @@
"""
任务实例模型
用于存储任务管理表的数据记录每次编辑任务的实例
"""
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey, JSON, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
import uuid
from data.models.base import BaseModel
class TaskInstanceStatus(enum.Enum):
"""任务实例状态枚举"""
EDITING = "editing" # 编辑中
PUBLISHED = "published" # 已发布
class TaskInstance(BaseModel):
"""任务实例模型"""
__tablename__ = "task_instances"
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
instance_id = Column(String(36), nullable=False, unique=True, index=True, comment="实例唯一ID用于外部引用")
task_id = Column(String(50), ForeignKey("tasks.task_id"), nullable=False, comment="关联的任务ID")
name = Column(String(100), nullable=False, comment="任务名称(复制自任务表)")
variables = Column(JSON, nullable=True, comment="任务变量")
priority = Column(Integer, default=1, comment="任务优先级")
input_params = Column(JSON, nullable=True, comment="任务输入参数")
block_outputs = Column(JSON, nullable=True, comment="块输出参数")
context_params = Column(JSON, nullable=True, comment="上下文参数")
status = Column(Enum(TaskInstanceStatus), default=TaskInstanceStatus.EDITING, comment="任务实例状态")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
is_deleted = Column(Boolean, default=False, comment="是否删除")
# 关联关系
task = relationship("Task", back_populates="instances")
def __init__(self, **kwargs):
"""初始化实例自动生成instance_id"""
if 'instance_id' not in kwargs:
kwargs['instance_id'] = str(uuid.uuid4())
super(TaskInstance, self).__init__(**kwargs)
def to_dict(self):
"""转换为字典"""
return {
"id": self.instance_id, # 使用instance_id作为对外ID
"instance_id": self.instance_id,
"task_id": self.task_id,
"name": self.name,
"variables": self.variables or {},
"priority": self.priority,
"input_params": self.input_params or {},
"block_outputs": self.block_outputs or {},
"context_params": self.context_params or {},
"status": self.status.value if self.status else None,
"created_at": int(self.created_at.timestamp() * 1000) if self.created_at else None,
"updated_at": int(self.updated_at.timestamp() * 1000) if self.updated_at else None,
"is_deleted": self.is_deleted
}

View File

@ -8,6 +8,13 @@
from contextlib import contextmanager
from config.database import DBConfig, CacheConfig
from config.component_config import (
COMPONENT_CATEGORIES,
get_component_types,
get_system_components,
CACHE_EXPIRE_TIME,
CACHE_KEYS
)
def get_session():
"""
@ -18,6 +25,19 @@ def get_session():
"""
return DBConfig.get_session()
def get_db():
"""
获取数据库会话生成器
Yields:
Session: 数据库会话对象
"""
db = get_session()
try:
yield db
finally:
db.close()
@contextmanager
def session_scope():
"""
@ -60,18 +80,8 @@ def _initialize_component_categories():
if ComponentCategory.query.count() > 0:
return
# 定义基础组件类别
categories = [
{"name": "基础组件", "code": "basic", "description": "基础功能组件", "sort_order": 1},
{"name": "流程组件", "code": "flow", "description": "流程控制组件", "sort_order": 2},
{"name": "机器人组件", "code": "robot", "description": "机器人控制组件", "sort_order": 3},
{"name": "库位组件", "code": "storage", "description": "库位管理组件", "sort_order": 4},
{"name": "设备组件", "code": "device", "description": "设备控制组件", "sort_order": 5},
{"name": "HTTP组件", "code": "http", "description": "HTTP请求组件", "sort_order": 6},
{"name": "脚本组件", "code": "script", "description": "脚本执行组件", "sort_order": 7},
{"name": "子任务组件", "code": "subtask", "description": "子任务组件", "sort_order": 8},
{"name": "任务组件", "code": "task", "description": "任务管理组件", "sort_order": 9}
]
# 从配置文件获取组件类别
categories = COMPONENT_CATEGORIES
# 批量创建
with session_scope() as session:
@ -93,34 +103,8 @@ def _initialize_component_types():
# 获取组件类别
categories = {category.code: category.id for category in ComponentCategory.query.all()}
# 定义基础组件类型
types = [
# 基础组件
{"name": "变量赋值", "code": "variable_assign", "category_id": categories["basic"], "description": "为变量赋值", "icon": "variable", "sort_order": 1},
{"name": "条件判断", "code": "condition", "category_id": categories["basic"], "description": "条件判断", "icon": "condition", "sort_order": 2},
{"name": "延时等待", "code": "delay", "category_id": categories["basic"], "description": "延时等待", "icon": "delay", "sort_order": 3},
# 流程组件
{"name": "If条件", "code": "if", "category_id": categories["flow"], "description": "If条件判断", "icon": "if", "sort_order": 1},
{"name": "If-Else条件", "code": "if_else", "category_id": categories["flow"], "description": "If-Else条件判断", "icon": "if_else", "sort_order": 2},
{"name": "循环", "code": "loop", "category_id": categories["flow"], "description": "循环执行", "icon": "loop", "sort_order": 3},
{"name": "并行执行", "code": "parallel", "category_id": categories["flow"], "description": "并行执行多个分支", "icon": "parallel", "sort_order": 4},
# 机器人组件
{"name": "选择机器人", "code": "select_robot", "category_id": categories["robot"], "description": "选择执行机器人", "icon": "robot", "sort_order": 1},
{"name": "机器人移动", "code": "robot_move", "category_id": categories["robot"], "description": "控制机器人移动", "icon": "move", "sort_order": 2},
{"name": "获取机器人状态", "code": "robot_status", "category_id": categories["robot"], "description": "获取机器人状态", "icon": "status", "sort_order": 3},
# HTTP组件
{"name": "HTTP请求", "code": "http_request", "category_id": categories["http"], "description": "发送HTTP请求", "icon": "http", "sort_order": 1},
{"name": "API调用", "code": "api_call", "category_id": categories["http"], "description": "调用系统API", "icon": "api", "sort_order": 2},
# 脚本组件
{"name": "JavaScript脚本", "code": "javascript", "category_id": categories["script"], "description": "执行JavaScript脚本", "icon": "script", "sort_order": 1},
# 子任务组件
{"name": "执行子任务", "code": "execute_subtask", "category_id": categories["subtask"], "description": "执行子任务", "icon": "subtask", "sort_order": 1}
]
# 从配置文件获取组件类型
types = get_component_types(categories)
# 批量创建
with session_scope() as session:
@ -142,74 +126,8 @@ def _initialize_system_components():
# 获取组件类型
types = {component_type.code: component_type.id for component_type in ComponentType.query.all()}
# 定义系统组件
components = [
{
"name": "变量赋值",
"code": "variable_assign",
"type_id": types["variable_assign"],
"description": "为变量赋值",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"variable_name": {"type": "string", "title": "变量名"},
"value_type": {"type": "string", "enum": ["string", "number", "boolean", "object"], "title": "值类型"},
"value": {"type": "string", "title": ""}
},
"required": ["variable_name", "value_type", "value"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "执行结果"}
}
}
},
{
"name": "条件判断",
"code": "condition",
"type_id": types["condition"],
"description": "条件判断",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"condition": {"type": "string", "title": "条件表达式"}
},
"required": ["condition"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "判断结果"}
}
}
},
{
"name": "If条件",
"code": "if",
"type_id": types["if"],
"description": "If条件判断",
"is_system": True,
"config_schema": {
"type": "object",
"properties": {
"condition": {"type": "string", "title": "条件表达式"}
},
"required": ["condition"]
},
"input_schema": {},
"output_schema": {
"type": "object",
"properties": {
"result": {"type": "boolean", "title": "判断结果"}
}
}
}
]
# 从配置文件获取系统组件
components = get_system_components(types)
# 批量创建
with session_scope() as session:
@ -234,7 +152,7 @@ def _cache_component_categories():
} for category in categories}
# 使用Redis缓存
CacheConfig.set("component_categories", categories_dict, expire=86400) # 缓存24小时
CacheConfig.set(CACHE_KEYS["COMPONENT_CATEGORIES"], categories_dict, expire=CACHE_EXPIRE_TIME)
def _cache_component_types():
"""缓存组件类型"""
@ -252,7 +170,7 @@ def _cache_component_types():
} for type in types}
# 使用Redis缓存
CacheConfig.set("component_types", types_dict, expire=86400) # 缓存24小时
CacheConfig.set(CACHE_KEYS["COMPONENT_TYPES"], types_dict, expire=CACHE_EXPIRE_TIME)
def _cache_system_components():
"""缓存系统组件"""
@ -271,4 +189,4 @@ def _cache_system_components():
} for component in components}
# 使用Redis缓存
CacheConfig.set("system_components", components_dict, expire=86400) # 缓存24小时
CacheConfig.set(CACHE_KEYS["SYSTEM_COMPONENTS"], components_dict, expire=CACHE_EXPIRE_TIME)

88
docker-compose.yml Normal file
View File

@ -0,0 +1,88 @@
version: '3.8'
services:
# 应用服务
app:
build: .
container_name: tianfeng-task
restart: always
ports:
- "8000:8000"
environment:
- TIANFENG_ENV=default
- DEBUG=false
- DB_HOST=db
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=password
- DB_NAME=tianfeng_task
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_DB=0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./logs:/app/logs
networks:
- tianfeng-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# MySQL数据库服务
db:
image: mysql:8.0
container_name: tianfeng-mysql
restart: always
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=tianfeng_task
- MYSQL_USER=tianfeng
- MYSQL_PASSWORD=tianfeng
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
networks:
- tianfeng-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis缓存服务
redis:
image: redis:6.2-alpine
container_name: tianfeng-redis
restart: always
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- tianfeng-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
mysql-data:
redis-data:
networks:
tianfeng-network:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
#!/bin/bash
set -e
# 等待数据库服务就绪
echo "等待数据库服务就绪..."
until nc -z ${DB_HOST} ${DB_PORT}; do
echo "数据库服务未就绪 - 等待..."
sleep 2
done
echo "数据库服务已就绪!"
# 等待Redis服务就绪
echo "等待Redis服务就绪..."
until nc -z ${REDIS_HOST} ${REDIS_PORT}; do
echo "Redis服务未就绪 - 等待..."
sleep 2
done
echo "Redis服务已就绪!"
# 初始化数据库(如果需要)
echo "初始化数据库..."
python -c "from app import init_database; init_database()"
# 启动应用
echo "启动应用..."
exec "$@"

View File

@ -0,0 +1,83 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
任务实例ID修复脚本
为所有现有的任务实例记录生成并设置instance_id
用法:
python scripts/fix_instance_ids.py
"""
import os
import sys
import logging
import uuid
from sqlalchemy import text
# 添加项目根目录到系统路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# 导入项目模块
from data.session import get_db
from data.models.task_instance import TaskInstance
# 设置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout), # 输出到控制台
logging.FileHandler("fix_instance_ids.log") # 输出到文件
]
)
logger = logging.getLogger("fix_instance_ids")
def fix_instance_ids():
"""为所有现有的任务实例记录生成并设置instance_id"""
try:
# 获取数据库会话
db = next(get_db())
# 查询所有没有instance_id的任务实例记录
instances = db.query(TaskInstance).filter(
(TaskInstance.instance_id == None) |
(TaskInstance.instance_id == '')
).all()
logger.info(f"发现 {len(instances)} 条没有instance_id的任务实例记录")
# 为每条记录生成并设置instance_id
for instance in instances:
instance_id = str(uuid.uuid4())
instance.instance_id = instance_id
logger.info(f"为任务实例 ID={instance.id} 设置instance_id={instance_id}")
# 提交更改
db.commit()
logger.info("所有任务实例记录的instance_id已更新")
return True
except Exception as e:
logger.error(f"修复任务实例ID失败: {str(e)}")
if 'db' in locals():
db.rollback()
return False
def main():
"""主函数"""
logger.info("开始修复任务实例ID...")
# 修复任务实例ID
success = fix_instance_ids()
if success:
logger.info("任务实例ID修复成功")
else:
logger.error("任务实例ID修复失败")
# 根据修复结果设置退出码
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

13
scripts/init-db.sql Normal file
View File

@ -0,0 +1,13 @@
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS tianfeng_task CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE tianfeng_task;
-- 创建用户并授权(如果不存在)
CREATE USER IF NOT EXISTS 'tianfeng'@'%' IDENTIFIED BY 'tianfeng';
GRANT ALL PRIVILEGES ON tianfeng_task.* TO 'tianfeng'@'%';
FLUSH PRIVILEGES;
-- 初始化基础数据可以在这里添加
-- 注意大部分表结构会由SQLAlchemy自动创建这里主要是为了初始化一些基础数据

View File

@ -0,0 +1,36 @@
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:48:29
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:50:12
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:51:14
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:51:54
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================

View File

@ -0,0 +1,186 @@
2025-03-17 16:48:29,929 - migration - INFO - 运行迁移,参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:48:29,929 - migration - INFO - 使用已存在的临时配置文件: D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp
2025-03-17 16:48:29,930 - migration - INFO - 命令参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:48:29,930 - migration - INFO - 执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
2025-03-17 16:48:29,930 - migration - INFO - 工作目录: D:\jsw_code\project\tianfeng_task
2025-03-17 16:48:29,930 - migration - INFO - Python 编码: utf-8
2025-03-17 16:48:29,930 - migration - INFO - 文件系统编码: utf-8
2025-03-17 16:48:29,930 - migration - INFO - 系统默认编码: cp936
2025-03-17 16:48:29,930 - migration - INFO - 正在执行迁移,日志将写入 logs\migration_upgrade_20250317.log...
2025-03-17 16:48:30,704 - migration - INFO - 命令执行状态: 成功
2025-03-17 16:48:30,704 - migration - INFO - 输出(最后部分):
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:48:29
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
2025-03-17 16:50:12,449 - migration - INFO - 运行迁移,参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:50:12,450 - migration - INFO - 使用已存在的临时配置文件: D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp
2025-03-17 16:50:12,450 - migration - INFO - 命令参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:50:12,450 - migration - INFO - 执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
2025-03-17 16:50:12,450 - migration - INFO - 工作目录: D:\jsw_code\project\tianfeng_task
2025-03-17 16:50:12,450 - migration - INFO - Python 编码: utf-8
2025-03-17 16:50:12,450 - migration - INFO - 文件系统编码: utf-8
2025-03-17 16:50:12,450 - migration - INFO - 系统默认编码: cp936
2025-03-17 16:50:12,450 - migration - INFO - 正在执行迁移,日志将写入 logs\migration_upgrade_20250317.log...
2025-03-17 16:50:13,150 - migration - INFO - 命令执行状态: 成功
2025-03-17 16:50:13,150 - migration - INFO - 输出(最后部分):
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:48:29
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:50:12
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
2025-03-17 16:51:14,892 - migration - INFO - 运行迁移,参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:51:14,892 - migration - INFO - 使用已存在的临时配置文件: D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp
2025-03-17 16:51:14,892 - migration - INFO - 命令参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:51:14,892 - migration - INFO - 执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
2025-03-17 16:51:14,908 - migration - INFO - 工作目录: D:\jsw_code\project\tianfeng_task
2025-03-17 16:51:14,908 - migration - INFO - Python 编码: utf-8
2025-03-17 16:51:14,908 - migration - INFO - 文件系统编码: utf-8
2025-03-17 16:51:14,908 - migration - INFO - 系统默认编码: cp936
2025-03-17 16:51:14,908 - migration - INFO - 正在执行迁移,日志将写入 logs\migration_upgrade_20250317.log...
2025-03-17 16:51:15,435 - migration - INFO - 命令执行状态: 成功
2025-03-17 16:51:15,435 - migration - INFO - 输出(最后部分):
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:48:29
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:50:12
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:51:14
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
2025-03-17 16:51:54,260 - migration - INFO - 运行迁移,参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:51:54,260 - migration - INFO - 使用已存在的临时配置文件: D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp
2025-03-17 16:51:54,260 - migration - INFO - 命令参数: {'revision': None, 'downgrade': False, 'verbose': False, 'message': None, 'branch': None, 'history': False, 'current': False, 'show': False, 'table': None, 'list_tables': False}
2025-03-17 16:51:54,260 - migration - INFO - 执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
2025-03-17 16:51:54,260 - migration - INFO - 工作目录: D:\jsw_code\project\tianfeng_task
2025-03-17 16:51:54,260 - migration - INFO - Python 编码: utf-8
2025-03-17 16:51:54,260 - migration - INFO - 文件系统编码: utf-8
2025-03-17 16:51:54,260 - migration - INFO - 系统默认编码: cp936
2025-03-17 16:51:54,260 - migration - INFO - 正在执行迁移,日志将写入 logs\migration_upgrade_20250317.log...
2025-03-17 16:51:54,809 - migration - INFO - 命令执行状态: 成功
2025-03-17 16:51:54,809 - migration - INFO - 输出(最后部分):
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:48:29
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:50:12
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:51:14
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
================================================================================
执行时间: 2025-03-17 16:51:54
执行命令: alembic -c D:\jsw_code\project\tianfeng_task\migrations\alembic.ini.tmp upgrade head
================================================================================
2025-03-17 17:11:12,595 - migration - INFO - 开始执行数据库自动迁移...
2025-03-17 17:11:12,595 - utils.db_migration - INFO - 开始数据库迁移
2025-03-17 17:11:12,657 - utils.db_migration - INFO - 发现 23 个模型
2025-03-17 17:11:12,669 - utils.db_migration - INFO - 表 subtasks 不存在,准备创建
2025-03-17 17:11:12,733 - utils.db_migration - INFO - 创建表 subtasks 成功
2025-03-17 17:11:12,734 - utils.db_migration - INFO - 表 subtask_versions 不存在,准备创建
2025-03-17 17:11:12,785 - utils.db_migration - INFO - 创建表 subtask_versions 成功
2025-03-17 17:11:12,792 - utils.db_migration - INFO - 表 task_flow_connections 不存在,准备创建
2025-03-17 17:11:12,794 - utils.db_migration - ERROR - 创建表 task_flow_connections 失败: (pymysql.err.OperationalError) (1824, "Failed to open the referenced table 'task_flow_nodes'")
[SQL:
CREATE TABLE task_flow_connections (
task_id INTEGER NOT NULL COMMENT '任务ID',
version_id INTEGER NOT NULL COMMENT '任务版本ID',
source_node_id INTEGER NOT NULL COMMENT '源节点ID',
target_node_id INTEGER NOT NULL COMMENT '目标节点ID',
label VARCHAR(100) COMMENT '连接标签',
`condition` VARCHAR(500) COMMENT '连接条件',
config JSON COMMENT '连接配置',
is_default BOOL COMMENT '是否为默认连接',
id INTEGER NOT NULL COMMENT '主键ID' AUTO_INCREMENT,
created_at DATETIME COMMENT '创建时间',
updated_at DATETIME COMMENT '更新时间',
is_deleted BOOL COMMENT '是否删除(软删除标记)',
PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks (id),
FOREIGN KEY(version_id) REFERENCES task_versions (id),
FOREIGN KEY(source_node_id) REFERENCES task_flow_nodes (id),
FOREIGN KEY(target_node_id) REFERENCES task_flow_nodes (id)
)
]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-03-17 17:11:12,794 - utils.db_migration - INFO - 表 task_flow_nodes 不存在,准备创建
2025-03-17 17:11:12,864 - utils.db_migration - INFO - 创建表 task_flow_nodes 成功
2025-03-17 17:11:12,867 - utils.db_migration - INFO - 表 task_import_exports 不存在,准备创建
2025-03-17 17:11:12,915 - utils.db_migration - INFO - 创建表 task_import_exports 成功
2025-03-17 17:11:12,916 - utils.db_migration - INFO - 表 task_inputs 不存在,准备创建
2025-03-17 17:11:12,968 - utils.db_migration - INFO - 创建表 task_inputs 成功
2025-03-17 17:11:12,969 - utils.db_migration - INFO - 表 task_input_values 不存在,准备创建
2025-03-17 17:11:13,027 - utils.db_migration - INFO - 创建表 task_input_values 成功
2025-03-17 17:11:13,028 - utils.db_migration - INFO - 发现新增列 task_instances.instance_id准备添加
2025-03-17 17:11:13,029 - utils.db_migration - INFO - 执行SQL: ALTER TABLE task_instances ADD COLUMN instance_id VARCHAR(36) NOT NULL
2025-03-17 17:11:13,061 - utils.db_migration - INFO - 添加列 task_instances.instance_id 成功
2025-03-17 17:11:13,074 - utils.db_migration - INFO - 表 user_operations 不存在,准备创建
2025-03-17 17:11:13,111 - utils.db_migration - INFO - 创建表 user_operations 成功
2025-03-17 17:11:13,111 - utils.db_migration - INFO - 数据库迁移完成
2025-03-17 17:11:13,111 - utils.db_migration - INFO - 数据库迁移成功
2025-03-17 17:11:13,111 - migration - INFO - 数据库迁移成功完成!

View File

@ -2,340 +2,63 @@
# -*- coding: utf-8 -*-
"""
数据库迁移执行脚本
数据库迁移脚本
用于自动检测模型变更并应用到数据库
用法:
python scripts/run_migration.py [--revision 版本号]
python scripts/run_migration.py [--verbose]
示例:
# 升级到最新版本
# 执行自动迁移
python scripts/run_migration.py
# 升级到指定版本
python scripts/run_migration.py --revision 001
# 降级到指定版本
python scripts/run_migration.py --revision 001 --downgrade
# 生成新的迁移脚本
python scripts/run_migration.py --generate "添加新字段"
# 为特定表生成迁移脚本
python scripts/run_migration.py --generate "为用户表添加邮箱字段" --table users
# 为多个表生成迁移脚本
python scripts/run_migration.py --generate "添加审计字段" --table users,orders,products
# 显示详细日志
python scripts/run_migration.py --verbose
"""
import argparse
import os
import sys
import logging
from pathlib import Path
import subprocess
import shutil
import tempfile
import locale
import datetime
# 将项目根目录添加到 Python 路径
root_dir = Path(__file__).parent.parent
sys.path.insert(0, str(root_dir))
# 添加项目根目录到系统路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# 导入项目日志模块
from utils.logger import get_logger, setup_logger
from utils.logger import setup_logger
from utils.db_migration import run_migration, DBMigration
# 设置日志
setup_logger()
logger = get_logger('migration')
def create_ascii_config(original_config_path):
"""
创建一个 ASCII 编码的配置文件副本
Args:
original_config_path (Path): 原始配置文件路径
Returns:
Path: 临时配置文件路径
"""
# 如果临时文件已存在,直接使用
temp_config_path = original_config_path.with_suffix('.ini.tmp')
if temp_config_path.exists():
logger.info(f"使用已存在的临时配置文件: {temp_config_path}")
return temp_config_path
# 创建临时文件
logger.info(f"创建 ASCII 编码的临时配置文件: {temp_config_path}")
# 复制配置文件内容,去除中文注释
with open(temp_config_path, 'w', encoding='ascii', errors='ignore') as temp_file:
temp_file.write("""
# Alembic Configuration File
# This file contains basic configuration for Alembic
[alembic]
# Path to migration scripts
script_location = migrations
# Template uses jinja2 format
output_encoding = utf-8
# Database connection configuration
# In practice, this value will be overridden by the configuration in env.py
sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
""")
return temp_config_path
def run_migration_command(command, args):
"""
执行迁移命令
Args:
command (str): 命令名称 'upgrade', 'downgrade', 'revision'
args (argparse.Namespace): 命令行参数
Returns:
bool: 是否成功执行命令
"""
# 获取项目根目录
root_dir = Path(__file__).parent.parent
# 原始配置文件路径
original_config_path = root_dir / "migrations" / "alembic.ini"
# 创建 ASCII 编码的临时配置文件
temp_config_path = create_ascii_config(original_config_path)
# 构建命令
cmd = ["alembic", "-c", str(temp_config_path)]
if args.verbose:
cmd.append("--verbose")
# 打印所有参数,帮助调试
logger.info(f"命令参数: {vars(args)}")
if command == 'upgrade':
cmd.extend(["upgrade", args.revision or "head"])
elif command == 'downgrade':
cmd.extend(["downgrade", args.revision or "base"])
elif command == 'revision':
logger.info(f"生成迁移脚本,描述: {args.message}")
cmd.extend(["revision", "--autogenerate", "-m", args.message])
# 如果指定了表名,创建一个临时的 env.py 文件,只包含指定的表
if args.table:
# 将表名列表转换为 Python 列表字符串
tables = [f"'{table.strip()}'" for table in args.table.split(',')]
tables_str = f"[{', '.join(tables)}]"
# 创建临时环境变量,传递表名列表
os.environ["ALEMBIC_TABLES"] = args.table
logger.info(f"指定迁移表: {args.table}")
if args.branch:
cmd.extend(["--branch", args.branch])
elif command == 'history':
cmd.append("history")
if args.verbose:
cmd.append("-v")
elif command == 'current':
cmd.append("current")
elif command == 'show':
cmd.extend(["show", args.revision or "head"])
elif command == 'list_tables':
# 这不是 alembic 命令,而是我们自定义的命令
return list_database_tables()
else:
logger.error(f"未知命令: {command}")
return False
# 执行命令
logger.info(f"执行命令: {' '.join(cmd)}")
logger.info(f"工作目录: {root_dir}")
logger.info(f"Python 编码: {sys.getdefaultencoding()}")
logger.info(f"文件系统编码: {sys.getfilesystemencoding()}")
logger.info(f"系统默认编码: {locale.getpreferredencoding()}")
# 设置环境变量,确保使用 UTF-8 编码
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
# 创建命令输出日志文件 - 使用项目日志目录
from config.settings import LogConfig
log_config = LogConfig.as_dict()
log_dir = Path(os.path.dirname(log_config["file"]))
log_dir.mkdir(exist_ok=True)
# 使用日期和命令类型命名日志文件
today = datetime.datetime.now().strftime('%Y%m%d')
log_file = log_dir / f"migration_{command}_{today}.log"
try:
# 使用直接执行命令的方式,避免 subprocess 的编码问题
logger.info(f"正在执行迁移,日志将写入 {log_file}...")
# 使用 subprocess.Popen 而不是 subprocess.run
with open(log_file, "a", encoding="utf-8") as f_out:
# 添加分隔线和时间戳
f_out.write(f"\n\n{'='*80}\n")
f_out.write(f"执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f_out.write(f"执行命令: {' '.join(cmd)}\n")
f_out.write(f"{'='*80}\n\n")
process = subprocess.Popen(
cmd,
cwd=str(root_dir),
env=env,
stdout=f_out,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace" # 使用 replace 策略处理无法解码的字符
)
process.wait()
# 读取日志文件
with open(log_file, "r", encoding="utf-8") as f:
# 只读取最后 50 行,避免日志过长
lines = f.readlines()
output = ''.join(lines[-50:]) if len(lines) > 50 else ''.join(lines)
logger.info(f"命令执行状态: {'成功' if process.returncode == 0 else '失败'}")
logger.info(f"输出(最后部分):\n{output}")
# 清理环境变量
if 'ALEMBIC_TABLES' in os.environ:
del os.environ['ALEMBIC_TABLES']
return process.returncode == 0
except Exception as e:
logger.error(f"执行命令时发生错误: {e}", exc_info=True)
# 清理环境变量
if 'ALEMBIC_TABLES' in os.environ:
del os.environ['ALEMBIC_TABLES']
return False
def list_database_tables():
"""
列出数据库中的所有表
Returns:
bool: 是否成功执行命令
"""
try:
# 导入数据库配置
from config.database import DBConfig
# 获取数据库连接
engine = DBConfig.engine
# 获取所有表名
from sqlalchemy import inspect
inspector = inspect(engine)
tables = inspector.get_table_names()
logger.info("数据库中的表:")
for i, table in enumerate(tables, 1):
logger.info(f"{i}. {table}")
return True
except Exception as e:
logger.error(f"获取数据库表时发生错误: {e}", exc_info=True)
return False
def run_migration(args):
"""
执行数据库迁移
Args:
args (argparse.Namespace): 命令行参数
Returns:
bool: 是否成功执行迁移
"""
# 打印所有参数,帮助调试
logger.info(f"运行迁移,参数: {vars(args)}")
if args.list_tables:
# 列出数据库中的所有表
return run_migration_command('list_tables', args)
elif args.message:
# 生成新的迁移脚本
logger.info(f"生成新的迁移脚本,描述: {args.message}")
return run_migration_command('revision', args)
elif args.history:
# 显示迁移历史
return run_migration_command('history', args)
elif args.current:
# 显示当前版本
return run_migration_command('current', args)
elif args.show:
# 显示指定版本的详细信息
return run_migration_command('show', args)
elif args.downgrade:
# 降级到指定版本
return run_migration_command('downgrade', args)
else:
# 升级到指定版本
return run_migration_command('upgrade', args)
logger = logging.getLogger('migration')
def main():
parser = argparse.ArgumentParser(description="执行数据库迁移")
parser.add_argument("--revision", help="版本号,为空表示升级到最新版本")
parser.add_argument("--downgrade", action="store_true", help="是否降级")
"""主函数"""
parser = argparse.ArgumentParser(description="执行数据库自动迁移")
parser.add_argument("--verbose", "-v", action="store_true", help="显示详细日志")
parser.add_argument("--generate", "--gen", "-m", dest="message", help="生成新的迁移脚本,需要提供描述信息")
parser.add_argument("--branch", "-b", help="分支名称,用于生成迁移脚本")
parser.add_argument("--history", action="store_true", help="显示迁移历史")
parser.add_argument("--current", action="store_true", help="显示当前版本")
parser.add_argument("--show", action="store_true", help="显示指定版本的详细信息")
parser.add_argument("--table", "-t", help="指定要迁移的表名,多个表用逗号分隔")
parser.add_argument("--list-tables", action="store_true", help="列出数据库中的所有表")
args = parser.parse_args()
success = run_migration(args)
# 设置日志级别
log_level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger.info("开始执行数据库自动迁移...")
# 运行迁移
success = run_migration()
if success:
logger.info("数据库迁移成功完成!")
else:
logger.error("数据库迁移失败!")
# 根据迁移结果设置退出码
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,92 @@
"""
常用参数服务模块
提供获取各种常用参数数据的服务
"""
from typing import Dict, Any, List, Optional
import json
import random
from datetime import datetime
from config.component_config import CommonParamType
class CommonParamsService:
"""常用参数服务类"""
def __init__(self):
"""初始化服务"""
# 模拟数据实际项目中应该从数据库或外部API获取
self.mock_data = {
CommonParamType.ROBOT_ID: [
{"id": "robot-001", "name": "机器人1号"},
{"id": "robot-002", "name": "机器人2号"},
{"id": "robot-003", "name": "机器人3号"}
],
CommonParamType.ROBOT_GROUP: [
{"id": "group-001", "name": "机器人组A"},
{"id": "group-002", "name": "机器人组B"},
{"id": "group-003", "name": "机器人组C"}
],
CommonParamType.ROBOT_TAG: [
{"id": "tag-001", "name": "标签1"},
{"id": "tag-002", "name": "标签2"},
{"id": "tag-003", "name": "标签3"}
],
CommonParamType.STORAGE_AREA_ID: [
{"id": "area-001", "name": "库区1"},
{"id": "area-002", "name": "库区2"},
{"id": "area-003", "name": "库区3"}
],
CommonParamType.STORAGE_AREA: [
{"id": "area-001", "name": "库区1", "code": "A001"},
{"id": "area-002", "name": "库区2", "code": "A002"},
{"id": "area-003", "name": "库区3", "code": "A003"}
],
CommonParamType.SITE: [
{"id": "site-001", "name": "站点1", "code": "S001"},
{"id": "site-002", "name": "站点2", "code": "S002"},
{"id": "site-003", "name": "站点3", "code": "S003"}
],
CommonParamType.BIN_TASK: [
{"id": "bin-001", "name": "Bin任务1"},
{"id": "bin-002", "name": "Bin任务2"},
{"id": "bin-003", "name": "Bin任务3"}
],
CommonParamType.WORKSTATION: [
{"id": "ws-001", "name": "工位1"},
{"id": "ws-002", "name": "工位2"},
{"id": "ws-003", "name": "工位3"}
],
CommonParamType.POST: [
{"id": "post-001", "name": "岗位1"},
{"id": "post-002", "name": "岗位2"},
{"id": "post-003", "name": "岗位3"}
],
CommonParamType.USER: [
{"id": "user-001", "name": "用户1"},
{"id": "user-002", "name": "用户2"},
{"id": "user-003", "name": "用户3"}
],
CommonParamType.CACHE: [
{"key": "cache-001", "value": "缓存值1"},
{"key": "cache-002", "value": "缓存值2"},
{"key": "cache-003", "value": "缓存值3"}
],
CommonParamType.BUILT_IN_FUNCTION: [
{"name": "getCurrentTime", "description": "获取当前时间"},
{"name": "generateUUID", "description": "生成UUID"},
{"name": "formatDate", "description": "格式化日期"}
]
}
def get_param_data(self, param_type: str) -> List[Dict[str, Any]]:
"""
获取指定类型的常用参数数据
Args:
param_type: 参数类型
Returns:
参数数据列表
"""
# 实际项目中应该根据param_type调用相应的API或查询数据库
# 这里使用模拟数据
return self.mock_data.get(param_type, [])

View File

@ -0,0 +1,281 @@
"""
任务实例服务模块
提供任务实例的增删改查服务
"""
from typing import Dict, Any, List, Optional, Tuple
from sqlalchemy.orm import Session
from data.session import get_db
from data.models.task_instance import TaskInstance, TaskInstanceStatus
from data.models.task import Task
from datetime import datetime
class TaskInstanceService:
"""任务实例服务类"""
def __init__(self):
"""初始化服务"""
self.db = next(get_db())
def create_instance(self, task_id: str, name: Optional[str] = None, variables: Optional[Dict[str, Any]] = None,
priority: int = 1, input_params: Optional[Dict[str, Any]] = None,
block_outputs: Optional[Dict[str, Any]] = None, context_params: Optional[Dict[str, Any]] = None,
status: TaskInstanceStatus = TaskInstanceStatus.EDITING) -> Dict[str, Any]:
"""
创建任务实例
Args:
task_id: 任务ID
name: 任务名称如果为空则从任务表中获取
variables: 任务变量
priority: 任务优先级
input_params: 任务输入参数
block_outputs: 块输出参数
context_params: 上下文参数
status: 任务实例状态
Returns:
任务实例字典
"""
# 检查任务是否存在
task = self.db.query(Task).filter(Task.task_id == task_id, Task.is_deleted == False).first()
if not task:
raise ValueError(f"任务不存在: {task_id}")
# 如果名称为空,则使用任务表中的名称
if not name:
name = task.name
# 创建任务实例
instance = TaskInstance(
task_id=task_id,
name=name,
variables=variables or {},
priority=priority,
input_params=input_params or {},
block_outputs=block_outputs or {},
context_params=context_params or {},
status=status
)
# 保存到数据库
self.db.add(instance)
self.db.commit()
self.db.refresh(instance)
return instance.to_dict()
def get_instance_by_id(self, instance_id: str) -> Optional[Dict[str, Any]]:
"""
根据实例ID获取任务实例
Args:
instance_id: 任务实例ID
Returns:
任务实例字典如果不存在则返回None
"""
instance = self.db.query(TaskInstance).filter(TaskInstance.instance_id == instance_id, TaskInstance.is_deleted == False).first()
if not instance:
return None
return instance.to_dict()
def get_instances_by_task_id(self, task_id: str) -> List[Dict[str, Any]]:
"""
根据任务ID获取所有任务实例
Args:
task_id: 任务ID
Returns:
任务实例字典列表
"""
instances = self.db.query(TaskInstance).filter(TaskInstance.task_id == task_id, TaskInstance.is_deleted == False).all()
return [instance.to_dict() for instance in instances]
def get_latest_instance_by_task_id(self, task_id: str) -> Optional[Dict[str, Any]]:
"""
根据任务ID获取最新的任务实例
Args:
task_id: 任务ID
Returns:
最新的任务实例字典如果不存在则返回None
"""
instance = self.db.query(TaskInstance).filter(
TaskInstance.task_id == task_id,
TaskInstance.is_deleted == False
).order_by(TaskInstance.created_at.desc()).first()
if not instance:
return None
return instance.to_dict()
def get_editing_instance_by_task_id(self, task_id: str) -> Optional[Dict[str, Any]]:
"""
根据任务ID获取编辑中的任务实例
Args:
task_id: 任务ID
Returns:
编辑中的任务实例字典如果不存在则返回None
"""
instance = self.db.query(TaskInstance).filter(
TaskInstance.task_id == task_id,
TaskInstance.status == TaskInstanceStatus.EDITING,
TaskInstance.is_deleted == False
).order_by(TaskInstance.created_at.desc()).first()
if not instance:
return None
return instance.to_dict()
def get_latest_published_instance_by_task_id(self, task_id: str) -> Optional[Dict[str, Any]]:
"""
根据任务ID获取最新的已发布任务实例
Args:
task_id: 任务ID
Returns:
最新的已发布任务实例字典如果不存在则返回None
"""
instance = self.db.query(TaskInstance).filter(
TaskInstance.task_id == task_id,
TaskInstance.status == TaskInstanceStatus.PUBLISHED,
TaskInstance.is_deleted == False
).order_by(TaskInstance.created_at.desc()).first()
if not instance:
return None
return instance.to_dict()
def update_instance(self, instance_id: str, variables: Optional[Dict[str, Any]] = None,
priority: Optional[int] = None, input_params: Optional[Dict[str, Any]] = None,
block_outputs: Optional[Dict[str, Any]] = None, context_params: Optional[Dict[str, Any]] = None,
status: Optional[TaskInstanceStatus] = None) -> Optional[Dict[str, Any]]:
"""
更新任务实例
Args:
instance_id: 任务实例ID
variables: 任务变量
priority: 任务优先级
input_params: 任务输入参数
block_outputs: 块输出参数
context_params: 上下文参数
status: 任务实例状态
Returns:
更新后的任务实例字典如果不存在则返回None
"""
# 获取任务实例
instance = self.db.query(TaskInstance).filter(TaskInstance.instance_id == instance_id, TaskInstance.is_deleted == False).first()
if not instance:
return None
# 更新字段
if variables is not None:
instance.variables = variables
if priority is not None:
instance.priority = priority
if input_params is not None:
instance.input_params = input_params
if block_outputs is not None:
instance.block_outputs = block_outputs
if context_params is not None:
instance.context_params = context_params
if status is not None:
instance.status = status
# 保存到数据库
self.db.commit()
self.db.refresh(instance)
return instance.to_dict()
def publish_instance(self, instance_id: str) -> Optional[Dict[str, Any]]:
"""
发布任务实例
Args:
instance_id: 任务实例ID
Returns:
发布后的任务实例字典如果不存在则返回None
"""
return self.update_instance(instance_id, status=TaskInstanceStatus.PUBLISHED)
def delete_instance(self, instance_id: str) -> bool:
"""
删除任务实例
Args:
instance_id: 任务实例ID
Returns:
是否删除成功
"""
# 获取任务实例
instance = self.db.query(TaskInstance).filter(TaskInstance.instance_id == instance_id, TaskInstance.is_deleted == False).first()
if not instance:
return False
# 标记为已删除
instance.is_deleted = True
self.db.commit()
return True
def get_or_create_editing_instance(self, task_id: str) -> Dict[str, Any]:
"""
获取或创建编辑中的任务实例
如果任务已有编辑中的实例则返回该实例
如果任务没有编辑中的实例但有已发布的实例则基于最新的已发布实例创建新的编辑中实例
如果任务没有任何实例则创建新的编辑中实例
Args:
task_id: 任务ID
Returns:
编辑中的任务实例字典
"""
# 检查是否有编辑中的实例
editing_instance = self.get_editing_instance_by_task_id(task_id)
if editing_instance:
return editing_instance
# 检查是否有已发布的实例
published_instance = self.get_latest_published_instance_by_task_id(task_id)
if published_instance:
# 基于已发布实例创建新的编辑中实例
return self.create_instance(
task_id=task_id,
name=published_instance.get("name"),
variables=published_instance.get("variables"),
priority=published_instance.get("priority", 1),
input_params=published_instance.get("input_params"),
block_outputs=published_instance.get("block_outputs"),
context_params=published_instance.get("context_params"),
status=TaskInstanceStatus.EDITING
)
# 获取任务信息
task = self.db.query(Task).filter(Task.task_id == task_id, Task.is_deleted == False).first()
if not task:
raise ValueError(f"任务不存在: {task_id}")
# 创建新的编辑中实例
return self.create_instance(
task_id=task_id,
name=task.name,
variables=task.variables if hasattr(task, "variables") else {},
priority=1,
status=TaskInstanceStatus.EDITING
)

Binary file not shown.

218
utils/db_migration.py Normal file
View File

@ -0,0 +1,218 @@
"""
数据库迁移工具
用于自动检测模型变更并应用到数据库
"""
import logging
import sqlalchemy as sa
from sqlalchemy import inspect, text, Enum
from sqlalchemy.exc import OperationalError, ProgrammingError
from data.session import get_db
from config.database import DBConfig
import importlib
import pkgutil
import data.models
import enum
import json
# 设置日志
logger = logging.getLogger(__name__)
class DBMigration:
"""数据库迁移工具类"""
def __init__(self):
"""初始化迁移工具"""
self.db = next(get_db())
self.engine = self.db.bind
self.inspector = inspect(self.engine)
self.metadata = sa.MetaData()
self.metadata.reflect(bind=self.engine)
def get_all_models(self):
"""获取所有模型类"""
models = []
# 导入所有模型模块
for _, name, _ in pkgutil.iter_modules(data.models.__path__):
try:
module = importlib.import_module(f"data.models.{name}")
# 获取模块中的所有类
for attr_name in dir(module):
attr = getattr(module, attr_name)
# 检查是否为模型类
if isinstance(attr, type) and hasattr(attr, "__tablename__") and attr.__name__ != "BaseModel":
models.append(attr)
except ImportError as e:
logger.error(f"导入模块 data.models.{name} 失败: {str(e)}")
return models
def get_table_columns(self, table_name):
"""获取表的所有列信息"""
columns = {}
try:
for column in self.inspector.get_columns(table_name):
columns[column["name"]] = column
except Exception as e:
logger.error(f"获取表 {table_name} 的列信息失败: {str(e)}")
return columns
def get_model_columns(self, model):
"""获取模型的所有列信息"""
columns = {}
for name, column in model.__table__.columns.items():
columns[name] = column
return columns
def check_table_exists(self, table_name):
"""检查表是否存在"""
return self.inspector.has_table(table_name)
def create_table(self, model):
"""创建表"""
try:
model.__table__.create(self.engine)
logger.info(f"创建表 {model.__tablename__} 成功")
return True
except Exception as e:
logger.error(f"创建表 {model.__tablename__} 失败: {str(e)}")
return False
def get_column_type_sql(self, column):
"""获取列类型的SQL表示"""
try:
# 处理枚举类型
if isinstance(column.type, Enum):
# MySQL中枚举类型的处理
if self.engine.dialect.name == 'mysql':
enum_values = [f"'{val}'" for val in column.type.enums]
return f"ENUM({', '.join(enum_values)})"
# PostgreSQL中枚举类型的处理
elif self.engine.dialect.name == 'postgresql':
# 获取枚举类型名称
enum_name = column.type.name or f"{column.table.name}_{column.name}_enum"
# 创建枚举类型
enum_values = [f"'{val}'" for val in column.type.enums]
self.db.execute(text(f"CREATE TYPE IF NOT EXISTS {enum_name} AS ENUM ({', '.join(enum_values)})"))
self.db.commit()
return enum_name
# 其他类型直接使用SQLAlchemy的编译功能
return column.type.compile(self.engine.dialect)
except Exception as e:
logger.error(f"获取列类型失败: {str(e)}")
# 默认返回VARCHAR类型
return "VARCHAR(255)"
def get_column_default_sql(self, column):
"""获取列默认值的SQL表示"""
try:
default = column.default
if default is None:
return ""
# 处理服务器端默认值
if default.is_sequence or default.is_callable:
return "" # 服务器端默认值不需要在ADD COLUMN中指定
# 处理标量默认值
if default.is_scalar:
if isinstance(default.arg, bool):
return f" DEFAULT {1 if default.arg else 0}" if self.engine.dialect.name == 'mysql' else f" DEFAULT {str(default.arg).lower()}"
elif isinstance(default.arg, (int, float)):
return f" DEFAULT {default.arg}"
elif isinstance(default.arg, str):
return f" DEFAULT '{default.arg}'"
elif isinstance(default.arg, enum.Enum):
return f" DEFAULT '{default.arg.value}'"
elif default.arg is None:
return " DEFAULT NULL"
# 处理JSON默认值
if hasattr(column.type, 'python_type') and column.type.python_type == dict:
if default.arg is None:
return " DEFAULT '{}'"
return f" DEFAULT '{json.dumps(default.arg)}'"
return ""
except Exception as e:
logger.error(f"获取列默认值失败: {str(e)}")
return ""
def add_column(self, table_name, column_name, column):
"""添加列"""
try:
# 获取列类型
column_type = self.get_column_type_sql(column)
# 获取列默认值
default_value = self.get_column_default_sql(column)
# 获取列是否可为空
nullable = "" if column.nullable else " NOT NULL"
# 构建SQL语句
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}{nullable}{default_value}"
# 执行SQL
logger.info(f"执行SQL: {sql}")
self.db.execute(text(sql))
self.db.commit()
logger.info(f"添加列 {table_name}.{column_name} 成功")
return True
except Exception as e:
logger.error(f"添加列 {table_name}.{column_name} 失败: {str(e)}")
self.db.rollback()
return False
def migrate(self):
"""执行迁移"""
models = self.get_all_models()
logger.info(f"发现 {len(models)} 个模型")
for model in models:
table_name = model.__tablename__
# 检查表是否存在
if not self.check_table_exists(table_name):
logger.info(f"{table_name} 不存在,准备创建")
self.create_table(model)
continue
# 获取表的列信息
table_columns = self.get_table_columns(table_name)
# 获取模型的列信息
model_columns = self.get_model_columns(model)
# 检查是否有新增的列
for column_name, column in model_columns.items():
if column_name not in table_columns:
logger.info(f"发现新增列 {table_name}.{column_name},准备添加")
self.add_column(table_name, column_name, column)
logger.info("数据库迁移完成")
def run_migration():
"""运行数据库迁移"""
try:
logger.info("开始数据库迁移")
migration = DBMigration()
migration.migrate()
logger.info("数据库迁移成功")
return True
except Exception as e:
logger.error(f"数据库迁移失败: {str(e)}")
return False
if __name__ == "__main__":
# 设置日志格式
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# 运行迁移
run_migration()

View File

@ -302,17 +302,25 @@
2. **核心参数配置**
- **是否异步执行任务**(可选项):
- 选择是否以异步方式执行子任务
- 支持两种传参类型:
- 简单值:直接手动输入
- 表达式:通过系统默认参数拖拽生成,使用`${}`语法
- **任务变量ID**(必选项):
- **任务实例ID**(可选项):
- 指定子任务的唯一标识符
- **输入参数配置**
- 根据子任务定义的输入参数进行配置
- **输出参数映射**(可选项):
- 定义子任务输出结果与上层任务变量的映射关系
- 支持两种传参类型:
- 简单值:直接手动输入
- 表达式:通过系统默认参数拖拽生成,使用`${}`语法
3. **通用配置**
- 通过顶部的"任务输入参数"按钮进行配置,而非在核心参数配置中设置
- 输入参数配置适用于所有组件块,是通用功能
- 在此处可以定义和设置子任务所需的输入参数
**注意**:输入参数是所有块通用的配置项,不属于子任务特有的核心参数配置。每个参数都支持简单值和表达式两种传参类型。
详细说明参见:[天风任务-子任务功能说明.md](天风任务-子任务功能说明.md)
### 脚本组件功能详细说明
脚本组件是天风任务模块低代码配置工具中的核心功能之一它允许用户通过编写和配置JavaScript脚本来实现复杂的业务逻辑和数据处理功能。脚本组件为任务流程提供了灵活的编程能力使用户能够突破标准组件的限制实现个性化的任务处理逻辑。