From ead8a2ba5feff3c5c6d6daf26ec26666b8a6fe17 Mon Sep 17 00:00:00 2001 From: fluchw <2427607997@qq.com> Date: Thu, 5 Feb 2026 17:14:51 +0800 Subject: [PATCH 1/2] feat(wecom): add user nickname/avatar display and fix timezone issue WeChat KF improvements: - Fix KeyError when sync_msg response lacks open_kfid field - Fetch customer nickname and avatar via batchget_customer API Conversation history enhancements: - Add user_name and avatar fields to database schema - Display user name and avatar in WebUI conversation list - Auto-migrate existing databases with new columns Bug fixes: - Fix 8-hour timestamp offset caused by SQLite losing timezone info --- astrbot/core/agent/message.py | 5 ++ astrbot/core/astr_main_agent.py | 22 ++++++++- astrbot/core/conversation_mgr.py | 20 +++++++- astrbot/core/db/__init__.py | 4 ++ astrbot/core/db/po.py | 7 +++ astrbot/core/db/sqlite.py | 46 ++++++++++++++++++- astrbot/core/platform/astr_message_event.py | 6 +++ astrbot/core/platform/astrbot_message.py | 2 + .../platform/sources/wecom/wecom_adapter.py | 25 ++++++++-- astrbot/dashboard/routes/conversation.py | 6 ++- .../locales/en-US/features/conversation.json | 2 + .../locales/zh-CN/features/conversation.json | 2 + dashboard/src/views/ConversationPage.vue | 13 ++++++ 13 files changed, 151 insertions(+), 9 deletions(-) diff --git a/astrbot/core/agent/message.py b/astrbot/core/agent/message.py index 582b1eef2..99faf0d13 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -172,6 +172,9 @@ class Message(BaseModel): content: str | list[ContentPart] | None = None """The content of the message.""" + name: str | None = None + """Optional name of the sender, used to identify different users in conversation.""" + tool_calls: list[ToolCall] | list[dict] | None = None """The tool calls of the message.""" @@ -198,6 +201,8 @@ def serialize(self, handler): data.pop("tool_calls", None) if self.tool_call_id is None: data.pop("tool_call_id", None) + if self.name is None: + data.pop("name", None) return data diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 690a6404c..6d42db00c 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -145,15 +145,33 @@ async def _get_session_conv( ) -> Conversation: conv_mgr = plugin_context.conversation_manager umo = event.unified_msg_origin + user_name = event.get_sender_name() + avatar = event.get_sender_avatar() cid = await conv_mgr.get_curr_conversation_id(umo) if not cid: - cid = await conv_mgr.new_conversation(umo, event.get_platform_id()) + cid = await conv_mgr.new_conversation(umo, event.get_platform_id(), user_name=user_name, avatar=avatar) conversation = await conv_mgr.get_conversation(umo, cid) if not conversation: - cid = await conv_mgr.new_conversation(umo, event.get_platform_id()) + cid = await conv_mgr.new_conversation(umo, event.get_platform_id(), user_name=user_name, avatar=avatar) conversation = await conv_mgr.get_conversation(umo, cid) if not conversation: raise RuntimeError("无法创建新的对话。") + # 如果已有对话但 user_name 或 avatar 为空,更新它们 + need_update = False + if conversation.user_name is None and user_name: + need_update = True + if conversation.avatar is None and avatar: + need_update = True + if need_update: + await conv_mgr.db.update_conversation( + cid, + user_name=user_name if conversation.user_name is None else None, + avatar=avatar if conversation.avatar is None else None, + ) + if conversation.user_name is None and user_name: + conversation.user_name = user_name + if conversation.avatar is None and avatar: + conversation.avatar = avatar return conversation diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index a0a0c0e2f..150342e33 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -6,6 +6,7 @@ import json from collections.abc import Awaitable, Callable +from datetime import timezone from astrbot.core import sp from astrbot.core.agent.message import AssistantMessageSegment, UserMessageSegment @@ -58,8 +59,15 @@ async def _trigger_session_deleted(self, unified_msg_origin: str) -> None: def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation: """将 ConversationV2 对象转换为 Conversation 对象""" - created_at = int(conv_v2.created_at.timestamp()) - updated_at = int(conv_v2.updated_at.timestamp()) + # SQLite 读回的 datetime 可能丢失时区信息,需要显式标记为 UTC + ca = conv_v2.created_at + if ca.tzinfo is None: + ca = ca.replace(tzinfo=timezone.utc) + ua = conv_v2.updated_at + if ua.tzinfo is None: + ua = ua.replace(tzinfo=timezone.utc) + created_at = int(ca.timestamp()) + updated_at = int(ua.timestamp()) return Conversation( platform_id=conv_v2.platform_id, user_id=conv_v2.user_id, @@ -70,6 +78,8 @@ def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation: created_at=created_at, updated_at=updated_at, token_usage=conv_v2.token_usage, + user_name=conv_v2.user_name, + avatar=conv_v2.avatar, ) async def new_conversation( @@ -79,11 +89,15 @@ async def new_conversation( content: list[dict] | None = None, title: str | None = None, persona_id: str | None = None, + user_name: str | None = None, + avatar: str | None = None, ) -> str: """新建对话,并将当前会话的对话转移到新对话. Args: unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + user_name (str | None): 用户名称 + avatar (str | None): 用户头像 URL Returns: conversation_id (str): 对话 ID, 是 uuid 格式的字符串 @@ -101,6 +115,8 @@ async def new_conversation( content=content, title=title, persona_id=persona_id, + user_name=user_name, + avatar=avatar, ) self.session_conversations[unified_msg_origin] = conv.conversation_id await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id) diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 7b67b8755..5c0dbf41f 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -145,6 +145,8 @@ async def create_conversation( cid: str | None = None, created_at: datetime.datetime | None = None, updated_at: datetime.datetime | None = None, + user_name: str | None = None, + avatar: str | None = None, ) -> ConversationV2: """Create a new conversation.""" ... @@ -157,6 +159,8 @@ async def update_conversation( persona_id: str | None = None, content: list[dict] | None = None, token_usage: int | None = None, + user_name: str | None = None, + avatar: str | None = None, ) -> None: """Update a conversation's history.""" ... diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index 81649c0d7..aac6f6c5b 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -54,6 +54,9 @@ class ConversationV2(TimestampMixin, SQLModel, table=True): ) platform_id: str = Field(nullable=False) user_id: str = Field(nullable=False) + user_name: str | None = Field(default=None, max_length=255) + avatar: str | None = Field(default=None, max_length=512) + """用户头像 URL""" content: list | None = Field(default=None, sa_type=JSON) title: str | None = Field(default=None, max_length=255) @@ -418,6 +421,10 @@ class Conversation: updated_at: int = 0 token_usage: int = 0 """对话的总 token 数量。AstrBot 会保留最近一次 LLM 请求返回的总 token 数,方便统计。token_usage 可能为 0,表示未知。""" + user_name: str | None = None + """发送消息的用户名称""" + avatar: str | None = None + """用户头像 URL""" class Personality(TypedDict): diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 153e13e8b..2fab75b85 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -57,6 +57,10 @@ async def initialize(self) -> None: # 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容) await self._ensure_persona_folder_columns(conn) await self._ensure_persona_skills_column(conn) + # 确保 conversations 表有 user_name 列(前向兼容) + await self._ensure_conversation_user_name_column(conn) + # 确保 conversations 表有 avatar 列(前向兼容) + await self._ensure_conversation_avatar_column(conn) await conn.commit() async def _ensure_persona_folder_columns(self, conn) -> None: @@ -91,6 +95,38 @@ async def _ensure_persona_skills_column(self, conn) -> None: if "skills" not in columns: await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON")) + async def _ensure_conversation_user_name_column(self, conn) -> None: + """确保 conversations 表有 user_name 列。 + + 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel + 的 metadata.create_all 自动创建这些列。 + """ + result = await conn.execute(text("PRAGMA table_info(conversations)")) + columns = {row[1] for row in result.fetchall()} + + if "user_name" not in columns: + await conn.execute( + text( + "ALTER TABLE conversations ADD COLUMN user_name VARCHAR(255) DEFAULT NULL" + ) + ) + + async def _ensure_conversation_avatar_column(self, conn) -> None: + """确保 conversations 表有 avatar 列。 + + 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel + 的 metadata.create_all 自动创建这些列。 + """ + result = await conn.execute(text("PRAGMA table_info(conversations)")) + columns = {row[1] for row in result.fetchall()} + + if "avatar" not in columns: + await conn.execute( + text( + "ALTER TABLE conversations ADD COLUMN avatar VARCHAR(512) DEFAULT NULL" + ) + ) + # ==== # Platform Statistics # ==== @@ -259,6 +295,8 @@ async def create_conversation( cid=None, created_at=None, updated_at=None, + user_name=None, + avatar=None, ): kwargs = {} if cid: @@ -276,13 +314,15 @@ async def create_conversation( platform_id=platform_id, title=title, persona_id=persona_id, + user_name=user_name, + avatar=avatar, **kwargs, ) session.add(new_conversation) return new_conversation async def update_conversation( - self, cid, title=None, persona_id=None, content=None, token_usage=None + self, cid, title=None, persona_id=None, content=None, token_usage=None, user_name=None, avatar=None ): async with self.get_db() as session: session: AsyncSession @@ -299,6 +339,10 @@ async def update_conversation( values["content"] = content if token_usage is not None: values["token_usage"] = token_usage + if user_name is not None: + values["user_name"] = user_name + if avatar is not None: + values["avatar"] = avatar if not values: return None query = query.values(**values) diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index b99a5778b..95efdf63e 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -191,6 +191,12 @@ def get_sender_name(self) -> str: return self.message_obj.sender.nickname return "" + def get_sender_avatar(self) -> str | None: + """获取消息发送者的头像 URL。(可能会返回 None)""" + if hasattr(self.message_obj.sender, 'avatar'): + return self.message_obj.sender.avatar + return None + def set_extra(self, key, value): """设置额外的信息。""" self._extras[key] = value diff --git a/astrbot/core/platform/astrbot_message.py b/astrbot/core/platform/astrbot_message.py index 253963322..c8de70db2 100644 --- a/astrbot/core/platform/astrbot_message.py +++ b/astrbot/core/platform/astrbot_message.py @@ -10,6 +10,8 @@ class MessageMember: user_id: str # 发送者id nickname: str | None = None + avatar: str | None = None + """用户头像 URL""" def __str__(self): # 使用 f-string 来构建返回的字符串表示形式 diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index adc24578f..5013493c2 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -184,10 +184,10 @@ def __init__( async def callback(msg: BaseMessage): if msg.type == "unknown" and msg._data["Event"] == "kf_msg_or_event": + token = msg._data["Token"] + kfid = msg._data["OpenKfId"] def get_latest_msg_item() -> dict | None: - token = msg._data["Token"] - kfid = msg._data["OpenKfId"] has_more = 1 ret = {} while has_more: @@ -203,6 +203,7 @@ def get_latest_msg_item() -> dict | None: get_latest_msg_item, ) if msg_new: + msg_new["open_kfid"] = kfid await self.convert_wechat_kf_message(msg_new) return await self.convert_message(msg) @@ -350,11 +351,29 @@ async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None: async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None: msgtype = msg.get("msgtype") external_userid = cast(str, msg.get("external_userid")) + + # 尝试获取客户昵称和头像 + nickname = external_userid + avatar = None + try: + customer_info = await asyncio.get_event_loop().run_in_executor( + None, + self.wechat_kf_api.batchget_customer, + external_userid, + ) + logger.info(f"获取客户信息: {customer_info}") + customer_list = customer_info.get("customer_list", []) + if customer_list: + nickname = customer_list[0].get("nickname", external_userid) + avatar = customer_list[0].get("avatar", None) + except Exception as e: + logger.debug(f"获取客户信息失败: {e}") + abm = AstrBotMessage() abm.raw_message = msg abm.raw_message["_wechat_kf_flag"] = None # 方便处理 abm.self_id = msg["open_kfid"] - abm.sender = MessageMember(external_userid, external_userid) + abm.sender = MessageMember(external_userid, nickname, avatar) abm.session_id = external_userid abm.type = MessageType.FRIEND_MESSAGE abm.message_id = msg.get("msgid", uuid.uuid4().hex[:8]) diff --git a/astrbot/dashboard/routes/conversation.py b/astrbot/dashboard/routes/conversation.py index 513d3603f..ee659fd77 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -1,5 +1,6 @@ import json import traceback +from dataclasses import asdict from datetime import datetime from io import BytesIO @@ -88,8 +89,11 @@ async def list_conversations(self): (total_count + page_size - 1) // page_size if total_count > 0 else 1 ) + # 将 Conversation dataclass 对象转换为字典 + conversations_dict = [asdict(conv) for conv in conversations] + result = { - "conversations": conversations, + "conversations": conversations_dict, "pagination": { "page": page, "page_size": page_size, diff --git a/dashboard/src/i18n/locales/en-US/features/conversation.json b/dashboard/src/i18n/locales/en-US/features/conversation.json index 3e8cb7128..b580bda0b 100644 --- a/dashboard/src/i18n/locales/en-US/features/conversation.json +++ b/dashboard/src/i18n/locales/en-US/features/conversation.json @@ -28,6 +28,8 @@ "cid": "Conversation ID", "umo": "Unified Message Origin", "sessionId": "Session ID", + "userName": "User Name", + "avatar": "Avatar", "createdAt": "Created At", "updatedAt": "Updated At", "actions": "Actions" diff --git a/dashboard/src/i18n/locales/zh-CN/features/conversation.json b/dashboard/src/i18n/locales/zh-CN/features/conversation.json index 8a5ca6eb5..3247aa7d4 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/conversation.json +++ b/dashboard/src/i18n/locales/zh-CN/features/conversation.json @@ -28,6 +28,8 @@ "cid": "对话 ID", "umo": "消息会话来源", "sessionId": "会话 ID", + "userName": "用户名", + "avatar": "头像", "createdAt": "创建时间", "updatedAt": "更新时间", "actions": "操作" diff --git a/dashboard/src/views/ConversationPage.vue b/dashboard/src/views/ConversationPage.vue index 2a615b294..5d576b379 100644 --- a/dashboard/src/views/ConversationPage.vue +++ b/dashboard/src/views/ConversationPage.vue @@ -97,6 +97,17 @@ {{ item.sessionInfo.sessionId || tm('status.unknown') }} + + + + @@ -448,6 +459,8 @@ export default { { title: this.tm('table.headers.sessionId'), key: 'sessionId', sortable: true, width: '100px' }, ], }, + { title: this.tm('table.headers.userName'), key: 'user_name', sortable: true, width: '120px' }, + { title: this.tm('table.headers.avatar'), key: 'avatar', sortable: false, width: '80px' }, { title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' }, { title: this.tm('table.headers.updatedAt'), key: 'updated_at', sortable: true, width: '180px' }, { title: this.tm('table.headers.actions'), key: 'actions', sortable: false, align: 'center' } From 90156d2b9a9399399c4538757c4df35fb30d46ea Mon Sep 17 00:00:00 2001 From: fluchw <2427607997@qq.com> Date: Thu, 5 Feb 2026 18:21:47 +0800 Subject: [PATCH 2/2] refactor: address code review feedback - Extract reusable _ensure_column helper to reduce migration code duplication - Downgrade customer info logging to debug level to avoid PII exposure - Add TTL cache for batchget_customer API to reduce redundant calls - Simplify conversation backfill logic using single updates dict --- astrbot/core/astr_main_agent.py | 21 ++--- astrbot/core/db/sqlite.py | 81 ++++++++----------- .../platform/sources/wecom/wecom_adapter.py | 60 ++++++++++---- 3 files changed, 85 insertions(+), 77 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 6d42db00c..d28607bfb 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -9,6 +9,7 @@ import zoneinfo from collections.abc import Coroutine from dataclasses import dataclass, field +from typing import Any from astrbot.api import sp from astrbot.core import logger @@ -157,21 +158,15 @@ async def _get_session_conv( if not conversation: raise RuntimeError("无法创建新的对话。") # 如果已有对话但 user_name 或 avatar 为空,更新它们 - need_update = False + updates: dict[str, Any] = {} if conversation.user_name is None and user_name: - need_update = True + updates["user_name"] = user_name if conversation.avatar is None and avatar: - need_update = True - if need_update: - await conv_mgr.db.update_conversation( - cid, - user_name=user_name if conversation.user_name is None else None, - avatar=avatar if conversation.avatar is None else None, - ) - if conversation.user_name is None and user_name: - conversation.user_name = user_name - if conversation.avatar is None and avatar: - conversation.avatar = avatar + updates["avatar"] = avatar + if updates: + await conv_mgr.db.update_conversation(cid, **updates) + for field, value in updates.items(): + setattr(conversation, field, value) return conversation diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 2fab75b85..ad165c012 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -63,69 +63,52 @@ async def initialize(self) -> None: await self._ensure_conversation_avatar_column(conn) await conn.commit() - async def _ensure_persona_folder_columns(self, conn) -> None: - """确保 personas 表有 folder_id 和 sort_order 列。 + async def _ensure_column( + self, conn, table: str, column: str, ddl: str + ) -> None: + """确保指定表有指定列,如果不存在则添加。 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel 的 metadata.create_all 自动创建这些列。 + + Args: + conn: 数据库连接 + table: 表名 + column: 列名 + ddl: ALTER TABLE 语句中的列定义(不包含 ALTER TABLE ... ADD COLUMN 部分) """ - result = await conn.execute(text("PRAGMA table_info(personas)")) + result = await conn.execute(text(f"PRAGMA table_info({table})")) columns = {row[1] for row in result.fetchall()} - if "folder_id" not in columns: - await conn.execute( - text( - "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL" - ) - ) - if "sort_order" not in columns: + if column not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0") + text(f"ALTER TABLE {table} ADD COLUMN {ddl}") ) - async def _ensure_persona_skills_column(self, conn) -> None: - """确保 personas 表有 skills 列。 - - 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel - 的 metadata.create_all 自动创建这些列。 - """ - result = await conn.execute(text("PRAGMA table_info(personas)")) - columns = {row[1] for row in result.fetchall()} + async def _ensure_persona_folder_columns(self, conn) -> None: + """确保 personas 表有 folder_id 和 sort_order 列。""" + await self._ensure_column( + conn, "personas", "folder_id", "folder_id VARCHAR(36) DEFAULT NULL" + ) + await self._ensure_column( + conn, "personas", "sort_order", "sort_order INTEGER DEFAULT 0" + ) - if "skills" not in columns: - await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON")) + async def _ensure_persona_skills_column(self, conn) -> None: + """确保 personas 表有 skills 列。""" + await self._ensure_column(conn, "personas", "skills", "skills JSON") async def _ensure_conversation_user_name_column(self, conn) -> None: - """确保 conversations 表有 user_name 列。 - - 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel - 的 metadata.create_all 自动创建这些列。 - """ - result = await conn.execute(text("PRAGMA table_info(conversations)")) - columns = {row[1] for row in result.fetchall()} - - if "user_name" not in columns: - await conn.execute( - text( - "ALTER TABLE conversations ADD COLUMN user_name VARCHAR(255) DEFAULT NULL" - ) - ) + """确保 conversations 表有 user_name 列。""" + await self._ensure_column( + conn, "conversations", "user_name", "user_name VARCHAR(255) DEFAULT NULL" + ) async def _ensure_conversation_avatar_column(self, conn) -> None: - """确保 conversations 表有 avatar 列。 - - 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel - 的 metadata.create_all 自动创建这些列。 - """ - result = await conn.execute(text("PRAGMA table_info(conversations)")) - columns = {row[1] for row in result.fetchall()} - - if "avatar" not in columns: - await conn.execute( - text( - "ALTER TABLE conversations ADD COLUMN avatar VARCHAR(512) DEFAULT NULL" - ) - ) + """确保 conversations 表有 avatar 列。""" + await self._ensure_column( + conn, "conversations", "avatar", "avatar VARCHAR(512) DEFAULT NULL" + ) # ==== # Platform Statistics diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 5013493c2..421f5e7a0 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -1,9 +1,10 @@ import asyncio import os import sys +import time import uuid from collections.abc import Awaitable, Callable -from typing import Any, cast +from typing import Any, NamedTuple, cast import quart from requests import Response @@ -38,6 +39,17 @@ from typing_extensions import override +# 客户信息缓存条目 +class CustomerCacheEntry(NamedTuple): + nickname: str + avatar: str | None + expire_at: float + + +# 客户信息 TTL 缓存(默认 5 分钟) +CUSTOMER_CACHE_TTL = 300 + + class WecomServer: def __init__(self, event_queue: asyncio.Queue, config: dict): self.server = quart.Quart(__name__) @@ -173,6 +185,8 @@ def __init__( # 微信客服 self.kf_name = self.config.get("kf_name", None) + # 客户信息缓存 (external_userid -> CustomerCacheEntry) + self._customer_cache: dict[str, CustomerCacheEntry] = {} if self.kf_name: # inject self.wechat_kf_api = WeChatKF(client=self.client) @@ -352,22 +366,38 @@ async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None: msgtype = msg.get("msgtype") external_userid = cast(str, msg.get("external_userid")) - # 尝试获取客户昵称和头像 + # 尝试从缓存获取客户信息 nickname = external_userid avatar = None - try: - customer_info = await asyncio.get_event_loop().run_in_executor( - None, - self.wechat_kf_api.batchget_customer, - external_userid, - ) - logger.info(f"获取客户信息: {customer_info}") - customer_list = customer_info.get("customer_list", []) - if customer_list: - nickname = customer_list[0].get("nickname", external_userid) - avatar = customer_list[0].get("avatar", None) - except Exception as e: - logger.debug(f"获取客户信息失败: {e}") + now = time.time() + cached = self._customer_cache.get(external_userid) + if cached and cached.expire_at > now: + # 缓存命中 + nickname = cached.nickname + avatar = cached.avatar + logger.debug(f"客户信息缓存命中: external_userid={external_userid}") + else: + # 缓存未命中或已过期,调用 API + try: + customer_info = await asyncio.get_event_loop().run_in_executor( + None, + self.wechat_kf_api.batchget_customer, + external_userid, + ) + # 避免在日志中输出完整客户信息(包含昵称、头像等敏感数据) + logger.debug(f"获取客户信息成功: external_userid={external_userid}") + customer_list = customer_info.get("customer_list", []) + if customer_list: + nickname = customer_list[0].get("nickname", external_userid) + avatar = customer_list[0].get("avatar", None) + # 更新缓存 + self._customer_cache[external_userid] = CustomerCacheEntry( + nickname=nickname, + avatar=avatar, + expire_at=now + CUSTOMER_CACHE_TTL, + ) + except Exception as e: + logger.debug(f"获取客户信息失败: {e}") abm = AstrBotMessage() abm.raw_message = msg