mirror of
https://github.com/arch3rPro/1Panel-Appstore.git
synced 2026-06-10 00:19:38 +08:00
chore: optimize 1panel app builder skill
This commit is contained in:
+42
-181
@@ -1,200 +1,61 @@
|
||||
# 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
|
||||
# GitHub 项目
|
||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
||||
cd /root/github/1Panel-Appstore/skills
|
||||
|
||||
# docker-compose 文件链接
|
||||
./scripts/generate-app.sh https://raw.githubusercontent.com/.../docker-compose.yml
|
||||
# 从 compose 生成草稿,跳过图标下载
|
||||
./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. 输出示例
|
||||
## 生成脚本
|
||||
|
||||
```
|
||||
./apps/alist/v3.45.0/
|
||||
├── data.yml # 应用元数据
|
||||
├── docker-compose.yml # 编排文件
|
||||
├── logo.png # 应用图标
|
||||
├── README.md # 中文简介
|
||||
└── README_en.md # 英文简介
|
||||
`scripts/generate-app.sh` 支持 GitHub URL、compose URL、本地 compose 文件和 `docker run` 命令。
|
||||
|
||||
常用参数:
|
||||
|
||||
```text
|
||||
--output <dir> 输出目录,默认 ./apps
|
||||
--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
|
||||
name: AList
|
||||
tags:
|
||||
- 实用工具
|
||||
- 云存储
|
||||
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
|
||||
```
|
||||
- `auto`: 优先缓存,缺失时尝试网络源;找不到也不阻塞生成。
|
||||
- `skip`: 跳过图标处理,适合草稿生成。
|
||||
- `cache-only`: 只读 `skills/.cache/icons`,不访问网络。
|
||||
- `required`: 找不到有效图标时失败。
|
||||
|
||||
#### 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
|
||||
```
|
||||
`scripts/validate-app.sh` 会检查:
|
||||
|
||||
## 高级用法
|
||||
- 应用目录结构和必需文件。
|
||||
- `additionalProperties.key` 是否等于目录名。
|
||||
- `PANEL_APP_PORT_*` 是否在版本 `data.yml` 中定义。
|
||||
- `latest/` 镜像是否使用 `latest` tag。
|
||||
- `logo.png` 是否疑似 HTML/XML 错误响应。
|
||||
- 1Panel 常见 compose 约束,如 `${CONTAINER_NAME}`、`1panel-network`、`createdBy: "Apps"`。
|
||||
|
||||
### 指定输出目录
|
||||
|
||||
```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
|
||||
生成结果仍需要人工审查 metadata、README、架构、端口语义和多服务依赖。
|
||||
|
||||
+114
-413
@@ -1,451 +1,152 @@
|
||||
---
|
||||
name: 1panel-app-builder
|
||||
description: >
|
||||
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.
|
||||
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.
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
```
|
||||
app-key/ # App identifier (lowercase, hyphenated)
|
||||
├── logo.png # App icon (64x64 or 128x128 recommended)
|
||||
├── data.yml # App metadata (top-level)
|
||||
├── README.md # Chinese documentation
|
||||
├── README_en.md # English documentation (optional)
|
||||
└── 1.0.0/ # Version directory (semver or "latest")
|
||||
├── data.yml # Parameter definitions (form fields)
|
||||
├── docker-compose.yml # Compose file with variable substitution
|
||||
├── data/ # Persistent data directory
|
||||
└── scripts/ # Optional scripts
|
||||
└── upgrade.sh # Upgrade script
|
||||
- Add or package a 1Panel local app store app.
|
||||
- Convert a GitHub project, `docker-compose.yml`, or `docker run` command into `apps/<app-key>/`.
|
||||
- Fix 1Panel app metadata, port variables, compose files, README files, icons, or version directories.
|
||||
- Validate an app package before commit.
|
||||
|
||||
For detailed real app examples, read `references/1panel-examples.md` only when needed.
|
||||
|
||||
## Required Shape
|
||||
|
||||
```text
|
||||
apps/<app-key>/
|
||||
├── 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:**
|
||||
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
|
||||
## 1Panel Rules
|
||||
|
||||
**From docker-compose.yml:**
|
||||
1. Parse all services defined
|
||||
2. Extract: image versions, port mappings, volume mounts, environment variables
|
||||
3. Identify the main service (typically the one with web UI)
|
||||
- `container_name` should be `${CONTAINER_NAME}`.
|
||||
- Services should use `restart: always`.
|
||||
- Use external `1panel-network`.
|
||||
- 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:**
|
||||
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
|
||||
Preferred port variables:
|
||||
|
||||
### Step 2: Resolve Latest Version (Create Version + latest)
|
||||
|
||||
Always create **two** version directories when possible:
|
||||
|
||||
- `latest/` uses image tag `latest`
|
||||
- `<version>/` uses the **latest concrete tag** from the registry
|
||||
|
||||
**How to resolve the latest concrete tag:**
|
||||
|
||||
1. If the image is on **GitHub Container Registry** (`ghcr.io`), query tags from GitHub Packages (GHCR).
|
||||
2. Otherwise, query **Docker Hub** tags.
|
||||
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.
|
||||
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
|
||||
```text
|
||||
PANEL_APP_PORT_HTTP
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
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 - 自动生成应用配置
|
||||
Generate a draft:
|
||||
|
||||
```bash
|
||||
# GitHub 项目
|
||||
./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"
|
||||
./scripts/generate-app.sh --app-key my-app --name MyApp --version 1.2.3 --icon-mode skip ./docker-compose.yml
|
||||
```
|
||||
|
||||
### 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
|
||||
./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
|
||||
./scripts/validate-app.sh ./apps/myapp
|
||||
./scripts/validate-app.sh ../apps/<app-key>
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
Validate Skill tooling:
|
||||
|
||||
- `references/1panel-examples.md` - 完整的真实应用配置示例(AList、NocoDB等)
|
||||
- `examples/example-usage.md` - 使用示例和工作流程
|
||||
```bash
|
||||
./tests/run_all.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
## Icon Policy
|
||||
|
||||
**Issue**: App won't start
|
||||
- Check if ports are already in use
|
||||
- Verify volume permissions
|
||||
- Check container logs: `docker logs ${CONTAINER_NAME}`
|
||||
Icon lookup order:
|
||||
|
||||
**Issue**: Data not persisting
|
||||
- Ensure volumes are correctly mapped to `./data/`
|
||||
- Check directory permissions
|
||||
1. Known explicit URL (`--icon-url`).
|
||||
2. Local cache under `skills/.cache/icons`.
|
||||
3. Dashboard Icons.
|
||||
4. Simple Icons.
|
||||
5. selfh.st Icons.
|
||||
|
||||
**Issue**: Environment variables not working
|
||||
- Verify variable names match between data.yml and docker-compose.yml
|
||||
- Check default values are appropriate
|
||||
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.
|
||||
|
||||
**Issue**: 图标下载失败
|
||||
- 从以下网站手动下载:
|
||||
- https://dashboardicons.com/icons?q={app-name}
|
||||
- https://simpleicons.org/?q={app-name}
|
||||
- https://selfh.st/icons/
|
||||
## Validation Gate
|
||||
|
||||
Before considering an app ready:
|
||||
|
||||
- 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,
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
"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应用商店的配置",
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
"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应用",
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,237 +1,71 @@
|
||||
# 使用示例
|
||||
|
||||
## 示例 1:从 GitHub 项目生成
|
||||
## 从本地 compose 生成草稿
|
||||
|
||||
```bash
|
||||
# 输入
|
||||
./scripts/generate-app.sh https://github.com/alist-org/alist
|
||||
cd /root/github/1Panel-Appstore/skills
|
||||
|
||||
# 输出
|
||||
./apps/alist/v3.45.0/
|
||||
./scripts/generate-app.sh \
|
||||
--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
|
||||
├── docker-compose.yml
|
||||
├── logo.png
|
||||
├── 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
|
||||
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 命令生成
|
||||
## docker run 输入
|
||||
|
||||
```bash
|
||||
# 输入
|
||||
./scripts/generate-app.sh "docker run -d --name=nginx -p 80:80 nginx:latest"
|
||||
|
||||
# 输出
|
||||
./apps/nginx/latest/
|
||||
├── data.yml
|
||||
├── docker-compose.yml
|
||||
├── logo.png
|
||||
├── README.md
|
||||
└── README_en.md
|
||||
./scripts/generate-app.sh \
|
||||
--app-key nginx-demo \
|
||||
--name NginxDemo \
|
||||
--version 1.25.3 \
|
||||
--icon-mode cache-only \
|
||||
"docker run -d --name nginx-demo -p 8080:80 -v ./data/html:/usr/share/nginx/html nginx:1.25.3"
|
||||
```
|
||||
|
||||
### 生成的 docker-compose.yml
|
||||
脚本会解析:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
container_name: ${CONTAINER_NAME}
|
||||
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
|
||||
```
|
||||
- `--name` 作为服务名候选。
|
||||
- `-p` 生成 `PANEL_APP_PORT_*` 表单字段。
|
||||
- `-v` 保留到 compose 的 `volumes`。
|
||||
- 镜像 tag 作为版本候选;显式 `--version` 优先。
|
||||
|
||||
---
|
||||
|
||||
## 示例 3:从 docker-compose 文件链接生成
|
||||
## 图标处理
|
||||
|
||||
```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
|
||||
|
||||
# 输出
|
||||
./apps/portainer/latest/
|
||||
├── data.yml
|
||||
├── docker-compose.yml
|
||||
├── logo.png
|
||||
├── README.md
|
||||
└── README_en.md
|
||||
# 已知准确图标 URL 时强制下载,失败则退出非零
|
||||
./scripts/download-icon.sh --mode required --url https://example.com/nginx.png nginx ../apps/nginx-demo/logo.png
|
||||
```
|
||||
|
||||
---
|
||||
脚本不会创建占位图。找不到真实图标时,应保留缺失状态并在交付说明中指出。
|
||||
|
||||
## 示例 4:从本地文件生成
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# 创建测试文件
|
||||
cat > /tmp/test-compose.yml << 'EOF'
|
||||
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
|
||||
./scripts/validate-app.sh ../apps/demo-app
|
||||
./tests/run_all.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示例 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 # 修改版本号
|
||||
```
|
||||
`validate-app.sh` 负责结构和常见规则检查;仍需要人工检查 README、分类标签、架构支持、默认端口和多服务依赖是否合理。
|
||||
|
||||
+151
-43
@@ -14,6 +14,9 @@ YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 路径配置
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# 图标源配置
|
||||
DASHBOARD_ICONS="https://dashboardicons.com/icons"
|
||||
SIMPLE_ICONS="https://cdn.simpleicons.org"
|
||||
@@ -21,6 +24,12 @@ SELFHST_ICONS="https://selfh.st/icons"
|
||||
|
||||
# 默认输出尺寸
|
||||
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() {
|
||||
@@ -28,13 +37,23 @@ print_help() {
|
||||
${BLUE}Icon Downloader${NC} - 从多个图标源下载应用图标
|
||||
|
||||
${YELLOW}用法:${NC}
|
||||
$0 <应用名称> [输出文件] [尺寸]
|
||||
$0 [选项] <应用名称> [输出文件] [尺寸]
|
||||
|
||||
${YELLOW}参数:${NC}
|
||||
应用名称 - 应用的名称(如 alist, nginx, redis)
|
||||
输出文件 - 图标保存路径(默认: ./logo.png)
|
||||
尺寸 - 图标尺寸(默认: 200x200)
|
||||
|
||||
${YELLOW}选项:${NC}
|
||||
--mode auto|required|skip|cache-only
|
||||
auto: 优先缓存,缺失时尝试下载,未找到不创建占位图
|
||||
required: 找不到图标时返回失败
|
||||
skip: 跳过图标下载
|
||||
cache-only: 只使用缓存,不访问网络
|
||||
--cache-dir <目录>
|
||||
图标缓存目录(默认: skills/.cache/icons)
|
||||
--url <URL> 从指定 URL 下载图标
|
||||
|
||||
${YELLOW}图标源优先级:${NC}
|
||||
1. Dashboard Icons (dashboardicons.com)
|
||||
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_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() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
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)
|
||||
if command -v convert &>/dev/null; then
|
||||
convert "$output" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
||||
@@ -67,6 +132,7 @@ download_and_resize() {
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -77,20 +143,32 @@ try_download_icon() {
|
||||
local size="$3"
|
||||
|
||||
local name_lower
|
||||
name_lower=$(echo "$app_name" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
|
||||
name_lower=$(normalize_name "$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
|
||||
log_info "尝试 Dashboard Icons..."
|
||||
local dashboard_url="${DASHBOARD_ICONS}/${name_lower}.png"
|
||||
if download_and_resize "$dashboard_url" "$output" "$size"; then
|
||||
save_to_cache "$app_name" "$output"
|
||||
log_info "✓ 从 Dashboard Icons 下载成功"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 尝试带 -dark 后缀
|
||||
if download_and_resize "${DASHBOARD_ICONS}/${name_lower}-dark.png" "$output" "$size"; then
|
||||
save_to_cache "$app_name" "$output"
|
||||
log_info "✓ 从 Dashboard Icons (dark) 下载成功"
|
||||
return 0
|
||||
fi
|
||||
@@ -99,18 +177,20 @@ try_download_icon() {
|
||||
log_info "尝试 Simple Icons..."
|
||||
local simple_url="${SIMPLE_ICONS}/${name_lower}"
|
||||
if download_and_resize "$simple_url" "$output" "$size"; then
|
||||
save_to_cache "$app_name" "$output"
|
||||
log_info "✓ 从 Simple Icons 下载成功"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 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 成功"
|
||||
# 如果有 SVG,尝试转换
|
||||
if command -v convert &>/dev/null; then
|
||||
convert "${output%.png}.svg" -resize "${size}x${size}" "$output" 2>/dev/null || true
|
||||
if [[ -s "$output" ]]; then
|
||||
rm -f "${output%.png}.svg"
|
||||
save_to_cache "$app_name" "$output"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -120,6 +200,7 @@ try_download_icon() {
|
||||
log_info "尝试 selfh.st Icons..."
|
||||
local selfhst_url="${SELFHST_ICONS}/${name_lower}.png"
|
||||
if download_and_resize "$selfhst_url" "$output" "$size"; then
|
||||
save_to_cache "$app_name" "$output"
|
||||
log_info "✓ 从 selfh.st Icons 下载成功"
|
||||
return 0
|
||||
fi
|
||||
@@ -127,43 +208,58 @@ try_download_icon() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 创建占位图标
|
||||
create_placeholder() {
|
||||
local output="$1"
|
||||
local size="$2"
|
||||
parse_args() {
|
||||
POSITIONAL=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
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 "创建占位图标"
|
||||
|
||||
# 使用 ImageMagick 创建简单的占位图标
|
||||
if command -v convert &>/dev/null; then
|
||||
convert -size "${size}x${size}" xc:'#4A90E2' \
|
||||
-gravity center \
|
||||
-pointsize 48 \
|
||||
-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"
|
||||
case "$MODE" in
|
||||
auto|required|skip|cache-only) ;;
|
||||
*)
|
||||
log_error "无效模式: $MODE"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
local app_name="${1:-}"
|
||||
local output="${2:-./logo.png}"
|
||||
local size="${3:-$DEFAULT_SIZE}"
|
||||
parse_args "$@"
|
||||
|
||||
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
|
||||
@@ -171,10 +267,19 @@ main() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 确保输出目录存在
|
||||
local output_dir
|
||||
output_dir=$(dirname "$output")
|
||||
mkdir -p "$output_dir"
|
||||
if [[ "$MODE" == "skip" ]]; then
|
||||
log_info "跳过图标下载: $app_name"
|
||||
exit 0
|
||||
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
|
||||
@@ -182,13 +287,16 @@ main() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 下载失败,创建占位符
|
||||
create_placeholder "$output" "$size"
|
||||
log_warn "未找到图标,已创建占位符: $output"
|
||||
rm -f "$output" "${output%.png}.svg"
|
||||
log_warn "未找到图标,未创建占位图: $output"
|
||||
log_info "请手动从以下网站下载合适的图标:"
|
||||
echo " - https://dashboardicons.com/icons?q=${app_name}"
|
||||
echo " - https://simpleicons.org/?q=${app_name}"
|
||||
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)"
|
||||
TEMPLATE_DIR="${SCRIPT_DIR}/../templates"
|
||||
OUTPUT_BASE="${2:-./apps}"
|
||||
ICON_SOURCES=(
|
||||
"https://dashboardicons.com/icons"
|
||||
"https://simpleicons.org/icons"
|
||||
"https://selfh.st/icons"
|
||||
)
|
||||
OUTPUT_BASE="./apps"
|
||||
ICON_MODE="auto"
|
||||
ICON_URL=""
|
||||
ICON_CACHE_DIR="${SCRIPT_DIR}/../.cache/icons"
|
||||
USER_APP_KEY=""
|
||||
USER_APP_NAME=""
|
||||
USER_VERSION=""
|
||||
FORCE=false
|
||||
DRY_RUN=false
|
||||
POSITIONAL=()
|
||||
SERVICE_VOLUMES=()
|
||||
SERVICE_ENV=()
|
||||
|
||||
# 打印帮助
|
||||
print_help() {
|
||||
@@ -31,7 +37,7 @@ print_help() {
|
||||
${BLUE}1Panel App Builder${NC} - 快速生成 1Panel 应用配置
|
||||
|
||||
${YELLOW}用法:${NC}
|
||||
$0 <输入源> [输出目录]
|
||||
$0 [选项] <输入源> [输出目录]
|
||||
|
||||
${YELLOW}输入源支持:${NC}
|
||||
1. GitHub 项目链接
|
||||
@@ -59,6 +65,17 @@ ${YELLOW}示例:${NC}
|
||||
$0 https://github.com/alist-org/alist
|
||||
$0 ./my-docker-compose.yml ./my-apps
|
||||
$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
|
||||
}
|
||||
|
||||
@@ -67,6 +84,79 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${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)
|
||||
parse_image_ref() {
|
||||
local image_ref="$1"
|
||||
@@ -110,10 +200,10 @@ get_latest_tag_docker_hub() {
|
||||
local repo_path="$2"
|
||||
local url="https://hub.docker.com/v2/repositories/${namespace}/${repo_path}/tags?page_size=100&ordering=last_updated"
|
||||
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
|
||||
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
|
||||
echo "$semver"
|
||||
return 0
|
||||
@@ -130,16 +220,16 @@ get_latest_tag_ghcr() {
|
||||
local repo_path="$2"
|
||||
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
|
||||
echo "latest"
|
||||
return 0
|
||||
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
|
||||
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
|
||||
echo "$semver"
|
||||
return 0
|
||||
@@ -278,11 +368,11 @@ extract_from_github() {
|
||||
|
||||
# 获取项目信息
|
||||
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_KEY=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
APP_KEY=$(normalize_app_key "$APP_NAME")
|
||||
DESCRIPTION=$(echo "$repo_info" | jq -r '.description // empty')
|
||||
GITHUB="$github_url"
|
||||
WEBSITE=$(echo "$repo_info" | jq -r '.homepage // empty')
|
||||
@@ -306,11 +396,11 @@ extract_from_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
|
||||
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"
|
||||
|
||||
# 提取端口
|
||||
@@ -318,12 +408,24 @@ extract_from_compose() {
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" || "$line" == "null" ]] && continue
|
||||
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
|
||||
PORT_ENTRIES=("8080")
|
||||
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 "镜像: $IMAGE_BASE:$TAG"
|
||||
log_info "端口数量: ${#PORT_ENTRIES[@]}"
|
||||
@@ -341,7 +443,7 @@ extract_from_docker_run() {
|
||||
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=()
|
||||
@@ -354,8 +456,20 @@ extract_from_docker_run() {
|
||||
PORT_ENTRIES=("8080")
|
||||
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_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||
|
||||
log_info "应用名称: $APP_NAME"
|
||||
log_info "镜像: $IMAGE_BASE:$TAG"
|
||||
@@ -466,11 +580,41 @@ EOF
|
||||
|
||||
cat >> "$output_file" << EOF
|
||||
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
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> "$output_file" << EOF
|
||||
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
|
||||
- PGID=0
|
||||
- UMASK=022
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> "$output_file" << EOF
|
||||
image: ${IMAGE_BASE:-app}:${image_tag}
|
||||
labels:
|
||||
createdBy: "Apps"
|
||||
@@ -531,58 +675,30 @@ EOF
|
||||
download_icon() {
|
||||
local app_name="$1"
|
||||
local output_file="$2"
|
||||
local github_url="${3:-}"
|
||||
local icon_name
|
||||
local args=(--mode "$ICON_MODE" --cache-dir "$ICON_CACHE_DIR")
|
||||
|
||||
icon_name=$(echo "$app_name" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
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
|
||||
if [[ -n "$ICON_URL" ]]; then
|
||||
args+=(--url "$ICON_URL")
|
||||
fi
|
||||
|
||||
# 尝试各个图标源
|
||||
for source in "${ICON_SOURCES[@]}"; do
|
||||
local icon_url="${source}/${icon_name}.png"
|
||||
if curl -sSL -o "$output_file" "$icon_url" 2>/dev/null && [[ -s "$output_file" ]]; then
|
||||
log_info "图标下载成功: $icon_url"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
if bash "${SCRIPT_DIR}/download-icon.sh" "${args[@]}" "$app_name" "$output_file"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$ICON_MODE" == "required" ]]; then
|
||||
log_error "图标为 required 模式,但未能获取图标"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_warn "未找到图标,请手动补充 logo.png"
|
||||
return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
local input="$1"
|
||||
parse_args "$@"
|
||||
|
||||
local input="${POSITIONAL[0]:-}"
|
||||
local input_type
|
||||
|
||||
# 参数检查
|
||||
@@ -607,16 +723,16 @@ main() {
|
||||
fi
|
||||
;;
|
||||
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"
|
||||
APP_NAME="$SERVICE_NAME"
|
||||
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||
;;
|
||||
compose_file)
|
||||
COMPOSE_CONTENT=$(cat "$input")
|
||||
extract_from_compose "$COMPOSE_CONTENT"
|
||||
APP_NAME="$SERVICE_NAME"
|
||||
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
|
||||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||||
;;
|
||||
docker_run)
|
||||
extract_from_docker_run "$input"
|
||||
@@ -627,6 +743,16 @@ main() {
|
||||
;;
|
||||
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=()
|
||||
CONTAINER_PORTS=()
|
||||
@@ -651,16 +777,33 @@ main() {
|
||||
done
|
||||
|
||||
# 获取最新版本号
|
||||
VERSION_TAG=$(get_latest_tag)
|
||||
if [[ -z "$VERSION_TAG" ]]; then
|
||||
VERSION_TAG="$TAG"
|
||||
if [[ -n "$USER_VERSION" ]]; then
|
||||
VERSION_TAG="$USER_VERSION"
|
||||
else
|
||||
VERSION_TAG=$(get_latest_tag)
|
||||
if [[ -z "$VERSION_TAG" ]]; then
|
||||
VERSION_TAG="$TAG"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$VERSION_TAG" ]]; then
|
||||
VERSION_TAG="latest"
|
||||
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 + 具体版本)
|
||||
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"
|
||||
|
||||
local output_dir_version=""
|
||||
@@ -687,7 +830,7 @@ main() {
|
||||
fi
|
||||
|
||||
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
|
||||
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_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; ((WARNINGS++)); }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; ((ERRORS++)); }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; ((WARNINGS+=1)); }
|
||||
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() {
|
||||
@@ -66,11 +78,11 @@ check_directory_structure() {
|
||||
|
||||
# 查找版本目录
|
||||
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
|
||||
log_error "未找到版本目录(如 1.0.0/, v1.0.0/, latest/)"
|
||||
return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查每个版本目录
|
||||
@@ -90,6 +102,9 @@ check_directory_structure() {
|
||||
# 验证顶层 data.yml
|
||||
validate_top_data_yml() {
|
||||
local data_file="$1"
|
||||
local app_dir app_name
|
||||
app_dir="$(dirname "$data_file")"
|
||||
app_name="$(basename "$app_dir")"
|
||||
|
||||
log_info "验证顶层 data.yml"
|
||||
|
||||
@@ -115,6 +130,14 @@ validate_top_data_yml() {
|
||||
log_error "data.yml 缺少 additionalProperties.key"
|
||||
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
|
||||
if ! grep -q "tags:" "$data_file"; then
|
||||
log_warn "data.yml 缺少 tags(推荐)"
|
||||
@@ -153,6 +176,86 @@ validate_version_data_yml() {
|
||||
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
|
||||
validate_docker_compose() {
|
||||
local compose_file="$1"
|
||||
@@ -226,14 +329,17 @@ main() {
|
||||
# 执行检查
|
||||
check_directory_structure "$app_dir"
|
||||
validate_top_data_yml "$app_dir/data.yml"
|
||||
validate_logo_file "$app_dir/logo.png"
|
||||
|
||||
# 查找并验证版本目录
|
||||
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
|
||||
validate_version_data_yml "$version_dir/data.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
|
||||
|
||||
# 输出结果
|
||||
|
||||
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