Files
ai-proj-helper/skills-req/req-plugin/notify.sh
John Qiu 712063071c refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4),
skills-integration(8), skills-biz(4), skills-workflow(7)

generate-marketplace.py 改为自动扫描所有 skills-* 目录。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:31:58 +10:30

456 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <REQ-ID> -t <type> [-e <emails>] [-m <message>] [-a <attachment>] [-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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
# 转换标题
escaped=$(echo "$escaped" | sed -E 's/^### (.*)$/<h3>\1<\/h3>/g')
escaped=$(echo "$escaped" | sed -E 's/^## (.*)$/<h2>\1<\/h2>/g')
escaped=$(echo "$escaped" | sed -E 's/^# (.*)$/<h1>\1<\/h1>/g')
# 转换粗体
escaped=$(echo "$escaped" | sed -E 's/\*\*([^*]+)\*\*/<strong>\1<\/strong>/g')
# 转换代码块
escaped=$(echo "$escaped" | sed -E 's/`([^`]+)`/<code>\1<\/code>/g')
# 转换列表项
escaped=$(echo "$escaped" | sed -E 's/^- (.*)$/<li>\1<\/li>/g')
escaped=$(echo "$escaped" | sed -E 's/^\* (.*)$/<li>\1<\/li>/g')
# 转换复选框
escaped=$(echo "$escaped" | sed 's/\[x\]/✅/g; s/\[ \]/⬜/g')
# 转换表格分隔线(简化处理)
escaped=$(echo "$escaped" | sed '/^|[-|]*$/d')
# 转换表格行
escaped=$(echo "$escaped" | sed -E 's/^\| (.+) \|$/<tr><td>\1<\/td><\/tr>/g')
escaped=$(echo "$escaped" | sed 's/ \| /<\/td><td>/g')
# 转换换行
escaped=$(echo "$escaped" | sed 's/$/<br>/g')
# 转换水平线
escaped=$(echo "$escaped" | sed 's/^---$/<hr>/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 <<EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px 10px 0 0; }
.header h1 { margin: 0 0 10px 0; font-size: 24px; }
.header .req-id { font-size: 18px; opacity: 0.9; }
.summary { background: #f8f9fa; padding: 20px 30px; border: 1px solid #e9ecef; }
.info-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
.info-table td { padding: 10px 15px; border-bottom: 1px solid #dee2e6; }
.info-table td:first-child { font-weight: 600; color: #495057; width: 100px; }
.status-badge { display: inline-block; padding: 4px 12px; border-radius: 15px; font-size: 13px; font-weight: 600; }
.status-success { background: #d4edda; color: #155724; }
.doc-section { background: #fff; border: 1px solid #e9ecef; border-top: none; padding: 30px; }
.doc-title { background: #343a40; color: white; padding: 15px 30px; margin: 0; font-size: 16px; }
.doc-content { font-size: 14px; line-height: 1.8; }
.doc-content h1 { font-size: 22px; color: #2c3e50; border-bottom: 2px solid #667eea; padding-bottom: 10px; margin-top: 25px; }
.doc-content h2 { font-size: 18px; color: #34495e; margin-top: 20px; }
.doc-content h3 { font-size: 16px; color: #495057; margin-top: 15px; }
.doc-content code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'SF Mono', Monaco, monospace; font-size: 13px; }
.doc-content table { border-collapse: collapse; width: 100%; margin: 15px 0; }
.doc-content td, .doc-content th { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
.doc-content tr:nth-child(even) { background: #f9f9f9; }
.doc-content li { margin: 5px 0; }
.doc-content hr { border: none; border-top: 1px solid #eee; margin: 20px 0; }
.footer { background: #f1f3f4; padding: 15px 30px; border-radius: 0 0 10px 10px; font-size: 12px; color: #666; text-align: center; }
</style>
</head>
<body>
<div class="header">
<h1>📋 需求${type_desc}通知</h1>
<div class="req-id">${req_id}</div>
</div>
<div class="summary">
<table class="info-table">
<tr><td>需求编号</td><td><strong>${req_id}</strong></td></tr>
<tr><td>通知类型</td><td>${type_desc}</td></tr>
<tr><td>完成时间</td><td>${timestamp}</td></tr>
<tr><td>状态</td><td><span class="status-badge status-success">✓ 已完成</span></td></tr>
</table>
$(if [ -n "$extra_message" ]; then echo "<p><strong>📝 说明:</strong>${extra_message}</p>"; fi)
</div>
$(if [ -n "$doc_html" ]; then
echo "<div class=\"doc-title\">📄 完整文档内容</div>"
echo "<div class=\"doc-section\"><div class=\"doc-content\">${doc_html}</div></div>"
fi)
<div class="footer">
此邮件由 <strong>REQ 需求管理系统</strong> 自动发送 | 文档同步自思源笔记:需求管理${doc_path}<br>
完整 Markdown 文档已作为附件发送
</div>
</body>
</html>
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