npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

health-relay-client

v1.0.4

Published

HealthClaw Mac Client - OpenClaw Data Collector & Health API

Readme

health-relay-client 技术文档

Mac mini 客户端 - 数据收集 + 中继通信


1. 项目信息

| 属性 | 值 | |------|-----| | 项目名 | health-relay-client | | 语言 | Node.js | | 依赖 | ws, better-sqlite3 | | 部署 | Mac mini | | 端口 | 18790 (Health API) | | 进程管理 | PM2 |


2. 核心职责

  1. OpenClaw 数据收集:定期采集 OpenClaw 各项数据
  2. Health 数据接收:通过 HTTP API 接收 iOS 上报的 Health 数据
  3. 数据存储:SQLite 本地持久化
  4. 中继连接:WebSocket 连接中继服务器
  5. 实时推送:有新数据时主动推送给 iOS

3. 项目结构

health-relay-client/
├── src/
│   ├── index.js              # 入口
│   ├── config.js             # 配置加载
│   ├── relay-client.js       # 中继客户端
│   ├── collectors/
│   │   ├── openclaw.js       # OpenClaw 数据收集总入口
│   │   ├── models.js         # 模型列表
│   │   ├── agents.js         # Agent 列表
│   │   ├── crons.js          # Cron 任务
│   │   ├── skills.js         # Skills 列表
│   │   ├── logs.js           # 日志
│   │   ├── usage.js          # Token 使用量
│   │   └── config.js         # 配置文件(脱敏)
│   ├── health-api.js         # Health 数据接收 HTTP API
│   ├── storage/
│   │   ├── sqlite.js         # SQLite 操作
│   │   └── schema.js         # 表结构
│   ├── pushers/
│   │   └── relay-pusher.js   # 中继推送
│   └── utils/
│       ├── sanitize.js       # 脱敏
│       └── logger.js         # 日志
├── package.json
├── ecosystem.config.js       # PM2 配置
└── README.md

4. 借鉴 host-connector 的核心设计

4.1 WebSocket 中继连接

const WebSocket = require('ws');
const os = require('os');

class RelayClient {
  constructor() {
    this.ws = null;
    this.reconnectDelay = 1000;
    this.stopped = false;
    this.HOSTNAME = os.hostname();
    this.RELAY_URL = `ws://119.45.24.29:5201/relay/health-${this.HOSTNAME}?secret=${ACCESS_CODE}`;
  }

  connect() {
    if (this.stopped) return;
    console.log('[relay] Connecting...');
    this.ws = new WebSocket(this.RELAY_URL);

    this.ws.on('open', () => {
      console.log('[relay] ✅ Connected');
      this.reconnectDelay = 1000;
      this.startHeartbeat();
    });

    this.ws.on('message', (data) => {
      this.handleMessage(JSON.parse(data.toString()));
    });

    this.ws.on('close', (code, reason) => {
      console.log(`[relay] Disconnected: ${code}`);
      this.scheduleReconnect();
    });

    this.ws.on('error', (err) => {
      console.error('[relay] Error:', err.message);
    });
  }

  startHeartbeat() {
    setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.ping();
      }
    }, 30000);
  }

  scheduleReconnect() {
    if (this.stopped) return;
    console.log(`[relay] Reconnecting in ${this.reconnectDelay}ms...`);
    setTimeout(() => {
      if (!this.stopped) this.connect();
    }, this.reconnectDelay);
    this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
  }

  send(msg) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(msg));
    }
  }
}

4.2 消息处理

handleMessage(msg) {
  if (msg.type === 'req') {
    const { method, id, params } = msg;
    console.log(`[relay] ← ${method}`);

    switch (method) {
      case 'openclaw.status':
        this.collectOpenClawData().then(data => {
          this.send({ type: 'res', id, ok: true, data });
        });
        break;
      case 'health.latest':
        this.getLatestHealthData().then(data => {
          this.send({ type: 'res', id, ok: true, data });
        });
        break;
      default:
        this.send({ type: 'res', id, ok: false, error: 'Unknown method' });
    }
  }
}

5. OpenClaw 数据收集

5.1 收集方式一览

| 数据 | 来源 | 采集频率 | |------|------|----------| | 模型列表 | ~/.openclaw/openclaw.jsonmodels.providers | 5分钟 | | Agent 列表 | 解析配置 + 读 IDENTITY.md | 5分钟 | | Cron 任务 | execSync('openclaw cron list --json') | 1分钟 | | Skills | 扫描 ~/.openclaw/skills/*/SKILL.md | 5分钟 | | 日志 | 读 ~/.openclaw/logs/openclaw.log 末尾100行 | 1分钟 | | Token 用量 | 读 ~/.clawcontrol/usage.json | 5分钟 | | 配置文件 | 读 openclaw.json(脱敏) | 手动 |

5.2 配置文件脱敏

const SENSITIVE_KEYS = ['apiKey', 'token', 'password', 'secret', 'credential', 'accessCode'];

function sanitize(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  if (Array.isArray(obj)) return obj.map(sanitize);
  const out = {};
  for (const [k, v] of Object.entries(obj)) {
    out[k] = SENSITIVE_KEYS.some(s => k.toLowerCase().includes(s))
      ? '[已隐藏]'
      : sanitize(v);
  }
  return out;
}

5.3 Agent 列表采集

function collectAgents() {
  const ocConfig = JSON.parse(fs.readFileSync('~/.openclaw/openclaw.json', 'utf8'));
  const agents = ocConfig.agents?.list || [];
  
  return agents.map(agent => {
    const workspaceDir = agent.id === 'main'
      ? '~/.openclaw/workspace'
      : `~/.openclaw/workspace-${agent.id}`;
    const identityPath = path.join(workspaceDir, 'IDENTITY.md');
    
    let name = agent.name || agent.id;
    let emoji = '🤖';
    
    if (fs.existsSync(identityPath)) {
      const content = fs.readFileSync(identityPath, 'utf8');
      const nameMatch = content.match(/[-*]\s*Name[::]\s*(.+)/i);
      const emojiMatch = content.match(/[-*]\s*Emoji[::]\s*(.+)/i);
      if (nameMatch) name = nameMatch[1].trim();
      if (emojiMatch) emoji = emojiMatch[1].trim();
    }
    
    return { id: agent.id, name, emoji, model: agent.model };
  });
}

6. Health API 接收

6.1 HTTP 服务

const http = require('http');

function startHealthAPI() {
  const server = http.createServer((req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Content-Type', 'application/json');

    if (req.method === 'OPTIONS') {
      res.writeHead(204);
      res.end();
      return;
    }

    if (req.method === 'POST' && req.url === '/api/health/sync') {
      let body = '';
      req.on('data', chunk => body += chunk);
      req.on('end', async () => {
        try {
          const data = JSON.parse(body);
          await storage.saveHealthData(data);
          // 可选:推送给已连接的 iOS
          relayClient.pushHealthUpdate(data);
          res.end(JSON.stringify({ ok: true }));
        } catch (e) {
          res.end(JSON.stringify({ ok: false, error: e.message }));
        }
      });
    } else if (req.method === 'GET' && req.url === '/api/health/latest') {
      const latest = storage.getLatestHealthData();
      res.end(JSON.stringify(latest));
    } else {
      res.end(JSON.stringify({ error: 'Not Found' }));
    }
  });

  server.listen(18790, () => {
    console.log('[health-api] Listening on :18790');
  });
}

6.2 接收的数据格式

{
  "type": "heartrate",
  "value": 72,
  "timestamp": 1743734400000
}

7. SQLite 数据存储

7.1 表结构

-- Apple Health 数据表
CREATE TABLE health_heartrate (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    value REAL NOT NULL,
    type TEXT DEFAULT 'rest'
);

CREATE TABLE health_sleep (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    start_time INTEGER NOT NULL,
    end_time INTEGER NOT NULL,
    duration INTEGER NOT NULL,
    quality REAL
);

CREATE TABLE health_steps (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    count INTEGER NOT NULL,
    goal INTEGER
);

CREATE TABLE health_workout (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    type TEXT,
    duration INTEGER,
    calories INTEGER
);

CREATE TABLE health_weight (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    weight REAL NOT NULL,
    bmi REAL
);

CREATE TABLE health_oxygen (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    value REAL NOT NULL
);

CREATE TABLE health_blood_pressure (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp INTEGER NOT NULL,
    systolic INTEGER NOT NULL,
    diastolic INTEGER NOT NULL
);

-- OpenClaw 数据表
CREATE TABLE oc_models (id TEXT PRIMARY KEY, name TEXT, provider TEXT, context_window INTEGER, updated_at INTEGER);
CREATE TABLE oc_agents (id TEXT PRIMARY KEY, name TEXT, emoji TEXT, model TEXT, updated_at INTEGER);
CREATE TABLE oc_token_usage (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, total_calls INTEGER, last_used TEXT, timestamp INTEGER);
CREATE TABLE oc_crons (id TEXT PRIMARY KEY, name TEXT, cron_expr TEXT, next_run TEXT, enabled INTEGER DEFAULT 1, last_run TEXT, updated_at INTEGER);
CREATE TABLE oc_skills (id TEXT PRIMARY KEY, name TEXT, description TEXT, location TEXT, enabled INTEGER DEFAULT 1, updated_at INTEGER);
CREATE TABLE oc_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, level TEXT, message TEXT, timestamp INTEGER);

CREATE INDEX idx_health_timestamp ON health_heartrate(timestamp);
CREATE INDEX idx_oc_logs_timestamp ON oc_logs(timestamp);

8. PM2 进程守护

8.1 ecosystem.config.js

module.exports = {
  apps: [{
    name: 'health-relay-client',
    script: './src/index.js',
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '200M',
    env: {
      NODE_ENV: 'production',
      RELAY_HOST: '119.45.24.29',
      RELAY_PORT: '5201',
      ACCESS_CODE: 'health-secret-code-2024',
      HEALTH_API_PORT: '18790'
    }
  }]
};

8.2 部署命令

# 在 Mac mini 上
cd ~/health-relay-client
npm install
pm2 start ecosystem.config.js
pm2 save
pm2 startup  # 开机自启

9. 配置

9.1 环境变量或 config.json

{
  "relayHost": "119.45.24.29",
  "relayPort": 5201,
  "accessCode": "health-secret-code-2024",
  "healthApiPort": 18790,
  "macHostname": "LeoMac-mini",
  "openClawDir": "~/.openclaw",
  "usageFile": "~/.clawcontrol/usage.json"
}

10. 定时任务

| 任务 | 间隔 | 说明 | |------|------|------| | OpenClaw 状态采集 | 1分钟 | 全量数据 | | 日志采集 | 1分钟 | 末尾100行 | | 模型/Agent/Skills | 5分钟 | 配置解析 | | Token 用量 | 5分钟 | usage.json | | Health API | 实时 | 接收 iOS 数据 |


11. 依赖

{
  "dependencies": {
    "ws": "^8.20.0",
    "better-sqlite3": "^11.0.0"
  }
}