399 lines
12 KiB
Bash
399 lines
12 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
# GitHub 任务轮询 — 迭代循环模式
|
|||
|
|
# 每次轮询只处理 checklist 中的一项,直到全部完成
|
|||
|
|
# 完成后自动交接给大龙虾(项目经理)
|
|||
|
|
|
|||
|
|
REPO="$GITHUB_REPO"
|
|||
|
|
BOT="$BOT_NAME"
|
|||
|
|
ROLE="$BOT_ROLE"
|
|||
|
|
CODE_REPO_DIR="/home/node/code-repo"
|
|||
|
|
SKILLS_REPO_DIR="/home/node/skills-repo"
|
|||
|
|
WORKSPACE="/home/node/.openclaw/workspace"
|
|||
|
|
SKILLS_DIR="/home/node/.openclaw/skills"
|
|||
|
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
|||
|
|
|
|||
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|||
|
|
echo "[$TIMESTAMP] $BOT 开始轮询任务..."
|
|||
|
|
|
|||
|
|
health=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18789/healthz 2>/dev/null)
|
|||
|
|
if [ "$health" != "200" ]; then
|
|||
|
|
echo "⚠️ Gateway 未就绪 (HTTP $health), 跳过本轮"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 同步技能仓的新技能 ──
|
|||
|
|
if [ -d "$SKILLS_REPO_DIR/.git" ]; then
|
|||
|
|
cd "$SKILLS_REPO_DIR" && git pull --rebase 2>/dev/null
|
|||
|
|
for f in "$SKILLS_REPO_DIR"/*.md; do
|
|||
|
|
[ -f "$f" ] && cp "$f" "$SKILLS_DIR/" 2>/dev/null
|
|||
|
|
done
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 1. 先找 in-progress 的任务(上次没做完的) ──
|
|||
|
|
issue_json=$(gh issue list -R "$REPO" -l "status:in-progress" -l "role:${BOT}" \
|
|||
|
|
--json number,title,body -q '.[0]' 2>/dev/null)
|
|||
|
|
|
|||
|
|
# ── 2. 没有 in-progress 就找 pending 的 ──
|
|||
|
|
if [ -z "$issue_json" ] || [ "$issue_json" = "null" ]; then
|
|||
|
|
issue_json=$(gh issue list -R "$REPO" -l "status:pending" -l "role:${BOT}" \
|
|||
|
|
--json number,title,body -q '.[0]' 2>/dev/null)
|
|||
|
|
|
|||
|
|
if [ -z "$issue_json" ] || [ "$issue_json" = "null" ]; then
|
|||
|
|
echo "📭 没有待处理任务"
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
number=$(echo "$issue_json" | jq -r '.number')
|
|||
|
|
gh issue edit "$number" -R "$REPO" \
|
|||
|
|
--remove-label "status:pending" \
|
|||
|
|
--add-label "status:in-progress" 2>/dev/null
|
|||
|
|
echo "📋 领取新任务 #$number"
|
|||
|
|
else
|
|||
|
|
echo "🔄 继续处理进行中任务"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
number=$(echo "$issue_json" | jq -r '.number')
|
|||
|
|
title=$(echo "$issue_json" | jq -r '.title')
|
|||
|
|
body=$(echo "$issue_json" | jq -r '.body')
|
|||
|
|
|
|||
|
|
echo "📋 任务 #$number: $title"
|
|||
|
|
|
|||
|
|
# ── 3. 确定当前阶段,只解析对应 section 的 checklist ──
|
|||
|
|
current_phase="unknown"
|
|||
|
|
section_keyword=""
|
|||
|
|
if echo "$body" | grep -q '\*\*phase:design\*\*'; then
|
|||
|
|
current_phase="design"
|
|||
|
|
section_keyword="需求清单"
|
|||
|
|
elif echo "$body" | grep -q '\*\*phase:coding\*\*'; then
|
|||
|
|
current_phase="coding"
|
|||
|
|
section_keyword="编码清单"
|
|||
|
|
elif echo "$body" | grep -q '\*\*phase:review\*\*'; then
|
|||
|
|
current_phase="review"
|
|||
|
|
section_keyword="测试清单"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
next_item=""
|
|||
|
|
next_item_num=0
|
|||
|
|
section_item_num=0
|
|||
|
|
total_items=0
|
|||
|
|
done_items=0
|
|||
|
|
global_count=0
|
|||
|
|
in_target_section=0
|
|||
|
|
|
|||
|
|
while IFS= read -r line; do
|
|||
|
|
if echo "$line" | grep -qi "$section_keyword"; then
|
|||
|
|
in_target_section=1
|
|||
|
|
elif echo "$line" | grep -qE '^## |^---'; then
|
|||
|
|
if [ $in_target_section -eq 1 ] && [ $total_items -gt 0 ]; then
|
|||
|
|
in_target_section=0
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if echo "$line" | grep -qE '^\- \[[ x]\] [0-9]+\.'; then
|
|||
|
|
global_count=$((global_count + 1))
|
|||
|
|
|
|||
|
|
if [ $in_target_section -eq 1 ]; then
|
|||
|
|
total_items=$((total_items + 1))
|
|||
|
|
if echo "$line" | grep -qE '^\- \[x\]'; then
|
|||
|
|
done_items=$((done_items + 1))
|
|||
|
|
elif [ -z "$next_item" ]; then
|
|||
|
|
next_item=$(echo "$line" | sed 's/^- \[ \] [0-9]*\.\s*//')
|
|||
|
|
next_item_num=$global_count
|
|||
|
|
section_item_num=$total_items
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
done <<< "$body"
|
|||
|
|
|
|||
|
|
if [ $total_items -eq 0 ]; then
|
|||
|
|
echo "⚠️ 当前阶段 ($current_phase) 没有 checklist,按整体任务处理"
|
|||
|
|
next_item="$body"
|
|||
|
|
total_items=1
|
|||
|
|
next_item_num=1
|
|||
|
|
section_item_num=1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [ -z "$next_item" ] && [ $total_items -gt 0 ]; then
|
|||
|
|
echo "✅ 当前阶段所有项目已完成 ($done_items/$total_items)"
|
|||
|
|
|
|||
|
|
phase_label="设计"
|
|||
|
|
[ "$current_phase" = "coding" ] && phase_label="编码"
|
|||
|
|
[ "$current_phase" = "review" ] && phase_label="测试"
|
|||
|
|
|
|||
|
|
echo "📌 ${phase_label}阶段全部完成,交接给大龙虾"
|
|||
|
|
|
|||
|
|
gh issue edit "$number" -R "$REPO" \
|
|||
|
|
--remove-label "role:${BOT}" \
|
|||
|
|
--add-label "role:dalongxia" 2>/dev/null
|
|||
|
|
|
|||
|
|
tmphandoff=$(mktemp /tmp/issue-handoff-XXXXXX.md)
|
|||
|
|
cat > "$tmphandoff" << HANDOFF_EOF
|
|||
|
|
🦞 **${phase_label}阶段全部完成** ($done_items/$total_items) — 交接给大龙虾
|
|||
|
|
|
|||
|
|
**执行者**: ${BOT}
|
|||
|
|
**完成阶段**: ${current_phase}
|
|||
|
|
**完成项数**: ${done_items}/${total_items}
|
|||
|
|
|
|||
|
|
请大龙虾(项目经理):
|
|||
|
|
1. 审查本阶段成果
|
|||
|
|
2. 分析整体项目进度
|
|||
|
|
3. 决定下一步安排(编写下阶段清单、指派执行者)
|
|||
|
|
HANDOFF_EOF
|
|||
|
|
gh issue comment "$number" -R "$REPO" --body-file "$tmphandoff" 2>/dev/null
|
|||
|
|
rm -f "$tmphandoff"
|
|||
|
|
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo "🎯 处理第 $section_item_num/$total_items 项: $next_item"
|
|||
|
|
|
|||
|
|
tmpstart=$(mktemp /tmp/issue-start-XXXXXX.md)
|
|||
|
|
cat > "$tmpstart" << START_EOF
|
|||
|
|
🔄 **${BOT}** 开始处理第 **$section_item_num/$total_items** 项:
|
|||
|
|
|
|||
|
|
> $next_item
|
|||
|
|
|
|||
|
|
⏰ $TIMESTAMP
|
|||
|
|
START_EOF
|
|||
|
|
gh issue comment "$number" -R "$REPO" --body-file "$tmpstart" 2>/dev/null
|
|||
|
|
rm -f "$tmpstart"
|
|||
|
|
|
|||
|
|
# ── 4. 构建 prompt 给 bot ──
|
|||
|
|
phase_instruction=""
|
|||
|
|
case "$current_phase" in
|
|||
|
|
design)
|
|||
|
|
phase_instruction="你当前处于【设计阶段】。请对以下需求进行详细的技术设计:包括数据模型、API 接口设计、技术方案、架构说明。输出要具体、可执行。"
|
|||
|
|
;;
|
|||
|
|
coding)
|
|||
|
|
phase_instruction="你当前处于【编码阶段】。请按照设计方案完成以下编码任务。代码必须完整、可运行、有注释。所有代码文件写到工作目录。"
|
|||
|
|
;;
|
|||
|
|
review)
|
|||
|
|
phase_instruction="你当前处于【测试/审查阶段】。请检查以下测试条目,验证功能是否正确,输出测试报告。如果发现问题,详细说明问题位置和修复建议。"
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
phase_instruction="请认真完成以下任务。"
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
task_prompt="你是 ${BOT},角色是 ${ROLE}。
|
|||
|
|
|
|||
|
|
## 当前任务
|
|||
|
|
项目: ${title} (Issue #${number})
|
|||
|
|
当前阶段: ${current_phase}
|
|||
|
|
当前进度: ${done_items}/${total_items} 已完成
|
|||
|
|
|
|||
|
|
## 本次要处理的项目(第 ${section_item_num} 项)
|
|||
|
|
${next_item}
|
|||
|
|
|
|||
|
|
## 工作指引
|
|||
|
|
${phase_instruction}
|
|||
|
|
|
|||
|
|
## 项目完整背景
|
|||
|
|
${body}
|
|||
|
|
|
|||
|
|
## 重要规则
|
|||
|
|
1. 你只需处理上面【第 ${section_item_num} 项】,不要做其他项
|
|||
|
|
2. 完成后给出完整的交付物
|
|||
|
|
3. 如果遇到问题无法解决,说明具体卡在哪里
|
|||
|
|
4. 如果学到了新技能,在 /home/node/.openclaw/skills/ 创建 .md 记录"
|
|||
|
|
|
|||
|
|
echo "🧠 发送任务到 AI..."
|
|||
|
|
start_time=$(date +%s)
|
|||
|
|
|
|||
|
|
result=$(openclaw agent \
|
|||
|
|
--message "$task_prompt" \
|
|||
|
|
--session-id "github-task-$number-item-$next_item_num" \
|
|||
|
|
--thinking medium \
|
|||
|
|
--timeout 300 \
|
|||
|
|
--json 2>&1) || true
|
|||
|
|
|
|||
|
|
end_time=$(date +%s)
|
|||
|
|
duration=$(( end_time - start_time ))
|
|||
|
|
|
|||
|
|
response=$(echo "$result" | jq -r '.response // empty' 2>/dev/null)
|
|||
|
|
if [ -z "$response" ]; then
|
|||
|
|
response=$(echo "$result" | jq -r '.text // empty' 2>/dev/null)
|
|||
|
|
fi
|
|||
|
|
if [ -z "$response" ]; then
|
|||
|
|
response=$(echo "$result" | jq -r '.content // empty' 2>/dev/null)
|
|||
|
|
fi
|
|||
|
|
if [ -z "$response" ]; then
|
|||
|
|
response="$result"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 5. 代码推送 ──
|
|||
|
|
commit_url=""
|
|||
|
|
if [ -d "$CODE_REPO_DIR/.git" ]; then
|
|||
|
|
task_dir="$CODE_REPO_DIR/task-${number}"
|
|||
|
|
mkdir -p "$task_dir"
|
|||
|
|
|
|||
|
|
has_code=false
|
|||
|
|
for f in "$WORKSPACE"/*.py "$WORKSPACE"/*.js "$WORKSPACE"/*.ts "$WORKSPACE"/*.html \
|
|||
|
|
"$WORKSPACE"/*.css "$WORKSPACE"/*.json "$WORKSPACE"/*.go "$WORKSPACE"/*.rs \
|
|||
|
|
"$WORKSPACE"/*.java "$WORKSPACE"/*.swift "$WORKSPACE"/*.sh "$WORKSPACE"/*.md \
|
|||
|
|
"$WORKSPACE"/*.yaml "$WORKSPACE"/*.yml "$WORKSPACE"/*.toml "$WORKSPACE"/*.sql \
|
|||
|
|
"$WORKSPACE"/*.jsx "$WORKSPACE"/*.tsx "$WORKSPACE"/*.vue "$WORKSPACE"/*.wxml \
|
|||
|
|
"$WORKSPACE"/*.wxss; do
|
|||
|
|
if [ -f "$f" ]; then
|
|||
|
|
cp "$f" "$task_dir/" 2>/dev/null
|
|||
|
|
has_code=true
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
for d in "$WORKSPACE"/*/; do
|
|||
|
|
if [ -d "$d" ] && [ "$(basename "$d")" != "." ]; then
|
|||
|
|
dname=$(basename "$d")
|
|||
|
|
case "$dname" in
|
|||
|
|
node_modules|.git|__pycache__|.openclaw|.cache|plugins) continue ;;
|
|||
|
|
esac
|
|||
|
|
cp -r "$d" "$task_dir/" 2>/dev/null
|
|||
|
|
has_code=true
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
if [ "$has_code" = true ]; then
|
|||
|
|
cd "$CODE_REPO_DIR"
|
|||
|
|
git add -A 2>/dev/null
|
|||
|
|
commit_msg="task-${number} item-${next_item_num}: ${next_item}
|
|||
|
|
|
|||
|
|
执行者: ${BOT} (${ROLE})
|
|||
|
|
进度: ${next_item_num}/${total_items}
|
|||
|
|
耗时: ${duration}s"
|
|||
|
|
git commit -m "$commit_msg" 2>/dev/null
|
|||
|
|
git push origin main 2>/dev/null && echo "📦 代码已推送"
|
|||
|
|
commit_hash=$(git rev-parse --short HEAD 2>/dev/null)
|
|||
|
|
commit_url="https://github.com/${GITHUB_OWNER}/${CODE_REPO}/commit/${commit_hash}"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 6. 技能推送 ──
|
|||
|
|
skills_url=""
|
|||
|
|
if [ -d "$SKILLS_REPO_DIR/.git" ]; then
|
|||
|
|
has_new_skills=false
|
|||
|
|
for f in "$SKILLS_DIR"/*.md; do
|
|||
|
|
if [ -f "$f" ]; then
|
|||
|
|
fname=$(basename "$f")
|
|||
|
|
if [ "$fname" = "github-task-worker.md" ]; then continue; fi
|
|||
|
|
if [ ! -f "$SKILLS_REPO_DIR/$fname" ] || ! diff -q "$f" "$SKILLS_REPO_DIR/$fname" >/dev/null 2>&1; then
|
|||
|
|
cp "$f" "$SKILLS_REPO_DIR/" 2>/dev/null
|
|||
|
|
has_new_skills=true
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
if [ "$has_new_skills" = true ]; then
|
|||
|
|
cd "$SKILLS_REPO_DIR"
|
|||
|
|
git add -A 2>/dev/null
|
|||
|
|
git commit -m "skill: 新增/更新技能 (task-${number} item-${next_item_num})" 2>/dev/null
|
|||
|
|
git push origin main 2>/dev/null && echo "🧠 技能已推送"
|
|||
|
|
skill_hash=$(git rev-parse --short HEAD 2>/dev/null)
|
|||
|
|
skills_url="https://github.com/${GITHUB_OWNER}/${SKILLS_REPO}/commit/${skill_hash}"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 7. 更新 Issue body:勾选已完成项 + 更新当前阶段进度 ──
|
|||
|
|
new_done=$((done_items + 1))
|
|||
|
|
|
|||
|
|
updated_body=$(echo "$body" | awk -v item_num="$next_item_num" '
|
|||
|
|
BEGIN { count = 0 }
|
|||
|
|
/^- \[[ x]\] [0-9]+\./ {
|
|||
|
|
count++
|
|||
|
|
if (count == item_num) {
|
|||
|
|
sub(/^- \[ \]/, "- [x]")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
{ print }
|
|||
|
|
')
|
|||
|
|
|
|||
|
|
if [ "$current_phase" = "design" ]; then
|
|||
|
|
updated_body=$(echo "$updated_body" | sed "s/设计: [0-9]*\/[0-9]*/设计: ${new_done}\/${total_items}/")
|
|||
|
|
elif [ "$current_phase" = "coding" ]; then
|
|||
|
|
updated_body=$(echo "$updated_body" | sed "s/编码: [0-9]*\/[0-9]*/编码: ${new_done}\/${total_items}/")
|
|||
|
|
elif [ "$current_phase" = "review" ]; then
|
|||
|
|
updated_body=$(echo "$updated_body" | sed "s/测试: [0-9]*\/[0-9]*/测试: ${new_done}\/${total_items}/")
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
tmpbody=$(mktemp /tmp/issue-body-XXXXXX.md)
|
|||
|
|
printf '%s' "$updated_body" > "$tmpbody"
|
|||
|
|
gh issue edit "$number" -R "$REPO" --body-file "$tmpbody" 2>/dev/null
|
|||
|
|
edit_rc=$?
|
|||
|
|
rm -f "$tmpbody"
|
|||
|
|
if [ $edit_rc -ne 0 ]; then
|
|||
|
|
echo "⚠️ Issue body 更新失败 (rc=$edit_rc)"
|
|||
|
|
fi
|
|||
|
|
echo "📝 已勾选第 $section_item_num 项,进度: $new_done/$total_items"
|
|||
|
|
|
|||
|
|
# ── 8. 评论交付结果 ──
|
|||
|
|
max_len=60000
|
|||
|
|
if [ ${#response} -gt $max_len ]; then
|
|||
|
|
response="${response:0:$max_len}
|
|||
|
|
|
|||
|
|
...(输出过长,已截断)"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
finish_time=$(date '+%Y-%m-%d %H:%M:%S')
|
|||
|
|
|
|||
|
|
extra_section=""
|
|||
|
|
if [ -n "$commit_url" ]; then
|
|||
|
|
extra_section="
|
|||
|
|
**📦 代码已提交**: [查看 commit](${commit_url})"
|
|||
|
|
fi
|
|||
|
|
if [ -n "$skills_url" ]; then
|
|||
|
|
extra_section="${extra_section}
|
|||
|
|
**🧠 技能已更新**: [查看 commit](${skills_url})"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
tmpcomment=$(mktemp /tmp/issue-comment-XXXXXX.md)
|
|||
|
|
cat > "$tmpcomment" << COMMENT_EOF
|
|||
|
|
## ✅ 第 ${section_item_num}/${total_items} 项完成
|
|||
|
|
|
|||
|
|
**执行者**: ${BOT} (${ROLE})
|
|||
|
|
**完成项**: ${next_item}
|
|||
|
|
**进度**: ${new_done}/${total_items}
|
|||
|
|
**耗时**: ${duration} 秒
|
|||
|
|
**完成时间**: ${finish_time}${extra_section}
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
${response}
|
|||
|
|
COMMENT_EOF
|
|||
|
|
gh issue comment "$number" -R "$REPO" --body-file "$tmpcomment" 2>/dev/null
|
|||
|
|
comment_rc=$?
|
|||
|
|
rm -f "$tmpcomment"
|
|||
|
|
if [ $comment_rc -ne 0 ]; then
|
|||
|
|
echo "⚠️ 完成评论发布失败 (rc=$comment_rc)"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 9. 判断是否全部完成 → 交接给大龙虾 ──
|
|||
|
|
if [ $new_done -ge $total_items ]; then
|
|||
|
|
echo "🎉 当前阶段所有项目已完成!交接给大龙虾"
|
|||
|
|
|
|||
|
|
phase_label="设计"
|
|||
|
|
[ "$current_phase" = "coding" ] && phase_label="编码"
|
|||
|
|
[ "$current_phase" = "review" ] && phase_label="测试"
|
|||
|
|
|
|||
|
|
gh issue edit "$number" -R "$REPO" \
|
|||
|
|
--remove-label "role:${BOT}" \
|
|||
|
|
--add-label "role:dalongxia" 2>/dev/null
|
|||
|
|
|
|||
|
|
tmphandoff=$(mktemp /tmp/issue-handoff-XXXXXX.md)
|
|||
|
|
cat > "$tmphandoff" << HANDOFF_EOF
|
|||
|
|
🦞 **${phase_label}阶段全部完成** ($total_items/$total_items) — 交接给大龙虾
|
|||
|
|
|
|||
|
|
**执行者**: ${BOT}
|
|||
|
|
**完成阶段**: ${current_phase}
|
|||
|
|
**完成项数**: ${total_items}/${total_items}
|
|||
|
|
|
|||
|
|
请大龙虾(项目经理):
|
|||
|
|
1. 审查本阶段成果
|
|||
|
|
2. 分析整体项目进度
|
|||
|
|
3. 决定下一步安排(编写下阶段清单、指派执行者)
|
|||
|
|
HANDOFF_EOF
|
|||
|
|
gh issue comment "$number" -R "$REPO" --body-file "$tmphandoff" 2>/dev/null
|
|||
|
|
rm -f "$tmphandoff"
|
|||
|
|
else
|
|||
|
|
echo "📌 还剩 $((total_items - new_done)) 项未完成,等待下次轮询"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo "✅ 第 $section_item_num/$total_items 项处理完成 (耗时 ${duration}s)"
|