mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf98b82cf2 | ||
|
|
edd70b943d | ||
|
|
3cbc823085 | ||
|
|
48becf2c51 | ||
|
|
56c686cd5a | ||
|
|
208273c0dd | ||
|
|
2ff7ca3025 | ||
|
|
61a2361730 | ||
|
|
f80f997a89 | ||
|
|
18529a42c1 | ||
|
|
3e707b4b6e | ||
|
|
62f0a938a8 | ||
|
|
ad3a163d82 | ||
|
|
f5a4503610 | ||
|
|
ec012cf5ed | ||
|
|
d70eceb72c | ||
|
|
f271608114 | ||
|
|
793f0a9c10 | ||
|
|
4f2ec195fc | ||
|
|
e6bc009414 | ||
|
|
20dc8fb5ab | ||
|
|
9a71edfeb0 | ||
|
|
fe3fd664af | ||
|
|
6402755ac6 | ||
|
|
ac8fe049de | ||
|
|
955b391253 | ||
|
|
08c6672841 | ||
|
|
8917050fae | ||
|
|
21daef46f7 | ||
|
|
8ad60b5b64 | ||
|
|
7e17c96c30 | ||
|
|
f17b06767e | ||
|
|
70a29fc623 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -19,7 +19,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 如何重现这个问题,越详细越好;提供越多信息,我们会越快解决问题。
|
||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
@@ -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
143
README.md
@@ -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/>
|
||||
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/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">
|
||||
[](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.
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway 云部署
|
||||
#### Railway Cloud Deployment
|
||||
|
||||
[](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.
|
||||
|
||||

|
||||
160
README_CN.md
Normal file
160
README_CN.md
Normal 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/>
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/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">
|
||||
[](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 模板。
|
||||
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
|
||||
#### Railway 云部署
|
||||
|
||||
[](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 按钮,获取最新动态。
|
||||
|
||||

|
||||
137
README_EN.md
137
README_EN.md
@@ -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/>
|
||||
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/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.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway Cloud Deployment
|
||||
|
||||
[](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>
|
||||
@@ -23,7 +23,7 @@
|
||||
[](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 ボタンをクリックして、最新の更新を取得してください。
|
||||
|
||||

|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 连接,监听消息"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import dingtalk_stream
|
||||
import dingtalk_stream # type: ignore
|
||||
|
||||
|
||||
class DingTalkEvent(dict):
|
||||
|
||||
@@ -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)
|
||||
|
||||
8
main.py
8
main.py
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import argparse
|
||||
# LangBot 终端启动入口
|
||||
# 在此层级解决依赖项检查。
|
||||
# LangBot/main.py
|
||||
@@ -16,6 +17,10 @@ asciiart = r"""
|
||||
|
||||
|
||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
||||
parser = argparse.ArgumentParser(description='LangBot')
|
||||
parser.add_argument('--skip-plugin-deps-check', action='store_true', help='跳过插件依赖项检查', default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(asciiart)
|
||||
|
||||
import sys
|
||||
@@ -39,7 +44,8 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
|
||||
sys.exit(0)
|
||||
|
||||
# check plugin deps
|
||||
await deps.precheck_plugin_deps()
|
||||
if not args.skip_plugin_deps_check:
|
||||
await deps.precheck_plugin_deps()
|
||||
|
||||
# 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
||||
import pydantic.version
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import quart
|
||||
|
||||
from .. import group
|
||||
from ... import group
|
||||
|
||||
|
||||
@group.group_class('pipelines', '/api/v1/pipelines')
|
||||
79
pkg/api/http/controller/groups/pipelines/webchat.py
Normal file
79
pkg/api/http/controller/groups/pipelines/webchat.py
Normal 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)}')
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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。"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,12 @@ class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
||||
.values({'config': config, 'for_version': self.ap.ver_mgr.get_current_version()})
|
||||
.values(
|
||||
{
|
||||
'config': config,
|
||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
|
||||
49
pkg/persistence/migrations/dbm003_n8n_config.py
Normal file
49
pkg/persistence/migrations/dbm003_n8n_config.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
from ...entity.persistence import pipeline as persistence_pipeline
|
||||
|
||||
|
||||
@migration.migration_class(3)
|
||||
class DBMigrateN8nConfig(migration.DBMigration):
|
||||
"""N8n配置"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""升级"""
|
||||
# read all pipelines
|
||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
||||
|
||||
for pipeline in pipelines:
|
||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
||||
|
||||
config = serialized_pipeline['config']
|
||||
|
||||
if 'n8n-service-api' not in config['ai']:
|
||||
config['ai']['n8n-service-api'] = {
|
||||
'webhook-url': 'http://your-n8n-webhook-url',
|
||||
'auth-type': 'none',
|
||||
'basic-username': '',
|
||||
'basic-password': '',
|
||||
'jwt-secret': '',
|
||||
'jwt-algorithm': 'HS256',
|
||||
'header-name': '',
|
||||
'header-value': '',
|
||||
'timeout': 120,
|
||||
'output-key': 'response',
|
||||
}
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
||||
.values(
|
||||
{
|
||||
'config': config,
|
||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""降级"""
|
||||
pass
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
209
pkg/platform/sources/webchat.py
Normal file
209
pkg/platform/sources/webchat.py
Normal 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)]
|
||||
16
pkg/platform/sources/webchat.yaml
Normal file
16
pkg/platform/sources/webchat.yaml
Normal 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"
|
||||
@@ -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,
|
||||
|
||||
@@ -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}'
|
||||
@@ -1,87 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import google.genai
|
||||
from google.genai import types
|
||||
|
||||
from .. import errors, requester
|
||||
from ....core import entities as core_entities
|
||||
from ... import entities as llm_entities
|
||||
from ...tools import entities as tools_entities
|
||||
from . import chatcmpl
|
||||
|
||||
|
||||
class GeminiChatCompletions(requester.LLMAPIRequester):
|
||||
class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
"""Google Gemini API 请求器"""
|
||||
|
||||
default_config: dict[str, typing.Any] = {
|
||||
'base_url': 'https://generativelanguage.googleapis.com',
|
||||
'base_url': 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化 Gemini API 客户端"""
|
||||
pass
|
||||
|
||||
async def invoke_llm(
|
||||
self,
|
||||
query: core_entities.Query,
|
||||
model: requester.RuntimeLLMModel,
|
||||
messages: typing.List[llm_entities.Message],
|
||||
funcs: typing.List[tools_entities.LLMFunction] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
) -> llm_entities.Message:
|
||||
"""调用 Gemini API 生成回复"""
|
||||
try:
|
||||
self.client = google.genai.Client(
|
||||
api_key=model.token_mgr.get_token(),
|
||||
http_options=types.HttpOptions(api_version='v1alpha'),
|
||||
)
|
||||
contents = []
|
||||
|
||||
system_content = None
|
||||
|
||||
for message in messages:
|
||||
role = message.role
|
||||
parts = []
|
||||
|
||||
if isinstance(message.content, str):
|
||||
parts.append(types.Part.from_text(text=message.content))
|
||||
elif isinstance(message.content, list):
|
||||
for content in message.content:
|
||||
if content.type == 'text':
|
||||
parts.append(types.Part.from_text(text=content.text))
|
||||
# elif content.type == 'image_url':
|
||||
# parts.append(types.Part.from_image_url(url=content.image_url))
|
||||
|
||||
if role == 'system':
|
||||
system_content = parts
|
||||
else:
|
||||
content = types.Content(role=role, parts=parts)
|
||||
contents.append(content)
|
||||
|
||||
response = self.client.models.generate_content(
|
||||
model=model.model_entity.name,
|
||||
contents=contents,
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=system_content,
|
||||
**extra_args,
|
||||
),
|
||||
)
|
||||
|
||||
return llm_entities.Message(
|
||||
role='assistant',
|
||||
content=response.candidates[0].content.parts[0].text,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e).lower()
|
||||
if 'invalid api key' in error_message:
|
||||
raise errors.RequesterError(f'无效的 API 密钥: {str(e)}')
|
||||
elif 'not found' in error_message:
|
||||
raise errors.RequesterError(f'请求路径错误或模型无效: {str(e)}')
|
||||
elif any(keyword in error_message for keyword in ['rate limit', 'quota', 'permission denied']):
|
||||
raise errors.RequesterError(f'请求过于频繁或余额不足: {str(e)}')
|
||||
elif 'timeout' in error_message:
|
||||
raise errors.RequesterError(f'请求超时: {str(e)}')
|
||||
else:
|
||||
raise errors.RequesterError(f'Gemini API 请求错误: {str(e)}')
|
||||
|
||||
@@ -14,7 +14,7 @@ spec:
|
||||
zh_Hans: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: "https://generativelanguage.googleapis.com"
|
||||
default: "https://generativelanguage.googleapis.com/v1beta/openai"
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
|
||||
3
pkg/provider/modelmgr/requesters/ppio.svg
Normal file
3
pkg/provider/modelmgr/requesters/ppio.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.7888 0.215881C13.3449 0.215881 0 13.5422 0 29.986C0 38.0916 3.24782 45.4527 8.51506 50.8223V30.0139C8.51506 24.3372 10.7299 18.9769 14.7408 14.966C18.7704 10.9365 24.112 8.74025 29.7981 8.74025H29.9749L29.7888 8.75886C41.5423 8.75886 51.0718 18.2883 51.0718 30.0326C51.0718 31.0562 50.9973 32.0613 50.8577 33.057L38.8343 20.9964C36.4333 18.5954 33.2134 17.2646 29.8074 17.2646C26.4013 17.2646 23.1907 18.5954 20.7805 20.9964C18.3609 23.4159 17.0394 26.6172 17.0394 30.0326C17.0394 33.4479 18.3702 36.6492 20.7805 39.0688C23.1814 41.4697 26.4013 42.8005 29.8074 42.8005C33.2134 42.8005 36.424 41.4697 38.8343 39.0688C41.077 36.826 42.3706 33.8946 42.5474 30.7584L49.6014 37.8403C46.4839 45.7319 38.797 51.3249 29.7981 51.3249C25.1357 51.3249 20.6874 49.8359 17.0301 47.072V56.9178C20.9014 58.7604 25.2195 59.7841 29.7794 59.7841C46.2233 59.7841 59.5682 46.4578 59.5682 30.0139C59.5868 13.5515 46.2512 0.225187 29.7981 0.225187L29.7888 0.215881Z" fill="#0062E2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -5,6 +5,7 @@ metadata:
|
||||
label:
|
||||
en_US: ppio
|
||||
zh_Hans: 派欧云
|
||||
icon: ppio.svg
|
||||
spec:
|
||||
config:
|
||||
- name: base_url
|
||||
|
||||
159
pkg/provider/runners/n8nsvapi.py
Normal file
159
pkg/provider/runners/n8nsvapi.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import json
|
||||
import uuid
|
||||
import aiohttp
|
||||
|
||||
from .. import runner
|
||||
from ...core import app, entities as core_entities
|
||||
from .. import entities as llm_entities
|
||||
|
||||
|
||||
class N8nAPIError(Exception):
|
||||
"""N8n API 请求失败"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
@runner.runner_class('n8n-service-api')
|
||||
class N8nServiceAPIRunner(runner.RequestRunner):
|
||||
"""N8n Service API 工作流请求器"""
|
||||
|
||||
def __init__(self, ap: app.Application, pipeline_config: dict):
|
||||
self.ap = ap
|
||||
self.pipeline_config = pipeline_config
|
||||
|
||||
# 获取webhook URL
|
||||
self.webhook_url = self.pipeline_config['ai']['n8n-service-api']['webhook-url']
|
||||
|
||||
# 获取超时设置,默认为120秒
|
||||
self.timeout = self.pipeline_config['ai']['n8n-service-api'].get('timeout', 120)
|
||||
|
||||
# 获取输出键名,默认为response
|
||||
self.output_key = self.pipeline_config['ai']['n8n-service-api'].get('output-key', 'response')
|
||||
|
||||
# 获取认证类型,默认为none
|
||||
self.auth_type = self.pipeline_config['ai']['n8n-service-api'].get('auth-type', 'none')
|
||||
|
||||
# 根据认证类型获取相应的认证信息
|
||||
if self.auth_type == 'basic':
|
||||
self.basic_username = self.pipeline_config['ai']['n8n-service-api'].get('basic-username', '')
|
||||
self.basic_password = self.pipeline_config['ai']['n8n-service-api'].get('basic-password', '')
|
||||
elif self.auth_type == 'jwt':
|
||||
self.jwt_secret = self.pipeline_config['ai']['n8n-service-api'].get('jwt-secret', '')
|
||||
self.jwt_algorithm = self.pipeline_config['ai']['n8n-service-api'].get('jwt-algorithm', 'HS256')
|
||||
elif self.auth_type == 'header':
|
||||
self.header_name = self.pipeline_config['ai']['n8n-service-api'].get('header-name', '')
|
||||
self.header_value = self.pipeline_config['ai']['n8n-service-api'].get('header-value', '')
|
||||
|
||||
async def _preprocess_user_message(self, query: core_entities.Query) -> str:
|
||||
"""预处理用户消息,提取纯文本
|
||||
|
||||
Returns:
|
||||
str: 纯文本消息
|
||||
"""
|
||||
plain_text = ''
|
||||
|
||||
if isinstance(query.user_message.content, list):
|
||||
for ce in query.user_message.content:
|
||||
if ce.type == 'text':
|
||||
plain_text += ce.text
|
||||
# 注意:n8n webhook目前不支持直接处理图片,如需支持可在此扩展
|
||||
elif isinstance(query.user_message.content, str):
|
||||
plain_text = query.user_message.content
|
||||
|
||||
return plain_text
|
||||
|
||||
async def _call_webhook(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""调用n8n webhook"""
|
||||
# 生成会话ID(如果不存在)
|
||||
if not query.session.using_conversation.uuid:
|
||||
query.session.using_conversation.uuid = str(uuid.uuid4())
|
||||
|
||||
# 预处理用户消息
|
||||
plain_text = await self._preprocess_user_message(query)
|
||||
|
||||
# 准备请求数据
|
||||
payload = {
|
||||
# 基本消息内容
|
||||
'message': plain_text,
|
||||
'user_message_text': plain_text,
|
||||
'conversation_id': query.session.using_conversation.uuid,
|
||||
'session_id': query.variables.get('session_id', ''),
|
||||
'user_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
||||
'msg_create_time': query.variables.get('msg_create_time', ''),
|
||||
}
|
||||
|
||||
# 添加所有变量到payload
|
||||
payload.update(query.variables)
|
||||
|
||||
try:
|
||||
# 准备请求头和认证信息
|
||||
headers = {}
|
||||
auth = None
|
||||
|
||||
# 根据认证类型设置相应的认证信息
|
||||
if self.auth_type == 'basic':
|
||||
# 使用Basic认证
|
||||
auth = aiohttp.BasicAuth(self.basic_username, self.basic_password)
|
||||
self.ap.logger.debug(f'using basic auth: {self.basic_username}')
|
||||
elif self.auth_type == 'jwt':
|
||||
# 使用JWT认证
|
||||
import jwt
|
||||
import time
|
||||
|
||||
# 创建JWT令牌
|
||||
payload_jwt = {
|
||||
'exp': int(time.time()) + 3600, # 1小时过期
|
||||
'iat': int(time.time()),
|
||||
'sub': 'n8n-webhook',
|
||||
}
|
||||
token = jwt.encode(payload_jwt, self.jwt_secret, algorithm=self.jwt_algorithm)
|
||||
|
||||
# 添加到Authorization头
|
||||
headers['Authorization'] = f'Bearer {token}'
|
||||
self.ap.logger.debug('using jwt auth')
|
||||
elif self.auth_type == 'header':
|
||||
# 使用自定义请求头认证
|
||||
headers[self.header_name] = self.header_value
|
||||
self.ap.logger.debug(f'using header auth: {self.header_name}')
|
||||
else:
|
||||
self.ap.logger.debug('no auth')
|
||||
|
||||
# 调用webhook
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
self.webhook_url, json=payload, headers=headers, auth=auth, timeout=self.timeout
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
raise Exception(f'n8n webhook call failed: {response.status}, {error_text}')
|
||||
|
||||
# 解析响应
|
||||
response_data = await response.json()
|
||||
self.ap.logger.debug(f'n8n webhook response: {response_data}')
|
||||
|
||||
# 从响应中提取输出
|
||||
if self.output_key in response_data:
|
||||
output_content = response_data[self.output_key]
|
||||
else:
|
||||
# 如果没有指定的输出键,则使用整个响应
|
||||
output_content = json.dumps(response_data, ensure_ascii=False)
|
||||
|
||||
# 返回消息
|
||||
yield llm_entities.Message(
|
||||
role='assistant',
|
||||
content=output_content,
|
||||
)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'n8n webhook call exception: {str(e)}')
|
||||
raise N8nAPIError(f'n8n webhook call exception: {str(e)}')
|
||||
|
||||
async def run(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||
"""运行请求"""
|
||||
async for msg in self._call_webhook(query):
|
||||
yield msg
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
semantic_version = 'v4.0.4'
|
||||
semantic_version = 'v4.0.7'
|
||||
|
||||
required_database_version = 2
|
||||
required_database_version = 3
|
||||
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
||||
|
||||
debug_mode = False
|
||||
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "langbot"
|
||||
version = "4.0.3"
|
||||
version = "4.0.7"
|
||||
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10.1"
|
||||
@@ -45,11 +45,10 @@ dependencies = [
|
||||
"websockets>=15.0.1",
|
||||
"python-socks>=2.7.1", # dingtalk missing dependency
|
||||
"taskgroup==0.0.0a4", # graingert/taskgroup#20
|
||||
"pip>=25.1.1", # pkg.core.bootutils.deps
|
||||
"google-genai>=1.15.0",
|
||||
"google-generativeai>=0.8.5",
|
||||
"pip>=25.1.1",
|
||||
"ruff>=0.11.9",
|
||||
"pre-commit>=4.2.0",
|
||||
"uv>=0.7.11",
|
||||
]
|
||||
keywords = [
|
||||
"bot",
|
||||
@@ -181,3 +180,4 @@ skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
"api-key": "your-api-key",
|
||||
"app-id": "your-app-id",
|
||||
"references-quote": "参考资料来自:"
|
||||
},
|
||||
"n8n-service-api": {
|
||||
"webhook-url": "http://your-n8n-webhook-url",
|
||||
"auth-type": "none",
|
||||
"basic-username": "",
|
||||
"basic-password": "",
|
||||
"jwt-secret": "",
|
||||
"jwt-algorithm": "HS256",
|
||||
"header-name": "",
|
||||
"header-value": "",
|
||||
"timeout": 120,
|
||||
"output-key": "response"
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
|
||||
@@ -31,6 +31,10 @@ stages:
|
||||
label:
|
||||
en_US: Aliyun Dashscope App API
|
||||
zh_Hans: 阿里云百炼平台 API
|
||||
- name: n8n-service-api
|
||||
label:
|
||||
en_US: n8n Workflow API
|
||||
zh_Hans: n8n 工作流 API
|
||||
- name: local-agent
|
||||
label:
|
||||
en_US: Local Agent
|
||||
@@ -170,3 +174,127 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: '参考资料来自:'
|
||||
- name: n8n-service-api
|
||||
label:
|
||||
en_US: n8n Workflow API
|
||||
zh_Hans: n8n 工作流 API
|
||||
description:
|
||||
en_US: Configure the n8n workflow API of the pipeline
|
||||
zh_Hans: 配置 n8n 工作流 API
|
||||
config:
|
||||
- name: webhook-url
|
||||
label:
|
||||
en_US: Webhook URL
|
||||
zh_Hans: Webhook URL
|
||||
description:
|
||||
en_US: The webhook URL of the n8n workflow
|
||||
zh_Hans: n8n 工作流的 webhook URL
|
||||
type: string
|
||||
required: true
|
||||
- name: auth-type
|
||||
label:
|
||||
en_US: Authentication Type
|
||||
zh_Hans: 认证类型
|
||||
description:
|
||||
en_US: The authentication type for the webhook call
|
||||
zh_Hans: webhook 调用的认证类型
|
||||
type: select
|
||||
required: true
|
||||
default: 'none'
|
||||
options:
|
||||
- name: 'none'
|
||||
label:
|
||||
en_US: None
|
||||
zh_Hans: 无认证
|
||||
- name: 'basic'
|
||||
label:
|
||||
en_US: Basic Auth
|
||||
zh_Hans: 基本认证
|
||||
- name: 'jwt'
|
||||
label:
|
||||
en_US: JWT
|
||||
zh_Hans: JWT认证
|
||||
- name: 'header'
|
||||
label:
|
||||
en_US: Header Auth
|
||||
zh_Hans: 请求头认证
|
||||
- name: basic-username
|
||||
label:
|
||||
en_US: Username
|
||||
zh_Hans: 用户名
|
||||
description:
|
||||
en_US: The username for Basic Auth
|
||||
zh_Hans: 基本认证的用户名
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
- name: basic-password
|
||||
label:
|
||||
en_US: Password
|
||||
zh_Hans: 密码
|
||||
description:
|
||||
en_US: The password for Basic Auth
|
||||
zh_Hans: 基本认证的密码
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
- name: jwt-secret
|
||||
label:
|
||||
en_US: Secret
|
||||
zh_Hans: 密钥
|
||||
description:
|
||||
en_US: The secret for JWT authentication
|
||||
zh_Hans: JWT认证的密钥
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
- name: jwt-algorithm
|
||||
label:
|
||||
en_US: Algorithm
|
||||
zh_Hans: 算法
|
||||
description:
|
||||
en_US: The algorithm for JWT authentication
|
||||
zh_Hans: JWT认证的算法
|
||||
type: string
|
||||
required: false
|
||||
default: 'HS256'
|
||||
- name: header-name
|
||||
label:
|
||||
en_US: Header Name
|
||||
zh_Hans: 请求头名称
|
||||
description:
|
||||
en_US: The header name for Header Auth
|
||||
zh_Hans: 请求头认证的名称
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
- name: header-value
|
||||
label:
|
||||
en_US: Header Value
|
||||
zh_Hans: 请求头值
|
||||
description:
|
||||
en_US: The header value for Header Auth
|
||||
zh_Hans: 请求头认证的值
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
zh_Hans: 超时时间
|
||||
description:
|
||||
en_US: The timeout in seconds for the webhook call
|
||||
zh_Hans: webhook 调用的超时时间(秒)
|
||||
type: integer
|
||||
required: false
|
||||
default: 120
|
||||
- name: output-key
|
||||
label:
|
||||
en_US: Output Key
|
||||
zh_Hans: 输出键名
|
||||
description:
|
||||
en_US: The key name of the output in the webhook response
|
||||
zh_Hans: webhook 响应中输出内容的键名
|
||||
type: string
|
||||
required: false
|
||||
default: 'response'
|
||||
|
||||
283
web/package-lock.json
generated
283
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -113,4 +113,4 @@
|
||||
height: 100%;
|
||||
width: 3rem;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
|
||||
/**
|
||||
* N8n认证表单组件
|
||||
* 根据选择的认证类型动态显示相应的表单项
|
||||
*/
|
||||
export default function N8nAuthFormComponent({
|
||||
itemConfigList,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
}: {
|
||||
itemConfigList: IDynamicFormItemSchema[];
|
||||
onSubmit?: (val: object) => unknown;
|
||||
initialValues?: Record<string, string>;
|
||||
}) {
|
||||
// 当前选择的认证类型
|
||||
const [authType, setAuthType] = useState<string>(
|
||||
initialValues?.['auth-type'] || 'none',
|
||||
);
|
||||
|
||||
// 根据 itemConfigList 动态生成 zod schema
|
||||
const formSchema = z.object(
|
||||
itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
let fieldSchema;
|
||||
switch (item.type) {
|
||||
case 'integer':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'float':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'boolean':
|
||||
fieldSchema = z.boolean();
|
||||
break;
|
||||
case 'string':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'array[string]':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'select':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'llm-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'prompt-editor':
|
||||
fieldSchema = z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
|
||||
if (
|
||||
item.required &&
|
||||
(fieldSchema instanceof z.ZodString ||
|
||||
fieldSchema instanceof z.ZodArray)
|
||||
) {
|
||||
fieldSchema = fieldSchema.min(1, { message: '此字段为必填项' });
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[item.name]: fieldSchema,
|
||||
};
|
||||
},
|
||||
{} as Record<string, z.ZodTypeAny>,
|
||||
),
|
||||
);
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: itemConfigList.reduce((acc, item) => {
|
||||
// 优先使用 initialValues,如果没有则使用默认值
|
||||
const value = initialValues?.[item.name] ?? item.default;
|
||||
return {
|
||||
...acc,
|
||||
[item.name]: value,
|
||||
};
|
||||
}, {} as FormValues),
|
||||
});
|
||||
|
||||
// 当 initialValues 变化时更新表单值
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
// 合并默认值和初始值
|
||||
const mergedValues = itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = initialValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
Object.entries(mergedValues).forEach(([key, value]) => {
|
||||
form.setValue(key as keyof FormValues, value);
|
||||
});
|
||||
|
||||
// 更新认证类型
|
||||
setAuthType((mergedValues['auth-type'] as string) || 'none');
|
||||
}
|
||||
}, [initialValues, form, itemConfigList]);
|
||||
|
||||
// 监听表单值变化
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((value, { name }) => {
|
||||
// 如果认证类型变化,更新状态
|
||||
if (name === 'auth-type') {
|
||||
setAuthType(value['auth-type'] as string);
|
||||
}
|
||||
|
||||
// 获取完整的表单值,确保包含所有默认值
|
||||
const formValues = form.getValues();
|
||||
const finalValues = itemConfigList.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
onSubmit?.(finalValues);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, onSubmit, itemConfigList]);
|
||||
|
||||
// 根据认证类型过滤表单项
|
||||
const filteredConfigList = itemConfigList.filter((config) => {
|
||||
// 始终显示webhook-url、auth-type、timeout和output-key
|
||||
if (
|
||||
['webhook-url', 'auth-type', 'timeout', 'output-key'].includes(
|
||||
config.name,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据认证类型显示相应的表单项
|
||||
if (authType === 'basic' && config.name.startsWith('basic-')) {
|
||||
return true;
|
||||
}
|
||||
if (authType === 'jwt' && config.name.startsWith('jwt-')) {
|
||||
return true;
|
||||
}
|
||||
if (authType === 'header' && config.name.startsWith('header-')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
{filteredConfigList.map((config) => (
|
||||
<FormField
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{i18nObj(config.label)}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{i18nObj(config.description)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
];
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
|
||||
.sidebarChildContainer {
|
||||
width: 9rem;
|
||||
width: 9.8rem;
|
||||
height: 3rem;
|
||||
padding-left: 1.6rem;
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
@@ -97,4 +99,9 @@
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.debugButtonIcon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -244,6 +245,37 @@ export default function PipelineFormComponent({
|
||||
if (stage.name !== currentRunner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 对于n8n-service-api配置,使用N8nAuthFormComponent处理表单联动
|
||||
if (stage.name === 'n8n-service-api') {
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">{i18nObj(stage.label)}</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{i18nObj(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<N8nAuthFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
const currentValues =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.getValues(formName) as Record<string, any>) || {};
|
||||
form.setValue(formName, {
|
||||
...currentValues,
|
||||
[stage.name]: values,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
31
web/src/app/home/pipelines/debug-dialog/AtBadge.tsx
Normal file
31
web/src/app/home/pipelines/debug-dialog/AtBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
422
web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx
Normal file
422
web/src/app/home/pipelines/debug-dialog/DebugDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function PluginForm({
|
||||
};
|
||||
|
||||
if (!pluginInfo || !pluginConfig) {
|
||||
return <div>加载中...</div>;
|
||||
return <div>{t('plugins.loading')}</div>;
|
||||
}
|
||||
|
||||
function deletePlugin() {
|
||||
|
||||
@@ -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;
|
||||
@@ -203,77 +204,10 @@ export interface MarketPluginResponse {
|
||||
}
|
||||
|
||||
interface GetPipelineConfig {
|
||||
ai: {
|
||||
'dashscope-app-api': {
|
||||
'api-key': string;
|
||||
'app-id': string;
|
||||
'app-type': 'agent' | 'workflow';
|
||||
'references-quote'?: string;
|
||||
};
|
||||
'dify-service-api': {
|
||||
'api-key': string;
|
||||
'app-type': 'chat' | 'agent' | 'workflow';
|
||||
'base-url': string;
|
||||
'thinking-convert': 'plain' | 'original' | 'remove';
|
||||
timeout?: number;
|
||||
};
|
||||
'local-agent': {
|
||||
'max-round': number;
|
||||
model: string;
|
||||
prompt: Array<{
|
||||
content: string;
|
||||
role: string;
|
||||
}>;
|
||||
};
|
||||
runner: {
|
||||
runner: 'local-agent' | 'dify-service-api' | 'dashscope-app-api';
|
||||
};
|
||||
};
|
||||
output: {
|
||||
'force-delay': {
|
||||
max: number;
|
||||
min: number;
|
||||
};
|
||||
'long-text-processing': {
|
||||
'font-path': string;
|
||||
strategy: 'forward' | 'image';
|
||||
threshold: number;
|
||||
};
|
||||
misc: {
|
||||
'at-sender': boolean;
|
||||
'hide-exception': boolean;
|
||||
'quote-origin': boolean;
|
||||
'track-function-calls': boolean;
|
||||
};
|
||||
};
|
||||
safety: {
|
||||
'content-filter': {
|
||||
'check-sensitive-words': boolean;
|
||||
scope: 'all' | 'income-msg' | 'output-msg';
|
||||
};
|
||||
'rate-limit': {
|
||||
limitation: number;
|
||||
strategy: 'drop' | 'wait';
|
||||
'window-length': number;
|
||||
};
|
||||
};
|
||||
trigger: {
|
||||
'access-control': {
|
||||
blacklist: string[];
|
||||
mode: 'blacklist' | 'whitelist';
|
||||
whitelist: string[];
|
||||
};
|
||||
'group-respond-rules': {
|
||||
at: boolean;
|
||||
prefix: string[];
|
||||
random: number;
|
||||
regexp: string[];
|
||||
};
|
||||
'ignore-rules': {
|
||||
prefix: string[];
|
||||
regexp: string[];
|
||||
};
|
||||
};
|
||||
ai: object;
|
||||
output: object;
|
||||
safety: object;
|
||||
trigger: object;
|
||||
}
|
||||
|
||||
interface GetPipeline {
|
||||
@@ -295,3 +229,11 @@ export interface GetPipelineResponseData {
|
||||
export interface GetPipelineMetadataResponseData {
|
||||
configs: PipelineConfigTab[];
|
||||
}
|
||||
|
||||
export interface ApiRespWebChatMessage {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export interface ApiRespWebChatMessages {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
7
web/src/app/infra/entities/message/index.ts
Normal file
7
web/src/app/infra/entities/message/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Message {
|
||||
id: number;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
message_chain: object[];
|
||||
timestamp: string;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
48
web/src/components/ui/popover.tsx
Normal file
48
web/src/components/ui/popover.tsx
Normal 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 };
|
||||
58
web/src/components/ui/scroll-area.tsx
Normal file
58
web/src/components/ui/scroll-area.tsx
Normal 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 };
|
||||
@@ -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',
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
239
web/src/i18n/locales/ja-JP.ts
Normal file
239
web/src/i18n/locales/ja-JP.ts
Normal 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;
|
||||
@@ -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: '初始化失败:',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user