fix: resolve Feishu cross-app notification routing bug
Implement per-app open_id storage via user_feishu_open_ids table with union_id-based cross-app user identification. WS handlers now auto-capture open_id+union_id and resolve/associate user accounts. Schedule notifications route through the correct bot's open_id instead of always falling back to 苹果. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,9 +89,16 @@ async def bind_feishu(
|
||||
if not data.open_id or not data.open_id.strip():
|
||||
raise HTTPException(status_code=400, detail="open_id 不能为空")
|
||||
|
||||
current_user.feishu_open_id = data.open_id.strip()
|
||||
open_id = data.open_id.strip()
|
||||
current_user.feishu_open_id = open_id
|
||||
db.commit()
|
||||
logger.info("飞书绑定成功: user=%s open_id=%s", current_user.id, data.open_id)
|
||||
# 同步写入多应用 open_id 表(含 union_id 用于跨应用识别)
|
||||
from app.services.feishu_open_id_service import save_open_id
|
||||
from app.services.feishu_app_service import lookup_union_id_by_open_id
|
||||
from app.core.config import settings
|
||||
union_id = lookup_union_id_by_open_id(open_id)
|
||||
save_open_id(db, app_id=settings.FEISHU_APP_ID or "", open_id=open_id, user_id=current_user.id, union_id=union_id)
|
||||
logger.info("飞书绑定成功: user=%s open_id=%s union_id=%s", current_user.id, open_id, union_id)
|
||||
return {"message": "飞书账号绑定成功"}
|
||||
|
||||
|
||||
@@ -132,7 +139,13 @@ async def lookup_and_bind(
|
||||
|
||||
current_user.feishu_open_id = open_id
|
||||
db.commit()
|
||||
logger.info("飞书自动绑定成功: user=%s email=%s open_id=%s", current_user.id, current_user.email, open_id)
|
||||
# 同步写入多应用 open_id 表(含 union_id 用于跨应用识别)
|
||||
from app.services.feishu_open_id_service import save_open_id
|
||||
from app.services.feishu_app_service import lookup_union_id_by_open_id
|
||||
from app.core.config import settings
|
||||
union_id = lookup_union_id_by_open_id(open_id)
|
||||
save_open_id(db, app_id=settings.FEISHU_APP_ID or "", open_id=open_id, user_id=current_user.id, union_id=union_id)
|
||||
logger.info("飞书自动绑定成功: user=%s email=%s open_id=%s union_id=%s", current_user.id, current_user.email, open_id, union_id)
|
||||
|
||||
# 发送测试消息
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
@@ -176,6 +189,12 @@ async def bind_pending(
|
||||
open_id = ids[-1]
|
||||
current_user.feishu_open_id = open_id
|
||||
db.commit()
|
||||
# 同步写入多应用 open_id 表(含 union_id 用于跨应用识别)
|
||||
from app.services.feishu_open_id_service import save_open_id
|
||||
from app.services.feishu_app_service import lookup_union_id_by_open_id
|
||||
from app.core.config import settings
|
||||
union_id = lookup_union_id_by_open_id(open_id)
|
||||
save_open_id(db, app_id=settings.FEISHU_APP_ID or "", open_id=open_id, user_id=current_user.id, union_id=union_id)
|
||||
clear_pending_open_ids()
|
||||
|
||||
logger.info("飞书事件绑定成功: user=%s open_id=%s", current_user.id, open_id)
|
||||
|
||||
@@ -18,5 +18,6 @@ from app.models.agent_learning_pattern import AgentLearningPattern
|
||||
from app.models.agent_schedule import AgentSchedule
|
||||
from app.models.knowledge_base import KnowledgeBase, Document, DocumentChunk
|
||||
from app.models.notification import Notification
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk", "Notification"]
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk", "Notification", "UserFeishuOpenId"]
|
||||
32
backend/app/models/user_feishu_open_id.py
Normal file
32
backend/app/models/user_feishu_open_id.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""用户飞书多应用 open_id 映射表 — 每个飞书应用给同一用户分配不同的 open_id"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, func, UniqueConstraint, Index
|
||||
from sqlalchemy.dialects.mysql import CHAR
|
||||
from app.core.database import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class UserFeishuOpenId(Base):
|
||||
"""用户在各飞书应用下的 open_id 映射。
|
||||
|
||||
飞书的 open_id 按应用隔离:同一个用户在苹果/灵犀/橙子/苏瑶/甜甜
|
||||
各自拥有不同的 open_id。union_id 在同一租户下跨应用相同,用于跨应用识别用户。
|
||||
"""
|
||||
__tablename__ = "user_feishu_open_ids"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="主键")
|
||||
user_id = Column(CHAR(36), nullable=True, index=True, comment="平台用户 ID(首次捕获时可能为空)")
|
||||
app_id = Column(String(64), nullable=False, index=True, comment="飞书应用 app_id")
|
||||
open_id = Column(String(64), nullable=False, comment="该应用下的用户 open_id")
|
||||
union_id = Column(String(64), nullable=True, index=True, comment="飞书 union_id,跨应用唯一,用于关联同一用户")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("app_id", "open_id", name="uq_app_open_id"),
|
||||
Index("ix_user_app", "user_id", "app_id"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserFeishuOpenId(user={self.user_id}, app={self.app_id})>"
|
||||
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def compute_next_run(cron_expression: str, after: Optional[datetime] = None, tz: str = "UTC") -> datetime:
|
||||
def compute_next_run(cron_expression: str, after: Optional[datetime] = None, tz: str = "Asia/Shanghai") -> datetime:
|
||||
"""根据 cron 表达式计算下一次执行时间。
|
||||
|
||||
Args:
|
||||
@@ -65,11 +65,16 @@ def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
||||
logger.warning("定时任务 %s 关联的 Agent %s 不存在", schedule.id, schedule.agent_id)
|
||||
return None
|
||||
|
||||
# 创建执行记录(关联 schedule_id)
|
||||
# 创建执行记录(关联 schedule_id),标记为定时任务提醒
|
||||
execution = Execution(
|
||||
agent_id=schedule.agent_id,
|
||||
schedule_id=schedule.id,
|
||||
input_data={"message": schedule.input_message},
|
||||
input_data={
|
||||
"USER_INPUT": f"[定时任务提醒] {schedule.input_message}",
|
||||
"query": f"[定时任务提醒] {schedule.input_message}",
|
||||
"message": schedule.input_message,
|
||||
"is_scheduled_reminder": True,
|
||||
},
|
||||
status="pending",
|
||||
)
|
||||
db.add(execution)
|
||||
@@ -83,14 +88,24 @@ def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
||||
str(execution.id),
|
||||
f"agent_{schedule.agent_id}",
|
||||
agent.workflow_config,
|
||||
{"message": schedule.input_message},
|
||||
{
|
||||
"USER_INPUT": f"[定时任务提醒] {schedule.input_message}",
|
||||
"query": f"[定时任务提醒] {schedule.input_message}",
|
||||
"message": schedule.input_message,
|
||||
"is_scheduled_reminder": True,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# 无工作流配置:走简单 Agent 异步执行(传入已创建的 execution_id)
|
||||
from app.tasks.agent_tasks import execute_agent_task
|
||||
task = execute_agent_task.delay(
|
||||
str(schedule.agent_id),
|
||||
{"message": schedule.input_message},
|
||||
{
|
||||
"USER_INPUT": f"[定时任务提醒] {schedule.input_message}",
|
||||
"query": f"[定时任务提醒] {schedule.input_message}",
|
||||
"message": schedule.input_message,
|
||||
"is_scheduled_reminder": True,
|
||||
},
|
||||
execution_id=str(execution.id),
|
||||
)
|
||||
execution.task_id = task.id
|
||||
@@ -186,7 +201,21 @@ def notify_schedule_result(db: Session, execution, status: str, error_message: O
|
||||
|
||||
if status == "completed":
|
||||
title = f"定时任务「{schedule.name}」执行成功"
|
||||
content = f"Agent 已按计划执行完成。"
|
||||
# 优先使用 Agent 执行结果,其次使用定时任务的提醒内容
|
||||
result_text = ""
|
||||
if execution.output_data:
|
||||
if isinstance(execution.output_data, dict):
|
||||
result_text = (
|
||||
execution.output_data.get("result")
|
||||
or execution.output_data.get("output")
|
||||
or execution.output_data.get("text")
|
||||
or ""
|
||||
)
|
||||
elif isinstance(execution.output_data, str):
|
||||
result_text = execution.output_data
|
||||
if not result_text:
|
||||
result_text = schedule.input_message or ""
|
||||
content = result_text if result_text else "Agent 已按计划执行完成。"
|
||||
else:
|
||||
title = f"定时任务「{schedule.name}」执行失败"
|
||||
content = f"错误信息: {error_message or '未知错误'}"
|
||||
@@ -225,13 +254,13 @@ def notify_schedule_result(db: Session, execution, status: str, error_message: O
|
||||
except Exception as e:
|
||||
logger.warning("飞书 webhook 通知发送失败: %s", e)
|
||||
|
||||
# 如果用户绑定了飞书账号,通过飞书应用发送通知
|
||||
# 如果用户绑定了飞书账号,通过对应的飞书应用发送通知
|
||||
try:
|
||||
from app.models.user import User
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
from app.services.feishu_open_id_service import get_app_id_for_agent, get_open_id_for_app
|
||||
|
||||
schedule_user = db.query(User).filter(User.id == schedule.user_id).first()
|
||||
if schedule_user and schedule_user.feishu_open_id:
|
||||
if schedule_user:
|
||||
detail_link = None
|
||||
try:
|
||||
from app.core.config import settings
|
||||
@@ -240,13 +269,46 @@ def notify_schedule_result(db: Session, execution, status: str, error_message: O
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
send_message_to_user(
|
||||
open_id=schedule_user.feishu_open_id,
|
||||
title=title,
|
||||
content=content,
|
||||
status=status,
|
||||
detail_link=detail_link,
|
||||
)
|
||||
agent_id = str(execution.agent_id) if execution.agent_id else ""
|
||||
app_id = get_app_id_for_agent(agent_id)
|
||||
per_app_open_id = get_open_id_for_app(db, schedule.user_id, app_id) if app_id else None
|
||||
send_ok = False
|
||||
|
||||
# 如果找到了该用户在此应用下的专属 open_id,直接用对应应用发送
|
||||
if per_app_open_id and app_id:
|
||||
if app_id == (settings.LINGXI_APP_ID or ""):
|
||||
try:
|
||||
from app.services.lingxi_app_service import send_message_to_user as send_msg
|
||||
send_msg(open_id=per_app_open_id, title=title, content=content, status=status, detail_link=detail_link)
|
||||
send_ok = True
|
||||
except Exception:
|
||||
logger.info("灵犀发送失败,fallback 到主飞书应用")
|
||||
elif app_id == (settings.ORANGE_APP_ID or ""):
|
||||
try:
|
||||
from app.services.orange_app_service import send_message_to_user as send_msg
|
||||
send_msg(open_id=per_app_open_id, title=title, content=content, status=status, detail_link=detail_link)
|
||||
send_ok = True
|
||||
except Exception:
|
||||
logger.info("橙子发送失败,fallback 到主飞书应用")
|
||||
elif app_id == (settings.SUYAO_APP_ID or ""):
|
||||
try:
|
||||
from app.services.suyao_app_service import send_message_to_user as send_msg
|
||||
send_msg(open_id=per_app_open_id, title=title, content=content, status=status, detail_link=detail_link)
|
||||
send_ok = True
|
||||
except Exception:
|
||||
logger.info("苏瑶发送失败,fallback 到主飞书应用")
|
||||
elif app_id == (settings.TIANTIAN_APP_ID or ""):
|
||||
try:
|
||||
from app.services.tiantian_app_service import send_message_to_user as send_msg
|
||||
send_msg(open_id=per_app_open_id, title=title, content=content, status=status, detail_link=detail_link)
|
||||
send_ok = True
|
||||
except Exception:
|
||||
logger.info("甜甜发送失败,fallback 到主飞书应用")
|
||||
|
||||
# Fallback: 使用主飞书应用(苹果)的 open_id 发送
|
||||
if not send_ok and schedule_user.feishu_open_id:
|
||||
from app.services.feishu_app_service import send_message_to_user as send_msg
|
||||
send_msg(open_id=schedule_user.feishu_open_id, title=title, content=content, status=status, detail_link=detail_link)
|
||||
except Exception as e:
|
||||
logger.warning("飞书应用通知发送失败: %s", e)
|
||||
except Exception as e:
|
||||
|
||||
@@ -194,6 +194,44 @@ def lookup_user_by_email(email: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def lookup_union_id_by_open_id(open_id: str) -> Optional[str]:
|
||||
"""通过 open_id 查询飞书用户的 union_id(跨应用唯一标识)。
|
||||
|
||||
同一个用户的 union_id 在所有应用下相同,可用于跨应用关联。
|
||||
|
||||
Args:
|
||||
open_id: 飞书用户的 open_id(必须在当前苹果应用下有权限访问)
|
||||
|
||||
Returns:
|
||||
union_id 字符串,未找到返回 None
|
||||
"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.get(
|
||||
f"https://open.feishu.cn/open-apis/contact/v3/users/{open_id}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
user_data = result.get("data", {}).get("user", {})
|
||||
union_id = user_data.get("union_id")
|
||||
if union_id:
|
||||
logger.info("飞书 union_id 查询成功: open_id=%s union_id=%s", open_id[:20], union_id)
|
||||
return union_id
|
||||
logger.info("飞书用户未找到 union_id: open_id=%s", open_id[:20])
|
||||
return None
|
||||
else:
|
||||
logger.warning("飞书 union_id 查询失败: code=%s msg=%s", result.get("code"), result.get("msg"))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("飞书 union_id 查询异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_verification_token() -> str:
|
||||
"""获取飞书应用的 Verification Token(用于验证事件回调)。"""
|
||||
return settings.FEISHU_VERIFICATION_TOKEN
|
||||
|
||||
211
backend/app/services/feishu_open_id_service.py
Normal file
211
backend/app/services/feishu_open_id_service.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""飞书多应用 open_id 管理 — 存储/查询各应用下用户的 open_id,union_id 跨应用关联"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_app_id_for_agent(agent_id: str) -> str:
|
||||
"""根据 agent_id 返回对应的飞书应用 app_id。默认返回主飞书应用(苹果)。"""
|
||||
if settings.LINGXI_AGENT_ID and agent_id == settings.LINGXI_AGENT_ID:
|
||||
return settings.LINGXI_APP_ID or ""
|
||||
if settings.ORANGE_AGENT_ID and agent_id == settings.ORANGE_AGENT_ID:
|
||||
return settings.ORANGE_APP_ID or ""
|
||||
if settings.SUYAO_AGENT_ID and agent_id == settings.SUYAO_AGENT_ID:
|
||||
return settings.SUYAO_APP_ID or ""
|
||||
if settings.TIANTIAN_AGENT_ID and agent_id == settings.TIANTIAN_AGENT_ID:
|
||||
return settings.TIANTIAN_APP_ID or ""
|
||||
return settings.FEISHU_APP_ID or ""
|
||||
|
||||
|
||||
def save_open_id(
|
||||
db: Session,
|
||||
app_id: str,
|
||||
open_id: str,
|
||||
union_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""保存或更新 (app_id, open_id) 记录。
|
||||
|
||||
如果已有同 app+open_id 的记录,更新 union_id 和 user_id;
|
||||
否则新建记录。user_id 可以为空(首次捕获时可能未知)。
|
||||
"""
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
|
||||
try:
|
||||
existing = (
|
||||
db.query(UserFeishuOpenId)
|
||||
.filter(
|
||||
UserFeishuOpenId.app_id == app_id,
|
||||
UserFeishuOpenId.open_id == open_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
changed = False
|
||||
if union_id and existing.union_id != union_id:
|
||||
existing.union_id = union_id
|
||||
changed = True
|
||||
if user_id and existing.user_id != user_id:
|
||||
existing.user_id = user_id
|
||||
changed = True
|
||||
if changed:
|
||||
db.commit()
|
||||
else:
|
||||
record = UserFeishuOpenId(
|
||||
user_id=user_id,
|
||||
app_id=app_id,
|
||||
open_id=open_id,
|
||||
union_id=union_id,
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning("保存飞书 open_id 失败: %s", e)
|
||||
db.rollback()
|
||||
|
||||
|
||||
def find_user_id_by_open_id(db: Session, app_id: str, open_id: str) -> Optional[str]:
|
||||
"""通过 (app_id, open_id) 查找已关联的 user_id。"""
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
|
||||
try:
|
||||
record = (
|
||||
db.query(UserFeishuOpenId)
|
||||
.filter(
|
||||
UserFeishuOpenId.app_id == app_id,
|
||||
UserFeishuOpenId.open_id == open_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return record.user_id if record else None
|
||||
except Exception as e:
|
||||
logger.warning("查询 user_id 失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def find_user_id_by_union_id(db: Session, union_id: str) -> Optional[str]:
|
||||
"""通过 union_id 查找已关联的 user_id(跨应用识别)。"""
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
|
||||
try:
|
||||
record = (
|
||||
db.query(UserFeishuOpenId)
|
||||
.filter(
|
||||
UserFeishuOpenId.union_id == union_id,
|
||||
UserFeishuOpenId.user_id.isnot(None),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return record.user_id if record else None
|
||||
except Exception as e:
|
||||
logger.warning("通过 union_id 查询 user_id 失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_open_id_for_app(db: Session, user_id: str, app_id: str) -> Optional[str]:
|
||||
"""查询用户在某飞书应用下的 open_id。"""
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
|
||||
try:
|
||||
record = (
|
||||
db.query(UserFeishuOpenId)
|
||||
.filter(
|
||||
UserFeishuOpenId.user_id == user_id,
|
||||
UserFeishuOpenId.app_id == app_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
return record.open_id if record else None
|
||||
except Exception as e:
|
||||
logger.warning("查询飞书 open_id 失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def link_open_id_to_user(
|
||||
db: Session,
|
||||
app_id: str,
|
||||
open_id: str,
|
||||
user_id: str,
|
||||
union_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""将 (app_id, open_id) 关联到平台用户。已有记录则更新,否则创建。"""
|
||||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id, user_id=user_id)
|
||||
|
||||
|
||||
def resolve_user_and_save(
|
||||
db: Session,
|
||||
app_id: str,
|
||||
open_id: str,
|
||||
union_id: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""WS 消息处理入口:自动解析用户身份并保存 open_id。
|
||||
|
||||
查找策略(按优先级):
|
||||
1. 已有 (app_id, open_id) 记录中的 user_id
|
||||
2. 通过 union_id 在 user_feishu_open_ids 中查找
|
||||
3. 通过 open_id 匹配 User.feishu_open_id(苹果旧数据兼容)
|
||||
4. 通过 union_id 匹配 User.feishu_union_id
|
||||
|
||||
找到用户后自动 link,否则只保存 (app_id, open_id, union_id)。
|
||||
|
||||
Returns:
|
||||
找到的 user_id,未找到返回 None
|
||||
"""
|
||||
from app.models.user import User
|
||||
|
||||
# 1. 已有记录
|
||||
existing_user_id = find_user_id_by_open_id(db, app_id, open_id)
|
||||
if existing_user_id:
|
||||
# 补充 union_id
|
||||
if union_id:
|
||||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id, user_id=existing_user_id)
|
||||
return existing_user_id
|
||||
|
||||
# 2. 通过 union_id 查找
|
||||
if union_id:
|
||||
user_id_by_union = find_user_id_by_union_id(db, union_id)
|
||||
if user_id_by_union:
|
||||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=user_id_by_union, union_id=union_id)
|
||||
return user_id_by_union
|
||||
|
||||
# 3. 兼容旧 User.feishu_open_id(苹果的 open_id)
|
||||
user = db.query(User).filter(User.feishu_open_id == open_id).first()
|
||||
if user:
|
||||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=user.id, union_id=union_id)
|
||||
return user.id
|
||||
|
||||
# 4. 有 union_id 但前面都没匹配到:尝试通过飞书 API 查找苹果 open_id 的 union_id
|
||||
if union_id:
|
||||
from app.services.feishu_app_service import lookup_union_id_by_open_id
|
||||
|
||||
users_with_apple = db.query(User).filter(User.feishu_open_id.isnot(None)).all()
|
||||
for u in users_with_apple:
|
||||
apple_union_id = lookup_union_id_by_open_id(u.feishu_open_id)
|
||||
if apple_union_id and apple_union_id == union_id:
|
||||
# 回填苹果记录的 union_id
|
||||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||||
apple_record = (
|
||||
db.query(UserFeishuOpenId)
|
||||
.filter(
|
||||
UserFeishuOpenId.user_id == u.id,
|
||||
UserFeishuOpenId.app_id == (settings.FEISHU_APP_ID or ""),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if apple_record:
|
||||
apple_record.union_id = union_id
|
||||
db.commit()
|
||||
# 关联当前记录
|
||||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=u.id, union_id=union_id)
|
||||
return u.id
|
||||
|
||||
# 5. 未找到,只保存记录(user_id 为空)
|
||||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id)
|
||||
return None
|
||||
@@ -70,6 +70,21 @@ def _get_sender_open_id(data) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
"""从 Feishu 消息事件中提取发送者 union_id(跨应用唯一)。"""
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
"""获取聊天类型。"""
|
||||
try:
|
||||
@@ -103,6 +118,7 @@ def _reply_card(open_id: str, title: str, content: str, status: str = "info"):
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理飞书消息。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
@@ -128,11 +144,21 @@ async def _handle_message_async(data):
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.user import User
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.FEISHU_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.feishu_open_id == open_id).first()
|
||||
if not user and resolved_uid:
|
||||
user = db.query(User).filter(User.id == resolved_uid).first()
|
||||
if not user:
|
||||
_reply_to_feishu(open_id, "你的账号未绑定平台用户,请先在平台绑定飞书。")
|
||||
return
|
||||
|
||||
@@ -58,6 +58,20 @@ def _get_sender_open_id(data) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
try:
|
||||
ev = data.event
|
||||
@@ -110,6 +124,7 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str]
|
||||
|
||||
async def _handle_message_async(data):
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
@@ -124,11 +139,18 @@ async def _handle_message_async(data):
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.LINGXI_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
agent_id = settings.LINGXI_AGENT_ID
|
||||
if not agent_id:
|
||||
_reply_to_feishu(open_id, "灵犀尚未配置,请联系管理员。")
|
||||
@@ -183,7 +205,7 @@ async def _handle_message_async(data):
|
||||
vector_memory_enabled=bool(cfg.get("memory_vector_enabled", True)),
|
||||
learning_enabled=bool(cfg.get("memory_learning", True)),
|
||||
),
|
||||
user_id=None,
|
||||
user_id=resolved_uid,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +66,21 @@ def _get_sender_open_id(data) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
"""从消息事件中提取发送者 union_id(跨应用唯一)。"""
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
"""获取聊天类型。"""
|
||||
try:
|
||||
@@ -125,6 +140,7 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str]
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理橙子消息 — 固定使用橙子助手 Agent。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
@@ -139,11 +155,18 @@ async def _handle_message_async(data):
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.ORANGE_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
# 固定使用橙子助手 Agent
|
||||
agent_id = settings.ORANGE_AGENT_ID
|
||||
if not agent_id:
|
||||
@@ -199,7 +222,7 @@ async def _handle_message_async(data):
|
||||
vector_memory_enabled=bool(cfg.get("memory_vector_enabled", True)),
|
||||
learning_enabled=bool(cfg.get("memory_learning", True)),
|
||||
),
|
||||
user_id=None,
|
||||
user_id=resolved_uid,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +66,21 @@ def _get_sender_open_id(data) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
"""从消息事件中提取发送者 union_id(跨应用唯一)。"""
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
"""获取聊天类型。"""
|
||||
try:
|
||||
@@ -125,6 +140,7 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str]
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理苏瑶消息 — 固定使用苏瑶 Agent。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
@@ -139,11 +155,18 @@ async def _handle_message_async(data):
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.SUYAO_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
# 固定使用苏瑶 Agent
|
||||
agent_id = settings.SUYAO_AGENT_ID
|
||||
if not agent_id:
|
||||
@@ -199,7 +222,7 @@ async def _handle_message_async(data):
|
||||
vector_memory_enabled=bool(cfg.get("memory_vector_enabled", True)),
|
||||
learning_enabled=bool(cfg.get("memory_learning", True)),
|
||||
),
|
||||
user_id=None,
|
||||
user_id=resolved_uid,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -58,6 +58,20 @@ def _get_sender_open_id(data) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_union_id(data) -> Optional[str]:
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "union_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
try:
|
||||
ev = data.event
|
||||
@@ -110,6 +124,7 @@ def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str]
|
||||
|
||||
async def _handle_message_async(data):
|
||||
open_id = _get_sender_open_id(data)
|
||||
union_id = _get_sender_union_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
@@ -124,11 +139,18 @@ async def _handle_message_async(data):
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_open_id_service import resolve_user_and_save
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 自动保存/关联此应用的 open_id(跨应用识别)
|
||||
resolved_uid = resolve_user_and_save(
|
||||
db, app_id=settings.TIANTIAN_APP_ID or "",
|
||||
open_id=open_id, union_id=union_id,
|
||||
)
|
||||
|
||||
agent_id = settings.TIANTIAN_AGENT_ID
|
||||
if not agent_id:
|
||||
_reply_to_feishu(open_id, "甜甜尚未配置,请联系管理员。")
|
||||
@@ -181,7 +203,7 @@ async def _handle_message_async(data):
|
||||
vector_memory_enabled=bool(cfg.get("memory_vector_enabled", True)),
|
||||
learning_enabled=bool(cfg.get("memory_learning", True)),
|
||||
),
|
||||
user_id=None,
|
||||
user_id=resolved_uid,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user