| name | fnos-publish |
| description | 飞牛 App 发布助手 —— 交互式勾选,把任意飞牛项目打成 .fpk 并推镜像到 Docker Hub,支持国内 fnOS 与海外 Fygo OS 双平台。当用户说「发布飞牛 app」「打包上架」「打飞牛包」「出 fpk」「推镜像到 docker」「上架应用中心」「发海外版/Fygo」「fnos publish」「打包并上传 docker」时使用。镜像在飞牛 NAS 上原生构建(x86_64),国内直连 Docker Hub 被墙时自动走本地代理 + crane 推送。 |
飞牛 App 发布助手
把任意飞牛项目(含 fnos/manifest + Dockerfile + fnos/app/docker/docker-compose.yaml)打包并发布。脚本 fnos_publish.sh 按 manifest 的 appname 自省,同一份逻辑通用于所有同构项目。交互式勾选:先用 AskUserQuestion 让用户选清楚,再执行。
脚本路径:~/.claude/skills/fnos-publish/fnos_publish.sh
交互流程(务必先问清再动手)
用 AskUserQuestion 依次收集(能自动确定的就别问):
- 选 App(若当前 cwd 已是飞牛项目则默认它;否则在你放飞牛项目的目录下扫描候选,可设
FNOS_PROJECTS_DIR)
for d in "${FNOS_PROJECTS_DIR:-.}"/*/; do [ -f "$d/fnos/manifest" ] && \
printf "%s\t%s\n" "$(basename "$d")" "$(grep -E '^display_name' "$d/fnos/manifest" | awk -F= '{print $2}' | xargs)"; done
- 目标平台(multiSelect):国内 fnOS / 海外 Fygo OS →
--target inland|oversea|both
- 动作(multiSelect):
- 在 NAS 构建镜像(默认开;不勾=
--no-build 复用已有镜像 tar)
- 推镜像到 Docker Hub →
--push
- 国内额外出离线版 →
--offline
- 上传 .fpk 到 NAS →
--upload
- 若选了
--push 且 docker 未登录 Docker Hub → 让用户提供 access token,先非交互登录。
- 若选了 海外 且项目没有
fnos/manifest.oversea → 提示是否现在生成英文版 manifest(见下)。
确认后进入项目目录执行,例如:
cd /path/to/your/fnos-project && \
DOCKER_USER=your-dockerhub-user NAS_HOST=your-nas-ip NAS_PASS='****' \
bash ~/.claude/skills/fnos-publish/fnos_publish.sh --target both --push --offline
产物(dist/)
| target | 文件 | 说明 |
|---|
| inland | <app>-<ver>.fpk | 国内默认版,image 改写成 <user>/<app>:<ver>,应用中心自动从 Docker Hub 拉 |
inland+--offline | <app>-<ver>-offline.fpk + <app>-image-<ver>.tar.gz | 离线版,pull_policy:never + 本地 load |
| oversea | <app>-<ver>-oversea.fpk | 海外版,image 同样走 Docker Hub(海外无墙,天然合适) |
| — | 安装说明-*.txt / 发布说明-海外.txt | 给用户/审核的说明 |
国内 fnOS vs 海外 Fygo OS 差异
| 维度 | 国内 fnOS (developer.fnnas.com) | 海外 Fygo OS (developer.fygonas.com) |
|---|
| 打包工具 | fnpack build | fygopack create / fygopack pack |
| 上架 | appcenter-cli / 官方流程 | 邮件提交 app-review@fygo.io |
| manifest | arch=x86_64、service_port… | 示例有 platform、maintainer_url、ctl_stop;建议英文 display_name/desc |
| 语言 | 中文 | en / ja / zh(海外优先英文) |
| 镜像仓库 | 国内走阿里云 mirror | Docker Hub(对海外用户无墙,直接用 <user>/<app>) |
本脚本对两端都用「手工 tar 复刻官方 .fpk 结构」出包,fygopack 的完整 manifest 字段规范官方文档未给全。若要官方校验,在装有 fygopack 的机器上跑一次 fygopack pack 核对字段后再提交审核。
🌐 海外 i18n / 语言转化(四层,缺一层就会露中文)
海外包不是「换个英文 manifest」就完事——UI、桌面入口、说明都要英文。脚本已自动处理能自动的部分,其余是应用侧前置条件,--target oversea|both 时会打印缺失清单:
| 层 | 素材 | 谁负责 |
|---|
| 1. 应用信息 | fnos/manifest.oversea:英文 display_name/desc(脚本优先用它) | 你提供 |
| 2. 桌面入口 | fnos/app/ui/config.oversea:桌面图标 title 改英文(脚本自动覆盖进包) | 你提供 |
| 3. 前端 UI | 应用必须已接 i18n(Umi locale / react-i18next)并有 en-US 词条 | 应用代码 |
| 4. 默认语言 | 脚本自动在海外 compose 的 environment 注入 APP_LOCALE=en-US + LANG=en_US.UTF-8 | 脚本自动 |
约定:前端/后端启动时读环境变量 APP_LOCALE(缺省 zh-CN),据此选默认语言。这样同一个 Docker 镜像既能国内跑中文、海外跑英文,不必构建两份镜像。给各飞牛 app 统一加这段即可:
const locale = (import.meta as any)?.env?.APP_LOCALE
|| (window as any).APP_LOCALE || 'zh-CN';
setLocale(locale, false);
LOCALE = os.getenv("APP_LOCALE", "zh-CN")
若某 app 尚未接 i18n,海外包里 UI 仍是中文——这层打包改不了,得先在应用里补 i18n + en-US 词条。脚本会明确告警,别忽略。
生成海外素材
fnos/manifest.oversea:从 fnos/manifest 复制,display_name/desc 改英文,其余保留。
fnos/app/ui/config.oversea:从 fnos/app/ui/config 复制,title 改英文。
- 缺失时脚本用中文素材兜底并告警,Fygo 审核前务必补齐。
⚠️ 陷阱 1:本地 tag vs registry 引用(别手改源 compose)
源 docker-compose.yaml 里 image: <app>:latest 是本地构建 tag。脚本已自动分流:registry 版 sed 改写成 <user>/<app>:<ver>,离线版加 pull_policy: never。不要手动改源 compose 的 image 行。打完可校验:
mkdir -p /tmp/c && tar xzf dist/<app>-<ver>.fpk -C /tmp/c app.tgz && \
tar xzf /tmp/c/app.tgz -C /tmp/c docker/docker-compose.yaml && grep -E 'image:|pull_policy' /tmp/c/docker/docker-compose.yaml
⚠️ 陷阱 2:国内直连 Docker Hub 上传被定向阻断(脚本已自动绕过)
docker push 由 daemon 发起,国内直连 registry-1.docker.io 时控制面能过,一传大 blob(如 opencv/numpy 层 ~200MB)就在 Head .../blobs/sha256:... 处 EOF——定向阻断,重试无效;且 Docker Desktop 的 daemon 在 VM 里吃不到宿主 127.0.0.1 代理。
脚本的 --push 已内置解法:探测本地代理(Clash Verge 常见 7897,Clash 7890),命中则用 **crane(纯 CLI,直吃 HTTPS_PROXY,读 ~/.docker/config.json 登录态)**经代理直推;未探测到代理才退回 docker push。
前置:先登录(token 从 Docker Hub → Account Settings → Personal access tokens,给 Read/Write):
echo '<dckr_pat_...>' | docker login -u <your-dockerhub-user> --password-stdin
可用 PROXY=http://127.0.0.1:<port> 显式指定代理端口。
推送后必做
- 首次推某仓库后,到 Docker Hub 把
<user>/<app> 设 Public(否则别人无登录拉不到)。
验证:DOCKER_CONFIG=$(mktemp -d) HTTPS_PROXY=http://127.0.0.1:7897 crane manifest <user>/<app>:<ver> 能拿到=Public。
- 清理:解压出的
dist/<app>-image-<ver>.tar(~几百 MB)验证后删,保留 .tar.gz。
- 安全:docker access token 用完可到 Docker Hub 吊销重建;
dist/ 别提交进 git。
版本号
单一事实源:fnos/manifest 的 version(脚本读它当 <ver>),与后端 main.py 的 version="…" 保持一致。升版本先改这两处再打包。
决策提示(给 Claude)
- 只改前端/
fnos/ 配置、没动后端 → 勾「复用镜像」(--no-build)。
- 改了后端/依赖/Dockerfile → 必须重新构建(默认)。
- 「让别人也能装」→ 必须
--push + 仓库设 Public,否则只能发离线版两文件。
- 缺
NAS_PASS 会卡在 SSH 密码交互 → 先问用户要密码或让其 export NAS_PASS。
- 海外提交前确认英文 manifest 与 Public 仓库就绪。