@sunnoy/wecom 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/crypto.js CHANGED
@@ -1,108 +1,135 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
2
- import { XMLParser, XMLBuilder } from "fast-xml-parser";
3
- import { CONSTANTS } from "./utils.js";
4
2
  import { logger } from "./logger.js";
3
+ import { CONSTANTS } from "./utils.js";
5
4
 
6
5
  /**
7
6
  * Enterprise WeChat Intelligent Robot Crypto Implementation
8
7
  * Simplified for AI Bot mode (no corpId validation)
9
8
  */
10
9
  export class WecomCrypto {
11
- token;
12
- encodingAesKey;
13
- aesKey;
14
- iv;
15
-
16
- constructor(token, encodingAesKey) {
17
- if (!encodingAesKey || encodingAesKey.length !== CONSTANTS.AES_KEY_LENGTH) {
18
- throw new Error(`EncodingAESKey invalid: length must be ${CONSTANTS.AES_KEY_LENGTH}`);
19
- }
20
- if (!token) {
21
- throw new Error("Token is required");
22
- }
23
- this.token = token;
24
- this.encodingAesKey = encodingAesKey;
25
- this.aesKey = Buffer.from(encodingAesKey + "=", "base64");
26
- this.iv = this.aesKey.subarray(0, 16);
27
- logger.debug("WecomCrypto initialized (AI Bot mode)");
10
+ token;
11
+ encodingAesKey;
12
+ aesKey;
13
+ iv;
14
+
15
+ constructor(token, encodingAesKey) {
16
+ if (!encodingAesKey || encodingAesKey.length !== CONSTANTS.AES_KEY_LENGTH) {
17
+ throw new Error(`EncodingAESKey invalid: length must be ${CONSTANTS.AES_KEY_LENGTH}`);
18
+ }
19
+ if (!token) {
20
+ throw new Error("Token is required");
28
21
  }
22
+ this.token = token;
23
+ this.encodingAesKey = encodingAesKey;
24
+ this.aesKey = Buffer.from(encodingAesKey + "=", "base64");
25
+ this.iv = this.aesKey.subarray(0, 16);
26
+ logger.debug("WecomCrypto initialized (AI Bot mode)");
27
+ }
29
28
 
30
- getSignature(timestamp, nonce, encrypt) {
31
- const shasum = createHash("sha1");
32
- const arr = [this.token, timestamp, nonce, encrypt].sort();
33
- shasum.update(arr.join(""));
34
- return shasum.digest("hex");
29
+ getSignature(timestamp, nonce, encrypt) {
30
+ const shasum = createHash("sha1");
31
+ // WeCom requires plain lexicographic sorting before SHA1; localeCompare is locale-sensitive.
32
+ const sorted = [this.token, timestamp, nonce, encrypt]
33
+ .map((value) => String(value))
34
+ .toSorted();
35
+ shasum.update(sorted.join(""));
36
+ return shasum.digest("hex");
37
+ }
38
+
39
+ decrypt(text) {
40
+ let decipher;
41
+ try {
42
+ decipher = createDecipheriv("aes-256-cbc", this.aesKey, this.iv);
43
+ decipher.setAutoPadding(false);
44
+ } catch (e) {
45
+ throw new Error(`Decrypt init failed: ${String(e)}`, { cause: e });
35
46
  }
36
47
 
37
- decrypt(text) {
38
- let decipher;
39
- try {
40
- decipher = createDecipheriv("aes-256-cbc", this.aesKey, this.iv);
41
- decipher.setAutoPadding(false);
42
- } catch (e) {
43
- throw new Error(`Decrypt init failed: ${String(e)}`);
44
- }
48
+ let deciphered = Buffer.concat([decipher.update(text, "base64"), decipher.final()]);
45
49
 
46
- let deciphered = Buffer.concat([
47
- decipher.update(text, "base64"),
48
- decipher.final(),
49
- ]);
50
+ deciphered = this.decodePkcs7(deciphered);
50
51
 
51
- deciphered = this.decodePkcs7(deciphered);
52
+ // Format: 16 random bytes | 4 bytes msg_len | msg_content | appid
53
+ const content = deciphered.subarray(16);
54
+ const lenList = content.subarray(0, 4);
55
+ const xmlLen = lenList.readUInt32BE(0);
56
+ const xmlContent = content.subarray(4, 4 + xmlLen).toString("utf-8");
57
+ // For AI Bot mode, corpId/appid is empty, skip validation
52
58
 
53
- // Format: 16 random bytes | 4 bytes msg_len | msg_content | appid
54
- const content = deciphered.subarray(16);
55
- const lenList = content.subarray(0, 4);
56
- const xmlLen = lenList.readUInt32BE(0);
57
- const xmlContent = content.subarray(4, 4 + xmlLen).toString("utf-8");
58
- // For AI Bot mode, corpId/appid is empty, skip validation
59
+ return { message: xmlContent };
60
+ }
59
61
 
60
- return { message: xmlContent };
61
- }
62
+ encrypt(text) {
63
+ // For AI Bot mode, corpId is empty
64
+ const random16 = randomBytes(16);
65
+ const msgBuffer = Buffer.from(text);
66
+ const lenBuffer = Buffer.alloc(4);
67
+ lenBuffer.writeUInt32BE(msgBuffer.length, 0);
62
68
 
63
- encrypt(text) {
64
- // For AI Bot mode, corpId is empty
65
- const random16 = randomBytes(16);
66
- const msgBuffer = Buffer.from(text);
67
- const lenBuffer = Buffer.alloc(4);
68
- lenBuffer.writeUInt32BE(msgBuffer.length, 0);
69
+ const rawMsg = Buffer.concat([random16, lenBuffer, msgBuffer]);
70
+ const encoded = this.encodePkcs7(rawMsg);
69
71
 
70
- const rawMsg = Buffer.concat([random16, lenBuffer, msgBuffer]);
71
- const encoded = this.encodePkcs7(rawMsg);
72
+ const cipher = createCipheriv("aes-256-cbc", this.aesKey, this.iv);
73
+ cipher.setAutoPadding(false);
74
+ const ciphered = Buffer.concat([cipher.update(encoded), cipher.final()]);
75
+ return ciphered.toString("base64");
76
+ }
72
77
 
73
- const cipher = createCipheriv("aes-256-cbc", this.aesKey, this.iv);
74
- cipher.setAutoPadding(false);
75
- const ciphered = Buffer.concat([cipher.update(encoded), cipher.final()]);
76
- return ciphered.toString("base64");
77
- }
78
+ encodePkcs7(buff) {
79
+ const blockSize = CONSTANTS.AES_BLOCK_SIZE;
80
+ const amountToPad = blockSize - (buff.length % blockSize);
81
+ const pad = Buffer.alloc(amountToPad, amountToPad);
82
+ return Buffer.concat([buff, pad]);
83
+ }
78
84
 
79
- encodePkcs7(buff) {
80
- const blockSize = CONSTANTS.AES_BLOCK_SIZE;
81
- const amountToPad = blockSize - (buff.length % blockSize);
82
- const pad = Buffer.alloc(amountToPad, amountToPad);
83
- return Buffer.concat([buff, pad]);
85
+ decodePkcs7(buff) {
86
+ const pad = buff[buff.length - 1];
87
+ if (pad < 1 || pad > CONSTANTS.AES_BLOCK_SIZE) {
88
+ throw new Error(`Invalid PKCS7 padding: ${pad}`);
84
89
  }
90
+ for (let i = buff.length - pad; i < buff.length; i++) {
91
+ if (buff[i] !== pad) {
92
+ throw new Error("Invalid PKCS7 padding: inconsistent padding bytes");
93
+ }
94
+ }
95
+ return buff.subarray(0, buff.length - pad);
96
+ }
85
97
 
86
- decodePkcs7(buff) {
87
- const pad = buff[buff.length - 1];
88
- if (pad < 1 || pad > CONSTANTS.AES_BLOCK_SIZE) {
89
- throw new Error(`Invalid PKCS7 padding: ${pad}`);
90
- }
91
- for (let i = buff.length - pad; i < buff.length; i++) {
92
- if (buff[i] !== pad) {
93
- throw new Error("Invalid PKCS7 padding: inconsistent padding bytes");
94
- }
98
+ /**
99
+ * Decrypt image/media file from Enterprise WeChat.
100
+ * Images are encrypted with AES-256-CBC using the same key as messages.
101
+ * Note: WeCom uses PKCS7 padding to 32-byte blocks (not standard 16-byte).
102
+ * @param {Buffer} encryptedData - The encrypted image data (raw bytes, not base64)
103
+ * @returns {Buffer} - Decrypted image data
104
+ */
105
+ decryptMedia(encryptedData) {
106
+ const decipher = createDecipheriv("aes-256-cbc", this.aesKey, this.iv);
107
+ decipher.setAutoPadding(false);
108
+ const decrypted = Buffer.concat([
109
+ decipher.update(encryptedData),
110
+ decipher.final(),
111
+ ]);
112
+
113
+ // Remove PKCS7 padding manually (padded to 32-byte blocks).
114
+ const padLen = decrypted[decrypted.length - 1];
115
+ let unpadded = decrypted;
116
+ if (padLen >= 1 && padLen <= 32) {
117
+ let validPadding = true;
118
+ for (let i = decrypted.length - padLen; i < decrypted.length; i++) {
119
+ if (decrypted[i] !== padLen) {
120
+ validPadding = false;
121
+ break;
95
122
  }
96
- return buff.subarray(0, buff.length - pad);
123
+ }
124
+ if (validPadding) {
125
+ unpadded = decrypted.subarray(0, decrypted.length - padLen);
126
+ }
97
127
  }
98
- }
99
128
 
100
- export const xmlParser = new XMLParser({
101
- ignoreAttributes: true,
102
- parseTagValue: false
103
- });
104
-
105
- export const xmlBuilder = new XMLBuilder({
106
- format: false,
107
- ignoreAttributes: true
108
- });
129
+ logger.debug("Media decrypted successfully", {
130
+ inputSize: encryptedData.length,
131
+ outputSize: unpadded.length,
132
+ });
133
+ return unpadded;
134
+ }
135
+ }
package/dynamic-agent.js CHANGED
@@ -1,120 +1,103 @@
1
- import { logger } from "./logger.js";
2
-
3
1
  /**
4
- * Dynamic Agent Manager (Minimal Version)
5
- *
6
- * 极简版:插件只负责生成 AgentId
7
- * 所有 Workspace 创建和 Bootstrap 文件由 OpenClaw 主程序自动处理
8
- *
9
- * 流程:
10
- * 1. 插件收到消息 → generateAgentId() 生成 agentId
11
- * 2. 插件构造 SessionKey → `agent:{agentId}:{peerKind}:{peerId}`
12
- * 3. OpenClaw 解析 SessionKey → 提取 agentId
13
- * 4. OpenClaw 调用 resolveAgentWorkspaceDir() → fallback 到 ~/.openclaw/workspace-{agentId}
14
- * 5. OpenClaw 调用 ensureAgentWorkspace() → 自动创建目录和 Bootstrap 文件
2
+ * Dynamic agent helpers.
3
+ *
4
+ * This plugin only computes deterministic agent ids/session keys.
5
+ * Workspace/bootstrap creation is handled by OpenClaw core.
15
6
  */
16
7
 
17
8
  /**
18
- * 生成 AgentId
19
- * 规范:wecom-dm-{userId} 或 wecom-group-{groupId}
20
- *
21
- * @param {string} chatType - "dm" "group"
22
- * @param {string} peerId - userId 或 groupId
9
+ * Build a deterministic agent id for dm/group contexts.
10
+ *
11
+ * @param {string} chatType - "dm" or "group"
12
+ * @param {string} peerId - user id or group id
23
13
  * @returns {string} agentId
24
14
  */
25
15
  export function generateAgentId(chatType, peerId) {
26
- const sanitizedId = String(peerId).toLowerCase().replace(/[^a-z0-9_-]/g, "_");
27
- if (chatType === "group") {
28
- return `wecom-group-${sanitizedId}`;
29
- }
30
- return `wecom-dm-${sanitizedId}`;
16
+ const sanitizedId = String(peerId)
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9_-]/g, "_");
19
+ if (chatType === "group") {
20
+ return `wecom-group-${sanitizedId}`;
21
+ }
22
+ return `wecom-dm-${sanitizedId}`;
31
23
  }
32
24
 
33
25
  /**
34
- * 获取动态 Agent 配置
26
+ * Resolve runtime dynamic-agent settings from config.
35
27
  */
36
28
  export function getDynamicAgentConfig(config) {
37
- const wecom = config?.channels?.wecom || {};
38
- return {
39
- enabled: wecom.dynamicAgents?.enabled !== false,
40
-
41
- // 私聊配置
42
- dmCreateAgent: wecom.dm?.createAgentOnFirstMessage !== false,
43
-
44
- // 群聊配置
45
- groupEnabled: wecom.groupChat?.enabled !== false,
46
- groupRequireMention: wecom.groupChat?.requireMention !== false,
47
- groupMentionPatterns: wecom.groupChat?.mentionPatterns || ["@"],
48
- groupCreateAgent: wecom.groupChat?.createAgentOnFirstMessage !== false,
49
- groupHistoryLimit: wecom.groupChat?.historyLimit || 10,
50
- };
29
+ const wecom = config?.channels?.wecom || {};
30
+ return {
31
+ enabled: wecom.dynamicAgents?.enabled !== false,
32
+ dmCreateAgent: wecom.dm?.createAgentOnFirstMessage !== false,
33
+ groupEnabled: wecom.groupChat?.enabled !== false,
34
+ groupRequireMention: wecom.groupChat?.requireMention !== false,
35
+ groupMentionPatterns: wecom.groupChat?.mentionPatterns || ["@"],
36
+ };
51
37
  }
52
38
 
53
39
  /**
54
- * 检查是否应该为此消息创建/使用动态 Agent
55
- *
56
- * @param {Object} options
57
- * @param {string} options.chatType - "dm" 或 "group"
58
- * @param {Object} options.config - openclaw 配置
59
- * @returns {boolean}
40
+ * Decide whether this message context should route to a dynamic agent.
60
41
  */
61
42
  export function shouldUseDynamicAgent({ chatType, config }) {
62
- const dynamicConfig = getDynamicAgentConfig(config);
63
-
64
- if (!dynamicConfig.enabled) {
65
- return false;
66
- }
67
-
68
- if (chatType === "dm") {
69
- return dynamicConfig.dmCreateAgent;
70
- }
71
-
72
- if (chatType === "group") {
73
- return dynamicConfig.groupCreateAgent;
74
- }
75
-
43
+ const dynamicConfig = getDynamicAgentConfig(config);
44
+ if (!dynamicConfig.enabled) {
76
45
  return false;
46
+ }
47
+ if (chatType === "group") {
48
+ return dynamicConfig.groupEnabled;
49
+ }
50
+ return dynamicConfig.dmCreateAgent;
77
51
  }
78
52
 
79
53
  /**
80
- * 检查群聊消息是否满足触发条件(@提及)
54
+ * Decide whether a group message should trigger a response.
81
55
  */
82
56
  export function shouldTriggerGroupResponse(content, config) {
83
- const dynamicConfig = getDynamicAgentConfig(config);
84
-
85
- if (!dynamicConfig.groupEnabled) {
86
- return false;
87
- }
88
-
89
- if (!dynamicConfig.groupRequireMention) {
90
- return true; // 不需要 @,所有消息都触发
91
- }
57
+ const dynamicConfig = getDynamicAgentConfig(config);
92
58
 
93
- // 检查是否包含 @提及
94
- const patterns = dynamicConfig.groupMentionPatterns;
95
- for (const pattern of patterns) {
96
- if (content.includes(pattern)) {
97
- return true;
98
- }
59
+ if (!dynamicConfig.groupEnabled) {
60
+ return false;
61
+ }
62
+
63
+ if (!dynamicConfig.groupRequireMention) {
64
+ return true;
65
+ }
66
+
67
+ // Match any configured mention marker in the original message content.
68
+ // Use word-boundary check to avoid false positives on email addresses.
69
+ const patterns = dynamicConfig.groupMentionPatterns;
70
+ for (const pattern of patterns) {
71
+ const escaped = escapeRegExp(pattern);
72
+ // @ must NOT be preceded by a word char (avoids user@domain false matches).
73
+ const re = new RegExp(`(?:^|(?<=\\s|[^\\w]))${escaped}`, "u");
74
+ if (re.test(content)) {
75
+ return true;
99
76
  }
77
+ }
100
78
 
101
- return false;
79
+ return false;
102
80
  }
103
81
 
104
82
  /**
105
- * 从群聊消息中提取实际内容(移除 @提及)
83
+ * Remove configured mention markers from group message text.
106
84
  */
107
85
  export function extractGroupMessageContent(content, config) {
108
- const dynamicConfig = getDynamicAgentConfig(config);
109
- let cleanContent = content;
110
-
111
- // 移除 @提及 pattern
112
- const patterns = dynamicConfig.groupMentionPatterns;
113
- for (const pattern of patterns) {
114
- // 移除 @xxx 格式的提及(包括后面可能的空格)
115
- const regex = new RegExp(`${pattern}\\S*\\s*`, "g");
116
- cleanContent = cleanContent.replace(regex, "");
117
- }
86
+ const dynamicConfig = getDynamicAgentConfig(config);
87
+ let cleanContent = content;
88
+
89
+ const patterns = dynamicConfig.groupMentionPatterns;
90
+ for (const pattern of patterns) {
91
+ const escapedPattern = escapeRegExp(pattern);
92
+ // Only strip @name tokens that are NOT part of email-style addresses.
93
+ // Require the pattern to be preceded by start-of-string or whitespace.
94
+ const regex = new RegExp(`(?:^|(?<=\\s))${escapedPattern}\\S*\\s*`, "gu");
95
+ cleanContent = cleanContent.replace(regex, "");
96
+ }
97
+
98
+ return cleanContent.trim();
99
+ }
118
100
 
119
- return cleanContent.trim();
101
+ function escapeRegExp(value) {
102
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
120
103
  }
@@ -1,5 +1,5 @@
1
- import { readFile } from "fs/promises";
2
1
  import { createHash } from "crypto";
2
+ import { readFile } from "fs/promises";
3
3
  import { logger } from "./logger.js";
4
4
 
5
5
  /**
@@ -11,8 +11,8 @@ import { logger } from "./logger.js";
11
11
 
12
12
  // Image format signatures (magic bytes)
13
13
  const IMAGE_SIGNATURES = {
14
- JPG: [0xFF, 0xD8, 0xFF],
15
- PNG: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
14
+ JPG: [0xff, 0xd8, 0xff],
15
+ PNG: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
16
16
  };
17
17
 
18
18
  // 2MB size limit (before base64 encoding)
@@ -25,23 +25,23 @@ const MAX_IMAGE_SIZE = 2 * 1024 * 1024;
25
25
  * @throws {Error} If file not found or cannot be read
26
26
  */
27
27
  export async function loadImageFromPath(filePath) {
28
- try {
29
- logger.debug("Loading image from path", { filePath });
30
- const buffer = await readFile(filePath);
31
- logger.debug("Image loaded successfully", {
32
- filePath,
33
- size: buffer.length
34
- });
35
- return buffer;
36
- } catch (error) {
37
- if (error.code === "ENOENT") {
38
- throw new Error(`Image file not found: ${filePath}`);
39
- } else if (error.code === "EACCES") {
40
- throw new Error(`Permission denied reading image: ${filePath}`);
41
- } else {
42
- throw new Error(`Failed to read image file: ${error.message}`);
43
- }
28
+ try {
29
+ logger.debug("Loading image from path", { filePath });
30
+ const buffer = await readFile(filePath);
31
+ logger.debug("Image loaded successfully", {
32
+ filePath,
33
+ size: buffer.length,
34
+ });
35
+ return buffer;
36
+ } catch (error) {
37
+ if (error.code === "ENOENT") {
38
+ throw new Error(`Image file not found: ${filePath}`, { cause: error });
39
+ } else if (error.code === "EACCES") {
40
+ throw new Error(`Permission denied reading image: ${filePath}`, { cause: error });
41
+ } else {
42
+ throw new Error(`Failed to read image file: ${error.message}`, { cause: error });
44
43
  }
44
+ }
45
45
  }
46
46
 
47
47
  /**
@@ -50,7 +50,7 @@ export async function loadImageFromPath(filePath) {
50
50
  * @returns {string} Base64-encoded string
51
51
  */
52
52
  export function encodeImageToBase64(buffer) {
53
- return buffer.toString("base64");
53
+ return buffer.toString("base64");
54
54
  }
55
55
 
56
56
  /**
@@ -59,7 +59,7 @@ export function encodeImageToBase64(buffer) {
59
59
  * @returns {string} MD5 hash in hexadecimal
60
60
  */
61
61
  export function calculateMD5(buffer) {
62
- return createHash("md5").update(buffer).digest("hex");
62
+ return createHash("md5").update(buffer).digest("hex");
63
63
  }
64
64
 
65
65
  /**
@@ -68,16 +68,14 @@ export function calculateMD5(buffer) {
68
68
  * @throws {Error} If size exceeds 2MB limit
69
69
  */
70
70
  export function validateImageSize(buffer) {
71
- const sizeBytes = buffer.length;
72
- const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
71
+ const sizeBytes = buffer.length;
72
+ const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
73
73
 
74
- if (sizeBytes > MAX_IMAGE_SIZE) {
75
- throw new Error(
76
- `Image size ${sizeMB}MB exceeds 2MB limit (actual: ${sizeBytes} bytes)`
77
- );
78
- }
74
+ if (sizeBytes > MAX_IMAGE_SIZE) {
75
+ throw new Error(`Image size ${sizeMB}MB exceeds 2MB limit (actual: ${sizeBytes} bytes)`);
76
+ }
79
77
 
80
- logger.debug("Image size validated", { sizeBytes, sizeMB });
78
+ logger.debug("Image size validated", { sizeBytes, sizeMB });
81
79
  }
82
80
 
83
81
  /**
@@ -87,34 +85,29 @@ export function validateImageSize(buffer) {
87
85
  * @throws {Error} If format is not supported
88
86
  */
89
87
  export function detectImageFormat(buffer) {
90
- // Check PNG signature
91
- if (buffer.length >= IMAGE_SIGNATURES.PNG.length) {
92
- const isPNG = IMAGE_SIGNATURES.PNG.every(
93
- (byte, index) => buffer[index] === byte
94
- );
95
- if (isPNG) {
96
- logger.debug("Image format detected: PNG");
97
- return "PNG";
98
- }
88
+ // Check PNG signature
89
+ if (buffer.length >= IMAGE_SIGNATURES.PNG.length) {
90
+ const isPNG = IMAGE_SIGNATURES.PNG.every((byte, index) => buffer[index] === byte);
91
+ if (isPNG) {
92
+ logger.debug("Image format detected: PNG");
93
+ return "PNG";
99
94
  }
100
-
101
- // Check JPG signature
102
- if (buffer.length >= IMAGE_SIGNATURES.JPG.length) {
103
- const isJPG = IMAGE_SIGNATURES.JPG.every(
104
- (byte, index) => buffer[index] === byte
105
- );
106
- if (isJPG) {
107
- logger.debug("Image format detected: JPG");
108
- return "JPG";
109
- }
95
+ }
96
+
97
+ // Check JPG signature
98
+ if (buffer.length >= IMAGE_SIGNATURES.JPG.length) {
99
+ const isJPG = IMAGE_SIGNATURES.JPG.every((byte, index) => buffer[index] === byte);
100
+ if (isJPG) {
101
+ logger.debug("Image format detected: JPG");
102
+ return "JPG";
110
103
  }
104
+ }
111
105
 
112
- // Unknown format
113
- const header = buffer.slice(0, 16).toString("hex");
114
- throw new Error(
115
- `Unsupported image format. Only JPG and PNG are supported. ` +
116
- `File header: ${header}`
117
- );
106
+ // Unknown format
107
+ const header = buffer.subarray(0, 16).toString("hex");
108
+ throw new Error(
109
+ `Unsupported image format. Only JPG and PNG are supported. File header: ${header}`,
110
+ );
118
111
  }
119
112
 
120
113
  /**
@@ -137,43 +130,43 @@ export function detectImageFormat(buffer) {
137
130
  * // Returns: { base64: "...", md5: "...", format: "JPG", size: 123456 }
138
131
  */
139
132
  export async function prepareImageForMsgItem(filePath) {
140
- logger.debug("Starting image processing pipeline", { filePath });
141
-
142
- try {
143
- // Step 1: Load image
144
- const buffer = await loadImageFromPath(filePath);
145
-
146
- // Step 2: Validate size
147
- validateImageSize(buffer);
148
-
149
- // Step 3: Detect format
150
- const format = detectImageFormat(buffer);
151
-
152
- // Step 4: Encode to base64
153
- const base64 = encodeImageToBase64(buffer);
154
-
155
- // Step 5: Calculate MD5
156
- const md5 = calculateMD5(buffer);
157
-
158
- logger.info("Image processed successfully", {
159
- filePath,
160
- format,
161
- size: buffer.length,
162
- md5,
163
- base64Length: base64.length
164
- });
165
-
166
- return {
167
- base64,
168
- md5,
169
- format,
170
- size: buffer.length
171
- };
172
- } catch (error) {
173
- logger.error("Image processing failed", {
174
- filePath,
175
- error: error.message
176
- });
177
- throw error;
178
- }
133
+ logger.debug("Starting image processing pipeline", { filePath });
134
+
135
+ try {
136
+ // Step 1: Load image
137
+ const buffer = await loadImageFromPath(filePath);
138
+
139
+ // Step 2: Validate size
140
+ validateImageSize(buffer);
141
+
142
+ // Step 3: Detect format
143
+ const format = detectImageFormat(buffer);
144
+
145
+ // Step 4: Encode to base64
146
+ const base64 = encodeImageToBase64(buffer);
147
+
148
+ // Step 5: Calculate MD5
149
+ const md5 = calculateMD5(buffer);
150
+
151
+ logger.info("Image processed successfully", {
152
+ filePath,
153
+ format,
154
+ size: buffer.length,
155
+ md5,
156
+ base64Length: base64.length,
157
+ });
158
+
159
+ return {
160
+ base64,
161
+ md5,
162
+ format,
163
+ size: buffer.length,
164
+ };
165
+ } catch (error) {
166
+ logger.error("Image processing failed", {
167
+ filePath,
168
+ error: error.message,
169
+ });
170
+ throw error;
171
+ }
179
172
  }