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

@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""
SSE 流式响应追踪辅助函数
提供一组轻量函数,在 SSE event_generator 内部调用,
追踪流式响应全过程SSE_START → AI_CALL → SSE_EVENT → SSE_END / AI_ERROR
所有函数在无活跃 TraceContext 时静默跳过,不影响业务逻辑。
SSE_EVENT span 每 10 个 token 记录一次,避免 span 爆炸。
"""
from __future__ import annotations
from datetime import datetime
from app.trace.context import (
SpanType,
TraceSpan,
TraceType,
get_current_trace,
)
def record_sse_start(
endpoint: str,
user_id: int | None = None,
chat_id: str = "",
) -> None:
"""记录 SSE_START span流开始。
同时将 trace_type 切换为 "sse"
"""
ctx = get_current_trace()
if ctx is None:
return
# 切换 trace 类型为 SSE
ctx.trace_type = TraceType.SSE
ctx.add_span(TraceSpan(
span_type=SpanType.SSE_START,
module="trace.sse_wrapper",
function="record_sse_start",
description_zh=f"SSE 流开始: endpoint={endpoint}, chat_id={chat_id}",
description_en=f"SSE stream started: endpoint={endpoint}, chat_id={chat_id}",
params={"endpoint": endpoint, "user_id": user_id, "chat_id": chat_id},
result_summary="",
duration_ms=0.0,
timestamp=datetime.now().isoformat(),
))
def record_ai_call(
app_id: str,
prompt_length: int,
session_id: str = "",
) -> None:
"""记录 AI_CALL spanDashScope API 调用。"""
ctx = get_current_trace()
if ctx is None:
return
ctx.add_span(TraceSpan(
span_type=SpanType.AI_CALL,
module="trace.sse_wrapper",
function="record_ai_call",
description_zh=f"AI 调用: app_id={app_id}, prompt 长度={prompt_length}",
description_en=f"AI call: app_id={app_id}, prompt_length={prompt_length}",
params={
"app_id": app_id,
"prompt_length": prompt_length,
"session_id": session_id,
},
result_summary="",
duration_ms=0.0,
timestamp=datetime.now().isoformat(),
))
def record_sse_token(token_count: int, total_tokens: int) -> None:
"""记录 SSE_EVENT span每 10 个 token 记录一次,避免 span 爆炸。
仅当 total_tokens 是 10 的倍数时才记录 span。
"""
ctx = get_current_trace()
if ctx is None:
return
# 每 10 个 token 记录一次
if total_tokens % 10 != 0:
return
ctx.add_span(TraceSpan(
span_type=SpanType.SSE_EVENT,
module="trace.sse_wrapper",
function="record_sse_token",
description_zh=f"SSE token 流: 本批 {token_count} token, 累计 {total_tokens}",
description_en=f"SSE token stream: batch {token_count}, cumulative {total_tokens}",
params={"token_count": token_count, "total_tokens": total_tokens},
result_summary=f"cumulative={total_tokens}",
duration_ms=0.0,
timestamp=datetime.now().isoformat(),
))
def record_sse_end(
total_tokens: int,
total_duration_ms: float,
completed: bool = True,
) -> None:
"""记录 SSE_END span流结束。"""
ctx = get_current_trace()
if ctx is None:
return
ctx.add_span(TraceSpan(
span_type=SpanType.SSE_END,
module="trace.sse_wrapper",
function="record_sse_end",
description_zh=f"SSE 流结束: 总 token={total_tokens}, 耗时={total_duration_ms:.0f}ms, 完成={completed}",
description_en=f"SSE stream ended: total_tokens={total_tokens}, duration={total_duration_ms:.0f}ms, completed={completed}",
params={
"total_tokens": total_tokens,
"total_duration_ms": total_duration_ms,
"completed": completed,
},
result_summary=f"tokens={total_tokens}, completed={completed}",
duration_ms=total_duration_ms,
timestamp=datetime.now().isoformat(),
))
def record_ai_error(error_type: str, message: str) -> None:
"""记录 AI_ERROR spanAI 调用失败。"""
ctx = get_current_trace()
if ctx is None:
return
ctx.add_span(TraceSpan(
span_type=SpanType.AI_ERROR,
module="trace.sse_wrapper",
function="record_ai_error",
description_zh=f"AI 调用失败: {error_type} - {message}",
description_en=f"AI call failed: {error_type} - {message}",
params={"error_type": error_type, "message": message},
result_summary=f"{error_type}: {message}",
duration_ms=0.0,
timestamp=datetime.now().isoformat(),
))