Skip to content

Commit 077ae0b

Browse files
committed
fix(cron): refresh official category for official-gateway MCP servers
1 parent 12fd402 commit 077ae0b

3 files changed

Lines changed: 181 additions & 3 deletions

File tree

src/dashboard/apigateway/apigateway/apps/mcp_server/tasks.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
ADD_STAGE_MCP_SERVER_PERMISSIONS_BEFORE_RELEASE_UPDATE_TASK_NAME,
3030
RECONCILE_STAGE_MCP_SERVER_PERMISSIONS_AFTER_RELEASE_TASK_NAME,
3131
)
32-
from apigateway.apps.mcp_server.models import MCPServer
32+
from apigateway.apps.mcp_server.models import MCPServer, MCPServerCategory
3333
from apigateway.biz.mcp_server import (
3434
MCPServerHandler,
3535
MCPServerPromptHandler,
@@ -444,3 +444,25 @@ def sync_mcp_server_after_release(
444444
stage_id,
445445
release_history_id,
446446
)
447+
448+
449+
@shared_task(name="apigateway.apps.mcp_server.tasks.refresh_official_mcp_server_category", ignore_result=True)
450+
def refresh_official_mcp_server_category() -> None:
451+
"""定时给官方网关下的 MCP Server 补齐「官方」分类,只补不删。"""
452+
official_category = MCPServerCategory.objects.filter(name="Official", is_active=True).first()
453+
if official_category is None:
454+
return
455+
456+
missing_ids = list(
457+
MCPServer.objects.filter(gateway__is_official=True)
458+
.exclude(categories=official_category)
459+
.values_list("id", flat=True)
460+
)
461+
if not missing_ids:
462+
return
463+
464+
batch_size = 500
465+
for start in range(0, len(missing_ids), batch_size):
466+
official_category.mcp_servers.add(*missing_ids[start : start + batch_size])
467+
468+
logger.info("refresh_official_mcp_server_category completed, added=%d", len(missing_ids))

src/dashboard/apigateway/apigateway/conf/celery_conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"task": "apigateway.apps.mcp_server.tasks.sync_mcp_server_prompts",
8484
"schedule": crontab(minute="*/10"),
8585
},
86+
"apigateway.apps.mcp_server.tasks.refresh_official_mcp_server_category": {
87+
"task": "apigateway.apps.mcp_server.tasks.refresh_official_mcp_server_category",
88+
"schedule": crontab(minute=0),
89+
},
8690
}
8791

8892
CELERY_CHORD_UNLOCK_MAX_RETRIES = 60

src/dashboard/apigateway/apigateway/tests/apps/mcp_server/test_tasks.py

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,32 @@
1616
# to the current version of the project delivered to anyone in the future.
1717
#
1818
from unittest.mock import Mock, call, patch
19+
from uuid import uuid4
1920

2021
import pytest
22+
from celery.schedules import crontab
2123
from ddf import G
2224

23-
from apigateway.apps.mcp_server.models import MCPServer
25+
from apigateway.apps.mcp_server.constants import (
26+
FEATURED_MCP_CATEGORY_NAME,
27+
OFFICIAL_MCP_CATEGORY_NAME,
28+
MCPServerStatusEnum,
29+
)
30+
from apigateway.apps.mcp_server.models import MCPServer, MCPServerCategory
2431
from apigateway.apps.mcp_server.tasks import (
2532
_fetch_updated_prompts,
2633
add_stage_mcp_server_permissions_before_release_update,
2734
reconcile_stage_mcp_server_permissions_after_release,
35+
refresh_official_mcp_server_category,
2836
sync_mcp_server_after_release,
2937
)
38+
from apigateway.conf.celery_conf import CELERY_BEAT_SCHEDULE
3039
from apigateway.core.constants import (
3140
PublishEventNameTypeEnum,
3241
PublishEventStatusEnum,
3342
ReleaseHistoryStatusEnum,
3443
)
35-
from apigateway.core.models import PublishEvent, Release, ReleaseHistory, ResourceVersion
44+
from apigateway.core.models import Gateway, PublishEvent, Release, ReleaseHistory, ResourceVersion, Stage
3645

3746

3847
class TestFetchUpdatedPrompts:
@@ -367,3 +376,146 @@ def test_save_exception_logged(self, fake_gateway, fake_stage, release_history):
367376
release_history_id=release_history.id,
368377
mcp_servers_data=mcp_data,
369378
)
379+
380+
381+
class TestRefreshOfficialMcpServerCategory:
382+
def _category(self, name: str, display_name: str, *, is_active: bool = True) -> MCPServerCategory:
383+
category, _ = MCPServerCategory.objects.get_or_create(
384+
name=name,
385+
defaults={"display_name": display_name, "is_active": is_active},
386+
)
387+
if category.is_active != is_active or category.display_name != display_name:
388+
category.is_active = is_active
389+
category.display_name = display_name
390+
category.save(update_fields=["is_active", "display_name"])
391+
return category
392+
393+
def _gateway(self, *, is_official: bool) -> tuple[Gateway, Stage]:
394+
gateway = G(Gateway, is_official=is_official)
395+
return gateway, G(Stage, gateway=gateway)
396+
397+
def _server(self, gateway: Gateway, stage: Stage, **kwargs) -> MCPServer:
398+
return G(
399+
MCPServer,
400+
gateway=gateway,
401+
stage=stage,
402+
name=f"mcp-{uuid4().hex[:12]}",
403+
**kwargs,
404+
)
405+
406+
def _category_names(self, mcp_server: MCPServer) -> set[str]:
407+
return set(mcp_server.categories.values_list("name", flat=True))
408+
409+
def test_adds_official_category_to_all_servers_on_official_gateway(self):
410+
official = self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
411+
gateway, stage = self._gateway(is_official=True)
412+
public_active = self._server(
413+
gateway,
414+
stage,
415+
is_public=True,
416+
status=MCPServerStatusEnum.ACTIVE.value,
417+
)
418+
private_inactive = self._server(
419+
gateway,
420+
stage,
421+
is_public=False,
422+
status=MCPServerStatusEnum.INACTIVE.value,
423+
)
424+
425+
refresh_official_mcp_server_category()
426+
427+
assert official in public_active.categories.all()
428+
assert official in private_inactive.categories.all()
429+
430+
def test_does_not_duplicate_existing_official_category(self):
431+
official = self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
432+
gateway, stage = self._gateway(is_official=True)
433+
mcp_server = self._server(gateway, stage)
434+
mcp_server.categories.add(official)
435+
436+
refresh_official_mcp_server_category()
437+
438+
assert list(mcp_server.categories.all()) == [official]
439+
440+
def test_keeps_official_category_on_non_official_gateway(self):
441+
official = self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
442+
gateway, stage = self._gateway(is_official=False)
443+
mcp_server = self._server(gateway, stage)
444+
mcp_server.categories.add(official)
445+
446+
refresh_official_mcp_server_category()
447+
448+
assert official in mcp_server.categories.all()
449+
450+
def test_does_not_add_official_category_to_non_official_gateway(self):
451+
self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
452+
gateway, stage = self._gateway(is_official=False)
453+
mcp_server = self._server(gateway, stage)
454+
455+
refresh_official_mcp_server_category()
456+
457+
assert not mcp_server.categories.filter(name=OFFICIAL_MCP_CATEGORY_NAME).exists()
458+
459+
def test_preserves_featured_and_business_categories(self):
460+
official = self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
461+
featured = self._category(FEATURED_MCP_CATEGORY_NAME, "精选推荐")
462+
devops = G(
463+
MCPServerCategory,
464+
name=f"DevOps-{uuid4().hex[:8]}",
465+
display_name="持续交付",
466+
is_active=True,
467+
)
468+
gateway, stage = self._gateway(is_official=True)
469+
mcp_server = self._server(gateway, stage)
470+
mcp_server.categories.set([featured, devops])
471+
472+
refresh_official_mcp_server_category()
473+
474+
assert self._category_names(mcp_server) == {
475+
official.name,
476+
featured.name,
477+
devops.name,
478+
}
479+
480+
def test_skips_when_official_category_missing(self):
481+
MCPServerCategory.objects.filter(name=OFFICIAL_MCP_CATEGORY_NAME).delete()
482+
gateway, stage = self._gateway(is_official=True)
483+
mcp_server = self._server(gateway, stage)
484+
485+
refresh_official_mcp_server_category()
486+
487+
assert not mcp_server.categories.exists()
488+
489+
def test_skips_when_official_category_inactive(self):
490+
official = self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源", is_active=False)
491+
gateway, stage = self._gateway(is_official=True)
492+
mcp_server = self._server(gateway, stage)
493+
494+
refresh_official_mcp_server_category()
495+
496+
assert official not in mcp_server.categories.all()
497+
498+
def test_does_not_update_mcp_server_business_fields(self):
499+
self._category(OFFICIAL_MCP_CATEGORY_NAME, "官方资源")
500+
gateway, stage = self._gateway(is_official=True)
501+
mcp_server = self._server(
502+
gateway,
503+
stage,
504+
title="keep-title",
505+
description="keep-desc",
506+
is_public=False,
507+
)
508+
original_updated_time = mcp_server.updated_time
509+
510+
refresh_official_mcp_server_category()
511+
mcp_server.refresh_from_db()
512+
513+
assert mcp_server.title == "keep-title"
514+
assert mcp_server.description == "keep-desc"
515+
assert mcp_server.is_public is False
516+
assert mcp_server.updated_time == original_updated_time
517+
518+
def test_hourly_schedule_is_registered(self):
519+
entry = CELERY_BEAT_SCHEDULE["apigateway.apps.mcp_server.tasks.refresh_official_mcp_server_category"]
520+
assert entry["task"] == "apigateway.apps.mcp_server.tasks.refresh_official_mcp_server_category"
521+
assert entry["schedule"] == crontab(minute=0)

0 commit comments

Comments
 (0)