dsh-pad-kit 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ # dsh-pad-kit 电脑端安装器(Windows)
2
+ # 用法:右键「使用 PowerShell 运行」,或在 PowerShell 里:
3
+ # powershell -ExecutionPolicy Bypass -File .\install-windows.ps1
4
+ # 作用:把插件放到 %USERPROFILE%\.dsh\plugins\、建 junction、在 profile 里登记、追加激活行。
5
+ # 幂等:重复运行只做补齐,不会重复写入。
6
+
7
+ param(
8
+ [string]$Profile = 'web',
9
+ [string]$DshHome = (Join-Path $env:USERPROFILE '.dsh')
10
+ )
11
+
12
+ $ErrorActionPreference = 'Stop'
13
+ $pkgName = '@local/dsh-pad-kit'
14
+ $pluginDirName = 'dsh-pad-kit'
15
+ $src = $PSScriptRoot
16
+
17
+ function Say($msg) { Write-Host $msg }
18
+ function Fail($msg) { Write-Host "错误:$msg" -ForegroundColor Red; exit 1 }
19
+
20
+ Say "==> 1/5 检查来源目录"
21
+ $needed = @('package.json', 'cordis.patch.yml', 'lib\plugin.js', 'dsh-android-kit.tgz', 'kit\termux.apk')
22
+ foreach ($f in $needed) {
23
+ if (-not (Test-Path (Join-Path $src $f))) { Fail "来源里缺少 $f(请在解压后的 dsh-pad-kit 目录里运行本脚本)" }
24
+ }
25
+ Say " 来源 OK:$src"
26
+
27
+ Say "==> 2/5 复制到 $DshHome\plugins\$pluginDirName"
28
+ $pluginsRoot = Join-Path $DshHome 'plugins'
29
+ $target = Join-Path $pluginsRoot $pluginDirName
30
+ New-Item -ItemType Directory -Force -Path $pluginsRoot | Out-Null
31
+ if (Test-Path $target) {
32
+ foreach ($f in $needed) {
33
+ $dstFile = Join-Path $target $f
34
+ New-Item -ItemType Directory -Force -Path (Split-Path $dstFile) | Out-Null
35
+ Copy-Item (Join-Path $src $f) $dstFile -Force
36
+ }
37
+ Say " 已存在,已就地更新关键文件"
38
+ } else {
39
+ Copy-Item $src $target -Recurse -Force
40
+ Say " 已复制"
41
+ }
42
+
43
+ Say "==> 3/5 建 junction(让 node 能按包名解析)"
44
+ $profileDir = Join-Path $DshHome "profiles\$Profile"
45
+ if (-not (Test-Path $profileDir)) { Fail "找不到 profile 目录:$profileDir(DSH 至少启动过一次吗?)" }
46
+ $linkDir = Join-Path $profileDir 'node_modules\@local'
47
+ New-Item -ItemType Directory -Force -Path $linkDir | Out-Null
48
+ $link = Join-Path $linkDir $pluginDirName
49
+ if (Test-Path $link) {
50
+ (Get-Item $link).Delete()
51
+ Say " 旧链接已移除"
52
+ }
53
+ cmd /c mklink /J "`"$link`"" "`"$target`"" | Out-Null
54
+ if (-not (Test-Path $link)) { Fail "junction 创建失败:$link" }
55
+ Say " OK:$link"
56
+
57
+ Say "==> 4/5 在 profile package.json 里登记依赖"
58
+ $pkgFile = Join-Path $profileDir 'package.json'
59
+ if (-not (Test-Path $pkgFile)) { Fail "找不到 $pkgFile" }
60
+ $backup = "$pkgFile.bak-pad-kit"
61
+ Copy-Item $pkgFile $backup -Force
62
+ $json = Get-Content $pkgFile -Raw -Encoding UTF8 | ConvertFrom-Json
63
+ if (-not $json.dependencies) { $json | Add-Member -NotePropertyName dependencies -NotePropertyValue ([pscustomobject]@{}) }
64
+ $wanted = "link:$($target -replace '\\', '/')"
65
+ if ($json.dependencies.PSObject.Properties.Name -contains $pkgName) {
66
+ $json.dependencies.$pkgName = $wanted
67
+ } else {
68
+ $json.dependencies | Add-Member -NotePropertyName $pkgName -NotePropertyValue $wanted
69
+ }
70
+ $out = ConvertTo-Json -InputObject $json -Depth 20
71
+ [System.IO.File]::WriteAllText($pkgFile, $out, (New-Object System.Text.UTF8Encoding($false)))
72
+ Say " OK(备份:$backup)"
73
+
74
+ Say "==> 5/5 在 cordis.patch.yml 追加激活行"
75
+ $patchFile = Join-Path $profileDir 'cordis.patch.yml'
76
+ if (-not (Test-Path $patchFile)) { Fail "找不到 $patchFile" }
77
+ Copy-Item $patchFile "$patchFile.bak-pad-kit" -Force
78
+ $text = [System.IO.File]::ReadAllText($patchFile)
79
+ if ($text -match '(?m)^\s*-\s*id:\s*pad-kit\s*$') {
80
+ Say " 已存在 pad-kit 行,跳过"
81
+ } else {
82
+ $block = @"
83
+
84
+ # dsh-pad-kit:平板上跑 DSH + 电脑 SSH 远程管理
85
+ - insert:
86
+ - id: pad-kit
87
+ name: '$pkgName'
88
+ config:
89
+ httpPort: 8090
90
+ apkName: termux.apk
91
+ timeoutMs: 120000
92
+ "@
93
+ [System.IO.File]::WriteAllText($patchFile, ($text.TrimEnd() + $block + "`n"), (New-Object System.Text.UTF8Encoding($false)))
94
+ Say " 已追加"
95
+ }
96
+
97
+ Write-Host ''
98
+ Write-Host '完成。还有两步是必须人工做的:' -ForegroundColor Green
99
+ Write-Host ' 1) 重启 DSH(新增插件行不会热加载),重启后工具列表里会出现 17 个 tablet_* 工具'
100
+ Write-Host ' 2) 首次装平板时,防火墙要放行一次 8090(会弹窗,选「允许」)'
101
+ Write-Host ''
102
+ Write-Host '下一步:在 DSH 里调用 tablet_install_page,按返回的地址在平板上操作。'
@@ -0,0 +1,102 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ # DSH 平板端引导:自动判断环境(有没有 Termux、curl 好不好、ssh 有没有)再决定怎么走
3
+ set -u
4
+ BASE="__BASE__"
5
+ PUBKEY="__PUBKEY__"
6
+ step() { printf '\n[dsh-kit] %s\n' "$*"; }
7
+ die() { printf '\n[dsh-kit] 失败:%s\n' "$*"; exit 1; }
8
+
9
+ # ---------- 0. 环境判断:这里如果不是 Termux,直接给出结论 ----------
10
+ step "0/5 环境检查"
11
+ if [ -z "${PREFIX:-}" ] || [ ! -d "/data/data/com.termux" ]; then
12
+ die "这里不是 Termux 环境。请先在平板装 Termux(安装页上有 APK),再在 Termux 里运行这一行。"
13
+ fi
14
+ command -v pkg >/dev/null 2>&1 || die "找不到 pkg 命令:Termux 版本过旧(Play 商店版)。请卸载后装 F-Droid/GitHub 版 0.118+。"
15
+ ARCH="$(uname -m)"
16
+ case "$ARCH" in
17
+ aarch64|arm64) ;;
18
+ *) die "只支持 arm64 平板,当前架构是 $ARCH。" ;;
19
+ esac
20
+ SDK="$(getprop ro.build.version.sdk 2>/dev/null || echo 0)"
21
+ case "$SDK" in ''|*[!0-9]*) SDK=0 ;; esac
22
+ [ "$SDK" -ge 24 ] || die "需要 Android 7.0 及以上(当前 SDK=$SDK)。"
23
+ FREE_MB="$(df -k /data 2>/dev/null | awk 'NR==2{print int($4/1024)}')"
24
+ case "$FREE_MB" in ''|*[!0-9]*) FREE_MB=0 ;; esac
25
+ [ "$FREE_MB" -ge 1500 ] || die "空间不足:/data 只剩 ${FREE_MB}MB,至少要 1.5GB。"
26
+ echo " Termux=$PREFIX"
27
+ echo " 架构=$ARCH Android SDK=$SDK 剩余空间=${FREE_MB}MB"
28
+
29
+ # ---------- 1. 先保证 curl 可用,否则后面什么都下不了 ----------
30
+ step "1/5 检查 curl 与网络"
31
+ if ! curl --version >/dev/null 2>&1; then
32
+ echo " curl 不可用(常见:libssl 符号缺失),先整体升级一版"
33
+ apt update >/dev/null 2>&1 || true
34
+ apt full-upgrade -y -o Dpkg::Options::="--force-confnew" >/dev/null 2>&1 || true
35
+ curl --version >/dev/null 2>&1 || die "curl 仍然不可用。请手动执行:apt update && apt full-upgrade -y 之后重跑这一行。"
36
+ echo " curl 已修复:$(curl --version | head -n1)"
37
+ fi
38
+ if ! curl -fsS -m 8 "$BASE/healthz" >/dev/null 2>&1; then
39
+ die "连不上电脑 $BASE。三条常见原因:①两台设备不在同一 Wi-Fi ②路由器开了 AP 隔离/访客网络 ③电脑防火墙拦了 8090。"
40
+ fi
41
+ echo " 已连通电脑:$BASE"
42
+
43
+ # ---------- 2. 装 openssh(缺包时换官方源重试) ----------
44
+ step "2/5 安装 openssh"
45
+ if ! command -v sshd >/dev/null 2>&1; then
46
+ pkg install -y openssh >/dev/null 2>&1 || true
47
+ fi
48
+ if ! command -v sshd >/dev/null 2>&1; then
49
+ echo " 默认源里没有 openssh,切到官方源重试"
50
+ echo "deb https://packages.termux.dev/apt/termux-main stable main" > "$PREFIX/etc/apt/sources.list"
51
+ apt update >/dev/null 2>&1 || true
52
+ apt install -y openssh >/dev/null 2>&1 || true
53
+ fi
54
+ command -v sshd >/dev/null 2>&1 || die "openssh 装不上。手动执行:pkg install openssh"
55
+ echo " sshd=$(command -v sshd)"
56
+
57
+ # ---------- 3. 公钥 + 起服务 ----------
58
+ step "3/5 配置 SSH 免密登录"
59
+ mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
60
+ touch "$HOME/.ssh/authorized_keys"; chmod 600 "$HOME/.ssh/authorized_keys"
61
+ grep -qF "${PUBKEY%% *}" "$HOME/.ssh/authorized_keys" 2>/dev/null || echo "$PUBKEY" >> "$HOME/.ssh/authorized_keys"
62
+ [ -f "$PREFIX/etc/ssh/ssh_host_ed25519_key" ] || ssh-keygen -A >/dev/null 2>&1
63
+ pkill sshd 2>/dev/null; sleep 1
64
+ nohup sshd >/dev/null 2>&1
65
+ sleep 1
66
+ if pgrep sshd >/dev/null 2>&1; then echo " sshd 已启动(8022)"; else echo " ! sshd 没起来,电脑端可能连不上"; fi
67
+
68
+ # ---------- 4. 报到:电脑据此自动登记这台平板 ----------
69
+ step "4/5 向电脑报到"
70
+ USER_NAME="$(whoami)"
71
+ if curl -fsS "$BASE/register?user=$USER_NAME&port=8022" >/dev/null 2>&1; then
72
+ echo " 已报到:$USER_NAME @ 8022"
73
+ else
74
+ echo " 报到失败(不影响平板本地使用,只是电脑端暂时不能远程操作这台平板)"
75
+ fi
76
+
77
+ # ---------- 开机自启脚本(由 Termux:Boot 触发,装不装 Boot 应用都能先放好) ----------
78
+ mkdir -p "$HOME/.termux/boot"
79
+ cat > "$HOME/.termux/boot/start-dsh.sh" <<BOOTEOF
80
+ #!/data/data/com.termux/files/usr/bin/bash
81
+ export HOME=/data/data/com.termux/files/home
82
+ export PREFIX=/data/data/com.termux/files/usr
83
+ export PATH=\$PREFIX/bin:\$PATH
84
+ {
85
+ echo "=== boot \$(date '+%Y-%m-%d %H:%M:%S') ==="
86
+ command -v termux-wake-lock >/dev/null 2>&1 && termux-wake-lock
87
+ sleep 8
88
+ [ -x "\$HOME/start-dsh.sh" ] && bash "\$HOME/start-dsh.sh" || echo "missing \$HOME/start-dsh.sh"
89
+ } >> "\$HOME/dsh-boot.log" 2>&1
90
+ BOOTEOF
91
+ chmod +x "$HOME/.termux/boot/start-dsh.sh"
92
+ echo " 开机脚本已放好:~/.termux/boot/start-dsh.sh(装了 Termux:Boot 才会自动执行)"
93
+
94
+ __SKIP_DSH__
95
+
96
+ # ---------- 5. 装 DSH 本体 ----------
97
+ step "5/5 安装 DSH 本体(node → dsh → 原生绑定 → 两个安卓补丁 → 起服务)"
98
+ cd "$HOME" || die "无法进入 HOME 目录"
99
+ curl -fL -o dsh-android-kit.tgz "$BASE/kit.tgz" || die "下载套件失败,检查网络后重跑"
100
+ rm -rf "$HOME/dsh-android-kit"
101
+ tar -xzf dsh-android-kit.tgz -C "$HOME" || die "解压失败"
102
+ bash "$HOME/dsh-android-kit/setup.sh"
@@ -0,0 +1,33 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+
4
+ const DIR = '/data/data/com.termux/files/home/dshpad';
5
+
6
+ function currentUrl() {
7
+ try {
8
+ const t = fs.readFileSync(DIR + '/url.txt', 'utf8').trim();
9
+ if (t.startsWith('http')) return t;
10
+ } catch (e) {}
11
+ try {
12
+ const log = fs.readFileSync(DIR + '/web.log', 'utf8');
13
+ const m = log.match(/http:\/\/127\.0\.0\.1:3080\/\?token=[A-Za-z0-9_-]+/g);
14
+ if (m && m.length) return m[m.length - 1];
15
+ } catch (e) {}
16
+ return null;
17
+ }
18
+
19
+ http.createServer((req, res) => {
20
+ if (req.url.startsWith('/healthz')) {
21
+ res.writeHead(200, { 'content-type': 'text/plain' });
22
+ res.end('ok');
23
+ return;
24
+ }
25
+ const target = currentUrl();
26
+ if (!target) {
27
+ res.writeHead(503, { 'content-type': 'text/html; charset=utf-8' });
28
+ res.end('<meta name="viewport" content="width=device-width,initial-scale=1"><body style="font:20px sans-serif;padding:24px">DSH 还没有启动。<br>请在 Termux 里运行:<br><code>bash ~/start-dsh.sh</code></body>');
29
+ return;
30
+ }
31
+ res.writeHead(302, { location: target, 'cache-control': 'no-store' });
32
+ res.end();
33
+ }).listen(3081, '127.0.0.1', () => console.log('HOME LISTENING 3081'));
@@ -0,0 +1,56 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ # 把编译好的 require_builtin.node 安装成 node-addon-require-builtin-android-arm64 可选包
3
+ # 用法:install-addon.sh <require_builtin.node 路径> <目标目录>
4
+ set -eu
5
+ BIN="${1:?需要 require_builtin.node 路径}"
6
+ TARGET="${2:?需要目标目录}"
7
+
8
+ [ -f "$BIN" ] || { echo "找不到 $BIN"; exit 1; }
9
+
10
+ rm -rf "$TARGET"
11
+ mkdir -p "$TARGET/prebuilt"
12
+ cp "$BIN" "$TARGET/prebuilt/android-arm64-napi-v9.node"
13
+ chmod 755 "$TARGET/prebuilt/android-arm64-napi-v9.node"
14
+
15
+ cat > "$TARGET/package.json" <<'EOF'
16
+ {
17
+ "name": "node-addon-require-builtin-android-arm64",
18
+ "version": "0.1.9",
19
+ "description": "Locally built android-arm64 native addon package for node-addon-require-builtin.",
20
+ "main": "./index.js",
21
+ "os": ["android"],
22
+ "cpu": ["arm64"],
23
+ "engines": { "node": ">=20" },
24
+ "files": ["index.js", "prebuilds.json", "prebuilt/"],
25
+ "dependencies": { "node-addon-native-custom-loader": "*" },
26
+ "license": "MIT"
27
+ }
28
+ EOF
29
+
30
+ cat > "$TARGET/prebuilds.json" <<'EOF'
31
+ {
32
+ "platform": "android-arm64",
33
+ "binaries": [
34
+ {
35
+ "backend": "napi",
36
+ "abi": "napi-v9",
37
+ "binaryTag": "napi-v9",
38
+ "node": ">=20",
39
+ "path": "prebuilt/android-arm64-napi-v9.node"
40
+ }
41
+ ]
42
+ }
43
+ EOF
44
+
45
+ cat > "$TARGET/index.js" <<'EOF'
46
+ 'use strict';
47
+ const { loadPrebuild } = require('node-addon-native-custom-loader');
48
+ module.exports = loadPrebuild(__dirname);
49
+ EOF
50
+
51
+ echo "[install-addon] 已安装到 $TARGET"
52
+ node -e "
53
+ const addon = require(process.argv[1]);
54
+ console.log('[install-addon] requireBuiltin=' + typeof addon.requireBuiltin);
55
+ console.log('[install-addon] info=' + JSON.stringify(addon.getNativeBindingInfo()));
56
+ " "$TARGET" 2>&1 || true
Binary file
@@ -0,0 +1,75 @@
1
+ #!/data/data/com.termux/files/usr/bin/node
2
+ /*
3
+ * Android 兼容补丁 1/2:flock
4
+ *
5
+ * 官方 @deepseek-ai/node-addon-system 没有发布 android-arm64 的编译产物,
6
+ * 源码里写死 platform !== 'linux' && platform !== 'darwin' 就抛错。
7
+ * 这里把 Android 视为支持平台,并把锁降级为无操作(单实例本地运行不需要跨进程锁)。
8
+ *
9
+ * 幂等:已打过就跳过。
10
+ */
11
+ 'use strict';
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+ const { execSync } = require('node:child_process');
15
+
16
+ const MARK = 'PATCHED-ANDROID-FLOCK';
17
+
18
+ function dshRoot() {
19
+ const cands = [];
20
+ if (process.env.PREFIX) cands.push(path.join(process.env.PREFIX, 'lib/node_modules/@deepseek-ai/dsh'));
21
+ try {
22
+ const root = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
23
+ if (root) cands.push(path.join(root, '@deepseek-ai/dsh'));
24
+ } catch (e) { /* npm 不可用就靠下面的候选 */ }
25
+ cands.push('/data/data/com.termux/files/usr/lib/node_modules/@deepseek-ai/dsh');
26
+ for (const candidate of cands) {
27
+ if (candidate && fs.existsSync(path.join(candidate, 'node_modules/@deepseek-ai/node-addon-system'))) return candidate;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ const root = dshRoot();
33
+ if (!root) {
34
+ console.error('[patch-flock] 找不到 dsh 安装目录');
35
+ process.exit(1);
36
+ }
37
+
38
+ const file = path.join(root, 'node_modules/@deepseek-ai/node-addon-system/lib/flock.js');
39
+ if (!fs.existsSync(file)) {
40
+ console.error('[patch-flock] 找不到 ' + file);
41
+ process.exit(1);
42
+ }
43
+
44
+ let src = fs.readFileSync(file, 'utf8');
45
+ if (src.includes(MARK)) {
46
+ console.log('[patch-flock] 已打过补丁,跳过');
47
+ process.exit(0);
48
+ }
49
+
50
+ const anchorGuard = "if (platform !== 'linux' && platform !== 'darwin') {";
51
+ const anchorLock = 'export async function tryLockExclusive(fd) {';
52
+
53
+ if (!src.includes(anchorGuard) || !src.includes(anchorLock)) {
54
+ console.error('[patch-flock] 代码结构变了,找不到锚点,请手动检查 ' + file);
55
+ process.exit(1);
56
+ }
57
+
58
+ src = src.replace(anchorGuard, "if (platform !== 'linux' && platform !== 'darwin' && platform !== 'android') {");
59
+
60
+ const lockHead = anchorLock + '\n const errno = await new Promise((resolve) => {';
61
+ if (src.includes(lockHead)) {
62
+ src = src.replace(
63
+ lockHead,
64
+ anchorLock + '\n'
65
+ + ' // ' + MARK + ': Android 没有官方 flock 原生绑定,单实例本地运行下退化为无锁。\n'
66
+ + " if (process.platform === 'android')\n return;\n"
67
+ + ' const errno = await new Promise((resolve) => {',
68
+ );
69
+ } else {
70
+ console.error('[patch-flock] tryLockExclusive 结构不匹配,请手动检查');
71
+ process.exit(1);
72
+ }
73
+
74
+ fs.writeFileSync(file, src, 'utf8');
75
+ console.log('[patch-flock] 已打补丁:' + file + ' (' + Buffer.byteLength(src) + ' bytes)');
@@ -0,0 +1,133 @@
1
+ #!/data/data/com.termux/files/usr/bin/node
2
+ /*
3
+ * Android 兼容补丁 2/2:硬链接
4
+ *
5
+ * Android 的 /data 分区禁止 hardlink()(实测:ln 返回 Permission denied),
6
+ * 而 DSH 的会话持久化用 link() 做原子发布。这里把 link() 包成
7
+ * 「先试 link,遇到 EACCES/EPERM/ENOSYS/ENOTSUP/EOPNOTSUPP 就退化成 copyFile(...,1)」,
8
+ * 1 = COPYFILE_EXCL,保持原来「目标已存在就 EEXIST」的语义。
9
+ *
10
+ * 幂等:已打过就跳过。
11
+ */
12
+ 'use strict';
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+ const { execSync } = require('node:child_process');
16
+
17
+ const MARK = 'COPYFILE_EXCL: Android forbids hard links';
18
+ const REL = 'node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib';
19
+
20
+ function dshRoot() {
21
+ const cands = [];
22
+ if (process.env.PREFIX) cands.push(path.join(process.env.PREFIX, 'lib/node_modules/@deepseek-ai/dsh'));
23
+ try {
24
+ const root = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
25
+ if (root) cands.push(path.join(root, '@deepseek-ai/dsh'));
26
+ } catch (e) { /* 忽略,用下面的候选 */ }
27
+ cands.push('/data/data/com.termux/files/usr/lib/node_modules/@deepseek-ai/dsh');
28
+ for (const candidate of cands) {
29
+ if (candidate && fs.existsSync(path.join(candidate, REL))) return candidate;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function fallback(ind, srcExpr, dstExpr, copyExpr) {
35
+ const t = ind + '\t';
36
+ const codes = ['EACCES', 'EPERM', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP'];
37
+ const cond = codes.map((c) => '__dshCode !== "' + c + '"').join(' && ');
38
+ return [
39
+ ind + 'try {',
40
+ t + 'await internals.fs.link(' + srcExpr + ', ' + dstExpr + ');',
41
+ ind + '} catch (__dshLinkError) {',
42
+ t + 'const __dshCode = __dshLinkError && __dshLinkError.code;',
43
+ t + 'if (' + cond + ') throw __dshLinkError;',
44
+ t + 'await ' + copyExpr + '(' + srcExpr + ', ' + dstExpr + ', 1); // ' + MARK,
45
+ ind + '}',
46
+ ].join('\n');
47
+ }
48
+
49
+ const root = dshRoot();
50
+ if (!root) {
51
+ console.error('[patch-link] 找不到 dsh-session-persistence-jsonl,先装好 @deepseek-ai/dsh');
52
+ process.exit(1);
53
+ }
54
+ const BASE = path.join(root, REL);
55
+ const IDX = path.join(BASE, 'index.js');
56
+ const WRK = path.join(BASE, 'worker.cjs');
57
+
58
+ let failed = false;
59
+ const report = [];
60
+
61
+ // ---- index.js ----
62
+ try {
63
+ let src = fs.readFileSync(IDX, 'utf8');
64
+ if (src.includes(MARK)) {
65
+ report.push('index.js 已打过补丁,跳过');
66
+ } else {
67
+ const notes = [];
68
+ if (src.includes('import { link, lstat,')) {
69
+ src = src.replace('import { link, lstat,', 'import { copyFile, link, lstat,');
70
+ notes.push('补上 copyFile 导入');
71
+ } else if (src.includes('import { copyFile, link, lstat,')) {
72
+ notes.push('copyFile 导入已存在');
73
+ } else {
74
+ throw new Error('找不到 import { link, lstat, 锚点');
75
+ }
76
+
77
+ const site1 = /^([ \t]*)await internals\.fs\.link\(staged, currentPath\);$/m;
78
+ if (site1.test(src)) {
79
+ src = src.replace(site1, (m, ind) => fallback(ind, 'staged', 'currentPath', 'copyFile'));
80
+ notes.push('publishCurrentExclusive 已打补丁');
81
+ }
82
+
83
+ const site2 = /^([ \t]*)await link\(tmp, finalPath\);$/m;
84
+ if (site2.test(src)) {
85
+ src = src.replace(site2, (m, ind) => {
86
+ const t = ind + '\t';
87
+ const codes = ['EACCES', 'EPERM', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP'];
88
+ const cond = codes.map((c) => '__dshCode !== "' + c + '"').join(' && ');
89
+ return [
90
+ ind + 'try {',
91
+ t + 'await link(tmp, finalPath);',
92
+ ind + '} catch (__dshLinkError) {',
93
+ t + 'const __dshCode = __dshLinkError && __dshLinkError.code;',
94
+ t + 'if (' + cond + ') throw __dshLinkError;',
95
+ t + 'await copyFile(tmp, finalPath, 1); // ' + MARK,
96
+ ind + '}',
97
+ ].join('\n');
98
+ });
99
+ notes.push('materializePosix 已打补丁');
100
+ }
101
+
102
+ if (notes.length === 0) throw new Error('两处 link 锚点都没找到');
103
+ fs.writeFileSync(IDX, src, 'utf8');
104
+ report.push('index.js:' + notes.join(',') + '(' + Buffer.byteLength(src) + ' 字节)');
105
+ }
106
+ } catch (e) {
107
+ report.push('index.js 失败:' + e.message);
108
+ failed = true;
109
+ }
110
+
111
+ // ---- worker.cjs(可能不存在)----
112
+ try {
113
+ if (!fs.existsSync(WRK)) {
114
+ report.push('worker.cjs 不存在,跳过');
115
+ } else {
116
+ let src = fs.readFileSync(WRK, 'utf8');
117
+ if (src.includes(MARK)) {
118
+ report.push('worker.cjs 已打过补丁,跳过');
119
+ } else {
120
+ const site1 = /^([ \t]*)await internals\.fs\.link\(staged, currentPath\);$/m;
121
+ if (!site1.test(src)) throw new Error('找不到 link(staged, currentPath) 锚点');
122
+ src = src.replace(site1, (m, ind) => fallback(ind, 'staged', 'currentPath', 'node_fs_promises.copyFile'));
123
+ fs.writeFileSync(WRK, src, 'utf8');
124
+ report.push('worker.cjs 已打补丁(' + Buffer.byteLength(src) + ' 字节)');
125
+ }
126
+ }
127
+ } catch (e) {
128
+ report.push('worker.cjs 失败:' + e.message);
129
+ failed = true;
130
+ }
131
+
132
+ for (const line of report) console.log('[patch-link] ' + line);
133
+ process.exit(failed ? 1 : 0);
package/kit/setup.sh ADDED
@@ -0,0 +1,125 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # DSH Android (Termux) 一键安装器
4
+ # 在平板的 Termux 里执行:bash setup.sh
5
+ # 作用:装 node → 装 dsh → 编译 android-arm64 原生绑定 → 打 3 个补丁
6
+ # → 起服务(3080)+ 固定入口(3081)→ 打印可打开的网址
7
+ # ---------------------------------------------------------------------------
8
+ set -u
9
+ KIT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ HOME_DIR="${HOME:-/data/data/com.termux/files/home}"
11
+ PREFIX_DIR="${PREFIX:-/data/data/com.termux/files/usr}"
12
+
13
+ LOGDIR="$HOME_DIR/.dsh-kit/logs"
14
+ mkdir -p "$LOGDIR" 2>/dev/null
15
+
16
+ log() { printf '\n[kit] %s\n' "$*"; }
17
+ warn() { printf '[kit] ! %s\n' "$*"; }
18
+ fail() { printf '\n[kit] 失败:%s\n' "$*"; exit 1; }
19
+
20
+ log "DSH Android 安装器"
21
+ log "kit 目录:$KIT_DIR"
22
+ log "HOME:$HOME_DIR"
23
+
24
+ ARCH="$(uname -m 2>/dev/null || echo unknown)"
25
+ case "$ARCH" in
26
+ aarch64|arm64) ;;
27
+ *) fail "只支持 arm64/aarch64 平板,当前是 $ARCH" ;;
28
+ esac
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # 1. 基础依赖
32
+ # ---------------------------------------------------------------------------
33
+ log "1/6 检查并安装依赖:nodejs clang make python3 binutils tar"
34
+ if command -v pkg >/dev/null 2>&1; then
35
+ pkg install -y nodejs clang make python3 binutils tar >$LOGDIR/kit-pkg.log 2>&1 || warn "pkg install 有警告,见 $LOGDIR/kit-pkg.log"
36
+ else
37
+ warn "没有 pkg 命令,假定依赖已手动装好"
38
+ fi
39
+
40
+ command -v node >/dev/null 2>&1 || fail "node 没装上"
41
+ command -v npm >/dev/null 2>&1 || fail "npm 没装上"
42
+ log " node $(node -v) / npm $(npm -v)"
43
+ command -v clang >/dev/null 2>&1 || warn "没有 clang,编译原生绑定大概率失败"
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # 2. 安装 DSH CLI
47
+ # ---------------------------------------------------------------------------
48
+ log "2/6 检查 DSH CLI"
49
+ if command -v dsh >/dev/null 2>&1; then
50
+ log " 已安装:$(dsh --version 2>&1 | head -n 1)"
51
+ else
52
+ log " 正在安装 @deepseek-ai/dsh(--ignore-scripts,跳过 koffi 的 cmake 编译)"
53
+ npm i -g @deepseek-ai/dsh --ignore-scripts --no-audit --no-fund >$LOGDIR/kit-npm.log 2>&1 \
54
+ || { tail -n 20 $LOGDIR/kit-npm.log; fail "npm 安装 dsh 失败(见 $LOGDIR/kit-npm.log)"; }
55
+ command -v dsh >/dev/null 2>&1 || fail "dsh 装完但不在 PATH"
56
+ fi
57
+
58
+ PKG_ROOT="$(npm root -g 2>/dev/null)"
59
+ DSH_DIR="$PKG_ROOT/@deepseek-ai/dsh"
60
+ [ -d "$DSH_DIR" ] || fail "找不到 dsh 安装目录:$DSH_DIR"
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # 3. 编译 android-arm64 原生绑定(官方没发布)
64
+ # ---------------------------------------------------------------------------
65
+ log "3/6 检查 node-addon-require-builtin 的 android-arm64 绑定"
66
+ ADDON_DIR="$(find "$PKG_ROOT" -type d -name 'node-addon-require-builtin' -not -path '*-android-arm64*' 2>/dev/null | head -n 1)"
67
+ [ -n "${ADDON_DIR:-}" ] || fail "找不到 node-addon-require-builtin 包"
68
+ TARGET_DIR="$(dirname "$ADDON_DIR")/node-addon-require-builtin-android-arm64"
69
+ BINDING="$TARGET_DIR/prebuilt/android-arm64-napi-v9.node"
70
+
71
+ if [ -f "$BINDING" ]; then
72
+ log " 已存在,跳过编译:$BINDING"
73
+ else
74
+ log " 开始编译(1~3 分钟,取决于平板性能)"
75
+ WORK="$HOME_DIR/.dsh-kit/narb"
76
+ rm -rf "$WORK"; mkdir -p "$WORK"
77
+ tar -xzf "$KIT_DIR/narb-build.tgz" -C "$WORK" || fail "解压 narb-build.tgz 失败"
78
+ ( cd "$WORK" \
79
+ && npm install --no-audit --no-fund >$LOGDIR/kit-narb-npm.log 2>&1 \
80
+ && ./node_modules/.bin/node-gyp rebuild --nodedir="$PREFIX_DIR" \
81
+ --python="$(command -v python3)" -j 4 >$LOGDIR/kit-narb-build.log 2>&1 ) \
82
+ || { tail -n 30 $LOGDIR/kit-narb-build.log 2>/dev/null; fail "原生绑定编译失败(见 $LOGDIR/kit-narb-build.log)"; }
83
+ [ -f "$WORK/build/Release/require_builtin.node" ] || fail "编译没产出 require_builtin.node"
84
+ bash "$KIT_DIR/install-addon.sh" "$WORK/build/Release/require_builtin.node" "$TARGET_DIR" \
85
+ || fail "安装原生绑定包失败"
86
+ fi
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # 4. 打补丁(flock / 硬链接)
90
+ # ---------------------------------------------------------------------------
91
+ log "4/6 打 Android 兼容补丁"
92
+ node "$KIT_DIR/patch-flock.js" || fail "flock 补丁失败"
93
+ node "$KIT_DIR/patch-link.js" || fail "硬链接补丁失败"
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # 5. 安装启动脚本与固定入口
97
+ # ---------------------------------------------------------------------------
98
+ log "5/6 安装启动脚本"
99
+ install -m 644 "$KIT_DIR/dsh-home.js" "$HOME_DIR/dsh-home.js" || fail "安装 dsh-home.js 失败"
100
+ install -m 755 "$KIT_DIR/start-dsh.sh" "$HOME_DIR/start-dsh.sh" || fail "安装 start-dsh.sh 失败"
101
+ mkdir -p "$HOME_DIR/dshpad"
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # 6. 启动
105
+ # ---------------------------------------------------------------------------
106
+ log "6/6 启动 DSH 与固定入口"
107
+ bash "$HOME_DIR/start-dsh.sh"
108
+
109
+ FIXED="http://127.0.0.1:3081/"
110
+ TOKEN_URL="$(cat "$HOME_DIR/dshpad/url.txt" 2>/dev/null)"
111
+ cat <<EOF
112
+
113
+ ============================================================
114
+ 安装完成
115
+ ------------------------------------------------------------
116
+ 固定网址(以后只记这个,重启后仍可用):$FIXED
117
+ 当前 token 直连:${TOKEN_URL:-(未取到,见 ~/dshpad/web.log)}
118
+
119
+ 在平板浏览器打开后:
120
+ 菜单 → 添加到主屏幕,以后一点就进
121
+
122
+ 平板重启 / Termux 被杀后重新拉起:
123
+ bash ~/start-dsh.sh
124
+ ============================================================
125
+ EOF
@@ -0,0 +1,39 @@
1
+ #!/data/data/com.termux/files/usr/bin/bash
2
+ export HOME=/data/data/com.termux/files/home
3
+ mkdir -p $HOME/dshpad
4
+ termux-wake-lock 2>/dev/null
5
+
6
+ echo "=== 停掉旧的 dsh 与旧入口 ==="
7
+ pkill -f 'dsh --profile web' 2>/dev/null
8
+ pkill -f dsh-home.js 2>/dev/null
9
+ sleep 2
10
+
11
+ echo "=== 启动 dsh web ==="
12
+ rm -f $HOME/dshpad/web.log $HOME/dshpad/url.txt
13
+ nohup dsh --profile web --host 127.0.0.1 --port 3080 --no-open > $HOME/dshpad/web.log 2>&1 &
14
+
15
+ URL=""
16
+ for i in $(seq 1 40); do
17
+ URL=$(grep -o 'http://127.0.0.1:3080/?token=[A-Za-z0-9_-]*' $HOME/dshpad/web.log 2>/dev/null | tail -1)
18
+ [ -n "$URL" ] && break
19
+ sleep 1
20
+ done
21
+
22
+ if [ -z "$URL" ]; then
23
+ echo "FAILED: 没有拿到 token URL"
24
+ tail -20 $HOME/dshpad/web.log
25
+ exit 1
26
+ fi
27
+ echo "$URL" > $HOME/dshpad/url.txt
28
+ echo "TOKEN_URL=$URL"
29
+
30
+ echo "=== 启动固定入口 3081 ==="
31
+ nohup node $HOME/dsh-home.js > $HOME/dshpad/home.log 2>&1 &
32
+ sleep 2
33
+ cat $HOME/dshpad/home.log
34
+
35
+ echo "=== 自检 ==="
36
+ curl -s -o /dev/null -w "home=%{http_code}\n" http://127.0.0.1:3081/
37
+ curl -s -o /dev/null -w "direct=%{http_code}\n" "http://127.0.0.1:3080/?token=$(echo "$URL" | sed 's/.*token=//')"
38
+ curl -sI http://127.0.0.1:3081/ | grep -i '^location' | head -1
39
+ echo "FIXED_URL=http://127.0.0.1:3081/"