添加用户消息记录接口;
This commit is contained in:
389
backend/scripts/decrypt_chat_data.py
Normal file
389
backend/scripts/decrypt_chat_data.py
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
企业微信会话存档解密脚本
|
||||
使用RSA私钥解密加密的会话数据
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 添加当前目录到路径,以便导入wework_ctypes
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from wework_ctypes import WeWorkFinanceSDK, WeWorkSDKError
|
||||
|
||||
def load_rsa_private_key(key_path: str) -> str:
|
||||
"""
|
||||
加载RSA私钥
|
||||
|
||||
Args:
|
||||
key_path: 私钥文件路径
|
||||
|
||||
Returns:
|
||||
私钥内容字符串
|
||||
"""
|
||||
try:
|
||||
with open(key_path, 'r') as f:
|
||||
key_content = f.read()
|
||||
return key_content
|
||||
except Exception as e:
|
||||
raise Exception(f"加载RSA私钥失败: {e}")
|
||||
|
||||
def rsa_decrypt_base64(encrypted_data_base64: str, private_key: str) -> str:
|
||||
"""
|
||||
使用RSA私钥解密Base64编码的数据
|
||||
|
||||
Args:
|
||||
encrypted_data_base64: Base64编码的加密数据
|
||||
private_key: RSA私钥字符串
|
||||
|
||||
Returns:
|
||||
解密后的字符串
|
||||
"""
|
||||
try:
|
||||
# 使用cryptography库进行RSA解密
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import base64
|
||||
|
||||
# 加载私钥
|
||||
private_key_obj = serialization.load_pem_private_key(
|
||||
private_key.encode('utf-8'),
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
# Base64解码
|
||||
encrypted_data = base64.b64decode(encrypted_data_base64)
|
||||
|
||||
# RSA解密 (使用PKCS1v15填充)
|
||||
decrypted_data = private_key_obj.decrypt(
|
||||
encrypted_data,
|
||||
padding.PKCS1v15()
|
||||
)
|
||||
|
||||
return decrypted_data.decode('utf-8')
|
||||
|
||||
except ImportError:
|
||||
raise Exception("需要安装cryptography库: pip install cryptography")
|
||||
except Exception as e:
|
||||
raise Exception(f"RSA解密失败: {e}")
|
||||
|
||||
def decrypt_chat_message(encrypt_random_key: str, encrypt_chat_msg: str,
|
||||
rsa_private_key_path: str, publickey_ver: int = None) -> tuple:
|
||||
"""
|
||||
解密聊天消息
|
||||
|
||||
Args:
|
||||
encrypt_random_key: 加密的随机密钥(Base64编码)
|
||||
encrypt_chat_msg: 加密的聊天消息
|
||||
rsa_private_key_path: RSA私钥文件路径或目录
|
||||
publickey_ver: publickey_ver版本号,用于动态匹配私钥
|
||||
|
||||
Returns:
|
||||
(解密后的消息字典, 实际使用的私钥路径)
|
||||
"""
|
||||
print(f"正在解密消息...")
|
||||
print(f"加密密钥长度: {len(encrypt_random_key)}")
|
||||
print(f"加密消息长度: {len(encrypt_chat_msg)}")
|
||||
|
||||
# 动态确定私钥路径
|
||||
actual_key_path = rsa_private_key_path
|
||||
|
||||
# 检查是否使用默认私钥路径(用户未指定自定义路径)
|
||||
# 通过检查路径是否指向默认文件来判断
|
||||
default_key_path = Path("cert/1_rsa.key")
|
||||
specified_key_path = Path(rsa_private_key_path)
|
||||
|
||||
# 如果使用默认私钥路径且提供了publickey_ver,则动态匹配
|
||||
if publickey_ver is not None and specified_key_path == default_key_path:
|
||||
# 构建动态私钥路径:cert/{publickey_ver}_rsa.key
|
||||
dynamic_key_path = f"cert/{publickey_ver}_rsa.key"
|
||||
dynamic_path = Path(dynamic_key_path)
|
||||
if dynamic_path.exists():
|
||||
actual_key_path = dynamic_key_path
|
||||
print(f"检测到publickey_ver={publickey_ver},使用动态私钥: {actual_key_path}")
|
||||
else:
|
||||
print(f"警告: 动态私钥文件不存在: {dynamic_key_path},使用默认私钥: {actual_key_path}")
|
||||
|
||||
# 1. 加载RSA私钥
|
||||
print(f"加载RSA私钥: {actual_key_path}")
|
||||
private_key = load_rsa_private_key(actual_key_path)
|
||||
print(f"✓ RSA私钥加载成功")
|
||||
|
||||
# 2. 使用RSA私钥解密encrypt_random_key得到encrypt_key
|
||||
print("正在使用RSA解密随机密钥...")
|
||||
try:
|
||||
encrypt_key = rsa_decrypt_base64(encrypt_random_key, private_key)
|
||||
print(f"✓ RSA解密成功,encrypt_key长度: {len(encrypt_key)}")
|
||||
except Exception as e:
|
||||
print(f"✗ RSA解密失败: {e}")
|
||||
# 尝试直接使用encrypt_random_key作为encrypt_key(某些情况下可能已经解密)
|
||||
print("尝试使用原始encrypt_random_key作为encrypt_key...")
|
||||
encrypt_key = encrypt_random_key
|
||||
|
||||
# 3. 使用SDK解密聊天消息
|
||||
print("正在使用SDK解密聊天消息...")
|
||||
try:
|
||||
# 初始化SDK(只需要解密功能,不需要corpid和secret)
|
||||
with WeWorkFinanceSDK("dummy", "dummy") as sdk:
|
||||
decrypted_msg = sdk.decrypt_data(encrypt_key, encrypt_chat_msg)
|
||||
print("✓ SDK解密成功")
|
||||
|
||||
# 尝试解析JSON
|
||||
try:
|
||||
result = json.loads(decrypted_msg)
|
||||
return result, actual_key_path
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON,返回原始文本
|
||||
return {"raw_message": decrypted_msg}, actual_key_path
|
||||
|
||||
except WeWorkSDKError as e:
|
||||
raise Exception(f"SDK解密失败: {e}")
|
||||
except Exception as e:
|
||||
raise Exception(f"解密过程出错: {e}")
|
||||
|
||||
def decrypt_file(input_file: str, rsa_private_key_path: str, output_file: str = None):
|
||||
"""
|
||||
解密文件中的聊天数据
|
||||
|
||||
Args:
|
||||
input_file: 输入文件路径(JSON格式)
|
||||
rsa_private_key_path: RSA私钥文件路径
|
||||
output_file: 输出文件路径(可选)
|
||||
"""
|
||||
print("=" * 60)
|
||||
print("企业微信会话存档解密脚本")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查输入文件
|
||||
input_path = Path(input_file)
|
||||
if not input_path.exists():
|
||||
print(f"错误: 输入文件不存在: {input_file}")
|
||||
return 1
|
||||
|
||||
# 检查RSA私钥文件(只检查用户指定的路径,动态路径在解密时检查)
|
||||
rsa_path = Path(rsa_private_key_path)
|
||||
if not rsa_path.exists():
|
||||
print(f"错误: 指定的RSA私钥文件不存在: {rsa_private_key_path}")
|
||||
return 1
|
||||
|
||||
print(f"输入文件: {input_path.absolute()}")
|
||||
print(f"指定私钥: {rsa_path.absolute()}")
|
||||
print("-" * 60)
|
||||
|
||||
# 用于记录实际使用的私钥路径
|
||||
actual_key_paths_used = set()
|
||||
|
||||
try:
|
||||
# 读取输入文件
|
||||
with open(input_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"文件加载成功")
|
||||
|
||||
# 检查数据格式
|
||||
if "chatdata" not in data:
|
||||
print("错误: 文件中没有chatdata字段")
|
||||
return 1
|
||||
|
||||
chatdata = data.get("chatdata", [])
|
||||
chat_count = len(chatdata)
|
||||
print(f"共发现 {chat_count} 条聊天记录")
|
||||
|
||||
if chat_count == 0:
|
||||
print("没有可解密的聊天记录")
|
||||
return 0
|
||||
|
||||
# 解密所有消息
|
||||
decrypted_messages = []
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for i, msg in enumerate(chatdata):
|
||||
|
||||
# print('msg', type(msg))
|
||||
|
||||
seq = msg.get("seq", i + 1)
|
||||
msgid = msg.get("msgid", f"msg_{i}")
|
||||
encrypt_random_key = msg.get("encrypt_random_key", "")
|
||||
encrypt_chat_msg = msg.get("encrypt_chat_msg", "")
|
||||
|
||||
print(f"\n[{i+1}/{chat_count}] 解密消息 seq={seq}, msgid={msgid[:20]}...")
|
||||
|
||||
if not encrypt_random_key or not encrypt_chat_msg:
|
||||
print(f" 跳过: 缺少加密数据")
|
||||
fail_count += 1
|
||||
decrypted_messages.append({
|
||||
"seq": seq,
|
||||
"msgid": msgid,
|
||||
"error": "缺少加密数据",
|
||||
"original": msg
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
# 获取publickey_ver
|
||||
publickey_ver = msg.get("publickey_ver")
|
||||
|
||||
# 解密消息
|
||||
decrypted_msg, actual_key_path = decrypt_chat_message(
|
||||
encrypt_random_key,
|
||||
encrypt_chat_msg,
|
||||
rsa_private_key_path,
|
||||
publickey_ver
|
||||
)
|
||||
|
||||
# 记录实际使用的私钥路径
|
||||
actual_key_paths_used.add(actual_key_path)
|
||||
|
||||
# 添加元数据
|
||||
if isinstance(decrypted_msg, dict):
|
||||
decrypted_msg["_metadata"] = {
|
||||
"seq": seq,
|
||||
"msgid": msgid,
|
||||
"decrypt_time": datetime.now().isoformat(),
|
||||
"original_has_encryption": True
|
||||
}
|
||||
|
||||
decrypted_messages.append({
|
||||
"seq": seq,
|
||||
"msgid": msgid,
|
||||
"decrypted": decrypted_msg,
|
||||
"original_summary": {
|
||||
"publickey_ver": msg.get("publickey_ver"),
|
||||
"encrypt_key_len": len(encrypt_random_key),
|
||||
"encrypt_msg_len": len(encrypt_chat_msg)
|
||||
}
|
||||
})
|
||||
|
||||
success_count += 1
|
||||
print(f" ✓ 解密成功")
|
||||
|
||||
# 显示解密后的消息摘要
|
||||
if isinstance(decrypted_msg, dict):
|
||||
msg_type = decrypted_msg.get("msgtype", "unknown")
|
||||
from_user = decrypted_msg.get("from", "unknown")
|
||||
to_user = decrypted_msg.get("tolist", ['unknown'])[0]
|
||||
if isinstance(decrypted_msg.get("tolist"), list) and len(decrypted_msg.get("tolist")) > 1:
|
||||
to_user += f" 等 {len(decrypted_msg.get('tolist'))} 人"
|
||||
|
||||
print(f" 类型: {msg_type}, 来自: {from_user}, 发送给: {to_user}")
|
||||
|
||||
# 成功解密后继续处理下一条消息
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 解密失败: {e}")
|
||||
fail_count += 1
|
||||
decrypted_messages.append({
|
||||
"seq": seq,
|
||||
"msgid": msgid,
|
||||
"error": str(e),
|
||||
"original": msg
|
||||
})
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print(f"解密完成: 成功 {success_count} 条, 失败 {fail_count} 条")
|
||||
|
||||
# 显示实际使用的私钥信息
|
||||
if actual_key_paths_used:
|
||||
print(f"实际使用的私钥文件:")
|
||||
for key_path in sorted(actual_key_paths_used):
|
||||
print(f" - {key_path}")
|
||||
|
||||
# 生成输出文件名
|
||||
if output_file is None:
|
||||
timestamp = datetime.now().strftime("%y%m%d_%H%M%S")
|
||||
output_file = f"data/decrypted_{input_path.stem}_{timestamp}.json"
|
||||
|
||||
output_path = Path(output_file)
|
||||
|
||||
# 准备输出数据
|
||||
output_data = {
|
||||
"metadata": {
|
||||
"input_file": str(input_path.absolute()),
|
||||
"decrypt_time": datetime.now().isoformat(),
|
||||
"total_messages": chat_count,
|
||||
"successful_decrypts": success_count,
|
||||
"failed_decrypts": fail_count,
|
||||
"specified_key": str(rsa_path.absolute()),
|
||||
"actual_keys_used": sorted([str(Path(p).absolute()) for p in actual_key_paths_used])
|
||||
},
|
||||
"decrypted_messages": decrypted_messages,
|
||||
"original_metadata": data.get("_metadata", {})
|
||||
}
|
||||
|
||||
# 保存解密结果
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(output_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"解密结果已保存到: {output_path.absolute()}")
|
||||
|
||||
# 显示一些解密后的消息示例
|
||||
if success_count > 0:
|
||||
print("\n解密消息示例:")
|
||||
for i, msg in enumerate(decrypted_messages[:3]): # 显示前3条
|
||||
if "decrypted" in msg:
|
||||
decrypted = msg["decrypted"]
|
||||
if isinstance(decrypted, dict):
|
||||
seq = msg.get("seq", "N/A")
|
||||
msg_type = decrypted.get("msgtype", "unknown")
|
||||
content = decrypted.get("content", "无内容")[:100]
|
||||
print(f" [{i+1}] seq={seq}, 类型={msg_type}")
|
||||
print(f" 内容: {content}...")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("解密完成")
|
||||
|
||||
return 0
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON解析错误: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"解密过程出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="企业微信会话存档解密脚本",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 解密data目录下的文件
|
||||
%(prog)s data/chat_0_100_251203.json
|
||||
|
||||
# 指定RSA私钥路径
|
||||
%(prog)s data/chat_0_100_251203.json --key /path/to/private_key.pem
|
||||
|
||||
# 指定输出文件
|
||||
%(prog)s data/chat_0_100_251203.json --output decrypted_result.json
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('input_file', help='输入文件路径(JSON格式)')
|
||||
parser.add_argument('--key', '-k', default='cert/1_rsa.key',
|
||||
help='RSA私钥文件路径,默认: cert/1_rsa.key')
|
||||
parser.add_argument('--output', '-o', help='输出文件路径,默认自动生成')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return decrypt_file(args.input_file, args.key, args.output)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
704
backend/scripts/load_chat_data.py
Normal file
704
backend/scripts/load_chat_data.py
Normal file
@@ -0,0 +1,704 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
企业微信会话存档SDK测试脚本
|
||||
简单测试脚本,调用SDK获取会话数据并自动解密和存储到数据库
|
||||
|
||||
使用示例:
|
||||
python test_chat_data.py # 使用默认seq=0
|
||||
python test_chat_data.py --seq 1000 # 从seq=1000开始拉取
|
||||
python test_chat_data.py -s 5000 # 从seq=5000开始拉取
|
||||
python test_chat_data.py --help # 显示帮助信息
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 添加当前目录到路径,以便导入wework_ctypes
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# 添加backend目录到路径,以便导入模型
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from wework_ctypes import WeWorkFinanceSDK, WeWorkSDKError, ERROR_CODES
|
||||
|
||||
|
||||
def load_rsa_private_key(key_path: str) -> str:
|
||||
"""
|
||||
加载RSA私钥
|
||||
|
||||
Args:
|
||||
key_path: 私钥文件路径
|
||||
|
||||
Returns:
|
||||
私钥内容字符串
|
||||
"""
|
||||
try:
|
||||
with open(key_path, "r") as f:
|
||||
key_content = f.read()
|
||||
return key_content
|
||||
except Exception as e:
|
||||
raise Exception(f"加载RSA私钥失败: {e}")
|
||||
|
||||
|
||||
def rsa_decrypt_base64(encrypted_data_base64: str, private_key: str) -> str:
|
||||
"""
|
||||
使用RSA私钥解密Base64编码的数据
|
||||
|
||||
Args:
|
||||
encrypted_data_base64: Base64编码的加密数据
|
||||
private_key: RSA私钥字符串
|
||||
|
||||
Returns:
|
||||
解密后的字符串
|
||||
"""
|
||||
try:
|
||||
# 使用cryptography库进行RSA解密
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import base64
|
||||
|
||||
# 加载私钥
|
||||
private_key_obj = serialization.load_pem_private_key(
|
||||
private_key.encode("utf-8"), password=None, backend=default_backend()
|
||||
)
|
||||
|
||||
# Base64解码
|
||||
encrypted_data = base64.b64decode(encrypted_data_base64)
|
||||
|
||||
# RSA解密 (使用PKCS1v15填充)
|
||||
decrypted_data = private_key_obj.decrypt(encrypted_data, padding.PKCS1v15())
|
||||
|
||||
return decrypted_data.decode("utf-8")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("需要安装cryptography库: pip install cryptography")
|
||||
except Exception as e:
|
||||
raise Exception(f"RSA解密失败: {e}")
|
||||
|
||||
|
||||
def decrypt_chat_message(
|
||||
sdk,
|
||||
encrypt_random_key: str,
|
||||
encrypt_chat_msg: str,
|
||||
rsa_private_key_path: str,
|
||||
publickey_ver: int = None,
|
||||
) -> dict:
|
||||
"""
|
||||
解密聊天消息
|
||||
|
||||
Args:
|
||||
sdk: WeWorkFinanceSDK实例
|
||||
encrypt_random_key: 加密的随机密钥(Base64编码)
|
||||
encrypt_chat_msg: 加密的聊天消息
|
||||
rsa_private_key_path: RSA私钥文件路径或目录
|
||||
publickey_ver: publickey_ver版本号,用于动态匹配私钥
|
||||
|
||||
Returns:
|
||||
解密后的消息字典
|
||||
"""
|
||||
# 动态确定私钥路径
|
||||
actual_key_path = rsa_private_key_path
|
||||
|
||||
# 检查是否使用默认私钥路径(用户未指定自定义路径)
|
||||
# 通过检查路径是否指向默认文件来判断
|
||||
default_key_path = Path("cert/1_rsa.key")
|
||||
specified_key_path = Path(rsa_private_key_path)
|
||||
|
||||
# 如果使用默认私钥路径且提供了publickey_ver,则动态匹配
|
||||
if publickey_ver is not None and specified_key_path == default_key_path:
|
||||
# 构建动态私钥路径:cert/{publickey_ver}_rsa.key
|
||||
dynamic_key_path = f"cert/{publickey_ver}_rsa.key"
|
||||
dynamic_path = Path(dynamic_key_path)
|
||||
if dynamic_path.exists():
|
||||
actual_key_path = dynamic_key_path
|
||||
else:
|
||||
# 如果动态私钥不存在,使用默认私钥
|
||||
pass
|
||||
|
||||
# 1. 加载RSA私钥
|
||||
private_key = load_rsa_private_key(actual_key_path)
|
||||
|
||||
# 2. 使用RSA私钥解密encrypt_random_key得到encrypt_key
|
||||
try:
|
||||
encrypt_key = rsa_decrypt_base64(encrypt_random_key, private_key)
|
||||
except Exception as e:
|
||||
# 尝试直接使用encrypt_random_key作为encrypt_key(某些情况下可能已经解密)
|
||||
encrypt_key = encrypt_random_key
|
||||
|
||||
# 3. 使用SDK解密聊天消息
|
||||
decrypted_msg = sdk.decrypt_data(encrypt_key, encrypt_chat_msg)
|
||||
|
||||
# 4. 解析JSON
|
||||
try:
|
||||
result = json.loads(decrypted_msg)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON,返回原始文本
|
||||
return {"raw_message": decrypted_msg}
|
||||
|
||||
|
||||
def decrypt_chat_data(chatdata, sdk, rsa_private_key_path="cert/1_rsa.key"):
|
||||
"""
|
||||
解密聊天数据
|
||||
|
||||
Args:
|
||||
chatdata: 原始聊天数据列表
|
||||
sdk: WeWorkFinanceSDK实例
|
||||
rsa_private_key_path: RSA私钥文件路径
|
||||
|
||||
Returns:
|
||||
(decrypted_chatdata, stats)
|
||||
decrypted_chatdata: 解密后的聊天数据列表
|
||||
stats: 解密统计信息字典
|
||||
"""
|
||||
decrypted_chatdata = []
|
||||
stats = {
|
||||
"total_encrypted_messages": 0,
|
||||
"successful_decrypts": 0,
|
||||
"failed_decrypts": 0,
|
||||
}
|
||||
|
||||
# 检查私钥文件是否存在
|
||||
rsa_path = Path(rsa_private_key_path)
|
||||
if not rsa_path.exists():
|
||||
print(f"警告: RSA私钥文件不存在: {rsa_private_key_path}")
|
||||
print("将跳过解密,保存原始加密数据")
|
||||
return chatdata, stats
|
||||
|
||||
print(f"使用RSA私钥: {rsa_path.absolute()}")
|
||||
print("开始解密聊天数据...")
|
||||
|
||||
for i, msg in enumerate(chatdata):
|
||||
encrypt_random_key = msg.get("encrypt_random_key", "")
|
||||
encrypt_chat_msg = msg.get("encrypt_chat_msg", "")
|
||||
|
||||
# 检查是否有加密数据
|
||||
if not encrypt_random_key or not encrypt_chat_msg:
|
||||
# 没有加密数据,直接添加到结果中
|
||||
decrypted_chatdata.append(msg)
|
||||
continue
|
||||
|
||||
stats["total_encrypted_messages"] += 1
|
||||
|
||||
try:
|
||||
# 获取publickey_ver
|
||||
publickey_ver = msg.get("publickey_ver")
|
||||
|
||||
# 解密消息
|
||||
decrypted_msg = decrypt_chat_message(
|
||||
sdk,
|
||||
encrypt_random_key,
|
||||
encrypt_chat_msg,
|
||||
rsa_private_key_path,
|
||||
publickey_ver,
|
||||
)
|
||||
|
||||
# 创建新的消息对象,保留原始字段但移除加密字段
|
||||
new_msg = {}
|
||||
for key, value in msg.items():
|
||||
if key not in ["encrypt_random_key", "encrypt_chat_msg"]:
|
||||
new_msg[key] = value
|
||||
|
||||
# 将解密后的字段合并到消息中
|
||||
if isinstance(decrypted_msg, dict):
|
||||
for key, value in decrypted_msg.items():
|
||||
new_msg[key] = value
|
||||
|
||||
decrypted_chatdata.append(new_msg)
|
||||
stats["successful_decrypts"] += 1
|
||||
|
||||
# 显示进度
|
||||
if stats["successful_decrypts"] % 100 == 0:
|
||||
print(f" 已解密 {stats['successful_decrypts']} 条消息...")
|
||||
|
||||
except Exception as e:
|
||||
stats["failed_decrypts"] += 1
|
||||
# 解密失败,跳过此消息(不添加到结果中)
|
||||
continue
|
||||
|
||||
return decrypted_chatdata, stats
|
||||
|
||||
|
||||
async def save_messages_to_database(messages: List[Dict[str, Any]]) -> Dict[str, int]:
|
||||
"""
|
||||
将消息保存到数据库
|
||||
|
||||
Args:
|
||||
messages: 消息字典列表
|
||||
|
||||
Returns:
|
||||
存储统计信息字典
|
||||
"""
|
||||
stats = {
|
||||
"total_messages": len(messages),
|
||||
"saved": 0,
|
||||
"skipped_duplicates": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
|
||||
if not messages:
|
||||
print("没有消息需要存储到数据库")
|
||||
return stats
|
||||
|
||||
print(f"\n开始将 {len(messages)} 条消息存储到数据库...")
|
||||
|
||||
try:
|
||||
# 导入数据库相关模块
|
||||
from pymongo import AsyncMongoClient
|
||||
from beanie import init_beanie
|
||||
from app.models import models
|
||||
from app.models.message import Message
|
||||
|
||||
# 从环境变量获取数据库配置
|
||||
mongodb_url = os.getenv(
|
||||
"MONGODB_URL",
|
||||
"mongodb://localhost:27017",
|
||||
)
|
||||
database_name = os.getenv("DATABASE_NAME", "douba_chat_dev")
|
||||
|
||||
# 初始化数据库连接
|
||||
client = AsyncMongoClient(mongodb_url)
|
||||
await init_beanie(database=client[database_name], document_models=models)
|
||||
|
||||
print(f"数据库连接成功: {database_name}")
|
||||
|
||||
# 批量处理消息
|
||||
batch_size = 100
|
||||
for i in range(0, len(messages), batch_size):
|
||||
batch = messages[i : i + batch_size]
|
||||
batch_msgids = [msg.get("msgid") for msg in batch if msg.get("msgid")]
|
||||
|
||||
# 检查哪些消息已经存在
|
||||
existing_messages = await Message.find(
|
||||
{"msgid": {"$in": batch_msgids}}
|
||||
).to_list()
|
||||
existing_msgids = {msg.msgid for msg in existing_messages}
|
||||
|
||||
# 过滤掉已存在的消息
|
||||
new_messages = []
|
||||
for msg in batch:
|
||||
msgid = msg.get("msgid")
|
||||
if not msgid:
|
||||
stats["failed"] += 1
|
||||
continue
|
||||
|
||||
if msgid in existing_msgids:
|
||||
stats["skipped_duplicates"] += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
# 创建Message实例
|
||||
# 注意:使用by_alias=True来处理"from"字段的别名
|
||||
message_data = {
|
||||
"msgid": msg.get("msgid"),
|
||||
"action": msg.get("action"),
|
||||
"from": msg.get("from"), # 使用别名
|
||||
"tolist": msg.get("tolist", []),
|
||||
"roomid": msg.get("roomid"),
|
||||
"text": msg.get("text", {}),
|
||||
"msgtime": msg.get("msgtime"),
|
||||
"msgtype": msg.get("msgtype"),
|
||||
"remark": msg.get("remark"),
|
||||
"seq": msg.get("seq", 1000),
|
||||
"isEnabled": True,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now(),
|
||||
}
|
||||
|
||||
# 移除None值
|
||||
message_data = {
|
||||
k: v for k, v in message_data.items() if v is not None
|
||||
}
|
||||
|
||||
message = Message(**message_data)
|
||||
new_messages.append(message)
|
||||
except Exception as e:
|
||||
print(f"创建消息实例失败 (msgid: {msgid}): {e}")
|
||||
stats["failed"] += 1
|
||||
|
||||
# 批量插入新消息
|
||||
if new_messages:
|
||||
try:
|
||||
await Message.insert_many(new_messages)
|
||||
stats["saved"] += len(new_messages)
|
||||
print(
|
||||
f" 已存储 {len(new_messages)} 条消息 (进度: {i + len(batch)}/{len(messages)})"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"批量插入失败: {e}")
|
||||
stats["failed"] += len(new_messages)
|
||||
|
||||
# 关闭数据库连接
|
||||
await client.close()
|
||||
|
||||
print(f"数据库存储完成:")
|
||||
print(f" 总计消息: {stats['total_messages']}")
|
||||
print(f" 成功存储: {stats['saved']}")
|
||||
print(f" 跳过重复: {stats['skipped_duplicates']}")
|
||||
print(f" 存储失败: {stats['failed']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"数据库存储过程中发生错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
stats["failed"] = len(messages)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:测试获取会话数据"""
|
||||
# 解析命令行参数
|
||||
parser = argparse.ArgumentParser(
|
||||
description="企业微信会话存档SDK测试脚本",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
%(prog)s # 使用默认seq=0
|
||||
%(prog)s --seq 1000 # 从seq=1000开始拉取
|
||||
%(prog)s -s 5000 # 从seq=5000开始拉取
|
||||
%(prog)s --limit 500 # 一次拉取500条消息
|
||||
%(prog)s --timeout 30 # 设置超时时间为30秒
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--seq",
|
||||
type=int,
|
||||
default=0,
|
||||
help="起始seq值,首次拉取使用0,后续拉取使用上次返回的最大seq值 (默认: 0)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--limit",
|
||||
type=int,
|
||||
default=1000,
|
||||
help="一次拉取的消息数量限制 (默认: 1000)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t", "--timeout", type=int, default=10, help="请求超时时间(秒) (默认: 10)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--proxy",
|
||||
type=str,
|
||||
default="",
|
||||
help="代理服务器地址,格式: http://proxy.example.com:8080 (默认: 空)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--passwd", type=str, default="", help="代理服务器密码 (默认: 空)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print("企业微信会话存档SDK测试脚本")
|
||||
print("=" * 60)
|
||||
print(f"命令行参数: seq={args.seq}, limit={args.limit}, timeout={args.timeout}s")
|
||||
if args.proxy:
|
||||
print(f"代理设置: {args.proxy}")
|
||||
|
||||
# 从环境变量获取配置
|
||||
corpid = os.getenv("CORPID", "wwdf65802ca25ec195")
|
||||
secret = os.getenv("WEWORK_SECRET", "-Ta6WMWxBhfGolWnnlO15nQckj3DRKAowUOdX2fwvzE")
|
||||
|
||||
print(f"企业ID: {corpid}")
|
||||
print(f"Secret: {secret[:10]}...") # 只显示前10个字符,避免泄露完整密钥
|
||||
|
||||
# 检查动态库是否存在
|
||||
lib_path = Path(__file__).parent.parent / "lib" / "sdk" / "libWeWorkFinanceSdk_C.so"
|
||||
if not lib_path.exists():
|
||||
print(f"错误: 动态库不存在: {lib_path}")
|
||||
print("请确保已正确放置libWeWorkFinanceSdk_C.so文件")
|
||||
return 1
|
||||
|
||||
print(f"动态库路径: {lib_path}")
|
||||
|
||||
# 确保data目录存在
|
||||
data_dir = Path("data")
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
print(f"数据目录: {data_dir.absolute()}")
|
||||
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
# 初始化SDK(使用上下文管理器确保资源正确释放)
|
||||
print("正在初始化SDK...")
|
||||
with WeWorkFinanceSDK(corpid, secret, str(lib_path)) as sdk:
|
||||
print("✓ SDK初始化成功")
|
||||
|
||||
# 设置拉取参数(从命令行参数获取)
|
||||
seq = args.seq
|
||||
limit = args.limit
|
||||
timeout = args.timeout
|
||||
|
||||
print(f"\n正在拉取会话数据...")
|
||||
print(f"参数: seq={seq}, limit={limit}, timeout={timeout}s")
|
||||
if args.proxy:
|
||||
print(f"代理: {args.proxy}")
|
||||
|
||||
# 调用SDK获取聊天数据
|
||||
result_str = sdk.get_chat_data(
|
||||
seq=seq,
|
||||
limit=limit,
|
||||
proxy=args.proxy, # 使用命令行指定的代理
|
||||
passwd=args.passwd, # 使用命令行指定的代理密码
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
print("✓ 数据拉取成功")
|
||||
print("-" * 60)
|
||||
|
||||
# 尝试解析JSON
|
||||
try:
|
||||
result = json.loads(result_str)
|
||||
|
||||
# 检查错误码
|
||||
errcode = result.get("errcode", -1)
|
||||
errmsg = result.get("errmsg", "")
|
||||
|
||||
if errcode == 0:
|
||||
print(f"成功: {errmsg}")
|
||||
|
||||
# 获取聊天数据
|
||||
chatdata = result.get("chatdata", [])
|
||||
original_chat_count = len(chatdata)
|
||||
|
||||
print(f"共获取 {original_chat_count} 条聊天记录")
|
||||
|
||||
if original_chat_count > 0:
|
||||
# 显示第一条消息的摘要信息
|
||||
print("\n第一条消息摘要:")
|
||||
first_msg = chatdata[0]
|
||||
print(f" seq: {first_msg.get('seq', 'N/A')}")
|
||||
print(f" msgid: {first_msg.get('msgid', 'N/A')}")
|
||||
print(
|
||||
f" publickey_ver: {first_msg.get('publickey_ver', 'N/A')}"
|
||||
)
|
||||
|
||||
# 检查是否有加密数据
|
||||
encrypt_random_key = first_msg.get("encrypt_random_key", "")
|
||||
encrypt_chat_msg = first_msg.get("encrypt_chat_msg", "")
|
||||
|
||||
if encrypt_random_key and encrypt_chat_msg:
|
||||
print(f" 加密密钥长度: {len(encrypt_random_key)}")
|
||||
print(f" 加密消息长度: {len(encrypt_chat_msg)}")
|
||||
print(" 注意: 消息已加密,将自动解密")
|
||||
else:
|
||||
print(" 消息未加密")
|
||||
|
||||
# 解密聊天数据
|
||||
decrypted_chatdata, decrypt_stats = decrypt_chat_data(chatdata, sdk)
|
||||
|
||||
# 更新结果中的chatdata
|
||||
result["chatdata"] = decrypted_chatdata
|
||||
decrypted_chat_count = len(decrypted_chatdata)
|
||||
|
||||
# 显示解密统计
|
||||
print("\n" + "-" * 60)
|
||||
print("解密完成:")
|
||||
print(f" 原始消息总数: {original_chat_count}")
|
||||
print(
|
||||
f" 加密消息数量: {decrypt_stats['total_encrypted_messages']}"
|
||||
)
|
||||
print(f" 成功解密: {decrypt_stats['successful_decrypts']}")
|
||||
print(f" 解密失败: {decrypt_stats['failed_decrypts']}")
|
||||
print(f" 最终输出消息数: {decrypted_chat_count}")
|
||||
|
||||
# 显示最大seq值,用于下次拉取
|
||||
if decrypted_chat_count > 0:
|
||||
max_seq = max(msg.get("seq", 0) for msg in decrypted_chatdata)
|
||||
print(f"\n最大seq值: {max_seq} (下次拉取建议使用此值)")
|
||||
else:
|
||||
max_seq = seq
|
||||
|
||||
# 生成文件名:chat_{start_seq}_{end_seq}_{date}.json
|
||||
start_seq = seq
|
||||
end_seq = max_seq if decrypted_chat_count > 0 else seq
|
||||
date_str = datetime.now().strftime("%y%m%d") # YYMMDD格式
|
||||
|
||||
filename = f"chat_{start_seq}_{end_seq}_{date_str}.json"
|
||||
output_path = data_dir / filename
|
||||
|
||||
# 添加元数据到结果中
|
||||
if "_metadata" not in result:
|
||||
result["_metadata"] = {}
|
||||
|
||||
# 更新chat_count为解密后的消息数量
|
||||
result["_metadata"].update(
|
||||
{
|
||||
"filename": filename,
|
||||
"save_path": str(output_path),
|
||||
"save_time": datetime.now().isoformat(),
|
||||
"seq_range": f"{start_seq}-{end_seq}",
|
||||
"chat_count": decrypted_chat_count, # 使用解密后的数量
|
||||
"parameters": {
|
||||
"request_seq": seq,
|
||||
"limit": limit,
|
||||
"timeout": timeout,
|
||||
"proxy": args.proxy if args.proxy else "未使用",
|
||||
"command_line_args": {
|
||||
"seq": args.seq,
|
||||
"limit": args.limit,
|
||||
"timeout": args.timeout,
|
||||
"proxy": args.proxy,
|
||||
"passwd": "已设置" if args.passwd else "未设置",
|
||||
},
|
||||
},
|
||||
"decrypt_info": {
|
||||
"total_encrypted_messages": decrypt_stats[
|
||||
"total_encrypted_messages"
|
||||
],
|
||||
"successful_decrypts": decrypt_stats[
|
||||
"successful_decrypts"
|
||||
],
|
||||
"failed_decrypts": decrypt_stats["failed_decrypts"],
|
||||
"decrypt_time": datetime.now().isoformat(),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 保存到data目录
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n解密结果已保存到: {output_path}")
|
||||
print(f"文件名格式: chat_{start_seq}_{end_seq}_{date_str}.json")
|
||||
print(
|
||||
f"说明: seq范围 {start_seq}-{end_seq}, 日期 {datetime.now().strftime('%Y-%m-%d')}"
|
||||
)
|
||||
print(f"包含 {decrypted_chat_count} 条解密后的消息")
|
||||
|
||||
# 显示一些解密后的消息示例
|
||||
if decrypted_chat_count > 0:
|
||||
print("\n解密消息示例:")
|
||||
for i, msg in enumerate(decrypted_chatdata[:3]): # 显示前3条
|
||||
seq = msg.get("seq", "N/A")
|
||||
msgid = msg.get("msgid", "N/A")[:20]
|
||||
msgtype = msg.get("msgtype", "unknown")
|
||||
from_user = msg.get("from", "unknown")
|
||||
|
||||
print(
|
||||
f" [{i+1}] seq={seq}, msgid={msgid}..., 类型={msgtype}, 来自={from_user}"
|
||||
)
|
||||
|
||||
# 存储消息到数据库
|
||||
if decrypted_chat_count > 0:
|
||||
print("\n" + "=" * 60)
|
||||
print("开始存储消息到数据库...")
|
||||
try:
|
||||
# 运行异步函数
|
||||
db_stats = asyncio.run(
|
||||
save_messages_to_database(decrypted_chatdata)
|
||||
)
|
||||
|
||||
# 更新元数据中的数据库存储信息
|
||||
if "db_store_info" not in result["_metadata"]:
|
||||
result["_metadata"]["db_store_info"] = {}
|
||||
|
||||
result["_metadata"]["db_store_info"].update(
|
||||
{
|
||||
"db_stats": db_stats,
|
||||
"db_store_time": datetime.now().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
# 重新保存JSON文件以包含数据库存储信息
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"数据库存储信息已更新到: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"数据库存储过程中发生错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
else:
|
||||
print(f"错误: errcode={errcode}, errmsg={errmsg}")
|
||||
|
||||
# 显示详细的错误说明
|
||||
if errcode in ERROR_CODES:
|
||||
print(f"详细说明: {ERROR_CODES[errcode]}")
|
||||
|
||||
# 根据错误码提供建议
|
||||
if errcode == 10000:
|
||||
print("建议: 请检查请求参数是否正确")
|
||||
elif errcode == 10001:
|
||||
print("建议: 请检查网络连接和代理设置")
|
||||
elif errcode == 10002:
|
||||
print("建议: 数据格式可能不正确,请检查响应数据")
|
||||
elif errcode == 10004:
|
||||
print("建议: 请检查密钥配置是否正确")
|
||||
elif errcode == 10006:
|
||||
print("建议: 解密失败,请检查私钥和加密数据")
|
||||
elif errcode == 10007:
|
||||
print("建议: 找不到消息加密版本的私钥,需要重新传入私钥对")
|
||||
elif errcode == 10009:
|
||||
print("建议: IP地址不在白名单中,请检查IP配置")
|
||||
elif errcode == 10010:
|
||||
print("建议: 数据已过期,请重新拉取数据")
|
||||
elif errcode == 10011:
|
||||
print("建议: 证书错误,请检查证书配置")
|
||||
|
||||
print("\n完整响应:")
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON解析错误: {e}")
|
||||
print("原始响应数据:")
|
||||
print(result_str[:500] + "..." if len(result_str) > 500 else result_str)
|
||||
|
||||
# 保存原始数据到文件(使用日期命名)
|
||||
date_str = datetime.now().strftime("%y%m%d")
|
||||
raw_filename = f"chat_raw_{date_str}.txt"
|
||||
raw_output_path = data_dir / raw_filename
|
||||
|
||||
with open(raw_output_path, "w", encoding="utf-8") as f:
|
||||
f.write(result_str)
|
||||
print(f"原始数据已保存到: {raw_output_path}")
|
||||
|
||||
# 同时保存一份到当前目录
|
||||
with open("chat_data_raw.txt", "w", encoding="utf-8") as f:
|
||||
f.write(result_str)
|
||||
print(f"备份原始数据: chat_data_raw.txt")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("测试完成")
|
||||
|
||||
except WeWorkSDKError as e:
|
||||
print(f"SDK错误: {e}")
|
||||
return 1
|
||||
except FileNotFoundError as e:
|
||||
print(f"文件错误: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
430
backend/scripts/wework_ctypes.py
Normal file
430
backend/scripts/wework_ctypes.py
Normal file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
企业微信会话存档SDK的ctypes封装层
|
||||
基于libWeWorkFinanceSdk_C.so动态库
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Any
|
||||
|
||||
# 错误码定义(从WeWorkFinanceSdk_C.h提取)
|
||||
ERROR_CODES = {
|
||||
10000: "参数错误,请求参数错误",
|
||||
10001: "网络错误,网络请求错误",
|
||||
10002: "数据解析失败",
|
||||
10003: "系统失败",
|
||||
10004: "密钥错误导致加密失败",
|
||||
10005: "fileid错误",
|
||||
10006: "解密失败",
|
||||
10007: "找不到消息加密版本的私钥,需要重新传入私钥对",
|
||||
10008: "解析encrypt_key出错",
|
||||
10009: "ip非法",
|
||||
10010: "数据过期",
|
||||
10011: "证书错误"
|
||||
}
|
||||
|
||||
class WeWorkSDKError(Exception):
|
||||
"""企业微信SDK错误基类"""
|
||||
def __init__(self, code: int, message: str = ""):
|
||||
self.code = code
|
||||
if not message and code in ERROR_CODES:
|
||||
message = ERROR_CODES[code]
|
||||
super().__init__(f"SDK错误 {code}: {message}")
|
||||
|
||||
# 定义C结构体
|
||||
class Slice_t(ctypes.Structure):
|
||||
"""Slice结构体,对应C中的Slice_t"""
|
||||
_fields_ = [
|
||||
("buf", ctypes.c_char_p),
|
||||
("len", ctypes.c_int)
|
||||
]
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""将C字符串转换为Python字符串"""
|
||||
if self.buf and self.len > 0:
|
||||
return self.buf[:self.len].decode('utf-8', errors='ignore')
|
||||
return ""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.to_string()
|
||||
|
||||
class MediaData_t(ctypes.Structure):
|
||||
"""媒体数据结构体,对应C中的MediaData_t"""
|
||||
_fields_ = [
|
||||
("outindexbuf", ctypes.c_char_p),
|
||||
("out_len", ctypes.c_int),
|
||||
("data", ctypes.c_char_p),
|
||||
("data_len", ctypes.c_int),
|
||||
("is_finish", ctypes.c_int)
|
||||
]
|
||||
|
||||
def get_outindex(self) -> str:
|
||||
"""获取输出索引"""
|
||||
if self.outindexbuf and self.out_len > 0:
|
||||
return self.outindexbuf[:self.out_len].decode('utf-8', errors='ignore')
|
||||
return ""
|
||||
|
||||
def get_data(self) -> bytes:
|
||||
"""获取媒体数据"""
|
||||
if self.data and self.data_len > 0:
|
||||
return self.data[:self.data_len]
|
||||
return b""
|
||||
|
||||
def is_finished(self) -> bool:
|
||||
"""是否完成"""
|
||||
return self.is_finish == 1
|
||||
|
||||
# WeWorkFinanceSdk_t是不透明指针
|
||||
WeWorkFinanceSdk_t = ctypes.c_void_p
|
||||
|
||||
class WeWorkFinanceSDK:
|
||||
"""企业微信会话存档SDK Python封装"""
|
||||
|
||||
def __init__(self, corpid: str, secret: str, lib_path: Optional[str] = None):
|
||||
"""
|
||||
初始化SDK
|
||||
|
||||
Args:
|
||||
corpid: 企业ID
|
||||
secret: 聊天内容存档的Secret
|
||||
lib_path: 动态库路径,默认为lib/sdk/libWeWorkFinanceSdk_C.so
|
||||
"""
|
||||
self.corpid = corpid
|
||||
self.secret = secret
|
||||
self._sdk_ptr = None
|
||||
self._lib = None
|
||||
|
||||
# 设置库路径
|
||||
if lib_path is None:
|
||||
# 默认路径
|
||||
base_dir = Path(__file__).parent.parent
|
||||
lib_path = str(base_dir / "lib" / "sdk" / "libWeWorkFinanceSdk_C.so")
|
||||
|
||||
self._load_library(lib_path)
|
||||
self._init_sdk()
|
||||
|
||||
def _load_library(self, lib_path: str):
|
||||
"""加载动态库并设置函数原型"""
|
||||
if not os.path.exists(lib_path):
|
||||
raise FileNotFoundError(f"动态库不存在: {lib_path}")
|
||||
|
||||
# 加载库
|
||||
self._lib = ctypes.CDLL(lib_path)
|
||||
|
||||
# 定义函数原型
|
||||
|
||||
# NewSdk: 返回WeWorkFinanceSdk_t指针
|
||||
self._lib.NewSdk.restype = WeWorkFinanceSdk_t
|
||||
self._lib.NewSdk.argtypes = []
|
||||
|
||||
# Init: 返回int
|
||||
self._lib.Init.restype = ctypes.c_int
|
||||
self._lib.Init.argtypes = [WeWorkFinanceSdk_t, ctypes.c_char_p, ctypes.c_char_p]
|
||||
|
||||
# DestroySdk: 无返回值
|
||||
self._lib.DestroySdk.restype = None
|
||||
self._lib.DestroySdk.argtypes = [WeWorkFinanceSdk_t]
|
||||
|
||||
# GetChatData: 返回int
|
||||
self._lib.GetChatData.restype = ctypes.c_int
|
||||
self._lib.GetChatData.argtypes = [
|
||||
WeWorkFinanceSdk_t,
|
||||
ctypes.c_ulonglong, # seq
|
||||
ctypes.c_uint, # limit
|
||||
ctypes.c_char_p, # proxy
|
||||
ctypes.c_char_p, # passwd
|
||||
ctypes.c_int, # timeout
|
||||
ctypes.POINTER(Slice_t) # chatDatas
|
||||
]
|
||||
|
||||
# GetMediaData: 返回int
|
||||
self._lib.GetMediaData.restype = ctypes.c_int
|
||||
self._lib.GetMediaData.argtypes = [
|
||||
WeWorkFinanceSdk_t,
|
||||
ctypes.c_char_p, # indexbuf
|
||||
ctypes.c_char_p, # sdkFileid
|
||||
ctypes.c_char_p, # proxy
|
||||
ctypes.c_char_p, # passwd
|
||||
ctypes.c_int, # timeout
|
||||
ctypes.POINTER(MediaData_t) # media_data
|
||||
]
|
||||
|
||||
# DecryptData: 返回int
|
||||
self._lib.DecryptData.restype = ctypes.c_int
|
||||
self._lib.DecryptData.argtypes = [
|
||||
ctypes.c_char_p, # encrypt_key
|
||||
ctypes.c_char_p, # encrypt_msg
|
||||
ctypes.POINTER(Slice_t) # msg
|
||||
]
|
||||
|
||||
# NewSlice: 返回Slice_t指针
|
||||
self._lib.NewSlice.restype = ctypes.POINTER(Slice_t)
|
||||
self._lib.NewSlice.argtypes = []
|
||||
|
||||
# FreeSlice: 无返回值
|
||||
self._lib.FreeSlice.restype = None
|
||||
self._lib.FreeSlice.argtypes = [ctypes.POINTER(Slice_t)]
|
||||
|
||||
# NewMediaData: 返回MediaData_t指针
|
||||
self._lib.NewMediaData.restype = ctypes.POINTER(MediaData_t)
|
||||
self._lib.NewMediaData.argtypes = []
|
||||
|
||||
# FreeMediaData: 无返回值
|
||||
self._lib.FreeMediaData.restype = None
|
||||
self._lib.FreeMediaData.argtypes = [ctypes.POINTER(MediaData_t)]
|
||||
|
||||
# 辅助函数(可选)
|
||||
if hasattr(self._lib, 'GetContentFromSlice'):
|
||||
self._lib.GetContentFromSlice.restype = ctypes.c_char_p
|
||||
self._lib.GetContentFromSlice.argtypes = [ctypes.POINTER(Slice_t)]
|
||||
|
||||
if hasattr(self._lib, 'GetSliceLen'):
|
||||
self._lib.GetSliceLen.restype = ctypes.c_int
|
||||
self._lib.GetSliceLen.argtypes = [ctypes.POINTER(Slice_t)]
|
||||
|
||||
def _init_sdk(self):
|
||||
"""初始化SDK实例"""
|
||||
# 创建SDK实例
|
||||
self._sdk_ptr = self._lib.NewSdk()
|
||||
if not self._sdk_ptr:
|
||||
raise WeWorkSDKError(-1, "创建SDK实例失败")
|
||||
|
||||
# 初始化SDK
|
||||
corpid_bytes = self.corpid.encode('utf-8')
|
||||
secret_bytes = self.secret.encode('utf-8')
|
||||
|
||||
ret = self._lib.Init(self._sdk_ptr, corpid_bytes, secret_bytes)
|
||||
if ret != 0:
|
||||
self._lib.DestroySdk(self._sdk_ptr)
|
||||
self._sdk_ptr = None
|
||||
raise WeWorkSDKError(ret, f"SDK初始化失败,错误码: {ret}")
|
||||
|
||||
def get_chat_data(self, seq: int = 0, limit: int = 100,
|
||||
proxy: str = "", passwd: str = "",
|
||||
timeout: int = 5) -> str:
|
||||
"""
|
||||
拉取会话存档
|
||||
|
||||
Args:
|
||||
seq: 从指定的seq开始拉取消息,首次使用请使用0
|
||||
limit: 一次拉取的消息条数,最大值1000条
|
||||
proxy: 代理链接,如:socks5://10.0.0.1:8081 或 http://10.0.0.1:8081
|
||||
passwd: 代理账号密码,如:user_name:passwd_123
|
||||
timeout: 超时时间,单位秒
|
||||
|
||||
Returns:
|
||||
返回JSON格式的聊天数据字符串
|
||||
|
||||
Raises:
|
||||
WeWorkSDKError: SDK操作失败
|
||||
"""
|
||||
if not self._sdk_ptr:
|
||||
raise WeWorkSDKError(-1, "SDK未初始化")
|
||||
|
||||
# 创建Slice
|
||||
chat_datas_ptr = self._lib.NewSlice()
|
||||
if not chat_datas_ptr:
|
||||
raise WeWorkSDKError(-1, "创建Slice失败")
|
||||
|
||||
try:
|
||||
# 准备参数
|
||||
proxy_bytes = proxy.encode('utf-8') if proxy else b""
|
||||
passwd_bytes = passwd.encode('utf-8') if passwd else b""
|
||||
|
||||
# 调用GetChatData
|
||||
ret = self._lib.GetChatData(
|
||||
self._sdk_ptr,
|
||||
ctypes.c_ulonglong(seq),
|
||||
ctypes.c_uint(limit),
|
||||
proxy_bytes,
|
||||
passwd_bytes,
|
||||
ctypes.c_int(timeout),
|
||||
chat_datas_ptr
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
raise WeWorkSDKError(ret, f"获取聊天数据失败,错误码: {ret}")
|
||||
|
||||
# 获取数据
|
||||
chat_datas = chat_datas_ptr.contents
|
||||
result = chat_datas.to_string()
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
# 释放资源
|
||||
self._lib.FreeSlice(chat_datas_ptr)
|
||||
|
||||
def get_media_data(self, sdk_fileid: str, save_path: str,
|
||||
proxy: str = "", passwd: str = "",
|
||||
timeout: int = 5) -> bool:
|
||||
"""
|
||||
拉取媒体文件(自动处理分片)
|
||||
|
||||
Args:
|
||||
sdk_fileid: 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid
|
||||
save_path: 媒体文件保存路径
|
||||
proxy: 代理链接
|
||||
passwd: 代理账号密码
|
||||
timeout: 超时时间,单位秒
|
||||
|
||||
Returns:
|
||||
True表示成功,False表示失败
|
||||
|
||||
Raises:
|
||||
WeWorkSDKError: SDK操作失败
|
||||
"""
|
||||
if not self._sdk_ptr:
|
||||
raise WeWorkSDKError(-1, "SDK未初始化")
|
||||
|
||||
index = ""
|
||||
is_finish = 0
|
||||
file_written = False
|
||||
|
||||
# 确保目录存在
|
||||
save_dir = os.path.dirname(save_path)
|
||||
if save_dir and not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
# 打开文件准备写入(二进制追加模式)
|
||||
try:
|
||||
with open(save_path, 'ab') as f:
|
||||
pass
|
||||
except IOError as e:
|
||||
raise WeWorkSDKError(-1, f"无法创建文件 {save_path}: {e}")
|
||||
|
||||
while is_finish == 0:
|
||||
# 创建MediaData
|
||||
media_data_ptr = self._lib.NewMediaData()
|
||||
if not media_data_ptr:
|
||||
raise WeWorkSDKError(-1, "创建MediaData失败")
|
||||
|
||||
try:
|
||||
# 准备参数
|
||||
index_bytes = index.encode('utf-8') if index else b""
|
||||
sdk_fileid_bytes = sdk_fileid.encode('utf-8')
|
||||
proxy_bytes = proxy.encode('utf-8') if proxy else b""
|
||||
passwd_bytes = passwd.encode('utf-8') if passwd else b""
|
||||
|
||||
# 调用GetMediaData
|
||||
ret = self._lib.GetMediaData(
|
||||
self._sdk_ptr,
|
||||
index_bytes,
|
||||
sdk_fileid_bytes,
|
||||
proxy_bytes,
|
||||
passwd_bytes,
|
||||
ctypes.c_int(timeout),
|
||||
media_data_ptr
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
raise WeWorkSDKError(ret, f"获取媒体数据失败,错误码: {ret}")
|
||||
|
||||
# 获取数据
|
||||
media_data = media_data_ptr.contents
|
||||
|
||||
# 写入文件
|
||||
data = media_data.get_data()
|
||||
if data:
|
||||
try:
|
||||
with open(save_path, 'ab') as f:
|
||||
f.write(data)
|
||||
file_written = True
|
||||
except IOError as e:
|
||||
raise WeWorkSDKError(-1, f"写入文件失败: {e}")
|
||||
|
||||
# 更新索引和完成状态
|
||||
index = media_data.get_outindex()
|
||||
is_finish = media_data.is_finish
|
||||
|
||||
finally:
|
||||
# 释放资源
|
||||
self._lib.FreeMediaData(media_data_ptr)
|
||||
|
||||
return file_written
|
||||
|
||||
def decrypt_data(self, encrypt_key: str, encrypt_msg: str) -> str:
|
||||
"""
|
||||
解密会话内容
|
||||
|
||||
Args:
|
||||
encrypt_key: getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容
|
||||
encrypt_msg: getchatdata返回的encrypt_chat_msg
|
||||
|
||||
Returns:
|
||||
解密后的消息明文
|
||||
|
||||
Raises:
|
||||
WeWorkSDKError: SDK操作失败
|
||||
"""
|
||||
# 创建Slice
|
||||
msgs_ptr = self._lib.NewSlice()
|
||||
if not msgs_ptr:
|
||||
raise WeWorkSDKError(-1, "创建Slice失败")
|
||||
|
||||
try:
|
||||
# 准备参数
|
||||
encrypt_key_bytes = encrypt_key.encode('utf-8')
|
||||
encrypt_msg_bytes = encrypt_msg.encode('utf-8')
|
||||
|
||||
# 调用DecryptData
|
||||
ret = self._lib.DecryptData(
|
||||
encrypt_key_bytes,
|
||||
encrypt_msg_bytes,
|
||||
msgs_ptr
|
||||
)
|
||||
|
||||
if ret != 0:
|
||||
raise WeWorkSDKError(ret, f"解密数据失败,错误码: {ret}")
|
||||
|
||||
# 获取解密后的数据
|
||||
msgs = msgs_ptr.contents
|
||||
result = msgs.to_string()
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
# 释放资源
|
||||
self._lib.FreeSlice(msgs_ptr)
|
||||
|
||||
def close(self):
|
||||
"""释放SDK资源"""
|
||||
if self._sdk_ptr and self._lib:
|
||||
self._lib.DestroySdk(self._sdk_ptr)
|
||||
self._sdk_ptr = None
|
||||
|
||||
def __enter__(self):
|
||||
"""上下文管理器入口"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""上下文管理器退出,自动释放资源"""
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保资源释放"""
|
||||
self.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单测试
|
||||
print("企业微信SDK ctypes封装层")
|
||||
print(f"Slice_t大小: {ctypes.sizeof(Slice_t)} 字节")
|
||||
print(f"MediaData_t大小: {ctypes.sizeof(MediaData_t)} 字节")
|
||||
|
||||
# 测试库加载
|
||||
try:
|
||||
base_dir = Path(__file__).parent.parent
|
||||
lib_path = str(base_dir / "lib" / "sdk" / "libWeWorkFinanceSdk_C.so")
|
||||
|
||||
if os.path.exists(lib_path):
|
||||
print(f"动态库存在: {lib_path}")
|
||||
# 尝试加载库但不初始化SDK(需要有效的corpid和secret)
|
||||
lib = ctypes.CDLL(lib_path)
|
||||
print("动态库加载成功")
|
||||
else:
|
||||
print(f"警告: 动态库不存在: {lib_path}")
|
||||
except Exception as e:
|
||||
print(f"加载动态库时出错: {e}")
|
||||
186
backend/scripts/wework_sdk.py
Normal file
186
backend/scripts/wework_sdk.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
企业微信会话存档SDK命令行工具
|
||||
Python重构版本,替代原始的tool_testSdk.cpp
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 添加父目录到路径,以便导入wework_ctypes
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from wework_ctypes import WeWorkFinanceSDK, WeWorkSDKError
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
# 从环境变量获取默认值
|
||||
default_corpid = os.getenv("CORPID", "wwdf65802ca25ec195")
|
||||
default_secret = os.getenv("WEWORK_SECRET", "-Ta6WMWxBhfGolWnnlO15nQckj3DRKAowUOdX2fwvzE")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="企业微信会话存档SDK工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 拉取会话存档
|
||||
%(prog)s chatmsg 0 100 "" "" 5
|
||||
|
||||
# 拉取媒体文件
|
||||
%(prog)s mediadata "fileid" "" "" 5 "/tmp/media.file"
|
||||
|
||||
# 解密数据
|
||||
%(prog)s decryptdata "encrypt_key" "encrypt_chat_msg"
|
||||
|
||||
# 使用代理拉取会话存档
|
||||
%(prog)s chatmsg 0 100 "http://proxy.example.com:8080" "user:pass" 10
|
||||
"""
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='命令')
|
||||
|
||||
# chatmsg命令
|
||||
chatmsg_parser = subparsers.add_parser('chatmsg', help='拉取会话存档')
|
||||
chatmsg_parser.add_argument('seq', type=int, help='从指定的seq开始拉取消息,首次使用请使用0')
|
||||
chatmsg_parser.add_argument('limit', type=int, help='一次拉取的消息条数,最大值1000条')
|
||||
chatmsg_parser.add_argument('proxy', type=str, help='代理链接,如:socks5://10.0.0.1:8081 或 http://10.0.0.1:8081')
|
||||
chatmsg_parser.add_argument('passwd', type=str, help='代理账号密码,如:user_name:passwd_123')
|
||||
chatmsg_parser.add_argument('timeout', type=int, help='超时时间,单位秒')
|
||||
|
||||
# mediadata命令
|
||||
mediadata_parser = subparsers.add_parser('mediadata', help='拉取媒体文件')
|
||||
mediadata_parser.add_argument('fileid', type=str, help='媒体文件id,从解密后的会话存档中得到')
|
||||
mediadata_parser.add_argument('proxy', type=str, help='代理链接')
|
||||
mediadata_parser.add_argument('passwd', type=str, help='代理账号密码')
|
||||
mediadata_parser.add_argument('timeout', type=int, help='超时时间,单位秒')
|
||||
mediadata_parser.add_argument('savefile', type=str, help='媒体文件保存路径')
|
||||
|
||||
# decryptdata命令
|
||||
decryptdata_parser = subparsers.add_parser('decryptdata', help='解密会话存档内容')
|
||||
decryptdata_parser.add_argument('encrypt_key', type=str, help='拉取会话存档返回的encrypt_random_key,使用配置在企业微信管理台的rsa公钥对应的私钥解密后得到encrypt_key')
|
||||
decryptdata_parser.add_argument('encrypt_chat_msg', type=str, help='拉取会话存档返回的encrypt_chat_msg')
|
||||
|
||||
# 添加全局选项
|
||||
parser.add_argument('--corpid', type=str,
|
||||
default=default_corpid,
|
||||
help=f'企业ID,默认从环境变量CORPID读取 (当前: {default_corpid})')
|
||||
parser.add_argument('--secret', type=str,
|
||||
default=default_secret,
|
||||
help=f'聊天内容存档的Secret,默认从环境变量WEWORK_SECRET读取')
|
||||
parser.add_argument('--lib-path', type=str,
|
||||
help='libWeWorkFinanceSdk_C.so路径,默认使用lib/sdk/libWeWorkFinanceSdk_C.so')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
args = parse_args()
|
||||
|
||||
if not args.command:
|
||||
print("错误: 需要指定命令 (chatmsg, mediadata, decryptdata)")
|
||||
print("使用 --help 查看帮助")
|
||||
return 1
|
||||
|
||||
try:
|
||||
# 初始化SDK
|
||||
with WeWorkFinanceSDK(args.corpid, args.secret, args.lib_path) as sdk:
|
||||
|
||||
if args.command == 'chatmsg':
|
||||
# 拉取会话存档
|
||||
print(f"拉取会话存档: seq={args.seq}, limit={args.limit}, timeout={args.timeout}s")
|
||||
if args.proxy:
|
||||
print(f"使用代理: {args.proxy}")
|
||||
|
||||
result = sdk.get_chat_data(
|
||||
seq=args.seq,
|
||||
limit=args.limit,
|
||||
proxy=args.proxy,
|
||||
passwd=args.passwd,
|
||||
timeout=args.timeout
|
||||
)
|
||||
|
||||
# 尝试解析JSON并美化输出
|
||||
try:
|
||||
data = json.loads(result)
|
||||
print("获取聊天数据成功:")
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
|
||||
# 如果有聊天数据,显示统计信息
|
||||
if 'chatdata' in data and data['chatdata']:
|
||||
print(f"\n共获取 {len(data['chatdata'])} 条聊天记录")
|
||||
if data['chatdata']:
|
||||
max_seq = max(msg.get('seq', 0) for msg in data['chatdata'])
|
||||
print(f"最大seq值: {max_seq} (下次拉取建议使用此值)")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("获取聊天数据成功 (原始数据):")
|
||||
print(result)
|
||||
|
||||
return 0
|
||||
|
||||
elif args.command == 'mediadata':
|
||||
# 拉取媒体文件
|
||||
print(f"拉取媒体文件: fileid={args.fileid}, timeout={args.timeout}s")
|
||||
print(f"保存到: {args.savefile}")
|
||||
if args.proxy:
|
||||
print(f"使用代理: {args.proxy}")
|
||||
|
||||
success = sdk.get_media_data(
|
||||
sdk_fileid=args.fileid,
|
||||
save_path=args.savefile,
|
||||
proxy=args.proxy,
|
||||
passwd=args.passwd,
|
||||
timeout=args.timeout
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"媒体文件下载完成: {args.savefile}")
|
||||
|
||||
# 检查文件大小
|
||||
if os.path.exists(args.savefile):
|
||||
size = os.path.getsize(args.savefile)
|
||||
print(f"文件大小: {size} 字节 ({size/1024:.2f} KB)")
|
||||
else:
|
||||
print("媒体文件下载失败")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
elif args.command == 'decryptdata':
|
||||
# 解密会话存档内容
|
||||
print("解密会话存档内容...")
|
||||
|
||||
result = sdk.decrypt_data(args.encrypt_key, args.encrypt_chat_msg)
|
||||
|
||||
# 尝试解析JSON并美化输出
|
||||
try:
|
||||
data = json.loads(result)
|
||||
print("解密成功:")
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
except json.JSONDecodeError:
|
||||
print("解密成功 (原始数据):")
|
||||
print(result)
|
||||
|
||||
return 0
|
||||
|
||||
except WeWorkSDKError as e:
|
||||
print(f"SDK错误: {e}")
|
||||
return 1
|
||||
except FileNotFoundError as e:
|
||||
print(f"文件错误: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"未知错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user