fix: adapt companion panel embedding and release 3.6.3 - #244
Merged
Conversation
适配 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,并更新集成文档。
Contributor
Reviewer's Guide本 PR 通过运行时探测 Group Chat Plus 的安全响应头,识别 iframe 不可嵌入场景并将嵌入壳安全降级为新窗口打开,同时补充测试并统一发布 3.6.3 版本;LivingMemory 兼容性经复核无需代码调整。 Sequence diagram for Group Chat Plus iframe compatibility detectionsequenceDiagram
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
Flow diagram for blocked companion panel fallbackflowchart 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]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
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
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
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 匹配与并发合并测试。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
基于本地拉取最新的两个伴随插件仓库做兼容性适配(group_chat_plus
89ae2e1/ V1.2.3.hotfix.2,livingmemoryc2e7330/ 2.6.0-beta.3),并完成 AstrBot4.27.4(commit1a0499878)下的兼容性验证。问题:Group Chat Plus 面板 iframe 被静默拦截(有完整证据链)
web_src/src/pages/reply-strategy/ReplyStrategyPage.tsx通过 iframe 加载同源嵌入壳webui/blueprints/integrations.py,嵌入壳再以 iframe 指向http://{host}:{port}/panel?embed=1(webui/services/integration_service.py:292)。X-Frame-Options: DENY(其web/server.py:408)与 CSPframe-ancestors 'none'(web/server.py:445,自 commit2ae46a1起引入),且全代码无任何embed参数处理(grep 无结果)。修复(仅后端,无需重建 Dashboard 静态资源)
webui/services/integration_service.py:_probe_embeddable():对面板 URL 做一次 GET(2s 超时,结果缓存 60s),依据X-Frame-Options/ CSPframe-ancestors判定是否允许 iframe 嵌入(嵌入壳与面板必然不同源,SAMEORIGIN/'self'/'none'均视为拒绝)。_group_chat_plus_dashboard输出embeddable,被阻止时kind降级为external并附提示;探测不可达(None)时保持原有可用性判定不变。get_embed_target透传embeddable并生成明确的message。webui/blueprints/integrations.py:嵌入壳在embeddable=False时不渲染注定失败的 iframe,改为渲染「面板禁止内嵌」提示块,保留头部「新窗口打开」入口。兼容性复核结论(无需改动的部分)
LivingMemory、plugin.initializer.memory_engine(main.py:114)、memory_engine.graph_store(core/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/memories、memory_engine.get_statistics()。graph_store 直读适配器无需改动。config_manager.webui_settings)已被 AstrBot 官方插件页面机制取代(schema 中已无 webui 配置,页面为pages/dashboard/index.html,经/plugins/{plugin_id}/pages/{page_name}提供)。现有代码读不到该属性时本就优雅降级为「本地图谱」模式,故不改动;且 AstrBot 插件页面需 JWT 鉴权(Authorization 头),iframe 内无法携带凭据,不宜直接嵌入。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)。测试
745 → 751 passed,冒烟测试tests/integration/test_package_imports.py test_webui_static_assets.py19 passed,ruff 全部通过。版本
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:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests:
Chores: