dsh-host-web-compat
Discovered★ 4dsh 宿主插件——经 webserver 钩子向页面注入旧内核浏览器 polyfill。
dsh-host-web-compat
DeepSeek Harness × Android ecosystem · dsh-mobile-apk (shell APK) · dsh-shell-termux (shell) · dsh-client-ui-responsive (mobile UI)
Host plugin for DeepSeek Harness that injects
legacy-browser polyfills into every served page via the webserver tapIndex hook, and bridges the
shell APK's directory picker and path-open channels.
Background
Older kernels (MuMu's bundled browser, older WebViews) lack APIs the host page calls directly:
- missing
AbortSignal.any()breaks the workspace directory picker's concurrent RPC cancellation; - missing
Promise.withResolvers()breaks the host boot-ready tail script, failing the plugin tree; - missing global
Iterator(Chrome 122+) makes upstream 0.1.5 client bundles (documentpreview and others) throwIterator is not definedat import time ("Failed to load plugins").
The plugin injects idempotent polyfills at the HTML level — no browser-side changes needed.
Quick start
1. Install — package into the profile's node_modules.
2. Mount — in the profile's cordis.patch.yml:
- insert:
- id: web-compat
name: '@dsh-android/dsh-host-web-compat'
disabled: !!js process.platform !== 'android'
3. Restart the service; the picker works again in old kernels.
Polyfills injected
| API | condition | note |
|---|---|---|
AbortSignal.any | missing | Chrome 116+ / Node 20.3+ |
AbortSignal.timeout | missing | Chrome 103+ / Node 17.3+ |
structuredClone | missing | Chrome 98+ / Node 17+ |
Object.hasOwn | missing | Chrome 93+ / Safari 15.4+ |
Array.prototype.at | missing | Chrome 92+ |
String.prototype.replaceAll | missing | Chrome 85+ |
crypto.randomUUID | missing | Chrome 92+, secure context required |
Promise.withResolvers | missing | Chrome 119+; the host boot-ready tail depends on it |
global Iterator + iterator helpers | missing | Chrome 122+ (map/filter/take/drop/flatMap/toArray/forEach/some/every/find/reduce + Iterator.from) |
Object.groupBy / Map.groupBy | missing | Chrome 117+ |
Set.prototype set methods | missing | Chrome 122+ (union/intersection/difference/symmetricDifference/isSubsetOf/isSupersetOf/isDisjointFrom) |
Array.fromAsync | missing | Chrome 121+ |
Idempotent: skipped when already present. Every snippet shares one <script> element, so each
one must end on a complete statement (see Development constraints).
Other injections
- Directory-picker bridge (
__dshBridgeplus the/api/android/dir-pick/*polling endpoints that return real SAF paths); - open-path (
window.__dshOpenPath: chat mentions and tool-row paths into the shell chooser) — 0.14 makes resolution session-scoped (relative paths resolve against the active session cwd); - theme bridge (
__dshThemeBridge: system light/dark into page theme variables); - agent tool-row path recognition — 0.14 keys on DOM facts (not CSS-module class names) when clicking an absolute path in a tool row hands it to the shell chooser;
- clipboard fallback (2026-10-03): a global wrapper over
navigator.clipboard.writeText. Android WebView denies the async Clipboard API (NotAllowedError: Write permission denied; WebView has no permission prompt), and upstreamwriteClipboardreturns false from itscatch— it only falls back toexecCommand('copy')when the API is absent — so every copy button wrote nothing and showed nothing. The wrapper falls back to the shell bridgewindow.androidBridge.copyText(ClipboardManager), then toexecCommand('copy'), and emits one plain-language notice when all three fail. Patching the API instead of each caller covers CodeBlock / MessageIconActions / TerminalBlock / HoverCard / user-questions / trajectory and third-party plugins, which all read the API at click time; - boot watchdog: 40s stuck diagnostics plus the three contract lines the shell APK parses by
field position (
[dsh-boot-stall]stuck,[dsh-boot-ready]really rendered,[dsh-boot-failed]client plugin tree failed) — see Boot diagnostics contract.
Boot diagnostics contract
The watchdog reports on the console.error channel. The shell APK matches the line prefix and then
reads the remaining fields by position, so the field order below is part of the interface:
| line | meaning | fields after the prefix |
|---|---|---|
[dsh-boot-stall] dsh-boot-diag source=page-stall ... | 40s without a render | pageSideRuntime=<folded JSON> detail=<folded k=v> |
[dsh-boot-ready] dsh-boot-diag source=page-ready ... | #root rendered and the boot page is gone | pageSideRuntime=<folded JSON> |
[dsh-boot-failed] dsh-boot-diag source=page-plugin-fail ... | client plugin tree failed | detail=reason=<first line, 200 chars> takenMs=<ms> failedCount=<N> rendered=<bool>, failedIds=<comma list or ->, pageSideRuntime=<folded JSON, last> |
failedIds keeps only items that look like a loader entry id
(/^[@A-Za-z0-9][@A-Za-z0-9._\/-]*$/, at most 120 characters), at most 8 items, unquoted and
space-free; - when none is readable. It sits before pageSideRuntime because fold
truncates at 2048 characters and a truncated JSON tail cannot be parsed at all.
Readiness is rendered() && !bootPagePresent(). A failed client plugin tree renders a boot page
that still gives #root children, so the old rendered()-only criterion reported it as ready: the
shell stopped its stall timer and the failure stayed silent. The failure line is published from two
idempotent trigger points — the MutationObserver callback (2s debounce) and every readyWatch
tick, which covers a failure page that reached its final state before the observer was installed.
pendingBoot() is unchanged, so a failure page deliberately does not raise the 40s overlay: it
exits through [dsh-boot-failed] instead.
Development constraints
Snippets live in the POLYFILLS array; the assembly rule is POLYFILL_SCRIPT_BODY in
lib/index.js:
- Every snippet must end with
;or}. All snippets share one<script>element, and a single syntax error makes the parser reject the whole element: the page then behaves as if no polyfill existed while the served HTML still contains the snippet text (measured 2026-09-10 — the Set-methods IIFE ended with})()against the next snippet'sif (, so WebView 110 reportedIterator is not definedat import time). apply()parse-checks the assembled markup withnew Functionat load and throws on failure (loud failure beats a silent downgrade).- Shim the real structure, not just the name: the global
Iteratormust be a constructor whose.prototypeIS%IteratorPrototype%(bundled code such as pdfjs patchesIterator.prototype.xdirectly), and iterator wrappers must inherit that same prototype or chained helpers break.
Tests
node scripts/smoke-injections.mjs # assembly + per-script parse + page-marker assertions, no install needed
node scripts/boot-watchdog.test.mjs # watchdog behaviour: pendingBoot / ready / failure contract line
The script loads the real plugin against a stubbed cordis, runs one tapIndex transform, and parses
every served script element — the only check that catches an assembly syntax error (grepping the text
does not).
Comments
Loading…
Similar plugins
by SJTUMalPan
DSH 宿主插件:把 dsh-usage-stats 生成的用量报表挂到宿主 Web 的 /usage,鉴权复用宿主登录态(零依赖)
★ 0
MIT
JavaScript
Sep 27, 2026
dsh plugin --profile web add dsh-usage-dashboardby tingaidehua
DSH plugin: publish /app/<slug>/ and /demo/<slug>/ static or proxy sites
★ 0
JavaScript
Oct 12, 2026
dsh plugin --profile web add dsh-deploy-webby iasiv5
DSH 插件:(Web Profile 专用)自动跳过 BrowserAuth,访问 Web 地址即可直接使用,无需每次复制启动 URL 中的随机 Token 到浏览器。
★ 3
↓ 1.3k/wk
MIT
JavaScript
Oct 28, 2026
dsh plugin --profile web add @iasiv5/dsh-skip-browser-authby buhuikongpan
DSH 分层插件管理器:原生插件按 系统层/WebUI 层/工具层 只读展示,用户扩展支持停用/启用、补登记、卸载与可编辑描述。
★ 8
JavaScript
Aug 20, 2026
dsh plugin --profile web add dsh-pluginmanagerby KaiYuan0422
一个简单的dsh插件:可以创建一个直接双击启动dsh web界面的脚本,可以自定义图标
★ 0
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add dsh-launcherby WSGsety
DSH web plugin manager — enable/disable installed plugins from Settings. · DSH 网页插件管理:设置页直接启停已安装插件。
★ 0
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add @wsgsety/dsh-plugin-manager