文件预览

index.js

查看 Garmin Connect Sync (国际→国内) 技能包中的文件内容。

文件内容

index.js

/**
 * Garmin Sync Skill - International → China Account
 * Uses Python garminconnect library for actual Garmin API interaction.
 */

const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const SCRIPT = path.join(__dirname, 'scripts', 'garmin_sync.py');
const STATE_FILE = path.join(
  process.env.GARMIN_SYNC_DIR || path.join(require('os').homedir(), '.garmin-sync'),
  'sync-state.json'
);

/**
 * Build env for the Python script, merging process env with Garmin config.
 */
function buildEnv() {
  return {
    ...process.env,
    GARMIN_INTL_USERNAME: process.env.GARMIN_INTL_USERNAME || '',
    GARMIN_INTL_PASSWORD: process.env.GARMIN_INTL_PASSWORD || '',
    GARMIN_CN_USERNAME: process.env.GARMIN_CN_USERNAME || '',
    GARMIN_CN_PASSWORD: process.env.GARMIN_CN_PASSWORD || '',
    GARMIN_SYNC_DIR: process.env.GARMIN_SYNC_DIR || '',
    GARMIN_TOKEN_DIR: process.env.GARMIN_TOKEN_DIR || '',
    PYTHONUNBUFFERED: '1',
  };
}

function runPython(args = []) {
  try {
    const env = buildEnv();
    const output = execSync(`python3 "${SCRIPT}" ${args.join(' ')}`, {
      env,
      cwd: __dirname,
      timeout: 300000, // 5 min for sync operations
      maxBuffer: 10 * 1024 * 1024,
    });
    return { success: true, output: output.toString() };
  } catch (error) {
    return {
      success: false,
      output: error.stdout?.toString() || '',
      error: error.stderr?.toString() || error.message,
    };
  }
}

/**
 * Check if Garmin credentials are configured.
 */
function checkConfig() {
  const intlUser = process.env.GARMIN_INTL_USERNAME;
  const intlPass = process.env.GARMIN_INTL_PASSWORD;
  const cnUser = process.env.GARMIN_CN_USERNAME;
  const cnPass = process.env.GARMIN_CN_PASSWORD;

  if (!intlUser || !intlPass || !cnUser || !cnPass) {
    return {
      configured: false,
      message: [
        '❌ Garmin 账号未配置,请设置以下环境变量:',
        '',
        '```',
        'GARMIN_INTL_USERNAME=your_intl@email.com',
        'GARMIN_INTL_PASSWORD=***',
        'GARMIN_CN_USERNAME=your_cn@email.com',
        'GARMIN_CN_PASSWORD=***',
        '```',
        '',
        '或用 OpenClaw 配置:',
        '`openclaw config set env.GARMIN_INTL_USERNAME your@email.com`',
      ].join('\n'),
    };
  }

  return { configured: true };
}

/**
 * Parse input to determine command.
 */
function parseInput(input) {
  if (typeof input === 'string') {
    const cmd = input.trim().toLowerCase();
    if (['sync', 'status', 'list', 'auth-test', 'test_auth'].includes(cmd)) {
      return { command: cmd === 'test_auth' ? 'auth-test' : cmd, params: {} };
    }
    // Default to status
    return { command: 'status', params: {} };
  }

  if (typeof input === 'object') {
    const { command = 'status', ...params } = input;
    return { command: command.toLowerCase(), params };
  }

  return { command: 'status', params: {} };
}

/**
 * Main handler called by OpenClaw.
 */
async function handler(input) {
  const { command, params } = parseInput(input);
  const config = checkConfig();

  if (!config.configured) {
    return config.message;
  }

  const days = params.days || params.days_back || 7;
  const maxActivities = params.max || params.max_activities || 10;

  try {
    let result;

    switch (command) {
      case 'auth-test': {
        result = runPython(['auth-test']);
        if (result.success) {
          return '✅ **Garmin 账号认证测试通过!**\n\n国际版和国内版账号均可正常登录。';
        }
        return `❌ **认证测试失败**\n\`\`\`\n${result.error || result.output}\n\`\`\``;
      }

      case 'list': {
        result = runPython(['list', `--days=${days}`]);
        if (result.success) {
          // Return the raw log output
          return `📋 **最近 ${days} 天的活动**\n\`\`\`\n${result.output.trim()}\n\`\`\``;
        }
        return `❌ 获取活动列表失败:\n\`\`\`\n${result.error}\n\`\`\``;
      }

      case 'sync': {
        result = runPython(['sync', `--days=${days}`, `--max=${maxActivities}`]);
        // Extract the summary from output
        if (result.success) {
          const lines = result.output.trim().split('\n');
          const summary = lines.filter(l => l.includes('Sync complete') || l.includes('No new'));
          const detailLines = lines.filter(l => l.includes('[%]') || l.includes('✅') || l.includes('❌'));
          return [
            '🔄 **Garmin 数据同步完成**',
            '',
            '```',
            ...(summary.length ? summary : lines.slice(-5)),
            '```',
            '',
            '💡 提示: 使用 `status` 查看详细同步状态',
          ].join('\n');
        }
        return `❌ **同步失败**\n\`\`\`\n${result.error || result.output.slice(-500)}\n\`\`\``;
      }

      case 'status':
      default: {
        result = runPython(['status']);
        if (result.success) {
          const lines = result.output.trim().split('\n');
          return [
            '📊 **Garmin 同步状态**',
            '',
            '```',
            ...lines,
            '```',
          ].join('\n');
        }
        // Still show something useful
        return '📊 **Garmin 同步状态**\n\n尚未运行过同步。请先运行 `sync` 或 `auth-test`。';
      }
    }
  } catch (error) {
    return `❌ 执行失败: ${error.message}`;
  }
}

/**
 * Get skill info.
 */
function getInfo() {
  return {
    name: 'garmin-sync',
    description: '佳明国际账号数据同步到国内账号',
    version: '2.0.0',
    commands: ['sync', 'status', 'list', 'auth-test'],
    config: {
      env: {
        GARMIN_INTL_USERNAME: { required: true, desc: '国际版邮箱' },
        GARMIN_INTL_PASSWORD: { required: true, desc: '国际版密码' },
        GARMIN_CN_USERNAME: { required: true, desc: '国内版邮箱' },
        GARMIN_CN_PASSWORD: { required: true, desc: '国内版密码' },
      },
    },
  };
}

module.exports = { handler, getInfo };