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. 核心职责
- OpenClaw 数据收集:定期采集 OpenClaw 各项数据
- Health 数据接收:通过 HTTP API 接收 iOS 上报的 Health 数据
- 数据存储:SQLite 本地持久化
- 中继连接:WebSocket 连接中继服务器
- 实时推送:有新数据时主动推送给 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.md4. 借鉴 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.json → models.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"
}
}