fix(plugin): align plugin list endpoint counts with live endpoint state (#37179)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
呆萌闷油瓶
2026-06-10 08:11:11 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 4fb3210f9a
commit 9ac71329a4
10 changed files with 345 additions and 23 deletions
+8 -2
View File
@@ -36,7 +36,10 @@ class PluginEndpointClient(BasePluginClient):
def list_endpoints(self, tenant_id: str, user_id: str, page: int, page_size: int):
"""
List all endpoints for the given tenant and user.
List all endpoints for the given tenant.
The daemon list route binds only tenant and pagination fields; user_id is
retained in this client signature for consistency with endpoint services.
"""
return self._request_with_plugin_daemon_response(
"GET",
@@ -47,7 +50,10 @@ class PluginEndpointClient(BasePluginClient):
def list_endpoints_for_single_plugin(self, tenant_id: str, user_id: str, plugin_id: str, page: int, page_size: int):
"""
List all endpoints for the given tenant, user and plugin.
List all endpoints for the given tenant and plugin.
The daemon list route binds tenant, plugin and pagination fields; user_id
is retained in this client signature for consistency with endpoint services.
"""
return self._request_with_plugin_daemon_response(
"GET",
+103 -3
View File
@@ -4,6 +4,12 @@ This module owns plugin daemon management calls that are shared by API services
and core runtimes. Plugin model provider discovery is cached here, alongside
plugin install, uninstall, and upgrade invalidation, so all cache mutations for
plugin-owned provider metadata stay tenant-scoped and in one place.
The console plugin list also normalizes endpoint setup counters against live
endpoint records. Some plugin daemon builds return stale ``endpoints_*``
aggregates in ``management/list`` even while plugin-scoped endpoint queries are
current, so the API reconciles those counts before serving workspace plugin
metadata.
"""
import logging
@@ -38,6 +44,7 @@ from core.plugin.entities.plugin_daemon import (
)
from core.plugin.impl.asset import PluginAssetManager
from core.plugin.impl.debugging import PluginDebuggingClient
from core.plugin.impl.endpoint import PluginEndpointClient
from core.plugin.impl.model import PluginModelClient
from core.plugin.impl.plugin import PluginInstaller
from extensions.ext_database import db
@@ -69,6 +76,9 @@ class PluginService:
REDIS_TTL = 60 * 5 # 5 minutes
PLUGIN_MODEL_PROVIDERS_REDIS_KEY_PREFIX = "plugin_model_providers:tenant_id:"
PLUGIN_INSTALL_TASK_TERMINAL_STATUSES = (PluginInstallTaskStatus.Success, PluginInstallTaskStatus.Failed)
# Mirror the detail-panel endpoint query size so list reconciliation and
# the visible endpoint drawer exercise the same daemon pagination path.
ENDPOINT_RECONCILIATION_PAGE_SIZE = 100
@classmethod
def _get_plugin_model_providers_cache_key(cls, tenant_id: str) -> str:
@@ -287,14 +297,104 @@ class PluginService:
return plugins
@staticmethod
def list_with_total(tenant_id: str, page: int, page_size: int) -> PluginListResponse:
"""
list all plugins of the tenant
def list_with_total(tenant_id: str, user_id: str, page: int, page_size: int) -> PluginListResponse:
"""List tenant plugins with endpoint counts reconciled from live records.
The plugin daemon's ``management/list`` payload is tenant-scoped, but
some daemon builds undercount or stale-cache plugin endpoint aggregates.
The list response therefore refreshes counters from the daemon's
tenant-scoped endpoint records before returning workspace plugin metadata.
"""
manager = PluginInstaller()
plugins = manager.list_plugins_with_total(tenant_id, page, page_size)
PluginService._reconcile_endpoint_counts(tenant_id, user_id, plugins.list)
return plugins
@staticmethod
def _normalize_endpoint_count(value: object) -> int:
"""Convert daemon endpoint counters to safe non-negative integers.
Some daemon builds use ``-1`` as an "unknown / not synced yet" sentinel
for endpoint counters. That value is acceptable internally as a daemon
transport detail, but it must never leak through the console API because
the UI displays these counters directly.
"""
if value is None:
return 0
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return max(0, value)
if isinstance(value, str):
try:
return max(0, int(value))
except ValueError:
return 0
return 0
@classmethod
def _normalize_plugin_endpoint_counts(cls, plugin: PluginEntity) -> None:
"""Clamp endpoint counters on plugin entities before returning them."""
plugin.endpoints_setups = cls._normalize_endpoint_count(plugin.endpoints_setups)
plugin.endpoints_active = cls._normalize_endpoint_count(plugin.endpoints_active)
@classmethod
def _reconcile_endpoint_counts(cls, tenant_id: str, user_id: str, plugins: Sequence[PluginEntity]) -> None:
"""Refresh endpoint counters from live plugin endpoint records.
``management/list`` is the source of truth for plugin installations, but
some daemon versions lag when populating ``endpoints_setups`` and
``endpoints_active``. The plugin-scoped endpoint listing is the same
tenant-scoped source the console detail panel uses after reinstall flows,
so the list view recomputes counts per plugin instead of trusting stale
daemon aggregates.
"""
endpoint_client = PluginEndpointClient()
for plugin in plugins:
cls._normalize_plugin_endpoint_counts(plugin)
if plugin.declaration.endpoint is None:
continue
page = 1
endpoints_setups = 0
endpoints_active = 0
try:
while True:
endpoints = endpoint_client.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=plugin.plugin_id,
page=page,
page_size=cls.ENDPOINT_RECONCILIATION_PAGE_SIZE,
)
endpoints_setups += len(endpoints)
endpoints_active += sum(int(endpoint.enabled) for endpoint in endpoints)
if len(endpoints) < cls.ENDPOINT_RECONCILIATION_PAGE_SIZE:
break
page += 1
except Exception:
logger.warning(
(
"Failed to reconcile live endpoint counters for tenant %s plugin %s; "
"falling back to daemon plugin stats."
),
tenant_id,
plugin.plugin_id,
exc_info=True,
)
continue
plugin.endpoints_setups = cls._normalize_endpoint_count(endpoints_setups)
plugin.endpoints_active = cls._normalize_endpoint_count(endpoints_active)
@staticmethod
def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]:
"""