|
@@ -11,13 +11,15 @@
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
import asyncio
|
|
import asyncio
|
|
|
|
|
+import json
|
|
|
import logging
|
|
import logging
|
|
|
import os
|
|
import os
|
|
|
|
|
+import sys
|
|
|
import uuid
|
|
import uuid
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
|
|
+from decimal import Decimal, ROUND_HALF_UP
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from typing import Optional
|
|
from typing import Optional
|
|
|
-import sys
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
|
|
|
@@ -38,7 +40,13 @@ from db import (
|
|
|
update_task_status,
|
|
update_task_status,
|
|
|
update_task_on_complete,
|
|
update_task_on_complete,
|
|
|
)
|
|
)
|
|
|
-from db.schedule import STATUS_RUNNING, STATUS_SUCCESS, STATUS_FAILED
|
|
|
|
|
|
|
+from db.schedule import (
|
|
|
|
|
+ STATUS_RUNNING,
|
|
|
|
|
+ STATUS_SUCCESS,
|
|
|
|
|
+ STATUS_FAILED,
|
|
|
|
|
+ get_latest_day_limit_coast,
|
|
|
|
|
+ get_total_token_coast_between,
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
# 配置日志
|
|
# 配置日志
|
|
|
log_dir = Path(__file__).parent / '.cache'
|
|
log_dir = Path(__file__).parent / '.cache'
|
|
@@ -106,12 +114,53 @@ class TaskResponse(BaseModel):
|
|
|
|
|
|
|
|
# ============ 核心函数 ============
|
|
# ============ 核心函数 ============
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+def _load_token_coast_from_meta(trace_id: str) -> Optional[Decimal]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 从 TRACE_DIR/{trace_id}/meta.json 读取本次任务的 token 费用,并转成两位小数的 Decimal。
|
|
|
|
|
+ 优先读取 total_cost 字段,兼容 total_coast;读取或解析失败返回 None。
|
|
|
|
|
+ """
|
|
|
|
|
+ trace_dir = Path(os.getenv("TRACE_DIR", ".cache/traces"))
|
|
|
|
|
+ meta_path = trace_dir / trace_id / "meta.json"
|
|
|
|
|
+ if not meta_path.exists():
|
|
|
|
|
+ logger.warning("未找到 meta.json,trace_id=%s, path=%s", trace_id, meta_path)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ with meta_path.open("r", encoding="utf-8") as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("读取 meta.json 失败: trace_id=%s, error=%s", trace_id, e)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ raw_cost = data.get("total_cost")
|
|
|
|
|
+ if raw_cost is None:
|
|
|
|
|
+ raw_cost = data.get("total_coast")
|
|
|
|
|
+ if raw_cost is None:
|
|
|
|
|
+ logger.warning("meta.json 中未找到 total_cost/total_coast 字段: trace_id=%s", trace_id)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ cost_decimal = Decimal(str(raw_cost)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
|
|
|
+ return cost_decimal
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning("解析 token 费用失败: trace_id=%s, raw=%s, error=%s", trace_id, raw_cost, e)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _update_scheduled_task_complete(demand_id: int, trace_id: str, status: int) -> None:
|
|
def _update_scheduled_task_complete(demand_id: int, trace_id: str, status: int) -> None:
|
|
|
- """定时任务完成时更新 trace_id 和 status,静默处理异常"""
|
|
|
|
|
|
|
+ """
|
|
|
|
|
+ 定时任务完成时更新 trace_id、status 以及 token_coast(若能从 meta.json 成功解析)。
|
|
|
|
|
+ 静默处理异常,不影响整体调度流程。
|
|
|
|
|
+ """
|
|
|
try:
|
|
try:
|
|
|
- update_task_on_complete(demand_id, trace_id, status)
|
|
|
|
|
|
|
+ token_coast: Optional[Decimal] = None
|
|
|
|
|
+ if trace_id:
|
|
|
|
|
+ token_coast = _load_token_coast_from_meta(trace_id)
|
|
|
|
|
+
|
|
|
|
|
+ update_task_on_complete(demand_id, trace_id, status, token_coast)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.warning(f"更新任务状态失败: {e}")
|
|
|
|
|
|
|
+ logger.warning("更新任务状态或 token_coast 失败: %s", e)
|
|
|
|
|
|
|
|
|
|
|
|
|
async def execute_task(
|
|
async def execute_task(
|
|
@@ -223,6 +272,22 @@ async def scheduled_tick():
|
|
|
logger.info("定时任务跳过:demand_task_oprate 最新记录 is_open=0")
|
|
logger.info("定时任务跳过:demand_task_oprate 最新记录 is_open=0")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
|
|
+ # 检查当日 token_coast 是否已超出日预算(以 SCHEDULER_TZ 当地时间为准)
|
|
|
|
|
+ day_limit_coast = get_latest_day_limit_coast()
|
|
|
|
|
+ if day_limit_coast is not None:
|
|
|
|
|
+ now_local = datetime.now(SCHEDULER_TZ)
|
|
|
|
|
+ day_start = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
|
+ day_end = day_start.replace(day=day_start.day + 1)
|
|
|
|
|
+
|
|
|
|
|
+ used_today = get_total_token_coast_between(day_start, day_end)
|
|
|
|
|
+ if used_today >= day_limit_coast:
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "定时任务跳过:当日 token_coast 已达上限,used=%s, limit=%s",
|
|
|
|
|
+ used_today,
|
|
|
|
|
+ day_limit_coast,
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
# 无空闲并发槽则不派发;保持 tick 很快返回,避免阻塞调度器。
|
|
# 无空闲并发槽则不派发;保持 tick 很快返回,避免阻塞调度器。
|
|
|
if task_semaphore._value <= 0:
|
|
if task_semaphore._value <= 0:
|
|
|
logger.info("定时任务跳过:无空闲并发槽")
|
|
logger.info("定时任务跳过:无空闲并发槽")
|