Compare commits

..

53 Commits

Author SHA1 Message Date
Junyan Qin
d3a147bbdd chore: bump version 4.3.0b3 2025-08-23 20:08:29 +08:00
Junyan Qin
8eb1b8759b chore: bump version to '4.3.0b2' 2025-08-23 20:06:19 +08:00
Junyan Qin
0155d3b0b9 fix: conflict in table plugin_settings 2025-08-23 20:05:24 +08:00
Junyan Qin
e47a5b4e0d chore: bump langbot_plugin version 2025-08-23 17:12:29 +08:00
Junyan Qin
4012310d99 chore: bump version 4.3.0b1 2025-08-21 10:49:51 +08:00
Junyan Qin
9e9bc88473 chore: remove plugin reorder functionality 2025-08-21 10:47:53 +08:00
Junyan Qin
53ade384eb feat: bump version of langbot-plugin 2025-08-20 23:26:32 +08:00
Junyan Qin
8b2480ad3b feat: setting plugin config 2025-08-17 21:01:43 +08:00
Junyan Qin
b176959836 feat: plugin deletion and upgrade 2025-08-17 18:07:51 +08:00
Junyan Qin
a0c42a5f6e feat: plugin operations 2025-08-17 16:51:44 +08:00
Junyan Qin
17d997c88e fix: i18n fallback 2025-08-17 11:43:38 +08:00
Junyan Qin
0ea7609ff1 perf: frontend 2025-08-16 23:23:24 +08:00
Junyan Qin
28d4b1dd61 feat: marketplace page 2025-08-16 18:05:33 +08:00
Junyan Qin
5179b3e53a feat: trace plugin installation 2025-08-16 15:42:49 +08:00
Junyan Qin
288b294148 feat: plugin installation webui 2025-08-15 22:05:39 +08:00
Junyan Qin
b464d238c5 feat: plugin installation 2025-08-15 21:30:26 +08:00
Junyan Qin
e1a78e8ff9 feat: tag debugging plugins in webui 2025-08-15 19:11:49 +08:00
Junyan Qin
2b8eb5f01c fix: bot switching 2025-08-15 17:02:00 +08:00
Junyan Qin
bf2bc70794 feat: refactor webui httpclient 2025-08-14 23:55:14 +08:00
Junyan Qin
ebe0b68e8f feat: set cloud_service_url 2025-08-14 23:42:57 +08:00
Junyan Qin
39c50d3c12 feat: get_bot_info api 2025-08-13 20:54:43 +08:00
Junyan Qin
621f1301b3 fix: message chain init 2025-08-11 17:24:57 +08:00
Junyan Qin
0b60ef0d06 chore: bump langbot-plugin version to 0.1.1a1 2025-08-09 21:06:31 +08:00
Junyan Qin
41650b585a perf: dispose process 2025-08-02 23:54:06 +08:00
Junyan Qin
f5b893cfe0 feat: kill runtime process when exit in stdio mode 2025-07-16 22:43:39 +08:00
Junyan Qin
e0abd19636 feat: get plugin info 2025-07-13 22:14:22 +08:00
Junyan Qin
4380041c7f feat(ui): list plugins 2025-07-13 22:03:47 +08:00
Junyan Qin
65814a4644 feat: binary storage api 2025-07-13 21:39:33 +08:00
Junyan Qin
7237294008 perf: longer timeout for emit_event 2025-07-13 20:48:15 +08:00
Junyan Qin
214bc8ada9 feat: backward call apis 2025-07-13 20:45:45 +08:00
Junyan Qin
6a1de889b4 refactor: switch llm_entities to plugin sdk 2025-07-13 20:30:17 +08:00
Junyan Qin
4a319b2b20 feat: query-based apis 2025-07-13 18:41:04 +08:00
Junyan Qin
9f269d1614 feat: get bot uuid api 2025-07-13 17:44:20 +08:00
Junyan Qin
4b57771eb1 feat: reply_message api 2025-07-13 16:31:25 +08:00
Junyan Qin
5922be7e15 feat: command execution via plugin 2025-07-13 10:26:48 +08:00
Junyan Qin
10a44c70b6 feat: switch command entities to sdk 2025-07-10 10:51:36 +08:00
Junyan Qin
5b044a1917 feat: add Tool component 2025-07-06 21:03:33 +08:00
Junyan Qin
a60aa6f644 feat: runtime reconnecting 2025-07-02 22:20:20 +08:00
Junyan Qin
1a10b40b17 refactor: use emit_event from connector 2025-07-02 12:46:30 +08:00
Junyan Qin
e2124054bf feat: switch all event emitting logic to new method 2025-07-02 11:58:10 +08:00
Junyan Qin
ee3da8aa17 feat: adapt more events 2025-07-02 11:04:03 +08:00
Junyan Qin
c246470b37 feat: minor changes adapt to event emitting 2025-07-01 22:44:46 +08:00
Junyan Qin
f474e42b79 fix: serialization bug in events emitting 2025-06-30 21:49:59 +08:00
Junyan Qin
5553a86ac8 feat: preliminary migration of events entities 2025-06-30 21:49:59 +08:00
Junyan Qin
01613b2f0d chore: remove adapter meta manifest from components.yaml 2025-06-30 21:49:59 +08:00
Junyan Qin
a177786063 feat: switch message platform adapters to sdk 2025-06-30 21:49:59 +08:00
Junyan Qin
62b2884011 chore: delete Query class 2025-06-30 21:47:40 +08:00
Junyan Qin
6b782f8761 feat: switch Query to langbot-plugin definition 2025-06-30 21:47:40 +08:00
Junyan Qin
0c2560cafb feat: switch tool entities and format 2025-06-30 21:47:40 +08:00
Junyan Qin
c5eeab2fd0 feat: listing plugins 2025-06-30 21:43:43 +08:00
Junyan Qin
6f2fd72af6 feat(plugin): basic communication 2025-06-30 21:43:43 +08:00
Junyan Qin
2d06f1cadb feat: connector for plugin runtime 2025-06-30 21:43:43 +08:00
Junyan Qin
af493c117c deps: add langbot-plugin 2025-06-30 21:43:42 +08:00
351 changed files with 6273 additions and 18053 deletions

View File

@@ -9,7 +9,7 @@
*请在方括号间写`x`以打勾 / Please tick the box with `x`* *请在方括号间写`x`以打勾 / Please tick the box with `x`*
- [ ] 阅读仓库[贡献指引](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)? - [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)?
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer? - [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected. - [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.

1
.gitignore vendored
View File

@@ -43,4 +43,3 @@ test.py
/web_ui /web_ui
.venv/ .venv/
uv.lock uv.lock
/test

View File

@@ -1,40 +1,50 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <a href="https://langbot.app">
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/> <img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a> </a>
<div align="center"> <div align="center">
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a> <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>
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<a href="https://langbot.app">项目主页</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/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a> <a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a> <a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<div align="center">
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
简体中文 / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
</div> </div>
</p> </p>
LangBot 是一个开源的大语言模型原生即时通信机器人开发平台,旨在提供开箱即用的 IM 机器人开发体验,具有 Agent、RAG、MCP 等多种 LLM 应用功能,适配全球主流即时通信平台,并提供丰富的 API 接口,支持自定义开发。 ## ✨ 特性
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态能力并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
## 📦 开始使用 ## 📦 开始使用
#### Docker Compose 部署 #### Docker Compose 部署
```bash ```bash
git clone https://github.com/langbot-app/LangBot git clone https://github.com/RockChinQ/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -61,25 +71,23 @@ docker compose up -d
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。 直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
## 😎 保持更新 ## 📸 效果展示
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。 <img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
![star gif](https://docs.langbot.app/star.gif) <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"/>
- 💬 大模型对话、Agent支持多种大模型适配群聊和私聊具有多轮对话、工具调用、多模态、流式输出能力自带 RAG知识库实现并深度适配 [Dify](https://dify.ai)。 <img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。 <img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
或访问 demo 环境:https://demo.langbot.dev/ - WebUI Demo: https://demo.langbot.dev/
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456` - 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。 - 注意:仅展示webui效果,公开环境,请不要在其中填入您的任何敏感信息。
## 🔌 组件兼容性
### 消息平台 ### 消息平台
@@ -96,6 +104,10 @@ docker compose up -d
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | ✅ | | | Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 正在开发中
### 大模型能力 ### 大模型能力
@@ -107,10 +119,8 @@ docker compose up -d
| [Anthropic](https://www.anthropic.com/) | ✅ | | | [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | | | [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 | | [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 | | [302 AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | | | [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 | | [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 | | [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
@@ -138,8 +148,14 @@ docker compose up -d
## 😘 社区贡献 ## 😘 社区贡献
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献: 感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
## 😎 保持更新
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
![star gif](https://docs.langbot.app/star.gif)

View File

@@ -1,34 +1,48 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <a href="https://langbot.app">
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/> <img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a> </a>
<div align="center"> <div align="center">
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language) <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>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
<a href="https://langbot.app">Home</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/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> <a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a> <a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
<div align="center">
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / English / [日本語](README_JP.md) / (PR for your language)
</div> </div>
</p> </p>
LangBot is an open-source LLM native instant messaging robot development platform, aiming to provide out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, adapting to global instant messaging platforms, and providing rich API interfaces, supporting custom development. ## ✨ 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 ## 📦 Getting Started
#### Docker Compose Deployment #### Docker Compose Deployment
```bash ```bash
git clone https://github.com/langbot-app/LangBot git clone https://github.com/RockChinQ/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -55,25 +69,23 @@ Community contributed Zeabur template.
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation. Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
## 😎 Stay Ahead ## 📸 Demo
Click the Star and Watch button in the upper right corner of the repository to get the latest updates. <img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
![star gif](https://docs.langbot.app/star.gif) <img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
## ✨ Features <img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai). <img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html). <img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
Or visit the demo environment: https://demo.langbot.dev/ - WebUI Demo: https://demo.langbot.dev/
- Login information: Email: `demo@langbot.app` Password: `langbot123456` - Login information: Email: `demo@langbot.app` Password: `langbot123456`
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment. - Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
## 🔌 Component Compatibility
### Message Platform ### Message Platform
@@ -89,6 +101,10 @@ Or visit the demo environment: https://demo.langbot.dev/
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | ✅ | | | Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: In development
### LLMs ### LLMs
@@ -100,11 +116,9 @@ Or visit the demo environment: https://demo.langbot.dev/
| [Anthropic](https://www.anthropic.com/) | ✅ | | | [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
| [Dify](https://dify.ai) | ✅ | LLMOps platform | | [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform | | [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform | | [302 AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | | | [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform | | [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform | | [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
@@ -117,8 +131,14 @@ Or visit the demo environment: https://demo.langbot.dev/
## 🤝 Community Contribution ## 🤝 Community Contribution
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to 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/langbot-app/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
## 😎 Stay Ahead
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
![star gif](https://docs.langbot.app/star.gif)

View File

@@ -1,34 +1,47 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <a href="https://langbot.app">
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/> <img src="https://docs.langbot.app/social.png" alt="LangBot"/>
</a> </a>
<div align="center"> <div align="center">
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language) <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>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
<a href="https://langbot.app">ホーム</a> <a href="https://langbot.app">ホーム</a>
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> <a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> <a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a>
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a> <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">
😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README_CN.md) / [English](README.md) / [日本語](README_JP.md) / (PR for your language)
</div> </div>
</p> </p>
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。 ## ✨ 機能
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
## 📦 始め方 ## 📦 始め方
#### Docker Compose デプロイ #### Docker Compose デプロイ
```bash ```bash
git clone https://github.com/langbot-app/LangBot git clone https://github.com/RockChinQ/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -37,7 +50,7 @@ http://localhost:5300 にアクセスして使用を開始します。
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。 詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
#### Panelでのワンクリックデプロイ #### BTPanelでのワンクリックデプロイ
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。 LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
@@ -55,25 +68,23 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。 リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
## 😎 最新情報を入手 ## 📸 デモ
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。 <img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
![star gif](https://docs.langbot.app/star.gif) <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"/>
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG知識ベースを組み込み、[Dify](https://dify.ai) と深く統合。 <img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。 <img alt="返信効果(インターネットプラグイン付き)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
または、デモ環境にアクセスしてください: https://demo.langbot.dev/ - WebUIデモ: https://demo.langbot.dev/
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456` - ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。 - 注意: WebUIの効果のみを示しています。公開環境では機密情報を入力しないでください。
## 🔌 コンポーネントの互換性
### メッセージプラットフォーム ### メッセージプラットフォーム
@@ -89,6 +100,10 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| Discord | ✅ | | | Discord | ✅ | |
| Telegram | ✅ | | | Telegram | ✅ | |
| Slack | ✅ | | | Slack | ✅ | |
| LINE | 🚧 | |
| WhatsApp | 🚧 | |
🚧: 開発中
### LLMs ### LLMs
@@ -100,10 +115,8 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [Anthropic](https://www.anthropic.com/) | ✅ | | | [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム | | [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム | | [302 AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | | | [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム | | [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム | | [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
@@ -117,8 +130,14 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
## 🤝 コミュニティ貢献 ## 🤝 コミュニティ貢献
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。 LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
<a href="https://github.com/langbot-app/LangBot/graphs/contributors"> <a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" /> <img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
</a> </a>
## 😎 最新情報を入手
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
![star gif](https://docs.langbot.app/star.gif)

View File

@@ -1,140 +0,0 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
</a>
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langbot-app/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/langbot-app/LangBot)](https://github.com/langbot-app/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[![star](https://gitcode.com/RockChinQ/LangBot/star/badge.svg)](https://gitcode.com/RockChinQ/LangBot)
<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/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交外掛</a>
</div>
</p>
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
## 📦 開始使用
#### Docker Compose 部署
```bash
git clone https://github.com/langbot-app/LangBot
cd LangBot
docker compose up -d
```
訪問 http://localhost:5300 即可開始使用。
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
#### 寶塔面板部署
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
#### Zeabur 雲端部署
社群貢獻的 Zeabur 模板。
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH)
#### Railway 雲端部署
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF)
#### 手動部署
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
## 😎 保持更新
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
![star gif](https://docs.langbot.app/star.gif)
## ✨ 特性
- 💬 大模型對話、Agent支援多種大模型適配群聊和私聊具有多輪對話、工具調用、多模態、流式輸出能力自帶 RAG知識庫實現並深度適配 [Dify](https://dify.ai)。
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
或訪問 demo 環境https://demo.langbot.dev/
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
### 訊息平台
| 平台 | 狀態 | 備註 |
| --- | --- | --- |
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
| 微信 | ✅ | |
| 企微對外客服 | ✅ | |
| 微信公眾號 | ✅ | |
| Lark | ✅ | |
| DingTalk | ✅ | |
| Discord | ✅ | |
| Telegram | ✅ | |
| Slack | ✅ | |
### 大模型能力
| 模型 | 狀態 | 備註 |
| --- | --- | --- |
| [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/) | ✅ | |
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
| [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/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>

View File

@@ -9,7 +9,6 @@ spec:
components: components:
ComponentTemplate: ComponentTemplate:
fromFiles: fromFiles:
- pkg/platform/adapter.yaml
- pkg/provider/modelmgr/requester.yaml - pkg/provider/modelmgr/requester.yaml
MessagePlatformAdapter: MessagePlatformAdapter:
fromDirs: fromDirs:

View File

@@ -253,43 +253,6 @@ class DingTalkClient:
await self.logger.error(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()}') raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
async def create_and_card(
self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False
):
content_key = 'content'
card_data = {content_key: ''}
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
# print(card_instance)
# 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards
card_instance_id = await card_instance.async_create_and_deliver_card(
temp_card_id,
card_data,
)
return card_instance, card_instance_id
async def send_card_message(self, card_instance, card_instance_id: str, content: str, is_final: bool):
content_key = 'content'
try:
await card_instance.async_streaming(
card_instance_id,
content_key=content_key,
content_value=content,
append=False,
finished=is_final,
failed=False,
)
except Exception as e:
self.logger.exception(e)
await card_instance.async_streaming(
card_instance_id,
content_key=content_key,
content_value='',
append=False,
finished=is_final,
failed=True,
)
async def start(self): async def start(self):
"""启动 WebSocket 连接,监听消息""" """启动 WebSocket 连接,监听消息"""
await self.client.start() await self.client.start()

View File

@@ -3,7 +3,7 @@ from quart import request
import httpx import httpx
from quart import Quart from quart import Quart
from typing import Callable, Dict, Any from typing import Callable, Dict, Any
from pkg.platform.types import events as platform_events import langbot_plugin.api.entities.builtin.platform.events as platform_events
from .qqofficialevent import QQOfficialEvent from .qqofficialevent import QQOfficialEvent
import json import json
import traceback import traceback

View File

@@ -4,7 +4,7 @@ from quart import Quart, jsonify, request
from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.web.async_client import AsyncWebClient
from .slackevent import SlackEvent from .slackevent import SlackEvent
from typing import Callable from typing import Callable
from pkg.platform.types import events as platform_events import langbot_plugin.api.entities.builtin.platform.events as platform_events
class SlackClient: class SlackClient:

View File

@@ -1 +1,4 @@
from .client import WeChatPadClient as WeChatPadClient from .client import WeChatPadClient
__all__ = ['WeChatPadClient']

View File

@@ -8,7 +8,7 @@ from quart import Quart
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Callable, Dict, Any from typing import Callable, Dict, Any
from .wecomevent import WecomEvent from .wecomevent import WecomEvent
from pkg.platform.types import message as platform_message import langbot_plugin.api.entities.builtin.platform.message as platform_message
import aiofiles import aiofiles

View File

@@ -8,7 +8,7 @@ from quart import Quart
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Callable from typing import Callable
from .wecomcsevent import WecomCSEvent from .wecomcsevent import WecomCSEvent
from pkg.platform.types import message as platform_message import langbot_plugin.api.entities.builtin.platform.message as platform_message
import aiofiles import aiofiles

12
main.py
View File

@@ -11,7 +11,7 @@ asciiart = r"""
|____\__,_|_||_\__, |___/\___/\__| |____\__,_|_||_\__, |___/\___/\__|
|___/ |___/
⭐️ Open Source 开源地址: https://github.com/langbot-app/LangBot ⭐️ Open Source 开源地址: https://github.com/RockChinQ/LangBot
📖 Documentation 文档地址: https://docs.langbot.app 📖 Documentation 文档地址: https://docs.langbot.app
""" """
@@ -47,13 +47,13 @@ async def main_entry(loop: asyncio.AbstractEventLoop):
if not args.skip_plugin_deps_check: if not args.skip_plugin_deps_check:
await deps.precheck_plugin_deps() await deps.precheck_plugin_deps()
# 检查pydantic版本如果没有 pydantic.v1则把 pydantic 映射为 v1 # # 检查pydantic版本如果没有 pydantic.v1则把 pydantic 映射为 v1
import pydantic.version # import pydantic.version
if pydantic.version.VERSION < '2.0': # if pydantic.version.VERSION < '2.0':
import pydantic # import pydantic
sys.modules['pydantic.v1'] = pydantic # sys.modules['pydantic.v1'] = pydantic
# 检查配置文件 # 检查配置文件

View File

@@ -11,10 +11,10 @@ from ....core import app
preregistered_groups: list[type[RouterGroup]] = [] preregistered_groups: list[type[RouterGroup]] = []
"""Pre-registered list of RouterGroup""" """RouterGroup 的预注册列表"""
def group_class(name: str, path: str) -> typing.Callable[[typing.Type[RouterGroup]], typing.Type[RouterGroup]]: def group_class(name: str, path: str) -> None:
"""注册一个 RouterGroup""" """注册一个 RouterGroup"""
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]: def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
@@ -27,7 +27,7 @@ def group_class(name: str, path: str) -> typing.Callable[[typing.Type[RouterGrou
class AuthType(enum.Enum): class AuthType(enum.Enum):
"""Authentication type""" """认证类型"""
NONE = 'none' NONE = 'none'
USER_TOKEN = 'user-token' USER_TOKEN = 'user-token'
@@ -56,7 +56,7 @@ class RouterGroup(abc.ABC):
auth_type: AuthType = AuthType.USER_TOKEN, auth_type: AuthType = AuthType.USER_TOKEN,
**options: typing.Any, **options: typing.Any,
) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator ) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator
"""Register a route""" """注册一个路由"""
def decorator(f: RouteCallable) -> RouteCallable: def decorator(f: RouteCallable) -> RouteCallable:
nonlocal rule nonlocal rule
@@ -64,11 +64,11 @@ class RouterGroup(abc.ABC):
async def handler_error(*args, **kwargs): async def handler_error(*args, **kwargs):
if auth_type == AuthType.USER_TOKEN: if auth_type == AuthType.USER_TOKEN:
# get token from Authorization header # Authorization头中获取token
token = quart.request.headers.get('Authorization', '').replace('Bearer ', '') token = quart.request.headers.get('Authorization', '').replace('Bearer ', '')
if not token: if not token:
return self.http_status(401, -1, 'No valid user token provided') return self.http_status(401, -1, '未提供有效的用户令牌')
try: try:
user_email = await self.ap.user_service.verify_jwt_token(token) user_email = await self.ap.user_service.verify_jwt_token(token)
@@ -76,9 +76,9 @@ class RouterGroup(abc.ABC):
# check if this account exists # check if this account exists
user = await self.ap.user_service.get_user_by_email(user_email) user = await self.ap.user_service.get_user_by_email(user_email)
if not user: if not user:
return self.http_status(401, -1, 'User not found') return self.http_status(401, -1, '用户不存在')
# check if f accepts user_email parameter # 检查f是否接受user_email参数
if 'user_email' in f.__code__.co_varnames: if 'user_email' in f.__code__.co_varnames:
kwargs['user_email'] = user_email kwargs['user_email'] = user_email
except Exception as e: except Exception as e:
@@ -86,11 +86,10 @@ class RouterGroup(abc.ABC):
try: try:
return await f(*args, **kwargs) return await f(*args, **kwargs)
except Exception: # 自动 500
except Exception as e: # 自动 500
traceback.print_exc() traceback.print_exc()
# return self.http_status(500, -2, str(e)) # return self.http_status(500, -2, str(e))
return self.http_status(500, -2, str(e)) return self.http_status(500, -2, 'internal server error')
new_f = handler_error new_f = handler_error
new_f.__name__ = (self.name + rule).replace('/', '__') new_f.__name__ = (self.name + rule).replace('/', '__')
@@ -102,7 +101,7 @@ class RouterGroup(abc.ABC):
return decorator return decorator
def success(self, data: typing.Any = None) -> quart.Response: def success(self, data: typing.Any = None) -> quart.Response:
"""Return a 200 response""" """返回一个 200 响应"""
return quart.jsonify( return quart.jsonify(
{ {
'code': 0, 'code': 0,
@@ -112,7 +111,7 @@ class RouterGroup(abc.ABC):
) )
def fail(self, code: int, msg: str) -> quart.Response: def fail(self, code: int, msg: str) -> quart.Response:
"""Return an error response""" """返回一个异常响应"""
return quart.jsonify( return quart.jsonify(
{ {
@@ -121,6 +120,6 @@ class RouterGroup(abc.ABC):
} }
) )
def http_status(self, status: int, code: int, msg: str) -> typing.Tuple[quart.Response, int]: def http_status(self, status: int, code: int, msg: str) -> quart.Response:
"""返回一个指定状态码的响应""" """返回一个指定状态码的响应"""
return (self.fail(code, msg), status) return self.fail(code, msg), status

View File

@@ -2,10 +2,6 @@ from __future__ import annotations
import quart import quart
import mimetypes import mimetypes
import uuid
import asyncio
import quart.datastructures
from .. import group from .. import group
@@ -24,23 +20,3 @@ class FilesRouterGroup(group.RouterGroup):
mime_type = 'image/jpeg' mime_type = 'image/jpeg'
return quart.Response(image_bytes, mimetype=mime_type) return quart.Response(image_bytes, mimetype=mime_type)
@self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> quart.Response:
request = quart.request
# get file bytes from 'file'
file = (await request.files)['file']
assert isinstance(file, quart.datastructures.FileStorage)
file_bytes = await asyncio.to_thread(file.stream.read)
extension = file.filename.split('.')[-1]
file_name = file.filename.split('.')[0]
file_key = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension
# save file to storage
await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes)
return self.success(
data={
'file_id': file_key,
}
)

View File

@@ -1,90 +0,0 @@
import quart
from ... import group
@group.group_class('knowledge_base', '/api/v1/knowledge/bases')
class KnowledgeBaseRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['POST', 'GET'])
async def handle_knowledge_bases() -> quart.Response:
if quart.request.method == 'GET':
knowledge_bases = await self.ap.knowledge_service.get_knowledge_bases()
return self.success(data={'bases': knowledge_bases})
elif quart.request.method == 'POST':
json_data = await quart.request.json
knowledge_base_uuid = await self.ap.knowledge_service.create_knowledge_base(json_data)
return self.success(data={'uuid': knowledge_base_uuid})
return self.http_status(405, -1, 'Method not allowed')
@self.route(
'/<knowledge_base_uuid>',
methods=['GET', 'DELETE', 'PUT'],
)
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response:
if quart.request.method == 'GET':
knowledge_base = await self.ap.knowledge_service.get_knowledge_base(knowledge_base_uuid)
if knowledge_base is None:
return self.http_status(404, -1, 'knowledge base not found')
return self.success(
data={
'base': knowledge_base,
}
)
elif quart.request.method == 'PUT':
json_data = await quart.request.json
await self.ap.knowledge_service.update_knowledge_base(knowledge_base_uuid, json_data)
return self.success({})
elif quart.request.method == 'DELETE':
await self.ap.knowledge_service.delete_knowledge_base(knowledge_base_uuid)
return self.success({})
@self.route(
'/<knowledge_base_uuid>/files',
methods=['GET', 'POST'],
)
async def get_knowledge_base_files(knowledge_base_uuid: str) -> str:
if quart.request.method == 'GET':
files = await self.ap.knowledge_service.get_files_by_knowledge_base(knowledge_base_uuid)
return self.success(
data={
'files': files,
}
)
elif quart.request.method == 'POST':
json_data = await quart.request.json
file_id = json_data.get('file_id')
if not file_id:
return self.http_status(400, -1, 'File ID is required')
# 调用服务层方法将文件与知识库关联
task_id = await self.ap.knowledge_service.store_file(knowledge_base_uuid, file_id)
return self.success(
{
'task_id': task_id,
}
)
@self.route(
'/<knowledge_base_uuid>/files/<file_id>',
methods=['DELETE'],
)
async def delete_specific_file_in_kb(file_id: str, knowledge_base_uuid: str) -> str:
await self.ap.knowledge_service.delete_file(knowledge_base_uuid, file_id)
return self.success({})
@self.route(
'/<knowledge_base_uuid>/retrieve',
methods=['POST'],
)
async def retrieve_knowledge_base(knowledge_base_uuid: str) -> str:
json_data = await quart.request.json
query = json_data.get('query')
results = await self.ap.knowledge_service.retrieve_knowledge_base(knowledge_base_uuid, query)
return self.success(data={'results': results})

View File

@@ -11,11 +11,7 @@ class PipelinesRouterGroup(group.RouterGroup):
@self.route('', methods=['GET', 'POST']) @self.route('', methods=['GET', 'POST'])
async def _() -> str: async def _() -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
sort_by = quart.request.args.get('sort_by', 'created_at') return self.success(data={'pipelines': await self.ap.pipeline_service.get_pipelines()})
sort_order = quart.request.args.get('sort_order', 'DESC')
return self.success(
data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)}
)
elif quart.request.method == 'POST': elif quart.request.method == 'POST':
json_data = await quart.request.json json_data = await quart.request.json

View File

@@ -1,5 +1,3 @@
import json
import quart import quart
from ... import group from ... import group
@@ -10,19 +8,11 @@ class WebChatDebugRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('/send', methods=['POST']) @self.route('/send', methods=['POST'])
async def send_message(pipeline_uuid: str) -> str: async def send_message(pipeline_uuid: str) -> str:
"""Send a message to the pipeline for debugging""" """发送调试消息到流水线"""
async def stream_generator(generator):
yield 'data: {"type": "start"}\n\n'
async for message in generator:
yield f'data: {json.dumps({"message": message})}\n\n'
yield 'data: {"type": "end"}\n\n'
try: try:
data = await quart.request.get_json() data = await quart.request.get_json()
session_type = data.get('session_type', 'person') session_type = data.get('session_type', 'person')
message_chain_obj = data.get('message', []) message_chain_obj = data.get('message', [])
is_stream = data.get('is_stream', False)
if not message_chain_obj: if not message_chain_obj:
return self.http_status(400, -1, 'message is required') return self.http_status(400, -1, 'message is required')
@@ -35,40 +25,20 @@ class WebChatDebugRouterGroup(group.RouterGroup):
if not webchat_adapter: if not webchat_adapter:
return self.http_status(404, -1, 'WebChat adapter not found') return self.http_status(404, -1, 'WebChat adapter not found')
if is_stream: result = await webchat_adapter.send_webchat_message(pipeline_uuid, session_type, message_chain_obj)
generator = webchat_adapter.send_webchat_message(
pipeline_uuid, session_type, message_chain_obj, is_stream
)
# 设置正确的响应头
headers = {
'Content-Type': 'text/event-stream',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
return quart.Response(stream_generator(generator), mimetype='text/event-stream',headers=headers)
else: # non-stream return self.success(
result = None data={
async for message in webchat_adapter.send_webchat_message( 'message': result,
pipeline_uuid, session_type, message_chain_obj }
): )
result = message
if result is not None:
return self.success(
data={
'message': result,
}
)
else:
return self.http_status(400, -1, 'message is required')
except Exception as e: except Exception as e:
return self.http_status(500, -1, f'Internal server error: {str(e)}') return self.http_status(500, -1, f'Internal server error: {str(e)}')
@self.route('/messages/<session_type>', methods=['GET']) @self.route('/messages/<session_type>', methods=['GET'])
async def get_messages(pipeline_uuid: str, session_type: str) -> str: async def get_messages(pipeline_uuid: str, session_type: str) -> str:
"""Get the message history of the pipeline for debugging""" """获取调试消息历史"""
try: try:
if session_type not in ['person', 'group']: if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group') return self.http_status(400, -1, 'session_type must be person or group')
@@ -87,7 +57,7 @@ class WebChatDebugRouterGroup(group.RouterGroup):
@self.route('/reset/<session_type>', methods=['POST']) @self.route('/reset/<session_type>', methods=['POST'])
async def reset_session(session_type: str) -> str: async def reset_session(session_type: str) -> str:
"""Reset the debug session""" """重置调试会话"""
try: try:
if session_type not in ['person', 'group']: if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group') return self.http_status(400, -1, 'session_type must be person or group')

View File

@@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import base64
import quart import quart
from .....core import taskmgr from .....core import taskmgr
from .. import group from .. import group
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
@group.group_class('plugins', '/api/v1/plugins') @group.group_class('plugins', '/api/v1/plugins')
@@ -12,35 +13,22 @@ class PluginsRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN) @self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
plugins = self.ap.plugin_mgr.plugins() plugins = await self.ap.plugin_connector.list_plugins()
plugins_data = [plugin.model_dump() for plugin in plugins] return self.success(data={'plugins': plugins})
return self.success(data={'plugins': plugins_data})
@self.route( @self.route(
'/<author>/<plugin_name>/toggle', '/<author>/<plugin_name>/upgrade',
methods=['PUT'],
auth_type=group.AuthType.USER_TOKEN,
)
async def _(author: str, plugin_name: str) -> str:
data = await quart.request.json
target_enabled = data.get('target_enabled')
await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled)
return self.success()
@self.route(
'/<author>/<plugin_name>/update',
methods=['POST'], methods=['POST'],
auth_type=group.AuthType.USER_TOKEN, auth_type=group.AuthType.USER_TOKEN,
) )
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
ctx = taskmgr.TaskContext.new() ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task( wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx), self.ap.plugin_connector.upgrade_plugin(author, plugin_name, task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name=f'plugin-update-{plugin_name}', name=f'plugin-upgrade-{plugin_name}',
label=f'Updating plugin {plugin_name}', label=f'Upgrading plugin {plugin_name}',
context=ctx, context=ctx,
) )
return self.success(data={'task_id': wrapper.id}) return self.success(data={'task_id': wrapper.id})
@@ -52,14 +40,14 @@ class PluginsRouterGroup(group.RouterGroup):
) )
async def _(author: str, plugin_name: str) -> str: async def _(author: str, plugin_name: str) -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
if plugin is None: if plugin is None:
return self.http_status(404, -1, 'plugin not found') return self.http_status(404, -1, 'plugin not found')
return self.success(data={'plugin': plugin.model_dump()}) return self.success(data={'plugin': plugin})
elif quart.request.method == 'DELETE': elif quart.request.method == 'DELETE':
ctx = taskmgr.TaskContext.new() ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task( wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx), self.ap.plugin_connector.delete_plugin(author, plugin_name, task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name=f'plugin-remove-{plugin_name}', name=f'plugin-remove-{plugin_name}',
label=f'Removing plugin {plugin_name}', label=f'Removing plugin {plugin_name}',
@@ -74,24 +62,19 @@ class PluginsRouterGroup(group.RouterGroup):
auth_type=group.AuthType.USER_TOKEN, auth_type=group.AuthType.USER_TOKEN,
) )
async def _(author: str, plugin_name: str) -> quart.Response: async def _(author: str, plugin_name: str) -> quart.Response:
plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name) plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
if plugin is None: if plugin is None:
return self.http_status(404, -1, 'plugin not found') return self.http_status(404, -1, 'plugin not found')
if quart.request.method == 'GET': if quart.request.method == 'GET':
return self.success(data={'config': plugin.plugin_config}) return self.success(data={'config': plugin['plugin_config']})
elif quart.request.method == 'PUT': elif quart.request.method == 'PUT':
data = await quart.request.json data = await quart.request.json
await self.ap.plugin_mgr.set_plugin_config(plugin, data) await self.ap.plugin_connector.set_plugin_config(author, plugin_name, data)
return self.success(data={}) return self.success(data={})
@self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
data = await quart.request.json
await self.ap.plugin_mgr.reorder_plugins(data.get('plugins'))
return self.success()
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) @self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
data = await quart.request.json data = await quart.request.json
@@ -102,7 +85,47 @@ class PluginsRouterGroup(group.RouterGroup):
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx), self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name='plugin-install-github', name='plugin-install-github',
label=f'Installing plugin ...{short_source_str}', label=f'Installing plugin from github ...{short_source_str}',
context=ctx,
)
return self.success(data={'task_id': wrapper.id})
@self.route('/install/marketplace', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
data = await quart.request.json
ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
kind='plugin-operation',
name='plugin-install-marketplace',
label=f'Installing plugin from marketplace ...{data}',
context=ctx,
)
return self.success(data={'task_id': wrapper.id})
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
file = (await quart.request.files).get('file')
if file is None:
return self.http_status(400, -1, 'file is required')
file_bytes = file.read()
file_base64 = base64.b64encode(file_bytes).decode('utf-8')
data = {
'plugin_file': file_base64,
}
ctx = taskmgr.TaskContext.new()
wrapper = self.ap.task_mgr.create_user_task(
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
kind='plugin-operation',
name='plugin-install-local',
label=f'Installing plugin from local ...{file.filename}',
context=ctx, context=ctx,
) )

View File

@@ -9,18 +9,18 @@ class LLMModelsRouterGroup(group.RouterGroup):
@self.route('', methods=['GET', 'POST']) @self.route('', methods=['GET', 'POST'])
async def _() -> str: async def _() -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
return self.success(data={'models': await self.ap.llm_model_service.get_llm_models()}) return self.success(data={'models': await self.ap.model_service.get_llm_models()})
elif quart.request.method == 'POST': elif quart.request.method == 'POST':
json_data = await quart.request.json json_data = await quart.request.json
model_uuid = await self.ap.llm_model_service.create_llm_model(json_data) model_uuid = await self.ap.model_service.create_llm_model(json_data)
return self.success(data={'uuid': model_uuid}) return self.success(data={'uuid': model_uuid})
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE']) @self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
async def _(model_uuid: str) -> str: async def _(model_uuid: str) -> str:
if quart.request.method == 'GET': if quart.request.method == 'GET':
model = await self.ap.llm_model_service.get_llm_model(model_uuid) model = await self.ap.model_service.get_llm_model(model_uuid)
if model is None: if model is None:
return self.http_status(404, -1, 'model not found') return self.http_status(404, -1, 'model not found')
@@ -29,11 +29,11 @@ class LLMModelsRouterGroup(group.RouterGroup):
elif quart.request.method == 'PUT': elif quart.request.method == 'PUT':
json_data = await quart.request.json json_data = await quart.request.json
await self.ap.llm_model_service.update_llm_model(model_uuid, json_data) await self.ap.model_service.update_llm_model(model_uuid, json_data)
return self.success() return self.success()
elif quart.request.method == 'DELETE': elif quart.request.method == 'DELETE':
await self.ap.llm_model_service.delete_llm_model(model_uuid) await self.ap.model_service.delete_llm_model(model_uuid)
return self.success() return self.success()
@@ -41,49 +41,6 @@ class LLMModelsRouterGroup(group.RouterGroup):
async def _(model_uuid: str) -> str: async def _(model_uuid: str) -> str:
json_data = await quart.request.json json_data = await quart.request.json
await self.ap.llm_model_service.test_llm_model(model_uuid, json_data) await self.ap.model_service.test_llm_model(model_uuid, json_data)
return self.success()
@group.group_class('models/embedding', '/api/v1/provider/models/embedding')
class EmbeddingModelsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('', methods=['GET', 'POST'])
async def _() -> str:
if quart.request.method == 'GET':
return self.success(data={'models': await self.ap.embedding_models_service.get_embedding_models()})
elif quart.request.method == 'POST':
json_data = await quart.request.json
model_uuid = await self.ap.embedding_models_service.create_embedding_model(json_data)
return self.success(data={'uuid': model_uuid})
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
async def _(model_uuid: str) -> str:
if quart.request.method == 'GET':
model = await self.ap.embedding_models_service.get_embedding_model(model_uuid)
if model is None:
return self.http_status(404, -1, 'model not found')
return self.success(data={'model': model})
elif quart.request.method == 'PUT':
json_data = await quart.request.json
await self.ap.embedding_models_service.update_embedding_model(model_uuid, json_data)
return self.success()
elif quart.request.method == 'DELETE':
await self.ap.embedding_models_service.delete_embedding_model(model_uuid)
return self.success()
@self.route('/<model_uuid>/test', methods=['POST'])
async def _(model_uuid: str) -> str:
json_data = await quart.request.json
await self.ap.embedding_models_service.test_embedding_model(model_uuid, json_data)
return self.success() return self.success()

View File

@@ -8,8 +8,7 @@ class RequestersRouterGroup(group.RouterGroup):
async def initialize(self) -> None: async def initialize(self) -> None:
@self.route('', methods=['GET']) @self.route('', methods=['GET'])
async def _() -> quart.Response: async def _() -> quart.Response:
model_type = quart.request.args.get('type', '') return self.success(data={'requesters': self.ap.model_mgr.get_available_requesters_info()})
return self.success(data={'requesters': self.ap.model_mgr.get_available_requesters_info(model_type)})
@self.route('/<requester_name>', methods=['GET']) @self.route('/<requester_name>', methods=['GET'])
async def _(requester_name: str) -> quart.Response: async def _(requester_name: str) -> quart.Response:

View File

@@ -14,6 +14,11 @@ class SystemRouterGroup(group.RouterGroup):
'version': constants.semantic_version, 'version': constants.semantic_version,
'debug': constants.debug_mode, 'debug': constants.debug_mode,
'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()), 'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()),
'cloud_service_url': (
self.ap.instance_config.data['plugin']['cloud_service_url']
if 'cloud_service_url' in self.ap.instance_config.data['plugin']
else 'https://space.langbot.app'
),
} }
) )
@@ -35,16 +40,7 @@ class SystemRouterGroup(group.RouterGroup):
return self.success(data=task.to_dict()) return self.success(data=task.to_dict())
@self.route('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN) @self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
json_data = await quart.request.json
scope = json_data.get('scope')
await self.ap.reload(scope=scope)
return self.success()
@self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str: async def _() -> str:
if not constants.debug_mode: if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden') return self.http_status(403, 403, 'Forbidden')
@@ -54,3 +50,39 @@ class SystemRouterGroup(group.RouterGroup):
ap = self.ap ap = self.ap
return self.success(data=exec(py_code, {'ap': ap})) return self.success(data=exec(py_code, {'ap': ap}))
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden')
data = await quart.request.json
return self.success(
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
)
@self.route(
'/debug/plugin/action',
methods=['POST'],
auth_type=group.AuthType.USER_TOKEN,
)
async def _() -> str:
if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden')
data = await quart.request.json
class AnoymousAction:
value = 'anonymous_action'
def __init__(self, value: str):
self.value = value
resp = await self.ap.plugin_connector.handler.call_action(
AnoymousAction(data['action']),
data['data'],
timeout=data.get('timeout', 10),
)
return self.success(data=resp)

View File

@@ -1,6 +1,5 @@
import quart import quart
import argon2 import argon2
import asyncio
from .. import group from .. import group
@@ -14,7 +13,7 @@ class UserRouterGroup(group.RouterGroup):
return self.success(data={'initialized': await self.ap.user_service.is_initialized()}) return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
if await self.ap.user_service.is_initialized(): if await self.ap.user_service.is_initialized():
return self.fail(1, 'System already initialized') return self.fail(1, '系统已初始化')
json_data = await quart.request.json json_data = await quart.request.json
@@ -32,7 +31,7 @@ class UserRouterGroup(group.RouterGroup):
try: try:
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password']) token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
except argon2.exceptions.VerifyMismatchError: except argon2.exceptions.VerifyMismatchError:
return self.fail(1, 'Invalid username or password') return self.fail(1, '用户名或密码错误')
return self.success(data={'token': token}) return self.success(data={'token': token})
@@ -41,45 +40,3 @@ class UserRouterGroup(group.RouterGroup):
token = await self.ap.user_service.generate_jwt_token(user_email) token = await self.ap.user_service.generate_jwt_token(user_email)
return self.success(data={'token': token}) return self.success(data={'token': token})
@self.route('/reset-password', methods=['POST'], auth_type=group.AuthType.NONE)
async def _() -> str:
json_data = await quart.request.json
user_email = json_data['user']
recovery_key = json_data['recovery_key']
new_password = json_data['new_password']
# hard sleep 3s for security
await asyncio.sleep(3)
if not await self.ap.user_service.is_initialized():
return self.http_status(400, -1, 'System not initialized')
user_obj = await self.ap.user_service.get_user_by_email(user_email)
if user_obj is None:
return self.http_status(400, -1, 'User not found')
if recovery_key != self.ap.instance_config.data['system']['recovery_key']:
return self.http_status(403, -1, 'Invalid recovery key')
await self.ap.user_service.reset_password(user_email, new_password)
return self.success(data={'user': user_email})
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _(user_email: str) -> str:
json_data = await quart.request.json
current_password = json_data['current_password']
new_password = json_data['new_password']
try:
await self.ap.user_service.change_password(user_email, current_password, new_password)
except argon2.exceptions.VerifyMismatchError:
return self.http_status(400, -1, 'Current password is incorrect')
except ValueError as e:
return self.http_status(400, -1, str(e))
return self.success(data={'user': user_email})

View File

@@ -14,13 +14,11 @@ from . import group
from .groups import provider as groups_provider from .groups import provider as groups_provider
from .groups import platform as groups_platform from .groups import platform as groups_platform
from .groups import pipelines as groups_pipelines from .groups import pipelines as groups_pipelines
from .groups import knowledge as groups_knowledge
importutil.import_modules_in_pkg(groups) importutil.import_modules_in_pkg(groups)
importutil.import_modules_in_pkg(groups_provider) importutil.import_modules_in_pkg(groups_provider)
importutil.import_modules_in_pkg(groups_platform) importutil.import_modules_in_pkg(groups_platform)
importutil.import_modules_in_pkg(groups_pipelines) importutil.import_modules_in_pkg(groups_pipelines)
importutil.import_modules_in_pkg(groups_knowledge)
class HTTPController: class HTTPController:
@@ -47,7 +45,7 @@ class HTTPController:
try: try:
await self.quart_app.run_task(*args, **kwargs) await self.quart_app.run_task(*args, **kwargs)
except Exception as e: except Exception as e:
self.ap.logger.error(f'Failed to start HTTP service: {e}') self.ap.logger.error(f'启动 HTTP 服务失败: {e}')
self.ap.task_mgr.create_task( self.ap.task_mgr.create_task(
exception_handler( exception_handler(

View File

@@ -10,23 +10,27 @@ from ....entity.persistence import pipeline as persistence_pipeline
class BotService: class BotService:
"""Bot service""" """机器人服务"""
ap: app.Application ap: app.Application
def __init__(self, ap: app.Application) -> None: def __init__(self, ap: app.Application) -> None:
self.ap = ap self.ap = ap
async def get_bots(self) -> list[dict]: async def get_bots(self, include_secret: bool = True) -> list[dict]:
"""Get all bots""" """获取所有机器人"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot)) result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
bots = result.all() bots = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) for bot in bots] masked_columns = []
if not include_secret:
masked_columns = ['adapter_config']
async def get_bot(self, bot_uuid: str) -> dict | None: return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) for bot in bots]
"""Get bot"""
async def get_bot(self, bot_uuid: str, include_secret: bool = True) -> dict | None:
"""获取机器人"""
result = await self.ap.persistence_mgr.execute_async( result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid) sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
) )
@@ -36,10 +40,30 @@ class BotService:
if bot is None: if bot is None:
return None return None
return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) masked_columns = []
if not include_secret:
masked_columns = ['adapter_config']
return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns)
async def get_runtime_bot_info(self, bot_uuid: str, include_secret: bool = True) -> dict:
"""获取机器人运行时信息"""
persistence_bot = await self.get_bot(bot_uuid, include_secret)
if persistence_bot is None:
raise Exception('Bot not found')
adapter_runtime_values = {}
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
if runtime_bot is not None:
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
persistence_bot['adapter_runtime_values'] = adapter_runtime_values
return persistence_bot
async def create_bot(self, bot_data: dict) -> str: async def create_bot(self, bot_data: dict) -> str:
"""Create bot""" """创建机器人"""
# TODO: 检查配置信息格式 # TODO: 检查配置信息格式
bot_data['uuid'] = str(uuid.uuid4()) bot_data['uuid'] = str(uuid.uuid4())
@@ -63,7 +87,7 @@ class BotService:
return bot_data['uuid'] return bot_data['uuid']
async def update_bot(self, bot_uuid: str, bot_data: dict) -> None: async def update_bot(self, bot_uuid: str, bot_data: dict) -> None:
"""Update bot""" """更新机器人"""
if 'uuid' in bot_data: if 'uuid' in bot_data:
del bot_data['uuid'] del bot_data['uuid']
@@ -99,7 +123,7 @@ class BotService:
session.using_conversation = None session.using_conversation = None
async def delete_bot(self, bot_uuid: str) -> None: async def delete_bot(self, bot_uuid: str) -> None:
"""Delete bot""" """删除机器人"""
await self.ap.platform_mgr.remove_bot(bot_uuid) await self.ap.platform_mgr.remove_bot(bot_uuid)
await self.ap.persistence_mgr.execute_async( await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid) sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)

View File

@@ -1,120 +0,0 @@
from __future__ import annotations
import uuid
import sqlalchemy
from ....core import app
from ....entity.persistence import rag as persistence_rag
class KnowledgeService:
"""知识库服务"""
ap: app.Application
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_knowledge_bases(self) -> list[dict]:
"""获取所有知识库"""
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
knowledge_bases = result.all()
return [
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
for knowledge_base in knowledge_bases
]
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
"""获取知识库"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
knowledge_base = result.first()
if knowledge_base is None:
return None
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
async def create_knowledge_base(self, kb_data: dict) -> str:
"""创建知识库"""
kb_data['uuid'] = str(uuid.uuid4())
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
kb = await self.get_knowledge_base(kb_data['uuid'])
await self.ap.rag_mgr.load_knowledge_base(kb)
return kb_data['uuid']
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
"""更新知识库"""
if 'uuid' in kb_data:
del kb_data['uuid']
if 'embedding_model_uuid' in kb_data:
del kb_data['embedding_model_uuid']
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_rag.KnowledgeBase)
.values(kb_data)
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
kb = await self.get_knowledge_base(kb_uuid)
await self.ap.rag_mgr.load_knowledge_base(kb)
async def store_file(self, kb_uuid: str, file_id: str) -> int:
"""存储文件"""
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
# await self.ap.rag_mgr.store_file(file_id)
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
return await runtime_kb.store_file(file_id)
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
"""检索知识库"""
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
return [
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
]
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
"""获取知识库文件"""
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
)
files = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_rag.File, file) for file in files]
async def delete_file(self, kb_uuid: str, file_id: str) -> None:
"""删除文件"""
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
if runtime_kb is None:
raise Exception('Knowledge base not found')
await runtime_kb.delete_file(file_id)
async def delete_knowledge_base(self, kb_uuid: str) -> None:
"""删除知识库"""
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
)
# delete files
files = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
)
for file in files:
# delete chunks
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_rag.Chunk).where(persistence_rag.Chunk.file_id == file.uuid)
)
# delete file
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
)

View File

@@ -7,20 +7,28 @@ from ....core import app
from ....entity.persistence import model as persistence_model from ....entity.persistence import model as persistence_model
from ....entity.persistence import pipeline as persistence_pipeline from ....entity.persistence import pipeline as persistence_pipeline
from ....provider.modelmgr import requester as model_requester from ....provider.modelmgr import requester as model_requester
from ....provider import entities as llm_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message
class LLMModelsService: class ModelsService:
ap: app.Application ap: app.Application
def __init__(self, ap: app.Application) -> None: def __init__(self, ap: app.Application) -> None:
self.ap = ap self.ap = ap
async def get_llm_models(self) -> list[dict]: async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel)) result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
models = result.all() models = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models]
masked_columns = []
if not include_secret:
masked_columns = ['api_keys']
return [
self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns)
for model in models
]
async def create_llm_model(self, model_data: dict) -> str: async def create_llm_model(self, model_data: dict) -> str:
model_data['uuid'] = str(uuid.uuid4()) model_data['uuid'] = str(uuid.uuid4())
@@ -99,93 +107,7 @@ class LLMModelsService:
await runtime_llm_model.requester.invoke_llm( await runtime_llm_model.requester.invoke_llm(
query=None, query=None,
model=runtime_llm_model, model=runtime_llm_model,
messages=[llm_entities.Message(role='user', content='Hello, world!')], messages=[provider_message.Message(role='user', content='Hello, world!')],
funcs=[], funcs=[],
extra_args=model_data.get('extra_args', {}),
)
class EmbeddingModelsService:
ap: app.Application
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_embedding_models(self) -> list[dict]:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
models = result.all()
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model) for model in models]
async def create_embedding_model(self, model_data: dict) -> str:
model_data['uuid'] = str(uuid.uuid4())
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_model.EmbeddingModel).values(**model_data)
)
embedding_model = await self.get_embedding_model(model_data['uuid'])
await self.ap.model_mgr.load_embedding_model(embedding_model)
return model_data['uuid']
async def get_embedding_model(self, model_uuid: str) -> dict | None:
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.uuid == model_uuid
)
)
model = result.first()
if model is None:
return None
return self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
async def update_embedding_model(self, model_uuid: str, model_data: dict) -> None:
if 'uuid' in model_data:
del model_data['uuid']
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_model.EmbeddingModel)
.where(persistence_model.EmbeddingModel.uuid == model_uuid)
.values(**model_data)
)
await self.ap.model_mgr.remove_embedding_model(model_uuid)
embedding_model = await self.get_embedding_model(model_uuid)
await self.ap.model_mgr.load_embedding_model(embedding_model)
async def delete_embedding_model(self, model_uuid: str) -> None:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.delete(persistence_model.EmbeddingModel).where(
persistence_model.EmbeddingModel.uuid == model_uuid
)
)
await self.ap.model_mgr.remove_embedding_model(model_uuid)
async def test_embedding_model(self, model_uuid: str, model_data: dict) -> None:
runtime_embedding_model: model_requester.RuntimeEmbeddingModel | None = None
if model_uuid != '_':
for model in self.ap.model_mgr.embedding_models:
if model.model_entity.uuid == model_uuid:
runtime_embedding_model = model
break
if runtime_embedding_model is None:
raise Exception('model not found')
else:
runtime_embedding_model = await self.ap.model_mgr.init_runtime_embedding_model(model_data)
await runtime_embedding_model.requester.invoke_embedding(
model=runtime_embedding_model,
input_text=['Hello, world!'],
extra_args={}, extra_args={},
) )

View File

@@ -38,21 +38,9 @@ class PipelineService:
self.ap.pipeline_config_meta_output.data, self.ap.pipeline_config_meta_output.data,
] ]
async def get_pipelines(self, sort_by: str = 'created_at', sort_order: str = 'DESC') -> list[dict]: async def get_pipelines(self) -> list[dict]:
query = sqlalchemy.select(persistence_pipeline.LegacyPipeline) result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
if sort_by == 'created_at':
if sort_order == 'DESC':
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.desc())
else:
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.asc())
elif sort_by == 'updated_at':
if sort_order == 'DESC':
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc())
else:
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.asc())
result = await self.ap.persistence_mgr.execute_async(query)
pipelines = result.all() pipelines = result.all()
return [ return [
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)

View File

@@ -73,27 +73,3 @@ class UserService:
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret'] jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user'] return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
async def reset_password(self, user_email: str, new_password: str) -> None:
ph = argon2.PasswordHasher()
hashed_password = ph.hash(new_password)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
)
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
ph = argon2.PasswordHasher()
user_obj = await self.get_user_by_email(user_email)
if user_obj is None:
raise ValueError('User not found')
ph.verify(user_obj.password, current_password)
hashed_password = ph.hash(new_password)
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
)

View File

@@ -2,9 +2,12 @@ from __future__ import annotations
import typing import typing
from ..core import app, entities as core_entities from ..core import app
from . import entities, operator, errors from . import operator
from ..utils import importutil from ..utils import importutil
import langbot_plugin.api.entities.builtin.provider.session as provider_session
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
# 引入所有算子以便注册 # 引入所有算子以便注册
from . import operators from . import operators
@@ -13,13 +16,11 @@ importutil.import_modules_in_pkg(operators)
class CommandManager: class CommandManager:
"""命令管理器"""
ap: app.Application ap: app.Application
cmd_list: list[operator.CommandOperator] cmd_list: list[operator.CommandOperator]
""" """
运行时命令列表,扁平存储,各个对象包含对应的子节点引用 Runtime command list, flat storage, each object contains a reference to the corresponding child node
""" """
def __init__(self, ap: app.Application): def __init__(self, ap: app.Application):
@@ -55,43 +56,28 @@ class CommandManager:
async def _execute( async def _execute(
self, self,
context: entities.ExecuteContext, context: command_context.ExecuteContext,
operator_list: list[operator.CommandOperator], operator_list: list[operator.CommandOperator],
operator: operator.CommandOperator = None, operator: operator.CommandOperator = None,
) -> typing.AsyncGenerator[entities.CommandReturn, None]: ) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""执行命令""" """执行命令"""
found = False command_list = await self.ap.plugin_connector.list_commands()
if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名
for oper in operator_list:
if (context.crt_params[0] == oper.name or context.crt_params[0] in oper.alias) and (
oper.parent_class is None or oper.parent_class == operator.__class__
):
found = True
context.crt_command = context.crt_params[0] for command in command_list:
context.crt_params = context.crt_params[1:] if command.metadata.name == context.command:
async for ret in self.ap.plugin_connector.execute_command(context):
async for ret in self._execute(context, oper.children, oper): yield ret
yield ret break
break else:
yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(context.command))
if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错
if operator is None:
yield entities.CommandReturn(error=errors.CommandNotFoundError(context.crt_params[0]))
else:
if operator.lowest_privilege > context.privilege:
yield entities.CommandReturn(error=errors.CommandPrivilegeError(operator.name))
else:
async for ret in operator.execute(context):
yield ret
async def execute( async def execute(
self, self,
command_text: str, command_text: str,
query: core_entities.Query, query: pipeline_query.Query,
session: core_entities.Session, session: provider_session.Session,
) -> typing.AsyncGenerator[entities.CommandReturn, None]: ) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""执行命令""" """执行命令"""
privilege = 1 privilege = 1
@@ -99,8 +85,8 @@ class CommandManager:
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']: if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
privilege = 2 privilege = 2
ctx = entities.ExecuteContext( ctx = command_context.ExecuteContext(
query=query, query_id=query.query_id,
session=session, session=session,
command_text=command_text, command_text=command_text,
command='', command='',
@@ -110,5 +96,9 @@ class CommandManager:
privilege=privilege, privilege=privilege,
) )
ctx.command = ctx.params[0]
ctx.shift()
async for ret in self._execute(ctx, self.cmd_list): async for ret in self._execute(ctx, self.cmd_list):
yield ret yield ret

View File

@@ -1,74 +0,0 @@
from __future__ import annotations
import typing
import pydantic.v1 as pydantic
from ..core import entities as core_entities
from . import errors
from ..platform.types import message as platform_message
class CommandReturn(pydantic.BaseModel):
"""命令返回值"""
text: typing.Optional[str] = None
"""文本
"""
image: typing.Optional[platform_message.Image] = None
"""弃用"""
image_url: typing.Optional[str] = None
"""图片链接
"""
error: typing.Optional[errors.CommandError] = None
"""错误
"""
class Config:
arbitrary_types_allowed = True
class ExecuteContext(pydantic.BaseModel):
"""单次命令执行上下文"""
query: core_entities.Query
"""本次消息的请求对象"""
session: core_entities.Session
"""本次消息所属的会话对象"""
command_text: str
"""命令完整文本"""
command: str
"""命令名称"""
crt_command: str
"""当前命令
多级命令中crt_command为当前命令command为根命令。
例如:!plugin on Webwlkr
处理到plugin时command为plugincrt_command为plugin
处理到on时command为plugincrt_command为on
"""
params: list[str]
"""命令参数
整个命令以空格分割后的参数列表
"""
crt_params: list[str]
"""当前命令参数
多级命令中crt_params为当前命令参数params为根命令参数。
例如:!plugin on Webwlkr
处理到plugin时params为['on', 'Webwlkr']crt_params为['on', 'Webwlkr']
处理到on时params为['on', 'Webwlkr']crt_params为['Webwlkr']
"""
privilege: int
"""发起人权限"""

View File

@@ -1,26 +0,0 @@
class CommandError(Exception):
def __init__(self, message: str = None):
self.message = message
def __str__(self):
return self.message
class CommandNotFoundError(CommandError):
def __init__(self, message: str = None):
super().__init__('未知命令: ' + message)
class CommandPrivilegeError(CommandError):
def __init__(self, message: str = None):
super().__init__('权限不足: ' + message)
class ParamNotEnoughError(CommandError):
def __init__(self, message: str = None):
super().__init__('参数不足: ' + message)
class CommandOperationError(CommandError):
def __init__(self, message: str = None):
super().__init__('操作失败: ' + message)

View File

@@ -4,7 +4,7 @@ import typing
import abc import abc
from ..core import app from ..core import app
from . import entities from langbot_plugin.api.entities.builtin.command import context as command_context
preregistered_operators: list[typing.Type[CommandOperator]] = [] preregistered_operators: list[typing.Type[CommandOperator]] = []
@@ -95,16 +95,18 @@ class CommandOperator(metaclass=abc.ABCMeta):
pass pass
@abc.abstractmethod @abc.abstractmethod
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""实现此方法以执行命令 """实现此方法以执行命令
支持多次yield以返回多个结果。 支持多次yield以返回多个结果。
例如:一个安装插件的命令,可能会有下载、解压、安装等多个步骤,每个步骤都可以返回一个结果。 例如:一个安装插件的命令,可能会有下载、解压、安装等多个步骤,每个步骤都可以返回一个结果。
Args: Args:
context (entities.ExecuteContext): 命令执行上下文 context (command_context.ExecuteContext): 命令执行上下文
Yields: Yields:
entities.CommandReturn: 命令返回封装 command_context.CommandReturn: 命令返回封装
""" """
pass pass

View File

@@ -2,14 +2,17 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>') @operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>')
class CmdOperator(operator.CommandOperator): class CmdOperator(operator.CommandOperator):
"""命令列表""" """命令列表"""
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""执行""" """执行"""
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
reply_str = '当前所有命令: \n\n' reply_str = '当前所有命令: \n\n'
@@ -20,7 +23,7 @@ class CmdOperator(operator.CommandOperator):
reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助' reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助'
yield entities.CommandReturn(text=reply_str.strip()) yield command_context.CommandReturn(text=reply_str.strip())
else: else:
cmd_name = context.crt_params[0] cmd_name = context.crt_params[0]
@@ -33,9 +36,9 @@ class CmdOperator(operator.CommandOperator):
break break
if cmd is None: if cmd is None:
yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name)) yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(cmd_name))
else: else:
reply_str = f'{cmd.name}: {cmd.help}\n\n' reply_str = f'{cmd.name}: {cmd.help}\n\n'
reply_str += f'使用方法: \n{cmd.usage}' reply_str += f'使用方法: \n{cmd.usage}'
yield entities.CommandReturn(text=reply_str.strip()) yield command_context.CommandReturn(text=reply_str.strip())

View File

@@ -2,23 +2,26 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all') @operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all')
class DelOperator(operator.CommandOperator): class DelOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if context.session.conversations: if context.session.conversations:
delete_index = 0 delete_index = 0
if len(context.crt_params) > 0: if len(context.crt_params) > 0:
try: try:
delete_index = int(context.crt_params[0]) delete_index = int(context.crt_params[0])
except Exception: except Exception:
yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('索引必须是整数'))
return return
if delete_index < 0 or delete_index >= len(context.session.conversations): if delete_index < 0 or delete_index >= len(context.session.conversations):
yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('索引超出范围'))
return return
# 倒序 # 倒序
@@ -29,15 +32,17 @@ class DelOperator(operator.CommandOperator):
del context.session.conversations[to_delete_index] del context.session.conversations[to_delete_index]
yield entities.CommandReturn(text=f'已删除对话: {delete_index}') yield command_context.CommandReturn(text=f'已删除对话: {delete_index}')
else: else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话'))
@operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator) @operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator)
class DelAllOperator(operator.CommandOperator): class DelAllOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
context.session.conversations = [] context.session.conversations = []
context.session.using_conversation = None context.session.using_conversation = None
yield entities.CommandReturn(text='已删除所有对话') yield command_context.CommandReturn(text='已删除所有对话')

View File

@@ -1,19 +1,20 @@
from __future__ import annotations from __future__ import annotations
from typing import AsyncGenerator from typing import AsyncGenerator
from .. import operator, entities from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context
@operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func') @operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func')
class FuncOperator(operator.CommandOperator): class FuncOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> AsyncGenerator[command_context.CommandReturn, None]:
reply_str = '当前已启用的内容函数: \n\n' reply_str = '当前已启用的内容函数: \n\n'
index = 1 index = 1
all_functions = await self.ap.tool_mgr.get_all_functions( all_functions = await self.ap.tool_mgr.get_all_tools()
plugin_enabled=True,
)
for func in all_functions: for func in all_functions:
reply_str += '{}. {}:\n{}\n\n'.format( reply_str += '{}. {}:\n{}\n\n'.format(
@@ -23,4 +24,4 @@ class FuncOperator(operator.CommandOperator):
) )
index += 1 index += 1
yield entities.CommandReturn(text=reply_str) yield command_context.CommandReturn(text=reply_str)

View File

@@ -2,14 +2,17 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context
@operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>') @operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>')
class HelpOperator(operator.CommandOperator): class HelpOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接https://langbot.app' help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接https://langbot.app'
help += '\n发送命令 !cmd 可查看命令列表' help += '\n发送命令 !cmd 可查看命令列表'
yield entities.CommandReturn(text=help) yield command_context.CommandReturn(text=help)

View File

@@ -3,26 +3,31 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='last', help='切换到前一个对话', usage='!last') @operator.operator_class(name='last', help='切换到前一个对话', usage='!last')
class LastOperator(operator.CommandOperator): class LastOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if context.session.conversations: if context.session.conversations:
# 找到当前会话的上一个会话 # 找到当前会话的上一个会话
for index in range(len(context.session.conversations) - 1, -1, -1): for index in range(len(context.session.conversations) - 1, -1, -1):
if context.session.conversations[index] == context.session.using_conversation: if context.session.conversations[index] == context.session.using_conversation:
if index == 0: if index == 0:
yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了')) yield command_context.CommandReturn(
error=command_errors.CommandOperationError('已经是第一个对话了')
)
return return
else: else:
context.session.using_conversation = context.session.conversations[index - 1] context.session.using_conversation = context.session.conversations[index - 1]
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S') time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
yield entities.CommandReturn( yield command_context.CommandReturn(
text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}' text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}'
) )
return return
else: else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话'))

View File

@@ -2,19 +2,22 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>') @operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>')
class ListOperator(operator.CommandOperator): class ListOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
page = 0 page = 0
if len(context.crt_params) > 0: if len(context.crt_params) > 0:
try: try:
page = int(context.crt_params[0] - 1) page = int(context.crt_params[0] - 1)
except Exception: except Exception:
yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('页码应为整数'))
return return
record_per_page = 10 record_per_page = 10
@@ -45,4 +48,4 @@ class ListOperator(operator.CommandOperator):
else: else:
content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}' content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}'
yield entities.CommandReturn(text=f'{page + 1} 页 (时间倒序):\n{content}') yield command_context.CommandReturn(text=f'{page + 1} 页 (时间倒序):\n{content}')

View File

@@ -2,26 +2,31 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='next', help='切换到后一个对话', usage='!next') @operator.operator_class(name='next', help='切换到后一个对话', usage='!next')
class NextOperator(operator.CommandOperator): class NextOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if context.session.conversations: if context.session.conversations:
# 找到当前会话的下一个会话 # 找到当前会话的下一个会话
for index in range(len(context.session.conversations)): for index in range(len(context.session.conversations)):
if context.session.conversations[index] == context.session.using_conversation: if context.session.conversations[index] == context.session.using_conversation:
if index == len(context.session.conversations) - 1: if index == len(context.session.conversations) - 1:
yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了')) yield command_context.CommandReturn(
error=command_errors.CommandOperationError('已经是最后一个对话了')
)
return return
else: else:
context.session.using_conversation = context.session.conversations[index + 1] context.session.using_conversation = context.session.conversations[index + 1]
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S') time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
yield entities.CommandReturn( yield command_context.CommandReturn(
text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}' text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}'
) )
return return
else: else:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话'))

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
import typing import typing
import traceback import traceback
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class( @operator.operator_class(
@@ -11,7 +12,9 @@ from .. import operator, entities, errors
usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>', usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>',
) )
class PluginOperator(operator.CommandOperator): class PluginOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
plugin_list = self.ap.plugin_mgr.plugins() plugin_list = self.ap.plugin_mgr.plugins()
reply_str = '所有插件({}):\n'.format(len(plugin_list)) reply_str = '所有插件({}):\n'.format(len(plugin_list))
idx = 0 idx = 0
@@ -27,32 +30,36 @@ class PluginOperator(operator.CommandOperator):
idx += 1 idx += 1
yield entities.CommandReturn(text=reply_str) yield command_context.CommandReturn(text=reply_str)
@operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator) @operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator)
class PluginGetOperator(operator.CommandOperator): class PluginGetOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址')) yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件仓库地址'))
else: else:
repo = context.crt_params[0] repo = context.crt_params[0]
yield entities.CommandReturn(text='正在安装插件...') yield command_context.CommandReturn(text='正在安装插件...')
try: try:
await self.ap.plugin_mgr.install_plugin(repo) await self.ap.plugin_mgr.install_plugin(repo)
yield entities.CommandReturn(text='插件安装成功,请重启程序以加载插件') yield command_context.CommandReturn(text='插件安装成功,请重启程序以加载插件')
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件安装失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件安装失败: ' + str(e)))
@operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator) @operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator)
class PluginUpdateOperator(operator.CommandOperator): class PluginUpdateOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称'))
else: else:
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
@@ -60,24 +67,26 @@ class PluginUpdateOperator(operator.CommandOperator):
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is not None: if plugin_container is not None:
yield entities.CommandReturn(text='正在更新插件...') yield command_context.CommandReturn(text='正在更新插件...')
await self.ap.plugin_mgr.update_plugin(plugin_name) await self.ap.plugin_mgr.update_plugin(plugin_name)
yield entities.CommandReturn(text='插件更新成功,请重启程序以加载插件') yield command_context.CommandReturn(text='插件更新成功,请重启程序以加载插件')
else: else:
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: 未找到插件')) yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: 未找到插件'))
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e)))
@operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator) @operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator)
class PluginUpdateAllOperator(operator.CommandOperator): class PluginUpdateAllOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
try: try:
plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()] plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()]
if plugins: if plugins:
yield entities.CommandReturn(text='正在更新插件...') yield command_context.CommandReturn(text='正在更新插件...')
updated = [] updated = []
try: try:
for plugin_name in plugins: for plugin_name in plugins:
@@ -85,20 +94,22 @@ class PluginUpdateAllOperator(operator.CommandOperator):
updated.append(plugin_name) updated.append(plugin_name)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e)))
yield entities.CommandReturn(text='已更新插件: {}'.format(', '.join(updated))) yield command_context.CommandReturn(text='已更新插件: {}'.format(', '.join(updated)))
else: else:
yield entities.CommandReturn(text='没有可更新的插件') yield command_context.CommandReturn(text='没有可更新的插件')
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件更新失败: ' + str(e)))
@operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator) @operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator)
class PluginDelOperator(operator.CommandOperator): class PluginDelOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称'))
else: else:
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
@@ -106,51 +117,55 @@ class PluginDelOperator(operator.CommandOperator):
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name) plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
if plugin_container is not None: if plugin_container is not None:
yield entities.CommandReturn(text='正在删除插件...') yield command_context.CommandReturn(text='正在删除插件...')
await self.ap.plugin_mgr.uninstall_plugin(plugin_name) await self.ap.plugin_mgr.uninstall_plugin(plugin_name)
yield entities.CommandReturn(text='插件删除成功,请重启程序以加载插件') yield command_context.CommandReturn(text='插件删除成功,请重启程序以加载插件')
else: else:
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: 未找到插件')) yield command_context.CommandReturn(error=command_errors.CommandError('插件删除失败: 未找到插件'))
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件删除失败: ' + str(e)))
@operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator) @operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator)
class PluginEnableOperator(operator.CommandOperator): class PluginEnableOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称'))
else: else:
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
try: try:
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True): if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True):
yield entities.CommandReturn(text='已启用插件: {}'.format(plugin_name)) yield command_context.CommandReturn(text='已启用插件: {}'.format(plugin_name))
else: else:
yield entities.CommandReturn( yield command_context.CommandReturn(
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) error=command_errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
) )
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件状态修改失败: ' + str(e)))
@operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator) @operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator)
class PluginDisableOperator(operator.CommandOperator): class PluginDisableOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
if len(context.crt_params) == 0: if len(context.crt_params) == 0:
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称')) yield command_context.CommandReturn(error=command_errors.ParamNotEnoughError('请提供插件名称'))
else: else:
plugin_name = context.crt_params[0] plugin_name = context.crt_params[0]
try: try:
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False): if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False):
yield entities.CommandReturn(text='已禁用插件: {}'.format(plugin_name)) yield command_context.CommandReturn(text='已禁用插件: {}'.format(plugin_name))
else: else:
yield entities.CommandReturn( yield command_context.CommandReturn(
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name)) error=command_errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
) )
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e))) yield command_context.CommandReturn(error=command_errors.CommandError('插件状态修改失败: ' + str(e)))

View File

@@ -2,19 +2,22 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt') @operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt')
class PromptOperator(operator.CommandOperator): class PromptOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""执行""" """执行"""
if context.session.using_conversation is None: if context.session.using_conversation is None:
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话')) yield command_context.CommandReturn(error=command_errors.CommandOperationError('当前没有对话'))
else: else:
reply_str = '当前对话所有内容:\n\n' reply_str = '当前对话所有内容:\n\n'
for msg in context.session.using_conversation.messages: for msg in context.session.using_conversation.messages:
reply_str += f'{msg.role}: {msg.content}\n' reply_str += f'{msg.role}: {msg.content}\n'
yield entities.CommandReturn(text=reply_str) yield command_context.CommandReturn(text=reply_str)

View File

@@ -2,15 +2,18 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities, errors from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
@operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend') @operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend')
class ResendOperator(operator.CommandOperator): class ResendOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
# 回滚到最后一条用户message前 # 回滚到最后一条用户message前
if context.session.using_conversation is None: if context.session.using_conversation is None:
yield entities.CommandReturn(error=errors.CommandError('当前没有对话')) yield command_context.CommandReturn(error=command_errors.CommandError('当前没有对话'))
else: else:
conv_msg = context.session.using_conversation.messages conv_msg = context.session.using_conversation.messages
@@ -23,4 +26,4 @@ class ResendOperator(operator.CommandOperator):
conv_msg.pop() conv_msg.pop()
# 不重发了,提示用户已删除就行了 # 不重发了,提示用户已删除就行了
yield entities.CommandReturn(text='已删除最后一次请求记录') yield command_context.CommandReturn(text='已删除最后一次请求记录')

View File

@@ -2,13 +2,16 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context
@operator.operator_class(name='reset', help='重置当前会话', usage='!reset') @operator.operator_class(name='reset', help='重置当前会话', usage='!reset')
class ResetOperator(operator.CommandOperator): class ResetOperator(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
"""执行""" """执行"""
context.session.using_conversation = None context.session.using_conversation = None
yield entities.CommandReturn(text='已重置当前会话') yield command_context.CommandReturn(text='已重置当前会话')

View File

@@ -1,11 +0,0 @@
from __future__ import annotations
import typing
from .. import operator, entities
@operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2)
class UpdateCommand(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
yield entities.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。')

View File

@@ -2,12 +2,15 @@ from __future__ import annotations
import typing import typing
from .. import operator, entities from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context
@operator.operator_class(name='version', help='显示版本信息', usage='!version') @operator.operator_class(name='version', help='显示版本信息', usage='!version')
class VersionCommand(operator.CommandOperator): class VersionCommand(operator.CommandOperator):
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]: async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}' reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}'
try: try:
@@ -16,4 +19,4 @@ class VersionCommand(operator.CommandOperator):
except Exception: except Exception:
pass pass
yield entities.CommandReturn(text=reply_str.strip()) yield command_context.CommandReturn(text=reply_str.strip())

View File

@@ -6,7 +6,7 @@ from .. import model as file_model
class JSONConfigFile(file_model.ConfigFile): class JSONConfigFile(file_model.ConfigFile):
"""JSON config file""" """JSON配置文件"""
def __init__( def __init__(
self, self,
@@ -42,7 +42,7 @@ class JSONConfigFile(file_model.ConfigFile):
try: try:
cfg = json.load(f) cfg = json.load(f)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise Exception(f'Syntax error in config file {self.config_file_name}: {e}') raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}')
if completion: if completion:
for key in self.template_data: for key in self.template_data:

View File

@@ -7,13 +7,13 @@ from .. import model as file_model
class PythonModuleConfigFile(file_model.ConfigFile): class PythonModuleConfigFile(file_model.ConfigFile):
"""Python module config file""" """Python模块配置文件"""
config_file_name: str = None config_file_name: str = None
"""Config file name""" """配置文件名"""
template_file_name: str = None template_file_name: str = None
"""Template file name""" """模板文件名"""
def __init__(self, config_file_name: str, template_file_name: str) -> None: def __init__(self, config_file_name: str, template_file_name: str) -> None:
self.config_file_name = config_file_name self.config_file_name = config_file_name
@@ -42,7 +42,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
cfg[key] = getattr(module, key) cfg[key] = getattr(module, key)
# complete from template module file # 从模板模块文件中进行补全
if completion: if completion:
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0] module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -60,7 +60,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
return cfg return cfg
async def save(self, data: dict): async def save(self, data: dict):
logging.warning('Python module config file does not support saving') logging.warning('Python模块配置文件不支持保存')
def save_sync(self, data: dict): def save_sync(self, data: dict):
logging.warning('Python module config file does not support saving') logging.warning('Python模块配置文件不支持保存')

View File

@@ -6,7 +6,7 @@ from .. import model as file_model
class YAMLConfigFile(file_model.ConfigFile): class YAMLConfigFile(file_model.ConfigFile):
"""YAML config file""" """YAML配置文件"""
def __init__( def __init__(
self, self,
@@ -42,7 +42,7 @@ class YAMLConfigFile(file_model.ConfigFile):
try: try:
cfg = yaml.load(f, Loader=yaml.FullLoader) cfg = yaml.load(f, Loader=yaml.FullLoader)
except yaml.YAMLError as e: except yaml.YAMLError as e:
raise Exception(f'Syntax error in config file {self.config_file_name}: {e}') raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}')
if completion: if completion:
for key in self.template_data: for key in self.template_data:

View File

@@ -5,27 +5,27 @@ from .impls import pymodule, json as json_file, yaml as yaml_file
class ConfigManager: class ConfigManager:
"""Config file manager""" """配置文件管理器"""
name: str = None name: str = None
"""Config manager name""" """配置管理器名"""
description: str = None description: str = None
"""Config manager description""" """配置管理器描述"""
schema: dict = None schema: dict = None
"""Config file schema """配置文件 schema
Must conform to JSON Schema Draft 7 specification 需要符合 JSON Schema Draft 7 规范
""" """
file: file_model.ConfigFile = None file: file_model.ConfigFile = None
"""Config file instance""" """配置文件实例"""
data: dict = None data: dict = None
"""Config data""" """配置数据"""
doc_link: str = None doc_link: str = None
"""Config file documentation link""" """配置文件文档链接"""
def __init__(self, cfg_file: file_model.ConfigFile) -> None: def __init__(self, cfg_file: file_model.ConfigFile) -> None:
self.file = cfg_file self.file = cfg_file
@@ -42,15 +42,15 @@ class ConfigManager:
async def load_python_module_config(config_name: str, template_name: str, completion: bool = True) -> ConfigManager: async def load_python_module_config(config_name: str, template_name: str, completion: bool = True) -> ConfigManager:
"""Load Python module config file """加载Python模块配置文件
Args: Args:
config_name (str): Config file name config_name (str): 配置文件名
template_name (str): Template file name template_name (str): 模板文件名
completion (bool): Whether to automatically complete the config file in memory completion (bool): 是否自动补全内存中的配置文件
Returns: Returns:
ConfigManager: Config file manager ConfigManager: 配置文件管理器
""" """
cfg_inst = pymodule.PythonModuleConfigFile(config_name, template_name) cfg_inst = pymodule.PythonModuleConfigFile(config_name, template_name)
@@ -66,13 +66,13 @@ async def load_json_config(
template_data: dict = None, template_data: dict = None,
completion: bool = True, completion: bool = True,
) -> ConfigManager: ) -> ConfigManager:
"""Load JSON config file """加载JSON配置文件
Args: Args:
config_name (str): Config file name config_name (str): 配置文件名
template_name (str): Template file name template_name (str): 模板文件名
template_data (dict): Template data template_data (dict): 模板数据
completion (bool): Whether to automatically complete the config file in memory completion (bool): 是否自动补全内存中的配置文件
""" """
cfg_inst = json_file.JSONConfigFile(config_name, template_name, template_data) cfg_inst = json_file.JSONConfigFile(config_name, template_name, template_data)
@@ -88,16 +88,16 @@ async def load_yaml_config(
template_data: dict = None, template_data: dict = None,
completion: bool = True, completion: bool = True,
) -> ConfigManager: ) -> ConfigManager:
"""Load YAML config file """加载YAML配置文件
Args: Args:
config_name (str): Config file name config_name (str): 配置文件名
template_name (str): Template file name template_name (str): 模板文件名
template_data (dict): Template data template_data (dict): 模板数据
completion (bool): Whether to automatically complete the config file in memory completion (bool): 是否自动补全内存中的配置文件
Returns: Returns:
ConfigManager: Config file manager ConfigManager: 配置文件管理器
""" """
cfg_inst = yaml_file.YAMLConfigFile(config_name, template_name, template_data) cfg_inst = yaml_file.YAMLConfigFile(config_name, template_name, template_data)

View File

@@ -2,16 +2,16 @@ import abc
class ConfigFile(metaclass=abc.ABCMeta): class ConfigFile(metaclass=abc.ABCMeta):
"""Config file abstract class""" """配置文件抽象类"""
config_file_name: str = None config_file_name: str = None
"""Config file name""" """配置文件名"""
template_file_name: str = None template_file_name: str = None
"""Template file name""" """模板文件名"""
template_data: dict = None template_data: dict = None
"""Template data""" """模板数据"""
@abc.abstractmethod @abc.abstractmethod
def exists(self) -> bool: def exists(self) -> bool:

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import logging import logging
import asyncio import asyncio
import traceback import traceback
import sys
import os import os
from ..platform import botmgr as im_mgr from ..platform import botmgr as im_mgr
@@ -12,7 +11,7 @@ from ..provider.modelmgr import modelmgr as llm_model_mgr
from ..provider.tools import toolmgr as llm_tool_mgr from ..provider.tools import toolmgr as llm_tool_mgr
from ..config import manager as config_mgr from ..config import manager as config_mgr
from ..command import cmdmgr from ..command import cmdmgr
from ..plugin import manager as plugin_mgr from ..plugin import connector as plugin_connector
from ..pipeline import pool from ..pipeline import pool
from ..pipeline import controller, pipelinemgr from ..pipeline import controller, pipelinemgr
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
@@ -22,18 +21,15 @@ from ..api.http.service import user as user_service
from ..api.http.service import model as model_service from ..api.http.service import model as model_service
from ..api.http.service import pipeline as pipeline_service from ..api.http.service import pipeline as pipeline_service
from ..api.http.service import bot as bot_service from ..api.http.service import bot as bot_service
from ..api.http.service import knowledge as knowledge_service
from ..discover import engine as discover_engine from ..discover import engine as discover_engine
from ..storage import mgr as storagemgr from ..storage import mgr as storagemgr
from ..utils import logcache from ..utils import logcache
from . import taskmgr from . import taskmgr
from . import entities as core_entities from . import entities as core_entities
from ..rag.knowledge import kbmgr as rag_mgr
from ..vector import mgr as vectordb_mgr
class Application: class Application:
"""Runtime application object and context""" """运行时应用对象和上下文"""
event_loop: asyncio.AbstractEventLoop = None event_loop: asyncio.AbstractEventLoop = None
@@ -50,12 +46,10 @@ class Application:
model_mgr: llm_model_mgr.ModelManager = None model_mgr: llm_model_mgr.ModelManager = None
rag_mgr: rag_mgr.RAGManager = None # TODO 移动到 pipeline 里
# TODO move to pipeline
tool_mgr: llm_tool_mgr.ToolManager = None tool_mgr: llm_tool_mgr.ToolManager = None
# ======= Config manager ======= # ======= 配置管理器 =======
command_cfg: config_mgr.ConfigManager = None # deprecated command_cfg: config_mgr.ConfigManager = None # deprecated
@@ -69,7 +63,7 @@ class Application:
instance_config: config_mgr.ConfigManager = None instance_config: config_mgr.ConfigManager = None
# ======= Metadata config manager ======= # ======= 元数据配置管理器 =======
sensitive_meta: config_mgr.ConfigManager = None sensitive_meta: config_mgr.ConfigManager = None
@@ -80,7 +74,7 @@ class Application:
# ========================= # =========================
plugin_mgr: plugin_mgr.PluginManager = None plugin_connector: plugin_connector.PluginRuntimeConnector = None
query_pool: pool.QueryPool = None query_pool: pool.QueryPool = None
@@ -98,8 +92,6 @@ class Application:
persistence_mgr: persistencemgr.PersistenceManager = None persistence_mgr: persistencemgr.PersistenceManager = None
vector_db_mgr: vectordb_mgr.VectorDBManager = None
http_ctrl: http_controller.HTTPController = None http_ctrl: http_controller.HTTPController = None
log_cache: logcache.LogCache = None log_cache: logcache.LogCache = None
@@ -110,16 +102,12 @@ class Application:
user_service: user_service.UserService = None user_service: user_service.UserService = None
llm_model_service: model_service.LLMModelsService = None model_service: model_service.ModelsService = None
embedding_models_service: model_service.EmbeddingModelsService = None
pipeline_service: pipeline_service.PipelineService = None pipeline_service: pipeline_service.PipelineService = None
bot_service: bot_service.BotService = None bot_service: bot_service.BotService = None
knowledge_service: knowledge_service.KnowledgeService = None
def __init__(self): def __init__(self):
pass pass
@@ -128,7 +116,7 @@ class Application:
async def run(self): async def run(self):
try: try:
await self.plugin_mgr.initialize_plugins() await self.plugin_connector.initialize_plugins()
# 后续可能会允许动态重启其他任务 # 后续可能会允许动态重启其他任务
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程 # 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
@@ -154,7 +142,6 @@ class Application:
name='http-api-controller', name='http-api-controller',
scopes=[core_entities.LifecycleControlScope.APPLICATION], scopes=[core_entities.LifecycleControlScope.APPLICATION],
) )
self.task_mgr.create_task( self.task_mgr.create_task(
never_ending(), never_ending(),
name='never-ending-task', name='never-ending-task',
@@ -166,11 +153,14 @@ class Application:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: except Exception as e:
self.logger.error(f'Application runtime fatal exception: {e}') self.logger.error(f'应用运行致命异常: {e}')
self.logger.debug(f'Traceback: {traceback.format_exc()}') self.logger.debug(f'Traceback: {traceback.format_exc()}')
def dispose(self):
self.plugin_connector.dispose()
async def print_web_access_info(self): async def print_web_access_info(self):
"""Print access webui tips""" """打印访问 webui 的提示"""
if not os.path.exists(os.path.join('.', 'web/out')): if not os.path.exists(os.path.join('.', 'web/out')):
self.logger.warning('WebUI 文件缺失请根据文档部署https://docs.langbot.app/zh') self.logger.warning('WebUI 文件缺失请根据文档部署https://docs.langbot.app/zh')
@@ -195,59 +185,3 @@ class Application:
""".strip() """.strip()
for line in tips.split('\n'): for line in tips.split('\n'):
self.logger.info(line) self.logger.info(line)
async def reload(
self,
scope: core_entities.LifecycleControlScope,
):
match scope:
case core_entities.LifecycleControlScope.PLATFORM.value:
self.logger.info('Hot reload scope=' + scope)
await self.platform_mgr.shutdown()
self.platform_mgr = im_mgr.PlatformManager(self)
await self.platform_mgr.initialize()
self.task_mgr.create_task(
self.platform_mgr.run(),
name='platform-manager',
scopes=[
core_entities.LifecycleControlScope.APPLICATION,
core_entities.LifecycleControlScope.PLATFORM,
],
)
case core_entities.LifecycleControlScope.PLUGIN.value:
self.logger.info('Hot reload scope=' + scope)
await self.plugin_mgr.destroy_plugins()
# 删除 sys.module 中所有的 plugins/* 下的模块
for mod in list(sys.modules.keys()):
if mod.startswith('plugins.'):
del sys.modules[mod]
self.plugin_mgr = plugin_mgr.PluginManager(self)
await self.plugin_mgr.initialize()
await self.plugin_mgr.initialize_plugins()
await self.plugin_mgr.load_plugins()
await self.plugin_mgr.initialize_plugins()
case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info('Hot reload scope=' + scope)
await self.tool_mgr.shutdown()
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
await llm_model_mgr_inst.initialize()
self.model_mgr = llm_model_mgr_inst
llm_session_mgr_inst = llm_session_mgr.SessionManager(self)
await llm_session_mgr_inst.initialize()
self.sess_mgr = llm_session_mgr_inst
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self)
await llm_tool_mgr_inst.initialize()
self.tool_mgr = llm_tool_mgr_inst
case _:
pass

View File

@@ -8,7 +8,7 @@ from . import app
from . import stage from . import stage
from ..utils import constants, importutil from ..utils import constants, importutil
# Import startup stage implementation to register # 引入启动阶段实现以便注册
from . import stages from . import stages
importutil.import_modules_in_pkg(stages) importutil.import_modules_in_pkg(stages)
@@ -25,7 +25,7 @@ stage_order = [
async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application: async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
# Determine if it is debug mode # 确定是否为调试模式
if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['true', '1']: if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['true', '1']:
constants.debug_mode = True constants.debug_mode = True
@@ -33,7 +33,7 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
ap.event_loop = loop ap.event_loop = loop
# Execute startup stage # 执行启动阶段
for stage_name in stage_order: for stage_name in stage_order:
stage_cls = stage.preregistered_stages[stage_name] stage_cls = stage.preregistered_stages[stage_name]
stage_inst = stage_cls() stage_inst = stage_cls()
@@ -47,12 +47,12 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
async def main(loop: asyncio.AbstractEventLoop): async def main(loop: asyncio.AbstractEventLoop):
try: try:
# Hang system signal processing # 挂系统信号处理
import signal import signal
def signal_handler(sig, frame): def signal_handler(sig, frame):
print('[Signal] Program exit.') app_inst.dispose()
# ap.shutdown() print('[Signal] 程序退出.')
os._exit(0) os._exit(0)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)

View File

@@ -2,8 +2,8 @@ import pip
import os import os
from ...utils import pkgmgr from ...utils import pkgmgr
# Check dependencies to prevent users from not installing # 检查依赖,防止用户未安装
# Left is the import name, right is the dependency name # 左边为引入名称,右边为依赖名称
required_deps = { required_deps = {
'requests': 'requests', 'requests': 'requests',
'openai': 'openai', 'openai': 'openai',
@@ -65,7 +65,7 @@ async def install_deps(deps: list[str]):
async def precheck_plugin_deps(): async def precheck_plugin_deps():
print('[Startup] Prechecking plugin dependencies...') print('[Startup] Prechecking plugin dependencies...')
# Only execute plugin dependency installation when the plugins directory exists # 只有在plugins目录存在时才执行插件依赖安装
if os.path.exists('plugins'): if os.path.exists('plugins'):
for dir in os.listdir('plugins'): for dir in os.listdir('plugins'):
subdir = os.path.join('plugins', dir) subdir = os.path.join('plugins', dir)

View File

@@ -17,7 +17,7 @@ log_colors_config = {
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger: async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
# Remove all existing loggers # 删除所有现有的logger
for handler in logging.root.handlers[:]: for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler) logging.root.removeHandler(handler)
@@ -54,13 +54,13 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.
handler.setFormatter(color_formatter) handler.setFormatter(color_formatter)
qcg_logger.addHandler(handler) qcg_logger.addHandler(handler)
qcg_logger.debug('Logging initialized, log level: %s' % level) qcg_logger.debug('日志初始化完成,日志级别:%s' % level)
logging.basicConfig( logging.basicConfig(
level=logging.CRITICAL, # Set log output format level=logging.CRITICAL, # 设置日志输出格式
format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s', format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s',
# Log output format # 日志输出的格式
# -8 is a placeholder, left-align the output, and output length is 8 # -8表示占位符让输出左对齐输出长度都为8位
datefmt='%Y-%m-%d %H:%M:%S', # Time output format datefmt='%Y-%m-%d %H:%M:%S', # 时间输出的格式
handlers=[logging.NullHandler()], handlers=[logging.NullHandler()],
) )

View File

@@ -1,18 +1,6 @@
from __future__ import annotations from __future__ import annotations
import enum import enum
import typing
import datetime
import asyncio
import pydantic.v1 as pydantic
from ..provider import entities as llm_entities
from ..provider.modelmgr import requester
from ..provider.tools import entities as tools_entities
from ..platform import adapter as msadapter
from ..platform.types import message as platform_message
from ..platform.types import events as platform_events
class LifecycleControlScope(enum.Enum): class LifecycleControlScope(enum.Enum):
@@ -20,159 +8,3 @@ class LifecycleControlScope(enum.Enum):
PLATFORM = 'platform' PLATFORM = 'platform'
PLUGIN = 'plugin' PLUGIN = 'plugin'
PROVIDER = 'provider' PROVIDER = 'provider'
class LauncherTypes(enum.Enum):
"""一个请求的发起者类型"""
PERSON = 'person'
"""私聊"""
GROUP = 'group'
"""群聊"""
class Query(pydantic.BaseModel):
"""一次请求的信息封装"""
query_id: int
"""请求ID添加进请求池时生成"""
launcher_type: LauncherTypes
"""会话类型platform处理阶段设置"""
launcher_id: typing.Union[int, str]
"""会话IDplatform处理阶段设置"""
sender_id: typing.Union[int, str]
"""发送者IDplatform处理阶段设置"""
message_event: platform_events.MessageEvent
"""事件platform收到的原始事件"""
message_chain: platform_message.MessageChain
"""消息链platform收到的原始消息链"""
bot_uuid: typing.Optional[str] = None
"""机器人UUID。"""
pipeline_uuid: typing.Optional[str] = None
"""流水线UUID。"""
pipeline_config: typing.Optional[dict[str, typing.Any]] = None
"""流水线配置,由 Pipeline 在运行开始时设置。"""
adapter: msadapter.MessagePlatformAdapter
"""消息平台适配器对象单个app中可能启用了多个消息平台适配器此对象表明发起此query的适配器"""
session: typing.Optional[Session] = None
"""会话对象,由前置处理器阶段设置"""
messages: typing.Optional[list[llm_entities.Message]] = []
"""历史消息列表,由前置处理器阶段设置"""
prompt: typing.Optional[llm_entities.Prompt] = None
"""情景预设内容,由前置处理器阶段设置"""
user_message: typing.Optional[llm_entities.Message] = None
"""此次请求的用户消息对象,由前置处理器阶段设置"""
variables: typing.Optional[dict[str, typing.Any]] = None
"""变量由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。"""
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
"""使用的对话模型,由前置处理器阶段设置"""
use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None
"""使用的函数,由前置处理器阶段设置"""
resp_messages: (
typing.Optional[list[llm_entities.Message]]
| typing.Optional[list[platform_message.MessageChain]]
| typing.Optional[list[llm_entities.MessageChunk]]
) = []
"""由Process阶段生成的回复消息对象列表"""
resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None
"""回复消息链从resp_messages包装而得"""
# ======= 内部保留 =======
current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None
"""当前所处阶段"""
class Config:
arbitrary_types_allowed = True
# ========== 插件可调用的 API请求 API ==========
def set_variable(self, key: str, value: typing.Any):
"""设置变量"""
if self.variables is None:
self.variables = {}
self.variables[key] = value
def get_variable(self, key: str) -> typing.Any:
"""获取变量"""
if self.variables is None:
return None
return self.variables.get(key)
def get_variables(self) -> dict[str, typing.Any]:
"""获取所有变量"""
if self.variables is None:
return {}
return self.variables
class Conversation(pydantic.BaseModel):
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation但只有一个当前使用的 Conversation"""
prompt: llm_entities.Prompt
messages: list[llm_entities.Message]
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
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。"""
class Config:
arbitrary_types_allowed = True
class Session(pydantic.BaseModel):
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
launcher_type: LauncherTypes
launcher_id: typing.Union[int, str]
sender_id: typing.Optional[typing.Union[int, str]] = 0
use_prompt_name: typing.Optional[str] = 'default'
using_conversation: typing.Optional[Conversation] = None
conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list)
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
semaphore: typing.Optional[asyncio.Semaphore] = None
"""当前会话的信号量,用于限制并发"""
class Config:
arbitrary_types_allowed = True

View File

@@ -7,11 +7,11 @@ from . import app
preregistered_migrations: list[typing.Type[Migration]] = [] preregistered_migrations: list[typing.Type[Migration]] = []
"""Currently not supported for extension""" """当前阶段暂不支持扩展"""
def migration_class(name: str, number: int): def migration_class(name: str, number: int):
"""Register a migration""" """注册一个迁移"""
def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]: def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]:
cls.name = name cls.name = name
@@ -23,7 +23,7 @@ def migration_class(name: str, number: int):
class Migration(abc.ABC): class Migration(abc.ABC):
"""A version migration""" """一个版本的迁移"""
name: str name: str
@@ -36,10 +36,10 @@ class Migration(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
async def need_migrate(self) -> bool: async def need_migrate(self) -> bool:
"""Determine if the current environment needs to run this migration""" """判断当前环境是否需要运行此迁移"""
pass pass
@abc.abstractmethod @abc.abstractmethod
async def run(self): async def run(self):
"""Run migration""" """执行迁移"""
pass pass

View File

@@ -9,7 +9,7 @@ preregistered_notes: list[typing.Type[LaunchNote]] = []
def note_class(name: str, number: int): def note_class(name: str, number: int):
"""Register a launch information""" """注册一个启动信息"""
def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]: def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]:
cls.name = name cls.name = name
@@ -21,7 +21,7 @@ def note_class(name: str, number: int):
class LaunchNote(abc.ABC): class LaunchNote(abc.ABC):
"""Launch information""" """启动信息"""
name: str name: str
@@ -34,10 +34,10 @@ class LaunchNote(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
async def need_show(self) -> bool: async def need_show(self) -> bool:
"""Determine if the current environment needs to display this launch information""" """判断当前环境是否需要显示此启动信息"""
pass pass
@abc.abstractmethod @abc.abstractmethod
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]: async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
"""Generate launch information""" """生成启动信息"""
pass pass

View File

@@ -7,7 +7,7 @@ from .. import note
@note.note_class('ClassicNotes', 1) @note.note_class('ClassicNotes', 1)
class ClassicNotes(note.LaunchNote): class ClassicNotes(note.LaunchNote):
"""Classic launch information""" """经典启动信息"""
async def need_show(self) -> bool: async def need_show(self) -> bool:
return True return True

View File

@@ -9,7 +9,7 @@ from .. import note
@note.note_class('SelectionModeOnWindows', 2) @note.note_class('SelectionModeOnWindows', 2)
class SelectionModeOnWindows(note.LaunchNote): class SelectionModeOnWindows(note.LaunchNote):
"""Selection mode prompt information on Windows""" """Windows 上的选择模式提示信息"""
async def need_show(self) -> bool: async def need_show(self) -> bool:
return os.name == 'nt' return os.name == 'nt'
@@ -19,8 +19,3 @@ class SelectionModeOnWindows(note.LaunchNote):
"""您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""", """您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""",
logging.INFO, logging.INFO,
) )
yield (
"""You are using Windows system, if the top left corner of the window displays "Selection" mode, the program will be paused running, please right-click on the blank area in the window to exit the selection mode.""",
logging.INFO,
)

View File

@@ -7,9 +7,9 @@ from . import app
preregistered_stages: dict[str, typing.Type[BootingStage]] = {} preregistered_stages: dict[str, typing.Type[BootingStage]] = {}
"""Pre-registered request processing stages. All request processing stage classes are registered in this dictionary during initialization. """预注册的请求处理阶段。在初始化时,所有请求处理阶段类会被注册到此字典中。
Currently not supported for extension 当前阶段暂不支持扩展
""" """
@@ -22,11 +22,11 @@ def stage_class(name: str):
class BootingStage(abc.ABC): class BootingStage(abc.ABC):
"""Booting stage""" """启动阶段"""
name: str = None name: str = None
@abc.abstractmethod @abc.abstractmethod
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Run""" """启动"""
pass pass

View File

@@ -1,15 +1,15 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from .. import stage, app from .. import stage, app
from ...utils import version, proxy, announce from ...utils import version, proxy, announce
from ...pipeline import pool, controller, pipelinemgr from ...pipeline import pool, controller, pipelinemgr
from ...plugin import manager as plugin_mgr from ...plugin import connector as plugin_connector
from ...command import cmdmgr from ...command import cmdmgr
from ...provider.session import sessionmgr as llm_session_mgr from ...provider.session import sessionmgr as llm_session_mgr
from ...provider.modelmgr import modelmgr as llm_model_mgr from ...provider.modelmgr import modelmgr as llm_model_mgr
from ...provider.tools import toolmgr as llm_tool_mgr from ...provider.tools import toolmgr as llm_tool_mgr
from ...rag.knowledge import kbmgr as rag_mgr
from ...platform import botmgr as im_mgr from ...platform import botmgr as im_mgr
from ...persistence import mgr as persistencemgr from ...persistence import mgr as persistencemgr
from ...api.http.controller import main as http_controller from ...api.http.controller import main as http_controller
@@ -17,20 +17,18 @@ from ...api.http.service import user as user_service
from ...api.http.service import model as model_service from ...api.http.service import model as model_service
from ...api.http.service import pipeline as pipeline_service from ...api.http.service import pipeline as pipeline_service
from ...api.http.service import bot as bot_service from ...api.http.service import bot as bot_service
from ...api.http.service import knowledge as knowledge_service
from ...discover import engine as discover_engine from ...discover import engine as discover_engine
from ...storage import mgr as storagemgr from ...storage import mgr as storagemgr
from ...utils import logcache from ...utils import logcache
from ...vector import mgr as vectordb_mgr
from .. import taskmgr from .. import taskmgr
@stage.stage_class('BuildAppStage') @stage.stage_class('BuildAppStage')
class BuildAppStage(stage.BootingStage): class BuildAppStage(stage.BootingStage):
"""Build LangBot application""" """构建应用阶段"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Build LangBot application""" """构建app对象的各个组件对象并初始化"""
ap.task_mgr = taskmgr.AsyncTaskManager(ap) ap.task_mgr = taskmgr.AsyncTaskManager(ap)
discover = discover_engine.ComponentDiscoveryEngine(ap) discover = discover_engine.ComponentDiscoveryEngine(ap)
@@ -45,7 +43,7 @@ class BuildAppStage(stage.BootingStage):
await ver_mgr.initialize() await ver_mgr.initialize()
ap.ver_mgr = ver_mgr ap.ver_mgr = ver_mgr
# Send announcement # 发送公告
ann_mgr = announce.AnnouncementManager(ap) ann_mgr = announce.AnnouncementManager(ap)
ap.ann_mgr = ann_mgr ap.ann_mgr = ann_mgr
@@ -62,10 +60,13 @@ class BuildAppStage(stage.BootingStage):
ap.persistence_mgr = persistence_mgr_inst ap.persistence_mgr = persistence_mgr_inst
await persistence_mgr_inst.initialize() await persistence_mgr_inst.initialize()
plugin_mgr_inst = plugin_mgr.PluginManager(ap) async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
await plugin_mgr_inst.initialize() await asyncio.sleep(3)
ap.plugin_mgr = plugin_mgr_inst await plugin_connector_inst.initialize()
await plugin_mgr_inst.load_plugins()
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
await plugin_connector_inst.initialize()
ap.plugin_connector = plugin_connector_inst
cmd_mgr_inst = cmdmgr.CommandManager(ap) cmd_mgr_inst = cmdmgr.CommandManager(ap)
await cmd_mgr_inst.initialize() await cmd_mgr_inst.initialize()
@@ -91,15 +92,6 @@ class BuildAppStage(stage.BootingStage):
await pipeline_mgr.initialize() await pipeline_mgr.initialize()
ap.pipeline_mgr = pipeline_mgr ap.pipeline_mgr = pipeline_mgr
rag_mgr_inst = rag_mgr.RAGManager(ap)
await rag_mgr_inst.initialize()
ap.rag_mgr = rag_mgr_inst
# 初始化向量数据库管理器
vectordb_mgr_inst = vectordb_mgr.VectorDBManager(ap)
await vectordb_mgr_inst.initialize()
ap.vector_db_mgr = vectordb_mgr_inst
http_ctrl = http_controller.HTTPController(ap) http_ctrl = http_controller.HTTPController(ap)
await http_ctrl.initialize() await http_ctrl.initialize()
ap.http_ctrl = http_ctrl ap.http_ctrl = http_ctrl
@@ -107,11 +99,8 @@ class BuildAppStage(stage.BootingStage):
user_service_inst = user_service.UserService(ap) user_service_inst = user_service.UserService(ap)
ap.user_service = user_service_inst ap.user_service = user_service_inst
llm_model_service_inst = model_service.LLMModelsService(ap) model_service_inst = model_service.ModelsService(ap)
ap.llm_model_service = llm_model_service_inst ap.model_service = model_service_inst
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
ap.embedding_models_service = embedding_models_service_inst
pipeline_service_inst = pipeline_service.PipelineService(ap) pipeline_service_inst = pipeline_service.PipelineService(ap)
ap.pipeline_service = pipeline_service_inst ap.pipeline_service = pipeline_service_inst
@@ -119,8 +108,5 @@ class BuildAppStage(stage.BootingStage):
bot_service_inst = bot_service.BotService(ap) bot_service_inst = bot_service.BotService(ap)
ap.bot_service = bot_service_inst ap.bot_service = bot_service_inst
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
ap.knowledge_service = knowledge_service_inst
ctrl = controller.Controller(ap) ctrl = controller.Controller(ap)
ap.ctrl = ctrl ap.ctrl = ctrl

View File

@@ -7,18 +7,11 @@ from .. import stage, app
@stage.stage_class('GenKeysStage') @stage.stage_class('GenKeysStage')
class GenKeysStage(stage.BootingStage): class GenKeysStage(stage.BootingStage):
"""Generate keys stage""" """生成密钥阶段"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Generate keys""" """启动"""
if not ap.instance_config.data['system']['jwt']['secret']: if not ap.instance_config.data['system']['jwt']['secret']:
ap.instance_config.data['system']['jwt']['secret'] = secrets.token_hex(16) ap.instance_config.data['system']['jwt']['secret'] = secrets.token_hex(16)
await ap.instance_config.dump_config() await ap.instance_config.dump_config()
if 'recovery_key' not in ap.instance_config.data['system']:
ap.instance_config.data['system']['recovery_key'] = ''
if not ap.instance_config.data['system']['recovery_key']:
ap.instance_config.data['system']['recovery_key'] = secrets.token_hex(3).upper()
await ap.instance_config.dump_config()

View File

@@ -8,10 +8,10 @@ from ..bootutils import config
@stage.stage_class('LoadConfigStage') @stage.stage_class('LoadConfigStage')
class LoadConfigStage(stage.BootingStage): class LoadConfigStage(stage.BootingStage):
"""Load config file stage""" """加载配置文件阶段"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Load config file""" """启动"""
# ======= deprecated ======= # ======= deprecated =======
if os.path.exists('data/config/command.json'): if os.path.exists('data/config/command.json'):

View File

@@ -11,13 +11,10 @@ importutil.import_modules_in_pkg(migrations)
@stage.stage_class('MigrationStage') @stage.stage_class('MigrationStage')
class MigrationStage(stage.BootingStage): class MigrationStage(stage.BootingStage):
"""Migration stage """迁移阶段"""
These migrations are legacy, only performed in version 3.x
"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Run migration""" """启动"""
if any( if any(
[ [
@@ -32,7 +29,7 @@ class MigrationStage(stage.BootingStage):
migrations = migration.preregistered_migrations migrations = migration.preregistered_migrations
# Sort by migration number # 按照迁移号排序
migrations.sort(key=lambda x: x.number) migrations.sort(key=lambda x: x.number)
for migration_cls in migrations: for migration_cls in migrations:
@@ -40,4 +37,4 @@ class MigrationStage(stage.BootingStage):
if await migration_instance.need_migrate(): if await migration_instance.need_migrate():
await migration_instance.run() await migration_instance.run()
print(f'Migration {migration_instance.name} executed') print(f'已执行迁移 {migration_instance.name}')

View File

@@ -8,7 +8,7 @@ from ..bootutils import log
class PersistenceHandler(logging.Handler, object): class PersistenceHandler(logging.Handler, object):
""" """
Save logs to database 保存日志到数据库
""" """
ap: app.Application ap: app.Application
@@ -19,9 +19,9 @@ class PersistenceHandler(logging.Handler, object):
def emit(self, record): def emit(self, record):
""" """
emit function is a required function for custom handler classes, here you can process the log messages as needed, such as sending logs to the server emit函数为自定义handler类时必重写的函数这里可以根据需要对日志消息做一些处理比如发送日志到服务器
Emit a record 发出记录(Emit a record)
""" """
try: try:
msg = self.format(record) msg = self.format(record)
@@ -34,10 +34,10 @@ class PersistenceHandler(logging.Handler, object):
@stage.stage_class('SetupLoggerStage') @stage.stage_class('SetupLoggerStage')
class SetupLoggerStage(stage.BootingStage): class SetupLoggerStage(stage.BootingStage):
"""Setup logger stage""" """设置日志器阶段"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""Setup logger""" """启动"""
persistence_handler = PersistenceHandler('LoggerHandler', ap) persistence_handler = PersistenceHandler('LoggerHandler', ap)
extra_handlers = [] extra_handlers = []

View File

@@ -12,10 +12,10 @@ importutil.import_modules_in_pkg(notes)
@stage.stage_class('ShowNotesStage') @stage.stage_class('ShowNotesStage')
class ShowNotesStage(stage.BootingStage): class ShowNotesStage(stage.BootingStage):
"""Show notes stage""" """显示启动信息阶段"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
# Sort # 排序
note.preregistered_notes.sort(key=lambda x: x.number) note.preregistered_notes.sort(key=lambda x: x.number)
for note_cls in note.preregistered_notes: for note_cls in note.preregistered_notes:

View File

@@ -9,13 +9,13 @@ from . import entities as core_entities
class TaskContext: class TaskContext:
"""Task tracking context""" """任务跟踪上下文"""
current_action: str current_action: str
"""Current action being executed""" """当前正在执行的动作"""
log: str log: str
"""Log""" """记录日志"""
def __init__(self): def __init__(self):
self.current_action = 'default' self.current_action = 'default'
@@ -58,40 +58,40 @@ placeholder_context: TaskContext | None = None
class TaskWrapper: class TaskWrapper:
"""Task wrapper""" """任务包装器"""
_id_index: int = 0 _id_index: int = 0
"""Task ID index""" """任务ID索引"""
id: int id: int
"""Task ID""" """任务ID"""
task_type: str = 'system' # Task type: system or user task_type: str = 'system' # 任务类型: system user
"""Task type""" """任务类型"""
kind: str = 'system_task' # Task type determined by the initiator, usually the same task type kind: str = 'system_task' # 由发起者确定任务种类,通常同质化的任务种类相同
"""Task type""" """任务种类"""
name: str = '' name: str = ''
"""Task unique name""" """任务唯一名称"""
label: str = '' label: str = ''
"""Task display name""" """任务显示名称"""
task_context: TaskContext task_context: TaskContext
"""Task context""" """任务上下文"""
task: asyncio.Task task: asyncio.Task
"""Task""" """任务"""
task_stack: list = None task_stack: list = None
"""Task stack""" """任务堆栈"""
ap: app.Application ap: app.Application
"""Application instance""" """应用实例"""
scopes: list[core_entities.LifecycleControlScope] scopes: list[core_entities.LifecycleControlScope]
"""Task scope""" """任务所属生命周期控制范围"""
def __init__( def __init__(
self, self,
@@ -165,13 +165,13 @@ class TaskWrapper:
class AsyncTaskManager: class AsyncTaskManager:
"""Save all asynchronous tasks in the app """保存app中的所有异步任务
Include system-level and user-level (plugin installation, update, etc. initiated by users directly)""" 包含系统级的和用户级(插件安装、更新等由用户直接发起的)的"""
ap: app.Application ap: app.Application
tasks: list[TaskWrapper] tasks: list[TaskWrapper]
"""All tasks""" """所有任务"""
def __init__(self, ap: app.Application): def __init__(self, ap: app.Application):
self.ap = ap self.ap = ap

View File

@@ -4,7 +4,7 @@ from .base import Base
class Bot(Base): class Bot(Base):
"""Bot""" """机器人"""
__tablename__ = 'bots' __tablename__ = 'bots'

View File

@@ -0,0 +1,22 @@
import sqlalchemy
from .base import Base
class BinaryStorage(Base):
"""Current for plugin use only"""
__tablename__ = 'binary_storages'
unique_key = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True)
key = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
owner_type = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
owner = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
value = sqlalchemy.Column(sqlalchemy.LargeBinary, nullable=False)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,
nullable=False,
server_default=sqlalchemy.func.now(),
onupdate=sqlalchemy.func.now(),
)

View File

@@ -12,7 +12,7 @@ initial_metadata = [
class Metadata(Base): class Metadata(Base):
"""Database metadata""" """数据库元数据"""
__tablename__ = 'metadata' __tablename__ = 'metadata'

View File

@@ -4,7 +4,7 @@ from .base import Base
class LLMModel(Base): class LLMModel(Base):
"""LLM model""" """LLM 模型"""
__tablename__ = 'llm_models' __tablename__ = 'llm_models'
@@ -23,24 +23,3 @@ class LLMModel(Base):
server_default=sqlalchemy.func.now(), server_default=sqlalchemy.func.now(),
onupdate=sqlalchemy.func.now(), onupdate=sqlalchemy.func.now(),
) )
class EmbeddingModel(Base):
"""Embedding 模型"""
__tablename__ = 'embedding_models'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,
nullable=False,
server_default=sqlalchemy.func.now(),
onupdate=sqlalchemy.func.now(),
)

View File

@@ -4,7 +4,7 @@ from .base import Base
class LegacyPipeline(Base): class LegacyPipeline(Base):
"""Legacy pipeline""" """旧版流水线"""
__tablename__ = 'legacy_pipelines' __tablename__ = 'legacy_pipelines'
@@ -20,12 +20,13 @@ class LegacyPipeline(Base):
) )
for_version = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) for_version = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False) is_default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=False)
stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) stages = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
class PipelineRunRecord(Base): class PipelineRunRecord(Base):
"""Pipeline run record""" """流水线运行记录"""
__tablename__ = 'pipeline_run_records' __tablename__ = 'pipeline_run_records'
@@ -42,4 +43,3 @@ class PipelineRunRecord(Base):
started_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False) started_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False)
finished_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False) finished_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False)
result = sqlalchemy.Column(sqlalchemy.JSON, nullable=False) result = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
knowledge_base_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)

View File

@@ -4,7 +4,7 @@ from .base import Base
class PluginSetting(Base): class PluginSetting(Base):
"""Plugin setting""" """插件配置"""
__tablename__ = 'plugin_settings' __tablename__ = 'plugin_settings'
@@ -13,6 +13,8 @@ class PluginSetting(Base):
enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True)
priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict)
install_source = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, default='github')
install_info = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
updated_at = sqlalchemy.Column( updated_at = sqlalchemy.Column(
sqlalchemy.DateTime, sqlalchemy.DateTime,

View File

@@ -1,50 +0,0 @@
import sqlalchemy
from .base import Base
# Base = declarative_base()
# DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./rag_knowledge.db')
# print("Using database URL:", DATABASE_URL)
# engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# def create_db_and_tables():
# """Creates all database tables defined in the Base."""
# Base.metadata.create_all(bind=engine)
# print('Database tables created or already exist.')
class KnowledgeBase(Base):
__tablename__ = 'knowledge_bases'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
name = sqlalchemy.Column(sqlalchemy.String, index=True)
description = sqlalchemy.Column(sqlalchemy.Text)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
class File(Base):
__tablename__ = 'knowledge_base_files'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
kb_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
file_name = sqlalchemy.Column(sqlalchemy.String)
extension = sqlalchemy.Column(sqlalchemy.String)
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
status = sqlalchemy.Column(sqlalchemy.String, default='pending') # pending, processing, completed, failed
class Chunk(Base):
__tablename__ = 'knowledge_base_chunks'
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
file_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
text = sqlalchemy.Column(sqlalchemy.Text)
# class Vector(Base):
# __tablename__ = 'knowledge_base_vectors'
# uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
# chunk_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)
# embedding = sqlalchemy.Column(sqlalchemy.LargeBinary)

View File

@@ -1,13 +0,0 @@
from sqlalchemy import Column, Integer, ForeignKey, LargeBinary
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Vector(Base):
__tablename__ = 'vectors'
id = Column(Integer, primary_key=True, index=True)
chunk_id = Column(Integer, ForeignKey('chunks.id'), unique=True)
embedding = Column(LargeBinary) # Store embeddings as binary
chunk = relationship('Chunk', back_populates='vector')

View File

@@ -1,13 +0,0 @@
from __future__ import annotations
import pydantic
from typing import Any
class RetrieveResultEntry(pydantic.BaseModel):
id: str
metadata: dict[str, Any]
distance: float

View File

@@ -11,7 +11,7 @@ preregistered_managers: list[type[BaseDatabaseManager]] = []
def manager_class(name: str) -> None: def manager_class(name: str) -> None:
"""Register a database manager class""" """注册一个数据库管理类"""
def decorator(cls: type[BaseDatabaseManager]) -> type[BaseDatabaseManager]: def decorator(cls: type[BaseDatabaseManager]) -> type[BaseDatabaseManager]:
cls.name = name cls.name = name
@@ -22,7 +22,7 @@ def manager_class(name: str) -> None:
class BaseDatabaseManager(abc.ABC): class BaseDatabaseManager(abc.ABC):
"""Base database manager class""" """基础数据库管理类"""
name: str name: str

View File

@@ -7,7 +7,7 @@ from .. import database
@database.manager_class('sqlite') @database.manager_class('sqlite')
class SQLiteDatabaseManager(database.BaseDatabaseManager): class SQLiteDatabaseManager(database.BaseDatabaseManager):
"""SQLite database manager""" """SQLite 数据库管理类"""
async def initialize(self) -> None: async def initialize(self) -> None:
sqlite_path = 'data/langbot.db' sqlite_path = 'data/langbot.db'

View File

@@ -22,12 +22,12 @@ importutil.import_modules_in_pkg(persistence)
class PersistenceManager: class PersistenceManager:
"""Persistence module manager""" """持久化模块管理器"""
ap: app.Application ap: app.Application
db: database.BaseDatabaseManager db: database.BaseDatabaseManager
"""Database manager""" """数据库管理器"""
meta: sqlalchemy.MetaData meta: sqlalchemy.MetaData
@@ -44,6 +44,38 @@ class PersistenceManager:
await self.create_tables() await self.create_tables()
# run migrations
database_version = await self.execute_async(
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version')
)
database_version = int(database_version.fetchone()[1])
required_database_version = constants.required_database_version
if database_version < required_database_version:
migrations = migration.preregistered_db_migrations
migrations.sort(key=lambda x: x.number)
last_migration_number = database_version
for migration_cls in migrations:
migration_instance = migration_cls(self.ap)
if (
migration_instance.number > database_version
and migration_instance.number <= required_database_version
):
await migration_instance.upgrade()
await self.execute_async(
sqlalchemy.update(metadata.Metadata)
.where(metadata.Metadata.key == 'database_version')
.values({'value': str(migration_instance.number)})
)
last_migration_number = migration_instance.number
self.ap.logger.info(f'Migration {migration_instance.number} completed.')
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
async def create_tables(self): async def create_tables(self):
# create tables # create tables
async with self.get_db_engine().connect() as conn: async with self.get_db_engine().connect() as conn:
@@ -79,7 +111,7 @@ class PersistenceManager:
'stages': pipeline_service.default_stage_order, 'stages': pipeline_service.default_stage_order,
'is_default': True, 'is_default': True,
'name': 'ChatPipeline', 'name': 'ChatPipeline',
'description': 'Default pipeline, new bots will be bound to this pipeline | 默认提供的流水线,您配置的机器人将自动绑定到此流水线', 'description': '默认提供的流水线,您配置的机器人、第一个模型将自动绑定到此流水线',
'config': pipeline_config, 'config': pipeline_config,
} }
@@ -87,38 +119,6 @@ class PersistenceManager:
# ================================= # =================================
# run migrations
database_version = await self.execute_async(
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version')
)
database_version = int(database_version.fetchone()[1])
required_database_version = constants.required_database_version
if database_version < required_database_version:
migrations = migration.preregistered_db_migrations
migrations.sort(key=lambda x: x.number)
last_migration_number = database_version
for migration_cls in migrations:
migration_instance = migration_cls(self.ap)
if (
migration_instance.number > database_version
and migration_instance.number <= required_database_version
):
await migration_instance.upgrade()
await self.execute_async(
sqlalchemy.update(metadata.Metadata)
.where(metadata.Metadata.key == 'database_version')
.values({'value': str(migration_instance.number)})
)
last_migration_number = migration_instance.number
self.ap.logger.info(f'Migration {migration_instance.number} completed.')
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult: async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult:
async with self.get_db_engine().connect() as conn: async with self.get_db_engine().connect() as conn:
result = await conn.execute(*args, **kwargs) result = await conn.execute(*args, **kwargs)
@@ -128,10 +128,13 @@ class PersistenceManager:
def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine: def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine:
return self.db.get_engine() return self.db.get_engine()
def serialize_model(self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base) -> dict: def serialize_model(
self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base, masked_columns: list[str] = []
) -> dict:
return { return {
column.name: getattr(data, column.name) column.name: getattr(data, column.name)
if not isinstance(getattr(data, column.name), (datetime.datetime)) if not isinstance(getattr(data, column.name), (datetime.datetime))
else getattr(data, column.name).isoformat() else getattr(data, column.name).isoformat()
for column in model.__table__.columns for column in model.__table__.columns
if column.name not in masked_columns
} }

View File

@@ -10,7 +10,7 @@ preregistered_db_migrations: list[typing.Type[DBMigration]] = []
def migration_class(number: int): def migration_class(number: int):
"""Migration class decorator""" """迁移类装饰器"""
def wrapper(cls: typing.Type[DBMigration]) -> typing.Type[DBMigration]: def wrapper(cls: typing.Type[DBMigration]) -> typing.Type[DBMigration]:
cls.number = number cls.number = number
@@ -21,20 +21,20 @@ def migration_class(number: int):
class DBMigration(abc.ABC): class DBMigration(abc.ABC):
"""Database migration""" """数据库迁移"""
number: int number: int
"""Migration number""" """迁移号"""
def __init__(self, ap: app.Application): def __init__(self, ap: app.Application):
self.ap = ap self.ap = ap
@abc.abstractmethod @abc.abstractmethod
async def upgrade(self): async def upgrade(self):
"""Upgrade""" """升级"""
pass pass
@abc.abstractmethod @abc.abstractmethod
async def downgrade(self): async def downgrade(self):
"""Downgrade""" """降级"""
pass pass

View File

@@ -15,21 +15,21 @@ from ...entity.persistence import (
@migration.migration_class(1) @migration.migration_class(1)
class DBMigrateV3Config(migration.DBMigration): class DBMigrateV3Config(migration.DBMigration):
"""Migrate v3 config to v4 database""" """从 v3 的配置迁移到 v4 的数据库"""
async def upgrade(self): async def upgrade(self):
"""Upgrade""" """升级"""
""" """
Migrate all config files under data/config. 将 data/config 下的所有配置文件进行迁移。
After migration, all previous config files are saved under data/legacy/config. 迁移后,之前的配置文件都保存到 data/legacy/config 下。
After migration, all config files under data/metadata/ are saved under data/legacy/metadata. 迁移后data/metadata/ 下的所有配置文件都保存到 data/legacy/metadata 下。
""" """
if self.ap.provider_cfg is None: if self.ap.provider_cfg is None:
return return
# ======= Migrate model ======= # ======= 迁移模型 =======
# Only migrate the currently selected model # 只迁移当前选中的模型
model_name = self.ap.provider_cfg.data.get('model', 'gpt-4o') model_name = self.ap.provider_cfg.data.get('model', 'gpt-4o')
model_requester = 'openai-chat-completions' model_requester = 'openai-chat-completions'
@@ -91,8 +91,8 @@ class DBMigrateV3Config(migration.DBMigration):
sqlalchemy.insert(persistence_model.LLMModel).values(**llm_model_data) sqlalchemy.insert(persistence_model.LLMModel).values(**llm_model_data)
) )
# ======= Migrate pipeline config ======= # ======= 迁移流水线配置 =======
# Modify to default pipeline # 修改到默认流水线
default_pipeline = [ default_pipeline = [
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline) self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
for pipeline in ( for pipeline in (
@@ -184,8 +184,8 @@ class DBMigrateV3Config(migration.DBMigration):
.where(persistence_pipeline.LegacyPipeline.uuid == default_pipeline['uuid']) .where(persistence_pipeline.LegacyPipeline.uuid == default_pipeline['uuid'])
) )
# ======= Migrate bot ======= # ======= 迁移机器人 =======
# Only migrate enabled bots # 只迁移启用的机器人
for adapter in self.ap.platform_cfg.data.get('platform-adapters', []): for adapter in self.ap.platform_cfg.data.get('platform-adapters', []):
if not adapter.get('enable'): if not adapter.get('enable'):
continue continue
@@ -207,12 +207,11 @@ class DBMigrateV3Config(migration.DBMigration):
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(**bot_data)) await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(**bot_data))
# ======= Migrate system settings ======= # ======= 迁移系统设置 =======
self.ap.instance_config.data['admins'] = self.ap.system_cfg.data['admin-sessions'] self.ap.instance_config.data['admins'] = self.ap.system_cfg.data['admin-sessions']
self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port'] self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port']
self.ap.instance_config.data['command'] = { self.ap.instance_config.data['command'] = {
'prefix': self.ap.command_cfg.data['command-prefix'], 'prefix': self.ap.command_cfg.data['command-prefix'],
'enable': self.ap.command_cfg.data['command-enable'],
'privilege': self.ap.command_cfg.data['privilege'], 'privilege': self.ap.command_cfg.data['privilege'],
} }
self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency'] self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency']
@@ -224,7 +223,7 @@ class DBMigrateV3Config(migration.DBMigration):
await self.ap.instance_config.dump_config() await self.ap.instance_config.dump_config()
# ======= move files ======= # ======= move files =======
# Migrate all config files under data/config # 迁移 data/config 下的所有配置文件
all_legacy_dir_name = [ all_legacy_dir_name = [
'config', 'config',
# 'metadata', # 'metadata',
@@ -247,4 +246,4 @@ class DBMigrateV3Config(migration.DBMigration):
move_legacy_files(dir_name) move_legacy_files(dir_name)
async def downgrade(self): async def downgrade(self):
"""Downgrade""" """降级"""

View File

@@ -7,10 +7,10 @@ from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(2) @migration.migration_class(2)
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration): class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
"""Combine quote message config""" """引用消息合并配置"""
async def upgrade(self): async def upgrade(self):
"""Upgrade""" """升级"""
# read all pipelines # read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
@@ -37,5 +37,5 @@ class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
) )
async def downgrade(self): async def downgrade(self):
"""Downgrade""" """降级"""
pass pass

View File

@@ -7,10 +7,10 @@ from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(3) @migration.migration_class(3)
class DBMigrateN8nConfig(migration.DBMigration): class DBMigrateN8nConfig(migration.DBMigration):
"""N8n config""" """N8n配置"""
async def upgrade(self): async def upgrade(self):
"""Upgrade""" """升级"""
# read all pipelines # read all pipelines
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline)) pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
@@ -45,5 +45,5 @@ class DBMigrateN8nConfig(migration.DBMigration):
) )
async def downgrade(self): async def downgrade(self):
"""Downgrade""" """降级"""
pass pass

View File

@@ -0,0 +1,20 @@
from .. import migration
@migration.migration_class(4)
class DBMigratePluginConfig(migration.DBMigration):
"""插件配置"""
async def upgrade(self):
"""升级"""
if 'plugin' not in self.ap.instance_config.data:
self.ap.instance_config.data['plugin'] = {
'runtime_ws_url': 'ws://localhost:5400/control/ws',
}
await self.ap.instance_config.dump_config()
async def downgrade(self):
"""降级"""
pass

View File

@@ -1,38 +0,0 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(4)
class DBMigrateRAGKBUUID(migration.DBMigration):
"""RAG知识库UUID"""
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 'knowledge-base' not in config['ai']['local-agent']:
config['ai']['local-agent']['knowledge-base'] = ''
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

View File

@@ -1,38 +0,0 @@
from .. import migration
import sqlalchemy
from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(5)
class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
"""Pipeline remove cot config"""
async def upgrade(self):
"""Upgrade"""
# 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 'remove-think' not in config['output']['misc']:
config['output']['misc']['remove-think'] = False
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):
"""Downgrade"""
pass

View File

@@ -0,0 +1,32 @@
import sqlalchemy
from .. import migration
@migration.migration_class(5)
class DBMigratePluginInstallSource(migration.DBMigration):
"""插件安装来源"""
async def upgrade(self):
"""升级"""
# 查询表结构获取所有列名(异步执行 SQL
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(plugin_settings);'))
# fetchall() 是同步方法,无需 await
columns = [row[1] for row in result.fetchall()]
# 检查并添加 install_source 列
if 'install_source' not in columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
"ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'"
)
)
# 检查并添加 install_info 列
if 'install_info' not in columns:
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'")
)
async def downgrade(self):
"""降级"""
pass

View File

@@ -1,20 +1,20 @@
from __future__ import annotations from __future__ import annotations
from .. import stage, entities from .. import stage, entities
from ...core import entities as core_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
@stage.stage_class('BanSessionCheckStage') @stage.stage_class('BanSessionCheckStage')
class BanSessionCheckStage(stage.PipelineStage): class BanSessionCheckStage(stage.PipelineStage):
"""Access control processing stage """访问控制处理阶段
Only check if the group or personal number in the query is in the access control list. 仅检查query中群号或个人号是否在访问控制列表中。
""" """
async def initialize(self, pipeline_config: dict): async def initialize(self, pipeline_config: dict):
pass pass
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
found = False found = False
mode = query.pipeline_config['trigger']['access-control']['mode'] mode = query.pipeline_config['trigger']['access-control']['mode']
@@ -30,10 +30,6 @@ class BanSessionCheckStage(stage.PipelineStage):
if sess == f'{query.launcher_type.value}_{query.launcher_id}': if sess == f'{query.launcher_type.value}_{query.launcher_id}':
found = True found = True
break break
# 使用 *_id 来表示加白/拉黑某用户的私聊和群聊场景
if sess.startswith('*_') and (sess[2:] == query.launcher_id or sess[2:] == query.sender_id):
found = True
break
ctn = False ctn = False
@@ -45,7 +41,5 @@ class BanSessionCheckStage(stage.PipelineStage):
return entities.StageProcessResult( return entities.StageProcessResult(
result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT, result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT,
new_query=query, new_query=query,
console_notice=f'Ignore message according to access control: {query.launcher_type.value}_{query.launcher_id}' console_notice=f'根据访问控制忽略消息: {query.launcher_type.value}_{query.launcher_id}' if not ctn else '',
if not ctn
else '',
) )

View File

@@ -3,12 +3,11 @@ from __future__ import annotations
from ...core import app from ...core import app
from .. import stage, entities from .. import stage, entities
from ...core import entities as core_entities
from . import filter as filter_model, entities as filter_entities from . import filter as filter_model, entities as filter_entities
from ...provider import entities as llm_entities from langbot_plugin.api.entities.builtin.provider import message as provider_message
from ...platform.types import message as platform_message import langbot_plugin.api.entities.builtin.platform.message as platform_message
from ...utils import importutil from ...utils import importutil
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from . import filters from . import filters
importutil.import_modules_in_pkg(filters) importutil.import_modules_in_pkg(filters)
@@ -58,7 +57,7 @@ class ContentFilterStage(stage.PipelineStage):
async def _pre_process( async def _pre_process(
self, self,
message: str, message: str,
query: core_entities.Query, query: pipeline_query.Query,
) -> entities.StageProcessResult: ) -> entities.StageProcessResult:
"""请求llm前处理消息 """请求llm前处理消息
只要有一个不通过就不放行,只放行 PASS 的消息 只要有一个不通过就不放行,只放行 PASS 的消息
@@ -86,14 +85,14 @@ class ContentFilterStage(stage.PipelineStage):
elif result.level == filter_entities.ResultLevel.PASS: # 传到下一个 elif result.level == filter_entities.ResultLevel.PASS: # 传到下一个
message = result.replacement message = result.replacement
query.message_chain = platform_message.MessageChain(platform_message.Plain(message)) query.message_chain = platform_message.MessageChain([platform_message.Plain(text=message)])
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
async def _post_process( async def _post_process(
self, self,
message: str, message: str,
query: core_entities.Query, query: pipeline_query.Query,
) -> entities.StageProcessResult: ) -> entities.StageProcessResult:
"""请求llm后处理响应 """请求llm后处理响应
只要是 PASS 或者 MASKED 的就通过此 filter将其 replacement 设置为message进入下一个 filter 只要是 PASS 或者 MASKED 的就通过此 filter将其 replacement 设置为message进入下一个 filter
@@ -123,7 +122,7 @@ class ContentFilterStage(stage.PipelineStage):
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
"""处理""" """处理"""
if stage_inst_name == 'PreContentFilterStage': if stage_inst_name == 'PreContentFilterStage':
contain_non_text = False contain_non_text = False
@@ -142,7 +141,7 @@ class ContentFilterStage(stage.PipelineStage):
return await self._pre_process(str(query.message_chain).strip(), query) return await self._pre_process(str(query.message_chain).strip(), query)
elif stage_inst_name == 'PostContentFilterStage': elif stage_inst_name == 'PostContentFilterStage':
# 仅处理 query.resp_messages[-1].content 是 str 的情况 # 仅处理 query.resp_messages[-1].content 是 str 的情况
if isinstance(query.resp_messages[-1], llm_entities.Message) and isinstance( if isinstance(query.resp_messages[-1], provider_message.Message) and isinstance(
query.resp_messages[-1].content, str query.resp_messages[-1].content, str
): ):
return await self._post_process(query.resp_messages[-1].content, query) return await self._post_process(query.resp_messages[-1].content, query)

View File

@@ -1,6 +1,6 @@
import enum import enum
import pydantic.v1 as pydantic import pydantic
class ResultLevel(enum.Enum): class ResultLevel(enum.Enum):

View File

@@ -3,9 +3,9 @@ from __future__ import annotations
import abc import abc
import typing import typing
from ...core import app, entities as core_entities from ...core import app
from . import entities from . import entities
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
preregistered_filters: list[typing.Type[ContentFilter]] = [] preregistered_filters: list[typing.Type[ContentFilter]] = []
@@ -13,13 +13,13 @@ preregistered_filters: list[typing.Type[ContentFilter]] = []
def filter_class( def filter_class(
name: str, name: str,
) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: ) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]:
"""Content filter class decorator """内容过滤器类装饰器
Args: Args:
name (str): Filter name name (str): 过滤器名称
Returns: Returns:
typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: Decorator typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: 装饰器
""" """
def decorator(cls: typing.Type[ContentFilter]) -> typing.Type[ContentFilter]: def decorator(cls: typing.Type[ContentFilter]) -> typing.Type[ContentFilter]:
@@ -35,7 +35,7 @@ def filter_class(
class ContentFilter(metaclass=abc.ABCMeta): class ContentFilter(metaclass=abc.ABCMeta):
"""Content filter abstract class""" """内容过滤器抽象类"""
name: str name: str
@@ -46,31 +46,31 @@ class ContentFilter(metaclass=abc.ABCMeta):
@property @property
def enable_stages(self): def enable_stages(self):
"""Enabled stages """启用的阶段
Default is the two stages before and after the message request to AI. 默认为消息请求AI前后的两个阶段。
entity.EnableStage.PRE: Before message request to AI, the content to check is the user's input message. entity.EnableStage.PRE: 消息请求AI前此时需要检查的内容是用户的输入消息。
entity.EnableStage.POST: After message request to AI, the content to check is the AI's reply message. entity.EnableStage.POST: 消息请求AI后此时需要检查的内容是AI的回复消息。
""" """
return [entities.EnableStage.PRE, entities.EnableStage.POST] return [entities.EnableStage.PRE, entities.EnableStage.POST]
async def initialize(self): async def initialize(self):
"""Initialize filter""" """初始化过滤器"""
pass pass
@abc.abstractmethod @abc.abstractmethod
async def process(self, query: core_entities.Query, message: str = None, image_url=None) -> entities.FilterResult: async def process(self, query: pipeline_query.Query, message: str = None, image_url=None) -> entities.FilterResult:
"""Process message """处理消息
It is divided into two stages, depending on the value of enable_stages. 分为前后阶段,具体取决于 enable_stages 的值。
For content filters, you do not need to consider the stage of the message, you only need to check the message content. 对于内容过滤器来说,不需要考虑消息所处的阶段,只需要检查消息内容即可。
Args: Args:
message (str): Content to check message (str): 需要检查的内容
image_url (str): URL of the image to check image_url (str): 要检查的图片的 URL
Returns: Returns:
entities.FilterResult: Filter result, please refer to the documentation of entities.FilterResult class entities.FilterResult: 过滤结果,具体内容请查看 entities.FilterResult 类的文档
""" """
raise NotImplementedError raise NotImplementedError

View File

@@ -4,8 +4,7 @@ import aiohttp
from .. import entities from .. import entities
from .. import filter as filter_model from .. import filter as filter_model
from ....core import entities as core_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}' BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}'
BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token' BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token'
@@ -27,7 +26,7 @@ class BaiduCloudExamine(filter_model.ContentFilter):
) as resp: ) as resp:
return (await resp.json())['access_token'] return (await resp.json())['access_token']
async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
BAIDU_EXAMINE_URL.format(await self._get_token()), BAIDU_EXAMINE_URL.format(await self._get_token()),

View File

@@ -3,17 +3,17 @@ import re
from .. import filter as filter_model from .. import filter as filter_model
from .. import entities from .. import entities
from ....core import entities as core_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
@filter_model.filter_class('ban-word-filter') @filter_model.filter_class('ban-word-filter')
class BanWordFilter(filter_model.ContentFilter): class BanWordFilter(filter_model.ContentFilter):
"""Filter content""" """根据内容过滤"""
async def initialize(self): async def initialize(self):
pass pass
async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
found = False found = False
for word in self.ap.sensitive_meta.data['words']: for word in self.ap.sensitive_meta.data['words']:

View File

@@ -3,12 +3,12 @@ import re
from .. import entities from .. import entities
from .. import filter as filter_model from .. import filter as filter_model
from ....core import entities as core_entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
@filter_model.filter_class('content-ignore') @filter_model.filter_class('content-ignore')
class ContentIgnore(filter_model.ContentFilter): class ContentIgnore(filter_model.ContentFilter):
"""Ignore message according to content""" """根据内容忽略消息"""
@property @property
def enable_stages(self): def enable_stages(self):
@@ -16,7 +16,7 @@ class ContentIgnore(filter_model.ContentFilter):
entities.EnableStage.PRE, entities.EnableStage.PRE,
] ]
async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult: async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
if 'prefix' in query.pipeline_config['trigger']['ignore-rules']: if 'prefix' in query.pipeline_config['trigger']['ignore-rules']:
for rule in query.pipeline_config['trigger']['ignore-rules']['prefix']: for rule in query.pipeline_config['trigger']['ignore-rules']['prefix']:
if message.startswith(rule): if message.startswith(rule):
@@ -24,7 +24,7 @@ class ContentIgnore(filter_model.ContentFilter):
level=entities.ResultLevel.BLOCK, level=entities.ResultLevel.BLOCK,
replacement='', replacement='',
user_notice='', user_notice='',
console_notice='Ignore message according to prefix rule in ignore_rules', console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息',
) )
if 'regexp' in query.pipeline_config['trigger']['ignore-rules']: if 'regexp' in query.pipeline_config['trigger']['ignore-rules']:
@@ -34,7 +34,7 @@ class ContentIgnore(filter_model.ContentFilter):
level=entities.ResultLevel.BLOCK, level=entities.ResultLevel.BLOCK,
replacement='', replacement='',
user_notice='', user_notice='',
console_notice='Ignore message according to regexp rule in ignore_rules', console_notice='根据 ignore_rules 中的 regexp 规则,忽略消息',
) )
return entities.FilterResult( return entities.FilterResult(

View File

@@ -3,7 +3,10 @@ from __future__ import annotations
import asyncio import asyncio
import traceback import traceback
from ..core import app, entities from ..core import app
from ..core import entities as core_entities
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
class Controller: class Controller:
@@ -22,19 +25,19 @@ class Controller:
"""事件处理循环""" """事件处理循环"""
try: try:
while True: while True:
selected_query: entities.Query = None selected_query: pipeline_query.Query = None
# 取请求 # 取请求
async with self.ap.query_pool: async with self.ap.query_pool:
queries: list[entities.Query] = self.ap.query_pool.queries queries: list[pipeline_query.Query] = self.ap.query_pool.queries
for query in queries: for query in queries:
session = await self.ap.sess_mgr.get_session(query) session = await self.ap.sess_mgr.get_session(query)
self.ap.logger.debug(f'Checking query {query} session {session}') self.ap.logger.debug(f'Checking query {query} session {session}')
if not session.semaphore.locked(): if not session._semaphore.locked():
selected_query = query selected_query = query
await session.semaphore.acquire() await session._semaphore.acquire()
break break
@@ -46,7 +49,7 @@ class Controller:
if selected_query: if selected_query:
async def _process_query(selected_query: entities.Query): async def _process_query(selected_query: pipeline_query.Query):
async with self.semaphore: # 总并发上限 async with self.semaphore: # 总并发上限
# find pipeline # find pipeline
# Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one. # Here firstly find the bot, then find the pipeline, in case the bot adapter's config is not the latest one.
@@ -59,7 +62,7 @@ class Controller:
await pipeline.run(selected_query) await pipeline.run(selected_query)
async with self.ap.query_pool: async with self.ap.query_pool:
(await self.ap.sess_mgr.get_session(selected_query)).semaphore.release() (await self.ap.sess_mgr.get_session(selected_query))._semaphore.release()
# 通知其他协程,有新的请求可以处理了 # 通知其他协程,有新的请求可以处理了
self.ap.query_pool.condition.notify_all() self.ap.query_pool.condition.notify_all()
@@ -68,8 +71,8 @@ class Controller:
kind='query', kind='query',
name=f'query-{selected_query.query_id}', name=f'query-{selected_query.query_id}',
scopes=[ scopes=[
entities.LifecycleControlScope.APPLICATION, core_entities.LifecycleControlScope.APPLICATION,
entities.LifecycleControlScope.PLATFORM, core_entities.LifecycleControlScope.PLATFORM,
], ],
) )

View File

@@ -3,10 +3,10 @@ from __future__ import annotations
import enum import enum
import typing import typing
import pydantic.v1 as pydantic import pydantic
from ..platform.types import message as platform_message
from ..core import entities import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.builtin.platform.message as platform_message
class ResultType(enum.Enum): class ResultType(enum.Enum):
@@ -20,7 +20,7 @@ class ResultType(enum.Enum):
class StageProcessResult(pydantic.BaseModel): class StageProcessResult(pydantic.BaseModel):
result_type: ResultType result_type: ResultType
new_query: entities.Query new_query: pipeline_query.Query
user_notice: typing.Optional[ user_notice: typing.Optional[
typing.Union[ typing.Union[

Some files were not shown because too many files have changed in this diff Show More