Source code and tools in automation, networking, and media processing — each with implementation notes.
"""
荣耀云登录-截图机器人(Windows 兼容版)
"""
import time
import random
import logging
from datetime import datetime
from pathlib import Path
import schedule
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
# ---------- 参数 ----------
LOGIN_URL = ("https://hnid-drcn.cloud.hihonor.com/CAS/portal/loginAuth.html?...") # URL truncated for brevity in display
ACCOUNT = "###########"
PASSWORD = "######"
BASE_DIR = Path(__file__).parent.resolve()
SCREENSHOT_DIR = BASE_DIR / "screenshots"
LOG_FILE = BASE_DIR / "honor_spy.log"
INTERVAL_MINUTES = 8
PAGE_TIMEOUT = 30_000
RETRY_MAX = 3
# ---------- 日志 ----------
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[logging.FileHandler(LOG_FILE, encoding="utf-8", delay=True),
logging.StreamHandler()]
)
log = logging.getLogger("honor_spy")
# ---------- 工具 ----------
def timestamp_filename() -> str:
# ✅ Windows 兼容:小时和分钟去前导零用 %#H 和 %#M
return datetime.now().strftime("%Y年%m月%d日%#H点%#M分.png")
def random_sleep(a: float = 0.5, b: float = 1.5):
time.sleep(random.uniform(a, b))
# ---------- 主流程 ----------
def login_and_shot(page):
log.info("打开登录页")
page.goto(LOGIN_URL, timeout=PAGE_TIMEOUT)
log.info("输入账号")
account = page.locator('input[ht="input_pwdlogin_account"]')
account.click()
account.fill(ACCOUNT)
random_sleep()
log.info("输入密码")
pwd = page.locator('input[ht="input_pwdlogin_pwd"]')
pwd.click()
pwd.fill(PASSWORD)
random_sleep()
log.info("点击登录")
page.locator('div[ht="click_pwdlogin_submitLogin"]').click()
log.info("等待进入查找设备页")
loc_btn = page.locator('div.operation_button[aria-label="定位设备按钮"]')
loc_btn.wait_for(state="visible", timeout=PAGE_TIMEOUT)
log.info("点击【定位设备】5次")
for _ in range(5):
loc_btn.click()
time.sleep(0.8)
filename = SCREENSHOT_DIR / timestamp_filename()
page.screenshot(path=filename, full_page=True)
log.info("已截图 -> %s", filename)
# ---------- 定时任务 ----------
def job():
log.info("=== 定时任务开始 ===")
for attempt in range(1, RETRY_MAX + 1):
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--disable-gpu", "--no-sandbox"])
context = browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
)
page = context.new_page()
login_and_shot(page)
browser.close()
break
except PWTimeout:
log.warning("第 %s 次尝试超时,重试...", attempt)
except Exception as e:
log.exception("第 %s 次尝试异常: %s", attempt, e)
else:
log.error("已达到最大重试次数,放弃本次截图")
log.info("=== 定时任务结束 ===")
# ---------- 入口 ----------
if __name__ == "__main__":
log.info("荣耀云截图机器人启动,每 %s 分钟执行一次", INTERVAL_MINUTES)
schedule.every(INTERVAL_MINUTES).minutes.do(job)
job() # 立即跑一次
while True:
schedule.run_pending()
time.sleep(1)
import os
import socket
import zipfile
import io
import time
from flask import Flask, request, send_from_directory, jsonify, render_template_string, send_file
# ================= 配置区域 =================
UPLOAD_FOLDER = 'CYBER_DRIVE_STORAGE'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 * 1024 # 无限制
# ================= 修复版前端代码 (刷新功能) =================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>极域 · 赛博传输终端</title>
<!-- ... CSS styles omitted for brevity, see full source ... -->
<style>
:root { --bg-deep: #050505; --neon-blue: #00f3ff; }
body { background: var(--bg-deep); color: white; font-family: sans-serif; }
</style>
</head>
<body>
<div class="container">
<h1>CYBER DRIVE</h1>
<div id="dropZone">Drop Files Here</div>
</div>
<script>
// ... JavaScript logic for file upload/download ...
</script>
</body>
</html>
"""
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/list_files')
def list_files_api():
try:
files = []
paths = sorted(os.listdir(app.config['UPLOAD_FOLDER']), key=lambda x: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], x)), reverse=True)
for f in paths:
if not f.startswith('.'):
fp = os.path.join(app.config['UPLOAD_FOLDER'], f)
if os.path.isfile(fp):
files.append({'name': f, 'size': os.path.getsize(fp)})
return jsonify(files)
except: return jsonify([])
@app.route('/upload', methods=['POST'])
def upload_file():
if 'files[]' not in request.files: return jsonify({'error': 'No file'}), 400
for f in request.files.getlist('files[]'):
if f.filename: f.save(os.path.join(app.config['UPLOAD_FOLDER'], f.filename))
return jsonify({'success': True})
@app.route('/download/')
def download_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
def get_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
s.close()
except: IP = '127.0.0.1'
return IP
if __name__ == '__main__':
print(f"\n >>> 修复版已启动: http://{get_ip()}:5000 <<<\n")
app.run(host='0.0.0.0', port=5000, threaded=True)
# --- 每用户独立存储目录与配额统计 ---
def get_user_dir(username):
path = os.path.join(app.config['STORAGE_ROOT'], username)
if not os.path.exists(path): os.makedirs(path)
return path
def get_folder_size(start_path):
total_size = 0
with os.scandir(start_path) as it:
for entry in it:
if entry.is_file():
total_size += entry.stat().st_size
elif entry.is_dir():
total_size += get_folder_size(entry.path)
return total_size
@app.route('/api/files', methods=['POST'])
@login_required
def list_files():
data = request.json
rel_path = data.get('path', '')
if '..' in rel_path: rel_path = '' # 路径穿越防护:禁止跳出用户目录
base_dir = get_user_dir(current_user.username)
target_dir = os.path.join(base_dir, rel_path)
# ... 遍历目录,分别收集 folders / files,附带大小与修改时间 ...
used_size = get_folder_size(base_dir)
quota_info = {
'used': readable_size(used_size),
'total': readable_size(app.config['USER_QUOTA']),
'percent': min(100, (used_size / app.config['USER_QUOTA']) * 100)
}
return jsonify({'folders': folders, 'files': files, 'quota': quota_info})
@app.route('/download_executor', methods=['POST'])
@login_required
def download_executor():
# ... 单文件:RFC 5987 编码中文文件名,避免移动端乱码
filename = quote(file_list[0])
rv = send_file(target, as_attachment=True, download_name=file_list[0])
rv.headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{filename}"
return rv
# ... 多文件:临时 ZIP + 64KB 分块流式输出,内存占用恒定
with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
for fname in file_list:
abs_path = os.path.join(full_current_path, fname)
if os.path.isfile(abs_path):
zf.write(abs_path, fname)
elif os.path.isdir(abs_path):
for root, _, files_in_dir in os.walk(abs_path):
for file in files_in_dir:
zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file), full_current_path))
def generate():
with temp_zip:
while True:
chunk = temp_zip.read(65536)
if not chunk: break
yield chunk
return Response(stream_with_context(generate()),
mimetype='application/zip',
headers={'Content-Disposition': f'attachment; filename={download_name}'})
/** 带上传进度回调的 RequestBody:writeTo 时统计已写字节, 回调进度(0~100) */
class ProgressRequestBody(
private val delegate: RequestBody,
private val onProgress: (Float) -> Unit
) : RequestBody() {
override fun contentType(): MediaType? = delegate.contentType()
override fun contentLength(): Long = delegate.contentLength()
override fun writeTo(sink: BufferedSink) {
val total = contentLength()
val counting = object : ForwardingSink(sink) {
private var sent = 0L
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
sent += byteCount
if (total > 0L) {
val p = (sent * 100f / total).coerceIn(0f, 100f)
onProgress(p)
}
}
}
val buffered = counting.buffer()
delegate.writeTo(buffered)
buffered.flush()
}
}
/** Multipart 上传:按需包装 ProgressRequestBody,获得 0~100 的进度回调 */
suspend fun ApiService.uploadFile(
bizType: String,
file: File,
onProgress: ((Float) -> Unit)? = null
): UploadResult {
val base = file.asRequestBody("application/octet-stream".toMediaType())
val reqFile = if (onProgress != null) ProgressRequestBody(base, onProgress) else base
val part = MultipartBody.Part.createFormData("file", file.name, reqFile)
val biz = bizType.toRequestBody("text/plain".toMediaType())
val resp = uploadLocal(part, biz)
return resp.data ?: throw IllegalStateException(resp.msg ?: "上传失败")
}
def capture_screen_jpeg(scale=1.0, quality=60):
"""抓取全部屏幕并用 GDI+ 编码为 JPEG。返回 (bytes, width, height)。
采集全程在「Per-Monitor V2 的 DPI 感知上下文」里执行(_DpiCaptureScope)。
不这么做的话,在 4K + 显示缩放的机器上只会抓到左上角一块。
"""
if not IS_WIN or not gdiplus_init():
raise OSError("当前环境不支持屏幕捕获")
scale = max(0.1, min(1.0, float(scale or 1.0)))
quality = max(1, min(100, int(quality or 60)))
with _DpiCaptureScope():
return _capture_screen_jpeg_inner(scale, quality)
def _capture_screen_jpeg_inner(scale, quality):
with _gdi_lock: # GDI 句柄非线程安全,全局串行
vx, vy, vw, vh = screen_metrics()
hdc_screen = user32.GetDC(None)
hdc_mem = gdi32.CreateCompatibleDC(hdc_screen)
hbm = gdi32.CreateCompatibleBitmap(hdc_screen, vw, vh)
old = gdi32.SelectObject(hdc_mem, hbm)
try:
# 屏幕位图 -> 内存 DC
if not gdi32.BitBlt(hdc_mem, 0, 0, vw, vh, hdc_screen, vx, vy, SRCCOPY):
raise OSError("BitBlt 失败")
# HBITMAP -> GDI+ Image
if _gp.GdipCreateBitmapFromHBITMAP(hbm, None, ctypes.byref(img)) != 0:
raise OSError("GdipCreateBitmapFromHBITMAP 失败")
if scale < 0.999: # 高质量双三次插值缩放,降低回传带宽
_gp.GdipSetInterpolationMode(gfx, 7) # HighQualityBicubic
_gp.GdipDrawImageRectI(gfx, img, 0, 0, tw, th)
data = _save_gdiplus_image(img, FORMAT_JPEG, quality)
return data, tw, th
finally: # 逐层释放 GDI/GDI+ 句柄,防止泄漏
_gp.GdipDeleteGraphics(gfx)
_gp.GdipDisposeImage(img)
gdi32.SelectObject(hdc_mem, old)
gdi32.DeleteObject(hbm)
gdi32.DeleteDC(hdc_mem)
user32.ReleaseDC(None, hdc_screen)