browser-workspace 0.4.2 → 0.4.5

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 (107) hide show
  1. package/README.md +15 -1
  2. package/SKILL.md +22 -21
  3. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  4. package/package.json +1 -1
  5. package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
  6. package/platforms/chatgpt/tests/test_contract.py +7 -0
  7. package/pyproject.toml +6 -2
  8. package/session_daemon.py +91 -3
  9. package/src/browser_workspace.egg-info/PKG-INFO +2 -1
  10. package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
  11. package/src/browser_workspace.egg-info/entry_points.txt +2 -0
  12. package/src/browser_workspace.egg-info/requires.txt +1 -0
  13. package/src/browser_workspace.egg-info/top_level.txt +1 -1
  14. package/src/browser_workspace_app/__init__.py +1 -0
  15. package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
  16. package/src/browser_workspace_app/cli.py +236 -0
  17. package/src/browser_workspace_app/extension/manifest.json +26 -0
  18. package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
  19. package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
  20. package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
  21. package/src/browser_workspace_app/extension/workspace.html +8 -0
  22. package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
  23. package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
  24. package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
  25. package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
  26. package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
  27. package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
  28. package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
  29. package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
  30. package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
  31. package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
  32. package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
  33. package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
  34. package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
  35. package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
  36. package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
  37. package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
  38. package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
  39. package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
  40. package/src/browser_workspace_app/managed_chrome.py +244 -0
  41. package/src/browser_workspace_app/node/admin.mjs +44 -0
  42. package/src/browser_workspace_app/package.json +36 -0
  43. package/src/browser_workspace_app/platform_runner.py +204 -0
  44. package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
  45. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  46. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  47. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
  48. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
  49. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
  50. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
  51. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
  52. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
  53. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
  54. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
  55. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
  56. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
  57. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
  58. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  59. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
  60. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
  61. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
  62. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
  63. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
  64. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
  65. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
  66. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
  67. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
  68. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
  69. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  70. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
  71. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
  72. package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
  73. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
  74. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
  75. package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
  76. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
  77. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
  78. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
  79. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
  80. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
  81. package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
  82. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
  83. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
  84. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
  85. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
  86. package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
  87. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
  88. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
  89. package/src/browser_workspace_app/pyproject.toml +33 -0
  90. package/src/browser_workspace_app/session_client.py +107 -0
  91. package/src/browser_workspace_app/session_daemon.py +431 -0
  92. package/src/browser_harness/__init__.py +0 -2
  93. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  94. package/src/browser_harness/_ipc.py +0 -201
  95. package/src/browser_harness/admin.py +0 -1588
  96. package/src/browser_harness/auth.py +0 -546
  97. package/src/browser_harness/daemon.py +0 -894
  98. package/src/browser_harness/helpers.py +0 -668
  99. package/src/browser_harness/macos.py +0 -131
  100. package/src/browser_harness/mcp_cli.py +0 -16
  101. package/src/browser_harness/paths.py +0 -49
  102. package/src/browser_harness/recorder.py +0 -343
  103. package/src/browser_harness/run.py +0 -413
  104. package/src/browser_harness/telemetry.py +0 -308
  105. package/src/browser_harness/video-template.html +0 -970
  106. package/src/browser_harness/video.py +0 -749
  107. package/src/browser_harness/video_render.py +0 -524
@@ -0,0 +1,210 @@
1
+ """Microsoft Teams meeting browser mechanics.
2
+
3
+ Keep the instant-meeting navigation in one browser action so target identity remains stable.
4
+ """
5
+
6
+ import json
7
+ import time
8
+
9
+ CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
10
+ ACTION = CFG.get("action", "status")
11
+ TEAMS_URL = "https://teams.cloud.microsoft/"
12
+ ALLOWED_ACTIONS = {"start-instant-meeting", "status", "leave"}
13
+
14
+
15
+ def fail(message, **extra):
16
+ print(json.dumps({"action": ACTION, "ok": False, "error": message, **extra}, ensure_ascii=False))
17
+ raise SystemExit(2)
18
+
19
+
20
+ def evidence(**extra):
21
+ record = {"action": ACTION, "ok": True, "url": page_info().get("url"), **extra}
22
+ print(json.dumps(record, ensure_ascii=False))
23
+ return record
24
+
25
+
26
+ def wait_until(predicate, timeout=30.0, interval=0.25, message="condition not reached"):
27
+ deadline = time.monotonic() + timeout
28
+ while time.monotonic() < deadline:
29
+ value = predicate()
30
+ if value:
31
+ return value
32
+ time.sleep(interval)
33
+ fail(message, title=page_info().get("title"), url=page_info().get("url"))
34
+
35
+
36
+ def teams_status():
37
+ return js("""(() => ({
38
+ title: document.title,
39
+ prejoin: !!document.querySelector('#prejoin-join-button'),
40
+ duration: document.querySelector('#call-duration-custom,[data-tid="call-duration"]')?.textContent || null,
41
+ hangup: !!document.querySelector('#hangup-button'),
42
+ profile: document.querySelector('#idna-me-control-avatar-trigger')?.getAttribute('aria-label') || ''
43
+ }))()""")
44
+
45
+
46
+ def wait_app_bar():
47
+ return wait_until(
48
+ lambda: js("""!![...document.querySelectorAll('button')].find(
49
+ b => (b.getAttribute('aria-label') || '').startsWith('Calendar'))"""),
50
+ timeout=40,
51
+ message="Teams app bar did not expose Calendar",
52
+ )
53
+
54
+
55
+ def click_calendar():
56
+ ok = js("""(() => {
57
+ const button = [...document.querySelectorAll('button')].find(
58
+ b => (b.getAttribute('aria-label') || '').startsWith('Calendar'));
59
+ if (!button) return false;
60
+ button.click();
61
+ return true;
62
+ })()""")
63
+ if not ok:
64
+ fail("Calendar button was not clickable")
65
+
66
+
67
+ def calendar_target(parent_target):
68
+ def resolve():
69
+ targets = cdp("Target.getTargets").get("targetInfos", [])
70
+ matches = [
71
+ t for t in targets
72
+ if t.get("type") == "iframe"
73
+ and "outlook.office.com/hosted/calendar" in t.get("url", "")
74
+ and t.get("parentId") == parent_target
75
+ ]
76
+ return matches[0].get("targetId") if matches else None
77
+
78
+ return wait_until(resolve, timeout=30, message="Teams Calendar iframe did not become ready")
79
+
80
+
81
+ def click_meet_now(target_id):
82
+ wait_until(
83
+ lambda: js("""!![...document.querySelectorAll('button')].find(
84
+ b => (b.getAttribute('aria-label') || '') === 'Start an instant Teams meeting.')""", target_id=target_id),
85
+ timeout=30,
86
+ message="Meet now button did not appear",
87
+ )
88
+ ok = js("""(() => {
89
+ const button = [...document.querySelectorAll('button')].find(
90
+ b => (b.getAttribute('aria-label') || '') === 'Start an instant Teams meeting.');
91
+ if (!button) return false;
92
+ button.click();
93
+ return true;
94
+ })()""", target_id=target_id)
95
+ if not ok:
96
+ fail("Meet now button was not clickable")
97
+
98
+
99
+
100
+
101
+ def start_meeting_dialog(target_id):
102
+ """Handle Teams' intermediate 'Start a meeting now' dialog when present."""
103
+ def find_button():
104
+ return js("""(() => {
105
+ const buttons = [...document.querySelectorAll('button')];
106
+ const b = buttons.find(x => (x.innerText || '').includes('Start meeting'));
107
+ return !!b;
108
+ })()""", target_id=target_id)
109
+
110
+ try:
111
+ wait_until(find_button, timeout=8, message="Start meeting dialog did not appear")
112
+ except SystemExit:
113
+ return False
114
+
115
+ ok = js("""(() => {
116
+ const b = [...document.querySelectorAll('button')].find(
117
+ x => (x.innerText || '').includes('Start meeting'));
118
+ if (!b) return false;
119
+ b.click();
120
+ return true;
121
+ })()""", target_id=target_id)
122
+ if not ok:
123
+ fail("Start meeting button was not clickable")
124
+ return True
125
+
126
+ def wait_prejoin():
127
+ wait_until(
128
+ lambda: js("!!document.querySelector('#prejoin-join-button')"),
129
+ timeout=40,
130
+ message="Meet now did not reach Teams pre-join",
131
+ )
132
+ return teams_status()
133
+
134
+
135
+ def join_now():
136
+ ok = js("""(() => {
137
+ const button = document.querySelector('#prejoin-join-button');
138
+ if (!button) return false;
139
+ button.click();
140
+ return true;
141
+ })()""")
142
+ if not ok:
143
+ fail("Join now button was not clickable")
144
+
145
+
146
+ def wait_in_call():
147
+ wait_until(
148
+ lambda: (lambda s: s.get("duration") or s.get("hangup"))(teams_status()),
149
+ timeout=40,
150
+ message="Teams did not reach in-call state after Join now",
151
+ )
152
+ return teams_status()
153
+
154
+
155
+ def start_instant_meeting():
156
+ target = new_tab(TEAMS_URL)
157
+ wait_app_bar()
158
+ initial = teams_status()
159
+ click_calendar()
160
+ cal = calendar_target(target)
161
+ click_meet_now(cal)
162
+ start_meeting_dialog(cal)
163
+ try:
164
+ prejoin = wait_prejoin()
165
+ except SystemExit:
166
+ current = teams_status()
167
+ if "in a call" in (current.get("profile") or "").lower():
168
+ fail(
169
+ "account is already in a call; instant Meet now could not reach pre-join",
170
+ profile=current.get("profile"),
171
+ )
172
+ raise
173
+ join_now()
174
+ incall = wait_in_call()
175
+ return evidence(target_id=target, initial=initial, prejoin=prejoin, in_call=incall)
176
+
177
+
178
+ def leave():
179
+ state = teams_status()
180
+ if not state.get("duration") and not state.get("hangup"):
181
+ return evidence(left=False, state=state)
182
+
183
+ ok = js("""(() => {
184
+ const button = document.querySelector('#hangup-button') ||
185
+ [...document.querySelectorAll('button')].find(
186
+ b => /leave|hang up/i.test(b.getAttribute('aria-label') || b.innerText || ''));
187
+ if (!button) return false;
188
+ button.click();
189
+ return true;
190
+ })()""")
191
+ if not ok:
192
+ fail("in-call state observed but leave/hangup control was not clickable", state=state)
193
+
194
+ wait_until(
195
+ lambda: not (teams_status().get("duration") or teams_status().get("hangup")),
196
+ timeout=20,
197
+ message="meeting did not exit after leave",
198
+ )
199
+ return evidence(left=True, before=state, after=teams_status())
200
+
201
+
202
+ if ACTION not in ALLOWED_ACTIONS:
203
+ fail(f"unsupported action: {ACTION}")
204
+
205
+ if ACTION == "start-instant-meeting":
206
+ start_instant_meeting()
207
+ elif ACTION == "leave":
208
+ leave()
209
+ else:
210
+ evidence(state=teams_status())
@@ -0,0 +1,21 @@
1
+ # Instant meeting
2
+
3
+ Status: `verified` on 2026-10-01.
4
+
5
+ Preconditions:
6
+ - Microsoft Teams is already authenticated in the browser context.
7
+ - The action may create and join a real meeting.
8
+
9
+ Canonical flow:
10
+ 1. Open `https://teams.cloud.microsoft/`.
11
+ 2. Wait for the left application rail.
12
+ 3. Click **Calendar**.
13
+ 4. Resolve the hosted calendar target/iframe when required.
14
+ 5. Click **Meet now**.
15
+ 6. If the intermediate dialog appears, click **Start meeting**.
16
+ 7. Wait for pre-join.
17
+ 8. Click **Join now**.
18
+ 9. Verify in-call state using call duration or hangup controls.
19
+ 10. When cleanup is requested, click leave/hangup and verify the call controls disappear.
20
+
21
+ Do not identify browser ownership by URL. Do not touch unrelated tabs when the account is already in another call.
@@ -0,0 +1,25 @@
1
+ platform: microsoft-teams
2
+ status: verified
3
+ last_verified: 2026-10-01
4
+ actions:
5
+ meeting: "actions/_meeting.py"
6
+ verification:
7
+ status: verified
8
+ last_verified: 2026-10-01
9
+ evidence: "Live E2E verified Calendar -> Meet now -> Start meeting -> pre-join -> Join now -> in-call and meeting exit on teams.cloud.microsoft. Generic platform-runner status execution and automatic tab cleanup were also live-verified on 2026-10-01."
10
+ url_patterns:
11
+ - "https://teams.cloud.microsoft/*"
12
+ - "https://outlook.office.com/hosted/calendar/*"
13
+ authentication:
14
+ required: true
15
+ notes: "Use an already authenticated Microsoft Teams browser context; never store credentials or session state."
16
+ side_effects:
17
+ - "start-instant-meeting can create and join a real Teams meeting"
18
+ - "leave exits the current meeting"
19
+ provenance:
20
+ source: "Live Browser Workspace exploration for MeetMate E2E"
21
+ migrated_code: false
22
+ flows:
23
+ instant-meeting: {status: verified, evidence: "Live verified through in-call state on 2026-10-01."}
24
+ meeting-status: {status: verified, evidence: "Live call state observed via call duration and hangup controls."}
25
+ leave: {status: verified, evidence: "Live E2E exited the test meeting cleanly."}
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+
3
+ ROOT = Path(__file__).resolve().parents[1]
4
+ ACTION = (ROOT / "actions" / "_meeting.py").read_text()
5
+ README = (ROOT / "README.md").read_text()
6
+ MANIFEST = (ROOT / "manifest.yaml").read_text()
7
+
8
+
9
+ def test_canonical_instant_meeting_path():
10
+ assert "Calendar -> Meet now -> Start meeting (when shown) -> pre-join -> Join now" in README
11
+ assert "Start an instant Teams meeting." in ACTION
12
+ assert "#prejoin-join-button" in ACTION
13
+ assert "Start meeting" in ACTION
14
+ assert "#call-duration-custom" in ACTION
15
+
16
+
17
+ def test_action_uses_browser_helper_path():
18
+ assert "new_tab(TEAMS_URL)" in ACTION
19
+ assert "websocket" not in ACTION.lower()
20
+
21
+
22
+ def test_manifest_declares_side_effects():
23
+ assert "instant-meeting" in MANIFEST
24
+ assert "real Teams meeting" in MANIFEST
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: tiktok-platform
3
+ description: "TikTok Browser Workspace upload mechanics."
4
+ ---
5
+
6
+ # TikTok
7
+
8
+ Use the parent Browser Workspace skill contract first. The
9
+ `actions/` directory contains the canonical migrated upload mechanics;
10
+ `skills/post` retains payload and publishing orchestration.
@@ -0,0 +1,180 @@
1
+ import time, json
2
+
3
+ CFG = json.load(open("__CFG_PATH__"))
4
+
5
+ total_steps = 7
6
+ step = [0]
7
+ def next_step(label):
8
+ step[0] += 1
9
+ print(f"[{step[0]}/{total_steps}] {label}")
10
+
11
+ next_step("Opening TikTok upload page...")
12
+ new_tab("https://www.tiktok.com/creator#/upload?scene=creator_center")
13
+ wait_for_load()
14
+ time.sleep(5)
15
+
16
+ url = page_info()["url"]
17
+ if "login" in url.lower() or "accounts" in url.lower():
18
+ print("ERROR: Not logged in. Log in at tiktok.com first.")
19
+ capture_screenshot()
20
+ raise SystemExit(1)
21
+
22
+ # Check for uid null error (not logged in)
23
+ # The page loads but shows login modal if not authenticated
24
+ login_check = js('document.querySelector("[class*=login-modal],[class*=LoginModal]") ? "login" : "ok"')
25
+ if login_check == "login":
26
+ print("ERROR: Login modal detected. Log in at tiktok.com first.")
27
+ capture_screenshot()
28
+ raise SystemExit(1)
29
+
30
+ # Wait for upload area to appear
31
+ for i in range(15):
32
+ found = js('document.querySelector("input[type=file],input[accept*=video],[class*=upload-btn],[class*=upload-card]") ? "found" : "wait"')
33
+ if found == "found":
34
+ break
35
+ time.sleep(2)
36
+ else:
37
+ print("ERROR: Upload area not found. May not be logged in.")
38
+ capture_screenshot()
39
+ raise SystemExit(1)
40
+
41
+ print(" -> Logged in, page loaded.")
42
+
43
+ next_step("Uploading video: " + CFG["video"])
44
+ # TikTok has a file input, may be hidden — try multiple selectors
45
+ try:
46
+ upload_file("input[type=file]", CFG["video"])
47
+ except Exception:
48
+ # Fallback: find input via DOM search
49
+ doc = cdp("DOM.getDocument", depth=-1, pierce=True)
50
+ sr = cdp("DOM.performSearch", query='input[type="file"]')
51
+ if sr.get("resultCount", 0) > 0:
52
+ nodes = cdp("DOM.getSearchResults", searchId=sr["searchId"], fromIndex=0, toIndex=1)
53
+ nid = nodes["nodeIds"][0]
54
+ cdp("DOM.setFileInputFiles", files=[CFG["video"]], nodeId=nid)
55
+ cdp("DOM.discardSearchResults", searchId=sr["searchId"])
56
+ else:
57
+ print("ERROR: Cannot find file input.")
58
+ capture_screenshot()
59
+ raise SystemExit(1)
60
+
61
+ next_step("Waiting for video upload + processing...")
62
+ for attempt in range(120):
63
+ time.sleep(5)
64
+ # Check for upload completion indicators
65
+ done = js('''
66
+ (function() {
67
+ var indicators = document.querySelectorAll("[class*=progress],[class*=percent],[class*=upload-progress]");
68
+ for (var i = 0; i < indicators.length; i++) {
69
+ var t = indicators[i].textContent;
70
+ if (t && (t.indexOf("100") >= 0 || t.indexOf("Complete") >= 0 || t.indexOf("Done") >= 0)) return "done";
71
+ }
72
+ // Check if caption editor appeared (means upload accepted)
73
+ if (document.querySelector("[class*=caption],[class*=editor] [contenteditable],[data-text=true]")) return "done";
74
+ return "uploading";
75
+ })()
76
+ ''')
77
+ if done == "done":
78
+ print(" -> Upload done!")
79
+ break
80
+ if attempt % 6 == 5:
81
+ pct = js('var el=document.querySelector("[class*=progress-text],[class*=percent]"); el ? el.textContent.trim() : "..."')
82
+ print(f" -> Uploading... {pct} ({(attempt+1)*5}s)")
83
+ else:
84
+ print("WARNING: Upload may still be processing.")
85
+ capture_screenshot()
86
+ time.sleep(3)
87
+
88
+ next_step("Setting caption...")
89
+ caption_escaped = json.dumps(CFG["caption"])
90
+ # TikTok uses a contenteditable div or DraftJS editor for caption
91
+ js('''
92
+ (function() {
93
+ var ed = document.querySelector("[class*=caption-editor] [contenteditable],[class*=notranslate][contenteditable],[data-text=true],.DraftEditor-root [contenteditable],[class*=editor-container] [contenteditable]");
94
+ if (!ed) ed = document.querySelector("[contenteditable=true]");
95
+ if (ed) {
96
+ ed.focus();
97
+ ed.textContent = ''' + caption_escaped + ''';
98
+ ed.dispatchEvent(new Event("input", {bubbles: true}));
99
+ }
100
+ })()
101
+ ''')
102
+ time.sleep(1)
103
+
104
+ # Set visibility if not public
105
+ if CFG["visibility"] != "public":
106
+ next_step("Setting visibility to " + CFG["visibility"] + "...")
107
+ # Click "Who can watch this video" dropdown
108
+ js('''
109
+ (function() {
110
+ var selects = document.querySelectorAll("[class*=select],[class*=dropdown],[class*=visibility]");
111
+ for (var i = 0; i < selects.length; i++) {
112
+ var t = selects[i].textContent;
113
+ if (t && (t.indexOf("Everyone") >= 0 || t.indexOf("Public") >= 0 || t.indexOf("Friends") >= 0)) {
114
+ selects[i].click();
115
+ break;
116
+ }
117
+ }
118
+ })()
119
+ ''')
120
+ time.sleep(1)
121
+ # Map visibility to TikTok's menu options
122
+ vis_map = {"friends": "Friends", "private": "Only me"}
123
+ vis_text = vis_map.get(CFG["visibility"], "Everyone")
124
+ js('var opts=document.querySelectorAll("[class*=option],[class*=menu-item],[role=option]");for(var i=0;i<opts.length;i++){if(opts[i].textContent.indexOf("' + vis_text + '")>=0){opts[i].click();break}};""')
125
+ time.sleep(1)
126
+ else:
127
+ next_step("Visibility: public (default)")
128
+
129
+ # Toggle social features if needed
130
+ toggles_changed = []
131
+ if not CFG.get("allow_comments", True):
132
+ toggles_changed.append("comments off")
133
+ if not CFG.get("allow_duets", True):
134
+ toggles_changed.append("duets off")
135
+ if not CFG.get("allow_stitch", True):
136
+ toggles_changed.append("stitch off")
137
+
138
+ if toggles_changed:
139
+ next_step("Adjusting toggles: " + ", ".join(toggles_changed))
140
+ # TikTok has toggle switches for comments, duets, stitch
141
+ js('''
142
+ (function() {
143
+ var toggles = document.querySelectorAll("[class*=toggle],[class*=switch]");
144
+ var labels = ["comment", "duet", "stitch"];
145
+ var states = [''' + json.dumps(CFG.get("allow_comments", True)) + ''', ''' + json.dumps(CFG.get("allow_duets", True)) + ''', ''' + json.dumps(CFG.get("allow_stitch", True)) + '''];
146
+ toggles.forEach(function(t) {
147
+ var text = (t.closest("[class*=form-item],[class*=setting]") || t.parentElement).textContent.toLowerCase();
148
+ for (var i = 0; i < labels.length; i++) {
149
+ if (text.indexOf(labels[i]) >= 0) {
150
+ var isOn = t.classList.contains("on") || t.getAttribute("aria-checked") === "true" || t.querySelector("[class*=active]");
151
+ if (isOn !== states[i]) t.click();
152
+ }
153
+ }
154
+ });
155
+ })()
156
+ ''')
157
+ time.sleep(1)
158
+ else:
159
+ next_step("Social features: defaults (all enabled)")
160
+
161
+ capture_screenshot()
162
+
163
+ next_step("Posting video...")
164
+ # Click the Post button
165
+ js('''
166
+ (function() {
167
+ var btns = document.querySelectorAll("button,[class*=btn-post],[class*=post-button]");
168
+ for (var i = 0; i < btns.length; i++) {
169
+ var t = btns[i].textContent.trim();
170
+ if (t === "Post" || t === "发布") {
171
+ btns[i].click();
172
+ return;
173
+ }
174
+ }
175
+ })()
176
+ ''')
177
+ time.sleep(5)
178
+
179
+ capture_screenshot()
180
+ print("Done! Video posted to TikTok.")
@@ -0,0 +1,99 @@
1
+ platform: tiktok
2
+ status: migrated_unverified
3
+ last_verified: null
4
+ actions:
5
+ post: actions/_post.py
6
+ verification:
7
+ status: migrated_unverified
8
+ last_verified: null
9
+ evidence: Existing upload mechanics migrated from the working post adapter. Current
10
+ Browser Workspace live verification pending.
11
+ url_patterns:
12
+ - https://www.tiktok.com/creator*
13
+ authentication:
14
+ required: true
15
+ notes: Use the caller's authenticated browser context; never store credentials.
16
+ provenance:
17
+ source: skills/post/scripts/platforms/tiktok
18
+ migrated_code: true
19
+ flows:
20
+ post:
21
+ status: migrated_unverified
22
+ evidence: Migrated action; current Browser Workspace live verification pending.
23
+ display_name: TikTok
24
+ aliases: []
25
+ media:
26
+ modes:
27
+ - video
28
+ video:
29
+ required: true
30
+ formats:
31
+ - mp4
32
+ max_size_gb: 10
33
+ max_duration_minutes: 10
34
+ fields:
35
+ caption:
36
+ cli: --caption
37
+ required: true
38
+ type: string
39
+ max_chars_including_tags: 4000
40
+ implemented: true
41
+ tags:
42
+ cli: --tags
43
+ required: false
44
+ type: string_list
45
+ max_count: 10
46
+ implemented: true
47
+ render: 'append as #tag'
48
+ cover:
49
+ cli: --cover
50
+ required: false
51
+ type: file
52
+ implemented: false
53
+ status: cli_only
54
+ note: The migrated CLI validates the file but the browser body does not currently
55
+ apply it.
56
+ visibility:
57
+ cli: --visibility
58
+ required: false
59
+ type: enum
60
+ values:
61
+ - public
62
+ - friends
63
+ - private
64
+ default: public
65
+ implemented: true
66
+ allow_comments:
67
+ cli: --no-comments
68
+ required: false
69
+ type: boolean
70
+ default: true
71
+ implemented: true
72
+ allow_duets:
73
+ cli: --no-duets
74
+ required: false
75
+ type: boolean
76
+ default: true
77
+ implemented: true
78
+ allow_stitch:
79
+ cli: --no-stitch
80
+ required: false
81
+ type: boolean
82
+ default: true
83
+ implemented: true
84
+ capabilities:
85
+ draft: false
86
+ publish: true
87
+ visibility: true
88
+ custom_thumbnail: false
89
+ social_interaction_controls: true
90
+ post_status: false
91
+ read_comments: false
92
+ write_comment: false
93
+ reply_comments: false
94
+ content_mapping:
95
+ headline: null
96
+ main_text: caption
97
+ discovery_terms: tags
98
+ notes:
99
+ - The current adapter performs the final Post action; it has no draft mode.
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: wechat-channels-platform
3
+ description: "WeChat Channels Browser Workspace posting and manager assets."
4
+ ---
5
+
6
+ # WeChat Channels
7
+
8
+ Use the parent Browser Workspace skill contract first. The
9
+ `actions/` directory is the canonical home for the migrated posting,
10
+ manager, and shadow-DOM helper mechanics used by `skills/post`.