功能(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
+205
View File
@@ -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_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