diff --git a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.i18n.yaml deleted file mode 100644 index 52c95c707..000000000 --- a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.i18n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority. -2026-08-13-standalone-npm-workspace.md: 85494b6bae0445c83e7633f1bd5595a80723b13d -2026-08-13-standalone-npm-workspace.zh.md: 818751fc7e9cbc752b924aa3264b8073af1e73f2 diff --git a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.md b/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.md deleted file mode 100644 index 85494b6ba..000000000 --- a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: Standalone npm browser workspace - -Status: implemented - -English | [中文](2026-08-13-standalone-npm-workspace.zh.md) - -## Problem - -The browser bridge repository depended on a sibling DeepSeek Harness source checkout for TypeScript project references, workspace dependencies, test resolution, and the `dsh` executable. That topology prevented the repository from installing, building, testing, or running against a published npm release by itself. - -## Decision - -The repository is one pnpm workspace containing the bridge package and Chrome extension. Its root manifest installs a pinned `@deepseek-ai/dsh` release candidate, and the bridge package consumes the matching published `@deepseek-ai` packages through ordinary peer and development dependencies. - -Published SDK packages use the scoped Cordis and Schemastery identities. Bridge source and tests import those scoped packages directly so Cordis service declaration merging and runtime class identity remain singular. The bridge follows the published service names, including `webServer`, `workspaceRegistry`, `userQuestions`, and `dsh-home-paths`. - -The extension depends on the local bridge workspace package and imports the protocol through its `./src/*` export. The root build orders the bridge before the extension, while the bridge's built `protocol` export remains available to external consumers. - -The published `dsh` launcher resolves out-of-tree plugins from the selected profile rather than the invoking workspace. The installer therefore registers the built local bridge package with the `web` profile. The package's profile-layer activation contract is owned by the [browser profile bundle decision](2026-08-14-browser-profile-bundle.md). - -The project `.npmrc` selects the public registry without carrying credentials. The workspace explicitly allows only the install scripts required by the pinned dsh runtime and extension build. - -## Alternatives considered - -**Keep the host-checkout symlink.** This preserved source-level development but made the external repository non-installable and coupled every command to an unrelated parent workspace, defeating npm release validation. - -**Alias scoped Cordis packages under their former unscoped names.** This preserved import spelling but could install two module identities beside published Harness packages, breaking Context declaration merging and class compatibility. Direct scoped imports keep one runtime and type identity. - -## Verification - -The root `typecheck`, `build`, and `test` scripts cover both workspace packages. The bridge suite boots the published Loader and Harness services, and its Chromium test exercises discovery, WebSocket authentication, gateway RPC, deferred session creation, and workspace grouping through the built extension. Startup smoke tests boot the registered `web` profile through both the workspace-pinned launcher and the public npm launcher, then read the discovery endpoint. - -## Consequences - -A clean checkout installs and runs without Harness source or npm credentials, and the lockfile records the complete public npm dependency graph. Updating the npm release requires coordinated manifest, API, lockfile, and end-to-end verification changes. Installation executes the explicitly allowlisted native build steps pulled in by the dsh runtime. diff --git a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.zh.md b/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.zh.md deleted file mode 100644 index 818751fc7..000000000 --- a/.agents/notes/implemented/architecture/2026-08-13-standalone-npm-workspace.zh.md +++ /dev/null @@ -1,35 +0,0 @@ -# Agent Note: 独立 npm 浏览器 workspace - -Status: implemented - -[English](2026-08-13-standalone-npm-workspace.md) | 中文 - -## 问题 - -浏览器桥仓库曾依赖同级的 DeepSeek Harness 源码 checkout,以提供 TypeScript 项目引用、workspace 依赖、测试解析与 `dsh` 可执行文件。这种拓扑使仓库无法独立针对已发布的 npm 版本完成安装、构建、测试或运行。 - -## 决策 - -该仓库是一个包含桥接包与 Chrome 扩展的 pnpm workspace。根 manifest(元数据清单)安装固定版本的 `@deepseek-ai/dsh` 候选发布版,桥接包则通过普通的对等依赖(peer dependency)与开发依赖使用与之匹配的已发布 `@deepseek-ai` 包。 - -已发布的 SDK 包使用带 scope 的 Cordis 与 Schemastery 标识。桥接源码与测试直接导入这些带 scope 的包,使 Cordis 服务声明合并与运行时类标识始终保持单一。桥接包遵循已发布的服务名称,包括 `webServer`、`workspaceRegistry`、`userQuestions` 与 `dsh-home-paths`。 - -扩展依赖本地桥接 workspace 包,并通过其 `./src/*` export 导入协议。根构建会先构建桥接包、再构建扩展;桥接包构建后的 `protocol` export 仍可供外部消费方使用。 - -已发布的 `dsh` 启动器解析外部插件时,以所选 profile 而非发起调用的 workspace 为准。因此,安装器会把构建后的本地桥接包注册到 `web` profile。包的 profile 层激活契约由[浏览器 profile bundle 决策](2026-08-14-browser-profile-bundle.zh.md)统一规定。 - -项目 `.npmrc` 只选择公共注册表,不携带凭据。该 workspace 仅明确放行固定版本的 dsh 运行时与扩展构建所需的安装脚本。 - -## 替代方案 - -**保留指向宿主 checkout 的符号链接。**这样可以保留源码级开发方式,但会使外部仓库无法安装,并将每条命令与无关的父 workspace 绑定,从而无法验证 npm 发布版。 - -**在 Cordis 包过去不带 scope 的名称下为带 scope 的包设置别名。**这样可以保留 import 的拼写,但可能在已发布的 Harness 包旁边安装两个模块标识,破坏 Context 声明合并与类兼容性。直接导入带 scope 的包,可以保持唯一的运行时与类型标识。 - -## 验证 - -根目录的 `typecheck`、`build` 与 `test` 脚本覆盖两个 workspace 包。桥接套件会启动已发布的 Loader 与 Harness 服务,其 Chromium 测试通过构建后的扩展覆盖发现、WebSocket 认证、网关 RPC、延迟会话创建与 workspace 分组。启动冒烟测试会分别通过 workspace 固定的启动器与公共 npm 启动器运行已注册的 `web` profile,并读取发现端点。 - -## 后果 - -干净 checkout 无需 Harness 源码或 npm 凭据即可安装与运行,lockfile 会记录完整的公共 npm 依赖图。更新 npm 发布版时,需要协同修改 manifest、API、lockfile 与端到端验证。安装会执行由 dsh 运行时拉取、已明确加入允许清单的原生构建步骤。 diff --git a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.i18n.yaml deleted file mode 100644 index c0783a384..000000000 --- a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.i18n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority. -2026-08-14-browser-profile-bundle.md: f0a8b607830ec3fac435b38a3ac911c5ba2e2fae -2026-08-14-browser-profile-bundle.zh.md: 5807f8aadf44e2c83d5cdf1aa313830489fd13dc diff --git a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.md b/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.md deleted file mode 100644 index f0a8b6078..000000000 --- a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.md +++ /dev/null @@ -1,39 +0,0 @@ -# Agent Note: Browser profile bundle - -Status: implemented - -English | [中文](2026-08-14-browser-profile-bundle.zh.md) - -## Problem - -Installing the bridge as a plain profile dependency made its JavaScript resolvable but did not mount it in the `web` composition. The repository compensated with a root `--patch` file, so `pnpm start` could load the bridge while the standard `npx @deepseek-ai/dsh web` command could not. The two launchers therefore had different behavior and the bridge activation contract lived outside its package. - -## Decision - -The bridge package declares `dsh.bundle.patch` as `./cordis.patch.yml`, exports that file, and includes it in the published file set. The installer adds the linked package to the local `web` profile, allowing the Harness profile manager to register its patch layer. - -`pnpm start` runs the workspace-pinned `dsh web`, while `npx @deepseek-ai/dsh web` runs npm's current public release. After installation, both commands resolve the same local profile bundle without a repository-relative `--patch` argument. - -The bridge build invokes `tsdown` through the workspace script path. Runtime configuration, build tools, and plugin code all live inside this repository or its declared package graph; no command resolves dependencies from a parent checkout or parent `node_modules` directory. - -The installer supports two source modes before running the same build and registration path. When it resides in a complete checkout, it uses that checkout and never downloads or overwrites source files. When streamed without a workspace, it downloads the `main` archive into the installer-owned `~/.dsh/dsh-browser` directory, validates the workspace shape, and re-executes the installed copy. A marker distinguishes the managed directory from user-owned content; remote installation refuses to overwrite a non-empty unmarked directory. - -The managed workspace persists because the `web` profile links the bridge package by its local path. Re-running the remote command refreshes that workspace while retaining pnpm's `node_modules`; re-running a cloned installer continues to use its current branch. - -The installer updates the stable unpacked-extension directory in place and distinguishes first installation from reloads. The extension treats the former loopback default as automatic discovery, probes a local bridge before opening each WebSocket, and retries discovery when the panel opens or the keepalive alarm fires. This avoids routine connection-refused errors while dsh is absent and connects after it becomes available. - -## Alternatives considered - -**Keep the root overlay and document an extra npx flag.** This would preserve two activation paths and would not make the standard npm command work as documented by Harness. - -**Copy the patch into both the root and the package.** Duplicate configuration could drift or mount the plugin twice. The package owns the single profile layer. - -**Require every user to clone the repository.** A clone is useful when editing or selecting branches, but Git and checkout management are unnecessary for ordinary installation. The managed archive preserves the local package path required by profile linking without exposing that workflow to users. - -## Verification - -A clean lockfile install checks the public release dependency graph. Package assembly confirms that `cordis.patch.yml` is included. An isolated-home shell smoke exercises both workspace detection and streamed bootstrap, including managed updates and refusal to overwrite an unmarked path. Isolated-home startup smokes add the bundle to a fresh `web` profile, boot both `pnpm start` and `npx @deepseek-ai/dsh web`, and require a successful `/ext/bridge-config` response. Extension tests require a successful availability probe before constructing a loopback WebSocket. - -## Consequences - -The installer must run once for each dsh home that should expose browser control. Managed installation trades Git-free onboarding for an installer-owned source directory that users must not treat as a working copy; developers retain ordinary clone semantics. The unversioned remote command follows the repository's current `main`, and the unversioned npx command follows npm's current dist-tag, so release changes still require compatibility verification; the workspace lockfile remains the reproducible dependency path for each selected source revision. diff --git a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.zh.md b/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.zh.md deleted file mode 100644 index 5807f8aad..000000000 --- a/.agents/notes/implemented/architecture/2026-08-14-browser-profile-bundle.zh.md +++ /dev/null @@ -1,39 +0,0 @@ -# Agent Note: 浏览器 profile bundle - -Status: implemented - -[English](2026-08-14-browser-profile-bundle.md) | 中文 - -## 问题 - -把桥接包装成普通 profile 依赖只能让 JavaScript 可解析,不能把它挂载到 `web` 组合。仓库曾用根目录 `--patch` 文件补足这一步,因此 `pnpm start` 可以加载桥接插件,标准的 `npx @deepseek-ai/dsh web` 命令却不能。两种启动器的行为并不一致,而且桥接插件的激活契约位于包外。 - -## 决策 - -桥接包把 `dsh.bundle.patch` 声明为 `./cordis.patch.yml`,导出该文件并将其纳入发布文件集合。安装器把链接形式的包添加到本机 `web` profile,让 Harness profile 管理器注册它的 patch 层。 - -`pnpm start` 运行 workspace 固定版本的 `dsh web`,`npx @deepseek-ai/dsh web` 则运行 npm 当前的公开版本。安装完成后,两种命令都会解析同一个本机 profile bundle,无需仓库相对路径形式的 `--patch` 参数。 - -桥接包通过 workspace 脚本路径调用 `tsdown`。运行时配置、构建工具与插件代码全部位于本仓库或其声明的包依赖图中;任何命令都不会从父 checkout 或父目录的 `node_modules` 解析依赖。 - -安装器在进入同一条构建和注册路径之前支持两种源码模式。脚本位于完整 checkout 中时,直接使用该 checkout,绝不下载或覆盖源码文件。脚本在没有 workspace 的情况下通过流式方式运行时,会把 `main` 分支压缩包下载到安装器持有的 `~/.dsh/dsh-browser` 目录,验证 workspace 结构,再重新执行已安装的脚本副本。标记文件用于区分托管目录和用户内容;非空目录如果没有该标记,远程安装会拒绝覆盖。 - -托管 workspace 必须保留,因为 `web` profile 会通过本机路径链接桥接包。再次运行远程命令会刷新该 workspace,同时保留 pnpm 的 `node_modules`;再次运行 clone 中的安装器仍会使用它当前所在的分支。 - -安装器会在原地更新稳定的已解压扩展目录,并区分首次安装与重新加载。扩展把原先的本机默认地址视为自动探测,在每次创建本机 WebSocket 前先探测桥接服务,并在打开侧边栏或保活闹钟触发时重试探测。这样可以避免 dsh 尚未运行时反复记录拒绝连接错误,并在服务可用后建立连接。 - -## 替代方案 - -**保留根叠加配置并为 npx 记录额外参数。**这样会继续维护两条激活路径,也无法让标准 npm 命令按照 Harness 的文档直接工作。 - -**在根目录与包内各复制一份 patch。**重复配置可能产生漂移或把插件挂载两次。profile 层只由该包统一持有。 - -**要求所有用户 clone 仓库。**需要修改源码或选择分支时,clone 很有用;普通安装不需要 Git 和 checkout 管理。托管压缩包保留了 profile 链接所需的本机包路径,同时无需用户处理这套流程。 - -## 验证 - -使用干净 lockfile 安装检查公开发布版的依赖图;通过包组装确认包含 `cordis.patch.yml`。隔离 home 的 shell 冒烟测试覆盖 workspace 检测和流式自举,包括托管更新及拒绝覆盖未标记路径。在隔离的 home 中执行启动冒烟测试:把 bundle 添加到全新的 `web` profile,分别启动 `pnpm start` 与 `npx @deepseek-ai/dsh web`,并要求 `/ext/bridge-config` 成功响应。扩展测试要求本机可用性探测成功后才创建 WebSocket。 - -## 后果 - -每个需要浏览器操作能力的 dsh home 都必须先运行一次安装器。托管安装以安装器持有且用户不应作为工作副本修改的源码目录,换取无需 Git 的安装体验;开发者仍保留普通 clone 语义。未指定版本的远程命令会跟随仓库当前的 `main`,未指定版本的 npx 命令会跟随 npm 当前 dist-tag,因此发布变化后仍需验证兼容性;对于每个选定的源码版本,workspace lockfile 继续提供可复现的依赖路径。 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7c1d615cc..c7b3a9de3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,40 +1,9 @@ - - - ## 概述 / Summary - - - -## 动机 / Motivation - - - - -## 改动内容 / Changes - - - - -- - -## 测试 / Testing - - - - -- - -## 影响范围 / Impact - - - - -- + + -## Checklist +## 验证 / Testing -- [ ] 已在相关平台实测通过 / Tested on the relevant platforms -- [ ] 已通过 `pnpm run build` / `bash -n scripts/install.sh` 等基础检查 / Passed basic checks -- [ ] 文档已同步更新(如需要)/ Docs updated if needed -- [ ] 不包含无关改动 / No unrelated changes included + + diff --git a/.github/workflows/uninstall.yml b/.github/workflows/uninstall.yml new file mode 100644 index 000000000..a4fd776b9 --- /dev/null +++ b/.github/workflows/uninstall.yml @@ -0,0 +1,31 @@ +name: Uninstall lifecycle + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + uninstall: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + env: + TEST_PWSH: '1' + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + with: + run_install: false + - run: pnpm install --frozen-lockfile + - run: pnpm --filter dsh-browser-extension build + - run: pnpm test:uninstall diff --git a/.gitignore b/.gitignore index 97861b955..94a9f66a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ node_modules/ -**/node_modules/ **/lib/ !benchmark/lib/ !benchmark/lib/** **/dist/ **/dist-firefox/ +coverage/ .artifacts/ diff --git a/README.i18n.yaml b/README.i18n.yaml deleted file mode 100644 index 02268f125..000000000 --- a/README.i18n.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Bilingual consistency record for the repository README files. -# README.md is the default English page; README.zh.md is its Chinese counterpart. -# Values are git blob hashes from the last confirmed-consistent review. -README.md: bd8959e81cbc1bb1016d1f686ade8b59bb6c32f4 -README.zh.md: ba858f21da58c435cdf21098b2a683e6b54e54b4 diff --git a/README.md b/README.md index bd8959e81..a13e9eb28 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Connect [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) to t Pages become structured text with a numbered inventory of interactive elements. Tools address elements by number or selector; `browser_screenshot` captures the controlled tab's viewport after approval. The side panel also accepts PNG, JPEG, WebP, and GIF attachments when the host advertises image support. > [!IMPORTANT] -> The workspace targets the latest public dsh release, currently pinned to 0.2.1-rc.5. Older DSH releases are not supported. +> The workspace is pinned to DSH 0.2.1-rc.5. ## Quick install -The standard `dsh plugin` command alone cannot install this project. The integration contains both a dsh bridge plugin and a browser extension. The one-line installer currently sets up the Chrome build. +The installer installs the bridge plugin and Chrome extension. macOS and Linux: @@ -67,57 +67,41 @@ The paired Playwright / extension duration ratio was **1.24** (95% CI **1.16–1 Clicks, text entry, and scrolling run in the selected document, including cross-origin iframes. Chrome uses its debugger for native keyboard events, caret insertion, system paste, and file uploads. Screenshots require an active tab in a focused window; `browser_follow_tab { activate: true }` brings the controlled page to the front. -## Repository layout - -``` -packages/browser/bridge-browser/ - cordis.patch.yml -extensions/dsh-browser/ -scripts/install.sh -scripts/install.ps1 -``` - -## Why this design - -- **Your real browser, not a headless copy**: the model works in the page you already have open, retaining logins, sessions, and cookies. -- **A text-first page interface**: numbered controls, stable IDs across snapshots, delta updates, and masked sensitive values make pages operable without screenshots; user-attached chat images use dsh's separate multimodal message path. -- **Pointing instead of describing**: highlight the passage you mean and the side panel quotes it, so "explain this" needs no page tour. The quote is captured only while a panel is open, and nothing is sent until you send the message. -- **A narrow privacy boundary**: passwords and payment-card values are always rendered as `••••` and never leave the page. -- **A guarded bridge**: authenticated handshakes protect remote connections, privileged gateway methods reject non-loopback callers, and the extension binds tools to one user-controlled tab. - ## Detailed installation and usage Requirements: Node.js `>=24`, Corepack/pnpm, and Chrome 125+ or Firefox 140+. Windows additionally needs Windows PowerShell 5.1, which ships with Windows, or PowerShell 7+. ### Install or update -For a managed installation, run: +Run the [quick install command](#quick-install) to install or update. The installer opens `chrome://extensions`; load `~/.dsh/browser-extension` as an unpacked extension the first time, or click **Reload** after an update. Restart dsh if it is already running. + +To install the current branch from a source checkout instead: ```sh -curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash +git clone https://github.com/Lum1104/dsh-browser.git +cd dsh-browser +./scripts/install.sh ``` -or, on Windows: +On Windows, run `.\scripts\install.ps1` from the checkout instead. After pulling or switching revisions, rerun the installer and reload the extension. -```powershell -$s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s -``` +### Uninstall -The installer downloads `main`, builds and registers the bridge plugin, builds the Chrome extension into `~/.dsh/browser-extension`, and opens `chrome://extensions`. On the first install, load that directory as an unpacked extension; on updates, click **Reload**. Restart dsh if it is already running. +Open **Settings → Uninstall dsh-browser** in the extension, copy the command, and run it in Terminal. -`scripts/install.sh` covers macOS and Linux, and `scripts/install.ps1` covers Windows; both write the same managed workspace and the same install metadata. The installer copies the extension path to the clipboard when a clipboard tool is available (`pbcopy`, `wl-copy`, `xclip`, `xsel`, or PowerShell's `Set-Clipboard`), and prints the path either way. When no Chrome or Chromium install is found, it prints the command that installs one; set `DSH_INSTALL_BROWSER=1` to let the installer attempt that install itself. +You can also run it directly. On macOS or Linux: -The Windows command downloads `install.ps1` and runs it rather than piping it into `Invoke-Expression`: the script is UTF-8 with a byte order mark so Windows PowerShell renders its Chinese output, and `Invoke-Expression` rejects a leading mark. Local checkout paths may contain spaces; the installer registers the bridge through a profile-local directory junction so the package spec never contains the absolute Windows path. +```sh +curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.sh | bash +``` -To install the current branch from a source checkout instead: +On Windows: -```sh -git clone https://github.com/Lum1104/dsh-browser.git -cd dsh-browser -./scripts/install.sh +```powershell +& { $ErrorActionPreference = 'Stop'; $d=New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) ('dsh-browser-uninstall.'+[Guid]::NewGuid().ToString('N'))); try { $s=Join-Path $d.FullName 'uninstall.ps1'; Invoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s } finally { Remove-Item -LiteralPath $d.FullName -Recurse -Force } } ``` -On Windows, run `.\scripts\install.ps1` from the checkout instead. After pulling or switching revisions, rerun the installer and reload the extension. +From a checkout, run `bash scripts/uninstall.sh` or `.\scripts\uninstall.ps1`. ### Firefox source build @@ -150,15 +134,17 @@ Local Chrome use requires no configuration; Firefox requires the local bridge to **Side panel stays "Not connected"** -- Make sure dsh web is running locally (default `http://127.0.0.1:3080`). -- Verify the bridge is loaded: open `http://127.0.0.1:3080/ext/bridge-config`. It should return JSON such as `{"wsUrl":"ws://127.0.0.1:3080/ext/bridge"}`. If it returns a web page instead of JSON, the running dsh predates the bridge registration — restart dsh and refresh the page; the extension reconnects on its own. -- Discovery runs once when the panel opens. A filled-in address is tried first; if a **loopback** address does not answer, the panel makes one automatic pass (last working URL → `dsh web` defaults → the plugin's fixed beacon ports 43189–43198). When that pass finds nothing the status reads **No service found** and nothing retries until you reopen the panel or press **Detect**. -- DSH Desktop assigns a random local Web port on every launch, which is why the bridge plugin also listens on a fixed loopback beacon port. If the panel cannot find it, open **Settings → Detect** to list every local instance and click the one to use; the address can still be typed by hand. Remote `--host` deployments need the address and token filled in manually; Firefox always requires the token. +- Make sure DSH is running. After installing or updating, restart DSH and reopen the side panel. +- Open **Settings → Detect** and select your DSH instance. +- In Firefox, enter the token from `~/.dsh/ext-bridge-token` in Settings. +- For a remote DSH instance, enter its server address and bridge token in Settings. ## Development The bridge plugin and Chrome/Firefox extension are both members of this repository's workspace. Run all commands from the repository root. For the first development installation, run `pnpm install`. +Package documentation: [browser bridge](packages/browser/bridge-browser/README.md) and [browser extension](extensions/dsh-browser/README.md). + ```sh pnpm run build pnpm run typecheck @@ -178,23 +164,18 @@ pnpm --filter dsh-browser-extension run test Notes: - The bridge plugin must have a built `lib/` before startup because the loader consumes it; both `scripts/install.sh` and the root `pnpm run build` build the plugin before the extension. -- The bridge build copies its browser client with Node.js `copyFileSync`, so the same package script works without a Unix `cp` executable. - The dependencies of `@deepseek-ai/dsh` and the bridge plugin are pinned to the same tested public release line. An upgrade must update the manifests and lockfile together and rerun the root checks. `check:runtime` checks the resolved DSH dependencies and lockfile; `test:smoke` starts the real web host in a temporary DSH home and verifies the bridge and session reads after a restart, without model credentials. CI runs these checks after a clean installation. After building, run `pnpm test:browser` with a Playwright Chromium installation, or set `CHROMIUM_PATH` to a Chromium executable that supports loading unpacked extensions. This starts real DSH and Chromium instances to verify sharing controls, developer-tools consent, input, iframe uploads and targeting, and CSS edits. Site isolation is enabled and verified for cross-site frames; element clicks, input, and scrolling cover same-origin, cross-origin, and nested frames. It needs no model credentials and saves results under `.artifacts/browser-smoke/`. -If you encounter `cache.hydratePrepared is not a function`, update the repository, rerun `pnpm install --frozen-lockfile` and `pnpm run build`, then restart `pnpm start`. Session data and the global package cache can be kept. +After building the extension, run `pnpm test:uninstall`. Run `pnpm test:uninstall:ui` for the settings UI tests; Playwright Chromium is required. ## Security -- The bridge path sits outside the `/api` trust boundary and performs its own bearer-token authentication. -- Local Chrome extension origins retain zero-configuration loopback access; Firefox origins are per-install UUIDs and must present the bearer token. -- Privileged gateway methods such as `settings.*`, `credentials.*`, and `host.open*` reject non-loopback sources. -- Text snapshots mask password and payment-card values. Screenshots capture visible pixels without field masking and require approval unless unrestricted control is enabled; page sharing set to `off` blocks screenshots under restricted control. Screenshots and attached chat images use dsh's durable attachment service. Developer tools have a separate high-privilege consent gate. -- When work begins, the assistant binds to the active tab (at prompt submission, or at the first direct browser-tool call). If you switch tabs manually, later browser actions pause and the side panel asks whether the assistant should stay on the original page or follow the current one, and offers "jump back to the assistant page" to move your own tab back to the assistant's page. Choosing the original page permits background operation; apart from that button the extension never silently retargets or changes your visible tab. Closing the controlled tab also pauses tools until you explicitly select the current page. -- Text you highlight is captured only while a side panel is open and page sharing is not `off`, and never from password or payment-card fields. It stays inside the extension until you send the message, is dropped when you dismiss it or its page navigates or closes, and reaches the model inside the same untrusted-content boundary as page snapshots — including its source title and URL, which the page also controls. -- Page-authored text is wrapped as untrusted input. The default `auto` mode reads only the controlled tab without an extra prompt; privacy-sensitive users can select `ask` for per-read confirmation or `off` to block reads entirely. In `ask` mode, the read dialog can allow one read or persistently switch back to `auto`; this can be reversed in Settings. Read page text is sent to the selected model. -- Click, type, keypress, navigation, history, and reload calls fail closed until the user approves them. An origin may be trusted for the current side-panel session (cleared when the last panel closes or the service worker restarts), while permanent trust is managed explicitly in Settings. Explicit cross-origin `browser_navigate` calls and unknown history destinations always prompt again. -- **Allow unrestricted browser control** is an explicit global opt-in. It becomes active only after the setting is saved successfully; while enabled, page reads, page actions, and tab list/follow/close operations run without approval prompts. Calls capture their access mode when received, so enabling unrestricted control never retroactively elevates an existing restricted call. Disabling it takes effect immediately, cancels calls that have not dispatched an action, waits for already-dispatched browser operations to settle, and only then saves the restrictive setting. A rapid re-enable remains restricted until that revocation finishes, and concurrent saves persist in request order. Browser-protected DOM content remains inaccessible in either mode. +- Page text read by the assistant and attachments you send are passed to the selected model. +- Page content sharing offers **Share automatically** (default), **Ask every time**, and **Off**. +- Text snapshots mask password and payment-card values. Screenshots capture visible pixels without masking. +- Page operations require approval by default. You can trust individual sites or enable **Allow unrestricted browser control**. Developer tools require separate authorization. +- When you switch tabs, the assistant asks which page to continue working on. diff --git a/README.zh.md b/README.zh.md index ba858f21d..eaf7be6b0 100644 --- a/README.zh.md +++ b/README.zh.md @@ -11,11 +11,11 @@ 页面会转换为结构化文本和带编号的交互元素清单。工具通过编号或选择器定位元素;`browser_screenshot` 经批准后截取受控标签页的可视区域。宿主声明图片能力时,侧栏也可发送 PNG、JPEG、WebP 和 GIF 附件。 > [!IMPORTANT] -> 工作区针对最新公开发布的 dsh,目前固定使用 0.2.1-rc.5;不支持旧版 DSH。 +> 工作区固定使用 DSH 0.2.1-rc.5。 ## 快速安装 -本项目不能只使用标准的 `dsh plugin` 命令安装。它同时包含 dsh bridge plugin 和浏览器扩展。一行安装器目前会安装 Chrome 构建。 +安装器会安装桥插件和 Chrome 扩展。 macOS 与 Linux: @@ -67,57 +67,41 @@ Playwright / 扩展的配对耗时比为 **1.24**(95% CI **1.16–1.34**): 点击、文本输入和滚动操作在指定文档中执行,支持跨域 iframe。Chrome 通过调试器执行原生键盘事件、光标位置插入、系统粘贴和文件上传。截图要求受控标签页处于活动状态且窗口聚焦,可以使用 `browser_follow_tab { activate: true }` 将页面切到前台。 -## 组成 - -``` -packages/browser/bridge-browser/ - cordis.patch.yml -extensions/dsh-browser/ -scripts/install.sh -scripts/install.ps1 -``` - -## 为什么这样设计 - -- **使用你的真实浏览器,而不是无头副本**:模型操作你已经打开的页面,登录态、会话和 Cookie 均会保留。 -- **纯文本页面接口**:编号控件、跨快照稳定 ID、delta 更新和敏感值掩码,使模型无需截图也能操作页面;用户主动添加的对话图片走 dsh 独立的多模态消息通道。 -- **用「指」代替「描述」**:直接划选你要问的那段文字,侧栏会把它引用下来,说「解释这个」不必再描述整页内容。只有侧栏打开时才会捕获,并且在你发送消息之前不会离开浏览器。 -- **收窄隐私边界**:密码和支付卡字段始终显示为 `••••`,字段值不会离开页面。 -- **受保护的桥连接**:远程连接使用认证握手,特权网关方法拒绝非回环调用方,扩展把工具绑定到一个由用户控制的标签页。 - ## 详细安装与使用 前置要求:Node.js `>=24`、Corepack/pnpm,以及 Chrome 125+ 或 Firefox 140+。Windows 还需要系统自带的 Windows PowerShell 5.1,或 PowerShell 7+。 ### 安装或更新 -托管安装请运行: +运行[快速安装命令](#快速安装)即可安装或更新。安装器打开 `chrome://extensions` 后,首次安装时加载 `~/.dsh/browser-extension` 目录;更新后点击**重新加载**。如果 dsh 已在运行,请重启。 + +如需从源码 checkout 安装当前分支: ```sh -curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash +git clone https://github.com/Lum1104/dsh-browser.git +cd dsh-browser +./scripts/install.sh ``` -Windows 请运行: +Windows 请在 checkout 中运行 `.\scripts\install.ps1`。拉取或切换版本后,请重新运行安装器并重新加载扩展。 -```powershell -$s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s -``` +### 卸载 -安装器会下载 `main`、构建并注册桥插件、把 Chrome 扩展构建到 `~/.dsh/browser-extension`,然后打开 `chrome://extensions`。首次安装时,请把该目录作为已解压扩展加载;更新时点击**重新加载**。如果 dsh 已在运行,请重启。 +在扩展的**设置 → 卸载 dsh-browser**中复制命令,然后在终端运行。 -`scripts/install.sh` 覆盖 macOS 与 Linux,`scripts/install.ps1` 覆盖 Windows;两者写入同一个托管工作区和同一份安装元数据。当系统提供剪贴板工具(`pbcopy`、`wl-copy`、`xclip`、`xsel` 或 PowerShell 的 `Set-Clipboard`)时,安装器会把扩展路径复制到剪贴板;无论是否复制成功都会打印该路径。若未检测到 Chrome/Chromium,安装器会打印对应的安装命令;设置 `DSH_INSTALL_BROWSER=1` 可让安装器尝试自动安装。 +也可以直接运行。macOS 或 Linux: -Windows 命令先下载 `install.ps1` 再执行,而不是管道给 `Invoke-Expression`:脚本是带 BOM 的 UTF-8,Windows PowerShell 依赖 BOM 才能正确显示中文,而 `Invoke-Expression` 无法处理开头的 BOM。本地 checkout 路径可以包含空格;安装器通过 profile 内的目录联接注册桥插件,因此包规格中不会出现 Windows 绝对路径。 +```sh +curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.sh | bash +``` -如需从源码 checkout 安装当前分支: +Windows 请运行: -```sh -git clone https://github.com/Lum1104/dsh-browser.git -cd dsh-browser -./scripts/install.sh +```powershell +& { $ErrorActionPreference = 'Stop'; $d=New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) ('dsh-browser-uninstall.'+[Guid]::NewGuid().ToString('N'))); try { $s=Join-Path $d.FullName 'uninstall.ps1'; Invoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s } finally { Remove-Item -LiteralPath $d.FullName -Recurse -Force } } ``` -Windows 请在 checkout 中运行 `.\scripts\install.ps1`。拉取或切换版本后,请重新运行安装器并重新加载扩展。 +在源码目录中运行 `bash scripts/uninstall.sh` 或 `.\scripts\uninstall.ps1`。 ### Firefox 源码构建 @@ -150,15 +134,17 @@ Chrome 本机使用无需配置;Firefox 需要填写上述本地桥 token。 **侧边栏一直显示「未连接」** -- 确认本机 dsh web 正在运行(默认 `http://127.0.0.1:3080`)。 -- 确认桥接已加载:浏览器打开 `http://127.0.0.1:3080/ext/bridge-config`,应返回类似 `{"wsUrl":"ws://127.0.0.1:3080/ext/bridge"}` 的 JSON。如果返回的是网页而不是 JSON,说明当前运行的 dsh 早于桥接注册——重启 dsh 并刷新页面即可,扩展会自动重连。 -- 面板打开时探测一次:已填地址优先尝试;回环地址连不上时再自动探测一次(上次成功的地址 → `dsh web` 默认端口 → 插件的固定信标端口 43189–43198)。都没找到时状态显示「未找到服务」,在重新打开面板或点「自动检测」之前不会重试。 -- DSH Desktop 每次启动都会重新随机分配本地 Web 端口,所以桥接插件同时监听一个固定回环信标端口。面板找不到时,打开**设置 → 自动检测**列出本机所有实例,点击其中一条即可填入地址;地址仍可手动填写。远程 `--host` 部署需要手动填写地址和 token;Firefox 始终需要 token。 +- 确认 DSH 正在运行。安装或更新后,重启 DSH 并重新打开侧栏。 +- 打开**设置 → 自动检测**,选择需要连接的 DSH 实例。 +- 使用 Firefox 时,在设置中填写 `~/.dsh/ext-bridge-token` 中的 token。 +- 连接远程 DSH 时,在设置中填写服务器地址和对应的桥接 token。 ## 开发 桥接插件和 Chrome/Firefox 扩展都属于本仓库 workspace;所有命令均在本仓库根目录执行。首次开发安装运行 `pnpm install`。 +开发文档:[浏览器桥插件](packages/browser/bridge-browser/README.zh.md)与[浏览器扩展](extensions/dsh-browser/README.zh.md)。 + ```sh pnpm run build pnpm run typecheck @@ -178,23 +164,18 @@ pnpm --filter dsh-browser-extension run test 注意: - 启动前桥接插件必须已有 `lib/` 供 Loader 加载;`scripts/install.sh` 和根目录 `pnpm run build` 都会先构建插件再构建扩展。 -- 桥构建使用 Node.js `copyFileSync` 复制浏览器客户端,因此同一条包脚本不依赖 Unix `cp` 命令。 - `@deepseek-ai/dsh` 与桥接插件的依赖固定在同一条经过验证的公开发布线上;升级时必须同时更新 manifest、锁文件并重跑根目录检查。 `check:runtime` 检查实际解析的 DSH 依赖和锁文件;`test:smoke` 使用临时 DSH home 启动真实 web 宿主,验证桥接和重启后的会话读取,无需模型密钥。CI 在干净安装后运行这些检查。 构建后运行 `pnpm test:browser`,需要已安装 Playwright Chromium,或通过 `CHROMIUM_PATH` 指定支持加载未打包扩展的 Chromium 可执行文件。测试会启动真实 DSH 和 Chromium,验证页面共享、开发者工具授权、输入、iframe 上传与定位,以及 CSS 修改。测试开启并确认跨站 iframe 的进程隔离,覆盖同源、跨源和嵌套 iframe 中的元素点击、输入与滚动。测试无需模型密钥,结果保存在 `.artifacts/browser-smoke/`。 -如果遇到 `cache.hydratePrepared is not a function`,更新仓库后重新运行 `pnpm install --frozen-lockfile` 和 `pnpm run build`,再重启 `pnpm start`。无需删除会话数据或清空全局缓存。 +构建扩展后运行 `pnpm test:uninstall`。设置界面测试使用 `pnpm test:uninstall:ui`,需要 Playwright Chromium。 ## 安全 -- 桥路径在 `/api` 信任栅栏之外,自带 bearer token 认证。 -- Chrome 扩展的本地 Origin 保留零配置回环访问;Firefox Origin 是每次安装生成的 UUID,必须携带 bearer token。 -- 特权网关方法(`settings.*`/`credentials.*`/`host.open*`)对非回环来源一律拒绝。 -- 文本快照会遮盖密码和卡号值。截图直接捕获可见像素,不会遮盖字段;除非启用了完全控制,否则截图需要审批,页面共享设为「关闭」时会阻止截图。截图和对话图片使用 dsh 持久附件服务。开发者工具有独立的高权限授权。 -- 助手开始工作时会绑定当时的活动标签页(提交提示时绑定;直接调用浏览器工具时则在首次调用绑定)。用户手动切页后,后续浏览器操作会暂停,侧栏会询问让助手留在原页面还是跟随当前页面,并提供「跳转回到助手页面」把你自己正在看的标签页切回助手页面;选择留在原页面后允许在后台继续,除该按钮外扩展不会切换用户正在看的页面、也不会静默改绑。受控标签页关闭后也会暂停,直到用户显式选择当前页面。 -- 只有在侧栏打开、且页面共享不是「关闭」时才会捕获划选内容,密码和卡号字段永不读取。内容在发送之前始终留在扩展内部;移除、页面跳转或标签页关闭都会丢弃它;发送时与页面快照一样包在不可信内容边界内,来源标题和 URL 同样由页面提供,因此也放在边界之内。 -- 网页文字会标记为不可信输入。默认「自动共享」只按需读取受控标签页且不额外弹窗;对隐私敏感时可选择「每次询问」,或用「关闭」完全阻断读取。在「每次询问」模式下,读取弹窗可以仅允许一次,也可以持久切回自动读取;之后仍可在设置中关闭。读取的页面文字会发送给当前选择的模型。 -- 点击、输入、按键、导航、历史跳转和刷新默认失败关闭,必须由用户批准。可以只在当前侧栏会话中信任单个 origin(最后一个侧栏关闭或 Service Worker 重启即清空);永久信任需在设置中显式管理。显式跨域 `browser_navigate` 和未知目标的历史跳转始终重新询问。 -- **允许模型完全控制浏览器**是显式的全局选择,只有设置保存成功后才会生效。启用后,页面读取、页面操作和标签页列出/跟随/关闭都不会再请求确认。调用在收到时固定其访问模式,因此开启完全控制不会追溯提升已经开始的受限调用。关闭会立即生效:取消尚未下发操作的调用,等待已经下发到浏览器的操作完成,再保存限制设置。快速重新开启也会在旧权限撤销完成前保持受限,并发保存会按请求顺序落盘。无论是否启用,浏览器受保护页面的 DOM 内容都无法访问。 +- 助手读取的页面文字和你发送的附件会交给当前选择的模型。 +- 页面内容共享可以选择**自动共享**(默认)、**每次询问**或**关闭**。 +- 文本快照会遮盖密码和卡号值;截图保留可见像素,不会遮盖字段。 +- 页面操作默认需要审批。你可以信任指定网站,或开启**允许模型完全控制浏览器**。开发者工具需要单独授权。 +- 切换标签页后,助手会询问接下来操作哪个页面。 diff --git a/extensions/dsh-browser/.gitignore b/extensions/dsh-browser/.gitignore deleted file mode 100644 index b94707787..000000000 --- a/extensions/dsh-browser/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -dist/ diff --git a/extensions/dsh-browser/README.i18n.yaml b/extensions/dsh-browser/README.i18n.yaml deleted file mode 100644 index 6f5a3ba27..000000000 --- a/extensions/dsh-browser/README.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write dsh-browser/extensions/dsh-browser/README.md -README.md: b7616050fb86b97cdca40a50fdebb2c86aa3e797 -README.zh.md: 473461e54defd70e2c07389576f791e81feb8de8 diff --git a/extensions/dsh-browser/README.md b/extensions/dsh-browser/README.md index b7616050f..00e400b8f 100644 --- a/extensions/dsh-browser/README.md +++ b/extensions/dsh-browser/README.md @@ -6,7 +6,6 @@ The **browser-operation end** of dsh: the model reads and operates the browser p Browser pages are rendered as structured text with a numbered interactive-element inventory. `browser_screenshot` can also capture the controlled viewport after approval. When the DSH host advertises image support, the side panel accepts PNG, JPEG, WebP, and GIF attachments and renders their durable history references. - ## What the model can do | Capability | Action | Notes | @@ -32,15 +31,6 @@ Browser pages are rendered as structured text with a numbered interactive-elemen ## Architecture -``` -side panel (React) ◄─port─► background SW/event page ◄─WS─► dsh bridge plugin - │ - tabs.sendMessage (DSH_ACTION, DSH_SELECTION_WATCH) - ▲ DSH_SELECTION - ▼ - content script (snapshot/actions/privacy/selection) -``` - - **background** (`src/background/`): bridge connection (token auth + exponential-backoff reconnect + keepalive), gateway RPC client, and **fail-closed tool dispatch to a user-controlled tab**. - **content script** (`src/content/`): text-only snapshot (readability main text + numbered interactive inventory + form fields), **stable element numbers** (`data-dsh-el`), delta changes, click/type/press/scroll/navigate actions, sensitive-field masking, and a debounced selection watcher that stays disarmed until a side panel is open and page sharing is not `off`. - **panel** (`src/panel/`): React conversation UI (resumable session/history/live events/settings); host-advertised image limits gate image selection and preflight checks, durable attachments render through session-authorized reads, messages render as sanitized Markdown, `ask_user_question` requests render as answerable cards, manual tab switches render a control-handoff strip, an active turn exposes a standard stop control, and a highlighted page passage renders as a removable quote that the next prompt carries inside the untrusted-content boundary. @@ -57,59 +47,11 @@ pnpm --filter dsh-browser-extension run test Run these commands from the repository root. Chrome outputs to `extensions/dsh-browser/dist/`; Firefox outputs to `extensions/dsh-browser/dist-firefox/`. -## Install and use - -The recommended zero-configuration command does not require Git or a local clone: - -1. **Build and install the extension**: - - ```sh - curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash - ``` - - On Windows, run this in PowerShell instead: - - ```powershell - $s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s - ``` - - The script downloads a managed workspace to `~/.dsh/dsh-browser`, builds the bridge plugin, registers its official bundle in the local dsh `web` profile, builds the extension, copies the output to the stable directory `~/.dsh/browser-extension`, and opens `chrome://extensions`. Enable Developer mode, choose Load unpacked, and select the extension directory. Running the command again updates the managed installation. - - A cloned checkout uses the same installer without downloading or overwriting source files: +For extension development, load `extensions/dsh-browser/dist/` from `chrome://extensions`, or load `extensions/dsh-browser/dist-firefox/manifest.json` from `about:debugging#/runtime/this-firefox`. Rebuild and reload after code changes. - ```sh - git clone https://github.com/Lum1104/dsh-browser.git - cd dsh-browser - ./scripts/install.sh - ``` - - Windows checkouts run `.\scripts\install.ps1` instead. Paths containing spaces are supported: the installer uses a profile-local directory junction instead of putting the absolute checkout path in pnpm's link spec. - -2. **Start dsh with the bridge plugin mounted**. Use either the workspace-pinned runtime: - - ```sh - cd ~/.dsh/dsh-browser && pnpm start - ``` - - From a clone, run `pnpm start` in the repository root instead. - - Or use the exact supported public runtime: - - ```sh - npx @deepseek-ai/dsh@0.2.1-rc.5 web - ``` - - Both commands load the same bundle from the local `web` profile. Port 3080 is used by default; append `--port ` when it is occupied. - - **DSH Desktop users**: the Desktop app launches its host with `--port 0`, so the OS assigns a different local Web port on every start and no fixed port list can predict it. The bridge plugin therefore also listens on a fixed loopback discovery beacon (`43189`–`43198`, first free port) that advertises the real bridge URL. Discovery runs once when the panel opens: a filled-in address is tried first, and only a loopback address that does not answer falls back to one automatic pass. When nothing is found the status reads "No service found" and nothing retries; open **Settings → Detect** to list every local instance and pick one, or type `http://127.0.0.1:` by hand. To pin the port instead, override the `webserver` row in `~/.dsh/profiles/desktop/cordis.patch.yml` — patch `config` is replaced wholesale, so repeat `host` as well. - - Loading or reloading the extension is passive: it does not probe local ports or open a WebSocket until the side panel is opened. A healthy connection established by the user may remain available for background approvals after the panel closes, but it will not reconnect without an open panel if it drops or another browser replaces it. - -3. **Use it**: open a normal `http://` or `https://` page and click the DeepSeek whale icon. Both builds auto-discover local dsh. Chrome loopback connections need no address or token; Firefox must be given the token from `~/.dsh/ext-bridge-token` because a `moz-extension://` UUID is not an add-on identity. Chat directly or click "Read page" first. - -Pages that were already open before extension installation or reload are instrumented automatically on the first action, so they do not require a manual refresh. Browser-internal and protected pages such as `chrome://` and the Chrome Web Store expose only tab metadata and browser-level HTTP(S) navigation, back, forward, and reload; their DOM cannot be read or operated. +## Install and use -For extension-only development, load `extensions/dsh-browser/dist/` from `chrome://extensions`, or run `build:firefox` and load `extensions/dsh-browser/dist-firefox/manifest.json` from `about:debugging#/runtime/this-firefox`. Rebuild and reload after code changes. +See the root README for [installation, updates, startup, and uninstall](../../README.md#detailed-installation-and-usage). ## Composer controls: permission, model, commands, and skills @@ -179,7 +121,5 @@ Chrome uses `sidePanel`; Firefox uses `sidebar_action`. Both request `storage` ( - Developer tools require Chrome 125+, a setting/session grant or per-call approval, and an available debugger connection. Chrome shows its debugging banner while the connection remains attached, including during network capture. - Tab affinity is global to that extension connection rather than per chat session. - Accessible cross-origin iframes are snapshotted and operated with stable `(frame, index)` addresses. Restricted or short-lived frames are reported as unavailable without failing the whole page snapshot. -- Captcha/image-only controls cannot be handled — the tool result reports "elements with no accessible name" and asks the user to complete that step manually. - No automatic token rotation. - Synthetic `browser_press` events do not trigger browser-native default actions such as Tab focus movement, arrow-key scrolling, or Enter activation. Chrome's trusted input channel supports these actions; Firefox uses synthetic input. -- `browser_wait` considers page load plus a fixed quiet window, but does not observe continuously changing DOM state; a live-updating SPA may be reported as stable. diff --git a/extensions/dsh-browser/README.zh.md b/extensions/dsh-browser/README.zh.md index 473461e54..76238e0d7 100644 --- a/extensions/dsh-browser/README.zh.md +++ b/extensions/dsh-browser/README.zh.md @@ -6,7 +6,6 @@ dsh 的**浏览器操作端**:让模型直接读取并操作你在浏览器里 浏览器页面以结构化文本和带编号的交互元素清单呈现。`browser_screenshot` 也可经批准后截取受控页面的可视区域。DSH 宿主声明图片能力时,侧栏接收 PNG、JPEG、WebP 和 GIF,并渲染会话中的持久图片附件。 - ## 模型能做什么 | 能力 | 动作 | 说明 | @@ -32,15 +31,6 @@ dsh 的**浏览器操作端**:让模型直接读取并操作你在浏览器里 ## 架构 -``` -side panel (React) ◄─port─► background SW/事件页 ◄─WS─► dsh bridge plugin - │ - tabs.sendMessage (DSH_ACTION, DSH_SELECTION_WATCH) - ▲ DSH_SELECTION - ▼ - content script (snapshot/actions/privacy/selection) -``` - - **background**(`src/background/`):桥连接(token 认证 + 指数退避重连 + 保活)、网关 RPC 客户端,以及**失败关闭地分发工具到用户受控标签页**。 - **content script**(`src/content/`):纯文本快照(可读性主文 + 编号交互清单 + 表单字段)、**稳定编号**(`data-dsh-el`)、delta 变化、点击/输入/按键/滚动/导航动作、敏感字段掩码,以及带防抖的划选监听——只有侧栏打开且页面共享不是「关闭」时才会启用。 - **panel**(`src/panel/`):React 对话界面(可续接会话/历史/实时事件/设置);图片选择和预检由宿主声明的限制控制,持久图片通过会话授权读取;消息以已消毒的 Markdown 渲染,`ask_user_question` 请求会显示成可直接作答的卡片,手动切页时显示控制权交接条,运行中的回合提供标准停止按钮,页面划选的段落显示为可移除的引用卡片,并在下一条消息中包在不可信内容边界里发送。 @@ -57,59 +47,11 @@ pnpm --filter dsh-browser-extension run test 请在仓库根目录执行这些命令。Chrome 产物输出到 `extensions/dsh-browser/dist/`;Firefox 产物输出到 `extensions/dsh-browser/dist-firefox/`。 -## 安装与使用 - -推荐的零配置命令无需安装 Git,也无需提前 clone: - -1. **构建并安装扩展**: - - ```sh - curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash - ``` - - Windows 请改在 PowerShell 中运行: - - ```powershell - $s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s - ``` - - 脚本会把托管 workspace 下载到 `~/.dsh/dsh-browser`,构建桥插件,把它的官方 bundle 注册到本机 dsh 的 `web` profile,再构建扩展并把产物复制到稳定目录 `~/.dsh/browser-extension`,然后打开 `chrome://extensions`。开启开发者模式,选择「加载已解压的扩展程序」,加载扩展目录。再次运行该命令会更新托管安装。 - - clone 得到的 checkout 也使用同一个安装器,而且不会下载或覆盖源码: +开发扩展时,Chrome 从 `chrome://extensions` 加载 `extensions/dsh-browser/dist/`;Firefox 从 `about:debugging#/runtime/this-firefox` 加载 `extensions/dsh-browser/dist-firefox/manifest.json`。代码更新后需重新构建并重新加载。 - ```sh - git clone https://github.com/Lum1104/dsh-browser.git - cd dsh-browser - ./scripts/install.sh - ``` - - Windows checkout 请运行 `.\scripts\install.ps1`。路径可以包含空格:安装器通过 profile 内的目录联接注册插件,不会把 checkout 的绝对路径写入 pnpm link 规格。 - -2. **启动 dsh 并挂载桥插件**。可以使用 workspace 固定的运行时: - - ```sh - cd ~/.dsh/dsh-browser && pnpm start - ``` - - 如果使用 clone,请改为在仓库根目录运行 `pnpm start`。 - - 或使用受支持的精确公开版本: - - ```sh - npx @deepseek-ai/dsh@0.2.1-rc.5 web - ``` - - 两种命令都会从本机 `web` profile 加载同一个 bundle。默认端口为 3080;如被占用,可追加 `--port `。 - - **DSH Desktop 用户**:桌面版启动 Host 时用 `--port 0`,本地 Web 端口每次启动都由系统重新分配,固定端口列表无法预知。因此桥接插件会同时在固定的回环信标端口(`43189`–`43198`,取第一个空闲端口)上通告真实桥地址。面板打开时探测一次:已填地址优先尝试,只有回环地址连不上时才自动探测一次。都没找到时状态显示「未找到服务」,不再重试;此时打开**设置 → 自动检测**列出本机所有实例并点选,或直接手填 `http://127.0.0.1:<端口>`。若想固定端口,可在 `~/.dsh/profiles/desktop/cordis.patch.yml` 覆盖 `webserver` 行——patch 的 `config` 是整体替换,务必把 `host` 一起写出。 - - 加载或重新加载扩展本身是被动的:只有打开侧栏后,扩展才会探测本机端口并创建 WebSocket。用户已建立的健康连接可在侧栏关闭后继续用于后台审批;但连接一旦掉线或被另一浏览器替换,没有打开侧栏时就不会重连。 - -3. **开始使用**:打开普通的 `http://` 或 `https://` 页面,点击 DeepSeek 鲸鱼图标打开侧边栏。两个构建都会自动探测本机 dsh。Chrome 回环连接无需地址或 Token;Firefox 的 `moz-extension://` UUID 不能证明扩展身份,必须在设置中填入 `~/.dsh/ext-bridge-token`。可以直接对话,或先点「读取页面」。 - -页面即使在扩展安装或重载之前已经打开,也会在第一次操作时自动补加载内容脚本,无需手动刷新。`chrome://`、Chrome Web Store 等浏览器内置或受保护页面只提供标签页元数据,以及浏览器级 HTTP(S) 导航、后退、前进和刷新;不能读取或操作其 DOM。 +## 安装与使用 -如果只开发扩展,Chrome 从 `chrome://extensions` 加载 `extensions/dsh-browser/dist/`;Firefox 运行 `build:firefox` 后,从 `about:debugging#/runtime/this-firefox` 加载 `extensions/dsh-browser/dist-firefox/manifest.json`。代码更新后需重新构建并重新加载。 +安装、更新、启动和卸载方式见根目录 README 的[详细安装与使用](../../README.zh.md#详细安装与使用)。 ## 输入区控制:权限模式、模型、命令与 Skill @@ -151,7 +93,5 @@ Chrome 使用 `sidePanel`,Firefox 使用 `sidebar_action`。两者都申请 `s - 开发者工具需要 Chrome 125 以上、设置或会话授权或逐次审批,以及可用的调试连接。连接保持期间会显示 Chrome 的调试提示,包括网络抓包期间。 - 标签页绑定属于整个扩展连接,而不是单个对话会话。 - 可访问的跨源 iframe 会进入快照,并通过稳定的 `(frame, index)` 地址执行操作;受保护或已销毁的 frame 会标记为不可访问,不影响整页快照。 -- 验证码/纯图片按钮无法处理——工具结果会标注"存在无文本可访问名的元素",提示用户手动完成该步。 - 令牌无自动轮换。 - `browser_press` 的合成按键不触发浏览器原生默认行为,例如 Tab 焦点移动、方向键滚动或 Enter 激活。Chrome 的真实输入通道支持这些操作;Firefox 使用合成输入。 -- `browser_wait` 以加载完成 + 固定静默窗口为准,不观察持续 DOM 更新(连续刷新的 SPA 可能被报为稳定)。 diff --git a/extensions/dsh-browser/assets/icons/deepseek.ico b/extensions/dsh-browser/assets/icons/deepseek.ico deleted file mode 100644 index 8e78c0b72..000000000 Binary files a/extensions/dsh-browser/assets/icons/deepseek.ico and /dev/null differ diff --git a/extensions/dsh-browser/package.json b/extensions/dsh-browser/package.json index f7877e569..b16fbac25 100644 --- a/extensions/dsh-browser/package.json +++ b/extensions/dsh-browser/package.json @@ -1,6 +1,6 @@ { "name": "dsh-browser-extension", - "description": "Chrome and Firefox MV3 extension: sidebar chat with a local dsh instance and text-only read/operate of a user-controlled tab through the dsh browser bridge", + "description": "Chrome and Firefox MV3 extension: sidebar chat and browser tools for a local dsh instance", "version": "0.1.4", "author": "Yuxiang Lin", "license": "MIT", @@ -30,7 +30,6 @@ "jsdom": "^26.1.0", "typescript": "^5.6.2", "vite": "^6.0.0", - "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.11" } } diff --git a/extensions/dsh-browser/scripts/build.mjs b/extensions/dsh-browser/scripts/build.mjs index 29fc3d2ea..e1c0138fe 100644 --- a/extensions/dsh-browser/scripts/build.mjs +++ b/extensions/dsh-browser/scripts/build.mjs @@ -1,8 +1,9 @@ /** - * Build all three extension targets sequentially into dist/ (or dist-firefox/ + * Build extension targets sequentially into dist/ (or dist-firefox/ * with --firefox): - * background (es|iife) → content (iife) → panel (React). The first target - * cleans the output; the later ones append. Pass --watch for dev rebuilds. + * background (es|iife) → content (iife) → panel (React) → offscreen (iife). + * The first target cleans the output; the later ones append. + * Pass --watch for dev rebuilds. */ import { spawn, spawnSync } from 'node:child_process' @@ -24,7 +25,7 @@ const configs = [ ] if (watch) { - // 三个 watcher 并行启动(串行时第一个永不停机,后面的永远不会启动)。 + // 并行启动各个构建目标的 watcher。 const children = configs.map((config) => spawn('vite', ['build', '--config', config, '--watch'], { cwd: root, stdio: 'inherit', diff --git a/extensions/dsh-browser/src/background/devtools-controller.ts b/extensions/dsh-browser/src/background/devtools-controller.ts index 67b35a2ed..bdf263c42 100644 --- a/extensions/dsh-browser/src/background/devtools-controller.ts +++ b/extensions/dsh-browser/src/background/devtools-controller.ts @@ -784,9 +784,3 @@ function matcher(pattern: unknown): ((url: string) => boolean) | undefined { } return (url) => url.includes(pattern) } - -/** Serialize one capture state for the panel. */ -export function captureStatusText(capture: NetworkCapture): string { - const state = capture.state() - return `Network capture ${state.attached ? 'active' : 'inactive'} · ${state.entries} entries · generation ${state.generation}${state.tabId === undefined ? '' : ` · tab ${state.tabId}`}` -} diff --git a/extensions/dsh-browser/src/background/devtools-elements.ts b/extensions/dsh-browser/src/background/devtools-elements.ts index 9810c810d..dd82fb885 100644 --- a/extensions/dsh-browser/src/background/devtools-elements.ts +++ b/extensions/dsh-browser/src/background/devtools-elements.ts @@ -418,15 +418,6 @@ const LONGHANDS_OF: Record = { inset: ['top', 'right', 'bottom', 'left'], } -/** Which shorthand family a property belongs to; longhands belong to their own. */ -export function propertyFamily(name: string): string { - const normalized = name.trim().toLowerCase() - for (const [shorthand, longhands] of Object.entries(LONGHANDS_OF)) { - if (normalized === shorthand || longhands.includes(normalized)) return shorthand - } - return normalized -} - /** * Canonicalize one declaration block. * diff --git a/extensions/dsh-browser/src/background/devtools-format.ts b/extensions/dsh-browser/src/background/devtools-format.ts index 8ed13b272..4c554f52d 100644 --- a/extensions/dsh-browser/src/background/devtools-format.ts +++ b/extensions/dsh-browser/src/background/devtools-format.ts @@ -17,7 +17,7 @@ import { REDACTED, truncateText, } from './devtools-serialization.ts' -import type { ConsoleEntry, ConsoleLevel, NetworkEntry } from './devtools-network.ts' +import type { ConsoleEntry, NetworkEntry } from './devtools-network.ts' import type { ElementView } from './devtools-elements.ts' /** Render one inspected element report. */ export function renderElement(view: ElementView): string { @@ -99,11 +99,6 @@ export function renderConsoleLogs(entries: readonly ConsoleEntry[], sinceIndex: return lines.join('\n') } -/** Render one evaluation result, already formatted by the serialization layer. */ -export function renderEvaluation(text: string, ok: boolean): string { - return `${ok ? 'Evaluation result:' : 'Evaluation failed:'}\n${text}` -} - /** Render the capture control answer. */ export function renderCaptureState(state: { attached: boolean; tabId?: number; entries: number; generation: number }, action: string): string { const lines = [`Network capture ${state.attached ? 'active' : 'inactive'}.`] @@ -204,8 +199,3 @@ export function renderBody( if (options.truncated) lines.push('', '(body truncated to the configured budget)') return lines.join('\n') } - -/** Levels are exposed so the tool layer can validate its own filter input. */ -export function isConsoleLevel(value: string): value is ConsoleLevel { - return value === 'log' || value === 'info' || value === 'warning' || value === 'error' || value === 'exception' -} diff --git a/extensions/dsh-browser/src/background/input-state.ts b/extensions/dsh-browser/src/background/input-state.ts index 855ed582e..662afb3a4 100644 --- a/extensions/dsh-browser/src/background/input-state.ts +++ b/extensions/dsh-browser/src/background/input-state.ts @@ -4,7 +4,7 @@ * @module */ -import { MODIFIER_BITS, modifierMask, type ModifierName } from './keys.ts' +import { modifierMask, type ModifierName } from './keys.ts' /** One tab's input state. */ export class InputState { @@ -61,6 +61,3 @@ export class InputStateStore { this.states.clear() } } - -/** Modifier bitmask constant re-exported for callers that build chords. */ -export { MODIFIER_BITS } diff --git a/extensions/dsh-browser/src/panel/App.tsx b/extensions/dsh-browser/src/panel/App.tsx index 32fadfc69..14be854bc 100644 --- a/extensions/dsh-browser/src/panel/App.tsx +++ b/extensions/dsh-browser/src/panel/App.tsx @@ -38,6 +38,7 @@ import { import { canReturnToControlledTab } from './tab-handoff.ts' import { QuestionCard } from './QuestionCard.tsx' import { UpdateCard } from './UpdateCard.tsx' +import { UninstallCard } from './UninstallCard.tsx' import { MessageImages } from './MessageImages.tsx' import type { QuestionAnswer } from './questions.ts' import { @@ -2868,6 +2869,7 @@ export function App(): React.JSX.Element {

{copy.settings.snapshotHint(caps?.snapshotMaxChars ?? DEFAULT_SNAPSHOT_MAX_CHARS)}

+ {approvalDialog} ) } diff --git a/extensions/dsh-browser/src/panel/UninstallCard.tsx b/extensions/dsh-browser/src/panel/UninstallCard.tsx new file mode 100644 index 000000000..869430268 --- /dev/null +++ b/extensions/dsh-browser/src/panel/UninstallCard.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react' +import type { PanelCopy } from './strings.ts' +import { readExtensionInstallInfo, type ExtensionInstallInfo } from './updates.ts' +import { uninstallCommand } from './uninstall.ts' + +export function UninstallCard({ copy }: { copy: PanelCopy['uninstall'] }): React.JSX.Element { + const [info, setInfo] = useState(null) + const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle') + useEffect(() => { + let active = true + void readExtensionInstallInfo(chrome.runtime.getURL('install-info.json')).then(value => { + if (active) setInfo(value) + }) + return () => { active = false } + }, []) + const command = info === null ? null : uninstallCommand(info) + + return ( +
+ {copy.title} +
+

{copy.runCommand}

+ {command !== null &&
{command}
} +
+ +
+ {copyState === 'error' ? copy.copyError : copyState === 'copied' ? copy.copied : ''} +
+
+ ) +} diff --git a/extensions/dsh-browser/src/panel/permissions.ts b/extensions/dsh-browser/src/panel/permissions.ts index 9df44680c..288a256db 100644 --- a/extensions/dsh-browser/src/panel/permissions.ts +++ b/extensions/dsh-browser/src/panel/permissions.ts @@ -32,9 +32,6 @@ export interface SessionPermissionDirectory { options: PermissionPresetOption[] } -/** Built-in preset values whose labels the panel owns, exactly like the Web GUI. */ -const BUILT_IN_PRESETS = new Set(['read-only', 'workspace-write', 'danger-full-access', 'auto']) - /** Presets that change the risk posture and therefore need a confirmation. */ const RISKY_PRESETS = new Set(['danger-full-access', 'auto']) @@ -116,11 +113,6 @@ export function isExperimentalPreset(value: string): boolean { return value === 'auto' } -/** Whether the value is one the panel localizes itself. */ -export function isBuiltInPreset(value: string): boolean { - return BUILT_IN_PRESETS.has(value) -} - /** The exact Host command line that switches a Session preset. */ export function permissionSwitchLine(value: string): string { return `/permission ${value}` diff --git a/extensions/dsh-browser/src/panel/strings.ts b/extensions/dsh-browser/src/panel/strings.ts index 7a321fd4b..245ac0ec6 100644 --- a/extensions/dsh-browser/src/panel/strings.ts +++ b/extensions/dsh-browser/src/panel/strings.ts @@ -178,6 +178,13 @@ export interface PanelCopy { copied: string copyError: string } + uninstall: { + title: string + runCommand: string + copyCommand: string + copied: string + copyError: string + } textSize: { open: string title: string @@ -480,6 +487,13 @@ const EN: PanelCopy = { copied: 'Command copied', copyError: 'Could not copy the command. Run the installer from the original installation source instead.', }, + uninstall: { + title: 'Uninstall dsh-browser', + runCommand: 'Run this command in Terminal.', + copyCommand: 'Copy uninstall command', + copied: 'Command copied', + copyError: 'Copy failed', + }, textSize: { open: 'Text size', title: 'Text size', @@ -784,6 +798,13 @@ const ZH: PanelCopy = { copied: '命令已复制', copyError: '无法复制命令,请回到原安装来源重新运行安装脚本。', }, + uninstall: { + title: '卸载 dsh-browser', + runCommand: '在终端运行以下命令。', + copyCommand: '复制卸载命令', + copied: '命令已复制', + copyError: '复制失败', + }, textSize: { open: '字号', title: '字号', diff --git a/extensions/dsh-browser/src/panel/styles.css b/extensions/dsh-browser/src/panel/styles.css index 36ded073d..f214b7c77 100644 --- a/extensions/dsh-browser/src/panel/styles.css +++ b/extensions/dsh-browser/src/panel/styles.css @@ -2115,6 +2115,36 @@ textarea:focus-visible { box-shadow: 0 6px 18px rgba(23, 32, 51, 0.05); } +.uninstall-card summary { + padding: 14px; + cursor: pointer; + color: var(--danger); + font-weight: 650; +} + +.uninstall-content { + display: flex; + flex-direction: column; + gap: 10px; + padding: 0 14px 14px; + font-size: calc(11px * var(--ui-scale)); + line-height: 1.6; + overflow-wrap: anywhere; +} + +.uninstall-content p { + margin: 0; +} + +.uninstall-command { + margin: 0; + padding: 10px; + border-radius: 8px; + background: var(--blue-wash); + white-space: pre-wrap; + user-select: text; +} + .update-card { position: relative; display: flex; diff --git a/extensions/dsh-browser/src/panel/uninstall.ts b/extensions/dsh-browser/src/panel/uninstall.ts new file mode 100644 index 000000000..b3942b06c --- /dev/null +++ b/extensions/dsh-browser/src/panel/uninstall.ts @@ -0,0 +1,25 @@ +import { powerShellQuote, shellQuote, type ExtensionInstallInfo } from './updates.ts' + +const SCRIPT_ROOT = 'https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts' + +export function uninstallCommand(info: ExtensionInstallInfo, userAgent: string = navigator.userAgent): string { + const home = info.mode === 'unknown' ? undefined : info.dshHome + const windows = /windows/i.test(userAgent) + if (home !== undefined) { + const separator = windows ? '\\' : '/' + const source = info.mode === 'checkout' ? info.sourceRoot : `${home}${separator}dsh-browser` + const script = `${source}${separator}scripts${separator}uninstall.${windows ? 'ps1' : 'sh'}` + return windows + ? `powershell -NoProfile -ExecutionPolicy Bypass -File ${powerShellQuote(script)} --home ${powerShellQuote(home)}` + : `bash ${shellQuote(script)} --home ${shellQuote(home)}` + } + if (windows) { + return '& { $ErrorActionPreference = \'Stop\'; ' + + '$d=New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) (\'dsh-browser-uninstall.\'+[Guid]::NewGuid().ToString(\'N\'))); ' + + 'try { $s=Join-Path $d.FullName \'uninstall.ps1\'; ' + + `Invoke-WebRequest -UseBasicParsing ${SCRIPT_ROOT}/uninstall.ps1 -OutFile $s; ` + + 'powershell -NoProfile -ExecutionPolicy Bypass -File $s } ' + + 'finally { Remove-Item -LiteralPath $d.FullName -Recurse -Force } }' + } + return `curl -fsSL ${SCRIPT_ROOT}/uninstall.sh | bash` +} diff --git a/extensions/dsh-browser/src/panel/updates.ts b/extensions/dsh-browser/src/panel/updates.ts index ad33b3575..8d6282d53 100644 --- a/extensions/dsh-browser/src/panel/updates.ts +++ b/extensions/dsh-browser/src/panel/updates.ts @@ -28,8 +28,8 @@ export interface ExtensionUpdateResult { } export type ExtensionInstallInfo = - | { mode: 'managed' } - | { mode: 'checkout'; sourceRoot: string } + | { mode: 'managed'; dshHome?: string } + | { mode: 'checkout'; sourceRoot: string; dshHome?: string } | { mode: 'unknown' } /** A drive-letter root (C:\dsh-browser) or a UNC share (\\host\share\dsh-browser). */ @@ -45,14 +45,17 @@ function isAbsolutePath(value: string): boolean { /** Parse installer-written provenance without trusting malformed local data. */ export function parseExtensionInstallInfo(value: unknown): ExtensionInstallInfo { if (typeof value !== 'object' || value === null) return { mode: 'unknown' } - const info = value as { schemaVersion?: unknown; mode?: unknown; sourceRoot?: unknown } + const info = value as { schemaVersion?: unknown; mode?: unknown; sourceRoot?: unknown; dshHome?: unknown } if (info.schemaVersion !== 1) return { mode: 'unknown' } - if (info.mode === 'managed') return { mode: 'managed' } + if (info.dshHome !== undefined && (typeof info.dshHome !== 'string' + || !isAbsolutePath(info.dshHome) || /[\u0000-\u001f]/u.test(info.dshHome))) return { mode: 'unknown' } + const location = typeof info.dshHome === 'string' ? { dshHome: info.dshHome } : {} + if (info.mode === 'managed') return { mode: 'managed', ...location } if (info.mode === 'checkout' && typeof info.sourceRoot === 'string' && isAbsolutePath(info.sourceRoot) && !info.sourceRoot.includes('\0')) { - return { mode: 'checkout', sourceRoot: info.sourceRoot } + return { mode: 'checkout', sourceRoot: info.sourceRoot, ...location } } return { mode: 'unknown' } } @@ -71,12 +74,12 @@ export async function readExtensionInstallInfo( } } -function shellQuote(value: string): string { +export function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'` } -function powerShellQuote(value: string): string { - return `'${value.replaceAll("'", "''")}'` +export function powerShellQuote(value: string): string { + return `'${value.replace(/['\u2018-\u201b]/gu, quote => quote + quote)}'` } /** @@ -91,8 +94,7 @@ export function checkoutInstallCommand(sourceRoot: string): string { } /** - * A managed install records no path, so the panel picks the installer by the platform it is - * running on — which is the machine the extension was installed to. + * Select the managed installer for the browser's current platform. */ export function managedInstallCommand(userAgent: string = navigator.userAgent): string { return /windows/i.test(userAgent) ? WINDOWS_UPDATE_COMMAND : UPDATE_COMMAND diff --git a/extensions/dsh-browser/tests/panel-styles.spec.ts b/extensions/dsh-browser/tests/panel-styles.spec.ts deleted file mode 100644 index 19cefba0a..000000000 --- a/extensions/dsh-browser/tests/panel-styles.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -// @vitest-environment jsdom -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -describe('panel layout styles', () => { - it('keeps the settings view within the viewport so overflowing content scrolls', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - const settingsRule = styles.match(/\.settings\s*\{([^}]*)\}/)?.[1] - - expect(settingsRule).toBeDefined() - expect(settingsRule).toMatch(/(?:^|\n)\s*height:\s*100vh;/) - expect(settingsRule).toMatch(/(?:^|\n)\s*height:\s*100dvh;/) - expect(settingsRule).toMatch(/(?:^|\n)\s*overflow-y:\s*auto;/) - expect(settingsRule).toMatch(/(?:^|\n)\s*overscroll-behavior:\s*contain;/) - }) - - it('keeps settings sections at their natural height so the view can overflow', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - const settingsChildrenRule = styles.match(/\.settings\s*>\s*\*\s*\{([^}]*)\}/)?.[1] - - expect(settingsChildrenRule).toBeDefined() - expect(settingsChildrenRule).toMatch(/(?:^|\n)\s*flex-shrink:\s*0;/) - }) - - it('places the composer control chips on their own row with upward menus', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - const controlsRule = styles.match(/\.composer-controls\s*\{([^}]*)\}/)?.[1] - const chipRule = styles.match(/\.composer-model,\s*\n\.composer-chip\s*\{([^}]*)\}/)?.[1] - const menuRule = styles.match(/\.composer-model-menu\s*\{([^}]*)\}/)?.[1] - const chipMenuRule = styles.match(/\.composer-chip-menu\s*\{([^}]*)\}/)?.[1] - - // The chip row sits under the composer box and wraps on a narrow panel. - expect(controlsRule).toBeDefined() - expect(controlsRule).toMatch(/(?:^|\n)\s*display:\s*flex;/) - expect(controlsRule).toMatch(/(?:^|\n)\s*flex-wrap:\s*wrap;/) - expect(controlsRule).toMatch(/(?:^|\n)\s*margin-top:\s*8px;/) - expect(chipRule).toBeDefined() - expect(chipRule).toMatch(/(?:^|\n)\s*position:\s*relative;/) - expect(menuRule).toBeDefined() - expect(menuRule).toMatch(/(?:^|\n)\s*position:\s*absolute;/) - expect(menuRule).toMatch(/(?:^|\n)\s*bottom:\s*calc\(100% \+ 6px\);/) - expect(chipMenuRule).toBeDefined() - expect(chipMenuRule).toMatch(/(?:^|\n)\s*position:\s*absolute;/) - expect(chipMenuRule).toMatch(/(?:^|\n)\s*bottom:\s*calc\(100% \+ 6px\);/) - }) - - it('renders the slash menu inside the composer shell so it can point upward', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - const composerRule = styles.match(/\.composer\s*\{([^}]*)\}/)?.[1] - const slashRule = styles.match(/\.slash-menu\s*\{([^}]*)\}/)?.[1] - - expect(composerRule).toBeDefined() - expect(composerRule).toMatch(/(?:^|\n)\s*position:\s*relative;/) - expect(slashRule).toBeDefined() - expect(slashRule).toMatch(/(?:^|\n)\s*position:\s*absolute;/) - expect(slashRule).toMatch(/(?:^|\n)\s*overscroll-behavior:\s*contain;/) - }) - - it('renders the developer-tools state inside its own switch, not as a row', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - - // The switch keeps one settings entry, with a row wrapper only so the - // attached state can offer a detach action beside it. - const rowRule = styles.match(/\.settings\s+\.setting-toggle-row\s*\{([^}]*)\}/)?.[1] - expect(rowRule).toBeDefined() - expect(rowRule).toMatch(/(?:^|\n)\s*display:\s*flex;/) - expect(rowRule).toMatch(/(?:^|\n)\s*padding:\s*13px;/) - expect(rowRule).toMatch(/(?:^|\n)\s*border-bottom:\s*1px solid var\(--line\);/) - - // The inner switch must not draw a second border of its own. - const innerLabelRule = styles.match(/\.settings\s+\.setting-toggle-row\s*>\s*label\.setting-toggle\s*\{([^}]*)\}/)?.[1] - expect(innerLabelRule).toBeDefined() - expect(innerLabelRule).toMatch(/(?:^|\n)\s*padding:\s*0;/) - expect(innerLabelRule).toMatch(/(?:^|\n)\s*border-bottom:\s*0;/) - - const statusRule = styles.match(/\.settings\s+label\.setting-toggle\s+\.devtools-status\s*\{([^}]*)\}/)?.[1] - expect(statusRule).toBeDefined() - expect(statusRule).toMatch(/(?:^|\n)\s*font-size:\s*calc\(9\.5px \* var\(--ui-scale\)\);/) - - const errorRule = styles.match(/\.settings\s+label\.setting-toggle\s+\.devtools-status\.is-error\s*\{([^}]*)\}/)?.[1] - expect(errorRule).toBeDefined() - expect(errorRule).toMatch(/(?:^|\n)\s*color:\s*var\(--danger\);/) - - // An unsupported platform must not look like an enabled switch. - const disabledRule = styles.match(/\.setting-toggle-input:disabled\s*\+\s*\.setting-toggle-control\s*\{([^}]*)\}/)?.[1] - expect(disabledRule).toBeDefined() - expect(disabledRule).toMatch(/(?:^|\n)\s*opacity:\s*0\.45;/) - }) -}) diff --git a/extensions/dsh-browser/tests/ui-scale.spec.ts b/extensions/dsh-browser/tests/ui-scale.spec.ts index 173873279..36029f4c9 100644 --- a/extensions/dsh-browser/tests/ui-scale.spec.ts +++ b/extensions/dsh-browser/tests/ui-scale.spec.ts @@ -1,5 +1,4 @@ // @vitest-environment jsdom -import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' import { applyUiScale, @@ -48,16 +47,4 @@ describe('panel text scale', () => { applyUiScale(42, root) expect(root.style.getPropertyValue(UI_SCALE_PROPERTY)).toBe('1.75') }) - - it('routes every stylesheet font-size through the scale token', () => { - const styles = readFileSync(`${process.cwd()}/src/panel/styles.css`, 'utf8') - const declarations = styles.match(/font-size:[^;]+;/g) ?? [] - - expect(declarations.length).toBeGreaterThan(0) - const unscaled = declarations.filter((declaration) => ( - /[0-9]px/.test(declaration) && !declaration.includes(`var(${UI_SCALE_PROPERTY})`) - )) - expect(unscaled).toEqual([]) - expect(styles).toMatch(new RegExp(`${UI_SCALE_PROPERTY}:\\s*1;`)) - }) }) diff --git a/extensions/dsh-browser/tests/uninstall.spec.ts b/extensions/dsh-browser/tests/uninstall.spec.ts new file mode 100644 index 000000000..785b1371e --- /dev/null +++ b/extensions/dsh-browser/tests/uninstall.spec.ts @@ -0,0 +1,48 @@ +// @vitest-environment jsdom +import { describe, expect, it } from 'vitest' +import { parseExtensionInstallInfo } from '../src/panel/updates.ts' +import { uninstallCommand } from '../src/panel/uninstall.ts' + +describe('uninstall handoff', () => { + it('carries the installer-recorded home into the command', () => { + const info = parseExtensionInstallInfo({ + schemaVersion: 1, mode: 'managed', application: 'dsh-browser', dshHome: "/Users/dev's home/DSH 中文", + }) + expect(uninstallCommand(info, 'Macintosh')).toContain("--home '/Users/dev'\"'\"'s home/DSH 中文'") + expect(uninstallCommand(info, 'Linux')).toContain("/dsh-browser/scripts/uninstall.sh' --home") + }) + + it('uses the standard uninstall command for old and unknown installations', () => { + for (const info of [ + parseExtensionInstallInfo({ schemaVersion: 1, mode: 'managed' }), + parseExtensionInstallInfo({ schemaVersion: 1, mode: 'checkout', sourceRoot: '/source' }), + parseExtensionInstallInfo(null), + ]) { + expect(uninstallCommand(info, 'Linux')) + .toBe('curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.sh | bash') + expect(uninstallCommand(info, 'Windows')).toContain('-File $s } finally {') + } + }) + + it('quotes Windows directories for the installed PowerShell script', () => { + const command = uninstallCommand({ mode: 'managed', dshHome: "C:\\DSH 空间\\dev's home" }, 'Windows NT') + expect(command).toContain("--home 'C:\\DSH 空间\\dev''s home'") + expect(command).toContain("dsh-browser\\scripts\\uninstall.ps1' --home") + const legacy = uninstallCommand({ mode: 'unknown' }, 'Windows') + expect(legacy).toContain('Invoke-WebRequest -UseBasicParsing') + expect(legacy).toContain("$ErrorActionPreference = 'Stop'") + }) + + it('uses the originating checkout for a local installation', () => { + expect(uninstallCommand({ mode: 'checkout', sourceRoot: '/source checkout', dshHome: '/custom home' }, 'Linux')) + .toBe("bash '/source checkout/scripts/uninstall.sh' --home '/custom home'") + expect(uninstallCommand({ mode: 'checkout', sourceRoot: 'C:\\source checkout', dshHome: 'C:\\custom home' }, 'Windows')) + .toBe("powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\\source checkout\\scripts\\uninstall.ps1' --home 'C:\\custom home'") + }) + + it('rejects invalid installation directories', () => { + for (const dshHome of ['', '../home', 'C:relative', '/path\ncommand', '/path\0']) { + expect(parseExtensionInstallInfo({ schemaVersion: 1, mode: 'managed', dshHome })).toEqual({ mode: 'unknown' }) + } + }) +}) diff --git a/extensions/dsh-browser/tests/updates.spec.ts b/extensions/dsh-browser/tests/updates.spec.ts index fae4bd54a..5d79a5ab1 100644 --- a/extensions/dsh-browser/tests/updates.spec.ts +++ b/extensions/dsh-browser/tests/updates.spec.ts @@ -124,27 +124,14 @@ describe('extension update checks', () => { version: string content_security_policy: { extension_pages: string } } - const installer = readFileSync(`${extensionRoot}/../../scripts/install.sh`, 'utf8') - expect(packageManifest.version).toBe(chromeManifest.version) expect(chromeManifest.content_security_policy.extension_pages).toContain(new URL(UPDATE_MANIFEST_URL).origin) - expect(installer).toContain('INSTALL_MODE="managed"') - expect(installer).toContain('INSTALL_MODE="checkout"') - expect(installer).toContain('$DIST_DIR/install-info.json') }) - it('keeps the Windows installer writing the same install provenance', () => { + it('keeps the Windows installer encoded for PowerShell 5.1', () => { const extensionRoot = process.cwd() const installerPath = `${extensionRoot}/../../scripts/install.ps1` - const installer = readFileSync(installerPath, 'utf8') - - // Windows PowerShell decodes a BOM-less script with the machine's ANSI codepage, which - // turns every Chinese message into mojibake. Losing the BOM is silent, so pin it here. + // Windows PowerShell 5.1 需要 BOM 才能正确读取 UTF-8 中文。 expect(readFileSync(installerPath).subarray(0, 3)).toEqual(Buffer.from([0xef, 0xbb, 0xbf])) - expect(installer).toContain("$InstallMode = if (Test-Path") - expect(installer).toContain("{ 'managed' } else { 'checkout' }") - expect(installer).toContain("Join-Path $DistDir 'install-info.json'") - expect(installer).toContain('schemaVersion = 1') - expect(WINDOWS_UPDATE_COMMAND).toContain('scripts/install.ps1') }) }) diff --git a/extensions/dsh-browser/vite.shared.ts b/extensions/dsh-browser/vite.shared.ts index d02e89b5a..ea6042a80 100644 --- a/extensions/dsh-browser/vite.shared.ts +++ b/extensions/dsh-browser/vite.shared.ts @@ -1,14 +1,7 @@ import { copyFileSync, cpSync, mkdirSync } from 'node:fs' import { resolve } from 'node:path' -import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vite' -/** - * Shared build plumbing for the extension's three targets (background ES - * service worker, iife content script, React panel). Each target has its own - * config file; scripts/build.mjs runs them sequentially into one dist/. - */ - /** * Build target: `chrome` (default) or `firefox` (set EXT_TARGET=firefox or * pass --firefox to scripts/build.mjs). Each target gets its own manifest and @@ -30,9 +23,7 @@ export const copyManifest = { }, } -/** Shared plugins for every target: tsconfig paths (plugin protocol source, - * SDK-like source consumption) plus the manifest copy. */ -export const sharedPlugins = [tsconfigPaths({ projects: ['./tsconfig.json'] }), copyManifest] +export const sharedPlugins = [copyManifest] /** Shared build options for the non-panel targets. */ export function targetBuild(entry: string, format: 'es' | 'iife', entryFileNames: string, emptyOutDir: boolean) { diff --git a/extensions/dsh-browser/vitest.config.ts b/extensions/dsh-browser/vitest.config.ts index c7a392916..12c8a39b8 100644 --- a/extensions/dsh-browser/vitest.config.ts +++ b/extensions/dsh-browser/vitest.config.ts @@ -1,8 +1,6 @@ -import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' export default defineConfig({ - plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], test: { include: ['tests/**/*.spec.ts'], environment: 'node', diff --git a/package.json b/package.json index e1cc772b5..0e59f97bd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "typecheck": "pnpm --filter @yuxianglin/dsh-bridge-browser run typecheck && pnpm --filter dsh-browser-extension run typecheck", "check:runtime": "node scripts/check-runtime.mjs", "test:smoke": "node scripts/smoke-runtime.mjs", - "test:browser": "node scripts/smoke-browser.mjs" + "test:browser": "node scripts/smoke-browser.mjs", + "test:uninstall": "node --test scripts/tests/uninstall*.test.mjs", + "test:uninstall:ui": "node scripts/smoke-uninstall-ui.mjs" }, "dependencies": { "@deepseek-ai/cordis-plugin-group": "~1.0.5", @@ -21,7 +23,6 @@ "@deepseek-ai/dsh-agent-default-model": "0.2.1-rc.5", "@deepseek-ai/dsh-anonymous-user-id": "0.2.1-rc.5", "@deepseek-ai/dsh-api-gateway": "0.2.1-rc.5", - "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.5", "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.5", "@deepseek-ai/dsh-authorization": "0.2.1-rc.5", "@deepseek-ai/dsh-bash-local": "0.2.1-rc.5", @@ -29,7 +30,6 @@ "@deepseek-ai/dsh-compaction": "0.2.1-rc.5", "@deepseek-ai/dsh-deepseek-account": "0.2.1-rc.5", "@deepseek-ai/dsh-fs": "0.2.1-rc.5", - "@deepseek-ai/dsh-hook-protocol": "0.2.1-rc.5", "@deepseek-ai/dsh-jobs": "0.2.1-rc.5", "@deepseek-ai/dsh-llm-deepseek": "0.2.1-rc.5", "@deepseek-ai/dsh-output-retention": "0.2.1-rc.5", @@ -46,8 +46,6 @@ "@deepseek-ai/dsh-shell": "0.2.1-rc.5", "@deepseek-ai/dsh-spill": "0.2.1-rc.5", "@deepseek-ai/dsh-subagent": "0.2.1-rc.5", - "@deepseek-ai/dsh-subagent-fork-in-process": "0.2.1-rc.5", - "@deepseek-ai/dsh-subagent-spawn-in-process": "0.2.1-rc.5", "@deepseek-ai/dsh-timeout": "0.2.1-rc.5", "@deepseek-ai/dsh-util-time": "0.2.1-rc.5", "@deepseek-ai/dsh-util-workspace-path": "0.2.1-rc.5", diff --git a/packages/browser/bridge-browser/README.i18n.yaml b/packages/browser/bridge-browser/README.i18n.yaml deleted file mode 100644 index 3359674e1..000000000 --- a/packages/browser/bridge-browser/README.i18n.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# side as of the last confirmed-consistent state. Both languages carry equal authority; -# after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write dsh-browser/packages/browser/bridge-browser/README.md - -README.md: ec11a0cfec3e58d07dcc8e8fb52870049d38d4b0 -README.zh.md: 3260c0550dcf3aec6f2d0b046608704df741e05d diff --git a/packages/browser/bridge-browser/README.md b/packages/browser/bridge-browser/README.md index ec11a0cfe..a734a102d 100644 --- a/packages/browser/bridge-browser/README.md +++ b/packages/browser/bridge-browser/README.md @@ -6,12 +6,11 @@ The **browser-operation bridge** for dsh mounts a token-authenticated WebSocket Page snapshots contain structured text with numbered controls and masked form fields. Actions target element numbers or selectors. Screenshots and chat images use DSH's durable attachment service; deferred sessions expose image limits when the host mounts that service. - ## Config | Key | Type | Default | Description | |---|---|---|---| -| `token` | `string` | generated | Fixed bearer token. When absent, a token is generated on first boot, persisted at `~/.dsh/ext-bridge-token` (chmod 0600), and printed in the boot log. | +| `token` | `string` | generated | Fixed bearer token. When absent, a token is generated on first boot and saved to `~/.dsh/ext-bridge-token` (chmod 0600). The boot log shows the file path. | | `toolTimeoutMs` | `number` | 90000 | Per-tool-call budget, leaving time for the extension's 60-second approval window. | | `snapshotMaxChars` | `number` | 32000 | Upper bound on one rendered snapshot's characters, minimum 500 (also negotiated to the extension via `hello.ok` caps). | | `maxInteractiveItems` | `number` | 60 | Upper bound on interactive inventory items per snapshot. | @@ -25,32 +24,16 @@ Workspace grouping is best-effort. If the composition has no workspace domain, d ## Usage -The remote installer downloads an installer-managed workspace, builds the plugin, and registers its official bundle in the local dsh `web` profile. It requires neither Git nor a local clone: - -```sh -curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash -cd ~/.dsh/dsh-browser && pnpm start -``` - -On Windows, run the PowerShell installer instead: - -```powershell -$s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s -cd $HOME\.dsh\dsh-browser; pnpm start -``` +See the root README for [installation, updates, startup, and uninstall](../../../README.md#detailed-installation-and-usage). -Developers can instead clone the repository and run `./scripts/install.sh` followed by `pnpm start` from that checkout. The local mode uses the current branch without downloading or overwriting source files. Both installation modes register the same profile bundle; build tools resolve only from the selected workspace and never from a parent checkout or parent `node_modules` directory. +## Development -The workspace targets the latest public dsh release, currently pinned to 0.2.1-rc.5. Older DSH releases are not supported: +The package declares `dsh.bundle.patch` as `./cordis.patch.yml`. The installer registers the local package in the `web` profile, which loads that patch. -```sh -npx @deepseek-ai/dsh@0.2.1-rc.5 web -``` +The bridge and extension share one pnpm workspace. Bridge source imports `@deepseek-ai/cordis` and `@deepseek-ai/schemastery` directly, and its DSH dependencies match the workspace's pinned release. The root build builds the bridge before the extension. The package manifest declares that support itself: `engines.dsh` (`^0.2.1-rc.5`) and `dsh.manifestVersion: 1` let dsh refuse an incompatible host before loading the plugin, and the Plugins page reads the localized title and description from `locale/en.json` / `locale/zh.json` plus the icon from `icon.svg` without activating the plugin. -The installer copies the unpacked extension to `~/.dsh/browser-extension` and opens `chrome://extensions`. Load that stable directory in Chrome and use the side panel. Loopback connections are discovered automatically and require no token entry; non-loopback deployments still require the configured bearer token. - ## Host methods the panel drives Beyond plain gateway passthrough (`rpc`), the adapter composes a few Host @@ -99,10 +82,10 @@ permission switch and answers `provisional-session` for everything else. ## Wire protocol -Frames are JSON objects discriminated by `t`, defined in [`protocol.ts`](src/protocol.ts) — the single source of truth shared with the extension through the workspace package's `./src/*` export. The built package also publishes `@yuxianglin/dsh-bridge-browser/protocol` for external consumers. +Frames are JSON objects discriminated by `t`, defined in [`protocol.ts`](src/protocol.ts) and shared with the extension through the workspace package's `./src/*` export. The built package exposes `@yuxianglin/dsh-bridge-browser/protocol` for external consumers. - Client → server: `hello` (auth + caps), `rpc` (gateway method passthrough), `respond` (resolve a host interaction by its RPC id), `tool.result`, `pong`. -- Server → client: `hello.ok` (echoes negotiated caps), `rpc.result`, `respond.result` (correlated acceptance or error), `event` (bridge-owned projection of dsh Remote streams and waterfalls), `tool.call`, `ping`, `error`. +- Server → client: `hello.ok` (echoes negotiated caps), `rpc.result`, `respond.result` (correlated acceptance or error), `event` (bridge-owned projection of dsh Remote streams and waterfalls), `tool.call`, `tool.cancel`, `ping`, `error`. Each `respond` carries a globally unique transport id as well as the host interaction's `rpcId`. The extension routes its receipt only to the panel that initiated it and rejects pending responses on timeout, panel closure, or bridge disconnection. @@ -134,22 +117,13 @@ This group is registered only while the connected extension advertises the `devT Capture notes: Chrome allows only one debugger per tab, so the browser's own DevTools cannot stay open on a tab while capture is attached; the error names that cause. Capture is in-memory, bounded (200 requests, 500 console entries) and resets on attach and on every top-level navigation. All returned page data is untrusted input. -## Model Experience - -- **Token effect**: one `browser_snapshot` (default 32k chars) costs roughly 8–10k tokens for typical English text; the exact count depends on language and tokenizer, and delta snapshots cost a fraction of that. The system-prompt section tells the model to snapshot on demand rather than hoard page text. -- **KV-cache effect**: none beyond ordinary tool results; snapshots are not cached server-side. -- **Latency**: each action awaits the extension's real-page execution plus settle detection (typically 0.2–2s; navigation up to 5s). -- **Failure modes**: `bridge-closed` (extension not connected), `timeout`, `no-active-tab`, `content-unavailable` (page needs a refresh), `action-failed` (stale inventory index — the model should re-snapshot). - ## Extension points - The tool set is the consumer surface; the seam is the bridge wire (`protocol.ts`). Add tools by registering on `ctx.tools` and dispatching over the bridge; the extension's content script dispatches by action name. - Negotiated caps (`hello.ok`) let the plugin dictate snapshot budgets to the extension without a shared config file. -## Known Limitations and Deferred Work +## Known limitations -- One active extension connection (a second window replaces the first). +- One active extension connection at a time. - Accessible cross-origin iframes are snapshotted and operated with stable `(frame, index)` addresses. Restricted or short-lived frames are reported as unavailable without failing the whole page snapshot. - Token rotation is manual (edit `~/.dsh/ext-bridge-token` or set `token` in config); no expiry. -- The Playwright-driven extension e2e self-skips without a usable Chromium executable or a built extension bundle. -- Approval is enforced in the extension service worker rather than delegated to model behavior. A future dsh tool-pipeline integration may surface the same policy in other clients. diff --git a/packages/browser/bridge-browser/README.zh.md b/packages/browser/bridge-browser/README.zh.md index 3260c0550..3c76df45f 100644 --- a/packages/browser/bridge-browser/README.zh.md +++ b/packages/browser/bridge-browser/README.zh.md @@ -6,12 +6,11 @@ dsh 的**浏览器操作桥**挂载经过 token 认证的 WebSocket 通道(`/e 页面快照包含结构化文本、编号控件和遮盖敏感值的表单字段。操作通过元素编号或选择器定位。截图和对话图片使用 DSH 持久附件服务;宿主挂载该服务时,延迟创建的会话会声明图片限制。 - ## 配置 | 键 | 类型 | 默认 | 说明 | |---|---|---|---| -| `token` | `string` | 自动生成 | 固定 bearer token。缺省时首次启动生成,写入 `~/.dsh/ext-bridge-token`(0600)并打印在启动日志。 | +| `token` | `string` | 自动生成 | 固定 bearer token。缺省时首次启动生成,保存到 `~/.dsh/ext-bridge-token`(0600)。启动日志显示文件路径。 | | `toolTimeoutMs` | `number` | 90000 | 单次工具调用预算,为扩展的 60 秒审批窗口预留时间。 | | `snapshotMaxChars` | `number` | 32000 | 单次快照渲染字符上限,最小为 500(经 `hello.ok` caps 协商给扩展)。 | | `maxInteractiveItems` | `number` | 60 | 单次快照交互清单条数上限。 | @@ -25,32 +24,16 @@ dsh 的**浏览器操作桥**挂载经过 token 认证的 WebSocket 通道(`/e ## 使用 -远程安装器会下载一个由脚本托管的 workspace,构建插件,并将它的官方 bundle 注册到本机 dsh 的 `web` profile。该方式无需 Git,也无需提前 clone: - -```sh -curl -fsSL https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.sh | bash -cd ~/.dsh/dsh-browser && pnpm start -``` - -Windows 请改用 PowerShell 安装器: - -```powershell -$s="$env:TEMP\dsh-install.ps1"; irm https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/install.ps1 -OutFile $s; powershell -NoProfile -ExecutionPolicy Bypass -File $s -cd $HOME\.dsh\dsh-browser; pnpm start -``` +安装、更新、启动和卸载方式见根目录 README 的[详细安装与使用](../../../README.zh.md#详细安装与使用)。 -开发者也可以 clone 仓库,在 checkout 中依次运行 `./scripts/install.sh` 和 `pnpm start`。本地模式直接使用当前分支,不会下载或覆盖源码。两种安装模式都会注册同一个 profile bundle;构建工具只从选定的 workspace 解析,绝不读取父 checkout 或父目录的 `node_modules`。 +## 开发 -工作区针对最新公开发布的 dsh,目前固定使用 0.2.1-rc.5;不支持旧版 DSH: +插件通过 `package.json` 的 `dsh.bundle.patch` 声明加载 `./cordis.patch.yml`。安装器将本地包注册到 `web` profile,由该 profile 加载配置。 -```sh -npx @deepseek-ai/dsh@0.2.1-rc.5 web -``` +桥接插件和扩展共用一个 pnpm workspace。桥接源码直接导入 `@deepseek-ai/cordis` 和 `@deepseek-ai/schemastery`,DSH 依赖与工作区固定版本保持一致。根目录构建命令先构建桥接插件,再构建扩展。 插件清单自带这层声明:`engines.dsh`(`^0.2.1-rc.5`)与 `dsh.manifestVersion: 1` 让 dsh 在加载插件前拒绝不兼容的宿主;插件页直接读取 `locale/en.json` / `locale/zh.json` 里的标题与简介、以及 `icon.svg` 图标来展示,无需激活插件。 -安装器会把已解压扩展复制到 `~/.dsh/browser-extension` 并打开 `chrome://extensions`。在 Chrome 中加载这个稳定目录,然后使用侧边栏。扩展会自动发现回环连接,无需输入 token;非回环部署仍需要配置的 bearer token。 - ## 面板驱动的 Host 方法 除了纯网关透传(`rpc`),适配器还组合了侧栏需要的若干 Host 方法。它们是面板操作输入区权限/模型/命令/Skill 的唯一词汇,且从不会作为工具暴露给模型。 @@ -81,10 +64,10 @@ npx @deepseek-ai/dsh@0.2.1-rc.5 web ## 线协议 -帧为按 `t` 判别的 JSON 对象,定义在 [`protocol.ts`](src/protocol.ts),是通过 workspace 包的 `./src/*` export 与扩展共享的真源。构建后的包还会发布 `@yuxianglin/dsh-bridge-browser/protocol`,供外部消费方使用。 +消息帧是通过 `t` 区分类型的 JSON 对象,定义在 [`protocol.ts`](src/protocol.ts),通过 workspace 包的 `./src/*` export 与扩展共享。构建后的包提供 `@yuxianglin/dsh-bridge-browser/protocol` 入口,供外部调用方使用。 - 客户端 → 服务端:`hello`(认证+caps)、`rpc`(网关方法透传)、`respond`(按 RPC id 结算宿主交互)、`tool.result`、`pong`。 -- 服务端 → 客户端:`hello.ok`(回显协商后的 caps)、`rpc.result`、`respond.result`(相关联的受理结果或错误)、`event`(由 dsh Remote 流与 waterfall 投影而来的 bridge 内部事件)、`tool.call`、`ping`、`error`。 +- 服务端 → 客户端:`hello.ok`(回显协商后的 caps)、`rpc.result`、`respond.result`(相关联的受理结果或错误)、`event`(由 dsh Remote 流与 waterfall 投影而来的 bridge 内部事件)、`tool.call`、`tool.cancel`、`ping`、`error`。 每个 `respond` 同时携带全局唯一的传输 id 与宿主交互的 `rpcId`。扩展只把回执路由给发起操作的面板,并在超时、面板关闭或桥断线时拒绝尚未完成的响应。 @@ -116,22 +99,13 @@ npx @deepseek-ai/dsh@0.2.1-rc.5 web 抓包说明:Chrome 同一标签页只允许一个调试器,因此附加期间浏览器自带的开发者工具无法在该标签页打开,错误信息会点明这一原因。抓包数据保存在内存中且有上限(200 条请求、500 条控制台记录),附加时与每次顶层导航时都会重置。所有返回的页面数据都视为不可信输入。 -## 模型体验 - -- **Token 影响**:一次 `browser_snapshot`(默认 32k 字符)对常见英文文本约为 8–10k token,具体取决于语言和分词器;delta 快照只需零头。系统提示段落引导模型按需快照而非囤积页面文本。 -- **KV 缓存影响**:无(快照不做服务端缓存)。 -- **延迟**:每次动作等待扩展在真实页面执行 + 稳定检测(通常 0.2–2s;导航最长 5s)。 -- **失败模式**:`bridge-closed`(扩展未连接)、`timeout`、`no-active-tab`、`content-unavailable`(页面需刷新)、`action-failed`(编号过期——模型应重新快照)。 - ## 扩展点 - 工具集是消费面;seam 是桥接线(`protocol.ts`)。在 `ctx.tools` 注册新工具并经由桥分发即可,扩展的 content script 按动作名分发。 - 协商 caps(`hello.ok`)让插件无需共享配置文件即可向扩展下达快照预算。 -## 已知限制与后续工作 +## 已知限制 -- 仅一个活动扩展连接(第二个窗口顶替第一个)。 +- 同时仅有一个活动扩展连接。 - 可访问的跨源 iframe 会进入快照,并通过稳定的 `(frame, index)` 地址执行操作;受保护或已销毁的 frame 会标记为不可访问,不影响整页快照。 - token 手动轮换(改 `~/.dsh/ext-bridge-token` 或配置 `token`),无过期。 -- Playwright 驱动的扩展 e2e 会在缺少可用的 Chromium 可执行文件或构建完成的扩展包时自行跳过。 -- 审批由扩展 service worker 强制执行,而不是依赖模型自觉。未来接入 dsh 工具管线时可以把同一策略暴露给其它客户端。 diff --git a/packages/browser/bridge-browser/coverage/base.css b/packages/browser/bridge-browser/coverage/base.css deleted file mode 100644 index f418035b4..000000000 --- a/packages/browser/bridge-browser/coverage/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/packages/browser/bridge-browser/coverage/block-navigation.js b/packages/browser/bridge-browser/coverage/block-navigation.js deleted file mode 100644 index 530d1ed2b..000000000 --- a/packages/browser/bridge-browser/coverage/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/packages/browser/bridge-browser/coverage/clover.xml b/packages/browser/bridge-browser/coverage/clover.xml deleted file mode 100644 index d66839343..000000000 --- a/packages/browser/bridge-browser/coverage/clover.xml +++ /dev/null @@ -1,364 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/browser/bridge-browser/coverage/coverage-final.json b/packages/browser/bridge-browser/coverage/coverage-final.json deleted file mode 100644 index aabc7c620..000000000 --- a/packages/browser/bridge-browser/coverage/coverage-final.json +++ /dev/null @@ -1,9 +0,0 @@ -{"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/index.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/index.ts","statementMap":{"0":{"start":{"line":37,"column":20},"end":{"line":37,"column":null}},"1":{"start":{"line":40,"column":22},"end":{"line":40,"column":null}},"2":{"start":{"line":43,"column":32},"end":{"line":43,"column":null}},"3":{"start":{"line":46,"column":35},"end":{"line":46,"column":null}},"4":{"start":{"line":49,"column":38},"end":{"line":49,"column":null}},"5":{"start":{"line":52,"column":6},"end":{"line":52,"column":null}},"6":{"start":{"line":55,"column":37},"end":{"line":55,"column":null}},"7":{"start":{"line":73,"column":33},"end":{"line":80,"column":null}},"8":{"start":{"line":87,"column":2},"end":{"line":89,"column":null}},"9":{"start":{"line":88,"column":4},"end":{"line":88,"column":null}},"10":{"start":{"line":98,"column":35},"end":{"line":105,"column":null}},"11":{"start":{"line":106,"column":2},"end":{"line":106,"column":null}},"12":{"start":{"line":107,"column":2},"end":{"line":107,"column":null}},"13":{"start":{"line":108,"column":2},"end":{"line":108,"column":null}},"14":{"start":{"line":109,"column":2},"end":{"line":109,"column":null}},"15":{"start":{"line":120,"column":19},"end":{"line":120,"column":null}},"16":{"start":{"line":122,"column":19},"end":{"line":122,"column":null}},"17":{"start":{"line":125,"column":8},"end":{"line":132,"column":null}},"18":{"start":{"line":129,"column":19},"end":{"line":129,"column":44}},"19":{"start":{"line":133,"column":17},"end":{"line":143,"column":null}},"20":{"start":{"line":136,"column":28},"end":{"line":136,"column":null}},"21":{"start":{"line":145,"column":33},"end":{"line":148,"column":null}},"22":{"start":{"line":147,"column":38},"end":{"line":147,"column":78}},"23":{"start":{"line":149,"column":2},"end":{"line":149,"column":null}},"24":{"start":{"line":149,"column":19},"end":{"line":149,"column":58}},"25":{"start":{"line":151,"column":2},"end":{"line":151,"column":null}},"26":{"start":{"line":151,"column":6},"end":{"line":151,"column":41}},"27":{"start":{"line":151,"column":25},"end":{"line":151,"column":41}},"28":{"start":{"line":157,"column":32},"end":{"line":164,"column":null}},"29":{"start":{"line":161,"column":6},"end":{"line":161,"column":null}},"30":{"start":{"line":162,"column":6},"end":{"line":162,"column":null}},"31":{"start":{"line":165,"column":2},"end":{"line":165,"column":null}},"32":{"start":{"line":165,"column":19},"end":{"line":165,"column":57}},"33":{"start":{"line":167,"column":2},"end":{"line":174,"column":null}},"34":{"start":{"line":168,"column":10},"end":{"line":172,"column":null}},"35":{"start":{"line":173,"column":4},"end":{"line":173,"column":null}},"36":{"start":{"line":173,"column":19},"end":{"line":173,"column":71}},"37":{"start":{"line":173,"column":61},"end":{"line":173,"column":71}},"38":{"start":{"line":178,"column":23},"end":{"line":178,"column":null}},"39":{"start":{"line":179,"column":2},"end":{"line":186,"column":null}},"40":{"start":{"line":180,"column":4},"end":{"line":185,"column":null}},"41":{"start":{"line":180,"column":21},"end":{"line":185,"column":8}},"42":{"start":{"line":188,"column":2},"end":{"line":192,"column":null}},"43":{"start":{"line":193,"column":2},"end":{"line":193,"column":null}}},"fnMap":{"0":{"name":"assertPositiveInteger","decl":{"start":{"line":86,"column":16},"end":{"line":86,"column":38}},"loc":{"start":{"line":86,"column":73},"end":{"line":90,"column":null}},"line":86},"1":{"name":"resolveConfig","decl":{"start":{"line":97,"column":16},"end":{"line":97,"column":30}},"loc":{"start":{"line":97,"column":62},"end":{"line":110,"column":null}},"line":97},"2":{"name":"apply","decl":{"start":{"line":119,"column":22},"end":{"line":119,"column":28}},"loc":{"start":{"line":119,"column":73},"end":{"line":194,"column":null}},"line":119},"3":{"name":"(anonymous_3)","decl":{"start":{"line":128,"column":15},"end":{"line":128,"column":null}},"loc":{"start":{"line":129,"column":17},"end":{"line":129,"column":null}},"line":129},"4":{"name":"(anonymous_4)","decl":{"start":{"line":136,"column":4},"end":{"line":136,"column":17}},"loc":{"start":{"line":136,"column":28},"end":{"line":136,"column":null}},"line":136},"5":{"name":"(anonymous_5)","decl":{"start":{"line":147,"column":4},"end":{"line":147,"column":14}},"loc":{"start":{"line":147,"column":36},"end":{"line":147,"column":null}},"line":147},"6":{"name":"(anonymous_6)","decl":{"start":{"line":149,"column":6},"end":{"line":149,"column":19}},"loc":{"start":{"line":149,"column":19},"end":{"line":149,"column":58}},"line":149},"7":{"name":"(anonymous_7)","decl":{"start":{"line":151,"column":6},"end":{"line":151,"column":25}},"loc":{"start":{"line":151,"column":6},"end":{"line":151,"column":41}},"line":151},"8":{"name":"(anonymous_8)","decl":{"start":{"line":160,"column":4},"end":{"line":160,"column":14}},"loc":{"start":{"line":160,"column":28},"end":{"line":163,"column":null}},"line":160},"9":{"name":"(anonymous_9)","decl":{"start":{"line":165,"column":6},"end":{"line":165,"column":19}},"loc":{"start":{"line":165,"column":19},"end":{"line":165,"column":57}},"line":165},"10":{"name":"(anonymous_10)","decl":{"start":{"line":167,"column":6},"end":{"line":167,"column":19}},"loc":{"start":{"line":167,"column":19},"end":{"line":174,"column":5}},"line":167},"11":{"name":"(anonymous_11)","decl":{"start":{"line":173,"column":4},"end":{"line":173,"column":17}},"loc":{"start":{"line":173,"column":17},"end":{"line":173,"column":null}},"line":173},"12":{"name":"(anonymous_12)","decl":{"start":{"line":180,"column":8},"end":{"line":180,"column":21}},"loc":{"start":{"line":180,"column":21},"end":{"line":185,"column":8}},"line":180}},"branchMap":{"0":{"loc":{"start":{"line":87,"column":2},"end":{"line":89,"column":null}},"type":"if","locations":[{"start":{"line":87,"column":2},"end":{"line":89,"column":null}},{"start":{},"end":{}}],"line":87},"1":{"loc":{"start":{"line":87,"column":6},"end":{"line":87,"column":45}},"type":"binary-expr","locations":[{"start":{"line":87,"column":6},"end":{"line":87,"column":34}},{"start":{"line":87,"column":34},"end":{"line":87,"column":45}}],"line":87},"2":{"loc":{"start":{"line":99,"column":8},"end":{"line":99,"column":null}},"type":"cond-expr","locations":[{"start":{"line":99,"column":37},"end":{"line":99,"column":42}},{"start":{"line":99,"column":42},"end":{"line":99,"column":null}}],"line":99},"3":{"loc":{"start":{"line":100,"column":19},"end":{"line":100,"column":null}},"type":"binary-expr","locations":[{"start":{"line":100,"column":19},"end":{"line":100,"column":43}},{"start":{"line":100,"column":43},"end":{"line":100,"column":null}}],"line":100},"4":{"loc":{"start":{"line":101,"column":22},"end":{"line":101,"column":null}},"type":"binary-expr","locations":[{"start":{"line":101,"column":22},"end":{"line":101,"column":49}},{"start":{"line":101,"column":49},"end":{"line":101,"column":null}}],"line":101},"5":{"loc":{"start":{"line":102,"column":25},"end":{"line":102,"column":null}},"type":"binary-expr","locations":[{"start":{"line":102,"column":25},"end":{"line":102,"column":55}},{"start":{"line":102,"column":55},"end":{"line":102,"column":null}}],"line":102},"6":{"loc":{"start":{"line":103,"column":26},"end":{"line":103,"column":null}},"type":"binary-expr","locations":[{"start":{"line":103,"column":26},"end":{"line":103,"column":57}},{"start":{"line":103,"column":57},"end":{"line":103,"column":null}}],"line":103},"7":{"loc":{"start":{"line":104,"column":24},"end":{"line":104,"column":null}},"type":"binary-expr","locations":[{"start":{"line":104,"column":24},"end":{"line":104,"column":53}},{"start":{"line":104,"column":53},"end":{"line":104,"column":null}}],"line":104},"8":{"loc":{"start":{"line":179,"column":2},"end":{"line":186,"column":null}},"type":"if","locations":[{"start":{"line":179,"column":2},"end":{"line":186,"column":null}},{"start":{},"end":{}}],"line":179},"9":{"loc":{"start":{"line":189,"column":4},"end":{"line":191,"column":null}},"type":"cond-expr","locations":[{"start":{"line":190,"column":8},"end":{"line":190,"column":null}},{"start":{"line":191,"column":8},"end":{"line":191,"column":null}}],"line":189}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":3,"6":3,"7":3,"8":28,"9":5,"10":9,"11":9,"12":9,"13":9,"14":9,"15":7,"16":7,"17":5,"18":1,"19":5,"20":2,"21":5,"22":2,"23":5,"24":5,"25":5,"26":5,"27":3,"28":5,"29":1,"30":1,"31":5,"32":5,"33":5,"34":5,"35":5,"36":3,"37":33,"38":5,"39":5,"40":3,"41":3,"42":5,"43":7},"f":{"0":28,"1":9,"2":7,"3":1,"4":2,"5":2,"6":5,"7":8,"8":1,"9":5,"10":5,"11":3,"12":3},"b":{"0":[5,23],"1":[28,27],"2":[4,5],"3":[9,1],"4":[9,1],"5":[9,1],"6":[9,4],"7":[9,5],"8":[3,2],"9":[1,4]},"meta":{"lastBranch":10,"lastFunction":13,"lastStatement":44,"seen":{"s:37:20:37:Infinity":0,"s:40:22:40:Infinity":1,"s:43:32:43:Infinity":2,"s:46:35:46:Infinity":3,"s:49:38:49:Infinity":4,"s:52:6:52:Infinity":5,"s:55:37:55:Infinity":6,"s:73:33:80:Infinity":7,"f:86:16:86:38":0,"b:87:2:89:Infinity:undefined:undefined:undefined:undefined":0,"s:87:2:89:Infinity":8,"b:87:6:87:34:87:34:87:45":1,"s:88:4:88:Infinity":9,"f:97:16:97:30":1,"s:98:35:105:Infinity":10,"b:99:37:99:42:99:42:99:Infinity":2,"b:100:19:100:43:100:43:100:Infinity":3,"b:101:22:101:49:101:49:101:Infinity":4,"b:102:25:102:55:102:55:102:Infinity":5,"b:103:26:103:57:103:57:103:Infinity":6,"b:104:24:104:53:104:53:104:Infinity":7,"s:106:2:106:Infinity":11,"s:107:2:107:Infinity":12,"s:108:2:108:Infinity":13,"s:109:2:109:Infinity":14,"f:119:22:119:28":2,"s:120:19:120:Infinity":15,"s:122:19:122:Infinity":16,"s:125:8:132:Infinity":17,"f:128:15:128:Infinity":3,"s:129:19:129:44":18,"s:133:17:143:Infinity":19,"f:136:4:136:17":4,"s:136:28:136:Infinity":20,"s:145:33:148:Infinity":21,"f:147:4:147:14":5,"s:147:38:147:78":22,"s:149:2:149:Infinity":23,"f:149:6:149:19":6,"s:149:19:149:58":24,"s:151:2:151:Infinity":25,"f:151:6:151:25":7,"s:151:6:151:41":26,"s:151:25:151:41":27,"s:157:32:164:Infinity":28,"f:160:4:160:14":8,"s:161:6:161:Infinity":29,"s:162:6:162:Infinity":30,"s:165:2:165:Infinity":31,"f:165:6:165:19":9,"s:165:19:165:57":32,"s:167:2:174:Infinity":33,"f:167:6:167:19":10,"s:168:10:172:Infinity":34,"s:173:4:173:Infinity":35,"f:173:4:173:17":11,"s:173:19:173:71":36,"s:173:61:173:71":37,"s:178:23:178:Infinity":38,"b:179:2:186:Infinity:undefined:undefined:undefined:undefined":8,"s:179:2:186:Infinity":39,"s:180:4:185:Infinity":40,"f:180:8:180:21":12,"s:180:21:185:8":41,"s:188:2:192:Infinity":42,"b:190:8:190:Infinity:191:8:191:Infinity":9,"s:193:2:193:Infinity":43},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/invariant.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/invariant.ts","statementMap":{"0":{"start":{"line":10,"column":21},"end":{"line":10,"column":null}},"1":{"start":{"line":13,"column":20},"end":{"line":13,"column":null}},"2":{"start":{"line":15,"column":22},"end":{"line":15,"column":null}},"3":{"start":{"line":24,"column":6},"end":{"line":24,"column":null}},"4":{"start":{"line":31,"column":13},"end":{"line":32,"column":null}},"5":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":24,"column":6},"end":{"line":24,"column":42}},"loc":{"start":{"line":24,"column":42},"end":{"line":24,"column":null}},"line":24},"1":{"name":"(anonymous_1)","decl":{"start":{"line":31,"column":13},"end":{"line":31,"column":22}},"loc":{"start":{"line":32,"column":2},"end":{"line":32,"column":null}},"line":32}},"branchMap":{},"s":{"0":9,"1":9,"2":9,"3":9,"4":9,"5":9},"f":{"0":9,"1":9},"b":{},"meta":{"lastBranch":0,"lastFunction":2,"lastStatement":6,"seen":{"s:10:21:10:Infinity":0,"s:13:20:13:Infinity":1,"s:15:22:15:Infinity":2,"s:24:6:24:Infinity":3,"f:24:6:24:42":0,"s:31:13:32:Infinity":4,"f:31:13:31:22":1,"s:32:2:32:Infinity":5},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/protocol.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/protocol.ts","statementMap":{"0":{"start":{"line":16,"column":27},"end":{"line":16,"column":null}},"1":{"start":{"line":19,"column":34},"end":{"line":19,"column":null}},"2":{"start":{"line":22,"column":32},"end":{"line":22,"column":null}},"3":{"start":{"line":25,"column":32},"end":{"line":25,"column":null}},"4":{"start":{"line":28,"column":35},"end":{"line":28,"column":null}},"5":{"start":{"line":95,"column":2},"end":{"line":100,"column":null}},"6":{"start":{"line":110,"column":2},"end":{"line":110,"column":null}},"7":{"start":{"line":120,"column":2},"end":{"line":124,"column":null}},"8":{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},"9":{"start":{"line":123,"column":4},"end":{"line":123,"column":null}},"10":{"start":{"line":125,"column":2},"end":{"line":125,"column":null}},"11":{"start":{"line":125,"column":75},"end":{"line":125,"column":null}},"12":{"start":{"line":126,"column":16},"end":{"line":126,"column":null}},"13":{"start":{"line":127,"column":2},"end":{"line":127,"column":null}},"14":{"start":{"line":127,"column":35},"end":{"line":127,"column":null}},"15":{"start":{"line":128,"column":2},"end":{"line":177,"column":null}},"16":{"start":{"line":130,"column":6},"end":{"line":133,"column":null}},"17":{"start":{"line":135,"column":6},"end":{"line":137,"column":null}},"18":{"start":{"line":139,"column":6},"end":{"line":139,"column":null}},"19":{"start":{"line":139,"column":40},"end":{"line":139,"column":null}},"20":{"start":{"line":140,"column":6},"end":{"line":142,"column":null}},"21":{"start":{"line":141,"column":8},"end":{"line":141,"column":null}},"22":{"start":{"line":143,"column":6},"end":{"line":145,"column":null}},"23":{"start":{"line":147,"column":6},"end":{"line":147,"column":null}},"24":{"start":{"line":149,"column":6},"end":{"line":151,"column":null}},"25":{"start":{"line":153,"column":6},"end":{"line":153,"column":null}},"26":{"start":{"line":153,"column":40},"end":{"line":153,"column":null}},"27":{"start":{"line":154,"column":6},"end":{"line":156,"column":null}},"28":{"start":{"line":155,"column":8},"end":{"line":155,"column":null}},"29":{"start":{"line":157,"column":6},"end":{"line":159,"column":null}},"30":{"start":{"line":161,"column":6},"end":{"line":163,"column":null}},"31":{"start":{"line":165,"column":6},"end":{"line":168,"column":null}},"32":{"start":{"line":170,"column":6},"end":{"line":170,"column":null}},"33":{"start":{"line":172,"column":6},"end":{"line":174,"column":null}},"34":{"start":{"line":176,"column":6},"end":{"line":176,"column":null}},"35":{"start":{"line":181,"column":2},"end":{"line":181,"column":null}},"36":{"start":{"line":181,"column":51},"end":{"line":181,"column":null}},"37":{"start":{"line":182,"column":15},"end":{"line":182,"column":null}},"38":{"start":{"line":183,"column":2},"end":{"line":185,"column":null}},"39":{"start":{"line":189,"column":2},"end":{"line":191,"column":null}}},"fnMap":{"0":{"name":"isServerFrame","decl":{"start":{"line":94,"column":16},"end":{"line":94,"column":30}},"loc":{"start":{"line":94,"column":72},"end":{"line":101,"column":null}},"line":94},"1":{"name":"isClientFrame","decl":{"start":{"line":109,"column":16},"end":{"line":109,"column":30}},"loc":{"start":{"line":109,"column":72},"end":{"line":111,"column":null}},"line":109},"2":{"name":"parseBridgeFrame","decl":{"start":{"line":118,"column":16},"end":{"line":118,"column":33}},"loc":{"start":{"line":118,"column":72},"end":{"line":178,"column":null}},"line":118},"3":{"name":"isCaps","decl":{"start":{"line":180,"column":9},"end":{"line":180,"column":16}},"loc":{"start":{"line":180,"column":53},"end":{"line":186,"column":null}},"line":180},"4":{"name":"isToolError","decl":{"start":{"line":188,"column":9},"end":{"line":188,"column":21}},"loc":{"start":{"line":188,"column":57},"end":{"line":192,"column":null}},"line":188}},"branchMap":{"0":{"loc":{"start":{"line":95,"column":9},"end":{"line":100,"column":null}},"type":"binary-expr","locations":[{"start":{"line":95,"column":9},"end":{"line":95,"column":null}},{"start":{"line":96,"column":7},"end":{"line":96,"column":null}},{"start":{"line":97,"column":7},"end":{"line":97,"column":null}},{"start":{"line":98,"column":7},"end":{"line":98,"column":null}},{"start":{"line":99,"column":7},"end":{"line":99,"column":null}},{"start":{"line":100,"column":7},"end":{"line":100,"column":null}}],"line":95},"1":{"loc":{"start":{"line":110,"column":9},"end":{"line":110,"column":null}},"type":"binary-expr","locations":[{"start":{"line":110,"column":9},"end":{"line":110,"column":32}},{"start":{"line":110,"column":32},"end":{"line":110,"column":53}},{"start":{"line":110,"column":53},"end":{"line":110,"column":82}},{"start":{"line":110,"column":82},"end":{"line":110,"column":null}}],"line":110},"2":{"loc":{"start":{"line":125,"column":2},"end":{"line":125,"column":null}},"type":"if","locations":[{"start":{"line":125,"column":2},"end":{"line":125,"column":null}},{"start":{},"end":{}}],"line":125},"3":{"loc":{"start":{"line":125,"column":6},"end":{"line":125,"column":75}},"type":"binary-expr","locations":[{"start":{"line":125,"column":6},"end":{"line":125,"column":35}},{"start":{"line":125,"column":35},"end":{"line":125,"column":53}},{"start":{"line":125,"column":53},"end":{"line":125,"column":75}}],"line":125},"4":{"loc":{"start":{"line":127,"column":2},"end":{"line":127,"column":null}},"type":"if","locations":[{"start":{"line":127,"column":2},"end":{"line":127,"column":null}},{"start":{},"end":{}}],"line":127},"5":{"loc":{"start":{"line":128,"column":2},"end":{"line":177,"column":null}},"type":"switch","locations":[{"start":{"line":129,"column":4},"end":{"line":133,"column":null}},{"start":{"line":134,"column":4},"end":{"line":137,"column":null}},{"start":{"line":138,"column":4},"end":{"line":145,"column":null}},{"start":{"line":146,"column":4},"end":{"line":147,"column":null}},{"start":{"line":148,"column":4},"end":{"line":151,"column":null}},{"start":{"line":152,"column":4},"end":{"line":159,"column":null}},{"start":{"line":160,"column":4},"end":{"line":163,"column":null}},{"start":{"line":164,"column":4},"end":{"line":168,"column":null}},{"start":{"line":169,"column":4},"end":{"line":170,"column":null}},{"start":{"line":171,"column":4},"end":{"line":174,"column":null}},{"start":{"line":175,"column":4},"end":{"line":176,"column":null}}],"line":128},"6":{"loc":{"start":{"line":130,"column":13},"end":{"line":133,"column":null}},"type":"cond-expr","locations":[{"start":{"line":132,"column":10},"end":{"line":132,"column":null}},{"start":{"line":133,"column":10},"end":{"line":133,"column":null}}],"line":130},"7":{"loc":{"start":{"line":130,"column":13},"end":{"line":131,"column":null}},"type":"binary-expr","locations":[{"start":{"line":130,"column":13},"end":{"line":130,"column":null}},{"start":{"line":131,"column":11},"end":{"line":131,"column":null}}],"line":130},"8":{"loc":{"start":{"line":135,"column":13},"end":{"line":137,"column":null}},"type":"cond-expr","locations":[{"start":{"line":136,"column":10},"end":{"line":136,"column":null}},{"start":{"line":137,"column":10},"end":{"line":137,"column":null}}],"line":135},"9":{"loc":{"start":{"line":135,"column":13},"end":{"line":135,"column":null}},"type":"binary-expr","locations":[{"start":{"line":135,"column":13},"end":{"line":135,"column":45}},{"start":{"line":135,"column":45},"end":{"line":135,"column":null}}],"line":135},"10":{"loc":{"start":{"line":139,"column":6},"end":{"line":139,"column":null}},"type":"if","locations":[{"start":{"line":139,"column":6},"end":{"line":139,"column":null}},{"start":{},"end":{}}],"line":139},"11":{"loc":{"start":{"line":140,"column":6},"end":{"line":142,"column":null}},"type":"if","locations":[{"start":{"line":140,"column":6},"end":{"line":142,"column":null}},{"start":{},"end":{}}],"line":140},"12":{"loc":{"start":{"line":140,"column":10},"end":{"line":140,"column":50}},"type":"binary-expr","locations":[{"start":{"line":140,"column":10},"end":{"line":140,"column":31}},{"start":{"line":140,"column":31},"end":{"line":140,"column":50}}],"line":140},"13":{"loc":{"start":{"line":143,"column":13},"end":{"line":145,"column":null}},"type":"cond-expr","locations":[{"start":{"line":144,"column":10},"end":{"line":144,"column":null}},{"start":{"line":145,"column":10},"end":{"line":145,"column":null}}],"line":143},"14":{"loc":{"start":{"line":149,"column":13},"end":{"line":151,"column":null}},"type":"cond-expr","locations":[{"start":{"line":150,"column":10},"end":{"line":150,"column":null}},{"start":{"line":151,"column":10},"end":{"line":151,"column":null}}],"line":149},"15":{"loc":{"start":{"line":153,"column":6},"end":{"line":153,"column":null}},"type":"if","locations":[{"start":{"line":153,"column":6},"end":{"line":153,"column":null}},{"start":{},"end":{}}],"line":153},"16":{"loc":{"start":{"line":154,"column":6},"end":{"line":156,"column":null}},"type":"if","locations":[{"start":{"line":154,"column":6},"end":{"line":156,"column":null}},{"start":{},"end":{}}],"line":154},"17":{"loc":{"start":{"line":154,"column":10},"end":{"line":154,"column":50}},"type":"binary-expr","locations":[{"start":{"line":154,"column":10},"end":{"line":154,"column":31}},{"start":{"line":154,"column":31},"end":{"line":154,"column":50}}],"line":154},"18":{"loc":{"start":{"line":157,"column":13},"end":{"line":159,"column":null}},"type":"cond-expr","locations":[{"start":{"line":158,"column":10},"end":{"line":158,"column":null}},{"start":{"line":159,"column":10},"end":{"line":159,"column":null}}],"line":157},"19":{"loc":{"start":{"line":157,"column":13},"end":{"line":157,"column":null}},"type":"binary-expr","locations":[{"start":{"line":157,"column":13},"end":{"line":157,"column":48}},{"start":{"line":157,"column":48},"end":{"line":157,"column":null}}],"line":157},"20":{"loc":{"start":{"line":161,"column":13},"end":{"line":163,"column":null}},"type":"cond-expr","locations":[{"start":{"line":162,"column":10},"end":{"line":162,"column":null}},{"start":{"line":163,"column":10},"end":{"line":163,"column":null}}],"line":161},"21":{"loc":{"start":{"line":161,"column":13},"end":{"line":161,"column":null}},"type":"binary-expr","locations":[{"start":{"line":161,"column":13},"end":{"line":161,"column":48}},{"start":{"line":161,"column":48},"end":{"line":161,"column":null}}],"line":161},"22":{"loc":{"start":{"line":165,"column":13},"end":{"line":168,"column":null}},"type":"cond-expr","locations":[{"start":{"line":167,"column":10},"end":{"line":167,"column":null}},{"start":{"line":168,"column":10},"end":{"line":168,"column":null}}],"line":165},"23":{"loc":{"start":{"line":165,"column":13},"end":{"line":166,"column":null}},"type":"binary-expr","locations":[{"start":{"line":165,"column":13},"end":{"line":165,"column":45}},{"start":{"line":165,"column":45},"end":{"line":165,"column":null}},{"start":{"line":166,"column":11},"end":{"line":166,"column":45}},{"start":{"line":166,"column":45},"end":{"line":166,"column":68}},{"start":{"line":166,"column":68},"end":{"line":166,"column":null}}],"line":165},"24":{"loc":{"start":{"line":172,"column":13},"end":{"line":174,"column":null}},"type":"cond-expr","locations":[{"start":{"line":173,"column":10},"end":{"line":173,"column":null}},{"start":{"line":174,"column":10},"end":{"line":174,"column":null}}],"line":172},"25":{"loc":{"start":{"line":172,"column":13},"end":{"line":172,"column":null}},"type":"binary-expr","locations":[{"start":{"line":172,"column":13},"end":{"line":172,"column":47}},{"start":{"line":172,"column":47},"end":{"line":172,"column":null}}],"line":172},"26":{"loc":{"start":{"line":181,"column":2},"end":{"line":181,"column":null}},"type":"if","locations":[{"start":{"line":181,"column":2},"end":{"line":181,"column":null}},{"start":{},"end":{}}],"line":181},"27":{"loc":{"start":{"line":181,"column":6},"end":{"line":181,"column":51}},"type":"binary-expr","locations":[{"start":{"line":181,"column":6},"end":{"line":181,"column":35}},{"start":{"line":181,"column":35},"end":{"line":181,"column":51}}],"line":181},"28":{"loc":{"start":{"line":183,"column":9},"end":{"line":185,"column":null}},"type":"binary-expr","locations":[{"start":{"line":183,"column":9},"end":{"line":183,"column":null}},{"start":{"line":184,"column":7},"end":{"line":184,"column":52}},{"start":{"line":184,"column":52},"end":{"line":184,"column":null}},{"start":{"line":185,"column":7},"end":{"line":185,"column":55}},{"start":{"line":185,"column":55},"end":{"line":185,"column":null}}],"line":183},"29":{"loc":{"start":{"line":189,"column":9},"end":{"line":191,"column":null}},"type":"binary-expr","locations":[{"start":{"line":189,"column":9},"end":{"line":189,"column":38}},{"start":{"line":189,"column":38},"end":{"line":189,"column":null}},{"start":{"line":190,"column":7},"end":{"line":190,"column":null}},{"start":{"line":191,"column":7},"end":{"line":191,"column":null}}],"line":189}},"s":{"0":5,"1":5,"2":5,"3":5,"4":5,"5":8,"6":6,"7":93,"8":93,"9":2,"10":91,"11":2,"12":89,"13":89,"14":1,"15":88,"16":37,"17":16,"18":8,"19":1,"20":7,"21":4,"22":3,"23":4,"24":3,"25":6,"26":1,"27":5,"28":2,"29":3,"30":4,"31":4,"32":2,"33":3,"34":1,"35":40,"36":1,"37":39,"38":39,"39":3},"f":{"0":8,"1":6,"2":93,"3":40,"4":3},"b":{"0":[8,7,6,5,3,2],"1":[6,4,3,2],"2":[2,89],"3":[91,91,90],"4":[1,88],"5":[88,16,8,4,3,6,4,4,2,3,1],"6":[33,4],"7":[37,37],"8":[15,1],"9":[16,15],"10":[1,7],"11":[4,3],"12":[7,5],"13":[2,1],"14":[2,1],"15":[1,5],"16":[2,3],"17":[5,3],"18":[1,2],"19":[3,1],"20":[2,2],"21":[4,3],"22":[3,1],"23":[4,4,4,4,4],"24":[2,1],"25":[3,2],"26":[1,39],"27":[40,39],"28":[39,38,36,35,35],"29":[3,2,2,2]},"meta":{"lastBranch":30,"lastFunction":5,"lastStatement":40,"seen":{"s:16:27:16:Infinity":0,"s:19:34:19:Infinity":1,"s:22:32:22:Infinity":2,"s:25:32:25:Infinity":3,"s:28:35:28:Infinity":4,"f:94:16:94:30":0,"s:95:2:100:Infinity":5,"b:95:9:95:Infinity:96:7:96:Infinity:97:7:97:Infinity:98:7:98:Infinity:99:7:99:Infinity:100:7:100:Infinity":0,"f:109:16:109:30":1,"s:110:2:110:Infinity":6,"b:110:9:110:32:110:32:110:53:110:53:110:82:110:82:110:Infinity":1,"f:118:16:118:33":2,"s:120:2:124:Infinity":7,"s:121:4:121:Infinity":8,"s:123:4:123:Infinity":9,"b:125:2:125:Infinity:undefined:undefined:undefined:undefined":2,"s:125:2:125:Infinity":10,"b:125:6:125:35:125:35:125:53:125:53:125:75":3,"s:125:75:125:Infinity":11,"s:126:16:126:Infinity":12,"b:127:2:127:Infinity:undefined:undefined:undefined:undefined":4,"s:127:2:127:Infinity":13,"s:127:35:127:Infinity":14,"b:129:4:133:Infinity:134:4:137:Infinity:138:4:145:Infinity:146:4:147:Infinity:148:4:151:Infinity:152:4:159:Infinity:160:4:163:Infinity:164:4:168:Infinity:169:4:170:Infinity:171:4:174:Infinity:175:4:176:Infinity":5,"s:128:2:177:Infinity":15,"s:130:6:133:Infinity":16,"b:132:10:132:Infinity:133:10:133:Infinity":6,"b:130:13:130:Infinity:131:11:131:Infinity":7,"s:135:6:137:Infinity":17,"b:136:10:136:Infinity:137:10:137:Infinity":8,"b:135:13:135:45:135:45:135:Infinity":9,"b:139:6:139:Infinity:undefined:undefined:undefined:undefined":10,"s:139:6:139:Infinity":18,"s:139:40:139:Infinity":19,"b:140:6:142:Infinity:undefined:undefined:undefined:undefined":11,"s:140:6:142:Infinity":20,"b:140:10:140:31:140:31:140:50":12,"s:141:8:141:Infinity":21,"s:143:6:145:Infinity":22,"b:144:10:144:Infinity:145:10:145:Infinity":13,"s:147:6:147:Infinity":23,"s:149:6:151:Infinity":24,"b:150:10:150:Infinity:151:10:151:Infinity":14,"b:153:6:153:Infinity:undefined:undefined:undefined:undefined":15,"s:153:6:153:Infinity":25,"s:153:40:153:Infinity":26,"b:154:6:156:Infinity:undefined:undefined:undefined:undefined":16,"s:154:6:156:Infinity":27,"b:154:10:154:31:154:31:154:50":17,"s:155:8:155:Infinity":28,"s:157:6:159:Infinity":29,"b:158:10:158:Infinity:159:10:159:Infinity":18,"b:157:13:157:48:157:48:157:Infinity":19,"s:161:6:163:Infinity":30,"b:162:10:162:Infinity:163:10:163:Infinity":20,"b:161:13:161:48:161:48:161:Infinity":21,"s:165:6:168:Infinity":31,"b:167:10:167:Infinity:168:10:168:Infinity":22,"b:165:13:165:45:165:45:165:Infinity:166:11:166:45:166:45:166:68:166:68:166:Infinity":23,"s:170:6:170:Infinity":32,"s:172:6:174:Infinity":33,"b:173:10:173:Infinity:174:10:174:Infinity":24,"b:172:13:172:47:172:47:172:Infinity":25,"s:176:6:176:Infinity":34,"f:180:9:180:16":3,"b:181:2:181:Infinity:undefined:undefined:undefined:undefined":26,"s:181:2:181:Infinity":35,"b:181:6:181:35:181:35:181:51":27,"s:181:51:181:Infinity":36,"s:182:15:182:Infinity":37,"s:183:2:185:Infinity":38,"b:183:9:183:Infinity:184:7:184:52:184:52:184:Infinity:185:7:185:55:185:55:185:Infinity":28,"f:188:9:188:21":4,"s:189:2:191:Infinity":39,"b:189:9:189:38:189:38:189:Infinity:190:7:190:Infinity:191:7:191:Infinity":29},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/server.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/server.ts","statementMap":{"0":{"start":{"line":44,"column":27},"end":{"line":55,"column":null}},"1":{"start":{"line":59,"column":2},"end":{"line":59,"column":null}},"2":{"start":{"line":68,"column":4},"end":{"line":68,"column":null}},"3":{"start":{"line":65,"column":13},"end":{"line":65,"column":null}},"4":{"start":{"line":69,"column":4},"end":{"line":69,"column":null}},"5":{"start":{"line":118,"column":2},"end":{"line":118,"column":null}},"6":{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},"7":{"start":{"line":129,"column":27},"end":{"line":129,"column":null}},"8":{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},"9":{"start":{"line":130,"column":29},"end":{"line":130,"column":null}},"10":{"start":{"line":131,"column":2},"end":{"line":131,"column":null}},"11":{"start":{"line":139,"column":25},"end":{"line":139,"column":null}},"12":{"start":{"line":140,"column":44},"end":{"line":140,"column":null}},"13":{"start":{"line":141,"column":34},"end":{"line":141,"column":null}},"14":{"start":{"line":142,"column":19},"end":{"line":142,"column":null}},"15":{"start":{"line":144,"column":31},"end":{"line":144,"column":55}},"16":{"start":{"line":153,"column":19},"end":{"line":153,"column":null}},"17":{"start":{"line":154,"column":19},"end":{"line":154,"column":null}},"18":{"start":{"line":155,"column":4},"end":{"line":155,"column":null}},"19":{"start":{"line":155,"column":56},"end":{"line":155,"column":88}},"20":{"start":{"line":174,"column":17},"end":{"line":174,"column":null}},"21":{"start":{"line":175,"column":4},"end":{"line":177,"column":null}},"22":{"start":{"line":176,"column":6},"end":{"line":176,"column":null}},"23":{"start":{"line":181,"column":4},"end":{"line":183,"column":null}},"24":{"start":{"line":182,"column":6},"end":{"line":182,"column":null}},"25":{"start":{"line":184,"column":10},"end":{"line":184,"column":null}},"26":{"start":{"line":185,"column":4},"end":{"line":209,"column":null}},"27":{"start":{"line":187,"column":12},"end":{"line":192,"column":null}},"28":{"start":{"line":188,"column":8},"end":{"line":188,"column":null}},"29":{"start":{"line":189,"column":8},"end":{"line":189,"column":null}},"30":{"start":{"line":190,"column":8},"end":{"line":190,"column":null}},"31":{"start":{"line":191,"column":8},"end":{"line":191,"column":null}},"32":{"start":{"line":193,"column":12},"end":{"line":195,"column":null}},"33":{"start":{"line":194,"column":8},"end":{"line":194,"column":null}},"34":{"start":{"line":196,"column":6},"end":{"line":198,"column":null}},"35":{"start":{"line":197,"column":8},"end":{"line":197,"column":null}},"36":{"start":{"line":199,"column":6},"end":{"line":199,"column":null}},"37":{"start":{"line":200,"column":6},"end":{"line":200,"column":null}},"38":{"start":{"line":201,"column":6},"end":{"line":208,"column":null}},"39":{"start":{"line":220,"column":4},"end":{"line":220,"column":null}},"40":{"start":{"line":220,"column":21},"end":{"line":220,"column":null}},"41":{"start":{"line":221,"column":4},"end":{"line":221,"column":null}},"42":{"start":{"line":223,"column":18},"end":{"line":223,"column":null}},"43":{"start":{"line":224,"column":4},"end":{"line":224,"column":null}},"44":{"start":{"line":225,"column":4},"end":{"line":225,"column":null}},"45":{"start":{"line":225,"column":43},"end":{"line":225,"column":null}},"46":{"start":{"line":226,"column":4},"end":{"line":226,"column":null}},"47":{"start":{"line":227,"column":4},"end":{"line":235,"column":null}},"48":{"start":{"line":228,"column":6},"end":{"line":234,"column":null}},"49":{"start":{"line":236,"column":4},"end":{"line":236,"column":null}},"50":{"start":{"line":241,"column":4},"end":{"line":241,"column":null}},"51":{"start":{"line":245,"column":49},"end":{"line":247,"column":null}},"52":{"start":{"line":246,"column":6},"end":{"line":246,"column":null}},"53":{"start":{"line":249,"column":10},"end":{"line":283,"column":null}},"54":{"start":{"line":250,"column":19},"end":{"line":250,"column":null}},"55":{"start":{"line":251,"column":12},"end":{"line":251,"column":null}},"56":{"start":{"line":252,"column":6},"end":{"line":255,"column":null}},"57":{"start":{"line":253,"column":8},"end":{"line":253,"column":null}},"58":{"start":{"line":254,"column":8},"end":{"line":254,"column":null}},"59":{"start":{"line":256,"column":6},"end":{"line":281,"column":null}},"60":{"start":{"line":258,"column":8},"end":{"line":261,"column":null}},"61":{"start":{"line":259,"column":10},"end":{"line":259,"column":null}},"62":{"start":{"line":260,"column":10},"end":{"line":260,"column":null}},"63":{"start":{"line":270,"column":32},"end":{"line":272,"column":null}},"64":{"start":{"line":273,"column":8},"end":{"line":276,"column":null}},"65":{"start":{"line":274,"column":10},"end":{"line":274,"column":null}},"66":{"start":{"line":275,"column":10},"end":{"line":275,"column":null}},"67":{"start":{"line":277,"column":8},"end":{"line":277,"column":null}},"68":{"start":{"line":278,"column":8},"end":{"line":278,"column":null}},"69":{"start":{"line":279,"column":8},"end":{"line":279,"column":null}},"70":{"start":{"line":280,"column":8},"end":{"line":280,"column":null}},"71":{"start":{"line":282,"column":6},"end":{"line":282,"column":null}},"72":{"start":{"line":284,"column":10},"end":{"line":287,"column":null}},"73":{"start":{"line":285,"column":6},"end":{"line":285,"column":null}},"74":{"start":{"line":285,"column":36},"end":{"line":285,"column":null}},"75":{"start":{"line":286,"column":6},"end":{"line":286,"column":null}},"76":{"start":{"line":286,"column":59},"end":{"line":286,"column":null}},"77":{"start":{"line":288,"column":4},"end":{"line":288,"column":null}},"78":{"start":{"line":289,"column":4},"end":{"line":289,"column":null}},"79":{"start":{"line":290,"column":4},"end":{"line":290,"column":null}},"80":{"start":{"line":295,"column":4},"end":{"line":295,"column":null}},"81":{"start":{"line":296,"column":18},"end":{"line":296,"column":null}},"82":{"start":{"line":297,"column":17},"end":{"line":297,"column":null}},"83":{"start":{"line":297,"column":37},"end":{"line":297,"column":66}},"84":{"start":{"line":298,"column":10},"end":{"line":312,"column":null}},"85":{"start":{"line":299,"column":6},"end":{"line":311,"column":null}},"86":{"start":{"line":300,"column":8},"end":{"line":306,"column":null}},"87":{"start":{"line":301,"column":10},"end":{"line":301,"column":null}},"88":{"start":{"line":301,"column":48},"end":{"line":301,"column":null}},"89":{"start":{"line":302,"column":10},"end":{"line":305,"column":null}},"90":{"start":{"line":308,"column":8},"end":{"line":310,"column":null}},"91":{"start":{"line":309,"column":10},"end":{"line":309,"column":null}},"92":{"start":{"line":313,"column":4},"end":{"line":313,"column":null}},"93":{"start":{"line":314,"column":4},"end":{"line":314,"column":null}},"94":{"start":{"line":315,"column":4},"end":{"line":318,"column":null}},"95":{"start":{"line":316,"column":6},"end":{"line":316,"column":null}},"96":{"start":{"line":317,"column":6},"end":{"line":317,"column":null}},"97":{"start":{"line":322,"column":4},"end":{"line":340,"column":null}},"98":{"start":{"line":324,"column":8},"end":{"line":324,"column":null}},"99":{"start":{"line":325,"column":8},"end":{"line":325,"column":null}},"100":{"start":{"line":327,"column":8},"end":{"line":327,"column":null}},"101":{"start":{"line":328,"column":8},"end":{"line":328,"column":null}},"102":{"start":{"line":339,"column":8},"end":{"line":339,"column":null}},"103":{"start":{"line":344,"column":17},"end":{"line":344,"column":null}},"104":{"start":{"line":348,"column":22},"end":{"line":348,"column":null}},"105":{"start":{"line":349,"column":4},"end":{"line":352,"column":null}},"106":{"start":{"line":350,"column":6},"end":{"line":350,"column":null}},"107":{"start":{"line":351,"column":6},"end":{"line":351,"column":null}},"108":{"start":{"line":353,"column":17},"end":{"line":353,"column":null}},"109":{"start":{"line":354,"column":20},"end":{"line":358,"column":null}},"110":{"start":{"line":359,"column":4},"end":{"line":375,"column":null}},"111":{"start":{"line":360,"column":23},"end":{"line":360,"column":null}},"112":{"start":{"line":361,"column":19},"end":{"line":361,"column":null}},"113":{"start":{"line":362,"column":6},"end":{"line":365,"column":null}},"114":{"start":{"line":363,"column":8},"end":{"line":363,"column":null}},"115":{"start":{"line":364,"column":8},"end":{"line":364,"column":null}},"116":{"start":{"line":367,"column":6},"end":{"line":371,"column":null}},"117":{"start":{"line":368,"column":8},"end":{"line":368,"column":null}},"118":{"start":{"line":370,"column":8},"end":{"line":370,"column":null}},"119":{"start":{"line":372,"column":6},"end":{"line":372,"column":null}},"120":{"start":{"line":374,"column":6},"end":{"line":374,"column":null}},"121":{"start":{"line":379,"column":20},"end":{"line":379,"column":null}},"122":{"start":{"line":380,"column":4},"end":{"line":380,"column":null}},"123":{"start":{"line":380,"column":31},"end":{"line":380,"column":null}},"124":{"start":{"line":381,"column":4},"end":{"line":381,"column":null}},"125":{"start":{"line":382,"column":4},"end":{"line":382,"column":null}},"126":{"start":{"line":383,"column":4},"end":{"line":384,"column":null}},"127":{"start":{"line":383,"column":12},"end":{"line":383,"column":null}},"128":{"start":{"line":384,"column":9},"end":{"line":384,"column":null}},"129":{"start":{"line":389,"column":17},"end":{"line":389,"column":null}},"130":{"start":{"line":390,"column":4},"end":{"line":390,"column":null}},"131":{"start":{"line":390,"column":23},"end":{"line":390,"column":null}},"132":{"start":{"line":391,"column":4},"end":{"line":391,"column":null}},"133":{"start":{"line":392,"column":4},"end":{"line":392,"column":null}},"134":{"start":{"line":393,"column":4},"end":{"line":393,"column":null}},"135":{"start":{"line":394,"column":4},"end":{"line":396,"column":null}},"136":{"start":{"line":395,"column":6},"end":{"line":395,"column":null}},"137":{"start":{"line":397,"column":4},"end":{"line":401,"column":null}},"138":{"start":{"line":398,"column":6},"end":{"line":398,"column":null}},"139":{"start":{"line":399,"column":6},"end":{"line":399,"column":null}},"140":{"start":{"line":400,"column":6},"end":{"line":400,"column":null}},"141":{"start":{"line":413,"column":2},"end":{"line":417,"column":null}},"142":{"start":{"line":414,"column":18},"end":{"line":414,"column":null}},"143":{"start":{"line":415,"column":4},"end":{"line":415,"column":null}},"144":{"start":{"line":415,"column":34},"end":{"line":415,"column":null}},"145":{"start":{"line":416,"column":4},"end":{"line":416,"column":null}},"146":{"start":{"line":418,"column":2},"end":{"line":418,"column":null}},"147":{"start":{"line":429,"column":2},"end":{"line":433,"column":null}},"148":{"start":{"line":430,"column":21},"end":{"line":430,"column":null}},"149":{"start":{"line":431,"column":4},"end":{"line":431,"column":null}},"150":{"start":{"line":431,"column":59},"end":{"line":431,"column":null}},"151":{"start":{"line":432,"column":4},"end":{"line":432,"column":null}},"152":{"start":{"line":434,"column":2},"end":{"line":434,"column":null}}},"fnMap":{"0":{"name":"isLoopbackAddress","decl":{"start":{"line":58,"column":16},"end":{"line":58,"column":34}},"loc":{"start":{"line":58,"column":72},"end":{"line":60,"column":null}},"line":58},"1":{"name":"constructor","decl":{"start":{"line":64,"column":2},"end":{"line":64,"column":null}},"loc":{"start":{"line":67,"column":4},"end":{"line":70,"column":null}},"line":67},"2":{"name":"sendFrame","decl":{"start":{"line":114,"column":9},"end":{"line":114,"column":19}},"loc":{"start":{"line":114,"column":60},"end":{"line":119,"column":null}},"line":114},"3":{"name":"messageToText","decl":{"start":{"line":128,"column":16},"end":{"line":128,"column":30}},"loc":{"start":{"line":128,"column":77},"end":{"line":132,"column":null}},"line":128},"4":{"name":"constructor_2","decl":{"start":{"line":144,"column":2},"end":{"line":144,"column":14}},"loc":{"start":{"line":144,"column":55},"end":{"line":144,"column":null}},"line":144},"5":{"name":"handleUpgrade","decl":{"start":{"line":152,"column":2},"end":{"line":152,"column":16}},"loc":{"start":{"line":152,"column":74},"end":{"line":156,"column":null}},"line":152},"6":{"name":"(anonymous_6)","decl":{"start":{"line":155,"column":40},"end":{"line":155,"column":47}},"loc":{"start":{"line":155,"column":54},"end":{"line":155,"column":89}},"line":155},"7":{"name":"requestTool","decl":{"start":{"line":168,"column":2},"end":{"line":168,"column":null}},"loc":{"start":{"line":173,"column":22},"end":{"line":210,"column":null}},"line":173},"8":{"name":"(anonymous_8)","decl":{"start":{"line":185,"column":15},"end":{"line":185,"column":33}},"loc":{"start":{"line":185,"column":53},"end":{"line":209,"column":5}},"line":185},"9":{"name":"(anonymous_9)","decl":{"start":{"line":187,"column":12},"end":{"line":187,"column":22}},"loc":{"start":{"line":187,"column":55},"end":{"line":192,"column":null}},"line":187},"10":{"name":"(anonymous_10)","decl":{"start":{"line":193,"column":12},"end":{"line":193,"column":34}},"loc":{"start":{"line":193,"column":34},"end":{"line":195,"column":null}},"line":193},"11":{"name":"(anonymous_11)","decl":{"start":{"line":196,"column":14},"end":{"line":196,"column":31}},"loc":{"start":{"line":196,"column":31},"end":{"line":198,"column":9}},"line":196},"12":{"name":"(anonymous_12)","decl":{"start":{"line":201,"column":90},"end":{"line":201,"column":94}},"loc":{"start":{"line":201,"column":104},"end":{"line":208,"column":7}},"line":201},"13":{"name":"close","decl":{"start":{"line":217,"column":8},"end":{"line":217,"column":31}},"loc":{"start":{"line":217,"column":31},"end":{"line":237,"column":null}},"line":217},"14":{"name":"(anonymous_14)","decl":{"start":{"line":227,"column":14},"end":{"line":227,"column":29}},"loc":{"start":{"line":227,"column":49},"end":{"line":235,"column":5}},"line":227},"15":{"name":"(anonymous_15)","decl":{"start":{"line":228,"column":15},"end":{"line":228,"column":22}},"loc":{"start":{"line":228,"column":32},"end":{"line":234,"column":7}},"line":228},"16":{"name":"hasConnection","decl":{"start":{"line":240,"column":2},"end":{"line":240,"column":27}},"loc":{"start":{"line":240,"column":27},"end":{"line":242,"column":null}},"line":240},"17":{"name":"attach","decl":{"start":{"line":244,"column":2},"end":{"line":244,"column":10}},"loc":{"start":{"line":244,"column":101},"end":{"line":291,"column":null}},"line":244},"18":{"name":"(anonymous_18)","decl":{"start":{"line":245,"column":49},"end":{"line":245,"column":66}},"loc":{"start":{"line":245,"column":66},"end":{"line":247,"column":7}},"line":245},"19":{"name":"(anonymous_19)","decl":{"start":{"line":249,"column":10},"end":{"line":249,"column":23}},"loc":{"start":{"line":249,"column":71},"end":{"line":283,"column":null}},"line":249},"20":{"name":"(anonymous_20)","decl":{"start":{"line":284,"column":10},"end":{"line":284,"column":32}},"loc":{"start":{"line":284,"column":32},"end":{"line":287,"column":null}},"line":284},"21":{"name":"promote","decl":{"start":{"line":294,"column":2},"end":{"line":294,"column":10}},"loc":{"start":{"line":294,"column":74},"end":{"line":319,"column":null}},"line":294},"22":{"name":"(anonymous_22)","decl":{"start":{"line":297,"column":17},"end":{"line":297,"column":35}},"loc":{"start":{"line":297,"column":35},"end":{"line":297,"column":69}},"line":297},"23":{"name":"(anonymous_23)","decl":{"start":{"line":298,"column":18},"end":{"line":298,"column":30}},"loc":{"start":{"line":298,"column":30},"end":{"line":312,"column":7}},"line":298},"24":{"name":"(anonymous_24)","decl":{"start":{"line":315,"column":12},"end":{"line":315,"column":27}},"loc":{"start":{"line":315,"column":27},"end":{"line":318,"column":5}},"line":315},"25":{"name":"handleReadyFrame","decl":{"start":{"line":321,"column":2},"end":{"line":321,"column":10}},"loc":{"start":{"line":321,"column":53},"end":{"line":341,"column":null}},"line":321},"26":{"name":"handleRpc","decl":{"start":{"line":343,"column":16},"end":{"line":343,"column":26}},"loc":{"start":{"line":343,"column":84},"end":{"line":376,"column":null}},"line":343},"27":{"name":"settleTool","decl":{"start":{"line":378,"column":2},"end":{"line":378,"column":10}},"loc":{"start":{"line":378,"column":70},"end":{"line":385,"column":null}},"line":378},"28":{"name":"replaceConnection","decl":{"start":{"line":388,"column":2},"end":{"line":388,"column":10}},"loc":{"start":{"line":388,"column":36},"end":{"line":402,"column":null}},"line":388},"29":{"name":"payloadCode","decl":{"start":{"line":412,"column":16},"end":{"line":412,"column":28}},"loc":{"start":{"line":412,"column":61},"end":{"line":419,"column":null}},"line":412},"30":{"name":"payloadMessage","decl":{"start":{"line":428,"column":16},"end":{"line":428,"column":31}},"loc":{"start":{"line":428,"column":57},"end":{"line":435,"column":null}},"line":428}},"branchMap":{"0":{"loc":{"start":{"line":59,"column":9},"end":{"line":59,"column":null}},"type":"binary-expr","locations":[{"start":{"line":59,"column":9},"end":{"line":59,"column":36}},{"start":{"line":59,"column":36},"end":{"line":59,"column":57}},{"start":{"line":59,"column":57},"end":{"line":59,"column":null}}],"line":59},"1":{"loc":{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},"type":"if","locations":[{"start":{"line":129,"column":2},"end":{"line":129,"column":null}},{"start":{},"end":{}}],"line":129},"2":{"loc":{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},"type":"if","locations":[{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},{"start":{},"end":{}}],"line":130},"3":{"loc":{"start":{"line":153,"column":19},"end":{"line":153,"column":null}},"type":"binary-expr","locations":[{"start":{"line":153,"column":19},"end":{"line":153,"column":54}},{"start":{"line":153,"column":54},"end":{"line":153,"column":null}}],"line":153},"4":{"loc":{"start":{"line":172,"column":4},"end":{"line":172,"column":null}},"type":"default-arg","locations":[{"start":{"line":172,"column":24},"end":{"line":172,"column":null}}],"line":172},"5":{"loc":{"start":{"line":175,"column":4},"end":{"line":177,"column":null}},"type":"if","locations":[{"start":{"line":175,"column":4},"end":{"line":177,"column":null}},{"start":{},"end":{}}],"line":175},"6":{"loc":{"start":{"line":181,"column":4},"end":{"line":183,"column":null}},"type":"if","locations":[{"start":{"line":181,"column":4},"end":{"line":183,"column":null}},{"start":{},"end":{}}],"line":181},"7":{"loc":{"start":{"line":220,"column":4},"end":{"line":220,"column":null}},"type":"if","locations":[{"start":{"line":220,"column":4},"end":{"line":220,"column":null}},{"start":{},"end":{}}],"line":220},"8":{"loc":{"start":{"line":223,"column":18},"end":{"line":223,"column":null}},"type":"cond-expr","locations":[{"start":{"line":223,"column":42},"end":{"line":223,"column":47}},{"start":{"line":223,"column":47},"end":{"line":223,"column":null}}],"line":223},"9":{"loc":{"start":{"line":247,"column":7},"end":{"line":247,"column":51}},"type":"binary-expr","locations":[{"start":{"line":247,"column":7},"end":{"line":247,"column":35}},{"start":{"line":247,"column":35},"end":{"line":247,"column":51}}],"line":247},"10":{"loc":{"start":{"line":252,"column":6},"end":{"line":255,"column":null}},"type":"if","locations":[{"start":{"line":252,"column":6},"end":{"line":255,"column":null}},{"start":{},"end":{}}],"line":252},"11":{"loc":{"start":{"line":256,"column":6},"end":{"line":281,"column":null}},"type":"if","locations":[{"start":{"line":256,"column":6},"end":{"line":281,"column":null}},{"start":{},"end":{}}],"line":256},"12":{"loc":{"start":{"line":258,"column":8},"end":{"line":261,"column":null}},"type":"if","locations":[{"start":{"line":258,"column":8},"end":{"line":261,"column":null}},{"start":{},"end":{}}],"line":258},"13":{"loc":{"start":{"line":270,"column":32},"end":{"line":272,"column":null}},"type":"binary-expr","locations":[{"start":{"line":270,"column":32},"end":{"line":270,"column":null}},{"start":{"line":271,"column":13},"end":{"line":271,"column":null}},{"start":{"line":272,"column":13},"end":{"line":272,"column":null}}],"line":270},"14":{"loc":{"start":{"line":273,"column":8},"end":{"line":276,"column":null}},"type":"if","locations":[{"start":{"line":273,"column":8},"end":{"line":276,"column":null}},{"start":{},"end":{}}],"line":273},"15":{"loc":{"start":{"line":273,"column":12},"end":{"line":273,"column":76}},"type":"binary-expr","locations":[{"start":{"line":273,"column":12},"end":{"line":273,"column":32}},{"start":{"line":273,"column":32},"end":{"line":273,"column":76}}],"line":273},"16":{"loc":{"start":{"line":285,"column":6},"end":{"line":285,"column":null}},"type":"if","locations":[{"start":{"line":285,"column":6},"end":{"line":285,"column":null}},{"start":{},"end":{}}],"line":285},"17":{"loc":{"start":{"line":286,"column":6},"end":{"line":286,"column":null}},"type":"if","locations":[{"start":{"line":286,"column":6},"end":{"line":286,"column":null}},{"start":{},"end":{}}],"line":286},"18":{"loc":{"start":{"line":286,"column":10},"end":{"line":286,"column":59}},"type":"binary-expr","locations":[{"start":{"line":286,"column":10},"end":{"line":286,"column":35}},{"start":{"line":286,"column":35},"end":{"line":286,"column":59}}],"line":286},"19":{"loc":{"start":{"line":297,"column":69},"end":{"line":297,"column":113}},"type":"binary-expr","locations":[{"start":{"line":297,"column":69},"end":{"line":297,"column":97}},{"start":{"line":297,"column":97},"end":{"line":297,"column":113}}],"line":297},"20":{"loc":{"start":{"line":301,"column":10},"end":{"line":301,"column":null}},"type":"if","locations":[{"start":{"line":301,"column":10},"end":{"line":301,"column":null}},{"start":{},"end":{}}],"line":301},"21":{"loc":{"start":{"line":308,"column":8},"end":{"line":310,"column":null}},"type":"if","locations":[{"start":{"line":308,"column":8},"end":{"line":310,"column":null}},{"start":{},"end":{}}],"line":308},"22":{"loc":{"start":{"line":308,"column":12},"end":{"line":308,"column":71}},"type":"binary-expr","locations":[{"start":{"line":308,"column":12},"end":{"line":308,"column":37}},{"start":{"line":308,"column":37},"end":{"line":308,"column":71}}],"line":308},"23":{"loc":{"start":{"line":322,"column":4},"end":{"line":340,"column":null}},"type":"switch","locations":[{"start":{"line":323,"column":6},"end":{"line":325,"column":null}},{"start":{"line":326,"column":6},"end":{"line":328,"column":null}},{"start":{"line":329,"column":6},"end":{"line":329,"column":null}},{"start":{"line":330,"column":6},"end":{"line":330,"column":null}},{"start":{"line":331,"column":6},"end":{"line":331,"column":null}},{"start":{"line":332,"column":6},"end":{"line":332,"column":null}},{"start":{"line":333,"column":6},"end":{"line":333,"column":null}},{"start":{"line":334,"column":6},"end":{"line":334,"column":null}},{"start":{"line":335,"column":6},"end":{"line":335,"column":null}},{"start":{"line":336,"column":6},"end":{"line":339,"column":null}}],"line":322},"24":{"loc":{"start":{"line":327,"column":44},"end":{"line":327,"column":81}},"type":"cond-expr","locations":[{"start":{"line":327,"column":55},"end":{"line":327,"column":70}},{"start":{"line":327,"column":70},"end":{"line":327,"column":81}}],"line":327},"25":{"loc":{"start":{"line":348,"column":22},"end":{"line":348,"column":null}},"type":"binary-expr","locations":[{"start":{"line":348,"column":22},"end":{"line":348,"column":62}},{"start":{"line":348,"column":62},"end":{"line":348,"column":null}}],"line":348},"26":{"loc":{"start":{"line":349,"column":4},"end":{"line":352,"column":null}},"type":"if","locations":[{"start":{"line":349,"column":4},"end":{"line":352,"column":null}},{"start":{},"end":{}}],"line":349},"27":{"loc":{"start":{"line":362,"column":6},"end":{"line":365,"column":null}},"type":"if","locations":[{"start":{"line":362,"column":6},"end":{"line":365,"column":null}},{"start":{},"end":{}}],"line":362},"28":{"loc":{"start":{"line":380,"column":4},"end":{"line":380,"column":null}},"type":"if","locations":[{"start":{"line":380,"column":4},"end":{"line":380,"column":null}},{"start":{},"end":{}}],"line":380},"29":{"loc":{"start":{"line":383,"column":4},"end":{"line":384,"column":null}},"type":"if","locations":[{"start":{"line":383,"column":4},"end":{"line":384,"column":null}},{"start":{"line":384,"column":9},"end":{"line":384,"column":null}}],"line":383},"30":{"loc":{"start":{"line":390,"column":4},"end":{"line":390,"column":null}},"type":"if","locations":[{"start":{"line":390,"column":4},"end":{"line":390,"column":null}},{"start":{},"end":{}}],"line":390},"31":{"loc":{"start":{"line":394,"column":4},"end":{"line":396,"column":null}},"type":"if","locations":[{"start":{"line":394,"column":4},"end":{"line":396,"column":null}},{"start":{},"end":{}}],"line":394},"32":{"loc":{"start":{"line":394,"column":8},"end":{"line":394,"column":94}},"type":"binary-expr","locations":[{"start":{"line":394,"column":8},"end":{"line":394,"column":49}},{"start":{"line":394,"column":49},"end":{"line":394,"column":94}}],"line":394},"33":{"loc":{"start":{"line":413,"column":2},"end":{"line":417,"column":null}},"type":"if","locations":[{"start":{"line":413,"column":2},"end":{"line":417,"column":null}},{"start":{},"end":{}}],"line":413},"34":{"loc":{"start":{"line":413,"column":6},"end":{"line":413,"column":55}},"type":"binary-expr","locations":[{"start":{"line":413,"column":6},"end":{"line":413,"column":37}},{"start":{"line":413,"column":37},"end":{"line":413,"column":55}}],"line":413},"35":{"loc":{"start":{"line":415,"column":4},"end":{"line":415,"column":null}},"type":"if","locations":[{"start":{"line":415,"column":4},"end":{"line":415,"column":null}},{"start":{},"end":{}}],"line":415},"36":{"loc":{"start":{"line":429,"column":2},"end":{"line":433,"column":null}},"type":"if","locations":[{"start":{"line":429,"column":2},"end":{"line":433,"column":null}},{"start":{},"end":{}}],"line":429},"37":{"loc":{"start":{"line":429,"column":6},"end":{"line":429,"column":55}},"type":"binary-expr","locations":[{"start":{"line":429,"column":6},"end":{"line":429,"column":37}},{"start":{"line":429,"column":37},"end":{"line":429,"column":55}}],"line":429},"38":{"loc":{"start":{"line":431,"column":4},"end":{"line":431,"column":null}},"type":"if","locations":[{"start":{"line":431,"column":4},"end":{"line":431,"column":null}},{"start":{},"end":{}}],"line":431},"39":{"loc":{"start":{"line":431,"column":8},"end":{"line":431,"column":59}},"type":"binary-expr","locations":[{"start":{"line":431,"column":8},"end":{"line":431,"column":39}},{"start":{"line":431,"column":39},"end":{"line":431,"column":59}}],"line":431}},"s":{"0":4,"1":35,"2":9,"3":9,"4":9,"5":99,"6":52,"7":1,"8":51,"9":50,"10":1,"11":35,"12":35,"13":35,"14":35,"15":35,"16":31,"17":31,"18":31,"19":31,"20":10,"21":10,"22":2,"23":8,"24":1,"25":7,"26":7,"27":7,"28":2,"29":2,"30":2,"31":2,"32":7,"33":1,"34":7,"35":1,"36":7,"37":7,"38":7,"39":35,"40":2,"41":33,"42":33,"43":35,"44":35,"45":21,"46":33,"47":33,"48":33,"49":33,"50":4,"51":31,"52":1,"53":31,"54":49,"55":49,"56":49,"57":1,"58":1,"59":48,"60":30,"61":1,"62":1,"63":29,"64":30,"65":3,"66":3,"67":26,"68":26,"69":26,"70":26,"71":18,"72":31,"73":31,"74":5,"75":31,"76":6,"77":31,"78":31,"79":31,"80":26,"81":26,"82":26,"83":1,"84":26,"85":26,"86":26,"87":59,"88":1,"89":58,"90":2,"91":1,"92":26,"93":26,"94":26,"95":26,"96":26,"97":18,"98":13,"99":13,"100":3,"101":3,"102":2,"103":13,"104":13,"105":13,"106":1,"107":1,"108":12,"109":12,"110":12,"111":12,"112":11,"113":11,"114":1,"115":1,"116":10,"117":10,"118":1,"119":10,"120":1,"121":3,"122":3,"123":1,"124":2,"125":2,"126":2,"127":2,"128":1,"129":65,"130":65,"131":39,"132":26,"133":26,"134":26,"135":26,"136":19,"137":26,"138":3,"139":3,"140":3,"141":5,"142":3,"143":3,"144":2,"145":1,"146":2,"147":5,"148":4,"149":4,"150":2,"151":2,"152":1},"f":{"0":35,"1":9,"2":99,"3":52,"4":35,"5":31,"6":31,"7":10,"8":7,"9":2,"10":1,"11":1,"12":7,"13":35,"14":33,"15":33,"16":4,"17":31,"18":1,"19":49,"20":31,"21":26,"22":1,"23":26,"24":26,"25":18,"26":13,"27":3,"28":65,"29":5,"30":5},"b":{"0":[35,7,6],"1":[1,51],"2":[50,1],"3":[31,29],"4":[10],"5":[2,8],"6":[1,7],"7":[2,33],"8":[14,19],"9":[31,30],"10":[1,48],"11":[30,18],"12":[1,29],"13":[29,27,5],"14":[3,27],"15":[30,25],"16":[5,26],"17":[6,25],"18":[31,7],"19":[26,25],"20":[1,58],"21":[1,1],"22":[2,1],"23":[13,3,1,2,2,2,2,2,2,2],"24":[2,1],"25":[13,1],"26":[1,12],"27":[1,10],"28":[1,2],"29":[1,1],"30":[39,26],"31":[19,7],"32":[26,7],"33":[3,2],"34":[5,4],"35":[2,1],"36":[4,1],"37":[5,4],"38":[2,2],"39":[4,3]},"meta":{"lastBranch":40,"lastFunction":31,"lastStatement":153,"seen":{"s:44:27:55:Infinity":0,"f:58:16:58:34":0,"s:59:2:59:Infinity":1,"b:59:9:59:36:59:36:59:57:59:57:59:Infinity":0,"f:64:2:64:Infinity":1,"s:68:4:68:Infinity":2,"s:65:13:65:Infinity":3,"s:69:4:69:Infinity":4,"f:114:9:114:19":2,"s:118:2:118:Infinity":5,"f:128:16:128:30":3,"b:129:2:129:Infinity:undefined:undefined:undefined:undefined":1,"s:129:2:129:Infinity":6,"s:129:27:129:Infinity":7,"b:130:2:130:Infinity:undefined:undefined:undefined:undefined":2,"s:130:2:130:Infinity":8,"s:130:29:130:Infinity":9,"s:131:2:131:Infinity":10,"s:139:25:139:Infinity":11,"s:140:44:140:Infinity":12,"s:141:34:141:Infinity":13,"s:142:19:142:Infinity":14,"f:144:2:144:14":4,"s:144:31:144:55":15,"f:152:2:152:16":5,"s:153:19:153:Infinity":16,"b:153:19:153:54:153:54:153:Infinity":3,"s:154:19:154:Infinity":17,"s:155:4:155:Infinity":18,"f:155:40:155:47":6,"s:155:56:155:88":19,"f:168:2:168:Infinity":7,"b:172:24:172:Infinity":4,"s:174:17:174:Infinity":20,"b:175:4:177:Infinity:undefined:undefined:undefined:undefined":5,"s:175:4:177:Infinity":21,"s:176:6:176:Infinity":22,"b:181:4:183:Infinity:undefined:undefined:undefined:undefined":6,"s:181:4:183:Infinity":23,"s:182:6:182:Infinity":24,"s:184:10:184:Infinity":25,"s:185:4:209:Infinity":26,"f:185:15:185:33":8,"s:187:12:192:Infinity":27,"f:187:12:187:22":9,"s:188:8:188:Infinity":28,"s:189:8:189:Infinity":29,"s:190:8:190:Infinity":30,"s:191:8:191:Infinity":31,"s:193:12:195:Infinity":32,"f:193:12:193:34":10,"s:194:8:194:Infinity":33,"s:196:6:198:Infinity":34,"f:196:14:196:31":11,"s:197:8:197:Infinity":35,"s:199:6:199:Infinity":36,"s:200:6:200:Infinity":37,"s:201:6:208:Infinity":38,"f:201:90:201:94":12,"f:217:8:217:31":13,"b:220:4:220:Infinity:undefined:undefined:undefined:undefined":7,"s:220:4:220:Infinity":39,"s:220:21:220:Infinity":40,"s:221:4:221:Infinity":41,"s:223:18:223:Infinity":42,"b:223:42:223:47:223:47:223:Infinity":8,"s:224:4:224:Infinity":43,"s:225:4:225:Infinity":44,"s:225:43:225:Infinity":45,"s:226:4:226:Infinity":46,"s:227:4:235:Infinity":47,"f:227:14:227:29":14,"s:228:6:234:Infinity":48,"f:228:15:228:22":15,"s:236:4:236:Infinity":49,"f:240:2:240:27":16,"s:241:4:241:Infinity":50,"f:244:2:244:10":17,"s:245:49:247:Infinity":51,"f:245:49:245:66":18,"s:246:6:246:Infinity":52,"b:247:7:247:35:247:35:247:51":9,"s:249:10:283:Infinity":53,"f:249:10:249:23":19,"s:250:19:250:Infinity":54,"s:251:12:251:Infinity":55,"b:252:6:255:Infinity:undefined:undefined:undefined:undefined":10,"s:252:6:255:Infinity":56,"s:253:8:253:Infinity":57,"s:254:8:254:Infinity":58,"b:256:6:281:Infinity:undefined:undefined:undefined:undefined":11,"s:256:6:281:Infinity":59,"b:258:8:261:Infinity:undefined:undefined:undefined:undefined":12,"s:258:8:261:Infinity":60,"s:259:10:259:Infinity":61,"s:260:10:260:Infinity":62,"s:270:32:272:Infinity":63,"b:270:32:270:Infinity:271:13:271:Infinity:272:13:272:Infinity":13,"b:273:8:276:Infinity:undefined:undefined:undefined:undefined":14,"s:273:8:276:Infinity":64,"b:273:12:273:32:273:32:273:76":15,"s:274:10:274:Infinity":65,"s:275:10:275:Infinity":66,"s:277:8:277:Infinity":67,"s:278:8:278:Infinity":68,"s:279:8:279:Infinity":69,"s:280:8:280:Infinity":70,"s:282:6:282:Infinity":71,"s:284:10:287:Infinity":72,"f:284:10:284:32":20,"b:285:6:285:Infinity:undefined:undefined:undefined:undefined":16,"s:285:6:285:Infinity":73,"s:285:36:285:Infinity":74,"b:286:6:286:Infinity:undefined:undefined:undefined:undefined":17,"s:286:6:286:Infinity":75,"b:286:10:286:35:286:35:286:59":18,"s:286:59:286:Infinity":76,"s:288:4:288:Infinity":77,"s:289:4:289:Infinity":78,"s:290:4:290:Infinity":79,"f:294:2:294:10":21,"s:295:4:295:Infinity":80,"s:296:18:296:Infinity":81,"s:297:17:297:Infinity":82,"f:297:17:297:35":22,"s:297:37:297:66":83,"b:297:69:297:97:297:97:297:113":19,"s:298:10:312:Infinity":84,"f:298:18:298:30":23,"s:299:6:311:Infinity":85,"s:300:8:306:Infinity":86,"b:301:10:301:Infinity:undefined:undefined:undefined:undefined":20,"s:301:10:301:Infinity":87,"s:301:48:301:Infinity":88,"s:302:10:305:Infinity":89,"b:308:8:310:Infinity:undefined:undefined:undefined:undefined":21,"s:308:8:310:Infinity":90,"b:308:12:308:37:308:37:308:71":22,"s:309:10:309:Infinity":91,"s:313:4:313:Infinity":92,"s:314:4:314:Infinity":93,"s:315:4:318:Infinity":94,"f:315:12:315:27":24,"s:316:6:316:Infinity":95,"s:317:6:317:Infinity":96,"f:321:2:321:10":25,"b:323:6:325:Infinity:326:6:328:Infinity:329:6:329:Infinity:330:6:330:Infinity:331:6:331:Infinity:332:6:332:Infinity:333:6:333:Infinity:334:6:334:Infinity:335:6:335:Infinity:336:6:339:Infinity":23,"s:322:4:340:Infinity":97,"s:324:8:324:Infinity":98,"s:325:8:325:Infinity":99,"s:327:8:327:Infinity":100,"b:327:55:327:70:327:70:327:81":24,"s:328:8:328:Infinity":101,"s:339:8:339:Infinity":102,"f:343:16:343:26":26,"s:344:17:344:Infinity":103,"s:348:22:348:Infinity":104,"b:348:22:348:62:348:62:348:Infinity":25,"b:349:4:352:Infinity:undefined:undefined:undefined:undefined":26,"s:349:4:352:Infinity":105,"s:350:6:350:Infinity":106,"s:351:6:351:Infinity":107,"s:353:17:353:Infinity":108,"s:354:20:358:Infinity":109,"s:359:4:375:Infinity":110,"s:360:23:360:Infinity":111,"s:361:19:361:Infinity":112,"b:362:6:365:Infinity:undefined:undefined:undefined:undefined":27,"s:362:6:365:Infinity":113,"s:363:8:363:Infinity":114,"s:364:8:364:Infinity":115,"s:367:6:371:Infinity":116,"s:368:8:368:Infinity":117,"s:370:8:370:Infinity":118,"s:372:6:372:Infinity":119,"s:374:6:374:Infinity":120,"f:378:2:378:10":27,"s:379:20:379:Infinity":121,"b:380:4:380:Infinity:undefined:undefined:undefined:undefined":28,"s:380:4:380:Infinity":122,"s:380:31:380:Infinity":123,"s:381:4:381:Infinity":124,"s:382:4:382:Infinity":125,"b:383:4:384:Infinity:384:9:384:Infinity":29,"s:383:4:384:Infinity":126,"s:383:12:383:Infinity":127,"s:384:9:384:Infinity":128,"f:388:2:388:10":28,"s:389:17:389:Infinity":129,"b:390:4:390:Infinity:undefined:undefined:undefined:undefined":30,"s:390:4:390:Infinity":130,"s:390:23:390:Infinity":131,"s:391:4:391:Infinity":132,"s:392:4:392:Infinity":133,"s:393:4:393:Infinity":134,"b:394:4:396:Infinity:undefined:undefined:undefined:undefined":31,"s:394:4:396:Infinity":135,"b:394:8:394:49:394:49:394:94":32,"s:395:6:395:Infinity":136,"s:397:4:401:Infinity":137,"s:398:6:398:Infinity":138,"s:399:6:399:Infinity":139,"s:400:6:400:Infinity":140,"f:412:16:412:28":29,"b:413:2:417:Infinity:undefined:undefined:undefined:undefined":33,"s:413:2:417:Infinity":141,"b:413:6:413:37:413:37:413:55":34,"s:414:18:414:Infinity":142,"b:415:4:415:Infinity:undefined:undefined:undefined:undefined":35,"s:415:4:415:Infinity":143,"s:415:34:415:Infinity":144,"s:416:4:416:Infinity":145,"s:418:2:418:Infinity":146,"f:428:16:428:31":30,"b:429:2:433:Infinity:undefined:undefined:undefined:undefined":36,"s:429:2:433:Infinity":147,"b:429:6:429:37:429:37:429:55":37,"s:430:21:430:Infinity":148,"b:431:4:431:Infinity:undefined:undefined:undefined:undefined":38,"s:431:4:431:Infinity":149,"b:431:8:431:39:431:39:431:59":39,"s:431:59:431:Infinity":150,"s:432:4:432:Infinity":151,"s:434:2:434:Infinity":152},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/session-deferral.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/session-deferral.ts","statementMap":{"0":{"start":{"line":21,"column":27},"end":{"line":21,"column":null}},"1":{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},"2":{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},"3":{"start":{"line":45,"column":22},"end":{"line":45,"column":null}},"4":{"start":{"line":46,"column":24},"end":{"line":46,"column":null}},"5":{"start":{"line":48,"column":8},"end":{"line":53,"column":null}},"6":{"start":{"line":49,"column":19},"end":{"line":49,"column":null}},"7":{"start":{"line":50,"column":4},"end":{"line":52,"column":null}},"8":{"start":{"line":51,"column":6},"end":{"line":51,"column":null}},"9":{"start":{"line":51,"column":36},"end":{"line":51,"column":null}},"10":{"start":{"line":55,"column":8},"end":{"line":56,"column":null}},"11":{"start":{"line":56,"column":4},"end":{"line":56,"column":null}},"12":{"start":{"line":58,"column":2},"end":{"line":98,"column":null}},"13":{"start":{"line":63,"column":8},"end":{"line":63,"column":null}},"14":{"start":{"line":64,"column":26},"end":{"line":64,"column":null}},"15":{"start":{"line":65,"column":8},"end":{"line":65,"column":null}},"16":{"start":{"line":66,"column":8},"end":{"line":66,"column":null}},"17":{"start":{"line":69,"column":8},"end":{"line":69,"column":null}},"18":{"start":{"line":69,"column":57},"end":{"line":69,"column":null}},"19":{"start":{"line":70,"column":8},"end":{"line":70,"column":null}},"20":{"start":{"line":73,"column":22},"end":{"line":73,"column":null}},"21":{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},"22":{"start":{"line":74,"column":33},"end":{"line":74,"column":null}},"23":{"start":{"line":75,"column":25},"end":{"line":75,"column":null}},"24":{"start":{"line":76,"column":24},"end":{"line":79,"column":null}},"25":{"start":{"line":80,"column":8},"end":{"line":86,"column":null}},"26":{"start":{"line":81,"column":10},"end":{"line":81,"column":null}},"27":{"start":{"line":82,"column":10},"end":{"line":85,"column":null}},"28":{"start":{"line":83,"column":20},"end":{"line":83,"column":68}},"29":{"start":{"line":84,"column":20},"end":{"line":84,"column":68}},"30":{"start":{"line":87,"column":24},"end":{"line":87,"column":null}},"31":{"start":{"line":88,"column":8},"end":{"line":93,"column":null}},"32":{"start":{"line":92,"column":10},"end":{"line":92,"column":null}},"33":{"start":{"line":94,"column":8},"end":{"line":94,"column":null}},"34":{"start":{"line":95,"column":8},"end":{"line":95,"column":null}}},"fnMap":{"0":{"name":"withSessionDeferral","decl":{"start":{"line":42,"column":16},"end":{"line":42,"column":36}},"loc":{"start":{"line":42,"column":79},"end":{"line":99,"column":null}},"line":42},"1":{"name":"(anonymous_1)","decl":{"start":{"line":48,"column":8},"end":{"line":48,"column":28}},"loc":{"start":{"line":48,"column":28},"end":{"line":53,"column":null}},"line":48},"2":{"name":"(anonymous_2)","decl":{"start":{"line":55,"column":8},"end":{"line":55,"column":20}},"loc":{"start":{"line":56,"column":4},"end":{"line":56,"column":null}},"line":56},"3":{"name":"create","decl":{"start":{"line":62,"column":12},"end":{"line":62,"column":19}},"loc":{"start":{"line":62,"column":43},"end":{"line":67,"column":null}},"line":62},"4":{"name":"history","decl":{"start":{"line":68,"column":12},"end":{"line":68,"column":20}},"loc":{"start":{"line":68,"column":45},"end":{"line":71,"column":null}},"line":68},"5":{"name":"prompt","decl":{"start":{"line":72,"column":12},"end":{"line":72,"column":19}},"loc":{"start":{"line":72,"column":43},"end":{"line":96,"column":null}},"line":72},"6":{"name":"(anonymous_6)","decl":{"start":{"line":82,"column":23},"end":{"line":82,"column":null}},"loc":{"start":{"line":83,"column":18},"end":{"line":83,"column":null}},"line":83},"7":{"name":"(anonymous_7)","decl":{"start":{"line":83,"column":68},"end":{"line":83,"column":null}},"loc":{"start":{"line":84,"column":18},"end":{"line":84,"column":null}},"line":84}},"branchMap":{"0":{"loc":{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},"type":"if","locations":[{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},{"start":{},"end":{}}],"line":43},"1":{"loc":{"start":{"line":51,"column":6},"end":{"line":51,"column":null}},"type":"if","locations":[{"start":{"line":51,"column":6},"end":{"line":51,"column":null}},{"start":{},"end":{}}],"line":51},"2":{"loc":{"start":{"line":56,"column":4},"end":{"line":56,"column":null}},"type":"binary-expr","locations":[{"start":{"line":56,"column":4},"end":{"line":56,"column":25}},{"start":{"line":56,"column":25},"end":{"line":56,"column":null}}],"line":56},"3":{"loc":{"start":{"line":69,"column":8},"end":{"line":69,"column":null}},"type":"if","locations":[{"start":{"line":69,"column":8},"end":{"line":69,"column":null}},{"start":{},"end":{}}],"line":69},"4":{"loc":{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},"type":"if","locations":[{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},{"start":{},"end":{}}],"line":74},"5":{"loc":{"start":{"line":76,"column":24},"end":{"line":79,"column":null}},"type":"binary-expr","locations":[{"start":{"line":76,"column":24},"end":{"line":76,"column":36}},{"start":{"line":76,"column":36},"end":{"line":79,"column":null}}],"line":76},"6":{"loc":{"start":{"line":80,"column":8},"end":{"line":86,"column":null}},"type":"if","locations":[{"start":{"line":80,"column":8},"end":{"line":86,"column":null}},{"start":{},"end":{}}],"line":80},"7":{"loc":{"start":{"line":88,"column":8},"end":{"line":93,"column":null}},"type":"if","locations":[{"start":{"line":88,"column":8},"end":{"line":93,"column":null}},{"start":{},"end":{}}],"line":88}},"s":{"0":4,"1":15,"2":3,"3":12,"4":12,"5":12,"6":11,"7":11,"8":2,"9":1,"10":12,"11":11,"12":12,"13":11,"14":11,"15":11,"16":11,"17":7,"18":3,"19":4,"20":9,"21":9,"22":1,"23":8,"24":8,"25":9,"26":7,"27":7,"28":6,"29":1,"30":8,"31":7,"32":1,"33":6,"34":6},"f":{"0":15,"1":11,"2":11,"3":11,"4":7,"5":9,"6":6,"7":1},"b":{"0":[3,12],"1":[1,1],"2":[11,10],"3":[3,4],"4":[1,8],"5":[8,7],"6":[7,2],"7":[1,6]},"meta":{"lastBranch":8,"lastFunction":8,"lastStatement":35,"seen":{"s:21:27:21:Infinity":0,"f:42:16:42:36":0,"b:43:2:43:Infinity:undefined:undefined:undefined:undefined":0,"s:43:2:43:Infinity":1,"s:43:16:43:Infinity":2,"s:45:22:45:Infinity":3,"s:46:24:46:Infinity":4,"s:48:8:53:Infinity":5,"f:48:8:48:28":1,"s:49:19:49:Infinity":6,"s:50:4:52:Infinity":7,"b:51:6:51:Infinity:undefined:undefined:undefined:undefined":1,"s:51:6:51:Infinity":8,"s:51:36:51:Infinity":9,"s:55:8:56:Infinity":10,"f:55:8:55:20":2,"s:56:4:56:Infinity":11,"b:56:4:56:25:56:25:56:Infinity":2,"s:58:2:98:Infinity":12,"f:62:12:62:19":3,"s:63:8:63:Infinity":13,"s:64:26:64:Infinity":14,"s:65:8:65:Infinity":15,"s:66:8:66:Infinity":16,"f:68:12:68:20":4,"b:69:8:69:Infinity:undefined:undefined:undefined:undefined":3,"s:69:8:69:Infinity":17,"s:69:57:69:Infinity":18,"s:70:8:70:Infinity":19,"f:72:12:72:19":5,"s:73:22:73:Infinity":20,"b:74:8:74:Infinity:undefined:undefined:undefined:undefined":4,"s:74:8:74:Infinity":21,"s:74:33:74:Infinity":22,"s:75:25:75:Infinity":23,"s:76:24:79:Infinity":24,"b:76:24:76:36:76:36:79:Infinity":5,"b:80:8:86:Infinity:undefined:undefined:undefined:undefined":6,"s:80:8:86:Infinity":25,"s:81:10:81:Infinity":26,"s:82:10:85:Infinity":27,"f:82:23:82:Infinity":6,"s:83:20:83:68":28,"f:83:68:83:Infinity":7,"s:84:20:84:68":29,"s:87:24:87:Infinity":30,"b:88:8:93:Infinity:undefined:undefined:undefined:undefined":7,"s:88:8:93:Infinity":31,"s:92:10:92:Infinity":32,"s:94:8:94:Infinity":33,"s:95:8:95:Infinity":34},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/session-workspace.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/session-workspace.ts","statementMap":{"0":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"1":{"start":{"line":31,"column":28},"end":{"line":31,"column":null}},"2":{"start":{"line":34,"column":8},"end":{"line":65,"column":null}},"3":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"4":{"start":{"line":35,"column":40},"end":{"line":35,"column":null}},"5":{"start":{"line":36,"column":4},"end":{"line":63,"column":null}},"6":{"start":{"line":37,"column":6},"end":{"line":62,"column":null}},"7":{"start":{"line":38,"column":8},"end":{"line":38,"column":null}},"8":{"start":{"line":39,"column":30},"end":{"line":39,"column":null}},"9":{"start":{"line":40,"column":8},"end":{"line":43,"column":null}},"10":{"start":{"line":41,"column":10},"end":{"line":41,"column":null}},"11":{"start":{"line":42,"column":10},"end":{"line":42,"column":null}},"12":{"start":{"line":44,"column":25},"end":{"line":47,"column":null}},"13":{"start":{"line":48,"column":8},"end":{"line":54,"column":null}},"14":{"start":{"line":49,"column":10},"end":{"line":52,"column":null}},"15":{"start":{"line":53,"column":10},"end":{"line":53,"column":null}},"16":{"start":{"line":55,"column":8},"end":{"line":55,"column":null}},"17":{"start":{"line":57,"column":8},"end":{"line":60,"column":null}},"18":{"start":{"line":61,"column":8},"end":{"line":61,"column":null}},"19":{"start":{"line":64,"column":4},"end":{"line":64,"column":null}},"20":{"start":{"line":67,"column":2},"end":{"line":80,"column":null}},"21":{"start":{"line":72,"column":8},"end":{"line":72,"column":null}},"22":{"start":{"line":72,"column":55},"end":{"line":72,"column":null}},"23":{"start":{"line":73,"column":28},"end":{"line":73,"column":null}},"24":{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},"25":{"start":{"line":74,"column":39},"end":{"line":74,"column":null}},"26":{"start":{"line":75,"column":24},"end":{"line":75,"column":null}},"27":{"start":{"line":76,"column":8},"end":{"line":76,"column":null}},"28":{"start":{"line":77,"column":8},"end":{"line":77,"column":null}}},"fnMap":{"0":{"name":"withSessionWorkspace","decl":{"start":{"line":26,"column":16},"end":{"line":26,"column":null}},"loc":{"start":{"line":30,"column":12},"end":{"line":81,"column":null}},"line":30},"1":{"name":"(anonymous_1)","decl":{"start":{"line":34,"column":8},"end":{"line":34,"column":66}},"loc":{"start":{"line":34,"column":66},"end":{"line":65,"column":null}},"line":34},"2":{"name":"(anonymous_2)","decl":{"start":{"line":36,"column":24},"end":{"line":36,"column":36}},"loc":{"start":{"line":36,"column":36},"end":{"line":63,"column":7}},"line":36},"3":{"name":"create","decl":{"start":{"line":71,"column":12},"end":{"line":71,"column":19}},"loc":{"start":{"line":71,"column":28},"end":{"line":78,"column":null}},"line":71}},"branchMap":{"0":{"loc":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"type":"if","locations":[{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},{"start":{},"end":{}}],"line":31},"1":{"loc":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"type":"if","locations":[{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},{"start":{},"end":{}}],"line":35},"2":{"loc":{"start":{"line":40,"column":8},"end":{"line":43,"column":null}},"type":"if","locations":[{"start":{"line":40,"column":8},"end":{"line":43,"column":null}},{"start":{},"end":{}}],"line":40},"3":{"loc":{"start":{"line":48,"column":8},"end":{"line":54,"column":null}},"type":"if","locations":[{"start":{"line":48,"column":8},"end":{"line":54,"column":null}},{"start":{},"end":{}}],"line":48},"4":{"loc":{"start":{"line":72,"column":8},"end":{"line":72,"column":null}},"type":"if","locations":[{"start":{"line":72,"column":8},"end":{"line":72,"column":null}},{"start":{},"end":{}}],"line":72},"5":{"loc":{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},"type":"if","locations":[{"start":{"line":74,"column":8},"end":{"line":74,"column":null}},{"start":{},"end":{}}],"line":74}},"s":{"0":11,"1":2,"2":9,"3":9,"4":3,"5":6,"6":6,"7":6,"8":6,"9":6,"10":1,"11":1,"12":5,"13":4,"14":2,"15":2,"16":2,"17":1,"18":1,"19":6,"20":9,"21":10,"22":1,"23":9,"24":9,"25":6,"26":3,"27":3,"28":3},"f":{"0":11,"1":9,"2":6,"3":10},"b":{"0":[2,9],"1":[3,6],"2":[1,5],"3":[2,2],"4":[1,9],"5":[6,3]},"meta":{"lastBranch":6,"lastFunction":4,"lastStatement":29,"seen":{"f:26:16:26:Infinity":0,"b:31:2:31:Infinity:undefined:undefined:undefined:undefined":0,"s:31:2:31:Infinity":0,"s:31:28:31:Infinity":1,"s:34:8:65:Infinity":2,"f:34:8:34:66":1,"b:35:4:35:Infinity:undefined:undefined:undefined:undefined":1,"s:35:4:35:Infinity":3,"s:35:40:35:Infinity":4,"s:36:4:63:Infinity":5,"f:36:24:36:36":2,"s:37:6:62:Infinity":6,"s:38:8:38:Infinity":7,"s:39:30:39:Infinity":8,"b:40:8:43:Infinity:undefined:undefined:undefined:undefined":2,"s:40:8:43:Infinity":9,"s:41:10:41:Infinity":10,"s:42:10:42:Infinity":11,"s:44:25:47:Infinity":12,"b:48:8:54:Infinity:undefined:undefined:undefined:undefined":3,"s:48:8:54:Infinity":13,"s:49:10:52:Infinity":14,"s:53:10:53:Infinity":15,"s:55:8:55:Infinity":16,"s:57:8:60:Infinity":17,"s:61:8:61:Infinity":18,"s:64:4:64:Infinity":19,"s:67:2:80:Infinity":20,"f:71:12:71:19":3,"b:72:8:72:Infinity:undefined:undefined:undefined:undefined":4,"s:72:8:72:Infinity":21,"s:72:55:72:Infinity":22,"s:73:28:73:Infinity":23,"b:74:8:74:Infinity:undefined:undefined:undefined:undefined":5,"s:74:8:74:Infinity":24,"s:74:39:74:Infinity":25,"s:75:24:75:Infinity":26,"s:76:8:76:Infinity":27,"s:77:8:77:Infinity":28},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/token.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/token.ts","statementMap":{"0":{"start":{"line":18,"column":31},"end":{"line":18,"column":null}},"1":{"start":{"line":26,"column":2},"end":{"line":26,"column":null}},"2":{"start":{"line":40,"column":22},"end":{"line":40,"column":null}},"3":{"start":{"line":41,"column":20},"end":{"line":41,"column":null}},"4":{"start":{"line":42,"column":2},"end":{"line":42,"column":null}},"5":{"start":{"line":42,"column":75},"end":{"line":42,"column":null}},"6":{"start":{"line":43,"column":2},"end":{"line":43,"column":null}},"7":{"start":{"line":51,"column":2},"end":{"line":51,"column":null}},"8":{"start":{"line":60,"column":2},"end":{"line":64,"column":null}},"9":{"start":{"line":61,"column":4},"end":{"line":61,"column":null}},"10":{"start":{"line":63,"column":4},"end":{"line":63,"column":null}},"11":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"12":{"start":{"line":74,"column":15},"end":{"line":74,"column":null}},"13":{"start":{"line":75,"column":2},"end":{"line":75,"column":null}},"14":{"start":{"line":76,"column":2},"end":{"line":76,"column":null}},"15":{"start":{"line":77,"column":2},"end":{"line":77,"column":null}},"16":{"start":{"line":92,"column":2},"end":{"line":94,"column":null}},"17":{"start":{"line":93,"column":4},"end":{"line":93,"column":null}},"18":{"start":{"line":95,"column":20},"end":{"line":95,"column":null}},"19":{"start":{"line":96,"column":2},"end":{"line":96,"column":null}},"20":{"start":{"line":96,"column":55},"end":{"line":96,"column":null}},"21":{"start":{"line":97,"column":16},"end":{"line":97,"column":null}},"22":{"start":{"line":98,"column":2},"end":{"line":98,"column":null}},"23":{"start":{"line":99,"column":2},"end":{"line":99,"column":null}}},"fnMap":{"0":{"name":"generateToken","decl":{"start":{"line":25,"column":16},"end":{"line":25,"column":30}},"loc":{"start":{"line":25,"column":58},"end":{"line":27,"column":null}},"line":25},"1":{"name":"verifyToken","decl":{"start":{"line":36,"column":16},"end":{"line":36,"column":28}},"loc":{"start":{"line":36,"column":71},"end":{"line":44,"column":null}},"line":36},"2":{"name":"tokenFilePath","decl":{"start":{"line":50,"column":16},"end":{"line":50,"column":40}},"loc":{"start":{"line":50,"column":40},"end":{"line":52,"column":null}},"line":50},"3":{"name":"readTokenFile","decl":{"start":{"line":59,"column":22},"end":{"line":59,"column":36}},"loc":{"start":{"line":59,"column":97},"end":{"line":65,"column":null}},"line":59},"4":{"name":"writeTokenFile","decl":{"start":{"line":72,"column":22},"end":{"line":72,"column":37}},"loc":{"start":{"line":72,"column":99},"end":{"line":78,"column":null}},"line":72},"5":{"name":"resolveToken","decl":{"start":{"line":88,"column":22},"end":{"line":88,"column":null}},"loc":{"start":{"line":91,"column":64},"end":{"line":100,"column":null}},"line":91}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":30},"end":{"line":25,"column":58}},"type":"default-arg","locations":[{"start":{"line":25,"column":46},"end":{"line":25,"column":58}}],"line":25},"1":{"loc":{"start":{"line":42,"column":2},"end":{"line":42,"column":null}},"type":"if","locations":[{"start":{"line":42,"column":2},"end":{"line":42,"column":null}},{"start":{},"end":{}}],"line":42},"2":{"loc":{"start":{"line":42,"column":6},"end":{"line":42,"column":75}},"type":"binary-expr","locations":[{"start":{"line":42,"column":6},"end":{"line":42,"column":34}},{"start":{"line":42,"column":34},"end":{"line":42,"column":75}}],"line":42},"3":{"loc":{"start":{"line":59,"column":36},"end":{"line":59,"column":97}},"type":"default-arg","locations":[{"start":{"line":59,"column":51},"end":{"line":59,"column":97}}],"line":59},"4":{"loc":{"start":{"line":72,"column":52},"end":{"line":72,"column":99}},"type":"default-arg","locations":[{"start":{"line":72,"column":67},"end":{"line":72,"column":99}}],"line":72},"5":{"loc":{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},"type":"default-arg","locations":[{"start":{"line":90,"column":17},"end":{"line":90,"column":null}}],"line":90},"6":{"loc":{"start":{"line":92,"column":2},"end":{"line":94,"column":null}},"type":"if","locations":[{"start":{"line":92,"column":2},"end":{"line":94,"column":null}},{"start":{},"end":{}}],"line":92},"7":{"loc":{"start":{"line":92,"column":6},"end":{"line":92,"column":57}},"type":"binary-expr","locations":[{"start":{"line":92,"column":6},"end":{"line":92,"column":34}},{"start":{"line":92,"column":34},"end":{"line":92,"column":57}}],"line":92},"8":{"loc":{"start":{"line":96,"column":2},"end":{"line":96,"column":null}},"type":"if","locations":[{"start":{"line":96,"column":2},"end":{"line":96,"column":null}},{"start":{},"end":{}}],"line":96},"9":{"loc":{"start":{"line":96,"column":6},"end":{"line":96,"column":55}},"type":"binary-expr","locations":[{"start":{"line":96,"column":6},"end":{"line":96,"column":33}},{"start":{"line":96,"column":33},"end":{"line":96,"column":55}}],"line":96}},"s":{"0":5,"1":7,"2":34,"3":34,"4":34,"5":8,"6":26,"7":5,"8":3,"9":3,"10":2,"11":3,"12":3,"13":3,"14":3,"15":3,"16":8,"17":5,"18":3,"19":3,"20":1,"21":2,"22":2,"23":2},"f":{"0":7,"1":34,"2":5,"3":3,"4":3,"5":8},"b":{"0":[7],"1":[8,26],"2":[34,33],"3":[3],"4":[3],"5":[8],"6":[5,3],"7":[8,5],"8":[1,2],"9":[3,1]},"meta":{"lastBranch":10,"lastFunction":6,"lastStatement":24,"seen":{"s:18:31:18:Infinity":0,"f:25:16:25:30":0,"b:25:46:25:58":0,"s:26:2:26:Infinity":1,"f:36:16:36:28":1,"s:40:22:40:Infinity":2,"s:41:20:41:Infinity":3,"b:42:2:42:Infinity:undefined:undefined:undefined:undefined":1,"s:42:2:42:Infinity":4,"b:42:6:42:34:42:34:42:75":2,"s:42:75:42:Infinity":5,"s:43:2:43:Infinity":6,"f:50:16:50:40":2,"s:51:2:51:Infinity":7,"f:59:22:59:36":3,"b:59:51:59:97":3,"s:60:2:64:Infinity":8,"s:61:4:61:Infinity":9,"s:63:4:63:Infinity":10,"f:72:22:72:37":4,"b:72:67:72:99":4,"s:73:2:73:Infinity":11,"s:74:15:74:Infinity":12,"s:75:2:75:Infinity":13,"s:76:2:76:Infinity":14,"s:77:2:77:Infinity":15,"f:88:22:88:Infinity":5,"b:90:17:90:Infinity":5,"b:92:2:94:Infinity:undefined:undefined:undefined:undefined":6,"s:92:2:94:Infinity":16,"b:92:6:92:34:92:34:92:57":7,"s:93:4:93:Infinity":17,"s:95:20:95:Infinity":18,"b:96:2:96:Infinity:undefined:undefined:undefined:undefined":8,"s:96:2:96:Infinity":19,"b:96:6:96:33:96:33:96:55":9,"s:96:55:96:Infinity":20,"s:97:16:97:Infinity":21,"s:98:2:98:Infinity":22,"s:99:2:99:Infinity":23},"fnNames":{}}} -,"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/tools.ts": {"path":"/Users/yuxianglin/Desktop/test-Lum1104/dsh-browser/packages/browser/bridge-browser/src/tools.ts","statementMap":{"0":{"start":{"line":35,"column":46},"end":{"line":46,"column":null}},"1":{"start":{"line":43,"column":19},"end":{"line":43,"column":null}},"2":{"start":{"line":44,"column":4},"end":{"line":44,"column":null}},"3":{"start":{"line":51,"column":22},"end":{"line":51,"column":null}},"4":{"start":{"line":54,"column":34},"end":{"line":66,"column":null}},"5":{"start":{"line":84,"column":20},"end":{"line":84,"column":null}},"6":{"start":{"line":85,"column":15},"end":{"line":88,"column":null}},"7":{"start":{"line":86,"column":19},"end":{"line":86,"column":null}},"8":{"start":{"line":87,"column":4},"end":{"line":87,"column":null}},"9":{"start":{"line":90,"column":2},"end":{"line":92,"column":null}},"10":{"start":{"line":91,"column":4},"end":{"line":91,"column":null}},"11":{"start":{"line":93,"column":2},"end":{"line":93,"column":null}},"12":{"start":{"line":98,"column":2},"end":{"line":100,"column":null}},"13":{"start":{"line":99,"column":4},"end":{"line":99,"column":null}},"14":{"start":{"line":101,"column":2},"end":{"line":101,"column":null}},"15":{"start":{"line":110,"column":8},"end":{"line":128,"column":null}},"16":{"start":{"line":110,"column":42},"end":{"line":128,"column":null}},"17":{"start":{"line":122,"column":16},"end":{"line":122,"column":null}},"18":{"start":{"line":123,"column":6},"end":{"line":126,"column":null}},"19":{"start":{"line":130,"column":8},"end":{"line":140,"column":null}},"20":{"start":{"line":130,"column":39},"end":{"line":140,"column":null}},"21":{"start":{"line":139,"column":29},"end":{"line":139,"column":null}},"22":{"start":{"line":142,"column":8},"end":{"line":162,"column":null}},"23":{"start":{"line":142,"column":38},"end":{"line":162,"column":null}},"24":{"start":{"line":155,"column":16},"end":{"line":155,"column":null}},"25":{"start":{"line":156,"column":6},"end":{"line":160,"column":null}},"26":{"start":{"line":164,"column":8},"end":{"line":174,"column":null}},"27":{"start":{"line":164,"column":39},"end":{"line":174,"column":null}},"28":{"start":{"line":173,"column":29},"end":{"line":173,"column":null}},"29":{"start":{"line":176,"column":8},"end":{"line":193,"column":null}},"30":{"start":{"line":176,"column":40},"end":{"line":193,"column":null}},"31":{"start":{"line":187,"column":16},"end":{"line":187,"column":null}},"32":{"start":{"line":188,"column":6},"end":{"line":191,"column":null}},"33":{"start":{"line":195,"column":8},"end":{"line":205,"column":null}},"34":{"start":{"line":195,"column":42},"end":{"line":205,"column":null}},"35":{"start":{"line":204,"column":29},"end":{"line":204,"column":null}},"36":{"start":{"line":207,"column":8},"end":{"line":214,"column":null}},"37":{"start":{"line":207,"column":120},"end":{"line":214,"column":null}},"38":{"start":{"line":213,"column":30},"end":{"line":213,"column":null}},"39":{"start":{"line":216,"column":8},"end":{"line":229,"column":null}},"40":{"start":{"line":216,"column":41},"end":{"line":229,"column":null}},"41":{"start":{"line":226,"column":16},"end":{"line":226,"column":null}},"42":{"start":{"line":227,"column":6},"end":{"line":227,"column":null}},"43":{"start":{"line":231,"column":8},"end":{"line":244,"column":null}},"44":{"start":{"line":231,"column":38},"end":{"line":244,"column":null}},"45":{"start":{"line":241,"column":16},"end":{"line":241,"column":null}},"46":{"start":{"line":242,"column":6},"end":{"line":242,"column":null}},"47":{"start":{"line":246,"column":2},"end":{"line":258,"column":null}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":42,"column":2},"end":{"line":42,"column":11}},"loc":{"start":{"line":42,"column":28},"end":{"line":45,"column":null}},"line":42},"1":{"name":"registerBrowserTools","decl":{"start":{"line":79,"column":16},"end":{"line":79,"column":null}},"loc":{"start":{"line":83,"column":27},"end":{"line":94,"column":null}},"line":83},"2":{"name":"(anonymous_2)","decl":{"start":{"line":85,"column":15},"end":{"line":85,"column":22}},"loc":{"start":{"line":85,"column":122},"end":{"line":88,"column":null}},"line":85},"3":{"name":"normalizeTextResult","decl":{"start":{"line":97,"column":9},"end":{"line":97,"column":29}},"loc":{"start":{"line":97,"column":72},"end":{"line":102,"column":null}},"line":97},"4":{"name":"defineTools","decl":{"start":{"line":109,"column":9},"end":{"line":109,"column":21}},"loc":{"start":{"line":109,"column":81},"end":{"line":259,"column":null}},"line":109},"5":{"name":"(anonymous_5)","decl":{"start":{"line":110,"column":8},"end":{"line":110,"column":42}},"loc":{"start":{"line":110,"column":42},"end":{"line":128,"column":null}},"line":110},"6":{"name":"(anonymous_6)","decl":{"start":{"line":121,"column":4},"end":{"line":121,"column":14}},"loc":{"start":{"line":121,"column":29},"end":{"line":127,"column":null}},"line":121},"7":{"name":"(anonymous_7)","decl":{"start":{"line":130,"column":8},"end":{"line":130,"column":39}},"loc":{"start":{"line":130,"column":39},"end":{"line":140,"column":null}},"line":130},"8":{"name":"(anonymous_8)","decl":{"start":{"line":139,"column":4},"end":{"line":139,"column":14}},"loc":{"start":{"line":139,"column":29},"end":{"line":139,"column":null}},"line":139},"9":{"name":"(anonymous_9)","decl":{"start":{"line":142,"column":8},"end":{"line":142,"column":38}},"loc":{"start":{"line":142,"column":38},"end":{"line":162,"column":null}},"line":142},"10":{"name":"(anonymous_10)","decl":{"start":{"line":154,"column":4},"end":{"line":154,"column":14}},"loc":{"start":{"line":154,"column":29},"end":{"line":161,"column":null}},"line":154},"11":{"name":"(anonymous_11)","decl":{"start":{"line":164,"column":8},"end":{"line":164,"column":39}},"loc":{"start":{"line":164,"column":39},"end":{"line":174,"column":null}},"line":164},"12":{"name":"(anonymous_12)","decl":{"start":{"line":173,"column":4},"end":{"line":173,"column":14}},"loc":{"start":{"line":173,"column":29},"end":{"line":173,"column":null}},"line":173},"13":{"name":"(anonymous_13)","decl":{"start":{"line":176,"column":8},"end":{"line":176,"column":40}},"loc":{"start":{"line":176,"column":40},"end":{"line":193,"column":null}},"line":176},"14":{"name":"(anonymous_14)","decl":{"start":{"line":186,"column":4},"end":{"line":186,"column":14}},"loc":{"start":{"line":186,"column":29},"end":{"line":192,"column":null}},"line":186},"15":{"name":"(anonymous_15)","decl":{"start":{"line":195,"column":8},"end":{"line":195,"column":42}},"loc":{"start":{"line":195,"column":42},"end":{"line":205,"column":null}},"line":195},"16":{"name":"(anonymous_16)","decl":{"start":{"line":204,"column":4},"end":{"line":204,"column":14}},"loc":{"start":{"line":204,"column":29},"end":{"line":204,"column":null}},"line":204},"17":{"name":"(anonymous_17)","decl":{"start":{"line":207,"column":8},"end":{"line":207,"column":18}},"loc":{"start":{"line":207,"column":120},"end":{"line":214,"column":null}},"line":207},"18":{"name":"(anonymous_18)","decl":{"start":{"line":213,"column":4},"end":{"line":213,"column":14}},"loc":{"start":{"line":213,"column":30},"end":{"line":213,"column":null}},"line":213},"19":{"name":"(anonymous_19)","decl":{"start":{"line":216,"column":8},"end":{"line":216,"column":41}},"loc":{"start":{"line":216,"column":41},"end":{"line":229,"column":null}},"line":216},"20":{"name":"(anonymous_20)","decl":{"start":{"line":225,"column":4},"end":{"line":225,"column":14}},"loc":{"start":{"line":225,"column":29},"end":{"line":228,"column":null}},"line":225},"21":{"name":"(anonymous_21)","decl":{"start":{"line":231,"column":8},"end":{"line":231,"column":38}},"loc":{"start":{"line":231,"column":38},"end":{"line":244,"column":null}},"line":231},"22":{"name":"(anonymous_22)","decl":{"start":{"line":240,"column":4},"end":{"line":240,"column":14}},"loc":{"start":{"line":240,"column":29},"end":{"line":243,"column":null}},"line":240}},"branchMap":{"0":{"loc":{"start":{"line":98,"column":2},"end":{"line":100,"column":null}},"type":"if","locations":[{"start":{"line":98,"column":2},"end":{"line":100,"column":null}},{"start":{},"end":{}}],"line":98},"1":{"loc":{"start":{"line":98,"column":6},"end":{"line":98,"column":112}},"type":"binary-expr","locations":[{"start":{"line":98,"column":6},"end":{"line":98,"column":36}},{"start":{"line":98,"column":36},"end":{"line":98,"column":55}},{"start":{"line":98,"column":55},"end":{"line":98,"column":112}}],"line":98},"2":{"loc":{"start":{"line":124,"column":11},"end":{"line":124,"column":null}},"type":"cond-expr","locations":[{"start":{"line":124,"column":35},"end":{"line":124,"column":56}},{"start":{"line":124,"column":56},"end":{"line":124,"column":null}}],"line":124},"3":{"loc":{"start":{"line":125,"column":11},"end":{"line":125,"column":null}},"type":"cond-expr","locations":[{"start":{"line":125,"column":36},"end":{"line":125,"column":59}},{"start":{"line":125,"column":59},"end":{"line":125,"column":null}}],"line":125},"4":{"loc":{"start":{"line":159,"column":11},"end":{"line":159,"column":null}},"type":"cond-expr","locations":[{"start":{"line":159,"column":37},"end":{"line":159,"column":62}},{"start":{"line":159,"column":62},"end":{"line":159,"column":null}}],"line":159},"5":{"loc":{"start":{"line":190,"column":11},"end":{"line":190,"column":null}},"type":"cond-expr","locations":[{"start":{"line":190,"column":36},"end":{"line":190,"column":59}},{"start":{"line":190,"column":59},"end":{"line":190,"column":null}}],"line":190},"6":{"loc":{"start":{"line":227,"column":49},"end":{"line":227,"column":106}},"type":"cond-expr","locations":[{"start":{"line":227,"column":76},"end":{"line":227,"column":103}},{"start":{"line":227,"column":103},"end":{"line":227,"column":106}}],"line":227},"7":{"loc":{"start":{"line":242,"column":45},"end":{"line":242,"column":84}},"type":"cond-expr","locations":[{"start":{"line":242,"column":66},"end":{"line":242,"column":81}},{"start":{"line":242,"column":81},"end":{"line":242,"column":84}}],"line":242}},"s":{"0":4,"1":1,"2":1,"3":4,"4":4,"5":13,"6":13,"7":18,"8":18,"9":13,"10":143,"11":13,"12":18,"13":17,"14":1,"15":13,"16":13,"17":3,"18":3,"19":13,"20":13,"21":1,"22":13,"23":13,"24":2,"25":2,"26":13,"27":13,"28":1,"29":13,"30":13,"31":2,"32":2,"33":13,"34":13,"35":1,"36":13,"37":39,"38":3,"39":13,"40":13,"41":2,"42":2,"43":13,"44":13,"45":3,"46":3,"47":13},"f":{"0":1,"1":13,"2":18,"3":18,"4":13,"5":13,"6":3,"7":13,"8":1,"9":13,"10":2,"11":13,"12":1,"13":13,"14":2,"15":13,"16":1,"17":39,"18":3,"19":13,"20":2,"21":13,"22":3},"b":{"0":[17,1],"1":[18,18,17],"2":[2,1],"3":[1,2],"4":[1,1],"5":[1,1],"6":[1,1],"7":[1,2]},"meta":{"lastBranch":8,"lastFunction":23,"lastStatement":48,"seen":{"s:35:46:46:Infinity":0,"f:42:2:42:11":0,"s:43:19:43:Infinity":1,"s:44:4:44:Infinity":2,"s:51:22:51:Infinity":3,"s:54:34:66:Infinity":4,"f:79:16:79:Infinity":1,"s:84:20:84:Infinity":5,"s:85:15:88:Infinity":6,"f:85:15:85:22":2,"s:86:19:86:Infinity":7,"s:87:4:87:Infinity":8,"s:90:2:92:Infinity":9,"s:91:4:91:Infinity":10,"s:93:2:93:Infinity":11,"f:97:9:97:29":3,"b:98:2:100:Infinity:undefined:undefined:undefined:undefined":0,"s:98:2:100:Infinity":12,"b:98:6:98:36:98:36:98:55:98:55:98:112":1,"s:99:4:99:Infinity":13,"s:101:2:101:Infinity":14,"f:109:9:109:21":4,"s:110:8:128:Infinity":15,"f:110:8:110:42":5,"s:110:42:128:Infinity":16,"f:121:4:121:14":6,"s:122:16:122:Infinity":17,"s:123:6:126:Infinity":18,"b:124:35:124:56:124:56:124:Infinity":2,"b:125:36:125:59:125:59:125:Infinity":3,"s:130:8:140:Infinity":19,"f:130:8:130:39":7,"s:130:39:140:Infinity":20,"f:139:4:139:14":8,"s:139:29:139:Infinity":21,"s:142:8:162:Infinity":22,"f:142:8:142:38":9,"s:142:38:162:Infinity":23,"f:154:4:154:14":10,"s:155:16:155:Infinity":24,"s:156:6:160:Infinity":25,"b:159:37:159:62:159:62:159:Infinity":4,"s:164:8:174:Infinity":26,"f:164:8:164:39":11,"s:164:39:174:Infinity":27,"f:173:4:173:14":12,"s:173:29:173:Infinity":28,"s:176:8:193:Infinity":29,"f:176:8:176:40":13,"s:176:40:193:Infinity":30,"f:186:4:186:14":14,"s:187:16:187:Infinity":31,"s:188:6:191:Infinity":32,"b:190:36:190:59:190:59:190:Infinity":5,"s:195:8:205:Infinity":33,"f:195:8:195:42":15,"s:195:42:205:Infinity":34,"f:204:4:204:14":16,"s:204:29:204:Infinity":35,"s:207:8:214:Infinity":36,"f:207:8:207:18":17,"s:207:120:214:Infinity":37,"f:213:4:213:14":18,"s:213:30:213:Infinity":38,"s:216:8:229:Infinity":39,"f:216:8:216:41":19,"s:216:41:229:Infinity":40,"f:225:4:225:14":20,"s:226:16:226:Infinity":41,"s:227:6:227:Infinity":42,"b:227:76:227:103:227:103:227:106":6,"s:231:8:244:Infinity":43,"f:231:8:231:38":21,"s:231:38:244:Infinity":44,"f:240:4:240:14":22,"s:241:16:241:Infinity":45,"s:242:6:242:Infinity":46,"b:242:66:242:81:242:81:242:84":7,"s:246:2:258:Infinity":47},"fnNames":{}}} -} diff --git a/packages/browser/bridge-browser/coverage/favicon.png b/packages/browser/bridge-browser/coverage/favicon.png deleted file mode 100644 index c1525b811..000000000 Binary files a/packages/browser/bridge-browser/coverage/favicon.png and /dev/null differ diff --git a/packages/browser/bridge-browser/coverage/index.html b/packages/browser/bridge-browser/coverage/index.html deleted file mode 100644 index 5898dbeda..000000000 --- a/packages/browser/bridge-browser/coverage/index.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 100% - Statements - 379/379 -
- - -
- 100% - Branches - 254/254 -
- - -
- 100% - Functions - 92/92 -
- - -
- 100% - Lines - 334/334 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
index.ts -
-
100%44/44100%20/20100%13/13100%37/37
invariant.ts -
-
100%6/6100%0/0100%2/2100%6/6
protocol.ts -
-
100%40/40100%84/84100%5/5100%35/35
server.ts -
-
100%153/153100%89/89100%31/31100%139/139
session-deferral.ts -
-
100%35/35100%16/16100%8/8100%31/31
session-workspace.ts -
-
100%29/29100%12/12100%4/4100%25/25
token.ts -
-
100%24/24100%16/16100%6/6100%22/22
tools.ts -
-
100%48/48100%17/17100%23/23100%39/39
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/index.ts.html b/packages/browser/bridge-browser/coverage/index.ts.html deleted file mode 100644 index 67da0840e..000000000 --- a/packages/browser/bridge-browser/coverage/index.ts.html +++ /dev/null @@ -1,667 +0,0 @@ - - - - - - Code coverage report for index.ts - - - - - - - - - -
-
-

All files index.ts

-
- -
- 100% - Statements - 44/44 -
- - -
- 100% - Branches - 20/20 -
- - -
- 100% - Functions - 13/13 -
- - -
- 100% - Lines - 37/37 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -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  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -  -3x -  -  -3x -  -  -3x -  -  -3x -  -  -3x -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -28x -5x -  -  -  -  -  -  -  -  -  -9x -  -  -  -  -  -  -  -9x -9x -9x -9x -  -  -  -  -  -  -  -  -  -  -7x -  -7x -  -  -5x -  -  -  -1x -  -  -  -5x -  -  -2x -  -  -  -  -  -  -  -  -5x -  -2x -  -5x -  -5x -  -  -  -  -  -5x -  -  -  -1x -1x -  -  -5x -  -5x -5x -  -  -  -  -33x -  -  -  -  -5x -5x -3x -  -  -  -  -  -  -  -5x -  -  -  -  -7x -  - 
/**
- * `@yuxianglin/dsh-bridge-browser`: token-authenticated WebSocket bridge for
- * the browser extension plus the text-only `browser_*` tool set.
- *
- * The bridge mounts its own upgrade route (`/ext/bridge`) on the host
- * webserver, OUTSIDE the /api trust fence — so it brings its own bearer-token
- * authentication (first frame `hello` within HELLO_TIMEOUT_MS). Gateway RPCs
- * from the extension are dispatched through the same fetch-shaped handler the
- * /api carrier uses, and session events are pumped per connection. Tools
- * execute by dispatching `tool.call` frames to the connected extension, which
- * performs the action in the user's active tab.
- *
- * Opt-in by design: nothing is registered unless this plugin appears in the
- * composition. No dsh core code is touched.
- *
- * @module @yuxianglin/dsh-bridge-browser
- */
- 
-import { randomUUID } from 'node:crypto'
-import type { Context } from 'cordis'
-import z from 'schemastery'
-import type {} from '@deepseek-ai/dsh-tools'
-import type {} from '@deepseek-ai/dsh-host-apiproxy'
-import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'
-import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
-import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
-import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy'
-import { dshHomePath } from '@deepseek-ai/dsh-paths'
-import { BridgeServer } from './server.ts'
-import { registerBrowserTools } from './tools.ts'
-import { BRIDGE_CONFIG_PATH, BRIDGE_PATH } from './protocol.ts'
-import { withSessionDeferral } from './session-deferral.ts'
-import { withSessionWorkspace } from './session-workspace.ts'
-import { resolveToken } from './token.ts'
- 
-/** Cordis plugin name used by loader diagnostics. */
-export const name = 'bridge-browser'
- 
-/** Services required by this plugin. */
-export const inject = ['httpServer', 'apiProxy', 'tools']
- 
-/** Default per-tool-call budget (ms). */
-const DEFAULT_TOOL_TIMEOUT_MS = 60_000
- 
-/** Default rendered-snapshot character budget (protects model context). */
-const DEFAULT_SNAPSHOT_MAX_CHARS = 12_000
- 
-/** Default cap on interactive inventory items per snapshot. */
-const DEFAULT_MAX_INTERACTIVE_ITEMS = 60
- 
-/** Default directory backing the browser extension's session group. */
-const DEFAULT_SESSION_WORKSPACE_PATH = dshHomePath('browser-sessions')
- 
-/** Default: sessions materialize only on the first message (open-and-close leaves no trace). */
-const DEFAULT_DEFER_SESSION_CREATE = true
- 
-/** Plugin config: deployment-varying tunables only; the wire contract stays fixed. */
-export interface Config {
-  /** Fixed bearer token. When absent, a token is generated on first boot and persisted under the dsh home (0600). */
-  token?: string
-  /** Per-tool-call timeout in ms. Defaults to 60000. */
-  toolTimeoutMs?: number
-  /** Upper bound on one snapshot's rendered characters. Defaults to 12000. */
-  snapshotMaxChars?: number
-  /** Upper bound on interactive inventory items per snapshot. Defaults to 60. */
-  maxInteractiveItems?: number
-  /** Dedicated workspace path for extension-created sessions. Empty disables grouping. */
-  sessionWorkspacePath?: string
-  /** Defer real session creation until the first prompt. Defaults to true. */
-  deferSessionCreate?: boolean
-}
- 
-export const Config: z<Config> = z.object({
-  token: z.string(),
-  toolTimeoutMs: z.number().step(1).min(1).default(DEFAULT_TOOL_TIMEOUT_MS),
-  snapshotMaxChars: z.number().step(1).min(1).default(DEFAULT_SNAPSHOT_MAX_CHARS),
-  maxInteractiveItems: z.number().step(1).min(1).default(DEFAULT_MAX_INTERACTIVE_ITEMS),
-  sessionWorkspacePath: z.string().default(DEFAULT_SESSION_WORKSPACE_PATH),
-  deferSessionCreate: z.boolean().default(DEFAULT_DEFER_SESSION_CREATE),
-})
- 
-/** The shape after schemastery applies its defaults to every field. */
-type ResolvedConfig = Required<Omit<Config, 'token'>> & Pick<Config, 'token'>
- 
-/** Configured budgets must be positive integers. Exported for validation tests. */
-export function assertPositiveInteger(name: string, value: number): void {
-  if (!Number.isInteger(value) || value < 1) {
-    throw new Error(`bridge-browser: ${name} must be a positive integer`)
-  }
-}
- 
-/**
- * Apply defaults and direct-call validation at the plugin boundary.
- * @param config - Loader-resolved or directly supplied plugin configuration.
- * @returns a complete configuration ready for runtime use.
- */
-export function resolveConfig(config: Config): ResolvedConfig {
-  const resolved: ResolvedConfig = {
-    ...(config.token === undefined ? {} : { token: config.token }),
-    toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS,
-    snapshotMaxChars: config.snapshotMaxChars ?? DEFAULT_SNAPSHOT_MAX_CHARS,
-    maxInteractiveItems: config.maxInteractiveItems ?? DEFAULT_MAX_INTERACTIVE_ITEMS,
-    sessionWorkspacePath: config.sessionWorkspacePath ?? DEFAULT_SESSION_WORKSPACE_PATH,
-    deferSessionCreate: config.deferSessionCreate ?? DEFAULT_DEFER_SESSION_CREATE,
-  }
-  assertPositiveInteger('toolTimeoutMs', resolved.toolTimeoutMs)
-  assertPositiveInteger('snapshotMaxChars', resolved.snapshotMaxChars)
-  assertPositiveInteger('maxInteractiveItems', resolved.maxInteractiveItems)
-  return resolved
-}
- 
-/**
- * Mount the bridge: resolve the token, register the upgrade route, the tool
- * set, and an optional system-prompt section, all effect-scoped for HMR.
- *
- * @param ctx - Cordis context.
- * @param config - plugin config (schema defaults applied).
- */
-export async function apply(ctx: Context, config: Config): Promise<void> {
-  const resolved = resolveConfig(config)
- 
-  const tokenRes = await resolveToken(resolved.token)
-  // Workspace grouping wraps the gateway create; session deferral wraps the
-  // result so materialization at first prompt still flows through grouping.
-  const api: ApiProxy = withSessionDeferral(
-    withSessionWorkspace(
-      ctx.apiProxy,
-      resolved.sessionWorkspacePath,
-      message => { ctx.logger.warn(message) },
-    ),
-    resolved.deferSessionCreate,
-  )
-  const server = new BridgeServer({
-    token: tokenRes.token,
-    apiHandler: toFetchHandler(api),
-    openEvents: (signal) => api.events.mux({ rpcId: RpcId(randomUUID()), payload: {} }, signal),
-    toolTimeoutMs: resolved.toolTimeoutMs,
-    caps: {
-      textOnly: true,
-      snapshotMaxChars: resolved.snapshotMaxChars,
-      maxInteractiveItems: resolved.maxInteractiveItems,
-    },
-  })
- 
-  const route: WebUpgradeRoute = {
-    path: BRIDGE_PATH,
-    handler: (req, socket, head) => { server.handleUpgrade(req, socket, head) },
-  }
-  ctx.effect(() => ctx.httpServer.registerUpgrade(route), 'bridge-browser: /ext/bridge upgrade route')
-  // 异步 disposer:HMR/卸载时先等桥完全关闭(socket/泵/acceptor 静默)再继续。
-  ctx.effect(() => () => server.close(), 'bridge-browser: bridge server')
- 
-  // Zero-config discovery endpoint: the extension fetches this to learn the
-  // bridge WebSocket URL without any manual configuration. The URL carries no
-  // secret (loopback connections skip the token); non-loopback deployments
-  // keep requiring the token on the WS itself.
-  const configRoute: WebRoute = {
-    kind: 'exact',
-    path: BRIDGE_CONFIG_PATH,
-    handler: (_req, res) => {
-      res.writeHead(200, { 'content-type': 'application/json' })
-      res.end(JSON.stringify({ wsUrl: `ws://127.0.0.1:${ctx.httpServer.port}${BRIDGE_PATH}` }))
-    },
-  }
-  ctx.effect(() => ctx.httpServer.register(configRoute), 'bridge-browser: /ext/bridge-config route')
- 
-  ctx.effect(() => {
-    const disposers = registerBrowserTools(ctx, server, {
-      toolTimeoutMs: resolved.toolTimeoutMs,
-      snapshotMaxChars: resolved.snapshotMaxChars,
-      maxInteractiveItems: resolved.maxInteractiveItems,
-    })
-    return () => { for (const dispose of disposers.values()) dispose() }
-  }, 'bridge-browser: browser tools')
- 
-  // Optional system-prompt contribution: a one-line hint only — the model is
-  // told to fetch snapshots on demand instead of hoarding page text.
-  const systemPrompt = ctx.get('systemPrompt')
-  if (systemPrompt !== undefined) {
-    ctx.effect(() => systemPrompt.section({
-      name: 'tool:bridge-browser',
-      order: 107,
-      text: 'A browser bridge may be connected. To read or operate the user\'s active browser page, call browser_snapshot '
-        + '(text-only; numbered items are the click/type targets). Never assume page content you have not snapshot.',
-    }), 'bridge-browser: system prompt section')
-  }
- 
-  ctx.logger.info(
-    tokenRes.generated
-      ? `browser bridge: new token generated and persisted at ${tokenRes.file} (chmod 0600); connect the extension and paste it in its settings`
-      : `browser bridge: using token from ${tokenRes.file}`,
-  )
-  ctx.logger.info(`browser bridge: listening on ${BRIDGE_PATH}`)
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/invariant.ts.html b/packages/browser/bridge-browser/coverage/invariant.ts.html deleted file mode 100644 index 34c7cff8d..000000000 --- a/packages/browser/bridge-browser/coverage/invariant.ts.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - Code coverage report for invariant.ts - - - - - - - - - -
-
-

All files invariant.ts

-
- -
- 100% - Statements - 6/6 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 6/6 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34  -  -  -  -  -  -  -  -  -9x -  -  -9x -  -9x -  -  -  -  -  -  -  -  -9x -  -  -  -  -  -  -9x -9x -  - 
/**
- * Package-owned invariant companion for `@yuxianglin/dsh-bridge-browser`.
- * @module @yuxianglin/dsh-bridge-browser/invariant
- */
- 
-/* jscpd:ignore-start */
-import type { Context } from 'cordis'
-import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
- 
-const PACKAGE_NAME = '@yuxianglin/dsh-bridge-browser'
- 
-/** Cordis companion plugin name. */
-export const name = 'bridge-browser-invariant'
-/** Service required before the companion can reserve package ownership. */
-export const inject = ['invariants']
- 
-/**
- * No runtime invariant: the bridge's connection registry and pending tool map
- * are instance-private (no published event stream to assert against), and the
- * wire contract is pinned by protocol.ts and covered by its unit tests. The
- * tools are plain ctx.tools registrations observed by dsh-tools' own
- * invariant.
- */
-const install: InvariantInstaller = () => {}
- 
-/**
- * Register this package's invariant companion.
- * @param ctx - Cordis context carrying the invariant service.
- * @returns the installed registration's disposer after setup succeeds.
- */
-export const apply = (ctx: Context): Promise<() => void> =>
-  Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
-/* jscpd:ignore-end */
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/prettify.css b/packages/browser/bridge-browser/coverage/prettify.css deleted file mode 100644 index b317a7cda..000000000 --- a/packages/browser/bridge-browser/coverage/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/packages/browser/bridge-browser/coverage/prettify.js b/packages/browser/bridge-browser/coverage/prettify.js deleted file mode 100644 index b3225238f..000000000 --- a/packages/browser/bridge-browser/coverage/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/packages/browser/bridge-browser/coverage/protocol.ts.html b/packages/browser/bridge-browser/coverage/protocol.ts.html deleted file mode 100644 index ecd6212e2..000000000 --- a/packages/browser/bridge-browser/coverage/protocol.ts.html +++ /dev/null @@ -1,661 +0,0 @@ - - - - - - Code coverage report for protocol.ts - - - - - - - - - -
-
-

All files protocol.ts

-
- -
- 100% - Statements - 40/40 -
- - -
- 100% - Branches - 84/84 -
- - -
- 100% - Functions - 5/5 -
- - -
- 100% - Lines - 35/35 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -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  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -5x -  -  -5x -  -  -5x -  -  -5x -  -  -5x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -8x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -6x -  -  -  -  -  -  -  -  -  -93x -93x -  -2x -  -91x -89x -89x -88x -  -37x -  -  -  -  -16x -  -  -  -8x -7x -4x -  -3x -  -  -  -4x -  -3x -  -  -  -6x -5x -2x -  -3x -  -  -  -4x -  -  -  -4x -  -  -  -  -2x -  -3x -  -  -  -1x -  -  -  -  -40x -39x -39x -  -  -  -  -  -3x -  -  -  - 
/**
- * Wire contract between the dsh bridge plugin and the browser extension.
- *
- * Zero-dependency module (pure types, constants, and a parser): both the
- * plugin (node) and the Chrome extension (browser bundle) import this file, so
- * the frame shapes can never drift between the two halves.
- *
- * Frames are one JSON object per WebSocket message, discriminated by `t`.
- * Correlation ids (`id`) are minted by the requestor and echoed by the
- * responder; they are opaque strings, never parsed.
- *
- * @module
- */
- 
-/** WebSocket pathname the bridge plugin registers on the host webserver. */
-export const BRIDGE_PATH = '/ext/bridge'
- 
-/** Zero-config discovery endpoint: returns `{ wsUrl }` for the extension. */
-export const BRIDGE_CONFIG_PATH = '/ext/bridge-config'
- 
-/** Seconds a fresh socket may take to present `hello` before it is closed. */
-export const HELLO_TIMEOUT_MS = 5_000
- 
-/** Server-side ping cadence; the client answers `pong` to prove liveness. */
-export const PING_INTERVAL_MS = 30_000
- 
-/** Default bytes of the generated bearer token (256-bit). */
-export const DEFAULT_TOKEN_BYTES = 32
- 
-/** Error codes a tool call may settle with. Open set: consumers must tolerate unknown codes. */
-export type ToolErrorCode =
-  | 'no-active-tab'
-  | 'content-unavailable'
-  | 'action-failed'
-  | 'timeout'
-  | 'bridge-closed'
-  | 'bad-args'
-  | 'internal'
- 
-/** One tool-call failure: stable machine code plus human text for the model. */
-export interface ToolError {
-  code: ToolErrorCode
-  message: string
-}
- 
-/** Capabilities negotiated in `hello`/`hello.ok`. The extension performs its own actions; these bounds shape page snapshots. */
-export interface BridgeCaps {
-  /** The extension renders page state as text only (no screenshots). */
-  textOnly: true
-  /** Upper bound on one rendered snapshot's characters (plugin config). */
-  snapshotMaxChars: number
-  /** Upper bound on interactive inventory items per snapshot (plugin config). */
-  maxInteractiveItems: number
-}
- 
-/** Frames sent by the extension to the bridge plugin. */
-export type ClientFrame =
-  /** First frame, within HELLO_TIMEOUT_MS of socket open. */
-  | { t: 'hello'; token: string; caps: BridgeCaps }
-  /** Unary gateway RPC passthrough (method names from the apiproxy RpcMethodMap). */
-  | { t: 'rpc'; id: string; method: string; payload: unknown }
-  /** Result of a previously dispatched tool call. */
-  | { t: 'tool.result'; id: string; ok: true; result: unknown }
-  | { t: 'tool.result'; id: string; ok: false; error: ToolError }
-  /** Liveness reply. */
-  | { t: 'pong' }
- 
-/** Frames sent by the bridge plugin to the extension. */
-export type ServerFrame =
-  /** Accepted after a valid `hello`. */
-  | { t: 'hello.ok'; caps: BridgeCaps }
-  /** Reply to an `rpc` frame; `result` is the apiproxy ServerResponse envelope. */
-  | { t: 'rpc.result'; id: string; ok: true; result: unknown }
-  | { t: 'rpc.result'; id: string; ok: false; error: { code: string; message: string } }
-  /** One gateway event envelope (the same server-request shape the GUI's /api/events.mux carries). */
-  | { t: 'event'; frame: { rpcId: string; method: string; payload: unknown } }
-  /** A model-requested browser action to execute in the active tab. */
-  | { t: 'tool.call'; id: string; name: string; args: Record<string, unknown> }
-  /** Liveness probe. */
-  | { t: 'ping' }
-  /** Fatal connection error; the client should re-authenticate. */
-  | { t: 'error'; code: string; message: string }
- 
-/** Any frame on the wire. */
-export type BridgeFrame = ClientFrame | ServerFrame
- 
-/**
- * Type guard: is this frame one the SERVER may send? Client-only shapes
- * (hello/tool.result/pong) narrow out, so server-side consumers never
- * dispatch on their own request vocabulary.
- * @param frame - parsed frame.
- * @returns true for server-sendable frames.
- */
-export function isServerFrame(frame: BridgeFrame): frame is ServerFrame {
-  return frame.t === 'hello.ok'
-    || frame.t === 'rpc.result'
-    || frame.t === 'event'
-    || frame.t === 'tool.call'
-    || frame.t === 'ping'
-    || frame.t === 'error'
-}
- 
-/**
- * Type guard: is this frame one the CLIENT may send? Server-only shapes
- * narrow out, so client-side consumers never dispatch on server vocabulary.
- * @param frame - parsed frame.
- * @returns true for client-sendable frames.
- */
-export function isClientFrame(frame: BridgeFrame): frame is ClientFrame {
-  return frame.t === 'hello' || frame.t === 'rpc' || frame.t === 'tool.result' || frame.t === 'pong'
-}
- 
-/**
- * Parse one WebSocket message into a frame.
- * @param text - raw message text.
- * @returns the frame, or `undefined` when the message is not a valid frame.
- */
-export function parseBridgeFrame(text: string): BridgeFrame | undefined {
-  let value: unknown
-  try {
-    value = JSON.parse(text)
-  } catch {
-    return undefined
-  }
-  if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
-  const frame = value as Record<string, unknown>
-  if (typeof frame.t !== 'string') return undefined
-  switch (frame.t) {
-    case 'hello':
-      return typeof frame.token === 'string'
-        && isCaps(frame.caps)
-        ? { t: 'hello', token: frame.token, caps: frame.caps }
-        : undefined
-    case 'rpc':
-      return typeof frame.id === 'string' && typeof frame.method === 'string'
-        ? { t: 'rpc', id: frame.id, method: frame.method, payload: frame.payload }
-        : undefined
-    case 'tool.result':
-      if (typeof frame.id !== 'string') return undefined
-      if (frame.ok === true && 'result' in frame) {
-        return { t: 'tool.result', id: frame.id, ok: true, result: frame.result }
-      }
-      return isToolError(frame.error)
-        ? { t: 'tool.result', id: frame.id, ok: false, error: frame.error }
-        : undefined
-    case 'pong':
-      return { t: 'pong' }
-    case 'hello.ok':
-      return isCaps(frame.caps)
-        ? { t: 'hello.ok', caps: frame.caps }
-        : undefined
-    case 'rpc.result':
-      if (typeof frame.id !== 'string') return undefined
-      if (frame.ok === true && 'result' in frame) {
-        return { t: 'rpc.result', id: frame.id, ok: true, result: frame.result }
-      }
-      return typeof frame.error === 'object' && frame.error !== null
-        ? { t: 'rpc.result', id: frame.id, ok: false, error: frame.error as { code: string; message: string } }
-        : undefined
-    case 'event':
-      return typeof frame.frame === 'object' && frame.frame !== null
-        ? { t: 'event', frame: frame.frame as ServerFrame extends { t: 'event' } ? ServerFrame['frame'] : never }
-        : undefined
-    case 'tool.call':
-      return typeof frame.id === 'string' && typeof frame.name === 'string'
-        && typeof frame.args === 'object' && frame.args !== null && !Array.isArray(frame.args)
-        ? { t: 'tool.call', id: frame.id, name: frame.name, args: frame.args as Record<string, unknown> }
-        : undefined
-    case 'ping':
-      return { t: 'ping' }
-    case 'error':
-      return typeof frame.code === 'string' && typeof frame.message === 'string'
-        ? { t: 'error', code: frame.code, message: frame.message }
-        : undefined
-    default:
-      return undefined
-  }
-}
- 
-function isCaps(value: unknown): value is BridgeCaps {
-  if (typeof value !== 'object' || value === null) return false
-  const caps = value as Record<string, unknown>
-  return caps.textOnly === true
-    && typeof caps.snapshotMaxChars === 'number' && caps.snapshotMaxChars > 0
-    && typeof caps.maxInteractiveItems === 'number' && caps.maxInteractiveItems > 0
-}
- 
-function isToolError(value: unknown): value is ToolError {
-  return typeof value === 'object' && value !== null
-    && typeof (value as Record<string, unknown>).code === 'string'
-    && typeof (value as Record<string, unknown>).message === 'string'
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/server.ts.html b/packages/browser/bridge-browser/coverage/server.ts.html deleted file mode 100644 index 718b5aa23..000000000 --- a/packages/browser/bridge-browser/coverage/server.ts.html +++ /dev/null @@ -1,1390 +0,0 @@ - - - - - - Code coverage report for server.ts - - - - - - - - - -
-
-

All files server.ts

-
- -
- 100% - Statements - 153/153 -
- - -
- 100% - Branches - 89/89 -
- - -
- 100% - Functions - 31/31 -
- - -
- 100% - Lines - 139/139 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -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 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -4x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -35x -  -  -  -  -  -9x -  -  -9x -9x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -99x -  -  -  -  -  -  -  -  -  -  -52x -51x -1x -  -  -  -  -  -  -  -35x -35x -35x -35x -  -35x -  -  -  -  -  -  -  -  -31x -31x -31x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -10x -10x -2x -  -  -  -  -8x -1x -  -7x -7x -  -7x -2x -2x -2x -2x -  -7x -1x -  -7x -1x -  -7x -7x -7x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -35x -33x -  -33x -35x -35x -33x -33x -33x -  -  -  -  -  -  -  -33x -  -  -  -  -4x -  -  -  -31x -1x -  -  -31x -49x -49x -49x -1x -1x -  -48x -  -30x -1x -1x -  -  -  -  -  -  -  -  -  -29x -  -  -30x -3x -3x -  -26x -26x -26x -26x -  -18x -  -31x -31x -31x -  -31x -31x -31x -  -  -  -  -26x -26x -26x -26x -26x -26x -59x -58x -  -  -  -  -  -2x -1x -  -  -  -26x -26x -26x -26x -26x -  -  -  -  -18x -  -13x -13x -  -3x -3x -  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -13x -  -  -  -13x -13x -1x -1x -  -12x -12x -  -  -  -  -12x -12x -11x -11x -1x -1x -  -  -10x -10x -  -1x -  -10x -  -1x -  -  -  -  -3x -3x -2x -2x -2x -1x -  -  -  -  -65x -65x -26x -26x -26x -26x -19x -  -26x -3x -3x -3x -  -  -  -  -  -  -  -  -  -  -  -  -5x -3x -3x -1x -  -2x -  -  -  -  -  -  -  -  -  -  -5x -4x -4x -2x -  -1x -  - 
/**
- * Bridge WebSocket carrier: token-authenticated connection registry, gateway
- * RPC passthrough, per-connection event pump, and tool-call dispatch to the
- * connected browser extension.
- *
- * The route this server mounts (`/ext/bridge`) lives OUTSIDE the /api trust
- * fence (which only guards the client-connection routes), so the bridge brings
- * its own authentication: a bearer token presented in the `hello` frame within
- * HELLO_TIMEOUT_MS. Gateway RPCs are dispatched through the same fetch-shaped
- * handler the /api carrier uses (`toFetchHandler`), so schema validation and
- * error envelopes are identical to the GUI path. Methods the /api carrier
- * pins to loopback (`PRIVILEGED_METHODS`) stay loopback-only here regardless
- * of the token, defense in depth for `--host 0.0.0.0` deployments.
- *
- * One active connection at a time: a new authenticated socket replaces the
- * previous one (the old socket is closed and its in-flight tool calls settle
- * as `bridge-closed`).
- *
- * @module
- */
- 
-import { randomUUID } from 'node:crypto'
-import type { IncomingMessage } from 'node:http'
-import type { Duplex } from 'node:stream'
-import { WebSocket, WebSocketServer } from 'ws'
-import type { MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
-import {
-  HELLO_TIMEOUT_MS,
-  PING_INTERVAL_MS,
-  parseBridgeFrame,
-  type BridgeFrame,
-  type BridgeCaps,
-  type ClientFrame,
-  type ToolErrorCode,
-} from './protocol.ts'
-import { verifyToken } from './token.ts'
- 
-/**
- * Gateway methods the /api carrier pins to loopback (mirror of
- * client-connection's PRIVILEGED_METHODS; kept verbatim so the two fences
- * cannot drift). The bridge rejects these for non-loopback remotes even with
- * a valid token.
- */
-const PRIVILEGED_METHODS = new Set([
-  'host.pickDirectory',
-  'host.openPath',
-  'settings.describe',
-  'settings.openDocument',
-  'settings.update',
-  'settings.replace',
-  'settings.mutate',
-  'credentials.describe',
-  'credentials.set',
-  'credentials.unset',
-])
- 
-/** Loopback IPv4/IPv6 literals (IPv4-mapped included). Exported for tests and reuse. */
-export function isLoopbackAddress(address: string | undefined): boolean {
-  return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
-}
- 
-/** Error thrown by requestTool; the tool registry turns it into an isError result. */
-export class BridgeToolError extends Error {
-  constructor(
-    readonly code: ToolErrorCode,
-    message: string,
-  ) {
-    super(message)
-    this.name = 'BridgeToolError'
-  }
-}
- 
-/** Dependencies the bridge needs from the host. */
-export interface BridgeServerDeps {
-  /** Bearer token the extension must present in `hello`. */
-  token: string
-  /** Fetch-shaped gateway carrier (from `toFetchHandler(ctx.apiProxy)`). */
-  apiHandler: { fetch: (request: Request) => Promise<Response> }
-  /** Per-connection event stream (usually `ctx.apiProxy.events.mux`). */
-  openEvents: (signal: AbortSignal) => AsyncIterable<RpcRequest<MuxFrame>>
-  /** Default per-tool-call timeout in ms. */
-  toolTimeoutMs: number
-  /** Capabilities to echo in `hello.ok` (negotiated snapshot budgets). */
-  caps: BridgeCaps
-  /**
-   * Test seam: force the remote address seen by the privilege gate. The
-   * sandbox cannot bind arbitrary loopback literals, so the non-loopback
-   * branch is exercised through this override; production never sets it.
-   */
-  remoteAddressOverride?: string
-  /** Seconds a fresh socket may present `hello`; defaults to HELLO_TIMEOUT_MS. */
-  helloTimeoutMs?: number
-  /** Server ping cadence; defaults to PING_INTERVAL_MS. */
-  pingIntervalMs?: number
-}
- 
-/** One in-flight tool call awaiting the extension's `tool.result`. */
-interface PendingTool {
-  resolve: (result: unknown) => void
-  reject: (error: BridgeToolError) => void
-  timer: NodeJS.Timeout
-}
- 
-/** A socket that passed authentication and owns the single active slot. */
-interface ReadyConnection {
-  ws: WebSocket
-  /** Remote address captured at upgrade time (loopback gate for privileged methods). */
-  remoteAddress: string | undefined
-  abort: AbortController
-  pump: Promise<void>
-  ping: NodeJS.Timeout
-}
- 
-function sendFrame(ws: WebSocket, frame: BridgeFrame): void {
-  /* v8 ignore next -- teardown race: the socket can die between a pump's
-  readiness check and this write; the guard refuses writes on dead sockets */
-  if (ws.readyState !== WebSocket.OPEN) return
-  ws.send(JSON.stringify(frame))
-}
- 
-/**
- * Decode one ws message payload to text. Exported so all three delivery
- * shapes (fragmented buffer list, Buffer, ArrayBuffer) are unit-testable
- * directly — node ws only ever delivers Buffers in practice.
- * @param data - ws message payload.
- * @returns the decoded UTF-8 text.
- */
-export function messageToText(data: Buffer | ArrayBuffer | Buffer[]): string {
-  if (Array.isArray(data)) return Buffer.concat(data).toString('utf8')
-  if (Buffer.isBuffer(data)) return data.toString('utf8')
-  return Buffer.from(data).toString('utf8')
-}
- 
-/**
- * Token-authenticated bridge server. Construct once per plugin instance;
- * dispose with {@link close}.
- */
-export class BridgeServer {
-  private readonly wss = new WebSocketServer({ noServer: true })
-  private current: ReadyConnection | null = null
-  private readonly pendingTools = new Map<string, PendingTool>()
-  private closed = false
- 
-  constructor(private readonly deps: BridgeServerDeps) {}
- 
-  /**
-   * Handle one HTTP upgrade for the bridge path.
-   * @param req - upgrade request (carries the client's remote address).
-   * @param socket - raw socket transferred by the HTTP server.
-   * @param head - bytes already read after the upgrade headers.
-   */
-  handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void {
-    const remote = this.deps.remoteAddressOverride ?? req.socket.remoteAddress
-    const origin = req.headers.origin
-    this.wss.handleUpgrade(req, socket, head, (ws) => { this.attach(ws, remote, origin) })
-  }
- 
-  /**
-   * Request one browser action from the connected extension.
-   * @param name - tool name (also the wire action name).
-   * @param args - validated tool arguments.
-   * @param signal - caller cancellation (abort settles the call as cancelled).
-   * @param timeoutMs - per-call budget; defaults to the plugin config value.
-   * @returns the extension's action result.
-   * @throws BridgeToolError when no extension is connected, the call times
-   *   out, is cancelled, or the extension reports a failure.
-   */
-  requestTool(
-    name: string,
-    args: Record<string, unknown>,
-    signal: AbortSignal,
-    timeoutMs: number = this.deps.toolTimeoutMs,
-  ): Promise<unknown> {
-    const conn = this.current
-    if (conn === null) {
-      throw new BridgeToolError('bridge-closed', 'no browser extension is connected to the bridge')
-    }
-    // A caller that already aborted must not dispatch: the abort listener
-    // below does not replay for pre-aborted signals, so the call would be
-    // sent to the extension and executed despite the cancellation.
-    if (signal.aborted) {
-      throw new BridgeToolError('bridge-closed', 'tool call cancelled before dispatch')
-    }
-    const id = randomUUID()
-    return new Promise<unknown>((resolve, reject) => {
-      let timer: NodeJS.Timeout
-      const settle = (error: BridgeToolError): void => {
-        clearTimeout(timer)
-        this.pendingTools.delete(id)
-        signal.removeEventListener('abort', onAbort)
-        reject(error)
-      }
-      const onAbort = (): void => {
-        settle(new BridgeToolError('bridge-closed', 'tool call cancelled before the extension answered'))
-      }
-      timer = setTimeout(() => {
-        settle(new BridgeToolError('timeout', `browser action "${name}" timed out after ${timeoutMs}ms`))
-      }, timeoutMs)
-      signal.addEventListener('abort', onAbort, { once: true })
-      this.pendingTools.set(id, { resolve, reject, timer })
-      conn.ws.send(JSON.stringify({ t: 'tool.call', id, name, args } satisfies BridgeFrame), (error) => {
-        /* v8 ignore next -- teardown race: when the write fails, the socket's
-        close handler settles the same call with the same code; the callback
-        path is a defensive second settle, covered via the close path */
-        if (error != null) {
-          settle(new BridgeToolError('bridge-closed', `bridge socket failed before delivery: ${error.message}`))
-        }
-      })
-    })
-  }
- 
-  /**
-   * Terminate the server: close the acceptor, drop all sockets, reject all
-   * in-flight tool calls.
-   * @returns a promise resolving after the acceptor and all pumps stop.
-   */
-  async close(): Promise<void> {
-    // Idempotent: a second close must not touch the acceptor (ws throws
-    // "The server is not running" when closing an already-closed server).
-    if (this.closed) return
-    this.closed = true
-    // Capture the live pump BEFORE replaceConnection nulls the connection.
-    const pumps = this.current === null ? [] : [this.current.pump]
-    this.replaceConnection()
-    for (const socket of this.wss.clients) socket.terminate()
-    this.current = null
-    await new Promise<void>((resolve, reject) => {
-      this.wss.close((error) => {
-        /* v8 ignore next -- acceptor close cannot fail: close() is idempotent
-        and the noServer acceptor only reports teardown of already-terminated clients */
-        if (error === undefined) resolve()
-        /* v8 ignore next -- same unreachable arm */
-        else reject(error)
-      })
-    })
-    await Promise.all(pumps)
-  }
- 
-  /** @returns whether an authenticated extension is currently connected. */
-  hasConnection(): boolean {
-    return this.current !== null
-  }
- 
-  private attach(ws: WebSocket, remoteAddress: string | undefined, origin: string | undefined): void {
-    let helloTimer: NodeJS.Timeout | undefined = setTimeout(() => {
-      ws.close(4001, 'hello timeout')
-    }, this.deps.helloTimeoutMs ?? HELLO_TIMEOUT_MS)
- 
-    const onMessage = (data: Buffer | ArrayBuffer | Buffer[]): void => {
-      const text = messageToText(data)
-      const frame = parseBridgeFrame(text)
-      if (frame === undefined) {
-        ws.close(1008, 'unparseable frame')
-        return
-      }
-      if (helloTimer !== undefined) {
-        // Pending state: only `hello` is legal.
-        if (frame.t !== 'hello') {
-          ws.close(1008, 'hello first')
-          return
-        }
-        // Zero-config local mode: loopback sockets skip the token (the
-        // extension auto-discovers the bridge and connects without setup).
-        // WebSockets have no same-origin policy, so a malicious page could
-        // open a cross-origin socket to 127.0.0.1 with a loopback remote —
-        // the loopback shortcut therefore requires a chrome-extension://
-        // Origin (only extension contexts can present one; pages cannot
-        // forge the header). Non-loopback remotes must still present the
-        // bearer token.
-        const loopbackNoToken = isLoopbackAddress(remoteAddress)
-          && typeof origin === 'string'
-          && origin.startsWith('chrome-extension://')
-        if (!loopbackNoToken && !verifyToken(this.deps.token, frame.token)) {
-          ws.close(4002, 'bad token')
-          return
-        }
-        clearTimeout(helloTimer)
-        helloTimer = undefined
-        this.promote(ws, remoteAddress)
-        return
-      }
-      this.handleReadyFrame(frame)
-    }
-    const onClose = (): void => {
-      if (helloTimer !== undefined) clearTimeout(helloTimer)
-      if (this.current !== null && this.current.ws === ws) this.replaceConnection()
-    }
-    ws.on('message', onMessage)
-    ws.once('close', onClose)
-    ws.once('error', onClose)
-  }
- 
-  /** Promote an authenticated socket to the single active slot. */
-  private promote(ws: WebSocket, remoteAddress: string | undefined): void {
-    this.replaceConnection()
-    const abort = new AbortController()
-    const ping = setInterval(() => { sendFrame(ws, { t: 'ping' }) }, this.deps.pingIntervalMs ?? PING_INTERVAL_MS)
-    const pump = (async () => {
-      try {
-        for await (const envelope of this.deps.openEvents(abort.signal)) {
-          if (ws.readyState !== WebSocket.OPEN) break
-          sendFrame(ws, {
-            t: 'event',
-            frame: { rpcId: envelope.rpcId, method: envelope.payload.type, payload: envelope.payload },
-          })
-        }
-      } catch (error: unknown) {
-        if (!abort.signal.aborted && ws.readyState === WebSocket.OPEN) {
-          sendFrame(ws, { t: 'error', code: 'stream-failed', message: String(error) })
-        }
-      }
-    })()
-    this.current = { ws, remoteAddress, abort, pump, ping }
-    sendFrame(ws, { t: 'hello.ok', caps: this.deps.caps })
-    ws.once('close', () => {
-      clearInterval(ping)
-      abort.abort()
-    })
-  }
- 
-  private handleReadyFrame(frame: BridgeFrame): void {
-    switch (frame.t) {
-      case 'rpc':
-        void this.handleRpc(frame)
-        break
-      case 'tool.result':
-        this.settleTool(frame.id, frame.ok, frame.ok ? frame.result : frame.error)
-        break
-      case 'pong':
-      case 'hello':
-      case 'hello.ok':
-      case 'rpc.result':
-      case 'event':
-      case 'tool.call':
-      case 'ping':
-      case 'error':
-        // Protocol violations and unsolicited server-side shapes are ignored;
-        // the extension is the only sender on this channel.
-        break
-    }
-  }
- 
-  private async handleRpc(frame: Extract<ClientFrame, { t: 'rpc' }>): Promise<void> {
-    const conn = this.current
-    /* v8 ignore next -- replacement race: a frame can land between a socket
-    replacement and the next promotion; the re-check keeps the handler total */
-    if (conn === null) return
-    const forbidden = PRIVILEGED_METHODS.has(frame.method) && !isLoopbackAddress(conn.remoteAddress)
-    if (forbidden) {
-      sendFrame(conn.ws, { t: 'rpc.result', id: frame.id, ok: false, error: { code: 'forbidden', message: 'method is loopback-only' } })
-      return
-    }
-    const body = JSON.stringify({ type: 'client-request', rpcId: frame.id, method: frame.method, payload: frame.payload })
-    const request = new Request(new URL(`/api/${frame.method}`, 'http://dsh.internal'), {
-      method: 'POST',
-      headers: { 'content-type': 'application/json' },
-      body,
-    })
-    try {
-      const response = await this.deps.apiHandler.fetch(request)
-      const text = await response.text()
-      if (!response.ok) {
-        sendFrame(conn.ws, { t: 'rpc.result', id: frame.id, ok: false, error: { code: 'http', message: text } })
-        return
-      }
-      let result: unknown
-      try {
-        result = JSON.parse(text)
-      } catch {
-        result = text
-      }
-      sendFrame(conn.ws, { t: 'rpc.result', id: frame.id, ok: true, result })
-    } catch (error: unknown) {
-      sendFrame(conn.ws, { t: 'rpc.result', id: frame.id, ok: false, error: { code: 'internal', message: String(error) } })
-    }
-  }
- 
-  private settleTool(id: string, ok: boolean, payload: unknown): void {
-    const pending = this.pendingTools.get(id)
-    if (pending === undefined) return
-    clearTimeout(pending.timer)
-    this.pendingTools.delete(id)
-    if (ok) pending.resolve(payload)
-    else pending.reject(new BridgeToolError(payloadCode(payload), payloadMessage(payload)))
-  }
- 
-  /** Close the current connection (if any) and settle its in-flight calls. */
-  private replaceConnection(): void {
-    const conn = this.current
-    if (conn === null) return
-    this.current = null
-    clearInterval(conn.ping)
-    conn.abort.abort()
-    if (conn.ws.readyState === WebSocket.OPEN || conn.ws.readyState === WebSocket.CONNECTING) {
-      conn.ws.close(4000, 'replaced')
-    }
-    for (const [id, pending] of this.pendingTools) {
-      clearTimeout(pending.timer)
-      this.pendingTools.delete(id)
-      pending.reject(new BridgeToolError('bridge-closed', 'the extension connection was replaced'))
-    }
-  }
-}
- 
-/**
- * Tool error payload → stable code. The wire parser enforces string fields,
- * so the fallback branches are parser-gated; exported so the fallback
- * contract is unit-testable directly.
- * @param payload - extension-reported error payload.
- * @returns the stable error code.
- */
-export function payloadCode(payload: unknown): ToolErrorCode {
-  if (typeof payload === 'object' && payload !== null) {
-    const code = (payload as { code?: unknown }).code
-    if (typeof code === 'string') return code as ToolErrorCode
-    return 'internal'
-  }
-  return 'internal'
-}
- 
-/**
- * Tool error payload → message. The wire parser enforces string fields, so
- * the fallback branches are parser-gated; exported so the fallback contract
- * is unit-testable directly.
- * @param payload - extension-reported error payload.
- * @returns the human-readable message.
- */
-export function payloadMessage(payload: unknown): string {
-  if (typeof payload === 'object' && payload !== null) {
-    const message = (payload as { message?: unknown }).message
-    if (typeof message === 'string' && message.length > 0) return message
-    return 'browser action failed'
-  }
-  return 'browser action failed'
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/session-deferral.ts.html b/packages/browser/bridge-browser/coverage/session-deferral.ts.html deleted file mode 100644 index 79ebf65b8..000000000 --- a/packages/browser/bridge-browser/coverage/session-deferral.ts.html +++ /dev/null @@ -1,382 +0,0 @@ - - - - - - Code coverage report for session-deferral.ts - - - - - - - - - -
-
-

All files session-deferral.ts

-
- -
- 100% - Statements - 35/35 -
- - -
- 100% - Branches - 16/16 -
- - -
- 100% - Functions - 8/8 -
- - -
- 100% - Lines - 31/31 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -4x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -15x -  -12x -12x -  -12x -11x -11x -2x -  -  -  -12x -11x -  -12x -  -  -  -  -11x -11x -11x -11x -  -  -7x -4x -  -  -9x -9x -8x -8x -  -  -  -9x -7x -7x -6x -1x -  -  -8x -7x -  -  -  -1x -  -6x -6x -  -  -  -  - 
/**
- * Defer real session creation until the first prompt.
- *
- * The panel calls `session.create` as soon as it connects, but a session that
- * is opened and never used should leave zero trace in the store/GUI. This
- * wrapper answers `session.create` with a provisional id (minted locally,
- * nothing persisted), serves `session.history` for provisional ids as empty,
- * and materializes the real session — same id, original create payload — on
- * the first `session.prompt` for that id. Abandoned provisional ids are
- * pruned after {@link PROVISIONAL_TTL_MS}.
- *
- * @module @yuxianglin/dsh-bridge-browser/src/session-deferral
- */
- 
-import { randomUUID } from 'node:crypto'
-import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api'
-import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
-import type { SessionId } from '@deepseek-ai/dsh-session/types'
- 
-/** Provisional entries older than this are dropped on the next create. */
-const PROVISIONAL_TTL_MS = 30 * 60_000
- 
-type CreateRequest = Parameters<ApiProxy['sessions']['create']>[0]
-type HistoryRequest = Parameters<ApiProxy['sessions']['history']>[0]
-type PromptRequest = Parameters<ApiProxy['sessions']['prompt']>[0]
- 
-interface ProvisionalEntry {
-  /** The original create payload, replayed at materialization (keeps cwd/workspaceId). */
-  payload: CreateRequest['payload']
-  createdAt: number
-}
- 
-/**
- * Wrap the gateway sessions API so `session.create` returns a provisional id
- * without creating anything; the real session materializes on the first
- * `session.prompt` for that id.
- *
- * @param api - Gateway API implementation.
- * @param enabled - Whether deferral is active; false returns the API untouched.
- * @returns the original API when disabled, otherwise the wrapped API.
- */
-export function withSessionDeferral(api: ApiProxy, enabled: boolean): ApiProxy {
-  if (!enabled) return api
- 
-  const provisional = new Map<SessionId, ProvisionalEntry>()
-  const materializing = new Map<SessionId, ReturnType<ApiProxy['sessions']['create']>>()
- 
-  const prune = (): void => {
-    const cutoff = Date.now() - PROVISIONAL_TTL_MS
-    for (const [id, entry] of provisional) {
-      if (entry.createdAt < cutoff) provisional.delete(id)
-    }
-  }
- 
-  const mintedId = (payload: CreateRequest['payload']): SessionId =>
-    payload.sessionId ?? `session-${randomUUID()}` as SessionId
- 
-  return {
-    ...api,
-    sessions: {
-      ...api.sessions,
-      async create(request: CreateRequest) {
-        prune()
-        const sessionId = mintedId(request.payload)
-        provisional.set(sessionId, { payload: { ...request.payload }, createdAt: Date.now() })
-        return { rpcId: request.rpcId, result: { ok: true, value: { sessionId } } }
-      },
-      async history(request: HistoryRequest) {
-        if (!provisional.has(request.payload.sessionId)) return api.sessions.history(request)
-        return { rpcId: request.rpcId, result: { ok: true, value: { events: [], hasMore: false } } }
-      },
-      async prompt(request: PromptRequest) {
-        const entry = provisional.get(request.payload.sessionId)
-        if (entry === undefined) return api.sessions.prompt(request)
-        const existing = materializing.get(request.payload.sessionId)
-        const pending = existing ?? api.sessions.create({
-          rpcId: RpcId(randomUUID()),
-          payload: { ...entry.payload, sessionId: request.payload.sessionId },
-        })
-        if (existing === undefined) {
-          materializing.set(request.payload.sessionId, pending)
-          void pending.then(
-            () => { materializing.delete(request.payload.sessionId) },
-            () => { materializing.delete(request.payload.sessionId) },
-          )
-        }
-        const created = await pending
-        if (!created.result.ok) {
-          // The create failure value shape differs from prompt's success
-          // shape; the carrier relays only result.ok/error, so the value
-          // side is irrelevant here.
-          return created as unknown as Awaited<ReturnType<ApiProxy['sessions']['prompt']>>
-        }
-        provisional.delete(request.payload.sessionId)
-        return api.sessions.prompt(request)
-      },
-    },
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/session-workspace.ts.html b/packages/browser/bridge-browser/coverage/session-workspace.ts.html deleted file mode 100644 index aeb61b691..000000000 --- a/packages/browser/bridge-browser/coverage/session-workspace.ts.html +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - Code coverage report for session-workspace.ts - - - - - - - - - -
-
-

All files session-workspace.ts

-
- -
- 100% - Statements - 29/29 -
- - -
- 100% - Branches - 12/12 -
- - -
- 100% - Functions - 4/4 -
- - -
- 100% - Lines - 25/25 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -11x -  -  -9x -9x -6x -6x -6x -6x -6x -1x -1x -  -5x -  -  -  -4x -2x -  -  -  -2x -  -2x -  -1x -  -  -  -1x -  -  -6x -  -  -9x -  -  -  -  -10x -9x -9x -3x -3x -3x -  -  -  -  - 
/**
- * Best-effort workspace grouping for sessions created through the browser
- * bridge. The wrapper changes only implicit `session.create` requests;
- * explicit workspace choices and every other gateway method pass through.
- * @module @yuxianglin/dsh-bridge-browser/src/session-workspace
- */
- 
-import { randomUUID } from 'node:crypto'
-import { mkdir } from 'node:fs/promises'
-import type { ApiProxy, WorkspaceId } from '@deepseek-ai/dsh-host-apiproxy/api'
-import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
- 
-type Warn = (message: string) => void
- 
-/**
- * Add a dedicated Workspace to implicit session creation without making
- * grouping a session-creation dependency. The first implicit create mkdirs
- * and registers the configured path; that result, including failure, is
- * cached for the wrapper lifetime.
- *
- * @param api - Injected gateway API implementation.
- * @param workspacePath - Dedicated directory, or an empty string to opt out.
- * @param warn - Logger called once when grouping cannot be established.
- * @returns the original API for opt-out, otherwise an API with wrapped session creation.
- */
-export function withSessionWorkspace(
-  api: ApiProxy,
-  workspacePath: string,
-  warn: Warn,
-): ApiProxy {
-  if (workspacePath === '') return api
- 
-  let workspacePromise: Promise<WorkspaceId | undefined> | undefined
-  const ensureWorkspace = (): Promise<WorkspaceId | undefined> => {
-    if (workspacePromise !== undefined) return workspacePromise
-    workspacePromise = (async () => {
-      try {
-        await mkdir(workspacePath, { recursive: true })
-        const workspaceApi = (api as { workspace?: ApiProxy['workspace'] }).workspace
-        if (workspaceApi === undefined) {
-          warn(`browser bridge: workspace API is unavailable; sessions will remain ungrouped`)
-          return undefined
-        }
-        const response = await workspaceApi.create({
-          rpcId: RpcId(randomUUID()),
-          payload: { path: workspacePath },
-        })
-        if (!response.result.ok) {
-          warn(
-            `browser bridge: workspace.create failed for "${workspacePath}" `
-            + `(${response.result.error.code}: ${response.result.error.message}); sessions will remain ungrouped`,
-          )
-          return undefined
-        }
-        return response.result.value.workspace.workspaceId
-      } catch (error: unknown) {
-        warn(
-          `browser bridge: could not prepare session workspace "${workspacePath}": `
-          + `${String(error)}; sessions will remain ungrouped`,
-        )
-        return undefined
-      }
-    })()
-    return workspacePromise
-  }
- 
-  return {
-    ...api,
-    sessions: {
-      ...api.sessions,
-      async create(request) {
-        if (request.payload.workspaceId !== undefined) return api.sessions.create(request)
-        const workspaceId = await ensureWorkspace()
-        if (workspaceId === undefined) return api.sessions.create(request)
-        const payload = { ...request.payload, workspaceId }
-        delete payload.cwd
-        return api.sessions.create({ ...request, payload })
-      },
-    },
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/sort-arrow-sprite.png b/packages/browser/bridge-browser/coverage/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316e..000000000 Binary files a/packages/browser/bridge-browser/coverage/sort-arrow-sprite.png and /dev/null differ diff --git a/packages/browser/bridge-browser/coverage/sorter.js b/packages/browser/bridge-browser/coverage/sorter.js deleted file mode 100644 index 4ed70ae5a..000000000 --- a/packages/browser/bridge-browser/coverage/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/packages/browser/bridge-browser/coverage/token.ts.html b/packages/browser/bridge-browser/coverage/token.ts.html deleted file mode 100644 index a0fe5855d..000000000 --- a/packages/browser/bridge-browser/coverage/token.ts.html +++ /dev/null @@ -1,385 +0,0 @@ - - - - - - Code coverage report for token.ts - - - - - - - - - -
-
-

All files token.ts

-
- -
- 100% - Statements - 24/24 -
- - -
- 100% - Branches - 16/16 -
- - -
- 100% - Functions - 6/6 -
- - -
- 100% - Lines - 22/22 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -5x -  -  -  -  -  -  -  -7x -  -  -  -  -  -  -  -  -  -  -  -  -  -34x -34x -34x -26x -  -  -  -  -  -  -  -5x -  -  -  -  -  -  -  -  -3x -3x -  -2x -  -  -  -  -  -  -  -  -  -3x -3x -3x -3x -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -8x -5x -  -3x -3x -2x -2x -2x -  - 
/**
- * Bridge bearer-token lifecycle: generation, constant-time verification, and
- * file persistence under the dsh home directory.
- *
- * The token authenticates the browser extension against the bridge WebSocket.
- * It is NOT the /api trust fence (that stays untouched); it is the bridge
- * path's own auth because the bridge route lives outside the fence by design.
- *
- * @module
- */
- 
-import { randomBytes, timingSafeEqual } from 'node:crypto'
-import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
-import { dirname } from 'node:path'
-import { dshHomePath } from '@deepseek-ai/dsh-paths'
- 
-/** File name of the persisted token inside the dsh home. */
-export const TOKEN_FILE_NAME = 'ext-bridge-token'
- 
-/**
- * Generate a fresh token as lowercase hex.
- * @param bytes - entropy bytes; defaults to DEFAULT_TOKEN_BYTES (256-bit).
- * @returns the hex token string.
- */
-export function generateToken(bytes: number = 32): string {
-  return randomBytes(bytes).toString('hex')
-}
- 
-/**
- * Constant-time token comparison. Length mismatch fails fast (still constant
- * time on the compared prefix) — a wrong-length token can never verify.
- * @param expected - the configured token.
- * @param actual - the token presented by the client.
- * @returns true only when both are equal-length hex and byte-equal.
- */
-export function verifyToken(expected: string, actual: string): boolean {
-  // UTF-8 byte comparison, not hex decoding: hex would silently truncate
-  // non-hex configured tokens (Buffer.from('fixed-token','hex') is empty)
-  // and collapse 'deadbeef-team' to 'deadbeef', losing token entropy.
-  const expectedBuf = Buffer.from(expected, 'utf8')
-  const actualBuf = Buffer.from(actual, 'utf8')
-  if (expectedBuf.length === 0 || expectedBuf.length !== actualBuf.length) return false
-  return timingSafeEqual(expectedBuf, actualBuf)
-}
- 
-/**
- * Path of the persisted token file under the dsh home.
- * @returns absolute path like `~/.dsh/ext-bridge-token`.
- */
-export function tokenFilePath(): string {
-  return dshHomePath(TOKEN_FILE_NAME)
-}
- 
-/**
- * Read the persisted token; returns undefined when absent or unreadable.
- * @param file - token file path.
- * @returns the stored hex token, trimmed.
- */
-export async function readTokenFile(file: string = tokenFilePath()): Promise<string | undefined> {
-  try {
-    return (await readFile(file, 'utf8')).trim()
-  } catch {
-    return undefined
-  }
-}
- 
-/**
- * Persist a token atomically (temp file + rename) with 0600 permissions.
- * @param token - hex token to persist.
- * @param file - token file path.
- */
-export async function writeTokenFile(token: string, file: string = tokenFilePath()): Promise<void> {
-  await mkdir(dirname(file), { recursive: true })
-  const temp = `${file}.tmp-${process.pid}`
-  await writeFile(temp, `${token}\n`, { mode: 0o600 })
-  await chmod(temp, 0o600)
-  await rename(temp, file)
-}
- 
-/**
- * Resolve the bridge token: an explicitly configured token wins; otherwise the
- * persisted file is reused when present, and a fresh token is generated and
- * persisted otherwise.
- * @param configured - token from plugin config, or undefined.
- * @param file - token file path (injectable for tests).
- * @returns `{ token, file, generated }` where `generated` records whether a new token was minted.
- */
-export async function resolveToken(
-  configured: string | undefined,
-  file: string = tokenFilePath(),
-): Promise<{ token: string; file: string; generated: boolean }> {
-  if (configured !== undefined && configured.length > 0) {
-    return { token: configured, file, generated: false }
-  }
-  const persisted = await readTokenFile(file)
-  if (persisted !== undefined && persisted.length > 0) return { token: persisted, file, generated: false }
-  const token = generateToken()
-  await writeTokenFile(token, file)
-  return { token, file, generated: true }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/coverage/tools.ts.html b/packages/browser/bridge-browser/coverage/tools.ts.html deleted file mode 100644 index 4ecabc8c3..000000000 --- a/packages/browser/bridge-browser/coverage/tools.ts.html +++ /dev/null @@ -1,862 +0,0 @@ - - - - - - Code coverage report for tools.ts - - - - - - - - - -
-
-

All files tools.ts

-
- -
- 100% - Statements - 48/48 -
- - -
- 100% - Branches - 17/17 -
- - -
- 100% - Functions - 23/23 -
- - -
- 100% - Lines - 39/39 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -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 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -4x -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -4x -  -  -4x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -13x -13x -18x -18x -  -  -13x -143x -  -13x -  -  -  -  -18x -17x -  -1x -  -  -  -  -  -  -  -  -13x -  -  -  -  -  -  -  -  -  -  -  -3x -3x -  -  -  -  -  -  -13x -  -  -  -  -  -  -  -  -1x -  -  -13x -  -  -  -  -  -  -  -  -  -  -  -  -2x -2x -  -  -  -  -  -  -  -13x -  -  -  -  -  -  -  -  -1x -  -  -13x -  -  -  -  -  -  -  -  -  -  -2x -2x -  -  -  -  -  -  -13x -  -  -  -  -  -  -  -  -1x -  -  -39x -  -  -  -  -  -3x -  -  -13x -  -  -  -  -  -  -  -  -  -2x -2x -  -  -  -13x -  -  -  -  -  -  -  -  -  -3x -3x -  -  -  -13x -  -  -  -  -  -  -  -  -  -  -  -  -  - 
/**
- * Model-facing browser tools. Every tool executes by dispatching a `tool.call`
- * over the bridge to the connected extension, which performs the action in the
- * user's active tab and returns a pure-text result.
- *
- * The whole surface is text-only by design (DeepSeek models have no vision):
- * `browser_snapshot` renders the page as structured text with a numbered
- * interactive inventory, and every other tool addresses elements by that
- * inventory's stable index. Results are single `{ text }` objects rendered as
- * one text ContentBlock.
- *
- * @module
- */
- 
-import type { Context } from 'cordis'
-import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
-import type { BridgeServer } from './server.ts'
- 
-/** Options resolved from plugin config before tool registration. */
-export interface BrowserToolsOptions {
-  /** Per-tool-call budget in ms (also the bridge's default). */
-  toolTimeoutMs: number
-  /** Upper bound on one snapshot's rendered characters. */
-  snapshotMaxChars: number
-  /** Upper bound on interactive inventory items per snapshot. */
-  maxInteractiveItems: number
-}
- 
-/** Canonical tool result: one text payload. */
-interface TextResult {
-  text: string
-}
- 
-/** Output contract shared by every browser tool. */
-const TEXT_OUTPUT: ToolDefinition['output'] = {
-  schema: {
-    type: 'object',
-    additionalProperties: false,
-    properties: { text: { type: 'string' } },
-    required: ['text'],
-  },
-  render: (_args, value) => {
-    const result = value as unknown as TextResult
-    return [{ type: 'text', text: result.text }]
-  },
-}
- 
-/** 显式 JSON Schema object 顶层:空 parameters 对象会被 DeepSeek 适配器序列化成
- * `{ type: null }` 并遭 API 拒绝(400 INVALID_REQUEST),所以每个工具的参数
- * schema 都必须显式声明 `type: 'object'`。 */
-const OBJECT_SCHEMA = { type: 'object' as const, additionalProperties: false as const }
- 
-/** The keys the extension accepts as wire action names (tool name == action name). */
-export const BROWSER_TOOL_NAMES = [
-  'browser_snapshot',
-  'browser_click',
-  'browser_type',
-  'browser_press',
-  'browser_scroll',
-  'browser_navigate',
-  'browser_back',
-  'browser_forward',
-  'browser_reload',
-  'browser_get_text',
-  'browser_wait',
-] as const
- 
-/**
- * Register the browser tools on `ctx.tools`. Disposers are returned for the
- * caller's effect to own; each tool's cooperative timeout budget is declared
- * so `@deepseek-ai/dsh-timeout-policy` can enforce it, and every execute
- * forwards `exec.signal` into the bridge call (abort settles it).
- *
- * @param ctx - Cordis context with the tools service.
- * @param bridge - the authenticated bridge server.
- * @param options - resolved tool budgets.
- * @returns disposers keyed by tool name.
- */
-export function registerBrowserTools(
-  ctx: Context,
-  bridge: BridgeServer,
-  options: BrowserToolsOptions,
-): Map<string, () => void> {
-  const disposers = new Map<string, () => void>()
-  const call = async (exec: { signal: AbortSignal }, name: string, args: Record<string, unknown>): Promise<TextResult> => {
-    const result = await bridge.requestTool(name, args, exec.signal, options.toolTimeoutMs)
-    return normalizeTextResult(result, name)
-  }
- 
-  for (const tool of defineTools(call, options)) {
-    disposers.set(tool.name, ctx.tools.register(tool))
-  }
-  return disposers
-}
- 
-/** Normalize the extension's result payload to the canonical `{ text }` shape. */
-function normalizeTextResult(result: unknown, name: string): TextResult {
-  if (typeof result === 'object' && result !== null && typeof (result as { text?: unknown }).text === 'string') {
-    return { text: (result as { text: string }).text }
-  }
-  return { text: `${name} returned no text: ${JSON.stringify(result)}` }
-}
- 
-interface Call {
-  (exec: { signal: AbortSignal }, name: string, args: Record<string, unknown>): Promise<TextResult>
-}
- 
-/** The v1 tool set, model-perspective contracts only (no transport vocabulary). */
-function defineTools(call: Call, options: BrowserToolsOptions): ToolDefinition[] {
-  const snapshot = (): ToolDefinition => ({
-    name: 'browser_snapshot',
-    description: '读取当前浏览器页面的结构化文本快照(无截图):标题、URL、正文摘要、带编号的可交互元素清单、表单字段。'
-      + '编号是后续 browser_click/browser_type 等操作的定位依据。页面未变化时设置 delta=true 只返回变化部分,节省上下文。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      delta: { type: 'boolean', description: 'true 时只返回相对上次快照的变化(编号、URL、标题)。默认 false 返回完整快照。' },
-      region: { type: 'string', description: '可选:只读取页面某个区域(CSS 选择器或 "main"),用于懒加载内容。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => {
-      const a = args as { delta?: boolean; region?: string }
-      return call(exec, 'browser_snapshot', {
-        ...a.delta !== undefined ? { delta: a.delta } : {},
-        ...a.region !== undefined ? { region: a.region } : {},
-      })
-    },
-  })
- 
-  const click = (): ToolDefinition => ({
-    name: 'browser_click',
-    description: '点击当前页面清单中编号为 index 的可交互元素。编号来自最近一次 browser_snapshot;页面变化后编号可能重排,重排时会明确提示。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      index: { type: 'number', required: true, description: 'browser_snapshot 清单中的元素编号。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => call(exec, 'browser_click', args as Record<string, unknown>),
-  })
- 
-  const type = (): ToolDefinition => ({
-    name: 'browser_type',
-    description: '向当前页面编号为 index 的输入框输入文本。默认追加到现有值之后;replace=true 时先清空再输入。'
-      + '敏感字段(密码/卡号)的值不会回传,输入后立即清空本地记录。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      index: { type: 'number', required: true, description: '表单字段编号(来自 browser_snapshot 的 forms 清单)。' },
-      text: { type: 'string', required: true, description: '要输入的文本。' },
-      replace: { type: 'boolean', description: 'true 时清空现有值后输入。默认追加。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => {
-      const a = args as { index: number; text: string; replace?: boolean }
-      return call(exec, 'browser_type', {
-        index: a.index,
-        text: a.text,
-        ...a.replace !== undefined ? { replace: a.replace } : {},
-      })
-    },
-  })
- 
-  const press = (): ToolDefinition => ({
-    name: 'browser_press',
-    description: '向当前页面发送一个按键。常用值:Enter、Tab、Escape、ArrowUp、ArrowDown、ArrowLeft、ArrowRight、Backspace、Delete。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      key: { type: 'string', required: true, description: '按键名(KeyboardEvent.key 语义)。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => call(exec, 'browser_press', args as Record<string, unknown>),
-  })
- 
-  const scroll = (): ToolDefinition => ({
-    name: 'browser_scroll',
-    description: '滚动当前页面。direction 为 up/down/top/bottom;amount 为像素数(默认一屏)。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      direction: { type: 'string', required: true, enum: ['up', 'down', 'top', 'bottom'], description: '滚动方向。' },
-      amount: { type: 'number', description: '滚动像素数;top/bottom 时忽略。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => {
-      const a = args as { direction: 'up' | 'down' | 'top' | 'bottom'; amount?: number }
-      return call(exec, 'browser_scroll', {
-        direction: a.direction,
-        ...a.amount !== undefined ? { amount: a.amount } : {},
-      })
-    },
-  })
- 
-  const navigate = (): ToolDefinition => ({
-    name: 'browser_navigate',
-    description: '在当前标签页导航到指定 URL。保留当前登录状态(cookie/会话)。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      url: { type: 'string', required: true, description: '完整 URL(http/https)。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => call(exec, 'browser_navigate', args as Record<string, unknown>),
-  })
- 
-  const simple = (name: 'browser_back' | 'browser_forward' | 'browser_reload', description: string): ToolDefinition => ({
-    name,
-    description,
-    parameters: { ...OBJECT_SCHEMA, properties: {} },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (_args, exec) => call(exec, name, {}),
-  })
- 
-  const getText = (): ToolDefinition => ({
-    name: 'browser_get_text',
-    description: '读取当前页面指定区域的文本(用于懒加载内容或局部更新)。不带 selector 时返回整个页面的纯文本。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      selector: { type: 'string', description: 'CSS 选择器;缺省为整个页面。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => {
-      const a = args as { selector?: string }
-      return call(exec, 'browser_get_text', { ...a.selector !== undefined ? { selector: a.selector } : {} })
-    },
-  })
- 
-  const wait = (): ToolDefinition => ({
-    name: 'browser_wait',
-    description: '等待页面稳定(加载完成且无 DOM 变化)。在点击或导航后需要等结果渲染时使用。',
-    parameters: {
-      ...OBJECT_SCHEMA,
-      ms: { type: 'number', description: '额外等待毫秒数;缺省只做稳定性检测。' },
-    },
-    timeoutMs: options.toolTimeoutMs,
-    output: TEXT_OUTPUT,
-    execute: (args, exec) => {
-      const a = args as { ms?: number }
-      return call(exec, 'browser_wait', { ...a.ms !== undefined ? { ms: a.ms } : {} })
-    },
-  })
- 
-  return [
-    snapshot(),
-    click(),
-    type(),
-    press(),
-    scroll(),
-    navigate(),
-    simple('browser_back', '返回上一页。'),
-    simple('browser_forward', '前进到下一页。'),
-    simple('browser_reload', '重新加载当前页面。'),
-    getText(),
-    wait(),
-  ]
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/packages/browser/bridge-browser/src/bridge-url.ts b/packages/browser/bridge-browser/src/bridge-url.ts deleted file mode 100644 index cf957e80a..000000000 --- a/packages/browser/bridge-browser/src/bridge-url.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Resolve the browser-extension bridge WebSocket URL from the current page - * location or a discovery response. Shared by the settings row and tests. - * @module @yuxianglin/dsh-bridge-browser/src/bridge-url - */ - -import { BRIDGE_CONFIG_PATH, BRIDGE_PATH } from './protocol.ts' - -/** Minimal Location fields needed to rebuild a loopback-friendly ws URL. */ -export interface BridgeLocationLike { - protocol: string - hostname: string - port: string - host: string -} - -/** - * Build `ws(s)://…/ext/bridge` from the page that hosts the dsh web UI. - * Loopback hostnames are normalized to `127.0.0.1` so the address pastes cleanly - * into the Chrome extension settings. - */ -export function bridgeWsUrlFromLocation( - location: BridgeLocationLike, - bridgePath: string = BRIDGE_PATH, -): string { - const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:' - const hostname = location.hostname === 'localhost' ? '127.0.0.1' : location.hostname - const host = location.port === '' ? hostname : `${hostname}:${location.port}` - return `${wsProtocol}//${host}${bridgePath}` -} - -/** - * Prefer the discovery endpoint; fall back to reconstructing from `location`. - * @param fetchImpl - injectable fetch (defaults to global fetch). - * @param location - page location used for fallback and relative discovery URL. - */ -export async function resolveBridgeWsUrl( - location: BridgeLocationLike & { origin?: string }, - fetchImpl: typeof fetch = fetch, - bridgeConfigPath: string = BRIDGE_CONFIG_PATH, -): Promise { - const fallback = bridgeWsUrlFromLocation(location) - try { - const base = location.origin ?? `${location.protocol}//${location.host}` - const response = await fetchImpl(`${base}${bridgeConfigPath}`, { - signal: AbortSignal.timeout(1_500), - }) - if (!response.ok) return fallback - const body = await response.json() as { wsUrl?: unknown } - if (typeof body.wsUrl === 'string' && (body.wsUrl.startsWith('ws://') || body.wsUrl.startsWith('wss://'))) { - return body.wsUrl - } - } catch { - // Discovery is best-effort: the settings row still shows the reconstructed URL. - } - return fallback -} diff --git a/packages/browser/bridge-browser/src/devtools-tools.ts b/packages/browser/bridge-browser/src/devtools-tools.ts index 8edf220d0..9f5fb8e42 100644 --- a/packages/browser/bridge-browser/src/devtools-tools.ts +++ b/packages/browser/bridge-browser/src/devtools-tools.ts @@ -17,7 +17,6 @@ import type { Context } from '@deepseek-ai/cordis' import { defineTool, type ToolDefinition, type ToolRunContext } from '@deepseek-ai/dsh-tools' -import { DEVTOOLS_TOOL_NAMES } from './protocol.ts' /** Options resolved from plugin config before tool registration. */ export interface BrowserDevToolsOptions { @@ -266,8 +265,3 @@ export function defineDevTools(call: Call, options: BrowserDevToolsOptions): Too requestBody(), ] } - -/** The registered names, in the order this module defines them. */ -export function devToolsToolNames(): readonly string[] { - return DEVTOOLS_TOOL_NAMES -} diff --git a/packages/browser/bridge-browser/src/extension-sessions.ts b/packages/browser/bridge-browser/src/extension-sessions.ts index fdba0e0f9..63ce48a7d 100644 --- a/packages/browser/bridge-browser/src/extension-sessions.ts +++ b/packages/browser/bridge-browser/src/extension-sessions.ts @@ -18,23 +18,4 @@ export class ExtensionSessionRegistry { has(sessionId: string | undefined): boolean { return typeof sessionId === 'string' && this.ids.has(sessionId) } - - /** Test helper: drop all tracked ids. */ - clear(): void { - this.ids.clear() - } -} - -/** - * Decide whether the bridge should own ask_user_question for this request. - * Desktop sessions must fall through to the native answerer waterfall. - */ -export function shouldBridgeOwnQuestion(input: { - hasExtensionConnection: boolean - sessionId: string | undefined - extensionSessions: Pick -}): boolean { - return input.hasExtensionConnection - && input.sessionId !== undefined - && input.extensionSessions.has(input.sessionId) } diff --git a/packages/browser/bridge-browser/src/host-api.ts b/packages/browser/bridge-browser/src/host-api.ts index f75d6d534..3681f5200 100644 --- a/packages/browser/bridge-browser/src/host-api.ts +++ b/packages/browser/bridge-browser/src/host-api.ts @@ -2,7 +2,7 @@ * Bridge-owned Host API consumed by the WebSocket carrier. * * This boundary keeps release-specific Host topology out of the browser wire - * server. dsh 0.1.5 implements it with Typert Remotes and Connection. + * server. dsh implements it with Typert Remotes and Connection. * * @module */ @@ -43,21 +43,6 @@ export interface BrowserHostApi { respond(rpcId: string, result: RespondResult, signal: AbortSignal): Promise } -/** Convert an arbitrary Host rejection to the open wire failure vocabulary. */ -export function hostFailure(error: unknown): HostRpcFailure { - if (isRecord(error)) { - const code = typeof error.code === 'string' ? error.code : 'internal' - const message = typeof error.message === 'string' ? error.message : String(error) - const details = isRecord(error.details) ? error.details : {} - return { code, message, details } - } - return { - code: 'internal', - message: error instanceof Error ? error.message : String(error), - details: {}, - } -} - /** Narrow unknown JSON-like data without accepting arrays. */ export function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) diff --git a/packages/browser/bridge-browser/src/index.ts b/packages/browser/bridge-browser/src/index.ts index 1d04f1173..7cd5960c0 100644 --- a/packages/browser/bridge-browser/src/index.ts +++ b/packages/browser/bridge-browser/src/index.ts @@ -108,7 +108,10 @@ export interface Config { beaconPorts?: number[] } -export const Config: z = z.object({ +/** The shape after schemastery applies its defaults to every field. */ +type ResolvedConfig = Required> & Pick + +export const Config: z = z.object({ token: z.string(), toolTimeoutMs: z.number().step(1).min(1).default(DEFAULT_TOOL_TIMEOUT_MS), snapshotMaxChars: z.number().step(1).min(MIN_SNAPSHOT_MAX_CHARS).default(DEFAULT_SNAPSHOT_MAX_CHARS), @@ -120,63 +123,17 @@ export const Config: z = z.object({ beaconPorts: z.array(z.number().step(1).min(1).max(65535)).default([...BEACON_PORTS]), }) -/** The shape after schemastery applies its defaults to every field. */ -type ResolvedConfig = Required> & Pick - -/** Configured budgets must be positive integers. Exported for validation tests. */ -export function assertPositiveInteger(name: string, value: number): void { - if (!Number.isInteger(value) || value < 1) { - throw new Error(`bridge-browser: ${name} must be a positive integer`) - } -} - -/** - * Apply defaults and direct-call validation at the plugin boundary. - * @param config - Loader-resolved or directly supplied plugin configuration. - * @returns a complete configuration ready for runtime use. - */ -export function resolveConfig(config: Config): ResolvedConfig { - const resolved: ResolvedConfig = { - ...(config.token === undefined ? {} : { token: config.token }), - toolTimeoutMs: config.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS, - snapshotMaxChars: config.snapshotMaxChars ?? DEFAULT_SNAPSHOT_MAX_CHARS, - maxInteractiveItems: config.maxInteractiveItems ?? DEFAULT_MAX_INTERACTIVE_ITEMS, - devToolsMaxChars: config.devToolsMaxChars ?? DEFAULT_DEVTOOLS_MAX_CHARS, - sessionWorkspacePath: config.sessionWorkspacePath ?? DEFAULT_SESSION_WORKSPACE_PATH, - deferSessionCreate: config.deferSessionCreate ?? DEFAULT_DEFER_SESSION_CREATE, - beacon: config.beacon ?? DEFAULT_BEACON, - beaconPorts: [...(config.beaconPorts ?? BEACON_PORTS)], - } - if (resolved.beacon && resolved.beaconPorts.length === 0) { - throw new Error('bridge-browser: beaconPorts must name at least one port when the beacon is enabled') - } - for (const port of resolved.beaconPorts) { - if (!Number.isInteger(port) || port < 1 || port > 65535) { - throw new Error(`bridge-browser: beaconPorts must contain integers from 1 through 65535, got ${String(port)}`) - } - } - assertPositiveInteger('toolTimeoutMs', resolved.toolTimeoutMs) - assertPositiveInteger('snapshotMaxChars', resolved.snapshotMaxChars) - if (resolved.snapshotMaxChars < MIN_SNAPSHOT_MAX_CHARS) { - throw new Error(`bridge-browser: snapshotMaxChars must be at least ${MIN_SNAPSHOT_MAX_CHARS}`) - } - assertPositiveInteger('maxInteractiveItems', resolved.maxInteractiveItems) - assertPositiveInteger('devToolsMaxChars', resolved.devToolsMaxChars) - if (resolved.devToolsMaxChars < MIN_SNAPSHOT_MAX_CHARS) { - throw new Error(`bridge-browser: devToolsMaxChars must be at least ${MIN_SNAPSHOT_MAX_CHARS}`) - } - return resolved -} - /** * Mount the bridge: resolve the token, register the upgrade route, the tool * set, and an optional system-prompt section, all effect-scoped for HMR. * * @param ctx - Cordis context. - * @param config - plugin config (schema defaults applied). + * @param resolved - Cordis 完成校验与默认值填充的配置。 */ -export async function apply(ctx: Context, config: Config): Promise { - const resolved = resolveConfig(config) +export async function apply(ctx: Context, resolved: ResolvedConfig): Promise { + if (resolved.beacon && resolved.beaconPorts.length === 0) { + throw new Error('bridge-browser: beaconPorts must name at least one port when the beacon is enabled') + } const gateway = ctx.get('typertGateway') as unknown as GatewayCandidate | undefined const connection = ctx.get('connection') as unknown as HostConnectionLike | undefined diff --git a/packages/browser/bridge-browser/src/protocol.ts b/packages/browser/bridge-browser/src/protocol.ts index 229553eb2..8e4af99cd 100644 --- a/packages/browser/bridge-browser/src/protocol.ts +++ b/packages/browser/bridge-browser/src/protocol.ts @@ -48,9 +48,6 @@ export const HELLO_TIMEOUT_MS = 5_000 /** Server-side ping cadence; the client answers `pong` to prove liveness. */ export const PING_INTERVAL_MS = 30_000 -/** Default bytes of the generated bearer token (256-bit). */ -export const DEFAULT_TOKEN_BYTES = 32 - /** Default rendered-snapshot character budget. */ export const DEFAULT_SNAPSHOT_MAX_CHARS = 32_000 @@ -177,16 +174,6 @@ export function isServerFrame(frame: BridgeFrame): frame is ServerFrame { || frame.t === 'error' } -/** - * Type guard: is this frame one the CLIENT may send? Server-only shapes - * narrow out, so client-side consumers never dispatch on server vocabulary. - * @param frame - parsed frame. - * @returns true for client-sendable frames. - */ -export function isClientFrame(frame: BridgeFrame): frame is ClientFrame { - return frame.t === 'hello' || frame.t === 'rpc' || frame.t === 'respond' || frame.t === 'tool.result' || frame.t === 'pong' -} - /** * Parse one WebSocket message into a frame. * @param text - raw message text. diff --git a/packages/browser/bridge-browser/src/remote-host-api.ts b/packages/browser/bridge-browser/src/remote-host-api.ts index fb4222694..c6f0c4153 100644 --- a/packages/browser/bridge-browser/src/remote-host-api.ts +++ b/packages/browser/bridge-browser/src/remote-host-api.ts @@ -16,11 +16,8 @@ import type { HostRpcFailure, HostRpcResult, } from './host-api.ts' -import { hostFailure, isRecord } from './host-api.ts' -import { - ExtensionSessionRegistry, - shouldBridgeOwnQuestion, -} from './extension-sessions.ts' +import { isRecord } from './host-api.ts' +import { ExtensionSessionRegistry } from './extension-sessions.ts' import type { RespondResult } from './protocol.ts' /** Structural subset of dsh 0.2.1's Host TypertGateway service. */ @@ -481,11 +478,7 @@ class RemoteHostApi implements BrowserHostApi { } private failure(error: unknown): HostRpcFailure { - try { - return this.gateway.wireStream.failure(error) - } catch { - return hostFailure(error) - } + return this.gateway.wireStream.failure(error) } private async sendRemoteEventResult( @@ -790,11 +783,7 @@ class EventGeneration { } // Desktop-owned sessions keep the native waterfall. Only forward questions // for sessions the extension successfully created or prompted. - if (!shouldBridgeOwnQuestion({ - hasExtensionConnection: true, - sessionId: value.agentId, - extensionSessions: this.extensionSessions, - })) { + if (!this.extensionSessions.has(value.agentId)) { const clientId = this.clientId if (clientId !== undefined) { await this.sendResult(clientId, value.eventId, { kind: 'next' }, this.signal) diff --git a/packages/browser/bridge-browser/src/server.ts b/packages/browser/bridge-browser/src/server.ts index 9d35ac2c7..afa8f1c7f 100644 --- a/packages/browser/bridge-browser/src/server.ts +++ b/packages/browser/bridge-browser/src/server.ts @@ -385,7 +385,7 @@ export class BridgeServer { void this.handleRespond(frame) break case 'tool.result': - this.settleTool(frame.id, frame.ok, frame.ok ? frame.result : frame.error) + this.settleTool(frame) break case 'pong': case 'hello': @@ -512,13 +512,13 @@ export class BridgeServer { } } - private settleTool(id: string, ok: boolean, payload: unknown): void { - const pending = this.pendingTools.get(id) + private settleTool(frame: Extract): void { + const pending = this.pendingTools.get(frame.id) if (pending === undefined) return clearTimeout(pending.timer) - this.pendingTools.delete(id) - if (ok) pending.resolve(payload) - else pending.reject(new BridgeToolError(payloadCode(payload), payloadMessage(payload))) + this.pendingTools.delete(frame.id) + if (frame.ok) pending.resolve(frame.result) + else pending.reject(new BridgeToolError(frame.error.code, frame.error.message || 'browser action failed')) } /** Close the current connection (if any) and settle its in-flight calls. */ @@ -560,35 +560,3 @@ function orderedSessionId(frame: Extract): string | u const sessionId = (frame.payload as Record).sessionId return typeof sessionId === 'string' ? sessionId : undefined } - -/** - * Tool error payload → stable code. The wire parser enforces string fields, - * so the fallback branches are parser-gated; exported so the fallback - * contract is unit-testable directly. - * @param payload - extension-reported error payload. - * @returns the stable error code. - */ -export function payloadCode(payload: unknown): ToolErrorCode { - if (typeof payload === 'object' && payload !== null) { - const code = (payload as { code?: unknown }).code - if (typeof code === 'string') return code as ToolErrorCode - return 'internal' - } - return 'internal' -} - -/** - * Tool error payload → message. The wire parser enforces string fields, so - * the fallback branches are parser-gated; exported so the fallback contract - * is unit-testable directly. - * @param payload - extension-reported error payload. - * @returns the human-readable message. - */ -export function payloadMessage(payload: unknown): string { - if (typeof payload === 'object' && payload !== null) { - const message = (payload as { message?: unknown }).message - if (typeof message === 'string' && message.length > 0) return message - return 'browser action failed' - } - return 'browser action failed' -} diff --git a/packages/browser/bridge-browser/src/token.ts b/packages/browser/bridge-browser/src/token.ts index 7b48a0785..c7ebfd9b2 100644 --- a/packages/browser/bridge-browser/src/token.ts +++ b/packages/browser/bridge-browser/src/token.ts @@ -19,7 +19,7 @@ export const TOKEN_FILE_NAME = 'ext-bridge-token' /** * Generate a fresh token as lowercase hex. - * @param bytes - entropy bytes; defaults to DEFAULT_TOKEN_BYTES (256-bit). + * @param bytes - 随机数据的字节数,默认 32(256 位)。 * @returns the hex token string. */ export function generateToken(bytes: number = 32): string { diff --git a/packages/browser/bridge-browser/tests/bridge-url.spec.ts b/packages/browser/bridge-browser/tests/bridge-url.spec.ts deleted file mode 100644 index 26236e9f5..000000000 --- a/packages/browser/bridge-browser/tests/bridge-url.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { bridgeWsUrlFromLocation, resolveBridgeWsUrl } from '../src/bridge-url.ts' - -describe('bridgeWsUrlFromLocation', () => { - it('builds a loopback ws URL and normalizes localhost', () => { - expect(bridgeWsUrlFromLocation({ - protocol: 'http:', - hostname: '127.0.0.1', - port: '63566', - host: '127.0.0.1:63566', - })).toBe('ws://127.0.0.1:63566/ext/bridge') - - expect(bridgeWsUrlFromLocation({ - protocol: 'http:', - hostname: 'localhost', - port: '43189', - host: 'localhost:43189', - })).toBe('ws://127.0.0.1:43189/ext/bridge') - }) - - it('uses wss on https pages and omits the default port when absent', () => { - expect(bridgeWsUrlFromLocation({ - protocol: 'https:', - hostname: 'example.com', - port: '', - host: 'example.com', - })).toBe('wss://example.com/ext/bridge') - }) -}) - -describe('resolveBridgeWsUrl', () => { - it('prefers /ext/bridge-config when available', async () => { - const fetchImpl = vi.fn(async () => new Response( - JSON.stringify({ wsUrl: 'ws://127.0.0.1:43189/ext/bridge' }), - { status: 200, headers: { 'content-type': 'application/json' } }, - )) - await expect(resolveBridgeWsUrl({ - protocol: 'http:', - hostname: '127.0.0.1', - port: '9999', - host: '127.0.0.1:9999', - origin: 'http://127.0.0.1:9999', - }, fetchImpl as unknown as typeof fetch)).resolves.toBe('ws://127.0.0.1:43189/ext/bridge') - expect(fetchImpl).toHaveBeenCalledWith( - 'http://127.0.0.1:9999/ext/bridge-config', - expect.objectContaining({ signal: expect.any(AbortSignal) }), - ) - }) - - it('falls back to the page location when discovery fails', async () => { - const fetchImpl = vi.fn(async () => { - throw new Error('offline') - }) - await expect(resolveBridgeWsUrl({ - protocol: 'http:', - hostname: '127.0.0.1', - port: '63566', - host: '127.0.0.1:63566', - origin: 'http://127.0.0.1:63566', - }, fetchImpl as unknown as typeof fetch)).resolves.toBe('ws://127.0.0.1:63566/ext/bridge') - }) -}) diff --git a/packages/browser/bridge-browser/tests/e2e/bridge-extension.e2e.spec.ts b/packages/browser/bridge-browser/tests/e2e/bridge-extension.e2e.spec.ts deleted file mode 100644 index fed43d903..000000000 --- a/packages/browser/bridge-browser/tests/e2e/bridge-extension.e2e.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Browser smoke: the built MV3 extension connects through a real WebSocket to - * the migrated bridge carrier. dsh 0.1.2 Remote semantics are covered by - * remote-host-api.spec; this test deliberately owns no pre-0.1.2 Host shim. - */ - -import { existsSync } from 'node:fs' -import { mkdtemp, rm } from 'node:fs/promises' -import { createServer, type Server } from 'node:http' -import type { AddressInfo } from 'node:net' -import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { chromium, type BrowserContext } from 'playwright-core' -import { BridgeServer } from '../../src/server.ts' -import type { BrowserHostApi, HostRpcCall, HostRpcResult } from '../../src/host-api.ts' - -const TOKEN = 'e2e0e2e0e2e0e2e0e2e0e2e0e2e0e2e0' -const EXTENSION_DIR = resolve(import.meta.dirname, '../../../../../extensions/dsh-browser/dist') - -function chromiumExecutable(): string | undefined { - const fromEnv = process.env.PLAYWRIGHT_CHROMIUM_PATH - if (fromEnv !== undefined && existsSync(fromEnv)) return fromEnv - const cacheRoot = join(process.env.HOME ?? '', 'Library', 'Caches', 'ms-playwright') - if (!existsSync(cacheRoot)) return undefined - for (const dir of ['chromium-1217', 'chromium-1226', 'chromium-1181']) { - for (const candidate of [ - join(cacheRoot, dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'), - join(cacheRoot, dir, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'), - join(cacheRoot, dir, 'chrome-linux', 'chrome'), - ]) { - if (existsSync(candidate)) return candidate - } - } - return undefined -} - -function abortWait(signal: AbortSignal): Promise { - if (signal.aborted) return Promise.resolve() - return new Promise((resolveWait) => { - signal.addEventListener('abort', () => { resolveWait() }, { once: true }) - }) -} - -let executable: string | undefined -let browser: BrowserContext | undefined -let profile: string | undefined -let http: Server | undefined -let bridge: BridgeServer | undefined -let port: number | undefined -const calls: HostRpcCall[] = [] - -beforeAll(async () => { - executable = chromiumExecutable() - if (executable === undefined || !existsSync(join(EXTENSION_DIR, 'manifest.json'))) return - - const api: BrowserHostApi = { - async call(request): Promise { - calls.push(request) - if (request.method === 'session.create') return { ok: true, value: { sessionId: 'session-browser-e2e' } } - if (request.method === 'session.history') { - return { ok: true, value: { events: [], hasMore: false } } - } - if (request.method === 'session.list') return { ok: true, value: { items: [] } } - if (request.method === 'workspace.list') { - return { ok: true, value: { items: [], archivedSessionIds: [] } } - } - return { ok: true, value: {} } - }, - async *events(signal) { await abortWait(signal) }, - async respond() { return { accepted: false, reason: 'not-pending' } }, - } - bridge = new BridgeServer({ - token: TOKEN, - api, - toolTimeoutMs: 10_000, - caps: { textOnly: true, snapshotMaxChars: 32_000, maxInteractiveItems: 60 }, - injectBrowserSnapshot: () => {}, - purgeSession: async () => {}, - }) - http = createServer((req, res) => { - if (req.url === '/ext/bridge-config') { - res.writeHead(200, { 'content-type': 'application/json' }) - res.end(JSON.stringify({ wsUrl: `ws://127.0.0.1:${String(port)}/ext/bridge` })) - return - } - res.writeHead(404) - res.end('not found') - }) - http.on('upgrade', (req, socket, head) => { bridge?.handleUpgrade(req, socket, head) }) - await new Promise((resolveListen) => { http?.listen(0, '127.0.0.1', resolveListen) }) - port = (http.address() as AddressInfo).port - - profile = await mkdtemp(join(tmpdir(), 'dsh-browser-extension-e2e-')) - browser = await chromium.launchPersistentContext(profile, { - executablePath: executable, - channel: 'chromium', - headless: true, - args: [ - `--disable-extensions-except=${EXTENSION_DIR}`, - `--load-extension=${EXTENSION_DIR}`, - ], - }) -}) - -afterAll(async () => { - await browser?.close() - await bridge?.close() - if (http !== undefined) { - await new Promise((resolveClose) => { http?.close(() => { resolveClose() }) }) - } - if (profile !== undefined) await rm(profile, { recursive: true, force: true }) -}) - -describe('extension ↔ migrated bridge smoke', () => { - it('connects, negotiates caps, and initializes a Session through the private bridge protocol', { timeout: 60_000 }, async () => { - if (executable === undefined) { - console.warn('SKIP: no usable Chromium') - return - } - if (browser === undefined || port === undefined) { - console.warn('SKIP: extension dist not built') - return - } - - let worker = browser.serviceWorkers()[0] - worker ??= await browser.waitForEvent('serviceworker', { timeout: 30_000 }) - const extensionId = new URL(worker.url()).host - const panel = await browser.newPage() - await panel.goto(`chrome-extension://${extensionId}/panel/index.html`) - await panel.waitForSelector('header.topbar', { timeout: 15_000 }) - - await panel.click('button[aria-label="打开设置"]') - await panel.fill('input[placeholder*="自动检测"]', `ws://127.0.0.1:${String(port)}`) - await panel.fill('input[type="password"]', TOKEN) - await panel.click('text=保存并连接') - - await expect.poll( - () => panel.locator('.connection').textContent(), - { timeout: 30_000 }, - ).toContain('已连接') - await expect.poll( - () => calls.some(request => request.method === 'session.history'), - { timeout: 15_000 }, - ).toBe(true) - expect(calls.map(request => request.method)).toEqual(expect.arrayContaining([ - 'session.create', 'session.history', - ])) - await panel.close() - }) -}) diff --git a/packages/browser/bridge-browser/tests/extension-sessions.spec.ts b/packages/browser/bridge-browser/tests/extension-sessions.spec.ts index f0720b20a..650d0717f 100644 --- a/packages/browser/bridge-browser/tests/extension-sessions.spec.ts +++ b/packages/browser/bridge-browser/tests/extension-sessions.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { ExtensionSessionRegistry, shouldBridgeOwnQuestion } from '../src/extension-sessions.ts' +import { ExtensionSessionRegistry } from '../src/extension-sessions.ts' describe('ExtensionSessionRegistry', () => { it('tracks non-empty session ids only', () => { @@ -13,34 +13,3 @@ describe('ExtensionSessionRegistry', () => { expect(registry.has('other')).toBe(false) }) }) - -describe('shouldBridgeOwnQuestion', () => { - it('leaves Desktop sessions to the native waterfall', () => { - const extensionSessions = new ExtensionSessionRegistry() - expect(shouldBridgeOwnQuestion({ - hasExtensionConnection: true, - sessionId: 'desktop-session', - extensionSessions, - })).toBe(false) - }) - - it('owns questions only for extension-driven sessions while connected', () => { - const extensionSessions = new ExtensionSessionRegistry() - extensionSessions.note('ext-session') - expect(shouldBridgeOwnQuestion({ - hasExtensionConnection: true, - sessionId: 'ext-session', - extensionSessions, - })).toBe(true) - expect(shouldBridgeOwnQuestion({ - hasExtensionConnection: false, - sessionId: 'ext-session', - extensionSessions, - })).toBe(false) - expect(shouldBridgeOwnQuestion({ - hasExtensionConnection: true, - sessionId: undefined, - extensionSessions, - })).toBe(false) - }) -}) diff --git a/packages/browser/bridge-browser/tests/index.spec.ts b/packages/browser/bridge-browser/tests/index.spec.ts index 19b2005e0..0db413828 100644 --- a/packages/browser/bridge-browser/tests/index.spec.ts +++ b/packages/browser/bridge-browser/tests/index.spec.ts @@ -5,7 +5,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Context } from '@deepseek-ai/cordis' import { dshHomePath } from '@deepseek-ai/dsh-home-paths' import { BEACON_PORTS } from '../src/protocol.ts' -import { apply, assertPositiveInteger, Config, resolveConfig } from '../src/index.ts' +import { apply as applyPlugin, Config } from '../src/index.ts' + +async function apply(ctx: Context, config: Config): Promise { + await applyPlugin(ctx, new Config(config)) +} /** * The beacon opens real sockets; those are covered by discovery-beacon.spec.ts. @@ -63,33 +67,22 @@ afterEach(async () => { for (const dir of dirs.splice(0)) await rm(dir, { recursive: true, force: true }) }) -describe('assertPositiveInteger', () => { - it('accepts positive integers and rejects everything else', () => { - expect(() => assertPositiveInteger('x', 1)).not.toThrow() - expect(() => assertPositiveInteger('x', 0)).toThrow(/must be a positive integer/) - expect(() => assertPositiveInteger('x', -1)).toThrow(/must be a positive integer/) - expect(() => assertPositiveInteger('x', 1.5)).toThrow(/must be a positive integer/) - }) -}) - /** Valid budgets (the Loader applies schema defaults; hand-built tests pass them explicitly). */ const VALID = { toolTimeoutMs: 90_000, snapshotMaxChars: 32_000, maxInteractiveItems: 60, devToolsMaxChars: 24_000 } describe('config', () => { it('resolves defaults, including an enabled workspace under the dsh home', () => { - expect(resolveConfig({})).toEqual({ + expect(new Config()).toEqual({ ...VALID, sessionWorkspacePath: dshHomePath('browser-sessions'), deferSessionCreate: true, beacon: true, beaconPorts: [...BEACON_PORTS], }) - expect(new Config().sessionWorkspacePath).toBe(dshHomePath('browser-sessions')) - expect(new Config().beaconPorts).toEqual([...BEACON_PORTS]) }) it('preserves explicit values and the empty-string workspace opt-out', () => { - expect(resolveConfig({ + expect(new Config({ token: 'fixed', toolTimeoutMs: 1, snapshotMaxChars: 500, @@ -108,17 +101,16 @@ describe('config', () => { beacon: false, beaconPorts: [...BEACON_PORTS], }) - expect(new Config({ sessionWorkspacePath: '' }).sessionWorkspacePath).toBe('') }) it('rejects beacon ports that are not usable ports', () => { - expect(() => resolveConfig({ beaconPorts: [0] })).toThrow(/1 through 65535/) - expect(() => resolveConfig({ beaconPorts: [1.5] })).toThrow(/1 through 65535/) - expect(() => resolveConfig({ beaconPorts: [] })).toThrow(/at least one port/) + expect(() => new Config({ beaconPorts: [0] })).toThrow() + expect(() => new Config({ beaconPorts: [1.5] })).toThrow() + expect(() => new Config({ beaconPorts: [65536] })).toThrow() }) it('keeps an empty port list when the beacon is disabled', () => { - expect(resolveConfig({ beacon: false, beaconPorts: [] }).beaconPorts).toEqual([]) + expect(new Config({ beacon: false, beaconPorts: [] }).beaconPorts).toEqual([]) }) }) @@ -167,7 +159,11 @@ describe('apply', () => { it('rejects invalid budgets loudly', async () => { await expect(apply(stubContext(), { ...VALID, toolTimeoutMs: 0 })).rejects.toThrow(/toolTimeoutMs/) await expect(apply(stubContext(), { ...VALID, snapshotMaxChars: -1 })).rejects.toThrow(/snapshotMaxChars/) - await expect(apply(stubContext(), { ...VALID, snapshotMaxChars: 499 })).rejects.toThrow(/at least 500/) + await expect(apply(stubContext(), { ...VALID, snapshotMaxChars: 499 })).rejects.toThrow(/snapshotMaxChars/) + }) + + it('requires at least one port when the beacon is enabled', async () => { + await expect(apply(stubContext(), { beaconPorts: [] })).rejects.toThrow(/at least one port/) }) it('advertises the bridge on the default beacon ports', async () => { diff --git a/packages/browser/bridge-browser/tests/manifest.spec.ts b/packages/browser/bridge-browser/tests/manifest.spec.ts index 193ce2726..d73b0dde3 100644 --- a/packages/browser/bridge-browser/tests/manifest.spec.ts +++ b/packages/browser/bridge-browser/tests/manifest.spec.ts @@ -29,24 +29,11 @@ describe('plugin manifest', () => { expect(engines.node).toBe('>=24') }) - it('mirrors the pinned runtime in peer and dev dependencies', async () => { + it('pins runtime peer dependencies', () => { const peers = manifest.peerDependencies as Record - const dev = manifest.devDependencies as Record const dshPeers = Object.keys(peers).filter(name => name.startsWith('@deepseek-ai/dsh')) expect(dshPeers.length).toBeGreaterThan(0) for (const name of dshPeers) expect(peers[name]).toBe('0.2.1-rc.5') - for (const name of dshPeers) { - // Every runtime peer is also a build-time dependency of this workspace. - expect(dev[name] ?? peers[name]).toBeDefined() - } - }) - - it('stops exporting the invariant companion that dsh 0.2.1 removed', () => { - const exports = manifest.exports as Record - const files = manifest.files as string[] - expect(Object.keys(exports)).not.toContain('./invariant') - expect(files).not.toContain('lib/invariant.js') - expect(peersAndDev(manifest)).not.toContain('@deepseek-ai/dsh-invariants') }) it('publishes the resources the Plugins page reads', () => { @@ -77,10 +64,3 @@ describe('plugin manifest', () => { expect((meta.description as string).trim()).not.toBe('') }) }) - -/** Every DSH package this manifest references, as a flat name list. */ -function peersAndDev(value: Record): string[] { - const peers = Object.keys((value.peerDependencies ?? {}) as Record) - const dev = Object.keys((value.devDependencies ?? {}) as Record) - return [...new Set([...peers, ...dev])] -} diff --git a/packages/browser/bridge-browser/tests/protocol.spec.ts b/packages/browser/bridge-browser/tests/protocol.spec.ts index 98fba10ee..992ecb8de 100644 --- a/packages/browser/bridge-browser/tests/protocol.spec.ts +++ b/packages/browser/bridge-browser/tests/protocol.spec.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest' import { DEVTOOLS_CAPS_VERSION, DEVTOOLS_TOOL_NAMES, - isClientFrame, isDevToolsCaps, isServerFrame, parseBridgeFrame, @@ -122,17 +121,11 @@ describe('parseBridgeFrame', () => { const server = parseBridgeFrame(JSON.stringify({ t: 'tool.call', id: '1', name: 'browser_click', args: {}, expiresAt: 123 }))! const client = parseBridgeFrame(JSON.stringify({ t: 'hello', token: 't', caps: { textOnly: true, snapshotMaxChars: 500, maxInteractiveItems: 10 } }))! expect(isServerFrame(server)).toBe(true) - expect(isClientFrame(server)).toBe(false) expect(isServerFrame(client)).toBe(false) - expect(isClientFrame(client)).toBe(true) for (const t of ['hello.ok', 'rpc.result', 'respond.result', 'event', 'tool.call', 'tool.cancel', 'ping', 'error'] as const) { const frame = parseBridgeFrame(JSON.stringify(serverShape(t)))! expect(isServerFrame(frame)).toBe(true) } - for (const t of ['hello', 'rpc', 'respond', 'tool.result', 'pong'] as const) { - const frame = parseBridgeFrame(JSON.stringify(clientShape(t)))! - expect(isClientFrame(frame)).toBe(true) - } }) it('rejects malformed payloads', () => { @@ -167,14 +160,3 @@ function serverShape(t: 'hello.ok' | 'rpc.result' | 'respond.result' | 'event' | case 'error': return { t, code: 'x', message: 'm' } } } - -/** Minimal valid shape per client-side frame type (for classification tests). */ -function clientShape(t: 'hello' | 'rpc' | 'respond' | 'tool.result' | 'pong'): Record { - switch (t) { - case 'hello': return { t, token: 'x', caps: { textOnly: true, snapshotMaxChars: 500, maxInteractiveItems: 10 } } - case 'rpc': return { t, id: '1', method: 'x', payload: {} } - case 'respond': return { t, id: '1', rpcId: 'q', result: { ok: true, value: {} } } - case 'tool.result': return { t, id: '1', ok: true, result: {} } - case 'pong': return { t } - } -} diff --git a/packages/browser/bridge-browser/tests/server.spec.ts b/packages/browser/bridge-browser/tests/server.spec.ts index 321de6ac1..98e3abe1b 100644 --- a/packages/browser/bridge-browser/tests/server.spec.ts +++ b/packages/browser/bridge-browser/tests/server.spec.ts @@ -2,7 +2,7 @@ import { createServer, type Server } from 'node:http' import type { AddressInfo } from 'node:net' import { afterEach, describe, expect, it, vi } from 'vitest' import WebSocket from 'ws' -import { BridgeServer, BridgeToolError, isLoopbackAddress, messageToText, payloadCode, payloadMessage } from '../src/server.ts' +import { BridgeServer, BridgeToolError, isLoopbackAddress, messageToText } from '../src/server.ts' import { BRIDGE_INJECT_BROWSER_SNAPSHOT_METHOD, BRIDGE_SESSION_PURGE_METHOD, @@ -113,17 +113,6 @@ describe('BridgeServer', () => { expect(messageToText(new TextEncoder().encode('x').buffer)).toBe('x') }) - it('extracts tool error codes and messages with parser-gated fallbacks', () => { - expect(payloadCode({ code: 'timeout', message: 'm' })).toBe('timeout') - expect(payloadCode({ code: 42, message: 'm' })).toBe('internal') - expect(payloadCode('garbage')).toBe('internal') - expect(payloadCode(null)).toBe('internal') - expect(payloadMessage({ code: 'x', message: 'm' })).toBe('m') - expect(payloadMessage({ code: 'x', message: '' })).toBe('browser action failed') - expect(payloadMessage({ code: 'x', message: 42 })).toBe('browser action failed') - expect(payloadMessage('garbage')).toBe('browser action failed') - }) - it('reports negotiated developer-tools support and notifies on each hello', async () => { const onCapsChange = vi.fn() const h = await startBridge({ onCapsChange }) @@ -525,7 +514,10 @@ describe('BridgeServer', () => { ws.close() }) - it('propagates extension-reported tool errors', async () => { + it.each([ + ['blocked', 'blocked'], + ['', 'browser action failed'], + ])('propagates the extension-reported tool error %j', async (message, expectedMessage) => { const h = await startBridge() harnesses.push(h) const { ws, frames } = await connect(h.url) @@ -534,9 +526,9 @@ describe('BridgeServer', () => { const result = h.bridge.requestTool('browser_navigate', { url: 'https://x' }, new AbortController().signal) await waitFor(() => frames.some((f) => f.t === 'tool.call')) const call = frames.find((f) => f.t === 'tool.call') as Extract - send(ws, { t: 'tool.result', id: call.id, ok: false, error: { code: 'action-failed', message: 'blocked' } }) + send(ws, { t: 'tool.result', id: call.id, ok: false, error: { code: 'action-failed', message } }) await expect(result).rejects.toBeInstanceOf(BridgeToolError) - await expect(result).rejects.toMatchObject({ code: 'action-failed', message: 'blocked' }) + await expect(result).rejects.toMatchObject({ code: 'action-failed', message: expectedMessage }) ws.close() }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cd494b81..5c82ba88e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: '@deepseek-ai/dsh-api-gateway': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5) - '@deepseek-ai/dsh-api-remotes': - specifier: 0.2.1-rc.5 - version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)) '@deepseek-ai/dsh-atomic-write': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5) @@ -50,9 +47,6 @@ importers: '@deepseek-ai/dsh-fs': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-brand@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-llm@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-sandbox@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-llm@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))) - '@deepseek-ai/dsh-hook-protocol': - specifier: 0.2.1-rc.5 - version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))(@deepseek-ai/dsh-shell@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-sandbox@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-llm@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))))(@deepseek-ai/dsh-subprocess@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-http-proxy@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))) '@deepseek-ai/dsh-jobs': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(ea009c7b924fabbf2a06dd439d07a23c) @@ -101,12 +95,6 @@ importers: '@deepseek-ai/dsh-subagent': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(829fa3f26582b8b902c27168ccbbe4e2) - '@deepseek-ai/dsh-subagent-fork-in-process': - specifier: 0.2.1-rc.5 - version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-agent@0.2.1-rc.5(cbd9b78489f743dd32cfe135059e56db))(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))(@deepseek-ai/dsh-subagent@0.2.1-rc.5(829fa3f26582b8b902c27168ccbbe4e2)) - '@deepseek-ai/dsh-subagent-spawn-in-process': - specifier: 0.2.1-rc.5 - version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-subagent@0.2.1-rc.5(829fa3f26582b8b902c27168ccbbe4e2)) '@deepseek-ai/dsh-timeout': specifier: 0.2.1-rc.5 version: 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5) @@ -162,9 +150,6 @@ importers: vite: specifier: ^6.0.0 version: 6.4.3(@types/node@22.20.1)(yaml@2.9.1) - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.1)) vitest: specifier: ^4.1.11 version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.1)) @@ -1575,13 +1560,6 @@ packages: peerDependencies: '@deepseek-ai/cordis': ~4.0.5 - '@deepseek-ai/dsh-hook-protocol@0.2.1-rc.5': - resolution: {integrity: sha512-I84YqcYUxA6qyjfhJLsCv8Xel9I9zAxgU/FTYgh3kXL951D7+m0NR6453q1tufUqHzXQfdS+sSmN8WZ9jhB7Wg==} - peerDependencies: - '@deepseek-ai/cordis': ~4.0.5 - '@deepseek-ai/dsh-session': 0.2.1-rc.5 - '@deepseek-ai/dsh-shell': 0.2.1-rc.5 - '@deepseek-ai/dsh-host-directory-picker-auto@0.2.1-rc.5': resolution: {integrity: sha512-fwPycD2sr3H027/aOUrixRcYJu/wEesQ9yvsHRV3q9dztBCD+3cLg/KiW+r5iFzblC2W/DhYa89QSpgfrBAEIA==} peerDependencies: @@ -3838,7 +3816,9 @@ packages: '@smithy/core@3.38.0': resolution: {integrity: sha512-aLyyCnK2p4yvNA2TaM+5owXqb+rZGSiUKEsNP329AzeYnERtAz7NrlTbRsaxqmZ0ooxwoID8LMm959UGQQDIsg==} engines: {node: '>=18.0.0'} - deprecated: Deprecated due to a bug in custom logger data redaction. See https://github.com/aws/aws-sdk-js-v3/issues/8393. + deprecated: |- + Deprecated due to a bug in custom logger data redaction. Upgrade to the latest version. If you used a custom logger with this + version, review your logs and rotate any credentials or other secrets. See https://github.com/aws/aws-sdk-js-v3/issues/8393. '@smithy/credential-provider-imds@4.5.2': resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==} @@ -4380,9 +4360,6 @@ packages: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - google-auth-library@10.9.1: resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} engines: {node: '>=18'} @@ -5069,17 +5046,6 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - deprecated: unmaintained - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tsdown@0.22.2: resolution: {integrity: sha512-VX9gsyKXsTnBZjnIM4jsHl9aRv+GfgkE/k1hQslilaBfZMlaw3JuGR+6yhiU0QxWBtOCDnTjwOSoXzgB7Rr50g==} engines: {node: ^22.18.0 || >=24.0.0} @@ -5171,11 +5137,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - vite@6.4.3: resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -7071,12 +7032,6 @@ snapshots: dependencies: '@deepseek-ai/cordis': 4.0.5(@deepseek-ai/cordis-plugin-include@1.0.10)(@deepseek-ai/cordis-plugin-loader@1.0.6) - '@deepseek-ai/dsh-hook-protocol@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))(@deepseek-ai/dsh-shell@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-sandbox@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-llm@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))))(@deepseek-ai/dsh-subprocess@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-http-proxy@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))))': - dependencies: - '@deepseek-ai/cordis': 4.0.5(@deepseek-ai/cordis-plugin-include@1.0.10)(@deepseek-ai/cordis-plugin-loader@1.0.6) - '@deepseek-ai/dsh-session': 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)) - '@deepseek-ai/dsh-shell': 0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-sandbox@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-llm@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-session@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))))(@deepseek-ai/dsh-subprocess@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-http-proxy@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))) - '@deepseek-ai/dsh-host-directory-picker-auto@0.2.1-rc.5(@deepseek-ai/cordis-plugin-loader@1.0.6)(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-client-connection@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)(@deepseek-ai/dsh-scope@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5)))(@deepseek-ai/dsh-client-ui-directory-picker-browse@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-client-ui-directory-picker-native@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-host-directory-picker-browse@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-host-directory-picker-native@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))(@deepseek-ai/dsh-host-webserver@0.2.1-rc.5(@deepseek-ai/cordis@4.0.5))': dependencies: '@deepseek-ai/cordis': 4.0.5(@deepseek-ai/cordis-plugin-include@1.0.10)(@deepseek-ai/cordis-plugin-loader@1.0.6) @@ -10155,8 +10110,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - globrex@0.1.2: {} - google-auth-library@10.9.1: dependencies: base64-js: 1.5.1 @@ -10850,10 +10803,6 @@ snapshots: ts-algebra@2.0.0: {} - tsconfck@3.1.6(typescript@5.9.3): - optionalDependencies: - typescript: 5.9.3 - tsdown@0.22.2(typescript@6.0.3): dependencies: ansis: 4.3.1 @@ -10924,16 +10873,6 @@ snapshots: vary@1.1.2: {} - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.1)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) - vite: 6.4.3(@types/node@22.20.1)(yaml@2.9.1) - transitivePeerDependencies: - - supports-color - - typescript - vite@6.4.3(@types/node@22.20.1)(yaml@2.9.1): dependencies: esbuild: 0.25.12 diff --git a/scripts/check-runtime.mjs b/scripts/check-runtime.mjs index cb864ee78..4ae61d6f0 100644 --- a/scripts/check-runtime.mjs +++ b/scripts/check-runtime.mjs @@ -1,46 +1,31 @@ import assert from 'node:assert/strict' -import { readFileSync } from 'node:fs' +import { readFileSync, realpathSync } from 'node:fs' import { createRequire } from 'node:module' -import { pathToFileURL } from 'node:url' +import { join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' const root = new URL('../', import.meta.url) const require = createRequire(new URL('package.json', root)) const manifest = JSON.parse(readFileSync(new URL('package.json', root), 'utf8')) const version = manifest.dependencies['@deepseek-ai/dsh'] -const lockfile = readFileSync(new URL('pnpm-lock.yaml', root), 'utf8') -// Inspect package keys, including snapshots, without depending on pnpm's -// private APIs or mistaking version text inside a peer suffix for a package. -const lockedPackages = [...lockfile.matchAll(/^ '(@deepseek-ai\/dsh(?:-[^@']+)?)@([^(' :]+)[^']*':/gm)] +const anchor = require.resolve('@deepseek-ai/dsh/package.json') +const hostRequire = createRequire(anchor) +const { load } = hostRequire('js-yaml') +const lockfile = load(readFileSync(new URL('pnpm-lock.yaml', root), 'utf8')) +const lockedPackages = Object.keys(lockfile.packages) + .map(key => [key.slice(0, key.lastIndexOf('@')), key.slice(key.lastIndexOf('@') + 1)]) + .filter(([name]) => /^@deepseek-ai\/dsh(?:-|$)/.test(name)) assert.ok(lockedPackages.length > 0, 'no DSH package keys found in lockfile') -for (const [, name, resolved] of lockedPackages) { - assert.notEqual(name, '@deepseek-ai/dsh-host-apiproxy', 'legacy ApiProxy must not be installed') +for (const [name, resolved] of lockedPackages) { assert.equal(resolved, version, `${name}: lockfile has a different DSH release`) } -// Follow the same first-provider traversal as dsh-app-boot's profile fallback. -const anchor = require.resolve('@deepseek-ai/dsh/package.json') -const queue = [anchor] -const providers = new Map([['@deepseek-ai/dsh', anchor]]) -for (let i = 0; i < queue.length; i++) { - const parent = queue[i] - const data = JSON.parse(readFileSync(parent, 'utf8')) - const resolve = createRequire(parent) - for (const name of Object.keys({ ...data.dependencies, ...data.peerDependencies })) { - if (providers.has(name)) continue - let path - try { path = resolve.resolve(`${name}/package.json`) } catch (error) { - // Other packages can hide package.json; DSH publishes its manifests. - if (name.startsWith('@deepseek-ai/dsh') && error.code !== 'MODULE_NOT_FOUND') throw error - continue - } - providers.set(name, path) - queue.push(path) - } -} -for (const [name, path] of providers) { +const { createRuntimeResolution } = await import(pathToFileURL(hostRequire.resolve('@deepseek-ai/dsh-app-boot')).href) +const resolution = await createRuntimeResolution({ installAnchor: anchor, home: fileURLToPath(root) }) +const providers = new Map(resolution.entries.map(({ name, packageDir }) => [name, join(packageDir, 'package.json')])) +for (const { name, version: resolved, packageDir } of resolution.entries) { if (!/^@deepseek-ai\/dsh(?:-|$)/.test(name)) continue - const data = JSON.parse(readFileSync(path, 'utf8')) - assert.equal(data.version, version, `${name}: runtime provider at ${path}`) + assert.equal(resolved, version, `${name}: runtime provider at ${packageDir}`) } for (const name of ['dsh-session-query', 'dsh-session-projection-cache']) { const path = providers.get(`@deepseek-ai/${name}`) @@ -48,8 +33,9 @@ for (const name of ['dsh-session-query', 'dsh-session-projection-cache']) { console.log(`${name}@${version}: ${path}`) } const cacheName = '@deepseek-ai/dsh-session-projection-cache' -assert.equal(require.resolve(`${cacheName}/package.json`), providers.get(cacheName), 'root and host must resolve the same cache') -const entry = createRequire(providers.get(cacheName)).resolve(cacheName) +const cacheManifest = realpathSync(providers.get(cacheName)) +assert.equal(require.resolve(`${cacheName}/package.json`), cacheManifest, 'root and host must resolve the same cache') +const entry = createRequire(cacheManifest).resolve(cacheName) const { default: Cache } = await import(pathToFileURL(entry).href) assert.equal(typeof Cache.prototype.hydratePrepared, 'function', `${entry}: hydratePrepared missing`) console.log('DSH runtime dependency checks passed') diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 06874ef3b..526143924 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -455,14 +455,9 @@ New-Item -ItemType Directory -Path $DistDir -Force | Out-Null Sync-Directory -Source (Join-Path $Ext 'dist') -Destination $DistDir $InstallMode = if (Test-Path -LiteralPath (Join-Path $Root '.managed-by-install-sh') -PathType Leaf) { 'managed' } else { 'checkout' } -$InstallInfo = [ordered]@{ schemaVersion = 1; mode = $InstallMode } -if ($InstallMode -eq 'checkout') { $InstallInfo['sourceRoot'] = $Root } -# ConvertTo-Json escapes the backslashes in a Windows path; hand-built JSON would not. -$InstallInfoJson = ($InstallInfo | ConvertTo-Json -Depth 3) + "`n" -[System.IO.File]::WriteAllText( - (Join-Path $DistDir 'install-info.json'), - $InstallInfoJson, - (New-Object System.Text.UTF8Encoding $false)) +& node (Join-Path $Root 'scripts\write-install-info.mjs') ` + (Join-Path $DistDir 'install-info.json') $InstallMode $Root $DshHomeDir +if ($LASTEXITCODE -ne 0) { Stop-Install '写入安装信息失败。' 'Writing install metadata failed.' } $ClipboardReady = Set-ClipboardText $DistDir $ChromeOpened = Start-ExtensionsPage (Get-BrowserPath) diff --git a/scripts/install.sh b/scripts/install.sh index 4c2e716a3..cb841df07 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -346,16 +346,8 @@ if [ -f "$ROOT/.managed-by-install-sh" ]; then else INSTALL_MODE="checkout" fi -node -e ' - const { writeFileSync } = require("node:fs"); - const [filename, mode, sourceRoot] = process.argv.slice(1); - const info = { - schemaVersion: 1, - mode, - ...(mode === "checkout" ? { sourceRoot } : {}), - }; - writeFileSync(filename, `${JSON.stringify(info, null, 2)}\n`); -' "$DIST_DIR/install-info.json" "$INSTALL_MODE" "$ROOT" +node "$ROOT/scripts/write-install-info.mjs" \ + "$DIST_DIR/install-info.json" "$INSTALL_MODE" "$ROOT" "$DSH_HOME_DIR" if copy_to_clipboard "$DIST_DIR"; then CLIPBOARD_READY=1 else diff --git a/scripts/smoke-uninstall-ui.mjs b/scripts/smoke-uninstall-ui.mjs new file mode 100644 index 000000000..fa7469614 --- /dev/null +++ b/scripts/smoke-uninstall-ui.mjs @@ -0,0 +1,111 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { cp, mkdir, mkdtemp, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = fileURLToPath(new URL('../', import.meta.url)) +const require = createRequire(join(root, 'packages', 'browser', 'bridge-browser', 'package.json')) +const { chromium } = require('playwright-core') +const artifacts = join(root, '.artifacts', 'uninstall-ui') +await mkdir(artifacts, { recursive: true }) +const directory = await mkdtemp(join(artifacts, 'run-')) +const runtimeTmp = join(directory, 'runtime') +await mkdir(runtimeTmp) +const results = [] + +for (const mode of ['managed', 'checkout', 'unknown']) { + const home = join(directory, mode, "DSH 中文 & user's home") + const extension = join(home, 'browser-extension') + await mkdir(home, { recursive: true }) + await cp(join(root, 'extensions', 'dsh-browser', 'dist'), extension, { recursive: true }) + if (mode !== 'unknown') { + const source = mode === 'managed' ? join(home, 'dsh-browser') : join(directory, mode, 'source checkout 中文') + await mkdir(join(source, 'scripts'), { recursive: true }) + for (const name of ['uninstall.sh', 'uninstall.ps1', 'uninstall.mjs']) { + await cp(join(root, 'scripts', name), join(source, 'scripts', name)) + } + if (mode === 'managed') await writeFile(join(source, '.managed-by-install-sh'), '') + const result = spawnSync(process.execPath, [join(root, 'scripts', 'write-install-info.mjs'), + join(extension, 'install-info.json'), mode, source, home], { encoding: 'utf8' }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stderr) + } + const launch = () => chromium.launchPersistentContext(join(directory, mode, 'chromium'), { + executablePath: process.env.CHROMIUM_PATH ?? chromium.executablePath(), + channel: 'chromium', headless: true, + env: { ...process.env, TMPDIR: runtimeTmp, TMP: runtimeTmp, TEMP: runtimeTmp }, + args: [`--disable-extensions-except=${extension}`, `--load-extension=${extension}`], + }) + let browser = await launch() + try { + let worker = browser.serviceWorkers()[0] ?? await browser.waitForEvent('serviceworker') + const id = new URL(worker.url()).host + // 验证服务离线时仍可操作卸载入口。 + await worker.evaluate(async () => { + await chrome.storage.local.set({ + dshSettings: { bridgeUrl: 'ws://uninstall.invalid:43189/ext/bridge', autoResumeSession: false }, + dshPanelUiLocale: 'en', + }) + }) + await browser.close() + browser = await launch() + worker = browser.serviceWorkers()[0] ?? await browser.waitForEvent('serviceworker') + const page = await browser.newPage() + for (const locale of ['en', 'zh']) { + await worker.evaluate(async locale => { await chrome.storage.local.set({ dshPanelUiLocale: locale }) }, locale) + await page.goto(`chrome-extension://${id}/panel/index.html`) + await page.getByRole('button', { name: locale === 'en' ? 'Open settings' : '打开设置', exact: true }).click() + const card = page.locator('.uninstall-card') + await card.locator('summary').click() + await card.locator('.uninstall-command').waitFor() + const command = await card.locator('.uninstall-command').innerText() + if (mode !== 'unknown') { + assert.ok(command.includes('--home')) + assert.ok(command.includes(process.platform === 'win32' ? "''" : "'\"'\"'")) + const options = { encoding: 'utf8', env: process.env, timeout: 30_000 } + const preview = process.platform === 'win32' + ? spawnSync('powershell.exe', ['-NoProfile', '-Command', `${command} --dry-run`], options) + : spawnSync('bash', ['-c', `${command} --dry-run`], options) + if (preview.error) throw preview.error + assert.equal(preview.status, 0, preview.stdout + preview.stderr) + assert.ok(preview.stdout.includes(home)) + } else { + assert.ok(command.endsWith('| bash') || command.includes('-File $s } finally {')) + } + await page.bringToFront() + await card.getByRole('button', { name: locale === 'en' ? 'Copy uninstall command' : '复制卸载命令', exact: true }).click() + await card.getByRole('button', { name: locale === 'en' ? 'Command copied' : '命令已复制', exact: true }).waitFor() + // 在失焦的子页面中触发真实剪贴板拒绝,再验证重新复制。 + const navigation = page.waitForEvent('framenavigated', frame => frame.name() === 'uninstall-copy-check') + await page.evaluate(() => { + const frame = document.createElement('iframe') + frame.name = 'uninstall-copy-check' + frame.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;z-index:999999' + frame.src = location.href + document.body.appendChild(frame) + }) + const frame = await navigation + await frame.getByRole('button', { name: locale === 'en' ? 'Open settings' : '打开设置', exact: true }).click() + const copyCard = frame.locator('.uninstall-card') + await copyCard.locator('summary').click() + await copyCard.locator('.uninstall-command').waitFor() + await card.locator('.uninstall-command').focus() + assert.equal(await frame.evaluate(() => document.hasFocus()), false) + await copyCard.locator('button').evaluate(button => button.click()) + const copyError = locale === 'en' ? 'Copy failed' : '复制失败' + await copyCard.getByRole('status').filter({ hasText: copyError }).waitFor() + assert.equal(await copyCard.getByRole('status').innerText(), copyError) + await copyCard.getByRole('button', { name: locale === 'en' ? 'Copy uninstall command' : '复制卸载命令', exact: true }).click() + await copyCard.getByRole('button', { name: locale === 'en' ? 'Command copied' : '命令已复制', exact: true }).waitFor() + await page.locator('iframe[name="uninstall-copy-check"]').evaluate(frame => frame.remove()) + results.push({ mode, locale, command, copied: true, copyFailureReported: true, retried: true }) + } + } finally { + await browser.close() + } +} + +await writeFile(join(directory, 'results.json'), JSON.stringify(results, null, 2) + '\n') +console.log(`Uninstall UI passed: ${results.length} cases. Evidence: ${directory}`) diff --git a/scripts/tests/uninstall-bootstrap.test.mjs b/scripts/tests/uninstall-bootstrap.test.mjs new file mode 100644 index 000000000..dfc75ff9b --- /dev/null +++ b/scripts/tests/uninstall-bootstrap.test.mjs @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import test from 'node:test' +import { uninstallCommand } from '../../extensions/dsh-browser/src/panel/uninstall.ts' +import { powerShellQuote } from '../../extensions/dsh-browser/src/panel/updates.ts' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const artifacts = join(root, '.artifacts', 'uninstall-bootstrap') +mkdirSync(artifacts, { recursive: true }) +const run = mkdtempSync(join(artifacts, 'run-')) +const proxy = 'http://127.0.0.1:1' + +function workspace(name) { + const base = join(run, name) + const cwd = join(base, 'working') + const temporary = join(base, 'temporary') + mkdirSync(cwd, { recursive: true }) + mkdirSync(temporary) + if (process.platform !== 'win32') chmodSync(cwd, 0o555) + return { + base, cwd, temporary, + env: { ...process.env, TMPDIR: temporary, TMP: temporary, TEMP: temporary, + HTTPS_PROXY: proxy, https_proxy: proxy, HTTP_PROXY: proxy, http_proxy: proxy, NO_PROXY: '', no_proxy: '' }, + } +} + +function cleaned(fixture) { + assert.deepEqual(readdirSync(fixture.cwd), []) + assert.deepEqual(readdirSync(fixture.temporary).filter(name => name.startsWith('dsh-browser-uninstall.')), []) +} + +test('streamed Bash preserves download failures and removes its temporary directory', { + skip: process.platform === 'win32', +}, () => { + const fixture = workspace('bash') + try { + const result = spawnSync('bash', ['-s', '--', '--dry-run'], { + cwd: fixture.cwd, env: fixture.env, encoding: 'utf8', timeout: 30_000, + input: readFileSync(join(root, 'scripts', 'uninstall.sh'), 'utf8'), + }) + if (result.error) throw result.error + assert.equal(result.status, 7, result.stdout + result.stderr) + assert.doesNotMatch(result.stderr, /unbound variable/) + cleaned(fixture) + } finally { + chmodSync(fixture.cwd, 0o755) + } +}) + +const powershells = [ + ...(process.platform === 'win32' ? ['powershell.exe'] : []), + ...(process.env.TEST_PWSH === '1' ? ['pwsh'] : []), +] + +for (const shell of powershells) { + for (const entry of ['script', 'command']) { + test(`${shell} ${entry} cleans up a failed download outside the working directory`, () => { + const fixture = workspace(`${shell}-${entry}`) + const script = join(fixture.base, 'uninstall.ps1') + cpSync(join(root, 'scripts', 'uninstall.ps1'), script) + const command = entry === 'script' ? `& ${powerShellQuote(script)} --dry-run` + : uninstallCommand({ mode: 'unknown' }, 'Windows') + // 设置真实代理连接失败,覆盖下载中断后的清理。 + const source = `$ErrorActionPreference = 'Stop'; [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy('${proxy}'); ${command}` + try { + const result = spawnSync(shell, ['-NoProfile', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')], { + cwd: fixture.cwd, env: fixture.env, encoding: 'utf8', timeout: 30_000, + }) + if (result.error) throw result.error + assert.equal(result.status, 1, result.stdout + result.stderr) + assert.match(result.stderr, /Invoke-WebRequest/) + cleaned(fixture) + } finally { + if (process.platform !== 'win32') chmodSync(fixture.cwd, 0o755) + } + }) + } +} + +console.log(`Bootstrap evidence: ${run}`) diff --git a/scripts/tests/uninstall-command.test.mjs b/scripts/tests/uninstall-command.test.mjs new file mode 100644 index 000000000..050bd1c08 --- /dev/null +++ b/scripts/tests/uninstall-command.test.mjs @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { mkdirSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import test from 'node:test' +import { powerShellQuote } from '../../extensions/dsh-browser/src/panel/updates.ts' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const temporary = join(root, '.artifacts', 'uninstall-command') +mkdirSync(temporary, { recursive: true }) +const shells = [ + ...(process.platform === 'win32' ? ['powershell.exe'] : []), + ...(process.env.TEST_PWSH === '1' ? ['pwsh'] : []), +] + +for (const shell of shells) { + test(`${shell} preserves paths containing every single-quote delimiter`, () => { + const paths = ["'", '\u2018', '\u2019', '\u201a', '\u201b'] + .map(quote => `C:\\DSH 中文\\O${quote}Brien & $user`) + const source = `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false; $values = @(${paths.map(powerShellQuote).join(', ')}); ConvertTo-Json -Compress -InputObject $values` + const result = spawnSync(shell, ['-NoProfile', '-EncodedCommand', Buffer.from(source, 'utf16le').toString('base64')], { + cwd: root, encoding: 'utf8', timeout: 15_000, + env: { ...process.env, TMPDIR: temporary, TMP: temporary, TEMP: temporary }, + }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.deepEqual(JSON.parse(result.stdout), paths) + }) +} diff --git a/scripts/tests/uninstall.test.mjs b/scripts/tests/uninstall.test.mjs new file mode 100644 index 000000000..9d3218799 --- /dev/null +++ b/scripts/tests/uninstall.test.mjs @@ -0,0 +1,376 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { + cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync, +} from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import test from 'node:test' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const artifacts = join(root, '.artifacts', 'uninstall-tests') +mkdirSync(artifacts, { recursive: true }) +const run = mkdtempSync(join(artifacts, 'run-')) +const anchor = realpathSync(join(root, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')) +const require = createRequire(anchor) +const { initProfile } = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-app-boot')).href) +const { runPluginCommand } = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-plugin-manager/operations')).href) +const current = '@yuxianglin/dsh-bridge-browser' +const legacy = '@deepseek-ai/dsh-bridge-browser' +const other = '@deepseek-ai/dsh-experimental-terminal-bundle' + +function writeJson(path, value) { + writeFileSync(path, JSON.stringify(value, null, 2) + '\n') +} + +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')) +} + +async function addBridge(fixture, profileName, name, source = fixture.bridge) { + const dir = join(fixture.home, 'profiles', profileName) + initProfile(dir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + const result = await runPluginCommand({ profile: profileName, dir, installAnchor: anchor, cwd: dir }, + ['add', '-w', `${name}@link:${source}`], { + execution: 'service', outputBytes: 16384, env: { DSH_HOME: fixture.home }, + }) + assert.equal(result.exitCode, 0, result.output) +} + +async function installation(name, { mode = 'checkout', bridges = [current], oldMetadata = false, sourceInHome = false } = {}) { + const base = join(run, name) + const home = join(base, 'DSH 空间 & user') + const source = mode === 'managed' || sourceInHome ? join(home, 'dsh-browser') : join(base, "source checkout 中文 ' &") + const extension = join(home, 'browser-extension') + const profile = join(home, 'profiles', 'web') + mkdirSync(source, { recursive: true }) + cpSync(join(root, 'package.json'), join(source, 'package.json')) + if (mode === 'managed') writeFileSync(join(source, '.managed-by-install-sh'), '') + cpSync(join(root, 'scripts'), join(source, 'scripts'), { recursive: true }) + const bridge = join(source, 'packages', 'browser', 'bridge-browser') + cpSync(join(root, 'packages', 'browser', 'bridge-browser'), bridge, { + recursive: true, filter: path => !path.split(/[\\/]/u).includes('node_modules'), + }) + symlinkSync(join(root, 'node_modules'), join(source, 'node_modules'), 'junction') + cpSync(join(root, 'extensions', 'dsh-browser', 'dist'), extension, { recursive: true }) + const info = { schemaVersion: 1, mode, ...(mode === 'checkout' ? { sourceRoot: source } : {}) } + if (oldMetadata) writeJson(join(extension, 'install-info.json'), info) + else { + const result = spawnSync(process.execPath, [join(root, 'scripts', 'write-install-info.mjs'), + join(extension, 'install-info.json'), mode, source, home], { encoding: 'utf8' }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stderr) + assert.equal(readJson(join(extension, 'install-info.json')).dshHome, realpathSync(home)) + } + mkdirSync(join(home, 'sessions'), { recursive: true }) + writeFileSync(join(home, 'sessions', 'preserved.txt'), 'session data to preserve\n') + writeJson(join(home, 'settings.json'), { account: 'preserved account settings' }) + writeFileSync(join(home, 'ext-bridge-token'), 'test-bridge-token\n') + initProfile(profile, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', other]) + const fixture = { base, home, source, extension, profile, bridge } + for (const name of bridges) await addBridge(fixture, 'web', name) + return fixture +} + +function uninstall(fixture, args = [], { entry = 'node', env = {}, fromSource = false } = {}) { + const options = { + cwd: fromSource ? fixture.source : root, + encoding: 'utf8', env: { ...process.env, DSH_HOME: fixture.home, ...env }, timeout: 120_000, + } + const script = join(fromSource ? fixture.source : root, 'scripts', `uninstall.${entry === 'node' ? 'mjs' : entry === 'bash' ? 'sh' : 'ps1'}`) + const result = entry === 'node' + ? spawnSync(process.execPath, [script, ...args], options) + : entry === 'bash' + ? spawnSync('bash', [script, ...args], options) + : spawnSync(entry, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, ...args], options) + if (result.error) throw result.error + writeFileSync(join(fixture.base, `uninstall-${entry}.log`), result.stdout + result.stderr) + return result +} + +function preserved(fixture) { + assert.equal(readFileSync(join(fixture.home, 'sessions', 'preserved.txt'), 'utf8'), 'session data to preserve\n') + assert.deepEqual(readJson(join(fixture.home, 'settings.json')), { account: 'preserved account settings' }) + const manifest = readJson(join(fixture.profile, 'package.json')) + assert.deepEqual(manifest.dsh.profile.bundles, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', other]) + assert.equal(existsSync(join(fixture.profile, 'cordis.patch.yml')), true) +} + +test('managed installation: native removal, owned files, preserved data and repeat execution', async () => { + const fixture = await installation('managed', { mode: 'managed' }) + await addBridge(fixture, 'web', other, dirname(require.resolve(`${other}/package.json`))) + const otherDependency = readJson(join(fixture.profile, 'package.json')).dependencies[other] + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + const manifest = readJson(join(fixture.profile, 'package.json')) + assert.equal(manifest.dependencies?.[current], undefined) + assert.equal(existsSync(join(fixture.profile, 'node_modules', current)), false) + assert.equal(manifest.dependencies[other], otherDependency) + assert.equal(existsSync(join(fixture.profile, 'node_modules', other, 'package.json')), true) + assert.equal(existsSync(fixture.source), false) + assert.equal(existsSync(fixture.extension), false) + assert.equal(existsSync(join(fixture.home, 'ext-bridge-token')), false) + preserved(fixture) + const repeat = uninstall(fixture) + assert.equal(repeat.status, 0, repeat.stderr) + preserved(fixture) +}) + +test('checkout and junction: current and legacy names removed, target source retained', async () => { + const fixture = await installation('checkout', { bridges: [current, legacy], oldMetadata: true }) + symlinkSync(fixture.bridge, join(fixture.profile, '.dsh-browser-source'), 'junction') + const result = uninstall(fixture, ['--home', fixture.home], { env: { DSH_HOME: join(fixture.base, 'unused home') } }) + assert.equal(result.status, 0, result.stdout + result.stderr) + const manifest = readJson(join(fixture.profile, 'package.json')) + assert.equal(manifest.dependencies?.[current], undefined) + assert.equal(manifest.dependencies?.[legacy], undefined) + assert.equal(existsSync(join(fixture.source, 'scripts', 'install.sh')), true) + assert.equal(existsSync(join(fixture.bridge, 'package.json')), true) + assert.equal(existsSync(join(fixture.profile, '.dsh-browser-source')), false) + preserved(fixture) +}) + +test('other profiles retain their bridge, shared workspace and token', async () => { + const fixture = await installation('shared', { mode: 'managed' }) + await addBridge(fixture, 'another-profile', current) + const otherManifest = readFileSync(join(fixture.home, 'profiles', 'another-profile', 'package.json'), 'utf8') + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.source), true) + assert.equal(readFileSync(join(fixture.home, 'ext-bridge-token'), 'utf8'), 'test-bridge-token\n') + assert.equal(readFileSync(join(fixture.home, 'profiles', 'another-profile', 'package.json'), 'utf8'), otherManifest) + preserved(fixture) +}) + +test('linked profiles retain the shared workspace and token', async () => { + const fixture = await installation('linked-profile', { mode: 'managed' }) + await addBridge(fixture, 'another-profile', current) + const profile = join(fixture.home, 'profiles', 'another-profile') + const external = join(fixture.base, 'external-profile') + renameSync(profile, external) + symlinkSync(external, profile, 'junction') + const before = readFileSync(join(external, 'package.json'), 'utf8') + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.source), true) + assert.equal(readFileSync(join(fixture.home, 'ext-bridge-token'), 'utf8'), 'test-bridge-token\n') + assert.equal(readFileSync(join(external, 'package.json'), 'utf8'), before) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('other profiles retain the shared source junction and their package resolution', async () => { + const fixture = await installation('shared-junction', { mode: 'managed' }) + const link = join(fixture.profile, '.dsh-browser-source') + symlinkSync(fixture.bridge, link, 'junction') + await addBridge(fixture, 'another-profile', current, '../web/.dsh-browser-source') + const profile = join(fixture.home, 'profiles', 'another-profile') + const profileManifest = readFileSync(join(profile, 'package.json'), 'utf8') + const packageManifest = join(profile, 'node_modules', current, 'package.json') + const before = readFileSync(packageManifest, 'utf8') + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(link), true) + assert.equal(readFileSync(packageManifest, 'utf8'), before) + assert.equal(readFileSync(join(profile, 'package.json'), 'utf8'), profileManifest) + preserved(fixture) +}) + +test('checkout stored at the managed path is preserved as a checkout', async () => { + const fixture = await installation('checkout-in-home', { sourceInHome: true }) + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(join(fixture.source, 'scripts', 'install.sh')), true) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('metadata for a different home stops before any removal', async () => { + const fixture = await installation('wrong-home') + const path = join(fixture.extension, 'install-info.json') + const info = readJson(path) + info.dshHome = join(fixture.base, 'another home') + writeJson(path, info) + const result = uninstall(fixture) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Install metadata names a different home/) + assert.ok(readJson(join(fixture.profile, 'package.json')).dependencies[current]) + assert.equal(existsSync(fixture.extension), true) +}) + +test('the uninstall command completes without terminal prompts', async () => { + const fixture = await installation('direct-command') + const result = spawnSync(process.execPath, [join(root, 'scripts', 'uninstall.mjs'), '--home', fixture.home], { + encoding: 'utf8', env: process.env, timeout: 30_000, + }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('dry run leaves profile and installed files intact', async () => { + const fixture = await installation('dry-run') + const before = readFileSync(join(fixture.profile, 'package.json'), 'utf8') + const result = uninstall(fixture, ['--dry-run']) + assert.equal(result.status, 0, result.stderr) + assert.equal(readFileSync(join(fixture.profile, 'package.json'), 'utf8'), before) + assert.equal(existsSync(fixture.extension), true) +}) + +test('native pnpm failure stops before deleting any installation files', async () => { + const fixture = await installation('pnpm-failure', { mode: 'managed' }) + writeFileSync(join(fixture.profile, 'pnpm-workspace.yaml'), 'packages: [\n') + const before = readFileSync(join(fixture.profile, 'package.json'), 'utf8') + const result = uninstall(fixture) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Plugin removal failed; file cleanup stopped/) + assert.equal(readFileSync(join(fixture.profile, 'package.json'), 'utf8'), before) + assert.equal(existsSync(fixture.source), true) + assert.equal(existsSync(fixture.extension), true) + assert.equal(existsSync(join(fixture.home, 'ext-bridge-token')), true) +}) + +test('unowned directory is retained while the current installation is removed', async () => { + const fixture = await installation('unowned', { mode: 'managed' }) + rmSync(join(fixture.source, '.managed-by-install-sh')) + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.source), true) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('extension directory symlink is refused without deleting its target', async () => { + const fixture = await installation('symlink') + rmSync(fixture.extension, { recursive: true }) + symlinkSync(fixture.source, fixture.extension, 'junction') + const result = uninstall(fixture) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Unexpected path type/) + assert.equal(existsSync(join(fixture.source, 'scripts', 'install.sh')), true) + assert.ok(readJson(join(fixture.profile, 'package.json')).dependencies[current]) +}) + +test('profile lock contention stops before changes', async () => { + const fixture = await installation('locked') + writeFileSync(join(fixture.profile, 'package.json.lock'), `${process.pid}\n`) + const result = uninstall(fixture) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /timed out waiting for the writer lock/) + assert.equal(existsSync(fixture.extension), true) + assert.ok(readJson(join(fixture.profile, 'package.json')).dependencies[current]) +}) + +test('bundle-only interrupted removal is reconciled with the native manifest writer', async () => { + const fixture = await installation('bundle-only', { bridges: [] }) + const path = join(fixture.profile, 'package.json') + const manifest = readJson(path) + manifest.dsh.profile.bundles.push(current, legacy) + writeJson(path, manifest) + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + preserved(fixture) +}) + +test('broken source junction is removed without following its missing target', async () => { + const fixture = await installation('broken-junction', { mode: 'managed' }) + const link = join(fixture.profile, '.dsh-browser-source') + symlinkSync(fixture.bridge, link, 'junction') + rmSync(fixture.bridge, { recursive: true }) + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.source), false) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('missing extension metadata uses verified extension identity', async () => { + const fixture = await installation('no-metadata', { mode: 'managed' }) + rmSync(join(fixture.extension, 'install-info.json')) + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('recorded installation is removable when extension resources are damaged', async () => { + const fixture = await installation('damaged-extension', { mode: 'managed' }) + writeFileSync(join(fixture.extension, 'manifest.json'), '{') + writeFileSync(join(fixture.extension, '_locales', 'en', 'messages.json'), '{') + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.extension), false) + assert.equal(existsSync(fixture.source), false) + preserved(fixture) +}) + +test('source junction removal leaves its target contents untouched', async () => { + const fixture = await installation('junction-target') + const target = join(fixture.base, 'external-source') + mkdirSync(target) + writeFileSync(join(target, 'package.json'), '{') + const link = join(fixture.profile, '.dsh-browser-source') + symlinkSync(target, link, 'junction') + const result = uninstall(fixture) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(link), false) + assert.equal(readFileSync(join(target, 'package.json'), 'utf8'), '{') + preserved(fixture) +}) + +test('standalone helper discovers an old checkout through the installed bridge', async () => { + const fixture = await installation('standalone-old-checkout') + rmSync(join(fixture.extension, 'install-info.json')) + const helper = join(fixture.base, 'uninstall.mjs') + cpSync(join(root, 'scripts', 'uninstall.mjs'), helper) + const result = spawnSync(process.execPath, [helper, '--home', fixture.home], { + encoding: 'utf8', env: process.env, timeout: 120_000, + }) + if (result.error) throw result.error + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.source), true) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('an explicit runtime removes the bridge when the linked checkout manifest is damaged', async () => { + const fixture = await installation('explicit-runtime') + writeFileSync(join(fixture.source, 'package.json'), '{') + const result = uninstall(fixture, ['--runtime-root', root]) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(readFileSync(join(fixture.source, 'package.json'), 'utf8'), '{') + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('incomplete bootstrap with no profile needs no DSH runtime', () => { + const base = join(run, 'incomplete') + const home = join(base, 'home') + mkdirSync(join(home, 'dsh-browser'), { recursive: true }) + writeFileSync(join(home, 'dsh-browser', '.managed-by-install-sh'), '') + const result = uninstall({ base, home }) + assert.equal(result.status, 0, result.stderr) + assert.equal(existsSync(join(home, 'dsh-browser')), false) + assert.equal(existsSync(join(home, 'profiles')), false) +}) + +test('platform entry script forwards arguments and runs the actual uninstaller', async () => { + const fixture = await installation('entry', { mode: 'managed' }) + const result = uninstall(fixture, ['--home', fixture.home], { + entry: process.platform === 'win32' ? 'powershell.exe' : 'bash', fromSource: true, + }) + assert.equal(result.status, 0, result.stdout + result.stderr) + assert.equal(existsSync(fixture.extension), false) + preserved(fixture) +}) + +test('PowerShell 7 entry uses the same cleanup when requested', { skip: process.env.TEST_PWSH !== '1' }, async () => { + const fixture = await installation('pwsh-entry', { mode: 'managed' }) + const result = uninstall(fixture, ['--home', fixture.home], { entry: 'pwsh', fromSource: true }) + assert.equal(result.status, 0, result.stdout + result.stderr) + preserved(fixture) +}) + +console.log(`Uninstall evidence: ${run}`) diff --git a/scripts/uninstall.mjs b/scripts/uninstall.mjs new file mode 100644 index 000000000..fad1acd29 --- /dev/null +++ b/scripts/uninstall.mjs @@ -0,0 +1,231 @@ +import { createRequire } from 'node:module' +import { + lstatSync, readFileSync, readdirSync, realpathSync, rmSync, +} from 'node:fs' +import { homedir } from 'node:os' +import { dirname, isAbsolute, join, relative, resolve, sep, toNamespacedPath } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { parseArgs } from 'node:util' + +const BRIDGES = ['@yuxianglin/dsh-bridge-browser', '@deepseek-ai/dsh-bridge-browser'] +const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'] +const scriptRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +function fail(zh, en) { + throw new Error(`${zh}\n${en}`) +} + +function pair(zh, en) { + console.log(`${zh}\n ${en}`) +} + +function stat(path) { + return lstatSync(path, { throwIfNoEntry: false }) +} + +function regular(path, kind) { + const item = stat(path) + if (item && (item.isSymbolicLink() || !(kind === 'directory' ? item.isDirectory() : item.isFile()))) { + fail(`路径类型不符合预期:${path}`, `Unexpected path type: ${path}`) + } + return item +} + +function json(path) { + if (!stat(path)) return undefined + const value = JSON.parse(readFileSync(path, 'utf8')) + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + fail(`JSON 文件必须包含对象:${path}`, `JSON file must contain an object: ${path}`) + } + return value +} + +function absolute(path) { + if (typeof path !== 'string' || !isAbsolute(path) || /[\u0000-\u001f]/u.test(path)) { + fail(`请指定完整的绝对路径:${path}`, `Specify an absolute path: ${path}`) + } + return resolve(path) +} + +function contains(parent, child) { + const part = relative(toNamespacedPath(parent), toNamespacedPath(child)) + return part === '' || (!part.startsWith(`..${sep}`) && part !== '..' && !isAbsolute(part)) +} + +function samePath(left, right) { + return relative(toNamespacedPath(left), toNamespacedPath(right)) === '' +} + +function dependencies(manifest) { + return Object.assign({}, ...DEPENDENCY_FIELDS.map(field => manifest?.[field])) +} + +function references(manifest) { + const deps = dependencies(manifest) + return BRIDGES.filter(name => Object.hasOwn(deps, name) || manifest?.dsh?.profile?.bundles?.includes(name)) +} + +function profiles(home) { + const root = join(home, 'profiles') + if (!stat(root)) return [] + return readdirSync(root, { withFileTypes: true }).flatMap(entry => { + if (!entry.isDirectory() && !entry.isSymbolicLink()) return [] + const name = entry.name + const dir = join(root, name) + const manifest = json(join(dir, 'package.json')) + return manifest ? [{ name, dir, manifest, references: references(manifest) }] : [] + }) +} + +function inspect(home) { + const extension = join(home, 'browser-extension') + const managed = join(home, 'dsh-browser') + const sourceLink = join(home, 'profiles', 'web', '.dsh-browser-source') + const token = join(home, 'ext-bridge-token') + const installedProfiles = profiles(home) + const web = installedProfiles.find(profile => profile.name === 'web') + const otherProfiles = installedProfiles.filter(profile => profile.name !== 'web' && profile.references.length > 0) + const extensionStat = regular(extension, 'directory') + const info = extensionStat ? json(join(extension, 'install-info.json')) : undefined + if (info && (info.schemaVersion !== 1 || !['managed', 'checkout'].includes(info.mode))) { + fail(`安装信息无法识别:${extension}`, `Unrecognized install metadata: ${extension}`) + } + if (info?.mode === 'checkout') absolute(info.sourceRoot) + if (info?.dshHome !== undefined && !samePath(absolute(info.dshHome), home)) { + fail(`安装记录指向其他目录:${info.dshHome}`, `Install metadata names a different home: ${info.dshHome}`) + } + if (extensionStat && !(info?.application === 'dsh-browser' && info.dshHome !== undefined)) { + const manifest = json(join(extension, 'manifest.json')) + const messages = json(join(extension, '_locales', 'en', 'messages.json')) + if (!(manifest?.name === '__MSG_extensionName__' && messages?.extensionName?.message === 'dsh Browser Assistant')) { + fail(`无法确认扩展目录的归属:${extension}`, `Cannot establish ownership of extension directory: ${extension}`) + } + } + const managedStat = stat(managed) + const ownedManaged = managedStat?.isDirectory() + && stat(join(managed, '.managed-by-install-sh'))?.isFile() + && !stat(join(managed, '.git')) + && !(info?.mode === 'checkout' && contains(managed, info.sourceRoot)) + const linkStat = stat(sourceLink) + if (linkStat && !linkStat.isSymbolicLink()) { + fail(`安装联接路径已被其他内容占用:${sourceLink}`, `The installation junction path contains other data: ${sourceLink}`) + } + regular(token, 'file') + // 其他 profile 仍引用桥插件时,保留共享联接、源码与 token。 + const targets = [extension] + if (otherProfiles.length === 0) { + targets.push(sourceLink, token) + if (ownedManaged) targets.push(managed) + } + return { + home, managed, info, web, targets, + } +} + +async function loadRuntime(plan, runtimeRoot) { + if (!plan.web?.references.length) return undefined + const linkedRoots = runtimeRoot ? [] : plan.web.references.flatMap(name => { + const manifest = join(plan.web.dir, 'node_modules', name, 'package.json') + if (!stat(manifest)) return [] + const root = resolve(dirname(realpathSync(manifest)), '../../..') + return json(join(root, 'package.json'))?.name === 'dsh-browser' ? [root] : [] + }) + const roots = runtimeRoot ? [absolute(runtimeRoot)] + : [plan.info?.sourceRoot, plan.managed, ...linkedRoots, scriptRoot].filter(Boolean) + const root = roots.find(candidate => stat(join(candidate, 'node_modules', '@deepseek-ai', 'dsh', 'package.json'))) + if (!root) { + fail('未找到 DSH 运行时。请使用 --runtime-root 指定已安装依赖的源码目录。', + 'DSH runtime not found. Pass --runtime-root with a checkout whose dependencies are installed.') + } + const anchor = realpathSync(join(root, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')) + const require = createRequire(anchor) + const operations = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-plugin-manager/operations')).href) + const atomic = await import(pathToFileURL(require.resolve('@deepseek-ai/dsh-atomic-write')).href) + return { ...operations, ...atomic, anchor } +} + +function describe(plan) { + pair(`卸载目标:${plan.home}`, `Uninstall target: ${plan.home}`) + pair(`移除 web profile 桥插件:${plan.web?.references.join(', ') || '已移除'}`, + `Remove web profile bridges: ${plan.web?.references.join(', ') || 'already removed'}`) + for (const path of plan.targets) { + if (stat(path)) pair(`删除:${path}`, `Delete: ${path}`) + } +} + +async function removeBridge(plan, runtime) { + const dir = plan.web.dir + const manifest = json(join(dir, 'package.json')) + const names = BRIDGES.filter(name => Object.hasOwn(dependencies(manifest), name)) + if (names.length) { + const result = await runtime.runProfilePnpm({ + profile: 'web', dir, home: plan.home, installAnchor: runtime.anchor, cwd: dir, + }, ['remove', ...names, '-w'], { + execution: 'cli', outputBytes: 16384, env: { DSH_HOME: plan.home }, + onOutput: (text, stream) => process[stream].write(text), + }) + if (result.exitCode !== 0) fail(`移除插件失败,停止清理文件。诊断文件:${result.logPath}`, + `Plugin removal failed; file cleanup stopped. Diagnostics: ${result.logPath}`) + } + const after = json(join(dir, 'package.json')) + if (BRIDGES.some(name => Object.hasOwn(dependencies(after), name))) { + fail('桥插件依赖仍然存在,停止清理文件。', 'Bridge dependencies remain; file cleanup stopped.') + } + // 原生 remove 只整理本次删除的依赖;处理中断安装留下的独立 bundle 记录。 + const bundles = after.dsh?.profile?.bundles + if (bundles?.some(name => BRIDGES.includes(name))) { + after.dsh.profile.bundles = bundles.filter(name => !BRIDGES.includes(name)) + await runtime.saveManifest(dir, after) + } +} + +function cleanup(plan) { + for (const path of plan.targets) { + const now = stat(path) + if (!now) continue + // Node 的 rm 不遍历符号链接或 Windows junction 的目标。 + if (path === plan.managed) process.chdir(plan.home) + rmSync(path, { recursive: now.isDirectory(), force: false }) + } +} + +async function main() { + if (Number(process.versions.node.split('.')[0]) < 24) { + fail('卸载需要 Node.js 24 或更新版本。', 'Uninstall requires Node.js 24 or newer.') + } + const { values } = parseArgs({ options: { + home: { type: 'string' }, 'runtime-root': { type: 'string' }, + 'dry-run': { type: 'boolean' }, help: { type: 'boolean' }, + } }) + if (values.help) { + console.log(`Usage: uninstall [--home ] [--dry-run] [--runtime-root ] + + --home DSH directory (defaults to DSH_HOME or ~/.dsh) + --dry-run List the files and plugins to remove + --runtime-root Use DSH dependencies from this checkout`) + return + } + let home = values.home ?? process.env.DSH_HOME ?? join(homedir(), '.dsh') + home = absolute(home) + if (stat(home)) home = realpathSync(home) + regular(home, 'directory') + const plan = inspect(home) + if (values['dry-run']) { + describe(plan) + return + } + const runtime = await loadRuntime(plan, values['runtime-root']) + pair('正在卸载 dsh-browser…', 'Uninstalling dsh-browser…') + const execute = async () => { + if (runtime) await removeBridge(plan, runtime) + cleanup(plan) + } + if (runtime) await runtime.withFileLock(join(plan.web.dir, 'package.json'), execute) + else await execute() + pair('请在浏览器扩展管理页面移除“dsh 浏览器助手”,完成卸载。', + 'Remove dsh Browser Assistant from your browser to finish uninstalling.') + console.log('Chrome: chrome://extensions · Edge: edge://extensions · Firefox: about:addons') + pair('如果 DSH 正在运行,请重启 DSH。', 'If DSH is running, restart it.') +} + +await main() diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 new file mode 100644 index 000000000..3f58380ee --- /dev/null +++ b/scripts/uninstall.ps1 @@ -0,0 +1,38 @@ +# Windows PowerShell 5.1 使用 UTF-8 BOM 读取中文。 +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false + +Get-Command node -ErrorAction Stop | Out-Null +$Engine = if ($PSScriptRoot) { Join-Path $PSScriptRoot 'uninstall.mjs' } else { '' } +$DownloadDir = $null +$OriginalLocation = (Get-Location).ProviderPath +$OriginalDirectory = [System.IO.Directory]::GetCurrentDirectory() +try { + if (-not $Engine -or -not (Test-Path -LiteralPath $Engine -PathType Leaf)) { + $DownloadDir = Join-Path ([System.IO.Path]::GetTempPath()) ('dsh-browser-uninstall.' + [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $DownloadDir | Out-Null + $Engine = Join-Path $DownloadDir 'uninstall.mjs' + Invoke-WebRequest -UseBasicParsing ` + -Uri 'https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.mjs' ` + -OutFile $Engine + } + + # 同时释放 runspace 与进程的当前目录。 + $ExecutionRoot = [System.IO.Path]::GetPathRoot($Engine) + Set-Location -LiteralPath $ExecutionRoot + [System.IO.Directory]::SetCurrentDirectory($ExecutionRoot) + & node $Engine @args + $Result = $LASTEXITCODE +} finally { + if (Test-Path -LiteralPath $OriginalLocation -PathType Container) { + Set-Location -LiteralPath $OriginalLocation + } + if (Test-Path -LiteralPath $OriginalDirectory -PathType Container) { + [System.IO.Directory]::SetCurrentDirectory($OriginalDirectory) + } + if ($DownloadDir -and (Test-Path -LiteralPath $DownloadDir)) { + Remove-Item -LiteralPath $DownloadDir -Recurse -Force + } +} +exit $Result diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 000000000..dcae12644 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +download_dir="" + +main() { + local engine="" script_dir + if ! command -v node >/dev/null; then + printf '%s\n' '需要 Node.js 24 或更新版本。 / Node.js 24 or newer is required.' >&2 + return 1 + fi + if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [ -f "$script_dir/uninstall.mjs" ]; then + engine="$script_dir/uninstall.mjs" + fi + fi + if [ -z "$engine" ]; then + if ! command -v curl >/dev/null; then + printf '%s\n' '下载卸载模块需要 curl。 / curl is required to download the uninstaller.' >&2 + return 1 + fi + download_dir="$(mktemp -d "${TMPDIR:-/tmp}/dsh-browser-uninstall.XXXXXX")" + trap 'if [ -d "$download_dir" ]; then rm -rf -- "$download_dir"; fi' EXIT + engine="$download_dir/uninstall.mjs" + curl --fail --location --silent --show-error \ + https://raw.githubusercontent.com/Lum1104/dsh-browser/refs/heads/main/scripts/uninstall.mjs \ + --output "$engine" + fi + node "$engine" "$@" +} + +main "$@" diff --git a/scripts/write-install-info.mjs b/scripts/write-install-info.mjs new file mode 100644 index 000000000..011241587 --- /dev/null +++ b/scripts/write-install-info.mjs @@ -0,0 +1,14 @@ +import { realpathSync, writeFileSync } from 'node:fs' + +const [filename, mode, sourceRoot, dshHome] = process.argv.slice(2) +if (!filename || !['managed', 'checkout'].includes(mode) || !sourceRoot || !dshHome) { + throw new Error('Usage: write-install-info.mjs ') +} +const info = { + schemaVersion: 1, + application: 'dsh-browser', + mode, + dshHome: realpathSync(dshHome), + ...(mode === 'checkout' ? { sourceRoot: realpathSync(sourceRoot) } : {}), +} +writeFileSync(filename, `${JSON.stringify(info, null, 2)}\n`)