ai-battle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/ai-battle.sh +1728 -0
  4. package/package.json +29 -0
package/ai-battle.sh ADDED
@@ -0,0 +1,1728 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # ai-battle.sh — AI 圆桌讨论工具
4
+ #
5
+ # 让多个 AI Agent(Claude/Codex/Gemini…)对同一问题进行
6
+ # 结构化讨论,自动管理轮次、检测共识、保存全部讨论记录。
7
+ #
8
+ # 用法:
9
+ # ai-battle [--agents claude,codex] [--rounds 10] [--god] [--referee]
10
+ # ai-battle --help
11
+ #
12
+ # 依赖: jq, bash 4+, (可选) codex, claude, gemini
13
+ # ============================================================
14
+ set -euo pipefail
15
+
16
+ # ======================== 加载 .env ========================
17
+ # 如果执行目录存在 .env 文件,自动加载环境变量
18
+ if [ -f ".env" ]; then
19
+ set -a
20
+ # shellcheck source=/dev/null
21
+ source .env
22
+ set +a
23
+ fi
24
+
25
+ # ======================== 版本 ========================
26
+ VERSION="0.1.0"
27
+
28
+ # ======================== 颜色 ========================
29
+ BLUE='\033[1;34m'
30
+ GREEN='\033[1;32m'
31
+ YELLOW='\033[1;33m'
32
+ RED='\033[1;31m'
33
+ CYAN='\033[0;36m'
34
+ BOLD='\033[1m'
35
+ NC='\033[0m'
36
+
37
+ # ======================== 默认配置 ========================
38
+ DEFAULT_AGENTS="claude,codex"
39
+ DEFAULT_MAX_ROUNDS=10
40
+ PROBLEM_FILE="problem.md"
41
+ ROUNDS_DIR="rounds"
42
+ CONSENSUS_FILE="consensus.md"
43
+ LOG_FILE=".debate.log"
44
+ CONFIG_FILE=".debate.json"
45
+ REFEREE_PROMPT_FILE="referee.md"
46
+ SESSIONS_DIR=".sessions"
47
+
48
+ # ======================== Codex 配置 ========================
49
+ CODEX_MODEL="${CODEX_MODEL:-gpt-5.3-codex}"
50
+
51
+ # ======================== Agent 注册表 ========================
52
+ # 已注册的 agent 名称列表
53
+ REGISTERED_AGENTS=()
54
+
55
+ # 注册 agent
56
+ # 用法: register_agent <name>
57
+ # 要求: 必须实现 check_<name>() 和 call_<name>() 两个函数
58
+ register_agent() {
59
+ REGISTERED_AGENTS+=("$1")
60
+ }
61
+
62
+ # ======================== Agent: Claude (CLI) ========================
63
+ # 前置条件: 用户需自行设置 claude 所需的环境变量, 例如:
64
+ # export ANTHROPIC_BASE_URL="https://open.bigmodel.cn/api/anthropic"
65
+ # export ANTHROPIC_AUTH_TOKEN="your-token"
66
+ # export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
67
+ # export API_TIMEOUT_MS=600000
68
+ # export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-4.7"
69
+
70
+ # 检查 Claude CLI 是否可用
71
+ # 返回: 0=可用, 1=不可用
72
+ check_claude() {
73
+ if ! command -v claude &>/dev/null; then
74
+ echo -e " ${RED}✗ claude 命令不在 PATH 中${NC}" >&2
75
+ return 1
76
+ fi
77
+ # 调用测试:分离 stdout/stderr,检查退出码
78
+ local resp errmsg
79
+ local tmperr
80
+ tmperr=$(mktemp)
81
+ resp=$(claude -p "hello" --output-format text 2>"$tmperr") && local rc=0 || local rc=$?
82
+ errmsg=$(cat "$tmperr" 2>/dev/null)
83
+ rm -f "$tmperr"
84
+
85
+ if [ $rc -ne 0 ] || [ -z "$resp" ]; then
86
+ # 输出具体错误信息帮助诊断
87
+ if [ -n "$errmsg" ]; then
88
+ echo -e " ${RED}✗ claude 调用失败: ${errmsg}${NC}" >&2
89
+ elif [ -n "$resp" ]; then
90
+ # 退出码非零但有 stdout(可能是错误信息输出到了 stdout)
91
+ echo -e " ${RED}✗ claude 调用失败: ${resp}${NC}" >&2
92
+ else
93
+ echo -e " ${RED}✗ claude 命令无响应 (请检查环境变量配置)${NC}" >&2
94
+ fi
95
+ return 1
96
+ fi
97
+ return 0
98
+ }
99
+
100
+ # 调用 Claude CLI(支持原生 --system-prompt)
101
+ # 参数: $1=system_prompt $2=user_message
102
+ # 输出: stdout 回复文本
103
+ call_claude() {
104
+ local system_prompt="$1"
105
+ local user_msg="$2"
106
+ local session_tag="${3:-}" # 可选: session 文件名标识
107
+ local max_retries=3
108
+ local retry_delay=10
109
+
110
+ local attempt=1
111
+ while [ $attempt -le $max_retries ]; do
112
+ local raw_out
113
+ raw_out=$(mktemp)
114
+
115
+ # 捕获原始输出(stream-json 格式)
116
+ claude -p "$user_msg" --system-prompt "$system_prompt" --output-format stream-json \
117
+ > "$raw_out" 2>&1 || true
118
+
119
+ # 保存原始 session 记录
120
+ if [ -n "$session_tag" ] && [ -d "$SESSIONS_DIR" ]; then
121
+ cp "$raw_out" "$SESSIONS_DIR/${session_tag}_claude.jsonl"
122
+ fi
123
+
124
+ # 从 stream-json 提取文本内容 (Claude CLI 格式: type=result 的 .result 字段)
125
+ local text
126
+ text=$(jq -r 'select(.type=="result") | .result // empty' "$raw_out" 2>/dev/null || true)
127
+
128
+ # 备选: 从 type=assistant 的 message.content[].text 提取
129
+ if [ -z "$text" ]; then
130
+ text=$(jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text // empty' "$raw_out" 2>/dev/null || true)
131
+ fi
132
+
133
+ # 如果 stream-json 提取失败,回退到 text 格式
134
+ if [ -z "$text" ]; then
135
+ text=$(claude -p "$user_msg" --system-prompt "$system_prompt" --output-format text 2>/dev/null || true)
136
+ fi
137
+
138
+ rm -f "$raw_out"
139
+
140
+ if [ -n "$text" ]; then
141
+ echo "$text"
142
+ return 0
143
+ fi
144
+
145
+ echo -e "${YELLOW}WARN: Claude CLI 第 $attempt 次失败,${retry_delay}s 后重试...${NC}" >&2
146
+ sleep $retry_delay
147
+ attempt=$((attempt + 1))
148
+ retry_delay=$((retry_delay * 2))
149
+ done
150
+
151
+ echo -e "${RED}ERROR: Claude CLI $max_retries 次重试均失败${NC}" >&2
152
+ return 1
153
+ }
154
+
155
+ # 生成 Claude 指令文件
156
+ # 参数: $1=max_rounds $2=problem_text
157
+ generate_claude_md() {
158
+ local max_rounds="$1"
159
+ local problem="$2"
160
+ cat << CLAUDE_EOF
161
+ # AI 圆桌讨论 — Claude 参与者
162
+
163
+ ## 身份
164
+ 你是 **Claude**,正在与其他 AI 进行结构化技术讨论。
165
+
166
+ ## 讨论规则
167
+ 1. 阅读 \`$PROBLEM_FILE\` 了解讨论主题
168
+ 2. 查看 \`$ROUNDS_DIR/\` 目录确定当前轮次 N
169
+ 3. 如果对方的回复已存在,先仔细阅读
170
+ 4. 将你的回复写入 \`$ROUNDS_DIR/round_N_claude.md\`
171
+
172
+ ## 当前配置
173
+ - 最大轮次: **$max_rounds**
174
+ - 讨论问题: $problem
175
+
176
+ ## 回复要求
177
+ - 深入分析,不要敷衍;如不同意对方,明确反驳
178
+ - 达成共识时在回复最后一行写: \`AGREED: <结论>\`
179
+ - 不要过早同意,确保分析完整
180
+
181
+ ## 约束
182
+ - 每次只写一轮,写完等待下一轮指示
183
+ - 不要修改其他 Agent 的文件
184
+ CLAUDE_EOF
185
+ }
186
+
187
+ register_agent "claude"
188
+
189
+ # ======================== Agent: Codex ========================
190
+
191
+ # 检查 Codex 是否可用
192
+ check_codex() {
193
+ if ! command -v codex &>/dev/null; then
194
+ echo -e " ${RED}✗ codex 命令不在 PATH 中${NC}" >&2
195
+ return 1
196
+ fi
197
+ # 实际调用验证认证和可用性
198
+ local resp errmsg
199
+ local tmperr
200
+ tmperr=$(mktemp)
201
+ resp=$(codex exec "hello" 2>"$tmperr") && local rc=0 || local rc=$?
202
+ errmsg=$(cat "$tmperr" 2>/dev/null)
203
+ rm -f "$tmperr"
204
+
205
+ if [ $rc -ne 0 ] || [ -z "$resp" ]; then
206
+ if [ -n "$errmsg" ]; then
207
+ echo -e " ${RED}✗ codex 调用失败: ${errmsg}${NC}" >&2
208
+ elif [ -n "$resp" ]; then
209
+ echo -e " ${RED}✗ codex 调用失败: ${resp}${NC}" >&2
210
+ else
211
+ echo -e " ${RED}✗ codex 命令无响应 (请检查认证配置)${NC}" >&2
212
+ fi
213
+ return 1
214
+ fi
215
+ return 0
216
+ }
217
+
218
+ # 调用 Codex
219
+ # 参数: $1=system_prompt $2=user_message $3=session_tag(可选)
220
+ # 输出: stdout 回复文本
221
+ call_codex() {
222
+ local system_prompt="$1"
223
+ local user_msg="$2"
224
+ local session_tag="${3:-}" # 可选: session 文件名标识
225
+ local max_retries=3
226
+ local retry_delay=10
227
+
228
+ local full_prompt="$system_prompt
229
+
230
+ $user_msg
231
+
232
+ 重要:直接输出你的分析内容,不要写任何文件,不要输出方案图或伪代码,直接给出你的讨论观点。"
233
+
234
+ # 确保 codex 有 git 仓库
235
+ if [ ! -d ".git" ]; then
236
+ git init -q
237
+ git add -A 2>/dev/null || true
238
+ git commit -q -m "debate-init" 2>/dev/null || true
239
+ fi
240
+
241
+ local attempt=1
242
+ while [ $attempt -le $max_retries ]; do
243
+ local tmpout
244
+ tmpout=$(mktemp)
245
+
246
+ codex exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox \
247
+ -m "$CODEX_MODEL" "$full_prompt" \
248
+ 2>&1 | tee "$tmpout" > /dev/null || true
249
+
250
+ # 保存原始 session 记录
251
+ if [ -n "$session_tag" ] && [ -d "$SESSIONS_DIR" ]; then
252
+ cp "$tmpout" "$SESSIONS_DIR/${session_tag}_codex.raw"
253
+ fi
254
+
255
+ local text=""
256
+
257
+ # 从输出中提取有效内容(跳过 codex 元数据行)
258
+ text=$(grep -v '^\(thinking\|exec\|mcp startup\|OpenAI Codex\|--------\|workdir:\|model:\|provider:\|approval:\|sandbox:\|reasoning\|session id:\|user$\|tokens used\|Exit code:\|^$\)' "$tmpout" 2>/dev/null | sed '/^$/d')
259
+
260
+ rm -f "$tmpout"
261
+
262
+ if [ -n "$text" ]; then
263
+ echo "$text"
264
+ return 0
265
+ fi
266
+
267
+ echo -e "${YELLOW}WARN: Codex 第 $attempt 次失败,${retry_delay}s 后重试...${NC}" >&2
268
+ sleep $retry_delay
269
+ attempt=$((attempt + 1))
270
+ retry_delay=$((retry_delay * 2))
271
+ done
272
+
273
+ echo -e "${RED}ERROR: Codex $max_retries 次重试均失败${NC}" >&2
274
+ return 1
275
+ }
276
+
277
+ # 生成 Codex 指令文件
278
+ generate_codex_md() {
279
+ local max_rounds="$1"
280
+ local problem="$2"
281
+ cat << CODEX_EOF
282
+ # AI 圆桌讨论 — Codex 参与者
283
+
284
+ ## 身份
285
+ 你是 **Codex**,正在与其他 AI 进行结构化技术讨论。
286
+
287
+ ## 讨论规则
288
+ 1. 阅读 \`$PROBLEM_FILE\` 了解讨论主题
289
+ 2. 查看 \`$ROUNDS_DIR/\` 目录确定当前轮次 N
290
+ 3. 如果对方的回复已存在,先仔细阅读
291
+ 4. 将你的回复写入 \`$ROUNDS_DIR/round_N_codex.md\`
292
+
293
+ ## 当前配置
294
+ - 最大轮次: **$max_rounds**
295
+ - 讨论问题: $problem
296
+
297
+ ## 回复要求
298
+ - 深入分析,不要敷衍;如不同意对方,明确反驳
299
+ - 达成共识时在回复最后一行写: \`AGREED: <结论>\`
300
+ - 不要过早同意,确保分析完整
301
+
302
+ ## 约束
303
+ - 每次只写一轮,写完等待下一轮指示
304
+ - 不要修改其他 Agent 的文件
305
+ CODEX_EOF
306
+ }
307
+
308
+ register_agent "codex"
309
+
310
+ # ======================== Agent: Gemini (CLI) ========================
311
+ # 前置条件: 安装 Gemini CLI (npm install -g @anthropic-ai/gemini-cli 或参考官方文档)
312
+ # 可选环境变量:
313
+ # export GEMINI_API_KEY="your-key"
314
+
315
+ # 检查 Gemini CLI 是否可用
316
+ # 返回: 0=可用, 1=不可用
317
+ check_gemini() {
318
+ if ! command -v gemini &>/dev/null; then
319
+ echo -e " ${RED}✗ gemini 命令不在 PATH 中${NC}" >&2
320
+ return 1
321
+ fi
322
+ # 调用测试:分离 stdout/stderr,检查退出码
323
+ local resp errmsg tmperr
324
+ tmperr=$(mktemp)
325
+ resp=$(gemini -p "hello" --output-format text 2>"$tmperr") && local rc=0 || local rc=$?
326
+ errmsg=$(cat "$tmperr" 2>/dev/null)
327
+ rm -f "$tmperr"
328
+
329
+ if [ $rc -ne 0 ] || [ -z "$resp" ]; then
330
+ if [ -n "$errmsg" ]; then
331
+ echo -e " ${RED}✗ gemini 调用失败: ${errmsg}${NC}" >&2
332
+ elif [ -n "$resp" ]; then
333
+ echo -e " ${RED}✗ gemini 调用失败: ${resp}${NC}" >&2
334
+ else
335
+ echo -e " ${RED}✗ gemini 命令无响应 (请检查 API Key 配置)${NC}" >&2
336
+ fi
337
+ return 1
338
+ fi
339
+ return 0
340
+ }
341
+
342
+ # 调用 Gemini CLI
343
+ # 参数: $1=system_prompt $2=user_message
344
+ # 输出: stdout 回复文本
345
+ call_gemini() {
346
+ local system_prompt="$1"
347
+ local user_msg="$2"
348
+ local session_tag="${3:-}" # 可选: session 文件名标识
349
+ local max_retries=3
350
+ local retry_delay=10
351
+
352
+ local full_msg="${system_prompt}
353
+
354
+ ${user_msg}"
355
+
356
+ local attempt=1
357
+ while [ $attempt -le $max_retries ]; do
358
+ local raw_out
359
+ raw_out=$(mktemp)
360
+
361
+ # 捕获原始输出
362
+ gemini -p "$full_msg" --output-format json \
363
+ > "$raw_out" 2>&1 || true
364
+
365
+ # 保存原始 session 记录
366
+ if [ -n "$session_tag" ] && [ -d "$SESSIONS_DIR" ]; then
367
+ cp "$raw_out" "$SESSIONS_DIR/${session_tag}_gemini.json"
368
+ fi
369
+
370
+ # 从 json 提取文本内容 (Gemini CLI 格式: type=result 的 .result 字段)
371
+ local text
372
+ text=$(jq -r 'select(.type=="result") | .result // empty' "$raw_out" 2>/dev/null || true)
373
+
374
+ # 备选: 尝试数组格式
375
+ if [ -z "$text" ]; then
376
+ text=$(jq -r '.[] | select(.type=="text") | .text // empty' "$raw_out" 2>/dev/null || true)
377
+ fi
378
+
379
+ # 如果 json 提取失败,回退到 text 格式
380
+ if [ -z "$text" ]; then
381
+ text=$(gemini -p "$full_msg" --output-format text 2>/dev/null || true)
382
+ fi
383
+
384
+ rm -f "$raw_out"
385
+
386
+ if [ -n "$text" ]; then
387
+ echo "$text"
388
+ return 0
389
+ fi
390
+
391
+ echo -e "${YELLOW}WARN: Gemini CLI 第 $attempt 次失败,${retry_delay}s 后重试...${NC}" >&2
392
+ sleep $retry_delay
393
+ attempt=$((attempt + 1))
394
+ retry_delay=$((retry_delay * 2))
395
+ done
396
+
397
+ echo -e "${RED}ERROR: Gemini CLI $max_retries 次重试均失败${NC}" >&2
398
+ return 1
399
+ }
400
+
401
+ # 生成 Gemini 指令文件
402
+ # 参数: $1=max_rounds $2=problem_text
403
+ generate_gemini_md() {
404
+ local max_rounds="$1"
405
+ local problem="$2"
406
+ cat << GEMINI_EOF
407
+ # AI 圆桌讨论 — Gemini 参与者
408
+
409
+ ## 身份
410
+ 你是 **Gemini**,正在与其他 AI 进行结构化技术讨论。
411
+
412
+ ## 讨论规则
413
+ 1. 阅读 \`$PROBLEM_FILE\` 了解讨论主题
414
+ 2. 查看 \`$ROUNDS_DIR/\` 目录确定当前轮次 N
415
+ 3. 如果对方的回复已存在,先仔细阅读
416
+ 4. 将你的回复写入 \`$ROUNDS_DIR/round_N_gemini.md\`
417
+
418
+ ## 当前配置
419
+ - 最大轮次: **$max_rounds**
420
+ - 讨论问题: $problem
421
+
422
+ ## 回复要求
423
+ - 深入分析,不要敷衍;如不同意对方,明确反驳
424
+ - 达成共识时在回复最后一行写: \`AGREED: <结论>\`
425
+ - 不要过早同意,确保分析完整
426
+
427
+ ## 约束
428
+ - 每次只写一轮,写完等待下一轮指示
429
+ - 不要修改其他 Agent 的文件
430
+ GEMINI_EOF
431
+ }
432
+
433
+ register_agent "gemini"
434
+
435
+ # ======================== 工具函数 ========================
436
+
437
+ # Round 层级重试包装器
438
+ # 当 agent 内部重试耗尽后,询问用户是否继续重试
439
+ # 参数: $1=agent名称(display) $2+=要执行的命令
440
+ # 输出: stdout 命令结果
441
+ # 返回: 0=成功, 1=用户选择退出
442
+ retry_call_agent() {
443
+ local agent_display="$1"
444
+ shift
445
+
446
+ while true; do
447
+ local result
448
+ result=$("$@" 2>/dev/null) && local rc=0 || local rc=$?
449
+
450
+ if [ $rc -eq 0 ] && [ -n "$result" ]; then
451
+ echo "$result"
452
+ return 0
453
+ fi
454
+
455
+ echo "" >&2
456
+ echo -e "${RED}━━━ ⚠️ ${agent_display} 调用失败 ━━━${NC}" >&2
457
+ echo -e " ${BOLD}r)${NC} 重试" >&2
458
+ echo -e " ${BOLD}s)${NC} 跳过该 agent 本轮发言" >&2
459
+ echo -e " ${BOLD}q)${NC} 退出讨论" >&2
460
+ echo -ne "${BOLD}请选择 [r/s/q]: ${NC}" >&2
461
+ local choice
462
+ read -er choice
463
+ case "$choice" in
464
+ s|S)
465
+ echo -e "${YELLOW} 跳过 ${agent_display}${NC}" >&2
466
+ echo "" # 返回空字符串
467
+ return 0
468
+ ;;
469
+ q|Q)
470
+ echo -e "${CYAN}已退出。${NC}" >&2
471
+ exit 1
472
+ ;;
473
+ *) # 默认重试
474
+ echo -e "${CYAN} 重试 ${agent_display}...${NC}" >&2
475
+ ;;
476
+ esac
477
+ done
478
+ }
479
+
480
+ # 从实例名提取基础 agent 类型
481
+ # 用法: agent_base "claude_1" → "claude",agent_base "claude" → "claude"
482
+ agent_base() {
483
+ echo "$1" | sed 's/_[0-9]*$//'
484
+ }
485
+
486
+ # 日志输出(同时写终端和日志文件)
487
+ log_and_print() {
488
+ echo -e "$1"
489
+ echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE"
490
+ }
491
+
492
+ # 检查 AGREED 关键字(支持行内匹配,不要求行首)
493
+ has_agreed() {
494
+ echo "$1" | grep -qiE '\*{0,2}AGREED:'
495
+ }
496
+
497
+ # 提取 AGREED 后的结论
498
+ extract_agreed() {
499
+ echo "$1" | grep -ioE '\*{0,2}AGREED:[[:space:]]*.*' | head -1 \
500
+ | sed 's/^\*\{0,2\}[Aa][Gg][Rr][Ee][Ee][Dd]:[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/\*\{1,2\}$//'
501
+ }
502
+
503
+ # Agent 颜色映射(自动提取基础类型)
504
+ agent_color() {
505
+ local base
506
+ base=$(agent_base "$1")
507
+ case "$base" in
508
+ claude) echo "$BLUE" ;;
509
+ codex) echo "$GREEN" ;;
510
+ gemini) echo "\033[1;35m" ;; # 紫色
511
+ *) echo "$YELLOW" ;;
512
+ esac
513
+ }
514
+
515
+ # Agent 指令文件名映射(支持实例名,如 claude_1 → CLAUDE_1.md)
516
+ agent_md_file() {
517
+ local base
518
+ base=$(agent_base "$1")
519
+ local suffix=""
520
+ if [ "$1" != "$base" ]; then
521
+ suffix="_${1##*_}" # 提取 _N 后缀
522
+ fi
523
+ case "$base" in
524
+ claude) echo "CLAUDE${suffix}.md" ;;
525
+ codex) echo "AGENTS${suffix}.md" ;;
526
+ gemini) echo "GEMINI${suffix}.md" ;;
527
+ *) echo "${1^^}.md" ;;
528
+ esac
529
+ }
530
+
531
+ # 上帝视角: 等待用户输入补充信息
532
+ # 参数: $1=当前轮次
533
+ # 输出: stdout 用户输入的补充信息(可为空)
534
+ god_input() {
535
+ local round="$1"
536
+ echo "" >&2
537
+ echo -e "${CYAN}━━━ 👁️ 上帝视角 [Round ${round} 结束] ━━━${NC}" >&2
538
+ echo -e "${CYAN}输入补充信息(直接回车跳过,多行输入以 END 结束):${NC}" >&2
539
+ echo -ne "${CYAN}> ${NC}" >&2
540
+
541
+ local first_line
542
+ read -er first_line
543
+
544
+ # 直接回车 = 跳过
545
+ if [ -z "$first_line" ]; then
546
+ echo -e "${CYAN} (跳过)${NC}" >&2
547
+ echo ""
548
+ return
549
+ fi
550
+
551
+ local input="$first_line"
552
+
553
+ # 如果第一行不是 END,继续读取多行
554
+ if [ "$first_line" != "END" ]; then
555
+ while true; do
556
+ echo -ne "${CYAN}> ${NC}" >&2
557
+ local line
558
+ read -er line
559
+ if [ "$line" = "END" ] || [ -z "$line" ]; then
560
+ break
561
+ fi
562
+ input+=$'\n'"$line"
563
+ done
564
+ fi
565
+
566
+ # 保存上帝视角输入
567
+ echo "$input" > "$ROUNDS_DIR/god_round_${round}.md"
568
+ log_and_print "${CYAN}👁️ 上帝视角注入: $(echo "$input" | head -1)...${NC}"
569
+ echo "$input"
570
+ }
571
+
572
+ # 系统提示(通用)
573
+ build_system_prompt() {
574
+ local max_rounds="$1"
575
+ cat << SYSPROMPT
576
+ 你正在参与一场 AI 圆桌讨论。规则:
577
+ 1. 深入分析问题,给出有价值的观点
578
+ 2. 积极回应对方——认同有道理的部分,挑战有漏洞的部分
579
+ 3. 每轮回复应增加新信息或新见解,不要重复
580
+ 4. 确信分析完整且正确时,在回复最后一行写 AGREED: <共识结论>
581
+ 5. 不要过早同意。最多 $max_rounds 轮讨论。
582
+
583
+ 重要格式要求:
584
+ - 当你要表示同意时,必须在回复中直接输出独立一行 AGREED: <结论>
585
+ - 不要描述你会写什么,直接写出来。错误示例:"我在文件中写入 AGREED:" — 正确示例:直接输出 AGREED: xxx
586
+ - AGREED 行必须独占一行,不要嵌套在其他句子中
587
+ SYSPROMPT
588
+ }
589
+
590
+ # ======================== 裁判功能 ========================
591
+
592
+ # 裁判 system prompt(自由辩论模式)
593
+ # 裁判独立于参与者,客观总结并判断是否实质达成一致
594
+ build_referee_free_prompt() {
595
+ cat << 'REFEREE_FREE'
596
+ 你是一位公正的讨论裁判。你的职责是:
597
+ 1. 客观总结各参与者本轮的核心观点
598
+ 2. 识别各方的共识和分歧
599
+ 3. 判断是否已实质达成一致(即使没有明确说 AGREED)
600
+
601
+ 请严格按以下格式输出:
602
+
603
+ ## 本轮总结
604
+ (简要概括各参与者的核心论点)
605
+
606
+ ## 共识与分歧
607
+ (列出已达成一致的要点和仍有分歧的要点)
608
+
609
+ ## 裁判判定
610
+ CONSENSUS: YES 或 CONSENSUS: NO
611
+ (如果 YES,必须紧接着下一行输出)
612
+ AGREED: <最终结论的简要概括>
613
+
614
+ 注意:只有在各方观点确实在关键议题上没有实质分歧时才判定 YES。
615
+ 不要过早判定共识,除非分歧已经真正解决。
616
+ REFEREE_FREE
617
+ }
618
+
619
+ # 裁判 system prompt(上帝视角模式)
620
+ # 格式化输出差异点,辅助上帝做出判断
621
+ build_referee_god_prompt() {
622
+ cat << 'REFEREE_GOD'
623
+ 你是一位公正的讨论裁判,你需要为「上帝视角」的操控者提供决策辅助。
624
+
625
+ 请严格按以下格式输出:
626
+
627
+ ## 本轮总结
628
+ (简要概括各参与者的核心论点,2-3 句话)
629
+
630
+ ## 差异对比表
631
+ | 议题 | 参与者A | 参与者B | ... |
632
+ |------|---------|---------|-----|
633
+ | 议题1 | 立场/方案 | 立场/方案 | ... |
634
+ | 议题2 | 立场/方案 | 立场/方案 | ... |
635
+
636
+ (用实际参与者名称替换表头)
637
+
638
+ ## 关键分歧点
639
+ (列出 2-3 个最重要的分歧,简要说明各方理由)
640
+
641
+ ## 建议关注
642
+ (给上帝视角操控者的建议:哪些分歧值得介入引导?)
643
+
644
+ 注意:保持客观,不要偏向任何一方。
645
+ REFEREE_GOD
646
+ }
647
+
648
+ # 裁判最终总结 prompt(讨论结束后生成综合总结)
649
+ build_referee_final_prompt() {
650
+ cat << 'REFEREE_FINAL'
651
+ 你是讨论裁判。请综合全部讨论记录,输出一份完整的总结文档。
652
+
653
+ 输出格式要求(Markdown):
654
+ # 总结:<主题标题>
655
+
656
+ ## 1. 问题概述
657
+ 简明扼要地总结讨论主题和核心问题。
658
+
659
+ ## 2. 主要共识
660
+ 列出讨论中各方达成一致的关键点。
661
+
662
+ ## 3. 主要分歧
663
+ 列出讨论中各方未达成一致的点,并说明各方理由。
664
+
665
+ ## 4. 关键结论
666
+ 列出讨论中确定的重要结论及其依据。
667
+
668
+ ## 5. 待解决问题
669
+ 列出仍需进一步讨论或行动的问题(如果有)。
670
+
671
+ 规则:
672
+ - 直接输出 Markdown 内容,不要输出任何解释性前缀
673
+ - 使用中文
674
+ - 内容必须基于讨论记录中的实际观点,不要编造
675
+ - 保持结构清晰、客观中立
676
+ REFEREE_FINAL
677
+ }
678
+
679
+ # 调用裁判
680
+ # 参数: $1=裁判 agent 基础类型, $2=裁判 prompt, $3=用户消息, $4=session tag(可选)
681
+ # 输出: stdout 裁判总结文本
682
+ call_referee() {
683
+ local referee_base="$1"
684
+ local referee_prompt="$2"
685
+ local user_msg="$3"
686
+ local session_tag="${4:-}"
687
+ "call_${referee_base}" "$referee_prompt" "$user_msg" "$session_tag"
688
+ }
689
+
690
+ # 裁判生成最终设计方案
691
+ # 参数: $1=裁判 base 类型, $2=自定义提示词(可空)
692
+ # 依赖全局: ROUNDS_DIR, PROBLEM_FILE, CONSENSUS_FILE, CONFIG_FILE
693
+ generate_final_summary() {
694
+ local ref_base="$1"
695
+ local custom_prompt="${2:-}"
696
+
697
+ log_and_print ""
698
+ log_and_print "${BOLD}📝 裁判正在生成最终总结...${NC}"
699
+
700
+ # 收集所有轮次内容
701
+ local all_rounds=""
702
+ for f in $(ls -1 "$ROUNDS_DIR"/*.md 2>/dev/null | sort); do
703
+ local fname
704
+ fname=$(basename "$f")
705
+ all_rounds+="\n--- ${fname} ---\n"
706
+ all_rounds+=$(cat "$f")
707
+ all_rounds+="\n"
708
+ done
709
+
710
+ local problem
711
+ problem=$(cat "$PROBLEM_FILE")
712
+
713
+ local consensus=""
714
+ if [ -f "$CONSENSUS_FILE" ]; then
715
+ consensus=$(cat "$CONSENSUS_FILE")
716
+ fi
717
+
718
+ local final_prompt
719
+ final_prompt=$(build_referee_final_prompt)
720
+ if [ -n "$custom_prompt" ]; then
721
+ final_prompt+=$'\n\n[额外指示]\n'"$custom_prompt"
722
+ fi
723
+
724
+ local user_content="<discussion_topic>
725
+ ${problem}
726
+ </discussion_topic>
727
+
728
+ <consensus>
729
+ ${consensus:-未达成明确共识}
730
+ </consensus>
731
+
732
+ <discussion_records>
733
+ ${all_rounds}
734
+ </discussion_records>"
735
+
736
+ local summary_text
737
+ summary_text=$(call_referee "$ref_base" "$final_prompt" "$user_content" "final_summary")
738
+
739
+ if [ -z "$summary_text" ]; then
740
+ log_and_print "${RED}❌ 生成最终总结失败${NC}"
741
+ return 1
742
+ fi
743
+
744
+ local summary_file="SUMMARY.md"
745
+ echo "$summary_text" > "$summary_file"
746
+
747
+ log_and_print ""
748
+ log_and_print "${BOLD}╔══════════════════════════════════════════╗${NC}"
749
+ log_and_print "${BOLD}║ 📋 最终总结已生成 ║${NC}"
750
+ log_and_print "${BOLD}╚══════════════════════════════════════════╝${NC}"
751
+ log_and_print " 📄 文件: ${BOLD}${summary_file}${NC}"
752
+ log_and_print " 🔨 裁判: ${ref_base}"
753
+ log_and_print ""
754
+ }
755
+
756
+ # ======================== 命令: run ========================
757
+ cmd_run() {
758
+ local agents="$DEFAULT_AGENTS"
759
+ local max_rounds="$DEFAULT_MAX_ROUNDS"
760
+ local god_mode=false
761
+ local referee_mode=false
762
+ local referee_agent="" # 空=使用第一个 agent
763
+
764
+ # 解析参数
765
+ while [[ $# -gt 0 ]]; do
766
+ case "$1" in
767
+ --agents|-a) agents="$2"; shift 2 ;;
768
+ --rounds|-r) max_rounds="$2"; shift 2 ;;
769
+ --god|-g) god_mode=true; shift ;;
770
+ --referee) referee_mode=true
771
+ # 可选参数: --referee [agent_name]
772
+ if [[ $# -gt 1 && ! "$2" =~ ^-- ]]; then
773
+ referee_agent="$2"; shift
774
+ fi
775
+ shift ;;
776
+ *) echo -e "${RED}未知参数: $1${NC}"; cmd_help; exit 1 ;;
777
+ esac
778
+ done
779
+
780
+ # ---- 阶段 0: 裁判提示词检查 ----
781
+ local referee_custom_prompt=""
782
+ if $referee_mode; then
783
+ if [ -f "$REFEREE_PROMPT_FILE" ] && [ -s "$REFEREE_PROMPT_FILE" ]; then
784
+ referee_custom_prompt=$(cat "$REFEREE_PROMPT_FILE")
785
+ echo -e "${CYAN}📋 已加载裁判提示词: ${REFEREE_PROMPT_FILE}${NC}"
786
+ else
787
+ echo -e "${YELLOW}⚠️ 未找到裁判提示词文件: ${REFEREE_PROMPT_FILE}${NC}"
788
+ echo -e " 你可以创建该文件来自定义裁判的行为,例如:"
789
+ echo -e " ${CYAN}echo '重点关注架构设计的可行性和成本' > ${REFEREE_PROMPT_FILE}${NC}"
790
+ echo ""
791
+ echo -ne "${BOLD}是否跳过,使用默认裁判提示词?[Y/n]: ${NC}"
792
+ local ref_choice
793
+ read -er ref_choice
794
+ case "$ref_choice" in
795
+ n|N|no|NO)
796
+ echo -e "${CYAN}请创建 ${REFEREE_PROMPT_FILE} 后重新运行。${NC}"
797
+ exit 0
798
+ ;;
799
+ *)
800
+ echo -e "${CYAN}使用默认裁判提示词。${NC}"
801
+ ;;
802
+ esac
803
+ echo ""
804
+ fi
805
+ fi
806
+
807
+ # ---- 阶段 1: 初始化 ----
808
+
809
+ # 检查是否存在历史讨论记录
810
+ local resume_mode=false
811
+ if [ -f "$CONFIG_FILE" ] || [ -d "$ROUNDS_DIR" ] && [ "$(ls -A "$ROUNDS_DIR" 2>/dev/null)" ]; then
812
+ local prev_status=""
813
+ local prev_round=0
814
+ local prev_agents=""
815
+ local prev_max_rounds=0
816
+ if [ -f "$CONFIG_FILE" ]; then
817
+ prev_status=$(jq -r '.status // "unknown"' "$CONFIG_FILE" 2>/dev/null)
818
+ prev_round=$(jq -r '.current_round // 0' "$CONFIG_FILE" 2>/dev/null)
819
+ prev_agents=$(jq -r '.agents // ""' "$CONFIG_FILE" 2>/dev/null)
820
+ prev_max_rounds=$(jq -r '.max_rounds // 0' "$CONFIG_FILE" 2>/dev/null)
821
+ fi
822
+ local round_count=0
823
+ if [ -d "$ROUNDS_DIR" ]; then
824
+ round_count=$(ls -1 "$ROUNDS_DIR"/*.md 2>/dev/null | wc -l | xargs)
825
+ fi
826
+
827
+ echo -e "${YELLOW}⚠️ 检测到历史讨论记录${NC}"
828
+ echo -e " 状态: ${BOLD}${prev_status}${NC}"
829
+ echo -e " 轮次: ${BOLD}${prev_round}/${prev_max_rounds}${NC}"
830
+ echo -e " 参与: ${BOLD}${prev_agents}${NC}"
831
+ echo -e " 文件: ${round_count} 个回复记录"
832
+ echo ""
833
+ echo -e " ${BOLD}1)${NC} 清理历史,重新开始"
834
+ # 仅当配置完整且有历史轮次时,才提供继续选项
835
+ if [ -n "$prev_agents" ] && [ "$prev_round" -gt 0 ] 2>/dev/null; then
836
+ echo -e " ${BOLD}2)${NC} 从 Round $((prev_round + 1)) 继续讨论"
837
+ echo -e " ${BOLD}3)${NC} 取消执行"
838
+ echo ""
839
+ echo -ne "${BOLD}请选择 [1/2/3]: ${NC}"
840
+ else
841
+ echo -e " ${BOLD}2)${NC} 取消执行"
842
+ echo ""
843
+ echo -ne "${BOLD}请选择 [1/2]: ${NC}"
844
+ fi
845
+ local choice
846
+ read -er choice
847
+
848
+ case "$choice" in
849
+ 1)
850
+ echo -e "${CYAN}清理历史记录...${NC}"
851
+ rm -rf "$ROUNDS_DIR" "$SESSIONS_DIR" "$CONFIG_FILE" "$CONSENSUS_FILE" "$LOG_FILE" SUMMARY.md
852
+ ;;
853
+ 2)
854
+ # 仅当有完整配置时 2=继续,否则 2=取消
855
+ if [ -n "$prev_agents" ] && [ "$prev_round" -gt 0 ] 2>/dev/null; then
856
+ resume_mode=true
857
+ # 恢复历史 agents(必须一致),max_rounds 使用命令行参数
858
+ agents="$prev_agents"
859
+ echo -e "${CYAN}从 Round $((prev_round + 1)) 继续讨论 (最大轮次: ${max_rounds})...${NC}"
860
+ else
861
+ echo -e "${CYAN}已取消。${NC}"
862
+ exit 0
863
+ fi
864
+ ;;
865
+ *)
866
+ echo -e "${CYAN}已取消。${NC}"
867
+ exit 0
868
+ ;;
869
+ esac
870
+ echo ""
871
+ fi
872
+
873
+ # 检查 problem.md
874
+ if [ ! -f "$PROBLEM_FILE" ]; then
875
+ echo -e "${RED}❌ 当前目录缺少 $PROBLEM_FILE${NC}"
876
+ echo -e "${CYAN}请先创建讨论问题:${NC}"
877
+ echo -e " echo \"你的问题\" > $PROBLEM_FILE"
878
+ exit 1
879
+ fi
880
+
881
+ if [ ! -s "$PROBLEM_FILE" ]; then
882
+ echo -e "${RED}❌ $PROBLEM_FILE 为空${NC}"
883
+ exit 1
884
+ fi
885
+
886
+ local problem
887
+ problem=$(cat "$PROBLEM_FILE")
888
+
889
+ echo -e "${BOLD}🔍 检查 Agent 可用性...${NC}"
890
+ echo ""
891
+
892
+ # 解析 agent 列表,支持重复 agent(如 claude,claude)
893
+ IFS=',' read -ra RAW_AGENT_LIST <<< "$agents"
894
+ local available_agents=()
895
+
896
+ # 统计每个 agent 类型出现次数(bash 3.x 兼容,用字符串模拟)
897
+ # 格式: "|name:count|name:count|"
898
+ local _agent_total="|"
899
+ for agent in "${RAW_AGENT_LIST[@]}"; do
900
+ agent=$(echo "$agent" | xargs) # trim
901
+ local _cur_total
902
+ _cur_total=$(echo "$_agent_total" | sed -n "s/.*|${agent}:\([0-9]*\)|.*/\1/p")
903
+ _cur_total=$(( ${_cur_total:-0} + 1 ))
904
+ if echo "$_agent_total" | grep -q "|${agent}:"; then
905
+ _agent_total=$(echo "$_agent_total" | sed "s/|${agent}:[0-9]*|/|${agent}:${_cur_total}|/")
906
+ else
907
+ _agent_total="${_agent_total}${agent}:${_cur_total}|"
908
+ fi
909
+ done
910
+
911
+ # ---- Phase 1: 去重收集 agent 类型 + 注册验证 ----
912
+ local unique_agents="|" # 需要检查的不同 agent 类型(去重)
913
+
914
+ for agent in "${RAW_AGENT_LIST[@]}"; do
915
+ agent=$(echo "$agent" | xargs) # trim
916
+
917
+ # 检查是否已注册
918
+ local registered=false
919
+ for ra in "${REGISTERED_AGENTS[@]}"; do
920
+ if [ "$ra" == "$agent" ]; then
921
+ registered=true
922
+ break
923
+ fi
924
+ done
925
+
926
+ if ! $registered; then
927
+ echo -e " ${RED}✗ $agent — 未注册的 Agent${NC}"
928
+ echo -e " 已注册: ${REGISTERED_AGENTS[*]}"
929
+ exit 1
930
+ fi
931
+
932
+ # 收集不重复的 agent 类型
933
+ if ! echo "$unique_agents" | grep -q "|${agent}|"; then
934
+ unique_agents="${unique_agents}${agent}|"
935
+ fi
936
+ done
937
+
938
+ # ---- Phase 2: 并发检查所有不同类型的可用性 ----
939
+ # 为每个 agent 类型创建状态文件,后台并发执行
940
+ local check_pids=()
941
+ local check_agents=()
942
+ local check_status_dir
943
+ check_status_dir=$(mktemp -d)
944
+
945
+ # 从 unique_agents 字符串解析出 agent 列表
946
+ local _ua_list
947
+ _ua_list=$(echo "$unique_agents" | tr '|' '\n' | sed '/^$/d')
948
+
949
+ while IFS= read -r agent; do
950
+ [ -z "$agent" ] && continue
951
+ check_agents+=("$agent")
952
+ # 后台执行可用性检查,结果写入状态文件
953
+ (
954
+ if "check_$agent" 2>"${check_status_dir}/${agent}.err"; then
955
+ echo "ok" > "${check_status_dir}/${agent}.status"
956
+ else
957
+ echo "fail" > "${check_status_dir}/${agent}.status"
958
+ fi
959
+ ) &
960
+ check_pids+=($!)
961
+ done <<< "$_ua_list"
962
+
963
+ echo -ne " ${CYAN}并发检查 ${#check_agents[@]} 个 Agent...${NC}"
964
+
965
+ # 等待所有检查完成
966
+ for pid in "${check_pids[@]}"; do
967
+ wait "$pid" 2>/dev/null || true
968
+ done
969
+ echo "" # 检查完成后换行
970
+
971
+ # 汇总检查结果
972
+ for agent in "${check_agents[@]}"; do
973
+ local status
974
+ status=$(cat "${check_status_dir}/${agent}.status" 2>/dev/null || echo "fail")
975
+ if [ "$status" = "ok" ]; then
976
+ echo -e " ${BOLD}$agent${NC} ${GREEN}✓ 可用${NC}"
977
+ else
978
+ echo -e " ${BOLD}$agent${NC} ${RED}✗ 不可用${NC}"
979
+ # 显示错误信息
980
+ if [ -f "${check_status_dir}/${agent}.err" ] && [ -s "${check_status_dir}/${agent}.err" ]; then
981
+ cat "${check_status_dir}/${agent}.err" >&2
982
+ fi
983
+ rm -rf "$check_status_dir"
984
+ exit 1
985
+ fi
986
+ done
987
+ rm -rf "$check_status_dir"
988
+
989
+ # ---- Phase 3: 生成实例名 ----
990
+ local _agent_count="|"
991
+ for agent in "${RAW_AGENT_LIST[@]}"; do
992
+ agent=$(echo "$agent" | xargs) # trim
993
+
994
+ # 生成实例名:有重复则添加 _1, _2 后缀
995
+ local _cur_count
996
+ _cur_count=$(echo "$_agent_count" | sed -n "s/.*|${agent}:\([0-9]*\)|.*/\1/p")
997
+ _cur_count=$(( ${_cur_count:-0} + 1 ))
998
+ if echo "$_agent_count" | grep -q "|${agent}:"; then
999
+ _agent_count=$(echo "$_agent_count" | sed "s/|${agent}:[0-9]*|/|${agent}:${_cur_count}|/")
1000
+ else
1001
+ _agent_count="${_agent_count}${agent}:${_cur_count}|"
1002
+ fi
1003
+
1004
+ local instance_name="$agent"
1005
+ local _total
1006
+ _total=$(echo "$_agent_total" | sed -n "s/.*|${agent}:\([0-9]*\)|.*/\1/p")
1007
+ if [ "${_total:-1}" -gt 1 ]; then
1008
+ instance_name="${agent}_${_cur_count}"
1009
+ fi
1010
+
1011
+ available_agents+=("$instance_name")
1012
+ done
1013
+
1014
+ echo ""
1015
+
1016
+ if [ ${#available_agents[@]} -lt 2 ]; then
1017
+ echo -e "${RED}❌ 至少需要 2 个可用 Agent 才能进行讨论${NC}"
1018
+ exit 1
1019
+ fi
1020
+
1021
+ # 创建 rounds 目录
1022
+ mkdir -p "$ROUNDS_DIR"
1023
+ mkdir -p "$SESSIONS_DIR"
1024
+
1025
+ # 动态生成各 Agent 的指令文件(新建和恢复模式都需要更新)
1026
+ for agent in "${available_agents[@]}"; do
1027
+ local md_file base
1028
+ md_file=$(agent_md_file "$agent")
1029
+ base=$(agent_base "$agent")
1030
+ "generate_${base}_md" "$max_rounds" "$problem" > "$md_file"
1031
+ if $resume_mode; then
1032
+ echo -e " ${CYAN}🔄 更新 $md_file (轮次: $max_rounds)${NC}"
1033
+ else
1034
+ echo -e " ${CYAN}📝 生成 $md_file${NC}"
1035
+ fi
1036
+ done
1037
+
1038
+ # 初始化 git(codex 需要)
1039
+ if [ ! -d ".git" ]; then
1040
+ git init -q
1041
+ echo ".debate.log" >> .gitignore 2>/dev/null || true
1042
+ echo ".debate.json" >> .gitignore 2>/dev/null || true
1043
+ git add -A 2>/dev/null || true
1044
+ git commit -q -m "ai-battle: init" 2>/dev/null || true
1045
+ fi
1046
+
1047
+ # ---- 阶段 2: 开始讨论 ----
1048
+
1049
+ local agent_count=${#available_agents[@]}
1050
+
1051
+ local sys_prompt
1052
+ sys_prompt=$(build_system_prompt "$max_rounds")
1053
+
1054
+ # 上帝视角累积信息
1055
+ local god_context=""
1056
+
1057
+ # 构建 agents 显示标题
1058
+ local agents_display="${available_agents[0]}"
1059
+ for ((idx=1; idx<agent_count; idx++)); do
1060
+ agents_display+=" vs ${available_agents[$idx]}"
1061
+ done
1062
+
1063
+ echo ""
1064
+ log_and_print "${BOLD}╔══════════════════════════════════════════╗${NC}"
1065
+ log_and_print "${BOLD}║ AI 圆桌讨论: ${agents_display}${NC}"
1066
+ log_and_print "${BOLD}╚══════════════════════════════════════════╝${NC}"
1067
+ echo ""
1068
+ log_and_print " 📝 问题: $(head -1 "$PROBLEM_FILE")"
1069
+ log_and_print " 🤖 Agent: ${available_agents[*]}"
1070
+ log_and_print " 🔄 最大轮次: $max_rounds"
1071
+ if $god_mode; then
1072
+ log_and_print "${CYAN} 👁️ 上帝视角: 开启${NC}"
1073
+ fi
1074
+
1075
+ # 裁判 agent 解析: 默认使用第一个 available agent 的基础类型
1076
+ local referee_base=""
1077
+ if $referee_mode; then
1078
+ if [ -n "$referee_agent" ]; then
1079
+ # 验证指定的裁判 agent 是否已注册
1080
+ local ref_valid=false
1081
+ for ra in "${REGISTERED_AGENTS[@]}"; do
1082
+ if [ "$ra" == "$referee_agent" ]; then
1083
+ ref_valid=true
1084
+ break
1085
+ fi
1086
+ done
1087
+ if ! $ref_valid; then
1088
+ echo -e "${RED}❌ 裁判 agent '$referee_agent' 未注册${NC}"
1089
+ exit 1
1090
+ fi
1091
+ referee_base="$referee_agent"
1092
+ else
1093
+ referee_base=$(agent_base "${available_agents[0]}")
1094
+ fi
1095
+ log_and_print " 🔨 裁判: ${BOLD}${referee_base}${NC}"
1096
+ fi
1097
+
1098
+ log_and_print "${CYAN} 日志: tail -f $LOG_FILE${NC}"
1099
+ echo ""
1100
+
1101
+ # 使用普通数组存储每个 agent 的最新回复(索引与 available_agents 对应)
1102
+ local responses=()
1103
+
1104
+ # 恢复模式起始轮次(默认从 Round 2 开始)
1105
+ local round=2
1106
+
1107
+ if $resume_mode; then
1108
+ # ---- 恢复模式: 从历史记录恢复状态 ----
1109
+ local prev_round
1110
+ prev_round=$(jq -r '.current_round // 0' "$CONFIG_FILE" 2>/dev/null)
1111
+ round=$((prev_round + 1))
1112
+
1113
+ # 更新配置: 状态为 running,max_rounds 使用命令行参数
1114
+ jq --argjson m "$max_rounds" '.status = "running" | .max_rounds = $m' \
1115
+ "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" \
1116
+ && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1117
+
1118
+ log_and_print "${CYAN}🔄 恢复模式: 从 Round ${round} 继续${NC}"
1119
+ log_and_print "${CYAN} 加载历史回复 (Round ${prev_round})...${NC}"
1120
+
1121
+ # 从最近一轮的回复文件恢复 responses[] 数组
1122
+ for ((i=0; i<agent_count; i++)); do
1123
+ local agent="${available_agents[$i]}"
1124
+ local reply_file="$ROUNDS_DIR/round_${prev_round}_${agent}.md"
1125
+ if [ -f "$reply_file" ]; then
1126
+ responses[$i]=$(cat "$reply_file")
1127
+ log_and_print " ${GREEN}✓${NC} ${agent} Round ${prev_round} 回复已加载"
1128
+ else
1129
+ log_and_print "${RED}❌ 缺少 ${reply_file},无法恢复${NC}"
1130
+ exit 1
1131
+ fi
1132
+ done
1133
+
1134
+ # 恢复上帝视角历史信息
1135
+ local god_file="$ROUNDS_DIR/god_round_${prev_round}.md"
1136
+ if [ -f "$god_file" ]; then
1137
+ god_context=$(cat "$god_file")
1138
+ log_and_print " ${GREEN}✓${NC} 上帝视角 Round ${prev_round} 信息已加载"
1139
+ fi
1140
+
1141
+ echo ""
1142
+ else
1143
+ # ---- 正常模式: 写入配置并执行 Round 1 ----
1144
+
1145
+ # 写入配置(供恢复模式使用)
1146
+ jq -n \
1147
+ --arg agents "$(IFS=,; echo "${available_agents[*]}")" \
1148
+ --argjson max_rounds "$max_rounds" \
1149
+ --arg problem "$problem" \
1150
+ '{agents: $agents, max_rounds: $max_rounds, problem: $problem,
1151
+ status: "running", current_round: 0}' \
1152
+ > "$CONFIG_FILE"
1153
+
1154
+ # 清空日志
1155
+ : > "$LOG_FILE"
1156
+
1157
+ # ---- Round 1: 所有 agent 并行独立思考 ----
1158
+ log_and_print "${CYAN}⏳ Round 1: ${agent_count} 位参与者独立思考中...${NC}"
1159
+
1160
+ local tmp_files=()
1161
+ local pids=()
1162
+ for ((i=0; i<agent_count; i++)); do
1163
+ local agent="${available_agents[$i]}"
1164
+ local base
1165
+ base=$(agent_base "$agent")
1166
+ local tmp
1167
+ tmp=$(mktemp)
1168
+ tmp_files+=("$tmp")
1169
+ (
1170
+ "call_${base}" "$sys_prompt" "请对以下问题给出你的深入分析:
1171
+
1172
+ $problem" "round_1_${agent}" > "$tmp" 2>/dev/null
1173
+ ) &
1174
+ pids+=($!)
1175
+ done
1176
+
1177
+ # 等待全部完成
1178
+ for pid in "${pids[@]}"; do
1179
+ wait "$pid" 2>/dev/null || true
1180
+ done
1181
+
1182
+ # 读取所有回复,失败的 agent 交互式重试
1183
+ for ((i=0; i<agent_count; i++)); do
1184
+ local agent="${available_agents[$i]}"
1185
+ local base
1186
+ base=$(agent_base "$agent")
1187
+ local color
1188
+ color=$(agent_color "$agent")
1189
+ if [ -s "${tmp_files[$i]}" ]; then
1190
+ responses[$i]=$(cat "${tmp_files[$i]}")
1191
+ echo "${responses[$i]}" > "$ROUNDS_DIR/round_1_${agent}.md"
1192
+ log_and_print ""
1193
+ log_and_print "${color}━━━ ${agent} [Round 1/${max_rounds}] ━━━${NC}"
1194
+ log_and_print "${color}${responses[$i]}${NC}"
1195
+ else
1196
+ log_and_print "${YELLOW}⚠️ ${agent} Round 1 并发调用失败,进入交互式重试...${NC}"
1197
+ responses[$i]=$(retry_call_agent "${agent} [Round 1]" "call_${base}" "$sys_prompt" "请对以下问题给出你的深入分析:
1198
+
1199
+ $problem" "round_1_${agent}")
1200
+ if [ -n "${responses[$i]}" ]; then
1201
+ echo "${responses[$i]}" > "$ROUNDS_DIR/round_1_${agent}.md"
1202
+ log_and_print ""
1203
+ log_and_print "${color}━━━ ${agent} [Round 1/${max_rounds}] ━━━${NC}"
1204
+ log_and_print "${color}${responses[$i]}${NC}"
1205
+ else
1206
+ log_and_print "${YELLOW}⏭️ ${agent} Round 1 已跳过${NC}"
1207
+ fi
1208
+ fi
1209
+ rm -f "${tmp_files[$i]}"
1210
+ done
1211
+
1212
+ log_and_print ""
1213
+ log_and_print "${CYAN}✅ Round 1 完成${NC}"
1214
+
1215
+ # 检查 Round 1 共识(所有人都 AGREED 才算)
1216
+ local all_agreed_r1=true
1217
+ for ((i=0; i<agent_count; i++)); do
1218
+ if ! has_agreed "${responses[$i]}"; then
1219
+ all_agreed_r1=false
1220
+ break
1221
+ fi
1222
+ done
1223
+ if $all_agreed_r1; then
1224
+ local conclusion
1225
+ conclusion=$(extract_agreed "${responses[0]}")
1226
+ finish_consensus "$conclusion" 1 "$max_rounds"
1227
+ $referee_mode && generate_final_summary "$referee_base" "$referee_custom_prompt"
1228
+ exit 0
1229
+ fi
1230
+
1231
+ # 上帝视角: Round 1 后注入
1232
+ if $god_mode; then
1233
+ god_context=$(god_input 1)
1234
+ fi
1235
+ fi
1236
+
1237
+ # 恢复模式 + 上帝视角: 判断上次是否在上帝视角阶段退出
1238
+ if $resume_mode && $god_mode; then
1239
+ local prev_round_num=$((round - 1))
1240
+ local god_file="$ROUNDS_DIR/god_round_${prev_round_num}.md"
1241
+ if [ ! -f "$god_file" ]; then
1242
+ # 上次在上帝视角阶段退出,重新触发
1243
+ log_and_print "${CYAN}👁️ 恢复上帝视角 (Round ${prev_round_num})${NC}"
1244
+ god_context=$(god_input "$prev_round_num")
1245
+ fi
1246
+ # 文件已存在的情况,god_context 在上面恢复阶段已加载
1247
+ fi
1248
+
1249
+ # ---- Round 2+: N 方顺序对话 ----
1250
+ # 提取为函数逻辑,供主循环和追加轮次复用
1251
+ while [ "$round" -le "$max_rounds" ]; do
1252
+ local remaining=$((max_rounds - round))
1253
+
1254
+ # 每个 agent 依次发言,看到所有其他 agent 的上一轮回复
1255
+ for ((i=0; i<agent_count; i++)); do
1256
+ local agent="${available_agents[$i]}"
1257
+ local base
1258
+ base=$(agent_base "$agent")
1259
+ local color
1260
+ color=$(agent_color "$agent")
1261
+
1262
+ log_and_print ""
1263
+ log_and_print "${CYAN}⏳ Round $round: ${agent} 思考中...${NC}"
1264
+
1265
+ # 构建其他 agent 回复的 XML 块
1266
+ local others_responses=""
1267
+ for ((j=0; j<agent_count; j++)); do
1268
+ if [ "$j" != "$i" ]; then
1269
+ local other="${available_agents[$j]}"
1270
+ others_responses+="<${other}_response>
1271
+ ${responses[$j]}
1272
+ </${other}_response>
1273
+
1274
+ "
1275
+ fi
1276
+ done
1277
+
1278
+ local prompt="以下是其他参与者的上一轮回复:\n\n${others_responses}请回应以上观点。剩余 $remaining 轮。"
1279
+ if [ -n "$god_context" ]; then
1280
+ prompt+="\n\n[上帝视角 - 额外补充信息]\n${god_context}"
1281
+ fi
1282
+
1283
+ responses[$i]=$(retry_call_agent "${agent} [Round ${round}]" "call_${base}" "$sys_prompt" "$prompt" "round_${round}_${agent}")
1284
+ if [ -z "${responses[$i]}" ]; then
1285
+ log_and_print "${YELLOW}⏭️ ${agent} Round ${round} 已跳过${NC}"
1286
+ continue
1287
+ fi
1288
+ echo "${responses[$i]}" > "$ROUNDS_DIR/round_${round}_${agent}.md"
1289
+ log_and_print ""
1290
+ log_and_print "${color}━━━ ${agent} [Round ${round}/${max_rounds}] ━━━${NC}"
1291
+ log_and_print "${color}${responses[$i]}${NC}"
1292
+
1293
+ # 检查该 agent 是否提出 AGREED
1294
+ if has_agreed "${responses[$i]}"; then
1295
+ local conclusion
1296
+ conclusion=$(extract_agreed "${responses[$i]}")
1297
+ log_and_print "${CYAN}${agent} 提出共识,等待其他 agent 确认...${NC}"
1298
+
1299
+ # 向所有其他 agent 发送确认请求
1300
+ local all_confirmed=true
1301
+ for ((k=0; k<agent_count; k++)); do
1302
+ if [ "$k" != "$i" ]; then
1303
+ local other="${available_agents[$k]}"
1304
+ local other_base
1305
+ other_base=$(agent_base "$other")
1306
+ local other_color
1307
+ other_color=$(agent_color "$other")
1308
+
1309
+ local confirm
1310
+ confirm=$(retry_call_agent "${other} [确认]" "call_${other_base}" "$sys_prompt" "${agent} 提出了共识:
1311
+
1312
+ <proposed_consensus>
1313
+ $conclusion
1314
+ </proposed_consensus>
1315
+
1316
+ 请审查这个结论是否完整正确。如果同意,在回复末尾写 AGREED: <结论>。如果不同意,说明原因。" "round_${round}_${other}_confirm")
1317
+ if [ -z "$confirm" ]; then
1318
+ log_and_print "${YELLOW}⏭️ ${other} 确认已跳过,视为不同意${NC}"
1319
+ all_confirmed=false
1320
+ break
1321
+ fi
1322
+ echo "$confirm" > "$ROUNDS_DIR/round_${round}_${other}_confirm.md"
1323
+ log_and_print "${other_color}━━━ ${other} [确认] ━━━${NC}"
1324
+ log_and_print "${other_color}${confirm}${NC}"
1325
+
1326
+ if has_agreed "$confirm"; then
1327
+ log_and_print "${CYAN} ✓ ${other} 同意${NC}"
1328
+ else
1329
+ log_and_print "${YELLOW} ✗ ${other} 不同意,讨论继续${NC}"
1330
+ responses[$k]="$confirm" # 更新该 agent 的回复供后续轮次使用
1331
+ all_confirmed=false
1332
+ break
1333
+ fi
1334
+ fi
1335
+ done
1336
+
1337
+ if $all_confirmed; then
1338
+ local final
1339
+ final=$(extract_agreed "${responses[$i]}")
1340
+ finish_consensus "$final" "$round" "$max_rounds"
1341
+ $referee_mode && generate_final_summary "$referee_base" "$referee_custom_prompt"
1342
+ exit 0
1343
+ fi
1344
+ fi
1345
+ done
1346
+
1347
+ # ---- 裁判总结: 所有 agent 发言完毕后 ----
1348
+ if $referee_mode; then
1349
+ log_and_print ""
1350
+ log_and_print "${BOLD}\033[1;37m━━━ 🔨 裁判总结 [Round ${round}/${max_rounds}] ━━━${NC}"
1351
+ log_and_print "${CYAN}⏳ 裁判分析中...${NC}"
1352
+
1353
+ # 构建所有 agent 回复的汇总
1354
+ local all_responses=""
1355
+ for ((ri=0; ri<agent_count; ri++)); do
1356
+ local ra="${available_agents[$ri]}"
1357
+ all_responses+="<${ra}_response>
1358
+ ${responses[$ri]}
1359
+ </${ra}_response>
1360
+
1361
+ "
1362
+ done
1363
+
1364
+ # 根据模式选择裁判 prompt
1365
+ local ref_prompt
1366
+ if $god_mode; then
1367
+ ref_prompt=$(build_referee_god_prompt)
1368
+ else
1369
+ ref_prompt=$(build_referee_free_prompt)
1370
+ fi
1371
+ # 追加自定义裁判提示词
1372
+ if [ -n "$referee_custom_prompt" ]; then
1373
+ ref_prompt+=$'\n\n[额外指示]\n'"$referee_custom_prompt"
1374
+ fi
1375
+
1376
+ local referee_result
1377
+ referee_result=$(call_referee "$referee_base" "$ref_prompt" "以下是 Round ${round} 各参与者的回复:
1378
+
1379
+ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1380
+
1381
+ # 保存裁判结果
1382
+ echo "$referee_result" > "$ROUNDS_DIR/referee_round_${round}.md"
1383
+ log_and_print "${BOLD}\033[1;37m${referee_result}${NC}"
1384
+
1385
+ # 自由辩论模式: 裁判检测共识
1386
+ if ! $god_mode; then
1387
+ if echo "$referee_result" | grep -qiE 'CONSENSUS:[[:space:]]*YES'; then
1388
+ local ref_conclusion
1389
+ ref_conclusion=$(echo "$referee_result" | grep -ioE '\*{0,2}AGREED:[[:space:]]*.*' | head -1 \
1390
+ | sed 's/^\*\{0,2\}[Aa][Gg][Rr][Ee][Ee][Dd]:[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/\*\{1,2\}$//')
1391
+ if [ -n "$ref_conclusion" ]; then
1392
+ log_and_print "${CYAN}🔨 裁判判定: 已达成共识${NC}"
1393
+ finish_consensus "$ref_conclusion" "$round" "$max_rounds"
1394
+ generate_final_summary "$referee_base" "$referee_custom_prompt"
1395
+ exit 0
1396
+ fi
1397
+ fi
1398
+ fi
1399
+ fi
1400
+
1401
+ # 更新配置
1402
+ jq --argjson r "$round" '.current_round = $r | .status = "running"' \
1403
+ "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1404
+
1405
+ # 上帝视角: 每轮结束后注入(裁判总结后再让 god 输入)
1406
+ if $god_mode && [ "$round" -lt "$max_rounds" ]; then
1407
+ god_context=$(god_input "$round")
1408
+ fi
1409
+
1410
+ round=$((round + 1))
1411
+ done
1412
+
1413
+ # 超过最大轮次,询问是否追加
1414
+ while true; do
1415
+ log_and_print ""
1416
+ log_and_print "${YELLOW}╔════════════════════════════════════════╗${NC}"
1417
+ log_and_print "${YELLOW}║ 讨论结束,未达成共识 ║${NC}"
1418
+ log_and_print "${YELLOW}╚════════════════════════════════════════╝${NC}"
1419
+ log_and_print "${CYAN}已完成 $((round - 1)) 轮,所有回复保存在: $ROUNDS_DIR/${NC}"
1420
+
1421
+ # 交互式询问是否追加轮次
1422
+ local extra_rounds=""
1423
+ echo ""
1424
+ echo -ne "${BOLD}是否追加讨论轮次?输入轮次数(默认 ${max_rounds},0 或回车结束): ${NC}"
1425
+ read -er extra_rounds
1426
+
1427
+ # 空输入或 0 表示结束
1428
+ if [ -z "$extra_rounds" ] || [ "$extra_rounds" = "0" ]; then
1429
+ log_and_print "${CYAN}讨论结束。${NC}"
1430
+ jq '.status = "no_consensus"' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" \
1431
+ && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1432
+ $referee_mode && generate_final_summary "$referee_base" "$referee_custom_prompt"
1433
+ return
1434
+ fi
1435
+
1436
+ # 验证输入为正整数
1437
+ if ! [[ "$extra_rounds" =~ ^[1-9][0-9]*$ ]]; then
1438
+ echo -e "${RED}请输入正整数${NC}"
1439
+ continue
1440
+ fi
1441
+
1442
+ local new_max=$((round - 1 + extra_rounds))
1443
+ max_rounds="$new_max"
1444
+ log_and_print "${CYAN}追加 $extra_rounds 轮,总轮次上限: $max_rounds${NC}"
1445
+ echo ""
1446
+
1447
+ # 更新配置
1448
+ jq --argjson m "$max_rounds" '.max_rounds = $m' \
1449
+ "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1450
+
1451
+ # 继续讨论循环(逻辑与上方 Round 2+ 完全相同)
1452
+ while [ "$round" -le "$max_rounds" ]; do
1453
+ local remaining=$((max_rounds - round))
1454
+
1455
+ for ((i=0; i<agent_count; i++)); do
1456
+ local agent="${available_agents[$i]}"
1457
+ local base
1458
+ base=$(agent_base "$agent")
1459
+ local color
1460
+ color=$(agent_color "$agent")
1461
+
1462
+ log_and_print ""
1463
+ log_and_print "${CYAN}⏳ Round $round: ${agent} 思考中...${NC}"
1464
+
1465
+ # 构建其他 agent 回复的 XML 块
1466
+ local others_responses=""
1467
+ for ((j=0; j<agent_count; j++)); do
1468
+ if [ "$j" != "$i" ]; then
1469
+ local other="${available_agents[$j]}"
1470
+ others_responses+="<${other}_response>
1471
+ ${responses[$j]}
1472
+ </${other}_response>
1473
+
1474
+ "
1475
+ fi
1476
+ done
1477
+
1478
+ local prompt="以下是其他参与者的上一轮回复:\n\n${others_responses}请回应以上观点。剩余 $remaining 轮。"
1479
+ if [ -n "$god_context" ]; then
1480
+ prompt+="\n\n[上帝视角 - 额外补充信息]\n${god_context}"
1481
+ fi
1482
+
1483
+ responses[$i]=$(retry_call_agent "${agent} [Round ${round}]" "call_${base}" "$sys_prompt" "$prompt" "round_${round}_${agent}")
1484
+ if [ -z "${responses[$i]}" ]; then
1485
+ log_and_print "${YELLOW}⏭️ ${agent} Round ${round} 已跳过${NC}"
1486
+ continue
1487
+ fi
1488
+ echo "${responses[$i]}" > "$ROUNDS_DIR/round_${round}_${agent}.md"
1489
+ log_and_print ""
1490
+ log_and_print "${color}━━━ ${agent} [Round ${round}/${max_rounds}] ━━━${NC}"
1491
+ log_and_print "${color}${responses[$i]}${NC}"
1492
+
1493
+ # 检查该 agent 是否提出 AGREED
1494
+ if has_agreed "${responses[$i]}"; then
1495
+ local conclusion
1496
+ conclusion=$(extract_agreed "${responses[$i]}")
1497
+ log_and_print "${CYAN}${agent} 提出共识,等待其他 agent 确认...${NC}"
1498
+
1499
+ local all_confirmed=true
1500
+ for ((k=0; k<agent_count; k++)); do
1501
+ if [ "$k" != "$i" ]; then
1502
+ local other="${available_agents[$k]}"
1503
+ local other_base
1504
+ other_base=$(agent_base "$other")
1505
+ local other_color
1506
+ other_color=$(agent_color "$other")
1507
+
1508
+ local confirm
1509
+ confirm=$(retry_call_agent "${other} [确认]" "call_${other_base}" "$sys_prompt" "${agent} 提出了共识:
1510
+
1511
+ <proposed_consensus>
1512
+ $conclusion
1513
+ </proposed_consensus>
1514
+
1515
+ 请审查这个结论是否完整正确。如果同意,在回复末尾写 AGREED: <结论>。如果不同意,说明原因。" "round_${round}_${other}_confirm")
1516
+ if [ -z "$confirm" ]; then
1517
+ log_and_print "${YELLOW}⏭️ ${other} 确认已跳过,视为不同意${NC}"
1518
+ all_confirmed=false
1519
+ break
1520
+ fi
1521
+ echo "$confirm" > "$ROUNDS_DIR/round_${round}_${other}_confirm.md"
1522
+ log_and_print "${other_color}━━━ ${other} [确认] ━━━${NC}"
1523
+ log_and_print "${other_color}${confirm}${NC}"
1524
+
1525
+ if has_agreed "$confirm"; then
1526
+ log_and_print "${CYAN} ✓ ${other} 同意${NC}"
1527
+ else
1528
+ log_and_print "${YELLOW} ✗ ${other} 不同意,讨论继续${NC}"
1529
+ responses[$k]="$confirm"
1530
+ all_confirmed=false
1531
+ break
1532
+ fi
1533
+ fi
1534
+ done
1535
+
1536
+ if $all_confirmed; then
1537
+ local final
1538
+ final=$(extract_agreed "${responses[$i]}")
1539
+ finish_consensus "$final" "$round" "$max_rounds"
1540
+ $referee_mode && generate_final_summary "$referee_base" "$referee_custom_prompt"
1541
+ exit 0
1542
+ fi
1543
+ fi
1544
+ done
1545
+
1546
+ # ---- 裁判总结: 追加轮次中同样执行 ----
1547
+ if $referee_mode; then
1548
+ log_and_print ""
1549
+ log_and_print "${BOLD}\033[1;37m━━━ 🔨 裁判总结 [Round ${round}/${max_rounds}] ━━━${NC}"
1550
+ log_and_print "${CYAN}⏳ 裁判分析中...${NC}"
1551
+
1552
+ local all_responses=""
1553
+ for ((ri=0; ri<agent_count; ri++)); do
1554
+ local ra="${available_agents[$ri]}"
1555
+ all_responses+="<${ra}_response>
1556
+ ${responses[$ri]}
1557
+ </${ra}_response>
1558
+
1559
+ "
1560
+ done
1561
+
1562
+ local ref_prompt
1563
+ if $god_mode; then
1564
+ ref_prompt=$(build_referee_god_prompt)
1565
+ else
1566
+ ref_prompt=$(build_referee_free_prompt)
1567
+ fi
1568
+ # 追加自定义裁判提示词
1569
+ if [ -n "$referee_custom_prompt" ]; then
1570
+ ref_prompt+=$'\n\n[额外指示]\n'"$referee_custom_prompt"
1571
+ fi
1572
+
1573
+ local referee_result
1574
+ referee_result=$(call_referee "$referee_base" "$ref_prompt" "以下是 Round ${round} 各参与者的回复:
1575
+
1576
+ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1577
+
1578
+ echo "$referee_result" > "$ROUNDS_DIR/referee_round_${round}.md"
1579
+ log_and_print "${BOLD}\033[1;37m${referee_result}${NC}"
1580
+
1581
+ if ! $god_mode; then
1582
+ if echo "$referee_result" | grep -qiE 'CONSENSUS:[[:space:]]*YES'; then
1583
+ local ref_conclusion
1584
+ ref_conclusion=$(echo "$referee_result" | grep -ioE '\*{0,2}AGREED:[[:space:]]*.*' | head -1 \
1585
+ | sed 's/^\*\{0,2\}[Aa][Gg][Rr][Ee][Ee][Dd]:[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/\*\{1,2\}$//')
1586
+ if [ -n "$ref_conclusion" ]; then
1587
+ log_and_print "${CYAN}🔨 裁判判定: 已达成共识${NC}"
1588
+ finish_consensus "$ref_conclusion" "$round" "$max_rounds"
1589
+ generate_final_summary "$referee_base" "$referee_custom_prompt"
1590
+ exit 0
1591
+ fi
1592
+ fi
1593
+ fi
1594
+ fi
1595
+
1596
+ # 更新配置
1597
+ jq --argjson r "$round" '.current_round = $r | .status = "running"' \
1598
+ "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1599
+
1600
+ # 上帝视角: 每轮结束后注入(裁判总结后再让 god 输入)
1601
+ if $god_mode && [ "$round" -lt "$max_rounds" ]; then
1602
+ god_context=$(god_input "$round")
1603
+ fi
1604
+
1605
+ round=$((round + 1))
1606
+ done
1607
+ done
1608
+ }
1609
+
1610
+ # 共识达成处理
1611
+ finish_consensus() {
1612
+ local conclusion="$1" round="$2" max_rounds="$3"
1613
+ echo "$conclusion" > "$CONSENSUS_FILE"
1614
+
1615
+ log_and_print ""
1616
+ log_and_print "${YELLOW}╔════════════════════════════════════════╗${NC}"
1617
+ log_and_print "${YELLOW}║ 🎉 达成共识! ║${NC}"
1618
+ log_and_print "${YELLOW}╚════════════════════════════════════════╝${NC}"
1619
+ log_and_print "${BOLD}结论: ${conclusion}${NC}"
1620
+ log_and_print "${CYAN}轮次: ${round}/${max_rounds}${NC}"
1621
+
1622
+ jq --arg c "$conclusion" '.status = "consensus" | .conclusion = $c' \
1623
+ "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1624
+ }
1625
+
1626
+
1627
+ # ======================== 命令: help ========================
1628
+ cmd_help() {
1629
+ cat << HELP
1630
+
1631
+ ai-battle — AI 圆桌讨论工具 (v${VERSION})
1632
+
1633
+ 让多个 AI Agent 对同一问题进行结构化讨论,自动管理轮次、
1634
+ 检测共识、保存全部讨论记录。
1635
+
1636
+ 前置条件:
1637
+ 1. 在当前目录创建 problem.md,写入讨论问题
1638
+ 2. 确保参与讨论的 Agent CLI 工具已安装并可用
1639
+
1640
+ 依赖: jq, bash 4+, (可选) claude, codex, gemini
1641
+
1642
+ 用法:
1643
+ ai-battle [options]
1644
+ ai-battle help
1645
+
1646
+ 参数:
1647
+ --agents, -a <a1,a2> 选择参与的 Agent(默认: claude,codex)
1648
+ 支持同类 Agent: --agents gemini,gemini
1649
+ --rounds, -r <N> 最大讨论轮次(默认: 10)
1650
+ --god, -g 开启上帝视角(每轮结束后可注入补充信息)
1651
+ --referee [agent] 开启裁判模式(每轮总结差异/检测共识,结束时生成最终总结)
1652
+ 可选指定裁判使用的 Agent(默认: 第一个参与者)
1653
+
1654
+ 已注册 Agent:
1655
+ claude 通过 Claude CLI 调用 (需 claude 命令)
1656
+ codex 通过 codex exec 调用 (需 codex 命令)
1657
+ gemini 通过 Gemini CLI 调用 (需 gemini 命令)
1658
+
1659
+ 使用示例:
1660
+ # 快速开始
1661
+ mkdir my-topic && cd my-topic
1662
+ echo "微服务 vs 单体架构的优缺点?" > problem.md
1663
+ ai-battle --agents claude,gemini --rounds 8
1664
+
1665
+ # 同类 Agent 自我辩论
1666
+ ai-battle --agents gemini,gemini
1667
+
1668
+ # 三方圆桌讨论
1669
+ ai-battle --agents claude,codex,gemini --rounds 5
1670
+
1671
+ # 裁判模式(每轮总结 + 结束时生成 SUMMARY.md)
1672
+ ai-battle --agents claude,codex,gemini --referee --rounds 5
1673
+
1674
+ # 指定 claude 做裁判
1675
+ ai-battle --agents codex,gemini --referee claude --rounds 5
1676
+
1677
+ # 上帝视角(每轮可人工补充信息)
1678
+ ai-battle --agents claude,codex --god
1679
+
1680
+ # 裁判 + 上帝视角
1681
+ ai-battle --agents claude,codex --referee --god
1682
+
1683
+ 产出文件:
1684
+ rounds/ 每轮讨论记录 (round_N_<agent>.md)
1685
+ rounds/referee_*.md 裁判总结记录 (开启 --referee 时)
1686
+ rounds/god_*.md 上帝视角注入记录 (开启 --god 时)
1687
+ .sessions/ Agent CLI 原始输出记录 (stream-json/json/raw)
1688
+ consensus.md 共识结论(如达成)
1689
+ SUMMARY.md 最终总结 (裁判自动生成)
1690
+ .debate.log 完整日志 (可用 tail -f 实时查看)
1691
+
1692
+ 扩展 Agent:
1693
+ 在脚本中实现 check_<name>()、call_<name>()、generate_<name>_md()
1694
+ 然后调用 register_agent "<name>"
1695
+
1696
+ 配置文件:
1697
+ .env 自动加载环境变量 (启动时)
1698
+ referee.md 裁判自定义提示词 (开启 --referee 时)
1699
+
1700
+ 环境变量:
1701
+ Claude:
1702
+ ANTHROPIC_BASE_URL API 地址
1703
+ ANTHROPIC_AUTH_TOKEN 认证 Token
1704
+ ANTHROPIC_DEFAULT_SONNET_MODEL 模型名称
1705
+ API_TIMEOUT_MS 超时时间 (毫秒)
1706
+
1707
+ Codex:
1708
+ CODEX_MODEL 模型名称 (默认: gpt-5.3-codex)
1709
+
1710
+ Gemini:
1711
+ GEMINI_API_KEY API Key (如需自定义)
1712
+
1713
+ HELP
1714
+ }
1715
+
1716
+ # ======================== 主入口 ========================
1717
+ main() {
1718
+ # 处理 help/version 等无参数命令
1719
+ case "${1:-}" in
1720
+ help|--help|-h) cmd_help; exit 0 ;;
1721
+ --version|-v) echo "ai-battle v$VERSION"; exit 0 ;;
1722
+ esac
1723
+
1724
+ # 默认行为: 启动讨论
1725
+ cmd_run "$@"
1726
+ }
1727
+
1728
+ main "$@"