用法:

bash ./push_quail.sh /Users/tetsuya/Dev/iamcheyan.com/app/pelican/content

脚本并指定包含日志的路径,脚本会批量上传文章和里面的图片到 Quaily 中。

#!/bin/bash
# 批量发布指定目录下的所有 markdown 文章到 Quaily
# 用法:./push.sh <目录路径>

set -e  # 遇到错误时退出

LIST_SLUG="tetsuya"   # 你的 Quaily list_slug,这里固定为 tetsuya

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 统一编码,避免中文/特殊字符路径在子进程中编码异常
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

# 统计变量
total_files=0
success_count=0
error_count=0
skip_count=0

# 检查参数
if [ $# -eq 0 ]; then
    echo -e "${YELLOW}用法:${NC}"
    echo -e "  ${BLUE}./push.sh <目录路径>${NC}  # 发布指定目录下的所有 markdown 文件"
    echo ""
    echo -e "${YELLOW}示例:${NC}"
    echo -e "  ${BLUE}./push.sh .${NC}              # 发布当前目录下的所有 markdown 文件"
    echo -e "  ${BLUE}./push.sh /path/to/articles${NC}  # 发布指定目录下的所有 markdown 文件"
    echo -e "  ${BLUE}./push.sh ~/Documents/blog${NC}   # 发布博客目录下的所有 markdown 文件"
    exit 0
fi

TARGET_DIR="$1"

# 检查目录是否存在
if [ ! -d "$TARGET_DIR" ]; then
    echo -e "${RED}❌ 错误: 目录不存在: $TARGET_DIR${NC}"
    exit 1
fi

# 获取绝对路径
TARGET_DIR=$(cd "$TARGET_DIR" && pwd)

echo -e "${BLUE}🚀 开始批量发布文章到 Quaily...${NC}"
echo -e "${BLUE}📋 目标列表: $LIST_SLUG${NC}"
echo -e "${BLUE}📁 搜索目录: $TARGET_DIR${NC}"
echo ""

# 检查 quail-cli 是否已安装
if ! command -v quail-cli &> /dev/null; then
    echo -e "${RED}❌ 错误: quail-cli 未安装或不在 PATH 中${NC}"
    echo "请先安装 quail-cli: go install github.com/quail-ink/quail-cli@latest"
    exit 1
fi

# 检查是否已登录
if ! quail-cli me &> /dev/null; then
    echo -e "${RED}❌ 错误: 未登录 Quaily${NC}"
    echo "请先运行: quail-cli login"
    exit 1
fi

# 获取访问令牌
ACCESS_TOKEN=$(grep -E '^\s*access_token:' ~/.config/quail-cli/config.yaml | awk '{print $2}' | tr -d '"')
if [ -z "$ACCESS_TOKEN" ]; then
    echo -e "${RED}❌ 错误: 无法获取访问令牌${NC}"
    echo "请先运行: quail-cli login"
    exit 1
fi

# 查找所有 .md 文件(递归搜索)
echo -e "${BLUE}🔍 正在搜索 markdown 文件...${NC}"
md_files=()
while IFS= read -r -d '' file; do
    md_files+=("$file")
done < <(find "$TARGET_DIR" -name "*.md" -type f -print0)

if [ ${#md_files[@]} -eq 0 ]; then
    echo -e "${YELLOW}⚠️ 在目录 $TARGET_DIR 及其子目录中没有找到 .md 文件${NC}"
    exit 0
fi

total_files=${#md_files[@]}
echo -e "${BLUE}📊 找到 $total_files 个 markdown 文件${NC}"
echo ""

# 显示找到的文件列表
echo -e "${BLUE}📋 找到的文件:${NC}"
for i in "${!md_files[@]}"; do
    # 计算相对路径
    relative_path="${md_files[$i]#$TARGET_DIR/}"
    echo -e "  ${YELLOW}$((i + 1)).${NC} $relative_path"
done
echo ""

# 上传图片到 Quail 的函数
upload_image_to_quail() {
    local image_path="$1"
    local temp_file=$(mktemp)
    local mime_type
    if command -v file >/dev/null 2>&1; then
        mime_type=$(file -b --mime-type "$image_path" 2>/dev/null || echo "application/octet-stream")
    else
        mime_type="application/octet-stream"
    fi
    # 将上传文件复制到仅包含 ASCII 的临时路径,避免 curl 在本地读取非 ASCII/特殊字符路径时报错
    local tmp_dir
    tmp_dir=$(mktemp -d)
    local orig_basename
    orig_basename=$(basename "$image_path")
    local ascii_basename
    if command -v iconv >/dev/null 2>&1; then
        ascii_basename=$(printf '%s' "$orig_basename" | iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null | sed 's/[^A-Za-z0-9._-]/_/g')
    else
        ascii_basename=$(printf '%s' "$orig_basename" | sed 's/[^A-Za-z0-9._-]/_/g')
    fi
    [ -z "$ascii_basename" ] && ascii_basename="upload.bin"
    local upload_local_path="$tmp_dir/$ascii_basename"
    cp "$image_path" "$upload_local_path"
    
    # 使用 curl 上传图片到 Quail Attachment API
    local response
    if ! response=$(curl -sS -w "%{http_code}" \
        -H "Authorization: Bearer $ACCESS_TOKEN" \
        -F "file=@${upload_local_path}" \
        -o "$temp_file" \
        "https://api.quail.ink/attachments"); then
        echo -e "    ❌ CURL 执行失败" >&2
        [ -s "$temp_file" ] && echo -e "    🔎 服务返回: $(cat "$temp_file")" >&2
        rm -f "$upload_local_path"
        rmdir "$tmp_dir" 2>/dev/null || true
        rm -f "$temp_file"
        return 1
    fi
    
    local http_code="${response: -3}"
    
    if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
        # 解析返回的 JSON 获取 view_url(兼容 data.view_url 与顶层 view_url)
        local view_url
        if command -v jq >/dev/null 2>&1; then
            view_url=$(jq -r '.data.view_url // .view_url // empty' "$temp_file")
        else
            view_url=$(grep -o '"view_url":"[^"[:space:]]*"' "$temp_file" | head -n1 | cut -d'"' -f4)
            # 再次兜底:直接匹配静态域名 URL
            [ -z "$view_url" ] && view_url=$(grep -o 'https://static\.quail\.ink/[^"[:space:]]*' "$temp_file" | head -n1)
        fi
        if [ -n "$view_url" ]; then
            echo "$view_url"
            rm -f "$upload_local_path"
            rmdir "$tmp_dir" 2>/dev/null || true
            rm -f "$temp_file"
            return 0
        fi
    fi
    
    echo -e "    ❌ 上传接口返回状态码: $http_code" >&2
    [ -s "$temp_file" ] && echo -e "    🔎 返回内容: $(cat "$temp_file")" >&2
    rm -f "$upload_local_path"
    rmdir "$tmp_dir" 2>/dev/null || true
    rm -f "$temp_file"
    return 1
}

# 处理 markdown 文件中的图片
process_images_in_file() {
    local file_path="$1"
    local processed_file="${file_path}.processed"
    
    # 复制原文件到处理文件
    cp "$file_path" "$processed_file"
    
    # 查找所有本地图片路径(保留空格与特殊字符,不做单词拆分)
    local images=()
    while IFS= read -r img; do
        # 仅保留括号内路径部分
        img=${img#*(}
        img=${img%)}
        # 过滤 http/https
        if echo "$img" | grep -qE '^https?://'; then
            continue
        fi
        images+=("$img")
    done < <(grep -o '!\[.*\]([^)]*)' "$processed_file")
    
    if [ ${#images[@]} -eq 0 ]; then
        return 0  # 没有图片需要处理
    fi
    
    local has_failed=false
    
    for image_path in "${images[@]}"; do
        # 处理相对路径
        local full_path="$image_path"
        if [[ "$image_path" != /* ]]; then
            local base_dir=$(dirname "$file_path")
            full_path="$base_dir/$image_path"
        fi
        
        if [ ! -f "$full_path" ]; then
            echo -e "    ❌ 图片文件不存在: $image_path"
            has_failed=true
            continue
        fi
        
        echo -e "    📤 上传图片: $(basename "$image_path")"
        echo -e "    📍 原始路径: $image_path"
        
        # 上传图片
        local quail_url=$(upload_image_to_quail "$full_path")
        
        if [ $? -eq 0 ] && [ -n "$quail_url" ]; then
            echo -e "    ✅ 上传成功"
            echo -e "    🔗 新路径: $quail_url"
            echo -e "    ↔️ 映射: $image_path -> $quail_url"
        echo -e "    ✏️ 正在替换路径..."
        # 替换文件中的路径(对特殊字符进行转义)
        local escaped_path
        escaped_path=$(printf '%s' "$image_path" | sed -e 's/[\\/&|.\[\]*^$+?{}()]/\\&/g')
        sed -i.bak "s|]($escaped_path)|]($quail_url)|g" "$processed_file"
            rm -f "${processed_file}.bak"
            echo -e "    ✅ 路径已替换"
        else
            echo -e "    ❌ 上传失败: $image_path"
            has_failed=true
        fi
    done
    
    if [ "$has_failed" = "true" ]; then
        return 1
    fi
    
    # 显示处理总结
    echo -e "    📊 图片处理总结:"
    echo -e "      🖼️ 处理图片: ${#images[@]} 个"
    echo -e "      ✅ 全部成功: ${#images[@]} 个"
    echo -e "      📄 文件已修改: $(basename "$processed_file")"
    
    return 0
}

# 处理每个文件
for i in "${!md_files[@]}"; do
    f="${md_files[$i]}"
    current=$((i + 1))
    # 计算相对路径
    relative_path="${f#$TARGET_DIR/}"
    
    echo -e "${BLUE}[$current/$total_files]${NC} 处理文件: ${YELLOW}$relative_path${NC}"
    
    # 提取 frontmatter 里的 slug 字段
    slug=$(grep -E '^slug:' "$f" | head -n1 | awk '{print $2}' | tr -d '"')
    
    if [ -z "$slug" ]; then
        echo -e "  ${YELLOW}⚠️ 跳过: 文件 $relative_path 没有找到 slug 字段${NC}"
        ((skip_count++))
        echo ""
        continue
    fi
    
    echo -e "  📝 Slug: $slug"
    
    # 始终尝试处理本地图片(不修改原文件,仅对 .processed 生效)
    echo -e "  🖼️ 检测并处理本地图片..."
    if process_images_in_file "$f"; then
        if [ -f "${f}.processed" ]; then
            echo -e "  ${GREEN}✅ 图片处理完成${NC}"
            target_file="${f}.processed"
        else
            echo -e "  ℹ️ 未发现本地图片"
            target_file="$f"
        fi
    else
        echo -e "  ${RED}❌ 图片处理失败${NC}"
        echo -e "  ${YELLOW}⚠️ 跳过此文件,避免发布包含本地图片路径的文章${NC}"
        ((error_count++))
        echo ""
        continue
    fi
    
    # 执行 upsert 操作(直接发布)
    echo -e "  📤 正在上传并发布文章..."
    if quail-cli post upsert "$target_file" -l "$LIST_SLUG" --publish > /dev/null 2>&1; then
        echo -e "  ${GREEN}✅ 上传并发布成功${NC}"
        
        # 清理临时文件
        if [ -f "${f}.processed" ]; then
            rm -f "${f}.processed"
        fi
        
        ((success_count++))
    else
        echo -e "  ${RED}❌ 上传或发布失败${NC}"
        echo -e "  ${YELLOW}💡 尝试手动发布: quail-cli post publish -l $LIST_SLUG -p $slug${NC}"
        
        # 清理临时文件
        if [ -f "${f}.processed" ]; then
            rm -f "${f}.processed"
        fi
        
        ((error_count++))
    fi
    echo ""
done

# 显示最终统计
echo -e "${BLUE}📊 处理完成!统计信息:${NC}"
echo -e "  ${GREEN}✅ 成功: $success_count${NC}"
echo -e "  ${RED}❌ 失败: $error_count${NC}"
echo -e "  ${YELLOW}⚠️ 跳过: $skip_count${NC}"
echo -e "  📁 总计: $total_files"

if [ $error_count -eq 0 ]; then
    echo -e "\n${GREEN}🎉 所有文章都已成功处理!${NC}"
else
    echo -e "\n${YELLOW}⚠️ 有 $error_count 个文件处理失败,请检查错误信息${NC}"
    exit 1
fi