Skip to content

FNOS-001 批量真机验收规程 ​

本规程规定批量验证转换应用能否正常安装、启动、打开的标准流程。它与 单应用测试用例集 的分工是:用例集定义「怎么测、什么算通过」, 本规程定义「怎么把这套用例跑遍全部应用并留下可复核证据」。

1. 为什么需要批量通道 ​

设备端 appcenter-cli 在多数目标设备上因 /run/trim_app_cgi/rpcbroker 权限直接 panic, 无法批量安装;逐个在应用中心 UI 手工安装也不具备可重复性与可留证性。因此批量验收走 Midscene 驱动 fnOS 应用中心 Web UI:复用已登录的真实浏览器会话,用应用中心自身的 HTTP 接口完成安装、启动、卸载,并把 UI 通道与设备端 CLI 通道的结论等效对待。

通道工具适用限制
设备端 CLIappcenter-cli目标设备上直接安装与检查需要设备侧 RPC 权限,权限不足时启动即 panic
Web UI 自动化Midscene(@midscene/web,Bridge 模式)appcenter-cli 不可用时,复用用户已登录的 Chrome 走应用中心只能观察接口返回与设备状态,不能直接读容器日志

两条通道不改变判定口径:结论必须落到「设备上发生了什么」,不能由本地构建结果代替。

2. 环境准备 ​

项说明
浏览器用户本人已登录 fnOS 的 Chrome,并安装 Midscene 扩展
桥配置由 DSH 本地桥提供的固定四行(curl /v1/midscene-env 取),不涉及任何第三方模型密钥
设备通道SSH 可读 /var/log/apps/<app>.log(应用生命周期日志)与 /vol2/@appdata、/vol2/@apphome
暂存目录设备上一个应用中心可见的目录(用于「以 NAS 路径安装」,免去逐个文件上传)

设备地址、SSH 账号、口令、令牌一律由环境变量传入,不写进任何文档、脚本或结果文件。 文档中设备一律以「目标测试设备」指代,命令示例用 ${FNOS_HOST} 占位。

3. 单应用流程(每个应用都走完这七步) ​

步动作判据
1本地 fnpack build产出 .fpk;失败记 BUILD_FAIL
2把 .fpk 放到设备暂存目录—
3应用中心解析安装包(packageSourceType=file + path)解析成功、拿到 appName 与 version
4读取安装向导 schema 并自动填参必填项全部可填;否则记 UNVERIFIED
5提交安装任务并轮询到终态安装任务成功;否则记 FAIL 或 BLOCKED
6等待应用进入 running,再探测入口端口顺序不可颠倒:先卸会导致端口已关闭,探针必然失败
7卸载(选「删除所有数据」)→ 清理残留目录 → 取失败日志卸载后设备上不得留下该应用

3.1 向导自动填参规则 ​

字段类型处理
端口(initValue 合法)用应用自带默认值,保持「测的就是发布配置」
password / 密钥类生成随机值;长度按 helpText 写明的位数(如「32 位随机字符串」),否则服务端会拒绝
文本类且带 rules[].pattern用 initValue 先过一遍正则,不通过则记 UNVERIFIED
必填但无默认值、且不是密钥记 UNVERIFIED,不猜值

必填项无法自动填写时必须记 UNVERIFIED 而不是猜一个值——猜出来的值会让「装上了」失去意义。

填参必须服从该字段自己的正则契约,尤其是数据库连接类的「合并字段」:有些应用的 DB_HOST 并不只填主机,而是要求「地址:端口/数据库名」(rules.pattern 形如 ^.+:\d+/.+$),compose 里 直接把它接在 user:pass@ 之后。只填裸 IP 会让 URL 少了端口与库名,容器连到默认端口/默认库后 起不来,入口 000 —— 现象与「应用缺陷」完全一样,但根因在填参。判定方法:该字段的正则同时 约束了冒号与斜杠,就按 地址:引擎端口/应用名 填。注意不能用 /\d/ 去测正则字符串—— 模式里的 \d 是两个字符,不含真实数字。

3.2 入口探测 ​

  • 入口地址取应用中心返回的 appServiceInfo.urls.port,拼 http://${FNOS_HOST}:<port>/。

  • 应用中心报 running 不等于容器已在监听:探针要重试若干次再下结论,否则会把「启动慢」误判成 FAIL。

  • 入口打不开时必须同时取容器现场:docker ps -a 上的名称/状态/端口映射,以及该容器 docker logs --tail 30。只看 HTTP 码无法区分三种完全不同的原因:

    容器现场含义
    Restarting (N) / Exited容器崩溃循环:应用自身起不来,去读容器日志定因
    Up ... 且端口映射正确,但 HTTP 仍 000进程在跑但没监听该端口,或仍在初始化
    查不到对应容器容器根本没被创建

    实测中大量「入口 000」都属于第一类(例如某应用已重启 243 次),若只记 HTTP 码会把它们 混成一类,既无法整改也无法判断是不是转换缺陷。

  • 判定可访问的 HTTP 码:200 / 301 / 302 / 303 / 307 / 401 / 403。404 也算「服务已响应」—— 应用 UI 可能在子路径下,不代表起不来。000(连接失败)视为不可访问。

4. 判定分级 ​

判定含义计入转换缺陷
PASS安装成功 + running + 入口可访问—
FAIL安装或启动失败,且日志指向转换自身的问题✅
BLOCKED依赖外部数据库/中间件、端口被占用等环境原因❌
UNVERIFIED向导必填项无法自动填写,需人工确认❌
BUILD_FAIL本地 fnpack build 失败✅

红线(与 FNOS-001-04-AC-06 一致):不得出现「安装成功且显示运行中,但实际不可访问」。 端口被占用时,允许的两种表现是:向导表单阶段拦截,或应用中心显示未运行。

5. 失败取证 ​

/var/log/apps/<app>.log 是每次安装都必须读的,不是「失败时才读」。它是生命周期脚本的 stdout/stderr 汇总,容器起不来时真正的报错往往只出现在这里(install_callback 的输出、 docker compose up 的失败原因)。踩过的坑:某数据库应用安装后一直停在 start,界面只显示 「docker 不可用」,实际是默认端口 5432 被系统内置服务占用,该报错只在这份日志里 (failed to bind host port ... address already in use)。若把它归成「docker 不可用」, 整改方向会完全跑偏。

场景取证方式
安装/启动失败SSH 读 /var/log/apps/<app>.log 尾部(生命周期脚本的 stdout/stderr 汇总)
入口不可访问同上 + 应用中心返回的 status、appServiceInfo,以及 docker ps -a 的容器状态与端口映射
容器崩溃循环docker logs --tail 30 <容器名>:缺依赖、配置解析失败、权限不足等真因只出现在容器日志里
卸载后残留查 /vol2/@appdata/<app>、/vol2/@apphome/<app> 是否仍在
端口被占用生命周期日志里 failed to bind host port ... address already in use;再 ss -ltn 确认占用者。默认端口撞系统服务时属环境冲突,改端口即可,不计转换缺陷
地址池耗尽docker network ls 与 ip route 对照:Docker 数据库里已无记录但内核仍占着网段,即孤儿网桥

日志读取不需要提权:应用生命周期日志对普通账号可读。日志里出现 exit status 1: open /docker/docker-compose.yaml 是卸载阶段的正常噪声(应用目录已被框架移除),判断失败原因要 看它之前的输出。

6. 设备清理纪律 ​

批量验收会反复在设备上装卸应用,必须保证:

  1. 逐个测试、逐个卸载,不在设备上堆积多个测试应用;
  2. 卸载后核对三项:桌面入口图标消失、入口端口不再服务(curl 返回 000)、应用中心列表中无该应用;
  3. 清理数据目录残留:应用卸载回调可能删不掉自己的数据目录(见 转换规约的待确认项),需由有权限的账号清掉, 否则设备上会堆积空目录;
  4. 暂存目录里的 .fpk 用完即删;
  5. 回收 compose 网络:卸载后确认 docker network ls 里没有该应用的 <app>_default, 仍在就删除。每个残留网络占掉一个 /16,累计十几个后 Docker 默认地址池耗尽,之后 任何应用都装不上(all predefined address pools have been fully subnetted)。 批量跑里每应用记录一条 networkLingered,既防累积,也是「平台是否回收网络」的证据。

7. 证据与可复核性 ​

  • 批量结果以 JSON Lines 逐条落盘,每个应用一条,中途崩溃不丢已完成部分;
  • 每条记录含:应用名、判定、阶段、应用状态、入口地址与 HTTP 码、卸载结果、残留计数、 失败时的日志尾部;
  • 结论进 真实环境验收记录,未执行的验收条件保持「未执行」。

8. 已知限制 ​

限制说明
鉴权应用中心的会话 Cookie(osrt / ost)是 HttpOnly,页内 JS 读不到,document.cookie 只剩埋点与语言项。所以不能「从浏览器抄一份 Cookie 交给 curl」——会话一过期整批就失效。正确做法是经 Midscene Bridge 连上已登录标签页,用页面同源 fetch 调接口,凭证由浏览器自己带
桥接稳定性扩展后台休眠会断开传输;标签页被用户关掉也会失去目标。验收器每次调用前确保连接,断开即重连,目标标签页缺失时自动重开一次,单次调用保持短往返
会话时效会话本身过期后所有调用返回未授权。验收器识别到 401/403 或空响应会抛 SESSION_EXPIRED 并中止整批,重新登录后重跑同一命令即可续跑
镜像拉取冷缓存下单个应用可能拉十几分钟;轮询窗口不足会把「还在拉镜像」误判成失败
镜像源被阻断目标测试设备可能只能访问部分站点:github.com、docker.com 返回 200,但 registry-1.docker.io / auth.docker.io 的 TCP 直接超时(域名还被解析到保留段地址)。此时 docker pull 必然失败,不得据此判应用缺陷。做法是先确认阻断范围(分别 curl 站点与 registry),再从设备可达的镜像加速器按前缀拉取,最后 docker tag 打回原名——compose 引用的是原名,无需改 daemon.json、不污染全局配置。判断镜像源是否真的可用看 /v2/ 返回 401(需鉴权=正常)而不是 000/HTML 页面;返回 HTML 说明那不是 registry 代理
向导必填项无法自动填写的应用记 UNVERIFIED。可选用占位值模式(FNOS_FILL_PLACEHOLDERS=1)让它也跑出结论,但结果必须带 placeholderFilled 标记,不得与真实填写混为一谈
设备级阻塞地址池耗尽、daemon 不可达、磁盘满会让所有安装失败,必须整批中止;单个镜像源不通只影响该应用,不得据此中止
重启循环的日志容器刚被 kill 时 docker logs 可能读不到内容。取证要重试并放大窗口;仍为空则补 docker inspect(ExitCode / OOMKilled),否则只剩 status=Restarting,无法归因
并发桥接端口互斥,不得并行跑两个验收器;脚本用结果文件旁的 .lock 保证单实例,第二个实例应当直接退出而不是抢占端口
原生应用没有对外服务端口的应用(如纯命令行工具类)安装后不常驻 running,状态多为 nostart。等待 running 对它们永远是超时,须把 nostart 当终态接受,是否可用交给安装成功与否判断

9. 已发现的缺陷类 ​

批量跑的价值不只是给出通过率,更在于把同一个原因造成的一批失败归并成缺陷类、一次修掉。

缺陷类现象根因处置
种子配置未随包安装报 Seed file missing: .../seed/... 并 exit 1单文件挂载需要源文件先存在,回调从 ${TRIM_APPDEST}/seed/ 播种,但种子从未入库;把种子放成顶层 seed/ 也没用——fnpack 只打包标准条目种子改放 apps/<app>/app/seed/<相对路径>:app/ 会打进 app.tgz 并解包到 TRIM_APPDEST,正好是回调期望的位置
卸载谎报删除打印 Application data deleted. 但 rm 报 Permission denied回调在删平台管理的目录,包用户无权 unlink 其父目录只清空目录内容并如实上报残留,见转换规约
.gitignore 误伤种子种子文件加不进版本库data/ 规则匹配任意层级,而种子路径必须镜像 data/...规则锚定到仓库根:/data/
数据目录属主未对齐(挂载点不可写)容器 Restarting (N)、入口 000;日志形如 EACCES: permission denied, copyfile ... -> /data/settings.js、PermissionError: ... '/app/webdav'、mkdir: cannot create directory '/bitnami/...'镜像启动时要把默认配置/运行时文件写进挂载目录,而以非 root(包用户)运行时该目录归 root 所有。install_callback 是空操作时没有任何环节把属主交给运行用户在 install_callback 里以包用户预建数据目录并自检可写(touch 探针,失败写用户可见错误而非静默重启)。不要为此提权到 root
非 root 运行与镜像自身用户冲突同上,/opt/config、/app/...、/root/server/logs 等镜像层内路径报权限拒绝有些镜像的 entrypoint 必须以 root 改写自身目录(如 Apache 改 /etc/apache2/),加 user: 覆盖后必然失败按镜像实际要求区分:能对齐属主的保持非 root;确实必须 root 的(改写系统目录)去掉 user: 覆盖,并在注释里写明真机结论
相对路径配置落盘错位PermissionError: ... '/app/app'配置项写成相对路径,进程 cwd 是 /app 时被拼成 /app/app容器内路径一律写绝对路径
端口未声明导致入口无监听者容器 Up、端口映射存在,但入口恒为 000compose 映射了某端口,而应用配置文件里没声明该端口(如 Manticore 的 HTTP 接口必须显式 listen = 9308:http)让配置文件声明的监听端口与 compose 映射逐一对齐
必需配置文件缺失Error: Config file 'mkdocs.yml' does not exist. 并重启循环镜像启动即要求数据目录内存在某配置文件,首次安装目录为空随包提供最小可用种子,install_callback 仅在缺失时铺入,不覆盖用户内容
配置保存后不生效「应用设置 → 运行设置」改完保存并重启,容器 env 仍是旧值;应用照旧连旧地址模板 cmd/main 的 ensure_containers_created 先判断「容器已存在」就 return,docker compose up -d 从未执行。注意不要把重建写进 config_callback:它以包用户运行、无 docker 权限,且平台检查其退出码,非 0 会让保存整体失败去掉提前 return,让 up -d 每次状态检查执行(配置未变时幂等,变了由 compose 自行重建);config_callback 保持空操作。判定依据是容器 ID 变化且新配置在入口生效

变更记录 ​

日期变更
2026-10-20建立本规程:把单应用验证流程批量化。确立「先探入口后卸载」的顺序纪律、「安装中心报 running 不等于已监听」的探针重试、按 helpText 位数生成密钥的填参规则,以及 PASS / FAIL / BLOCKED / UNVERIFIED / BUILD_FAIL 五级判定。同步确立设备地址与凭据不入文档的纪律。
2026-10-22通道改为应用中心 HTTP 接口直连(不再依赖浏览器自动化);补 ENV_BLOCKED 与整批中止判据、compose 网络回收、容器现场与容器日志取证、按容器状态区分「入口 000」的三种原因;新增「已发现的缺陷类」小节与占位值模式说明。
2026-10-28全量跑完 189 个应用,通过率 54.5%(103 PASS)。通道改为「Midscene Bridge 复用浏览器会话 + 页面同源 fetch」:HttpOnly Cookie 无法抄出,必须由浏览器代发。补原生应用(nostart)判定、重启循环日志重试与 docker inspect 兜底、凭据类向导字段自动生成、桥接断线与标签页丢失的自愈。缺陷类新增 6 类,全部围绕非 root 运行时的目录属主与镜像自身用户的冲突,其中 6 个应用已修复并真机复测通过。
2026-11-03补「镜像源被阻断」的绕行做法(可达加速器按前缀拉取后 docker tag 打回原名,不改全局配置)与「配置保存后不生效」缺陷类。通道在桥接之外增加 curl + 会话 Cookie(Cookie 从浏览器网络请求头读取,仍是 HttpOnly 不可由页内 JS 取),验收器支持 FNOS_SSH_PORT 指定非 22 的 SSH 端口。真机复验 4 个已构建应用全部 PASS。
2026-11-03抽检 10 个应用(覆盖无依赖 Web、PHP+MySQL、Node+MySQL、Go/Node+PostgreSQL)全部 PASS。补「合并式 DB_HOST 必须按应用自己的正则契约填 地址:端口/库名」的填参规则 —— 此前一律填裸 IP,让 fn-umami、fn-miniflux 连到默认 5432 后起不来,被误判成应用缺陷。另记:库名以向导 DB_NAME 的 initValue 为准(如 wiki),验证前须先在目标实例建库。设备升级到 fnOS 1.2.1000 后复验,通道与应用行为无变化。

基于 VitePress 构建