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.
Files changed (47) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/dist/build-info.json +2 -2
  3. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  4. package/dist/cli-startup-metadata.json +8 -8
  5. package/dist/control-ui/assets/{activity-DXrrQ3p0.js → activity-CCu43qU8.js} +2 -2
  6. package/dist/control-ui/assets/{agents-DBTWjl5N.js → agents-DCvsB0yO.js} +2 -2
  7. package/dist/control-ui/assets/{channel-config-extras-1Omw3P4a.js → channel-config-extras-GDGUjA2T.js} +2 -2
  8. package/dist/control-ui/assets/{channels-CLz01a7t.js → channels-CpM2j5xT.js} +2 -2
  9. package/dist/control-ui/assets/{cron-vTxFqNNV.js → cron-CLXNfwYa.js} +2 -2
  10. package/dist/control-ui/assets/{debug-CvwuX3fe.js → debug-BcJ34lrC.js} +2 -2
  11. package/dist/control-ui/assets/i18n-BWPaSddY.js +2 -0
  12. package/dist/control-ui/assets/{index-CFA4p4Li.js → index-CuBn2YpX.js} +5 -5
  13. package/dist/control-ui/assets/{instances-BbKVXLX1.js → instances-Bu6NM_Hs.js} +2 -2
  14. package/dist/control-ui/assets/{logs-B4vkHG-U.js → logs-atEFsTJJ.js} +2 -2
  15. package/dist/control-ui/assets/{nodes-DrdIa_Kj.js → nodes-Bd1WzVLK.js} +2 -2
  16. package/dist/control-ui/assets/{push-subscription-Ck2KdK-7.js → push-subscription-CY8GxC_U.js} +2 -2
  17. package/dist/control-ui/assets/{sessions-BqDAC2V3.js → sessions-C2r8pbt7.js} +2 -2
  18. package/dist/control-ui/assets/{skills-DCQx-dAB.js → skills-DIjn93ee.js} +2 -2
  19. package/dist/control-ui/assets/{skills-shared-BWrG-mcT.js → skills-shared-lQxAuc7D.js} +2 -2
  20. package/dist/control-ui/assets/{workboard-Cqwvi5Ma.js → workboard-CHb0if1l.js} +2 -2
  21. package/dist/control-ui/index.html +2 -2
  22. package/dist/control-ui/sw.js +1 -1
  23. package/dist/extensions/weixin/index.js +6 -6
  24. package/dist/gateway/protocol/index.d.ts +1 -1
  25. package/dist/{index-GK-13hii.d.ts → index-AZzJCgph.d.ts} +1 -1
  26. package/dist/{monitor-HjEwlqJs.js → monitor-2_c2Ttjf.js} +1 -1
  27. package/dist/plugin-sdk/.boundary-entry-shims.stamp +1 -1
  28. package/dist/plugin-sdk/agent-config-primitives.d.ts +1 -1
  29. package/dist/plugin-sdk/{bundled-channel-config-schema-BPFNnbwu.d.ts → bundled-channel-config-schema-UtIBjviA.d.ts} +30 -30
  30. package/dist/plugin-sdk/bundled-channel-config-schema.d.ts +3 -3
  31. package/dist/plugin-sdk/channel-config-primitives.d.ts +2 -2
  32. package/dist/plugin-sdk/channel-config-schema-legacy.d.ts +3 -3
  33. package/dist/plugin-sdk/channel-config-schema.d.ts +2 -2
  34. package/dist/plugin-sdk/channel-core.d.ts +1 -1
  35. package/dist/plugin-sdk/channel-plugin-common.d.ts +1 -1
  36. package/dist/plugin-sdk/compat.d.ts +2 -2
  37. package/dist/plugin-sdk/{config-schema-D7cABQ6o.d.ts → config-schema-DUddICQM.d.ts} +1 -1
  38. package/dist/plugin-sdk/config-schema.d.ts +6 -6
  39. package/dist/plugin-sdk/core.d.ts +1 -1
  40. package/dist/plugin-sdk/discord.d.ts +2 -2
  41. package/dist/plugin-sdk/tts-runtime.d.ts +1 -1
  42. package/dist/plugin-sdk/{zod-schema.core-CwBNqcXp.d.ts → zod-schema.core-B4_b2R5K.d.ts} +1 -1
  43. package/dist/plugins/runtime/index.js +2 -2
  44. package/dist/postinstall-inventory.json +22 -22
  45. package/dist/{send-media-Bl2ryo6a.js → send-media-OG5Gd31l.js} +252 -252
  46. package/package.json +1 -1
  47. 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, setContextToken as _, sendMessageItemWeixin as a, TypingStatus as b, getMimeFromFilename as c, clearContextTokensForAccount as d, findAccountIdsByContextToken as f, restoreContextTokens as g, isMediaItem as h, decryptAesEcb as i, redactBody as j, notifyStop as k, applyWeixinMessageSendingHook as l, getContextTokenFromMsgContext as m, downloadRemoteImageToTemp as n, sendMessageWeixin as o, getContextToken as p, buildCdnDownloadUrl as r, StreamingMarkdownFilter as s, sendWeixinMediaFile as t, emitWeixinMessageSent as u, weixinMessageToMsgContext as v, apiGetFetch as w, assertSessionActive as x, MessageItemType as y, loadWeixinAccount as z };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fengming",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Multi-channel AI gateway with extensible messaging integrations",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/fengming/fengming#readme",