From d7687913a9b734d4fb83b9c866b774fd2b82f823 Mon Sep 17 00:00:00 2001 From: "Junyan Qin (Chin)" Date: Mon, 10 Feb 2025 11:04:57 +0800 Subject: [PATCH 01/12] doc(README.md): update trendingshift badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc78032c..2921bf23 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
-RockChinQ%2FQChatGPT | Trendshift +RockChinQ%2FLangBot | Trendshift 项目主页功能介绍 | From 05c1fdaa9eb1f0f9d62c53285d75555385839d06 Mon Sep 17 00:00:00 2001 From: wangcham Date: Mon, 10 Feb 2025 06:08:59 -0500 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20add=20adapter=20for=20=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=85=AC=E4=BC=97=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/official_account_api/__init__.py | 0 libs/official_account_api/api.py | 175 ++++++++++++++++++++++++ libs/official_account_api/oaevent.py | 167 ++++++++++++++++++++++ libs/wecom_api/api.py | 3 +- pkg/platform/manager.py | 2 +- pkg/platform/sources/officialaccount.py | 155 +++++++++++++++++++++ pkg/platform/sources/qqofficial.py | 1 - pkg/platform/sources/wecom.py | 1 - 8 files changed, 500 insertions(+), 4 deletions(-) create mode 100644 libs/official_account_api/__init__.py create mode 100644 libs/official_account_api/api.py create mode 100644 libs/official_account_api/oaevent.py create mode 100644 pkg/platform/sources/officialaccount.py diff --git a/libs/official_account_api/__init__.py b/libs/official_account_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/official_account_api/api.py b/libs/official_account_api/api.py new file mode 100644 index 00000000..92ba47e5 --- /dev/null +++ b/libs/official_account_api/api.py @@ -0,0 +1,175 @@ +# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件 +import time +import traceback +from ..wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt +import xml.etree.ElementTree as ET +from quart import Quart,request +import hashlib +from typing import Callable, Dict, Any +from .oaevent import OAEvent +import httpx + +import asyncio +import time +import xml.etree.ElementTree as ET + + + +xml_template = """ + + + + {create_time} + + + +""" + +class OAClient(): + + def __init__(self,token:str,EncodingAESKey:str,AppID:str,Appsecret:str): + self.token = token + self.aes = EncodingAESKey + self.appid = AppID + self.appsecret = Appsecret + self.base_url = 'https://api.weixin.qq.com' + self.access_token = '' + self.app = Quart(__name__) + self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']) + self._message_handlers = { + "example":[], + } + self.access_token_expiry_time = None + self.msg_id_map = {} + + async def handle_callback_request(self): + + try: + # 每隔100毫秒查询是否生成ai回答 + start_time = time.time() + signature = request.args.get("signature", "") + timestamp = request.args.get("timestamp", "") + nonce = request.args.get("nonce", "") + echostr = request.args.get("echostr", "") + msg_signature = request.args.get("msg_signature","") + if msg_signature is None: + raise Exception("msg_signature不在请求体中") + + if request.method == 'GET': + # 校验签名 + check_str = "".join(sorted([self.token, timestamp, nonce])) + check_signature = hashlib.sha1(check_str.encode("utf-8")).hexdigest() + + if check_signature == signature: + return echostr # 验证成功返回echostr + else: + raise Exception("拒绝请求") + elif request.method == "POST": + encryt_msg = await request.data + wxcpt = WXBizMsgCrypt(self.token,self.aes,self.appid) + ret,xml_msg = wxcpt.DecryptMsg(encryt_msg,msg_signature,timestamp,nonce) + xml_msg = xml_msg.decode('utf-8') + + if ret != 0: + raise Exception("消息解密失败") + + message_data = await self.get_message(xml_msg) + if message_data : + event = OAEvent.from_payload(message_data) + if event: + await self._handle_message(event) + + root = ET.fromstring(xml_msg) + from_user = root.find("FromUserName").text # 发送者 + to_user = root.find("ToUserName").text # 机器人 + + from pkg.platform.sources import officialaccount + + timeout = 4.80 + interval = 0.1 + while True: + content = officialaccount.generated_content.pop(message_data["MsgId"], None) + if content: + response_xml = xml_template.format( + to_user=from_user, + from_user=to_user, + create_time=int(time.time()), + content = content + ) + + return response_xml + + if time.time() - start_time >= timeout: + break + + await asyncio.sleep(interval) + + if self.msg_id_map.get(message_data["MsgId"], 1) == 3: + + response_xml = xml_template.format( + to_user=from_user, + from_user=to_user, + create_time=int(time.time()), + content = "请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。" + ) + return response_xml + + except Exception as e: + traceback.print_exc() + + + async def get_message(self, xml_msg: str): + + root = ET.fromstring(xml_msg) + + message_data = { + "ToUserName": root.find("ToUserName").text, + "FromUserName": root.find("FromUserName").text, + "CreateTime": int(root.find("CreateTime").text), + "MsgType": root.find("MsgType").text, + "Content": root.find("Content").text if root.find("Content") is not None else None, + "MsgId": int(root.find("MsgId").text) if root.find("MsgId") is not None else None, + } + + return message_data + + + async def run_task(self, host: str, port: int, *args, **kwargs): + """ + 启动 Quart 应用。 + """ + await self.app.run_task(host=host, port=port, *args, **kwargs) + + + def on_message(self, msg_type: str): + """ + 注册消息类型处理器。 + """ + def decorator(func: Callable[[OAEvent], None]): + if msg_type not in self._message_handlers: + self._message_handlers[msg_type] = [] + self._message_handlers[msg_type].append(func) + return func + return decorator + + async def _handle_message(self, event: OAEvent): + """ + 处理消息事件。 + """ + message_id = event.message_id + if message_id in self.msg_id_map.keys(): + self.msg_id_map[message_id] += 1 + return + + self.msg_id_map[message_id] = 1 + msg_type = event.type + if msg_type in self._message_handlers: + for handler in self._message_handlers[msg_type]: + await handler(event) + + + + + + + diff --git a/libs/official_account_api/oaevent.py b/libs/official_account_api/oaevent.py new file mode 100644 index 00000000..ebbccd7e --- /dev/null +++ b/libs/official_account_api/oaevent.py @@ -0,0 +1,167 @@ +from typing import Dict, Any, Optional + + +class OAEvent(dict): + """ + 封装从微信公众号收到的事件数据对象(字典),提供属性以获取其中的字段。 + + 除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性是否存在(若不存在则返回 `None`)依事件类型不同而不同。 + """ + + @staticmethod + def from_payload(payload: Dict[str, Any]) -> Optional["OAEvent"]: + """ + 从微信公众号事件数据构造 `WecomEvent` 对象。 + + Args: + payload (Dict[str, Any]): 解密后的微信事件数据。 + + Returns: + Optional[OAEvent]: 如果事件数据合法,则返回 OAEvent 对象;否则返回 None。 + """ + try: + event = OAEvent(payload) + _ = event.type, event.detail_type # 确保必须字段存在 + return event + except KeyError: + return None + + @property + def type(self) -> str: + """ + 事件类型,例如 "message"、"event"、"text" 等。 + + Returns: + str: 事件类型。 + """ + return self.get("MsgType", "") + + @property + def picurl(self) -> str: + """ + 图片链接 + """ + return self.get("PicUrl","") + + @property + def detail_type(self) -> str: + """ + 事件详细类型,依 `type` 的不同而不同。例如: + - 消息事件: "text", "image", "voice", 等 + - 事件通知: "subscribe", "unsubscribe", "click", 等 + + Returns: + str: 事件详细类型。 + """ + if self.type == "event": + return self.get("Event", "") + return self.type + + @property + def name(self) -> str: + """ + 事件名,对于消息事件是 `type.detail_type`,对于其他事件是 `event_type`。 + + Returns: + str: 事件名。 + """ + return f"{self.type}.{self.detail_type}" + + @property + def user_id(self) -> Optional[str]: + """ + 发送方账号 + """ + return self.get("FromUserName") + + + @property + def receiver_id(self) -> Optional[str]: + """ + 接收者 ID,例如机器人自身的公众号微信 ID。 + + Returns: + Optional[str]: 接收者 ID。 + """ + return self.get("ToUserName") + + @property + def message_id(self) -> Optional[str]: + """ + 消息 ID,仅在消息类型事件中存在。 + + Returns: + Optional[str]: 消息 ID。 + """ + return self.get("MsgId") + + @property + def message(self) -> Optional[str]: + """ + 消息内容,仅在消息类型事件中存在。 + + Returns: + Optional[str]: 消息内容。 + """ + return self.get("Content") + + @property + def media_id(self) -> Optional[str]: + """ + 媒体文件 ID,仅在图片、语音等消息类型中存在。 + + Returns: + Optional[str]: 媒体文件 ID。 + """ + return self.get("MediaId") + + @property + def timestamp(self) -> Optional[int]: + """ + 事件发生的时间戳。 + + Returns: + Optional[int]: 时间戳。 + """ + return self.get("CreateTime") + + @property + def event_key(self) -> Optional[str]: + """ + 事件的 Key 值,例如点击菜单时的 `EventKey`。 + + Returns: + Optional[str]: 事件 Key。 + """ + return self.get("EventKey") + + def __getattr__(self, key: str) -> Optional[Any]: + """ + 允许通过属性访问数据中的任意字段。 + + Args: + key (str): 字段名。 + + Returns: + Optional[Any]: 字段值。 + """ + return self.get(key) + + def __setattr__(self, key: str, value: Any) -> None: + """ + 允许通过属性设置数据中的任意字段。 + + Args: + key (str): 字段名。 + value (Any): 字段值。 + """ + self[key] = value + + def __repr__(self) -> str: + """ + 生成事件对象的字符串表示。 + + Returns: + str: 字符串表示。 + """ + return f"" diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index e3376dd0..69f92e08 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -171,6 +171,7 @@ class WecomClient(): elif request.method == "POST": encrypt_msg = await request.data ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) + # print(xml_msg) if ret != 0: raise Exception(f"消息解密失败,错误码: {ret}") @@ -228,7 +229,7 @@ class WecomClient(): if message_data["MsgType"] == "image": message_data["MediaId"] = root.find("MediaId").text if root.find("MediaId") is not None else None message_data["PicUrl"] = root.find("PicUrl").text if root.find("PicUrl") is not None else None - + return message_data @staticmethod diff --git a/pkg/platform/manager.py b/pkg/platform/manager.py index 85302ca4..c70417d2 100644 --- a/pkg/platform/manager.py +++ b/pkg/platform/manager.py @@ -39,7 +39,7 @@ class PlatformManager: async def initialize(self): - from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat + from .sources import nakuru, aiocqhttp, qqbotpy, qqofficial, wecom, lark, discord, gewechat, officialaccount async def on_friend_message(event: platform_events.FriendMessage, adapter: msadapter.MessageSourceAdapter): diff --git a/pkg/platform/sources/officialaccount.py b/pkg/platform/sources/officialaccount.py new file mode 100644 index 00000000..856388f2 --- /dev/null +++ b/pkg/platform/sources/officialaccount.py @@ -0,0 +1,155 @@ +from __future__ import annotations +import typing +import asyncio +import traceback +import time +import datetime +from pkg.core import app +from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.types import events as platform_events, message as platform_message + +import aiocqhttp +import aiohttp +from libs.official_account_api.oaevent import OAEvent +from pkg.platform.adapter import MessageSourceAdapter +from pkg.platform.types import events as platform_events, message as platform_message +from libs.official_account_api.api import OAClient +from pkg.core import app +from .. import adapter +from ...pipeline.longtext.strategies import forward +from ...core import app +from ..types import message as platform_message +from ..types import events as platform_events +from ..types import entities as platform_entities +from ...command.errors import ParamNotEnoughError + + +# 生成的ai回答 +generated_content = {} + +class OAMessageConverter(adapter.MessageConverter): + @staticmethod + async def yiri2target(message_chain: platform_message.MessageChain): + for msg in message_chain: + if type(msg) is platform_message.Plain: + return msg.text + + + @staticmethod + async def target2yiri(message:str,message_id =-1): + yiri_msg_list = [] + yiri_msg_list.append( + platform_message.Source(id=message_id, time=datetime.datetime.now()) + ) + + yiri_msg_list.append(platform_message.Plain(text=message)) + chain = platform_message.MessageChain(yiri_msg_list) + + return chain + + +class OAEventConverter(adapter.EventConverter): + @staticmethod + async def target2yiri(event:OAEvent): + if event.type == "text": + yiri_chain = await OAMessageConverter.target2yiri( + event.message, event.message_id + ) + + friend = platform_entities.Friend( + id=event.user_id, + nickname=str(event.user_id), + remark="", + ) + + return platform_events.FriendMessage( + sender=friend, message_chain=yiri_chain, time=event.timestamp, source_platform_object=event + ) + else: + return None + +@adapter.adapter_class("officialaccount") +class OfficialAccountAdapter(adapter.MessageSourceAdapter): + + bot : OAClient + ap : app.Application + bot_account_id: str + message_converter: OAMessageConverter = OAMessageConverter() + event_converter: OAEventConverter = OAEventConverter() + config: dict + + + def __init__(self, config: dict, ap: app.Application): + self.config = config + + self.ap = ap + + required_keys = [ + "token", + "EncodingAESKey", + "AppSecret", + "AppID", + ] + missing_keys = [key for key in required_keys if key not in config] + if missing_keys: + raise ParamNotEnoughError("企业微信缺少相关配置项,请查看文档或联系管理员") + + self.bot = OAClient( + token=config['token'], + EncodingAESKey=config['EncodingAESKey'], + Appsecret=config['AppSecret'], + AppID=config['AppID'], + ) + + async def reply_message(self, message_source: platform_events.FriendMessage, message: platform_message.MessageChain, quote_origin: bool = False): + global generated_content + + content = await OAMessageConverter.yiri2target( + message + ) + + generated_content[message_source.message_chain.message_id] = content + + async def send_message( + self, target_type: str, target_id: str, message: platform_message.MessageChain + ): + pass + + + def register_listener(self, event_type: type, callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None]): + async def on_message(event: OAEvent): + self.bot_account_id = event.receiver_id + try: + return await callback( + await self.event_converter.target2yiri(event), self + ) + except: + traceback.print_exc() + + if event_type == platform_events.FriendMessage: + self.bot.on_message("text")(on_message) + elif event_type == platform_events.GroupMessage: + pass + + async def run_async(self): + async def shutdown_trigger_placeholder(): + while True: + await asyncio.sleep(1) + + await self.bot.run_task( + host=self.config["host"], + port=self.config["port"], + shutdown_trigger=shutdown_trigger_placeholder, + ) + + async def kill(self) -> bool: + return False + + async def unregister_listener( + self, + event_type: type, + callback: typing.Callable[[platform_events.Event, MessageSourceAdapter], None], + ): + return super().unregister_listener(event_type, callback) + + \ No newline at end of file diff --git a/pkg/platform/sources/qqofficial.py b/pkg/platform/sources/qqofficial.py index f41e84db..924e7ba0 100644 --- a/pkg/platform/sources/qqofficial.py +++ b/pkg/platform/sources/qqofficial.py @@ -47,7 +47,6 @@ class QQOfficialMessageConverter(adapter.MessageConverter): yiri_msg_list.append( platform_message.Image(base64=base64_url) ) - message = '' yiri_msg_list.append(platform_message.Plain(text=message)) chain = platform_message.MessageChain(yiri_msg_list) return chain diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 38de84e9..64b08abe 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -154,7 +154,6 @@ class WecomeAdapter(adapter.MessageSourceAdapter): message_converter: WecomMessageConverter = WecomMessageConverter() event_converter: WecomEventConverter = WecomEventConverter() config: dict - ap: app.Application def __init__(self, config: dict, ap: app.Application): self.config = config From 7ba655902bbee15da6c098417c86a63af3907097 Mon Sep 17 00:00:00 2001 From: wangcham Date: Mon, 10 Feb 2025 09:11:27 -0500 Subject: [PATCH 03/12] fix: wecom userid couldn't pass correctly --- libs/wecom_api/api.py | 1 - pkg/platform/sources/wecom.py | 9 ++++----- pkg/platform/types/entities.py | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/libs/wecom_api/api.py b/libs/wecom_api/api.py index 69f92e08..8aecca04 100644 --- a/libs/wecom_api/api.py +++ b/libs/wecom_api/api.py @@ -171,7 +171,6 @@ class WecomClient(): elif request.method == "POST": encrypt_msg = await request.data ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce) - # print(xml_msg) if ret != 0: raise Exception(f"消息解密失败,错误码: {ret}") diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index 64b08abe..f6ea64ab 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -119,9 +119,8 @@ class WecomEventConverter: yiri_chain = await WecomMessageConverter.target2yiri( event.message, event.message_id ) - friend = platform_entities.Friend( - id=event.user_id, + id=f"u{event.user_id}", nickname=str(event.agent_id), remark="", ) @@ -190,12 +189,12 @@ class WecomeAdapter(adapter.MessageSourceAdapter): message_source, self.bot_account_id, self.bot ) content_list = await WecomMessageConverter.yiri2target(message, self.bot) - + fixed_user_id = Wecom_event.user_id.lstrip('u') for content in content_list: if content["type"] == "text": - await self.bot.send_private_msg(Wecom_event.user_id, Wecom_event.agent_id, content["content"]) + await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content["content"]) elif content["type"] == "image": - await self.bot.send_image(Wecom_event.user_id, Wecom_event.agent_id, content["media_id"]) + await self.bot.send_image(fixed_user_id, Wecom_event.agent_id, content["media_id"]) async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain diff --git a/pkg/platform/types/entities.py b/pkg/platform/types/entities.py index 57d9372a..515f6b7a 100644 --- a/pkg/platform/types/entities.py +++ b/pkg/platform/types/entities.py @@ -38,6 +38,7 @@ class Friend(Entity): return self.nickname or self.remark or '' + class Permission(str, Enum): """群成员身份权限。""" Member = "MEMBER" From ac628b26d91a0cc19381474e04a53e77890c65c0 Mon Sep 17 00:00:00 2001 From: wangcham Date: Mon, 10 Feb 2025 09:16:33 -0500 Subject: [PATCH 04/12] =?UTF-8?q?feat=EF=BC=9Aadd=20support=20for=20wechat?= =?UTF-8?q?=20official=20account?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/official_account_api/api.py | 15 ++++++++------- templates/platform.json | 10 ++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/libs/official_account_api/api.py b/libs/official_account_api/api.py index 92ba47e5..0ed4d8b4 100644 --- a/libs/official_account_api/api.py +++ b/libs/official_account_api/api.py @@ -106,13 +106,14 @@ class OAClient(): if self.msg_id_map.get(message_data["MsgId"], 1) == 3: - response_xml = xml_template.format( - to_user=from_user, - from_user=to_user, - create_time=int(time.time()), - content = "请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。" - ) - return response_xml + # response_xml = xml_template.format( + # to_user=from_user, + # from_user=to_user, + # create_time=int(time.time()), + # content = "请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。" + # ) + print("请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。") + return '' except Exception as e: traceback.print_exc() diff --git a/templates/platform.json b/templates/platform.json index 8676cf99..5a1fca77 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -65,6 +65,16 @@ "callback_url": "http://your-callback-url:2286/gewechat/callback", "app_id": "", "token": "" + }, + { + "adapter":"officialaccount", + "enable": true, + "token": "", + "EncodingAESKey":"", + "AppSecret":"", + "AppID":"", + "host": "0.0.0.0", + "port": 2287 } ], "track-function-calls": true, From 6f32bf9621b1d80ffd83766d6442952ca58b7cdc Mon Sep 17 00:00:00 2001 From: wangcham Date: Mon, 10 Feb 2025 10:01:48 -0500 Subject: [PATCH 05/12] fix: wecom userid --- pkg/platform/sources/wecom.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/platform/sources/wecom.py b/pkg/platform/sources/wecom.py index f6ea64ab..5d127958 100644 --- a/pkg/platform/sources/wecom.py +++ b/pkg/platform/sources/wecom.py @@ -189,7 +189,9 @@ class WecomeAdapter(adapter.MessageSourceAdapter): message_source, self.bot_account_id, self.bot ) content_list = await WecomMessageConverter.yiri2target(message, self.bot) - fixed_user_id = Wecom_event.user_id.lstrip('u') + fixed_user_id = Wecom_event.user_id + # 删掉开头的u + fixed_user_id = fixed_user_id[1:] for content in content_list: if content["type"] == "text": await self.bot.send_private_msg(fixed_user_id, Wecom_event.agent_id, content["content"]) From b6e054a73fdec30fcb109475920b7d1d19c9805d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 00:23:38 +0800 Subject: [PATCH 06/12] chore: migrations for `officialaccount` adapter --- .../m027_wx_official_account_config.py | 32 +++++++++++++ pkg/core/stages/migrate.py | 2 +- templates/platform.json | 4 +- templates/schema/platform.json | 45 +++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 pkg/core/migrations/m027_wx_official_account_config.py diff --git a/pkg/core/migrations/m027_wx_official_account_config.py b/pkg/core/migrations/m027_wx_official_account_config.py new file mode 100644 index 00000000..510b7108 --- /dev/null +++ b/pkg/core/migrations/m027_wx_official_account_config.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from .. import migration + + +@migration.migration_class("wx-official-account-config", 27) +class WXOfficialAccountConfigMigration(migration.Migration): + """迁移""" + + async def need_migrate(self) -> bool: + """判断当前环境是否需要运行此迁移""" + + for adapter in self.ap.platform_cfg.data['platform-adapters']: + if adapter['adapter'] == 'officialaccount': + return False + + return True + + async def run(self): + """执行迁移""" + self.ap.platform_cfg.data['platform-adapters'].append({ + "adapter": "officialaccount", + "enable": False, + "token": "", + "EncodingAESKey": "", + "AppID": "", + "AppSecret": "", + "host": "0.0.0.0", + "port": 2287 + }) + + await self.ap.platform_cfg.dump_config() diff --git a/pkg/core/stages/migrate.py b/pkg/core/stages/migrate.py index 16faa53a..a1983f0b 100644 --- a/pkg/core/stages/migrate.py +++ b/pkg/core/stages/migrate.py @@ -9,7 +9,7 @@ from ..migrations import m005_deepseek_cfg_completion, m006_vision_config, m007_ from ..migrations import m010_ollama_requester_config, m011_command_prefix_config, m012_runner_config, m013_http_api_config, m014_force_delay_config from ..migrations import m015_gitee_ai_config, m016_dify_service_api, m017_dify_api_timeout_params, m018_xai_config, m019_zhipuai_config from ..migrations import m020_wecom_config, m021_lark_config, m022_lmstudio_config, m023_siliconflow_config, m024_discord_config, m025_gewechat_config -from ..migrations import m026_qqofficial_config +from ..migrations import m026_qqofficial_config, m027_wx_official_account_config @stage.stage_class("MigrationStage") class MigrationStage(stage.BootingStage): diff --git a/templates/platform.json b/templates/platform.json index 5a1fca77..58742b37 100644 --- a/templates/platform.json +++ b/templates/platform.json @@ -68,11 +68,11 @@ }, { "adapter":"officialaccount", - "enable": true, + "enable": false, "token": "", "EncodingAESKey":"", - "AppSecret":"", "AppID":"", + "AppSecret":"", "host": "0.0.0.0", "port": 2287 } diff --git a/templates/schema/platform.json b/templates/schema/platform.json index f2db0f79..9a5deeb0 100644 --- a/templates/schema/platform.json +++ b/templates/schema/platform.json @@ -331,6 +331,51 @@ "description": "gewechat 的 token" } } + }, + { + "title": "微信公众号适配器", + "description": "用于接入微信公众号", + "properties": { + "adapter": { + "type": "string", + "const": "officialaccount" + }, + "enable": { + "type": "boolean", + "default": false, + "description": "是否启用此适配器" + }, + "token": { + "type": "string", + "default": "", + "description": "微信公众号的token" + }, + "EncodingAESKey": { + "type": "string", + "default": "", + "description": "微信公众号的EncodingAESKey" + }, + "AppID": { + "type": "string", + "default": "", + "description": "微信公众号的AppID" + }, + "AppSecret": { + "type": "string", + "default": "", + "description": "微信公众号的AppSecret" + }, + "host": { + "type": "string", + "default": "0.0.0.0", + "description": "监听的IP地址" + }, + "port": { + "type": "integer", + "default": 2287, + "description": "监听的端口" + } + } } ] } From 8d00e710d55fd1a48b9d50fcc5f5949925aa5556 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 00:25:45 +0800 Subject: [PATCH 07/12] doc(README): add official account compatibility comment --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 2921bf23..faab043b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ | QQ 个人号 | ✅ | QQ 个人号私聊、群聊 | | QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 | | 企业微信 | ✅ | | +| 微信公众号 | ✅ | | | 飞书 | ✅ | | | Discord | ✅ | | | 个人微信 | ✅ | 使用 [Gewechat](https://github.com/Devo919/Gewechat) 接入 | diff --git a/README_EN.md b/README_EN.md index fd266515..59360ff6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -85,6 +85,7 @@ Directly use the released version to run, see the [Manual Deployment](https://do | Personal QQ | ✅ | | | QQ Official API | ✅ | | | WeCom | ✅ | | +| WeChat Official Account | ✅ | | | Lark | ✅ | | | Discord | ✅ | | | Personal WeChat | ✅ | Use [Gewechat](https://github.com/Devo919/Gewechat) to access | From 1b1ccdd733f8ac69a86b8722067c8ba53d4bf150 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Tue, 11 Feb 2025 03:07:31 +0900 Subject: [PATCH 08/12] docs: add Japanese README I created Japanese translated README. --- README.md | 2 +- README_EN.md | 2 +- README_JP.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 README_JP.md diff --git a/README.md b/README.md index 2921bf23..15c167e3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89) python -[简体中文](README.md) / [English](README_EN.md) +[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md)
diff --git a/README_EN.md b/README_EN.md index fd266515..02aca018 100644 --- a/README_EN.md +++ b/README_EN.md @@ -26,7 +26,7 @@ ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days)) python -[简体中文](README.md) / [English](README_EN.md) +[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) diff --git a/README_JP.md b/README_JP.md new file mode 100644 index 00000000..675f9bdd --- /dev/null +++ b/README_JP.md @@ -0,0 +1,123 @@ +

+ +LangBot + + +

+ +RockChinQ%2FQChatGPT | Trendshift + +ホーム | +機能 | +デプロイ | +FAQ | +プラグイン | +プラグインの提出 + +
+😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖 +
+ +
+ + + +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest) + ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days)) +python + +[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) + +
+ +

+ +## ✨ 機能 + +- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQチャンネル、WeCom、Lark、Discord、個人WeChatをサポートし、将来的にはWhatsApp、Telegramなどもサポート予定。 +- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。 +- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。豊富なエコシステム、現在数十の[プラグイン](https://docs.langbot.app/plugin/plugin-intro.html)が存在。 +- 😻 [新機能] Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。詳細は[ドキュメント](https://docs.langbot.app/webui/intro.html)を参照。 + +## 📦 始め方 + +> [!IMPORTANT] +> +> - どのデプロイ方法を始める前に、必ず[新規ユーザーガイド](https://docs.langbot.app/insight/guide.html)をお読みください。 +> - すべてのドキュメントは中国語で提供されています。近い将来、i18nバージョンを提供する予定です。 + +#### Docker Compose デプロイ + +Dockerに慣れているユーザーに適しています。[Dockerデプロイ](https://docs.langbot.app/deploy/langbot/docker.html)のドキュメントを参照してください。 + +#### BTPanelでのワンクリックデプロイ + +LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/deploy/langbot/one-click/bt.html)を使用して使用できます。 + +#### Zeaburクラウドデプロイ + +コミュニティが提供するZeaburテンプレート。 + +[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/zh-CN/templates/ZKTBDH) + +#### Railwayクラウドデプロイ + +[![Deploy on Railway](https://railway.com/button.svg)](https://railway.app/template/yRrAyL?referralCode=vogKPF) + +#### その他のデプロイ方法 + +リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/deploy/langbot/manual.html)のドキュメントを参照してください。 + +## 📸 デモ + +返信効果(インターネットプラグイン付き) + +- WebUIデモ: https://demo.langbot.dev/ + - ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456` + - 注意: WebUIの効果のみを示しています。公開環境では、機密情報を入力しないでください。 + +## 🔌 コンポーネントの互換性 + +### メッセージプラットフォーム + +| プラットフォーム | ステータス | 備考 | +| --- | --- | --- | +| 個人QQ | ✅ | | +| QQ公式API | ✅ | | +| WeCom | ✅ | | +| Lark | ✅ | | +| Discord | ✅ | | +| 個人WeChat | ✅ | [Gewechat](https://github.com/Devo919/Gewechat)を使用して接続 | +| Telegram | 🚧 | | +| WhatsApp | 🚧 | | +| DingTalk | 🚧 | | + +🚧: 開発中 + +### LLMs + +| LLM | ステータス | 備考 | +| --- | --- | --- | +| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 | +| [DeepSeek](https://www.deepseek.com/) | ✅ | | +| [Moonshot](https://www.moonshot.cn/) | ✅ | | +| [Anthropic](https://www.anthropic.com/) | ✅ | | +| [xAI](https://x.ai/) | ✅ | | +| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | | +| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム | +| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム | +| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム | +| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) | +| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) | +| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS) | + +## 🤝 コミュニティ貢献 + +以下の貢献者とコミュニティの皆さんの貢献に感謝します。 + + + + + + + From a6bc617a3bc66bc11f9a80bcbceec2e2a1b3f4ed Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 12:19:12 +0800 Subject: [PATCH 09/12] docs: add discord link --- README.md | 1 + README_EN.md | 2 +- README_JP.md | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 15c167e3..908701cb 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@
+[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) [![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-1030838208-blue)](https://qm.qq.com/q/PF9OuQCCcM) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest) ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89) diff --git a/README_EN.md b/README_EN.md index 02aca018..c0b53712 100644 --- a/README_EN.md +++ b/README_EN.md @@ -21,7 +21,7 @@
- +[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest) ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days)) python diff --git a/README_JP.md b/README_JP.md index 675f9bdd..216f40f7 100644 --- a/README_JP.md +++ b/README_JP.md @@ -20,8 +20,7 @@
- - +[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest) ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days)) python From ab8ef01c762774633561440ac4d688d7252d4593 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 12:27:27 +0800 Subject: [PATCH 10/12] docs: update trendshift badge link --- README_EN.md | 2 +- README_JP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README_EN.md b/README_EN.md index 9b923954..691774ef 100644 --- a/README_EN.md +++ b/README_EN.md @@ -5,7 +5,7 @@
-RockChinQ%2FQChatGPT | Trendshift +RockChinQ%2FLangBot | Trendshift HomeFeatures | diff --git a/README_JP.md b/README_JP.md index 216f40f7..d2303fba 100644 --- a/README_JP.md +++ b/README_JP.md @@ -5,7 +5,7 @@
-RockChinQ%2FQChatGPT | Trendshift +RockChinQ%2FLangBot | Trendshift ホーム機能 | From fabf93f741d4b7fedf8f5b524617b9bd453f83bf Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 12:56:13 +0800 Subject: [PATCH 11/12] chore: release v3.4.7 --- pkg/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index 61ed497b..7e582b0f 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,4 +1,4 @@ -semantic_version = "v3.4.6.2" +semantic_version = "v3.4.7" debug_mode = False From e89c6b68c957371bfb3f1d53cdb43385099b3931 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 11 Feb 2025 21:19:15 +0800 Subject: [PATCH 12/12] fix: f the stream resp --- pkg/provider/modelmgr/requesters/chatcmpl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/provider/modelmgr/requesters/chatcmpl.py b/pkg/provider/modelmgr/requesters/chatcmpl.py index 0b1c4cf2..c2edcf9a 100644 --- a/pkg/provider/modelmgr/requesters/chatcmpl.py +++ b/pkg/provider/modelmgr/requesters/chatcmpl.py @@ -61,7 +61,7 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): async for chunk in resp_gen: # print(chunk) - if not chunk: + if not chunk or not chunk.id or not chunk.choices or not chunk.choices[0] or not chunk.choices[0].delta: continue if chunk.choices[0].delta.content is not None: @@ -75,6 +75,9 @@ class OpenAIChatCompletions(requester.LLMAPIRequester): break else: tool_calls.append(tool_call) + + if chunk.choices[0].finish_reason is not None: + break real_tool_calls = []