dlinkclaw-openclaw 2026.8.24-alpha.1
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/README.en.md +46 -0
- package/README.md +25 -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 +53 -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 +99 -0
- package/dist/src/client/dl-client.js +535 -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 +1204 -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 +224 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +17 -0
- package/dist/src/monitor.js +320 -0
- package/dist/src/outbound.d.ts +4 -0
- package/dist/src/outbound.js +83 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +205 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +225 -0
- package/dist/src/services/services.d.ts +152 -0
- package/dist/src/services/services.js +1066 -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 +174 -0
- package/dist/src/services/ws-client.js +866 -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 +280 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +49 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +90 -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 +282 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +49 -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 +94 -0
- package/ext_capabilities/workrelay-speak-luxtts/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 +207 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +62 -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,153 @@
|
|
|
1
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
2
|
+
import { monitorDuolinkProvider } from "./monitor.js";
|
|
3
|
+
import { checkConfigured, deleteDLAccount, listDLConfiguredAccountIds, resolveDefaultDLAccountId, resolveDLAccount, setDLChannelAccountEnabled, dlSetupWizard, dlSetupAdapter, dlAccountStatus, dlDescribeAccount } from "./account.js";
|
|
4
|
+
import { formatPairingApproveHint } from "openclaw/plugin-sdk/core";
|
|
5
|
+
import { getDLLogger } from "./runtime.js";
|
|
6
|
+
import { dlOutbound, dlPairing } from "./outbound.js";
|
|
7
|
+
export const channelMeta = pkg.openclaw.channel;
|
|
8
|
+
export const LOG_TAG = channelMeta.selectionLabel;
|
|
9
|
+
const channelId = channelMeta.id;
|
|
10
|
+
export const dlChannel = {
|
|
11
|
+
id: channelId,
|
|
12
|
+
meta: {
|
|
13
|
+
...channelMeta,
|
|
14
|
+
},
|
|
15
|
+
configSchema: {
|
|
16
|
+
schema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
additionalProperties: true,
|
|
19
|
+
properties: {
|
|
20
|
+
enabled: { type: "boolean" },
|
|
21
|
+
apiKey: { type: "string" },
|
|
22
|
+
secret: { type: "string" },
|
|
23
|
+
baseUrl: { type: "string" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
capabilities: {
|
|
28
|
+
chatTypes: ['direct', 'group'],
|
|
29
|
+
reactions: true,
|
|
30
|
+
polls: false,
|
|
31
|
+
edit: false,
|
|
32
|
+
groupManagement: false,
|
|
33
|
+
threads: true,
|
|
34
|
+
reply: true,
|
|
35
|
+
media: true,
|
|
36
|
+
nativeCommands: true,
|
|
37
|
+
blockStreaming: true,
|
|
38
|
+
},
|
|
39
|
+
setupWizard: dlSetupWizard(channelId),
|
|
40
|
+
setup: dlSetupAdapter(channelId),
|
|
41
|
+
status: dlAccountStatus(channelId),
|
|
42
|
+
agentPrompt: {
|
|
43
|
+
messageToolHints: () => {
|
|
44
|
+
return [
|
|
45
|
+
// 2026-08-24 优化:target 格式补齐第 4 段 relayId(与 message.ts fromLabel 4 段格式保持一致,
|
|
46
|
+
// 否则 dl-client.ts autoReply 解析不到 relayId,出站消息无法路由到正确中继)
|
|
47
|
+
`- ${channelId} targeting: omit 'target' to reply to the current conversation (auto-inferred). Explicit targets: 'user:<userId>-<botId>-<botType>-<relayId>' or 'group:<chatId>-<botId>-<botType>-<relayId>'.`,
|
|
48
|
+
`- Example for user: user:I27484094711599104-I27484094749347841-{ewx|qq|dd|feishu|dLink}-<relayId> (botType varies by client).`,
|
|
49
|
+
`- Example for group: group:I27484094711599104-I27484094749347841-{ewx|qq|dd|feishu|dLink}-<relayId> (botType varies by client).`,
|
|
50
|
+
`- ${channelId} outbound with markdown text or media file upload can be realized.`,
|
|
51
|
+
`- **Outbound delivery**: All messages MUST route through ${channelId} channel. Never use webchat.`,
|
|
52
|
+
`- When creating cron jobs: always set delivery.channel: "${channelId}" and delivery.to: "${channelId}:group:<chatId>-<botId>-{dLink|ewx|qq|dd|feishu}-<relayId>" — botType depends on the client. Extract it from the current incoming message address. Omitting channel silently falls back to webchat.`,
|
|
53
|
+
`- When using the message tool: always channel: "${channelId}"`,
|
|
54
|
+
// 2026-08-24 新增:发送媒体/文件后必须补发一条文本确认,禁止静默(NO_REPLY)。
|
|
55
|
+
// 否则客户端 UI 一直显示"工作/转圈"状态(等不到完成信号),石头哥已明确要求。
|
|
56
|
+
`- **After sending media/files via the message tool, ALWAYS follow up with a short text confirmation (e.g. "✅ 已发出 3 个文件:a.png / b.pdf / c.txt"). Never reply NO_REPLY or stay silent after a media send — the client keeps showing a spinner/working state until it receives a final text reply.`,
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
reload: {
|
|
61
|
+
configPrefixes: [
|
|
62
|
+
`channels.${channelId}`
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
config: {
|
|
66
|
+
listAccountIds: (cfg) => listDLConfiguredAccountIds(cfg, channelId),
|
|
67
|
+
resolveAccount: (cfg, accountId) => resolveDLAccount({ cfg, accountId }, channelId),
|
|
68
|
+
defaultAccountId: (cfg) => resolveDefaultDLAccountId(cfg, channelId),
|
|
69
|
+
setAccountEnabled: (params) => setDLChannelAccountEnabled(params, channelId),
|
|
70
|
+
deleteAccount: (params) => deleteDLAccount(params, channelId),
|
|
71
|
+
isConfigured: (account) => {
|
|
72
|
+
const configured = checkConfigured(account.config);
|
|
73
|
+
account.config.configured = configured;
|
|
74
|
+
return configured;
|
|
75
|
+
},
|
|
76
|
+
describeAccount: (account, cfg) => dlDescribeAccount({ account, cfg }),
|
|
77
|
+
//暂时关闭inspectAccount: (cfg, accountId) => dlInspectAccount({ cfg, accountId, channelId })
|
|
78
|
+
},
|
|
79
|
+
resolver: {
|
|
80
|
+
//未了解其作用!,测试以观察日志
|
|
81
|
+
resolveTargets: (params) => {
|
|
82
|
+
const { accountId, inputs, kind, runtime } = params;
|
|
83
|
+
const logger = getDLLogger();
|
|
84
|
+
logger.info(`ResolveTargets account[${accountId}], kind=${kind}, input=[${inputs.join('')}]`);
|
|
85
|
+
const result = inputs.map(item => {
|
|
86
|
+
return {
|
|
87
|
+
input: item,
|
|
88
|
+
id: accountId || '',
|
|
89
|
+
resolved: true
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
return new Promise((ok) => {
|
|
93
|
+
ok(result);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
messaging: {
|
|
98
|
+
normalizeTarget(raw) {
|
|
99
|
+
const trimmed = raw.trim();
|
|
100
|
+
const logger = getDLLogger();
|
|
101
|
+
logger.info(`Messaging[normalizeTarget] target normalized >>> ${trimmed}`);
|
|
102
|
+
if (!trimmed)
|
|
103
|
+
return undefined;
|
|
104
|
+
return trimmed;
|
|
105
|
+
},
|
|
106
|
+
targetResolver: {
|
|
107
|
+
looksLikeId: (id) => {
|
|
108
|
+
const trimmed = id?.trim();
|
|
109
|
+
return Boolean(trimmed);
|
|
110
|
+
},
|
|
111
|
+
hint: "<userId|groupId>",
|
|
112
|
+
},
|
|
113
|
+
formatTargetDisplay(params) {
|
|
114
|
+
getDLLogger().info('Messaging[formatTargetDisplay]:' + JSON.stringify(params));
|
|
115
|
+
return `FormatTargetDisplay: ${params.target}`;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
security: {
|
|
119
|
+
resolveDmPolicy: ({ account }) => {
|
|
120
|
+
const basePath = `channels.${channelId}.`;
|
|
121
|
+
return {
|
|
122
|
+
policy: account.config.dmPolicy ?? "pairing",
|
|
123
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
124
|
+
policyPath: `${basePath}dmPolicy`,
|
|
125
|
+
allowFromPath: basePath,
|
|
126
|
+
approveHint: formatPairingApproveHint(channelId),
|
|
127
|
+
normalizeEntry: (raw) => {
|
|
128
|
+
getDLLogger().info('Security[resolveDmPolicy]:' + raw);
|
|
129
|
+
const result = raw.replace(new RegExp(`^${channelId}:`, "i"), "").trim();
|
|
130
|
+
getDLLogger().info('Security[resolveDmPolicy]:' + result);
|
|
131
|
+
return result;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
pairing: dlPairing(channelId),
|
|
137
|
+
outbound: dlOutbound(),
|
|
138
|
+
gateway: {
|
|
139
|
+
startAccount: async (ctx) => {
|
|
140
|
+
ctx.setStatus({
|
|
141
|
+
accountId: ctx.accountId,
|
|
142
|
+
lastStartAt: Date.now()
|
|
143
|
+
});
|
|
144
|
+
return monitorDuolinkProvider({
|
|
145
|
+
config: ctx.cfg,
|
|
146
|
+
runtime: ctx.runtime,
|
|
147
|
+
abortSignal: ctx.abortSignal,
|
|
148
|
+
accountId: ctx.accountId,
|
|
149
|
+
channelId
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const dlClients = new Map();
|
|
2
|
+
export const getDLClient = (accountId) => {
|
|
3
|
+
return dlClients.get(accountId) ?? null;
|
|
4
|
+
};
|
|
5
|
+
export const setDLClient = (accountId, client) => {
|
|
6
|
+
dlClients.set(accountId, client);
|
|
7
|
+
};
|
|
8
|
+
export const removeDLClient = (accountId) => {
|
|
9
|
+
dlClients.delete(accountId);
|
|
10
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** 仅包含图片时的消息占位符 */
|
|
2
|
+
export declare const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
|
|
3
|
+
export declare const MEDIA_VOICE_PLACEHOLDER = "<media:audio>";
|
|
4
|
+
export declare const MEDIA_VIDEO_PLACEHOLDER = "<media:video>";
|
|
5
|
+
/** 仅包含文件时的消息占位符 */
|
|
6
|
+
export declare const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
|
|
7
|
+
/** 默认媒体大小上限(MB)——插件↔服务端链路(下载/上传)的兜底限制 */
|
|
8
|
+
export declare 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 declare const DEFAULT_INSTALL_MEDIA_MAX_MB = 100;
|
|
15
|
+
/** 文本分块大小上限 */
|
|
16
|
+
export declare const TEXT_CHUNK_LIMIT = 4000;
|
|
17
|
+
/** 图片下载超时时间(毫秒) */
|
|
18
|
+
export declare const IMAGE_DOWNLOAD_TIMEOUT_MS: number;
|
|
19
|
+
/** 文件下载超时时间(毫秒) */
|
|
20
|
+
export declare const FILE_DOWNLOAD_TIMEOUT_MS: number;
|
|
21
|
+
/** 消息发送超时时间(毫秒) */
|
|
22
|
+
export declare const REPLY_SEND_TIMEOUT_MS = 15000;
|
|
23
|
+
/**
|
|
24
|
+
* 主动回复API
|
|
25
|
+
*/
|
|
26
|
+
export declare const AUTO_RELAY_API = "/guest/relayBot/sendNotification";
|
|
27
|
+
export declare const AUTO_RELAY_UPLOAD = "/guest/relayBot/uploadMediaFiles";
|
|
28
|
+
export declare const FILE_LIMITS: {
|
|
29
|
+
image: {
|
|
30
|
+
maxSize: number;
|
|
31
|
+
};
|
|
32
|
+
attachment: {
|
|
33
|
+
maxSize: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare const IMAGE_EXTS: Set<string>;
|
|
37
|
+
export type ATTACHMENT_KINK = "image" | "video" | "audio" | "document" | "unknown" | undefined;
|
|
@@ -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
|
+
}
|