功能(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:
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/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_id(fallback)
|
||||
# ============================================================================
|
||||
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 不 EOF(lark-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
|
||||
Reference in New Issue
Block a user