Compare commits

..

20 Commits

Author SHA1 Message Date
Junyan Qin
fdc79b8d77 chore: release v4.0.9 2025-07-16 11:39:15 +08:00
Junyan Qin
f244795e57 fix: rename to '302.AI' 2025-07-16 11:36:57 +08:00
Junyan Qin
5a2aa19d0f feat(aiocqhttp): no longer download files for now 2025-07-16 11:36:01 +08:00
Junyan Qin
81eb92646f doc: perf README_JP 2025-07-14 11:22:59 +08:00
Junyan Qin
019a9317e9 doc: perf README 2025-07-14 11:17:58 +08:00
TwperBody
858cfd8d5a Update package.json (#1570)
Compatible with the creation of environment variables in the Windows environment
2025-07-12 22:31:30 +08:00
Junyan Qin
bfdf238db5 chore: use new social image 2025-07-12 11:44:08 +08:00
Junyan Qin
c6e77e42be chore: switch some comments to en 2025-07-10 11:09:33 +08:00
Junyan Qin
4d0a39eb65 chore: switch comments to en 2025-07-10 11:01:16 +08:00
Junyan Qin
56248c350f chore: repo transferred 2025-07-07 19:00:55 +08:00
gaord
244aaf6e20 feat: 聊天的@用户id内容需要保留 (#1564)
* converters could use the application logger

* keep @targets in message for some plugins may need it to their functionality

* fix:form wxid in config

fix:传参问题,可以直接从config中拿到wxid

---------

Co-authored-by: fdc310 <82008029+fdc310@users.noreply.github.com>
2025-07-07 10:28:12 +08:00
Junyan Qin
a0b7d759ac chore: release v4.0.8.1 2025-07-06 10:46:32 +08:00
Junyan Qin
09884d3152 revert: 0203faa 2025-07-06 10:34:24 +08:00
Matthew_Astral
01f2ef5694 feat: new discord adapter (#1563) 2025-07-05 20:51:04 +08:00
Junyan Qin (Chin)
a01706d163 Feat/reset password (#1566)
* feat: reset password with recovery key

* perf: formatting and multi language
2025-07-05 17:36:35 +08:00
Junyan Qin
a8d03c98dc doc: replace comshare link 2025-07-04 11:37:31 +08:00
Junyan Qin
3f0153ea4d doc: fix incorrect 302.AI name 2025-07-03 17:26:17 +08:00
Junyan Qin
60b50a35f1 chore: release v4.0.8 2025-07-03 15:07:19 +08:00
Junyan Qin (Chin)
abd02f04af Feat/compshare requester (#1561)
* feat: add compshare requester

* doc: add compshare to README
2025-07-03 15:04:02 +08:00
Matthew_Astral
14411a8af6 Add Discord platform adapter implementation (#1560)
- Implement DiscordMessageConverter for message conversion
- Support image handling from base64, URL, and file paths
- Add DiscordEventConverter for event conversion
- Implement DiscordAdapter for Discord bot integration
- Support DM and TextChannel message handling
2025-07-02 09:48:49 +08:00
244 changed files with 8173 additions and 5803 deletions

View File

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

View File

@@ -1,32 +1,25 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
简体中文 / [English](README_EN.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/langbot-app/LangBot/star/badge.svg)](https://gitcode.com/langbot-app/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/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
<a href="https://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 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>
@@ -44,7 +37,7 @@
#### Docker Compose 部署
```bash
git clone https://github.com/RockChinQ/LangBot
git clone https://github.com/langbot-app/LangBot
cd LangBot
docker compose up -d
```
@@ -119,8 +112,9 @@ docker compose up -d
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [优云智算](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) | ✅ | 大模型聚合平台 |
| [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/) | ✅ | 本地大模型运行平台 |
@@ -148,10 +142,10 @@ docker compose up -d
## 😘 社区贡献
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
## 😎 保持更新

View File

@@ -1,30 +1,21 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[简体中文](README.md) / English / [日本語](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)
[![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://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
<div align="center">
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / English / [日本語](README_JP.md) / (PR for your language)
<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>
</div>
@@ -42,7 +33,7 @@
#### Docker Compose Deployment
```bash
git clone https://github.com/RockChinQ/LangBot
git clone https://github.com/langbot-app/LangBot
cd LangBot
docker compose up -d
```
@@ -116,9 +107,10 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource 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 |
| [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) | ✅ | |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
@@ -131,10 +123,10 @@ Directly use the released version to run, see the [Manual Deployment](https://do
## 🤝 Community Contribution
Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
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:
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a>
## 😎 Stay Ahead

View File

@@ -1,29 +1,21 @@
<p align="center">
<a href="https://langbot.app">
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
</a>
<div align="center">
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[简体中文](README.md) / [English](README_EN.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)
[![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://docs.langbot.app/en/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a>
<a href="https://github.com/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)
<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>
@@ -41,7 +33,7 @@
#### Docker Compose デプロイ
```bash
git clone https://github.com/RockChinQ/LangBot
git clone https://github.com/langbot-app/LangBot
cd LangBot
docker compose up -d
```
@@ -115,8 +107,9 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型と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) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
@@ -130,10 +123,10 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
## 🤝 コミュニティ貢献
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/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,6 +9,7 @@ spec:
components:
ComponentTemplate:
fromFiles:
- pkg/platform/adapter.yaml
- pkg/provider/modelmgr/requester.yaml
MessagePlatformAdapter:
fromDirs:

View File

@@ -3,7 +3,7 @@ from quart import request
import httpx
from quart import Quart
from typing import Callable, Dict, Any
import langbot_plugin.api.entities.builtin.platform.events as platform_events
from pkg.platform.types import events as platform_events
from .qqofficialevent import QQOfficialEvent
import json
import traceback
@@ -104,7 +104,7 @@ class QQOfficialClient:
return {'code': 0, 'message': 'success'}
except Exception as e:
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
return {'error': str(e)}, 400
async def run_task(self, host: str, port: int, *args, **kwargs):
@@ -168,6 +168,7 @@ class QQOfficialClient:
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + '/v2/users/' + user_openid + '/messages'
async with httpx.AsyncClient() as client:
headers = {
@@ -192,6 +193,7 @@ class QQOfficialClient:
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
async with httpx.AsyncClient() as client:
headers = {
@@ -207,7 +209,7 @@ class QQOfficialClient:
if response.status_code == 200:
return
else:
await self.logger.error(f'发送群聊消息失败:{response.json()}')
await self.logger.error(f"发送群聊消息失败:{response.json()}")
raise Exception(response.read().decode())
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
@@ -215,6 +217,7 @@ class QQOfficialClient:
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + '/channels/' + channel_id + '/messages'
async with httpx.AsyncClient() as client:
headers = {
@@ -237,6 +240,7 @@ class QQOfficialClient:
"""发送频道私聊消息"""
if not await self.check_access_token():
await self.get_access_token()
url = self.base_url + '/dms/' + guild_id + '/messages'
async with httpx.AsyncClient() as client:

View File

@@ -4,7 +4,7 @@ from quart import Quart, jsonify, request
from slack_sdk.web.async_client import AsyncWebClient
from .slackevent import SlackEvent
from typing import Callable
import langbot_plugin.api.entities.builtin.platform.events as platform_events
from pkg.platform.types import events as platform_events
class SlackClient:
@@ -34,6 +34,7 @@ class SlackClient:
if self.bot_user_id and bot_user_id == self.bot_user_id:
return jsonify({'status': 'ok'})
# 处理私信
if data and data.get('event', {}).get('channel_type') in ['im']:
@@ -51,7 +52,7 @@ class SlackClient:
return jsonify({'status': 'ok'})
except Exception as e:
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
raise (e)
async def _handle_message(self, event: SlackEvent):
@@ -81,7 +82,7 @@ class SlackClient:
self.bot_user_id = response['message']['bot_id']
return
except Exception as e:
await self.logger.error(f'Error in send_message: {e}')
await self.logger.error(f"Error in send_message: {e}")
raise e
async def send_message_to_one(self, text: str, user_id: str):
@@ -92,7 +93,7 @@ class SlackClient:
return
except Exception as e:
await self.logger.error(f'Error in send_message: {traceback.format_exc()}')
await self.logger.error(f"Error in send_message: {traceback.format_exc()}")
raise e
async def run_task(self, host: str, port: int, *args, **kwargs):

View File

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

View File

@@ -1,4 +1,4 @@
from libs.wechatpad_api.util.http_util import post_json
from libs.wechatpad_api.util.http_util import async_request, post_json
class ChatRoomApi:
@@ -7,6 +7,8 @@ class ChatRoomApi:
self.token = token
def get_chatroom_member_detail(self, chatroom_name):
params = {'ChatRoomName': chatroom_name}
params = {
"ChatRoomName": chatroom_name
}
url = self.base_url + '/group/GetChatroomMemberDetail'
return post_json(url, token=self.token, data=params)

View File

@@ -1,23 +1,32 @@
from libs.wechatpad_api.util.http_util import post_json
from libs.wechatpad_api.util.http_util import async_request, post_json
import httpx
import base64
class DownloadApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
def send_download(self, aeskey, file_type, file_url):
json_data = {'AesKey': aeskey, 'FileType': file_type, 'FileURL': file_url}
url = self.base_url + '/message/SendCdnDownload'
json_data = {
"AesKey": aeskey,
"FileType": file_type,
"FileURL": file_url
}
url = self.base_url + "/message/SendCdnDownload"
return post_json(url, token=self.token, data=json_data)
def get_msg_voice(self, buf_id, length, new_msgid):
json_data = {'Bufid': buf_id, 'Length': length, 'NewMsgId': new_msgid, 'ToUserName': ''}
url = self.base_url + '/message/GetMsgVoice'
def get_msg_voice(self,buf_id, length, new_msgid):
json_data = {
"Bufid": buf_id,
"Length": length,
"NewMsgId": new_msgid,
"ToUserName": ""
}
url = self.base_url + "/message/GetMsgVoice"
return post_json(url, token=self.token, data=json_data)
async def download_url_to_base64(self, download_url):
async with httpx.AsyncClient() as client:
response = await client.get(download_url)
@@ -27,4 +36,4 @@ class DownloadApi:
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
return base64_str
else:
raise Exception('获取文件失败')
raise Exception('获取文件失败')

View File

@@ -1,6 +1,11 @@
from libs.wechatpad_api.util.http_util import post_json,async_request
from typing import List, Dict, Any, Optional
class FriendApi:
"""联系人API类处理所有与联系人相关的操作"""
def __init__(self, base_url: str, token: str):
self.base_url = base_url
self.token = token

View File

@@ -1,34 +1,37 @@
from libs.wechatpad_api.util.http_util import post_json, get_json
from libs.wechatpad_api.util.http_util import async_request,post_json,get_json
class LoginApi:
def __init__(self, base_url: str, token: str = None, admin_key: str = None):
"""
'''
Args:
base_url: 原始路径
token: token
admin_key: 管理员key
"""
'''
self.base_url = base_url
self.token = token
# self.admin_key = admin_key
def get_token(self, admin_key, day: int = 365):
def get_token(self, admin_key, day: int=365):
# 获取普通token
url = f'{self.base_url}/admin/GenAuthKey1'
json_data = {'Count': 1, 'Days': day}
url = f"{self.base_url}/admin/GenAuthKey1"
json_data = {
"Count": 1,
"Days": day
}
return post_json(base_url=url, token=admin_key, data=json_data)
def get_login_qr(self, Proxy: str = ''):
"""
def get_login_qr(self, Proxy: str = ""):
'''
Args:
Proxy:异地使用时代理
Returns:json数据
"""
'''
"""
{
@@ -46,37 +49,54 @@ class LoginApi:
}
"""
# 获取登录二维码
url = f'{self.base_url}/login/GetLoginQrCodeNew'
#获取登录二维码
url = f"{self.base_url}/login/GetLoginQrCodeNew"
check = False
if Proxy != '':
if Proxy != "":
check = True
json_data = {'Check': check, 'Proxy': Proxy}
json_data = {
"Check": check,
"Proxy": Proxy
}
return post_json(base_url=url, token=self.token, data=json_data)
def get_login_status(self):
# 获取登录状态
url = f'{self.base_url}/login/GetLoginStatus'
return get_json(base_url=url, token=self.token)
def logout(self):
# 退出登录
url = f'{self.base_url}/login/LogOut'
return post_json(base_url=url, token=self.token)
def wake_up_login(self, Proxy: str = ''):
def wake_up_login(self, Proxy: str = ""):
# 唤醒登录
url = f'{self.base_url}/login/WakeUpLogin'
check = False
if Proxy != '':
if Proxy != "":
check = True
json_data = {'Check': check, 'Proxy': ''}
json_data = {
"Check": check,
"Proxy": ""
}
return post_json(base_url=url, token=self.token, data=json_data)
def login(self, admin_key):
def login(self,admin_key):
login_status = self.get_login_status()
if login_status['Code'] == 300 and login_status['Text'] == '你已退出微信':
print('token已经失效重新获取')
if login_status["Code"] == 300 and login_status["Text"] == "你已退出微信":
print("token已经失效重新获取")
token_data = self.get_token(admin_key)
self.token = token_data['Data'][0]
self.token = token_data["Data"][0]

View File

@@ -1,4 +1,5 @@
from libs.wechatpad_api.util.http_util import post_json
from libs.wechatpad_api.util.http_util import async_request, post_json
class MessageApi:
@@ -6,8 +7,8 @@ class MessageApi:
self.base_url = base_url
self.token = token
def post_text(self, to_wxid, content, ats: list = []):
"""
def post_text(self, to_wxid, content, ats: list= []):
'''
Args:
app_id: 微信id
@@ -17,64 +18,106 @@ class MessageApi:
Returns:
"""
url = self.base_url + '/message/SendTextMessage'
'''
url = self.base_url + "/message/SendTextMessage"
"""发送文字消息"""
json_data = {
'MsgItem': [
{'AtWxIDList': ats, 'ImageContent': '', 'MsgType': 0, 'TextContent': content, 'ToUserName': to_wxid}
]
}
return post_json(base_url=url, token=self.token, data=json_data)
"MsgItem": [
{
"AtWxIDList": ats,
"ImageContent": "",
"MsgType": 0,
"TextContent": content,
"ToUserName": to_wxid
}
]
}
return post_json(base_url=url, token=self.token, data=json_data)
def post_image(self, to_wxid, img_url, ats: list = []):
def post_image(self, to_wxid, img_url, ats: list= []):
"""发送图片消息"""
# 这里好像可以尝试发送多个暂时未测试
json_data = {
'MsgItem': [
{'AtWxIDList': ats, 'ImageContent': img_url, 'MsgType': 0, 'TextContent': '', 'ToUserName': to_wxid}
"MsgItem": [
{
"AtWxIDList": ats,
"ImageContent": img_url,
"MsgType": 0,
"TextContent": '',
"ToUserName": to_wxid
}
]
}
url = self.base_url + '/message/SendImageMessage'
url = self.base_url + "/message/SendImageMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration):
"""发送语音消息"""
json_data = {
'ToUserName': to_wxid,
'VoiceData': voice_data,
'VoiceFormat': voice_forma,
'VoiceSecond': voice_duration,
"ToUserName": to_wxid,
"VoiceData": voice_data,
"VoiceFormat": voice_forma,
"VoiceSecond": voice_duration
}
url = self.base_url + '/message/SendVoice'
url = self.base_url + "/message/SendVoice"
return post_json(base_url=url, token=self.token, data=json_data)
def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag):
"""发送名片消息"""
param = {
'CardAlias': alias,
'CardFlag': flag,
'CardNickName': nick_name,
'CardWxId': name_card_wxid,
'ToUserName': to_wxid,
"CardAlias": alias,
"CardFlag": flag,
"CardNickName": nick_name,
"CardWxId": name_card_wxid,
"ToUserName": to_wxid
}
url = f'{self.base_url}/message/ShareCardMessage'
url = f"{self.base_url}/message/ShareCardMessage"
return post_json(base_url=url, token=self.token, data=param)
def post_emoji(self, to_wxid, emoji_md5, emoji_size: int = 0):
def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0):
"""发送emoji消息"""
json_data = {'EmojiList': [{'EmojiMd5': emoji_md5, 'EmojiSize': emoji_size, 'ToUserName': to_wxid}]}
url = f'{self.base_url}/message/SendEmojiMessage'
json_data = {
"EmojiList": [
{
"EmojiMd5": emoji_md5,
"EmojiSize": emoji_size,
"ToUserName": to_wxid
}
]
}
url = f"{self.base_url}/message/SendEmojiMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def post_app_msg(self, to_wxid, xml_data, contenttype: int = 0):
def post_app_msg(self, to_wxid,xml_data, contenttype:int=0):
"""发送appmsg消息"""
json_data = {'AppList': [{'ContentType': contenttype, 'ContentXML': xml_data, 'ToUserName': to_wxid}]}
url = f'{self.base_url}/message/SendAppMessage'
json_data = {
"AppList": [
{
"ContentType": contenttype,
"ContentXML": xml_data,
"ToUserName": to_wxid
}
]
}
url = f"{self.base_url}/message/SendAppMessage"
return post_json(base_url=url, token=self.token, data=json_data)
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
"""撤回消息"""
param = {'ClientMsgId': msg_id, 'CreateTime': create_time, 'NewMsgId': new_msg_id, 'ToUserName': to_wxid}
url = f'{self.base_url}/message/RevokeMsg'
return post_json(base_url=url, token=self.token, data=param)
param = {
"ClientMsgId": msg_id,
"CreateTime": create_time,
"NewMsgId": new_msg_id,
"ToUserName": to_wxid
}
url = f"{self.base_url}/message/RevokeMsg"
return post_json(base_url=url, token=self.token, data=param)

View File

@@ -12,9 +12,12 @@ class UserApi:
return get_json(base_url=url, token=self.token)
def get_qr_code(self, recover: bool = True, style: int = 8):
def get_qr_code(self, recover:bool=True, style:int=8):
"""获取自己的二维码"""
param = {'Recover': recover, 'Style': style}
param = {
"Recover": recover,
"Style": style
}
url = f'{self.base_url}/user/GetMyQRCode'
return post_json(base_url=url, token=self.token, data=param)
@@ -23,8 +26,12 @@ class UserApi:
url = f'{self.base_url}/equipment/GetSafetyInfo'
return post_json(base_url=url, token=self.token)
async def update_head_img(self, head_img_base64):
async def update_head_img(self, head_img_base64):
"""修改头像"""
param = {'Base64': head_img_base64}
param = {
"Base64": head_img_base64
}
url = f'{self.base_url}/user/UploadHeadImage'
return await async_request(base_url=url, token_key=self.token, json=param)
return await async_request(base_url=url, token_key=self.token, json=param)

View File

@@ -1,3 +1,4 @@
from libs.wechatpad_api.api.login import LoginApi
from libs.wechatpad_api.api.friend import FriendApi
from libs.wechatpad_api.api.message import MessageApi
@@ -6,6 +7,9 @@ from libs.wechatpad_api.api.downloadpai import DownloadApi
from libs.wechatpad_api.api.chatroom import ChatRoomApi
class WeChatPadClient:
def __init__(self, base_url, token, logger=None):
self._login_api = LoginApi(base_url, token)
@@ -16,16 +20,16 @@ class WeChatPadClient:
self._chatroom_api = ChatRoomApi(base_url, token)
self.logger = logger
def get_token(self, admin_key, day: int):
"""获取token"""
def get_token(self,admin_key, day: int):
'''获取token'''
return self._login_api.get_token(admin_key, day)
def get_login_qr(self, Proxy: str = ''):
def get_login_qr(self, Proxy:str=""):
"""登录二维码"""
return self._login_api.get_login_qr(Proxy=Proxy)
def awaken_login(self, Proxy: str = ''):
"""唤醒登录"""
def awaken_login(self, Proxy:str=""):
'''唤醒登录'''
return self._login_api.wake_up_login(Proxy=Proxy)
def log_out(self):
@@ -36,57 +40,59 @@ class WeChatPadClient:
"""获取登录状态"""
return self._login_api.get_login_status()
def send_text_message(self, to_wxid, message, ats: list = []):
def send_text_message(self, to_wxid, message, ats: list=[]):
"""发送文本消息"""
return self._message_api.post_text(to_wxid, message, ats)
return self._message_api.post_text(to_wxid, message, ats)
def send_image_message(self, to_wxid, img_url, ats: list = []):
def send_image_message(self, to_wxid, img_url, ats: list=[]):
"""发送图片消息"""
return self._message_api.post_image(to_wxid, img_url, ats)
return self._message_api.post_image(to_wxid, img_url, ats)
def send_voice_message(self, to_wxid, voice_data, voice_forma, voice_duration):
"""发送音频消息"""
return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration)
return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration)
def send_app_message(self, to_wxid, app_message, type):
"""发送app消息"""
return self._message_api.post_app_msg(to_wxid, app_message, type)
return self._message_api.post_app_msg(to_wxid, app_message, type)
def send_emoji_message(self, to_wxid, emoji_md5, emoji_size):
"""发送emoji消息"""
return self._message_api.post_emoji(to_wxid, emoji_md5, emoji_size)
return self._message_api.post_emoji(to_wxid,emoji_md5,emoji_size)
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
"""撤回消息"""
return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time)
return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time)
def get_profile(self):
"""获取用户信息"""
return self._user_api.get_profile()
def get_qr_code(self, recover: bool = True, style: int = 8):
def get_qr_code(self, recover:bool=True, style:int=8):
"""获取用户二维码"""
return self._user_api.get_qr_code(recover=recover, style=style)
return self._user_api.get_qr_code(recover=recover, style=style)
def get_safety_info(self):
"""获取设备信息"""
return self._user_api.get_safety_info()
return self._user_api.get_safety_info()
def update_head_img(self, head_img_base64):
def update_head_img(self, head_img_base64):
"""上传用户头像"""
return self._user_api.update_head_img(head_img_base64)
return self._user_api.update_head_img(head_img_base64)
def cdn_download(self, aeskey, file_type, file_url):
"""cdn下载"""
return self._download_api.send_download(aeskey, file_type, file_url)
return self._download_api.send_download( aeskey, file_type, file_url)
def get_msg_voice(self, buf_id, length, msgid):
def get_msg_voice(self,buf_id, length, msgid):
"""下载语音"""
return self._download_api.get_msg_voice(buf_id, length, msgid)
async def download_base64(self, url):
async def download_base64(self,url):
return await self._download_api.download_url_to_base64(download_url=url)
def get_chatroom_member_detail(self, chatroom_name):
"""查看群成员详情"""
return self._chatroom_api.get_chatroom_member_detail(chatroom_name)

View File

@@ -1,9 +1,10 @@
import requests
import aiohttp
def post_json(base_url, token, data=None):
headers = {'Content-Type': 'application/json'}
headers = {
'Content-Type': 'application/json'
}
url = base_url + f'?key={token}'
@@ -17,12 +18,14 @@ def post_json(base_url, token, data=None):
else:
raise RuntimeError(response.text)
except Exception as e:
print(f'http请求失败, url={url}, exception={e}')
print(f"http请求失败, url={url}, exception={e}")
raise RuntimeError(str(e))
def get_json(base_url, token):
headers = {'Content-Type': 'application/json'}
headers = {
'Content-Type': 'application/json'
}
url = base_url + f'?key={token}'
@@ -36,18 +39,21 @@ def get_json(base_url, token):
else:
raise RuntimeError(response.text)
except Exception as e:
print(f'http请求失败, url={url}, exception={e}')
print(f"http请求失败, url={url}, exception={e}")
raise RuntimeError(str(e))
import aiohttp
import asyncio
async def async_request(
base_url: str,
token_key: str,
method: str = 'POST',
params: dict = None,
# headers: dict = None,
data: dict = None,
json: dict = None,
base_url: str,
token_key: str,
method: str = 'POST',
params: dict = None,
# headers: dict = None,
data: dict = None,
json: dict = None
):
"""
通用异步请求函数
@@ -61,11 +67,18 @@ async def async_request(
:param json: JSON数据
:return: 响应文本
"""
headers = {'Content-Type': 'application/json'}
url = f'{base_url}?key={token_key}'
headers = {
'Content-Type': 'application/json'
}
url = f"{base_url}?key={token_key}"
async with aiohttp.ClientSession() as session:
async with session.request(
method=method, url=url, params=params, headers=headers, data=data, json=json
method=method,
url=url,
params=params,
headers=headers,
data=data,
json=json
) as response:
response.raise_for_status() # 如果状态码不是200抛出异常
result = await response.json()
@@ -76,3 +89,4 @@ async def async_request(
# return await result
# else:
# raise RuntimeError("请求失败",response.text)

View File

@@ -1,34 +1,31 @@
import qrcode
def print_green(text):
print(f'\033[32m{text}\033[0m')
print(f"\033[32m{text}\033[0m")
def print_yellow(text):
print(f'\033[33m{text}\033[0m')
print(f"\033[33m{text}\033[0m")
def print_red(text):
print(f'\033[31m{text}\033[0m')
print(f"\033[31m{text}\033[0m")
def make_and_print_qr(url):
"""生成并打印二维码
Args:
url: 需要生成二维码的URL字符串
Returns:
None
功能:
1. 在终端打印二维码的ASCII图形
2. 同时提供在线二维码生成链接作为备选
"""
print_green('请扫描下方二维码登录')
print_green("请扫描下方二维码登录")
qr = qrcode.QRCode()
qr.add_data(url)
qr.make()
qr.print_ascii(invert=True)
print_green(f'也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}')
print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}")

View File

@@ -8,7 +8,7 @@ from quart import Quart
import xml.etree.ElementTree as ET
from typing import Callable, Dict, Any
from .wecomevent import WecomEvent
import langbot_plugin.api.entities.builtin.platform.message as platform_message
from pkg.platform.types import message as platform_message
import aiofiles
@@ -57,7 +57,7 @@ class WecomClient:
if 'access_token' in data:
return data['access_token']
else:
await self.logger.error(f'获取accesstoken失败:{response.json()}')
await self.logger.error(f"获取accesstoken失败:{response.json()}")
raise Exception(f'未获取access token: {data}')
async def get_users(self):
@@ -129,7 +129,7 @@ class WecomClient:
response = await client.post(url, json=params)
data = response.json()
except Exception as e:
await self.logger.error(f'发送图片失败:{data}')
await self.logger.error(f"发送图片失败:{data}")
raise Exception('Failed to send image: ' + str(e))
# 企业微信错误码40014和42001代表accesstoken问题
@@ -164,7 +164,7 @@ class WecomClient:
self.access_token = await self.get_access_token(self.secret)
return await self.send_private_msg(user_id, agent_id, content)
if data['errcode'] != 0:
await self.logger.error(f'发送消息失败:{data}')
await self.logger.error(f"发送消息失败:{data}")
raise Exception('Failed to send message: ' + str(data))
async def handle_callback_request(self):
@@ -181,7 +181,7 @@ class WecomClient:
echostr = request.args.get('echostr')
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
await self.logger.error('验证失败')
await self.logger.error("验证失败")
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
@@ -189,8 +189,9 @@ class WecomClient:
encrypt_msg = await request.data
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
await self.logger.error('消息解密失败')
await self.logger.error("消息解密失败")
raise Exception(f'消息解密失败,错误码: {ret}')
# 解析消息并处理
message_data = await self.get_message(xml_msg)
@@ -201,7 +202,7 @@ class WecomClient:
return 'success'
except Exception as e:
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
return f'Error processing request: {str(e)}', 400
async def run_task(self, host: str, port: int, *args, **kwargs):
@@ -300,7 +301,7 @@ class WecomClient:
except binascii.Error as e:
raise ValueError(f'Invalid base64 string: {str(e)}')
else:
await self.logger.error('Image对象出错')
await self.logger.error("Image对象出错")
raise ValueError('image对象出错')
# 设置 multipart/form-data 格式的文件
@@ -324,7 +325,7 @@ class WecomClient:
self.access_token = await self.get_access_token(self.secret)
media_id = await self.upload_to_work(image)
if data.get('errcode', 0) != 0:
await self.logger.error(f'上传图片失败:{data}')
await self.logger.error(f"上传图片失败:{data}")
raise Exception('failed to upload file')
media_id = data.get('media_id')

View File

@@ -8,7 +8,7 @@ from quart import Quart
import xml.etree.ElementTree as ET
from typing import Callable
from .wecomcsevent import WecomCSEvent
import langbot_plugin.api.entities.builtin.platform.message as platform_message
from pkg.platform.types import message as platform_message
import aiofiles
@@ -187,7 +187,7 @@ class WecomCSClient:
self.access_token = await self.get_access_token(self.secret)
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
if data['errcode'] != 0:
await self.logger.error(f'发送消息失败:{data}')
await self.logger.error(f"发送消息失败:{data}")
raise Exception('Failed to send message')
return data
@@ -227,7 +227,7 @@ class WecomCSClient:
return 'success'
except Exception as e:
if self.logger:
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
else:
traceback.print_exc()
return f'Error processing request: {str(e)}', 400

12
main.py
View File

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

View File

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

View File

@@ -8,7 +8,7 @@ class WebChatDebugRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/send', methods=['POST'])
async def send_message(pipeline_uuid: str) -> str:
"""发送调试消息到流水线"""
"""Send a message to the pipeline for debugging"""
try:
data = await quart.request.get_json()
session_type = data.get('session_type', 'person')
@@ -38,7 +38,7 @@ class WebChatDebugRouterGroup(group.RouterGroup):
@self.route('/messages/<session_type>', methods=['GET'])
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
"""获取调试消息历史"""
"""Get the message history of the pipeline for debugging"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')
@@ -57,7 +57,7 @@ class WebChatDebugRouterGroup(group.RouterGroup):
@self.route('/reset/<session_type>', methods=['POST'])
async def reset_session(session_type: str) -> str:
"""重置调试会话"""
"""Reset the debug session"""
try:
if session_type not in ['person', 'group']:
return self.http_status(400, -1, 'session_type must be person or group')

View File

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

View File

@@ -14,11 +14,6 @@ class SystemRouterGroup(group.RouterGroup):
'version': constants.semantic_version,
'debug': constants.debug_mode,
'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'
),
}
)
@@ -40,7 +35,16 @@ class SystemRouterGroup(group.RouterGroup):
return self.success(data=task.to_dict())
@self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
@self.route('/reload', 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:
if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden')
@@ -50,39 +54,3 @@ class SystemRouterGroup(group.RouterGroup):
ap = self.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,5 +1,6 @@
import quart
import argon2
import asyncio
from .. import group
@@ -13,7 +14,7 @@ class UserRouterGroup(group.RouterGroup):
return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
if await self.ap.user_service.is_initialized():
return self.fail(1, '系统已初始化')
return self.fail(1, 'System already initialized')
json_data = await quart.request.json
@@ -31,7 +32,7 @@ class UserRouterGroup(group.RouterGroup):
try:
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
except argon2.exceptions.VerifyMismatchError:
return self.fail(1, '用户名或密码错误')
return self.fail(1, 'Invalid username or password')
return self.success(data={'token': token})
@@ -40,3 +41,29 @@ class UserRouterGroup(group.RouterGroup):
token = await self.ap.user_service.generate_jwt_token(user_email)
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})

View File

@@ -45,7 +45,7 @@ class HTTPController:
try:
await self.quart_app.run_task(*args, **kwargs)
except Exception as e:
self.ap.logger.error(f'启动 HTTP 服务失败: {e}')
self.ap.logger.error(f'Failed to start HTTP service: {e}')
self.ap.task_mgr.create_task(
exception_handler(

View File

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

View File

@@ -7,7 +7,7 @@ from ....core import app
from ....entity.persistence import model as persistence_model
from ....entity.persistence import pipeline as persistence_pipeline
from ....provider.modelmgr import requester as model_requester
from langbot_plugin.api.entities.builtin.provider import message as provider_message
from ....provider import entities as llm_entities
class ModelsService:
@@ -16,19 +16,11 @@ class ModelsService:
def __init__(self, ap: app.Application) -> None:
self.ap = ap
async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
async def get_llm_models(self) -> list[dict]:
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
models = result.all()
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
]
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models]
async def create_llm_model(self, model_data: dict) -> str:
model_data['uuid'] = str(uuid.uuid4())
@@ -107,7 +99,7 @@ class ModelsService:
await runtime_llm_model.requester.invoke_llm(
query=None,
model=runtime_llm_model,
messages=[provider_message.Message(role='user', content='Hello, world!')],
messages=[llm_entities.Message(role='user', content='Hello, world!')],
funcs=[],
extra_args={},
)

View File

@@ -73,3 +73,12 @@ class UserService:
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
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)
)

View File

@@ -2,12 +2,9 @@ from __future__ import annotations
import typing
from ..core import app
from . import operator
from ..core import app, entities as core_entities
from . import entities, operator, errors
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
@@ -16,11 +13,13 @@ importutil.import_modules_in_pkg(operators)
class CommandManager:
"""命令管理器"""
ap: app.Application
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):
@@ -56,28 +55,43 @@ class CommandManager:
async def _execute(
self,
context: command_context.ExecuteContext,
context: entities.ExecuteContext,
operator_list: list[operator.CommandOperator],
operator: operator.CommandOperator = None,
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行命令"""
command_list = await self.ap.plugin_connector.list_commands()
found = False
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
for command in command_list:
if command.metadata.name == context.command:
async for ret in self.ap.plugin_connector.execute_command(context):
yield ret
break
else:
yield command_context.CommandReturn(error=command_errors.CommandNotFoundError(context.command))
context.crt_command = context.crt_params[0]
context.crt_params = context.crt_params[1:]
async for ret in self._execute(context, oper.children, oper):
yield ret
break
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(
self,
command_text: str,
query: pipeline_query.Query,
session: provider_session.Session,
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
query: core_entities.Query,
session: core_entities.Session,
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
"""执行命令"""
privilege = 1
@@ -85,8 +99,8 @@ class CommandManager:
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
privilege = 2
ctx = command_context.ExecuteContext(
query_id=query.query_id,
ctx = entities.ExecuteContext(
query=query,
session=session,
command_text=command_text,
command='',
@@ -96,9 +110,5 @@ class CommandManager:
privilege=privilege,
)
ctx.command = ctx.params[0]
ctx.shift()
async for ret in self._execute(ctx, self.cmd_list):
yield ret

74
pkg/command/entities.py Normal file
View File

@@ -0,0 +1,74 @@
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
"""发起人权限"""

26
pkg/command/errors.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,22 +2,19 @@ from __future__ import annotations
import typing
from .. import operator
from langbot_plugin.api.entities.builtin.command import context as command_context, errors as command_errors
from .. import operator, entities, errors
@operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>')
class ListOperator(operator.CommandOperator):
async def execute(
self, context: command_context.ExecuteContext
) -> typing.AsyncGenerator[command_context.CommandReturn, None]:
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
page = 0
if len(context.crt_params) > 0:
try:
page = int(context.crt_params[0] - 1)
except Exception:
yield command_context.CommandReturn(error=command_errors.CommandOperationError('页码应为整数'))
yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数'))
return
record_per_page = 10
@@ -48,4 +45,4 @@ class ListOperator(operator.CommandOperator):
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 command_context.CommandReturn(text=f'{page + 1} 页 (时间倒序):\n{content}')
yield entities.CommandReturn(text=f'{page + 1} 页 (时间倒序):\n{content}')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ from .. import model as file_model
class YAMLConfigFile(file_model.ConfigFile):
"""YAML配置文件"""
"""YAML config file"""
def __init__(
self,
@@ -42,7 +42,7 @@ class YAMLConfigFile(file_model.ConfigFile):
try:
cfg = yaml.load(f, Loader=yaml.FullLoader)
except yaml.YAMLError as e:
raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}')
raise Exception(f'Syntax error in config file {self.config_file_name}: {e}')
if completion:
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:
"""配置文件管理器"""
"""Config file manager"""
name: str = None
"""配置管理器名"""
"""Config manager name"""
description: str = None
"""配置管理器描述"""
"""Config manager description"""
schema: dict = None
"""配置文件 schema
需要符合 JSON Schema Draft 7 规范
"""Config file schema
Must conform to JSON Schema Draft 7 specification
"""
file: file_model.ConfigFile = None
"""配置文件实例"""
"""Config file instance"""
data: dict = None
"""配置数据"""
"""Config data"""
doc_link: str = None
"""配置文件文档链接"""
"""Config file documentation link"""
def __init__(self, cfg_file: file_model.ConfigFile) -> None:
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:
"""加载Python模块配置文件
"""Load Python module config file
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
completion (bool): 是否自动补全内存中的配置文件
config_name (str): Config file name
template_name (str): Template file name
completion (bool): Whether to automatically complete the config file in memory
Returns:
ConfigManager: 配置文件管理器
ConfigManager: Config file manager
"""
cfg_inst = pymodule.PythonModuleConfigFile(config_name, template_name)
@@ -66,13 +66,13 @@ async def load_json_config(
template_data: dict = None,
completion: bool = True,
) -> ConfigManager:
"""加载JSON配置文件
"""Load JSON config file
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
template_data (dict): 模板数据
completion (bool): 是否自动补全内存中的配置文件
config_name (str): Config file name
template_name (str): Template file name
template_data (dict): Template data
completion (bool): Whether to automatically complete the config file in memory
"""
cfg_inst = json_file.JSONConfigFile(config_name, template_name, template_data)
@@ -88,16 +88,16 @@ async def load_yaml_config(
template_data: dict = None,
completion: bool = True,
) -> ConfigManager:
"""加载YAML配置文件
"""Load YAML config file
Args:
config_name (str): 配置文件名
template_name (str): 模板文件名
template_data (dict): 模板数据
completion (bool): 是否自动补全内存中的配置文件
config_name (str): Config file name
template_name (str): Template file name
template_data (dict): Template data
completion (bool): Whether to automatically complete the config file in memory
Returns:
ConfigManager: 配置文件管理器
ConfigManager: Config file manager
"""
cfg_inst = yaml_file.YAMLConfigFile(config_name, template_name, template_data)

View File

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

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import asyncio
import traceback
import sys
import os
from ..platform import botmgr as im_mgr
@@ -11,7 +12,7 @@ from ..provider.modelmgr import modelmgr as llm_model_mgr
from ..provider.tools import toolmgr as llm_tool_mgr
from ..config import manager as config_mgr
from ..command import cmdmgr
from ..plugin import connector as plugin_connector
from ..plugin import manager as plugin_mgr
from ..pipeline import pool
from ..pipeline import controller, pipelinemgr
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
@@ -29,7 +30,7 @@ from . import entities as core_entities
class Application:
"""运行时应用对象和上下文"""
"""Runtime application object and context"""
event_loop: asyncio.AbstractEventLoop = None
@@ -46,10 +47,10 @@ class Application:
model_mgr: llm_model_mgr.ModelManager = None
# TODO 移动到 pipeline
# TODO move to pipeline
tool_mgr: llm_tool_mgr.ToolManager = None
# ======= 配置管理器 =======
# ======= Config manager =======
command_cfg: config_mgr.ConfigManager = None # deprecated
@@ -63,7 +64,7 @@ class Application:
instance_config: config_mgr.ConfigManager = None
# ======= 元数据配置管理器 =======
# ======= Metadata config manager =======
sensitive_meta: config_mgr.ConfigManager = None
@@ -74,7 +75,7 @@ class Application:
# =========================
plugin_connector: plugin_connector.PluginRuntimeConnector = None
plugin_mgr: plugin_mgr.PluginManager = None
query_pool: pool.QueryPool = None
@@ -116,7 +117,7 @@ class Application:
async def run(self):
try:
await self.plugin_connector.initialize_plugins()
await self.plugin_mgr.initialize_plugins()
# 后续可能会允许动态重启其他任务
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
@@ -153,14 +154,11 @@ class Application:
except asyncio.CancelledError:
pass
except Exception as e:
self.logger.error(f'应用运行致命异常: {e}')
self.logger.error(f'Application runtime fatal exception: {e}')
self.logger.debug(f'Traceback: {traceback.format_exc()}')
def dispose(self):
self.plugin_connector.dispose()
async def print_web_access_info(self):
"""打印访问 webui 的提示"""
"""Print access webui tips"""
if not os.path.exists(os.path.join('.', 'web/out')):
self.logger.warning('WebUI 文件缺失请根据文档部署https://docs.langbot.app/zh')
@@ -185,3 +183,59 @@ class Application:
""".strip()
for line in tips.split('\n'):
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 ..utils import constants, importutil
# 引入启动阶段实现以便注册
# Import startup stage implementation to register
from . import stages
importutil.import_modules_in_pkg(stages)
@@ -25,7 +25,7 @@ stage_order = [
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']:
constants.debug_mode = True
@@ -33,7 +33,7 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
ap.event_loop = loop
# 执行启动阶段
# Execute startup stage
for stage_name in stage_order:
stage_cls = stage.preregistered_stages[stage_name]
stage_inst = stage_cls()
@@ -47,12 +47,12 @@ async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
async def main(loop: asyncio.AbstractEventLoop):
try:
# 挂系统信号处理
# Hang system signal processing
import signal
def signal_handler(sig, frame):
app_inst.dispose()
print('[Signal] 程序退出.')
print('[Signal] Program exit.')
# ap.shutdown()
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)

View File

@@ -2,8 +2,8 @@ import pip
import os
from ...utils import pkgmgr
# 检查依赖,防止用户未安装
# 左边为引入名称,右边为依赖名称
# Check dependencies to prevent users from not installing
# Left is the import name, right is the dependency name
required_deps = {
'requests': 'requests',
'openai': 'openai',
@@ -65,7 +65,7 @@ async def install_deps(deps: list[str]):
async def precheck_plugin_deps():
print('[Startup] Prechecking plugin dependencies...')
# 只有在plugins目录存在时才执行插件依赖安装
# Only execute plugin dependency installation when the plugins directory exists
if os.path.exists('plugins'):
for dir in os.listdir('plugins'):
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:
# 删除所有现有的logger
# Remove all existing loggers
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
@@ -54,13 +54,13 @@ async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.
handler.setFormatter(color_formatter)
qcg_logger.addHandler(handler)
qcg_logger.debug('日志初始化完成,日志级别:%s' % level)
qcg_logger.debug('Logging initialized, log level: %s' % level)
logging.basicConfig(
level=logging.CRITICAL, # 设置日志输出格式
level=logging.CRITICAL, # Set log output format
format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s',
# 日志输出的格式
# -8表示占位符让输出左对齐输出长度都为8位
datefmt='%Y-%m-%d %H:%M:%S', # 时间输出的格式
# Log output format
# -8 is a placeholder, left-align the output, and output length is 8
datefmt='%Y-%m-%d %H:%M:%S', # Time output format
handlers=[logging.NullHandler()],
)

View File

@@ -1,6 +1,18 @@
from __future__ import annotations
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):
@@ -8,3 +20,157 @@ class LifecycleControlScope(enum.Enum):
PLATFORM = 'platform'
PLUGIN = 'plugin'
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]]
) = []
"""由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]] = []
"""当前阶段暂不支持扩展"""
"""Currently not supported for extension"""
def migration_class(name: str, number: int):
"""注册一个迁移"""
"""Register a migration"""
def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]:
cls.name = name
@@ -23,7 +23,7 @@ def migration_class(name: str, number: int):
class Migration(abc.ABC):
"""一个版本的迁移"""
"""A version migration"""
name: str
@@ -36,10 +36,10 @@ class Migration(abc.ABC):
@abc.abstractmethod
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移"""
"""Determine if the current environment needs to run this migration"""
pass
@abc.abstractmethod
async def run(self):
"""执行迁移"""
"""Run migration"""
pass

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ from .. import note
@note.note_class('SelectionModeOnWindows', 2)
class SelectionModeOnWindows(note.LaunchNote):
"""Windows 上的选择模式提示信息"""
"""Selection mode prompt information on Windows"""
async def need_show(self) -> bool:
return os.name == 'nt'
@@ -19,3 +19,8 @@ class SelectionModeOnWindows(note.LaunchNote):
"""您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""",
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]] = {}
"""预注册的请求处理阶段。在初始化时,所有请求处理阶段类会被注册到此字典中。
"""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):
"""启动阶段"""
"""Booting stage"""
name: str = None
@abc.abstractmethod
async def run(self, ap: app.Application):
"""启动"""
"""Run"""
pass

View File

@@ -1,11 +1,10 @@
from __future__ import annotations
import asyncio
from .. import stage, app
from ...utils import version, proxy, announce
from ...pipeline import pool, controller, pipelinemgr
from ...plugin import connector as plugin_connector
from ...plugin import manager as plugin_mgr
from ...command import cmdmgr
from ...provider.session import sessionmgr as llm_session_mgr
from ...provider.modelmgr import modelmgr as llm_model_mgr
@@ -25,10 +24,10 @@ from .. import taskmgr
@stage.stage_class('BuildAppStage')
class BuildAppStage(stage.BootingStage):
"""构建应用阶段"""
"""Build LangBot application"""
async def run(self, ap: app.Application):
"""构建app对象的各个组件对象并初始化"""
"""Build LangBot application"""
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
discover = discover_engine.ComponentDiscoveryEngine(ap)
@@ -43,7 +42,7 @@ class BuildAppStage(stage.BootingStage):
await ver_mgr.initialize()
ap.ver_mgr = ver_mgr
# 发送公告
# Send announcement
ann_mgr = announce.AnnouncementManager(ap)
ap.ann_mgr = ann_mgr
@@ -60,13 +59,10 @@ class BuildAppStage(stage.BootingStage):
ap.persistence_mgr = persistence_mgr_inst
await persistence_mgr_inst.initialize()
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
await asyncio.sleep(3)
await plugin_connector_inst.initialize()
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
await plugin_connector_inst.initialize()
ap.plugin_connector = plugin_connector_inst
plugin_mgr_inst = plugin_mgr.PluginManager(ap)
await plugin_mgr_inst.initialize()
ap.plugin_mgr = plugin_mgr_inst
await plugin_mgr_inst.load_plugins()
cmd_mgr_inst = cmdmgr.CommandManager(ap)
await cmd_mgr_inst.initialize()

View File

@@ -7,11 +7,18 @@ from .. import stage, app
@stage.stage_class('GenKeysStage')
class GenKeysStage(stage.BootingStage):
"""生成密钥阶段"""
"""Generate keys stage"""
async def run(self, ap: app.Application):
"""启动"""
"""Generate keys"""
if not ap.instance_config.data['system']['jwt']['secret']:
ap.instance_config.data['system']['jwt']['secret'] = secrets.token_hex(16)
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')
class LoadConfigStage(stage.BootingStage):
"""加载配置文件阶段"""
"""Load config file stage"""
async def run(self, ap: app.Application):
"""启动"""
"""Load config file"""
# ======= deprecated =======
if os.path.exists('data/config/command.json'):

View File

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

View File

@@ -8,7 +8,7 @@ from ..bootutils import log
class PersistenceHandler(logging.Handler, object):
"""
保存日志到数据库
Save logs to database
"""
ap: app.Application
@@ -19,9 +19,9 @@ class PersistenceHandler(logging.Handler, object):
def emit(self, record):
"""
emit函数为自定义handler类时必重写的函数这里可以根据需要对日志消息做一些处理比如发送日志到服务器
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 a record)
Emit a record
"""
try:
msg = self.format(record)
@@ -34,10 +34,10 @@ class PersistenceHandler(logging.Handler, object):
@stage.stage_class('SetupLoggerStage')
class SetupLoggerStage(stage.BootingStage):
"""设置日志器阶段"""
"""Setup logger stage"""
async def run(self, ap: app.Application):
"""启动"""
"""Setup logger"""
persistence_handler = PersistenceHandler('LoggerHandler', ap)
extra_handlers = []

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
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):
"""数据库元数据"""
"""Database metadata"""
__tablename__ = 'metadata'

View File

@@ -4,7 +4,7 @@ from .base import Base
class LLMModel(Base):
"""LLM 模型"""
"""LLM model"""
__tablename__ = 'llm_models'

View File

@@ -4,7 +4,7 @@ from .base import Base
class LegacyPipeline(Base):
"""旧版流水线"""
"""Legacy pipeline"""
__tablename__ = 'legacy_pipelines'
@@ -26,7 +26,7 @@ class LegacyPipeline(Base):
class PipelineRunRecord(Base):
"""流水线运行记录"""
"""Pipeline run record"""
__tablename__ = 'pipeline_run_records'

View File

@@ -4,7 +4,7 @@ from .base import Base
class PluginSetting(Base):
"""插件配置"""
"""Plugin setting"""
__tablename__ = 'plugin_settings'
@@ -13,8 +13,6 @@ class PluginSetting(Base):
enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True)
priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
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())
updated_at = sqlalchemy.Column(
sqlalchemy.DateTime,

View File

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

View File

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

View File

@@ -22,12 +22,12 @@ importutil.import_modules_in_pkg(persistence)
class PersistenceManager:
"""持久化模块管理器"""
"""Persistence module manager"""
ap: app.Application
db: database.BaseDatabaseManager
"""数据库管理器"""
"""Database manager"""
meta: sqlalchemy.MetaData
@@ -44,38 +44,6 @@ class PersistenceManager:
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):
# create tables
async with self.get_db_engine().connect() as conn:
@@ -111,7 +79,7 @@ class PersistenceManager:
'stages': pipeline_service.default_stage_order,
'is_default': True,
'name': 'ChatPipeline',
'description': '默认提供的流水线,您配置的机器人、第一个模型将自动绑定到此流水线',
'description': 'Default pipeline provided, your new bots will be automatically bound to this pipeline | 默认提供的流水线,您配置的机器人将自动绑定到此流水线',
'config': pipeline_config,
}
@@ -119,6 +87,38 @@ 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 with self.get_db_engine().connect() as conn:
result = await conn.execute(*args, **kwargs)
@@ -128,13 +128,10 @@ class PersistenceManager:
def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine:
return self.db.get_engine()
def serialize_model(
self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base, masked_columns: list[str] = []
) -> dict:
def serialize_model(self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base) -> dict:
return {
column.name: getattr(data, column.name)
if not isinstance(getattr(data, column.name), (datetime.datetime))
else getattr(data, column.name).isoformat()
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):
"""迁移类装饰器"""
"""Migration class decorator"""
def wrapper(cls: typing.Type[DBMigration]) -> typing.Type[DBMigration]:
cls.number = number
@@ -21,20 +21,20 @@ def migration_class(number: int):
class DBMigration(abc.ABC):
"""数据库迁移"""
"""Database migration"""
number: int
"""迁移号"""
"""Migration number"""
def __init__(self, ap: app.Application):
self.ap = ap
@abc.abstractmethod
async def upgrade(self):
"""升级"""
"""Upgrade"""
pass
@abc.abstractmethod
async def downgrade(self):
"""降级"""
"""Downgrade"""
pass

View File

@@ -15,21 +15,21 @@ from ...entity.persistence import (
@migration.migration_class(1)
class DBMigrateV3Config(migration.DBMigration):
"""从 v3 的配置迁移到 v4 的数据库"""
"""Migrate v3 config to v4 database"""
async def upgrade(self):
"""升级"""
"""Upgrade"""
"""
将 data/config 下的所有配置文件进行迁移。
迁移后,之前的配置文件都保存到 data/legacy/config 下。
迁移后data/metadata/ 下的所有配置文件都保存到 data/legacy/metadata 下。
Migrate all config files under data/config.
After migration, all previous config files are saved under data/legacy/config.
After migration, all config files under data/metadata/ are saved under data/legacy/metadata.
"""
if self.ap.provider_cfg is None:
return
# ======= 迁移模型 =======
# 只迁移当前选中的模型
# ======= Migrate model =======
# Only migrate the currently selected model
model_name = self.ap.provider_cfg.data.get('model', 'gpt-4o')
model_requester = 'openai-chat-completions'
@@ -91,8 +91,8 @@ class DBMigrateV3Config(migration.DBMigration):
sqlalchemy.insert(persistence_model.LLMModel).values(**llm_model_data)
)
# ======= 迁移流水线配置 =======
# 修改到默认流水线
# ======= Migrate pipeline config =======
# Modify to default pipeline
default_pipeline = [
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
for pipeline in (
@@ -184,8 +184,8 @@ class DBMigrateV3Config(migration.DBMigration):
.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', []):
if not adapter.get('enable'):
continue
@@ -207,7 +207,7 @@ class DBMigrateV3Config(migration.DBMigration):
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['api']['port'] = self.ap.system_cfg.data['http-api']['port']
self.ap.instance_config.data['command'] = {
@@ -223,7 +223,7 @@ class DBMigrateV3Config(migration.DBMigration):
await self.ap.instance_config.dump_config()
# ======= move files =======
# 迁移 data/config 下的所有配置文件
# Migrate all config files under data/config
all_legacy_dir_name = [
'config',
# 'metadata',
@@ -246,4 +246,4 @@ class DBMigrateV3Config(migration.DBMigration):
move_legacy_files(dir_name)
async def downgrade(self):
"""降级"""
"""Downgrade"""

View File

@@ -7,10 +7,10 @@ from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(2)
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
"""引用消息合并配置"""
"""Combine quote message config"""
async def upgrade(self):
"""升级"""
"""Upgrade"""
# read all pipelines
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):
"""降级"""
"""Downgrade"""
pass

View File

@@ -7,10 +7,10 @@ from ...entity.persistence import pipeline as persistence_pipeline
@migration.migration_class(3)
class DBMigrateN8nConfig(migration.DBMigration):
"""N8n配置"""
"""N8n config"""
async def upgrade(self):
"""升级"""
"""Upgrade"""
# read all pipelines
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):
"""降级"""
"""Downgrade"""
pass

View File

@@ -1,20 +0,0 @@
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,25 +0,0 @@
import sqlalchemy
from .. import migration
@migration.migration_class(5)
class DBMigratePluginInstallSource(migration.DBMigration):
"""插件安装来源"""
async def upgrade(self):
"""升级"""
# add new column install_source, use default value 'github', via alter table
await self.ap.persistence_mgr.execute_async(
sqlalchemy.text(
"ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'"
)
)
# add new column install_info, use default value {}, via alter table
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 .. import stage, entities
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ...core import entities as core_entities
@stage.stage_class('BanSessionCheckStage')
class BanSessionCheckStage(stage.PipelineStage):
"""访问控制处理阶段
"""Access control processing stage
仅检查query中群号或个人号是否在访问控制列表中。
Only check if the group or personal number in the query is in the access control list.
"""
async def initialize(self, pipeline_config: dict):
pass
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
found = False
mode = query.pipeline_config['trigger']['access-control']['mode']
@@ -41,5 +41,7 @@ class BanSessionCheckStage(stage.PipelineStage):
return entities.StageProcessResult(
result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT,
new_query=query,
console_notice=f'根据访问控制忽略消息: {query.launcher_type.value}_{query.launcher_id}' if not ctn else '',
console_notice=f'Ignore message according to access control: {query.launcher_type.value}_{query.launcher_id}'
if not ctn
else '',
)

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ import aiohttp
from .. import entities
from .. import filter as filter_model
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ....core import entities as core_entities
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'
@@ -26,7 +27,7 @@ class BaiduCloudExamine(filter_model.ContentFilter):
) as resp:
return (await resp.json())['access_token']
async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
async def process(self, query: core_entities.Query, message: str) -> entities.FilterResult:
async with aiohttp.ClientSession() as session:
async with session.post(
BAIDU_EXAMINE_URL.format(await self._get_token()),

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,10 @@ import traceback
from . import strategy
from .. import stage, entities
import langbot_plugin.api.entities.builtin.platform.message as platform_message
from ...core import entities as core_entities
from ...platform.types import message as platform_message
from ...utils import importutil
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from . import strategies
importutil.import_modules_in_pkg(strategies)
@@ -15,9 +16,9 @@ importutil.import_modules_in_pkg(strategies)
@stage.stage_class('LongTextProcessStage')
class LongTextProcessStage(stage.PipelineStage):
"""长消息处理阶段
"""Long message processing stage
改写:
Rewrite:
- resp_message_chain
"""
@@ -35,22 +36,22 @@ class LongTextProcessStage(stage.PipelineStage):
use_font = 'C:/Windows/Fonts/msyh.ttc'
if not os.path.exists(use_font):
self.ap.logger.warn(
'未找到字体文件且无法使用Windows自带字体更换为转发消息组件以发送长消息您可以在配置文件中调整相关设置。'
'Font file not found, and Windows system font cannot be used, switch to forward message component to send long messages, you can adjust the related settings in the configuration file.'
)
config['blob_message_strategy'] = 'forward'
else:
self.ap.logger.info('使用Windows自带字体:' + use_font)
self.ap.logger.info('Using Windows system font: ' + use_font)
config['font-path'] = use_font
else:
self.ap.logger.warn(
'未找到字体文件,且无法使用系统自带字体,更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。'
'Font file not found, and system font cannot be used, switch to forward message component to send long messages, you can adjust the related settings in the configuration file.'
)
pipeline_config['output']['long-text-processing']['strategy'] = 'forward'
except Exception:
traceback.print_exc()
self.ap.logger.error(
'加载字体文件失败({}),更换为转发消息组件以发送长消息,您可以在配置文件中调整相关设置。'.format(
'Failed to load font file ({}), switch to forward message component to send long messages, you can adjust the related settings in the configuration file.'.format(
use_font
)
)
@@ -62,12 +63,12 @@ class LongTextProcessStage(stage.PipelineStage):
self.strategy_impl = strategy_cls(self.ap)
break
else:
raise ValueError(f'未找到名为 {config["strategy"]} 的长消息处理策略')
raise ValueError(f'Long message processing strategy not found: {config["strategy"]}')
await self.strategy_impl.initialize()
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
# 检查是否包含非 Plain 组件
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
# Check if it contains non-Plain components
contains_non_plain = False
for msg in query.resp_message_chain[-1]:
@@ -76,7 +77,7 @@ class LongTextProcessStage(stage.PipelineStage):
break
if contains_non_plain:
self.ap.logger.debug('消息中包含非 Plain 组件,跳过长消息处理。')
self.ap.logger.debug('Message contains non-Plain components, skip long message processing.')
elif (
len(str(query.resp_message_chain[-1]))
> query.pipeline_config['output']['long-text-processing']['threshold']

View File

@@ -3,9 +3,9 @@ from __future__ import annotations
from .. import strategy as strategy_model
from ....core import entities as core_entities
from ....platform.types import message as platform_message
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.builtin.platform.message as platform_message
ForwardMessageDiaplay = platform_message.ForwardMessageDiaplay
Forward = platform_message.Forward
@@ -13,19 +13,19 @@ Forward = platform_message.Forward
@strategy_model.strategy_class('forward')
class ForwardComponentStrategy(strategy_model.LongTextStrategy):
async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]:
async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]:
display = ForwardMessageDiaplay(
title='群聊的聊天记录',
brief='[聊天记录]',
source='聊天记录',
preview=['QQ用户: ' + message],
summary='查看1条转发消息',
title='Group chat history',
brief='[Chat history]',
source='Chat history',
preview=['User: ' + message],
summary='View 1 forwarded message',
)
node_list = [
platform_message.ForwardMessageNode(
sender_id=query.adapter.bot_account_id,
sender_name='QQ用户',
sender_name='User',
message_chain=platform_message.MessageChain([message]),
)
]

View File

@@ -8,10 +8,10 @@ import re
from PIL import Image, ImageDraw, ImageFont
import functools
from ....platform.types import message as platform_message
from .. import strategy as strategy_model
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.builtin.platform.message as platform_message
from ....core import entities as core_entities
@strategy_model.strategy_class('image')
@@ -27,7 +27,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
encoding='utf-8',
)
async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]:
async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]:
img_path = self.text_to_image(
text_str=message,
save_as='temp/{}.png'.format(int(time.time())),
@@ -131,7 +131,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
text_str: str,
save_as='temp.png',
width=800,
query: pipeline_query.Query = None,
query: core_entities.Query = None,
):
text_str = text_str.replace('\t', ' ')

View File

@@ -4,9 +4,8 @@ import typing
from ...core import app
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ...core import entities as core_entities
from ...platform.types import message as platform_message
preregistered_strategies: list[typing.Type[LongTextStrategy]] = []
@@ -15,13 +14,13 @@ preregistered_strategies: list[typing.Type[LongTextStrategy]] = []
def strategy_class(
name: str,
) -> typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]:
"""长文本处理策略类装饰器
"""Long text processing strategy class decorator
Args:
name (str): 策略名称
name (str): Strategy name
Returns:
typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]: 装饰器
typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]: Decorator
"""
def decorator(cls: typing.Type[LongTextStrategy]) -> typing.Type[LongTextStrategy]:
@@ -37,7 +36,7 @@ def strategy_class(
class LongTextStrategy(metaclass=abc.ABCMeta):
"""长文本处理策略抽象类"""
"""Long text processing strategy abstract class"""
name: str
@@ -50,16 +49,16 @@ class LongTextStrategy(metaclass=abc.ABCMeta):
pass
@abc.abstractmethod
async def process(self, message: str, query: pipeline_query.Query) -> list[platform_message.MessageComponent]:
"""处理长文本
async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]:
"""Process long text
在 platform.json 中配置 long-text-process 字段,只要 文本长度超过了 threshold 就会调用此方法
If the text length exceeds the threshold, this method will be called.
Args:
message (str): 消息
query (core_entities.Query): 此次请求的上下文对象
message (str): Message
query (core_entities.Query): Query object
Returns:
list[platform_message.MessageComponent]: 转换后的 平台 消息组件列表
list[platform_message.MessageComponent]: Converted platform message components
"""
return []

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from .. import stage, entities
from ...core import entities as core_entities
from . import truncator
from ...utils import importutil
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from . import truncators
importutil.import_modules_in_pkg(truncators)
@@ -11,9 +12,9 @@ importutil.import_modules_in_pkg(truncators)
@stage.stage_class('ConversationMessageTruncator')
class ConversationMessageTruncator(stage.PipelineStage):
"""会话消息截断器
"""Conversation message truncator
用于截断会话消息链,以适应平台消息长度限制。
Used to truncate the conversation message chain to adapt to the LLM message length limit.
"""
trun: truncator.Truncator
@@ -26,10 +27,10 @@ class ConversationMessageTruncator(stage.PipelineStage):
self.trun = trun(self.ap)
break
else:
raise ValueError(f'未知的截断器: {use_method}')
raise ValueError(f'Unknown truncator: {use_method}')
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
"""处理"""
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
"""Process"""
query = await self.trun.truncate(query)
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
import typing
import abc
from ...core import app
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ...core import entities as core_entities, app
preregistered_truncators: list[typing.Type[Truncator]] = []
@@ -47,7 +47,7 @@ class Truncator(abc.ABC):
pass
@abc.abstractmethod
async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query:
async def truncate(self, query: core_entities.Query) -> core_entities.Query:
"""截断
一般只需要操作query.messages也可以扩展操作query.prompt, query.user_message。

View File

@@ -1,22 +1,22 @@
from __future__ import annotations
from .. import truncator
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from ....core import entities as core_entities
@truncator.truncator_class('round')
class RoundTruncator(truncator.Truncator):
"""前文回合数阶段器"""
"""Truncate the conversation message chain to adapt to the LLM message length limit."""
async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query:
"""截断"""
async def truncate(self, query: core_entities.Query) -> core_entities.Query:
"""Truncate"""
max_round = query.pipeline_config['ai']['local-agent']['max-round']
temp_messages = []
current_round = 0
# 从后往前遍历
# Traverse from back to front
for msg in query.messages[::-1]:
if current_round < max_round:
temp_messages.append(msg)

View File

@@ -5,18 +5,14 @@ import traceback
import sqlalchemy
from ..core import app
from ..core import app, entities
from . import entities as pipeline_entities
from ..entity.persistence import pipeline as persistence_pipeline
from . import stage
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.platform.events as platform_events
import langbot_plugin.api.entities.events as events
from ..platform.types import message as platform_message, events as platform_events
from ..plugin import events
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 . import (
resprule,
bansess,
@@ -79,17 +75,17 @@ class RuntimePipeline:
self.pipeline_entity = pipeline_entity
self.stage_containers = stage_containers
async def run(self, query: pipeline_query.Query):
async def run(self, query: entities.Query):
query.pipeline_config = self.pipeline_entity.config
await self.process_query(query)
async def _check_output(self, query: pipeline_query.Query, result: pipeline_entities.StageProcessResult):
async def _check_output(self, query: entities.Query, result: pipeline_entities.StageProcessResult):
"""检查输出"""
if result.user_notice:
# 处理str类型
if isinstance(result.user_notice, str):
result.user_notice = platform_message.MessageChain([platform_message.Plain(text=result.user_notice)])
result.user_notice = platform_message.MessageChain(platform_message.Plain(result.user_notice))
elif isinstance(result.user_notice, list):
result.user_notice = platform_message.MessageChain(*result.user_notice)
@@ -113,7 +109,7 @@ class RuntimePipeline:
async def _execute_from_stage(
self,
stage_index: int,
query: pipeline_query.Query,
query: entities.Query,
):
"""从指定阶段开始执行,实现了责任链模式和基于生成器的阶段分叉功能。
@@ -140,7 +136,7 @@ class RuntimePipeline:
while i < len(self.stage_containers):
stage_container = self.stage_containers[i]
query.current_stage_name = stage_container.inst_name # 标记到 Query 对象里
query.current_stage = stage_container # 标记到 Query 对象里
result = stage_container.inst.process(query, stage_container.inst_name)
@@ -173,26 +169,26 @@ class RuntimePipeline:
i += 1
async def process_query(self, query: pipeline_query.Query):
async def process_query(self, query: entities.Query):
"""处理请求"""
try:
# ======== 触发 MessageReceived 事件 ========
event_type = (
events.PersonMessageReceived
if query.launcher_type == provider_session.LauncherTypes.PERSON
if query.launcher_type == entities.LauncherTypes.PERSON
else events.GroupMessageReceived
)
event_obj = event_type(
query=query,
launcher_type=query.launcher_type.value,
launcher_id=query.launcher_id,
sender_id=query.sender_id,
message_chain=query.message_chain,
event_ctx = await self.ap.plugin_mgr.emit_event(
event=event_type(
launcher_type=query.launcher_type.value,
launcher_id=query.launcher_id,
sender_id=query.sender_id,
message_chain=query.message_chain,
query=query,
)
)
event_ctx = await self.ap.plugin_connector.emit_event(event_obj)
if event_ctx.is_prevented_default():
return
@@ -200,12 +196,11 @@ class RuntimePipeline:
await self._execute_from_stage(0, query)
except Exception as e:
inst_name = query.current_stage_name if query.current_stage_name else 'unknown'
inst_name = query.current_stage.inst_name if query.current_stage else 'unknown'
self.ap.logger.error(f'处理请求时出错 query_id={query.query_id} stage={inst_name} : {e}')
self.ap.logger.error(f'Traceback: {traceback.format_exc()}')
finally:
self.ap.logger.debug(f'Query {query} processed')
del self.ap.query_pool.cached_queries[query.query_id]
class PipelineManager:

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