Compare commits

...

2 Commits

Author SHA1 Message Date
huanghuoguoguo
779cf9899f fix(plugin): use specific runtime not connected error 2026-05-16 11:05:31 +08:00
RockChinQ
b251fc4b89 fix(plugin): resolve plugin page asset origin 2026-05-14 15:39:17 +08:00
4 changed files with 51 additions and 2 deletions

View File

@@ -39,6 +39,16 @@ def _normalize_plugin_asset_path(filepath: str) -> str | None:
return f'assets/{normalized}' return f'assets/{normalized}'
def _get_request_origin() -> str:
"""Return the public request origin, respecting reverse-proxy headers."""
forwarded_proto = quart.request.headers.get('X-Forwarded-Proto', '').split(',')[0].strip()
forwarded_host = quart.request.headers.get('X-Forwarded-Host', '').split(',')[0].strip()
scheme = forwarded_proto or quart.request.scheme
host = forwarded_host or quart.request.host
return f'{scheme}://{host}'
@group.group_class('plugins', '/api/v1/plugins') @group.group_class('plugins', '/api/v1/plugins')
class PluginsRouterGroup(group.RouterGroup): class PluginsRouterGroup(group.RouterGroup):
async def _check_extensions_limit(self) -> str | None: async def _check_extensions_limit(self) -> str | None:
@@ -189,7 +199,7 @@ class PluginsRouterGroup(group.RouterGroup):
# CSP for HTML pages served to sandboxed iframes (opaque origin). # CSP for HTML pages served to sandboxed iframes (opaque origin).
# 'self' doesn't work in sandboxed iframes — use actual server origin. # 'self' doesn't work in sandboxed iframes — use actual server origin.
if mime_type and mime_type.startswith('text/html'): if mime_type and mime_type.startswith('text/html'):
origin = f'{quart.request.scheme}://{quart.request.host}' origin = _get_request_origin()
resp.headers['Content-Security-Policy'] = ( resp.headers['Content-Security-Policy'] = (
f'default-src {origin}; ' f'default-src {origin}; '
f"script-src {origin} 'unsafe-inline'; " f"script-src {origin} 'unsafe-inline'; "

View File

@@ -35,6 +35,10 @@ from ..core import taskmgr
from ..entity.persistence import plugin as persistence_plugin from ..entity.persistence import plugin as persistence_plugin
class PluginRuntimeNotConnectedError(RuntimeError):
"""Raised when plugin runtime operations are requested before connection."""
class PluginRuntimeConnector: class PluginRuntimeConnector:
"""Plugin runtime connector""" """Plugin runtime connector"""
@@ -192,7 +196,7 @@ class PluginRuntimeConnector:
async def ping_plugin_runtime(self): async def ping_plugin_runtime(self):
if not hasattr(self, 'handler'): if not hasattr(self, 'handler'):
raise Exception('Plugin runtime is not connected') raise PluginRuntimeNotConnectedError('Plugin runtime is not connected')
return await self.handler.ping() return await self.handler.ping()

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.plugin.connector import PluginRuntimeConnector, PluginRuntimeNotConnectedError
def make_connector() -> PluginRuntimeConnector:
app = SimpleNamespace(instance_config=SimpleNamespace(data={'plugin': {'enable': True}}))
return PluginRuntimeConnector(app, AsyncMock())
@pytest.mark.asyncio
async def test_ping_plugin_runtime_raises_specific_error_when_not_connected():
connector = make_connector()
with pytest.raises(PluginRuntimeNotConnectedError, match='Plugin runtime is not connected'):
await connector.ping_plugin_runtime()
@pytest.mark.asyncio
async def test_ping_plugin_runtime_delegates_to_connected_handler():
connector = make_connector()
connector.handler = SimpleNamespace(ping=AsyncMock(return_value='pong'))
result = await connector.ping_plugin_runtime()
assert result == 'pong'
connector.handler.ping.assert_awaited_once()

View File

@@ -590,6 +590,9 @@ export class BackendClient extends BaseHttpClient {
name: string, name: string,
filepath: string, filepath: string,
): string { ): string {
if (this.instance.defaults.baseURL === '/') {
return `${window.location.origin}/api/v1/plugins/${author}/${name}/assets/${filepath}`;
}
return ( return (
this.instance.defaults.baseURL + this.instance.defaults.baseURL +
`/api/v1/plugins/${author}/${name}/assets/${filepath}` `/api/v1/plugins/${author}/${name}/assets/${filepath}`