mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 22:36:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e3abec72 | ||
|
|
fc2efdf994 | ||
|
|
6ed672d996 | ||
|
|
2bf593fa6b | ||
|
|
3182214663 | ||
|
|
20614b20b7 | ||
|
|
da323817f7 | ||
|
|
763c1a885c | ||
|
|
dbc09f46f4 | ||
|
|
cf43f09aff | ||
|
|
c3c51b0fbf | ||
|
|
8a42daa63f |
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.5.3"
|
version = "4.5.4"
|
||||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -63,7 +63,7 @@ dependencies = [
|
|||||||
"langchain-text-splitters>=0.0.1",
|
"langchain-text-splitters>=0.0.1",
|
||||||
"chromadb>=0.4.24",
|
"chromadb>=0.4.24",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"langbot-plugin==0.1.11",
|
"langbot-plugin==0.1.12",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"tboxsdk>=0.0.10",
|
"tboxsdk>=0.0.10",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
||||||
|
|
||||||
__version__ = '4.5.3'
|
__version__ = '4.5.4'
|
||||||
|
|||||||
@@ -109,14 +109,13 @@ class WecomClient:
|
|||||||
async def send_image(self, user_id: str, agent_id: int, media_id: str):
|
async def send_image(self, user_id: str, agent_id: int, media_id: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
url = self.base_url + '/media/upload?access_token=' + self.access_token
|
|
||||||
|
url = self.base_url + '/message/send?access_token=' + self.access_token
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
params = {
|
params = {
|
||||||
'touser': user_id,
|
'touser': user_id,
|
||||||
'toparty': '',
|
|
||||||
'totag': '',
|
|
||||||
'agentid': agent_id,
|
|
||||||
'msgtype': 'image',
|
'msgtype': 'image',
|
||||||
|
'agentid': agent_id,
|
||||||
'image': {
|
'image': {
|
||||||
'media_id': media_id,
|
'media_id': media_id,
|
||||||
},
|
},
|
||||||
@@ -125,19 +124,13 @@ class WecomClient:
|
|||||||
'enable_duplicate_check': 0,
|
'enable_duplicate_check': 0,
|
||||||
'duplicate_check_interval': 1800,
|
'duplicate_check_interval': 1800,
|
||||||
}
|
}
|
||||||
try:
|
response = await client.post(url, json=params)
|
||||||
response = await client.post(url, json=params)
|
data = response.json()
|
||||||
data = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'发送图片失败:{data}')
|
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
|
||||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_image(user_id, agent_id, media_id)
|
return await self.send_image(user_id, agent_id, media_id)
|
||||||
|
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f'发送图片失败:{data}')
|
||||||
raise Exception('Failed to send image: ' + str(data))
|
raise Exception('Failed to send image: ' + str(data))
|
||||||
|
|
||||||
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
||||||
|
|||||||
@@ -55,17 +55,6 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
return self.success(data=exec(py_code, {'ap': ap}))
|
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(
|
@self.route(
|
||||||
'/debug/plugin/action',
|
'/debug/plugin/action',
|
||||||
methods=['POST'],
|
methods=['POST'],
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class ResponseWrapper(stage.PipelineStage):
|
|||||||
if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用
|
if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用
|
||||||
function_names = [tc.function.name for tc in result.tool_calls]
|
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(
|
query.resp_message_chain.append(
|
||||||
platform_message.MessageChain([platform_message.Plain(text=reply_text)])
|
platform_message.MessageChain([platform_message.Plain(text=reply_text)])
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import httpx
|
import httpx
|
||||||
from async_lru import alru_cache
|
from async_lru import alru_cache
|
||||||
|
from langbot_plugin.api.entities.builtin.pipeline.query import provider_session
|
||||||
|
|
||||||
from ..core import app
|
from ..core import app
|
||||||
from . import handler
|
from . import handler
|
||||||
@@ -321,13 +322,20 @@ class PluginRuntimeConnector:
|
|||||||
return tools
|
return tools
|
||||||
|
|
||||||
async def call_tool(
|
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]:
|
) -> dict[str, Any]:
|
||||||
if not self.is_enable_plugin:
|
if not self.is_enable_plugin:
|
||||||
return {'error': 'Tool not found: plugin system is disabled'}
|
return {'error': 'Tool not found: plugin system is disabled'}
|
||||||
|
|
||||||
# Pass include_plugins to runtime for validation
|
# 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]:
|
async def list_commands(self, bound_plugins: list[str] | None = None) -> list[ComponentManifest]:
|
||||||
if not self.is_enable_plugin:
|
if not self.is_enable_plugin:
|
||||||
|
|||||||
@@ -620,7 +620,12 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def call_tool(
|
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]:
|
) -> dict[str, Any]:
|
||||||
"""Call tool"""
|
"""Call tool"""
|
||||||
result = await self.call_action(
|
result = await self.call_action(
|
||||||
@@ -628,6 +633,8 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
{
|
{
|
||||||
'tool_name': tool_name,
|
'tool_name': tool_name,
|
||||||
'tool_parameters': parameters,
|
'tool_parameters': parameters,
|
||||||
|
'session': session,
|
||||||
|
'query_id': query_id,
|
||||||
'include_plugins': include_plugins,
|
'include_plugins': include_plugins,
|
||||||
},
|
},
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
|||||||
|
|
||||||
parameters = json.loads(func.arguments)
|
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:
|
if is_stream:
|
||||||
msg = provider_message.MessageChunk(
|
msg = provider_message.MessageChunk(
|
||||||
role='tool',
|
role='tool',
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import abc
|
import abc
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from langbot_plugin.api.entities.events import pipeline_query
|
||||||
|
|
||||||
from ...core import app
|
from ...core import app
|
||||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ class ToolLoader(abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@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
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import enum
|
|||||||
import typing
|
import typing
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
import traceback
|
import traceback
|
||||||
|
from langbot_plugin.api.entities.events import pipeline_query
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
@@ -329,7 +330,7 @@ class MCPLoader(loader.ToolLoader):
|
|||||||
return True
|
return True
|
||||||
return False
|
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 session in self.sessions.values():
|
||||||
for function in session.get_tools():
|
for function in session.get_tools():
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import typing
|
import typing
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from langbot_plugin.api.entities.events import pipeline_query
|
||||||
|
|
||||||
from .. import loader
|
from .. import loader
|
||||||
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
|
||||||
|
|
||||||
@@ -43,9 +45,11 @@ class PluginToolLoader(loader.ToolLoader):
|
|||||||
return tool
|
return tool
|
||||||
return None
|
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:
|
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:
|
except Exception as e:
|
||||||
self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}')
|
self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}')
|
||||||
traceback.print_exc()
|
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 import loaders
|
||||||
from langbot.pkg.provider.tools.loaders import mcp as mcp_loader, plugin as plugin_loader
|
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
|
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)
|
importutil.import_modules_in_pkg(loaders)
|
||||||
|
|
||||||
@@ -91,13 +92,13 @@ class ToolManager:
|
|||||||
|
|
||||||
return tools
|
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):
|
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):
|
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:
|
else:
|
||||||
raise ValueError(f'未找到工具: {name}')
|
raise ValueError(f'未找到工具: {name}')
|
||||||
|
|
||||||
|
|||||||
@@ -289,12 +289,16 @@ export default function ApiIntegrationDialog({
|
|||||||
{t('common.noApiKeys')}
|
{t('common.noApiKeys')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('common.name')}</TableHead>
|
<TableHead className="min-w-[120px]">
|
||||||
<TableHead>{t('common.apiKeyValue')}</TableHead>
|
{t('common.name')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="min-w-[200px]">
|
||||||
|
{t('common.apiKeyValue')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-[100px]">
|
<TableHead className="w-[100px]">
|
||||||
{t('common.actions')}
|
{t('common.actions')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -372,16 +376,20 @@ export default function ApiIntegrationDialog({
|
|||||||
{t('common.noWebhooks')}
|
{t('common.noWebhooks')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md overflow-x-auto max-w-full">
|
||||||
<Table>
|
<Table className="table-fixed w-full">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>{t('common.name')}</TableHead>
|
<TableHead className="w-[150px]">
|
||||||
<TableHead>{t('common.webhookUrl')}</TableHead>
|
{t('common.name')}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-[380px]">
|
||||||
|
{t('common.webhookUrl')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-[80px]">
|
<TableHead className="w-[80px]">
|
||||||
{t('common.webhookEnabled')}
|
{t('common.webhookEnabled')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[100px]">
|
<TableHead className="w-[80px]">
|
||||||
{t('common.actions')}
|
{t('common.actions')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -389,20 +397,30 @@ export default function ApiIntegrationDialog({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{webhooks.map((webhook) => (
|
{webhooks.map((webhook) => (
|
||||||
<TableRow key={webhook.id}>
|
<TableRow key={webhook.id}>
|
||||||
<TableCell>
|
<TableCell className="truncate">
|
||||||
<div>
|
<div className="truncate">
|
||||||
<div className="font-medium">{webhook.name}</div>
|
<div
|
||||||
|
className="font-medium truncate"
|
||||||
|
title={webhook.name}
|
||||||
|
>
|
||||||
|
{webhook.name}
|
||||||
|
</div>
|
||||||
{webhook.description && (
|
{webhook.description && (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div
|
||||||
|
className="text-sm text-muted-foreground truncate"
|
||||||
|
title={webhook.description}
|
||||||
|
>
|
||||||
{webhook.description}
|
{webhook.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<code className="text-sm bg-muted px-2 py-1 rounded break-all">
|
<div className="overflow-x-auto max-w-[380px]">
|
||||||
{webhook.url}
|
<code className="text-sm bg-muted px-2 py-1 rounded whitespace-nowrap inline-block">
|
||||||
</code>
|
{webhook.url}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export default function DynamicFormItemComponent({
|
|||||||
model.requester,
|
model.requester,
|
||||||
)}
|
)}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-[8%]"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-medium">{model.name}</h4>
|
<h4 className="font-medium">{model.name}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export default function KBForm({
|
|||||||
model.requester,
|
model.requester,
|
||||||
)}
|
)}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-[8%]"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-medium">
|
<h4 className="font-medium">
|
||||||
{model.name}
|
{model.name}
|
||||||
|
|||||||
@@ -146,6 +146,26 @@ export default function PipelineExtension({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleAllPlugins = () => {
|
||||||
|
if (tempSelectedPluginIds.length === allPlugins.length) {
|
||||||
|
// Deselect all
|
||||||
|
setTempSelectedPluginIds([]);
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
setTempSelectedPluginIds(allPlugins.map((p) => getPluginId(p)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAllMCPServers = () => {
|
||||||
|
if (tempSelectedMCPIds.length === allMCPServers.length) {
|
||||||
|
// Deselect all
|
||||||
|
setTempSelectedMCPIds([]);
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
setTempSelectedMCPIds(allMCPServers.map((s) => s.uuid || ''));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmPluginSelection = async () => {
|
const handleConfirmPluginSelection = async () => {
|
||||||
const newSelected = allPlugins.filter((p) =>
|
const newSelected = allPlugins.filter((p) =>
|
||||||
tempSelectedPluginIds.includes(getPluginId(p)),
|
tempSelectedPluginIds.includes(getPluginId(p)),
|
||||||
@@ -214,7 +234,19 @@ export default function PipelineExtension({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<PluginComponentList
|
<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}
|
showComponentName={true}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
useBadge={true}
|
useBadge={true}
|
||||||
@@ -330,6 +362,23 @@ export default function PipelineExtension({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
<DialogTitle>{t('pipelines.extensions.selectPlugins')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{allPlugins.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 px-1 py-2 border-b cursor-pointer"
|
||||||
|
onClick={handleToggleAllPlugins}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
tempSelectedPluginIds.length === allPlugins.length &&
|
||||||
|
allPlugins.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleToggleAllPlugins}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t('pipelines.extensions.selectAll')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
||||||
{allPlugins.length === 0 ? (
|
{allPlugins.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
@@ -364,7 +413,19 @@ export default function PipelineExtension({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<PluginComponentList
|
<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}
|
showComponentName={true}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
useBadge={true}
|
useBadge={true}
|
||||||
@@ -404,6 +465,23 @@ export default function PipelineExtension({
|
|||||||
{t('pipelines.extensions.selectMCPServers')}
|
{t('pipelines.extensions.selectMCPServers')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{allMCPServers.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 px-1 py-2 border-b cursor-pointer"
|
||||||
|
onClick={handleToggleAllMCPServers}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
tempSelectedMCPIds.length === allMCPServers.length &&
|
||||||
|
allMCPServers.length > 0
|
||||||
|
}
|
||||||
|
onCheckedChange={handleToggleAllMCPServers}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t('pipelines.extensions.selectAll')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
<div className="flex-1 overflow-y-auto space-y-2 pr-2">
|
||||||
{allMCPServers.length === 0 ? (
|
{allMCPServers.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PluginComponent } from '@/app/infra/entities/plugin';
|
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
|
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -9,31 +8,22 @@ export default function PluginComponentList({
|
|||||||
showTitle,
|
showTitle,
|
||||||
useBadge,
|
useBadge,
|
||||||
t,
|
t,
|
||||||
|
responsive = false,
|
||||||
}: {
|
}: {
|
||||||
components: PluginComponent[];
|
components: Record<string, number>;
|
||||||
showComponentName: boolean;
|
showComponentName: boolean;
|
||||||
showTitle: boolean;
|
showTitle: boolean;
|
||||||
useBadge: boolean;
|
useBadge: boolean;
|
||||||
t: TFunction;
|
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> = {
|
const kindIconMap: Record<string, React.ReactNode> = {
|
||||||
Tool: <Wrench className="w-5 h-5" />,
|
Tool: <Wrench className="w-5 h-5" />,
|
||||||
EventListener: <AudioWaveform className="w-5 h-5" />,
|
EventListener: <AudioWaveform className="w-5 h-5" />,
|
||||||
Command: <Hash className="w-5 h-5" />,
|
Command: <Hash className="w-5 h-5" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const componentKindList = Object.keys(componentKindCount);
|
const componentKindList = Object.keys(components || {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -44,11 +34,21 @@ export default function PluginComponentList({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{useBadge && (
|
{useBadge && (
|
||||||
<Badge variant="outline">
|
<Badge
|
||||||
|
key={kind}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
{kindIconMap[kind]}
|
{kindIconMap[kind]}
|
||||||
{showComponentName &&
|
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||||
t('plugins.componentName.' + kind) + ' '}
|
{responsive ? (
|
||||||
{componentKindCount[kind]}
|
<span className="hidden md:inline">
|
||||||
|
{t('plugins.componentName.' + kind)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
showComponentName && t('plugins.componentName.' + kind)
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{components[kind]}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -58,9 +58,15 @@ export default function PluginComponentList({
|
|||||||
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
||||||
>
|
>
|
||||||
{kindIconMap[kind]}
|
{kindIconMap[kind]}
|
||||||
{showComponentName &&
|
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
|
||||||
t('plugins.componentName.' + kind) + ' '}
|
{responsive ? (
|
||||||
{componentKindCount[kind]}
|
<span className="hidden md:inline">
|
||||||
|
{t('plugins.componentName.' + kind)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
showComponentName && t('plugins.componentName.' + kind)
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{components[kind]}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -128,7 +128,18 @@ export default function PluginCardComponent({
|
|||||||
|
|
||||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||||
<PluginComponentList
|
<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}
|
showComponentName={false}
|
||||||
showTitle={true}
|
showTitle={true}
|
||||||
useBadge={false}
|
useBadge={false}
|
||||||
|
|||||||
@@ -160,7 +160,18 @@ export default function PluginForm({
|
|||||||
|
|
||||||
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
|
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
|
||||||
<PluginComponentList
|
<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}
|
showComponentName={true}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
useBadge={true}
|
useBadge={true}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} 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 PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||||
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
|
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
|
||||||
@@ -38,6 +39,7 @@ function MarketPageContent({
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [componentFilter, setComponentFilter] = useState<string>('all');
|
||||||
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
@@ -111,6 +113,7 @@ function MarketPageContent({
|
|||||||
),
|
),
|
||||||
githubURL: plugin.repository,
|
githubURL: plugin.repository,
|
||||||
version: plugin.latest_version,
|
version: plugin.latest_version,
|
||||||
|
components: plugin.components,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -124,25 +127,20 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
|
||||||
const { sortBy, sortOrder } = getCurrentSort();
|
const { sortBy, sortOrder } = getCurrentSort();
|
||||||
|
const filterValue =
|
||||||
|
componentFilter === 'all' ? undefined : componentFilter;
|
||||||
|
|
||||||
if (isSearch && searchQuery.trim()) {
|
// Always use searchMarketplacePlugins to support component filtering
|
||||||
response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
const response =
|
||||||
searchQuery.trim(),
|
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
|
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
filterValue,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
response = await getCloudServiceClientSync().getMarketplacePlugins(
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
sortOrder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: ApiRespMarketplacePlugins = response;
|
const data: ApiRespMarketplacePlugins = response;
|
||||||
const newPlugins = data.plugins.map(transformToVO);
|
const newPlugins = data.plugins.map(transformToVO);
|
||||||
@@ -167,7 +165,14 @@ function MarketPageContent({
|
|||||||
setIsLoadingMore(false);
|
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
|
// 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(() => {
|
useEffect(() => {
|
||||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||||
}, [sortOption]);
|
}, [sortOption, componentFilter]);
|
||||||
|
|
||||||
// 处理URL参数,检查是否需要打开插件详情对话框
|
// 处理URL参数,检查是否需要打开插件详情对话框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -263,6 +276,18 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
}, [currentPage, isLoadingMore, hasMore, fetchPlugins, searchQuery]);
|
||||||
|
|
||||||
|
// Check if content fills the viewport and load more if needed
|
||||||
|
const checkAndLoadMore = useCallback(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer || isLoading || isLoadingMore || !hasMore) return;
|
||||||
|
|
||||||
|
const { scrollHeight, clientHeight } = scrollContainer;
|
||||||
|
// If content doesn't fill the viewport (no scrollbar), load more
|
||||||
|
if (scrollHeight <= clientHeight) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
}, [loadMore, isLoading, isLoadingMore, hasMore]);
|
||||||
|
|
||||||
// Listen to scroll events on the scroll container
|
// Listen to scroll events on the scroll container
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
@@ -280,6 +305,25 @@ function MarketPageContent({
|
|||||||
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||||
}, [loadMore]);
|
}, [loadMore]);
|
||||||
|
|
||||||
|
// Check if we need to load more after content changes or initial load
|
||||||
|
useEffect(() => {
|
||||||
|
// Small delay to ensure DOM has updated
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
checkAndLoadMore();
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [plugins, checkAndLoadMore]);
|
||||||
|
|
||||||
|
// Also check on window resize
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
checkAndLoadMore();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [checkAndLoadMore]);
|
||||||
|
|
||||||
// 安装插件
|
// 安装插件
|
||||||
// const handleInstallPlugin = (plugin: PluginV4) => {
|
// const handleInstallPlugin = (plugin: PluginV4) => {
|
||||||
// console.log('install plugin', plugin);
|
// console.log('install plugin', plugin);
|
||||||
@@ -311,9 +355,59 @@ function MarketPageContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort dropdown */}
|
{/* Component filter and sort */}
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-3 sm:px-4">
|
||||||
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
|
{/* 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">
|
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
||||||
{t('market.sortBy')}:
|
{t('market.sortBy')}:
|
||||||
</span>
|
</span>
|
||||||
@@ -360,7 +454,7 @@ function MarketPageContent({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-6 pt-4">
|
||||||
{plugins.map((plugin) => (
|
{plugins.map((plugin) => (
|
||||||
<PluginMarketCardComponent
|
<PluginMarketCardComponent
|
||||||
key={plugin.pluginId}
|
key={plugin.pluginId}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { toast } from 'sonner';
|
|||||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
|
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||||
|
|
||||||
interface PluginDetailDialogProps {
|
interface PluginDetailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -104,6 +105,15 @@ export default function PluginDetailDialog({
|
|||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
|
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{plugin!.components && Object.keys(plugin!.components).length > 0 && (
|
||||||
|
<PluginComponentList
|
||||||
|
components={plugin!.components}
|
||||||
|
showComponentName={true}
|
||||||
|
showTitle={false}
|
||||||
|
useBadge={true}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{plugin!.repository && (
|
{plugin!.repository && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { PluginMarketCardVO } from './PluginMarketCardVO';
|
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({
|
export default function PluginMarketCardComponent({
|
||||||
cardVO,
|
cardVO,
|
||||||
@@ -7,18 +10,32 @@ export default function PluginMarketCardComponent({
|
|||||||
cardVO: PluginMarketCardVO;
|
cardVO: PluginMarketCardVO;
|
||||||
onPluginClick?: (author: string, pluginName: string) => void;
|
onPluginClick?: (author: string, pluginName: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
function handleCardClick() {
|
function handleCardClick() {
|
||||||
if (onPluginClick) {
|
if (onPluginClick) {
|
||||||
onPluginClick(cardVO.author, cardVO.pluginName);
|
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 (
|
return (
|
||||||
<div
|
<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}
|
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">
|
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0">
|
||||||
<img
|
<img
|
||||||
@@ -60,23 +77,45 @@ export default function PluginMarketCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
||||||
<svg
|
<div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
|
||||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
|
||||||
viewBox="0 0 24 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
strokeWidth="2"
|
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" />
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
<line x1="12" y1="15" x2="12" y2="3" />
|
<polyline points="7,10 12,15 17,10" />
|
||||||
</svg>
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
|
</svg>
|
||||||
{cardVO.installCount.toLocaleString()}
|
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
|
||||||
|
{cardVO.installCount.toLocaleString()}
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface IPluginMarketCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
githubURL: string;
|
githubURL: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
components?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||||
@@ -20,6 +21,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
|||||||
githubURL: string;
|
githubURL: string;
|
||||||
installCount: number;
|
installCount: number;
|
||||||
version: string;
|
version: string;
|
||||||
|
components?: Record<string, number>;
|
||||||
|
|
||||||
constructor(prop: IPluginMarketCardVO) {
|
constructor(prop: IPluginMarketCardVO) {
|
||||||
this.description = prop.description;
|
this.description = prop.description;
|
||||||
@@ -31,5 +33,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
|||||||
this.installCount = prop.installCount;
|
this.installCount = prop.installCount;
|
||||||
this.pluginId = prop.pluginId;
|
this.pluginId = prop.pluginId;
|
||||||
this.version = prop.version;
|
this.version = prop.version;
|
||||||
|
this.components = prop.components;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface PluginV4 {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
install_count: number;
|
install_count: number;
|
||||||
latest_version: string;
|
latest_version: string;
|
||||||
|
components: Record<string, number>;
|
||||||
status: PluginV4Status;
|
status: PluginV4Status;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
page_size: number,
|
page_size: number,
|
||||||
sort_by?: string,
|
sort_by?: string,
|
||||||
sort_order?: string,
|
sort_order?: string,
|
||||||
|
component_filter?: string,
|
||||||
): Promise<ApiRespMarketplacePlugins> {
|
): Promise<ApiRespMarketplacePlugins> {
|
||||||
return this.post<ApiRespMarketplacePlugins>(
|
return this.post<ApiRespMarketplacePlugins>(
|
||||||
'/api/v1/marketplace/plugins/search',
|
'/api/v1/marketplace/plugins/search',
|
||||||
@@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
page_size,
|
page_size,
|
||||||
sort_by,
|
sort_by,
|
||||||
sort_order,
|
sort_order,
|
||||||
|
component_filter,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,40 @@ import { cn } from '@/lib/utils';
|
|||||||
import { toggleVariants } from '@/components/ui/toggle';
|
import { toggleVariants } from '@/components/ui/toggle';
|
||||||
|
|
||||||
const ToggleGroupContext = React.createContext<
|
const ToggleGroupContext = React.createContext<
|
||||||
VariantProps<typeof toggleVariants>
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}
|
||||||
>({
|
>({
|
||||||
size: 'default',
|
size: 'default',
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
|
spacing: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
function ToggleGroup({
|
function ToggleGroup({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
|
spacing = 0,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
VariantProps<typeof toggleVariants>) {
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Root
|
<ToggleGroupPrimitive.Root
|
||||||
data-slot="toggle-group"
|
data-slot="toggle-group"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
style={{ '--gap': spacing } as React.CSSProperties}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext.Provider>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
@@ -55,12 +63,14 @@ function ToggleGroupItem({
|
|||||||
data-slot="toggle-group-item"
|
data-slot="toggle-group-item"
|
||||||
data-variant={context.variant || variant}
|
data-variant={context.variant || variant}
|
||||||
data-size={context.size || size}
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
className={cn(
|
className={cn(
|
||||||
toggleVariants({
|
toggleVariants({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const toggleVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -351,6 +351,8 @@ const enUS = {
|
|||||||
markAsRead: 'Mark as Read',
|
markAsRead: 'Mark as Read',
|
||||||
markAsReadSuccess: 'Marked as read',
|
markAsReadSuccess: 'Marked as read',
|
||||||
markAsReadFailed: 'Mark as read failed',
|
markAsReadFailed: 'Mark as read failed',
|
||||||
|
filterByComponent: 'Component',
|
||||||
|
allComponents: 'All Components',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
@@ -484,6 +486,7 @@ const enUS = {
|
|||||||
toolCount: '{{count}} tools',
|
toolCount: '{{count}} tools',
|
||||||
noPluginsInstalled: 'No installed plugins',
|
noPluginsInstalled: 'No installed plugins',
|
||||||
noMCPServersConfigured: 'No configured MCP servers',
|
noMCPServersConfigured: 'No configured MCP servers',
|
||||||
|
selectAll: 'Select All',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'Pipeline Chat',
|
title: 'Pipeline Chat',
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ const jaJP = {
|
|||||||
markAsRead: '既読',
|
markAsRead: '既読',
|
||||||
markAsReadSuccess: '既読に設定しました',
|
markAsReadSuccess: '既読に設定しました',
|
||||||
markAsReadFailed: '既読に設定に失敗しました',
|
markAsReadFailed: '既読に設定に失敗しました',
|
||||||
|
filterByComponent: 'コンポーネント',
|
||||||
|
allComponents: '全部コンポーネント',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
@@ -487,6 +489,7 @@ const jaJP = {
|
|||||||
toolCount: '{{count}}個のツール',
|
toolCount: '{{count}}個のツール',
|
||||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||||
|
selectAll: 'すべて選択',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: 'パイプラインのチャット',
|
title: 'パイプラインのチャット',
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ const zhHans = {
|
|||||||
markAsRead: '已读',
|
markAsRead: '已读',
|
||||||
markAsReadSuccess: '已标记为已读',
|
markAsReadSuccess: '已标记为已读',
|
||||||
markAsReadFailed: '标记为已读失败',
|
markAsReadFailed: '标记为已读失败',
|
||||||
|
filterByComponent: '组件',
|
||||||
|
allComponents: '全部组件',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
@@ -466,6 +468,7 @@ const zhHans = {
|
|||||||
toolCount: '{{count}} 个工具',
|
toolCount: '{{count}} 个工具',
|
||||||
noPluginsInstalled: '无已安装的插件',
|
noPluginsInstalled: '无已安装的插件',
|
||||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||||
|
selectAll: '全选',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流水线对话',
|
title: '流水线对话',
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ const zhHant = {
|
|||||||
markAsRead: '已讀',
|
markAsRead: '已讀',
|
||||||
markAsReadSuccess: '已標記為已讀',
|
markAsReadSuccess: '已標記為已讀',
|
||||||
markAsReadFailed: '標記為已讀失敗',
|
markAsReadFailed: '標記為已讀失敗',
|
||||||
|
filterByComponent: '組件',
|
||||||
|
allComponents: '全部組件',
|
||||||
},
|
},
|
||||||
mcp: {
|
mcp: {
|
||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
@@ -464,6 +466,7 @@ const zhHant = {
|
|||||||
toolCount: '{{count}} 個工具',
|
toolCount: '{{count}} 個工具',
|
||||||
noPluginsInstalled: '無已安裝的插件',
|
noPluginsInstalled: '無已安裝的插件',
|
||||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||||
|
selectAll: '全選',
|
||||||
},
|
},
|
||||||
debugDialog: {
|
debugDialog: {
|
||||||
title: '流程線對話',
|
title: '流程線對話',
|
||||||
|
|||||||
Reference in New Issue
Block a user