#!/bin/bash # tmux-msg.sh — tmux 面板间消息通信 CLI # # 用法: # tmux-msg.sh send "" [--from ] [--task-file ] # tmux-msg.sh status [msg_id] # tmux-msg.sh list [--from ] [--to ] [--status ] # # 功能: # - 发送任务到目标面板,自动生成消息 ID 和信封 # - 嵌入完成通知指令,让接收方 cc 完成后自动回复 # - 所有消息记录到 /workspace/.tmux-messages/ # # 示例: # tmux-msg.sh send %2 "实现用户 API" # tmux-msg.sh send %2 "实现用户 API" --from %1 # tmux-msg.sh send %2 --task-file /path/to/task.md # tmux-msg.sh status # tmux-msg.sh list --status pending set -euo pipefail MSG_DIR="${TMUX_MSG_DIR:-/workspace/.tmux-messages}" INDEX_FILE="${MSG_DIR}/index.jsonl" SEND_SCRIPT="/workspace/scripts/tmux-send-prompt.sh" # ─── 颜色 ─── GREEN='\033[32m' RED='\033[31m' YELLOW='\033[33m' CYAN='\033[36m' RESET='\033[0m' # ─── 辅助函数 ─── generate_msg_id() { echo "msg-$(date +%Y%m%d-%H%M%S)-$(head -c 2 /dev/urandom | od -A n -t x1 | tr -d ' ')" } get_pane_title() { local pane_id="$1" tmux display-message -t "$pane_id" -p '#{pane_title}' 2>/dev/null || echo "未知" } get_today_dir() { date +%Y/%m/%d } append_index() { local json_line="$1" mkdir -p "$(dirname "$INDEX_FILE")" ( flock -x 200 echo "$json_line" >> "$INDEX_FILE" ) 200>"${INDEX_FILE}.lock" } write_envelope() { local filepath="$1" json="$2" mkdir -p "$(dirname "$filepath")" local tmp="${filepath}.tmp" echo "$json" > "$tmp" mv "$tmp" "$filepath" } read_envelope() { local msg_id="$1" # 搜索消息文件 local found found=$(find "$MSG_DIR" -name "${msg_id}.json" -type f 2>/dev/null | head -1) if [[ -n "$found" ]]; then cat "$found" return 0 fi return 1 } update_envelope_status() { local msg_id="$1" status="$2" extra_json="${3:-}" local found found=$(find "$MSG_DIR" -name "${msg_id}.json" -type f 2>/dev/null | head -1) if [[ -z "$found" ]]; then return 1 fi local json json=$(cat "$found") # 更新 status 字段 json=$(echo "$json" | python3 -c " import sys, json d = json.load(sys.stdin) d['status'] = '${status}' ${extra_json:+exec(\"d.update(\" + '''${extra_json}''' + \")\")} json.dump(d, sys.stdout, ensure_ascii=False) " 2>/dev/null) || return 1 write_envelope "$found" "$json" } # ─── send 子命令 ─── cmd_send() { local to_pane='' task='' from_pane='' task_file='' # 解析参数 while [[ $# -gt 0 ]]; do case "$1" in --from) from_pane="${2:?--from 需要一个 pane ID}" shift 2 ;; --task-file) task_file="${2:?--task-file 需要一个文件路径}" shift 2 ;; -*) echo "错误: 未知选项 $1" >&2 exit 1 ;; *) if [[ -z "$to_pane" ]]; then to_pane="$1" elif [[ -z "$task" ]]; then task="$1" else echo "错误: 多余参数 '$1'" >&2 exit 1 fi shift ;; esac done if [[ -z "$to_pane" ]]; then echo "用法: tmux-msg.sh send \"\" [--from ] [--task-file ]" >&2 exit 1 fi # 确定发送方 if [[ -z "$from_pane" ]]; then from_pane="${TMUX_PANE:-$(tmux display-message -p '#{pane_id}' 2>/dev/null || echo '%0')}" fi # 确定任务内容 if [[ -n "$task_file" ]]; then if [[ ! -f "$task_file" ]]; then echo "错误: 文件不存在: $task_file" >&2 exit 1 fi task_file=$(realpath "$task_file") fi if [[ -z "$task" && -z "$task_file" ]]; then echo "错误: 需要提供任务描述或任务文件" >&2 exit 1 fi # 生成消息 ID local msg_id msg_id=$(generate_msg_id) local ts ts=$(date -Iseconds) # 解析 pane 标题 local from_title to_title from_title=$(get_pane_title "$from_pane") to_title=$(get_pane_title "$to_pane") # 任务摘要(用于日志) local task_summary if [[ -n "$task_file" ]]; then task_summary="[文件] $(basename "$task_file")" else task_summary=$(echo "$task" | head -c 60) fi # 创建消息信封 local day_dir day_dir=$(get_today_dir) local envelope_path="${MSG_DIR}/${day_dir}/${msg_id}.json" local task_json if [[ -n "$task_file" ]]; then task_json="null" else # 转义 JSON 字符串中的特殊字符 task_json=$(printf '%s' "$task" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))') fi local task_file_json if [[ -n "$task_file" ]]; then task_file_json=$(printf '%s' "$task_file" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read(), ensure_ascii=False))') else task_file_json="null" fi local envelope envelope=$(cat < "$tmp_prompt" # 发送到目标面板 echo -e "${CYAN}[tmux-msg]${RESET} 发送任务 ${msg_id}" echo -e " 来源: ${from_title} (${from_pane})" echo -e " 目标: ${to_title} (${to_pane})" echo -e " 摘要: ${task_summary}" if bash "$SEND_SCRIPT" "$to_pane" "$tmp_prompt" --from "$from_pane" ${TMUX_MSG_NO_VERIFY:+--no-verify} 2>&1; then # 更新状态为 sent local delivered_ts delivered_ts=$(date -Iseconds) update_envelope_status "$msg_id" "sent" "{'delivered_at': '${delivered_ts}'}" echo -e "${GREEN}[tmux-msg]${RESET} 任务已发送: ${msg_id}" else update_envelope_status "$msg_id" "send_failed" echo -e "${RED}[tmux-msg]${RESET} 任务发送失败: ${msg_id}" >&2 exit 1 fi # 提示词文件保留,供 cc 异步读取 echo "$msg_id" } # ─── status 子命令 ─── cmd_status() { local msg_id="${1:-}" if [[ -n "$msg_id" ]]; then # 显示特定消息状态 local envelope if ! envelope=$(read_envelope "$msg_id"); then echo "错误: 未找到消息 ${msg_id}" >&2 exit 1 fi echo "$envelope" | python3 -c " import sys, json d = json.load(sys.stdin) print(f'消息ID: {d[\"msg_id\"]}') print(f'方向: {d[\"direction\"]}') print(f'来源: {d[\"from_title\"]} ({d[\"from_pane\"]})') print(f'目标: {d[\"to_title\"]} ({d[\"to_pane\"]})') print(f'状态: {d[\"status\"]}') print(f'摘要: {d.get(\"task_summary\", \"\")}') print(f'创建: {d[\"created_at\"]}') if d.get('delivered_at'): print(f'送达: {d[\"delivered_at\"]}') if d.get('replied_at'): print(f'回复: {d[\"replied_at\"]}') print(f'回复摘要: {d.get(\"reply_summary\", \"\")}') if d.get('task_file'): print(f'任务文件: {d[\"task_file\"]}') " return fi # 显示所有待处理消息 if [[ ! -f "$INDEX_FILE" ]]; then echo "暂无消息记录" return fi echo -e "${CYAN}══ 消息状态总览 ══${RESET}" local pending=0 completed=0 failed=0 total=0 while IFS= read -r line; do [[ -z "$line" ]] && continue total=$((total + 1)) local status from to summary msg status=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['status'])" 2>/dev/null || echo "unknown") from=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['from'])" 2>/dev/null || echo "?") to=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['to'])" 2>/dev/null || echo "?") summary=$(echo "$line" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('summary','')[:40])" 2>/dev/null || echo "") msg=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['msg_id'])" 2>/dev/null || echo "?") local color="$RESET" case "$status" in created|sent|pending) color="$YELLOW"; pending=$((pending + 1)) ;; completed|replied) color="$GREEN"; completed=$((completed + 1)) ;; failed|send_failed|reply_failed) color="$RED"; failed=$((failed + 1)) ;; esac echo -e " ${color}${status}${RESET} ${msg} ${from}→${to} ${summary}" done < "$INDEX_FILE" echo "" echo "总计: ${total} | 完成: ${completed} | 待处理: ${pending} | 失败: ${failed}" } # ─── list 子命令 ─── cmd_list() { local filter_from='' filter_to='' filter_status='' while [[ $# -gt 0 ]]; do case "$1" in --from) filter_from="$2"; shift 2 ;; --to) filter_to="$2"; shift 2 ;; --status) filter_status="$2"; shift 2 ;; *) shift ;; esac done if [[ ! -f "$INDEX_FILE" ]]; then echo "暂无消息记录" return fi local count=0 while IFS= read -r line; do [[ -z "$line" ]] && continue # 过滤 if [[ -n "$filter_from" ]]; then local from from=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['from'])" 2>/dev/null) [[ "$from" != "$filter_from" ]] && continue fi if [[ -n "$filter_to" ]]; then local to to=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['to'])" 2>/dev/null) [[ "$to" != "$filter_to" ]] && continue fi if [[ -n "$filter_status" ]]; then local st st=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['status'])" 2>/dev/null) [[ "$st" != "$filter_status" ]] && continue fi count=$((count + 1)) echo "$line" | python3 -c " import sys, json d = json.loads(sys.stdin.read()) print(f'{d[\"msg_id\"]} {d[\"status\"]:12} {d[\"from\"]}→{d[\"to\"]} {d.get(\"summary\",\"\")[:50]}') " 2>/dev/null done < "$INDEX_FILE" echo "" echo "共 ${count} 条消息" } # ─── 主入口 ─── cmd="${1:-help}" shift || true case "$cmd" in send) cmd_send "$@" ;; status) cmd_status "$@" ;; list) cmd_list "$@" ;; help|--help|-h) echo "用法: tmux-msg.sh [options]" echo "" echo "命令:" echo " send \"\" [--from ] [--task-file ]" echo " 发送任务到目标面板" echo " status [msg_id]" echo " 查看消息状态(无参数则显示总览)" echo " list [--from ] [--to ] [--status ]" echo " 列出消息(可过滤)" ;; *) echo "错误: 未知命令 '$cmd'" >&2 echo "用法: tmux-msg.sh " >&2 exit 1 ;; esac