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