dlinkclaw-openclaw 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 +201 -0
- package/README.en.md +46 -0
- package/README.md +15 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -0
- package/dist/package.json +95 -0
- package/dist/src/account.d.ts +54 -0
- package/dist/src/account.js +394 -0
- package/dist/src/channel.d.ts +17 -0
- package/dist/src/channel.js +153 -0
- package/dist/src/client/client-manager.d.ts +4 -0
- package/dist/src/client/client-manager.js +10 -0
- package/dist/src/client/const.d.ts +37 -0
- package/dist/src/client/const.js +155 -0
- package/dist/src/client/crypto.d.ts +47 -0
- package/dist/src/client/crypto.js +260 -0
- package/dist/src/client/dl-client.d.ts +117 -0
- package/dist/src/client/dl-client.js +588 -0
- package/dist/src/client/dl-http.d.ts +27 -0
- package/dist/src/client/dl-http.js +223 -0
- package/dist/src/client/media-handler.d.ts +21 -0
- package/dist/src/client/media-handler.js +196 -0
- package/dist/src/client/message.d.ts +96 -0
- package/dist/src/client/message.js +1211 -0
- package/dist/src/client/timeout.d.ts +20 -0
- package/dist/src/client/timeout.js +36 -0
- package/dist/src/client/types.d.ts +226 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +18 -0
- package/dist/src/monitor.js +322 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +78 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +216 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/gateway-rpc.d.ts +37 -0
- package/dist/src/services/gateway-rpc.js +37 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +245 -0
- package/dist/src/services/services.d.ts +169 -0
- package/dist/src/services/services.js +1160 -0
- package/dist/src/services/types.d.ts +391 -0
- package/dist/src/services/types.js +5 -0
- package/dist/src/services/ws-client.d.ts +186 -0
- package/dist/src/services/ws-client.js +896 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +128 -0
- package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
- package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
- package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
- package/ext_capabilities/workrelay-speak/README.md +79 -0
- package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-speak/install.sh +329 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +111 -0
- package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
- package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
- package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
- package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
- package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
- package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
- package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-transcribe/README.md +76 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
- package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
- package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
- package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
- package/openclaw.plugin.json +142 -0
- package/package.json +95 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/** 仅包含图片时的消息占位符 */
|
|
2
|
+
export const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
|
|
3
|
+
export const MEDIA_VOICE_PLACEHOLDER = "<media:audio>";
|
|
4
|
+
export const MEDIA_VIDEO_PLACEHOLDER = "<media:video>";
|
|
5
|
+
/** 仅包含文件时的消息占位符 */
|
|
6
|
+
export const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
|
|
7
|
+
/** 默认媒体大小上限(MB)——插件↔服务端链路(下载/上传)的兜底限制 */
|
|
8
|
+
export const DEFAULT_MEDIA_MAX_MB = 50;
|
|
9
|
+
/**
|
|
10
|
+
* 2026-08-23 安装时写入 openclaw.json 的默认媒体上限(MB)
|
|
11
|
+
* 注意:OpenClaw 核心出站媒体校验读取的是 channels.<id>.mediaMaxMb(默认仅 5MB),
|
|
12
|
+
* 与 DEFAULT_MEDIA_MAX_MB(插件↔服务端)无关。安装向导写入本值后,核心出站即放行。
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_INSTALL_MEDIA_MAX_MB = 100;
|
|
15
|
+
/** 文本分块大小上限 */
|
|
16
|
+
export const TEXT_CHUNK_LIMIT = 4000;
|
|
17
|
+
/** 图片下载超时时间(毫秒) */
|
|
18
|
+
export const IMAGE_DOWNLOAD_TIMEOUT_MS = 60_000 * 2;
|
|
19
|
+
/** 文件下载超时时间(毫秒) */
|
|
20
|
+
export const FILE_DOWNLOAD_TIMEOUT_MS = 60_000 * 5;
|
|
21
|
+
/** 消息发送超时时间(毫秒) */
|
|
22
|
+
export const REPLY_SEND_TIMEOUT_MS = 15_000;
|
|
23
|
+
/**
|
|
24
|
+
* 主动回复API
|
|
25
|
+
*/
|
|
26
|
+
export const AUTO_RELAY_API = '/guest/relayBot/sendNotification';
|
|
27
|
+
export const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
28
|
+
// ====== 上传文件配置常量(根据服务器性能调整)=======
|
|
29
|
+
export const FILE_LIMITS = {
|
|
30
|
+
image: { maxSize: 10 * 1024 * 1024 }, // 图片 10MB
|
|
31
|
+
attachment: { maxSize: 50 * 1024 * 1024 }, // 附件 50MB
|
|
32
|
+
};
|
|
33
|
+
const IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'];
|
|
34
|
+
export const IMAGE_EXTS = new Set(IMAGE_TYPES);
|
|
35
|
+
//export const ATTACHMENT_EXTS = new Set([
|
|
36
|
+
// // ====== 图片 ======
|
|
37
|
+
// ...IMAGE_TYPES,
|
|
38
|
+
// // ====== Office 文档 ======
|
|
39
|
+
// '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
40
|
+
// // ====== 文本/代码(AI 最常处理的) ======
|
|
41
|
+
// '.txt', '.csv', '.json', '.xml', '.yaml', '.yml', '.md', '.log',
|
|
42
|
+
// // ====== PDF ======
|
|
43
|
+
// '.pdf',
|
|
44
|
+
// // ====== 压缩包 ======
|
|
45
|
+
// '.zip', '.rar', '.7z', '.tar', '.gz',
|
|
46
|
+
// // ====== 代码源文件 ======
|
|
47
|
+
// '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs',
|
|
48
|
+
// '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.swift',
|
|
49
|
+
// '.kt', '.sh', '.bat', '.ps1', '.sql', '.css', '.scss', '.less',
|
|
50
|
+
// '.html', '.vue', '.svelte',
|
|
51
|
+
// // ====== 配置文件 ======
|
|
52
|
+
// '.env', '.toml', '.ini', '.cfg',
|
|
53
|
+
// // ====== 电子书/文档 ======
|
|
54
|
+
// '.epub', '.mobi',
|
|
55
|
+
// // ====== 音频 ======
|
|
56
|
+
// '.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac', '.wma', '.opus',
|
|
57
|
+
// // ====== 视频 ======
|
|
58
|
+
// '.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm', '.m4v', '.mpg', '.mpeg', '.3gp'
|
|
59
|
+
//
|
|
60
|
+
//])
|
|
61
|
+
// export const ATTACHMENT_TYPE: Record<string, string> = {
|
|
62
|
+
// // ====== 图片类型 ======
|
|
63
|
+
// 'image/jpeg': '.jpg',
|
|
64
|
+
// 'image/jpg': '.jpg',
|
|
65
|
+
// 'image/png': '.png',
|
|
66
|
+
// 'image/gif': '.gif',
|
|
67
|
+
// 'image/webp': '.webp',
|
|
68
|
+
// 'image/bmp': '.bmp',
|
|
69
|
+
// 'image/svg+xml': '.svg',
|
|
70
|
+
// 'image/tiff': '.tiff',
|
|
71
|
+
// // ====== Office 文档 ======
|
|
72
|
+
// 'application/msword': '.doc',
|
|
73
|
+
// 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
|
|
74
|
+
// 'application/vnd.ms-excel': '.xls',
|
|
75
|
+
// 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
|
76
|
+
// 'application/vnd.ms-powerpoint': '.ppt',
|
|
77
|
+
// 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
|
|
78
|
+
// // ====== 文本/代码 ======
|
|
79
|
+
// 'text/plain': '.txt',
|
|
80
|
+
// 'text/csv': '.csv',
|
|
81
|
+
// 'application/json': '.json',
|
|
82
|
+
// 'application/xml': '.xml',
|
|
83
|
+
// 'text/xml': '.xml',
|
|
84
|
+
// 'text/yaml': '.yaml',
|
|
85
|
+
// 'application/x-yaml': '.yml',
|
|
86
|
+
// 'text/markdown': '.md',
|
|
87
|
+
// 'text/x-log': '.log',
|
|
88
|
+
// // ====== PDF ======
|
|
89
|
+
// 'application/pdf': '.pdf',
|
|
90
|
+
// // ====== 压缩包 ======
|
|
91
|
+
// 'application/zip': '.zip',
|
|
92
|
+
// 'application/x-rar-compressed': '.rar',
|
|
93
|
+
// 'application/x-7z-compressed': '.7z',
|
|
94
|
+
// 'application/x-tar': '.tar',
|
|
95
|
+
// 'application/gzip': '.gz',
|
|
96
|
+
// // ====== 代码源文件 ======
|
|
97
|
+
// 'application/javascript': '.js',
|
|
98
|
+
// 'text/javascript': '.js',
|
|
99
|
+
// 'application/typescript': '.ts',
|
|
100
|
+
// 'text/typescript': '.ts',
|
|
101
|
+
// 'text/jsx': '.jsx',
|
|
102
|
+
// 'text/tsx': '.tsx',
|
|
103
|
+
// 'text/x-python': '.py',
|
|
104
|
+
// 'text/x-java': '.java',
|
|
105
|
+
// 'text/x-go': '.go',
|
|
106
|
+
// 'text/x-rust': '.rs',
|
|
107
|
+
// 'text/x-c': '.c',
|
|
108
|
+
// 'text/x-c++': '.cpp',
|
|
109
|
+
// 'text/x-csharp': '.cs',
|
|
110
|
+
// 'text/x-php': '.php',
|
|
111
|
+
// 'text/x-ruby': '.rb',
|
|
112
|
+
// 'text/x-swift': '.swift',
|
|
113
|
+
// 'text/x-kotlin': '.kt',
|
|
114
|
+
// 'text/x-shellscript': '.sh',
|
|
115
|
+
// 'application/x-bat': '.bat',
|
|
116
|
+
// 'text/x-powershell': '.ps1',
|
|
117
|
+
// 'text/x-sql': '.sql',
|
|
118
|
+
// 'text/css': '.css',
|
|
119
|
+
// 'text/scss': '.scss',
|
|
120
|
+
// 'text/less': '.less',
|
|
121
|
+
// 'text/html': '.html',
|
|
122
|
+
// 'application/vue': '.vue',
|
|
123
|
+
// // ====== 配置文件 ======
|
|
124
|
+
// 'text/x-env': '.env',
|
|
125
|
+
// 'application/toml': '.toml',
|
|
126
|
+
// 'text/ini': '.ini',
|
|
127
|
+
// 'text/x-cfg': '.cfg',
|
|
128
|
+
// // ====== 电子书/文档 ======
|
|
129
|
+
// 'application/epub+zip': '.epub',
|
|
130
|
+
// 'application/x-mobipocket-ebook': '.mobi',
|
|
131
|
+
// // ====== 音频 ======
|
|
132
|
+
// 'audio/mpeg': '.mp3',
|
|
133
|
+
// 'audio/mp3': '.mp3',
|
|
134
|
+
// 'audio/wav': '.wav',
|
|
135
|
+
// 'audio/x-wav': '.wav',
|
|
136
|
+
// 'audio/ogg': '.ogg',
|
|
137
|
+
// 'audio/opus': '.opus',
|
|
138
|
+
// 'audio/mp4': '.m4a',
|
|
139
|
+
// 'audio/x-m4a': '.m4a',
|
|
140
|
+
// 'audio/flac': '.flac',
|
|
141
|
+
// 'audio/x-flac': '.flac',
|
|
142
|
+
// 'audio/aac': '.aac',
|
|
143
|
+
// 'audio/x-ms-wma': '.wma',
|
|
144
|
+
// // ====== 视频 ======
|
|
145
|
+
// 'video/mp4': '.mp4',
|
|
146
|
+
// 'video/mpeg': '.mpeg',
|
|
147
|
+
// 'video/x-msvideo': '.avi',
|
|
148
|
+
// 'video/quicktime': '.mov',
|
|
149
|
+
// 'video/x-ms-wmv': '.wmv',
|
|
150
|
+
// 'video/x-flv': '.flv',
|
|
151
|
+
// 'video/x-matroska': '.mkv',
|
|
152
|
+
// 'video/webm': '.webm',
|
|
153
|
+
// 'video/x-m4v': '.m4v',
|
|
154
|
+
// 'video/3gpp': '.3gp'
|
|
155
|
+
// };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MqttConfig } from "./dl-client.js";
|
|
2
|
+
export type DeviceConfig = {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
publicKey: string;
|
|
5
|
+
publicKeyPem: string;
|
|
6
|
+
privateKeyPem: string;
|
|
7
|
+
deviceToken?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function reqBootstrapToken(gatewayUrl: string): Promise<string>;
|
|
10
|
+
export declare function getDeviceId(): string;
|
|
11
|
+
export declare function getDeviceConfig(): DeviceConfig;
|
|
12
|
+
export declare function saveDeviceConfig(config: DeviceConfig): void;
|
|
13
|
+
export declare function decryptAesEcbPkcs5(encryptedData: string, key: string): MqttConfig;
|
|
14
|
+
export declare function signConnectPayload(params: {
|
|
15
|
+
deviceId: string;
|
|
16
|
+
privateKeyPem: string;
|
|
17
|
+
token: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientMode: string;
|
|
20
|
+
role: string;
|
|
21
|
+
scopes: Array<string>;
|
|
22
|
+
platform: string;
|
|
23
|
+
nonce: string;
|
|
24
|
+
deviceFamily?: string;
|
|
25
|
+
}): {
|
|
26
|
+
signedAtMs: number;
|
|
27
|
+
signature: string;
|
|
28
|
+
payload: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function generateSignature(params: Record<string, string>, secret: string): string;
|
|
31
|
+
export declare function md5(str: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* 使用 AES-256-CBC 解密文件
|
|
34
|
+
*
|
|
35
|
+
* @param encryptedBuffer - 加密的文件数据
|
|
36
|
+
* @param aesKey - Base64 编码的 AES-256 密钥
|
|
37
|
+
* @returns 解密后的文件 Buffer
|
|
38
|
+
*/
|
|
39
|
+
export declare function ewxDecryptFile(encryptedBuffer: Buffer, aesKey: string): Buffer;
|
|
40
|
+
/**
|
|
41
|
+
* 使用 AES-256-CBC 解密文件
|
|
42
|
+
*
|
|
43
|
+
* @param encryptedBuffer - 加密的文件数据
|
|
44
|
+
* @param aesKey - Base64 编码的 AES-256 密钥
|
|
45
|
+
* @returns 解密后的文件 Buffer
|
|
46
|
+
*/
|
|
47
|
+
export declare function dlinkDecryptFile(encryptedBuffer: Buffer, aesKey: string): Buffer;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import crypto, { createSecretKey } from "crypto";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
const DEVICE_FILE = '.dlinkclaw';
|
|
7
|
+
function base64url(buf) {
|
|
8
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
9
|
+
}
|
|
10
|
+
// function generateDeviceId(): string {
|
|
11
|
+
// const interfaces = os.networkInterfaces();
|
|
12
|
+
// for (const name of Object.keys(interfaces)) {
|
|
13
|
+
// const iface = interfaces[name];
|
|
14
|
+
// if (!iface) continue;
|
|
15
|
+
// for (const info of iface) {
|
|
16
|
+
// if (info.mac && info.mac !== "00:00:00:00:00:00") {
|
|
17
|
+
// return info.mac.replace(/:/g, "").toUpperCase();
|
|
18
|
+
// }
|
|
19
|
+
// }
|
|
20
|
+
// }
|
|
21
|
+
// return crypto.randomUUID().replace(/-/g, "").toUpperCase();
|
|
22
|
+
// }
|
|
23
|
+
function generateDeviceIdentity() {
|
|
24
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
25
|
+
const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
26
|
+
const privPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
27
|
+
// 提取 raw public key → deviceId = sha256(公钥)
|
|
28
|
+
const spki = crypto.createPublicKey(pubPem).export({ type: 'spki', format: 'der' });
|
|
29
|
+
const ED25519_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
30
|
+
const rawPub = spki.subarray(ED25519_PREFIX.length);
|
|
31
|
+
const deviceId = crypto.createHash('sha256').update(rawPub).digest('hex');
|
|
32
|
+
const pubKeyB64Url = base64url(rawPub);
|
|
33
|
+
return { deviceId, publicKey: pubKeyB64Url, privateKeyPem: privPem, publicKeyPem: pubPem };
|
|
34
|
+
}
|
|
35
|
+
export async function reqBootstrapToken(gatewayUrl) {
|
|
36
|
+
const safeUrl = gatewayUrl.replace(/[^a-zA-Z0-9:/._-]/g, '');
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
exec(`openclaw qr --json --url ${safeUrl}`, { timeout: 10000, encoding: 'utf-8' }, (err, stdout) => {
|
|
39
|
+
if (err)
|
|
40
|
+
return reject(new Error(`获取失败: ${err.message}`));
|
|
41
|
+
try {
|
|
42
|
+
const result = JSON.parse(stdout);
|
|
43
|
+
const decoded = JSON.parse(Buffer.from(result.setupCode, 'base64').toString());
|
|
44
|
+
resolve(decoded.bootstrapToken);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
reject(new Error(`解析失败: ${String(e)}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function getDeviceId() {
|
|
53
|
+
const userDir = os.homedir();
|
|
54
|
+
const filePath = path.join(userDir, DEVICE_FILE);
|
|
55
|
+
try {
|
|
56
|
+
const fileData = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
const result = JSON.parse(fileData);
|
|
58
|
+
return result.deviceId;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
const { deviceId, publicKey, publicKeyPem, privateKeyPem } = generateDeviceIdentity();
|
|
62
|
+
const model = {
|
|
63
|
+
deviceId: deviceId,
|
|
64
|
+
publicKey: publicKey,
|
|
65
|
+
publicKeyPem: publicKeyPem,
|
|
66
|
+
privateKeyPem: privateKeyPem
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const userDir = os.homedir();
|
|
70
|
+
const filePath = path.join(userDir, DEVICE_FILE);
|
|
71
|
+
fs.writeFileSync(filePath, JSON.stringify(model), 'utf-8');
|
|
72
|
+
return deviceId;
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
throw new Error(`Write file error ${String(e)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function getDeviceConfig() {
|
|
80
|
+
const userDir = os.homedir();
|
|
81
|
+
const filePath = path.join(userDir, DEVICE_FILE);
|
|
82
|
+
try {
|
|
83
|
+
const fileData = fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
const result = JSON.parse(fileData);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
throw new Error(String(e));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function saveDeviceConfig(config) {
|
|
92
|
+
try {
|
|
93
|
+
const userDir = os.homedir();
|
|
94
|
+
const filePath = path.join(userDir, DEVICE_FILE);
|
|
95
|
+
fs.writeFile(filePath, JSON.stringify(config), 'utf-8', (err) => {
|
|
96
|
+
if (err) {
|
|
97
|
+
console.error(`写DeviceConfig文件[${filePath}],失败${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error(`写DeviceConfig文件失败${String(e)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function decryptAesEcbPkcs5(encryptedData, key) {
|
|
106
|
+
const keyBytes = Buffer.from(key, "base64");
|
|
107
|
+
const keyLength = keyBytes.length;
|
|
108
|
+
let algorithm;
|
|
109
|
+
if (keyLength === 16) {
|
|
110
|
+
algorithm = "aes-128-ecb";
|
|
111
|
+
}
|
|
112
|
+
else if (keyLength === 24) {
|
|
113
|
+
algorithm = "aes-192-ecb";
|
|
114
|
+
}
|
|
115
|
+
else if (keyLength === 32) {
|
|
116
|
+
algorithm = "aes-256-ecb";
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`Invalid key length: ${keyLength}`);
|
|
120
|
+
}
|
|
121
|
+
const keySpec = crypto.createSecretKey(keyBytes);
|
|
122
|
+
const decipher = crypto.createDecipheriv(algorithm, keySpec, null);
|
|
123
|
+
decipher.setAutoPadding(true);
|
|
124
|
+
let decrypted = decipher.update(encryptedData, "base64", "utf8");
|
|
125
|
+
decrypted += decipher.final("utf8");
|
|
126
|
+
return JSON.parse(decrypted);
|
|
127
|
+
}
|
|
128
|
+
export function signConnectPayload(params) {
|
|
129
|
+
const { deviceId, clientId, clientMode, role, scopes, token, platform, deviceFamily, privateKeyPem, nonce } = params;
|
|
130
|
+
const signedAtMs = Date.now();
|
|
131
|
+
const payload = [
|
|
132
|
+
'v3',
|
|
133
|
+
deviceId,
|
|
134
|
+
clientId,
|
|
135
|
+
clientMode,
|
|
136
|
+
role,
|
|
137
|
+
scopes.join(','),
|
|
138
|
+
String(signedAtMs),
|
|
139
|
+
token,
|
|
140
|
+
nonce,
|
|
141
|
+
platform,
|
|
142
|
+
deviceFamily ?? ''
|
|
143
|
+
].join('|');
|
|
144
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
145
|
+
const signature = base64url(crypto.sign(null, Buffer.from(payload, 'utf8'), key));
|
|
146
|
+
return { signedAtMs, signature, payload };
|
|
147
|
+
}
|
|
148
|
+
export function generateSignature(params, secret) {
|
|
149
|
+
const sortedKeys = Object.keys(params).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase(), undefined, {
|
|
150
|
+
numeric: true,
|
|
151
|
+
sensitivity: 'base'
|
|
152
|
+
}));
|
|
153
|
+
const signString = sortedKeys
|
|
154
|
+
.map(key => `${key}=${params[key]}`)
|
|
155
|
+
.join("&") + `&key=${secret}`;
|
|
156
|
+
const signStr = signString.toUpperCase();
|
|
157
|
+
//console.log("签名字符串 >>> ", signStr);
|
|
158
|
+
return crypto.createHash("md5").update(signStr).digest("hex");
|
|
159
|
+
}
|
|
160
|
+
export function md5(str) {
|
|
161
|
+
return crypto.createHash("md5").update(str).digest("hex");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 使用 AES-256-CBC 解密文件
|
|
165
|
+
*
|
|
166
|
+
* @param encryptedBuffer - 加密的文件数据
|
|
167
|
+
* @param aesKey - Base64 编码的 AES-256 密钥
|
|
168
|
+
* @returns 解密后的文件 Buffer
|
|
169
|
+
*/
|
|
170
|
+
export function ewxDecryptFile(encryptedBuffer, aesKey) {
|
|
171
|
+
// 参数验证
|
|
172
|
+
if (!encryptedBuffer || encryptedBuffer.length === 0) {
|
|
173
|
+
throw new Error('decryptFile: encryptedBuffer is empty or not provided');
|
|
174
|
+
}
|
|
175
|
+
if (!aesKey || typeof aesKey !== 'string') {
|
|
176
|
+
throw new Error('decryptFile: aesKey must be a non-empty string');
|
|
177
|
+
}
|
|
178
|
+
// 将 Base64 编码的 aesKey 解码为 Buffer
|
|
179
|
+
const key = Buffer.from(aesKey, 'base64');
|
|
180
|
+
// IV 取 aesKey 解码后的前 16 字节
|
|
181
|
+
const iv = key.subarray(0, 16);
|
|
182
|
+
try {
|
|
183
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
184
|
+
// 关闭自动 padding,因为文档要求 PKCS#7 填充至 32 字节的倍数,
|
|
185
|
+
// 而 Node.js 默认按 16 字节 block 去除 padding,会导致 bad decrypt 错误
|
|
186
|
+
decipher.setAutoPadding(false);
|
|
187
|
+
const decrypted = Buffer.concat([
|
|
188
|
+
decipher.update(encryptedBuffer),
|
|
189
|
+
decipher.final(),
|
|
190
|
+
]);
|
|
191
|
+
// 手动去除 PKCS#7 填充(支持 32 字节 block)
|
|
192
|
+
const padLen = decrypted[decrypted.length - 1];
|
|
193
|
+
if (padLen < 1 || padLen > 32 || padLen > decrypted.length) {
|
|
194
|
+
throw new Error(`Invalid PKCS#7 padding value: ${padLen}`);
|
|
195
|
+
}
|
|
196
|
+
// 验证所有 padding 字节是否一致
|
|
197
|
+
for (let i = decrypted.length - padLen; i < decrypted.length; i++) {
|
|
198
|
+
if (decrypted[i] !== padLen) {
|
|
199
|
+
throw new Error('Invalid PKCS#7 padding: padding bytes mismatch');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return decrypted.subarray(0, decrypted.length - padLen);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw new Error(`decryptFile: Decryption failed - ${error.message}. This may indicate corrupted data or an incorrect aesKey.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 使用 AES-256-CBC 解密文件
|
|
210
|
+
*
|
|
211
|
+
* @param encryptedBuffer - 加密的文件数据
|
|
212
|
+
* @param aesKey - Base64 编码的 AES-256 密钥
|
|
213
|
+
* @returns 解密后的文件 Buffer
|
|
214
|
+
*/
|
|
215
|
+
export function dlinkDecryptFile(encryptedBuffer, aesKey) {
|
|
216
|
+
// 参数验证
|
|
217
|
+
if (!encryptedBuffer || encryptedBuffer.length === 0) {
|
|
218
|
+
throw new Error('dlinkDecryptFile: encryptedBuffer is empty or not provided');
|
|
219
|
+
}
|
|
220
|
+
if (!aesKey || typeof aesKey !== 'string') {
|
|
221
|
+
throw new Error('dlinkDecryptFile: aesKey must be a non-empty string');
|
|
222
|
+
}
|
|
223
|
+
// 将 Base64 编码的 aesKey 解码为 Buffer
|
|
224
|
+
const key = Buffer.from(aesKey, 'base64');
|
|
225
|
+
// IV 取 aesKey 解码后的前 16 字节
|
|
226
|
+
//const iv = key.subarray(0, 16);
|
|
227
|
+
const iv = encryptedBuffer.subarray(0, 16);
|
|
228
|
+
const encryptedData = encryptedBuffer.subarray(16);
|
|
229
|
+
try {
|
|
230
|
+
const keyObj = createSecretKey(new Uint8Array(key));
|
|
231
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', keyObj, new Uint8Array(iv));
|
|
232
|
+
// 关闭自动 padding,因为文档要求 PKCS#7 填充至 32 字节的倍数,
|
|
233
|
+
// 而 Node.js 默认按 16 字节 block 去除 padding,会导致 bad decrypt 错误
|
|
234
|
+
decipher.setAutoPadding(false);
|
|
235
|
+
// const decrypted = Buffer.concat([
|
|
236
|
+
// decipher.update(encryptedBuffer),
|
|
237
|
+
// decipher.final(),
|
|
238
|
+
// ]);
|
|
239
|
+
const a = decipher.update(new Uint8Array(encryptedData));
|
|
240
|
+
const decrypted = Buffer.concat([
|
|
241
|
+
new Uint8Array(a),
|
|
242
|
+
new Uint8Array(decipher.final()),
|
|
243
|
+
]);
|
|
244
|
+
// 手动去除 PKCS#7 填充(支持 32 字节 block)
|
|
245
|
+
const padLen = decrypted[decrypted.length - 1];
|
|
246
|
+
if (padLen < 1 || padLen > 32 || padLen > decrypted.length) {
|
|
247
|
+
throw new Error(`dlinkDecryptFile Invalid PKCS#7 padding value: ${padLen}`);
|
|
248
|
+
}
|
|
249
|
+
// 验证所有 padding 字节是否一致
|
|
250
|
+
for (let i = decrypted.length - padLen; i < decrypted.length; i++) {
|
|
251
|
+
if (decrypted[i] !== padLen) {
|
|
252
|
+
throw new Error('dlinkDecryptFile Invalid PKCS#7 padding: padding bytes mismatch');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return decrypted.subarray(0, decrypted.length - padLen);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
throw new Error(`dlinkDecryptFile: Decryption failed - ${error.message}. This may indicate corrupted data or an incorrect aesKey.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { OpenClawConfig, PluginLogger } from 'openclaw/plugin-sdk/core';
|
|
3
|
+
import { AccumulatorsStatus, AutoReplyOpts } from './message.js';
|
|
4
|
+
import { BotType, DLArchiveSession, DLKind, DLMessagePayload, DLResolvedAccount, MessageStatus } from './types.js';
|
|
5
|
+
import { CmdMessage, CmdResponse, EventType } from '../services/types.js';
|
|
6
|
+
export type DLContext = {
|
|
7
|
+
account: DLResolvedAccount;
|
|
8
|
+
channelId: string;
|
|
9
|
+
cfg: OpenClawConfig;
|
|
10
|
+
rbotId?: string;
|
|
11
|
+
};
|
|
12
|
+
export interface DLMessageContext {
|
|
13
|
+
ctx: DLContext;
|
|
14
|
+
msg: DLMessagePayload;
|
|
15
|
+
accStatus: AccumulatorsStatus;
|
|
16
|
+
}
|
|
17
|
+
export type DLReceiveMessageHandler = (topic: string, msg: DLMessagePayload) => void;
|
|
18
|
+
export interface MqttConfig {
|
|
19
|
+
url: string;
|
|
20
|
+
port: number;
|
|
21
|
+
websocketPort: number;
|
|
22
|
+
serverId: string;
|
|
23
|
+
qos: 0 | 1 | 2;
|
|
24
|
+
userName: string;
|
|
25
|
+
password: string;
|
|
26
|
+
willTopic: string;
|
|
27
|
+
serverTopics: Array<string>;
|
|
28
|
+
clientTopics: Array<string>;
|
|
29
|
+
openUserTopics: Array<string>;
|
|
30
|
+
openUserNotifyTopics: Array<string>;
|
|
31
|
+
}
|
|
32
|
+
export declare class DLClient extends EventEmitter {
|
|
33
|
+
private ctx;
|
|
34
|
+
private mqttConfig;
|
|
35
|
+
private mqttClient;
|
|
36
|
+
private cmdHandler;
|
|
37
|
+
private readonly http;
|
|
38
|
+
private readonly logger;
|
|
39
|
+
private readonly accountLog;
|
|
40
|
+
private lastInboundAt;
|
|
41
|
+
constructor(ctx: DLContext, logger: PluginLogger);
|
|
42
|
+
/**
|
|
43
|
+
* 2026-09-16 修复(石头哥报:加同事后 model.list / session.list 全空):
|
|
44
|
+
* cmdHandler 原来只在构造函数里创建一次,任何把它置空的路径(stop / 刷新配置)
|
|
45
|
+
* 都会导致「此后永远为 null」→ 所有 cmd 请求失败且不可自愈(只能重启 openclaw 网关)。
|
|
46
|
+
* 抽成幂等方法后,start() 会兜底重建。
|
|
47
|
+
*/
|
|
48
|
+
private initCmdHandler;
|
|
49
|
+
private fetchConfig;
|
|
50
|
+
private changeConnectStatus;
|
|
51
|
+
private connect;
|
|
52
|
+
private receiveEventHandle;
|
|
53
|
+
private closeHandler;
|
|
54
|
+
private publish;
|
|
55
|
+
getLastInboundAt(): number | null;
|
|
56
|
+
isConnected(): boolean;
|
|
57
|
+
start(): Promise<void>;
|
|
58
|
+
stop(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* 2026-09-15 方案A(石头哥确认):重新拉取 mqtt 配置(含 openUserTopics)。
|
|
61
|
+
* 触发时机:服务端新增/移除授权成员后,会往 bot 的 cmd topic 发 openUserChanged 信号。
|
|
62
|
+
*
|
|
63
|
+
* 2026-09-16 修复(石头哥报:加同事后 model.list / session.list 全部为空):
|
|
64
|
+
* 原实现是「断连 + 置空 cmdHandler + 重连」,有致命副作用——cmdHandler 只在构造函数
|
|
65
|
+
* 里创建一次,置空后 start() 不会重建 → 所有 cmd RPC 永久失效,只能重启 openclaw 网关恢复。
|
|
66
|
+
* 而本方法的目的只是刷新 openUserTopics——它是 publishEventToDLClient(L348)实时读取的,
|
|
67
|
+
* 与 MQTT 订阅目标(clientTopics)、Gateway WS 通道都无关 → 只刷新配置即可,不断连、不重连。
|
|
68
|
+
*/
|
|
69
|
+
refreshConfigAndReconnect(): Promise<void>;
|
|
70
|
+
exeCmd(cmdMessage: CmdMessage): Promise<CmdResponse | undefined>;
|
|
71
|
+
publishEventToDLClient(payload: any, event: EventType, sqos?: '0' | '1' | '2', to?: string): Promise<void>;
|
|
72
|
+
publishCmdResultToDLClient(topic: string, playload: CmdResponse, sqos?: '0' | '1' | '2'): Promise<void>;
|
|
73
|
+
publishEventToServer(playload: any, event: EventType, sqos?: '0' | '1' | '2'): Promise<void>;
|
|
74
|
+
publishCmdResultToServer(playload: CmdResponse, sqos?: '0' | '1' | '2'): Promise<void>;
|
|
75
|
+
publishToServer(params: {
|
|
76
|
+
streamId: string;
|
|
77
|
+
content: string;
|
|
78
|
+
finish: boolean;
|
|
79
|
+
kind: DLKind;
|
|
80
|
+
mediaFiles?: Array<string>;
|
|
81
|
+
session?: DLArchiveSession;
|
|
82
|
+
}, autoParams?: AutoReplyOpts): Promise<{
|
|
83
|
+
autoRelay: boolean;
|
|
84
|
+
reqId: string | undefined;
|
|
85
|
+
} | {
|
|
86
|
+
autoRelay: boolean;
|
|
87
|
+
reqId?: undefined;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* 减轻服务端负担,将处理状态直接推送到客户端(处理结果走服务端)
|
|
91
|
+
* @param params
|
|
92
|
+
*/
|
|
93
|
+
publishProcessingState(params: {
|
|
94
|
+
streamId: string;
|
|
95
|
+
content: string;
|
|
96
|
+
status: MessageStatus;
|
|
97
|
+
runId?: string;
|
|
98
|
+
kind: DLKind;
|
|
99
|
+
}): Promise<void>;
|
|
100
|
+
downloadFileRaw(url: string, botType: BotType): Promise<{
|
|
101
|
+
buffer: Buffer;
|
|
102
|
+
filename?: string;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* 对码成功回调通知
|
|
106
|
+
*/
|
|
107
|
+
approved(ok: boolean): Promise<void>;
|
|
108
|
+
autoReply(params: {
|
|
109
|
+
to: string;
|
|
110
|
+
text?: string;
|
|
111
|
+
mediaFiles?: Array<string>;
|
|
112
|
+
}): Promise<{
|
|
113
|
+
channel: string;
|
|
114
|
+
messageId: string;
|
|
115
|
+
chatId: string;
|
|
116
|
+
}>;
|
|
117
|
+
}
|