Files
jianxinandClaude Opus 4.8 432482b53b
ci/woodpecker/push/woodpecker Pipeline was successful
support standalone (.shr) activity download via form version
Branch download by the card form 'version' field: standalone packs a flat .shr (no xbot_robot/xbot_extensions, no internal deps), private keeps the current .bot. Also enable kaniko layer cache to skip pip install on code-only builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:33:28 +08:00

508 lines
16 KiB
Python

# 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 package_standalone_activity(token, robot_uuid, basedir):
"""
根据 uuid 将指令打包成单机版 shr 文件, 并存到指定的文件中
单机版不需要 xbot_robot/xbot_extensions 目录结构, 也不下载 internal 依赖,
直接将包解压后改版本号, 压缩成 .shr
:param token:
:param robot_uuid:
:param basedir: 文件存放路径
"""
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")
logger.info(f"打包单机版指令: {activity_name}")
# 去除不能作为windows文件名的字符
activity_name = sanitize_filename(activity_name, "")
activity_path = os.path.join(basedir, activity_name)
os.makedirs(activity_path)
download_name = download_file(packageUrl, activity_path, "xbot_robot.zip")
unzip_file(download_name, activity_path)
os.remove(download_name)
modify_package_version(activity_path, version)
activity_save_path = zip_file(activity_path, f"{activity_path}.shr")
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, package_type="private"):
"""自动推送官方指令bot包到指定用户
:param activity_code: 指令代码带版本号
:param open_id: 接收者open_id
:param package_type: 打包类型, private(联机版, .bot) 或 standalone(单机版, .shr)
"""
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__))
if package_type == "standalone":
activity_path = package_standalone_activity(
token=token, robot_uuid=activity_code, basedir=base_dir
)
else:
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.8",
"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))