ForcePilot/backend/package/yuxi/channels/session_mapper.py
Kris ede29b1809 refactor(channel): 完成频道模块大重构与功能扩展
本次提交对频道模块进行了全面重构并新增多项核心功能:
1.  优化适配器状态获取逻辑,修复状态返回空值问题
2.  新增4种频道异常类型,完善错误处理体系
3.  大幅精简Mixin类,移除冗余的抽象方法定义
4.  重构适配器注册系统,统一注册入口并新增内置适配器加载方法
5.  扩展插件系统,新增更多元数据配置项支持
6.  新增线程类型、会话范围等模型定义,扩展事件类型枚举
7.  优化用户映射逻辑,使用PostgreSQL upsert避免重复创建
8.  新增历史消息注入模块,支持多格式历史格式化与缓存管理
9.  新增线程能力配置与各平台预置适配配置
10. 新增线程绑定管理器,支持多类型线程绑定生命周期管理
11. 重构__init__.py,整理导出模块与类型
12. 扩展基础适配器类,新增凭证解析、状态存储等核心方法
13. 重写消息路由器,支持按频道加载策略、安全校验与多命令处理
14. 新增/history、/context、/summary等交互命令实现
15. 优化消息记录与统计逻辑,完善路由调度链路
2026-05-13 16:41:11 +08:00

142 lines
5.3 KiB
Python

from __future__ import annotations
import uuid as uuid_lib
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.channels.models import ChannelMessage
from yuxi.storage.postgres.models_business import User
from yuxi.storage.postgres.models_channels import ChannelThreadMapping, ChannelUserMapping
from yuxi.utils.datetime_utils import utc_now_naive
from yuxi.utils.logging_config import logger
VIRTUAL_DEPARTMENT_ID = -1
USER_SOURCE_PREFIX = "channel:"
class SessionMapper:
def __init__(self, db: AsyncSession, department_id: int = VIRTUAL_DEPARTMENT_ID):
self.db = db
self.department_id = department_id
async def resolve_user(self, message: ChannelMessage) -> str:
identity = message.identity
mapping = await self._get_user_mapping(identity.channel_id, identity.channel_user_id)
if mapping:
return mapping.internal_user_id
internal_user_id = f"ch_{identity.channel_id}_{uuid_lib.uuid4().hex[:8]}"
username = f"{identity.channel_id}_{identity.channel_user_id}"
source = f"{USER_SOURCE_PREFIX}{identity.channel_id}"
stmt = (
pg_insert(User)
.values(
username=username,
user_id=internal_user_id,
password_hash="",
role="user",
department_id=self.department_id,
source=source,
)
.on_conflict_do_update(
index_elements=["username"],
set_={"username": username},
)
.returning(User.user_id)
)
result = await self.db.execute(stmt)
actual_user_id = result.scalar_one()
try:
mapping = ChannelUserMapping(
channel_id=identity.channel_id,
channel_user_id=identity.channel_user_id,
internal_user_id=actual_user_id,
)
self.db.add(mapping)
await self.db.commit()
logger.info(
f"Created channel user mapping: {identity.channel_id}/{identity.channel_user_id} -> {actual_user_id}"
)
return actual_user_id
except IntegrityError:
await self.db.rollback()
mapping = await self._get_user_mapping(identity.channel_id, identity.channel_user_id)
if mapping:
return mapping.internal_user_id
raise
async def resolve_thread(self, message: ChannelMessage, internal_user_id: str) -> str:
identity = message.identity
mapping = await self._get_thread_mapping(identity.channel_id, identity.channel_chat_id, internal_user_id)
if mapping:
mapping.last_active_at = utc_now_naive()
await self.db.flush()
return mapping.thread_id
thread_id = str(uuid_lib.uuid4())
try:
mapping = ChannelThreadMapping(
channel_id=identity.channel_id,
channel_chat_id=identity.channel_chat_id,
internal_user_id=internal_user_id,
thread_id=thread_id,
last_active_at=utc_now_naive(),
)
self.db.add(mapping)
await self.db.commit()
logger.info(
f"Created channel thread mapping: "
f"{identity.channel_id}/{identity.channel_chat_id}/{internal_user_id} -> {thread_id}"
)
return thread_id
except IntegrityError:
await self.db.rollback()
mapping = await self._get_thread_mapping(identity.channel_id, identity.channel_chat_id, internal_user_id)
if mapping:
return mapping.thread_id
raise
async def reset_thread(self, message: ChannelMessage, internal_user_id: str) -> str:
identity = message.identity
mapping = await self._get_thread_mapping(identity.channel_id, identity.channel_chat_id, internal_user_id)
if mapping:
new_thread_id = str(uuid_lib.uuid4())
mapping.thread_id = new_thread_id
mapping.last_active_at = utc_now_naive()
await self.db.commit()
logger.info(f"Reset thread: {identity.channel_id}/{identity.channel_chat_id} -> {new_thread_id}")
return new_thread_id
return await self.resolve_thread(message, internal_user_id)
async def _get_user_mapping(self, channel_id: str, channel_user_id: str) -> ChannelUserMapping | None:
result = await self.db.execute(
select(ChannelUserMapping).where(
ChannelUserMapping.channel_id == channel_id,
ChannelUserMapping.channel_user_id == channel_user_id,
)
)
return result.scalar_one_or_none()
async def _get_thread_mapping(
self, channel_id: str, channel_chat_id: str, internal_user_id: str
) -> ChannelThreadMapping | None:
result = await self.db.execute(
select(ChannelThreadMapping).where(
ChannelThreadMapping.channel_id == channel_id,
ChannelThreadMapping.channel_chat_id == channel_chat_id,
ChannelThreadMapping.internal_user_id == internal_user_id,
)
)
return result.scalar_one_or_none()