fnos-publish
飞牛 App 发布助手 —— 交互式勾选,把任意飞牛项目打成 .fpk 并推镜像到 Docker Hub,支持国内 fnOS 与海外 Fygo OS 双平台。当用户说「发布飞牛 app」「打包上架」「打飞牛包」「出 fpk」「推镜像到 docker」「上架应用中心」「发海外版/Fygo」「fnos publish」「打包并上传 docker」时使用。镜像在飞牛 NAS 上原生构建(x86_64),国内直连 Docker Hub 被墙时自动走本地代理 + crane 推送。
来源信息
- 仓库
- go7th/fnos-publish
- 最近来源活动
- 2026年7月8日 08:02
- 检测到的 SKILL.md 语言
- 中文
- 星标
- 0
- 分支
- 0
安装方式
默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。
检查来源文件
决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。
正在显示 SKILL.md
SKILL.md
来源说明 · 只读预览- 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** 依次收集(能自动确定的就别问):
1. **选 App**(若当前 cwd 已是飞牛项目则默认它;否则在你放飞牛项目的目录下扫描候选,可设 `FNOS_PROJECTS_DIR`)
```bash
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
```
2. **目标平台**(multiSelect):国内 fnOS / 海外 Fygo OS →`--target inland|oversea|both`
3. **动作**(multiSelect):
- 在 NAS 构建镜像(默认开;不勾=`--no-build` 复用已有镜像 tar)
- 推镜像到 Docker Hub →`--push`
- 国内额外出离线版 →`--offline`
- 上传 .fpk 到 NAS →`--upload`
4. 若选了 `--push` 且 `docker` 未登录 Docker Hub → 让用户提供 access token,先非交互登录。
5. 若选了 **海外** 且项目没有 `fnos/manifest.oversea` → 提示是否现在生成英文版 manifest(见下)。
确认后进入项目目录执行,例如:
```bash
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 统一加这段即可:
```ts
// 前端(Umi):入口处
const locale = (import.meta as any)?.env?.APP_LOCALE
|| (window as any).APP_LOCALE || 'zh-CN'; // 由容器注入,海外=en-US
setLocale(locale, false);
```
```python
# 后端(FastAPI):读同名变量决定返回文案语言
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 行**。打完可校验:
```bash
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
# registry 版应是 <user>/<app>:<ver>;离线版应是 <app>:latest + pull_policy: never
```
## ⚠️ 陷阱 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):
```bash
echo '<dckr_pat_...>' | docker login -u <your-dockerhub-user> --password-stdin
```
可用 `PROXY=http://127.0.0.1:<port>` 显式指定代理端口。
## 推送后必做
1. 首次推某仓库后,到 Docker Hub 把 `<user>/<app>` 设 **Public**(否则别人无登录拉不到)。
验证:`DOCKER_CONFIG=$(mktemp -d) HTTPS_PROXY=http://127.0.0.1:7897 crane manifest <user>/<app>:<ver>` 能拿到=Public。
2. 清理:解压出的 `dist/<app>-image-<ver>.tar`(~几百 MB)验证后删,保留 `.tar.gz`。
3. 安全: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 仓库就绪。
在 GitHub 查看