包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
# AI_CHANGELOG
|
||
# - 2026-03-29 | Prompt: DWS_TASK_ENGINE ETL 任务 | 新建文件。
|
||
# 编排任务引擎全流程:完成检查 → 过期检查 → 任务生成。
|
||
# 通过 HTTP 调用后端 POST /api/internal/run-job 按 job_name 执行。
|
||
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
DWS 任务引擎编排任务(DWS_TASK_ENGINE)
|
||
|
||
在 DWS 指数计算完成后执行,按顺序调用后端任务引擎的各个步骤:
|
||
1. recall_completion_check — 检测召回是否完成,生成回访任务
|
||
2. task_expiry_check — 标记超时未处理的任务
|
||
3. task_generator — 根据 WBI/NCI/RS 指数生成/替换任务
|
||
|
||
通过 HTTP 调用后端 POST /api/internal/run-job(Internal-Token 认证),
|
||
每步失败仅记录日志,不中断后续步骤。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import requests
|
||
from dotenv import load_dotenv
|
||
|
||
from ..base_task import BaseTask, TaskContext
|
||
|
||
# 加载根 .env(BACKEND_API_URL / INTERNAL_API_TOKEN 不在 AppConfig 映射中)
|
||
# task_engine.py → dws/ → tasks/ → feiqiu/ → connectors/ → etl/ → apps/ → root
|
||
_REPO_ROOT = Path(__file__).resolve().parents[6]
|
||
load_dotenv(_REPO_ROOT / ".env", override=False)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_TIMEOUT = (5, 30) # 连接 5s,读取 30s(任务执行可能较慢)
|
||
|
||
# 按顺序执行的后端任务列表
|
||
_JOB_SEQUENCE = [
|
||
"recall_completion_check",
|
||
"task_expiry_check",
|
||
"task_generator",
|
||
]
|
||
|
||
|
||
def _run_backend_job(backend_url: str, token: str, job_name: str) -> dict:
|
||
"""调用后端 POST /api/internal/run-job 执行指定任务。
|
||
|
||
Returns:
|
||
{"success": bool, "message": str} 或 {"success": False, "message": error}
|
||
"""
|
||
url = f"{backend_url}/api/internal/run-job"
|
||
headers = {
|
||
"Authorization": f"Internal-Token {token}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
body = {"job_name": job_name}
|
||
|
||
try:
|
||
resp = requests.post(url, json=body, headers=headers, timeout=_TIMEOUT)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
# 后端 ResponseWrapperMiddleware 包装:{"code": 0, "data": {...}}
|
||
inner = data.get("data", data)
|
||
return {
|
||
"success": inner.get("success", False),
|
||
"message": inner.get("message", ""),
|
||
}
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"message": f"HTTP {resp.status_code}: {resp.text[:200]}",
|
||
}
|
||
except requests.RequestException as exc:
|
||
return {"success": False, "message": str(exc)}
|
||
|
||
|
||
class DwsTaskEngineTask(BaseTask):
|
||
"""DWS 任务引擎编排任务。
|
||
|
||
不读写 DWS 表,仅通过 HTTP 调用后端执行任务引擎步骤。
|
||
继承 BaseTask 而非 BaseDwsTask,因为不需要 DWS 层的数据操作方法。
|
||
"""
|
||
|
||
def get_task_code(self) -> str:
|
||
return "DWS_TASK_ENGINE"
|
||
|
||
def extract(self, context: TaskContext) -> dict[str, Any]:
|
||
"""无需提取数据,返回空上下文。"""
|
||
return {}
|
||
|
||
def load(self, extracted: dict[str, Any], context: TaskContext) -> dict[str, Any]:
|
||
"""按顺序调用后端任务引擎的各个步骤。"""
|
||
backend_url = os.environ.get("BACKEND_API_URL", "").rstrip("/")
|
||
token = os.environ.get("INTERNAL_API_TOKEN", "")
|
||
|
||
if not backend_url:
|
||
self.logger.error("DWS_TASK_ENGINE 跳过:BACKEND_API_URL 未配置")
|
||
return {"skipped": True, "reason": "BACKEND_API_URL 未配置"}
|
||
if not token:
|
||
self.logger.error("DWS_TASK_ENGINE 跳过:INTERNAL_API_TOKEN 未配置")
|
||
return {"skipped": True, "reason": "INTERNAL_API_TOKEN 未配置"}
|
||
|
||
results: dict[str, Any] = {}
|
||
|
||
for job_name in _JOB_SEQUENCE:
|
||
self.logger.info("DWS_TASK_ENGINE: 执行 %s ...", job_name)
|
||
result = _run_backend_job(backend_url, token, job_name)
|
||
success = result.get("success", False)
|
||
message = result.get("message", "")
|
||
|
||
results[job_name] = {"success": success, "message": message}
|
||
|
||
if success:
|
||
self.logger.info(
|
||
"DWS_TASK_ENGINE: %s 成功 — %s", job_name, message
|
||
)
|
||
else:
|
||
self.logger.warning(
|
||
"DWS_TASK_ENGINE: %s 失败 — %s", job_name, message
|
||
)
|
||
|
||
return results
|