Skill: frankieway/yuqing-data-to-bitable (version 2.0.0)

name: xiaoai_to_bitable version: "1.0.0" description: > 从小爱数据接口增量拉取数据，并写入飞书多维表（支持 case_id 自增、入库时间为写入时间、按字段自动创建/映射）。 entrypoint: command: "python" args: - "xiaoai_to_bitable_skill.py" inputs: - name: minutes type: integer required: false default: 60 description: 往前补偿的分钟数（基于当前时间，默认 60 分钟） - name: folder_id type: integer required: false default: 763579 description: 小爱接口 folder_id - name: customer_id type: string required: false default: "xmxa" description: 小爱接口 customer_id - name: bitable_url type: string required: false description: > 目标飞书多维表视图链接（包含 base/app_token 和 table 参数）。 若不提供，则使用脚本中内置的 BITABLE_URL。 outputs: - name: inserted_count type: integer description: 本次实际写入到多维表的记录数 permissions: network: - "https://open.feishu.cn" - "http://wisers-data-service.wisersone.com.cn"

path: README.MD
content: # xiaoai-yuqing-data-to-bitable-skill

path: README_ENHANCED.md
content: # 舆情数据同步 - 增强版说明

## 🚀 新增功能

### 1. 自动重试机制
- **最大重试次数**：3 次
- **重试间隔**：60 秒
- **自动修复**：第 1 次重试前执行自动修复

### 2. 健康检查
每次同步前自动检查：
- ✅ Python3 环境
- ✅ .env 配置文件
- ✅ 主脚本文件
- ✅ 磁盘空间（>90% 告警）
- ✅ 缓存目录

### 3. 自动修复
发现问题自动修复：
- 🔧 .env URL 引号问题
- 🔧 Python 缓存清理
- 🔧 死锁文件清理
- 🔧 日志文件轮转（>10MB）

### 4. 状态追踪
- 📊 实时同步状态（JSON 格式）
- 📈 成功率统计
- 🔢 连续失败计数
- ⏱️ 执行耗时记录

### 5. 失败告警
- 连续失败 ≥3 次时发送告警
- 错误日志单独记录
- 健康检查失败时立即告警

### 6. 锁机制
- 防止并发执行
- 死锁自动检测（>10 分钟）
- 异常退出自动清理

---

## 📋 文件说明

| 文件 | 说明 |
|------|------|
| `sync.sh` | 增强版同步脚本（带重试和修复） |
| `monitor.sh` | 监控工具（查看状态、手动修复等） |
| `.sync_status.json` | 同步状态文件 |
| `.sync.lock` | 锁文件 |
| `sync.log` | 主日志 |
| `error.log` | 错误日志 |
| `alerts.log` | 告警日志 |
| `.cache/` | 缓存目录（Token、唯一键） |

---

## 🔧 使用方法

### 查看状态
```bash
bash monitor.sh status
```

### 查看日志
```bash
# 最近 20 条日志
bash monitor.sh logs

# 最近 50 条日志
bash monitor.sh logs 50
```

### 查看错误
```bash
bash monitor.sh errors
```

### 健康检查
```bash
bash monitor.sh health
```

### 手动修复
```bash
bash monitor.sh fix
```

### 手动执行同步
```bash
bash monitor.sh run
# 或
bash sync.sh
```

### 重置状态
```bash
bash monitor.sh reset
```

---

## 📊 状态文件示例

```json
{
    "last_run": "2026-03-13T21:52:41+08:00",
    "status": "success",
    "inserted_count": 22,
    "duration_seconds": 8,
    "retry_count": 0,
    "error_message": "",
    "consecutive_failures": 0
}
```

---

## 🔄 定时任务

已配置 cron，每 10 分钟执行一次：
```bash
*/10 * * * * /home/admin/.openclaw/workspace/skills/yuqing-data-to-bitable/sync.sh
```

---

## ⚠️ 故障排查

### 连续失败处理
```bash
# 1. 查看状态
bash monitor.sh status

# 2. 查看错误日志
bash monitor.sh errors

# 3. 执行健康检查
bash monitor.sh health

# 4. 尝试修复
bash monitor.sh fix

# 5. 手动执行测试
bash monitor.sh run
```

### 常见问题

**Q: 提示"BITABLE_URL 未初始化"**
```bash
# 检查 .env 文件
cat .env | grep BITABLE_URL

# 确保 URL 用引号包裹
BITABLE_URL='https://xxx.feishu.cn/base/XXX?table=XXX'
```

**Q: 提示"已有任务在运行"**
```bash
# 检查锁文件
ls -la .sync.lock

# 如果是死锁（超过 10 分钟），清理它
rm -f .sync.lock
```

**Q: 同步很慢**
```bash
# 检查缓存是否生效
bash monitor.sh health

# 查看 Token 缓存和唯一键缓存状态
ls -la .cache/
```

---

## 📈 性能优化

- **Token 缓存**：2 小时有效期
- **唯一键缓存**：5 分钟有效期
- **连接池**：复用 HTTP 连接
- **批量操作**：500 条/批（飞书上限）
- **分页优化**：500 条/页

---

## 🆘 需要帮助？

```bash
bash monitor.sh help
```

---

*版本：2.0.0（增强版）*  
*更新时间：2026-03-13*


path: _meta.json
content: ownerId: kn7eag7808pwjcwsa9mw5hwkr582djk3
slug: yuqing-data-to-bitable
version: 2.0.0
publishedAt: 1773405222112

path: main.py
content: import argparse
from typing import Optional

import excel_to_feishu_bitable as m


def skill_run_once(
    minutes: int,          # 往前补偿的分钟数（相对于当前时间）
    folder_id: int,        # 小爱接口的 folder_id，用于限定数据范围
    customer_id: str,      # 小爱接口的 customer_id，用于区分不同客户
    app_id: str,           # 飞书开放平台应用的 APP_ID
    app_secret: str,       # 飞书开放平台应用的 APP_SECRET
    xiaoai_token: str,     # 调用小爱接口用的 token（Bearer 后面的部分）
    bitable_url: str,      # 目标飞书多维表视图链接（含 base/app_token 和 table 参数）
    xiaoai_base_url: str = "http://wisers-data-service.wisersone.com.cn",  # 小爱 API 基础域名
) -> int:
    """
    Skill 入口：执行一次从小爱到飞书多维表的增量同步。

    - minutes: 往前补偿的分钟数（基于当前时间）
    - folder_id: 小爱接口的 folder_id
    - customer_id: 小爱接口的 customer_id
    - bitable_url: 目标飞书多维表的完整 URL（包含 base/app_token 和 table 参数）

    返回本次实际写入的记录数。
    """
    # 覆盖全局配置，适配当前调用参数
    m.APP_ID = app_id
    m.APP_SECRET = app_secret
    m.[REDACTED_SECRET]
    m.BASE_URL = xiaoai_base_url
    m.DEFAULT_FOLDER_ID = folder_id
    m.DEFAULT_CUSTOMER_ID = customer_id
    m.BITABLE_URL = bitable_url

    # 复用原脚本中的 run_once 逻辑
    return m.run_once(minutes)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="XiaoAi -> Feishu Bitable incremental sync (OpenClaw Skill entry)"
    )
    parser.add_argument(
        "--minutes",  # 往前补偿的分钟数
        type=int,
        default=60,
        help="往前补偿的分钟数（默认 60 分钟）",
    )
    parser.add_argument(
        "--folder_id",  # 小爱接口的 folder_id
        type=int,
        required=True,
        help="小爱接口 folder_id",
    )
    parser.add_argument(
        "--customer_id",  # 小爱接口的 customer_id
        type=str,
        required=True,
        help="小爱接口 customer_id",
    )
    parser.add_argument(
        "--app_id",  # 飞书应用 APP_ID
        type=str,
        required=True,
        help="飞书应用 APP_ID",
    )
    parser.add_argument(
        "--app_secret",  # 飞书应用 APP_SECRET
        type=str,
        required=True,
        help="飞书应用 APP_SECRET",
    )
    parser.add_argument(
        "--xiaoai_token",  # 小爱接口 token（Bearer 后面的部分）
        type=str,
        required=True,
        help="小爱接口 token（Bearer 后面的部分）",
    )
    parser.add_argument(
        "--bitable_url",  # 飞书多维表视图链接（含 base/app_token 和 table 参数）
        type=str,
        required=True,
        help="飞书多维表视图链接（包含 base/app_token 和 table 参数）",
    )
    parser.add_argument(
        "--xiaoai_base_url",  # 小爱 API 基础域名（可选）
        type=str,
        default="http://wisers-data-service.wisersone.com.cn",
        help="小爱 API 基础域名（可选，默认 http://wisers-data-service.wisersone.com.cn）",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    inserted = skill_run_once(
        minutes=args.minutes,
        folder_id=args.folder_id,
        customer_id=args.customer_id,
        app_id=args.app_id,
        app_secret=args.app_secret,
        bitable_url=args.bitable_url,
        xiaoai_token=args.xiaoai_token,

        xiaoai_base_url=args.xiaoai_base_url,
    )
    # OpenClaw 可以从标准输出中解析 inserted_count
    print(f"inserted_count={inserted}")


if __name__ == "__main__":
    main()



path: monitor.sh
content: #!/bin/bash
# 舆情同步监控工具
# 用于手动检查状态、触发修复、查看统计

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATUS_FILE="$SCRIPT_DIR/.sync_status.json"
LOG_FILE="$SCRIPT_DIR/sync.log"
ERROR_LOG="$SCRIPT_DIR/error.log"

show_status() {
    echo "=========================================="
    echo "  舆情数据同步状态"
    echo "=========================================="
    
    if [ -f "$STATUS_FILE" ]; then
        echo ""
        echo "📊 最近一次同步状态："
        cat "$STATUS_FILE" | python3 -m json.tool 2>/dev/null || cat "$STATUS_FILE"
    else
        echo "⚠️  暂无状态文件"
    fi
    
    echo ""
    echo "📈 统计信息："
    
    if [ -f "$LOG_FILE" ]; then
        TOTAL_RUNS=$(grep -c "开始舆情数据同步任务" "$LOG_FILE" 2>/dev/null || echo 0)
        SUCCESS_RUNS=$(grep -c "同步成功" "$LOG_FILE" 2>/dev/null || echo 0)
        FAILED_RUNS=$(grep -c "同步失败" "$LOG_FILE" 2>/dev/null || echo 0)
        
        echo "  总执行次数：$TOTAL_RUNS"
        echo "  成功次数：$SUCCESS_RUNS"
        echo "  失败次数：$FAILED_RUNS"
        
        if [ $TOTAL_RUNS -gt 0 ]; then
            SUCCESS_RATE=$(echo "scale=2; $SUCCESS_RUNS * 100 / $TOTAL_RUNS" | bc 2>/dev/null || echo "N/A")
            echo "  成功率：${SUCCESS_RATE}%"
        fi
    fi
    
    echo ""
    echo "📁 文件信息："
    ls -lh "$SCRIPT_DIR"/*.log "$SCRIPT_DIR"/.sync_status.json 2>/dev/null | awk '{print "  " $9 ": " $5}'
    
    echo ""
    echo "=========================================="
}

show_recent_logs() {
    local lines=${1:-20}
    echo "=========================================="
    echo "  最近 $lines 条日志"
    echo "=========================================="
    tail -n $lines "$LOG_FILE"
}

show_errors() {
    local lines=${1:-50}
    echo "=========================================="
    echo "  最近 $lines 条错误"
    echo "=========================================="
    if [ -f "$ERROR_LOG" ]; then
        tail -n $lines "$ERROR_LOG"
    else
        echo "暂无错误日志"
    fi
}

run_health_check() {
    echo "=========================================="
    echo "  执行健康检查"
    echo "=========================================="
    . "$SCRIPT_DIR/.env"
    
    issues=0
    
    echo -n "  Python3 环境... "
    if command -v python3 &> /dev/null; then
        echo "✅"
    else
        echo "❌"
        ((issues++))
    fi
    
    echo -n "  .env 配置文件... "
    if [ -f "$SCRIPT_DIR/.env" ]; then
        echo "✅"
    else
        echo "❌"
        ((issues++))
    fi
    
    echo -n "  主脚本文件... "
    if [ -f "$SCRIPT_DIR/excel_to_feishu_bitable.py" ]; then
        echo "✅"
    else
        echo "❌"
        ((issues++))
    fi
    
    echo -n "  飞书 Token 缓存... "
    if [ -f "$SCRIPT_DIR/.cache/tenant_token.json" ]; then
        echo "✅"
    else
        echo "⚠️  无缓存（下次会重新获取）"
    fi
    
    echo -n "  唯一键缓存... "
    if [ -f "$SCRIPT_DIR/.cache/existing_keys.json" ]; then
        CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$SCRIPT_DIR/.cache/existing_keys.json" 2>/dev/null || echo 0) ))
        if [ $CACHE_AGE -lt 300 ]; then
            echo "✅ (有效)"
        else
            echo "⚠️  已过期 (${CACHE_AGE}s)"
        fi
    else
        echo "⚠️  无缓存"
    fi
    
    echo -n "  锁文件... "
    if [ -f "$SCRIPT_DIR/.sync.lock" ]; then
        LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$SCRIPT_DIR/.sync.lock" 2>/dev/null || echo 0) ))
        if [ $LOCK_AGE -gt 600 ]; then
            echo "⚠️  死锁 (${LOCK_AGE}s)，可清理"
        else
            echo "🔒 正常锁定中 (${LOCK_AGE}s)"
        fi
    else
        echo "✅"
    fi
    
    echo ""
    if [ $issues -gt 0 ]; then
        echo "❌ 发现 $issues 个问题"
    else
        echo "✅ 健康检查通过"
    fi
}

run_fix() {
    echo "=========================================="
    echo "  执行自动修复"
    echo "=========================================="
    
    fixed=0
    
    # 清理 Python 缓存
    if [ -d "$SCRIPT_DIR/__pycache__" ]; then
        echo -n "  清理 Python 缓存... "
        rm -rf "$SCRIPT_DIR/__pycache__"
        echo "✅"
        ((fixed++))
    fi
    
    # 清理死锁
    if [ -f "$SCRIPT_DIR/.sync.lock" ]; then
        LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$SCRIPT_DIR/.sync.lock" 2>/dev/null || echo 0) ))
        if [ $LOCK_AGE -gt 600 ]; then
            echo -n "  清理死锁文件... "
            rm -f "$SCRIPT_DIR/.sync.lock"
            echo "✅"
            ((fixed++))
        fi
    fi
    
    # 清理旧缓存
    if [ -d "$SCRIPT_DIR/.cache" ]; then
        echo -n "  清理过期缓存... "
        find "$SCRIPT_DIR/.cache" -type f -mmin +60 -delete 2>/dev/null
        echo "✅"
        ((fixed++))
    fi
    
    # 日志轮转
    if [ -f "$LOG_FILE" ]; then
        LOG_SIZE=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
        if [ $LOG_SIZE -gt 10485760 ]; then
            echo -n "  日志文件轮转... "
            mv "$LOG_FILE" "$LOG_FILE.$(date +%Y%m%d%H%M%S)"
            touch "$LOG_FILE"
            echo "✅"
            ((fixed++))
        fi
    fi
    
    echo ""
    if [ $fixed -gt 0 ]; then
        echo "✅ 完成 $fixed 项修复"
    else
        echo "ℹ️  无需修复"
    fi
}

run_sync_now() {
    echo "=========================================="
    echo "  手动执行同步"
    echo "=========================================="
    bash "$SCRIPT_DIR/sync.sh"
}

reset_status() {
    echo "=========================================="
    echo "  重置状态"
    echo "=========================================="
    rm -f "$STATUS_FILE" "$SCRIPT_DIR/.sync.lock"
    echo "✅ 状态已重置"
}

show_help() {
    echo "=========================================="
    echo "  舆情同步监控工具"
    echo "=========================================="
    echo ""
    echo "用法：$0 <命令>"
    echo ""
    echo "命令:"
    echo "  status      查看同步状态（默认）"
    echo "  logs [n]    查看最近 n 条日志（默认 20）"
    echo "  errors [n]  查看最近 n 条错误（默认 50）"
    echo "  health      执行健康检查"
    echo "  fix         执行自动修复"
    echo "  run         手动执行同步"
    echo "  reset       重置状态"
    echo "  help        显示帮助"
    echo ""
    echo "示例:"
    echo "  $0 status"
    echo "  $0 logs 50"
    echo "  $0 health"
    echo "  $0 fix && $0 run"
    echo ""
}

# 主逻辑
case "${1:-status}" in
    status)
        show_status
        ;;
    logs)
        show_recent_logs "${2:-20}"
        ;;
    errors)
        show_errors "${2:-50}"
        ;;
    health)
        run_health_check
        ;;
    fix)
        run_fix
        ;;
    run)
        run_sync_now
        ;;
    reset)
        reset_status
        ;;
    help|--help|-h)
        show_help
        ;;
    *)
        echo "❌ 未知命令：$1"
        show_help
        exit 1
        ;;
esac


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)