mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
Compare commits
7 Commits
763c1a885c
...
v4.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e3abec72 | ||
|
|
fc2efdf994 | ||
|
|
6ed672d996 | ||
|
|
2bf593fa6b | ||
|
|
3182214663 | ||
|
|
20614b20b7 | ||
|
|
da323817f7 |
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langbot"
|
||||
version = "4.5.3"
|
||||
version = "4.5.4"
|
||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
@@ -63,7 +63,7 @@ dependencies = [
|
||||
"langchain-text-splitters>=0.0.1",
|
||||
"chromadb>=0.4.24",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"langbot-plugin==0.1.11",
|
||||
"langbot-plugin==0.1.12",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"tboxsdk>=0.0.10",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
||||
|
||||
__version__ = '4.5.3'
|
||||
__version__ = '4.5.4'
|
||||
|
||||
@@ -55,17 +55,6 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
|
||||
return self.success(data=exec(py_code, {'ap': ap}))
|
||||
|
||||
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _() -> str:
|
||||
if not constants.debug_mode:
|
||||
return self.http_status(403, 403, 'Forbidden')
|
||||
|
||||
data = await quart.request.json
|
||||
|
||||
return self.success(
|
||||
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
|
||||
)
|
||||
|
||||
@self.route(
|
||||
'/debug/plugin/action',
|
||||
methods=['POST'],
|
||||
|
||||
@@ -96,7 +96,7 @@ class ResponseWrapper(stage.PipelineStage):
|
||||
if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用
|
||||
function_names = [tc.function.name for tc in result.tool_calls]
|
||||
|
||||
reply_text = f'调用函数 {".".join(function_names)}...'
|
||||
reply_text = f'Call {".".join(function_names)}...'
|
||||
|
||||
query.resp_message_chain.append(
|
||||
platform_message.MessageChain([platform_message.Plain(text=reply_text)])
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import sys
|
||||
import httpx
|
||||
from async_lru import alru_cache
|
||||
from langbot_plugin.api.entities.builtin.pipeline.query import provider_session
|
||||
|
||||
from ..core import app
|
||||
from . import handler
|
||||
@@ -321,13 +322,20 @@ class PluginRuntimeConnector:
|
||||
return tools
|
||||
|
||||
async def call_tool(
|
||||
self, tool_name: str, parameters: dict[str, Any], bound_plugins: list[str] | None = None
|
||||
self,
|
||||
tool_name: str,
|
||||
parameters: dict[str, Any],
|
||||
session: provider_session.Session,
|
||||
query_id: int,
|
||||
bound_plugins: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if not self.is_enable_plugin:
|
||||
return {'error': 'Tool not found: plugin system is disabled'}
|
||||
|
||||
# Pass include_plugins to runtime for validation
|
||||
return await self.handler.call_tool(tool_name, parameters, include_plugins=bound_plugins)
|
||||
return await self.handler.call_tool(
|
||||
tool_name, parameters, session.model_dump(serialize_as_any=True), query_id, include_plugins=bound_plugins
|
||||
)
|
||||
|
||||
async def list_commands(self, bound_plugins: list[str] | None = None) -> list[ComponentManifest]:
|
||||
if not self.is_enable_plugin:
|
||||
|
||||
@@ -620,7 +620,12 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
)
|
||||
|
||||
async def call_tool(
|
||||
self, tool_name: str, parameters: dict[str, Any], include_plugins: list[str] | None = None
|
||||
self,
|
||||
tool_name: str,
|
||||
parameters: dict[str, Any],
|
||||
session: dict[str, Any],
|
||||
query_id: int,
|
||||
include_plugins: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Call tool"""
|
||||
result = await self.call_action(
|
||||
@@ -628,6 +633,8 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
{
|
||||
'tool_name': tool_name,
|
||||
'tool_parameters': parameters,
|
||||
'session': session,
|
||||
'query_id': query_id,
|
||||
'include_plugins': include_plugins,
|
||||
},
|
||||
timeout=60,
|
||||
|
||||
@@ -196,7 +196,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
|
||||
parameters = json.loads(func.arguments)
|
||||
|
||||
func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters)
|
||||
func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters, query=query)
|
||||
if is_stream:
|
||||
msg = provider_message.MessageChunk(
|
||||
role='tool',
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from langbot_plugin.api.entities.events import pipeline_query
|
||||
|
||||
from ...core import app
|
||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||
|
||||
@@ -45,7 +47,7 @@ class ToolLoader(abc.ABC):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
|
||||
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
|
||||
"""执行工具调用"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import enum
|
||||
import typing
|
||||
from contextlib import AsyncExitStack
|
||||
import traceback
|
||||
from langbot_plugin.api.entities.events import pipeline_query
|
||||
import sqlalchemy
|
||||
import asyncio
|
||||
|
||||
@@ -329,7 +330,7 @@ class MCPLoader(loader.ToolLoader):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
|
||||
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
|
||||
"""执行工具调用"""
|
||||
for session in self.sessions.values():
|
||||
for function in session.get_tools():
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import typing
|
||||
import traceback
|
||||
|
||||
from langbot_plugin.api.entities.events import pipeline_query
|
||||
|
||||
from .. import loader
|
||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||
|
||||
@@ -43,9 +45,11 @@ class PluginToolLoader(loader.ToolLoader):
|
||||
return tool
|
||||
return None
|
||||
|
||||
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
|
||||
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
|
||||
try:
|
||||
return await self.ap.plugin_connector.call_tool(name, parameters)
|
||||
return await self.ap.plugin_connector.call_tool(
|
||||
name, parameters, session=query.session, query_id=query.query_id
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}')
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -7,6 +7,7 @@ from langbot.pkg.utils import importutil
|
||||
from langbot.pkg.provider.tools import loaders
|
||||
from langbot.pkg.provider.tools.loaders import mcp as mcp_loader, plugin as plugin_loader
|
||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||
from langbot_plugin.api.entities.events import pipeline_query
|
||||
|
||||
importutil.import_modules_in_pkg(loaders)
|
||||
|
||||
@@ -91,13 +92,13 @@ class ToolManager:
|
||||
|
||||
return tools
|
||||
|
||||
async def execute_func_call(self, name: str, parameters: dict) -> typing.Any:
|
||||
async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
|
||||
"""执行函数调用"""
|
||||
|
||||
if await self.plugin_tool_loader.has_tool(name):
|
||||
return await self.plugin_tool_loader.invoke_tool(name, parameters)
|
||||
return await self.plugin_tool_loader.invoke_tool(name, parameters, query)
|
||||
elif await self.mcp_tool_loader.has_tool(name):
|
||||
return await self.mcp_tool_loader.invoke_tool(name, parameters)
|
||||
return await self.mcp_tool_loader.invoke_tool(name, parameters, query)
|
||||
else:
|
||||
raise ValueError(f'未找到工具: {name}')
|
||||
|
||||
|
||||
@@ -234,7 +234,19 @@ export default function PipelineExtension({
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<PluginComponentList
|
||||
components={plugin.components}
|
||||
components={(() => {
|
||||
const componentKindCount: Record<string, number> =
|
||||
{};
|
||||
for (const component of plugin.components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
return componentKindCount;
|
||||
})()}
|
||||
showComponentName={true}
|
||||
showTitle={false}
|
||||
useBadge={true}
|
||||
@@ -401,7 +413,19 @@ export default function PipelineExtension({
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<PluginComponentList
|
||||
components={plugin.components}
|
||||
components={(() => {
|
||||
const componentKindCount: Record<string, number> =
|
||||
{};
|
||||
for (const component of plugin.components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
return componentKindCount;
|
||||
})()}
|
||||
showComponentName={true}
|
||||
showTitle={false}
|
||||
useBadge={true}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PluginComponent } from '@/app/infra/entities/plugin';
|
||||
import { TFunction } from 'i18next';
|
||||
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -9,31 +8,22 @@ export default function PluginComponentList({
|
||||
showTitle,
|
||||
useBadge,
|
||||
t,
|
||||
responsive = false,
|
||||
}: {
|
||||
components: PluginComponent[];
|
||||
components: Record<string, number>;
|
||||
showComponentName: boolean;
|
||||
showTitle: boolean;
|
||||
useBadge: boolean;
|
||||
t: TFunction;
|
||||
responsive?: boolean;
|
||||
}) {
|
||||
const componentKindCount: Record<string, number> = {};
|
||||
|
||||
for (const component of components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const kindIconMap: Record<string, React.ReactNode> = {
|
||||
Tool: <Wrench className="w-5 h-5" />,
|
||||
EventListener: <AudioWaveform className="w-5 h-5" />,
|
||||
Command: <Hash className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const componentKindList = Object.keys(componentKindCount);
|
||||
const componentKindList = Object.keys(components || {});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -44,11 +34,21 @@ export default function PluginComponentList({
|
||||
return (
|
||||
<>
|
||||
{useBadge && (
|
||||
<Badge variant="outline">
|
||||
<Badge
|
||||
key={kind}
|
||||
variant="outline"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{kindIconMap[kind]}
|
||||
{showComponentName &&
|
||||
t('plugins.componentName.' + kind) + ' '}
|
||||
{componentKindCount[kind]}
|
||||
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||
{responsive ? (
|
||||
<span className="hidden md:inline">
|
||||
{t('plugins.componentName.' + kind)}
|
||||
</span>
|
||||
) : (
|
||||
showComponentName && t('plugins.componentName.' + kind)
|
||||
)}
|
||||
<span className="ml-1">{components[kind]}</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -58,9 +58,15 @@ export default function PluginComponentList({
|
||||
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
||||
>
|
||||
{kindIconMap[kind]}
|
||||
{showComponentName &&
|
||||
t('plugins.componentName.' + kind) + ' '}
|
||||
{componentKindCount[kind]}
|
||||
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||
{responsive ? (
|
||||
<span className="hidden md:inline">
|
||||
{t('plugins.componentName.' + kind)}
|
||||
</span>
|
||||
) : (
|
||||
showComponentName && t('plugins.componentName.' + kind)
|
||||
)}
|
||||
<span className="ml-1">{components[kind]}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -128,7 +128,18 @@ export default function PluginCardComponent({
|
||||
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
<PluginComponentList
|
||||
components={cardVO.components}
|
||||
components={(() => {
|
||||
const componentKindCount: Record<string, number> = {};
|
||||
for (const component of cardVO.components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
return componentKindCount;
|
||||
})()}
|
||||
showComponentName={false}
|
||||
showTitle={true}
|
||||
useBadge={false}
|
||||
|
||||
@@ -160,7 +160,18 @@ export default function PluginForm({
|
||||
|
||||
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<PluginComponentList
|
||||
components={pluginInfo.components}
|
||||
components={(() => {
|
||||
const componentKindCount: Record<string, number> = {};
|
||||
for (const component of pluginInfo.components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
return componentKindCount;
|
||||
})()}
|
||||
showComponentName={true}
|
||||
showTitle={false}
|
||||
useBadge={true}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Search, Loader2 } from 'lucide-react';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { Search, Loader2, Wrench, AudioWaveform, Hash } from 'lucide-react';
|
||||
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
|
||||
@@ -38,6 +39,7 @@ function MarketPageContent({
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [componentFilter, setComponentFilter] = useState<string>('all');
|
||||
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
@@ -111,6 +113,7 @@ function MarketPageContent({
|
||||
),
|
||||
githubURL: plugin.repository,
|
||||
version: plugin.latest_version,
|
||||
components: plugin.components,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -124,25 +127,20 @@ function MarketPageContent({
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
const { sortBy, sortOrder } = getCurrentSort();
|
||||
const filterValue =
|
||||
componentFilter === 'all' ? undefined : componentFilter;
|
||||
|
||||
if (isSearch && searchQuery.trim()) {
|
||||
response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||
searchQuery.trim(),
|
||||
// Always use searchMarketplacePlugins to support component filtering
|
||||
const response =
|
||||
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
filterValue,
|
||||
);
|
||||
} else {
|
||||
response = await getCloudServiceClientSync().getMarketplacePlugins(
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
);
|
||||
}
|
||||
|
||||
const data: ApiRespMarketplacePlugins = response;
|
||||
const newPlugins = data.plugins.map(transformToVO);
|
||||
@@ -167,7 +165,14 @@ function MarketPageContent({
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort],
|
||||
[
|
||||
searchQuery,
|
||||
componentFilter,
|
||||
pageSize,
|
||||
transformToVO,
|
||||
plugins.length,
|
||||
getCurrentSort,
|
||||
],
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
@@ -212,10 +217,18 @@ function MarketPageContent({
|
||||
// fetchPlugins will be called by useEffect when sortOption changes
|
||||
}, []);
|
||||
|
||||
// 当排序选项变化时重新加载数据
|
||||
// 组件筛选变化处理
|
||||
const handleComponentFilterChange = useCallback((value: string) => {
|
||||
setComponentFilter(value);
|
||||
setCurrentPage(1);
|
||||
setPlugins([]);
|
||||
// fetchPlugins will be called by useEffect when componentFilter changes
|
||||
}, []);
|
||||
|
||||
// 当排序选项或组件筛选变化时重新加载数据
|
||||
useEffect(() => {
|
||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||
}, [sortOption]);
|
||||
}, [sortOption, componentFilter]);
|
||||
|
||||
// 处理URL参数,检查是否需要打开插件详情对话框
|
||||
useEffect(() => {
|
||||
@@ -342,9 +355,59 @@ function MarketPageContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort dropdown */}
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
||||
{/* Component filter and sort */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-3 sm:px-4">
|
||||
{/* Component filter */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-2">
|
||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||
{t('market.filterByComponent')}:
|
||||
</span>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
spacing={2}
|
||||
size="sm"
|
||||
value={componentFilter}
|
||||
onValueChange={(value) => {
|
||||
if (value) handleComponentFilterChange(value);
|
||||
}}
|
||||
className="justify-start"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="all"
|
||||
aria-label="All components"
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{t('market.allComponents')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="Tool"
|
||||
aria-label="Tool"
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Wrench className="h-4 w-4 mr-1" />
|
||||
{t('plugins.componentName.Tool')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="Command"
|
||||
aria-label="Command"
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Hash className="h-4 w-4 mr-1" />
|
||||
{t('plugins.componentName.Command')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="EventListener"
|
||||
aria-label="EventListener"
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<AudioWaveform className="h-4 w-4 mr-1" />
|
||||
{t('plugins.componentName.EventListener')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Sort dropdown */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||
{t('market.sortBy')}:
|
||||
</span>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toast } from 'sonner';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||
|
||||
interface PluginDetailDialogProps {
|
||||
open: boolean;
|
||||
@@ -104,6 +105,15 @@ export default function PluginDetailDialog({
|
||||
<Download className="w-4 h-4" />
|
||||
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
|
||||
</Badge>
|
||||
{plugin!.components && Object.keys(plugin!.components).length > 0 && (
|
||||
<PluginComponentList
|
||||
components={plugin!.components}
|
||||
showComponentName={true}
|
||||
showTitle={false}
|
||||
useBadge={true}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{plugin!.repository && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PluginMarketCardVO } from './PluginMarketCardVO';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
|
||||
|
||||
export default function PluginMarketCardComponent({
|
||||
cardVO,
|
||||
@@ -7,18 +10,32 @@ export default function PluginMarketCardComponent({
|
||||
cardVO: PluginMarketCardVO;
|
||||
onPluginClick?: (author: string, pluginName: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function handleCardClick() {
|
||||
if (onPluginClick) {
|
||||
onPluginClick(cardVO.author, cardVO.pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
const kindIconMap: Record<string, React.ReactNode> = {
|
||||
Tool: <Wrench className="w-4 h-4" />,
|
||||
EventListener: <AudioWaveform className="w-4 h-4" />,
|
||||
Command: <Hash className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
const componentKindNameMap: Record<string, string> = {
|
||||
Tool: t('plugins.componentName.Tool'),
|
||||
EventListener: t('plugins.componentName.EventListener'),
|
||||
Command: t('plugins.componentName.Command'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[100%] h-auto min-h-[8rem] sm:h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] cursor-pointer hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22]"
|
||||
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] cursor-pointer hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22]"
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<div className="w-full h-full flex flex-col justify-between gap-2">
|
||||
<div className="w-full h-full flex flex-col justify-between gap-3">
|
||||
{/* 上部分:插件信息 */}
|
||||
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0">
|
||||
<img
|
||||
@@ -60,23 +77,45 @@ export default function PluginMarketCardComponent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下部分:下载量 */}
|
||||
<div className="w-full flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
|
||||
{cardVO.installCount.toLocaleString()}
|
||||
{/* 下部分:下载量和组件列表 */}
|
||||
<div className="w-full flex flex-row items-center justify-between gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
|
||||
<div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
|
||||
{cardVO.installCount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 组件列表 */}
|
||||
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{Object.entries(cardVO.components).map(([kind, count]) => (
|
||||
<Badge
|
||||
key={kind}
|
||||
variant="outline"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{kindIconMap[kind]}
|
||||
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||
<span className="hidden md:inline">
|
||||
{componentKindNameMap[kind]}
|
||||
</span>
|
||||
<span className="ml-1">{count}</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface IPluginMarketCardVO {
|
||||
iconURL: string;
|
||||
githubURL: string;
|
||||
version: string;
|
||||
components?: Record<string, number>;
|
||||
}
|
||||
|
||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
@@ -20,6 +21,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
githubURL: string;
|
||||
installCount: number;
|
||||
version: string;
|
||||
components?: Record<string, number>;
|
||||
|
||||
constructor(prop: IPluginMarketCardVO) {
|
||||
this.description = prop.description;
|
||||
@@ -31,5 +33,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
this.installCount = prop.installCount;
|
||||
this.pluginId = prop.pluginId;
|
||||
this.version = prop.version;
|
||||
this.components = prop.components;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface PluginV4 {
|
||||
tags: string[];
|
||||
install_count: number;
|
||||
latest_version: string;
|
||||
components: Record<string, number>;
|
||||
status: PluginV4Status;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
page_size: number,
|
||||
sort_by?: string,
|
||||
sort_order?: string,
|
||||
component_filter?: string,
|
||||
): Promise<ApiRespMarketplacePlugins> {
|
||||
return this.post<ApiRespMarketplacePlugins>(
|
||||
'/api/v1/marketplace/plugins/search',
|
||||
@@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
page_size,
|
||||
sort_by,
|
||||
sort_order,
|
||||
component_filter,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,32 +8,40 @@ import { cn } from '@/lib/utils';
|
||||
import { toggleVariants } from '@/components/ui/toggle';
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number;
|
||||
}
|
||||
>({
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
spacing: 0,
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number;
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={{ '--gap': spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
|
||||
'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
@@ -55,12 +63,14 @@ function ToggleGroupItem({
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
|
||||
'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10',
|
||||
'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:data-[state=on]:bg-slate-700 dark:data-[state=on]:text-white [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -351,6 +351,8 @@ const enUS = {
|
||||
markAsRead: 'Mark as Read',
|
||||
markAsReadSuccess: 'Marked as read',
|
||||
markAsReadFailed: 'Mark as read failed',
|
||||
filterByComponent: 'Component',
|
||||
allComponents: 'All Components',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -353,6 +353,8 @@ const jaJP = {
|
||||
markAsRead: '既読',
|
||||
markAsReadSuccess: '既読に設定しました',
|
||||
markAsReadFailed: '既読に設定に失敗しました',
|
||||
filterByComponent: 'コンポーネント',
|
||||
allComponents: '全部コンポーネント',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -335,6 +335,8 @@ const zhHans = {
|
||||
markAsRead: '已读',
|
||||
markAsReadSuccess: '已标记为已读',
|
||||
markAsReadFailed: '标记为已读失败',
|
||||
filterByComponent: '组件',
|
||||
allComponents: '全部组件',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -333,6 +333,8 @@ const zhHant = {
|
||||
markAsRead: '已讀',
|
||||
markAsReadSuccess: '已標記為已讀',
|
||||
markAsReadFailed: '標記為已讀失敗',
|
||||
filterByComponent: '組件',
|
||||
allComponents: '全部組件',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
Reference in New Issue
Block a user