@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,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()
|