first commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
# Claude Code 协作 supervisor(单 agent 版)
|
||||
#
|
||||
# 在容器内运行一个 Claude Code headless 进程(claude -p)完成 /blackboard/task.md 中的任务。
|
||||
# agent 的工作区为 /blackboard/workspace,产物与轮次日志保留在黑板上;
|
||||
# 任务完成(agent 用 blackboard MCP 工具创建 DONE)后,supervisor 把结果
|
||||
# 以结构化 JSON(result.json)落盘并打印一行摘要,供外部调度方直接解析。
|
||||
#
|
||||
# 与 pi-coop supervisor 的关键差异(Claude Code CLI 与 pi 不同):
|
||||
# 1. 模型接入:Claude Code 用 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN /
|
||||
# ANTHROPIC_MODEL 环境变量(不是 --provider/--model flag)。
|
||||
# 第三方 Anthropic 兼容端点(如 DeepSeek)需 ANTHROPIC_API_KEY="" 触发
|
||||
# 回退到 ANTHROPIC_AUTH_TOKEN,且 ANTHROPIC_BASE_URL 不带 /v1(SDK 自己追加)。
|
||||
# 2. 工具注册:Claude Code 用 MCP server(--mcp-config),blackboard 工具
|
||||
# 通过 blackboard-server.mjs 提供,工具命名空间为 mcp__blackboard__*。
|
||||
# 3. 输出格式:claude -p --output-format json 输出单个 JSON,含 session_id
|
||||
# 字段,直接用 jq 提取即可(无需像 pi 那样解析 NDJSON 首行)。
|
||||
# 4. session 恢复:claude -p --resume <id>,跨轮保持上下文。
|
||||
# 5. 权限:--dangerously-skip-permissions 跳过所有权限弹窗(headless 必需),
|
||||
# 且必须以非 root 用户运行(node:22-alpine 自带 node 用户,uid 1000)。
|
||||
#
|
||||
# 环境变量(均由外部调用方传入,与 pi-coop 完全一致):
|
||||
# MODEL 模型名,如 deepseek-v4-flash (必填)
|
||||
# BASE_URL Anthropic 兼容 API base-url (必填)
|
||||
# API_KEY API key(注入为 ANTHROPIC_AUTH_TOKEN) (必填)
|
||||
# PROTOCOL 协议(仅 anthropic 受 Claude Code 支持) (可选)
|
||||
# TASK 任务描述 (必填;或挂载 $BB/task.md)
|
||||
# BLACKBOARD 黑板目录 (默认 /blackboard)
|
||||
# ROUND_MAX 最大轮次 (默认 10)
|
||||
# TIMEOUT 单轮超时秒数,0=不超时 (默认 600)
|
||||
# FAIL_MODE agent 失败时:stop=立即退出(默认)| continue=继续下一轮
|
||||
set -u
|
||||
|
||||
BB="${BLACKBOARD:-/blackboard}"
|
||||
PROMPTS="${PROMPTS:-/prompts}"
|
||||
MCP_CONFIG="${MCP_CONFIG:-/mcp-servers.json}"
|
||||
ROUND_MAX="${ROUND_MAX:-10}"
|
||||
TIMEOUT="${TIMEOUT:-600}"
|
||||
FAIL_MODE="${FAIL_MODE:-stop}"
|
||||
|
||||
log() { echo "[supervisor] $*"; }
|
||||
|
||||
# emit_result 把任务结果以结构化 JSON 写入 $BB/result.json 并打印一行摘要。
|
||||
# status: solved | unsolved | error | timeout
|
||||
emit_result() {
|
||||
local status="$1" summary="$2" code="$3"
|
||||
local flag="" artifacts_json="[]" summary_json='""' flag_json='""'
|
||||
|
||||
# flag 优先从 summary 提取,其次在 workspace 产物中全量检索
|
||||
if [ -n "$summary" ]; then
|
||||
flag=$(printf '%s' "$summary" | grep -oE 'flag\{[^}]+\}' | head -1)
|
||||
fi
|
||||
if [ -z "$flag" ] && [ -d "$BB/workspace" ]; then
|
||||
flag=$(grep -rhoE 'flag\{[^}]+\}' "$BB/workspace" 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
# 产物清单:workspace 下的全部文件(最多 50 个),经 python3 转义为 JSON 数组
|
||||
if [ -d "$BB/workspace" ]; then
|
||||
artifacts_json=$(
|
||||
cd "$BB/workspace" && find . -type f 2>/dev/null | sed 's|^\./||' | head -50 |
|
||||
python3 -c 'import json,sys; print(json.dumps([l.rstrip("\n") for l in sys.stdin]))' 2>/dev/null
|
||||
)
|
||||
[ -z "$artifacts_json" ] && artifacts_json="[]"
|
||||
fi
|
||||
|
||||
# summary 截断到 4000 字符并经 python3 转义,防止引号/换行破坏 JSON
|
||||
summary_json=$(printf '%s' "$summary" | head -c 4000 | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
|
||||
[ -z "$summary_json" ] && summary_json='""'
|
||||
flag_json=$(printf '%s' "$flag" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
|
||||
[ -z "$flag_json" ] && flag_json='""'
|
||||
|
||||
cat > "$BB/result.json" <<EOF
|
||||
{
|
||||
"status": "$status",
|
||||
"exit_code": $code,
|
||||
"summary": $summary_json,
|
||||
"flag": $flag_json,
|
||||
"artifacts": $artifacts_json
|
||||
}
|
||||
EOF
|
||||
log "===== 协作结束 ====="
|
||||
log "status=$status exit_code=$code"
|
||||
[ -n "$flag" ] && log "flag=$flag"
|
||||
log "结构化结果已写入:$BB/result.json"
|
||||
}
|
||||
|
||||
# ---- 前置校验 ----
|
||||
if [ -z "${MODEL:-}" ] || [ -z "${BASE_URL:-}" ] || [ -z "${API_KEY:-}" ]; then
|
||||
echo "错误:必须提供 MODEL / BASE_URL / API_KEY 环境变量" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -z "${TASK:-}" ] && [ ! -f "$BB/task.md" ]; then
|
||||
echo "错误:请通过 TASK 环境变量或挂载 $BB/task.md 提供任务" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -f "$BB/DONE" ]; then
|
||||
log "黑板已有完成标记,如需重新开始请删除 $BB/DONE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- 初始化黑板 ----
|
||||
mkdir -p "$BB/workspace" "$BB/logs" "$BB/sessions"
|
||||
if [ -n "${TASK:-}" ]; then
|
||||
printf '%s\n' "$TASK" > "$BB/task.md"
|
||||
fi
|
||||
# 协议注入:agent 的 cwd 是 $BB/workspace,AGENTS.md 注入链从 cwd 起,
|
||||
# Claude Code 会自动加载 cwd 下的 CLAUDE.md(AGENTS.md 兼容),因此把协议
|
||||
# 副本放进工作区(勿修改,它是每轮系统提示的一部分)
|
||||
cp "$PROMPTS/AGENTS.md" "$BB/workspace/CLAUDE.md"
|
||||
cp "$PROMPTS/AGENTS.md" "$BB/workspace/AGENTS.md"
|
||||
log "黑板初始化完成:$BB"
|
||||
log "任务:$(head -c 200 "$BB/task.md")"
|
||||
|
||||
# ---- 导出 Claude Code 模型配置环境变量 ----
|
||||
# 关键:ANTHROPIC_API_KEY 必须设为空字符串(而非 unset),Claude Code 才会
|
||||
# 回退到 ANTHROPIC_AUTH_TOKEN。BASE_URL 不带 /v1(SDK 自动追加 /v1/messages)。
|
||||
export ANTHROPIC_BASE_URL="$BASE_URL"
|
||||
export ANTHROPIC_AUTH_TOKEN="$API_KEY"
|
||||
export ANTHROPIC_API_KEY=""
|
||||
export ANTHROPIC_MODEL="$MODEL"
|
||||
# 把 opus/sonnet/haiku 三个别名都映射到目标模型,避免别名解析失败。
|
||||
export ANTHROPIC_DEFAULT_OPUS_MODEL="$MODEL"
|
||||
export ANTHROPIC_DEFAULT_SONNET_MODEL="$MODEL"
|
||||
export ANTHROPIC_DEFAULT_HAIKU_MODEL="$MODEL"
|
||||
# 禁用非必要流量(遥测/登录检测),第三方端点必需。
|
||||
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
||||
|
||||
# ---- 单轮运行:claude -p --output-format json,从 JSON 提取 session_id ----
|
||||
run_agent() {
|
||||
local round="$1"
|
||||
local session_file="$BB/sessions/agent.session"
|
||||
local task_prompt="轮次 $round 开始。先读取 /blackboard/task.md 中的任务,检查工作区已有产物与 DONE 状态,然后继续推进任务。全部工作完成、交付物完整时,用 blackboard 工具 done 创建完成标记(summary 写最终交付总结,含关键结果/flag/提交响应/产物清单)。"
|
||||
local args=(-p "$task_prompt"
|
||||
--output-format json
|
||||
--mcp-config "$MCP_CONFIG"
|
||||
--allowedTools "mcp__blackboard__*,Read,Write,Edit,Bash"
|
||||
--append-system-prompt-file "$PROMPTS/agent.md"
|
||||
--dangerously-skip-permissions)
|
||||
if [ -s "$session_file" ]; then
|
||||
args+=(--resume "$(cat "$session_file")")
|
||||
fi
|
||||
|
||||
export ROUND="$round" NAME="agent" BB="$BB"
|
||||
local logfile="$BB/logs/round-$round.log"
|
||||
log "第 $round 轮开始运行 agent(日志:$logfile)"
|
||||
# Claude Code 用 --cwd / 进程 cwd 作工作区,先进入 $BB/workspace
|
||||
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
|
||||
(cd "$BB/workspace" && timeout "$TIMEOUT" claude "${args[@]}") > "$logfile" 2>&1
|
||||
else
|
||||
(cd "$BB/workspace" && claude "${args[@]}") > "$logfile" 2>&1
|
||||
fi
|
||||
local rc=$?
|
||||
|
||||
# 从 --output-format json 的结果提取 session_id 供下一轮 --resume
|
||||
# JSON 形如 {"type":"result","session_id":"<uuid>",...}
|
||||
local sid
|
||||
sid=$(python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.loads(sys.stdin.read())
|
||||
if d.get("type") == "result":
|
||||
print(d.get("session_id", ""))
|
||||
except Exception:
|
||||
pass
|
||||
' < "$logfile" 2>/dev/null)
|
||||
if [ -n "$sid" ]; then
|
||||
printf '%s' "$sid" > "$session_file"
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ---- 主循环:逐轮运行 agent,直到 DONE 或达到轮次上限 ----
|
||||
for round in $(seq 1 "$ROUND_MAX"); do
|
||||
[ -f "$BB/DONE" ] && break
|
||||
log "===== 第 $round 轮开始 ====="
|
||||
|
||||
run_agent "$round"
|
||||
rc=$?
|
||||
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
log "第 $round 轮完成"
|
||||
else
|
||||
log "第 $round 轮失败(exit=$rc),日志见 $BB/logs/round-$round.log"
|
||||
if [ "$FAIL_MODE" = "stop" ]; then
|
||||
# 兜底:agent 虽超时/失败,但黑板已有提交成功证据(workspace 内 *.md 含 correct:true)
|
||||
# → 视为完成正常退出,避免"flag 已提交却因未写 DONE 被强杀(exit 143)"。
|
||||
_done=0
|
||||
for _f in "$BB"/workspace/*.md "$BB"/messages/*.md; do
|
||||
[ -f "$_f" ] && grep -q '"correct":true' "$_f" && _done=1 && break
|
||||
done
|
||||
if [ "$_done" = "1" ]; then
|
||||
log "检测到提交成功证据,自动标记完成"
|
||||
{ echo "Task finished (auto-detected submit success)"; } > "$BB/DONE"
|
||||
emit_result solved "$(cat "$BB/DONE")" 0
|
||||
exit 0
|
||||
fi
|
||||
if [ "$rc" -eq 124 ]; then
|
||||
emit_result timeout "" "$rc"
|
||||
else
|
||||
emit_result error "agent 退出码 $rc,日志见 logs/round-$round.log" "$rc"
|
||||
fi
|
||||
exit "$rc"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -f "$BB/DONE" ]; then
|
||||
log "检测到完成标记,任务结束"
|
||||
emit_result solved "$(cat "$BB/DONE")" 0
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "达到最大轮次 $ROUND_MAX 仍未完成,请检查黑板产物与日志"
|
||||
emit_result unsolved "" 1
|
||||
exit 1
|
||||
Reference in New Issue
Block a user