diff --git a/backend/app/api/feishu_bind.py b/backend/app/api/feishu_bind.py index d538678..1fc4ee7 100644 --- a/backend/app/api/feishu_bind.py +++ b/backend/app/api/feishu_bind.py @@ -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) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index ced1a6b..95ae907 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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"] \ No newline at end of file +__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"] \ No newline at end of file diff --git a/backend/app/models/user_feishu_open_id.py b/backend/app/models/user_feishu_open_id.py new file mode 100644 index 0000000..692e327 --- /dev/null +++ b/backend/app/models/user_feishu_open_id.py @@ -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"" diff --git a/backend/app/services/agent_schedule_service.py b/backend/app/services/agent_schedule_service.py index 3b55847..acc6def 100644 --- a/backend/app/services/agent_schedule_service.py +++ b/backend/app/services/agent_schedule_service.py @@ -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: diff --git a/backend/app/services/feishu_app_service.py b/backend/app/services/feishu_app_service.py index 7541e20..aa2c275 100644 --- a/backend/app/services/feishu_app_service.py +++ b/backend/app/services/feishu_app_service.py @@ -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 diff --git a/backend/app/services/feishu_open_id_service.py b/backend/app/services/feishu_open_id_service.py new file mode 100644 index 0000000..e25e4a0 --- /dev/null +++ b/backend/app/services/feishu_open_id_service.py @@ -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 diff --git a/backend/app/services/feishu_ws_handler.py b/backend/app/services/feishu_ws_handler.py index 3cecc1e..b52f9ed 100644 --- a/backend/app/services/feishu_ws_handler.py +++ b/backend/app/services/feishu_ws_handler.py @@ -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 diff --git a/backend/app/services/lingxi_ws_handler.py b/backend/app/services/lingxi_ws_handler.py index f81561e..aa286cd 100644 --- a/backend/app/services/lingxi_ws_handler.py +++ b/backend/app/services/lingxi_ws_handler.py @@ -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), ) diff --git a/backend/app/services/orange_ws_handler.py b/backend/app/services/orange_ws_handler.py index f569de0..cbde6ce 100644 --- a/backend/app/services/orange_ws_handler.py +++ b/backend/app/services/orange_ws_handler.py @@ -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), ) diff --git a/backend/app/services/suyao_ws_handler.py b/backend/app/services/suyao_ws_handler.py index 6757b91..5fc4284 100644 --- a/backend/app/services/suyao_ws_handler.py +++ b/backend/app/services/suyao_ws_handler.py @@ -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), ) diff --git a/backend/app/services/tiantian_ws_handler.py b/backend/app/services/tiantian_ws_handler.py index 776c58b..e3211db 100644 --- a/backend/app/services/tiantian_ws_handler.py +++ b/backend/app/services/tiantian_ws_handler.py @@ -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), )