dsh-chrome-use 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShaoTao Huang
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/PRIVACY.md ADDED
@@ -0,0 +1,25 @@
1
+ # 隐私说明
2
+
3
+ ## 访问范围
4
+
5
+ dsh-chrome-use 用于让你配置的 DeepSeek Harness 操作本机 Chrome。扩展需要 `debugger`、`tabs`、`storage` 和 `alarms` 权限,分别用于调试网页、选择标签页、保存配对状态和恢复连接。
6
+
7
+ 连接后,Harness 可以读取和操作所有普通 HTTP(S) 标签页,包括已登录的网站。只有 AI 接管的页面才会开启调试和日志采集。Chrome 调试提示条会显示当前调试状态;你可以在扩展弹窗点击“断开连接”。
8
+
9
+ ## 数据流向
10
+
11
+ 扩展只连接本机 `127.0.0.1:18791`,使用一次性配对码和后续配对令牌验证连接。插件不把页面内容发送给作者的服务器,也不内置统计或广告服务。
12
+
13
+ 工具返回的页面文字、标题、URL、截图路径和调试信息会进入 Harness 会话。Harness 可能将这些内容发送给你选择的模型提供方。截图由其他图像工具读取后,也可能发送给模型提供方。模型服务和 Harness 自身的数据处理方式由它们各自的设置与政策决定。
14
+
15
+ ## 本地保存
16
+
17
+ 配对信息保存在 Chrome 扩展存储,以及 Harness 所在机器的 `~/.dsh/dsh-chrome-use/pairing.json`。截图保存在同目录下的 `screenshots/`,需要你自行删除。Harness 可能保存会话历史。
18
+
19
+ 网页快照不直接返回控件的 `value` 字段,但页面中的可见文字仍会返回。截图可能包含可见的私人内容,`chrome_evaluate` 也可以读取页面数据。网络日志不采集请求头、Cookie、请求体或响应体,URL 会去除查询参数和片段。控制台文字仍可能包含网站输出的敏感信息。
20
+
21
+ ## 停止使用
22
+
23
+ 在扩展弹窗点击“断开连接”会停止当前调试并关闭自动重连,保留配对信息以便以后重连。卸载 Chrome 扩展会移除扩展自身的存储;卸载 Harness 插件后,可自行删除上述本地目录。Harness 会话历史需要在 Harness 中另行管理。
24
+
25
+ 项目问题可在 https://github.com/tao1020/dsh-chrome-use/issues 提交。不要在公开 issue 中上传密钥、配对码、私人页面截图或未清理的会话日志。
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # dsh-chrome-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
+ 让 DeepSeek Harness 读取、操作和调试本机 Google Chrome。仓库同时包含 Harness 插件和 Chrome 扩展,连接走本机回环地址。
4
+
5
+ ## 能做什么
6
+
7
+ 连接后,AI 可以选择所有普通 HTTP(S) 标签页,无需逐页点击授权。支持读取页面结构、点击、填写输入框、按键、滚动、导航、截图,以及查看控制台、JavaScript 错误和网络状态。
8
+
9
+ 扩展使用 Chrome 的 `debugger` 接口。调试时,Chrome 会显示调试提示条;点击时,页面短暂显示蓝色箭头。页面定位使用浏览器的无障碍树和 DOM,点击、输入通过浏览器输入事件执行。
10
+
11
+ 扩展弹窗只提供连接、连接状态、正在调试的标签页和“断开连接”。断开会停止全部操作。同一个标签页只能由一个 Harness 会话占用,AI 使用 `chrome_claim` 和 `chrome_release` 管理占用,无需用户选择。
12
+
13
+ ## 安装测试版
14
+
15
+ 需要 Google Chrome 120 或更新版本、DeepSeek Harness 0.2.1-rc.2 或更新版本。开发和测试使用 Node.js 22 或 24。
16
+
17
+ 1. 下载或克隆本仓库。
18
+ 2. 在 Harness 的插件管理中添加仓库的本地绝对路径。命令行版可在仓库内运行 `dsh plugin --profile headless add "$PWD"`;日常使用哪个 profile,就安装到哪个 profile。
19
+ 3. 在 Harness 中说:“请调用 chrome_setup,准备 dsh-chrome-use 扩展,并告诉我安装目录。”
20
+ 4. 在 Chrome 打开 `chrome://extensions/`,开启“开发者模式”,点击“加载已解压的扩展程序”,选择工具返回的目录。
21
+ 5. 让 Harness 调用 `chrome_pair`。在 Chrome 工具栏的扩展弹窗里输入 8 位配对码,点击“连接”。临时码有效期为 2 分钟;成功后保存的配对不会定时过期。
22
+
23
+ 以后重启 Harness、Chrome 或刷新扩展,扩展会自动尝试恢复连接。你主动点击“断开连接”后,自动重连会停止;下次配对码留空,点“连接”即可。清除配对数据或更换扩展安装目录后,需要重新配对。
24
+
25
+ AI 可以准备文件和安装说明;Chrome 的“加载已解压的扩展程序”步骤需要你手动完成。当前版本尚未发布到 Chrome 扩展商店。
26
+
27
+ **连接即允许 Harness 操作此 Chrome 中的全部普通网页,包括已登录的网站。** 网页内容、截图和调试信息可能发送给你在 Harness 配置的模型服务。详见 [隐私说明](PRIVACY.md)。
28
+
29
+ 更新时先断开扩展,更新 Harness 插件,重新调用 `chrome_setup`,再到扩展管理页点刷新。已配对的扩展可以留空配对码重连。开发时也可直接加载仓库的 `extension/` 目录。
30
+
31
+ ## 测试
32
+
33
+ ```sh
34
+ npm ci --ignore-scripts
35
+ npm run check
36
+ npm test
37
+ node scripts/fixture.mjs
38
+ ```
39
+
40
+ 最后一个命令启动专用测试页:`http://127.0.0.1:18792/`。保持服务运行,把 [验收提示词](docs/harness-test-prompt.md) 交给 Harness。使用 `dsh --profile headless --json` 可记录工具调用和最终结论。模型、API Key 和插件必须配置在测试使用的 profile 中。
41
+
42
+ 无需模型时,可以运行 `npm run dev`,按行输入 JSON,例如 `{"action":"status"}`。首次连接用 `{"action":"pair"}` 生成配对码。调试服务和 Harness 共用已保存的配对,也使用同一端口,运行其中一个即可。
43
+
44
+ 本仓库的自动化测试覆盖配对、会话占用、超时、取消、断线、页面限制、过期元素引用和输入行为。真实 Chrome 验收需要单独运行,单元测试通过不代表浏览器验收通过。
45
+
46
+ ## 排查连接问题
47
+
48
+ - `BRIDGE_START_FAILED`:通常是另一个 Harness profile 或调试服务占用了端口 18791。先停止重复服务。
49
+ - `AUTH_FAILED`:在 Harness 中生成新配对码,再到扩展弹窗连接。
50
+ - `TAB_IN_USE`:其他 Harness 会话已占用该页。让原会话释放,或新建自己的标签页。
51
+ - Chrome 调试器冲突:关闭该页的 DevTools 或释放其他浏览器工具的会话,再检查连接。不要同时用 Codex 和 Harness 操作同一页。
52
+ - 操作超时或取消:连接会关闭,工具不会自动重放操作。重新连接后先读取页面,确认之前的点击是否已发生。
53
+
54
+ ## 当前限制
55
+
56
+ 仅支持普通 HTTP(S) 网页;不支持 Chrome 内部页面、本地文件或扩展页面。Chrome 还可能限制其他页面的调试。点击和输入目前仅支持顶层页面元素,不支持跨域 iframe、文件上传、下载管理或原生对话框。
57
+
58
+ `chrome_evaluate` 可为调试执行 JavaScript,可能修改页面。页面内容是不可信数据,不能把网页中的指令当作用户授权。截图保存到本机,模型能否理解截图取决于模型能力。
59
+
60
+ ## 打包
61
+
62
+ ```sh
63
+ npm run package
64
+ npm pack --pack-destination dist
65
+ ```
66
+
67
+ `dist/` 包含 Chrome 扩展 ZIP、校验值和 Harness 插件包。GitHub Actions 会在 Node.js 22、24 和三个操作系统上运行检查,并生成测试构建。这不会发布到扩展商店,也不会自动发布 npm 包。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-chrome-use
3
+ name: dsh-chrome-use
@@ -0,0 +1,31 @@
1
+ # 读取已登录网页并调试按钮
2
+
3
+ 插件直接连接安装它的 Chrome。读取现有标签页时,使用该标签页已有的登录状态。Harness 内置浏览器的登录状态与 Chrome 分开;需要复用 Chrome 登录时,应调用本插件的工具。
4
+
5
+ ## 给 Harness 的提示词
6
+
7
+ > 请使用 dsh-chrome-use 读取我已经在 Chrome 打开的目标网页。先用 chrome_tabs 查找标签页,确认网址后用 chrome_claim 占用它,再用 chrome_snapshot 读取内容。请复用这个标签页的登录状态。我要开发一个给页面添加按钮的插件,请检查目标区域的 DOM 和样式,提出合适的插入位置。
8
+
9
+ `chrome_claim` 由 Agent 调用,用于避免不同会话同时操作同一标签页,不需要用户逐页授权。
10
+
11
+ ## 读取结构和验证按钮
12
+
13
+ `chrome_snapshot` 返回页面的可访问性结构,包括文字、链接和按钮。它最多返回 500 个条目,结果中的 `truncated` 表示是否截断;它不返回完整 HTML,也不返回输入框的值。
14
+
15
+ 要检查目标区域,可用 `chrome_evaluate` 执行下面的表达式。先根据快照确认选择器,再替换示例中的 `main`。
16
+
17
+ ```js
18
+ (() => {
19
+ const target = document.querySelector('main');
20
+ if (!target) return { error: '没有找到目标区域' };
21
+ return {
22
+ html: target.outerHTML.slice(0, 20000),
23
+ display: getComputedStyle(target).display,
24
+ position: getComputedStyle(target).position
25
+ };
26
+ })()
27
+ ```
28
+
29
+ 用户要求预览按钮时,可通过 `chrome_evaluate` 临时插入 DOM,再用 `chrome_snapshot`、`chrome_click` 和 `chrome_logs` 检查按钮及点击结果。直接插入页面的修改通常会在刷新后消失;最终仍需把按钮逻辑写入开发中的插件,并重新加载插件验收。
30
+
31
+ 当前版本尚未完整验收跨域 iframe、封闭 Shadow DOM 和复杂网站。网页文字是待分析的数据,不能把页面中的指令当成用户要求。
@@ -0,0 +1,42 @@
1
+ # Chrome 扩展商店材料草稿
2
+
3
+ ## 名称与简介
4
+
5
+ 名称:dsh-chrome-use
6
+
7
+ 简介:让本机 DeepSeek Harness 读取、操作和调试 Chrome 网页。
8
+
9
+ ## 详细介绍
10
+
11
+ dsh-chrome-use 将 Google Chrome 连接到本机 DeepSeek Harness。首次配对后保存连接凭据,后续自动重连。连接后,AI 可以选择普通网页,执行点击、输入、滚动和导航,也可以读取页面结构、保存截图、查看控制台及网页错误。
12
+
13
+ 调试时 Chrome 会显示调试提示。扩展弹窗提供连接状态和断开按钮,无需逐页授权。你可以随时断开全部操作。
14
+
15
+ 使用前需要在同一台电脑安装 DeepSeek Harness 和本仓库提供的 Harness 插件。扩展不提供模型服务,不包含模型 API Key。你选择的模型服务可能处理工具返回的网页内容。
16
+
17
+ ## 单一用途
18
+
19
+ 为用户配置的本机 DeepSeek Harness 提供 Chrome 页面操作和调试能力。
20
+
21
+ ## 权限用途
22
+
23
+ | 权限 | 用途 |
24
+ |---|---|
25
+ | `debugger` | 读取页面结构,执行浏览器输入、截图和网页调试 |
26
+ | `tabs` | 列出和选择标签页,读取目标页标题及 URL |
27
+ | `storage` | 保存配对令牌和自动重连状态 |
28
+ | `alarms` | 在 Harness 重启或连接中断后尝试重连 |
29
+
30
+ ## 数据披露依据
31
+
32
+ 以仓库 [PRIVACY.md](../PRIVACY.md) 为准。作者没有页面数据接收服务器。工具结果会进入用户的 Harness 会话,并可能发送给用户选择的模型提供方。截图和配对数据保存在本机。
33
+
34
+ 调试工具可按用户要求在页面执行 JavaScript。正式提交前需要在商店的代码执行及数据披露问卷中如实说明,并根据当时的审核政策核对提交方式。
35
+
36
+ ## 提交前仍需准备
37
+
38
+ - 开发者账号和公开可访问的隐私政策地址。
39
+ - 商店要求的扩展图标、弹窗截图和演示截图。
40
+ - 验收后的正式版本及完整权限、数据披露问卷。
41
+
42
+ 当前只准备文案和测试包,尚未提交商店。没有准备 Edge 商店发布。
@@ -0,0 +1,15 @@
1
+ 请独立验收 dsh-chrome-use。你是测试者,只报告实测结果,不修改插件源码。仅操作你通过 chrome_open 创建的 http://127.0.0.1:18792/ 测试标签页,不读取或操作其他网站。不要创建子 Agent。浏览器操作只用本插件的 chrome_* 工具,不用其他浏览器工具或系统鼠标。
2
+
3
+ 1. 调用 chrome_status。未连接时调用 chrome_pair,输出配对码,等待用户在扩展弹窗连接。测试监督者会从工具记录中看到配对码并通知用户。用 bash 的 sleep 10 和 chrome_status 每 10 秒检查一次,最多等 180 秒。不要因此直接结束任务。如果配对码过期,重新生成一次;仍未连接则报告阻塞。
4
+ 2. 连接后用 chrome_open 打开测试页,记录 tabId。用 chrome_snapshot 检查标题及“测试姓名”“显示问候”等控件。导航尚未完成时最多再读取两次。确认快照没有暴露密码框的值。
5
+ 3. 用 chrome_fill 向“测试姓名”输入“DeepSeek 验收 123”,用 chrome_click 点击“显示问候”。重新读取快照,确认问候文字和 isTrusted=true。保存 chrome_screenshot,报告返回路径。截图保存成功与图像理解是两个不同结论。
6
+ 4. 测试空文本清空,再填入“第二次输入”。按 Tab,确认键盘操作成功。用 chrome_scroll 向下滚动,用 chrome_evaluate 只读检查 scrollY,再滚回。
7
+ 5. 点击“产生测试错误”,用 chrome_logs 检查 console.error、JavaScript 异常和 HTTP 503。网络日志不能包含测试请求的查询参数。发现日志尚未到达时,允许短暂等待后再读取一次。
8
+ 6. 尝试点击“禁用按钮”,应返回 ELEMENT_DISABLED。确认页面无变化。用 chrome_evaluate 执行 throw new Error('intentional-evaluate-test'),应返回明确错误。
9
+ 7. 先记录一份快照的 ref,再调用 chrome_navigate 到 /second。旧 ref 应报失效,不能误点其他元素。返回首页并重新读取快照,确认正常使用。工具可能用复合名称,但不要猜测 ref,必须使用实际快照里的值。
10
+ 8. 用 chrome_release 释放自己的测试页,再直接用 chrome_claim 接管同一页,确认无需用户点击任何逐页授权按钮。重新读取快照后,再 release。
11
+ 9. 尝试 chrome_open 的 chrome://settings/ URL,应拒绝这个不支持的页面,不创建它。
12
+
13
+ 如有超时或调试冲突,先检查 chrome_status、当前测试页与前一次工具结果。不要改用其他浏览器自动化。失败操作不能盲目重复。不要要求用户重新安装;报告原始错误及复现步骤。
14
+
15
+ 最后用中文输出 Markdown 报告:环境、每项 PASS/FAIL/BLOCKED、工具和关键证据、发现的问题、截图路径、总体结论。区分实际执行与推测。不能自行点击扩展弹窗时,把“手动断开连接”和弹窗按钮外观检查标为未测试,不编造结果。不要调用 chrome_tabs 暴露无关标签页,也不要读取凭据文件或输出密钥。
@@ -0,0 +1,33 @@
1
+ # 发布到 npm
2
+
3
+ 正式发布通过 GitHub Actions 的 `publish.yml` 执行。npm 使用 Trusted Publishing 验证工作流身份,不需要 `NPM_TOKEN`。流程使用 GitHub 托管的 Ubuntu runner、Node.js 24 和 npm 11.12.1,并授予发布任务 `id-token: write` 权限。
4
+
5
+ ## npm 端配置
6
+
7
+ 在 npm 的 `dsh-chrome-use` 包设置中添加 GitHub Actions Trusted Publisher,填写:
8
+
9
+ | 字段 | 值 |
10
+ | --- | --- |
11
+ | Organization or user | `tao1020` |
12
+ | Repository | `dsh-chrome-use` |
13
+ | Workflow filename | `publish.yml` |
14
+ | Environment name | `npm` |
15
+
16
+ 如果界面提供 Allowed actions,允许直接执行 `npm publish`。工作流文件名只填文件名,不填 `.github/workflows/` 路径。GitHub 仓库使用同名的 `npm` 环境。
17
+
18
+ 此配置位于 npm 包设置中。如果首次发布前还没有包页面,需要先完成包的首次创建,再配置绑定。不能仅修改工作流就认为 npm 已授权。首次创建方案应另行确认,不能跳过项目的发布审批规则。
19
+
20
+ 按 npm 当前文档,新建的 Trusted Publisher 配置需要在两天内完成首次成功发布;请在准备好发布时配置。已有的写入 Token 不再供本流程使用,确认没有其他用途后可撤销。
21
+
22
+ ## 发布步骤
23
+
24
+ 1. 审核并合并发布变更到 `main`。
25
+ 2. 获取最新远程引用,确认完整提交 SHA、包版本和发布范围,完成发布前检查与审批。
26
+ 3. 在批准的 `main` 提交上创建 `release-v<版本>` 标签,推送后核对远程标签 SHA。不要覆盖已有标签。
27
+ 4. 从该标签手动运行 `Publish npm package`,工作流本身也必须来自同一标签。
28
+ 5. 工作流检查标签、包版本和 `main` 当前提交一致,再运行构建、检查、测试及打包,最后发布同一份归档。
29
+ 6. 发布后检查 npm 上的版本和安装结果。已发布版本不能用新内容覆盖。
30
+
31
+ 本流程不从个人分支发布,也不会因推送标签自动发布。若发布时 `main` 已前进,提交校验会失败;应重新确认发布范围,不要移动旧标签。
32
+
33
+ 参考:https://docs.npmjs.com/trusted-publishers/
@@ -0,0 +1,36 @@
1
+ # 本机验收记录
2
+
3
+ ## 环境与范围
4
+
5
+ 2026-10-28,在 macOS、真实 Google Chrome 和 Harness 0.2.1-rc.3 上测试。测试模型为 OpenCode Go 提供的 `deepseek-v4.1-flash`。通过 Harness 的 headless 命令行启动独立会话,保留工具调用记录和最终报告。
6
+
7
+ 浏览器验收针对扩展 0.1.1,只操作本机专用测试页。Codex 没有同时控制这些页面。首次测试发现后台标签页收不到鼠标事件;增加 `Page.bringToFront` 后重新验收通过。
8
+
9
+ ## 真实浏览器结果
10
+
11
+ | 项目 | 结果与证据 |
12
+ |---|---|
13
+ | 页面读取 | 正确读取测试页标题、按钮和输入框;没有返回密码明文 |
14
+ | 输入、点击 | 显示“你好,DeepSeek 验收 123”;页面记录 `isTrusted=true` |
15
+ | 清空、键盘 | 空文本清空成功;Tab 将焦点移动到问候按钮 |
16
+ | 滚动 | `scrollY` 从 0 到 600,再返回 0 |
17
+ | 截图 | 保存 PNG,121,014 字节;Codex 检查图像,确认包含问候文字和 `isTrusted=true` |
18
+ | 错误采集 | 捕获控制台错误、JavaScript 异常和 HTTP 503;网络 URL 未带测试查询参数 |
19
+ | 失败路径 | 禁用按钮报 `ELEMENT_DISABLED`;调试脚本异常报 `JAVASCRIPT_ERROR` |
20
+ | 页面导航 | 切换页面后旧元素引用报 `STALE_ELEMENT`;返回首页后仍可操作 |
21
+ | 免逐页授权 | `release` 后再次 `claim` 成功,全程无需用户点击授权 |
22
+ | 页面限制 | 拒绝 `chrome://settings/`,没有创建内部页面标签 |
23
+ | 服务重启 | 另开 Harness 进程后,第一次查询未连接,等待 10 秒后恢复;全程未调用 `chrome_pair` |
24
+ | 收尾 | 测试页已释放,最终占用列表为空 |
25
+
26
+ ## 自动化与检查
27
+
28
+ Node.js 24.21.0 上的 22 项测试通过。覆盖配对令牌持久化、桥接服务重启、扩展 worker 启动重连、手动断开停止重连、会话占用、取消、超时、延迟 attach 清理、页面限制、输入和日志边界。
29
+
30
+ 还检查了 JavaScript 语法、扩展 manifest、包版本一致性和打包文件。源码经过完整差异检查及第二次简化检查。
31
+
32
+ ## 未覆盖范围
33
+
34
+ 浏览器验收之后新增的 worker 启动自动恢复和弹窗配对提示,通过了自动化测试;尚未在重新加载后的真实 Chrome 中单独验收。真实浏览器中的手动“断开连接”按钮及弹窗外观也尚未验收。自动化测试已检查对应断开和重连行为。
35
+
36
+ 这份记录只证明本机专用测试页上的功能。尚未覆盖复杂网站、跨域 iframe、Windows 或 Linux 的真实浏览器,也未完成 Chrome 扩展商店审核。
@@ -0,0 +1,122 @@
1
+ import { BrowserController } from './controller.js';
2
+ import { PORT, MAX_MESSAGE } from './protocol.js';
3
+
4
+ let socket;
5
+ let connected = false;
6
+ let connecting;
7
+ let running;
8
+ let lastError = '';
9
+ const controller = new BrowserController(chrome, tabId => {
10
+ if (connected) socket.send(JSON.stringify({ type: 'detached', tabId }));
11
+ });
12
+ const badge = text => { chrome.action.setBadgeText({ text }); chrome.action.setBadgeBackgroundColor({ color: '#185adb' }); };
13
+
14
+ async function disconnect(forget = false) {
15
+ connected = false;
16
+ running?.abort();
17
+ socket?.close();
18
+ socket = undefined;
19
+ await controller.reset();
20
+ if (forget) await chrome.storage.local.remove('token');
21
+ await chrome.storage.local.set({ enabled: false });
22
+ await chrome.alarms.clear('reconnect');
23
+ badge('');
24
+ }
25
+
26
+ async function connect(code) {
27
+ if (connected) return;
28
+ if (connecting) return connecting;
29
+ connecting = (async () => {
30
+ const { token } = await chrome.storage.local.get('token');
31
+ if (!code && !token) throw new Error('Ask Harness for a pairing code first.');
32
+ await controller.reset();
33
+ const client = new WebSocket(`ws://127.0.0.1:${PORT}/bridge`);
34
+ socket = client;
35
+ await new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => { client.close(); reject(new Error('Connection timed out. Check that the Harness plugin is running.')); }, 6000);
37
+ client.onopen = () => client.send(JSON.stringify({ ...(code ? { type: 'pair', code } : { type: 'auth', token }), version: chrome.runtime.getManifest().version }));
38
+ client.onmessage = async event => {
39
+ try {
40
+ if (typeof event.data !== 'string' || event.data.length > MAX_MESSAGE) throw new Error('Invalid bridge message');
41
+ const message = JSON.parse(event.data);
42
+ if (message.type === 'ready') {
43
+ if (message.protocol !== 1 || !/^[a-f0-9]{64}$/.test(message.token)) throw new Error('Unsupported bridge protocol');
44
+ await chrome.storage.local.set({ token: message.token, enabled: true });
45
+ if (socket !== client || client.readyState !== WebSocket.OPEN) return;
46
+ connected = true;
47
+ lastError = '';
48
+ clearTimeout(timer);
49
+ await chrome.alarms.create('reconnect', { periodInMinutes: 0.5 });
50
+ badge('ON');
51
+ resolve();
52
+ } else if (message.type === 'error') {
53
+ lastError = String(message.error).slice(0, 2000);
54
+ reject(new Error(lastError));
55
+ } else if (message.type === 'command' && connected) {
56
+ if (running) throw new Error('Browser is busy');
57
+ if (typeof message.id !== 'string' || !Number.isFinite(message.deadline) || message.deadline <= Date.now() || message.deadline > Date.now() + 20000) throw new Error('Invalid command deadline');
58
+ const cancellation = new AbortController();
59
+ running = cancellation;
60
+ const deadline = setTimeout(() => cancellation.abort(), message.deadline - Date.now());
61
+ let response;
62
+ try { response = { result: await controller.execute(message.action, message.args, cancellation.signal) }; }
63
+ catch (error) { response = { error: String(error.message).slice(0, 2000) }; }
64
+ finally { clearTimeout(deadline); if (running === cancellation) running = undefined; }
65
+ if (socket === client && client.readyState === WebSocket.OPEN) {
66
+ const body = JSON.stringify({ type: 'result', id: message.id, ...response });
67
+ client.send(body.length <= MAX_MESSAGE ? body : JSON.stringify({ type: 'result', id: message.id, error: 'RESULT_TOO_LARGE' }));
68
+ }
69
+ if (cancellation.signal.aborted) { client.close(); await controller.reset(); }
70
+ }
71
+ } catch (error) {
72
+ lastError = String(error.message);
73
+ client.close();
74
+ reject(error);
75
+ }
76
+ };
77
+ client.onerror = () => { lastError = 'Cannot reach Harness on this computer. Start Harness with dsh-chrome-use installed.'; };
78
+ client.onclose = () => {
79
+ clearTimeout(timer);
80
+ if (socket === client) {
81
+ connected = false;
82
+ socket = undefined;
83
+ running?.abort();
84
+ controller.reset().catch(error => { lastError = error.message; });
85
+ badge('');
86
+ }
87
+ reject(new Error(lastError || 'Connection closed. Reconnect to continue.'));
88
+ };
89
+ });
90
+ })();
91
+ try { await connecting; } finally { connecting = undefined; }
92
+ }
93
+
94
+ chrome.runtime.onMessage.addListener((message, sender, respond) => {
95
+ if (sender.id !== chrome.runtime.id || sender.url !== chrome.runtime.getURL('popup.html')) return false;
96
+ (async () => {
97
+ if (message.action === 'connect') {
98
+ const code = String(message.code ?? '').trim();
99
+ if (code && !/^\d{8}$/.test(code)) throw new Error('Enter the 8-digit code from Harness.');
100
+ await connect(code || undefined);
101
+ } else if (message.action === 'disconnect') await disconnect();
102
+ else if (message.action !== 'status') throw new Error('Unknown action');
103
+ const { token, enabled } = await chrome.storage.local.get(['token', 'enabled']);
104
+ return { connected, paired: Boolean(token), reconnecting: Boolean(enabled) && !connected, error: lastError, tabs: (await chrome.tabs.query({})).filter(tab => controller.attached.has(tab.id)).map(tab => ({ id: tab.id, title: tab.title, attached: true })) };
105
+ })().then(result => respond({ ok: true, ...result }), error => respond({ ok: false, error: error.message }));
106
+ return true;
107
+ });
108
+
109
+ async function restoreConnection() {
110
+ if (connected || connecting) return;
111
+ const { enabled } = await chrome.storage.local.get('enabled');
112
+ if (enabled) {
113
+ // Keep retrying if Harness is started after Chrome or the extension is reloaded.
114
+ await chrome.alarms.create('reconnect', { periodInMinutes: 0.5 });
115
+ await connect().catch(error => { lastError = error.message; });
116
+ }
117
+ }
118
+ chrome.alarms.onAlarm.addListener(alarm => {
119
+ if (alarm.name === 'reconnect') restoreConnection().catch(error => { lastError = error.message; });
120
+ });
121
+ chrome.runtime.onStartup.addListener(() => restoreConnection().catch(error => { lastError = error.message; }));
122
+ restoreConnection().catch(error => { lastError = error.message; });