fengming 0.3.2 → 0.3.4
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/CHANGELOG.md +2 -3
- package/dist/build-info.json +2 -2
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/cli-startup-metadata.json +8 -8
- package/dist/control-ui/assets/{activity-DXrrQ3p0.js → activity-CCu43qU8.js} +2 -2
- package/dist/control-ui/assets/{agents-DBTWjl5N.js → agents-DCvsB0yO.js} +2 -2
- package/dist/control-ui/assets/{channel-config-extras-1Omw3P4a.js → channel-config-extras-GDGUjA2T.js} +2 -2
- package/dist/control-ui/assets/{channels-CLz01a7t.js → channels-CpM2j5xT.js} +2 -2
- package/dist/control-ui/assets/{cron-vTxFqNNV.js → cron-CLXNfwYa.js} +2 -2
- package/dist/control-ui/assets/{debug-CvwuX3fe.js → debug-BcJ34lrC.js} +2 -2
- package/dist/control-ui/assets/i18n-BWPaSddY.js +2 -0
- package/dist/control-ui/assets/{index-CFA4p4Li.js → index-CuBn2YpX.js} +5 -5
- package/dist/control-ui/assets/{instances-BbKVXLX1.js → instances-Bu6NM_Hs.js} +2 -2
- package/dist/control-ui/assets/{logs-B4vkHG-U.js → logs-atEFsTJJ.js} +2 -2
- package/dist/control-ui/assets/{nodes-DrdIa_Kj.js → nodes-Bd1WzVLK.js} +2 -2
- package/dist/control-ui/assets/{push-subscription-Ck2KdK-7.js → push-subscription-CY8GxC_U.js} +2 -2
- package/dist/control-ui/assets/{sessions-BqDAC2V3.js → sessions-C2r8pbt7.js} +2 -2
- package/dist/control-ui/assets/{skills-DCQx-dAB.js → skills-DIjn93ee.js} +2 -2
- package/dist/control-ui/assets/{skills-shared-BWrG-mcT.js → skills-shared-lQxAuc7D.js} +2 -2
- package/dist/control-ui/assets/{workboard-Cqwvi5Ma.js → workboard-CHb0if1l.js} +2 -2
- package/dist/control-ui/index.html +2 -2
- package/dist/control-ui/sw.js +1 -1
- package/dist/extensions/weixin/index.js +6 -6
- package/dist/gateway/protocol/index.d.ts +1 -1
- package/dist/{index-GK-13hii.d.ts → index-AZzJCgph.d.ts} +1 -1
- package/dist/{monitor-HjEwlqJs.js → monitor-2_c2Ttjf.js} +1 -1
- package/dist/plugin-sdk/.boundary-entry-shims.stamp +1 -1
- package/dist/plugin-sdk/agent-config-primitives.d.ts +1 -1
- package/dist/plugin-sdk/{bundled-channel-config-schema-BPFNnbwu.d.ts → bundled-channel-config-schema-UtIBjviA.d.ts} +30 -30
- package/dist/plugin-sdk/bundled-channel-config-schema.d.ts +3 -3
- package/dist/plugin-sdk/channel-config-primitives.d.ts +2 -2
- package/dist/plugin-sdk/channel-config-schema-legacy.d.ts +3 -3
- package/dist/plugin-sdk/channel-config-schema.d.ts +2 -2
- package/dist/plugin-sdk/channel-core.d.ts +1 -1
- package/dist/plugin-sdk/channel-plugin-common.d.ts +1 -1
- package/dist/plugin-sdk/compat.d.ts +2 -2
- package/dist/plugin-sdk/{config-schema-D7cABQ6o.d.ts → config-schema-DUddICQM.d.ts} +1 -1
- package/dist/plugin-sdk/config-schema.d.ts +6 -6
- package/dist/plugin-sdk/core.d.ts +1 -1
- package/dist/plugin-sdk/discord.d.ts +2 -2
- package/dist/plugin-sdk/tts-runtime.d.ts +1 -1
- package/dist/plugin-sdk/{zod-schema.core-CwBNqcXp.d.ts → zod-schema.core-B4_b2R5K.d.ts} +1 -1
- package/dist/plugins/runtime/index.js +2 -2
- package/dist/postinstall-inventory.json +22 -22
- package/dist/{send-media-Bl2ryo6a.js → send-media-OG5Gd31l.js} +252 -252
- package/package.json +1 -1
- package/dist/control-ui/assets/i18n-CqJHwmVQ.js +0 -2
|
@@ -909,6 +909,160 @@ function assertSessionActive(accountId) {
|
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
//#endregion
|
|
912
|
+
//#region extensions/weixin/src/cdn/aes-ecb.ts
|
|
913
|
+
/**
|
|
914
|
+
* Shared AES-128-ECB crypto utilities for CDN upload and download.
|
|
915
|
+
*/
|
|
916
|
+
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
917
|
+
function encryptAesEcb(plaintext, key) {
|
|
918
|
+
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
919
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
920
|
+
}
|
|
921
|
+
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
922
|
+
function decryptAesEcb(ciphertext, key) {
|
|
923
|
+
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
924
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
925
|
+
}
|
|
926
|
+
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
927
|
+
function aesEcbPaddedSize(plaintextSize) {
|
|
928
|
+
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region extensions/weixin/src/cdn/cdn-url.ts
|
|
932
|
+
/** Build a CDN download URL from encrypt_query_param. */
|
|
933
|
+
function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
|
|
934
|
+
return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
|
|
935
|
+
}
|
|
936
|
+
/** Build a CDN upload URL from upload_param and filekey. */
|
|
937
|
+
function buildCdnUploadUrl(params) {
|
|
938
|
+
return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
|
|
939
|
+
}
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region extensions/weixin/src/cdn/cdn-upload.ts
|
|
942
|
+
/** Maximum retry attempts for CDN upload. */
|
|
943
|
+
const UPLOAD_MAX_RETRIES = 3;
|
|
944
|
+
/**
|
|
945
|
+
* Upload one buffer to the Weixin CDN with AES-128-ECB encryption.
|
|
946
|
+
* Returns the download encrypted_query_param from the CDN response.
|
|
947
|
+
* Retries up to UPLOAD_MAX_RETRIES times on server errors; client errors (4xx) abort immediately.
|
|
948
|
+
*/
|
|
949
|
+
async function uploadBufferToCdn(params) {
|
|
950
|
+
const { buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey } = params;
|
|
951
|
+
const ciphertext = encryptAesEcb(buf, aeskey);
|
|
952
|
+
const trimmedFull = uploadFullUrl?.trim();
|
|
953
|
+
let cdnUrl;
|
|
954
|
+
if (trimmedFull) cdnUrl = trimmedFull;
|
|
955
|
+
else if (uploadParam) cdnUrl = buildCdnUploadUrl({
|
|
956
|
+
cdnBaseUrl,
|
|
957
|
+
uploadParam,
|
|
958
|
+
filekey
|
|
959
|
+
});
|
|
960
|
+
else throw new Error(`${label}: CDN upload URL missing (need upload_full_url or upload_param)`);
|
|
961
|
+
logger.debug(`${label}: CDN POST url=${redactUrl(cdnUrl)} ciphertextSize=${ciphertext.length}`);
|
|
962
|
+
let downloadParam;
|
|
963
|
+
let lastError;
|
|
964
|
+
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) try {
|
|
965
|
+
const res = await fetch(cdnUrl, {
|
|
966
|
+
method: "POST",
|
|
967
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
968
|
+
body: new Uint8Array(ciphertext)
|
|
969
|
+
});
|
|
970
|
+
if (res.status >= 400 && res.status < 500) {
|
|
971
|
+
const errMsg = res.headers.get("x-error-message") ?? await res.text();
|
|
972
|
+
logger.error(`${label}: CDN client error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
973
|
+
throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);
|
|
974
|
+
}
|
|
975
|
+
if (res.status !== 200) {
|
|
976
|
+
const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
|
|
977
|
+
logger.error(`${label}: CDN server error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
978
|
+
throw new Error(`CDN upload server error: ${errMsg}`);
|
|
979
|
+
}
|
|
980
|
+
downloadParam = res.headers.get("x-encrypted-param") ?? void 0;
|
|
981
|
+
if (!downloadParam) {
|
|
982
|
+
logger.error(`${label}: CDN response missing x-encrypted-param header attempt=${attempt}`);
|
|
983
|
+
throw new Error("CDN upload response missing x-encrypted-param header");
|
|
984
|
+
}
|
|
985
|
+
logger.debug(`${label}: CDN upload success attempt=${attempt}`);
|
|
986
|
+
break;
|
|
987
|
+
} catch (err) {
|
|
988
|
+
lastError = err;
|
|
989
|
+
if (err instanceof Error && err.message.includes("client error")) throw err;
|
|
990
|
+
if (attempt < UPLOAD_MAX_RETRIES) logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
|
|
991
|
+
else logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
|
|
992
|
+
}
|
|
993
|
+
if (!downloadParam) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
|
|
994
|
+
return { downloadParam };
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region extensions/weixin/src/media/mime.ts
|
|
998
|
+
const EXTENSION_TO_MIME = {
|
|
999
|
+
".pdf": "application/pdf",
|
|
1000
|
+
".doc": "application/msword",
|
|
1001
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1002
|
+
".xls": "application/vnd.ms-excel",
|
|
1003
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1004
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
1005
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1006
|
+
".txt": "text/plain",
|
|
1007
|
+
".csv": "text/csv",
|
|
1008
|
+
".zip": "application/zip",
|
|
1009
|
+
".tar": "application/x-tar",
|
|
1010
|
+
".gz": "application/gzip",
|
|
1011
|
+
".mp3": "audio/mpeg",
|
|
1012
|
+
".ogg": "audio/ogg",
|
|
1013
|
+
".wav": "audio/wav",
|
|
1014
|
+
".mp4": "video/mp4",
|
|
1015
|
+
".mov": "video/quicktime",
|
|
1016
|
+
".webm": "video/webm",
|
|
1017
|
+
".mkv": "video/x-matroska",
|
|
1018
|
+
".avi": "video/x-msvideo",
|
|
1019
|
+
".png": "image/png",
|
|
1020
|
+
".jpg": "image/jpeg",
|
|
1021
|
+
".jpeg": "image/jpeg",
|
|
1022
|
+
".gif": "image/gif",
|
|
1023
|
+
".webp": "image/webp",
|
|
1024
|
+
".bmp": "image/bmp"
|
|
1025
|
+
};
|
|
1026
|
+
const MIME_TO_EXTENSION = {
|
|
1027
|
+
"image/jpeg": ".jpg",
|
|
1028
|
+
"image/jpg": ".jpg",
|
|
1029
|
+
"image/png": ".png",
|
|
1030
|
+
"image/gif": ".gif",
|
|
1031
|
+
"image/webp": ".webp",
|
|
1032
|
+
"image/bmp": ".bmp",
|
|
1033
|
+
"video/mp4": ".mp4",
|
|
1034
|
+
"video/quicktime": ".mov",
|
|
1035
|
+
"video/webm": ".webm",
|
|
1036
|
+
"video/x-matroska": ".mkv",
|
|
1037
|
+
"video/x-msvideo": ".avi",
|
|
1038
|
+
"audio/mpeg": ".mp3",
|
|
1039
|
+
"audio/ogg": ".ogg",
|
|
1040
|
+
"audio/wav": ".wav",
|
|
1041
|
+
"application/pdf": ".pdf",
|
|
1042
|
+
"application/zip": ".zip",
|
|
1043
|
+
"application/x-tar": ".tar",
|
|
1044
|
+
"application/gzip": ".gz",
|
|
1045
|
+
"text/plain": ".txt",
|
|
1046
|
+
"text/csv": ".csv"
|
|
1047
|
+
};
|
|
1048
|
+
/** Get MIME type from filename extension. Returns "application/octet-stream" for unknown extensions. */
|
|
1049
|
+
function getMimeFromFilename(filename) {
|
|
1050
|
+
return EXTENSION_TO_MIME[path.extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
1051
|
+
}
|
|
1052
|
+
/** Get file extension from MIME type. Returns ".bin" for unknown types. */
|
|
1053
|
+
function getExtensionFromMime(mimeType) {
|
|
1054
|
+
return MIME_TO_EXTENSION[mimeType.split(";")[0].trim().toLowerCase()] ?? ".bin";
|
|
1055
|
+
}
|
|
1056
|
+
/** Get file extension from Content-Type header or URL path. Returns ".bin" for unknown. */
|
|
1057
|
+
function getExtensionFromContentTypeOrUrl(contentType, url) {
|
|
1058
|
+
if (contentType) {
|
|
1059
|
+
const ext = getExtensionFromMime(contentType);
|
|
1060
|
+
if (ext !== ".bin") return ext;
|
|
1061
|
+
}
|
|
1062
|
+
const ext = path.extname(new URL(url).pathname).toLowerCase();
|
|
1063
|
+
return new Set(Object.keys(EXTENSION_TO_MIME)).has(ext) ? ext : ".bin";
|
|
1064
|
+
}
|
|
1065
|
+
//#endregion
|
|
912
1066
|
//#region extensions/weixin/src/util/random.ts
|
|
913
1067
|
/**
|
|
914
1068
|
* Generate a prefixed unique ID using timestamp + crypto random bytes.
|
|
@@ -959,6 +1113,103 @@ const TypingStatus = {
|
|
|
959
1113
|
CANCEL: 2
|
|
960
1114
|
};
|
|
961
1115
|
//#endregion
|
|
1116
|
+
//#region extensions/weixin/src/cdn/upload.ts
|
|
1117
|
+
/**
|
|
1118
|
+
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
1119
|
+
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
1120
|
+
*/
|
|
1121
|
+
async function downloadRemoteImageToTemp(url, destDir) {
|
|
1122
|
+
logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
|
|
1123
|
+
const res = await fetch(url);
|
|
1124
|
+
if (!res.ok) {
|
|
1125
|
+
const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
|
|
1126
|
+
logger.error(`downloadRemoteImageToTemp: ${msg}`);
|
|
1127
|
+
throw new Error(msg);
|
|
1128
|
+
}
|
|
1129
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1130
|
+
logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
|
|
1131
|
+
await fs$1.mkdir(destDir, { recursive: true });
|
|
1132
|
+
const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
|
|
1133
|
+
const name = tempFileName("weixin-remote", ext);
|
|
1134
|
+
const filePath = path.join(destDir, name);
|
|
1135
|
+
await fs$1.writeFile(filePath, buf);
|
|
1136
|
+
logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
|
|
1137
|
+
return filePath;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Common upload pipeline: read file → hash → gen aeskey → getUploadUrl → uploadBufferToCdn → return info.
|
|
1141
|
+
*/
|
|
1142
|
+
async function uploadMediaToCdn(params) {
|
|
1143
|
+
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
1144
|
+
const plaintext = await fs$1.readFile(filePath);
|
|
1145
|
+
const rawsize = plaintext.length;
|
|
1146
|
+
const rawfilemd5 = crypto.createHash("md5").update(plaintext).digest("hex");
|
|
1147
|
+
const filesize = aesEcbPaddedSize(rawsize);
|
|
1148
|
+
const filekey = crypto.randomBytes(16).toString("hex");
|
|
1149
|
+
const aeskey = crypto.randomBytes(16);
|
|
1150
|
+
logger.debug(`${label}: file=${filePath} rawsize=${rawsize} filesize=${filesize} md5=${rawfilemd5} filekey=${filekey}`);
|
|
1151
|
+
const uploadUrlResp = await getUploadUrl({
|
|
1152
|
+
...opts,
|
|
1153
|
+
filekey,
|
|
1154
|
+
media_type: mediaType,
|
|
1155
|
+
to_user_id: toUserId,
|
|
1156
|
+
rawsize,
|
|
1157
|
+
rawfilemd5,
|
|
1158
|
+
filesize,
|
|
1159
|
+
no_need_thumb: true,
|
|
1160
|
+
aeskey: aeskey.toString("hex")
|
|
1161
|
+
});
|
|
1162
|
+
const uploadFullUrl = uploadUrlResp.upload_full_url?.trim();
|
|
1163
|
+
const uploadParam = uploadUrlResp.upload_param;
|
|
1164
|
+
if (!uploadFullUrl && !uploadParam) {
|
|
1165
|
+
logger.error(`${label}: getUploadUrl returned no upload URL (need upload_full_url or upload_param), resp=${JSON.stringify(uploadUrlResp)}`);
|
|
1166
|
+
throw new Error(`${label}: getUploadUrl returned no upload URL`);
|
|
1167
|
+
}
|
|
1168
|
+
const { downloadParam: downloadEncryptedQueryParam } = await uploadBufferToCdn({
|
|
1169
|
+
buf: plaintext,
|
|
1170
|
+
uploadFullUrl: uploadFullUrl || void 0,
|
|
1171
|
+
uploadParam: uploadParam ?? void 0,
|
|
1172
|
+
filekey,
|
|
1173
|
+
cdnBaseUrl,
|
|
1174
|
+
aeskey,
|
|
1175
|
+
label: `${label}[orig filekey=${filekey}]`
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
filekey,
|
|
1179
|
+
downloadEncryptedQueryParam,
|
|
1180
|
+
aeskey: aeskey.toString("hex"),
|
|
1181
|
+
fileSize: rawsize,
|
|
1182
|
+
fileSizeCiphertext: filesize
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/** Upload a local image file to the Weixin CDN with AES-128-ECB encryption. */
|
|
1186
|
+
async function uploadFileToWeixin(params) {
|
|
1187
|
+
return uploadMediaToCdn({
|
|
1188
|
+
...params,
|
|
1189
|
+
mediaType: UploadMediaType.IMAGE,
|
|
1190
|
+
label: "uploadFileToWeixin"
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
/** Upload a local video file to the Weixin CDN. */
|
|
1194
|
+
async function uploadVideoToWeixin(params) {
|
|
1195
|
+
return uploadMediaToCdn({
|
|
1196
|
+
...params,
|
|
1197
|
+
mediaType: UploadMediaType.VIDEO,
|
|
1198
|
+
label: "uploadVideoToWeixin"
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Upload a local file attachment (non-image, non-video) to the Weixin CDN.
|
|
1203
|
+
* Uses media_type=FILE; no thumbnail required.
|
|
1204
|
+
*/
|
|
1205
|
+
async function uploadFileAttachmentToWeixin(params) {
|
|
1206
|
+
return uploadMediaToCdn({
|
|
1207
|
+
...params,
|
|
1208
|
+
mediaType: UploadMediaType.FILE,
|
|
1209
|
+
label: "uploadFileAttachmentToWeixin"
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
//#endregion
|
|
962
1213
|
//#region extensions/weixin/src/messaging/inbound.ts
|
|
963
1214
|
/**
|
|
964
1215
|
* contextToken is issued per-message by the Weixin getupdates API and must
|
|
@@ -1174,75 +1425,6 @@ function emitWeixinMessageSent(params) {
|
|
|
1174
1425
|
fireAndForgetHook(Promise.resolve(hookRunner.runMessageSent(toPluginMessageSentEvent(canonical), toPluginMessageContext(canonical))), "weixin: message_sent plugin hook failed");
|
|
1175
1426
|
}
|
|
1176
1427
|
//#endregion
|
|
1177
|
-
//#region extensions/weixin/src/media/mime.ts
|
|
1178
|
-
const EXTENSION_TO_MIME = {
|
|
1179
|
-
".pdf": "application/pdf",
|
|
1180
|
-
".doc": "application/msword",
|
|
1181
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1182
|
-
".xls": "application/vnd.ms-excel",
|
|
1183
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1184
|
-
".ppt": "application/vnd.ms-powerpoint",
|
|
1185
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1186
|
-
".txt": "text/plain",
|
|
1187
|
-
".csv": "text/csv",
|
|
1188
|
-
".zip": "application/zip",
|
|
1189
|
-
".tar": "application/x-tar",
|
|
1190
|
-
".gz": "application/gzip",
|
|
1191
|
-
".mp3": "audio/mpeg",
|
|
1192
|
-
".ogg": "audio/ogg",
|
|
1193
|
-
".wav": "audio/wav",
|
|
1194
|
-
".mp4": "video/mp4",
|
|
1195
|
-
".mov": "video/quicktime",
|
|
1196
|
-
".webm": "video/webm",
|
|
1197
|
-
".mkv": "video/x-matroska",
|
|
1198
|
-
".avi": "video/x-msvideo",
|
|
1199
|
-
".png": "image/png",
|
|
1200
|
-
".jpg": "image/jpeg",
|
|
1201
|
-
".jpeg": "image/jpeg",
|
|
1202
|
-
".gif": "image/gif",
|
|
1203
|
-
".webp": "image/webp",
|
|
1204
|
-
".bmp": "image/bmp"
|
|
1205
|
-
};
|
|
1206
|
-
const MIME_TO_EXTENSION = {
|
|
1207
|
-
"image/jpeg": ".jpg",
|
|
1208
|
-
"image/jpg": ".jpg",
|
|
1209
|
-
"image/png": ".png",
|
|
1210
|
-
"image/gif": ".gif",
|
|
1211
|
-
"image/webp": ".webp",
|
|
1212
|
-
"image/bmp": ".bmp",
|
|
1213
|
-
"video/mp4": ".mp4",
|
|
1214
|
-
"video/quicktime": ".mov",
|
|
1215
|
-
"video/webm": ".webm",
|
|
1216
|
-
"video/x-matroska": ".mkv",
|
|
1217
|
-
"video/x-msvideo": ".avi",
|
|
1218
|
-
"audio/mpeg": ".mp3",
|
|
1219
|
-
"audio/ogg": ".ogg",
|
|
1220
|
-
"audio/wav": ".wav",
|
|
1221
|
-
"application/pdf": ".pdf",
|
|
1222
|
-
"application/zip": ".zip",
|
|
1223
|
-
"application/x-tar": ".tar",
|
|
1224
|
-
"application/gzip": ".gz",
|
|
1225
|
-
"text/plain": ".txt",
|
|
1226
|
-
"text/csv": ".csv"
|
|
1227
|
-
};
|
|
1228
|
-
/** Get MIME type from filename extension. Returns "application/octet-stream" for unknown extensions. */
|
|
1229
|
-
function getMimeFromFilename(filename) {
|
|
1230
|
-
return EXTENSION_TO_MIME[path.extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
1231
|
-
}
|
|
1232
|
-
/** Get file extension from MIME type. Returns ".bin" for unknown types. */
|
|
1233
|
-
function getExtensionFromMime(mimeType) {
|
|
1234
|
-
return MIME_TO_EXTENSION[mimeType.split(";")[0].trim().toLowerCase()] ?? ".bin";
|
|
1235
|
-
}
|
|
1236
|
-
/** Get file extension from Content-Type header or URL path. Returns ".bin" for unknown. */
|
|
1237
|
-
function getExtensionFromContentTypeOrUrl(contentType, url) {
|
|
1238
|
-
if (contentType) {
|
|
1239
|
-
const ext = getExtensionFromMime(contentType);
|
|
1240
|
-
if (ext !== ".bin") return ext;
|
|
1241
|
-
}
|
|
1242
|
-
const ext = path.extname(new URL(url).pathname).toLowerCase();
|
|
1243
|
-
return new Set(Object.keys(EXTENSION_TO_MIME)).has(ext) ? ext : ".bin";
|
|
1244
|
-
}
|
|
1245
|
-
//#endregion
|
|
1246
1428
|
//#region extensions/weixin/src/messaging/markdown-filter.ts
|
|
1247
1429
|
/**
|
|
1248
1430
|
* Streaming markdown filter — character-level state machine that strips
|
|
@@ -1819,188 +2001,6 @@ async function sendFileMessageWeixin(params) {
|
|
|
1819
2001
|
});
|
|
1820
2002
|
}
|
|
1821
2003
|
//#endregion
|
|
1822
|
-
//#region extensions/weixin/src/cdn/aes-ecb.ts
|
|
1823
|
-
/**
|
|
1824
|
-
* Shared AES-128-ECB crypto utilities for CDN upload and download.
|
|
1825
|
-
*/
|
|
1826
|
-
/** Encrypt buffer with AES-128-ECB (PKCS7 padding is default). */
|
|
1827
|
-
function encryptAesEcb(plaintext, key) {
|
|
1828
|
-
const cipher = createCipheriv("aes-128-ecb", key, null);
|
|
1829
|
-
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1830
|
-
}
|
|
1831
|
-
/** Decrypt buffer with AES-128-ECB (PKCS7 padding). */
|
|
1832
|
-
function decryptAesEcb(ciphertext, key) {
|
|
1833
|
-
const decipher = createDecipheriv("aes-128-ecb", key, null);
|
|
1834
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1835
|
-
}
|
|
1836
|
-
/** Compute AES-128-ECB ciphertext size (PKCS7 padding to 16-byte boundary). */
|
|
1837
|
-
function aesEcbPaddedSize(plaintextSize) {
|
|
1838
|
-
return Math.ceil((plaintextSize + 1) / 16) * 16;
|
|
1839
|
-
}
|
|
1840
|
-
//#endregion
|
|
1841
|
-
//#region extensions/weixin/src/cdn/cdn-url.ts
|
|
1842
|
-
/** Build a CDN download URL from encrypt_query_param. */
|
|
1843
|
-
function buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl) {
|
|
1844
|
-
return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`;
|
|
1845
|
-
}
|
|
1846
|
-
/** Build a CDN upload URL from upload_param and filekey. */
|
|
1847
|
-
function buildCdnUploadUrl(params) {
|
|
1848
|
-
return `${params.cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(params.uploadParam)}&filekey=${encodeURIComponent(params.filekey)}`;
|
|
1849
|
-
}
|
|
1850
|
-
//#endregion
|
|
1851
|
-
//#region extensions/weixin/src/cdn/cdn-upload.ts
|
|
1852
|
-
/** Maximum retry attempts for CDN upload. */
|
|
1853
|
-
const UPLOAD_MAX_RETRIES = 3;
|
|
1854
|
-
/**
|
|
1855
|
-
* Upload one buffer to the Weixin CDN with AES-128-ECB encryption.
|
|
1856
|
-
* Returns the download encrypted_query_param from the CDN response.
|
|
1857
|
-
* Retries up to UPLOAD_MAX_RETRIES times on server errors; client errors (4xx) abort immediately.
|
|
1858
|
-
*/
|
|
1859
|
-
async function uploadBufferToCdn(params) {
|
|
1860
|
-
const { buf, uploadFullUrl, uploadParam, filekey, cdnBaseUrl, label, aeskey } = params;
|
|
1861
|
-
const ciphertext = encryptAesEcb(buf, aeskey);
|
|
1862
|
-
const trimmedFull = uploadFullUrl?.trim();
|
|
1863
|
-
let cdnUrl;
|
|
1864
|
-
if (trimmedFull) cdnUrl = trimmedFull;
|
|
1865
|
-
else if (uploadParam) cdnUrl = buildCdnUploadUrl({
|
|
1866
|
-
cdnBaseUrl,
|
|
1867
|
-
uploadParam,
|
|
1868
|
-
filekey
|
|
1869
|
-
});
|
|
1870
|
-
else throw new Error(`${label}: CDN upload URL missing (need upload_full_url or upload_param)`);
|
|
1871
|
-
logger.debug(`${label}: CDN POST url=${redactUrl(cdnUrl)} ciphertextSize=${ciphertext.length}`);
|
|
1872
|
-
let downloadParam;
|
|
1873
|
-
let lastError;
|
|
1874
|
-
for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) try {
|
|
1875
|
-
const res = await fetch(cdnUrl, {
|
|
1876
|
-
method: "POST",
|
|
1877
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
1878
|
-
body: new Uint8Array(ciphertext)
|
|
1879
|
-
});
|
|
1880
|
-
if (res.status >= 400 && res.status < 500) {
|
|
1881
|
-
const errMsg = res.headers.get("x-error-message") ?? await res.text();
|
|
1882
|
-
logger.error(`${label}: CDN client error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
1883
|
-
throw new Error(`CDN upload client error ${res.status}: ${errMsg}`);
|
|
1884
|
-
}
|
|
1885
|
-
if (res.status !== 200) {
|
|
1886
|
-
const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
|
|
1887
|
-
logger.error(`${label}: CDN server error attempt=${attempt} status=${res.status} errMsg=${errMsg}`);
|
|
1888
|
-
throw new Error(`CDN upload server error: ${errMsg}`);
|
|
1889
|
-
}
|
|
1890
|
-
downloadParam = res.headers.get("x-encrypted-param") ?? void 0;
|
|
1891
|
-
if (!downloadParam) {
|
|
1892
|
-
logger.error(`${label}: CDN response missing x-encrypted-param header attempt=${attempt}`);
|
|
1893
|
-
throw new Error("CDN upload response missing x-encrypted-param header");
|
|
1894
|
-
}
|
|
1895
|
-
logger.debug(`${label}: CDN upload success attempt=${attempt}`);
|
|
1896
|
-
break;
|
|
1897
|
-
} catch (err) {
|
|
1898
|
-
lastError = err;
|
|
1899
|
-
if (err instanceof Error && err.message.includes("client error")) throw err;
|
|
1900
|
-
if (attempt < UPLOAD_MAX_RETRIES) logger.error(`${label}: attempt ${attempt} failed, retrying... err=${String(err)}`);
|
|
1901
|
-
else logger.error(`${label}: all ${UPLOAD_MAX_RETRIES} attempts failed err=${String(err)}`);
|
|
1902
|
-
}
|
|
1903
|
-
if (!downloadParam) throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`CDN upload failed after ${UPLOAD_MAX_RETRIES} attempts`);
|
|
1904
|
-
return { downloadParam };
|
|
1905
|
-
}
|
|
1906
|
-
//#endregion
|
|
1907
|
-
//#region extensions/weixin/src/cdn/upload.ts
|
|
1908
|
-
/**
|
|
1909
|
-
* Download a remote media URL (image, video, file) to a local temp file in destDir.
|
|
1910
|
-
* Returns the local file path; extension is inferred from Content-Type / URL.
|
|
1911
|
-
*/
|
|
1912
|
-
async function downloadRemoteImageToTemp(url, destDir) {
|
|
1913
|
-
logger.debug(`downloadRemoteImageToTemp: fetching url=${url}`);
|
|
1914
|
-
const res = await fetch(url);
|
|
1915
|
-
if (!res.ok) {
|
|
1916
|
-
const msg = `remote media download failed: ${res.status} ${res.statusText} url=${url}`;
|
|
1917
|
-
logger.error(`downloadRemoteImageToTemp: ${msg}`);
|
|
1918
|
-
throw new Error(msg);
|
|
1919
|
-
}
|
|
1920
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
1921
|
-
logger.debug(`downloadRemoteImageToTemp: downloaded ${buf.length} bytes`);
|
|
1922
|
-
await fs$1.mkdir(destDir, { recursive: true });
|
|
1923
|
-
const ext = getExtensionFromContentTypeOrUrl(res.headers.get("content-type"), url);
|
|
1924
|
-
const name = tempFileName("weixin-remote", ext);
|
|
1925
|
-
const filePath = path.join(destDir, name);
|
|
1926
|
-
await fs$1.writeFile(filePath, buf);
|
|
1927
|
-
logger.debug(`downloadRemoteImageToTemp: saved to ${filePath} ext=${ext}`);
|
|
1928
|
-
return filePath;
|
|
1929
|
-
}
|
|
1930
|
-
/**
|
|
1931
|
-
* Common upload pipeline: read file → hash → gen aeskey → getUploadUrl → uploadBufferToCdn → return info.
|
|
1932
|
-
*/
|
|
1933
|
-
async function uploadMediaToCdn(params) {
|
|
1934
|
-
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
1935
|
-
const plaintext = await fs$1.readFile(filePath);
|
|
1936
|
-
const rawsize = plaintext.length;
|
|
1937
|
-
const rawfilemd5 = crypto.createHash("md5").update(plaintext).digest("hex");
|
|
1938
|
-
const filesize = aesEcbPaddedSize(rawsize);
|
|
1939
|
-
const filekey = crypto.randomBytes(16).toString("hex");
|
|
1940
|
-
const aeskey = crypto.randomBytes(16);
|
|
1941
|
-
logger.debug(`${label}: file=${filePath} rawsize=${rawsize} filesize=${filesize} md5=${rawfilemd5} filekey=${filekey}`);
|
|
1942
|
-
const uploadUrlResp = await getUploadUrl({
|
|
1943
|
-
...opts,
|
|
1944
|
-
filekey,
|
|
1945
|
-
media_type: mediaType,
|
|
1946
|
-
to_user_id: toUserId,
|
|
1947
|
-
rawsize,
|
|
1948
|
-
rawfilemd5,
|
|
1949
|
-
filesize,
|
|
1950
|
-
no_need_thumb: true,
|
|
1951
|
-
aeskey: aeskey.toString("hex")
|
|
1952
|
-
});
|
|
1953
|
-
const uploadFullUrl = uploadUrlResp.upload_full_url?.trim();
|
|
1954
|
-
const uploadParam = uploadUrlResp.upload_param;
|
|
1955
|
-
if (!uploadFullUrl && !uploadParam) {
|
|
1956
|
-
logger.error(`${label}: getUploadUrl returned no upload URL (need upload_full_url or upload_param), resp=${JSON.stringify(uploadUrlResp)}`);
|
|
1957
|
-
throw new Error(`${label}: getUploadUrl returned no upload URL`);
|
|
1958
|
-
}
|
|
1959
|
-
const { downloadParam: downloadEncryptedQueryParam } = await uploadBufferToCdn({
|
|
1960
|
-
buf: plaintext,
|
|
1961
|
-
uploadFullUrl: uploadFullUrl || void 0,
|
|
1962
|
-
uploadParam: uploadParam ?? void 0,
|
|
1963
|
-
filekey,
|
|
1964
|
-
cdnBaseUrl,
|
|
1965
|
-
aeskey,
|
|
1966
|
-
label: `${label}[orig filekey=${filekey}]`
|
|
1967
|
-
});
|
|
1968
|
-
return {
|
|
1969
|
-
filekey,
|
|
1970
|
-
downloadEncryptedQueryParam,
|
|
1971
|
-
aeskey: aeskey.toString("hex"),
|
|
1972
|
-
fileSize: rawsize,
|
|
1973
|
-
fileSizeCiphertext: filesize
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
/** Upload a local image file to the Weixin CDN with AES-128-ECB encryption. */
|
|
1977
|
-
async function uploadFileToWeixin(params) {
|
|
1978
|
-
return uploadMediaToCdn({
|
|
1979
|
-
...params,
|
|
1980
|
-
mediaType: UploadMediaType.IMAGE,
|
|
1981
|
-
label: "uploadFileToWeixin"
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
/** Upload a local video file to the Weixin CDN. */
|
|
1985
|
-
async function uploadVideoToWeixin(params) {
|
|
1986
|
-
return uploadMediaToCdn({
|
|
1987
|
-
...params,
|
|
1988
|
-
mediaType: UploadMediaType.VIDEO,
|
|
1989
|
-
label: "uploadVideoToWeixin"
|
|
1990
|
-
});
|
|
1991
|
-
}
|
|
1992
|
-
/**
|
|
1993
|
-
* Upload a local file attachment (non-image, non-video) to the Weixin CDN.
|
|
1994
|
-
* Uses media_type=FILE; no thumbnail required.
|
|
1995
|
-
*/
|
|
1996
|
-
async function uploadFileAttachmentToWeixin(params) {
|
|
1997
|
-
return uploadMediaToCdn({
|
|
1998
|
-
...params,
|
|
1999
|
-
mediaType: UploadMediaType.FILE,
|
|
2000
|
-
label: "uploadFileAttachmentToWeixin"
|
|
2001
|
-
});
|
|
2002
|
-
}
|
|
2003
|
-
//#endregion
|
|
2004
2004
|
//#region extensions/weixin/src/messaging/send-media.ts
|
|
2005
2005
|
/**
|
|
2006
2006
|
* Upload a local file and send it as a weixin message, routing by MIME type:
|
|
@@ -2069,4 +2069,4 @@ async function sendWeixinMediaFile(params) {
|
|
|
2069
2069
|
});
|
|
2070
2070
|
}
|
|
2071
2071
|
//#endregion
|
|
2072
|
-
export { sendTyping as A, registerWeixinAccountId as B, pauseSession as C, getUpdates as D, getConfig as E, clearStaleAccountsForUserId as F, logger as G, saveWeixinAccount as H, deriveRawAccountId as I, resolveStateDir as K, listIndexedWeixinAccountIds as L, redactToken as M, CDN_BASE_URL as N, notifyStart as O, DEFAULT_BASE_URL as P, listWeixinAccountIds as R, getRemainingPauseMs as S, apiPostFetch as T, triggerWeixinChannelReload as U, resolveWeixinAccount as V, readFrameworkAllowFromList as W,
|
|
2072
|
+
export { sendTyping as A, registerWeixinAccountId as B, pauseSession as C, getUpdates as D, getConfig as E, clearStaleAccountsForUserId as F, logger as G, saveWeixinAccount as H, deriveRawAccountId as I, resolveStateDir as K, listIndexedWeixinAccountIds as L, redactToken as M, CDN_BASE_URL as N, notifyStart as O, DEFAULT_BASE_URL as P, listWeixinAccountIds as R, getRemainingPauseMs as S, apiPostFetch as T, triggerWeixinChannelReload as U, resolveWeixinAccount as V, readFrameworkAllowFromList as W, TypingStatus as _, applyWeixinMessageSendingHook as a, decryptAesEcb as b, findAccountIdsByContextToken as c, isMediaItem as d, restoreContextTokens as f, MessageItemType as g, downloadRemoteImageToTemp as h, StreamingMarkdownFilter as i, redactBody as j, notifyStop as k, getContextToken as l, weixinMessageToMsgContext as m, sendMessageItemWeixin as n, emitWeixinMessageSent as o, setContextToken as p, sendMessageWeixin as r, clearContextTokensForAccount as s, sendWeixinMediaFile as t, getContextTokenFromMsgContext as u, getMimeFromFilename as v, apiGetFetch as w, assertSessionActive as x, buildCdnDownloadUrl as y, loadWeixinAccount as z };
|