@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.
@@ -1,163 +0,0 @@
1
- import { logger } from "../logger.js";
2
- import { streamManager } from "../stream-manager.js";
3
- import { THINKING_PLACEHOLDER } from "./constants.js";
4
- import { activeStreamHistory, activeStreams, lastStreamByKey, messageBuffers } from "./state.js";
5
-
6
- export function getMessageStreamKey(message) {
7
- if (!message || typeof message !== "object") {
8
- return "";
9
- }
10
- const chatType = message.chatType || "single";
11
- const chatId = message.chatId || "";
12
- if (chatType === "group" && chatId) {
13
- return chatId;
14
- }
15
- return message.fromUser || "";
16
- }
17
-
18
- export function registerActiveStream(streamKey, streamId) {
19
- if (!streamKey || !streamId) {
20
- return;
21
- }
22
-
23
- const history = activeStreamHistory.get(streamKey) ?? [];
24
- const deduped = history.filter((id) => id !== streamId);
25
- deduped.push(streamId);
26
- activeStreamHistory.set(streamKey, deduped);
27
- activeStreams.set(streamKey, streamId);
28
- lastStreamByKey.set(streamKey, streamId);
29
- logger.info("registerActiveStream", {
30
- streamKey,
31
- streamId,
32
- historySize: deduped.length,
33
- history: deduped,
34
- });
35
- }
36
-
37
- export function unregisterActiveStream(streamKey, streamId) {
38
- if (!streamKey || !streamId) {
39
- return;
40
- }
41
-
42
- const history = activeStreamHistory.get(streamKey);
43
- if (!history || history.length === 0) {
44
- if (activeStreams.get(streamKey) === streamId) {
45
- activeStreams.delete(streamKey);
46
- }
47
- logger.info("unregisterActiveStream (empty history)", { streamKey, streamId });
48
- return;
49
- }
50
-
51
- const remaining = history.filter((id) => id !== streamId);
52
- if (remaining.length === 0) {
53
- activeStreamHistory.delete(streamKey);
54
- activeStreams.delete(streamKey);
55
- logger.info("unregisterActiveStream (last stream)", { streamKey, streamId });
56
- return;
57
- }
58
-
59
- activeStreamHistory.set(streamKey, remaining);
60
- activeStreams.set(streamKey, remaining[remaining.length - 1]);
61
- logger.info("unregisterActiveStream", {
62
- streamKey,
63
- streamId,
64
- remainingSize: remaining.length,
65
- remaining,
66
- });
67
- }
68
-
69
- export function resolveActiveStream(streamKey) {
70
- if (!streamKey) {
71
- return null;
72
- }
73
-
74
- const history = activeStreamHistory.get(streamKey);
75
- if (!history || history.length === 0) {
76
- activeStreams.delete(streamKey);
77
- return null;
78
- }
79
-
80
- const remaining = history.filter((id) => streamManager.hasStream(id));
81
- if (remaining.length === 0) {
82
- activeStreamHistory.delete(streamKey);
83
- activeStreams.delete(streamKey);
84
- return null;
85
- }
86
-
87
- activeStreamHistory.set(streamKey, remaining);
88
- const latest = remaining[remaining.length - 1];
89
- activeStreams.set(streamKey, latest);
90
- lastStreamByKey.set(streamKey, latest);
91
- return latest;
92
- }
93
-
94
- /**
95
- * Resolve a usable stream id for a sender/group.
96
- * Prefer active history; if that is temporarily empty, fall back to the latest
97
- * known stream id for the same key (when it still exists).
98
- */
99
- export function resolveRecoverableStream(streamKey) {
100
- const activeId = resolveActiveStream(streamKey);
101
- if (activeId) {
102
- return activeId;
103
- }
104
- if (!streamKey) {
105
- return null;
106
- }
107
- const recentId = lastStreamByKey.get(streamKey);
108
- if (!recentId) {
109
- return null;
110
- }
111
- if (!streamManager.hasStream(recentId)) {
112
- return null;
113
- }
114
- return recentId;
115
- }
116
-
117
- export function clearBufferedMessagesForStream(streamKey, reason) {
118
- const buffer = messageBuffers.get(streamKey);
119
- if (!buffer) {
120
- return 0;
121
- }
122
-
123
- messageBuffers.delete(streamKey);
124
- clearTimeout(buffer.timer);
125
-
126
- const notice = reason || "消息已被高优先级指令中断。";
127
- let drained = 0;
128
- for (const bufferedStreamId of buffer.streamIds || []) {
129
- if (!bufferedStreamId) {
130
- continue;
131
- }
132
- drained += 1;
133
- streamManager.replaceIfPlaceholder(bufferedStreamId, notice, THINKING_PLACEHOLDER);
134
- streamManager.finishStream(bufferedStreamId).then(() => {
135
- unregisterActiveStream(streamKey, bufferedStreamId);
136
- }).catch((err) => {
137
- logger.warn("WeCom: failed finishing buffered stream", {
138
- streamKey,
139
- streamId: bufferedStreamId,
140
- error: err?.message || String(err),
141
- });
142
- });
143
- }
144
-
145
- return drained;
146
- }
147
-
148
- /**
149
- * Handle stream error: replace placeholder with error message, finish stream, unregister.
150
- */
151
- export async function handleStreamError(streamId, streamKey, errorMessage) {
152
- if (!streamId) {
153
- return;
154
- }
155
- const stream = streamManager.getStream(streamId);
156
- if (stream && !stream.finished) {
157
- if (stream.content.trim() === THINKING_PLACEHOLDER.trim()) {
158
- streamManager.replaceIfPlaceholder(streamId, errorMessage, THINKING_PLACEHOLDER);
159
- }
160
- await streamManager.finishStream(streamId);
161
- }
162
- unregisterActiveStream(streamKey, streamId);
163
- }
@@ -1,28 +0,0 @@
1
- import { webhookTargets } from "./state.js";
2
-
3
- export function normalizeWebhookPath(raw) {
4
- const trimmed = (raw || "").trim();
5
- if (!trimmed) {
6
- return "/";
7
- }
8
- const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
9
- if (withSlash.length > 1 && withSlash.endsWith("/")) {
10
- return withSlash.slice(0, -1);
11
- }
12
- return withSlash;
13
- }
14
-
15
- export function registerWebhookTarget(target) {
16
- const key = normalizeWebhookPath(target.path);
17
- const entry = { ...target, path: key };
18
- const existing = webhookTargets.get(key) ?? [];
19
- webhookTargets.set(key, [...existing, entry]);
20
- return () => {
21
- const updated = (webhookTargets.get(key) ?? []).filter((e) => e !== entry);
22
- if (updated.length > 0) {
23
- webhookTargets.set(key, updated);
24
- } else {
25
- webhookTargets.delete(key);
26
- }
27
- };
28
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * WeCom XML Parser
3
- *
4
- * Simple regex-based parser for Agent mode XML callbacks.
5
- * No external dependencies — WeCom XML has a flat, predictable structure.
6
- *
7
- * Typical decrypted XML:
8
- * <xml>
9
- * <ToUserName><![CDATA[corpId]]></ToUserName>
10
- * <FromUserName><![CDATA[zhangsan]]></FromUserName>
11
- * <CreateTime>1348831860</CreateTime>
12
- * <MsgType><![CDATA[text]]></MsgType>
13
- * <Content><![CDATA[hello]]></Content>
14
- * <MsgId>1234567890123456</MsgId>
15
- * <AgentID>1000002</AgentID>
16
- * </xml>
17
- */
18
-
19
- /**
20
- * Extract the <Encrypt> field from the outer XML envelope.
21
- * Supports both CDATA and plain text formats.
22
- *
23
- * @param {string} xml - Raw XML string from WeCom POST body
24
- * @returns {string} The encrypted payload
25
- */
26
- export function extractEncryptFromXml(xml) {
27
- const cdataMatch = /<Encrypt><!\[CDATA\[(.*?)\]\]><\/Encrypt>/s.exec(xml);
28
- if (cdataMatch?.[1]) return cdataMatch[1];
29
-
30
- const plainMatch = /<Encrypt>(.*?)<\/Encrypt>/s.exec(xml);
31
- if (plainMatch?.[1]) return plainMatch[1];
32
-
33
- throw new Error("Invalid XML: missing Encrypt field");
34
- }
35
-
36
- /**
37
- * Parse a decrypted WeCom XML message into a flat key-value object.
38
- * Handles both CDATA-wrapped and plain text values.
39
- *
40
- * @param {string} xml - Decrypted XML string
41
- * @returns {Record<string, string>}
42
- */
43
- export function parseXml(xml) {
44
- const result = {};
45
-
46
- // Match <TagName><![CDATA[value]]></TagName> (CDATA)
47
- const cdataRegex = /<(\w+)><!\[CDATA\[([\s\S]*?)\]\]><\/\1>/g;
48
- let match;
49
- while ((match = cdataRegex.exec(xml)) !== null) {
50
- result[match[1]] = match[2];
51
- }
52
-
53
- // Match <TagName>value</TagName> (plain text, skip already-captured CDATA fields)
54
- const plainRegex = /<(\w+)>([^<]+)<\/\1>/g;
55
- while ((match = plainRegex.exec(xml)) !== null) {
56
- if (!(match[1] in result)) {
57
- result[match[1]] = match[2].trim();
58
- }
59
- }
60
-
61
- return result;
62
- }
63
-
64
- /** Extract message type (lowercase). */
65
- export function extractMsgType(msg) {
66
- return String(msg.MsgType ?? "").toLowerCase();
67
- }
68
-
69
- /** Extract sender user ID. */
70
- export function extractFromUser(msg) {
71
- return String(msg.FromUserName ?? "");
72
- }
73
-
74
- /** Extract group chat ID (undefined for DMs). */
75
- export function extractChatId(msg) {
76
- return msg.ChatId ? String(msg.ChatId) : undefined;
77
- }
78
-
79
- /** Extract message ID for deduplication. */
80
- export function extractMsgId(msg) {
81
- const raw = msg.MsgId ?? msg.MsgID ?? msg.msgid ?? msg.msgId;
82
- return raw != null ? String(raw) : undefined;
83
- }
84
-
85
- /** Extract file name (for file messages). */
86
- export function extractFileName(msg) {
87
- const raw = msg.FileName ?? msg.Filename ?? msg.fileName ?? msg.filename;
88
- return raw != null ? String(raw).trim() || undefined : undefined;
89
- }
90
-
91
- /** Extract media ID (for image/voice/video/file messages). */
92
- export function extractMediaId(msg) {
93
- const raw = msg.MediaId ?? msg.MediaID ?? msg.mediaid ?? msg.mediaId;
94
- return raw != null ? String(raw).trim() || undefined : undefined;
95
- }
96
-
97
- /**
98
- * Extract human-readable content from a parsed message.
99
- *
100
- * @param {Record<string, string>} msg - Parsed XML message
101
- * @returns {string}
102
- */
103
- export function extractContent(msg) {
104
- const msgType = extractMsgType(msg);
105
-
106
- switch (msgType) {
107
- case "text":
108
- return String(msg.Content ?? "");
109
- case "voice":
110
- return String(msg.Recognition ?? "") || "[语音消息]";
111
- case "image":
112
- return `[图片] ${msg.PicUrl ?? ""}`;
113
- case "file":
114
- return "[文件消息]";
115
- case "video":
116
- return "[视频消息]";
117
- case "location":
118
- return `[位置] ${msg.Label ?? ""} (${msg.Location_X ?? ""}, ${msg.Location_Y ?? ""})`;
119
- case "link":
120
- return `[链接] ${msg.Title ?? ""}\n${msg.Description ?? ""}\n${msg.Url ?? ""}`;
121
- case "event":
122
- return `[事件] ${msg.Event ?? ""} - ${msg.EventKey ?? ""}`;
123
- default:
124
- return `[${msgType || "未知消息类型"}]`;
125
- }
126
- }