feat: recognize CutOS agent runtime
This commit is contained in:
@@ -105,9 +105,14 @@ node bin/clawd.js
|
|||||||
| `CLAWD_LOG_FILE` | `1` | 是否写日志文件(`0` = 仅 stdout/journald) |
|
| `CLAWD_LOG_FILE` | `1` | 是否写日志文件(`0` = 仅 stdout/journald) |
|
||||||
| `CLAWD_LOG_DIR` | `~/.clawd/logs` | 日志文件目录 |
|
| `CLAWD_LOG_DIR` | `~/.clawd/logs` | 日志文件目录 |
|
||||||
| `CLAWD_CONFIG_DIR` | `~/.clawd` | 配置目录 |
|
| `CLAWD_CONFIG_DIR` | `~/.clawd` | 配置目录 |
|
||||||
|
| `AGENT_TYPE` | `openclaw` | Agent 类型:`openclaw` 或 `cutos-agent`;空值兼容为 `openclaw` |
|
||||||
|
|
||||||
systemd 安装后环境变量文件位于 `/etc/clawd/env`。
|
systemd 安装后环境变量文件位于 `/etc/clawd/env`。
|
||||||
|
|
||||||
|
切换 Agent 类型前,必须先在云端解绑设备,再修改 `/etc/clawd/env` 中的
|
||||||
|
`AGENT_TYPE`,重启 `clawd` 后重新绑定。`cutos-agent` 模式当前仅接入通用设备管理,
|
||||||
|
不会读写 OpenClaw 配置或调用 OpenClaw 微信能力。
|
||||||
|
|
||||||
## 服务管理
|
## 服务管理
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ const { ClawClient } = require('../lib/client');
|
|||||||
const config = require('../lib/config');
|
const config = require('../lib/config');
|
||||||
const log = require('../lib/logger');
|
const log = require('../lib/logger');
|
||||||
const { pollSms } = require('../drivers/sim/sms-reader');
|
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,失败不影响主流程)
|
// 每次启动同步 3588s demo 到 /usr/bin/demo(idempotent,失败不影响主流程)
|
||||||
const demoBin = path.join(__dirname, "..", "lib/resource/3588s/demo");
|
const demoBin = path.join(__dirname, "..", "lib/resource/3588s/demo");
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ CLAWD_LOG_LEVEL=info
|
|||||||
CLAWD_LOG_FILE=1
|
CLAWD_LOG_FILE=1
|
||||||
# Override server URL (default from config.json)
|
# Override server URL (default from config.json)
|
||||||
# CLAWD_SERVER=wss://claw.cutos.ai/ws
|
# CLAWD_SERVER=wss://claw.cutos.ai/ws
|
||||||
|
# Agent runtime: openclaw (default) or cutos-agent
|
||||||
|
# AGENT_TYPE=openclaw
|
||||||
# Enable Bluetooth monitor (bluetoothctl); disabled by default
|
# Enable Bluetooth monitor (bluetoothctl); disabled by default
|
||||||
# CLAWD_ENABLE_BT=1
|
# CLAWD_ENABLE_BT=1
|
||||||
# OpenVFD sysfs path (default: /sys/class/leds/openvfd)
|
# OpenVFD sysfs path (default: /sys/class/leds/openvfd)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
+23
-8
@@ -19,6 +19,7 @@ const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = re
|
|||||||
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
||||||
const sysCall = require('./sys-call');
|
const sysCall = require('./sys-call');
|
||||||
const led = require('./led');
|
const led = require('./led');
|
||||||
|
const { OPENCLAW, resolveAgentType } = require('./agent-type');
|
||||||
|
|
||||||
const MAX_BACKOFF_MS = 60_000;
|
const MAX_BACKOFF_MS = 60_000;
|
||||||
/** 连续若干轮 ping 后仍无 pong 才判定死链(单轮易因调度/弱网误判) */
|
/** 连续若干轮 ping 后仍无 pong 才判定死链(单轮易因调度/弱网误判) */
|
||||||
@@ -54,6 +55,7 @@ function btMonitorEnabled() {
|
|||||||
class ClawClient {
|
class ClawClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._cfg = config.load();
|
this._cfg = config.load();
|
||||||
|
this._agentType = resolveAgentType();
|
||||||
this._boxId = getBoxId();
|
this._boxId = getBoxId();
|
||||||
this._ws = null;
|
this._ws = null;
|
||||||
this._hbTimer = null;
|
this._hbTimer = null;
|
||||||
@@ -104,7 +106,7 @@ class ClawClient {
|
|||||||
// ── 生命周期 ─────────────────────────────────────────────────────────────────
|
// ── 生命周期 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}, agent = ${this._agentType}`);
|
||||||
|
|
||||||
if (this._cfg.claw_id) {
|
if (this._cfg.claw_id) {
|
||||||
this._setHostname(this._cfg.claw_id);
|
this._setHostname(this._cfg.claw_id);
|
||||||
@@ -193,7 +195,9 @@ class ClawClient {
|
|||||||
|
|
||||||
async _proceedWithConnection() {
|
async _proceedWithConnection() {
|
||||||
const [dashInfo] = await Promise.all([
|
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)),
|
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
|
||||||
]);
|
]);
|
||||||
this._dashInfo = dashInfo || {};
|
this._dashInfo = dashInfo || {};
|
||||||
@@ -377,6 +381,7 @@ class ClawClient {
|
|||||||
_sendConnect() {
|
_sendConnect() {
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'connect',
|
type: 'connect',
|
||||||
|
agent_type: this._agentType,
|
||||||
box_id: this._boxId,
|
box_id: this._boxId,
|
||||||
claw_id: this._cfg.claw_id ?? null,
|
claw_id: this._cfg.claw_id ?? null,
|
||||||
token: this._cfg.token ?? null,
|
token: this._cfg.token ?? null,
|
||||||
@@ -416,7 +421,9 @@ class ClawClient {
|
|||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
log.error('clawd', `服务器错误: ${msg.msg}`);
|
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', '硬件指纹不符,清除凭证重新注册...');
|
log.warn('clawd', '硬件指纹不符,清除凭证重新注册...');
|
||||||
this._cfg.claw_id = null;
|
this._cfg.claw_id = null;
|
||||||
this._cfg.token = null;
|
this._cfg.token = null;
|
||||||
@@ -452,7 +459,7 @@ class ClawClient {
|
|||||||
this._applyStatus(msg);
|
this._applyStatus(msg);
|
||||||
|
|
||||||
if (msg.frp && msg.frp.server && msg.frp.auth_token) {
|
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);
|
log.error('frpc', '启动失败:', e.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -474,7 +481,7 @@ class ClawClient {
|
|||||||
|
|
||||||
_applyStatus(msg) {
|
_applyStatus(msg) {
|
||||||
if (msg.status === 'inactive') {
|
if (msg.status === 'inactive') {
|
||||||
if (msg.provider && msg.provider.name) {
|
if (this._isOpenClaw() && msg.provider && msg.provider.name) {
|
||||||
removeProviderByName(String(msg.provider.name));
|
removeProviderByName(String(msg.provider.name));
|
||||||
}
|
}
|
||||||
this._cfg.activated = false;
|
this._cfg.activated = false;
|
||||||
@@ -491,7 +498,7 @@ class ClawClient {
|
|||||||
log.info('clawd', '╚════════════════════════════════════╝');
|
log.info('clawd', '╚════════════════════════════════════╝');
|
||||||
log.info('clawd', '');
|
log.info('clawd', '');
|
||||||
log.info('clawd', '等待激活,心跳正常运行...');
|
log.info('clawd', '等待激活,心跳正常运行...');
|
||||||
this._updateOpenClawOrigin('0000');
|
if (this._isOpenClaw()) this._updateOpenClawOrigin('0000');
|
||||||
} else {
|
} else {
|
||||||
this._cfg.activated = true;
|
this._cfg.activated = true;
|
||||||
config.save(this._cfg);
|
config.save(this._cfg);
|
||||||
@@ -499,7 +506,9 @@ class ClawClient {
|
|||||||
led.display.showTime();
|
led.display.showTime();
|
||||||
log.info('clawd', `已激活 claw_id = ${this._cfg.claw_id}`);
|
log.info('clawd', `已激活 claw_id = ${this._cfg.claw_id}`);
|
||||||
const clawIdStr = String(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, () => {
|
applyFullProviderFromVps(msg.provider, () => {
|
||||||
this._updateOpenClawOrigin(clawIdStr);
|
this._updateOpenClawOrigin(clawIdStr);
|
||||||
});
|
});
|
||||||
@@ -541,6 +550,7 @@ class ClawClient {
|
|||||||
// ── OpenClaw 配置 ────────────────────────────────────────────────────────────
|
// ── OpenClaw 配置 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_updateOpenClawOrigin(targetId) {
|
_updateOpenClawOrigin(targetId) {
|
||||||
|
if (!this._isOpenClaw()) return;
|
||||||
const { readFileSync, writeFileSync } = require('fs');
|
const { readFileSync, writeFileSync } = require('fs');
|
||||||
const configFile = resolveOpenclawConfigFile();
|
const configFile = resolveOpenclawConfigFile();
|
||||||
|
|
||||||
@@ -633,7 +643,7 @@ class ClawClient {
|
|||||||
this._hbCount++;
|
this._hbCount++;
|
||||||
|
|
||||||
// 每 30 次心跳(约 5 分钟)刷新一次 dashboard 信息
|
// 每 30 次心跳(约 5 分钟)刷新一次 dashboard 信息
|
||||||
if (this._hbCount % 30 === 0) {
|
if (this._isOpenClaw() && this._hbCount % 30 === 0) {
|
||||||
const freshInfo = await getDashboardInfo().catch(() => null);
|
const freshInfo = await getDashboardInfo().catch(() => null);
|
||||||
if (freshInfo && Object.keys(freshInfo).length > 0) {
|
if (freshInfo && Object.keys(freshInfo).length > 0) {
|
||||||
this._dashInfo = freshInfo;
|
this._dashInfo = freshInfo;
|
||||||
@@ -643,6 +653,7 @@ class ClawClient {
|
|||||||
// 每 METRICS_EVERY_N 次心跳(30 秒)采集一次指标,其余发轻量心跳
|
// 每 METRICS_EVERY_N 次心跳(30 秒)采集一次指标,其余发轻量心跳
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'heartbeat',
|
type: 'heartbeat',
|
||||||
|
agent_type: this._agentType,
|
||||||
claw_id: this._cfg.claw_id,
|
claw_id: this._cfg.claw_id,
|
||||||
token: this._cfg.token,
|
token: this._cfg.token,
|
||||||
version: CLAWD_VERSION,
|
version: CLAWD_VERSION,
|
||||||
@@ -666,6 +677,10 @@ class ClawClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isOpenClaw() {
|
||||||
|
return this._agentType === OPENCLAW;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 升级 ────────────────────────────────────────────────────────────────────
|
// ── 升级 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_sendUpgradeProgress(progress, step, failed = false, errorMsg = null) {
|
_sendUpgradeProgress(progress, step, failed = false, errorMsg = null) {
|
||||||
|
|||||||
+19
-17
@@ -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 { auth_token, dashboard_local_port = 18789 } = frpConfig;
|
||||||
const ttyRemotePort = 10000 + Number(clawId);
|
const ttyRemotePort = 10000 + Number(clawId);
|
||||||
const stcpBlock = sshSecretKey ? `
|
const stcpBlock = sshSecretKey ? `
|
||||||
@@ -156,6 +156,20 @@ name = "ssh-${clawId}-secret"
|
|||||||
type = "stcp"
|
type = "stcp"
|
||||||
secretKey = "${sshSecretKey}"
|
secretKey = "${sshSecretKey}"
|
||||||
localPort = 22
|
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 自动生成,请勿手动修改
|
const toml = `# 由 clawd 自动生成,请勿手动修改
|
||||||
serverAddr = "frp.claw.cutos.ai"
|
serverAddr = "frp.claw.cutos.ai"
|
||||||
@@ -167,22 +181,10 @@ token = "${auth_token}"
|
|||||||
|
|
||||||
[transport]
|
[transport]
|
||||||
tls.enable = true
|
tls.enable = true
|
||||||
|
${dashboardBlock}${ttyBlock}${stcpBlock}`;
|
||||||
[[proxies]]
|
|
||||||
name = "dashboard-${clawId}"
|
|
||||||
type = "http"
|
|
||||||
localPort = ${dashboard_local_port}
|
|
||||||
subdomain = "${clawId}"
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tty-${clawId}"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = ${TTYD_PORT}
|
|
||||||
remotePort = ${ttyRemotePort}
|
|
||||||
${stcpBlock}`;
|
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
|
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;
|
this._watchdog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(clawId, frpConfig, sshSecretKey) {
|
async start(clawId, frpConfig, sshSecretKey, dashboardEnabled = true) {
|
||||||
this.stop();
|
this.stop();
|
||||||
|
|
||||||
if (!fs.existsSync(FRPC_BIN)) {
|
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], {
|
this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], {
|
||||||
maxRestarts: 10,
|
maxRestarts: 10,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
|
const { OPENCLAW, resolveAgentType } = require('./agent-type');
|
||||||
|
|
||||||
// ── channel handlers ──────────────────────────────────────────────────────────
|
// ── channel handlers ──────────────────────────────────────────────────────────
|
||||||
const handlers = {
|
const handlers = {
|
||||||
@@ -39,6 +40,15 @@ function handle(msg, send) {
|
|||||||
return;
|
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 ────────────────────────────────────────────────────────────────
|
// ── cancel ────────────────────────────────────────────────────────────────
|
||||||
if (action === 'cancel') {
|
if (action === 'cancel') {
|
||||||
const task = running.get(callId);
|
const task = running.get(callId);
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"clawd": "./bin/clawd.js"
|
"clawd": "./bin/clawd.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node bin/clawd.js"
|
"start": "node bin/clawd.js",
|
||||||
|
"test": "node --test test/*.test.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claw",
|
"claw",
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user