作者: hungyi

  • 从「装个插件」到 1 GB 工作台:一次 DSH 插件安装的四层坑

    需求只有一句话:「安装这个插件 github.com/dsh-market/dsh-market」。原以为是一条 npm install 的事,实际连续踩了四层坑:仓库名和包名不一致、pnpm 需要 git 而机器上没有、git 协议连不上、目标插件根本不是「npm 式插件」,而是一套 1 GB 的自包含逆向分析工具链。

    本次操作在本人持有的 Windows 11 机器上进行,插件来自公开仓库、按其许可证使用;文中路径与账号相关部分已脱敏,不包含任何令牌、密钥或个人数据。

    一、先看环境

    项值
    宿主DeepSeek Harness Desktop 0.2.1-rc.3
    运行时Node 24.21.0 / pnpm 11.28.5(随包提供)
    DSH_HOMEC:\Users\<用户名>\.dsh
    profiledesktop,由 Electron 应用托管
    网络中国大陆,github.com 的分域名可达性不一致
    磁盘C: 剩余 118 GB,D: 剩余 716 GB

    先记住一个关键认知:DSH 的插件不是「丢进 plugins 目录」就生效,而是 profile 的 package.json 依赖 + dsh.profile.bundles 加载栈 + node_modules 三者配合,cordis.patch.yml 负责补丁层。

    二、第一层:仓库名不等于包名

    仓库叫 dsh-market/dsh-market,但 package.json 里的 name 是 dshmarket,没有连字符。npm 上搜 dsh-market 是 404,真正的包是 dshmarket@1.70.2,由 GitHub Actions 以 OIDC trusted publisher 发布并带 SLSA provenance 签名——这类可验签的社区包值得优先信任。

    dsh plugin --profile desktop add dshmarket

    输出 + dshmarket 1.70.2 与 Done in 5.6s using pnpm v11.28.5。dsh plugin add 不只会改依赖,还会把它追加进 bundle 加载栈:

    // profiles/desktop/package.json(安装后)
    {
      "dependencies": { "dshmarket": "^1.70.2" },
      "dsh": { "profile": { "bundles": [
        "@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dshmarket"
      ]}}
    }

    装完三秒内,插件自己写出了 .dsh-market\log.ndjson 和 state.json,说明宿主已经热加载,不需要重启。

    这里有个容易忽略的警告:插件的 README 明确写着桌面端要用应用内的「设置 → 插件」安装,如果对 web profile 执行 dsh plugin add,会装进 profiles/web,桌面端根本看不到。本次直接对 desktop profile 操作,才避开了这个坑。

    三、第二层:pnpm 需要 git,而机器上没有

    接着在市场里装另一个插件,报错:

    [ERR_PNPM_GIT_FETCH_FAILED] Failed to fetch from the git repository
    "https://github.com/Minglink/dsh-infinite-gen-4.git":
    'git' 不是内部或外部命令,也不是可运行的程序

    定位很明确:pnpm 在拉一个 git+https 依赖,调用的是系统 git,而 PATH、Program Files\Git、%LOCALAPPDATA%\Programs\Git 全是空的——不是网络问题,也不是插件作者的问题。好消息是失败的安装没有留下残留,package.json 和 pnpm-lock.yaml 里都没有半个条目。

    为什么市场会走到 git?它的安装策略是三档兜底:npm 包 → 作者提供的 Release 预构建 tarball → 整个 GitHub 源码仓库。这个插件只提供了 GitHub 源码,于是落到最后一档,必须调用 git。

    四、第三层:git 装好了,git 协议还是连不上

    选型上,winget install Git.Git 会走机器级安装并弹 UAC,在当前会话里容易卡住;MinGit 只有 40 MB,够 pnpm 用但功能不全;最后选了 PortableGit(约 60 MB,bash / ssh / lfs 都在),解压即用、不需要管理员权限。

    装完立刻遇到第二个问题:Windows 进程的环境变量在启动时就固定了,正在运行的 DSH 不会因为你改了用户 PATH 就看见 git。做法很土但有效——在一条已经在宿主 PATH 上、且用户可写的目录里放个 shim:

    @echo off
    "C:\Users\<用户名>\AppData\Local\Programs\PortableGit\cmd\git.exe" %*

    之后 cmd /c git --version 立刻返回 git version 2.56.0.windows.4,不用重启宿主。

    但 git 装上后,git ls-remote 依旧失败:Recv failure: Connection was reset。实测是分域名的——api.github.com、codeload.github.com 可达,github.com 的 git smart HTTP 连接会被重置。pnpm 调用 git,所以 pnpm 一起失败。

    镜像站要测三件事,不是测速

    镜像HEAD 延迟支持 git 协议支持 HTTP Range
    gh-proxy.com661 ms是否
    ghfast.top1860 ms是是(206)
    github.moeyy.xyz超时——
    直连 github.com1034 ms(假象)否,连接被重置—

    HEAD 快不代表能下载,更不代表能断点续传。这张表里的第三列,就是后面最耗时那个坑的伏笔。

    先确认镜像确实能跑 git 协议(而不是只能下载 raw 文件):git ls-remote https://ghfast.top/https://github.com/Minglink/dsh-infinite-gen-5.git 在 1.7 秒内返回了分支哈希。然后做全局 URL 重写,让所有 GitHub 的 HTTPS 操作透明走镜像:

    git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"

    效果很直接:普通 GitHub URL 的 ls-remote 从 24.7 秒超时变成 1.5 秒成功;git clone --depth 1 拉一个 75.7 MB 的仓库只用 15 秒。副作用要知道:这条配置对所有 GitHub HTTPS 操作生效,镜像不可用时记得撤销:

    git config --global --unset-all url.https://ghfast.top/https://github.com/.insteadOf

    五、第四层:源码包不等于完整包

    到这一步 git 已经可用,但继续调查发现:就算装成功也是残废的。

    • 仓库已经从 dsh-infinite-gen-4 改名为 dsh-infinite-gen-5,GitHub 会自动 301;
    • 插件自称「无限五代 IG5」,实际是一套逆向分析工作台:Ghidra 静态分析、x64dbg 原生调试、HTTP/HTTPS 采集、去混淆,Core 8 / Full 40 个工具;
    • package.json 的 files 列了 runtimes/、third_party/,但仓库里根本没有这两个目录;
    • 没有任何 postinstall 脚本,pnpm 装完就是一堆源码,runtimes/ 空着,引擎跑不起来;
    • 作者在 README 里写得很清楚:源码 ZIP 与 git clone 是维护源码,不是完整离线安装包。

    也就是说,市场里那条 git 路径对这个插件本身就不适用。它的官方安装器 install.ps1 会做市场不会做的两件事:把资产铺进 ~/.dsh/plugins,并在 profile 里建 junction、改 package.json。于是只剩两条路:

    路线下载量风险
    市场 git 装源码,再用 install.ps1 联网补资产75 MB + 约 1 GB多一步,中途失败概率高
    官方 v1.1.0 完整包离线安装1.02 GiB官方支持路径,确定性高

    六、1 GB 的下载,才是硬仗

    第一次用 Invoke-WebRequest 下 1.02 GB,到 707.8 MB 被镜像掐断:An existing connection was forcibly closed by the remote host。改用 curl -C - 续传,直接报:

    curl: (33) HTTP server does not seem to support byte ranges. Cannot resume.

    这就是前面那张表第三列的兑现:gh-proxy.com 不支持 Range。换成支持 206 的 ghfast.top,从 888 MB 处一次续到底:

    curl.exe -L --fail --retry 2 --connect-timeout 20 --speed-limit 1024 --speed-time 90 -C - -o $zip $url

    最终文件 1097399607 字节,SHA-256 为 83c32513533ce86a79c64db43eaaa421f4fa669520b75b4291eb9aca93fd3203,与 GitHub Release API 公布的 asset digest 逐字节一致。跨镜像续传的 1 GB 文件,没有哈希就没有「装的是什么」,这一步是整个流程里最值得做的校验。

    七、安装器 15 分钟没进入复制阶段

    官方安装器 install.ps1 写得很严谨,逻辑大致是六步:

    1. 读每个 profile 的 package.json,算 SHA-256 快照,防止并发改写;
    2. 校验整个运行包,52,000+ 个文件逐个哈希;
    3. 复制到 ~/.dsh/plugins/dsh-infinite-gen-5,先 staging,失败可回滚;
    4. 在每个 profile 的 node_modules 里建 Junction 指向物理目录;
    5. 改写 package.json:加入 file: 依赖并追加到 bundles;
    6. 旧版本移到 ~/.dsh/ig5/artifacts/ 备份。

    问题出在第 2 步:单线程 PowerShell 逐文件哈希 5 万多个文件,跑了 15 分钟、CPU 时间 700 秒还没进入复制阶段。既然 zip 的 SHA-256 已经和官方发布值对齐,这里选择跳过逐文件校验,按安装器的语义做等价手工安装:

    robocopy "D:\IG5\extract\plugin" "C:\Users\<用户名>\.dsh\plugins\dsh-infinite-gen-5" /E /MT:16 /R:1 /W:1
    
    New-Item -ItemType Junction -Path "C:\Users\<用户名>\.dsh\profiles\desktop\node_modules\dsh-infinite-gen-5" -Target "C:\Users\<用户名>\.dsh\plugins\dsh-infinite-gen-5"
    
    # 再改 profile/package.json:
    # "dsh-infinite-gen-5": "file:../../plugins/dsh-infinite-gen-5"

    53,362 个文件、2.19 GB,robocopy 多线程复制用了 0.9 分钟。安全性上有个巧合:完整包里 runtimes\manifest.json、runtimes\ghidra\runtime.json、runtimes\x64dbg\runtime.json 本来就是齐的,安装器的清单写入只是复制一份,所以跳过校验不影响运行时读取。另外做了两件保险:安装前把 profile 的 package.json、cordis.patch.yml、pnpm-lock.yaml 备份到另一个盘;装完清掉 2.2 GB 的临时解压目录和 75 MB 的克隆,只保留 1.02 GB 的 zip 供离线重装。

    八、验证:不是「文件存在」,是引擎真的启动

    改完 package.json 后宿主热重载,当前会话里直接出现了 ig5_* 工具和 8 个 ig5-* 技能(ig5_doctor、ig5_open、ig5_decompile、ig5_triage 等)。调用 ig5_doctor(engine="ghidra") 返回:

    {
      "ok": true,
      "engine": "ghidra",
      "version": "12.1.4",
      "python": "3.12.10",
      "pyghidra": "3.1.0",
      "runtimeReady": true,
      "startupVerified": true,
      "unsupported": ["dbg"]
    }

    startupVerified: true 意味着 Ghidra 12.1.4 真正启动成功,而不是「文件存在」。如实记录两个未验证项:x64dbg 调试车道那次 doctor 调用被中断,没有拿到结论;安装器被中断,所以包内 5 万多个文件没有逐文件哈希,完整性由 zip 的官方 SHA-256 锚定。

    九、最终状态

    项值
    Git2.56.0.windows.4(PortableGit,用户级)+ 全局镜像重写
    插件市场dshmarket@1.70.2
    IG5dsh-infinite-gen-5@1.1.0,53,362 文件 / 2.14 GB
    接入方式Junction + file: 依赖 + bundle 加载栈
    引擎Ghidra 12.1.4 已验证;x64dbg 未验证
    体积成本插件 2.14 GB + 离线 zip 1.02 GB

    十、可复用的经验

    1. 先确认包名。GitHub 仓库名和 npm 包名经常不一致;npm 上带签名或 provenance 的包优先。
    2. pnpm 的 git+https 依赖强依赖系统 git。Windows 上的表现是「’git’ 不是内部或外部命令」,很容易被误判成网络问题。
    3. 镜像测速要测三件事。HEAD 能否 200、能否跑 git 协议、是否支持 Range——三者不等价,这次就栽在第三项。
    4. url.insteadOf 是治本方案。一条 git 配置让所有 GitHub 依赖透明加速,比逐个替换 URL 靠谱,但要知道怎么撤销。
    5. 已运行进程不会刷新 PATH。想让工具「立刻生效」,要么重启宿主,要么在宿主 PATH 里放 shim。
    6. DSH 插件 = 依赖 + bundle 栈 + node_modules。Junction 让物理目录与 profile 解耦,file: 依赖绕开 pnpm 重新解析。
    7. 区分「源码仓库」和「发布完整包」。带原生运行时的插件几乎都只有 Release 完整包能跑,源码版装了也是空壳。
    8. 校验要挑成本。逐文件哈希 5 万个文件超过 15 分钟,收益有限;但对下载物做一次 SHA-256 是必需且便宜的,1 GB 文件约 5 秒。
    9. 改宿主配置前先备份。package.json、cordis.patch.yml、lockfile 各留一份,第三方安装器虽然也备份,自己有一份更安心。

    十一、合规与卸载

    这个插件是 CC BY-NC-SA 4.0 许可,自带原生二进制与调试能力:转载或再分发需要署名、以相同方式共享,且不得用于商业用途;工具本身面向的是自己有权分析的目标,不要用它处理别人的程序或数据。整篇记录均在本机、本人账号环境下完成,不含任何令牌、密钥与个人数据。

    卸载是三步:删掉 junction、从 package.json 去掉依赖与 bundle 项、删除 ~/.dsh/plugins/dsh-infinite-gen-5;插件也自带 uninstall.ps1,但删之前记得先备份 profile。

  • 给 Passport OS 加一个计算器:一次合规的二次开发记录

    本文记录的是在开源项目基础上、针对本人自购设备所做的一次功能扩展。所有改动均基于公开源码,遵守原项目许可证;文中不包含任何账号、令牌、Wi-Fi 密码、设备密钥或他人隐私数据。

    一、为什么选这个项目

    FoloToy AI Passport 是一块很小的随身设备:ESP32-C3、8 MB Flash、240×320 屏幕,只有上/下/确定三个按键。社区固件 Passport OS(github.com/manchunx7-bit/passport-os)以 MIT 许可证开源,已经内置首页、收音机、AI 语音、小智、Muse、俄罗斯方块、秒表、番茄钟等应用,并提供统一的启动器、设置、配网和电源管理。

    它的代码结构很适合做二次开发:

    • 应用层在 main/apps/,每个应用是一个独立目录;
    • 应用统一通过 passport_app_t 描述符注册到 main/launcher/app_registry.cc;
    • 通用界面组件、字体、图标在 main/ui/、main/fonts/、main/launcher/launcher.cc;
    • 板级驱动在 components/bsp/,不需要为了加功能去改硬件驱动。

    我想加的功能很简单:一个离线可用的计算器。它不依赖网络、不申请额外权限,也不会触碰设备上已有的账号或配网数据,是练习二次开发边界的一个合适小目标。

    二、动手前的合规边界

    二次开发最容易忽略的不是代码,而是许可与边界。这次我给自己定了四条规则:

    1. 只使用公开源码:基于 Passport OS 的 MIT 许可代码开发,保留原项目版权与许可声明;不复制、不反编译、不再分发任何未授权的第三方固件或专有资源。
    2. 不修改、不再分发第三方素材:项目中的 Muse、Jollybot 形象、字体等分别遵循各自目录下的 LICENSE/NOTICE,本次没有改动这些资源,也没有把它们单独抽出来传播。
    3. 不处理敏感数据:代码和本文都不包含 Wi-Fi 密码、API Token、设备密钥、序列号或他人数据;固件构建时使用项目的公开发布配置,清空默认的服务配置。
    4. 只在本人设备上验证:刷机仅针对自己持有的设备;不提供”破解””绕过””提权”类内容,也不把固件发布到任何应用商店或收费渠道。

    在这个前提下,二次开发就是正常的开源软件修改:读代码、加模块、跑测试、编固件、刷自己的设备。

    三、功能设计:三个按键怎么用计算器

    设备只有 UP / DOWN / OK 三个键,所以不能照搬手机计算器的九宫格点按。最终采用的交互是:

    • 屏幕下方是一块 4×5 的虚拟键盘:7 8 9 /、4 5 6 *、1 2 3 -、0 . = +、C < +/- %;
    • UP/DOWN 在按键之间线性移动高亮(走到底自动回到另一端),OK 输入当前高亮键;
    • 长按 UP 清空、长按 DOWN 退格、长按 OK 退出(沿用系统统一的返回手势);
    • 屏幕上方显示表达式与结果,出错(例如除以 0)显示 Error。

    计算逻辑被刻意做成纯 C 状态机,不依赖 LVGL/ESP-IDF:先按运算符优先级做乘除、再做加减,支持小数、取负、百分比、连续运算和表达式长度上限。这样可以用主机测试覆盖边界情况,而不必每次都刷机试。

    四、代码改动

    新增两个文件:

    • main/apps/calculator/calculator_core.c/.h:表达式状态机与求值。按键事件只修改状态,UI 只负责把 expr / result 显示出来;
    • main/apps/calculator/app_calculator.cc:LVGL 页面、键盘布局、高亮、按键分发,以及 passport_app_t 描述符。

    接入现有框架只需要几处小改动:

    • main/apps/app_base.h:应用 ID 枚举增加 APP_ID_CALCULATOR;
    • main/launcher/app_registry.cc:声明 g_calculator_app、加入启动器轮播列表、把计算器纳入允许按键音效的应用集合;
    • main/launcher/launcher.cc:为计算器补一个像素图标(机身 + 显示屏 + 3×2 按键点阵)和主题色;
    • main/CMakeLists.txt:加入新源文件与头文件目录;
    • tests/test_calculator.c + tools/test-host.sh:新增主机测试并接入测试脚本;
    • 中英文 README 的应用列表与数量同步更新。

    这体现了这类项目的一个好处:新增应用不需要改动系统层。注册表负责”发现应用”,启动器负责”展示应用”,应用自己负责”生命周期和按键”,三者解耦得很好。

    五、构建与验证

    构建使用项目要求的 ESP-IDF 5.5.3,命令行大致是:

    idf.py -B build-calc -D SDKCONFIG=build-calc/sdkconfig \
           -D PASSPORT_PUBLIC_RELEASE=ON -D PASSPORT_MUSE_USE_JOLLYBOT=ON build
    idf.py -B build-calc merge-bin -o build-calc/FoloToy-AI-Passport-full.bin
    python tools/verify_firmware.py build-calc --public

    验证分三层:

    1. 主机测试:计算器核心的状态机测试全部通过,覆盖优先级、小数、除零、退格、清空、取负、百分比、连续运算和长度上限;
    2. 固件校验:verify_firmware.py --public 检查分区布局、SRAM 预算、合并镜像和公开发布配置,全部 PASS;
    3. 真机测试:刷入设备后,启动器能看到”计算器”卡片与图标,进入后按键导航、1 + 2 = 3、错误显示和长按退出均正常。

    六、升级时如何不丢数据

    刷机有两种方式,区别很大:

    • 完整镜像写 0x0:会覆盖引导、分区表和 app,并且把 0x8000–0x10000 之间的空隙填成 0xFF,NVS(配网/资料)和 PHY 校准会被清掉;只适合首次安装或分区表变更。
    • 应用镜像写 0x10000:只覆盖 app 分区,NVS、PHY、cardid、storage(头像/名片)都保留;适合日常升级。
    python -m esptool --chip esp32c3 -p COM5 -b 460800 write_flash 0x10000 build-calc\passport-os.bin

    如果必须整片刷又要保留数据,可以先备份再恢复:

    python -m esptool -p COM5 read_flash 0x9000 0x6000 nvs.bin
    python -m esptool -p COM5 write_flash 0x9000 nvs.bin

    注意 NVS 结构要和新固件兼容,跨大版本时不要盲目恢复。

    七、这次二次开发的可复用经验

    1. 先读框架,再写功能:这类多应用固件的关键不是画界面,而是理解注册表、生命周期和按键分发;接入点找对了,代码量会很小。
    2. 逻辑与界面分离:把计算逻辑做成纯 C 状态机,既方便测试,也方便以后换成别的交互方式。
    3. 先查字体再写中文:嵌入式 UI 的中文字体通常是子集,新增中文文案前要确认字库覆盖,否则会出现方框或空白。本次用到的字符都在现有字库内,因此不需要重新生成字体。
    4. 构建环境要可控:Windows 下用 MSYS/Git Bash 调 ESP-IDF 会遇到 shell 识别问题;把 idf.py 的调用放到原生 cmd 环境、并显式处理 /tmp 路径映射,可以避免”配置成功但找不到产物”的坑。
    5. 测试分层:主机测试抓逻辑,固件校验抓布局与资源,真机测试抓交互与显示;三层都过再谈”完成”。

    八、合规声明与致谢

    • 本项目二次开发基于 MIT 许可证的 Passport OS 源码,保留原项目版权与许可声明;本文与改动不改变原项目的许可方式。
    • 未修改、未再分发 Muse、Jollybot 及其他第三方素材;相关资源仍遵循各自目录下的 LICENSE/NOTICE。
    • 未包含或传播任何账号、令牌、Wi-Fi 密码、设备密钥、序列号、日志或他人数据。
    • 未提供任何绕过技术保护措施、破解、提权或规避授权的内容;刷机仅针对本人持有的设备。
    • Passport OS 是社区固件,本文不代表 FoloToy 官方立场,也不宣称与原项目维护者有隶属或背书关系。

    感谢 Passport OS 的作者与社区把这样一套完整的固件开源出来,让”给随身小设备加一个小功能”变成一件可以在一个下午完成、并且可以坦然公开记录的事。

    九、后续可以做什么

    计算器只是一个开始。按同样的注册方式,还可以继续加:单位换算、计时器、离线词典、二维码显示等。真正需要谨慎的始终是两件事:尊重许可证与第三方资源,以及不碰别人的数据与设备。在这两条边界内,二次开发是一件很纯粹的事——读代码、写功能、刷自己的设备、把过程分享出来。