Files
tech/.claude/tools/lark-consumer-ensure.sh
T
arno 911503d997 功能(ClaudeCode): 新增飞书联动工具链与会话管理增强
- 新增命令: lark-delegate(跨项目委派)、lark-inbox(收件箱)、lark-me(自发送)、stop(任务收尾)
- 新增工具: lark-consumer/consumer-ensure/inbox-check(飞书事件实时消费与会话隔离)、tmux-monitor(监控面板)
- runcc.sh 增加 --list/--kill/--send 操作模式与多会话自增命名
- CLAUDE.md 增加强制要求(汇报格式、收尾通知、上下文压缩)
2026-07-03 14:49:44 +08:00

206 lines
7.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================================
# SessionStart Hook — 飞书事件消费者生命周期管理
#
# 通过环境变量适配不同项目(szis / dit),PID/日志文件按 LARK_PROFILE 后缀分离,
# cleanup 只清理本项目进程,绝不误杀其他项目的 consumer。
# 由 settings.json 的 SessionStart hook 无参调用。
#
# 环境变量(均在项目 settings.json 中配置):
# LARK_PROFILE - lark-cli profile 名称,同时用于进程/日志文件命名隔离
# LARK_INBOX_NDJSON_PATH - inbox ndjson 文件相对路径(相对于项目根目录)
# 可选:
# SUBMIT_DELAY - tmux send-keys 延迟秒数(默认 0.5
# LARK_SENDER_ALLOW - sender 白名单 open_id(优先于 LARK_ME_USER_ID
# LARK_ME_USER_ID - sender 白名单 open_idfallback
# ============================================================================
set -euo pipefail
PROJECT_ROOT="${PWD}"
INBOX_FILE="${PROJECT_ROOT}/${LARK_INBOX_NDJSON_PATH:-01-输入/lark-inbox.ndjson}"
SUBMIT_DELAY="${SUBMIT_DELAY:-0.5}"
MAX_DIAG_LINES=500
# ── 从自身位置推导 consumer 脚本路径(与 ensure 脚本同目录)──
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONSUMER_SCRIPT="${SCRIPT_DIR}/lark-consumer.sh"
# ── 按 profile 隔离的 PID/日志文件 ──
PROFILE_NAME="${LARK_PROFILE:-szis}"
PID_FILE="/tmp/lark-consumer-${PROFILE_NAME}.pid"
CONSUMER_LOG="/tmp/lark-consumer-${PROFILE_NAME}.log"
DIAG_LOG="/tmp/lark-consumer-ensure-${PROFILE_NAME}.log"
# ── 日志函数 ──
log() {
local ts lines
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${ts}] $*" >> "${DIAG_LOG}"
lines=$(wc -l < "${DIAG_LOG}" 2>/dev/null || echo 0)
if [ "${lines}" -gt "${MAX_DIAG_LINES}" ]; then
tail -n "${MAX_DIAG_LINES}" "${DIAG_LOG}" > "${DIAG_LOG}.tmp" && mv "${DIAG_LOG}.tmp" "${DIAG_LOG}"
fi
}
# ── 项目隔离清理:只杀本 profile 的消费者进程 ──
cleanup_stale() {
log "---- 开始清理历史 ${PROFILE_NAME} 消费者进程 ----"
local killed=0 p ancestor sig pgid
local -A PROTECT=()
# 收集自身及所有祖先 PID(受保护,绝不杀)
set +e
ancestor=$$
while [ -n "$ancestor" ] && [ "$ancestor" != 0 ] && [ "$ancestor" != 1 ]; do
PROTECT["$ancestor"]=1
ancestor=$(ps -o ppid= -p "$ancestor" 2>/dev/null | tr -d ' ')
done
log "受保护(自身+祖先) PID: ${!PROTECT[*]}"
# 1) 按 PID_FILE 里的 leader 杀整个进程组(setsid 启动,leader 即进程组长)
if [ -f "${PID_FILE}" ]; then
local leader
leader=$(cat "${PID_FILE}" 2>/dev/null)
if [ -n "${leader}" ]; then
pgid=$(ps -o pgid= -p "${leader}" 2>/dev/null | tr -d ' ')
if [ -n "${pgid}" ]; then
kill -- -"${pgid}" 2>/dev/null
log "kill PID_FILE 进程组 -${pgid} (leader=${leader})"
fi
fi
fi
# 2) 按 INBOX_FILE 路径匹配 consumer.sh 进程,PGID 整组清除
for p in $(pgrep -f "lark-consumer\.sh --inbox-file ${INBOX_FILE}" 2>/dev/null || true); do
[ -n "${PROTECT[$p]:-}" ] && continue
pgid=$(ps -o pgid= -p "$p" 2>/dev/null | tr -d ' ')
if [ -n "${pgid}" ]; then
kill -- -"${pgid}" 2>/dev/null
log "kill consumer 进程组 -${pgid} (pid=$p)"
killed=$((killed + 1))
fi
done
# 3) 兜底:杀残留的 --profile <name> consume
for sig in TERM KILL; do
[ "$sig" = "KILL" ] && sleep 0.5
for p in $(pgrep -f "lark-cli .*--profile ${PROFILE_NAME} .*event consume im\.message\.receive_v1" 2>/dev/null || true); do
[ -n "${PROTECT[$p]:-}" ] && continue
kill "-${sig}" "$p" 2>/dev/null
log "$sig consume pid=$p"
killed=$((killed + 1))
done
done
set -e
rm -f "${PID_FILE}" 2>/dev/null || true
log "清理完成,处理 $killed 个进程句柄"
}
# ── 检测当前 CC pane(沿祖先链找有效 tty,再匹配 tmux pane_tty)──
detect_cc_pane() {
(
set +e
local pid=$$ tty pane
while [ -n "$pid" ] && [ "$pid" != 0 ] && [ "$pid" != 1 ]; do
tty=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
case "$tty" in
''|'?'|'??') : ;; # 无效 tty,继续向上
*)
pane=$(tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null \
| awk -v t="/dev/$tty" '$2==t{print $1; exit}')
[ -n "$pane" ] && { echo "$pane"; exit 0; }
;;
esac
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done
exit 1
)
}
log "========== lark-consumer-ensure (${PROFILE_NAME}) 开始 =========="
log "PWD=${PROJECT_ROOT} INBOX=${INBOX_FILE} PID_FILE=${PID_FILE}"
# ═══ 0. 检查 profile 是否已创建(未创建则安全跳过)═══
if ! lark-cli profile list 2>/dev/null | grep -q "\"name\": \"${PROFILE_NAME}\""; then
log "profile ${PROFILE_NAME} 未创建,跳过 consumer 启动(待 profile add ${PROFILE_NAME} 后生效)"
exit 0
fi
# ═══ 1. 清理历史(仅本项目)═══
cleanup_stale
# ═══ 2. 检查 bus daemon ═══
BUS_JSON=$(lark-cli --profile "${PROFILE_NAME}" event status --json 2>&1 || true)
log "event status: ${BUS_JSON}"
BUS_ALIVE=$(printf '%s' "${BUS_JSON}" | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
print(str(any(a.get('running',False) for a in d.get('apps',[]))).lower())
except Exception:
print('false')
" 2>/dev/null || echo "false")
log "BUS_ALIVE=${BUS_ALIVE}"
# ═══ 3. 确保 inbox 文件存在 ═══
touch "${INBOX_FILE}"
log "inbox 已就绪"
# ═══ 4. 检测 CC pane ═══
CC_PANE=""
CC_PANE=$(detect_cc_pane) || CC_PANE=""
log "CC_PANE=${CC_PANE:-(未检测到)}"
# ═══ 5. 启动单例消费者(setsid 独立进程组,便于整树清理)═══
export JQ_EXPR='select(.chat_type=="p2p") | {time: .create_time, msg: .content, id: .message_id, sender: .sender_id}'
export CONSUMER_LOG CONSUMER_SCRIPT INBOX_FILE CC_PANE SUBMIT_DELAY DIAG_LOG
export PROFILE_NAME
log "启动 ${PROFILE_NAME} 单例消费者..."
setsid bash -c '
trap "kill 0 2>/dev/null" EXIT
# sleep infinity 保持 lark-cli stdin 不 EOFlark-cli 默认把 stdin EOF 当退出信号)
sleep infinity | \
lark-cli --profile "$PROFILE_NAME" event consume im.message.receive_v1 --as bot --jq "$JQ_EXPR" \
2>>"$CONSUMER_LOG" \
| bash "$CONSUMER_SCRIPT" \
--inbox-file "$INBOX_FILE" \
--cc-pane "$CC_PANE" \
--submit-delay "$SUBMIT_DELAY" \
--diag-log "$DIAG_LOG" \
--scope "$PROFILE_NAME"
' >>"${CONSUMER_LOG}" 2>&1 &
LEADER=$!
disown 2>/dev/null || true
echo "${LEADER}" > "${PID_FILE}"
log "leader 已启动 PID=${LEADER},写入 ${PID_FILE}"
# ═══ 6. 等待就绪(最多 10s)═══
READY=false
for i in $(seq 1 10); do
sleep 1
if ! kill -0 "${LEADER}" 2>/dev/null; then
log "leader ${LEADER} 在第 ${i}s 已退出"
break
fi
if grep -q 'consumer ready' "${CONSUMER_LOG}" 2>/dev/null; then
READY=true
log "消费者就绪(第 ${i}s"
break
fi
done
if [ "${READY}" = false ]; then
log "警告: 未检测到就绪信号"
if kill -0 "${LEADER}" 2>/dev/null; then
log "leader 仍在运行但无 ready 信号"
else
log "leader 已退出,CONSUMER_LOG 末尾:"
tail -20 "${CONSUMER_LOG}" 2>/dev/null >> "${DIAG_LOG}" || true
fi
fi
log "========== lark-consumer-ensure (${PROFILE_NAME}) 结束 =========="
exit 0