kimi-webbridge-dsh 1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/lib/index.js +357 -0
  4. package/package.json +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kimi-webbridge-dsh contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # kimi-webbridge-dsh
2
+
3
+ Kimi WebBridge 的 dsh 插件:通过本机 kimi-webbridge 守护进程(`127.0.0.1:10086`)驱动用户的真实浏览器(保留登录态)。
4
+
5
+ 注册一个模型工具 `kimi_webbridge`,覆盖导航、点击、输入、读取页面(accessibility tree + @e 引用)、截图、JS 求值、CDP、网络、上传、PDF 导出、标签页与会话管理。
6
+
7
+ ## 依赖
8
+
9
+ - Kimi WebBridge 守护进程已安装并运行(`irm https://cdn.kimi.com/webbridge/install.ps1 | iex`)
10
+ - 浏览器扩展已连接(`kimi-webbridge status` 显示 `extension_connected: true`)
11
+
12
+ ## 安装(一次性)
13
+
14
+ **方式 A:从 npm registry 安装(发布后)**
15
+
16
+ ```powershell
17
+ dsh plugin --profile web add kimi-webbridge-dsh
18
+ ```
19
+
20
+ **方式 B:本地源码安装(开发时)**
21
+
22
+ ```powershell
23
+ dsh plugin --profile web add E:\deepseek\kimi-webbridge-dsh
24
+ ```
25
+
26
+ 两种方式安装后,都需要把下面这段追加到 `~/.dsh/profiles/web/cordis.patch.yml`(用户补丁层,热重载生效,无需重启服务):
27
+
28
+ ```yaml
29
+ - insert:
30
+ - id: kimi-webbridge
31
+ name: kimi-webbridge-dsh
32
+ ```
33
+
34
+ ## 用法
35
+
36
+ 模型在新会话中可直接调用 `kimi_webbridge` 工具。核心规则:
37
+
38
+ - **一个任务 = 一个 session = 一个标签页组**:任务开始时选一个 session 名,之后每次调用都带上。
39
+ - **读页面用 snapshot**:返回 accessibility tree 和 `@e` 引用,点击/输入优先用 `@e` 引用而非手写 CSS。
40
+ - **截图返回文件路径**:用 read 工具打开查看。
41
+ - 守护进程不可达时工具会自动尝试启动它;若二进制缺失,会自动从官方 CDN(cdn.kimi.com,与官方安装器同源)下载并安装——**用户无需手动运行任何外部安装命令**。
42
+
43
+ ## 配置(可选)
44
+
45
+ 在 `cordis.patch.yml` 的插入行里加 `config:`:
46
+
47
+ ```yaml
48
+ - insert:
49
+ - id: kimi-webbridge
50
+ name: kimi-webbridge-dsh
51
+ config:
52
+ baseUrl: http://127.0.0.1:10086
53
+ requestTimeoutMs: 120000
54
+ autoStartDaemon: true
55
+ autoInstallDaemon: true
56
+ daemonVersion: latest
57
+ ```
58
+
59
+ | 配置项 | 默认值 | 说明 |
60
+ |---|---|---|
61
+ | `baseUrl` | `http://127.0.0.1:10086` | 守护进程地址 |
62
+ | `requestTimeoutMs` | `120000` | 单次请求超时(毫秒) |
63
+ | `autoStartDaemon` | `true` | 守护进程未运行时自动启动 |
64
+ | `autoInstallDaemon` | `true` | 二进制缺失时自动从官方 CDN 下载安装(关闭后需手动运行 `irm https://cdn.kimi.com/webbridge/install.ps1 \| iex`) |
65
+ | `daemonVersion` | `latest` | 自动安装时的版本(可固定如 `0.3.0`) |
66
+
67
+ ## 移除
68
+
69
+ ```powershell
70
+ dsh plugin --profile web remove kimi-webbridge-dsh
71
+ ```
72
+ 并把 `cordis.patch.yml` 里的插入行删掉。
package/lib/index.js ADDED
@@ -0,0 +1,357 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
3
+ import { HarnessError } from "@deepseek-ai/dsh-llm";
4
+ import { spawn } from "node:child_process";
5
+ import { createWriteStream, existsSync } from "node:fs";
6
+ import { chmod, mkdir, rename } from "node:fs/promises";
7
+ import { request as httpRequest } from "node:http";
8
+ import { request as httpsRequest } from "node:https";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+
12
+ /**
13
+ * Kimi WebBridge plugin for dsh.
14
+ *
15
+ * Registers one model-facing tool `kimi_webbridge` that drives the user's real
16
+ * browser (their login sessions) through the local kimi-webbridge daemon at
17
+ * `127.0.0.1:10086`.
18
+ *
19
+ * The plugin is self-sufficient: if the daemon binary is missing it is
20
+ * downloaded from the official Kimi CDN (the same source the official
21
+ * installer uses) and started automatically, so no external installer
22
+ * command is required. A browser extension must still be connected for
23
+ * browser actions to work.
24
+ *
25
+ * All requests go straight to the daemon over HTTP, so there are none of the
26
+ * shell-quoting / non-ASCII corruption issues of curl-based integration.
27
+ *
28
+ * @module kimi-webbridge-dsh
29
+ */
30
+
31
+ /** Cordis plugin name used by loader diagnostics. */
32
+ const name = "kimi-webbridge";
33
+
34
+ /** Services required by this plugin. */
35
+ const inject = ["tools", "systemPrompt"];
36
+
37
+ /** Every action the daemon accepts on POST /command, plus local "status". */
38
+ const ACTIONS = [
39
+ "status",
40
+ "navigate",
41
+ "find_tab",
42
+ "snapshot",
43
+ "click",
44
+ "fill",
45
+ "evaluate",
46
+ "cdp",
47
+ "screenshot",
48
+ "network",
49
+ "upload",
50
+ "save_as_pdf",
51
+ "list_tabs",
52
+ "close_tab",
53
+ "close_session"
54
+ ];
55
+
56
+ /** Runtime configuration schema for the plugin. */
57
+ const Config = z.object({
58
+ baseUrl: z.string().default("http://127.0.0.1:10086"),
59
+ requestTimeoutMs: z.number().step(1).min(1000).default(120000),
60
+ autoStartDaemon: z.boolean().default(true),
61
+ autoInstallDaemon: z.boolean().default(true),
62
+ daemonVersion: z.string().default("latest")
63
+ });
64
+
65
+ /** Official release CDN base. */
66
+ const CDN_BASE = "https://cdn.kimi.com/webbridge";
67
+
68
+ /**
69
+ * Absolute path of the kimi-webbridge daemon binary under the user home.
70
+ * Windows: ~/.kimi-webbridge/bin/kimi-webbridge.exe; elsewhere: ~/.kimi-webbridge/bin/kimi-webbridge.
71
+ */
72
+ function daemonBinPath() {
73
+ const dir = join(homedir(), ".kimi-webbridge", "bin");
74
+ return join(dir, process.platform === "win32" ? "kimi-webbridge.exe" : "kimi-webbridge");
75
+ }
76
+
77
+ /**
78
+ * Map the current Node platform/arch to the official release filename.
79
+ * Mirrors the official install.sh (darwin/linux) and install.ps1 (windows).
80
+ * @returns the release filename, or null when unsupported.
81
+ */
82
+ function releasePlatform() {
83
+ const p = process.platform;
84
+ const a = process.arch;
85
+ if (p === "win32" && a === "x64") return "windows-amd64.exe";
86
+ if (p === "darwin" && a === "arm64") return "darwin-arm64";
87
+ if (p === "darwin" && a === "x64") return "darwin-amd64";
88
+ if (p === "linux" && a === "arm64") return "linux-arm64";
89
+ if (p === "linux" && a === "x64") return "linux-amd64";
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * One HTTP JSON round-trip with a timeout and caller-abort support.
95
+ * @param url - absolute URL.
96
+ * @param options - method, optional JSON body, timeout, and AbortSignal.
97
+ * @returns the parsed JSON body (or raw text fallback) plus the status code.
98
+ */
99
+ function httpJson(url, { method = "GET", body, timeoutMs, signal } = {}) {
100
+ return new Promise((resolve, reject) => {
101
+ let target;
102
+ try {
103
+ target = new URL(url);
104
+ } catch (error) {
105
+ reject(new Error(`kimi-webbridge: invalid baseUrl ${url}: ${String(error)}`));
106
+ return;
107
+ }
108
+ const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
109
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
110
+ let req;
111
+ let timer;
112
+ const abort = (error) => {
113
+ clearTimeout(timer);
114
+ if (req) req.destroy(error);
115
+ else reject(error);
116
+ };
117
+ timer = setTimeout(() => abort(new Error(`kimi-webbridge: request timed out after ${timeoutMs}ms`)), timeoutMs);
118
+ const onSignal = () => abort(new Error("aborted"));
119
+ if (signal?.aborted) onSignal();
120
+ else signal?.addEventListener("abort", onSignal, { once: true });
121
+ req = transport(target, {
122
+ method,
123
+ headers: payload !== void 0 ? { "Content-Type": "application/json" } : {}
124
+ }, (res) => {
125
+ const chunks = [];
126
+ res.on("data", (chunk) => chunks.push(chunk));
127
+ res.on("end", () => {
128
+ clearTimeout(timer);
129
+ signal?.removeEventListener("abort", onSignal);
130
+ const text = Buffer.concat(chunks).toString("utf8");
131
+ let parsed;
132
+ try {
133
+ parsed = text.length > 0 ? JSON.parse(text) : null;
134
+ } catch {
135
+ parsed = { raw: text };
136
+ }
137
+ resolve({ status: res.statusCode ?? 0, body: parsed, text });
138
+ });
139
+ });
140
+ req.on("error", (error) => {
141
+ clearTimeout(timer);
142
+ signal?.removeEventListener("abort", onSignal);
143
+ reject(error);
144
+ });
145
+ if (payload !== void 0) req.write(payload);
146
+ req.end();
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Download a file to disk, following up to maxRedirects redirects.
152
+ * @param url - absolute download URL.
153
+ * @param dest - destination file path.
154
+ * @param options - per-request timeout and redirect budget.
155
+ * @returns the destination path on success.
156
+ */
157
+ function downloadFile(url, dest, { timeoutMs, maxRedirects = 3 } = {}) {
158
+ return new Promise((resolve, reject) => {
159
+ const fetch = (targetUrl, depth) => {
160
+ let target;
161
+ try {
162
+ target = new URL(targetUrl);
163
+ } catch (error) {
164
+ reject(new Error(`kimi-webbridge: invalid download URL ${targetUrl}: ${String(error)}`));
165
+ return;
166
+ }
167
+ const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
168
+ const req = transport(target, { method: "GET" }, (res) => {
169
+ const status = res.statusCode ?? 0;
170
+ const location = res.headers.location;
171
+ if (status >= 300 && status < 400 && location !== void 0 && depth > 0) {
172
+ res.resume();
173
+ fetch(new URL(location, target).toString(), depth - 1);
174
+ return;
175
+ }
176
+ if (status < 200 || status >= 300) {
177
+ res.resume();
178
+ reject(new Error(`download failed: HTTP ${status} for ${targetUrl}`));
179
+ return;
180
+ }
181
+ const file = createWriteStream(dest);
182
+ res.pipe(file);
183
+ file.on("finish", () => file.close(() => resolve(dest)));
184
+ file.on("error", reject);
185
+ res.on("error", reject);
186
+ });
187
+ req.on("error", reject);
188
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`download timed out after ${timeoutMs}ms`)));
189
+ req.end();
190
+ };
191
+ fetch(url, maxRedirects);
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Download the daemon binary from the official CDN into ~/.kimi-webbridge/bin.
197
+ * The URL layout mirrors the official installer: {CDN}/{version}/releases/kimi-webbridge-{platform}.
198
+ * @returns { bin, url } on success.
199
+ */
200
+ async function installDaemonBinary({ version, timeoutMs }) {
201
+ const platform = releasePlatform();
202
+ if (platform === null) {
203
+ throw new Error(`unsupported platform ${process.platform}-${process.arch} — install the daemon manually (see https://www.kimi.com/zh-cn/features/webbridge)`);
204
+ }
205
+ const bin = daemonBinPath();
206
+ await mkdir(dirname(bin), { recursive: true });
207
+ const url = `${CDN_BASE}/${encodeURIComponent(version)}/releases/kimi-webbridge-${platform}`;
208
+ const tmp = `${bin}.tmp-${process.pid}-${Date.now()}`;
209
+ await downloadFile(url, tmp, { timeoutMs });
210
+ await rename(tmp, bin);
211
+ if (process.platform !== "win32") await chmod(bin, 0o755);
212
+ return { bin, url };
213
+ }
214
+
215
+ /** Start the daemon detached (idempotent — no-ops when port 10086 is already bound). */
216
+ function startDaemonProcess() {
217
+ return new Promise((resolve) => {
218
+ const child = spawn(daemonBinPath(), ["start"], {
219
+ detached: true,
220
+ stdio: "ignore",
221
+ windowsHide: true
222
+ });
223
+ child.on("error", () => resolve(false));
224
+ child.unref();
225
+ // Give the daemon a moment to bind the port before retrying.
226
+ setTimeout(() => resolve(true), 1500);
227
+ });
228
+ }
229
+
230
+ /** Turn a thrown error into the canonical aborted HarnessError. */
231
+ function abortError() {
232
+ const error = new HarnessError("tool call aborted", TOOL_ABORTED);
233
+ error.name = "AbortError";
234
+ return error;
235
+ }
236
+
237
+ /** Shape one daemon response into the canonical tool value. */
238
+ function canonicalResult(result) {
239
+ if (result.status >= 200 && result.status < 300) {
240
+ const body = result.body;
241
+ return { ok: true, ...(body !== null && typeof body === "object" ? body : { value: body }) };
242
+ }
243
+ const body = result.body;
244
+ return {
245
+ ok: false,
246
+ httpStatus: result.status,
247
+ ...(body !== null && typeof body === "object" ? body : { message: result.text || `HTTP ${result.status}` })
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Cordis plugin apply: register the kimi_webbridge tool and a short usage
253
+ * section in the system prompt.
254
+ */
255
+ function apply(ctx, config = {}) {
256
+ const { baseUrl, requestTimeoutMs, autoStartDaemon, autoInstallDaemon, daemonVersion } = config;
257
+
258
+ ctx.systemPrompt.section({
259
+ name: "tool:kimi-webbridge",
260
+ order: 105,
261
+ text: "kimi_webbridge drives the user's real browser through the local kimi-webbridge daemon. Use it for any web task that needs a real browser: navigation, reading pages (snapshot returns an accessibility tree with @e refs), clicking, filling forms, screenshots, JS evaluation, and PDF export. Keep one session name per task and pass it on every call so tabs group under one label."
262
+ });
263
+
264
+ ctx.tools.register(defineTool({
265
+ name: "kimi_webbridge",
266
+ description: "Drive the user's real browser (with their login sessions) via the local kimi-webbridge daemon at " + baseUrl + ". The browser extension must be connected (status shows extension_connected). One task = one session = one tab group: pick a session name at the start of a task and pass it on every call. Actions: status (daemon + extension health); navigate {url, newTab?, group_title?} (first call opens a tab); find_tab {url, active?} (re-select a session tab, or borrow the user's active tab); snapshot (accessibility tree with @e refs — use it to read content and locate elements); click {selector} (@e ref or CSS); fill {selector, value} (inputs, textareas, and contenteditable editors; clear-and-insert); evaluate {code} (async JS in the page realm; wrap re-declarations in an IIFE; return compact JSON); cdp {method, params} (raw chrome.debugger passthrough); screenshot {format?, quality?, selector?, path?} (returns a file path — open it with the read tool to see it); network {cmd, filter?, requestId?}; upload {selector, files}; save_as_pdf {paper_format?, landscape?, scale?, print_background?, path?}; list_tabs; close_tab; close_session (only when the user asks to close the tabs). Prefer @e refs from snapshot over hand-written CSS selectors. When the daemon is unreachable the plugin starts it automatically; if the daemon binary is missing it is downloaded from the official Kimi CDN (config autoInstallDaemon:false disables this).",
267
+ parameters: {
268
+ action: {
269
+ type: "string",
270
+ required: true,
271
+ enum: ACTIONS,
272
+ description: "Which WebBridge action to perform."
273
+ },
274
+ session: {
275
+ type: "string",
276
+ description: "Task session name — one session per task, passed on every call so all tabs land in one group (e.g. \"camping-research\"). Defaults to \"default\"."
277
+ },
278
+ args: {
279
+ type: "object",
280
+ additionalProperties: true,
281
+ description: "Action-specific arguments (see the tool description for each action's fields)."
282
+ }
283
+ },
284
+ output: {
285
+ schema: {
286
+ type: "object",
287
+ additionalProperties: true
288
+ },
289
+ render(args, value) {
290
+ return [{ type: "text", text: JSON.stringify(value) }];
291
+ }
292
+ },
293
+ isConcurrencySafe() {
294
+ return false;
295
+ },
296
+ async execute(args, exec) {
297
+ const action = args.action;
298
+ const session = args.session ?? "default";
299
+ const actionArgs = args.args ?? {};
300
+ const target = action === "status" ? `${baseUrl}/status` : `${baseUrl}/command`;
301
+ const attempt = () => action === "status"
302
+ ? httpJson(target, { method: "GET", timeoutMs: requestTimeoutMs, signal: exec.signal })
303
+ : httpJson(target, {
304
+ method: "POST",
305
+ body: { action, args: actionArgs, session },
306
+ timeoutMs: requestTimeoutMs,
307
+ signal: exec.signal
308
+ });
309
+ let result;
310
+ try {
311
+ result = await attempt();
312
+ } catch (error) {
313
+ const aborted = error?.message === "aborted" || error?.name === "AbortError";
314
+ if (aborted) throw abortError();
315
+ const refused = error?.code === "ECONNREFUSED" || /ECONNREFUSED/.test(String(error?.message ?? error));
316
+ if (!refused) {
317
+ throw new Error(`kimi-webbridge: cannot reach daemon at ${baseUrl} (${error?.message ?? error}).`);
318
+ }
319
+ // The daemon is down. Provision the binary when missing, start it, then retry once.
320
+ const notes = [];
321
+ if (!existsSync(daemonBinPath())) {
322
+ if (!autoInstallDaemon) {
323
+ throw new Error(`kimi-webbridge: daemon binary not found at ${daemonBinPath()} and autoInstallDaemon is disabled. Install it with: irm https://cdn.kimi.com/webbridge/install.ps1 | iex`);
324
+ }
325
+ try {
326
+ const { url } = await installDaemonBinary({ version: daemonVersion, timeoutMs: requestTimeoutMs });
327
+ notes.push(`installed daemon from ${url}`);
328
+ } catch (installError) {
329
+ throw new Error(`kimi-webbridge: daemon binary missing and auto-install failed: ${installError?.message ?? installError}. Install it manually: irm https://cdn.kimi.com/webbridge/install.ps1 | iex`);
330
+ }
331
+ }
332
+ if (autoStartDaemon) {
333
+ await startDaemonProcess();
334
+ try {
335
+ result = await attempt();
336
+ } catch (retryError) {
337
+ const hint = notes.length > 0 ? " " + notes.join("; ") + "." : "";
338
+ throw new Error(`kimi-webbridge: daemon at ${baseUrl} still unreachable after starting it.${hint} Check ~/.kimi-webbridge/logs/daemon.log or reinstall: irm https://cdn.kimi.com/webbridge/install.ps1 | iex`);
339
+ }
340
+ } else {
341
+ throw new Error(`kimi-webbridge: daemon at ${baseUrl} is not running and autoStartDaemon is disabled. Start it with: ${daemonBinPath()} start`);
342
+ }
343
+ }
344
+ return canonicalResult(result);
345
+ },
346
+ presentCall(callArgs) {
347
+ return {
348
+ card: "generic",
349
+ title: `kimi_webbridge ${callArgs.action}`,
350
+ rawInput: JSON.stringify({ session: callArgs.session ?? "default", args: callArgs.args ?? {} }),
351
+ content: [{ type: "text", text: `action: ${callArgs.action}` }]
352
+ };
353
+ }
354
+ }));
355
+ }
356
+
357
+ export { Config, apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "kimi-webbridge-dsh",
3
+ "description": "Kimi WebBridge plugin for dsh: drive the user's real browser (their login sessions) through the local kimi-webbridge daemon at 127.0.0.1:10086. Registers a single kimi_webbridge tool covering navigate, click, fill, snapshot, screenshot, evaluate, cdp, network, upload, save_as_pdf, tab and session management.",
4
+ "version": "1.1.0",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "dsh",
9
+ "deepseek-harness",
10
+ "kimi",
11
+ "webbridge",
12
+ "browser",
13
+ "automation",
14
+ "plugin",
15
+ "cordis"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "main": "lib/index.js",
21
+ "exports": {
22
+ ".": {
23
+ "default": "./lib/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "README.md"
30
+ ],
31
+ "dependencies": {
32
+ "@deepseek-ai/schemastery": "^3.18.1",
33
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
34
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.7"
35
+ }
36
+ }