From 2c22e3d6c7865af59a7dcc499a8d1e80a46d2bfc Mon Sep 17 00:00:00 2001 From: Pine Date: Sat, 5 Sep 2026 10:50:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=B8=82=E5=9C=BA=E4=B8=93?= =?UTF-8?q?=E5=8C=BA=EF=BC=9A/plugins/market/search=20=E9=80=8F=E4=BC=A0?= =?UTF-8?q?=20region=20=E8=BF=87=E6=BB=A4=EF=BC=8C=E6=9D=A1=E7=9B=AE?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=20tags=20=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console/src/api/modules/pluginMarket.ts | 5 ++++ console/src/pages/AppCenter/AppMarket.tsx | 28 +++++++++++++------ console/src/pages/AppCenter/index.module.less | 16 +++++++++++ console/src/pages/AppCenter/index.tsx | 26 +++++++++++++++-- src/pineagents/app/routers/plugins.py | 8 +++++- 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/console/src/api/modules/pluginMarket.ts b/console/src/api/modules/pluginMarket.ts index 4ceaf27..bef6910 100644 --- a/console/src/api/modules/pluginMarket.ts +++ b/console/src/api/modules/pluginMarket.ts @@ -25,6 +25,8 @@ export interface MarketPluginEntry { is_featured?: boolean; /** Whether this plugin is currently trending (upstream). */ is_trending?: boolean; + /** 专区标记(云超服源透传 server-core tags;含 "yunnan-opc" 表示在云南专区展示)。 */ + tags?: string[]; /** ── 云超服统一市场(server-core)扩展字段 ── */ /** server-core 商品 id(购买/校验/下载安装包用)。 */ market_item_id?: string; @@ -64,6 +66,8 @@ export interface FetchMarketPluginsParams { page_size: number; /** 市场源:"pineagents" 走云超服统一市场(server-core 实时数据);缺省/其他代理到 AgentScope Platform。 */ source?: string; + /** 专区过滤(如 "yunnan-opc"):仅返回带该标记的上架应用。 */ + region?: string; } export async function fetchMarketPlugins( @@ -80,6 +84,7 @@ export async function fetchMarketPlugins( if (params.category) url.searchParams.set("category", params.category); if (params.sort_by) url.searchParams.set("sort_by", params.sort_by); if (params.source) url.searchParams.set("source", params.source); + if (params.region) url.searchParams.set("region", params.region); const response = await fetch(url.toString(), { headers: buildAuthHeaders(), diff --git a/console/src/pages/AppCenter/AppMarket.tsx b/console/src/pages/AppCenter/AppMarket.tsx index c26c12d..d37f81f 100644 --- a/console/src/pages/AppCenter/AppMarket.tsx +++ b/console/src/pages/AppCenter/AppMarket.tsx @@ -24,6 +24,7 @@ import { ExternalLink, Search, Sparkles, + MapPin, ShoppingCart, CheckCircle2, RefreshCw, @@ -76,7 +77,7 @@ function pickDescription(entry: MarketPluginEntry, language: string): string { interface AppMarketProps { onInstalled: () => void | Promise; - channel?: "official" | "community" | "pineagents"; + channel?: "official" | "community" | "pineagents" | "yunnan"; } export function AppMarket({ @@ -142,7 +143,8 @@ export function AppMarket({ page_size: MARKET_PAGE_SIZE, search: keyword || undefined, category: APP_CATEGORY, - source: channel === "pineagents" ? "pineagents" : undefined, + source: channel === "pineagents" || channel === "yunnan" ? "pineagents" : undefined, + region: channel === "yunnan" ? "yunnan-opc" : undefined, }, { signal }, ); @@ -156,7 +158,7 @@ export function AppMarket({ if (signal.aborted) return; // 云超服源返回 server-core 已上架全部商品,不做 is_featured 过滤。 setPlugins( - channel === "pineagents" + channel === "pineagents" || channel === "yunnan" ? entries : entries.filter((entry) => channel === "official" @@ -436,13 +438,13 @@ export function AppMarket({ [], ); - // 云超服源:进入时刷新一次已购状态(登录后) + // 云超服源(含专区):进入时刷新一次已购状态(登录后) useEffect(() => { - if (channel !== "pineagents") return; + if (channel !== "pineagents" && channel !== "yunnan") return; for (const entry of plugins) void refreshOwned(entry); }, [channel, plugins.length, refreshOwned]); // eslint-disable-line react-hooks/exhaustive-deps - const isCloudMarket = channel === "pineagents"; + const isCloudMarket = channel === "pineagents" || channel === "yunnan"; const actionFor = (entry: MarketPluginEntry) => { const installed = installedMap[entry.id]; @@ -508,9 +510,11 @@ export function AppMarket({ } description={ - isOfficial - ? t("appCenter.officialAppsEmpty", "No official apps found") - : t("appCenter.marketEmpty", "No apps found") + channel === "yunnan" + ? t("appCenter.yunnanZoneEmpty", "云南特色产业 OPC 转型专区建设中,特色应用将陆续上架") + : isOfficial + ? t("appCenter.officialAppsEmpty", "No official apps found") + : t("appCenter.marketEmpty", "No apps found") } className={styles.stateBlock} /> @@ -552,6 +556,12 @@ export function AppMarket({ {t("appCenter.featured", "精选")} )} + {(entry.tags ?? []).includes("yunnan-opc") && ( + + + {t("appCenter.yunnanZoneTag", "云南特色产业OPC")} + + )} const { Option } = Select; /** URL-persisted App Center views; unknown values fall back to installed. */ -type AppCenterView = "installed" | "official" | "market" | "pineagents"; +type AppCenterView = "installed" | "official" | "market" | "pineagents" | "yunnan"; export default function AppCenterPage() { const { t } = useTranslation(); @@ -83,7 +84,9 @@ export default function AppCenterPage() { : "installed" : viewParam === "pineagents" ? "pineagents" - : "installed"; + : viewParam === "yunnan" + ? "yunnan" + : "installed"; const switchView = (next: AppCenterView) => { const params = new URLSearchParams(searchParams); @@ -442,6 +445,15 @@ export default function AppCenterPage() { ), }, + { + key: "yunnan", + label: ( + + + {t("appCenter.yunnanZone", "云南特色产业OPC转型专区")} + + ), + }, ]; const installedContent = ( @@ -618,6 +630,16 @@ export default function AppCenterPage() { > + ) : view === "yunnan" ? ( + + + + } + > + + ) : ( installedContent )} diff --git a/src/pineagents/app/routers/plugins.py b/src/pineagents/app/routers/plugins.py index 760dd20..b7d5c86 100644 --- a/src/pineagents/app/routers/plugins.py +++ b/src/pineagents/app/routers/plugins.py @@ -1112,6 +1112,8 @@ def _pineagents_live_plugin(item: dict) -> dict: }, }, "is_featured": bool(item.get("is_featured")), + # 专区标记:tags 原样透传(前端以 tags 含 "yunnan-opc" 等判断专区) + "tags": item.get("tags") or [], # 定价 / 购买(云超服市场扩展字段) "market_item_id": item.get("id"), "price_fen": int(item.get("price_fen") or 0), @@ -1126,6 +1128,7 @@ async def _fetch_pineagents_live_market( page_number: int, page_size: int, auth_header: str | None, + region: str | None = None, ) -> dict: """从 server-core 统一市场实时拉取已上架应用(app),映射为前端结构。""" params: dict[str, object] = { @@ -1137,6 +1140,8 @@ async def _fetch_pineagents_live_market( params["q"] = search if category and category != "app": params["category"] = category + if region: + params["region"] = region headers = {"Authorization": auth_header} if auth_header else {} async with httpx.AsyncClient( base_url=DEMO_API_BASE_URL, @@ -1168,13 +1173,14 @@ async def search_market_plugins( category: Optional[str] = None, sort_by: Optional[str] = None, source: Optional[str] = None, + region: Optional[str] = None, ): """搜索插件。`source=pineagents` 时走云超服统一市场(server-core);否则代理到 AgentScope Platform。""" if source == "pineagents": auth_header = request.headers.get("Authorization", "") try: return await _fetch_pineagents_live_market( - search, category, page_number, page_size, auth_header, + search, category, page_number, page_size, auth_header, region, ) except Exception as exc: # noqa: BLE001 # 不再回退任何演示数据:云超服市场不可达时如实报错。