mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-23 01:46:37 +08:00
fix(plugin): honor requested marketplace version (#2555)
This commit is contained in:
@@ -139,6 +139,30 @@ def _decode_json_object(body: bytes, *, subject: str) -> dict[str, Any]:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _select_marketplace_plugin_version(
|
||||||
|
versions: Any,
|
||||||
|
*,
|
||||||
|
requested_version: str | None,
|
||||||
|
plugin_author: str,
|
||||||
|
plugin_name: str,
|
||||||
|
) -> str:
|
||||||
|
if not isinstance(versions, list) or not versions:
|
||||||
|
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions')
|
||||||
|
|
||||||
|
if requested_version is None:
|
||||||
|
candidate = versions[0]
|
||||||
|
if not isinstance(candidate, dict) or not candidate.get('version'):
|
||||||
|
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions')
|
||||||
|
return str(candidate['version'])
|
||||||
|
|
||||||
|
for candidate in versions:
|
||||||
|
if isinstance(candidate, dict) and str(candidate.get('version') or '') == requested_version:
|
||||||
|
return requested_version
|
||||||
|
raise ValueError(
|
||||||
|
f'Plugin {plugin_author}/{plugin_name} version {requested_version} is not available in marketplace'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PluginRuntimeNotConnectedError(RuntimeError):
|
class PluginRuntimeNotConnectedError(RuntimeError):
|
||||||
"""Raised when plugin runtime operations are requested before connection."""
|
"""Raised when plugin runtime operations are requested before connection."""
|
||||||
|
|
||||||
@@ -1611,6 +1635,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
execution_context: ExecutionContext,
|
execution_context: ExecutionContext,
|
||||||
plugin_author: str,
|
plugin_author: str,
|
||||||
plugin_name: str,
|
plugin_name: str,
|
||||||
|
plugin_version: str | None,
|
||||||
task_context: taskmgr.TaskContext | None,
|
task_context: taskmgr.TaskContext | None,
|
||||||
) -> tuple[bytes | None, str | None]:
|
) -> tuple[bytes | None, str | None]:
|
||||||
"""Return a plugin package, or install an MCP/skill and return none."""
|
"""Return a plugin package, or install an MCP/skill and return none."""
|
||||||
@@ -1675,20 +1700,19 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
subject='Marketplace plugin versions',
|
subject='Marketplace plugin versions',
|
||||||
)
|
)
|
||||||
versions = versions_payload.get('data', {}).get('versions', [])
|
versions = versions_payload.get('data', {}).get('versions', [])
|
||||||
if (
|
requested_version = str(plugin_version or '').strip()
|
||||||
not isinstance(versions, list)
|
version = _select_marketplace_plugin_version(
|
||||||
or not versions
|
versions,
|
||||||
or not isinstance(versions[0], dict)
|
requested_version=requested_version or None,
|
||||||
or not versions[0].get('version')
|
plugin_author=plugin_author,
|
||||||
):
|
plugin_name=plugin_name,
|
||||||
raise ValueError(f'Plugin {plugin_author}/{plugin_name} has no versions')
|
)
|
||||||
latest_version = str(versions[0]['version'])
|
|
||||||
_download_status, plugin_package = await _marketplace_get(
|
_download_status, plugin_package = await _marketplace_get(
|
||||||
client,
|
client,
|
||||||
f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{latest_version}',
|
f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{version}',
|
||||||
max_bytes=_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES,
|
max_bytes=_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES,
|
||||||
)
|
)
|
||||||
return plugin_package, latest_version
|
return plugin_package, version
|
||||||
|
|
||||||
def _admit_plugin_archive(
|
def _admit_plugin_archive(
|
||||||
self,
|
self,
|
||||||
@@ -1746,6 +1770,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
execution_context,
|
execution_context,
|
||||||
plugin_author,
|
plugin_author,
|
||||||
plugin_name,
|
plugin_name,
|
||||||
|
str(install_info.get('plugin_version') or '') or None,
|
||||||
task_context,
|
task_context,
|
||||||
)
|
)
|
||||||
if file_bytes is None:
|
if file_bytes is None:
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from langbot.pkg.plugin.connector import _select_marketplace_plugin_version
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('requested_version', 'expected'),
|
||||||
|
[
|
||||||
|
(None, '0.1.4'),
|
||||||
|
('0.1.3', '0.1.3'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_select_marketplace_plugin_version(requested_version, expected):
|
||||||
|
assert (
|
||||||
|
_select_marketplace_plugin_version(
|
||||||
|
[{'version': '0.1.4'}, {'version': '0.1.3'}],
|
||||||
|
requested_version=requested_version,
|
||||||
|
plugin_author='langbot-team',
|
||||||
|
plugin_name='RunnerDemo',
|
||||||
|
)
|
||||||
|
== expected
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_marketplace_plugin_version_rejects_requested_missing_version():
|
||||||
|
with pytest.raises(ValueError, match='version 0.1.2 is not available'):
|
||||||
|
_select_marketplace_plugin_version(
|
||||||
|
[{'version': '0.1.4'}],
|
||||||
|
requested_version='0.1.2',
|
||||||
|
plugin_author='langbot-team',
|
||||||
|
plugin_name='RunnerDemo',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('versions', [[], [{'unexpected': 'value'}], 'not-a-list'])
|
||||||
|
def test_select_marketplace_plugin_version_rejects_invalid_latest(versions):
|
||||||
|
with pytest.raises(ValueError, match='has no versions'):
|
||||||
|
_select_marketplace_plugin_version(
|
||||||
|
versions,
|
||||||
|
requested_version=None,
|
||||||
|
plugin_author='langbot-team',
|
||||||
|
plugin_name='RunnerDemo',
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user