338 lines
13 KiB
Bash
Executable File
338 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
# tmux-team.sh — 管理 tmux 4 Pane 团队工作空间
|
||
#
|
||
# 使用方法:
|
||
# bash scripts/tmux-team.sh # 创建默认会话 isos-team
|
||
# bash scripts/tmux-team.sh <名称> # 创建指定名称的会话
|
||
# bash scripts/tmux-team.sh -k [名称] # 杀死会话 (默认 isos-team)
|
||
# bash scripts/tmux-team.sh -l # 列出所有会话
|
||
# bash scripts/tmux-team.sh -a [名称] # 附加到已有会话
|
||
# bash scripts/tmux-team.sh -h # 显示帮助
|
||
#
|
||
# 行为 (create):
|
||
# - 已在 tmux 中: 直接在当前窗口构建布局
|
||
# - 不在 tmux 中: 启动新 tmux 会话并附加
|
||
#
|
||
# 视觉布局:
|
||
# +----------+--------------------+--------------------+
|
||
# | | Pane 2 (后端) | Pane 3 (前端) |
|
||
# | Pane 1 +--------------------+--------------------+
|
||
# | (项目) | Pane 4 (测试) | Pane 5 (对话) |
|
||
# | | | |
|
||
# +----------+--------------------+--------------------+
|
||
|
||
set -euo pipefail
|
||
|
||
WORKSPACE="/workspace"
|
||
DEFAULT_SESSION="isos-team"
|
||
|
||
# ── 参数解析 ──────────────────────────────────────────────
|
||
|
||
ACTION="create"
|
||
SESSION_NAME=""
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
-k|--kill)
|
||
ACTION="kill"
|
||
shift
|
||
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
|
||
SESSION_NAME="$1"; shift
|
||
fi
|
||
;;
|
||
-l|--list)
|
||
ACTION="list"; shift
|
||
;;
|
||
-a|--attach)
|
||
ACTION="attach"
|
||
shift
|
||
if [ $# -gt 0 ] && [ "${1:0:1}" != "-" ]; then
|
||
SESSION_NAME="$1"; shift
|
||
fi
|
||
;;
|
||
-h|--help)
|
||
ACTION="help"; shift
|
||
;;
|
||
-*)
|
||
echo "未知选项: $1" >&2; exit 1
|
||
;;
|
||
*)
|
||
SESSION_NAME="$1"; shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
SESSION_NAME="${SESSION_NAME:-$DEFAULT_SESSION}"
|
||
|
||
# ── 前置检查 ──────────────────────────────────────────────
|
||
|
||
if ! command -v tmux &>/dev/null; then
|
||
echo "错误: tmux 未安装" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ── 简单操作: 直接执行后退出 ──────────────────────────────
|
||
|
||
case "$ACTION" in
|
||
help)
|
||
echo "用法: bash $(basename "$0") [选项] [会话名称]"
|
||
echo ""
|
||
echo "选项:"
|
||
echo " (空) 创建默认会话 ($DEFAULT_SESSION)"
|
||
echo " <名称> 创建指定名称的会话"
|
||
echo " -k [名称] 杀死会话 (默认 $DEFAULT_SESSION)"
|
||
echo " -l, --list 列出所有 tmux 会话"
|
||
echo " -a [名称] 附加到已有会话"
|
||
echo " -h, --help 显示帮助信息"
|
||
exit 0
|
||
;;
|
||
list)
|
||
if tmux ls 2>/dev/null; then true; else echo "无活跃 tmux 会话"; fi
|
||
exit 0
|
||
;;
|
||
kill)
|
||
if tmux kill-session -t "$SESSION_NAME" 2>/dev/null; then
|
||
echo "会话 '$SESSION_NAME' 已终止"
|
||
exit 0
|
||
else
|
||
echo "会话 '$SESSION_NAME' 不存在" >&2
|
||
exit 1
|
||
fi
|
||
;;
|
||
attach)
|
||
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||
exec tmux attach-session -t "$SESSION_NAME"
|
||
else
|
||
echo "会话 '$SESSION_NAME' 不存在" >&2
|
||
exit 1
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
# ══════════════════════════════════════════════════════════
|
||
# 以下为 create 操作
|
||
# ══════════════════════════════════════════════════════════
|
||
|
||
# ── 基准比例 (源自 229×64 设计) ──────────────────────────
|
||
# Pane 1 宽度: 68/229 ≈ 30%
|
||
# Pane 4 高度: 20/64 ≈ 31%
|
||
# Pane 3 宽度: 79/161 ≈ 49% (占右区域)
|
||
# 使用先乘后除避免 bash 整数截断误差
|
||
|
||
BASE_W=229
|
||
BASE_H=64
|
||
BASE_P1_W=68
|
||
BASE_P4_H=20
|
||
BASE_P3_W=79
|
||
BASE_RIGHT_W=$((BASE_W - BASE_P1_W)) # 161
|
||
|
||
# ── 获取终端尺寸 ──────────────────────────────────────────
|
||
|
||
get_terminal_size() {
|
||
if [ -n "${TMUX:-}" ]; then
|
||
TERM_W=$(tmux display-message -p '#{window_width}')
|
||
TERM_H=$(tmux display-message -p '#{window_height}')
|
||
else
|
||
TERM_W=$(tput cols)
|
||
TERM_H=$(tput lines)
|
||
fi
|
||
}
|
||
|
||
# ── 等比例缩放计算 ───────────────────────────────────────
|
||
|
||
calc_proportions() {
|
||
local w="$1" h="$2"
|
||
|
||
PANE1_W=$((w * BASE_P1_W / BASE_W))
|
||
PANE4_H=$((h * BASE_P4_H / BASE_H))
|
||
local right_w=$((w - PANE1_W))
|
||
PANE3_W=$((right_w * BASE_P3_W / BASE_RIGHT_W))
|
||
|
||
# 保底最小值,防止极端小窗口出现 0 尺寸
|
||
PANE1_W=$((PANE1_W < 40 ? 40 : PANE1_W))
|
||
PANE4_H=$((PANE4_H < 8 ? 8 : PANE4_H))
|
||
PANE3_W=$((PANE3_W < 30 ? 30 : PANE3_W))
|
||
}
|
||
|
||
# ── 等待 shell 就绪并发送命令 ─────────────────────────────
|
||
# 等待 pane 中的 shell 完成初始化(出现提示符),再发送命令
|
||
# 解决: 在非 tmux 环境下,新 session 的 shell 可能未加载 .bashrc 别名
|
||
|
||
send_command_to_pane() {
|
||
local pane="$1"
|
||
local cmd="$2"
|
||
local max_wait=15
|
||
|
||
# 等待 shell 提示符出现(说明 .bashrc 已加载,别名可用)
|
||
for i in $(seq 1 "$max_wait"); do
|
||
if tmux capture-pane -t "$pane" -p 2>/dev/null | tail -3 \
|
||
| grep -qE '(\$|#|❯|>)\s*$'; then
|
||
echo "[tmux-team] pane $pane shell 就绪 (等待 ${i}s)"
|
||
break
|
||
fi
|
||
sleep 0.5
|
||
done
|
||
|
||
# 分步发送: 先文本,再 Enter(避免 Enter 丢失)
|
||
tmux send-keys -t "$pane" "$cmd"
|
||
sleep 0.3
|
||
tmux send-keys -t "$pane" Enter
|
||
|
||
# 验证命令是否已提交
|
||
sleep 1
|
||
local verify="${cmd:0:10}"
|
||
for i in $(seq 1 3); do
|
||
local captured
|
||
captured=$(tmux capture-pane -t "$pane" -p | tail -10)
|
||
if echo "$captured" | grep -qF "$verify"; then
|
||
echo "[tmux-team] Enter 未生效,重试第 $i 次..."
|
||
tmux send-keys -t "$pane" Enter
|
||
sleep 1
|
||
else
|
||
return 0
|
||
fi
|
||
done
|
||
echo "[tmux-team] 警告: pane $pane 命令 '$cmd' 提交状态不确定"
|
||
}
|
||
|
||
# ── 构建布局 (通用函数) ───────────────────────────────────
|
||
# 参数: $1 = 初始 pane target, $2 = 宽, $3 = 高
|
||
|
||
build_layout() {
|
||
local base_pane="$1"
|
||
local w="$2" h="$3"
|
||
calc_proportions "$w" "$h"
|
||
|
||
# Step 1: 水平分屏 → Pane 1 (左) | 右区域
|
||
local right_id
|
||
right_id=$(tmux split-window -h -t "$base_pane" -c "$WORKSPACE" -P -F '#{pane_id}')
|
||
tmux resize-pane -t "$base_pane" -x "$PANE1_W"
|
||
|
||
# Step 2: 右区域垂直分出底部 Pane 4
|
||
local bottom_id
|
||
bottom_id=$(tmux split-window -v -t "$right_id" -l "$PANE4_H" -c "$WORKSPACE" -P -F '#{pane_id}')
|
||
|
||
# Step 3: 右上水平分为两列: Pane 2 | Pane 3
|
||
local right_col2_id
|
||
right_col2_id=$(tmux split-window -h -t "$right_id" -l "$PANE3_W" -c "$WORKSPACE" -P -F '#{pane_id}')
|
||
|
||
# Step 4: 底部水平分为两列: Pane 4 (测试) | Pane 5 (对话)
|
||
local monitor_id
|
||
monitor_id=$(tmux split-window -h -t "$bottom_id" -l "$PANE3_W" -c "$WORKSPACE" -P -F '#{pane_id}')
|
||
|
||
# ── 命名窗格 ──
|
||
tmux select-pane -t "$base_pane" -T "项目"
|
||
tmux select-pane -t "$right_id" -T "后端"
|
||
tmux select-pane -t "$right_col2_id" -T "前端"
|
||
tmux select-pane -t "$bottom_id" -T "测试"
|
||
tmux select-pane -t "$monitor_id" -T "对话"
|
||
|
||
# ── 启动 AI 编码工具(等待 shell 就绪后再发送,附带角色名称)──
|
||
local runner="${TOOL_RUNNER:-cc}"
|
||
send_command_to_pane "$base_pane" "${runner} --name \"项目\""
|
||
send_command_to_pane "$right_id" "${runner} --name \"后端\""
|
||
send_command_to_pane "$right_col2_id" "${runner} --name \"前端\""
|
||
send_command_to_pane "$bottom_id" "${runner} --name \"测试\""
|
||
|
||
# ── 发送角色提示词 ──
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
|
||
# 根据 runner 类型选择提示词目录
|
||
if [ "$runner" = "oc" ]; then
|
||
PROMPT_DIR="$(dirname "$SCRIPT_DIR")/.opencode/agents"
|
||
PROMPT_FILES=("isos-pm.md" "isos-backend.md" "isos-frontend.md" "isos-test.md")
|
||
else
|
||
PROMPT_DIR="$(dirname "$SCRIPT_DIR")/.claude/agents"
|
||
PROMPT_FILES=("isos-project-manager-agent.md" "isos-backend-agent.md" "isos-frontend-agent.md" "isos-test-agent.md")
|
||
fi
|
||
|
||
# 并行发送,tmux-send-prompt.sh 会等待各工具就绪
|
||
bash "$SCRIPT_DIR/tmux-send-prompt.sh" "$base_pane" "${PROMPT_DIR}/${PROMPT_FILES[0]}" &
|
||
bash "$SCRIPT_DIR/tmux-send-prompt.sh" "$right_id" "${PROMPT_DIR}/${PROMPT_FILES[1]}" &
|
||
bash "$SCRIPT_DIR/tmux-send-prompt.sh" "$right_col2_id" "${PROMPT_DIR}/${PROMPT_FILES[2]}" &
|
||
bash "$SCRIPT_DIR/tmux-send-prompt.sh" "$bottom_id" "${PROMPT_DIR}/${PROMPT_FILES[3]}" &
|
||
wait
|
||
|
||
# ── 启动 cc 实时监控 (对话 pane) ──
|
||
sleep 1
|
||
tmux send-keys -t "$monitor_id" "bash ${WORKSPACE}/scripts/tmux-cc-monitor.sh watch ${base_pane} ${right_id} ${right_col2_id} ${bottom_id}" Enter
|
||
|
||
# 选中主 Agent 窗格
|
||
tmux select-pane -t "$base_pane"
|
||
}
|
||
|
||
# ── 输出信息 ──────────────────────────────────────────────
|
||
|
||
print_info() {
|
||
echo ""
|
||
echo "窗格布局:"
|
||
echo " +----------+--------------------+--------------------+"
|
||
echo " | | Pane 2 (后端) | Pane 3 (前端) |"
|
||
echo " | Pane 1 +--------------------+--------------------+"
|
||
echo " | (项目) | Pane 4 (测试) | Pane 5 (对话) |"
|
||
echo " | | | |"
|
||
echo " +----------+--------------------+--------------------+"
|
||
echo ""
|
||
echo "角色分配:"
|
||
echo " Pane 1: 项目 — 项目管理、任务协调"
|
||
echo " Pane 2: 后端 — 服务端开发 (FastAPI + SQLite)"
|
||
echo " Pane 3: 前端 — 桌面端开发 (Svelte 5 + PyWebView)"
|
||
echo " Pane 4: 测试 — 全级别测试和覆盖率"
|
||
echo " Pane 5: 对话 — 实时监控各面板 cc 对话"
|
||
echo ""
|
||
echo "所有窗格已启动 AI 编码工具并加载角色提示词"
|
||
}
|
||
|
||
# ── 创建布局 ──────────────────────────────────────────────
|
||
|
||
if [ -n "${TMUX:-}" ]; then
|
||
# ── 情况 1: 已在 tmux 中 → 重命名会话并在当前窗口构建布局 ──
|
||
|
||
tmux rename-session -t "$(tmux display-message -p '#{session_name}')" "$SESSION_NAME" 2>/dev/null || true
|
||
tmux rename-window -t "$(tmux display-message -p '#{window_id}')" "ISOS-Team" 2>/dev/null || true
|
||
current_pane=$(tmux display-message -p '#{pane_id}')
|
||
|
||
# 检查当前窗口是否已有多个 pane
|
||
pane_count=$(tmux list-panes -t "$(tmux display-message -p '#{window_id}')" | wc -l)
|
||
if [ "$pane_count" -gt 1 ]; then
|
||
echo "当前窗口已有 ${pane_count} 个窗格,跳过布局创建"
|
||
echo "请先关闭多余窗格或切换到新窗口后再试"
|
||
exit 1
|
||
fi
|
||
|
||
get_terminal_size
|
||
build_layout "$current_pane" "$TERM_W" "$TERM_H"
|
||
echo "团队工作空间已在当前窗口创建 (${TERM_W}×${TERM_H})"
|
||
print_info
|
||
|
||
else
|
||
# ── 情况 2: 不在 tmux 中 → 创建新会话并附加 ──
|
||
|
||
# 如果会话已存在,直接附加
|
||
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||
echo "会话 '$SESSION_NAME' 已存在,附加..."
|
||
exec tmux attach-session -t "$SESSION_NAME"
|
||
fi
|
||
|
||
get_terminal_size
|
||
|
||
# 创建新会话 (需要短暂后台创建以构建布局)
|
||
tmux new-session -d -s "$SESSION_NAME" -c "$WORKSPACE"
|
||
tmux rename-window -t "$SESSION_NAME:1" "ISOS-Team"
|
||
|
||
build_layout "$SESSION_NAME:1.1" "$TERM_W" "$TERM_H"
|
||
|
||
echo "团队工作空间 '$SESSION_NAME' 已创建 (${TERM_W}×${TERM_H})"
|
||
print_info
|
||
|
||
if [ "${OPENCODE:-}" = "1" ] || [ "${CLAUDECODE:-}" = "1" ]; then
|
||
# Claude Code 环境中无法 attach,仅打印提示
|
||
echo "提示: 在 AI 编码工具中执行,跳过 attach"
|
||
echo "请在终端中运行: tmux attach -t $SESSION_NAME"
|
||
else
|
||
# 用户终端中直接执行,自动 attach
|
||
echo "正在附加到会话..."
|
||
exec tmux attach-session -t "$SESSION_NAME"
|
||
fi
|
||
fi
|