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:
renjianbo
2026-05-06 00:36:40 +08:00
parent 3dd098482e
commit f33bc461ff
11 changed files with 503 additions and 24 deletions

View File

@@ -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)

View File

@@ -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"]

View 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})>"

View File

@@ -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:

View File

@@ -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

View File

@@ -0,0 +1,211 @@
"""飞书多应用 open_id 管理 — 存储/查询各应用下用户的 open_idunion_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

View File

@@ -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

View File

@@ -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),
)

View File

@@ -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),
)

View File

@@ -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),
)

View File

@@ -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),
)