dsh-plugin-browserskill
Tencent/BrowserSkillBrowserSkill 的 DeepSeek Harness 浏览器自动化桥接插件,通过原生浏览器工具控制可见的 Chrome 和 Edge Agent Window,支持可访问性与 VOM 页面观察、截图、隔离的多会话控制和 Web UI 实时观察浮层。
用 38 个工具从 agent 侧驱动 DSH 右推栏内置浏览器:打开、导航、读取页面正文与可交互元素、无障碍树、点击、填字、选下拉框、勾选、按键、滚动、拖动网页框边角改变大小,框内网页随之响应式重排,框宽收窄即呈现手机版布局、截图、整页截图、导出 PDF、导出 markdown、按选择器提取 JSON、下载文件、存取 Cookie,以及带序号的操作记录、重放与动作白名单。同时把内置 web_search 接到自建 SearXNG(经 SSH 隧道,Tavily 与真实浏览器作兜底),并注册抓取提供者:被墙的 GitHub 地址按实测可用的镜像顺序重试,并从响应头嗅探字符集,使 GBK 与 GB2312 页面正确解码。
Drive the built-in browser inside the DeepSeek Harness right sidebar.
The DSH right sidebar ships a browser tab (Ctrl+T, the official ui-sidebar-browser). It renders pages fine, but nothing could operate it from the agent side — you had to click and type by hand.
This plugin makes that browser operable. 38 tools, grouped by what they do:
Open and navigate
rightbar_open — open the sidebar browser tab, optionally with a URLrightbar_navigate — navigate to a URLrightbar_nav — back, forward, reloadrightbar_history — what was opened before, persisted across sessions and restartsrightbar_tabs — list, switch, close tabsObserve
rightbar_read — page text plus interactive elements carrying dsh-ref-N handlesrightbar_a11y — the accessibility tree: semantic roles, names, states. Cheaper on contextrightbar_controls — form controls with their current values and refsrightbar_captcha — locate CAPTCHAs and say which kindrightbar_info — address, title, load state, scroll positionrightbar_shot — screenshot to disk, returns the pathrightbar_shot_full — full-page screenshot, top to bottom in one imagerightbar_health — crash, loading, mute statusOperate
rightbar_click — click by ref or selector, or by x/y coordinatesrightbar_fill — type into an inputrightbar_type — type text character by characterrightbar_set_value — set or clear by ref, selector, or label textrightbar_select — pick from a dropdownrightbar_check — tick or untick checkboxes and radiosrightbar_key — press keys (Enter, Tab, arrows, Ctrl+A — real key events on desktop)rightbar_scroll — scroll the pageFrame size
rightbar_resize — resize the browser frame. handle drops a drag grip on the frame'sbottom-right corner; a pixel width sets it directly; presets phone(375) / narrow(480) / wide(720) / wider(960) switch quickly; reset restores the official default. Narrow the frame and the page's responsive layout reflows to its mobile shape on its own.
View and keep
rightbar_zoom — engine-level zoom, not font sizerightbar_find — find text in the pagerightbar_pdf — export the current page to PDFrightbar_style — inject CSS to change font size, hide overlays, go darkrightbar_eval — run a script in the page and bring back the resultExtract and download
rightbar_scrape — pull a list into JSON by CSS selectorrightbar_download — download a file to the hostrightbar_content — export the whole page as markdown, html, text, or jsonLogin state and teardown
rightbar_cookies — save or restore cookiesrightbar_session_reset — rebuild the browser sessionrightbar_close — close the browser tabRecord and guardrails
rightbar_ops — every operation with sequence, time, duration, outcomerightbar_replay — replay actions, by sequence, range, or last Nrightbar_restrict — whitelist actions (readonly mode)rightbar_search_status — which route the built-in web search is taking right nowOpening and navigation go through the official public API ctx.sidebarRight.openTab('browser', { params: { url } }). Reading and interaction run inside the browser guest through the same channel the official desktop build uses. No extra process, no second browser, no memory overhead, and nothing touches the browser you are already logged into.
Every other browser plugin on the market starts a *second* browser: a Playwright Chromium, an Electron window, or a CDP connection to your own Chrome. That is the right answer when you need isolation, downloads, or cookie persistence. It is the wrong answer when the browser you want is already sitting in the sidebar and you just want the agent to be able to use it.
which is what makes page reading possible.
sidebarRight.require() throws"no session surface is mounted" otherwise — that is official behaviour, not a plugin limitation.
dsh web (no desktop shell) the sidebar browser renders as an ``.Opening and navigation still work; reading and interaction cannot, because the browser same-origin policy forbids reading a cross-origin frame. The plugin reports that plainly instead of throwing an opaque error.
dsh plugin add github:Yingmoon0715/yingmoon-plugins#path:/plugins/rightbar-driverOr from the plugin market inside DSH.
DeepSeek's official web search becomes pay-per-call on 1 November 2026. This plugin also registers itself as a DSH search provider (id moon-local), so web_search runs on your own sources instead of the metered official one.
Three tiers, first success wins:
opens on demand and reopens after a drop. Configuration lives in DSH_HOME/search-sources.json.
tavilyKey is set.scrapes real result links. Last resort — it does touch sidebar tabs.
All three failing raises an error that names each tier's reason.
One rule worth keeping: provider availability must be a cheap check, never a network probe on every call. The plugin keeps an in-memory ledger — after all three tiers fail it reports unavailable for a 60-second cooldown, so the harness stops routing searches into a dead end.
Search is only half of it: a result you cannot open is not a result. This plugin also registers a fetch provider (moon-fetch) that keeps a mirror table and retries through it whenever the direct request fails or returns a non-2xx.
github.com, raw.githubusercontent.com, codeload.github.com, andapi.github.com through ghfast.top / gh-proxy.com, with kkgithub and bgithub as last resort.
kkgithub failed to connect, and bgithub returned 403 — so the working pair runs first.
GB2312 Chinese pages do not come back garbled.
Registering a second fetch provider makes the harness see more than one available provider, so pin it in the profile patch:
- id: web
name: '@deepseek-ai/dsh-web'
config:
searchProvider: moon-local
fetchProvider: moon-fetchLeave fetchProvider: http to keep the official fetcher.
Self-test: /rightbar-api/fetch-test?u= reports the final URL, status, length, and the first 300 characters.
Self-test: /rightbar-api/search-test?q=term runs through DSH's own ctx.web.search and reports lastVia, lastCount, lastError, and downUntil.
38 tools. The environment is stated honestly rather than passing clone results off as desktop results.
Verified on the Web clone (dsh web, sidebar browser is an `): open returns frames: 1; the whole resize path; tabs, info, health, ops, zoom, wait, find, style, eval; and the rejection path of scrape and history`.
Verified on desktop (Electron webview): an earlier 6-tool build was exercised one by one on the author's own DSH 0.2.1-rc.2 desktop — open returned frames: 1, read returned real page content (URL, title, 31 interactive elements, multilingual body), navigate went through webview.loadURL and really changed the URL, click really navigated, shot and shot_full wrote real screenshots. The 38-tool build has not been re-verified on desktop as a whole — that is the next step once a restart is possible.
Cannot be verified on the Web clone (browser same-origin policy, not a plugin defect): read, click, fill, type, set_value, select, check, key, scrape, download, cookies, a11y, controls, captcha, pdf, content all need to read inside the browser guest, and a cross-origin iframe refuses by design. Those calls return an explicit same-origin message rather than a result. On desktop the guest is a ` reached through executeJavaScript`, which is not subject to that limit.
Official assumptions checked before shipping — from the author's own app.asar build (0.2.1-rc.3), item by item rather than inferred: the data-sidebar-browser-frame attribute, createElement("webview"), host.append(element), the dshDesktop.protocolVersion === 1 branch, loadURL, dom-ready, and openTab are all present and unchanged. CSS class hashes differ between the asar build and the npm package (bZwPEG_* vs SB_kFW_*), but this plugin never references official class names — only tag names, that data attribute, and public APIs.
resize measured on the clone (direct calls to the real functions plus the full chain). The frame itself was measured directly, and the page inside was measured by the page itself: a test page reports its own window.innerWidth and which media query matched back to the parent via postMessage. Screenshots are unavailable on the Web build — rightbar_shot states plainly that an iframe has no capture interface — so this is harder evidence than a picture anyway: the numbers come from the page.
frame 745 (default) -> page viewport 745 -> desktop layout frame 375 / 480 / 600 -> page viewport 375 / 480 / 600 -> mobile layout (media query max-width:600px matched) frame 720 / 960 / 1200 -> same -> desktop layout reset -> page viewport 745 -> desktop layout
Frame width and page viewport match pixel for pixel at every step, with 600 as the boundary. What this means in practice: drag the frame below 600 and the page shows its mobile layout; drag it wider and it goes back to desktop. That reflow is the point of the feature, and it is measured, not assumed. the drag grip sits flush with the frame's bottom-right corner, gap 0 on both edges; setting width 600 moves the frame from 745 to 600; a simulated 120 px mouse move widens the frame by 120; reset returns to 745 leaving no inline residue; presets phone 375 / narrow 480 / wide 720 / wider 960 all exact; an illegal width is rejected with the preset list.
One implementation note: the official build puts the browser frame in a flex chain — the frame and every ancestor container are flex:1 1 auto, so the width comes out of the flex algorithm and writing width alone does nothing. resize uses width + flex:none + max-width together to take the frame out of flex sizing.
Search side: against the self-hosted SearXNG a CJK query returned five real results in 2.5 s; pointed at a dead port it failed in 3 ms and then reported unavailable, instead of stalling the whole call.
Both of these were invisible to the earlier verification, which only set widths and dispatched synthetic mouse events. They only showed up when the frame was dragged with real mouse input in both directions, which is the thing the feature is for.
1. Dragging wider than the viewport made the handle unreachable. The viewport is 746 px and the default frame is 745, so it fills the screen exactly. With no upper bound, asking for 900 px put the handle centre at 892 — 146 px outside the viewport, where document.elementFromPoint returns null. The user drags a little too far, the handle vanishes, and there is no way to drag it back except reset. Fixed by clamping to the room the container actually has (viewport right - frame left - 2), applied in one place so presets, pixel values and dragging all obey it.
2. Dragging left did nothing at all. Moving the mouse leftwards takes it over the iframe — the web page — and the mousemove events are swallowed by that document, so a listener on window never fires. Dragging rightwards worked only because the mouse leaves the frame and stays in the parent document. setPointerCapture was tried first and measured not to help. The fix is a full-screen transparent shield created on mousedown: the mouse never enters the iframe during a drag, so the events stay in the parent document. The shield is removed on mouseup, and also on window blur and document mouseleave so it can never get stuck covering the interface.
Measured after both fixes, with a real page loaded into the frame and real mouse events through CDP:
drag left 100 745 -> 645 drag right 100 645 -> 743 (clamped to the ceiling) drag left 250 743 -> 493 drag right 400 493 -> 743 (clamped) request 900 743 request 4000 743 reset 745
The handle is grabbable at every step and no shield is left behind.
Listed rather than hidden:
await in the dispatch turned the tool result into an empty object, whichlooked like "called it, nothing happened".