chore: optimize 1panel app builder skill

This commit is contained in:
arch3rPro
2026-05-26 01:07:55 +08:00
parent 155cc17d2c
commit 4092625c30
9 changed files with 905 additions and 925 deletions
+42 -181
View File
@@ -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
View File
@@ -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.
+3 -3
View File
@@ -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": []
}
]
+45 -211
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
# 输出结果
+16
View File
@@ -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."
+211
View File
@@ -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."