From 586b978f819488ab41e168edb41bf9a0bdd998ac Mon Sep 17 00:00:00 2001 From: yankun Date: Sun, 2 Aug 2026 12:38:06 +0800 Subject: [PATCH] feat: recognize CutOS agent runtime --- README.md | 11 ++++++++--- bin/clawd.js | 8 ++++++++ install.sh | 2 ++ lib/agent-type.js | 21 +++++++++++++++++++++ lib/client.js | 31 +++++++++++++++++++++++-------- lib/frpc.js | 36 +++++++++++++++++++----------------- lib/sys-call.js | 10 ++++++++++ package.json | 3 ++- test/agent-type.test.js | 20 ++++++++++++++++++++ 9 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 lib/agent-type.js create mode 100644 test/agent-type.test.js diff --git a/README.md b/README.md index 385429d..c0489a6 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,14 @@ node bin/clawd.js | `CLAWD_LOG_LEVEL` | `info` | 日志级别:debug / info / warn / error | | `CLAWD_LOG_FILE` | `1` | 是否写日志文件(`0` = 仅 stdout/journald) | | `CLAWD_LOG_DIR` | `~/.clawd/logs` | 日志文件目录 | -| `CLAWD_CONFIG_DIR` | `~/.clawd` | 配置目录 | - -systemd 安装后环境变量文件位于 `/etc/clawd/env`。 +| `CLAWD_CONFIG_DIR` | `~/.clawd` | 配置目录 | +| `AGENT_TYPE` | `openclaw` | Agent 类型:`openclaw` 或 `cutos-agent`;空值兼容为 `openclaw` | + +systemd 安装后环境变量文件位于 `/etc/clawd/env`。 + +切换 Agent 类型前,必须先在云端解绑设备,再修改 `/etc/clawd/env` 中的 +`AGENT_TYPE`,重启 `clawd` 后重新绑定。`cutos-agent` 模式当前仅接入通用设备管理, +不会读写 OpenClaw 配置或调用 OpenClaw 微信能力。 ## 服务管理 diff --git a/bin/clawd.js b/bin/clawd.js index d07bd3e..c2abe18 100755 --- a/bin/clawd.js +++ b/bin/clawd.js @@ -11,6 +11,14 @@ const { ClawClient } = require('../lib/client'); const config = require('../lib/config'); const log = require('../lib/logger'); const { pollSms } = require('../drivers/sim/sms-reader'); +const { resolveAgentType } = require('../lib/agent-type'); + +try { + resolveAgentType(); +} catch (err) { + log.error('clawd', err.message); + process.exit(1); +} // 每次启动同步 3588s demo 到 /usr/bin/demo(idempotent,失败不影响主流程) const demoBin = path.join(__dirname, "..", "lib/resource/3588s/demo"); diff --git a/install.sh b/install.sh index 1660b81..f94b681 100644 --- a/install.sh +++ b/install.sh @@ -199,6 +199,8 @@ CLAWD_LOG_LEVEL=info CLAWD_LOG_FILE=1 # Override server URL (default from config.json) # CLAWD_SERVER=wss://claw.cutos.ai/ws +# Agent runtime: openclaw (default) or cutos-agent +# AGENT_TYPE=openclaw # Enable Bluetooth monitor (bluetoothctl); disabled by default # CLAWD_ENABLE_BT=1 # OpenVFD sysfs path (default: /sys/class/leds/openvfd) diff --git a/lib/agent-type.js b/lib/agent-type.js new file mode 100644 index 0000000..d3afa9b --- /dev/null +++ b/lib/agent-type.js @@ -0,0 +1,21 @@ +'use strict'; + +const OPENCLAW = 'openclaw'; +const CUTOS_AGENT = 'cutos-agent'; +const SUPPORTED_AGENT_TYPES = new Set([OPENCLAW, CUTOS_AGENT]); + +function resolveAgentType(raw = process.env.AGENT_TYPE) { + const value = String(raw ?? '').trim().toLowerCase(); + const agentType = value || OPENCLAW; + if (!SUPPORTED_AGENT_TYPES.has(agentType)) { + throw new Error(`AGENT_TYPE must be one of: ${[...SUPPORTED_AGENT_TYPES].join(', ')}; received: ${raw}`); + } + return agentType; +} + +module.exports = { + OPENCLAW, + CUTOS_AGENT, + SUPPORTED_AGENT_TYPES, + resolveAgentType, +}; diff --git a/lib/client.js b/lib/client.js index 1382de4..fd97c2f 100644 --- a/lib/client.js +++ b/lib/client.js @@ -19,6 +19,7 @@ const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = re const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider'); const sysCall = require('./sys-call'); const led = require('./led'); +const { OPENCLAW, resolveAgentType } = require('./agent-type'); const MAX_BACKOFF_MS = 60_000; /** 连续若干轮 ping 后仍无 pong 才判定死链(单轮易因调度/弱网误判) */ @@ -54,6 +55,7 @@ function btMonitorEnabled() { class ClawClient { constructor() { this._cfg = config.load(); + this._agentType = resolveAgentType(); this._boxId = getBoxId(); this._ws = null; this._hbTimer = null; @@ -104,7 +106,7 @@ class ClawClient { // ── 生命周期 ───────────────────────────────────────────────────────────────── async start() { - log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`); + log.info('clawd', `启动中... 服务器 = ${this._cfg.server}, agent = ${this._agentType}`); if (this._cfg.claw_id) { this._setHostname(this._cfg.claw_id); @@ -193,7 +195,9 @@ class ClawClient { async _proceedWithConnection() { const [dashInfo] = await Promise.all([ - getDashboardInfo().catch(e => { log.warn('clawd', 'dashboard 信息获取失败:', e.message); return null; }), + this._isOpenClaw() + ? getDashboardInfo().catch(e => { log.warn('clawd', 'dashboard 信息获取失败:', e.message); return null; }) + : Promise.resolve({}), startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)), ]); this._dashInfo = dashInfo || {}; @@ -377,6 +381,7 @@ class ClawClient { _sendConnect() { const msg = { type: 'connect', + agent_type: this._agentType, box_id: this._boxId, claw_id: this._cfg.claw_id ?? null, token: this._cfg.token ?? null, @@ -416,7 +421,9 @@ class ClawClient { break; case 'error': log.error('clawd', `服务器错误: ${msg.msg}`); - if (msg.msg === 'hardware_mismatch') { + if (msg.msg && msg.msg.includes('agent_type_mismatch')) { + log.error('clawd', `Agent 类型切换被拒绝:请先在云端解绑设备,再设置 AGENT_TYPE=${this._agentType} 并重启 clawd`); + } else if (msg.msg === 'hardware_mismatch') { log.warn('clawd', '硬件指纹不符,清除凭证重新注册...'); this._cfg.claw_id = null; this._cfg.token = null; @@ -452,7 +459,7 @@ class ClawClient { this._applyStatus(msg); if (msg.frp && msg.frp.server && msg.frp.auth_token) { - this._frpc.start(msg.claw_id, msg.frp, this._cfg.ssh_secret_key ?? null).catch(e => { + this._frpc.start(msg.claw_id, msg.frp, this._cfg.ssh_secret_key ?? null, this._isOpenClaw()).catch(e => { log.error('frpc', '启动失败:', e.message); }); } @@ -474,7 +481,7 @@ class ClawClient { _applyStatus(msg) { if (msg.status === 'inactive') { - if (msg.provider && msg.provider.name) { + if (this._isOpenClaw() && msg.provider && msg.provider.name) { removeProviderByName(String(msg.provider.name)); } this._cfg.activated = false; @@ -491,7 +498,7 @@ class ClawClient { log.info('clawd', '╚════════════════════════════════════╝'); log.info('clawd', ''); log.info('clawd', '等待激活,心跳正常运行...'); - this._updateOpenClawOrigin('0000'); + if (this._isOpenClaw()) this._updateOpenClawOrigin('0000'); } else { this._cfg.activated = true; config.save(this._cfg); @@ -499,7 +506,9 @@ class ClawClient { led.display.showTime(); log.info('clawd', `已激活 claw_id = ${this._cfg.claw_id}`); const clawIdStr = String(this._cfg.claw_id); - if (isFullProvider(msg.provider)) { + if (!this._isOpenClaw()) { + log.info('clawd', `agent=${this._agentType},跳过 OpenClaw provider/origin 配置`); + } else if (isFullProvider(msg.provider)) { applyFullProviderFromVps(msg.provider, () => { this._updateOpenClawOrigin(clawIdStr); }); @@ -541,6 +550,7 @@ class ClawClient { // ── OpenClaw 配置 ──────────────────────────────────────────────────────────── _updateOpenClawOrigin(targetId) { + if (!this._isOpenClaw()) return; const { readFileSync, writeFileSync } = require('fs'); const configFile = resolveOpenclawConfigFile(); @@ -633,7 +643,7 @@ class ClawClient { this._hbCount++; // 每 30 次心跳(约 5 分钟)刷新一次 dashboard 信息 - if (this._hbCount % 30 === 0) { + if (this._isOpenClaw() && this._hbCount % 30 === 0) { const freshInfo = await getDashboardInfo().catch(() => null); if (freshInfo && Object.keys(freshInfo).length > 0) { this._dashInfo = freshInfo; @@ -643,6 +653,7 @@ class ClawClient { // 每 METRICS_EVERY_N 次心跳(30 秒)采集一次指标,其余发轻量心跳 const msg = { type: 'heartbeat', + agent_type: this._agentType, claw_id: this._cfg.claw_id, token: this._cfg.token, version: CLAWD_VERSION, @@ -666,6 +677,10 @@ class ClawClient { } } + _isOpenClaw() { + return this._agentType === OPENCLAW; + } + // ── 升级 ──────────────────────────────────────────────────────────────────── _sendUpgradeProgress(progress, step, failed = false, errorMsg = null) { diff --git a/lib/frpc.js b/lib/frpc.js index 17ff7c3..7d40554 100644 --- a/lib/frpc.js +++ b/lib/frpc.js @@ -147,7 +147,7 @@ function downloadFile(url, dest) { }); } -function writeFrpcConfig(clawId, frpConfig, sshSecretKey) { +function writeFrpcConfig(clawId, frpConfig, sshSecretKey, dashboardEnabled = true) { const { auth_token, dashboard_local_port = 18789 } = frpConfig; const ttyRemotePort = 10000 + Number(clawId); const stcpBlock = sshSecretKey ? ` @@ -156,6 +156,20 @@ name = "ssh-${clawId}-secret" type = "stcp" secretKey = "${sshSecretKey}" localPort = 22 +` : ''; + const ttyBlock = ` +[[proxies]] +name = "tty-${clawId}" +type = "tcp" +localPort = ${TTYD_PORT} +remotePort = ${ttyRemotePort} +`; + const dashboardBlock = dashboardEnabled ? ` +[[proxies]] +name = "dashboard-${clawId}" +type = "http" +localPort = ${dashboard_local_port} +subdomain = "${clawId}" ` : ''; const toml = `# 由 clawd 自动生成,请勿手动修改 serverAddr = "frp.claw.cutos.ai" @@ -167,22 +181,10 @@ token = "${auth_token}" [transport] tls.enable = true - -[[proxies]] -name = "dashboard-${clawId}" -type = "http" -localPort = ${dashboard_local_port} -subdomain = "${clawId}" - -[[proxies]] -name = "tty-${clawId}" -type = "tcp" -localPort = ${TTYD_PORT} -remotePort = ${ttyRemotePort} -${stcpBlock}`; +${dashboardBlock}${ttyBlock}${stcpBlock}`; fs.mkdirSync(CONFIG_DIR, { recursive: true }); fs.writeFileSync(FRPC_CONFIG, toml, 'utf8'); - log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}${sshSecretKey ? ', ssh stcp=enabled' : ''}`); + log.info('frpc', `frpc.toml 已写入: dashboard=${dashboardEnabled ? `subdomain ${clawId}` : 'disabled'}, tty tcp-port=${ttyRemotePort}${stcpBlock ? ', ssh stcp=enabled' : ''}`); } /** @@ -194,7 +196,7 @@ class FrpcManager { this._watchdog = null; } - async start(clawId, frpConfig, sshSecretKey) { + async start(clawId, frpConfig, sshSecretKey, dashboardEnabled = true) { this.stop(); if (!fs.existsSync(FRPC_BIN)) { @@ -206,7 +208,7 @@ class FrpcManager { } } - writeFrpcConfig(clawId, frpConfig, sshSecretKey); + writeFrpcConfig(clawId, frpConfig, sshSecretKey, dashboardEnabled); this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], { maxRestarts: 10, diff --git a/lib/sys-call.js b/lib/sys-call.js index d2db635..1d50a25 100644 --- a/lib/sys-call.js +++ b/lib/sys-call.js @@ -15,6 +15,7 @@ */ const log = require('./logger'); +const { OPENCLAW, resolveAgentType } = require('./agent-type'); // ── channel handlers ────────────────────────────────────────────────────────── const handlers = { @@ -39,6 +40,15 @@ function handle(msg, send) { return; } + if (api === 'channel.weixin' && resolveAgentType() !== OPENCLAW) { + send({ + id: callId, api, method, + action: 'finish', event: 'failed', + code: 404, message: `api unavailable for agent type: ${resolveAgentType()}`, + }); + return; + } + // ── cancel ──────────────────────────────────────────────────────────────── if (action === 'cancel') { const task = running.get(callId); diff --git a/package.json b/package.json index 03d1149..cb1d40c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "clawd": "./bin/clawd.js" }, "scripts": { - "start": "node bin/clawd.js" + "start": "node bin/clawd.js", + "test": "node --test test/*.test.js" }, "keywords": [ "claw", diff --git a/test/agent-type.test.js b/test/agent-type.test.js new file mode 100644 index 0000000..d1bfdcb --- /dev/null +++ b/test/agent-type.test.js @@ -0,0 +1,20 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { resolveAgentType } = require('../lib/agent-type'); + +test('missing and blank AGENT_TYPE default to openclaw', () => { + assert.equal(resolveAgentType(undefined), 'openclaw'); + assert.equal(resolveAgentType(''), 'openclaw'); + assert.equal(resolveAgentType(' '), 'openclaw'); +}); + +test('supported AGENT_TYPE values are normalized', () => { + assert.equal(resolveAgentType('OPENCLAW'), 'openclaw'); + assert.equal(resolveAgentType(' cutos-agent '), 'cutos-agent'); +}); + +test('unsupported AGENT_TYPE fails fast', () => { + assert.throws(() => resolveAgentType('other'), /AGENT_TYPE must be one of/); +});