#!/bin/bash # ============================================================================ # 飞书事件消费者 (纯 bash) — 从 lark-cli event consume 管道读取 NDJSON, # 去重后写入 inbox ndjson 文件,并通过 tmux send-keys 转发到当前 CC pane。 # # 用法: lark-consumer.sh --inbox-file <路径> --cc-pane \ # [--submit-delay <秒>] [--diag-log <路径>] [--scope <标识>] # # 由 lark-consumer-ensure.sh 经 setsid 启动,全局单例运行。 # 所有配置通过命令行参数传入,避免 shell 嵌入转义问题。 # # 环境变量: # LARK_SENDER_ALLOW - sender open_id 白名单(优先) # LARK_ME_USER_ID - sender open_id 白名单(fallback) # ============================================================================ set -eo pipefail # 不使用 set -u:空关联数组在 -u 下会触发"未绑定的变量"错误 # ── 参数解析 ── INBOX_FILE="" CC_PANE="" SUBMIT_DELAY="0.5" DIAG_LOG="" SCOPE="singleton" while [[ $# -gt 0 ]]; do case "$1" in --inbox-file) INBOX_FILE="$2"; shift 2 ;; --cc-pane) CC_PANE="$2"; shift 2 ;; --submit-delay) SUBMIT_DELAY="$2"; shift 2 ;; --diag-log) DIAG_LOG="$2"; shift 2 ;; --scope) SCOPE="$2"; shift 2 ;; *) echo "未知参数: $1" >&2; exit 1 ;; esac done # ── sender 白名单(三级 fallback:LARK_SENDER_ALLOW → LARK_ME_USER_ID → 默认值)── SENDER_ALLOW="${LARK_SENDER_ALLOW:-${LARK_ME_USER_ID:-ou_2c245a54dbea4bc2197f6d63bd940bb0}}" # ── 日志函数 ── diag() { [[ -n "${DIAG_LOG}" ]] || return 0 printf '[%s] [%s] [consumer] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "${SCOPE}" "$*" >> "${DIAG_LOG}" } # ── 检测目标 CC pane 是否存活 ── pane_alive() { [[ -z "${CC_PANE}" ]] && return 0 # 非 tmux 模式,永远 true tmux display-message -t "${CC_PANE}" -p '#{pane_id}' &>/dev/null } # ── 从 ndjson 行提取 JSON 字段 ── field() { jq -r "$1 // empty" <<< "$2" 2>/dev/null } # ── 加载已有 message_id 到关联数组(启动去重)── declare -A SEEN load_seen() { [[ -f "${INBOX_FILE}" ]] || return 0 local line mid while IFS= read -r line; do [[ -z "${line}" ]] && continue mid=$(field '.id' "${line}") || true [[ -n "${mid}" ]] && SEEN["${mid}"]=1 done < "${INBOX_FILE}" } diag "消费者 bash 进程启动 (PID=$$)" diag "inbox=${INBOX_FILE} cc_pane=${CC_PANE} delay=${SUBMIT_DELAY} sender=${SENDER_ALLOW}" load_seen diag "已加载 ${#SEEN[@]} 条历史 message_id" # 就绪信号(写入 stderr,由 ensure.sh 经 CONSUMER_LOG 检测) echo "consumer ready (pid=$$, seen=${#SEEN[@]})" >&2 # ── 主循环:逐行读取 lark-cli 输出 ── LAST_CHECK=$(date +%s) CHECK_INTERVAL=30 while IFS= read -r line; do [[ -z "${line}" ]] && continue # ── 定期巡检 CC pane 存活 ── _now=$(date +%s) if (( _now - LAST_CHECK > CHECK_INTERVAL )); then LAST_CHECK=${_now} if ! pane_alive; then diag "目标 pane 已消失(CC/session 退出),消费者主动退出" exit 0 fi fi # ── sender 白名单过滤 ── _sender=$(field '.sender' "${line}") || true if [[ -n "${SENDER_ALLOW}" && "${_sender}" != "${SENDER_ALLOW}" ]]; then diag "跳过非白名单 sender=${_sender}" continue fi # ── 去重 ── _mid=$(field '.id' "${line}") || true if [[ -z "${_mid}" ]]; then diag "警告: 缺少 message_id,跳过: ${line:0:80}" continue fi if [[ -n "${SEEN[${_mid}]:-}" ]]; then continue fi SEEN["${_mid}"]=1 # ── 写入 inbox(保留原始多行 msg)── printf '%s\n' "${line}" >> "${INBOX_FILE}" diag "新消息写入 inbox: id=${_mid}" # ── send-keys 转发到 CC pane ── if [[ -n "${CC_PANE}" ]]; then if ! pane_alive; then diag "转发前检测到 pane 已消失,消费者退出" exit 0 fi _msg=$(field '.msg' "${line}") || true if [[ -n "${_msg}" ]]; then # 多行压平:避免 tmux send-keys 把换行当多次回车,导致一条消息被拆成多次提交 _flat=$(printf '%s' "${_msg}" | tr '\n' ' ' | tr -s ' ') diag "转发 → pane=${CC_PANE} len=${#_flat}" if ! tmux send-keys -t "${CC_PANE}" -l "${_flat}" 2>/dev/null; then diag "转发失败: send-keys -l 返回非零,pane=${CC_PANE} 可能已不存在" continue fi sleep "${SUBMIT_DELAY}" if ! tmux send-keys -t "${CC_PANE}" Enter 2>/dev/null; then diag "转发失败: send-keys Enter 返回非零,pane=${CC_PANE} 可能已不存在" continue fi diag "转发完成" fi else diag "cc_pane 为空,仅写 inbox" fi done diag "stdin 已关闭(lark-cli 退出),消费者结束"