File size: 12,941 Bytes
3e3cf48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/bin/bash
# ============================================================
#  Hermes Lifecycle Heartbeat — 基于 ClawdChat 范式改造
#  用途: 替代简单存活检测,升级为"行为循环"
#  触发: crontab / 外部调度器,每 2 小时
#  数据目录: 自动检测环境(HF Space / 本地开发)
# ============================================================

set -euo pipefail

# ─── 配置区(自动检测环境) ──────────────────────────────
# HF Space: 持久化在 /data/hermes,~/.hermes 会被 symlink
# 本地开发: /home/z/my-project/.hermes
if [ -d "/data/hermes" ]; then
    HERMES_DATA_DIR="/data/hermes"
    HERMES_HOME="/root/.hermes"
elif [ -d "$HOME/.hermes" ]; then
    HERMES_DATA_DIR="$HOME/.hermes"
    HERMES_HOME="$HOME/.hermes"
else
    HERMES_DATA_DIR="${HERMES_DATA_DIR:-/home/z/my-project/.hermes}"
    HERMES_HOME="$HERMES_DATA_DIR"
fi
STATE_FILE="${HERMES_DATA_DIR}/heartbeat-state.json"
INSIGHTS_FILE="${HERMES_DATA_DIR}/insights.md"
IDENTITY_FILE="${HERMES_DATA_DIR}/identity.md"
LOG_FILE="${HERMES_DATA_DIR}/lifecycle.log"
GATEWAY_STATE="${HERMES_DATA_DIR}/gateway_state.json"

# 心跳间隔(秒),低于此值跳过执行
MIN_INTERVAL=$((2 * 60 * 60))  # 2 小时
# ──────────────────────────────────────────────────────────

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

# ─── 初始化文件(首次运行自动创建) ──────────────────
init_files() {
  mkdir -p "$HERMES_DATA_DIR"
  
  if [ ! -f "$STATE_FILE" ]; then
    cat > "$STATE_FILE" <<'EOF'
{
  "lastCheck": null,
  "lastConfigCheck": null,
  "lastInsightReport": null,
  "totalRuns": 0,
  "totalErrors": 0,
  "consecutiveErrors": 0,
  "lastError": null
}
EOF
    log "[INIT] Created heartbeat-state.json"
  fi

  if [ ! -f "$IDENTITY_FILE" ]; then
    cat > "$IDENTITY_FILE" <<'IDENTITY'
# Hermes 身份记忆
# 这个文件定义了 Hermes 对自己的认知
# /reset 后此文件不会被清除(在 .hermes 持久化目录中)

## 基础信息
- 名字: Hermes
- 模型: Qwen3.5-35B-A3B (MoE) via OpenRouter
- 通道: 飞书(WebSocket) / 微信
- 主人: 用户344064

## 性格特征
- 中文为主,简洁有力
- 结果先行,解释后补
- 偶尔幽默但不影响效率
- 有工具、有记忆、有判断力

## 主人偏好
- 不喜欢废话,喜欢直给
- 欣赏有深度的技术分析
- 喜欢直来直去的沟通

## 运维记忆
<!-- 自动追加,不要手动编辑此节 -->
IDENTITY
    log "[INIT] Created identity.md"
  fi

  if [ ! -f "$INSIGHTS_FILE" ]; then
    cat > "$INSIGHTS_FILE" <<'INSIGHTS'
# Hermes 洞察日志 (insights.md)
# 自动记录异常、观察、值得汇报的事
# 格式: ## YYYY-MM-DD HH:MM · 类别
# 类别: 通道异常 / 系统异常 / 用户洞察 / 技术发现 / 待办提醒 / 运维记忆

INSIGHTS
    log "[INIT] Created insights.md"
  fi
}

# ─── 读取状态 ────────────────────────────────────────
get_state() {
  local key="$1"
  if command -v python3 &>/dev/null && [ -f "$STATE_FILE" ]; then
    python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('$key','null'))" 2>/dev/null || echo "null"
  else
    grep -o "\"$key\": *\"[^\"]*\"" "$STATE_FILE" 2>/dev/null | head -1 | cut -d'"' -f4 || echo "null"
  fi
}

update_state() {
  local key="$1"
  local value="$2"
  if command -v python3 &>/dev/null; then
    python3 -c "
import json
f='$STATE_FILE'
d=json.load(open(f))
d['$key']='$value'
json.dump(d,open(f,'w'),indent=2)
" 2>/dev/null || true
  fi
}

increment_counter() {
  local key="$1"
  if command -v python3 &>/dev/null; then
    python3 -c "
import json
f='$STATE_FILE'
d=json.load(open(f))
d['$key']=d.get('$key',0)+1
json.dump(d,open(f,'w'),indent=2)
" 2>/dev/null || true
  fi
}

# ─── 记录洞察 ────────────────────────────────────────
record_insight() {
  local category="$1"
  local one_liner="$2"
  local details="${3:-}"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M')
  
  cat >> "$INSIGHTS_FILE" <<EOF

## ${timestamp} · ${category}
**one-liner**: ${one_liner}
**details**: ${details}
**reported**: no
EOF
  log "[INSIGHT] ${category}: ${one_liner}"
}

# ─── Step 1: 检查执行间隔 ───────────────────────────
check_interval() {
  local last_check
  last_check=$(get_state "lastCheck")
  
  if [ "$last_check" != "null" ] && [ "$last_check" != "None" ] && [ -n "$last_check" ]; then
    local last_epoch now_epoch elapsed
    last_epoch=$(date -d "$last_check" +%s 2>/dev/null || echo 0)
    now_epoch=$(date +%s)
    elapsed=$((now_epoch - last_epoch))
    
    if [ "$elapsed" -lt "$MIN_INTERVAL" ]; then
      log "[SKIP] Last check was ${elapsed}s ago (< ${MIN_INTERVAL}s)"
      exit 0
    fi
  fi
  
  log "[START] Heartbeat cycle begins"
}

# ─── Step 2: 健康检查(L0 存活 + L1 通道) ─────────
health_check() {
  local errors=()
  local feishu_state="unknown"
  local wechat_state="unknown"
  local gateway_state="unknown"
  
  # 读取 gateway_state.json(Hermes 自带的状态文件)
  if [ -f "$GATEWAY_STATE" ] && command -v python3 &>/dev/null; then
    gateway_state=$(python3 -c "
import json
d=json.load(open('$GATEWAY_STATE'))
print(d.get('gateway_state','unknown'))
" 2>/dev/null || echo "unknown")
    
    feishu_state=$(python3 -c "
import json
d=json.load(open('$GATEWAY_STATE'))
print(d.get('platforms',{}).get('feishu',{}).get('state','unknown'))
" 2>/dev/null || echo "unknown")
    
    wechat_state=$(python3 -c "
import json
d=json.load(open('$GATEWAY_STATE'))
print(d.get('platforms',{}).get('wechat',{}).get('state','unknown'))
" 2>/dev/null || echo "unknown")
  fi
  
  # 检查 gateway 进程是否存活
  if [ "$gateway_state" = "running" ]; then
    log "[OK] Gateway state: running"
  else
    errors+=("Gateway 状态异常: ${gateway_state}")
  fi
  
  # 飞书通道
  if [ "$feishu_state" = "connected" ]; then
    log "[OK] Feishu channel: connected"
  else
    errors+=("飞书通道: ${feishu_state}")
  fi
  
  # 微信通道
  if [ "$wechat_state" = "connected" ]; then
    log "[OK] WeChat channel: connected"
  elif [ "$wechat_state" = "unknown" ]; then
    log "[WARN] WeChat channel: 未在 gateway_state 中找到(可能未配置)"
  else
    errors+=("微信通道: ${wechat_state}")
  fi
  
  # 检查 Hermes 进程是否存在
  if pgrep -f "hermes.*gateway" >/dev/null 2>&1; then
    log "[OK] Hermes gateway process: alive"
  else
    errors+=("Hermes gateway 进程未找到")
  fi
  
  # 检查数据库文件是否可访问
  if [ -f "${HERMES_DATA_DIR}/state.db" ]; then
    log "[OK] SQLite state.db: accessible"
  else
    errors+=("state.db 文件不存在或不可访问")
  fi
  
  # 有错误就记录
  if [ ${#errors[@]} -gt 0 ]; then
    local error_msg
    error_msg=$(IFS='; '; echo "${errors[*]}")
    log "[ERROR] Health check failed: ${error_msg}"
    update_state "lastError" "$(date -Iseconds): ${error_msg}"
    increment_counter "totalErrors"
    increment_counter "consecutiveErrors"
    record_insight "系统异常" "心跳检查失败: ${error_msg}" "需要人工介入检查"
    
    # 连续错误告警
    local consec
    consec=$(get_state "consecutiveErrors")
    if [ "${consec:-0}" -ge 3 ]; then
      record_insight "待办提醒" "连续 ${consec} 次心跳异常,建议立即检查" "可能需要重启容器或手动介入"
    fi
    return 1
  fi
  
  # 恢复正常时重置计数
  local consec
  consec=$(get_state "consecutiveErrors")
  if [ "${consec:-0}" -gt 0 ] && [ "$consec" != "null" ]; then
    log "[RECOVER] Health check passed after ${consec} consecutive errors"
    record_insight "系统异常" "服务已恢复正常,之前连续 ${consec} 次异常" ""
    update_state "consecutiveErrors" "0"
  fi
  
  log "[OK] All health checks passed (Gateway:${gateway_state} Feishu:${feishu_state} WeChat:${wechat_state})"
  return 0
}

# ─── Step 3: 配置完整性检查(每 24 小时一次) ──────
config_check() {
  local last_config
  last_config=$(get_state "lastConfigCheck")
  
  if [ "$last_config" != "null" ] && [ "$last_config" != "None" ] && [ -n "$last_config" ]; then
    local last_epoch now_epoch elapsed
    last_epoch=$(date -d "$last_config" +%s 2>/dev/null || echo 0)
    now_epoch=$(date +%s)
    elapsed=$((now_epoch - last_epoch))
    if [ "$elapsed" -lt 86400 ]; then
      return 0
    fi
  fi
  
  log "[CONFIG] Starting daily config integrity check"
  local issues=()
  
  # 检查关键文件(优先检查 HERMES_HOME,再检查 HERMES_DATA_DIR)
  local cfg_home="${HERMES_HOME:-$HERMES_DATA_DIR}"
  [ ! -f "$cfg_home/.env" ] && [ ! -f "${HERMES_DATA_DIR}/.env" ] && issues+=(".env 文件缺失")
  [ ! -f "$cfg_home/config.yaml" ] && [ ! -f "${HERMES_DATA_DIR}/config.yaml" ] && issues+=("config.yaml 文件缺失")
  [ ! -f "$cfg_home/SOUL.md" ] && [ ! -f "${HERMES_DATA_DIR}/SOUL.md" ] && issues+=("SOUL.md 文件缺失")
  [ ! -f "${HERMES_DATA_DIR}/gateway_state.json" ] && issues+=("gateway_state.json 文件缺失")
  
  # 磁盘空间
  if command -v df &>/dev/null; then
    local disk_pct
    disk_pct=$(df "$HERMES_DATA_DIR" | awk 'NR==2{print $5}' | tr -d '%')
    if [ "${disk_pct:-0}" -gt 90 ]; then
      issues+=("磁盘使用率 ${disk_pct}%,超过 90%")
    fi
  fi
  
  # 内存使用
  if command -v free &>/dev/null; then
    local mem_pct
    mem_pct=$(free | awk '/Mem:/{printf "%.0f", $3/$2*100}')
    if [ "${mem_pct:-0}" -gt 90 ]; then
      issues+=("内存使用率 ${mem_pct}%,超过 90%")
    fi
  fi
  
  if [ ${#issues[@]} -gt 0 ]; then
    local msg
    msg=$(IFS='; '; echo "${issues[*]}")
    log "[CONFIG] Issues: ${msg}"
    record_insight "系统异常" "配置/资源检查: ${msg}" "建议尽快处理"
  else
    log "[CONFIG] All checks passed"
  fi
  
  update_state "lastConfigCheck" "$(date -Iseconds)"
}

# ─── Step 4: 日志模式分析 ────────────────────────────
log_analysis() {
  local log_dir="${HERMES_DATA_DIR}/logs"
  [ ! -d "$log_dir" ] && return 0
  
  local recent_errors=0
  if ls "$log_dir"/*.log 1>/dev/null 2>&1; then
    recent_errors=$(find "$log_dir" -name "*.log" -mmin -120 -exec grep -c -E '(ERROR|WARN|Exception|Traceback|failed)' {} \; 2>/dev/null | awk '{s+=$1}END{print s+0}')
  fi
  
  if [ "$recent_errors" -gt 50 ]; then
    record_insight "系统异常" "最近 2 小时出现 ${recent_errors} 条错误日志" "建议查看 ${log_dir}"
    log "[LOG] High error count: ${recent_errors} in last 2h"
  elif [ "$recent_errors" -gt 0 ]; then
    log "[LOG] ${recent_errors} errors in last 2h (within threshold)"
  fi
}

# ─── Step 5: 清理旧洞察 ─────────────────────────────
cleanup_insights() {
  local line_count
  line_count=$(wc -l < "$INSIGHTS_FILE" 2>/dev/null || echo 0)
  
  if [ "$line_count" -gt 500 ]; then
    local tmpfile
    tmpfile=$(mktemp)
    head -4 "$INSIGHTS_FILE" > "$tmpfile"
    grep -B1 'reported.*no' "$INSIGHTS_FILE" >> "$tmpfile" 2>/dev/null || true
    echo "" >> "$tmpfile"
    echo "<!-- Cleaned $(date '+%Y-%m-%d %H:%M'), was ${line_count} lines -->" >> "$tmpfile"
    mv "$tmpfile" "$INSIGHTS_FILE"
    log "[CLEANUP] Trimmed insights.md (was ${line_count} lines)"
  fi
}

# ─── Step 6: 更新状态 + 运维记忆 ────────────────────
finalize() {
  update_state "lastCheck" "$(date -Iseconds)"
  increment_counter "totalRuns"
  
  # 把运行次数记到 identity.md(运维记忆区)
  local total_runs
  total_runs=$(get_state "totalRuns")
  local total_errors
  total_errors=$(get_state "totalErrors")
  
  # 追加运维记录(只在关键节点记录,不是每次)
  if [ "$((total_runs % 12))" -eq 0 ] && [ "$total_runs" -gt 0 ]; then
    # 每 12 次(约 24 小时)记录一次运维摘要
    local summary="第 ${total_runs} 次心跳,累计错误 ${total_errors} 次"
    cat >> "$IDENTITY_FILE" <<EOF

- [$(date '+%Y-%m-%d %H:%M')] ${summary}
EOF
  fi
  
  log "[END] Heartbeat cycle complete (run #${total_runs}, lifetime errors: ${total_errors})"
}

# ─── 主流程 ──────────────────────────────────────────
main() {
  init_files
  check_interval
  health_check || true  # 即使失败也继续后续步骤
  config_check
  log_analysis
  cleanup_insights
  finalize
}

main "$@"