@11agents/cli 0.1.24 → 0.1.25

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 (71) hide show
  1. package/README.md +52 -0
  2. package/bin/11agents.js +12 -0
  3. package/mobile-runtime/README.md +19 -0
  4. package/mobile-runtime/configs/platforms/xiaohongshu_d01.json +73 -0
  5. package/mobile-runtime/configs/platforms/xiaohongshu_d02.json +70 -0
  6. package/mobile-runtime/configs/platforms/xiaohongshu_d03.json +73 -0
  7. package/mobile-runtime/configs/publish_policy.json +40 -0
  8. package/mobile-runtime/data-templates/README.md +4 -0
  9. package/mobile-runtime/data-templates/accounts.example.csv +6 -0
  10. package/mobile-runtime/data-templates/devices.example.csv +2 -0
  11. package/mobile-runtime/data-templates/publish_records.example.jsonl +2 -0
  12. package/mobile-runtime/data-templates/tasks.example.jsonl +5 -0
  13. package/mobile-runtime/data-templates/video_metrics.example.jsonl +1 -0
  14. package/mobile-runtime/python/pyproject.toml +34 -0
  15. package/mobile-runtime/python/src/device_control/__init__.py +5 -0
  16. package/mobile-runtime/python/src/device_control/adapters/__init__.py +31 -0
  17. package/mobile-runtime/python/src/device_control/adapters/base.py +43 -0
  18. package/mobile-runtime/python/src/device_control/adapters/facebook.py +30 -0
  19. package/mobile-runtime/python/src/device_control/adapters/instagram.py +25 -0
  20. package/mobile-runtime/python/src/device_control/adapters/reddit.py +29 -0
  21. package/mobile-runtime/python/src/device_control/adapters/tiktok.py +25 -0
  22. package/mobile-runtime/python/src/device_control/adapters/x.py +29 -0
  23. package/mobile-runtime/python/src/device_control/adapters/xiaohongshu.py +26 -0
  24. package/mobile-runtime/python/src/device_control/adb.py +161 -0
  25. package/mobile-runtime/python/src/device_control/appium_client.py +131 -0
  26. package/mobile-runtime/python/src/device_control/appium_manager.py +403 -0
  27. package/mobile-runtime/python/src/device_control/cli.py +1608 -0
  28. package/mobile-runtime/python/src/device_control/entrypoints.py +60 -0
  29. package/mobile-runtime/python/src/device_control/locks.py +162 -0
  30. package/mobile-runtime/python/src/device_control/metrics/__init__.py +33 -0
  31. package/mobile-runtime/python/src/device_control/metrics/collectors.py +320 -0
  32. package/mobile-runtime/python/src/device_control/metrics/tiktok_account_adb.py +367 -0
  33. package/mobile-runtime/python/src/device_control/metrics/tiktok_video_adb.py +714 -0
  34. package/mobile-runtime/python/src/device_control/models.py +439 -0
  35. package/mobile-runtime/python/src/device_control/publish_policy.py +173 -0
  36. package/mobile-runtime/python/src/device_control/publishers/__init__.py +24 -0
  37. package/mobile-runtime/python/src/device_control/publishers/facebook_adb.py +494 -0
  38. package/mobile-runtime/python/src/device_control/publishers/instagram_adb.py +663 -0
  39. package/mobile-runtime/python/src/device_control/publishers/reddit_adb.py +595 -0
  40. package/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +477 -0
  41. package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +259 -0
  42. package/mobile-runtime/python/src/device_control/publishers/ui_helpers.py +372 -0
  43. package/mobile-runtime/python/src/device_control/publishers/x_adb.py +636 -0
  44. package/mobile-runtime/python/src/device_control/publishers/xiaohongshu_adb.py +1143 -0
  45. package/mobile-runtime/python/src/device_control/store.py +137 -0
  46. package/mobile-runtime/scripts/appium_smoke.py +71 -0
  47. package/mobile-runtime/skills/android-collect-tiktok-metrics/SKILL.md +60 -0
  48. package/mobile-runtime/skills/android-collect-tiktok-metrics/agents/openai.yaml +4 -0
  49. package/mobile-runtime/skills/android-group-control-cli/SKILL.md +76 -0
  50. package/mobile-runtime/skills/android-group-control-cli/agents/openai.yaml +4 -0
  51. package/mobile-runtime/skills/android-group-control-cli/references/command-reference.md +122 -0
  52. package/mobile-runtime/skills/android-publish-facebook/SKILL.md +41 -0
  53. package/mobile-runtime/skills/android-publish-facebook/agents/openai.yaml +4 -0
  54. package/mobile-runtime/skills/android-publish-instagram/SKILL.md +45 -0
  55. package/mobile-runtime/skills/android-publish-instagram/agents/openai.yaml +4 -0
  56. package/mobile-runtime/skills/android-publish-reddit/SKILL.md +41 -0
  57. package/mobile-runtime/skills/android-publish-reddit/agents/openai.yaml +4 -0
  58. package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +43 -0
  59. package/mobile-runtime/skills/android-publish-tiktok/agents/openai.yaml +4 -0
  60. package/mobile-runtime/skills/android-publish-x/SKILL.md +40 -0
  61. package/mobile-runtime/skills/android-publish-x/agents/openai.yaml +4 -0
  62. package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +50 -0
  63. package/mobile-runtime/skills/android-publish-xiaohongshu/agents/openai.yaml +4 -0
  64. package/mobile-runtime/skills/mobile-publish-data-collection/SKILL.md +49 -0
  65. package/mobile-runtime/skills/mobile-publish-device-health/SKILL.md +47 -0
  66. package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +57 -0
  67. package/mobile-runtime/skills/mobile-publish-records/SKILL.md +29 -0
  68. package/package.json +4 -1
  69. package/scripts/mobile-postinstall.js +26 -0
  70. package/src/commands/mobile.js +695 -0
  71. package/src/commands/runtime.js +6 -5
@@ -0,0 +1,367 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import struct
5
+ import time
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from device_control.adb import AdbClient, CommandResult
13
+ from device_control.models import Device, TikTokAccountMetricsPeriod, TikTokAccountMetricsSnapshot, new_id, utc_now_iso
14
+ from device_control.publishers import ui_helpers as ui
15
+ from device_control.publishers.tiktok_adb import TIKTOK_PACKAGE
16
+
17
+
18
+ ACCOUNT_PERIODS: dict[str, int] = {
19
+ "7d": 7,
20
+ "28d": 28,
21
+ "60d": 60,
22
+ }
23
+
24
+ ACCOUNT_METRIC_KEYS = ("video_views", "profile_views", "total_likes", "total_comments", "shares")
25
+
26
+ # Coordinates calibrated from a 1080x2245 TikTok Studio analytics screenshot on D01.
27
+ BASE_SCREEN = (1080, 2245)
28
+ PERIOD_TAB_COORDS = {
29
+ "7d": (121, 473),
30
+ "28d": (308, 473),
31
+ "60d": (492, 473),
32
+ }
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class TikTokAccountMetricValues:
37
+ video_views: int | None = None
38
+ profile_views: int | None = None
39
+ total_likes: int | None = None
40
+ total_comments: int | None = None
41
+ shares: int | None = None
42
+ date_range: str = ""
43
+ raw: dict[str, Any] | None = None
44
+
45
+
46
+ class TikTokAccountMetricsAdbCollector:
47
+ """Read-only ADB collector for TikTok Studio account overview metrics."""
48
+
49
+ def __init__(self, adb: AdbClient, *, artifact_root: str | Path = "artifacts/screenshots") -> None:
50
+ self.adb = adb
51
+ self.artifact_root = Path(artifact_root)
52
+
53
+ def collect(
54
+ self,
55
+ device: Device,
56
+ *,
57
+ account_id: str,
58
+ periods: list[str] | None = None,
59
+ resume_analytics: bool = False,
60
+ manual_values: dict[str, TikTokAccountMetricValues] | None = None,
61
+ ) -> TikTokAccountMetricsSnapshot:
62
+ requested = _normalize_periods(periods)
63
+ manual_values = manual_values or {}
64
+ started_epoch = int(time.time())
65
+ started_at = _now_shanghai()
66
+ stamp = datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d-%H%M%S")
67
+ run_dir = self.artifact_root / f"{device.device_id}-tiktok-account-metrics-{stamp}"
68
+ run_dir.mkdir(parents=True, exist_ok=True)
69
+
70
+ raw: dict[str, Any] = {
71
+ "resume_analytics": resume_analytics,
72
+ "requested_periods": requested,
73
+ "metric_keys": list(ACCOUNT_METRIC_KEYS),
74
+ }
75
+ periods_out: dict[str, TikTokAccountMetricsPeriod] = {}
76
+
77
+ try:
78
+ self._prepare_device(device)
79
+ if not resume_analytics:
80
+ self._navigate_to_account_analytics(device, run_dir)
81
+
82
+ initial_shot = run_dir / "initial.png"
83
+ self._ok(self.adb.screenshot(device.adb_serial, initial_shot), "capture initial TikTok analytics screenshot")
84
+ width, height = _png_size(initial_shot) or BASE_SCREEN
85
+ raw["screen_size"] = {"width": width, "height": height}
86
+ raw["initial_screenshot_path"] = str(initial_shot)
87
+ self._scroll_overview_to_metrics_top(device, run_dir, width, height)
88
+
89
+ for period_key in requested:
90
+ x, y = _scale_coord(PERIOD_TAB_COORDS[period_key], width, height)
91
+ self._ok(self.adb.tap(device.adb_serial, x, y), f"tap TikTok analytics period {period_key}")
92
+ time.sleep(2)
93
+
94
+ screenshot_path = run_dir / f"{period_key}.png"
95
+ ui_path = run_dir / f"{period_key}.xml"
96
+ self._ok(self.adb.screenshot(device.adb_serial, screenshot_path), f"capture {period_key} screenshot")
97
+ dump = self.adb.dump_ui(device.adb_serial, ui_path)
98
+ if not dump.ok:
99
+ ui_path = Path("")
100
+
101
+ value = manual_values.get(period_key)
102
+ periods_out[period_key] = self._build_period(
103
+ period_key,
104
+ value,
105
+ screenshot_path=screenshot_path,
106
+ ui_dump_path=ui_path,
107
+ tap={"x": x, "y": y},
108
+ )
109
+
110
+ ended_epoch = int(time.time())
111
+ status = "collected" if all(period.has_all_metrics() for period in periods_out.values()) else "needs_human"
112
+ raw["extraction_status"] = (
113
+ "manual_values_recorded" if status == "collected" else "manual_values_required"
114
+ )
115
+ return TikTokAccountMetricsSnapshot(
116
+ snapshot_id=new_id("tkacct"),
117
+ platform="tiktok",
118
+ account_id=account_id,
119
+ device_id=device.device_id,
120
+ fetched_at=utc_now_iso(),
121
+ source="tiktok_studio_adb",
122
+ status=status,
123
+ periods=periods_out,
124
+ artifact_dir=str(run_dir),
125
+ started_at=started_at,
126
+ ended_at=_now_shanghai(),
127
+ duration_seconds=ended_epoch - started_epoch,
128
+ raw=raw,
129
+ )
130
+ except Exception as exc:
131
+ failure_shot = run_dir / "failed.png"
132
+ self.adb.screenshot(device.adb_serial, failure_shot)
133
+ ended_epoch = int(time.time())
134
+ return TikTokAccountMetricsSnapshot(
135
+ snapshot_id=new_id("tkacct"),
136
+ platform="tiktok",
137
+ account_id=account_id,
138
+ device_id=device.device_id,
139
+ fetched_at=utc_now_iso(),
140
+ source="tiktok_studio_adb",
141
+ status="failed",
142
+ periods=periods_out,
143
+ artifact_dir=str(run_dir),
144
+ started_at=started_at,
145
+ ended_at=_now_shanghai(),
146
+ duration_seconds=ended_epoch - started_epoch,
147
+ raw={
148
+ **raw,
149
+ "error": str(exc),
150
+ "failure_screenshot_path": str(failure_shot),
151
+ },
152
+ )
153
+
154
+ def _build_period(
155
+ self,
156
+ period_key: str,
157
+ value: TikTokAccountMetricValues | None,
158
+ *,
159
+ screenshot_path: Path,
160
+ ui_dump_path: Path,
161
+ tap: dict[str, int],
162
+ ) -> TikTokAccountMetricsPeriod:
163
+ raw = {"tap": tap}
164
+ if value is None:
165
+ raw["extraction_status"] = "manual_values_required"
166
+ return TikTokAccountMetricsPeriod(
167
+ period_key=period_key,
168
+ period_days=ACCOUNT_PERIODS[period_key],
169
+ screenshot_path=str(screenshot_path),
170
+ ui_dump_path=str(ui_dump_path),
171
+ extraction_method="manual_required",
172
+ raw=raw,
173
+ )
174
+
175
+ raw.update(dict(value.raw or {}))
176
+ raw["extraction_status"] = "manual_values_recorded"
177
+ return TikTokAccountMetricsPeriod(
178
+ period_key=period_key,
179
+ period_days=ACCOUNT_PERIODS[period_key],
180
+ date_range=value.date_range,
181
+ video_views=value.video_views,
182
+ profile_views=value.profile_views,
183
+ total_likes=value.total_likes,
184
+ total_comments=value.total_comments,
185
+ shares=value.shares,
186
+ screenshot_path=str(screenshot_path),
187
+ ui_dump_path=str(ui_dump_path),
188
+ extraction_method="manual",
189
+ raw=raw,
190
+ )
191
+
192
+ def _prepare_device(self, device: Device) -> None:
193
+ self.adb.wake(device.adb_serial)
194
+ time.sleep(0.5)
195
+ self.adb.swipe(device.adb_serial, 540, 1850, 540, 550, 350)
196
+ time.sleep(0.5)
197
+
198
+ def _scroll_overview_to_metrics_top(self, device: Device, run_dir: Path, width: int, height: int) -> None:
199
+ for index in range(3):
200
+ self.adb.swipe(
201
+ device.adb_serial,
202
+ width // 2,
203
+ int(height * 0.42),
204
+ width // 2,
205
+ int(height * 0.82),
206
+ 450,
207
+ )
208
+ time.sleep(0.8)
209
+ self.adb.screenshot(device.adb_serial, run_dir / f"scroll-top-{index + 1}.png")
210
+
211
+ def _navigate_to_account_analytics(self, device: Device, run_dir: Path) -> None:
212
+ self.adb.shell(device.adb_serial, "am", "force-stop", TIKTOK_PACKAGE)
213
+ time.sleep(1)
214
+ self._ok(self.adb.launch_package(device.adb_serial, TIKTOK_PACKAGE), "launch TikTok")
215
+ time.sleep(5)
216
+
217
+ self._tap_ratio(device, 0.91, 0.96, "open TikTok profile tab")
218
+ time.sleep(3)
219
+ self.adb.screenshot(device.adb_serial, run_dir / "profile.png")
220
+
221
+ self._tap_ratio(device, 0.94, 0.055, "open TikTok profile menu")
222
+ time.sleep(2)
223
+ menu_xml = run_dir / "profile-menu.xml"
224
+ root = self._dump_or_none(device, menu_xml)
225
+ if root is not None and ui.tap_first(
226
+ self.adb,
227
+ device,
228
+ root,
229
+ partial_texts={"TikTok Studio", "创作者工具", "创作者中心"},
230
+ sleep_s=3,
231
+ ):
232
+ pass
233
+ else:
234
+ self._tap_ratio(device, 0.25, 0.32, "open TikTok Studio fallback")
235
+ time.sleep(3)
236
+ self.adb.screenshot(device.adb_serial, run_dir / "tiktok-studio.png")
237
+
238
+ studio_xml = run_dir / "tiktok-studio.xml"
239
+ root = self._dump_or_none(device, studio_xml)
240
+ if root is not None and ui.tap_first(
241
+ self.adb,
242
+ device,
243
+ root,
244
+ partial_texts={"数据分析", "Analytics"},
245
+ sleep_s=4,
246
+ ):
247
+ return
248
+ self._tap_ratio(device, 0.20, 0.32, "open TikTok Studio analytics fallback")
249
+ time.sleep(4)
250
+
251
+ def _tap_ratio(self, device: Device, x_ratio: float, y_ratio: float, label: str) -> None:
252
+ width, height = self._screen_size(device)
253
+ self._ok(self.adb.tap(device.adb_serial, int(width * x_ratio), int(height * y_ratio)), label)
254
+
255
+ def _screen_size(self, device: Device) -> tuple[int, int]:
256
+ result = self.adb.shell(device.adb_serial, "wm", "size")
257
+ if result.ok:
258
+ for part in result.stdout.split():
259
+ if "x" not in part:
260
+ continue
261
+ left, right = part.split("x", 1)
262
+ if left.isdigit() and right.isdigit():
263
+ return int(left), int(right)
264
+ return BASE_SCREEN
265
+
266
+ def _dump_or_none(self, device: Device, path: Path):
267
+ result = self.adb.dump_ui(device.adb_serial, path)
268
+ if not result.ok:
269
+ return None
270
+ try:
271
+ import xml.etree.ElementTree as ET
272
+
273
+ return ET.parse(path).getroot()
274
+ except Exception:
275
+ return None
276
+
277
+ def _ok(self, result: CommandResult, label: str) -> None:
278
+ if not result.ok:
279
+ raise RuntimeError(f"{label} failed: {result.stderr or result.stdout}")
280
+
281
+
282
+ def parse_manual_values_json(raw_json: str) -> dict[str, TikTokAccountMetricValues]:
283
+ if not raw_json.strip():
284
+ return {}
285
+ loaded = json.loads(raw_json)
286
+ if not isinstance(loaded, dict):
287
+ raise ValueError("manual values JSON must be an object keyed by period")
288
+
289
+ parsed: dict[str, TikTokAccountMetricValues] = {}
290
+ for raw_period, raw_values in loaded.items():
291
+ period = normalize_period_key(str(raw_period))
292
+ if period not in ACCOUNT_PERIODS:
293
+ raise ValueError(f"unsupported TikTok account metrics period: {raw_period}")
294
+ if not isinstance(raw_values, dict):
295
+ raise ValueError(f"manual values for {raw_period} must be an object")
296
+ parsed[period] = TikTokAccountMetricValues(
297
+ video_views=_optional_int(raw_values.get("video_views")),
298
+ profile_views=_optional_int(raw_values.get("profile_views")),
299
+ total_likes=_optional_int(raw_values.get("total_likes")),
300
+ total_comments=_optional_int(raw_values.get("total_comments")),
301
+ shares=_optional_int(raw_values.get("shares")),
302
+ date_range=str(raw_values.get("date_range", "")),
303
+ raw=dict(raw_values.get("raw") or {}),
304
+ )
305
+ return parsed
306
+
307
+
308
+ def normalize_period_key(value: str) -> str:
309
+ key = value.strip().lower().replace(" ", "")
310
+ aliases = {
311
+ "7": "7d",
312
+ "7d": "7d",
313
+ "7day": "7d",
314
+ "7days": "7d",
315
+ "7天": "7d",
316
+ "28": "28d",
317
+ "28d": "28d",
318
+ "28day": "28d",
319
+ "28days": "28d",
320
+ "28天": "28d",
321
+ "60": "60d",
322
+ "60d": "60d",
323
+ "60day": "60d",
324
+ "60days": "60d",
325
+ "60天": "60d",
326
+ }
327
+ return aliases.get(key, key)
328
+
329
+
330
+ def _normalize_periods(periods: list[str] | None) -> list[str]:
331
+ if not periods:
332
+ return list(ACCOUNT_PERIODS)
333
+ normalized: list[str] = []
334
+ for period in periods:
335
+ key = normalize_period_key(period)
336
+ if key not in ACCOUNT_PERIODS:
337
+ raise ValueError(f"unsupported TikTok account metrics period: {period}")
338
+ if key not in normalized:
339
+ normalized.append(key)
340
+ return normalized
341
+
342
+
343
+ def _scale_coord(coord: tuple[int, int], width: int, height: int) -> tuple[int, int]:
344
+ base_width, base_height = BASE_SCREEN
345
+ x, y = coord
346
+ return round(x / base_width * width), round(y / base_height * height)
347
+
348
+
349
+ def _png_size(path: Path) -> tuple[int, int] | None:
350
+ try:
351
+ with path.open("rb") as handle:
352
+ header = handle.read(24)
353
+ if len(header) < 24 or header[:8] != b"\x89PNG\r\n\x1a\n":
354
+ return None
355
+ return struct.unpack(">II", header[16:24])
356
+ except Exception:
357
+ return None
358
+
359
+
360
+ def _optional_int(value: Any) -> int | None:
361
+ if value in (None, ""):
362
+ return None
363
+ return int(value)
364
+
365
+
366
+ def _now_shanghai() -> str:
367
+ return datetime.now(ZoneInfo("Asia/Shanghai")).replace(microsecond=0).isoformat()