mirror of
https://github.com/arch3rPro/1Panel-Appstore.git
synced 2026-06-10 08:31:37 +08:00
chore: optimize 1panel app builder skill
This commit is contained in:
+42
-181
@@ -1,200 +1,61 @@
|
|||||||
# 1Panel App Builder
|
# 1Panel App Builder
|
||||||
|
|
||||||
快速生成符合 1Panel 本地应用商店规范的 APP 配置文件。
|
这个目录提供 1Panel 本地应用商店打包辅助脚本。AI 使用入口是 `SKILL.md`,人工常用入口是下面几个脚本。
|
||||||
|
|
||||||
## 功能特性
|
## 快速使用
|
||||||
|
|
||||||
- ✅ 支持多种输入源(GitHub、docker-compose、docker run、本地文件)
|
|
||||||
- ✅ 自动抽取应用信息并生成标准化配置
|
|
||||||
- ✅ 自动下载应用图标(支持多个图标源)
|
|
||||||
- ✅ 生成中英文 README
|
|
||||||
- ✅ 符合 1Panel 应用商店规范
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
1panel-app-builder/
|
|
||||||
├── SKILL.md # 技能定义文件
|
|
||||||
├── README.md # 使用文档
|
|
||||||
├── templates/ # 配置模板
|
|
||||||
│ ├── data.yml.tpl # 应用元数据模板
|
|
||||||
│ └── docker-compose.yml.tpl # 编排文件模板
|
|
||||||
├── scripts/ # 工具脚本
|
|
||||||
│ ├── generate-app.sh # 主生成脚本
|
|
||||||
│ └── download-icon.sh # 图标下载工具
|
|
||||||
└── examples/ # 使用示例
|
|
||||||
└── example-usage.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 基本用法
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GitHub 项目
|
cd /root/github/1Panel-Appstore/skills
|
||||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
|
||||||
|
|
||||||
# docker-compose 文件链接
|
# 从 compose 生成草稿,跳过图标下载
|
||||||
./scripts/generate-app.sh https://raw.githubusercontent.com/.../docker-compose.yml
|
./scripts/generate-app.sh --app-key my-app --name MyApp --version 1.2.3 --icon-mode skip ./docker-compose.yml
|
||||||
|
|
||||||
# docker run 命令
|
# 下载或复用图标
|
||||||
./scripts/generate-app.sh "docker run -d --name=alist -p 5244:5244 xhofe/alist:v3.45.0"
|
./scripts/download-icon.sh --mode cache-only my-app ../apps/my-app/logo.png
|
||||||
|
./scripts/download-icon.sh --mode required --url https://example.com/logo.png my-app ../apps/my-app/logo.png
|
||||||
|
|
||||||
# 本地文件
|
# 验证应用
|
||||||
./scripts/generate-app.sh ./my-docker-compose.yml
|
./scripts/validate-app.sh ../apps/my-app
|
||||||
|
|
||||||
|
# 验证 skills 工具链
|
||||||
|
./tests/run_all.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 输出示例
|
## 生成脚本
|
||||||
|
|
||||||
```
|
`scripts/generate-app.sh` 支持 GitHub URL、compose URL、本地 compose 文件和 `docker run` 命令。
|
||||||
./apps/alist/v3.45.0/
|
|
||||||
├── data.yml # 应用元数据
|
常用参数:
|
||||||
├── docker-compose.yml # 编排文件
|
|
||||||
├── logo.png # 应用图标
|
```text
|
||||||
├── README.md # 中文简介
|
--output <dir> 输出目录,默认 ./apps
|
||||||
└── README_en.md # 英文简介
|
--app-key <key> 指定应用目录名
|
||||||
|
--name <name> 指定应用显示名
|
||||||
|
--version <tag> 指定具体版本目录和镜像 tag
|
||||||
|
--icon-mode <mode> auto|required|skip|cache-only
|
||||||
|
--icon-url <url> 使用指定图标 URL
|
||||||
|
--force 允许覆盖已有生成目录
|
||||||
|
--dry-run 只解析并输出结果,不写文件
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 生成的配置文件
|
## 图标策略
|
||||||
|
|
||||||
#### data.yml 示例
|
脚本不会创建占位图。图标模式:
|
||||||
|
|
||||||
```yaml
|
- `auto`: 优先缓存,缺失时尝试网络源;找不到也不阻塞生成。
|
||||||
name: AList
|
- `skip`: 跳过图标处理,适合草稿生成。
|
||||||
tags:
|
- `cache-only`: 只读 `skills/.cache/icons`,不访问网络。
|
||||||
- 实用工具
|
- `required`: 找不到有效图标时失败。
|
||||||
- 云存储
|
|
||||||
title: 支持多存储的文件列表程序和私人网盘
|
|
||||||
description: 支持多存储的文件列表程序和私人网盘
|
|
||||||
additionalProperties:
|
|
||||||
key: alist
|
|
||||||
name: AList
|
|
||||||
tags:
|
|
||||||
- Storage
|
|
||||||
- Tool
|
|
||||||
shortDescZh: 支持多存储的文件列表程序和私人网盘
|
|
||||||
shortDescEn: Supporting multi-storage file listing program
|
|
||||||
website: https://alist.nn.ci/
|
|
||||||
github: https://github.com/alist-org/alist
|
|
||||||
architectures:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
#### docker-compose.yml 示例
|
## 校验范围
|
||||||
|
|
||||||
```yaml
|
`scripts/validate-app.sh` 会检查:
|
||||||
services:
|
|
||||||
alist:
|
|
||||||
container_name: ${CONTAINER_NAME}
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:5244"
|
|
||||||
volumes:
|
|
||||||
- ./data/data:/opt/alist/data
|
|
||||||
environment:
|
|
||||||
- PUID=0
|
|
||||||
- PGID=0
|
|
||||||
- UMASK=022
|
|
||||||
image: xhofe/alist:v3.45.0
|
|
||||||
labels:
|
|
||||||
createdBy: "Apps"
|
|
||||||
networks:
|
|
||||||
1panel-network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
## 高级用法
|
- 应用目录结构和必需文件。
|
||||||
|
- `additionalProperties.key` 是否等于目录名。
|
||||||
|
- `PANEL_APP_PORT_*` 是否在版本 `data.yml` 中定义。
|
||||||
|
- `latest/` 镜像是否使用 `latest` tag。
|
||||||
|
- `logo.png` 是否疑似 HTML/XML 错误响应。
|
||||||
|
- 1Panel 常见 compose 约束,如 `${CONTAINER_NAME}`、`1panel-network`、`createdBy: "Apps"`。
|
||||||
|
|
||||||
### 指定输出目录
|
生成结果仍需要人工审查 metadata、README、架构、端口语义和多服务依赖。
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/generate-app.sh https://github.com/alist-org/alist ./my-apps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 单独下载图标
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/download-icon.sh alist ./logo.png 200
|
|
||||||
```
|
|
||||||
|
|
||||||
### 图标源优先级
|
|
||||||
|
|
||||||
1. [Dashboard Icons](https://dashboardicons.com/icons)
|
|
||||||
2. [Simple Icons](https://simpleicons.org/)
|
|
||||||
3. [selfh.st Icons](https://selfh.st/icons/)
|
|
||||||
|
|
||||||
## 依赖工具
|
|
||||||
|
|
||||||
必需:
|
|
||||||
- `bash` - 脚本执行环境
|
|
||||||
- `curl` - HTTP 请求
|
|
||||||
- `jq` - JSON 解析
|
|
||||||
- `yq` - YAML 解析(可选,用于高级解析)
|
|
||||||
|
|
||||||
可选:
|
|
||||||
- `ImageMagick` (convert) - 图标尺寸调整
|
|
||||||
- `sips` (macOS) - 图标尺寸调整
|
|
||||||
- `tree` - 目录树显示
|
|
||||||
|
|
||||||
## 配置模板说明
|
|
||||||
|
|
||||||
### data.yml 模板变量
|
|
||||||
|
|
||||||
| 变量 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| `${APP_NAME}` | 应用名称 | AList |
|
|
||||||
| `${APP_KEY}` | 应用键名 | alist |
|
|
||||||
| `${TITLE_ZH}` | 中文标题 | 文件列表程序 |
|
|
||||||
| `${DESC_ZH}` | 中文描述 | 支持多存储... |
|
|
||||||
| `${TAG_1}` | 标签1 | 实用工具 |
|
|
||||||
| `${WEBSITE}` | 官网 | https://alist.nn.ci/ |
|
|
||||||
| `${GITHUB}` | GitHub | https://github.com/alist-org/alist |
|
|
||||||
|
|
||||||
### docker-compose.yml 模板变量
|
|
||||||
|
|
||||||
| 变量 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| `${SERVICE_NAME}` | 服务名 | alist |
|
|
||||||
| `${IMAGE}` | 镜像名 | xhofe/alist |
|
|
||||||
| `${TAG}` | 镜像标签 | v3.45.0 |
|
|
||||||
| `${PORT}` | 端口 | 5244 |
|
|
||||||
|
|
||||||
## 下一步操作
|
|
||||||
|
|
||||||
生成配置后:
|
|
||||||
|
|
||||||
1. ✅ 检查 `data.yml` 中的应用信息是否准确
|
|
||||||
2. ✅ 补充完善应用描述和标签
|
|
||||||
3. ✅ 替换 `logo.png` 为合适的应用图标
|
|
||||||
4. ✅ 测试 `docker-compose.yml` 是否正常工作
|
|
||||||
5. ✅ 提交到 1Panel 应用商店仓库
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 图标下载失败怎么办?
|
|
||||||
|
|
||||||
A: 手动从以下网站下载图标:
|
|
||||||
- https://dashboardicons.com/icons?q=<应用名>
|
|
||||||
- https://simpleicons.org/?q=<应用名>
|
|
||||||
- https://selfh.st/icons/
|
|
||||||
|
|
||||||
### Q: 如何处理多服务应用?
|
|
||||||
|
|
||||||
A: 生成后手动编辑 `docker-compose.yml`,添加其他服务。
|
|
||||||
|
|
||||||
### Q: 版本号如何确定?
|
|
||||||
|
|
||||||
A: 默认从镜像标签提取,也可手动指定。
|
|
||||||
|
|
||||||
## 参考资源
|
|
||||||
|
|
||||||
- [1Panel 官方文档](https://1panel.cn/docs/)
|
|
||||||
- [1Panel 应用商店仓库](https://github.com/1Panel-dev/appstore)
|
|
||||||
- [Docker Compose 文档](https://docs.docker.com/compose/)
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|||||||
+114
-413
@@ -1,451 +1,152 @@
|
|||||||
---
|
---
|
||||||
name: 1panel-app-builder
|
name: 1panel-app-builder
|
||||||
description: >
|
description: Use when packaging Docker deployments as 1Panel local app store apps, including GitHub projects, docker-compose.yml files, docker run commands, app metadata, version directories, icons, README files, and 1Panel validation.
|
||||||
Build 1Panel local app store configurations from Docker deployments. Use when:
|
|
||||||
- User provides a GitHub project link and wants to add it to 1Panel app store
|
|
||||||
- User has a docker-compose.yml or docker run command and needs 1Panel app format
|
|
||||||
- User wants to create a local app for 1Panel panel
|
|
||||||
- User mentions "1Panel app", "1Panel 应用商店", "本地应用", "app store", or similar
|
|
||||||
|
|
||||||
Supports: GitHub repos, Docker Hub images, docker-compose files, docker run commands.
|
|
||||||
Output: Complete app folder with data.yml, docker-compose.yml, logo.png, README.md
|
|
||||||
|
|
||||||
Always use this skill for 1Panel app packaging - it knows the exact directory structure,
|
|
||||||
variable naming conventions (PANEL_APP_PORT_HTTP, CONTAINER_NAME), and 1panel-network
|
|
||||||
requirements that are easy to get wrong without guidance.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 1Panel App Store Builder
|
# 1Panel App Builder
|
||||||
|
|
||||||
Transform Docker deployments into 1Panel-compatible local app store packages.
|
Build or review 1Panel local app store packages from Docker deployments.
|
||||||
|
|
||||||
## Understanding 1Panel App Structure
|
## When To Use
|
||||||
|
|
||||||
A valid 1Panel app follows this directory structure:
|
Use this skill when the user asks to:
|
||||||
|
|
||||||
```
|
- Add or package a 1Panel local app store app.
|
||||||
app-key/ # App identifier (lowercase, hyphenated)
|
- Convert a GitHub project, `docker-compose.yml`, or `docker run` command into `apps/<app-key>/`.
|
||||||
├── logo.png # App icon (64x64 or 128x128 recommended)
|
- Fix 1Panel app metadata, port variables, compose files, README files, icons, or version directories.
|
||||||
├── data.yml # App metadata (top-level)
|
- Validate an app package before commit.
|
||||||
├── README.md # Chinese documentation
|
|
||||||
├── README_en.md # English documentation (optional)
|
For detailed real app examples, read `references/1panel-examples.md` only when needed.
|
||||||
└── 1.0.0/ # Version directory (semver or "latest")
|
|
||||||
├── data.yml # Parameter definitions (form fields)
|
## Required Shape
|
||||||
├── docker-compose.yml # Compose file with variable substitution
|
|
||||||
├── data/ # Persistent data directory
|
```text
|
||||||
└── scripts/ # Optional scripts
|
apps/<app-key>/
|
||||||
└── upgrade.sh # Upgrade script
|
├── data.yml
|
||||||
|
├── logo.png
|
||||||
|
├── README.md
|
||||||
|
├── README_en.md
|
||||||
|
└── <version>/
|
||||||
|
├── data.yml
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── data/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step-by-Step Workflow
|
Some apps also include `latest/`. When both `latest/` and a concrete version exist, `latest/` must use image tag `latest`, and the concrete version directory should use the pinned tag.
|
||||||
|
|
||||||
### Step 1: Gather Source Information
|
## Workflow
|
||||||
|
|
||||||
Based on user input type, extract Docker deployment details:
|
1. Inspect the source deployment.
|
||||||
|
- GitHub: inspect README/deploy docs and registry image.
|
||||||
|
- Compose: parse services, image tags, ports, volumes, environment, dependencies.
|
||||||
|
- Docker run: parse image, `--name`, `-p`, `-v`, and `-e`.
|
||||||
|
2. Choose a stable app key.
|
||||||
|
- Lowercase, hyphenated, directory-safe.
|
||||||
|
- `additionalProperties.key` must match the directory name.
|
||||||
|
3. Generate or edit app files.
|
||||||
|
- Prefer `latest/` plus a concrete version when a concrete image tag is known.
|
||||||
|
- Preserve source volumes and environment unless they conflict with 1Panel conventions.
|
||||||
|
4. Resolve a real icon.
|
||||||
|
- Do not create placeholders.
|
||||||
|
- Use `--icon-mode skip` for drafts, `cache-only` for offline work, and `required` when an icon is mandatory.
|
||||||
|
5. Validate before delivery.
|
||||||
|
- Run `skills/scripts/validate-app.sh ./apps/<app-key>`.
|
||||||
|
- For Skill script changes, run `skills/tests/run_all.sh`.
|
||||||
|
|
||||||
**From GitHub Link:**
|
## 1Panel Rules
|
||||||
1. Fetch the GitHub repository README.md
|
|
||||||
2. Look for Docker installation instructions (docker run, docker-compose)
|
|
||||||
3. Extract: image name, ports, volumes, environment variables
|
|
||||||
4. Note: project name, description, official website, supported architectures
|
|
||||||
|
|
||||||
**From docker-compose.yml:**
|
- `container_name` should be `${CONTAINER_NAME}`.
|
||||||
1. Parse all services defined
|
- Services should use `restart: always`.
|
||||||
2. Extract: image versions, port mappings, volume mounts, environment variables
|
- Use external `1panel-network`.
|
||||||
3. Identify the main service (typically the one with web UI)
|
- Port mappings should use `PANEL_APP_PORT_*` variables defined in the version `data.yml`.
|
||||||
|
- Persistent mounts should prefer relative `./data/...` paths.
|
||||||
|
- Add `labels: createdBy: "Apps"`.
|
||||||
|
- Keep metadata tags aligned with top-level `data.yaml`.
|
||||||
|
|
||||||
**From docker run command:**
|
Preferred port variables:
|
||||||
1. Parse flags: `-p` (ports), `-v` (volumes), `-e` (env vars), `--name`
|
|
||||||
2. Extract the image name and tag
|
|
||||||
3. Identify exposed ports and data directories
|
|
||||||
|
|
||||||
### Step 2: Resolve Latest Version (Create Version + latest)
|
```text
|
||||||
|
PANEL_APP_PORT_HTTP
|
||||||
Always create **two** version directories when possible:
|
PANEL_APP_PORT_HTTPS
|
||||||
|
PANEL_APP_PORT_API
|
||||||
- `latest/` uses image tag `latest`
|
PANEL_APP_PORT_ADMIN
|
||||||
- `<version>/` uses the **latest concrete tag** from the registry
|
PANEL_APP_PORT_PROXY
|
||||||
|
PANEL_APP_PORT_PROXY_HTTP
|
||||||
**How to resolve the latest concrete tag:**
|
PANEL_APP_PORT_PROXY_HTTPS
|
||||||
|
PANEL_APP_PORT_DB
|
||||||
1. If the image is on **GitHub Container Registry** (`ghcr.io`), query tags from GitHub Packages (GHCR).
|
PANEL_APP_PORT_SSH
|
||||||
2. Otherwise, query **Docker Hub** tags.
|
PANEL_APP_PORT_S3
|
||||||
3. Prefer the newest semver-like tag (e.g., `v1.2.3` or `1.2.3`). If none exist, pick the most recently updated non-`latest` tag.
|
PANEL_APP_PORT_SYNC
|
||||||
4. If you cannot resolve a concrete tag, fall back to the image tag from input and warn the user.
|
|
||||||
|
|
||||||
**Rule:** The `latest/` directory must **always** use `image: ...:latest`. The `<version>/` directory must **always** use `image: ...:<version>`.
|
|
||||||
|
|
||||||
### Step 3: Generate App Key and Metadata
|
|
||||||
|
|
||||||
**App Key Rules:**
|
|
||||||
- Lowercase only
|
|
||||||
- Use hyphens for multi-word names
|
|
||||||
- Keep it short but descriptive
|
|
||||||
- Examples: `alist`, `it-tools`, `n8n-zh`, `lobe-chat-data`
|
|
||||||
|
|
||||||
**Required Metadata (top-level data.yml):**
|
|
||||||
```yaml
|
|
||||||
name: AppName # Display name (can have capitals)
|
|
||||||
tags: # Chinese tags for categorization
|
|
||||||
- 开发工具
|
|
||||||
- 实用工具
|
|
||||||
title: Brief Chinese description
|
|
||||||
description: Same as title
|
|
||||||
additionalProperties:
|
|
||||||
key: app-key # Must match directory name
|
|
||||||
name: AppName # Same as name above
|
|
||||||
tags: # English tags
|
|
||||||
- DevTool
|
|
||||||
- Utility
|
|
||||||
shortDescZh: Chinese description
|
|
||||||
shortDescEn: English description
|
|
||||||
description:
|
|
||||||
en: Full English description
|
|
||||||
zh: Full Chinese description
|
|
||||||
# Add more languages if available: ja, ko, ru, pt-br, ms, zh-Hant
|
|
||||||
type: website # or "runtime" for databases, "tool" for CLI tools
|
|
||||||
crossVersionUpdate: true # Usually true
|
|
||||||
limit: 0 # 0 = unlimited instances
|
|
||||||
recommend: 0 # 0-100, higher = more recommended
|
|
||||||
website: https://example.com # Official website
|
|
||||||
github: https://github.com/org/repo
|
|
||||||
document: https://docs.example.com
|
|
||||||
architectures: # Supported CPU architectures
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
# Add: arm/v7, arm/v6, s390x if supported
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Define Parameters (version/data.yml)
|
## Scripts
|
||||||
|
|
||||||
Parameters become UI form fields in 1Panel. Define user-configurable values:
|
Run from `/root/github/1Panel-Appstore/skills` unless noted.
|
||||||
|
|
||||||
```yaml
|
Generate a draft:
|
||||||
additionalProperties:
|
|
||||||
formFields:
|
|
||||||
# Port parameter example
|
|
||||||
- default: 8080
|
|
||||||
edit: true
|
|
||||||
envKey: PANEL_APP_PORT_HTTP # Variable name used in docker-compose
|
|
||||||
labelEn: Web Port
|
|
||||||
labelZh: Web端口
|
|
||||||
required: true
|
|
||||||
rule: paramPort # Validation rule
|
|
||||||
type: number # Field type
|
|
||||||
label: # Multi-language labels
|
|
||||||
en: Web Port
|
|
||||||
zh: Web端口
|
|
||||||
ja: Webポート
|
|
||||||
ko: Web 포트
|
|
||||||
|
|
||||||
# Text parameter example (for API keys, passwords, etc.)
|
|
||||||
- default: ""
|
|
||||||
edit: true
|
|
||||||
envKey: API_KEY
|
|
||||||
labelEn: API Key
|
|
||||||
labelZh: API密钥
|
|
||||||
required: false
|
|
||||||
rule: paramCommon
|
|
||||||
type: text
|
|
||||||
|
|
||||||
# Password parameter example
|
|
||||||
- default: "changeme"
|
|
||||||
edit: true
|
|
||||||
envKey: ADMIN_PASSWORD
|
|
||||||
labelEn: Admin Password
|
|
||||||
labelZh: 管理员密码
|
|
||||||
required: true
|
|
||||||
rule: paramCommon
|
|
||||||
type: password
|
|
||||||
|
|
||||||
# Select parameter example
|
|
||||||
- default: "sqlite"
|
|
||||||
edit: true
|
|
||||||
envKey: DATABASE_TYPE
|
|
||||||
labelEn: Database Type
|
|
||||||
labelZh: 数据库类型
|
|
||||||
required: true
|
|
||||||
type: select
|
|
||||||
values:
|
|
||||||
- label: SQLite
|
|
||||||
value: sqlite
|
|
||||||
- label: PostgreSQL
|
|
||||||
value: postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameter Types:**
|
|
||||||
- `number`: For ports, counts
|
|
||||||
- `text`: For API keys, URLs, names
|
|
||||||
- `password`: For secrets (masked input)
|
|
||||||
- `select`: For dropdown choices
|
|
||||||
- `boolean`: For toggle switches
|
|
||||||
|
|
||||||
**Validation Rules:**
|
|
||||||
- `paramPort`: Valid port number (1-65535)
|
|
||||||
- `paramCommon`: Non-empty string
|
|
||||||
- `paramExtUrl`: Valid URL format
|
|
||||||
- Empty string: No validation
|
|
||||||
|
|
||||||
### Port envKey Naming (Use 1Panel Standard Names)
|
|
||||||
|
|
||||||
Prefer these **standard** `envKey` names (observed across apps in `apps/`):
|
|
||||||
|
|
||||||
- `PANEL_APP_PORT_HTTP` (primary Web/UI port)
|
|
||||||
- `PANEL_APP_PORT_HTTPS`
|
|
||||||
- `PANEL_APP_PORT_API`
|
|
||||||
- `PANEL_APP_PORT_ADMIN`
|
|
||||||
- `PANEL_APP_PORT_PROXY`
|
|
||||||
- `PANEL_APP_PORT_PROXY_HTTP`
|
|
||||||
- `PANEL_APP_PORT_PROXY_HTTPS`
|
|
||||||
- `PANEL_APP_PORT_DB`
|
|
||||||
- `PANEL_APP_PORT_SSH`
|
|
||||||
- `PANEL_APP_PORT_S3`
|
|
||||||
- `PANEL_APP_PORT_SYNC`
|
|
||||||
|
|
||||||
**Guideline:** Use the most semantically correct name first; only invent new suffixes if none of the standard names fit.
|
|
||||||
|
|
||||||
### Step 5: Create docker-compose.yml
|
|
||||||
|
|
||||||
Convert the original Docker deployment to use variable substitution:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
app-name:
|
|
||||||
container_name: ${CONTAINER_NAME}
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:8080"
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/data
|
|
||||||
environment:
|
|
||||||
- TZ=Asia/Shanghai
|
|
||||||
image: org/image:tag
|
|
||||||
labels:
|
|
||||||
createdBy: "Apps"
|
|
||||||
networks:
|
|
||||||
1panel-network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical Rules:**
|
|
||||||
1. Always use `${CONTAINER_NAME}` for container_name
|
|
||||||
2. Always use `restart: always`
|
|
||||||
3. Always connect to `1panel-network` (external network)
|
|
||||||
4. Port mapping uses `PANEL_APP_PORT_*` variables from version/data.yml
|
|
||||||
5. Volume paths use relative `./data/` for persistence
|
|
||||||
6. Add `labels: createdBy: "Apps"`
|
|
||||||
7. Keep original environment variables but use defaults
|
|
||||||
|
|
||||||
**Port Mapping Format (Important):**
|
|
||||||
|
|
||||||
Use quoted mappings like:
|
|
||||||
```
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:8080"
|
|
||||||
```
|
|
||||||
and ensure the same `envKey` exists in `version/data.yml`.
|
|
||||||
|
|
||||||
### Step 6: Create README Files
|
|
||||||
|
|
||||||
**README.md (Chinese):**
|
|
||||||
```markdown
|
|
||||||
# AppName
|
|
||||||
|
|
||||||
简短描述应用的功能和特点。
|
|
||||||
|
|
||||||
## 功能特点
|
|
||||||
|
|
||||||
- 特点1
|
|
||||||
- 特点2
|
|
||||||
- 特点3
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 默认端口
|
|
||||||
|
|
||||||
- Web界面: 8080
|
|
||||||
|
|
||||||
### 默认账号密码
|
|
||||||
|
|
||||||
- 用户名: admin
|
|
||||||
- 密码: changeme (请在部署后立即修改)
|
|
||||||
|
|
||||||
### 数据目录
|
|
||||||
|
|
||||||
应用数据存储在 `./data` 目录。
|
|
||||||
|
|
||||||
## 相关链接
|
|
||||||
|
|
||||||
- 官方网站: https://example.com
|
|
||||||
- GitHub: https://github.com/org/repo
|
|
||||||
- 文档: https://docs.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**README_en.md (English, optional):**
|
|
||||||
```markdown
|
|
||||||
# AppName
|
|
||||||
|
|
||||||
Brief description of the application.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Feature 1
|
|
||||||
- Feature 2
|
|
||||||
- Feature 3
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Default Port
|
|
||||||
|
|
||||||
- Web UI: 8080
|
|
||||||
|
|
||||||
### Default Credentials
|
|
||||||
|
|
||||||
- Username: admin
|
|
||||||
- Password: changeme (change after deployment)
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Website: https://example.com
|
|
||||||
- GitHub: https://github.com/org/repo
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 7: Download App Icon
|
|
||||||
|
|
||||||
**Priority order:**
|
|
||||||
1. **Search the GitHub repository** for common icon files (e.g., `logo.png`, `icon.png`, `assets/logo.png`, `.github/icon.png`). Use the repo's raw download URL if found.
|
|
||||||
2. If not found, search and download the app logo from these sources (in order):
|
|
||||||
|
|
||||||
1. **Dashboard Icons**: https://dashboardicons.com/icons?q={app-name}
|
|
||||||
2. **Simple Icons**: https://simpleicons.org/?q={app-name}
|
|
||||||
3. **Selfh.st Icons**: https://selfh.st/icons/
|
|
||||||
|
|
||||||
**Do not** create a placeholder or incorrect icon. If not found, warn and leave `logo.png` for manual replacement.
|
|
||||||
|
|
||||||
### Step 8: Create Data Directory Structure
|
|
||||||
|
|
||||||
Create the `data/` directory inside the version folder:
|
|
||||||
```bash
|
|
||||||
mkdir -p app-key/version/data
|
|
||||||
```
|
|
||||||
|
|
||||||
This directory will be mounted as a volume for persistent data.
|
|
||||||
|
|
||||||
## Quality Checklist
|
|
||||||
|
|
||||||
Before delivering the app package, verify:
|
|
||||||
|
|
||||||
- [ ] App key is lowercase with hyphens only
|
|
||||||
- [ ] All required fields in top-level data.yml are present
|
|
||||||
- [ ] `latest/` and `<version>/` directories both exist (when a concrete tag is resolvable)
|
|
||||||
- [ ] `latest/` uses image tag `latest`
|
|
||||||
- [ ] `<version>/` uses the concrete latest tag from registry
|
|
||||||
- [ ] Version data.yml has proper parameter definitions (formFields)
|
|
||||||
- [ ] docker-compose.yml uses variable substitution correctly
|
|
||||||
- [ ] `1panel-network` is defined as external network
|
|
||||||
- [ ] Volume paths use relative `./data/` format
|
|
||||||
- [ ] README files are created with proper documentation
|
|
||||||
- [ ] Logo file exists (logo.png)
|
|
||||||
- [ ] Version directory follows semver; avoid only `latest`
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Database Applications
|
|
||||||
```yaml
|
|
||||||
# For apps that need a database (PostgreSQL, MySQL, etc.)
|
|
||||||
# Include database as a separate service in docker-compose
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
db:
|
|
||||||
image: postgres:15
|
|
||||||
volumes:
|
|
||||||
- ./data/db:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=appdb
|
|
||||||
- POSTGRES_USER=appuser
|
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-Service Applications
|
|
||||||
```yaml
|
|
||||||
# For apps with multiple services (frontend + backend, etc.)
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
image: org/frontend:tag
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:80"
|
|
||||||
backend:
|
|
||||||
image: org/backend:tag
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_API}:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Applications with Reverse Proxy
|
|
||||||
```yaml
|
|
||||||
# For apps that don't expose ports directly
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
# No ports section - accessed through 1Panel's reverse proxy
|
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automation Scripts
|
|
||||||
|
|
||||||
Use the provided scripts for faster workflow:
|
|
||||||
|
|
||||||
### generate-app.sh - 自动生成应用配置
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# GitHub 项目
|
./scripts/generate-app.sh --app-key my-app --name MyApp --version 1.2.3 --icon-mode skip ./docker-compose.yml
|
||||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
|
||||||
|
|
||||||
# docker-compose 文件
|
|
||||||
./scripts/generate-app.sh ./docker-compose.yml
|
|
||||||
|
|
||||||
# docker run 命令
|
|
||||||
./scripts/generate-app.sh "docker run -d --name=nginx -p 80:80 nginx:latest"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### download-icon.sh - 下载应用图标
|
Useful generation options:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--output <dir> Output base directory. Default: ./apps
|
||||||
|
--app-key <key> Override app directory key.
|
||||||
|
--name <name> Override display name.
|
||||||
|
--version <tag> Override concrete version and image tag.
|
||||||
|
--icon-mode <mode> auto|required|skip|cache-only
|
||||||
|
--icon-url <url> Download icon from a known URL.
|
||||||
|
--force Allow overwriting an existing generated directory.
|
||||||
|
--dry-run Print parsed values without writing files.
|
||||||
|
```
|
||||||
|
|
||||||
|
Download an icon:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/download-icon.sh redis ./logo.png 200
|
./scripts/download-icon.sh --mode cache-only redis ./logo.png
|
||||||
|
./scripts/download-icon.sh --mode required --url https://example.com/logo.png myapp ./logo.png
|
||||||
```
|
```
|
||||||
|
|
||||||
### validate-app.sh - 验证配置完整性
|
Validate an app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/validate-app.sh ./apps/myapp
|
./scripts/validate-app.sh ../apps/<app-key>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reference Files
|
Validate Skill tooling:
|
||||||
|
|
||||||
- `references/1panel-examples.md` - 完整的真实应用配置示例(AList、NocoDB等)
|
```bash
|
||||||
- `examples/example-usage.md` - 使用示例和工作流程
|
./tests/run_all.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Icon Policy
|
||||||
|
|
||||||
**Issue**: App won't start
|
Icon lookup order:
|
||||||
- Check if ports are already in use
|
|
||||||
- Verify volume permissions
|
|
||||||
- Check container logs: `docker logs ${CONTAINER_NAME}`
|
|
||||||
|
|
||||||
**Issue**: Data not persisting
|
1. Known explicit URL (`--icon-url`).
|
||||||
- Ensure volumes are correctly mapped to `./data/`
|
2. Local cache under `skills/.cache/icons`.
|
||||||
- Check directory permissions
|
3. Dashboard Icons.
|
||||||
|
4. Simple Icons.
|
||||||
|
5. selfh.st Icons.
|
||||||
|
|
||||||
**Issue**: Environment variables not working
|
Missing icons are acceptable for drafts only. For final app delivery, leave `logo.png` absent and tell the user what is missing instead of inventing an inaccurate image.
|
||||||
- Verify variable names match between data.yml and docker-compose.yml
|
|
||||||
- Check default values are appropriate
|
|
||||||
|
|
||||||
**Issue**: 图标下载失败
|
## Validation Gate
|
||||||
- 从以下网站手动下载:
|
|
||||||
- https://dashboardicons.com/icons?q={app-name}
|
Before considering an app ready:
|
||||||
- https://simpleicons.org/?q={app-name}
|
|
||||||
- https://selfh.st/icons/
|
- Run `./scripts/validate-app.sh ../apps/<app-key>`.
|
||||||
|
- Confirm `latest/` uses `:latest`.
|
||||||
|
- Confirm concrete version directories use matching image tags or document the exception.
|
||||||
|
- Confirm every compose `PANEL_APP_PORT_*` variable exists in the version `data.yml`.
|
||||||
|
- Inspect generated README and metadata manually; the generator is a starting point, not an authority.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- Keeping a placeholder `logo.png`.
|
||||||
|
- Using a host absolute volume path when `./data/...` would work.
|
||||||
|
- Forgetting to define a port variable used by `docker-compose.yml`.
|
||||||
|
- Letting `additionalProperties.key` drift from the app directory name.
|
||||||
|
- Treating generated metadata, descriptions, tags, and architectures as final without review.
|
||||||
|
|||||||
@@ -4,19 +4,19 @@
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"prompt": "帮我把这个项目打包成1Panel本地应用:https://github.com/0xJacky/nginx-ui 这是一个Nginx配置管理工具,用docker部署,默认端口是9000",
|
"prompt": "帮我把这个项目打包成1Panel本地应用:https://github.com/0xJacky/nginx-ui 这是一个Nginx配置管理工具,用docker部署,默认端口是9000",
|
||||||
"expected_output": "Create a complete 1Panel app package for nginx-ui with proper directory structure, data.yml metadata, docker-compose.yml with variable substitution, README files, and placeholder logo",
|
"expected_output": "Create a 1Panel app package draft for nginx-ui with proper directory structure, data.yml metadata, docker-compose.yml variable substitution, README files, and either a real logo.png or a clear note that the icon is missing; never create a placeholder logo",
|
||||||
"files": []
|
"files": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"prompt": "我有一个docker-compose.yml文件,内容如下:\n```yaml\nversion: '3'\nservices:\n memos:\n image: neosmemo/memos:latest\n container_name: memos\n ports:\n - \"5230:5230\"\n volumes:\n - ./memos_data:/var/opt/memos\n restart: always\n```\n帮我生成1Panel应用商店的配置",
|
"prompt": "我有一个docker-compose.yml文件,内容如下:\n```yaml\nversion: '3'\nservices:\n memos:\n image: neosmemo/memos:latest\n container_name: memos\n ports:\n - \"5230:5230\"\n volumes:\n - ./memos_data:/var/opt/memos\n restart: always\n```\n帮我生成1Panel应用商店的配置",
|
||||||
"expected_output": "Parse the docker-compose file and generate 1Panel app package for memos with converted docker-compose.yml using PANEL_APP_PORT_HTTP variable, proper data.yml with port parameter definition, and supporting files",
|
"expected_output": "Parse the docker-compose file and generate a 1Panel app package for memos with PANEL_APP_PORT_HTTP, matching version data.yml port field, preserved volume mapping, and validation-ready supporting files",
|
||||||
"files": []
|
"files": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"prompt": "docker run -d --name umami -p 3000:3000 -e DATABASE_URL=postgresql://user:pass@db:5432/umami ghcr.io/umami-software/umami:postgresql-latest\n请帮我把这个umami网站统计工具做成1Panel应用",
|
"prompt": "docker run -d --name umami -p 3000:3000 -e DATABASE_URL=postgresql://user:pass@db:5432/umami ghcr.io/umami-software/umami:postgresql-latest\n请帮我把这个umami网站统计工具做成1Panel应用",
|
||||||
"expected_output": "Parse the docker run command, extract port 3000, environment variable DATABASE_URL, and generate 1Panel app package with appropriate parameter fields for database URL configuration",
|
"expected_output": "Parse the docker run command, extract port 3000 and DATABASE_URL, preserve the environment setting, and generate a 1Panel app package with appropriate form fields and validation guidance",
|
||||||
"files": []
|
"files": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,237 +1,71 @@
|
|||||||
# 使用示例
|
# 使用示例
|
||||||
|
|
||||||
## 示例 1:从 GitHub 项目生成
|
## 从本地 compose 生成草稿
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 输入
|
cd /root/github/1Panel-Appstore/skills
|
||||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
|
||||||
|
|
||||||
# 输出
|
./scripts/generate-app.sh \
|
||||||
./apps/alist/v3.45.0/
|
--app-key demo-app \
|
||||||
|
--name DemoApp \
|
||||||
|
--version 1.25.3 \
|
||||||
|
--icon-mode skip \
|
||||||
|
--output ../apps \
|
||||||
|
/tmp/demo-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
生成结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
../apps/demo-app/
|
||||||
├── data.yml
|
├── data.yml
|
||||||
├── docker-compose.yml
|
|
||||||
├── logo.png
|
|
||||||
├── README.md
|
├── README.md
|
||||||
└── README_en.md
|
├── README_en.md
|
||||||
|
├── latest/
|
||||||
|
│ ├── data.yml
|
||||||
|
│ └── docker-compose.yml
|
||||||
|
└── 1.25.3/
|
||||||
|
├── data.yml
|
||||||
|
└── docker-compose.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### 生成的 data.yml
|
`latest/docker-compose.yml` 使用 `image: ...:latest`,`1.25.3/docker-compose.yml` 使用 `image: ...:1.25.3`。
|
||||||
|
|
||||||
```yaml
|
## docker run 输入
|
||||||
name: AList
|
|
||||||
tags:
|
|
||||||
- 实用工具
|
|
||||||
- 云存储
|
|
||||||
title: AList - 文件列表程序
|
|
||||||
description: 支持多存储的文件列表程序和私人网盘
|
|
||||||
additionalProperties:
|
|
||||||
key: alist
|
|
||||||
name: AList
|
|
||||||
tags:
|
|
||||||
- Storage
|
|
||||||
- Tool
|
|
||||||
shortDescZh: 支持多存储的文件列表程序和私人网盘
|
|
||||||
shortDescEn: Supporting multi-storage file listing program
|
|
||||||
website: https://alist.nn.ci/
|
|
||||||
github: https://github.com/alist-org/alist
|
|
||||||
```
|
|
||||||
|
|
||||||
### 生成的 docker-compose.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
alist:
|
|
||||||
container_name: ${CONTAINER_NAME}
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:5244"
|
|
||||||
volumes:
|
|
||||||
- ./data/data:/opt/alist/data
|
|
||||||
environment:
|
|
||||||
- PUID=0
|
|
||||||
- PGID=0
|
|
||||||
- UMASK=022
|
|
||||||
image: xhofe/alist:v3.45.0
|
|
||||||
labels:
|
|
||||||
createdBy: "Apps"
|
|
||||||
networks:
|
|
||||||
1panel-network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 示例 2:从 docker run 命令生成
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 输入
|
./scripts/generate-app.sh \
|
||||||
./scripts/generate-app.sh "docker run -d --name=nginx -p 80:80 nginx:latest"
|
--app-key nginx-demo \
|
||||||
|
--name NginxDemo \
|
||||||
# 输出
|
--version 1.25.3 \
|
||||||
./apps/nginx/latest/
|
--icon-mode cache-only \
|
||||||
├── data.yml
|
"docker run -d --name nginx-demo -p 8080:80 -v ./data/html:/usr/share/nginx/html nginx:1.25.3"
|
||||||
├── docker-compose.yml
|
|
||||||
├── logo.png
|
|
||||||
├── README.md
|
|
||||||
└── README_en.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 生成的 docker-compose.yml
|
脚本会解析:
|
||||||
|
|
||||||
```yaml
|
- `--name` 作为服务名候选。
|
||||||
services:
|
- `-p` 生成 `PANEL_APP_PORT_*` 表单字段。
|
||||||
nginx:
|
- `-v` 保留到 compose 的 `volumes`。
|
||||||
container_name: ${CONTAINER_NAME}
|
- 镜像 tag 作为版本候选;显式 `--version` 优先。
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- 1panel-network
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:80"
|
|
||||||
volumes:
|
|
||||||
- ./data/data:/app/data
|
|
||||||
environment:
|
|
||||||
- PUID=0
|
|
||||||
- PGID=0
|
|
||||||
- UMASK=022
|
|
||||||
image: nginx:latest
|
|
||||||
labels:
|
|
||||||
createdBy: "Apps"
|
|
||||||
networks:
|
|
||||||
1panel-network:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
## 图标处理
|
||||||
|
|
||||||
## 示例 3:从 docker-compose 文件链接生成
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 输入
|
# 只使用缓存,不访问网络
|
||||||
./scripts/generate-app.sh https://raw.githubusercontent.com/portainer/portainer/develop/docker-compose.yml
|
./scripts/download-icon.sh --mode cache-only nginx ../apps/nginx-demo/logo.png
|
||||||
|
|
||||||
# 输出
|
# 已知准确图标 URL 时强制下载,失败则退出非零
|
||||||
./apps/portainer/latest/
|
./scripts/download-icon.sh --mode required --url https://example.com/nginx.png nginx ../apps/nginx-demo/logo.png
|
||||||
├── data.yml
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── logo.png
|
|
||||||
├── README.md
|
|
||||||
└── README_en.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
脚本不会创建占位图。找不到真实图标时,应保留缺失状态并在交付说明中指出。
|
||||||
|
|
||||||
## 示例 4:从本地文件生成
|
## 验证
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 创建测试文件
|
./scripts/validate-app.sh ../apps/demo-app
|
||||||
cat > /tmp/test-compose.yml << 'EOF'
|
./tests/run_all.sh
|
||||||
version: '3'
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
redis-data:
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 运行脚本
|
|
||||||
./scripts/generate-app.sh /tmp/test-compose.yml ./my-apps
|
|
||||||
|
|
||||||
# 输出
|
|
||||||
./my-apps/redis/7-alpine/
|
|
||||||
├── data.yml
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── logo.png
|
|
||||||
├── README.md
|
|
||||||
└── README_en.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
`validate-app.sh` 负责结构和常见规则检查;仍需要人工检查 README、分类标签、架构支持、默认端口和多服务依赖是否合理。
|
||||||
|
|
||||||
## 示例 5:单独下载图标
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 下载 Redis 图标
|
|
||||||
./scripts/download-icon.sh redis ./redis-logo.png 200
|
|
||||||
|
|
||||||
# 输出
|
|
||||||
✓ 从 Simple Icons 下载成功
|
|
||||||
✓ 图标下载完成: ./redis-logo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 示例 6:自定义输出目录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 输出到指定目录
|
|
||||||
./scripts/generate-app.sh https://github.com/portainer/portainer ~/my-1panel-apps
|
|
||||||
|
|
||||||
# 输出
|
|
||||||
/Users/username/my-1panel-apps/portainer/latest/
|
|
||||||
├── data.yml
|
|
||||||
├── docker-compose.yml
|
|
||||||
├── logo.png
|
|
||||||
├── README.md
|
|
||||||
└── README_en.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 完整工作流程示例
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 生成应用配置
|
|
||||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
|
||||||
|
|
||||||
# 2. 查看生成的文件
|
|
||||||
cd ./apps/alist/v3.45.0
|
|
||||||
cat data.yml
|
|
||||||
cat docker-compose.yml
|
|
||||||
|
|
||||||
# 3. 手动优化配置
|
|
||||||
# - 补充应用描述
|
|
||||||
# - 添加合适的标签
|
|
||||||
# - 替换 logo.png
|
|
||||||
|
|
||||||
# 4. 测试 docker-compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 5. 确认无误后,提交到应用商店
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: add alist app"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 问题:图标下载失败
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 手动下载图标
|
|
||||||
# 访问:https://dashboardicons.com/icons?q=alist
|
|
||||||
# 下载后保存为 logo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题:端口冲突
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 修改 docker-compose.yml 中的端口映射
|
|
||||||
ports:
|
|
||||||
- "${PANEL_APP_PORT_HTTP}:5244" # 修改此处
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题:镜像版本不对
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 手动编辑 docker-compose.yml
|
|
||||||
image: xhofe/alist:v3.45.0 # 修改版本号
|
|
||||||
```
|
|
||||||
|
|||||||
+151
-43
@@ -14,6 +14,9 @@ YELLOW='\033[1;33m'
|
|||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 路径配置
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# 图标源配置
|
# 图标源配置
|
||||||
DASHBOARD_ICONS="https://dashboardicons.com/icons"
|
DASHBOARD_ICONS="https://dashboardicons.com/icons"
|
||||||
SIMPLE_ICONS="https://cdn.simpleicons.org"
|
SIMPLE_ICONS="https://cdn.simpleicons.org"
|
||||||
@@ -21,6 +24,12 @@ SELFHST_ICONS="https://selfh.st/icons"
|
|||||||
|
|
||||||
# 默认输出尺寸
|
# 默认输出尺寸
|
||||||
DEFAULT_SIZE=200
|
DEFAULT_SIZE=200
|
||||||
|
MODE="auto"
|
||||||
|
CACHE_DIR="${ICON_CACHE_DIR:-${SCRIPT_DIR}/../.cache/icons}"
|
||||||
|
ICON_URL=""
|
||||||
|
CONNECT_TIMEOUT="${ICON_CONNECT_TIMEOUT:-3}"
|
||||||
|
MAX_TIME="${ICON_MAX_TIME:-8}"
|
||||||
|
RETRY="${ICON_RETRY:-1}"
|
||||||
|
|
||||||
# 打印帮助
|
# 打印帮助
|
||||||
print_help() {
|
print_help() {
|
||||||
@@ -28,13 +37,23 @@ print_help() {
|
|||||||
${BLUE}Icon Downloader${NC} - 从多个图标源下载应用图标
|
${BLUE}Icon Downloader${NC} - 从多个图标源下载应用图标
|
||||||
|
|
||||||
${YELLOW}用法:${NC}
|
${YELLOW}用法:${NC}
|
||||||
$0 <应用名称> [输出文件] [尺寸]
|
$0 [选项] <应用名称> [输出文件] [尺寸]
|
||||||
|
|
||||||
${YELLOW}参数:${NC}
|
${YELLOW}参数:${NC}
|
||||||
应用名称 - 应用的名称(如 alist, nginx, redis)
|
应用名称 - 应用的名称(如 alist, nginx, redis)
|
||||||
输出文件 - 图标保存路径(默认: ./logo.png)
|
输出文件 - 图标保存路径(默认: ./logo.png)
|
||||||
尺寸 - 图标尺寸(默认: 200x200)
|
尺寸 - 图标尺寸(默认: 200x200)
|
||||||
|
|
||||||
|
${YELLOW}选项:${NC}
|
||||||
|
--mode auto|required|skip|cache-only
|
||||||
|
auto: 优先缓存,缺失时尝试下载,未找到不创建占位图
|
||||||
|
required: 找不到图标时返回失败
|
||||||
|
skip: 跳过图标下载
|
||||||
|
cache-only: 只使用缓存,不访问网络
|
||||||
|
--cache-dir <目录>
|
||||||
|
图标缓存目录(默认: skills/.cache/icons)
|
||||||
|
--url <URL> 从指定 URL 下载图标
|
||||||
|
|
||||||
${YELLOW}图标源优先级:${NC}
|
${YELLOW}图标源优先级:${NC}
|
||||||
1. Dashboard Icons (dashboardicons.com)
|
1. Dashboard Icons (dashboardicons.com)
|
||||||
2. Simple Icons (simpleicons.org)
|
2. Simple Icons (simpleicons.org)
|
||||||
@@ -52,13 +71,59 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|||||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
normalize_name() {
|
||||||
|
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--' | tr -cd 'a-z0-9.-'
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_icon_file() {
|
||||||
|
local file="$1"
|
||||||
|
[[ -s "$file" ]] || return 1
|
||||||
|
if head -c 512 "$file" | grep -qiE '<!doctype|<html|<Error>|AccessDenied'; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_from_cache() {
|
||||||
|
local app_name="$1"
|
||||||
|
local output="$2"
|
||||||
|
local name_lower
|
||||||
|
name_lower=$(normalize_name "$app_name")
|
||||||
|
|
||||||
|
local ext cached
|
||||||
|
for ext in png svg webp jpg jpeg; do
|
||||||
|
cached="${CACHE_DIR}/${name_lower}.${ext}"
|
||||||
|
if valid_icon_file "$cached"; then
|
||||||
|
mkdir -p "$(dirname "$output")"
|
||||||
|
cp "$cached" "$output"
|
||||||
|
log_info "✓ 使用缓存图标: $cached"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
save_to_cache() {
|
||||||
|
local app_name="$1"
|
||||||
|
local file="$2"
|
||||||
|
local name_lower
|
||||||
|
name_lower=$(normalize_name "$app_name")
|
||||||
|
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
cp "$file" "${CACHE_DIR}/${name_lower}.png" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
# 下载并调整图标尺寸
|
# 下载并调整图标尺寸
|
||||||
download_and_resize() {
|
download_and_resize() {
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local output="$2"
|
local output="$2"
|
||||||
local size="$3"
|
local size="$3"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
|
||||||
if curl -sSL -o "$output" "$url" 2>/dev/null && [[ -s "$output" ]]; then
|
if curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$MAX_TIME" --retry "$RETRY" -o "$tmp" "$url" 2>/dev/null && valid_icon_file "$tmp"; then
|
||||||
|
mkdir -p "$(dirname "$output")"
|
||||||
|
mv "$tmp" "$output"
|
||||||
# 检查是否安装了 ImageMagick 或 sips (macOS)
|
# 检查是否安装了 ImageMagick 或 sips (macOS)
|
||||||
if command -v convert &>/dev/null; then
|
if command -v convert &>/dev/null; then
|
||||||
convert "$output" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
convert "$output" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
||||||
@@ -67,6 +132,7 @@ download_and_resize() {
|
|||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,20 +143,32 @@ try_download_icon() {
|
|||||||
local size="$3"
|
local size="$3"
|
||||||
|
|
||||||
local name_lower
|
local name_lower
|
||||||
name_lower=$(echo "$app_name" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
name_lower=$(normalize_name "$app_name")
|
||||||
|
|
||||||
log_info "尝试下载图标: $app_name"
|
log_info "尝试下载图标: $app_name"
|
||||||
|
|
||||||
|
if [[ -n "$ICON_URL" ]]; then
|
||||||
|
log_info "尝试指定图标 URL..."
|
||||||
|
if download_and_resize "$ICON_URL" "$output" "$size"; then
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
|
log_info "✓ 从指定 URL 下载成功"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 1. Dashboard Icons
|
# 1. Dashboard Icons
|
||||||
log_info "尝试 Dashboard Icons..."
|
log_info "尝试 Dashboard Icons..."
|
||||||
local dashboard_url="${DASHBOARD_ICONS}/${name_lower}.png"
|
local dashboard_url="${DASHBOARD_ICONS}/${name_lower}.png"
|
||||||
if download_and_resize "$dashboard_url" "$output" "$size"; then
|
if download_and_resize "$dashboard_url" "$output" "$size"; then
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
log_info "✓ 从 Dashboard Icons 下载成功"
|
log_info "✓ 从 Dashboard Icons 下载成功"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 尝试带 -dark 后缀
|
# 尝试带 -dark 后缀
|
||||||
if download_and_resize "${DASHBOARD_ICONS}/${name_lower}-dark.png" "$output" "$size"; then
|
if download_and_resize "${DASHBOARD_ICONS}/${name_lower}-dark.png" "$output" "$size"; then
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
log_info "✓ 从 Dashboard Icons (dark) 下载成功"
|
log_info "✓ 从 Dashboard Icons (dark) 下载成功"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -99,18 +177,20 @@ try_download_icon() {
|
|||||||
log_info "尝试 Simple Icons..."
|
log_info "尝试 Simple Icons..."
|
||||||
local simple_url="${SIMPLE_ICONS}/${name_lower}"
|
local simple_url="${SIMPLE_ICONS}/${name_lower}"
|
||||||
if download_and_resize "$simple_url" "$output" "$size"; then
|
if download_and_resize "$simple_url" "$output" "$size"; then
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
log_info "✓ 从 Simple Icons 下载成功"
|
log_info "✓ 从 Simple Icons 下载成功"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Simple Icons 也支持 SVG
|
# Simple Icons 也支持 SVG
|
||||||
if curl -sSL -o "${output%.png}.svg" "${SIMPLE_ICONS}/${name_lower}" 2>/dev/null && [[ -s "${output%.png}.svg" ]]; then
|
if curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$MAX_TIME" --retry "$RETRY" -o "${output%.png}.svg" "${SIMPLE_ICONS}/${name_lower}" 2>/dev/null && valid_icon_file "${output%.png}.svg"; then
|
||||||
log_info "✓ 从 Simple Icons 下载 SVG 成功"
|
log_info "✓ 从 Simple Icons 下载 SVG 成功"
|
||||||
# 如果有 SVG,尝试转换
|
# 如果有 SVG,尝试转换
|
||||||
if command -v convert &>/dev/null; then
|
if command -v convert &>/dev/null; then
|
||||||
convert "${output%.png}.svg" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
convert "${output%.png}.svg" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
||||||
if [[ -s "$output" ]]; then
|
if [[ -s "$output" ]]; then
|
||||||
rm -f "${output%.png}.svg"
|
rm -f "${output%.png}.svg"
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -120,6 +200,7 @@ try_download_icon() {
|
|||||||
log_info "尝试 selfh.st Icons..."
|
log_info "尝试 selfh.st Icons..."
|
||||||
local selfhst_url="${SELFHST_ICONS}/${name_lower}.png"
|
local selfhst_url="${SELFHST_ICONS}/${name_lower}.png"
|
||||||
if download_and_resize "$selfhst_url" "$output" "$size"; then
|
if download_and_resize "$selfhst_url" "$output" "$size"; then
|
||||||
|
save_to_cache "$app_name" "$output"
|
||||||
log_info "✓ 从 selfh.st Icons 下载成功"
|
log_info "✓ 从 selfh.st Icons 下载成功"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -127,43 +208,58 @@ try_download_icon() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建占位图标
|
parse_args() {
|
||||||
create_placeholder() {
|
POSITIONAL=()
|
||||||
local output="$1"
|
while [[ $# -gt 0 ]]; do
|
||||||
local size="$2"
|
case "$1" in
|
||||||
|
--mode)
|
||||||
|
MODE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cache-dir)
|
||||||
|
CACHE_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--url)
|
||||||
|
ICON_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
POSITIONAL+=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
log_error "未知选项: $1"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
POSITIONAL+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
log_warn "创建占位图标"
|
case "$MODE" in
|
||||||
|
auto|required|skip|cache-only) ;;
|
||||||
# 使用 ImageMagick 创建简单的占位图标
|
*)
|
||||||
if command -v convert &>/dev/null; then
|
log_error "无效模式: $MODE"
|
||||||
convert -size "${size}x${size}" xc:'#4A90E2' \
|
exit 2
|
||||||
-gravity center \
|
;;
|
||||||
-pointsize 48 \
|
esac
|
||||||
-fill white \
|
|
||||||
-annotate 0 "?" \
|
|
||||||
"$output" 2>/dev/null
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
# macOS sips 方式
|
|
||||||
if command -v sips &>/dev/null; then
|
|
||||||
# 创建一个简单的 1x1 像素 PNG 然后放大
|
|
||||||
local temp_file="/tmp/placeholder_${RANDOM}.png"
|
|
||||||
echo -n "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" | base64 -d > "$temp_file"
|
|
||||||
sips -z "$size" "$size" "$temp_file" --out "$output" &>/dev/null
|
|
||||||
rm -f "$temp_file"
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 最简单的占位符
|
|
||||||
echo -n "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" | base64 -d > "$output"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 主函数
|
# 主函数
|
||||||
main() {
|
main() {
|
||||||
local app_name="${1:-}"
|
parse_args "$@"
|
||||||
local output="${2:-./logo.png}"
|
|
||||||
local size="${3:-$DEFAULT_SIZE}"
|
local app_name="${POSITIONAL[0]:-}"
|
||||||
|
local output="${POSITIONAL[1]:-./logo.png}"
|
||||||
|
local size="${POSITIONAL[2]:-$DEFAULT_SIZE}"
|
||||||
|
|
||||||
# 参数检查
|
# 参数检查
|
||||||
if [[ -z "$app_name" ]] || [[ "$app_name" == "-h" ]] || [[ "$app_name" == "--help" ]]; then
|
if [[ -z "$app_name" ]] || [[ "$app_name" == "-h" ]] || [[ "$app_name" == "--help" ]]; then
|
||||||
@@ -171,10 +267,19 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 确保输出目录存在
|
if [[ "$MODE" == "skip" ]]; then
|
||||||
local output_dir
|
log_info "跳过图标下载: $app_name"
|
||||||
output_dir=$(dirname "$output")
|
exit 0
|
||||||
mkdir -p "$output_dir"
|
fi
|
||||||
|
|
||||||
|
if copy_from_cache "$app_name" "$output"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MODE" == "cache-only" ]]; then
|
||||||
|
log_warn "缓存中未找到图标: $app_name"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# 尝试下载
|
# 尝试下载
|
||||||
if try_download_icon "$app_name" "$output" "$size"; then
|
if try_download_icon "$app_name" "$output" "$size"; then
|
||||||
@@ -182,13 +287,16 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 下载失败,创建占位符
|
rm -f "$output" "${output%.png}.svg"
|
||||||
create_placeholder "$output" "$size"
|
log_warn "未找到图标,未创建占位图: $output"
|
||||||
log_warn "未找到图标,已创建占位符: $output"
|
|
||||||
log_info "请手动从以下网站下载合适的图标:"
|
log_info "请手动从以下网站下载合适的图标:"
|
||||||
echo " - https://dashboardicons.com/icons?q=${app_name}"
|
echo " - https://dashboardicons.com/icons?q=${app_name}"
|
||||||
echo " - https://simpleicons.org/?q=${app_name}"
|
echo " - https://simpleicons.org/?q=${app_name}"
|
||||||
echo " - https://selfh.st/icons/"
|
echo " - https://selfh.st/icons/"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "required" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 执行主函数
|
# 执行主函数
|
||||||
|
|||||||
Regular → Executable
+212
-69
@@ -18,12 +18,18 @@ NC='\033[0m' # No Color
|
|||||||
# 默认配置
|
# 默认配置
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TEMPLATE_DIR="${SCRIPT_DIR}/../templates"
|
TEMPLATE_DIR="${SCRIPT_DIR}/../templates"
|
||||||
OUTPUT_BASE="${2:-./apps}"
|
OUTPUT_BASE="./apps"
|
||||||
ICON_SOURCES=(
|
ICON_MODE="auto"
|
||||||
"https://dashboardicons.com/icons"
|
ICON_URL=""
|
||||||
"https://simpleicons.org/icons"
|
ICON_CACHE_DIR="${SCRIPT_DIR}/../.cache/icons"
|
||||||
"https://selfh.st/icons"
|
USER_APP_KEY=""
|
||||||
)
|
USER_APP_NAME=""
|
||||||
|
USER_VERSION=""
|
||||||
|
FORCE=false
|
||||||
|
DRY_RUN=false
|
||||||
|
POSITIONAL=()
|
||||||
|
SERVICE_VOLUMES=()
|
||||||
|
SERVICE_ENV=()
|
||||||
|
|
||||||
# 打印帮助
|
# 打印帮助
|
||||||
print_help() {
|
print_help() {
|
||||||
@@ -31,7 +37,7 @@ print_help() {
|
|||||||
${BLUE}1Panel App Builder${NC} - 快速生成 1Panel 应用配置
|
${BLUE}1Panel App Builder${NC} - 快速生成 1Panel 应用配置
|
||||||
|
|
||||||
${YELLOW}用法:${NC}
|
${YELLOW}用法:${NC}
|
||||||
$0 <输入源> [输出目录]
|
$0 [选项] <输入源> [输出目录]
|
||||||
|
|
||||||
${YELLOW}输入源支持:${NC}
|
${YELLOW}输入源支持:${NC}
|
||||||
1. GitHub 项目链接
|
1. GitHub 项目链接
|
||||||
@@ -59,6 +65,17 @@ ${YELLOW}示例:${NC}
|
|||||||
$0 https://github.com/alist-org/alist
|
$0 https://github.com/alist-org/alist
|
||||||
$0 ./my-docker-compose.yml ./my-apps
|
$0 ./my-docker-compose.yml ./my-apps
|
||||||
$0 "docker run -d --name nginx -p 80:80 nginx:latest"
|
$0 "docker run -d --name nginx -p 80:80 nginx:latest"
|
||||||
|
$0 --app-key demo-app --name DemoApp --version 1.2.3 --icon-mode skip ./docker-compose.yml
|
||||||
|
|
||||||
|
${YELLOW}选项:${NC}
|
||||||
|
--output <目录> 输出目录(默认: ./apps)
|
||||||
|
--app-key <key> 显式指定应用目录名
|
||||||
|
--name <名称> 显式指定应用显示名
|
||||||
|
--version <版本> 显式指定具体版本目录和镜像 tag
|
||||||
|
--icon-mode <模式> auto|required|skip|cache-only
|
||||||
|
--icon-url <URL> 使用指定图标 URL
|
||||||
|
--force 允许覆盖已有输出目录
|
||||||
|
--dry-run 只解析和打印结果,不写文件
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +84,79 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|||||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
normalize_app_key() {
|
||||||
|
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--' | tr -cd 'a-z0-9.-'
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
POSITIONAL=()
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--output|-o)
|
||||||
|
OUTPUT_BASE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--app-key)
|
||||||
|
USER_APP_KEY="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--name)
|
||||||
|
USER_APP_NAME="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
USER_VERSION="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--icon-mode)
|
||||||
|
ICON_MODE="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--icon-url)
|
||||||
|
ICON_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
FORCE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
POSITIONAL+=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
log_error "未知选项: $1"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
POSITIONAL+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$ICON_MODE" in
|
||||||
|
auto|required|skip|cache-only) ;;
|
||||||
|
*)
|
||||||
|
log_error "无效图标模式: $ICON_MODE"
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ ${#POSITIONAL[@]} -gt 1 ]]; then
|
||||||
|
OUTPUT_BASE="${POSITIONAL[1]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 解析镜像引用(支持 registry/namespace/repo:tag)
|
# 解析镜像引用(支持 registry/namespace/repo:tag)
|
||||||
parse_image_ref() {
|
parse_image_ref() {
|
||||||
local image_ref="$1"
|
local image_ref="$1"
|
||||||
@@ -110,10 +200,10 @@ get_latest_tag_docker_hub() {
|
|||||||
local repo_path="$2"
|
local repo_path="$2"
|
||||||
local url="https://hub.docker.com/v2/repositories/${namespace}/${repo_path}/tags?page_size=100&ordering=last_updated"
|
local url="https://hub.docker.com/v2/repositories/${namespace}/${repo_path}/tags?page_size=100&ordering=last_updated"
|
||||||
local tags
|
local tags
|
||||||
tags=$(curl -s "$url" | jq -r '.results[].name' 2>/dev/null || true)
|
tags=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" | jq -r '.results[].name' 2>/dev/null || true)
|
||||||
|
|
||||||
local semver
|
local semver
|
||||||
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\\.[0-9]+(\\.[0-9]+)?$' | head -n1 || true)
|
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\.[0-9]+(\.[0-9]+)?$' | sort -V | tail -n1 || true)
|
||||||
if [[ -n "$semver" ]]; then
|
if [[ -n "$semver" ]]; then
|
||||||
echo "$semver"
|
echo "$semver"
|
||||||
return 0
|
return 0
|
||||||
@@ -130,16 +220,16 @@ get_latest_tag_ghcr() {
|
|||||||
local repo_path="$2"
|
local repo_path="$2"
|
||||||
local token tags
|
local token tags
|
||||||
|
|
||||||
token=$(curl -s "https://ghcr.io/token?service=ghcr.io&scope=repository:${namespace}/${repo_path}:pull" | jq -r '.token' 2>/dev/null || true)
|
token=$(curl -fsSL --connect-timeout 5 --max-time 20 "https://ghcr.io/token?service=ghcr.io&scope=repository:${namespace}/${repo_path}:pull" | jq -r '.token' 2>/dev/null || true)
|
||||||
if [[ -z "$token" || "$token" == "null" ]]; then
|
if [[ -z "$token" || "$token" == "null" ]]; then
|
||||||
echo "latest"
|
echo "latest"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tags=$(curl -s -H "Authorization: Bearer ${token}" "https://ghcr.io/v2/${namespace}/${repo_path}/tags/list" | jq -r '.tags[]' 2>/dev/null || true)
|
tags=$(curl -fsSL --connect-timeout 5 --max-time 20 -H "Authorization: Bearer ${token}" "https://ghcr.io/v2/${namespace}/${repo_path}/tags/list" | jq -r '.tags[]' 2>/dev/null || true)
|
||||||
|
|
||||||
local semver
|
local semver
|
||||||
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\\.[0-9]+(\\.[0-9]+)?$' | sort -V | tail -n1 || true)
|
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\.[0-9]+(\.[0-9]+)?$' | sort -V | tail -n1 || true)
|
||||||
if [[ -n "$semver" ]]; then
|
if [[ -n "$semver" ]]; then
|
||||||
echo "$semver"
|
echo "$semver"
|
||||||
return 0
|
return 0
|
||||||
@@ -278,11 +368,11 @@ extract_from_github() {
|
|||||||
|
|
||||||
# 获取项目信息
|
# 获取项目信息
|
||||||
local repo_info
|
local repo_info
|
||||||
repo_info=$(curl -s "$api_url" 2>/dev/null || echo "{}")
|
repo_info=$(curl -fsSL --connect-timeout 5 --max-time 20 "$api_url" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
# 提取字段
|
# 提取字段
|
||||||
APP_NAME=$(echo "$repo_info" | jq -r '.name // empty')
|
APP_NAME=$(echo "$repo_info" | jq -r '.name // empty')
|
||||||
APP_KEY=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')
|
APP_KEY=$(normalize_app_key "$APP_NAME")
|
||||||
DESCRIPTION=$(echo "$repo_info" | jq -r '.description // empty')
|
DESCRIPTION=$(echo "$repo_info" | jq -r '.description // empty')
|
||||||
GITHUB="$github_url"
|
GITHUB="$github_url"
|
||||||
WEBSITE=$(echo "$repo_info" | jq -r '.homepage // empty')
|
WEBSITE=$(echo "$repo_info" | jq -r '.homepage // empty')
|
||||||
@@ -306,11 +396,11 @@ extract_from_compose() {
|
|||||||
log_info "正在解析 docker-compose 内容"
|
log_info "正在解析 docker-compose 内容"
|
||||||
|
|
||||||
# 提取服务名
|
# 提取服务名
|
||||||
SERVICE_NAME=$(echo "$compose_content" | yq '.services | keys | .[0]' 2>/dev/null || echo "")
|
SERVICE_NAME=$(echo "$compose_content" | yq -r '.services | keys | .[0]' 2>/dev/null || echo "")
|
||||||
|
|
||||||
# 提取镜像
|
# 提取镜像
|
||||||
local image
|
local image
|
||||||
image=$(echo "$compose_content" | yq ".services.${SERVICE_NAME}.image" 2>/dev/null || echo "")
|
image=$(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].image" 2>/dev/null || echo "")
|
||||||
parse_image_ref "$image"
|
parse_image_ref "$image"
|
||||||
|
|
||||||
# 提取端口
|
# 提取端口
|
||||||
@@ -318,12 +408,24 @@ extract_from_compose() {
|
|||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[[ -z "$line" || "$line" == "null" ]] && continue
|
[[ -z "$line" || "$line" == "null" ]] && continue
|
||||||
PORT_ENTRIES+=("$line")
|
PORT_ENTRIES+=("$line")
|
||||||
done < <(echo "$compose_content" | yq ".services.${SERVICE_NAME}.ports[]" 2>/dev/null || true)
|
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].ports[]" 2>/dev/null || true)
|
||||||
|
|
||||||
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
|
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
|
||||||
PORT_ENTRIES=("8080")
|
PORT_ENTRIES=("8080")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
SERVICE_VOLUMES=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" || "$line" == "null" ]] && continue
|
||||||
|
SERVICE_VOLUMES+=("$line")
|
||||||
|
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].volumes[]?" 2>/dev/null || true)
|
||||||
|
|
||||||
|
SERVICE_ENV=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" || "$line" == "null" ]] && continue
|
||||||
|
SERVICE_ENV+=("$line")
|
||||||
|
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].environment[]?" 2>/dev/null || true)
|
||||||
|
|
||||||
log_info "服务名: $SERVICE_NAME"
|
log_info "服务名: $SERVICE_NAME"
|
||||||
log_info "镜像: $IMAGE_BASE:$TAG"
|
log_info "镜像: $IMAGE_BASE:$TAG"
|
||||||
log_info "端口数量: ${#PORT_ENTRIES[@]}"
|
log_info "端口数量: ${#PORT_ENTRIES[@]}"
|
||||||
@@ -341,7 +443,7 @@ extract_from_docker_run() {
|
|||||||
parse_image_ref "$image_ref"
|
parse_image_ref "$image_ref"
|
||||||
|
|
||||||
# 提取容器名
|
# 提取容器名
|
||||||
SERVICE_NAME=$(echo "$cmd" | grep -oE '\-\-name[= ]+[^ ]+' | awk '{print $NF}' || echo "app")
|
SERVICE_NAME=$(echo "$cmd" | grep -oE '\-\-name(=| )[A-Za-z0-9_.-]+' | sed -E 's/^--name[= ]//' || echo "app")
|
||||||
|
|
||||||
# 提取端口
|
# 提取端口
|
||||||
PORT_ENTRIES=()
|
PORT_ENTRIES=()
|
||||||
@@ -354,8 +456,20 @@ extract_from_docker_run() {
|
|||||||
PORT_ENTRIES=("8080")
|
PORT_ENTRIES=("8080")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
SERVICE_VOLUMES=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
SERVICE_VOLUMES+=("$line")
|
||||||
|
done < <(echo "$cmd" | grep -oE '(-v|--volume)(=| )[^ ]+' | sed -E 's/^(-v|--volume)[= ]//' || true)
|
||||||
|
|
||||||
|
SERVICE_ENV=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
SERVICE_ENV+=("$line")
|
||||||
|
done < <(echo "$cmd" | grep -oE '(-e|--env)(=| )[^ ]+' | sed -E 's/^(-e|--env)[= ]//' || true)
|
||||||
|
|
||||||
APP_NAME="$SERVICE_NAME"
|
APP_NAME="$SERVICE_NAME"
|
||||||
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||||
|
|
||||||
log_info "应用名称: $APP_NAME"
|
log_info "应用名称: $APP_NAME"
|
||||||
log_info "镜像: $IMAGE_BASE:$TAG"
|
log_info "镜像: $IMAGE_BASE:$TAG"
|
||||||
@@ -466,11 +580,41 @@ EOF
|
|||||||
|
|
||||||
cat >> "$output_file" << EOF
|
cat >> "$output_file" << EOF
|
||||||
volumes:
|
volumes:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ((${#SERVICE_VOLUMES[@]} > 0)); then
|
||||||
|
local volume
|
||||||
|
for volume in "${SERVICE_VOLUMES[@]}"; do
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
|
- ${volume}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
else
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
- ./data/data:/app/data
|
- ./data/data:/app/data
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
environment:
|
environment:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ((${#SERVICE_ENV[@]} > 0)); then
|
||||||
|
local env
|
||||||
|
for env in "${SERVICE_ENV[@]}"; do
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
|
- ${env}
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
else
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
- PUID=0
|
- PUID=0
|
||||||
- PGID=0
|
- PGID=0
|
||||||
- UMASK=022
|
- UMASK=022
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$output_file" << EOF
|
||||||
image: ${IMAGE_BASE:-app}:${image_tag}
|
image: ${IMAGE_BASE:-app}:${image_tag}
|
||||||
labels:
|
labels:
|
||||||
createdBy: "Apps"
|
createdBy: "Apps"
|
||||||
@@ -531,58 +675,30 @@ EOF
|
|||||||
download_icon() {
|
download_icon() {
|
||||||
local app_name="$1"
|
local app_name="$1"
|
||||||
local output_file="$2"
|
local output_file="$2"
|
||||||
local github_url="${3:-}"
|
local args=(--mode "$ICON_MODE" --cache-dir "$ICON_CACHE_DIR")
|
||||||
local icon_name
|
|
||||||
|
|
||||||
icon_name=$(echo "$app_name" | tr '[:upper:]' '[:lower:]')
|
if [[ -n "$ICON_URL" ]]; then
|
||||||
|
args+=(--url "$ICON_URL")
|
||||||
log_info "尝试获取图标: $app_name"
|
|
||||||
|
|
||||||
# 优先从 GitHub 仓库内查找常见图标文件
|
|
||||||
if [[ -n "$github_url" ]]; then
|
|
||||||
local owner_repo repo_api candidate_paths
|
|
||||||
owner_repo=$(echo "$github_url" | sed -E 's|https?://github.com/||')
|
|
||||||
repo_api="https://api.github.com/repos/${owner_repo}"
|
|
||||||
candidate_paths=(
|
|
||||||
"logo.png"
|
|
||||||
"icon.png"
|
|
||||||
"logo.svg"
|
|
||||||
"icon.svg"
|
|
||||||
"assets/logo.png"
|
|
||||||
"assets/icon.png"
|
|
||||||
".github/logo.png"
|
|
||||||
".github/icon.png"
|
|
||||||
)
|
|
||||||
|
|
||||||
for path in "${candidate_paths[@]}"; do
|
|
||||||
local file_api="${repo_api}/contents/${path}"
|
|
||||||
local download_url
|
|
||||||
download_url=$(curl -s "$file_api" | jq -r '.download_url // empty' 2>/dev/null || true)
|
|
||||||
if [[ -n "$download_url" ]]; then
|
|
||||||
if curl -sSL -o "$output_file" "$download_url" 2>/dev/null && [[ -s "$output_file" ]]; then
|
|
||||||
log_info "图标来自仓库: ${path}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 尝试各个图标源
|
if bash "${SCRIPT_DIR}/download-icon.sh" "${args[@]}" "$app_name" "$output_file"; then
|
||||||
for source in "${ICON_SOURCES[@]}"; do
|
return 0
|
||||||
local icon_url="${source}/${icon_name}.png"
|
fi
|
||||||
if curl -sSL -o "$output_file" "$icon_url" 2>/dev/null && [[ -s "$output_file" ]]; then
|
|
||||||
log_info "图标下载成功: $icon_url"
|
if [[ "$ICON_MODE" == "required" ]]; then
|
||||||
return 0
|
log_error "图标为 required 模式,但未能获取图标"
|
||||||
fi
|
return 1
|
||||||
done
|
fi
|
||||||
|
|
||||||
log_warn "未找到图标,请手动补充 logo.png"
|
log_warn "未找到图标,请手动补充 logo.png"
|
||||||
return 1
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# 主函数
|
# 主函数
|
||||||
main() {
|
main() {
|
||||||
local input="$1"
|
parse_args "$@"
|
||||||
|
|
||||||
|
local input="${POSITIONAL[0]:-}"
|
||||||
local input_type
|
local input_type
|
||||||
|
|
||||||
# 参数检查
|
# 参数检查
|
||||||
@@ -607,16 +723,16 @@ main() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
compose_url)
|
compose_url)
|
||||||
COMPOSE_CONTENT=$(curl -s "$input" 2>/dev/null)
|
COMPOSE_CONTENT=$(curl -fsSL --connect-timeout 5 --max-time 20 "$input" 2>/dev/null)
|
||||||
extract_from_compose "$COMPOSE_CONTENT"
|
extract_from_compose "$COMPOSE_CONTENT"
|
||||||
APP_NAME="$SERVICE_NAME"
|
APP_NAME="$SERVICE_NAME"
|
||||||
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||||
;;
|
;;
|
||||||
compose_file)
|
compose_file)
|
||||||
COMPOSE_CONTENT=$(cat "$input")
|
COMPOSE_CONTENT=$(cat "$input")
|
||||||
extract_from_compose "$COMPOSE_CONTENT"
|
extract_from_compose "$COMPOSE_CONTENT"
|
||||||
APP_NAME="$SERVICE_NAME"
|
APP_NAME="$SERVICE_NAME"
|
||||||
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||||
;;
|
;;
|
||||||
docker_run)
|
docker_run)
|
||||||
extract_from_docker_run "$input"
|
extract_from_docker_run "$input"
|
||||||
@@ -627,6 +743,16 @@ main() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ -n "$USER_APP_NAME" ]]; then
|
||||||
|
APP_NAME="$USER_APP_NAME"
|
||||||
|
fi
|
||||||
|
if [[ -n "$USER_APP_KEY" ]]; then
|
||||||
|
APP_KEY=$(normalize_app_key "$USER_APP_KEY")
|
||||||
|
fi
|
||||||
|
if [[ -z "${APP_KEY:-}" ]]; then
|
||||||
|
APP_KEY=$(normalize_app_key "${APP_NAME:-app}")
|
||||||
|
fi
|
||||||
|
|
||||||
# 生成端口映射数组
|
# 生成端口映射数组
|
||||||
HOST_PORTS=()
|
HOST_PORTS=()
|
||||||
CONTAINER_PORTS=()
|
CONTAINER_PORTS=()
|
||||||
@@ -651,16 +777,33 @@ main() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# 获取最新版本号
|
# 获取最新版本号
|
||||||
VERSION_TAG=$(get_latest_tag)
|
if [[ -n "$USER_VERSION" ]]; then
|
||||||
if [[ -z "$VERSION_TAG" ]]; then
|
VERSION_TAG="$USER_VERSION"
|
||||||
VERSION_TAG="$TAG"
|
else
|
||||||
|
VERSION_TAG=$(get_latest_tag)
|
||||||
|
if [[ -z "$VERSION_TAG" ]]; then
|
||||||
|
VERSION_TAG="$TAG"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -z "$VERSION_TAG" ]]; then
|
if [[ -z "$VERSION_TAG" ]]; then
|
||||||
VERSION_TAG="latest"
|
VERSION_TAG="latest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo "app_key=${APP_KEY}"
|
||||||
|
echo "app_name=${APP_NAME:-}"
|
||||||
|
echo "service=${SERVICE_NAME:-}"
|
||||||
|
echo "image=${IMAGE_BASE:-}:${VERSION_TAG}"
|
||||||
|
echo "output=${OUTPUT_BASE}/${APP_KEY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# 创建输出目录(latest + 具体版本)
|
# 创建输出目录(latest + 具体版本)
|
||||||
local output_dir_latest="${OUTPUT_BASE}/${APP_KEY}/latest"
|
local output_dir_latest="${OUTPUT_BASE}/${APP_KEY}/latest"
|
||||||
|
if [[ -e "${OUTPUT_BASE}/${APP_KEY}" && "$FORCE" != "true" ]]; then
|
||||||
|
log_error "输出目录已存在: ${OUTPUT_BASE}/${APP_KEY}(如需覆盖请使用 --force)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
mkdir -p "$output_dir_latest"
|
mkdir -p "$output_dir_latest"
|
||||||
|
|
||||||
local output_dir_version=""
|
local output_dir_version=""
|
||||||
@@ -687,7 +830,7 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
generate_readme "${OUTPUT_BASE}/${APP_KEY}"
|
generate_readme "${OUTPUT_BASE}/${APP_KEY}"
|
||||||
download_icon "$APP_NAME" "${OUTPUT_BASE}/${APP_KEY}/logo.png" "${GITHUB:-}"
|
download_icon "$APP_NAME" "${OUTPUT_BASE}/${APP_KEY}/logo.png"
|
||||||
|
|
||||||
# 生成上级目录的 data.yml
|
# 生成上级目录的 data.yml
|
||||||
generate_data_yml "${OUTPUT_BASE}/${APP_KEY}/data.yml"
|
generate_data_yml "${OUTPUT_BASE}/${APP_KEY}/data.yml"
|
||||||
|
|||||||
Regular → Executable
+111
-5
@@ -40,8 +40,20 @@ EOF
|
|||||||
|
|
||||||
# 日志函数
|
# 日志函数
|
||||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; ((WARNINGS++)); }
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; ((WARNINGS+=1)); }
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; ((ERRORS++)); }
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; ((ERRORS+=1)); }
|
||||||
|
|
||||||
|
has_yq() {
|
||||||
|
command -v yq >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
yq_read() {
|
||||||
|
local file="$1"
|
||||||
|
local expr="$2"
|
||||||
|
if has_yq; then
|
||||||
|
yq -r "$expr" "$file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 检查目录结构
|
# 检查目录结构
|
||||||
check_directory_structure() {
|
check_directory_structure() {
|
||||||
@@ -66,11 +78,11 @@ check_directory_structure() {
|
|||||||
|
|
||||||
# 查找版本目录
|
# 查找版本目录
|
||||||
local version_dirs
|
local version_dirs
|
||||||
version_dirs=$(find "$app_dir" -maxdepth 1 -type d -name "v*" -o -name "latest" -o -name "[0-9]*" 2>/dev/null || echo "")
|
version_dirs=$(find "$app_dir" -mindepth 1 -maxdepth 1 -type d \( -name "v*" -o -name "latest" -o -name "[0-9]*" \) 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [[ -z "$version_dirs" ]]; then
|
if [[ -z "$version_dirs" ]]; then
|
||||||
log_error "未找到版本目录(如 1.0.0/, v1.0.0/, latest/)"
|
log_error "未找到版本目录(如 1.0.0/, v1.0.0/, latest/)"
|
||||||
return 1
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查每个版本目录
|
# 检查每个版本目录
|
||||||
@@ -90,6 +102,9 @@ check_directory_structure() {
|
|||||||
# 验证顶层 data.yml
|
# 验证顶层 data.yml
|
||||||
validate_top_data_yml() {
|
validate_top_data_yml() {
|
||||||
local data_file="$1"
|
local data_file="$1"
|
||||||
|
local app_dir app_name
|
||||||
|
app_dir="$(dirname "$data_file")"
|
||||||
|
app_name="$(basename "$app_dir")"
|
||||||
|
|
||||||
log_info "验证顶层 data.yml"
|
log_info "验证顶层 data.yml"
|
||||||
|
|
||||||
@@ -115,6 +130,14 @@ validate_top_data_yml() {
|
|||||||
log_error "data.yml 缺少 additionalProperties.key"
|
log_error "data.yml 缺少 additionalProperties.key"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if has_yq; then
|
||||||
|
local key
|
||||||
|
key=$(yq_read "$data_file" '.additionalProperties.key // ""')
|
||||||
|
if [[ -n "$key" && "$key" != "$app_name" ]]; then
|
||||||
|
log_error "additionalProperties.key 必须与应用目录名一致: key=$key dir=$app_name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 检查 tags
|
# 检查 tags
|
||||||
if ! grep -q "tags:" "$data_file"; then
|
if ! grep -q "tags:" "$data_file"; then
|
||||||
log_warn "data.yml 缺少 tags(推荐)"
|
log_warn "data.yml 缺少 tags(推荐)"
|
||||||
@@ -153,6 +176,86 @@ validate_version_data_yml() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extract_form_env_keys() {
|
||||||
|
local data_file="$1"
|
||||||
|
|
||||||
|
if [[ ! -f "$data_file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_yq; then
|
||||||
|
yq -r '.additionalProperties.formFields[]?.envKey // empty' "$data_file" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
grep -oE 'envKey:[[:space:]]*PANEL_APP_PORT_[A-Z0-9_]+' "$data_file" | awk '{print $2}' || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_compose_port_vars() {
|
||||||
|
local compose_file="$1"
|
||||||
|
grep -oE '\$\{PANEL_APP_PORT_[A-Z0-9_]+\}' "$compose_file" | sed -E 's/^\$\{//; s/\}$//' | sort -u || true
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_port_variables() {
|
||||||
|
local data_file="$1"
|
||||||
|
local compose_file="$2"
|
||||||
|
local defined used var
|
||||||
|
|
||||||
|
defined="$(extract_form_env_keys "$data_file" | sort -u)"
|
||||||
|
used="$(extract_compose_port_vars "$compose_file")"
|
||||||
|
|
||||||
|
while IFS= read -r var; do
|
||||||
|
[[ -z "$var" ]] && continue
|
||||||
|
if ! grep -qx "$var" <<< "$defined"; then
|
||||||
|
log_error "端口变量 ${var} 在 docker-compose.yml 中使用,但未在版本 data.yml 中定义"
|
||||||
|
fi
|
||||||
|
done <<< "$used"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_image_tags() {
|
||||||
|
local version_dir="$1"
|
||||||
|
local compose_file="$2"
|
||||||
|
local version image tag matched_version=false
|
||||||
|
|
||||||
|
version="$(basename "$version_dir")"
|
||||||
|
while IFS= read -r image; do
|
||||||
|
[[ -z "$image" || "$image" == "null" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$image" =~ @sha256: ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$image" =~ :([^/:]+)$ ]]; then
|
||||||
|
tag="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
tag="latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$version" == "latest" && "$tag" != "latest" ]]; then
|
||||||
|
log_error "latest 目录中的镜像必须使用 latest tag: $image"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$version" != "latest" && "$tag" == "$version" ]]; then
|
||||||
|
matched_version=true
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^[[:space:]]*image:[[:space:]]*' "$compose_file" | sed -E 's/^[[:space:]]*image:[[:space:]]*"?([^"]*)"?/\1/' || true)
|
||||||
|
|
||||||
|
if [[ "$version" != "latest" && "$matched_version" != "true" ]]; then
|
||||||
|
log_warn "版本目录 ${version} 未检测到与目录名一致的镜像 tag,请确认是否为多服务或特殊版本"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_logo_file() {
|
||||||
|
local logo_file="$1"
|
||||||
|
|
||||||
|
if [[ ! -f "$logo_file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if head -c 512 "$logo_file" | grep -qiE '<!doctype|<html|<Error>|AccessDenied'; then
|
||||||
|
log_error "logo.png 看起来不是有效图片,可能是下载失败返回的 HTML/XML"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 验证 docker-compose.yml
|
# 验证 docker-compose.yml
|
||||||
validate_docker_compose() {
|
validate_docker_compose() {
|
||||||
local compose_file="$1"
|
local compose_file="$1"
|
||||||
@@ -226,14 +329,17 @@ main() {
|
|||||||
# 执行检查
|
# 执行检查
|
||||||
check_directory_structure "$app_dir"
|
check_directory_structure "$app_dir"
|
||||||
validate_top_data_yml "$app_dir/data.yml"
|
validate_top_data_yml "$app_dir/data.yml"
|
||||||
|
validate_logo_file "$app_dir/logo.png"
|
||||||
|
|
||||||
# 查找并验证版本目录
|
# 查找并验证版本目录
|
||||||
local version_dirs
|
local version_dirs
|
||||||
version_dirs=$(find "$app_dir" -maxdepth 1 -type d \( -name "v*" -o -name "latest" -o -name "[0-9]*" \) 2>/dev/null || echo "")
|
version_dirs=$(find "$app_dir" -mindepth 1 -maxdepth 1 -type d \( -name "v*" -o -name "latest" -o -name "[0-9]*" \) 2>/dev/null || echo "")
|
||||||
|
|
||||||
for version_dir in $version_dirs; do
|
for version_dir in $version_dirs; do
|
||||||
validate_version_data_yml "$version_dir/data.yml"
|
validate_version_data_yml "$version_dir/data.yml"
|
||||||
validate_docker_compose "$version_dir/docker-compose.yml"
|
validate_docker_compose "$version_dir/docker-compose.yml"
|
||||||
|
validate_port_variables "$version_dir/data.yml" "$version_dir/docker-compose.yml"
|
||||||
|
validate_image_tags "$version_dir" "$version_dir/docker-compose.yml"
|
||||||
done
|
done
|
||||||
|
|
||||||
# 输出结果
|
# 输出结果
|
||||||
|
|||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
bash -n skills/scripts/download-icon.sh
|
||||||
|
bash -n skills/scripts/generate-app.sh
|
||||||
|
bash -n skills/scripts/validate-app.sh
|
||||||
|
bash -n skills/tests/test_skills_scripts.sh
|
||||||
|
|
||||||
|
bash skills/tests/test_skills_scripts.sh
|
||||||
|
|
||||||
|
echo "All skills checks passed."
|
||||||
Executable
+211
@@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_file() {
|
||||||
|
[[ -f "$1" ]] || fail "expected file: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_file() {
|
||||||
|
[[ ! -f "$1" ]] || fail "expected missing file: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local file="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
grep -qE -- "$pattern" "$file" || fail "expected pattern '$pattern' in $file"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
echo "== $name =="
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_download_icon_skip_mode_does_not_create_logo() {
|
||||||
|
local out="$TMP_DIR/skip/logo.png"
|
||||||
|
|
||||||
|
bash "$ROOT_DIR/skills/scripts/download-icon.sh" --mode skip demo-app "$out" >/tmp/download_icon_skip.out
|
||||||
|
|
||||||
|
assert_not_file "$out"
|
||||||
|
assert_contains /tmp/download_icon_skip.out "跳过图标下载"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_download_icon_cache_only_uses_cached_logo() {
|
||||||
|
local cache_dir="$TMP_DIR/icon-cache"
|
||||||
|
local out="$TMP_DIR/cache/logo.png"
|
||||||
|
mkdir -p "$cache_dir"
|
||||||
|
printf 'cached-logo' > "$cache_dir/demo-app.png"
|
||||||
|
|
||||||
|
bash "$ROOT_DIR/skills/scripts/download-icon.sh" --mode cache-only --cache-dir "$cache_dir" demo-app "$out" >/tmp/download_icon_cache.out
|
||||||
|
|
||||||
|
assert_file "$out"
|
||||||
|
cmp "$cache_dir/demo-app.png" "$out" || fail "cached logo was not copied"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_generate_app_accepts_explicit_options_and_skips_icon() {
|
||||||
|
local compose="$TMP_DIR/compose.yml"
|
||||||
|
local output="$TMP_DIR/generated"
|
||||||
|
cat > "$compose" <<'YAML'
|
||||||
|
services:
|
||||||
|
demo:
|
||||||
|
image: nginx:1.25.3
|
||||||
|
ports:
|
||||||
|
- "18080:80"
|
||||||
|
YAML
|
||||||
|
|
||||||
|
bash "$ROOT_DIR/skills/scripts/generate-app.sh" \
|
||||||
|
--app-key demo-app \
|
||||||
|
--name DemoApp \
|
||||||
|
--version 1.25.3 \
|
||||||
|
--output "$output" \
|
||||||
|
--icon-mode skip \
|
||||||
|
"$compose" >/tmp/generate_app_options.out
|
||||||
|
|
||||||
|
assert_file "$output/demo-app/latest/docker-compose.yml"
|
||||||
|
assert_file "$output/demo-app/1.25.3/docker-compose.yml"
|
||||||
|
assert_contains "$output/demo-app/data.yml" "^name: DemoApp$"
|
||||||
|
assert_contains "$output/demo-app/latest/docker-compose.yml" "image: nginx:latest"
|
||||||
|
assert_contains "$output/demo-app/1.25.3/docker-compose.yml" "image: nginx:1.25.3"
|
||||||
|
assert_not_file "$output/demo-app/logo.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_generate_app_preserves_compose_environment_and_volumes() {
|
||||||
|
local compose="$TMP_DIR/compose-preserve.yml"
|
||||||
|
local output="$TMP_DIR/generated-preserve"
|
||||||
|
cat > "$compose" <<'YAML'
|
||||||
|
services:
|
||||||
|
demo:
|
||||||
|
image: nginx:1.25.3
|
||||||
|
ports:
|
||||||
|
- "18080:80"
|
||||||
|
volumes:
|
||||||
|
- ./data/nginx:/etc/nginx/conf.d
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- DEMO_FLAG=true
|
||||||
|
YAML
|
||||||
|
|
||||||
|
bash "$ROOT_DIR/skills/scripts/generate-app.sh" \
|
||||||
|
--app-key demo-preserve \
|
||||||
|
--name DemoPreserve \
|
||||||
|
--version 1.25.3 \
|
||||||
|
--output "$output" \
|
||||||
|
--icon-mode skip \
|
||||||
|
"$compose" >/tmp/generate_app_preserve.out
|
||||||
|
|
||||||
|
assert_contains "$output/demo-preserve/1.25.3/docker-compose.yml" "./data/nginx:/etc/nginx/conf.d"
|
||||||
|
assert_contains "$output/demo-preserve/1.25.3/docker-compose.yml" "TZ=Asia/Shanghai"
|
||||||
|
assert_contains "$output/demo-preserve/1.25.3/docker-compose.yml" "DEMO_FLAG=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_validate_app_rejects_undefined_port_variable() {
|
||||||
|
local app="$TMP_DIR/bad-port/apps/demo-app"
|
||||||
|
mkdir -p "$app/1.0.0"
|
||||||
|
printf 'png' > "$app/logo.png"
|
||||||
|
cat > "$app/data.yml" <<'YAML'
|
||||||
|
name: DemoApp
|
||||||
|
tags:
|
||||||
|
- 实用工具
|
||||||
|
title: Demo
|
||||||
|
description: Demo
|
||||||
|
additionalProperties:
|
||||||
|
key: demo-app
|
||||||
|
name: DemoApp
|
||||||
|
architectures:
|
||||||
|
- amd64
|
||||||
|
YAML
|
||||||
|
cat > "$app/1.0.0/data.yml" <<'YAML'
|
||||||
|
additionalProperties:
|
||||||
|
formFields:
|
||||||
|
- default: 8080
|
||||||
|
edit: true
|
||||||
|
envKey: PANEL_APP_PORT_HTTP
|
||||||
|
labelEn: Web Port
|
||||||
|
labelZh: Web端口
|
||||||
|
required: true
|
||||||
|
rule: paramPort
|
||||||
|
type: number
|
||||||
|
YAML
|
||||||
|
cat > "$app/1.0.0/docker-compose.yml" <<'YAML'
|
||||||
|
services:
|
||||||
|
demo:
|
||||||
|
container_name: ${CONTAINER_NAME}
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
ports:
|
||||||
|
- "${PANEL_APP_PORT_ADMIN}:80"
|
||||||
|
image: nginx:1.0.0
|
||||||
|
labels:
|
||||||
|
createdBy: "Apps"
|
||||||
|
networks:
|
||||||
|
1panel-network:
|
||||||
|
external: true
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if bash "$ROOT_DIR/skills/scripts/validate-app.sh" "$app" >/tmp/validate_bad_port.out 2>&1; then
|
||||||
|
fail "validator should reject undefined port variable"
|
||||||
|
fi
|
||||||
|
assert_contains /tmp/validate_bad_port.out "未在版本 data.yml 中定义"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_validate_app_rejects_latest_wrong_tag() {
|
||||||
|
local app="$TMP_DIR/bad-latest/apps/demo-app"
|
||||||
|
mkdir -p "$app/latest"
|
||||||
|
printf 'png' > "$app/logo.png"
|
||||||
|
cat > "$app/data.yml" <<'YAML'
|
||||||
|
name: DemoApp
|
||||||
|
tags:
|
||||||
|
- 实用工具
|
||||||
|
title: Demo
|
||||||
|
description: Demo
|
||||||
|
additionalProperties:
|
||||||
|
key: demo-app
|
||||||
|
name: DemoApp
|
||||||
|
architectures:
|
||||||
|
- amd64
|
||||||
|
YAML
|
||||||
|
cat > "$app/latest/data.yml" <<'YAML'
|
||||||
|
additionalProperties:
|
||||||
|
formFields: []
|
||||||
|
YAML
|
||||||
|
cat > "$app/latest/docker-compose.yml" <<'YAML'
|
||||||
|
services:
|
||||||
|
demo:
|
||||||
|
container_name: ${CONTAINER_NAME}
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
image: nginx:1.25.3
|
||||||
|
labels:
|
||||||
|
createdBy: "Apps"
|
||||||
|
networks:
|
||||||
|
1panel-network:
|
||||||
|
external: true
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if bash "$ROOT_DIR/skills/scripts/validate-app.sh" "$app" >/tmp/validate_bad_latest.out 2>&1; then
|
||||||
|
fail "validator should reject latest directory with concrete tag"
|
||||||
|
fi
|
||||||
|
assert_contains /tmp/validate_bad_latest.out "latest 目录"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test "download icon skip mode" test_download_icon_skip_mode_does_not_create_logo
|
||||||
|
run_test "download icon cache-only mode" test_download_icon_cache_only_uses_cached_logo
|
||||||
|
run_test "generate app explicit options" test_generate_app_accepts_explicit_options_and_skips_icon
|
||||||
|
run_test "generate app preserves compose fields" test_generate_app_preserves_compose_environment_and_volumes
|
||||||
|
run_test "validate undefined port variable" test_validate_app_rejects_undefined_port_variable
|
||||||
|
run_test "validate latest image tag" test_validate_app_rejects_latest_wrong_tag
|
||||||
|
|
||||||
|
echo "All skills script tests passed."
|
||||||
Reference in New Issue
Block a user