@@ -73,3 +73,6 @@ coverage.json
|
||||
# uv
|
||||
.python-version.bak
|
||||
uv.lock
|
||||
|
||||
# tmux 消息
|
||||
.tmux-messages/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
## 项目历史
|
||||
|
||||
|
||||
@@ -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:<path>/**'` 取消跟踪
|
||||
3. 验证 `jj status` 中不再显示该路径
|
||||
Executable
+356
@@ -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" <<ENVEOF
|
||||
{"msg_id":"${test_msg_id}","direction":"request","from_pane":"%1","from_title":"PM","to_pane":"%2","to_title":"后端","task_summary":"测试任务","task_file":null,"status":"sent","created_at":"2026-04-19T12:00:00+08:00","delivered_at":"2026-04-19T12:00:01+08:00","replied_at":null,"reply_msg_id":null,"reply_summary":null,"error":null}
|
||||
ENVEOF
|
||||
echo "{\"msg_id\":\"${test_msg_id}\",\"direction\":\"request\",\"from\":\"%1\",\"to\":\"%2\",\"status\":\"sent\",\"summary\":\"测试任务\",\"ts\":\"2026-04-19T12:00:00+08:00\"}" >> "${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
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#!/bin/bash
|
||||
# tmux-msg-log.sh — 消息日志查询工具
|
||||
#
|
||||
# 用法:
|
||||
# tmux-msg-log.sh recent [--count 20]
|
||||
# tmux-msg-log.sh by-pane <pane_id>
|
||||
# tmux-msg-log.sh show <msg_id>
|
||||
# tmux-msg-log.sh stats
|
||||
# tmux-msg-log.sh log <msg_id> "<status_note>"
|
||||
#
|
||||
# 示例:
|
||||
# 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 <pane_id>}"
|
||||
|
||||
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 <msg_id>}"
|
||||
|
||||
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 <command> [options]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " recent [--count N] 最近 N 条消息(默认 20)"
|
||||
echo " by-pane <pane_id> 指定面板的消息"
|
||||
echo " show <msg_id> 消息详情"
|
||||
echo " list [--from <p>] [--to <p>] [--status <s>]"
|
||||
echo " 列出消息(可过滤)"
|
||||
echo " stats 统计摘要"
|
||||
;;
|
||||
*)
|
||||
echo "错误: 未知命令 '$cmd'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
# tmux-msg-notify.sh — 面板间消息完成通知
|
||||
#
|
||||
# 由接收方 cc 通过 Bash 工具调用,通知发送方任务完成。
|
||||
#
|
||||
# 用法:
|
||||
# tmux-msg-notify.sh <msg_id> <reply_to_pane> "<summary>"
|
||||
#
|
||||
# 行为:
|
||||
# 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 <msg_id> <reply_to_pane> \"<summary>\"}"
|
||||
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 <<EOJSON
|
||||
{"msg_id":"${REPLY_MSG_ID}","direction":"reply","in_reply_to":"${MSG_ID}","from_pane":"${SENDER_PANE}","from_title":"${SENDER_TITLE}","to_pane":"${REPLY_TO_PANE}","to_title":"${FROM_TITLE}","task_summary":null,"task_file":null,"status":"created","created_at":"${NOW}","delivered_at":null,"replied_at":null,"reply_msg_id":null,"reply_summary":"${SUMMARY}","error":null}
|
||||
EOJSON
|
||||
)
|
||||
|
||||
write_envelope "$REPLY_ENVELOPE_PATH" "$REPLY_ENVELOPE"
|
||||
|
||||
# 写入索引
|
||||
INDEX_LINE=$(printf '{"msg_id":"%s","direction":"reply","in_reply_to":"%s","from":"%s","to":"%s","status":"created","summary":"%s","ts":"%s"}\n' \
|
||||
"$REPLY_MSG_ID" "$MSG_ID" "$SENDER_PANE" "$REPLY_TO_PANE" "${SUMMARY}" "$NOW")
|
||||
append_index "$INDEX_LINE"
|
||||
|
||||
# 构建通知提示词
|
||||
REPLY_PROMPT="## 任务完成通知 [${REPLY_MSG_ID}]
|
||||
|
||||
来自: ${SENDER_TITLE} (Pane ${SENDER_PANE})
|
||||
原始任务: ${TASK_SUMMARY}
|
||||
消息 ID: ${MSG_ID}
|
||||
完成摘要: ${SUMMARY}
|
||||
|
||||
无需回复此通知。"
|
||||
|
||||
# 写提示词文件(持久化,供发送方 cc 异步读取)
|
||||
PROMPT_DIR="${MSG_DIR}/prompts"
|
||||
mkdir -p "$PROMPT_DIR"
|
||||
TMP_PROMPT="${PROMPT_DIR}/notify-${MSG_ID}.md"
|
||||
echo "$REPLY_PROMPT" > "$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] 完成"
|
||||
Executable
+413
@@ -0,0 +1,413 @@
|
||||
#!/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
|
||||
+102
-21
@@ -8,7 +8,12 @@
|
||||
# 4. 文件内容过长时 send-keys 不可靠
|
||||
#
|
||||
# 用法:
|
||||
# tmux-send-prompt.sh <pane_id> <text_string|text_file> [--submit-delay <seconds>]
|
||||
# tmux-send-prompt.sh <pane_id> <text_string|text_file> [options]
|
||||
#
|
||||
# 选项:
|
||||
# --submit-delay <seconds> 发送文本后等待多久再按 Enter(默认 1)
|
||||
# --from <pane_id> 发送方 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 <pane_id> <text|file> [--submit-delay <seconds>]}"
|
||||
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 <pane_id> <text|file> [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
|
||||
|
||||
Reference in New Issue
Block a user