Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f0a4b3b
fix(learning): save filtered messages to DB in batch learning path
NickCharlie Feb 25, 2026
7a6a5bf
fix(webui): use neutral fallback for response speed when no LLM data
NickCharlie Feb 25, 2026
a9a99f4
fix(persona): use global default persona in WebUI instead of random UMO
NickCharlie Feb 25, 2026
aaf9499
fix(db): correct ORM field mappings for psychological state and mood …
NickCharlie Feb 25, 2026
82ebe84
test(unit): add unit tests for core modules and expand coverage config
NickCharlie Feb 25, 2026
136431e
fix(db): disable SSL for MySQL 8 connections and harden session lifec…
NickCharlie Mar 2, 2026
cd454d6
fix(jargon): serialize dict/list meaning to JSON string before DB write
NickCharlie Mar 2, 2026
0756435
fix(state): use defensive getattr for ORM-to-dataclass component mapping
NickCharlie Mar 2, 2026
584e6af
fix(webui): resolve persona review revert crash and reviewed list gaps
NickCharlie Mar 2, 2026
68b6628
fix(webui): prefer facade over direct repository for style statistics
NickCharlie Mar 2, 2026
0377bc1
fix(db): replace SQLite StaticPool with NullPool to resolve concurren…
NickCharlie Mar 2, 2026
4e9bcb8
fix(commands): guard all command handlers against uninitialized services
NickCharlie Mar 2, 2026
a3bf381
fix(webui): remove gc.collect() calls that cause CPU spike during plu…
NickCharlie Mar 2, 2026
c5d623b
fix(webui): map style learning fields to unified format in reviewed list
NickCharlie Mar 2, 2026
aa71e62
chore(release): bump version to Next-2.0.6 and update changelog
NickCharlie Mar 2, 2026
f435375
docs: sync sponsorship section and QR code from main
NickCharlie Mar 2, 2026
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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,50 @@

所有重要更改都将记录在此文件中。

## [Next-2.0.6] - 2026-03-02

### 新功能

#### WebUI 监听地址可配置
- 新增 `web_interface_host` 配置项,允许用户自定义 WebUI 监听地址(默认 `0.0.0.0`)

### Bug 修复

#### SQLite 并发访问错误
- 修复 SQLite 连接池使用 `StaticPool`(共享单连接)导致 WebUI 并发请求事务状态污染的问题
- 改为 `NullPool`,每个会话获取独立连接,消除 "Cannot operate on a closed database" 错误

#### 插件卸载 CPU 100%
- 移除 WebUI 关停流程中 `server.py` 和 `manager.py` 的两次 `gc.collect()` 调用
- 每次 `gc.collect()` 遍历 ~200 个模块的对象图耗时 80+ 秒,导致卸载期间 CPU 满载

#### 命令处理器空指针
- 为全部 6 个管理命令(`learning_status`、`start_learning`、`stop_learning`、`force_learning`、`affection_status`、`set_mood`)添加空值守卫
- 当 `bootstrap()` 失败导致 `_command_handlers` 为 `None` 时,返回友好提示而非抛出 `'NoneType' object has no attribute` 异常

#### 人格审查系统
- 修复撤回操作崩溃和已审查列表数据缺失问题
- 修复风格学习审查记录在已审查历史中显示空内容、类型"未知"、置信度 0.0% 的问题,补全 `StyleLearningReview` 到前端统一格式的字段映射
- WebUI 风格统计查询改用 Facade 而非直接 Repository 调用

#### MySQL 8 连接
- 禁用 MySQL 8 默认 SSL 要求,解决 `ssl.SSLError` 连接失败
- 强化会话生命周期管理

#### ORM 字段映射
- 修正心理状态和情绪持久化的 ORM 字段映射
- 使用防御性 `getattr` 处理 ORM-to-dataclass 组件映射中的缺失属性

#### 其他修复
- WebUI 使用全局默认人格代替随机 UMO
- WebUI 响应速度指标无 LLM 数据时使用中性回退值
- 黑话 meaning 字段 dict/list 类型序列化为 JSON 字符串后写入数据库
- 批量学习路径中正确保存筛选后的消息到数据库
- 防护 `background_tasks` 在关停序列中的访问安全

### 测试
- 新增核心模块单元测试,扩展覆盖率配置

## [Next-2.0.5] - 2026-02-24

### Bug 修复
Expand Down
10 changes: 9 additions & 1 deletion README.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-Next--2.0.0-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-GPLv3-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-Next--2.0.6-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-GPLv3-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/)

[核心功能](#-我们能做什么) · [快速开始](#-快速开始) · [管理界面](#-可视化管理界面) · [社区交流](#-社区交流) · [贡献指南](CONTRIBUTING.md)

Expand Down Expand Up @@ -229,10 +229,18 @@ http://localhost:7833

---

---

<div align="center">

**如果觉得有帮助,欢迎 Star 支持!**

### 赞助支持

如果这个项目对你有帮助,欢迎通过爱发电赞助支持开发者持续维护:

<img src="image/afdian-NickMo.jpeg" alt="爱发电赞助二维码" width="200"/>

[回到顶部](#astrbot-自主学习插件)

</div>
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-Next--2.0.0-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-GPLv3-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-Next--2.0.6-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning) [![License](https://img.shields.io/badge/license-GPLv3-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
9 changes: 6 additions & 3 deletions core/database/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
避免 "Task got Future attached to a different loop" 错误。
"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import StaticPool
from sqlalchemy.pool import NullPool
from sqlalchemy import types as sa_types
from astrbot.api import logger
from typing import Optional
Expand Down Expand Up @@ -90,11 +90,13 @@ def _create_sqlite_engine(self):
logger.info(f"[DatabaseEngine] 创建数据库目录: {db_dir}")

# SQLite 配置
# StaticPool reuses a single connection, avoiding per-query overhead
# NullPool: 每个 session 独立创建/关闭连接,避免 StaticPool
# 单连接共享导致的并发事务状态污染和 "closed database" 错误。
# SQLite 建连成本极低(打开文件句柄),配合 WAL 模式可安全并发读。
engine = create_async_engine(
db_url,
echo=self.echo,
poolclass=StaticPool,
poolclass=NullPool,
connect_args={
'check_same_thread': False,
'timeout': 30,
Expand Down Expand Up @@ -138,6 +140,7 @@ def _create_mysql_engine(self):
connect_args={
'connect_timeout': 10,
'charset': 'utf8mb4',
'ssl': False,
}
)

Expand Down
12 changes: 12 additions & 0 deletions core/framework_llm_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ async def filter_chat_completion(
# 尝试延迟初始化
self._try_lazy_init()

# 确保 contexts 不为 None,避免 Provider 内部调用 len(None)
if contexts is None:
contexts = []

if not self.filter_provider:
logger.warning("筛选Provider未配置,尝试使用备选Provider或降级处理")
# 尝试使用其他可用的Provider作为备选
Expand Down Expand Up @@ -301,6 +305,10 @@ async def refine_chat_completion(
# 尝试延迟初始化
self._try_lazy_init()

# 确保 contexts 不为 None,避免 Provider 内部调用 len(None)
if contexts is None:
contexts = []

if not self.refine_provider:
logger.warning("提炼Provider未配置,尝试使用备选Provider或降级处理")
# 尝试使用其他可用的Provider作为备选
Expand Down Expand Up @@ -359,6 +367,10 @@ async def reinforce_chat_completion(
# 尝试延迟初始化
self._try_lazy_init()

# 确保 contexts 不为 None,避免 Provider 内部调用 len(None)
if contexts is None:
contexts = []

if not self.reinforce_provider:
logger.warning("强化Provider未配置,尝试使用备选Provider或降级处理")
# 尝试使用其他可用的Provider作为备选
Expand Down
Binary file added image/afdian-NickMo.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,40 +231,58 @@ async def inject_diversity_to_llm_request(self, event: AstrMessageEvent, req=Non
@filter.permission_type(PermissionType.ADMIN)
async def learning_status_command(self, event: AstrMessageEvent):
"""查看学习状态"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.learning_status(event):
yield result

@filter.command("start_learning")
@filter.permission_type(PermissionType.ADMIN)
async def start_learning_command(self, event: AstrMessageEvent):
"""手动启动学习"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.start_learning(event):
yield result

@filter.command("stop_learning")
@filter.permission_type(PermissionType.ADMIN)
async def stop_learning_command(self, event: AstrMessageEvent):
"""停止学习"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.stop_learning(event):
yield result

@filter.command("force_learning")
@filter.permission_type(PermissionType.ADMIN)
async def force_learning_command(self, event: AstrMessageEvent):
"""强制执行一次学习周期"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.force_learning(event):
yield result

@filter.command("affection_status")
@filter.permission_type(PermissionType.ADMIN)
async def affection_status_command(self, event: AstrMessageEvent):
"""查看好感度状态"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.affection_status(event):
yield result

@filter.command("set_mood")
@filter.permission_type(PermissionType.ADMIN)
async def set_mood_command(self, event: AstrMessageEvent):
"""手动设置bot情绪"""
if not self._command_handlers:
yield event.plain_result("插件服务未就绪,请检查启动日志")
return
async for result in self._command_handlers.set_mood(event):
yield result
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"
display_name: "self-learning"
description: "SELF LEARNING 自主学习插件 — 让 AI 聊天机器人自主学习对话风格、理解群组黑话、管理社交关系与好感度、自适应人格演化,像真人一样自然对话。(使用前必须手动备份人格数据)"
version: "Next-2.0.5"
version: "Next-2.0.6"
repo: "https://github.com/NickCharlie/astrbot_plugin_self_learning"
tags:
- "自学习"
Expand Down
14 changes: 5 additions & 9 deletions persona_web_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ async def get_all_personas_for_web(self) -> List[Dict[str, Any]]:
async def get_default_persona_for_web(self) -> Dict[str, Any]:
"""获取默认人格,格式化为Web界面需要的格式

使用 group_id_to_unified_origin 映射中的 UMO 来获取当前活跃配置的人格,
而非始终返回 default 配置的人格。
始终传入 None 调用 get_default_persona_v3 以获取 AstrBot 全局默认人格,
避免在多配置文件场景下因随机选取 UMO 而导致每次返回不同人格。
如需查看特定配置的人格,应通过 get_persona_for_group 并明确指定 group_id。
"""
fallback = {
"persona_id": "default",
Expand All @@ -157,14 +158,9 @@ async def get_default_persona_for_web(self) -> Dict[str, Any]:
return fallback

try:
# 尝试从映射中获取一个 UMO,以加载当前活跃配置的人格
umo = None
if self.group_id_to_unified_origin:
# 取任意一个 UMO(通常同一配置文件下的群组共享同一配置)
umo = next(iter(self.group_id_to_unified_origin.values()), None)

# 获取全局默认人格,不依赖 group_id_to_unified_origin 映射
default_persona = await self._run_on_main_loop(
self.persona_manager.get_default_persona_v3(umo)
self.persona_manager.get_default_persona_v3(None)
)

if default_persona:
Expand Down
20 changes: 18 additions & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ addopts =
# Strict markers (only registered markers allowed)
--strict-markers
# Coverage options
--cov=config
--cov=constants
--cov=exceptions
--cov=core
--cov=utils
--cov=services
--cov=webui
--cov-report=html
--cov-report=term-missing
Expand All @@ -40,7 +46,10 @@ markers =
auth: Authentication related tests
service: Service layer tests
blueprint: Blueprint/route tests
security: Security-related tests
core: Core module tests
config: Configuration tests
utils: Utility module tests
quality: Quality monitoring tests

# Log output
log_cli = true
Expand All @@ -53,7 +62,14 @@ asyncio_mode = auto

# Coverage options
[coverage:run]
source = webui
source =
config
constants
exceptions
core
utils
services
webui
omit =
*/tests/*
*/test_*.py
Expand Down
9 changes: 8 additions & 1 deletion repositories/bot_mood_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ async def save(self, mood_data: Dict[str, Any]) -> Optional[BotMood]:
mood = BotMood(**mood_data)
self.session.add(mood)
await self.session.commit()
await self.session.refresh(mood)
try:
await self.session.refresh(mood)
except Exception as refresh_err:
# 部分 async 驱动在 commit 后 refresh 可能失败,
# 此时 mood 对象已持久化且 ID 已赋值,可安全返回
logger.debug(
f"[BotMoodRepository] refresh after commit skipped: {refresh_err}"
)
return mood
except Exception as e:
await self.session.rollback()
Expand Down
Loading