feat:webui默认infoLog,移除display_message字段
This commit is contained in:
@@ -8,8 +8,6 @@ import random
|
||||
import re
|
||||
import time
|
||||
|
||||
import jieba
|
||||
|
||||
from src.chat.message_receive.chat_manager import chat_manager as _chat_manager
|
||||
from src.chat.message_receive.message import SessionMessage
|
||||
from src.common.logger import get_logger
|
||||
@@ -912,110 +910,3 @@ def parse_keywords_string(keywords_input) -> list[str]:
|
||||
return [keywords_str] if keywords_str else []
|
||||
|
||||
|
||||
def cut_key_words(concept_name: str) -> list[str]:
|
||||
"""对概念名称进行jieba分词,并过滤掉关键词列表中的关键词"""
|
||||
concept_name_tokens = list(jieba.cut(concept_name))
|
||||
|
||||
# 定义常见连词、停用词与标点
|
||||
conjunctions = {"和", "与", "及", "跟", "以及", "并且", "而且", "或", "或者", "并"}
|
||||
stop_words = {
|
||||
"的",
|
||||
"了",
|
||||
"呢",
|
||||
"吗",
|
||||
"吧",
|
||||
"啊",
|
||||
"哦",
|
||||
"恩",
|
||||
"嗯",
|
||||
"呀",
|
||||
"嘛",
|
||||
"哇",
|
||||
"在",
|
||||
"是",
|
||||
"很",
|
||||
"也",
|
||||
"又",
|
||||
"就",
|
||||
"都",
|
||||
"还",
|
||||
"更",
|
||||
"最",
|
||||
"被",
|
||||
"把",
|
||||
"给",
|
||||
"对",
|
||||
"和",
|
||||
"与",
|
||||
"及",
|
||||
"跟",
|
||||
"并",
|
||||
"而且",
|
||||
"或者",
|
||||
"或",
|
||||
"以及",
|
||||
}
|
||||
chinese_punctuations = set(",。!?、;:()【】《》“”‘’—…·-——,.!?;:()[]<>'\"/\\")
|
||||
|
||||
# 清理空白并初步过滤纯标点
|
||||
cleaned_tokens = []
|
||||
for tok in concept_name_tokens:
|
||||
t = tok.strip()
|
||||
if not t:
|
||||
continue
|
||||
# 去除纯标点
|
||||
if all(ch in chinese_punctuations for ch in t):
|
||||
continue
|
||||
cleaned_tokens.append(t)
|
||||
|
||||
# 合并连词两侧的词(仅当两侧都存在且不是标点/停用词时)
|
||||
merged_tokens = []
|
||||
i = 0
|
||||
n = len(cleaned_tokens)
|
||||
while i < n:
|
||||
tok = cleaned_tokens[i]
|
||||
if tok in conjunctions and merged_tokens and i + 1 < n:
|
||||
left = merged_tokens[-1]
|
||||
right = cleaned_tokens[i + 1]
|
||||
# 左右都需要是有效词
|
||||
if (
|
||||
left
|
||||
and right
|
||||
and left not in conjunctions
|
||||
and right not in conjunctions
|
||||
and left not in stop_words
|
||||
and right not in stop_words
|
||||
and not all(ch in chinese_punctuations for ch in left)
|
||||
and not all(ch in chinese_punctuations for ch in right)
|
||||
):
|
||||
# 合并为一个新词,并替换掉左侧与跳过右侧
|
||||
combined = f"{left}{tok}{right}"
|
||||
merged_tokens[-1] = combined
|
||||
i += 2
|
||||
continue
|
||||
# 常规推进
|
||||
merged_tokens.append(tok)
|
||||
i += 1
|
||||
|
||||
# 二次过滤:去除停用词、单字符纯标点与无意义项
|
||||
result_tokens = []
|
||||
seen = set()
|
||||
# ban_words = set(getattr(global_config.memory, "memory_ban_words", []) or [])
|
||||
for tok in merged_tokens:
|
||||
if tok in conjunctions:
|
||||
# 独立连词丢弃
|
||||
continue
|
||||
if tok in stop_words:
|
||||
continue
|
||||
# if tok in ban_words:
|
||||
# continue
|
||||
if all(ch in chinese_punctuations for ch in tok):
|
||||
continue
|
||||
if tok.strip() == "":
|
||||
continue
|
||||
if tok not in seen:
|
||||
seen.add(tok)
|
||||
result_tokens.append(tok)
|
||||
|
||||
filtered_concept_name_tokens = result_tokens
|
||||
return filtered_concept_name_tokens
|
||||
|
||||
@@ -79,7 +79,6 @@ class BufferCLI:
|
||||
)
|
||||
message.raw_message = MessageSequence([TextComponent(text=user_text)])
|
||||
message.processed_plain_text = user_text
|
||||
message.display_message = user_text
|
||||
message.initialized = True
|
||||
return message
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ class MaiMessage(BaseDatabaseDataModel[Messages]):
|
||||
self.reply_to: Optional[str] = None
|
||||
|
||||
self.processed_plain_text: Optional[str] = None
|
||||
self.display_message: Optional[str] = None
|
||||
self.raw_message: MessageSequence
|
||||
|
||||
@classmethod
|
||||
@@ -86,7 +85,6 @@ class MaiMessage(BaseDatabaseDataModel[Messages]):
|
||||
obj.reply_to = db_record.reply_to
|
||||
obj.session_id = db_record.session_id
|
||||
obj.processed_plain_text = db_record.processed_plain_text
|
||||
obj.display_message = db_record.display_message
|
||||
obj.raw_message = MessageUtils.from_db_record_msg_to_MaiSeq(db_record.raw_content)
|
||||
return obj
|
||||
|
||||
@@ -113,7 +111,6 @@ class MaiMessage(BaseDatabaseDataModel[Messages]):
|
||||
is_notify=self.is_notify,
|
||||
raw_content=MessageUtils.from_MaiSeq_to_db_record_msg(self.raw_message),
|
||||
processed_plain_text=self.processed_plain_text,
|
||||
display_message=self.display_message,
|
||||
additional_config=additional_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ class Messages(SQLModel, table=True):
|
||||
# 消息内容
|
||||
raw_content: bytes = Field(sa_column=Column(LargeBinary)) # msgpack后的原始消息内容
|
||||
processed_plain_text: Optional[str] = Field(default=None) # 平面化处理后的纯文本消息
|
||||
display_message: Optional[str] = Field(default=None) # 显示的消息内容(被放入Prompt)
|
||||
|
||||
# 其他配置
|
||||
additional_config: Optional[str] = Field(default=None) # 额外配置,JSON格式存储
|
||||
|
||||
@@ -8,12 +8,14 @@ from .registry import MigrationRegistry
|
||||
from .resolver import BaseSchemaVersionDetector, SchemaVersionResolver
|
||||
from .schema import SQLiteSchemaInspector
|
||||
from .v2_to_v3 import migrate_v2_to_v3
|
||||
from .v3_to_v4 import migrate_v3_to_v4
|
||||
from .version_store import SQLiteUserVersionStore
|
||||
|
||||
EMPTY_SCHEMA_VERSION = 0
|
||||
LEGACY_V1_SCHEMA_VERSION = 1
|
||||
V2_SCHEMA_VERSION = 2
|
||||
LATEST_SCHEMA_VERSION = 3
|
||||
V3_SCHEMA_VERSION = 3
|
||||
LATEST_SCHEMA_VERSION = 4
|
||||
|
||||
_LEGACY_V1_EXCLUSIVE_TABLES = (
|
||||
"chat_streams",
|
||||
@@ -78,9 +80,46 @@ class LatestSchemaVersionDetector(BaseSchemaVersionDetector):
|
||||
return None
|
||||
if not snapshot.has_column("person_info", "user_nickname"):
|
||||
return None
|
||||
if snapshot.has_column("mai_messages", "display_message"):
|
||||
return None
|
||||
return LATEST_SCHEMA_VERSION
|
||||
|
||||
|
||||
class V3SchemaVersionDetector(BaseSchemaVersionDetector):
|
||||
"""v3 schema 结构探测器。"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "v3_schema_detector"
|
||||
|
||||
def detect_version(self, snapshot: DatabaseSchemaSnapshot) -> Optional[int]:
|
||||
"""检测数据库是否为 v3 结构。"""
|
||||
|
||||
if any(snapshot.has_table(table_name) for table_name in _LEGACY_V1_EXCLUSIVE_TABLES):
|
||||
return None
|
||||
if not all(snapshot.has_table(table_name) for table_name in _COMMON_MARKER_TABLES):
|
||||
return None
|
||||
if snapshot.has_table("action_records"):
|
||||
return None
|
||||
if snapshot.has_table("thinking_questions"):
|
||||
return None
|
||||
if snapshot.has_column("images", "emotion"):
|
||||
return None
|
||||
if not snapshot.has_column("images", "image_hash"):
|
||||
return None
|
||||
if not snapshot.has_column("images", "full_path"):
|
||||
return None
|
||||
if not snapshot.has_column("images", "image_type"):
|
||||
return None
|
||||
if not snapshot.has_column("chat_history", "session_id"):
|
||||
return None
|
||||
if not snapshot.has_column("person_info", "user_nickname"):
|
||||
return None
|
||||
if not snapshot.has_column("mai_messages", "display_message"):
|
||||
return None
|
||||
return V3_SCHEMA_VERSION
|
||||
|
||||
|
||||
class V2SchemaVersionDetector(BaseSchemaVersionDetector):
|
||||
"""v2 schema 结构探测器。"""
|
||||
|
||||
@@ -174,6 +213,7 @@ def build_default_schema_version_detectors() -> List[BaseSchemaVersionDetector]:
|
||||
|
||||
return [
|
||||
LatestSchemaVersionDetector(),
|
||||
V3SchemaVersionDetector(),
|
||||
V2SchemaVersionDetector(),
|
||||
LegacyV1SchemaDetector(),
|
||||
]
|
||||
@@ -211,10 +251,17 @@ def build_default_migration_registry() -> MigrationRegistry:
|
||||
),
|
||||
MigrationStep(
|
||||
version_from=V2_SCHEMA_VERSION,
|
||||
version_to=LATEST_SCHEMA_VERSION,
|
||||
version_to=V3_SCHEMA_VERSION,
|
||||
name="v2_to_v3",
|
||||
description="移除废弃表,并将 emoji 标签统一收敛到 description 字段。",
|
||||
handler=migrate_v2_to_v3,
|
||||
),
|
||||
MigrationStep(
|
||||
version_from=V3_SCHEMA_VERSION,
|
||||
version_to=LATEST_SCHEMA_VERSION,
|
||||
name="v3_to_v4",
|
||||
description="移除 mai_messages.display_message 弃用列。",
|
||||
handler=migrate_v3_to_v4,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -489,9 +489,6 @@ def _build_legacy_message_additional_config(row: Mapping[str, Any]) -> Optional[
|
||||
|
||||
legacy_fields = {
|
||||
"intercept_message_level": row.get("intercept_message_level"),
|
||||
"interest_value": row.get("interest_value"),
|
||||
"key_words": row.get("key_words"),
|
||||
"key_words_lite": row.get("key_words_lite"),
|
||||
"priority_info": row.get("priority_info"),
|
||||
"priority_mode": row.get("priority_mode"),
|
||||
"selected_expressions": row.get("selected_expressions"),
|
||||
|
||||
155
src/common/database/migrations/v3_to_v4.py
Normal file
155
src/common/database/migrations/v3_to_v4.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""v3 schema 升级到 v4 的迁移逻辑。"""
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
from .exceptions import DatabaseMigrationExecutionError
|
||||
from .models import MigrationExecutionContext
|
||||
from .schema import SQLiteSchemaInspector
|
||||
|
||||
logger = get_logger("database_migration")
|
||||
|
||||
_V3_MESSAGES_BACKUP_TABLE = "__v3_mai_messages_backup"
|
||||
_V4_MESSAGES_CREATE_SQL = """
|
||||
CREATE TABLE mai_messages (
|
||||
id INTEGER NOT NULL,
|
||||
message_id VARCHAR(255) NOT NULL,
|
||||
timestamp DATETIME,
|
||||
platform VARCHAR(100) NOT NULL,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
user_nickname VARCHAR(255) NOT NULL,
|
||||
user_cardname VARCHAR(255),
|
||||
group_id VARCHAR(255),
|
||||
group_name VARCHAR(255),
|
||||
is_mentioned BOOLEAN NOT NULL,
|
||||
is_at BOOLEAN NOT NULL,
|
||||
session_id VARCHAR(255) NOT NULL,
|
||||
reply_to VARCHAR(255),
|
||||
is_emoji BOOLEAN NOT NULL,
|
||||
is_picture BOOLEAN NOT NULL,
|
||||
is_command BOOLEAN NOT NULL,
|
||||
is_notify BOOLEAN NOT NULL,
|
||||
raw_content BLOB,
|
||||
processed_plain_text VARCHAR,
|
||||
additional_config VARCHAR,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
"""
|
||||
_V4_MESSAGES_INDEX_STATEMENTS = (
|
||||
"CREATE INDEX ix_mai_messages_group_id ON mai_messages (group_id)",
|
||||
"CREATE INDEX ix_mai_messages_message_id ON mai_messages (message_id)",
|
||||
"CREATE INDEX ix_mai_messages_platform ON mai_messages (platform)",
|
||||
"CREATE INDEX ix_mai_messages_session_id ON mai_messages (session_id)",
|
||||
"CREATE INDEX ix_mai_messages_user_id ON mai_messages (user_id)",
|
||||
"CREATE INDEX ix_mai_messages_user_nickname ON mai_messages (user_nickname)",
|
||||
)
|
||||
|
||||
|
||||
def migrate_v3_to_v4(context: MigrationExecutionContext) -> None:
|
||||
"""执行 v3 到 v4 的 schema 迁移。"""
|
||||
|
||||
connection = context.connection
|
||||
total_records = _count_table_rows(connection, "mai_messages")
|
||||
context.start_progress(
|
||||
total_tables=1,
|
||||
total_records=total_records,
|
||||
description="v3 -> v4 迁移进度",
|
||||
table_unit_name="表",
|
||||
record_unit_name="记录",
|
||||
)
|
||||
|
||||
migrated_message_rows = _migrate_messages_table_to_v4(connection)
|
||||
context.advance_progress(
|
||||
records=migrated_message_rows,
|
||||
completed_tables=1,
|
||||
item_name="mai_messages",
|
||||
)
|
||||
|
||||
logger.info(f"v3 -> v4 数据库迁移完成: mai_messages重建={migrated_message_rows}")
|
||||
|
||||
|
||||
def _count_table_rows(connection: Connection, table_name: str) -> int:
|
||||
"""统计表记录数,不存在时返回 0。"""
|
||||
|
||||
schema_inspector = SQLiteSchemaInspector()
|
||||
if not schema_inspector.table_exists(connection, table_name):
|
||||
return 0
|
||||
row = connection.execute(text(f'SELECT COUNT(*) FROM "{table_name}"')).first()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
|
||||
def _migrate_messages_table_to_v4(connection: Connection) -> int:
|
||||
"""重建 ``mai_messages`` 表并移除弃用的 ``display_message`` 列。"""
|
||||
|
||||
schema_inspector = SQLiteSchemaInspector()
|
||||
if not schema_inspector.table_exists(connection, "mai_messages"):
|
||||
return 0
|
||||
if not schema_inspector.get_table_schema(connection, "mai_messages").has_column("display_message"):
|
||||
return _count_table_rows(connection, "mai_messages")
|
||||
if schema_inspector.table_exists(connection, _V3_MESSAGES_BACKUP_TABLE):
|
||||
raise DatabaseMigrationExecutionError(
|
||||
f"检测到残留备份表 {_V3_MESSAGES_BACKUP_TABLE},无法安全执行 v3 -> v4 mai_messages 迁移。"
|
||||
)
|
||||
|
||||
connection.exec_driver_sql(f'ALTER TABLE "mai_messages" RENAME TO "{_V3_MESSAGES_BACKUP_TABLE}"')
|
||||
connection.exec_driver_sql(_V4_MESSAGES_CREATE_SQL)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
f"""
|
||||
INSERT INTO mai_messages (
|
||||
id,
|
||||
message_id,
|
||||
timestamp,
|
||||
platform,
|
||||
user_id,
|
||||
user_nickname,
|
||||
user_cardname,
|
||||
group_id,
|
||||
group_name,
|
||||
is_mentioned,
|
||||
is_at,
|
||||
session_id,
|
||||
reply_to,
|
||||
is_emoji,
|
||||
is_picture,
|
||||
is_command,
|
||||
is_notify,
|
||||
raw_content,
|
||||
processed_plain_text,
|
||||
additional_config
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
message_id,
|
||||
timestamp,
|
||||
platform,
|
||||
user_id,
|
||||
user_nickname,
|
||||
user_cardname,
|
||||
group_id,
|
||||
group_name,
|
||||
is_mentioned,
|
||||
is_at,
|
||||
session_id,
|
||||
reply_to,
|
||||
is_emoji,
|
||||
is_picture,
|
||||
is_command,
|
||||
is_notify,
|
||||
raw_content,
|
||||
COALESCE(NULLIF(processed_plain_text, ''), display_message),
|
||||
additional_config
|
||||
FROM "{_V3_MESSAGES_BACKUP_TABLE}"
|
||||
ORDER BY id
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
migrated_rows = _count_table_rows(connection, "mai_messages")
|
||||
connection.exec_driver_sql(f'DROP TABLE "{_V3_MESSAGES_BACKUP_TABLE}"')
|
||||
for statement in _V4_MESSAGES_INDEX_STATEMENTS:
|
||||
connection.exec_driver_sql(statement)
|
||||
return migrated_rows
|
||||
@@ -207,7 +207,7 @@ async def handle_tool(
|
||||
sent_message = await send_service._send_to_target_with_message(
|
||||
message_sequence=reply_sequence,
|
||||
stream_id=tool_ctx.runtime.session_id,
|
||||
display_message=segment,
|
||||
processed_plain_text=segment,
|
||||
set_reply=segment_set_quote,
|
||||
reply_message=target_message if segment_set_quote else None,
|
||||
selected_expressions=reply_result.selected_expression_ids or None,
|
||||
|
||||
@@ -53,7 +53,6 @@ if TYPE_CHECKING:
|
||||
logger = get_logger("maisaka_reasoning_engine")
|
||||
|
||||
TIMING_GATE_CONTEXT_DROP_HEAD_RATIO = 0.7
|
||||
TIMING_GATE_MAX_TOKENS = 384
|
||||
TIMING_GATE_MAX_ATTEMPTS = 3
|
||||
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"}
|
||||
HISTORY_SILENT_TOOL_NAMES = {"finish"}
|
||||
@@ -140,7 +139,6 @@ class MaisakaReasoningEngine:
|
||||
system_prompt=system_prompt,
|
||||
request_kind="timing_gate",
|
||||
interrupt_flag=None,
|
||||
max_tokens=TIMING_GATE_MAX_TOKENS,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ class RuntimeCoreCapabilityMixin:
|
||||
content=command,
|
||||
stream_id=stream_id,
|
||||
storage_message=bool(args.get("storage_message", True)),
|
||||
display_message=str(args.get("display_message", "")),
|
||||
processed_plain_text=str(args.get("processed_plain_text", "")),
|
||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||
maisaka_source_kind=maisaka_source_kind,
|
||||
)
|
||||
@@ -228,7 +228,7 @@ class RuntimeCoreCapabilityMixin:
|
||||
message_type=message_type,
|
||||
content=content,
|
||||
stream_id=stream_id,
|
||||
display_message=str(args.get("display_message", "")),
|
||||
processed_plain_text=str(args.get("processed_plain_text", "")),
|
||||
typing=bool(args.get("typing", False)),
|
||||
storage_message=bool(args.get("storage_message", True)),
|
||||
sync_to_maisaka_history=sync_to_maisaka_history,
|
||||
|
||||
@@ -296,11 +296,13 @@ class RuntimeDataCapabilityMixin:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_messages(messages: list) -> List[Any]:
|
||||
def _serialize_messages(messages: list, include_binary_data: bool = True) -> List[Any]:
|
||||
result: List[Any] = []
|
||||
for msg in messages:
|
||||
if all(hasattr(msg, attr) for attr in ("message_id", "timestamp", "platform", "message_info", "raw_message")):
|
||||
result.append(dict(PluginMessageUtils._session_message_to_dict(msg)))
|
||||
result.append(
|
||||
dict(PluginMessageUtils._session_message_to_dict(msg, include_binary_data=include_binary_data))
|
||||
)
|
||||
elif hasattr(msg, "model_dump"):
|
||||
result.append(msg.model_dump())
|
||||
elif hasattr(msg, "__dict__"):
|
||||
@@ -321,7 +323,12 @@ class RuntimeDataCapabilityMixin:
|
||||
message_id=message_id,
|
||||
chat_id=str(args.get("chat_id") or args.get("stream_id") or "").strip() or None,
|
||||
)
|
||||
serialized_message = self._serialize_messages([message])[0] if message is not None else None
|
||||
include_binary_data = bool(args.get("include_binary_data", False))
|
||||
serialized_message = (
|
||||
self._serialize_messages([message], include_binary_data=include_binary_data)[0]
|
||||
if message is not None
|
||||
else None
|
||||
)
|
||||
return {"success": True, "message": serialized_message}
|
||||
except Exception as e:
|
||||
logger.error(f"[cap.message.get_by_id] 执行失败: {e}", exc_info=True)
|
||||
@@ -338,7 +345,13 @@ class RuntimeDataCapabilityMixin:
|
||||
limit_mode=args.get("limit_mode", "latest"),
|
||||
filter_mai=args.get("filter_mai", False),
|
||||
)
|
||||
return {"success": True, "messages": self._serialize_messages(messages)}
|
||||
return {
|
||||
"success": True,
|
||||
"messages": self._serialize_messages(
|
||||
messages,
|
||||
include_binary_data=bool(args.get("include_binary_data", False)),
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[cap.message.get_by_time] 执行失败: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -360,7 +373,13 @@ class RuntimeDataCapabilityMixin:
|
||||
filter_mai=args.get("filter_mai", False),
|
||||
filter_command=args.get("filter_command", False),
|
||||
)
|
||||
return {"success": True, "messages": self._serialize_messages(messages)}
|
||||
return {
|
||||
"success": True,
|
||||
"messages": self._serialize_messages(
|
||||
messages,
|
||||
include_binary_data=bool(args.get("include_binary_data", False)),
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[cap.message.get_by_time_in_chat] 执行失败: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -385,7 +404,13 @@ class RuntimeDataCapabilityMixin:
|
||||
limit_mode=args.get("limit_mode", "latest"),
|
||||
filter_mai=args.get("filter_mai", False),
|
||||
)
|
||||
return {"success": True, "messages": self._serialize_messages(messages)}
|
||||
return {
|
||||
"success": True,
|
||||
"messages": self._serialize_messages(
|
||||
messages,
|
||||
include_binary_data=bool(args.get("include_binary_data", False)),
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[cap.message.get_recent] 执行失败: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@@ -56,12 +56,14 @@ class MessageDict(TypedDict, total=False):
|
||||
session_id: str
|
||||
reply_to: Optional[str]
|
||||
processed_plain_text: Optional[str]
|
||||
display_message: Optional[str]
|
||||
|
||||
|
||||
class PluginMessageUtils:
|
||||
@staticmethod
|
||||
def _message_sequence_to_dict(message_sequence: MessageSequence) -> List[Dict[str, Any]]:
|
||||
def _message_sequence_to_dict(
|
||||
message_sequence: MessageSequence,
|
||||
include_binary_data: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""将消息组件序列转换为插件运行时使用的字典结构。
|
||||
|
||||
Args:
|
||||
@@ -70,10 +72,16 @@ class PluginMessageUtils:
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 供插件运行时协议使用的消息段字典列表。
|
||||
"""
|
||||
return [PluginMessageUtils._component_to_dict(component) for component in message_sequence.components]
|
||||
return [
|
||||
PluginMessageUtils._component_to_dict(component, include_binary_data=include_binary_data)
|
||||
for component in message_sequence.components
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _component_to_dict(component: StandardMessageComponents) -> Dict[str, Any]:
|
||||
def _component_to_dict(
|
||||
component: StandardMessageComponents,
|
||||
include_binary_data: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""将单个消息组件转换为插件运行时字典结构。
|
||||
|
||||
Args:
|
||||
@@ -91,8 +99,10 @@ class PluginMessageUtils:
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
if include_binary_data and (
|
||||
binary_data_base64 := PluginMessageUtils._binary_component_to_base64(component, "image")
|
||||
):
|
||||
serialized["binary_data_base64"] = binary_data_base64
|
||||
return serialized
|
||||
|
||||
if isinstance(component, EmojiComponent):
|
||||
@@ -101,8 +111,10 @@ class PluginMessageUtils:
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
if include_binary_data and (
|
||||
binary_data_base64 := PluginMessageUtils._binary_component_to_base64(component, "emoji")
|
||||
):
|
||||
serialized["binary_data_base64"] = binary_data_base64
|
||||
return serialized
|
||||
|
||||
if isinstance(component, VoiceComponent):
|
||||
@@ -111,7 +123,7 @@ class PluginMessageUtils:
|
||||
"data": component.content,
|
||||
"hash": component.binary_hash,
|
||||
}
|
||||
if component.binary_data:
|
||||
if include_binary_data and component.binary_data:
|
||||
serialized["binary_data_base64"] = base64.b64encode(component.binary_data).decode("utf-8")
|
||||
return serialized
|
||||
|
||||
@@ -140,13 +152,53 @@ class PluginMessageUtils:
|
||||
if isinstance(component, ForwardNodeComponent):
|
||||
return {
|
||||
"type": "forward",
|
||||
"data": [PluginMessageUtils._forward_component_to_dict(item) for item in component.forward_components],
|
||||
"data": [
|
||||
PluginMessageUtils._forward_component_to_dict(item, include_binary_data=include_binary_data)
|
||||
for item in component.forward_components
|
||||
],
|
||||
}
|
||||
|
||||
return {"type": "dict", "data": component.data}
|
||||
|
||||
@staticmethod
|
||||
def _forward_component_to_dict(component: ForwardComponent) -> Dict[str, Any]:
|
||||
def _binary_component_to_base64(component: Any, image_type: str) -> str:
|
||||
"""将图片或表情组件转换为 Base64,必要时通过 hash 从图片库加载文件。"""
|
||||
|
||||
if component.binary_data:
|
||||
return base64.b64encode(component.binary_data).decode("utf-8")
|
||||
|
||||
binary_hash = str(component.binary_hash or "").strip()
|
||||
if not binary_hash:
|
||||
return ""
|
||||
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
||||
from sqlmodel import select
|
||||
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import Images, ImageType
|
||||
|
||||
target_image_type = ImageType.IMAGE if image_type == "image" else ImageType.EMOJI
|
||||
with get_db_session(auto_commit=False) as db:
|
||||
statement = select(Images).filter_by(image_hash=binary_hash, image_type=target_image_type).limit(1)
|
||||
image_record = db.exec(statement).first()
|
||||
if image_record is None or image_record.no_file_flag:
|
||||
return ""
|
||||
|
||||
image_path = Path(image_record.full_path)
|
||||
if not image_path.is_file():
|
||||
return ""
|
||||
return base64.b64encode(image_path.read_bytes()).decode("utf-8")
|
||||
except Exception as exc:
|
||||
logger.debug("通过 hash 加载历史媒体失败: type=%s hash=%s error=%s", image_type, binary_hash, exc)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _forward_component_to_dict(
|
||||
component: ForwardComponent,
|
||||
include_binary_data: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""将单个转发节点组件转换为字典结构。
|
||||
|
||||
Args:
|
||||
@@ -160,7 +212,10 @@ class PluginMessageUtils:
|
||||
"user_nickname": component.user_nickname,
|
||||
"user_cardname": component.user_cardname,
|
||||
"message_id": component.message_id,
|
||||
"content": [PluginMessageUtils._component_to_dict(item) for item in component.content],
|
||||
"content": [
|
||||
PluginMessageUtils._component_to_dict(item, include_binary_data=include_binary_data)
|
||||
for item in component.content
|
||||
],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -341,7 +396,10 @@ class PluginMessageUtils:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _session_message_to_dict(session_message: SessionMessage) -> MessageDict:
|
||||
def _session_message_to_dict(
|
||||
session_message: SessionMessage,
|
||||
include_binary_data: bool = True,
|
||||
) -> MessageDict:
|
||||
"""
|
||||
将 SessionMessage 对象转换为字典格式(复用 MessageSequence.to_dict 方法)
|
||||
|
||||
@@ -357,7 +415,10 @@ class PluginMessageUtils:
|
||||
timestamp=str(session_message.timestamp.timestamp()), # 转换为时间戳字符串
|
||||
platform=session_message.platform,
|
||||
message_info=PluginMessageUtils._message_info_to_dict(session_message.message_info),
|
||||
raw_message=PluginMessageUtils._message_sequence_to_dict(session_message.raw_message),
|
||||
raw_message=PluginMessageUtils._message_sequence_to_dict(
|
||||
session_message.raw_message,
|
||||
include_binary_data=include_binary_data,
|
||||
),
|
||||
is_mentioned=session_message.is_mentioned,
|
||||
is_at=session_message.is_at,
|
||||
is_emoji=session_message.is_emoji,
|
||||
@@ -372,8 +433,6 @@ class PluginMessageUtils:
|
||||
message_dict["reply_to"] = session_message.reply_to
|
||||
if session_message.processed_plain_text is not None:
|
||||
message_dict["processed_plain_text"] = session_message.processed_plain_text
|
||||
if session_message.display_message is not None:
|
||||
message_dict["display_message"] = session_message.display_message
|
||||
|
||||
return message_dict
|
||||
|
||||
@@ -485,8 +544,5 @@ class PluginMessageUtils:
|
||||
session_message.processed_plain_text, str
|
||||
):
|
||||
session_message.processed_plain_text = None
|
||||
session_message.display_message = message_dict.get("display_message")
|
||||
if session_message.display_message is not None and not isinstance(session_message.display_message, str):
|
||||
session_message.display_message = None
|
||||
|
||||
return session_message
|
||||
|
||||
@@ -43,8 +43,6 @@ def _build_readable_line(
|
||||
def _normalize_messages(messages: List[SessionMessage]) -> List[SessionMessage]:
|
||||
normalized: List[SessionMessage] = []
|
||||
for message in messages:
|
||||
if not message.processed_plain_text:
|
||||
message.processed_plain_text = message.display_message or ""
|
||||
normalized.append(message)
|
||||
return normalized
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpe
|
||||
"type": "string",
|
||||
"description": "目标会话 ID。",
|
||||
},
|
||||
"display_message": {
|
||||
"processed_plain_text": {
|
||||
"type": "string",
|
||||
"description": "展示层文本。",
|
||||
"description": "可选的预处理纯文本内容。",
|
||||
},
|
||||
"typing": {
|
||||
"type": "boolean",
|
||||
@@ -97,7 +97,7 @@ def register_send_service_hook_specs(registry: HookSpecRegistry) -> List[HookSpe
|
||||
required=[
|
||||
"message",
|
||||
"stream_id",
|
||||
"display_message",
|
||||
"processed_plain_text",
|
||||
"typing",
|
||||
"set_reply",
|
||||
"storage_message",
|
||||
@@ -494,7 +494,7 @@ def _build_outbound_log_preview(message: SessionMessage, max_length: int = 160)
|
||||
Returns:
|
||||
str: 适用于日志展示的消息摘要。
|
||||
"""
|
||||
preview_text = (message.processed_plain_text or message.display_message or "").strip()
|
||||
preview_text = (message.processed_plain_text or "").strip()
|
||||
if not preview_text:
|
||||
preview_text = f"[{_describe_message_sequence(message.raw_message)}]"
|
||||
|
||||
@@ -507,7 +507,7 @@ def _build_outbound_log_preview(message: SessionMessage, max_length: int = 160)
|
||||
def _build_outbound_session_message(
|
||||
message_sequence: MessageSequence,
|
||||
stream_id: str,
|
||||
display_message: str = "",
|
||||
processed_plain_text: str = "",
|
||||
reply_message: Optional[MaiMessage] = None,
|
||||
selected_expressions: Optional[List[int]] = None,
|
||||
) -> Optional[SessionMessage]:
|
||||
@@ -516,7 +516,7 @@ def _build_outbound_session_message(
|
||||
Args:
|
||||
message_sequence: 待发送的消息组件序列。
|
||||
stream_id: 目标会话 ID。
|
||||
display_message: 用于界面展示的文本内容。
|
||||
processed_plain_text: 可选的预处理纯文本内容。
|
||||
reply_message: 被回复的锚点消息。
|
||||
selected_expressions: 可选的表情候选索引列表。
|
||||
|
||||
@@ -571,7 +571,7 @@ def _build_outbound_session_message(
|
||||
)
|
||||
outbound_message.raw_message = _clone_message_sequence(message_sequence)
|
||||
outbound_message.session_id = target_stream.session_id
|
||||
outbound_message.display_message = display_message
|
||||
outbound_message.processed_plain_text = processed_plain_text.strip() or _build_processed_plain_text(outbound_message)
|
||||
outbound_message.reply_to = anchor_message.message_id if anchor_message is not None else None
|
||||
message_flags = _detect_outbound_message_flags(outbound_message.raw_message)
|
||||
outbound_message.is_emoji = message_flags["is_emoji"]
|
||||
@@ -619,7 +619,8 @@ async def _prepare_message_for_platform_io(
|
||||
raise ValueError("set_reply=True 时必须提供 reply_message_id")
|
||||
_ensure_reply_component(message, reply_message_id)
|
||||
|
||||
message.processed_plain_text = _build_processed_plain_text(message)
|
||||
if set_reply or not message.processed_plain_text:
|
||||
message.processed_plain_text = _build_processed_plain_text(message)
|
||||
if typing:
|
||||
typing_time = calculate_typing_time(
|
||||
input_string=message.processed_plain_text or "",
|
||||
@@ -935,7 +936,7 @@ async def send_session_message(
|
||||
async def _send_to_target(
|
||||
message_sequence: MessageSequence,
|
||||
stream_id: str,
|
||||
display_message: str = "",
|
||||
processed_plain_text: str = "",
|
||||
typing: bool = False,
|
||||
set_reply: bool = False,
|
||||
reply_message: Optional[MaiMessage] = None,
|
||||
@@ -950,7 +951,7 @@ async def _send_to_target(
|
||||
await _send_to_target_with_message(
|
||||
message_sequence=message_sequence,
|
||||
stream_id=stream_id,
|
||||
display_message=display_message,
|
||||
processed_plain_text=processed_plain_text,
|
||||
typing=typing,
|
||||
set_reply=set_reply,
|
||||
reply_message=reply_message,
|
||||
@@ -967,7 +968,7 @@ async def _send_to_target(
|
||||
async def _send_to_target_with_message(
|
||||
message_sequence: MessageSequence,
|
||||
stream_id: str,
|
||||
display_message: str = "",
|
||||
processed_plain_text: str = "",
|
||||
typing: bool = False,
|
||||
set_reply: bool = False,
|
||||
reply_message: Optional[MaiMessage] = None,
|
||||
@@ -982,7 +983,7 @@ async def _send_to_target_with_message(
|
||||
Args:
|
||||
message_sequence: 待发送的消息组件序列。
|
||||
stream_id: 目标会话 ID。
|
||||
display_message: 用于界面展示的文本内容。
|
||||
processed_plain_text: 可选的预处理纯文本内容。
|
||||
typing: 是否显示输入中状态。
|
||||
set_reply: 是否在发送时附带引用回复。
|
||||
reply_message: 被回复的消息对象。
|
||||
@@ -1004,7 +1005,7 @@ async def _send_to_target_with_message(
|
||||
outbound_message = _build_outbound_session_message(
|
||||
message_sequence=message_sequence,
|
||||
stream_id=stream_id,
|
||||
display_message=display_message,
|
||||
processed_plain_text=processed_plain_text,
|
||||
reply_message=reply_message,
|
||||
selected_expressions=selected_expressions,
|
||||
)
|
||||
@@ -1015,7 +1016,7 @@ async def _send_to_target_with_message(
|
||||
"send_service.after_build_message",
|
||||
outbound_message,
|
||||
stream_id=stream_id,
|
||||
display_message=display_message,
|
||||
processed_plain_text=processed_plain_text,
|
||||
typing=typing,
|
||||
set_reply=set_reply,
|
||||
storage_message=storage_message,
|
||||
@@ -1068,7 +1069,6 @@ async def text_to_stream_with_message(
|
||||
return await _send_to_target_with_message(
|
||||
message_sequence=MessageSequence(components=[TextComponent(text=text)]),
|
||||
stream_id=stream_id,
|
||||
display_message="",
|
||||
typing=typing,
|
||||
set_reply=set_reply,
|
||||
reply_message=reply_message,
|
||||
@@ -1133,7 +1133,6 @@ async def emoji_to_stream_with_message(
|
||||
return await _send_to_target_with_message(
|
||||
message_sequence=_build_message_sequence_from_custom_message("emoji", emoji_base64),
|
||||
stream_id=stream_id,
|
||||
display_message="",
|
||||
typing=False,
|
||||
storage_message=storage_message,
|
||||
set_reply=set_reply,
|
||||
@@ -1202,7 +1201,6 @@ async def image_to_stream(
|
||||
return await _send_to_target(
|
||||
message_sequence=_build_message_sequence_from_custom_message("image", image_base64),
|
||||
stream_id=stream_id,
|
||||
display_message="",
|
||||
typing=False,
|
||||
storage_message=storage_message,
|
||||
set_reply=set_reply,
|
||||
@@ -1216,7 +1214,7 @@ async def custom_to_stream(
|
||||
message_type: str,
|
||||
content: str | Dict[str, Any],
|
||||
stream_id: str,
|
||||
display_message: str = "",
|
||||
processed_plain_text: str = "",
|
||||
typing: bool = False,
|
||||
reply_message: Optional[MaiMessage] = None,
|
||||
set_reply: bool = False,
|
||||
@@ -1231,7 +1229,7 @@ async def custom_to_stream(
|
||||
message_type: 自定义消息类型。
|
||||
content: 自定义消息内容。
|
||||
stream_id: 目标会话 ID。
|
||||
display_message: 用于展示的文本内容。
|
||||
processed_plain_text: 可选的预处理纯文本内容。
|
||||
typing: 是否显示输入中状态。
|
||||
reply_message: 被回复的消息对象。
|
||||
set_reply: 是否附带引用回复。
|
||||
@@ -1244,7 +1242,7 @@ async def custom_to_stream(
|
||||
return await _send_to_target(
|
||||
message_sequence=_build_message_sequence_from_custom_message(message_type, content),
|
||||
stream_id=stream_id,
|
||||
display_message=display_message,
|
||||
processed_plain_text=processed_plain_text,
|
||||
typing=typing,
|
||||
reply_message=reply_message,
|
||||
set_reply=set_reply,
|
||||
@@ -1258,7 +1256,7 @@ async def custom_to_stream(
|
||||
async def custom_reply_set_to_stream(
|
||||
reply_set: MessageSequence,
|
||||
stream_id: str,
|
||||
display_message: str = "",
|
||||
processed_plain_text: str = "",
|
||||
typing: bool = False,
|
||||
reply_message: Optional[MaiMessage] = None,
|
||||
set_reply: bool = False,
|
||||
@@ -1272,7 +1270,7 @@ async def custom_reply_set_to_stream(
|
||||
Args:
|
||||
reply_set: 待发送的消息组件序列。
|
||||
stream_id: 目标会话 ID。
|
||||
display_message: 用于展示的文本内容。
|
||||
processed_plain_text: 可选的预处理纯文本内容。
|
||||
typing: 是否显示输入中状态。
|
||||
reply_message: 被回复的消息对象。
|
||||
set_reply: 是否附带引用回复。
|
||||
@@ -1285,7 +1283,7 @@ async def custom_reply_set_to_stream(
|
||||
return await _send_to_target(
|
||||
message_sequence=reply_set,
|
||||
stream_id=stream_id,
|
||||
display_message=display_message,
|
||||
processed_plain_text=processed_plain_text,
|
||||
typing=typing,
|
||||
reply_message=reply_message,
|
||||
set_reply=set_reply,
|
||||
|
||||
@@ -112,7 +112,7 @@ class ChatHistoryManager:
|
||||
return {
|
||||
"id": msg.message_id,
|
||||
"type": "bot" if is_bot else "user",
|
||||
"content": msg.processed_plain_text or msg.display_message or "",
|
||||
"content": msg.processed_plain_text or "",
|
||||
"timestamp": msg.timestamp.timestamp(),
|
||||
"sender_name": user_info.user_nickname or (global_config.bot.nickname if is_bot else "未知用户"),
|
||||
"sender_id": "bot" if is_bot else user_id,
|
||||
@@ -175,11 +175,7 @@ class ChatHistoryManager:
|
||||
|
||||
user_info = target_msg.message_info.user_info
|
||||
if not has_content:
|
||||
content_text = (
|
||||
target_msg.processed_plain_text
|
||||
or target_msg.display_message
|
||||
or ""
|
||||
)
|
||||
content_text = target_msg.processed_plain_text or ""
|
||||
data["target_message_content"] = content_text
|
||||
if not has_sender:
|
||||
data["target_message_sender_id"] = user_info.user_id or ""
|
||||
|
||||
Reference in New Issue
Block a user