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>
This commit is contained in:
455
skills-req/req-plugin/notify.sh
Executable file
455
skills-req/req-plugin/notify.sh
Executable file
@@ -0,0 +1,455 @@
|
||||
#!/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/&/\&/g; s/</\</g; s/>/\>/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
|
||||
Reference in New Issue
Block a user