kimi-webbridge-dsh 1.4.6 → 1.4.8
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 +35 -16
- package/lib/index.js +178 -99
- package/package.json +5 -4
- package/skill.md +20 -8
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# kimi-webbridge-dsh
|
|
2
2
|
|
|
3
|
-
Kimi WebBridge
|
|
3
|
+
Kimi 浏览器扩展(**Kimi Browser Extension**,原 Kimi WebBridge)的 dsh 插件(**基础设施 + 技能注册**)。插件本身不代理浏览器操作,而是负责:
|
|
4
4
|
|
|
5
5
|
1. **保障桥完好**:守护进程二进制缺失时自动从官方 CDN 下载(跨平台、SHA-256 校验),守护进程未运行时自动拉起;
|
|
6
|
-
2. **注册 skill**:启动时把随包内置的 `skill.md` 注册为运行时 skill(`ctx.skills.register`,`source: bundled`),模型通过 `skill` 工具加载它,按文档用 HTTP 行使全部浏览器能力(
|
|
6
|
+
2. **注册 skill**:启动时把随包内置的 `skill.md` 注册为运行时 skill(`ctx.skills.register`,`source: bundled`),模型通过 `skill` 工具加载它,按文档用 HTTP 行使全部浏览器能力(25 个动作);
|
|
7
7
|
3. **注册一个瘦工具 `kimi_webbridge`**:只有 `status` 一个动作(守护进程 + 扩展健康检查),作为其他 skill(如 x-cli 系列)的桥健康锚点。
|
|
8
8
|
|
|
9
9
|
## 架构
|
|
@@ -11,7 +11,7 @@ Kimi WebBridge 的 dsh 插件(**基础设施 + 技能注册**)。插件本
|
|
|
11
11
|
| 层 | 角色 |
|
|
12
12
|
|---|---|
|
|
13
13
|
| 插件(本包) | 守护进程二进制供应 + 自动启动;apply() 时注册运行时 skill |
|
|
14
|
-
| skill(运行时注册,随包内置 `skill.md`) | 模型经 `skill` 工具加载,按文档驱动守护进程:navigate / find_tab / snapshot / click / mouse_click / fill / key_type / send_keys / evaluate / cdp / screenshot / network / upload / save_as_pdf / list_tabs / close_tab / close_session |
|
|
14
|
+
| skill(运行时注册,随包内置 `skill.md`) | 模型经 `skill` 工具加载,按文档驱动守护进程:navigate / find_tab / snapshot / read_page / find / click / mouse_click / hover / drag / fill / key_type / send_keys / select_option / dialog / evaluate / cdp / screenshot / network / upload / save_as_pdf / scroll / wait / list_tabs / close_tab / close_session |
|
|
15
15
|
| 瘦工具 `kimi_webbridge` | 仅 `status` 健康检查(自带守护进程自愈) |
|
|
16
16
|
|
|
17
17
|
## 安装(一次性,零手工配置)
|
|
@@ -30,20 +30,38 @@ dsh plugin --profile web add kimi-webbridge-dsh
|
|
|
30
30
|
|
|
31
31
|
## 依赖
|
|
32
32
|
|
|
33
|
-
- **浏览器扩展**:需安装并连接 Kimi
|
|
33
|
+
- **浏览器扩展**:需安装并连接 Kimi 浏览器扩展(Kimi Browser Extension,原 Kimi WebBridge)—— 从 [Chrome Web Store](https://chromewebstore.google.com/detail/kimi-webbridge/fldmhceldgbpfpkbgopacenieobmligc) 安装(`kimi_webbridge status` 显示 `extension_connected: true`;扩展版本应与守护进程一致)。
|
|
34
34
|
- **守护进程**:无需预装。插件在守护进程不可达时会自动启动它;二进制缺失时会自动从官方 CDN(cdn.kimi.com,与官方安装器同源)下载安装——全程无需手动运行任何安装命令。
|
|
35
35
|
|
|
36
|
+
### 版本对齐
|
|
37
|
+
|
|
38
|
+
插件按官方 CDN 的 `version.json` 供应守护进程,因此始终与官方发布对齐;上表的存档哈希对应 **v2.0.8**(守护进程与扩展的产品名已更名为 Kimi Browser Extension)。
|
|
39
|
+
|
|
40
|
+
`kimi_webbridge` 的 `status` 会原样透出守护进程的 `update_available`(有更新时)与 `version_mismatch`(守护进程与扩展不同步时)字段。按官方约定:这两个字段出现时**不要**自动执行 `upgrade` / `stop` / `restart` / `uninstall`(它们会杀掉正在运行的守护进程),而应把字段里的 `command` 原样交给用户执行。
|
|
41
|
+
|
|
42
|
+
### 兼容性(dsh 版本)
|
|
43
|
+
|
|
44
|
+
插件只 import 宿主提供的 `@deepseek-ai/*` 包,并声明为 `peerDependencies`(不打包自己的副本),所以与 dsh 版本解耦;已在 **dsh 0.1.5-rc.1** 上验证(工具注册、skill 注册、system prompt 注入、守护进程自愈均正常)。
|
|
45
|
+
|
|
46
|
+
| peer | 声明范围 |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `@deepseek-ai/dsh-tools` | `^0.1.0-rc.7 \|\| ^0.1.1-rc.2 \|\| ^0.1.2-rc.1 \|\| ^0.1.5-rc.1` |
|
|
49
|
+
| `@deepseek-ai/dsh-llm` | 同上 |
|
|
50
|
+
| `@deepseek-ai/schemastery` | `^3.18.1` |
|
|
51
|
+
|
|
52
|
+
⚠️ **预发布版必须逐个元组写进范围。** node-semver 只在范围里存在一个**与该版本 `major.minor.patch` 完全一致、且自身带预发布标签**的比较符时才放行预发布版;因此看起来很宽的 `>=0.0.1-rc.1 <0.2.0` 会静默排除所有预发布构建,用户会遇到 `ERESOLVE`。dsh 发布新的预发布系列(如 `0.1.6-rc.1`)时,需要同步追加一个 `|| ^<新元组>-rc.N` 分支。
|
|
53
|
+
|
|
36
54
|
### 跨平台支持
|
|
37
55
|
|
|
38
56
|
插件根据运行时硬件自动选择并下载对应的守护进程二进制(含 SHA-256 校验,与官方清单核对后才安装):
|
|
39
57
|
|
|
40
|
-
| 平台 | 二进制 | 存档哈希(
|
|
58
|
+
| 平台 | 二进制 | 存档哈希(v2.0.8) |
|
|
41
59
|
|---|---|---|
|
|
42
|
-
| Windows x64 | `windows-amd64.exe` | `
|
|
43
|
-
| macOS arm64 | `darwin-arm64` | `
|
|
44
|
-
| macOS x64 | `darwin-amd64` | `
|
|
45
|
-
| Linux arm64 | `linux-arm64` | `
|
|
46
|
-
| Linux x64 | `linux-amd64` | `
|
|
60
|
+
| Windows x64 | `windows-amd64.exe` | `eec1976d…433c6` |
|
|
61
|
+
| macOS arm64 | `darwin-arm64` | `04532d77…0a261` |
|
|
62
|
+
| macOS x64 | `darwin-amd64` | `931769e9…8cc0c` |
|
|
63
|
+
| Linux arm64 | `linux-arm64` | `ee201b44…34a3c` |
|
|
64
|
+
| Linux x64 | `linux-amd64` | `61de010c…ce271` |
|
|
47
65
|
|
|
48
66
|
不支持的平台组合会明确报错并提示手动安装。
|
|
49
67
|
|
|
@@ -58,16 +76,17 @@ dsh plugin --profile web add kimi-webbridge-dsh
|
|
|
58
76
|
|
|
59
77
|
下载流程:先落盘为临时文件(`kimi-webbridge(.exe).tmp-{pid}-{时间戳}`)→ SHA-256 与官方清单核对 → 匹配才重命名为最终路径;不匹配则删除临时文件并中止,任何未验证的二进制都不会留在磁盘。该路径与官方安装器 / `kimi-webbridge upgrade` 命令完全一致,插件、官方工具共用同一份守护进程。
|
|
60
78
|
|
|
61
|
-
## 功能(skill 承载,
|
|
79
|
+
## 功能(skill 承载,25 个动作)
|
|
62
80
|
|
|
63
81
|
| 能力 | 动作 |
|
|
64
82
|
|---|---|
|
|
65
83
|
| 导航 / 标签 | `navigate`、`find_tab`、`list_tabs`、`close_tab`、`close_session` |
|
|
66
|
-
| 读页面 | `snapshot`(accessibility tree + `@e`
|
|
67
|
-
| 点击 / 输入 | `click`、`mouse_click`(真实鼠标,可信输入)、`fill`、`key_type`(键入,可信)、`send_keys
|
|
84
|
+
| 读页面 | `snapshot`(accessibility tree + `@e` 引用)、`read_page`(全文文本)、`find`(页面内搜索) |
|
|
85
|
+
| 点击 / 输入 | `click`、`mouse_click`(真实鼠标,可信输入)、`hover`、`drag`、`fill`、`key_type`(键入,可信)、`send_keys`(组合键)、`select_option`、`dialog` |
|
|
68
86
|
| JS / CDP | `evaluate`(页面上下文)、`cdp`(chrome.debugger 直通) |
|
|
69
87
|
| 截图 / PDF | `screenshot`、`save_as_pdf`(写文件、返回路径) |
|
|
70
88
|
| 网络 / 上传 | `network`(请求捕获)、`upload`(文件上传) |
|
|
89
|
+
| 滚动 / 等待 | `scroll`(滚动/定位)、`wait`(等待文本/选择器条件) |
|
|
71
90
|
| 会话分组 | 一个任务 = 一个 session = 一个标签页组,可一键清理 |
|
|
72
91
|
|
|
73
92
|
## 配置(可选)
|
|
@@ -78,7 +97,7 @@ dsh plugin --profile web add kimi-webbridge-dsh
|
|
|
78
97
|
- id: kimi-webbridge
|
|
79
98
|
config:
|
|
80
99
|
autoInstallDaemon: false
|
|
81
|
-
daemonVersion:
|
|
100
|
+
daemonVersion: v2.0.8
|
|
82
101
|
```
|
|
83
102
|
|
|
84
103
|
| 配置项 | 默认值 | 说明 |
|
|
@@ -86,8 +105,8 @@ dsh plugin --profile web add kimi-webbridge-dsh
|
|
|
86
105
|
| `baseUrl` | `http://127.0.0.1:10086` | 守护进程地址 |
|
|
87
106
|
| `requestTimeoutMs` | `120000` | 单次请求超时(毫秒) |
|
|
88
107
|
| `autoStartDaemon` | `true` | 守护进程未运行时自动启动 |
|
|
89
|
-
| `autoInstallDaemon` | `true` | 二进制缺失时自动从官方 CDN 下载安装(关闭后需手动[从官方安装](https://www.kimi.
|
|
90
|
-
| `daemonVersion` | `latest` | 自动安装时的版本(可固定如 `
|
|
108
|
+
| `autoInstallDaemon` | `true` | 二进制缺失时自动从官方 CDN 下载安装(关闭后需手动[从官方安装](https://www.kimi.com/products/kimi-webbridge)) |
|
|
109
|
+
| `daemonVersion` | `latest` | 自动安装时的版本(可固定如 `v2.0.8`,需带 `v` 前缀与 CDN 路径一致;建议与扩展版本匹配) |
|
|
91
110
|
| `registerSkill` | `true` | 启动时把 `kimi-webbridge` skill 注册进运行时 registry(关闭后不注册) |
|
|
92
111
|
|
|
93
112
|
## 移除
|
package/lib/index.js
CHANGED
|
@@ -3,12 +3,12 @@ import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
3
3
|
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import { once } from "node:events";
|
|
6
7
|
import { createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import { chmod, mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
8
|
-
import { request as httpRequest } from "node:http";
|
|
9
|
-
import { request as httpsRequest } from "node:https";
|
|
10
9
|
import { homedir } from "node:os";
|
|
11
10
|
import { dirname, join } from "node:path";
|
|
11
|
+
import { Readable } from "node:stream";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -17,11 +17,15 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
* Infrastructure-only bridge plugin:
|
|
18
18
|
* - provisions the daemon binary (official Kimi CDN, SHA-256 verified) and
|
|
19
19
|
* auto-starts it, keeping the bridge healthy;
|
|
20
|
-
* - registers the packaged skill.md (the full
|
|
20
|
+
* - registers the packaged skill.md (the full 25-action HTTP protocol for
|
|
21
21
|
* driving the user's real browser) as a runtime skill via ctx.skills;
|
|
22
22
|
* - registers one thin tool `kimi_webbridge` whose only action is `status`
|
|
23
23
|
* (daemon + extension health check) as an anchor for other skills.
|
|
24
24
|
*
|
|
25
|
+
* Outbound HTTP uses the platform `fetch`, so requests inherit the process-wide
|
|
26
|
+
* proxy policy the harness installs from the launch environment (`HTTP_PROXY`,
|
|
27
|
+
* `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`); loopback traffic is never proxied.
|
|
28
|
+
*
|
|
25
29
|
* A browser extension must still be connected for browser actions to work.
|
|
26
30
|
*
|
|
27
31
|
* @module kimi-webbridge-dsh
|
|
@@ -80,7 +84,7 @@ function releasePlatform() {
|
|
|
80
84
|
* @returns a short human-readable install instruction.
|
|
81
85
|
*/
|
|
82
86
|
function manualInstallHint() {
|
|
83
|
-
const page = "https://www.kimi.
|
|
87
|
+
const page = "https://www.kimi.com/products/kimi-webbridge";
|
|
84
88
|
if (process.platform === "win32") {
|
|
85
89
|
return `run \"irm https://cdn.kimi.com/webbridge/install.ps1 | iex\" in PowerShell (or install from ${page})`;
|
|
86
90
|
}
|
|
@@ -101,106 +105,136 @@ function readSkillBody() {
|
|
|
101
105
|
return readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "skill.md"), "utf8");
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
/** Underlying transport `code` for a rejected `fetch`, which buries it one level down in `cause`. */
|
|
109
|
+
function transportErrorCode(error) {
|
|
110
|
+
const cause = error?.cause;
|
|
111
|
+
return cause?.code ?? cause?.errors?.[0]?.code ?? error?.code;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Human-readable reason for a rejected `fetch`, preferring the transport cause over "fetch failed". */
|
|
115
|
+
function transportErrorMessage(error) {
|
|
116
|
+
const cause = error?.cause;
|
|
117
|
+
return cause?.errors?.[0]?.message ?? cause?.message ?? error?.message ?? String(error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Shape a rejected `fetch` into the error this plugin's callers expect.
|
|
122
|
+
*
|
|
123
|
+
* A caller abort surfaces as a bare `aborted` error, which the tool maps to the canonical abort;
|
|
124
|
+
* a timeout names itself; and a transport failure keeps the underlying `code` (`ECONNREFUSED`,
|
|
125
|
+
* `ENOTFOUND`, …) so daemon self-healing still recognizes an unreachable local daemon.
|
|
126
|
+
*
|
|
127
|
+
* @param error - the rejection from `fetch` or from consuming its body.
|
|
128
|
+
* @param options - caller signal, whether our own timeout fired, and that timeout.
|
|
129
|
+
* @returns the error to throw.
|
|
130
|
+
*/
|
|
131
|
+
function fetchFailure(error, { signal, timedOut, timeoutMs } = {}) {
|
|
132
|
+
if (signal?.aborted) return new Error("aborted");
|
|
133
|
+
if (timedOut || error?.name === "TimeoutError") {
|
|
134
|
+
return new Error(`kimi-webbridge: request timed out after ${timeoutMs}ms`);
|
|
135
|
+
}
|
|
136
|
+
const failure = new Error(transportErrorMessage(error));
|
|
137
|
+
const code = transportErrorCode(error);
|
|
138
|
+
if (code !== void 0) failure.code = code;
|
|
139
|
+
return failure;
|
|
140
|
+
}
|
|
141
|
+
|
|
104
142
|
/**
|
|
105
143
|
* One HTTP JSON round-trip with a timeout and caller-abort support.
|
|
144
|
+
*
|
|
145
|
+
* Uses the platform `fetch`, so the request inherits the process-wide proxy policy the harness
|
|
146
|
+
* installs from the launch environment. An already-aborted caller signal fails immediately,
|
|
147
|
+
* without issuing a request.
|
|
148
|
+
*
|
|
106
149
|
* @param url - absolute URL.
|
|
107
150
|
* @param options - method, optional JSON body, timeout, and AbortSignal.
|
|
108
151
|
* @returns the parsed JSON body (or raw text fallback) plus the status code.
|
|
109
152
|
*/
|
|
110
|
-
function httpJson(url, { method = "GET", body, timeoutMs, signal } = {}) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (signal?.aborted) onSignal();
|
|
131
|
-
else signal?.addEventListener("abort", onSignal, { once: true });
|
|
132
|
-
req = transport(target, {
|
|
153
|
+
async function httpJson(url, { method = "GET", body, timeoutMs, signal } = {}) {
|
|
154
|
+
try {
|
|
155
|
+
new URL(url);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`kimi-webbridge: invalid baseUrl ${url}: ${String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
// Fail fast: never issue a request the caller has already cancelled.
|
|
160
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
let timedOut = false;
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
timedOut = true;
|
|
165
|
+
controller.abort();
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
const onCallerAbort = () => controller.abort();
|
|
169
|
+
signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
170
|
+
const payload = body === void 0 ? void 0 : JSON.stringify(body);
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(url, {
|
|
133
173
|
method,
|
|
134
|
-
headers: payload
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
138
|
-
res.on("end", () => {
|
|
139
|
-
clearTimeout(timer);
|
|
140
|
-
signal?.removeEventListener("abort", onSignal);
|
|
141
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
142
|
-
let parsed;
|
|
143
|
-
try {
|
|
144
|
-
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
145
|
-
} catch {
|
|
146
|
-
parsed = { raw: text };
|
|
147
|
-
}
|
|
148
|
-
resolve({ status: res.statusCode ?? 0, body: parsed, text });
|
|
149
|
-
});
|
|
174
|
+
headers: payload === void 0 ? void 0 : { "Content-Type": "application/json" },
|
|
175
|
+
body: payload,
|
|
176
|
+
signal: controller.signal
|
|
150
177
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
182
|
+
} catch {
|
|
183
|
+
parsed = { raw: text };
|
|
184
|
+
}
|
|
185
|
+
return { status: response.status, body: parsed, text };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw fetchFailure(error, { signal, timedOut, timeoutMs });
|
|
188
|
+
} finally {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
signal?.removeEventListener("abort", onCallerAbort);
|
|
191
|
+
}
|
|
159
192
|
}
|
|
160
193
|
|
|
161
194
|
/**
|
|
162
|
-
*
|
|
195
|
+
* Stream an HTTP response body to disk, following redirects.
|
|
196
|
+
*
|
|
197
|
+
* Uses the platform `fetch`, so a CDN download works behind the proxy the harness installs from
|
|
198
|
+
* the launch environment. The timeout is an *idle* budget — it restarts on every received chunk —
|
|
199
|
+
* so a large but progressing download is never cut off, matching the socket idle timeout the
|
|
200
|
+
* previous `node:http` implementation relied on.
|
|
201
|
+
*
|
|
163
202
|
* @param url - absolute download URL.
|
|
164
203
|
* @param dest - destination file path.
|
|
165
|
-
* @param options - per-
|
|
204
|
+
* @param options - per-chunk idle timeout in milliseconds.
|
|
166
205
|
* @returns the destination path on success.
|
|
167
206
|
*/
|
|
168
|
-
function downloadFile(url, dest, { timeoutMs
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
req.setTimeout(timeoutMs, () => req.destroy(new Error(`download timed out after ${timeoutMs}ms`)));
|
|
200
|
-
req.end();
|
|
201
|
-
};
|
|
202
|
-
fetch(url, maxRedirects);
|
|
203
|
-
});
|
|
207
|
+
async function downloadFile(url, dest, { timeoutMs }) {
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
let timedOut = false;
|
|
210
|
+
let timer;
|
|
211
|
+
const arm = () => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
timer = setTimeout(() => {
|
|
214
|
+
timedOut = true;
|
|
215
|
+
controller.abort();
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
timer.unref?.();
|
|
218
|
+
};
|
|
219
|
+
arm();
|
|
220
|
+
const file = createWriteStream(dest);
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
223
|
+
if (!response.ok) throw new Error(`download failed: HTTP ${response.status} for ${url}`);
|
|
224
|
+
for await (const chunk of Readable.fromWeb(response.body)) {
|
|
225
|
+
arm();
|
|
226
|
+
if (!file.write(chunk)) await once(file, "drain");
|
|
227
|
+
}
|
|
228
|
+
await new Promise((resolve, reject) => {
|
|
229
|
+
file.end((error) => (error ? reject(error) : resolve()));
|
|
230
|
+
});
|
|
231
|
+
return dest;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
file.destroy();
|
|
234
|
+
throw fetchFailure(error, { timedOut, timeoutMs });
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
204
238
|
}
|
|
205
239
|
|
|
206
240
|
/**
|
|
@@ -220,7 +254,7 @@ function downloadFile(url, dest, { timeoutMs, maxRedirects = 3 } = {}) {
|
|
|
220
254
|
async function installDaemonBinary({ version, timeoutMs }) {
|
|
221
255
|
const platform = releasePlatform();
|
|
222
256
|
if (platform === null) {
|
|
223
|
-
throw new Error(`unsupported platform ${process.platform}-${process.arch} — install the daemon manually (see https://www.kimi.com/
|
|
257
|
+
throw new Error(`unsupported platform ${process.platform}-${process.arch} — install the daemon manually (see https://www.kimi.com/products/kimi-webbridge)`);
|
|
224
258
|
}
|
|
225
259
|
const manifestKey = platform.endsWith(".exe") ? platform.slice(0, -4) : platform;
|
|
226
260
|
const bin = daemonBinPath();
|
|
@@ -238,15 +272,20 @@ async function installDaemonBinary({ version, timeoutMs }) {
|
|
|
238
272
|
// Manifest unavailable (e.g. old pinned version) — install without checksum.
|
|
239
273
|
}
|
|
240
274
|
const tmp = `${bin}.tmp-${process.pid}-${Date.now()}`;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
275
|
+
try {
|
|
276
|
+
await downloadFile(url, tmp, { timeoutMs });
|
|
277
|
+
if (expectedSha !== null) {
|
|
278
|
+
const actual = createHash("sha256").update(await readFile(tmp)).digest("hex");
|
|
279
|
+
if (actual !== expectedSha) {
|
|
280
|
+
throw new Error(`downloaded binary SHA-256 mismatch for ${platform}: expected ${expectedSha}, got ${actual}`);
|
|
281
|
+
}
|
|
247
282
|
}
|
|
283
|
+
await rename(tmp, bin);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// Never leave a partial or unverified download behind.
|
|
286
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
287
|
+
throw error;
|
|
248
288
|
}
|
|
249
|
-
await rename(tmp, bin);
|
|
250
289
|
if (process.platform !== "win32") await chmod(bin, 0o755);
|
|
251
290
|
return { bin, url, sha256: expectedSha };
|
|
252
291
|
}
|
|
@@ -273,6 +312,24 @@ function abortError() {
|
|
|
273
312
|
return error;
|
|
274
313
|
}
|
|
275
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Compare two dotted numeric versions, ignoring any prerelease/build suffix.
|
|
317
|
+
* @param a - left version (no leading "v").
|
|
318
|
+
* @param b - right version (no leading "v").
|
|
319
|
+
* @returns -1, 0, or 1.
|
|
320
|
+
*/
|
|
321
|
+
function compareVersions(a, b) {
|
|
322
|
+
const parts = (value) => value.split(/[.\-+]/).map((piece) => Number.parseInt(piece, 10));
|
|
323
|
+
const left = parts(a);
|
|
324
|
+
const right = parts(b);
|
|
325
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
326
|
+
const x = Number.isNaN(left[index]) ? 0 : left[index] ?? 0;
|
|
327
|
+
const y = Number.isNaN(right[index]) ? 0 : right[index] ?? 0;
|
|
328
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
276
333
|
/** Shape one daemon response into the canonical tool value. */
|
|
277
334
|
function canonicalResult(result) {
|
|
278
335
|
if (result.status >= 200 && result.status < 300) {
|
|
@@ -323,7 +380,7 @@ function apply(ctx, config = {}) {
|
|
|
323
380
|
|
|
324
381
|
ctx.tools.register(defineTool({
|
|
325
382
|
name: "kimi_webbridge",
|
|
326
|
-
description: "Health check for the local kimi-webbridge bridge (daemon at " + baseUrl + " and the browser extension). The plugin auto-provisions the daemon: it installs the binary from the official Kimi CDN when missing and starts it, then retries once. Returns daemon version/uptime and
|
|
383
|
+
description: "Health check for the local kimi-webbridge bridge (daemon at " + baseUrl + " and the browser extension). The plugin auto-provisions the daemon: it installs the binary from the official Kimi CDN when missing and starts it, then retries once. Returns daemon version/uptime, extension_connected, plus version (daemon) and extension_version (extension) — a mismatch means the components have drifted (the extension likely auto-updated). This tool does NOT perform browser actions — for navigation, clicking, typing, reading pages, screenshots, JS evaluation, CDP, uploads, PDF export and tab management, load the kimi-webbridge skill, which documents the full 25-action HTTP protocol (POST " + baseUrl + "/command) to drive the user's real browser.",
|
|
327
384
|
parameters: {
|
|
328
385
|
action: {
|
|
329
386
|
type: "string",
|
|
@@ -338,7 +395,29 @@ function apply(ctx, config = {}) {
|
|
|
338
395
|
additionalProperties: true
|
|
339
396
|
},
|
|
340
397
|
render(args, value) {
|
|
341
|
-
|
|
398
|
+
const blocks = [{ type: "text", text: JSON.stringify(value) }];
|
|
399
|
+
const norm = (s) => (typeof s === "string" ? s.replace(/^v/i, "") : "");
|
|
400
|
+
const daemonVer = norm(value?.version);
|
|
401
|
+
const extVer = norm(value?.extension_version);
|
|
402
|
+
if (daemonVer && extVer && daemonVer !== extVer) {
|
|
403
|
+
const daemonAhead = compareVersions(daemonVer, extVer) > 0;
|
|
404
|
+
blocks.push({
|
|
405
|
+
type: "text",
|
|
406
|
+
text: daemonAhead
|
|
407
|
+
? `⚠️ the browser extension (${value.extension_version}) is behind the daemon (${value.version}) — ask the user to update the extension, then retry.`
|
|
408
|
+
: `⚠️ version drift: daemon ${value.version} vs extension ${value.extension_version} — the browser extension likely auto-updated ahead of the daemon. Re-audit the 25-action skill list and update the daemon.`
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// The official convention: surface the release fields verbatim and let the
|
|
412
|
+
// user run the command — a model-triggered upgrade would kill the daemon.
|
|
413
|
+
const update = value?.update_available;
|
|
414
|
+
if (update !== null && typeof update === "object" && typeof update.command === "string") {
|
|
415
|
+
blocks.push({
|
|
416
|
+
type: "text",
|
|
417
|
+
text: `⬆️ update available: daemon ${update.current ?? "?"} → ${update.latest ?? "?"} — tell the user and hand them this command verbatim; do NOT run it yourself: ${update.command}`
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return blocks;
|
|
342
421
|
}
|
|
343
422
|
},
|
|
344
423
|
isConcurrencySafe() {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi-webbridge-dsh",
|
|
3
|
-
"description": "Infrastructure plugin for dsh: provisions the kimi-webbridge daemon (auto-install with SHA-256 verification, auto-start) and registers the kimi-webbridge skill that documents the full
|
|
4
|
-
"version": "1.4.
|
|
3
|
+
"description": "Infrastructure plugin for dsh: provisions the kimi-webbridge daemon (auto-install with SHA-256 verification, auto-start) and registers the kimi-webbridge skill that documents the full 25-action browser protocol. Registers a thin kimi_webbridge tool (status health check only).",
|
|
4
|
+
"version": "1.4.8",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": { "type": "git", "url": "git+https://github.com/cfanmaoli/kimi-webbridge-dsh.git" },
|
|
6
7
|
"type": "module",
|
|
7
8
|
"keywords": [
|
|
8
9
|
"dsh",
|
|
@@ -36,8 +37,8 @@
|
|
|
36
37
|
"cordis.patch.yml"
|
|
37
38
|
],
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.2",
|
|
40
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7 || ^0.1.1-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
|
|
41
42
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
42
43
|
}
|
|
43
44
|
}
|
package/skill.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Kimi WebBridge
|
|
1
|
+
# Kimi Browser Extension (Kimi 浏览器扩展, formerly Kimi WebBridge)
|
|
2
2
|
|
|
3
|
-
Control the user's real browser (their login sessions) through the local daemon at `http://127.0.0.1:10086
|
|
3
|
+
Control the user's real browser (their login sessions) through the local daemon at `http://127.0.0.1:10086` (the default address).
|
|
4
4
|
|
|
5
|
-
> This skill is registered by the `kimi-webbridge-dsh` plugin (plugin v1.4.
|
|
5
|
+
> This skill is registered by the `kimi-webbridge-dsh` plugin (plugin v1.4.8 / daemon v2.0.8). Do not hand-edit it.
|
|
6
6
|
|
|
7
7
|
## Health check first
|
|
8
8
|
|
|
9
|
-
Before driving the browser, call the **`kimi_webbridge` tool with action `status`**. It reports daemon + extension health and auto-provisions the daemon (downloads the binary from the official Kimi CDN when missing and starts it, then retries once). `extension_connected: true` means the browser extension is ready; if false, tell the user to install/connect the Kimi
|
|
9
|
+
Before driving the browser, call the **`kimi_webbridge` tool with action `status`**. It reports daemon + extension health and auto-provisions the daemon (downloads the binary from the official Kimi CDN when missing and starts it, then retries once). `extension_connected: true` means the browser extension is ready; if false, tell the user to install/connect the Kimi Browser Extension.
|
|
10
10
|
|
|
11
|
-
## The
|
|
11
|
+
## The 25 actions
|
|
12
12
|
|
|
13
13
|
Every action is one HTTP call: `POST http://127.0.0.1:10086/command` with body `{"action": "<name>", "args": {...}, "session": "<task-name>"}`.
|
|
14
14
|
|
|
@@ -31,6 +31,14 @@ Every action is one HTTP call: `POST http://127.0.0.1:10086/command` with body `
|
|
|
31
31
|
| `list_tabs` | — | `{success, tabs:[{tabId,url,title,active,groupTitle}]}` | |
|
|
32
32
|
| `close_tab` | — | `{success, closed}` | |
|
|
33
33
|
| `close_session` | — | `{success, closed}` | close all session tabs — only when the user asks |
|
|
34
|
+
| `read_page` | `max_chars?`, `start?` | `{text, complete, screens, chars}` | readable page text, paged — pass `start` to continue when `complete:false` |
|
|
35
|
+
| `find` | `query` | `{url, title, matches}` | search the page (accessibility tree) for a text query |
|
|
36
|
+
| `scroll` | `direction?` up/down, `to?` top/bottom, `selector?`, `text?` | `{scrolled, direction, atTop, atBottom, scrollY}` | scroll the page or an element into view |
|
|
37
|
+
| `hover` | `selector` (`@e` or CSS) | `{success, x, y, tag, text}` | hover over an element |
|
|
38
|
+
| `drag` | `from`, `to` (`@e`/CSS) | `{success, dispatched, verified, dropAccepted}` | HTML5 drag-and-drop from→to |
|
|
39
|
+
| `select_option` | `selector`, `values` (or `value`) | `{success, ...}` | pick options in a native `<select>`; custom dropdowns → click |
|
|
40
|
+
| `dialog` | `action` status/accept/dismiss, `text?` | `{open, type, message}` / `{success, action}` | handle JS dialogs (alert/confirm/prompt) |
|
|
41
|
+
| `wait` | `text?`, `text_gone?`, `selector?`, `timeout_ms?` | `{ok, ...}` / `{ok:false, timedOut, waitedMs}` | wait for a condition (text appears/disappears, selector visible) |
|
|
34
42
|
|
|
35
43
|
## Sending requests (shell-proof)
|
|
36
44
|
|
|
@@ -63,6 +71,8 @@ Call `close_session` only when the user explicitly asks to close the tabs.
|
|
|
63
71
|
- Trusted input: on sites that reject synthetic events (`event.isTrusted` checks — banking portals, captchas), use `mouse_click` instead of `click` and `key_type`/`send_keys` instead of `fill`. If those still fail, tell the user the page needs manual interaction.
|
|
64
72
|
- `send_keys` keys: Enter/Return, Escape/Esc, Tab, Backspace, Delete, Space, ArrowUp/Down/Left/Right, Home/End, PageUp/PageDown, F1–F12, single letters/digits. Modifiers: Alt/Ctrl/Control/Cmd/Meta/Shift, or `Mod` (Cmd on Mac, Ctrl elsewhere). Join with `+` ("Mod+A", "Shift+Tab"); space-separate several ("Enter Escape").
|
|
65
73
|
- Cross-origin iframes are not covered — navigate to the iframe URL directly.
|
|
74
|
+
- `read_page` returns the full readable page text (paged — follow `start` when `complete:false`); `find` searches the page text for a query. Both are text readers; `snapshot` is still how you get `@e` refs for interaction.
|
|
75
|
+
- `scroll` (direction/to/selector/text), `hover`, `drag` (from→to), `select_option` (native `<select>` only), and `dialog` (status/accept/dismiss) cover page interaction; `wait` blocks until a condition (text/selector) is met instead of blind sleeps.
|
|
66
76
|
|
|
67
77
|
## Screenshots and PDF
|
|
68
78
|
|
|
@@ -70,10 +80,12 @@ Call `close_session` only when the user explicitly asks to close the tabs.
|
|
|
70
80
|
|
|
71
81
|
## Daemon or extension problems
|
|
72
82
|
|
|
73
|
-
- Call the `kimi_webbridge` tool (`status`) — the plugin auto-provisions and starts the daemon. Never run `stop` / `restart` / `uninstall` yourself.
|
|
74
|
-
- If an error mentions "Please update the Kimi
|
|
83
|
+
- Call the `kimi_webbridge` tool (`status`) — the plugin auto-provisions and starts the daemon. Never run `stop` / `restart` / `upgrade` / `uninstall` yourself: they kill a running daemon.
|
|
84
|
+
- If an error mentions "Please update the Kimi Browser Extension", the extension is outdated — ask the user to update it (https://www.kimi.com/products/kimi-webbridge).
|
|
75
85
|
- If the daemon stays unreachable after the status tool's retry, point the user to the help page instead of deep-troubleshooting.
|
|
86
|
+
- When `status` reports `update_available` or `version_mismatch`, tell the user and hand them that field's `command` verbatim — that is what the field is for. Do **not** run it yourself.
|
|
87
|
+
- If the status tool warns that `version` (daemon) and `extension_version` (extension) differ, one side is behind (the extension follows the browser store's schedule): if the **extension** is behind, ask the user to update it; if the **daemon** is behind, hand them the `upgrade` command. Re-audit the 25-action list above after either change.
|
|
76
88
|
|
|
77
89
|
---
|
|
78
90
|
|
|
79
|
-
Protocol and workflow adapted from the Kimi
|
|
91
|
+
Protocol and workflow adapted from the Kimi Browser Extension community skill (Apache-2.0). Daemon binary and browser extension by Kimi. This skill is registered by kimi-webbridge-dsh.
|