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:
2026-03-14 11:31:58 +10:30
parent ea266e9cce
commit 712063071c
170 changed files with 341 additions and 346 deletions

455
skills-req/req-plugin/notify.sh Executable file
View 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/&/\&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