@sunnoy/wecom 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -845
- package/image-processor.js +30 -27
- package/index.js +10 -43
- package/package.json +5 -5
- package/think-parser.js +51 -11
- package/wecom/accounts.js +323 -189
- package/wecom/channel-plugin.js +543 -750
- package/wecom/constants.js +57 -47
- package/wecom/dm-policy.js +91 -0
- package/wecom/group-policy.js +85 -0
- package/wecom/onboarding.js +117 -0
- package/wecom/runtime-telemetry.js +330 -0
- package/wecom/sandbox.js +60 -0
- package/wecom/state.js +33 -35
- package/wecom/workspace-template.js +62 -5
- package/wecom/ws-monitor.js +1487 -0
- package/wecom/ws-state.js +160 -0
- package/crypto.js +0 -135
- package/stream-manager.js +0 -358
- package/webhook.js +0 -469
- package/wecom/agent-inbound.js +0 -541
- package/wecom/http-handler-state.js +0 -23
- package/wecom/http-handler.js +0 -395
- package/wecom/inbound-processor.js +0 -562
- package/wecom/media.js +0 -192
- package/wecom/outbound-delivery.js +0 -435
- package/wecom/response-url.js +0 -33
- package/wecom/stream-utils.js +0 -163
- package/wecom/webhook-targets.js +0 -28
- package/wecom/xml-parser.js +0 -126
package/image-processor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { logger } from "./logger.js";
|
|
|
6
6
|
* Image Processing Module for WeCom
|
|
7
7
|
*
|
|
8
8
|
* Handles loading, validating, and encoding images for WeCom msg_item
|
|
9
|
-
* Supports JPG and PNG formats up to
|
|
9
|
+
* Supports JPG and PNG formats up to 10MB
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
// Image format signatures (magic bytes)
|
|
@@ -15,8 +15,27 @@ const IMAGE_SIGNATURES = {
|
|
|
15
15
|
PNG: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
const MAX_IMAGE_SIZE =
|
|
18
|
+
// 10MB size limit (before base64 encoding)
|
|
19
|
+
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Process a raw image buffer into a WeCom msg_item payload.
|
|
23
|
+
* @param {Buffer} buffer
|
|
24
|
+
* @returns {{ base64: string, md5: string, format: string, size: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function prepareImageBufferForMsgItem(buffer) {
|
|
27
|
+
validateImageSize(buffer);
|
|
28
|
+
const format = detectImageFormat(buffer);
|
|
29
|
+
const base64 = encodeImageToBase64(buffer);
|
|
30
|
+
const md5 = calculateMD5(buffer);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
base64,
|
|
34
|
+
md5,
|
|
35
|
+
format,
|
|
36
|
+
size: buffer.length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
/**
|
|
22
41
|
* Load image file from filesystem
|
|
@@ -65,14 +84,14 @@ export function calculateMD5(buffer) {
|
|
|
65
84
|
/**
|
|
66
85
|
* Validate image size is within limits
|
|
67
86
|
* @param {Buffer} buffer - Image data buffer
|
|
68
|
-
* @throws {Error} If size exceeds
|
|
87
|
+
* @throws {Error} If size exceeds 10MB limit
|
|
69
88
|
*/
|
|
70
89
|
export function validateImageSize(buffer) {
|
|
71
90
|
const sizeBytes = buffer.length;
|
|
72
91
|
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
73
92
|
|
|
74
93
|
if (sizeBytes > MAX_IMAGE_SIZE) {
|
|
75
|
-
throw new Error(`Image size ${sizeMB}MB exceeds
|
|
94
|
+
throw new Error(`Image size ${sizeMB}MB exceeds 10MB limit (actual: ${sizeBytes} bytes)`);
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
logger.debug("Image size validated", { sizeBytes, sizeMB });
|
|
@@ -135,33 +154,17 @@ export async function prepareImageForMsgItem(filePath) {
|
|
|
135
154
|
try {
|
|
136
155
|
// Step 1: Load image
|
|
137
156
|
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);
|
|
157
|
+
const result = prepareImageBufferForMsgItem(buffer);
|
|
150
158
|
|
|
151
159
|
logger.info("Image processed successfully", {
|
|
152
160
|
filePath,
|
|
153
|
-
format,
|
|
154
|
-
size:
|
|
155
|
-
md5,
|
|
156
|
-
base64Length: base64.length,
|
|
161
|
+
format: result.format,
|
|
162
|
+
size: result.size,
|
|
163
|
+
md5: result.md5,
|
|
164
|
+
base64Length: result.base64.length,
|
|
157
165
|
});
|
|
158
166
|
|
|
159
|
-
return
|
|
160
|
-
base64,
|
|
161
|
-
md5,
|
|
162
|
-
format,
|
|
163
|
-
size: buffer.length,
|
|
164
|
-
};
|
|
167
|
+
return result;
|
|
165
168
|
} catch (error) {
|
|
166
169
|
logger.error("Image processing failed", {
|
|
167
170
|
filePath,
|
package/index.js
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
1
2
|
import { logger } from "./logger.js";
|
|
2
|
-
import { streamManager } from "./stream-manager.js";
|
|
3
3
|
import { wecomChannelPlugin } from "./wecom/channel-plugin.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
function emptyPluginConfigSchema() {
|
|
8
|
-
return {
|
|
9
|
-
safeParse(value) {
|
|
10
|
-
if (value === undefined) return { success: true, data: undefined };
|
|
11
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
-
return { success: false, error: { message: "expected config object" } };
|
|
13
|
-
}
|
|
14
|
-
return { success: true, data: value };
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let cleanupTimer = null;
|
|
4
|
+
import { setOpenclawConfig, setRuntime } from "./wecom/state.js";
|
|
5
|
+
import { buildReplyMediaGuidance } from "./wecom/ws-monitor.js";
|
|
20
6
|
|
|
21
7
|
const plugin = {
|
|
22
8
|
id: "wecom",
|
|
@@ -24,37 +10,18 @@ const plugin = {
|
|
|
24
10
|
description: "Enterprise WeChat AI Bot channel plugin for OpenClaw",
|
|
25
11
|
configSchema: emptyPluginConfigSchema(),
|
|
26
12
|
register(api) {
|
|
27
|
-
logger.info("WeCom plugin
|
|
28
|
-
|
|
13
|
+
logger.info("Registering WeCom WS plugin");
|
|
29
14
|
setRuntime(api.runtime);
|
|
30
15
|
setOpenclawConfig(api.config);
|
|
31
|
-
|
|
32
|
-
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
33
|
-
cleanupTimer = setInterval(() => {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
for (const streamId of streamMeta.keys()) {
|
|
36
|
-
if (!streamManager.hasStream(streamId)) {
|
|
37
|
-
streamMeta.delete(streamId);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
for (const [key, entry] of responseUrls.entries()) {
|
|
41
|
-
if (now > entry.expiresAt) {
|
|
42
|
-
responseUrls.delete(key);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}, 60_000);
|
|
46
|
-
cleanupTimer.unref();
|
|
47
|
-
|
|
48
16
|
api.registerChannel({ plugin: wecomChannelPlugin });
|
|
49
|
-
logger.info("WeCom channel registered");
|
|
50
17
|
|
|
51
|
-
api.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
18
|
+
api.on("before_prompt_build", (_event, ctx) => {
|
|
19
|
+
if (ctx.channelId !== "wecom") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const guidance = buildReplyMediaGuidance(api.config, ctx.agentId);
|
|
23
|
+
return { appendSystemContext: guidance };
|
|
56
24
|
});
|
|
57
|
-
logger.info("WeCom HTTP route registered (auth: plugin, match: prefix)");
|
|
58
25
|
},
|
|
59
26
|
};
|
|
60
27
|
|
package/package.json
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunnoy/wecom",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Enterprise WeChat AI Bot channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
9
9
|
"wecom",
|
|
10
|
-
"crypto.js",
|
|
11
10
|
"dynamic-agent.js",
|
|
12
11
|
"image-processor.js",
|
|
13
12
|
"logger.js",
|
|
14
13
|
"README.md",
|
|
15
14
|
"LICENSE",
|
|
16
15
|
"CONTRIBUTING.md",
|
|
17
|
-
"stream-manager.js",
|
|
18
16
|
"think-parser.js",
|
|
19
17
|
"utils.js",
|
|
20
|
-
"webhook.js",
|
|
21
18
|
"openclaw.plugin.json"
|
|
22
19
|
],
|
|
23
20
|
"peerDependencies": {
|
|
@@ -60,5 +57,8 @@
|
|
|
60
57
|
"plugin"
|
|
61
58
|
],
|
|
62
59
|
"author": "",
|
|
63
|
-
"license": "ISC"
|
|
60
|
+
"license": "ISC",
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@wecom/aibot-node-sdk": "^1.0.1"
|
|
63
|
+
}
|
|
64
64
|
}
|
package/think-parser.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse <think>...</think> tags from LLM output.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles WeCom thinking tags in streamed LLM output:
|
|
5
|
+
* - normalize tag variants to <think></think> for outbound WS content
|
|
6
|
+
* - split visible/thinking text for analysis and tests
|
|
7
|
+
* - ignore tags inside code blocks
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const QUICK_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought)\b/i;
|
|
@@ -43,13 +44,50 @@ function isInsideRegion(pos, regions) {
|
|
|
43
44
|
return false;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Normalize think tag variants to the canonical <think></think> form that
|
|
49
|
+
* WeCom clients recognize, while leaving code blocks untouched.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} text
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeThinkingTags(text) {
|
|
55
|
+
if (!text) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!QUICK_TAG_RE.test(text)) {
|
|
60
|
+
return String(text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const source = String(text);
|
|
64
|
+
const codeRegions = findCodeRegions(source);
|
|
65
|
+
const normalized = [];
|
|
66
|
+
let lastIndex = 0;
|
|
67
|
+
|
|
68
|
+
THINK_TAG_RE.lastIndex = 0;
|
|
69
|
+
for (const match of source.matchAll(THINK_TAG_RE)) {
|
|
70
|
+
const idx = match.index;
|
|
71
|
+
if (isInsideRegion(idx, codeRegions)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
normalized.push(source.slice(lastIndex, idx));
|
|
76
|
+
normalized.push(match[1] === "/" ? "</think>" : "<think>");
|
|
77
|
+
lastIndex = idx + match[0].length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
normalized.push(source.slice(lastIndex));
|
|
81
|
+
return normalized.join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
* Parse thinking content from text that may contain <think>...</think> tags.
|
|
48
86
|
*
|
|
49
87
|
* @param {string} text - Raw accumulated stream text
|
|
50
88
|
* @returns {{ visibleContent: string, thinkingContent: string, isThinking: boolean }}
|
|
51
|
-
* - visibleContent: text with think blocks removed
|
|
52
|
-
* - thinkingContent: concatenated thinking text
|
|
89
|
+
* - visibleContent: text with think blocks removed
|
|
90
|
+
* - thinkingContent: concatenated thinking text
|
|
53
91
|
* - isThinking: true when an unclosed <think> tag is present (streaming)
|
|
54
92
|
*/
|
|
55
93
|
export function parseThinkingContent(text) {
|
|
@@ -57,12 +95,14 @@ export function parseThinkingContent(text) {
|
|
|
57
95
|
return { visibleContent: "", thinkingContent: "", isThinking: false };
|
|
58
96
|
}
|
|
59
97
|
|
|
98
|
+
const source = String(text);
|
|
99
|
+
|
|
60
100
|
// Fast path: no think tags at all.
|
|
61
|
-
if (!QUICK_TAG_RE.test(
|
|
62
|
-
return { visibleContent:
|
|
101
|
+
if (!QUICK_TAG_RE.test(source)) {
|
|
102
|
+
return { visibleContent: source, thinkingContent: "", isThinking: false };
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
const codeRegions = findCodeRegions(
|
|
105
|
+
const codeRegions = findCodeRegions(source);
|
|
66
106
|
|
|
67
107
|
const visibleParts = [];
|
|
68
108
|
const thinkingParts = [];
|
|
@@ -70,7 +110,7 @@ export function parseThinkingContent(text) {
|
|
|
70
110
|
let inThinking = false;
|
|
71
111
|
|
|
72
112
|
THINK_TAG_RE.lastIndex = 0;
|
|
73
|
-
for (const match of
|
|
113
|
+
for (const match of source.matchAll(THINK_TAG_RE)) {
|
|
74
114
|
const idx = match.index;
|
|
75
115
|
const isClose = match[1] === "/";
|
|
76
116
|
|
|
@@ -79,7 +119,7 @@ export function parseThinkingContent(text) {
|
|
|
79
119
|
continue;
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
const segment =
|
|
122
|
+
const segment = source.slice(lastIndex, idx);
|
|
83
123
|
|
|
84
124
|
if (!inThinking) {
|
|
85
125
|
if (!isClose) {
|
|
@@ -103,7 +143,7 @@ export function parseThinkingContent(text) {
|
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
// Remaining text after the last tag.
|
|
106
|
-
const remaining =
|
|
146
|
+
const remaining = source.slice(lastIndex);
|
|
107
147
|
if (inThinking) {
|
|
108
148
|
// Unclosed <think>: remaining text is part of thinking (streaming state).
|
|
109
149
|
thinkingParts.push(remaining);
|