@iducky/media-agent 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/SHA256SUMS +94 -0
- package/bin/media-agent.mjs +89 -0
- package/docs/guides/capabilities.md +103 -0
- package/docs/guides/image-text-publishing.md +81 -0
- package/docs/guides/installation.md +103 -0
- package/docs/guides/runtime.md +13 -0
- package/manifest.json +382 -0
- package/package.json +42 -0
- package/pyproject.toml +20 -0
- package/resources/capabilities.json +34 -0
- package/resources/configs/accounts.yaml +21 -0
- package/resources/configs/profile.template.json +20 -0
- package/resources/configs/ranking-profiles.yaml +21 -0
- package/resources/configs/toutiao-profile.template.json +22 -0
- package/resources/configs/xiaohongshu-profile.template.json +27 -0
- package/resources/data/industry-taxonomy.yaml +326 -0
- package/skills/douyin-competitor-collect/SKILL.md +177 -0
- package/skills/douyin-competitor-collect/agents/openai.yaml +4 -0
- package/skills/douyin-competitor-collect/references/output-schema.md +178 -0
- package/skills/douyin-creator-image-text-publish/SKILL.md +44 -0
- package/skills/douyin-creator-image-text-publish/agents/openai.yaml +4 -0
- package/skills/douyin-creator-image-text-publish/references/LICENSE.social-auto-upload +21 -0
- package/skills/douyin-creator-image-text-publish/references/execution-contract.md +46 -0
- package/skills/douyin-creator-image-text-publish/references/upstream.md +30 -0
- package/skills/douyin-creator-index/SKILL.md +32 -0
- package/skills/douyin-creator-index/agents/openai.yaml +4 -0
- package/skills/douyin-creator-login/SKILL.md +58 -0
- package/skills/douyin-creator-login/agents/openai.yaml +4 -0
- package/skills/douyin-creator-publish/SKILL.md +56 -0
- package/skills/douyin-creator-publish/agents/openai.yaml +4 -0
- package/skills/douyin-enterprise-leads/SKILL.md +26 -0
- package/skills/douyin-enterprise-leads/agents/openai.yaml +4 -0
- package/skills/douyin-enterprise-leads-login/SKILL.md +36 -0
- package/skills/douyin-enterprise-leads-login/agents/openai.yaml +4 -0
- package/skills/douyin-enterprise-short-video-export/SKILL.md +29 -0
- package/skills/douyin-enterprise-short-video-export/agents/openai.yaml +4 -0
- package/skills/douyin-enterprise-video-rankings/SKILL.md +51 -0
- package/skills/douyin-enterprise-video-rankings/agents/openai.yaml +4 -0
- package/skills/douyin-enterprise-video-rankings/references/industry-taxonomy.md +29 -0
- package/skills/douyin-web-login/SKILL.md +102 -0
- package/skills/douyin-web-login/agents/openai.yaml +4 -0
- package/skills/toutiao-creator-article-draft/SKILL.md +154 -0
- package/skills/toutiao-creator-article-draft/agents/openai.yaml +4 -0
- package/skills/toutiao-web-login/SKILL.md +96 -0
- package/skills/toutiao-web-login/agents/openai.yaml +4 -0
- package/skills/xiaohongshu-creator-image-text-publish/SKILL.md +46 -0
- package/skills/xiaohongshu-creator-image-text-publish/agents/openai.yaml +4 -0
- package/skills/xiaohongshu-creator-image-text-publish/references/LICENSE.social-auto-upload +21 -0
- package/skills/xiaohongshu-creator-image-text-publish/references/execution-contract.md +46 -0
- package/skills/xiaohongshu-creator-image-text-publish/references/upstream.md +31 -0
- package/skills/xiaohongshu-creator-login/SKILL.md +106 -0
- package/skills/xiaohongshu-creator-login/agents/openai.yaml +4 -0
- package/skills/xiaohongshu-creator-publish/SKILL.md +153 -0
- package/skills/xiaohongshu-creator-publish/agents/openai.yaml +4 -0
- package/src/media_agent/__init__.py +1 -0
- package/src/media_agent/cli.py +28 -0
- package/src/media_agent/commands.sh +436 -0
- package/src/media_agent/platforms/__init__.py +1 -0
- package/src/media_agent/platforms/douyin/__init__.py +1 -0
- package/src/media_agent/platforms/douyin/check_login.py +196 -0
- package/src/media_agent/platforms/douyin/collect_industry_taxonomy.py +184 -0
- package/src/media_agent/platforms/douyin/collect_video_rankings.py +352 -0
- package/src/media_agent/platforms/douyin/douyin_full_login.py +391 -0
- package/src/media_agent/platforms/douyin/douyin_hotspot_v2.py +454 -0
- package/src/media_agent/platforms/douyin/douyin_publish.py +1135 -0
- package/src/media_agent/platforms/douyin/enterprise_login.py +128 -0
- package/src/media_agent/platforms/douyin/export_short_video.py +70 -0
- package/src/media_agent/platforms/douyin/login_controller.py +508 -0
- package/src/media_agent/platforms/douyin/validate_industry_taxonomy.py +152 -0
- package/src/media_agent/platforms/douyin/validate_rankings.py +254 -0
- package/src/media_agent/platforms/toutiao/__init__.py +1 -0
- package/src/media_agent/platforms/toutiao/toutiao_check_login.py +54 -0
- package/src/media_agent/platforms/toutiao/toutiao_login_controller.py +250 -0
- package/src/media_agent/platforms/toutiao/toutiao_login_evidence.py +50 -0
- package/src/media_agent/platforms/toutiao/toutiao_login_ipc.py +70 -0
- package/src/media_agent/platforms/xiaohongshu/__init__.py +1 -0
- package/src/media_agent/platforms/xiaohongshu/xiaohongshu_check_login.py +189 -0
- package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_controller.py +449 -0
- package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_evidence.py +64 -0
- package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_ipc.py +120 -0
- package/src/media_agent/platforms/xiaohongshu/xiaohongshu_publish.py +925 -0
- package/src/media_agent/runtime/__init__.py +1 -0
- package/src/media_agent/runtime/account_manager.py +672 -0
- package/src/media_agent/runtime/browser.py +5 -0
- package/src/media_agent/runtime/paths.py +7 -0
- package/src/media_agent/script_map.json +23 -0
- package/src/node/config.mjs +48 -0
- package/src/node/integrity.mjs +34 -0
- package/src/node/skills.mjs +85 -0
- package/tools/archive_releases.py +83 -0
- package/tools/artifacts.py +56 -0
- package/tools/build_release.py +82 -0
- package/tools/check_catalog.py +23 -0
- package/tools/install_runtime.py +144 -0
- package/tools/run_tests.py +21 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Legacy operation, executed only through main()."""
|
|
2
|
+
|
|
3
|
+
def main():
|
|
4
|
+
#!/usr/bin/env python3
|
|
5
|
+
"""
|
|
6
|
+
douyin_creator_personal_test: 完整登录与登录态持久化流程
|
|
7
|
+
"""
|
|
8
|
+
from media_agent.runtime.paths import runtime_home
|
|
9
|
+
import json, time, sys, os, signal
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from media_agent.runtime.browser import launch_persistent_context
|
|
13
|
+
|
|
14
|
+
BASE = runtime_home()
|
|
15
|
+
ALIAS = 'douyin_creator_personal_test'
|
|
16
|
+
PROFILE_DIR = BASE / 'profiles' / ALIAS
|
|
17
|
+
BROWSER_DATA = str(PROFILE_DIR / 'browser_data')
|
|
18
|
+
LOCK_FILE = BASE / 'locks' / f'{ALIAS}.lock'
|
|
19
|
+
QR_SCREENSHOT = str(BASE / 'screenshots' / f'{ALIAS}_qr.png')
|
|
20
|
+
FINAL_SCREENSHOT = str(BASE / 'screenshots' / f'{ALIAS}_login_result.png')
|
|
21
|
+
PERSISTENCE_SCREENSHOT = str(BASE / 'screenshots' / f'{ALIAS}_persistence.png')
|
|
22
|
+
TASK_ID = f'full_login_{ALIAS}_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
|
23
|
+
|
|
24
|
+
# ========== Report accumulator ==========
|
|
25
|
+
R = {'stage': 'init', 'errors': []}
|
|
26
|
+
def report(k, v):
|
|
27
|
+
R[k] = v
|
|
28
|
+
|
|
29
|
+
# ========== Stage 1: Lock & launch ==========
|
|
30
|
+
report('A_profile_path', str(PROFILE_DIR))
|
|
31
|
+
report('A_browser_data', BROWSER_DATA)
|
|
32
|
+
|
|
33
|
+
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
if LOCK_FILE.exists():
|
|
35
|
+
# Check if it's a zombie lock (no matching process)
|
|
36
|
+
try:
|
|
37
|
+
lock_data = json.loads(LOCK_FILE.read_text())
|
|
38
|
+
pid = lock_data.get('pid')
|
|
39
|
+
if pid:
|
|
40
|
+
try:
|
|
41
|
+
os.kill(pid, 0) # Signal 0 = existence check
|
|
42
|
+
print(f'ERROR|Lock active: pid={pid}', flush=True)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
except OSError:
|
|
45
|
+
print(f'WARN|Zombie lock pid={pid}, cleaning', flush=True)
|
|
46
|
+
LOCK_FILE.unlink()
|
|
47
|
+
except:
|
|
48
|
+
LOCK_FILE.unlink()
|
|
49
|
+
|
|
50
|
+
LOCK_FILE.write_text(json.dumps(
|
|
51
|
+
{'task_id': TASK_ID, 'locked_at': datetime.now(timezone.utc).isoformat(),
|
|
52
|
+
'pid': os.getpid(), 'manual_control': True}, indent=2))
|
|
53
|
+
print(f'LOCK|{TASK_ID}', flush=True)
|
|
54
|
+
report('I_lock_acquired', True)
|
|
55
|
+
|
|
56
|
+
config = json.loads((PROFILE_DIR / 'config.json').read_text())
|
|
57
|
+
fp = config['fingerprint']
|
|
58
|
+
context = launch_persistent_context(
|
|
59
|
+
user_data_dir=BROWSER_DATA,
|
|
60
|
+
headless=False, stealth_args=True, viewport=fp['viewport'],
|
|
61
|
+
locale=fp['locale'], timezone=fp['timezone'], humanize=False, geoip=False,
|
|
62
|
+
)
|
|
63
|
+
print('LAUNCH|Browser started', flush=True)
|
|
64
|
+
page = context.pages[0] if context.pages else context.new_page()
|
|
65
|
+
page.goto('https://creator.douyin.com/', wait_until='domcontentloaded', timeout=30000)
|
|
66
|
+
time.sleep(5)
|
|
67
|
+
report('B_launch_url', page.url)
|
|
68
|
+
|
|
69
|
+
# ========== Check if already logged in ==========
|
|
70
|
+
ch = page.content().lower()
|
|
71
|
+
already_in = '扫码登录' not in ch and '登录/注册' not in ch
|
|
72
|
+
if already_in:
|
|
73
|
+
print('ALREADY_LOGGED_IN|直接进入登录成功检查', flush=True)
|
|
74
|
+
report('B_first_scan', 'skipped_already_logged_in')
|
|
75
|
+
# Jump to login check
|
|
76
|
+
logged_in = True
|
|
77
|
+
# (goto login check section)
|
|
78
|
+
else:
|
|
79
|
+
report('B_first_scan', 'qr_sent')
|
|
80
|
+
|
|
81
|
+
# ========== Stage 2: QR code ==========
|
|
82
|
+
for i in range(30):
|
|
83
|
+
try:
|
|
84
|
+
for div in page.query_selector_all('div'):
|
|
85
|
+
box = div.bounding_box()
|
|
86
|
+
if box and abs(box['width'] - 250) < 10 and abs(box['height'] - 250) < 10:
|
|
87
|
+
div.screenshot(path=QR_SCREENSHOT)
|
|
88
|
+
if Path(QR_SCREENSHOT).exists() and Path(QR_SCREENSHOT).stat().st_size > 2000:
|
|
89
|
+
print(f'QR_OK|{QR_SCREENSHOT}', flush=True); break
|
|
90
|
+
except: pass
|
|
91
|
+
if Path(QR_SCREENSHOT).exists() and Path(QR_SCREENSHOT).stat().st_size > 2000: break
|
|
92
|
+
time.sleep(1)
|
|
93
|
+
print('QR_READY|请扫码,完成后回复:首次扫码完成', flush=True)
|
|
94
|
+
sys.stdout.flush()
|
|
95
|
+
|
|
96
|
+
# ========== Stage 3: Wait for scan ==========
|
|
97
|
+
SIG = BASE / 'locks' / f'{ALIAS}.continue'
|
|
98
|
+
print('WAIT_SCAN', flush=True)
|
|
99
|
+
for i in range(900):
|
|
100
|
+
time.sleep(1)
|
|
101
|
+
if SIG.exists():
|
|
102
|
+
SIG.unlink(missing_ok=True)
|
|
103
|
+
print('SCAN_DONE', flush=True); break
|
|
104
|
+
report('C_verify_triggered', False)
|
|
105
|
+
|
|
106
|
+
# ========== Stage 4: Identity verification popup ==========
|
|
107
|
+
print('WAIT_VERIFY', flush=True)
|
|
108
|
+
verify_found = False
|
|
109
|
+
for i in range(30):
|
|
110
|
+
time.sleep(1)
|
|
111
|
+
try:
|
|
112
|
+
ch = page.content().lower()
|
|
113
|
+
if '身份验证' in ch:
|
|
114
|
+
print('VERIFY_POPUP|身份验证弹窗已出现', flush=True)
|
|
115
|
+
report('C_verify_triggered', True)
|
|
116
|
+
verify_found = True
|
|
117
|
+
break
|
|
118
|
+
except: pass
|
|
119
|
+
|
|
120
|
+
if verify_found:
|
|
121
|
+
# Click "接收短信验证码" using text locator
|
|
122
|
+
try:
|
|
123
|
+
opt = page.locator('text=接收短信验证码').first
|
|
124
|
+
if opt and opt.is_visible():
|
|
125
|
+
opt.click()
|
|
126
|
+
time.sleep(2)
|
|
127
|
+
print('SMS_CLICKED|点击接收短信验证码', flush=True)
|
|
128
|
+
report('D_verify_method', 'sms_code')
|
|
129
|
+
else:
|
|
130
|
+
# Fallback: JS click
|
|
131
|
+
page.evaluate('''() => {
|
|
132
|
+
const els = document.querySelectorAll('div, span, button');
|
|
133
|
+
for (const el of els) {
|
|
134
|
+
if (el.textContent.includes('接收短信验证码')) {
|
|
135
|
+
el.click(); return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}''')
|
|
139
|
+
time.sleep(2)
|
|
140
|
+
print('SMS_CLICKED_JS', flush=True)
|
|
141
|
+
report('D_verify_method', 'sms_code_js')
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f'SMS_CLICK_ERR|{e}', flush=True)
|
|
144
|
+
report('D_verify_method', f'failed: {e}')
|
|
145
|
+
|
|
146
|
+
# Wait for SMS input field
|
|
147
|
+
print('WAIT_SMS_INPUT', flush=True)
|
|
148
|
+
for i in range(30):
|
|
149
|
+
time.sleep(1)
|
|
150
|
+
try:
|
|
151
|
+
ch = page.content().lower()
|
|
152
|
+
if '验证码' in ch and '请输入' in ch:
|
|
153
|
+
print('SMS_INPUT_READY', flush=True)
|
|
154
|
+
break
|
|
155
|
+
except: pass
|
|
156
|
+
|
|
157
|
+
# Confirm SMS sent to phone number
|
|
158
|
+
print('SMS_PROMPT|验证码输入框已准备好,请发送本次短信验证码。', flush=True)
|
|
159
|
+
sys.stdout.flush()
|
|
160
|
+
|
|
161
|
+
# ========== Stage 5: Wait for SMS code ==========
|
|
162
|
+
SIG_CODE = BASE / 'locks' / f'{ALIAS}.code_done'
|
|
163
|
+
print('WAIT_CODE', flush=True)
|
|
164
|
+
code_received = False
|
|
165
|
+
for i in range(900):
|
|
166
|
+
time.sleep(1)
|
|
167
|
+
if SIG_CODE.exists():
|
|
168
|
+
SIG_CODE.unlink(missing_ok=True)
|
|
169
|
+
code_received = True
|
|
170
|
+
print('CODE_DONE', flush=True)
|
|
171
|
+
break
|
|
172
|
+
# Also check if page changed (auto-login)
|
|
173
|
+
try:
|
|
174
|
+
ch = page.content().lower()
|
|
175
|
+
if '扫码登录' not in ch and '登录/注册' not in ch:
|
|
176
|
+
print('AUTO_LOGIN|页面已跳转', flush=True)
|
|
177
|
+
break
|
|
178
|
+
except: pass
|
|
179
|
+
|
|
180
|
+
if code_received:
|
|
181
|
+
# The code is stored in a temp file (not in logs/state)
|
|
182
|
+
code_file = BASE / 'locks' / f'{ALIAS}.code_value'
|
|
183
|
+
if code_file.exists():
|
|
184
|
+
code = code_file.read_text().strip()
|
|
185
|
+
code_file.unlink()
|
|
186
|
+
print(f'CODE_READ|长度={len(code)}', flush=True)
|
|
187
|
+
|
|
188
|
+
# Fill code using the correct modal locator
|
|
189
|
+
try:
|
|
190
|
+
# Locate the second_verify_mask modal
|
|
191
|
+
modal = page.locator('div[class*="second_verify_mask"]')
|
|
192
|
+
modal_count = modal.count()
|
|
193
|
+
code_input = modal.locator('input#button-input[name="button-input"][maxlength="6"]')
|
|
194
|
+
ci_count = code_input.count()
|
|
195
|
+
ci_visible = code_input.is_visible()
|
|
196
|
+
ci_enabled = code_input.is_enabled()
|
|
197
|
+
print(f'MODAL|count={modal_count}|input_count={ci_count}|visible={ci_visible}|enabled={ci_enabled}', flush=True)
|
|
198
|
+
|
|
199
|
+
if ci_count > 0 and ci_visible:
|
|
200
|
+
code_input.scroll_into_view_if_needed()
|
|
201
|
+
time.sleep(0.3)
|
|
202
|
+
code_input.click()
|
|
203
|
+
time.sleep(0.3)
|
|
204
|
+
code_input.fill(code)
|
|
205
|
+
time.sleep(0.5)
|
|
206
|
+
val = code_input.input_value()
|
|
207
|
+
if len(val) == 6:
|
|
208
|
+
print(f'CODE_FILLED|ok', flush=True)
|
|
209
|
+
report('E_code_submit', 'filled_ok')
|
|
210
|
+
else:
|
|
211
|
+
print(f'CODE_FILLED|len={len(val)}', flush=True)
|
|
212
|
+
report('E_code_submit', f'filled_short_{len(val)}')
|
|
213
|
+
else:
|
|
214
|
+
# Fallback: use direct selector
|
|
215
|
+
print(f'MODAL_LOOKUP_FALLBACK', flush=True)
|
|
216
|
+
code_input2 = page.locator('input#button-input[type="tel"][maxlength="6"]')
|
|
217
|
+
ci2_count = code_input2.count()
|
|
218
|
+
print(f'FALLBACK|count={ci2_count}', flush=True)
|
|
219
|
+
if ci2_count > 0:
|
|
220
|
+
code_input2.first.scroll_into_view_if_needed()
|
|
221
|
+
time.sleep(0.3)
|
|
222
|
+
code_input2.first.click()
|
|
223
|
+
time.sleep(0.3)
|
|
224
|
+
code_input2.first.fill(code)
|
|
225
|
+
time.sleep(0.5)
|
|
226
|
+
val = code_input2.first.input_value()
|
|
227
|
+
if len(val) == 6:
|
|
228
|
+
print(f'CODE_FILLED|ok', flush=True)
|
|
229
|
+
report('E_code_submit', 'filled_ok')
|
|
230
|
+
else:
|
|
231
|
+
print(f'CODE_FILLED|len={len(val)}', flush=True)
|
|
232
|
+
report('E_code_submit', f'filled_short_{len(val)}')
|
|
233
|
+
else:
|
|
234
|
+
print('CODE_INPUT_NOT_FOUND', flush=True)
|
|
235
|
+
report('E_code_submit', 'input_not_found')
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f'CODE_FILL_ERR|{e}', flush=True)
|
|
238
|
+
report('E_code_submit', f'error: {e}')
|
|
239
|
+
|
|
240
|
+
# Click 验证 button using JS within the same modal
|
|
241
|
+
try:
|
|
242
|
+
result = page.evaluate('''() => {
|
|
243
|
+
const modal = document.querySelector('div[class*="second_verify_mask"]');
|
|
244
|
+
if (!modal) return 'modal_not_found';
|
|
245
|
+
const els = modal.querySelectorAll('div, span, button, a');
|
|
246
|
+
for (const el of els) {
|
|
247
|
+
const t = el.textContent.trim();
|
|
248
|
+
if (t === '验证' && el.offsetParent !== null) {
|
|
249
|
+
el.click(); return 'clicked';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return 'not_found';
|
|
253
|
+
}''')
|
|
254
|
+
time.sleep(1)
|
|
255
|
+
print(f'VERIFY_CLICKED|{result}', flush=True)
|
|
256
|
+
report('E_verify_clicked', result)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f'VERIFY_ERR|{e}', flush=True)
|
|
259
|
+
report('E_verify_clicked', f'error: {e}')
|
|
260
|
+
|
|
261
|
+
# ========== Stage 6: Login check (120s) ==========
|
|
262
|
+
print('POST_VERIFY|等待登录...', flush=True)
|
|
263
|
+
logged_in = False
|
|
264
|
+
for i in range(60):
|
|
265
|
+
time.sleep(2)
|
|
266
|
+
try:
|
|
267
|
+
cookies = context.cookies()
|
|
268
|
+
has_sid = any(c['name'] == 'sessionid' for c in cookies)
|
|
269
|
+
has_sid_ss = any(c['name'] == 'sessionid_ss' for c in cookies)
|
|
270
|
+
has_sid_g = any(c['name'] == 'sid_guard' for c in cookies)
|
|
271
|
+
ch = page.content().lower()
|
|
272
|
+
still_login = '扫码登录' in ch or '登录/注册' in ch
|
|
273
|
+
verify_open = '身份验证' in ch
|
|
274
|
+
print(f'POST|{i*2}s|url={page.url}|login_form={still_login}|verify={verify_open}|sid={has_sid}|sid_ss={has_sid_ss}|sid_g={has_sid_g}', flush=True)
|
|
275
|
+
if has_sid and has_sid_ss and not still_login:
|
|
276
|
+
logged_in = True
|
|
277
|
+
print(f'LOGIN_SUCCESS|sessionid出现在{i*2}s', flush=True)
|
|
278
|
+
break
|
|
279
|
+
# Check for console elements
|
|
280
|
+
if not still_login and not verify_open:
|
|
281
|
+
logged_in = True
|
|
282
|
+
print(f'LOGIN_SUCCESS|页面已进入控制台 at {i*2}s', flush=True)
|
|
283
|
+
break
|
|
284
|
+
except: pass
|
|
285
|
+
|
|
286
|
+
report('F_login_url', page.url)
|
|
287
|
+
try:
|
|
288
|
+
ch = page.content().lower()
|
|
289
|
+
report('F_login_form_still', '扫码登录' in ch or '登录/注册' in ch)
|
|
290
|
+
report('F_verify_still', '身份验证' in ch)
|
|
291
|
+
except: pass
|
|
292
|
+
|
|
293
|
+
cookies = context.cookies()
|
|
294
|
+
report('G_cookie_sessionid', any(c['name'] == 'sessionid' for c in cookies))
|
|
295
|
+
report('G_cookie_sessionid_ss', any(c['name'] == 'sessionid_ss' for c in cookies))
|
|
296
|
+
report('G_cookie_sid_guard', any(c['name'] == 'sid_guard' for c in cookies))
|
|
297
|
+
report('G_cookie_uid_tt', any(c['name'] == 'uid_tt' for c in cookies))
|
|
298
|
+
for ck in cookies:
|
|
299
|
+
if ck['name'] in ['sessionid', 'sessionid_ss', 'sid_guard', 'uid_tt', 'has_biz_token', 'passport_csrf_token']:
|
|
300
|
+
print(f'CK|name={ck["name"]}|domain={ck["domain"]}|httponly={ck.get("httpOnly")}|expires={ck.get("expires","session")}', flush=True)
|
|
301
|
+
|
|
302
|
+
# Save state
|
|
303
|
+
state = json.loads((PROFILE_DIR / 'state.json').read_text())
|
|
304
|
+
state['is_logged_in'] = logged_in
|
|
305
|
+
state['session_start_at'] = datetime.now(timezone.utc).isoformat() if logged_in else None
|
|
306
|
+
state['last_login_check'] = datetime.now(timezone.utc).isoformat()
|
|
307
|
+
state['last_activity'] = datetime.now(timezone.utc).isoformat()
|
|
308
|
+
(PROFILE_DIR / 'state.json').write_text(json.dumps(state, indent=2, ensure_ascii=False))
|
|
309
|
+
|
|
310
|
+
# Screenshot
|
|
311
|
+
try:
|
|
312
|
+
page.screenshot(path=FINAL_SCREENSHOT, full_page=True)
|
|
313
|
+
print(f'SCREENSHOT|{FINAL_SCREENSHOT}', flush=True)
|
|
314
|
+
report('K_screenshot', FINAL_SCREENSHOT)
|
|
315
|
+
except: pass
|
|
316
|
+
|
|
317
|
+
report('F_logged_in', logged_in)
|
|
318
|
+
|
|
319
|
+
# ========== Stage 7: Graceful close ==========
|
|
320
|
+
if logged_in:
|
|
321
|
+
print('LOGIN_OK|正常关闭浏览器保存数据...', flush=True)
|
|
322
|
+
try:
|
|
323
|
+
context.close()
|
|
324
|
+
print('CLOSED|浏览器已正常关闭', flush=True)
|
|
325
|
+
report('H_browser_closed', True)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f'CLOSE_ERR|{e}', flush=True)
|
|
328
|
+
report('H_browser_closed', f'error: {e}')
|
|
329
|
+
time.sleep(10)
|
|
330
|
+
print('SLEEP_DONE|等待10秒落盘完成', flush=True)
|
|
331
|
+
|
|
332
|
+
# ========== Stage 8: Re-launch for persistence ==========
|
|
333
|
+
print('VERIFY_PERSISTENCE|重新打开验证免登录...', flush=True)
|
|
334
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
335
|
+
# Re-acquire lock
|
|
336
|
+
TASK_ID2 = f'persist_{ALIAS}_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
|
337
|
+
LOCK_FILE.write_text(json.dumps(
|
|
338
|
+
{'task_id': TASK_ID2, 'locked_at': datetime.now(timezone.utc).isoformat(),
|
|
339
|
+
'pid': os.getpid(), 'manual_control': True}, indent=2))
|
|
340
|
+
print(f'LOCK2|{TASK_ID2}', flush=True)
|
|
341
|
+
|
|
342
|
+
context2 = launch_persistent_context(
|
|
343
|
+
user_data_dir=BROWSER_DATA,
|
|
344
|
+
headless=False, stealth_args=True, viewport=fp['viewport'],
|
|
345
|
+
locale=fp['locale'], timezone=fp['timezone'], humanize=False, geoip=False,
|
|
346
|
+
)
|
|
347
|
+
page2 = context2.pages[0] if context2.pages else context2.new_page()
|
|
348
|
+
page2.goto('https://creator.douyin.com/', wait_until='domcontentloaded', timeout=30000)
|
|
349
|
+
time.sleep(5)
|
|
350
|
+
print(f'VERIFY_URL|{page2.url}', flush=True)
|
|
351
|
+
report('J_reopen_url', page2.url)
|
|
352
|
+
ch2 = page2.content().lower()
|
|
353
|
+
persisted = '扫码登录' not in ch2 and '登录/注册' not in ch2
|
|
354
|
+
print(f'PERSISTED|{persisted}', flush=True)
|
|
355
|
+
report('J_persisted', persisted)
|
|
356
|
+
cookies2 = context2.cookies()
|
|
357
|
+
report('J_reopen_has_sessionid', any(c['name'] == 'sessionid' for c in cookies2))
|
|
358
|
+
try:
|
|
359
|
+
page2.screenshot(path=PERSISTENCE_SCREENSHOT, full_page=True)
|
|
360
|
+
print(f'VERIFY_SS|{PERSISTENCE_SCREENSHOT}', flush=True)
|
|
361
|
+
report('K_persistence_screenshot', PERSISTENCE_SCREENSHOT)
|
|
362
|
+
except: pass
|
|
363
|
+
context2.close()
|
|
364
|
+
print('VERIFY_DONE', flush=True)
|
|
365
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
366
|
+
report('I_lock_released', True)
|
|
367
|
+
else:
|
|
368
|
+
print('LOGIN_FAILED|保持浏览器打开30秒后关闭...', flush=True)
|
|
369
|
+
report('H_browser_closed', False)
|
|
370
|
+
for i in range(30):
|
|
371
|
+
time.sleep(1)
|
|
372
|
+
if (BASE / 'locks' / f'{ALIAS}.close').exists():
|
|
373
|
+
break
|
|
374
|
+
try:
|
|
375
|
+
context.close()
|
|
376
|
+
print('CLOSED', flush=True)
|
|
377
|
+
except: pass
|
|
378
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
379
|
+
report('I_lock_released', True)
|
|
380
|
+
|
|
381
|
+
# ========== Final report ==========
|
|
382
|
+
print('='*60, flush=True)
|
|
383
|
+
print('FINAL_REPORT', flush=True)
|
|
384
|
+
print('='*60, flush=True)
|
|
385
|
+
for k, v in sorted(R.items()):
|
|
386
|
+
print(f' {k}: {v}', flush=True)
|
|
387
|
+
print('='*60, flush=True)
|
|
388
|
+
print('DONE', flush=True)
|
|
389
|
+
|
|
390
|
+
if __name__ == '__main__':
|
|
391
|
+
main()
|