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
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ browser-workspace session stop SESSION_ID
|
|
|
20
20
|
|
|
21
21
|
`--workspace` is hidden from normal CLI help. Normal sessions use `Harness`; product integrations may set an explicit workspace internally when they require isolation.
|
|
22
22
|
|
|
23
|
-
`session stop` closes all tabs created/owned by that session while leaving unrelated browser tabs untouched.
|
|
23
|
+
`session stop` closes all tabs created/owned by that session while leaving unrelated browser tabs untouched. Raw session CDP cannot create targets: `Target.createTarget` is rejected and callers must use `new_tab()` so workspace/session ownership is preserved.
|
|
24
24
|
|
|
25
25
|
## Included layers
|
|
26
26
|
|
|
27
27
|
```text
|
|
28
28
|
browser-workspace
|
|
29
|
-
├──
|
|
29
|
+
├── pyproject.toml pins the external browser-harness runtime dependency
|
|
30
30
|
├── agent-workspace/ Browser Workspace helper layer
|
|
31
31
|
├── extension/ Chrome workspace/tab manager
|
|
32
32
|
├── session_daemon.py persistent Python session service
|
|
@@ -136,3 +136,17 @@ Each platform manifest records explicit verification status and evidence. `verif
|
|
|
136
136
|
## npx skills installation
|
|
137
137
|
|
|
138
138
|
After `npx skills add ...`, invoke `~/.agents/skills/browser-workspace/bin/browser-workspace` directly. The first session start bootstraps the skill-owned Python virtual environment automatically; running `scripts/install.sh` is optional.
|
|
139
|
+
|
|
140
|
+
### Python distribution (PyPI)
|
|
141
|
+
|
|
142
|
+
Browser Workspace is also packaged as a Python CLI. Once its PyPI trusted publisher is configured and a `pypi-v*` release tag is published, invoke it with:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uvx browser-workspace status
|
|
146
|
+
uvx browser-workspace session list
|
|
147
|
+
uvx browser-workspace platform profile wechat-channels
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The package entry point is `browser_workspace_app.cli:main`, and Python dependencies are managed by `pyproject.toml`. This uses the same existing Chrome profile and Session Daemon socket as the previous npm CLI; it does **not** start a second browser or daemon when the existing one is healthy.
|
|
151
|
+
|
|
152
|
+
Before building, run `python tools/sync-pypi-package.py` to copy canonical Python and packaged platform assets to the wheel source. Run `uv build`, then validate with `uvx --from dist/browser_workspace-<version>-py3-none-any.whl browser-workspace status`. The PyPI publishing workflow uses GitHub OIDC trusted publishing; configure `lalalic/browser-workspace` with the `pypi` environment as the publisher on PyPI before the first tag release.
|
package/SKILL.md
CHANGED
|
@@ -9,31 +9,33 @@ Browser Workspace is the **only browser skill agents should load**. It includes
|
|
|
9
9
|
|
|
10
10
|
## Agent contract
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Install the Skill instructions through your preferred skill manager, but run the published Python CLI through **PyPI/uvx**. The Skill directory is documentation, not the executable source of truth:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
bw() { uvx browser-workspace "$@"; }
|
|
16
|
+
bw status
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
On first use, Browser Workspace
|
|
19
|
+
On first use, `uvx` installs Browser Workspace and Python dependencies from PyPI into its managed cache. No local source checkout, `.venv`, `.env`, or `npx` CLI is required. Use `uvx browser-workspace ...` consistently, including in NeoY/Agents Relay workers.
|
|
20
20
|
|
|
21
|
-
Use one
|
|
21
|
+
Use one session-owned execution path for orchestrated browser work:
|
|
22
22
|
|
|
23
23
|
```text
|
|
24
|
-
|
|
25
|
-
->
|
|
26
|
-
->
|
|
27
|
-
->
|
|
28
|
-
->
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
->
|
|
32
|
-
|
|
33
|
-
->
|
|
24
|
+
start one Browser Workspace session
|
|
25
|
+
-> retain SESSION_ID for the whole workflow
|
|
26
|
+
-> platform run <platform> <action> --session-id SESSION_ID
|
|
27
|
+
-> inspect each result
|
|
28
|
+
-> on recoverable failure, diagnose and self-heal in the SAME session
|
|
29
|
+
-> write every successful site-specific repair back to the platform code
|
|
30
|
+
-> continue until the external workflow reaches a terminal outcome
|
|
31
|
+
-> verify the terminal external state
|
|
32
|
+
finally
|
|
33
|
+
-> stop SESSION_ID
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
`platform run` therefore requires `--session-id` by default. If omitted, the CLI returns a structured error that recommends the `session start -> platform run --session-id -> self-heal -> session stop` workflow. `--auto-session` is an explicit one-shot escape hatch: it creates and cleans up a temporary session automatically, but **does not provide orchestrator self-heal** after failure.
|
|
37
|
+
|
|
38
|
+
There are no separate top-level `open`, `tabs`, `status`, or `screenshot` commands.
|
|
37
39
|
|
|
38
40
|
Every logical browser workflow must follow:
|
|
39
41
|
|
|
@@ -59,7 +61,7 @@ Rules:
|
|
|
59
61
|
Default workspace is `Harness`:
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
|
-
|
|
64
|
+
bw session start
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
Normal agents do not choose or manage workspaces. `session start` defaults to `Harness`; the `--workspace` implementation option is hidden from normal CLI help. Workspace lifecycle is not exposed through the agent CLI. Product runtimes that need persistent isolation discover the product-only Node admin helper with `browser-workspace status`, then call `ensureWorkspace` / `deleteWorkspace` through that helper.
|
|
@@ -67,7 +69,7 @@ Normal agents do not choose or manage workspaces. `session start` defaults to `H
|
|
|
67
69
|
Start directly at a URL when known:
|
|
68
70
|
|
|
69
71
|
```bash
|
|
70
|
-
|
|
72
|
+
bw session start --url https://example.com
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
The result is JSON containing `session_id`, `workspace`, `target_id`, the starting `url`, and `workspace_supported`.
|
|
@@ -79,7 +81,7 @@ Browser Workspace self-manages exactly one machine-wide persistent, visible Chro
|
|
|
79
81
|
`session exec` reads Python from stdin. Helpers are already imported; do not import another browser package.
|
|
80
82
|
|
|
81
83
|
```bash
|
|
82
|
-
|
|
84
|
+
bw session exec <session_id> <<'PY'
|
|
83
85
|
print(page_info())
|
|
84
86
|
print(snapshot())
|
|
85
87
|
PY
|
|
@@ -88,7 +90,7 @@ PY
|
|
|
88
90
|
Python state persists across later calls in the same session:
|
|
89
91
|
|
|
90
92
|
```bash
|
|
91
|
-
|
|
93
|
+
bw session exec <session_id> <<'PY'
|
|
92
94
|
meeting_name = "Demo"
|
|
93
95
|
print(current_tab())
|
|
94
96
|
PY
|
|
@@ -97,7 +99,7 @@ PY
|
|
|
97
99
|
Then later:
|
|
98
100
|
|
|
99
101
|
```bash
|
|
100
|
-
|
|
102
|
+
bw session exec <session_id> <<'PY'
|
|
101
103
|
print(meeting_name)
|
|
102
104
|
PY
|
|
103
105
|
```
|
|
@@ -107,19 +109,19 @@ A Python file may also be used with `--code-file`.
|
|
|
107
109
|
## Stop
|
|
108
110
|
|
|
109
111
|
```bash
|
|
110
|
-
|
|
112
|
+
bw session stop <session_id>
|
|
111
113
|
```
|
|
112
114
|
|
|
113
|
-
Stopping closes every tab owned or opened by that session—including tabs created with `new_tab()`, `bh.new_tab()`,
|
|
115
|
+
Stopping closes every tab owned or opened by that session—including tabs created with `new_tab()`, `bh.new_tab()`, and detected child/popup tabs—then destroys the session's Python namespace. Raw `cdp("Target.createTarget", ...)` is rejected; use `new_tab()` so Browser Workspace can own and group the tab. It does not close unrelated browser tabs. Do not reuse the ID afterward.
|
|
114
116
|
|
|
115
117
|
A safe shell structure is:
|
|
116
118
|
|
|
117
119
|
```bash
|
|
118
|
-
SESSION_JSON=$(
|
|
120
|
+
SESSION_JSON=$(bw session start)
|
|
119
121
|
SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
|
|
120
|
-
trap '
|
|
122
|
+
trap 'bw session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
bw session exec "$SESSION_ID" <<'PY'
|
|
123
125
|
print(snapshot())
|
|
124
126
|
PY
|
|
125
127
|
```
|
|
@@ -216,21 +218,41 @@ platforms/<name>/
|
|
|
216
218
|
|
|
217
219
|
Platform rules:
|
|
218
220
|
|
|
219
|
-
- Platform actions may use browser helpers such as `new_tab()`, `current_tab()`, `snapshot()`, `js()`, `cdp()`, uploads, downloads, and screenshots.
|
|
221
|
+
- Platform actions may use semantic browser helpers such as `new_tab()`, `current_tab()`, `snapshot()`, `click(selector)`, `fill_input(selector, text)`, `js()`, `cdp()`, uploads, downloads, and screenshots. `cdp()` must not create targets; `Target.createTarget` is blocked and tab creation must use `new_tab()`.
|
|
222
|
+
- Platform interaction code must be **semantic and element-based**. It must not use screen coordinates, viewport coordinates, `x/y` click targets, `click_at_xy()`, or coordinate calculations to interact with the page. Browser size, zoom, display resolution, and viewport geometry must not affect correctness.
|
|
220
223
|
- Platform actions must **not** create/stop Browser Workspace sessions, choose a workspace, call workspace-management APIs, invoke the Browser Workspace CLI, or import session/workspace infrastructure.
|
|
224
|
+
- The orchestrator owns the session lifecycle for normal platform execution: start once, retain the same session across execution and recovery, and stop only after terminal success, terminal failure, or an explicit human blocker.
|
|
225
|
+
- On any recoverable failure, the orchestrator must inspect the live page in the same session, diagnose the failure, repair/recover, and resume from the safest point rather than restarting the whole workflow.
|
|
226
|
+
- **Self-heal writeback is mandatory:** if recovery discovers a site-specific selector, flow, wait condition, validation rule, or other adapter fix that makes the action work, write that repair back to `platforms/<name>/actions/` (and update platform docs/verification metadata when applicable) before declaring the workflow complete. A repair that exists only in an ad-hoc session snippet is not complete.
|
|
227
|
+
- After a possible external side effect (publish, send, payment, delete, submit, etc.), recovery must be verification-first. Never repeat the side effect until read-only evidence proves the previous attempt did not take effect.
|
|
221
228
|
- Platform `README.md` and flow docs should describe site behavior, preconditions, side effects, and verification evidence—not Browser Workspace lifecycle mechanics.
|
|
222
|
-
- Platform actions must remain usable in either a generic runner-owned session or a product-owned browser context.
|
|
223
|
-
- The generic runner owns the normal lifecycle: start a default Harness session, inject browser helpers/config, execute the action, then always stop the session and close its owned tabs.
|
|
224
|
-
- Products that require a dedicated or persistent browser context (for example Family Tutor or Agents Relay) may execute the same platform action inside product-owned infrastructure. The platform code itself stays unchanged.
|
|
225
229
|
- Do not rediscover a known site's mechanics when a verified platform flow/action already exists.
|
|
226
230
|
|
|
227
|
-
|
|
231
|
+
Normal orchestrator workflow:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
SESSION_JSON=$(bw session start --url https://example.com)
|
|
235
|
+
SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
|
|
236
|
+
|
|
237
|
+
bw platform run microsoft-teams meeting \
|
|
238
|
+
--session-id "$SESSION_ID" \
|
|
239
|
+
--config /path/to/config.json
|
|
240
|
+
|
|
241
|
+
# If it fails: inspect/self-heal using the same SESSION_ID, write the repair
|
|
242
|
+
# back to the platform implementation, then resume safely.
|
|
243
|
+
|
|
244
|
+
bw session stop "$SESSION_ID"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
One-shot mode is explicit and is not self-healing:
|
|
228
248
|
|
|
229
249
|
```bash
|
|
230
|
-
|
|
250
|
+
bw platform run microsoft-teams meeting \
|
|
251
|
+
--auto-session \
|
|
252
|
+
--config /path/to/config.json
|
|
231
253
|
```
|
|
232
254
|
|
|
233
|
-
|
|
255
|
+
Use `--auto-session` only when losing the browser state after a failure is acceptable.
|
|
234
256
|
|
|
235
257
|
### Platform verification contract
|
|
236
258
|
|
|
@@ -274,13 +296,14 @@ Read the matching platform `README.md` and `manifest.yaml` before invoking or mo
|
|
|
274
296
|
|
|
275
297
|
## Installation
|
|
276
298
|
|
|
277
|
-
|
|
299
|
+
Use the published PyPI command (requires `uv`/`uvx`):
|
|
278
300
|
|
|
279
301
|
```bash
|
|
280
|
-
|
|
302
|
+
uvx browser-workspace status
|
|
303
|
+
uvx browser-workspace session list
|
|
281
304
|
```
|
|
282
305
|
|
|
283
|
-
|
|
306
|
+
`uvx` resolves the published Python wheel and installs the dependencies declared in `pyproject.toml`. Do not run the legacy bundled installer or point agents at a checked-out `bin/browser-workspace` script. The installed CLI reuses the same Browser Workspace Chrome profile and Session Daemon socket.
|
|
284
307
|
|
|
285
308
|
There is no required separate Browser Harness installation.
|
|
286
309
|
|
|
@@ -298,4 +321,4 @@ kgbghhigmbpefppgkocgjgnnnbhjchic
|
|
|
298
321
|
|
|
299
322
|
## Internal provenance
|
|
300
323
|
|
|
301
|
-
The low-level browser runtime is
|
|
324
|
+
The low-level browser runtime is the separately maintained `browser-harness` Python dependency. Browser Workspace owns the session/workspace/platform product layer and pins the exact Harness runtime revision it was tested with; Harness source is not vendored into this repository. Upstream licensing remains documented under `licenses/`.
|
|
Binary file
|
package/bin/browser-workspace
CHANGED
|
@@ -30,18 +30,165 @@ def status():
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
def main():
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
fmt=argparse.RawDescriptionHelpFormatter
|
|
34
|
+
p=argparse.ArgumentParser(
|
|
35
|
+
prog='browser-workspace',
|
|
36
|
+
description='Control the shared Browser Workspace Chrome, browser sessions, and platform automations.',
|
|
37
|
+
epilog='''Examples:
|
|
38
|
+
browser-workspace status
|
|
39
|
+
browser-workspace session start --url https://example.com
|
|
40
|
+
browser-workspace session list
|
|
41
|
+
browser-workspace session exec SESSION_ID --code-file script.py
|
|
42
|
+
browser-workspace session stop SESSION_ID
|
|
43
|
+
browser-workspace session stop --all
|
|
44
|
+
browser-workspace platform profile wechat-channels
|
|
45
|
+
browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
|
|
46
|
+
browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
|
|
47
|
+
browser-workspace platform run wechat-channels manage --auto-session --config status.json
|
|
48
|
+
''',
|
|
49
|
+
formatter_class=fmt,
|
|
50
|
+
)
|
|
51
|
+
sp=p.add_subparsers(dest='group',required=True,title='commands',metavar='COMMAND')
|
|
52
|
+
|
|
53
|
+
sp.add_parser(
|
|
54
|
+
'status',
|
|
55
|
+
help='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
|
|
56
|
+
description='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
|
|
57
|
+
epilog='Example:\n browser-workspace status',
|
|
58
|
+
formatter_class=fmt,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
session=sp.add_parser(
|
|
62
|
+
'session',
|
|
63
|
+
help='Create, inspect, execute code in, and stop browser sessions.',
|
|
64
|
+
description='Manage Browser Workspace sessions.',
|
|
65
|
+
epilog='''Examples:
|
|
66
|
+
browser-workspace session start --url https://example.com
|
|
67
|
+
browser-workspace session list
|
|
68
|
+
browser-workspace session exec SESSION_ID --code-file script.py
|
|
69
|
+
cat script.py | browser-workspace session exec SESSION_ID
|
|
70
|
+
browser-workspace session stop SESSION_ID
|
|
71
|
+
browser-workspace session stop --all
|
|
72
|
+
''',
|
|
73
|
+
formatter_class=fmt,
|
|
74
|
+
)
|
|
75
|
+
ssp=session.add_subparsers(dest='cmd',required=True,title='session commands',metavar='COMMAND')
|
|
76
|
+
|
|
77
|
+
start=ssp.add_parser(
|
|
78
|
+
'start',
|
|
79
|
+
help='Start a browser session, optionally opening a URL.',
|
|
80
|
+
description='Start a browser session. The shared managed Chrome is reused; this does not launch a separate browser per session.',
|
|
81
|
+
epilog='''Examples:
|
|
82
|
+
browser-workspace session start
|
|
83
|
+
browser-workspace session start --url https://example.com
|
|
84
|
+
''',
|
|
85
|
+
formatter_class=fmt,
|
|
86
|
+
)
|
|
87
|
+
start.add_argument('--workspace', help=argparse.SUPPRESS)
|
|
88
|
+
start.add_argument('--url', metavar='URL', help='URL to open when the session starts.')
|
|
89
|
+
|
|
90
|
+
ssp.add_parser(
|
|
91
|
+
'list',
|
|
92
|
+
help='List currently active Browser Workspace sessions.',
|
|
93
|
+
description='List currently active Browser Workspace sessions and their target tabs.',
|
|
94
|
+
epilog='Example:\n browser-workspace session list',
|
|
95
|
+
formatter_class=fmt,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
ex=ssp.add_parser(
|
|
99
|
+
'exec',
|
|
100
|
+
help='Execute Browser Workspace Python helpers inside an existing session.',
|
|
101
|
+
description='Execute Python using Browser Workspace helpers in an existing session. Read code from --code-file or stdin.',
|
|
102
|
+
epilog='''Examples:
|
|
103
|
+
browser-workspace session exec SESSION_ID --code-file script.py
|
|
104
|
+
|
|
105
|
+
browser-workspace session exec SESSION_ID <<'PY'
|
|
106
|
+
print(page_info())
|
|
107
|
+
print(snapshot(interactive_only=False))
|
|
108
|
+
PY
|
|
109
|
+
''',
|
|
110
|
+
formatter_class=fmt,
|
|
111
|
+
)
|
|
112
|
+
ex.add_argument('session_id', metavar='SESSION_ID', help='Session ID returned by "session start" or "session list".')
|
|
113
|
+
ex.add_argument('--code-file', metavar='PATH', help='Read Python code from this file instead of stdin.')
|
|
114
|
+
|
|
115
|
+
stop=ssp.add_parser(
|
|
116
|
+
'stop',
|
|
117
|
+
help='Stop one session or all sessions and release their Browser Workspace tabs.',
|
|
118
|
+
description='Stop one Browser Workspace session, or all sessions with --all.',
|
|
119
|
+
epilog='''Examples:
|
|
120
|
+
browser-workspace session stop SESSION_ID
|
|
121
|
+
browser-workspace session stop --all
|
|
122
|
+
''',
|
|
123
|
+
formatter_class=fmt,
|
|
124
|
+
)
|
|
125
|
+
stop.add_argument('session_id', nargs='?', metavar='SESSION_ID', help='Session ID to stop.')
|
|
126
|
+
stop.add_argument('--all', action='store_true', dest='stop_all', help='Stop all active Browser Workspace sessions.')
|
|
127
|
+
|
|
128
|
+
platform=sp.add_parser(
|
|
129
|
+
'platform',
|
|
130
|
+
help='Inspect and run reusable website-specific platform automations.',
|
|
131
|
+
description='Inspect and run reusable Browser Workspace platform automations.',
|
|
132
|
+
epilog='''Examples:
|
|
133
|
+
browser-workspace platform profile wechat-channels
|
|
134
|
+
browser-workspace platform source wechat-channels post --config post.json
|
|
135
|
+
browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
|
|
136
|
+
browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
|
|
137
|
+
browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
|
|
138
|
+
browser-workspace session stop SESSION_ID
|
|
139
|
+
|
|
140
|
+
One-shot mode (no orchestrator self-heal):
|
|
141
|
+
browser-workspace platform run wechat-channels manage --auto-session --config status.json
|
|
142
|
+
''',
|
|
143
|
+
formatter_class=fmt,
|
|
144
|
+
)
|
|
145
|
+
psp=platform.add_subparsers(dest='pcmd',required=True,title='platform commands',metavar='COMMAND')
|
|
146
|
+
|
|
147
|
+
profile=psp.add_parser(
|
|
148
|
+
'profile',
|
|
149
|
+
help='Show a platform manifest/profile: fields, media rules, and capabilities.',
|
|
150
|
+
description='Show the declared Browser Workspace profile for a platform.',
|
|
151
|
+
epilog='Example:\n browser-workspace platform profile wechat-channels',
|
|
152
|
+
formatter_class=fmt,
|
|
153
|
+
)
|
|
154
|
+
profile.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
|
|
155
|
+
|
|
156
|
+
source=psp.add_parser(
|
|
157
|
+
'source',
|
|
158
|
+
help='Print the fully prepared Python source for one platform action without running it.',
|
|
159
|
+
description='Render the action source after injecting the optional JSON config path. No browser action is executed.',
|
|
160
|
+
epilog='''Examples:
|
|
161
|
+
browser-workspace platform source wechat-channels post --config post.json
|
|
162
|
+
browser-workspace platform source wechat-channels manage --config status.json
|
|
163
|
+
''',
|
|
164
|
+
formatter_class=fmt,
|
|
165
|
+
)
|
|
166
|
+
source.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
|
|
167
|
+
source.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
|
|
168
|
+
source.add_argument('--config', metavar='PATH', help='JSON configuration file used to prepare the action.')
|
|
169
|
+
|
|
170
|
+
run=psp.add_parser(
|
|
171
|
+
'run',
|
|
172
|
+
help='Run one platform action in Browser Workspace.',
|
|
173
|
+
description='Run a declared platform action. A caller-owned --session-id is required by default so an orchestrator can inspect failures, self-heal in the same browser state, write repairs back to platform code, and close the session only after completion. --auto-session is an explicit one-shot mode without orchestrator self-heal.',
|
|
174
|
+
epilog='''Examples:
|
|
175
|
+
browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
|
|
176
|
+
browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
|
|
177
|
+
browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
|
|
178
|
+
browser-workspace session stop SESSION_ID
|
|
179
|
+
|
|
180
|
+
One-shot mode (no orchestrator self-heal):
|
|
181
|
+
browser-workspace platform run wechat-channels manage --auto-session --config status.json
|
|
182
|
+
''',
|
|
183
|
+
formatter_class=fmt,
|
|
184
|
+
)
|
|
185
|
+
run.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
|
|
186
|
+
run.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
|
|
187
|
+
run.add_argument('--config', metavar='PATH', help='JSON configuration file for the platform action.')
|
|
188
|
+
run.add_argument('--url', metavar='URL', help='Optional initial URL for a temporary session.')
|
|
189
|
+
session_mode = run.add_mutually_exclusive_group()
|
|
190
|
+
session_mode.add_argument('--session-id', metavar='SESSION_ID', help='Required by default. Reuse a caller-owned session so failures can be inspected and self-healed before the session is closed.')
|
|
191
|
+
session_mode.add_argument('--auto-session', action='store_true', help='Explicit one-shot mode: create and clean up a temporary session automatically. No orchestrator self-heal is available after failure.')
|
|
45
192
|
args=p.parse_args()
|
|
46
193
|
if args.group=='status': emit(status()); return
|
|
47
194
|
if args.group=='session' and args.cmd=='list':
|
|
@@ -71,5 +218,19 @@ def main():
|
|
|
71
218
|
from platform_runner import platform_profile
|
|
72
219
|
emit(platform_profile(args.platform)); return
|
|
73
220
|
if args.group=='platform' and args.pcmd=='run':
|
|
74
|
-
|
|
221
|
+
if not args.session_id and not args.auto_session:
|
|
222
|
+
emit({
|
|
223
|
+
'ok': False,
|
|
224
|
+
'error': 'session_id_required',
|
|
225
|
+
'message': 'platform run requires --session-id by default so the orchestrator can self-heal failures in the same browser session.',
|
|
226
|
+
'suggested_workflow': [
|
|
227
|
+
'browser-workspace session start --url <URL>',
|
|
228
|
+
'browser-workspace platform run <PLATFORM> <ACTION> --session-id SESSION_ID [--config FILE]',
|
|
229
|
+
'inspect/self-heal failures in the same SESSION_ID and write successful repairs back to platform code',
|
|
230
|
+
'browser-workspace session stop SESSION_ID',
|
|
231
|
+
],
|
|
232
|
+
'auto_session': 'Use --auto-session only for a one-shot run. It creates and closes its own session and does not provide orchestrator self-heal.',
|
|
233
|
+
})
|
|
234
|
+
raise SystemExit(2)
|
|
235
|
+
r=run_platform_action(args.platform,args.action,config_path=args.config,url=args.url,session_id=args.session_id,auto_session=args.auto_session); emit(r); raise SystemExit(0 if (r.get('result') or {}).get('ok',False) else 1)
|
|
75
236
|
if __name__=='__main__': main()
|
package/package.json
CHANGED
package/platform_runner.py
CHANGED
|
@@ -102,11 +102,16 @@ def declared_actions(platform: str) -> dict[str, Path]:
|
|
|
102
102
|
raise ValueError(f"{platform}/manifest.yaml has no actions block")
|
|
103
103
|
|
|
104
104
|
actions: dict[str, Path] = {}
|
|
105
|
-
for
|
|
106
|
-
r'^\s{2}([\w-]+):\s*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
for line in match.group("body").splitlines():
|
|
106
|
+
entry = re.match(r'^\s{2}([\w-]+):\s*(.+?)\s*$', line)
|
|
107
|
+
if not entry:
|
|
108
|
+
continue
|
|
109
|
+
name, relative = entry.groups()
|
|
110
|
+
if len(relative) >= 2 and relative[0] == relative[-1] and relative[0] in {"\"", "'"}:
|
|
111
|
+
relative = relative[1:-1]
|
|
112
|
+
# Actions are simple relative file paths; comments and nested mappings are not runnable actions.
|
|
113
|
+
if not relative or relative.startswith(("#", "{", "[")) or " #" in relative:
|
|
114
|
+
continue
|
|
110
115
|
path = PLATFORMS / platform / relative
|
|
111
116
|
if not path.is_file():
|
|
112
117
|
raise FileNotFoundError(f"declared platform action missing: {platform}/{name} -> {relative}")
|
|
@@ -146,6 +151,7 @@ def run_platform_action(
|
|
|
146
151
|
config_path: str | None = None,
|
|
147
152
|
url: str | None = None,
|
|
148
153
|
session_id: str | None = None,
|
|
154
|
+
auto_session: bool = False,
|
|
149
155
|
) -> dict:
|
|
150
156
|
path = action_path(platform, action)
|
|
151
157
|
config_file = Path(config_path) if config_path else None
|
|
@@ -156,7 +162,16 @@ def run_platform_action(
|
|
|
156
162
|
validate_platform_config(platform, action, config)
|
|
157
163
|
code = prepare_action(path, config_file)
|
|
158
164
|
|
|
159
|
-
|
|
165
|
+
if not session_id and not auto_session:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"platform run requires session_id; orchestrators should use: "
|
|
168
|
+
"session start -> platform run --session-id SESSION_ID -> self-heal in the same session -> session stop. "
|
|
169
|
+
"Use auto_session=True only for a one-shot run without orchestrator self-heal."
|
|
170
|
+
)
|
|
171
|
+
if session_id and auto_session:
|
|
172
|
+
raise ValueError("session_id and auto_session are mutually exclusive")
|
|
173
|
+
|
|
174
|
+
owned_session = bool(auto_session)
|
|
160
175
|
started = request({"op": "start", "workspace": None, "url": url}) if owned_session else {"session_id": session_id, "workspace_supported": None}
|
|
161
176
|
sid = started["session_id"]
|
|
162
177
|
result = None
|
|
@@ -168,6 +183,8 @@ def run_platform_action(
|
|
|
168
183
|
"platform": platform,
|
|
169
184
|
"action": action,
|
|
170
185
|
"workspace_supported": started.get("workspace_supported"),
|
|
186
|
+
"session_mode": "auto_session_no_self_heal" if owned_session else "caller_owned_self_heal_capable",
|
|
187
|
+
"self_heal_capable": not owned_session,
|
|
171
188
|
"result": result,
|
|
172
189
|
}
|
|
173
190
|
if started.get("warning"):
|
|
@@ -439,7 +439,14 @@ def _fill_prompt_if_needed(prompt, mode_label, timeout=30):
|
|
|
439
439
|
deadline = time.time() + timeout
|
|
440
440
|
last_error = None
|
|
441
441
|
while time.time() < deadline:
|
|
442
|
-
|
|
442
|
+
try:
|
|
443
|
+
selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
|
|
444
|
+
except RuntimeError as exc:
|
|
445
|
+
# Hydration and navigation can temporarily remove the composer.
|
|
446
|
+
# Continue the outer readiness window rather than aborting on the first poll.
|
|
447
|
+
last_error = exc
|
|
448
|
+
time.sleep(.25)
|
|
449
|
+
continue
|
|
443
450
|
if prompt_text_matches(_composer_text(selector), prompt):
|
|
444
451
|
return selector
|
|
445
452
|
|
|
@@ -567,5 +574,18 @@ while time.time() < deadline:
|
|
|
567
574
|
else:
|
|
568
575
|
raise RuntimeError("Temporary Chat activation was not observed")
|
|
569
576
|
|
|
570
|
-
|
|
577
|
+
try:
|
|
578
|
+
result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
|
|
579
|
+
except RuntimeError as exc:
|
|
580
|
+
# A URL-prefilled Temporary Chat can retain the pre-hydration home input
|
|
581
|
+
# without ever mounting the active composer. No send occurred, so retry
|
|
582
|
+
# once on a clean Temporary Chat page without URL prompt prefill.
|
|
583
|
+
if "composer readiness was not observed ready" not in str(exc):
|
|
584
|
+
raise
|
|
585
|
+
goto_url(temporary_chat_entry_url())
|
|
586
|
+
wait_for_load()
|
|
587
|
+
_click_temporary_chat_toggle()
|
|
588
|
+
if not _temporary_chat_enabled():
|
|
589
|
+
raise RuntimeError("Temporary Chat mode lost during composer recovery") from exc
|
|
590
|
+
result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
|
|
571
591
|
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
@@ -174,3 +174,10 @@ def test_new_turn_project_id_is_optional():
|
|
|
174
174
|
assert 'if not thread_id: raise RuntimeError("thread_id is required")' in turn
|
|
175
175
|
assert 'https://chatgpt.com/c/{quote(thread_id' in turn
|
|
176
176
|
assert 'https://chatgpt.com/g/{quote(project_id' in turn
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_temporary_submit_recovers_only_before_send_from_prefill_hydration():
|
|
180
|
+
code=(ACTIONS/"_temporary_submit.py").read_text()
|
|
181
|
+
assert 'goto_url(temporary_chat_entry_url())' in code
|
|
182
|
+
assert '"composer readiness was not observed ready" not in str(exc)' in code
|
|
183
|
+
assert 'if not _temporary_chat_enabled():' in code
|
|
@@ -6,7 +6,7 @@ import re
|
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
from urllib.parse import parse_qs, urlparse
|
|
8
8
|
|
|
9
|
-
MANAGER_URL = "https://channels.weixin.qq.com/platform/post"
|
|
9
|
+
MANAGER_URL = "https://channels.weixin.qq.com/platform/post/list"
|
|
10
10
|
|
|
11
11
|
STATUS_LABELS: tuple[tuple[str, str], ...] = (
|
|
12
12
|
("审核中", "reviewing"),
|