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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

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

## [4.3.0] - 2026-09-12

### LivingMemory 2.7 适配

- 功能融合页新增 LivingMemory **官方面板**入口:LivingMemory 2.6.0 起移除独立 WebUI、改为自带 AstrBot 官方插件页(`pages/dashboard/index.html`)。集成服务现在会自动探测插件 `pages/` 目录与 AstrBot 主配置 `dashboard.host/port`,生成 `dashboard.official_page_url`(AstrBot Dashboard 前端 hash 路由 `/#/plugin-page/<插件名>/<页名>`)。
- 功能融合页 LivingMemory 卡片新增「官方面板」按钮(新窗口打开,复用浏览器内 AstrBot 会话 JWT);LivingMemory 嵌入壳由「面板不可用」改为「面板禁止内嵌 + 新窗口打开」(官方插件页固定返回 `X-Frame-Options: SAMEORIGIN` 且资产需 JWT 换取,iframe 无法携带凭据,属预期行为,`embeddable` 相应置为 `false`)。
- 伴随插件契约核对(对照 livingmemory 2.7.0-beta.1、group_chat_plus V1.2.3.hotfix.2):注册名、`initializer.memory_engine`、`memory_engine.graph_store`、`graph_store.get_graph_snapshot` 签名与返回键、`memory_engine.get_statistics` 全部保持,本地记忆图谱直读链路无需改动;LivingMemory 2.6.x 引入的全量快照防御上限只作用于本插件未调用的 `get_full_graph_snapshot`。
- AstrBot 兼容性核对至 v4.28(commit e606a3037):本插件使用的全部 API 面(`astrbot.api.*`、`astrbot.api.event.filter`、`astrbot.api.web.request`、provider/star/config 工具)在 v4.28 中无变更。

### 版本

- 版本号由 4.2.0 提升至 **4.3.0**。

## [4.2.0] - 2026-09-05

### 黑话导入
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-4.2.0-blue.svg)](https://github.com/NickCharlie/astrbot_plugin_self_learning)
[![Version](https://img.shields.io/badge/version-4.3.0-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-4.2.0-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-4.3.0-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__ = "4.2.0"
__version__ = "4.3.0"

# Ensure parent namespace packages ("data", "data.plugins") are
# durably registered in sys.modules. AstrBot loads plugins via
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`
- 当前元数据版本: `4.2.0`
- 当前元数据版本: `4.3.0`
- 最低 AstrBot 版本: `4.11.4`
- 主要入口: `main.py`
- 配置入口: `_conf_schema.json`, `config.py`
Expand Down
6 changes: 4 additions & 2 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Dashboard -> 功能融合
页面内容:

- Self Learning 当前面板。
- LivingMemory 状态、外部面板入口和本地图谱适配 API 列表。
- LivingMemory 状态、官方插件页/外部面板入口和本地图谱适配 API 列表。
- Group Chat Plus 状态、面板入口和开发 API 列表。
- `Integration_Settings` 快速编辑。
- Self Learning Hub 稳定 HTTP 契约和可对接能力。
Expand Down Expand Up @@ -154,6 +154,7 @@ GET /api/hub/v1/status
| `delegated` | 当前能力是否已委托 |
| `plugin` | AstrBot star 元信息 |
| `dashboard` | 面板 URL、本地图谱路由、入口类型;`embeddable` 为 `false` 表示面板响应头禁止 iframe 嵌入(如 Group Chat Plus 的 `X-Frame-Options: DENY`),此时嵌入壳会提示改用新窗口打开 |
| `dashboard.official_page_url` | 伴随插件自带 AstrBot 官方插件页时的入口(LivingMemory 2.6.0+)。指向 AstrBot Dashboard 前端 hash 路由 `/#/plugin-page/<插件名>/<页名>`,由集成服务探测插件 `pages/<页名>/index.html` 与主配置 `dashboard.host/port` 自动生成;该页面需 AstrBot 会话 JWT,iframe 内不可用,只能新窗口打开(功能融合页的「官方面板」按钮) |
| `dev_api` | 该插件公开 API 列表 |
| `settings_group` | 相关配置组 |

Expand Down Expand Up @@ -262,8 +263,9 @@ ECharts 图谱 payload。记忆图和知识图谱都会优先读取 LivingMemory
| Dashboard 显示未委托 | 目标插件是否已加载、启用、名称是否匹配 |
| LivingMemory 已安装但仍写入本地记忆 | `delegate_memory_to_livingmemory` 和 `disable_local_memory_when_delegated` 是否为 `true` |
| Group Chat Plus 已安装但仍创建本地回复器 | `delegate_reply_to_group_chat_plus` 和 `disable_local_reply_when_delegated` 是否为 `true` |
| 面板入口为空 | 目标插件 Web 面板是否开启;图谱模块仍可通过本插件 `/api/graphs/*` 查看 |
| 面板入口为空 | 目标插件 Web 面板是否开启;LivingMemory 2.6.0+ 由集成服务自动探测其官方插件页(`pages/` 目录 + AstrBot 主配置 `dashboard` 段),图谱模块仍可通过本插件 `/api/graphs/*` 查看 |
| Group Chat Plus 嵌入壳显示"面板禁止内嵌" | 该面板自 v1.2.x 起返回 `X-Frame-Options: DENY`,无法被 iframe 嵌入,属预期行为;集成服务会探测响应头并自动改用新窗口入口 |
| LivingMemory 嵌入壳显示"面板禁止内嵌" | AstrBot 官方插件页固定返回 `X-Frame-Options: SAMEORIGIN` 且资产需 JWT 换取,iframe 内无法携带凭据,属预期行为;请用嵌入壳或功能融合页的「官方面板」新窗口打开 |
| API 列表不匹配 | 以目标插件当前开发 API 为准,更新 `webui/services/integration_service.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: "4.2.0"
version: "4.3.0"
repo: "https://github.com/NickCharlie/astrbot_plugin_self_learning"
tags:
- "自学习"
Expand Down
137 changes: 137 additions & 0 deletions tests/unit/test_integration_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,140 @@ def test_embed_shell_keeps_iframe_when_embedding_allowed():

assert '<iframe id="companion-frame"' in html
assert "面板禁止内嵌" not in html


def _lm_official_page_container(memory_star, *, astrbot_config=None):
delegation = SimpleNamespace(
status=lambda: {
"memory_delegated": True,
"memory_plugin": "astrbot_plugin_livingmemory",
"reply_delegated": False,
"reply_plugin": None,
},
memory_plugin=lambda: memory_star,
reply_plugin=lambda: None,
)
return SimpleNamespace(
plugin_config=SimpleNamespace(
delegate_memory_to_livingmemory=True,
livingmemory_plugin_name="astrbot_plugin_livingmemory",
disable_local_memory_when_delegated=True,
delegate_reply_to_group_chat_plus=False,
group_chat_plus_plugin_name="astrbot_plugin_group_chat_plus",
disable_local_reply_when_delegated=True,
knowledge_engine="legacy",
lightrag_query_mode="local",
),
astrbot_config=astrbot_config,
webui_config=SimpleNamespace(host="127.0.0.1", port=8989),
feature_delegation=delegation,
)


async def test_livingmemory_official_page_detected_from_plugin_pages(tmp_path):
# LM 2.6.0+ 移除独立 WebUI,改为自带 AstrBot 官方插件页 pages/dashboard/index.html
(tmp_path / "pages" / "dashboard").mkdir(parents=True)
(tmp_path / "pages" / "dashboard" / "index.html").write_text(
"<html></html>", encoding="utf-8"
)
memory_star = SimpleNamespace(
name="astrbot_plugin_livingmemory",
display_name="astrbot_plugin_livingmemory",
root_dir_name="astrbot_plugin_livingmemory",
module_path=str(tmp_path / "main.py"),
star_cls=SimpleNamespace(),
)
container = _lm_official_page_container(
memory_star,
astrbot_config={"dashboard": {"host": "0.0.0.0", "port": 6185}},
)

payload = await IntegrationService(container).get_status()
dashboards = {item["id"]: item for item in payload["dashboards"]}
dashboard = dashboards["livingmemory"]["dashboard"]
expected = (
"http://127.0.0.1:6185/#/plugin-page/"
"astrbot_plugin_livingmemory/dashboard"
)
assert dashboard["official_page_url"] == expected
assert dashboard["embeddable"] is False
assert dashboard["external_url"] is None
assert dashboard["available"] is True

embed = await IntegrationService(container).get_embed_target("livingmemory")
assert embed["target_url"] == expected
assert embed["embeddable"] is False
assert embed["available"] is True
assert "新窗口" in embed["message"]


async def test_livingmemory_official_page_resolves_dotted_module_path(tmp_path):
# 生产环境 star/base.py 会把 StarMetadata.module_path 覆写为 cls.__module__
# (点分导入路径),需通过 sys.modules 中已加载模块的 __file__ 还原插件目录。
import types

(tmp_path / "pages" / "dashboard").mkdir(parents=True)
(tmp_path / "pages" / "dashboard" / "index.html").write_text(
"<html></html>", encoding="utf-8"
)
fake_module = types.ModuleType("data.plugins.astrbot_plugin_livingmemory.main")
fake_module.__file__ = str(tmp_path / "main.py")
memory_star = SimpleNamespace(
name="astrbot_plugin_livingmemory",
display_name="astrbot_plugin_livingmemory",
root_dir_name="astrbot_plugin_livingmemory",
module_path="data.plugins.astrbot_plugin_livingmemory.main",
star_cls=SimpleNamespace(),
)
container = _lm_official_page_container(
memory_star,
astrbot_config={"dashboard": {"host": "0.0.0.0", "port": 6185}},
)

with patch.dict(
sys.modules,
{"data.plugins.astrbot_plugin_livingmemory.main": fake_module},
):
payload = await IntegrationService(container).get_status()

dashboard = {
item["id"]: item for item in payload["dashboards"]
}["livingmemory"]["dashboard"]
assert dashboard["official_page_url"] == (
"http://127.0.0.1:6185/#/plugin-page/astrbot_plugin_livingmemory/dashboard"
)
assert dashboard["embeddable"] is False


async def test_livingmemory_official_page_absent_without_pages_or_origin(tmp_path):
memory_star = SimpleNamespace(
name="astrbot_plugin_livingmemory",
display_name="astrbot_plugin_livingmemory",
root_dir_name="astrbot_plugin_livingmemory",
module_path=str(tmp_path / "main.py"),
star_cls=SimpleNamespace(),
)

# 插件没有 pages/ 目录:无从推断官方页入口
container = _lm_official_page_container(memory_star)
payload = await IntegrationService(container).get_status()
dashboard = {
item["id"]: item for item in payload["dashboards"]
}["livingmemory"]["dashboard"]
assert dashboard["official_page_url"] is None
assert dashboard["embeddable"] is None

# 有 pages/ 但容器没有 astrbot_config:拿不到 AstrBot Dashboard origin
(tmp_path / "pages" / "dashboard").mkdir(parents=True)
(tmp_path / "pages" / "dashboard" / "index.html").write_text(
"<html></html>", encoding="utf-8"
)
payload = await IntegrationService(container).get_status()
dashboard = {
item["id"]: item for item in payload["dashboards"]
}["livingmemory"]["dashboard"]
assert dashboard["official_page_url"] is None

embed = await IntegrationService(container).get_embed_target("livingmemory")
assert embed["target_url"] is None
assert embed["available"] is False

Large diffs are not rendered by default.

17 changes: 0 additions & 17 deletions web_res/static/dashboard/assets/index-BUfCwZyG.js

This file was deleted.

17 changes: 17 additions & 0 deletions web_res/static/dashboard/assets/index-DfjsDeQS.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions web_res/static/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<meta name="theme-color" content="#f4f7fb" />
<link rel="stylesheet" href="/static/vendor/material-icons/material-icons.css" />
<title>自主学习 · 监控板</title>
<script type="module" crossorigin src="/static/dashboard/assets/index-BUfCwZyG.js"></script>
<script type="module" crossorigin src="/static/dashboard/assets/index-DfjsDeQS.js"></script>
<link rel="modulepreload" crossorigin href="/static/dashboard/assets/echarts-BFYZiO-4.js">
<link rel="stylesheet" crossorigin href="/static/dashboard/assets/index-B_IP2WTj.css">
<link rel="stylesheet" crossorigin href="/static/dashboard/assets/index-B-FWb1Zi.css">
</head>
<body>
<noscript>此监控板需要启用 JavaScript。</noscript>
Expand Down
2 changes: 1 addition & 1 deletion web_src/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "astrbot-self-learning-dashboard",
"version": "4.2.0",
"version": "4.3.0",
"description": "Solid.js source for the self-learning plugin dashboard",
"type": "module",
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions web_src/src/components/business/IntegrationCard.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@
font-size: 30px;
}
}

.integration-card-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
11 changes: 9 additions & 2 deletions web_src/src/components/business/IntegrationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { textOrDash } from '../../lib/format';
import { object } from '../../pages/shared';
import styles from './IntegrationCard.module.scss';

export function IntegrationCard(props: { item: IntegrationItem; onOpen?: () => void }) {
export function IntegrationCard(props: {
item: IntegrationItem;
onOpen?: () => void;
onOpenOfficial?: () => void;
}) {
const available = () => props.item.active === true || object(props.item.dashboard).available === true;
return (
<Card class={styles['integration-card']}>
Expand All @@ -15,7 +19,10 @@ export function IntegrationCard(props: { item: IntegrationItem; onOpen?: () => v
</div>
<h3>{textOrDash(props.item.title ?? props.item.name ?? props.item.id)}</h3>
<p>{textOrDash(props.item.description ?? props.item.role)}</p>
<Show when={props.onOpen}><Button icon="open_in_new" disabled={!available()} onClick={props.onOpen}>打开面板</Button></Show>
<div class={styles['integration-card-actions']}>
<Show when={props.onOpen}><Button icon="open_in_new" disabled={!available()} onClick={props.onOpen}>打开面板</Button></Show>
<Show when={props.onOpenOfficial}><Button icon="dashboard" disabled={!available()} onClick={props.onOpenOfficial}>官方面板</Button></Show>
</div>
</Card>
);
}
17 changes: 16 additions & 1 deletion web_src/src/pages/integrations/IntegrationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,26 @@ export function IntegrationsPage() {
}
if (url) window.open(url, '_blank', 'noopener,noreferrer');
};
const openOfficialPage = (item: IntegrationItem) => {
const url = resolveHostUrl(String(object(item.dashboard).official_page_url ?? ''));
if (url) window.open(url, '_blank', 'noopener,noreferrer');
};
return (
<div class="page">
<PageHeader title="功能融合" description="查看配套插件状态,并导入世界书与 QQ 聊天记录。" icon="extension" actions={<Button icon="refresh" onClick={dashboard.loadIntegrations}>刷新状态</Button>} />
<Panel title="插件面板">
<div class={styles['integration-grid']}><For each={integrations()}>{(item) => <IntegrationCard item={item} onOpen={() => openIntegration(item)} />}</For></div>
<div class={styles['integration-grid']}>
<For each={integrations()}>{(item) => {
const officialUrl = String(object(item.dashboard).official_page_url ?? '');
return (
<IntegrationCard
item={item}
onOpen={() => openIntegration(item)}
onOpenOfficial={officialUrl ? () => openOfficialPage(item) : undefined}
/>
);
}}</For>
</div>
</Panel>
<div class={`two-column ${styles['integration-imports']}`}>
<Panel title="世界书导入" hint="先预览统计,再执行导入">
Expand Down
Loading
Loading