FNOS-009 fnos 插件真机验收与 BUG-04/05 修复记录
- 执行日期:2026-10-08
- 目标环境:飞牛 fnOS,
<nas>:5666(主机名<nas-host>),x86_64 - 应用版本:
fn-deepseek-harness5.5.2(修复包),DSH0.2.0-rc.2 - 安装位置:存储空间 1
- 关联需求:FNOS-009-02、 FNOS-009-09、 FNOS-009-10
- 关联缺陷:BUG-04、BUG-05
背景
本轮目标是走查 fnos 插件(@tnnevol/dsh-fnos)在真实 NAS 上的全部功能。走查在 「授权目录」一处连续暴露两个 P0 缺陷,修复后在同一设备上复验通过。
缺陷 1(BUG-04):授权目录列表永久停留加载中
现象:进入插件详情页后,授权目录区长期显示「正在加载授权目录…」,列表 0 行。
取证:
| 环节 | 观测 |
|---|---|
| 后端接口 | GET /plugins/dsh-fnos/authorized-directories → 200,返回 5 个目录 |
| 前端日志 | [dsh-fnos][authorized-directories] refresh-start → refresh-success |
| 校验日志 | 无任何 validate-* 日志(模块共 3 条分支日志) |
| DOM | .dsh-fnos-authorized-path-list 节点不存在 |
无 validate-* 日志说明执行停在 await sdk.ready() 之前/之中,未进入任何分支。
根因:两个叠加因素。
config/resource的api-scope未声明trim.file.userAccess,而校验逻辑调用authorizeUserFile(需要该 scope)。- 更关键:fnOS SDK 的桥接调用没有超时。反编译
@trimjs/web-app@0.4.2可见 call sender 仅postMessage后等待宿主Reply,宿主不回复则 Promise 永不 settle;ready()的最后回退分支(Penpal 握手)同样没有超时。
因此表现为「静默挂起」而非报错。
修复:
config/resource补trim.file.userAccess。authorizeSharedFile/authorizeUserFile每次调用加超时(默认 10s),超时按 「问不到」处理而不是「没权限」。sdk.ready()加超时,超时走既有的validateUnavailable降级路径。
缺陷 2(BUG-05):合法授权目录被误剔除并回写持久化
现象:在 fnOS「申请访问以下文件」弹框中点「允许」后,该目录不仅未出现在列表, 持久化记录还被清空。
取证:
控制台:validate-evicted {"count":1}
落盘: cordis.patch.yml → authorizedDirectories: [] # 授权记录被删除根因:校验逻辑按错误契约读取 SDK 应答。
// 实现的假设
export type FnosAuthorizeResult = { ok: boolean } | undefined
if (response.ok === true) return 'valid' // ok 恒为 undefined
// fnOS 的真实契约(官方文档 AppBridgeResponse<T>)
type AppBridgeResponse<T> = { code: number, msg: string, data: T } // code: 0 为成功response.ok 永远是 undefined,因此每个 removable 目录都会走完两个接口、 两次判定为假,最终落到 return 'invalid' —— 用户刚授权成功的目录被判为「无权限」, 从展示列表移除,并因 evicted === true 同步回写持久化。
为什么测试没拦住:单测替身也按 { ok: true } 构造,替身与实现同错,一路绿灯。 这与 版本管理规范 记录过的 v5.5.0 事故同类:校验与被校验对象 共享同一份错误假设时,测试无法提供保护。
修复:
- 改用
code === 0判定成功;单个接口返回非 0 只说明它不管辖该路径(用户目录必然 不被共享目录接口接受),继续探测下一个接口,只有两个接口都明确拒绝才判为无效。 - 测试改用真实应答结构,并新增「调用永不 settle」「ready 永不 settle」两个用例。
复验结果(5.5.2)
降级路径(BUG-04 修复)
控制台:validate-skipped {"reason":"sdk-unavailable"} # 不再 validate-evicted
界面: 已按保存的记录展示目录;当前无法校验权限。 # 降级提示
列表: 5 行(含 存储空间2/1000/fnos-fpk) # 目录未被剔除
按钮: 「添加授权目录」「刷新」均可用,加载文案已消失目录保留与持久化(BUG-05 修复)
授权 fnos-fpk 后:
列表(5 行):
存储空间2/1000/fnos-fpk [取消授权] ← 用户目录,可移除
存储空间1/@appshare/fn-deepseek-harness [应用共享目录]
存储空间1/@apphome/fn-deepseek-harness [应用共享目录]
存储空间1/@appdata/fn-deepseek-harness [应用共享目录]
存储空间1/@appconf/fn-deepseek-harness [应用共享目录]
落盘(cordis.patch.yml):
authorizedDirectories:
- /vol2/1000/fnos-fpk刷新列表后该目录仍从持久化记录展示。对比修复前同一路径被判无效并删除,行为已逆转。
本轮同时验证的 fnos 插件功能
| 功能 | 结果 | 证据 |
|---|---|---|
| 插件详情页结构 | 通过 | 配置区常展开;.dsh-fnos-authorized-path-list 为 max-height: 300px + overflow-y: auto;插件根节点 border-width: 0(无外层框) |
| 应用共享目录为只读 | 通过 | 4 个 removable: false 项不参与校验、不出现「取消授权」 |
| 授权目录添加 | 通过 | 点击「添加授权目录」调起 fnOS 原生选择器,含「我的文件/外接存储/远程挂载/他人共享/我的收藏」分组 |
| 主题跟随系统 | 通过 | <html data-ds-theme-source="system" style="color-scheme: light">,与 systemTheme: light 一致 |
| 三方插件 proxy 保存 | 通过 | 输入 /plugin-api 保存后提示「已保存」,落盘 gatewayProxyPaths: [/plugin-api] |
| proxy 保留路径校验 | 通过 | 输入 /api 被拒绝,提示不可使用 /api、/plugins 或网关保留路径 |
| 插件安装状态 | 通过 | 插件页「已安装 4」,「异常」与「不兼容」均为 0 |
权限校验的修复过程与最终结果
段首那次「校验不可用」不是环境限制,而是实现选错了接口。修复分四步,每步都在 本设备上复验,逐层暴露下一个问题:
| 步骤 | 版本 | 问题 | 真机结果 |
|---|---|---|---|
| 1 | 5.5.2 | 按 { ok: boolean } 读应答,真实契约是 AppBridgeResponse(code: 0 为成功),导致合法目录被判无权限并清空持久化(BUG-05);另加超时兜底(BUG-04) | 列表不再卡死、不再误删 |
| 2 | 5.5.3 | 校验调用 authorizeSharedFile / authorizeUserFile,而这两个是申请授权接口——每次刷新都弹「申请访问以下文件」,且返回值只表示这次申请的结果 | 弹框消失 |
| 3 | 5.5.4 | trim.file.getUserAccessibleFolders 的 uid 是必填参数,漏传导致 fnOS 返回 Appstore Error | 后端响应转为 valid: true |
| 4 | 5.5.5 | 客户端解析响应时丢弃了 Host 标注的 valid 字段,导致每条目录都被当成「无法判定」 | 降级提示消失,校验真正生效 |
5.5.5 复验(最终状态):
后端:GET /plugins/dsh-fnos/authorized-directories
→ {"path":"/vol2/1000/fnos-fpk","removable":true,"valid":true}
界面:插件详情页显示 5 行,fnos-fpk 标注「取消授权」
· 无「申请访问以下文件」弹框
· 无「已按保存的记录展示目录」降级提示
· 点「刷新」后状态不变(幂等)权限校验至此真正生效:valid === true 保留、valid === false 剔除并回写持久化、 valid === undefined(查询接口不可用)保留并提示降级。
遗留
- 本次未覆盖:无。上述三项已在同一设备的后续走查中完成(见下节)。
- 持久化记录在修复前被 BUG-05 清空过一次,本轮已重新授权恢复。
后续走查(同一设备,同日)
会话日志导出
在真实会话(含 1 轮对话)上执行两条导出路径:
| 路径 | 结果 | 证据 |
|---|---|---|
| 导出到 NAS | 通过 | 菜单列出 5 个已授权目录;选择 存储空间2/1000/fnos-fpk 后实际落盘 dsh-session-session-bb00b361-…-1791442490409.zip(10957 字节,属主 fn-deepseek-harness) |
| 导出到电脑 | 通过 | 界面提示「导出已开始下载」,走上游 sessionLogDownload.download() |
导出产物校验:
ZIP 条目:session.v4.jsonl(1 个)
完整性: OK
JSONL: 20 行,首行键 type/version/id/createdAt/cwd/isSeeded/delegationDepth/agentPreset
内容: 含会话 ID bb00b361 与本次消息「你好」路径交文件管理器打开
点击会话头部的「在文件管理打开」,fnOS 文件管理器被调起并定位到会话工作区目录:
面包屑:应用文件 > fn-deepseek-harness
内容: .agents / .cache / .local / .bash_history / .npmrc其中 .npmrc 的修改时间为本次安装写入镜像源的时间,说明打开的是当前实际目录而非固定路径。
FNOS-009-11 父子勾选解耦
「插入 NAS 文件或目录」的 TreeSelect 展开 存储空间2/1000/fnos-fpk 后得到真实父子结构 (父目录 + 导出产物 ZIP + FPK 文件),据此逐步验证:
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | 勾选父目录 | 父目录 checked;两个子项保持未勾选 |
| 2 | 再勾选子项 fn-deepseek-harness.fpk | 父与子同时处于勾选状态(Semi 默认 related 模式不允许此组合) |
| 3 | 取消父目录勾选 | 父目录取消;子项 fn-deepseek-harness.fpk 仍保持勾选 |
第 3 步即 AC-01 的核心:取消父目录不再连带取消已选子项,父子状态相互独立。
本轮走查的完整功能清单
| 功能 | 结果 |
|---|---|
| 插件详情页结构与目录列表 | 通过 |
| 授权目录添加 / 列出 / 保留 / 持久化 | 通过(修复 BUG-04/05 后) |
| 权限校验与失效剔除 | 通过(5.5.5 起真正生效:无弹框、无降级提示、valid: true 保留) |
| 应用共享目录只读 | 通过 |
| 主题跟随系统 | 通过 |
| 三方插件 proxy 保存与保留路径校验 | 通过 |
| 会话日志导出(NAS / 电脑) | 通过 |
| 路径交文件管理器打开 | 通过 |
| 对话内引用 NAS 文件(父子勾选解耦) | 通过 |
FNOS-009-06 用户数据兼容
升级前基线快照
在 5.5.5 状态、准备下一次升级前记录关键数据文件的哈希,用于升级后逐项比对 (sha256 取前 16 位):
| 数据文件 | 内容 | sha256(前16) | 大小 |
|---|---|---|---|
codebuddy-auth.json | CodeBuddy 账号凭据(6 个账号) | 2a92dfd037de584d | 31770 |
.credentials.yaml | DSH 凭据 | e13d0fbfa1478e4a | 161 |
profiles/web/cordis.patch.yml | 插件设置(授权目录、主题、默认模型) | 97eb09c1ee154b62 | 450 |
storages/workspace.json | 工作区与会话索引 | 443396b8d585ea9a | 680 |
累计升级过程的保留结果
本设备在 2026-10-08 连续执行 5 次安装/升级:
5.5.0 → 5.5.2 → 5.5.3 → 5.5.4 → 5.5.5每次升级后确认以下数据均保留、无需手动迁移:
| 数据项 | 结果 |
|---|---|
| CodeBuddy 账号凭据 | 6 个账号持续保留(5.5.x 期间未丢失) |
| 授权目录 | authorizedDirectories: [/vol2/1000/fnos-fpk] 保留 |
| 主题偏好 | systemTheme: light 保留 |
| 默认模型配置 | provider: codebuddy / model: deepseek-v4.1-flash 保留 |
| 工作区与会话 | 1 个工作区、2 个会话保留 |
说明:
authorizedDirectories在 BUG-05 期间曾被清空一次(缺陷本身,已修复), 修复后重新授权并稳定保留;其余数据项全程未受影响。
FNOS-009-12 真实 NAS 回调链路
安装日志 /var/log/apps/fn-deepseek-harness.log 的全量统计(截至 5.5.5 安装完成):
| 指标 | 数值 |
|---|---|
| 带 release-age 放行的插件调用 | 68 次 |
✓ Lockfile passes supply-chain policies | 65 次 |
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION | 0 次 |
自有插件(bundled-dsh-plugins 归档)放行调用 | 36 次 |
| 三方插件(registry 安装)放行调用 | 2 次 |
日志形态(每次调用都同时出现放行标记与策略通过行):
[17:00:42] Running dsh plugin --profile web remove @tnnevol/dsh-codebuddy with the minimum release age disabled.
✓ Lockfile passes supply-chain policies (verified 7s ago)
[17:00:43] Running dsh plugin --profile web add file:.../dsh-codebuddy.tgz with the minimum release age disabled.
✓ Lockfile passes supply-chain policies (verified 1s ago)判定:自有插件与三方插件表现一致,均使用放行调用;真实回调链路全程未出现 发布日期违规报错,即 FNOS-009-12-AC-01 在真机成立。三方插件 dshmarket@1.66.11 在多次升级中保持 exact version already matches 的版本收敛行为。
FNOS-009-03 未验证说明
Codex Auth 的登录流程(AC-01)与对话区用量显示(AC-03)未做真机验证,原因: 需要真实 ChatGPT/OpenAI 账号完成浏览器授权,本次不具备该条件。已完成的 Web 走查 覆盖 AC-02(模型目录同步、模型选择与全局模型设置)。
按用户决定(2026-10-08):本项不做进一步验证,保留已完成部分,不作为本次 验收的完成结论。
FNOS-009-07 上游增量逐项核对
依据本地 checkout ~/workspace/fork-pj/deepseek-harness 的 dsh-v0.1.7-rc.2 与 dsh-v0.2.0-rc.2 两个 tag,对四个插件实际消费的接缝逐项核实。
插件消费的接缝与对应增量
| 接缝 / 用法 | 上游包 | 两版本差异 | 判定 |
|---|---|---|---|
plugins.detail.section | client/ui-plugin-manager | renderSlot('plugins.detail.section', { subject }) 调用形参完全一致,仅行号从 452/496 变为 485/529;该包 17 个文件变化集中在刷新 toast、安装输入脱敏与测试 | 无回归 |
conversation.session.header.utilitiesconversation.input.left / .right | client/ui-slotsclient/ui-input-trigger | 两包各仅 1 个文件变化(package.json 版本) | 无回归 |
submit() 相关 | client/ui-conversation | 签名由 submit(mode) 变为 submit(mode, source?),source 为可选参数;插件未调用 submit | 无回归 |
SessionRowOwnerProps.displayTitle | client/ui-workspace | 类型声明两版本均为 displayTitle: string,语义未变;插件未引用 | 无回归 |
fork() 回调 | client/api-session-controller | 新增可选 onCreated;插件未使用该接口 | 无回归 |
MenuGroup / pointerModality | client/ui-primitives | v0.1.7 中 MenuGroup 出现 0 次、v0.2.0 中 4 处,确认为纯新增导出 | 无回归(增量导出) |
字号范围变化(12–17 → 10–22)
FONT_SIZE_MIN 由 12 降到 10、FONT_SIZE_MAX 由 17 升到 22。核对了落地方式:
- 主题通过
document.body.style.setProperty('--dsh-content-font-size', ...)应用字号; - 该变量名与用法在 v0.1.7-rc.2 与 v0.2.0-rc.2 完全一致(两版本各出现 1 次);
- 插件的
styles/index.scss使用固定 px(11/12/13/15px),不消费该变量 —— 行为与升级前逐字相同,扩大取值范围只是放宽可选区间,对固定值样式没有影响。
判定:范围放宽属宽松化变更,无回归。
结论
四插件消费的全部接缝在两版本间均无破坏性变更:5 项为可选增量或未引用、1 项为纯新增导出、 字号范围为宽松化调整。逐项核对未发现需要处置的行为回归,FNOS-009-07-AC-01 成立, 无需按 AC-02 登记新的处置结论。
FNOS-009-04 CodeBuddy 全新授权回流
为验证 AC-01「可完成浏览器授权登录,账号出现在账号列表」不是靠既有凭据的渲染假象, 在真实 NAS 上做了一次从零开始的授权:先移开 profile 里的 codebuddy-auth.json, 确认界面回到「账号管理 0 / 暂无已登录账号」,再走完整登录流程。
| 步骤 | 观察 | 判定 |
|---|---|---|
移开 codebuddy-auth.json | 界面账号管理归 0,提示「暂无已登录账号」 | 未读旧数据 |
| 点「添加账号」→ 填备注名「全新授权验证」→「打开登录」 | 打开 codebuddy.cn/login/?platform=CLI&state=... | 授权入口正常 |
| 浏览器完成 OAuth 授权回流 | 账号管理变为 1,卡片显示新备注名 | AC-01 成立 |
| 检查磁盘 | 生成新文件 3165 字节,账号 id fc10e370-034d-435c-8804-c3da50fd7549(备份中无此 id),含 accessToken / refreshToken,domain: www.codebuddy.cn | 全新授权,非刷新旧账号 |
| 从备份恢复 | 6 个原账号完好,界面 6/6 可用 | 数据无损失 |
该验证同时确认了插件读取凭据的两个前提:文件必须存在且属主可读 (
mode若为0000,isOwnerOnly守卫会放行但readFile失败,被静默吞成「未登录」)。
FNOS-009-06 升级基线快照(补充)
连续 5 次升级(5.5.0 → 5.5.5)后各数据项保持,且已记录升级前的 sha256 基线, 供后续升级逐字节比对。详见上文「FNOS-009-06 用户数据兼容」一节。