让 DeepSeek Harness 在 Android / Termux 上跑起来

DeepSeek Harness(dsh)是个跑在终端里的编码 Agent。它本身是跨平台的 Node.js 程序,但”跨平台”通常意味着 Windows / macOS / Linux——Android 不在名单里。
而 Termux 上的 Node.js 是真 Node.js,所以 dsh 能装上、能启动,然后在各种意想不到的地方炸掉。这个项目就是把每一处炸点固化成幂等可重放的补丁:
GithubAndroid 到底制造了哪些障碍
不是”Android 不兼容 Node”,而是三件事叠在一起:
SELinux 禁止
link()
app 数据目录下,裸link()稳定返回EACCES(errno=-13),而rename/open(wx)正常。dsh 里所有”原子发布”的实现都默认走硬链接。bionic 而不是 glibc
Termux 跑的是 Android 的 C 库。glibc 编译出来的预编译二进制一个都跑不了,原生模块要么为 bionic 重编,要么换实现。没有 android-arm64 预编译
node-pty、sharp这些包不发布 Android 产物,而node-gyp源码编译需要 NDK 工具链。
再加上生态侧的问题:pnpm 的构建门禁与硬链接导入都会失败。
八个补丁
| # | 目标 | 在 Android 上的症状 | 做法 |
|---|---|---|---|
| 1 | node-pty | 无 android-arm64 预编译,node-gyp 需要 NDK | 铺入自行构建的 pty.node;核心与 web profile(dsh-web 的 better-sidebar 也用)两处 |
| 2 | sharp | 原生 sharp 无法编译 | 改装 @img/sharp-wasm32(版本须与 sharp 一致) |
| 3 | node-addon-system/flock | bionic 上原生 flock 加载失败 | 换成 koffi FFI 走 libc flock |
| 4 | dsh-session-persistence-jsonl / dsh-attachment-local | SELinux 禁止 link(),新建文件 / 附件发布直接 EACCES | link 不可用时降级为 rename / 独占复制 |
| 5 | dsh 启动 wrapper | web profile 的 HMR 插件要求 --expose-internals | $PREFIX/bin/dsh 包装脚本 |
| 6 | dsh-client-connection 的会话 cookie | 由别的 App 拉起浏览器(termux-open → ACTION_VIEW)时,303 跳转那一跳不带 SameSite=Strict cookie,必然落到 401 | SameSite=Strict → Lax |
| 7 | dsh-fs-local | write 工具创建新文件必失败:EACCES: permission denied, link ... | link 不可用时先用 open(wx) 独占占名,再把已 fsync 的暂存文件 rename 过去 |
| 8 | dsh-session-persistence-jsonl/lib/worker.cjs | 同一个 publishCurrentExclusive 在 worker bundle 里没有降级分支 | 原地注入同样的降级 + 锚点断言 |
挑几个最典型的说说。
补丁 7:为什么 write 工具建新文件必失败
这是最影响日常使用的一条。dsh 的 write 工具写文件走的是”原子发布”:
- 覆盖已有文件 → 走
rename,Android 上正常 - 创建新文件 → 走
link+ 删除暂存,Android 上EACCES
所以症状很迷惑:改文件没问题,新建文件必炸。
补丁的做法是保留语义的降级:先用 open(wx) 在目标路径上独占占名,再把已经 fsync 的暂存文件 rename 过去。
1 | // link 不可用时的降级路径(并发保护不变) |
关键点是并发语义没有变松:open(wx) 的 EEXIST 依然会映射成”文件未被观测到”,所以”不覆盖他人字节”的保证还在。
补丁 6:那个必然出现的 401
Android 上 dsh web 的自动打开是”别的 App 拉起浏览器“(termux-open → ACTION_VIEW),这属于外部发起的顶层导航。在这种导航里,303 跳到 / 的那一跳不会带上 SameSite=Strict 的 cookie,于是必然落到:
1 | dsh web authentication required; reopen the URL printed by dsh web |
改成 SameSite=Lax 即可。安全性影响面限于 $DSH_HOME/.credentials.yaml 里那把密钥签发的会话 cookie 的携带条件——服务仍然只绑 127.0.0.1,API 另有 Host / Origin 围栏。
补丁 8:防”升级后静默复活”
session-persistence-jsonl 有两份产物:lib/index.js 和 lib/worker.cjs(迁移校验 worker 的整图 bundle)。第二份里也带着一份 publishCurrentExclusive,但没有降级分支。
当前这个 worker 只做只读校验、不发布,所以那份副本暂时不可达——属于”升级后可能静默复活成 EACCES“的隐患。
所以除了补上降级,还加了一条针对产物本身的断言:
任何
link()发布点后方 12 行内必须出现降级分支,否则中止重放。
1 | node check-session-publish-fallback.mjs |
断言检查的是产物,而不是”补丁脚本跑过没有”。所以 npm 重装把上游代码原样带回来时,apply.sh 会以非零退出中止,而不是留着一个只在真机上才炸的缺口。
用起来是什么样
1 | bash apply.sh # 全部步骤幂等;dsh 版本与 versions.txt 不一致时会警告并要求人工确认 |
apply.sh 会把改动直接写进 node_modules,所以升级 dsh 之后需要重放。脚本对每个补丁都做了锚点 / 幂等检查:
- 找不到锚点 → 报错退出,交人工核对(而不是默默跳过)
- 已经打过 → 跳过
- 最后一步断言硬链接降级覆盖完好 → 产物被上游还原时直接非零退出
版本不对时会先警告并要求确认,因为 JS 补丁可能不适用于新版 dsh:
1 | dsh=0.1.5-rc.1 |
已验证的行为
以下是 2026-09、dsh 0.1.5-rc.1 上的实测结论:
- 本机 app 数据目录内裸
link()稳定返回EACCES(errno=-13),rename/open(wx)正常——补丁前提成立。 flock补丁(koffi → libc)跨进程互斥有效:A 持锁期间 B、C 均EAGAIN(errno=11)。- 把两份产物里的
publishCurrentExclusive抠出来用真实 fs 驱动(即link必然EACCES的路径):- 目标不存在 → 降级
rename发布成功、暂存被清理 - 目标已存在 → 返回
false且不覆盖他人字节,独占语义不变
- 目标不存在 → 降级
- 会话载入(
open(id,"read"))不发布、不写盘、不 spawn 校验 worker;旧格式会话的迁移是惰性的,只有以write打开才会落盘发布。 dsh web --port 3085干净启动,无缺包 / 重复 id 告警;node-pty真机 spawn PTY 成功(PTY_OK_aarch64)。
插件生态的额外裁决
装 dsh-web 插件全家桶时还有一批坑,也一并记录在仓库里:
1 | dsh plugin --profile web add @linxin666/dsh-web-all@latest |
前置条件:
- pnpm:
dsh plugin只是 pnpm 转发器,缺 pnpm 直接退出 127。 - .npmrc 的导入方式:设为
package-import-method=copy。SELinux 禁止在 app 数据目录link(),pnpm 默认 hardlink 导入会失败(跟补丁 1 同一根因)。 - pnpm-workspace.yaml:
nodeLinker: hoisted—— 避免 isolated 布局把@linxin666/*子包收进嵌套目录导致Cannot find packageminimumReleaseAgeExclude: ['@linxin666/*']—— pnpm 11 内置的发布年龄门禁会静默装回旧版皮肤插件overrides: node-pty: 1.2.0-beta.15—— 与核心对齐(better-sidebar 的终端修复也要求同版本)allowBuilds—— pnpm 11 只认这个布尔映射;pnpm 10 的ignoredBuiltDependencies等键在 11 里被静默忽略,不写就会被ERR_PNPM_IGNORED_BUILDS拦成非 0,进而使dsh plugin跳过 bundle 层登记(插件装了但界面不出现)
原生依赖的裁决:
| 包 | 裁决 | 原因 |
|---|---|---|
node-pty | false(不编译) | 复用 android-arm64 预编译的 pty.node |
cpu-features | false | node-gyp 需要 NDK;ssh2 有纯 JS 回退 |
ssh2 | false | 同上,忽略后走 JS 实现,功能可用、速度略低 |
cloudflared | false | 无 android 预编译,且 Termux 是 bionic,跑不了 glibc 二进制 → 公网隧道不可用,局域网配对仍可用 |
最后一条值得单独提醒:在 Termux 上别指望 dsh web 的公网隧道,cloudflared 在这套环境下跑不起来。
插件都装好之后,如果想把界面配色也换掉:全家桶里的皮肤中心支持纯资产皮肤,我另外做了 20 款 base16 方案 —— 给 DSH Web GUI 换上 20 款 base16 配色。
来源与许可
session-persistence-jsonl.index.js、attachment-local.index.js派生自 MIT 许可的@deepseek-ai/dsh-session-persistence-jsonl、@deepseek-ai/dsh-attachment-local(版权归 DeepSeek)。flock.js派生自 BSD-3-Clause 许可的@deepseek-ai/node-addon-system。pty.node由 MIT 许可的 node-pty1.2.0-beta.15为 android-arm64 构建。- 其余脚本与文档:MIT。
补丁改的是
node_modules里的产物,所以请把apply.sh当成升级流程的一部分——每次npm install或升级 dsh 之后都重放一次。
- 标题: 让 DeepSeek Harness 在 Android / Termux 上跑起来
- 作者: Clevebitr
- 创建于 : 2026-09-14 11:40:00
- 更新于 : 2026-09-14 11:25:50
- 链接: https://blog.clevebitr.dpdns.org/2026/09/14/让DeepSeek-Harness在Android-Termux上跑起来/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。