@11agents/cli 0.1.23 → 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 +63 -28
@@ -0,0 +1,439 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Device:
11
+ device_id: str
12
+ adb_serial: str
13
+ ip_address: str
14
+ brand: str = ""
15
+ model: str = ""
16
+ android_version: str = ""
17
+ status: str = "active"
18
+ notes: str = ""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class PlatformAccount:
23
+ account_id: str
24
+ platform: str
25
+ username: str
26
+ bound_device_id: str
27
+ login_status: str = "unknown"
28
+ status: str = "active"
29
+ notes: str = ""
30
+
31
+
32
+ @dataclass
33
+ class PublishTask:
34
+ task_id: str
35
+ action: str
36
+ platform: str
37
+ post_type: str
38
+ account_id: str
39
+ device_id: str
40
+ target: str | None = None
41
+ title: str | None = None
42
+ body: str | None = None
43
+ link_url: str | None = None
44
+ media_path: str | None = None
45
+ caption: str | None = None
46
+ hashtags: list[str] = field(default_factory=list)
47
+ dry_run: bool = True
48
+ require_human_confirm: bool = True
49
+ status: str = "pending"
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict[str, Any]) -> "PublishTask":
53
+ return cls(
54
+ task_id=str(data["task_id"]),
55
+ action=str(data.get("action", "publish_content")),
56
+ platform=str(data["platform"]).lower(),
57
+ post_type=str(data["post_type"]).lower(),
58
+ account_id=str(data["account_id"]),
59
+ device_id=str(data["device_id"]),
60
+ target=data.get("target"),
61
+ title=data.get("title"),
62
+ body=data.get("body"),
63
+ link_url=data.get("link_url"),
64
+ media_path=data.get("media_path"),
65
+ caption=data.get("caption"),
66
+ hashtags=list(data.get("hashtags") or []),
67
+ dry_run=bool(data.get("dry_run", True)),
68
+ require_human_confirm=bool(data.get("require_human_confirm", True)),
69
+ status=str(data.get("status", "pending")),
70
+ )
71
+
72
+
73
+ def utc_now_iso() -> str:
74
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
75
+
76
+
77
+ def new_id(prefix: str) -> str:
78
+ return f"{prefix}_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}_{uuid4().hex[:8]}"
79
+
80
+
81
+ @dataclass
82
+ class PublishRecord:
83
+ record_id: str
84
+ platform: str
85
+ account_id: str
86
+ device_id: str
87
+ post_type: str
88
+ published_at: str
89
+ task_id: str = ""
90
+ local_media_path: str = ""
91
+ remote_media_path: str = ""
92
+ platform_post_id: str = ""
93
+ platform_permalink: str = ""
94
+ caption: str = ""
95
+ result_screenshot_path: str = ""
96
+ status: str = "published"
97
+ created_at: str = field(default_factory=utc_now_iso)
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: dict[str, Any]) -> "PublishRecord":
101
+ return cls(
102
+ record_id=str(data["record_id"]),
103
+ task_id=str(data.get("task_id", "")),
104
+ platform=str(data["platform"]).lower(),
105
+ account_id=str(data["account_id"]),
106
+ device_id=str(data["device_id"]),
107
+ post_type=str(data.get("post_type", "video")).lower(),
108
+ local_media_path=str(data.get("local_media_path", "")),
109
+ remote_media_path=str(data.get("remote_media_path", "")),
110
+ platform_post_id=str(data.get("platform_post_id", "")),
111
+ platform_permalink=str(data.get("platform_permalink", "")),
112
+ caption=str(data.get("caption", "")),
113
+ published_at=str(data["published_at"]),
114
+ result_screenshot_path=str(data.get("result_screenshot_path", "")),
115
+ status=str(data.get("status", "published")),
116
+ created_at=str(data.get("created_at", utc_now_iso())),
117
+ )
118
+
119
+ def to_dict(self) -> dict[str, Any]:
120
+ return {
121
+ "record_id": self.record_id,
122
+ "task_id": self.task_id,
123
+ "platform": self.platform,
124
+ "account_id": self.account_id,
125
+ "device_id": self.device_id,
126
+ "post_type": self.post_type,
127
+ "local_media_path": self.local_media_path,
128
+ "remote_media_path": self.remote_media_path,
129
+ "platform_post_id": self.platform_post_id,
130
+ "platform_permalink": self.platform_permalink,
131
+ "caption": self.caption,
132
+ "published_at": self.published_at,
133
+ "result_screenshot_path": self.result_screenshot_path,
134
+ "status": self.status,
135
+ "created_at": self.created_at,
136
+ }
137
+
138
+
139
+ @dataclass
140
+ class VideoMetricsSnapshot:
141
+ snapshot_id: str
142
+ record_id: str
143
+ platform: str
144
+ account_id: str
145
+ fetched_at: str
146
+ source: str
147
+ platform_post_id: str = ""
148
+ platform_permalink: str = ""
149
+ views: int | None = None
150
+ likes: int | None = None
151
+ comments: int | None = None
152
+ shares: int | None = None
153
+ saves: int | None = None
154
+ score: int | None = None
155
+ raw: dict[str, Any] = field(default_factory=dict)
156
+
157
+ @classmethod
158
+ def from_dict(cls, data: dict[str, Any]) -> "VideoMetricsSnapshot":
159
+ return cls(
160
+ snapshot_id=str(data["snapshot_id"]),
161
+ record_id=str(data["record_id"]),
162
+ platform=str(data["platform"]).lower(),
163
+ account_id=str(data["account_id"]),
164
+ platform_post_id=str(data.get("platform_post_id", "")),
165
+ platform_permalink=str(data.get("platform_permalink", "")),
166
+ fetched_at=str(data["fetched_at"]),
167
+ source=str(data.get("source", "unknown")),
168
+ views=_optional_int(data.get("views")),
169
+ likes=_optional_int(data.get("likes")),
170
+ comments=_optional_int(data.get("comments")),
171
+ shares=_optional_int(data.get("shares")),
172
+ saves=_optional_int(data.get("saves")),
173
+ score=_optional_int(data.get("score")),
174
+ raw=dict(data.get("raw") or {}),
175
+ )
176
+
177
+ def to_dict(self) -> dict[str, Any]:
178
+ return {
179
+ "snapshot_id": self.snapshot_id,
180
+ "record_id": self.record_id,
181
+ "platform": self.platform,
182
+ "account_id": self.account_id,
183
+ "platform_post_id": self.platform_post_id,
184
+ "platform_permalink": self.platform_permalink,
185
+ "fetched_at": self.fetched_at,
186
+ "source": self.source,
187
+ "views": self.views,
188
+ "likes": self.likes,
189
+ "comments": self.comments,
190
+ "shares": self.shares,
191
+ "saves": self.saves,
192
+ "score": self.score,
193
+ "raw": self.raw,
194
+ }
195
+
196
+
197
+ @dataclass
198
+ class TikTokAccountMetricsPeriod:
199
+ period_key: str
200
+ period_days: int
201
+ date_range: str = ""
202
+ video_views: int | None = None
203
+ profile_views: int | None = None
204
+ total_likes: int | None = None
205
+ total_comments: int | None = None
206
+ shares: int | None = None
207
+ screenshot_path: str = ""
208
+ ui_dump_path: str = ""
209
+ extraction_method: str = "manual_required"
210
+ raw: dict[str, Any] = field(default_factory=dict)
211
+
212
+ @classmethod
213
+ def from_dict(cls, data: dict[str, Any]) -> "TikTokAccountMetricsPeriod":
214
+ return cls(
215
+ period_key=str(data["period_key"]),
216
+ period_days=int(data["period_days"]),
217
+ date_range=str(data.get("date_range", "")),
218
+ video_views=_optional_int(data.get("video_views")),
219
+ profile_views=_optional_int(data.get("profile_views")),
220
+ total_likes=_optional_int(data.get("total_likes")),
221
+ total_comments=_optional_int(data.get("total_comments")),
222
+ shares=_optional_int(data.get("shares")),
223
+ screenshot_path=str(data.get("screenshot_path", "")),
224
+ ui_dump_path=str(data.get("ui_dump_path", "")),
225
+ extraction_method=str(data.get("extraction_method", "manual_required")),
226
+ raw=dict(data.get("raw") or {}),
227
+ )
228
+
229
+ def to_dict(self) -> dict[str, Any]:
230
+ return {
231
+ "period_key": self.period_key,
232
+ "period_days": self.period_days,
233
+ "date_range": self.date_range,
234
+ "video_views": self.video_views,
235
+ "profile_views": self.profile_views,
236
+ "total_likes": self.total_likes,
237
+ "total_comments": self.total_comments,
238
+ "shares": self.shares,
239
+ "screenshot_path": self.screenshot_path,
240
+ "ui_dump_path": self.ui_dump_path,
241
+ "extraction_method": self.extraction_method,
242
+ "raw": self.raw,
243
+ }
244
+
245
+ def has_all_metrics(self) -> bool:
246
+ return all(
247
+ value is not None
248
+ for value in (
249
+ self.video_views,
250
+ self.profile_views,
251
+ self.total_likes,
252
+ self.total_comments,
253
+ self.shares,
254
+ )
255
+ )
256
+
257
+
258
+ @dataclass
259
+ class TikTokAccountMetricsSnapshot:
260
+ snapshot_id: str
261
+ platform: str
262
+ account_id: str
263
+ device_id: str
264
+ fetched_at: str
265
+ source: str
266
+ status: str = "needs_human"
267
+ periods: dict[str, TikTokAccountMetricsPeriod] = field(default_factory=dict)
268
+ artifact_dir: str = ""
269
+ started_at: str = ""
270
+ ended_at: str = ""
271
+ duration_seconds: int | None = None
272
+ raw: dict[str, Any] = field(default_factory=dict)
273
+
274
+ @classmethod
275
+ def from_dict(cls, data: dict[str, Any]) -> "TikTokAccountMetricsSnapshot":
276
+ periods = {
277
+ str(key): TikTokAccountMetricsPeriod.from_dict(dict(value))
278
+ for key, value in dict(data.get("periods") or {}).items()
279
+ }
280
+ return cls(
281
+ snapshot_id=str(data["snapshot_id"]),
282
+ platform=str(data.get("platform", "tiktok")).lower(),
283
+ account_id=str(data["account_id"]),
284
+ device_id=str(data["device_id"]),
285
+ fetched_at=str(data["fetched_at"]),
286
+ source=str(data.get("source", "unknown")),
287
+ status=str(data.get("status", "needs_human")),
288
+ periods=periods,
289
+ artifact_dir=str(data.get("artifact_dir", "")),
290
+ started_at=str(data.get("started_at", "")),
291
+ ended_at=str(data.get("ended_at", "")),
292
+ duration_seconds=_optional_int(data.get("duration_seconds")),
293
+ raw=dict(data.get("raw") or {}),
294
+ )
295
+
296
+ def to_dict(self) -> dict[str, Any]:
297
+ return {
298
+ "snapshot_id": self.snapshot_id,
299
+ "platform": self.platform,
300
+ "account_id": self.account_id,
301
+ "device_id": self.device_id,
302
+ "fetched_at": self.fetched_at,
303
+ "source": self.source,
304
+ "status": self.status,
305
+ "periods": {key: period.to_dict() for key, period in self.periods.items()},
306
+ "artifact_dir": self.artifact_dir,
307
+ "started_at": self.started_at,
308
+ "ended_at": self.ended_at,
309
+ "duration_seconds": self.duration_seconds,
310
+ "raw": self.raw,
311
+ }
312
+
313
+
314
+ @dataclass
315
+ class TikTokVideoMetricsSnapshot:
316
+ snapshot_id: str
317
+ platform: str
318
+ account_id: str
319
+ device_id: str
320
+ fetched_at: str
321
+ source: str
322
+ status: str = "needs_human"
323
+ published_at: str = ""
324
+ published_at_raw: str = ""
325
+ video_order: int | None = None
326
+ video_views: int | None = None
327
+ likes: int | None = None
328
+ comments: int | None = None
329
+ shares: int | None = None
330
+ saves: int | None = None
331
+ total_play_time_seconds: int | None = None
332
+ total_play_time_raw: str = ""
333
+ average_watch_time_seconds: float | None = None
334
+ average_watch_time_raw: str = ""
335
+ completion_rate: float | None = None
336
+ completion_rate_raw: str = ""
337
+ new_followers: int | None = None
338
+ screenshot_path: str = ""
339
+ ui_dump_path: str = ""
340
+ artifact_dir: str = ""
341
+ started_at: str = ""
342
+ ended_at: str = ""
343
+ duration_seconds: int | None = None
344
+ raw: dict[str, Any] = field(default_factory=dict)
345
+
346
+ @classmethod
347
+ def from_dict(cls, data: dict[str, Any]) -> "TikTokVideoMetricsSnapshot":
348
+ raw = dict(data.get("raw") or {})
349
+ top_counters = dict(raw.get("top_counters") or {})
350
+ return cls(
351
+ snapshot_id=str(data["snapshot_id"]),
352
+ platform=str(data.get("platform", "tiktok")).lower(),
353
+ account_id=str(data["account_id"]),
354
+ device_id=str(data["device_id"]),
355
+ fetched_at=str(data["fetched_at"]),
356
+ source=str(data.get("source", "unknown")),
357
+ status=str(data.get("status", "needs_human")),
358
+ published_at=str(data.get("published_at", "")),
359
+ published_at_raw=str(data.get("published_at_raw", "")),
360
+ video_order=_optional_int(data.get("video_order")),
361
+ video_views=_optional_int(data.get("video_views", top_counters.get("views"))),
362
+ likes=_optional_int(data.get("likes", top_counters.get("likes"))),
363
+ comments=_optional_int(data.get("comments", top_counters.get("comments"))),
364
+ shares=_optional_int(data.get("shares", top_counters.get("shares"))),
365
+ saves=_optional_int(data.get("saves", top_counters.get("saves"))),
366
+ total_play_time_seconds=_optional_int(data.get("total_play_time_seconds")),
367
+ total_play_time_raw=str(data.get("total_play_time_raw", "")),
368
+ average_watch_time_seconds=_optional_float(data.get("average_watch_time_seconds")),
369
+ average_watch_time_raw=str(data.get("average_watch_time_raw", "")),
370
+ completion_rate=_optional_float(data.get("completion_rate")),
371
+ completion_rate_raw=str(data.get("completion_rate_raw", "")),
372
+ new_followers=_optional_int(data.get("new_followers")),
373
+ screenshot_path=str(data.get("screenshot_path", "")),
374
+ ui_dump_path=str(data.get("ui_dump_path", "")),
375
+ artifact_dir=str(data.get("artifact_dir", "")),
376
+ started_at=str(data.get("started_at", "")),
377
+ ended_at=str(data.get("ended_at", "")),
378
+ duration_seconds=_optional_int(data.get("duration_seconds")),
379
+ raw=raw,
380
+ )
381
+
382
+ def to_dict(self) -> dict[str, Any]:
383
+ return {
384
+ "snapshot_id": self.snapshot_id,
385
+ "platform": self.platform,
386
+ "account_id": self.account_id,
387
+ "device_id": self.device_id,
388
+ "fetched_at": self.fetched_at,
389
+ "source": self.source,
390
+ "status": self.status,
391
+ "published_at": self.published_at,
392
+ "published_at_raw": self.published_at_raw,
393
+ "video_order": self.video_order,
394
+ "video_views": self.video_views,
395
+ "likes": self.likes,
396
+ "comments": self.comments,
397
+ "shares": self.shares,
398
+ "saves": self.saves,
399
+ "total_play_time_seconds": self.total_play_time_seconds,
400
+ "total_play_time_raw": self.total_play_time_raw,
401
+ "average_watch_time_seconds": self.average_watch_time_seconds,
402
+ "average_watch_time_raw": self.average_watch_time_raw,
403
+ "completion_rate": self.completion_rate,
404
+ "completion_rate_raw": self.completion_rate_raw,
405
+ "new_followers": self.new_followers,
406
+ "screenshot_path": self.screenshot_path,
407
+ "ui_dump_path": self.ui_dump_path,
408
+ "artifact_dir": self.artifact_dir,
409
+ "started_at": self.started_at,
410
+ "ended_at": self.ended_at,
411
+ "duration_seconds": self.duration_seconds,
412
+ "raw": self.raw,
413
+ }
414
+
415
+ def has_key_metrics(self) -> bool:
416
+ return bool(self.published_at) and all(
417
+ value is not None
418
+ for value in (
419
+ self.video_views,
420
+ self.likes,
421
+ self.comments,
422
+ self.shares,
423
+ self.saves,
424
+ self.average_watch_time_seconds,
425
+ self.completion_rate,
426
+ )
427
+ )
428
+
429
+
430
+ def _optional_int(value: Any) -> int | None:
431
+ if value in (None, ""):
432
+ return None
433
+ return int(value)
434
+
435
+
436
+ def _optional_float(value: Any) -> float | None:
437
+ if value in (None, ""):
438
+ return None
439
+ return float(value)
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ DEFAULT_PUBLISH_POLICY_PATH = "configs/publish_policy.json"
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class XiaohongshuPublishPolicyDecision:
14
+ approval_bridge_applied: bool
15
+ approval_state: str
16
+ source_lifecycle_state: str
17
+ commercial_disclosure_decision: str
18
+ auto_confirm_no_disclosure_needed: bool
19
+ blockers: list[str] = field(default_factory=list)
20
+
21
+
22
+ def load_publish_policy(path: str | Path = DEFAULT_PUBLISH_POLICY_PATH) -> dict[str, Any]:
23
+ policy_path = Path(path).expanduser()
24
+ if not policy_path.exists():
25
+ return {}
26
+ with policy_path.open(encoding="utf-8") as handle:
27
+ loaded = json.load(handle)
28
+ if not isinstance(loaded, dict):
29
+ raise ValueError(f"publish policy must be a JSON object: {policy_path}")
30
+ return loaded
31
+
32
+
33
+ def explicit_live_publish_auto_confirm(policy: dict[str, Any] | None = None) -> bool:
34
+ global_policy = _mapping((policy or {}).get("global"))
35
+ return bool(global_policy.get("explicit_live_publish_auto_confirm", False))
36
+
37
+
38
+ def should_auto_confirm_instagram_reels_public(policy: dict[str, Any] | None = None) -> bool:
39
+ if not explicit_live_publish_auto_confirm(policy):
40
+ return False
41
+ instagram_policy = _mapping((policy or {}).get("instagram"))
42
+ return bool(instagram_policy.get("auto_confirm_reels_public", True))
43
+
44
+
45
+ def should_auto_confirm_xiaohongshu_no_disclosure(policy: dict[str, Any] | None = None) -> bool:
46
+ if not explicit_live_publish_auto_confirm(policy):
47
+ return False
48
+ xhs_policy = _mapping((policy or {}).get("xiaohongshu"))
49
+ disclosure_policy = _mapping(xhs_policy.get("commercial_disclosure"))
50
+ return bool(
51
+ disclosure_policy.get(
52
+ "auto_confirm_no_disclosure_needed",
53
+ xhs_policy.get("auto_confirm_no_disclosure_needed", False),
54
+ )
55
+ )
56
+
57
+
58
+ def should_trust_xiaohongshu_explicit_publish_request(policy: dict[str, Any] | None = None) -> bool:
59
+ if not explicit_live_publish_auto_confirm(policy):
60
+ return False
61
+ xhs_policy = _mapping((policy or {}).get("xiaohongshu"))
62
+ disclosure_policy = _mapping(xhs_policy.get("commercial_disclosure"))
63
+ return bool(disclosure_policy.get("trust_explicit_publish_request", True))
64
+
65
+
66
+ def xiaohongshu_package_disclosure_is_strict(policy: dict[str, Any] | None = None) -> bool:
67
+ xhs_policy = _mapping((policy or {}).get("xiaohongshu"))
68
+ disclosure_policy = _mapping(xhs_policy.get("commercial_disclosure"))
69
+ return bool(disclosure_policy.get("strict_package_disclosure_decision", False))
70
+
71
+
72
+ def decide_xiaohongshu_publish_package_policy(
73
+ package: dict[str, Any],
74
+ policy: dict[str, Any] | None = None,
75
+ ) -> XiaohongshuPublishPolicyDecision:
76
+ xhs_policy = dict((policy or {}).get("xiaohongshu") or {})
77
+ approval_policy = dict(xhs_policy.get("approval_bridge") or {})
78
+ disclosure_policy = dict(xhs_policy.get("commercial_disclosure") or {})
79
+ approval = dict(package.get("approval") or {})
80
+ disclosure = dict(package.get("disclosure") or {})
81
+
82
+ approval_state = _normalize(
83
+ approval.get("content_approval_state")
84
+ or approval.get("approval_state")
85
+ or approval.get("state")
86
+ or approval.get("lifecycle_state")
87
+ )
88
+ source_lifecycle_state = _normalize(
89
+ approval.get("source_lifecycle_state")
90
+ or approval.get("lifecycle_state")
91
+ or approval.get("source_approval_state")
92
+ )
93
+ explicit_publish_states = _normalized_set(
94
+ approval_policy.get("explicit_publish_states"),
95
+ default={
96
+ "approved_for_publish",
97
+ "approved_to_publish",
98
+ "approved_to_publish_by_request",
99
+ "publish_requested_by_member_comment",
100
+ "publish_requested_for_series_by_member_comment",
101
+ "publish_requested_for_single_post_by_member_comment",
102
+ },
103
+ )
104
+ approval_bridge_applied = bool(approval_policy.get("enabled", True)) and approval_state in explicit_publish_states
105
+
106
+ disclosure_decision = _normalize(
107
+ approval.get("commercial_disclosure_decision")
108
+ or approval.get("content_disclosure_decision")
109
+ or disclosure.get("commercial_disclosure_decision")
110
+ or disclosure.get("decision")
111
+ )
112
+ no_disclosure_decisions = _normalized_set(
113
+ disclosure_policy.get("no_disclosure_decisions"),
114
+ default={"no_disclosure_needed", "not_required", "not_needed", "none"},
115
+ )
116
+ requires_disclosure_decisions = _normalized_set(
117
+ disclosure_policy.get("requires_disclosure_decisions"),
118
+ default={"disclosure_required", "commercial_disclosure_required", "content_declaration_required", "required"},
119
+ )
120
+ default_decision = _normalize(disclosure_policy.get("default_decision") or "")
121
+ auto_confirm_enabled = bool(disclosure_policy.get("auto_confirm_no_disclosure_needed", False))
122
+ unresolved_follows_default = bool(disclosure_policy.get("unresolved_decisions_follow_default", False))
123
+ trust_explicit_publish = should_trust_xiaohongshu_explicit_publish_request(policy)
124
+
125
+ blockers: list[str] = []
126
+ if disclosure_decision in requires_disclosure_decisions:
127
+ if auto_confirm_enabled and trust_explicit_publish:
128
+ auto_confirm = True
129
+ else:
130
+ blockers.append(f"xiaohongshu disclosure decision requires declaration: {disclosure_decision}")
131
+ auto_confirm = False
132
+ elif disclosure_decision in no_disclosure_decisions:
133
+ auto_confirm = auto_confirm_enabled
134
+ elif not disclosure_decision or disclosure_decision in {"unresolved", "unknown", "pending"}:
135
+ auto_confirm = auto_confirm_enabled and (
136
+ (unresolved_follows_default and default_decision in no_disclosure_decisions)
137
+ or trust_explicit_publish
138
+ )
139
+ if not auto_confirm:
140
+ blockers.append("xiaohongshu disclosure decision is unresolved")
141
+ else:
142
+ if auto_confirm_enabled and trust_explicit_publish:
143
+ auto_confirm = True
144
+ else:
145
+ auto_confirm = False
146
+ blockers.append(f"unknown xiaohongshu disclosure decision: {disclosure_decision}")
147
+
148
+ return XiaohongshuPublishPolicyDecision(
149
+ approval_bridge_applied=approval_bridge_applied,
150
+ approval_state=approval_state,
151
+ source_lifecycle_state=source_lifecycle_state,
152
+ commercial_disclosure_decision=disclosure_decision or default_decision,
153
+ auto_confirm_no_disclosure_needed=auto_confirm,
154
+ blockers=blockers,
155
+ )
156
+
157
+
158
+ def _normalize(value: Any) -> str:
159
+ return str(value or "").strip().lower().replace("-", "_")
160
+
161
+
162
+ def _normalized_set(value: Any, *, default: set[str]) -> set[str]:
163
+ if value is None:
164
+ return set(default)
165
+ if not isinstance(value, list):
166
+ return set(default)
167
+ return {_normalize(item) for item in value if _normalize(item)}
168
+
169
+
170
+ def _mapping(value: Any) -> dict[str, Any]:
171
+ if isinstance(value, dict):
172
+ return value
173
+ return {}
@@ -0,0 +1,24 @@
1
+ from .facebook_adb import FacebookAdbPublishResult, FacebookAdbPublisher
2
+ from .instagram_adb import InstagramAdbPublishResult, InstagramAdbPublisher
3
+ from .reddit_adb import RedditAdbPublishResult, RedditAdbPublisher
4
+ from .tiktok_adb import TikTokAdbPublishResult, TikTokAdbPublisher
5
+ from .tiktok_appium import TikTokAppiumPublishResult, TikTokAppiumPublisher
6
+ from .x_adb import XAdbPublishResult, XAdbPublisher
7
+ from .xiaohongshu_adb import XiaohongshuAdbPublishResult, XiaohongshuAdbPublisher
8
+
9
+ __all__ = [
10
+ "FacebookAdbPublishResult",
11
+ "FacebookAdbPublisher",
12
+ "InstagramAdbPublishResult",
13
+ "InstagramAdbPublisher",
14
+ "RedditAdbPublishResult",
15
+ "RedditAdbPublisher",
16
+ "TikTokAdbPublishResult",
17
+ "TikTokAdbPublisher",
18
+ "TikTokAppiumPublishResult",
19
+ "TikTokAppiumPublisher",
20
+ "XAdbPublishResult",
21
+ "XAdbPublisher",
22
+ "XiaohongshuAdbPublishResult",
23
+ "XiaohongshuAdbPublisher",
24
+ ]