Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions _conf_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@
"current_persona_name": {
"description": "当前目标人格",
"type": "string",
"hint": "插件将学习并优化此人格的对话风格。需要在人格设置中存在",
"default": "default"
"hint": "插件将学习并优化此人格的对话风格。留空或填 default 表示跟随 AstrBot 当前人格;填写具体 ID 时需在人格设置中存在",
"default": ""
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 # 学习间隔(小时)
Expand Down Expand Up @@ -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),
Expand Down
60 changes: 60 additions & 0 deletions core/page_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import annotations

import asyncio
import json
import os
import sys
import time
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
131 changes: 124 additions & 7 deletions pages/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
height: 0,
canvasBound: false,
},
toastTimer: null,
};

const physics = {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) => `
<div class="table-row">
<span>${escapeHtml(item.id)}</span>
Expand Down Expand Up @@ -576,6 +608,7 @@
<p>${escapeHtml(item.few_shots_content || item.learned_patterns || "").slice(0, 220)}</p>
</div>
<div class="row-actions">
${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)}"`)}
Expand Down Expand Up @@ -616,11 +649,13 @@
<span>${escapeHtml(id)}</span>
<strong>${escapeHtml(item.name || id)}</strong>
<div class="row-actions">
${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")}
</div>
</div>`;
}).join("") || empty("暂无人格列表"));
state.pageData.lastPersonaItems = personas;

const backups = ((data.backups || {}).backups || []);
setText("persona-backup-count", fmt(backups.length, 0));
Expand Down Expand Up @@ -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("编辑表达方式", `
<label class="config-field"><span><strong>描述</strong></span><input id="modal-style-description" value="${escapeAttr(item.description || "")}"></label>
<label class="config-field"><span><strong>Few-shot 示例</strong></span><textarea id="modal-style-few-shots" rows="7">${escapeHtml(item.few_shots_content || "")}</textarea></label>
<label class="config-field"><span><strong>学习模式 JSON</strong></span><textarea id="modal-style-patterns" rows="7">${escapeHtml(patterns)}</textarea></label>
<button class="solid-button" type="button" id="modal-style-save" data-id="${escapeAttr(id)}">保存</button>
`);
}
}

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("编辑人格", `
<label class="config-field"><span><strong>人格 ID</strong></span><input id="modal-persona-id" value="${escapeAttr(personaId)}" disabled></label>
<label class="config-field"><span><strong>名称</strong></span><input id="modal-persona-name" value="${escapeAttr(item.name || personaId || "")}"></label>
<label class="config-field"><span><strong>系统提示词</strong></span><textarea id="modal-persona-prompt" rows="9">${escapeHtml(item.system_prompt || item.prompt || "")}</textarea></label>
<label class="config-field"><span><strong>开场对话 JSON</strong></span><textarea id="modal-persona-dialogs" rows="6">${escapeHtml(beginDialogs)}</textarea></label>
<button class="solid-button" type="button" id="modal-persona-save" data-persona-id="${escapeAttr(personaId)}">保存</button>
`);
return;
}
const body = {
action,
id: buttonEl.dataset.id,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions pages/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/test_webui_static_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading