feat: update skills

This commit is contained in:
arch3rPro
2026-04-01 18:41:10 +08:00
parent 6013a7944e
commit a81c787a0b
2 changed files with 388 additions and 47 deletions

View File

@@ -59,7 +59,23 @@ Based on user input type, extract Docker deployment details:
2. Extract the image name and tag 2. Extract the image name and tag
3. Identify exposed ports and data directories 3. Identify exposed ports and data directories
### Step 2: Generate App Key and Metadata ### 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:** **App Key Rules:**
- Lowercase only - Lowercase only
@@ -100,7 +116,7 @@ additionalProperties:
# Add: arm/v7, arm/v6, s390x if supported # Add: arm/v7, arm/v6, s390x if supported
``` ```
### Step 3: Define Parameters (version/data.yml) ### Step 4: Define Parameters (version/data.yml)
Parameters become UI form fields in 1Panel. Define user-configurable values: Parameters become UI form fields in 1Panel. Define user-configurable values:
@@ -170,7 +186,25 @@ additionalProperties:
- `paramExtUrl`: Valid URL format - `paramExtUrl`: Valid URL format
- Empty string: No validation - Empty string: No validation
### Step 4: Create docker-compose.yml ### 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: Convert the original Docker deployment to use variable substitution:
@@ -199,12 +233,20 @@ networks:
1. Always use `${CONTAINER_NAME}` for container_name 1. Always use `${CONTAINER_NAME}` for container_name
2. Always use `restart: always` 2. Always use `restart: always`
3. Always connect to `1panel-network` (external network) 3. Always connect to `1panel-network` (external network)
4. Port mapping uses `PANEL_APP_PORT_*` variables from data.yml 4. Port mapping uses `PANEL_APP_PORT_*` variables from version/data.yml
5. Volume paths use relative `./data/` for persistence 5. Volume paths use relative `./data/` for persistence
6. Add `labels: createdBy: "Apps"` 6. Add `labels: createdBy: "Apps"`
7. Keep original environment variables but use defaults 7. Keep original environment variables but use defaults
### Step 5: Create README Files **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):** **README.md (Chinese):**
```markdown ```markdown
@@ -269,17 +311,19 @@ Brief description of the application.
- GitHub: https://github.com/org/repo - GitHub: https://github.com/org/repo
``` ```
### Step 6: Download App Icon ### Step 7: Download App Icon
Search and download the app logo from these sources (in order): **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} 1. **Dashboard Icons**: https://dashboardicons.com/icons?q={app-name}
2. **Simple Icons**: https://simpleicons.org/?q={app-name} 2. **Simple Icons**: https://simpleicons.org/?q={app-name}
3. **Selfh.st Icons**: https://selfh.st/icons/ 3. **Selfh.st Icons**: https://selfh.st/icons/
Save as `logo.png` in the app root directory. **Do not** create a placeholder or incorrect icon. If not found, warn and leave `logo.png` for manual replacement.
### Step 7: Create Data Directory Structure ### Step 8: Create Data Directory Structure
Create the `data/` directory inside the version folder: Create the `data/` directory inside the version folder:
```bash ```bash
@@ -294,13 +338,16 @@ Before delivering the app package, verify:
- [ ] App key is lowercase with hyphens only - [ ] App key is lowercase with hyphens only
- [ ] All required fields in top-level data.yml are present - [ ] All required fields in top-level data.yml are present
- [ ] Version data.yml has proper parameter definitions - [ ] `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 - [ ] docker-compose.yml uses variable substitution correctly
- [ ] `1panel-network` is defined as external network - [ ] `1panel-network` is defined as external network
- [ ] Volume paths use relative `./data/` format - [ ] Volume paths use relative `./data/` format
- [ ] README files are created with proper documentation - [ ] README files are created with proper documentation
- [ ] Logo file exists (logo.png) - [ ] Logo file exists (logo.png)
- [ ] Version directory follows semver or uses "latest" - [ ] Version directory follows semver; avoid only `latest`
## Common Patterns ## Common Patterns

View File

@@ -67,6 +67,171 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 解析镜像引用(支持 registry/namespace/repo:tag
parse_image_ref() {
local image_ref="$1"
local image_no_tag tag
# 去掉 digest
image_ref="${image_ref%%@*}"
if [[ "$image_ref" =~ ^(.+):([^/:]+)$ ]]; then
image_no_tag="${BASH_REMATCH[1]}"
tag="${BASH_REMATCH[2]}"
else
image_no_tag="$image_ref"
tag="latest"
fi
local first_segment="${image_no_tag%%/*}"
if [[ "$image_no_tag" == */* ]] && ([[ "$first_segment" == *.* ]] || [[ "$first_segment" == *:* ]] || [[ "$first_segment" == "localhost" ]]); then
REGISTRY="$first_segment"
IMAGE_PATH="${image_no_tag#*/}"
else
REGISTRY="docker.io"
IMAGE_PATH="$image_no_tag"
fi
if [[ "$IMAGE_PATH" == */* ]]; then
NAMESPACE="${IMAGE_PATH%%/*}"
REPO_PATH="${IMAGE_PATH#*/}"
else
NAMESPACE="library"
REPO_PATH="$IMAGE_PATH"
fi
IMAGE_BASE="$image_no_tag"
TAG="$tag"
}
# 获取 Docker Hub 最新版本号
get_latest_tag_docker_hub() {
local namespace="$1"
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)
local semver
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\\.[0-9]+(\\.[0-9]+)?$' | head -n1 || true)
if [[ -n "$semver" ]]; then
echo "$semver"
return 0
fi
local nonlatest
nonlatest=$(echo "$tags" | grep -v '^latest$' | head -n1 || true)
echo "${nonlatest:-latest}"
}
# 获取 GHCR 最新版本号
get_latest_tag_ghcr() {
local namespace="$1"
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)
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)
local semver
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
fi
local nonlatest
nonlatest=$(echo "$tags" | grep -v '^latest$' | sort -V | tail -n1 || true)
echo "${nonlatest:-latest}"
}
# 统一获取最新版本号
get_latest_tag() {
if [[ "$REGISTRY" == "ghcr.io" ]]; then
get_latest_tag_ghcr "$NAMESPACE" "$REPO_PATH"
return 0
fi
# 默认走 Docker Hub
get_latest_tag_docker_hub "$NAMESPACE" "$REPO_PATH"
}
# 解析端口映射条目为 host:container
parse_port_entry() {
local entry="$1"
entry="${entry%\"}"
entry="${entry#\"}"
if [[ "$entry" == */* ]]; then
entry="${entry%/*}"
fi
IFS=':' read -r -a parts <<< "$entry"
local host_port container_port
if [[ ${#parts[@]} -eq 1 ]]; then
host_port="${parts[0]}"
container_port="${parts[0]}"
elif [[ ${#parts[@]} -eq 2 ]]; then
host_port="${parts[0]}"
container_port="${parts[1]}"
else
host_port="${parts[-2]}"
container_port="${parts[-1]}"
fi
echo "${host_port}:${container_port}"
}
# 根据端口号选择 1Panel 常用 envKey
map_port_envkey() {
local port="$1"
local index="$2"
case "$port" in
80|8080|3000|5173|8000|5000)
echo "PANEL_APP_PORT_HTTP"
;;
443|8443)
echo "PANEL_APP_PORT_HTTPS"
;;
22)
echo "PANEL_APP_PORT_SSH"
;;
3306|5432|27017|6379)
echo "PANEL_APP_PORT_DB"
;;
9000|9001|8081|7001)
echo "PANEL_APP_PORT_API"
;;
*)
if [[ "$index" -eq 0 ]]; then
echo "PANEL_APP_PORT_HTTP"
else
echo "PANEL_APP_PORT_API"
fi
;;
esac
}
# envKey 对应标签
labels_for_envkey() {
local envkey="$1"
case "$envkey" in
PANEL_APP_PORT_HTTP) echo "Web Port|Web端口" ;;
PANEL_APP_PORT_HTTPS) echo "HTTPS Port|HTTPS端口" ;;
PANEL_APP_PORT_API) echo "API Port|API端口" ;;
PANEL_APP_PORT_ADMIN) echo "Admin Port|管理端口" ;;
PANEL_APP_PORT_PROXY) echo "Proxy Port|代理端口" ;;
PANEL_APP_PORT_DB) echo "DB Port|数据库端口" ;;
PANEL_APP_PORT_SSH) echo "SSH Port|SSH端口" ;;
PANEL_APP_PORT_S3) echo "S3 Port|S3端口" ;;
PANEL_APP_PORT_PROXY_HTTP) echo "Proxy HTTP Port|代理HTTP端口" ;;
PANEL_APP_PORT_PROXY_HTTPS) echo "Proxy HTTPS Port|代理HTTPS端口" ;;
PANEL_APP_PORT_SYNC) echo "Sync Port|同步端口" ;;
*) echo "Port|端口" ;;
esac
}
# 检测输入类型 # 检测输入类型
detect_input_type() { detect_input_type() {
local input="$1" local input="$1"
@@ -146,22 +311,22 @@ extract_from_compose() {
# 提取镜像 # 提取镜像
local image local image
image=$(echo "$compose_content" | yq ".services.${SERVICE_NAME}.image" 2>/dev/null || echo "") image=$(echo "$compose_content" | yq ".services.${SERVICE_NAME}.image" 2>/dev/null || echo "")
parse_image_ref "$image"
# 解析镜像名和标签
if [[ "$image" =~ ^([^:]+):(.+)$ ]]; then
IMAGE="${BASH_REMATCH[1]}"
TAG="${BASH_REMATCH[2]}"
else
IMAGE="$image"
TAG="latest"
fi
# 提取端口 # 提取端口
PORT=$(echo "$compose_content" | yq ".services.${SERVICE_NAME}.ports[0]" 2>/dev/null | grep -oE '[0-9]+$' || echo "8080") PORT_ENTRIES=()
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)
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
PORT_ENTRIES=("8080")
fi
log_info "服务名: $SERVICE_NAME" log_info "服务名: $SERVICE_NAME"
log_info "镜像: $IMAGE:$TAG" log_info "镜像: $IMAGE_BASE:$TAG"
log_info "端口: $PORT" log_info "端口数量: ${#PORT_ENTRIES[@]}"
} }
# 从 docker run 命令提取信息 # 从 docker run 命令提取信息
@@ -171,21 +336,30 @@ extract_from_docker_run() {
log_info "正在解析 docker run 命令" log_info "正在解析 docker run 命令"
# 提取镜像 # 提取镜像
IMAGE=$(echo "$cmd" | grep -oE '[^ ]+:[^ ]+$' | cut -d':' -f1 || echo "") local image_ref
TAG=$(echo "$cmd" | grep -oE '[^ ]+:[^ ]+$' | cut -d':' -f2 || echo "latest") image_ref=$(echo "$cmd" | awk '{print $NF}')
parse_image_ref "$image_ref"
# 提取容器名 # 提取容器名
SERVICE_NAME=$(echo "$cmd" | grep -oE '\-\-name[= ]+[^ ]+' | awk '{print $NF}' || echo "app") SERVICE_NAME=$(echo "$cmd" | grep -oE '\-\-name[= ]+[^ ]+' | awk '{print $NF}' || echo "app")
# 提取端口 # 提取端口
PORT=$(echo "$cmd" | grep -oE '\-p[= ]+[0-9]+:[0-9]+' | grep -oE '[0-9]+$' || echo "8080") PORT_ENTRIES=()
while IFS= read -r line; do
[[ -z "$line" ]] && continue
PORT_ENTRIES+=("$line")
done < <(echo "$cmd" | grep -oE '\-p[= ]+[^ ]+' | awk '{print $NF}' || true)
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
PORT_ENTRIES=("8080")
fi
APP_NAME="$SERVICE_NAME" APP_NAME="$SERVICE_NAME"
APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]') APP_KEY=$(echo "$SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
log_info "应用名称: $APP_NAME" log_info "应用名称: $APP_NAME"
log_info "镜像: $IMAGE:$TAG" log_info "镜像: $IMAGE_BASE:$TAG"
log_info "端口: $PORT" log_info "端口数量: ${#PORT_ENTRIES[@]}"
} }
# 生成 data.yml # 生成 data.yml
@@ -231,9 +405,45 @@ additionalProperties:
EOF EOF
} }
# 生成 version/data.yml参数定义
generate_version_data_yml() {
local output_file="$1"
log_info "生成版本 data.yml: $output_file"
cat > "$output_file" << EOF
additionalProperties:
formFields:
EOF
local i
for ((i=0; i<${#CONTAINER_PORTS[@]}; i++)); do
local envkey="${PORT_ENV_KEYS[$i]}"
local default_port="${HOST_PORTS[$i]}"
local labels
labels=$(labels_for_envkey "$envkey")
local label_en="${labels%%|*}"
local label_zh="${labels##*|}"
cat >> "$output_file" << EOF
- default: ${default_port}
edit: true
envKey: ${envkey}
labelEn: ${label_en}
labelZh: ${label_zh}
required: true
rule: paramPort
type: number
label:
en: ${label_en}
zh: ${label_zh}
EOF
done
}
# 生成 docker-compose.yml # 生成 docker-compose.yml
generate_docker_compose() { generate_docker_compose() {
local output_file="$1" local output_file="$1"
local image_tag="$2"
log_info "生成 docker-compose.yml: $output_file" log_info "生成 docker-compose.yml: $output_file"
@@ -245,14 +455,23 @@ services:
networks: networks:
- 1panel-network - 1panel-network
ports: ports:
- "\${PANEL_APP_PORT_HTTP}:${PORT:-8080}" EOF
local i
for ((i=0; i<${#CONTAINER_PORTS[@]}; i++)); do
cat >> "$output_file" << EOF
- "\${${PORT_ENV_KEYS[$i]}}:${CONTAINER_PORTS[$i]}"
EOF
done
cat >> "$output_file" << EOF
volumes: volumes:
- ./data/data:/app/data - ./data/data:/app/data
environment: environment:
- PUID=0 - PUID=0
- PGID=0 - PGID=0
- UMASK=022 - UMASK=022
image: ${IMAGE:-app}:${TAG:-latest} image: ${IMAGE_BASE:-app}:${image_tag}
labels: labels:
createdBy: "Apps" createdBy: "Apps"
networks: networks:
@@ -312,11 +531,41 @@ EOF
download_icon() { download_icon() {
local app_name="$1" local app_name="$1"
local output_file="$2" local output_file="$2"
local github_url="${3:-}"
local icon_name local icon_name
icon_name=$(echo "$app_name" | tr '[:upper:]' '[:lower:]') icon_name=$(echo "$app_name" | tr '[:upper:]' '[:lower:]')
log_info "尝试下载图标: $app_name" log_info "尝试获取图标: $app_name"
# 优先从 GitHub 仓库内查找常见图标文件
if [[ -n "$github_url" ]]; then
local owner_repo repo_api candidate_paths
owner_repo=$(echo "$github_url" | sed -E 's|https?://github.com/||')
repo_api="https://api.github.com/repos/${owner_repo}"
candidate_paths=(
"logo.png"
"icon.png"
"logo.svg"
"icon.svg"
"assets/logo.png"
"assets/icon.png"
".github/logo.png"
".github/icon.png"
)
for path in "${candidate_paths[@]}"; do
local file_api="${repo_api}/contents/${path}"
local download_url
download_url=$(curl -s "$file_api" | jq -r '.download_url // empty' 2>/dev/null || true)
if [[ -n "$download_url" ]]; then
if curl -sSL -o "$output_file" "$download_url" 2>/dev/null && [[ -s "$output_file" ]]; then
log_info "图标来自仓库: ${path}"
return 0
fi
fi
done
fi
# 尝试各个图标源 # 尝试各个图标源
for source in "${ICON_SOURCES[@]}"; do for source in "${ICON_SOURCES[@]}"; do
@@ -327,9 +576,8 @@ download_icon() {
fi fi
done done
log_warn "未找到图标,使用占位符" log_warn "未找到图标,请手动补充 logo.png"
# 创建简单的占位图标1x1 像素 PNG return 1
echo -n "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" | base64 -d > "$output_file"
} }
# 主函数 # 主函数
@@ -379,24 +627,70 @@ main() {
;; ;;
esac esac
# 创建输出目录 # 生成端口映射数组
local output_dir="${OUTPUT_BASE}/${APP_KEY}/${TAG:-latest}" HOST_PORTS=()
mkdir -p "$output_dir" CONTAINER_PORTS=()
PORT_ENV_KEYS=()
PORT_ENV_KEYS_USED=()
local i
for ((i=0; i<${#PORT_ENTRIES[@]}; i++)); do
local mapping
mapping=$(parse_port_entry "${PORT_ENTRIES[$i]}")
local host_port="${mapping%%:*}"
local container_port="${mapping##*:}"
HOST_PORTS+=("$host_port")
CONTAINER_PORTS+=("$container_port")
local envkey
envkey=$(map_port_envkey "$container_port" "$i")
# 保证 envKey 唯一
if [[ " ${PORT_ENV_KEYS_USED[*]} " == *" ${envkey} "* ]]; then
envkey="${envkey}_${i}"
fi
PORT_ENV_KEYS+=("$envkey")
PORT_ENV_KEYS_USED+=("$envkey")
done
log_info "输出目录: $output_dir" # 获取最新版本号
VERSION_TAG=$(get_latest_tag)
if [[ -z "$VERSION_TAG" ]]; then
VERSION_TAG="$TAG"
fi
if [[ -z "$VERSION_TAG" ]]; then
VERSION_TAG="latest"
fi
# 生成文件 # 创建输出目录latest + 具体版本)
generate_data_yml "${output_dir}/data.yml" local output_dir_latest="${OUTPUT_BASE}/${APP_KEY}/latest"
generate_docker_compose "${output_dir}/docker-compose.yml" mkdir -p "$output_dir_latest"
generate_readme "$output_dir"
download_icon "$APP_NAME" "${output_dir}/logo.png" local output_dir_version=""
if [[ "$VERSION_TAG" != "latest" ]]; then
output_dir_version="${OUTPUT_BASE}/${APP_KEY}/${VERSION_TAG}"
mkdir -p "$output_dir_version"
else
log_warn "未能获取具体版本号version 目录将使用 latest请手动修正"
fi
log_info "输出目录: $output_dir_latest"
if [[ -n "$output_dir_version" ]]; then
log_info "输出目录: $output_dir_version"
fi
# 生成 latest 版本文件
generate_version_data_yml "${output_dir_latest}/data.yml"
generate_docker_compose "${output_dir_latest}/docker-compose.yml" "latest"
# 生成具体版本文件
if [[ -n "$output_dir_version" ]]; then
generate_version_data_yml "${output_dir_version}/data.yml"
generate_docker_compose "${output_dir_version}/docker-compose.yml" "$VERSION_TAG"
fi
generate_readme "${OUTPUT_BASE}/${APP_KEY}"
download_icon "$APP_NAME" "${OUTPUT_BASE}/${APP_KEY}/logo.png" "${GITHUB:-}"
# 生成上级目录的 data.yml # 生成上级目录的 data.yml
generate_data_yml "${OUTPUT_BASE}/${APP_KEY}/data.yml" generate_data_yml "${OUTPUT_BASE}/${APP_KEY}/data.yml"
cp "${output_dir}/logo.png" "${OUTPUT_BASE}/${APP_KEY}/logo.png" 2>/dev/null || true
cp "${output_dir}/README.md" "${OUTPUT_BASE}/${APP_KEY}/README.md" 2>/dev/null || true
cp "${output_dir}/README_en.md" "${OUTPUT_BASE}/${APP_KEY}/README_en.md" 2>/dev/null || true
# 输出结果 # 输出结果
echo "" echo ""
log_info "${GREEN}✓ 生成完成!${NC}" log_info "${GREEN}✓ 生成完成!${NC}"