在手机上离线运行编码智能体,无需电脑
31.6/100
综合分
3
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/4 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
该插件未发布到 npm,按作者文档从 GitHub 源安装即可(静态检查只覆盖 npm 路径,不代表插件不可用)。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add yueshen0211/dsh-android该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
是什么
dsh 原生插件 · dev
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 33 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
⚠︎ 实装验证未通过(unknown · 2026/10/30) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装仓库 package.json 标记 private,未发布到 npm,需从源码安装;安装清单取自 android/plugins/ui/package.json
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@dsh-mobile/ui(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/11 22:10:52
安全扫描自动 · 静态分析源码v2.7.0⚪ 扫描:无法判定尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0已尝试扫描但取不到可分析的源码:仓库或包不存在、已删除,或为非 JS/TS 实现、未随包发布源码。无法判定,不代表安全。
- · 包内无可扫 JS
- · TS 源码(可能为非 JS 实现或未发布源码),无法判定
扫描于 2026/10/31(0 天前) · 范围 全包扫描(包内未声明入口) · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 手机离线运行编码智能体
- 本地运行编程智能体
在 Android 上跑 DeepSeek Harness 编程智能体,全程本地运行
README
由 DeepSeek 最新模型翻译生成面向 Android 的 DeepSeek Harness
把完整的 DeepSeek Harness 编码智能体跑在手机上,无需任何桌面端:Node 引擎和 Web GUI 打包进单个 APK,直接在设备上启动。
不需要配套 PC。不需要 Termux。不需要局域网连接。引擎绑定 127.0.0.1,应用在 WebView 里渲染自己的 GUI。
状态:可运行的原型(M1)。 已在 Android 14 x86_64 模拟器上端到端验证:引擎能启动,GUI 能渲染,连接稳定。真机使用的 arm64 路径已构建,但仍需在设备上确认。见状态。
目录
- 这是什么
- 架构
- 难点
- 仓库结构
- 从零构建
- 运行与测试
- 配置
- 状态
- 限制
- 设计说明
这是什么
DSH 以 Node 应用的形式发布:一个配置档启动器,负责组装插件树并提供浏览器 UI。把它移植到 Android 不是重写——Android overlay 加一小层 Java 外壳就够了,而且没有 fork 或修改任何上游包。
展开完整文档(README 剩余 1 节)
面向 Android 的 DeepSeek Harness
把完整的 DeepSeek Harness 编码智能体跑在手机上,无需任何桌面端:Node 引擎和 Web GUI 打包进单个 APK,直接在设备上启动。
不需要配套 PC。不需要 Termux。不需要局域网连接。引擎绑定 127.0.0.1,应用在 WebView 里渲染自己的 GUI。
状态:可运行的原型(M1)。 已在 Android 14 x86_64 模拟器上端到端验证:引擎能启动,GUI 能渲染,连接稳定。真机使用的 arm64 路径已构建,但仍需在设备上确认。见状态。
目录
- 这是什么
- 架构
- 难点
- 仓库结构
- 从零构建
- 运行与测试
- 配置
- 状态
- 限制
- 设计说明
这是什么
DSH 以 Node 应用的形式发布:一个配置档启动器,负责组装插件树并提供浏览器 UI。把它移植到 Android 不是重写——Android overlay 加一小层 Java 外壳就够了,而且没有 fork 或修改任何上游包。
三部分让它跑起来:
| 部分 | 作用 |
| libnode.so | Node 26(来自 Termux 的 Android 构建),重命名并打补丁,使其能从 APK 的原生库目录作为可执行文件加载 |
| 引擎树 | DSH 的依赖闭包,首次启动时从 APK assets 解包到应用私有存储 |
| Java 外壳 | 在独立进程里启动引擎,等待它打印出 URL,然后让 WebView 指向该地址 |
引擎是未修改的已发布版本 @deepseek-ai/dsh@0.1.5-rc.3。所有平台差异都集中在一个 overlay 文件加一个小插件里。
架构
┌───────────────────────────────────────────────────────────────────────────┐
│ APK │
│ │
│ ┌─ app process ─────────────────────┐ ┌─ :engine process ────────────┐ │
│ │ MainActivity │ │ EngineService (foreground) │ │
│ │ WebView ── HTTP/WS ──────────────┼──▶│ EngineRuntime │ │
│ │ polls files/engine-state.txt ◀───┼───│ └─ libnode.so --expose-… │ │ │ └───────────────────────────────────┘ │ dsh --profile web │ │ │ │ --patch android.yml │ │ │ └──────────────────────────────┘ │ │ │ │ lib/<abi>/libnode.so + 10 .so the runtime, stored uncompressed │ │ assets/engine/node_modules/… the DSH tree, unpacked on first run │ │ files/engine/ 解包后的目录树(应用私有) │ │ files/dsh-home/ 会话、设置、凭据 │ └───────────────────────────────────────────────────────────────────────────┘
引擎保持一贯的设计:绑定回环地址,为每个进程生成一个启动 token,打印 dsh web: http://127.0.0.1:<port>/?token=…,WebView 加载时再用这个 token 换一个 HttpOnly 会话 cookie。应用只需要读那一行。
为什么引擎跑在独立进程里: 运行时崩溃不会把 UI 一起带走,长期存活的引擎能扛住切到后台,它的内存峰值也不会和 WebView 抢。
难啃的地方
下面这些都是在真机上试出来的。大部分在桌面上根本看不到。
Node ≥ 20 是硬性要求,而且没有哪个预编译 libnode 够新
dsh-app-boot 从 node:util 静态导入 parseEnv。Node 18 没有导出它:
SyntaxError: The requested module 'node:util' does not provide an export named 'parseEnv'
所有预编译的 Android libnode 都太旧了 —— nodejs-mobile 停在 18.20.4,puerts/backend-nodejs 停在 16.16 —— 所以运行时来自 Termux 软件包(nodejs 26.4.0,aarch64 和 x86_64 都有)。commander@15 还要求 >= 22.12.0,Node 26 满足。
Android 不会执行应用存储里的二进制文件
Android 10+ 把应用数据目录挂载为 noexec。唯一可写又可执行的位置是 nativeLibraryDir,而只有 lib.so 条目会被解压到那里。所以 GNU node 以 lib/<abi>/libnode.so 的形式发布,并从那个目录执行。
它的 DT_RUNPATH 被就地改写,从 Termux 硬编码的前缀改成 $ORIGIN,这样加载器就能找到它的同级文件。$ORIGIN 比它替换掉的路径短,所以字符串表条目大小不变,ELF 偏移也不会移动。
Android 在安装时会丢掉带版本号的 soname
libnode.so 需要 libz.so.1、libcrypto.so.3、libicuuc.so.78……但包管理器只解压名为 lib.so 的条目,所以这些名字在磁盘上根本不存在:
CANNOT LINK EXECUTABLE: library "libz.so.1" not found: needed by main executable
用带版本号的名字再塞一份重复文件也没用 —— 过滤发生在安装时。解决办法是把每个 DT_NEEDED 字符串改写成确实会被发布的无版本号名字(tools/elf-patch-needed.mjs),这样做是安全的,因为每次替换都严格更短。
补丁条目改不了某一行的 name
这一条悄无声息,正是它让应用卡在「starting the DSH engine」好几个小时:
patch: name mismatch for "attachment-local"
(expected "@deepseek-ai/dsh-attachment-local",
got "@dsh-mobile/attachment-android"), skipping
按 id 定位的补丁只接受 config 和 disabled。改了 name 会让加载器丢掉整个条目只给一条警告,这样原来的服务商还能继续加载。因此,替换插件意味着禁用上游那一行,再插入你自己的那一行(overlay 就是这么做的)。
两棵 node_modules 树都会解析插件名
引擎条目通过引擎树解析自己的导入,但加载器解析配置行的包名时,是相对 profile 目录来的:
Cannot find package '@dsh-mobile/attachment-android' imported from
.../dsh-home/profiles/web/
在桌面端,pnpm 会把 profile 的 node_modules 链接到安装树;APK 带不了符号链接。shell 会在启动时把打包好的插件铺进 profile 树。
缺失的原生模块,以及移除它们引发的连锁反应
| 包 | 问题 | 解决方式 |
| node-pty | 没有 android-arm64 预构建,而 dsh-subprocess-local 静态导入了它 | 用 child_process 支撑的 shim —— 老实说它不是 pty |
| sharp | 没有 android-arm64 构建 | 替换为纯 JS 服务商 |
| koffi | 需要 @koromix/koffi-android-{arm64,x64},而 npm 在 Windows 主机上拒绝安装(EBADPLATFORM) | 直接从 registry 拉取 |
禁用 sharp 支撑的那一行不是选项:attachments 是被消费的服务,删掉它会连锁波及 file-upload 和 session-controller,导致整个启动失败。替换服务商,别删那一行。
aapt2 改写路径的两种意外方式
- 反斜杠。 在 Windows 上,aapt2 写资源条目名时用的是主机分隔符(assets/engine\@scope\pkg\file)。这在 zip 里合法,但 Android 看不见:AssetManager 遍历归档时按 / 切分。第一次构建在 25k 个条目里产生了 135,178 字节的反斜杠,差点发布出一个完全没有资源的应用。打包时已归一化。
- 点文件被丢弃。 隐藏条目会从 -A 打包中悄悄消失。对约 70 个 npm 配置文件无所谓,但对 pi-ai 的 .manifest.json 是致命的,因为它是按名字导入的。staging 脚本重命名了那个文件,改写了它的导入方,并且只要还剩任何点前缀导入就让构建失败。
Android 拒绝两个 POSIX 调用,而会话存储依赖它们
两个都在会话路径上,所以修好之前,手机上每一次智能体运行都会失败——不是功能降级,而是彻底不能用。两个都替换掉了,没有 fork 上游包。
flock(2)。 dsh-session-persistence-jsonl 会对每个会话目录的 session.lock 加非阻塞排他锁,时机就在新建会话第一次落盘写入之前。上游条目还没到内核就拒绝了:
flock is not supported on android-arm64
因为 Node 在那里报告 process.platform === 'android',而守卫只接受 linux 和 darwin,而且上游没有发布 android 预构建产物(它的可选依赖只有 linux/darwin)。Bionic 确实实现了 flock,引擎代码树里也已经带了 Koffi —— 一个可用的 Android arm64 FFI —— 所以 android/shims/node-addon-system/ 用一个通过 Koffi 调用 libc 的包替换了原包。
这是真正的锁,不是桩。上游的浏览器 worker 把这个调用打桩成立即成功,因为它是单进程的;这里由内核干活,已在设备上验证:同一 inode 上的第二个描述符会以 EAGAIN 拒绝,关闭持有者后锁释放。这个 shim 替换了整个包,所以上游 landlock-run 那一半原样保留 —— dsh-sandbox-local 静态导入它,而它的 probe() 对缺失的二进制已经报告 unusable,这在 Android 上是正确的判定。
link(2)。 锁修好后,下一个失败是会话日志发布:
EACCES: permission denied, link '.../session.v3.jsonl.zstd.<rand>.tmp' -> '.../session.v3.jsonl.zstd'
Android 在应用存储里禁止硬链接,对任何目标都禁止,包括不存在的目标,所以这不是碰撞竞争。同目录下的 rename() 能成功 —— 但它会覆盖,而这正是当初选择 link 要避免的风险:link 就是原子的、不覆盖的发布,检查文件是否已存在只是更省事的预检。renameat2(RENAME_NOREPLACE) 是同样的操作,只是不用硬链接,所以 android/support/android-fs/ 通过 Koffi 调用它。
这个没法用换包解决,因为调用点是对 Node 内置模块的解构导入。属性打补丁也不行,原因值得知道:
从内置模块做 ESM 具名导入得到的是快照,不是活绑定。 在
require('node:fs/promises').link = ours 之后,无论是新 import('node:fs/promises'),
还是解构的 const { link } = …,报告的仍然是原始函数,而 CJS 对象报告的是打过补丁的版本。
已在设备上验证。
所以修正方案是预加载:register.mjs 安装一个进程内加载钩子,在模块加载时重写那一个导入,同时保留它从 node:fs/promises 取的其他所有绑定。EngineRuntime 把它作为绝对 --import 路径传入,模块放在 node_modules 下,这样它能解析到 Koffi。
UI 和引擎跑在不同的进程里
EngineService 跑在 :engine;activity 跑在主进程。因此通过 static 字段共享状态什么都共享不了 —— 每个进程各有一份副本。引擎正确报告了自己的 URL,而 UI 轮询的字段在它的进程里从未被写入,于是应用永远停在启动页,日志却一切正常。现在交接改用一个文件(files/engine-state.txt,原子发布),由 activity 轮询。
这也是这个 bug 在桌面端从未复现的原因:桌面端 service 和 UI 共享同一个进程。
老 WebView 需要 polyfill
内置 UI 面向现代浏览器。在模拟器镜像的 WebView(Chrome 113)上,外壳能渲染出来,但连不上:
| API | 需要 | 症状 |
| AbortSignal.any | Chrome 116 | connection lost, retry #1…#7 |
| 全局 Iterator 对象 | Chrome 122 | Failed to load plugins —— PDF.js 执行 Iterator.prototype.join = …,对象缺失就抛错 |
| Promise.withResolvers | Chrome 119 | 未捕获的 TypeError |
WebViewPolyfills.java 在 onPageStarted 里注入这些补丁。注意第二行:缺的是全局,不是某个辅助函数,所以带守卫的 if (typeof Iterator === 'undefined') return 补丁会静默失效,什么都不做。
手机上的 WebView 大概率更新,但 WebView 版本不是应用能控制的,所以应用自带补丁,而不是假定某个基线版本。
仓库结构
profile/
android.patch.yml ← 平台层。每个上游版本只需维护这一个文件 m0/ 实验性覆盖层,作为 M0 调查的记录保留 android/ app/ Java 外壳:WebView + EngineService(:engine) + EngineRuntime shims/node-pty/ 原生模块的替身,该模块没有 Android 构建 plugins/attachment-android/ 纯 JS 附件服务商(替换基于 sharp 的那个) tools/ android-env.ps1 激活工作区本地工具链 build-apk-cli.ps1 不依赖 Gradle 的 APK 构建:aapt2 → javac → d8 → zipalign → apksigner zip-add-nativelibs.mjs 打包 lib/<abi>/ 条目并规范化分隔符 elf-closure.mjs 解析二进制的 DT_NEEDED 闭包 elf-inspect.mjs ELF 头、解释器、RUNPATH elf-patch-needed.mjs 就地重写带版本号的 soname verify-engine.mjs M0 验证套件 ws-probe.mjs 信任围栏与 WebSocket 升级探测 cdp-.mjs 通过 DevTools 协议驱动设备上的 WebView (probe / click / touch / mutate / props / ancestors / shot) diagnose-device.ps1 一条命令生成设备报告,插着数据线时用 m1/ fetch → stage runtime → stage engine → 打开模拟器 GUI device/ 设备端诊断(通过 run-as 运行) docs/ 设计文档.md 设计文档(中文) M0-报告.md 引擎移植验证 M1-报告.md APK 里程碑 M2-计划.md 下一个里程碑:移动端 UI 适配(中文) M2-UI设计.md 移动端 UI 设计方案,含原型测量数据(中文)环境配置.md 环境配置、实测状态、沙箱限制(中文) release-notes-v0.1.0-m1.md 当前预发布版已发布的发行说明 NEXT-SESSION-HANDOFF.md 工作交接:发现、约定、下一步以下内容已生成,但刻意没有提交:.toolchain/(21 GB 的 JDK/SDK/NDK/模拟器)、android/app/src/main/assets/engine/(DSH 目录树)、android/app/src/main/jniLibs//(Node 运行时)。这些都由下面的脚本重新生成。
从零开始构建
需要 Windows,带 PowerShell,并且 PATH 里有 Node.js。其他东西全部下载到工作区,不会装到系统里。
完整的环境文档——实测版本、目录重定向的原因、沙箱限制,以及在另一台机器上从零开始的检查清单——见 docs/环境配置.md(中文)。
1. 工具链
| 组件 | 版本 | 来源 |
| JDK | Temurin 21 | https://api.adoptium.net/v3/binary/latest/21/ga/windows/x64/jdk/hotspot/normal/eclipse |
| cmdline-tools | 22.0 | https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip |
| SDK 包 | platform-tools、platforms;android-35、build-tools;35.0.0、ndk;27.0.12077973 | sdkmanager |
有两个环境细节很关键,tools/android-env.ps1 都处理好了:
- ANDROID_USER_HOME / ANDROID_PREFS_ROOT / GRADLE_USER_HOME / npm_config_cache 都重定向到工作区里。sdkmanager 默认写 %USERPROFILE%\.android,写不进去就报 Failed to download any source lists!——看着像网络故障,其实是权限问题。
- adb 把 ~/.android 写死在代码里,不认上面那些变量,所以这个目录必须存在。
. tools/android-env.ps1 # activate for the current shell
2. 运行时、引擎、APK
Native Node runtime for each ABI you want (arm64-v8a for phones, x86_64 for the emulator)
. tools/m1/fetch-runtime.ps1 -Abi arm64-v8a
. tools/m1/stage-runtime.ps1 -Abi arm64-v8a
. tools/m1/fetch-runtime.ps1 -Abi x86_64
. tools/m1/stage-runtime.ps1 -Abi x86_64
DSH dependency tree into assets (installs, patches, stages plugins)
. tools/m1/stage-engine.ps1
APK
. tools/build-apk-cli.ps1 -ProjectDir android -Module app
-PackageName dev.dsh.mobile -MinSdk 29 -TargetSdk 35
-Label 'DeepSeek Harness' -VersionName '0.1.0' -VersionCode 1
输出:android/app/build/cli-debug/dsh-debug.apk(两个 ABI 约 233 MB,单个约 140 MB)。
传 -Abis arm64-v8a(或 x86_64)就只打包一个运行时。发布版就是这么构建的,所以最终产物不取决于工作区里恰好准备了哪些 ABI——通常两个都有,因为模拟器要 x86_64,手机要 arm64-v8a。
为什么不用 Gradle
Gradle 守护进程在这个环境里起不来(Couldn't open current thread, error = 5,来自 native-platform.dll 以提权方式打开线程),所以构建脚本直接驱动 SDK 工具。在这里够用了:shell 是纯 Java,没有 AndroidX,也没有 Kotlin。有两个后果值得了解:
- 源码必须是 Java 8 字节码,用 -bootclasspath android.jar 编译。这样能守住 Android API 规范,但会隐藏 java.lang.invoke——所以 shell 用的是匿名类,不是 lambda。
- debug 配置会注入 android:debuggable="true",让 adb shell run-as 能用,下面的设备诊断也才跑得起来。
想加 Kotlin、AndroidX 或真正的资源,就得回到 Gradle。
运行与测试
模拟器
$env:ANDROID_AVD_HOME = "$PWD/.toolchain/android-user-home/avd"
& "$env:ANDROID_HOME/emulator/emulator.exe" -avd dsh-test -no-audio -no-boot-anim
-gpu swiftshader_indirect -no-snapshot-load
Open the on-device GUI in your desktop browser (resolves the live port and token)
. tools/m1/open-emulator-gui.ps1 -Open
-gpu swiftshader_indirect 是故意绕开硬件路径:宿主机装了虚拟显示适配器(Oray、GameViewer……)时,Vulkan 互操作路径会崩,报 Failed to find ColorBuffer。
用 x86_64 镜像,是因为在 x86_64 宿主机上跑 arm64 镜像要走 QEMU 指令翻译,每轮迭代(约 190 MB 解包,再加一次 Node 启动)会拖成几十分钟。同一条流水线构建两种 ABI,都打进一个 APK。
设备
$adb = "$env:ANDROID_HOME/platform-tools/adb.exe"
& $adb install -r android/app/build/cli-debug/dsh-debug.apk
& $adb logcat -c
& $adb shell am start -n dev.dsh.mobile/.MainActivity
& $adb logcat -s DshEngineService:* DshEngineRuntime:* DshMainActivity:*
预期输出:
DshEngineRuntime: launching: […/libnode.so, --expose-internals, <entry>, --profile, web, --patch, <abs>, --no-open, --port, 0] DshEngineService: dsh web: http://127.0.0.1:<port>/?token=…
DshEngineRuntime: engine ready on port <port>
DshMainActivity: loading engine url: http://127.0.0.1:<port>/?token=…
设备端诊断
比重建 APK 快得多,也是唯一能看清设备上真实情况的办法:
$base = ((& $adb shell pm path dev.dsh.mobile) -join '') -replace 'package:','' -replace '/base.apk','' -replace '\s',''
& $adb push tools/device/dump-tree.sh /data/local/tmp/ ; & $adb shell chmod 755 /data/local/tmp/dump-tree.sh
Does the overlay actually apply? (this is what exposed the name restriction)
& $adb shell run-as dev.dsh.mobile sh /data/local/tmp/dump-tree.sh $base/lib/arm64
Boot the engine in the foreground and read its full output
& $adb shell run-as dev.dsh.mobile sh /data/local/tmp/boot-engine.sh $base/lib/arm64
The failure log the service writes
& $adb shell run-as dev.dsh.mobile cat files/engine-boot-failure.log
Screenshot — turns "logs look fine but the UI is wrong" into a fact & $adb shell screencap -p /sdcard/s.png ; & $adb pull /sdcard/s.png .
UI 出问题时,别靠猜点击位置,直接通过 DevTools 协议驱动 WebView 坐标。adb input tap 必须按设备缩放(手机是 dpr 2.4375,模拟器不是),缩放错了的点击会落在空处,而且不报任何错;合成点击也无法可靠地送达 React 的委托处理器——所以很容易错误地得出「按钮没反应」的结论。CDP 直接定位元素,回读页面内容,并报告控制台错误:
Forward the WebView debug socket (use the MAIN process pid, not :engine)
$mainPid = ((& $adb shell pidof dev.dsh.mobile) -split '\s+')[0]
& $adb forward tcp:9222 localabstract:webview_devtools_remote_$mainPid
(Invoke-WebRequest http://127.0.0.1:9222/json -UseBasicParsing).Content # take webSocketDebuggerUrl
node tools/cdp-probe.mjs .m1/ws-url.txt # DOM summary, API availability, console
node tools/cdp-props.mjs .m1/ws-url.txt "工作区" # what handler is really attached
node tools/cdp-mutate.mjs .m1/ws-url.txt "工作区" # real input + every DOM mutation it causes
node tools/cdp-shot.mjs .m1/ws-url.txt "工作区" <serial> out.png # screenshot with the panel still open
cdp-shot.mjs 之所以存在,是因为 CDP 客户端一断开,下拉菜单立刻就关,所以「先点击,再截图」拆成两条命令永远行不通。
一个踩过坑才学到的提醒:模态框渲染在页面之上,但仍留在 DOM 里,所以某个元素可能确实存在、已启用、接线正确,却依然点不到。判定某个控件坏了之前,先检查有没有 [role=dialog]。
配置
Android 覆盖层
profile/android.patch.yml 就是整个平台层。它以 --patch 覆盖层的形式应用,所以必须是顶层 YAML 数组,元素为 loader 补丁条目。它做的事:
| 行 | 操作 | 原因 |
| sandbox-policy、approval | 固定为 danger-full-access / never | 能用的 sandbox 服务商无法移除(见下文);两行必须一起固定,否则 permission-presets 拒绝构造 |
| attachment-local | 禁用,然后插入 @dsh-mobile/attachment-android | sharp 没有 Android 构建;这一行是替换而非删除,因为 attachments 会被消费 |
| hmr | 禁用 | 桌面端开发机制;也是引擎需要 --expose-internals 的原因 |
| directory-picker、open-in-app | 禁用 | 由 shell 自己负责的桌面操作系统集成 |
| sandbox | 保持启用 | 禁用它就会移除唯一的 ctx.shell 服务商,引擎根本起不来 |
sandbox 这个决定最反直觉。Landlock 是 Linux 的 LSM,Android 并不暴露它,所以直觉做法是禁用这一行——结果直接失败:
@deepseek-ai/dsh-pwsh-sandbox: pending (waiting for service: sandbox)
@deepseek-ai/dsh-permission-presets: pending (waiting for service: shell)
shell 服务商注入 sandbox,而 permission-presets 注入 shell。保留这个服务商挂载时把权限模式固定为完全访问,永远走不到 Landlock 探测,因为只有真正请求隔离时才会走那条链路。
设备上的权限
应用以自身的 uid 运行智能体,工作区放在应用私有存储里,所以由操作系统沙箱取代 DSH 沙箱。引擎只绑定 127.0.0.1,WebView 也锁死:不能访问文件,不能访问内容,导航固定到回环地址。明文 HTTP 只对 127.0.0.1 放行。
状态
M0 —— 引擎移植验证:通过。
引擎带着 overlay 启动,打印自己的 URL,并完成浏览器握手(303 + HttpOnly cookie,不带 cookie 返回 401,带上返回 200)。Node 能力基线已确认:node:sqlite、worker_threads、child_process、WebCrypto、fetch、AbortSignal.timeout。
M1 —— 可安装 APK:可用,已在真机 arm64 手机和 x86_64 模拟器上端到端验证。
在 arm64 手机(Motorola XT2537-4,Android 16)上:热启动约 24 s 引擎就绪,从清空安装冷启动约 74 s,包含完整的资源解包;完整 GUI 渲染出来;工作区选择器端到端可用;真实提示词能返回模型回复,会话日志写入磁盘。在 Android 14(模拟器,x86_64)上:引擎约 10 s 就绪。两边所有错误计数器都是零:
Iterator: 0 AbortSignal.any: 0 Promise.withResolvers: 0
connection lost: 0 uncaught: 0 plugin import failures: 0 engine exits: 0
arm64 的结果原本是悬而未决的问题,现在已经解决——包括那些让手机失败、而模拟器看起来正常的 bug。它们没有一个和架构有关:
1. 补丁条目只能改 config 或 disabled。 想通过给某一行打补丁改 name 来换掉它的包,会被丢弃并给出警告,于是原来的服务商照样加载,照样失败。要替换服务商,就禁用上游那一行,再插入你自己的。
2. 静态变量不跨进程。 EngineService 跑在 :engine 进程,而 activity 跑在主进程,所以就绪的 URL 必须通过文件传递,不能用静态字段。
3. 禁用一行可能连带移除它的服务之外的东西。 dsh-host-directory-picker-auto 在运行时挂载一个配对的客户端插件;禁用它之后,工作区按钮渲染出来却毫无反应,日志里什么都没有。
4. 平台拒绝的两个 POSIX 调用,都在会话存储里,所以它们破坏的是每一次运行,而不是某个功能:flock(2),上游的入口在到达内核之前就按平台拒绝了它;以及 link(2),Android 在应用存储里对任何目标都拒绝。各自如何替换、又不用 fork 任何东西,见难点。
既然不该为了诊断下一个问题就非得插线,应用也会上报自己的进度:
- 启动界面显示引擎的实时输出,所以卡住时在设备上就能看见、能定位;
- 启动后 150 秒仍未就绪,就切换到「仍在启动」,并显示已用时间、引擎输出的最后几行,以及「复制诊断信息」按钮;
- 启动彻底失败时,files/engine-boot-failure.log 会记录完整原因。
过去看起来一模一样的两种结果,现在能区分开了:引擎始终没有就绪,还是就绪之后又退出了。
仍未验证的部分: 用真实图片走完图片附件的端到端流程,以及 shell 工具(随包提供的 web 配置档不挂载终端行,并把两者都禁用)。这两项都不影响正常使用,都列在限制里。
限制
| 领域 | 限制 |
| 图片 | 替换用的附件服务商按原样存储字节,并从容器头部校验尺寸,但不做转码。手机照片按全分辨率发送,消耗 token,而且不应用 EXIF 方向。降采样应该放在 shell 的导入路径里(已计划,使用 Android 的 ImageDecoder)。 |
| 终端 | node-pty 垫片由 child_process 支撑:没有控制终端,所以没有行编辑、作业控制或 resize。在随包提供的 web 配置档里不付出任何代价,因为它不挂载终端行,并把两个 shell 工具都禁用。 |
| 沙箱 | 每个会话都以完全访问权限运行,没有审批提示,因为 Android 无法强制执行 DSH 沙箱。边界是操作系统沙箱和应用私有存储。不要把引擎暴露到设备之外。 |
| 体积 | 同时包含两种 ABI 约 233 MB,只含一种约 140 MB。最大的单项是 libicudata.so(31.6 MB),可以用 ICU 的 icupkg 裁剪到几 MB;其余是 Node(47 MB)和引擎目录树(165 MB)。 |
| 首次启动 | 解包 25k 个文件 / 约 165 MB,在真机上约 74 秒(从清空安装后的冷启动,实测)。先保证正确性;下一步是打包成单个归档。 |
| ABI | 仅 arm64-v8a 和 x86_64。没有 32 位构建。 |
| 通知 | 运行时从不请求 POST_NOTIFICATIONS,所以在 Android 13+ 上前台服务通知可能不出现。服务仍在运行。 |
设计说明
代码依赖这些约定,如果你要扩展它,值得保留:
1. 绝不分叉上游包。 每一处平台差异都放在 overlay、垫片或插件里。正因如此,移植才能跟上上游发布。
2. 补丁条目只能改 config 和 disabled。 要替换服务商,就禁用上游那一行,再插入你自己的。
3. 禁用某一行之前,先检查它的服务有没有被消费。 attachments 会级联;替换服务商才是正确做法。
4. 把 aapt2 的行为当成硬约束。 它会丢掉以点开头的条目,并写入宿主机路径分隔符。暂存脚本里「遇到点开头的导入就大声报错」这项检查,就是为了在构建时而不是在手机上抓到下一个同类问题。
5. 版本标记要能反映内容。 标记里包含目录树的文件数量和字节总数;以前只用包版本号,设备可能一直跑着过期的目录树,却没有任何提示。
6. 优先在设备上诊断,别靠猜。 run-as 加 pm path,再直接运行 libnode.so,几秒就能得到答案,而重新构建 APK 要花上几分钟——截图还能终结日志说不清的争论。
致谢
引擎、插件架构以及整个 UI 都
DeepSeek Harness by DeepSeek. This repository
contains only the Android packaging layer.
The Node runtime is the Termux build, which is the only
maintained prebuilt Node for Android new enough to run the engine.
License
The packaging layer in this repository is MIT. Upstream components keep their own licenses: DSH is
MIT, the Termux Node runtime and its bundled libraries (ICU, OpenSSL, libc++, …) retain theirs.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
yueshen0211/dsh-android
在手机上离线运行编码智能体,无需电脑
相关插件 · 开发 / 工程
隐私简历生成器reactive-resume/reactive-resume
Reactive Resume 已迁至 GitHub 上的…
★ 45,125已汉化MCP
71.7
AI 架构文档生成器sopaco/deepwiki-rs
🚀 Litho 已进化为 Terrain —— 给你的 AI 智能体一张会呼吸的代码库地图。
★ 3,157已汉化DSH
70.8
长时任务执行闭环AMAP-ML/LongHorizon-Harness
面向 Computer-Use Agent 的 Loop Engineering
★ 1,764已汉化DSH
70.5
AI 编程纪律方法包GanyuanRan/Aegis
让 AI 编程 agent 变得可信:少返工、更安全、说“完成”前先给证据。
★ 1,384已汉化DSH
69.6
多代理编排运行时Enderfga/claw-orchestrator
面向编程智能体的运行时。把 Claude Code、Codex、Antigravity、Grok…
★ 586已汉化MCP
69.4
记忆进化插件csyangwen/dsh-memory-evolve
dsh-memory-evolve 使用场景指南
★ 461已汉化DSH
67.9