@iducky/media-agent 1.1.0 → 1.1.1

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 (36) hide show
  1. package/README.md +3 -3
  2. package/SHA256SUMS +34 -30
  3. package/bin/media-agent.mjs +19 -7
  4. package/docs/guides/capabilities.md +6 -2
  5. package/docs/guides/installation.md +42 -6
  6. package/manifest.json +74 -58
  7. package/package.json +1 -1
  8. package/pyproject.toml +1 -1
  9. package/resources/capabilities.json +43 -1
  10. package/skills/douyin-creator-index/SKILL.md +2 -0
  11. package/skills/douyin-creator-publish/SKILL.md +3 -1
  12. package/skills/douyin-enterprise-short-video-export/SKILL.md +2 -0
  13. package/skills/douyin-enterprise-video-rankings/SKILL.md +2 -0
  14. package/skills/xiaohongshu-creator-publish/SKILL.md +3 -1
  15. package/src/media_agent/cli.py +5 -0
  16. package/src/media_agent/commands.sh +20 -20
  17. package/src/media_agent/platforms/douyin/check_login.py +8 -11
  18. package/src/media_agent/platforms/douyin/collect_industry_taxonomy.py +10 -1
  19. package/src/media_agent/platforms/douyin/collect_video_rankings.py +14 -5
  20. package/src/media_agent/platforms/douyin/douyin_hotspot_v2.py +12 -3
  21. package/src/media_agent/platforms/douyin/douyin_publish.py +65 -218
  22. package/src/media_agent/platforms/douyin/enterprise_login.py +6 -11
  23. package/src/media_agent/platforms/douyin/export_short_video.py +10 -1
  24. package/src/media_agent/platforms/douyin/login_controller.py +4 -15
  25. package/src/media_agent/platforms/toutiao/toutiao_login_controller.py +4 -22
  26. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_check_login.py +8 -11
  27. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_login_controller.py +5 -17
  28. package/src/media_agent/platforms/xiaohongshu/xiaohongshu_publish.py +63 -182
  29. package/src/media_agent/runtime/account_manager.py +16 -88
  30. package/src/media_agent/runtime/arguments.py +69 -0
  31. package/src/media_agent/runtime/diagnostics.py +186 -0
  32. package/src/media_agent/runtime/locking.py +74 -0
  33. package/src/media_agent/runtime/publish_control.py +98 -0
  34. package/src/node/skills.mjs +2 -1
  35. package/tools/artifacts.py +1 -0
  36. package/tools/install_runtime.py +5 -0
@@ -30,5 +30,47 @@
30
30
  "agents/openai.yaml": "cfe112ae911831c7f9947b80bea7fa5adbaf44d8461bbe23c9d987aecae99f6e"
31
31
  }
32
32
  }
33
- ]
33
+ ],
34
+ "commands": {
35
+ "collect-douyin-index": {
36
+ "status": "IMPLEMENTATION_REQUIRED",
37
+ "execution": "skill",
38
+ "reason": "Legacy CLI does not verify all supplied arguments"
39
+ },
40
+ "collect-video-rankings": {
41
+ "status": "IMPLEMENTATION_REQUIRED",
42
+ "execution": "skill",
43
+ "reason": "Legacy CLI does not verify all supplied arguments"
44
+ },
45
+ "collect-industry-taxonomy": {
46
+ "status": "IMPLEMENTATION_REQUIRED",
47
+ "execution": "skill",
48
+ "reason": "Legacy CLI does not verify all supplied arguments"
49
+ },
50
+ "export-short-video": {
51
+ "status": "IMPLEMENTATION_REQUIRED",
52
+ "execution": "skill",
53
+ "reason": "Legacy CLI does not verify all supplied arguments"
54
+ },
55
+ "publish-prepare": {
56
+ "status": "OFFLINE_VERIFIED",
57
+ "execution": "cli"
58
+ },
59
+ "xhs-publish-prepare": {
60
+ "status": "IMPLEMENTED_UNVERIFIED",
61
+ "execution": "cli"
62
+ },
63
+ "douyin-creator-image-text-publish": {
64
+ "status": "ADAPTED_UNVERIFIED",
65
+ "execution": "skill"
66
+ },
67
+ "xiaohongshu-creator-image-text-publish": {
68
+ "status": "ADAPTED_UNVERIFIED",
69
+ "execution": "skill"
70
+ },
71
+ "toutiao-creator-article-draft": {
72
+ "status": "AGENT_WORKFLOW",
73
+ "execution": "skill"
74
+ }
75
+ }
34
76
  }
@@ -30,3 +30,5 @@ media-agent collect-douyin-index <profile_id> [--lists realtime,rising] [--pages
30
30
  v1.0.3 的 `douyin_hotspot_v2.py` 硬编码测试 Profile,`media-agent` 未把 `<profile_id>` 传入采集脚本。对其他 Profile 执行前必须确认安装版本已参数化,否则返回 `IMPLEMENTATION_REQUIRED`,不得声称使用了请求的账号。
31
31
 
32
32
  检查上述参数化限制时,必须读取实现 `src/media_agent/platforms/douyin/douyin_hotspot_v2.py` 和命令路由 `src/media_agent/commands.sh`;已安装环境对应 `<MEDIA_AGENT_HOME>/.app/current/` 下的这些路径。命令存在不代表参数化限制已消除。
33
+
34
+ 1.1.1 安全修订:旧采集/导出 CLI 的真实执行已在浏览器启动前拦截并返回 IMPLEMENTATION_REQUIRED(退出码 6)。不要尝试绕过门禁直接运行历史脚本;需要业务操作时按本 Skill 的可见浏览器工作流执行并核验输入条件。dry-run 成功不表示真实采集已支持。
@@ -10,7 +10,7 @@ description: 在抖音创作者中心准备、确认、提交和核验视频作
10
10
  ```bash
11
11
  media-agent publish-dry-run <profile_id> --media-path <path> [--scheduled-at <ISO>]
12
12
  media-agent publish-prepare <profile_id> --media-path <path> [--scheduled-at <ISO>]
13
- media-agent publish-confirm <profile_id>
13
+ media-agent publish-confirm <profile_id> --task-id <task_id>
14
14
  media-agent publish-status <profile_id> --task-id <task_id>
15
15
  media-agent publish-close <profile_id>
16
16
  ```
@@ -54,3 +54,5 @@ URL 跳转、点击计数、按钮消失或本地账本均不是成功证据。C
54
54
  ## 收尾
55
55
 
56
56
  成功或明确失败后正常关闭并释放 lock。`indeterminate` 默认保持现场。禁止 `kill`、`pkill`、覆盖账本原记录或自动重试。
57
+
58
+ 发布确认必须带 prepare 返回的 task_id;没有对应的存活准备进程时拒绝执行,不重新打开页面补点发布。确认信号不等于发布成功。结果 indeterminate 的素材禁止直接重试,应先人工核验。
@@ -27,3 +27,5 @@ media-agent export-short-video <profile_id> [--date YYYY-MM-DD]
27
27
  ## 返回
28
28
 
29
29
  返回 `profile_id`、日期范围、`file_name`、`file_size`、`sheet_names`、`data_rows`、下载状态、附件投递状态、浏览器关闭和 lock 释放状态。不得把榜单抓取结果冒充平台原始导出。
30
+
31
+ 1.1.1 安全修订:旧采集/导出 CLI 的真实执行已在浏览器启动前拦截并返回 IMPLEMENTATION_REQUIRED(退出码 6)。不要尝试绕过门禁直接运行历史脚本;需要业务操作时按本 Skill 的可见浏览器工作流执行并核验输入条件。dry-run 成功不表示真实采集已支持。
@@ -49,3 +49,5 @@ media-agent validate-rankings <task_dir>
49
49
  正常关闭浏览器并释放 lock;本地文件必须作为真实附件发送后才算完成。
50
50
 
51
51
  检查上述参数化限制时,必须读取实现 `src/media_agent/platforms/douyin/collect_video_rankings.py` 和命令路由 `src/media_agent/commands.sh`;已安装环境对应 `<MEDIA_AGENT_HOME>/.app/current/` 下的这些路径。命令存在不代表参数化限制已消除。
52
+
53
+ 1.1.1 安全修订:旧采集/导出 CLI 的真实执行已在浏览器启动前拦截并返回 IMPLEMENTATION_REQUIRED(退出码 6)。不要尝试绕过门禁直接运行历史脚本;需要业务操作时按本 Skill 的可见浏览器工作流执行并核验输入条件。dry-run 成功不表示真实采集已支持。
@@ -23,7 +23,7 @@ description: 在小红书创作服务平台上传、填写标题、发布并核
23
23
  ```bash
24
24
  media-agent xhs-publish-dry-run <profile_id> --media-path <path> # 素材校验 + SHA-256 账本去重,不开浏览器
25
25
  media-agent xhs-publish-prepare <profile_id> --media-path <path> --title <标题> # 上传、填标题并读回、写账本、保持浏览器
26
- media-agent xhs-publish-confirm <profile_id> # 单次真实点击“发布”
26
+ media-agent xhs-publish-confirm <profile_id> --task-id <task_id> # 单次真实点击“发布”
27
27
  media-agent xhs-publish-status <profile_id> [--task-id <id>] # 查询发布/审核状态
28
28
  media-agent xhs-publish-close <profile_id> # 正常关闭浏览器并释放 lock
29
29
  ```
@@ -151,3 +151,5 @@ current_url=<移除敏感查询参数后的 URL>
151
151
  ## 收尾
152
152
 
153
153
  成功、拒绝或只读查询结束后正常关闭 page/context/browser 并释放 lock,让 Profile 正常写盘。`PUBLISH_INDETERMINATE` 默认保留现场供人工检查。禁止使用 `kill`、`pkill`、清空 `browser_data`、删除作品或自动重复提交。
154
+
155
+ 发布确认必须带 prepare 返回的 task_id;没有对应的存活准备进程时拒绝执行,不重新打开页面补点发布。确认信号不等于发布成功。结果 indeterminate 的素材禁止直接重试,应先人工核验。
@@ -10,6 +10,11 @@ import sys
10
10
  def main(argv=None):
11
11
  args = list(sys.argv[1:] if argv is None else argv)
12
12
  package = Path(__file__).resolve().parent
13
+ if args and (args[0] in ('tasks', 'upgrade') or args[:2] == ['config', 'validate']):
14
+ from media_agent.runtime.diagnostics import main as diagnose
15
+ return diagnose(args)
16
+ from media_agent.runtime.arguments import validate
17
+ args = validate(args)
13
18
  if args and args[0] == 'run-script':
14
19
  mapping = json.loads((package / 'script_map.json').read_text())
15
20
  name = args[1].removesuffix('.py') if len(args) > 1 else ''
@@ -87,7 +87,6 @@ case "$CMD" in
87
87
  PROFILE_ID="$1"; require_profile "$PROFILE_ID"
88
88
  run_script login_controller.py "$PROFILE_ID" --send '{"action":"close"}'
89
89
  sleep 3
90
- rm -f "$SCRIPT_DIR/locks/${PROFILE_ID}.lock"
91
90
  ;;
92
91
 
93
92
  login-refresh-qr)
@@ -163,18 +162,8 @@ case "$CMD" in
163
162
  ;;
164
163
 
165
164
  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 "已关闭并释放锁"
165
+ echo "Use the owning controller's login-close, xhs-login-close, toutiao-login-close or publish-close command; no lock was removed."
166
+ exit 1
178
167
  ;;
179
168
 
180
169
  collect-douyin-index)
@@ -252,26 +241,32 @@ case "$CMD" in
252
241
 
253
242
  publish-prepare)
254
243
  PROFILE_ID="$1"; require_profile "$PROFILE_ID"; check_capability "$PROFILE_ID" "creator_publish"
255
- shift; MEDIA_PATH=""; SCHEDULED=""
244
+ shift; MEDIA_PATH=""; SCHEDULED=""; TEXT_ARGS=()
256
245
  while [[ $# -gt 0 ]]; do
257
246
  case "$1" in
258
247
  --help) echo "publish-prepare <profile_id> --media-path <path> [--scheduled-at <ISO>]"; exit 0 ;;
259
248
  --media-path) MEDIA_PATH="$2"; shift 2 ;;
260
249
  --scheduled-at) SCHEDULED="$2"; shift 2 ;;
250
+ --title|--description) TEXT_ARGS+=("$1" "$2"); shift 2 ;;
261
251
  *) shift ;;
262
252
  esac
263
253
  done
264
254
  [ -z "$MEDIA_PATH" ] && { echo "错误: 缺少 --media-path"; exit 1; }
265
255
  if [ -n "$SCHEDULED" ]; then
266
- run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" --scheduled-at "$SCHEDULED"
256
+ run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" "${TEXT_ARGS[@]}" --scheduled-at "$SCHEDULED"
267
257
  else
268
- run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH"
258
+ run_script douyin_publish.py --prepare --profile-id "$PROFILE_ID" --media-path "$MEDIA_PATH" "${TEXT_ARGS[@]}"
269
259
  fi
270
260
  ;;
271
261
 
272
262
  publish-confirm)
273
263
  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"
264
+ shift; TASK_ID=""
265
+ while [[ $# -gt 0 ]]; do
266
+ case "$1" in --task-id) TASK_ID="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac
267
+ done
268
+ [ -n "$TASK_ID" ] || { echo "TASK_ID_REQUIRED"; exit 1; }
269
+ run_script douyin_publish.py --confirm-submit --profile-id "$PROFILE_ID" --task-id "$TASK_ID"
275
270
  ;;
276
271
 
277
272
  publish-status)
@@ -363,7 +358,12 @@ case "$CMD" in
363
358
 
364
359
  xhs-publish-confirm)
365
360
  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"
361
+ shift; TASK_ID=""
362
+ while [[ $# -gt 0 ]]; do
363
+ case "$1" in --task-id) TASK_ID="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac
364
+ done
365
+ [ -n "$TASK_ID" ] || { echo "TASK_ID_REQUIRED"; exit 1; }
366
+ run_script xiaohongshu_publish.py --confirm-submit --profile-id "$PROFILE_ID" --task-id "$TASK_ID"
367
367
  ;;
368
368
 
369
369
  xhs-publish-status)
@@ -408,7 +408,7 @@ case "$CMD" in
408
408
  echo " collect-industry-taxonomy <id>"
409
409
  echo " publish-dry-run <id> --media-path <path>"
410
410
  echo " publish-prepare <id> --media-path <path> [--scheduled-at <ISO>]"
411
- echo " publish-confirm <id>"
411
+ echo " publish-confirm <id> --task-id <id>"
412
412
  echo " publish-status <id> [--task-id <id>]"
413
413
  echo " publish-close <id>"
414
414
  echo " toutiao-login-start <id> 启动今日头条持久化登录控制器"
@@ -426,7 +426,7 @@ case "$CMD" in
426
426
  echo " xhs-check-login <id> 小红书登录状态检查"
427
427
  echo " xhs-publish-dry-run <id> --media-path <path>"
428
428
  echo " xhs-publish-prepare <id> --media-path <path> --title <标题>"
429
- echo " xhs-publish-confirm <id>"
429
+ echo " xhs-publish-confirm <id> --task-id <id>"
430
430
  echo " xhs-publish-status <id> [--task-id <id>]"
431
431
  echo " xhs-publish-close <id>"
432
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"
@@ -42,15 +42,12 @@ def main():
42
42
  if not sites_to_check:
43
43
  sites_to_check = sites_cfg # backward compat
44
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))
45
+ TASK_ID = 'check-login'
46
+ from media_agent.runtime.locking import acquire, release
47
+ if not acquire(LOCK_FILE, TASK_ID):
48
+ print('PROFILE_LOCKED')
49
+ raise SystemExit(3)
50
+
54
51
 
55
52
  try:
56
53
  ctx = launch_persistent_context(
@@ -154,7 +151,7 @@ def main():
154
151
  }
155
152
 
156
153
  ctx.close()
157
- LOCK_FILE.unlink(missing_ok=True)
154
+ release(LOCK_FILE, TASK_ID)
158
155
 
159
156
  # Update state
160
157
  for site_name, r in results.items():
@@ -188,7 +185,7 @@ def main():
188
185
  print(json.dumps(output, indent=2, ensure_ascii=False))
189
186
  sys.exit(exit_code)
190
187
  except Exception as e:
191
- LOCK_FILE.unlink(missing_ok=True)
188
+ release(LOCK_FILE, TASK_ID)
192
189
  print(json.dumps({'profile_id': ALIAS, 'login_status': 'check_failed', 'error': str(e)[:100]}, indent=2))
193
190
  sys.exit(1)
194
191
 
@@ -1,6 +1,15 @@
1
1
  """Legacy operation, executed only through main()."""
2
2
 
3
3
  def main():
4
+ import json
5
+ import sys
6
+ print(json.dumps({'state': 'IMPLEMENTATION_REQUIRED',
7
+ 'operation': 'collect_industry_taxonomy',
8
+ 'reason': 'Legacy browser flow cannot yet verify all requested parameters; use the documented Skill workflow.'}))
9
+ raise SystemExit(6)
10
+
11
+
12
+ def _legacy_reference():
4
13
  #!/usr/bin/env python3
5
14
  """Collect industry taxonomy v2 — fixed readback and proper cascader interaction."""
6
15
  import yaml
@@ -22,7 +31,7 @@ def main():
22
31
  LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
23
32
  if LOCK_FILE.exists():
24
33
  try: os.kill(json.loads(LOCK_FILE.read_text()).get('pid',0), 0); print('LOCKED'); sys.exit(1)
25
- except: LOCK_FILE.unlink()
34
+ except Exception: LOCK_FILE.unlink()
26
35
  LOCK_FILE.write_text(json.dumps({'task_id':f'taxonomy_v2_{ts}','pid':os.getpid()}, indent=2))
27
36
  print(f'TASK|taxonomy_v2_{ts}', flush=True)
28
37
 
@@ -1,6 +1,15 @@
1
1
  """Legacy operation, executed only through main()."""
2
2
 
3
3
  def main():
4
+ import json
5
+ import sys
6
+ print(json.dumps({'state': 'IMPLEMENTATION_REQUIRED',
7
+ 'operation': 'collect_video_rankings',
8
+ 'reason': 'Legacy browser flow cannot yet verify all requested parameters; use the documented Skill workflow.'}))
9
+ raise SystemExit(6)
10
+
11
+
12
+ def _legacy_reference():
4
13
  #!/usr/bin/env python3
5
14
  """Collect video rankings - DOM-based extraction, verified locators."""
6
15
  from media_agent.runtime.paths import runtime_home
@@ -28,7 +37,7 @@ def main():
28
37
  LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
29
38
  if LOCK_FILE.exists():
30
39
  try: os.kill(json.loads(LOCK_FILE.read_text()).get('pid',0), 0); sys.exit(1)
31
- except: LOCK_FILE.unlink()
40
+ except Exception: LOCK_FILE.unlink()
32
41
  LOCK_FILE.write_text(json.dumps({'task_id':TASK_ID,'pid':os.getpid()}, indent=2))
33
42
  print(f'TASK|{TASK_ID}', flush=True)
34
43
 
@@ -119,21 +128,21 @@ def main():
119
128
 
120
129
  def click_tab(page, name):
121
130
  try: page.locator(f'text={name}').first.click(); time.sleep(5); return True
122
- except: return False
131
+ except Exception: return False
123
132
 
124
133
  def parse_numeric(val):
125
134
  if not val: return {'display': val, 'numeric': None}
126
135
  t = val.strip()
127
136
  if t.endswith('%'):
128
137
  try: return {'display': t, 'numeric': float(t[:-1]) / 100}
129
- except: return {'display': t, 'numeric': None}
138
+ except Exception: return {'display': t, 'numeric': None}
130
139
  if t.endswith('万'):
131
140
  try: return {'display': t, 'numeric': round(float(t[:-1]) * 10000)}
132
- except: return {'display': t, 'numeric': None}
141
+ except Exception: return {'display': t, 'numeric': None}
133
142
  try:
134
143
  n = float(t.replace(',', ''))
135
144
  return {'display': t, 'numeric': int(n) if n == int(n) else n}
136
- except:
145
+ except Exception:
137
146
  return {'display': t, 'numeric': None}
138
147
 
139
148
  # ===== Collect =====
@@ -1,6 +1,15 @@
1
1
  """Legacy operation, executed only through main()."""
2
2
 
3
3
  def main():
4
+ import json
5
+ import sys
6
+ print(json.dumps({'state': 'IMPLEMENTATION_REQUIRED',
7
+ 'operation': 'douyin_hotspot_v2',
8
+ 'reason': 'Legacy browser flow cannot yet verify all requested parameters; use the documented Skill workflow.'}))
9
+ raise SystemExit(6)
10
+
11
+
12
+ def _legacy_reference():
4
13
  #!/usr/bin/env python3
5
14
  """Douyin热点数据完整采集 v2 - 独立容器分页 + 趋势修复"""
6
15
  from media_agent.runtime.paths import runtime_home
@@ -27,7 +36,7 @@ def main():
27
36
  if pid:
28
37
  try: os.kill(pid, 0); print('ERROR|Lock active', flush=True); sys.exit(1)
29
38
  except OSError: LOCK_FILE.unlink()
30
- except: LOCK_FILE.unlink()
39
+ except Exception: LOCK_FILE.unlink()
31
40
  LOCK_FILE.write_text(json.dumps({'task_id':TASK_ID,'pid':os.getpid()}, indent=2))
32
41
  print(f'LOCK|{TASK_ID}', flush=True)
33
42
 
@@ -49,7 +58,7 @@ def main():
49
58
  try:
50
59
  confirm = page.locator('text=确认').first
51
60
  if confirm and confirm.is_visible(timeout=3000): confirm.click(); time.sleep(3)
52
- except: pass
61
+ except Exception: pass
53
62
 
54
63
  collected_at = datetime.now(timezone(timedelta(hours=8))).isoformat()
55
64
  source_url = page.url
@@ -123,7 +132,7 @@ def main():
123
132
  if '万' in t:
124
133
  return round(float(t.replace('万', '').strip()) * 10000), t
125
134
  try: return round(float(t.replace(',', ''))), t
126
- except: return 0, t
135
+ except Exception: return 0, t
127
136
 
128
137
  def parse_trend_dom(page):
129
138
  """Parse trend from DOM using JS to inspect icon/SVG/class."""