@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.
- package/README.md +160 -0
- package/changelog/v2.3.12.md +2 -0
- package/changelog/v2.3.13.md +19 -0
- package/changelog/v2.3.14.md +48 -0
- package/index.ts +4 -0
- package/package.json +4 -3
- package/src/agent/handler.event-filter.test.ts +15 -5
- package/src/agent/handler.ts +217 -74
- package/src/app/account-runtime.ts +1 -1
- package/src/app/index.ts +4 -0
- package/src/capability/agent/delivery-service.ts +16 -8
- package/src/capability/bot/fallback-delivery.ts +1 -1
- package/src/capability/bot/stream-orchestrator.ts +10 -10
- package/src/capability/doc/client.ts +910 -0
- package/src/capability/doc/schema.ts +1404 -0
- package/src/capability/doc/tool.ts +1165 -0
- package/src/capability/doc/types.ts +408 -0
- package/src/channel.ts +1 -1
- package/src/dynamic-agent.ts +2 -1
- package/src/outbound.ts +5 -5
- package/src/runtime/session-manager.ts +4 -4
- package/src/shared/media-service.ts +20 -0
- package/src/target.ts +11 -3
- package/src/transport/bot-webhook/active-reply.ts +4 -1
- package/src/transport/bot-ws/inbound.test.ts +50 -0
- package/src/transport/bot-ws/inbound.ts +12 -22
- package/src/transport/bot-ws/reply.test.ts +152 -37
- package/src/transport/bot-ws/reply.ts +135 -8
- package/src/transport/bot-ws/sdk-adapter.ts +1 -0
- package/src/types/runtime.ts +3 -0
|
@@ -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:
|
|
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.
|
package/src/dynamic-agent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
});
|