From d9f1990884a77eea79a5bf6cab446917addb93ac Mon Sep 17 00:00:00 2001 From: arno Date: Sun, 19 Apr 2026 21:47:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E6=B7=BB=E5=8A=A0=20tmux?= =?UTF-8?q?=20=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5=E4=B8=8E=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + member/vscode.chat.md | 59 ----- memory/MEMORY.md | 1 + memory/jj-untrack-ignored-files.md | 14 + scripts/test-tmux-msg.sh | 356 +++++++++++++++++++++++++ scripts/tmux-msg-log.sh | 289 ++++++++++++++++++++ scripts/tmux-msg-notify.sh | 160 +++++++++++ scripts/tmux-msg.sh | 413 +++++++++++++++++++++++++++++ scripts/tmux-send-prompt.sh | 123 +++++++-- 9 files changed, 1338 insertions(+), 80 deletions(-) create mode 100644 memory/jj-untrack-ignored-files.md create mode 100755 scripts/test-tmux-msg.sh create mode 100755 scripts/tmux-msg-log.sh create mode 100755 scripts/tmux-msg-notify.sh create mode 100755 scripts/tmux-msg.sh diff --git a/.gitignore b/.gitignore index e33d60e..6a21770 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ coverage.json # uv .python-version.bak uv.lock + +# tmux 消息 +.tmux-messages/ diff --git a/member/vscode.chat.md b/member/vscode.chat.md index 8e2069e..4ec762f 100644 --- a/member/vscode.chat.md +++ b/member/vscode.chat.md @@ -1,60 +1 @@ # vscode 的对话记录 -## 2026-04-19 20:58:35 - -注意: team/mirrors.md 中 pnpm 镜像行需要保留. - - -## 2026-04-19 20:58:58 - -直接执行 - - -## 2026-04-19 21:05:24 - -1. :需要精简和拆分. 2. 200行限制适用于 commands , 需要精简. - - -## 2026-04-19 21:17:46 - -请读取 /workspace/.claude/agents/isos-frontend-agent.md 并执行其中的内容 - - -## 2026-04-19 21:17:46 - -请读取 /workspace/.claude/agents/isos-backend-agent.md 并执行其中的内容 - - -## 2026-04-19 21:17:46 - -请读取 /workspace/.claude/agents/isos-test-agent.md 并执行其中的内容 - - -## 2026-04-19 21:21:27 - -在当前 session : `isos-team` 中 测试 /workspace/scripts/tmux-send-prompt.sh 脚本,是否能正常的发送消息给4个pane: `项目`, `后端`, `前端`, `测试` . 请先编写对这个脚本的测试方式, 提供3个方案选择, 我审批后执行. - - -## 2026-04-19 21:31:15 - -在 session : `isos-team` 中 测试 /workspace/scripts/tmux-send-prompt.sh 脚本. 主要目的是: 1. 检查消息发送过程是否正确, 消息是否输入了 `cc` 的输入框, 消息是否发送并开始让 `cc` 执行. 2. 新增加功能: 让 2个 pane 之间互相发送消息, pane 1 发送消息给 pane 2 后, 当 pane 2 执行完成, 发送消息主动通知 pane 1. 3. 增加功能: 记录 收到的消息, 执行结果, 反馈的消息. - - -## 2026-04-19 21:37:40 - -hi - - -## 2026-04-19 21:43:17 - -请回复 TEST_OK - - -## 2026-04-19 21:44:30 - -test text - - -## 2026-04-19 21:46:07 - -请回复 BACKEND_OK - diff --git a/memory/MEMORY.md b/memory/MEMORY.md index 0dd908c..0df726a 100644 --- a/memory/MEMORY.md +++ b/memory/MEMORY.md @@ -33,6 +33,7 @@ - [claude-code-rename](claude-code-rename.md) — --name 不可靠,必须用 /rename 持久化会话名称 - [devcontainer-sync](devcontainer-sync.md) — templates/ 是单一信源,运行时文件由 Dockerfile/post-create.sh 生成 - [tmux-send-keys-enter](tmux-send-keys-enter.md) — send-keys 后必须确认 Enter 已生效 +- [jj-untrack-ignored-files](jj-untrack-ignored-files.md) — jj 中 .gitignore 对已跟踪文件需用 jj file untrack ## 项目历史 diff --git a/memory/jj-untrack-ignored-files.md b/memory/jj-untrack-ignored-files.md new file mode 100644 index 0000000..af2c228 --- /dev/null +++ b/memory/jj-untrack-ignored-files.md @@ -0,0 +1,14 @@ +--- +name: jj untrack 忽略已跟踪文件 +description: jj 中 .gitignore 对已跟踪文件不生效,需用 jj file untrack 显式取消跟踪 +type: feedback +originSessionId: 9d4067bc-0888-4284-b2a7-cf3b0f392839 +--- +已加入 `.gitignore` 的路径如果在 jj 工作副本中已被跟踪(`A` 状态),`.gitignore` 不会生效。 + +**Why:** jj 自动快照工作副本,`git rm --cached` 的改动会被 jj 重新快照覆盖。jj 有自己的文件跟踪机制。 + +**How to apply:** +1. 先确认路径已加入 `.gitignore` +2. 用 `jj file untrack 'glob:/**'` 取消跟踪 +3. 验证 `jj status` 中不再显示该路径 diff --git a/scripts/test-tmux-msg.sh b/scripts/test-tmux-msg.sh new file mode 100755 index 0000000..7a6ba78 --- /dev/null +++ b/scripts/test-tmux-msg.sh @@ -0,0 +1,356 @@ +#!/bin/bash +# test-tmux-msg.sh — tmux-msg.sh / tmux-msg-notify.sh 单元测试 +# +# 用法: bash scripts/test-tmux-msg.sh +# +# 通过 mock tmux 命令隔离测试,覆盖消息协议的 12 个场景 + +set -euo pipefail + +MSG_SCRIPT="/workspace/scripts/tmux-msg.sh" +NOTIFY_SCRIPT="/workspace/scripts/tmux-msg-notify.sh" +LOG_SCRIPT="/workspace/scripts/tmux-msg-log.sh" +SEND_SCRIPT="/workspace/scripts/tmux-send-prompt.sh" +TEST_DIR=$(mktemp -d) +MOCK_DIR="${TEST_DIR}/mock-bin" +MSG_DIR="${TEST_DIR}/.tmux-messages" +LOG_FILE="${TEST_DIR}/test-output.log" + +# 替代 /workspace/.tmux-messages 为测试目录 +export TMUX_MSG_DIR_OVERRIDE="$MSG_DIR" + +# ─── 颜色输出 ─── +GREEN='\033[32m' +RED='\033[31m' +YELLOW='\033[33m' +RESET='\033[0m' + +pass=0 +fail=0 +skip=0 + +result() { + local label="$1" ok="$2" detail="${3:-}" + if [[ "$ok" == "PASS" ]]; then + ((pass++)) || true + echo -e " ${GREEN}✓ PASS${RESET} ${label}${detail:+ — ${detail}}" + elif [[ "$ok" == "SKIP" ]]; then + ((skip++)) || true + echo -e " ${YELLOW}⊘ SKIP${RESET} ${label}${detail:+ — ${detail}}" + else + ((fail++)) || true + echo -e " ${RED}✗ FAIL${RESET} ${label}${detail:+ — ${detail}}" + fi +} + +cleanup() { + rm -rf "$TEST_DIR" +} +trap cleanup EXIT + +# ─── 创建 mock tmux ─── +create_mock_tmux() { + mkdir -p "$MOCK_DIR" + cat > "${MOCK_DIR}/tmux" <<'MOCK' +#!/bin/bash +LOG="$(dirname "$0")/../tmux-calls.log" +echo "$@" >> "$LOG" +if [[ "$1" == "capture-pane" ]]; then + echo "Some output" + echo "❯ " +fi +if [[ "$1" == "display-message" ]]; then + echo "✳ MockPane" +fi +if [[ "$1" == "send-keys" ]]; then + # 模拟发送成功 + exit 0 +fi +MOCK + chmod +x "${MOCK_DIR}/tmux" +} + +clear_log() { + : > "${TEST_DIR}/tmux-calls.log" +} + +read_log() { + cat "${TEST_DIR}/tmux-calls.log" 2>/dev/null || true +} + +# 创建测试用的消息目录 +setup_msg_dir() { + mkdir -p "${MSG_DIR}/prompts" + mkdir -p "${MSG_DIR}/$(date +%Y/%m/%d)" + : > "${MSG_DIR}/index.jsonl" +} + +# ═══════════════════════════════════════════ +echo "" +echo "═══ tmux-msg 协议单元测试 ═══" +echo "" + +# ─── 1. send 文本字符串 ─── +echo "── 1. send 文本字符串 ──" +create_mock_tmux +setup_msg_dir +clear_log + +output=$(PATH="${MOCK_DIR}:${PATH}" TMUX_MSG_DIR="$MSG_DIR" TMUX_SEND_LOG="${TEST_DIR}/send.log" TMUX_MSG_NO_VERIFY=1 bash "$MSG_SCRIPT" send %2 "测试任务描述" --from %1 2>&1 || true) +msg_id=$(echo "$output" | grep -oE '^msg-[0-9]+-[0-9]+-[0-9a-f]+$' | head -1) + +if [[ -n "$msg_id" ]]; then + result "send → 返回有效 msg_id" "PASS" "$msg_id" +else + result "send → 返回有效 msg_id" "FAIL" "实际: $msg_id" +fi + +# 验证信封文件创建 +envelope=$(find "$MSG_DIR" -name "${msg_id}.json" -type f 2>/dev/null | head -1) +if [[ -n "$envelope" ]]; then + result "send → 信封文件已创建" "PASS" +else + result "send → 信封文件已创建" "FAIL" +fi + +# 验证索引更新 +if [[ -f "${MSG_DIR}/index.jsonl" ]] && grep -q "$msg_id" "${MSG_DIR}/index.jsonl"; then + result "send → index.jsonl 已更新" "PASS" +else + result "send → index.jsonl 已更新" "FAIL" +fi + +# ─── 2. send 包含回复指令 ─── +echo "" +echo "── 2. send 包含回复指令 ──" + +# 检查提示词文件包含 notify 指令 +prompt_file="${MSG_DIR}/prompts/${msg_id}.md" +if [[ -f "$prompt_file" ]] && grep -q "tmux-msg-notify" "$prompt_file"; then + result "send → 提示词包含通知指令" "PASS" +else + result "send → 提示词包含通知指令" "FAIL" +fi + +if [[ -f "$prompt_file" ]] && grep -q "跨面板任务" "$prompt_file"; then + result "send → 提示词包含任务头" "PASS" +else + result "send → 提示词包含任务头" "FAIL" +fi + +if [[ -f "$prompt_file" ]] && grep -q "%1" "$prompt_file"; then + result "send → 提示词包含来源 pane" "PASS" +else + result "send → 提示词包含来源 pane" "FAIL" +fi + +# ─── 3. 消息 ID 格式 ─── +echo "" +echo "── 3. 消息 ID 格式 ──" + +output2=$(PATH="${MOCK_DIR}:${PATH}" TMUX_MSG_DIR="$MSG_DIR" TMUX_SEND_LOG="${TEST_DIR}/send.log" TMUX_MSG_NO_VERIFY=1 bash "$MSG_SCRIPT" send %3 "第二个任务" 2>&1 || true) +msg_id2=$(echo "$output2" | grep -oE '^msg-[0-9]+-[0-9]+-[0-9a-f]+$' | head -1) + +if [[ -n "$msg_id2" ]]; then + result "消息 ID 格式正确" "PASS" "$msg_id2" +else + result "消息 ID 格式正确" "FAIL" "实际: $msg_id2" +fi + +if [[ "$msg_id" != "$msg_id2" ]]; then + result "消息 ID 唯一" "PASS" +else + result "消息 ID 唯一" "FAIL" "两次 send 返回相同 ID" +fi + +# ─── 4. pane 标题解析 ─── +echo "" +echo "── 4. pane 标题解析 ──" + +if [[ -f "$envelope" ]]; then + from_title=$(python3 -c "import json; print(json.load(open('$envelope'))['from_title'])" 2>/dev/null) + if [[ "$from_title" == "✳ MockPane" ]]; then + result "pane 标题从 mock 正确解析" "PASS" "$from_title" + else + result "pane 标题从 mock 正确解析" "FAIL" "实际: $from_title" + fi +fi + +# ─── 5. notify 成功 ─── +echo "" +echo "── 5. notify 成功 ──" +setup_msg_dir + +# 先手动创建一个消息信封用于 notify 测试 +test_msg_id="msg-20260419-120000-test" +day_dir=$(date +%Y/%m/%d) +mkdir -p "${MSG_DIR}/${day_dir}" +cat > "${MSG_DIR}/${day_dir}/${test_msg_id}.json" <> "${MSG_DIR}/index.jsonl" + +clear_log +output=$(PATH="${MOCK_DIR}:${PATH}" TMUX_MSG_DIR="$MSG_DIR" TMUX_SEND_LOG="${TEST_DIR}/send.log" TMUX_PANE="%2" bash "$NOTIFY_SCRIPT" "$test_msg_id" "%1" "已完成测试" 2>&1 || true) + +# 验证原消息状态更新 +updated_status=$(python3 -c "import json; print(json.load(open('${MSG_DIR}/${day_dir}/${test_msg_id}.json'))['status'])" 2>/dev/null) +if [[ "$updated_status" == "completed" ]]; then + result "notify → 原消息状态更新为 completed" "PASS" +else + result "notify → 原消息状态更新为 completed" "FAIL" "实际: $updated_status" +fi + +# 验证回复信封创建 +reply_envelope="${MSG_DIR}/${day_dir}/reply-${test_msg_id}.json" +if [[ -f "$reply_envelope" ]]; then + result "notify → 回复信封已创建" "PASS" +else + result "notify → 回复信封已创建" "FAIL" +fi + +# 验证 notify 调用了 send-prompt 发送回复 +log=$(read_log) +if echo "$log" | grep -q "send-keys"; then + result "notify → 调用了 tmux send-keys" "PASS" +else + result "notify → 调用了 tmux send-keys" "FAIL" +fi + +# ─── 6. notify 未知 msg_id 容错 ─── +echo "" +echo "── 6. notify 未知 msg_id ──" +clear_log + +output=$(PATH="${MOCK_DIR}:${PATH}" TMUX_MSG_DIR="$MSG_DIR" TMUX_SEND_LOG="${TEST_DIR}/send.log" TMUX_PANE="%2" bash "$NOTIFY_SCRIPT" "msg-nonexistent" "%1" "测试" 2>&1 || true) +if echo "$output" | grep -q "未找到消息"; then + result "notify 未知 msg_id → 宽容处理" "PASS" +else + result "notify 未知 msg_id → 宽容处理" "FAIL" "输出: $output" +fi + +# ─── 7. 目录结构 ─── +echo "" +echo "── 7. 目录结构 ──" + +today_dir=$(date +%Y/%m/%d) +if [[ -d "${MSG_DIR}/${today_dir}" ]]; then + result "日期分区目录已创建" "PASS" "${MSG_DIR}/${today_dir}" +else + result "日期分区目录已创建" "FAIL" +fi + +if [[ -d "${MSG_DIR}/prompts" ]]; then + result "prompts 目录已创建" "PASS" +else + result "prompts 目录已创建" "FAIL" +fi + +# ─── 8. index.jsonl 一致性 ─── +echo "" +echo "── 8. index.jsonl 一致性 ──" + +line_count=$(wc -l < "${MSG_DIR}/index.jsonl" | tr -d ' ') +if [[ "$line_count" -ge 1 ]]; then + result "index.jsonl 有内容" "PASS" "$line_count 行" +else + result "index.jsonl 有内容" "FAIL" "空文件" +fi + +# 验证每行都是有效 JSON +invalid_lines=0 +while IFS= read -r line; do + [[ -z "$line" ]] && continue + if ! echo "$line" | python3 -c "import sys,json; json.loads(sys.stdin.read())" 2>/dev/null; then + invalid_lines=$((invalid_lines + 1)) + fi +done < "${MSG_DIR}/index.jsonl" + +if [[ "$invalid_lines" -eq 0 ]]; then + result "index.jsonl 所有行都是有效 JSON" "PASS" +else + result "index.jsonl 所有行都是有效 JSON" "FAIL" "${invalid_lines} 行无效" +fi + +# ─── 9. log 工具查询 ─── +echo "" +echo "── 9. log 工具查询 ──" + +output=$(TMUX_MSG_DIR="$MSG_DIR" bash "$LOG_SCRIPT" recent 2>&1) +if echo "$output" | grep -q "消息"; then + result "log recent → 返回消息列表" "PASS" +else + result "log recent → 返回消息列表" "FAIL" +fi + +output=$(TMUX_MSG_DIR="$MSG_DIR" bash "$LOG_SCRIPT" show "$test_msg_id" 2>&1) +if echo "$output" | grep -q "$test_msg_id"; then + result "log show → 返回消息详情" "PASS" +else + result "log show → 返回消息详情" "FAIL" +fi + +output=$(TMUX_MSG_DIR="$MSG_DIR" bash "$LOG_SCRIPT" stats 2>&1) +if echo "$output" | grep -q "总计"; then + result "log stats → 返回统计" "PASS" +else + result "log stats → 返回统计" "FAIL" +fi + +# ─── 10. list 过滤 ─── +echo "" +echo "── 10. list 过滤 ──" + +output=$(TMUX_MSG_DIR="$MSG_DIR" bash "$LOG_SCRIPT" list --from %1 2>&1) +if echo "$output" | grep -q "%1"; then + result "list --from → 过滤生效" "PASS" +else + result "list --from → 过滤生效" "FAIL" +fi + +output=$(TMUX_MSG_DIR="$MSG_DIR" bash "$LOG_SCRIPT" by-pane %2 2>&1) +if echo "$output" | grep -q "%2"; then + result "by-pane → 过滤生效" "PASS" +else + result "by-pane → 过滤生效" "FAIL" +fi + +# ─── 11. send 参数校验 ─── +echo "" +echo "── 11. send 参数校验 ──" + +output=$(PATH="${MOCK_DIR}:${PATH}" TMUX_MSG_DIR="$MSG_DIR" TMUX_SEND_LOG="${TEST_DIR}/send.log" bash "$MSG_SCRIPT" send 2>&1 || true) +if echo "$output" | grep -q "用法\|需要"; then + result "send 无参数 → 提示用法" "PASS" +else + result "send 无参数 → 提示用法" "FAIL" +fi + +# ─── 12. 信封字段完整性 ─── +echo "" +echo "── 12. 信封字段完整性 ──" + +required_fields="msg_id direction from_pane from_title to_pane to_title task_summary status created_at" +missing_fields="" +for field in $required_fields; do + if ! python3 -c "import json; d=json.load(open('$envelope')); assert '$field' in d" 2>/dev/null; then + missing_fields="${missing_fields} ${field}" + fi +done + +if [[ -z "$missing_fields" ]]; then + result "信封字段完整" "PASS" +else + result "信封字段完整" "FAIL" "缺少:${missing_fields}" +fi + +# ═══════════════════════════════════════════ +echo "" +echo "════════════════════════════════════" +echo -e " ${GREEN}通过: ${pass}${RESET} ${RED}失败: ${fail}${RESET} ${YELLOW}跳过: ${skip}${RESET} 总计: $((pass + fail + skip))" +echo "════════════════════════════════════" +echo "" + +if [[ "$fail" -gt 0 ]]; then + exit 1 +fi diff --git a/scripts/tmux-msg-log.sh b/scripts/tmux-msg-log.sh new file mode 100755 index 0000000..d8e8b37 --- /dev/null +++ b/scripts/tmux-msg-log.sh @@ -0,0 +1,289 @@ +#!/bin/bash +# tmux-msg-log.sh — 消息日志查询工具 +# +# 用法: +# tmux-msg-log.sh recent [--count 20] +# tmux-msg-log.sh by-pane +# tmux-msg-log.sh show +# tmux-msg-log.sh stats +# tmux-msg-log.sh log "" +# +# 示例: +# tmux-msg-log.sh recent +# tmux-msg-log.sh by-pane %2 +# tmux-msg-log.sh show msg-20260419-142357-a1b2 +# tmux-msg-log.sh stats + +set -euo pipefail + +MSG_DIR="${TMUX_MSG_DIR:-/workspace/.tmux-messages}" +INDEX_FILE="${MSG_DIR}/index.jsonl" + +# ─── 颜色 ─── +GREEN='\033[32m' +RED='\033[31m' +YELLOW='\033[33m' +CYAN='\033[36m' +BOLD='\033[1m' +RESET='\033[0m' + +read_envelope() { + local msg_id="$1" + find "$MSG_DIR" -name "${msg_id}.json" -type f 2>/dev/null | head -1 +} + +# ─── recent 子命令 ─── + +cmd_recent() { + local count="${1:-20}" + + if [[ ! -f "$INDEX_FILE" ]]; then + echo "暂无消息记录" + return + fi + + echo -e "${BOLD}══ 最近 ${count} 条消息 ══${RESET}" + echo "" + + tail -n "$count" "$INDEX_FILE" | while IFS= read -r line; do + [[ -z "$line" ]] && continue + echo "$line" | python3 -c " +import sys, json +d = json.loads(sys.stdin.read()) +direction = d.get('direction', '?') +status = d.get('status', '?') +msg_id = d.get('msg_id', '?') +from_p = d.get('from', '?') +to_p = d.get('to', '?') +summary = d.get('summary', '')[:50] +ts = d.get('ts', '?') + +# 状态颜色 +if status in ('completed', 'delivered', 'replied'): + color = '\033[32m' +elif status in ('failed', 'send_failed', 'reply_failed'): + color = '\033[31m' +else: + color = '\033[33m' + +direction_icon = '→' if direction == 'request' else '←' +print(f' {color}{status:12}\033[0m {msg_id} {direction_icon} {from_p}→{to_p}') +print(f' {summary}') +print(f' {ts}') +print() +" 2>/dev/null + done +} + +# ─── by-pane 子命令 ─── + +cmd_by_pane() { + local pane_id="${1:?用法: tmux-msg-log.sh by-pane }" + + if [[ ! -f "$INDEX_FILE" ]]; then + echo "暂无消息记录" + return + fi + + echo -e "${BOLD}══ Pane ${pane_id} 的消息 ══${RESET}" + local count=0 + while IFS= read -r line; do + [[ -z "$line" ]] && continue + local match + match=$(echo "$line" | python3 -c " +import sys, json +d = json.loads(sys.stdin.read()) +if d.get('from') == '${pane_id}' or d.get('to') == '${pane_id}': + print('yes') +else: + print('no') +" 2>/dev/null) + [[ "$match" != "yes" ]] && continue + + count=$((count + 1)) + echo "$line" | python3 -c " +import sys, json +d = json.loads(sys.stdin.read()) +direction = d.get('direction', '?') +status = d.get('status', '?') +msg_id = d.get('msg_id', '?') +from_p = d.get('from', '?') +to_p = d.get('to', '?') +summary = d.get('summary', '')[:50] +role = '发送' if d.get('from') == '${pane_id}' else '接收' +print(f' [{role}] {status:12} {msg_id} {summary}') +" 2>/dev/null + done < "$INDEX_FILE" + + echo "" + echo "共 ${count} 条消息" +} + +# ─── show 子命令 ─── + +cmd_show() { + local msg_id="${1:?用法: tmux-msg-log.sh show }" + + local envelope_path + envelope_path=$(read_envelope "$msg_id") + if [[ -z "$envelope_path" ]]; then + echo "错误: 未找到消息 ${msg_id}" >&2 + exit 1 + fi + + python3 -c " +import sys, json +with open('${envelope_path}') as f: + d = json.load(f) + +print('══ 消息详情 ══') +print(f'消息ID: {d[\"msg_id\"]}') +print(f'方向: {d[\"direction\"]}') +if d.get('in_reply_to'): + print(f'回复消息: {d[\"in_reply_to\"]}') +print(f'来源: {d[\"from_title\"]} ({d[\"from_pane\"]})') +print(f'目标: {d[\"to_title\"]} ({d[\"to_pane\"]})') +print(f'状态: {d[\"status\"]}') +if d.get('task_summary'): + print(f'任务摘要: {d[\"task_summary\"]}') +if d.get('task_file'): + print(f'任务文件: {d[\"task_file\"]}') +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('error'): + print(f'错误: {d[\"error\"]}') +" 2>/dev/null +} + +# ─── stats 子命令 ─── + +cmd_stats() { + if [[ ! -f "$INDEX_FILE" ]]; then + echo "暂无消息记录" + return + fi + + local total=0 requests=0 replies=0 + local completed=0 pending=0 failed=0 + declare -A pane_counts + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + total=$((total + 1)) + + local direction status from to + direction=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('direction','?'))" 2>/dev/null) + status=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('status','?'))" 2>/dev/null) + from=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('from','?'))" 2>/dev/null) + to=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('to','?'))" 2>/dev/null) + + [[ "$direction" == "request" ]] && requests=$((requests + 1)) + [[ "$direction" == "reply" ]] && replies=$((replies + 1)) + + case "$status" in + completed|delivered|replied) completed=$((completed + 1)) ;; + created|sent|pending) pending=$((pending + 1)) ;; + failed|send_failed|reply_failed) failed=$((failed + 1)) ;; + esac + + pane_counts["$from"]=$(( ${pane_counts["$from"]:-0} + 1 )) + pane_counts["$to"]=$(( ${pane_counts["$to"]:-0} + 1 )) + done < "$INDEX_FILE" + + echo -e "${BOLD}══ 消息统计 ══${RESET}" + echo "" + echo " 总计: ${total}" + echo " 请求: ${requests}" + echo " 回复: ${replies}" + echo "" + echo -e " ${GREEN}完成: ${completed}${RESET} ${YELLOW}待处理: ${pending}${RESET} ${RED}失败: ${failed}${RESET}" + echo "" + + echo "各面板活跃度:" + for pane in "${!pane_counts[@]}"; do + printf " %-6s %d 条\n" "$pane" "${pane_counts[$pane]}" + done +} + +# ─── 主入口 ─── + +# ─── 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 + recent) cmd_recent "$@" ;; + by-pane) cmd_by_pane "$@" ;; + show) cmd_show "$@" ;; + stats) cmd_stats ;; + list) cmd_list "$@" ;; + help|--help|-h) + echo "用法: tmux-msg-log.sh [options]" + echo "" + echo "命令:" + echo " recent [--count N] 最近 N 条消息(默认 20)" + echo " by-pane 指定面板的消息" + echo " show 消息详情" + echo " list [--from

] [--to

] [--status ]" + echo " 列出消息(可过滤)" + echo " stats 统计摘要" + ;; + *) + echo "错误: 未知命令 '$cmd'" >&2 + exit 1 + ;; +esac diff --git a/scripts/tmux-msg-notify.sh b/scripts/tmux-msg-notify.sh new file mode 100755 index 0000000..372f58f --- /dev/null +++ b/scripts/tmux-msg-notify.sh @@ -0,0 +1,160 @@ +#!/bin/bash +# tmux-msg-notify.sh — 面板间消息完成通知 +# +# 由接收方 cc 通过 Bash 工具调用,通知发送方任务完成。 +# +# 用法: +# tmux-msg-notify.sh "

" +# +# 行为: +# 1. 更新原消息状态为 completed +# 2. 创建回复信封 +# 3. 通过 tmux-send-prompt.sh 向发送方发送通知 +# +# 示例: +# tmux-msg-notify.sh msg-20260419-142357-a1b2 %1 "已完成用户 API 实现" + +set -euo pipefail + +MSG_ID="${1:?用法: tmux-msg-notify.sh \"\"}" +REPLY_TO_PANE="${2:?需要 reply_to_pane 参数}" +SUMMARY="${3:?需要完成摘要}" + +MSG_DIR="${TMUX_MSG_DIR:-/workspace/.tmux-messages}" +INDEX_FILE="${MSG_DIR}/index.jsonl" +SEND_SCRIPT="/workspace/scripts/tmux-send-prompt.sh" + +# ─── 辅助函数 ─── + +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" +} + +get_pane_title() { + local pane_id="$1" + tmux display-message -t "$pane_id" -p '#{pane_title}' 2>/dev/null || echo "未知" +} + +# ─── 主流程 ─── + +echo "[tmux-notify] 处理完成通知: ${MSG_ID}" + +# 查找原消息信封 +ENVELOPE_PATH=$(find "$MSG_DIR" -name "${MSG_ID}.json" -type f 2>/dev/null | head -1) + +if [[ -z "$ENVELOPE_PATH" ]]; then + echo "[tmux-notify] 警告: 未找到消息 ${MSG_ID},仍发送通知" + # 即使找不到原消息,也发送通知(宽容模式) + FROM_TITLE=$(get_pane_title "$TMUX_PANE") + TO_TITLE=$(get_pane_title "$REPLY_TO_PANE") + + REPLY_PROMPT="[通知] 来自 ${FROM_TITLE} 的完成通知 + +原始任务 ID: ${MSG_ID} +完成摘要: ${SUMMARY} + +无需回复此通知。" + # 直接发送 + PROMPT_DIR_FALLBACK="${MSG_DIR}/prompts" + mkdir -p "$PROMPT_DIR_FALLBACK" + TMP_PROMPT="${PROMPT_DIR_FALLBACK}/notify-${MSG_ID}.md" + echo "$REPLY_PROMPT" > "$TMP_PROMPT" + bash "$SEND_SCRIPT" "$REPLY_TO_PANE" "$TMP_PROMPT" 2>&1 || true + echo "[tmux-notify] 通知已发送(无信封模式)" + exit 0 +fi + +# 读取原消息 +ENVELOPE=$(cat "$ENVELOPE_PATH") + +# 解析原消息字段 +FROM_PANE=$(echo "$ENVELOPE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['from_pane'])" 2>/dev/null) +FROM_TITLE=$(echo "$ENVELOPE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['from_title'])" 2>/dev/null) +TO_TITLE=$(echo "$ENVELOPE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['to_title'])" 2>/dev/null) +TASK_SUMMARY=$(echo "$ENVELOPE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('task_summary','')[:80])" 2>/dev/null) + +NOW=$(date -Iseconds) + +# 更新原消息状态 +python3 -c " +import sys, json +with open('${ENVELOPE_PATH}') as f: + d = json.load(f) +d['status'] = 'completed' +d['replied_at'] = '${NOW}' +d['reply_summary'] = '''${SUMMARY}''' +with open('${ENVELOPE_PATH}', 'w') as f: + json.dump(d, f, ensure_ascii=False, indent=2) +" 2>/dev/null + +# 创建回复信封 +REPLY_MSG_ID="reply-${MSG_ID}" +DAY_DIR=$(date +%Y/%m/%d) +REPLY_ENVELOPE_PATH="${MSG_DIR}/${DAY_DIR}/${REPLY_MSG_ID}.json" + +SENDER_PANE="${TMUX_PANE:-%0}" +SENDER_TITLE=$(get_pane_title "$SENDER_PANE") + +REPLY_ENVELOPE=$(cat < "$TMP_PROMPT" + +echo "[tmux-notify] 发送通知到 ${REPLY_TO_PANE}..." + +if bash "$SEND_SCRIPT" "$REPLY_TO_PANE" "$TMP_PROMPT" 2>&1; then + # 更新回复状态 + DELIVERED_TS=$(date -Iseconds) + python3 -c " +import sys, json +with open('${REPLY_ENVELOPE_PATH}') as f: + d = json.load(f) +d['status'] = 'delivered' +d['delivered_at'] = '${DELIVERED_TS}' +with open('${REPLY_ENVELOPE_PATH}', 'w') as f: + json.dump(d, f, ensure_ascii=False, indent=2) +" 2>/dev/null + + echo "[tmux-notify] 通知已发送: ${REPLY_MSG_ID}" +else + echo "[tmux-notify] 警告: 通知发送失败,但完成状态已记录" +fi + +# 提示词文件保留,供发送方 cc 异步读取 +echo "[tmux-notify] 完成" diff --git a/scripts/tmux-msg.sh b/scripts/tmux-msg.sh new file mode 100755 index 0000000..2349b58 --- /dev/null +++ b/scripts/tmux-msg.sh @@ -0,0 +1,413 @@ +#!/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 diff --git a/scripts/tmux-send-prompt.sh b/scripts/tmux-send-prompt.sh index 4ab8c9c..82b3e8d 100755 --- a/scripts/tmux-send-prompt.sh +++ b/scripts/tmux-send-prompt.sh @@ -8,7 +8,12 @@ # 4. 文件内容过长时 send-keys 不可靠 # # 用法: -# tmux-send-prompt.sh [--submit-delay ] +# tmux-send-prompt.sh [options] +# +# 选项: +# --submit-delay 发送文本后等待多久再按 Enter(默认 1) +# --from 发送方 pane ID(用于日志) +# --no-verify 跳过提交验证(非阻塞模式) # # 行为: # - 文本字符串: 直接发送 @@ -18,16 +23,77 @@ # tmux-send-prompt.sh %1 "短文本提示" # tmux-send-prompt.sh %1 /path/to/prompt.md # tmux-send-prompt.sh %1 "文本" --submit-delay 2 +# tmux-send-prompt.sh %1 "文本" --from %2 --no-verify set -euo pipefail -PANE_ID="${1:?用法: tmux-send-prompt.sh [--submit-delay ]}" -TEXT_OR_FILE="${2:?需要提供文本或文件路径}" -SUBMIT_DELAY="${3:-1}" +# ─── 参数解析 ─── +PANE_ID='' +TEXT_OR_FILE='' +SUBMIT_DELAY=1 +FROM_PANE='' +NO_VERIFY=false -# 构建发送内容:文件则让 cc 自行读取,文本则直接发送 +while [[ $# -gt 0 ]]; do + case "$1" in + --submit-delay) + SUBMIT_DELAY="${2:?--submit-delay 需要一个数值参数}" + shift 2 + ;; + --from) + FROM_PANE="${2:?--from 需要一个 pane ID}" + shift 2 + ;; + --no-verify) + NO_VERIFY=true + shift + ;; + -*) + echo "错误: 未知选项 $1" >&2 + exit 1 + ;; + *) + if [[ -z "$PANE_ID" ]]; then + PANE_ID="$1" + elif [[ -z "$TEXT_OR_FILE" ]]; then + TEXT_OR_FILE="$1" + elif [[ "$1" =~ ^[0-9]+$ ]]; then + # 向后兼容: 第三个位置参数为数字时视为 submit-delay + SUBMIT_DELAY="$1" + else + echo "错误: 多余参数 '$1'" >&2 + exit 1 + fi + shift + ;; + esac +done + +if [[ -z "$PANE_ID" ]]; then + echo "用法: tmux-send-prompt.sh [options]" >&2 + exit 1 +fi +if [[ -z "$TEXT_OR_FILE" ]]; then + echo "错误: 需要提供文本或文件路径" >&2 + exit 1 +fi + +# ─── 日志函数 ─── +LOG_FILE="${TMUX_SEND_LOG:-/workspace/.tmux-send-prompt.log}" + +log_call() { + local status="$1" detail="${2:-}" + local ts + ts=$(date -Iseconds) + local from_field="${FROM_PANE:-null}" + local text_len=${#TEXT_OR_FILE} + printf '{"ts":"%s","from":"%s","to":"%s","text_len":%d,"status":"%s","detail":"%s"}\n' \ + "$ts" "$from_field" "$PANE_ID" "$text_len" "$status" "$detail" \ + >> "$LOG_FILE" 2>/dev/null || true +} + +# ─── 构建发送内容 ─── if [[ -f "$TEXT_OR_FILE" ]]; then - # 解析为绝对路径,确保 cc 能正确找到文件 FILE_PATH=$(realpath "$TEXT_OR_FILE") TEXT="请读取 ${FILE_PATH} 并执行其中的内容" echo "[tmux-send] 检测到文件: ${FILE_PATH},将指示 cc 自行读取" @@ -35,39 +101,54 @@ else TEXT="$TEXT_OR_FILE" fi -# 步骤1: 等待 cc 的 prompt 出现(❯ 字符) +# ─── 步骤1: 等待 cc 的 prompt 出现(❯ 字符)─── +# cc 状态栏在 ❯ 下方占 3 行,需要 tail -6 才能捕获 echo "[tmux-send] 等待 cc 的 prompt 就绪..." for i in $(seq 1 30); do - LAST_LINE=$(tmux capture-pane -t "$PANE_ID" -p | tail -3 | grep -c '❯' || true) + LAST_LINE=$(tmux capture-pane -t "$PANE_ID" -p 2>/dev/null | tail -6 | grep -c '❯' || true) if [[ "$LAST_LINE" -ge 1 ]]; then echo "[tmux-send] cc 已就绪 (等待 ${i}s)" break fi + if [[ "$i" -eq 30 ]]; then + echo "[tmux-send] 警告: 30s 内未检测到 prompt,尝试发送" + log_call "timeout_wait" "prompt_not_found" + fi sleep 1 done -# 步骤2: 发送文本(不带 Enter) +# ─── 步骤2: 发送文本(不带 Enter)─── tmux send-keys -t "$PANE_ID" "$TEXT" sleep "${SUBMIT_DELAY}" -# 步骤3: 单独发送 Enter +# ─── 步骤3: 单独发送 Enter ─── tmux send-keys -t "$PANE_ID" Enter sleep 2 -# 步骤4: 验证 — 检查 ❯ 是否消失(cc 正在处理输入) -# 注意: 不能用文本匹配验证,因为 cc 会在输出中回显用户消息,导致误判 -sleep 2 -for i in $(seq 1 5); do - CAPTURED=$(tmux capture-pane -t "$PANE_ID" -p | tail -5) - if ! echo "$CAPTURED" | grep -q '❯'; then - echo "[tmux-send] 命令已成功提交 (第 $i 次检查)" +# ─── 步骤4: 验证(可选)─── +# cc 的 ❯ 始终在底部,无法通过 ❯ 消失判断提交成功。 +# 改为:对比发送前后 pane 内容,若内容变化则说明已提交。 +if [[ "$NO_VERIFY" == true ]]; then + echo "[tmux-send] 跳过验证(--no-verify)" + log_call "sent_no_verify" + exit 0 +fi + +BEFORE_HASH=$(tmux capture-pane -t "$PANE_ID" -p 2>/dev/null | md5sum | cut -d' ' -f1) + +for i in $(seq 1 3); do + sleep 3 + AFTER_HASH=$(tmux capture-pane -t "$PANE_ID" -p 2>/dev/null | md5sum | cut -d' ' -f1) + if [[ "$BEFORE_HASH" != "$AFTER_HASH" ]]; then + echo "[tmux-send] 命令已成功提交 (第 $i 次检查,内容已变化)" + log_call "success" "content_changed_on_attempt_$i" exit 0 fi - # ❯ 仍在,可能 Enter 未生效,重试 - echo "[tmux-send] ❯ 仍在,重试 Enter 第 $i 次..." + # 内容未变化,可能 Enter 未生效,重试一次 + echo "[tmux-send] 内容未变化,重试 Enter 第 $i 次..." tmux send-keys -t "$PANE_ID" Enter - sleep 3 done -echo "[tmux-send] 警告: 5次重试后 ❯ 仍存在,请手动检查" +log_call "failed" "content_unchanged_after_3_retries" +echo "[tmux-send] 警告: 3次重试后内容仍未变化,请手动检查" exit 1