Compare commits

...

22 Commits

Author SHA1 Message Date
Junyan Qin (Chin)
7538973b33 chore: release v3.4.14.3 (#1358) 2025-04-29 19:45:19 +08:00
shinelin
3554702054 feat(gewechat): 重构target2yiri代码+引用消息展开 (#1352)
* feat(gewechat): 重构target2yiri代码+引用消息展开

* feat(gewe): 引用消息,图片视频音频是单独的类型
2025-04-29 13:18:19 +08:00
Guanchao Wang
96183eb3e0 fix: access_token problems in wecomcs (#1355) 2025-04-29 13:04:52 +08:00
Guanchao Wang
778065f7fb fix: image couldn't be sent in lark (#1348) 2025-04-28 15:30:30 +08:00
shinelin
ac500266f3 feat(gewechat): 优化了代码结构+fix群聊艾特逻辑,新增消息类型 (#1336)
* feat(gewechat): 优化了代码结构+fix群聊艾特逻辑,新增消息类型

* feat(gewechat): 移除不合理的message定义,优化GewechatMessageConverter

* bugfix(gewechat): fix typo

* feat(gewechat): 去掉多余日志+公众号消息和文件消息转发+msg_source取空异常fix

* bugfix(message):删除image中的xml定义

* bugfix(message): fix typo
2025-04-27 20:48:55 +08:00
Junyan Qin (Chin)
efed9f3348 Merge pull request #1338 from RockChinQ/RockChinQ-patch-1
Update README_EN.md
2025-04-26 21:08:55 +08:00
Junyan Qin (Chin)
f1ed79fa4e 优化了处理语音消息和群聊图片消息,增加了发送语音消息(只能发送silk格式语音文件链接)和转发链接消息 (#1323)
* 优化了处理语音消息和处理群聊图片消息,增加了发送语音消息

* 增加了微信转发链接消息组件

* 增加了转发链接

* 修改字段内容手误问题

* 优化收到小程序,公众号转账等消息时将其通过unknown传递出来,并修复voice字段写错问题

* 移除有一处将数据当作base64处理并通过unknown中content(但是没有啊)传递。
2025-04-24 22:13:02 +08:00
Dong_master
cb7f7b80df 移除有一处将数据当作base64处理并通过unknown中content(但是没有啊)传递。 2025-04-24 22:05:54 +08:00
Dong_master
112f99d6d9 优化收到小程序,公众号转账等消息时将其通过unknown传递出来,并修复voice字段写错问题 2025-04-24 21:12:30 +08:00
Dong_master
00cafb1188 修改字段内容手误问题 2025-04-24 00:00:49 +08:00
Junyan Qin (Chin)
8af401eea4 chore: release v3.4.14.2 (#1326) 2025-04-23 17:34:00 +08:00
Junyan Qin (Chin)
446546b69f fix(dify runner): response message event incorrect when using agent app (#1325) 2025-04-23 16:55:52 +08:00
Dong_master
5c26ce215b 增加了转发链接 2025-04-23 02:36:36 +08:00
Dong_master
8ca714853a 增加了微信转发链接消息组件 2025-04-23 02:28:39 +08:00
Dong_master
577dc0d175 优化了处理语音消息和处理群聊图片消息,增加了发送语音消息 2025-04-23 02:25:58 +08:00
Junyan Qin (Chin)
8a6d9d76da perf: reduce newline in think tag converting (#1319) 2025-04-20 13:41:02 +08:00
Junyan Qin (Chin)
92acaf6c27 chore: release 3.4.14.1 (#1315) 2025-04-19 22:30:22 +08:00
Junyan Qin (Chin)
4d53b3cb06 doc: update README
doc: update README
2025-04-18 20:25:50 +08:00
Junyan Qin (Chin)
7cad4ffa37 Merge pull request #1311 from RockChinQ/feat/ppio
feat: add support for ppio
2025-04-17 16:36:01 +08:00
Junyan Qin
b6f312325f chore: fix 2025-04-17 16:33:35 +08:00
Junyan Qin
43a6492cab chore: migration for ppio config 2025-04-17 16:32:19 +08:00
WangCham
92e3546e8a feat: add support for ppio 2025-04-17 16:18:05 +08:00
17 changed files with 748 additions and 326 deletions

2
.gitignore vendored
View File

@@ -38,5 +38,5 @@ botpy.log*
/poc /poc
/libs/wecom_api/test.py /libs/wecom_api/test.py
/venv /venv
/jp-tyo-churros-05.rockchin.top
test.py test.py
/web_ui

View File

@@ -8,7 +8,7 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">项目主页</a> <a href="https://langbot.app">项目主页</a>
<a href="https://docs.langbot.app/insight/intro.html">功能介绍</a> <a href="https://docs.langbot.app/insight/intro.html">功能介绍</a>
<a href="https://docs.langbot.app/insight/guide.html">部署文档</a> <a href="https://docs.langbot.app/insight/guide.html">部署文档</a>
<a href="https://docs.langbot.app/usage/faq.html">常见问题</a> <a href="https://docs.langbot.app/usage/faq.html">常见问题</a>
@@ -110,6 +110,7 @@
| [Anthropic](https://www.anthropic.com/) | ✅ | | | [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [智谱AI](https://open.bigmodel.cn/) | ✅ | | | [智谱AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 | | [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 | | [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 | | [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |

View File

@@ -7,7 +7,7 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">Home</a> <a href="https://langbot.app">Home</a>
<a href="https://docs.langbot.app/insight/intro.html">Features</a> <a href="https://docs.langbot.app/insight/intro.html">Features</a>
<a href="https://docs.langbot.app/insight/guide.html">Deployment</a> <a href="https://docs.langbot.app/insight/guide.html">Deployment</a>
<a href="https://docs.langbot.app/usage/faq.html">FAQ</a> <a href="https://docs.langbot.app/usage/faq.html">FAQ</a>
@@ -45,6 +45,7 @@
> >
> - Before you start deploying in any way, please read the [New User Guide](https://docs.langbot.app/insight/guide.html). > - Before you start deploying in any way, please read the [New User Guide](https://docs.langbot.app/insight/guide.html).
> - All documentation is in Chinese, we will provide i18n version in the near future. > - All documentation is in Chinese, we will provide i18n version in the near future.
> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot).
#### Docker Compose Deployment #### Docker Compose Deployment
@@ -108,6 +109,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [Dify](https://dify.ai) | ✅ | LLMOps platform | | [Dify](https://dify.ai) | ✅ | LLMOps platform |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform | | [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform | | [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) | | [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |

View File

@@ -7,7 +7,7 @@
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://docs.langbot.app">ホーム</a> <a href="https://langbot.app">ホーム</a>
<a href="https://docs.langbot.app/insight/intro.html">機能</a> <a href="https://docs.langbot.app/insight/intro.html">機能</a>
<a href="https://docs.langbot.app/insight/guide.html">デプロイ</a> <a href="https://docs.langbot.app/insight/guide.html">デプロイ</a>
<a href="https://docs.langbot.app/usage/faq.html">FAQ</a> <a href="https://docs.langbot.app/usage/faq.html">FAQ</a>
@@ -44,6 +44,7 @@
> >
> - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。 > - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。
> - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。 > - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。
> - Read [the auto-generated wiki on DeepWiki](https://deepwiki.com/RockChinQ/LangBot)。
#### Docker Compose デプロイ #### Docker Compose デプロイ
@@ -106,6 +107,7 @@ LangBotはBTPanelにリストされています。BTPanelをインストール
| [Anthropic](https://www.anthropic.com/) | ✅ | | | [Anthropic](https://www.anthropic.com/) | ✅ | |
| [xAI](https://x.ai/) | ✅ | | | [xAI](https://x.ai/) | ✅ | |
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | | [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム | | [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム | | [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム | | [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |

View File

@@ -71,6 +71,8 @@ class WecomCSClient():
async def get_detailed_message_list(self,xml_msg:str): async def get_detailed_message_list(self,xml_msg:str):
# 在本方法中解析消息,并且获得消息的具体内容 # 在本方法中解析消息,并且获得消息的具体内容
if isinstance(xml_msg, bytes):
xml_msg = xml_msg.decode('utf-8')
root = ET.fromstring(xml_msg) root = ET.fromstring(xml_msg)
token = root.find("Token").text token = root.find("Token").text
open_kfid = root.find("OpenKfId").text open_kfid = root.find("OpenKfId").text
@@ -92,11 +94,12 @@ class WecomCSClient():
} }
response = await client.post(url,json=params) response = await client.post(url,json=params)
data = response.json() data = response.json()
if data['errcode'] != 0:
raise Exception("Failed to get message")
if data['errcode'] == 40014 or data['errcode'] == 42001: if data['errcode'] == 40014 or data['errcode'] == 42001:
self.access_token = await self.get_access_token(self.secret) self.access_token = await self.get_access_token(self.secret)
return await self.get_detailed_message_list(xml_msg) return await self.get_detailed_message_list(xml_msg)
if data['errcode'] != 0:
raise Exception("Failed to get message")
last_msg_data = data['msg_list'][-1] last_msg_data = data['msg_list'][-1]
open_kfid = last_msg_data.get("open_kfid") open_kfid = last_msg_data.get("open_kfid")
# 进行获取图片操作 # 进行获取图片操作
@@ -182,8 +185,11 @@ class WecomCSClient():
response = await client.post(url, json=payload) response = await client.post(url, json=payload)
data = response.json() data = response.json()
if data.get("errcode") != 0: if data['errcode'] == 40014 or data['errcode'] == 42001:
raise Exception(f"消息发送失败: {data}") self.access_token = await self.get_access_token(self.secret)
return await self.send_text_msg(open_kfid,external_userid,msgid,content)
if data['errcode'] != 0:
raise Exception("Failed to send message")
return data return data

View File

@@ -3,9 +3,9 @@ from __future__ import annotations
from .. import migration from .. import migration
@migration.migration_class("modelscope-config-completion", 4) @migration.migration_class("modelscope-config-completion", 39)
class ModelScopeConfigCompletionMigration(migration.Migration): class ModelScopeConfigCompletionMigration(migration.Migration):
"""OpenAI配置迁移 """ModelScope配置迁移
""" """
async def need_migrate(self) -> bool: async def need_migrate(self) -> bool:

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from .. import migration
@migration.migration_class("ppio-config", 40)
class PPIOConfigMigration(migration.Migration):
"""PPIO配置迁移
"""
async def need_migrate(self) -> bool:
"""判断当前环境是否需要运行此迁移
"""
return 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester'] \
or 'ppio' not in self.ap.provider_cfg.data['keys']
async def run(self):
"""执行迁移
"""
if 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester']:
self.ap.provider_cfg.data['requester']['ppio-chat-completions'] = {
'base-url': 'https://api.ppinfra.com/v3/openai',
'args': {},
'timeout': 120,
}
if 'ppio' not in self.ap.provider_cfg.data['keys']:
self.ap.provider_cfg.data['keys']['ppio'] = []
await self.ap.provider_cfg.dump_config()

View File

@@ -12,7 +12,7 @@ from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_conf
from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config from ..migrations import m026_qqofficial_config, m027_wx_official_account_config, m028_aliyun_requester_config
from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config from ..migrations import m029_dashscope_app_api_config, m030_lark_config_cmpl, m031_dingtalk_config, m032_volcark_config
from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message from ..migrations import m033_dify_thinking_config, m034_gewechat_file_url_config, m035_wxoa_mode, m036_wxoa_loading_message
from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion from ..migrations import m037_mcp_config, m038_tg_dingtalk_markdown, m039_modelscope_cfg_completion, m040_ppio_config
@stage.stage_class("MigrationStage") @stage.stage_class("MigrationStage")

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import gewechat_client import gewechat_client
import typing import typing
@@ -26,7 +24,8 @@ 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
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Optional, List, Tuple
from functools import partial
class GewechatMessageConverter(adapter.MessageConverter): class GewechatMessageConverter(adapter.MessageConverter):
@@ -58,168 +57,367 @@ class GewechatMessageConverter(adapter.MessageConverter):
elif isinstance(component, platform_message.WeChatLink): elif isinstance(component, platform_message.WeChatLink):
content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc, content_list.append({'type': 'WeChatLink', 'link_title': component.link_title, 'link_desc': component.link_desc,
'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url}) 'link_thumb_url': component.link_thumb_url, 'link_url': component.link_url})
elif isinstance(component, platform_message.WeChatForwardLink):
content_list.append({'type': 'WeChatForwardLink', 'xml_data': component.xml_data})
elif isinstance(component, platform_message.Voice): elif isinstance(component, platform_message.Voice):
content_list.append({"type": "voice", "url": component.url, "length": component.length}) content_list.append({"type": "voice", "url": component.url, "length": component.length})
elif isinstance(component, platform_message.WeChatForwardImage):
content_list.append({'type': 'WeChatForwardImage', 'xml_data': component.xml_data})
elif isinstance(component, platform_message.WeChatForwardFile):
content_list.append({'type': 'WeChatForwardFile', 'xml_data': component.xml_data})
elif isinstance(component, platform_message.WeChatAppMsg):
content_list.append({'type': 'WeChatAppMsg', 'app_msg': component.app_msg})
elif isinstance(component, platform_message.Forward): elif isinstance(component, platform_message.Forward):
for node in component.node_list: for node in component.node_list:
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain)) if node.message_chain:
content_list.extend(await GewechatMessageConverter.yiri2target(node.message_chain))
return content_list return content_list
async def target2yiri( async def target2yiri(
self, self,
message: dict, message: dict,
bot_account_id: str bot_account_id: str
) -> platform_message.MessageChain: ) -> platform_message.MessageChain:
"""外部消息转平台消息"""
# 数据预处理
message_list = []
ats_bot = False # 是否被@
content = message["Data"]["Content"]["string"]
content_no_preifx = content # 群消息则去掉前缀
is_group_message = self._is_group_message(message)
if is_group_message:
ats_bot = self._ats_bot(message, bot_account_id)
if "@所有人" in content:
message_list.append(platform_message.AtAll())
elif ats_bot:
message_list.append(platform_message.At(target=bot_account_id))
content_no_preifx, _ = self._extract_content_and_sender(content)
msg_type = message["Data"]["MsgType"]
# 映射消息类型到处理器方法
handler_map = {
1: self._handler_text,
3: self._handler_image,
34: self._handler_voice,
49: self._handler_compound, # 复合类型
}
if message["Data"]["MsgType"] == 1: # 分派处理
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉 handler = handler_map.get(msg_type, self._handler_default)
regex = re.compile(r"^wxid_.*:") handler_result = await handler(
# print(message) message = message, # 原始的message
content_no_preifx = content_no_preifx, # 处理后的content
)
line_split = message["Data"]["Content"]["string"].split("\n") if handler_result and len(handler_result) > 0:
message_list.extend(handler_result)
if len(line_split) > 0 and regex.match(line_split[0]): return platform_message.MessageChain(message_list)
message["Data"]["Content"]["string"] = "\n".join(line_split[1:])
async def _handler_text(
# 正则表达式模式,匹配'@'后跟任意数量的非空白字符 self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理文本消息 (msg_type=1)"""
if message and self._is_group_message(message):
pattern = r'@\S+' pattern = r'@\S+'
at_string = f"@{bot_account_id}" content_no_preifx = re.sub(pattern, '', content_no_preifx)
content_list = []
if at_string in message["Data"]["Content"]["string"]:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(message["Data"]["Content"]["string"].replace(at_string, '', 1)))
# 更优雅的替换改名后@机器人仅仅限于单独AT的情况
elif "PushContent" in message['Data'] and '在群聊中@了你' in message["Data"]["PushContent"]:
if '@所有人' in message["Data"]["Content"]["string"]: # at全员时候传入atll不当作at自己
content_list.append(platform_message.AtAll())
else:
content_list.append(platform_message.At(target=bot_account_id))
content_list.append(platform_message.Plain(re.sub(pattern, '', message["Data"]["Content"]["string"])))
else:
content_list = [platform_message.Plain(message["Data"]["Content"]["string"])]
return platform_message.MessageChain(content_list) return platform_message.MessageChain([platform_message.Plain(content_no_preifx)])
elif message["Data"]["MsgType"] == 3: async def _handler_image(
image_xml = message["Data"]["Content"]["string"] self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理图像消息 (msg_type=3)"""
try:
image_xml = content_no_preifx
if not image_xml: if not image_xml:
return platform_message.MessageChain([ return platform_message.MessageChain([platform_message.Unknown("[图片内容为空]")])
platform_message.Plain(text="[图片内容为空]")
])
base64_str, image_format = await image.get_gewechat_image_base64(
try: gewechat_url=self.config["gewechat_url"],
base64_str, image_format = await image.get_gewechat_image_base64( gewechat_file_url=self.config["gewechat_file_url"],
gewechat_url=self.config["gewechat_url"], app_id=self.config["app_id"],
gewechat_file_url=self.config["gewechat_file_url"], xml_content=image_xml,
app_id=self.config["app_id"], token=self.config["token"],
xml_content=image_xml, image_type=2,
token=self.config["token"],
image_type=2,
)
return platform_message.MessageChain([
platform_message.Image(
base64=f"data:image/{image_format};base64,{base64_str}"
)
])
except Exception as e:
print(f"处理图片消息失败: {str(e)}")
return platform_message.MessageChain([
platform_message.Plain(text=f"[图片处理失败]")
])
elif message["Data"]["MsgType"] == 34:
audio_base64 = message["Data"]["ImgBuf"]["buffer"]
return platform_message.MessageChain(
[platform_message.Voice(base64=f"data:audio/silk;base64,{audio_base64}")]
) )
elif message["Data"]["MsgType"] == 49:
# 支持微信聊天记录的消息类型,将 XML 内容转换为 MessageChain 传递 elements = [
platform_message.Image(base64=f"data:image/{image_format};base64,{base64_str}"),
platform_message.WeChatForwardImage(xml_data=image_xml) # 微信消息转发
]
return platform_message.MessageChain(elements)
except Exception as e:
print(f"处理图片失败: {str(e)}")
return platform_message.MessageChain([platform_message.Unknown("[图片处理失败]")])
async def _handler_voice(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理语音消息 (msg_type=34)"""
message_List = []
try:
# 从消息中提取语音数据(需根据实际数据结构调整字段名)
audio_base64 = message["Data"]["ImgBuf"]["buffer"]
# 验证语音数据有效性
if not audio_base64:
message_List.append(platform_message.Unknown(text="[语音内容为空]"))
return platform_message.MessageChain(message_List)
# 转换为平台支持的语音格式(如 Silk 格式)
voice_element = platform_message.Voice(
base64=f"data:audio/silk;base64,{audio_base64}"
)
message_List.append(voice_element)
except KeyError as e:
print(f"语音数据字段缺失: {str(e)}")
message_List.append(platform_message.Unknown(text="[语音数据解析失败]"))
except Exception as e:
print(f"处理语音消息异常: {str(e)}")
message_List.append(platform_message.Unknown(text="[语音处理失败]"))
return platform_message.MessageChain(message_List)
async def _handler_compound(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理复合消息 (msg_type=49),根据子类型分派"""
try:
xml_data = ET.fromstring(content_no_preifx)
appmsg_data = xml_data.find('.//appmsg')
data_type = appmsg_data.findtext('.//type', "")
# 二次分派处理器
sub_handler_map = {
'57': self._handler_compound_quote,
'5': self._handler_compound_link,
'6': self._handler_compound_file,
'33': self._handler_compound_mini_program,
'36': self._handler_compound_mini_program,
'2000': partial(self._handler_compound_unsupported, text="[转账消息]"),
'2001': partial(self._handler_compound_unsupported, text="[红包消息]"),
'51': partial(self._handler_compound_unsupported, text="[视频号消息]"),
}
handler = sub_handler_map.get(data_type, self._handler_compound_unsupported)
return await handler(
message=message, #原始msg
xml_data=xml_data, # xml数据
)
except Exception as e:
print(f"解析复合消息失败: {str(e)}")
return platform_message.MessageChain([platform_message.Unknown(text=content_no_preifx)])
async def _handler_compound_quote(
self,
message: Optional[dict],
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理引用消息 (data_type=57)"""
# print("_handler_compound_quote", ET.tostring(xml_data, encoding='unicode'))
appmsg_data = xml_data.find('.//appmsg')
quote_data = "" # 引用原文
quote_id = None # 引用消息的原发送者
tousername = None # 接收方: 所属微信的wxid
user_data = "" # 用户消息
sender_id = xml_data.findtext('.//fromusername') # 发送方:单聊用户/群member
if appmsg_data:
user_data = appmsg_data.findtext('.//title') or ""
quote_data = appmsg_data.find('.//refermsg').findtext('.//content')
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr')
if message:
tousername = message['Wxid']
message_list = []
# quote_data原始的消息
if quote_data:
quote_data_message_list = platform_message.MessageChain()
# 文本消息
try: try:
content = message["Data"]["Content"]["string"] if "<msg>" not in quote_data:
# 有三种可能的消息结构weid开头私聊直接<?xml>和直接<msg> quote_data_message_list.append(platform_message.Plain(quote_data))
if content.startswith('wxid'):
xml_list = content.split('\n')[2:]
xml_data = '\n'.join(xml_list)
elif content.startswith('<?xml'):
xml_list = content.split('\n')[1:]
xml_data = '\n'.join(xml_list)
else: else:
xml_data = content # 引用消息展开
quote_data_xml = ET.fromstring(quote_data)
content_data = ET.fromstring(xml_data) if quote_data_xml.find("img"):
# print(xml_data) quote_data_message_list.extend(await self._handler_image(None, quote_data))
# 拿到细分消息类型按照gewe接口中描述 elif quote_data_xml.find("voicemsg"):
''' quote_data_message_list.extend(await self._handler_voice(None, quote_data))
小程序33/36 elif quote_data_xml.find("videomsg"):
引用消息57 quote_data_message_list.extend(await self._handler_default(None, quote_data)) # 先不处理
转账消息2000 else:
红包消息2001 # appmsg
视频号消息51 quote_data_message_list.extend(await self._handler_compound(None, quote_data))
'''
appmsg_data = content_data.find('.//appmsg')
data_type = appmsg_data.find('.//type').text
if data_type == '57':
user_data = appmsg_data.find('.//title').text # 拿到用户消息
quote_data = appmsg_data.find('.//refermsg').find('.//content').text # 引用原文
sender_id = appmsg_data.find('.//refermsg').find('.//chatusr').text # 引用用户id
from_name = message['Data']['FromUserName']['string']
message_list =[]
if message['Wxid'] == sender_id and from_name.endswith('@chatroom'): # 因为引用机制暂时无法响应用户所以当引用用户是机器人是构建一个at激活机器人
message_list.append(platform_message.At(target=bot_account_id))
message_list.append(platform_message.Quote(
sender_id=sender_id,
origin=platform_message.MessageChain(
[platform_message.Plain(quote_data)]
)))
message_list.append(platform_message.Plain(user_data))
return platform_message.MessageChain(message_list)
elif data_type == '51':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[视频号消息]')]
)
# print(content_data)
elif data_type == '2000':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[转账消息]')]
)
elif data_type == '2001':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[红包消息]')]
)
elif data_type == '5':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[公众号消息]')]
)
elif data_type == '33' or data_type == '36':
return platform_message.MessageChain(
[platform_message.Plain(text=f'[小程序消息]')]
)
# print(data_type.text)
else:
try:
content_bytes = content.encode('utf-8')
decoded_content = base64.b64decode(content_bytes)
return platform_message.MessageChain(
[platform_message.Unknown(content=decoded_content)]
)
except Exception as e:
return platform_message.MessageChain(
[platform_message.Plain(text=content)]
)
except Exception as e: except Exception as e:
print(f"Error processing type 49 message: {str(e)}") print(f"处理引用消息异常 expcetion:{e}")
return platform_message.MessageChain( quote_data_message_list.append(platform_message.Plain(quote_data))
[platform_message.Plain(text="[无法解析的消息]")] message_list.append(
platform_message.Quote(
sender_id=sender_id,
origin=quote_data_message_list,
) )
)
if len(user_data) > 0:
pattern = r'^@\S+'
user_data = re.sub(pattern, '', user_data)
message_list.append(platform_message.Plain(user_data))
# print(f"**_handler_compound_quote message_list len={len(message_list)}")
# for comp in message_list:
# if isinstance(comp, platform_message.Quote):
# print(f"**_handler_compound_quote send_id {comp.sender_id}" )
# for quote_item in comp.origin:
# print(f"******* _handler_compound_quote item {quote_item.type} + message: {quote_item}" )
# else:
# print(f"_handler_compound_quote type: {comp.type} + message: {comp}")
return platform_message.MessageChain(message_list)
async def _handler_compound_file(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理文件消息 (data_type=6)"""
xml_data_str = ET.tostring(xml_data, encoding='unicode')
return platform_message.MessageChain([
platform_message.WeChatForwardFile(xml_data=xml_data_str)
])
async def _handler_compound_link(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理链接消息(如公众号文章、外部网页)"""
message_list = []
try:
# 解析 XML 中的链接参数
appmsg = xml_data.find('.//appmsg')
if appmsg is None:
return platform_message.MessageChain()
message_list.append(
platform_message.WeChatLink(
link_title = appmsg.findtext('title', ''),
link_desc = appmsg.findtext('des', ''),
link_url = appmsg.findtext('url', ''),
link_thumb_url = appmsg.findtext("thumburl", '') # 这个字段拿不到
)
)
# 转发消息
xml_data_str = ET.tostring(xml_data, encoding='unicode')
# print(xml_data_str)
message_list.append(
platform_message.WeChatForwardLink(
xml_data=xml_data_str
)
)
except Exception as e:
print(f"解析链接消息失败: {str(e)}")
return platform_message.MessageChain(message_list)
async def _handler_compound_mini_program(
self,
message: dict,
xml_data: ET.Element
) -> platform_message.MessageChain:
"""处理小程序消息(如小程序卡片、服务通知)"""
xml_data_str = ET.tostring(xml_data, encoding='unicode')
return platform_message.MessageChain([
platform_message.WeChatForwardMiniPrograms(xml_data=xml_data_str)
])
async def _handler_default(
self,
message: Optional[dict],
content_no_preifx: str
) -> platform_message.MessageChain:
"""处理未知消息类型"""
if message:
msg_type = message["Data"]["MsgType"]
else:
msg_type = ""
return platform_message.MessageChain([
platform_message.Unknown(text=f"[未知消息类型 msg_type:{msg_type}]")
])
def _handler_compound_unsupported(
self,
message: dict,
xml_data: str,
text: Optional[str] = None
) -> platform_message.MessageChain:
"""处理未支持复合消息类型(msg_type=49)子类型"""
if not text:
text = f"[xml_data={xml_data}]"
content_list = []
content_list.append(
platform_message.Unknown(text=f"[处理未支持复合消息类型[msg_type=49]|{text}"))
return platform_message.MessageChain(content_list)
# 返回是否被艾特
def _ats_bot(self, message: dict, bot_account_id:str) -> bool:
ats_bot = False
try:
to_user_name = message['Wxid'] # 接收方: 所属微信的wxid
raw_content = message["Data"]["Content"]["string"] # 原始消息内容
content_no_prefix, _ = self._extract_content_and_sender(raw_content)
# 直接艾特机器人
ats_bot = ats_bot or (f"@{bot_account_id}" in content_no_prefix)
# 文本类@bot
push_content = message.get('Data', {}).get('PushContent', '')
ats_bot = ats_bot or ('在群聊中@了你' in push_content)
# 引用别人时@bot
msg_source = message.get('Data', {}).get('MsgSource', '') or ''
if len(msg_source) > 0:
msg_source_data = ET.fromstring(msg_source)
at_user_list = msg_source_data.findtext("atuserlist") or ""
ats_bot = ats_bot or (to_user_name in at_user_list)
# 引用bot
if message.get('Data', {}).get('MsgType', 0) == 49:
xml_data = ET.fromstring(content_no_prefix)
appmsg_data = xml_data.find('.//appmsg')
tousername = message['Wxid'] # 接收方: 所属微信的wxid
quote_id = appmsg_data.find('.//refermsg').findtext('.//chatusr') # 引用消息的原发送者
ats_bot = ats_bot or (quote_id == tousername)
except Exception as e:
print(f"_ats_bot got except: {e}")
finally:
return ats_bot
# 提取一下content前面的sender_id, 和去掉前缀的内容
def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]:
try:
# 检查消息开头,如果有 wxid_sbitaz0mt65n22:\n 则删掉
# add: 有些用户的wxid不是上述格式。换成user_name:
regex = re.compile(r"^[a-zA-Z0-9_\-]{5,20}:")
line_split = raw_content.split("\n")
if len(line_split) > 0 and regex.match(line_split[0]):
raw_content = "\n".join(line_split[1:])
sender_id = line_split[0].strip(":")
return raw_content, sender_id
except Exception as e:
print(f"_extract_content_and_sender got except: {e}")
finally:
return raw_content, None
# 是否是群消息
def _is_group_message(self, message: dict)->bool:
from_user_name = message['Data']['FromUserName']['string']
return from_user_name.endswith("@chatroom")
class GewechatEventConverter(adapter.EventConverter): class GewechatEventConverter(adapter.EventConverter):
@@ -311,7 +509,6 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
def __init__(self, config: dict, ap: app.Application): def __init__(self, config: dict, ap: app.Application):
self.config = config self.config = config
self.ap = ap self.ap = ap
self.quart_app = quart.Quart(__name__) self.quart_app = quart.Quart(__name__)
self.message_converter = GewechatMessageConverter(config) self.message_converter = GewechatMessageConverter(config)
@@ -346,50 +543,118 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
return 'ok' return 'ok'
async def _handle_message(
self,
message: platform_message.MessageChain,
target_id: str
):
"""统一消息处理核心逻辑"""
content_list = await self.message_converter.yiri2target(message)
at_targets = [item["target"] for item in content_list if item["type"] == "at"]
# 处理@逻辑
at_targets = at_targets or []
member_info = []
if at_targets:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
target_id,
at_targets[::-1]
)["data"]
# 处理消息组件
for msg in content_list:
# 文本消息处理@
if msg['type'] == 'text' and at_targets:
for member in member_info:
msg['content'] = f'@{member["nickName"]} {msg["content"]}'
# 统一消息派发
handler_map = {
'text': lambda msg: self.bot.post_text(
app_id=self.config['app_id'],
to_wxid=target_id,
content=msg['content'],
ats=",".join(at_targets)
),
'image': lambda msg: self.bot.post_image(
app_id=self.config['app_id'],
to_wxid=target_id,
img_url=msg["image"]
),
'WeChatForwardMiniPrograms': lambda msg: self.bot.forward_mini_app(
app_id=self.config['app_id'],
to_wxid=target_id,
xml=msg['xml_data'],
cover_img_url=msg.get('image_url')
),
'WeChatEmoji': lambda msg: self.bot.post_emoji(
app_id=self.config['app_id'],
to_wxid=target_id,
emoji_md5=msg['emoji_md5'],
emoji_size=msg['emoji_size']
),
'WeChatLink': lambda msg: self.bot.post_link(
app_id=self.config['app_id'],
to_wxid=target_id,
title=msg['link_title'],
desc=msg['link_desc'],
link_url=msg['link_url'],
thumb_url=msg['link_thumb_url'],
),
'WeChatMiniPrograms': lambda msg: self.bot.post_mini_app(
app_id=self.config['app_id'],
to_wxid=target_id,
mini_app_id=msg['mini_app_id'],
display_name=msg['display_name'],
page_path=msg['page_path'],
cover_img_url=msg['cover_img_url'],
title=msg['title'],
user_name=msg['user_name']
),
'WeChatForwardLink': lambda msg: self.bot.forward_url(
app_id=self.config['app_id'],
to_wxid=target_id,
xml=msg['xml_data']
),
'WeChatForwardImage': lambda msg: self.bot.forward_image(
app_id=self.config['app_id'],
to_wxid=target_id,
xml=msg['xml_data']
),
'WeChatForwardFile': lambda msg: self.bot.forward_file(
app_id=self.config['app_id'],
to_wxid=target_id,
xml=msg['xml_data']
),
'voice': lambda msg: self.bot.post_voice(
app_id=self.config['app_id'],
to_wxid=target_id,
voice_url=msg['url'],
voice_duration=msg['length']
),
'WeChatAppMsg': lambda msg: self.bot.post_app_msg(
app_id=self.config['app_id'],
to_wxid=target_id,
appmsg=msg['app_msg']
),
'at': lambda msg: None
}
if handler := handler_map.get(msg['type']):
handler(msg)
else:
self.ap.logger.warning(f"未处理的消息类型: {msg['type']}")
continue
async def send_message( async def send_message(
self, self,
target_type: str, target_type: str,
target_id: str, target_id: str,
message: platform_message.MessageChain message: platform_message.MessageChain
): ):
geweap_msg = await self.message_converter.yiri2target(message) """主动发送消息"""
# 此处加上群消息at处理 return await self._handle_message(message, target_id)
ats = [item["target"] for item in geweap_msg if item["type"] == "at"]
for msg in geweap_msg:
# at主动发送消息
if msg['type'] == 'text':
if ats:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
target_id,
ats[::-1]
)["data"]
for member in member_info:
msg['content'] = f'@{member["nickName"]} {msg["content"]}'
self.bot.post_text(app_id=self.config['app_id'], to_wxid=target_id, content=msg['content'],
ats=",".join(ats))
elif msg['type'] == 'image':
self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
elif msg['type'] == 'WeChatMiniPrograms':
self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id']
, display_name=msg['display_name'], page_path=msg['page_path']
, cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name'])
elif msg['type'] == 'WeChatForwardMiniPrograms':
self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url'])
elif msg['type'] == 'WeChatEmoji':
self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id,
emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'])
elif msg['type'] == 'WeChatLink':
self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id
,title=msg['link_title'], desc=msg['link_desc']
, link_url=msg['link_url'], thumb_url=msg['link_thumb_url'])
async def reply_message( async def reply_message(
self, self,
@@ -397,46 +662,10 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
message: platform_message.MessageChain, message: platform_message.MessageChain,
quote_origin: bool = False quote_origin: bool = False
): ):
content_list = await self.message_converter.yiri2target(message) """回复消息"""
if message_source.source_platform_object:
ats = [item["target"] for item in content_list if item["type"] == "at"] target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"]
target_id = message_source.source_platform_object["Data"]["FromUserName"]["string"] return await self._handle_message(message, target_id)
for msg in content_list:
if msg["type"] == "text":
if ats:
member_info = self.bot.get_chatroom_member_detail(
self.config["app_id"],
message_source.source_platform_object["Data"]["FromUserName"]["string"],
ats[::-1]
)["data"]
for member in member_info:
msg['content'] = f'@{member["nickName"]} {msg["content"]}'
self.bot.post_text(
app_id=self.config["app_id"],
to_wxid=message_source.source_platform_object["Data"]["FromUserName"]["string"],
content=msg["content"],
ats=",".join(ats)
)
elif msg['type'] == 'image':
self.bot.post_image(app_id=self.config['app_id'], to_wxid=target_id, img_url=msg["image"])
elif msg['type'] == 'WeChatMiniPrograms':
self.bot.post_mini_app(app_id=self.config['app_id'], to_wxid=target_id, mini_app_id=msg['mini_app_id']
, display_name=msg['display_name'], page_path=msg['page_path']
, cover_img_url=msg['cover_img_url'], title=msg['title'], user_name=msg['user_name'])
elif msg['type'] == 'WeChatForwardMiniPrograms':
self.bot.forward_mini_app(app_id=self.config['app_id'], to_wxid=target_id, xml=msg['xml_data'], cover_img_url=msg['image_url'])
elif msg['type'] == 'WeChatEmoji':
self.bot.post_emoji(app_id=self.config['app_id'], to_wxid=target_id,
emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size'])
elif msg['type'] == 'WeChatLink':
self.bot.post_link(app_id=self.config['app_id'], to_wxid=target_id
, title=msg['link_title'], desc=msg['link_desc']
, link_url=msg['link_url'], thumb_url=msg['link_thumb_url'])
async def is_muted(self, group_id: int) -> bool: async def is_muted(self, group_id: int) -> bool:
pass pass
@@ -490,8 +719,13 @@ class GeWeChatAdapter(adapter.MessagePlatformAdapter):
time.sleep(2) time.sleep(2)
ret = self.bot.set_callback(self.config["token"], self.config["callback_url"]) try:
print('设置 Gewechat 回调:', ret) # gewechat-server容器重启, token会变但是还会登录成功
# 换新token也会收不到回调要重新登陆下。
ret = self.bot.set_callback(self.config["token"], self.config["callback_url"])
except Exception as e:
raise Exception(f"设置 Gewechat 回调失败, token失效 {e}")
threading.Thread(target=gewechat_login_process).start() threading.Thread(target=gewechat_login_process).start()

View File

@@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import lark_oapi import lark_oapi
from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody, CreateImageResponse
import traceback
import typing import typing
import asyncio import asyncio
import traceback import traceback
@@ -60,12 +61,22 @@ class LarkMessageConverter(adapter.MessageConverter):
message_chain: platform_message.MessageChain, api_client: lark_oapi.Client message_chain: platform_message.MessageChain, api_client: lark_oapi.Client
) -> typing.Tuple[list]: ) -> typing.Tuple[list]:
message_elements = [] message_elements = []
pending_paragraph = [] pending_paragraph = []
for msg in message_chain: for msg in message_chain:
if isinstance(msg, platform_message.Plain): if isinstance(msg, platform_message.Plain):
pending_paragraph.append({"tag": "md", "text": msg.text}) # Ensure text is valid UTF-8
try:
text = msg.text.encode('utf-8').decode('utf-8')
pending_paragraph.append({"tag": "md", "text": text})
except UnicodeError:
# If text is not valid UTF-8, try to decode with other encodings
try:
text = msg.text.encode('latin1').decode('utf-8')
pending_paragraph.append({"tag": "md", "text": text})
except UnicodeError:
# If still fails, replace invalid characters
text = msg.text.encode('utf-8', errors='replace').decode('utf-8')
pending_paragraph.append({"tag": "md", "text": text})
elif isinstance(msg, platform_message.At): elif isinstance(msg, platform_message.At):
pending_paragraph.append( pending_paragraph.append(
{"tag": "at", "user_id": msg.target, "style": []} {"tag": "at", "user_id": msg.target, "style": []}
@@ -73,51 +84,84 @@ class LarkMessageConverter(adapter.MessageConverter):
elif isinstance(msg, platform_message.AtAll): elif isinstance(msg, platform_message.AtAll):
pending_paragraph.append({"tag": "at", "user_id": "all", "style": []}) pending_paragraph.append({"tag": "at", "user_id": "all", "style": []})
elif isinstance(msg, platform_message.Image): elif isinstance(msg, platform_message.Image):
image_bytes = None image_bytes = None
if msg.base64: if msg.base64:
image_bytes = base64.b64decode(msg.base64) try:
# Remove data URL prefix if present
if msg.base64.startswith('data:'):
msg.base64 = msg.base64.split(',', 1)[1]
image_bytes = base64.b64decode(msg.base64)
except Exception as e:
traceback.print_exc()
continue
elif msg.url: elif msg.url:
async with aiohttp.ClientSession() as session: try:
async with session.get(msg.url) as response: async with aiohttp.ClientSession() as session:
image_bytes = await response.read() async with session.get(msg.url) as response:
if response.status == 200:
image_bytes = await response.read()
else:
traceback.print_exc()
continue
except Exception as e:
traceback.print_exc()
continue
elif msg.path: elif msg.path:
with open(msg.path, "rb") as f: try:
image_bytes = f.read() with open(msg.path, "rb") as f:
image_bytes = f.read()
except Exception as e:
traceback.print_exc()
continue
request: CreateImageRequest = ( if image_bytes is None:
CreateImageRequest.builder() continue
.request_body(
CreateImageRequestBody.builder()
.image_type("message")
.image(image_bytes)
.build()
)
.build()
)
response: CreateImageResponse = await api_client.im.v1.image.acreate( try:
request # Create a temporary file to store the image bytes
) import tempfile
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(image_bytes)
temp_file.flush()
if not response.success(): # Create image request using the temporary file
raise Exception( request = CreateImageRequest.builder()\
f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" .request_body(
) CreateImageRequestBody.builder()
.image_type("message")
.image(open(temp_file.name, "rb"))
.build()
)\
.build()
image_key = response.data.image_key response = await api_client.im.v1.image.acreate(request)
message_elements.append(pending_paragraph) if not response.success():
message_elements.append( raise Exception(
[ f"client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
{ )
"tag": "img",
"image_key": image_key, image_key = response.data.image_key
}
] message_elements.append(pending_paragraph)
) message_elements.append(
pending_paragraph = [] [
{
"tag": "img",
"image_key": image_key,
}
]
)
pending_paragraph = []
except Exception as e:
traceback.print_exc()
continue
finally:
# Clean up the temporary file
import os
if 'temp_file' in locals():
os.unlink(temp_file.name)
elif isinstance(msg, platform_message.Forward): elif isinstance(msg, platform_message.Forward):
for node in msg.node_list: for node in msg.node_list:
message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client)) message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client))

View File

@@ -1,16 +1,15 @@
import itertools import itertools
import logging import logging
import typing
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
import typing
import pydantic.v1 as pydantic import pydantic.v1 as pydantic
from . import entities as platform_entities from . import entities as platform_entities
from .base import PlatformBaseModel, PlatformIndexedMetaclass, PlatformIndexedModel from .base import PlatformBaseModel, PlatformIndexedMetaclass, PlatformIndexedModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -642,7 +641,8 @@ class Unknown(MessageComponent):
"""消息组件类型。""" """消息组件类型。"""
text: str text: str
"""文本。""" """文本。"""
def __str__(self):
return f'Unknown Message: {self.text}'
class Voice(MessageComponent): class Voice(MessageComponent):
"""语音。""" """语音。"""
@@ -837,6 +837,8 @@ class WeChatForwardMiniPrograms(MessageComponent):
xml_data: str xml_data: str
"""首页图片""" """首页图片"""
image_url: typing.Optional[str] = None image_url: typing.Optional[str] = None
def __str__(self):
return self.xml_data
class WeChatEmoji(MessageComponent): class WeChatEmoji(MessageComponent):
@@ -860,3 +862,35 @@ class WeChatLink(MessageComponent):
"""链接略缩图""" """链接略缩图"""
link_thumb_url: str = '' link_thumb_url: str = ''
class WeChatForwardLink(MessageComponent):
"""转发链接。个人微信专用组件。"""
type: str = 'WeChatForwardLink'
"""xml数据"""
xml_data: str
def __str__(self):
return self.xml_data
class WeChatForwardImage(MessageComponent):
"""转发图片。个人微信专用组件。"""
type: str = 'WeChatForwardImage'
"""xml数据"""
xml_data: str
def __str__(self):
return self.xml_data
class WeChatForwardFile(MessageComponent):
"""转发文件。个人微信专用组件。"""
type: str = 'WeChatForwardFile'
"""xml数据"""
xml_data: str
def __str__(self):
return self.xml_data
class WeChatAppMsg(MessageComponent):
"""通用appmsg发送。个人微信专用组件。"""
type: str = 'WeChatAppMsg'
"""xml数据"""
app_msg: str
def __str__(self):
return self.app_msg

View File

@@ -65,7 +65,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester):
# deepseek的reasoner模型 # deepseek的reasoner模型
if reasoning_content is not None: if reasoning_content is not None:
chatcmpl_message['content'] = "<think>\n" + reasoning_content + "\n</think>\n\n"+ chatcmpl_message['content'] chatcmpl_message['content'] = "<think>\n" + reasoning_content + "\n</think>\n"+ chatcmpl_message['content']
message = llm_entities.Message(**chatcmpl_message) message = llm_entities.Message(**chatcmpl_message)

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
import openai
from . import chatcmpl, modelscopechatcmpl
from .. import requester
from ....core import app
class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions):
"""欧派云 ChatCompletion API 请求器"""
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['ppio-chat-completions']

View File

@@ -0,0 +1,34 @@
apiVersion: v1
kind: LLMAPIRequester
metadata:
name: ppio-chat-completions
label:
en_US: ppio
zh_CN: 派欧云
spec:
config:
- name: base-url
label:
en_US: Base URL
zh_CN: 基础 URL
type: string
required: true
default: "https://api.ppinfra.com/v3/openai"
- name: args
label:
en_US: Args
zh_CN: 附加参数
type: object
required: true
default: {}
- name: timeout
label:
en_US: Timeout
zh_CN: 超时时间
type: int
required: true
default: 120
execution:
python:
path: ./ppiochatcmpl.py
attr: PPIOChatCompletions

View File

@@ -164,12 +164,14 @@ class DifyServiceAPIRunner(runner.RequestRunner):
for image_id in image_ids for image_id in image_ids
] ]
ignored_events = ["agent_message"] ignored_events = []
inputs = {} inputs = {}
inputs.update(query.variables) inputs.update(query.variables)
pending_agent_message = ''
async for chunk in self.dify_client.chat_messages( async for chunk in self.dify_client.chat_messages(
inputs=inputs, inputs=inputs,
query=plain_text, query=plain_text,
@@ -183,50 +185,55 @@ class DifyServiceAPIRunner(runner.RequestRunner):
if chunk["event"] in ignored_events: if chunk["event"] in ignored_events:
continue continue
if chunk["event"] == "agent_thought":
if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过
continue
if chunk['thought'].strip() != '': # 文字回复内容
msg = llm_entities.Message(
role="assistant",
content=chunk["thought"],
)
yield msg
if chunk['tool']:
msg = llm_entities.Message(
role="assistant",
tool_calls=[
llm_entities.ToolCall(
id=chunk['id'],
type="function",
function=llm_entities.FunctionCall(
name=chunk["tool"],
arguments=json.dumps({}),
),
)
],
)
yield msg
if chunk['event'] == 'message_file':
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
base_url = self.dify_client.base_url
if base_url.endswith('/v1'):
base_url = base_url[:-3]
image_url = base_url + chunk['url']
if chunk['event'] == 'agent_message':
pending_agent_message += chunk['answer']
else:
if pending_agent_message.strip() != '':
pending_agent_message = pending_agent_message.replace('</details>Action:', '</details>')
yield llm_entities.Message( yield llm_entities.Message(
role="assistant", role="assistant",
content=[llm_entities.ContentElement.from_image_url(image_url)], content=self._try_convert_thinking(pending_agent_message),
) )
if chunk['event'] == 'error': pending_agent_message = ''
raise errors.DifyAPIError("dify 服务错误: " + chunk['message'])
if chunk["event"] == "agent_thought":
if chunk['tool'] != '' and chunk['observation'] != '': # 工具调用结果,跳过
continue
if chunk['tool']:
msg = llm_entities.Message(
role="assistant",
tool_calls=[
llm_entities.ToolCall(
id=chunk['id'],
type="function",
function=llm_entities.FunctionCall(
name=chunk["tool"],
arguments=json.dumps({}),
),
)
],
)
yield msg
if chunk['event'] == 'message_file':
if chunk['type'] == 'image' and chunk['belongs_to'] == 'assistant':
base_url = self.dify_client.base_url
if base_url.endswith('/v1'):
base_url = base_url[:-3]
image_url = base_url + chunk['url']
yield llm_entities.Message(
role="assistant",
content=[llm_entities.ContentElement.from_image_url(image_url)],
)
if chunk['event'] == 'error':
raise errors.DifyAPIError("dify 服务错误: " + chunk['message'])
query.session.using_conversation.uuid = chunk["conversation_id"] query.session.using_conversation.uuid = chunk["conversation_id"]
@@ -303,11 +310,11 @@ class DifyServiceAPIRunner(runner.RequestRunner):
msg = llm_entities.Message( msg = llm_entities.Message(
role="assistant", role="assistant",
content=chunk["data"]["outputs"][ content=self._try_convert_thinking(chunk["data"]["outputs"][
self.ap.provider_cfg.data["dify-service-api"]["workflow"][ self.ap.provider_cfg.data["dify-service-api"]["workflow"][
"output-key" "output-key"
] ]
], ]),
) )
yield msg yield msg

View File

@@ -1,4 +1,4 @@
semantic_version = "v3.4.14" semantic_version = "v3.4.14.3"
debug_mode = False debug_mode = False

View File

@@ -34,6 +34,9 @@
], ],
"modelscope": [ "modelscope": [
"xxxxxxxx" "xxxxxxxx"
],
"ppio": [
"xxxxxxxx"
] ]
}, },
"requester": { "requester": {
@@ -103,6 +106,11 @@
"base-url": "https://api-inference.modelscope.cn/v1", "base-url": "https://api-inference.modelscope.cn/v1",
"args": {}, "args": {},
"timeout": 120 "timeout": 120
},
"ppio-chat-completions": {
"base-url": "https://api.ppinfra.com/v3/openai",
"args": {},
"timeout": 120
} }
}, },
"model": "gpt-4o", "model": "gpt-4o",