diff --git a/_conf_schema.json b/_conf_schema.json
index 01003142..ca2e719b 100644
--- a/_conf_schema.json
+++ b/_conf_schema.json
@@ -102,8 +102,8 @@
"current_persona_name": {
"description": "当前目标人格",
"type": "string",
- "hint": "插件将学习并优化此人格的对话风格。需要在人格设置中存在",
- "default": "default"
+ "hint": "插件将学习并优化此人格的对话风格。留空或填 default 表示跟随 AstrBot 当前人格;填写具体 ID 时需在人格设置中存在",
+ "default": ""
}
}
},
diff --git a/config.py b/config.py
index 0ba4a815..16f0de7a 100644
--- a/config.py
+++ b/config.py
@@ -112,7 +112,7 @@ class PluginConfig(BaseModel):
disable_local_reply_when_delegated: bool = True # 检测到 Group Chat Plus 时禁用本地回复器
# 当前人格设置
- current_persona_name: str = "default"
+ current_persona_name: str = ""
# 学习参数
learning_interval_hours: int = 6 # 学习间隔(小时)
@@ -353,7 +353,7 @@ def create_from_config(cls, config: dict, data_dir: Optional[str] = None) -> 'Pl
target_qq_list=target_settings.get('target_qq_list', []),
target_blacklist=target_settings.get('target_blacklist', []),
- current_persona_name=target_settings.get('current_persona_name', 'default'),
+ current_persona_name=target_settings.get('current_persona_name', ''),
filter_provider_id=model_configuration.get('filter_provider_id', None),
refine_provider_id=model_configuration.get('refine_provider_id', None),
diff --git a/core/page_api.py b/core/page_api.py
index 2fe6dd41..3e4f6820 100644
--- a/core/page_api.py
+++ b/core/page_api.py
@@ -10,6 +10,7 @@
from __future__ import annotations
import asyncio
+import json
import os
import sys
import time
@@ -212,6 +213,9 @@ async def post_style_action(self) -> dict[str, Any]:
self._body_int(body, "id")
)
return self._operation(success, message)
+ if action == "update":
+ success, message, item = await self._update_style_review(body)
+ return self._operation(success, message, item=item)
return self._operation(False, f"未知表达学习操作: {action or '(empty)'}")
except Exception as exc:
logger.error(f"[PluginPageAPI] style action failed: {exc}", exc_info=True)
@@ -1159,6 +1163,62 @@ async def _delete_content_item(self, bucket: str, item_id: int) -> tuple[bool, s
logger.error(f"[PluginPageAPI] delete content failed: {exc}", exc_info=True)
return False, str(exc)
+ async def _update_style_review(self, body: Mapping[str, Any]) -> tuple[bool, str, dict[str, Any]]:
+ review_id = self._body_int(body, "id")
+ updates: dict[str, Any] = {}
+
+ if "description" in body:
+ updates["description"] = str(body.get("description") or "")
+ if "few_shots_content" in body:
+ updates["few_shots_content"] = str(body.get("few_shots_content") or "")
+ if "learned_patterns" in body:
+ patterns = body.get("learned_patterns")
+ updates["learned_patterns"] = (
+ json.dumps(patterns, ensure_ascii=False)
+ if isinstance(patterns, (list, dict))
+ else str(patterns or "")
+ )
+
+ if not updates:
+ return False, "没有可保存的表达方式字段", {}
+
+ database_manager = getattr(self._container(), "database_manager", None)
+ if not database_manager or not hasattr(database_manager, "get_session"):
+ return False, "数据库管理器未初始化", {}
+
+ try:
+ from sqlalchemy import select
+
+ try:
+ from ..models.orm import StyleLearningReview
+ except ImportError:
+ from models.orm import StyleLearningReview
+
+ async with database_manager.get_session() as session:
+ result = await session.execute(
+ select(StyleLearningReview).where(StyleLearningReview.id == review_id)
+ )
+ review = result.scalar_one_or_none()
+ if not review:
+ return False, f"表达方式审查 {review_id} 不存在", {}
+ for key, value in updates.items():
+ setattr(review, key, value)
+ if hasattr(review, "updated_at"):
+ review.updated_at = datetime.now()
+ item = {
+ "id": review.id,
+ "description": review.description,
+ "few_shots_content": review.few_shots_content,
+ "learned_patterns": review.learned_patterns,
+ "status": review.status,
+ "group_id": review.group_id,
+ }
+ await session.commit()
+ return True, "表达方式已更新", item
+ except Exception as exc:
+ logger.error(f"[PluginPageAPI] update style review failed: {exc}", exc_info=True)
+ return False, str(exc), {}
+
async def _delete_learning_batch(self, batch_id: int) -> tuple[bool, str]:
return await self._delete_content_item("history", batch_id)
diff --git a/pages/dashboard/app.js b/pages/dashboard/app.js
index f8c3c4c6..975a1b3d 100644
--- a/pages/dashboard/app.js
+++ b/pages/dashboard/app.js
@@ -40,6 +40,7 @@
height: 0,
canvasBound: false,
},
+ toastTimer: null,
};
const physics = {
@@ -188,13 +189,32 @@
function showToast(message, tone = "ok") {
const region = $("toast-region");
if (!region) return;
+ if (state.toastTimer) {
+ clearTimeout(state.toastTimer);
+ state.toastTimer = null;
+ }
+ region.replaceChildren();
const el = document.createElement("div");
el.className = `toast ${tone}`;
- el.textContent = message;
+ const text = document.createElement("span");
+ text.textContent = message;
+ const close = document.createElement("button");
+ close.className = "toast-close";
+ close.type = "button";
+ close.setAttribute("aria-label", "关闭提示");
+ close.textContent = "×";
+ close.addEventListener("click", () => {
+ if (state.toastTimer) clearTimeout(state.toastTimer);
+ el.remove();
+ });
+ el.append(text, close);
region.appendChild(el);
- setTimeout(() => {
+ state.toastTimer = setTimeout(() => {
el.classList.add("leaving");
- setTimeout(() => el.remove(), 220);
+ setTimeout(() => {
+ el.remove();
+ if (state.toastTimer) state.toastTimer = null;
+ }, 220);
}, 3200);
}
@@ -212,14 +232,24 @@
const modal = $("detail-modal");
setText("modal-title", title);
setHtml("modal-body", html);
- if (modal && typeof modal.showModal === "function") {
- modal.showModal();
+ if (!modal) return;
+ if (modal.open && typeof modal.close === "function") {
+ modal.close();
+ }
+ if (typeof modal.showModal === "function") {
+ try {
+ modal.showModal();
+ return;
+ } catch (_) {}
}
+ modal.setAttribute("open", "");
}
function closeModal() {
const modal = $("detail-modal");
- if (modal && typeof modal.close === "function") modal.close();
+ if (!modal) return;
+ if (typeof modal.close === "function") modal.close();
+ else modal.removeAttribute("open");
}
function resolvePageFromHash() {
@@ -527,6 +557,7 @@
renderGenericBarChart("style-pattern-chart", chartItems);
const reviews = ((data.reviews || {}).reviews || []);
setHtml("expression-review-list", reviews.map((item) => styleReviewHtml(item)).join("") || empty("暂无表达审查"));
+ state.pageData.lastStyleItems = reviews;
}
function renderReviews(data) {
@@ -541,6 +572,7 @@
setHtml("persona-review-list", personaPending.map((item) => personaReviewHtml(item)).join("") || empty("暂无人格更新"));
setHtml("style-review-list", styleReviews.map((item) => styleReviewHtml(item)).join("") || empty("暂无表达审查"));
setHtml("jargon-review-list", pendingJargon.map((item) => jargonReviewHtml(item)).join("") || empty("暂无黑话候选"));
+ state.pageData.lastStyleItems = styleReviews;
setHtml("reviewed-persona-list", personaReviewed.slice(0, 12).map((item) => `
${escapeHtml(item.id)}
@@ -576,6 +608,7 @@
${escapeHtml(item.few_shots_content || item.learned_patterns || "").slice(0, 220)}
+ ${button("编辑", `data-style-action="edit" data-id="${escapeAttr(item.id)}"`)}
${button("详情", `data-review-action="detail" data-kind="style" data-id="${escapeAttr(item.id)}"`)}
${button("批准", `data-review-action="approve" data-kind="style" data-id="${escapeAttr(item.id)}"`, "solid-button")}
${button("拒绝", `data-review-action="reject" data-kind="style" data-id="${escapeAttr(item.id)}"`)}
@@ -616,11 +649,13 @@
${escapeHtml(id)}
${escapeHtml(item.name || id)}
+ ${button("编辑", `data-persona-action="edit" data-persona-id="${escapeAttr(id)}"`)}
${button("导出", `data-persona-action="export" data-persona-id="${escapeAttr(id)}"`)}
${button("删除", `data-persona-action="delete" data-persona-id="${escapeAttr(id)}"`, "danger-button")}
`;
}).join("") || empty("暂无人格列表"));
+ state.pageData.lastPersonaItems = personas;
const backups = ((data.backups || {}).backups || []);
setText("persona-backup-count", fmt(backups.length, 0));
@@ -951,8 +986,50 @@
await loadPageData(state.page, { force: true });
}
+ function modalFieldValue(id) {
+ return $(id)?.value ?? "";
+ }
+
+ function parseModalJson(id, fallback) {
+ const raw = modalFieldValue(id).trim();
+ if (!raw) return fallback;
+ try {
+ return JSON.parse(raw);
+ } catch (_) {
+ return raw.split(/\n+/).map((line) => line.trim()).filter(Boolean);
+ }
+ }
+
+ async function handleStyleAction(action, id) {
+ if (action === "edit") {
+ const item = (state.pageData.lastStyleItems || []).find((entry) => String(entry.id) === String(id)) || {};
+ const patterns = typeof item.learned_patterns === "string"
+ ? item.learned_patterns
+ : JSON.stringify(item.learned_patterns || [], null, 2);
+ showModal("编辑表达方式", `
+
+
+
+
+ `);
+ }
+ }
+
async function handlePersonaAction(buttonEl) {
const action = buttonEl.dataset.personaAction;
+ if (action === "edit") {
+ const personaId = buttonEl.dataset.personaId;
+ const item = (state.pageData.lastPersonaItems || []).find((entry) => String(entry.persona_id || entry.id || entry.name) === String(personaId)) || {};
+ const beginDialogs = JSON.stringify(item.begin_dialogs || [], null, 2);
+ showModal("编辑人格", `
+
+
+
+
+
+ `);
+ return;
+ }
const body = {
action,
id: buttonEl.dataset.id,
@@ -1064,12 +1141,13 @@
});
document.addEventListener("click", async (event) => {
- const target = event.target.closest("[data-route-card],[data-refresh-page],[data-review-action],[data-jargon-action],[data-persona-action],[data-content-action],[data-settings-group]");
+ const target = event.target.closest("[data-route-card],[data-refresh-page],[data-review-action],[data-jargon-action],[data-style-action],[data-persona-action],[data-content-action],[data-settings-group]");
if (!target) return;
if (target.dataset.routeCard) navigateToPage(target.dataset.routeCard);
if (target.dataset.refreshPage) loadPageData(target.dataset.refreshPage, { force: true });
if (target.dataset.reviewAction) await handleReviewAction(target.dataset.kind, target.dataset.id, target.dataset.reviewAction);
if (target.dataset.jargonAction) await handleJargonAction(target.dataset.jargonAction, target.dataset.id);
+ if (target.dataset.styleAction) await handleStyleAction(target.dataset.styleAction, target.dataset.id);
if (target.dataset.personaAction) await handlePersonaAction(target);
if (target.dataset.contentAction) await handleContentAction(target);
if (target.dataset.settingsGroup) {
@@ -1099,6 +1177,45 @@
await loadPageData("jargon-learning", { force: true });
});
+ document.addEventListener("click", async (event) => {
+ const save = event.target.closest("#modal-style-save");
+ if (!save) return;
+ const result = await apiPost("style/action", {
+ action: "update",
+ id: save.dataset.id,
+ description: modalFieldValue("modal-style-description"),
+ few_shots_content: modalFieldValue("modal-style-few-shots"),
+ learned_patterns: parseModalJson("modal-style-patterns", []),
+ });
+ closeModal();
+ showToast(result.message || "表达方式已更新", result.success ? "ok" : "error");
+ state.pageData.style = null;
+ state.pageData.lastStyleItems = [];
+ await loadPageData("expression-learning", { force: true });
+ });
+
+ document.addEventListener("click", async (event) => {
+ const save = event.target.closest("#modal-persona-save");
+ if (!save) return;
+ const personaId = save.dataset.personaId;
+ const result = await apiPost("persona/action", {
+ action: "update",
+ persona_id: personaId,
+ persona: {
+ persona_id: personaId,
+ name: modalFieldValue("modal-persona-name"),
+ system_prompt: modalFieldValue("modal-persona-prompt"),
+ prompt: modalFieldValue("modal-persona-prompt"),
+ begin_dialogs: parseModalJson("modal-persona-dialogs", []),
+ },
+ });
+ closeModal();
+ showToast(result.message || "人格已更新", result.success ? "ok" : "error");
+ state.pageData.persona = null;
+ state.pageData.lastPersonaItems = [];
+ await loadPageData("persona-learning", { force: true });
+ });
+
qsa(".nav-item").forEach((item) => {
item.addEventListener("click", (event) => {
event.preventDefault();
diff --git a/pages/dashboard/styles.css b/pages/dashboard/styles.css
index 4670009d..6124232a 100644
--- a/pages/dashboard/styles.css
+++ b/pages/dashboard/styles.css
@@ -997,6 +997,10 @@ textarea {
}
.toast {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
max-width: min(360px, calc(100vw - 36px));
padding: 10px 12px;
border: 1px solid var(--border);
@@ -1007,6 +1011,21 @@ textarea {
animation: toastIn 180ms var(--spring);
}
+.toast-close {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 0;
+ color: var(--muted);
+ background: transparent;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.toast-close:hover {
+ color: var(--text);
+}
+
.toast.error {
color: #991b1b;
}
diff --git a/tests/integration/test_webui_static_assets.py b/tests/integration/test_webui_static_assets.py
index a7b6a9e6..3e57252b 100644
--- a/tests/integration/test_webui_static_assets.py
+++ b/tests/integration/test_webui_static_assets.py
@@ -86,7 +86,17 @@ def test_embedded_plugin_page_uses_astrbot_bridge_and_module_dashboard():
assert 'apiGet("persona"' in script
assert 'apiGet("graphs"' in script
assert 'apiPost("reviews/action"' in script
+ assert 'apiPost("style/action"' in script
+ assert 'apiPost("persona/action"' in script
assert 'apiPost("settings/action"' in script
+ assert 'data-jargon-action="edit"' in script
+ assert 'data-style-action="edit"' in script
+ assert 'data-persona-action="edit"' in script
+ assert 'id="modal-jargon-save"' in script
+ assert 'id="modal-style-save"' in script
+ assert 'id="modal-persona-save"' in script
+ assert "region.replaceChildren()" in script
+ assert "toast-close" in script
assert 'return `page/${String(path || "")' in script
assert "initSpringMotion" in script
assert "startGraphRender" in script
diff --git a/tests/unit/test_persona_selection.py b/tests/unit/test_persona_selection.py
index 8c0cff50..e8d2b870 100644
--- a/tests/unit/test_persona_selection.py
+++ b/tests/unit/test_persona_selection.py
@@ -1,9 +1,14 @@
from types import SimpleNamespace
-from unittest.mock import AsyncMock
+from unittest.mock import AsyncMock, Mock
import pytest
-from utils.persona_selection import get_persona_identifier, resolve_target_persona
+from utils.persona_selection import (
+ _RECENT_WARNINGS,
+ get_configured_persona_id,
+ get_persona_identifier,
+ resolve_target_persona,
+)
@pytest.mark.asyncio
@@ -53,6 +58,33 @@ async def test_resolve_target_persona_falls_back_to_single_existing_when_default
assert persona["persona_id"] == "suleng"
assert persona["selection_source"] == "single_existing"
+ manager.get_persona.assert_not_awaited()
+
+
+def test_default_persona_config_means_follow_astrbot_current_persona():
+ assert get_configured_persona_id(SimpleNamespace(current_persona_name="default")) is None
+ assert get_configured_persona_id(SimpleNamespace(current_persona_name="")) is None
+
+
+@pytest.mark.asyncio
+async def test_missing_current_persona_warning_is_throttled():
+ _RECENT_WARNINGS.clear()
+ manager = AsyncMock()
+ manager.get_default_persona_v3.return_value = {
+ "persona_id": "default",
+ "name": "default",
+ "prompt": "Default prompt",
+ }
+ manager.get_persona.return_value = None
+ manager.get_all_personas.return_value = []
+ log = SimpleNamespace(warning=Mock())
+
+ config = SimpleNamespace(current_persona_name="")
+
+ await resolve_target_persona(manager, config, "aiocqhttp:group:1", require_existing=True, log=log)
+ await resolve_target_persona(manager, config, "aiocqhttp:group:1", require_existing=True, log=log)
+
+ assert log.warning.call_count == 1
def test_get_persona_identifier_uses_persona_id_before_display_name():
diff --git a/utils/persona_selection.py b/utils/persona_selection.py
index ee0ba4d4..634e4e24 100644
--- a/utils/persona_selection.py
+++ b/utils/persona_selection.py
@@ -1,8 +1,13 @@
"""Helpers for resolving the AstrBot persona targeted by this plugin."""
import inspect
+import time
from typing import Any, Dict, Optional
+_WARNING_TTL_SECONDS = 300.0
+_RECENT_WARNINGS: Dict[str, float] = {}
+
+
def _is_unset_mock(obj: Any, name: str) -> bool:
return (
obj is not None
@@ -43,7 +48,7 @@ def get_configured_persona_id(config: Any) -> Optional[str]:
if value is None:
return None
persona_id = str(value).strip()
- if not persona_id or persona_id == "[%None]":
+ if not persona_id or persona_id == "[%None]" or persona_id.lower() == "default":
return None
return persona_id
@@ -97,6 +102,18 @@ def _warn(log: Any, message: str) -> None:
log.warning(message)
+def _warn_once(log: Any, message: str, *, key: Optional[str] = None) -> None:
+ if not log or not hasattr(log, "warning"):
+ return
+ cache_key = key or message
+ now = time.monotonic()
+ last = _RECENT_WARNINGS.get(cache_key)
+ if last is not None and now - last < _WARNING_TTL_SECONDS:
+ return
+ _RECENT_WARNINGS[cache_key] = now
+ log.warning(message)
+
+
async def _read_persona_by_id(
persona_manager: Any,
persona_id: str,
@@ -108,14 +125,18 @@ async def _read_persona_by_id(
try:
persona = await _maybe_await(get_persona(persona_id))
except Exception as exc:
- _warn(log, f"读取人格 {persona_id} 失败: {exc}")
+ _warn_once(log, f"读取人格 {persona_id} 失败: {exc}", key=f"read:{persona_id}:{exc}")
return None
normalized = normalize_persona_data(persona)
if normalized:
returned_id = normalized.get("persona_id")
if returned_id and str(returned_id) != persona_id:
- _warn(log, f"读取人格 {persona_id} 返回了 {returned_id},按未命中处理")
+ _warn_once(
+ log,
+ f"读取人格 {persona_id} 返回了 {returned_id},按未命中处理",
+ key=f"mismatch:{persona_id}:{returned_id}",
+ )
return None
normalized["persona_id"] = returned_id or persona_id
normalized.setdefault("name", normalized["persona_id"])
@@ -164,7 +185,11 @@ async def resolve_target_persona_from_web(
if configured:
configured["selection_source"] = "plugin_config"
return configured
- _warn(log, f"插件配置的人格 {configured_id} 不存在,将尝试 AstrBot 当前人格")
+ _warn_once(
+ log,
+ f"插件配置的人格 {configured_id} 不存在,将尝试 AstrBot 当前人格",
+ key=f"web-config-missing:{configured_id}",
+ )
get_persona_for_group = _explicit_attr(persona_web_manager, "get_persona_for_group")
get_all_personas = _explicit_attr(persona_web_manager, "get_all_personas_for_web")
@@ -182,7 +207,11 @@ async def resolve_target_persona_from_web(
if existing:
existing["selection_source"] = "astrbot_default"
return existing
- _warn(log, f"AstrBot 当前人格 {normalized['persona_id']} 不存在于 PersonaManager")
+ _warn_once(
+ log,
+ f"AstrBot 当前人格 {normalized['persona_id']} 不存在于 PersonaManager",
+ key=f"web-current-missing:{normalized['persona_id']}",
+ )
else:
normalized["selection_source"] = "astrbot_default"
return normalized
@@ -199,7 +228,11 @@ async def resolve_target_persona_from_web(
if existing:
existing["selection_source"] = "astrbot_default"
return existing
- _warn(log, f"AstrBot 全局人格 {normalized['persona_id']} 不存在于 PersonaManager")
+ _warn_once(
+ log,
+ f"AstrBot 全局人格 {normalized['persona_id']} 不存在于 PersonaManager",
+ key=f"web-global-missing:{normalized['persona_id']}",
+ )
else:
normalized["selection_source"] = "astrbot_default"
return normalized
@@ -212,7 +245,11 @@ async def resolve_target_persona_from_web(
if persona and persona.get("persona_id")
]
if len(normalized) == 1:
- _warn(log, f"目标人格不可用,回退到唯一可用人格 {normalized[0]['persona_id']}")
+ _warn_once(
+ log,
+ f"目标人格不可用,回退到唯一可用人格 {normalized[0]['persona_id']}",
+ key=f"web-single-fallback:{normalized[0]['persona_id']}",
+ )
normalized[0]["selection_source"] = "single_existing"
return normalized[0]
@@ -229,7 +266,7 @@ async def _read_single_existing_persona(
try:
personas = await _maybe_await(get_all_personas())
except Exception as exc:
- _warn(log, f"读取人格列表失败: {exc}")
+ _warn_once(log, f"读取人格列表失败: {exc}", key=f"list:{exc}")
return None
normalized = [
@@ -238,7 +275,11 @@ async def _read_single_existing_persona(
if persona and persona.get("persona_id")
]
if len(normalized) == 1:
- _warn(log, f"目标人格不可用,回退到唯一可用人格 {normalized[0]['persona_id']}")
+ _warn_once(
+ log,
+ f"目标人格不可用,回退到唯一可用人格 {normalized[0]['persona_id']}",
+ key=f"single-fallback:{normalized[0]['persona_id']}",
+ )
return normalized[0]
return None
@@ -260,19 +301,24 @@ async def resolve_target_persona(
return None
configured_id = get_configured_persona_id(config)
+ placeholder_persona: Optional[Dict[str, Any]] = None
if configured_id:
configured = await _read_persona_by_id(persona_manager, configured_id, log)
if configured:
configured["selection_source"] = "plugin_config"
return configured
- _warn(log, f"插件配置的人格 {configured_id} 不存在,将尝试 AstrBot 当前人格")
+ _warn_once(
+ log,
+ f"插件配置的人格 {configured_id} 不存在,将尝试 AstrBot 当前人格",
+ key=f"config-missing:{configured_id}",
+ )
get_default_persona = _explicit_attr(persona_manager, "get_default_persona_v3")
if get_default_persona:
try:
default_persona = await _maybe_await(get_default_persona(umo))
except Exception as exc:
- _warn(log, f"读取 AstrBot 当前人格失败: {exc}")
+ _warn_once(log, f"读取 AstrBot 当前人格失败: {exc}", key=f"current:{umo}:{exc}")
else:
normalized = normalize_persona_data(default_persona)
if normalized:
@@ -285,17 +331,18 @@ async def resolve_target_persona(
normalized["selection_source"] = "astrbot_default"
return normalized
if existing_id:
- existing = await _read_persona_by_id(persona_manager, existing_id, log)
- if existing:
- normalized["selection_source"] = "astrbot_default"
- return normalized
- _warn(log, f"AstrBot 当前人格 {existing_id} 不存在于 PersonaManager")
+ _warn_once(
+ log,
+ "AstrBot 当前人格 default 可能是占位值,将尝试回退到可用人格",
+ key="current-placeholder-default",
+ )
+ placeholder_persona = normalized
if umo != "default":
try:
default_persona = await _maybe_await(get_default_persona("default"))
except Exception as exc:
- _warn(log, f"读取 AstrBot default 人格失败: {exc}")
+ _warn_once(log, f"读取 AstrBot default 人格失败: {exc}", key=f"default:{exc}")
else:
normalized = normalize_persona_data(default_persona)
if normalized:
@@ -308,16 +355,23 @@ async def resolve_target_persona(
normalized["selection_source"] = "astrbot_default"
return normalized
if existing_id:
- existing = await _read_persona_by_id(persona_manager, existing_id, log)
- if existing:
- normalized["selection_source"] = "astrbot_default"
- return normalized
- _warn(log, f"AstrBot default 人格 {existing_id} 不存在于 PersonaManager")
+ _warn_once(
+ log,
+ "AstrBot default 人格可能是占位值,将尝试回退到可用人格",
+ key="current-placeholder-default",
+ )
+ placeholder_persona = placeholder_persona or normalized
if require_existing:
single_persona = await _read_single_existing_persona(persona_manager, log)
if single_persona:
+ if placeholder_persona and str(single_persona.get("persona_id") or "").lower() == "default":
+ placeholder_persona["selection_source"] = "astrbot_default_placeholder"
+ return placeholder_persona
single_persona["selection_source"] = "single_existing"
return single_persona
+ if placeholder_persona:
+ placeholder_persona["selection_source"] = "astrbot_default_placeholder"
+ return placeholder_persona
return None