dsh-fnos
DeepSeek Harness(DSH)插件:在本机通过 DSH 远程管理 飞牛 fnOS 设备。
授权登录、文档目录树浏览、文件/文件夹 @引用 插入,全部在 DSH 界面里完成,不需要终端。
功能
| 功能 | 说明 |
|---|---|
| 授权登录 | 走 trim-cli login 的 OAuth 设备码流程,在插件详情页里点一下打开浏览器,粘回一次性授权码即可 |
| 文件/文件夹选择器 | 输入框左侧的飞牛图标,点开是 NAS 目录树;Ctrl/Cmd + 点击 直接以 @文件名 引用插入输入框 |
| 目录树缓存 | 内存直读 + 落盘,打开走 peek() 恒为毫秒级;后台刷新默认关闭(FNO_BACKGROUND_REFRESH=1 可开) |
| 主题跟随 | 全部使用 --dsw-alias-* 主题变量 + 毛玻璃,能跟随 DSH 皮肤(含第三方主题) |
| 插件详情页配置 | 主机 / 端口 / 协议 / 跳过证书校验,写在插件自己的详情页里 |
安装
最快:把仓库地址粘给插件管理器
plugin_manager install_bundle target = github:MeetoHub/dsh-fnos
也可以用 Release 里的 tgz(版本固定,不拉整个仓库):
https://github.com/MeetoHub/dsh-fnos/releases/download/v0.1.0/dsh-fnos-0.1.0.tgz
装完重启一次 DSH,然后在插件详情页填设备地址即可。
三种安装方式的完整说明、前置条件、卸载与排错见 docs/INSTALL.md。
从源码安装(开发)
# 在 DSH profile 目录下
pnpm add link:/path/to/dsh-fnos
或在 DSH 的插件管理器里安装 link:/path/to/dsh-fnos。改代码即时生效,无需重装。
启用后需要重启一次 DSH(宿主半边是 Node 模块,启动时装载)。
语言包
locale/zh.json、locale/en.json 提供插件显示名与描述。
使用
- 设置 → 插件 → 飞牛 NAS → 点进详情页
- 填 主机 / 端口 / 协议(自签证书记得勾「跳过证书校验」)
- 点 登录飞牛 NAS → 浏览器打开授权页 → 登录后把页面上的一次性授权码粘回来 → 点「完成登录」
- 连接成功后,输入框左侧会出现飞牛图标(绿点 = 已连接)
- 点图标打开目录树:
- 点文件夹名 → 进入下一级
Ctrl / Cmd + 点击→ 直接以@文件夹/引用插入- 点文件名 → 直接以
@文件名引用插入 - 顶部搜索框可按名字过滤当前目录
架构
插件由两个半边组成,通过 DSH 的 Cordis 插件系统装配。
宿主半边(lib/entry.js → lib/impl.js)
lib/entry.js:稳定 shim。每次apply()用带时间戳的 URL 动态import('./impl.js?t=…'),用来击穿 Node 的 ESM 模块缓存 —— 改完代码在插件管理器里关一次再开就生效,不必重启 DSH。lib/impl.js:注册 HTTP 路由、fnos_probe工具、目录缓存与后台刷新。lib/auth.js:授权状态机(start/submit/cancel/verify/reconcile)。
客户端半边(lib/client.js)
通过 window.__ModuleLoader__.load({ id, factory }) 注册,占三个槽位:
| 槽位 | 用途 | key |
|---|---|---|
plugins.bundle.config |
插件详情页里的配置 + 授权(主入口) | dsh-fnos(npm 包名) |
conversation.input.left |
输入框左侧图标 + 目录树浮层 | — |
main |
完整面板 | dsh-fnos |
HTTP 路由
⚠️ 刻意不挂在
/plugins/<id>/下 —— 声明dsh.client之后clientModules会占用该命名空间分发前端产物,子路径会被截走(表现为空 404 / 405)。
| 路由 | 说明 |
|---|---|
GET /fnos/probe |
插件装载状态与宿主服务可用性 |
GET /fnos/auth/status |
授权状态(无活跃会话时会主动查真实凭据) |
POST /fnos/auth/start |
启动登录,返回授权 URL |
POST /fnos/auth/submit |
提交一次性授权码 |
POST /fnos/auth/cancel |
取消 |
GET /fnos/files?path= |
列目录(带缓存,?fresh=1 强制刷新) |
目录缓存设计
读:内存 Map(毫秒级),过期也先返回旧值并标 stale
写:按需刷新(打开目录时)+ 整表覆盖落盘 .cache/dirtree.json
后台:默认关闭。设 FNO_BACKGROUND_REFRESH=1 才启用自调度刷新(20s、每轮 1 条、不重叠)
上限:400 条,超出按最久未更新淘汰
为什么不直接每次实时拉:trim-cli 每次调用都要分配真终端(spawnTerminal),单次 1–3 秒。缓存后打开文件树的实测对比:
| 场景 | 耗时 |
|---|---|
| 冷启动(实时调 NAS) | 3366 ms |
| 缓存命中 | 3–81 ms |
| DSH 重启后(从磁盘恢复) | 9–67 ms |
为什么不默认开后台刷新:spawnTerminal 每次调用在本平台会残留约 11 个句柄,
调用越频繁涨得越快。而打开文件树靠 peek() 走内存、从不阻塞,后台刷新只买到
"数据更新鲜"这一点,不值得用句柄泄漏去换。详见踩坑记录第 9 条。
开发踩坑记录
这些都是实际调试中撞出来的,改代码前建议先读一遍。
Loader 行的
name必须等于包名。dsh-client-modules只把浏览器半边挂在「说明符恰为包名」的那一行上,用别名会导致宿主正常、界面静默消失。id不能与官方@tnnevol/dsh-fnos冲突,否则你的行根本不会创建(表现为所有路由 404)。ctx.setInterval不可用 ——timer不在插件inject里,会抛cannot get property "timer" without inject。用全局setInterval+ctx.effect清理。普通
spawn拿不到trim-cli的 stdout(Windows 实测为空),必须用ctx.subprocess.spawnTerminal分配真终端。insertReference支持零宽 span,不需要先打/做锚点。官方自己的insertFileReferences就是{ start: at, end: at }。组件不能直接引用
apply的ctx—— 组件在模块作用域,要通过register({ inject: (sessionId) => ({ ctx, sessionId }) })传进去。handle.done不能用来判断登录成败。trim-cli授权成功后不打印成功文案、直接静默退出(exit=0),唯一可信的是system info能否取到设备数据。PTY 会把授权 URL 按终端宽度折行。匹配前必须先去换行(
\r\n|\r|\n),否则只能拿到第一段(约 140 字符),导致授权页报「参数异常」。spawnTerminal的代价被严重低估 —— 这是本项目最严重的一次事故。 两个独立问题叠加,实测主进程涨到 5900+ 句柄 / 377 线程 / 2.6 GB 并反复崩溃:(a) 调度重叠(放大器):
setInterval驱动 async tick,无法 await。单轮刷 10 条 × 单次 1–3 秒 = 10–30 秒,超过 20 秒间隔后轮次重叠,每轮新增 10 个 PTY 且旧的未回收。 修法:自调度(上一轮结束才排下一轮)+ 每轮只做 1 条。 实测:句柄增速 +329/60s → +38/60s。(b) 每次调用固定残留约 11 句柄(真凶):即使串行调用、
terminate()已 await、 监听已解除、done已落定,单次spawnTerminal仍会留下句柄和线程。 所以只要还在频繁 spawn,泄漏就还在 —— 只修调度只能减缓约 9 倍。 修法:减少 spawn 本身。后台刷新改为默认关闭(FNO_BACKGROUND_REFRESH=1才开), 预热跳过已缓存目录(装载时的 44 次 spawn 降到 0,耗时 156s → 3.5s)。 实测:150 秒净增 +13 句柄 / −2 线程 / −97 MB(噪音级,GC 正常回收)。附带教训:
Promise.race+ 裸setTimeout会留下悬挂定时器(闭包还拖住终端句柄), 要换成可清除的settlesWithin();terminate()之后还要解监听、等done落定。后台刷新要先判授权。未授权时每轮都会 spawn 一个注定失败的终端,纯浪费; 授权结果记忆 60 秒即可,不必每轮校验。
已知限制
- 海外版 FygoOS 固件:授权页点「授权」后返回「参数异常」。原因是该固件的 OAuth 授权端点不识别
trim-cli的client_id,属设备侧问题,插件无法绕过。国内版 fnOS 无此问题。 - 部分命令走另一套地址:
file usage等会连服务端下发的xxx:15667,与配置的:5667不一致时会报错。file ls系列不受影响。 - 目前只实现了只读能力(浏览/选取)。写操作(上传/删除/移动)尚未暴露为工具。
目录结构
dsh-fnos/
├── lib/
│ ├── entry.js # 宿主入口 shim(ESM 缓存击穿)
│ ├── impl.js # 宿主主逻辑:路由 / 工具 / 缓存
│ ├── auth.js # 授权状态机
│ └── client.js # 客户端半边(UI)
├── bin/ # 各平台预编译 trim-cli
├── locale/ # zh / en 语言包
├── entries/ # 技能条目文档
├── reference/ # trim-cli 命令参考
├── scripts/ # 技能脚本
├── cordis.patch.yml # 插件装载补丁
├── SKILL.md # 技能描述
├── manifest.json
└── icon.png
关于 bin/ 里的二进制
bin/ 下是 fnOS 官方的 trim-cli 预编译产物(六个平台各一份,共约 41 MB),随插件一同分发以便开箱可用。
这些不是本项目的产物,版权归飞牛 fnOS 所有,使用须遵守飞牛的相关条款与许可。本项目的 MIT 许可只覆盖自己编写的代码,即 lib/、cordis.patch.yml、locale/、entries/、reference/、scripts/、README.md 等文件。
如果要在自己的分发中替换或去掉这些二进制:删掉 bin/,并把 trim-cli 放到 PATH 即可(lib/impl.js 的 cliPath() 会优先用包内路径)。
开发
开发循环、分支与版本约定、发布流程,以及三道安全防线(本地 hook / CI / 发布复检)见 docs/DEVELOPMENT.md。
新克隆仓库后请先启用本地敏感内容扫描:
git config core.hooksPath scripts/hooks
License
MIT —— 仅覆盖本项目自有代码;bin/ 下的二进制见上一节。
No comments yet. Be the first to write one.