feat: recognize CutOS agent runtime

This commit is contained in:
yankun
2026-08-02 12:54:17 +08:00
parent 7de0777748
commit 586b978f81
9 changed files with 113 additions and 29 deletions
+21
View File
@@ -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
View File
@@ -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) {
+19 -17
View File
@@ -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,
+10
View File
@@ -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);