@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,128 @@
|
|
|
1
|
+
"""Legacy operation, executed only through main()."""
|
|
2
|
+
|
|
3
|
+
def main():
|
|
4
|
+
#!/usr/bin/env python3
|
|
5
|
+
"""Enterprise login flow. Usage: python3 enterprise_login.py <profile_id> [--site creator|leads]"""
|
|
6
|
+
from media_agent.runtime.paths import runtime_home
|
|
7
|
+
import json, time, sys, os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime, timezone, timedelta
|
|
10
|
+
from media_agent.runtime.browser import launch_persistent_context
|
|
11
|
+
|
|
12
|
+
BASE = runtime_home()
|
|
13
|
+
ALIAS = sys.argv[1] if len(sys.argv) > 1 else sys.exit(1)
|
|
14
|
+
SITE = 'leads' # default
|
|
15
|
+
for i, a in enumerate(sys.argv):
|
|
16
|
+
if a == '--site' and i + 1 < len(sys.argv):
|
|
17
|
+
SITE = sys.argv[i + 1]; break
|
|
18
|
+
|
|
19
|
+
PROFILE_DIR = BASE / 'profiles' / ALIAS
|
|
20
|
+
BROWSER_DATA = str(PROFILE_DIR / 'browser_data')
|
|
21
|
+
LOCK_FILE = BASE / 'locks' / f'{ALIAS}.lock'
|
|
22
|
+
SCREENSHOTS_DIR = BASE / 'screenshots'
|
|
23
|
+
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
# ===== Lock =====
|
|
26
|
+
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
if LOCK_FILE.exists():
|
|
28
|
+
try:
|
|
29
|
+
d = json.loads(LOCK_FILE.read_text()); pid = d.get('pid')
|
|
30
|
+
if pid:
|
|
31
|
+
try: os.kill(pid, 0); print(f'STATUS|PROFILE_LOCKED|pid={pid}', flush=True); sys.exit(3)
|
|
32
|
+
except OSError: LOCK_FILE.unlink()
|
|
33
|
+
except: LOCK_FILE.unlink()
|
|
34
|
+
|
|
35
|
+
TASK_ID = f'login_{SITE}_{ALIAS}_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
|
36
|
+
LOCK_FILE.write_text(json.dumps({'task_id': TASK_ID, 'locked_at': datetime.now(timezone.utc).isoformat(), 'pid': os.getpid(), 'site': SITE}, indent=2))
|
|
37
|
+
print(f'LOCK|acquired|{TASK_ID}', flush=True)
|
|
38
|
+
|
|
39
|
+
# ===== Launch =====
|
|
40
|
+
cfg = json.loads((PROFILE_DIR / 'config.json').read_text())
|
|
41
|
+
sites = cfg.get('sites', {})
|
|
42
|
+
site_cfg = sites.get(SITE, {})
|
|
43
|
+
target_url = site_cfg.get('target_url', cfg['account'].get('target_url', 'https://leads.cluerich.com/pc/auth/login'))
|
|
44
|
+
|
|
45
|
+
ctx = launch_persistent_context(
|
|
46
|
+
user_data_dir=BROWSER_DATA, headless=False, stealth_args=True,
|
|
47
|
+
viewport=cfg['fingerprint']['viewport'],
|
|
48
|
+
locale=cfg['fingerprint']['locale'], timezone=cfg['fingerprint']['timezone'],
|
|
49
|
+
humanize=cfg.get('humanize', True), geoip=False,
|
|
50
|
+
)
|
|
51
|
+
print('LAUNCH|browser started', flush=True)
|
|
52
|
+
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
|
53
|
+
|
|
54
|
+
# ===== Navigate =====
|
|
55
|
+
page.goto(target_url, wait_until='load', timeout=60000)
|
|
56
|
+
time.sleep(5)
|
|
57
|
+
print(f'URL|{page.url}', flush=True)
|
|
58
|
+
print(f'SITE|{SITE}', flush=True)
|
|
59
|
+
print(f'TITLE|{page.title()}', flush=True)
|
|
60
|
+
|
|
61
|
+
# ===== Screenshot =====
|
|
62
|
+
ss = str(SCREENSHOTS_DIR / f'{ALIAS}_{SITE}_login_page.png')
|
|
63
|
+
page.screenshot(path=ss, full_page=True)
|
|
64
|
+
print(f'SCREENSHOT|{ss}', flush=True)
|
|
65
|
+
|
|
66
|
+
# ===== Check for QR code =====
|
|
67
|
+
qr_path = str(SCREENSHOTS_DIR / f'{ALIAS}_{SITE}_qr.png')
|
|
68
|
+
qr_found = False
|
|
69
|
+
for i in range(10):
|
|
70
|
+
try:
|
|
71
|
+
for div in page.query_selector_all('div'):
|
|
72
|
+
box = div.bounding_box()
|
|
73
|
+
if box and 150 < box['width'] < 350 and 150 < box['height'] < 350:
|
|
74
|
+
div.screenshot(path=qr_path)
|
|
75
|
+
if Path(qr_path).exists() and Path(qr_path).stat().st_size > 2000:
|
|
76
|
+
print(f'QR_OK|{qr_path}|size={Path(qr_path).stat().st_size}', flush=True)
|
|
77
|
+
qr_found = True; break
|
|
78
|
+
except: pass
|
|
79
|
+
if qr_found: break
|
|
80
|
+
time.sleep(1)
|
|
81
|
+
|
|
82
|
+
if not qr_found:
|
|
83
|
+
page.screenshot(path=qr_path, full_page=True)
|
|
84
|
+
print(f'QR_FALLBACK|{qr_path}', flush=True)
|
|
85
|
+
|
|
86
|
+
# ===== Output state =====
|
|
87
|
+
print(f'STATE|WAIT_QR_SCAN', flush=True)
|
|
88
|
+
print(f'SUMMARY|task_id={TASK_ID}|profile_id={ALIAS}|site={SITE}|state=WAIT_QR_SCAN|url={page.url}|browser=open|lock=locked|next_action=wait_user_scan', flush=True)
|
|
89
|
+
|
|
90
|
+
# ===== Wait for user scan =====
|
|
91
|
+
SIG = BASE / 'locks' / f'{ALIAS}.continue'
|
|
92
|
+
print(f'WAIT|等待用户扫码...', flush=True)
|
|
93
|
+
for i in range(600):
|
|
94
|
+
time.sleep(2)
|
|
95
|
+
if SIG.exists():
|
|
96
|
+
SIG.unlink(missing_ok=True); print(f'SCAN_DONE', flush=True); break
|
|
97
|
+
if i % 15 == 0: print(f'WAITING|{i*2}s', flush=True)
|
|
98
|
+
|
|
99
|
+
# ===== After scan =====
|
|
100
|
+
time.sleep(5)
|
|
101
|
+
print(f'URL_AFTER_SCAN|{page.url}', flush=True)
|
|
102
|
+
print(f'TITLE|{page.title()}', flush=True)
|
|
103
|
+
|
|
104
|
+
body = page.content().lower()
|
|
105
|
+
if '运营工作台' in body or '数据分析' in body or '线索管理' in body or 'creator-micro' in page.url:
|
|
106
|
+
print('LOGIN_SUCCESS|进入后台', flush=True)
|
|
107
|
+
else:
|
|
108
|
+
print('LOGIN_CHECK|需进一步验证', flush=True)
|
|
109
|
+
|
|
110
|
+
ss2 = str(SCREENSHOTS_DIR / f'{ALIAS}_{SITE}_post_scan.png')
|
|
111
|
+
page.screenshot(path=ss2, full_page=True)
|
|
112
|
+
print(f'SCREENSHOT|{ss2}', flush=True)
|
|
113
|
+
|
|
114
|
+
# Keep open for manual inspection
|
|
115
|
+
print('KA|保持打开,等待.close信号', flush=True)
|
|
116
|
+
for i in range(600):
|
|
117
|
+
time.sleep(2)
|
|
118
|
+
if (BASE / 'locks' / f'{ALIAS}.close').exists(): break
|
|
119
|
+
|
|
120
|
+
# Close
|
|
121
|
+
ctx.close()
|
|
122
|
+
print('CLOSED', flush=True)
|
|
123
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
124
|
+
print('LOCK_RELEASED', flush=True)
|
|
125
|
+
print('DONE', flush=True)
|
|
126
|
+
|
|
127
|
+
if __name__ == '__main__':
|
|
128
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Legacy operation, executed only through main()."""
|
|
2
|
+
|
|
3
|
+
def main():
|
|
4
|
+
#!/usr/bin/env python3
|
|
5
|
+
"""Export short video detail data. Usage: python3 export_short_video.py <profile_id> <date>"""
|
|
6
|
+
from media_agent.runtime.paths import runtime_home
|
|
7
|
+
import json, time, os, sys, re, shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime, timezone, timedelta
|
|
10
|
+
from media_agent.runtime.browser import launch_persistent_context
|
|
11
|
+
|
|
12
|
+
BASE = runtime_home()
|
|
13
|
+
ALIAS = sys.argv[1] if len(sys.argv) > 1 else sys.exit(1)
|
|
14
|
+
TARGET_DATE = sys.argv[2] if len(sys.argv) > 2 else (datetime.now() - timedelta(1)).strftime('%Y-%m-%d')
|
|
15
|
+
|
|
16
|
+
PROFILE_DIR = BASE / 'profiles' / ALIAS
|
|
17
|
+
BROWSER_DATA = str(PROFILE_DIR / 'browser_data')
|
|
18
|
+
LOCK_FILE = BASE / 'locks' / f'{ALIAS}.lock'
|
|
19
|
+
|
|
20
|
+
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
if LOCK_FILE.exists():
|
|
22
|
+
try:
|
|
23
|
+
os.kill(json.loads(LOCK_FILE.read_text()).get('pid', 0), 0)
|
|
24
|
+
print('PROFILE_LOCKED'); sys.exit(3)
|
|
25
|
+
except:
|
|
26
|
+
LOCK_FILE.unlink()
|
|
27
|
+
LOCK_FILE.write_text(json.dumps({'task_id': 'export-short-video', 'pid': os.getpid()}, indent=2))
|
|
28
|
+
|
|
29
|
+
cfg = json.loads((PROFILE_DIR / 'config.json').read_text())
|
|
30
|
+
ctx = launch_persistent_context(
|
|
31
|
+
user_data_dir=BROWSER_DATA, headless=False, stealth_args=True,
|
|
32
|
+
viewport=cfg['fingerprint']['viewport'],
|
|
33
|
+
locale=cfg['fingerprint']['locale'],
|
|
34
|
+
timezone=cfg['fingerprint']['timezone'],
|
|
35
|
+
humanize=False, geoip=False
|
|
36
|
+
)
|
|
37
|
+
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
|
38
|
+
page.goto('https://leads.cluerich.com/pc/analysis/short-video/list', wait_until='load', timeout=60000)
|
|
39
|
+
time.sleep(8)
|
|
40
|
+
|
|
41
|
+
body = page.content().lower()
|
|
42
|
+
if not any(k in body for k in ['运营工作台', '数据分析']):
|
|
43
|
+
print('LOGIN_REQUIRED'); ctx.close(); LOCK_FILE.unlink(); sys.exit(2)
|
|
44
|
+
print('LOGIN_OK', flush=True)
|
|
45
|
+
|
|
46
|
+
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
47
|
+
OUT_DIR = BASE / 'tasks' / 'douyin_enterprise_short_video' / ts
|
|
48
|
+
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
page.evaluate('window.scrollTo(0, 1600)')
|
|
51
|
+
time.sleep(2)
|
|
52
|
+
|
|
53
|
+
export_btn = page.locator('button:has-text("导出数据")').first
|
|
54
|
+
if export_btn.count() > 0:
|
|
55
|
+
with page.expect_download(timeout=60000) as dl_info:
|
|
56
|
+
export_btn.click()
|
|
57
|
+
download = dl_info.value
|
|
58
|
+
fname = download.suggested_filename
|
|
59
|
+
fpath = OUT_DIR / fname
|
|
60
|
+
download.save_as(str(fpath))
|
|
61
|
+
print(f'DOWNLOADED|{fpath}|{fpath.stat().st_size} bytes', flush=True)
|
|
62
|
+
else:
|
|
63
|
+
print('EXPORT_BUTTON_NOT_FOUND', flush=True)
|
|
64
|
+
|
|
65
|
+
ctx.close()
|
|
66
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
67
|
+
print('DONE', flush=True)
|
|
68
|
+
|
|
69
|
+
if __name__ == '__main__':
|
|
70
|
+
main()
|