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:
@@ -27,9 +27,11 @@ from typing import Any
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.ai.bailian_client import BailianClient
|
||||
from app.ai.config import AIConfig
|
||||
from app.ai.dashscope_client import DashScopeClient
|
||||
from app.database import get_connection
|
||||
from app.services import fdw_queries
|
||||
from app.trace.decorators import trace_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,6 +48,7 @@ class ChatService:
|
||||
# CHAT-1: 对话历史列表
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("查询对话历史", "Get chat history")
|
||||
def get_chat_history(
|
||||
self,
|
||||
user_id: int,
|
||||
@@ -149,6 +152,7 @@ class ChatService:
|
||||
# 对话复用 / 创建
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("查找或创建对话", "Get or create session")
|
||||
def get_or_create_session(
|
||||
self,
|
||||
user_id: int,
|
||||
@@ -213,7 +217,7 @@ class ChatService:
|
||||
context_type: str,
|
||||
context_id: str | None,
|
||||
) -> int:
|
||||
"""创建新对话记录,返回 conversation_id。"""
|
||||
"""创建新对话记录,返回 conversation_id。同时生成 session_id。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
@@ -230,11 +234,23 @@ class ChatService:
|
||||
INSERT INTO biz.ai_conversations
|
||||
(user_id, nickname, app_id, site_id, context_type, context_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
RETURNING id, EXTRACT(EPOCH FROM created_at)::bigint
|
||||
""",
|
||||
(str(user_id), nickname, APP_ID, site_id, context_type, context_id),
|
||||
)
|
||||
new_id = cur.fetchone()[0]
|
||||
result = cur.fetchone()
|
||||
new_id = result[0]
|
||||
created_ts = result[1]
|
||||
|
||||
# 生成 session_id 并回写(格式:conv_{id}_{timestamp})
|
||||
session_id = f"conv_{new_id}_{created_ts}"
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.ai_conversations SET session_id = %s WHERE id = %s
|
||||
""",
|
||||
(session_id, new_id),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return new_id
|
||||
except Exception:
|
||||
@@ -243,10 +259,26 @@ class ChatService:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@trace_service("获取对话 session_id", "Get session ID")
|
||||
def get_session_id(self, chat_id: int) -> str | None:
|
||||
"""获取对话的 session_id。无记录或字段为空时返回 None。"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT session_id FROM biz.ai_conversations WHERE id = %s",
|
||||
(chat_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return row[0] if row and row[0] else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CHAT-2: 消息列表
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("查询消息列表", "Get messages")
|
||||
def get_messages(
|
||||
self,
|
||||
chat_id: int,
|
||||
@@ -312,6 +344,7 @@ class ChatService:
|
||||
# CHAT-3: 发送消息(同步回复)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("发送消息并获取回复", "Send message sync")
|
||||
async def send_message_sync(
|
||||
self,
|
||||
chat_id: int,
|
||||
@@ -368,6 +401,7 @@ class ChatService:
|
||||
# referenceCard 组装
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("构建引用卡片", "Build reference card")
|
||||
def build_reference_card(
|
||||
self,
|
||||
customer_id: int,
|
||||
@@ -438,6 +472,7 @@ class ChatService:
|
||||
# 标题生成
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@trace_service("生成对话标题", "Generate title")
|
||||
def generate_title(
|
||||
self,
|
||||
title: str | None = None,
|
||||
@@ -582,11 +617,13 @@ class ChatService:
|
||||
user_id: int,
|
||||
site_id: int,
|
||||
) -> tuple[str, int | None]:
|
||||
"""调用百炼 API 获取非流式回复,返回 (reply_text, tokens_used)。
|
||||
"""调用 DashScope Application API 获取非流式回复,返回 (reply_text, tokens_used)。
|
||||
|
||||
构建历史消息上下文发送给 AI。
|
||||
通过 Application.call() 调用 App1(通用对话),prompt 为最近历史拼接。
|
||||
"""
|
||||
bailian = _get_bailian_client()
|
||||
# CHANGE 2026-03-22 | BailianClient → DashScopeClient(P14 迁移收尾)
|
||||
client = _get_dashscope_client()
|
||||
ai_config = AIConfig.from_env()
|
||||
|
||||
# 获取历史消息作为上下文(最近 20 条)
|
||||
conn = get_connection()
|
||||
@@ -604,33 +641,21 @@ class ChatService:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 构建消息列表
|
||||
messages: list[dict] = []
|
||||
# 取最近 20 条(含刚写入的 user 消息)
|
||||
# 拼接历史消息为 prompt 文本
|
||||
recent = history[-20:] if len(history) > 20 else history
|
||||
prompt_parts: list[str] = []
|
||||
for role, msg_content in recent:
|
||||
messages.append({"role": role, "content": msg_content})
|
||||
prompt_parts.append(f"[{role}]: {msg_content}")
|
||||
prompt = "\n".join(prompt_parts)
|
||||
|
||||
# 如果没有 system 消息,添加默认 system prompt
|
||||
if not messages or messages[0]["role"] != "system":
|
||||
system_prompt = {
|
||||
"role": "system",
|
||||
"content": json.dumps(
|
||||
{"task": "你是台球门店的 AI 助手,根据用户的问题和当前页面上下文提供帮助。"},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
messages.insert(0, system_prompt)
|
||||
# 通过 Application API 调用 App1
|
||||
result, tokens_used, _session_id = await client.call_app(
|
||||
ai_config.app_id_1_chat, prompt,
|
||||
)
|
||||
|
||||
# 非流式调用(chat_stream 用于 SSE,这里用 chat_stream 收集完整回复)
|
||||
full_parts: list[str] = []
|
||||
async for chunk in bailian.chat_stream(messages):
|
||||
full_parts.append(chunk)
|
||||
|
||||
reply = "".join(full_parts)
|
||||
# 流式模式不返回 tokens_used,按字符数估算
|
||||
estimated_tokens = len(reply)
|
||||
return reply, estimated_tokens
|
||||
# 从返回结果提取文本回复
|
||||
reply = result.get("text", "") if isinstance(result, dict) else str(result)
|
||||
return reply, tokens_used
|
||||
|
||||
@staticmethod
|
||||
def _get_consumption_30d(conn: Any, site_id: int, member_id: int) -> Decimal | None:
|
||||
@@ -673,13 +698,8 @@ class ChatService:
|
||||
# ── 模块级辅助函数 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def _get_bailian_client() -> BailianClient:
|
||||
"""从环境变量构建 BailianClient,缺失时报错。"""
|
||||
api_key = os.environ.get("BAILIAN_API_KEY")
|
||||
base_url = os.environ.get("BAILIAN_BASE_URL")
|
||||
model = os.environ.get("BAILIAN_MODEL")
|
||||
if not api_key or not base_url or not model:
|
||||
raise RuntimeError(
|
||||
"百炼 API 环境变量缺失,需要 BAILIAN_API_KEY、BAILIAN_BASE_URL、BAILIAN_MODEL"
|
||||
)
|
||||
return BailianClient(api_key=api_key, base_url=base_url, model=model)
|
||||
def _get_dashscope_client() -> DashScopeClient:
|
||||
"""从环境变量构建 DashScopeClient,缺失时报错。"""
|
||||
# CHANGE 2026-03-22 | BailianClient → DashScopeClient(P14 迁移收尾)
|
||||
ai_config = AIConfig.from_env()
|
||||
return DashScopeClient(api_key=ai_config.api_key, workspace_id=ai_config.workspace_id)
|
||||
|
||||
Reference in New Issue
Block a user