Compare commits

...

4 Commits

Author SHA1 Message Date
Junyan Qin
127a38b15c chore: bump version 4.3.9 2025-10-22 18:52:45 +08:00
fdc310
e4729337c8 Feat/coze runner (#1714)
* feat:add coze api client and coze runner and coze config

* del print

* fix:Change the default setting of the plugin system to true

* fix:del multimodal-support config, default multimodal-support,and in cozeapi.py Obtain timeout and auto-save-history config

* chore: add comment for coze.com

---------

Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-10-17 18:13:03 +08:00
Junyan Qin
5fa75330cf perf: store pipeline sort method 2025-10-12 21:11:30 +08:00
Junyan Qin
547e3d098e perf: add component list in plugin detail dialog 2025-10-12 19:57:42 +08:00
23 changed files with 713 additions and 79 deletions

View File

View File

@@ -0,0 +1,192 @@
import json
import asyncio
import aiohttp
import io
from typing import Dict, List, Any, AsyncGenerator
import os
from pathlib import Path
class AsyncCozeAPIClient:
def __init__(self, api_key: str, api_base: str = "https://api.coze.cn"):
self.api_key = api_key
self.api_base = api_base
self.session = None
async def __aenter__(self):
"""支持异步上下文管理器"""
await self.coze_session()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""退出时自动关闭会话"""
await self.close()
async def coze_session(self):
"""确保HTTP session存在"""
if self.session is None:
connector = aiohttp.TCPConnector(
ssl=False if self.api_base.startswith("http://") else True,
limit=100,
limit_per_host=30,
keepalive_timeout=30,
enable_cleanup_closed=True,
)
timeout = aiohttp.ClientTimeout(
total=120, # 默认超时时间
connect=30,
sock_read=120,
)
headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "text/event-stream",
}
self.session = aiohttp.ClientSession(
headers=headers, timeout=timeout, connector=connector
)
return self.session
async def close(self):
"""显式关闭会话"""
if self.session and not self.session.closed:
await self.session.close()
self.session = None
async def upload(
self,
file,
) -> str:
# 处理 Path 对象
if isinstance(file, Path):
if not file.exists():
raise ValueError(f"File not found: {file}")
with open(file, "rb") as f:
file = f.read()
# 处理文件路径字符串
elif isinstance(file, str):
if not os.path.isfile(file):
raise ValueError(f"File not found: {file}")
with open(file, "rb") as f:
file = f.read()
# 处理文件对象
elif hasattr(file, 'read'):
file = file.read()
session = await self.coze_session()
url = f"{self.api_base}/v1/files/upload"
try:
file_io = io.BytesIO(file)
async with session.post(
url,
data={
"file": file_io,
},
timeout=aiohttp.ClientTimeout(total=60),
) as response:
if response.status == 401:
raise Exception("Coze API 认证失败,请检查 API Key 是否正确")
response_text = await response.text()
if response.status != 200:
raise Exception(
f"文件上传失败,状态码: {response.status}, 响应: {response_text}"
)
try:
result = await response.json()
except json.JSONDecodeError:
raise Exception(f"文件上传响应解析失败: {response_text}")
if result.get("code") != 0:
raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}")
file_id = result["data"]["id"]
return file_id
except asyncio.TimeoutError:
raise Exception("文件上传超时")
except Exception as e:
raise Exception(f"文件上传失败: {str(e)}")
async def chat_messages(
self,
bot_id: str,
user_id: str,
additional_messages: List[Dict] | None = None,
conversation_id: str | None = None,
auto_save_history: bool = True,
stream: bool = True,
timeout: float = 120,
) -> AsyncGenerator[Dict[str, Any], None]:
"""发送聊天消息并返回流式响应
Args:
bot_id: Bot ID
user_id: 用户ID
additional_messages: 额外消息列表
conversation_id: 会话ID
auto_save_history: 是否自动保存历史
stream: 是否流式响应
timeout: 超时时间
"""
session = await self.coze_session()
url = f"{self.api_base}/v3/chat"
payload = {
"bot_id": bot_id,
"user_id": user_id,
"stream": stream,
"auto_save_history": auto_save_history,
}
if additional_messages:
payload["additional_messages"] = additional_messages
params = {}
if conversation_id:
params["conversation_id"] = conversation_id
try:
async with session.post(
url,
json=payload,
params=params,
timeout=aiohttp.ClientTimeout(total=timeout),
) as response:
if response.status == 401:
raise Exception("Coze API 认证失败,请检查 API Key 是否正确")
if response.status != 200:
raise Exception(f"Coze API 流式请求失败,状态码: {response.status}")
async for chunk in response.content:
chunk = chunk.decode("utf-8")
if chunk != '\n':
if chunk.startswith("event:"):
chunk_type = chunk.replace("event:", "", 1).strip()
elif chunk.startswith("data:"):
chunk_data = chunk.replace("data:", "", 1).strip()
else:
yield {"event": chunk_type, "data": json.loads(chunk_data)}
except asyncio.TimeoutError:
raise Exception(f"Coze API 流式请求超时 ({timeout}秒)")
except Exception as e:
raise Exception(f"Coze API 流式请求失败: {str(e)}")

View File

@@ -0,0 +1,312 @@
from __future__ import annotations
import typing
import json
import uuid
import base64
from .. import runner
from ...core import app
import langbot_plugin.api.entities.builtin.provider.message as provider_message
from ...utils import image
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from libs.coze_server_api.client import AsyncCozeAPIClient
@runner.runner_class('coze-api')
class CozeAPIRunner(runner.RequestRunner):
"""Coze API 对话请求器"""
def __init__(self, ap: app.Application, pipeline_config: dict):
self.pipeline_config = pipeline_config
self.ap = ap
self.agent_token = pipeline_config["ai"]['coze-api']['api-key']
self.bot_id = pipeline_config["ai"]['coze-api'].get('bot-id')
self.chat_timeout = pipeline_config["ai"]['coze-api'].get('timeout')
self.auto_save_history = pipeline_config["ai"]['coze-api'].get('auto_save_history')
self.api_base = pipeline_config["ai"]['coze-api'].get('api-base')
self.coze = AsyncCozeAPIClient(
self.agent_token,
self.api_base
)
def _process_thinking_content(
self,
content: str,
) -> tuple[str, str]:
"""处理思维链内容
Args:
content: 原始内容
Returns:
(处理后的内容, 提取的思维链内容)
"""
remove_think = self.pipeline_config.get('output', {}).get('misc', {}).get('remove-think', False)
thinking_content = ''
# 从 content 中提取 <think> 标签内容
if content and '<think>' in content and '</think>' in content:
import re
think_pattern = r'<think>(.*?)</think>'
think_matches = re.findall(think_pattern, content, re.DOTALL)
if think_matches:
thinking_content = '\n'.join(think_matches)
# 移除 content 中的 <think> 标签
content = re.sub(think_pattern, '', content, flags=re.DOTALL).strip()
# 根据 remove_think 参数决定是否保留思维链
if remove_think:
return content, ''
else:
# 如果有思维链内容,将其以 <think> 格式添加到 content 开头
if thinking_content:
content = f'<think>\n{thinking_content}\n</think>\n{content}'.strip()
return content, thinking_content
async def _preprocess_user_message(self, query: pipeline_query.Query) -> list[dict]:
"""预处理用户消息转换为Coze消息格式
Returns:
list[dict]: Coze消息列表
"""
messages = []
if isinstance(query.user_message.content, list):
# 多模态消息处理
content_parts = []
for ce in query.user_message.content:
if ce.type == 'text':
content_parts.append({"type": "text", "text": ce.text})
elif ce.type == 'image_base64':
image_b64, image_format = await image.extract_b64_and_format(ce.image_base64)
file_bytes = base64.b64decode(image_b64)
file_id = await self._get_file_id(file_bytes)
content_parts.append({"type": "image", "file_id": file_id})
elif ce.type == 'file':
# 处理文件上传到Coze
file_id = await self._get_file_id(ce.file)
content_parts.append({"type": "file", "file_id": file_id})
# 创建多模态消息
if content_parts:
messages.append({
"role": "user",
"content": json.dumps(content_parts),
"content_type": "object_string",
"meta_data": None
})
elif isinstance(query.user_message.content, str):
# 纯文本消息
messages.append({
"role": "user",
"content": query.user_message.content,
"content_type": "text",
"meta_data": None
})
return messages
async def _get_file_id(self, file) -> str:
"""上传文件到Coze服务
Args:
file: 文件
Returns:
str: 文件ID
"""
file_id = await self.coze.upload(file=file)
return file_id
async def _chat_messages(
self, query: pipeline_query.Query
) -> typing.AsyncGenerator[provider_message.Message, None]:
"""调用聊天助手(非流式)
注意由于cozepy没有提供非流式API这里使用流式API并在结束后一次性返回完整内容
"""
user_id = f'{query.launcher_id}_{query.sender_id}'
# 预处理用户消息
additional_messages = await self._preprocess_user_message(query)
# 获取会话ID
conversation_id = None
# 收集完整内容
full_content = ''
full_reasoning = ''
try:
# 调用Coze API流式接口
async for chunk in self.coze.chat_messages(
bot_id=self.bot_id,
user_id=user_id,
additional_messages=additional_messages,
conversation_id=conversation_id,
timeout=self.chat_timeout,
auto_save_history=self.auto_save_history,
stream=True
):
self.ap.logger.debug(f'coze-chat-stream: {chunk}')
event_type = chunk.get('event')
data = chunk.get('data', {})
if event_type == 'conversation.message.delta':
# 收集内容
if 'content' in data:
full_content += data.get('content', '')
# 收集推理内容(如果有)
if 'reasoning_content' in data:
full_reasoning += data.get('reasoning_content', '')
elif event_type == 'done':
# 保存会话ID
if 'conversation_id' in data:
conversation_id = data.get('conversation_id')
elif event_type == 'error':
# 处理错误
error_msg = f"Coze API错误: {data.get('message', '未知错误')}"
yield provider_message.Message(
role='assistant',
content=error_msg,
)
return
# 处理思维链内容
content, thinking_content = self._process_thinking_content(full_content)
if full_reasoning:
remove_think = self.pipeline_config.get('output', {}).get('misc', {}).get('remove-think', False)
if not remove_think:
content = f'<think>\n{full_reasoning}\n</think>\n{content}'.strip()
# 一次性返回完整内容
yield provider_message.Message(
role='assistant',
content=content,
)
# 保存会话ID
if conversation_id and query.session.using_conversation:
query.session.using_conversation.uuid = conversation_id
except Exception as e:
self.ap.logger.error(f'Coze API错误: {str(e)}')
yield provider_message.Message(
role='assistant',
content=f'Coze API调用失败: {str(e)}',
)
async def _chat_messages_chunk(
self, query: pipeline_query.Query
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
"""调用聊天助手(流式)"""
user_id = f'{query.launcher_id}_{query.sender_id}'
# 预处理用户消息
additional_messages = await self._preprocess_user_message(query)
# 获取会话ID
conversation_id = None
start_reasoning = False
stop_reasoning = False
message_idx = 1
is_final = False
full_content = ''
remove_think = self.pipeline_config.get('output', {}).get('misc', {}).get('remove-think', False)
try:
# 调用Coze API流式接口
async for chunk in self.coze.chat_messages(
bot_id=self.bot_id,
user_id=user_id,
additional_messages=additional_messages,
conversation_id=conversation_id,
timeout=self.chat_timeout,
auto_save_history=self.auto_save_history,
stream=True
):
self.ap.logger.debug(f'coze-chat-stream-chunk: {chunk}')
event_type = chunk.get('event')
data = chunk.get('data', {})
content = ""
if event_type == 'conversation.message.delta':
message_idx += 1
# 处理内容增量
if "reasoning_content" in data and not remove_think:
reasoning_content = data.get('reasoning_content', '')
if reasoning_content and not start_reasoning:
content = f"<think/>\n"
start_reasoning = True
content += reasoning_content
if 'content' in data:
if data.get('content', ''):
content += data.get('content', '')
if not stop_reasoning and start_reasoning:
content = f"</think>\n{content}"
stop_reasoning = True
elif event_type == 'done':
# 保存会话ID
if 'conversation_id' in data:
conversation_id = data.get('conversation_id')
if query.session.using_conversation:
query.session.using_conversation.uuid = conversation_id
is_final = True
elif event_type == 'error':
# 处理错误
error_msg = f"Coze API错误: {data.get('message', '未知错误')}"
yield provider_message.MessageChunk(
role='assistant',
content=error_msg,
finish_reason='error'
)
return
full_content += content
if message_idx % 8 == 0 or is_final:
if full_content:
yield provider_message.MessageChunk(
role='assistant',
content=full_content,
is_final=is_final
)
except Exception as e:
self.ap.logger.error(f'Coze API流式调用错误: {str(e)}')
yield provider_message.MessageChunk(
role='assistant',
content=f'Coze API流式调用失败: {str(e)}',
finish_reason='error'
)
async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
"""运行"""
msg_seq = 0
if await query.adapter.is_stream_output_supported():
async for msg in self._chat_messages_chunk(query):
if isinstance(msg, provider_message.MessageChunk):
msg_seq += 1
msg.msg_sequence = msg_seq
yield msg
else:
async for msg in self._chat_messages(query):
yield msg

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.3.8' semantic_version = 'v4.3.9'
required_database_version = 8 required_database_version = 8
"""Tag the version of the database schema, used to check if the database needs to be migrated""" """Tag the version of the database schema, used to check if the database needs to be migrated"""

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "langbot" name = "langbot"
version = "4.3.8" version = "4.3.9"
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"
requires-python = ">=3.10.1,<4.0" requires-python = ">=3.10.1,<4.0"

View File

@@ -41,4 +41,4 @@ plugin:
enable: true enable: true
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws' runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
enable_marketplace: true enable_marketplace: true
cloud_service_url: 'https://space.langbot.app' cloud_service_url: 'https://space.langbot.app'

View File

@@ -43,6 +43,10 @@ stages:
label: label:
en_US: Langflow API en_US: Langflow API
zh_Hans: Langflow API zh_Hans: Langflow API
- name: coze-api
label:
en_US: Coze API
zh_Hans: 扣子 API
- name: local-agent - name: local-agent
label: label:
en_US: Local Agent en_US: Local Agent
@@ -380,4 +384,57 @@ stages:
zh_Hans: 可选的流程调整参数 zh_Hans: 可选的流程调整参数
type: json type: json
required: false required: false
default: '{}' default: '{}'
- name: coze-api
label:
en_US: coze API
zh_Hans: 扣子 API
description:
en_US: Configure the Coze API of the pipeline
zh_Hans: 配置Coze API
config:
- name: api-key
label:
en_US: API Key
zh_Hans: API 密钥
description:
en_US: The API key for the Coze server
zh_Hans: Coze服务器的 API 密钥
type: string
required: true
- name: bot-id
label:
en_US: Bot ID
zh_Hans: 机器人 ID
description:
en_US: The ID of the bot to run
zh_Hans: 要运行的机器人 ID
type: string
required: true
- name: api-base
label:
en_US: API Base URL
zh_Hans: API 基础 URL
description:
en_US: The base URL for the Coze API, please use https://api.coze.com for global Coze edition(coze.com).
zh_Hans: Coze API 的基础 URL请使用 https://api.coze.com 用于全球 Coze 版coze.com
type: string
default: "https://api.coze.cn"
- name: auto-save-history
label:
en_US: Auto Save History
zh_Hans: 自动保存历史
description:
en_US: Whether to automatically save conversation history
zh_Hans: 是否自动保存对话历史
type: boolean
default: true
- name: timeout
label:
en_US: Request Timeout
zh_Hans: 请求超时
description:
en_US: Timeout in seconds for API requests
zh_Hans: API 请求超时时间(秒)
type: number
default: 120

View File

@@ -29,7 +29,17 @@ export default function PluginConfigPage() {
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC'); const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
useEffect(() => { useEffect(() => {
getPipelines(); // Load sort preference from localStorage
const savedSortBy = localStorage.getItem('pipeline_sort_by');
const savedSortOrder = localStorage.getItem('pipeline_sort_order');
if (savedSortBy && savedSortOrder) {
setSortByValue(savedSortBy);
setSortOrderValue(savedSortOrder);
getPipelines(savedSortBy, savedSortOrder);
} else {
getPipelines();
}
}, []); }, []);
function getPipelines( function getPipelines(
@@ -91,6 +101,11 @@ export default function PluginConfigPage() {
const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim()); const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
setSortByValue(newSortBy); setSortByValue(newSortBy);
setSortOrderValue(newSortOrder); setSortOrderValue(newSortOrder);
// Save sort preference to localStorage
localStorage.setItem('pipeline_sort_by', newSortBy);
localStorage.setItem('pipeline_sort_order', newSortOrder);
getPipelines(newSortBy, newSortOrder); getPipelines(newSortBy, newSortOrder);
} }
@@ -135,6 +150,12 @@ export default function PluginConfigPage() {
> >
{t('pipelines.newestCreated')} {t('pipelines.newestCreated')}
</SelectItem> </SelectItem>
<SelectItem
value="created_at,ASC"
className="text-gray-900 dark:text-gray-100"
>
{t('pipelines.earliestCreated')}
</SelectItem>
<SelectItem <SelectItem
value="updated_at,DESC" value="updated_at,DESC"
className="text-gray-900 dark:text-gray-100" className="text-gray-900 dark:text-gray-100"

View File

@@ -0,0 +1,75 @@
import { PluginComponent } from '@/app/infra/entities/plugin';
import { TFunction } from 'i18next';
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
export default function PluginComponentList({
components,
showComponentName,
showTitle,
useBadge,
t,
}: {
components: PluginComponent[];
showComponentName: boolean;
showTitle: boolean;
useBadge: boolean;
t: TFunction;
}) {
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);
return (
<>
{showTitle && <div>{t('plugins.componentsList')}</div>}
{componentKindList.length > 0 && (
<>
{componentKindList.map((kind) => {
return (
<>
{useBadge && (
<Badge variant="outline">
{kindIconMap[kind]}
{showComponentName &&
t('plugins.componentName.' + kind) + ' '}
{componentKindCount[kind]}
</Badge>
)}
{!useBadge && (
<div
key={kind}
className="flex flex-row items-center justify-start gap-[0.2rem]"
>
{kindIconMap[kind]}
{showComponentName &&
t('plugins.componentName.' + kind) + ' '}
{componentKindCount[kind]}
</div>
)}
</>
);
})}
</>
)}
{componentKindList.length === 0 && <div>{t('plugins.noComponents')}</div>}
</>
);
}

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
import PluginCardComponent from '@/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent'; import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
import PluginForm from '@/app/home/plugins/plugin-installed/plugin-form/PluginForm'; import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-form/PluginForm';
import styles from '@/app/home/plugins/plugins.module.css'; import styles from '@/app/home/plugins/plugins.module.css';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { import {

View File

@@ -1,21 +1,10 @@
import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO'; import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
import { useState } from 'react'; import { useState } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next'; import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react';
import {
AudioWaveform,
Wrench,
Hash,
BugIcon,
ExternalLink,
Ellipsis,
Trash,
ArrowUp,
} from 'lucide-react';
import { getCloudServiceClientSync } from '@/app/infra/http'; import { getCloudServiceClientSync } from '@/app/infra/http';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { PluginComponent } from '@/app/infra/entities/plugin';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -23,49 +12,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
function getComponentList(components: PluginComponent[], t: TFunction) {
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);
return (
<>
<div>{t('plugins.componentsList')}</div>
{componentKindList.length > 0 && (
<>
{componentKindList.map((kind) => {
return (
<div
key={kind}
className="flex flex-row items-center justify-start gap-[0.4rem]"
>
{kindIconMap[kind]} {componentKindCount[kind]}
</div>
);
})}
</>
)}
{componentKindList.length === 0 && <div>{t('plugins.noComponents')}</div>}
</>
);
}
export default function PluginCardComponent({ export default function PluginCardComponent({
cardVO, cardVO,
@@ -180,7 +127,13 @@ export default function PluginCardComponent({
</div> </div>
<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]">
{getComponentList(cardVO.components, t)} <PluginComponentList
components={cardVO.components}
showComponentName={false}
showTitle={true}
useBadge={false}
t={t}
/>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
export default function PluginForm({ export default function PluginForm({
pluginAuthor, pluginAuthor,
@@ -78,6 +79,17 @@ export default function PluginForm({
}, },
)} )}
</div> </div>
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
<PluginComponentList
components={pluginInfo.components}
showComponentName={true}
showTitle={false}
useBadge={true}
t={t}
/>
</div>
{pluginInfo.manifest.manifest.spec.config.length > 0 && ( {pluginInfo.manifest.manifest.spec.config.length > 0 && (
<DynamicFormComponent <DynamicFormComponent
itemConfigList={pluginInfo.manifest.manifest.spec.config} itemConfigList={pluginInfo.manifest.manifest.spec.config}

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import PluginInstalledComponent, { import PluginInstalledComponent, {
PluginInstalledComponentRef, PluginInstalledComponentRef,
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; } from '@/app/home/plugins/components/plugin-installed/PluginInstalledComponent';
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import MarketPage from '@/app/home/plugins/components/plugin-market/PluginMarketComponent';
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; // import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import styles from './plugins.module.css'; import styles from './plugins.module.css';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

View File

@@ -206,9 +206,11 @@ const enUS = {
deleteConfirm: 'Delete Confirmation', deleteConfirm: 'Delete Confirmation',
deleteSuccess: 'Delete successful', deleteSuccess: 'Delete successful',
modifyFailed: 'Modify failed: ', modifyFailed: 'Modify failed: ',
eventCount: 'Events: {{count}}', componentName: {
toolCount: 'Tools: {{count}}', Tool: 'Tool',
starCount: 'Stars: {{count}}', EventListener: 'Event Listener',
Command: 'Command',
},
uploadLocal: 'Upload Local', uploadLocal: 'Upload Local',
debugging: 'Debugging', debugging: 'Debugging',
uploadLocalPlugin: 'Upload Local Plugin', uploadLocalPlugin: 'Upload Local Plugin',
@@ -298,6 +300,7 @@ const enUS = {
defaultBadge: 'Default', defaultBadge: 'Default',
sortBy: 'Sort by', sortBy: 'Sort by',
newestCreated: 'Newest Created', newestCreated: 'Newest Created',
earliestCreated: 'Earliest Created',
recentlyEdited: 'Recently Edited', recentlyEdited: 'Recently Edited',
earliestEdited: 'Earliest Edited', earliestEdited: 'Earliest Edited',
basicInfo: 'Basic', basicInfo: 'Basic',

View File

@@ -207,9 +207,11 @@ const jaJP = {
deleteConfirm: '削除の確認', deleteConfirm: '削除の確認',
deleteSuccess: '削除に成功しました', deleteSuccess: '削除に成功しました',
modifyFailed: '変更に失敗しました:', modifyFailed: '変更に失敗しました:',
eventCount: 'イベント:{{count}}', componentName: {
toolCount: 'ツール:{{count}}', Tool: 'ツール',
starCount: 'スター:{{count}}', EventListener: 'イベント監視器',
Command: 'コマンド',
},
uploadLocal: 'ローカルアップロード', uploadLocal: 'ローカルアップロード',
debugging: 'デバッグ中', debugging: 'デバッグ中',
uploadLocalPlugin: 'ローカルプラグインのアップロード', uploadLocalPlugin: 'ローカルプラグインのアップロード',
@@ -300,6 +302,7 @@ const jaJP = {
defaultBadge: 'デフォルト', defaultBadge: 'デフォルト',
sortBy: '並び順', sortBy: '並び順',
newestCreated: '最新作成', newestCreated: '最新作成',
earliestCreated: '最古作成',
recentlyEdited: '最近編集', recentlyEdited: '最近編集',
earliestEdited: '最古編集', earliestEdited: '最古編集',
basicInfo: '基本情報', basicInfo: '基本情報',

View File

@@ -199,9 +199,11 @@ const zhHans = {
deleteConfirm: '删除确认', deleteConfirm: '删除确认',
deleteSuccess: '删除成功', deleteSuccess: '删除成功',
modifyFailed: '修改失败:', modifyFailed: '修改失败:',
eventCount: '事件:{{count}}', componentName: {
toolCount: '工具:{{count}}', Tool: '工具',
starCount: '星标:{{count}}', EventListener: '事件监听器',
Command: '命令',
},
uploadLocal: '本地上传', uploadLocal: '本地上传',
debugging: '调试中', debugging: '调试中',
uploadLocalPlugin: '上传本地插件', uploadLocalPlugin: '上传本地插件',
@@ -285,6 +287,7 @@ const zhHans = {
defaultBadge: '默认', defaultBadge: '默认',
sortBy: '排序方式', sortBy: '排序方式',
newestCreated: '最新创建', newestCreated: '最新创建',
earliestCreated: '最早创建',
recentlyEdited: '最近编辑', recentlyEdited: '最近编辑',
earliestEdited: '最早编辑', earliestEdited: '最早编辑',
basicInfo: '基础信息', basicInfo: '基础信息',

View File

@@ -197,9 +197,11 @@ const zhHant = {
close: '關閉', close: '關閉',
deleteConfirm: '刪除確認', deleteConfirm: '刪除確認',
modifyFailed: '修改失敗:', modifyFailed: '修改失敗:',
eventCount: '事件:{{count}}', componentName: {
toolCount: '工具:{{count}}', Tool: '工具',
starCount: '星標:{{count}}', EventListener: '事件監聽器',
Command: '命令',
},
uploadLocal: '本地上傳', uploadLocal: '本地上傳',
debugging: '調試中', debugging: '調試中',
uploadLocalPlugin: '上傳本地插件', uploadLocalPlugin: '上傳本地插件',
@@ -283,6 +285,7 @@ const zhHant = {
defaultBadge: '預設', defaultBadge: '預設',
sortBy: '排序方式', sortBy: '排序方式',
newestCreated: '最新建立', newestCreated: '最新建立',
earliestCreated: '最早建立',
recentlyEdited: '最近編輯', recentlyEdited: '最近編輯',
earliestEdited: '最早編輯', earliestEdited: '最早編輯',
basicInfo: '基本資訊', basicInfo: '基本資訊',