@yanhaidao/wecom 2.3.12 → 2.3.14

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.
@@ -0,0 +1,408 @@
1
+
2
+ export interface DocMemberEntry {
3
+ userid?: string;
4
+ partyid?: string;
5
+ tagid?: string;
6
+ /**
7
+ * 权限位:1-查看,2-编辑,7-管理
8
+ */
9
+ auth?: number;
10
+ type?: number; // 1:用户, 2:部门
11
+ tmp_external_userid?: string;
12
+ }
13
+
14
+ export interface Node {
15
+ begin: number;
16
+ end: number;
17
+ property: Property;
18
+ type: NodeType;
19
+ children: Node[];
20
+ text?: string;
21
+ }
22
+
23
+ export enum NodeType {
24
+ Document = "Document",
25
+ MainStory = "MainStory",
26
+ Section = "Section",
27
+ Paragraph = "Paragraph",
28
+ Table = "Table",
29
+ TableRow = "TableRow",
30
+ TableCell = "TableCell",
31
+ Text = "Text",
32
+ Drawing = "Drawing"
33
+ }
34
+
35
+ export interface Property {
36
+ section_property?: SectionProperty;
37
+ paragraph_property?: ParagraphProperty;
38
+ run_property?: RunProperty;
39
+ table_property?: TableProperty;
40
+ table_row_property?: TableRowProperty;
41
+ table_cell_property?: TableCellProperty;
42
+ drawing_property?: DrawingProperty;
43
+ }
44
+
45
+ export interface SectionProperty {
46
+ page_size?: PageSize;
47
+ page_margins?: PageMargins;
48
+ }
49
+
50
+ export interface PageSize {
51
+ width: number;
52
+ height: number;
53
+ orientation?: PageOrientation;
54
+ }
55
+
56
+ export interface PageOrientation {
57
+ orientation: "PAGE_ORIENTATION_PORTRAIT" | "PAGE_ORIENTATION_LANDSCAPE" | "PAGE_ORIENTATION_UNSPECIFIED";
58
+ }
59
+
60
+ export interface PageMargins {
61
+ top: number;
62
+ right: number;
63
+ bottom: number;
64
+ left: number;
65
+ }
66
+
67
+ export interface ParagraphProperty {
68
+ number_property?: NumberProperty;
69
+ spacing?: Spacing;
70
+ indent?: Indent;
71
+ alignment_type?: AlignmentType;
72
+ text_direction?: TextDirection;
73
+ }
74
+
75
+ export interface NumberProperty {
76
+ nesting_level: number;
77
+ number_id: string;
78
+ }
79
+
80
+ export interface Spacing {
81
+ before?: number;
82
+ after?: number;
83
+ line?: number;
84
+ line_rule?: LineSpacingRule;
85
+ }
86
+
87
+ export enum LineSpacingRule {
88
+ AUTO = "LINE_SPACING_RULE_AUTO",
89
+ EXACT = "LINE_SPACING_RULE_EXACT",
90
+ AT_LEAST = "LINE_SPACING_RULE_AT_LEAST",
91
+ UNSPECIFIED = "PAGE_ORIENTATION_UNSPECIFIED" // Note: User text had a copy-paste error here, listing PAGE_ORIENTATION_UNSPECIFIED
92
+ }
93
+
94
+ export interface Indent {
95
+ left?: number;
96
+ left_chars?: number;
97
+ right?: number;
98
+ right_chars?: number;
99
+ hanging?: number;
100
+ hanging_chars?: number;
101
+ first_line?: number;
102
+ first_line_chars?: number;
103
+ }
104
+
105
+ export enum AlignmentType {
106
+ UNSPECIFIED = "ALIGNMENT_TYPE_UNSPECIFIED",
107
+ CENTER = "ALIGNMENT_TYPE_CENTER",
108
+ BOTH = "ALIGNMENT_TYPE_BOTH", // Justified
109
+ DISTRIBUTE = "ALIGNMENT_TYPE_DISTRIBUTE",
110
+ LEFT = "ALIGNMENT_TYPE_LEFT",
111
+ RIGHT = "ALIGNMENT_TYPE_RIGHT"
112
+ }
113
+
114
+ export enum TextDirection {
115
+ UNSPECIFIED = "TEXT_DIRECTION_UNSPECIFIED",
116
+ RTL = "TEXT_DIRECTION_RIGHT_TO_LEFT",
117
+ LTR = "TEXT_DIRECTION_LEFT_TO_RIGHT"
118
+ }
119
+
120
+ export interface RunProperty {
121
+ font?: string;
122
+ bold?: boolean;
123
+ italics?: boolean;
124
+ underline?: boolean;
125
+ strike?: boolean;
126
+ color?: string; // RRGGBB
127
+ spacing?: number;
128
+ size?: number; // half-points
129
+ shading?: Shading;
130
+ vertical_align?: TextVerticalAlign;
131
+ is_placeholder?: boolean;
132
+ }
133
+
134
+ export interface Shading {
135
+ foreground_color: string; // RRGGBB
136
+ background_color: string; // RRGGBB
137
+ }
138
+
139
+ export enum TextVerticalAlign {
140
+ UNSPECIFIED = "RUN_VERTICAL_ALIGN_UNSPECIFIED",
141
+ BASELINE = "RUN_VERTICAL_ALIGN_BASELINE",
142
+ SUPER_SCRIPT = "RUN_VERTICAL_ALIGN_SUPER_SCRIPT",
143
+ SUB_SCRIPT = "RUN_VERTICAL_ALIGN_SUB_SCRIPT"
144
+ }
145
+
146
+ export interface TableProperty {
147
+ table_width?: TableWidth;
148
+ horizontal_alignment_type?: TableHorizontalAlignmentType;
149
+ table_layout?: TableLayoutType;
150
+ }
151
+
152
+ export interface TableWidth {
153
+ width: number;
154
+ type: TableWidthType;
155
+ }
156
+
157
+ export enum TableHorizontalAlignmentType {
158
+ UNSPECIFIED = "TABLE_HORIZONTAL_ALIGNMENT_TYPE_UNSPECIFIED",
159
+ CENTER = "TABLE_HORIZONTAL_ALIGNMENT_TYPE_CENTER",
160
+ LEFT = "TABLE_HORIZONTAL_ALIGNMENT_TYPE_LEFT",
161
+ START = "TABLE_HORIZONTAL_ALIGNMENT_TYPE_START"
162
+ }
163
+
164
+ export enum TableLayoutType {
165
+ UNSPECIFIED = "TABLE_LAYOUT_TYPE_UNSPECIFIED",
166
+ FIXED = "TABLE_LAYOUT_TYPE_FIXED",
167
+ AUTO_FIT = "TABLE_LAYOUT_TYPE_AUTO_FIT"
168
+ }
169
+
170
+ export enum TableWidthType {
171
+ UNSPECIFIED = "TABLE_LAYOUT_TYPE_UNSPECIFIED",
172
+ FIXED = "TABLE_LAYOUT_TYPE_FIXED",
173
+ AUTO_FIT = "TABLE_LAYOUT_TYPE_AUTO_FIT"
174
+ }
175
+
176
+ export interface TableRowProperty {
177
+ is_header?: boolean;
178
+ }
179
+
180
+ export interface TableCellProperty {
181
+ table_cell_borders?: Borders;
182
+ vertical_alignment?: VerticalAlignment;
183
+ }
184
+
185
+ export interface Borders {
186
+ top?: BorderProperty;
187
+ left?: BorderProperty;
188
+ bottom?: BorderProperty;
189
+ right?: BorderProperty;
190
+ }
191
+
192
+ export interface BorderProperty {
193
+ color: string; // RRGGBB
194
+ width: number;
195
+ }
196
+
197
+ export enum VerticalAlignment {
198
+ UNSPECIFIED = "VERTICAL_ALIGNMENT__UNSPECIFIED",
199
+ TOP = "VERTICAL_ALIGNMENT_TOP",
200
+ CENTER = "VERTICAL_ALIGNMENT_CENTER",
201
+ BOTH = "VERTICAL_ALIGNMENT_BOTH",
202
+ BOTTOM = "VERTICAL_ALIGNMENT_BOTTOM"
203
+ }
204
+
205
+ export interface DrawingProperty {
206
+ inline_keyword?: Inline;
207
+ anchor?: Anchor;
208
+ is_placeholder?: boolean;
209
+ }
210
+
211
+ export interface Inline {
212
+ picture?: InlinePicture;
213
+ addon?: InlineAddon;
214
+ }
215
+
216
+ export interface InlinePicture {
217
+ uri: string;
218
+ relative_rect?: RelativeRect;
219
+ shape?: ShapeProperties;
220
+ }
221
+
222
+ export interface RelativeRect {
223
+ left: number;
224
+ top: number;
225
+ right: number;
226
+ bottom: number;
227
+ }
228
+
229
+ export interface ShapeProperties {
230
+ transform?: Transform2D;
231
+ }
232
+
233
+ export interface Transform2D {
234
+ extent?: PositiveSize2D;
235
+ rotation?: number;
236
+ }
237
+
238
+ export interface PositiveSize2D {
239
+ cx: number;
240
+ cy: number;
241
+ }
242
+
243
+ export interface InlineAddon {
244
+ addon_id: string;
245
+ addon_source: AddonSourceType;
246
+ }
247
+
248
+ export enum AddonSourceType {
249
+ UNSPECIFIED = "ADDON_SOURCE_TYPE_UNSPECIFIED",
250
+ NONE = "ADDON_SOURCE_TYPE_NONE",
251
+ LATEX = "ADDON_SOURCE_TYPE_LATEX",
252
+ SIGN = "ADDON_SOURCE_TYPE_SIGN",
253
+ SIGN_BAR = "ADDON_SOURCE_TYPE_SIGN_BAR"
254
+ }
255
+
256
+ export interface Anchor {
257
+ picture?: AnchorPicture;
258
+ }
259
+
260
+ export interface AnchorPicture {
261
+ uri: string;
262
+ relative_rect?: RelativeRect;
263
+ shape?: ShapeProperties;
264
+ position_horizontal?: PositionHorizontal;
265
+ position_vertical?: PositionVertical;
266
+ wrap_none?: boolean;
267
+ wrap_square?: WrapSquare;
268
+ wrap_top_and_bottom?: boolean;
269
+ behind_doc?: boolean;
270
+ allow_overlap?: boolean;
271
+ }
272
+
273
+ export interface PositionHorizontal {
274
+ pos_offset: number;
275
+ relative_from: RelativeFromHorizontal;
276
+ }
277
+
278
+ export enum RelativeFromHorizontal {
279
+ UNSPECIFIED = "RELATIVE_FROM_HORIZONTAL_UNSPECIFIED",
280
+ MARGIN = "RELATIVE_FROM_HORIZONTAL_MARGIN",
281
+ PAGE = "RELATIVE_FROM_HORIZONTAL_PAGE",
282
+ COLUMN = "RELATIVE_FROM_HORIZONTAL_COLUMN",
283
+ CHARACTER = "RELATIVE_FROM_HORIZONTAL_CHARACTER",
284
+ LEFT_MARGIN = "RELATIVE_FROM_HORIZONTAL_LEFT_MARGIN",
285
+ RIGHT_MARGIN = "RELATIVE_FROM_HORIZONTAL_RIGHT_MARGIN",
286
+ INSIDE_MARGIN = "RELATIVE_FROM_HORIZONTAL_INSIDE_MARGIN",
287
+ OUTSIDE_MARGIN = "RELATIVE_FROM_HORIZONTAL_OUTSIDE_MARGIN"
288
+ }
289
+
290
+ export interface PositionVertical {
291
+ pos_offset: number;
292
+ relative_from: RelativeFromVertical;
293
+ }
294
+
295
+ export enum RelativeFromVertical {
296
+ UNSPECIFIED = "RELATIVE_FROM_VERTICAL_UNSPECIFIED",
297
+ MARGIN = "RELATIVE_FROM_VERTICAL_MARGIN",
298
+ PAGE = "RELATIVE_FROM_VERTICAL_PAGE",
299
+ PARAGRAPH = "RELATIVE_FROM_VERTICAL_PARAGRAPH",
300
+ LINE = "RELATIVE_FROM_VERTICAL_LINE",
301
+ TOP_MARGIN = "RELATIVE_FROM_VERTICAL_TOP_MARGIN",
302
+ BOTTOM_MARGIN = "RELATIVE_FROM_VERTICAL_BOTTOM_MARGIN",
303
+ INSIDE_MARGIN = "RELATIVE_FROM_VERTICAL_INSIDE_MARGIN",
304
+ OUTSIDE_MARGIN = "RELATIVE_FROM_VERTICAL_OUTSIDE_MARGIN"
305
+ }
306
+
307
+ export interface WrapSquare {
308
+ wrap_text: WrapText;
309
+ }
310
+
311
+ export enum WrapText {
312
+ UNSPECIFIED = "WRAP_TEXT_BOTH_UNSPECIFIED",
313
+ BOTH_SIDES = "WRAP_TEXT_BOTH_SIDES",
314
+ LEFT = "WRAP_TEXT_LEFT",
315
+ RIGHT = "WRAP_TEXT_RIGHT",
316
+ LARGEST = "WRAP_TEXT_LARGEST"
317
+ }
318
+
319
+
320
+ // --- Update Requests ---
321
+
322
+ export interface Location {
323
+ index: number;
324
+ }
325
+
326
+ export interface Range {
327
+ start_index: number;
328
+ length: number;
329
+ }
330
+
331
+ export interface ReplaceTextRequest {
332
+ text: string;
333
+ ranges: Range[];
334
+ }
335
+
336
+ export interface InsertTextRequest {
337
+ text: string;
338
+ location: Location;
339
+ }
340
+
341
+ export interface DeleteContentRequest {
342
+ range: Range;
343
+ }
344
+
345
+ export interface InsertImageRequest {
346
+ image_id: string;
347
+ location: Location;
348
+ width?: number;
349
+ height?: number;
350
+ }
351
+
352
+ export interface InsertPageBreakRequest {
353
+ location: Location;
354
+ }
355
+
356
+ export interface InsertTableRequest {
357
+ rows: number;
358
+ cols: number;
359
+ location: Location;
360
+ }
361
+
362
+ export interface InsertParagraphRequest {
363
+ location: Location;
364
+ }
365
+
366
+ export interface TextProperty {
367
+ bold?: boolean;
368
+ italics?: boolean; // User text says "italics", Schema says "italic". User text for RunProperty says "italics", UpdateTextProperty example says "bold" but doesn't list italics explicitly in example, but RunProperty does. Standard WeCom API is "italics"? My schema says "italic". I will use "italics" as per user provided text for RunProperty, but UpdateTextProperty might differ.
369
+ // User text for TextProperty example: bold, color, background_color.
370
+ // RunProperty has "italics".
371
+ // I will check the user provided TextProperty definition again.
372
+ // "blod" (typo in user text), color, background_color.
373
+ // It doesn't list italics in TextProperty section, but RunProperty does.
374
+ // I will support what is likely correct.
375
+ underline?: boolean;
376
+ strikethrough?: boolean;
377
+ color?: string;
378
+ background_color?: string;
379
+ font_size?: number;
380
+ }
381
+
382
+ export interface UpdateTextPropertyRequest {
383
+ text_property: TextProperty;
384
+ ranges: Range[];
385
+ }
386
+
387
+ export interface UpdateRequest {
388
+ replace_text?: ReplaceTextRequest;
389
+ insert_text?: InsertTextRequest;
390
+ delete_content?: DeleteContentRequest;
391
+ insert_image?: InsertImageRequest;
392
+ insert_page_break?: InsertPageBreakRequest;
393
+ insert_table?: InsertTableRequest;
394
+ insert_paragraph?: InsertParagraphRequest;
395
+ update_text_property?: UpdateTextPropertyRequest;
396
+ }
397
+
398
+ export interface BatchUpdateDocResponse {
399
+ errcode: number;
400
+ errmsg: string;
401
+ }
402
+
403
+ export interface GetDocContentResponse {
404
+ errcode: number;
405
+ errmsg: string;
406
+ version: number;
407
+ document: Node;
408
+ }
package/src/channel.ts CHANGED
@@ -65,7 +65,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
65
65
  threads: false,
66
66
  polls: false,
67
67
  nativeCommands: false,
68
- blockStreaming: true,
68
+ blockStreaming: false,
69
69
  },
70
70
  reload: { configPrefixes: ["channels.wecom"] },
71
71
  // NOTE: We intentionally avoid Zod -> JSON Schema conversion at plugin-load time.
@@ -51,7 +51,8 @@ export function generateAgentId(chatType: "dm" | "group", peerId: string, accoun
51
51
  export function buildAgentSessionTarget(userId: string, accountId?: string): string {
52
52
  const normalizedUserId = String(userId).trim();
53
53
  const sanitizedAccountId = sanitizeDynamicIdPart(accountId ?? "default") || "default";
54
- return `wecom-agent:${sanitizedAccountId}:${normalizedUserId}`;
54
+ // Always use explicit user: prefix to avoid ambiguity with numeric party IDs
55
+ return `wecom-agent:${sanitizedAccountId}:user:${normalizedUserId}`;
55
56
  }
56
57
 
57
58
  /**
package/src/outbound.ts CHANGED
@@ -49,7 +49,7 @@ function resolveAgentConfigOrThrow(params: {
49
49
  );
50
50
  }
51
51
  // 注意:不要在日志里输出 corpSecret 等敏感信息
52
- console.log(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
52
+ getAccountRuntime(account.accountId)?.log.info?.(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
53
53
  return account;
54
54
  }
55
55
 
@@ -158,7 +158,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
158
158
 
159
159
  if (looksLikeNewSessionAck) {
160
160
  if (!isAgentSessionTarget) {
161
- console.log(`[wecom-outbound] Suppressed command ack to avoid Bot/Agent double-reply (len=${trimmed.length})`);
161
+ getAccountRuntime(agent.accountId)?.log.info?.(`[wecom-outbound] Suppressed command ack to avoid Bot/Agent double-reply (len=${trimmed.length})`);
162
162
  return { channel: "wecom", messageId: `suppressed-${Date.now()}`, timestamp: Date.now() };
163
163
  }
164
164
 
@@ -167,11 +167,11 @@ export const wecomOutbound: ChannelOutboundAdapter = {
167
167
  return m?.[1]?.trim();
168
168
  })();
169
169
  const rewritten = modelLabel ? `✅ 已开启新会话(模型:${modelLabel})` : "✅ 已开启新会话。";
170
- console.log(`[wecom-outbound] Rewrote command ack for agent session (len=${rewritten.length})`);
170
+ getAccountRuntime(agent.accountId)?.log.info?.(`[wecom-outbound] Rewrote command ack for agent session (len=${rewritten.length})`);
171
171
  outgoingText = rewritten;
172
172
  }
173
173
 
174
- console.log(`[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`);
174
+ getAccountRuntime(agent.accountId)?.log.info?.(`[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`);
175
175
 
176
176
  let sentViaBotWs = false;
177
177
  try {
@@ -191,7 +191,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
191
191
  console.log(`[wecom-outbound] Successfully sent Agent text to ${String(to ?? "")}`);
192
192
  }
193
193
  } catch (err) {
194
- console.error(`[wecom-outbound] Failed to send text to ${String(to ?? "")}:`, err);
194
+ getAccountRuntime(agent.accountId)?.log.error?.(`[wecom-outbound] Failed to send text to ${String(to ?? "")}: ${err instanceof Error ? err.message : String(err)}`);
195
195
  throw err;
196
196
  }
197
197
 
@@ -46,8 +46,8 @@ export async function prepareInboundSession(params: {
46
46
  From:
47
47
  event.conversation.peerKind === "group"
48
48
  ? `wecom:group:${event.conversation.peerId}`
49
- : `wecom:${event.conversation.senderId}`,
50
- To: `wecom:${event.conversation.peerId}`,
49
+ : `wecom:user:${event.conversation.senderId}`,
50
+ To: event.conversation.peerKind === "group" ? `wecom:group:${event.conversation.peerId}` : `wecom:user:${event.conversation.peerId}`,
51
51
  SessionKey: route.sessionKey,
52
52
  AccountId: route.accountId,
53
53
  ChatType: event.conversation.peerKind,
@@ -57,7 +57,7 @@ export async function prepareInboundSession(params: {
57
57
  Provider: "wecom",
58
58
  Surface: "wecom",
59
59
  OriginatingChannel: "wecom",
60
- OriginatingTo: `wecom:${event.conversation.peerId}`,
60
+ OriginatingTo: event.conversation.peerKind === "group" ? `wecom:group:${event.conversation.peerId}` : `wecom:user:${event.conversation.peerId}`,
61
61
  MessageSid: event.messageId,
62
62
  CommandAuthorized: true,
63
63
  MediaPath: mediaPath,
@@ -69,7 +69,7 @@ export async function prepareInboundSession(params: {
69
69
  storePath,
70
70
  sessionKey: ctx.SessionKey ?? route.sessionKey,
71
71
  ctx,
72
- onRecordError: () => {},
72
+ onRecordError: () => { },
73
73
  });
74
74
 
75
75
  return { route, ctx, storePath };
@@ -2,6 +2,7 @@ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
3
  import type { NormalizedMediaAttachment } from "./media-types.js";
4
4
  import type { UnifiedInboundEvent } from "../types/index.js";
5
+ import { decryptWecomMediaWithMeta } from "../media.js";
5
6
 
6
7
  export class WecomMediaService {
7
8
  constructor(private readonly core: PluginRuntime) {}
@@ -15,6 +16,21 @@ export class WecomMediaService {
15
16
  };
16
17
  }
17
18
 
19
+ /**
20
+ * Download and decrypt WeCom AES-encrypted media.
21
+ * Bot-ws: each message carries a unique per-URL aeskey in the message body.
22
+ * Bot-webhook: uses the account-level EncodingAESKey.
23
+ * Both use AES-256-CBC with PKCS#7 padding (32-byte block), IV = key[:16].
24
+ */
25
+ async downloadEncryptedMedia(params: { url: string; aesKey: string }): Promise<NormalizedMediaAttachment> {
26
+ const decrypted = await decryptWecomMediaWithMeta(params.url, params.aesKey);
27
+ return {
28
+ buffer: decrypted.buffer,
29
+ contentType: decrypted.sourceContentType,
30
+ filename: decrypted.sourceFilename,
31
+ };
32
+ }
33
+
18
34
  async saveInboundAttachment(event: UnifiedInboundEvent, attachment: NormalizedMediaAttachment): Promise<string> {
19
35
  const saved = await this.core.channel.media.saveMediaBuffer(
20
36
  attachment.buffer,
@@ -31,6 +47,10 @@ export class WecomMediaService {
31
47
  if (!first?.remoteUrl) {
32
48
  return undefined;
33
49
  }
50
+ // Bot-ws media is AES-encrypted; use decryption when aesKey is present
51
+ if (first.aesKey) {
52
+ return this.downloadEncryptedMedia({ url: first.remoteUrl, aesKey: first.aesKey });
53
+ }
34
54
  return this.downloadRemoteMedia({ url: first.remoteUrl });
35
55
  }
36
56
  }
package/src/target.ts CHANGED
@@ -35,12 +35,12 @@ export interface ScopedWecomTarget {
35
35
  * 2. 检查显式类型前缀 (party:, tag:, group:, user:)。
36
36
  * 3. 启发式回退 (无前缀时):
37
37
  * - 以 "wr" 或 "wc" 开头 -> Chat ID (群聊)
38
- * - 纯数字 -> Party ID (部门)
38
+ * - 纯数字 -> 默认 Party ID (部门);如果 preferUserForDigits 为 true 则视为 User ID
39
39
  * - 其他 -> User ID (用户)
40
40
  *
41
41
  * @param raw - The raw target string (e.g. "party:1", "zhangsan", "wecom:wr123")
42
42
  */
43
- export function resolveWecomTarget(raw: string | undefined): WecomTarget | undefined {
43
+ export function resolveWecomTarget(raw: string | undefined, options?: { preferUserForDigits?: boolean }): WecomTarget | undefined {
44
44
  if (!raw?.trim()) return undefined;
45
45
 
46
46
  // 1. Remove standard namespace prefixes (移除标准命名空间前缀)
@@ -78,10 +78,16 @@ export function resolveWecomTarget(raw: string | undefined): WecomTarget | undef
78
78
  // 纯数字优先被视为部门 ID (Parties),方便运维配置 (如 "1" 代表根部门)
79
79
  // 如果必须要发送给纯数字 ID 的用户,请使用显式前缀 "user:1001"
80
80
  if (/^\d+$/.test(clean)) {
81
+ if (options?.preferUserForDigits) {
82
+ return { touser: clean };
83
+ }
81
84
  return { toparty: clean };
82
85
  }
83
86
 
84
87
  // Default to User (默认为用户)
88
+ // 注意:纯数字通常可能是 UserID (内部成员 ID),也可能是 PartyID。
89
+ // 为了兼容性,如果没有前缀且不匹配群聊规则,我们将其视为 UserID。
90
+ // 如果需要明确发送给部门,请使用 "party:1" 前缀。
85
91
  return { touser: clean };
86
92
  }
87
93
 
@@ -93,7 +99,9 @@ export function resolveScopedWecomTarget(raw: string | undefined, defaultAccount
93
99
  if (agentScoped) {
94
100
  const accountId = agentScoped[1]?.trim() || defaultAccountId;
95
101
  const rawTarget = agentScoped[2]?.trim() || "";
96
- const target = resolveWecomTarget(rawTarget);
102
+ // Agent scoped targets are almost always users in a conversation context.
103
+ // In this scope, we prefer treating numeric IDs as User IDs to avoid 81013 errors.
104
+ const target = resolveWecomTarget(rawTarget, { preferUserForDigits: true });
97
105
  return target ? { accountId, target, rawTarget } : undefined;
98
106
  }
99
107
 
@@ -15,7 +15,10 @@ export async function useActiveReplyOnce(
15
15
  streamId: string,
16
16
  fn: (params: { responseUrl: string; proxyUrl?: string }) => Promise<void>,
17
17
  ): Promise<void> {
18
- return activeReplyStore.use(streamId, fn);
18
+ return activeReplyStore.use(streamId, async (params) => {
19
+ await new Promise((resolve) => setTimeout(resolve, 1000));
20
+ await fn(params);
21
+ });
19
22
  }
20
23
 
21
24
  export async function sendActiveMessage(streamId: string, content: string): Promise<void> {
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { mapBotWsFrameToInboundEvent } from "./inbound.js";
4
+ import type { ResolvedBotAccount } from "../../types/index.js";
5
+
6
+ function createBotAccount(): ResolvedBotAccount {
7
+ return {
8
+ accountId: "haidao",
9
+ configured: true,
10
+ primaryTransport: "ws",
11
+ wsConfigured: true,
12
+ webhookConfigured: false,
13
+ config: {},
14
+ ws: {
15
+ botId: "bot-id",
16
+ secret: "secret",
17
+ },
18
+ token: "",
19
+ encodingAESKey: "",
20
+ receiveId: "",
21
+ botId: "bot-id",
22
+ secret: "secret",
23
+ };
24
+ }
25
+
26
+ describe("mapBotWsFrameToInboundEvent", () => {
27
+ it("includes quote content in text events", () => {
28
+ const event = mapBotWsFrameToInboundEvent({
29
+ account: createBotAccount(),
30
+ frame: {
31
+ cmd: "aibot_msg_callback",
32
+ headers: { req_id: "req-1" },
33
+ body: {
34
+ msgid: "msg-1",
35
+ msgtype: "text",
36
+ chattype: "group",
37
+ chatid: "group-1",
38
+ from: { userid: "user-1" },
39
+ text: { content: "@daodao 这个线索价值" },
40
+ quote: {
41
+ msgtype: "text",
42
+ text: { content: "原始引用内容" },
43
+ },
44
+ },
45
+ },
46
+ });
47
+
48
+ expect(event.text).toBe("@daodao 这个线索价值\n\n> 原始引用内容");
49
+ });
50
+ });