@yanhaidao/wecom 2.3.160 → 2.3.180
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 +235 -399
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2136 -0
- package/changelog/v2.3.18.md +22 -0
- package/index.ts +39 -3
- package/package.json +2 -3
- package/src/agent/handler.event-filter.test.ts +11 -0
- package/src/agent/handler.ts +732 -643
- package/src/app/account-runtime.ts +46 -20
- package/src/app/index.ts +19 -1
- package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
- package/src/capability/calendar/client.ts +815 -0
- package/src/capability/calendar/index.ts +3 -0
- package/src/capability/calendar/schema.ts +417 -0
- package/src/capability/calendar/tool.ts +417 -0
- package/src/capability/calendar/types.ts +309 -0
- package/src/capability/doc/client.ts +567 -62
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1510 -1178
- package/src/capability/doc/types.ts +130 -14
- package/src/capability/mcp/index.ts +10 -0
- package/src/capability/mcp/schema.ts +107 -0
- package/src/capability/mcp/tool.ts +170 -0
- package/src/capability/mcp/transport.ts +394 -0
- package/src/channel.ts +70 -28
- package/src/config/schema.ts +71 -102
- package/src/outbound.test.ts +91 -14
- package/src/outbound.ts +143 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/transport/bot-ws/media.ts +269 -0
- package/src/transport/bot-ws/reply.test.ts +85 -17
- package/src/transport/bot-ws/reply.ts +109 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +88 -12
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
package/src/config/schema.ts
CHANGED
|
@@ -1,114 +1,83 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const schemaWithJson = schema as T & { toJSONSchema?: (...args: unknown[]) => unknown };
|
|
5
|
-
if (typeof schemaWithJson.toJSONSchema === "function") {
|
|
6
|
-
schemaWithJson.toJSONSchema = schemaWithJson.toJSONSchema.bind(schema);
|
|
7
|
-
}
|
|
8
|
-
return schema;
|
|
1
|
+
export interface DmConfig {
|
|
2
|
+
policy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
3
|
+
allowFrom?: (string | number)[];
|
|
9
4
|
}
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const mediaSchema = z
|
|
19
|
-
.object({
|
|
20
|
-
tempDir: z.string().optional(),
|
|
21
|
-
retentionHours: z.number().optional(),
|
|
22
|
-
cleanupOnStart: z.boolean().optional(),
|
|
23
|
-
maxBytes: z.number().optional(),
|
|
24
|
-
})
|
|
25
|
-
.optional();
|
|
6
|
+
export interface MediaConfig {
|
|
7
|
+
tempDir?: string;
|
|
8
|
+
retentionHours?: number;
|
|
9
|
+
cleanupOnStart?: boolean;
|
|
10
|
+
maxBytes?: number;
|
|
11
|
+
}
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
.optional();
|
|
13
|
+
export interface NetworkConfig {
|
|
14
|
+
egressProxyUrl?: string;
|
|
15
|
+
}
|
|
32
16
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
.optional();
|
|
17
|
+
export interface RoutingConfig {
|
|
18
|
+
failClosedOnDefaultRoute?: boolean;
|
|
19
|
+
}
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})
|
|
44
|
-
.optional();
|
|
21
|
+
export interface BotWsConfig {
|
|
22
|
+
botId: string;
|
|
23
|
+
secret: string;
|
|
24
|
+
}
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
.optional();
|
|
26
|
+
export interface BotWebhookConfig {
|
|
27
|
+
token: string;
|
|
28
|
+
encodingAESKey: string;
|
|
29
|
+
receiveId?: string;
|
|
30
|
+
}
|
|
53
31
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
65
|
-
.optional();
|
|
32
|
+
export interface BotConfig {
|
|
33
|
+
primaryTransport?: "ws" | "webhook";
|
|
34
|
+
streamPlaceholderContent?: string;
|
|
35
|
+
welcomeText?: string;
|
|
36
|
+
dm?: DmConfig;
|
|
37
|
+
aibotid?: string;
|
|
38
|
+
botIds?: string[];
|
|
39
|
+
ws?: BotWsConfig;
|
|
40
|
+
webhook?: BotWebhookConfig;
|
|
41
|
+
}
|
|
66
42
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
})
|
|
78
|
-
.refine((value) => Boolean(value.agentSecret?.trim() || value.corpSecret?.trim()), {
|
|
79
|
-
path: ["agentSecret"],
|
|
80
|
-
message: "agentSecret 不能为空",
|
|
81
|
-
})
|
|
82
|
-
.optional();
|
|
43
|
+
export interface AgentConfig {
|
|
44
|
+
corpId: string;
|
|
45
|
+
agentSecret?: string;
|
|
46
|
+
corpSecret?: string;
|
|
47
|
+
agentId?: number | string;
|
|
48
|
+
token: string;
|
|
49
|
+
encodingAESKey: string;
|
|
50
|
+
welcomeText?: string;
|
|
51
|
+
dm?: DmConfig;
|
|
52
|
+
}
|
|
83
53
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
.optional();
|
|
54
|
+
export interface DynamicAgentsConfig {
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
dmCreateAgent?: boolean;
|
|
57
|
+
groupEnabled?: boolean;
|
|
58
|
+
adminUsers?: string[];
|
|
59
|
+
}
|
|
92
60
|
|
|
93
|
-
|
|
94
|
-
enabled
|
|
95
|
-
name
|
|
96
|
-
bot
|
|
97
|
-
agent
|
|
98
|
-
}
|
|
61
|
+
export interface AccountConfig {
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
name?: string;
|
|
64
|
+
bot?: BotConfig;
|
|
65
|
+
agent?: AgentConfig;
|
|
66
|
+
}
|
|
99
67
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
68
|
+
export interface WecomConfigInput {
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
bot?: BotConfig;
|
|
71
|
+
agent?: AgentConfig;
|
|
72
|
+
accounts?: Record<string, AccountConfig>;
|
|
73
|
+
defaultAccount?: string;
|
|
74
|
+
media?: MediaConfig;
|
|
75
|
+
network?: NetworkConfig;
|
|
76
|
+
routing?: RoutingConfig;
|
|
77
|
+
dynamicAgents?: DynamicAgentsConfig;
|
|
78
|
+
}
|
|
113
79
|
|
|
114
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @deprecated No longer a Zod schema. Kept as a type-only export for backward compatibility.
|
|
82
|
+
*/
|
|
83
|
+
export const WecomConfigSchema = undefined;
|
package/src/outbound.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { BotWsPushHandle } from "./app/index.js";
|
|
2
3
|
|
|
3
4
|
vi.mock("./transport/agent-api/core.js", () => ({
|
|
4
5
|
sendText: vi.fn(),
|
|
@@ -7,6 +8,25 @@ vi.mock("./transport/agent-api/core.js", () => ({
|
|
|
7
8
|
}));
|
|
8
9
|
|
|
9
10
|
describe("wecomOutbound", () => {
|
|
11
|
+
const createBotWsHandle = (overrides: Partial<BotWsPushHandle> = {}): BotWsPushHandle => ({
|
|
12
|
+
isConnected: () => true,
|
|
13
|
+
sendMarkdown: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
replyCommand: vi.fn().mockResolvedValue({ errcode: 0 }),
|
|
15
|
+
sendMedia: vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" }),
|
|
16
|
+
...overrides,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const runtime = await import("./runtime.js");
|
|
21
|
+
runtime.setWecomRuntime({
|
|
22
|
+
channel: {
|
|
23
|
+
text: {
|
|
24
|
+
chunkText: (text: string) => [text],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} as any);
|
|
28
|
+
});
|
|
29
|
+
|
|
10
30
|
afterEach(async () => {
|
|
11
31
|
const runtime = await import("./runtime.js");
|
|
12
32
|
runtime.unregisterBotWsPushHandle("default");
|
|
@@ -80,9 +100,9 @@ describe("wecomOutbound", () => {
|
|
|
80
100
|
};
|
|
81
101
|
|
|
82
102
|
// Chat ID (wr/wc) is intentionally NOT supported for Agent outbound.
|
|
83
|
-
await expect(
|
|
84
|
-
|
|
85
|
-
);
|
|
103
|
+
await expect(
|
|
104
|
+
wecomOutbound.sendText({ cfg, to: "wr123", text: "hello" } as any),
|
|
105
|
+
).rejects.toThrow(/不支持向群 chatId 发送/);
|
|
86
106
|
expect(api.sendText).not.toHaveBeenCalled();
|
|
87
107
|
|
|
88
108
|
// Test: User ID (Default)
|
|
@@ -186,10 +206,12 @@ describe("wecomOutbound", () => {
|
|
|
186
206
|
const api = await import("./transport/agent-api/core.js");
|
|
187
207
|
const sendMarkdown = vi.fn().mockResolvedValue(undefined);
|
|
188
208
|
const now = vi.spyOn(Date, "now").mockReturnValue(789);
|
|
189
|
-
runtime.registerBotWsPushHandle(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
runtime.registerBotWsPushHandle(
|
|
210
|
+
"acct-ws",
|
|
211
|
+
createBotWsHandle({
|
|
212
|
+
sendMarkdown,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
193
215
|
(api.sendText as any).mockClear();
|
|
194
216
|
|
|
195
217
|
const cfg = {
|
|
@@ -271,15 +293,70 @@ describe("wecomOutbound", () => {
|
|
|
271
293
|
expect(api.sendText).not.toHaveBeenCalled();
|
|
272
294
|
});
|
|
273
295
|
|
|
274
|
-
it("
|
|
296
|
+
it("prefers Bot WS for outbound media when ws is the active bot transport", async () => {
|
|
275
297
|
const { wecomOutbound } = await import("./outbound.js");
|
|
276
298
|
const runtime = await import("./runtime.js");
|
|
277
299
|
const api = await import("./transport/agent-api/core.js");
|
|
278
|
-
const
|
|
279
|
-
runtime.registerBotWsPushHandle(
|
|
280
|
-
|
|
281
|
-
|
|
300
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
301
|
+
runtime.registerBotWsPushHandle(
|
|
302
|
+
"default",
|
|
303
|
+
createBotWsHandle({
|
|
304
|
+
sendMedia,
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
308
|
+
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
309
|
+
(api.sendMedia as any).mockClear();
|
|
310
|
+
|
|
311
|
+
const cfg = {
|
|
312
|
+
channels: {
|
|
313
|
+
wecom: {
|
|
314
|
+
enabled: true,
|
|
315
|
+
bot: {
|
|
316
|
+
primaryTransport: "ws",
|
|
317
|
+
ws: {
|
|
318
|
+
botId: "bot-1",
|
|
319
|
+
secret: "secret-1",
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
agent: {
|
|
323
|
+
corpId: "corp",
|
|
324
|
+
corpSecret: "secret",
|
|
325
|
+
agentId: 1000002,
|
|
326
|
+
token: "token",
|
|
327
|
+
encodingAESKey: "aes",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
await wecomOutbound.sendMedia({
|
|
334
|
+
cfg,
|
|
335
|
+
to: "user:zhangsan",
|
|
336
|
+
text: "caption",
|
|
337
|
+
mediaUrl: "https://example.com/media.png",
|
|
338
|
+
} as any);
|
|
339
|
+
|
|
340
|
+
expect(sendMedia).toHaveBeenCalledWith({
|
|
341
|
+
chatId: "zhangsan",
|
|
342
|
+
mediaUrl: "https://example.com/media.png",
|
|
343
|
+
mediaLocalRoots: undefined,
|
|
344
|
+
text: "caption",
|
|
282
345
|
});
|
|
346
|
+
expect(api.sendMedia).not.toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("falls back to Agent media when Bot WS media delivery returns a non-fatal failure", async () => {
|
|
350
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
351
|
+
const runtime = await import("./runtime.js");
|
|
352
|
+
const api = await import("./transport/agent-api/core.js");
|
|
353
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: false, error: "upload failed" });
|
|
354
|
+
runtime.registerBotWsPushHandle(
|
|
355
|
+
"default",
|
|
356
|
+
createBotWsHandle({
|
|
357
|
+
sendMedia,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
283
360
|
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
284
361
|
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
285
362
|
(api.sendMedia as any).mockClear();
|
|
@@ -321,8 +398,8 @@ describe("wecomOutbound", () => {
|
|
|
321
398
|
mediaUrl: "https://example.com/media.png",
|
|
322
399
|
} as any);
|
|
323
400
|
|
|
401
|
+
expect(sendMedia).toHaveBeenCalledTimes(1);
|
|
324
402
|
expect(api.sendMedia).toHaveBeenCalledTimes(1);
|
|
325
|
-
expect(sendMarkdown).not.toHaveBeenCalled();
|
|
326
403
|
});
|
|
327
404
|
|
|
328
405
|
it("uses account-scoped agent config in matrix mode", async () => {
|
package/src/outbound.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
|
|
4
2
|
import { WecomAgentDeliveryService } from "./capability/agent/index.js";
|
|
3
|
+
import {
|
|
4
|
+
resolveWecomAccount,
|
|
5
|
+
resolveWecomAccountConflict,
|
|
6
|
+
resolveWecomAccounts,
|
|
7
|
+
} from "./config/index.js";
|
|
5
8
|
import { getAccountRuntime, getBotWsPushHandle, getWecomRuntime } from "./runtime.js";
|
|
6
9
|
import { resolveScopedWecomTarget } from "./target.js";
|
|
7
10
|
|
|
@@ -49,7 +52,9 @@ function resolveAgentConfigOrThrow(params: {
|
|
|
49
52
|
);
|
|
50
53
|
}
|
|
51
54
|
// 注意:不要在日志里输出 corpSecret 等敏感信息
|
|
52
|
-
getAccountRuntime(account.accountId)?.log.info?.(
|
|
55
|
+
getAccountRuntime(account.accountId)?.log.info?.(
|
|
56
|
+
`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`,
|
|
57
|
+
);
|
|
53
58
|
return account;
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -89,7 +94,13 @@ function shouldPreferBotWsOutbound(params: {
|
|
|
89
94
|
accountId: params.accountId,
|
|
90
95
|
});
|
|
91
96
|
return {
|
|
92
|
-
preferred:
|
|
97
|
+
preferred:
|
|
98
|
+
!isExplicitAgentTarget(params.to) &&
|
|
99
|
+
Boolean(
|
|
100
|
+
account.bot?.configured &&
|
|
101
|
+
account.bot.primaryTransport === "ws" &&
|
|
102
|
+
account.bot.wsConfigured,
|
|
103
|
+
),
|
|
93
104
|
accountId: account.accountId,
|
|
94
105
|
};
|
|
95
106
|
}
|
|
@@ -122,12 +133,66 @@ async function sendTextViaBotWs(params: {
|
|
|
122
133
|
`WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
|
|
123
134
|
);
|
|
124
135
|
}
|
|
125
|
-
console.log(
|
|
136
|
+
console.log(
|
|
137
|
+
`[wecom-outbound] Sending Bot WS active message to target=${String(params.to ?? "")} chatId=${chatId} (len=${params.text.length})`,
|
|
138
|
+
);
|
|
126
139
|
await handle.sendMarkdown(chatId, params.text);
|
|
127
140
|
console.log(`[wecom-outbound] Successfully sent Bot WS active message to ${chatId}`);
|
|
128
141
|
return true;
|
|
129
142
|
}
|
|
130
143
|
|
|
144
|
+
async function sendMediaViaBotWs(params: {
|
|
145
|
+
cfg: ChannelOutboundContext["cfg"];
|
|
146
|
+
accountId?: string | null;
|
|
147
|
+
to: string | undefined;
|
|
148
|
+
mediaUrl: string;
|
|
149
|
+
text?: string;
|
|
150
|
+
mediaLocalRoots?: readonly string[];
|
|
151
|
+
}): Promise<{
|
|
152
|
+
attempted: boolean;
|
|
153
|
+
sent: boolean;
|
|
154
|
+
reason?: string;
|
|
155
|
+
}> {
|
|
156
|
+
const { preferred, accountId } = shouldPreferBotWsOutbound(params);
|
|
157
|
+
if (!preferred) {
|
|
158
|
+
return { attempted: false, sent: false };
|
|
159
|
+
}
|
|
160
|
+
const chatId = resolveBotWsChatTarget({
|
|
161
|
+
to: params.to,
|
|
162
|
+
accountId,
|
|
163
|
+
});
|
|
164
|
+
if (!chatId) {
|
|
165
|
+
return { attempted: false, sent: false };
|
|
166
|
+
}
|
|
167
|
+
const handle = getBotWsPushHandle(accountId);
|
|
168
|
+
if (!handle) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`WeCom outbound account=${accountId} is configured for Bot WS active push, but no live WS runtime is registered.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (!handle.isConnected()) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
console.log(
|
|
179
|
+
`[wecom-outbound] Sending Bot WS media to target=${String(params.to ?? "")} chatId=${chatId} media=${params.mediaUrl}`,
|
|
180
|
+
);
|
|
181
|
+
const result = await handle.sendMedia({
|
|
182
|
+
chatId,
|
|
183
|
+
mediaUrl: params.mediaUrl,
|
|
184
|
+
text: params.text,
|
|
185
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
186
|
+
});
|
|
187
|
+
if (result.ok) {
|
|
188
|
+
console.log(`[wecom-outbound] Successfully sent Bot WS media to ${chatId}`);
|
|
189
|
+
return { attempted: true, sent: true };
|
|
190
|
+
}
|
|
191
|
+
const reason = result.rejectReason || result.error || "unknown";
|
|
192
|
+
console.warn(`[wecom-outbound] Bot WS media failed for ${chatId}: ${reason}`);
|
|
193
|
+
return { attempted: true, sent: false, reason };
|
|
194
|
+
}
|
|
195
|
+
|
|
131
196
|
export const wecomOutbound: ChannelOutboundAdapter = {
|
|
132
197
|
deliveryMode: "direct",
|
|
133
198
|
chunkerMode: "text",
|
|
@@ -155,8 +220,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
155
220
|
const trimmed = String(outgoingText ?? "").trim();
|
|
156
221
|
const rawTo = typeof to === "string" ? to.trim().toLowerCase() : "";
|
|
157
222
|
const isAgentSessionTarget = rawTo.startsWith("wecom-agent:");
|
|
158
|
-
const looksLikeNewSessionAck =
|
|
159
|
-
/new session started/i.test(trimmed) && /model:/i.test(trimmed);
|
|
223
|
+
const looksLikeNewSessionAck = /new session started/i.test(trimmed) && /model:/i.test(trimmed);
|
|
160
224
|
|
|
161
225
|
if (looksLikeNewSessionAck) {
|
|
162
226
|
if (!isAgentSessionTarget) {
|
|
@@ -174,7 +238,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
174
238
|
|
|
175
239
|
let sentViaBotWs = false;
|
|
176
240
|
let agent: any = null;
|
|
177
|
-
|
|
241
|
+
|
|
178
242
|
try {
|
|
179
243
|
sentViaBotWs = await sendTextViaBotWs({
|
|
180
244
|
cfg,
|
|
@@ -185,7 +249,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
185
249
|
if (!sentViaBotWs) {
|
|
186
250
|
// Defer Agent resolution until needed for fallback
|
|
187
251
|
agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
188
|
-
getAccountRuntime(agent.accountId)?.log.info?.(
|
|
252
|
+
getAccountRuntime(agent.accountId)?.log.info?.(
|
|
253
|
+
`[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`,
|
|
254
|
+
);
|
|
189
255
|
const deliveryService = new WecomAgentDeliveryService(agent);
|
|
190
256
|
await deliveryService.sendText({
|
|
191
257
|
to,
|
|
@@ -195,7 +261,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
195
261
|
}
|
|
196
262
|
} catch (err) {
|
|
197
263
|
if (agent) {
|
|
198
|
-
getAccountRuntime(agent.accountId)?.log.error?.(
|
|
264
|
+
getAccountRuntime(agent.accountId)?.log.error?.(
|
|
265
|
+
`[wecom-outbound] Failed to send text to ${String(to ?? "")}: ${err instanceof Error ? err.message : String(err)}`,
|
|
266
|
+
);
|
|
199
267
|
}
|
|
200
268
|
throw err;
|
|
201
269
|
}
|
|
@@ -206,18 +274,37 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
206
274
|
timestamp: Date.now(),
|
|
207
275
|
};
|
|
208
276
|
},
|
|
209
|
-
sendMedia: async ({
|
|
277
|
+
sendMedia: async ({
|
|
278
|
+
cfg,
|
|
279
|
+
to,
|
|
280
|
+
text,
|
|
281
|
+
mediaUrl,
|
|
282
|
+
accountId,
|
|
283
|
+
mediaLocalRoots,
|
|
284
|
+
}: ChannelOutboundContext) => {
|
|
210
285
|
// signal removed - not supported in current SDK
|
|
286
|
+
if (!mediaUrl) {
|
|
287
|
+
throw new Error("WeCom outbound requires mediaUrl.");
|
|
288
|
+
}
|
|
211
289
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
290
|
+
const botWs = await sendMediaViaBotWs({
|
|
291
|
+
cfg,
|
|
292
|
+
accountId,
|
|
293
|
+
to,
|
|
294
|
+
text,
|
|
295
|
+
mediaUrl,
|
|
296
|
+
mediaLocalRoots,
|
|
297
|
+
});
|
|
298
|
+
if (botWs.sent) {
|
|
299
|
+
return {
|
|
300
|
+
channel: "wecom",
|
|
301
|
+
messageId: `bot-ws-media-${Date.now()}`,
|
|
302
|
+
timestamp: Date.now(),
|
|
303
|
+
};
|
|
215
304
|
}
|
|
305
|
+
|
|
216
306
|
const agent = resolveAgentConfigOrThrow({ cfg, accountId });
|
|
217
307
|
const deliveryService = new WecomAgentDeliveryService(agent);
|
|
218
|
-
if (!mediaUrl) {
|
|
219
|
-
throw new Error("WeCom outbound requires mediaUrl.");
|
|
220
|
-
}
|
|
221
308
|
|
|
222
309
|
let buffer: Buffer;
|
|
223
310
|
let contentType: string;
|
|
@@ -246,23 +333,49 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
246
333
|
// 根据扩展名推断 content-type
|
|
247
334
|
const ext = path.extname(mediaUrl).slice(1).toLowerCase();
|
|
248
335
|
const mimeTypes: Record<string, string> = {
|
|
249
|
-
jpg: "image/jpeg",
|
|
250
|
-
|
|
251
|
-
|
|
336
|
+
jpg: "image/jpeg",
|
|
337
|
+
jpeg: "image/jpeg",
|
|
338
|
+
png: "image/png",
|
|
339
|
+
gif: "image/gif",
|
|
340
|
+
webp: "image/webp",
|
|
341
|
+
bmp: "image/bmp",
|
|
342
|
+
mp3: "audio/mpeg",
|
|
343
|
+
wav: "audio/wav",
|
|
344
|
+
amr: "audio/amr",
|
|
345
|
+
mp4: "video/mp4",
|
|
346
|
+
pdf: "application/pdf",
|
|
347
|
+
doc: "application/msword",
|
|
252
348
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
253
|
-
xls: "application/vnd.ms-excel",
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
349
|
+
xls: "application/vnd.ms-excel",
|
|
350
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
351
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
352
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
353
|
+
txt: "text/plain",
|
|
354
|
+
csv: "text/csv",
|
|
355
|
+
tsv: "text/tab-separated-values",
|
|
356
|
+
md: "text/markdown",
|
|
357
|
+
json: "application/json",
|
|
358
|
+
xml: "application/xml",
|
|
359
|
+
yaml: "application/yaml",
|
|
360
|
+
yml: "application/yaml",
|
|
361
|
+
zip: "application/zip",
|
|
362
|
+
rar: "application/vnd.rar",
|
|
363
|
+
"7z": "application/x-7z-compressed",
|
|
364
|
+
tar: "application/x-tar",
|
|
365
|
+
gz: "application/gzip",
|
|
366
|
+
tgz: "application/gzip",
|
|
367
|
+
rtf: "application/rtf",
|
|
368
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
260
369
|
};
|
|
261
370
|
contentType = mimeTypes[ext] || "application/octet-stream";
|
|
262
|
-
console.log(
|
|
371
|
+
console.log(
|
|
372
|
+
`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`,
|
|
373
|
+
);
|
|
263
374
|
}
|
|
264
375
|
|
|
265
|
-
console.log(
|
|
376
|
+
console.log(
|
|
377
|
+
`[wecom-outbound] Sending media to ${String(to ?? "")} (filename=${filename}, contentType=${contentType})`,
|
|
378
|
+
);
|
|
266
379
|
|
|
267
380
|
try {
|
|
268
381
|
await deliveryService.sendMedia({
|
|
@@ -280,7 +393,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
|
|
|
280
393
|
|
|
281
394
|
return {
|
|
282
395
|
channel: "wecom",
|
|
283
|
-
messageId:
|
|
396
|
+
messageId: `${botWs.attempted ? "agent-fallback-media" : "agent-media"}-${Date.now()}`,
|
|
284
397
|
timestamp: Date.now(),
|
|
285
398
|
};
|
|
286
399
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
2
|
import { dispatchRuntimeReply } from "./reply-orchestrator.js";
|
|
4
3
|
|
|
5
4
|
describe("dispatchRuntimeReply", () => {
|
|
6
5
|
it("enables block streaming for bot-ws replies", async () => {
|
|
7
|
-
const dispatchReplyWithBufferedBlockDispatcher = vi
|
|
6
|
+
const dispatchReplyWithBufferedBlockDispatcher = vi
|
|
7
|
+
.fn()
|
|
8
|
+
.mockResolvedValue({ queuedFinal: false, counts: { block: 0, final: 0, tool: 0 } });
|
|
8
9
|
const core = {
|
|
9
10
|
channel: {
|
|
10
11
|
reply: {
|
|
@@ -35,4 +36,36 @@ describe("dispatchRuntimeReply", () => {
|
|
|
35
36
|
}),
|
|
36
37
|
);
|
|
37
38
|
});
|
|
39
|
+
|
|
40
|
+
it("synthesizes a final close for bot-ws when only block replies were queued", async () => {
|
|
41
|
+
const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue({
|
|
42
|
+
queuedFinal: false,
|
|
43
|
+
counts: { block: 1, final: 0, tool: 0 },
|
|
44
|
+
});
|
|
45
|
+
const deliver = vi.fn().mockResolvedValue(undefined);
|
|
46
|
+
const core = {
|
|
47
|
+
channel: {
|
|
48
|
+
reply: {
|
|
49
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
} as any;
|
|
53
|
+
|
|
54
|
+
await dispatchRuntimeReply({
|
|
55
|
+
core,
|
|
56
|
+
cfg: {} as any,
|
|
57
|
+
session: { ctx: { SessionKey: "session-a" } } as any,
|
|
58
|
+
replyHandle: {
|
|
59
|
+
context: {
|
|
60
|
+
transport: "bot-ws",
|
|
61
|
+
accountId: "default",
|
|
62
|
+
raw: { transport: "bot-ws", envelopeType: "ws", body: {} },
|
|
63
|
+
},
|
|
64
|
+
deliver,
|
|
65
|
+
} as any,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(deliver).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(deliver).toHaveBeenCalledWith({ text: "" }, { kind: "final" });
|
|
70
|
+
});
|
|
38
71
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
2
|
import type { ReplyHandle } from "../types/index.js";
|
|
4
3
|
import type { PreparedSession } from "./session-manager.js";
|
|
5
4
|
|
|
@@ -29,7 +28,7 @@ export async function dispatchRuntimeReply(params: {
|
|
|
29
28
|
replyHandle: ReplyHandle;
|
|
30
29
|
}): Promise<void> {
|
|
31
30
|
const { core, cfg, session, replyHandle } = params;
|
|
32
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
31
|
+
const result = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
33
32
|
ctx: session.ctx,
|
|
34
33
|
cfg,
|
|
35
34
|
replyOptions:
|
|
@@ -52,4 +51,17 @@ export async function dispatchRuntimeReply(params: {
|
|
|
52
51
|
},
|
|
53
52
|
},
|
|
54
53
|
});
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
replyHandle.context.transport === "bot-ws" &&
|
|
57
|
+
result &&
|
|
58
|
+
result.queuedFinal !== true &&
|
|
59
|
+
(result.counts?.block ?? 0) > 0
|
|
60
|
+
) {
|
|
61
|
+
await dispatchReplyPayload({
|
|
62
|
+
replyHandle,
|
|
63
|
+
payload: { text: "" },
|
|
64
|
+
kind: "final",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
55
67
|
}
|