# import requests import logging import os import re import requests import zipfile import shutil import uuid import os import json from urllib import parse APPS_COUNT = 0 SDKS_COUNT = 0 import json import os import lark_oapi as lark from lark_oapi.api.im.v1 import * sdk_external_dependencies = [] class CustomFormatter(logging.Formatter): grey = "\x1b[38;20m" yellow = "\x1b[33;20m" red = "\x1b[31;20m" bold_red = "\x1b[31;1m" reset = "\x1b[0m" green = "\x1b[32m" format = "%(asctime)s: %(message)s" FORMATS = { logging.DEBUG: grey + format + reset, logging.INFO: green + format + reset, logging.WARNING: yellow + format + reset, logging.ERROR: red + format + reset, logging.CRITICAL: bold_red + format + reset, } date_format = "%Y-%m-%d %H:%M:%S" def format(self, record): log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt, self.date_format) return formatter.format(record) # 初始化日志 logger = logging.getLogger("App") logger.setLevel(logging.DEBUG) ch = logging.StreamHandler() ch.setFormatter(CustomFormatter()) logger.addHandler(ch) def sanitize_filename(filename, replace_char="_"): """ 去除 Windows 文件名中的非法字符 参数: filename (str): 要处理的原始文件名 replace_char (str): 替换非法字符的字符,默认为下划线 返回: str: 处理后的合法文件名 """ # Windows 文件系统不允许的字符 illegal_chars = r'[<>:"/\\|?*\x00-\x1f]' # 替换非法字符 sanitized = re.sub(illegal_chars, replace_char, filename) # 同时去除文件名开头和结尾的空格和点 sanitized = sanitized.strip(" .") # 保留文件扩展名中的点 return sanitized def get_token(username, password): """ 利用网页端获取 token :param username: 用户名 :param password: 密码 :return: token """ url = "https://api.winrobot360.com/oauth/token" data = { "username": username, "password": password, "grant_type": "password", "scope": "all", } payload = parse.urlencode(data) headers = { "accept": "application/json", "authorization": "Basic Y29uc29sZTpzVVdpeTZzTmlYZDBlNUxo", "content-type": "application/x-www-form-urlencoded;charset=UTF-8", } response = requests.request("POST", url, headers=headers, data=payload).json() if response.get("success") == "false": raise ValueError(response.get("msg")) return response.get("access_token") def get_client_version(token): """获取当期登录账号客户端的版本""" url = "https://api.winrobot360.com/api/v1/user/info/rich" payload = {} headers = { "Authorization": f"bearer {token}", "Accept": "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml", } response = requests.request("GET", url, headers=headers, data=payload) return response.json().get("data").get("clientVersion") def get_activity_detail(token, sdk_uuid): """ 获取自定义指令的相关信息 :param token: token :param sdk: 自定义指令 :return: package_name, package_url """ name, version = sdk_uuid.split("==") url = f"https://api.winrobot360.com/api/v1/robot/use/activity/package/detail?packageName={name}&version={version}" payload = {} headers = { "Host": "api.winrobot360.com", "Authorization": f"bearer {token}", "Accept": "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml", "User-Agent": "RestSharp/107.3.0.0", "Accept-Encoding": "json", } response = requests.request("GET", url, headers=headers, data=payload) return response.json().get("data") def download_file(url, save_path, filename): """ 根据链接下载文件 :param url: 链接 :param save_path: 保存路径 :param filename: 保存文件名 :return: 保存文件文件名 """ res = requests.get(url) with open(os.path.join(save_path, filename), "wb") as f: f.write(res.content) return os.path.join(save_path, filename) def zip_file(startdir, file_news=None): """ 压缩文件 :param startdir: 要压缩的文件夹路径 :param file_news: 压缩后文件名 """ file_news = file_news if file_news else startdir + ".bot" z = zipfile.ZipFile(file_news, "w", zipfile.ZIP_DEFLATED) for dirpath, dirnames, filenames in os.walk(startdir): fpath = dirpath.replace(startdir, "") fpath = fpath and fpath + os.sep or "" for filename in filenames: z.write(os.path.join(dirpath, filename), fpath + filename) z.close() return file_news def unzip_file(zip_file_path, unzip_path): """ 解压文件 :param zip_file_path: 解压的文件路径 :param unzip_path: 解压(输出)的路径 :return: """ zip_file = zipfile.ZipFile(zip_file_path) for file in zip_file.namelist(): zip_file.extract(file, unzip_path) zip_file.close() def modify_package_version(package_dirctory: str, version: str): """ 修改 package.json 文件中的版本号 """ package_json_path = os.path.join(package_dirctory, "package.json") with open(package_json_path, encoding="utf8") as f: package_json = json.load(f) package_json["version"] = version with open(package_json_path, "w") as f: json.dump(package_json, f, indent=4, ensure_ascii=False) def package_activity(token, robot_uuid, basedir): """ 根据 uuid 将应用打包成 bot 文件, 并存到指定的文件中 :param token: :param roobt_uuid: :param basedir: 文件存放路径 """ # 1. 根据机器人 uuid 获取机器人详细信息 activity_detail = get_activity_detail(token=token, sdk_uuid=robot_uuid) activity_name = activity_detail.get("name") version = activity_detail.get("releaseVersion") packageUrl = activity_detail.get("packageUrl") internal_dependencies = activity_detail.get("flatInternalDependencies") logger.info(f"打包指令: {activity_name}") # 去除不能作为windows文件名的字符 activity_name = sanitize_filename(activity_name, "") # 2. 下载 activity 文件 activity_path = os.path.join(basedir, activity_name) os.makedirs(activity_path) download_name = download_file(packageUrl, activity_path, "xbot_robot.zip") xbot_robot_path = os.path.join(activity_path, "xbot_robot") os.makedirs(xbot_robot_path) unzip_file(download_name, xbot_robot_path) os.remove(download_name) modify_package_version(xbot_robot_path, version) # 3. 下载 activity 的 internal dependence xbot_extensions_path = os.path.join(activity_path, "xbot_extensions") os.makedirs(xbot_extensions_path) with open(os.path.join(xbot_extensions_path, "requirement.txt"), "w") as f: f.write("\n".join(internal_dependencies)) with open(os.path.join(xbot_extensions_path, "__init__.py"), "w") as f: f.write("\n".join([f"from . import {item}" for item in internal_dependencies])) for dependence in internal_dependencies: dependence_detail = get_activity_detail(token, dependence) name = dependence_detail.get("packageName") version = dependence_detail.get("releaseVersion") package_url = dependence_detail.get("packageUrl") dependence_save_path = os.path.join(xbot_extensions_path, name) os.makedirs(dependence_save_path) download_name = download_file(package_url, xbot_extensions_path, "package.zip") unzip_file(download_name, dependence_save_path) modify_package_version(dependence_save_path, version) os.remove(download_name) activity_save_path = zip_file(activity_path) shutil.rmtree(activity_path) return activity_save_path def send_message(file_path, open_id, open_message_id): """发送指令文件到指定用户""" # 创建client YOUR_APP_ID = "cli_a5110cd1da70900b" YOUR_APP_SECRET = "XwTUlUq7dGK1XexUS7ljShJJ7t1MIwXF" client = ( lark.Client.builder() .app_id(YOUR_APP_ID) .app_secret(YOUR_APP_SECRET) .log_level(lark.LogLevel.DEBUG) .build() ) # 构造请求对象 file = open(file_path, "rb") _, filename = os.path.split(file_path) request: CreateFileRequest = ( CreateFileRequest.builder() .request_body( CreateFileRequestBody.builder() .file_type("stream") .file_name(filename) .file(file) .build() ) .build() ) # 发起请求 response: CreateFileResponse = client.im.v1.file.create(request) # 处理失败返回 if not response.success(): lark.logger.error( f"client.im.v1.file.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" ) content = {"file_key": response.data.file_key} uuid_str = str(uuid.uuid4()) request: CreateMessageRequest = ( CreateMessageRequest.builder() .receive_id_type("open_id") .request_body( CreateMessageRequestBody.builder() .receive_id(open_id) .msg_type("file") .content(json.dumps(content)) .build() ) .build() ) # 发起请求 response: CreateMessageResponse = client.im.v1.message.create(request) # 处理失败返回 if not response.success(): lark.logger.error( f"client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" ) return # 处理业务结果 lark.logger.info(lark.JSON.marshal(response.data, indent=4)) # 推送下载成功 content = {"text": "指令包已推送,请通过应用机器人点击下载"} request: ReplyMessageRequest = ( ReplyMessageRequest.builder() .message_id(open_message_id) .request_body( ReplyMessageRequestBody.builder() .content(content=json.dumps(content)) .msg_type("text") .reply_in_thread(True) .build() ) .build() ) # 发起请求 response: CreateMessageResponse = client.im.v1.message.create(request) # 处理失败返回 if not response.success(): lark.logger.error( f"client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" ) return # 处理业务结果 lark.logger.info(lark.JSON.marshal(response.data, indent=4)) def push_xbot_activity_bot(activity_code, open_id, open_message_id): """自动推送官方指令bot包到指定用户 :param activity_code: 指令代码带版本号 :param open_id: 接收者open_id """ username = "剑心@fckj" password = "320753691love" # open_id = "ou_d542891996bf1961f9a94563e6bd4d49" token = get_token(username=username, password=password) logger.error(f"使用 {username} 账号登陆") version = get_client_version(token=token) logger.error(f"当前账号客户端版本号为: {version}") base_dir = os.path.dirname(os.path.realpath(__file__)) activity_path = package_activity( token=token, robot_uuid=activity_code, basedir=base_dir ) send_message(activity_path, open_id, open_message_id) def get_office_activity_names_with_viewname(token): """ 获取自定义指令的名称及版本 :return: list, 返回自定义指令 名_版本号 """ page = 1 uuid = [] url = "https://api.winrobot360.com/api/v1/robot/activity/package/list?source=market&page=%s&size=100&uiaType=uiatype" payload = {} headers = { "Authorization": f"bearer {token}", } activity_data = [] response = requests.request("GET", url % page, headers=headers, data=payload) page_info = response.json().get("page").get("pages") for page in range(1, page_info + 1): response = requests.request("GET", url % page, headers=headers, data=payload) for item in response.json().get("data"): uuid.append(item.get("packageName") + "==" + item.get("packageVersion")) # activity_data[item.get("packageViewName")] = item.get("packageName") + "==" + item.get("packageVersion") # activity_data.append({"activity_name": item.get("packageViewName"), "activity_code": item.get("packageName") + "==" + item.get("packageVersion")}) activity_data.append( { "text": item.get("packageViewName"), "value": f'{item.get("packageName")}=={item.get("packageVersion")}', } ) return activity_data def send_select_activity_card_message(message_id): username = "剑心@fckj" password = "320753691love" # open_id = "ou_d542891996bf1961f9a94563e6bd4d49" token = get_token(username=username, password=password) activity_data = get_office_activity_names_with_viewname(token) content = { "type": "template", "data": { "template_id": "AAqR6hyJFojEs", "template_version_name": "1.0.6", "template_variable": {"activity_list": activity_data}, }, } APP_ID = "cli_a5110cd1da70900b" APP_SECRET = "XwTUlUq7dGK1XexUS7ljShJJ7t1MIwXF" client = ( lark.Client.builder() .app_id(APP_ID) .app_secret(APP_SECRET) .log_level(lark.LogLevel.DEBUG) .build() ) # 构造请求对象 request: ReplyMessageRequest = ( ReplyMessageRequest.builder() .message_id(message_id) .request_body( ReplyMessageRequestBody.builder() .content(content=json.dumps(content)) .msg_type("interactive") .reply_in_thread(True) .build() ) .build() ) # 发起请求 response: CreateMessageResponse = client.im.v1.message.create(request) # 处理失败返回 if not response.success(): lark.logger.error( f"client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}" ) return # 处理业务结果 lark.logger.info(lark.JSON.marshal(response.data, indent=4))