迁移前提醒,以及对表达[[*]]的正确迁移
This commit is contained in:
6
bot.py
6
bot.py
@@ -1,13 +1,11 @@
|
|||||||
# raise RuntimeError("System Not Ready")
|
# raise RuntimeError("System Not Ready")
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from rich.traceback import install
|
from rich.traceback import install
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
# import shutil
|
# import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -16,6 +14,7 @@ import traceback
|
|||||||
|
|
||||||
from src.common.i18n import set_locale, t, tn
|
from src.common.i18n import set_locale, t, tn
|
||||||
from src.common.logger import get_logger, initialize_logging, shutdown_logging
|
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__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -91,6 +90,7 @@ def run_runner_process():
|
|||||||
# 此时应该作为 Runner 运行。
|
# 此时应该作为 Runner 运行。
|
||||||
if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
|
if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
require_legacy_upgrade_confirmation(Path(script_dir))
|
||||||
run_runner_process()
|
run_runner_process()
|
||||||
# 如果作为模块导入,不执行 Runner 逻辑,但也不应该执行下面的 Worker 逻辑
|
# 如果作为模块导入,不执行 Runner 逻辑,但也不应该执行下面的 Worker 逻辑
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -103,6 +103,8 @@ if os.environ.get("MAIBOT_WORKER_PROCESS") != "1":
|
|||||||
# 这是正常的,但为了避免重复的初始化日志,我们在 initialize_logging() 中添加了防重复机制
|
# 这是正常的,但为了避免重复的初始化日志,我们在 initialize_logging() 中添加了防重复机制
|
||||||
# 不过由于是不同进程,每个进程仍会初始化一次,这是预期的行为
|
# 不过由于是不同进程,每个进程仍会初始化一次,这是预期的行为
|
||||||
|
|
||||||
|
require_legacy_upgrade_confirmation(Path(script_dir))
|
||||||
|
|
||||||
from src.main import MainSystem # noqa
|
from src.main import MainSystem # noqa
|
||||||
from src.manager.async_task_manager import async_task_manager # noqa
|
from src.manager.async_task_manager import async_task_manager # noqa
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ DEFAULT_LOG_DIR = Path("logs") / "maisaka_reply_effect"
|
|||||||
DEFAULT_MANUAL_DIR = Path("logs") / "maisaka_reply_effect_manual"
|
DEFAULT_MANUAL_DIR = Path("logs") / "maisaka_reply_effect_manual"
|
||||||
DEFAULT_HOST = "127.0.0.1"
|
DEFAULT_HOST = "127.0.0.1"
|
||||||
DEFAULT_PORT = 8765
|
DEFAULT_PORT = 8765
|
||||||
|
DEFAULT_RECORD_LIMIT = 20
|
||||||
|
|
||||||
|
|
||||||
def normalize_name(value: str) -> str:
|
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()):
|
for chat_dir in sorted(path for path in self.log_dir.iterdir() if path.is_dir()):
|
||||||
records = list(chat_dir.glob("*.json"))
|
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())
|
annotation_dir = self.manual_dir / normalize_name(chat_dir.name)
|
||||||
finalized_count = 0
|
annotated_count = len(list(annotation_dir.glob("*.json"))) if annotation_dir.exists() else 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
|
|
||||||
chats.append(
|
chats.append(
|
||||||
{
|
{
|
||||||
"chat_id": chat_dir.name,
|
"chat_id": chat_dir.name,
|
||||||
"record_count": len(records),
|
"record_count": len(records),
|
||||||
"finalized_count": finalized_count,
|
"finalized_count": None,
|
||||||
"pending_count": pending_count,
|
"pending_count": None,
|
||||||
"annotated_count": annotated_count,
|
"annotated_count": annotated_count,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -75,9 +69,15 @@ class ReplyEffectRepository:
|
|||||||
chat_id: str | None = None,
|
chat_id: str | None = None,
|
||||||
status: str = "",
|
status: str = "",
|
||||||
annotated: str = "",
|
annotated: str = "",
|
||||||
) -> list[dict[str, Any]]:
|
limit: int = DEFAULT_RECORD_LIMIT,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> dict[str, Any]:
|
||||||
records: list[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)
|
payload = load_json_file(record_file)
|
||||||
if not payload:
|
if not payload:
|
||||||
continue
|
continue
|
||||||
@@ -88,16 +88,30 @@ class ReplyEffectRepository:
|
|||||||
continue
|
continue
|
||||||
if annotated == "no" and summary["manual"] is not None:
|
if annotated == "no" and summary["manual"] is not None:
|
||||||
continue
|
continue
|
||||||
|
matched_count += 1
|
||||||
|
if matched_count <= normalized_offset:
|
||||||
|
continue
|
||||||
|
if len(records) >= normalized_limit:
|
||||||
|
has_more = True
|
||||||
|
break
|
||||||
records.append(summary)
|
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)
|
record_file = self._find_record_file(chat_id, effect_id)
|
||||||
if record_file is None:
|
if record_file is None:
|
||||||
return {}
|
return {}
|
||||||
payload = load_json_file(record_file)
|
payload = load_json_file(record_file)
|
||||||
if not payload:
|
if not payload:
|
||||||
return {}
|
return {}
|
||||||
|
self._strip_heavy_reply_metadata(payload)
|
||||||
|
if compact:
|
||||||
|
payload["context_snapshot"] = []
|
||||||
payload["_manual"] = self.get_annotation(chat_id, effect_id)
|
payload["_manual"] = self.get_annotation(chat_id, effect_id)
|
||||||
payload["_record_path"] = str(record_file)
|
payload["_record_path"] = str(record_file)
|
||||||
return payload
|
return payload
|
||||||
@@ -114,7 +128,7 @@ class ReplyEffectRepository:
|
|||||||
effect_id = normalize_name(str(payload.get("effect_id") or ""))
|
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":
|
if not chat_id or chat_id == "unknown" or not effect_id or effect_id == "unknown":
|
||||||
raise ValueError("缺少 chat_id 或 effect_id")
|
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("找不到对应的回复效果记录")
|
raise ValueError("找不到对应的回复效果记录")
|
||||||
|
|
||||||
manual_score = payload.get("manual_score")
|
manual_score = payload.get("manual_score")
|
||||||
@@ -151,29 +165,56 @@ class ReplyEffectRepository:
|
|||||||
write_json_file(self._annotation_path(chat_id, effect_id), annotation)
|
write_json_file(self._annotation_path(chat_id, effect_id), annotation)
|
||||||
return 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():
|
if not self.log_dir.exists():
|
||||||
return []
|
return []
|
||||||
if chat_id:
|
if chat_id:
|
||||||
chat_dir = self.log_dir / normalize_name(chat_id)
|
chat_dir = self.log_dir / normalize_name(chat_id)
|
||||||
if not chat_dir.exists() or not chat_dir.is_dir():
|
if not chat_dir.exists() or not chat_dir.is_dir():
|
||||||
return []
|
return []
|
||||||
return sorted(chat_dir.glob("*.json"))
|
return self._sort_record_files(chat_dir.glob("*.json"), newest_first=newest_first)
|
||||||
|
|
||||||
record_files: list[Path] = []
|
record_files: list[Path] = []
|
||||||
for chat_dir in self.log_dir.iterdir():
|
for chat_dir in self.log_dir.iterdir():
|
||||||
if chat_dir.is_dir():
|
if chat_dir.is_dir():
|
||||||
record_files.extend(chat_dir.glob("*.json"))
|
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:
|
def _find_record_file(self, chat_id: str, effect_id: str) -> Path | None:
|
||||||
normalized_effect_id = normalize_name(effect_id)
|
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):
|
for record_file in self._iter_record_files(chat_id):
|
||||||
payload = load_json_file(record_file)
|
payload = load_json_file(record_file)
|
||||||
if normalize_name(str(payload.get("effect_id") or "")) == normalized_effect_id:
|
if normalize_name(str(payload.get("effect_id") or "")) == normalized_effect_id:
|
||||||
return record_file
|
return record_file
|
||||||
return None
|
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:
|
def _annotation_path(self, chat_id: str, record_file_or_effect_id: Path | str) -> Path:
|
||||||
if isinstance(record_file_or_effect_id, Path):
|
if isinstance(record_file_or_effect_id, Path):
|
||||||
payload = load_json_file(record_file_or_effect_id)
|
payload = load_json_file(record_file_or_effect_id)
|
||||||
@@ -214,6 +255,24 @@ class ReplyEffectRepository:
|
|||||||
return normalized_text
|
return normalized_text
|
||||||
return f"{normalized_text[: limit - 1]}…"
|
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):
|
class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
|
||||||
repository: ReplyEffectRepository
|
repository: ReplyEffectRepository
|
||||||
@@ -232,14 +291,17 @@ class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
|
|||||||
chat_id=self._first(query, "chat_id"),
|
chat_id=self._first(query, "chat_id"),
|
||||||
status=self._first(query, "status"),
|
status=self._first(query, "status"),
|
||||||
annotated=self._first(query, "annotated"),
|
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
|
return
|
||||||
if parsed.path == "/api/record":
|
if parsed.path == "/api/record":
|
||||||
query = parse_qs(parsed.query)
|
query = parse_qs(parsed.query)
|
||||||
record = self.repository.get_record(
|
record = self.repository.get_record(
|
||||||
normalize_name(self._first(query, "chat_id")),
|
normalize_name(self._first(query, "chat_id")),
|
||||||
normalize_name(self._first(query, "effect_id")),
|
normalize_name(self._first(query, "effect_id")),
|
||||||
|
compact=self._first(query, "compact") in {"1", "true", "yes"},
|
||||||
)
|
)
|
||||||
if not record:
|
if not record:
|
||||||
self._send_json({"error": "record not found"}, status=404)
|
self._send_json({"error": "record not found"}, status=404)
|
||||||
@@ -360,6 +422,13 @@ class ReplyEffectPreviewHandler(BaseHTTPRequestHandler):
|
|||||||
values = query.get(key) or [""]
|
values = query.get(key) or [""]
|
||||||
return values[0]
|
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"""<!doctype html>
|
INDEX_HTML = r"""<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -563,7 +632,8 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
document.getElementById("detailPane").innerHTML = "选择一条记录查看详情";
|
document.getElementById("detailPane").innerHTML = "选择一条记录查看详情";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecords() {
|
async function loadRecords(offset = recordOffset) {
|
||||||
|
recordOffset = Math.max(0, offset);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (selectedChat) params.set("chat_id", selectedChat);
|
if (selectedChat) params.set("chat_id", selectedChat);
|
||||||
const status = document.getElementById("statusFilter").value;
|
const status = document.getElementById("statusFilter").value;
|
||||||
@@ -601,7 +671,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
selectedEffect = effectId;
|
selectedEffect = effectId;
|
||||||
renderChats();
|
renderChats();
|
||||||
renderRecords();
|
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);
|
renderDetail(data.record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,17 +961,18 @@ INDEX_HTML_V2 = r"""<!doctype html>
|
|||||||
<section class="content">
|
<section class="content">
|
||||||
<div id="browsePanel">
|
<div id="browsePanel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<select id="statusFilter" onchange="loadRecords()">
|
<select id="statusFilter" onchange="loadRecords(0)">
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
<option value="finalized">已完成</option>
|
<option value="finalized">已完成</option>
|
||||||
<option value="pending">观察中</option>
|
<option value="pending">观察中</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="annotationFilter" onchange="loadRecords()">
|
<select id="annotationFilter" onchange="loadRecords(0)">
|
||||||
<option value="">全部标注</option>
|
<option value="">全部标注</option>
|
||||||
<option value="yes">已人工评分</option>
|
<option value="yes">已人工评分</option>
|
||||||
<option value="no">未人工评分</option>
|
<option value="no">未人工评分</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="recordPager" class="toolbar"></div>
|
||||||
<div id="recordList"></div>
|
<div id="recordList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ratingPanel" class="hidden">
|
<div id="ratingPanel" class="hidden">
|
||||||
@@ -911,6 +982,7 @@ INDEX_HTML_V2 = r"""<!doctype html>
|
|||||||
<button class="secondary" onclick="moveRating(1)">下一条</button>
|
<button class="secondary" onclick="moveRating(1)">下一条</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="ratingQueueInfo" class="meta"></div>
|
<div id="ratingQueueInfo" class="meta"></div>
|
||||||
|
<div id="ratingPager" class="toolbar"></div>
|
||||||
<div id="ratingQueueList"></div>
|
<div id="ratingQueueList"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1095,6 +1167,7 @@ INDEX_HTML_V2 = r"""<!doctype html>
|
|||||||
if (selectedChat) params.set("chat_id", selectedChat);
|
if (selectedChat) params.set("chat_id", selectedChat);
|
||||||
params.set("status", "finalized");
|
params.set("status", "finalized");
|
||||||
params.set("annotated", "no");
|
params.set("annotated", "no");
|
||||||
|
params.set("limit", String(RATING_QUEUE_LIMIT));
|
||||||
const data = await api(`/api/records?${params.toString()}`);
|
const data = await api(`/api/records?${params.toString()}`);
|
||||||
ratingQueue = data.records || [];
|
ratingQueue = data.records || [];
|
||||||
ratingIndex = 0;
|
ratingIndex = 0;
|
||||||
@@ -1671,6 +1744,12 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
let selectedFivePointScore = 0;
|
let selectedFivePointScore = 0;
|
||||||
let currentTargetMessageId = "";
|
let currentTargetMessageId = "";
|
||||||
let currentMessageIndex = new Map();
|
let currentMessageIndex = new Map();
|
||||||
|
let recordOffset = 0;
|
||||||
|
let recordHasMore = false;
|
||||||
|
let ratingOffset = 0;
|
||||||
|
let ratingHasMore = false;
|
||||||
|
const RECORD_LIST_LIMIT = 20;
|
||||||
|
const RATING_QUEUE_LIMIT = 20;
|
||||||
|
|
||||||
async function api(path, options) {
|
async function api(path, options) {
|
||||||
const res = await fetch(path, options);
|
const res = await fetch(path, options);
|
||||||
@@ -1686,9 +1765,9 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
renderChats();
|
renderChats();
|
||||||
renderRateChatSelect();
|
renderRateChatSelect();
|
||||||
if (activeMode === "rate") {
|
if (activeMode === "rate") {
|
||||||
await loadRatingQueue();
|
await loadRatingQueue(0);
|
||||||
} else {
|
} else {
|
||||||
await loadRecords();
|
await loadRecords(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,10 +1779,10 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
document.getElementById("browsePanel").classList.toggle("hidden", mode !== "browse");
|
document.getElementById("browsePanel").classList.toggle("hidden", mode !== "browse");
|
||||||
document.getElementById("ratingPanel").classList.toggle("hidden", mode !== "rate");
|
document.getElementById("ratingPanel").classList.toggle("hidden", mode !== "rate");
|
||||||
if (mode === "rate") {
|
if (mode === "rate") {
|
||||||
loadRatingQueue();
|
loadRatingQueue(0);
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove("rate-drawer-collapsed");
|
document.body.classList.remove("rate-drawer-collapsed");
|
||||||
loadRecords();
|
loadRecords(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1719,7 +1798,7 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
<div class="chat-item ${chat.chat_id === selectedChat ? "active" : ""}"
|
<div class="chat-item ${chat.chat_id === selectedChat ? "active" : ""}"
|
||||||
onclick="selectChat('${escapeAttr(chat.chat_id)}')">
|
onclick="selectChat('${escapeAttr(chat.chat_id)}')">
|
||||||
<div class="chat-id">${escapeHtml(chat.chat_id)}</div>
|
<div class="chat-id">${escapeHtml(chat.chat_id)}</div>
|
||||||
<div class="meta">记录 ${chat.record_count} | 完成 ${chat.finalized_count} | 人工 ${chat.annotated_count}</div>
|
<div class="meta">记录 ${chat.record_count} | 人工 ${chat.annotated_count}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join("") || `<div class="empty">没有聊天流</div>`;
|
`).join("") || `<div class="empty">没有聊天流</div>`;
|
||||||
}
|
}
|
||||||
@@ -1741,9 +1820,9 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
renderRateChatSelect();
|
renderRateChatSelect();
|
||||||
document.getElementById("detailPane").innerHTML = "选择一条记录查看详情";
|
document.getElementById("detailPane").innerHTML = "选择一条记录查看详情";
|
||||||
if (activeMode === "rate") {
|
if (activeMode === "rate") {
|
||||||
await loadRatingQueue();
|
await loadRatingQueue(0);
|
||||||
} else {
|
} else {
|
||||||
await loadRecords();
|
await loadRecords(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1760,9 +1839,13 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
const annotated = document.getElementById("annotationFilter").value;
|
const annotated = document.getElementById("annotationFilter").value;
|
||||||
if (status) params.set("status", status);
|
if (status) params.set("status", status);
|
||||||
if (annotated) params.set("annotated", annotated);
|
if (annotated) params.set("annotated", annotated);
|
||||||
|
params.set("limit", String(RECORD_LIST_LIMIT));
|
||||||
|
params.set("offset", String(recordOffset));
|
||||||
const data = await api(`/api/records?${params.toString()}`);
|
const data = await api(`/api/records?${params.toString()}`);
|
||||||
records = data.records || [];
|
records = data.records || [];
|
||||||
|
recordHasMore = Boolean(data.has_more);
|
||||||
renderRecords();
|
renderRecords();
|
||||||
|
renderRecordPager();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecords() {
|
function renderRecords() {
|
||||||
@@ -1788,6 +1871,17 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
}).join("") || `<div class="empty">没有记录</div>`;
|
}).join("") || `<div class="empty">没有记录</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRecordPager() {
|
||||||
|
const pager = document.getElementById("recordPager");
|
||||||
|
if (!pager) return;
|
||||||
|
const pageNumber = Math.floor(recordOffset / RECORD_LIST_LIMIT) + 1;
|
||||||
|
pager.innerHTML = `
|
||||||
|
<button class="secondary" ${recordOffset <= 0 ? "disabled" : ""} onclick="loadRecords(${Math.max(0, recordOffset - RECORD_LIST_LIMIT)})">上一页</button>
|
||||||
|
<span class="meta">第 ${pageNumber} 页,每页 ${RECORD_LIST_LIMIT} 条</span>
|
||||||
|
<button class="secondary" ${!recordHasMore ? "disabled" : ""} onclick="loadRecords(${recordOffset + RECORD_LIST_LIMIT})">下一页</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDetail(chatId, effectId) {
|
async function loadDetail(chatId, effectId) {
|
||||||
selectedChat = chatId;
|
selectedChat = chatId;
|
||||||
selectedEffect = effectId;
|
selectedEffect = effectId;
|
||||||
@@ -1803,10 +1897,9 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
const manual = record._manual || {};
|
const manual = record._manual || {};
|
||||||
const followups = record.followup_messages || [];
|
const followups = record.followup_messages || [];
|
||||||
currentTargetMessageId = String(reply.target_message_id || "");
|
currentTargetMessageId = String(reply.target_message_id || "");
|
||||||
const context = normalizeContextMessages(record.context_snapshot || []);
|
|
||||||
const normalizedFollowups = normalizeFollowupMessages(followups);
|
const normalizedFollowups = normalizeFollowupMessages(followups);
|
||||||
const botReply = normalizeBotReply(reply);
|
const botReply = normalizeBotReply(reply);
|
||||||
buildCurrentMessageIndex(context, botReply, normalizedFollowups);
|
buildCurrentMessageIndex([], botReply, normalizedFollowups);
|
||||||
selectedFivePointScore = Number(manual.manual_score_5 || score100ToFive(manual.manual_score) || 0);
|
selectedFivePointScore = Number(manual.manual_score_5 || score100ToFive(manual.manual_score) || 0);
|
||||||
document.getElementById("detailPane").innerHTML = `
|
document.getElementById("detailPane").innerHTML = `
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@@ -1842,22 +1935,23 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
<h2>后续消息</h2>
|
<h2>后续消息</h2>
|
||||||
${renderMessageCards(normalizedFollowups, "暂无")}
|
${renderMessageCards(normalizedFollowups, "暂无")}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
|
||||||
<h2>完整 JSON</h2>
|
|
||||||
<pre>${escapeHtml(JSON.stringify(record, null, 2))}</pre>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRatingQueue() {
|
async function loadRatingQueue(offset = ratingOffset) {
|
||||||
|
ratingOffset = Math.max(0, offset);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (selectedChat) params.set("chat_id", selectedChat);
|
if (selectedChat) params.set("chat_id", selectedChat);
|
||||||
params.set("status", "finalized");
|
params.set("status", "finalized");
|
||||||
params.set("annotated", "no");
|
params.set("annotated", "no");
|
||||||
|
params.set("limit", String(RATING_QUEUE_LIMIT));
|
||||||
|
params.set("offset", String(ratingOffset));
|
||||||
const data = await api(`/api/records?${params.toString()}`);
|
const data = await api(`/api/records?${params.toString()}`);
|
||||||
ratingQueue = data.records || [];
|
ratingQueue = data.records || [];
|
||||||
|
ratingHasMore = Boolean(data.has_more);
|
||||||
ratingIndex = 0;
|
ratingIndex = 0;
|
||||||
renderRatingQueue();
|
renderRatingQueue();
|
||||||
|
renderRatingPager();
|
||||||
if (ratingQueue.length) {
|
if (ratingQueue.length) {
|
||||||
await loadRatingDetail(0);
|
await loadRatingDetail(0);
|
||||||
} else {
|
} else {
|
||||||
@@ -1881,6 +1975,17 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
`).join("") || `<div class="empty">没有待评分记录</div>`;
|
`).join("") || `<div class="empty">没有待评分记录</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRatingPager() {
|
||||||
|
const pager = document.getElementById("ratingPager");
|
||||||
|
if (!pager) return;
|
||||||
|
const pageNumber = Math.floor(ratingOffset / RATING_QUEUE_LIMIT) + 1;
|
||||||
|
pager.innerHTML = `
|
||||||
|
<button class="secondary" ${ratingOffset <= 0 ? "disabled" : ""} onclick="loadRatingQueue(${Math.max(0, ratingOffset - RATING_QUEUE_LIMIT)})">上一页</button>
|
||||||
|
<span class="meta">第 ${pageNumber} 页,每页 ${RATING_QUEUE_LIMIT} 条</span>
|
||||||
|
<button class="secondary" ${!ratingHasMore ? "disabled" : ""} onclick="loadRatingQueue(${ratingOffset + RATING_QUEUE_LIMIT})">下一页</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRatingDetail(index) {
|
async function loadRatingDetail(index) {
|
||||||
if (!ratingQueue.length) return;
|
if (!ratingQueue.length) return;
|
||||||
ratingIndex = Math.max(0, Math.min(index, ratingQueue.length - 1));
|
ratingIndex = Math.max(0, Math.min(index, ratingQueue.length - 1));
|
||||||
@@ -1999,9 +2104,10 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (activeMode === "rate" && moveNext) {
|
if (activeMode === "rate" && moveNext) {
|
||||||
await loadRatingQueue();
|
await advanceRatingQueueAfterSave(effectId);
|
||||||
} else {
|
} else {
|
||||||
await reloadAll();
|
markRecordAnnotated(effectId, selectedFivePointScore);
|
||||||
|
renderRecords();
|
||||||
await loadDetail(chatId, effectId);
|
await loadDetail(chatId, effectId);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2009,6 +2115,50 @@ INDEX_HTML_V3 = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function advanceRatingQueueAfterSave(effectId) {
|
||||||
|
const savedIndex = ratingQueue.findIndex(record => record.effect_id === effectId);
|
||||||
|
if (savedIndex >= 0) {
|
||||||
|
ratingQueue.splice(savedIndex, 1);
|
||||||
|
}
|
||||||
|
if (!ratingQueue.length) {
|
||||||
|
if (ratingHasMore) {
|
||||||
|
await loadRatingQueue(ratingOffset);
|
||||||
|
refreshChatsQuietly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderRatingQueue();
|
||||||
|
renderRatingPager();
|
||||||
|
renderEmptyRatingDetail();
|
||||||
|
refreshChatsQuietly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ratingIndex = Math.min(savedIndex >= 0 ? savedIndex : ratingIndex, ratingQueue.length - 1);
|
||||||
|
renderRatingQueue();
|
||||||
|
renderRatingPager();
|
||||||
|
await loadRatingDetail(ratingIndex);
|
||||||
|
refreshChatsQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRecordAnnotated(effectId, score) {
|
||||||
|
const item = records.find(record => record.effect_id === effectId);
|
||||||
|
if (!item) return;
|
||||||
|
item.manual = {
|
||||||
|
manual_score_5: score,
|
||||||
|
evaluator: currentEvaluator(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshChatsQuietly() {
|
||||||
|
try {
|
||||||
|
const data = await api("/api/chats");
|
||||||
|
chats = data.chats || [];
|
||||||
|
renderChats();
|
||||||
|
renderRateChatSelect();
|
||||||
|
} catch (_err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeContextMessages(context) {
|
function normalizeContextMessages(context) {
|
||||||
const items = Array.isArray(context) ? context : [];
|
const items = Array.isArray(context) ? context : [];
|
||||||
return items.filter(item => !isToolContextMessage(item)).map((item, index) => {
|
return items.filter(item => !isToolContextMessage(item)).map((item, index) => {
|
||||||
|
|||||||
@@ -84,6 +84,20 @@ def _parse_triplet_target(s: str) -> Optional[dict[str, str]]:
|
|||||||
return {"platform": platform, "item_id": item_id, "rule_type": rule_type}
|
return {"platform": platform, "item_id": item_id, "rule_type": rule_type}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_expression_group_target(s: str) -> Optional[dict[str, str]]:
|
||||||
|
"""
|
||||||
|
解析表达互通组目标,兼容旧版 "*" 全局共享标记。
|
||||||
|
"""
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized_value = s.strip()
|
||||||
|
if normalized_value == "*":
|
||||||
|
return {"platform": "*", "item_id": "*", "rule_type": "group"}
|
||||||
|
|
||||||
|
return _parse_triplet_target(normalized_value)
|
||||||
|
|
||||||
|
|
||||||
def _parse_enable_disable(v: Any) -> Optional[bool]:
|
def _parse_enable_disable(v: Any) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
兼容旧值 "enable"/"disable" 以及 bool。
|
兼容旧值 "enable"/"disable" 以及 bool。
|
||||||
@@ -174,7 +188,7 @@ def _migrate_expression_groups(expr: dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
targets: list[dict[str, str]] = []
|
targets: list[dict[str, str]] = []
|
||||||
for item in group_items:
|
for item in group_items:
|
||||||
parsed = _parse_triplet_target(str(item))
|
parsed = _parse_expression_group_target(str(item))
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
return False
|
return False
|
||||||
targets.append(parsed)
|
targets.append(parsed)
|
||||||
|
|||||||
223
src/config/legacy_upgrade_confirmation.py
Normal file
223
src/config/legacy_upgrade_confirmation.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LEGACY_UPGRADE_CONFIRM_ENV = "MAIBOT_LEGACY_0X_UPGRADE_CONFIRMED"
|
||||||
|
LEGACY_0X_BOT_CONFIG_BOUNDARY = "8.9.4"
|
||||||
|
LEGACY_0X_MODEL_CONFIG_BOUNDARY = "1.14.1"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_semver(version: str) -> tuple[int, int, int] | None:
|
||||||
|
"""解析三段式版本号。"""
|
||||||
|
parts = version.strip().split(".")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return tuple(int(part) for part in parts) # type: ignore[return-value]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_config_constant(project_root: Path, name: str) -> str | None:
|
||||||
|
"""从配置模块源码读取版本常量,避免提前触发配置加载和自动迁移。"""
|
||||||
|
config_source_path = project_root / "src" / "config" / "config.py"
|
||||||
|
try:
|
||||||
|
config_source = config_source_path.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pattern = rf'^{re.escape(name)}:\s*str\s*=\s*"([^"]+)"'
|
||||||
|
match = re.search(pattern, config_source, flags=re.MULTILINE)
|
||||||
|
if match is None:
|
||||||
|
return None
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_inner_config_version(config_path: Path) -> str | None:
|
||||||
|
"""读取配置文件 [inner].version,失败时返回 None。"""
|
||||||
|
if not config_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_text = config_path.read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
in_inner_table = False
|
||||||
|
for raw_line in config_text.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if line.startswith("[") and line.endswith("]"):
|
||||||
|
in_inner_table = line == "[inner]"
|
||||||
|
continue
|
||||||
|
if not in_inner_table:
|
||||||
|
continue
|
||||||
|
match = re.match(r'^version\s*=\s*"([^"]+)"', line)
|
||||||
|
if match is not None:
|
||||||
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_version_lower(current_version: str | None, target_version: str | None) -> bool:
|
||||||
|
current_parts = _parse_semver(current_version or "")
|
||||||
|
target_parts = _parse_semver(target_version or "")
|
||||||
|
if current_parts is None or target_parts is None:
|
||||||
|
return False
|
||||||
|
return current_parts < target_parts
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_legacy_config_confirmation(
|
||||||
|
current_version: str | None,
|
||||||
|
target_version: str | None,
|
||||||
|
legacy_boundary_version: str,
|
||||||
|
) -> bool:
|
||||||
|
"""判断配置更新是否属于 0.x -> 1.0.0 边界升级。"""
|
||||||
|
return _is_version_lower(current_version, legacy_boundary_version) and _is_version_lower(
|
||||||
|
current_version,
|
||||||
|
target_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_sqlite_schema(db_path: Path) -> tuple[int, dict[str, set[str]]] | None:
|
||||||
|
"""读取 SQLite user_version 与用户表列名,不创建新数据库文件。"""
|
||||||
|
if not db_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
database_uri = f"file:{db_path.as_posix()}?mode=ro"
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(database_uri, uri=True) as connection:
|
||||||
|
user_version_row = connection.execute("PRAGMA user_version").fetchone()
|
||||||
|
if user_version_row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
table_rows = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
AND name NOT LIKE 'sqlite_%'
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
tables: dict[str, set[str]] = {}
|
||||||
|
for (table_name,) in table_rows:
|
||||||
|
table_name = str(table_name)
|
||||||
|
escaped_table_name = table_name.replace('"', '""')
|
||||||
|
column_rows = connection.execute(f'PRAGMA table_info("{escaped_table_name}")').fetchall()
|
||||||
|
tables[table_name] = {str(row[1]) for row in column_rows}
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return int(user_version_row[0]), tables
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_legacy_0x_database(db_path: Path) -> bool:
|
||||||
|
"""检测旧版 0.x 数据库结构。"""
|
||||||
|
schema = _load_sqlite_schema(db_path)
|
||||||
|
if schema is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_version, tables = schema
|
||||||
|
if user_version == 1:
|
||||||
|
return True
|
||||||
|
if user_version > 1 or not tables:
|
||||||
|
return False
|
||||||
|
|
||||||
|
legacy_exclusive_tables = {
|
||||||
|
"chat_streams",
|
||||||
|
"emoji",
|
||||||
|
"emoji_description_cache",
|
||||||
|
"expression",
|
||||||
|
"group_info",
|
||||||
|
"image_descriptions",
|
||||||
|
"jargon",
|
||||||
|
"messages",
|
||||||
|
"thinking_back",
|
||||||
|
}
|
||||||
|
if legacy_exclusive_tables.intersection(tables):
|
||||||
|
return True
|
||||||
|
|
||||||
|
legacy_shared_markers = (
|
||||||
|
("action_records", ("chat_id", "time")),
|
||||||
|
("chat_history", ("chat_id", "original_text")),
|
||||||
|
("images", ("emoji_hash", "path", "type")),
|
||||||
|
("llm_usage", ("model_api_provider", "status")),
|
||||||
|
("online_time", ("duration",)),
|
||||||
|
("person_info", ("nickname", "group_nick_name")),
|
||||||
|
)
|
||||||
|
for table_name, required_columns in legacy_shared_markers:
|
||||||
|
table_columns = tables.get(table_name)
|
||||||
|
if table_columns is not None and all(column_name in table_columns for column_name in required_columns):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def collect_legacy_upgrade_reasons(project_root: Path) -> list[str]:
|
||||||
|
"""收集启动前需要用户确认的 0.x 升级风险项。"""
|
||||||
|
reasons: list[str] = []
|
||||||
|
|
||||||
|
db_path = project_root / "data" / "MaiBot.db"
|
||||||
|
if _detect_legacy_0x_database(db_path):
|
||||||
|
reasons.append(f"检测到旧版 0.x 数据库结构,将更新数据库:{db_path}")
|
||||||
|
|
||||||
|
bot_config_path = project_root / "config" / "bot_config.toml"
|
||||||
|
bot_config_version = _read_inner_config_version(bot_config_path)
|
||||||
|
target_bot_config_version = _read_config_constant(project_root, "CONFIG_VERSION")
|
||||||
|
if _needs_legacy_config_confirmation(
|
||||||
|
bot_config_version,
|
||||||
|
target_bot_config_version,
|
||||||
|
LEGACY_0X_BOT_CONFIG_BOUNDARY,
|
||||||
|
):
|
||||||
|
reasons.append(
|
||||||
|
"检测到主配置文件版本较旧,将更新配置文件:"
|
||||||
|
f"{bot_config_path} ({bot_config_version} -> {target_bot_config_version})"
|
||||||
|
)
|
||||||
|
|
||||||
|
model_config_path = project_root / "config" / "model_config.toml"
|
||||||
|
model_config_version = _read_inner_config_version(model_config_path)
|
||||||
|
target_model_config_version = _read_config_constant(project_root, "MODEL_CONFIG_VERSION")
|
||||||
|
if _needs_legacy_config_confirmation(
|
||||||
|
model_config_version,
|
||||||
|
target_model_config_version,
|
||||||
|
LEGACY_0X_MODEL_CONFIG_BOUNDARY,
|
||||||
|
):
|
||||||
|
reasons.append(
|
||||||
|
"检测到模型配置文件版本较旧,将更新配置文件:"
|
||||||
|
f"{model_config_path} ({model_config_version} -> {target_model_config_version})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return reasons
|
||||||
|
|
||||||
|
|
||||||
|
def require_legacy_upgrade_confirmation(project_root: Path) -> None:
|
||||||
|
"""在执行 0.x 升级迁移前要求用户显式确认。"""
|
||||||
|
if os.getenv(LEGACY_UPGRADE_CONFIRM_ENV) == "1":
|
||||||
|
return
|
||||||
|
|
||||||
|
reasons = collect_legacy_upgrade_reasons(project_root)
|
||||||
|
if not reasons:
|
||||||
|
return
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 72)
|
||||||
|
print("MaiBot 升级提示")
|
||||||
|
print("检测到当前实例可能是从 1.0.0 以前的 0.x.x 版本升级而来。")
|
||||||
|
print("继续启动将会执行自动升级,可能包括数据库结构更新和配置文件更新。")
|
||||||
|
print("建议在继续前备份 data/ 与 config/ 目录。")
|
||||||
|
print()
|
||||||
|
for reason in reasons:
|
||||||
|
print(f"- {reason}")
|
||||||
|
print("=" * 72)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_input = input("确认继续升级并启动吗?请输入 y 后回车:").strip().lower()
|
||||||
|
except EOFError:
|
||||||
|
user_input = ""
|
||||||
|
if user_input != "y":
|
||||||
|
print("未确认升级,启动已取消。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.environ[LEGACY_UPGRADE_CONFIRM_ENV] = "1"
|
||||||
@@ -1099,15 +1099,6 @@ class DebugConfig(ConfigBase):
|
|||||||
)
|
)
|
||||||
"""是否启用 Maisaka 阶段看板"""
|
"""是否启用 Maisaka 阶段看板"""
|
||||||
|
|
||||||
show_prompt: bool = Field(
|
|
||||||
default=False,
|
|
||||||
json_schema_extra={
|
|
||||||
"x-widget": "switch",
|
|
||||||
"x-icon": "eye",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
"""是否显示prompt"""
|
|
||||||
|
|
||||||
show_maisaka_thinking: bool = Field(
|
show_maisaka_thinking: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
|||||||
Reference in New Issue
Block a user