@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.
Files changed (96) hide show
  1. package/README.md +90 -0
  2. package/SHA256SUMS +94 -0
  3. package/bin/media-agent.mjs +89 -0
  4. package/docs/guides/capabilities.md +103 -0
  5. package/docs/guides/image-text-publishing.md +81 -0
  6. package/docs/guides/installation.md +103 -0
  7. package/docs/guides/runtime.md +13 -0
  8. package/manifest.json +382 -0
  9. package/package.json +42 -0
  10. package/pyproject.toml +20 -0
  11. package/resources/capabilities.json +34 -0
  12. package/resources/configs/accounts.yaml +21 -0
  13. package/resources/configs/profile.template.json +20 -0
  14. package/resources/configs/ranking-profiles.yaml +21 -0
  15. package/resources/configs/toutiao-profile.template.json +22 -0
  16. package/resources/configs/xiaohongshu-profile.template.json +27 -0
  17. package/resources/data/industry-taxonomy.yaml +326 -0
  18. package/skills/douyin-competitor-collect/SKILL.md +177 -0
  19. package/skills/douyin-competitor-collect/agents/openai.yaml +4 -0
  20. package/skills/douyin-competitor-collect/references/output-schema.md +178 -0
  21. package/skills/douyin-creator-image-text-publish/SKILL.md +44 -0
  22. package/skills/douyin-creator-image-text-publish/agents/openai.yaml +4 -0
  23. package/skills/douyin-creator-image-text-publish/references/LICENSE.social-auto-upload +21 -0
  24. package/skills/douyin-creator-image-text-publish/references/execution-contract.md +46 -0
  25. package/skills/douyin-creator-image-text-publish/references/upstream.md +30 -0
  26. package/skills/douyin-creator-index/SKILL.md +32 -0
  27. package/skills/douyin-creator-index/agents/openai.yaml +4 -0
  28. package/skills/douyin-creator-login/SKILL.md +58 -0
  29. package/skills/douyin-creator-login/agents/openai.yaml +4 -0
  30. package/skills/douyin-creator-publish/SKILL.md +56 -0
  31. package/skills/douyin-creator-publish/agents/openai.yaml +4 -0
  32. package/skills/douyin-enterprise-leads/SKILL.md +26 -0
  33. package/skills/douyin-enterprise-leads/agents/openai.yaml +4 -0
  34. package/skills/douyin-enterprise-leads-login/SKILL.md +36 -0
  35. package/skills/douyin-enterprise-leads-login/agents/openai.yaml +4 -0
  36. package/skills/douyin-enterprise-short-video-export/SKILL.md +29 -0
  37. package/skills/douyin-enterprise-short-video-export/agents/openai.yaml +4 -0
  38. package/skills/douyin-enterprise-video-rankings/SKILL.md +51 -0
  39. package/skills/douyin-enterprise-video-rankings/agents/openai.yaml +4 -0
  40. package/skills/douyin-enterprise-video-rankings/references/industry-taxonomy.md +29 -0
  41. package/skills/douyin-web-login/SKILL.md +102 -0
  42. package/skills/douyin-web-login/agents/openai.yaml +4 -0
  43. package/skills/toutiao-creator-article-draft/SKILL.md +154 -0
  44. package/skills/toutiao-creator-article-draft/agents/openai.yaml +4 -0
  45. package/skills/toutiao-web-login/SKILL.md +96 -0
  46. package/skills/toutiao-web-login/agents/openai.yaml +4 -0
  47. package/skills/xiaohongshu-creator-image-text-publish/SKILL.md +46 -0
  48. package/skills/xiaohongshu-creator-image-text-publish/agents/openai.yaml +4 -0
  49. package/skills/xiaohongshu-creator-image-text-publish/references/LICENSE.social-auto-upload +21 -0
  50. package/skills/xiaohongshu-creator-image-text-publish/references/execution-contract.md +46 -0
  51. package/skills/xiaohongshu-creator-image-text-publish/references/upstream.md +31 -0
  52. package/skills/xiaohongshu-creator-login/SKILL.md +106 -0
  53. package/skills/xiaohongshu-creator-login/agents/openai.yaml +4 -0
  54. package/skills/xiaohongshu-creator-publish/SKILL.md +153 -0
  55. package/skills/xiaohongshu-creator-publish/agents/openai.yaml +4 -0
  56. package/src/media_agent/__init__.py +1 -0
  57. package/src/media_agent/cli.py +28 -0
  58. package/src/media_agent/commands.sh +436 -0
  59. package/src/media_agent/platforms/__init__.py +1 -0
  60. package/src/media_agent/platforms/douyin/__init__.py +1 -0
  61. package/src/media_agent/platforms/douyin/check_login.py +196 -0
  62. package/src/media_agent/platforms/douyin/collect_industry_taxonomy.py +184 -0
  63. package/src/media_agent/platforms/douyin/collect_video_rankings.py +352 -0
  64. package/src/media_agent/platforms/douyin/douyin_full_login.py +391 -0
  65. package/src/media_agent/platforms/douyin/douyin_hotspot_v2.py +454 -0
  66. package/src/media_agent/platforms/douyin/douyin_publish.py +1135 -0
  67. package/src/media_agent/platforms/douyin/enterprise_login.py +128 -0
  68. package/src/media_agent/platforms/douyin/export_short_video.py +70 -0
  69. package/src/media_agent/platforms/douyin/login_controller.py +508 -0
  70. package/src/media_agent/platforms/douyin/validate_industry_taxonomy.py +152 -0
  71. package/src/media_agent/platforms/douyin/validate_rankings.py +254 -0
  72. package/src/media_agent/platforms/toutiao/__init__.py +1 -0
  73. package/src/media_agent/platforms/toutiao/toutiao_check_login.py +54 -0
  74. package/src/media_agent/platforms/toutiao/toutiao_login_controller.py +250 -0
  75. package/src/media_agent/platforms/toutiao/toutiao_login_evidence.py +50 -0
  76. package/src/media_agent/platforms/toutiao/toutiao_login_ipc.py +70 -0
  77. package/src/media_agent/platforms/xiaohongshu/__init__.py +1 -0
  78. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_check_login.py +189 -0
  79. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_controller.py +449 -0
  80. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_evidence.py +64 -0
  81. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_ipc.py +120 -0
  82. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_publish.py +925 -0
  83. package/src/media_agent/runtime/__init__.py +1 -0
  84. package/src/media_agent/runtime/account_manager.py +672 -0
  85. package/src/media_agent/runtime/browser.py +5 -0
  86. package/src/media_agent/runtime/paths.py +7 -0
  87. package/src/media_agent/script_map.json +23 -0
  88. package/src/node/config.mjs +48 -0
  89. package/src/node/integrity.mjs +34 -0
  90. package/src/node/skills.mjs +85 -0
  91. package/tools/archive_releases.py +83 -0
  92. package/tools/artifacts.py +56 -0
  93. package/tools/build_release.py +82 -0
  94. package/tools/check_catalog.py +23 -0
  95. package/tools/install_runtime.py +144 -0
  96. 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()