OpenClaw 节点设备管理指南
节点(Nodes)是 OpenClaw 连接物理设备的桥梁,支持 Android、iOS、macOS 等设备。本文详解节点配对、设备管理、远程控制等核心功能。
概述
OpenClaw 节点系统支持:
- 📱 Android 设备连接
- 🍎 iOS 设备连接
- 💻 macOS 设备连接
- 📷 相机控制
- 📍 位置获取
- 🔔 通知管理
- 📸 屏幕录制
- 🖥️ 远程操作
一、节点架构
1.1 连接模式
┌─────────────────────────────────────────────────────┐
│ 节点连接模式 │
├─────────────────────────────────────────────────────┤
│ 本地 Wi-Fi │ 同一局域网内直连 │
│ 远程 VPS │ 通过公网服务器中转 │
│ Tailscale │ 通过 Tailscale 组网 │
│ 手动配置 │ 指定 IP 和端口连接 │
└─────────────────────────────────────────────────────┘1.2 设备配对流程
1. 网关启动设备配对插件
2. 生成配对码/二维码
3. 手机 App 扫描或输入配对码
4. 交换认证信息
5. 建立安全连接
6. 设备上线二、设备配对
2.1 启用配对插件
yaml
# ~/.openclaw/config.yaml
plugins:
entries:
- name: "device-pair"
enabled: true
config:
publicUrl: "https://your-domain.com" # 公网地址
port: 80802.2 启动配对
bash
# 查看配对状态
openclaw nodes pairing status
# 启动配对(生成二维码)
openclaw nodes pairing start
# 停止配对
openclaw nodes pairing stop2.3 手动连接
如果自动配对失败,可以手动配置:
javascript
// 获取配对信息
const pairingInfo = await nodes({
action: 'describe'
})
console.log('配对信息:', pairingInfo)
// 手动连接
await nodes({
action: 'pairing',
deviceId: 'manual',
address: '192.168.1.100',
port: 8080
})2.4 实战案例 1:配对诊断工具
javascript
class PairingDiagnostics {
async check() {
const report = {
status: 'unknown',
issues: [],
recommendations: []
}
// 1. 检查插件是否启用
const pluginEnabled = await this.checkPluginEnabled()
if (!pluginEnabled) {
report.issues.push('设备配对插件未启用')
report.recommendations.push('在 config.yaml 中启用 device-pair 插件')
}
// 2. 检查公网 URL 配置
const publicUrl = await this.getPublicUrl()
if (!publicUrl) {
report.issues.push('未配置 publicUrl')
report.recommendations.push('设置 plugins.device-pair.config.publicUrl')
} else if (!publicUrl.startsWith('https://')) {
report.issues.push('publicUrl 不是 HTTPS')
report.recommendations.push('使用 HTTPS 以保证安全')
}
// 3. 检查端口可访问性
const portOpen = await this.checkPortAccessibility()
if (!portOpen) {
report.issues.push('端口不可访问')
report.recommendations.push('检查防火墙设置,开放 8080 端口')
}
// 4. 检查本地网络
const localNetwork = await this.checkLocalNetwork()
if (!localNetwork) {
report.issues.push('本地网络异常')
report.recommendations.push('确认设备在同一 Wi-Fi 网络')
}
// 5. 检查已配对设备
const pairedDevices = await this.getPairedDevices()
report.pairedCount = pairedDevices.length
// 总结状态
if (report.issues.length === 0) {
report.status = 'healthy'
} else if (report.issues.length <= 2) {
report.status = 'warning'
} else {
report.status = 'critical'
}
return report
}
async checkPluginEnabled() {
try {
const config = await gateway({ action: 'config.get' })
const plugin = config.plugins?.entries?.find(p => p.name === 'device-pair')
return plugin?.enabled === true
} catch (e) {
return false
}
}
async getPublicUrl() {
try {
const config = await gateway({ action: 'config.get' })
return config.plugins?.entries?.find(p => p.name === 'device-pair')?.config?.publicUrl
} catch (e) {
return null
}
}
async checkPortAccessibility() {
try {
const result = await exec({
command: 'nc -z localhost 8080 && echo "open" || echo "closed"'
})
return result.stdout.trim() === 'open'
} catch (e) {
return false
}
}
async checkLocalNetwork() {
try {
const result = await exec({
command: 'ip addr show | grep "inet " | grep -v 127.0.0.1'
})
return result.stdout.trim().length > 0
} catch (e) {
return false
}
}
async getPairedDevices() {
try {
const status = await nodes({ action: 'status' })
return status.nodes || []
} catch (e) {
return []
}
}
async generateReport() {
const diagnosis = await this.check()
let report = `# 设备配对诊断报告\n\n`
report += `## 状态:${this.getStatusIcon(diagnosis.status)}\n\n`
if (diagnosis.issues.length > 0) {
report += `## ❌ 问题\n`
for (const issue of diagnosis.issues) {
report += `- ${issue}\n`
}
report += `\n`
}
if (diagnosis.recommendations.length > 0) {
report += `## 💡 建议\n`
for (const rec of diagnosis.recommendations) {
report += `- ${rec}\n`
}
report += `\n`
}
report += `## 📊 统计\n`
report += `- 已配对设备:${diagnosis.pairedCount || 0}\n`
return report
}
getStatusIcon(status) {
const icons = {
healthy: '✅ 健康',
warning: '⚠️ 警告',
critical: '❌ 严重',
unknown: '❓ 未知'
}
return icons[status] || status
}
}
// 使用
const diagnostics = new PairingDiagnostics()
const report = await diagnostics.generateReport()
console.log(report)三、设备管理
3.1 列出设备
javascript
// 获取所有节点状态
async function listNodes() {
const status = await nodes({ action: 'status' })
return status.nodes || []
}
// 获取设备详情
async function getNodeDetails(deviceId) {
return await nodes({
action: 'device_info',
deviceId
})
}
// 获取设备健康状态
async function getNodeHealth(deviceId) {
return await nodes({
action: 'device_health',
deviceId
})
}
// 获取设备权限
async function getNodePermissions(deviceId) {
return await nodes({
action: 'device_permissions',
deviceId
})
}3.2 设备状态监控
javascript
class DeviceMonitor {
constructor() {
this.devices = new Map()
this.alerts = []
}
async refresh() {
const nodes = await listNodes()
for (const node of nodes) {
const info = await getNodeDetails(node.id)
const health = await getNodeHealth(node.id)
this.devices.set(node.id, {
...node,
info,
health,
lastUpdate: Date.now()
})
}
return this.devices
}
getStatusReport() {
const devices = Array.from(this.devices.values())
return {
total: devices.length,
online: devices.filter(d => d.connected).length,
battery: devices.map(d => ({
name: d.name || d.id,
level: d.info?.battery?.level,
charging: d.info?.battery?.charging,
status: this.getBatteryStatus(d.info?.battery?.level)
})),
storage: devices.map(d => ({
name: d.name || d.id,
used: d.info?.storage?.used,
total: d.info?.storage?.total
}))
}
}
getBatteryStatus(level) {
if (!level) return 'unknown'
if (level > 80) return 'excellent'
if (level > 50) return 'good'
if (level > 20) return 'low'
return 'critical'
}
checkAlerts() {
const alerts = []
for (const [id, device] of this.devices) {
// 低电量告警
if (device.info?.battery?.level < 20) {
alerts.push({
type: 'low_battery',
device: device.name || id,
level: device.info.battery.level,
severity: device.info.battery.level < 10 ? 'critical' : 'warning'
})
}
// 存储空间告警
if (device.info?.storage) {
const usagePercent = (device.info.storage.used / device.info.storage.total) * 100
if (usagePercent > 90) {
alerts.push({
type: 'low_storage',
device: device.name || id,
usage: usagePercent.toFixed(1),
severity: 'warning'
})
}
}
// 离线告警
if (!device.connected) {
const offlineDuration = Date.now() - device.lastUpdate
if (offlineDuration > 300000) { // 5 分钟
alerts.push({
type: 'offline',
device: device.name || id,
duration: Math.round(offlineDuration / 60000),
severity: 'warning'
})
}
}
}
this.alerts = alerts
return alerts
}
async sendAlerts() {
for (const alert of this.alerts) {
const message = this.formatAlert(alert)
try {
await message({
action: 'send',
target: '老大',
message
})
} catch (e) {
console.error('发送告警失败:', e)
}
}
this.alerts = []
}
formatAlert(alert) {
const icons = {
low_battery: '🪫',
low_storage: '💾',
offline: '📴'
}
const severityIcons = {
critical: '🚨',
warning: '⚠️'
}
switch (alert.type) {
case 'low_battery':
return `${severityIcons[alert.severity]} ${icons.low_battery} 设备 ${alert.device} 电量过低:${alert.level}%`
case 'low_storage':
return `${severityIcons[alert.severity]} ${icons.low_storage} 设备 ${alert.device} 存储空间不足:${alert.usage}%`
case 'offline':
return `${severityIcons[alert.severity]} ${icons.offline} 设备 ${alert.device} 离线:${alert.duration}分钟`
default:
return `⚠️ 设备告警:${JSON.stringify(alert)}`
}
}
}
// 使用
const monitor = new DeviceMonitor()
// 定期监控
async function deviceMonitoringJob() {
await monitor.refresh()
const report = monitor.getStatusReport()
console.log('设备状态报告:')
console.log(`总设备:${report.total}`)
console.log(`在线:${report.online}`)
// 检查告警
const alerts = monitor.checkAlerts()
if (alerts.length > 0) {
console.log(`发现 ${alerts.length} 个告警`)
await monitor.sendAlerts()
}
}3.3 实战案例 2:设备批量管理
javascript
class DeviceManager {
// 批量发送通知
async broadcastNotification(title, body, options = {}) {
const { priority = 'active', devices = null } = options
const nodes = devices ? devices : await listNodes()
const results = []
for (const node of nodes) {
try {
await nodes({
action: 'notify',
deviceId: node.id,
title,
body,
priority,
delivery: options.delivery || 'system'
})
results.push({ device: node.id, success: true })
} catch (error) {
results.push({ device: node.id, success: false, error: error.message })
}
}
return results
}
// 批量获取位置
async getLocations(timeout = 10000) {
const nodes = await listNodes()
const results = []
for (const node of nodes) {
try {
const location = await nodes({
action: 'location_get',
deviceId: node.id,
desiredAccuracy: 'balanced',
locationTimeoutMs: timeout
})
results.push({ device: node.id, location, success: true })
} catch (error) {
results.push({ device: node.id, success: false, error: error.message })
}
}
return results
}
// 批量截取屏幕
async captureScreens(options = {}) {
const { durationMs = 5000, fps = 2 } = options
const nodes = await listNodes()
const results = []
for (const node of nodes) {
try {
const screen = await nodes({
action: 'screen_record',
deviceId: node.id,
durationMs,
fps,
includeAudio: options.includeAudio || false
})
results.push({ device: node.id, screen, success: true })
} catch (error) {
results.push({ device: node.id, success: false, error: error.message })
}
}
return results
}
// 批量检查相机
async checkCameras() {
const nodes = await listNodes()
const results = []
for (const node of nodes) {
try {
// 列出相机
const cameras = await nodes({
action: 'camera_list',
deviceId: node.id
})
// 拍摄照片
const photo = await nodes({
action: 'camera_snap',
deviceId: node.id,
facing: 'front'
})
results.push({
device: node.id,
cameras: cameras,
photo: photo ? 'success' : 'failed',
success: true
})
} catch (error) {
results.push({ device: node.id, success: false, error: error.message })
}
}
return results
}
// 生成设备报告
async generateReport() {
const nodes = await listNodes()
let report = `# 设备管理报告\n\n`
report += `生成时间:${new Date().toLocaleString('zh-CN')}\n\n`
report += `## 设备列表\n\n`
report += `| 设备 | 状态 | 电量 | 存储 | 位置 |\n`
report += `|------|------|------|------|------|\n`
for (const node of nodes) {
const info = await getNodeDetails(node.id)
const status = node.connected ? '🟢 在线' : '🔴 离线'
const battery = info?.battery?.level ? `${info.battery.level}%` : 'N/A'
const storage = info?.storage?.used
? `${(info.storage.used / 1024 / 1024 / 1024).toFixed(1)}GB`
: 'N/A'
const location = info?.location?.address || 'N/A'
report += `| ${node.name || node.id} | ${status} | ${battery} | ${storage} | ${location} |\n`
}
return report
}
}
// 使用
const manager = new DeviceManager()
// 批量通知
await manager.broadcastNotification(
'系统通知',
'OpenClaw 将进行例行维护,请保存好工作。',
{ priority: 'timeSensitive' }
)
// 生成报告
const report = await manager.generateReport()
console.log(report)四、相机控制
4.1 相机操作
javascript
// 列出相机
async function listCameras(deviceId) {
return await nodes({
action: 'camera_list',
deviceId
})
}
// 拍摄照片
async function takePhoto(deviceId, options = {}) {
return await nodes({
action: 'camera_snap',
deviceId,
facing: options.facing || 'back', // front | back | both
quality: options.quality || 80,
maxWidth: options.maxWidth || 1920
})
}
// 录制视频
async function recordVideo(deviceId, options = {}) {
return await nodes({
action: 'camera_clip',
deviceId,
facing: options.facing || 'back',
durationMs: options.durationMs || 10000,
quality: options.quality || 70
})
}
// 获取最新照片
async function getLatestPhotos(deviceId, limit = 5) {
return await nodes({
action: 'photos_latest',
deviceId,
limit
})
}4.2 实战案例 3:安全监控应用
javascript
class SecurityMonitor {
constructor() {
this.motionDetected = false
this.recording = false
}
// 定时拍照监控
async startMonitoring(deviceId, intervalMinutes = 5) {
console.log(`启动安全监控,设备:${deviceId}`)
setInterval(async () => {
try {
// 拍摄照片
const photo = await takePhoto(deviceId, {
facing: 'back',
quality: 70
})
// 分析照片(使用 AI)
const analysis = await sessions_spawn({
task: `分析这张照片是否有异常情况:
- 是否有人员出现
- 是否有物品移动
- 是否有异常光线变化
返回 JSON: {"hasActivity": boolean, "description": "..."}`,
attachments: [{
name: 'monitoring.jpg',
content: photo,
encoding: 'base64'
}],
mode: 'run'
})
const result = JSON.parse(analysis.output)
if (result.hasActivity) {
console.log('⚠️ 检测到活动:', result.description)
// 发送告警
await message({
action: 'send',
target: '老大',
message: `🚨 安全监控告警\n\n${result.description}\n时间:${new Date().toLocaleString('zh-CN')}`
})
}
} catch (error) {
console.error('监控失败:', error)
}
}, intervalMinutes * 60 * 1000)
}
// 检测到异常时录制视频
async recordOnAlert(deviceId, durationSeconds = 30) {
if (this.recording) {
console.log('已在录制中')
return
}
this.recording = true
try {
const video = await recordVideo(deviceId, {
facing: 'back',
durationMs: durationSeconds * 1000,
quality: 70
})
// 保存视频
await write({
path: `/home/pao/.openclaw/workspace/security/alert-${Date.now()}.mp4`,
content: video,
encoding: 'base64'
})
console.log('✅ 视频已保存')
} finally {
this.recording = false
}
}
// 远程查看
async remoteView(deviceId) {
// 拍摄当前画面
const photo = await takePhoto(deviceId, {
facing: 'back',
quality: 90
})
// 获取位置
const location = await nodes({
action: 'location_get',
deviceId
})
return {
photo,
location,
timestamp: Date.now()
}
}
}
// 使用
const monitor = new SecurityMonitor()
// 启动监控
await monitor.startMonitoring('device-id-here', 5)五、位置服务
5.1 位置获取
javascript
// 获取设备位置
async function getLocation(deviceId, options = {}) {
return await nodes({
action: 'location_get',
deviceId,
desiredAccuracy: options.accuracy || 'balanced', // coarse | balanced | precise
locationTimeoutMs: options.timeout || 10000
})
}
// 解析位置信息
function parseLocation(locationData) {
if (!locationData || !locationData.location) {
return null
}
const loc = locationData.location
return {
latitude: loc.latitude,
longitude: loc.longitude,
accuracy: loc.accuracy,
address: loc.address,
timestamp: new Date(loc.timestamp).toLocaleString('zh-CN')
}
}5.2 实战案例 4:位置追踪应用
javascript
class LocationTracker {
constructor() {
this.history = new Map()
this.geofences = []
}
// 添加地理围栏
addGeofence(name, center, radiusMeters) {
this.geofences.push({
name,
center: { lat: center.lat, lng: center.lng },
radius: radiusMeters
})
}
// 追踪设备位置
async track(deviceId) {
const location = await getLocation(deviceId)
const parsed = parseLocation(location)
if (!parsed) {
console.log('无法获取位置')
return null
}
// 保存历史
if (!this.history.has(deviceId)) {
this.history.set(deviceId, [])
}
this.history.get(deviceId).push({
...parsed,
recordedAt: Date.now()
})
// 保留最近 100 条
const history = this.history.get(deviceId)
if (history.length > 100) {
history.shift()
}
// 检查地理围栏
const fenceAlerts = this.checkGeofences(parsed)
return {
location: parsed,
geofenceAlerts: fenceAlerts
}
}
checkGeofences(location) {
const alerts = []
for (const fence of this.geofences) {
const distance = this.calculateDistance(
location.latitude,
location.longitude,
fence.center.lat,
fence.center.lng
)
if (distance <= fence.radius) {
alerts.push({
fence: fence.name,
status: 'inside',
distance: distance.toFixed(0)
})
} else {
alerts.push({
fence: fence.name,
status: 'outside',
distance: distance.toFixed(0)
})
}
}
return alerts
}
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000 // 地球半径(米)
const dLat = this.toRad(lat2 - lat1)
const dLon = this.toRad(lon2 - lon1)
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}
toRad(deg) {
return deg * Math.PI / 180
}
// 生成位置报告
generateReport(deviceId) {
const history = this.history.get(deviceId) || []
if (history.length === 0) {
return '无位置数据'
}
let report = `# 位置追踪报告\n\n`
report += `设备:${deviceId}\n`
report += `记录数:${history.length}\n\n`
report += `## 最近位置\n`
const latest = history[history.length - 1]
report += `- 地址:${latest.address}\n`
report += `- 坐标:${latest.latitude}, ${latest.longitude}\n`
report += `- 精度:${latest.accuracy}米\n`
report += `- 时间:${latest.timestamp}\n\n`
report += `## 位置历史\n`
report += `| 时间 | 地址 | 精度 |\n`
report += `|------|------|------|\n`
for (const record of history.slice(-10)) {
report += `| ${record.timestamp} | ${record.address} | ${record.accuracy}m |\n`
}
return report
}
}
// 使用
const tracker = new LocationTracker()
// 添加地理围栏
tracker.addGeofence('家', { lat: 39.9042, lng: 116.4074 }, 500)
tracker.addGeofence('公司', { lat: 39.9087, lng: 116.3975 }, 300)
// 追踪位置
const result = await tracker.track('device-id')
console.log(result)
// 生成报告
const report = tracker.generateReport('device-id')
console.log(report)六、通知管理
6.1 通知操作
javascript
// 列出通知
async function listNotifications(deviceId) {
return await nodes({
action: 'notifications_list',
deviceId
})
}
// 发送通知
async function sendNotification(deviceId, options) {
return await nodes({
action: 'notify',
deviceId,
title: options.title,
body: options.body,
priority: options.priority || 'active', // passive | active | timeSensitive
delivery: options.delivery || 'system', // system | overlay | auto
sound: options.sound
})
}
// 操作通知
async function actionNotification(deviceId, notificationKey, action) {
return await nodes({
action: 'notifications_action',
deviceId,
notificationKey,
notificationAction: action, // open | dismiss | reply
notificationReplyText: action === 'reply' ? options.replyText : undefined
})
}6.2 实战案例 5:通知自动化
javascript
class NotificationAutomation {
constructor() {
this.rules = []
}
// 添加通知规则
addRule(rule) {
this.rules.push(rule)
}
// 监听并处理通知
async processNotifications(deviceId) {
const notifications = await listNotifications(deviceId)
for (const notification of notifications.notifications || []) {
// 检查规则
for (const rule of this.rules) {
if (this.matchRule(notification, rule)) {
await this.executeRule(notification, rule)
break
}
}
}
}
matchRule(notification, rule) {
// 匹配应用
if (rule.app && notification.app !== rule.app) {
return false
}
// 匹配标题关键词
if (rule.titleContains && !notification.title.includes(rule.titleContains)) {
return false
}
// 匹配内容关键词
if (rule.bodyContains && !notification.body.includes(rule.bodyContains)) {
return false
}
return true
}
async executeRule(notification, rule) {
console.log(`执行规则:${rule.name}`)
switch (rule.action) {
case 'forward':
// 转发到其他设备
await this.forwardNotification(notification, rule.targets)
break
case 'log':
// 记录到日志
await this.logNotification(notification, rule.logFile)
break
case 'auto_reply':
// 自动回复
await this.autoReply(notification, rule.replyText)
break
case 'trigger':
// 触发其他操作
await this.triggerAction(notification, rule.trigger)
break
}
}
async forwardNotification(notification, targets) {
for (const target of targets) {
await sendNotification(target.deviceId, {
title: `[转发] ${notification.title}`,
body: notification.body,
priority: 'active'
})
}
}
async logNotification(notification, logFile) {
const entry = `[${new Date().toISOString()}] ${notification.app}: ${notification.title} - ${notification.body}\n`
await exec({
command: `echo "${entry}" >> ${logFile}`
})
}
async autoReply(notification, replyText) {
await actionNotification(notification.deviceId, notification.key, 'reply', {
replyText
})
}
async triggerAction(notification, trigger) {
// 执行触发动作
if (trigger.type === 'exec') {
await exec({ command: trigger.command })
} else if (trigger.type === 'message') {
await message({
action: 'send',
target: trigger.target,
message: trigger.message
})
}
}
}
// 使用
const automation = new NotificationAutomation()
// 添加规则:转发微信消息
automation.addRule({
name: '转发微信',
app: '微信',
titleContains: '消息',
action: 'forward',
targets: [{ deviceId: 'tablet-id' }]
})
// 添加规则:记录短信
automation.addRule({
name: '记录短信',
app: '信息',
action: 'log',
logFile: '/home/pao/.openclaw/logs/sms.log'
})
// 添加规则:验证码自动回复
automation.addRule({
name: '验证码处理',
titleContains: '验证码',
action: 'auto_reply',
replyText: '已收到'
})
// 处理通知
await automation.processNotifications('phone-id')七、故障排查
7.1 常见问题
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 配对失败 | 网络不通 | 检查防火墙、公网 URL |
| 设备离线 | Wi-Fi 断开 | 检查设备网络连接 |
| 位置获取失败 | 权限未授予 | 在设备上授予位置权限 |
| 相机不可用 | 权限问题 | 检查相机权限 |
| 通知不显示 | 通知权限 | 检查通知权限设置 |
7.2 诊断命令
bash
# 检查网关状态
openclaw gateway status
# 检查插件状态
openclaw plugins list
# 查看节点日志
openclaw nodes logs --follow
# 测试设备连接
openclaw nodes ping --device <device-id>八、总结
核心要点
- 节点是连接物理设备的桥梁
- 配对配置决定连接方式
- 设备管理需要定期监控
- 相机、位置、通知是核心功能
- 安全监控是重要应用场景
进阶方向
- 📱 开发自定义设备插件
- 🏠 构建智能家居集成
- 🚨 实现安全监控系统
- 📍 开发位置服务应用
🟢🐉 开始管理你的设备节点吧!