commit 39684c4232cb8cd4a4157dae56fdcccbdcfbb9f8 Author: mrlee <470349233@qq.com> Date: Thu Oct 1 19:02:25 2026 +0800 feat: import Havline fnOS application package diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd685d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +fnpack.exe +.build/ +dist/ +*.fpk +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..65c0aca --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Havline fnOS 应用打包 + +本目录保存 Havline 的飞牛 fnOS 应用工程。 + +- 应用工程:[`havline/`](havline/) +- 官方工具:`fnpack` +- 当前工具版本:`1.2.3` +- 主程序仓库:`F:\Fonu-main\havline` + +## 当前状态 + +已经完成: + +- 使用 `fnpack create havline --template docker` 生成官方 Docker 应用骨架; +- 配置 `manifest`、应用入口、Docker Compose、权限和资源; +- 增加安装向导、配置向导和卸载向导; +- 增加安装、升级、配置和卸载生命周期脚本; +- 支持通过环境变量 `HAVLINE_INITIAL_ADMIN_PASSWORD` 初始化管理员。 + +仍需完成: + +- 将 `app/docker/docker-compose.yaml` 中的 `ghcr.io/your-org/havline` 替换为实际多架构镜像地址; +- 将模板图标替换为 Havline 正式图标; +- 在飞牛测试设备验证安装、启动、入口、升级和卸载; +- 验证 amd64 与 arm64 镜像均可用。 + +## 打包 + +Windows PowerShell: + +```powershell +cd F:\Fonu-main\fnos +.\build.ps1 +``` + +脚本会先复制一份不含 `.DS_Store` 的干净工程,再调用 `fnpack` 打包,输出到: + +```text +fnos/dist/havline-.fpk +``` + +如果 `fnpack` 已在 PATH 中,也可以传入路径: + +```powershell +.\build.ps1 -FnpackPath fnpack +``` + +## 测试安装 + +```bash +appcenter-cli install-fpk havline.fpk +``` + +安装后检查: + +1. 应用中心显示 Havline; +2. Docker 镜像可以拉取; +3. Havline 容器能够启动; +4. 桌面入口可以打开管理界面; +5. 数据写入 `${TRIM_PKGVAR}`; +6. 卸载默认保留数据。 + +## 当前默认端口 + +| 用途 | 安装向导默认值 | 容器端口 | +| --- | ---: | ---: | +| 管理界面 | `6893` | `6893` | +| HTTP 反向代理 | `18080` | `80` | +| HTTPS 反向代理 | `9443` | `443` | + +## 首发边界 + +- 不挂载 Docker Socket。 +- 不启动 Havline 自带 updater。 +- 不使用 host 网络。 +- 不共享数据库、证书、密钥、Nginx、FRP 和 cloudflared 目录。 +- 应用中心负责安装、升级、启动、停止和卸载。 diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..b5b9bbb --- /dev/null +++ b/build.ps1 @@ -0,0 +1,41 @@ +param( + [string]$FnpackPath = "" +) + +$ErrorActionPreference = "Stop" +$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$project = Join-Path $root "fnos\havline" +$version = (Get-Content -Raw (Join-Path $root "havline\VERSION")).Trim() + +if (-not (Test-Path -LiteralPath $project)) { + throw "找不到飞牛应用工程:$project" +} + +if (-not $FnpackPath) { + $localTool = Join-Path $PSScriptRoot "fnpack.exe" + $FnpackPath = if (Test-Path -LiteralPath $localTool) { $localTool } else { "fnpack" } +} + +$stamp = Get-Date -Format "yyyyMMddHHmmss" +$stage = Join-Path $PSScriptRoot ".build\havline-$stamp" +New-Item -ItemType Directory -Force $stage | Out-Null +robocopy $project $stage /E /XF .DS_Store /NFL /NDL /NJH /NJS /NP | Out-Null +if ($LASTEXITCODE -ge 8) { + throw "复制飞牛应用工程失败,robocopy 退出码:$LASTEXITCODE" +} + +Push-Location $PSScriptRoot +try { + & $FnpackPath build -d $stage + if ($LASTEXITCODE -ne 0) { + throw "fnpack build 失败,退出码:$LASTEXITCODE" + } + $output = Join-Path $PSScriptRoot "havline.fpk" + $dist = Join-Path $PSScriptRoot "dist" + New-Item -ItemType Directory -Force $dist | Out-Null + Copy-Item -LiteralPath $output -Destination (Join-Path $dist "havline-$version.fpk") -Force + Write-Output "FPK: $(Join-Path $dist "havline-$version.fpk")" +} +finally { + Pop-Location +} diff --git a/havline/ICON.PNG b/havline/ICON.PNG new file mode 100644 index 0000000..c221ba6 Binary files /dev/null and b/havline/ICON.PNG differ diff --git a/havline/ICON_256.PNG b/havline/ICON_256.PNG new file mode 100644 index 0000000..d5c1bd4 Binary files /dev/null and b/havline/ICON_256.PNG differ diff --git a/havline/app/docker/docker-compose.yaml b/havline/app/docker/docker-compose.yaml new file mode 100644 index 0000000..ea42396 --- /dev/null +++ b/havline/app/docker/docker-compose.yaml @@ -0,0 +1,21 @@ +services: + havline: + # TODO: 发布多架构镜像后,把 your-org 替换为实际镜像命名空间。 + image: ghcr.io/your-org/havline:${TRIM_APPVER} + container_name: havline + restart: unless-stopped + ports: + - "${wizard_service_port}:6893" + - "${wizard_nginx_http_port}:80" + - "${wizard_nginx_https_port}:443" + volumes: + - "${TRIM_PKGVAR}:/data" + environment: + HAVLINE_DATA_DIR: /data + HAVLINE_LISTEN: ":6893" + HAVLINE_NGINX_HTTP_PORT: "80" + HAVLINE_NGINX_HTTPS_PORT: "443" + HAVLINE_SESSION_SECRET: "" + HAVLINE_UPDATER_SOCKET: "" + HAVLINE_INITIAL_ADMIN_PASSWORD: "${wizard_initial_admin_password}" + TZ: "${wizard_timezone}" diff --git a/havline/app/ui/config b/havline/app/ui/config new file mode 100644 index 0000000..a4dadc3 --- /dev/null +++ b/havline/app/ui/config @@ -0,0 +1,16 @@ +{ + ".url": { + "havline.main": { + "title": "Havline", + "icon": "images/icon_{0}.png", + "type": "iframe", + "protocol": "http", + "port": "${wizard_service_port}", + "url": "/", + "allUsers": false, + "control": { + "accessPerm": "readonly" + } + } + } +} diff --git a/havline/app/ui/images/icon_256.png b/havline/app/ui/images/icon_256.png new file mode 100644 index 0000000..d5c1bd4 Binary files /dev/null and b/havline/app/ui/images/icon_256.png differ diff --git a/havline/app/ui/images/icon_64.png b/havline/app/ui/images/icon_64.png new file mode 100644 index 0000000..c221ba6 Binary files /dev/null and b/havline/app/ui/images/icon_64.png differ diff --git a/havline/cmd/config_callback b/havline/cmd/config_callback new file mode 100644 index 0000000..a87b239 --- /dev/null +++ b/havline/cmd/config_callback @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +for var in wizard_service_port wizard_nginx_http_port wizard_nginx_https_port; do + value="${!var:-}" + if ! [[ "$value" =~ ^[0-9]+$ ]]; then + echo "$var 必须是数字端口" > "$TRIM_TEMP_LOGFILE" + exit 1 + fi +done + +exit 0 diff --git a/havline/cmd/config_init b/havline/cmd/config_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/havline/cmd/config_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/havline/cmd/install_callback b/havline/cmd/install_callback new file mode 100644 index 0000000..e380a62 --- /dev/null +++ b/havline/cmd/install_callback @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +mkdir -p "$TRIM_PKGVAR" "$TRIM_PKGVAR/backups" +chown -R "$TRIM_USERNAME:$TRIM_GROUPNAME" "$TRIM_PKGVAR" + +echo "Havline 数据目录已准备:$TRIM_PKGVAR" +exit 0 diff --git a/havline/cmd/install_init b/havline/cmd/install_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/havline/cmd/install_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/havline/cmd/main b/havline/cmd/main new file mode 100644 index 0000000..40a9506 --- /dev/null +++ b/havline/cmd/main @@ -0,0 +1,29 @@ +#!/bin/bash +set -u + +CONTAINER_NAME="havline" +LOG_FILE="${TRIM_TEMP_LOGFILE:-/dev/stderr}" + +log_error() { + echo "$1" > "$LOG_FILE" +} + +is_running() { + docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -q '"Status": "running"' +} + +case "${1:-}" in + start|stop) + exit 0 + ;; + status) + if is_running; then + exit 0 + fi + exit 3 + ;; + *) + log_error "Unknown command: ${1:-}" + exit 1 + ;; +esac diff --git a/havline/cmd/uninstall_callback b/havline/cmd/uninstall_callback new file mode 100644 index 0000000..7c99014 --- /dev/null +++ b/havline/cmd/uninstall_callback @@ -0,0 +1,8 @@ +#!/bin/bash +set -u + +if [ "${wizard_uninstall_mode:-keep}" = "purge" ]; then + rm -rf "$TRIM_PKGVAR" +fi + +exit 0 diff --git a/havline/cmd/uninstall_init b/havline/cmd/uninstall_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/havline/cmd/uninstall_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/havline/cmd/upgrade_callback b/havline/cmd/upgrade_callback new file mode 100644 index 0000000..c22a518 --- /dev/null +++ b/havline/cmd/upgrade_callback @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +if [ -f "$TRIM_PKGVAR/havline.db" ] && [ ! -r "$TRIM_PKGVAR/havline.db" ]; then + echo "数据库不可读:$TRIM_PKGVAR/havline.db" > "$TRIM_TEMP_LOGFILE" + exit 1 +fi + +if [ -f "$TRIM_PKGVAR/session_secret" ] && [ ! -r "$TRIM_PKGVAR/session_secret" ]; then + echo "会话密钥不可读:$TRIM_PKGVAR/session_secret" > "$TRIM_TEMP_LOGFILE" + exit 1 +fi + +exit 0 diff --git a/havline/cmd/upgrade_init b/havline/cmd/upgrade_init new file mode 100644 index 0000000..06bd986 --- /dev/null +++ b/havline/cmd/upgrade_init @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/havline/config/privilege b/havline/config/privilege new file mode 100644 index 0000000..cff6a3a --- /dev/null +++ b/havline/config/privilege @@ -0,0 +1,7 @@ +{ + "defaults": { + "run-as": "package" + }, + "username": "havline", + "groupname": "havline" +} diff --git a/havline/config/resource b/havline/config/resource new file mode 100644 index 0000000..633747b --- /dev/null +++ b/havline/config/resource @@ -0,0 +1,17 @@ +{ + "docker-project": { + "projects": [ + { + "name": "havline", + "path": "docker" + } + ] + }, + "data-share": { + "shares": [ + { + "name": "havline/backups" + } + ] + } +} diff --git a/havline/manifest b/havline/manifest new file mode 100644 index 0000000..ad86aa8 --- /dev/null +++ b/havline/manifest @@ -0,0 +1,14 @@ +appname = havline +version = 1.9.0 +display_name = Havline +desc = NAS 公网访问管理:反向代理、DDNS、HTTPS 证书、FRP、Cloudflare Tunnel 与一键升级 +platform = all +source = thirdparty +maintainer = Havline +maintainer_url = http://45.207.223.190:10082/mrlee/Havline +distributor = Havline +desktop_uidir = ui +desktop_applaunchname = havline.main +checkport = false +ctl_stop = true +changelog = 首发飞牛应用:支持安装向导、端口配置、数据持久化和应用中心升级 diff --git a/havline/wizard/config b/havline/wizard/config new file mode 100644 index 0000000..dd0a823 --- /dev/null +++ b/havline/wizard/config @@ -0,0 +1,42 @@ +[ + { + "stepTitle": "端口设置", + "items": [ + { + "type": "text", + "field": "wizard_nginx_http_port", + "label": "HTTP 反向代理端口", + "initValue": "18080", + "rules": [ + { "required": true, "message": "请输入 HTTP 端口" }, + { "pattern": "^[0-9]+$", "message": "端口必须是数字" } + ] + }, + { + "type": "text", + "field": "wizard_nginx_https_port", + "label": "HTTPS 反向代理端口", + "initValue": "9443", + "rules": [ + { "required": true, "message": "请输入 HTTPS 端口" }, + { "pattern": "^[0-9]+$", "message": "端口必须是数字" } + ] + } + ] + }, + { + "stepTitle": "基础设置", + "items": [ + { + "type": "select", + "field": "wizard_timezone", + "label": "时区", + "initValue": "Asia/Shanghai", + "options": [ + { "label": "Asia/Shanghai", "value": "Asia/Shanghai" }, + { "label": "UTC", "value": "UTC" } + ] + } + ] + } +] diff --git a/havline/wizard/install b/havline/wizard/install new file mode 100644 index 0000000..f187338 --- /dev/null +++ b/havline/wizard/install @@ -0,0 +1,70 @@ +[ + { + "stepTitle": "端口设置", + "items": [ + { + "type": "text", + "field": "wizard_service_port", + "label": "Havline 管理端口", + "initValue": "6893", + "rules": [ + { "required": true, "message": "请输入管理端口" }, + { "pattern": "^[0-9]+$", "message": "端口必须是数字" } + ] + }, + { + "type": "text", + "field": "wizard_nginx_http_port", + "label": "HTTP 反向代理端口", + "initValue": "18080", + "rules": [ + { "required": true, "message": "请输入 HTTP 端口" }, + { "pattern": "^[0-9]+$", "message": "端口必须是数字" } + ] + }, + { + "type": "text", + "field": "wizard_nginx_https_port", + "label": "HTTPS 反向代理端口", + "initValue": "9443", + "rules": [ + { "required": true, "message": "请输入 HTTPS 端口" }, + { "pattern": "^[0-9]+$", "message": "端口必须是数字" } + ] + } + ] + }, + { + "stepTitle": "管理员初始化", + "items": [ + { + "type": "password", + "field": "wizard_initial_admin_password", + "label": "初始管理员密码", + "rules": [ + { "required": true, "message": "请输入初始管理员密码" }, + { "min": 8, "message": "密码至少 8 位" } + ] + }, + { + "type": "tips", + "helpText": "首次启动时使用该密码创建 admin 管理员。登录后请在系统设置中修改密码。" + } + ] + }, + { + "stepTitle": "基础设置", + "items": [ + { + "type": "select", + "field": "wizard_timezone", + "label": "时区", + "initValue": "Asia/Shanghai", + "options": [ + { "label": "Asia/Shanghai", "value": "Asia/Shanghai" }, + { "label": "UTC", "value": "UTC" } + ] + } + ] + } +] diff --git a/havline/wizard/uninstall b/havline/wizard/uninstall new file mode 100644 index 0000000..3545dd5 --- /dev/null +++ b/havline/wizard/uninstall @@ -0,0 +1,21 @@ +[ + { + "stepTitle": "卸载选项", + "items": [ + { + "type": "radio", + "field": "wizard_uninstall_mode", + "label": "数据处理", + "initValue": "keep", + "options": [ + { "label": "保留数据(推荐)", "value": "keep" }, + { "label": "删除全部数据", "value": "purge" } + ] + }, + { + "type": "tips", + "helpText": "删除数据将移除数据库、证书、Nginx 配置、FRP 与 Cloudflare 配置,且无法恢复。" + } + ] + } +]