@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,436 @@
1
+ #!/bin/bash
2
+ # 社交媒体账号管理 CLI v3.3
3
+ set -e
4
+
5
+ SCRIPT_DIR="${MEDIA_AGENT_HOME:-$HOME/.media-agent/social-accounts}"
6
+ export MEDIA_AGENT_HOME="$SCRIPT_DIR"
7
+ PYTHON="${MEDIA_AGENT_PYTHON:-python3}"
8
+ run_script() { "$PYTHON" -m media_agent.cli run-script "$@"; }
9
+
10
+ require_profile() {
11
+ [[ "$1" =~ ^[A-Za-z0-9_-]+$ ]] || { echo "错误: 无效 profile_id"; exit 1; }
12
+ if [ -z "$1" ]; then echo "错误: 缺少 profile_id"; echo "用法: media-agent $CMD <profile_id> [args]"; exit 1; fi
13
+ }
14
+
15
+ check_capability() {
16
+ local pid="$1" cap="$2"
17
+ local has
18
+ has=$("$PYTHON" - "$SCRIPT_DIR/profiles/$pid/config.json" "$cap" <<'PY_CAP'
19
+ import json, sys
20
+ try:
21
+ cfg = json.load(open(sys.argv[1]))
22
+ caps = cfg.get('account', {}).get('capabilities', [])
23
+ if not caps:
24
+ atype = cfg.get('account', {}).get('account_type', '')
25
+ if atype == 'personal': caps = ['creator_index_collect']
26
+ elif atype == 'enterprise': caps = ['leads_export', 'leads_rankings', 'leads_taxonomy']
27
+ print('ok' if sys.argv[2] in caps else 'missing')
28
+ except (OSError, ValueError):
29
+ print('missing')
30
+ PY_CAP
31
+ )
32
+ if [ "$has" != "ok" ]; then echo "错误: $pid 缺少 capability: $cap"; exit 4; fi
33
+ }
34
+
35
+ require_toutiao_profile() {
36
+ local pid="$1" config="$SCRIPT_DIR/profiles/$1/config.json"
37
+ [ -f "$config" ] || { echo "错误: Profile 不存在: $pid"; exit 5; }
38
+ "$PYTHON" - "$config" <<'PY'
39
+ import json, sys
40
+ cfg=json.load(open(sys.argv[1]))
41
+ raise SystemExit(0 if cfg.get('account',{}).get('platform','').lower() == 'toutiao' else 4)
42
+ PY
43
+ rc=$?
44
+ [ $rc -eq 0 ] || { echo "错误: $pid 不是 toutiao Profile"; exit $rc; }
45
+ }
46
+
47
+ CMD="${1:---help}"; shift 2>/dev/null || true
48
+
49
+ case "$CMD" in
50
+ enterprise-login)
51
+ require_profile "$1"
52
+ [ -f "$SCRIPT_DIR/profiles/$1/config.json" ] || { echo "错误: Profile 不存在: $1"; exit 5; }
53
+ run_script enterprise_login.py "$@"
54
+ ;;
55
+ validate-rankings) run_script validate_rankings.py "$@" ;;
56
+ validate-industry-taxonomy) run_script validate_industry_taxonomy.py "$@" ;;
57
+
58
+ init) run_script account_manager.py init "$@" ;;
59
+ list|ls) run_script account_manager.py list ;;
60
+ status|st) require_profile "$1"; run_script account_manager.py status "$1" ;;
61
+ locks) run_script account_manager.py locks ;;
62
+
63
+ login-start)
64
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
65
+ run_script login_controller.py "$PROFILE_ID" &
66
+ disown
67
+ sleep 3
68
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"status"}'
69
+ ;;
70
+
71
+ login-status)
72
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
73
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"status"}'
74
+ ;;
75
+
76
+ login-scan-done)
77
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
78
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"scan_done"}'
79
+ ;;
80
+
81
+ login-submit-code)
82
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
83
+ run_script login_controller.py "$PROFILE_ID" --submit-code-stdin
84
+ ;;
85
+
86
+ login-close)
87
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
88
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"close"}'
89
+ sleep 3
90
+ rm -f "$SCRIPT_DIR/locks/${PROFILE_ID}.lock"
91
+ ;;
92
+
93
+ login-refresh-qr)
94
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
95
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"refresh_qr"}'
96
+ ;;
97
+
98
+ login-resend-code)
99
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
100
+ run_script login_controller.py "$PROFILE_ID" --send '{"action":"resend_code"}'
101
+ ;;
102
+
103
+ check-login)
104
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
105
+ SITE="all"
106
+ for i in $(seq 1 $#); do
107
+ if [ "${!i}" = "--site" ]; then idx=$((i+1)); SITE="${!idx}"; break; fi
108
+ done
109
+ run_script check_login.py "$PROFILE_ID" --site "$SITE"
110
+ ;;
111
+
112
+ toutiao-login-start)
113
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
114
+ mkdir -p "$SCRIPT_DIR/logs"
115
+ run_script toutiao_login_controller.py "$PROFILE_ID" \
116
+ >> "$SCRIPT_DIR/logs/${PROFILE_ID}_toutiao_controller.log" 2>&1 &
117
+ disown
118
+ for _ in $(seq 1 45); do
119
+ [ -S "$SCRIPT_DIR/controllers/${PROFILE_ID}.sock" ] && break
120
+ sleep 1
121
+ done
122
+ run_script toutiao_login_ipc.py "$PROFILE_ID" status --base "$SCRIPT_DIR"
123
+ ;;
124
+
125
+ toutiao-login-status)
126
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
127
+ run_script toutiao_login_ipc.py "$PROFILE_ID" status --base "$SCRIPT_DIR"
128
+ ;;
129
+
130
+ toutiao-login-scan-done)
131
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
132
+ shift
133
+ EXPECTED=""
134
+ while [[ $# -gt 0 ]]; do
135
+ case "$1" in --expected-account-name) EXPECTED="$2"; shift 2 ;; *) shift ;; esac
136
+ done
137
+ if [ -n "$EXPECTED" ]; then
138
+ run_script toutiao_login_ipc.py "$PROFILE_ID" scan-done --expected-account-name "$EXPECTED" --base "$SCRIPT_DIR"
139
+ else
140
+ run_script toutiao_login_ipc.py "$PROFILE_ID" scan-done --base "$SCRIPT_DIR"
141
+ fi
142
+ ;;
143
+
144
+ toutiao-login-verify-account)
145
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"; shift
146
+ [ "$1" = "--expected-account-name" ] && [ -n "$2" ] || { echo "错误: 缺少 --expected-account-name"; exit 1; }
147
+ run_script toutiao_login_ipc.py "$PROFILE_ID" verify-account --expected-account-name "$2" --base "$SCRIPT_DIR"
148
+ ;;
149
+
150
+ toutiao-login-refresh-qr)
151
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
152
+ run_script toutiao_login_ipc.py "$PROFILE_ID" refresh-qr --base "$SCRIPT_DIR"
153
+ ;;
154
+
155
+ toutiao-login-close)
156
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
157
+ run_script toutiao_login_ipc.py "$PROFILE_ID" close --base "$SCRIPT_DIR"
158
+ ;;
159
+
160
+ toutiao-check-login)
161
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; require_toutiao_profile "$PROFILE_ID"
162
+ run_script toutiao_check_login.py "$PROFILE_ID" --base "$SCRIPT_DIR"
163
+ ;;
164
+
165
+ close)
166
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
167
+ echo "关闭: $PROFILE_ID"
168
+ touch "$SCRIPT_DIR/locks/${PROFILE_ID}.close"
169
+ # Wait for browser to actually close (check with lsof)
170
+ for i in $(seq 1 30); do
171
+ if ! lsof -n 2>/dev/null | grep -q "$PROFILE_ID/browser_data"; then
172
+ break
173
+ fi
174
+ sleep 2
175
+ done
176
+ rm -f "$SCRIPT_DIR/locks/${PROFILE_ID}.lock" "$SCRIPT_DIR/locks/${PROFILE_ID}.close"
177
+ echo "已关闭并释放锁"
178
+ ;;
179
+
180
+ collect-douyin-index)
181
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_index_collect"
182
+ shift; LISTS="realtime,rising"; PAGES=3
183
+ while [[ $# -gt 0 ]]; do
184
+ case "$1" in --lists) LISTS="$2"; shift 2 ;; --pages) PAGES="$2"; shift 2 ;; *) shift ;; esac
185
+ done
186
+ run_script check_login.py "$PROFILE_ID" --site creator > /dev/null 2>&1
187
+ if [ $? -ne 0 ]; then echo "状态: LOGIN_REQUIRED"; exit 2; fi
188
+ run_script douyin_hotspot_v2.py
189
+ ;;
190
+
191
+ export-short-video)
192
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "leads_export"
193
+ shift; DATE=""
194
+ while [[ $# -gt 0 ]]; do
195
+ case "$1" in
196
+ --help) echo "export-short-video <profile_id> [--date YYYY-MM-DD]"; exit 0 ;;
197
+ --dry-run) echo "[DRY-RUN] export-short-video $PROFILE_ID"; exit 0 ;;
198
+ --date) DATE="$2"; shift 2 ;; *) shift ;;
199
+ esac
200
+ done
201
+ [ -z "$DATE" ] && DATE=$(date -v-1d +%Y-%m-%d 2>/dev/null || "$PYTHON" -c "from datetime import date,timedelta; print((date.today()-timedelta(1)).isoformat())")
202
+ run_script export_short_video.py "$PROFILE_ID" "$DATE"
203
+ ;;
204
+
205
+ collect-video-rankings)
206
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "leads_rankings"
207
+ shift; INDUSTRY_L1=""; INDUSTRY_L2=""; DATE=""; PAGES=2
208
+ while [[ $# -gt 0 ]]; do
209
+ case "$1" in
210
+ --help) echo "collect-video-rankings <profile_id> --industry-l1 <L1> --industry-l2 <L2>"; exit 0 ;;
211
+ --dry-run) echo "[DRY-RUN] collect-video-rankings $PROFILE_ID"; exit 0 ;;
212
+ --industry-l1) INDUSTRY_L1="$2"; shift 2 ;; --industry-l2) INDUSTRY_L2="$2"; shift 2 ;;
213
+ --date) DATE="$2"; shift 2 ;; --pages) PAGES="$2"; shift 2 ;; *) shift ;;
214
+ esac
215
+ done
216
+ [ -z "$DATE" ] && DATE=$(date -v-1d +%Y-%m-%d 2>/dev/null || "$PYTHON" -c "from datetime import date,timedelta; print((date.today()-timedelta(1)).isoformat())")
217
+ [ -z "$INDUSTRY_L1" ] || [ -z "$INDUSTRY_L2" ] && { echo "WAIT_INDUSTRY_SELECTION"; exit 6; }
218
+ run_script collect_video_rankings.py
219
+ ;;
220
+
221
+ collect-industry-taxonomy)
222
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "leads_taxonomy"
223
+ shift
224
+ while [[ $# -gt 0 ]]; do
225
+ case "$1" in
226
+ --help) echo "collect-industry-taxonomy <profile_id> (candidate-only)"; exit 0 ;;
227
+ --dry-run) echo "[DRY-RUN] collect-industry-taxonomy $PROFILE_ID (candidate-only)"; exit 0 ;;
228
+ *) shift ;;
229
+ esac
230
+ done
231
+ run_script collect_industry_taxonomy.py
232
+ ;;
233
+
234
+ publish-dry-run)
235
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
236
+ shift; MEDIA_PATH=""; SCHEDULED=""
237
+ while [[ $# -gt 0 ]]; do
238
+ case "$1" in
239
+ --help) echo "publish-dry-run <profile_id> --media-path <path> [--scheduled-at <ISO>]"; exit 0 ;;
240
+ --media-path) MEDIA_PATH="$2"; shift 2 ;;
241
+ --scheduled-at) SCHEDULED="$2"; shift 2 ;;
242
+ *) shift ;;
243
+ esac
244
+ done
245
+ [ -z "$MEDIA_PATH" ] && { echo "错误: 缺少 --media-path"; exit 1; }
246
+ if [ -n "$SCHEDULED" ]; then
247
+ run_script douyin_publish.py --dry-run --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" --scheduled-at "$SCHEDULED"
248
+ else
249
+ run_script douyin_publish.py --dry-run --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH"
250
+ fi
251
+ ;;
252
+
253
+ publish-prepare)
254
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
255
+ shift; MEDIA_PATH=""; SCHEDULED=""
256
+ while [[ $# -gt 0 ]]; do
257
+ case "$1" in
258
+ --help) echo "publish-prepare <profile_id> --media-path <path> [--scheduled-at <ISO>]"; exit 0 ;;
259
+ --media-path) MEDIA_PATH="$2"; shift 2 ;;
260
+ --scheduled-at) SCHEDULED="$2"; shift 2 ;;
261
+ *) shift ;;
262
+ esac
263
+ done
264
+ [ -z "$MEDIA_PATH" ] && { echo "错误: 缺少 --media-path"; exit 1; }
265
+ if [ -n "$SCHEDULED" ]; then
266
+ run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" --scheduled-at "$SCHEDULED"
267
+ else
268
+ run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH"
269
+ fi
270
+ ;;
271
+
272
+ publish-confirm)
273
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
274
+ run_script douyin_publish.py --confirm-submit --profile-id "$PROFILE_ID"
275
+ ;;
276
+
277
+ publish-status)
278
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
279
+ shift; TASK_ID=""
280
+ while [[ $# -gt 0 ]]; do
281
+ case "$1" in
282
+ --help) echo "publish-status <profile_id> [--task-id <id>]"; exit 0 ;;
283
+ --task-id) TASK_ID="$2"; shift 2 ;;
284
+ *) shift ;;
285
+ esac
286
+ done
287
+ if [ -n "$TASK_ID" ]; then
288
+ run_script douyin_publish.py --status --profile-id "$PROFILE_ID" --task-id "$TASK_ID"
289
+ else
290
+ run_script douyin_publish.py --status --profile-id "$PROFILE_ID"
291
+ fi
292
+ ;;
293
+
294
+ publish-close)
295
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
296
+ run_script douyin_publish.py --close --profile-id "$PROFILE_ID"
297
+ ;;
298
+
299
+ xhs-login-start)
300
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
301
+ rm -f "$SCRIPT_DIR/signals/${PROFILE_ID}_response.json"
302
+ mkdir -p "$SCRIPT_DIR/logs"
303
+ run_script xiaohongshu_login_controller.py "$PROFILE_ID" \
304
+ >> "$SCRIPT_DIR/logs/${PROFILE_ID}_xiaohongshu_controller.log" 2>&1 &
305
+ disown
306
+ run_script xiaohongshu_login_ipc.py "$PROFILE_ID" wait-start
307
+ ;;
308
+
309
+ xhs-login-status)
310
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
311
+ run_script xiaohongshu_login_ipc.py "$PROFILE_ID" status
312
+ ;;
313
+
314
+ xhs-login-scan-done)
315
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
316
+ run_script xiaohongshu_login_ipc.py "$PROFILE_ID" scan-done
317
+ ;;
318
+
319
+ xhs-login-refresh-qr)
320
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
321
+ run_script xiaohongshu_login_ipc.py "$PROFILE_ID" refresh-qr
322
+ ;;
323
+
324
+ xhs-login-close)
325
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
326
+ run_script xiaohongshu_login_ipc.py "$PROFILE_ID" close
327
+ ;;
328
+
329
+ xhs-check-login)
330
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"
331
+ run_script xiaohongshu_check_login.py "$PROFILE_ID"
332
+ ;;
333
+
334
+ xhs-publish-dry-run)
335
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
336
+ shift; MEDIA_PATH=""
337
+ while [[ $# -gt 0 ]]; do
338
+ case "$1" in
339
+ --help) echo "xhs-publish-dry-run <profile_id> --media-path <path>"; exit 0 ;;
340
+ --media-path) MEDIA_PATH="$2"; shift 2 ;;
341
+ *) shift ;;
342
+ esac
343
+ done
344
+ [ -z "$MEDIA_PATH" ] && { echo "错误: 缺少 --media-path"; exit 1; }
345
+ run_script xiaohongshu_publish.py --dry-run --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH"
346
+ ;;
347
+
348
+ xhs-publish-prepare)
349
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
350
+ shift; MEDIA_PATH=""; TITLE=""
351
+ while [[ $# -gt 0 ]]; do
352
+ case "$1" in
353
+ --help) echo "xhs-publish-prepare <profile_id> --media-path <path> --title <标题>"; exit 0 ;;
354
+ --media-path) MEDIA_PATH="$2"; shift 2 ;;
355
+ --title) TITLE="$2"; shift 2 ;;
356
+ *) shift ;;
357
+ esac
358
+ done
359
+ [ -z "$MEDIA_PATH" ] && { echo "错误: 缺少 --media-path"; exit 1; }
360
+ [ -z "$TITLE" ] && { echo "错误: 缺少 --title"; exit 1; }
361
+ run_script xiaohongshu_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" --title "$TITLE"
362
+ ;;
363
+
364
+ xhs-publish-confirm)
365
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
366
+ run_script xiaohongshu_publish.py --confirm-submit --profile-id "$PROFILE_ID"
367
+ ;;
368
+
369
+ xhs-publish-status)
370
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
371
+ shift; TASK_ID=""
372
+ while [[ $# -gt 0 ]]; do
373
+ case "$1" in
374
+ --help) echo "xhs-publish-status <profile_id> [--task-id <id>]"; exit 0 ;;
375
+ --task-id) TASK_ID="$2"; shift 2 ;;
376
+ *) shift ;;
377
+ esac
378
+ done
379
+ if [ -n "$TASK_ID" ]; then
380
+ run_script xiaohongshu_publish.py --status --profile-id "$PROFILE_ID" --task-id "$TASK_ID"
381
+ else
382
+ run_script xiaohongshu_publish.py --status --profile-id "$PROFILE_ID"
383
+ fi
384
+ ;;
385
+
386
+ xhs-publish-close)
387
+ PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
388
+ run_script xiaohongshu_publish.py --close --profile-id "$PROFILE_ID"
389
+ ;;
390
+
391
+ help|--help|-h)
392
+ echo "media-agent - 社媒账号管理命令"
393
+ echo " login-start <id> 启动持久化登录控制器 (daemon)"
394
+ echo " login-status <id> 查询控制器状态"
395
+ echo " login-scan-done <id> 通知控制器扫码完成"
396
+ echo " login-refresh-qr <id> 刷新过期二维码 (当前页面内)"
397
+ echo " login-submit-code <id> 提交验证码 (从stdin读取, 不落盘)"
398
+ echo " login-resend-code <id> 重新发送短信验证码 (保留当前会话)"
399
+ echo " login-close <id> 正常关闭控制器并释放锁"
400
+ echo " check-login <id> [--site] 检查登录状态"
401
+ echo " enterprise-login <id> --site leads"
402
+ echo " validate-rankings <task_dir>"
403
+ echo " validate-industry-taxonomy <candidate.yaml>"
404
+ echo " close <id> 关闭浏览器并释放锁"
405
+ echo " collect-douyin-index <id>"
406
+ echo " export-short-video <id>"
407
+ echo " collect-video-rankings <id>"
408
+ echo " collect-industry-taxonomy <id>"
409
+ echo " publish-dry-run <id> --media-path <path>"
410
+ echo " publish-prepare <id> --media-path <path> [--scheduled-at <ISO>]"
411
+ echo " publish-confirm <id>"
412
+ echo " publish-status <id> [--task-id <id>]"
413
+ echo " publish-close <id>"
414
+ echo " toutiao-login-start <id> 启动今日头条持久化登录控制器"
415
+ echo " toutiao-login-status <id> 查询今日头条控制器状态"
416
+ echo " toutiao-login-scan-done <id> [--expected-account-name <昵称>]"
417
+ echo " toutiao-login-verify-account <id> --expected-account-name <昵称>"
418
+ echo " toutiao-login-refresh-qr <id> 在原二维码容器刷新"
419
+ echo " toutiao-login-close <id> 由原控制器正常关闭"
420
+ echo " toutiao-check-login <id> 独立只读检查登录状态"
421
+ echo " xhs-login-start <id> 小红书登录控制器 (daemon)"
422
+ echo " xhs-login-status <id> 小红书控制器状态"
423
+ echo " xhs-login-scan-done <id> 小红书扫码完成"
424
+ echo " xhs-login-refresh-qr <id> 小红书刷新二维码"
425
+ echo " xhs-login-close <id> 小红书关闭控制器"
426
+ echo " xhs-check-login <id> 小红书登录状态检查"
427
+ echo " xhs-publish-dry-run <id> --media-path <path>"
428
+ echo " xhs-publish-prepare <id> --media-path <path> --title <标题>"
429
+ echo " xhs-publish-confirm <id>"
430
+ echo " xhs-publish-status <id> [--task-id <id>]"
431
+ echo " xhs-publish-close <id>"
432
+ echo "Exit: 0=OK 1=err 2=LOGIN_REQUIRED 3=PROFILE_LOCKED 4=NO_CAPABILITY 5=NOT_FOUND 6=WAIT_INDUSTRY 7=indeterminate 8=DUPLICATE_BLOCKED"
433
+ ;;
434
+
435
+ *) echo "未知命令: $CMD"; echo "使用: media-agent help"; exit 1 ;;
436
+ esac
@@ -0,0 +1 @@
1
+ """Media Agent capabilities."""
@@ -0,0 +1 @@
1
+ """Media Agent capabilities."""
@@ -0,0 +1,196 @@
1
+ """Legacy operation, executed only through main()."""
2
+
3
+ def main():
4
+ #!/usr/bin/env python3
5
+ """Real page login check. Usage: python3 check_login.py <profile_id> [--site creator|leads|all]"""
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
+
12
+ BASE = runtime_home()
13
+ ALIAS = sys.argv[1] if len(sys.argv) > 1 else sys.exit(1)
14
+ SITE = 'all' # 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
+ LOCK_FILE = BASE / 'locks' / f'{ALIAS}.lock'
21
+
22
+ if not PROFILE_DIR.exists():
23
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'unknown', 'error': 'PROFILE_NOT_FOUND'}, indent=2))
24
+ sys.exit(5)
25
+
26
+ cfg = json.loads((PROFILE_DIR / 'config.json').read_text())
27
+ state = json.loads((PROFILE_DIR / 'state.json').read_text()) if (PROFILE_DIR / 'state.json').exists() else {}
28
+
29
+ # Determine sites to check
30
+ sites_cfg = cfg.get('sites', {})
31
+ if not sites_cfg:
32
+ # Backward compat: single site
33
+ target = cfg.get('account', {}).get('target_url', 'https://creator.douyin.com/')
34
+ sites_cfg = {'default': {'target_url': target}}
35
+
36
+ if SITE == 'creator':
37
+ sites_to_check = {'creator': sites_cfg.get('creator', {})}
38
+ elif SITE == 'leads':
39
+ sites_to_check = {'leads': sites_cfg.get('leads', {})}
40
+ else:
41
+ sites_to_check = {k: v for k, v in sites_cfg.items() if k in ('creator', 'leads')}
42
+ if not sites_to_check:
43
+ sites_to_check = sites_cfg # backward compat
44
+
45
+ LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
46
+ if LOCK_FILE.exists():
47
+ try:
48
+ os.kill(json.loads(LOCK_FILE.read_text()).get('pid', 0), 0)
49
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'locked', 'lock_status': 'locked'}, indent=2))
50
+ sys.exit(3)
51
+ except:
52
+ LOCK_FILE.unlink()
53
+ LOCK_FILE.write_text(json.dumps({'task_id': 'check-login', 'pid': os.getpid(), 'time': datetime.now().isoformat(), 'site': SITE}, indent=2))
54
+
55
+ try:
56
+ ctx = launch_persistent_context(
57
+ user_data_dir=str(PROFILE_DIR / 'browser_data'),
58
+ headless=False, stealth_args=True,
59
+ viewport=cfg['fingerprint']['viewport'],
60
+ locale=cfg['fingerprint']['locale'],
61
+ timezone=cfg['fingerprint']['timezone'],
62
+ humanize=False, geoip=False
63
+ )
64
+ page = ctx.pages[0] if ctx.pages else ctx.new_page()
65
+
66
+ results = {}
67
+ for site_name, site_cfg in sites_to_check.items():
68
+ target_url = site_cfg.get('target_url', '')
69
+ page.goto(target_url, wait_until='load', timeout=60000)
70
+ time.sleep(5)
71
+
72
+ url = page.url
73
+ title = page.title()
74
+
75
+ # DOM-based detection
76
+ dom = page.evaluate('''() => {
77
+ const result = {};
78
+ const all = document.querySelectorAll('a, div, span, li');
79
+ // Creator-specific: more specific dashboard indicators
80
+ const isCreator = '${site_name}' === 'creator';
81
+ const dashboardKeywords = isCreator
82
+ ? ['发布视频', '内容管理', '作品管理', '创作灵感', '互动管理']
83
+ : ['运营工作台', '数据分析', '线索管理', '昨日数据概览', '线索版'];
84
+ // Also check for creator-micro in URL (creator only)
85
+ const hasCreatorMicro = isCreator && window.location.href.includes('creator-micro');
86
+ const foundDashboard = [];
87
+ const seenDash = new Set();
88
+ for (const el of all) {
89
+ if (el.offsetParent === null) continue;
90
+ const t = el.textContent.trim();
91
+ for (const kw of dashboardKeywords) {
92
+ if (t.includes(kw) && !seenDash.has(kw)) { seenDash.add(kw); foundDashboard.push(kw); }
93
+ }
94
+ }
95
+ result.dashboard_detected = foundDashboard.length >= 2 || (isCreator && hasCreatorMicro);
96
+ result.dashboard_evidence = foundDashboard;
97
+ if (isCreator && hasCreatorMicro) result.dashboard_evidence.push('creator-micro URL');
98
+
99
+ const loginKeywords = ['扫码登录', '验证码登录', '密码登录', '手机号登录', '其他方式'];
100
+ const foundLogin = []; const seenLogin = new Set();
101
+ for (const el of all) {
102
+ if (el.offsetParent === null) continue;
103
+ const t = el.textContent.trim();
104
+ for (const kw of loginKeywords) {
105
+ if (t.includes(kw) && !seenLogin.has(kw)) { seenLogin.add(kw); foundLogin.push(kw); }
106
+ }
107
+ }
108
+ const loginInputs = document.querySelectorAll('input[type="tel"], input[placeholder*="手机"], input[placeholder*="验证码"]');
109
+ const hasLoginInputs = Array.from(loginInputs).some(el => el.offsetParent !== null);
110
+ result.login_form_detected = foundLogin.length > 0 || hasLoginInputs;
111
+ result.login_form_evidence = foundLogin;
112
+ if (hasLoginInputs) result.login_form_evidence.push('login_input_visible');
113
+
114
+ const qrImgs = document.querySelectorAll('img[src*="qr" i], canvas');
115
+ let hasQrImg = false;
116
+ for (const el of qrImgs) {
117
+ if (el.offsetParent === null) continue;
118
+ const rect = el.getBoundingClientRect();
119
+ if (Math.abs(rect.width - rect.height) < 20 && rect.width >= 100 && rect.width < 400) { hasQrImg = true; break; }
120
+ }
121
+ const qrDivs = document.querySelectorAll('div');
122
+ let hasQrText = false;
123
+ for (const el of qrDivs) {
124
+ if (el.offsetParent === null) continue;
125
+ const t = el.textContent.trim();
126
+ if ((t === '扫码登录' || t.includes('二维码')) && t.length < 20) { hasQrText = true; break; }
127
+ }
128
+ result.qr_detected = hasQrImg || hasQrText;
129
+ result.qr_evidence = [];
130
+ if (hasQrImg) result.qr_evidence.push('qr_image_visible');
131
+ if (hasQrText) result.qr_evidence.push('qr_text_visible');
132
+ return result;
133
+ }''')
134
+
135
+ dashboard = dom['dashboard_detected']
136
+ login_form = dom['login_form_detected']
137
+ qr = dom['qr_detected']
138
+
139
+ if dashboard and not login_form and not qr:
140
+ status = 'logged_in'
141
+ elif (login_form or qr) and not dashboard:
142
+ status = 'login_required'
143
+ elif dashboard and (login_form or qr):
144
+ status = 'indeterminate'
145
+ else:
146
+ status = 'indeterminate'
147
+
148
+ results[site_name] = {
149
+ 'target_url': target_url, 'current_url': url[:120], 'page_title': title,
150
+ 'dashboard_detected': dashboard, 'dashboard_evidence': dom['dashboard_evidence'],
151
+ 'login_form_detected': login_form, 'login_form_evidence': dom['login_form_evidence'],
152
+ 'qr_detected': qr, 'qr_evidence': dom['qr_evidence'],
153
+ 'login_status': status,
154
+ }
155
+
156
+ ctx.close()
157
+ LOCK_FILE.unlink(missing_ok=True)
158
+
159
+ # Update state
160
+ for site_name, r in results.items():
161
+ if site_name in ('creator', 'leads', 'default'):
162
+ key = site_name if site_name != 'default' else 'default'
163
+ if key in state:
164
+ state[key]['login_status'] = r['login_status']
165
+ state[key]['checked_at'] = datetime.now(timezone.utc).isoformat()
166
+ state[key]['dashboard_detected'] = r['dashboard_detected']
167
+ state[key]['login_form_detected'] = r['login_form_detected']
168
+ state[key]['qr_detected'] = r['qr_detected']
169
+ state[key]['evidence'] = r['login_status']
170
+ (PROFILE_DIR / 'state.json').write_text(json.dumps(state, indent=2))
171
+
172
+ # Compute overall status
173
+ statuses = [r['login_status'] for r in results.values()]
174
+ if all(s == 'logged_in' for s in statuses):
175
+ overall = 'logged_in'; exit_code = 0
176
+ elif any(s == 'login_required' for s in statuses):
177
+ overall = 'login_required'; exit_code = 2
178
+ elif any(s == 'indeterminate' for s in statuses):
179
+ overall = 'indeterminate'; exit_code = 7
180
+ else:
181
+ overall = 'indeterminate'; exit_code = 7
182
+
183
+ output = {
184
+ 'profile_id': ALIAS, 'site': SITE, 'overall_status': overall,
185
+ 'sites': results,
186
+ 'browser_closed': True, 'lock_released': True,
187
+ }
188
+ print(json.dumps(output, indent=2, ensure_ascii=False))
189
+ sys.exit(exit_code)
190
+ except Exception as e:
191
+ LOCK_FILE.unlink(missing_ok=True)
192
+ print(json.dumps({'profile_id': ALIAS, 'login_status': 'check_failed', 'error': str(e)[:100]}, indent=2))
193
+ sys.exit(1)
194
+
195
+ if __name__ == '__main__':
196
+ main()