Merge branch 'dev' of https://github.com/Mai-with-u/MaiBot into dev
This commit is contained in:
@@ -21,3 +21,5 @@ temp/
|
||||
tmp/
|
||||
mai_knowledge/
|
||||
depends-data/
|
||||
!depends-data/
|
||||
!depends-data/char_frequency.json
|
||||
|
||||
@@ -22,16 +22,26 @@ import { zhCN } from 'date-fns/locale'
|
||||
|
||||
// 字号配置
|
||||
type FontSize = 'xs' | 'sm' | 'base'
|
||||
type LogLevelFilter = LogEntry['level'] | 'all'
|
||||
|
||||
const fontSizeConfig: Record<FontSize, { label: string; rowHeight: number; class: string }> = {
|
||||
xs: { label: '小', rowHeight: 28, class: 'text-[10px] sm:text-xs' },
|
||||
sm: { label: '中', rowHeight: 36, class: 'text-xs sm:text-sm' },
|
||||
base: { label: '大', rowHeight: 44, class: 'text-sm sm:text-base' },
|
||||
}
|
||||
|
||||
const levelPriority: Record<LogEntry['level'], number> = {
|
||||
DEBUG: 10,
|
||||
INFO: 20,
|
||||
WARNING: 30,
|
||||
ERROR: 40,
|
||||
CRITICAL: 50,
|
||||
}
|
||||
|
||||
export function LogViewerPage() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [levelFilter, setLevelFilter] = useState<string>('all')
|
||||
const [levelFilter, setLevelFilter] = useState<LogLevelFilter>('INFO')
|
||||
const [moduleFilter, setModuleFilter] = useState<string>('all')
|
||||
const [dateFrom, setDateFrom] = useState<Date | undefined>(undefined)
|
||||
const [dateTo, setDateTo] = useState<Date | undefined>(undefined)
|
||||
@@ -154,8 +164,10 @@ export function LogViewerPage() {
|
||||
log.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
log.module.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
// 级别过滤
|
||||
const matchesLevel = levelFilter === 'all' || log.level === levelFilter
|
||||
// 级别过滤:选择某个级别时显示该级别及以上的日志
|
||||
const matchesLevel =
|
||||
levelFilter === 'all' ||
|
||||
levelPriority[log.level] >= levelPriority[levelFilter]
|
||||
|
||||
// 模块过滤
|
||||
const matchesModule = moduleFilter === 'all' || log.module === moduleFilter
|
||||
@@ -355,17 +367,17 @@ export function LogViewerPage() {
|
||||
<CollapsibleContent className="space-y-2">
|
||||
{/* 级别和模块筛选 */}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:gap-2">
|
||||
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
||||
<Select value={levelFilter} onValueChange={(value) => setLevelFilter(value as LogLevelFilter)}>
|
||||
<SelectTrigger className="w-full sm:flex-1 h-8 text-xs">
|
||||
<Filter className="h-3.5 w-3.5 mr-1.5" />
|
||||
<SelectValue placeholder="级别" />
|
||||
<SelectValue placeholder="最低级别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部级别</SelectItem>
|
||||
<SelectItem value="DEBUG">DEBUG</SelectItem>
|
||||
<SelectItem value="INFO">INFO</SelectItem>
|
||||
<SelectItem value="WARNING">WARNING</SelectItem>
|
||||
<SelectItem value="ERROR">ERROR</SelectItem>
|
||||
<SelectItem value="DEBUG">DEBUG 及以上</SelectItem>
|
||||
<SelectItem value="INFO">INFO 及以上</SelectItem>
|
||||
<SelectItem value="WARNING">WARNING 及以上</SelectItem>
|
||||
<SelectItem value="ERROR">ERROR 及以上</SelectItem>
|
||||
<SelectItem value="CRITICAL">CRITICAL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -25,6 +25,7 @@ services:
|
||||
- ./data/MaiMBot/emoji:/data/emoji # 持久化表情包
|
||||
- ./data/MaiMBot/plugins:/MaiMBot/plugins # 插件目录
|
||||
- ./data/MaiMBot/logs:/MaiMBot/logs # 日志目录
|
||||
- ./depends-data:/MaiMBot/depends-data:ro # 运行时资源文件
|
||||
# - site-packages:/usr/local/lib/python3.13/site-packages # 持久化Python包,需要时启用
|
||||
restart: always
|
||||
networks:
|
||||
|
||||
@@ -193,7 +193,6 @@ def _build_incoming_message(
|
||||
message.is_command = False
|
||||
message.is_notify = False
|
||||
message.processed_plain_text = text
|
||||
message.display_message = text
|
||||
message.initialized = True
|
||||
return message
|
||||
|
||||
|
||||
@@ -754,15 +754,6 @@ def test_default_bootstrapper_can_migrate_legacy_v1_database(tmp_path: Path) ->
|
||||
"""
|
||||
)
|
||||
).mappings().one()
|
||||
action_row = connection.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT session_id, action_name, action_display_prompt
|
||||
FROM action_records
|
||||
WHERE action_id = 'action-1'
|
||||
"""
|
||||
)
|
||||
).mappings().one()
|
||||
tool_row = connection.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -796,6 +787,8 @@ def test_default_bootstrapper_can_migrate_legacy_v1_database(tmp_path: Path) ->
|
||||
assert snapshot.has_table("chat_sessions")
|
||||
assert snapshot.has_table("mai_messages")
|
||||
assert snapshot.has_table("tool_records")
|
||||
assert not snapshot.has_table("action_records")
|
||||
assert not snapshot.has_column("mai_messages", "display_message")
|
||||
|
||||
unpacked_raw_content = msgpack.unpackb(message_row["raw_content"], raw=False)
|
||||
additional_config = json.loads(message_row["additional_config"])
|
||||
@@ -807,9 +800,6 @@ def test_default_bootstrapper_can_migrate_legacy_v1_database(tmp_path: Path) ->
|
||||
assert message_row["processed_plain_text"] == "你好"
|
||||
assert unpacked_raw_content == [{"type": "text", "data": "你好呀"}]
|
||||
assert additional_config == {"priority_mode": "high", "source": "legacy"}
|
||||
assert action_row["session_id"] == "session-1"
|
||||
assert action_row["action_name"] == "search"
|
||||
assert action_row["action_display_prompt"] == "执行搜索"
|
||||
assert tool_row["session_id"] == "session-1"
|
||||
assert tool_row["tool_name"] == "search"
|
||||
assert tool_row["tool_display_prompt"] == "执行搜索"
|
||||
@@ -848,8 +838,8 @@ def test_legacy_v1_migration_reports_table_progress(tmp_path: Path) -> None:
|
||||
|
||||
migration_plan = manager.migrate(target_version=LATEST_SCHEMA_VERSION)
|
||||
|
||||
assert migration_plan.step_count() == 1
|
||||
assert len(reporter_instances) == 1
|
||||
assert migration_plan.step_count() == 3
|
||||
assert len(reporter_instances) == 3
|
||||
reporter_events = reporter_instances[0].events
|
||||
|
||||
assert reporter_events[0] == ("open", None, None, None)
|
||||
@@ -894,10 +884,6 @@ def test_initialize_database_calls_bootstrapper_before_create_all(
|
||||
del bind
|
||||
call_order.append("create_all")
|
||||
|
||||
def _fake_migrate_action_records() -> None:
|
||||
"""记录轻量补迁移调用。"""
|
||||
call_order.append("migrate_action_records")
|
||||
|
||||
def _fake_finalize_database(migration_state: DatabaseMigrationState) -> None:
|
||||
"""记录迁移收尾调用。
|
||||
|
||||
@@ -912,13 +898,11 @@ def test_initialize_database_calls_bootstrapper_before_create_all(
|
||||
monkeypatch.setattr(database_module._migration_bootstrapper, "prepare_database", _fake_prepare_database)
|
||||
monkeypatch.setattr(database_module._migration_bootstrapper, "finalize_database", _fake_finalize_database)
|
||||
monkeypatch.setattr(database_module.SQLModel.metadata, "create_all", _fake_create_all)
|
||||
monkeypatch.setattr(database_module, "_migrate_action_records_to_tool_records", _fake_migrate_action_records)
|
||||
|
||||
database_module.initialize_database()
|
||||
|
||||
assert call_order == [
|
||||
"prepare_database",
|
||||
"create_all",
|
||||
"migrate_action_records",
|
||||
"finalize_database",
|
||||
]
|
||||
|
||||
11
pytests/config_test/test_model_info_normalization.py
Normal file
11
pytests/config_test/test_model_info_normalization.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from src.config.model_configs import ModelInfo
|
||||
|
||||
|
||||
def test_model_identifier_strips_surrounding_whitespace() -> None:
|
||||
model_info = ModelInfo(
|
||||
api_provider="test-provider",
|
||||
model_identifier=" glm-5.1 ",
|
||||
name="test-model",
|
||||
)
|
||||
|
||||
assert model_info.model_identifier == "glm-5.1"
|
||||
@@ -41,7 +41,6 @@ def test_build_message_returns_session_message_with_maisaka_metadata() -> None:
|
||||
assert message.message_id == "maisaka-msg-1"
|
||||
assert message.timestamp == timestamp
|
||||
assert message.processed_plain_text == "展示消息内容"
|
||||
assert message.display_message == "展示消息内容"
|
||||
assert message.raw_message is raw_message
|
||||
|
||||
assert get_message_role(message) == "assistant"
|
||||
|
||||
@@ -554,7 +554,6 @@ async def test_inbound_codec_resolves_at_to_group_cardname() -> None:
|
||||
)
|
||||
|
||||
assert message_dict["processed_plain_text"] == "@群昵称"
|
||||
assert message_dict["display_message"] == "@群昵称"
|
||||
assert message_dict["raw_message"] == [
|
||||
{
|
||||
"type": "at",
|
||||
@@ -599,7 +598,6 @@ async def test_inbound_codec_falls_back_to_qq_nickname_when_group_cardname_is_em
|
||||
)
|
||||
|
||||
assert message_dict["processed_plain_text"] == "@QQ昵称"
|
||||
assert message_dict["display_message"] == "@QQ昵称"
|
||||
assert message_dict["raw_message"] == [
|
||||
{
|
||||
"type": "at",
|
||||
@@ -640,7 +638,6 @@ async def test_inbound_codec_falls_back_to_stranger_nickname_when_group_profile_
|
||||
)
|
||||
|
||||
assert message_dict["processed_plain_text"] == "@QQ昵称"
|
||||
assert message_dict["display_message"] == "@QQ昵称"
|
||||
assert message_dict["raw_message"] == [
|
||||
{
|
||||
"type": "at",
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.model_configs import APIProvider, ReasoningParseMode, ToolArgumentParseMode
|
||||
from src.llm_models.model_client.openai_client import (
|
||||
_OpenAIStreamAccumulator,
|
||||
_build_reasoning_key,
|
||||
_default_normal_response_parser,
|
||||
_parse_tool_arguments,
|
||||
_sanitize_messages_for_toolless_request,
|
||||
)
|
||||
from src.llm_models.payload_content.message import Message, RoleType, TextMessagePart
|
||||
from src.llm_models.payload_content.tool_option import ToolCall
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parse_mode", list(ToolArgumentParseMode))
|
||||
def test_parse_tool_arguments_treats_blank_arguments_as_empty_dict(parse_mode: ToolArgumentParseMode) -> None:
|
||||
assert _parse_tool_arguments("", parse_mode, None) == {}
|
||||
assert _parse_tool_arguments(" ", parse_mode, None) == {}
|
||||
|
||||
|
||||
def test_normal_response_parser_accepts_empty_string_arguments_for_parameterless_tool() -> None:
|
||||
response = SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason="tool_calls",
|
||||
message=SimpleNamespace(
|
||||
content=None,
|
||||
tool_calls=[
|
||||
SimpleNamespace(
|
||||
id="finish-call",
|
||||
type="function",
|
||||
function=SimpleNamespace(name="finish", arguments=""),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
usage=None,
|
||||
model="glm-5.1",
|
||||
)
|
||||
|
||||
api_response, usage_record = _default_normal_response_parser(
|
||||
response,
|
||||
reasoning_parse_mode=ReasoningParseMode.AUTO,
|
||||
tool_argument_parse_mode=ToolArgumentParseMode.AUTO,
|
||||
reasoning_key=None,
|
||||
)
|
||||
|
||||
assert len(api_response.tool_calls) == 1
|
||||
assert api_response.tool_calls[0].func_name == "finish"
|
||||
assert api_response.tool_calls[0].args == {}
|
||||
assert usage_record is None
|
||||
|
||||
|
||||
def test_sanitize_messages_for_toolless_request_drops_assistant_tool_call_without_parts() -> None:
|
||||
messages = [
|
||||
Message(
|
||||
|
||||
@@ -31,7 +31,6 @@ def test_plugin_message_utils_preserves_binary_components_and_reply_metadata() -
|
||||
)
|
||||
message.session_id = "qq:20001:10001"
|
||||
message.processed_plain_text = "binary payload"
|
||||
message.display_message = "binary payload"
|
||||
message.raw_message = MessageSequence(
|
||||
components=[
|
||||
TextComponent("hello"),
|
||||
|
||||
@@ -298,7 +298,7 @@ async def test_private_outbound_message_preserves_bot_sender_and_receiver_user(
|
||||
outbound_message = send_service._build_outbound_session_message(
|
||||
message_sequence=MessageSequence(components=[TextComponent(text="你好")]),
|
||||
stream_id="test-session",
|
||||
display_message="你好",
|
||||
processed_plain_text="你好",
|
||||
)
|
||||
|
||||
assert outbound_message is not None
|
||||
@@ -329,7 +329,7 @@ async def test_group_outbound_message_preserves_bot_sender_and_target_group(
|
||||
outbound_message = send_service._build_outbound_session_message(
|
||||
message_sequence=MessageSequence(components=[TextComponent(text="大家好")]),
|
||||
stream_id="group-session",
|
||||
display_message="大家好",
|
||||
processed_plain_text="大家好",
|
||||
)
|
||||
|
||||
assert outbound_message is not None
|
||||
|
||||
@@ -114,14 +114,10 @@ def _merge_bucket_to_message(bucket: List[DatabaseMessages]) -> DatabaseMessages
|
||||
time=latest.time,
|
||||
chat_id=latest.chat_id,
|
||||
reply_to=latest.reply_to,
|
||||
interest_value=latest.interest_value,
|
||||
key_words=latest.key_words,
|
||||
key_words_lite=latest.key_words_lite,
|
||||
is_mentioned=latest.is_mentioned,
|
||||
is_at=latest.is_at,
|
||||
reply_probability_boost=latest.reply_probability_boost,
|
||||
processed_plain_text="\n".join(merged_texts) if merged_texts else latest.processed_plain_text,
|
||||
display_message=latest.display_message,
|
||||
priority_mode=latest.priority_mode,
|
||||
priority_info=latest.priority_info,
|
||||
additional_config=latest.additional_config,
|
||||
|
||||
@@ -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
|
||||
@@ -351,6 +351,7 @@ class ModelInfo(ConfigBase):
|
||||
Gemini 客户端会按自身支持的字段筛选并映射到 GenerateContentConfig、EmbedContentConfig 或音频请求配置中。"""
|
||||
|
||||
def model_post_init(self, context: Any = None):
|
||||
self.model_identifier = self.model_identifier.strip()
|
||||
if not self.model_identifier:
|
||||
raise ValueError(t("config.model_identifier_empty_generic"))
|
||||
if not self.name:
|
||||
|
||||
@@ -585,6 +585,9 @@ def _parse_tool_arguments(
|
||||
Raises:
|
||||
RespParseException: 当参数无法解析为字典时抛出。
|
||||
"""
|
||||
if not raw_arguments.strip():
|
||||
return {}
|
||||
|
||||
try:
|
||||
if parse_mode == ToolArgumentParseMode.STRICT:
|
||||
arguments: Any = json.loads(raw_arguments)
|
||||
|
||||
@@ -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