mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
Compare commits
22 Commits
763c1a885c
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba1de9408d | ||
|
|
f49f9fdff1 | ||
|
|
6b20cb7144 | ||
|
|
2e1f16d7b4 | ||
|
|
50c33c5213 | ||
|
|
ace6d62d76 | ||
|
|
b7c4c21796 | ||
|
|
66602da9cb | ||
|
|
31b483509c | ||
|
|
ba7cf69c9d | ||
|
|
37296be67e | ||
|
|
6c03a1dd31 | ||
|
|
b75ec9e989 | ||
|
|
5c8523e4ef | ||
|
|
9802a42a9e | ||
|
|
99e3abec72 | ||
|
|
fc2efdf994 | ||
|
|
6ed672d996 | ||
|
|
2bf593fa6b | ||
|
|
3182214663 | ||
|
|
20614b20b7 | ||
|
|
da323817f7 |
@@ -8,7 +8,7 @@
|
||||
|
||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
@@ -83,10 +83,10 @@ docker compose up -d
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)。
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)等 LLMOps 平台。
|
||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||
- 🧩 插件扩展、活跃社区:高稳定性、高安全性的生产级插件系统,支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
||||
|
||||
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
@@ -77,10 +79,10 @@ Click the Star and Watch button in the upper right corner of the repository to g
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
||||
- 🧩 Plugin Extension, Active Community: High stability, high security production-level plugin system; Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
||||
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
||||
|
||||
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
143
README_ES.md
Normal file
143
README_ES.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">Inicio</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Despliegue</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Enviar Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot es una plataforma de desarrollo de robots de mensajería instantánea nativa de LLM de código abierto, con el objetivo de proporcionar una experiencia de desarrollo de robots de mensajería instantánea lista para usar, con funciones de aplicación LLM como Agent, RAG, MCP, adaptándose a plataformas de mensajería instantánea globales y proporcionando interfaces API ricas, compatible con desarrollo personalizado.
|
||||
|
||||
## 📦 Comenzar
|
||||
|
||||
#### Inicio Rápido
|
||||
|
||||
Use `uvx` para iniciar con un comando (necesita instalar [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
|
||||
#### Despliegue con Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
|
||||
Documentación detallada [Despliegue con Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Despliegue con un clic en BTPanel
|
||||
|
||||
LangBot ha sido listado en BTPanel. Si tiene BTPanel instalado, puede usar la [documentación](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) para usarlo.
|
||||
|
||||
#### Despliegue en la Nube Zeabur
|
||||
|
||||
Plantilla de Zeabur contribuida por la comunidad.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Despliegue en la Nube Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Otros Métodos de Despliegue
|
||||
|
||||
Use directamente la versión publicada para ejecutar, consulte la documentación de [Despliegue Manual](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Despliegue en Kubernetes
|
||||
|
||||
Consulte la documentación de [Despliegue en Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Manténgase Actualizado
|
||||
|
||||
Haga clic en los botones Star y Watch en la esquina superior derecha del repositorio para obtener las últimas actualizaciones.
|
||||
|
||||

|
||||
|
||||
## ✨ Características
|
||||
|
||||
- 💬 Chat con LLM / Agent: Compatible con múltiples LLMs, adaptado para chats grupales y privados; Admite conversaciones de múltiples rondas, llamadas a herramientas, capacidades multimodales y de salida en streaming. Implementación RAG (base de conocimientos) incorporada, e integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Soporte Multiplataforma: Actualmente compatible con QQ, QQ Channel, WeCom, WeChat personal, Lark, DingTalk, Discord, Telegram, etc.
|
||||
- 🛠️ Alta Estabilidad, Rico en Funciones: Control de acceso nativo, limitación de velocidad, filtrado de palabras sensibles, etc.; Fácil de usar, admite múltiples métodos de despliegue. Compatible con múltiples configuraciones de pipeline, diferentes bots para diferentes escenarios.
|
||||
- 🧩 Extensión de Plugin, Comunidad Activa: Sistema de plugin de alta estabilidad, alta seguridad de nivel de producción; Compatible con mecanismos de plugin impulsados por eventos, extensión de componentes, etc.; Integración del protocolo [MCP](https://modelcontextprotocol.io/) de Anthropic; Actualmente cuenta con cientos de plugins.
|
||||
- 😻 Interfaz Web: Admite la gestión de instancias de LangBot a través del navegador. No es necesario escribir archivos de configuración manualmente.
|
||||
|
||||
Para especificaciones más detalladas, consulte la [documentación](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
O visite el entorno de demostración: https://demo.langbot.dev/
|
||||
- Información de inicio de sesión: Correo electrónico: `demo@langbot.app` Contraseña: `langbot123456`
|
||||
- Nota: Solo para demostración de WebUI, por favor no ingrese información confidencial en el entorno público.
|
||||
|
||||
### Plataformas de Mensajería
|
||||
|
||||
| Plataforma | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personal | ✅ | |
|
||||
| QQ API Oficial | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personal | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible para cualquier modelo con formato de interfaz OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plataforma de agregación LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plataforma LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Gateway de interfaz LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Gateway LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Compatible con acceso a herramientas a través del protocolo MCP |
|
||||
|
||||
## 🤝 Contribución de la Comunidad
|
||||
|
||||
Gracias a los siguientes [contribuidores de código](https://github.com/langbot-app/LangBot/graphs/contributors) y otros miembros de la comunidad por sus contribuciones a LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
143
README_FR.md
Normal file
143
README_FR.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">Accueil</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Déploiement</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Soumettre un Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot est une plateforme de développement de robots de messagerie instantanée native LLM open source, visant à fournir une expérience de développement de robots de messagerie instantanée prête à l'emploi, avec des fonctionnalités d'application LLM telles qu'Agent, RAG, MCP, s'adaptant aux plateformes de messagerie instantanée mondiales et fournissant des interfaces API riches, prenant en charge le développement personnalisé.
|
||||
|
||||
## 📦 Commencer
|
||||
|
||||
#### Démarrage Rapide
|
||||
|
||||
Utilisez `uvx` pour démarrer avec une commande (besoin d'installer [uv](https://docs.astral.sh/uv/getting-started/installation/)) :
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
|
||||
#### Déploiement avec Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
|
||||
Documentation détaillée [Déploiement Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Déploiement en un clic sur BTPanel
|
||||
|
||||
LangBot a été répertorié sur BTPanel. Si vous avez installé BTPanel, vous pouvez utiliser la [documentation](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) pour l'utiliser.
|
||||
|
||||
#### Déploiement Cloud Zeabur
|
||||
|
||||
Modèle Zeabur contribué par la communauté.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Déploiement Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Autres Méthodes de Déploiement
|
||||
|
||||
Utilisez directement la version publiée pour exécuter, consultez la documentation de [Déploiement Manuel](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Déploiement Kubernetes
|
||||
|
||||
Consultez la documentation de [Déploiement Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Restez à Jour
|
||||
|
||||
Cliquez sur les boutons Star et Watch dans le coin supérieur droit du dépôt pour obtenir les dernières mises à jour.
|
||||
|
||||

|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
- 💬 Chat avec LLM / Agent : Prend en charge plusieurs LLM, adapté aux chats de groupe et privés ; Prend en charge les conversations multi-tours, les appels d'outils, les capacités multimodales et de sortie en streaming. Implémentation RAG (base de connaissances) intégrée, et intégration profonde avec [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Support Multi-plateforme : Actuellement compatible avec QQ, QQ Channel, WeCom, WeChat personnel, Lark, DingTalk, Discord, Telegram, etc.
|
||||
- 🛠️ Haute Stabilité, Riche en Fonctionnalités : Contrôle d'accès natif, limitation de débit, filtrage de mots sensibles, etc. ; Facile à utiliser, prend en charge plusieurs méthodes de déploiement. Prend en charge plusieurs configurations de pipeline, différents bots pour différents scénarios.
|
||||
- 🧩 Extension de Plugin, Communauté Active : Système de plugin de haute stabilité, haute sécurité de niveau production; Prend en charge les mécanismes de plugin pilotés par événements, l'extension de composants, etc. ; Intégration du protocole [MCP](https://modelcontextprotocol.io/) d'Anthropic ; Dispose actuellement de centaines de plugins.
|
||||
- 😻 Interface Web : Prend en charge la gestion des instances LangBot via le navigateur. Pas besoin d'écrire manuellement les fichiers de configuration.
|
||||
|
||||
Pour des spécifications plus détaillées, veuillez consulter la [documentation](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Ou visitez l'environnement de démonstration : https://demo.langbot.dev/
|
||||
- Informations de connexion : Email : `demo@langbot.app` Mot de passe : `langbot123456`
|
||||
- Note : Pour la démonstration WebUI uniquement, veuillez ne pas entrer d'informations sensibles dans l'environnement public.
|
||||
|
||||
### Plateformes de Messagerie
|
||||
|
||||
| Plateforme | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personnel | ✅ | |
|
||||
| API Officielle QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personnel | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible pour tout modèle au format d'interface OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plateforme d'agrégation LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plateforme LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Passerelle d'interface LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Prend en charge l'accès aux outils via le protocole MCP |
|
||||
|
||||
## 🤝 Contribution de la Communauté
|
||||
|
||||
Merci aux [contributeurs de code](https://github.com/langbot-app/LangBot/graphs/contributors) suivants et aux autres membres de la communauté pour leurs contributions à LangBot :
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language)
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
@@ -77,10 +79,10 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
|
||||
|
||||
## ✨ 機能
|
||||
|
||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) などの LLMOps プラットフォームと深く統合。
|
||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||
- 🧩 プラグイン拡張、活発なコミュニティ: 高い安定性、高いセキュリティの生産レベルのプラグインシステム;イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
||||
|
||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
||||
|
||||
143
README_KO.md
Normal file
143
README_KO.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">홈</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">배포</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">플러그인</a> |
|
||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">플러그인 제출</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot은 오픈 소스 LLM 네이티브 인스턴트 메시징 로봇 개발 플랫폼으로, Agent, RAG, MCP 등 다양한 LLM 애플리케이션 기능을 갖춘 즉시 사용 가능한 IM 로봇 개발 경험을 제공하며, 글로벌 인스턴트 메시징 플랫폼에 적응하고 풍부한 API 인터페이스를 제공하여 맞춤형 개발을 지원합니다.
|
||||
|
||||
## 📦 시작하기
|
||||
|
||||
#### 빠른 시작
|
||||
|
||||
`uvx`를 사용하여 한 명령으로 시작하세요 ([uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
|
||||
#### Docker Compose 배포
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
|
||||
자세한 문서는 [Docker 배포](https://docs.langbot.app/en/deploy/langbot/docker.html)를 참조하세요.
|
||||
|
||||
#### BTPanel 원클릭 배포
|
||||
|
||||
LangBot은 BTPanel에 등록되어 있습니다. BTPanel을 설치한 경우 [문서](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)를 사용하여 사용할 수 있습니다.
|
||||
|
||||
#### Zeabur 클라우드 배포
|
||||
|
||||
커뮤니티에서 제공하는 Zeabur 템플릿입니다.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway 클라우드 배포
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 기타 배포 방법
|
||||
|
||||
릴리스 버전을 직접 사용하여 실행하려면 [수동 배포](https://docs.langbot.app/en/deploy/langbot/manual.html) 문서를 참조하세요.
|
||||
|
||||
#### Kubernetes 배포
|
||||
|
||||
[Kubernetes 배포](./docker/README_K8S.md) 문서를 참조하세요.
|
||||
|
||||
## 😎 최신 정보 받기
|
||||
|
||||
리포지토리 오른쪽 상단의 Star 및 Watch 버튼을 클릭하여 최신 업데이트를 받으세요.
|
||||
|
||||

|
||||
|
||||
## ✨ 기능
|
||||
|
||||
- 💬 LLM / Agent와 채팅: 여러 LLM을 지원하며 그룹 채팅 및 개인 채팅에 적응; 멀티 라운드 대화, 도구 호출, 멀티모달, 스트리밍 출력 기능을 지원합니다. 내장된 RAG(지식 베이스) 구현 및 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 등의 LLMOps 플랫폼과 깊이 통합됩니다.
|
||||
- 🤖 다중 플랫폼 지원: 현재 QQ, QQ Channel, WeCom, 개인 WeChat, Lark, DingTalk, Discord, Telegram 등을 지원합니다.
|
||||
- 🛠️ 높은 안정성, 풍부한 기능: 네이티브 액세스 제어, 속도 제한, 민감한 단어 필터링 등의 메커니즘; 사용하기 쉽고 여러 배포 방법을 지원합니다. 여러 파이프라인 구성을 지원하며 다양한 시나리오에 대해 다른 봇을 사용할 수 있습니다.
|
||||
- 🧩 플러그인 확장, 활발한 커뮤니티: 고안정성, 고보안 생산 수준의 플러그인 시스템; 이벤트 기반, 컴포넌트 확장 등의 플러그인 메커니즘을 지원; Anthropic [MCP 프로토콜](https://modelcontextprotocol.io/) 통합; 현재 수백 개의 플러그인이 있습니다.
|
||||
- 😻 웹 UI: 브라우저를 통해 LangBot 인스턴스 관리를 지원합니다. 구성 파일을 수동으로 작성할 필요가 없습니다.
|
||||
|
||||
더 자세한 사양은 [문서](https://docs.langbot.app/en/insight/features.html)를 참조하세요.
|
||||
|
||||
또는 데모 환경을 방문하세요: https://demo.langbot.dev/
|
||||
- 로그인 정보: 이메일: `demo@langbot.app` 비밀번호: `langbot123456`
|
||||
- 참고: WebUI 데모 전용이므로 공개 환경에서는 민감한 정보를 입력하지 마세요.
|
||||
|
||||
### 메시징 플랫폼
|
||||
|
||||
| 플랫폼 | 상태 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| 개인 QQ | ✅ | |
|
||||
| QQ 공식 API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| 개인 WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | 상태 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | 모든 OpenAI 인터페이스 형식 모델에 사용 가능 |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLM 집계 플랫폼 |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps 플랫폼 |
|
||||
| [Ollama](https://ollama.com/) | ✅ | 로컬 LLM 실행 플랫폼 |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 로컬 LLM 실행 플랫폼 |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM 인터페이스 게이트웨이(MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM 게이트웨이(MaaS), LLMOps 플랫폼 |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM 게이트웨이(MaaS), LLMOps 플랫폼 |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM 게이트웨이(MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCP 프로토콜을 통한 도구 액세스 지원 |
|
||||
|
||||
## 🤝 커뮤니티 기여
|
||||
|
||||
다음 [코드 기여자](https://github.com/langbot-app/LangBot/graphs/contributors) 및 커뮤니티의 다른 구성원들의 LangBot 기여에 감사드립니다:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
143
README_RU.md
Normal file
143
README_RU.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">Главная</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Развертывание</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Плагин</a> |
|
||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Отправить плагин</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot — это платформа разработки ботов для мгновенных сообщений на основе LLM с открытым исходным кодом, целью которой является предоставление готового к использованию опыта разработки ботов для IM, с функциями приложений LLM, такими как Agent, RAG, MCP, адаптацией к глобальным платформам мгновенных сообщений и предоставлением богатых API-интерфейсов, поддерживающих пользовательскую разработку.
|
||||
|
||||
## 📦 Начало работы
|
||||
|
||||
#### Быстрый старт
|
||||
|
||||
Используйте `uvx` для запуска одной командой (требуется установка [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
|
||||
#### Развертывание с Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
|
||||
Подробная документация [Развертывание Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Развертывание одним кликом на BTPanel
|
||||
|
||||
LangBot добавлен в BTPanel. Если у вас установлен BTPanel, вы можете использовать [документацию](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) для его использования.
|
||||
|
||||
#### Облачное развертывание Zeabur
|
||||
|
||||
Шаблон Zeabur, предоставленный сообществом.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Облачное развертывание Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Другие методы развертывания
|
||||
|
||||
Используйте выпущенную версию напрямую для запуска, см. документацию [Ручное развертывание](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Развертывание Kubernetes
|
||||
|
||||
См. документацию [Развертывание Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Оставайтесь в курсе
|
||||
|
||||
Нажмите кнопки Star и Watch в правом верхнем углу репозитория, чтобы получать последние обновления.
|
||||
|
||||

|
||||
|
||||
## ✨ Функции
|
||||
|
||||
- 💬 Чат с LLM / Agent: Поддержка нескольких LLM, адаптация к групповым и личным чатам; Поддержка многораундовых разговоров, вызовов инструментов, мультимодальных возможностей и потоковой передачи. Встроенная реализация RAG (база знаний) и глубокая интеграция с [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) 등의 LLMOps 플랫포트폼과 깊이 통합됩니다.
|
||||
- 🤖 Многоплатформенная поддержка: В настоящее время поддерживает QQ, QQ Channel, WeCom, личный WeChat, Lark, DingTalk, Discord, Telegram и т.д.
|
||||
- 🛠️ Высокая стабильность, богатство функций: Нативный контроль доступа, ограничение скорости, фильтрация чувствительных слов и т.д.; Простота в использовании, поддержка нескольких методов развертывания. Поддержка нескольких конфигураций конвейера, разные боты для разных сценариев.
|
||||
- 🧩 Расширение плагинов, активное сообщество: Высокая стабильность, высокая безопасность уровня производства; Поддержка механизмов плагинов, управляемых событиями, расширения компонентов и т.д.; Интеграция протокола [MCP](https://modelcontextprotocol.io/) от Anthropic; В настоящее время сотни плагинов.
|
||||
- 😻 Веб-интерфейс: Поддержка управления экземплярами LangBot через браузер. Нет необходимости вручную писать конфигурационные файлы.
|
||||
|
||||
Для более подробных спецификаций обратитесь к [документации](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Или посетите демонстрационную среду: https://demo.langbot.dev/
|
||||
- Информация для входа: Email: `demo@langbot.app` Пароль: `langbot123456`
|
||||
- Примечание: Только для демонстрации WebUI, пожалуйста, не вводите конфиденциальную информацию в общедоступной среде.
|
||||
|
||||
### Платформы обмена сообщениями
|
||||
|
||||
| Платформа | Статус | Примечания |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| Личный QQ | ✅ | |
|
||||
| Официальный API QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| Личный WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Статус | Примечания |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Доступна для любой модели формата интерфейса OpenAI |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Платформа агрегации LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Платформа LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Платформа локального запуска LLM |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Платформа локального запуска LLM |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Шлюз интерфейса LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Шлюз LLM (MaaS), платформа LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Шлюз LLM (MaaS), платформа LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Шлюз LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Поддержка доступа к инструментам через протокол MCP |
|
||||
|
||||
## 🤝 Вклад сообщества
|
||||
|
||||
Спасибо следующим [контрибьюторам кода](https://github.com/langbot-app/LangBot/graphs/contributors) и другим членам сообщества за их вклад в LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
@@ -79,10 +79,10 @@ docker compose up -d
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 等 LLMOps 平台。
|
||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
||||
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
||||
- 🧩 外掛擴展、活躍社群:高穩定性、高安全性的生產級外掛系統;支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
||||
|
||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
143
README_VI.md
Normal file
143
README_VI.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
<a href="https://langbot.app">Trang chủ</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Triển khai</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Gửi Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot là một nền tảng phát triển robot nhắn tin tức thời gốc LLM mã nguồn mở, nhằm mục đích cung cấp trải nghiệm phát triển robot IM sẵn sàng sử dụng, với các chức năng ứng dụng LLM như Agent, RAG, MCP, thích ứng với các nền tảng nhắn tin tức thời toàn cầu và cung cấp giao diện API phong phú, hỗ trợ phát triển tùy chỉnh.
|
||||
|
||||
## 📦 Bắt đầu
|
||||
|
||||
#### Khởi động Nhanh
|
||||
|
||||
Sử dụng `uvx` để khởi động bằng một lệnh (cần cài đặt [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
|
||||
#### Triển khai Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
|
||||
Tài liệu chi tiết [Triển khai Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Triển khai Một cú nhấp chuột trên BTPanel
|
||||
|
||||
LangBot đã được liệt kê trên BTPanel. Nếu bạn đã cài đặt BTPanel, bạn có thể sử dụng [tài liệu](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) để sử dụng nó.
|
||||
|
||||
#### Triển khai Cloud Zeabur
|
||||
|
||||
Mẫu Zeabur được đóng góp bởi cộng đồng.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Triển khai Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Các Phương pháp Triển khai Khác
|
||||
|
||||
Sử dụng trực tiếp phiên bản phát hành để chạy, xem tài liệu [Triển khai Thủ công](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Triển khai Kubernetes
|
||||
|
||||
Tham khảo tài liệu [Triển khai Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Cập nhật Mới nhất
|
||||
|
||||
Nhấp vào các nút Star và Watch ở góc trên bên phải của kho lưu trữ để nhận các bản cập nhật mới nhất.
|
||||
|
||||

|
||||
|
||||
## ✨ Tính năng
|
||||
|
||||
- 💬 Chat với LLM / Agent: Hỗ trợ nhiều LLM, thích ứng với chat nhóm và chat riêng tư; Hỗ trợ các cuộc trò chuyện nhiều vòng, gọi công cụ, khả năng đa phương thức và đầu ra streaming. Triển khai RAG (cơ sở kiến thức) tích hợp sẵn và tích hợp sâu với [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) v.v. LLMOps platforms.
|
||||
- 🤖 Hỗ trợ Đa nền tảng: Hiện hỗ trợ QQ, QQ Channel, WeCom, WeChat cá nhân, Lark, DingTalk, Discord, Telegram, v.v.
|
||||
- 🛠️ Độ ổn định Cao, Tính năng Phong phú: Kiểm soát truy cập gốc, giới hạn tốc độ, lọc từ nhạy cảm, v.v.; Dễ sử dụng, hỗ trợ nhiều phương pháp triển khai. Hỗ trợ nhiều cấu hình pipeline, các bot khác nhau cho các kịch bản khác nhau.
|
||||
- 🧩 Mở rộng Plugin, Cộng đồng Hoạt động: Hỗ trợ các cơ chế plugin hướng sự kiện, mở rộng thành phần, v.v.; Tích hợp giao thức [MCP](https://modelcontextprotocol.io/) của Anthropic; Hiện có hàng trăng plugin.
|
||||
- 😻 Giao diện Web: Hỗ trợ quản lý các phiên bản LangBot thông qua trình duyệt. Không cần viết tệp cấu hình thủ công.
|
||||
|
||||
Để biết thêm thông số kỹ thuật chi tiết, vui lòng tham khảo [tài liệu](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Hoặc truy cập môi trường demo: https://demo.langbot.dev/
|
||||
- Thông tin đăng nhập: Email: `demo@langbot.app` Mật khẩu: `langbot123456`
|
||||
- Lưu ý: Chỉ dành cho demo WebUI, vui lòng không nhập bất kỳ thông tin nhạy cảm nào trong môi trường công cộng.
|
||||
|
||||
### Nền tảng Nhắn tin
|
||||
|
||||
| Nền tảng | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Cá nhân | ✅ | |
|
||||
| QQ API Chính thức | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Cá nhân | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Có sẵn cho bất kỳ mô hình định dạng giao diện OpenAI nào |
|
||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
||||
| [xAI](https://x.ai/) | ✅ | |
|
||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Nền tảng tổng hợp LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Nền tảng LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Cổng giao diện LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Cổng LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Hỗ trợ truy cập công cụ qua giao thức MCP |
|
||||
|
||||
## 🤝 Đóng góp Cộng đồng
|
||||
|
||||
Cảm ơn các [người đóng góp mã](https://github.com/langbot-app/LangBot/graphs/contributors) sau đây và các thành viên khác trong cộng đồng vì những đóng góp của họ cho LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
@@ -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.13b1",
|
||||
"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'
|
||||
|
||||
@@ -32,6 +32,7 @@ class AsyncDifyServiceClient:
|
||||
conversation_id: str = '',
|
||||
files: list[dict[str, typing.Any]] = [],
|
||||
timeout: float = 30.0,
|
||||
model_config: dict[str, typing.Any] | None = None,
|
||||
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
|
||||
"""发送消息"""
|
||||
if response_mode != 'streaming':
|
||||
@@ -42,6 +43,16 @@ class AsyncDifyServiceClient:
|
||||
trust_env=True,
|
||||
timeout=timeout,
|
||||
) as client:
|
||||
payload = {
|
||||
'inputs': inputs,
|
||||
'query': query,
|
||||
'user': user,
|
||||
'response_mode': response_mode,
|
||||
'conversation_id': conversation_id,
|
||||
'files': files,
|
||||
'model_config': model_config or {},
|
||||
}
|
||||
|
||||
async with client.stream(
|
||||
'POST',
|
||||
'/chat-messages',
|
||||
@@ -49,14 +60,7 @@ class AsyncDifyServiceClient:
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json={
|
||||
'inputs': inputs,
|
||||
'query': query,
|
||||
'user': user,
|
||||
'response_mode': response_mode,
|
||||
'conversation_id': conversation_id,
|
||||
'files': files,
|
||||
},
|
||||
json=payload,
|
||||
) as r:
|
||||
async for chunk in r.aiter_lines():
|
||||
if r.status_code != 200:
|
||||
|
||||
@@ -82,6 +82,16 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
|
||||
return self.success(data={})
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/readme',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
)
|
||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
||||
language = quart.request.args.get('language', 'en')
|
||||
readme = await self.ap.plugin_connector.get_plugin_readme(author, plugin_name, language=language)
|
||||
return self.success(data={'readme': readme})
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/icon',
|
||||
methods=['GET'],
|
||||
@@ -96,6 +106,17 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
|
||||
return quart.Response(icon_data, mimetype=mime_type)
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/assets/<filepath>',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.NONE,
|
||||
)
|
||||
async def _(author: str, plugin_name: str, filepath: str) -> quart.Response:
|
||||
asset_data = await self.ap.plugin_connector.get_plugin_assets(author, plugin_name, filepath)
|
||||
asset_bytes = base64.b64decode(asset_data['asset_base64'])
|
||||
mime_type = asset_data['mime_type']
|
||||
return quart.Response(asset_bytes, mimetype=mime_type)
|
||||
|
||||
@self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _() -> str:
|
||||
"""Get releases from a GitHub repository URL"""
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -99,7 +99,7 @@ class PreProcessor(stage.PipelineStage):
|
||||
content_list: list[provider_message.ContentElement] = []
|
||||
|
||||
plain_text = ''
|
||||
qoute_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
||||
quote_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
||||
|
||||
for me in query.message_chain:
|
||||
if isinstance(me, platform_message.Plain):
|
||||
@@ -114,7 +114,7 @@ class PreProcessor(stage.PipelineStage):
|
||||
elif isinstance(me, platform_message.File):
|
||||
# if me.url is not None:
|
||||
content_list.append(provider_message.ContentElement.from_file_url(me.url, me.name))
|
||||
elif isinstance(me, platform_message.Quote) and qoute_msg:
|
||||
elif isinstance(me, platform_message.Quote) and quote_msg:
|
||||
for msg in me.origin:
|
||||
if isinstance(msg, platform_message.Plain):
|
||||
content_list.append(provider_message.ContentElement.from_text(msg.text))
|
||||
|
||||
@@ -40,6 +40,7 @@ class ChatMessageHandler(handler.MessageHandler):
|
||||
launcher_id=query.launcher_id,
|
||||
sender_id=query.sender_id,
|
||||
text_message=str(query.message_chain),
|
||||
message_chain=query.message_chain,
|
||||
query=query,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -209,7 +209,7 @@ class AiocqhttpMessageConverter(abstract_platform_adapter.AbstractMessageConvert
|
||||
elif msg_data['type'] == 'text':
|
||||
reply_list.append(platform_message.Plain(text=msg_data['data']['text']))
|
||||
|
||||
elif msg_data['type'] == 'forward': # 这里来应该传入转发消息组,暂时传入qoute
|
||||
elif msg_data['type'] == 'forward': # 这里来应该传入转发消息组,暂时传入Quote
|
||||
for forward_msg_datas in msg_data['data']['content']:
|
||||
for forward_msg_data in forward_msg_datas['message']:
|
||||
await process_message_data(forward_msg_data, reply_list)
|
||||
@@ -259,7 +259,7 @@ class AiocqhttpMessageConverter(abstract_platform_adapter.AbstractMessageConvert
|
||||
# await process_message_data(msg_data, yiri_msg_list)
|
||||
pass
|
||||
|
||||
elif msg.type == 'reply': # 此处处理引用消息传入Qoute
|
||||
elif msg.type == 'reply': # 此处处理引用消息传入Quote
|
||||
msg_datas = await bot.get_msg(message_id=msg.data['id'])
|
||||
|
||||
for msg_data in msg_datas['message']:
|
||||
|
||||
@@ -7,7 +7,9 @@ import typing
|
||||
import os
|
||||
import sys
|
||||
import httpx
|
||||
import sqlalchemy
|
||||
from async_lru import alru_cache
|
||||
from langbot_plugin.api.entities.builtin.pipeline.query import provider_session
|
||||
|
||||
from ..core import app
|
||||
from . import handler
|
||||
@@ -26,6 +28,7 @@ from langbot_plugin.api.entities.builtin.command import (
|
||||
)
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
||||
from ..core import taskmgr
|
||||
from ..entity.persistence import plugin as persistence_plugin
|
||||
|
||||
|
||||
class PluginRuntimeConnector:
|
||||
@@ -278,7 +281,61 @@ class PluginRuntimeConnector:
|
||||
if not self.is_enable_plugin:
|
||||
return []
|
||||
|
||||
return await self.handler.list_plugins()
|
||||
plugins = await self.handler.list_plugins()
|
||||
|
||||
# Sort plugins: debug plugins first, then by installation time (newest first)
|
||||
# Get installation timestamps from database in a single query
|
||||
plugin_timestamps = {}
|
||||
|
||||
if plugins:
|
||||
# Build list of (author, name) tuples for all plugins
|
||||
plugin_ids = []
|
||||
for plugin in plugins:
|
||||
author = plugin.get('manifest', {}).get('manifest', {}).get('metadata', {}).get('author', '')
|
||||
name = plugin.get('manifest', {}).get('manifest', {}).get('metadata', {}).get('name', '')
|
||||
if author and name:
|
||||
plugin_ids.append((author, name))
|
||||
|
||||
# Fetch all timestamps in a single query using OR conditions
|
||||
if plugin_ids:
|
||||
conditions = [
|
||||
sqlalchemy.and_(
|
||||
persistence_plugin.PluginSetting.plugin_author == author,
|
||||
persistence_plugin.PluginSetting.plugin_name == name,
|
||||
)
|
||||
for author, name in plugin_ids
|
||||
]
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(
|
||||
persistence_plugin.PluginSetting.plugin_author,
|
||||
persistence_plugin.PluginSetting.plugin_name,
|
||||
persistence_plugin.PluginSetting.created_at,
|
||||
).where(sqlalchemy.or_(*conditions))
|
||||
)
|
||||
|
||||
for row in result:
|
||||
plugin_id = f'{row.plugin_author}/{row.plugin_name}'
|
||||
plugin_timestamps[plugin_id] = row.created_at
|
||||
|
||||
# Sort: debug plugins first (descending), then by created_at (descending)
|
||||
def sort_key(plugin):
|
||||
author = plugin.get('manifest', {}).get('manifest', {}).get('metadata', {}).get('author', '')
|
||||
name = plugin.get('manifest', {}).get('manifest', {}).get('metadata', {}).get('name', '')
|
||||
plugin_id = f'{author}/{name}'
|
||||
|
||||
is_debug = plugin.get('debug', False)
|
||||
created_at = plugin_timestamps.get(plugin_id)
|
||||
|
||||
# Return tuple: (not is_debug, -timestamp)
|
||||
# not is_debug: False (0) for debug plugins, True (1) for non-debug
|
||||
# -timestamp: to sort newest first (will be None for plugins without timestamp)
|
||||
timestamp_value = -created_at.timestamp() if created_at else 0
|
||||
return (not is_debug, timestamp_value)
|
||||
|
||||
plugins.sort(key=sort_key)
|
||||
|
||||
return plugins
|
||||
|
||||
async def get_plugin_info(self, author: str, plugin_name: str) -> dict[str, Any]:
|
||||
return await self.handler.get_plugin_info(author, plugin_name)
|
||||
@@ -290,6 +347,14 @@ class PluginRuntimeConnector:
|
||||
async def get_plugin_icon(self, plugin_author: str, plugin_name: str) -> dict[str, Any]:
|
||||
return await self.handler.get_plugin_icon(plugin_author, plugin_name)
|
||||
|
||||
@alru_cache(ttl=5 * 60) # 5 minutes
|
||||
async def get_plugin_readme(self, plugin_author: str, plugin_name: str, language: str = 'en') -> str:
|
||||
return await self.handler.get_plugin_readme(plugin_author, plugin_name, language)
|
||||
|
||||
@alru_cache(ttl=5 * 60)
|
||||
async def get_plugin_assets(self, plugin_author: str, plugin_name: str, filepath: str) -> dict[str, Any]:
|
||||
return await self.handler.get_plugin_assets(plugin_author, plugin_name, filepath)
|
||||
|
||||
async def emit_event(
|
||||
self,
|
||||
event: events.BaseEventModel,
|
||||
@@ -321,13 +386,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:
|
||||
|
||||
@@ -602,6 +602,51 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
'mime_type': mime_type,
|
||||
}
|
||||
|
||||
async def get_plugin_readme(self, plugin_author: str, plugin_name: str, language: str = 'en') -> str:
|
||||
"""Get plugin readme"""
|
||||
try:
|
||||
result = await self.call_action(
|
||||
LangBotToRuntimeAction.GET_PLUGIN_README,
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
'plugin_name': plugin_name,
|
||||
'language': language,
|
||||
},
|
||||
timeout=20,
|
||||
)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return ''
|
||||
|
||||
readme_file_key = result.get('readme_file_key')
|
||||
if not readme_file_key:
|
||||
return ''
|
||||
|
||||
readme_bytes = await self.read_local_file(readme_file_key)
|
||||
await self.delete_local_file(readme_file_key)
|
||||
|
||||
return readme_bytes.decode('utf-8')
|
||||
|
||||
async def get_plugin_assets(self, plugin_author: str, plugin_name: str, filepath: str) -> dict[str, Any]:
|
||||
"""Get plugin assets"""
|
||||
result = await self.call_action(
|
||||
LangBotToRuntimeAction.GET_PLUGIN_ASSETS_FILE,
|
||||
{
|
||||
'plugin_author': plugin_author,
|
||||
'plugin_name': plugin_name,
|
||||
'file_path': filepath,
|
||||
},
|
||||
timeout=20,
|
||||
)
|
||||
asset_file_key = result['file_file_key']
|
||||
mime_type = result['mime_type']
|
||||
asset_bytes = await self.read_local_file(asset_file_key)
|
||||
await self.delete_local_file(asset_file_key)
|
||||
return {
|
||||
'asset_base64': base64.b64encode(asset_bytes).decode('utf-8'),
|
||||
'mime_type': mime_type,
|
||||
}
|
||||
|
||||
async def cleanup_plugin_data(self, plugin_author: str, plugin_name: str) -> None:
|
||||
"""Cleanup plugin settings and binary storage"""
|
||||
# Delete plugin settings
|
||||
@@ -620,7 +665,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,9 +678,11 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
{
|
||||
'tool_name': tool_name,
|
||||
'tool_parameters': parameters,
|
||||
'session': session,
|
||||
'query_id': query_id,
|
||||
'include_plugins': include_plugins,
|
||||
},
|
||||
timeout=60,
|
||||
timeout=180,
|
||||
)
|
||||
|
||||
return result['tool_response']
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -68,6 +68,33 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
|
||||
return plain_text
|
||||
|
||||
async def _process_stream_response(self, response: aiohttp.ClientResponse) -> typing.AsyncGenerator[
|
||||
provider_message.Message, None]:
|
||||
"""处理流式响应"""
|
||||
full_content = ""
|
||||
message_idx = 0
|
||||
is_final = False
|
||||
async for chunk in response.content.iter_chunked(1024):
|
||||
if not chunk:
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(chunk)
|
||||
if data.get('type') == 'item' and 'content' in data:
|
||||
message_idx += 1
|
||||
content = data['content']
|
||||
full_content += content
|
||||
elif data.get('type') == 'end':
|
||||
is_final = True
|
||||
if is_final or message_idx % 8 == 0:
|
||||
yield provider_message.MessageChunk(
|
||||
role='assistant',
|
||||
content=full_content,
|
||||
is_final=is_final,
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
self.ap.logger.warning(f"Failed to parse final JSON line: {response.text()}")
|
||||
|
||||
async def _call_webhook(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||
"""调用n8n webhook"""
|
||||
# 生成会话ID(如果不存在)
|
||||
@@ -80,6 +107,7 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
# 准备请求数据
|
||||
payload = {
|
||||
# 基本消息内容
|
||||
'chatInput' :plain_text, # 考虑到之前用户直接用的message model这里添加新键
|
||||
'message': plain_text,
|
||||
'user_message_text': plain_text,
|
||||
'conversation_id': query.session.using_conversation.uuid,
|
||||
@@ -91,6 +119,11 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
# 添加所有变量到payload
|
||||
payload.update(query.variables)
|
||||
|
||||
try:
|
||||
is_stream = await query.adapter.is_stream_output_supported()
|
||||
except AttributeError:
|
||||
is_stream = False
|
||||
|
||||
try:
|
||||
# 准备请求头和认证信息
|
||||
headers = {}
|
||||
@@ -126,30 +159,57 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
|
||||
# 调用webhook
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
if is_stream:
|
||||
# 流式请求
|
||||
async with session.post(
|
||||
self.webhook_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=self.timeout
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
|
||||
# 解析响应
|
||||
response_data = await response.json()
|
||||
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||
|
||||
# 从响应中提取输出
|
||||
if self.output_key in response_data:
|
||||
output_content = response_data[self.output_key]
|
||||
# 处理流式响应
|
||||
async for chunk in self._process_stream_response(response):
|
||||
yield chunk
|
||||
else:
|
||||
# 如果没有指定的输出键,则使用整个响应
|
||||
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||
async with session.post(
|
||||
self.webhook_url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
timeout=self.timeout
|
||||
) as response:
|
||||
try:
|
||||
async for chunk in self._process_stream_response(response):
|
||||
output_content = chunk.content if chunk.is_final else ''
|
||||
except:
|
||||
# 非流式请求(保持原有逻辑)
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
|
||||
# 返回消息
|
||||
yield provider_message.Message(
|
||||
role='assistant',
|
||||
content=output_content,
|
||||
)
|
||||
# 解析响应
|
||||
response_data = await response.json()
|
||||
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||
|
||||
# 从响应中提取输出
|
||||
if self.output_key in response_data:
|
||||
output_content = response_data[self.output_key]
|
||||
else:
|
||||
# 如果没有指定的输出键,则使用整个响应
|
||||
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||
|
||||
# 返回消息
|
||||
yield provider_message.Message(
|
||||
role='assistant',
|
||||
content=output_content,
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'n8n webhook call exception: {str(e)}')
|
||||
raise N8nAPIError(f'n8n webhook call exception: {str(e)}')
|
||||
@@ -157,4 +217,4 @@ class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
async def run(self, query: pipeline_query.Query) -> typing.AsyncGenerator[provider_message.Message, None]:
|
||||
"""运行请求"""
|
||||
async for msg in self._call_webhook(query):
|
||||
yield msg
|
||||
yield msg
|
||||
@@ -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}')
|
||||
|
||||
|
||||
1
tests/unit_tests/plugin/__init__.py
Normal file
1
tests/unit_tests/plugin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Plugin connector unit tests
|
||||
228
tests/unit_tests/plugin/test_plugin_list_sorting.py
Normal file
228
tests/unit_tests/plugin/test_plugin_list_sorting.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""Test plugin list sorting functionality."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_list_sorting_debug_first():
|
||||
"""Test that debug plugins appear before non-debug plugins."""
|
||||
from src.langbot.pkg.plugin.connector import PluginRuntimeConnector
|
||||
|
||||
# Mock the application
|
||||
mock_app = MagicMock()
|
||||
mock_app.instance_config.data.get.return_value = {'enable': True}
|
||||
mock_app.logger = MagicMock()
|
||||
|
||||
# Create connector
|
||||
connector = PluginRuntimeConnector(mock_app, AsyncMock())
|
||||
connector.handler = MagicMock()
|
||||
|
||||
# Mock plugin data with different debug states and timestamps
|
||||
now = datetime.now()
|
||||
mock_plugins = [
|
||||
{
|
||||
'debug': False,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author1',
|
||||
'name': 'plugin1',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'debug': True,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author2',
|
||||
'name': 'plugin2',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author3',
|
||||
'name': 'plugin3',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
connector.handler.list_plugins = AsyncMock(return_value=mock_plugins)
|
||||
|
||||
# Mock database query to return all timestamps in a single batch
|
||||
async def mock_execute_async(query):
|
||||
mock_result = MagicMock()
|
||||
|
||||
# Create mock rows for all plugins with timestamps
|
||||
mock_rows = []
|
||||
|
||||
# plugin1: oldest, plugin2: middle, plugin3: newest
|
||||
mock_row1 = MagicMock()
|
||||
mock_row1.plugin_author = 'author1'
|
||||
mock_row1.plugin_name = 'plugin1'
|
||||
mock_row1.created_at = now - timedelta(days=2)
|
||||
mock_rows.append(mock_row1)
|
||||
|
||||
mock_row2 = MagicMock()
|
||||
mock_row2.plugin_author = 'author2'
|
||||
mock_row2.plugin_name = 'plugin2'
|
||||
mock_row2.created_at = now - timedelta(days=1)
|
||||
mock_rows.append(mock_row2)
|
||||
|
||||
mock_row3 = MagicMock()
|
||||
mock_row3.plugin_author = 'author3'
|
||||
mock_row3.plugin_name = 'plugin3'
|
||||
mock_row3.created_at = now
|
||||
mock_rows.append(mock_row3)
|
||||
|
||||
# Make the result iterable
|
||||
mock_result.__iter__ = lambda self: iter(mock_rows)
|
||||
|
||||
return mock_result
|
||||
|
||||
mock_app.persistence_mgr.execute_async = mock_execute_async
|
||||
|
||||
# Call list_plugins
|
||||
result = await connector.list_plugins()
|
||||
|
||||
# Verify sorting: debug plugin should be first
|
||||
assert len(result) == 3
|
||||
assert result[0]['debug'] is True # plugin2 (debug)
|
||||
assert result[0]['manifest']['manifest']['metadata']['name'] == 'plugin2'
|
||||
|
||||
# Remaining should be sorted by created_at (newest first)
|
||||
assert result[1]['debug'] is False
|
||||
assert result[1]['manifest']['manifest']['metadata']['name'] == 'plugin3' # newest non-debug
|
||||
assert result[2]['debug'] is False
|
||||
assert result[2]['manifest']['manifest']['metadata']['name'] == 'plugin1' # oldest non-debug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_list_sorting_by_installation_time():
|
||||
"""Test that non-debug plugins are sorted by installation time (newest first)."""
|
||||
from src.langbot.pkg.plugin.connector import PluginRuntimeConnector
|
||||
|
||||
# Mock the application
|
||||
mock_app = MagicMock()
|
||||
mock_app.instance_config.data.get.return_value = {'enable': True}
|
||||
mock_app.logger = MagicMock()
|
||||
|
||||
# Create connector
|
||||
connector = PluginRuntimeConnector(mock_app, AsyncMock())
|
||||
connector.handler = MagicMock()
|
||||
|
||||
# Mock plugin data - all non-debug with different installation times
|
||||
now = datetime.now()
|
||||
mock_plugins = [
|
||||
{
|
||||
'debug': False,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author1',
|
||||
'name': 'oldest_plugin',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author2',
|
||||
'name': 'middle_plugin',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'debug': False,
|
||||
'manifest': {
|
||||
'manifest': {
|
||||
'metadata': {
|
||||
'author': 'author3',
|
||||
'name': 'newest_plugin',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
connector.handler.list_plugins = AsyncMock(return_value=mock_plugins)
|
||||
|
||||
# Mock database query to return all timestamps in a single batch
|
||||
async def mock_execute_async(query):
|
||||
mock_result = MagicMock()
|
||||
|
||||
# Create mock rows for all plugins with timestamps
|
||||
mock_rows = []
|
||||
|
||||
# oldest_plugin: oldest, middle_plugin: middle, newest_plugin: newest
|
||||
mock_row1 = MagicMock()
|
||||
mock_row1.plugin_author = 'author1'
|
||||
mock_row1.plugin_name = 'oldest_plugin'
|
||||
mock_row1.created_at = now - timedelta(days=10)
|
||||
mock_rows.append(mock_row1)
|
||||
|
||||
mock_row2 = MagicMock()
|
||||
mock_row2.plugin_author = 'author2'
|
||||
mock_row2.plugin_name = 'middle_plugin'
|
||||
mock_row2.created_at = now - timedelta(days=5)
|
||||
mock_rows.append(mock_row2)
|
||||
|
||||
mock_row3 = MagicMock()
|
||||
mock_row3.plugin_author = 'author3'
|
||||
mock_row3.plugin_name = 'newest_plugin'
|
||||
mock_row3.created_at = now
|
||||
mock_rows.append(mock_row3)
|
||||
|
||||
# Make the result iterable
|
||||
mock_result.__iter__ = lambda self: iter(mock_rows)
|
||||
|
||||
return mock_result
|
||||
|
||||
mock_app.persistence_mgr.execute_async = mock_execute_async
|
||||
|
||||
# Call list_plugins
|
||||
result = await connector.list_plugins()
|
||||
|
||||
# Verify sorting: newest first
|
||||
assert len(result) == 3
|
||||
assert result[0]['manifest']['manifest']['metadata']['name'] == 'newest_plugin'
|
||||
assert result[1]['manifest']['manifest']['metadata']['name'] == 'middle_plugin'
|
||||
assert result[2]['manifest']['manifest']['metadata']['name'] == 'oldest_plugin'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_list_empty():
|
||||
"""Test that empty plugin list is handled correctly."""
|
||||
from src.langbot.pkg.plugin.connector import PluginRuntimeConnector
|
||||
|
||||
# Mock the application
|
||||
mock_app = MagicMock()
|
||||
mock_app.instance_config.data.get.return_value = {'enable': True}
|
||||
mock_app.logger = MagicMock()
|
||||
|
||||
# Create connector
|
||||
connector = PluginRuntimeConnector(mock_app, AsyncMock())
|
||||
connector.handler = MagicMock()
|
||||
|
||||
# Mock empty plugin list
|
||||
connector.handler.list_plugins = AsyncMock(return_value=[])
|
||||
|
||||
# Call list_plugins
|
||||
result = await connector.list_plugins()
|
||||
|
||||
# Verify empty list
|
||||
assert len(result) == 0
|
||||
1865
web/package-lock.json
generated
1865
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
||||
"axios": "^1.12.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"i18next": "^25.1.2",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"input-otp": "^1.4.2",
|
||||
@@ -59,6 +60,11 @@
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
@@ -72,6 +78,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.2.4",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
@@ -81,5 +88,6 @@
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@8.9.2+sha512.b9d35fe91b2a5854dadc43034a3e7b2e675fa4b56e20e8e09ef078fa553c18f8aed44051e7b36e8b8dd435f97eb0c44c4ff3b44fc7c6fa7d21e1fac17bbe661e"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||
import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
|
||||
import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-form/PluginForm';
|
||||
import PluginReadme from '@/app/home/plugins/components/plugin-installed/plugin-readme/PluginReadme';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
@@ -39,6 +40,8 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||
null,
|
||||
);
|
||||
const [readmeModalOpen, setReadmeModalOpen] = useState<boolean>(false);
|
||||
const [readmePlugin, setReadmePlugin] = useState<PluginCardVO | null>(null);
|
||||
const [showOperationModal, setShowOperationModal] = useState(false);
|
||||
const [operationType, setOperationType] = useState<PluginOperationType>(
|
||||
PluginOperationType.DELETE,
|
||||
@@ -106,6 +109,11 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function handleViewReadme(plugin: PluginCardVO) {
|
||||
setReadmePlugin(plugin);
|
||||
setReadmeModalOpen(true);
|
||||
}
|
||||
|
||||
function handlePluginDelete(plugin: PluginCardVO) {
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.DELETE);
|
||||
@@ -316,6 +324,25 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={readmeModalOpen} onOpenChange={setReadmeModalOpen}>
|
||||
<DialogContent className="sm:max-w-[900px] max-w-[90vw] max-h-[85vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-2 border-b">
|
||||
<DialogTitle>
|
||||
{readmePlugin &&
|
||||
`${readmePlugin.author}/${readmePlugin.name} - ${t('plugins.readme')}`}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{readmePlugin && (
|
||||
<PluginReadme
|
||||
pluginAuthor={readmePlugin.author}
|
||||
pluginName={readmePlugin.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{pluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
@@ -324,6 +351,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
onCardClick={() => handlePluginClick(vo)}
|
||||
onDeleteClick={() => handlePluginDelete(vo)}
|
||||
onUpgradeClick={() => handlePluginUpdate(vo)}
|
||||
onViewReadme={() => handleViewReadme(vo)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,15 @@ import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/Plu
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react';
|
||||
import {
|
||||
BugIcon,
|
||||
ExternalLink,
|
||||
Ellipsis,
|
||||
Trash,
|
||||
ArrowUp,
|
||||
Settings,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -19,53 +27,59 @@ export default function PluginCardComponent({
|
||||
onCardClick,
|
||||
onDeleteClick,
|
||||
onUpgradeClick,
|
||||
onViewReadme,
|
||||
}: {
|
||||
cardVO: PluginCardVO;
|
||||
onCardClick: () => void;
|
||||
onDeleteClick: (cardVO: PluginCardVO) => void;
|
||||
onUpgradeClick: (cardVO: PluginCardVO) => void;
|
||||
onViewReadme: (cardVO: PluginCardVO) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22]"
|
||||
onClick={onCardClick}
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22] relative transition-all duration-200 hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] hover:scale-[1.005]"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
if (!dropdownOpen) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
{/* <svg
|
||||
className="w-16 h-16 text-[#2288ee]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 4C8 2.34315 9.34315 1 11 1C12.6569 1 14 2.34315 14 4C14 4.35064 13.9398 4.68722 13.8293 5H18C18.5523 5 19 5.44772 19 6V10.1707C19.3128 10.0602 19.6494 10 20 10C21.6569 10 23 11.3431 23 13C23 14.6569 21.6569 16 20 16C19.6494 16 19.3128 15.9398 19 15.8293V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H8.17071C8.06015 4.68722 8 4.35064 8 4Z"></path>
|
||||
</svg> */}
|
||||
{/* Icon - fixed width */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(cardVO.author, cardVO.name)}
|
||||
alt="plugin icon"
|
||||
className="w-16 h-16 rounded-[8%]"
|
||||
className="w-16 h-16 rounded-[8%] flex-shrink-0"
|
||||
/>
|
||||
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666] dark:text-[#999]">
|
||||
{/* Content area - flexible width with min-width to prevent overflow */}
|
||||
<div className="flex-1 min-w-0 h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
{/* Top content area - allows overflow with max height */}
|
||||
<div className="flex flex-col items-start justify-start w-full min-w-0 flex-1 overflow-hidden">
|
||||
<div className="flex flex-col items-start justify-start w-full min-w-0">
|
||||
<div className="text-[0.7rem] text-[#666] dark:text-[#999] truncate w-full">
|
||||
{cardVO.author} / {cardVO.name}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0]">
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem] flex-wrap max-w-full">
|
||||
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0] truncate max-w-[10rem]">
|
||||
{cardVO.label}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] flex-shrink-0"
|
||||
>
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400 flex-shrink-0"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
@@ -76,23 +90,15 @@ export default function PluginCardComponent({
|
||||
{cardVO.install_source === 'github' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-blue-400 text-blue-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
cardVO.install_info.github_url,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
className="text-[0.7rem] border-blue-400 text-blue-400 flex-shrink-0"
|
||||
>
|
||||
{t('plugins.fromGithub')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'local' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-green-400 text-green-400"
|
||||
className="text-[0.7rem] border-green-400 text-green-400 flex-shrink-0"
|
||||
>
|
||||
{t('plugins.fromLocal')}
|
||||
</Badge>
|
||||
@@ -100,20 +106,9 @@ export default function PluginCardComponent({
|
||||
{cardVO.install_source === 'marketplace' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-purple-400 text-purple-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
getCloudServiceClientSync().getPluginMarketplaceURL(
|
||||
cardVO.author,
|
||||
cardVO.name,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
className="text-[0.7rem] border-purple-400 text-purple-400 flex-shrink-0"
|
||||
>
|
||||
{t('plugins.fromMarketplace')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
@@ -121,14 +116,26 @@ export default function PluginCardComponent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2 dark:text-[#999]">
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2 dark:text-[#999] w-full">
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
{/* Components list - fixed at bottom */}
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem] flex-shrink-0 min-h-[1.5rem]">
|
||||
<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}
|
||||
@@ -137,13 +144,25 @@ export default function PluginCardComponent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-between h-full">
|
||||
{/* Menu button - fixed width and position */}
|
||||
<div className="flex flex-col items-center justify-between h-full relative z-20 flex-shrink-0">
|
||||
<div className="flex items-center justify-center"></div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<DropdownMenu
|
||||
open={dropdownOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDropdownOpen(open);
|
||||
if (!open) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||
>
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -162,8 +181,33 @@ export default function PluginCardComponent({
|
||||
<span>{t('plugins.update')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/**view source */}
|
||||
{(cardVO.install_source === 'github' ||
|
||||
cardVO.install_source === 'marketplace') && (
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (cardVO.install_source === 'github') {
|
||||
window.open(cardVO.install_info.github_url, '_blank');
|
||||
} else if (cardVO.install_source === 'marketplace') {
|
||||
window.open(
|
||||
getCloudServiceClientSync().getPluginMarketplaceURL(
|
||||
cardVO.author,
|
||||
cardVO.name,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
<span>{t('plugins.viewSource')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer text-red-600 focus:text-red-600"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteClick(cardVO);
|
||||
@@ -178,6 +222,45 @@ export default function PluginCardComponent({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover overlay with action buttons */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gray-100/55 dark:bg-black/35 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-200 z-10 ${
|
||||
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewReadme(cardVO);
|
||||
}}
|
||||
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
||||
isHovered
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-1 opacity-0'
|
||||
}`}
|
||||
style={{ transitionDelay: isHovered ? '10ms' : '0ms' }}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
{t('plugins.readme')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCardClick();
|
||||
}}
|
||||
variant="outline"
|
||||
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
||||
isHovered
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-1 opacity-0'
|
||||
}`}
|
||||
style={{ transitionDelay: isHovered ? '20ms' : '0ms' }}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
{t('plugins.config')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
@@ -173,7 +184,7 @@ export default function PluginForm({
|
||||
itemConfigList={pluginInfo.manifest.manifest.spec.config}
|
||||
initialValues={pluginConfig.config as Record<string, object>}
|
||||
onSubmit={(values) => {
|
||||
// 只保存表单值的引用,不触发状态更新
|
||||
// 只保存表单值的引用,不触发状态更新
|
||||
currentFormValues.current = values;
|
||||
}}
|
||||
onFileUploaded={(fileKey) => {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import { getAPILanguageCode } from '@/i18n/I18nProvider';
|
||||
import './github-markdown.css';
|
||||
|
||||
export default function PluginReadme({
|
||||
pluginAuthor,
|
||||
pluginName,
|
||||
}: {
|
||||
pluginAuthor: string;
|
||||
pluginName: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [readme, setReadme] = useState<string>('');
|
||||
const [isLoadingReadme, setIsLoadingReadme] = useState(false);
|
||||
|
||||
const language = getAPILanguageCode();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch plugin README
|
||||
setIsLoadingReadme(true);
|
||||
httpClient
|
||||
.getPluginReadme(pluginAuthor, pluginName, language)
|
||||
.then((res) => {
|
||||
setReadme(res.readme);
|
||||
})
|
||||
.catch(() => {
|
||||
setReadme('');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingReadme(false);
|
||||
});
|
||||
}, [pluginAuthor, pluginName]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-auto">
|
||||
{isLoadingReadme ? (
|
||||
<div className="p-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('plugins.loadingReadme')}
|
||||
</div>
|
||||
) : readme ? (
|
||||
<div className="markdown-body p-6 max-w-none pt-0">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
rehypeHighlight,
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
behavior: 'wrap',
|
||||
properties: {
|
||||
className: ['anchor'],
|
||||
},
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal">{children}</ol>
|
||||
),
|
||||
li: ({ children }) => <li className="ml-4">{children}</li>,
|
||||
img: ({ src, alt, ...props }) => {
|
||||
let imageSrc = src || '';
|
||||
|
||||
if (typeof imageSrc !== 'string') {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className="max-w-full h-auto rounded-lg my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
imageSrc &&
|
||||
!imageSrc.startsWith('http://') &&
|
||||
!imageSrc.startsWith('https://') &&
|
||||
!imageSrc.startsWith('data:')
|
||||
) {
|
||||
imageSrc = imageSrc.replace(/^(\.\/|\/)+/, '');
|
||||
|
||||
if (!imageSrc.startsWith('assets/')) {
|
||||
imageSrc = `assets/${imageSrc}`;
|
||||
}
|
||||
|
||||
const assetPath = imageSrc.replace(/^assets\//, '');
|
||||
imageSrc = httpClient.getPluginAssetURL(
|
||||
pluginAuthor,
|
||||
pluginName,
|
||||
assetPath,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt || ''}
|
||||
className="max-w-lg h-auto my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('plugins.noReadme')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/* GitHub-style Markdown CSS */
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
color: var(--color-fg-default);
|
||||
background-color: transparent;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans',
|
||||
Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Hide light theme highlight.js styles in dark mode */
|
||||
.dark .markdown-body .hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure code blocks have proper styling */
|
||||
.markdown-body pre code.hljs {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.markdown-body .octicon {
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.markdown-body h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-body h6 {
|
||||
font-size: 0.85em;
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
margin: 0 0 16px 0;
|
||||
padding: 0 1em;
|
||||
color: var(--color-fg-muted);
|
||||
border-left: 0.25em solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.markdown-body ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-body li + li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-body li > p {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body li > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body li > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Nested lists */
|
||||
.markdown-body ul ul,
|
||||
.markdown-body ul ol,
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ol ul {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body ul ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
.markdown-body ul ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: var(--color-neutral-muted);
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
|
||||
'Liberation Mono', monospace;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: var(--color-accent-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
background-color: transparent;
|
||||
border-top: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: var(--color-canvas-subtle);
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
}
|
||||
|
||||
.markdown-body img {
|
||||
max-width: 50%;
|
||||
box-sizing: content-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-body hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: var(--color-border-default);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Light theme colors */
|
||||
.markdown-body {
|
||||
--color-fg-default: #1f2328;
|
||||
--color-fg-muted: #656d76;
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: #d8dee4;
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||
--color-accent-fg: #0969da;
|
||||
}
|
||||
|
||||
/* Dark theme colors */
|
||||
.dark .markdown-body {
|
||||
--color-fg-default: #e6edf3;
|
||||
--color-fg-muted: #8d96a0;
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-accent-fg: #4493f8;
|
||||
}
|
||||
|
||||
/* Code highlighting styles */
|
||||
.markdown-body .hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--color-fg-default);
|
||||
}
|
||||
|
||||
/* Light theme syntax highlighting */
|
||||
.markdown-body .hljs-comment,
|
||||
.markdown-body .hljs-quote {
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-keyword,
|
||||
.markdown-body .hljs-selector-tag,
|
||||
.markdown-body .hljs-subst {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-number,
|
||||
.markdown-body .hljs-literal,
|
||||
.markdown-body .hljs-variable,
|
||||
.markdown-body .hljs-template-variable,
|
||||
.markdown-body .hljs-tag .hljs-attr {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-string,
|
||||
.markdown-body .hljs-doctag {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-title,
|
||||
.markdown-body .hljs-section,
|
||||
.markdown-body .hljs-selector-id {
|
||||
color: #6f42c1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-type,
|
||||
.markdown-body .hljs-class .hljs-title {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-tag,
|
||||
.markdown-body .hljs-name,
|
||||
.markdown-body .hljs-attribute {
|
||||
color: #22863a;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-regexp,
|
||||
.markdown-body .hljs-link {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-symbol,
|
||||
.markdown-body .hljs-bullet {
|
||||
color: #e36209;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-built_in,
|
||||
.markdown-body .hljs-builtin-name {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-meta {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-deletion {
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-addition {
|
||||
background-color: #e6ffed;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-body .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Dark theme syntax highlighting */
|
||||
.dark .markdown-body .hljs-comment,
|
||||
.dark .markdown-body .hljs-quote {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-keyword,
|
||||
.dark .markdown-body .hljs-selector-tag,
|
||||
.dark .markdown-body .hljs-subst {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-number,
|
||||
.dark .markdown-body .hljs-literal,
|
||||
.dark .markdown-body .hljs-variable,
|
||||
.dark .markdown-body .hljs-template-variable,
|
||||
.dark .markdown-body .hljs-tag .hljs-attr {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-string,
|
||||
.dark .markdown-body .hljs-doctag {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-title,
|
||||
.dark .markdown-body .hljs-section,
|
||||
.dark .markdown-body .hljs-selector-id {
|
||||
color: #d2a8ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-type,
|
||||
.dark .markdown-body .hljs-class .hljs-title {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-tag,
|
||||
.dark .markdown-body .hljs-name,
|
||||
.dark .markdown-body .hljs-attribute {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-regexp,
|
||||
.dark .markdown-body .hljs-link {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-symbol,
|
||||
.dark .markdown-body .hljs-bullet {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-built_in,
|
||||
.dark .markdown-body .hljs-builtin-name {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-meta {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-deletion {
|
||||
background-color: rgba(248, 81, 73, 0.15);
|
||||
}
|
||||
|
||||
.dark .markdown-body .hljs-addition {
|
||||
background-color: rgba(46, 160, 67, 0.15);
|
||||
}
|
||||
@@ -10,10 +10,10 @@ 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';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
@@ -38,6 +38,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);
|
||||
@@ -46,15 +47,6 @@ function MarketPageContent({
|
||||
const [total, setTotal] = useState(0);
|
||||
const [sortOption, setSortOption] = useState('install_count_desc');
|
||||
|
||||
// Plugin detail dialog state
|
||||
const [selectedPluginAuthor, setSelectedPluginAuthor] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedPluginName, setSelectedPluginName] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const pageSize = 16; // 每页16个,4行x4列
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -111,6 +103,7 @@ function MarketPageContent({
|
||||
),
|
||||
githubURL: plugin.repository,
|
||||
version: plugin.latest_version,
|
||||
components: plugin.components,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -124,25 +117,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 +155,14 @@ function MarketPageContent({
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort],
|
||||
[
|
||||
searchQuery,
|
||||
componentFilter,
|
||||
pageSize,
|
||||
transformToVO,
|
||||
plugins.length,
|
||||
getCurrentSort,
|
||||
],
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
@@ -212,38 +207,59 @@ 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参数,检查是否需要打开插件详情对话框
|
||||
// 处理URL参数,重定向到 LangBot Space
|
||||
useEffect(() => {
|
||||
const author = searchParams.get('author');
|
||||
const pluginName = searchParams.get('plugin');
|
||||
|
||||
if (author && pluginName) {
|
||||
setSelectedPluginAuthor(author);
|
||||
setSelectedPluginName(pluginName);
|
||||
setDialogOpen(true);
|
||||
const detailUrl = `https://space.langbot.app/market/${author}/${pluginName}`;
|
||||
window.open(detailUrl, '_blank');
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// 插件详情对话框处理函数
|
||||
const handlePluginClick = useCallback(
|
||||
(author: string, pluginName: string) => {
|
||||
setSelectedPluginAuthor(author);
|
||||
setSelectedPluginName(pluginName);
|
||||
setDialogOpen(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
// 处理安装插件
|
||||
const handleInstallPlugin = useCallback(
|
||||
async (author: string, pluginName: string) => {
|
||||
try {
|
||||
// Find the full plugin object from the list
|
||||
const pluginVO = plugins.find(
|
||||
(p) => p.author === author && p.pluginName === pluginName,
|
||||
);
|
||||
if (!pluginVO) {
|
||||
console.error('Plugin not found:', author, pluginName);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleDialogClose = useCallback(() => {
|
||||
setDialogOpen(false);
|
||||
setSelectedPluginAuthor(null);
|
||||
setSelectedPluginName(null);
|
||||
}, []);
|
||||
// Fetch full plugin details to get PluginV4 object
|
||||
const response = await getCloudServiceClientSync().getPluginDetail(
|
||||
author,
|
||||
pluginName,
|
||||
);
|
||||
const pluginV4: PluginV4 = response.plugin;
|
||||
|
||||
// Call the install function passed from parent
|
||||
installPlugin(pluginV4);
|
||||
} catch (error) {
|
||||
console.error('Failed to install plugin:', error);
|
||||
toast.error(t('market.installFailed'));
|
||||
}
|
||||
},
|
||||
[plugins, installPlugin, t],
|
||||
);
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
@@ -342,9 +358,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>
|
||||
@@ -396,7 +462,7 @@ function MarketPageContent({
|
||||
<PluginMarketCardComponent
|
||||
key={plugin.pluginId}
|
||||
cardVO={plugin}
|
||||
onPluginClick={handlePluginClick}
|
||||
onInstall={handleInstallPlugin}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -413,20 +479,20 @@ function MarketPageContent({
|
||||
{!hasMore && plugins.length > 0 && (
|
||||
<div className="text-center text-muted-foreground py-6">
|
||||
{t('market.allLoaded')}
|
||||
{' · '}
|
||||
<a
|
||||
href="https://github.com/langbot-app/langbot-plugin-demo/issues/new?template=plugin-request.yml"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{t('market.requestPlugin')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plugin detail dialog */}
|
||||
<PluginDetailDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={handleDialogClose}
|
||||
author={selectedPluginAuthor}
|
||||
pluginName={selectedPluginName}
|
||||
installPlugin={installPlugin}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Download, Users } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
|
||||
interface PluginDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
author: string | null;
|
||||
pluginName: string | null;
|
||||
installPlugin: (plugin: PluginV4) => void;
|
||||
}
|
||||
|
||||
export default function PluginDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
author,
|
||||
pluginName,
|
||||
installPlugin,
|
||||
}: PluginDetailDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [plugin, setPlugin] = useState<PluginV4 | null>(null);
|
||||
const [readme, setReadme] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingReadme, setIsLoadingReadme] = useState(false);
|
||||
|
||||
// 获取插件详情和README
|
||||
useEffect(() => {
|
||||
if (open && author && pluginName) {
|
||||
fetchPluginData();
|
||||
}
|
||||
}, [open, author, pluginName]);
|
||||
|
||||
const fetchPluginData = async () => {
|
||||
if (!author || !pluginName) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 获取插件详情
|
||||
const detailResponse = await getCloudServiceClientSync().getPluginDetail(
|
||||
author,
|
||||
pluginName,
|
||||
);
|
||||
setPlugin(detailResponse.plugin);
|
||||
|
||||
// 获取README
|
||||
setIsLoadingReadme(true);
|
||||
try {
|
||||
const readmeResponse =
|
||||
await getCloudServiceClientSync().getPluginREADME(author, pluginName);
|
||||
setReadme(readmeResponse.readme);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load README:', error);
|
||||
setReadme(t('market.noReadme'));
|
||||
} finally {
|
||||
setIsLoadingReadme(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch plugin details:', error);
|
||||
toast.error(t('market.loadFailed'));
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const PluginHeader = () => (
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<img
|
||||
src={getCloudServiceClientSync().getPluginIconURL(author!, pluginName!)}
|
||||
alt={plugin!.name}
|
||||
className="w-16 h-16 rounded-xl border bg-gray-50 object-cover flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2 dark:text-white">
|
||||
{extractI18nObject(plugin!.label) || plugin!.name}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-3 dark:text-gray-400">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>
|
||||
{plugin!.author} / {plugin!.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline" className="dark:bg-gray-800 dark:text-white">
|
||||
v{plugin!.latest_version}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="flex items-center gap-1 dark:bg-gray-800 dark:text-white"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
|
||||
</Badge>
|
||||
{plugin!.repository && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(plugin!.repository, '_blank');
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z" />
|
||||
</svg>
|
||||
GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PluginDescription = () => (
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 leading-relaxed text-base dark:text-gray-400">
|
||||
{extractI18nObject(plugin!.description) || t('market.noDescription')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PluginOptions = () => (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={() => installPlugin(plugin!)}
|
||||
className="w-full h-12 text-base font-medium"
|
||||
>
|
||||
<Download className="w-5 h-5 mr-2" />
|
||||
{t('market.install')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ReadmeContent = () => (
|
||||
<div className="prose prose-sm max-w-none text-gray-800 dark:text-gray-400">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// 表格组件
|
||||
table: ({ ...props }) => (
|
||||
<div className="my-6 w-full overflow-x-auto rounded-lg">
|
||||
<table
|
||||
className="w-full border-collapse bg-white dark:bg-gray-800"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
thead: ({ ...props }) => (
|
||||
<thead
|
||||
className="bg-gray-50 dark:bg-gray-900 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
tbody: ({ ...props }) => (
|
||||
<tbody
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
th: ({ ...props }) => (
|
||||
<th
|
||||
className="px-4 py-3 text-left text-sm font-semibold text-gray-900 border-r border-gray-200 last:border-r-0 dark:border-gray-700 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ ...props }) => (
|
||||
<td
|
||||
className="px-4 py-3 text-sm text-gray-700 border-r border-gray-200 last:border-r-0 dark:border-gray-700 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
tr: ({ ...props }) => (
|
||||
<tr
|
||||
className="hover:bg-gray-50 transition-colors dark:hover:bg-gray-800 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// 删除线支持
|
||||
del: ({ ...props }) => (
|
||||
<del
|
||||
className="text-gray-500 line-through dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// Todo 列表支持
|
||||
input: ({ type, checked, ...props }) => {
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled
|
||||
className="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-default dark:border-gray-700"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <input type={type} {...props} />;
|
||||
},
|
||||
ul: ({ ...props }) => (
|
||||
<ul className="list-disc ml-5 dark:text-gray-400" {...props} />
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol className="list-decimal ml-5 dark:text-gray-400" {...props} />
|
||||
),
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
h1: ({ ...props }) => (
|
||||
<h1
|
||||
className="text-3xl font-bold my-2 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2
|
||||
className="text-2xl font-semibold mb-2 mt-4 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3
|
||||
className="text-xl font-semibold mb-2 mt-4 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h4: ({ ...props }) => (
|
||||
<h4
|
||||
className="text-lg font-semibold mb-2 mt-4 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h5: ({ ...props }) => (
|
||||
<h5
|
||||
className="text-base font-semibold mb-2 mt-4 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h6: ({ ...props }) => (
|
||||
<h6
|
||||
className="text-sm font-semibold mb-2 mt-4 dark:text-gray-400"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
p: ({ ...props }) => (
|
||||
<p className="leading-relaxed dark:text-gray-400" {...props} />
|
||||
),
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isCodeBlock = match ? true : false;
|
||||
|
||||
// 如果是代码块(有语言标识),由 pre 标签处理样式,淡灰色底,黑色字
|
||||
if (isCodeBlock) {
|
||||
return (
|
||||
<code
|
||||
className="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono dark:bg-gray-800 dark:text-gray-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
// 内联代码样式 - 淡灰色底
|
||||
return (
|
||||
<code
|
||||
className="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono inline-block dark:bg-gray-800 dark:text-gray-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: ({ ...props }) => (
|
||||
<pre
|
||||
className="bg-gray-100 text-gray-800 rounded-lg my-4 shadow-sm max-h-[500px] relative dark:bg-gray-800 dark:text-gray-400"
|
||||
style={{
|
||||
// 内边距确保内容不被滚动条覆盖
|
||||
padding: '16px',
|
||||
// 保持代码不换行以启用横向滚动
|
||||
whiteSpace: 'pre',
|
||||
// 滚动设置
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
// 确保滚动条在内部
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
// 图片组件 - 转换本地路径为API路径
|
||||
img: ({ src, alt, ...props }) => {
|
||||
// 处理图片路径
|
||||
let imageSrc = src || '';
|
||||
|
||||
// 确保 src 是字符串类型
|
||||
if (typeof imageSrc !== 'string') {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className="max-w-full h-auto rounded-lg my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果是相对路径,转换为API路径
|
||||
if (
|
||||
imageSrc &&
|
||||
!imageSrc.startsWith('http://') &&
|
||||
!imageSrc.startsWith('https://') &&
|
||||
!imageSrc.startsWith('data:')
|
||||
) {
|
||||
// 移除开头的 ./ 或 / (支持多个前缀)
|
||||
imageSrc = imageSrc.replace(/^(\.\/|\/)+/, '');
|
||||
|
||||
// 如果路径以 assets/ 开头,直接使用
|
||||
// 否则假设它在 assets/ 目录下
|
||||
if (!imageSrc.startsWith('assets/')) {
|
||||
imageSrc = `assets/${imageSrc}`;
|
||||
}
|
||||
|
||||
// 移除 assets/ 前缀以构建API URL
|
||||
const assetPath = imageSrc.replace(/^assets\//, '');
|
||||
imageSrc = getCloudServiceClientSync().getPluginAssetURL(
|
||||
author!,
|
||||
pluginName!,
|
||||
assetPath,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt || ''}
|
||||
className="max-w-lg h-auto my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!min-w-[50vw] max-w-none max-h-[90vh] h-[90vh] p-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12 h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">{t('market.loading')}</span>
|
||||
</div>
|
||||
) : plugin ? (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 插件信息区域 */}
|
||||
<div className="flex-shrink-0 bg-white border-b m-4 pt-2 dark:bg-black">
|
||||
<div className="flex gap-6 p-2 px-4">
|
||||
<div className="flex-1">
|
||||
<PluginHeader />
|
||||
<PluginDescription />
|
||||
</div>
|
||||
<div className="w-40 pr-4 flex-shrink-0">
|
||||
<PluginOptions />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* README 区域 */}
|
||||
<div className="flex-1 overflow-hidden px-8">
|
||||
<div className="h-full bg-white overflow-y-auto pb-2 dark:bg-black">
|
||||
{isLoadingReadme ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-3 text-gray-600">
|
||||
{t('market.loading')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<ReadmeContent />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,58 @@
|
||||
import { PluginMarketCardVO } from './PluginMarketCardVO';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Wrench,
|
||||
AudioWaveform,
|
||||
Hash,
|
||||
Download,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function PluginMarketCardComponent({
|
||||
cardVO,
|
||||
onPluginClick,
|
||||
onInstall,
|
||||
}: {
|
||||
cardVO: PluginMarketCardVO;
|
||||
onPluginClick?: (author: string, pluginName: string) => void;
|
||||
onInstall?: (author: string, pluginName: string) => void;
|
||||
}) {
|
||||
function handleCardClick() {
|
||||
if (onPluginClick) {
|
||||
onPluginClick(cardVO.author, cardVO.pluginName);
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
function handleInstallClick(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (onInstall) {
|
||||
onInstall(cardVO.author, cardVO.pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewDetailsClick(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
const detailUrl = `https://space.langbot.app/market/${cardVO.author}/${cardVO.pluginName}`;
|
||||
window.open(detailUrl, '_blank');
|
||||
}
|
||||
|
||||
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]"
|
||||
onClick={handleCardClick}
|
||||
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] hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] transition-all duration-200 hover:scale-[1.005] dark:bg-[#1f1f22] relative"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<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,25 +94,76 @@ 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>
|
||||
|
||||
{/* Hover overlay with action buttons */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gray-100/55 dark:bg-black/35 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-200 ${
|
||||
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
onClick={handleInstallClick}
|
||||
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
||||
isHovered ? 'translate-y-0 opacity-100' : 'translate-y-1 opacity-0'
|
||||
}`}
|
||||
style={{ transitionDelay: isHovered ? '10ms' : '0ms' }}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{t('market.install')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleViewDetailsClick}
|
||||
variant="outline"
|
||||
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
||||
isHovered ? 'translate-y-0 opacity-100' : 'translate-y-1 opacity-0'
|
||||
}`}
|
||||
style={{ transitionDelay: isHovered ? '20ms' : '0ms' }}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{t('market.viewDetails')}
|
||||
</Button>
|
||||
</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;
|
||||
|
||||
@@ -483,6 +483,27 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.delete(`/api/v1/plugins/config-files/${fileKey}`);
|
||||
}
|
||||
|
||||
public getPluginReadme(
|
||||
author: string,
|
||||
name: string,
|
||||
language: string = 'en',
|
||||
): Promise<{ readme: string }> {
|
||||
return this.get(
|
||||
`/api/v1/plugins/${author}/${name}/readme?language=${language}`,
|
||||
);
|
||||
}
|
||||
|
||||
public getPluginAssetURL(
|
||||
author: string,
|
||||
name: string,
|
||||
filepath: string,
|
||||
): string {
|
||||
return (
|
||||
this.instance.defaults.baseURL +
|
||||
`/api/v1/plugins/${author}/${name}/assets/${filepath}`
|
||||
);
|
||||
}
|
||||
|
||||
public getPluginIconURL(author: string, name: string): string {
|
||||
if (this.instance.defaults.baseURL === '/') {
|
||||
const url = window.location.href;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -59,9 +61,11 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
public getPluginREADME(
|
||||
author: string,
|
||||
pluginName: string,
|
||||
language?: string,
|
||||
): Promise<{ readme: string }> {
|
||||
return this.get<{ readme: string }>(
|
||||
`/api/v1/marketplace/plugins/${author}/${pluginName}/resources/README`,
|
||||
language ? { language } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,6 +82,6 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
}
|
||||
|
||||
public getPluginMarketplaceURL(author: string, name: string): string {
|
||||
return `${this.baseURL}/market?author=${author}&plugin=${name}`;
|
||||
return `https://space.langbot.app/market/${author}/${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -37,3 +37,20 @@ export const extractI18nObject = (i18nObject: I18nObject): string => {
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
// 工具函数:将 i18n 语言代码转换为 API 语言代码
|
||||
// i18n 使用:zh-Hans, en-US, ja-JP
|
||||
// API 使用:zh_Hans, en, ja_JP
|
||||
export const getAPILanguageCode = (): string => {
|
||||
const language = i18n.language;
|
||||
// zh-Hans -> zh_Hans
|
||||
if (language === 'zh-Hans') return 'zh_Hans';
|
||||
// zh-Hant -> zh_Hant
|
||||
if (language === 'zh-Hant') return 'zh_Hant';
|
||||
// en-US -> en
|
||||
if (language === 'en-US') return 'en';
|
||||
// ja-JP -> ja_JP
|
||||
if (language === 'ja-JP') return 'ja_JP';
|
||||
// 默认返回 en
|
||||
return 'en';
|
||||
};
|
||||
|
||||
@@ -280,6 +280,11 @@ const enUS = {
|
||||
saveConfigSuccessDebugPlugin:
|
||||
'Configuration saved successfully, please manually restart the plugin',
|
||||
saveConfigError: 'Configuration save failed: ',
|
||||
config: 'Configuration',
|
||||
readme: 'Documentation',
|
||||
viewSource: 'View Source',
|
||||
loadingReadme: 'Loading documentation...',
|
||||
noReadme: 'This plugin does not provide README documentation',
|
||||
fileUpload: {
|
||||
tooLarge: 'File size exceeds 10MB limit',
|
||||
success: 'File uploaded successfully',
|
||||
@@ -351,6 +356,10 @@ const enUS = {
|
||||
markAsRead: 'Mark as Read',
|
||||
markAsReadSuccess: 'Marked as read',
|
||||
markAsReadFailed: 'Mark as read failed',
|
||||
filterByComponent: 'Component',
|
||||
allComponents: 'All Components',
|
||||
requestPlugin: 'Request Plugin',
|
||||
viewDetails: 'View Details',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -281,6 +281,11 @@ const jaJP = {
|
||||
saveConfigSuccessDebugPlugin:
|
||||
'設定を保存しました。手動でプラグインを再起動してください',
|
||||
saveConfigError: '設定の保存に失敗しました:',
|
||||
config: '設定',
|
||||
readme: 'ドキュメント',
|
||||
viewSource: 'ソースを表示',
|
||||
loadingReadme: 'ドキュメントを読み込み中...',
|
||||
noReadme: 'このプラグインはREADMEドキュメントを提供していません',
|
||||
fileUpload: {
|
||||
tooLarge: 'ファイルサイズが 10MB の制限を超えています',
|
||||
success: 'ファイルのアップロードに成功しました',
|
||||
@@ -353,6 +358,10 @@ const jaJP = {
|
||||
markAsRead: '既読',
|
||||
markAsReadSuccess: '既読に設定しました',
|
||||
markAsReadFailed: '既読に設定に失敗しました',
|
||||
filterByComponent: 'コンポーネント',
|
||||
allComponents: '全部コンポーネント',
|
||||
requestPlugin: 'プラグインをリクエスト',
|
||||
viewDetails: '詳細を表示',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -266,6 +266,11 @@ const zhHans = {
|
||||
saveConfigSuccessNormal: '保存配置成功',
|
||||
saveConfigSuccessDebugPlugin: '保存配置成功,请手动重启插件',
|
||||
saveConfigError: '保存配置失败:',
|
||||
config: '配置',
|
||||
readme: '文档',
|
||||
viewSource: '查看来源',
|
||||
loadingReadme: '正在加载文档...',
|
||||
noReadme: '该插件没有提供 README 文档',
|
||||
fileUpload: {
|
||||
tooLarge: '文件大小超过 10MB 限制',
|
||||
success: '文件上传成功',
|
||||
@@ -335,6 +340,10 @@ const zhHans = {
|
||||
markAsRead: '已读',
|
||||
markAsReadSuccess: '已标记为已读',
|
||||
markAsReadFailed: '标记为已读失败',
|
||||
filterByComponent: '组件',
|
||||
allComponents: '全部组件',
|
||||
requestPlugin: '请求插件',
|
||||
viewDetails: '查看详情',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
@@ -265,6 +265,11 @@ const zhHant = {
|
||||
saveConfigSuccessNormal: '儲存配置成功',
|
||||
saveConfigSuccessDebugPlugin: '儲存配置成功,請手動重啟插件',
|
||||
saveConfigError: '儲存配置失敗:',
|
||||
config: '配置',
|
||||
readme: '文件',
|
||||
viewSource: '查看來源',
|
||||
loadingReadme: '正在載入文件...',
|
||||
noReadme: '該插件沒有提供 README 文件',
|
||||
fileUpload: {
|
||||
tooLarge: '檔案大小超過 10MB 限制',
|
||||
success: '檔案上傳成功',
|
||||
@@ -333,6 +338,10 @@ const zhHant = {
|
||||
markAsRead: '已讀',
|
||||
markAsReadSuccess: '已標記為已讀',
|
||||
markAsReadFailed: '標記為已讀失敗',
|
||||
filterByComponent: '組件',
|
||||
allComponents: '全部組件',
|
||||
requestPlugin: '請求插件',
|
||||
viewDetails: '查看詳情',
|
||||
},
|
||||
mcp: {
|
||||
title: 'MCP',
|
||||
|
||||
10
web/src/i18next.d.ts
vendored
Normal file
10
web/src/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'react-i18next';
|
||||
|
||||
declare module 'react-i18next' {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: 'translation';
|
||||
resources: {
|
||||
translation: typeof import('./i18n/locales/zh-Hans').default;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user