claw_messenger 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +577 -0
- package/bin/auto-init.js +104 -0
- package/bin/cli.js +5 -0
- package/bin/diagnose-plugin.js +174 -0
- package/bin/dm-bridge.cjs +12 -0
- package/bin/install.js +452 -0
- package/bin/postinstall.js +23 -0
- package/bin/qr-crypto-node.js +186 -0
- package/bin/setup.js +262 -0
- package/dist/auto-register.d.ts +49 -0
- package/dist/auto-register.js +328 -0
- package/dist/bridge-runner.d.ts +1 -0
- package/dist/bridge-runner.js +107 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/device-status.d.ts +30 -0
- package/dist/device-status.js +109 -0
- package/dist/env-polyfill.d.ts +3 -0
- package/dist/env-polyfill.js +166 -0
- package/dist/group-config-manager.d.ts +22 -0
- package/dist/group-config-manager.js +130 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +36 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +103 -0
- package/dist/mac-address.d.ts +1 -0
- package/dist/mac-address.js +46 -0
- package/dist/openclaw-client.d.ts +41 -0
- package/dist/openclaw-client.js +530 -0
- package/dist/openclaw-config.d.ts +41 -0
- package/dist/openclaw-config.js +359 -0
- package/dist/openclaw.plugin.json +40 -0
- package/dist/package.json +112 -0
- package/dist/plugin-entry.d.ts +54 -0
- package/dist/plugin-entry.js +772 -0
- package/dist/postinstall.js +23 -0
- package/dist/rongcloud-client.d.ts +16 -0
- package/dist/rongcloud-client.js +274 -0
- package/dist/rongcloud-server-api.d.ts +53 -0
- package/dist/rongcloud-server-api.js +221 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +97 -0
- package/openclaw.plugin.json +40 -0
- package/package.json +112 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自动注册模块
|
|
3
|
+
* 通过服务端接口注册节点,获取融云 Token
|
|
4
|
+
*/
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import { readFile, access, writeFile, mkdir, copyFile } from 'fs/promises';
|
|
11
|
+
import https from 'https';
|
|
12
|
+
import { getMacAddress } from './mac-address.js';
|
|
13
|
+
import { createLogger } from './logger.js';
|
|
14
|
+
const log = createLogger('auto-register');
|
|
15
|
+
const DEFAULT_SERVER_URL = 'https://newsradar.dreamdt.cn/im';
|
|
16
|
+
const TOKEN_VALIDITY_MS = 7 * 24 * 60 * 60 * 1000; // 7天
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.claw-bridge', 'openclaw');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
+
const LEGACY_CONFIG_FILE = path.join(os.homedir(), '.claw-bridge', 'config.json');
|
|
20
|
+
/**
|
|
21
|
+
* 生成节点ID
|
|
22
|
+
*/
|
|
23
|
+
function generateNodeId() {
|
|
24
|
+
const mac = getMacAddress();
|
|
25
|
+
const random = crypto.randomBytes(3).toString('hex');
|
|
26
|
+
return `claw_${mac.replace(/:/g, '').substring(0, 6)}_${random}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 从 OpenClaw USER.md 获取昵称
|
|
30
|
+
*/
|
|
31
|
+
async function getNicknameFromOpenClaw() {
|
|
32
|
+
try {
|
|
33
|
+
const homeDir = os.homedir();
|
|
34
|
+
const userMdPath = path.join(homeDir, '.openclaw', 'workspace', 'USER.md');
|
|
35
|
+
await access(userMdPath);
|
|
36
|
+
const content = await readFile(userMdPath, 'utf8');
|
|
37
|
+
const nicknameMatch = content.match(/nickname\s*:\s*(.+)/i);
|
|
38
|
+
if (nicknameMatch && nicknameMatch[1]) {
|
|
39
|
+
return nicknameMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
catch (_a) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 保存昵称到 OpenClaw USER.md
|
|
49
|
+
*/
|
|
50
|
+
async function saveNicknameToOpenClaw(nickname) {
|
|
51
|
+
try {
|
|
52
|
+
const homeDir = os.homedir();
|
|
53
|
+
const openclawDir = path.join(homeDir, '.openclaw', 'workspace');
|
|
54
|
+
const userMdPath = path.join(openclawDir, 'USER.md');
|
|
55
|
+
await mkdir(openclawDir, { recursive: true });
|
|
56
|
+
let content = '';
|
|
57
|
+
try {
|
|
58
|
+
content = await readFile(userMdPath, 'utf8');
|
|
59
|
+
if (/nickname\s*:/.test(content)) {
|
|
60
|
+
content = content.replace(/nickname\s*:.*/g, `nickname: ${nickname}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
content += `\nnickname: ${nickname}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (_a) {
|
|
67
|
+
content = `# OpenClaw User Profile\n\nnickname: ${nickname}\n`;
|
|
68
|
+
}
|
|
69
|
+
await writeFile(userMdPath, content, 'utf8');
|
|
70
|
+
log.info(`昵称已保存到 OpenClaw: ${userMdPath}`);
|
|
71
|
+
}
|
|
72
|
+
catch (_b) { }
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 获取设备信息
|
|
76
|
+
*/
|
|
77
|
+
function getDeviceInfo() {
|
|
78
|
+
var _a;
|
|
79
|
+
return {
|
|
80
|
+
deviceName: os.hostname(),
|
|
81
|
+
platform: os.platform(),
|
|
82
|
+
arch: os.arch(),
|
|
83
|
+
totalMem: os.totalmem(),
|
|
84
|
+
freeMem: os.freemem(),
|
|
85
|
+
cpuModel: ((_a = os.cpus()[0]) === null || _a === void 0 ? void 0 : _a.model) || 'Unknown',
|
|
86
|
+
cpuCount: os.cpus().length,
|
|
87
|
+
macAddress: getMacAddress()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 通过服务端刷新节点融云 Token
|
|
92
|
+
* 服务端接口: POST /api/claw/refresh-token/<node_id>
|
|
93
|
+
*/
|
|
94
|
+
async function refreshNodeToken(serverUrl, nodeId) {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
try {
|
|
97
|
+
log.info(`[AutoRegister] 刷新节点 token: ${nodeId}`);
|
|
98
|
+
const resp = await axios.post(`${serverUrl}/api/claw/refresh-token/${nodeId}`, {}, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
|
|
99
|
+
const rawToken = (_b = (_a = resp.data) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.token;
|
|
100
|
+
const token = typeof rawToken === 'string' ? rawToken : '';
|
|
101
|
+
if (!token) {
|
|
102
|
+
log.error(`[AutoRegister] 刷新 token 接口未返回有效 token, response=${JSON.stringify(resp.data)}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return token;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
log.error(`[AutoRegister] 刷新节点 token 失败: ${err.message}`);
|
|
109
|
+
if (err.response) {
|
|
110
|
+
log.error(`[AutoRegister] 服务端响应: status=${err.response.status}, data=${JSON.stringify(err.response.data)}`);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 注册节点 - 调用服务端接口
|
|
117
|
+
*/
|
|
118
|
+
export async function registerNode(serverUrl, nodeName) {
|
|
119
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
120
|
+
const url = serverUrl || process.env.DM_SERVER_URL || DEFAULT_SERVER_URL;
|
|
121
|
+
const nodeId = generateNodeId();
|
|
122
|
+
const nickname = nodeName || await getNicknameFromOpenClaw() || os.hostname();
|
|
123
|
+
const macAddress = getMacAddress();
|
|
124
|
+
log.info(`[AutoRegister] 注册节点: ${nodeId}`);
|
|
125
|
+
log.info(`[AutoRegister] 昵称: ${nickname}`);
|
|
126
|
+
log.info(`[AutoRegister] MAC: ${macAddress}`);
|
|
127
|
+
try {
|
|
128
|
+
const response = await axios.post(`${url}/api/claw/register`, {
|
|
129
|
+
node_id: nodeId,
|
|
130
|
+
name: nickname,
|
|
131
|
+
mac_address: macAddress
|
|
132
|
+
}, {
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
timeout: 15000
|
|
135
|
+
});
|
|
136
|
+
if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.code) === 200) {
|
|
137
|
+
const rawToken = ((_b = response.data.data) === null || _b === void 0 ? void 0 : _b.token) || ((_c = response.data.data) === null || _c === void 0 ? void 0 : _c.rong_token) || response.data.rong_token;
|
|
138
|
+
let token = typeof rawToken === 'string' ? rawToken : '';
|
|
139
|
+
const serverNodeId = ((_d = response.data.data) === null || _d === void 0 ? void 0 : _d.node_id) || response.data.node_id || '';
|
|
140
|
+
// 使用服务端返回的 node_id,而不是客户端生成的
|
|
141
|
+
const actualNodeId = serverNodeId || nodeId;
|
|
142
|
+
// 适配:部分部署环境下 register 返回的 token 可能是模型配置对象,
|
|
143
|
+
// 尝试通过 refresh-token 接口获取真正的融云 token
|
|
144
|
+
if (!token && actualNodeId) {
|
|
145
|
+
log.warn(`[AutoRegister] 注册接口 token 无效,尝试刷新 token...`);
|
|
146
|
+
token = (await refreshNodeToken(url, actualNodeId)) || '';
|
|
147
|
+
}
|
|
148
|
+
if (!token) {
|
|
149
|
+
log.error(`[AutoRegister] 注册接口未返回有效 token, response=${JSON.stringify(response.data)}`);
|
|
150
|
+
return { nodeId, nodeName: nickname, token: '', success: false };
|
|
151
|
+
}
|
|
152
|
+
log.info(`[AutoRegister] 注册成功, nodeId=${actualNodeId}, Token: ${token.substring(0, 20)}...`);
|
|
153
|
+
await saveConfig({ nodeId: actualNodeId, nodeName: nickname, token, macAddress });
|
|
154
|
+
return { nodeId: actualNodeId, nodeName: nickname, token, success: true };
|
|
155
|
+
}
|
|
156
|
+
else if (((_e = response.data) === null || _e === void 0 ? void 0 : _e.code) === 409) {
|
|
157
|
+
log.info(`[AutoRegister] 节点已存在,刷新 Token...`);
|
|
158
|
+
const token = (await refreshNodeToken(url, nodeId)) || '';
|
|
159
|
+
if (!token) {
|
|
160
|
+
log.error(`[AutoRegister] 节点已存在但刷新 token 失败`);
|
|
161
|
+
return { nodeId, nodeName: nickname, token: '', success: false };
|
|
162
|
+
}
|
|
163
|
+
await saveConfig({ nodeId, nodeName: nickname, token, macAddress });
|
|
164
|
+
return { nodeId, nodeName: nickname, token, success: true };
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
log.error(`[AutoRegister] 注册失败: code=${(_f = response.data) === null || _f === void 0 ? void 0 : _f.code}, message=${(_g = response.data) === null || _g === void 0 ? void 0 : _g.message}, data=${JSON.stringify(response.data)}`);
|
|
168
|
+
return { nodeId, nodeName: nickname, token: '', success: false };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
log.error(`[AutoRegister] 注册异常: ${err.message}`);
|
|
173
|
+
if (err.response) {
|
|
174
|
+
log.error(`[AutoRegister] 服务端响应: status=${err.response.status}, data=${JSON.stringify(err.response.data)}`);
|
|
175
|
+
}
|
|
176
|
+
return { nodeId, nodeName: nickname, token: '', success: false };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 获取或注册 Token
|
|
181
|
+
* 始终从服务端获取新 token,不使用本地缓存(缓存 token 可能已过期导致 31029)
|
|
182
|
+
*
|
|
183
|
+
* @param nodeName 节点昵称
|
|
184
|
+
* @param forceRegister 是否强制重新注册(忽略已有配置)
|
|
185
|
+
*/
|
|
186
|
+
export async function getOrRegisterToken(nodeName, forceRegister = false) {
|
|
187
|
+
try {
|
|
188
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
catch (_a) { }
|
|
191
|
+
// 1. 优先尝试从 config.json 获取 nodeId,然后向服务端请求新 token
|
|
192
|
+
// 但如果 forceRegister=true,则跳过此步骤,直接重新注册
|
|
193
|
+
if (!forceRegister) {
|
|
194
|
+
const existingConfig = await loadConfig();
|
|
195
|
+
if ((existingConfig === null || existingConfig === void 0 ? void 0 : existingConfig.nodeId) && typeof existingConfig.token === 'string') {
|
|
196
|
+
const url = process.env.DM_SERVER_URL || DEFAULT_SERVER_URL;
|
|
197
|
+
try {
|
|
198
|
+
log.info(`[AutoRegister] 从服务端刷新 token, nodeId=${existingConfig.nodeId}`);
|
|
199
|
+
const token = (await refreshNodeToken(url, existingConfig.nodeId)) || '';
|
|
200
|
+
if (token) {
|
|
201
|
+
// 更新本地配置中的 token
|
|
202
|
+
await saveConfig({
|
|
203
|
+
nodeId: existingConfig.nodeId,
|
|
204
|
+
nodeName: existingConfig.nodeName || nodeName || '',
|
|
205
|
+
token,
|
|
206
|
+
macAddress: existingConfig.macAddress || '',
|
|
207
|
+
});
|
|
208
|
+
log.info(`[AutoRegister] token 已刷新, nodeId=${existingConfig.nodeId}`);
|
|
209
|
+
return token;
|
|
210
|
+
}
|
|
211
|
+
log.warn(`[AutoRegister] 服务端刷新 token 失败,将重新注册`);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
log.error(`[AutoRegister] 刷新 token 异常: ${err.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
log.info('[AutoRegister] 强制重新注册,清除旧配置...');
|
|
220
|
+
try {
|
|
221
|
+
await fs.promises.unlink(CONFIG_FILE);
|
|
222
|
+
log.info('[AutoRegister] 旧配置已清除');
|
|
223
|
+
}
|
|
224
|
+
catch (_b) {
|
|
225
|
+
// 文件不存在,忽略错误
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// 2. 如果没有 config 或刷新失败,走全新注册流程
|
|
229
|
+
log.info('[AutoRegister] 尝试服务端注册...');
|
|
230
|
+
const result = await registerNode(undefined, nodeName);
|
|
231
|
+
if (result.success && result.token) {
|
|
232
|
+
log.info(`[AutoRegister] 注册成功, nodeId=${result.nodeId}`);
|
|
233
|
+
return result.token;
|
|
234
|
+
}
|
|
235
|
+
log.error('[AutoRegister] 获取 token 失败,融云功能将不可用');
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 保存配置
|
|
240
|
+
*/
|
|
241
|
+
async function saveConfig(config) {
|
|
242
|
+
try {
|
|
243
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
244
|
+
}
|
|
245
|
+
catch (_a) { }
|
|
246
|
+
const data = {
|
|
247
|
+
nodeId: config.nodeId,
|
|
248
|
+
nodeName: config.nodeName,
|
|
249
|
+
token: config.token,
|
|
250
|
+
macAddress: config.macAddress,
|
|
251
|
+
createdAt: new Date().toISOString(),
|
|
252
|
+
expiresAt: Date.now() + TOKEN_VALIDITY_MS
|
|
253
|
+
};
|
|
254
|
+
await writeFile(CONFIG_FILE, JSON.stringify(data, null, 2));
|
|
255
|
+
log.info(`[AutoRegister] 配置已保存: ${CONFIG_FILE}`);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 加载配置
|
|
259
|
+
*/
|
|
260
|
+
export async function loadConfig() {
|
|
261
|
+
try {
|
|
262
|
+
// 兼容迁移:旧版缓存位于 ~/.claw-bridge/config.json,新版位于 ~/.claw-bridge/openclaw/config.json
|
|
263
|
+
// 如果新路径不存在但旧路径存在,复制旧配置到新路径,避免用户重新注册
|
|
264
|
+
try {
|
|
265
|
+
await access(CONFIG_FILE);
|
|
266
|
+
}
|
|
267
|
+
catch (_a) {
|
|
268
|
+
try {
|
|
269
|
+
await access(LEGACY_CONFIG_FILE);
|
|
270
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
271
|
+
await copyFile(LEGACY_CONFIG_FILE, CONFIG_FILE);
|
|
272
|
+
}
|
|
273
|
+
catch (_b) {
|
|
274
|
+
// 迁移失败或旧文件不存在,继续正常加载
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await access(CONFIG_FILE);
|
|
278
|
+
const content = await readFile(CONFIG_FILE, 'utf8');
|
|
279
|
+
return JSON.parse(content);
|
|
280
|
+
}
|
|
281
|
+
catch (_c) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export { getMacAddress, getDeviceInfo, getNicknameFromOpenClaw };
|
|
286
|
+
let cachedAppKey;
|
|
287
|
+
let cachedAppSecret;
|
|
288
|
+
export async function getAppKey(serverUrl) {
|
|
289
|
+
var _a, _b;
|
|
290
|
+
if (cachedAppKey)
|
|
291
|
+
return cachedAppKey;
|
|
292
|
+
const baseUrl = (serverUrl || process.env.DM_SERVER_URL || DEFAULT_SERVER_URL).replace(/\/$/, '');
|
|
293
|
+
try {
|
|
294
|
+
const response = await axios.get(`${baseUrl}/api/config/rongcloud`, { timeout: 10000 });
|
|
295
|
+
if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.code) === 200 && ((_b = response.data.data) === null || _b === void 0 ? void 0 : _b.appKey)) {
|
|
296
|
+
cachedAppKey = response.data.data.appKey;
|
|
297
|
+
return cachedAppKey;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
log.warn(`[AutoRegister] 从服务端获取 AppKey 失败: ${e.message}`);
|
|
302
|
+
}
|
|
303
|
+
return process.env.DM_APP_KEY || 'bmdehs6pbyyks';
|
|
304
|
+
}
|
|
305
|
+
export async function getAppSecret(serverUrl, token, nodeId) {
|
|
306
|
+
var _a, _b;
|
|
307
|
+
if (cachedAppSecret)
|
|
308
|
+
return cachedAppSecret;
|
|
309
|
+
const baseUrl = serverUrl.replace(/\/$/, '');
|
|
310
|
+
try {
|
|
311
|
+
const headers = { 'X-Node-Token': token };
|
|
312
|
+
if (nodeId)
|
|
313
|
+
headers['X-Node-Id'] = nodeId;
|
|
314
|
+
const response = await axios.get(`${baseUrl}/api/config/rongcloud/secret`, {
|
|
315
|
+
timeout: 10000,
|
|
316
|
+
headers,
|
|
317
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
|
318
|
+
});
|
|
319
|
+
if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.code) === 200 && ((_b = response.data.data) === null || _b === void 0 ? void 0 : _b.appSecret)) {
|
|
320
|
+
cachedAppSecret = response.data.data.appSecret;
|
|
321
|
+
return cachedAppSecret;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
log.warn(`[AutoRegister] 从服务端获取 AppSecret 失败: ${e.message}`);
|
|
326
|
+
}
|
|
327
|
+
return process.env.DM_APP_SECRET;
|
|
328
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Runner - CLI 模式
|
|
3
|
+
* 用于在命令行中直接运行 OpenClaw Agent
|
|
4
|
+
*/
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as crypto from 'crypto';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import { createLogger } from './logger.js';
|
|
12
|
+
import { spawnOpenClaw } from './utils.js';
|
|
13
|
+
const log = createLogger('bridge-runner');
|
|
14
|
+
const SERVER_URL = process.env.DM_SERVER_URL || 'https://newsradar.dreamdt.cn/im';
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.claw-bridge', 'openclaw');
|
|
16
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout
|
|
20
|
+
});
|
|
21
|
+
const question = (prompt) => {
|
|
22
|
+
return new Promise(resolve => rl.question(prompt, resolve));
|
|
23
|
+
};
|
|
24
|
+
function generateNodeId() {
|
|
25
|
+
const random = crypto.randomBytes(3).toString('hex');
|
|
26
|
+
return `claw_${random}`;
|
|
27
|
+
}
|
|
28
|
+
async function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
31
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (_a) { }
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async function saveConfig(config) {
|
|
39
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
40
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
43
|
+
log.info(`[Bridge] 配置已保存: ${CONFIG_FILE}`);
|
|
44
|
+
}
|
|
45
|
+
async function registerNode(nodeName) {
|
|
46
|
+
var _a, _b, _c;
|
|
47
|
+
const nodeId = generateNodeId();
|
|
48
|
+
log.info(`\n[Bridge] 正在注册节点: ${nodeId}...`);
|
|
49
|
+
try {
|
|
50
|
+
const resp = await axios.post(`${SERVER_URL}/api/claw/register`, {
|
|
51
|
+
node_id: nodeId,
|
|
52
|
+
name: nodeName,
|
|
53
|
+
}, { timeout: 10000 });
|
|
54
|
+
if (((_a = resp.data) === null || _a === void 0 ? void 0 : _a.code) === 200 || ((_b = resp.data) === null || _b === void 0 ? void 0 : _b.code) === 409) {
|
|
55
|
+
return { nodeId };
|
|
56
|
+
}
|
|
57
|
+
log.error('[Bridge] 注册失败:', ((_c = resp.data) === null || _c === void 0 ? void 0 : _c.message) || '未知错误');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
log.error('[Bridge] 注册异常:', err.message);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function callOpenClaw(message) {
|
|
66
|
+
if (!message || !message.trim()) {
|
|
67
|
+
return '消息内容为空';
|
|
68
|
+
}
|
|
69
|
+
const sessionId = `bridge-${Date.now()}`;
|
|
70
|
+
log.info('[Bridge] 调用 OpenClaw CLI...');
|
|
71
|
+
return spawnOpenClaw(['agent', '-m', message, '--session-id', sessionId]);
|
|
72
|
+
}
|
|
73
|
+
async function startInteractiveMode() {
|
|
74
|
+
log.info('\n[Bridge] ========== 交互模式 ==========');
|
|
75
|
+
log.info('输入消息获取 OpenClaw 回复,输入 quit 退出');
|
|
76
|
+
log.info('========================================\n');
|
|
77
|
+
while (true) {
|
|
78
|
+
const input = await question('你: ');
|
|
79
|
+
if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') {
|
|
80
|
+
log.info('[Bridge] 退出');
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (!input.trim())
|
|
84
|
+
continue;
|
|
85
|
+
const reply = await callOpenClaw(input);
|
|
86
|
+
log.info(`\nOpenClaw: ${reply}\n`);
|
|
87
|
+
}
|
|
88
|
+
rl.close();
|
|
89
|
+
}
|
|
90
|
+
async function main() {
|
|
91
|
+
log.info('[Bridge] claw_messenger v1.0.0');
|
|
92
|
+
log.info(`[Bridge] 配置文件: ${CONFIG_FILE}\n`);
|
|
93
|
+
let config = await loadConfig();
|
|
94
|
+
if (config) {
|
|
95
|
+
log.info('[Bridge] 已加载配置:');
|
|
96
|
+
log.info(` 节点 ID: ${config.nodeId}`);
|
|
97
|
+
log.info(` 节点名称: ${config.nodeName}`);
|
|
98
|
+
}
|
|
99
|
+
log.info('\n========================================');
|
|
100
|
+
log.info(' claw_messenger - OpenClaw 桥接');
|
|
101
|
+
log.info('========================================\n');
|
|
102
|
+
await startInteractiveMode();
|
|
103
|
+
}
|
|
104
|
+
main().catch(err => {
|
|
105
|
+
log.error('[Bridge] 启动失败:', err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { createLogger } from './logger.js';
|
|
6
|
+
import { loadConfig, getOrRegisterToken, getAppKey } from './auto-register.js';
|
|
7
|
+
const log = createLogger('cli');
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0] || 'help';
|
|
10
|
+
const LOG_DIR = path.join(os.homedir(), '.clawmessenger', 'logs');
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const PKG_PATH = path.join(__dirname, '..', 'package.json');
|
|
15
|
+
function readPackageVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(PKG_PATH, 'utf-8');
|
|
18
|
+
return JSON.parse(content).version || 'unknown';
|
|
19
|
+
}
|
|
20
|
+
catch (_a) {
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function testConnection() {
|
|
25
|
+
log.info('[clawmessenger] 正在获取节点配置...');
|
|
26
|
+
const config = await loadConfig();
|
|
27
|
+
if (!config) {
|
|
28
|
+
log.error('[clawmessenger] 未找到节点配置,请先运行安装脚本或发送消息触发自动注册');
|
|
29
|
+
log.info(`[clawmessenger] 配置路径: ${path.join(os.homedir(), '.claw-bridge', 'openclaw', 'config.json')}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
log.info(`[clawmessenger] 节点 ID: ${config.nodeId}`);
|
|
33
|
+
log.info(`[clawmessenger] 节点名称: ${config.nodeName}`);
|
|
34
|
+
let token = config.token;
|
|
35
|
+
if (!token) {
|
|
36
|
+
log.info('[clawmessenger] 本地 token 为空,尝试重新获取...');
|
|
37
|
+
token = await getOrRegisterToken(config.nodeName, false);
|
|
38
|
+
}
|
|
39
|
+
if (!token) {
|
|
40
|
+
log.error('[clawmessenger] 无法获取融云 token');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
log.info('[clawmessenger] 融云 token 已获取');
|
|
44
|
+
let appKey;
|
|
45
|
+
try {
|
|
46
|
+
appKey = await getAppKey();
|
|
47
|
+
log.info(`[clawmessenger] 融云 appKey: ${appKey}`);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
log.error('[clawmessenger] 获取融云 appKey 失败:', err.message);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
log.info('[clawmessenger] 正在连接融云 IM(5秒后自动断开)...');
|
|
54
|
+
try {
|
|
55
|
+
const mod = await import('./rongcloud-client.js');
|
|
56
|
+
const RongCloudClient = mod.RongCloudClient;
|
|
57
|
+
const client = new RongCloudClient({
|
|
58
|
+
appKey,
|
|
59
|
+
token,
|
|
60
|
+
accountId: config.nodeId,
|
|
61
|
+
log,
|
|
62
|
+
});
|
|
63
|
+
const connected = await client.connect();
|
|
64
|
+
if (connected) {
|
|
65
|
+
log.info('[clawmessenger] 融云连接成功');
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
client.disconnect().catch(() => { });
|
|
68
|
+
log.info('[clawmessenger] 已断开融云连接');
|
|
69
|
+
}, 5000);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
log.error('[clawmessenger] 融云连接失败');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
log.error('[clawmessenger] 连接融云异常:', err.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function showConfig() {
|
|
80
|
+
const config = await loadConfig();
|
|
81
|
+
if (!config) {
|
|
82
|
+
log.warn('[clawmessenger] 未找到节点配置');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
log.info('[clawmessenger] 节点配置:');
|
|
86
|
+
log.info(` nodeId: ${config.nodeId}`);
|
|
87
|
+
log.info(` nodeName: ${config.nodeName}`);
|
|
88
|
+
log.info(` macAddress:${config.macAddress}`);
|
|
89
|
+
log.info(` expiresAt: ${config.expiresAt ? new Date(config.expiresAt).toISOString() : '无'}`);
|
|
90
|
+
}
|
|
91
|
+
async function showLogs() {
|
|
92
|
+
log.info(`[clawmessenger] 日志目录: ${LOG_DIR}`);
|
|
93
|
+
try {
|
|
94
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
95
|
+
log.warn('[clawmessenger] 日志目录尚未创建,将在首次运行时生成');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const files = fs.readdirSync(LOG_DIR).filter(f => f.endsWith('.log')).sort().reverse();
|
|
99
|
+
if (files.length === 0) {
|
|
100
|
+
log.warn('[clawmessenger] 日志目录为空');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
log.info('[clawmessenger] 最近日志文件:');
|
|
104
|
+
files.slice(0, 10).forEach(f => log.info(` ${f}`));
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.error('[clawmessenger] 读取日志目录失败:', err.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function main() {
|
|
111
|
+
switch (command) {
|
|
112
|
+
case 'status':
|
|
113
|
+
log.info('[clawmessenger] 插件状态: 就绪');
|
|
114
|
+
log.info('[clawmessenger] 功能: 融云 IM 桥接、OpenClaw 对话、流式消息、媒体消息');
|
|
115
|
+
log.info('[clawmessenger] 使用说明:');
|
|
116
|
+
log.info(' - 私聊: 直接发送消息给节点');
|
|
117
|
+
log.info(' - 群聊: @节点名称 发送消息');
|
|
118
|
+
log.info(' - 流式消息: 自动启用,支持打字机效果');
|
|
119
|
+
log.info(' - 媒体消息: 支持图片/文件收发');
|
|
120
|
+
break;
|
|
121
|
+
case 'config':
|
|
122
|
+
await showConfig();
|
|
123
|
+
break;
|
|
124
|
+
case 'test-connection':
|
|
125
|
+
await testConnection();
|
|
126
|
+
break;
|
|
127
|
+
case 'logs':
|
|
128
|
+
await showLogs();
|
|
129
|
+
break;
|
|
130
|
+
case 'version':
|
|
131
|
+
log.info(`[clawmessenger] 版本: ${readPackageVersion()}`);
|
|
132
|
+
break;
|
|
133
|
+
case 'help':
|
|
134
|
+
default:
|
|
135
|
+
log.info(`
|
|
136
|
+
用法:
|
|
137
|
+
clawmessenger status 查看插件状态和功能说明
|
|
138
|
+
clawmessenger config 显示当前节点配置
|
|
139
|
+
clawmessenger test-connection 测试融云 IM 连接
|
|
140
|
+
clawmessenger logs 列出最近日志文件
|
|
141
|
+
clawmessenger version 显示插件版本
|
|
142
|
+
clawmessenger help 显示此帮助信息
|
|
143
|
+
|
|
144
|
+
核心功能:
|
|
145
|
+
- 节点注册: 安装时自动向虾说后端注册节点
|
|
146
|
+
- 融云连接: 自动连接融云 IM,接收消息
|
|
147
|
+
- OpenClaw 对话: 将消息转发给 OpenClaw 并返回回复
|
|
148
|
+
- 流式消息: 支持 SSE 流式传输,实时显示回复
|
|
149
|
+
- 媒体消息: 支持图片/文件收发
|
|
150
|
+
- 群聊管理: 支持群聊 @提及、对话轮数限制
|
|
151
|
+
|
|
152
|
+
配置:
|
|
153
|
+
- 节点配置: ~/.claw-bridge/openclaw/config.json
|
|
154
|
+
- OpenClaw 配置: ~/.openclaw/openclaw.json
|
|
155
|
+
- 日志目录: ~/.clawmessenger/logs
|
|
156
|
+
`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
main().catch((err) => {
|
|
161
|
+
log.error('[clawmessenger] 错误:', err.message);
|
|
162
|
+
console.error('[clawmessenger] 错误:', err instanceof Error ? err.message : String(err));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface DeviceRunningStatus {
|
|
2
|
+
openclaw_status: number;
|
|
3
|
+
opencode_status: number;
|
|
4
|
+
status_message: string;
|
|
5
|
+
mac_address: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 快速检测本地端口是否监听
|
|
10
|
+
* @param port 端口号
|
|
11
|
+
* @param host 主机地址,默认 127.0.0.1
|
|
12
|
+
* @param timeoutMs 超时时间,默认 2000ms
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkPort(port: number, host?: string, timeoutMs?: number): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* 获取设备上 OpenClaw / OpenCode 的运行状态
|
|
17
|
+
* 通过检查本地端口 18789(OpenClaw gateway)和 4096(OpenCode)实现
|
|
18
|
+
*/
|
|
19
|
+
export declare function getDeviceRunningStatus(openclawPort?: number, opencodePort?: number): Promise<DeviceRunningStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* 判断消息是否为设备状态请求
|
|
22
|
+
*/
|
|
23
|
+
export declare function isDeviceStatusRequest(msg: any): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 从状态请求消息中提取 request_id 和来源用户ID
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseDeviceStatusRequest(msg: any): {
|
|
28
|
+
requestId: string;
|
|
29
|
+
sourceId: string;
|
|
30
|
+
} | null;
|