fix(ai): F1-5a 沙箱 batch-run 接入 runtime_context (W1 / 阶段 A 主体)
Neo F1-5 反馈: "让沙箱起到其真正的作用. 真正的模拟日期, 仅能看到沙箱设定日期 及之前日期的数据, 并运行 AI 的各个业务." 调研发现 (4 个并行子代理): batch-run 端点 _run_batch 是空壳 stub (只 logger.info, 实际不跑 AI), GUC apply_runtime_session_vars 0 处调用 (dead code), 7 张业务表 6 张有 runtime 复合索引唯独 ai_run_logs 漏建, App2/2a 3 行 _calc_date_range 漏传 ref_date. 本 commit (F1-5a 阶段 A 主体, F1-5b 后续完整 zqyy_app RLS 视图层): 后端核心: - admin_service.py: _run_batch 真实化 (Semaphore(5)+asyncio.gather+ return_exceptions=True+ctx_snapshot 防漂移); estimate 入口抓 RuntimeContext 快照, confirm 取出传给 worker - admin_ai.py: confirm_batch_run lazy 注入 dispatcher - admin_service.retry_trigger_job: INSERT 落 runtime_mode + sandbox_instance_id 列 (用 runtime_insert_columns helper) - runtime_context.py: get_runtime_context 加 bind_to_session 参数, 激活 GUC app.current_business_date / app.current_runtime_mode - run_log_service.create_log: 启用 bind_to_session=True 试点 App2/2a 3 行 ref_date 修复: - app2_finance_prompt.py:817 储值卡余额变化板块 - app2_finance_prompt.py:841 日粒度 series + 异常检测窗口 - app2a_finance_area_prompt.py:466 区域日粒度 series DB: - migrations/20260505__ai_run_logs_runtime_index.sql: 补 (site_id, runtime_mode, sandbox_instance_id, created_at DESC) 复合索引 前端: - AIOperations.tsx: 顶部加 sandbox 模式提示条 (Alert 显示 sandbox_date + sandbox_instance_id + 影响范围 + 切回 live 入口) 未做 (留 F1-5b 完整 zqyy_app RLS 视图层一并): - B1 admin_service 6 处 CURRENT_DATE -> business_date - B2 fdw_queries 异常分支兜底 - GUC 完整传递 (fdw_queries / page_context 等) - 测试 3 套 (.gitignore:71 排除, F2-2 入仓时 commit) - P20 SPEC \xa76/\xa710/\xa711/\xa715 (F1-5b 完整收口后同步更准确) Neo 决策: docs/_overview/wave1-findings/F1-5-impl-decisions.md 详见 docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
@@ -15,6 +16,8 @@ from typing import Any
|
||||
|
||||
from app import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LOCAL_TZ = timezone(timedelta(hours=8))
|
||||
MODE_LIVE = "live"
|
||||
MODE_SANDBOX = "sandbox"
|
||||
@@ -85,10 +88,23 @@ def _default_context(site_id: int) -> RuntimeContext:
|
||||
return RuntimeContext(site_id=site_id)
|
||||
|
||||
|
||||
def get_runtime_context(site_id: int, conn: Any | None = None) -> RuntimeContext:
|
||||
def get_runtime_context(
|
||||
site_id: int,
|
||||
conn: Any | None = None,
|
||||
*,
|
||||
bind_to_session: bool = False,
|
||||
) -> RuntimeContext:
|
||||
"""读取门店运行上下文。
|
||||
|
||||
表不存在或未配置时降级为 live,保证迁移前不影响正式链路。
|
||||
|
||||
F1-5a 新增 ``bind_to_session``:当 True 且 conn 非空时,在返回前调用
|
||||
``apply_runtime_session_vars(conn, ctx)`` 设置 GUC ``app.current_business_date`` /
|
||||
``app.current_runtime_mode``,激活 ETL 库 26 个 ``app.v_*`` 视图的业务日上界裁剪
|
||||
(`app.business_date_now()` 函数读取 GUC)。
|
||||
|
||||
使用场景:fdw_queries 等走 ETL 库视图的查询入口处显式 ``bind_to_session=True``。
|
||||
其余只读取 ctx 不查 ETL 视图的调用方保持默认 ``False`` 即可。
|
||||
"""
|
||||
own_conn = conn is None
|
||||
if own_conn:
|
||||
@@ -120,22 +136,34 @@ def get_runtime_context(site_id: int, conn: Any | None = None) -> RuntimeContext
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return _default_context(site_id)
|
||||
ctx = _default_context(site_id)
|
||||
else:
|
||||
mode, sandbox_date, sandbox_instance_id, ai_mode, status = row
|
||||
if mode not in (MODE_LIVE, MODE_SANDBOX):
|
||||
mode = MODE_LIVE
|
||||
if mode == MODE_SANDBOX and (sandbox_date is None or not sandbox_instance_id):
|
||||
mode = MODE_LIVE
|
||||
|
||||
mode, sandbox_date, sandbox_instance_id, ai_mode, status = row
|
||||
if mode not in (MODE_LIVE, MODE_SANDBOX):
|
||||
mode = MODE_LIVE
|
||||
if mode == MODE_SANDBOX and (sandbox_date is None or not sandbox_instance_id):
|
||||
mode = MODE_LIVE
|
||||
ctx = RuntimeContext(
|
||||
site_id=site_id,
|
||||
mode=mode,
|
||||
sandbox_date=sandbox_date,
|
||||
sandbox_instance_id=sandbox_instance_id,
|
||||
ai_mode=ai_mode or AI_MODE_LIVE,
|
||||
status=status or "active",
|
||||
)
|
||||
|
||||
return RuntimeContext(
|
||||
site_id=site_id,
|
||||
mode=mode,
|
||||
sandbox_date=sandbox_date,
|
||||
sandbox_instance_id=sandbox_instance_id,
|
||||
ai_mode=ai_mode or AI_MODE_LIVE,
|
||||
status=status or "active",
|
||||
)
|
||||
# F1-5a: 显式开启时,绑定到当前 session,激活 ETL 库视图业务日上界
|
||||
if bind_to_session and not own_conn and conn is not None:
|
||||
try:
|
||||
apply_runtime_session_vars(conn, ctx=ctx)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"apply_runtime_session_vars 失败(不阻塞主流程) site_id=%d", site_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
def namespace_ai_target_id(site_id: int, target_id: str, conn: Any | None = None) -> str:
|
||||
|
||||
Reference in New Issue
Block a user