Compare commits

...

25 Commits

Author SHA1 Message Date
Junyan Qin
bf98b82cf2 chore: release v4.0.7 2025-06-18 13:10:20 +08:00
Junyan Qin (Chin)
edd70b943d Update bug-report_en.yml 2025-06-18 09:48:42 +08:00
Junyan Qin
3cbc823085 doc: make en README as default 2025-06-17 22:51:51 +08:00
Sheldon.li
48becf2c51 refactor(ContentFilterStage): Add logic for handling empty messages (#1525)
-In the ContentFilterStage, logic for handling empty messages has been added to ensure that the pipeline continues to process even when the message is empty.
- This change enhances the robustness of content filtering, preventing potential issues caused by empty messages.
- This optimization was implemented to address the issue where, when someone is @ in a group chat and a message is sent without any content, the Source type messages in the message chain are lost.
2025-06-17 22:12:55 +08:00
devin-ai-integration[bot]
56c686cd5a feat: add Japanese (ja-JP) language support (#1537)
* feat: add Japanese (ja-JP) language support

- Add comprehensive Japanese translation file (ja-JP.ts)
- Update i18n configuration to include Japanese locale
- Add Japanese language option to login and register page dropdowns
- Implement Japanese language detection and switching logic
- Maintain fallback to en-US for missing translations in flexible components

Co-Authored-By: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>

* perf: ui for ja-JP

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
2025-06-16 21:30:57 +08:00
Junyan Qin (Chin)
208273c0dd Update README.md 2025-06-16 21:01:11 +08:00
fdc310
2ff7ca3025 feat:add file url and add onebotv11(napcat) send file and seve file in local. (#1533)
* feat:add file url and add onebotv11(napcat) send file and seve file in local.

* del print
2025-06-15 17:22:35 +08:00
fdc310
61a2361730 feat:add new messagetyps WeChatFile and add wechat file is accepted and transmitted in base64 format. (#1531) 2025-06-15 17:17:08 +08:00
Junyan Qin
f80f997a89 chore: update version field in pyproject.toml 2025-06-11 10:24:18 +08:00
Junyan Qin
18529a42c1 chore: release v4.0.6 2025-06-11 10:23:46 +08:00
Junyan Qin (Chin)
3e707b4b6e feat: reset all associated session after bot and pipeline modified (#1517) 2025-06-09 21:50:08 +08:00
Junyan Qin
62f0a938a8 chore: remove legacy test in fe 2025-06-09 17:56:37 +08:00
Junyan Qin
ad3a163d82 fix: ruff linter error in libs 2025-06-09 17:56:21 +08:00
Junyan Qin
f5a4503610 perf: add text comment on bot log button 2025-06-09 15:27:17 +08:00
Junyan Qin
ec012cf5ed doc: update README 2025-06-09 10:20:11 +08:00
Junyan Qin
d70eceb72c fix(DebugDialog): \n not supported 2025-06-08 21:41:44 +08:00
devin-ai-integration[bot]
f271608114 feat: add dynamic base URL configuration using environment variables (#1511)
- Replace hardcoded base URL in HttpClient.ts with environment variable support
- Add NEXT_PUBLIC_API_BASE_URL environment variable for dynamic configuration
- Add dev:local script for development with localhost:5300 backend
- Development: uses localhost:5300, Production: uses / (relative path)
- Eliminates need for manual code changes when switching environments

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
2025-06-08 17:44:40 +08:00
Junyan Qin
793f0a9c10 fix: base url 2025-06-08 17:34:32 +08:00
devin-ai-integration[bot]
4f2ec195fc feat: add WebChat adapter for pipeline debugging (#1510)
* feat: add WebChat adapter for pipeline debugging

- Create WebChatAdapter for handling debug messages in pipeline testing
- Add HTTP API endpoints for debug message sending and retrieval
- Implement frontend debug dialog with session switching (private/group chat)
- Add Chinese i18n translations for debug interface
- Auto-create default WebChat bot during database initialization
- Support fixed session IDs: webchatperson and webchatgroup for testing

Co-Authored-By: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>

* perf: ui for webchat

* feat: complete webchat backend

* feat: core chat apis

* perf: button style in pipeline card

* perf: log btn in bot card

* perf: webchat entities definition

* fix: bugs

* perf: web chat

* perf: dialog styles

* perf: styles

* perf: styles

* fix: group invalid in webchat

* perf: simulate real im message

* perf: group timeout toast

* feat(webchat): add supports for mentioning bot in group

* perf(webchat): at component styles

* perf: at badge display in message

* fix: linter errors

* fix: webchat was listed on adapter list

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
2025-06-08 15:34:26 +08:00
Junyan Qin (Chin)
e6bc009414 feat: add i18n support for initialization page and fix plugin loading text (#1505)
* feat: add i18n support for initialization page and fix plugin loading text

- Add language selector to register/initialization page with Chinese and English options
- Add register section translations to both zh-Hans.ts and en-US.ts
- Replace hardcoded Chinese texts in register page with i18n translation calls
- Fix hardcoded '加载中...' text in plugin configuration dialog to use t('plugins.loading')
- Follow existing login page pattern for language selector implementation
- Maintain consistent UI/UX design with proper language switching functionality

Co-Authored-By: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>

* perf: language selecting logic

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
2025-06-06 21:29:36 +08:00
Junyan Qin
20dc8fb5ab perf: language selecting logic 2025-06-06 21:27:08 +08:00
Devin AI
9a71edfeb0 feat: add i18n support for initialization page and fix plugin loading text
- Add language selector to register/initialization page with Chinese and English options
- Add register section translations to both zh-Hans.ts and en-US.ts
- Replace hardcoded Chinese texts in register page with i18n translation calls
- Fix hardcoded '加载中...' text in plugin configuration dialog to use t('plugins.loading')
- Follow existing login page pattern for language selector implementation
- Maintain consistent UI/UX design with proper language switching functionality

Co-Authored-By: Junyan Qin <Chin>, 秦骏言 in Chinese, you can call me my english name Rock Chin. <rockchinq@gmail.com>
2025-06-06 10:50:31 +00:00
Guanchao Wang
fe3fd664af Fix/slack image (#1501)
* fix: dingtalk adapters couldn't handle images

* fix: slack adapter couldn't put the image in logger
2025-06-06 10:04:00 +08:00
Guanchao Wang
6402755ac6 fix: dingtalk adapters couldn't handle images (#1500) 2025-06-05 23:37:58 +08:00
Junyan Qin
ac8fe049de fix: uv removes it self 2025-06-05 11:12:04 +08:00
54 changed files with 2128 additions and 375 deletions

View File

@@ -19,7 +19,7 @@ body:
- type: textarea
attributes:
label: Reproduction steps
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
validations:
required: false
- type: textarea

143
README.md
View File

@@ -1,4 +1,3 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
@@ -8,42 +7,39 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/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>
<a href="https://langbot.app">Home</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/RockChinQ/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">Submit Plugin</a>
<div align="center">
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
[简体中文](README_CN.md) / [English](README.md) / [日本語](README_JP.md) / (PR for your language)
</div>
</p>
> 近期 GeWeChat 项目归档,我们已经适配 WeChatPad 协议端,个微恢复正常使用,详情请查看文档。
## ✨ Features
## ✨ 特性
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). 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.
- 😻 [New] Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
## 📦 Getting Started
## 📦 开始使用
#### Docker Compose 部署
#### Docker Compose Deployment
```bash
git clone https://github.com/RockChinQ/LangBot
@@ -51,106 +47,97 @@ cd LangBot
docker compose up -d
```
访问 http://localhost:5300 即可开始使用。
Visit http://localhost:5300 to start using it.
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
#### 宝塔面板部署
#### One-click Deployment on BTPanel
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
#### Zeabur 云部署
#### Zeabur Cloud Deployment
社区贡献的 Zeabur 模板。
Community contributed Zeabur template.
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railway 云部署
#### Railway Cloud Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手动部署
#### Other Deployment Methods
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
## 📸 效果展示
## 📸 Demo
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意仅展示webui效果公开环境请不要在其中填入您的任何敏感信息。
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
- Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
## 🔌 组件兼容性
## 🔌 Component Compatibility
### 消息平台
### Message Platform
| 平台 | 状态 | 备注 |
| Platform | Status | Remarks |
| --- | --- | --- |
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 企微对外客服 | ✅ | |
| 个人微信 | ✅ | |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| Personal WeChat | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 正在开发中
🚧: In development
### 大模型能力
### LLMs
| 模型 | 状态 | 备注 |
| LLM | Status | Remarks |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
### TTS
## 🤝 Community Contribution
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## 😘 社区贡献
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
## 😎 Stay Ahead
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
![star gif](https://docs.langbot.app/star.gif)

160
README_CN.md Normal file
View File

@@ -0,0 +1,160 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/RockChinQ/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 align="center">
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
[简体中文](README_CN.md) / [English](README.md) / [日本語](README_JP.md) / (PR for your language)
</div>
</p>
## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
## 📦 开始使用
#### Docker Compose 部署
```bash
git clone https://github.com/RockChinQ/LangBot
cd LangBot
docker compose up -d
```
访问 http://localhost:5300 即可开始使用。
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
#### 宝塔面板部署
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 云部署
社区贡献的 Zeabur 模板。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 云部署
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手动部署
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
## 📸 效果展示
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="450px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意仅展示webui效果公开环境请不要在其中填入您的任何敏感信息。
## 🔌 组件兼容性
### 消息平台
| 平台 | 状态 | 备注 |
| --- | --- | --- |
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
| 企业微信 | ✅ | |
| 企微对外客服 | ✅ | |
| 个人微信 | ✅ | |
| 微信公众号 | ✅ | |
| 飞书 | ✅ | |
| 钉钉 | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 正在开发中
### 大模型能力
| 模型 | 状态 | 备注 |
| --- | --- | --- |
| [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/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
### TTS
| 平台/模型 | 备注 |
| --- | --- |
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
### 文生图
| 平台/模型 | 备注 |
| --- | --- |
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
## 😘 社区贡献
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
## 😎 保持更新
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
![star gif](https://docs.langbot.app/star.gif)

View File

@@ -1,137 +0,0 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://langbot.app">Home</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/RockChinQ/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">Submit Plugin</a>
<div align="center">
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
</div>
</p>
## ✨ Features
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). 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.
- 😻 [New] Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
## 📦 Getting Started
#### Docker Compose Deployment
```bash
git clone https://github.com/RockChinQ/LangBot
cd LangBot
docker compose up -d
```
Visit http://localhost:5300 to start using it.
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
#### One-click Deployment on BTPanel
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
#### Zeabur Cloud Deployment
Community contributed Zeabur template.
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/en-US/templates/ZKTBDH)
#### Railway Cloud Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### Other Deployment Methods
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
## 📸 Demo
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
- WebUI Demo: https://demo.langbot.dev/
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
- Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
## 🔌 Component Compatibility
### Message Platform
| Platform | Status | Remarks |
| --- | --- | --- |
| Personal QQ | ✅ | |
| QQ Official API | ✅ | |
| WeCom | ✅ | |
| WeComCS | ✅ | |
| Personal WeChat | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: In development
### LLMs
| LLM | Status | Remarks |
| --- | --- | --- |
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
| [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/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
## 🤝 Community Contribution
Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>

View File

@@ -23,7 +23,7 @@
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
[简体中文](README_CN.md) / [English](README.md) / [日本語](README_JP.md) / (PR for your language)
</div>
@@ -134,3 +134,9 @@ LangBot への貢献に対して、以下の [コード貢献者](https://github
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a>
## 😎 最新情報を入手
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
![star gif](https://docs.langbot.app/star.gif)

View File

@@ -1,4 +1,4 @@
from v1 import client
from v1 import client # type: ignore
import asyncio
@@ -8,19 +8,13 @@ import json
class TestDifyClient:
async def test_chat_messages(self):
cln = client.AsyncDifyServiceClient(
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
)
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
async for chunk in cln.chat_messages(
inputs={}, query='调用工具查看现在几点?', user='test'
):
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
print(json.dumps(chunk, ensure_ascii=False, indent=4))
async def test_upload_file(self):
cln = client.AsyncDifyServiceClient(
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
)
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
file_bytes = open('img.png', 'rb').read()
@@ -32,9 +26,7 @@ class TestDifyClient:
print(json.dumps(resp, ensure_ascii=False, indent=4))
async def test_workflow_run(self):
cln = client.AsyncDifyServiceClient(
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
)
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
# resp = await cln.workflow_run(inputs={}, user="test")
# # print(json.dumps(resp, ensure_ascii=False, indent=4))

View File

@@ -1,5 +1,5 @@
import asyncio
import dingtalk_stream
import dingtalk_stream # type: ignore
from dingtalk_stream import AckMessage
@@ -27,9 +27,3 @@ class EchoTextHandler(dingtalk_stream.ChatbotHandler):
await asyncio.sleep(0.1) # 异步等待,避免阻塞
return self.incoming_message
async def get_dingtalk_client(client_id, client_secret):
from api import DingTalkClient # 延迟导入,避免循环导入
return DingTalkClient(client_id, client_secret)

View File

@@ -2,7 +2,7 @@ import base64
import json
import time
from typing import Callable
import dingtalk_stream
import dingtalk_stream # type: ignore
from .EchoHandler import EchoTextHandler
from .dingtalkevent import DingTalkEvent
import httpx
@@ -49,8 +49,8 @@ class DingTalkClient:
self.access_token = response_data.get('accessToken')
expires_in = int(response_data.get('expireIn', 7200))
self.access_token_expiry_time = time.time() + expires_in - 60
except Exception as e:
await self.logger.error("failed to get access token in dingtalk")
except Exception:
await self.logger.error('failed to get access token in dingtalk')
async def is_token_expired(self):
"""检查token是否过期"""
@@ -75,7 +75,7 @@ class DingTalkClient:
result = response.json()
download_url = result.get('downloadUrl')
else:
await self.logger.error(f"failed to get download url: {response.json()}")
await self.logger.error(f'failed to get download url: {response.json()}')
if download_url:
return await self.download_url_to_base64(download_url)
@@ -86,10 +86,11 @@ class DingTalkClient:
if response.status_code == 200:
file_bytes = response.content
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str
mime_type = response.headers.get('Content-Type', 'application/octet-stream')
base64_str = base64.b64encode(file_bytes).decode('utf-8')
return f'data:{mime_type};base64,{base64_str}'
else:
await self.logger.error(f"failed to get files: {response.json()}")
await self.logger.error(f'failed to get files: {response.json()}')
async def get_audio_url(self, download_code: str):
if not await self.check_access_token():
@@ -105,7 +106,7 @@ class DingTalkClient:
if download_url:
return await self.download_url_to_base64(download_url)
else:
await self.logger.error(f"failed to get audio: {response.json()}")
await self.logger.error(f'failed to get audio: {response.json()}')
else:
raise Exception(f'Error: {response.status_code}, {response.text}')
@@ -117,12 +118,12 @@ class DingTalkClient:
if event:
await self._handle_message(event)
async def send_message(self, content: str, incoming_message,at:bool):
async def send_message(self, content: str, incoming_message, at: bool):
if self.markdown_card:
if at:
self.EchoTextHandler.reply_markdown(
title='@'+incoming_message.sender_nick+' '+content,
text='@'+incoming_message.sender_nick+' '+content,
title='@' + incoming_message.sender_nick + ' ' + content,
text='@' + incoming_message.sender_nick + ' ' + content,
incoming_message=incoming_message,
)
else:
@@ -192,9 +193,9 @@ class DingTalkClient:
copy_message_data = message_data.copy()
del copy_message_data['IncomingMessage']
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
except Exception as e:
except Exception:
if self.logger:
await self.logger.error(f"Error in get_message: {traceback.format_exc()}")
await self.logger.error(f'Error in get_message: {traceback.format_exc()}')
else:
traceback.print_exc()
@@ -223,8 +224,8 @@ class DingTalkClient:
if response.status_code == 200:
return
except Exception:
await self.logger.error(f"failed to send proactive massage to person: {traceback.format_exc()}")
raise Exception(f"failed to send proactive massage to person: {traceback.format_exc()}")
await self.logger.error(f'failed to send proactive massage to person: {traceback.format_exc()}')
raise Exception(f'failed to send proactive massage to person: {traceback.format_exc()}')
async def send_proactive_message_to_group(self, target_id: str, content: str):
if not await self.check_access_token():
@@ -249,8 +250,8 @@ class DingTalkClient:
if response.status_code == 200:
return
except Exception:
await self.logger.error(f"failed to send proactive massage to group: {traceback.format_exc()}")
raise Exception(f"failed to send proactive massage to group: {traceback.format_exc()}")
await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}')
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
async def start(self):
"""启动 WebSocket 连接,监听消息"""

View File

@@ -1,5 +1,5 @@
from typing import Dict, Any, Optional
import dingtalk_stream
import dingtalk_stream # type: ignore
class DingTalkEvent(dict):

View File

@@ -1,7 +1,7 @@
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
import time
import traceback
from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
from libs.wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
import xml.etree.ElementTree as ET
from quart import Quart, request
import hashlib
@@ -55,7 +55,7 @@ class OAClient:
echostr = request.args.get('echostr', '')
msg_signature = request.args.get('msg_signature', '')
if msg_signature is None:
await self.logger.error(f'msg_signature不在请求体中')
await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中')
if request.method == 'GET':
@@ -66,7 +66,7 @@ class OAClient:
if check_signature == signature:
return echostr # 验证成功返回echostr
else:
await self.logger.error(f'拒绝请求')
await self.logger.error('拒绝请求')
raise Exception('拒绝请求')
elif request.method == 'POST':
encryt_msg = await request.data
@@ -75,9 +75,9 @@ class OAClient:
xml_msg = xml_msg.decode('utf-8')
if ret != 0:
await self.logger.error(f'消息解密失败')
await self.logger.error('消息解密失败')
raise Exception('消息解密失败')
message_data = await self.get_message(xml_msg)
if message_data:
event = OAEvent.from_payload(message_data)
@@ -214,7 +214,7 @@ class OAClientForLongerResponse:
msg_signature = request.args.get('msg_signature', '')
if msg_signature is None:
await self.logger.error(f'msg_signature不在请求体中')
await self.logger.error('msg_signature不在请求体中')
raise Exception('msg_signature不在请求体中')
if request.method == 'GET':
@@ -229,9 +229,8 @@ class OAClientForLongerResponse:
xml_msg = xml_msg.decode('utf-8')
if ret != 0:
await self.logger.error(f'消息解密失败')
await self.logger.error('消息解密失败')
raise Exception('消息解密失败')
# 解析 XML
root = ET.fromstring(xml_msg)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import quart
from .. import group
from ... import group
@group.group_class('pipelines', '/api/v1/pipelines')

View File

@@ -0,0 +1,79 @@
import quart
from ... import group
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
class WebChatDebugRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/send', methods=['POST'])
async def send_message(pipeline_uuid: str) -> str:
"""发送调试消息到流水线"""
try:
data = await quart.request.get_json()
session_type = data.get('session_type', 'person')
message_chain_obj = data.get('message', [])
if not message_chain_obj:
return self.http_status(400, -1, 'message is required')
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj)
return self.success(
data={
'message': result,
}
)
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/messages/<session_type>', methods=['GET'])
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
"""获取调试消息历史"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
return self.success(data={'messages': messages})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/reset/<session_type>', methods=['POST'])
async def reset_session(session_type: str) -> str:
"""重置调试会话"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
webchat_adapter = None
for bot in self.ap.platform_mgr.bots:
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
webchat_adapter = bot.adapter
break
if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found')
webchat_adapter.reset_debug_session(session_type)
return self.success(data={'message': 'Session reset successfully'})
except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}')

View File

@@ -13,10 +13,12 @@ from . import groups
from . import group
from .groups import provider as groups_provider
from .groups import platform as groups_platform
from .groups import pipelines as groups_pipelines
importutil.import_modules_in_pkg(groups)
importutil.import_modules_in_pkg(groups_provider)
importutil.import_modules_in_pkg(groups_platform)
importutil.import_modules_in_pkg(groups_pipelines)
class HTTPController:

View File

@@ -93,6 +93,11 @@ class BotService:
if runtime_bot.enable:
await runtime_bot.run()
# update all conversation that use this bot
for session in self.ap.sess_mgr.session_list:
if session.using_conversation is not None and session.using_conversation.bot_uuid == bot_uuid:
session.using_conversation = None
async def delete_bot(self, bot_uuid: str) -> None:
"""删除机器人"""
await self.ap.platform_mgr.remove_bot(bot_uuid)

View File

@@ -112,6 +112,11 @@ class PipelineService:
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
# update all conversation that use this pipeline
for session in self.ap.sess_mgr.session_list:
if session.using_conversation is not None and session.using_conversation.pipeline_uuid == pipeline_uuid:
session.using_conversation = None
async def delete_pipeline(self, pipeline_uuid: str) -> None:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(

View File

@@ -137,6 +137,12 @@ class Conversation(pydantic.BaseModel):
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
pipeline_uuid: str
"""流水线UUID。"""
bot_uuid: str
"""机器人UUID。"""
uuid: typing.Optional[str] = None
"""该对话的 uuid在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""

View File

@@ -66,13 +66,15 @@ class PersistenceManager:
# write default pipeline
result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline))
default_pipeline_uuid = None
if result.first() is None:
self.ap.logger.info('Creating default pipeline...')
pipeline_config = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
default_pipeline_uuid = str(uuid.uuid4())
pipeline_data = {
'uuid': str(uuid.uuid4()),
'uuid': default_pipeline_uuid,
'for_version': self.ap.ver_mgr.get_current_version(),
'stages': pipeline_service.default_stage_order,
'is_default': True,
@@ -82,6 +84,7 @@ class PersistenceManager:
}
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
# =================================
# run migrations

View File

@@ -66,6 +66,8 @@ class ContentFilterStage(stage.PipelineStage):
if query.pipeline_config['safety']['content-filter']['scope'] == 'output-msg':
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
if not message.strip():
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
else:
for filter in self.filter_chain:
if filter_entities.EnableStage.PRE in filter.enable_stages:

View File

@@ -51,11 +51,10 @@ class Controller:
# find pipeline
# Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one.
# Like aiocqhttp, once a client is connected, even the adapter was updated and restarted, the existing client connection will not be affected.
bot = await self.ap.platform_mgr.get_bot_by_uuid(selected_query.bot_uuid)
if bot:
pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(
bot.bot_entity.use_pipeline_uuid
)
pipeline_uuid = selected_query.pipeline_uuid
if pipeline_uuid:
pipeline = await self.ap.pipeline_mgr.get_pipeline_by_uuid(pipeline_uuid)
if pipeline:
await pipeline.run(selected_query)

View File

@@ -35,6 +35,7 @@ class QueryPool:
message_event: platform_events.MessageEvent,
message_chain: platform_message.MessageChain,
adapter: msadapter.MessagePlatformAdapter,
pipeline_uuid: typing.Optional[str] = None,
) -> entities.Query:
async with self.condition:
query = entities.Query(
@@ -48,6 +49,7 @@ class QueryPool:
resp_messages=[],
resp_message_chain=[],
adapter=adapter,
pipeline_uuid=pipeline_uuid,
)
self.queries.append(query)
self.query_id_counter += 1

View File

@@ -45,6 +45,8 @@ class PreProcessor(stage.PipelineStage):
query,
session,
query.pipeline_config['ai']['local-agent']['prompt'],
query.pipeline_uuid,
query.bot_uuid,
)
conversation.use_llm_model = llm_model

View File

@@ -5,7 +5,6 @@ import asyncio
import traceback
import sqlalchemy
# FriendMessage, Image, MessageChain, Plain
from . import adapter as msadapter
@@ -78,6 +77,7 @@ class RuntimeBot:
message_event=event,
message_chain=event.message_chain,
adapter=adapter,
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
)
async def on_group_message(
@@ -102,6 +102,7 @@ class RuntimeBot:
message_event=event,
message_chain=event.message_chain,
adapter=adapter,
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
)
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
@@ -144,6 +145,8 @@ class PlatformManager:
bots: list[RuntimeBot]
webchat_proxy_bot: RuntimeBot
adapter_components: list[engine.Component]
adapter_dict: dict[str, type[msadapter.MessagePlatformAdapter]]
@@ -161,6 +164,31 @@ class PlatformManager:
adapter_dict[component.metadata.name] = component.get_python_component_class()
self.adapter_dict = adapter_dict
webchat_adapter_class = self.adapter_dict['webchat']
# initialize webchat adapter
webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap)
webchat_adapter_inst = webchat_adapter_class(
{},
self.ap,
webchat_logger,
)
self.webchat_proxy_bot = RuntimeBot(
ap=self.ap,
bot_entity=persistence_bot.Bot(
uuid='webchat-proxy-bot',
name='WebChat',
description='',
adapter='webchat',
adapter_config={},
enable=True,
),
adapter=webchat_adapter_inst,
logger=webchat_logger,
)
await self.webchat_proxy_bot.initialize()
await self.load_bots_from_db()
def get_running_adapters(self) -> list[msadapter.MessagePlatformAdapter]:
@@ -220,7 +248,9 @@ class PlatformManager:
return
def get_available_adapters_info(self) -> list[dict]:
return [component.to_plain_dict() for component in self.adapter_components]
return [
component.to_plain_dict() for component in self.adapter_components if component.metadata.name != 'webchat'
]
def get_available_adapter_info_by_name(self, name: str) -> dict | None:
for component in self.adapter_components:
@@ -273,6 +303,8 @@ class PlatformManager:
async def run(self):
# This method will only be called when the application launching
await self.webchat_proxy_bot.run()
for bot in self.bots:
if bot.enable:
await bot.run()

View File

@@ -61,6 +61,9 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif type(msg) is platform_message.Forward:
for node in msg.node_list:
msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0])
elif isinstance(msg, platform_message.File):
msg_list.append({"type":"file", "data":{'file': msg.url, "name": msg.name}})
else:
msg_list.append(aiocqhttp.MessageSegment.text(str(msg)))
@@ -69,6 +72,7 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
@staticmethod
async def target2yiri(message: str, message_id: int = -1,bot=None):
# print(message)
message = aiocqhttp.Message(message)
async def process_message_data(msg_data, reply_list):
@@ -134,6 +138,18 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
reply_msg = platform_message.Quote(message_id=msg.data["id"],sender_id=msg_datas["user_id"],origin=reply_list)
yiri_msg_list.append(reply_msg)
elif msg.type == 'file':
# file_name = msg.data['file']
file_id = msg.data['file_id']
file_data = await bot.get_file(file_id=file_id)
file_name = file_data.get('file_name')
file_path = file_data.get('file')
file_url = file_data.get('file_url')
file_size = file_data.get('file_size')
yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size))
@@ -154,9 +170,11 @@ class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod
async def target2yiri(event: aiocqhttp.Event,bot=None):
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id,bot)
if event.message_type == 'group':
permission = 'MEMBER'
@@ -235,6 +253,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
if target_type == 'group':
await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg)
elif target_type == 'person':
await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg)

View File

@@ -0,0 +1,209 @@
import asyncio
import logging
import typing
from datetime import datetime
from pydantic import BaseModel
from .. import adapter as msadapter
from ..types import events as platform_events, message as platform_message, entities as platform_entities
from ...core import app
from ..logger import EventLogger
logger = logging.getLogger(__name__)
class WebChatMessage(BaseModel):
id: int
role: str
content: str
message_chain: list[dict]
timestamp: str
class WebChatSession:
id: str
message_lists: dict[str, list[WebChatMessage]] = {}
resp_waiters: dict[int, asyncio.Future[WebChatMessage]]
def __init__(self, id: str):
self.id = id
self.message_lists = {}
self.resp_waiters = {}
def get_message_list(self, pipeline_uuid: str) -> list[WebChatMessage]:
if pipeline_uuid not in self.message_lists:
self.message_lists[pipeline_uuid] = []
return self.message_lists[pipeline_uuid]
class WebChatAdapter(msadapter.MessagePlatformAdapter):
"""WebChat调试适配器用于流水线调试"""
webchat_person_session: WebChatSession
webchat_group_session: WebChatSession
listeners: typing.Dict[
typing.Type[platform_events.Event],
typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], None],
] = {}
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
self.ap = ap
self.logger = logger
self.config = config
self.webchat_person_session = WebChatSession(id='webchatperson')
self.webchat_group_session = WebChatSession(id='webchatgroup')
self.bot_account_id = 'webchatbot'
async def send_message(
self,
target_type: str,
target_id: str,
message: platform_message.MessageChain,
) -> dict:
"""发送消息到调试会话"""
session_key = target_id
if session_key not in self.debug_messages:
self.debug_messages[session_key] = []
message_data = {
'id': len(self.debug_messages[session_key]) + 1,
'type': 'bot',
'content': str(message),
'timestamp': datetime.now().isoformat(),
'message_chain': [component.__dict__ for component in message],
}
self.debug_messages[session_key].append(message_data)
await self.logger.info(f'Send message to {session_key}: {message}')
return message_data
async def reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
quote_origin: bool = False,
) -> dict:
"""回复消息"""
message_data = WebChatMessage(
id=-1,
role='assistant',
content=str(message),
message_chain=[component.__dict__ for component in message],
timestamp=datetime.now().isoformat(),
)
# notify waiter
if isinstance(message_source, platform_events.FriendMessage):
self.webchat_person_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data)
elif isinstance(message_source, platform_events.GroupMessage):
self.webchat_group_session.resp_waiters[message_source.message_chain.message_id].set_result(message_data)
return message_data.model_dump()
def register_listener(
self,
event_type: typing.Type[platform_events.Event],
func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]],
):
"""注册事件监听器"""
self.listeners[event_type] = func
def unregister_listener(
self,
event_type: typing.Type[platform_events.Event],
func: typing.Callable[[platform_events.Event, msadapter.MessagePlatformAdapter], typing.Awaitable[None]],
):
"""取消注册事件监听器"""
del self.listeners[event_type]
async def run_async(self):
"""运行适配器"""
await self.logger.info('WebChat调试适配器已启动')
try:
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
await self.logger.info('WebChat调试适配器已停止')
raise
async def kill(self):
"""停止适配器"""
await self.logger.info('WebChat调试适配器正在停止')
async def send_webchat_message(
self, pipeline_uuid: str, session_type: str, message_chain_obj: typing.List[dict]
) -> dict:
"""发送调试消息到流水线"""
if session_type == 'person':
use_session = self.webchat_person_session
else:
use_session = self.webchat_group_session
message_chain = platform_message.MessageChain.parse_obj(message_chain_obj)
message_id = len(use_session.get_message_list(pipeline_uuid)) + 1
use_session.get_message_list(pipeline_uuid).append(
WebChatMessage(
id=message_id,
role='user',
content=str(message_chain),
message_chain=message_chain_obj,
timestamp=datetime.now().isoformat(),
)
)
message_chain.insert(0, platform_message.Source(id=message_id, time=datetime.now().timestamp()))
if session_type == 'person':
sender = platform_entities.Friend(id='webchatperson', nickname='User')
event = platform_events.FriendMessage(
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
)
else:
group = platform_entities.Group(
id='webchatgroup', name='Group', permission=platform_entities.Permission.Member
)
sender = platform_entities.GroupMember(
id='webchatperson',
member_name='User',
group=group,
permission=platform_entities.Permission.Member,
)
event = platform_events.GroupMessage(
sender=sender, message_chain=message_chain, time=datetime.now().timestamp()
)
self.ap.platform_mgr.webchat_proxy_bot.bot_entity.use_pipeline_uuid = pipeline_uuid
if event.__class__ in self.listeners:
await self.listeners[event.__class__](event, self)
# set waiter
waiter = asyncio.Future[WebChatMessage]()
use_session.resp_waiters[message_id] = waiter
waiter.add_done_callback(lambda future: use_session.resp_waiters.pop(message_id))
resp_message = await waiter
resp_message.id = len(use_session.get_message_list(pipeline_uuid)) + 1
use_session.get_message_list(pipeline_uuid).append(resp_message)
return resp_message.model_dump()
def get_webchat_messages(self, pipeline_uuid: str, session_type: str) -> list[dict]:
"""获取调试消息历史"""
if session_type == 'person':
return [message.model_dump() for message in self.webchat_person_session.get_message_list(pipeline_uuid)]
else:
return [message.model_dump() for message in self.webchat_group_session.get_message_list(pipeline_uuid)]

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: MessagePlatformAdapter
metadata:
name: webchat
label:
en_US: "WebChat Debug"
zh_Hans: "网页聊天调试"
description:
en_US: "WebChat adapter for pipeline debugging"
zh_Hans: "用于流水线调试的网页聊天适配器"
icon: ""
spec: {}
execution:
python:
path: "webchat.py"
attr: "WebChatAdapter"

View File

@@ -235,6 +235,7 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
'57': self._handler_compound_quote,
'5': self._handler_compound_link,
'6': self._handler_compound_file,
'74': self._handler_compound_file,
'33': self._handler_compound_mini_program,
'36': self._handler_compound_mini_program,
'2000': partial(self._handler_compound_unsupported, text="[转账消息]"),
@@ -320,10 +321,41 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理文件消息 (data_type=6)"""
xml_data_str = ET.tostring(xml_data, encoding='unicode')
return platform_message.MessageChain([
platform_message.WeChatForwardFile(xml_data=xml_data_str)
])
file_data = xml_data.find('.//appmsg')
if file_data.findtext('.//type', "") == "74":
return None
else:
xml_data_str = ET.tostring(xml_data, encoding='unicode')
# print(xml_data_str)
# 提取img标签的属性
# print(xml_data)
file_name = file_data.find('title').text
file_id = file_data.find('md5').text
# file_szie = file_data.find('totallen')
# print(file_data)
if file_data is not None:
aeskey = xml_data.findtext('.//appattach/aeskey')
cdnthumburl = xml_data.findtext('.//appattach/cdnattachurl')
# cdnmidimgurl = img_tag.get('cdnmidimgurl')
# print(aeskey,cdnthumburl)
file_data = self.bot.cdn_download(aeskey=aeskey, file_type=5, file_url=cdnthumburl)
file_base64 = file_data["Data"]['FileData']
# print(file_data)
file_size = file_data["Data"]['TotalSize']
# print(file_base64)
return platform_message.MessageChain([
platform_message.WeChatFile(file_id=file_id, file_name=file_name, file_size=file_size,
file_base64=file_base64),
platform_message.WeChatForwardFile(xml_data=xml_data_str)
])
async def _handler_compound_link(
self,

View File

@@ -800,12 +800,14 @@ class File(MessageComponent):
type: str = 'File'
"""消息组件类型。"""
id: str
id: str = ''
"""文件识别 ID。"""
name: str
"""文件名称。"""
size: int
size: int = ''
"""文件大小。"""
url: str
"""文件路径"""
def __str__(self):
return f'[文件]{self.name}'
@@ -922,3 +924,22 @@ class WeChatForwardQuote(MessageComponent):
def __str__(self):
return self.app_msg
class WeChatFile(MessageComponent):
"""文件。"""
type: str = 'File'
"""消息组件类型。"""
file_id: str = ''
"""文件识别 ID。"""
file_name: str = ''
"""文件名称。"""
file_size: int = ''
"""文件大小。"""
file_path: str = ''
"""文件地址"""
file_base64: str = ''
"""base64"""
def __str__(self):
return f'[文件]{self.file_name}'

View File

@@ -41,6 +41,8 @@ class SessionManager:
query: core_entities.Query,
session: core_entities.Session,
prompt_config: list[dict],
pipeline_uuid: str,
bot_uuid: str,
) -> core_entities.Conversation:
"""获取对话或创建对话"""
@@ -58,13 +60,15 @@ class SessionManager:
messages=prompt_messages,
)
if session.using_conversation is None:
if session.using_conversation is None or session.using_conversation.pipeline_uuid != pipeline_uuid:
conversation = core_entities.Conversation(
prompt=prompt,
messages=[],
use_funcs=await self.ap.tool_mgr.get_all_functions(
plugin_enabled=True,
),
pipeline_uuid=pipeline_uuid,
bot_uuid=bot_uuid,
)
session.conversations.append(conversation)
session.using_conversation = conversation

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.0.5'
semantic_version = 'v4.0.7'
required_database_version = 3
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""

View File

@@ -204,7 +204,9 @@ async def get_slack_image_to_base64(pic_url: str, bot_token: str):
try:
async with aiohttp.ClientSession() as session:
async with session.get(pic_url, headers=headers) as resp:
image_data = await resp.read()
return base64.b64encode(image_data).decode('utf-8')
mime_type = resp.headers.get("Content-Type", "application/octet-stream")
file_bytes = await resp.read()
base64_str = base64.b64encode(file_bytes).decode("utf-8")
return f"data:{mime_type};base64,{base64_str}"
except Exception as e:
raise (e)
raise (e)

View File

@@ -1,6 +1,6 @@
[project]
name = "langbot"
version = "4.0.3"
version = "4.0.7"
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
readme = "README.md"
requires-python = ">=3.10.1"
@@ -48,6 +48,7 @@ dependencies = [
"pip>=25.1.1",
"ruff>=0.11.9",
"pre-commit>=4.2.0",
"uv>=0.7.11",
]
keywords = [
"bot",

283
web/package-lock.json generated
View File

@@ -15,6 +15,8 @@
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
@@ -1255,6 +1257,215 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz",
@@ -1389,6 +1600,78 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
"integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz",

View File

@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev:local": "NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
@@ -23,6 +24,8 @@
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",

View File

@@ -4,6 +4,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { Switch } from '@/components/ui/switch';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
export default function BotCard({
botCardVO,
@@ -92,25 +93,25 @@ export default function BotCard({
e.stopPropagation();
}}
/>
<div
className={`${styles.botLogsIcon}`}
<Button
variant="outline"
className="w-auto h-[40px]"
onClick={(e) => {
onClickLogIcon();
e.stopPropagation();
}}
>
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-[24px] h-[24px] z-10"
viewBox="0 0 24 24"
width="48"
height="48"
fill="currentColor"
>
<path
d="M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9ZM8 7H11V9H8V7ZM8 11H16V13H8V11ZM8 15H16V17H8V15Z"
fill="#9A9A9A"
/>
<path d="M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9ZM8 7H11V9H8V7ZM8 11H16V13H8V11ZM8 15H16V17H8V15Z"></path>
</svg>
</div>
{t('bots.log')}
</Button>
</div>
</div>
</div>

View File

@@ -1,41 +0,0 @@
import {
DynamicFormItemType,
IDynamicFormItemSchema,
} from '@/app/infra/entities/form/dynamic';
import { DynamicFormItemConfig } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
export const testDynamicConfigList: IDynamicFormItemSchema[] = [
new DynamicFormItemConfig({
default: '',
id: '111',
label: {
zh_Hans: '测试字段string',
en_US: 'eng test',
},
name: 'string_test',
required: false,
type: DynamicFormItemType.STRING,
}),
new DynamicFormItemConfig({
default: '',
id: '222',
label: {
zh_Hans: '测试字段int',
en_US: 'int eng test',
},
name: 'int_test',
required: true,
type: DynamicFormItemType.INT,
}),
new DynamicFormItemConfig({
default: '',
id: '333',
label: {
zh_Hans: '测试字段boolean',
en_US: 'boolean eng test',
},
name: 'boolean_test',
required: false,
type: DynamicFormItemType.BOOLEAN,
}),
];

View File

@@ -65,7 +65,7 @@
}
.sidebarChildContainer {
width: 9rem;
width: 9.8rem;
height: 3rem;
padding-left: 1.6rem;
font-size: 1rem;

View File

@@ -1,9 +1,22 @@
import styles from './pipelineCard.module.css';
import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
export default function PipelineCard({
cardVO,
onDebug,
}: {
cardVO: PipelineCardVO;
onDebug: (pipelineId: string) => void;
}) {
const { t } = useTranslation();
const handleDebugClick = (e: React.MouseEvent) => {
e.stopPropagation();
onDebug(cardVO.id);
};
return (
<div className={`${styles.cardContainer}`}>
<div className={`${styles.basicInfoContainer}`}>
@@ -48,6 +61,22 @@ export default function PipelineCard({ cardVO }: { cardVO: PipelineCardVO }) {
</div>
</div>
)}
<Button
variant="outline"
onClick={handleDebugClick}
title={t('pipelines.chat')}
className="mt-auto"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={styles.debugButtonIcon}
>
<path d="M13 19.9C15.2822 19.4367 17 17.419 17 15V12C17 11.299 16.8564 10.6219 16.5846 10H7.41538C7.14358 10.6219 7 11.299 7 12V15C7 17.419 8.71776 19.4367 11 19.9V14H13V19.9ZM5.5358 17.6907C5.19061 16.8623 5 15.9534 5 15H2V13H5V12C5 11.3573 5.08661 10.7348 5.2488 10.1436L3.0359 8.86602L4.0359 7.13397L6.05636 8.30049C6.11995 8.19854 6.18609 8.09835 6.25469 8H17.7453C17.8139 8.09835 17.88 8.19854 17.9436 8.30049L19.9641 7.13397L20.9641 8.86602L18.7512 10.1436C18.9134 10.7348 19 11.3573 19 12V13H22V15H19C19 15.9534 18.8094 16.8623 18.4642 17.6907L20.9641 19.134L19.9641 20.866L17.4383 19.4077C16.1549 20.9893 14.1955 22 12 22C9.80453 22 7.84512 20.9893 6.56171 19.4077L4.0359 20.866L3.0359 19.134L5.5358 17.6907ZM8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6H8Z"></path>
</svg>
{t('pipelines.chat')}
</Button>
</div>
</div>
);

View File

@@ -67,9 +67,11 @@
.operationContainer {
display: flex;
flex-direction: row;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
gap: 0.5rem;
width: 5rem;
width: 8rem;
}
.operationDefaultBadge {
@@ -98,3 +100,8 @@
font-weight: bold;
max-width: 100%;
}
.debugButtonIcon {
width: 1.2rem;
height: 1.2rem;
}

View File

@@ -0,0 +1,31 @@
import { Badge } from '@/components/ui/badge';
import { X } from 'lucide-react';
interface AtBadgeProps {
targetName: string;
readonly?: boolean;
onRemove?: () => void;
}
export default function AtBadge({
targetName,
readonly = false,
onRemove,
}: AtBadgeProps) {
return (
<Badge
variant="secondary"
className="flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 text-blue-600 hover:bg-blue-200"
>
@{targetName}
{!readonly && onRemove && (
<button
onClick={onRemove}
className="ml-1 hover:text-blue-800 focus:outline-none"
>
<X className="h-3 w-3" />
</button>
)}
</Badge>
);
}

View File

@@ -0,0 +1,422 @@
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { httpClient } from '@/app/infra/http/HttpClient';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { Pipeline } from '@/app/infra/entities/api';
import { Message } from '@/app/infra/entities/message';
import { toast } from 'sonner';
import AtBadge from './AtBadge';
interface MessageComponent {
type: 'At' | 'Plain';
target?: string;
text?: string;
}
interface DebugDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
pipelineId: string;
}
export default function DebugDialog({
open,
onOpenChange,
pipelineId,
}: DebugDialogProps) {
const { t } = useTranslation();
const [selectedPipelineId, setSelectedPipelineId] = useState(pipelineId);
const [sessionType, setSessionType] = useState<'person' | 'group'>('person');
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
const [showAtPopover, setShowAtPopover] = useState(false);
const [hasAt, setHasAt] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (open) {
setSelectedPipelineId(pipelineId);
loadPipelines();
loadMessages(pipelineId);
}
}, [open, pipelineId]);
useEffect(() => {
if (open) {
loadMessages(selectedPipelineId);
}
}, [sessionType, selectedPipelineId]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
!inputRef.current?.contains(event.target as Node)
) {
setShowAtPopover(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
useEffect(() => {
if (showAtPopover) {
setIsHovering(true);
}
}, [showAtPopover]);
const loadPipelines = async () => {
try {
const response = await httpClient.getPipelines();
setPipelines(response.pipelines);
} catch (error) {
console.error('Failed to load pipelines:', error);
}
};
const loadMessages = async (pipelineId: string) => {
try {
const response = await httpClient.getWebChatHistoryMessages(
pipelineId,
sessionType,
);
setMessages(response.messages);
} catch (error) {
console.error('Failed to load messages:', error);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (sessionType === 'group') {
if (value.endsWith('@')) {
setShowAtPopover(true);
} else if (showAtPopover && (!value.includes('@') || value.length > 1)) {
setShowAtPopover(false);
}
}
setInputValue(value);
};
const handleAtSelect = () => {
setHasAt(true);
setShowAtPopover(false);
setInputValue(inputValue.slice(0, -1));
};
const handleAtRemove = () => {
setHasAt(false);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (showAtPopover) {
handleAtSelect();
} else {
sendMessage();
}
} else if (e.key === 'Backspace' && hasAt && inputValue === '') {
handleAtRemove();
}
};
const sendMessage = async () => {
if (!inputValue.trim() && !hasAt) return;
try {
const messageChain = [];
let text_content = inputValue.trim();
if (hasAt) {
text_content = ' ' + text_content;
}
if (hasAt) {
messageChain.push({
type: 'At',
target: 'webchatbot',
});
}
messageChain.push({
type: 'Plain',
text: text_content,
});
if (hasAt) {
// for showing
text_content = '@webchatbot' + text_content;
}
const userMessage: Message = {
id: -1,
role: 'user',
content: text_content,
timestamp: new Date().toISOString(),
message_chain: messageChain,
};
setMessages((prevMessages) => [...prevMessages, userMessage]);
setInputValue('');
setHasAt(false);
const response = await httpClient.sendWebChatMessage(
sessionType,
messageChain,
selectedPipelineId,
120000,
);
setMessages((prevMessages) => [...prevMessages, response.message]);
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
) {
console.log(error, 'type of error', typeof error);
console.error('Failed to send message:', error);
if (!error.message.includes('timeout') && sessionType === 'person') {
toast.error(t('pipelines.debugDialog.sendFailed'));
}
} finally {
inputRef.current?.focus();
}
};
// const resetSession = async () => {
// try {
// await httpClient.resetWebChatSession(selectedPipelineId, sessionType);
// setMessages([]);
// } catch (error) {
// console.error('Failed to reset session:', error);
// }
// };
const renderMessageContent = (message: Message) => {
return (
<span className="text-base leading-relaxed align-middle whitespace-pre-wrap">
{(message.message_chain as MessageComponent[]).map(
(component, index) => {
if (component.type === 'At') {
return (
<AtBadge
key={index}
targetName={component.target || ''}
readonly={true}
/>
);
} else if (component.type === 'Plain') {
return <span key={index}>{component.text}</span>;
}
return null;
},
)}
</span>
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white">
<DialogHeader className="pl-2">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-4 font-bold">
{t('pipelines.debugDialog.title')}
<Select
value={selectedPipelineId}
onValueChange={(value) => {
setSelectedPipelineId(value);
loadMessages(value);
}}
>
<SelectTrigger className="bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-xl shadow-lg">
{pipelines.map((pipeline) => (
<SelectItem
key={pipeline.uuid}
value={pipeline.uuid || ''}
className="rounded-lg"
>
{pipeline.name}
</SelectItem>
))}
</SelectContent>
</Select>
</DialogTitle>
</div>
</DialogHeader>
<div className="flex flex-1 h-full min-h-0 border-t">
<div className="w-50 bg-white border-r p-6 pl-0 rounded-l-2xl flex-shrink-0 flex flex-col justify-start gap-4">
<div className="flex flex-col gap-2">
<Button
variant="ghost"
className={`w-full justify-center rounded-md px-4 py-6 text-base font-medium transition-none ${
sessionType === 'person'
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
: 'bg-white text-gray-800 hover:bg-gray-100'
} border-0 shadow-none`}
onClick={() => setSessionType('person')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 22C4 17.5817 7.58172 14 12 14C16.4183 14 20 17.5817 20 22H18C18 18.6863 15.3137 16 12 16C8.68629 16 6 18.6863 6 22H4ZM12 13C8.685 13 6 10.315 6 7C6 3.685 8.685 1 12 1C15.315 1 18 3.685 18 7C18 10.315 15.315 13 12 13ZM12 11C14.21 11 16 9.21 16 7C16 4.79 14.21 3 12 3C9.79 3 8 4.79 8 7C8 9.21 9.79 11 12 11Z"></path>
</svg>
{t('pipelines.debugDialog.privateChat')}
</Button>
<Button
variant="ghost"
className={`w-full justify-center rounded-md px-4 py-6 text-base font-medium transition-none ${
sessionType === 'group'
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
: 'bg-white text-gray-800 hover:bg-gray-100'
} border-0 shadow-none`}
onClick={() => setSessionType('group')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 22C2 17.5817 5.58172 14 10 14C14.4183 14 18 17.5817 18 22H16C16 18.6863 13.3137 16 10 16C6.68629 16 4 18.6863 4 22H2ZM10 13C6.685 13 4 10.315 4 7C4 3.685 6.685 1 10 1C13.315 1 16 3.685 16 7C16 10.315 13.315 13 10 13ZM10 11C12.21 11 14 9.21 14 7C14 4.79 12.21 3 10 3C7.79 3 6 4.79 6 7C6 9.21 7.79 11 10 11ZM18.2837 14.7028C21.0644 15.9561 23 18.752 23 22H21C21 19.564 19.5483 17.4671 17.4628 16.5271L18.2837 14.7028ZM17.5962 3.41321C19.5944 4.23703 21 6.20361 21 8.5C21 11.3702 18.8042 13.7252 16 13.9776V11.9646C17.6967 11.7222 19 10.264 19 8.5C19 7.11935 18.2016 5.92603 17.041 5.35635L17.5962 3.41321Z"></path>
</svg>
{t('pipelines.debugDialog.groupChat')}
</Button>
</div>
<div className="flex-1" />
</div>
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white">
<div className="space-y-6">
{messages.length === 0 ? (
<div className="text-center text-muted-foreground py-12 text-lg">
{t('pipelines.debugDialog.noMessages')}
</div>
) : (
messages.map((message) => (
<div
key={message.id + message.timestamp}
className={cn(
'flex',
message.role === 'user'
? 'justify-end'
: 'justify-start',
)}
>
<div
className={cn(
'max-w-md px-5 py-3 rounded-2xl',
message.role === 'user'
? 'bg-[#2288ee] text-white rounded-br-none'
: 'bg-gray-100 text-gray-900 rounded-bl-none',
)}
>
{renderMessageContent(message)}
<div
className={cn(
'text-xs mt-2',
message.role === 'user'
? 'text-white/70'
: 'text-gray-500',
)}
>
{message.role === 'user'
? t('pipelines.debugDialog.userMessage')
: t('pipelines.debugDialog.botMessage')}
</div>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
<div className="border-t p-4 pb-0 bg-white flex gap-2">
<div className="flex-1 flex items-center gap-2">
{hasAt && (
<AtBadge targetName="webchatbot" onRemove={handleAtRemove} />
)}
<div className="relative flex-1">
<Input
ref={inputRef}
value={inputValue}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
placeholder={t('pipelines.debugDialog.inputPlaceholder')}
className="flex-1 rounded-md px-3 py-2 border border-gray-300 focus:border-[#2288ee] transition-none text-base"
/>
{showAtPopover && (
<div
ref={popoverRef}
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-white shadow-lg"
>
<div
className={cn(
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
isHovering ? 'bg-gray-100' : 'bg-white',
)}
onClick={handleAtSelect}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<span>
@webchatbot - {t('pipelines.debugDialog.atTips')}
</span>
</div>
</div>
)}
</div>
</div>
<Button
onClick={sendMessage}
disabled={!inputValue.trim() && !hasAt}
className="rounded-md bg-[#2288ee] hover:bg-[#2288ee] w-20 text-white px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none"
>
<>{t('pipelines.debugDialog.send')}</>
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -15,6 +15,8 @@ import {
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import DebugDialog from './debug-dialog/DebugDialog';
export default function PluginConfigPage() {
const { t } = useTranslation();
const [modalOpen, setModalOpen] = useState<boolean>(false);
@@ -32,6 +34,8 @@ export default function PluginConfigPage() {
const [disableForm, setDisableForm] = useState(false);
const [selectedPipelineIsDefault, setSelectedPipelineIsDefault] =
useState(false);
const [debugDialogOpen, setDebugDialogOpen] = useState(false);
const [debugPipelineId, setDebugPipelineId] = useState('');
useEffect(() => {
getPipelines();
@@ -92,6 +96,11 @@ export default function PluginConfigPage() {
});
}
const handleDebug = (pipelineId: string) => {
setDebugPipelineId(pipelineId);
setDebugDialogOpen(true);
};
return (
<div className={styles.configPageContainer}>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
@@ -149,11 +158,17 @@ export default function PluginConfigPage() {
getSelectedPipelineForm(pipeline.id);
}}
>
<PipelineCard cardVO={pipeline} />
<PipelineCard cardVO={pipeline} onDebug={handleDebug} />
</div>
);
})}
</div>
<DebugDialog
open={debugDialogOpen}
onOpenChange={setDebugDialogOpen}
pipelineId={debugPipelineId}
/>
</div>
);
}

View File

@@ -72,7 +72,7 @@ export default function PluginForm({
};
if (!pluginInfo || !pluginConfig) {
return <div>...</div>;
return <div>{t('plugins.loading')}</div>;
}
function deletePlugin() {

View File

@@ -1,6 +1,7 @@
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
import { PipelineConfigTab } from '@/app/infra/entities/pipeline';
import { I18nLabel } from '@/app/infra/entities/common';
import { Message } from '@/app/infra/entities/message';
export interface ApiResponse<T> {
code: number;
@@ -228,3 +229,11 @@ export interface GetPipelineResponseData {
export interface GetPipelineMetadataResponseData {
configs: PipelineConfigTab[];
}
export interface ApiRespWebChatMessage {
message: Message;
}
export interface ApiRespWebChatMessages {
messages: Message[];
}

View File

@@ -0,0 +1,7 @@
export interface Message {
id: number;
role: 'user' | 'assistant';
content: string;
message_chain: object[];
timestamp: string;
}

View File

@@ -29,6 +29,8 @@ import {
GetPipelineResponseData,
GetPipelineMetadataResponseData,
AsyncTask,
ApiRespWebChatMessage,
ApiRespWebChatMessages,
} from '@/app/infra/entities/api';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
@@ -301,6 +303,43 @@ class HttpClient {
return this.delete(`/api/v1/pipelines/${uuid}`);
}
// ============ Debug WebChat API ============
public sendWebChatMessage(
sessionType: string,
messageChain: object[],
pipelineId: string,
timeout: number = 15000,
): Promise<ApiRespWebChatMessage> {
return this.post(
`/api/v1/pipelines/${pipelineId}/chat/send`,
{
session_type: sessionType,
message: messageChain,
},
{
timeout,
},
);
}
public getWebChatHistoryMessages(
pipelineId: string,
sessionType: string,
): Promise<ApiRespWebChatMessages> {
return this.get(
`/api/v1/pipelines/${pipelineId}/chat/messages/${sessionType}`,
);
}
public resetWebChatSession(
pipelineId: string,
sessionType: string,
): Promise<{ message: string }> {
return this.post(
`/api/v1/pipelines/${pipelineId}/chat/reset/${sessionType}`,
);
}
// ============ Platform API ============
public getAdapters(): Promise<ApiRespPlatformAdapters> {
return this.get('/api/v1/platform/adapters');
@@ -455,9 +494,15 @@ class HttpClient {
}
}
// export const httpClient = new HttpClient('https://event-log.langbot.dev');
// export const httpClient = new HttpClient('http://localhost:5300');
export const httpClient = new HttpClient('/');
const getBaseURL = (): string => {
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) {
return process.env.NEXT_PUBLIC_API_BASE_URL;
}
return '/';
};
export const httpClient = new HttpClient(getBaseURL());
// 临时写法未来两种Client都继承自HttpClient父类不允许共享方法
export const spaceClient = new HttpClient('https://space.langbot.app');

View File

@@ -61,19 +61,37 @@ export default function Login() {
}, []);
const judgeLanguage = () => {
// here's for user have never set the language
// judge the language by the browser
const language = navigator.language;
if (language) {
let lang = 'zh-Hans';
if (language === 'zh-CN') {
lang = 'zh-Hans';
} else {
lang = 'en-US';
}
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
setCurrentLanguage('zh-Hans');
localStorage.setItem('langbot_language', 'zh-Hans');
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
setCurrentLanguage('ja-JP');
localStorage.setItem('langbot_language', 'ja-JP');
} else {
setCurrentLanguage('en-US');
localStorage.setItem('langbot_language', 'en-US');
}
// check if the language is already set
const lang = localStorage.getItem('langbot_language');
if (lang) {
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
localStorage.setItem('langbot_language', lang);
return;
} else {
const language = navigator.language;
if (language) {
let lang = 'zh-Hans';
if (language === 'zh-CN') {
lang = 'zh-Hans';
} else if (language === 'ja' || language === 'ja-JP') {
lang = 'ja-JP';
} else {
lang = 'en-US';
}
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
localStorage.setItem('langbot_language', lang);
}
}
};
@@ -146,6 +164,7 @@ export default function Login() {
<SelectContent>
<SelectItem value="zh-Hans"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
<SelectItem value="ja-JP"></SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -8,6 +8,13 @@ import {
CardTitle,
CardDescription,
} from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
@@ -19,23 +26,28 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useRouter } from 'next/navigation';
import { Mail, Lock } from 'lucide-react';
import { Mail, Lock, Globe } from 'lucide-react';
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
const formSchema = z.object({
email: z.string().email('请输入有效的邮箱地址'),
password: z.string().min(1, '请输入密码'),
});
const formSchema = (t: (key: string) => string) =>
z.object({
email: z.string().email(t('common.invalidEmail')),
password: z.string().min(1, t('common.emptyPassword')),
});
export default function Register() {
const router = useRouter();
const { t } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
resolver: zodResolver(formSchema(t)),
defaultValues: {
email: '',
password: '',
@@ -43,9 +55,53 @@ export default function Register() {
});
useEffect(() => {
judgeLanguage();
getIsInitialized();
}, []);
const judgeLanguage = () => {
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
setCurrentLanguage('zh-Hans');
localStorage.setItem('langbot_language', 'zh-Hans');
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
setCurrentLanguage('ja-JP');
localStorage.setItem('langbot_language', 'ja-JP');
} else {
setCurrentLanguage('en-US');
localStorage.setItem('langbot_language', 'en-US');
}
// check if the language is already set
const lang = localStorage.getItem('langbot_language');
console.log('lang: ', lang);
if (lang) {
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
} else {
const language = navigator.language;
if (language) {
let lang = 'zh-Hans';
if (language === 'zh-CN') {
lang = 'zh-Hans';
} else if (language === 'ja' || language === 'ja-JP') {
lang = 'ja-JP';
} else {
lang = 'en-US';
}
console.log('language: ', lang);
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
localStorage.setItem('langbot_language', lang);
}
}
};
const handleLanguageChange = (value: string) => {
console.log('handleLanguageChange: ', value);
i18n.changeLanguage(value);
setCurrentLanguage(value);
localStorage.setItem('langbot_language', value);
};
function getIsInitialized() {
httpClient
.checkIfInited()
@@ -59,7 +115,7 @@ export default function Register() {
});
}
function onSubmit(values: z.infer<typeof formSchema>) {
function onSubmit(values: z.infer<ReturnType<typeof formSchema>>) {
handleRegister(values.email, values.password);
}
@@ -68,31 +124,47 @@ export default function Register() {
.initUser(username, password)
.then((res) => {
console.log('init user success: ', res);
toast.success('初始化成功 请登录');
toast.success(t('register.initSuccess'));
router.push('/login');
})
.catch((err) => {
console.log('init user error: ', err);
toast.error('初始化失败:' + err.message);
toast.error(t('register.initFailed') + err.message);
});
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<Card className="w-[360px]">
<Card className="w-[375px]">
<CardHeader>
<div className="flex justify-end mb-6">
<Select
value={currentLanguage}
onValueChange={handleLanguageChange}
>
<SelectTrigger className="w-[140px]">
<Globe className="h-4 w-4 mr-2" />
<SelectValue placeholder={t('common.language')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-Hans"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
<SelectItem value="ja-JP"></SelectItem>
</SelectContent>
</Select>
</div>
<img
src={langbotIcon.src}
alt="LangBot"
className="w-16 h-16 mb-4 mx-auto"
/>
<CardTitle className="text-2xl text-center">
LangBot 👋
{t('register.title')}
</CardTitle>
<CardDescription className="text-center">
LangBot
{t('register.description')}
<br />
{t('register.adminAccountNote')}
</CardDescription>
</CardHeader>
<CardContent>
@@ -103,12 +175,12 @@ export default function Register() {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('common.email')}</FormLabel>
<FormControl>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
placeholder="输入邮箱地址"
placeholder={t('common.enterEmail')}
className="pl-10"
{...field}
/>
@@ -124,13 +196,13 @@ export default function Register() {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('common.password')}</FormLabel>
<FormControl>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
type="password"
placeholder="输入密码"
placeholder={t('common.enterPassword')}
className="pl-10"
{...field}
/>
@@ -142,7 +214,7 @@ export default function Register() {
/>
<Button type="submit" className="w-full mt-4 cursor-pointer">
{t('register.register')}
</Button>
</form>
</Form>

View File

@@ -0,0 +1,48 @@
'use client';
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '@/lib/utils';
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,58 @@
'use client';
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '@/lib/utils';
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
'flex touch-none p-px transition-colors select-none',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -6,6 +6,7 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import enUS from './locales/en-US';
import zhHans from './locales/zh-Hans';
import jaJP from './locales/ja-JP';
i18n
.use(LanguageDetector)
@@ -18,6 +19,9 @@ i18n
'zh-Hans': {
translation: zhHans,
},
'ja-JP': {
translation: jaJP,
},
},
fallbackLng: 'zh-Hans',
debug: process.env.NODE_ENV === 'development',

View File

@@ -128,6 +128,7 @@ const enUS = {
earlier: 'Earlier',
dateFormat: '{{month}}/{{day}}',
setBotEnableError: 'Failed to set bot enable status',
log: 'Log',
},
plugins: {
title: 'Plugins',
@@ -183,6 +184,7 @@ const enUS = {
'Pipelines define the processing flow for message events, used to bind to bots',
createPipeline: 'Create Pipeline',
editPipeline: 'Edit Pipeline',
chat: 'Chat',
getPipelineListError: 'Failed to get pipeline list: ',
daysAgo: 'days ago',
today: 'Today',
@@ -202,6 +204,34 @@ const enUS = {
deleteConfirmation:
'Are you sure you want to delete this pipeline? Bots bound to this pipeline will not work.',
defaultPipelineCannotDelete: 'Default pipeline cannot be deleted',
debugDialog: {
title: 'Pipeline Chat',
selectPipeline: 'Select Pipeline',
sessionType: 'Session Type',
privateChat: 'Private Chat',
groupChat: 'Group Chat',
send: 'Send',
reset: 'Reset Conversation',
inputPlaceholder: 'Enter message...',
noMessages: 'No messages',
userMessage: 'User',
botMessage: 'Bot',
sendFailed: 'Send failed',
resetSuccess: 'Conversation reset successfully',
resetFailed: 'Reset failed',
loadMessagesFailed: 'Failed to load messages',
loadPipelinesFailed: 'Failed to load pipelines',
atTips: 'Mention the bot',
},
},
register: {
title: 'Initialize LangBot 👋',
description: 'This is your first time starting LangBot',
adminAccountNote:
'The email and password you fill in will be used as the initial administrator account',
register: 'Register',
initSuccess: 'Initialization successful, please login',
initFailed: 'Initialization failed: ',
},
};

View File

@@ -0,0 +1,239 @@
const jaJP = {
common: {
login: 'ログイン',
logout: 'ログアウト',
email: 'メールアドレス',
password: 'パスワード',
welcome: 'LangBot へおかえりなさい 👋',
continueToLogin: 'ログインしてください',
loginSuccess: 'ログインに成功しました',
loginFailed:
'ログインに失敗しました。メールアドレスまたはパスワードをご確認ください',
enterEmail: 'メールアドレスを入力',
enterPassword: 'パスワードを入力',
invalidEmail: '有効なメールアドレスを入力してください',
emptyPassword: 'パスワードを入力してください',
language: '言語',
helpDocs: 'ヘルプドキュメント',
create: '作成',
edit: '編集',
delete: '削除',
add: '追加',
select: '選択してください',
cancel: 'キャンセル',
submit: '送信',
error: 'エラー',
success: '成功',
save: '保存',
saving: '保存中...',
confirm: '確認',
confirmDelete: '削除の確認',
deleteConfirmation: '本当に削除しますか?',
selectOption: 'オプションを選択',
required: '必須',
enable: '有効にする',
name: '名前',
description: '説明',
close: '閉じる',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',
addRound: 'ラウンドを追加',
copySuccess: 'コピーに成功しました',
test: 'テスト',
},
notFound: {
title: 'ページが見つかりません',
description:
'お探しのページは存在しないようです。入力したURLが正しいか確認するか、ホームページに戻ってください。',
back: '戻る',
home: 'ホームに戻る',
help: 'ヘルプドキュメントを見る',
},
models: {
title: 'モデル設定',
description: 'パイプラインで使用できるモデルを設定・管理',
createModel: 'モデルを作成',
editModel: 'モデルを編集',
getModelListError: 'モデルリストの取得に失敗しました:',
modelName: 'モデル名',
modelProvider: 'モデルプロバイダー',
modelBaseURL: 'ベースURL',
modelAbilities: 'モデル機能',
saveSuccess: '保存に成功しました',
saveError: '保存に失敗しました:',
createSuccess: '作成に成功しました',
createError: '作成に失敗しました:',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',
deleteConfirmation: '本当にこのモデルを削除しますか?',
modelNameRequired: 'モデル名は必須です',
modelProviderRequired: 'モデルプロバイダーは必須です',
requestURLRequired: 'リクエストURLは必須です',
apiKeyRequired: 'APIキーは必須です',
keyNameRequired: 'キー名は必須です',
mustBeValidNumber: '有効な数値である必要があります',
mustBeTrueOrFalse: 'true または false である必要があります',
requestURL: 'リクエストURL',
apiKey: 'APIキー',
abilities: '機能',
selectModelAbilities: 'モデル機能を選択',
visionAbility: '視覚機能',
functionCallAbility: '関数呼び出し',
extraParameters: '追加パラメータ',
addParameter: 'パラメータを追加',
keyName: 'キー名',
type: 'タイプ',
value: '値',
string: '文字列',
number: '数値',
boolean: 'ブール値',
extraParametersDescription:
'リクエストボディに追加されるパラメータmax_tokens、temperature、top_p など)',
selectModelProvider: 'モデルプロバイダーを選択',
modelProviderDescription: 'プロバイダーが提供するモデル名をご入力ください',
selectModel: 'モデルを選択してください',
testSuccess: 'テストに成功しました',
testError: 'テストに失敗しました。モデル設定を確認してください',
},
bots: {
title: 'ボット',
description:
'ボットの作成と管理を行います。LangBotと各プラットフォームを接続するためのエントリーポイントです',
createBot: 'ボットを作成',
editBot: 'ボットを編集',
getBotListError: 'ボットリストの取得に失敗しました:',
botName: 'ボット名',
botDescription: 'ボットの説明',
botNameRequired: 'ボット名は必須です',
botDescriptionRequired: 'ボットの説明は必須です',
adapterRequired: 'アダプターは必須です',
defaultDescription: 'ボット',
getBotConfigError: 'ボット設定の取得に失敗しました:',
saveSuccess: '保存に成功しました',
saveError: '保存に失敗しました:',
createSuccess:
'作成が完了しました。有効化するか、パイプラインの設定を行ってください',
createError: '作成に失敗しました:',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',
deleteConfirmation: '本当にこのボットを削除しますか?',
platformAdapter: 'プラットフォーム/アダプター選択',
selectAdapter: 'アダプターを選択',
adapterConfig: 'アダプター設定',
bindPipeline: 'パイプラインを紐付け',
selectPipeline: 'パイプラインを選択',
botLogTitle: 'ボットログ',
enableAutoRefresh: '自動更新を有効にする',
session: 'セッション',
yesterday: '昨日',
earlier: 'それ以前',
dateFormat: '{{month}}月{{day}}日',
setBotEnableError: 'ボットの有効状態の設定に失敗しました',
log: 'ログ',
},
plugins: {
title: 'プラグイン',
description: 'LangBotの機能を拡張するプラグインをインストール・設定',
createPlugin: 'プラグインを作成',
editPlugin: 'プラグインを編集',
installed: 'インストール済み',
marketplace: 'プラグインマーケット',
arrange: '並び替え',
install: 'インストール',
installFromGithub: 'GitHubからプラグインをインストール',
onlySupportGithub: '現在はGitHubからのインストールのみサポートしています',
enterGithubLink: 'プラグインのGitHubリンクを入力してください',
installing: 'プラグインをインストール中...',
installSuccess: 'プラグインのインストールに成功しました',
installFailed: 'プラグインのインストールに失敗しました:',
searchPlugin: 'プラグインを検索',
sortBy: '並び順',
mostStars: 'スター数順',
recentlyAdded: '最近追加',
recentlyUpdated: '最近更新',
noMatchingPlugins: '一致するプラグインが見つかりません',
loading: '読み込み中...',
getPluginListError: 'プラグインリストの取得に失敗しました:',
noPluginInstalled: 'プラグインがインストールされていません',
pluginConfig: 'プラグイン設定',
pluginSort: 'プラグインの並び替え',
pluginSortDescription:
'プラグインの順序は、同一イベント内での処理順序に影響します。カードをドラッグして並び替えが可能です',
pluginSortSuccess: 'プラグインの並び替えに成功しました',
pluginSortError: 'プラグインの並び替えに失敗しました:',
pluginNoConfig: 'プラグインに設定項目がありません。',
deleting: '削除中...',
deletePlugin: 'プラグインを削除',
cancel: 'キャンセル',
saveConfig: '設定を保存',
saving: '保存中...',
confirmDeletePlugin:
'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?',
confirmDelete: '削除を確認',
deleteError: '削除に失敗しました:',
close: '閉じる',
deleteConfirm: '削除の確認',
modifyFailed: '変更に失敗しました:',
eventCount: 'イベント:{{count}}',
toolCount: 'ツール:{{count}}',
starCount: 'スター:{{count}}',
},
pipelines: {
title: 'パイプライン',
description:
'メッセージイベントの処理フローを定義し、ボットに紐付けて使用するパイプラインです',
createPipeline: 'パイプラインを作成',
editPipeline: 'パイプラインを編集',
chat: 'チャット',
getPipelineListError: 'パイプラインリストの取得に失敗しました:',
daysAgo: '日前',
today: '今日',
updateTime: '更新日時',
defaultBadge: 'デフォルト',
basicInfo: '基本情報',
aiCapabilities: 'AI機能',
triggerConditions: 'トリガー条件',
safetyControls: '安全制御',
outputProcessing: '出力処理',
nameRequired: '名前は必須です',
descriptionRequired: '説明は必須です',
createSuccess:
'作成が完了しました。パイプラインの詳細パラメータを設定してください',
createError: '作成に失敗しました:',
saveSuccess: '保存に成功しました',
saveError: '保存に失敗しました:',
deleteConfirmation:
'本当にこのパイプラインを削除しますか?このパイプラインに紐付けられたボットは動作しなくなります。',
defaultPipelineCannotDelete: 'デフォルトパイプラインは削除できません',
debugDialog: {
title: 'パイプラインのチャット',
selectPipeline: 'パイプラインを選択',
sessionType: 'セッションタイプ',
privateChat: 'プライベートチャット',
groupChat: 'グループチャット',
send: '送信',
reset: '会話をリセット',
inputPlaceholder: 'メッセージを入力...',
noMessages: 'メッセージがありません',
userMessage: 'ユーザー',
botMessage: 'ボット',
sendFailed: '送信に失敗しました',
resetSuccess: '会話がリセットされました',
resetFailed: 'リセットに失敗しました',
loadMessagesFailed: 'メッセージの読み込みに失敗しました',
loadPipelinesFailed: 'パイプラインの読み込みに失敗しました',
atTips: 'ボットをメンション',
},
},
register: {
title: 'LangBot を初期化 👋',
description: 'これはLangBotの初回起動です',
adminAccountNote:
'入力したメールアドレスとパスワードが初期管理者アカウントになります',
register: '登録',
initSuccess: '初期化に成功しました。ログインしてください',
initFailed: '初期化に失敗しました:',
},
};
export default jaJP;

View File

@@ -126,6 +126,7 @@ const zhHans = {
earlier: '更久之前',
dateFormat: '{{month}}月{{day}}日',
setBotEnableError: '设置机器人启用状态失败',
log: '日志',
},
plugins: {
title: '插件管理',
@@ -178,6 +179,7 @@ const zhHans = {
description: '流水线定义了对消息事件的处理流程,用于绑定到机器人',
createPipeline: '创建流水线',
editPipeline: '编辑流水线',
chat: '对话',
getPipelineListError: '获取流水线列表失败:',
daysAgo: '天前',
today: '今天',
@@ -197,6 +199,33 @@ const zhHans = {
deleteConfirmation:
'你确定要删除这个流水线吗?已绑定此流水线的机器人将无法使用。',
defaultPipelineCannotDelete: '默认流水线不可删除',
debugDialog: {
title: '流水线对话',
selectPipeline: '选择流水线',
sessionType: '会话类型',
privateChat: '私聊',
groupChat: '群聊',
send: '发送',
reset: '重置对话',
inputPlaceholder: '输入消息...',
noMessages: '暂无消息',
userMessage: '用户',
botMessage: '机器人',
sendFailed: '发送失败',
resetSuccess: '对话已重置',
resetFailed: '重置失败',
loadMessagesFailed: '加载消息失败',
loadPipelinesFailed: '加载流水线失败',
atTips: '提及机器人',
},
},
register: {
title: '初始化 LangBot 👋',
description: '这是您首次启动 LangBot',
adminAccountNote: '您填写的邮箱和密码将作为初始管理员账号',
register: '注册',
initSuccess: '初始化成功 请登录',
initFailed: '初始化失败:',
},
};