first commit

This commit is contained in:
2026-06-25 17:41:06 +08:00
commit b2f23a933b
370 changed files with 30526 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
# app/controllers/automation.py (继续追加)
from app.core.crud import CRUDBase
from typing import List, Optional, Dict, Any
from app.models.automation import Action
from app.models.enums import ActionType
from app.schemas.automation import ActionCreate, ActionUpdate
from app.utils.common import transform_pydantic_to_list
from app.schemas.automation import NotifyAction
class ActionController(CRUDBase[Action, ActionCreate, ActionUpdate]):
def __init__(self):
super().__init__(model=Action)
async def get_pending_actions_by_task(self, task_id: int) -> List[Action]:
"""获取某任务中未完成的动作"""
return await self.model.filter(task_id=task_id, done=False).all()
async def list_automation_actions(self) -> List[Dict[str, Any]]:
"""获取动作类型映射"""
ActionTypeMap = [
{"value": ActionType.CREATE_GROUP, "label": "创建群聊", "automation": True, "allowAutoExecute": False, "autoExecute": False, "enable": True},
{"value": ActionType.VIEW_ERP_LOG, "label": "查看ERP日志", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
{"value": ActionType.BIND_USER, "label": "绑定用户", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
{"value": ActionType.BIND_ORDER, "label": "绑定订单", "automation": False, "allowAutoExecute": False, "autoExecute": False, "enable": True},
{"value": ActionType.WRITE_REMARK, "label": "写ERP额外备注", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True},
{"value": ActionType.SEND_WECHAT_NOTIFY, "label": "发送通知", "automation": True, "allowAutoExecute": True, "autoExecute": True, "enable": True, "schema": transform_pydantic_to_list(NotifyAction)},
{"value": ActionType.CLEAN_GROUP, "label": "清理群聊", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
{"value": ActionType.SET_GROUP_ADMIN, "label": "设置群管理员", "automation": True, "allowAutoExecute": False, "autoExecute": True, "enable": True},
]
return [item for item in ActionTypeMap if item["automation"]]
async def mark_action_done(
self,
action_id: int,
userid: str,
username: str,
result: Dict[str, Any],
notes: Optional[str] = None
) -> Action:
"""标记动作为已完成"""
update_data = ActionUpdate(
done=True,
done_at=self.model._meta.db_fields.get("done_at").to_db_value(None, None), # 实际用 datetime.now()
userid=userid,
username=username,
result=result,
notes=notes
)
# Note: 建议在 service 层处理 done_at = datetime.utcnow()
return await self.update(action_id, update_data)
async def create_actions_for_task(self, task_id: int, actions_def: List[Dict]) -> List[Action]:
"""为任务批量创建动作实例"""
actions = []
for act in actions_def:
action_in = ActionCreate(
task_id=task_id,
type=act.get("type"),
detail=act.get("detail", {}),
done=False
)
action = await self.create(action_in)
actions.append(action)
return actions
async def trigger_scenarios(self, scenario_list: List[Action], event_data: Dict[str, Any]) -> None:
"""触发场景执行"""
for scenario in scenario_list:
if not scenario.enabled:
continue
for action in scenario.actions:
if not action.enabled:
continue
if action.type == ActionType.SEND_WECHAT_NOTIFY:
await self.trigger_scenarios_by_action(action, event_data)
action_controller = ActionController()
+222
View File
@@ -0,0 +1,222 @@
# app/controllers/automation.py
from typing import List, Optional, Tuple, Dict, Any
from app.core.crud import CRUDBase
from app.models.automation import Scenario, ScenarioTriggerIndex, ScenarioScope
from app.schemas.automation import ScenarioCreate, ScenarioUpdate
from app.utils.event_task import event_manager
import logging
logger = logging.getLogger(__name__)
class ScenarioController(CRUDBase[Scenario, ScenarioCreate, ScenarioUpdate]):
def __init__(self):
super().__init__(model=Scenario)
async def update_trigger_index(self, scenario: Scenario, user_id: str) -> Scenario:
"""更新场景"""
add_list = []
event_set = set()
for condition in scenario.trigger.get('conditions') or []:
event_name = condition.get('event_name', '')
if event_name and event_name in event_set:
continue
event_set.add(event_name)
orm = ScenarioTriggerIndex(
owner_user_id=user_id,
scenario_id=scenario.id,
is_global=scenario.is_global,
event_name=condition.get('event_name', ''),
enabled=condition.get('enabled', True),
scope=condition.get('scope', ScenarioScope.PERSONAL),
)
add_list.append(orm)
if add_list:
await ScenarioTriggerIndex.bulk_create(add_list)
return True
return False
async def new_scenario(self, scenario_in: ScenarioCreate, user_id: str) -> Scenario:
"""创建新场景"""
obj = await self.create(scenario_in)
# 更新触发条件
if obj.trigger:
await self.update_trigger_index(obj, user_id)
return obj
async def update_scenario(self, scenario_in: ScenarioUpdate, user_id: str) -> Scenario:
"""更新场景"""
obj = await self.update(id=scenario_in.id, obj_in=scenario_in)
# 更新触发条件
if obj.trigger:
await ScenarioTriggerIndex.filter(scenario_id=obj.id).delete()
await self.update_trigger_index(obj, user_id)
return obj
async def remove_scenario(self, scenario_id: int) -> None:
"""删除场景"""
await self.model.filter(id=scenario_id).delete()
await ScenarioTriggerIndex.filter(scenario_id=scenario_id).delete()
return True
async def list_automation_events(self) -> List[Dict[str, Any]]:
"""获取所有自动化事件"""
return [a for a in event_manager.automation_event_handlers if a.get("automation_event")]
async def get_global_scenarios(self, enabled: bool = True) -> List[Scenario]:
"""获取所有启用的全局场景"""
return await self.model.filter(is_global=True, enabled=enabled).all()
async def get_user_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
"""获取某用户的启用个人场景"""
return await self.model.filter(owner_user_id=user_id, is_global=False, enabled=enabled).all()
async def get_applicable_scenarios(self, user_id: str, enabled: bool = True) -> List[Scenario]:
"""获取对某用户生效的所有场景(全局 + 个人)"""
global_scenarios = await self.get_global_scenarios(enabled=enabled)
personal_scenarios = await self.get_user_scenarios(user_id, enabled=enabled)
return global_scenarios + personal_scenarios
class AutomationScenarioController(ScenarioController):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def find_active_scenarios(self, event_name: str, event_data: Dict[str, Any]) -> List[Scenario]:
"""查找所有实际激活的场景配置"""
scenario_index_list = await ScenarioTriggerIndex.filter(enabled=True, event_name=event_name).all()
scenario_list = []
if scenario_index_list:
scenario_list = await self.model.filter(id__in=[s.scenario_id for s in scenario_index_list]).all()
logger.info(f'find_active_scenarios: {len(scenario_list)}')
active_scenarios = []
for scenario in scenario_list:
result, reason = await self.check_is_applicable(scenario, event_data)
if result:
active_scenarios.append((scenario, reason))
return active_scenarios
async def check_is_applicable(self, scenario: Scenario, event_data: Dict[str, Any]) -> tuple[bool, str]:
"""检查场景是否适用于当前事件"""
trigger = scenario.trigger
# 如果没有条件,默认返回True,即只要事件发生就会激活场景
if not trigger or not trigger.get('conditions'):
logger.info(f'scenario {scenario.id} trigger without conditions, always return True')
return True, '没有设置具体的触发条件,事件发生就激活场景'
logic = trigger.get('logic', 'and')
conditions = trigger.get('conditions', [])
if logic == 'or':
for condition in conditions:
condition = condition.get('condition', {})
result, reason = self.check_condition(condition, event_data)
if result:
logger.info(f'scenario {scenario.id} trigger condition {condition} return True, reason: {reason}')
return True, reason
return False, '所有触发条件都不满足'
elif logic == 'and':
reason_list = []
for condition in conditions:
condition = condition.get('condition', {})
result, reason = self.check_condition(condition, event_data)
if not result:
logger.info(f'scenario {scenario.id} trigger condition {condition} return False, reason: {reason}')
return False, f'触发条件 {condition} 不满足, 原因: {reason}'
else:
reason_list.append(reason)
return True, f'{", ".join(reason_list)}'
raise ValueError(f"Invalid logic operator: {logic}")
def check_condition(self, condition: Dict[str, Any], event_data: Dict[str, Any]) -> tuple[bool, str]:
"""检查单个条件是否满足"""
def find_field_val(field: str, event_data: Dict[str, Any]) -> Any:
"""递归查找字段值"""
if '.' in field:
parts = field.split('.')
current = event_data
for part in parts:
if isinstance(current, list):
# current = current[int(part)]
result = []
for item in current:
# find_field_val
field_val = find_field_val(part, item)
if field_val is not None:
result.append(field_val)
return result
elif isinstance(current, dict):
current = current.get(part, None)
else:
return None
return current
return event_data.get(field, None)
if not condition or not condition.get('field', ''):
logger.error(f'scenario check_condition: field is empty, 当做无效条件处理')
return True, '没有设置具体的触发条件,事件发生就激活场景'
field = condition.get('field', '')
value = condition.get('value', '')
operator = condition.get('operator', '')
field_val = find_field_val(field, event_data)
if field_val is None:
logger.error(f'scenario check_condition: field {field} value is None, event_data: {event_data}')
return False, f'触发条件 {condition} 字段 {field} 不存在'
# 字符串比较
if operator in ['contains', 'not_contains']:
# if not isinstance(field_val, str):
# return False, f'字段 {field} 不是字符串类型'
if operator == 'contains' and value in field_val:
return True, f'字段 {field} 包含 {value}'
if operator == 'not_contains' and value not in field_val:
return True, f'字段 {field} 不包含 {value}'
return False, f'字段 {field} 不满足 {operator} {value}'
# 数字、时间比较
if operator in ['gt', 'gte', 'lt', 'lte', 'eq', 'ne']:
if not isinstance(field_val, (int, float, str)):
return False, f'字段 {field} 不是数字或时间类型'
try:
field_val = float(field_val)
value = float(value)
except ValueError:
return False, f'字段 {field} 不是数字或时间类型'
if operator == 'gt' and field_val > value:
return True, f'字段 {field} 大于 {value}'
if operator == 'gte' and field_val >= value:
return True, f'字段 {field} 大于等于 {value}'
if operator == 'lt' and field_val < value:
return True, f'字段 {field} 小于 {value}'
if operator == 'lte' and field_val <= value:
return True, f'字段 {field} 小于等于 {value}'
if operator == 'eq' and field_val == value:
return True, f'字段 {field} 等于 {value}'
if operator == 'ne' and field_val != value:
return True, f'字段 {field} 不等于 {value}'
return False, f'字段 {field} 不满足 {operator} {value}'
automation_scenario_controller = AutomationScenarioController()
+77
View File
@@ -0,0 +1,77 @@
# app/controllers/automation.py (继续追加)
from app.core.crud import CRUDBase
from typing import List, Optional, Dict, Any, Tuple
from tortoise.expressions import Q
from app.models.automation import Task, Scenario, ScenarioScope
from app.schemas.automation import TaskCreate, TaskUpdate
from datetime import datetime, timedelta
class TaskController(CRUDBase[Task, TaskCreate, TaskUpdate]):
def __init__(self):
super().__init__(model=Task)
async def get_user_tasks(
self,
user_id: str,
status: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> Tuple[int, List[Task]]:
"""分页获取某用户的待办(支持按状态过滤)"""
query = Q(assignee_user_id=user_id)
if status:
query &= Q(status=status)
return await self.list(page=page, page_size=page_size, search=query, order=["-created_at"])
async def get_tasks_by_customer(self, customer_id: str) -> List[Task]:
"""获取与某客户相关的所有待办(用于侧边栏上下文)"""
return await self.model.filter(related_customer_id=customer_id).order_by("-created_at").all()
async def create_from_scenario(
self,
scenario: Scenario,
event_data: Optional[Dict[str, Any]] = None,
assignee_user_id: Optional[str] = None,
assignee_username: Optional[str] = None,
reason: Optional[str] = None,
) -> Task:
"""根据场景模板创建待办任务"""
# 计算截止时间(示例:场景中定义的 due_days 天后)
due_at = scenario.due_days and (datetime.now() + timedelta(days=scenario.due_days)) or None
owner_user_id = None
if scenario.scope == ScenarioScope.ALL:
owner_user_id = ScenarioScope.ALL
elif scenario.scope == ScenarioScope.PERSONAL:
owner_user_id = scenario.owner_user_id
task_in = TaskCreate(
title=scenario.title,
reason=reason,
notes=scenario.notes,
event_data=event_data or {},
source_scenario=scenario,
source_scenario_id=scenario.id,
owner_user_id=owner_user_id,
assignee_user_id=assignee_user_id,
assignee_username=assignee_username,
due_at=due_at,
status="pending",
auto_closeable=True,
)
return await self.create(task_in)
async def auto_close_by_event(self, event_type: str, payload: Dict[str, Any]) -> List[Task]:
"""
根据事件自动关闭匹配的待办(用于自动消除)
例如:当 group_id 匹配且动作含 create_group 时,关闭待办
"""
# 示例逻辑:后续可扩展为规则引擎
closed_tasks = []
# 这里可遍历 pending 任务,检查 content 中的动作是否已被事件满足
# 为简化,此处留作扩展点
return closed_tasks
task_controller = TaskController()