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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 人格学习
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<br>

[![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)

Expand Down
2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
34 changes: 34 additions & 0 deletions core/page_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- "自学习"
Expand Down
63 changes: 60 additions & 3 deletions pages/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) || []);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 15 additions & 3 deletions pages/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,21 +154,33 @@ <h3>审查队列</h3>
<div class="panel">
<div class="panel-heading">
<h3>人格更新</h3>
<span class="mini-badge" id="persona-review-count">0</span>
<div class="inline-actions compact-actions">
<span class="mini-badge" id="persona-review-count">0</span>
<button class="ghost-button" type="button" data-batch-review-kind="persona" data-batch-review-action="approve">全批</button>
<button class="ghost-button" type="button" data-batch-review-kind="persona" data-batch-review-action="reject">全拒</button>
</div>
</div>
<div class="review-list" id="persona-review-list"></div>
</div>
<div class="panel">
<div class="panel-heading">
<h3>表达审查</h3>
<span class="mini-badge" id="style-review-count">0</span>
<div class="inline-actions compact-actions">
<span class="mini-badge" id="style-review-count">0</span>
<button class="ghost-button" type="button" data-batch-review-kind="style" data-batch-review-action="approve">全批</button>
<button class="ghost-button" type="button" data-batch-review-kind="style" data-batch-review-action="reject">全拒</button>
</div>
</div>
<div class="review-list" id="style-review-list"></div>
</div>
<div class="panel">
<div class="panel-heading">
<h3>黑话候选</h3>
<span class="mini-badge" id="jargon-review-count">0</span>
<div class="inline-actions compact-actions">
<span class="mini-badge" id="jargon-review-count">0</span>
<button class="ghost-button" type="button" data-batch-review-kind="jargon" data-batch-review-action="approve">全批</button>
<button class="ghost-button" type="button" data-batch-review-kind="jargon" data-batch-review-action="reject">全拒</button>
</div>
</div>
<div class="review-list" id="jargon-review-list"></div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions pages/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 38 additions & 4 deletions services/integration/maibot_learning_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
}
Expand All @@ -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]:
Expand All @@ -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]:
Expand All @@ -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]:
Expand Down Expand Up @@ -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}
Expand Down
29 changes: 29 additions & 0 deletions tests/integration/test_jargon_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Loading
Loading