@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,70 @@
1
+ #!/usr/bin/env python3
2
+ """Bounded request-correlated IPC client for the Toutiao controller."""
3
+
4
+ from __future__ import annotations
5
+ from media_agent.runtime.paths import runtime_home
6
+
7
+ import argparse
8
+ import json
9
+ import socket
10
+ import uuid
11
+ from pathlib import Path
12
+
13
+
14
+ def send_command(base: Path, profile_id: str, action: str, *, timeout: float = 10.0, **data) -> dict:
15
+ request_id = uuid.uuid4().hex
16
+ socket_path = base / "controllers" / f"{profile_id}.sock"
17
+ if not socket_path.exists():
18
+ return {"state": "CONTROLLER_NOT_FOUND", "request_id": request_id, "profile_id": profile_id}
19
+ request = {"profile_id": profile_id, "request_id": request_id, "action": action, **data}
20
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
21
+ client.settimeout(timeout)
22
+ try:
23
+ client.connect(str(socket_path))
24
+ client.sendall(json.dumps(request, ensure_ascii=False).encode())
25
+ try:
26
+ client.shutdown(socket.SHUT_WR)
27
+ except OSError:
28
+ # 半关闭只是 EOF 提示;请求已完整发送,对端先关闭时(如 macOS 偶发
29
+ # ENOTCONN)不得把整个请求判为失败,继续读取已缓冲的响应。
30
+ pass
31
+ chunks = []
32
+ while True:
33
+ chunk = client.recv(4096)
34
+ if not chunk:
35
+ break
36
+ chunks.append(chunk)
37
+ if not chunks:
38
+ return {"state": "IPC_TIMEOUT", "request_id": request_id, "profile_id": profile_id}
39
+ response = json.loads(b"".join(chunks).decode())
40
+ if response.get("request_id") != request_id:
41
+ return {"state": "STALE_RESPONSE", "request_id": request_id, "profile_id": profile_id}
42
+ return response
43
+ except (socket.timeout, TimeoutError):
44
+ return {"state": "IPC_TIMEOUT", "request_id": request_id, "profile_id": profile_id}
45
+ except (ConnectionRefusedError, ConnectionResetError, BrokenPipeError):
46
+ return {"state": "CONTROLLER_DEAD", "request_id": request_id, "profile_id": profile_id}
47
+ except OSError as exc:
48
+ return {"state": "IPC_ERROR", "request_id": request_id, "profile_id": profile_id,
49
+ "error": str(exc)[:120]}
50
+ finally:
51
+ client.close()
52
+
53
+
54
+ def main() -> int:
55
+ parser = argparse.ArgumentParser()
56
+ parser.add_argument("profile_id")
57
+ parser.add_argument("action", choices=("status", "scan-done", "verify-account", "refresh-qr", "close"))
58
+ parser.add_argument("--expected-account-name")
59
+ parser.add_argument("--base", type=Path, default=runtime_home())
60
+ args = parser.parse_args()
61
+ payload = {}
62
+ if args.expected_account_name is not None:
63
+ payload["expected_account_name"] = args.expected_account_name
64
+ result = send_command(args.base, args.profile_id, args.action.replace("-", "_"), **payload)
65
+ print(json.dumps(result, ensure_ascii=False, indent=2))
66
+ return 0 if result.get("state") not in {"CONTROLLER_NOT_FOUND", "CONTROLLER_DEAD", "IPC_TIMEOUT", "IPC_ERROR"} else 1
67
+
68
+
69
+ if __name__ == "__main__":
70
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Media Agent capabilities."""
@@ -0,0 +1,189 @@
1
+ """Legacy operation, executed only through main()."""
2
+
3
+ def main():
4
+ #!/usr/bin/env python3
5
+ """Xiaohongshu Creator login check. Usage: python3 xiaohongshu_check_login.py <profile_id>"""
6
+ from media_agent.runtime.paths import runtime_home
7
+ import json, time, os, sys
8
+ from pathlib import Path
9
+ from datetime import datetime, timezone
10
+ from media_agent.runtime.browser import launch_persistent_context
11
+ from media_agent.platforms.xiaohongshu.xiaohongshu_login_evidence import classify_login_evidence
12
+
13
+ BASE = runtime_home()
14
+ ALIAS = sys.argv[1] if len(sys.argv) > 1 else sys.exit(1)
15
+
16
+ PROFILE_DIR = BASE / 'profiles' / ALIAS
17
+ LOCK_FILE = BASE / 'locks' / f'{ALIAS}.lock'
18
+
19
+ if not PROFILE_DIR.exists():
20
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'unknown', 'error': 'PROFILE_NOT_FOUND'}, indent=2))
21
+ sys.exit(5)
22
+
23
+ cfg = json.loads((PROFILE_DIR / 'config.json').read_text())
24
+ state = json.loads((PROFILE_DIR / 'state.json').read_text()) if (PROFILE_DIR / 'state.json').exists() else {}
25
+
26
+ LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
27
+ if LOCK_FILE.exists():
28
+ try:
29
+ os.kill(json.loads(LOCK_FILE.read_text()).get('pid', 0), 0)
30
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'locked', 'lock_status': 'locked'}, indent=2))
31
+ sys.exit(3)
32
+ except:
33
+ LOCK_FILE.unlink()
34
+ LOCK_FILE.write_text(json.dumps({'task_id': 'xhs-check-login', 'pid': os.getpid(), 'time': datetime.now().isoformat()}, indent=2))
35
+
36
+ ctx = None
37
+ try:
38
+ ctx = launch_persistent_context(
39
+ user_data_dir=str(PROFILE_DIR / 'browser_data'),
40
+ headless=False, stealth_args=True,
41
+ viewport=cfg['fingerprint']['viewport'],
42
+ locale=cfg['fingerprint']['locale'],
43
+ timezone=cfg['fingerprint']['timezone'],
44
+ humanize=False, geoip=False
45
+ )
46
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
47
+ page.goto('https://creator.xiaohongshu.com/', wait_until='load', timeout=60000)
48
+ time.sleep(5)
49
+
50
+ url = page.url
51
+ title = page.title()
52
+
53
+ dom = page.evaluate('''() => {
54
+ const result = {};
55
+ const all = document.querySelectorAll('a, div, span, li, nav, header, aside');
56
+
57
+ // Dashboard indicators
58
+ const dashboardKeywords = ['创作服务平台', '首页', '笔记管理', '数据看板', '创作中心', '发布笔记', '我的笔记'];
59
+ const foundDashboard = [];
60
+ const seenDash = new Set();
61
+ for (const el of all) {
62
+ if (el.offsetParent === null) continue;
63
+ const t = el.textContent.trim();
64
+ for (const kw of dashboardKeywords) {
65
+ if (t.includes(kw) && !seenDash.has(kw)) { seenDash.add(kw); foundDashboard.push(kw); }
66
+ }
67
+ }
68
+ const hasHomeRoute = window.location.href.includes('/new/home') || window.location.href.includes('/creator');
69
+ const hasAuthRoute = !window.location.href.includes('/login');
70
+ result.dashboard_detected = foundDashboard.length >= 2 || (foundDashboard.length >= 1 && hasHomeRoute && hasAuthRoute);
71
+ result.dashboard_evidence = foundDashboard;
72
+ if (hasHomeRoute) result.dashboard_evidence.push('backend_route');
73
+
74
+ // Login form indicators
75
+ const loginKeywords = ['短信登录', 'APP扫一扫登录', '扫码登录', '手机号'];
76
+ const foundLogin = []; const seenLogin = new Set();
77
+ for (const el of all) {
78
+ if (el.offsetParent === null) continue;
79
+ const t = el.textContent.trim();
80
+ for (const kw of loginKeywords) {
81
+ if (t.includes(kw) && !seenLogin.has(kw)) { seenLogin.add(kw); foundLogin.push(kw); }
82
+ }
83
+ }
84
+ const loginInputs = document.querySelectorAll('input[type="tel"], input[placeholder*="手机"]');
85
+ const hasLoginInputs = Array.from(loginInputs).some(el => el.offsetParent !== null);
86
+ result.login_form_detected = foundLogin.length > 0 || hasLoginInputs;
87
+ result.login_form_evidence = foundLogin;
88
+ if (hasLoginInputs) result.login_form_evidence.push('login_input_visible');
89
+
90
+ // QR code detection
91
+ const qrImgs = document.querySelectorAll('img, canvas');
92
+ let hasQrImg = false;
93
+ for (const el of qrImgs) {
94
+ if (el.offsetParent === null) continue;
95
+ const rect = el.getBoundingClientRect();
96
+ if (Math.abs(rect.width - rect.height) < 30 && rect.width >= 100 && rect.width < 400) { hasQrImg = true; break; }
97
+ }
98
+ let hasQrText = false;
99
+ for (const el of document.querySelectorAll('div, span')) {
100
+ if (el.offsetParent === null) continue;
101
+ const t = el.textContent.trim();
102
+ if ((t.includes('扫码') || t.includes('二维码') || t.includes('小红书扫码')) && t.length < 30) { hasQrText = true; break; }
103
+ }
104
+ result.qr_detected = hasQrImg || hasQrText;
105
+ result.qr_evidence = [];
106
+ if (hasQrImg) result.qr_evidence.push('qr_image_visible');
107
+ if (hasQrText) result.qr_evidence.push('qr_text_visible');
108
+
109
+ // Account name: use structural selectors only; never hard-code an account.
110
+ let accountName = '';
111
+ const accountSelectors = [
112
+ '[class*="user-name"]',
113
+ '[class*="userName"]',
114
+ '[class*="nickname"]',
115
+ '[class*="nick-name"]',
116
+ '[class*="account-name"]',
117
+ 'header [class*="name"]'
118
+ ];
119
+ for (const selector of accountSelectors) {
120
+ for (const el of document.querySelectorAll(selector)) {
121
+ if (el.offsetParent === null) continue;
122
+ const t = el.textContent.trim();
123
+ if (t.length >= 2 && t.length <= 40 && !dashboardKeywords.includes(t)) {
124
+ accountName = t;
125
+ break;
126
+ }
127
+ }
128
+ if (accountName) break;
129
+ }
130
+ result.account_name = accountName || 'unknown';
131
+
132
+ return result;
133
+ }''')
134
+
135
+ login_form = dom['login_form_detected']
136
+ qr = dom['qr_detected']
137
+ status = classify_login_evidence(
138
+ url=url,
139
+ dashboard_evidence=dom['dashboard_evidence'],
140
+ login_form_detected=login_form,
141
+ qr_detected=qr,
142
+ )
143
+ dashboard = status == 'logged_in' or dom['dashboard_detected']
144
+
145
+ ctx.close()
146
+ LOCK_FILE.unlink(missing_ok=True)
147
+
148
+ # Update state
149
+ site_state = state.setdefault('xiaohongshu', {})
150
+ site_state['login_status'] = status
151
+ site_state['checked_at'] = datetime.now(timezone.utc).isoformat()
152
+ site_state['dashboard_detected'] = dashboard
153
+ site_state['login_form_detected'] = login_form
154
+ site_state['qr_detected'] = qr
155
+ (PROFILE_DIR / 'state.json').write_text(json.dumps(state, indent=2))
156
+
157
+ exit_code = 0 if status == 'logged_in' else (2 if status == 'login_required' else 7)
158
+
159
+ output = {
160
+ 'profile_id': ALIAS,
161
+ 'site': 'xiaohongshu',
162
+ 'login_status': status,
163
+ 'current_url': url[:120],
164
+ 'page_title': title,
165
+ 'dashboard_detected': dashboard,
166
+ 'dashboard_evidence': dom['dashboard_evidence'],
167
+ 'login_form_detected': login_form,
168
+ 'login_form_evidence': dom['login_form_evidence'],
169
+ 'qr_detected': qr,
170
+ 'qr_evidence': dom['qr_evidence'],
171
+ 'account_name': dom['account_name'],
172
+ 'browser_closed': True,
173
+ 'lock_released': True,
174
+ }
175
+ print(json.dumps(output, indent=2, ensure_ascii=False))
176
+ sys.exit(exit_code)
177
+ except Exception as e:
178
+ try:
179
+ if ctx:
180
+ ctx.close()
181
+ except Exception:
182
+ pass
183
+ LOCK_FILE.unlink(missing_ok=True)
184
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'check_failed', 'error': str(e)[:100]}, indent=2))
185
+ sys.exit(1)
186
+
187
+
188
+ if __name__ == '__main__':
189
+ main()