AstrBot 4.3.3__py3-none-any.whl → 4.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,8 +40,15 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
40
40
  timeout = cfg.get("timeout", 10)
41
41
 
42
42
  try:
43
+ if "transport" in cfg:
44
+ transport_type = cfg["transport"]
45
+ elif "type" in cfg:
46
+ transport_type = cfg["type"]
47
+ else:
48
+ raise Exception("MCP 连接配置缺少 transport 或 type 字段")
49
+
43
50
  async with aiohttp.ClientSession() as session:
44
- if cfg.get("transport") == "streamable_http":
51
+ if transport_type == "streamable_http":
45
52
  test_payload = {
46
53
  "jsonrpc": "2.0",
47
54
  "method": "initialize",
@@ -121,7 +128,14 @@ class MCPClient:
121
128
  if not success:
122
129
  raise Exception(error_msg)
123
130
 
124
- if cfg.get("transport") != "streamable_http":
131
+ if "transport" in cfg:
132
+ transport_type = cfg["transport"]
133
+ elif "type" in cfg:
134
+ transport_type = cfg["type"]
135
+ else:
136
+ raise Exception("MCP 连接配置缺少 transport 或 type 字段")
137
+
138
+ if transport_type != "streamable_http":
125
139
  # SSE transport method
126
140
  self._streams_context = sse_client(
127
141
  url=cfg["url"],
@@ -134,7 +148,7 @@ class MCPClient:
134
148
  )
135
149
 
136
150
  # Create a new client session
137
- read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
151
+ read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
138
152
  self.session = await self.exit_stack.enter_async_context(
139
153
  mcp.ClientSession(
140
154
  *streams,
@@ -159,7 +173,7 @@ class MCPClient:
159
173
  )
160
174
 
161
175
  # Create a new client session
162
- read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
176
+ read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
163
177
  self.session = await self.exit_stack.enter_async_context(
164
178
  mcp.ClientSession(
165
179
  read_stream=read_s,
@@ -9,3 +9,4 @@ class AstrAgentContext:
9
9
  first_provider_request: ProviderRequest
10
10
  curr_provider_request: ProviderRequest
11
11
  streaming: bool
12
+ tool_call_timeout: int = 60 # Default tool call timeout in seconds
@@ -6,7 +6,7 @@ import os
6
6
 
7
7
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
8
8
 
9
- VERSION = "4.3.3"
9
+ VERSION = "4.3.5"
10
10
  DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
11
11
 
12
12
  # 默认配置
@@ -57,6 +57,7 @@ DEFAULT_CONFIG = {
57
57
  "web_search": False,
58
58
  "websearch_provider": "default",
59
59
  "websearch_tavily_key": [],
60
+ "websearch_baidu_app_builder_key": "",
60
61
  "web_search_link": False,
61
62
  "display_reasoning_text": False,
62
63
  "identifier": False,
@@ -71,6 +72,7 @@ DEFAULT_CONFIG = {
71
72
  "show_tool_use_status": False,
72
73
  "streaming_segmented": False,
73
74
  "max_agent_step": 30,
75
+ "tool_call_timeout": 60,
74
76
  },
75
77
  "provider_stt_settings": {
76
78
  "enable": False,
@@ -207,6 +209,18 @@ CONFIG_METADATA_2 = {
207
209
  "callback_server_host": "0.0.0.0",
208
210
  "port": 6195,
209
211
  },
212
+ "企业微信智能机器人": {
213
+ "id": "wecom_ai_bot",
214
+ "type": "wecom_ai_bot",
215
+ "enable": True,
216
+ "wecomaibot_init_respond_text": "💭 思考中...",
217
+ "wecomaibot_friend_message_welcome_text": "",
218
+ "wecom_ai_bot_name": "",
219
+ "token": "",
220
+ "encoding_aes_key": "",
221
+ "callback_server_host": "0.0.0.0",
222
+ "port": 6198,
223
+ },
210
224
  "飞书(Lark)": {
211
225
  "id": "lark",
212
226
  "type": "lark",
@@ -447,10 +461,25 @@ CONFIG_METADATA_2 = {
447
461
  "type": "string",
448
462
  "hint": "aiocqhttp 适配器的反向 Websocket Token。未设置则不启用 Token 验证。",
449
463
  },
464
+ "wecom_ai_bot_name": {
465
+ "description": "企业微信智能机器人的名字",
466
+ "type": "string",
467
+ "hint": "请务必填写正确,否则无法使用一些指令。",
468
+ },
469
+ "wecomaibot_init_respond_text": {
470
+ "description": "企业微信智能机器人初始响应文本",
471
+ "type": "string",
472
+ "hint": "当机器人收到消息时,首先回复的文本内容。留空则使用默认值。",
473
+ },
474
+ "wecomaibot_friend_message_welcome_text": {
475
+ "description": "企业微信智能机器人私聊欢迎语",
476
+ "type": "string",
477
+ "hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。",
478
+ },
450
479
  "lark_bot_name": {
451
480
  "description": "飞书机器人的名字",
452
481
  "type": "string",
453
- "hint": "请务必填对,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。",
482
+ "hint": "请务必填写正确,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。",
454
483
  },
455
484
  "discord_token": {
456
485
  "description": "Discord Bot Token",
@@ -1845,6 +1874,10 @@ CONFIG_METADATA_2 = {
1845
1874
  "description": "工具调用轮数上限",
1846
1875
  "type": "int",
1847
1876
  },
1877
+ "tool_call_timeout": {
1878
+ "description": "工具调用超时时间(秒)",
1879
+ "type": "int",
1880
+ },
1848
1881
  },
1849
1882
  },
1850
1883
  "provider_stt_settings": {
@@ -2063,7 +2096,7 @@ CONFIG_METADATA_3 = {
2063
2096
  "provider_settings.websearch_provider": {
2064
2097
  "description": "网页搜索提供商",
2065
2098
  "type": "string",
2066
- "options": ["default", "tavily"],
2099
+ "options": ["default", "tavily", "baidu_ai_search"],
2067
2100
  },
2068
2101
  "provider_settings.websearch_tavily_key": {
2069
2102
  "description": "Tavily API Key",
@@ -2074,6 +2107,14 @@ CONFIG_METADATA_3 = {
2074
2107
  "provider_settings.websearch_provider": "tavily",
2075
2108
  },
2076
2109
  },
2110
+ "provider_settings.websearch_baidu_app_builder_key": {
2111
+ "description": "百度千帆智能云 APP Builder API Key",
2112
+ "type": "string",
2113
+ "hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list",
2114
+ "condition": {
2115
+ "provider_settings.websearch_provider": "baidu_ai_search",
2116
+ },
2117
+ },
2077
2118
  "provider_settings.web_search_link": {
2078
2119
  "description": "显示来源引用",
2079
2120
  "type": "bool",
@@ -2109,6 +2150,10 @@ CONFIG_METADATA_3 = {
2109
2150
  "description": "工具调用轮数上限",
2110
2151
  "type": "int",
2111
2152
  },
2153
+ "provider_settings.tool_call_timeout": {
2154
+ "description": "工具调用超时时间(秒)",
2155
+ "type": "int",
2156
+ },
2112
2157
  "provider_settings.streaming_response": {
2113
2158
  "description": "流式回复",
2114
2159
  "type": "bool",
@@ -6,6 +6,7 @@ import asyncio
6
6
  import copy
7
7
  import json
8
8
  import traceback
9
+ from datetime import timedelta
9
10
  from typing import AsyncGenerator, Union
10
11
  from astrbot.core.conversation_mgr import Conversation
11
12
  from astrbot.core import logger
@@ -185,21 +186,33 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
185
186
  handler=awaitable,
186
187
  **tool_args,
187
188
  )
188
- async for resp in wrapper:
189
- if resp is not None:
190
- if isinstance(resp, mcp.types.CallToolResult):
191
- yield resp
189
+ # async for resp in wrapper:
190
+ while True:
191
+ try:
192
+ resp = await asyncio.wait_for(
193
+ anext(wrapper),
194
+ timeout=run_context.context.tool_call_timeout,
195
+ )
196
+ if resp is not None:
197
+ if isinstance(resp, mcp.types.CallToolResult):
198
+ yield resp
199
+ else:
200
+ text_content = mcp.types.TextContent(
201
+ type="text",
202
+ text=str(resp),
203
+ )
204
+ yield mcp.types.CallToolResult(content=[text_content])
192
205
  else:
193
- text_content = mcp.types.TextContent(
194
- type="text",
195
- text=str(resp),
196
- )
197
- yield mcp.types.CallToolResult(content=[text_content])
198
- else:
199
- # NOTE: Tool 在这里直接请求发送消息给用户
200
- # TODO: 是否需要判断 event.get_result() 是否为空?
201
- # 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容"
202
- yield None
206
+ # NOTE: Tool 在这里直接请求发送消息给用户
207
+ # TODO: 是否需要判断 event.get_result() 是否为空?
208
+ # 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容"
209
+ yield None
210
+ except asyncio.TimeoutError:
211
+ raise Exception(
212
+ f"tool {tool.name} execution timeout after {run_context.context.tool_call_timeout} seconds."
213
+ )
214
+ except StopAsyncIteration:
215
+ break
203
216
 
204
217
  @classmethod
205
218
  async def _execute_mcp(
@@ -217,6 +230,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
217
230
  res = await session.call_tool(
218
231
  name=tool.name,
219
232
  arguments=tool_args,
233
+ read_timeout_seconds=timedelta(
234
+ seconds=run_context.context.tool_call_timeout
235
+ ),
220
236
  )
221
237
  if not res:
222
238
  return
@@ -307,6 +323,7 @@ class LLMRequestSubStage(Stage):
307
323
  )
308
324
  self.streaming_response: bool = settings["streaming_response"]
309
325
  self.max_step: int = settings.get("max_agent_step", 30)
326
+ self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
310
327
  if isinstance(self.max_step, bool): # workaround: #2622
311
328
  self.max_step = 30
312
329
  self.show_tool_use: bool = settings.get("show_tool_use_status", True)
@@ -473,6 +490,7 @@ class LLMRequestSubStage(Stage):
473
490
  first_provider_request=req,
474
491
  curr_provider_request=req,
475
492
  streaming=self.streaming_response,
493
+ tool_call_timeout=self.tool_call_timeout,
476
494
  )
477
495
  await agent_runner.reset(
478
496
  provider=provider,
@@ -74,7 +74,7 @@ class PipelineScheduler:
74
74
  await self._process_stages(event)
75
75
 
76
76
  # 如果没有发送操作, 则发送一个空消息, 以便于后续的处理
77
- if event.get_platform_name() == "webchat":
77
+ if event.get_platform_name() in ["webchat", "wecom_ai_bot"]:
78
78
  await event.send(None)
79
79
 
80
80
  logger.debug("pipeline 执行完毕。")
@@ -82,6 +82,10 @@ class PlatformManager:
82
82
  from .sources.wecom.wecom_adapter import (
83
83
  WecomPlatformAdapter, # noqa: F401
84
84
  )
85
+ case "wecom_ai_bot":
86
+ from .sources.wecom_ai_bot.wecomai_adapter import (
87
+ WecomAIBotAdapter, # noqa: F401
88
+ )
85
89
  case "weixin_official_account":
86
90
  from .sources.weixin_official_account.weixin_offacc_adapter import (
87
91
  WeixinOfficialAccountPlatformAdapter, # noqa: F401
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
2
2
  from astrbot.api import logger
3
3
  from astrbot.api.event import AstrMessageEvent, MessageChain
4
4
  from astrbot.api.platform import AstrBotMessage, PlatformMetadata
5
- from astrbot.api.message_components import Plain, Image, At, File, Record
5
+ from astrbot.api.message_components import Plain, Image, At, File, Record, Video, Reply
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from .satori_adapter import SatoriPlatformAdapter
@@ -87,6 +87,17 @@ class SatoriPlatformEvent(AstrMessageEvent):
87
87
  except Exception as e:
88
88
  logger.error(f"语音转换为base64失败: {e}")
89
89
 
90
+ elif isinstance(component, Reply):
91
+ content_parts.append(f'<reply id="{component.id}"/>')
92
+
93
+ elif isinstance(component, Video):
94
+ try:
95
+ video_path_url = await component.convert_to_file_path()
96
+ if video_path_url:
97
+ content_parts.append(f'<video src="{video_path_url}"/>')
98
+ except Exception as e:
99
+ logger.error(f"视频文件转换失败: {e}")
100
+
90
101
  content = "".join(content_parts)
91
102
  channel_id = session_id
92
103
  data = {"channel_id": channel_id, "content": content}
@@ -166,6 +177,17 @@ class SatoriPlatformEvent(AstrMessageEvent):
166
177
  except Exception as e:
167
178
  logger.error(f"语音转换为base64失败: {e}")
168
179
 
180
+ elif isinstance(component, Reply):
181
+ content_parts.append(f'<reply id="{component.id}"/>')
182
+
183
+ elif isinstance(component, Video):
184
+ try:
185
+ video_path_url = await component.convert_to_file_path()
186
+ if video_path_url:
187
+ content_parts.append(f'<video src="{video_path_url}"/>')
188
+ except Exception as e:
189
+ logger.error(f"视频文件转换失败: {e}")
190
+
169
191
  content = "".join(content_parts)
170
192
  channel_id = self.session_id
171
193
  data = {"channel_id": channel_id, "content": content}
@@ -91,7 +91,6 @@ class WebChatAdapter(Platform):
91
91
 
92
92
  abm = AstrBotMessage()
93
93
  abm.self_id = "webchat"
94
- abm.tag = "webchat"
95
94
  abm.sender = MessageMember(username, username)
96
95
 
97
96
  abm.type = MessageType.FRIEND_MESSAGE
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env python
2
+ # -*- encoding:utf-8 -*-
3
+
4
+ """对企业微信发送给企业后台的消息加解密示例代码.
5
+ @copyright: Copyright (c) 1998-2020 Tencent Inc.
6
+
7
+ """
8
+ # ------------------------------------------------------------------------
9
+
10
+ import logging
11
+ import base64
12
+ import random
13
+ import hashlib
14
+ import time
15
+ import struct
16
+ from Crypto.Cipher import AES
17
+ import socket
18
+ import json
19
+
20
+ from . import ierror
21
+
22
+ """
23
+ 关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
24
+ 请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
25
+ 下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
26
+ """
27
+
28
+
29
+ class FormatException(Exception):
30
+ pass
31
+
32
+
33
+ def throw_exception(message, exception_class=FormatException):
34
+ """my define raise exception function"""
35
+ raise exception_class(message)
36
+
37
+
38
+ class SHA1:
39
+ """计算企业微信的消息签名接口"""
40
+
41
+ def getSHA1(self, token, timestamp, nonce, encrypt):
42
+ """用SHA1算法生成安全签名
43
+ @param token: 票据
44
+ @param timestamp: 时间戳
45
+ @param encrypt: 密文
46
+ @param nonce: 随机字符串
47
+ @return: 安全签名
48
+ """
49
+ try:
50
+ # 确保所有输入都是字符串类型
51
+ if isinstance(encrypt, bytes):
52
+ encrypt = encrypt.decode("utf-8")
53
+
54
+ sortlist = [str(token), str(timestamp), str(nonce), str(encrypt)]
55
+ sortlist.sort()
56
+ sha = hashlib.sha1()
57
+ sha.update("".join(sortlist).encode("utf-8"))
58
+ return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
59
+
60
+ except Exception as e:
61
+ print(e)
62
+ return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
63
+
64
+
65
+ class JsonParse:
66
+ """提供提取消息格式中的密文及生成回复消息格式的接口"""
67
+
68
+ # json消息模板
69
+ AES_TEXT_RESPONSE_TEMPLATE = """{
70
+ "encrypt": "%(msg_encrypt)s",
71
+ "msgsignature": "%(msg_signaturet)s",
72
+ "timestamp": "%(timestamp)s",
73
+ "nonce": "%(nonce)s"
74
+ }"""
75
+
76
+ def extract(self, jsontext):
77
+ """提取出json数据包中的加密消息
78
+ @param jsontext: 待提取的json字符串
79
+ @return: 提取出的加密消息字符串
80
+ """
81
+ try:
82
+ json_dict = json.loads(jsontext)
83
+ return ierror.WXBizMsgCrypt_OK, json_dict["encrypt"]
84
+ except Exception as e:
85
+ print(e)
86
+ return ierror.WXBizMsgCrypt_ParseJson_Error, None
87
+
88
+ def generate(self, encrypt, signature, timestamp, nonce):
89
+ """生成json消息
90
+ @param encrypt: 加密后的消息密文
91
+ @param signature: 安全签名
92
+ @param timestamp: 时间戳
93
+ @param nonce: 随机字符串
94
+ @return: 生成的json字符串
95
+ """
96
+ resp_dict = {
97
+ "msg_encrypt": encrypt,
98
+ "msg_signaturet": signature,
99
+ "timestamp": timestamp,
100
+ "nonce": nonce,
101
+ }
102
+ resp_json = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
103
+ return resp_json
104
+
105
+
106
+ class PKCS7Encoder:
107
+ """提供基于PKCS7算法的加解密接口"""
108
+
109
+ block_size = 32
110
+
111
+ def encode(self, text):
112
+ """对需要加密的明文进行填充补位
113
+ @param text: 需要进行填充补位操作的明文(bytes类型)
114
+ @return: 补齐明文字符串(bytes类型)
115
+ """
116
+ text_length = len(text)
117
+ # 计算需要填充的位数
118
+ amount_to_pad = self.block_size - (text_length % self.block_size)
119
+ if amount_to_pad == 0:
120
+ amount_to_pad = self.block_size
121
+ # 获得补位所用的字符
122
+ pad = bytes([amount_to_pad])
123
+ # 确保text是bytes类型
124
+ if isinstance(text, str):
125
+ text = text.encode("utf-8")
126
+ return text + pad * amount_to_pad
127
+
128
+ def decode(self, decrypted):
129
+ """删除解密后明文的补位字符
130
+ @param decrypted: 解密后的明文
131
+ @return: 删除补位字符后的明文
132
+ """
133
+ pad = ord(decrypted[-1])
134
+ if pad < 1 or pad > 32:
135
+ pad = 0
136
+ return decrypted[:-pad]
137
+
138
+
139
+ class Prpcrypt(object):
140
+ """提供接收和推送给企业微信消息的加解密接口"""
141
+
142
+ def __init__(self, key):
143
+ # self.key = base64.b64decode(key+"=")
144
+ self.key = key
145
+ # 设置加解密模式为AES的CBC模式
146
+ self.mode = AES.MODE_CBC
147
+
148
+ def encrypt(self, text, receiveid):
149
+ """对明文进行加密
150
+ @param text: 需要加密的明文
151
+ @return: 加密得到的字符串
152
+ """
153
+ # 16位随机字符串添加到明文开头
154
+ text = text.encode()
155
+ text = (
156
+ self.get_random_str()
157
+ + struct.pack("I", socket.htonl(len(text)))
158
+ + text
159
+ + receiveid.encode()
160
+ )
161
+
162
+ # 使用自定义的填充方式对明文进行补位填充
163
+ pkcs7 = PKCS7Encoder()
164
+ text = pkcs7.encode(text)
165
+ # 加密
166
+ cryptor = AES.new(self.key, self.mode, self.key[:16]) # type: ignore
167
+ try:
168
+ ciphertext = cryptor.encrypt(text)
169
+ # 使用BASE64对加密后的字符串进行编码
170
+ return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
171
+ except Exception as e:
172
+ logger = logging.getLogger("astrbot")
173
+ logger.error(e)
174
+ return ierror.WXBizMsgCrypt_EncryptAES_Error, None
175
+
176
+ def decrypt(self, text, receiveid):
177
+ """对解密后的明文进行补位删除
178
+ @param text: 密文
179
+ @return: 删除填充补位后的明文
180
+ """
181
+ try:
182
+ cryptor = AES.new(self.key, self.mode, self.key[:16]) # type: ignore
183
+ # 使用BASE64对密文进行解码,然后AES-CBC解密
184
+ plain_text = cryptor.decrypt(base64.b64decode(text))
185
+ except Exception as e:
186
+ print(e)
187
+ return ierror.WXBizMsgCrypt_DecryptAES_Error, None
188
+ try:
189
+ pad = plain_text[-1]
190
+ # 去掉补位字符串
191
+ # pkcs7 = PKCS7Encoder()
192
+ # plain_text = pkcs7.encode(plain_text)
193
+ # 去除16位随机字符串
194
+ content = plain_text[16:-pad]
195
+ json_len = socket.ntohl(struct.unpack("I", content[:4])[0])
196
+ json_content = content[4 : json_len + 4].decode("utf-8")
197
+ from_receiveid = content[json_len + 4 :].decode("utf-8")
198
+ except Exception as e:
199
+ print(e)
200
+ return ierror.WXBizMsgCrypt_IllegalBuffer, None
201
+ if from_receiveid != receiveid:
202
+ print("receiveid not match", receiveid, from_receiveid)
203
+ return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
204
+ return 0, json_content
205
+
206
+ def get_random_str(self):
207
+ """随机生成16位字符串
208
+ @return: 16位字符串
209
+ """
210
+ return str(random.randint(1000000000000000, 9999999999999999)).encode()
211
+
212
+
213
+ class WXBizJsonMsgCrypt(object):
214
+ # 构造函数
215
+ def __init__(self, sToken, sEncodingAESKey, sReceiveId):
216
+ try:
217
+ self.key = base64.b64decode(sEncodingAESKey + "=")
218
+ assert len(self.key) == 32
219
+ except Exception as e:
220
+ throw_exception(f"[error]: EncodingAESKey invalid: {e}", FormatException)
221
+ # return ierror.WXBizMsgCrypt_IllegalAesKey,None
222
+ self.m_sToken = sToken
223
+ self.m_sReceiveId = sReceiveId
224
+
225
+ # 验证URL
226
+ # @param sMsgSignature: 签名串,对应URL参数的msg_signature
227
+ # @param sTimeStamp: 时间戳,对应URL参数的timestamp
228
+ # @param sNonce: 随机串,对应URL参数的nonce
229
+ # @param sEchoStr: 随机串,对应URL参数的echostr
230
+ # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
231
+ # @return:成功0,失败返回对应的错误码
232
+
233
+ def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
234
+ sha1 = SHA1()
235
+ ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
236
+ if ret != 0:
237
+ return ret, None
238
+ if not signature == sMsgSignature:
239
+ return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
240
+ pc = Prpcrypt(self.key)
241
+ ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId)
242
+ return ret, sReplyEchoStr
243
+
244
+ def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
245
+ # 将企业回复用户的消息加密打包
246
+ # @param sReplyMsg: 企业号待回复用户的消息,json格式的字符串
247
+ # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
248
+ # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
249
+ # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串,
250
+ # return:成功0,sEncryptMsg,失败返回对应的错误码None
251
+ pc = Prpcrypt(self.key)
252
+ ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId)
253
+ encrypt = encrypt.decode("utf-8") # type: ignore
254
+ if ret != 0:
255
+ return ret, None
256
+ if timestamp is None:
257
+ timestamp = str(int(time.time()))
258
+ # 生成安全签名
259
+ sha1 = SHA1()
260
+ ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
261
+ if ret != 0:
262
+ return ret, None
263
+ jsonParse = JsonParse()
264
+ return ret, jsonParse.generate(encrypt, signature, timestamp, sNonce)
265
+
266
+ def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
267
+ # 检验消息的真实性,并且获取解密后的明文
268
+ # @param sMsgSignature: 签名串,对应URL参数的msg_signature
269
+ # @param sTimeStamp: 时间戳,对应URL参数的timestamp
270
+ # @param sNonce: 随机串,对应URL参数的nonce
271
+ # @param sPostData: 密文,对应POST请求的数据
272
+ # json_content: 解密后的原文,当return返回0时有效
273
+ # @return: 成功0,失败返回对应的错误码
274
+ # 验证安全签名
275
+ jsonParse = JsonParse()
276
+ ret, encrypt = jsonParse.extract(sPostData)
277
+ if ret != 0:
278
+ return ret, None
279
+ sha1 = SHA1()
280
+ ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
281
+ if ret != 0:
282
+ return ret, None
283
+ if not signature == sMsgSignature:
284
+ print("signature not match")
285
+ print(signature)
286
+ return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
287
+ pc = Prpcrypt(self.key)
288
+ ret, json_content = pc.decrypt(encrypt, self.m_sReceiveId)
289
+ return ret, json_content
@@ -0,0 +1,17 @@
1
+ """
2
+ 企业微信智能机器人平台适配器包
3
+ """
4
+
5
+ from .wecomai_adapter import WecomAIBotAdapter
6
+ from .wecomai_api import WecomAIBotAPIClient
7
+ from .wecomai_event import WecomAIBotMessageEvent
8
+ from .wecomai_server import WecomAIBotServer
9
+ from .wecomai_utils import WecomAIBotConstants
10
+
11
+ __all__ = [
12
+ "WecomAIBotAdapter",
13
+ "WecomAIBotAPIClient",
14
+ "WecomAIBotMessageEvent",
15
+ "WecomAIBotServer",
16
+ "WecomAIBotConstants",
17
+ ]
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #########################################################################
4
+ # Author: jonyqin
5
+ # Created Time: Thu 11 Sep 2014 01:53:58 PM CST
6
+ # File Name: ierror.py
7
+ # Description:定义错误码含义
8
+ #########################################################################
9
+ WXBizMsgCrypt_OK = 0
10
+ WXBizMsgCrypt_ValidateSignature_Error = -40001
11
+ WXBizMsgCrypt_ParseJson_Error = -40002
12
+ WXBizMsgCrypt_ComputeSignature_Error = -40003
13
+ WXBizMsgCrypt_IllegalAesKey = -40004
14
+ WXBizMsgCrypt_ValidateCorpid_Error = -40005
15
+ WXBizMsgCrypt_EncryptAES_Error = -40006
16
+ WXBizMsgCrypt_DecryptAES_Error = -40007
17
+ WXBizMsgCrypt_IllegalBuffer = -40008
18
+ WXBizMsgCrypt_EncodeBase64_Error = -40009
19
+ WXBizMsgCrypt_DecodeBase64_Error = -40010
20
+ WXBizMsgCrypt_GenReturnJson_Error = -40011