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.
Files changed (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. 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
+ }