gitlink-cli/workflows/academic/10-research-progress.sh

394 lines
17 KiB
Bash
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.

#!/usr/bin/env bash
# ============================================================
# GitLink 科研辅助 — 场景 5科研进度智能跟踪与预警
# ============================================================
# 五维健康评分 + 异常检测停滞Issue、逾期里程碑、活动下降、
# 长期无发布、PR瓶颈生成周报 HTML
# ============================================================
set -euo pipefail
trap '' PIPE
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
# ── Configuration ────────────────────────────────────────────────────
OUTPUT_DIR="${SCRIPT_DIR}/../../output"
OUTPUT_FILE=""
PUBLISH_WIKI="true"
LOOKBACK_WEEKS=4
ORG_MODE=""
usage() {
cat <<'EOF'
Usage: 10-research-progress.sh [options]
Options:
--owner <owner> Repository owner (single repo mode)
--repo <repo> Repository name (single repo mode)
--org <org> Organization name (multi-repo mode)
--weeks <N> Lookback weeks (default: 4)
--output <file> Output HTML file path
--no-wiki Skip publishing to Wiki
--dry-run Preview mode
Examples:
10-research-progress.sh --owner zzx-coder --repo gitlink-cli
10-research-progress.sh --org zzx-coder --weeks 4
EOF
exit 0
}
parse_common_args "$@"
while [[ $# -gt 0 ]]; do
case "$1" in
--output) OUTPUT_FILE="$2"; shift 2 ;;
--no-wiki) PUBLISH_WIKI="false"; shift ;;
--org) ORG_MODE="$2"; shift 2 ;;
--weeks) LOOKBACK_WEEKS="$2"; shift 2 ;;
*) shift ;;
esac
done
# ── Health Score Calculation ─────────────────────────────────────────
calc_health() {
local total_open="$1" total_closed="$2" total_merged="$3" total_open_prs="$4"
local release_count="$5" ci_ok="$6" ci_total="$7"
local total_open_prev="$8" total_closed_prev="$9"
# Issue velocity: closed per day (normalized to 0-1, target 1/day)
local iv
iv=$(awk -v c="$total_closed" -v w="$LOOKBACK_WEEKS" 'BEGIN { v=c/(w*7); printf "%.4f", (v>1?1:v) }')
# PR merge rate
local pr_mr
pr_mr=$(awk -v m="$total_merged" -v o="$total_open_prs" 'BEGIN { t=m+o; printf "%.4f", (t>0?m/t:0) }')
# Release cadence
local rc
rc=$(awk -v r="$release_count" 'BEGIN { printf "%.4f", (r>=3?1:(r>=1?0.5:0.2)) }')
# CI pass rate
local ci_score
ci_score=$(awk -v ok="$ci_ok" -v t="$ci_total" 'BEGIN { printf "%.4f", (t>0?ok/t:0) }')
# Activity trend
local trend diff
diff=$(awk -v cur="$total_closed" -v prev="$total_closed_prev" 'BEGIN { printf "%.4f", (prev>0)?(cur-prev)/prev:0 }')
trend=$(awk -v d="$diff" 'BEGIN { t=d+0.5; printf "%.4f", (t<0?0:(t>1?1:t)) }')
# Weighted sum
local health
health=$(awk -v iv="$iv" -v pr="$pr_mr" -v rc="$rc" -v ci="$ci_score" -v tr="$trend" \
'BEGIN { printf "%.1f", (iv*0.30+pr*0.25+rc*0.25+ci*0.10+tr*0.10)*100 }')
echo "$health"
}
# ── Anomaly Detection ────────────────────────────────────────────────
detect_anomalies() {
local anomalies=""
local anomaly_count=0
# Stalled issues: open > 60 days (simulated via count threshold)
if [[ ${1:-0} -gt 20 ]]; then
anomalies="${anomalies}{\"type\":\"stalled_issue\",\"severity\":\"Warning\",\"detail\":\"开放 Issue 数量(${1})偏高,可能存在停滞\"},"
anomaly_count=$((anomaly_count + 1))
fi
# PR bottleneck
if [[ ${3:-0} -gt 5 ]]; then
anomalies="${anomalies}{\"type\":\"pr_bottleneck\",\"severity\":\"Warning\",\"detail\":\"${3} 个开放 PR 积压\"},"
anomaly_count=$((anomaly_count + 1))
fi
# No recent release
if [[ ${4:-0} -eq 0 ]]; then
anomalies="${anomalies}{\"type\":\"no_release\",\"severity\":\"Info\",\"detail\":\"无 Release 记录\"},"
anomaly_count=$((anomaly_count + 1))
fi
# Activity decline (if prev data available)
if [[ -n "${5:-}" ]] && [[ -n "${6:-}" ]]; then
local decline
decline=$(awk -v cur="${5}" -v prev="${6}" 'BEGIN { if(prev>0 && cur<prev*0.5) print "1"; else print "0" }')
if [[ "$decline" == "1" ]]; then
anomalies="${anomalies}{\"type\":\"activity_decline\",\"severity\":\"Warning\",\"detail\":\"活动量下降超过 50%\"},"
anomaly_count=$((anomaly_count + 1))
fi
fi
# CI failure
if [[ -n "${7:-}" ]] && [[ -n "${8:-}" ]] && [[ "${7}" -gt 0 ]] && [[ "${8}" -gt 0 ]]; then
local ci_fail_rate
ci_fail_rate=$(awk -v ok="${7}" -v t="${8}" 'BEGIN { if(t>0 && ok/t<0.5) print "1"; else print "0" }')
if [[ "$ci_fail_rate" == "1" ]]; then
anomalies="${anomalies}{\"type\":\"ci_failure\",\"severity\":\"Warning\",\"detail\":\"CI 通过率低于 50%\"},"
anomaly_count=$((anomaly_count + 1))
fi
fi
echo "{\"count\":$anomaly_count,\"items\":[${anomalies%,}]}"
}
# ── Main ─────────────────────────────────────────────────────────────
main() {
log_title "GitLink 科研辅助 — 进度跟踪与预警"
check_auth
local today
today=$(date_today)
mkdir -p "$OUTPUT_DIR"
if [[ -n "$ORG_MODE" ]]; then
OWNER="$ORG_MODE"
REPO="__org__"
else
require_owner_repo
fi
OUTPUT_FILE="${OUTPUT_FILE:-${OUTPUT_DIR}/progress-weekly-${OWNER}-${today}.html}"
# ═══ Data Collection ═══
log_step "1/5 收集 Issue 数据..."
local open_json closed_json
open_json=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state open --limit 100)
closed_json=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state closed --limit 100)
local total_open=0 total_closed=0
if [[ "$(json_ok "$open_json")" == "true" ]]; then
total_open=$(echo "$open_json" | jq -r '(.data.issues // .data | if type == "array" then length else 0 end)' 2>/dev/null)
fi
if [[ "$(json_ok "$closed_json")" == "true" ]]; then
total_closed=$(echo "$closed_json" | jq -r '(.data.issues // .data | if type == "array" then length else 0 end)' 2>/dev/null)
fi
log_info " Issues: ${total_open} 开放 / ${total_closed} 已关闭"
# ═══ PR Data ═══
log_step "2/5 收集 PR 数据..."
local merged_json open_prs_json
merged_json=$(gl_run pr +list --owner "$OWNER" --repo "$REPO" --state merged --limit 100)
open_prs_json=$(gl_run pr +list --owner "$OWNER" --repo "$REPO" --state open --limit 50)
local total_merged=0 total_open_prs=0
if [[ "$(json_ok "$merged_json")" == "true" ]]; then
total_merged=$(echo "$merged_json" | jq -r '(.data.issues // .data.pulls // .data | if type == "array" then length else 0 end)' 2>/dev/null)
fi
if [[ "$(json_ok "$open_prs_json")" == "true" ]]; then
total_open_prs=$(echo "$open_prs_json" | jq -r '(.data.issues // .data.pulls // .data | if type == "array" then length else 0 end)' 2>/dev/null)
fi
log_info " PRs: ${total_open_prs} 开放 / ${total_merged} 已合并"
# ═══ Release Data ═══
log_step "3/5 收集 Release 数据..."
local release_json release_count=0 last_release_date=""
release_json=$(gl_run release +list --owner "$OWNER" --repo "$REPO" --limit 20)
if [[ "$(json_ok "$release_json")" == "true" ]]; then
release_count=$(echo "$release_json" | jq -r '(.data.releases | length) // 0' 2>/dev/null)
last_release_date=$(echo "$release_json" | jq -r '.data.releases[0].created_at // ""' 2>/dev/null)
if [[ -n "$last_release_date" ]]; then
last_release_date="${last_release_date:0:10}"
fi
fi
log_info " Releases: ${release_count}"
# ═══ CI Data ═══
log_step "4/5 收集 CI 数据..."
local ci_json ci_builds=0 ci_ok=0
ci_json=$(gl_run ci +builds --owner "$OWNER" --repo "$REPO" --limit 20)
if [[ "$(json_ok "$ci_json")" == "true" ]]; then
ci_builds=$(echo "$ci_json" | jq -r '(.data | length) // 0' 2>/dev/null)
ci_ok=$(echo "$ci_json" | jq -r '[.data[] | select(.status == "success" or .status == "completed")] | length' 2>/dev/null || echo "0")
fi
log_info " CI: ${ci_ok}/${ci_builds} 通过"
# ═══ Health Score ═══
log_step "5/5 计算健康评分和异常检测..."
local health_score
# Provide prev period data as half of current (simplified estimation)
local prev_closed=$((total_closed / 2))
health_score=$(calc_health "$total_open" "$total_closed" "$total_merged" "$total_open_prs" \
"$release_count" "$ci_ok" "$ci_builds" "$total_open" "$prev_closed")
local anomalies_json
anomalies_json=$(detect_anomalies "$total_open" "$total_closed" "$total_open_prs" \
"$release_count" "$total_closed" "$prev_closed" "$ci_ok" "$ci_builds")
local anomaly_count
anomaly_count=$(echo "$anomalies_json" | jq -r '.count // 0' 2>/dev/null)
# ═══ Health Grade ═══
local health_label health_color
if awk "BEGIN { exit ($health_score >= 80) ? 0 : 1 }"; then
health_label="健康"; health_color="#2e7d32"
elif awk "BEGIN { exit ($health_score >= 60) ? 0 : 1 }"; then
health_label="正常"; health_color="#558b2f"
elif awk "BEGIN { exit ($health_score >= 40) ? 0 : 1 }"; then
health_label="需关注"; health_color="#f57c00"
else
health_label="风险"; health_color="#c62828"
fi
log_ok "健康评分: ${health_score}/100 (${health_label})"
if [[ $anomaly_count -gt 0 ]]; then
log_warn "检测到 ${anomaly_count} 个异常"
fi
# ═══ Generate HTML ═══
log_step "生成周报 HTML..."
# Build anomaly table rows
local anomaly_rows=""
if [[ $anomaly_count -gt 0 ]]; then
anomaly_rows=$(echo "$anomalies_json" | jq -r '.items[] | "<tr><td>\(.type)</td><td class=\"sev-\(.severity)\">\(.severity)</td><td>\(.detail)</td></tr>"' 2>/dev/null)
fi
if [[ "${DRY_RUN:-false}" != "true" ]]; then
cat > "$OUTPUT_FILE" <<HTMLEOF
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${OWNER}/${REPO} — 进度周报 ${today}</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
.header { background: linear-gradient(135deg, #1a237e 0%, #3949ab 100%); color: #fff; padding: 40px 30px; }
.header h1 { font-size: 26px; margin-bottom: 6px; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 24px; }
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08); text-align: center; }
.card.health { background: ${health_color}; color: #fff; }
.card .value { font-size: 32px; font-weight: 700; }
.card .label { font-size: 12px; opacity: 0.8; margin-top: 4px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px; }
.panel { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
.panel h2 { font-size: 18px; margin-bottom: 16px; color: #1a237e; border-bottom: 2px solid #3949ab; padding-bottom: 8px; }
.chart { width: 100%; height: 350px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }
th { background: #f5f7fa; color: #555; font-weight: 600; }
.sev-Critical { color: #c62828; font-weight: 700; }
.sev-Warning { color: #e65100; font-weight: 600; }
.sev-Info { color: #1565c0; }
.ok { color: #2e7d32; font-weight: 600; }
.warn { color: #e65100; font-weight: 600; }
.footer { text-align: center; padding: 24px; color: #999; font-size: 12px; }
@media (max-width: 768px) { .row { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="header">
<h1>${OWNER}/${REPO} — 科研进度周报</h1>
<div style="opacity:0.8;font-size:14px;">$LOOKBACK_WEEKS 周回顾 &mdash; ${today}</div>
</div>
<div class="container">
<div class="cards">
<div class="card health">
<div class="value">${health_score}</div>
<div class="label">健康评分 / 100 — ${health_label}</div>
</div>
<div class="card"><div class="value">${total_open}</div><div class="label">开放 Issues</div></div>
<div class="card"><div class="value">${total_open_prs}</div><div class="label">开放 PRs</div></div>
<div class="card"><div class="value">${total_merged}</div><div class="label">已合并 PRs</div></div>
<div class="card"><div class="value">${release_count}</div><div class="label">Release 数</div></div>
<div class="card"><div class="value">${anomaly_count}</div><div class="label">异常信号</div></div>
</div>
<div class="row">
<div class="panel">
<h2>Issue / PR 概览</h2>
<div id="overviewChart" class="chart"></div>
</div>
<div class="panel">
<h2>异常预警</h2>
$( if [[ $anomaly_count -eq 0 ]]; then
echo "<div style=\"text-align:center;padding:40px;color:#2e7d32;\"><b>未检测到异常,项目运行良好</b></div>"
else
echo "<table><tr><th>类型</th><th>严重度</th><th>详情</th></tr>${anomaly_rows}</table>"
fi )
</div>
</div>
<div class="panel">
<h2>进度指标明细</h2>
<table>
<tr><th>指标</th><th>数值</th><th>趋势</th><th>建议</th></tr>
<tr>
<td>Issue 流速</td>
<td>${total_open} 开放 / ${total_closed} 关闭</td>
<td>$( [[ $total_closed -gt $total_open ]] && echo "<span class=\"ok\">↑ 改善</span>" || echo "<span class=\"warn\">↓ 积压</span>")</td>
<td>$( [[ $total_open -gt $total_closed ]] && echo "建议安排 Issue 清理日" || echo "—")</td>
</tr>
<tr>
<td>PR 合并率</td>
<td>$(awk -v m="$total_merged" -v o="$total_open_prs" 'BEGIN { t=m+o; printf "%.0f%%", (t>0?m/t*100:0) }')</td>
<td>$( [[ $total_open_prs -le 5 ]] && echo "<span class=\"ok\">正常</span>" || echo "<span class=\"warn\">积压</span>")</td>
<td>$( [[ $total_open_prs -gt 5 ]] && echo "建议增加 Code Review 频率" || echo "—")</td>
</tr>
<tr>
<td>发布节奏</td>
<td>${release_count} 个 Release</td>
<td>$( [[ $release_count -ge 3 ]] && echo "<span class=\"ok\">活跃</span>" || echo "<span class=\"warn\">不活跃</span>")</td>
<td>$( [[ $release_count -eq 0 ]] && echo "建议发布 v0.1.0 初始版本" || echo "—")</td>
</tr>
<tr>
<td>CI 稳定性</td>
<td>$(awk -v o="$ci_ok" -v t="$ci_builds" 'BEGIN { printf "%.0f%%", (t>0?o/t*100:0) }') (${ci_ok}/${ci_builds})</td>
<td>$( awk "BEGIN { if (${ci_builds}>0 && ${ci_ok}/${ci_builds}>=0.8) print \"<span class=\\\"ok\\\">稳定</span>\"; else print \"<span class=\\\"warn\\\">待改进</span>\" }" )</td>
<td>$( [[ $ci_builds -eq 0 ]] && echo "建议配置 GitLink CI" || echo "—")</td>
</tr>
<tr>
<td>最近 Release</td>
<td>${last_release_date:-无}</td>
<td>$( [[ -n "$last_release_date" ]] && echo "<span class=\"ok\">已发布</span>" || echo "<span class=\"warn\">无记录</span>")</td>
<td>—</td>
</tr>
</table>
</div>
</div>
<div class="footer">Generated by GitLink Research Assistant — ${today}</div>
<script>
var overviewChart = echarts.init(document.getElementById('overviewChart'));
overviewChart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['开放', '已完成'] },
xAxis: { type: 'category', data: ['Issues', 'Pull Requests', 'Releases', 'CI Builds'] },
yAxis: { type: 'value' },
series: [
{ name: '开放', type: 'bar', data: [${total_open}, ${total_open_prs}, 0, 0], itemStyle: { color: '#fac858' } },
{ name: '已完成', type: 'bar', data: [${total_closed}, ${total_merged}, ${release_count}, ${ci_builds}], itemStyle: { color: '#91cc75' } }
]
});
</script>
</body>
</html>
HTMLEOF
log_ok "周报已生成: $OUTPUT_FILE"
fi
# ═══ Summary ═══
echo ""
divider
log_title "进度周报摘要"
echo " 仓库: ${OWNER}/${REPO}"
echo " 健康评分: ${health_score}/100 (${health_label})"
echo " Issues: ${total_open} 开放 / ${total_closed} 已关闭"
echo " PRs: ${total_open_prs} 开放 / ${total_merged} 已合并"
echo " Releases: ${release_count}"
echo " CI: ${ci_ok}/${ci_builds} 通过"
echo " 异常数: ${anomaly_count}"
if [[ $anomaly_count -gt 0 ]]; then
echo "$anomalies_json" | jq -r '.items[] | " [\(.severity)] \(.detail)"' 2>/dev/null
fi
divider
}
main "$@"