Files
2026-04-19 22:23:34 +08:00

414 lines
13 KiB
Bash
Executable File

#!/bin/bash
# tmux-msg.sh — tmux 面板间消息通信 CLI
#
# 用法:
# tmux-msg.sh send <to_pane> "<task>" [--from <pane>] [--task-file <path>]
# tmux-msg.sh status [msg_id]
# tmux-msg.sh list [--from <pane>] [--to <pane>] [--status <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 <to_pane> \"<task>\" [--from <pane>] [--task-file <path>]" >&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 <<EOJSON
{"msg_id":"${msg_id}","direction":"request","from_pane":"${from_pane}","from_title":"${from_title}","to_pane":"${to_pane}","to_title":"${to_title}","task_summary":${task_json},"task_file":${task_file_json},"status":"created","created_at":"${ts}","delivered_at":null,"replied_at":null,"reply_msg_id":null,"reply_summary":null,"error":null}
EOJSON
)
write_envelope "$envelope_path" "$envelope"
# 写入索引
local index_line
index_line=$(printf '{"msg_id":"%s","direction":"request","from":"%s","to":"%s","status":"created","summary":"%s","ts":"%s"}\n' \
"$msg_id" "$from_pane" "$to_pane" "$task_summary" "$ts")
append_index "$index_line"
# 构建 cc 可读提示词
local notify_script="/workspace/scripts/tmux-msg-notify.sh"
local prompt_text
if [[ -n "$task_file" ]]; then
prompt_text=$(cat <<PROMPT
## 跨面板任务 [${msg_id}]
来源: ${from_title} (Pane ${from_pane})
任务文件: ${task_file}
请读取上述任务文件并完成其中的任务。
完成后,请使用 Bash 工具执行以下命令通知来源方:
bash ${notify_script} ${msg_id} ${from_pane} "{完成摘要}"
其中 {完成摘要} 替换为你的实际完成情况(如: 已完成、失败-原因、部分完成-说明)。
PROMPT
)
else
prompt_text=$(cat <<PROMPT
## 跨面板任务 [${msg_id}]
来源: ${from_title} (Pane ${from_pane})
任务: ${task}
请完成以上任务。
完成后,请使用 Bash 工具执行以下命令通知来源方:
bash ${notify_script} ${msg_id} ${from_pane} "{完成摘要}"
其中 {完成摘要} 替换为你的实际完成情况(如: 已完成、失败-原因、部分完成-说明)。
PROMPT
)
fi
# 写提示词文件(持久化,cc 需要异步读取)
local prompt_dir="${MSG_DIR}/prompts"
mkdir -p "$prompt_dir"
local tmp_prompt="${prompt_dir}/${msg_id}.md"
echo "$prompt_text" > "$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 <command> [options]"
echo ""
echo "命令:"
echo " send <to_pane> \"<task>\" [--from <pane>] [--task-file <path>]"
echo " 发送任务到目标面板"
echo " status [msg_id]"
echo " 查看消息状态(无参数则显示总览)"
echo " list [--from <pane>] [--to <pane>] [--status <status>]"
echo " 列出消息(可过滤)"
;;
*)
echo "错误: 未知命令 '$cmd'" >&2
echo "用法: tmux-msg.sh <send|status|list|help>" >&2
exit 1
;;
esac