@11agents/cli 0.1.41 → 0.1.43
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/mobile-runtime/python/src/device_control/cli.py +17 -2
- package/mobile-runtime/python/src/device_control/metrics/tiktok_video_adb.py +8 -3
- package/mobile-runtime/python/src/device_control/publishers/tiktok_adb.py +679 -12
- package/mobile-runtime/python/src/device_control/publishers/tiktok_appium.py +173 -14
- package/mobile-runtime/python/src/device_control/publishers/x_adb.py +319 -5
- package/mobile-runtime/skills/android-collect-tiktok-metrics/SKILL.md +76 -0
- package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +3 -3
- package/mobile-runtime/skills/android-publish-x/SKILL.md +5 -1
- package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +1 -1
- package/mobile-runtime/skills/mobile-publish-data-collection/SKILL.md +79 -3
- package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/knowledge.js +23 -1
- package/src/commands/mobile.js +29 -6
- package/src/commands/runtime.js +4 -1
- package/src/schema.js +5 -2
|
@@ -10,7 +10,12 @@ from device_control.adb import AdbClient
|
|
|
10
10
|
from device_control.appium_client import AppiumClient
|
|
11
11
|
from device_control.models import Device, PublishRecord, utc_now_iso
|
|
12
12
|
from device_control.publishers import ui_helpers as ui
|
|
13
|
-
from device_control.publishers.tiktok_adb import
|
|
13
|
+
from device_control.publishers.tiktok_adb import (
|
|
14
|
+
TIKTOK_PACKAGE,
|
|
15
|
+
TikTokAdbLinkResult,
|
|
16
|
+
TikTokAdbPublisher,
|
|
17
|
+
_is_tiktok_create_home_surface,
|
|
18
|
+
)
|
|
14
19
|
from device_control.store import append_publish_record
|
|
15
20
|
|
|
16
21
|
|
|
@@ -27,6 +32,12 @@ class TikTokAppiumPublishResult:
|
|
|
27
32
|
duration_seconds: int
|
|
28
33
|
remote_media_path: str
|
|
29
34
|
screenshot_path: str
|
|
35
|
+
platform_permalink: str = ""
|
|
36
|
+
platform_post_id: str = ""
|
|
37
|
+
link_status: str = ""
|
|
38
|
+
link_error: str = ""
|
|
39
|
+
link_screenshot_path: str = ""
|
|
40
|
+
clipboard_method: str = ""
|
|
30
41
|
error: str = ""
|
|
31
42
|
|
|
32
43
|
|
|
@@ -45,7 +56,12 @@ class TikTokAppiumPublisher:
|
|
|
45
56
|
self.appium_server = appium_server
|
|
46
57
|
self.records_path = Path(records_path)
|
|
47
58
|
self.artifact_root = Path(artifact_root)
|
|
48
|
-
self.media_helper = TikTokAdbPublisher(
|
|
59
|
+
self.media_helper = TikTokAdbPublisher(
|
|
60
|
+
adb,
|
|
61
|
+
appium_server=appium_server,
|
|
62
|
+
records_path=records_path,
|
|
63
|
+
artifact_root=artifact_root,
|
|
64
|
+
)
|
|
49
65
|
|
|
50
66
|
def publish_video(
|
|
51
67
|
self,
|
|
@@ -68,6 +84,8 @@ class TikTokAppiumPublisher:
|
|
|
68
84
|
remote = f"/sdcard/DCIM/Camera/groupctl-{device.device_id}-tiktok-appium-{stamp}{local.suffix or '.mp4'}"
|
|
69
85
|
record_id = f"pub_{stamp.replace('-', '')}_{device.device_id.lower()}_tiktok_appium"
|
|
70
86
|
appium = AppiumClient(self.appium_server)
|
|
87
|
+
appium_active = False
|
|
88
|
+
link_result: TikTokAdbLinkResult | None = None
|
|
71
89
|
|
|
72
90
|
try:
|
|
73
91
|
self._prepare_device(device)
|
|
@@ -77,6 +95,7 @@ class TikTokAppiumPublisher:
|
|
|
77
95
|
time.sleep(1)
|
|
78
96
|
|
|
79
97
|
appium.start_session(_capabilities(device.adb_serial))
|
|
98
|
+
appium_active = True
|
|
80
99
|
time.sleep(5)
|
|
81
100
|
|
|
82
101
|
appium.source(run_dir / "home.xml")
|
|
@@ -114,8 +133,24 @@ class TikTokAppiumPublisher:
|
|
|
114
133
|
if dry_run:
|
|
115
134
|
status = "dry_run"
|
|
116
135
|
else:
|
|
117
|
-
self._tap_publish_button(device)
|
|
136
|
+
self._tap_publish_button(device, run_dir)
|
|
137
|
+
try:
|
|
138
|
+
appium.delete_session()
|
|
139
|
+
finally:
|
|
140
|
+
appium_active = False
|
|
118
141
|
final_shot = self.media_helper._wait_for_publish_confirmation(device, run_dir)
|
|
142
|
+
link_result = self.media_helper.copy_current_or_latest_video_link(
|
|
143
|
+
device,
|
|
144
|
+
run_dir=run_dir / "link",
|
|
145
|
+
video_order=1,
|
|
146
|
+
close_after_recovery=True,
|
|
147
|
+
)
|
|
148
|
+
if link_result.status != "retrieved" or not link_result.platform_permalink:
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
"TikTok post-publish link recovery failed: "
|
|
151
|
+
f"{link_result.error or link_result.status}"
|
|
152
|
+
)
|
|
153
|
+
final_shot = Path(link_result.screenshot_path)
|
|
119
154
|
status = "published"
|
|
120
155
|
|
|
121
156
|
ended_epoch = int(time.time())
|
|
@@ -130,13 +165,14 @@ class TikTokAppiumPublisher:
|
|
|
130
165
|
post_type="video",
|
|
131
166
|
local_media_path=str(local),
|
|
132
167
|
remote_media_path=remote,
|
|
168
|
+
platform_post_id=link_result.platform_post_id if link_result else "",
|
|
169
|
+
platform_permalink=link_result.platform_permalink if link_result else "",
|
|
133
170
|
caption=caption,
|
|
134
171
|
published_at=utc_now_iso(),
|
|
135
172
|
result_screenshot_path=str(final_shot),
|
|
136
173
|
status="published",
|
|
137
174
|
),
|
|
138
175
|
)
|
|
139
|
-
self.media_helper._close_tiktok_best_effort(device, run_dir)
|
|
140
176
|
return TikTokAppiumPublishResult(
|
|
141
177
|
device_id=device.device_id,
|
|
142
178
|
status=status,
|
|
@@ -146,6 +182,12 @@ class TikTokAppiumPublisher:
|
|
|
146
182
|
duration_seconds=ended_epoch - started_epoch,
|
|
147
183
|
remote_media_path=remote,
|
|
148
184
|
screenshot_path=str(final_shot),
|
|
185
|
+
platform_permalink=link_result.platform_permalink if link_result else "",
|
|
186
|
+
platform_post_id=link_result.platform_post_id if link_result else "",
|
|
187
|
+
link_status=link_result.status if link_result else "",
|
|
188
|
+
link_error=link_result.error if link_result else "",
|
|
189
|
+
link_screenshot_path=link_result.screenshot_path if link_result else "",
|
|
190
|
+
clipboard_method=link_result.clipboard_method if link_result else "",
|
|
149
191
|
)
|
|
150
192
|
except Exception as exc:
|
|
151
193
|
failure_shot = run_dir / "failed.png"
|
|
@@ -153,6 +195,7 @@ class TikTokAppiumPublisher:
|
|
|
153
195
|
appium.screenshot(failure_shot)
|
|
154
196
|
except Exception:
|
|
155
197
|
self.adb.screenshot(device.adb_serial, failure_shot)
|
|
198
|
+
self.media_helper._close_tiktok_best_effort(device, run_dir)
|
|
156
199
|
return TikTokAppiumPublishResult(
|
|
157
200
|
device_id=device.device_id,
|
|
158
201
|
status="failed",
|
|
@@ -162,13 +205,20 @@ class TikTokAppiumPublisher:
|
|
|
162
205
|
duration_seconds=int(time.time()) - started_epoch,
|
|
163
206
|
remote_media_path=remote,
|
|
164
207
|
screenshot_path=str(failure_shot),
|
|
208
|
+
platform_permalink=link_result.platform_permalink if link_result else "",
|
|
209
|
+
platform_post_id=link_result.platform_post_id if link_result else "",
|
|
210
|
+
link_status=link_result.status if link_result else "",
|
|
211
|
+
link_error=link_result.error if link_result else "",
|
|
212
|
+
link_screenshot_path=link_result.screenshot_path if link_result else "",
|
|
213
|
+
clipboard_method=link_result.clipboard_method if link_result else "",
|
|
165
214
|
error=str(exc),
|
|
166
215
|
)
|
|
167
216
|
finally:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
if appium_active:
|
|
218
|
+
try:
|
|
219
|
+
appium.delete_session()
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
172
222
|
|
|
173
223
|
def _prepare_device(self, device: Device) -> None:
|
|
174
224
|
self._ok(self.adb.wake(device.adb_serial), "wake device")
|
|
@@ -187,6 +237,9 @@ class TikTokAppiumPublisher:
|
|
|
187
237
|
raise RuntimeError(f"{action} failed: {last_error}")
|
|
188
238
|
|
|
189
239
|
def _set_caption(self, appium: AppiumClient, device: Device, caption: str, run_dir: Path) -> None:
|
|
240
|
+
if caption.isascii():
|
|
241
|
+
self._set_caption_with_adb(device, caption, run_dir)
|
|
242
|
+
return
|
|
190
243
|
try:
|
|
191
244
|
field = appium.find_element("id", f"{TIKTOK_PACKAGE}:id/gjp")
|
|
192
245
|
appium.click(field)
|
|
@@ -200,20 +253,76 @@ class TikTokAppiumPublisher:
|
|
|
200
253
|
self.adb.screenshot(device.adb_serial, run_dir / "caption-failed.png")
|
|
201
254
|
raise RuntimeError(f"caption input failed: {exc}") from exc
|
|
202
255
|
|
|
256
|
+
def _set_caption_with_adb(self, device: Device, caption: str, run_dir: Path) -> None:
|
|
257
|
+
try:
|
|
258
|
+
node = None
|
|
259
|
+
try:
|
|
260
|
+
root = ui.dump_ui(self.adb, device, run_dir / "before-caption.xml")
|
|
261
|
+
node = ui.find_node(
|
|
262
|
+
root,
|
|
263
|
+
partial_texts={"添加描述", "描述", "Add description", "Describe"},
|
|
264
|
+
clickable_only=False,
|
|
265
|
+
)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
if node is not None:
|
|
269
|
+
ui.tap_node(self.adb, device, node, sleep_s=1)
|
|
270
|
+
else:
|
|
271
|
+
width, height = self._screen_size(device)
|
|
272
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.18), int(height * 0.13)), "focus caption field")
|
|
273
|
+
time.sleep(1)
|
|
274
|
+
result = self.adb.input_text(device.adb_serial, caption)
|
|
275
|
+
if not result.ok:
|
|
276
|
+
raise RuntimeError(result.stderr or result.stdout or "adb input text failed")
|
|
277
|
+
time.sleep(1)
|
|
278
|
+
self.adb.keyevent(device.adb_serial, "KEYCODE_BACK")
|
|
279
|
+
time.sleep(1)
|
|
280
|
+
self.adb.screenshot(device.adb_serial, run_dir / "after-caption.png")
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
self.adb.screenshot(device.adb_serial, run_dir / "caption-failed.png")
|
|
283
|
+
raise RuntimeError(f"caption input failed: {exc}") from exc
|
|
284
|
+
|
|
203
285
|
def _tap_album_entry(self, device: Device) -> None:
|
|
204
|
-
self.
|
|
286
|
+
width, height = self._screen_size(device)
|
|
287
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.09), int(height * 0.92)), "open album")
|
|
205
288
|
|
|
206
289
|
def _tap_first_gallery_video(self, device: Device) -> None:
|
|
207
|
-
self.
|
|
290
|
+
width, height = self._screen_size(device)
|
|
291
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.28), int(height * 0.20)), "select first gallery video")
|
|
208
292
|
|
|
209
293
|
def _tap_gallery_next(self, device: Device) -> None:
|
|
210
|
-
self.
|
|
294
|
+
width, height = self._screen_size(device)
|
|
295
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.73), int(height * 0.92)), "next from gallery")
|
|
211
296
|
|
|
212
297
|
def _tap_editor_next(self, device: Device) -> None:
|
|
213
|
-
self.
|
|
298
|
+
width, height = self._screen_size(device)
|
|
299
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.74), int(height * 0.93)), "next from editor")
|
|
300
|
+
|
|
301
|
+
def _tap_publish_button(self, device: Device, run_dir: Path) -> None:
|
|
302
|
+
width, height = self._screen_size(device)
|
|
303
|
+
for attempt in range(3):
|
|
304
|
+
root = None
|
|
305
|
+
try:
|
|
306
|
+
root = ui.dump_ui(self.adb, device, run_dir / f"before-publish-tap-{attempt + 1}.xml")
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
if root is not None:
|
|
310
|
+
node = _find_publish_button(root, width, height, prefer_bottom=True)
|
|
311
|
+
if node is not None:
|
|
312
|
+
ui.tap_node(self.adb, device, node, sleep_s=2)
|
|
313
|
+
else:
|
|
314
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.75), int(height * 0.92)), "publish")
|
|
315
|
+
time.sleep(2)
|
|
316
|
+
else:
|
|
317
|
+
self._ok(self.adb.tap(device.adb_serial, int(width * 0.75), int(height * 0.92)), "publish")
|
|
318
|
+
time.sleep(2)
|
|
319
|
+
|
|
320
|
+
self.adb.screenshot(device.adb_serial, run_dir / f"after-publish-tap-{attempt + 1}.png")
|
|
321
|
+
if not self._still_on_publish_form(device, run_dir, attempt + 1):
|
|
322
|
+
self.adb.screenshot(device.adb_serial, run_dir / "after-publish-tap.png")
|
|
323
|
+
return
|
|
214
324
|
|
|
215
|
-
|
|
216
|
-
self._ok(self.adb.tap(device.adb_serial, 790, 2240), "publish")
|
|
325
|
+
self.adb.screenshot(device.adb_serial, run_dir / "after-publish-tap.png")
|
|
217
326
|
|
|
218
327
|
def _raise_if_live_surface(self, device: Device, run_dir: Path, action: str) -> None:
|
|
219
328
|
try:
|
|
@@ -221,6 +330,8 @@ class TikTokAppiumPublisher:
|
|
|
221
330
|
except Exception:
|
|
222
331
|
return
|
|
223
332
|
text = ui.all_text(root)
|
|
333
|
+
if _is_tiktok_create_home_surface(root):
|
|
334
|
+
raise RuntimeError(f"{action} failed: TikTok is still on the create camera/template surface")
|
|
224
335
|
if (
|
|
225
336
|
("访问你的麦克风" in text or "访问麦克风" in text)
|
|
226
337
|
and ("直播" in text or "LIVE" in text.upper())
|
|
@@ -234,6 +345,17 @@ class TikTokAppiumPublisher:
|
|
|
234
345
|
if not result.ok:
|
|
235
346
|
raise RuntimeError(f"{action} failed: {result.stderr or result.stdout}")
|
|
236
347
|
|
|
348
|
+
def _screen_size(self, device: Device) -> tuple[int, int]:
|
|
349
|
+
return self.media_helper._screen_size(device)
|
|
350
|
+
|
|
351
|
+
def _still_on_publish_form(self, device: Device, run_dir: Path, attempt: int) -> bool:
|
|
352
|
+
try:
|
|
353
|
+
root = ui.dump_ui(self.adb, device, run_dir / f"after-publish-tap-{attempt}.xml")
|
|
354
|
+
except Exception:
|
|
355
|
+
return False
|
|
356
|
+
text = ui.all_text(root)
|
|
357
|
+
return "发布" in text and ("粉丝可以查看这条发布内容" in text or "更多选项" in text or "分享到" in text)
|
|
358
|
+
|
|
237
359
|
|
|
238
360
|
def _capabilities(serial: str) -> dict:
|
|
239
361
|
return {
|
|
@@ -258,3 +380,40 @@ def _now_shanghai() -> str:
|
|
|
258
380
|
|
|
259
381
|
def _slug(value: str) -> str:
|
|
260
382
|
return "".join(char.lower() if char.isalnum() else "-" for char in value).strip("-")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _find_publish_button(root, width: int, height: int, *, prefer_bottom: bool):
|
|
386
|
+
candidates = []
|
|
387
|
+
for node in root.iter("node"):
|
|
388
|
+
if not ui.is_visible(node):
|
|
389
|
+
continue
|
|
390
|
+
text = node.attrib.get("text", "")
|
|
391
|
+
desc = node.attrib.get("content-desc", "")
|
|
392
|
+
if text not in {"发布", "Post"} and desc not in {"发布", "Post"}:
|
|
393
|
+
continue
|
|
394
|
+
value = ui.bounds(node)
|
|
395
|
+
if not value:
|
|
396
|
+
continue
|
|
397
|
+
left, top, right, bottom = value
|
|
398
|
+
center_x = (left + right) // 2
|
|
399
|
+
center_y = (top + bottom) // 2
|
|
400
|
+
if center_x < int(width * 0.45):
|
|
401
|
+
continue
|
|
402
|
+
if center_y < int(height * 0.22):
|
|
403
|
+
region = "top"
|
|
404
|
+
elif center_y > int(height * 0.72):
|
|
405
|
+
region = "bottom"
|
|
406
|
+
else:
|
|
407
|
+
continue
|
|
408
|
+
candidates.append((region, node))
|
|
409
|
+
if prefer_bottom:
|
|
410
|
+
bottom = [node for region, node in candidates if region == "bottom"]
|
|
411
|
+
if bottom:
|
|
412
|
+
return ui.sorted_visible_nodes(bottom)[-1]
|
|
413
|
+
top = [node for region, node in candidates if region == "top"]
|
|
414
|
+
if top:
|
|
415
|
+
return ui.sorted_visible_nodes(top)[-1]
|
|
416
|
+
bottom = [node for region, node in candidates if region == "bottom"]
|
|
417
|
+
if bottom:
|
|
418
|
+
return ui.sorted_visible_nodes(bottom)[-1]
|
|
419
|
+
return None
|