mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 23:06:03 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
239223be3f | ||
|
|
b112cb320c | ||
|
|
5aaf2ba3ef | ||
|
|
f1e9f46af1 | ||
|
|
8dfef1d118 | ||
|
|
919a621bf8 | ||
|
|
3ac96f464d | ||
|
|
f9f03b81d1 | ||
|
|
42171a9c07 | ||
|
|
f1f00115c9 | ||
|
|
59bff61409 | ||
|
|
778693a804 | ||
|
|
e5b2da225c | ||
|
|
4a988b89a2 | ||
|
|
e5e8807312 | ||
|
|
1376530c2e | ||
|
|
7d34a2154b | ||
|
|
ff335130ae | ||
|
|
0afef0ac0f | ||
|
|
6447f270ea | ||
|
|
81be62e1a4 | ||
|
|
409909ccb1 | ||
|
|
b821b69dbb | ||
|
|
7e2448655e | ||
|
|
a7d2a68639 | ||
|
|
aba51409a7 | ||
|
|
5e5d37cbf1 | ||
|
|
e5a99a0fe4 |
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: 漏洞反馈
|
name: 漏洞反馈
|
||||||
description: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 运行环境
|
label: 运行环境
|
||||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
|
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -19,12 +19,12 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 复现步骤
|
label: 复现步骤
|
||||||
description: 如何重现这个问题,越详细越好;请贴上所有相关的配置文件和元数据文件(注意隐去敏感信息)
|
description: 如何重现这个问题,越详细越好;提供越多信息,我们会越快解决问题。
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 启用的插件
|
label: 启用的插件
|
||||||
description: 有些情况可能和插件功能有关,建议提供插件启用情况。可以使用`!plugin`命令查看已启用的插件
|
description: 有些情况可能和插件功能有关,建议提供插件启用情况。
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
30
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://docs.langbot.app/en/workshop/network-details.html
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug?"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Runtime environment
|
||||||
|
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
||||||
|
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Exception
|
||||||
|
description: Describe the exception in detail, what happened and when it happened. **Please include log information.**
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Enabled plugins
|
||||||
|
description: Some cases may be related to plugin functionality, so please provide the plugin enablement status.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: 需求建议
|
name: 需求建议
|
||||||
title: "[Feature]: "
|
title: "[Feature]: "
|
||||||
labels: ["改进"]
|
labels: []
|
||||||
description: "新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
|
description: "【供中文用户】新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
|
||||||
body:
|
body:
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/feature-request_en.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature-request_en.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Feature request
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: []
|
||||||
|
description: "New features or existing feature improvements should use this template; issues that do not match will be closed directly"
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: This is a?
|
||||||
|
description: New feature request or existing feature improvement
|
||||||
|
options:
|
||||||
|
- New feature
|
||||||
|
- Existing feature improvement
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Detailed description
|
||||||
|
description: Detailed description, the more detailed the better
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
2
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
2
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: 提交新插件
|
name: 提交新插件
|
||||||
title: "[Plugin]: 请求登记新插件"
|
title: "[Plugin]: 请求登记新插件"
|
||||||
labels: ["独立插件"]
|
labels: ["独立插件"]
|
||||||
description: "本模板供且仅供提交新插件使用"
|
description: "【供中文用户】本模板供且仅供提交新插件使用"
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
24
.github/ISSUE_TEMPLATE/submit-plugin_en.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/submit-plugin_en.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Submit a new plugin
|
||||||
|
title: "[Plugin]: Request to register a new plugin"
|
||||||
|
labels: ["Independent Plugin"]
|
||||||
|
description: "This template is only for submitting new plugins"
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Plugin name
|
||||||
|
description: Fill in the name of the plugin
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Plugin code repository address
|
||||||
|
description: Only support Github
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Plugin description
|
||||||
|
description: The description of the plugin
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
27
.github/pull_request_template.md
vendored
27
.github/pull_request_template.md
vendored
@@ -1,20 +1,21 @@
|
|||||||
## 概述
|
## 概述 / Overview
|
||||||
|
|
||||||
实现/解决/优化的内容:
|
> 请在此部分填写你实现/解决/优化的内容:
|
||||||
|
> Summary of what you implemented/solved/optimized:
|
||||||
|
|
||||||
## 检查清单
|
## 检查清单 / Checklist
|
||||||
|
|
||||||
### PR 作者完成
|
### PR 作者完成 / For PR author
|
||||||
|
|
||||||
*请在方括号间写`x`以打勾
|
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
||||||
|
|
||||||
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗?
|
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)?
|
||||||
- [ ] 与项目所有者沟通过了吗?
|
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
|
||||||
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。
|
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.
|
||||||
|
|
||||||
### 项目所有者完成
|
### 项目维护者完成 / For project maintainer
|
||||||
|
|
||||||
- [ ] 相关 issues 链接了吗?
|
- [ ] 相关 issues 链接了吗? / Have you linked the related issues?
|
||||||
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗?
|
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗? / Have you written the configuration items? Have you written the migration? Has it taken effect?
|
||||||
- [ ] 依赖加到 pyproject.toml 和 core/bootutils/deps.py 了吗
|
- [ ] 依赖加到 pyproject.toml 和 core/bootutils/deps.py 了吗 / Have you added the dependencies to pyproject.toml and core/bootutils/deps.py?
|
||||||
- [ ] 文档编写了吗?
|
- [ ] 文档编写了吗? / Have you written the documentation?
|
||||||
@@ -5,22 +5,27 @@
|
|||||||
### 贡献形式
|
### 贡献形式
|
||||||
|
|
||||||
- 提交PR,解决issues中提到的bug或期待的功能
|
- 提交PR,解决issues中提到的bug或期待的功能
|
||||||
- 提交PR,实现您设想的功能(请先提出issue与作者沟通)
|
- 提交PR,实现您设想的功能(请先提出issue与项目维护者沟通)
|
||||||
- 优化代码架构,使各个模块的组织更加整洁优雅
|
|
||||||
- 在issues中提出发现的bug或者期待的功能
|
|
||||||
- 为本项目在其他社交平台撰写文章、制作视频等
|
- 为本项目在其他社交平台撰写文章、制作视频等
|
||||||
- 为本项目的衍生项目作出贡献,或开发插件增加功能
|
- 为本项目的衍生项目作出贡献,或开发插件增加功能
|
||||||
|
|
||||||
### 如何开始
|
### 沟通语言规范
|
||||||
|
|
||||||
- 加入本项目交流群,一同探讨项目相关事务
|
- 在 PR 和 Commit Message 中请使用全英文
|
||||||
- 解决本项目或衍生项目的issues中亟待解决的问题
|
- 对于中文用户,issue 中可以使用中文
|
||||||
- 阅读并完善本项目文档
|
|
||||||
- 在各个社交媒体撰写本项目教程等
|
|
||||||
|
|
||||||
### 代码规范
|
<hr/>
|
||||||
|
|
||||||
- 代码中的注解`务必`符合Google风格的规范
|
## Guidelines
|
||||||
- 模块顶部的引入代码请遵循`系统模块`、`第三方库模块`、`自定义模块`的顺序进行引入
|
|
||||||
- `不要`直接引入模块的特定属性,而是引入这个模块,再通过`xxx.yyy`的形式使用属性
|
### Contribution
|
||||||
- 任何作用域的字段`必须`先声明后使用,并在声明处注明类型提示
|
|
||||||
|
- Submit PRs to solve bugs or features in the issues
|
||||||
|
- Submit PRs to implement your ideas (Please create an issue first and communicate with the project maintainer)
|
||||||
|
- Write articles or make videos about this project on other social platforms
|
||||||
|
- Contribute to the development of derivative projects, or develop plugins to add features
|
||||||
|
|
||||||
|
### Spoken Language
|
||||||
|
|
||||||
|
- Use English in PRs and Commit Messages
|
||||||
|
- For English users, you can use English in issues
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DingTalkClient:
|
|||||||
robot_name: str,
|
robot_name: str,
|
||||||
robot_code: str,
|
robot_code: str,
|
||||||
markdown_card: bool,
|
markdown_card: bool,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
"""初始化 WebSocket 连接并自动启动"""
|
"""初始化 WebSocket 连接并自动启动"""
|
||||||
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
||||||
@@ -34,6 +35,7 @@ class DingTalkClient:
|
|||||||
self.robot_code = robot_code
|
self.robot_code = robot_code
|
||||||
self.access_token_expiry_time = ''
|
self.access_token_expiry_time = ''
|
||||||
self.markdown_card = markdown_card
|
self.markdown_card = markdown_card
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def get_access_token(self):
|
async def get_access_token(self):
|
||||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
||||||
@@ -48,7 +50,7 @@ class DingTalkClient:
|
|||||||
expires_in = int(response_data.get('expireIn', 7200))
|
expires_in = int(response_data.get('expireIn', 7200))
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
self.access_token_expiry_time = time.time() + expires_in - 60
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(e)
|
await self.logger.error("failed to get access token in dingtalk")
|
||||||
|
|
||||||
async def is_token_expired(self):
|
async def is_token_expired(self):
|
||||||
"""检查token是否过期"""
|
"""检查token是否过期"""
|
||||||
@@ -73,7 +75,7 @@ class DingTalkClient:
|
|||||||
result = response.json()
|
result = response.json()
|
||||||
download_url = result.get('downloadUrl')
|
download_url = result.get('downloadUrl')
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
await self.logger.error(f"failed to get download url: {response.json()}")
|
||||||
|
|
||||||
if download_url:
|
if download_url:
|
||||||
return await self.download_url_to_base64(download_url)
|
return await self.download_url_to_base64(download_url)
|
||||||
@@ -87,7 +89,7 @@ class DingTalkClient:
|
|||||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
||||||
return base64_str
|
return base64_str
|
||||||
else:
|
else:
|
||||||
raise Exception('获取文件失败')
|
await self.logger.error(f"failed to get files: {response.json()}")
|
||||||
|
|
||||||
async def get_audio_url(self, download_code: str):
|
async def get_audio_url(self, download_code: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
@@ -103,7 +105,7 @@ class DingTalkClient:
|
|||||||
if download_url:
|
if download_url:
|
||||||
return await self.download_url_to_base64(download_url)
|
return await self.download_url_to_base64(download_url)
|
||||||
else:
|
else:
|
||||||
raise Exception('获取音频失败')
|
await self.logger.error(f"failed to get audio: {response.json()}")
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ class DingTalkClient:
|
|||||||
if event:
|
if event:
|
||||||
await self._handle_message(event)
|
await self._handle_message(event)
|
||||||
|
|
||||||
async def send_message(self, content: str, incoming_message,at:bool):
|
async def send_message(self, content: str, incoming_message,at:bool):
|
||||||
if self.markdown_card:
|
if self.markdown_card:
|
||||||
if at:
|
if at:
|
||||||
self.EchoTextHandler.reply_markdown(
|
self.EchoTextHandler.reply_markdown(
|
||||||
@@ -190,8 +192,11 @@ class DingTalkClient:
|
|||||||
copy_message_data = message_data.copy()
|
copy_message_data = message_data.copy()
|
||||||
del copy_message_data['IncomingMessage']
|
del copy_message_data['IncomingMessage']
|
||||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
if self.logger:
|
||||||
|
await self.logger.error(f"Error in get_message: {traceback.format_exc()}")
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
return message_data
|
return message_data
|
||||||
|
|
||||||
@@ -214,9 +219,12 @@ class DingTalkClient:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
||||||
|
raise Exception(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
||||||
|
|
||||||
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
@@ -237,9 +245,12 @@ class DingTalkClient:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
||||||
|
raise Exception(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""启动 WebSocket 连接,监听消息"""
|
"""启动 WebSocket 连接,监听消息"""
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ xml_template = """
|
|||||||
|
|
||||||
|
|
||||||
class OAClient:
|
class OAClient:
|
||||||
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str):
|
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
self.appid = AppID
|
self.appid = AppID
|
||||||
@@ -43,6 +43,7 @@ class OAClient:
|
|||||||
self.access_token_expiry_time = None
|
self.access_token_expiry_time = None
|
||||||
self.msg_id_map = {}
|
self.msg_id_map = {}
|
||||||
self.generated_content = {}
|
self.generated_content = {}
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -54,6 +55,7 @@ class OAClient:
|
|||||||
echostr = request.args.get('echostr', '')
|
echostr = request.args.get('echostr', '')
|
||||||
msg_signature = request.args.get('msg_signature', '')
|
msg_signature = request.args.get('msg_signature', '')
|
||||||
if msg_signature is None:
|
if msg_signature is None:
|
||||||
|
await self.logger.error(f'msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@@ -64,6 +66,7 @@ class OAClient:
|
|||||||
if check_signature == signature:
|
if check_signature == signature:
|
||||||
return echostr # 验证成功返回echostr
|
return echostr # 验证成功返回echostr
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'拒绝请求')
|
||||||
raise Exception('拒绝请求')
|
raise Exception('拒绝请求')
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
encryt_msg = await request.data
|
encryt_msg = await request.data
|
||||||
@@ -72,8 +75,9 @@ class OAClient:
|
|||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
|
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error(f'消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
if message_data:
|
if message_data:
|
||||||
event = OAEvent.from_payload(message_data)
|
event = OAEvent.from_payload(message_data)
|
||||||
@@ -114,6 +118,7 @@ class OAClient:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_message(self, xml_msg: str):
|
async def get_message(self, xml_msg: str):
|
||||||
@@ -176,6 +181,7 @@ class OAClientForLongerResponse:
|
|||||||
AppID: str,
|
AppID: str,
|
||||||
Appsecret: str,
|
Appsecret: str,
|
||||||
LoadingMessage: str,
|
LoadingMessage: str,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
@@ -197,6 +203,7 @@ class OAClientForLongerResponse:
|
|||||||
self.loading_message = LoadingMessage
|
self.loading_message = LoadingMessage
|
||||||
self.msg_queue = {}
|
self.msg_queue = {}
|
||||||
self.user_msg_queue = {}
|
self.user_msg_queue = {}
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -207,6 +214,7 @@ class OAClientForLongerResponse:
|
|||||||
msg_signature = request.args.get('msg_signature', '')
|
msg_signature = request.args.get('msg_signature', '')
|
||||||
|
|
||||||
if msg_signature is None:
|
if msg_signature is None:
|
||||||
|
await self.logger.error(f'msg_signature不在请求体中')
|
||||||
raise Exception('msg_signature不在请求体中')
|
raise Exception('msg_signature不在请求体中')
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
@@ -221,7 +229,9 @@ class OAClientForLongerResponse:
|
|||||||
xml_msg = xml_msg.decode('utf-8')
|
xml_msg = xml_msg.decode('utf-8')
|
||||||
|
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error(f'消息解密失败')
|
||||||
raise Exception('消息解密失败')
|
raise Exception('消息解密失败')
|
||||||
|
|
||||||
|
|
||||||
# 解析 XML
|
# 解析 XML
|
||||||
root = ET.fromstring(xml_msg)
|
root = ET.fromstring(xml_msg)
|
||||||
@@ -270,6 +280,7 @@ class OAClientForLongerResponse:
|
|||||||
return response_xml
|
return response_xml
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def get_message(self, xml_msg: str):
|
async def get_message(self, xml_msg: str):
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def handle_validation(body: dict, bot_secret: str):
|
|||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
class QQOfficialClient:
|
||||||
def __init__(self, secret: str, token: str, app_id: str):
|
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -49,6 +49,7 @@ class QQOfficialClient:
|
|||||||
self.base_url = 'https://api.sgroup.qq.com'
|
self.base_url = 'https://api.sgroup.qq.com'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.access_token_expiry_time = None
|
self.access_token_expiry_time = None
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def check_access_token(self):
|
async def check_access_token(self):
|
||||||
"""检查access_token是否存在"""
|
"""检查access_token是否存在"""
|
||||||
@@ -77,6 +78,7 @@ class QQOfficialClient:
|
|||||||
if access_token:
|
if access_token:
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f'获取access_token失败: {response_data}')
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
raise Exception(f'获取access_token失败: {e}')
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -102,7 +104,7 @@ class QQOfficialClient:
|
|||||||
return {'code': 0, 'message': 'success'}
|
return {'code': 0, 'message': 'success'}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return {'error': str(e)}, 400
|
return {'error': str(e)}, 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -166,6 +168,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -178,9 +181,11 @@ class QQOfficialClient:
|
|||||||
'msg_id': msg_id,
|
'msg_id': msg_id,
|
||||||
}
|
}
|
||||||
response = await client.post(url, headers=headers, json=data)
|
response = await client.post(url, headers=headers, json=data)
|
||||||
|
response_data = response.json()
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
||||||
raise ValueError(response)
|
raise ValueError(response)
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
||||||
@@ -188,6 +193,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -203,6 +209,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"发送群聊消息失败:{response.json()}")
|
||||||
raise Exception(response.read().decode())
|
raise Exception(response.read().decode())
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
||||||
@@ -210,6 +217,7 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
url = self.base_url + '/channels/' + channel_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -225,12 +233,14 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
||||||
"""发送频道私聊消息"""
|
"""发送频道私聊消息"""
|
||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
url = self.base_url + '/dms/' + guild_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -247,6 +257,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
||||||
raise Exception(response)
|
raise Exception(response)
|
||||||
|
|
||||||
async def is_token_expired(self):
|
async def is_token_expired(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
from quart import Quart, jsonify, request
|
from quart import Quart, jsonify, request
|
||||||
from slack_sdk.web.async_client import AsyncWebClient
|
from slack_sdk.web.async_client import AsyncWebClient
|
||||||
from .slackevent import SlackEvent
|
from .slackevent import SlackEvent
|
||||||
@@ -7,7 +8,7 @@ from pkg.platform.types import events as platform_events
|
|||||||
|
|
||||||
|
|
||||||
class SlackClient:
|
class SlackClient:
|
||||||
def __init__(self, bot_token: str, signing_secret: str):
|
def __init__(self, bot_token: str, signing_secret: str, logger: None):
|
||||||
self.bot_token = bot_token
|
self.bot_token = bot_token
|
||||||
self.signing_secret = signing_secret
|
self.signing_secret = signing_secret
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
@@ -19,6 +20,7 @@ class SlackClient:
|
|||||||
'example': [],
|
'example': [],
|
||||||
}
|
}
|
||||||
self.bot_user_id = None # 避免机器人回复自己的消息
|
self.bot_user_id = None # 避免机器人回复自己的消息
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
try:
|
try:
|
||||||
@@ -32,6 +34,7 @@ class SlackClient:
|
|||||||
|
|
||||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
# 处理私信
|
# 处理私信
|
||||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||||
@@ -49,6 +52,7 @@ class SlackClient:
|
|||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
raise (e)
|
raise (e)
|
||||||
|
|
||||||
async def _handle_message(self, event: SlackEvent):
|
async def _handle_message(self, event: SlackEvent):
|
||||||
@@ -78,6 +82,7 @@ class SlackClient:
|
|||||||
self.bot_user_id = response['message']['bot_id']
|
self.bot_user_id = response['message']['bot_id']
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def send_message_to_one(self, text: str, user_id: str):
|
async def send_message_to_one(self, text: str, user_id: str):
|
||||||
@@ -88,6 +93,7 @@ class SlackClient:
|
|||||||
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in send_message: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ from libs.wechatpad_api.api.chatroom import ChatRoomApi
|
|||||||
|
|
||||||
|
|
||||||
class WeChatPadClient:
|
class WeChatPadClient:
|
||||||
def __init__(self,base_url, token):
|
def __init__(self, base_url, token, logger=None):
|
||||||
self._login_api = LoginApi(base_url, token)
|
self._login_api = LoginApi(base_url, token)
|
||||||
self._friend_api = FriendApi(base_url, token)
|
self._friend_api = FriendApi(base_url, token)
|
||||||
self._message_api = MessageApi(base_url, token)
|
self._message_api = MessageApi(base_url, token)
|
||||||
self._user_api = UserApi(base_url, token)
|
self._user_api = UserApi(base_url, token)
|
||||||
self._download_api = DownloadApi(base_url, token)
|
self._download_api = DownloadApi(base_url, token)
|
||||||
self._chatroom_api = ChatRoomApi(base_url, token)
|
self._chatroom_api = ChatRoomApi(base_url, token)
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
def get_token(self,admin_key, day: int):
|
def get_token(self,admin_key, day: int):
|
||||||
'''获取token'''
|
'''获取token'''
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .WXBizMsgCrypt3 import WXBizMsgCrypt
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import httpx
|
import httpx
|
||||||
|
import traceback
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Callable, Dict, Any
|
from typing import Callable, Dict, Any
|
||||||
@@ -19,6 +20,7 @@ class WecomClient:
|
|||||||
token: str,
|
token: str,
|
||||||
EncodingAESKey: str,
|
EncodingAESKey: str,
|
||||||
contacts_secret: str,
|
contacts_secret: str,
|
||||||
|
logger: None,
|
||||||
):
|
):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
@@ -28,6 +30,7 @@ class WecomClient:
|
|||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.secret_for_contacts = contacts_secret
|
self.secret_for_contacts = contacts_secret
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command',
|
'/callback/command',
|
||||||
@@ -54,6 +57,7 @@ class WecomClient:
|
|||||||
if 'access_token' in data:
|
if 'access_token' in data:
|
||||||
return data['access_token']
|
return data['access_token']
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error(f"获取accesstoken失败:{response.json()}")
|
||||||
raise Exception(f'未获取access token: {data}')
|
raise Exception(f'未获取access token: {data}')
|
||||||
|
|
||||||
async def get_users(self):
|
async def get_users(self):
|
||||||
@@ -125,6 +129,7 @@ class WecomClient:
|
|||||||
response = await client.post(url, json=params)
|
response = await client.post(url, json=params)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"发送图片失败:{data}")
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
raise Exception('Failed to send image: ' + str(e))
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||||
@@ -159,6 +164,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_private_msg(user_id, agent_id, content)
|
return await self.send_private_msg(user_id, agent_id, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message: ' + str(data))
|
raise Exception('Failed to send message: ' + str(data))
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -175,6 +181,7 @@ class WecomClient:
|
|||||||
echostr = request.args.get('echostr')
|
echostr = request.args.get('echostr')
|
||||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("验证失败")
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
@@ -182,7 +189,9 @@ class WecomClient:
|
|||||||
encrypt_msg = await request.data
|
encrypt_msg = await request.data
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
|
await self.logger.error("消息解密失败")
|
||||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||||
|
|
||||||
|
|
||||||
# 解析消息并处理
|
# 解析消息并处理
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
@@ -193,6 +202,7 @@ class WecomClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -291,6 +301,7 @@ class WecomClient:
|
|||||||
except binascii.Error as e:
|
except binascii.Error as e:
|
||||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
else:
|
else:
|
||||||
|
await self.logger.error("Image对象出错")
|
||||||
raise ValueError('image对象出错')
|
raise ValueError('image对象出错')
|
||||||
|
|
||||||
# 设置 multipart/form-data 格式的文件
|
# 设置 multipart/form-data 格式的文件
|
||||||
@@ -314,6 +325,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
media_id = await self.upload_to_work(image)
|
media_id = await self.upload_to_work(image)
|
||||||
if data.get('errcode', 0) != 0:
|
if data.get('errcode', 0) != 0:
|
||||||
|
await self.logger.error(f"上传图片失败:{data}")
|
||||||
raise Exception('failed to upload file')
|
raise Exception('failed to upload file')
|
||||||
|
|
||||||
media_id = data.get('media_id')
|
media_id = data.get('media_id')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import aiofiles
|
|||||||
|
|
||||||
|
|
||||||
class WecomCSClient:
|
class WecomCSClient:
|
||||||
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str):
|
def __init__(self, corpid: str, secret: str, token: str, EncodingAESKey: str, logger: None):
|
||||||
self.corpid = corpid
|
self.corpid = corpid
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
self.access_token_for_contacts = ''
|
self.access_token_for_contacts = ''
|
||||||
@@ -21,6 +21,7 @@ class WecomCSClient:
|
|||||||
self.aes = EncodingAESKey
|
self.aes = EncodingAESKey
|
||||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
|
self.logger = logger
|
||||||
self.app = Quart(__name__)
|
self.app = Quart(__name__)
|
||||||
self.app.add_url_rule(
|
self.app.add_url_rule(
|
||||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||||
@@ -186,6 +187,7 @@ class WecomCSClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
|
await self.logger.error(f"发送消息失败:{data}")
|
||||||
raise Exception('Failed to send message')
|
raise Exception('Failed to send message')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -224,7 +226,10 @@ class WecomCSClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
if self.logger:
|
||||||
|
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
|
|||||||
22
pkg/api/http/controller/groups/files.py
Normal file
22
pkg/api/http/controller/groups/files.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import quart
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
from .. import group
|
||||||
|
|
||||||
|
|
||||||
|
@group.group_class('files', '/api/v1/files')
|
||||||
|
class FilesRouterGroup(group.RouterGroup):
|
||||||
|
async def initialize(self) -> None:
|
||||||
|
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||||
|
async def _(image_key: str) -> quart.Response:
|
||||||
|
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
||||||
|
return quart.Response(status=404)
|
||||||
|
|
||||||
|
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
||||||
|
mime_type = mimetypes.guess_type(image_key)[0]
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
|
return quart.Response(image_bytes, mimetype=mime_type)
|
||||||
@@ -29,3 +29,16 @@ class BotsRouterGroup(group.RouterGroup):
|
|||||||
elif quart.request.method == 'DELETE':
|
elif quart.request.method == 'DELETE':
|
||||||
await self.ap.bot_service.delete_bot(bot_uuid)
|
await self.ap.bot_service.delete_bot(bot_uuid)
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
|
@self.route('/<bot_uuid>/logs', methods=['POST'])
|
||||||
|
async def _(bot_uuid: str) -> str:
|
||||||
|
json_data = await quart.request.json
|
||||||
|
from_index = json_data.get('from_index', -1)
|
||||||
|
max_count = json_data.get('max_count', 10)
|
||||||
|
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
||||||
|
return self.success(
|
||||||
|
data={
|
||||||
|
'logs': logs,
|
||||||
|
'total_count': total_count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ class LLMModelsRouterGroup(group.RouterGroup):
|
|||||||
await self.ap.model_service.delete_llm_model(model_uuid)
|
await self.ap.model_service.delete_llm_model(model_uuid)
|
||||||
|
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
|
@self.route('/<model_uuid>/test', methods=['POST'])
|
||||||
|
async def _(model_uuid: str) -> str:
|
||||||
|
json_data = await quart.request.json
|
||||||
|
|
||||||
|
await self.ap.model_service.test_llm_model(model_uuid, json_data)
|
||||||
|
|
||||||
|
return self.success()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
import typing
|
||||||
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
from ....entity.persistence import bot as persistence_bot
|
from ....entity.persistence import bot as persistence_bot
|
||||||
@@ -98,3 +99,14 @@ class BotService:
|
|||||||
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def list_event_logs(
|
||||||
|
self, bot_uuid: str, from_index: int, max_count: int
|
||||||
|
) -> typing.Tuple[list[dict], int, int, int]:
|
||||||
|
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||||
|
if runtime_bot is None:
|
||||||
|
raise Exception('Bot not found')
|
||||||
|
|
||||||
|
logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count)
|
||||||
|
|
||||||
|
return [log.to_json() for log in logs], total_count
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import sqlalchemy
|
|||||||
from ....core import app
|
from ....core import app
|
||||||
from ....entity.persistence import model as persistence_model
|
from ....entity.persistence import model as persistence_model
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
from ....entity.persistence import pipeline as persistence_pipeline
|
||||||
|
from ....provider.modelmgr import requester as model_requester
|
||||||
|
from ....provider import entities as llm_entities
|
||||||
|
|
||||||
|
|
||||||
class ModelsService:
|
class ModelsService:
|
||||||
@@ -78,3 +80,26 @@ class ModelsService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
||||||
|
|
||||||
|
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
||||||
|
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
|
||||||
|
|
||||||
|
if model_uuid != '_':
|
||||||
|
for model in self.ap.model_mgr.llm_models:
|
||||||
|
if model.model_entity.uuid == model_uuid:
|
||||||
|
runtime_llm_model = model
|
||||||
|
break
|
||||||
|
|
||||||
|
if runtime_llm_model is None:
|
||||||
|
raise Exception('model not found')
|
||||||
|
|
||||||
|
else:
|
||||||
|
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
|
||||||
|
|
||||||
|
await runtime_llm_model.requester.invoke_llm(
|
||||||
|
query=None,
|
||||||
|
model=runtime_llm_model,
|
||||||
|
messages=[llm_entities.Message(role='user', content='Hello, world!')],
|
||||||
|
funcs=[],
|
||||||
|
extra_args={},
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from ..api.http.service import model as model_service
|
|||||||
from ..api.http.service import pipeline as pipeline_service
|
from ..api.http.service import pipeline as pipeline_service
|
||||||
from ..api.http.service import bot as bot_service
|
from ..api.http.service import bot as bot_service
|
||||||
from ..discover import engine as discover_engine
|
from ..discover import engine as discover_engine
|
||||||
from ..utils import logcache, ip
|
from ..storage import mgr as storagemgr
|
||||||
|
from ..utils import logcache
|
||||||
from . import taskmgr
|
from . import taskmgr
|
||||||
from . import entities as core_entities
|
from . import entities as core_entities
|
||||||
|
|
||||||
@@ -96,6 +97,8 @@ class Application:
|
|||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
log_cache: logcache.LogCache = None
|
||||||
|
|
||||||
|
storage_mgr: storagemgr.StorageMgr = None
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
# ========= HTTP Services =========
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
user_service: user_service.UserService = None
|
||||||
@@ -166,23 +169,16 @@ class Application:
|
|||||||
|
|
||||||
host_ip = '127.0.0.1'
|
host_ip = '127.0.0.1'
|
||||||
|
|
||||||
public_ip = await ip.get_myip()
|
|
||||||
|
|
||||||
port = self.instance_config.data['api']['port']
|
port = self.instance_config.data['api']['port']
|
||||||
|
|
||||||
tips = f"""
|
tips = f"""
|
||||||
=======================================
|
=======================================
|
||||||
✨ 您可通过以下方式访问管理面板
|
✨ Access WebUI / 访问管理面板
|
||||||
|
|
||||||
🏠 本地地址:http://{host_ip}:{port}/
|
🏠 Local Address: http://{host_ip}:{port}/
|
||||||
🌐 公网地址:http://{public_ip}:{port}/
|
🌐 Public Address: http://<Your Public IP>:{port}/
|
||||||
|
|
||||||
📌 如果您在容器中运行此程序,请确保容器的 {port} 端口已对外暴露
|
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
||||||
🔗 若要使用公网地址访问,请阅读以下须知
|
|
||||||
1. 公网地址仅供参考,请以您的主机公网 IP 为准;
|
|
||||||
2. 要使用公网地址访问,请确保您的主机具有公网 IP,并且系统防火墙已放行 {port} 端口;
|
|
||||||
|
|
||||||
🤯 WebUI 仍处于 Beta 测试阶段,如有问题或建议请反馈到 https://github.com/RockChinQ/LangBot/issues
|
|
||||||
=======================================
|
=======================================
|
||||||
""".strip()
|
""".strip()
|
||||||
for line in tips.split('\n'):
|
for line in tips.split('\n'):
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...api.http.service import model as model_service
|
|||||||
from ...api.http.service import pipeline as pipeline_service
|
from ...api.http.service import pipeline as pipeline_service
|
||||||
from ...api.http.service import bot as bot_service
|
from ...api.http.service import bot as bot_service
|
||||||
from ...discover import engine as discover_engine
|
from ...discover import engine as discover_engine
|
||||||
|
from ...storage import mgr as storagemgr
|
||||||
from ...utils import logcache
|
from ...utils import logcache
|
||||||
from .. import taskmgr
|
from .. import taskmgr
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ class BuildAppStage(stage.BootingStage):
|
|||||||
log_cache = logcache.LogCache()
|
log_cache = logcache.LogCache()
|
||||||
ap.log_cache = log_cache
|
ap.log_cache = log_cache
|
||||||
|
|
||||||
|
storage_mgr_inst = storagemgr.StorageMgr(ap)
|
||||||
|
await storage_mgr_inst.initialize()
|
||||||
|
ap.storage_mgr = storage_mgr_inst
|
||||||
|
|
||||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
||||||
ap.persistence_mgr = persistence_mgr_inst
|
ap.persistence_mgr = persistence_mgr_inst
|
||||||
await persistence_mgr_inst.initialize()
|
await persistence_mgr_inst.initialize()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from .. import migration
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from ...entity.persistence import pipeline as persistence_pipeline
|
||||||
|
|
||||||
|
|
||||||
|
@migration.migration_class(2)
|
||||||
|
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
||||||
|
"""引用消息合并配置"""
|
||||||
|
|
||||||
|
async def upgrade(self):
|
||||||
|
"""升级"""
|
||||||
|
# read all pipelines
|
||||||
|
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
||||||
|
|
||||||
|
for pipeline in pipelines:
|
||||||
|
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
||||||
|
|
||||||
|
config = serialized_pipeline['config']
|
||||||
|
|
||||||
|
if 'misc' not in config['trigger']:
|
||||||
|
config['trigger']['misc'] = {}
|
||||||
|
|
||||||
|
if 'combine-quote-message' not in config['trigger']['misc']:
|
||||||
|
config['trigger']['misc']['combine-quote-message'] = False
|
||||||
|
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||||
|
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
||||||
|
.values({'config': config, 'for_version': self.ap.ver_mgr.get_current_version()})
|
||||||
|
)
|
||||||
|
|
||||||
|
async def downgrade(self):
|
||||||
|
"""降级"""
|
||||||
|
pass
|
||||||
@@ -20,9 +20,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=16)
|
@functools.lru_cache(maxsize=16)
|
||||||
def get_font(self, query: core_entities.Query):
|
def get_font(self, font_path: str):
|
||||||
return ImageFont.truetype(
|
return ImageFont.truetype(
|
||||||
query.pipeline_config['output']['long-text-processing']['font-path'],
|
font_path,
|
||||||
32,
|
32,
|
||||||
encoding='utf-8',
|
encoding='utf-8',
|
||||||
)
|
)
|
||||||
@@ -146,7 +146,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
self.ap.logger.debug('lines: {}, text_width: {}'.format(lines, text_width))
|
self.ap.logger.debug('lines: {}, text_width: {}'.format(lines, text_width))
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# 如果长了就分割
|
# 如果长了就分割
|
||||||
line_width = self.get_font(query).getlength(line)
|
line_width = self.get_font(query.pipeline_config['output']['long-text-processing']['font-path']).getlength(
|
||||||
|
line
|
||||||
|
)
|
||||||
self.ap.logger.debug('line_width: {}'.format(line_width))
|
self.ap.logger.debug('line_width: {}'.format(line_width))
|
||||||
if line_width < text_width:
|
if line_width < text_width:
|
||||||
final_lines.append(line)
|
final_lines.append(line)
|
||||||
@@ -167,7 +169,9 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
|
|
||||||
final_lines.append(rest_text[:point])
|
final_lines.append(rest_text[:point])
|
||||||
rest_text = rest_text[point:]
|
rest_text = rest_text[point:]
|
||||||
line_width = self.text_render_font.getlength(rest_text)
|
line_width = self.get_font(
|
||||||
|
query.pipeline_config['output']['long-text-processing']['font-path']
|
||||||
|
).getlength(rest_text)
|
||||||
if line_width < text_width:
|
if line_width < text_width:
|
||||||
final_lines.append(rest_text)
|
final_lines.append(rest_text)
|
||||||
break
|
break
|
||||||
@@ -187,7 +191,7 @@ class Text2ImageStrategy(strategy_model.LongTextStrategy):
|
|||||||
(offset_x, offset_y + 35 * line_number),
|
(offset_x, offset_y + 35 * line_number),
|
||||||
final_line,
|
final_line,
|
||||||
fill=(0, 0, 0),
|
fill=(0, 0, 0),
|
||||||
font=self.text_render_font,
|
font=self.get_font(query.pipeline_config['output']['long-text-processing']['font-path']),
|
||||||
)
|
)
|
||||||
# 遍历此行,检查是否有emoji
|
# 遍历此行,检查是否有emoji
|
||||||
idx_in_line = 0
|
idx_in_line = 0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
|
|
||||||
if selected_runner == 'local-agent':
|
if selected_runner == 'local-agent':
|
||||||
query.use_funcs = (
|
query.use_funcs = (
|
||||||
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('tool_call') else None
|
conversation.use_funcs if query.use_llm_model.model_entity.abilities.__contains__('func_call') else None
|
||||||
)
|
)
|
||||||
|
|
||||||
query.variables = {
|
query.variables = {
|
||||||
@@ -81,6 +81,7 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
content_list = []
|
content_list = []
|
||||||
|
|
||||||
plain_text = ''
|
plain_text = ''
|
||||||
|
qoute_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
||||||
|
|
||||||
for me in query.message_chain:
|
for me in query.message_chain:
|
||||||
if isinstance(me, platform_message.Plain):
|
if isinstance(me, platform_message.Plain):
|
||||||
@@ -92,6 +93,16 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
):
|
):
|
||||||
if me.base64 is not None:
|
if me.base64 is not None:
|
||||||
content_list.append(llm_entities.ContentElement.from_image_base64(me.base64))
|
content_list.append(llm_entities.ContentElement.from_image_base64(me.base64))
|
||||||
|
elif isinstance(me, platform_message.Quote) and qoute_msg:
|
||||||
|
for msg in me.origin:
|
||||||
|
if isinstance(msg, platform_message.Plain):
|
||||||
|
content_list.append(llm_entities.ContentElement.from_text(msg.text))
|
||||||
|
elif isinstance(msg, platform_message.Image):
|
||||||
|
if selected_runner != 'local-agent' or query.use_llm_model.model_entity.abilities.__contains__(
|
||||||
|
'vision'
|
||||||
|
):
|
||||||
|
if msg.base64 is not None:
|
||||||
|
content_list.append(llm_entities.ContentElement.from_image_base64(msg.base64))
|
||||||
|
|
||||||
query.variables['user_message_text'] = plain_text
|
query.variables['user_message_text'] = plain_text
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from ..core import app, entities as core_entities
|
|||||||
from . import entities
|
from . import entities
|
||||||
|
|
||||||
|
|
||||||
preregistered_stages: dict[str, PipelineStage] = {}
|
preregistered_stages: dict[str, type[PipelineStage]] = {}
|
||||||
|
|
||||||
|
|
||||||
def stage_class(name: str):
|
def stage_class(name: str) -> typing.Callable[[type[PipelineStage]], type[PipelineStage]]:
|
||||||
def decorator(cls):
|
def decorator(cls: type[PipelineStage]) -> type[PipelineStage]:
|
||||||
preregistered_stages[name] = cls
|
preregistered_stages[name] = cls
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import abc
|
|||||||
from ..core import app
|
from ..core import app
|
||||||
from .types import message as platform_message
|
from .types import message as platform_message
|
||||||
from .types import events as platform_events
|
from .types import events as platform_events
|
||||||
|
from .logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
||||||
@@ -22,7 +23,9 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
logger: EventLogger
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器
|
"""初始化适配器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -31,6 +34,7 @@ class MessagePlatformAdapter(metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||||
"""主动发送消息
|
"""主动发送消息
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ import sqlalchemy
|
|||||||
from . import adapter as msadapter
|
from . import adapter as msadapter
|
||||||
|
|
||||||
from ..core import app, entities as core_entities, taskmgr
|
from ..core import app, entities as core_entities, taskmgr
|
||||||
from .types import events as platform_events
|
from .types import events as platform_events, message as platform_message
|
||||||
|
|
||||||
from ..discover import engine
|
from ..discover import engine
|
||||||
|
|
||||||
from ..entity.persistence import bot as persistence_bot
|
from ..entity.persistence import bot as persistence_bot
|
||||||
|
|
||||||
|
from .logger import EventLogger
|
||||||
|
|
||||||
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
|
# 处理 3.4 移除了 YiriMirai 之后,插件的兼容性问题
|
||||||
from . import types as mirai
|
from . import types as mirai
|
||||||
|
|
||||||
@@ -37,23 +39,37 @@ class RuntimeBot:
|
|||||||
|
|
||||||
task_context: taskmgr.TaskContext
|
task_context: taskmgr.TaskContext
|
||||||
|
|
||||||
|
logger: EventLogger
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ap: app.Application,
|
ap: app.Application,
|
||||||
bot_entity: persistence_bot.Bot,
|
bot_entity: persistence_bot.Bot,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
|
logger: EventLogger,
|
||||||
):
|
):
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
self.bot_entity = bot_entity
|
self.bot_entity = bot_entity
|
||||||
self.enable = bot_entity.enable
|
self.enable = bot_entity.enable
|
||||||
self.adapter = adapter
|
self.adapter = adapter
|
||||||
self.task_context = taskmgr.TaskContext()
|
self.task_context = taskmgr.TaskContext()
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
async def on_friend_message(
|
async def on_friend_message(
|
||||||
event: platform_events.FriendMessage,
|
event: platform_events.FriendMessage,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
):
|
):
|
||||||
|
image_components = [
|
||||||
|
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.logger.info(
|
||||||
|
f'{event.message_chain}',
|
||||||
|
images=image_components,
|
||||||
|
message_session_id=f'person_{event.sender.id}',
|
||||||
|
)
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
await self.ap.query_pool.add_query(
|
||||||
bot_uuid=self.bot_entity.uuid,
|
bot_uuid=self.bot_entity.uuid,
|
||||||
launcher_type=core_entities.LauncherTypes.PERSON,
|
launcher_type=core_entities.LauncherTypes.PERSON,
|
||||||
@@ -68,6 +84,16 @@ class RuntimeBot:
|
|||||||
event: platform_events.GroupMessage,
|
event: platform_events.GroupMessage,
|
||||||
adapter: msadapter.MessagePlatformAdapter,
|
adapter: msadapter.MessagePlatformAdapter,
|
||||||
):
|
):
|
||||||
|
image_components = [
|
||||||
|
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
||||||
|
]
|
||||||
|
|
||||||
|
await self.logger.info(
|
||||||
|
f'{event.message_chain}',
|
||||||
|
images=image_components,
|
||||||
|
message_session_id=f'group_{event.group.id}',
|
||||||
|
)
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
await self.ap.query_pool.add_query(
|
||||||
bot_uuid=self.bot_entity.uuid,
|
bot_uuid=self.bot_entity.uuid,
|
||||||
launcher_type=core_entities.LauncherTypes.GROUP,
|
launcher_type=core_entities.LauncherTypes.GROUP,
|
||||||
@@ -92,10 +118,7 @@ class RuntimeBot:
|
|||||||
self.task_context.set_current_action('Exited.')
|
self.task_context.set_current_action('Exited.')
|
||||||
return
|
return
|
||||||
self.task_context.set_current_action('Exited with error.')
|
self.task_context.set_current_action('Exited with error.')
|
||||||
self.task_context.log(f'平台适配器运行出错: {e}')
|
await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback.format_exc()}')
|
||||||
self.task_context.log(f'Traceback: {traceback.format_exc()}')
|
|
||||||
self.ap.logger.error(f'平台适配器运行出错: {e}')
|
|
||||||
self.ap.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
self.task_wrapper = self.ap.task_mgr.create_task(
|
self.task_wrapper = self.ap.task_mgr.create_task(
|
||||||
exception_wrapper(),
|
exception_wrapper(),
|
||||||
@@ -166,9 +189,15 @@ class PlatformManager:
|
|||||||
elif isinstance(bot_entity, dict):
|
elif isinstance(bot_entity, dict):
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity)
|
bot_entity = persistence_bot.Bot(**bot_entity)
|
||||||
|
|
||||||
adapter_inst = self.adapter_dict[bot_entity.adapter](bot_entity.adapter_config, self.ap)
|
logger = EventLogger(name=f'platform-adapter-{bot_entity.name}', ap=self.ap)
|
||||||
|
|
||||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst)
|
adapter_inst = self.adapter_dict[bot_entity.adapter](
|
||||||
|
bot_entity.adapter_config,
|
||||||
|
self.ap,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
||||||
|
|
||||||
await runtime_bot.initialize()
|
await runtime_bot.initialize()
|
||||||
|
|
||||||
|
|||||||
233
pkg/platform/logger.py
Normal file
233
pkg/platform/logger.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing
|
||||||
|
import mimetypes
|
||||||
|
import time
|
||||||
|
import enum
|
||||||
|
import pydantic
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from .types import message as platform_message
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogLevel(enum.Enum):
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
DEBUG = 'debug'
|
||||||
|
INFO = 'info'
|
||||||
|
WARNING = 'warning'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
|
||||||
|
class EventLog(pydantic.BaseModel):
|
||||||
|
seq_id: int
|
||||||
|
"""日志序号"""
|
||||||
|
|
||||||
|
timestamp: int
|
||||||
|
"""日志时间戳"""
|
||||||
|
|
||||||
|
level: EventLogLevel
|
||||||
|
"""日志级别"""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
"""日志文本"""
|
||||||
|
|
||||||
|
images: typing.Optional[list[str]] = None
|
||||||
|
"""日志图片 URL 列表,需要通过 /api/v1/image/{uuid} 获取图片"""
|
||||||
|
|
||||||
|
message_session_id: typing.Optional[str] = None
|
||||||
|
"""消息会话ID,仅收发消息事件有值"""
|
||||||
|
|
||||||
|
def to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
'seq_id': self.seq_id,
|
||||||
|
'timestamp': self.timestamp,
|
||||||
|
'level': self.level.value,
|
||||||
|
'text': self.text,
|
||||||
|
'images': self.images,
|
||||||
|
'message_session_id': self.message_session_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MAX_LOG_COUNT = 200
|
||||||
|
DELETE_COUNT_PER_TIME = 50
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
"""used for logging bot events"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
seq_id_inc: int
|
||||||
|
|
||||||
|
logs: list[EventLog]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
ap: app.Application,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.ap = ap
|
||||||
|
self.logs = []
|
||||||
|
self.seq_id_inc = 0
|
||||||
|
|
||||||
|
async def get_logs(self, from_seq_id: int, max_count: int) -> typing.Tuple[list[EventLog], int]:
|
||||||
|
"""
|
||||||
|
获取日志,从 from_seq_id 开始获取 max_count 条历史日志
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_seq_id: 起始序号,-1 表示末尾
|
||||||
|
max_count: 最大数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[list[EventLog], int]: 日志列表,日志总数
|
||||||
|
"""
|
||||||
|
if len(self.logs) == 0:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
if from_seq_id <= -1:
|
||||||
|
from_seq_id = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
min_seq_id_in_logs = self.logs[0].seq_id
|
||||||
|
max_seq_id_in_logs = self.logs[-1].seq_id
|
||||||
|
|
||||||
|
if from_seq_id < min_seq_id_in_logs: # 需要的整个范围都已经被删除
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
if (
|
||||||
|
from_seq_id > max_seq_id_in_logs and from_seq_id - max_count > max_seq_id_in_logs
|
||||||
|
): # 需要的整个范围都还没生成
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
end_index = 1
|
||||||
|
|
||||||
|
for i, log in enumerate(self.logs):
|
||||||
|
if log.seq_id >= from_seq_id:
|
||||||
|
end_index = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
start_index = max(0, end_index - max_count)
|
||||||
|
|
||||||
|
if max_count > 0:
|
||||||
|
return self.logs[start_index:end_index], len(self.logs)
|
||||||
|
else:
|
||||||
|
return [], len(self.logs)
|
||||||
|
|
||||||
|
async def _truncate_logs(self):
|
||||||
|
if len(self.logs) > MAX_LOG_COUNT:
|
||||||
|
for i in range(DELETE_COUNT_PER_TIME):
|
||||||
|
for image_key in self.logs[i].images:
|
||||||
|
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
||||||
|
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
||||||
|
|
||||||
|
async def _add_log(
|
||||||
|
self,
|
||||||
|
level: EventLogLevel,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
image_keys = []
|
||||||
|
|
||||||
|
if images is None:
|
||||||
|
images = []
|
||||||
|
|
||||||
|
if message_session_id is None:
|
||||||
|
message_session_id = ''
|
||||||
|
|
||||||
|
if not isinstance(message_session_id, str):
|
||||||
|
message_session_id = str(message_session_id)
|
||||||
|
|
||||||
|
for img in images:
|
||||||
|
img_bytes, mime_type = await img.get_bytes()
|
||||||
|
extension = mimetypes.guess_extension(mime_type)
|
||||||
|
if extension is None:
|
||||||
|
extension = '.jpg'
|
||||||
|
image_key = f'{message_session_id}-{uuid.uuid4()}{extension}'
|
||||||
|
await self.ap.storage_mgr.storage_provider.save(image_key, img_bytes)
|
||||||
|
image_keys.append(image_key)
|
||||||
|
|
||||||
|
self.logs.append(
|
||||||
|
EventLog(
|
||||||
|
seq_id=self.seq_id_inc,
|
||||||
|
timestamp=int(time.time()),
|
||||||
|
level=level,
|
||||||
|
text=text,
|
||||||
|
images=image_keys,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.seq_id_inc += 1
|
||||||
|
|
||||||
|
await self._truncate_logs()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not no_throw:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
async def info(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.INFO,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def debug(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.DEBUG,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def warning(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.WARNING,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def error(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
images: typing.Optional[list[platform_message.Image]] = None,
|
||||||
|
message_session_id: typing.Optional[str] = None,
|
||||||
|
no_throw: bool = True,
|
||||||
|
):
|
||||||
|
await self._add_log(
|
||||||
|
level=EventLogLevel.ERROR,
|
||||||
|
text=text,
|
||||||
|
images=images,
|
||||||
|
message_session_id=message_session_id,
|
||||||
|
no_throw=no_throw,
|
||||||
|
)
|
||||||
@@ -12,9 +12,11 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
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,
|
||||||
@@ -66,14 +68,40 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
|||||||
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):
|
async def target2yiri(message: str, message_id: int = -1,bot=None):
|
||||||
message = aiocqhttp.Message(message)
|
message = aiocqhttp.Message(message)
|
||||||
|
|
||||||
|
async def process_message_data(msg_data, reply_list):
|
||||||
|
if msg_data["type"] == "image":
|
||||||
|
image_base64, image_format = await image.qq_image_url_to_base64(msg_data["data"]['url'])
|
||||||
|
reply_list.append(
|
||||||
|
platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}'))
|
||||||
|
|
||||||
|
elif msg_data["type"] == "text":
|
||||||
|
reply_list.append(platform_message.Plain(text=msg_data["data"]["text"]))
|
||||||
|
|
||||||
|
elif msg_data["type"] == "forward": # 这里来应该传入转发消息组,暂时传入qoute
|
||||||
|
for forward_msg_datas in msg_data["data"]["content"]:
|
||||||
|
for forward_msg_data in forward_msg_datas["message"]:
|
||||||
|
await process_message_data(forward_msg_data, reply_list)
|
||||||
|
|
||||||
|
elif msg_data["type"] == "at":
|
||||||
|
if msg_data["data"]['qq'] == 'all':
|
||||||
|
reply_list.append(platform_message.AtAll())
|
||||||
|
else:
|
||||||
|
reply_list.append(
|
||||||
|
platform_message.At(
|
||||||
|
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()))
|
||||||
|
|
||||||
for msg in message:
|
for msg in message:
|
||||||
|
reply_list = []
|
||||||
if msg.type == 'at':
|
if msg.type == 'at':
|
||||||
if msg.data['qq'] == 'all':
|
if msg.data['qq'] == 'all':
|
||||||
yiri_msg_list.append(platform_message.AtAll())
|
yiri_msg_list.append(platform_message.AtAll())
|
||||||
@@ -88,20 +116,46 @@ class AiocqhttpMessageConverter(adapter.MessageConverter):
|
|||||||
elif msg.type == 'image':
|
elif msg.type == 'image':
|
||||||
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'])
|
||||||
yiri_msg_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}'))
|
yiri_msg_list.append(platform_message.Image(base64=f'data:image/{image_format};base64,{image_base64}'))
|
||||||
|
elif msg.type == 'forward':
|
||||||
|
# 暂时不太合理
|
||||||
|
# msg_datas = await bot.get_msg(message_id=message_id)
|
||||||
|
# print(msg_datas)
|
||||||
|
# for msg_data in msg_datas["message"]:
|
||||||
|
# await process_message_data(msg_data, yiri_msg_list)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
elif msg.type == 'reply': # 此处处理引用消息传入Qoute
|
||||||
|
msg_datas = await bot.get_msg(message_id=msg.data["id"])
|
||||||
|
|
||||||
|
for msg_data in msg_datas["message"]:
|
||||||
|
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)
|
||||||
|
yiri_msg_list.append(reply_msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
async def target2yiri(event: aiocqhttp.Event,bot=None):
|
||||||
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id)
|
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'
|
||||||
@@ -156,8 +210,11 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
on_websocket_connection_event_cache: typing.List[typing.Callable[[aiocqhttp.Event], None]] = []
|
||||||
|
|
||||||
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def shutdown_trigger_placeholder():
|
async def shutdown_trigger_placeholder():
|
||||||
while True:
|
while True:
|
||||||
@@ -166,6 +223,7 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
self.config['shutdown_trigger'] = shutdown_trigger_placeholder
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.on_websocket_connection_event_cache = []
|
||||||
|
|
||||||
if 'access-token' in config:
|
if 'access-token' in config:
|
||||||
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
self.bot = aiocqhttp.CQHttp(access_token=config['access-token'])
|
||||||
@@ -205,8 +263,9 @@ 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)
|
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()}')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
if event_type == platform_events.GroupMessage:
|
if event_type == platform_events.GroupMessage:
|
||||||
@@ -214,6 +273,16 @@ class AiocqhttpAdapter(adapter.MessagePlatformAdapter):
|
|||||||
elif event_type == platform_events.FriendMessage:
|
elif event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('private')(on_message)
|
self.bot.on_message('private')(on_message)
|
||||||
|
|
||||||
|
async def on_websocket_connection(event: aiocqhttp.Event):
|
||||||
|
for event in self.on_websocket_connection_event_cache:
|
||||||
|
if event.self_id == event.self_id and event.time == event.time:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_websocket_connection_event_cache.append(event)
|
||||||
|
await self.logger.info(f'WebSocket connection established, bot id: {event.self_id}')
|
||||||
|
|
||||||
|
self.bot.on_websocket_connection(on_websocket_connection)
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
self,
|
self,
|
||||||
event_type: typing.Type[platform_events.Event],
|
event_type: typing.Type[platform_events.Event],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..types import events as platform_events
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from libs.dingtalk_api.api import DingTalkClient
|
from libs.dingtalk_api.api import DingTalkClient
|
||||||
import datetime
|
import datetime
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DingTalkMessageConverter(adapter.MessageConverter):
|
class DingTalkMessageConverter(adapter.MessageConverter):
|
||||||
@@ -99,9 +100,10 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
event_converter: DingTalkEventConverter = DingTalkEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'client_id',
|
'client_id',
|
||||||
'client_secret',
|
'client_secret',
|
||||||
@@ -120,6 +122,7 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
robot_name=config['robot_name'],
|
robot_name=config['robot_name'],
|
||||||
robot_code=config['robot_code'],
|
robot_code=config['robot_code'],
|
||||||
markdown_card=config['markdown_card'],
|
markdown_card=config['markdown_card'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -154,8 +157,8 @@ class DingTalkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
await self.event_converter.target2yiri(event, self.config['robot_name']),
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in dingtalk callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('FriendMessage')(on_message)
|
self.bot.on_message('FriendMessage')(on_message)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class DiscordMessageConverter(adapter.MessageConverter):
|
class DiscordMessageConverter(adapter.MessageConverter):
|
||||||
@@ -170,9 +171,10 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.bot_account_id = self.config['client_id']
|
self.bot_account_id = self.config['client_id']
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ...utils import image
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class GewechatMessageConverter(adapter.MessageConverter):
|
class GewechatMessageConverter(adapter.MessageConverter):
|
||||||
@@ -371,7 +372,7 @@ class GewechatMessageConverter(adapter.MessageConverter):
|
|||||||
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
|
||||||
ats_bot = ats_bot or (quote_id == tousername)
|
ats_bot = ats_bot or (quote_id == tousername)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'_ats_bot got except: {e}')
|
print(f'Error in gewechat _ats_bot: {e}')
|
||||||
finally:
|
finally:
|
||||||
return ats_bot
|
return ats_bot
|
||||||
|
|
||||||
@@ -477,9 +478,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
|
|
||||||
self.message_converter = GewechatMessageConverter(config)
|
self.message_converter = GewechatMessageConverter(config)
|
||||||
@@ -503,7 +505,7 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
await self.logger.error(f'Error in gewechat callback: {traceback.format_exc()}')
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class AESCipher(object):
|
class AESCipher(object):
|
||||||
@@ -338,9 +339,10 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
quart_app: quart.Quart
|
quart_app: quart.Quart
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.quart_app = quart.Quart(__name__)
|
self.quart_app = quart.Quart(__name__)
|
||||||
self.listeners = {}
|
self.listeners = {}
|
||||||
|
|
||||||
@@ -376,15 +378,15 @@ class LarkAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if 'im.message.receive_v1' == type:
|
if 'im.message.receive_v1' == type:
|
||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
return {'code': 200, 'message': 'ok'}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in lark callback: {traceback.format_exc()}")
|
||||||
return {'code': 500, 'message': 'error'}
|
return {'code': 500, 'message': 'error'}
|
||||||
|
|
||||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...pipeline.longtext.strategies import forward
|
|||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||||
@@ -71,9 +72,8 @@ class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
|||||||
content=content_list,
|
content=content_list,
|
||||||
)
|
)
|
||||||
nakuru_forward_node_list.append(nakuru_forward_node)
|
nakuru_forward_node_list.append(nakuru_forward_node)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
nakuru_msg_list.append(nakuru_forward_node_list)
|
nakuru_msg_list.append(nakuru_forward_node_list)
|
||||||
@@ -178,12 +178,13 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
|
|
||||||
cfg: dict
|
cfg: dict
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap):
|
def __init__(self, cfg: dict, ap, logger: EventLogger):
|
||||||
"""初始化nakuru-project的对象"""
|
"""初始化nakuru-project的对象"""
|
||||||
cfg['port'] = cfg['ws_port']
|
cfg['port'] = cfg['ws_port']
|
||||||
del cfg['ws_port']
|
del cfg['ws_port']
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
self.listener_list = []
|
self.listener_list = []
|
||||||
self.bot = nakuru.CQHTTP(**self.cfg)
|
self.bot = nakuru.CQHTTP(**self.cfg)
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ class NakuruAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
# 注册监听器
|
# 注册监听器
|
||||||
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
self.bot.receiver(source_cls.__name__)(listener_wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in nakuru register_listener: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .. import adapter
|
|||||||
from ...core import app
|
from ...core import app
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OAMessageConverter(adapter.MessageConverter):
|
class OAMessageConverter(adapter.MessageConverter):
|
||||||
@@ -63,10 +64,10 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: OAEventConverter = OAEventConverter()
|
event_converter: OAEventConverter = OAEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'token',
|
'token',
|
||||||
@@ -85,6 +86,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
elif self.config['Mode'] == 'passive':
|
elif self.config['Mode'] == 'passive':
|
||||||
self.bot = OAClientForLongerResponse(
|
self.bot = OAClientForLongerResponse(
|
||||||
@@ -93,6 +95,7 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
Appsecret=config['AppSecret'],
|
Appsecret=config['AppSecret'],
|
||||||
AppID=config['AppID'],
|
AppID=config['AppID'],
|
||||||
LoadingMessage=config['LoadingMessage'],
|
LoadingMessage=config['LoadingMessage'],
|
||||||
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise KeyError('请设置微信公众号通信模式')
|
raise KeyError('请设置微信公众号通信模式')
|
||||||
@@ -122,8 +125,8 @@ class OfficialAccountAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in officialaccount callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ spec:
|
|||||||
label:
|
label:
|
||||||
en_US: Host
|
en_US: Host
|
||||||
zh_Hans: 监听主机
|
zh_Hans: 监听主机
|
||||||
|
description:
|
||||||
|
en_US: The host that Official Account listens on for Webhook connections.
|
||||||
|
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: 0.0.0.0
|
default: 0.0.0.0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...config import manager as cfg_mgr
|
|||||||
from ...platform.types import entities as platform_entities
|
from ...platform.types import entities as platform_entities
|
||||||
from ...platform.types import events as platform_events
|
from ...platform.types import events as platform_events
|
||||||
from ...platform.types import message as platform_message
|
from ...platform.types import message as platform_message
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class OfficialGroupMessage(platform_events.GroupMessage):
|
class OfficialGroupMessage(platform_events.GroupMessage):
|
||||||
@@ -357,10 +358,11 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
group_msg_seq = None
|
group_msg_seq = None
|
||||||
c2c_msg_seq = None
|
c2c_msg_seq = None
|
||||||
|
|
||||||
def __init__(self, cfg: dict, ap: app.Application):
|
def __init__(self, cfg: dict, ap: app.Application, logger: EventLogger):
|
||||||
"""初始化适配器"""
|
"""初始化适配器"""
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
self.group_msg_seq = 1
|
self.group_msg_seq = 1
|
||||||
self.c2c_msg_seq = 1
|
self.c2c_msg_seq = 1
|
||||||
@@ -499,7 +501,7 @@ class OfficialAdapter(adapter_model.MessagePlatformAdapter):
|
|||||||
for event_handler in event_handler_mapping[event_type]:
|
for event_handler in event_handler_mapping[event_type]:
|
||||||
setattr(self.bot, event_handler, wrapper)
|
setattr(self.bot, event_handler, wrapper)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
self.logger.error(f"Error in qqbotpy callback: {traceback.format_exc()}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def unregister_listener(
|
def unregister_listener(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...command.errors import ParamNotEnoughError
|
|||||||
from libs.qq_official_api.api import QQOfficialClient
|
from libs.qq_official_api.api import QQOfficialClient
|
||||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(adapter.MessageConverter):
|
class QQOfficialMessageConverter(adapter.MessageConverter):
|
||||||
@@ -139,9 +140,10 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'appid',
|
'appid',
|
||||||
@@ -155,6 +157,7 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
app_id=config['appid'],
|
app_id=config['appid'],
|
||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -221,8 +224,8 @@ class QQOfficialAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'justbot'
|
self.bot_account_id = 'justbot'
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in qqofficial callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .. import adapter
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class SlackMessageConverter(adapter.MessageConverter):
|
class SlackMessageConverter(adapter.MessageConverter):
|
||||||
@@ -91,9 +92,10 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: SlackEventConverter = SlackEventConverter()
|
event_converter: SlackEventConverter = SlackEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'bot_token',
|
'bot_token',
|
||||||
'signing_secret',
|
'signing_secret',
|
||||||
@@ -102,7 +104,7 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
if missing_keys:
|
if missing_keys:
|
||||||
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
raise ParamNotEnoughError('Slack机器人缺少相关配置项,请查看文档或联系管理员')
|
||||||
|
|
||||||
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'])
|
self.bot = SlackClient(bot_token=self.config['bot_token'], signing_secret=self.config['signing_secret'], logger=self.logger)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
self,
|
self,
|
||||||
@@ -137,8 +139,8 @@ class SlackAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = 'SlackBot'
|
self.bot_account_id = 'SlackBot'
|
||||||
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:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in slack callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('im')(on_message)
|
self.bot.on_message('im')(on_message)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...core import app
|
|||||||
from ..types import message as platform_message
|
from ..types import message as platform_message
|
||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class TelegramMessageConverter(adapter.MessageConverter):
|
class TelegramMessageConverter(adapter.MessageConverter):
|
||||||
@@ -147,9 +148,10 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def telegram_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if update.message.from_user.is_bot:
|
if update.message.from_user.is_bot:
|
||||||
@@ -158,8 +160,8 @@ class TelegramAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
lb_event = await self.event_converter.target2yiri(update, self.bot, self.bot_account_id)
|
||||||
await self.listeners[type(lb_event)](lb_event, self)
|
await self.listeners[type(lb_event)](lb_event, self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
print(traceback.format_exc())
|
await self.logger.error(f"Error in telegram callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
self.application = ApplicationBuilder().token(self.config['token']).build()
|
self.application = ApplicationBuilder().token(self.config['token']).build()
|
||||||
self.bot = self.application.bot
|
self.bot = self.application.bot
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -533,9 +534,10 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
typing.Callable[[platform_events.Event, adapter.MessagePlatformAdapter], None],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
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)
|
||||||
@@ -550,7 +552,7 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
try:
|
try:
|
||||||
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
event = await self.event_converter.target2yiri(data.copy(), self.bot_account_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wechatpad callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
if event.__class__ in self.listeners:
|
||||||
await self.listeners[event.__class__](event, self)
|
await self.listeners[event.__class__](event, self)
|
||||||
@@ -694,7 +696,8 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
self.bot = WeChatPadClient(
|
self.bot = WeChatPadClient(
|
||||||
self.config['wechatpad_url'],
|
self.config['wechatpad_url'],
|
||||||
self.config["token"]
|
self.config["token"],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
self.ap.logger.info(self.config["token"])
|
self.ap.logger.info(self.config["token"])
|
||||||
thread_1 = threading.Event()
|
thread_1 = threading.Event()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from ...core import app
|
|||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
from ...utils import image
|
from ...utils import image
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -134,10 +135,10 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -156,6 +157,7 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
contacts_secret=config['contacts_secret'],
|
contacts_secret=config['contacts_secret'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -199,8 +201,8 @@ class WecomAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecom callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pkg.core import app
|
|||||||
from .. import adapter
|
from .. import adapter
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
from ...command.errors import ParamNotEnoughError
|
from ...command.errors import ParamNotEnoughError
|
||||||
|
from ..logger import EventLogger
|
||||||
|
|
||||||
|
|
||||||
class WecomMessageConverter(adapter.MessageConverter):
|
class WecomMessageConverter(adapter.MessageConverter):
|
||||||
@@ -124,10 +125,10 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
event_converter: WecomEventConverter = WecomEventConverter()
|
event_converter: WecomEventConverter = WecomEventConverter()
|
||||||
config: dict
|
config: dict
|
||||||
|
|
||||||
def __init__(self, config: dict, ap: app.Application):
|
def __init__(self, config: dict, ap: app.Application, logger: EventLogger):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
'corpid',
|
'corpid',
|
||||||
@@ -144,6 +145,7 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
secret=config['secret'],
|
secret=config['secret'],
|
||||||
token=config['token'],
|
token=config['token'],
|
||||||
EncodingAESKey=config['EncodingAESKey'],
|
EncodingAESKey=config['EncodingAESKey'],
|
||||||
|
logger=self.logger
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -176,8 +178,8 @@ class WecomCSAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot_account_id = event.receiver_id
|
self.bot_account_id = event.receiver_id
|
||||||
try:
|
try:
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
return await callback(await self.event_converter.target2yiri(event), self)
|
||||||
except:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
await self.logger.error(f"Error in wecomcs callback: {traceback.format_exc()}")
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
if event_type == platform_events.FriendMessage:
|
||||||
self.bot.on_message('text')(on_message)
|
self.bot.on_message('text')(on_message)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import httpx
|
||||||
import pydantic.v1 as pydantic
|
import pydantic.v1 as pydantic
|
||||||
|
|
||||||
from . import entities as platform_entities
|
from . import entities as platform_entities
|
||||||
@@ -552,52 +555,29 @@ class Image(MessageComponent):
|
|||||||
image_id = image_id[1:]
|
image_id = image_id[1:]
|
||||||
return image_id
|
return image_id
|
||||||
|
|
||||||
async def download(
|
async def get_bytes(self) -> typing.Tuple[bytes, str]:
|
||||||
self,
|
"""获取图片的 bytes 和 mime type"""
|
||||||
filename: typing.Union[str, Path, None] = None,
|
if self.url:
|
||||||
directory: typing.Union[str, Path, None] = None,
|
async with httpx.AsyncClient() as client:
|
||||||
determine_type: bool = True,
|
response = await client.get(self.url)
|
||||||
):
|
response.raise_for_status()
|
||||||
"""下载图片到本地。
|
return response.content, response.headers.get('Content-Type')
|
||||||
|
elif self.base64:
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
|
||||||
Args:
|
split_index = self.base64.find(';base64,')
|
||||||
filename: 下载到本地的文件路径。与 `directory` 二选一。
|
if split_index == -1:
|
||||||
directory: 下载到本地的文件夹路径。与 `filename` 二选一。
|
raise ValueError('Invalid base64 string')
|
||||||
determine_type: 是否自动根据图片类型确定拓展名,默认为 True。
|
|
||||||
"""
|
|
||||||
if not self.url:
|
|
||||||
logger.warning(f'图片 `{self.uuid}` 无 url 参数,下载失败。')
|
|
||||||
return
|
|
||||||
|
|
||||||
import httpx
|
mime_type = self.base64[5:split_index]
|
||||||
|
base64_data = self.base64[split_index + 8 :]
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
return base64.b64decode(base64_data), mime_type
|
||||||
response = await client.get(self.url)
|
elif self.path:
|
||||||
response.raise_for_status()
|
async with aiofiles.open(self.path, 'rb') as f:
|
||||||
content = response.content
|
return await f.read(), 'image/jpeg'
|
||||||
|
else:
|
||||||
if filename:
|
raise ValueError('Can not get bytes from image')
|
||||||
path = Path(filename)
|
|
||||||
if determine_type:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = path.with_suffix('.' + str(imghdr.what(None, content)))
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
elif directory:
|
|
||||||
import imghdr
|
|
||||||
|
|
||||||
path = Path(directory)
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
path = path / f'{self.uuid}.{imghdr.what(None, content)}'
|
|
||||||
else:
|
|
||||||
raise ValueError('请指定文件路径或文件夹路径!')
|
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
|
|
||||||
async with aiofiles.open(path, 'wb') as f:
|
|
||||||
await f.write(content)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_local(
|
async def from_local(
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import sqlalchemy
|
|||||||
|
|
||||||
from . import entities, requester
|
from . import entities, requester
|
||||||
from ...core import app
|
from ...core import app
|
||||||
from ...core import entities as core_entities
|
|
||||||
from .. import entities as llm_entities
|
|
||||||
from ..tools import entities as tools_entities
|
|
||||||
from ...discover import engine
|
from ...discover import engine
|
||||||
from . import token
|
from . import token
|
||||||
from ...entity.persistence import model as persistence_model
|
from ...entity.persistence import model as persistence_model
|
||||||
@@ -69,12 +66,11 @@ class ModelManager:
|
|||||||
for llm_model in llm_models:
|
for llm_model in llm_models:
|
||||||
await self.load_llm_model(llm_model)
|
await self.load_llm_model(llm_model)
|
||||||
|
|
||||||
async def load_llm_model(
|
async def init_runtime_llm_model(
|
||||||
self,
|
self,
|
||||||
model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict,
|
model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict,
|
||||||
):
|
):
|
||||||
"""加载模型"""
|
"""初始化运行时模型"""
|
||||||
|
|
||||||
if isinstance(model_info, sqlalchemy.Row):
|
if isinstance(model_info, sqlalchemy.Row):
|
||||||
model_info = persistence_model.LLMModel(**model_info._mapping)
|
model_info = persistence_model.LLMModel(**model_info._mapping)
|
||||||
elif isinstance(model_info, dict):
|
elif isinstance(model_info, dict):
|
||||||
@@ -92,6 +88,15 @@ class ModelManager:
|
|||||||
),
|
),
|
||||||
requester=requester_inst,
|
requester=requester_inst,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return runtime_llm_model
|
||||||
|
|
||||||
|
async def load_llm_model(
|
||||||
|
self,
|
||||||
|
model_info: persistence_model.LLMModel | sqlalchemy.Row[persistence_model.LLMModel] | dict,
|
||||||
|
):
|
||||||
|
"""加载模型"""
|
||||||
|
runtime_llm_model = await self.init_runtime_llm_model(model_info)
|
||||||
self.llm_models.append(runtime_llm_model)
|
self.llm_models.append(runtime_llm_model)
|
||||||
|
|
||||||
async def get_model_by_name(self, name: str) -> entities.LLMModelInfo: # deprecated
|
async def get_model_by_name(self, name: str) -> entities.LLMModelInfo: # deprecated
|
||||||
@@ -132,12 +137,3 @@ class ModelManager:
|
|||||||
if component.metadata.name == name:
|
if component.metadata.name == name:
|
||||||
return component
|
return component
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def invoke_llm(
|
|
||||||
self,
|
|
||||||
query: core_entities.Query,
|
|
||||||
model_uuid: str,
|
|
||||||
messages: list[llm_entities.Message],
|
|
||||||
funcs: list[tools_entities.LLMFunction] = None,
|
|
||||||
) -> llm_entities.Message:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
|
|||||||
|
|
||||||
if chunk.choices[0].delta.tool_calls is not None:
|
if chunk.choices[0].delta.tool_calls is not None:
|
||||||
for tool_call in chunk.choices[0].delta.tool_calls:
|
for tool_call in chunk.choices[0].delta.tool_calls:
|
||||||
|
if tool_call.function.arguments is None:
|
||||||
|
continue
|
||||||
for tc in tool_calls:
|
for tc in tool_calls:
|
||||||
if tc.index == tool_call.index:
|
if tc.index == tool_call.index:
|
||||||
tc.function.arguments += tool_call.function.arguments
|
tc.function.arguments += tool_call.function.arguments
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
async def _chat_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
async def _chat_messages(self, query: core_entities.Query) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||||
"""调用聊天助手"""
|
"""调用聊天助手"""
|
||||||
cov_id = query.session.using_conversation.uuid or ''
|
cov_id = query.session.using_conversation.uuid or ''
|
||||||
|
query.variables['conversation_id'] = cov_id
|
||||||
|
|
||||||
plain_text, image_ids = await self._preprocess_user_message(query)
|
plain_text, image_ids = await self._preprocess_user_message(query)
|
||||||
|
|
||||||
@@ -155,6 +156,7 @@ class DifyServiceAPIRunner(runner.RequestRunner):
|
|||||||
) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
) -> typing.AsyncGenerator[llm_entities.Message, None]:
|
||||||
"""调用聊天助手"""
|
"""调用聊天助手"""
|
||||||
cov_id = query.session.using_conversation.uuid or ''
|
cov_id = query.session.using_conversation.uuid or ''
|
||||||
|
query.variables['conversation_id'] = cov_id
|
||||||
|
|
||||||
plain_text, image_ids = await self._preprocess_user_message(query)
|
plain_text, image_ids = await self._preprocess_user_message(query)
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ class RuntimeMCPSession:
|
|||||||
|
|
||||||
for tool in tools.tools:
|
for tool in tools.tools:
|
||||||
|
|
||||||
async def func(query: core_entities.Query, **kwargs):
|
async def func(query: core_entities.Query, *, _tool=tool, **kwargs):
|
||||||
result = await self.session.call_tool(tool.name, kwargs)
|
result = await self.session.call_tool(_tool.name, kwargs)
|
||||||
if result.isError:
|
if result.isError:
|
||||||
raise Exception(result.content[0].text)
|
raise Exception(result.content[0].text)
|
||||||
return result.content[0].text
|
return result.content[0].text
|
||||||
|
|||||||
0
pkg/storage/__init__.py
Normal file
0
pkg/storage/__init__.py
Normal file
21
pkg/storage/mgr.py
Normal file
21
pkg/storage/mgr.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
from . import provider
|
||||||
|
from .providers import localstorage
|
||||||
|
|
||||||
|
|
||||||
|
class StorageMgr:
|
||||||
|
"""存储管理器"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
storage_provider: provider.StorageProvider
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
self.storage_provider = localstorage.LocalStorageProvider(ap)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
await self.storage_provider.initialize()
|
||||||
44
pkg/storage/provider.py
Normal file
44
pkg/storage/provider.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
|
||||||
|
|
||||||
|
class StorageProvider(abc.ABC):
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
pass
|
||||||
0
pkg/storage/providers/__init__.py
Normal file
0
pkg/storage/providers/__init__.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
45
pkg/storage/providers/localstorage.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from ...core import app
|
||||||
|
|
||||||
|
from .. import provider
|
||||||
|
|
||||||
|
|
||||||
|
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStorageProvider(provider.StorageProvider):
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
super().__init__(ap)
|
||||||
|
if not os.path.exists(LOCAL_STORAGE_PATH):
|
||||||
|
os.makedirs(LOCAL_STORAGE_PATH)
|
||||||
|
|
||||||
|
async def save(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
value: bytes,
|
||||||
|
):
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'wb') as f:
|
||||||
|
await f.write(value)
|
||||||
|
|
||||||
|
async def load(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bytes:
|
||||||
|
async with aiofiles.open(os.path.join(LOCAL_STORAGE_PATH, f'{key}'), 'rb') as f:
|
||||||
|
return await f.read()
|
||||||
|
|
||||||
|
async def exists(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
return os.path.exists(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
key: str,
|
||||||
|
):
|
||||||
|
os.remove(os.path.join(LOCAL_STORAGE_PATH, f'{key}'))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
semantic_version = 'v4.0.3.1'
|
semantic_version = 'v4.0.4'
|
||||||
|
|
||||||
required_database_version = 1
|
required_database_version = 2
|
||||||
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""
|
||||||
|
|
||||||
debug_mode = False
|
debug_mode = False
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
def get_func_schema(function: callable) -> dict:
|
def get_func_schema(function: typing.Callable) -> dict:
|
||||||
"""
|
"""
|
||||||
Return the data schema of a function.
|
Return the data schema of a function.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import aiohttp
|
|
||||||
|
|
||||||
|
|
||||||
async def get_myip() -> str:
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
|
||||||
async with session.get('https://ip.useragentinfo.com/myip') as response:
|
|
||||||
return await response.text()
|
|
||||||
except Exception:
|
|
||||||
return '0.0.0.0'
|
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
"ignore-rules": {
|
"ignore-rules": {
|
||||||
"prefix": [],
|
"prefix": [],
|
||||||
"regexp": []
|
"regexp": []
|
||||||
|
},
|
||||||
|
"misc": {
|
||||||
|
"combine-quote-message": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safety": {
|
"safety": {
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ stages:
|
|||||||
en_US: Random
|
en_US: Random
|
||||||
zh_Hans: 随机
|
zh_Hans: 随机
|
||||||
description:
|
description:
|
||||||
en_US: The probability of the random response, range from 0.0 to 1.0
|
en_US: 'Probability of automatically responding to messages that are not matched by other rules. Range: 0.0-1.0 (0%-100%).'
|
||||||
zh_Hans: 随机响应概率,范围为 0.0-1.0,对应 0% 到 100%
|
zh_Hans: '自动响应其他规则未匹配的消息的概率。范围:0.0-1.0 (0%-100%)。'
|
||||||
type: float
|
type: float
|
||||||
required: false
|
required: false
|
||||||
default: 0
|
default: 0
|
||||||
@@ -117,3 +117,18 @@ stages:
|
|||||||
type: array[string]
|
type: array[string]
|
||||||
required: true
|
required: true
|
||||||
default: []
|
default: []
|
||||||
|
- name: misc
|
||||||
|
label:
|
||||||
|
en_US: Misc
|
||||||
|
zh_Hans: 杂项
|
||||||
|
config:
|
||||||
|
- name: combine-quote-message
|
||||||
|
label:
|
||||||
|
en_US: Combine Quote Message
|
||||||
|
zh_Hans: 合并引用消息
|
||||||
|
description:
|
||||||
|
en_US: If enabled, the bot will combine the quote message with the user's message
|
||||||
|
zh_Hans: 如果启用,将合并引用消息与用户发送的消息
|
||||||
|
type: boolean
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
|||||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -36,6 +36,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.5",
|
"tailwindcss": "^4.1.5",
|
||||||
@@ -6126,6 +6127,15 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-photo-view": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-photo-view/-/react-photo-view-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-MfOWVPxuibncRLaycZUNxqYU8D9IA+rbGDDaq6GM8RIoGJal592hEJoRAyRSI7ZxyyJNJTLMUWWL3UIXHJJOpw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.5",
|
"tailwindcss": "^4.1.5",
|
||||||
|
|||||||
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
63
web/src/app/home/bots/bot-log/BotLogManager.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import {
|
||||||
|
BotLog,
|
||||||
|
GetBotLogsResponse,
|
||||||
|
} from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
|
||||||
|
export class BotLogManager {
|
||||||
|
private botId: string;
|
||||||
|
private callbacks: ((_: BotLog[]) => void)[] = [];
|
||||||
|
private intervalIds: number[] = [];
|
||||||
|
|
||||||
|
constructor(botId: string) {
|
||||||
|
this.botId = botId;
|
||||||
|
}
|
||||||
|
|
||||||
|
startListenServerPush() {
|
||||||
|
const timerNumber = setInterval(() => {
|
||||||
|
this.getLogList(-1, 50).then((response) => {
|
||||||
|
this.callbacks.forEach((callback) =>
|
||||||
|
callback(this.parseResponse(response)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
this.intervalIds.push(Number(timerNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopServerPush() {
|
||||||
|
this.intervalIds.forEach((id) => clearInterval(id));
|
||||||
|
this.intervalIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeLogPush(callback: (_: BotLog[]) => void) {
|
||||||
|
if (!this.callbacks.includes(callback)) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.callbacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志页的基本信息
|
||||||
|
*/
|
||||||
|
private getLogList(next: number, count: number = 20) {
|
||||||
|
return httpClient.getBotLogs(this.botId, {
|
||||||
|
from_index: next,
|
||||||
|
max_count: count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFirstPage() {
|
||||||
|
return this.parseResponse(await this.getLogList(-1, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMore(position: number, total: number) {
|
||||||
|
return this.parseResponse(await this.getLogList(position, total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseResponse(httpResponse: GetBotLogsResponse): BotLog[] {
|
||||||
|
return httpResponse.logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
116
web/src/app/home/bots/bot-log/view/BotLogCard.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { PhotoProvider } from 'react-photo-view';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const baseURL = httpClient.getBaseUrl();
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
|
||||||
|
// 获取各个时间部分
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1; // 月份从0开始,需要+1
|
||||||
|
const day = date.getDate();
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
// 判断时间范围
|
||||||
|
const isToday = now.toDateString() === date.toDateString();
|
||||||
|
const isYesterday =
|
||||||
|
new Date(now.setDate(now.getDate() - 1)).toDateString() ===
|
||||||
|
date.toDateString();
|
||||||
|
const isThisYear = now.getFullYear() === year;
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
return `${hours}:${minutes}`; // 今天的消息:小时:分钟
|
||||||
|
} else if (isYesterday) {
|
||||||
|
return `${t('bots.yesterday')} ${hours}:${minutes}`; // 昨天的消息:昨天 小时:分钟
|
||||||
|
} else if (isThisYear) {
|
||||||
|
return t('bots.dateFormat', { month, day }); // 本年消息:x月x日
|
||||||
|
} else {
|
||||||
|
return t('bots.earlier'); // 更早的消息:更久之前
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubChatId(str: string) {
|
||||||
|
const strArr = str.split('');
|
||||||
|
return strArr;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`${styles.botLogCardContainer}`}>
|
||||||
|
{/* 头部标签,时间 */}
|
||||||
|
<div className={`${styles.cardTitleContainer}`}>
|
||||||
|
<div className={`flex flex-row gap-4`}>
|
||||||
|
<div className={`${styles.tag}`}>{botLog.level}</div>
|
||||||
|
{botLog.message_session_id && (
|
||||||
|
<div
|
||||||
|
className={`${styles.tag} ${styles.chatTag}`}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(botLog.message_session_id)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t('common.copySuccess'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="icon"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="1664"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z"
|
||||||
|
p-id="1665"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z"
|
||||||
|
p-id="1666"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z"
|
||||||
|
p-id="1667"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
{/* 会话ID */}
|
||||||
|
|
||||||
|
<span className={`${styles.chatId}`}>
|
||||||
|
{getSubChatId(botLog.message_session_id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{formatTime(botLog.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.cardTitleContainer} ${styles.cardText}`}>
|
||||||
|
{botLog.text}
|
||||||
|
</div>
|
||||||
|
<PhotoProvider className={``}>
|
||||||
|
<div className={`w-50 mt-2`}>
|
||||||
|
{botLog.images.map((item) => (
|
||||||
|
<img
|
||||||
|
key={item}
|
||||||
|
src={`${baseURL}/api/v1/files/image/${item}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PhotoProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
129
web/src/app/home/bots/bot-log/view/BotLogListComponent.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { BotLogManager } from '@/app/home/bots/bot-log/BotLogManager';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BotLog } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
import { BotLogCard } from '@/app/home/bots/bot-log/view/BotLogCard';
|
||||||
|
import styles from './botLog.module.css';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function BotLogListComponent({ botId }: { botId: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const manager = useRef(new BotLogManager(botId)).current;
|
||||||
|
const [botLogList, setBotLogList] = useState<BotLog[]>([]);
|
||||||
|
const [autoFlush, setAutoFlush] = useState(true);
|
||||||
|
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initComponent();
|
||||||
|
return () => {
|
||||||
|
onDestroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
botLogListRef.current = botLogList;
|
||||||
|
}, [botLogList]);
|
||||||
|
|
||||||
|
// 观测自动刷新状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFlush) {
|
||||||
|
manager.startListenServerPush();
|
||||||
|
} else {
|
||||||
|
manager.stopServerPush();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
manager.stopServerPush();
|
||||||
|
};
|
||||||
|
}, [autoFlush]);
|
||||||
|
|
||||||
|
function initComponent() {
|
||||||
|
// 订阅日志推送
|
||||||
|
manager.subscribeLogPush(handleBotLogPush);
|
||||||
|
// 加载第一页日志
|
||||||
|
manager.loadFirstPage().then((response) => {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
});
|
||||||
|
// 监听滚动
|
||||||
|
listenScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDestroy() {
|
||||||
|
manager.dispose();
|
||||||
|
removeScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenScroll() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.addEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScrollListener() {
|
||||||
|
if (!listContainerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = listContainerRef.current;
|
||||||
|
list.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
// 加载更多日志
|
||||||
|
const list = botLogListRef.current;
|
||||||
|
const lastSeq = list[list.length - 1].seq_id;
|
||||||
|
if (lastSeq === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manager.loadMore(lastSeq - 1, 10).then((response) => {
|
||||||
|
setBotLogList([...list, ...response.reverse()]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotLogPush(response: BotLog[]) {
|
||||||
|
setBotLogList(response.reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
if (!listContainerRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
listContainerRef.current;
|
||||||
|
const isBottom = scrollTop + clientHeight >= scrollHeight - 5;
|
||||||
|
const isTop = scrollTop === 0;
|
||||||
|
|
||||||
|
if (isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
if (isTop) {
|
||||||
|
setAutoFlush(true);
|
||||||
|
}
|
||||||
|
if (!isTop && !isBottom) {
|
||||||
|
setAutoFlush(false);
|
||||||
|
}
|
||||||
|
}, 300), // 防抖延迟 300ms
|
||||||
|
[botLogList], // 依赖项为空
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.botLogListContainer} px-6`}
|
||||||
|
ref={listContainerRef}
|
||||||
|
>
|
||||||
|
<div className={`${styles.listHeader}`}>
|
||||||
|
<div className={'mr-2'}>{t('bots.enableAutoRefresh')}</div>
|
||||||
|
<Switch checked={autoFlush} onCheckedChange={(e) => setAutoFlush(e)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{botLogList.map((botLog) => {
|
||||||
|
return <BotLogCard botLog={botLog} key={botLog.seq_id} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
68
web/src/app/home/bots/bot-log/view/botLog.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.botLogListContainer {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 10rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botLogCardContainer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background-color: #a5d8ff;
|
||||||
|
color: #ffffff;
|
||||||
|
max-width: 16rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatTag {
|
||||||
|
color: #626262;
|
||||||
|
background-color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatId {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitleContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardText {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
@@ -1,7 +1,34 @@
|
|||||||
import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO';
|
import { BotCardVO } from '@/app/home/bots/components/bot-card/BotCardVO';
|
||||||
import styles from './botCard.module.css';
|
import styles from './botCard.module.css';
|
||||||
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export default function BotCard({
|
||||||
|
botCardVO,
|
||||||
|
clickLogIconCallback,
|
||||||
|
setBotEnableCallback,
|
||||||
|
}: {
|
||||||
|
botCardVO: BotCardVO;
|
||||||
|
clickLogIconCallback: (id: string) => void;
|
||||||
|
setBotEnableCallback: (id: string, enable: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
function onClickLogIcon() {
|
||||||
|
clickLogIconCallback(botCardVO.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBotEnable(enable: boolean) {
|
||||||
|
return httpClient.updateBot(botCardVO.id, {
|
||||||
|
name: botCardVO.name,
|
||||||
|
description: botCardVO.description,
|
||||||
|
adapter: botCardVO.adapter,
|
||||||
|
adapter_config: botCardVO.adapterConfig,
|
||||||
|
enable: enable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function BotCard({ botCardVO }: { botCardVO: BotCardVO }) {
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.cardContainer}`}>
|
<div className={`${styles.cardContainer}`}>
|
||||||
<div className={`${styles.iconBasicInfoContainer}`}>
|
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||||
@@ -47,6 +74,44 @@ export default function BotCard({ botCardVO }: { botCardVO: BotCardVO }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={`${styles.botOperationContainer}`}>
|
||||||
|
<Switch
|
||||||
|
checked={botCardVO.enable}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
setBotEnable(e)
|
||||||
|
.then(() => {
|
||||||
|
setBotEnableCallback(botCardVO.id, e);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast.error(t('bots.setBotEnableError'));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`${styles.botLogsIcon}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClickLogIcon();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-[24px] h-[24px] z-10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9ZM8 7H11V9H8V7ZM8 11H16V13H8V11ZM8 15H16V17H8V15Z"
|
||||||
|
fill="#9A9A9A"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ export interface IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BotCardVO implements IBotCardVO {
|
export class BotCardVO implements IBotCardVO {
|
||||||
@@ -12,15 +15,21 @@ export class BotCardVO implements IBotCardVO {
|
|||||||
iconURL: string;
|
iconURL: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
adapter: string;
|
||||||
adapterLabel: string;
|
adapterLabel: string;
|
||||||
|
adapterConfig: object;
|
||||||
usePipelineName: string;
|
usePipelineName: string;
|
||||||
|
enable: boolean;
|
||||||
|
|
||||||
constructor(props: IBotCardVO) {
|
constructor(props: IBotCardVO) {
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.iconURL = props.iconURL;
|
this.iconURL = props.iconURL;
|
||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.description = props.description;
|
this.description = props.description;
|
||||||
|
this.adapter = props.adapter;
|
||||||
|
this.adapterConfig = props.adapterConfig;
|
||||||
this.adapterLabel = props.adapterLabel;
|
this.adapterLabel = props.adapterLabel;
|
||||||
this.usePipelineName = props.usePipelineName;
|
this.usePipelineName = props.usePipelineName;
|
||||||
|
this.enable = props.enable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* background-color: aqua; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconImage {
|
.iconImage {
|
||||||
@@ -30,9 +29,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoContainer {
|
.basicInfoContainer {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoNameContainer {
|
.basicInfoNameContainer {
|
||||||
@@ -43,12 +44,18 @@
|
|||||||
.basicInfoName {
|
.basicInfoName {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoDescription {
|
.basicInfoDescription {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #b1b1b1;
|
color: #b1b1b1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoAdapterContainer {
|
.basicInfoAdapterContainer {
|
||||||
@@ -88,3 +95,22 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bigText {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botOperationContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
width: 3rem;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
@@ -202,6 +202,7 @@ export default function BotForm({
|
|||||||
default: item.default,
|
default: item.default,
|
||||||
id: UUID.generate(),
|
id: UUID.generate(),
|
||||||
label: item.label,
|
label: item.label,
|
||||||
|
description: item.description,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
required: item.required,
|
required: item.required,
|
||||||
type: parseDynamicFormItemType(item.type),
|
type: parseDynamicFormItemType(item.type),
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { i18nObj } from '@/i18n/I18nProvider';
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
|
import { BotLogListComponent } from '@/app/home/bots/bot-log/view/BotLogListComponent';
|
||||||
|
|
||||||
export default function BotConfigPage() {
|
export default function BotConfigPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// 编辑机器人的modal
|
||||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
// 机器人日志的modal
|
||||||
|
const [logModalOpen, setLogModalOpen] = useState<boolean>(false);
|
||||||
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
const [botList, setBotList] = useState<BotCardVO[]>([]);
|
||||||
const [isEditForm, setIsEditForm] = useState(false);
|
const [isEditForm, setIsEditForm] = useState(false);
|
||||||
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
const [nowSelectedBotUUID, setNowSelectedBotUUID] = useState<string>();
|
||||||
|
const [nowSelectedBotLog, setNowSelectedBotLog] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getBotList();
|
getBotList();
|
||||||
@@ -47,10 +52,13 @@ export default function BotConfigPage() {
|
|||||||
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
iconURL: httpClient.getAdapterIconURL(bot.adapter),
|
||||||
name: bot.name,
|
name: bot.name,
|
||||||
description: bot.description,
|
description: bot.description,
|
||||||
|
adapter: bot.adapter,
|
||||||
|
adapterConfig: bot.adapter_config,
|
||||||
adapterLabel:
|
adapterLabel:
|
||||||
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
adapterList.find((item) => item.value === bot.adapter)?.label ||
|
||||||
bot.adapter.substring(0, 10),
|
bot.adapter.substring(0, 10),
|
||||||
usePipelineName: bot.use_pipeline_name || '',
|
usePipelineName: bot.use_pipeline_name || '',
|
||||||
|
enable: bot.enable || false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setBotList(botList);
|
setBotList(botList);
|
||||||
@@ -76,6 +84,11 @@ export default function BotConfigPage() {
|
|||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickLogIcon(botId: string) {
|
||||||
|
setNowSelectedBotLog(botId);
|
||||||
|
setLogModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.configPageContainer}>
|
<div className={styles.configPageContainer}>
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||||
@@ -107,6 +120,15 @@ export default function BotConfigPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={logModalOpen} onOpenChange={setLogModalOpen}>
|
||||||
|
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
|
<DialogTitle>{t('bots.botLogTitle')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<BotLogListComponent botId={nowSelectedBotLog || ''} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
{/* 注意:其余的返回内容需要保持在Spin组件外部 */}
|
||||||
<div className={`${styles.botListContainer}`}>
|
<div className={`${styles.botListContainer}`}>
|
||||||
<CreateCardComponent
|
<CreateCardComponent
|
||||||
@@ -123,7 +145,22 @@ export default function BotConfigPage() {
|
|||||||
selectBot(cardVO.id);
|
selectBot(cardVO.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BotCard botCardVO={cardVO} />
|
<BotCard
|
||||||
|
botCardVO={cardVO}
|
||||||
|
clickLogIconCallback={(id) => {
|
||||||
|
onClickLogIcon(id);
|
||||||
|
}}
|
||||||
|
setBotEnableCallback={(id, enable) => {
|
||||||
|
setBotList(
|
||||||
|
botList.map((bot) => {
|
||||||
|
if (bot.id === id) {
|
||||||
|
return { ...bot, enable: enable };
|
||||||
|
}
|
||||||
|
return bot;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import styles from './HomeSidebar.module.css';
|
import styles from './HomeSidebar.module.css';
|
||||||
import { I18nText } from '@/app/infra/entities/api';
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
export interface ISidebarChildVO {
|
export interface ISidebarChildVO {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -7,7 +7,7 @@ export interface ISidebarChildVO {
|
|||||||
name: string;
|
name: string;
|
||||||
route: string;
|
route: string;
|
||||||
description: string;
|
description: string;
|
||||||
helpLink: I18nText;
|
helpLink: I18nLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SidebarChildVO {
|
export class SidebarChildVO {
|
||||||
@@ -16,7 +16,7 @@ export class SidebarChildVO {
|
|||||||
name: string;
|
name: string;
|
||||||
route: string;
|
route: string;
|
||||||
description: string;
|
description: string;
|
||||||
helpLink: I18nText;
|
helpLink: I18nLabel;
|
||||||
|
|
||||||
constructor(props: ISidebarChildVO) {
|
constructor(props: ISidebarChildVO) {
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { i18nObj } from '@/i18n/I18nProvider';
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
import styles from './HomeTittleBar.module.css';
|
import styles from './HomeTittleBar.module.css';
|
||||||
import { I18nText } from '@/app/infra/entities/api';
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
export default function HomeTitleBar({
|
export default function HomeTitleBar({
|
||||||
title,
|
title,
|
||||||
@@ -9,7 +9,7 @@ export default function HomeTitleBar({
|
|||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
helpLink: I18nText;
|
helpLink: I18nLabel;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.titleBarContainer}`}>
|
<div className={`${styles.titleBarContainer}`}>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar';
|
|||||||
import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar';
|
import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||||
import { I18nText } from '@/app/infra/entities/api';
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
export default function HomeLayout({
|
export default function HomeLayout({
|
||||||
children,
|
children,
|
||||||
@@ -14,7 +14,7 @@ export default function HomeLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const [title, setTitle] = useState<string>('');
|
const [title, setTitle] = useState<string>('');
|
||||||
const [subtitle, setSubtitle] = useState<string>('');
|
const [subtitle, setSubtitle] = useState<string>('');
|
||||||
const [helpLink, setHelpLink] = useState<I18nText>({
|
const [helpLink, setHelpLink] = useState<I18nLabel>({
|
||||||
en_US: '',
|
en_US: '',
|
||||||
zh_Hans: '',
|
zh_Hans: '',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,3 +119,12 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #2288ee;
|
color: #2288ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bigText {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default function LLMForm({
|
|||||||
name: '',
|
name: '',
|
||||||
model_provider: '',
|
model_provider: '',
|
||||||
url: '',
|
url: '',
|
||||||
api_key: '',
|
api_key: 'sk-xxxxx',
|
||||||
abilities: [],
|
abilities: [],
|
||||||
extra_args: [],
|
extra_args: [],
|
||||||
},
|
},
|
||||||
@@ -130,6 +130,8 @@ export default function LLMForm({
|
|||||||
const [requesterDefaultURLList, setRequesterDefaultURLList] = useState<
|
const [requesterDefaultURLList, setRequesterDefaultURLList] = useState<
|
||||||
string[]
|
string[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [modelTesting, setModelTesting] = useState(false);
|
||||||
|
const [currentModelProvider, setCurrentModelProvider] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initLLMModelFormComponent().then(() => {
|
initLLMModelFormComponent().then(() => {
|
||||||
@@ -137,6 +139,7 @@ export default function LLMForm({
|
|||||||
getLLMConfig(initLLMId).then((val) => {
|
getLLMConfig(initLLMId).then((val) => {
|
||||||
form.setValue('name', val.name);
|
form.setValue('name', val.name);
|
||||||
form.setValue('model_provider', val.model_provider);
|
form.setValue('model_provider', val.model_provider);
|
||||||
|
setCurrentModelProvider(val.model_provider);
|
||||||
form.setValue('url', val.url);
|
form.setValue('url', val.url);
|
||||||
form.setValue('api_key', val.api_key);
|
form.setValue('api_key', val.api_key);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
@@ -166,7 +169,6 @@ export default function LLMForm({
|
|||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -308,6 +310,34 @@ export default function LLMForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testLLMModelInForm() {
|
||||||
|
setModelTesting(true);
|
||||||
|
httpClient
|
||||||
|
.testLLMModel('_', {
|
||||||
|
uuid: '',
|
||||||
|
name: form.getValues('name'),
|
||||||
|
description: '',
|
||||||
|
requester: form.getValues('model_provider'),
|
||||||
|
requester_config: {
|
||||||
|
base_url: form.getValues('url'),
|
||||||
|
timeout: 120,
|
||||||
|
},
|
||||||
|
api_keys: [form.getValues('api_key')],
|
||||||
|
abilities: form.getValues('abilities'),
|
||||||
|
extra_args: form.getValues('extra_args'),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
toast.success(t('models.testSuccess'));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(t('models.testError'));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setModelTesting(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -380,6 +410,7 @@ export default function LLMForm({
|
|||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
field.onChange(value);
|
field.onChange(value);
|
||||||
|
setCurrentModelProvider(value);
|
||||||
const index = requesterNameList.findIndex(
|
const index = requesterNameList.findIndex(
|
||||||
(item) => item.value === value,
|
(item) => item.value === value,
|
||||||
);
|
);
|
||||||
@@ -426,22 +457,28 @@ export default function LLMForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
{!['lmstudio-chat-completions', 'ollama-chat'].includes(
|
||||||
name="api_key"
|
currentModelProvider,
|
||||||
render={({ field }) => (
|
) && (
|
||||||
<FormItem>
|
<FormField
|
||||||
<FormLabel>
|
control={form.control}
|
||||||
{t('models.apiKey')}
|
name="api_key"
|
||||||
<span className="text-red-500">*</span>
|
render={({ field }) => (
|
||||||
</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>
|
||||||
<Input {...field} />
|
{t('models.apiKey')}
|
||||||
</FormControl>
|
<span className="text-red-500">*</span>
|
||||||
<FormMessage />
|
</FormLabel>
|
||||||
</FormItem>
|
<FormControl>
|
||||||
)}
|
<Input {...field} />
|
||||||
/>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="abilities"
|
name="abilities"
|
||||||
@@ -579,6 +616,15 @@ export default function LLMForm({
|
|||||||
{editMode ? t('common.save') : t('common.submit')}
|
{editMode ? t('common.save') : t('common.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => testLLMModelInForm()}
|
||||||
|
disabled={modelTesting}
|
||||||
|
>
|
||||||
|
{t('common.test')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoNameContainer {
|
.basicInfoNameContainer {
|
||||||
@@ -88,3 +89,12 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #ffcd27;
|
color: #ffcd27;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bigText {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||||
import { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
import { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||||
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -7,11 +8,6 @@ export interface ApiResponse<T> {
|
|||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface I18nText {
|
|
||||||
en_US: string;
|
|
||||||
zh_Hans: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsyncTaskCreatedResp {
|
export interface AsyncTaskCreatedResp {
|
||||||
task_id: number;
|
task_id: number;
|
||||||
}
|
}
|
||||||
@@ -26,8 +22,8 @@ export interface ApiRespProviderRequester {
|
|||||||
|
|
||||||
export interface Requester {
|
export interface Requester {
|
||||||
name: string;
|
name: string;
|
||||||
label: I18nText;
|
label: I18nLabel;
|
||||||
description: I18nText;
|
description: I18nLabel;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
spec: {
|
spec: {
|
||||||
config: IDynamicFormItemSchema[];
|
config: IDynamicFormItemSchema[];
|
||||||
@@ -84,22 +80,14 @@ export interface ApiRespPlatformAdapter {
|
|||||||
|
|
||||||
export interface Adapter {
|
export interface Adapter {
|
||||||
name: string;
|
name: string;
|
||||||
label: I18nText;
|
label: I18nLabel;
|
||||||
description: I18nText;
|
description: I18nLabel;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
spec: {
|
spec: {
|
||||||
config: AdapterSpecConfig[];
|
config: IDynamicFormItemSchema[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdapterSpecConfig {
|
|
||||||
default: string | number | boolean | Array<unknown>;
|
|
||||||
label: I18nText;
|
|
||||||
name: string;
|
|
||||||
required: boolean;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiRespPlatformBots {
|
export interface ApiRespPlatformBots {
|
||||||
bots: Bot[];
|
bots: Bot[];
|
||||||
}
|
}
|
||||||
@@ -133,8 +121,8 @@ export interface ApiRespPlugin {
|
|||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
author: string;
|
author: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: I18nText;
|
description: I18nLabel;
|
||||||
label: I18nText;
|
label: I18nLabel;
|
||||||
version: string;
|
version: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
GetPipelineMetadataResponseData,
|
GetPipelineMetadataResponseData,
|
||||||
AsyncTask,
|
AsyncTask,
|
||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
|
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||||
|
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||||
|
|
||||||
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
|
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
|
||||||
interface JSONObject {
|
interface JSONObject {
|
||||||
@@ -54,12 +56,14 @@ export let systemInfo: ApiRespSystemInfo | null = null;
|
|||||||
class HttpClient {
|
class HttpClient {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
private disableToken: boolean = false;
|
private disableToken: boolean = false;
|
||||||
|
private baseURL: string;
|
||||||
// 暂不需要SSR
|
// 暂不需要SSR
|
||||||
// private ssrInstance: AxiosInstance | null = null
|
// private ssrInstance: AxiosInstance | null = null
|
||||||
|
|
||||||
constructor(baseURL?: string, disableToken?: boolean) {
|
constructor(baseURL: string, disableToken?: boolean) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
this.instance = axios.create({
|
this.instance = axios.create({
|
||||||
baseURL: baseURL || this.getBaseUrl(),
|
baseURL: baseURL,
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -75,15 +79,9 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底URL,如果使用未配置会走到这里
|
// 外部获取baseURL的方法
|
||||||
private getBaseUrl(): string {
|
getBaseUrl(): string {
|
||||||
// NOT IMPLEMENT
|
return this.baseURL;
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
// 服务端环境
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
// 客户端环境
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取Session
|
// 获取Session
|
||||||
@@ -271,6 +269,10 @@ class HttpClient {
|
|||||||
return this.put(`/api/v1/provider/models/llm/${uuid}`, model);
|
return this.put(`/api/v1/provider/models/llm/${uuid}`, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public testLLMModel(uuid: string, model: LLMModel): Promise<object> {
|
||||||
|
return this.post(`/api/v1/provider/models/llm/${uuid}/test`, model);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Pipeline API ============
|
// ============ Pipeline API ============
|
||||||
public getGeneralPipelineMetadata(): Promise<GetPipelineMetadataResponseData> {
|
public getGeneralPipelineMetadata(): Promise<GetPipelineMetadataResponseData> {
|
||||||
// as designed, this method will be deprecated, and only for developer to check the prefered config schema
|
// as designed, this method will be deprecated, and only for developer to check the prefered config schema
|
||||||
@@ -341,6 +343,13 @@ class HttpClient {
|
|||||||
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getBotLogs(
|
||||||
|
botId: string,
|
||||||
|
request: GetBotLogsRequest,
|
||||||
|
): Promise<GetBotLogsResponse> {
|
||||||
|
return this.post(`/api/v1/platform/bots/${botId}/logs`, request);
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Plugins API ============
|
// ============ Plugins API ============
|
||||||
public getPlugins(): Promise<ApiRespPlugins> {
|
public getPlugins(): Promise<ApiRespPlugins> {
|
||||||
return this.get('/api/v1/plugins');
|
return this.get('/api/v1/plugins');
|
||||||
@@ -446,7 +455,7 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const httpClient = new HttpClient("https://version-4.langbot.dev");
|
// export const httpClient = new HttpClient('https://event-log.langbot.dev');
|
||||||
// export const httpClient = new HttpClient('http://localhost:5300');
|
// export const httpClient = new HttpClient('http://localhost:5300');
|
||||||
export const httpClient = new HttpClient('/');
|
export const httpClient = new HttpClient('/');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface GetBotLogsRequest {
|
||||||
|
from_index: number; // 从某索引开始往前找,-1代表结尾,也就是拉取最新的
|
||||||
|
max_count: number; // 最大拉取数量
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface GetBotLogsResponse {
|
||||||
|
logs: BotLog[];
|
||||||
|
total_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BotLog {
|
||||||
|
images: [];
|
||||||
|
level: string;
|
||||||
|
message_session_id: string;
|
||||||
|
seq_id: number;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import './global.css';
|
import './global.css';
|
||||||
|
import 'react-photo-view/dist/react-photo-view.css';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import I18nProvider from '@/i18n/I18nProvider';
|
import I18nProvider from '@/i18n/I18nProvider';
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ function DialogContent({
|
|||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onInteractOutside={() => {}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import '@/i18n';
|
import '@/i18n';
|
||||||
import { I18nText } from '@/app/infra/entities/api';
|
import { I18nLabel } from '@/app/infra/entities/common';
|
||||||
|
|
||||||
interface I18nProviderProps {
|
interface I18nProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -11,10 +11,10 @@ interface I18nProviderProps {
|
|||||||
export default function I18nProvider({ children }: I18nProviderProps) {
|
export default function I18nProvider({ children }: I18nProviderProps) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
export function i18nObj(i18nText: I18nText): string {
|
export function i18nObj(i18nLabel: I18nLabel): string {
|
||||||
const language = localStorage.getItem('langbot_language');
|
const language = localStorage.getItem('langbot_language');
|
||||||
if ((language === 'zh-Hans' && i18nText.zh_Hans) || !i18nText.en_US) {
|
if ((language === 'zh-Hans' && i18nLabel.zh_Hans) || !i18nLabel.en_US) {
|
||||||
return i18nText.zh_Hans;
|
return i18nLabel.zh_Hans;
|
||||||
}
|
}
|
||||||
return i18nText.en_US;
|
return i18nLabel.en_US;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const enUS = {
|
|||||||
deleteSuccess: 'Deleted successfully',
|
deleteSuccess: 'Deleted successfully',
|
||||||
deleteError: 'Delete failed: ',
|
deleteError: 'Delete failed: ',
|
||||||
addRound: 'Add Round',
|
addRound: 'Add Round',
|
||||||
|
copySuccess: 'Copy Successfully',
|
||||||
|
test: 'Test',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
title: 'Page not found',
|
title: 'Page not found',
|
||||||
@@ -89,6 +91,8 @@ const enUS = {
|
|||||||
modelProviderDescription:
|
modelProviderDescription:
|
||||||
'Please fill in the model name provided by the supplier',
|
'Please fill in the model name provided by the supplier',
|
||||||
selectModel: 'Select Model',
|
selectModel: 'Select Model',
|
||||||
|
testSuccess: 'Test successful',
|
||||||
|
testError: 'Test failed, please check your model configuration',
|
||||||
},
|
},
|
||||||
bots: {
|
bots: {
|
||||||
title: 'Bots',
|
title: 'Bots',
|
||||||
@@ -117,6 +121,13 @@ const enUS = {
|
|||||||
adapterConfig: 'Adapter Configuration',
|
adapterConfig: 'Adapter Configuration',
|
||||||
bindPipeline: 'Bind Pipeline',
|
bindPipeline: 'Bind Pipeline',
|
||||||
selectPipeline: 'Select Pipeline',
|
selectPipeline: 'Select Pipeline',
|
||||||
|
botLogTitle: 'Bot Log',
|
||||||
|
enableAutoRefresh: 'Enable Auto Refresh',
|
||||||
|
session: 'Session',
|
||||||
|
yesterday: 'Yesterday',
|
||||||
|
earlier: 'Earlier',
|
||||||
|
dateFormat: '{{month}}/{{day}}',
|
||||||
|
setBotEnableError: 'Failed to set bot enable status',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: 'Plugins',
|
title: 'Plugins',
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const zhHans = {
|
|||||||
deleteSuccess: '删除成功',
|
deleteSuccess: '删除成功',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
addRound: '添加回合',
|
addRound: '添加回合',
|
||||||
|
copySuccess: '复制成功',
|
||||||
|
test: '测试',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
title: '页面不存在',
|
title: '页面不存在',
|
||||||
@@ -89,6 +91,8 @@ const zhHans = {
|
|||||||
selectModelProvider: '选择模型供应商',
|
selectModelProvider: '选择模型供应商',
|
||||||
modelProviderDescription: '请填写供应商向您提供的模型名称',
|
modelProviderDescription: '请填写供应商向您提供的模型名称',
|
||||||
selectModel: '请选择模型',
|
selectModel: '请选择模型',
|
||||||
|
testSuccess: '测试成功',
|
||||||
|
testError: '测试失败,请检查模型配置',
|
||||||
},
|
},
|
||||||
bots: {
|
bots: {
|
||||||
title: '机器人',
|
title: '机器人',
|
||||||
@@ -115,6 +119,13 @@ const zhHans = {
|
|||||||
adapterConfig: '适配器配置',
|
adapterConfig: '适配器配置',
|
||||||
bindPipeline: '绑定流水线',
|
bindPipeline: '绑定流水线',
|
||||||
selectPipeline: '选择流水线',
|
selectPipeline: '选择流水线',
|
||||||
|
botLogTitle: '机器人日志',
|
||||||
|
enableAutoRefresh: '开启自动刷新',
|
||||||
|
session: '会话',
|
||||||
|
yesterday: '昨天',
|
||||||
|
earlier: '更久之前',
|
||||||
|
dateFormat: '{{month}}月{{day}}日',
|
||||||
|
setBotEnableError: '设置机器人启用状态失败',
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: '插件管理',
|
title: '插件管理',
|
||||||
|
|||||||
Reference in New Issue
Block a user