From a68dca1584bda1a9a271998839baf3365b78bcb2 Mon Sep 17 00:00:00 2001
From: SengokuCola <1026294844@qq.com>
Date: Sun, 19 Apr 2026 14:21:05 +0800
Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=89=8D=E6=8F=90=E9=86=92?=
=?UTF-8?q?=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=AF=B9=E8=A1=A8=E8=BE=BE[[*]]?=
=?UTF-8?q?=E7=9A=84=E6=AD=A3=E7=A1=AE=E8=BF=81=E7=A7=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bot.py | 6 +-
scripts/preview_reply_effect_scores.py | 230 ++++++++++++++++++----
src/config/legacy_migration.py | 16 +-
src/config/legacy_upgrade_confirmation.py | 223 +++++++++++++++++++++
src/config/official_configs.py | 9 -
5 files changed, 432 insertions(+), 52 deletions(-)
create mode 100644 src/config/legacy_upgrade_confirmation.py
diff --git a/bot.py b/bot.py
index 97bf15d4..1a13e46e 100644
--- a/bot.py
+++ b/bot.py
@@ -1,13 +1,11 @@
# raise RuntimeError("System Not Ready")
from pathlib import Path
-
from rich.traceback import install
import asyncio
import hashlib
import os
import platform
-
# import shutil
import subprocess
import sys
@@ -16,6 +14,7 @@ import traceback
from src.common.i18n import set_locale, t, tn
from src.common.logger import get_logger, initialize_logging, shutdown_logging
+from src.config.legacy_upgrade_confirmation import require_legacy_upgrade_confirmation
# 设置工作目录为脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -91,6 +90,7 @@ def run_runner_process():
# 此时应该作为 Runner 运行。
if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
if __name__ == "__main__":
+ require_legacy_upgrade_confirmation(Path(script_dir))
run_runner_process()
# 如果作为模块导入,不执行 Runner 逻辑,但也不应该执行下面的 Worker 逻辑
sys.exit(0)
@@ -103,6 +103,8 @@ if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
# 这是正常的,但为了避免重复的初始化日志,我们在 initialize_logging() 中添加了防重复机制
# 不过由于是不同进程,每个进程仍会初始化一次,这是预期的行为
+require_legacy_upgrade_confirmation(Path(script_dir))
+
from src.main import MainSystem # noqa
from src.manager.async_task_manager import async_task_manager # noqa
diff --git a/scripts/preview_reply_effect_scores.py b/scripts/preview_reply_effect_scores.py
index 359290ec..4b43b17e 100644
--- a/scripts/preview_reply_effect_scores.py
+++ b/scripts/preview_reply_effect_scores.py
@@ -14,6 +14,7 @@ DEFAULT_LOG_DIR = Path("logs") / "maisaka_reply_effect"
DEFAULT_MANUAL_DIR = Path("logs") / "maisaka_reply_effect_manual"
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8765
+DEFAULT_RECORD_LIMIT = 20
def normalize_name(value: str) -> str:
@@ -49,21 +50,14 @@ class ReplyEffectRepository:
for chat_dir in sorted(path for path in self.log_dir.iterdir() if path.is_dir()):
records = list(chat_dir.glob("*.json"))
- annotated_count = sum(1 for record_file in records if self._annotation_path(chat_dir.name, record_file).exists())
- finalized_count = 0
- pending_count = 0
- for record_file in records:
- payload = load_json_file(record_file)
- if payload.get("status") == "finalized":
- finalized_count += 1
- else:
- pending_count += 1
+ annotation_dir = self.manual_dir / normalize_name(chat_dir.name)
+ annotated_count = len(list(annotation_dir.glob("*.json"))) if annotation_dir.exists() else 0
chats.append(
{
"chat_id": chat_dir.name,
"record_count": len(records),
- "finalized_count": finalized_count,
- "pending_count": pending_count,
+ "finalized_count": None,
+ "pending_count": None,
"annotated_count": annotated_count,
}
)
@@ -75,9 +69,15 @@ class ReplyEffectRepository:
chat_id: str | None = None,
status: str = "",
annotated: str = "",
- ) -> list[dict[str, Any]]:
+ limit: int = DEFAULT_RECORD_LIMIT,
+ offset: int = 0,
+ ) -> dict[str, Any]:
records: list[dict[str, Any]] = []
- for record_file in self._iter_record_files(chat_id):
+ normalized_limit = max(1, min(1000, int(limit or DEFAULT_RECORD_LIMIT)))
+ normalized_offset = max(0, int(offset or 0))
+ matched_count = 0
+ has_more = False
+ for record_file in self._iter_record_files(chat_id, newest_first=True):
payload = load_json_file(record_file)
if not payload:
continue
@@ -88,16 +88,30 @@ class ReplyEffectRepository:
continue
if annotated == "no" and summary["manual"] is not None:
continue
+ matched_count += 1
+ if matched_count <= normalized_offset:
+ continue
+ if len(records) >= normalized_limit:
+ has_more = True
+ break
records.append(summary)
- return sorted(records, key=lambda item: str(item.get("created_at") or ""), reverse=True)
+ return {
+ "records": records,
+ "has_more": has_more,
+ "limit": normalized_limit,
+ "offset": normalized_offset,
+ }
- def get_record(self, chat_id: str, effect_id: str) -> dict[str, Any]:
+ def get_record(self, chat_id: str, effect_id: str, *, compact: bool = False) -> dict[str, Any]:
record_file = self._find_record_file(chat_id, effect_id)
if record_file is None:
return {}
payload = load_json_file(record_file)
if not payload:
return {}
+ self._strip_heavy_reply_metadata(payload)
+ if compact:
+ payload["context_snapshot"] = []
payload["_manual"] = self.get_annotation(chat_id, effect_id)
payload["_record_path"] = str(record_file)
return payload
@@ -114,7 +128,7 @@ class ReplyEffectRepository:
effect_id = normalize_name(str(payload.get("effect_id") or ""))
if not chat_id or chat_id == "unknown" or not effect_id or effect_id == "unknown":
raise ValueError("缺少 chat_id 或 effect_id")
- if self._find_record_file(chat_id, effect_id) is None:
+ if not self._record_exists(chat_id, effect_id):
raise ValueError("找不到对应的回复效果记录")
manual_score = payload.get("manual_score")
@@ -151,29 +165,56 @@ class ReplyEffectRepository:
write_json_file(self._annotation_path(chat_id, effect_id), annotation)
return annotation
- def _iter_record_files(self, chat_id: str | None = None) -> list[Path]:
+ def _iter_record_files(self, chat_id: str | None = None, *, newest_first: bool = False) -> list[Path]:
if not self.log_dir.exists():
return []
if chat_id:
chat_dir = self.log_dir / normalize_name(chat_id)
if not chat_dir.exists() or not chat_dir.is_dir():
return []
- return sorted(chat_dir.glob("*.json"))
+ return self._sort_record_files(chat_dir.glob("*.json"), newest_first=newest_first)
record_files: list[Path] = []
for chat_dir in self.log_dir.iterdir():
if chat_dir.is_dir():
record_files.extend(chat_dir.glob("*.json"))
- return record_files
+ return self._sort_record_files(record_files, newest_first=newest_first)
+
+ @staticmethod
+ def _sort_record_files(record_files: Any, *, newest_first: bool) -> list[Path]:
+ return sorted(
+ record_files,
+ key=lambda path: (path.stat().st_mtime, path.name),
+ reverse=newest_first,
+ )
def _find_record_file(self, chat_id: str, effect_id: str) -> Path | None:
normalized_effect_id = normalize_name(effect_id)
+ direct_match = self._direct_record_file(chat_id, effect_id)
+ if direct_match is not None:
+ return direct_match
for record_file in self._iter_record_files(chat_id):
payload = load_json_file(record_file)
if normalize_name(str(payload.get("effect_id") or "")) == normalized_effect_id:
return record_file
return None
+ def _record_exists(self, chat_id: str, effect_id: str) -> bool:
+ if self._direct_record_file(chat_id, effect_id) is not None:
+ return True
+ return self._find_record_file(chat_id, effect_id) is not None
+
+ def _direct_record_file(self, chat_id: str, effect_id: str) -> Path | None:
+ chat_dir = self.log_dir / normalize_name(chat_id)
+ if not chat_dir.exists() or not chat_dir.is_dir():
+ return None
+ normalized_effect_id = normalize_name(effect_id)
+ matches = sorted(chat_dir.glob(f"*_{normalized_effect_id}.json"))
+ if not matches:
+ exact_path = chat_dir / f"{normalized_effect_id}.json"
+ return exact_path if exact_path.exists() else None
+ return matches[-1]
+
def _annotation_path(self, chat_id: str, record_file_or_effect_id: Path | str) -> Path:
if isinstance(record_file_or_effect_id, Path):
payload = load_json_file(record_file_or_effect_id)
@@ -214,6 +255,24 @@ class ReplyEffectRepository:
return normalized_text
return f"{normalized_text[: limit - 1]}…"
+ @staticmethod
+ def _strip_heavy_reply_metadata(payload: dict[str, Any]) -> None:
+ reply = payload.get("reply")
+ if not isinstance(reply, dict):
+ return
+ metadata = reply.get("reply_metadata")
+ if not isinstance(metadata, dict):
+ return
+ monitor_detail = metadata.get("monitor_detail")
+ if not isinstance(monitor_detail, dict):
+ return
+ compact_detail: dict[str, Any] = {}
+ for key in ("metrics", "output_text", "extra_sections"):
+ value = monitor_detail.get(key)
+ if value:
+ compact_detail[key] = value
+ metadata["monitor_detail"] = compact_detail
+
class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
repository: ReplyEffectRepository
@@ -232,14 +291,17 @@ class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
chat_id=self._first(query, "chat_id"),
status=self._first(query, "status"),
annotated=self._first(query, "annotated"),
+ limit=self._int(query, "limit", DEFAULT_RECORD_LIMIT),
+ offset=self._int(query, "offset", 0),
)
- self._send_json({"records": records})
+ self._send_json(records)
return
if parsed.path == "/api/record":
query = parse_qs(parsed.query)
record = self.repository.get_record(
normalize_name(self._first(query, "chat_id")),
normalize_name(self._first(query, "effect_id")),
+ compact=self._first(query, "compact") in {"1", "true", "yes"},
)
if not record:
self._send_json({"error": "record not found"}, status=404)
@@ -360,6 +422,13 @@ class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
values = query.get(key) or [""]
return values[0]
+ @classmethod
+ def _int(cls, query: dict[str, list[str]], key: str, default: int) -> int:
+ try:
+ return int(cls._first(query, key) or default)
+ except ValueError:
+ return default
+
INDEX_HTML = r"""
@@ -563,7 +632,8 @@ INDEX_HTML = r"""
document.getElementById("detailPane").innerHTML = "选择一条记录查看详情";
}
- async function loadRecords() {
+ async function loadRecords(offset = recordOffset) {
+ recordOffset = Math.max(0, offset);
const params = new URLSearchParams();
if (selectedChat) params.set("chat_id", selectedChat);
const status = document.getElementById("statusFilter").value;
@@ -601,7 +671,7 @@ INDEX_HTML = r"""
selectedEffect = effectId;
renderChats();
renderRecords();
- const data = await api(`/api/record?chat_id=${encodeURIComponent(chatId)}&effect_id=${encodeURIComponent(effectId)}`);
+ const data = await api(`/api/record?chat_id=${encodeURIComponent(chatId)}&effect_id=${encodeURIComponent(effectId)}&compact=1`);
renderDetail(data.record);
}
@@ -891,17 +961,18 @@ INDEX_HTML_V2 = r"""
${escapeHtml(JSON.stringify(record, null, 2))}
-