#!/usr/bin/env bash # OpenClaw + memory-tencentdb(原 memory-tdai)诊断数据导出脚本 # 注:插件已更名为 memory-tencentdb,但数据目录始终为 memory-tdai(代码硬编码) # 用法: bash export-diagnostic.sh [输出目录] # 默认输出到 ~/Downloads/openclaw-diagnostic-/ set -euo pipefail # ── 参数 ── OUTPUT_BASE="${1:-$HOME/Downloads}" TIMESTAMP=$(date +%Y%m%d-%H%M%S) EXPORT_DIR="${OUTPUT_BASE}/openclaw-diagnostic-${TIMESTAMP}" ARCHIVE_PATH="${EXPORT_DIR}.tar.gz" # ── OpenClaw 工作目录探测 ── if [ -n "${OPENCLAW_STATE_DIR:-}" ]; then STATE_DIR="$OPENCLAW_STATE_DIR" elif [ -d "$HOME/.openclaw" ]; then STATE_DIR="$HOME/.openclaw" elif [ -d "$HOME/.clawdbot" ]; then STATE_DIR="$HOME/.clawdbot" else echo "❌ 未找到 OpenClaw 工作目录 (~/.openclaw 或 ~/.clawdbot)" exit 1 fi echo "📂 OpenClaw 工作目录: $STATE_DIR" echo "📦 导出目录: $EXPORT_DIR" mkdir -p "$EXPORT_DIR" # ── 1. 收集环境信息 ── echo "🔍 收集环境信息..." { echo "=== 导出时间 ===" date -Iseconds 2>/dev/null || date echo "" echo "=== 系统信息 ===" echo "OS: $(uname -a)" echo "Node: $(node --version 2>/dev/null || echo 'not found')" echo "pnpm: $(pnpm --version 2>/dev/null || echo 'not found')" echo "" echo "=== OpenClaw 版本 ===" openclaw --version 2>/dev/null || pnpm openclaw --version 2>/dev/null || echo "(unknown)" echo "" echo "=== 工作目录 ===" echo "STATE_DIR: $STATE_DIR" echo "" echo "=== 目录结构 ===" ls -la "$STATE_DIR/" 2>/dev/null || echo "(empty)" echo "" echo "=== memory-tdai 目录结构 ===" ls -laR "$STATE_DIR/memory-tdai/" 2>/dev/null || echo "(not found)" echo "" echo "=== 磁盘占用 ===" du -sh "$STATE_DIR/memory-tdai/"* 2>/dev/null || echo "(not found)" } > "$EXPORT_DIR/env-info.txt" 2>&1 # ── 2. 收集 OpenClaw 日志 ── echo "📋 收集 OpenClaw 日志..." mkdir -p "$EXPORT_DIR/logs" # 网关日志 (~/.openclaw/logs/) if [ -d "$STATE_DIR/logs" ]; then cp -r "$STATE_DIR/logs/" "$EXPORT_DIR/logs/gateway-logs/" 2>/dev/null || true fi # 滚动日志 (/tmp/openclaw/) TMP_LOG_DIR="/tmp/openclaw" if [ -d "$TMP_LOG_DIR" ]; then mkdir -p "$EXPORT_DIR/logs/rolling-logs" # 只取最近 3 个日志文件 ls -t "$TMP_LOG_DIR"/openclaw-*.log 2>/dev/null | head -3 | while read -r f; do # 每个文件只取最后 5000 行,避免过大 tail -5000 "$f" > "$EXPORT_DIR/logs/rolling-logs/$(basename "$f")" 2>/dev/null || true done fi # ── 3. 收集记忆插件数据 ── # 注:数据目录名为 memory-tdai(历史原因,插件更名为 memory-tencentdb 后未改目录名) echo "🧠 收集记忆插件数据..." MEMORY_DIR="$STATE_DIR/memory-tdai" if [ -d "$MEMORY_DIR" ]; then mkdir -p "$EXPORT_DIR/memory-tdai" # L0 对话记录 (JSONL) if [ -d "$MEMORY_DIR/conversations" ]; then cp -r "$MEMORY_DIR/conversations/" "$EXPORT_DIR/memory-tdai/conversations/" 2>/dev/null || true fi # L1 结构化记忆 (JSONL) if [ -d "$MEMORY_DIR/records" ]; then cp -r "$MEMORY_DIR/records/" "$EXPORT_DIR/memory-tdai/records/" 2>/dev/null || true fi # L2 场景文件 (Markdown) if [ -d "$MEMORY_DIR/scene_blocks" ]; then cp -r "$MEMORY_DIR/scene_blocks/" "$EXPORT_DIR/memory-tdai/scene_blocks/" 2>/dev/null || true fi # L3 用户画像 [ -f "$MEMORY_DIR/persona.md" ] && cp "$MEMORY_DIR/persona.md" "$EXPORT_DIR/memory-tdai/" 2>/dev/null || true # checkpoint + scene_index if [ -d "$MEMORY_DIR/.metadata" ]; then cp -r "$MEMORY_DIR/.metadata/" "$EXPORT_DIR/memory-tdai/.metadata/" 2>/dev/null || true fi # SQLite 数据库(用于检查向量/FTS索引状态) [ -f "$MEMORY_DIR/vectors.db" ] && cp "$MEMORY_DIR/vectors.db" "$EXPORT_DIR/memory-tdai/" 2>/dev/null || true # 备份目录(可选,可能较大) if [ -d "$MEMORY_DIR/.backup" ]; then cp -r "$MEMORY_DIR/.backup/" "$EXPORT_DIR/memory-tdai/.backup/" 2>/dev/null || true fi else echo " ⚠️ 未找到 memory-tdai 数据目录(memory-tencentdb 插件的数据也存储在此目录)" fi # ── 4. 收集 OpenClaw 配置(脱敏) ── echo "🔧 收集 OpenClaw 配置(已脱敏)..." CONFIG_FILE="$STATE_DIR/openclaw.json" if [ -f "$CONFIG_FILE" ]; then # 使用 node 脱敏处理配置 node -e " const fs = require('fs'); const JSON5 = (() => { try { return require('json5'); } catch { return JSON; } })(); const raw = fs.readFileSync('$CONFIG_FILE', 'utf-8'); let cfg; try { cfg = JSON5.parse(raw); } catch { cfg = JSON.parse(raw); } // 递归脱敏函数 function redact(obj, path) { if (!obj || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map((v, i) => redact(v, path + '[' + i + ']')); const result = {}; for (const [k, v] of Object.entries(obj)) { const fullPath = path ? path + '.' + k : k; // 脱敏规则:API key、token、password、secret 类字段 if (/api_?key|token|password|secret|credential/i.test(k) && typeof v === 'string') { result[k] = v.length > 0 ? '***REDACTED(' + v.length + 'chars)***' : ''; } // 脱敏 SecretRef 对象 else if (v && typeof v === 'object' && v.source && v.id && v.provider) { result[k] = { source: v.source, provider: v.provider, id: '***REDACTED***' }; } // 整体跳过的顶层敏感块 else if (['models', 'secrets', 'channels', 'env'].includes(k) && !path) { result[k] = '***REDACTED_SECTION(use openclaw config get ' + k + ' to inspect)***'; } // gateway.auth 内的 token/password else if (path === 'gateway.auth' && /token|password/i.test(k)) { result[k] = typeof v === 'string' ? '***REDACTED***' : v; } else { result[k] = redact(v, fullPath); } } return result; } const redacted = redact(cfg, ''); // plugins 已经过递归 redact(),其中 apiKey/token/password/secret 等字段 // 会被自动脱敏,同时保留 provider/model/enabled 等排查所需的非敏感配置 fs.writeFileSync('$EXPORT_DIR/openclaw-config-redacted.json', JSON.stringify(redacted, null, 2)); console.log(' ✅ 配置已脱敏导出'); " 2>&1 || { echo " ⚠️ Node 脱敏失败,使用 grep 粗略脱敏" # 粗略脱敏:删除包含敏感关键字的行 grep -v -iE '(api.?key|token|password|secret|credential).*:.*"[^"]{8,}"' "$CONFIG_FILE" \ | sed -E 's/"(models|secrets|channels|env)"\s*:\s*\{[^}]*\}/"__REDACTED_SECTION__"/g' \ > "$EXPORT_DIR/openclaw-config-redacted.json" 2>/dev/null || true } else echo " ⚠️ 未找到配置文件" fi # ── 5. 收集插件安装信息 ── echo "🔌 收集插件安装信息..." if [ -d "$STATE_DIR/extensions" ]; then { echo "=== 已安装插件 ===" ls -la "$STATE_DIR/extensions/" 2>/dev/null echo "" for ext_dir in "$STATE_DIR/extensions"/*/; do [ -d "$ext_dir" ] || continue pkg="$ext_dir/node_modules/openclaw/package.json" plugin_pkg="$ext_dir/package.json" echo "--- $(basename "$ext_dir") ---" if [ -f "$plugin_pkg" ]; then node -e "const p=require('$plugin_pkg'); console.log('name:', p.name, 'version:', p.version)" 2>/dev/null || true fi done } > "$EXPORT_DIR/plugins-info.txt" 2>&1 fi # ── 6. 打包 ── echo "📦 打包中..." cd "$(dirname "$EXPORT_DIR")" tar -czf "$ARCHIVE_PATH" "$(basename "$EXPORT_DIR")" # 计算大小 ARCHIVE_SIZE=$(du -sh "$ARCHIVE_PATH" | cut -f1) echo "" echo "═══════════════════════════════════════════════════" echo " ✅ 诊断数据导出完成" echo "═══════════════════════════════════════════════════" echo "" echo " 📦 压缩包: $ARCHIVE_PATH" echo " 📏 大小: $ARCHIVE_SIZE" echo "" echo " 包含内容:" echo " - env-info.txt — 环境信息、目录结构" echo " - logs/ — OpenClaw 网关日志 + 滚动日志" echo " - memory-tdai/ — 记忆插件全量数据 (L0~L3 + SQLite)" echo " - openclaw-config-redacted.json — 脱敏后的配置文件" echo " - plugins-info.txt — 插件安装信息" echo "" echo " ⚠️ 安全提示:" echo " - 配置文件已自动脱敏(API Key、Token、Password 等已移除)" echo " - models/secrets/channels/env 等敏感配置块已整体替换" echo " - 记忆数据中可能包含用户对话内容,请确认后再发送" echo "" echo " 📤 请手动检查后发送给研发团队" echo "═══════════════════════════════════════════════════"