import base64 import json import os import time import random import hashlib import ctypes from datetime import datetime import Crypto from django.core.cache import cache from url_md_handle import url_to_markdown import ctypes import json import base64 import time from ctypes import c_void_p, c_char_p, c_uint, c_ulonglong, c_int from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA import config # 导入配置 def gen_real_file_path(): """生成本地文件路径和对应URL""" auto_path = f"{datetime.now().year}/{datetime.now().month}/{datetime.now().day}" real_path = os.path.join(config.BASE_UPLOADS, auto_path) if not os.path.exists(real_path): os.makedirs(real_path) filename = time.strftime("%H%M%S", time.localtime(time.time())) + str(random.randint(100, 999)) filename = hashlib.md5(filename.encode("utf-8")).hexdigest() real_filepath = os.path.join(real_path, filename) url_filepath = f"{config.UPLOADS_URL}/{auto_path}/{filename}" return real_filepath, url_filepath def get_document_file_type(file_name): """根据文件扩展名判断类型""" file_type = "other" file_split = os.path.splitext(file_name) if len(file_split) == 2: ext = file_split[1].lower() if ext in [".txt", ".md", ".markdown"]: file_type = "txt" if ext == ".txt" else "md" elif ext in [".java", ".py", ".c", ".cpp", ".sql", ".sh", ".bat", ".js", ".css", ".ts", ".tsx", ".jsx", ".vue"]: file_type = "code" elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".gif"]: file_type = "image" elif ext in [".mp4", ".mov", ".m3u8", ".webm", ".ogv"]: file_type = "video" elif ext in [".mp3", ".wav", ".ogg", ".amr"]: file_type = "sound" return file_type def get_text_content(file_path): """读取文本内容""" try: with open(file_path, "r", encoding="utf-8") as f: return f.read() except Exception as e: print(f"--> get_text_content 异常: {e}") return None class WeWorkFinanceSDK: def __init__(self, dll_path: str): self._dll = ctypes.cdll.LoadLibrary(dll_path) # --- 返回类型 --- self._dll.NewSdk.restype = c_void_p self._dll.NewSlice.restype = c_void_p # --- 函数签名(关键:避免 OverflowError) --- self._dll.Init.argtypes = [c_void_p, c_char_p, c_char_p] self._dll.Init.restype = c_int self._dll.DestroySdk.argtypes = [c_void_p] self._dll.GetChatData.argtypes = [ c_void_p, # sdk c_ulonglong, # seq c_uint, # limit c_char_p, # proxy c_char_p, # passwd c_uint, # timeout c_void_p # slice ] self._dll.GetChatData.restype = c_int self._dll.GetContentFromSlice.argtypes = [c_void_p] self._dll.GetContentFromSlice.restype = c_char_p self._dll.FreeSlice.argtypes = [c_void_p] self._dll.DecryptData.argtypes = [ c_char_p, # decrypt_key(用RSA解出来的随机密钥,字符串) c_char_p, # encrypt_chat_msg(Base64字符串) c_void_p # out slice ] self._dll.DecryptData.restype = c_int # 可选:媒体相关(这里不演示下载) # self._dll.NewMediaData.restype = c_void_p # ... # 创建 SDK 实例 self.sdk = self._dll.NewSdk() if not self.sdk: raise RuntimeError("NewSdk 返回空指针") def init(self, corpid: bytes, secret: bytes): ret = self._dll.Init(self.sdk, corpid, secret) if ret != 0: raise RuntimeError(f"SDK 初始化失败,错误码: {ret}") print("SDK 初始化成功") def destroy(self): if self.sdk: self._dll.DestroySdk(self.sdk) self.sdk = None print("SDK 已销毁") def new_slice(self): return self._dll.NewSlice() def free_slice(self, slice_ptr): self._dll.FreeSlice(slice_ptr) def slice_to_str(self, slice_ptr) -> str: data = self._dll.GetContentFromSlice(slice_ptr) return data.decode("utf-8") if data else "" def get_chat_data(self, seq: int, limit: int = 1000, timeout_sec: int = 5, proxy: bytes | None = None, passwd: bytes | None = None) -> dict: sp = self.new_slice() try: ret = self._dll.GetChatData( self.sdk, c_ulonglong(seq), c_uint(limit), proxy, # None 会变成 NULL passwd, # None 会变成 NULL c_uint(timeout_sec), sp ) if ret != 0: raise RuntimeError(f"GetChatData 失败,错误码: {ret}") raw = self.slice_to_str(sp) return json.loads(raw) if raw else {} finally: self.free_slice(sp) def decrypt_data(self, decrypt_key_utf8: bytes, encrypt_chat_msg_b64: bytes) -> dict: """ 用 SDK 解密 encrypt_chat_msg(必须传 UTF-8 的 decrypt_key 文本 & Base64 的密文) """ sp = self.new_slice() try: ret = self._dll.DecryptData(decrypt_key_utf8, encrypt_chat_msg_b64, sp) if ret != 0: raise RuntimeError(f"DecryptData 失败,错误码: {ret}") raw = self.slice_to_str(sp) return json.loads(raw) if raw else {} finally: self.free_slice(sp) class WeChatWorkHandler: def __init__(self, sdk, wx_work): self.sdk = sdk self.wx_work = wx_work from datetime import datetime def _get_datetime_str(self, timestamp): # timestamp 是13位整数, 由于精确到秒, 所以只需要10位(除以1000) new_timestamp = timestamp if (9999999999 < new_timestamp): new_timestamp = timestamp / 1000 # 计算 localtime = time.localtime(new_timestamp) datetime_str = time.strftime("%Y-%m-%d %H:%M:%S", localtime) return datetime_str def _get_media_file(self, sdkfileid, save_filename): # 初始化媒体文件拉取的buf # 媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。 # indexbuf一般格式如右侧所示,”Range: bytes = 524288 - 1048575“,表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 indexbuf = "" while True: media_data = self.wx_work.NewMediaData() ret = self.wx_work.GetMediaData(self.sdk, indexbuf, sdkfileid, "", "", 5, media_data) if (0 != ret): print("-- GetMediaData --> 获取失败:", ret) self.wx_work.FreeMediaData(media_data) break # 用msgid作为文件名,这里根据需求制定文件名 # save_filename = os.getcwd() + "/images/" + result["msgid"] + file_ext # ab为二进制文件追加读写,用于分片写入不会覆盖前面已经写入的 with open(save_filename, 'ab') as f: # 这里注意ctypes.string_at的参数(address,size),size就是分片长度,得到 bytes res = ctypes.string_at(self.wx_work.GetData(media_data), self.wx_work.GetDataLen(media_data)) f.write(res) # 拉取完最后一个分片 if (1 == self.wx_work.IsMediaDataFinish(media_data)): self.wx_work.FreeMediaData(media_data) break else: # 获取下一个分片的indexbuf indexbuf = self.wx_work.GetOutIndexBuf(media_data) # 返回 return True def _handle_sync_msg_file(self, seqNo, msgId, msgTime, msgFrom, msgType, msgObj, isSaveDatabase=False): title = "" file_ext = "" file_type = "" file_size = 0 file_content = None sdkfileid = msgObj.get("sdkfileid") datetime_str = self._get_datetime_str(msgTime) if ("video" == msgType): file_ext = ".mp4" title = "视频_" + datetime_str.replace(" ", "").replace(":", "") + file_ext elif ("voice" == msgType): file_ext = ".amr" title = "语音_" + datetime_str.replace(" ", "").replace(":", "") + file_ext elif ("image" == msgType): file_ext = ".png" title = "照片_" + datetime_str.replace(" ", "").replace(":", "") + file_ext elif ("file" == msgType): file_ext = "." + msgObj.get("fileext") title = msgObj.get("filename") # 拉取媒体文件 (real_save_path, url_save_path) = gen_real_file_path() real_save_path = real_save_path + file_ext url_save_path = url_save_path + file_ext self._get_media_file(sdkfileid, real_save_path) file_size = os.path.getsize(real_save_path) # 获取类型 file_type = get_document_file_type(url_save_path) if ("md" == file_type): file_content = get_text_content(real_save_path) elif ("txt" == file_type): file_content = get_text_content(real_save_path) elif ("code" == file_type): file_content = get_text_content(real_save_path) # # 保存到数据库 # if isSaveDatabase: # self._save_database(title, file_type, url_save_path, None, file_content, file_size, file_ext, msgFrom, datetime_str) # 返回 resData = { "title": title, "file_type": file_type, "url_save_path": url_save_path, "real_save_path": real_save_path, "file_content": file_content, "file_size": file_size, "file_ext": file_ext, "datetime_str": datetime_str, } return resData # 处理文本类型 def _handle_sync_msg_text(self, seqNo, msgId, msgTime, msgFrom, msgType, msgObj, isSaveDatabase=False): content = msgObj.get("content") datetime_str = self._get_datetime_str(msgTime) print(f"-- {msgType} --> msgFrom={msgFrom}, content={content}") # 调用 云文档 --> 创建 markdown 文件 # if isSaveDatabase: # self._save_database(content[:50], "md", None, None, content, len(content), ".md", msgFrom, datetime_str) # 返回 resData = { "content": content, "datetime_str": datetime_str, } return resData # 处理链接 def _handle_sync_msg_link(self, seqNo, msgId, msgTime, msgFrom, msgType, msgObj, isSaveDatabase=False): file_content = "" title = msgObj.get("title") description = msgObj.get("description") link_url = msgObj.get("link_url") image_url = msgObj.get("image_url") datetime_str = self._get_datetime_str(msgTime) print(f"-- {msgType} --> title={title}, description={description}, link_url={link_url}, image_url={image_url}") if (description and (0 title={title}, address={address}, longitude={longitude}, latitude={latitude}, zoom={zoom}") file_content = f"{title}\n地址:{address}\n精度:{longitude}\n维度:{latitude}" # # 保存到数据库 # if isSaveDatabase: # self._save_database(title, "md", None, None, file_content, len(file_content), ".md", msgFrom, datetime_str) # 返回 resData = { "title": title, "address": address, "longitude": longitude, "latitude": latitude, "file_content": file_content, "datetime_str": datetime_str, } return resData # 处理消息记录 def _handle_sync_msg_mix(self, seqNo, msgId, msgTime, msgFrom, msgType, msgObj, isSaveDatabase=False): datetime_str = self._get_datetime_str(msgTime) title = msgObj.get("title") items = msgObj.get("item") file_content = "#### " + title for item in items: item_type = item.get("type") item_msgtime = item.get("msgtime") item_content = item.get("content") item_datetime_str = self._get_datetime_str(item_msgtime) if (item_content and (0 < len(item_content))): item_content_obj = json.loads(item_content) if ("ChatRecordText" == item_type): # 处理文本类型 resData = self._handle_sync_msg_text(None, None, item_msgtime, None, "text", item_content_obj, False) tmp_content = resData.get("content") elif ("ChatRecordImage" == item_type): # 处理文件 resData = self._handle_sync_msg_file(None, None, item_msgtime, None, "image", item_content_obj, False) tmp_content = f'![{resData.get("title")}]({resData.get("url_save_path")})' elif ("ChatRecordVideo" == item_type): # 处理文件 resData = self._handle_sync_msg_file(None, None, item_msgtime, None, "video", item_content_obj, False) # tmp_content = resData.get("title") + "\n视频链接:" + resData.get("url_save_path") tmp_content = f'!video[{resData.get("title")}]({resData.get("url_save_path")})' elif ("ChatRecordFile" == item_type): # 处理文件 resData = self._handle_sync_msg_file(None, None, item_msgtime, None, "file", item_content_obj, False) tmp_content = resData.get("title") + "\n文件链接:" + resData.get("url_save_path") elif ("ChatRecordLink" == item_type): # 处理链接 resData = self._handle_sync_msg_link(None, None, item_msgtime, None, "link", item_content_obj, False) tmp_content = resData.get("title") + "\n原文链接:" + resData.get("link_url") elif ("ChatRecordLocation" == item_type): # 处理位置 resData = self._handle_sync_msg_location(None, None, item_msgtime, None, "location", item_content_obj, False) tmp_content = resData.get("file_content") elif ("ChatRecordMixed" == item_type): # 处理消息记录 resData = self._handle_sync_msg_mix(None, None, item_msgtime, None, None, item_content_obj, False) tmp_content = "---\n\n" + resData.get("file_content") + "\n\n---" else: tmp_content = "[该消息类型暂不能展示]" # 构造内容 file_content = file_content + "\n\n" + "> " + item_datetime_str + "\n" + tmp_content # 显示 print(f"----> item_type={item_type}, item_msgtime={item_msgtime}, item_content={item_content}") print(f"----> file_content={file_content}") # 保存到数据库 # if isSaveDatabase: # self._save_database(title, "md", None, None, file_content, len(file_content), ".md", msgFrom, datetime_str) # 返回 resData = { "title": title, "file_content": file_content, "datetime_str": datetime_str, } return resData def sync_msg(self): private_key = RSA.importKey(self.PRI_KEY) cipher = PKCS1_v1_5.new(private_key) slice_ptr = self.wx_work.NewSlice() print("-------------------------------",self.seq) ret = self.wx_work.GetChatData(self.sdk, self.seq, 1000, None, None, 5, slice_ptr) if ret != 0: print("GetChatData 错误:", ret) return origin_data = self.wx_work.GetContentFromSlice(slice_ptr) self.wx_work.FreeSlice(slice_ptr) chat_data = json.loads(origin_data).get("chatdata", []) if not chat_data: print("没有新消息") return for msg in chat_data: try: # RSA 解密随机密钥 encrypt_key_bytes = cipher.decrypt( base64.b64decode(msg.get("encrypt_random_key")), None ) encrypt_key_str = encrypt_key_bytes.decode("utf-8") # 解密消息 slice_ptr = self.wx_work.NewSlice() decrypt_ret = self.wx_work.DecryptData( encrypt_key_str.encode("utf-8"), msg.get("encrypt_chat_msg").encode("utf-8"), slice_ptr ) if decrypt_ret != 0: print(f"DecryptData 错误: {decrypt_ret}, msgid={msg.get('msgid')}") continue result_json = self.wx_work.GetContentFromSlice(slice_ptr) self.wx_work.FreeSlice(slice_ptr) result = json.loads(result_json) # 更新 seq self.seq = msg.get("seq", self.seq) seqNo = msg.get("seq") msgId = result.get("msgid") msgTime = result.get("msgtime") msgFrom = result.get("from") msgType = result.get("msgtype") msgObj = result.get(msgType) # if (("video" == msgType) or ("voice" == msgType) or ("image" == msgType) or ("file" == msgType)): # # 处理图片、音频、视频、文件 # self._handle_sync_msg_file(seqNo, msgId, msgTime, msgFrom, msgType, msgObj, True) print(seqNo) if ("text" == msgType): self._handle_sync_msg_text(seqNo, msgId, msgTime, msgFrom, msgType, msgObj, True) elif ("link" == msgType): self._handle_sync_msg_link(seqNo, msgId, msgTime, msgFrom, msgType, msgObj, True) elif ("location" == msgType): self._handle_sync_msg_location(seqNo, msgId, msgTime, msgFrom, msgType, msgObj, True) elif ("chatrecord" == msgType): self._handle_sync_msg_mix(seqNo, msgId, msgTime, msgFrom, msgType, msgObj, True) except Exception as e: print("处理消息异常:", e, "msg=", msg) # 释放 SDK self.wx_work.DestroySdk(self.sdk) # ====== 使用示例 ====== if __name__ == "__main__": DLL_PATH = r"D:\pyproject\box\qywx-sdk\windows\C_sdk\FinanceSdkDemo\WeWorkFinanceSdk.dll" CORP_ID = b"wwed4ac0dd4e1d398d" SECRET = b"ZPZPpW0tMaIRxhAcywO_lW21ACDUiEBkxJwgrRFRdfg" # 1️⃣ 初始化 SDK sdk_obj = WeWorkFinanceSDK(DLL_PATH) sdk_obj.init(CORP_ID, SECRET) # 2️⃣ 加载 RSA 私钥 with open("private.pem", "rb") as f: PRI_KEY = f.read() # 3️⃣ 初始化 Handler wx_handler = WeChatWorkHandler(sdk_obj.sdk, sdk_obj._dll) wx_handler.PRI_KEY = PRI_KEY # 4️⃣ 设置起始 seq wx_handler.seq = 0 # 5️⃣ 同步消息 wx_handler.sync_msg()