Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions astrbot/core/agent/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

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


Expand Down
22 changes: 20 additions & 2 deletions astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 为空,更新它们
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
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


Expand Down
20 changes: 18 additions & 2 deletions astrbot/core/conversation_mgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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 格式的字符串

Expand All @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
...
Expand All @@ -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."""
...
Expand Down
7 changes: 7 additions & 0 deletions astrbot/core/db/po.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
46 changes: 45 additions & 1 deletion astrbot/core/db/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
# ====
Expand Down Expand Up @@ -259,6 +295,8 @@ async def create_conversation(
cid=None,
created_at=None,
updated_at=None,
user_name=None,
avatar=None,
):
kwargs = {}
if cid:
Expand All @@ -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
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions astrbot/core/platform/astr_message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions astrbot/core/platform/astrbot_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
class MessageMember:
user_id: str # 发送者id
nickname: str | None = None
avatar: str | None = None
"""用户头像 URL"""

def __str__(self):
# 使用 f-string 来构建返回的字符串表示形式
Expand Down
25 changes: 22 additions & 3 deletions astrbot/core/platform/sources/wecom/wecom_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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}")
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
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])
Expand Down
6 changes: 5 additions & 1 deletion astrbot/dashboard/routes/conversation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import traceback
from dataclasses import asdict
from datetime import datetime
from io import BytesIO

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/conversation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/conversation.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"cid": "对话 ID",
"umo": "消息会话来源",
"sessionId": "会话 ID",
"userName": "用户名",
"avatar": "头像",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"actions": "操作"
Expand Down
13 changes: 13 additions & 0 deletions dashboard/src/views/ConversationPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
</template>

<template v-slot:item.user_name="{ item }">
<span>{{ item.user_name || '-' }}</span>
</template>

<template v-slot:item.avatar="{ item }">
<v-avatar v-if="item.avatar" size="32">
<v-img :src="item.avatar" :alt="item.user_name || 'avatar'"></v-img>
</v-avatar>
<span v-else>-</span>
</template>

<template v-slot:item.created_at="{ item }">
{{ formatTimestamp(item.created_at) }}
</template>
Expand Down Expand Up @@ -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' }
Expand Down