Compare commits

...

11 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
66 changed files with 504 additions and 423 deletions

View File

@@ -9,7 +9,7 @@
*请在方括号间写`x`以打勾 / Please tick the box with `x`* *请在方括号间写`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? - [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected. - [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.

View File

@@ -1,32 +1,25 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <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> </a>
<div align="center"> <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://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a> <a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a> <a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a>
<a href="https://github.com/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> </div>
@@ -44,7 +37,7 @@
#### Docker Compose 部署 #### Docker Compose 部署
```bash ```bash
git clone https://github.com/RockChinQ/LangBot git clone https://github.com/langbot-app/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -149,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"> <a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a> </a>
## 😎 保持更新 ## 😎 保持更新

View File

@@ -1,30 +1,21 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <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> </a>
<div align="center"> <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://langbot.app">Home</a>
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a> <a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> <a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a>
<a href="https://github.com/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> <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 align="center">
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README.md) / English / [日本語](README_JP.md) / (PR for your language)
</div> </div>
@@ -42,7 +33,7 @@
#### Docker Compose Deployment #### Docker Compose Deployment
```bash ```bash
git clone https://github.com/RockChinQ/LangBot git clone https://github.com/langbot-app/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -132,10 +123,10 @@ Directly use the released version to run, see the [Manual Deployment](https://do
## 🤝 Community Contribution ## 🤝 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"> <a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a> </a>
## 😎 Stay Ahead ## 😎 Stay Ahead

View File

@@ -1,29 +1,21 @@
<p align="center"> <p align="center">
<a href="https://langbot.app"> <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> </a>
<div align="center"> <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://langbot.app">ホーム</a>
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> <a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> <a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a>
<a href="https://github.com/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">
😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖
</div>
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
[简体中文](README_CN.md) / [English](README.md) / [日本語](README_JP.md) / (PR for your language)
</div> </div>
@@ -41,7 +33,7 @@
#### Docker Compose デプロイ #### Docker Compose デプロイ
```bash ```bash
git clone https://github.com/RockChinQ/LangBot git clone https://github.com/langbot-app/LangBot
cd LangBot cd LangBot
docker compose up -d docker compose up -d
``` ```
@@ -131,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"> <a href="https://github.com/langbot-app/LangBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" /> <img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
</a> </a>
## 😎 最新情報を入手 ## 😎 最新情報を入手

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 📖 Documentation 文档地址: https://docs.langbot.app
""" """

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ class PluginsRouterGroup(group.RouterGroup):
self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx), self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name=f'plugin-update-{plugin_name}', name=f'plugin-update-{plugin_name}',
label=f'更新插件 {plugin_name}', label=f'Updating plugin {plugin_name}',
context=ctx, context=ctx,
) )
return self.success(data={'task_id': wrapper.id}) return self.success(data={'task_id': wrapper.id})
@@ -62,7 +62,7 @@ class PluginsRouterGroup(group.RouterGroup):
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx), self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name=f'plugin-remove-{plugin_name}', name=f'plugin-remove-{plugin_name}',
label=f'删除插件 {plugin_name}', label=f'Removing plugin {plugin_name}',
context=ctx, context=ctx,
) )
@@ -102,7 +102,7 @@ class PluginsRouterGroup(group.RouterGroup):
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx), self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
kind='plugin-operation', kind='plugin-operation',
name='plugin-install-github', name='plugin-install-github',
label=f'安装插件 ...{short_source_str}', label=f'Installing plugin ...{short_source_str}',
context=ctx, context=ctx,
) )

View File

@@ -14,7 +14,7 @@ class UserRouterGroup(group.RouterGroup):
return self.success(data={'initialized': await self.ap.user_service.is_initialized()}) return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
if await self.ap.user_service.is_initialized(): if await self.ap.user_service.is_initialized():
return self.fail(1, '系统已初始化') return self.fail(1, 'System already initialized')
json_data = await quart.request.json json_data = await quart.request.json
@@ -32,7 +32,7 @@ class UserRouterGroup(group.RouterGroup):
try: try:
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password']) token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
except argon2.exceptions.VerifyMismatchError: except argon2.exceptions.VerifyMismatchError:
return self.fail(1, '用户名或密码错误') return self.fail(1, 'Invalid username or password')
return self.success(data={'token': token}) return self.success(data={'token': token})
@@ -54,15 +54,15 @@ class UserRouterGroup(group.RouterGroup):
await asyncio.sleep(3) await asyncio.sleep(3)
if not await self.ap.user_service.is_initialized(): if not await self.ap.user_service.is_initialized():
return self.http_status(400, -1, 'system not initialized') return self.http_status(400, -1, 'System not initialized')
user_obj = await self.ap.user_service.get_user_by_email(user_email) user_obj = await self.ap.user_service.get_user_by_email(user_email)
if user_obj is None: if user_obj is None:
return self.http_status(400, -1, 'user not found') return self.http_status(400, -1, 'User not found')
if recovery_key != self.ap.instance_config.data['system']['recovery_key']: if recovery_key != self.ap.instance_config.data['system']['recovery_key']:
return self.http_status(403, -1, 'invalid recovery key') return self.http_status(403, -1, 'Invalid recovery key')
await self.ap.user_service.reset_password(user_email, new_password) await self.ap.user_service.reset_password(user_email, new_password)

View File

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

View File

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

View File

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

View File

@@ -7,13 +7,13 @@ from .. import model as file_model
class PythonModuleConfigFile(file_model.ConfigFile): class PythonModuleConfigFile(file_model.ConfigFile):
"""Python模块配置文件""" """Python module config file"""
config_file_name: str = None config_file_name: str = None
"""配置文件名""" """Config file name"""
template_file_name: str = None template_file_name: str = None
"""模板文件名""" """Template file name"""
def __init__(self, config_file_name: str, template_file_name: str) -> None: def __init__(self, config_file_name: str, template_file_name: str) -> None:
self.config_file_name = config_file_name self.config_file_name = config_file_name
@@ -42,7 +42,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
cfg[key] = getattr(module, key) cfg[key] = getattr(module, key)
# 从模板模块文件中进行补全 # complete from template module file
if completion: if completion:
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0] module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -60,7 +60,7 @@ class PythonModuleConfigFile(file_model.ConfigFile):
return cfg return cfg
async def save(self, data: dict): async def save(self, data: dict):
logging.warning('Python模块配置文件不支持保存') logging.warning('Python module config file does not support saving')
def save_sync(self, data: dict): 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): class YAMLConfigFile(file_model.ConfigFile):
"""YAML配置文件""" """YAML config file"""
def __init__( def __init__(
self, self,
@@ -42,7 +42,7 @@ class YAMLConfigFile(file_model.ConfigFile):
try: try:
cfg = yaml.load(f, Loader=yaml.FullLoader) cfg = yaml.load(f, Loader=yaml.FullLoader)
except yaml.YAMLError as e: except yaml.YAMLError as e:
raise Exception(f'配置文件 {self.config_file_name} 语法错误: {e}') raise Exception(f'Syntax error in config file {self.config_file_name}: {e}')
if completion: if completion:
for key in self.template_data: for key in self.template_data:

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ from . import entities as core_entities
class Application: class Application:
"""运行时应用对象和上下文""" """Runtime application object and context"""
event_loop: asyncio.AbstractEventLoop = None event_loop: asyncio.AbstractEventLoop = None
@@ -47,10 +47,10 @@ class Application:
model_mgr: llm_model_mgr.ModelManager = None model_mgr: llm_model_mgr.ModelManager = None
# TODO 移动到 pipeline # TODO move to pipeline
tool_mgr: llm_tool_mgr.ToolManager = None tool_mgr: llm_tool_mgr.ToolManager = None
# ======= 配置管理器 ======= # ======= Config manager =======
command_cfg: config_mgr.ConfigManager = None # deprecated command_cfg: config_mgr.ConfigManager = None # deprecated
@@ -64,7 +64,7 @@ class Application:
instance_config: config_mgr.ConfigManager = None instance_config: config_mgr.ConfigManager = None
# ======= 元数据配置管理器 ======= # ======= Metadata config manager =======
sensitive_meta: config_mgr.ConfigManager = None sensitive_meta: config_mgr.ConfigManager = None
@@ -154,11 +154,11 @@ class Application:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: 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()}') self.logger.debug(f'Traceback: {traceback.format_exc()}')
async def print_web_access_info(self): async def print_web_access_info(self):
"""打印访问 webui 的提示""" """Print access webui tips"""
if not os.path.exists(os.path.join('.', 'web/out')): if not os.path.exists(os.path.join('.', 'web/out')):
self.logger.warning('WebUI 文件缺失请根据文档部署https://docs.langbot.app/zh') self.logger.warning('WebUI 文件缺失请根据文档部署https://docs.langbot.app/zh')
@@ -190,7 +190,7 @@ class Application:
): ):
match scope: match scope:
case core_entities.LifecycleControlScope.PLATFORM.value: case core_entities.LifecycleControlScope.PLATFORM.value:
self.logger.info('执行热重载 scope=' + scope) self.logger.info('Hot reload scope=' + scope)
await self.platform_mgr.shutdown() await self.platform_mgr.shutdown()
self.platform_mgr = im_mgr.PlatformManager(self) self.platform_mgr = im_mgr.PlatformManager(self)
@@ -206,7 +206,7 @@ class Application:
], ],
) )
case core_entities.LifecycleControlScope.PLUGIN.value: case core_entities.LifecycleControlScope.PLUGIN.value:
self.logger.info('执行热重载 scope=' + scope) self.logger.info('Hot reload scope=' + scope)
await self.plugin_mgr.destroy_plugins() await self.plugin_mgr.destroy_plugins()
# 删除 sys.module 中所有的 plugins/* 下的模块 # 删除 sys.module 中所有的 plugins/* 下的模块
@@ -222,7 +222,7 @@ class Application:
await self.plugin_mgr.load_plugins() await self.plugin_mgr.load_plugins()
await self.plugin_mgr.initialize_plugins() await self.plugin_mgr.initialize_plugins()
case core_entities.LifecycleControlScope.PROVIDER.value: case core_entities.LifecycleControlScope.PROVIDER.value:
self.logger.info('执行热重载 scope=' + scope) self.logger.info('Hot reload scope=' + scope)
await self.tool_mgr.shutdown() await self.tool_mgr.shutdown()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,10 +24,10 @@ from .. import taskmgr
@stage.stage_class('BuildAppStage') @stage.stage_class('BuildAppStage')
class BuildAppStage(stage.BootingStage): class BuildAppStage(stage.BootingStage):
"""构建应用阶段""" """Build LangBot application"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""构建app对象的各个组件对象并初始化""" """Build LangBot application"""
ap.task_mgr = taskmgr.AsyncTaskManager(ap) ap.task_mgr = taskmgr.AsyncTaskManager(ap)
discover = discover_engine.ComponentDiscoveryEngine(ap) discover = discover_engine.ComponentDiscoveryEngine(ap)
@@ -42,7 +42,7 @@ class BuildAppStage(stage.BootingStage):
await ver_mgr.initialize() await ver_mgr.initialize()
ap.ver_mgr = ver_mgr ap.ver_mgr = ver_mgr
# 发送公告 # Send announcement
ann_mgr = announce.AnnouncementManager(ap) ann_mgr = announce.AnnouncementManager(ap)
ap.ann_mgr = ann_mgr ap.ann_mgr = ann_mgr

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from ..bootutils import log
class PersistenceHandler(logging.Handler, object): class PersistenceHandler(logging.Handler, object):
""" """
保存日志到数据库 Save logs to database
""" """
ap: app.Application ap: app.Application
@@ -19,9 +19,9 @@ class PersistenceHandler(logging.Handler, object):
def emit(self, record): def emit(self, record):
""" """
emit函数为自定义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: try:
msg = self.format(record) msg = self.format(record)
@@ -34,10 +34,10 @@ class PersistenceHandler(logging.Handler, object):
@stage.stage_class('SetupLoggerStage') @stage.stage_class('SetupLoggerStage')
class SetupLoggerStage(stage.BootingStage): class SetupLoggerStage(stage.BootingStage):
"""设置日志器阶段""" """Setup logger stage"""
async def run(self, ap: app.Application): async def run(self, ap: app.Application):
"""启动""" """Setup logger"""
persistence_handler = PersistenceHandler('LoggerHandler', ap) persistence_handler = PersistenceHandler('LoggerHandler', ap)
extra_handlers = [] extra_handlers = []

View File

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

View File

@@ -9,13 +9,13 @@ from . import entities as core_entities
class TaskContext: class TaskContext:
"""任务跟踪上下文""" """Task tracking context"""
current_action: str current_action: str
"""当前正在执行的动作""" """Current action being executed"""
log: str log: str
"""记录日志""" """Log"""
def __init__(self): def __init__(self):
self.current_action = 'default' self.current_action = 'default'
@@ -58,40 +58,40 @@ placeholder_context: TaskContext | None = None
class TaskWrapper: class TaskWrapper:
"""任务包装器""" """Task wrapper"""
_id_index: int = 0 _id_index: int = 0
"""任务ID索引""" """Task ID index"""
id: int 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 = '' name: str = ''
"""任务唯一名称""" """Task unique name"""
label: str = '' label: str = ''
"""任务显示名称""" """Task display name"""
task_context: TaskContext task_context: TaskContext
"""任务上下文""" """Task context"""
task: asyncio.Task task: asyncio.Task
"""任务""" """Task"""
task_stack: list = None task_stack: list = None
"""任务堆栈""" """Task stack"""
ap: app.Application ap: app.Application
"""应用实例""" """Application instance"""
scopes: list[core_entities.LifecycleControlScope] scopes: list[core_entities.LifecycleControlScope]
"""任务所属生命周期控制范围""" """Task scope"""
def __init__( def __init__(
self, self,
@@ -165,13 +165,13 @@ class TaskWrapper:
class AsyncTaskManager: class AsyncTaskManager:
"""保存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 ap: app.Application
tasks: list[TaskWrapper] tasks: list[TaskWrapper]
"""所有任务""" """All tasks"""
def __init__(self, ap: app.Application): def __init__(self, ap: app.Application):
self.ap = ap self.ap = ap

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ from .base import Base
class PluginSetting(Base): class PluginSetting(Base):
"""插件配置""" """Plugin setting"""
__tablename__ = 'plugin_settings' __tablename__ = 'plugin_settings'

View File

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

View File

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

View File

@@ -22,12 +22,12 @@ importutil.import_modules_in_pkg(persistence)
class PersistenceManager: class PersistenceManager:
"""持久化模块管理器""" """Persistence module manager"""
ap: app.Application ap: app.Application
db: database.BaseDatabaseManager db: database.BaseDatabaseManager
"""数据库管理器""" """Database manager"""
meta: sqlalchemy.MetaData meta: sqlalchemy.MetaData
@@ -79,7 +79,7 @@ class PersistenceManager:
'stages': pipeline_service.default_stage_order, 'stages': pipeline_service.default_stage_order,
'is_default': True, 'is_default': True,
'name': 'ChatPipeline', 'name': 'ChatPipeline',
'description': '默认提供的流水线,您配置的机器人、第一个模型将自动绑定到此流水线', 'description': 'Default pipeline provided, your new bots will be automatically bound to this pipeline | 默认提供的流水线,您配置的机器人将自动绑定到此流水线',
'config': pipeline_config, 'config': pipeline_config,
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,9 @@ from ...core import entities as core_entities
@stage.stage_class('BanSessionCheckStage') @stage.stage_class('BanSessionCheckStage')
class BanSessionCheckStage(stage.PipelineStage): 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): async def initialize(self, pipeline_config: dict):
@@ -41,5 +41,7 @@ class BanSessionCheckStage(stage.PipelineStage):
return entities.StageProcessResult( return entities.StageProcessResult(
result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT, result_type=entities.ResultType.CONTINUE if ctn else entities.ResultType.INTERRUPT,
new_query=query, new_query=query,
console_notice=f'根据访问控制忽略消息: {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

@@ -13,13 +13,13 @@ preregistered_filters: list[typing.Type[ContentFilter]] = []
def filter_class( def filter_class(
name: str, name: str,
) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: ) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]:
"""内容过滤器类装饰器 """Content filter class decorator
Args: Args:
name (str): 过滤器名称 name (str): Filter name
Returns: 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]: def decorator(cls: typing.Type[ContentFilter]) -> typing.Type[ContentFilter]:
@@ -35,7 +35,7 @@ def filter_class(
class ContentFilter(metaclass=abc.ABCMeta): class ContentFilter(metaclass=abc.ABCMeta):
"""内容过滤器抽象类""" """Content filter abstract class"""
name: str name: str
@@ -46,31 +46,31 @@ class ContentFilter(metaclass=abc.ABCMeta):
@property @property
def enable_stages(self): def enable_stages(self):
"""启用的阶段 """Enabled stages
默认为消息请求AI前后的两个阶段。 Default is the two stages before and after the message request to AI.
entity.EnableStage.PRE: 消息请求AI前此时需要检查的内容是用户的输入消息。 entity.EnableStage.PRE: Before message request to AI, the content to check is the user's input message.
entity.EnableStage.POST: 消息请求AI后此时需要检查的内容是AI的回复消息。 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] return [entities.EnableStage.PRE, entities.EnableStage.POST]
async def initialize(self): async def initialize(self):
"""初始化过滤器""" """Initialize filter"""
pass pass
@abc.abstractmethod @abc.abstractmethod
async def process(self, query: core_entities.Query, message: str = None, image_url=None) -> entities.FilterResult: async def process(self, query: 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: Args:
message (str): 需要检查的内容 message (str): Content to check
image_url (str): 要检查的图片的 URL image_url (str): URL of the image to check
Returns: Returns:
entities.FilterResult: 过滤结果,具体内容请查看 entities.FilterResult 类的文档 entities.FilterResult: Filter result, please refer to the documentation of entities.FilterResult class
""" """
raise NotImplementedError raise NotImplementedError

View File

@@ -8,7 +8,7 @@ from ....core import entities as core_entities
@filter_model.filter_class('ban-word-filter') @filter_model.filter_class('ban-word-filter')
class BanWordFilter(filter_model.ContentFilter): class BanWordFilter(filter_model.ContentFilter):
"""根据内容过滤""" """Filter content"""
async def initialize(self): async def initialize(self):
pass pass

View File

@@ -8,7 +8,7 @@ from ....core import entities as core_entities
@filter_model.filter_class('content-ignore') @filter_model.filter_class('content-ignore')
class ContentIgnore(filter_model.ContentFilter): class ContentIgnore(filter_model.ContentFilter):
"""根据内容忽略消息""" """Ignore message according to content"""
@property @property
def enable_stages(self): def enable_stages(self):
@@ -24,7 +24,7 @@ class ContentIgnore(filter_model.ContentFilter):
level=entities.ResultLevel.BLOCK, level=entities.ResultLevel.BLOCK,
replacement='', replacement='',
user_notice='', user_notice='',
console_notice='根据 ignore_rules 中的 prefix 规则,忽略消息', console_notice='Ignore message according to prefix rule in ignore_rules',
) )
if 'regexp' in query.pipeline_config['trigger']['ignore-rules']: if 'regexp' in query.pipeline_config['trigger']['ignore-rules']:
@@ -34,7 +34,7 @@ class ContentIgnore(filter_model.ContentFilter):
level=entities.ResultLevel.BLOCK, level=entities.ResultLevel.BLOCK,
replacement='', replacement='',
user_notice='', user_notice='',
console_notice='根据 ignore_rules 中的 regexp 规则,忽略消息', console_notice='Ignore message according to regexp rule in ignore_rules',
) )
return entities.FilterResult( return entities.FilterResult(

View File

@@ -16,9 +16,9 @@ importutil.import_modules_in_pkg(strategies)
@stage.stage_class('LongTextProcessStage') @stage.stage_class('LongTextProcessStage')
class LongTextProcessStage(stage.PipelineStage): class LongTextProcessStage(stage.PipelineStage):
"""长消息处理阶段 """Long message processing stage
改写: Rewrite:
- resp_message_chain - resp_message_chain
""" """
@@ -36,22 +36,22 @@ class LongTextProcessStage(stage.PipelineStage):
use_font = 'C:/Windows/Fonts/msyh.ttc' use_font = 'C:/Windows/Fonts/msyh.ttc'
if not os.path.exists(use_font): if not os.path.exists(use_font):
self.ap.logger.warn( 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' config['blob_message_strategy'] = 'forward'
else: else:
self.ap.logger.info('使用Windows自带字体:' + use_font) self.ap.logger.info('Using Windows system font: ' + use_font)
config['font-path'] = use_font config['font-path'] = use_font
else: else:
self.ap.logger.warn( 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' pipeline_config['output']['long-text-processing']['strategy'] = 'forward'
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
self.ap.logger.error( 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 use_font
) )
) )
@@ -63,12 +63,12 @@ class LongTextProcessStage(stage.PipelineStage):
self.strategy_impl = strategy_cls(self.ap) self.strategy_impl = strategy_cls(self.ap)
break break
else: else:
raise ValueError(f'未找到名为 {config["strategy"]} 的长消息处理策略') raise ValueError(f'Long message processing strategy not found: {config["strategy"]}')
await self.strategy_impl.initialize() await self.strategy_impl.initialize()
async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult: async def process(self, query: core_entities.Query, stage_inst_name: str) -> entities.StageProcessResult:
# 检查是否包含非 Plain 组件 # Check if it contains non-Plain components
contains_non_plain = False contains_non_plain = False
for msg in query.resp_message_chain[-1]: for msg in query.resp_message_chain[-1]:
@@ -77,7 +77,7 @@ class LongTextProcessStage(stage.PipelineStage):
break break
if contains_non_plain: if contains_non_plain:
self.ap.logger.debug('消息中包含非 Plain 组件,跳过长消息处理。') self.ap.logger.debug('Message contains non-Plain components, skip long message processing.')
elif ( elif (
len(str(query.resp_message_chain[-1])) len(str(query.resp_message_chain[-1]))
> query.pipeline_config['output']['long-text-processing']['threshold'] > query.pipeline_config['output']['long-text-processing']['threshold']

View File

@@ -15,17 +15,17 @@ Forward = platform_message.Forward
class ForwardComponentStrategy(strategy_model.LongTextStrategy): class ForwardComponentStrategy(strategy_model.LongTextStrategy):
async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]: async def process(self, message: str, query: core_entities.Query) -> list[platform_message.MessageComponent]:
display = ForwardMessageDiaplay( display = ForwardMessageDiaplay(
title='群聊的聊天记录', title='Group chat history',
brief='[聊天记录]', brief='[Chat history]',
source='聊天记录', source='Chat history',
preview=['QQ用户: ' + message], preview=['User: ' + message],
summary='查看1条转发消息', summary='View 1 forwarded message',
) )
node_list = [ node_list = [
platform_message.ForwardMessageNode( platform_message.ForwardMessageNode(
sender_id=query.adapter.bot_account_id, sender_id=query.adapter.bot_account_id,
sender_name='QQ用户', sender_name='User',
message_chain=platform_message.MessageChain([message]), message_chain=platform_message.MessageChain([message]),
) )
] ]

View File

@@ -14,13 +14,13 @@ preregistered_strategies: list[typing.Type[LongTextStrategy]] = []
def strategy_class( def strategy_class(
name: str, name: str,
) -> typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]: ) -> typing.Callable[[typing.Type[LongTextStrategy]], typing.Type[LongTextStrategy]]:
"""长文本处理策略类装饰器 """Long text processing strategy class decorator
Args: Args:
name (str): 策略名称 name (str): Strategy name
Returns: 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]: def decorator(cls: typing.Type[LongTextStrategy]) -> typing.Type[LongTextStrategy]:
@@ -36,7 +36,7 @@ def strategy_class(
class LongTextStrategy(metaclass=abc.ABCMeta): class LongTextStrategy(metaclass=abc.ABCMeta):
"""长文本处理策略抽象类""" """Long text processing strategy abstract class"""
name: str name: str
@@ -50,15 +50,15 @@ class LongTextStrategy(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
async def process(self, message: str, query: core_entities.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: Args:
message (str): 消息 message (str): Message
query (core_entities.Query): 此次请求的上下文对象 query (core_entities.Query): Query object
Returns: Returns:
list[platform_message.MessageComponent]: 转换后的 平台 消息组件列表 list[platform_message.MessageComponent]: Converted platform message components
""" """
return [] return []

View File

@@ -12,9 +12,9 @@ importutil.import_modules_in_pkg(truncators)
@stage.stage_class('ConversationMessageTruncator') @stage.stage_class('ConversationMessageTruncator')
class ConversationMessageTruncator(stage.PipelineStage): 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 trun: truncator.Truncator
@@ -27,10 +27,10 @@ class ConversationMessageTruncator(stage.PipelineStage):
self.trun = trun(self.ap) self.trun = trun(self.ap)
break break
else: else:
raise ValueError(f'未知的截断器: {use_method}') raise ValueError(f'Unknown truncator: {use_method}')
async def process(self, query: core_entities.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) query = await self.trun.truncate(query)
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)

View File

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

View File

@@ -11,11 +11,11 @@ from ...platform.types import message as platform_message
@stage.stage_class('PreProcessor') @stage.stage_class('PreProcessor')
class PreProcessor(stage.PipelineStage): class PreProcessor(stage.PipelineStage):
"""请求预处理阶段 """Request pre-processing stage
签出会话、prompt、上文、模型、内容函数。 Check out session, prompt, context, model, and content functions.
改写: Rewrite:
- session - session
- prompt - prompt
- messages - messages
@@ -29,12 +29,12 @@ class PreProcessor(stage.PipelineStage):
query: core_entities.Query, query: core_entities.Query,
stage_inst_name: str, stage_inst_name: str,
) -> entities.StageProcessResult: ) -> entities.StageProcessResult:
"""处理""" """Process"""
selected_runner = query.pipeline_config['ai']['runner']['runner'] selected_runner = query.pipeline_config['ai']['runner']['runner']
session = await self.ap.sess_mgr.get_session(query) session = await self.ap.sess_mgr.get_session(query)
# local-agent 时,llm_model None # When not local-agent, llm_model is None
llm_model = ( llm_model = (
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model']) await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
if selected_runner == 'local-agent' if selected_runner == 'local-agent'
@@ -51,7 +51,7 @@ class PreProcessor(stage.PipelineStage):
conversation.use_llm_model = llm_model conversation.use_llm_model = llm_model
# 设置query # Set query
query.session = session query.session = session
query.prompt = conversation.prompt.copy() query.prompt = conversation.prompt.copy()
query.messages = conversation.messages.copy() query.messages = conversation.messages.copy()
@@ -109,7 +109,7 @@ class PreProcessor(stage.PipelineStage):
query.variables['user_message_text'] = plain_text query.variables['user_message_text'] = plain_text
query.user_message = llm_entities.Message(role='user', content=content_list) query.user_message = llm_entities.Message(role='user', content=content_list)
# =========== 触发事件 PromptPreProcessing # =========== Trigger event PromptPreProcessing
event_ctx = await self.ap.plugin_mgr.emit_event( event_ctx = await self.ap.plugin_mgr.emit_event(
event=events.PromptPreProcessing( event=events.PromptPreProcessing(

View File

@@ -25,7 +25,7 @@ class MessageHandler(metaclass=abc.ABCMeta):
def cut_str(self, s: str) -> str: def cut_str(self, s: str) -> str:
""" """
取字符串第一行最多20个字符若有多行或超过20个字符则加省略号 Take the first line of the string, up to 20 characters, if there are multiple lines, or more than 20 characters, add an ellipsis
""" """
s0 = s.split('\n')[0] s0 = s.split('\n')[0]
if len(s0) > 20 or '\n' in s: if len(s0) > 20 or '\n' in s:

View File

@@ -22,11 +22,11 @@ class ChatMessageHandler(handler.MessageHandler):
self, self,
query: core_entities.Query, query: core_entities.Query,
) -> typing.AsyncGenerator[entities.StageProcessResult, None]: ) -> typing.AsyncGenerator[entities.StageProcessResult, None]:
"""处理""" """Process"""
# API # Call API
# 生成器 # generator
# 触发插件事件 # Trigger plugin event
event_class = ( event_class = (
events.PersonNormalMessageReceived events.PersonNormalMessageReceived
if query.launcher_type == core_entities.LauncherTypes.PERSON if query.launcher_type == core_entities.LauncherTypes.PERSON
@@ -54,7 +54,7 @@ class ChatMessageHandler(handler.MessageHandler):
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
else: else:
if event_ctx.event.alter is not None: if event_ctx.event.alter is not None:
# if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter # if isinstance(event_ctx.event, str): # Currently not considering multi-modal alter
query.user_message.content = event_ctx.event.alter query.user_message.content = event_ctx.event.alter
text_length = 0 text_length = 0
@@ -65,12 +65,12 @@ class ChatMessageHandler(handler.MessageHandler):
runner = r(self.ap, query.pipeline_config) runner = r(self.ap, query.pipeline_config)
break break
else: else:
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}') raise ValueError(f'Request runner not found: {query.pipeline_config["ai"]["runner"]["runner"]}')
async for result in runner.run(query): async for result in runner.run(query):
query.resp_messages.append(result) query.resp_messages.append(result)
self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}') self.ap.logger.info(f'Response({query.query_id}): {self.cut_str(result.readable_str())}')
if result.content is not None: if result.content is not None:
text_length += len(result.content) text_length += len(result.content)
@@ -80,7 +80,7 @@ class ChatMessageHandler(handler.MessageHandler):
query.session.using_conversation.messages.append(query.user_message) query.session.using_conversation.messages.append(query.user_message)
query.session.using_conversation.messages.extend(query.resp_messages) query.session.using_conversation.messages.extend(query.resp_messages)
except Exception as e: except Exception as e:
self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}') self.ap.logger.error(f'Request failed({query.query_id}): {type(e).__name__} {str(e)}')
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception'] hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']

View File

@@ -15,7 +15,7 @@ class CommandHandler(handler.MessageHandler):
self, self,
query: core_entities.Query, query: core_entities.Query,
) -> typing.AsyncGenerator[entities.StageProcessResult, None]: ) -> typing.AsyncGenerator[entities.StageProcessResult, None]:
"""处理""" """Process"""
command_text = str(query.message_chain).strip()[1:] command_text = str(query.message_chain).strip()[1:]
@@ -70,7 +70,7 @@ class CommandHandler(handler.MessageHandler):
) )
) )
self.ap.logger.info(f'命令({query.query_id})报错: {self.cut_str(str(ret.error))}') self.ap.logger.info(f'Command({query.query_id}) error: {self.cut_str(str(ret.error))}')
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
elif ret.text is not None or ret.image_url is not None: elif ret.text is not None or ret.image_url is not None:
@@ -89,7 +89,7 @@ class CommandHandler(handler.MessageHandler):
) )
) )
self.ap.logger.info(f'命令返回: {self.cut_str(str(content[0]))}') self.ap.logger.info(f'Command returned: {self.cut_str(str(content[0]))}')
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query) yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
else: else:

View File

@@ -33,11 +33,11 @@ class Processor(stage.PipelineStage):
query: core_entities.Query, query: core_entities.Query,
stage_inst_name: str, stage_inst_name: str,
) -> entities.StageProcessResult: ) -> entities.StageProcessResult:
"""处理""" """Process"""
message_text = str(query.message_chain).strip() message_text = str(query.message_chain).strip()
self.ap.logger.info( self.ap.logger.info(
f'处理 {query.launcher_type.value}_{query.launcher_id} 的请求({query.query_id}): {message_text}' f'Processing request from {query.launcher_type.value}_{query.launcher_id} ({query.query_id}): {message_text}'
) )
async def generator(): async def generator():

View File

@@ -16,7 +16,6 @@ from ..logger import EventLogger
class AiocqhttpMessageConverter(adapter.MessageConverter): class AiocqhttpMessageConverter(adapter.MessageConverter):
@staticmethod @staticmethod
async def yiri2target( async def yiri2target(
message_chain: platform_message.MessageChain, message_chain: platform_message.MessageChain,
@@ -62,87 +61,169 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
for node in msg.node_list: for node in msg.node_list:
msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0]) msg_list.extend((await AiocqhttpMessageConverter.yiri2target(node.message_chain))[0])
elif isinstance(msg, platform_message.File): elif isinstance(msg, platform_message.File):
msg_list.append({"type":"file", "data":{'file': msg.url, "name": msg.name}}) msg_list.append({'type': 'file', 'data': {'file': msg.url, 'name': msg.name}})
elif isinstance(msg, platform_message.Face): elif isinstance(msg, platform_message.Face):
if msg.face_type=='face': if msg.face_type == 'face':
msg_list.append(aiocqhttp.MessageSegment.face(msg.face_id)) msg_list.append(aiocqhttp.MessageSegment.face(msg.face_id))
elif msg.face_type=='rps': elif msg.face_type == 'rps':
msg_list.append(aiocqhttp.MessageSegment.rps()) msg_list.append(aiocqhttp.MessageSegment.rps())
elif msg.face_type=='dice': elif msg.face_type == 'dice':
msg_list.append(aiocqhttp.MessageSegment.dice()) msg_list.append(aiocqhttp.MessageSegment.dice())
else: else:
msg_list.append(aiocqhttp.MessageSegment.text(str(msg))) msg_list.append(aiocqhttp.MessageSegment.text(str(msg)))
return msg_list, msg_id, msg_time return msg_list, msg_id, msg_time
@staticmethod @staticmethod
async def target2yiri(message: str, message_id: int = -1,bot=None): async def target2yiri(message: str, message_id: int = -1, bot: aiocqhttp.CQHttp = None):
print(message)
message = aiocqhttp.Message(message) message = aiocqhttp.Message(message)
def get_face_name(face_id): def get_face_name(face_id):
face_code_dict = { face_code_dict = {
"2": '好色', '2': '好色',
"4": "得意", "5": "流泪", "8": "", "9": "大哭", "10": "尴尬", "12": "调皮", "14": "微笑", "16": "", '4': '得意',
"21": "可爱", '5': '流泪',
"23": "傲慢", "24": "饥饿", "25": "", "26": "惊恐", "27": "流汗", "28": "憨笑", "29": "悠闲", '8': '',
"30": "奋斗", '9': '大哭',
"32": "疑问", "33": "", "34": "", "38": "敲打", "39": "再见", "41": "发抖", "42": "爱情", '10': '尴尬',
"43": "跳跳", '12': '调皮',
"49": "拥抱", "53": "蛋糕", "60": "咖啡", "63": "玫瑰", "66": "爱心", "74": "太阳", "75": "月亮", '14': '微笑',
"76": "", '16': '',
"78": "握手", "79": "胜利", "85": "飞吻", "89": "西瓜", "96": "冷汗", "97": "擦汗", "98": "抠鼻", '21': '可爱',
"99": "鼓掌", '23': '傲慢',
"100": "糗大了", "101": "坏笑", "102": "左哼哼", "103": "右哼哼", "104": "哈欠", "106": "委屈", '24': '饥饿',
"109": "左亲亲", '25': '',
"111": "可怜", "116": "示爱", "118": "抱拳", "120": "拳头", "122": "爱你", "123": "NO", "124": "OK", '26': '惊恐',
"125": "转圈", '27': '流汗',
"129": "挥手", "144": "喝彩", "147": "棒棒糖", "171": "", "173": "泪奔", "174": "无奈", "175": "卖萌", '28': '憨笑',
"176": "小纠结", "179": "doge", "180": "惊喜", "181": "骚扰", "182": "笑哭", "183": "我最美", '29': '悠闲',
"201": "点赞", '30': '奋斗',
"203": "托脸", "212": "托腮", "214": "啵啵", "219": "蹭一蹭", "222": "抱抱", "227": "拍手", '32': '疑问',
"232": "佛系", '33': '',
"240": "喷脸", "243": "甩头", "246": "加油抱抱", "262": "脑阔疼", "264": "捂脸", "265": "辣眼睛", '34': '',
"266": "哦哟", '38': '敲打',
"267": "头秃", "268": "问号脸", "269": "暗中观察", "270": "emm", "271": "吃瓜", "272": "呵呵哒", '39': '再见',
"273": "我酸了", '41': '发抖',
"277": "汪汪", "278": "", "281": "无眼笑", "282": "敬礼", "284": "面无表情", "285": "摸鱼", '42': '爱情',
"287": "", '43': '跳跳',
"289": "睁眼", "290": "敲开心", "293": "摸锦鲤", "294": "期待", "297": "拜谢", "298": "元宝", '49': '拥抱',
"299": "牛啊", '53': '蛋糕',
"305": "右亲亲", "306": "牛气冲天", "307": "喵喵", "314": "仔细分析", "315": "加油", "318": "崇拜", '60': '咖啡',
"319": "比心", '63': '玫瑰',
"320": "庆祝", "322": "拒绝", "324": "吃糖", "326": "生气" '66': '爱心',
'74': '太阳',
'75': '月亮',
'76': '',
'78': '握手',
'79': '胜利',
'85': '飞吻',
'89': '西瓜',
'96': '冷汗',
'97': '擦汗',
'98': '抠鼻',
'99': '鼓掌',
'100': '糗大了',
'101': '坏笑',
'102': '左哼哼',
'103': '右哼哼',
'104': '哈欠',
'106': '委屈',
'109': '左亲亲',
'111': '可怜',
'116': '示爱',
'118': '抱拳',
'120': '拳头',
'122': '爱你',
'123': 'NO',
'124': 'OK',
'125': '转圈',
'129': '挥手',
'144': '喝彩',
'147': '棒棒糖',
'171': '',
'173': '泪奔',
'174': '无奈',
'175': '卖萌',
'176': '小纠结',
'179': 'doge',
'180': '惊喜',
'181': '骚扰',
'182': '笑哭',
'183': '我最美',
'201': '点赞',
'203': '托脸',
'212': '托腮',
'214': '啵啵',
'219': '蹭一蹭',
'222': '抱抱',
'227': '拍手',
'232': '佛系',
'240': '喷脸',
'243': '甩头',
'246': '加油抱抱',
'262': '脑阔疼',
'264': '捂脸',
'265': '辣眼睛',
'266': '哦哟',
'267': '头秃',
'268': '问号脸',
'269': '暗中观察',
'270': 'emm',
'271': '吃瓜',
'272': '呵呵哒',
'273': '我酸了',
'277': '汪汪',
'278': '',
'281': '无眼笑',
'282': '敬礼',
'284': '面无表情',
'285': '摸鱼',
'287': '',
'289': '睁眼',
'290': '敲开心',
'293': '摸锦鲤',
'294': '期待',
'297': '拜谢',
'298': '元宝',
'299': '牛啊',
'305': '右亲亲',
'306': '牛气冲天',
'307': '喵喵',
'314': '仔细分析',
'315': '加油',
'318': '崇拜',
'319': '比心',
'320': '庆祝',
'322': '拒绝',
'324': '吃糖',
'326': '生气',
} }
return face_code_dict.get(face_id,'') return face_code_dict.get(face_id, '')
async def process_message_data(msg_data, reply_list): async def process_message_data(msg_data, reply_list):
if msg_data["type"] == "image": if msg_data['type'] == 'image':
image_base64, image_format = await image.qq_image_url_to_base64(msg_data["data"]['url']) image_base64, image_format = await image.qq_image_url_to_base64(msg_data['data']['url'])
reply_list.append( reply_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}'))
platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}'))
elif msg_data["type"] == "text": elif msg_data['type'] == 'text':
reply_list.append(platform_message.Plain(text=msg_data["data"]["text"])) reply_list.append(platform_message.Plain(text=msg_data['data']['text']))
elif msg_data["type"] == "forward": # 这里来应该传入转发消息组暂时传入qoute elif msg_data['type'] == 'forward': # 这里来应该传入转发消息组暂时传入qoute
for forward_msg_datas in msg_data["data"]["content"]: for forward_msg_datas in msg_data['data']['content']:
for forward_msg_data in forward_msg_datas["message"]: for forward_msg_data in forward_msg_datas['message']:
await process_message_data(forward_msg_data, reply_list) await process_message_data(forward_msg_data, reply_list)
elif msg_data["type"] == "at": elif msg_data['type'] == 'at':
if msg_data["data"]['qq'] == 'all': if msg_data['data']['qq'] == 'all':
reply_list.append(platform_message.AtAll()) reply_list.append(platform_message.AtAll())
else: else:
reply_list.append( reply_list.append(
platform_message.At( platform_message.At(
target=msg_data["data"]['qq'], target=msg_data['data']['qq'],
) )
) )
yiri_msg_list = [] yiri_msg_list = []
yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now())) yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now()))
@@ -161,10 +242,10 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
elif msg.type == 'text': elif msg.type == 'text':
yiri_msg_list.append(platform_message.Plain(text=msg.data['text'])) yiri_msg_list.append(platform_message.Plain(text=msg.data['text']))
elif msg.type == 'image': elif msg.type == 'image':
emoji_id = msg.data.get("emoji_package_id", None) emoji_id = msg.data.get('emoji_package_id', None)
if emoji_id: if emoji_id:
face_id = emoji_id face_id = emoji_id
face_name = msg.data.get("summary", '') face_name = msg.data.get('summary', '')
image_msg = platform_message.Face(face_id=face_id, face_name=face_name) image_msg = platform_message.Face(face_id=face_id, face_name=face_name)
else: else:
image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url']) image_base64, image_format = await image.qq_image_url_to_base64(msg.data['url'])
@@ -178,65 +259,53 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
# await process_message_data(msg_data, yiri_msg_list) # await process_message_data(msg_data, yiri_msg_list)
pass pass
elif msg.type == 'reply': # 此处处理引用消息传入Qoute elif msg.type == 'reply': # 此处处理引用消息传入Qoute
msg_datas = await bot.get_msg(message_id=msg.data["id"]) msg_datas = await bot.get_msg(message_id=msg.data['id'])
for msg_data in msg_datas["message"]: for msg_data in msg_datas['message']:
await process_message_data(msg_data, reply_list) await process_message_data(msg_data, reply_list)
reply_msg = platform_message.Quote(message_id=msg.data["id"],sender_id=msg_datas["user_id"],origin=reply_list) reply_msg = platform_message.Quote(
message_id=msg.data['id'], sender_id=msg_datas['user_id'], origin=reply_list
)
yiri_msg_list.append(reply_msg) yiri_msg_list.append(reply_msg)
elif msg.type == 'file': # 这里下载所有文件会导致下载文件过多,暂时不下载
# file_name = msg.data['file'] # elif msg.type == 'file':
file_id = msg.data['file_id'] # # file_name = msg.data['file']
file_data = await bot.get_file(file_id=file_id) # file_id = msg.data['file_id']
file_name = file_data.get('file_name') # file_data = await bot.get_file(file_id=file_id)
file_path = file_data.get('file') # file_name = file_data.get('file_name')
file_url = file_data.get('file_url') # file_path = file_data.get('file')
file_size = file_data.get('file_size') # file_url = file_data.get('file_url')
yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size)) # file_size = file_data.get('file_size')
# yiri_msg_list.append(platform_message.File(id=file_id, name=file_name,url=file_url,size=file_size))
elif msg.type == 'face': elif msg.type == 'face':
face_id = msg.data['id'] face_id = msg.data['id']
face_name = msg.data['raw']['faceText'] face_name = msg.data['raw']['faceText']
if not face_name: if not face_name:
face_name = get_face_name(face_id) face_name = get_face_name(face_id)
yiri_msg_list.append(platform_message.Face(face_id=int(face_id),face_name=face_name.replace('/',''))) yiri_msg_list.append(platform_message.Face(face_id=int(face_id), face_name=face_name.replace('/', '')))
elif msg.type == 'rps': elif msg.type == 'rps':
face_id = msg.data['result'] face_id = msg.data['result']
yiri_msg_list.append(platform_message.Face(face_type="rps",face_id=int(face_id),face_name='猜拳')) yiri_msg_list.append(platform_message.Face(face_type='rps', face_id=int(face_id), face_name='猜拳'))
elif msg.type == 'dice': elif msg.type == 'dice':
face_id = msg.data['result'] face_id = msg.data['result']
yiri_msg_list.append(platform_message.Face(face_type='dice',face_id=int(face_id),face_name='骰子')) yiri_msg_list.append(platform_message.Face(face_type='dice', face_id=int(face_id), face_name='骰子'))
chain = platform_message.MessageChain(yiri_msg_list) chain = platform_message.MessageChain(yiri_msg_list)
return chain return chain
class AiocqhttpEventConverter(adapter.EventConverter): class AiocqhttpEventConverter(adapter.EventConverter):
@staticmethod @staticmethod
async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int): async def yiri2target(event: platform_events.MessageEvent, bot_account_id: int):
return event.source_platform_object return event.source_platform_object
@staticmethod @staticmethod
async def target2yiri(event: aiocqhttp.Event,bot=None): async def target2yiri(event: aiocqhttp.Event, bot=None):
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id,bot) yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot)
if event.message_type == 'group': if event.message_type == 'group':
permission = 'MEMBER' permission = 'MEMBER'
@@ -316,7 +385,6 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0] aiocq_msg = (await AiocqhttpMessageConverter.yiri2target(message))[0]
if target_type == 'group': if target_type == 'group':
await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg) await self.bot.send_group_msg(group_id=int(target_id), message=aiocq_msg)
elif target_type == 'person': elif target_type == 'person':
await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg) await self.bot.send_private_msg(user_id=int(target_id), message=aiocq_msg)
@@ -345,7 +413,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
async def on_message(event: aiocqhttp.Event): async def on_message(event: aiocqhttp.Event):
self.bot_account_id = event.self_id self.bot_account_id = event.self_id
try: try:
return await callback(await self.event_converter.target2yiri(event,self.bot), self) return await callback(await self.event_converter.target2yiri(event, self.bot), self)
except Exception: except Exception:
await self.logger.error(f'Error in on_message: {traceback.format_exc()}') await self.logger.error(f'Error in on_message: {traceback.format_exc()}')
traceback.print_exc() traceback.print_exc()

View File

@@ -38,10 +38,10 @@ import logging
class WeChatPadMessageConverter(adapter.MessageConverter): class WeChatPadMessageConverter(adapter.MessageConverter):
def __init__(self, config: dict): def __init__(self, config: dict, logger: logging.Logger):
self.config = config self.config = config
self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"]) self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"])
self.logger = logging.getLogger("WeChatPadMessageConverter") self.logger = logger
@staticmethod @staticmethod
async def yiri2target( async def yiri2target(
@@ -90,21 +90,30 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
async def target2yiri( async def target2yiri(
self, self,
message: dict, message: dict,
bot_account_id: str bot_account_id: str,
) -> platform_message.MessageChain: ) -> platform_message.MessageChain:
"""外部消息转平台消息""" """外部消息转平台消息"""
# 数据预处理 # 数据预处理
message_list = [] message_list = []
bot_wxid = self.config['wxid']
ats_bot = False # 是否被@ ats_bot = False # 是否被@
content = message["content"]["str"] content = message["content"]["str"]
content_no_preifx = content # 群消息则去掉前缀 content_no_preifx = content # 群消息则去掉前缀
is_group_message = self._is_group_message(message) is_group_message = self._is_group_message(message)
if is_group_message: if is_group_message:
ats_bot = self._ats_bot(message, bot_account_id) ats_bot = self._ats_bot(message, bot_account_id)
self.logger.info(f"ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}")
if "@所有人" in content: if "@所有人" in content:
message_list.append(platform_message.AtAll()) message_list.append(platform_message.AtAll())
elif ats_bot: elif ats_bot:
message_list.append(platform_message.At(target=bot_account_id)) message_list.append(platform_message.At(target=bot_account_id))
# 解析@信息并生成At组件
at_targets = self._extract_at_targets(message)
for target_id in at_targets:
if target_id != bot_wxid: # 避免重复添加机器人的At
message_list.append(platform_message.At(target=target_id))
content_no_preifx, _ = self._extract_content_and_sender(content) content_no_preifx, _ = self._extract_content_and_sender(content)
msg_type = message["msg_type"] msg_type = message["msg_type"]
@@ -458,6 +467,23 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
finally: finally:
return ats_bot return ats_bot
# 提取一下at的wxid列表
def _extract_at_targets(self, message: dict) -> list[str]:
"""从消息中提取被@用户的ID列表"""
at_targets = []
try:
# 从msg_source中解析atuserlist
msg_source = message.get('msg_source', '') or ''
if len(msg_source) > 0:
msg_source_data = ET.fromstring(msg_source)
at_user_list = msg_source_data.findtext("atuserlist") or ""
if at_user_list:
# atuserlist格式通常是逗号分隔的用户ID列表
at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()]
except Exception as e:
self.logger.error(f"_extract_at_targets got except: {e}")
return at_targets
# 提取一下content前面的sender_id, 和去掉前缀的内容 # 提取一下content前面的sender_id, 和去掉前缀的内容
def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]: def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]:
try: try:
@@ -482,10 +508,10 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
class WeChatPadEventConverter(adapter.EventConverter): class WeChatPadEventConverter(adapter.EventConverter):
def __init__(self, config: dict): def __init__(self, config: dict, logger: logging.Logger):
self.config = config self.config = config
self.message_converter = WeChatPadMessageConverter(config) self.message_converter = WeChatPadMessageConverter(config, logger)
self.logger = logging.getLogger("WeChatPadEventConverter") self.logger = logger
@staticmethod @staticmethod
async def yiri2target( async def yiri2target(
@@ -496,7 +522,7 @@ class WeChatPadEventConverter(adapter.EventConverter):
async def target2yiri( async def target2yiri(
self, self,
event: dict, event: dict,
bot_account_id: str bot_account_id: str,
) -> platform_events.MessageEvent: ) -> platform_events.MessageEvent:
# 排除公众号以及微信团队消息 # 排除公众号以及微信团队消息
@@ -572,8 +598,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
self.logger = logger self.logger = logger
self.quart_app = quart.Quart(__name__) self.quart_app = quart.Quart(__name__)
self.message_converter = WeChatPadMessageConverter(config) self.message_converter = WeChatPadMessageConverter(config, ap.logger)
self.event_converter = WeChatPadEventConverter(config) self.event_converter = WeChatPadEventConverter(config, ap.logger)
async def ws_message(self, data): async def ws_message(self, data):
"""处理接收到的消息""" """处理接收到的消息"""

View File

@@ -7,7 +7,7 @@ from . import chatcmpl
class AI302ChatCompletions(chatcmpl.OpenAIChatCompletions): class AI302ChatCompletions(chatcmpl.OpenAIChatCompletions):
"""302 AI ChatCompletion API 请求器""" """302.AI ChatCompletion API 请求器"""
client: openai.AsyncClient client: openai.AsyncClient

View File

@@ -46,7 +46,7 @@ class AnnouncementManager:
async def fetch_all(self) -> list[Announcement]: async def fetch_all(self) -> list[Announcement]:
"""获取所有公告""" """获取所有公告"""
resp = requests.get( resp = requests.get(
url='https://api.github.com/repos/RockChinQ/LangBot/contents/res/announcement.json', url='https://api.github.com/repos/langbot-app/LangBot/contents/res/announcement.json',
proxies=self.ap.proxy_mgr.get_forward_proxies(), proxies=self.ap.proxy_mgr.get_forward_proxies(),
timeout=5, timeout=5,
) )

View File

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

View File

@@ -29,7 +29,7 @@ class VersionManager:
async def get_release_list(self) -> list: async def get_release_list(self) -> list:
"""获取发行列表""" """获取发行列表"""
rls_list_resp = requests.get( rls_list_resp = requests.get(
url='https://api.github.com/repos/RockChinQ/LangBot/releases', url='https://api.github.com/repos/langbot-app/LangBot/releases',
proxies=self.ap.proxy_mgr.get_forward_proxies(), proxies=self.ap.proxy_mgr.get_forward_proxies(),
timeout=5, timeout=5,
) )

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "langbot" name = "langbot"
version = "4.0.8.1" version = "4.0.9"
description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台" description = "高稳定、支持扩展、多模态 - 大模型原生即时通信机器人平台"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10.1" requires-python = ">=3.10.1"
@@ -80,7 +80,7 @@ classifiers = [
[project.urls] [project.urls]
Homepage = "https://langbot.app" Homepage = "https://langbot.app"
Documentation = "https://docs.langbot.app" Documentation = "https://docs.langbot.app"
Repository = "https://github.com/RockChinQ/langbot" Repository = "https://github.com/langbot-app/LangBot"
[dependency-groups] [dependency-groups]
dev = [ dev = [

View File

@@ -5,6 +5,7 @@
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"dev:local": "NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 next dev --turbopack", "dev:local": "NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 next dev --turbopack",
"dev:local:win": "set NEXT_PUBLIC_API_BASE_URL=http://localhost:5300 && next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",