@11agents/cli 0.1.36 → 0.1.38
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/data-templates/README.md +2 -1
- package/mobile-runtime/data-templates/devices.example.csv +11 -2
- package/mobile-runtime/python/src/device_control/cli.py +92 -29
- package/mobile-runtime/python/src/device_control/models.py +1 -0
- package/mobile-runtime/python/src/device_control/store.py +1 -0
- package/mobile-runtime/skills/android-publish-tiktok/SKILL.md +2 -0
- package/mobile-runtime/skills/android-publish-xiaohongshu/SKILL.md +2 -0
- package/mobile-runtime/skills/mobile-publish-execution/SKILL.md +3 -2
- package/package.json +1 -1
- package/src/commands/mobile.js +125 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# 11agents Mobile Data Templates
|
|
2
2
|
|
|
3
3
|
These files seed `~/.11agents/mobile/data` during `11agents mobile setup`.
|
|
4
|
-
|
|
4
|
+
`devices.example.csv` contains the stable D01-D10 device registry. Keep those hardware ids in sync across runtime machines so a physical device keeps the same Dxx id even when it is attached through a different USB port or ADB transport.
|
|
5
|
+
Replace example accounts with company-owned, authorized Android accounts before publishing.
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
device_id,adb_serial,ip_address,brand,model,android_version,status,notes
|
|
2
|
-
D01,
|
|
1
|
+
device_id,adb_serial,hardware_id,ip_address,brand,model,android_version,status,notes
|
|
2
|
+
D01,e6209341,e6209341,,,,,active,stable device registry slot 1
|
|
3
|
+
D02,7441ce65,7441ce65,,,,,active,stable device registry slot 2
|
|
4
|
+
D03,2eecd64c,2eecd64c,,,,,active,stable device registry slot 3
|
|
5
|
+
D04,bfa3e5ba,bfa3e5ba,,,,,active,stable device registry slot 4
|
|
6
|
+
D05,78d99cb0,78d99cb0,,,,,active,stable device registry slot 5
|
|
7
|
+
D06,2c461593,2c461593,,,,,active,stable device registry slot 6
|
|
8
|
+
D07,2cd62d93,2cd62d93,,,,,active,stable device registry slot 7
|
|
9
|
+
D08,f8cabef1,f8cabef1,,,,,active,stable device registry slot 8
|
|
10
|
+
D09,8b427827,8b427827,,,,,active,stable device registry slot 9
|
|
11
|
+
D10,3185bee8,3185bee8,,,,,active,stable device registry slot 10
|
|
@@ -4,7 +4,7 @@ import argparse
|
|
|
4
4
|
import csv
|
|
5
5
|
import json
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
-
from dataclasses import asdict
|
|
7
|
+
from dataclasses import asdict, replace
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
from .adb import AdbClient, resolve_adb_bin
|
|
@@ -349,7 +349,7 @@ def main() -> None:
|
|
|
349
349
|
return
|
|
350
350
|
|
|
351
351
|
if args.command == "ensure-appium":
|
|
352
|
-
udids = _resolve_appium_udids(args.devices, args.device, args.udid)
|
|
352
|
+
udids = _resolve_appium_udids(args.devices, args.device, args.udid, adb)
|
|
353
353
|
try:
|
|
354
354
|
result = ensure_appium_server(
|
|
355
355
|
args.server,
|
|
@@ -387,12 +387,13 @@ def main() -> None:
|
|
|
387
387
|
return
|
|
388
388
|
|
|
389
389
|
if args.command == "list-devices":
|
|
390
|
-
devices =
|
|
390
|
+
devices = _load_devices(args.devices, adb, resolve_transports=args.health)
|
|
391
391
|
connected = set(adb.list_devices()) if args.health else set()
|
|
392
392
|
for device in devices.values():
|
|
393
393
|
row = {
|
|
394
394
|
"device_id": device.device_id,
|
|
395
395
|
"adb_serial": device.adb_serial,
|
|
396
|
+
"hardware_id": device.hardware_id,
|
|
396
397
|
"ip_address": device.ip_address,
|
|
397
398
|
"brand": device.brand,
|
|
398
399
|
"model": device.model,
|
|
@@ -413,7 +414,7 @@ def main() -> None:
|
|
|
413
414
|
if args.health:
|
|
414
415
|
suffix = f" online={row['online']} battery={row.get('battery', '')} focus={row.get('focus', '')}"
|
|
415
416
|
print(
|
|
416
|
-
f"{device.device_id} serial={device.adb_serial} ip={device.ip_address} "
|
|
417
|
+
f"{device.device_id} serial={device.adb_serial} hardware_id={device.hardware_id} ip={device.ip_address} "
|
|
417
418
|
f"model={device.model} status={device.status}{suffix}"
|
|
418
419
|
)
|
|
419
420
|
return
|
|
@@ -428,6 +429,7 @@ def main() -> None:
|
|
|
428
429
|
row = {
|
|
429
430
|
"device_id": args.device_id,
|
|
430
431
|
"adb_serial": serial,
|
|
432
|
+
"hardware_id": _device_hardware_id(adb, serial) or serial,
|
|
431
433
|
"ip_address": _ip_from_serial(serial),
|
|
432
434
|
"brand": adb.getprop(serial, "ro.product.brand"),
|
|
433
435
|
"model": adb.getprop(serial, "ro.product.model"),
|
|
@@ -443,33 +445,34 @@ def main() -> None:
|
|
|
443
445
|
return
|
|
444
446
|
|
|
445
447
|
if args.command == "connect":
|
|
446
|
-
devices =
|
|
448
|
+
devices = _load_devices(args.devices, adb, resolve_transports=False)
|
|
447
449
|
for device in devices.values():
|
|
448
450
|
if device.status != "active":
|
|
449
451
|
print(f"{device.device_id}: skipped status={device.status}")
|
|
450
452
|
continue
|
|
451
|
-
|
|
453
|
+
target = device.ip_address or device.adb_serial
|
|
454
|
+
result = adb.connect(target)
|
|
452
455
|
status = "ok" if result.ok else "fail"
|
|
453
|
-
print(f"{device.device_id} {
|
|
456
|
+
print(f"{device.device_id} {target}: {status} {result.stdout or result.stderr}")
|
|
454
457
|
return
|
|
455
458
|
|
|
456
459
|
if args.command == "health":
|
|
457
|
-
devices =
|
|
460
|
+
devices = _load_devices(args.devices, adb)
|
|
458
461
|
connected = set(adb.list_devices())
|
|
459
462
|
for device in devices.values():
|
|
460
463
|
online = device.adb_serial in connected
|
|
461
464
|
if not online:
|
|
462
|
-
print(f"{device.device_id}: offline serial={device.adb_serial}")
|
|
465
|
+
print(f"{device.device_id}: offline serial={device.adb_serial} hardware_id={device.hardware_id}")
|
|
463
466
|
continue
|
|
464
467
|
battery = adb.battery_level(device.adb_serial)
|
|
465
468
|
model = adb.getprop(device.adb_serial, "ro.product.model")
|
|
466
469
|
android = adb.getprop(device.adb_serial, "ro.build.version.release")
|
|
467
470
|
focus = adb.current_focus(device.adb_serial)
|
|
468
|
-
print(f"{device.device_id}: online battery={battery} model={model} android={android} focus={focus}")
|
|
471
|
+
print(f"{device.device_id}: online battery={battery} serial={device.adb_serial} hardware_id={device.hardware_id} model={model} android={android} focus={focus}")
|
|
469
472
|
return
|
|
470
473
|
|
|
471
474
|
if args.command == "screenshot":
|
|
472
|
-
device = _get_device(args.devices, args.device_id)
|
|
475
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
473
476
|
out = Path(args.out_dir) / f"{device.device_id}.png"
|
|
474
477
|
result = adb.screenshot(device.adb_serial, out)
|
|
475
478
|
if result.ok:
|
|
@@ -479,7 +482,7 @@ def main() -> None:
|
|
|
479
482
|
return
|
|
480
483
|
|
|
481
484
|
if args.command == "dump-ui":
|
|
482
|
-
device = _get_device(args.devices, args.device_id)
|
|
485
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
483
486
|
out = Path(args.out_dir) / f"{device.device_id}.xml"
|
|
484
487
|
result = adb.dump_ui(device.adb_serial, out)
|
|
485
488
|
if result.ok:
|
|
@@ -490,25 +493,25 @@ def main() -> None:
|
|
|
490
493
|
return
|
|
491
494
|
|
|
492
495
|
if args.command == "tap":
|
|
493
|
-
device = _get_device(args.devices, args.device_id)
|
|
496
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
494
497
|
result = adb.tap(device.adb_serial, args.x, args.y)
|
|
495
498
|
print(result.stdout or result.stderr)
|
|
496
499
|
return
|
|
497
500
|
|
|
498
501
|
if args.command == "keyevent":
|
|
499
|
-
device = _get_device(args.devices, args.device_id)
|
|
502
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
500
503
|
result = adb.keyevent(device.adb_serial, args.keycode)
|
|
501
504
|
print(result.stdout or result.stderr)
|
|
502
505
|
return
|
|
503
506
|
|
|
504
507
|
if args.command == "input-text":
|
|
505
|
-
device = _get_device(args.devices, args.device_id)
|
|
508
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
506
509
|
result = adb.input_text(device.adb_serial, args.text)
|
|
507
510
|
print(result.stdout or result.stderr)
|
|
508
511
|
return
|
|
509
512
|
|
|
510
513
|
if args.command == "launch":
|
|
511
|
-
device = _get_device(args.devices, args.device_id)
|
|
514
|
+
device = _get_device(args.devices, args.device_id, adb)
|
|
512
515
|
result = adb.launch_package(device.adb_serial, args.package)
|
|
513
516
|
print(result.stdout or result.stderr)
|
|
514
517
|
return
|
|
@@ -632,7 +635,7 @@ def main() -> None:
|
|
|
632
635
|
return
|
|
633
636
|
|
|
634
637
|
if args.command == "collect-tiktok-account-metrics-adb":
|
|
635
|
-
device = _get_device(args.devices, args.device)
|
|
638
|
+
device = _get_device(args.devices, args.device, adb)
|
|
636
639
|
account_id = args.account_id or f"{args.account_prefix}_{device.device_id.lower()}"
|
|
637
640
|
manual_values = _load_tiktok_account_metric_values(args.values_json, args.values_file)
|
|
638
641
|
collector = TikTokAccountMetricsAdbCollector(adb, artifact_root=args.artifact_root)
|
|
@@ -673,7 +676,7 @@ def main() -> None:
|
|
|
673
676
|
return
|
|
674
677
|
|
|
675
678
|
if args.command == "collect-tiktok-video-metrics-adb":
|
|
676
|
-
device = _get_device(args.devices, args.device)
|
|
679
|
+
device = _get_device(args.devices, args.device, adb)
|
|
677
680
|
account_id = args.account_id or f"{args.account_prefix}_{device.device_id.lower()}"
|
|
678
681
|
manual_values = _load_tiktok_video_metric_values(args.values_json, args.values_file)
|
|
679
682
|
collector = TikTokVideoMetricsAdbCollector(adb, artifact_root=args.artifact_root)
|
|
@@ -715,7 +718,7 @@ def main() -> None:
|
|
|
715
718
|
return
|
|
716
719
|
|
|
717
720
|
if args.command == "publish-tiktok-adb":
|
|
718
|
-
devices =
|
|
721
|
+
devices = _load_devices(args.devices, adb)
|
|
719
722
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
720
723
|
if not selected:
|
|
721
724
|
raise SystemExit("No target devices. Use --device D01 or --range 1-10.")
|
|
@@ -751,7 +754,7 @@ def main() -> None:
|
|
|
751
754
|
return
|
|
752
755
|
|
|
753
756
|
if args.command in {"publish-tiktok", "publish-tiktok-appium"}:
|
|
754
|
-
devices =
|
|
757
|
+
devices = _load_devices(args.devices, adb)
|
|
755
758
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
756
759
|
if not selected:
|
|
757
760
|
raise SystemExit("No target devices. Use --device D03 or --range 1-5.")
|
|
@@ -787,7 +790,7 @@ def main() -> None:
|
|
|
787
790
|
return
|
|
788
791
|
|
|
789
792
|
if args.command in {"publish-reddit", "publish-reddit-adb"}:
|
|
790
|
-
devices =
|
|
793
|
+
devices = _load_devices(args.devices, adb)
|
|
791
794
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
792
795
|
if not selected:
|
|
793
796
|
raise SystemExit("No target devices. Use --device D01 or --range 1-10.")
|
|
@@ -832,7 +835,7 @@ def main() -> None:
|
|
|
832
835
|
return
|
|
833
836
|
|
|
834
837
|
if args.command == "publish-facebook":
|
|
835
|
-
devices =
|
|
838
|
+
devices = _load_devices(args.devices, adb)
|
|
836
839
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
837
840
|
if not selected:
|
|
838
841
|
raise SystemExit("No target devices. Use --device D03 or --range 1-5.")
|
|
@@ -871,7 +874,7 @@ def main() -> None:
|
|
|
871
874
|
|
|
872
875
|
if args.command == "publish-instagram":
|
|
873
876
|
_apply_instagram_publish_policy(args)
|
|
874
|
-
devices =
|
|
877
|
+
devices = _load_devices(args.devices, adb)
|
|
875
878
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
876
879
|
if not selected:
|
|
877
880
|
raise SystemExit("No target devices. Use --device D03 or --range 1-5.")
|
|
@@ -911,7 +914,7 @@ def main() -> None:
|
|
|
911
914
|
return
|
|
912
915
|
|
|
913
916
|
if args.command == "publish-x":
|
|
914
|
-
devices =
|
|
917
|
+
devices = _load_devices(args.devices, adb)
|
|
915
918
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
916
919
|
if not selected:
|
|
917
920
|
raise SystemExit("No target devices. Use --device D03 or --range 1-5.")
|
|
@@ -955,7 +958,7 @@ def main() -> None:
|
|
|
955
958
|
if args.command == "publish-xiaohongshu":
|
|
956
959
|
_apply_xiaohongshu_publish_package(args)
|
|
957
960
|
_apply_xiaohongshu_live_publish_policy(args)
|
|
958
|
-
devices =
|
|
961
|
+
devices = _load_devices(args.devices, adb)
|
|
959
962
|
selected = _select_devices(devices, args.device, args.ranges)
|
|
960
963
|
if not selected:
|
|
961
964
|
raise SystemExit("No target devices. Use --device Dxx or --range 1-5.")
|
|
@@ -1011,7 +1014,7 @@ def main() -> None:
|
|
|
1011
1014
|
device_id = args.device or (record.device_id if record else "")
|
|
1012
1015
|
if not device_id:
|
|
1013
1016
|
raise SystemExit("copy-xiaohongshu-link requires --device or --record-id for a record with device_id")
|
|
1014
|
-
device = _get_device(args.devices, device_id)
|
|
1017
|
+
device = _get_device(args.devices, device_id, adb)
|
|
1015
1018
|
_ensure_appium_preflight(args, [device])
|
|
1016
1019
|
publisher = XiaohongshuAdbPublisher(
|
|
1017
1020
|
adb,
|
|
@@ -1048,17 +1051,77 @@ def main() -> None:
|
|
|
1048
1051
|
return
|
|
1049
1052
|
|
|
1050
1053
|
|
|
1051
|
-
|
|
1054
|
+
DEVICE_IDENTITY_PROPS = (
|
|
1055
|
+
"ro.serialno",
|
|
1056
|
+
"ro.boot.serialno",
|
|
1057
|
+
"ro.vendor.boot.serialno",
|
|
1058
|
+
"ro.product.serialno",
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _load_devices(devices_path: str, adb: AdbClient, *, resolve_transports: bool = True):
|
|
1052
1063
|
devices = load_devices(devices_path)
|
|
1064
|
+
if not resolve_transports:
|
|
1065
|
+
return devices
|
|
1066
|
+
return _resolve_device_transports(devices, adb)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def _resolve_device_transports(devices: dict[str, object], adb: AdbClient):
|
|
1070
|
+
transport_by_identity = _connected_transport_by_identity(adb)
|
|
1071
|
+
if not transport_by_identity:
|
|
1072
|
+
return devices
|
|
1073
|
+
resolved = {}
|
|
1074
|
+
for device_id, device in devices.items():
|
|
1075
|
+
transport = ""
|
|
1076
|
+
for identity in _device_identity_candidates(device):
|
|
1077
|
+
transport = transport_by_identity.get(identity)
|
|
1078
|
+
if transport:
|
|
1079
|
+
break
|
|
1080
|
+
resolved[device_id] = replace(device, adb_serial=transport) if transport and transport != device.adb_serial else device
|
|
1081
|
+
return resolved
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _connected_transport_by_identity(adb: AdbClient) -> dict[str, str]:
|
|
1085
|
+
out: dict[str, str] = {}
|
|
1086
|
+
for transport in adb.list_devices():
|
|
1087
|
+
for identity in {transport, _device_hardware_id(adb, transport)}:
|
|
1088
|
+
clean = _clean_device_identity(identity)
|
|
1089
|
+
if clean:
|
|
1090
|
+
out[clean] = transport
|
|
1091
|
+
return out
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _device_hardware_id(adb: AdbClient, transport: str) -> str:
|
|
1095
|
+
for prop in DEVICE_IDENTITY_PROPS:
|
|
1096
|
+
value = adb.getprop(transport, prop).strip()
|
|
1097
|
+
if value and value.lower() not in {"unknown", "null", "none"}:
|
|
1098
|
+
return value
|
|
1099
|
+
return ""
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _device_identity_candidates(device) -> list[str]:
|
|
1103
|
+
candidates = [
|
|
1104
|
+
getattr(device, "hardware_id", ""),
|
|
1105
|
+
getattr(device, "adb_serial", ""),
|
|
1106
|
+
]
|
|
1107
|
+
return [clean for clean in (_clean_device_identity(item) for item in candidates) if clean]
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _clean_device_identity(value: str) -> str:
|
|
1111
|
+
return str(value or "").strip().lower()
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _get_device(devices_path: str, device_id: str, adb: AdbClient):
|
|
1115
|
+
devices = _load_devices(devices_path, adb)
|
|
1053
1116
|
if device_id not in devices:
|
|
1054
1117
|
raise SystemExit(f"Unknown device_id={device_id}")
|
|
1055
1118
|
return devices[device_id]
|
|
1056
1119
|
|
|
1057
1120
|
|
|
1058
|
-
def _resolve_appium_udids(devices_path: str, device_ids: list[str], udids: list[str]) -> list[str]:
|
|
1121
|
+
def _resolve_appium_udids(devices_path: str, device_ids: list[str], udids: list[str], adb: AdbClient) -> list[str]:
|
|
1059
1122
|
resolved = [item for item in udids if item]
|
|
1060
1123
|
if device_ids:
|
|
1061
|
-
devices =
|
|
1124
|
+
devices = _load_devices(devices_path, adb)
|
|
1062
1125
|
for device_id in device_ids:
|
|
1063
1126
|
if device_id not in devices:
|
|
1064
1127
|
raise SystemExit(f"Unknown device_id={device_id}")
|
|
@@ -23,6 +23,7 @@ def load_devices(path: str | Path) -> dict[str, Device]:
|
|
|
23
23
|
device_id=row["device_id"],
|
|
24
24
|
adb_serial=row["adb_serial"],
|
|
25
25
|
ip_address=row.get("ip_address", ""),
|
|
26
|
+
hardware_id=row.get("hardware_id", "") or row.get("physical_id", "") or row["adb_serial"],
|
|
26
27
|
brand=row.get("brand", ""),
|
|
27
28
|
model=row.get("model", ""),
|
|
28
29
|
android_version=row.get("android_version", ""),
|
|
@@ -36,6 +36,8 @@ Use `--range`, `--parallel`, and `--max-concurrency N` only when the approved pl
|
|
|
36
36
|
|
|
37
37
|
Successful live publish is only confirmed after the CLI taps the publish button and verifies TikTok publish completion. After writing the publish record, the CLI force-stops TikTok and captures `after-app-close.png` in the run artifacts.
|
|
38
38
|
|
|
39
|
+
When the agent environment contains `ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK`, the CLI sends a DingTalk notification after status `published`; `ELEVENAGENTS_PUBLISH_DINGTALK_SECRET` is optional. TikTok notification content does not show a link.
|
|
40
|
+
|
|
39
41
|
## Output
|
|
40
42
|
|
|
41
43
|
Return publish status, record id, account/device, screenshot path, permalink or platform post id when available, duration, error action, and the data-collection handoff:
|
|
@@ -35,6 +35,8 @@ Structured package:
|
|
|
35
35
|
|
|
36
36
|
Successful live publish is only confirmed after the CLI taps the final publish button, waits without tapping while Xiaohongshu shows upload/progress state, closes and relaunches Xiaohongshu, then opens the "我" page, opens the note whose title matches the expected title or the first visible profile note as a verified fallback, and verifies the detail page against the expected title/body. The CLI recovers the note link by default for live publish; use `--no-copy-link-after-publish` only when link recovery is explicitly not wanted. After writing the publish record, the CLI force-stops Xiaohongshu and captures `after-app-close.png` in the run artifacts. If that evidence does not match, treat the result as `failed`; do not claim publish success from the publish button tap alone.
|
|
37
37
|
|
|
38
|
+
When the agent environment contains `ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK`, the CLI sends a DingTalk notification after status `published`; `ELEVENAGENTS_PUBLISH_DINGTALK_SECRET` is optional. The Xiaohongshu notification includes the recovered link when available and still sends if the link was not recovered.
|
|
39
|
+
|
|
38
40
|
Link copy after publish or during the recovery skill:
|
|
39
41
|
|
|
40
42
|
```bash
|
|
@@ -8,7 +8,7 @@ description: Use when a mobile publish agent needs to prepare or dry-run `11agen
|
|
|
8
8
|
## Required Inputs
|
|
9
9
|
- Approved execution plan or dry-run request.
|
|
10
10
|
- Installed `@11agents/cli` and completed `11agents mobile setup`.
|
|
11
|
-
- Optional placeholders: `ELEVENAGENTS_MOBILE_HOME`, `ELEVENAGENTS_MOBILE_DEFAULT_DEVICE`, `ELEVENAGENTS_MOBILE_DEFAULT_RANGE`, and `
|
|
11
|
+
- Optional placeholders: `ELEVENAGENTS_MOBILE_HOME`, `ELEVENAGENTS_MOBILE_DEFAULT_DEVICE`, `ELEVENAGENTS_MOBILE_DEFAULT_RANGE`, `ELEVENAGENTS_MOBILE_DEFAULT_MAX_CONCURRENCY`, `ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK`, and `ELEVENAGENTS_PUBLISH_DINGTALK_SECRET`.
|
|
12
12
|
- Media files, captions/titles/body/link fields, target device or range, optional account id/account prefix, and dry-run/live mode.
|
|
13
13
|
|
|
14
14
|
If mobile setup, device online status, media path, app login readiness, or approval status is missing, ask the user before running commands. Missing account CSV alone is not a blocker when the target app is already logged in on the device.
|
|
@@ -51,7 +51,8 @@ If mobile setup, device online status, media path, app login readiness, or appro
|
|
|
51
51
|
11. Use `11agents mobile publish-tiktok`, `publish-instagram`, `publish-facebook`, `publish-reddit`, `publish-x`, and `publish-xiaohongshu`.
|
|
52
52
|
12. Prefer `--json` and parse status, duration, record id, screenshot path, permalink, platform post id, and error.
|
|
53
53
|
13. Pass `--task-id <task_id>` so logs land at `~/.11agents/mobile/runs/<task_id>/log`.
|
|
54
|
-
14.
|
|
54
|
+
14. Do not run custom DingTalk webhook scripts after Xiaohongshu or TikTok publish. When the agent environment contains `ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK`, the CLI sends the publish-success notification automatically; `ELEVENAGENTS_PUBLISH_DINGTALK_SECRET` is optional for signed robots.
|
|
55
|
+
15. Stop on `failed`; surface screenshot and error instead of repeated retries.
|
|
55
56
|
|
|
56
57
|
## Output
|
|
57
58
|
Return readiness status, dry-run result or execution preparation summary, device/app health notes, optional account-registry notes, command family selected, exact safe `11agents mobile` command shape, log path, and missing env placeholders.
|
package/package.json
CHANGED
package/src/commands/mobile.js
CHANGED
|
@@ -99,8 +99,9 @@ Runtime:
|
|
|
99
99
|
Python/device-control code is bundled with @11agents/cli.
|
|
100
100
|
Mutable runtime state lives in ~/.11agents/mobile by default.
|
|
101
101
|
Every command writes logs under ~/.11agents/mobile/runs/<task_id>/log.
|
|
102
|
-
|
|
103
|
-
and optional
|
|
102
|
+
Xiaohongshu/TikTok publish-success DingTalk notification is enabled by
|
|
103
|
+
ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK and optional
|
|
104
|
+
ELEVENAGENTS_PUBLISH_DINGTALK_SECRET.`)
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function mobileHome(flags = {}, env = process.env) {
|
|
@@ -234,6 +235,104 @@ async function writeFileIfMissing(filePath, content) {
|
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
async function mergeDeviceRegistry(filePath, templateContent) {
|
|
239
|
+
let currentContent = ''
|
|
240
|
+
try {
|
|
241
|
+
currentContent = await readFile(filePath, 'utf8')
|
|
242
|
+
} catch {
|
|
243
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
244
|
+
await writeFile(filePath, templateContent)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
const current = parseCsv(currentContent)
|
|
248
|
+
const template = parseCsv(templateContent)
|
|
249
|
+
const headers = [...current.headers]
|
|
250
|
+
for (const header of template.headers) {
|
|
251
|
+
if (!headers.includes(header)) headers.push(header)
|
|
252
|
+
}
|
|
253
|
+
const rows = current.rows.map(row => ({ ...row }))
|
|
254
|
+
const rowByDeviceId = new Map(rows.map(row => [row.device_id, row]))
|
|
255
|
+
for (const seed of template.rows) {
|
|
256
|
+
const deviceId = seed.device_id
|
|
257
|
+
const existing = rowByDeviceId.get(deviceId)
|
|
258
|
+
if (!existing) {
|
|
259
|
+
rows.push({ ...seed })
|
|
260
|
+
rowByDeviceId.set(deviceId, rows[rows.length - 1])
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
if (isCanonicalDeviceId(deviceId)) {
|
|
264
|
+
const managedCanonicalRow = /^stable device registry slot/i.test(existing.notes || '')
|
|
265
|
+
const serialChanged = existing.adb_serial && existing.adb_serial !== seed.adb_serial
|
|
266
|
+
existing.adb_serial = seed.adb_serial
|
|
267
|
+
existing.hardware_id = seed.hardware_id || seed.adb_serial
|
|
268
|
+
if (managedCanonicalRow || serialChanged) {
|
|
269
|
+
existing.brand = seed.brand || ''
|
|
270
|
+
existing.model = seed.model || ''
|
|
271
|
+
existing.android_version = seed.android_version || ''
|
|
272
|
+
}
|
|
273
|
+
if (!existing.status) existing.status = seed.status || 'active'
|
|
274
|
+
if (!existing.notes || /replace with|single-device POC/i.test(existing.notes)) existing.notes = seed.notes || ''
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await writeFile(filePath, formatCsv(headers, rows))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isCanonicalDeviceId(deviceId = '') {
|
|
281
|
+
return /^D(?:0[1-9]|10)$/.test(String(deviceId))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseCsv(text = '') {
|
|
285
|
+
const lines = String(text || '').split(/\r?\n/).filter(line => line.length)
|
|
286
|
+
if (!lines.length) return { headers: [], rows: [] }
|
|
287
|
+
const headers = parseCsvLine(lines[0])
|
|
288
|
+
return {
|
|
289
|
+
headers,
|
|
290
|
+
rows: lines.slice(1).map(line => {
|
|
291
|
+
const values = parseCsvLine(line)
|
|
292
|
+
return Object.fromEntries(headers.map((header, index) => [header, values[index] || '']))
|
|
293
|
+
}),
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseCsvLine(line = '') {
|
|
298
|
+
const out = []
|
|
299
|
+
let value = ''
|
|
300
|
+
let quoted = false
|
|
301
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
302
|
+
const ch = line[i]
|
|
303
|
+
if (ch === '"') {
|
|
304
|
+
if (quoted && line[i + 1] === '"') {
|
|
305
|
+
value += '"'
|
|
306
|
+
i += 1
|
|
307
|
+
} else {
|
|
308
|
+
quoted = !quoted
|
|
309
|
+
}
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
if (ch === ',' && !quoted) {
|
|
313
|
+
out.push(value)
|
|
314
|
+
value = ''
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
value += ch
|
|
318
|
+
}
|
|
319
|
+
out.push(value)
|
|
320
|
+
return out
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatCsv(headers, rows) {
|
|
324
|
+
return [
|
|
325
|
+
headers.join(','),
|
|
326
|
+
...rows.map(row => headers.map(header => csvCell(row[header] || '')).join(',')),
|
|
327
|
+
].join('\n') + '\n'
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function csvCell(value) {
|
|
331
|
+
const text = String(value ?? '')
|
|
332
|
+
if (!/[",\n\r]/.test(text)) return text
|
|
333
|
+
return `"${text.replaceAll('"', '""')}"`
|
|
334
|
+
}
|
|
335
|
+
|
|
237
336
|
async function scaffoldMobileHome(home) {
|
|
238
337
|
await mkdir(home, { recursive: true })
|
|
239
338
|
await mkdir(path.join(home, 'data'), { recursive: true })
|
|
@@ -247,7 +346,7 @@ async function scaffoldMobileHome(home) {
|
|
|
247
346
|
path.join(home, 'data', 'accounts.csv'),
|
|
248
347
|
await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'accounts.example.csv'), 'utf8')
|
|
249
348
|
)
|
|
250
|
-
await
|
|
349
|
+
await mergeDeviceRegistry(
|
|
251
350
|
path.join(home, 'data', 'devices.csv'),
|
|
252
351
|
await readFile(path.join(MOBILE_DATA_TEMPLATE_ROOT, 'devices.example.csv'), 'utf8')
|
|
253
352
|
)
|
|
@@ -597,11 +696,10 @@ async function notifyMobilePublishSuccess({ command, context, parsed, env = proc
|
|
|
597
696
|
const config = dingTalkConfig(env)
|
|
598
697
|
if (!config.webhook || !Array.isArray(parsed)) return
|
|
599
698
|
const platform = platformFromMobileCommand(command)
|
|
600
|
-
if (!platform) return
|
|
699
|
+
if (!shouldNotifyPublishPlatform(platform)) return
|
|
601
700
|
for (const row of parsed) {
|
|
602
701
|
if (!isPublishedResult(row)) continue
|
|
603
702
|
const link = publishResultLink(row)
|
|
604
|
-
if (!link) continue
|
|
605
703
|
const content = buildPublishNotificationContent({
|
|
606
704
|
platform,
|
|
607
705
|
task: context.taskTitle || context.taskId,
|
|
@@ -623,8 +721,20 @@ async function notifyMobilePublishSuccess({ command, context, parsed, env = proc
|
|
|
623
721
|
|
|
624
722
|
function dingTalkConfig(env = process.env) {
|
|
625
723
|
return {
|
|
626
|
-
webhook: env.ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK
|
|
627
|
-
|
|
724
|
+
webhook: env.ELEVENAGENTS_PUBLISH_DINGTALK_WEBHOOK
|
|
725
|
+
|| env.ELEVENAGENTS_DINGTALK_WEBHOOK
|
|
726
|
+
|| env.GTM_DINGTALK_WEBHOOK
|
|
727
|
+
|| env.DINGTALK_WEBHOOK
|
|
728
|
+
|| '',
|
|
729
|
+
secret: env.ELEVENAGENTS_PUBLISH_DINGTALK_SECRET
|
|
730
|
+
|| env.ELEVENAGENTS_PUBLISH_DINGTALK_KEY
|
|
731
|
+
|| env.ELEVENAGENTS_DINGTALK_SECRET
|
|
732
|
+
|| env.ELEVENAGENTS_DINGTALK_KEY
|
|
733
|
+
|| env.GTM_DINGTALK_SECRET
|
|
734
|
+
|| env.GTM_DINGTALK_KEY
|
|
735
|
+
|| env.DINGTALK_SECRET
|
|
736
|
+
|| env.DINGTALK_KEY
|
|
737
|
+
|| '',
|
|
628
738
|
}
|
|
629
739
|
}
|
|
630
740
|
|
|
@@ -644,6 +754,10 @@ function isPublishedResult(row) {
|
|
|
644
754
|
return row && typeof row === 'object' && row.status === 'published'
|
|
645
755
|
}
|
|
646
756
|
|
|
757
|
+
function shouldNotifyPublishPlatform(platform) {
|
|
758
|
+
return platform === 'xiaohongshu' || platform === 'tiktok'
|
|
759
|
+
}
|
|
760
|
+
|
|
647
761
|
function publishResultLink(row) {
|
|
648
762
|
return String(
|
|
649
763
|
row.platform_permalink
|
|
@@ -658,7 +772,9 @@ function buildPublishNotificationContent({ platform, task, deviceId = '', link }
|
|
|
658
772
|
const label = platformLabel(platform)
|
|
659
773
|
const taskText = String(task || 'task').trim()
|
|
660
774
|
const deviceText = deviceId ? `(设备 ${deviceId})` : ''
|
|
661
|
-
return `${label} ${taskText}${deviceText}
|
|
775
|
+
if (platform === 'tiktok') return `${label} ${taskText}${deviceText}已发布`
|
|
776
|
+
const linkText = String(link || '').trim() || '未回收'
|
|
777
|
+
return `${label} ${taskText}${deviceText}已发布,链接:${linkText}`
|
|
662
778
|
}
|
|
663
779
|
|
|
664
780
|
function platformLabel(platform) {
|
|
@@ -831,6 +947,7 @@ export const mobileInternals = {
|
|
|
831
947
|
parseRawArgs,
|
|
832
948
|
platformFromMobileCommand,
|
|
833
949
|
publishResultLink,
|
|
950
|
+
shouldNotifyPublishPlatform,
|
|
834
951
|
signedDingTalkUrl,
|
|
835
952
|
stripWrapperFlags,
|
|
836
953
|
taskIdFrom,
|