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
+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
# 输出结果