@anweat/dsh-browser 0.1.17 → 0.2.0-rc.1

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 (162) hide show
  1. package/README.md +238 -71
  2. package/assets/skills/dsh-browser/SKILL.md +120 -0
  3. package/assets/skills/dsh-browser/references/develop.md +48 -0
  4. package/assets/skills/dsh-browser/references/observe.md +91 -0
  5. package/assets/skills/dsh-browser/references/opencli.md +25 -0
  6. package/assets/skills/dsh-browser/references/recipes.md +149 -0
  7. package/assets/skills/dsh-browser/references/userscripts.md +44 -0
  8. package/cordis.patch.yml +4 -0
  9. package/lib/actions/act.d.ts +3 -0
  10. package/lib/actions/act.js +147 -0
  11. package/lib/actions/act.js.map +1 -0
  12. package/lib/actions/automation.d.ts +3 -0
  13. package/lib/actions/automation.js +356 -0
  14. package/lib/actions/automation.js.map +1 -0
  15. package/lib/actions/crawl.d.ts +3 -0
  16. package/lib/actions/crawl.js +28 -0
  17. package/lib/actions/crawl.js.map +1 -0
  18. package/lib/actions/errors.d.ts +53 -0
  19. package/lib/actions/errors.js +178 -0
  20. package/lib/actions/errors.js.map +1 -0
  21. package/lib/actions/index-view.d.ts +42 -0
  22. package/lib/actions/index-view.js +268 -0
  23. package/lib/actions/index-view.js.map +1 -0
  24. package/lib/actions/inspect.d.ts +3 -0
  25. package/lib/actions/inspect.js +35 -0
  26. package/lib/actions/inspect.js.map +1 -0
  27. package/lib/actions/observe.d.ts +3 -0
  28. package/lib/actions/observe.js +121 -0
  29. package/lib/actions/observe.js.map +1 -0
  30. package/lib/actions/opencli.d.ts +3 -0
  31. package/lib/actions/opencli.js +51 -0
  32. package/lib/actions/opencli.js.map +1 -0
  33. package/lib/actions/registry.d.ts +35 -0
  34. package/lib/actions/registry.js +89 -0
  35. package/lib/actions/registry.js.map +1 -0
  36. package/lib/actions/run.d.ts +34 -0
  37. package/lib/actions/run.js +226 -0
  38. package/lib/actions/run.js.map +1 -0
  39. package/lib/actions/runtime.d.ts +3 -0
  40. package/lib/actions/runtime.js +33 -0
  41. package/lib/actions/runtime.js.map +1 -0
  42. package/lib/actions/schema.d.ts +31 -0
  43. package/lib/actions/schema.js +309 -0
  44. package/lib/actions/schema.js.map +1 -0
  45. package/lib/actions/script.d.ts +3 -0
  46. package/lib/actions/script.js +97 -0
  47. package/lib/actions/script.js.map +1 -0
  48. package/lib/actions/shared.d.ts +31 -0
  49. package/lib/actions/shared.js +45 -0
  50. package/lib/actions/shared.js.map +1 -0
  51. package/lib/actions/surface.d.ts +22 -0
  52. package/lib/actions/surface.js +18 -0
  53. package/lib/actions/surface.js.map +1 -0
  54. package/lib/actions/target.d.ts +3 -0
  55. package/lib/actions/target.js +66 -0
  56. package/lib/actions/target.js.map +1 -0
  57. package/lib/actions/types.d.ts +181 -0
  58. package/lib/actions/types.js +48 -0
  59. package/lib/actions/types.js.map +1 -0
  60. package/lib/activation-rules.d.ts +29 -0
  61. package/lib/activation-rules.js +33 -0
  62. package/lib/activation-rules.js.map +1 -0
  63. package/lib/approval-policy.d.ts +40 -2
  64. package/lib/approval-policy.js +184 -97
  65. package/lib/approval-policy.js.map +1 -1
  66. package/lib/automation-assets-rpc.d.ts +8 -2
  67. package/lib/automation-assets-rpc.js +67 -6
  68. package/lib/automation-assets-rpc.js.map +1 -1
  69. package/lib/automation-assets.d.ts +152 -4
  70. package/lib/automation-assets.js +255 -15
  71. package/lib/automation-assets.js.map +1 -1
  72. package/lib/automation-convert.d.ts +23 -0
  73. package/lib/automation-convert.js +110 -0
  74. package/lib/automation-convert.js.map +1 -0
  75. package/lib/automation-development.d.ts +26 -0
  76. package/lib/automation-development.js +47 -0
  77. package/lib/automation-development.js.map +1 -1
  78. package/lib/automation-execution.d.ts +57 -1
  79. package/lib/automation-execution.js +167 -11
  80. package/lib/automation-execution.js.map +1 -1
  81. package/lib/automation-journal.d.ts +92 -0
  82. package/lib/automation-journal.js +449 -0
  83. package/lib/automation-journal.js.map +1 -0
  84. package/lib/automation-v2.d.ts +138 -0
  85. package/lib/automation-v2.js +631 -0
  86. package/lib/automation-v2.js.map +1 -0
  87. package/lib/automation.d.ts +82 -5
  88. package/lib/automation.js +239 -82
  89. package/lib/automation.js.map +1 -1
  90. package/lib/browser-service.d.ts +304 -32
  91. package/lib/browser-service.js +631 -182
  92. package/lib/browser-service.js.map +1 -1
  93. package/lib/client/SettingsCard.js +95 -57
  94. package/lib/client/SettingsCard.js.map +1 -1
  95. package/lib/client/asset-editor.d.ts +55 -0
  96. package/lib/client/asset-editor.js +101 -0
  97. package/lib/client/asset-editor.js.map +1 -0
  98. package/lib/client/automation-assets-client.d.ts +99 -2
  99. package/lib/client/automation-assets-client.js +243 -10
  100. package/lib/client/automation-assets-client.js.map +1 -1
  101. package/lib/client/form.d.ts +48 -4
  102. package/lib/client/form.js +142 -29
  103. package/lib/client/form.js.map +1 -1
  104. package/lib/client/index.d.ts +25 -2
  105. package/lib/client/index.js +5 -1
  106. package/lib/client/index.js.map +1 -1
  107. package/lib/client/locales.d.ts +85 -6
  108. package/lib/client/locales.js +64 -12
  109. package/lib/client/locales.js.map +1 -1
  110. package/lib/client/prompts-form.d.ts +67 -0
  111. package/lib/client/prompts-form.js +140 -0
  112. package/lib/client/prompts-form.js.map +1 -0
  113. package/lib/client/prompts-status-client.d.ts +34 -0
  114. package/lib/client/prompts-status-client.js +53 -0
  115. package/lib/client/prompts-status-client.js.map +1 -0
  116. package/lib/client.js +1914 -329
  117. package/lib/client.js.map +1 -1
  118. package/lib/config.d.ts +50 -1
  119. package/lib/config.js +31 -1
  120. package/lib/config.js.map +1 -1
  121. package/lib/dialogs.d.ts +37 -0
  122. package/lib/dialogs.js +73 -0
  123. package/lib/dialogs.js.map +1 -0
  124. package/lib/freedom.d.ts +23 -7
  125. package/lib/freedom.js +48 -59
  126. package/lib/freedom.js.map +1 -1
  127. package/lib/index.d.ts +4 -4
  128. package/lib/index.js +36 -10
  129. package/lib/index.js.map +1 -1
  130. package/lib/journal.d.ts +109 -0
  131. package/lib/journal.js +238 -0
  132. package/lib/journal.js.map +1 -0
  133. package/lib/locator.d.ts +82 -0
  134. package/lib/locator.js +190 -0
  135. package/lib/locator.js.map +1 -0
  136. package/lib/observe-script.d.ts +27 -0
  137. package/lib/observe-script.js +565 -0
  138. package/lib/observe-script.js.map +1 -0
  139. package/lib/observe.d.ts +179 -0
  140. package/lib/observe.js +368 -0
  141. package/lib/observe.js.map +1 -0
  142. package/lib/policy-keys.d.ts +30 -0
  143. package/lib/policy-keys.js +34 -0
  144. package/lib/policy-keys.js.map +1 -0
  145. package/lib/prompt-limits.d.ts +30 -0
  146. package/lib/prompt-limits.js +31 -0
  147. package/lib/prompt-limits.js.map +1 -0
  148. package/lib/prompts.d.ts +131 -0
  149. package/lib/prompts.js +295 -0
  150. package/lib/prompts.js.map +1 -0
  151. package/lib/skill.d.ts +100 -0
  152. package/lib/skill.js +181 -0
  153. package/lib/skill.js.map +1 -0
  154. package/lib/tool-defs.d.ts +72 -0
  155. package/lib/tool-defs.js +67 -0
  156. package/lib/tool-defs.js.map +1 -0
  157. package/lib/tools.d.ts +19 -4
  158. package/lib/tools.js +124 -922
  159. package/lib/tools.js.map +1 -1
  160. package/package.json +5 -1
  161. package/scripts/dump-prompts.mjs +29 -0
  162. package/scripts/measure-tool-surface.mjs +190 -0
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: dsh-browser
3
+ description: Use to open, read, or operate web pages, and to reuse or develop browser automation assets (recipes, userscripts) with the dsh-browser tools browser_index and browser_call. 打开、读取、操作网页,或复用、开发浏览器自动化资产时使用。
4
+ ---
5
+
6
+ # dsh-browser
7
+
8
+ Browser capabilities come through two tools. `browser_index` shows what exists; `browser_call` runs one action and returns `{ok, action, seq?, executionStatus, result | error{code,message,hint}}`. Actions are named `group.action`. Do not guess other tool names.
9
+
10
+ ## 1. Reuse first
11
+
12
+ At the start of a task, search for an existing verified asset. If one fits, run it instead of exploring the site again.
13
+
14
+ ```json call
15
+ {"action":"automation.search","args":{"query":"search issues","domain":"github.com"}}
16
+ ```
17
+
18
+ ```json call
19
+ {"action":"automation.run","args":{"id":"asset-id-from-search","url":"https://github.com/search","inputs":{"query":"dsh"}}}
20
+ ```
21
+
22
+ Two calls, no schemas needed; only active assets are returned.
23
+
24
+ ## 2. Finding actions
25
+
26
+ ```json index
27
+ {}
28
+ ```
29
+
30
+ lists the groups and the environment state. `{"group":"act"}` lists a group, `{"action":"act.click"}` gives a schema, `{"query":"upload"}` searches. `automation.develop` has sub-actions; one sub-action's schema:
31
+
32
+ ```json index
33
+ {"action":"automation.develop.save"}
34
+ ```
35
+
36
+ Six calls you rarely need a schema for:
37
+
38
+ ```json call
39
+ {"action":"target.open","args":{"url":"https://example.com/"}}
40
+ ```
41
+
42
+ ```json call
43
+ {"action":"observe.read","args":{}}
44
+ ```
45
+
46
+ ```json call
47
+ {"action":"act.click","args":{"locator":{"role":"button","name":"Search"}}}
48
+ ```
49
+
50
+ ```json call
51
+ {"action":"act.fill","args":{"locator":{"label":"Query"},"text":"dsh"}}
52
+ ```
53
+
54
+ ```json call
55
+ {"action":"act.wait","args":{"locator":{"text":"Results"}}}
56
+ ```
57
+
58
+ A wrong argument returns `INVALID_ARGS` with the schema attached: fix and resend.
59
+
60
+ ## 3. Locating elements
61
+
62
+ - Prefer a `locator` with `role` + `name`, then `label`, `text`, `testId`; a CSS `selector` only if nothing semantic exists.
63
+ - Locators are strict: one matching several elements does nothing and fails `LOCATOR_AMBIGUOUS` (`error.candidates` lists five). Narrow it (`exact`, a better name, `frame`/`framePath`); only if you must pick one, add `index` and `indexReason`.
64
+ - `observe.read` with `sections` (`controls`, `links`, `tables`; `references/observe.md`) gives checked locators for `act.*`; `ambiguous:true` means none is unique.
65
+ - Results carry `targetId` and `generation`. Pass `generation` as `expectGeneration` to `act.*`: if the page navigated since, it fails `TARGET_STALE` instead of acting.
66
+ - `act.fill` replaces, `act.type` presses keys, `act.clear` empties. A popup joins `target.list`; switch with `target.select`.
67
+
68
+ ## 4. Side effects and failure
69
+
70
+ `act.*` change the page and may need the user's approval in `standard` mode. Read `error.code` and `error.hint`:
71
+
72
+ | code | what to do |
73
+ |---|---|
74
+ | `INVALID_ARGS` | fix the arguments using the attached schema |
75
+ | `LOCATOR_NOT_FOUND` | read the page, adjust the locator, `act.wait` for dynamic content; nothing was done |
76
+ | `LOCATOR_AMBIGUOUS` | several matched, nothing was done: choose from `error.candidates` |
77
+ | `NOT_ACTIONABLE` | hidden, disabled, or covered: wait or dismiss the overlay; nothing was done |
78
+ | `TARGET_CLOSED` | call `target.open` again |
79
+ | `TARGET_STALE` | the page navigated (also when a read hit a navigation twice); nothing was done: `observe.read` again, use the new `generation` |
80
+ | `DEADLINE` | with `executionStatus: "outcome_unknown"` it may have taken effect: verify with `observe.read`, never resubmit blindly |
81
+ | `OUTCOME_UNKNOWN` | a page-changing step timed out mid-way: check with `observe.read` before any retry |
82
+ | `VALIDATION_FAILED` | the steps ran but an assert or postcondition did not hold: read the page, fix the recipe; do not repeat submits |
83
+ | `VALIDATION_MISSING` | a v2 draft has no `assert` or postcondition, so its test cannot pass: add one, save, test again |
84
+ | `CANCELLED` | stopped; steps that already ran stay done (see `effects`) |
85
+ | `POLICY_DENIED` / `CAPABILITY_UNAVAILABLE` | not allowed or not set up here (Chromium missing: `runtime.install`) |
86
+
87
+ Recipe runs (`automation.run_recipe`, `automation.run`, `automation.develop` test) return the whole run in `result` even when `ok` is false: `executionStatus`, `validationStatus` (`not_checked`, `passed`, `failed`), `completedSteps`, `failedStep`, `effects` (`none`, `observed`, `unknown`), `outputs`. `completed` with `not_checked` means the steps ran, not that the result is right; nothing is rolled back (`references/recipes.md`).
88
+
89
+ After a submit, purchase, or any irreversible step, confirm the result on the page (confirmation text, new URL, changed list) before telling the user it worked.
90
+
91
+ ## 5. Choosing a path (cheapest first)
92
+
93
+ 1. A saved asset: `automation.search` then `automation.run`.
94
+ 2. OpenCLI adapters if the platform is covered: `opencli.catalog`, `opencli.run` (`references/opencli.md`).
95
+ 3. Read-only extraction: `script.catalog`, `script.run_builtin`, a userscript (`references/userscripts.md`).
96
+ 4. Page interaction: `target.open`, `observe.read`, `act.*` (`references/observe.md`).
97
+ 5. Bounded multi-step work: `automation.run_recipe` (`references/recipes.md`); `crawl.crawl` for anonymous crawls.
98
+
99
+ ## 6. Turning a success into an asset
100
+
101
+ When an exploration worked and will likely repeat, do not rewrite it by hand. Every page-touching call you made is in a session journal (its `seq` is in each reply); build the draft from it, then test it on other inputs:
102
+
103
+ ```json call
104
+ {"action":"automation.develop","args":{"action":"draft_from_journal","name":"Keyword search","parameters":[{"seq":3,"field":"text","name":"keyword"}],"extract":[{"seq":7,"as":"items"}],"postconditions":[{"output":"items","allowEmpty":true},{"selector":"#status"}]}}
105
+ ```
106
+
107
+ ```json call
108
+ {"action":"automation.develop","args":{"action":"test","id":"draft-id","url":"https://example.com/search","inputSets":[{"keyword":"alpha"},{"keyword":"beta"}]}}
109
+ ```
110
+
111
+ `parameters` turns a typed value into an input, `extract` names the `observe.read` that is the result, `postconditions` check it; failed calls and plain observations are left out. Read `unmapped` (what a recipe cannot express: the draft is then incomplete and cannot be activated), `warnings`, `pendingDisambiguation`. `inputSets` (2 to 5) run each in a fresh context and all must pass; `PARAMETERIZATION_SUSPECT` means different inputs gave the same output. Details: `references/develop.md`.
112
+
113
+ Write a recipe by hand (`save`, `validate`, `test`; schema v2 in `references/recipes.md`) only when the journal cannot cover it. `convert` turns a v1 asset into a v2 draft.
114
+
115
+ Every save is a new revision and a test vouches only for the revision it ran on. A draft is not usable until the user activates a tested revision; never claim it is ready. To repair an active asset, `{"action":"fork","id":...}` gives an editable copy. Never put cookies, tokens, passwords, or page content in an asset.
116
+
117
+ ## 7. Boundaries
118
+
119
+ - The user's automationMode decides what is exposed and what asks for approval. Do not route around a refusal.
120
+ - Logged-in access uses a named AuthProfile (`authProfile` on `target.open`). Respect the site's rules and the usage buffer.
@@ -0,0 +1,48 @@
1
+ # Developing a draft from an exploration
2
+
3
+ The shortest path from "it worked once" to a reusable asset: explore with `browser_call`, `draft_from_journal`, test with `inputSets`. The user activates; you never do.
4
+
5
+ ## The journal
6
+
7
+ Every page-touching call (`target.*`, `observe.*`, `act.*`, `script.*`, `automation.run_recipe`, `automation.run`) is recorded in this session's journal and its reply carries `seq`. The journal keeps the last 200 calls, lives in memory only, and is dropped with the session. A call refused for bad arguments is not recorded; a failed call is, with its error code.
8
+
9
+ Each entry has the action, page `targetId`, `generationBefore` and `generationAfter`, the locator, scrubbed parameters, the outcome (`ok`, `executionStatus`, `errorCode`), `effects`, and a short summary. Typed text is never stored in an entry: `act.fill` and `act.type` text is a reference `{ref, length, sensitive?}`. The text itself is kept in memory only while it is short (200 characters or fewer) and does not look like a token or card number, and not at all when the page says the field is a password or the field is named like a secret. A `target.open` URL loses secret query values and userinfo. `observe.read` entries are observation points: they never become steps unless you name one in `extract`.
10
+
11
+ ## draft_from_journal
12
+
13
+ ```json call
14
+ {"action":"automation.develop","args":{"action":"draft_from_journal","name":"Keyword search","fromSeq":1,"toSeq":9,"exclude":[4],"parameters":[{"seq":3,"field":"text","name":"keyword","type":"string"}],"extract":[{"seq":8,"as":"items","mode":"text","dedupe":true}],"postconditions":[{"output":"items","allowEmpty":true},{"selector":"#status"}]}}
15
+ ```
16
+
17
+ - `fromSeq`, `toSeq`: the slice (default: the whole journal). `exclude`: more seq to leave out. Failed calls and observations are already left out.
18
+ - `parameters`: `field` is `text` (act.fill, act.type), `values` (act.select) or `url` (a later target.open). Without it the typed text stays a literal, and `parameterCandidates` lists what you could parameterize. A value that was not kept (secret, too long) becomes a required input automatically (`SENSITIVE_VALUE_BECAME_INPUT`, or `VALUE_BECAME_INPUT` for a long value); pass it when testing and running.
19
+ - `extract`: `seq` of an `observe.read` that read the result (with a `locator`); `as` names the output; `mode` text, html or links; `dedupe` removes repeated lines (text) or elements (links). `extractCandidates` lists the scoped reads you could use.
20
+ - `postconditions`: written as given. Without them the reply only suggests some (`suggestedPostconditions`) and a test cannot pass (`NO_VERIFIER`). For a result that may legitimately be empty use `{"output":"items","allowEmpty":true}` together with a success marker such as `{"selector":"#status"}`: an empty result then passes, a missing container still fails.
21
+ - `id`: replace the draft of an earlier call (a new revision). Each call is one draft write; a session has a few (3 by default), so pass `postconditions` the first time.
22
+ - `domains` default to the hosts of the pages involved; `name` is required.
23
+
24
+ The reply: `steps`, `complete`, `sourceMap` (seq to step number; the opening `target.open` maps to 0: it is the `url` of test and run, `suggestedTestUrl`), `observationPoints`, `excluded` (with reasons), `unmapped`, `inputSchema`, `pendingDisambiguation` (steps that pick a match by `index`), `parameterCandidates`, `extractCandidates`, `suggestedPostconditions`, `domains`, `warnings`.
25
+
26
+ ## What maps and what does not
27
+
28
+ Mapped: `act.click`, `hover`, `fill`, `type`, `clear`, `press`, `check`, `scroll`, `select` (one value), `act.wait` for a visible locator, plain URL text, network idle or a delay, a later `target.open` (a `goto`), a named `observe.read` (an `extract`). Unmapped, each with its reason: `script.*`, `act.upload`, `target.select` and any step on another page (a v2 recipe runs on one page), multi-value select, wait for hidden or detached, glob URL patterns, a frame chosen by name or url, a `target.open` whose URL carried a secret, a whole recipe run.
29
+
30
+ Any `unmapped` entry makes the draft `complete: false` (`INCOMPLETE_DRAFT`): it cannot be activated until you finish the recipe yourself and `save` it (that save replaces it). Warnings to act on: `GENERATION_JUMP` (the page moved between two calls without an action: add a wait or a goto), `EXCLUDED_NAVIGATION`, `NO_OPENING_STEP` (the test url must be the page the first step starts on), `FIXED_DELAY`, `NO_OUTPUT`, `FAILED_STEP_EXCLUDED` (the draft assumes later steps did not depend on it), `FAILED_STEP_EFFECTS_UNKNOWN`, `PARAMETER_NOT_APPLIED` and `EXTRACT_NOT_APPLIED` (the named seq is not a step), `JOURNAL_TRUNCATED` (the start of the exploration fell out of the 200 calls).
31
+
32
+ ## Testing with inputSets
33
+
34
+ ```json call
35
+ {"action":"automation.develop","args":{"action":"test","id":"draft-id","url":"https://example.com/search","inputSets":[{"keyword":"alpha"},{"keyword":"beta"}]}}
36
+ ```
37
+
38
+ `inputSets` takes 2 to 5 input objects, all valid for the `inputSchema` and different from each other (checked before anything runs). Each runs in a new BrowserContext, never in your session page, with its own cookies; the first failing set stops the test. All sets must pass for the credential to be `passed`; it records how many sets ran and a digest of each set's inputs and outputs, never the values. The reply has one entry per set (`outputs`, `validationStatus`, `failedStep`), not repeated steps.
39
+
40
+ If different inputs gave identical outputs the test still passes but replies `warnings: [PARAMETERIZATION_SUSPECT]` with the set numbers, and the credential carries it: the input probably does not reach the page, or the output ignores it. Fix that before asking the user to activate. Without `inputSets` a test is one run on the session page, as before. An asset that declares inputs cannot be activated unless its passing test covered at least `automationAssets.minInputSetsForActivation` input sets (default 2): a single-input test cannot show the recipe does not hard-code that input, and activation is refused with `insufficient-input-sets`. Assets without inputs are not affected.
41
+
42
+ ## The whole path
43
+
44
+ 1. `automation.search`; reuse if an asset fits.
45
+ 2. Explore: `target.open`, `observe.read`, `act.*`, `act.wait`, `observe.read` with a `locator` for the result.
46
+ 3. `draft_from_journal` with `parameters`, `extract`, `postconditions`; read `unmapped` and `warnings`.
47
+ 4. `test` with `inputSets` (include a keyword with no result when an empty result is legitimate).
48
+ 5. Tell the user the draft and its tested revision; they activate it with that revision.
@@ -0,0 +1,91 @@
1
+ # Reading what the browser returns
2
+
3
+ Load this when a page result is hard to interpret.
4
+
5
+ ## observe.read and action results
6
+
7
+ `target.open`, `observe.read`, and the `act.*` actions return the page state in `result`:
8
+
9
+ - `url`: the current address (check it after a click to see whether navigation happened).
10
+ - `title`: the page title.
11
+ - `text`: readable page text, bounded. Long pages are cut; if the part you need is missing, scroll with `act.scroll` and read again, or extract it with a recipe or built-in script.
12
+ - `targetId` and `generation`: which page of your session this is and its version (see Page generations below).
13
+ - `screenshotPath`: a file path on the host (only when a screenshot was taken, for example by `target.open`).
14
+
15
+ A `truncation` field on the envelope means long text fields were shortened; the result is still valid JSON.
16
+
17
+ ## Reading structure: sections
18
+
19
+ `observe.read` returns only `text` by default. Ask for more with `sections`, any of `content`, `controls`, `links`, `tables`:
20
+
21
+ ```json call
22
+ {"action":"observe.read","args":{"sections":["controls"]}}
23
+ ```
24
+
25
+ Narrow it to part of the page with a `locator` (everything below that element is read; it must match exactly one element) or a viewport `region`, and bound it with `maxItems` (per section) and `maxBytes` (whole result):
26
+
27
+ ```json call
28
+ {"action":"observe.read","args":{"sections":["controls","tables"],"locator":{"role":"form"},"maxItems":20,"maxBytes":12000}}
29
+ ```
30
+
31
+ Each control record has `role`, `name`, `type`, a `locator`, the `actions` that can work now, the states it has (`visible`, `disabled`, `readonly`, `checked`, `expanded`), `options` for a select or radio group, declared `constraints` (`required`, `min`, `max`, `step`, `pattern`, `minlength`, `maxlength`, `accept`, `multiple`), the browser's own `validity`, and a `source` (`dom` or `aria`). The full field list: `{"action":"observe.read.controls"}` in `browser_index`.
32
+
33
+ - The `locator` was checked against the live page: it matches that element and no other. Pass it unchanged to `act.*`. Inside an iframe it carries `framePath`; open shadow DOM needs nothing extra.
34
+ - `ambiguous: true`: nothing unique exists (two identical unlabeled buttons, say). `matches` says how many elements the locator hits and `nth` which one this is; acting on it as is returns `LOCATOR_AMBIGUOUS`. Pin it with `index: nth` and an `indexReason`, or scope the read to its container.
35
+ - A field that is missing was not observed or does not apply (a range input has no `readonly`); `false` always means observed and not so. An empty list is an answer: `options: []` is a select with no options, `rows: []` with `totalRows: 0` an empty table, `controls: []` a page without controls. A scope that does not exist is `LOCATOR_NOT_FOUND`.
36
+ - Values are not returned. `hasValue` says whether a field is empty. With `includeValues: true` plain fields return `value`, but never password, hidden, file, or token-like fields: those only carry `hasValue` and `sensitive: true`.
37
+ - `frames` lists the iframes searched. A cross-origin or sandboxed one has `crossOrigin: true` and its content is not observed, and `limits` says so; you can still reach into it with `framePath` if you know the element. Closed shadow roots cannot be read.
38
+ - `links` have `text`, absolute `href` (a secret-looking query value such as a token shows as `[redacted]`), and a `locator`. `tables` have `headers`, the first `maxItems` `rows`, `totalRows`, and `coverage`: `partial` (with a `reason`) means the page has more rows than the DOM shows, because of a virtualized list or a pager; scroll or use the pager, then read again.
39
+ - Output is cut at item boundaries to fit `maxBytes` and is always valid JSON. `truncation: {omittedBySection, reason}` names what is missing and `counts` how many the page has. Ask for one section, raise the limits, or scope with a `locator`. Details: `{"action":"observe.read.truncation"}`.
40
+
41
+ ## Page generations
42
+
43
+ Every page state has a `generation`. It changes when the page's main frame navigates: a new page load, reload, history back or forward, `location` change, `history.pushState` or `replaceState`, a hash change. It does not change for a DOM re-render or for an iframe navigating on its own. Values only grow within your session and never repeat, so a `generation` cannot match a different page.
44
+
45
+ Pass the `generation` you observed as `expectGeneration` to an `act.*` call. If the page has moved on, the call fails with `TARGET_STALE` before doing anything, and `error.current` has the new `targetId` and `generation`: observe again and decide whether the page is still the one you meant.
46
+
47
+ ```json call
48
+ {"action":"act.click","args":{"locator":{"role":"button","name":"Create account","exact":true},"expectGeneration":3}}
49
+ ```
50
+
51
+ Recipes do not take it: they bind the page again on every run.
52
+
53
+ ## Screenshots
54
+
55
+ ```json call
56
+ {"action":"observe.screenshot","args":{"locator":{"role":"heading","name":"Pricing"}}}
57
+ ```
58
+
59
+ returns `{path}` inside `snapshotDir`. Without a locator it captures the full page. A `filename` must be a plain name ending in `.png`, `.jpg`, or `.jpeg`.
60
+
61
+ ## Console and failed requests
62
+
63
+ Capture is off unless the page was opened with it:
64
+
65
+ ```json call
66
+ {"action":"target.open","args":{"url":"https://example.com/","capture":["console","network"]}}
67
+ ```
68
+
69
+ Then `inspect.console` (filter with `level`) and `inspect.requests` return bounded, redacted records: console text, and failed or HTTP 4xx/5xx requests (method, url, status). No bodies or headers are recorded. Records reset on the next `target.open`.
70
+
71
+ ## Waiting
72
+
73
+ `act.wait` takes exactly one of: a `locator`/`selector` (with `state`), `urlPattern`, `networkIdle: true`, or `timeMs` (max 10000).
74
+
75
+ ## Several pages
76
+
77
+ Each session has its own context; other sessions cannot see it. A link with `target=_blank` or `window.open` adds a popup to your page list without making it active. `target.list` shows every page (`id`, `url`, `title`, `active`); switch with `target.select`, then `observe.read` and `act.*` work on that page.
78
+
79
+ ```json call
80
+ {"action":"target.list","args":{}}
81
+ ```
82
+
83
+ ```json call
84
+ {"action":"target.select","args":{"id":"t2"}}
85
+ ```
86
+
87
+ Ids are never reused in a session. A popup that closed gives `TARGET_CLOSED` on select; list again. If the active page closes, the session falls back to another open page of the same context. `target.close` discards all of the session's pages.
88
+
89
+ ## Typing and clearing
90
+
91
+ `act.fill` replaces a value at once (no key events). `act.type` presses the keys one by one, for autocompletes and masked inputs. `act.clear` empties a field and fires the input event, so controlled inputs update their state; do not fill an empty string to clear.
@@ -0,0 +1,25 @@
1
+ # OpenCLI site adapters
2
+
3
+ OpenCLI ships adapters for many platforms (for example Reddit, Xiaohongshu) that can be cheaper and more robust than driving pages by hand.
4
+
5
+ ## Flow
6
+
7
+ 1. `opencli.status` checks the daemon, extension, and Browser Bridge. Do not infer connectivity from `runtime.status` alone: it only shows the config switch.
8
+ 2. `opencli.catalog` finds exact command names. Filter instead of dumping everything.
9
+
10
+ ```json call
11
+ {"action":"opencli.catalog","args":{"site":"reddit","access":"read","limit":10}}
12
+ ```
13
+
14
+ 3. `opencli.run` runs one command with verbatim arguments after `opencli`.
15
+
16
+ ```json call
17
+ {"action":"opencli.run","args":{"args":["reddit","search","dsh","-f","json"]}}
18
+ ```
19
+
20
+ ## Notes
21
+
22
+ - `access` in the catalog is the adapter's own declaration, not a guarantee. Treat `write` commands (post, delete) as irreversible and expect an approval prompt.
23
+ - `opencli.run` can reuse the logged-in Chrome profile, so it asks for approval unless the user chose `unrestricted`.
24
+ - Pass `-f json` where supported so the output is easy to read.
25
+ - Chinese community sites usually need a domain-scoped AuthProfile configured by the user; if a command reports a login problem, tell the user rather than trying to log in.
@@ -0,0 +1,149 @@
1
+ # Recipes: bounded multi-step browser work
2
+
3
+ A recipe is 1 to 25 declarative steps run on the current page. Run one inline with `automation.run_recipe`, or save it as a draft with `automation.develop` so it can be tested, activated by the user, and reused with `automation.run`.
4
+
5
+ There are two schemas. **Write new recipes as v2** (`"schemaVersion":2`): locators are strict, so a step never silently acts on the wrong element. v1 (the default for an inline recipe without `schemaVersion`, and for every asset saved before v2) keeps its old behaviour: CSS `selector`, always the first match.
6
+
7
+ ## v2 steps
8
+
9
+ Each step has a `type` and a `locator` where it acts on an element. A locator is one of `{"role":"button","name":"Save"}` (optional `exact`), `{"label":"Query"}`, `{"text":"More"}`, `{"testId":"save"}`, `{"css":"#id"}`, plus optional `framePath` (iframe selectors, outermost first).
10
+
11
+ | type | fields |
12
+ |---|---|
13
+ | `wait` | `condition`: `locator` (with `locator`), `text` or `url` (with `value`), `load`, or `time` (with `waitMs`); optional `timeoutMs` |
14
+ | `click`, `hover`, `clear` | `locator` |
15
+ | `fill` | `locator`, `value` (non-empty; to set `""` write `allowEmpty: true`, or use `clear`) |
16
+ | `type` | `locator`, `value` (key by key) |
17
+ | `press` | `key`, optional `locator` |
18
+ | `select` | `locator`, `value` |
19
+ | `check` | `locator`, optional `checked` (false unchecks) |
20
+ | `scroll` | `deltaY`, `waitMs` |
21
+ | `goto` | `url`: absolute http(s), inside the recipe's domains (see below) |
22
+ | `extract` | optional `locator` (default: the page), `mode`: `text`, `html`, `links`, `attribute` (with `attribute`), `limit`, optional `as` (names the output) |
23
+ | `assert` | `locator`, `text`, or `urlIncludes` that must become true |
24
+ | `screenshot` | none |
25
+
26
+ `extract` and `assert` wait at most 5 s unless `timeoutMs` says otherwise, so a selector that is not there fails fast with `LOCATOR_NOT_FOUND`. Every other step waits 15 s. Read-only steps (`wait`, `goto`, `extract`, `assert`, `screenshot`) run without approval; anything else may ask the user.
27
+
28
+ ```json call
29
+ {"action":"automation.run_recipe","args":{"url":"https://example.com/","schemaVersion":2,"steps":[{"type":"click","locator":{"role":"link","name":"More information"}},{"type":"assert","urlIncludes":"iana.org"},{"type":"extract","locator":{"css":"main"},"as":"body"}],"postconditions":[{"output":"body","nonEmpty":true}]}}
30
+ ```
31
+
32
+ ### When a locator is ambiguous
33
+
34
+ A v2 locator must match exactly one element. If it matches several, the step is **not executed** (no click, no typing; `effects` stays as it was) and the reply is `LOCATOR_AMBIGUOUS` with `error.candidates`: `total` and the first five matches (`role`, `name`, `text`, `visible`). Fix the locator, do not guess:
35
+
36
+ 1. Make it specific: `exact: true`, a fuller `name`, a `label`, a `testId`, or a `framePath`.
37
+ 2. Only if the elements really are interchangeable, pick one with `"index": 1` **and** an `"indexReason": "why this one"`. An `index` without a reason is rejected.
38
+
39
+ A step that finds no element at all (`LOCATOR_NOT_FOUND`) did nothing either: `executionStatus` is `failed` and `effects` does not become `unknown`. Only a step that found its element, started, and then timed out is `outcome_unknown`.
40
+
41
+ ## Reading the result
42
+
43
+ A recipe never fails by throwing away what it did. `result` always holds:
44
+
45
+ | field | meaning |
46
+ |---|---|
47
+ | `executionStatus` | `completed`; `failed` (a step failed or the deadline passed); `cancelled`; `outcome_unknown` (a step that changes the page was being performed when it timed out) |
48
+ | `validationStatus` | `passed`: there is at least one `assert` or postcondition and every one held. `failed`: one did not hold. `not_checked`: nothing verified the result |
49
+ | `completedSteps` | the steps that finished: `step`, `action`, `ok`, and for extract/screenshot `output`, the index into `outputs` (the value is stored once, there) |
50
+ | `failedStep` | `index` (1-based), `action`, `errorCode`, the original `message`, and `candidates` for `LOCATOR_AMBIGUOUS`; for a cancel or deadline, the step that did not start. A failed postcondition is reported as a step after the last one with `action: "postcondition"` |
51
+ | `effects` | `none`: nothing changed. `observed`: fill, type, clear, click, press, select or check steps completed. `unknown`: one of those failed in flight, so it may have happened |
52
+ | `outputs` | values from extract and screenshot steps, each with `step`, `action`, `value`, and `name` when the extract had `as`; an existing but empty element gives `""`, a missing element is `LOCATOR_NOT_FOUND` |
53
+ | `legacyFallback` | only on stored v1 assets: an unknown extract `mode` ran as `links` |
54
+
55
+ `ok` is true only when `executionStatus` is `completed` and `validationStatus` is not `failed`. Next steps:
56
+
57
+ - `OUTCOME_UNKNOWN`: do not run the recipe again. Read the page (`observe.read`) to see whether the click, fill or submit took effect, then continue from the real state.
58
+ - `VALIDATION_FAILED`: the page did not show the expected result. Read it, then correct the recipe, the assert or the postcondition. Steps before the failure already ran; do not repeat a submit.
59
+ - `failed` with `effects: observed`: the earlier steps are done. Resume from `failedStep.index`, not from the start.
60
+ - `failed` with `NOT_ACTIONABLE` and `effects: none`: the element was found but stayed disabled, hidden or moving until the timeout, so the step never ran. Wait for the page state it needs (`wait`), then retry.
61
+ - `cancelled`: the step that was running finished before the call returned; the page stays open and usable.
62
+
63
+ A test or run counts as passed only when it completed and nothing failed validation.
64
+
65
+ ## Verifying the result: assert and postconditions
66
+
67
+ `assert` steps check in the middle of a recipe; **postconditions** are checked after all steps and belong to the asset. Use exactly one kind per entry:
68
+
69
+ - `{"selector":"#done"}`: a CSS selector that is visible. `{"text":"Saved"}`: text that is visible. `{"urlIncludes":"/inbox"}`: the final URL contains it. Each waits up to `timeoutMs` (default 5000).
70
+ - `{"output":"results","nonEmpty":true}`: the named extract output is not empty. `{"output":"results","allowEmpty":true}`: it only has to have been produced (an empty result is legitimate).
71
+
72
+ `validationStatus` is `passed` only when every `assert` and every postcondition holds. **A v2 draft with no `assert` and no postcondition can be saved, but its test cannot pass**: the reply is `ok: false` with `error.code: "VALIDATION_MISSING"` and `testStatus: "failed"`. Add a check that proves the business result (confirmation text, new URL, a non-empty output).
73
+
74
+ ## Inputs and outputs
75
+
76
+ `{{name}}` in any string of a step or postcondition becomes an input. For v2, declare each one in `inputSchema` and the caller's values are checked and converted before the browser is touched:
77
+
78
+ ```json call
79
+ {"action":"automation.develop","args":{"action":"save","kind":"recipe","schemaVersion":2,"name":"Catalog apply","domains":["example.com"],"recipe":[{"type":"fill","locator":{"label":"Limit"},"value":"{{limit}}"},{"type":"select","locator":{"label":"Sort"},"value":"{{sort}}"},{"type":"click","locator":{"role":"button","name":"Apply"}},{"type":"extract","locator":{"css":"#rows"},"as":"rows"}],"inputSchema":[{"name":"limit","type":"number","example":"3"},{"name":"sort","type":"enum","enumValues":["new","top"]}],"outputSchema":[{"name":"rows","type":"string"}],"postconditions":[{"text":"Sorted by {{sort}}, limit {{limit}}"},{"output":"rows","nonEmpty":true}]}}
80
+ ```
81
+
82
+ - `type`: `string`, `number` (a number or numeric text; stored canonically), or `enum` (must be one of `enumValues`). `required` defaults to true; an omitted optional input becomes an empty string. Wrong or missing inputs fail with `INVALID_ARGS` before anything runs.
83
+ - `outputSchema` names and types the outputs: each entry needs an `extract` with the same `as`. `number` parses the text (thousands commas allowed), `json` parses JSON (use it with `links`); a value that does not convert fails validation. `"dedupe":true` (string or json) removes repeated lines or elements, keeping the first of each, before the postconditions are checked.
84
+ - `requiredCapabilities` is recorded for later; it is not enforced.
85
+ - `automation.search` shows a v2 asset's `inputSchema`, so you can call `automation.run` without opening the recipe.
86
+
87
+ ## goto
88
+
89
+ `goto` navigates the recipe's page. For a saved asset the URL must be inside its `domains` (the host or a subdomain), checked when saving and again when running, including where a redirect lands. For an inline recipe it may stay on the origin the recipe started on, or go to hosts listed in `allowedDomains`. A URL outside that fails with `POLICY_DENIED` before any step runs.
90
+
91
+ ```json call
92
+ {"action":"automation.develop","args":{"action":"save","kind":"recipe","schemaVersion":2,"name":"Reset filter","domains":["example.com"],"recipe":[{"type":"goto","url":"https://example.com/search"},{"type":"clear","locator":{"label":"Filter"}},{"type":"assert","text":"All items"}]}}
93
+ ```
94
+
95
+ ## Saving a draft
96
+
97
+ Save (`automation.develop`, `save`) replaces the whole draft, so send every field each time; `browser_index({action:"automation.develop.save"})` has its schema. Then `{"action":"validate","id":...}` and `{"action":"test","id":...,"url":...,"inputs":{...}}`. Test with realistic inputs, and when the recipe takes any, with `inputSets` (2 to 5 different inputs, each in a fresh context; `references/develop.md`), checking that the outputs differ. A draft explored with `browser_call` is built with `draft_from_journal` instead of by hand (`references/develop.md`). Activation is the user's decision. Do not put page content, cookies or passwords into an asset.
98
+
99
+ **Revisions.** Every save makes a new `revision` (with a `contentHash` over the steps and schema fields) and clears the test result. A test is recorded as a credential for the revision and content it ran on, with only a digest of the inputs; the test reply carries `revision` and `contentHash`. After any edit, test again. The user can activate only a revision whose latest test passed on exactly that content.
100
+
101
+ ## Repairing an active asset
102
+
103
+ An active asset cannot be edited. When it stops working (the page changed), copy it, change the copy, test the copy:
104
+
105
+ ```json call
106
+ {"action":"automation.develop","args":{"action":"fork","id":"active-asset-id"}}
107
+ ```
108
+
109
+ The draft records `sourceAssetId` and `sourceRevision`. Failing tests of the draft never touch the active asset, which keeps running (`automation.run`). Once the draft passes with realistic inputs, tell the user it is ready; their activation archives the old asset and makes the draft the active one.
110
+
111
+ ## Converting a v1 asset
112
+
113
+ ```json call
114
+ {"action":"automation.develop","args":{"action":"convert","id":"v1-asset-id"}}
115
+ ```
116
+
117
+ creates a **new** v2 draft and leaves the original (even if active) exactly as it was; the draft records `sourceAssetId` and `sourceRevision`. Every CSS selector becomes `{"css": ...}` marked `explicitFirst`: it still takes the first match, as in v1, and is listed in `pendingDisambiguation`. To finish the migration: edit each listed step to a locator that matches one element (or an `index` with a reason), add an `assert` or a postcondition, save, and test. The `notes` in the reply list what else changed (shorter default timeouts, string inputs). An unknown `extract` mode or `wait` condition in the source refuses the conversion and says which step.
118
+
119
+ ## v1 steps (existing assets)
120
+
121
+ A v1 step has a `type` and the fields it needs. Selectors are CSS and always use the first match.
122
+
123
+ | type | fields |
124
+ |---|---|
125
+ | `wait` | `condition`: `selector` or `text` (with `value`), `load`, or `time` (with `waitMs`); optional `timeoutMs` |
126
+ | `click` | `selector` |
127
+ | `fill`, `type` | `selector`, `value` (`fill` replaces the value; `type` presses keys) |
128
+ | `press` | `key`, optional `selector` |
129
+ | `select` | `selector`, `value` |
130
+ | `check` | `selector`, optional `checked` (false unchecks) |
131
+ | `hover` | `selector` |
132
+ | `scroll` | `deltaY`, `waitMs` |
133
+ | `extract` | optional `selector`, `mode`: `text`, `html`, `links`, `attribute` (with `attribute`), `limit` |
134
+ | `assert` | `selector` or `text` that must become visible |
135
+ | `screenshot` | none |
136
+
137
+ ```json call
138
+ {"action":"automation.run_recipe","args":{"url":"https://example.com/","steps":[{"type":"wait","condition":"selector","value":"main"},{"type":"extract","selector":"main","mode":"text","limit":20}]}}
139
+ ```
140
+
141
+ A v1 recipe with no `assert` still passes a test, but is stored as `legacy-unverified` (`evidenceLevel`): add an `assert` that proves the business result to get `verified`.
142
+
143
+ ## Known limits
144
+
145
+ - A step that fails stops the recipe. Earlier steps have already run and are not rolled back, so do not retry a recipe that already submitted something without checking the page.
146
+ - A step that is already running cannot be interrupted. Cancel and deadline take effect between steps, so a long `wait` or a slow click delays the return by up to its own timeout.
147
+ - `extract.mode` must be `text`, `html`, `links` or `attribute`, and a `wait` condition one of those listed above; other values are rejected when a recipe is run inline or saved.
148
+ - A recipe passing a test with no `assert` (v1) means the steps ran, not that the business result was correct.
149
+ - An inline v2 recipe is not offered as a reusable candidate; save it as a draft if it should be reused.
@@ -0,0 +1,44 @@
1
+ # Userscripts and built-in scripts
2
+
3
+ For extraction that is awkward through clicking, run JavaScript in a fresh page context.
4
+
5
+ ## Built-in scripts first
6
+
7
+ They are trusted and read-only. `script.catalog` lists them (`article-clean`, `links`, `jsonld`, `forms`).
8
+
9
+ ```json call
10
+ {"action":"script.run_builtin","args":{"url":"https://example.com/","scriptId":"links"}}
11
+ ```
12
+
13
+ ## Your own userscript
14
+
15
+ Format: a Tampermonkey-style metadata block, then a function body that returns a JSON-serializable value. Only `@grant none` is supported (no `GM_*` APIs, no `@require`), and `@match` must cover the target URL. Declared inputs arrive in `__DSH_INPUTS__`.
16
+
17
+ ```
18
+ // ==UserScript==
19
+ // @name Read Heading
20
+ // @match https://example.com/*
21
+ // @grant none
22
+ // ==/UserScript==
23
+ return { heading: document.querySelector('h1')?.textContent || '', input: __DSH_INPUTS__.query || '' }
24
+ ```
25
+
26
+ Validate before running; validation does not execute anything and reports the hash and capabilities.
27
+
28
+ ```json call
29
+ {"action":"script.validate","args":{"source":"// ==UserScript==\n// @name Read Heading\n// @match https://example.com/*\n// @grant none\n// ==/UserScript==\nreturn document.title","url":"https://example.com/"}}
30
+ ```
31
+
32
+ ```json call
33
+ {"action":"script.run_userscript","args":{"url":"https://example.com/","source":"// ==UserScript==\n// @name Read Heading\n// @match https://example.com/*\n// @grant none\n// ==/UserScript==\nreturn document.title"}}
34
+ ```
35
+
36
+ Running external code asks the user for approval unless the mode is `unrestricted`. If the page redirects outside the `@match`, the run is rejected.
37
+
38
+ ## One-off expressions
39
+
40
+ `script.evaluate` runs a single JavaScript expression (up to 20,000 characters) on the current page with its origin and login state, so it can read cookies and storage that are not HttpOnly. Use it sparingly; it also asks for approval.
41
+
42
+ ```json call
43
+ {"action":"script.evaluate","args":{"expression":"document.title"}}
44
+ ```
package/cordis.patch.yml CHANGED
@@ -8,6 +8,10 @@
8
8
  # DSH 0.1.7-rc.2 persists editable config in a separate top-level profile row;
9
9
  # placing config inside insert can make settings writes appear successful but
10
10
  # disappear on restart. Config schema defaults supply the initial values.
11
+ #
12
+ # Model-facing text (tool descriptions, root guide/note, summaries, error hints, skill) can be
13
+ # overridden under `prompts` in that config row; the settings card's "Export default text" (or `pnpm prompts:dump` in a
14
+ # repository checkout) prints every default in the exact structure. See README "自定义提示文本".
11
15
  - insert:
12
16
  - id: browser
13
17
  name: '@anweat/dsh-browser'
@@ -0,0 +1,3 @@
1
+ /** `act` group: interact with the page. @module dsh-browser/actions/act */
2
+ import type { ActionDef } from './types.ts';
3
+ export declare const ACT_ACTIONS: ActionDef[];