Install
openclaw skills install xingtu-task-invite-code获取星图招募任务(进行中的任务)-邀约达人-二维码邀请 把每个任务的邀约二维码下载到本地电脑,D:\xingtu\task-invite,以任务ID命名每个文件夹
openclaw skills install xingtu-task-invite-codeThis skill automates batch downloading of QR code invitation images from XingTu (星图) recruitment tasks. The complete workflow involves: cookie authentication -> task list retrieval with pagination -> per-task browser automation to download QR codes.
Key tools used: agent-browser (browser automation, cookie injection, file download), PowerShell Invoke-WebRequest (API calls), Python subprocess (batch orchestration).
CRITICAL: Never use mock or simulated data. All data must come from real API calls and browser interactions. If any step fails, report the actual error rather than fabricating results.
Before starting, ensure the agent-browser skill is loaded via use_skill agent-browser. The skill uses these agent-browser commands:
| Command | Purpose |
|---|---|
agent-browser open <url> | Navigate to URL |
agent-browser get url | Get current page URL (for login detection) |
agent-browser eval "<js>" | Execute JavaScript (for cookie injection & DOM manipulation) |
agent-browser snapshot -i | Get interactive element tree with refs |
agent-browser click <ref> | Click element by ref |
agent-browser screenshot | Take screenshot for debugging |
agent-browser close | Close browser session |
All file paths MUST be resolved dynamically based on the runtime environment. Never hardcode C:\Users\xxx\ in scripts.
| Resource | Dynamic Resolution (Python) | Dynamic Resolution (PowerShell) |
|---|---|---|
| Cookie file | os.path.expanduser('~/.xingtuCookie.txt') | "$env:USERPROFILE\.xingtuCookie.txt" |
| User home | os.path.expanduser('~') | $env:USERPROFILE |
| agent-browser | os.path.join(os.environ['APPDATA'], 'npm', 'agent-browser.cmd') | "$env:APPDATA\npm\agent-browser.cmd" |
| Downloads | os.path.expanduser('~/Downloads') | "$env:USERPROFILE\Downloads" |
| Task cache | os.path.join(WORKSPACE, 'tasks_cache.json') | "$PWD\tasks_cache.json" |
| Progress file | os.path.join(WORKSPACE, 'qr_progress.json') | "$PWD\qr_progress.json" |
| Output root | Configurable, default D:\xingtu\task-invite | Same |
WORKSPACE = the directory where the batch script runs (typically the current project workspace). Use os.getcwd() or os.path.dirname(os.path.abspath(__file__)).
The skill follows a sequential multi-phase workflow. Each phase must complete successfully before proceeding to the next.
Session rule: Keep the same agent-browser daemon alive across all phases. Only call agent-browser close at the very end (or on fatal error). Do NOT close between steps.
Read ~/.xingtuCookie.txt (i.e., {USERPROFILE}\.xingtuCookie.txt) using read_file.
Use PowerShell to make a test API call:
$cookieFile = "$env:USERPROFILE\.xingtuCookie.txt"
$cookie = (Get-Content $cookieFile -Encoding UTF8 -Raw).Trim()
$headers = @{
"Accept" = "application/json, text/plain, */*"
"Content-Type" = "application/json"
"agw-js-conv" = "str"
"Cookie" = $cookie
"User-Agent" = "Apifox/1.0.0 (https://apifox.com)"
"Host" = "www.xingtu.cn"
"Accept-Charset" = "UTF-8"
}
$response = Invoke-WebRequest -Uri "https://www.xingtu.cn/gw/api/task/provider_get_task_order_list?page=1&limit=1" -Headers $headers -Method GET -TimeoutSec 15
# Check response: if status 200 and JSON contains valid data (not "用户未登录" or status_code 11001), cookie is valid
Validation criteria:
status_code: 11001 (未登录) -> cookie valid, proceed to Phase 2.status_code: 11001 -> Case B (cookie expired).sessionid=, uid_tt=, sid_guard=, etc.).
~/.xingtuCookie.txt and go to Phase 2. If invalid, warn and go to Step 1.4.When to use: User has provided a cookie string but it contains httpOnly cookies that don't work via document.cookie injection. This is the PREFERRED method when cookie is available.
⚠️ Known limitation: Some essential cookies (e.g., sessionid, sid_guard) may be httpOnly and CANNOT be injected via document.cookie. In this case, agent-browser eval injection will still result in a login redirect. Fall through to Step 1.5 (manual login).
# Python subprocess is PREFERRED over PowerShell for eval calls
# PowerShell truncates JS eval parameters containing special characters
import subprocess, json, time, os
AGENT = os.path.join(os.environ['APPDATA'], 'npm', 'agent-browser.cmd')
COOKIE_FILE = os.path.expanduser('~/.xingtuCookie.txt')
def ag(*args):
cmd = [AGENT] + list(args)
r = subprocess.run(cmd, capture_output=True, timeout=30)
return r.stdout.decode('utf-8', errors='replace').strip()
def ev(js):
js1 = ' '.join(js.split()) # collapse whitespace
r = subprocess.run([AGENT, 'eval', js1], capture_output=True, timeout=15)
return r.stdout.decode('utf-8', errors='replace').strip()
# Step 1: Open xingtu.cn first (must be on the domain before setting cookies)
ag('open', 'https://www.xingtu.cn')
time.sleep(2)
# Step 2: Read cookie and inject via document.cookie
cookie_str = open(COOKIE_FILE, encoding='utf-8').read().strip()
pairs = cookie_str.split('; ')
cookie_json = json.dumps(pairs)
js_inject = f"""(function(){{
var pairs={cookie_json};
var c=0;
for(var i=0;i<pairs.length;i++){{
var kv=pairs[i].trim().split('=');
if(kv.length>=2){{
try{{document.cookie=kv[0]+'='+kv.slice(1).join('=')+';path=/;domain=.xingtu.cn';c++;}}catch(e){{}}
}}
}}
// Also set without domain for current path
for(var i=0;i<pairs.length;i++){{
var kv=pairs[i].trim().split('=');
if(kv.length>=2){{
try{{document.cookie=kv[0]+'='+kv.slice(1).join('=')+';path=/';c++;}}catch(e){{}}
}}
}}
return c;
}})()"""
count = ev(js_inject)
print(f"Injected {count} cookies")
# Step 3: Verify by navigating to a known task page
ag('open', 'https://www.xingtu.cn/provider/pages/recruit/management/7644492294913278002')
time.sleep(6)
current_url = ev("location.href")
if 'login' in current_url.lower() or 'sso' in current_url.lower():
print("Cookie injection insufficient (httpOnly cookies). Falling back to manual login.")
# Go to Step 1.5
else:
print("Cookie injection successful. Proceeding to Phase 2.")
# Proceed to Phase 2
Error Handling:
ev("location.href") still shows login page after injection: httpOnly cookies are blocking. Fall through to Step 1.5.ev(js_inject) returns 0: no cookies were injectable. Check cookie file format.Execution steps:
agent-browser skill if not already loaded.agent-browser open "https://sso.oceanengine.com/xingtu/login?role=7"
agent-browser get url:
# Poll every 5 seconds, max 24 times (2 minutes)
for ($i = 1; $i -le 24; $i++) {
Start-Sleep -Seconds 5
$url = (agent-browser get url 2>&1 | Select-String -Pattern "^https?://" | Out-String).Trim()
if ($url -notmatch "sso.oceanengine.com" -and $url -match "xingtu") {
Write-Host "LOGIN_DETECTED: $url"
break
}
}
sso.oceanengine.com (redirected to xingtu.cn):
agent-browser eval "document.cookie"~/.xingtuCookie.txt.Error Handling:
agent-browser eval "document.cookie" returns empty: the session may not have cookies on the current domain. Navigate to https://www.xingtu.cn first, then retry.Read the cookie string from ~/.xingtuCookie.txt using read_file.
Use PowerShell Invoke-WebRequest to make POST requests:
URL: https://www.xingtu.cn/gw/api/task/provider_get_task_order_list
Headers:
Accept: application/json, text/plain, */*
Content-Type: application/json
agw-js-conv: str
Cookie: {{cookie}}
User-Agent: Apifox/1.0.0 (https://apifox.com)
Host: www.xingtu.cn
Accept-Charset: UTF-8
Accept-Encoding: gzip, deflate
Request Body per page:
{
"page": {{page_number}},
"limit": 10,
"query": {
"order_status": [2],
"task_category_list": [133],
"pay_type_list": [3, 4, 12]
}
}
Pagination implementation:
$cookieFile = "$env:USERPROFILE\.xingtuCookie.txt"
$cookie = (Get-Content $cookieFile -Encoding UTF8 -Raw).Trim()
$allTasks = @()
$page = 1
$hasMore = $true
while ($hasMore) {
$body = @{ page = $page; limit = 10; query = @{ order_status = @(2); task_category_list = @(133); pay_type_list = @(3, 4, 12) } } | ConvertTo-Json -Depth 5
$headers = @{...} # same headers as above with Cookie=$cookie
$response = Invoke-RestMethod -Uri "https://www.xingtu.cn/gw/api/task/provider_get_task_order_list" -Method POST -Headers $headers -Body $body -ContentType "application/json"
if ($response.data.list.Count -eq 0) { break }
$allTasks += $response.data.list
$page++
Start-Sleep -Milliseconds 500 # rate limit protection
}
CRITICAL: Before entering Phase 3, filter out tasks that cannot have QR codes:
# Python reference implementation
import json
with open('tasks_cache.json', 'w', encoding='utf-8') as f:
json.dump(all_tasks, f, ensure_ascii=False)
# Pre-filter: only keep tasks with participants
tasks = [t for t in all_tasks
if int(t['challenge_info'].get('participate_author_count') or 0) > 0]
skipped_zero = len(all_tasks) - len(tasks)
if skipped_zero > 0:
print(f"跳过 {skipped_zero} 个达人报名数为 0 的任务(弹窗无法打开)")
Why pre-filter: Tasks with participate_author_count == 0 have no registered influencers. The "邀约达人" modal cannot open for these tasks because there are no influencers to invite. Attempting to download QR codes for them will always fail.
Logging: For each filtered task, record task ID + name with status no_participants.
Error Handling:
status_code: 11001: cookie expired mid-way. Re-run Phase 1, then resume from failed page.From the collected and filtered task list, extract each task's ID field (challenge_info.id). Log total count: 共找到 N 个可下载任务(已过滤 M 个无达人任务).
⚠️ Critical: Use Python subprocess.run() with array arguments for all agent-browser interactions in this phase. Do NOT use PowerShell for agent-browser calls because:
eval arguments containing special characters (parentheses, quotes, arrows)subprocess.run() with array mode preserves arguments intact# CORRECT - Python subprocess with array args
import subprocess, time, json, os, glob, shutil, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stdout.reconfigure(line_buffering=True)
AGENT = os.path.join(os.environ['APPDATA'], 'npm', 'agent-browser.cmd')
WORKSPACE = os.getcwd() # current project directory
DOWNLOADS = os.path.expanduser('~/Downloads')
def ab(args, timeout=20):
"""Run agent-browser command and return stdout"""
cmd = [AGENT] + (args if isinstance(args, list) else args.split())
return subprocess.run(cmd, capture_output=True, timeout=timeout).stdout.decode('utf-8', errors='replace')
def e(js, timeout=20):
"""Evaluate JavaScript in browser, return result string"""
js1 = ' '.join(js.split()) # collapse whitespace, critical for arg passing
r = subprocess.run([AGENT, 'eval', js1], capture_output=True, timeout=timeout)
out = r.stdout.decode('utf-8', errors='replace').strip()
if out.startswith('"') and out.endswith('"'):
try: out = json.loads(out)
except: pass
return out
New-Item -ItemType Directory -Force -Path "D:\xingtu\task-invite"
PROGRESS_FILE = os.path.join(WORKSPACE, 'qr_progress.json')
# Load existing progress
if os.path.exists(PROGRESS_FILE):
with open(PROGRESS_FILE, encoding='utf-8') as f:
done = json.load(f)
else:
done = []
done_ids = {r['task_id'] for r in done if r['status'] in ('ok', 'skipped')}
pending = [t for t in tasks if t['challenge_info']['id'] not in done_ids]
For each pending task, use agent-browser (already authenticated from Phase 1):
ab(['open', f'https://www.xingtu.cn/provider/pages/recruit/management/{tid}'], timeout=20)
time.sleep(5) # ⚠️ Do NOT use wait --load networkidle - it hangs on SPAs
⚠️ Anti-pattern: agent-browser wait --load networkidle hangs indefinitely on SPA (Single Page Application) pages like xingtu.cn because the page continuously emits network events through XHR polling and WebSocket connections. ALWAYS use fixed time.sleep() instead.
Error Handling:
⚠️ Anti-pattern: Do NOT store snapshot -i ref IDs and use them later. Refs expire after page repaint / DOM mutation. Use eval to find and click buttons directly.
clicked = False
for retry in range(5): # Retry up to 5 times, page may still be loading
r = e("""(function(){
var b = document.querySelectorAll('button');
for (var i=0; i<b.length; i++) {
if (b[i].textContent.includes('邀约达人') && b[i].offsetParent !== null) {
b[i].click();
return 'clicked';
}
}
return 'not found';
})()""")
if r == 'clicked':
clicked = True
break
time.sleep(2) # Wait between retries for page to load
if not clicked:
# Record failure and skip
done.append({'task_id': tid, 'name': tname, 'status': 'no_invite_btn'})
continue
Error Handling:
no_invite_btn, skip task. Some task pages have unusual DOM layouts.no_popup, skip task.# Wait for popup to become visible (up to 8 seconds)
for i in range(16):
s = e("""(function(){
var p = document.querySelector('.ovui-popup__lock');
return p && p.offsetWidth > 0 ? 'visible' : 'hidden';
})()""")
if s == 'visible':
break
time.sleep(0.5)
if s != 'visible':
done.append({'task_id': tid, 'name': tname, 'status': 'no_popup'})
continue
Popup container: The invite modal uses ovui-popup__lock class (NOT el-dialog__wrapper). This was discovered through screenshot debugging.
time.sleep(1) # Let popup animation finish
e("""(function(){
var p = document.querySelector('.ovui-popup__lock');
if (!p) return 'no popup';
var items = p.querySelectorAll('.ovui-radio-item');
for (var i=0; i<items.length; i++) {
if (items[i].offsetParent === null) continue; // hidden elements
if (items[i].textContent.trim() == '二维码邀请') {
items[i].click();
return 'clicked';
}
}
return 'not found';
})()""")
time.sleep(2) # Wait for QR code to render
Radio element: .ovui-radio-item with text exactly "二维码邀请" (NOT a button or span).
dl = e("""(function(){
var bs = document.querySelectorAll('button');
for (var i=0; i<bs.length; i++) {
if (bs[i].textContent.trim() == '下载图片' && bs[i].offsetParent !== null) {
return 'found';
}
}
return 'not found';
})()""")
if dl != 'found':
done.append({'task_id': tid, 'name': tname, 'status': 'no_dl_btn'})
# Close popup before skipping
e("""(function(){
var p = document.querySelector('.ovui-popup__lock');
if (!p) return;
var c = p.querySelector('[class*=close]');
if (c) c.click();
})()""")
continue
Error Handling:
no_dl_btn. This happens on some task pages (e.g., task 7641072633181650953) where the DOM renders differently.# Track existing files in Downloads BEFORE clicking
before = {f: os.path.getmtime(f) for f in glob.glob(os.path.join(DOWNLOADS, '*.png'))}
# Click download button
e("""(function(){
var bs = document.querySelectorAll('button');
for (var i=0; i<bs.length; i++) {
if (bs[i].textContent.trim() == '下载图片' && bs[i].offsetParent !== null) {
bs[i].click();
return 'clicked';
}
}
return 'not found';
})()""")
# Wait for new file to appear in Downloads (up to 30 seconds)
new_file = None
for i in range(60):
for f in glob.glob(os.path.join(DOWNLOADS, '*.png')):
if f not in before:
time.sleep(2) # Let file finish writing
if os.path.getsize(f) > 100000: # ⚠️ Validate file size
new_file = f
break
if new_file:
break
time.sleep(0.5)
if new_file:
sz = os.path.getsize(new_file)
shutil.move(new_file, qr_path) # Move from Downloads to output
print(f" OK: {sz:,}b") # Expected: ~234,000 bytes
done.append({'task_id': tid, 'name': tname, 'status': 'ok', 'size': sz})
else:
print(f" FAIL: download timeout")
done.append({'task_id': tid, 'name': tname, 'status': 'download_timeout'})
Download behavior notes:
~/Downloads/ with filename = task name (e.g., WL-1.3.1-万益蓝女性益生菌_好货koc-6月.png), NOT task_idError Handling:
download_timeout.download_small.# Always close popup after each task to prevent DOM pollution
e("""(function(){
var p = document.querySelector('.ovui-popup__lock');
if (!p) return;
// Try close button first
var c = p.querySelector('[class*=close]');
if (c) {
c.click();
return 'closed by button';
}
// Fallback: press Escape
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape', keyCode: 27, bubbles: true
}));
return 'closed by escape';
})()""")
time.sleep(0.5)
# CRITICAL: Write progress file after each task completes (or fails)
# This enables safe resume if the process is interrupted
with open(PROGRESS_FILE, 'w', encoding='utf-8') as f:
json.dump(done, f, ensure_ascii=False)
Add a 0.5-1 second delay between tasks to avoid rate limiting. Not strictly necessary since each task takes 10-20 seconds, but adds safety.
agent-browser close
| 字段 | 说明 |
|---|---|
| 总任务数 | Total from API (including filtered) |
| 符合条件 | After participate_author_count > 0 filter |
| 成功下载 | Tasks with QR code successfully saved (>100KB) |
| 跳过/失败 | Tasks skipped or failed, with reason codes |
| 输出目录 | D:\xingtu\task-invite\ |
Failure reason codes:
no_participants: participate_author_count == 0, cannot open modalno_invite_btn: "邀约达人" button not found (DOM layout issue)no_popup: Click on invite button didn't open modalno_dl_btn: "下载图片" button not found in modaldownload_timeout: Download button clicked but no file appeared in 30sdownload_small: Downloaded file < 100KB (not a valid QR code)For each failed/skipped task, list: task ID, task name, failure reason code.
When running batch download for 50+ tasks, use background execution to avoid session timeout:
REM Start batch in independent minimized CMD window
cmd /c start "xingtu_batch" /min cmd /c "python -u inject_and_batch.py > batch_log.txt 2>&1"
Why this pattern:
cmd /c start creates a truly independent process (not tied to PowerShell session)/min minimizes the window⚠️ Anti-pattern: Using PowerShell Start-Job or background jobs. These are tied to the PowerShell session and will be killed if the session ends.
# Check log file periodically
import os
log_file = 'batch_log.txt'
if os.path.exists(log_file):
lines = open(log_file, encoding='utf-8', errors='replace').readlines()
print(f"Progress: {len(lines)} lines, {os.path.getsize(log_file)} bytes")
# Show last 5 lines for recent status
for l in lines[-5:]:
print(l.rstrip())
IMPORTANT: After loading this skill, the agent MUST follow this cycle:
Never stop at "showing the plan" -- the skill is designed to be executed end-to-end.
qrcode.png under D:\xingtu\task-invite\{{task_id}}\.errors='replace' when reading/writing files. Set sys.stdout encoding to avoid print crashes.以下是实际操作中反复验证得出的经验,每个坑都浪费过大量时间。严格遵守这些规则可以避免 90% 的故障。
agent-browser wait --load networkidle 永远卡死现象: SPA 页面(如 xingtu.cn)持续发送 XHR 轮询和 WebSocket 心跳包,networkidle 条件永远不满足。
解决方案: 永远不要用 wait --load networkidle。改用固定 time.sleep(N),N 根据实测调整(导航后 5s,弹窗后 1-2s)。
# ❌ BAD
ab('wait --load networkidle')
# ✅ GOOD
ab(['open', url])
time.sleep(5) # empirically determined for xingtu.cn
eval 参数被截断现象: PowerShell 传递包含括号、引号、尖括号的 JS 代码给 agent-browser eval 时,参数在 shell 层被截断或错误转义。
# ❌ BAD - PowerShell truncates the JS after certain characters
agent-browser eval "(function(){var b=document.querySelectorAll('button');...})()"
解决方案: 使用 Python subprocess.run() + 数组参数模式。数组模式不会经过 shell 解析,参数完整传递。
# ✅ GOOD
subprocess.run([AGENT, 'eval', js_code], capture_output=True)
现象: 使用 agent-browser snapshot -i 获取 ref ID(如 [ref=e22]),然后在页面发生任何变化(导航、弹窗、AJAX 更新)后使用该 ref,点击失败。
解决方案:
eval 直接查找并点击元素,不依赖 ref IDeval 模式天然免疫 DOM 更新问题# ❌ BAD - ref may be expired
ag('snapshot -i') # gets ref=e22
# ... page changes ...
ag('click', '[ref=e22]') # FAILS
# ✅ GOOD - always finds the current element
e("""(function(){
var b = document.querySelectorAll('button');
for (var i=0; i<b.length; i++) {
if (b[i].textContent.includes('邀约达人') && b[i].offsetParent !== null) {
b[i].click();
return 'clicked';
}
}
return 'not found';
})()""")
participate_author_count == 0 的任务弹窗打不开现象: 对于没有达人报名的任务,点击"邀约达人"按钮后弹窗无法打开(因为没有可邀约的对象)。
解决方案: Phase 2 结束后立即预过滤,避免在 Phase 3 中浪费时间。
tasks = [t for t in all_tasks
if int(t['challenge_info'].get('participate_author_count') or 0) > 0]
ovui-popup__lock)现象: 星图使用 ovui-popup__lock 作为弹窗容器(不是常见的 el-dialog__wrapper 或 modal)。如果用错选择器,会一直找不到弹窗。
已验证的选择器:
.ovui-popup__lock.ovui-radio-item(文本 "二维码邀请")button(文本 "下载图片")[class*=close](在 popup 内部)document.cookie 注入无效现象: 用户提供的 cookie 字符串中,sessionid、sid_guard 等关键 cookie 带有 httpOnly 标志。通过 document.cookie = ... 注入时,浏览器拒绝设置这些 cookie。
解决方案: 先尝试 eval 注入。如果注入后页面仍然跳转到登录页,则必须走手动浏览器登录流程(Step 1.5)。
检测方法:注入后导航到任务详情页,用 ev("location.href") 检查是否在 login/sso 页面。
现象: agent-browser download 命令下载到 ~/Downloads/,文件名为任务名称(中文),不是 task_id。
解决方案:
.png 文件.png 文件shutil.move() 将文件移动到 D:\xingtu\task-invite\{task_id}\qrcode.pngbefore = {f: os.path.getmtime(f) for f in glob.glob(os.path.join(DOWNLOADS, '*.png'))}
# ... click download ...
# Find new file
for f in glob.glob(os.path.join(DOWNLOADS, '*.png')):
if f not in before and os.path.getsize(f) > 100000:
shutil.move(f, qr_path)
现象: 批量处理 50+ 任务时,如果中途崩溃(浏览器崩溃、网络断开、Cookie 过期),没有进度记录的话需要全部重来。
解决方案: 每完成一个任务(无论成功或失败)立即写 qr_progress.json。这样任何时候中断都可以从中断点继续。
# After EACH task, not at the end:
with open(PROGRESS_FILE, 'w', encoding='utf-8') as f:
json.dump(done, f, ensure_ascii=False)
现象: 如果弹窗不关闭,下一个任务的页面可能残留上一个弹窗的 DOM 元素,导致 querySelector 找到过期元素。
解决方案: 每个任务处理完(无论成功/失败)后,主动关闭弹窗。使用 close 按钮 + Escape 双重兜底。
e("""(function(){
var p = document.querySelector('.ovui-popup__lock');
if (!p) return;
var c = p.querySelector('[class*=close]');
if (c) c.click();
else document.dispatchEvent(new KeyboardEvent('keydown',
{key:'Escape', keyCode:27, bubbles:true}));
})()""")
现象: Windows 环境下 print() 输出中文时抛出 UnicodeEncodeError,或者日志文件出现乱码。
解决方案: 在所有 Python 脚本开头设置 stdout 编码:
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stdout.reconfigure(line_buffering=True) # 实时输出,不缓冲
同时,所有 open() 调用使用 encoding='utf-8'。
1. 导航到任务页 → time.sleep(5) ← 别用 networkidle!
2. eval 点击"邀约达人" → 最多重试 5 次
3. 等待 .ovui-popup__lock → 最多等 8 秒
4. eval 点击"二维码邀请" → .ovui-radio-item
5. 验证"下载图片"按钮存在 → 不存在则记录 no_dl_btn
6. 记录 Downloads 已有 PNG
7. eval 点击"下载图片"
8. 检测 Downloads 新文件 → 等待 30 秒 → 验证 > 100KB
9. shutil.move → D:\xingtu\task-invite\{task_id}\qrcode.png
10. 关闭弹窗 → 保存进度 → 下一个任务