@free-lzj/dsh-browser-use 0.0.0-stage → 0.1.0

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 CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # @free-lzj/dsh-browser-use
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 用 `@` 提一个**你自己浏览器里已经开着的标签页**,然后让 agent 读它、点它、填它、上传下载、看控制台和网络。
4
+
5
+ 不是新开一个干净窗口,也不是截图:BrowserSkill 的扩展跑在你原来的浏览器配置里,带着你已有的登录态和 cookie。
6
+
7
+ 装这一个插件就够了——**不需要再装 `browser-skill` 技能**,它的能力、工具和操作规范都在包里。
8
+
9
+ ## 它给 dsh 添两样东西
10
+
11
+ **一、`@` 菜单里的「浏览器」组**
12
+
13
+ 列出你窗口里的所有标签页(当前激活的排前面,每行带站点与路径),选中插入原子提及:
14
+
15
+ ```text
16
+ @[AI场景设置](dsh-browser:299872373)
17
+ ```
18
+
19
+ **二、BrowserSkill 的全部工具**
20
+
21
+ 以 `mcp__browser-skill__*` 前缀注册,六个工具覆盖它的完整动作集:
22
+
23
+ | 工具 | 能干什么 |
24
+ |---|---|
25
+ | `bsk_session` | 起 / 停会话,`sync` 拿当前页面的语义结构和 `context_id` |
26
+ | `bsk_page` | 导航、刷新、前进后退、等待导航或元素 |
27
+ | `bsk_inspect` | `observe` / `snapshot` / `get-html` / `console` / `network` / `extract` |
28
+ | `bsk_interact` | 点击、填表、按键、下拉、悬停、拖拽、滚轮、焦点、上传 |
29
+ | `bsk_tabs` | 列出 / 新建 / 选中 / 关闭 / 借用 / 归还标签页 |
30
+ | `bsk_assist` | 请求用户帮忙(登录、验证码、支付确认)、处理原生对话框 |
31
+
32
+ 还随包注册一个 `browser-use` 技能,讲清观察层次、引用失效、借用边界、换人接手的时机——这些原本是单独的 `browser-skill` 技能在教模型的事。
33
+
34
+ ## 装之前
35
+
36
+ **本机必须有 BrowserSkill 的 `bsk` 可执行文件,并且它连着你平常那个浏览器。**
37
+
38
+ ```sh
39
+ bsk status # 要能看到 browsers connected 1
40
+ ```
41
+
42
+ 插件不装扩展、不配浏览器;`bsk` 是它的运行时(见文末声明)。缺了 `bsk` 时插件照常加载,只是没有浏览器工具——`@` 提及和技能仍在。
43
+
44
+ ## 安装
45
+
46
+ ```sh
47
+ dsh plugin --profile web add @free-lzj/dsh-browser-use
48
+ ```
49
+
50
+ 或者从本仓库用 overlay 起一个第二实例看效果(不写进用户的 profile):
51
+
52
+ ```sh
53
+ pnpm run build
54
+ dsh --profile web --patch examples/browser-use.cordis.yml --port 0 --no-open
55
+ ```
56
+
57
+ ## 配置
58
+
59
+ 没有配置页,只有一个环境变量:
60
+
61
+ | 名字 | 默认 | 说明 |
62
+ |---|---|---|
63
+ | `DSH_BSK_PATH` | `bsk` | `bsk` 可执行文件。机器上有多个 `bsk`(旧的全局安装)时用它点明是哪一个 |
64
+
65
+ ## 怎么用
66
+
67
+ 1. 输入框打 `@`,在「浏览器」里挑一行(打字同时过滤标题和 URL)。
68
+ 2. 回车插入提及,然后说要干什么。
69
+ 3. agent 自己 `bsk_session` 起会话、`bsk_inspect` 观察、`bsk_interact` 操作。
70
+
71
+ 没 `@` 也能用:直接说"看看我浏览器里那个 Jira 页面",它会 `bsk_tabs` 列出来再挑。
72
+
73
+ ## 为什么不用官方的 browser-use provider
74
+
75
+ dsh 有一套 `ctx.browserUse` 单槽 provider 机制,官方两个实现(Chrome DevTools MCP、Playwright MCP)都走 `@deepseek-ai/dsh-experimental-browser-use-runtime` 的 `mountSessionMcp`。
76
+
77
+ 那个运行时包**不随 dsh 分发**,而且它的 `dsh-scope` / `dsh-mcp-client` 依赖一旦被装进 profile 就会和 dsh 自带的那份并存,作用域标记对不上,第二个 Agent 创建会失败。所以这里直接挂 `@deepseek-ai/dsh-mcp-client`(dsh 自带),代价是**整个 Host 一个 MCP 服务器**而不是每 Session 一个:
78
+
79
+ - 好处:任何会话都能用浏览器,资源也只有一份;
80
+ - 代价:两个会话同时驱动浏览器时共用同一个 BrowserSkill 会话(同一个 Agent Window)。一个人在一个对话里干活时没有区别。
81
+
82
+ ## 加一类新的可 @ 的东西
83
+
84
+ `@` 本身属于 dsh:`dsh-client-ui-input-trigger` 拥有菜单,`dsh-client-ui-reference` 往里注册了文件与对话。本包注册第二个 source,所以「加一类东西」在 Host 侧只意味着写一个 provider:
85
+
86
+ ```ts
87
+ export interface MentionProvider {
88
+ readonly id: string
89
+ readonly label: string
90
+ candidates(query: string, signal: AbortSignal): Promise<readonly MentionItem[]>
91
+ }
92
+ ```
93
+
94
+ `src/browser-mention.ts` 是唯一的实现,`src/index.ts` 里一行 `registry.register(...)`。路由、工具、浏览器半边都不认识「浏览器」这三个字。
95
+
96
+ ## 已知限制
97
+
98
+ - **`@` 分组标题只有中英两份**:将来的新 provider 没配翻译时用它在 Host 侧给的 `label`,不会空着,但也不跟界面语言切换。
99
+ - **图标借用 `session` 字形**:`ui-primitives` 里还没有专门的窗口图标。
100
+ - **不做上下文注入**:选中的标签页不会自动把内容塞进上下文,agent 得自己 `observe` 一次。
101
+ - **`bsk` 会话归 BrowserSkill 管**:插件的 `@` 侧自己维持一个常驻 bsk 会话用来列标签页,dsh 正常退出会停掉;进程被强杀时会留在 `bsk session list` 里,需要手动 `bsk session stop <id>`。
102
+ - **只列用户窗口**:agent 自己那个 Agent Window(`scope: agent`)不会出现在 `@` 里——它是工具,不是你想提的东西。
103
+
104
+ ## 第三方声明
105
+
106
+ **本包不包含任何第三方源代码。** `src/` 与 `tests/` 下的代码都是为这个插件新写的;dsh 那些 `@deepseek-ai/dsh-*` 包是 peer dependency(运行时提供,未复制)。
107
+
108
+ 它依赖和参考的外部东西是这些:
109
+
110
+ | 名称 | 关系 | 说明 |
111
+ |---|---|---|
112
+ | **BrowserSkill / `bsk`** | 运行时依赖,**未随包分发** | 用户本机自行安装的第三方 CLI。本包通过 `bsk mcp`(stdio)调用它,工具定义与工具描述都由它运行时提供,本包只转发。`bsk` 自身的许可需向其发布方确认,本仓库不为其授权背书 |
113
+ | `browser-skill` 技能 | 能力与操作规范的参考 | 本包的 `src/skill.ts` 是按 `bsk` 的公开 CLI 契约与这套技能的**行为规范**重写的说明文,不是其原文的复制;不随包含其文件 |
114
+ | MCP 协议 / Model Context Protocol | 协议 | 工具以标准 MCP 暴露,无代码内嵌 |
@@ -0,0 +1,11 @@
1
+ # dsh bundle patch: installing this package as a bundle makes `dsh plugin add`
2
+ # insert the one Loader row the browser capability needs, so it mounts on the
3
+ # next start.
4
+ #
5
+ # The id doubles as this package's route prefix (`/dsh-browser-use`) and as the
6
+ # locale namespace key the browser half registers. It must stay `browser-use`:
7
+ # the harness's own sidebar browser uses `browser`, and two Loader rows sharing
8
+ # an id are rejected by the plugin market's preflight.
9
+ - insert:
10
+ - id: browser-use
11
+ name: '@free-lzj/dsh-browser-use'
package/lib/client.js ADDED
@@ -0,0 +1,193 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@free-lzj/dsh-browser-use",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region lib/types/protocol.js
8
+ /**
9
+ * Shapes and routes both halves agree on.
10
+ *
11
+ * The browser half never imports the Host half's runtime values — only these
12
+ * types and the small amount of shared protocol they carry — so this module must
13
+ * stay free of `node:*` imports and of anything that only exists on one side.
14
+ *
15
+ * @module @free-lzj/dsh-browser-use/protocol
16
+ */
17
+ /** Where the browser half asks for mention candidates. */
18
+ const MENTIONS_PATH = "/dsh-browser-use/mentions";
19
+ //#endregion
20
+ //#region lib/types/client/locales.js
21
+ /**
22
+ * The `@` menu's copy for this plugin.
23
+ *
24
+ * Only the section heading needs translating: every row's text is the user's own
25
+ * page title, which no dictionary can hold.
26
+ *
27
+ * @module @free-lzj/dsh-browser-use/client/locales
28
+ */
29
+ /** Dictionary namespace owned by this plugin. */
30
+ const NS = "browserUse";
31
+ /** Simplified Chinese dictionary (the key-set source of truth). */
32
+ const zh = { sectionBrowser: "浏览器" };
33
+ /** English dictionary, key-identical to the Chinese source of truth. */
34
+ const en = { sectionBrowser: "Browser" };
35
+ //#endregion
36
+ //#region lib/types/client/index.js
37
+ /**
38
+ * Mentionable things in the `@` menu — browser half.
39
+ *
40
+ * The harness already owns `@`: `ui-input-trigger` renders the grouped menu and
41
+ * routes a pick to whichever source registered the row. Files and sessions come
42
+ * from `ui-reference`; this source adds everything that is neither, and the
43
+ * Host's provider registry decides what that is today.
44
+ *
45
+ * A pick inserts one atomic chip whose serialized form is the same
46
+ * `@[label](scheme:id)` mention the harness uses for a session. That is not
47
+ * decoration: the model reads the chip as text, so the mention has to carry the
48
+ * id the `browser_tab` tool can act on.
49
+ *
50
+ * @module @free-lzj/dsh-browser-use/client
51
+ */
52
+ /** Required services: the trigger registry and the copy. */
53
+ const inject = ["inputTriggers", "locale"];
54
+ /** Source name. Unique within `@`, and the serializer routing key of every chip this source inserts. */
55
+ const SOURCE_NAME = "mention";
56
+ /** Registration order against `ui-reference`; the file/session group stays first. */
57
+ const SOURCE_ORDER = 10;
58
+ /**
59
+ * The mention scheme a ref is rewritten to.
60
+ *
61
+ * `browser:299872133` becomes `dsh-browser:299872133` so every scheme this menu
62
+ * produces looks alike (`dsh-session:` already does), and nothing mistakes a tab
63
+ * id for a path.
64
+ * @param ref - provider-qualified ref, as the Host returned it.
65
+ * @returns the mention's own address.
66
+ */
67
+ function mentionAddress(ref) {
68
+ const at = ref.indexOf(":");
69
+ if (at < 0) return `dsh-${ref}`;
70
+ return `dsh-${ref.slice(0, at)}:${ref.slice(at + 1)}`;
71
+ }
72
+ /**
73
+ * Render one row's section heading.
74
+ * @param group - the provider's answer.
75
+ * @param t - the namespace's translator.
76
+ * @returns the translated heading when we have one, else the provider's own label.
77
+ */
78
+ function sectionLabel(group, t) {
79
+ return group.id === "browser" ? t("sectionBrowser") : group.label;
80
+ }
81
+ /**
82
+ * Ask the Host what can be mentioned.
83
+ * @param query - text after `@`.
84
+ * @param signal - the menu's cancellation.
85
+ * @returns every reachable group; an unreachable Host yields no rows rather than an error.
86
+ */
87
+ async function fetchGroups(query, signal) {
88
+ try {
89
+ const response = await fetch(`${MENTIONS_PATH}?q=${encodeURIComponent(query)}`, {
90
+ signal,
91
+ headers: { accept: "application/json" }
92
+ });
93
+ if (!response.ok) return [];
94
+ const body = await response.json();
95
+ return Array.isArray(body.groups) ? body.groups : [];
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+ /**
101
+ * One candidate row per mentionable thing.
102
+ * @param group - the provider's answer.
103
+ * @param section - rendered heading.
104
+ * @returns rows for the menu.
105
+ */
106
+ function rowsOf(group, section) {
107
+ return group.items.map((item) => {
108
+ const value = JSON.stringify({
109
+ ref: item.ref,
110
+ label: item.label
111
+ });
112
+ return {
113
+ name: item.ref,
114
+ label: item.label,
115
+ ...item.description === void 0 ? {} : { description: item.description },
116
+ icon: "session",
117
+ section,
118
+ value
119
+ };
120
+ });
121
+ }
122
+ /**
123
+ * Register the mention source.
124
+ * @param ctx - client root context.
125
+ */
126
+ function apply(ctx) {
127
+ ctx.effect(() => ctx.locale.register(NS, {
128
+ zh,
129
+ en
130
+ }), "browser-use: dictionaries");
131
+ const t = ctx.locale.bind(NS);
132
+ const inputTriggers = ctx.get("inputTriggers");
133
+ if (inputTriggers === void 0) {
134
+ ctx.logger?.warn?.("browser-use: no input trigger pipeline, @ mention not registered");
135
+ return;
136
+ }
137
+ const source = {
138
+ trigger: "@",
139
+ name: SOURCE_NAME,
140
+ order: SOURCE_ORDER,
141
+ showGroupTitle: false,
142
+ async candidates(_session, req) {
143
+ if (req.quoted === true) return [];
144
+ const groups = await fetchGroups(req.query, req.signal);
145
+ if (req.signal.aborted) return [];
146
+ return groups.flatMap((group) => rowsOf(group, sectionLabel(group, t)));
147
+ },
148
+ onPick({ candidate }) {
149
+ const value = parseValue(candidate.value);
150
+ if (value === void 0) return void 0;
151
+ const mention = `@[${value.label}](${mentionAddress(value.ref)})`;
152
+ return { insert: {
153
+ source: SOURCE_NAME,
154
+ ref: mention,
155
+ label: value.label,
156
+ appearance: "file",
157
+ clipboardText: mention
158
+ } };
159
+ },
160
+ codec: {
161
+ clipboardText: (ref) => ref,
162
+ serialize: async (ref, _signal) => ref
163
+ }
164
+ };
165
+ ctx.effect(() => inputTriggers.registerSource(source), "browser-use: @ source");
166
+ }
167
+ /**
168
+ * Read a row's pick payload.
169
+ * @param value - the row's opaque `value`.
170
+ * @returns the ref and label, or undefined when the row was not ours.
171
+ */
172
+ function parseValue(value) {
173
+ if (value === void 0) return void 0;
174
+ try {
175
+ const parsed = JSON.parse(value);
176
+ if (typeof parsed.ref !== "string" || typeof parsed.label !== "string") return void 0;
177
+ return {
178
+ ref: parsed.ref,
179
+ label: parsed.label
180
+ };
181
+ } catch {
182
+ return;
183
+ }
184
+ }
185
+ //#endregion
186
+ exports.apply = apply;
187
+ exports.inject = inject;
188
+ exports.mentionAddress = mentionAddress;
189
+ return module.exports;
190
+ }
191
+ });
192
+
193
+ //# sourceMappingURL=client.js.map