mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 06:47:13 +00:00
feat(runner): filter marketplace recommendations by explicit usage
This commit is contained in:
+1
-1
@@ -232,4 +232,4 @@ line-ending = "auto"
|
||||
|
||||
[tool.uv.sources]
|
||||
# Development contract: update to the matching SDK release before publishing.
|
||||
langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" }
|
||||
langbot-plugin = { git = "https://github.com/langbot-app/langbot-plugin-sdk", rev = "c67e6c85a0cde8ae2b20cbd89e33805a68382563" }
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ metadata:
|
||||
en_US: Deterministic runner fixture that returns stable QA sentinel output.
|
||||
zh_Hans: 返回稳定 QA 哨兵输出的确定性 runner 夹具。
|
||||
spec:
|
||||
usages: [agent]
|
||||
capabilities:
|
||||
streaming: true
|
||||
tool_calling: false
|
||||
|
||||
@@ -55,7 +55,7 @@ class RunnerDescriptor(pydantic.BaseModel):
|
||||
"""Original manifest for reference"""
|
||||
|
||||
component_kind: typing.Literal['Runner'] = 'Runner'
|
||||
usages: list[typing.Literal['agent', 'event']] = pydantic.Field(default_factory=lambda: ['agent'])
|
||||
usages: list[typing.Literal['agent', 'event']] = pydantic.Field(min_length=1)
|
||||
supported_event_patterns: list[str] = pydantic.Field(default_factory=lambda: ['*'])
|
||||
|
||||
model_config = pydantic.ConfigDict(
|
||||
|
||||
@@ -32,6 +32,7 @@ def make_descriptor(
|
||||
permissions: dict | None = None,
|
||||
) -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id='plugin:test/runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -91,6 +91,7 @@ class TestContextValidation:
|
||||
def _make_descriptor(self):
|
||||
"""Create a mock runner descriptor."""
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id='plugin:test/plugin/runner',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -125,6 +125,7 @@ class MockApplication:
|
||||
class FakeRunnerRegistry:
|
||||
async def get(self, context, runner_id, bound_plugins=None):
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -23,6 +23,7 @@ SOURCE = {'source': 'native', 'source_id': None}
|
||||
|
||||
async def _resources(tool_mgr, rag_mgr, binding):
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=LOCAL_RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
|
||||
@@ -164,6 +164,7 @@ class FakeConversation:
|
||||
|
||||
def make_descriptor() -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
|
||||
@@ -55,6 +55,7 @@ class FakeApplication:
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
'name': 'default',
|
||||
'label': {'en_US': 'Local Agent'},
|
||||
'usages': ['agent'],
|
||||
'capabilities': {'streaming': True},
|
||||
'permissions': {},
|
||||
'config_schema': [],
|
||||
@@ -68,6 +69,7 @@ class FakeApplication:
|
||||
'id': 'plugin:alice/my-agent/custom',
|
||||
'name': 'custom',
|
||||
'label': {'en_US': 'Custom Agent'},
|
||||
'usages': ['agent'],
|
||||
'capabilities': {},
|
||||
'permissions': {},
|
||||
'config_schema': [{'name': 'param1', 'type': 'string'}],
|
||||
@@ -308,6 +310,7 @@ class TestDescriptorValidation:
|
||||
def test_validate_runner_descriptor(self):
|
||||
"""Validate correctly built descriptor."""
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id='plugin:test/my-runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -323,6 +326,7 @@ class TestDescriptorValidation:
|
||||
def test_descriptor_capabilities(self):
|
||||
"""Descriptor capability helper methods."""
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id='plugin:test/my-runner/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
@@ -365,3 +369,18 @@ async def test_registry_filters_usages_without_splitting_component_identity():
|
||||
assert [item.runner_name for item in processors] == ['events', 'both']
|
||||
assert agents[-1].id == processors[-1].id
|
||||
assert (await registry.get(TEST_CONTEXT, processors[0].id)).usages == ['event']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovery_rejects_runner_without_usage_declaration():
|
||||
ap = FakeApplication()
|
||||
original = ap.plugin_connector.list_runners
|
||||
|
||||
async def missing_usage(bound_plugins=None):
|
||||
runners = await original(bound_plugins)
|
||||
runners[0]['manifest'].pop('usages')
|
||||
return runners
|
||||
|
||||
ap.plugin_connector.list_runners = missing_usage
|
||||
runners = await RunnerRegistry(ap).list_runners(TEST_CONTEXT, use_cache=False)
|
||||
assert [runner.id for runner in runners] == ['plugin:alice/my-agent/custom']
|
||||
|
||||
@@ -39,6 +39,7 @@ def make_descriptor(
|
||||
permissions: dict | None = None,
|
||||
) -> RunnerDescriptor:
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -37,6 +37,7 @@ class FakeApplication:
|
||||
def make_descriptor():
|
||||
"""Create a test descriptor."""
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id='plugin:langbot-team/LocalAgent/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent', 'zh_Hans': '内置 Agent'},
|
||||
|
||||
@@ -25,6 +25,7 @@ from langbot.pkg.agent.runner.state_scope import (
|
||||
def make_descriptor(runner_id: str = 'plugin:test/my-runner/default') -> RunnerDescriptor:
|
||||
"""Create a test descriptor."""
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
|
||||
@@ -26,6 +26,7 @@ class FakeRegistry:
|
||||
def make_runner(runner_id: str, config_schema: list[dict]):
|
||||
parts = runner_id.removeprefix('plugin:').split('/')
|
||||
return RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': runner_id},
|
||||
|
||||
@@ -32,6 +32,7 @@ RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
def attach_runner_descriptor(app):
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
|
||||
@@ -17,6 +17,7 @@ def _attach_runner_descriptor(app):
|
||||
from langbot.pkg.agent.runner.descriptor import RunnerDescriptor
|
||||
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
|
||||
@@ -80,6 +80,7 @@ def _make_app(*, skill_service) -> SimpleNamespace:
|
||||
model = SimpleNamespace(model_entity=SimpleNamespace(uuid='model-1', abilities={'func_call'}))
|
||||
tool_mgr = SimpleNamespace(get_resolved_tool_catalog=AsyncMock(return_value=[]))
|
||||
descriptor = RunnerDescriptor(
|
||||
usages=['agent'],
|
||||
id=_RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
|
||||
@@ -2119,7 +2119,7 @@ requires-dist = [
|
||||
{ name = "ebooklib", specifier = ">=0.18" },
|
||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" },
|
||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=c67e6c85a0cde8ae2b20cbd89e33805a68382563" },
|
||||
{ name = "langchain", specifier = ">=1.3.9" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
||||
@@ -2186,7 +2186,7 @@ dev = [
|
||||
[[package]]
|
||||
name = "langbot-plugin"
|
||||
version = "0.5.5"
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=92a9e03fa9c791f4ed30cc3f5f0602c13b800d28#92a9e03fa9c791f4ed30cc3f5f0602c13b800d28" }
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk?rev=c67e6c85a0cde8ae2b20cbd89e33805a68382563#c67e6c85a0cde8ae2b20cbd89e33805a68382563" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function PluginProcessorSettings({
|
||||
<div className="p-2 text-sm text-muted-foreground">
|
||||
{t('agents.eventProcessor.noComponents')}
|
||||
<Button asChild variant="link" className="h-auto px-0">
|
||||
<Link to="/home/plugins">
|
||||
<Link to="/home/add-extension?type=plugin&component=Runner&runner_usage=event">
|
||||
{t('agents.eventProcessor.installPlugin')}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -227,7 +227,7 @@ export default function RunnerSelect({
|
||||
setCatalogLoading(true);
|
||||
setCatalogError(false);
|
||||
try {
|
||||
const catalog = await loadRunnerCatalog();
|
||||
const catalog = await loadRunnerCatalog('agent');
|
||||
setMarketplaceRunners(catalog.marketplaceRunners);
|
||||
setInstalledPluginIds(catalog.installedPluginIds);
|
||||
setInstalledPluginDescriptions(catalog.installedPluginDescriptions);
|
||||
@@ -392,7 +392,7 @@ export default function RunnerSelect({
|
||||
{t('agents.marketplaceRunners')}
|
||||
</span>
|
||||
<a
|
||||
href="https://space.langbot.app/market?type=plugin&component=Runner"
|
||||
href="https://space.langbot.app/market?type=plugin&component=Runner&runner_usage=agent"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-medium text-foreground hover:bg-accent"
|
||||
|
||||
@@ -3,7 +3,11 @@ import { getCloudServiceClient } from '@/app/infra/http';
|
||||
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
|
||||
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
||||
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||
import type { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import {
|
||||
supportsRunnerUsage,
|
||||
type PluginV4,
|
||||
type RunnerUsage,
|
||||
} from '@/app/infra/entities/plugin';
|
||||
import type { I18nObject } from '@/app/infra/entities/common';
|
||||
|
||||
export const RUNNER_COMPONENT_FILTER = 'Runner';
|
||||
@@ -140,7 +144,9 @@ export function subscribePendingRunnerInstall(
|
||||
window.removeEventListener(RUNNER_INSTALL_INTENT_EVENT, handleChange);
|
||||
}
|
||||
|
||||
export async function loadRunnerCatalog(): Promise<RunnerCatalog> {
|
||||
export async function loadRunnerCatalog(
|
||||
usage: RunnerUsage,
|
||||
): Promise<RunnerCatalog> {
|
||||
const cloudClient = await getCloudServiceClient();
|
||||
const [firstSearchResult, recommendationResult, installedResult] =
|
||||
await Promise.all([
|
||||
@@ -150,6 +156,7 @@ export async function loadRunnerCatalog(): Promise<RunnerCatalog> {
|
||||
page_size: RUNNER_CATALOG_PAGE_SIZE,
|
||||
type_filter: 'plugin',
|
||||
component_filter: RUNNER_COMPONENT_FILTER,
|
||||
runner_usage: usage,
|
||||
}),
|
||||
cloudClient.getRecommendationLists().catch(() => ({ lists: [] })),
|
||||
httpClient.getPlugins().catch(() => ({ plugins: [] })),
|
||||
@@ -167,6 +174,7 @@ export async function loadRunnerCatalog(): Promise<RunnerCatalog> {
|
||||
page_size: RUNNER_CATALOG_PAGE_SIZE,
|
||||
type_filter: 'plugin',
|
||||
component_filter: RUNNER_COMPONENT_FILTER,
|
||||
runner_usage: usage,
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -189,7 +197,7 @@ export async function loadRunnerCatalog(): Promise<RunnerCatalog> {
|
||||
}
|
||||
|
||||
const marketplaceRunners = catalogPlugins
|
||||
.filter((plugin) => plugin.components?.[RUNNER_COMPONENT_FILTER])
|
||||
.filter((plugin) => supportsRunnerUsage(plugin, usage))
|
||||
.sort((left, right) => {
|
||||
const leftOrder = recommendationOrder.get(marketplacePluginId(left));
|
||||
const rightOrder = recommendationOrder.get(marketplacePluginId(right));
|
||||
|
||||
@@ -126,6 +126,14 @@ function MarketPageContent({
|
||||
loadMarketFilters().componentFilter ??
|
||||
'all',
|
||||
);
|
||||
const [runnerUsage, setRunnerUsage] = useState(() => {
|
||||
const value = searchParams.get('runner_usage');
|
||||
return value === 'agent' || value === 'event' ? value : 'all';
|
||||
});
|
||||
const activeRunnerUsage =
|
||||
componentFilter === 'Runner' && runnerUsage !== 'all'
|
||||
? (runnerUsage as 'agent' | 'event')
|
||||
: undefined;
|
||||
const [typeFilter, setTypeFilter] = useState<string>(() => {
|
||||
if (getComponentFilterFromQuery(searchParams)) {
|
||||
return 'plugin';
|
||||
@@ -138,7 +146,9 @@ function MarketPageContent({
|
||||
return saved && MARKET_TYPE_VALUES.includes(saved) ? saved : 'all';
|
||||
});
|
||||
const activeAdvancedFilters =
|
||||
(typeFilter === 'all' ? 0 : 1) + (componentFilter === 'all' ? 0 : 1);
|
||||
(typeFilter === 'all' ? 0 : 1) +
|
||||
(componentFilter === 'all' ? 0 : 1) +
|
||||
(activeRunnerUsage ? 1 : 0);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>(
|
||||
() => loadMarketFilters().selectedTags ?? [],
|
||||
);
|
||||
@@ -313,6 +323,7 @@ function MarketPageContent({
|
||||
type_filter: typeFilter === 'all' ? undefined : typeFilter,
|
||||
component_filter:
|
||||
componentFilter === 'all' ? undefined : componentFilter,
|
||||
runner_usage: activeRunnerUsage,
|
||||
tags_filter: selectedTags.length > 0 ? selectedTags : undefined,
|
||||
});
|
||||
|
||||
@@ -344,6 +355,7 @@ function MarketPageContent({
|
||||
[
|
||||
searchQuery,
|
||||
componentFilter,
|
||||
activeRunnerUsage,
|
||||
selectedTags,
|
||||
pageSize,
|
||||
transformToVO,
|
||||
@@ -502,6 +514,10 @@ function MarketPageContent({
|
||||
setPlugins([]);
|
||||
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (value !== 'Runner') {
|
||||
setRunnerUsage('all');
|
||||
params.delete('runner_usage');
|
||||
}
|
||||
if (value === 'all') {
|
||||
params.delete('component');
|
||||
} else {
|
||||
@@ -517,7 +533,7 @@ function MarketPageContent({
|
||||
// 当排序选项或组件筛选或类型筛选变化时重新加载数据
|
||||
useEffect(() => {
|
||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||
}, [sortOption, componentFilter, typeFilter]);
|
||||
}, [sortOption, componentFilter, typeFilter, activeRunnerUsage]);
|
||||
|
||||
// Tags 筛选变化时重新搜索
|
||||
useEffect(() => {
|
||||
@@ -826,6 +842,38 @@ function MarketPageContent({
|
||||
})}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{componentFilter === 'Runner' && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
{t('market.runnerUsage')}
|
||||
</div>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
value={runnerUsage}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
setRunnerUsage(value);
|
||||
setCurrentPage(1);
|
||||
setPlugins([]);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (value === 'all') params.delete('runner_usage');
|
||||
else params.set('runner_usage', value);
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value="all">
|
||||
{t('market.runnerUsageAll')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="agent">
|
||||
{t('market.runnerUsageAgent')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="event">
|
||||
{t('market.runnerUsageEvent')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
@@ -52,8 +52,23 @@ export interface PluginV4 {
|
||||
hot_score?: number;
|
||||
latest_version: string;
|
||||
components: Record<string, number>;
|
||||
runner_usages?: RunnerUsage[];
|
||||
status: PluginV4Status;
|
||||
type?: 'plugin' | 'mcp' | 'skill';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type RunnerUsage = 'agent' | 'event';
|
||||
|
||||
/** Unknown usage metadata must not become an install recommendation. */
|
||||
export function supportsRunnerUsage(
|
||||
plugin: PluginV4,
|
||||
usage: RunnerUsage,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
plugin.components?.Runner &&
|
||||
Array.isArray(plugin.runner_usages) &&
|
||||
plugin.runner_usages.includes(usage),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import {
|
||||
ApiRespMarketplacePluginDetail,
|
||||
ApiRespMarketplacePlugins,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import {
|
||||
PluginV4,
|
||||
RunnerUsage,
|
||||
supportsRunnerUsage,
|
||||
} from '@/app/infra/entities/plugin';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
|
||||
/**
|
||||
@@ -39,6 +43,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
component_filter?: string,
|
||||
tags_filter?: string[],
|
||||
type_filter?: string,
|
||||
runner_usage?: RunnerUsage,
|
||||
): Promise<ApiRespMarketplacePlugins> {
|
||||
// Use different endpoints based on type_filter
|
||||
if (type_filter === 'mcp') {
|
||||
@@ -90,6 +95,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
sort_by,
|
||||
sort_order,
|
||||
component_filter,
|
||||
runner_usage,
|
||||
tags_filter,
|
||||
type_filter,
|
||||
},
|
||||
@@ -104,6 +110,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
sort_order?: string;
|
||||
type_filter?: string;
|
||||
component_filter?: string;
|
||||
runner_usage?: RunnerUsage;
|
||||
tags_filter?: string[];
|
||||
}): Promise<ApiRespMarketplacePlugins> {
|
||||
return this.post<{ extensions: PluginV4[]; total: number }>(
|
||||
@@ -125,7 +132,15 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
total: resp?.total || 0,
|
||||
};
|
||||
})
|
||||
.catch(() => this.searchMarketplaceExtensionsLegacy(data));
|
||||
.catch(() => this.searchMarketplaceExtensionsLegacy(data))
|
||||
.then((result) => ({
|
||||
...result,
|
||||
plugins: data.runner_usage
|
||||
? result.plugins.filter((plugin) =>
|
||||
supportsRunnerUsage(plugin, data.runner_usage!),
|
||||
)
|
||||
: result.plugins,
|
||||
}));
|
||||
}
|
||||
|
||||
public getMarketplaceLikedExtensions(
|
||||
@@ -162,6 +177,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
sort_order?: string;
|
||||
type_filter?: string;
|
||||
component_filter?: string;
|
||||
runner_usage?: RunnerUsage;
|
||||
tags_filter?: string[];
|
||||
}): Promise<ApiRespMarketplacePlugins> {
|
||||
const query = data.query || '';
|
||||
@@ -183,6 +199,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
data.component_filter,
|
||||
data.tags_filter,
|
||||
data.component_filter ? 'plugin' : data.type_filter,
|
||||
data.runner_usage,
|
||||
).catch((error) => {
|
||||
if (data.type_filter === 'mcp' || data.type_filter === 'skill') {
|
||||
return { plugins: [], total: 0 };
|
||||
|
||||
@@ -225,7 +225,7 @@ export default function WizardPage() {
|
||||
setIsRunnerCatalogLoading(true);
|
||||
setRunnerCatalogError(false);
|
||||
try {
|
||||
const catalog = await fetchRunnerCatalog();
|
||||
const catalog = await fetchRunnerCatalog('agent');
|
||||
setMarketplaceRunners(catalog.marketplaceRunners);
|
||||
setInstalledPluginIds(catalog.installedPluginIds);
|
||||
} catch (error) {
|
||||
@@ -2087,7 +2087,7 @@ function StepAIEngine({
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to="/home/extensions?type=plugin&component=Runner">
|
||||
<Link to="/home/extensions?type=plugin&component=Runner&runner_usage=agent">
|
||||
{t('wizard.aiEngine.browseRunners')}
|
||||
<ExternalLink className="size-4" />
|
||||
</Link>
|
||||
|
||||
@@ -1229,6 +1229,11 @@ const enUS = {
|
||||
},
|
||||
},
|
||||
market: {
|
||||
runnerUsage: 'Runner usage',
|
||||
runnerUsageAll: 'All',
|
||||
runnerUsageAgent: 'Agent / Pipeline',
|
||||
runnerUsageEvent: 'Plugin processor',
|
||||
|
||||
searchPlaceholder: 'Search plugins...',
|
||||
searchPlaceholderCount:
|
||||
'Search {{count}} extensions, capabilities, or use cases...',
|
||||
@@ -2543,9 +2548,9 @@ const enUS = {
|
||||
catalogUnavailable: 'Runner catalog is unavailable',
|
||||
catalogUnavailableDescription:
|
||||
'Installed runners are still available. Retry the catalog or browse Extensions.',
|
||||
noMarketplaceRunners: 'No Runner extensions are published yet',
|
||||
noMarketplaceRunners: 'No Runner plugins match this usage',
|
||||
noMarketplaceRunnersDescription:
|
||||
'Retry after runner extensions are published to the configured Marketplace.',
|
||||
'Use an installed Runner or try again later.',
|
||||
browseRunners: 'Browse Runner Extensions',
|
||||
installAndContinue: 'Install & Continue',
|
||||
installing: 'Installing...',
|
||||
|
||||
@@ -1149,6 +1149,11 @@ const jaJP = {
|
||||
uploadPluginOnly: '.lbpkg プラグインパッケージのみ対応しています',
|
||||
},
|
||||
market: {
|
||||
runnerUsage: 'ランナーの用途',
|
||||
runnerUsageAll: 'すべて',
|
||||
runnerUsageAgent: 'Agent / パイプライン',
|
||||
runnerUsageEvent: 'プラグインプロセッサー',
|
||||
|
||||
searchPlaceholder: 'プラグインを検索...',
|
||||
searchPlaceholderCount:
|
||||
'{{count}} 個の拡張機能・機能・ユースケースを検索...',
|
||||
@@ -2323,9 +2328,9 @@ const jaJP = {
|
||||
catalogUnavailable: 'Runner カタログを読み込めません',
|
||||
catalogUnavailableDescription:
|
||||
'インストール済みの Runner は引き続き使用できます。再試行するか、拡張機能を確認してください。',
|
||||
noMarketplaceRunners: 'Runner 拡張機能はまだ公開されていません',
|
||||
noMarketplaceRunners: 'この用途に対応するランナープラグインはありません',
|
||||
noMarketplaceRunnersDescription:
|
||||
'設定済みのマーケットプレイスに Runner 拡張機能が公開された後、再試行してください。',
|
||||
'インストール済みのランナーを使うか、後でもう一度お試しください。',
|
||||
browseRunners: 'Runner 拡張機能を見る',
|
||||
installAndContinue: 'インストールして続行',
|
||||
installing: 'インストール中...',
|
||||
|
||||
@@ -1166,6 +1166,11 @@ const zhHans = {
|
||||
},
|
||||
},
|
||||
market: {
|
||||
runnerUsage: '运行器用途',
|
||||
runnerUsageAll: '全部',
|
||||
runnerUsageAgent: 'Agent / 流水线',
|
||||
runnerUsageEvent: '插件处理器',
|
||||
|
||||
searchPlaceholder: '搜索插件...',
|
||||
searchPlaceholderCount: '搜索 {{count}} 个扩展、能力或场景...',
|
||||
searchResults: '搜索到 {{count}} 个扩展',
|
||||
@@ -2404,9 +2409,8 @@ const zhHans = {
|
||||
catalogUnavailable: '无法加载运行器目录',
|
||||
catalogUnavailableDescription:
|
||||
'已安装的运行器仍可使用。你可以重试,或前往扩展页面查看。',
|
||||
noMarketplaceRunners: '市场暂未发布运行器扩展',
|
||||
noMarketplaceRunnersDescription:
|
||||
'请在运行器扩展发布到当前配置的市场后重试。',
|
||||
noMarketplaceRunners: '暂无适用于此用途的运行器插件',
|
||||
noMarketplaceRunnersDescription: '可以使用已安装的运行器,或稍后重试。',
|
||||
browseRunners: '浏览运行器扩展',
|
||||
installAndContinue: '安装并继续',
|
||||
installing: '正在安装...',
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
function load(path, imports = {}) {
|
||||
const exports = {};
|
||||
const source = fs.readFileSync(new URL(path, import.meta.url), 'utf8');
|
||||
const js = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
}).outputText;
|
||||
new Function('exports', 'require', js)(exports, (name) => {
|
||||
if (!(name in imports)) throw new Error(`Unexpected import: ${name}`);
|
||||
return imports[name];
|
||||
});
|
||||
return exports;
|
||||
}
|
||||
const entities = load('../../src/app/infra/entities/plugin/index.ts');
|
||||
const plugin = (name, runner_usages) => ({
|
||||
name,
|
||||
author: 'test',
|
||||
components: { Runner: 1 },
|
||||
runner_usages,
|
||||
install_count: 0,
|
||||
latest_version: '1',
|
||||
});
|
||||
const plugins = [
|
||||
plugin('agent', ['agent']),
|
||||
plugin('event', ['event']),
|
||||
plugin('both', ['agent', 'event']),
|
||||
plugin('unknown', undefined),
|
||||
];
|
||||
test('recommendations require explicit usage and Runner component', () => {
|
||||
assert.deepEqual(
|
||||
plugins
|
||||
.filter((p) => entities.supportsRunnerUsage(p, 'agent'))
|
||||
.map((p) => p.name),
|
||||
['agent', 'both'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
plugins
|
||||
.filter((p) => entities.supportsRunnerUsage(p, 'event'))
|
||||
.map((p) => p.name),
|
||||
['event', 'both'],
|
||||
);
|
||||
assert.equal(
|
||||
entities.supportsRunnerUsage({ ...plugins[0], components: {} }, 'agent'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
test('catalog filters every page and recommendations cannot reintroduce incompatible plugins', async () => {
|
||||
const requests = [];
|
||||
const catalog = load('../../src/app/home/agents/runner-marketplace.ts', {
|
||||
'@/app/infra/entities/plugin': entities,
|
||||
'@/app/infra/http/HttpClient': {
|
||||
httpClient: { getPlugins: async () => ({ plugins: [] }) },
|
||||
},
|
||||
'@/app/infra/http': {
|
||||
getCloudServiceClient: async () => ({
|
||||
searchMarketplaceExtensions: async (request) => {
|
||||
requests.push(request);
|
||||
return {
|
||||
total: 101,
|
||||
plugins:
|
||||
request.page === 1 ? plugins.slice(0, 2) : plugins.slice(2),
|
||||
};
|
||||
},
|
||||
getRecommendationLists: async () => ({
|
||||
lists: [{ plugins: [plugins[1], plugins[2]] }],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'@/app/infra/http/workspaceContext': {},
|
||||
});
|
||||
const result = await catalog.loadRunnerCatalog('agent');
|
||||
assert.deepEqual(
|
||||
result.marketplaceRunners.map((p) => p.name),
|
||||
['both', 'agent'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
requests.map((r) => r.runner_usage),
|
||||
['agent', 'agent'],
|
||||
);
|
||||
});
|
||||
test('legacy API fallback preserves usage and rejects missing usage metadata', async () => {
|
||||
const requests = [];
|
||||
class BaseHttpClient {
|
||||
async post(url, data) {
|
||||
requests.push({ url, data });
|
||||
if (url.endsWith('/extensions/search')) throw new Error('old endpoint');
|
||||
return { plugins, total: plugins.length };
|
||||
}
|
||||
}
|
||||
const { CloudServiceClient } = load(
|
||||
'../../src/app/infra/http/CloudServiceClient.ts',
|
||||
{
|
||||
'./BaseHttpClient': { BaseHttpClient },
|
||||
'@/app/infra/entities/plugin': entities,
|
||||
},
|
||||
);
|
||||
const result = await new CloudServiceClient().searchMarketplaceExtensions({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
type_filter: 'plugin',
|
||||
component_filter: 'Runner',
|
||||
runner_usage: 'event',
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.plugins.map((p) => p.name),
|
||||
['event', 'both'],
|
||||
);
|
||||
assert.equal(requests[1].data.runner_usage, 'event');
|
||||
});
|
||||
Reference in New Issue
Block a user