browser-workspace 0.4.1 → 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.
- package/README.md +16 -2
- package/SKILL.md +60 -37
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/bin/browser-workspace +174 -13
- package/package.json +1 -1
- package/platform_runner.py +23 -6
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/platforms/wechat-channels/actions/_common.py +1 -1
- package/platforms/wechat-channels/actions/_post.py +37 -38
- package/platforms/wechat-channels/manifest.yaml +19 -13
- package/platforms/xhs/actions/_post.py +109 -20
- package/platforms/xhs/manifest.yaml +14 -5
- package/platforms/xhs/tests/test_contract.py +17 -0
- package/platforms/youtube/actions/_post.py +7 -4
- package/pyproject.toml +6 -2
- package/session_client.py +21 -11
- package/session_daemon.py +115 -9
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- 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`.
|