#!/bin/bash # REQ 需求发布通知脚本 v3.2.0 # 用于在需求发布完成后发送邮件通知到邮件组 # 功能:HTML正文(含文档内容) + Markdown附件 # ==================== 配置 ==================== # 默认邮件组 DEFAULT_EMAIL_GROUP=( "qiudl@zhiyuncai.com" "fuxing@zhiyuncai.com" "haiqing@zhiyuncai.com" "wuweier@zhiyuncai.com" ) # 思源笔记配置 SIYUAN_API_URL="${SIYUAN_URL:-http://100.118.62.18:6806}" SIYUAN_TOKEN="${SIYUAN_TOKEN:-nfnycjb1g8vbexb2}" SIYUAN_NOTEBOOK_NAME="需求管理" # 发件人 FROM_NAME="REQ 需求管理系统" # 临时目录 TMP_DIR="/tmp/req-notify-$$" # ==================== 函数定义 ==================== # 使用方法 usage() { echo "Usage: $0 -r -t [-e ] [-m ] [-a ] [-n]" echo "" echo "Options:" echo " -r REQ-ID 需求编号 (如 REQ-2026-0007)" echo " -t type 通知类型: prd|dev|test|deploy|archive" echo " -e emails 收件人邮箱,多个用逗号分隔 (覆盖默认邮件组)" echo " -a attachment 附件文件路径 (可选,覆盖自动获取)" echo " -n 不附加文档 (仅发送通知)" echo " -m message 附加消息" echo " -h 显示帮助" echo "" echo "默认邮件组 (${#DEFAULT_EMAIL_GROUP[@]} 人):" for email in "${DEFAULT_EMAIL_GROUP[@]}"; do echo " - $email" done echo "" echo "通知类型与文档映射:" echo " prd → 01-PRD" echo " dev → 02-开发设计" echo " test → 03-测试报告" echo " deploy → 04-发布记录" echo " archive → 05-生命周期总结" exit 1 } # 清理临时文件 cleanup() { rm -rf "$TMP_DIR" 2>/dev/null } trap cleanup EXIT # 从思源笔记获取文档内容 fetch_siyuan_doc() { local doc_path="$1" local output_file="$2" echo "正在从思源笔记获取文档: $doc_path" # 1. 先查询文档 ID local query="SELECT id, content FROM blocks WHERE type='d' AND hpath LIKE '%${doc_path}%' LIMIT 1" local response=$(curl -s -X POST "${SIYUAN_API_URL}/api/query/sql" \ -H "Authorization: Token ${SIYUAN_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"stmt\": \"${query}\"}") local doc_id=$(echo "$response" | jq -r '.data[0].id // empty') if [ -z "$doc_id" ]; then echo "警告: 未找到文档 $doc_path" return 1 fi # 2. 导出文档为 Markdown local export_response=$(curl -s -X POST "${SIYUAN_API_URL}/api/export/exportMdContent" \ -H "Authorization: Token ${SIYUAN_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"id\": \"${doc_id}\"}") local content=$(echo "$export_response" | jq -r '.data.content // empty') if [ -z "$content" ]; then echo "警告: 文档内容为空" return 1 fi # 3. 保存到文件 echo "$content" > "$output_file" echo "文档已保存: $output_file ($(wc -c < "$output_file") 字节)" return 0 } # 根据通知类型获取文档路径 get_doc_path() { local req_id="$1" local notify_type="$2" case $notify_type in prd) echo "/${req_id}/01-PRD" ;; dev) echo "/${req_id}/02-开发设计" ;; test) echo "/${req_id}/03-测试报告" ;; deploy) echo "/${req_id}/04-发布记录" ;; archive) echo "/${req_id}/05-生命周期总结" ;; *) echo "" ;; esac } # 获取文档文件名 get_doc_filename() { local req_id="$1" local notify_type="$2" case $notify_type in prd) echo "${req_id}_01-PRD.md" ;; dev) echo "${req_id}_02-开发设计.md" ;; test) echo "${req_id}_03-测试报告.md" ;; deploy) echo "${req_id}_04-发布记录.md" ;; archive) echo "${req_id}_05-生命周期总结.md" ;; *) echo "${req_id}_文档.md" ;; esac } # 获取通知标题 get_subject() { local req_id="$1" local notify_type="$2" case $notify_type in prd) echo "[REQ-PRD] ${req_id} PRD文档已完成" ;; dev) echo "[REQ-开发] ${req_id} 开发完成" ;; test) echo "[REQ-测试] ${req_id} 测试完成" ;; deploy) echo "[REQ-发布] ${req_id} 已成功发布" ;; archive) echo "[REQ-归档] ${req_id} 已归档" ;; *) echo "[REQ] ${req_id} 通知" ;; esac } # 获取通知类型描述 get_type_desc() { local notify_type="$1" case $notify_type in prd) echo "PRD文档完成" ;; dev) echo "开发完成" ;; test) echo "测试完成" ;; deploy) echo "发布完成" ;; archive) echo "需求归档" ;; *) echo "状态更新" ;; esac } # 将 Markdown 转换为 HTML (简单转换) markdown_to_html() { local md_content="$1" # 转义 HTML 特殊字符 local escaped=$(echo "$md_content" | sed 's/&/\&/g; s//\>/g') # 转换标题 escaped=$(echo "$escaped" | sed -E 's/^### (.*)$/

\1<\/h3>/g') escaped=$(echo "$escaped" | sed -E 's/^## (.*)$/

\1<\/h2>/g') escaped=$(echo "$escaped" | sed -E 's/^# (.*)$/

\1<\/h1>/g') # 转换粗体 escaped=$(echo "$escaped" | sed -E 's/\*\*([^*]+)\*\*/\1<\/strong>/g') # 转换代码块 escaped=$(echo "$escaped" | sed -E 's/`([^`]+)`/\1<\/code>/g') # 转换列表项 escaped=$(echo "$escaped" | sed -E 's/^- (.*)$/
  • \1<\/li>/g') escaped=$(echo "$escaped" | sed -E 's/^\* (.*)$/
  • \1<\/li>/g') # 转换复选框 escaped=$(echo "$escaped" | sed 's/\[x\]/✅/g; s/\[ \]/⬜/g') # 转换表格分隔线(简化处理) escaped=$(echo "$escaped" | sed '/^|[-|]*$/d') # 转换表格行 escaped=$(echo "$escaped" | sed -E 's/^\| (.+) \|$/\1<\/td><\/tr>/g') escaped=$(echo "$escaped" | sed 's/ \| /<\/td>/g') # 转换换行 escaped=$(echo "$escaped" | sed 's/$/
    /g') # 转换水平线 escaped=$(echo "$escaped" | sed 's/^---$/
    /g') echo "$escaped" } # 生成邮件正文 (HTML格式,包含文档内容) generate_body_html() { local req_id="$1" local notify_type="$2" local timestamp="$3" local extra_message="$4" local doc_path="$5" local doc_content="$6" local type_desc=$(get_type_desc "$notify_type") # 转换文档内容为 HTML local doc_html="" if [ -n "$doc_content" ]; then doc_html=$(markdown_to_html "$doc_content") fi cat <

    📋 需求${type_desc}通知

    ${req_id}
    需求编号${req_id}
    通知类型${type_desc}
    完成时间${timestamp}
    状态✓ 已完成
    $(if [ -n "$extra_message" ]; then echo "

    📝 说明:${extra_message}

    "; fi)
    $(if [ -n "$doc_html" ]; then echo "
    📄 完整文档内容
    " echo "
    ${doc_html}
    " fi) EOF } # 发送带附件的邮件 (MIME格式) send_email_with_attachment() { local recipient="$1" local subject="$2" local body_html="$3" local attachment_file="$4" local attachment_name="$5" local boundary="----=_Part_$(date +%s)_$RANDOM" # 读取附件内容并 base64 编码 local attachment_content="" if [ -f "$attachment_file" ]; then attachment_content=$(base64 < "$attachment_file") fi # 构建 MIME 邮件 { echo "Subject: =?UTF-8?B?$(echo -n "$subject" | base64)?=" echo "To: $recipient" echo "MIME-Version: 1.0" echo "Content-Type: multipart/mixed; boundary=\"$boundary\"" echo "" echo "--$boundary" echo "Content-Type: text/html; charset=UTF-8" echo "Content-Transfer-Encoding: base64" echo "" echo "$body_html" | base64 # 添加附件 if [ -n "$attachment_content" ]; then echo "" echo "--$boundary" echo "Content-Type: text/markdown; charset=UTF-8; name=\"$attachment_name\"" echo "Content-Transfer-Encoding: base64" echo "Content-Disposition: attachment; filename=\"$attachment_name\"" echo "" echo "$attachment_content" fi echo "" echo "--$boundary--" } | msmtp "$recipient" 2>/dev/null return $? } # ==================== 主程序 ==================== # 解析参数 REQ_ID="" NOTIFY_TYPE="" CUSTOM_EMAILS="" CUSTOM_ATTACHMENT="" NO_ATTACHMENT=false EXTRA_MESSAGE="" while getopts "r:t:e:a:m:nh" opt; do case $opt in r) REQ_ID="$OPTARG" ;; t) NOTIFY_TYPE="$OPTARG" ;; e) CUSTOM_EMAILS="$OPTARG" ;; a) CUSTOM_ATTACHMENT="$OPTARG" ;; n) NO_ATTACHMENT=true ;; m) EXTRA_MESSAGE="$OPTARG" ;; h) usage ;; *) usage ;; esac done # 验证必需参数 if [ -z "$REQ_ID" ] || [ -z "$NOTIFY_TYPE" ]; then echo "Error: -r 和 -t 参数是必需的" usage fi # 验证通知类型 case $NOTIFY_TYPE in prd|dev|test|deploy|archive) ;; *) echo "Error: 未知的通知类型: $NOTIFY_TYPE" echo "支持的类型: prd, dev, test, deploy, archive" exit 1 ;; esac # 构建收件人列表 declare -a RECIPIENTS if [ -n "$CUSTOM_EMAILS" ]; then IFS=',' read -ra RECIPIENTS <<< "$CUSTOM_EMAILS" else RECIPIENTS=("${DEFAULT_EMAIL_GROUP[@]}") fi # 获取时间戳 TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") # 获取文档信息 DOC_PATH=$(get_doc_path "$REQ_ID" "$NOTIFY_TYPE") DOC_FILENAME=$(get_doc_filename "$REQ_ID" "$NOTIFY_TYPE") SUBJECT=$(get_subject "$REQ_ID" "$NOTIFY_TYPE") # 创建临时目录 mkdir -p "$TMP_DIR" # 准备附件和文档内容 ATTACHMENT_FILE="" DOC_CONTENT="" if [ "$NO_ATTACHMENT" = false ]; then if [ -n "$CUSTOM_ATTACHMENT" ] && [ -f "$CUSTOM_ATTACHMENT" ]; then ATTACHMENT_FILE="$CUSTOM_ATTACHMENT" DOC_CONTENT=$(cat "$ATTACHMENT_FILE") echo "使用指定附件: $ATTACHMENT_FILE" else # 从思源笔记获取文档 ATTACHMENT_FILE="${TMP_DIR}/${DOC_FILENAME}" if fetch_siyuan_doc "$DOC_PATH" "$ATTACHMENT_FILE"; then DOC_CONTENT=$(cat "$ATTACHMENT_FILE") else echo "警告: 无法获取文档,将发送不带附件的邮件" ATTACHMENT_FILE="" fi fi fi # 生成邮件正文(包含文档内容) BODY_HTML=$(generate_body_html "$REQ_ID" "$NOTIFY_TYPE" "$TIMESTAMP" "$EXTRA_MESSAGE" "$DOC_PATH" "$DOC_CONTENT") # 显示发送信息 echo "==========================================" echo "📧 发送需求通知邮件" echo "==========================================" echo "需求编号: $REQ_ID" echo "通知类型: $NOTIFY_TYPE ($(get_type_desc "$NOTIFY_TYPE"))" echo "邮件主题: $SUBJECT" echo "附件: $(if [ -n "$ATTACHMENT_FILE" ]; then echo "$DOC_FILENAME"; else echo "无"; fi)" echo "收件人列表 (${#RECIPIENTS[@]} 人):" for email in "${RECIPIENTS[@]}"; do echo " 📬 $email" done echo "==========================================" # 发送邮件 SUCCESS_COUNT=0 FAIL_COUNT=0 for RECIPIENT in "${RECIPIENTS[@]}"; do RECIPIENT=$(echo "$RECIPIENT" | tr -d ' ') if [ -z "$RECIPIENT" ]; then continue fi if send_email_with_attachment "$RECIPIENT" "$SUBJECT" "$BODY_HTML" "$ATTACHMENT_FILE" "$DOC_FILENAME"; then echo "✅ 已发送: $RECIPIENT" ((SUCCESS_COUNT++)) else echo "❌ 发送失败: $RECIPIENT" ((FAIL_COUNT++)) fi done # 显示结果 echo "==========================================" echo "📊 发送完成" echo " ✅ 成功: $SUCCESS_COUNT" echo " ❌ 失败: $FAIL_COUNT" echo " 🕐 时间: $TIMESTAMP" echo "==========================================" if [ $FAIL_COUNT -gt 0 ]; then echo "部分邮件发送失败,请检查 ~/.msmtp.log" exit 1 fi exit 0