让 DeepSeek Harness 在 Android / Termux 上跑起来

Clevebitr Lv3

DeepSeek Harness(dsh)是个跑在终端里的编码 Agent。它本身是跨平台的 Node.js 程序,但”跨平台”通常意味着 Windows / macOS / Linux——Android 不在名单里。

而 Termux 上的 Node.js 是真 Node.js,所以 dsh 能装上、能启动,然后在各种意想不到的地方炸掉。这个项目就是把每一处炸点固化成幂等可重放的补丁:

Github

Android 到底制造了哪些障碍

不是”Android 不兼容 Node”,而是三件事叠在一起:

  1. SELinux 禁止 link()
    app 数据目录下,裸 link() 稳定返回 EACCES(errno=-13),而 rename / open(wx) 正常。dsh 里所有”原子发布”的实现都默认走硬链接。

  2. bionic 而不是 glibc
    Termux 跑的是 Android 的 C 库。glibc 编译出来的预编译二进制一个都跑不了,原生模块要么为 bionic 重编,要么换实现。

  3. 没有 android-arm64 预编译
    node-pty、sharp 这些包不发布 Android 产物,而 node-gyp 源码编译需要 NDK 工具链。

再加上生态侧的问题:pnpm 的构建门禁与硬链接导入都会失败。

八个补丁

#目标在 Android 上的症状做法
1node-pty无 android-arm64 预编译,node-gyp 需要 NDK铺入自行构建的 pty.node;核心与 web profile(dsh-web 的 better-sidebar 也用)两处
2sharp原生 sharp 无法编译改装 @img/sharp-wasm32(版本须与 sharp 一致)
3node-addon-system/flockbionic 上原生 flock 加载失败换成 koffi FFI 走 libc flock
4dsh-session-persistence-jsonl / dsh-attachment-localSELinux 禁止 link(),新建文件 / 附件发布直接 EACCESlink 不可用时降级为 rename / 独占复制
5dsh 启动 wrapperweb profile 的 HMR 插件要求 --expose-internals$PREFIX/bin/dsh 包装脚本
6dsh-client-connection 的会话 cookie由别的 App 拉起浏览器(termux-open → ACTION_VIEW)时,303 跳转那一跳不带 SameSite=Strict cookie,必然落到 401SameSite=Strict → Lax
7dsh-fs-localwrite 工具创建新文件必失败:EACCES: permission denied, link ...link 不可用时先用 open(wx) 独占占名,再把已 fsync 的暂存文件 rename 过去
8dsh-session-persistence-jsonl/lib/worker.cjs同一个 publishCurrentExclusive 在 worker bundle 里没有降级分支原地注入同样的降级 + 锚点断言

挑几个最典型的说说。

补丁 7:为什么 write 工具建新文件必失败

这是最影响日常使用的一条。dsh 的 write 工具写文件走的是”原子发布”:

  • 覆盖已有文件 → 走 rename,Android 上正常
  • 创建新文件 → 走 link + 删除暂存,Android 上 EACCES

所以症状很迷惑:改文件没问题,新建文件必炸。

补丁的做法是保留语义的降级:先用 open(wx) 在目标路径上独占占名,再把已经 fsync 的暂存文件 rename 过去。

1
2
3
// link 不可用时的降级路径(并发保护不变)
// open(wx) 占名 → 目标已存在则 EEXIST → 仍然映射为 FS_NOT_OBSERVED
// rename 覆盖自己占下的名字 → 不会覆盖别人的字节

关键点是并发语义没有变松: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
2
bash apply.sh      # 全部步骤幂等;dsh 版本与 versions.txt 不一致时会警告并要求人工确认
dsh --version && dsh web --no-open --port 3085 # 验证

apply.sh 会把改动直接写进 node_modules,所以升级 dsh 之后需要重放。脚本对每个补丁都做了锚点 / 幂等检查:

  • 找不到锚点 → 报错退出,交人工核对(而不是默默跳过)
  • 已经打过 → 跳过
  • 最后一步断言硬链接降级覆盖完好 → 产物被上游还原时直接非零退出

版本不对时会先警告并要求确认,因为 JS 补丁可能不适用于新版 dsh:

1
2
3
dsh=0.1.5-rc.1
node-pty=1.2.0-beta.15
sharp=0.35.4

已验证的行为

以下是 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
2
3
dsh plugin --profile web add @linxin666/dsh-web-all@latest
dsh web --dump-config # 预检:bundle 层是否登记
dsh web --no-open --port 3085 # 真机验收:另起实例,不动 3080

前置条件:

  • 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 package
    • minimumReleaseAgeExclude: ['@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-ptyfalse(不编译)复用 android-arm64 预编译的 pty.node
cpu-featuresfalsenode-gyp 需要 NDK;ssh2 有纯 JS 回退
ssh2false同上,忽略后走 JS 实现,功能可用、速度略低
cloudflaredfalse无 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-pty 1.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 进行许可。