Merge remote-tracking branch 'langbot-app/master' into feat/sandbox

Resolve conflicts in:
- .github/workflows/run-tests.yml: keep master's src/langbot/** paths plus feat/** push branch
- src/langbot/pkg/plugin/connector.py: keep both branches' marketplace MCP/skill
  install logic (HEAD) and runtime/wait helpers (master); add missing return in
  _inspect_plugin_package so LOCAL/GITHUB install paths get author/name back
- tests/unit_tests/pipeline/test_n8nsvapi.py: keep HEAD's try/finally sys.modules
  save/restore pattern
- web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx: union
  imports + keep HEAD's disable_if/tooltip support and master's QrCodeLoginDialog
- web/src/i18n/locales/*: union of disjoint top-level keys from both branches
- web/src/app/home/market/page.tsx: accept our deletion (unified extensions page)
- uv.lock: regenerate via uv sync --dev
This commit is contained in:
Junyan Qin
2026-05-20 23:58:21 +08:00
209 changed files with 39875 additions and 4661 deletions

View File

@@ -11,8 +11,10 @@ import zipfile
import yaml
from urllib.parse import urlparse
import posixpath
import sqlalchemy
from .....core import taskmgr
from .....entity.persistence import plugin as persistence_plugin
from .. import group
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
@@ -43,6 +45,16 @@ def _normalize_plugin_asset_path(filepath: str) -> str | None:
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')
class PluginsRouterGroup(group.RouterGroup):
@staticmethod
@@ -233,7 +245,15 @@ class PluginsRouterGroup(group.RouterGroup):
return self.http_status(404, -1, 'plugin not found')
if quart.request.method == 'GET':
return self.success(data={'config': plugin['plugin_config']})
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_plugin.PluginSetting.config)
.where(persistence_plugin.PluginSetting.plugin_author == author)
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
)
persisted_config = result.scalar_one_or_none()
config = persisted_config if persisted_config is not None else plugin['plugin_config']
return self.success(data={'config': config})
elif quart.request.method == 'PUT':
data = await quart.request.json
@@ -284,7 +304,7 @@ class PluginsRouterGroup(group.RouterGroup):
# CSP for HTML pages served to sandboxed iframes (opaque origin).
# 'self' doesn't work in sandboxed iframes — use actual server origin.
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'] = (
f'default-src {origin}; '
f"script-src {origin} 'unsafe-inline'; "