Skip to content

fix: adapt companion panel embedding and release 3.6.3 - #244

Merged
EterUltimate merged 3 commits into
mainfrom
fix/companion-panel-embed-3.6.3
Aug 28, 2026
Merged

fix: adapt companion panel embedding and release 3.6.3#244
EterUltimate merged 3 commits into
mainfrom
fix/companion-panel-embed-3.6.3

Conversation

@EterUltimate

@EterUltimate EterUltimate commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

概述

基于本地拉取最新的两个伴随插件仓库做兼容性适配(group_chat_plus 89ae2e1 / V1.2.3.hotfix.2,livingmemory c2e7330 / 2.6.0-beta.3),并完成 AstrBot 4.27.4(commit 1a0499878)下的兼容性验证。

问题:Group Chat Plus 面板 iframe 被静默拦截(有完整证据链)

  • 本插件「回复策略」页 web_src/src/pages/reply-strategy/ReplyStrategyPage.tsx 通过 iframe 加载同源嵌入壳 webui/blueprints/integrations.py,嵌入壳再以 iframe 指向 http://{host}:{port}/panel?embed=1webui/services/integration_service.py:292)。
  • 最新 group_chat_plus 的面板服务端固定返回 X-Frame-Options: DENY(其 web/server.py:408)与 CSP frame-ancestors 'none'web/server.py:445,自 commit 2ae46a1 起引入),且全代码无任何 embed 参数处理(grep 无结果)。
  • 结果:浏览器静默拒绝渲染内嵌面板,用户只看到空白框(嵌入壳页脚的「若浏览器阻止嵌入」提示即为此现象)。此前 [Bug] Group Chat Plus 面板 连接拒绝 #241 报告的「面板连接拒绝」属另一类问题(远程部署的浏览器侧地址解析,已由嵌入壳的 resolveTarget 修复),本 PR 处理的是框架头层面的拦截。

修复(仅后端,无需重建 Dashboard 静态资源)

  1. webui/services/integration_service.py
    • 新增 _probe_embeddable():对面板 URL 做一次 GET(2s 超时,结果缓存 60s),依据 X-Frame-Options / CSP frame-ancestors 判定是否允许 iframe 嵌入(嵌入壳与面板必然不同源,SAMEORIGIN/'self'/'none' 均视为拒绝)。
    • _group_chat_plus_dashboard 输出 embeddable,被阻止时 kind 降级为 external 并附提示;探测不可达(None)时保持原有可用性判定不变。
    • get_embed_target 透传 embeddable 并生成明确的 message
  2. webui/blueprints/integrations.py:嵌入壳在 embeddable=False 时不渲染注定失败的 iframe,改为渲染「面板禁止内嵌」提示块,保留头部「新窗口打开」入口。

兼容性复核结论(无需改动的部分)

  • LivingMemory 2.6.0-beta.3(mixin 大重构):本插件依赖的契约全部保持 —— 注册名 LivingMemoryplugin.initializer.memory_enginemain.py:114)、memory_engine.graph_storecore/managers/memory_engine.py:127)、graph_store.get_graph_snapshot(session_id, persona_id, limit_memories, limit_entries, limit_nodes, limit_edges)storage/graph_store_snapshot.py:320,async)与快照键 nodes/edges/entries/memoriesmemory_engine.get_statistics()。graph_store 直读适配器无需改动。
  • LivingMemory 独立 WebUI 配置(config_manager.webui_settings)已被 AstrBot 官方插件页面机制取代(schema 中已无 webui 配置,页面为 pages/dashboard/index.html,经 /plugins/{plugin_id}/pages/{page_name} 提供)。现有代码读不到该属性时本就优雅降级为「本地图谱」模式,故不改动;且 AstrBot 插件页面需 JWT 鉴权(Authorization 头),iframe 内无法携带凭据,不宜直接嵌入。
  • AstrBot 4.27.4 验证:插件在 AstrBot venv(Python 3.12.13)下整包编译零错误、astrbot_plugin_self_learning.main 及本次改动模块可导入;@filter.after_message_sent / on_llm_request / permission_type / platform_adapter_type / command 均存在于 astrbot/api/event/filter/__init__.py;已同步安装至 AstrBot/data/plugins/astrbot_plugin_self_learning(3.6.3)。

测试

  • 新增 6 个单元测试:frame 头判定规则、探测缓存(60s 内仅一次请求)、不可达返回 None、被阻止面板的 dashboard/嵌入目标降级行为、嵌入壳「禁止内嵌」与「允许嵌入」两种渲染。
  • 本地全套 745 → 751 passed,冒烟测试 tests/integration/test_package_imports.py test_webui_static_assets.py 19 passed,ruff 全部通过。

版本

  • 元数据、运行时包、Dashboard、文档版本统一提升至 3.6.3,CHANGELOG 增补 3.6.3 条目。

Summary by Sourcery

Adapt companion dashboard integration for modern panel security policies and release version 3.6.3.

New Features:

  • Detect companion panel response headers and clearly handle dashboards that cannot be embedded in an iframe by offering a new-window fallback.

Bug Fixes:

  • Prevent Group Chat Plus panels blocked by X-Frame-Options or CSP frame-ancestors policies from appearing as blank embedded panels.
  • Avoid blocking the Quart event loop during panel compatibility checks and consolidate concurrent probes for the same target.

Enhancements:

  • Improve frame-ancestor validation against the Dashboard origin and expose embedding status and explanatory messages through integration APIs.
  • Revalidate LivingMemory 2.6.0-beta.3 compatibility without requiring adapter changes.

Build:

  • Normalize Dashboard HTML line endings and enforce LF output across platforms with repository attributes.

Documentation:

  • Document iframe-blocked companion panels and the automatic external-panel fallback.

Tests:

  • Add coverage for frame-header policies, probe caching and concurrency, unreachable panels, fallback metadata, and embedded-shell rendering.

Chores:

  • Bump plugin, runtime package, Dashboard, and documentation versions to 3.6.3 and update the changelog.

适配 Group Chat Plus 最新面板安全策略(v1.2.x 起固定返回 X-Frame-Options: DENY
与 CSP frame-ancestors 'none',且未支持 ?embed=1),其面板 iframe 会被浏览器
静默拦截,回复策略页只显示空白框。集成服务现探测面板真实响应头(缓存 60 秒),
被阻止时降级为 external 模式:嵌入壳展示明确提示并保留新窗口打开入口,探测
不可达时保持原可用性判定。同时复核 LivingMemory 2.6.0-beta.3:注册名、
initializer.memory_engine、memory_engine.graph_store、get_graph_snapshot 签名
与 get_statistics 在 mixin 重构后全部保持兼容,无需改动。

新增响应头判定、探测缓存、降级行为与嵌入壳渲染的单元测试,版本统一提升至
3.6.3,并更新集成文档。
@sourcery-ai

sourcery-ai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

本 PR 通过运行时探测 Group Chat Plus 的安全响应头,识别 iframe 不可嵌入场景并将嵌入壳安全降级为新窗口打开,同时补充测试并统一发布 3.6.3 版本;LivingMemory 兼容性经复核无需代码调整。

Sequence diagram for Group Chat Plus iframe compatibility detection

sequenceDiagram
    participant Browser
    participant EmbedShell as integrations.py
    participant Service as IntegrationService
    participant Panel as GroupChatPlusPanel

    Browser->>EmbedShell: GET embed target
    EmbedShell->>Service: get_embed_target(group_chat_plus)
    Service->>Panel: _probe_embeddable(panel_url)
    Panel-->>Service: HTTP response headers
    Service->>Service: _frame_headers_block(headers)
    Service-->>EmbedShell: embeddable and message
    alt embeddable is false
        EmbedShell-->>Browser: Render blocked notice and new-window link
    else embeddable is true
        EmbedShell-->>Browser: Render iframe
        Browser->>Panel: Load panel URL in iframe
    else panel unreachable
        EmbedShell-->>Browser: Preserve existing availability behavior
    end
Loading

Flow diagram for blocked companion panel fallback

flowchart TD
    A[Build Group Chat Plus panel URL] --> B[_probe_embeddable]
    B --> C{Response headers block iframe?}
    C -->|Yes| D[Set kind to external]
    D --> E[Render panel forbidden notice]
    E --> F[Keep new-window link]
    C -->|No| G[Keep embedded_external]
    G --> H[Render iframe]
    C -->|Unreachable| I[Return None]
    I --> J[Preserve existing availability behavior]
Loading

File-Level Changes

Change Details Files
检测伴随面板响应头并在禁止 iframe 时自动降级为外部打开模式。
  • 新增带 2 秒超时和 60 秒缓存的面板探测。
  • 解析 X-Frame-Options 与 CSP frame-ancestors,区分可嵌入、被阻止和不可达状态。
  • 为 Group Chat Plus dashboard 与 embed target 透传 embeddable 状态、入口类型和用户提示。
webui/services/integration_service.py
让嵌入壳根据面板兼容性渲染明确的降级提示。
  • 禁止嵌入时不创建 iframe,展示原因及新窗口入口。
  • 可嵌入时保留原 iframe 渲染,不可用时保留原空状态。
webui/blueprints/integrations.py
增加集成兼容性和嵌入降级行为的自动化覆盖。
  • 覆盖响应头判定、探测缓存和不可达处理。
  • 覆盖 Group Chat Plus 状态降级及嵌入壳两种渲染路径。
tests/unit/test_integration_service.py
将项目及相关发布文档统一升级到 3.6.3,并记录兼容性结论。
  • 同步插件元数据、Python 包、Dashboard、README 和文档版本。
  • 补充 Group Chat Plus iframe 限制与 LivingMemory 契约复核说明。
__init__.py
metadata.yaml
web_src/package.json
README.md
README_EN.md
docs/README.md
CHANGELOG.md
docs/integrations.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="webui/services/integration_service.py" line_range="132" />
<code_context>
+        request = urllib.request.Request(
+            url, headers={"User-Agent": "self-learning-embed-probe"}
+        )
+        with urllib.request.urlopen(request, timeout=_EMBED_PROBE_TIMEOUT) as response:
+            result = not _frame_headers_block(response.headers)
+    except Exception:
</code_context>
<issue_to_address>
**issue (performance):** `_probe_embeddable` performs a synchronous `urllib.request.urlopen` with a two-second timeout from request handlers that are async Quart coroutines, blocking the event loop while the companion panel is slow or unreachable. During that probe, unrelated WebUI requests handled by the same worker stop progressing.

**Triggers:** When the Group Chat Plus panel is unavailable or takes longer than the probe timeout to respond.

**Suggested fix:** Run the blocking probe in an executor or replace it with an async HTTP client before calling it from async handlers.
</issue_to_address>

### Comment 2
<location path="webui/services/integration_service.py" line_range="108-114" />
<code_context>
+    xfo = str(headers.get("X-Frame-Options", "") or "").strip().strip("'\"").lower()
+    if xfo in {"deny", "sameorigin"}:
+        return True
+    csp = str(headers.get("Content-Security-Policy", "") or "").lower()
+    for directive in csp.split(";"):
+        parts = directive.split()
+        if parts and parts[0] == "frame-ancestors":
+            values = [value.strip("'\"") for value in parts[1:]]
+            # 嵌入页与伴随面板必然是不同源(不同端口),'self' 与 'none' 均视为拒绝。
+            return not values or "none" in values or "self" in values
+    return False
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The CSP parser rejects any policy containing `'self'` in `frame-ancestors`, even when the same directive also explicitly allows the self-learning Dashboard origin. A valid policy such as `frame-ancestors 'self' https://dashboard.example` is therefore reported as non-embeddable despite allowing this parent origin.

**Triggers:** When a companion panel uses a CSP `frame-ancestors` allowlist containing both `'self'` and the self-learning Dashboard origin.

**Suggested fix:** Parse the allowed source list against the actual parent origin instead of treating the presence of `'self'` as an unconditional rejection.
</issue_to_address>

### Comment 3
<location path="webui/services/integration_service.py" line_range="123-136" />
<code_context>
+
+    返回 True=允许、False=被响应头阻止、None=不可达。结果缓存 60 秒。
+    """
+    now = time.time()
+    cached = _EMBED_PROBE_CACHE.get(url)
+    if cached and now - cached[0] < _EMBED_PROBE_TTL:
+        return cached[1]
+    result: Optional[bool] = None
+    try:
+        request = urllib.request.Request(
+            url, headers={"User-Agent": "self-learning-embed-probe"}
+        )
+        with urllib.request.urlopen(request, timeout=_EMBED_PROBE_TIMEOUT) as response:
+            result = not _frame_headers_block(response.headers)
+    except Exception:
+        result = None
+    _EMBED_PROBE_CACHE[url] = (now, result)
+    return result
+
</code_context>
<issue_to_address>
**nitpick (bug_risk):** The global probe cache is read and written without synchronization, so concurrent status or embed requests that arrive during a cache miss each perform their own network probe instead of sharing one result. This defeats the intended one-request-per-cache-window behavior under concurrent dashboard loads.

**Triggers:** When multiple WebUI requests for the same Group Chat Plus panel arrive concurrently after the cache entry expires or is absent.

**Suggested fix:** Protect cache lookup and refresh with a lock or coalesce in-flight probes per URL.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: webui/services/integration_service.py:132, webui/services/integration_service.py:114


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread webui/services/integration_service.py
Comment thread webui/services/integration_service.py Outdated
Comment thread webui/services/integration_service.py Outdated
Dashboard build 工作流的产物一致性检查(git diff --exit-code)自 3.6.x 起失败:
web_res/static/dashboard/index.html 由 Windows 直接提交时为 CRLF,而 CI 在
Linux 上以 LF 模板构建产出 LF,整文件被判为差异。统一为 LF,并新增
.gitattributes 将 web_src/index.html 与该产物固定为 text eol=lf。
按审查意见修正三处探测实现:
- 阻塞的 urllib 探测移入 executor 执行,避免在 Quart 异步处理器中卡住事件循环。
- frame-ancestors 白名单按父页面真实 origin 逐项匹配(含 *.<domain> 通配、
  端口与协议校验),修复 'self' 与显式放行源共存时被误判为禁止嵌入。
- 同一面板的并发未命中探测通过 in-flight future 合并为一次请求。
- IntegrationService.get_status/get_embed_target 改为 async,更新 hub 服务、
  page_api 调用点与 hub 蓝图测试桩;新增 origin 匹配与并发合并测试。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@EterUltimate
EterUltimate merged commit f1cd2c0 into main Aug 28, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant