diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a85d94..fc875ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ 所有重要更改都将记录在此文件中。 +## [3.2.7] - 2026-06-10 + +### MaiBot 迁移 + +- 修复 MaiBot 学习数据导入结果展示混淆的问题,表达方式、黑话和 A_memorix 记忆现在明确进入各自的审查/学习目标。 +- MaiBot 导入预览和导入结果增加分类去向与待审拆分统计,便于确认表达不会被误认为人格学习。 +- 修复 Web 表单传入字符串布尔值时 `"false"` 被当作开启的问题,避免导入开关失效。 + +### WebUI 审查 + +- 为独立 WebUI 与 AstrBot 内嵌 Dashboard 增加人格更新、表达审查和黑话候选的批量通过/拒绝入口。 +- 新增风格学习审查与黑话候选的批量审查 API,并复用现有单条审查逻辑以保持应用副作用一致。 +- 审查队列中的人格列表不再混入风格学习记录,避免“记忆/主题/表达都跑到人格那边”的观感。 + +### 版本 + +- 将插件发布版本号提升至 `3.2.7`。 + ## [3.2.6] - 2026-06-10 ### 人格学习 diff --git a/README.md b/README.md index 630615c0..738429be 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 让 AstrBot 在群聊中持续采集、学习、审查并注入上下文,使 Bot 逐步具备表达风格、群组黑话、社交关系、长期记忆和人格演化能力。 -[![Version](https://img.shields.io/badge/version-3.2.6-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) +[![Version](https://img.shields.io/badge/version-3.2.7-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) diff --git a/README_EN.md b/README_EN.md index 6cd94711..c6f2b9c7 100644 --- a/README_EN.md +++ b/README_EN.md @@ -14,7 +14,7 @@
-[![Version](https://img.shields.io/badge/version-3.2.6-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) +[![Version](https://img.shields.io/badge/version-3.2.7-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) [![AstrBot](https://img.shields.io/badge/AstrBot-%3E%3D4.11.4-orange.svg)](https://github.com/Soulter/AstrBot) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) [Features](#what-we-can-do) · [Quick Start](#quick-start) · [Web UI](#visual-management-interface) · [Community](#community) · [Contributing](CONTRIBUTING.md) diff --git a/__init__.py b/__init__.py index c42a4289..a1d89a04 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ # AstrBot 自学习插件 -__version__ = "3.2.6" +__version__ = "3.2.7" # Ensure parent namespace packages ("data", "data.plugins") are # durably registered in sys.modules. AstrBot loads plugins via diff --git a/core/page_api.py b/core/page_api.py index 486558ea..48754b6f 100644 --- a/core/page_api.py +++ b/core/page_api.py @@ -279,6 +279,40 @@ async def post_reviews_action(self) -> dict[str, Any]: result.get("message") or result.get("error") or "批量删除完成", result=result, ) + if action == "batch_review_style": + learning_service = imports.LearningService(container) + review_ids = [ + self._as_int(item, 0) + for item in self._body_list(body, "ids", fallback_key="review_ids") + ] + review_ids = [review_id for review_id in review_ids if review_id] + result = await learning_service.batch_review_style_learning_reviews( + review_ids, + str(body.get("decision") or body.get("review_action") or "approve"), + str(body.get("comment") or ""), + ) + return self._operation( + bool(result.get("success")), + result.get("message") or result.get("error") or "批量表达审查完成", + result=result, + ) + if action == "batch_review_jargon": + jargon_service = imports.JargonService(container) + jargon_ids = [ + self._as_int(item, 0) + for item in self._body_list(body, "ids", fallback_key="jargon_ids") + ] + jargon_ids = [jargon_id for jargon_id in jargon_ids if jargon_id] + result = await jargon_service.batch_review_jargon( + jargon_ids, + str(body.get("decision") or body.get("review_action") or "approve"), + meaning=body.get("meaning"), + ) + return self._operation( + bool(result.get("success")), + result.get("message") or result.get("error") or "批量黑话审查完成", + result=result, + ) if action.startswith("style_"): learning_service = imports.LearningService(container) review_id = self._body_int(body, "id") diff --git a/docs/README.md b/docs/README.md index 4c3a513b..88517c1f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ AstrBot 自主学习插件的实现文档和使用文档。 - 插件名: `astrbot_plugin_self_learning` - 展示名: `self-learning` -- 当前元数据版本: `3.2.6` +- 当前元数据版本: `3.2.7` - 最低 AstrBot 版本: `4.11.4` - 主要入口: `main.py` - 配置入口: `_conf_schema.json`, `config.py` diff --git a/metadata.yaml b/metadata.yaml index ed728992..c65117c7 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,7 +2,7 @@ name: "astrbot_plugin_self_learning" author: "NickMo, EterUltimate" display_name: "self-learning" description: "SELF LEARNING 自主学习插件 — 让 AI 聊天机器人自主学习对话风格、理解群组黑话、管理社交关系与好感度、自适应人格演化,像真人一样自然对话。(使用前必须手动备份人格数据)" -version: "3.2.6" +version: "3.2.7" repo: "https://github.com/NickCharlie/astrbot_plugin_self_learning" tags: - "自学习" diff --git a/pages/dashboard/app.js b/pages/dashboard/app.js index a64f83c3..67ea9ac7 100644 --- a/pages/dashboard/app.js +++ b/pages/dashboard/app.js @@ -561,7 +561,8 @@ } function renderReviews(data) { - const personaPending = ((data.persona_pending || {}).updates || []); + const personaPending = ((data.persona_pending || {}).updates || []) + .filter((item) => item && item.review_source !== "style_learning"); const personaReviewed = ((data.persona_reviewed || {}).updates || []); const styleReviews = ((data.style_reviews || {}).reviews || []); const pendingJargon = (((data.jargon_pending || {}).jargon_list) || []); @@ -880,7 +881,62 @@ function renderMaiBotImportPreview(summary) { const output = $("maibot-import-output"); if (!output || !summary) return; - output.textContent = JSON.stringify(summary, null, 2); + const counts = summary.counts || {}; + const breakdown = summary.review_breakdown || {}; + const destinations = summary.destinations || {}; + const lines = []; + if (Object.keys(counts).length) { + lines.push(`预览: 表达 ${fmt(counts.expressions, 0)} · 黑话 ${fmt(counts.jargons, 0)} · 记忆 ${fmt(counts.memories, 0)}`); + } + if (Object.keys(breakdown).length) { + lines.push(`导入: 表达审查 ${fmt(breakdown.style_learning_reviews, 0)} · 黑话候选 ${fmt(breakdown.jargon_candidates, 0)} · 记忆审查 ${fmt(breakdown.persona_memory_reviews, 0)}`); + } + if (Object.keys(destinations).length) { + lines.push(`分类去向: 表达 -> ${destinations.expressions}; 黑话 -> ${destinations.jargons}; 记忆 -> ${destinations.memories}`); + } + output.textContent = `${lines.join("\n")}${lines.length ? "\n\n" : ""}${JSON.stringify(summary, null, 2)}`; + } + + function currentBatchReviewIds(kind) { + const reviews = state.pageData.reviews || {}; + if (kind === "persona") { + return ((reviews.persona_pending || {}).updates || []) + .filter((item) => item && item.review_source !== "style_learning") + .map((item) => item.id) + .filter((id) => id !== undefined && id !== null && String(id) !== ""); + } + if (kind === "style") { + return ((reviews.style_reviews || {}).reviews || []) + .map((item) => item.id) + .filter((id) => id !== undefined && id !== null && String(id) !== ""); + } + if (kind === "jargon") { + return (((reviews.jargon_pending || {}).jargon_list) || []) + .map((item) => item.id) + .filter((id) => id !== undefined && id !== null && String(id) !== ""); + } + return []; + } + + async function handleBatchReviewAction(kind, action) { + const ids = currentBatchReviewIds(kind); + if (!ids.length) { + showToast("当前页没有可批量处理的审查项", "error"); + return; + } + const typeText = { persona: "人格更新", style: "表达审查", jargon: "黑话候选" }[kind] || "审查项"; + const actionText = action === "approve" ? "通过" : "拒绝"; + if (!window.confirm(`确定批量${actionText}当前页 ${ids.length} 条${typeText}?`)) return; + + const payload = { + action: kind === "persona" ? "batch_review" : kind === "style" ? "batch_review_style" : "batch_review_jargon", + ids, + decision: action, + }; + const result = await apiPost("reviews/action", payload); + showToast(result.message || "批量审查完成", result.success ? "ok" : "error"); + state.pageData.reviews = null; + await loadPageData(state.page, { force: true }); } async function runMaiBotImportAction(action) { @@ -1199,11 +1255,12 @@ $("maibot-import-button")?.addEventListener("click", () => runMaiBotImportAction("maibot_import")); document.addEventListener("click", async (event) => { - 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]"); + const target = event.target.closest("[data-route-card],[data-refresh-page],[data-review-action],[data-batch-review-kind],[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.batchReviewKind) await handleBatchReviewAction(target.dataset.batchReviewKind, target.dataset.batchReviewAction || "approve"); 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); diff --git a/pages/dashboard/index.html b/pages/dashboard/index.html index d0302b8f..3152e2d2 100644 --- a/pages/dashboard/index.html +++ b/pages/dashboard/index.html @@ -154,21 +154,33 @@

审查队列

人格更新

- 0 +
+ 0 + + +

表达审查

- 0 +
+ 0 + + +

黑话候选

- 0 +
+ 0 + + +
diff --git a/pages/dashboard/styles.css b/pages/dashboard/styles.css index b4ca380a..75ff41ed 100644 --- a/pages/dashboard/styles.css +++ b/pages/dashboard/styles.css @@ -199,6 +199,17 @@ h1 { flex-wrap: wrap; } +.compact-actions { + justify-content: flex-end; + gap: 6px; +} + +.compact-actions .ghost-button { + min-height: 28px; + padding: 0 8px; + font-size: 12px; +} + .icon-button, .ghost-button, .solid-button, diff --git a/services/integration/maibot_learning_importer.py b/services/integration/maibot_learning_importer.py index 1d150156..274fcae9 100644 --- a/services/integration/maibot_learning_importer.py +++ b/services/integration/maibot_learning_importer.py @@ -236,6 +236,12 @@ async def import_package( "expression_patterns_imported": 0, "jargons_imported": 0, "memory_reviews_imported": 0, + "destinations": _maibot_import_destinations(), + "review_breakdown": { + "style_learning_reviews": 0, + "jargon_candidates": 0, + "persona_memory_reviews": 0, + }, "skipped": 0, "errors": [], } @@ -253,6 +259,11 @@ async def import_package( await self._import_memories(package, result, default_group_id=default_group_id) result["success"] = not result["errors"] + result["review_breakdown"] = { + "style_learning_reviews": result["expressions_imported"], + "jargon_candidates": result["jargons_imported"], + "persona_memory_reviews": result["memory_reviews_imported"], + } return result async def import_from_source(self, **kwargs: Any) -> dict[str, Any]: @@ -265,10 +276,13 @@ async def import_from_source(self, **kwargs: Any) -> dict[str, Any]: return await self.import_package( package, default_group_id=str(kwargs.get("default_group_id") or "global"), - import_expressions=bool(kwargs.get("import_expressions", True)), - import_jargons=bool(kwargs.get("import_jargons", True)), - import_memories=bool(kwargs.get("import_memories", True)), - approve_checked_expressions=bool(kwargs.get("approve_checked_expressions", True)), + import_expressions=_to_bool(kwargs.get("import_expressions", True), True), + import_jargons=_to_bool(kwargs.get("import_jargons", True), True), + import_memories=_to_bool(kwargs.get("import_memories", True), True), + approve_checked_expressions=_to_bool( + kwargs.get("approve_checked_expressions", True), + True, + ), ) def package_summary(self, package: MaiBotLearningPackage) -> dict[str, Any]: @@ -293,6 +307,12 @@ def package_summary(self, package: MaiBotLearningPackage) -> dict[str, Any]: "jargons": [asdict(item) for item in package.jargons[:5]], "memories": [asdict(item) for item in package.memories[:3]], }, + "destinations": _maibot_import_destinations(), + "review_breakdown": { + "style_learning_reviews": len(package.expressions), + "jargon_candidates": len(package.jargons), + "persona_memory_reviews": len(package.memories), + }, } def export_json(self, **kwargs: Any) -> dict[str, Any]: @@ -862,6 +882,20 @@ def _optional_bool(value: Any) -> Optional[bool]: return None +def _to_bool(value: Any, default: bool) -> bool: + parsed = _optional_bool(value) + return default if parsed is None else parsed + + +def _maibot_import_destinations() -> dict[str, str]: + return { + "expressions": "style_learning_reviews", + "approved_expression_patterns": "expression_patterns", + "jargons": "jargon", + "memories": "persona_update_reviews", + } + + def _pick_keys(item: Mapping[str, Any], cls: type) -> dict[str, Any]: annotations = getattr(cls, "__annotations__", {}) return {key: item[key] for key in annotations if key in item} diff --git a/tests/integration/test_jargon_blueprint.py b/tests/integration/test_jargon_blueprint.py index 4218b1e3..6d10fae3 100644 --- a/tests/integration/test_jargon_blueprint.py +++ b/tests/integration/test_jargon_blueprint.py @@ -99,3 +99,32 @@ async def test_jargon_list_search_respects_pending_filter(client): assert response.status_code == 200 data = await response.get_json() assert [item["term"] for item in data["jargon_list"]] == ["待审"] + + +@pytest.mark.asyncio +async def test_jargon_batch_review_route_calls_service(client, monkeypatch): + calls = [] + + class _FakeJargonService: + def __init__(self, container): + self.container = container + + async def batch_review_jargon(self, jargon_ids, action, meaning=None): + calls.append((jargon_ids, action, meaning)) + return { + "success": True, + "message": "批量审查完成", + "details": {"success_count": len(jargon_ids), "failed_count": 0}, + } + + monkeypatch.setattr(jargon_module, "JargonService", _FakeJargonService) + + response = await client.post( + "/api/jargon/batch_review", + json={"jargon_ids": [7, 8], "action": "reject", "meaning": "ignored"}, + ) + + assert response.status_code == 200 + data = await response.get_json() + assert data["success"] is True + assert calls == [([7, 8], "reject", "ignored")] diff --git a/tests/integration/test_learning_content_blueprint.py b/tests/integration/test_learning_content_blueprint.py index e61fbee4..78ed4a00 100644 --- a/tests/integration/test_learning_content_blueprint.py +++ b/tests/integration/test_learning_content_blueprint.py @@ -3,6 +3,7 @@ from __future__ import annotations from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from quart import Quart @@ -46,6 +47,40 @@ async def test_learning_content_route_returns_all_buckets(client): assert data["history"] == [] +@pytest.mark.asyncio +async def test_style_learning_batch_review_route_calls_service(client, monkeypatch): + calls = [] + + class _FakeLearningService: + def __init__(self, container): + self.container = container + + async def batch_review_style_learning_reviews(self, review_ids, action, comment=""): + calls.append((review_ids, action, comment)) + return { + "success": True, + "message": "批量审查完成", + "details": {"success_count": len(review_ids), "failed_count": 0}, + } + + monkeypatch.setattr( + learning_module, + "get_container", + lambda: SimpleNamespace(database_manager=SimpleNamespace()), + ) + monkeypatch.setattr(learning_module, "LearningService", _FakeLearningService) + + response = await client.post( + "/api/style_learning/reviews/batch_review", + json={"review_ids": [1, 2], "action": "approve", "comment": "batch"}, + ) + + assert response.status_code == 200 + data = await response.get_json() + assert data["success"] is True + assert calls == [([1, 2], "approve", "batch")] + + @pytest.mark.asyncio async def test_learning_content_route_returns_database_rows(client, monkeypatch): engine = create_async_engine("sqlite+aiosqlite:///:memory:") diff --git a/tests/integration/test_webui_static_assets.py b/tests/integration/test_webui_static_assets.py index 3e57252b..2ab4deba 100644 --- a/tests/integration/test_webui_static_assets.py +++ b/tests/integration/test_webui_static_assets.py @@ -89,6 +89,16 @@ def test_embedded_plugin_page_uses_astrbot_bridge_and_module_dashboard(): assert 'apiPost("style/action"' in script assert 'apiPost("persona/action"' in script assert 'apiPost("settings/action"' in script + assert 'data-batch-review-kind="persona"' in index + assert 'data-batch-review-kind="style"' in index + assert 'data-batch-review-kind="jargon"' in index + assert "function handleBatchReviewAction" in script + assert "batch_review_style" in script + assert "batch_review_jargon" in script + assert 'review_source !== "style_learning"' in script + assert "分类去向" in script + assert "style_learning_reviews" in script + assert "persona_memory_reviews" in script assert 'data-jargon-action="edit"' in script assert 'data-style-action="edit"' in script assert 'data-persona-action="edit"' in script @@ -175,6 +185,31 @@ def test_dashboard_review_details_use_backend_structured_fields(): assert "renderContextExamples(item)" in text +def test_dashboard_exposes_batch_review_actions(): + text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8") + embedded_index = (PLUGIN_ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8") + + for action in [ + "persona-batch-approve", + "persona-batch-reject", + "style-batch-approve", + "style-batch-reject", + "jargon-batch-approve", + "jargon-batch-reject", + ]: + assert f'data-dashboard-action="{action}"' in text + + assert "function currentDashboardReviewIds(kind)" in text + assert "function batchReviewDashboardQueue(kind, action)" in text + assert "/api/persona_updates/batch_review" in text + assert "/api/style_learning/reviews/batch_review" in text + assert "/api/jargon/batch_review" in text + assert "review_source !== 'style_learning'" in text + assert 'data-batch-review-kind="persona"' in embedded_index + assert 'data-batch-review-kind="style"' in embedded_index + assert 'data-batch-review-kind="jargon"' in embedded_index + + def test_dashboard_review_deletes_use_inline_confirmation(): text = (PLUGIN_ROOT / "web_res" / "static" / "html" / "dashboard.html").read_text(encoding="utf-8") diff --git a/tests/unit/test_jargon_service.py b/tests/unit/test_jargon_service.py index 5e7698ce..64eb98c3 100644 --- a/tests/unit/test_jargon_service.py +++ b/tests/unit/test_jargon_service.py @@ -141,6 +141,73 @@ async def test_review_jargon_updates_candidate_status(): ) +@pytest.mark.unit +@pytest.mark.asyncio +async def test_batch_review_jargon_reviews_each_candidate(): + database_manager = SimpleNamespace( + get_jargon_by_id=AsyncMock( + side_effect=[ + { + "id": 7, + "content": "上强度", + "meaning": "", + "is_jargon": False, + "count": 4, + "chat_id": "group-a", + "raw_content": "[]", + }, + { + "id": 7, + "content": "上强度", + "meaning": "", + "is_jargon": True, + "is_complete": True, + "count": 4, + "chat_id": "group-a", + "raw_content": "[]", + }, + { + "id": 8, + "content": "绷不住", + "meaning": "", + "is_jargon": False, + "count": 2, + "chat_id": "group-a", + "raw_content": "[]", + }, + { + "id": 8, + "content": "绷不住", + "meaning": "", + "is_jargon": True, + "is_complete": True, + "count": 2, + "chat_id": "group-a", + "raw_content": "[]", + }, + ] + ), + update_jargon=AsyncMock(return_value=True), + ) + service = JargonService(SimpleNamespace(database_manager=database_manager)) + + result = await service.batch_review_jargon([7, "8"], "approve") + + assert result["success"] is True + assert result["details"]["success_count"] == 2 + assert result["details"]["failed_count"] == 0 + assert database_manager.update_jargon.await_args_list[0].args[0] == { + "id": 7, + "is_jargon": True, + "is_complete": True, + } + assert database_manager.update_jargon.await_args_list[1].args[0] == { + "id": 8, + "is_jargon": True, + "is_complete": True, + } + + @pytest.mark.unit @pytest.mark.asyncio async def test_get_jargon_list_can_filter_pending_candidates(): diff --git a/tests/unit/test_learning_chain_regressions.py b/tests/unit/test_learning_chain_regressions.py index 4fa4a891..189c08f2 100644 --- a/tests/unit/test_learning_chain_regressions.py +++ b/tests/unit/test_learning_chain_regressions.py @@ -1186,6 +1186,44 @@ async def review_persona_update(self, update_id, action): assert calls == [("style_42", "approve")] +@pytest.mark.unit +@pytest.mark.asyncio +async def test_learning_service_batch_style_reviews_use_unified_persona_review_path( + monkeypatch, +): + calls = [] + + class _FakePersonaReviewService: + def __init__(self, container): + self.container = container + + async def review_persona_update(self, update_id, action, comment=""): + calls.append((update_id, action, comment)) + return True, "ok" + + monkeypatch.setattr( + "self_learning_EterU.webui.services.learning_service.PersonaReviewService", + _FakePersonaReviewService, + ) + + service = LearningService( + SimpleNamespace( + database_manager=SimpleNamespace(), + persona_updater=SimpleNamespace(), + ) + ) + + result = await service.batch_review_style_learning_reviews([1, "2"], "approve", "batch") + + assert result["success"] is True + assert result["details"]["success_count"] == 2 + assert result["details"]["failed_count"] == 0 + assert calls == [ + ("style_1", "approve", "batch"), + ("style_2", "approve", "batch"), + ] + + @pytest.mark.unit @pytest.mark.asyncio async def test_learning_service_style_review_exposes_detail_fields(): diff --git a/tests/unit/test_maibot_learning_importer.py b/tests/unit/test_maibot_learning_importer.py index cf26bbc3..44aec97d 100644 --- a/tests/unit/test_maibot_learning_importer.py +++ b/tests/unit/test_maibot_learning_importer.py @@ -244,6 +244,17 @@ async def test_maibot_learning_importer_imports_into_plugin_tables(tmp_path): assert result["expression_patterns_imported"] == 1 assert result["jargons_imported"] == 1 assert result["memory_reviews_imported"] == 2 + assert result["destinations"] == { + "expressions": "style_learning_reviews", + "approved_expression_patterns": "expression_patterns", + "jargons": "jargon", + "memories": "persona_update_reviews", + } + assert result["review_breakdown"] == { + "style_learning_reviews": 1, + "jargon_candidates": 1, + "persona_memory_reviews": 2, + } async with manager.get_session() as session: style_reviews = (await session.execute(select(StyleLearningReview))).scalars().all() @@ -264,3 +275,76 @@ async def test_maibot_learning_importer_imports_into_plugin_tables(tmp_path): assert persona_reviews[0].group_id == "group-1" finally: await manager.stop() + + +@pytest.mark.asyncio +async def test_maibot_learning_importer_import_from_source_parses_string_flags(tmp_path): + maibot_db = tmp_path / "maibot.db" + memorix_db = tmp_path / "metadata.db" + _create_maibot_db(maibot_db) + _create_memorix_db(memorix_db) + + manager = SQLAlchemyDatabaseManager( + PluginConfig( + data_dir=str(tmp_path / "plugin"), + db_type="sqlite", + enable_web_interface=False, + ) + ) + try: + assert await manager.start() is True + importer = MaiBotLearningImporter(manager) + package = importer.load_package(db_path=maibot_db, memorix_db_path=memorix_db) + result = await importer.import_from_source( + payload=package.to_dict(), + import_expressions="false", + import_jargons="false", + import_memories="true", + ) + + assert result["success"] is True + assert result["expressions_imported"] == 0 + assert result["jargons_imported"] == 0 + assert result["memory_reviews_imported"] == 2 + assert result["review_breakdown"] == { + "style_learning_reviews": 0, + "jargon_candidates": 0, + "persona_memory_reviews": 2, + } + + async with manager.get_session() as session: + style_reviews = (await session.execute(select(StyleLearningReview))).scalars().all() + jargons = (await session.execute(select(Jargon))).scalars().all() + persona_reviews = ( + await session.execute( + select(PersonaLearningReview).where( + PersonaLearningReview.update_type == "maibot_memory" + ) + ) + ).scalars().all() + + assert style_reviews == [] + assert jargons == [] + assert len(persona_reviews) == 2 + finally: + await manager.stop() + + +def test_maibot_learning_importer_preview_reports_destinations(tmp_path): + maibot_db = tmp_path / "maibot.db" + memorix_db = tmp_path / "metadata.db" + _create_maibot_db(maibot_db) + _create_memorix_db(memorix_db) + + importer = MaiBotLearningImporter() + package = importer.load_package(db_path=maibot_db, memorix_db_path=memorix_db) + summary = importer.package_summary(package) + + assert summary["destinations"]["expressions"] == "style_learning_reviews" + assert summary["destinations"]["jargons"] == "jargon" + assert summary["destinations"]["memories"] == "persona_update_reviews" + assert summary["review_breakdown"] == { + "style_learning_reviews": 1, + "jargon_candidates": 1, + "persona_memory_reviews": 2, + } diff --git a/web_res/static/html/dashboard.html b/web_res/static/html/dashboard.html index aa835521..2edbb3bf 100644 --- a/web_res/static/html/dashboard.html +++ b/web_res/static/html/dashboard.html @@ -3051,7 +3051,15 @@

人格备份

待审人格

- 0 +
+ 0 + + +
@@ -3061,7 +3069,15 @@

待审人格

风格审查

- 0 +
+ 0 + + +
@@ -3087,6 +3103,12 @@

黑话与批次

--
+ +