feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本

包含多个会话的累积代码变更:
- 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>
This commit is contained in:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -35,6 +35,7 @@ from app.schemas.execution import (
from app.schemas.tasks import TaskConfigSchema
from app.services.task_executor import task_executor
from app.services.task_queue import task_queue
from app.services.output_cleanup import cleanup_output_dirs
logger = logging.getLogger(__name__)
@@ -188,6 +189,142 @@ async def cancel_execution(
return {"message": "已发送取消信号"}
import re as _re
def _parse_config_from_command(
task_codes: list[str],
command: str | None,
site_id: int,
) -> TaskConfigSchema:
"""从旧记录的 command 字符串解析出原始 CLI 参数,构建 TaskConfigSchema。
旧记录没有 config JSONB 列,但 command 包含完整的 CLI 参数。
"""
kwargs: dict = {
"tasks": task_codes or [],
"store_id": site_id,
}
if command:
# 解析 --flow
m = _re.search(r"--flow\s+(\S+)", command)
if m:
kwargs["flow"] = m.group(1)
# 解析 --processing-mode
m = _re.search(r"--processing-mode\s+(\S+)", command)
if m:
kwargs["processing_mode"] = m.group(1)
# 解析 --lookback-hours
m = _re.search(r"--lookback-hours\s+(\d+)", command)
if m:
kwargs["lookback_hours"] = int(m.group(1))
# 解析 --overlap-seconds
m = _re.search(r"--overlap-seconds\s+(\d+)", command)
if m:
kwargs["overlap_seconds"] = int(m.group(1))
# 解析 --window-start / --window-end
m = _re.search(r"--window-start\s+(\S+)", command)
if m:
kwargs["window_start"] = m.group(1)
kwargs["window_mode"] = "custom"
m = _re.search(r"--window-end\s+(\S+)", command)
if m:
kwargs["window_end"] = m.group(1)
# 解析 --dry-run
if "--dry-run" in command:
kwargs["dry_run"] = True
# 解析 --force-full
if "--force-full" in command:
kwargs["force_full"] = True
# 解析 --fetch-before-verify
if "--fetch-before-verify" in command:
kwargs["fetch_before_verify"] = True
return TaskConfigSchema(**kwargs)
# ── POST /api/execution/{id}/rerun — 重新执行 ───────────────
# CHANGE 2026-03-22 | 支持对任意历史任务重新执行
@router.post("/{execution_id}/rerun", response_model=ExecutionRunResponse)
async def rerun_execution(
execution_id: str,
user: CurrentUser = Depends(get_current_user),
) -> ExecutionRunResponse:
"""根据历史执行记录重新执行相同的任务。
优先从 config JSONB 列还原完整配置;若旧记录无 config 列,
回退到 task_codes + 默认配置。
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT task_codes, site_id, config, command
FROM task_execution_log
WHERE id = %s AND site_id = %s
""",
(execution_id, user.site_id),
)
row = cur.fetchone()
conn.commit()
finally:
conn.close()
if row is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="执行记录不存在",
)
task_codes = row[0] or []
config_json = row[2] # JSONB可能为 None旧记录
command_str = row[3] # command 字符串,用于旧记录回退解析
if not task_codes and not config_json:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="原执行记录无任务代码,无法重新执行",
)
# CHANGE 2026-03-22 | 优先从存储的完整 config 还原,保留原始 processing_mode/lookback 等参数
if config_json and isinstance(config_json, dict):
# 覆盖 store_id 为当前用户的(安全)
config_json["store_id"] = user.site_id
config = TaskConfigSchema(**config_json)
else:
# 旧记录无 config 列,尝试从 command 字符串解析原始参数
config = _parse_config_from_command(task_codes, command_str, user.site_id)
new_execution_id = str(uuid.uuid4())
asyncio.create_task(
task_executor.execute(
config=config,
execution_id=new_execution_id,
site_id=user.site_id,
)
)
logger.info(
"重新执行 [%s] → [%s], tasks=%s",
execution_id, new_execution_id, task_codes,
)
return ExecutionRunResponse(
execution_id=new_execution_id,
message=f"已基于 {execution_id[:8]}… 重新执行",
)
# ── GET /api/execution/history — 执行历史 ────────────────────
@router.get("/history", response_model=list[ExecutionHistoryItem])
@@ -281,3 +418,21 @@ async def get_execution_logs(
output_log=row[0],
error_log=row[1],
)
# ── POST /api/execution/cleanup-output — 清理输出目录 ────────
# CHANGE 2026-03-27 | 新增:执行前清理 EXPORT_ROOT 下旧运行记录,每类任务只保留最近 10 个
@router.post("/cleanup-output")
async def cleanup_output(
user: CurrentUser = Depends(get_current_user),
) -> dict:
"""清理 EXPORT_ROOT 下每个任务文件夹的旧运行记录,只保留最近 10 个。"""
try:
result = cleanup_output_dirs(keep=10)
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(exc),
)
return result