功能(ClaudeCode): 新增飞书联动工具链与会话管理增强

- 新增命令: lark-delegate(跨项目委派)、lark-inbox(收件箱)、lark-me(自发送)、stop(任务收尾)
- 新增工具: lark-consumer/consumer-ensure/inbox-check(飞书事件实时消费与会话隔离)、tmux-monitor(监控面板)
- runcc.sh 增加 --list/--kill/--send 操作模式与多会话自增命名
- CLAUDE.md 增加强制要求(汇报格式、收尾通知、上下文压缩)
This commit is contained in:
2026-07-03 14:49:44 +08:00
parent cd6a130b29
commit 911503d997
11 changed files with 1455 additions and 39 deletions
+508
View File
@@ -0,0 +1,508 @@
#!/bin/bash
# ==============================================================================
# tmux 实时监控面板 — 通用版
#
# 在当前 tmux 窗口右侧(或左侧)开启一个固定宽度的监控面板,实时展示全部
# 会话 / 窗口 / 面板的层级状态、运行命令、路径与状态标签,按终端宽度自适应。
#
# 使用方法:
# tmux-monitor.sh # 创建监控面板 (默认 1 秒刷新, 右侧, 宽 60)
# tmux-monitor.sh 3 # 创建监控面板 (自定义刷新间隔)
# tmux-monitor.sh -w 80 # 指定面板宽度
# tmux-monitor.sh -p L # 面板放左侧 (默认 R 右侧)
# tmux-monitor.sh -w 80 -p L 2 # 组合: 宽 80 / 左侧 / 2 秒刷新
# tmux-monitor.sh -k # 关闭监控面板
# tmux-monitor.sh -r # 重启监控面板
# tmux-monitor.sh -s # 查看监控面板状态
# tmux-monitor.sh -h # 显示帮助
# tmux-monitor.sh --display # (内部) 运行显示循环
#
# 通用性说明:
# - 脚本路径自动解析 (readlink -f BASH_SOURCE), 可软链到任意位置运行
# - 面板定位使用全局唯一的 pane_id, 不受窗口切换 / 多会话影响
# - 不依赖任何特定工作目录
# ==============================================================================
# ── 脚本自身路径解析(核心:去除硬编码,支持软链) ──────────
# 优先 BASH_SOURCE(即使被 source 也能定位),readlink -f 解析符号链接
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
SCRIPT_NAME="$(basename "$SCRIPT_PATH")"
# ── 默认配置 ─────────────────────────────────────────────
DEFAULT_INTERVAL=1
DEFAULT_WIDTH=60
DEFAULT_POS="R" # R=右侧, L=左侧
# ── 参数解析 ──────────────────────────────────────────────
ACTION="create"
INTERVAL=""
WIDTH=""
POS=""
while [ $# -gt 0 ]; do
case "$1" in
--display)
ACTION="display"; shift
;;
-k|--kill)
ACTION="kill"; shift
;;
-r|--restart)
ACTION="restart"; shift
;;
-s|--status)
ACTION="status"; shift
;;
-h|--help)
ACTION="help"; shift
;;
-w|--width)
[ $# -ge 2 ] || { echo "错误: $1 需要参数" >&2; exit 1; }
WIDTH="$2"; shift 2
;;
-p|--pos)
[ $# -ge 2 ] || { echo "错误: $1 需要参数" >&2; exit 1; }
POS="$2"; shift 2
;;
--)
shift; break
;;
-*)
echo "未知选项: $1 (使用 -h 查看帮助)" >&2; exit 1
;;
*)
INTERVAL="$1"; shift
;;
esac
done
# 应用默认值
INTERVAL="${INTERVAL:-$DEFAULT_INTERVAL}"
WIDTH="${WIDTH:-$DEFAULT_WIDTH}"
POS="${POS:-$DEFAULT_POS}"
# 规范化位置参数:L→左, 其余一律视为 R
case "$(echo "$POS" | tr '[:lower:]' '[:upper:]')" in
L|LEFT) POS="L" ;;
*) POS="R" ;;
esac
# ── 前置检查 ──────────────────────────────────────────────
if ! command -v tmux &>/dev/null; then
echo "错误: tmux 未安装" >&2
exit 1
fi
# ── 管理函数 ──────────────────────────────────────────────
# 查找运行中的监控面板,返回其 pane_id(全局唯一,形如 %5)
find_monitor_pane() {
tmux list-panes -a -F '#{pane_id} #{pane_start_command}' 2>/dev/null \
| grep -iF "$SCRIPT_NAME" \
| head -1 \
| awk '{print $1}'
}
do_kill() {
local pane_id
pane_id=$(find_monitor_pane)
if [ -n "$pane_id" ]; then
tmux kill-pane -t "$pane_id" 2>/dev/null
echo "监控面板 (${pane_id}) 已关闭"
return 0
else
echo "未找到运行中的监控面板"
return 1
fi
}
do_status() {
local pane_id
pane_id=$(find_monitor_pane)
if [ -n "$pane_id" ]; then
# 通过 pane_id 反查其所属会话/窗口/尺寸
local info
info=$(tmux list-panes -a -F \
'#{pane_id}|#{session_name}:#{window_index}.#{pane_index}|#{pane_width}x#{pane_height}|#{pane_dead}' \
2>/dev/null | grep "^${pane_id}|")
if [ -n "$info" ]; then
local pid_loc sw size dead
pid_loc="${info%%|*}"
info="${info#*|}"
sw="${info%%|*}"
info="${info#*|}"
size="${info%%|*}"
dead="${info##*|}"
local state="运行中"
[ "$dead" = "1" ] && state="${R:-}已死亡${N:-}"
echo "监控面板状态: ${state}"
echo " pane_id : ${pid_loc}"
echo " 位置 : ${sw}"
echo " 尺寸 : ${size}"
echo " 脚本 : ${SCRIPT_PATH}"
else
echo "监控面板 (${pane_id}) 状态不可读"
fi
return 0
else
echo "未找到运行中的监控面板"
return 1
fi
}
do_create() {
if [ -z "${TMUX:-}" ]; then
echo "错误: 需要在 tmux 会话内运行 (检测到 \$TMUX 为空)" >&2
return 1
fi
# 宽度校验:必须为正整数
case "$WIDTH" in
''|*[!0-9]*) echo "错误: 面板宽度必须为正整数 (当前: $WIDTH)" >&2; return 1 ;;
esac
[ "$WIDTH" -ge 10 ] || { echo "错误: 面板宽度过小 (最小 10, 当前: $WIDTH)" >&2; return 1; }
# 关闭已有监控面板(幂等:支持反复创建)
local pane_id
pane_id=$(find_monitor_pane)
if [ -n "$pane_id" ]; then
tmux kill-pane -t "$pane_id" 2>/dev/null
fi
# 刷新间隔校验:必须为正整数
case "$INTERVAL" in
''|*[!0-9]*) echo "错误: 刷新间隔必须为正整数 (当前: $INTERVAL)" >&2; return 1 ;;
esac
# 通过环境变量把刷新间隔传入 display 子进程
local launch_cmd="TMUX_MONITOR_INTERVAL=${INTERVAL} bash '${SCRIPT_PATH}' --display"
# 按位置创建面板:-h 水平分割;-b 将新面板置于左侧
if [ "$POS" = "L" ]; then
tmux split-window -hb -l "$WIDTH" "$launch_cmd"
else
tmux split-window -h -l "$WIDTH" "$launch_cmd"
fi
echo "监控面板已启动 (${INTERVAL} 秒刷新, ${POS}侧, 宽 ${WIDTH})"
}
# ── 非显示操作: 执行后退出 ───────────────────────────────
if [ "$ACTION" != "display" ]; then
case "$ACTION" in
help)
cat <<EOF
用法: ${SCRIPT_NAME} [选项] [刷新间隔]
选项:
(空) 创建监控面板 (默认 ${DEFAULT_INTERVAL} 秒刷新, ${DEFAULT_POS}侧, 宽 ${DEFAULT_WIDTH})
<秒数> 自定义刷新间隔
-w, --width N 面板宽度 (默认 ${DEFAULT_WIDTH})
-p, --pos L|R 面板位置: L=左 R=右 (默认 ${DEFAULT_POS})
-k, --kill 关闭监控面板
-r, --restart 重启监控面板
-s, --status 查看监控面板状态
-h, --help 显示本帮助
--display (内部) 运行显示循环
示例:
${SCRIPT_NAME} # 默认创建
${SCRIPT_NAME} 3 # 3 秒刷新
${SCRIPT_NAME} -w 80 -p L 2 # 宽 80 / 左侧 / 2 秒
${SCRIPT_NAME} -k # 关闭面板
EOF
;;
kill)
do_kill
;;
status)
do_status
;;
restart)
do_kill 2>/dev/null || true
do_create
;;
create)
do_create
;;
esac
exit $?
fi
# ══════════════════════════════════════════════════════════
# 以下为 display 模式: 运行监控显示循环
# ══════════════════════════════════════════════════════════
# 颜色定义 — 使用 $'...' ANSI-C 引号,赋值时即产生真正的 ESC 字节
readonly R=$'\033[0;31m' # RED
readonly G=$'\033[0;32m' # GREEN
readonly Y=$'\033[0;33m' # YELLOW
readonly B=$'\033[0;34m' # BLUE
readonly P=$'\033[0;35m' # PURPLE
readonly C=$'\033[0;36m' # CYAN
readonly D=$'\033[0;90m' # GRAY/DIM
readonly BD=$'\033[1m' # BOLD
readonly N=$'\033[0m' # RESET
# 刷新间隔(秒)— 由管理模式通过 TMUX_MONITOR_INTERVAL 传入
INTERVAL="${TMUX_MONITOR_INTERVAL:-$DEFAULT_INTERVAL}"
# ── 工具函数 ──────────────────────────────────────────────
# 生成指定宽度的分隔线
make_sep() {
local width="${1:-80}"
local sep_char='─'
printf '%*s' "$width" '' | tr ' ' "$sep_char"
}
# 判断字符串是否为非负整数
is_uint() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
# ── 显示层级常量 ─────────────────────────────────────────
# 根据终端宽度决定显示详细程度
LVL_MINIMAL=0 # < 50 列:仅显示核心信息
LVL_COMPACT=1 # 50-69 列:省略 PID、路径、子命令
LVL_NORMAL=2 # 70-99 列:省略子命令详情
LVL_FULL=3 # >= 100 列:显示全部
get_display_level() {
local cols="$1"
if [ "$cols" -lt 50 ]; then
echo "$LVL_MINIMAL"
elif [ "$cols" -lt 70 ]; then
echo "$LVL_COMPACT"
elif [ "$cols" -lt 100 ]; then
echo "$LVL_NORMAL"
else
echo "$LVL_FULL"
fi
}
# 记录终端尺寸,用于检测 resize
last_cols=0
last_rows=0
while true; do
# ── 检测终端尺寸变化,清屏防重叠 ──
cur_cols=$(tput cols 2>/dev/null || echo 80)
cur_rows=$(tput lines 2>/dev/null || echo 24)
if [ "$cur_cols" != "$last_cols" ] || [ "$cur_rows" != "$last_rows" ]; then
tput clear
last_cols=$cur_cols
last_rows=$cur_rows
fi
# 当前显示层级
level=$(get_display_level "$cur_cols")
# 动态分隔线
SEP=$(make_sep "$cur_cols")
# ── 构建输出到缓冲区 ──
buf=""
buf="${buf}\n"
buf="${buf}${BD}${SEP}${N}\n"
# 标题行:自适应宽度
title=" tmux 实时监控 $(date '+%H:%M:%S')"
if [ "$level" -ge "$LVL_COMPACT" ]; then
buf="${buf}${C}${BD}${title}${N}\n"
else
buf="${buf}${C}${BD} 监控 $(date '+%H:%M:%S')${N}\n"
fi
buf="${buf}${BD}${SEP}${N}\n"
buf="${buf}\n"
cur_session=$(tmux display-message -p '#{session_name}' 2>/dev/null)
# 一次性获取所有面板信息(含状态字段)
panes_info=$(tmux list-panes -a -F \
'#{session_name}|#{session_created}|#{session_attached}|#{window_index}|#{window_name}|#{window_active}|#{window_width}x#{window_height}|#{pane_index}|#{pane_title}|#{pane_current_command}|#{pane_current_path}|#{pane_active}|#{pane_pid}|#{pane_width}x#{pane_height}|#{pane_dead}|#{pane_dead_status}|#{pane_in_mode}|#{pane_mode}' \
2>/dev/null)
total_sessions=0
total_windows=0
total_panes=0
session_idx=0
if [ -n "$panes_info" ]; then
prev_session=""
prev_window=""
while IFS='|' read -r ssn s_created s_attached widx wname w_active w_size pidx ptitle cmd path active pid p_size p_dead p_dead_status p_in_mode p_mode; do
# ── 新会话 ──
if [ "$ssn" != "$prev_session" ]; then
[ -n "$prev_session" ] && buf="${buf}\n"
total_sessions=$((total_sessions + 1))
session_idx=$((session_idx + 1))
if [ "$ssn" = "$cur_session" ]; then
s_marker="${G}${N}"
s_name="${BD}${G}${ssn}${N}"
else
s_marker="${D}${N}"
s_name="${P}${ssn}${N}"
fi
# Session 状态标签
s_status=""
if [ "$s_attached" = "0" ]; then
s_status=" ${Y}${BD}[detached]${N}"
fi
s_time=""
if [ "$level" -ge "$LVL_NORMAL" ] && is_uint "$s_created" && [ "$s_created" -gt 0 ]; then
s_time=$(date -d "@$s_created" '+%m-%d %H:%M' 2>/dev/null)
fi
# 根据宽度调整 Session 行
if [ "$level" -ge "$LVL_COMPACT" ]; then
s_line=" ${s_marker} ${BD}Session${N} ${session_idx} ${s_name}${s_status}"
else
s_line=" ${s_marker} ${BD}S${N}${session_idx} ${s_name}${s_status}"
fi
[ -n "$s_time" ] && s_line="${s_line} ${D}[自 ${s_time}]${N}"
buf="${buf}${s_line}\n"
prev_session="$ssn"
prev_window=""
fi
# ── 新窗口 ──
if [ "$widx:$wname" != "$prev_window" ]; then
total_windows=$((total_windows + 1))
if [ "$w_active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
w_marker="${G}${N}"
w_name="${BD}${G}${wname}${N}"
else
w_marker="${D}${N}"
w_name="${Y}${wname}${N}"
fi
# Window 状态标签
w_status=""
if [ "$w_active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
w_status=" ${G}[active]${N}"
fi
# 根据宽度决定是否显示尺寸
if [ "$level" -ge "$LVL_COMPACT" ]; then
w_line=" ${D}├─${N} ${w_marker} ${BD}Win${N} ${widx} ${w_name}${w_status} ${D}[${w_size}]${N}"
else
w_line=" ${D}${N} ${w_marker} ${w_name}${w_status}"
fi
buf="${buf}${w_line}\n"
prev_window="$widx:$wname"
fi
# ── 面板 ──
total_panes=$((total_panes + 1))
if [ "$active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
p_marker="${G}${N}"
else
p_marker="${D}${N}"
fi
case "$cmd" in
bash|zsh|fish|sh) cmd_color="$D" ;;
vim|nvim|vi) cmd_color="$G" ;;
python|python3) cmd_color="$Y" ;;
node) cmd_color="$G" ;;
ssh) cmd_color="$R" ;;
"$SCRIPT_NAME") cmd_color="$C" ;;
*) cmd_color="$N" ;;
esac
# 路径缩写:根据层级决定缩写程度
short_path=""
if [ "$level" -ge "$LVL_NORMAL" ]; then
short_path=$(echo "$path" | sed "s|^$HOME|~|" | awk -F/ '{
if(NF>3) print ".."/$(NF-1)/$NF; else print $0
}')
elif [ "$level" -ge "$LVL_COMPACT" ]; then
short_path=$(echo "$path" | sed "s|^$HOME|~|" | awk -F/ '{
print $NF
}')
fi
# 子命令:仅在 FULL 级别显示
child_cmd=""
if [ "$level" -ge "$LVL_FULL" ] && [ "$(uname)" = "Linux" ]; then
child_pid=$(ps --ppid "$pid" -o pid= 2>/dev/null | head -1 | tr -d ' ')
if [ -n "$child_pid" ]; then
child_cmd=$(ps -p "$child_pid" -o args= 2>/dev/null)
fi
fi
# 格式: 根据层级决定显示内容
p_title="${ptitle}"
[ -z "$p_title" ] && p_title="$cmd"
# Pane 状态标签
p_status=""
if [ "$p_dead" = "1" ]; then
p_status=" ${R}${BD}[dead:${p_dead_status}]${N}"
fi
if [ "$p_in_mode" = "1" ] && [ -n "$p_mode" ]; then
p_status="${p_status} ${Y}[${p_mode}]${N}"
fi
if [ "$active" = "1" ] && [ "$ssn" = "$cur_session" ]; then
p_status="${p_status} ${G}[active]${N}"
fi
if [ "$level" -ge "$LVL_NORMAL" ]; then
# NORMAL / FULL: Pane 编号 + 标题 + 命令 + 尺寸 + 状态
p_line=" ${D}│ ├─${N} ${p_marker} ${BD}Pane${N} ${pidx} ${C}${p_title}${N} ${cmd_color}${cmd}${N} ${D}[${p_size}]${N}${p_status}"
if [ "$level" -ge "$LVL_FULL" ]; then
p_line="${p_line} ${D}PID:${pid}${N}"
fi
elif [ "$level" -ge "$LVL_COMPACT" ]; then
# COMPACT: Pane 编号 + 标题 + 命令
p_line=" ${D}│ ├${N} ${p_marker} ${BD}P${N}${pidx} ${C}${p_title}${N} ${cmd_color}${cmd}${N}"
else
# MINIMAL: 仅标题 + 命令
p_line=" ${D}${N} ${p_marker} ${C}${p_title}${N}"
fi
buf="${buf}${p_line}\n"
if [ -n "$child_cmd" ]; then
c_line=" ${D}│ │ ↳${N} ${D}${child_cmd}${N}"
buf="${buf}${c_line}\n"
fi
if [ -n "$short_path" ]; then
sp_line=" ${D}│ │ ${N}${D}${short_path}${N}"
buf="${buf}${sp_line}\n"
fi
done <<< "$panes_info"
fi
# ── 系统概要 ──
buf="${buf}\n"
buf="${buf}${D}${SEP}${N}\n"
buf="${buf} ${BD}总计:${N} ${total_sessions} 会话 | ${total_windows} 窗口 | ${total_panes} 面板\n"
buf="${buf}${D}${SEP}${N}\n"
buf="${buf} ${D}${INTERVAL} 秒刷新 | Ctrl+C 退出${N}\n"
# ── 双缓冲输出:移到顶部 → 写入 → 清除尾部残留 ──
# 每行末尾添加 \033[K(清除到行尾),防止短行覆盖长行时残留旧字符
buf="${buf//\\n/\\033[K\\n}"
tput cup 0 0
printf '%b' "$buf"
tput ed
sleep "$INTERVAL"
done