@yanhaidao/wecom 2.3.160 → 2.3.190
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 +294 -379
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2288 -0
- package/changelog/v2.3.18.md +22 -0
- package/changelog/v2.3.19.md +73 -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 +20 -1
- package/src/capability/bot/stream-orchestrator.ts +1 -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 +788 -64
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1517 -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/index.ts +7 -1
- package/src/config/media.test.ts +113 -0
- package/src/config/media.ts +133 -6
- package/src/config/schema.ts +74 -102
- package/src/outbound.test.ts +250 -15
- package/src/outbound.ts +155 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/routing-bridge.test.ts +115 -0
- package/src/runtime/routing-bridge.ts +26 -1
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
- package/src/transport/bot-ws/media.test.ts +44 -0
- package/src/transport/bot-ws/media.ts +272 -0
- package/src/transport/bot-ws/reply.test.ts +216 -18
- package/src/transport/bot-ws/reply.ts +116 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +89 -12
- package/src/types/config.ts +3 -0
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
package/src/config/media.ts
CHANGED
|
@@ -1,14 +1,141 @@
|
|
|
1
|
-
import
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
resolveChannelMediaMaxBytes,
|
|
5
|
+
resolvePreferredOpenClawTmpDir,
|
|
6
|
+
type OpenClawConfig,
|
|
7
|
+
} from "openclaw/plugin-sdk";
|
|
2
8
|
|
|
3
9
|
// 默认给一个相对“够用”的上限(80MB),避免视频/较大文件频繁触发失败。
|
|
4
10
|
// 仍保留上限以防止恶意大文件把进程内存打爆(下载实现会读入内存再保存)。
|
|
5
11
|
export const DEFAULT_WECOM_MEDIA_MAX_BYTES = 80 * 1024 * 1024;
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
function parsePositiveNumber(value: unknown): number | undefined {
|
|
14
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveStateDirForWecomMedia(): string {
|
|
22
|
+
const stateOverride =
|
|
23
|
+
process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim();
|
|
24
|
+
if (stateOverride) {
|
|
25
|
+
return stateOverride;
|
|
26
|
+
}
|
|
27
|
+
return path.join(os.homedir(), ".openclaw");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeWecomLocalRoot(root: string): string | undefined {
|
|
31
|
+
const trimmed = root.trim();
|
|
32
|
+
if (!trimmed) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(trimmed.replace(/^~(?=\/|$)/, os.homedir()));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getWecomCommonUserMediaLocalRoots(): readonly string[] {
|
|
39
|
+
const home = os.homedir();
|
|
40
|
+
return [
|
|
41
|
+
path.join(home, "Desktop"),
|
|
42
|
+
path.join(home, "Documents"),
|
|
43
|
+
path.join(home, "Downloads"),
|
|
44
|
+
path.join(home, "Movies"),
|
|
45
|
+
path.join(home, "Pictures"),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getWecomDefaultMediaLocalRoots(): readonly string[] {
|
|
50
|
+
const stateDir = path.resolve(resolveStateDirForWecomMedia());
|
|
51
|
+
return [
|
|
52
|
+
path.resolve(resolvePreferredOpenClawTmpDir()),
|
|
53
|
+
stateDir,
|
|
54
|
+
path.join(stateDir, "media"),
|
|
55
|
+
path.join(stateDir, "agents"),
|
|
56
|
+
path.join(stateDir, "workspace"),
|
|
57
|
+
path.join(stateDir, "sandboxes"),
|
|
58
|
+
...getWecomCommonUserMediaLocalRoots(),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveWecomConfiguredMediaLocalRoots(cfg: OpenClawConfig): readonly string[] {
|
|
63
|
+
const rawWecom = cfg.channels?.wecom as
|
|
64
|
+
| {
|
|
65
|
+
media?: { localRoots?: unknown };
|
|
66
|
+
mediaLocalRoots?: unknown;
|
|
67
|
+
}
|
|
68
|
+
| undefined;
|
|
69
|
+
const configured = Array.isArray(rawWecom?.media?.localRoots)
|
|
70
|
+
? rawWecom.media.localRoots
|
|
71
|
+
: Array.isArray(rawWecom?.mediaLocalRoots)
|
|
72
|
+
? rawWecom.mediaLocalRoots
|
|
73
|
+
: [];
|
|
74
|
+
return configured
|
|
75
|
+
.filter((root): root is string => typeof root === "string")
|
|
76
|
+
.map(normalizeWecomLocalRoot)
|
|
77
|
+
.filter((root): root is string => Boolean(root));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveWecomMergedMediaLocalRoots(params: {
|
|
81
|
+
cfg: OpenClawConfig;
|
|
82
|
+
baseRoots?: readonly string[];
|
|
83
|
+
}): readonly string[] {
|
|
84
|
+
const merged: string[] = [];
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
const pushRoot = (root: string) => {
|
|
87
|
+
const normalized = normalizeWecomLocalRoot(root);
|
|
88
|
+
if (!normalized || seen.has(normalized)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
seen.add(normalized);
|
|
92
|
+
merged.push(normalized);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const root of getWecomDefaultMediaLocalRoots()) {
|
|
96
|
+
pushRoot(root);
|
|
97
|
+
}
|
|
98
|
+
for (const root of params.baseRoots ?? []) {
|
|
99
|
+
pushRoot(root);
|
|
100
|
+
}
|
|
101
|
+
for (const root of resolveWecomConfiguredMediaLocalRoots(params.cfg)) {
|
|
102
|
+
pushRoot(root);
|
|
103
|
+
}
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveLegacyWecomMediaMaxBytes(cfg: OpenClawConfig): number | undefined {
|
|
8
108
|
const raw = (cfg.channels?.wecom as any)?.media?.maxBytes;
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
11
|
-
return Math.floor(
|
|
109
|
+
const bytes = parsePositiveNumber(raw);
|
|
110
|
+
if (bytes) {
|
|
111
|
+
return Math.floor(bytes);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveWecomMediaMaxBytes(
|
|
117
|
+
cfg: OpenClawConfig,
|
|
118
|
+
accountId?: string | null,
|
|
119
|
+
): number {
|
|
120
|
+
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
|
121
|
+
cfg,
|
|
122
|
+
accountId,
|
|
123
|
+
resolveChannelLimitMb: ({ cfg, accountId }) => {
|
|
124
|
+
const wecom = cfg.channels?.wecom as
|
|
125
|
+
| {
|
|
126
|
+
mediaMaxMb?: unknown;
|
|
127
|
+
accounts?: Record<string, { mediaMaxMb?: unknown }>;
|
|
128
|
+
}
|
|
129
|
+
| undefined;
|
|
130
|
+
const accountLimitMb = parsePositiveNumber(wecom?.accounts?.[accountId]?.mediaMaxMb);
|
|
131
|
+
if (accountLimitMb) {
|
|
132
|
+
return accountLimitMb;
|
|
133
|
+
}
|
|
134
|
+
return parsePositiveNumber(wecom?.mediaMaxMb);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (mediaMaxBytes) {
|
|
138
|
+
return mediaMaxBytes;
|
|
12
139
|
}
|
|
13
|
-
return DEFAULT_WECOM_MEDIA_MAX_BYTES;
|
|
140
|
+
return resolveLegacyWecomMediaMaxBytes(cfg) ?? DEFAULT_WECOM_MEDIA_MAX_BYTES;
|
|
14
141
|
}
|
package/src/config/schema.ts
CHANGED
|
@@ -1,114 +1,86 @@
|
|
|
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
|
+
localRoots?: string[];
|
|
12
|
+
}
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
.optional();
|
|
14
|
+
export interface NetworkConfig {
|
|
15
|
+
egressProxyUrl?: string;
|
|
16
|
+
}
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
.optional();
|
|
18
|
+
export interface RoutingConfig {
|
|
19
|
+
failClosedOnDefaultRoute?: boolean;
|
|
20
|
+
}
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})
|
|
44
|
-
.optional();
|
|
22
|
+
export interface BotWsConfig {
|
|
23
|
+
botId: string;
|
|
24
|
+
secret: string;
|
|
25
|
+
}
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
.optional();
|
|
27
|
+
export interface BotWebhookConfig {
|
|
28
|
+
token: string;
|
|
29
|
+
encodingAESKey: string;
|
|
30
|
+
receiveId?: string;
|
|
31
|
+
}
|
|
53
32
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
65
|
-
.optional();
|
|
33
|
+
export interface BotConfig {
|
|
34
|
+
primaryTransport?: "ws" | "webhook";
|
|
35
|
+
streamPlaceholderContent?: string;
|
|
36
|
+
welcomeText?: string;
|
|
37
|
+
dm?: DmConfig;
|
|
38
|
+
aibotid?: string;
|
|
39
|
+
botIds?: string[];
|
|
40
|
+
ws?: BotWsConfig;
|
|
41
|
+
webhook?: BotWebhookConfig;
|
|
42
|
+
}
|
|
66
43
|
|
|
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();
|
|
44
|
+
export interface AgentConfig {
|
|
45
|
+
corpId: string;
|
|
46
|
+
agentSecret?: string;
|
|
47
|
+
corpSecret?: string;
|
|
48
|
+
agentId?: number | string;
|
|
49
|
+
token: string;
|
|
50
|
+
encodingAESKey: string;
|
|
51
|
+
welcomeText?: string;
|
|
52
|
+
dm?: DmConfig;
|
|
53
|
+
}
|
|
83
54
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
.optional();
|
|
55
|
+
export interface DynamicAgentsConfig {
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
dmCreateAgent?: boolean;
|
|
58
|
+
groupEnabled?: boolean;
|
|
59
|
+
adminUsers?: string[];
|
|
60
|
+
}
|
|
92
61
|
|
|
93
|
-
|
|
94
|
-
enabled
|
|
95
|
-
name
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
62
|
+
export interface AccountConfig {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
name?: string;
|
|
65
|
+
mediaMaxMb?: number;
|
|
66
|
+
bot?: BotConfig;
|
|
67
|
+
agent?: AgentConfig;
|
|
68
|
+
}
|
|
99
69
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
70
|
+
export interface WecomConfigInput {
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
mediaMaxMb?: number;
|
|
73
|
+
bot?: BotConfig;
|
|
74
|
+
agent?: AgentConfig;
|
|
75
|
+
accounts?: Record<string, AccountConfig>;
|
|
76
|
+
defaultAccount?: string;
|
|
77
|
+
media?: MediaConfig;
|
|
78
|
+
network?: NetworkConfig;
|
|
79
|
+
routing?: RoutingConfig;
|
|
80
|
+
dynamicAgents?: DynamicAgentsConfig;
|
|
81
|
+
}
|
|
113
82
|
|
|
114
|
-
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated No longer a Zod schema. Kept as a type-only export for backward compatibility.
|
|
85
|
+
*/
|
|
86
|
+
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,228 @@ 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
|
+
maxBytes: 80 * 1024 * 1024,
|
|
343
|
+
mediaUrl: "https://example.com/media.png",
|
|
344
|
+
mediaLocalRoots: expect.any(Array),
|
|
345
|
+
text: "caption",
|
|
282
346
|
});
|
|
347
|
+
expect(api.sendMedia).not.toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("merges configured media local roots into Bot WS sends", async () => {
|
|
351
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
352
|
+
const runtime = await import("./runtime.js");
|
|
353
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-merged" });
|
|
354
|
+
runtime.registerBotWsPushHandle(
|
|
355
|
+
"default",
|
|
356
|
+
createBotWsHandle({
|
|
357
|
+
sendMedia,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const cfg = {
|
|
362
|
+
channels: {
|
|
363
|
+
wecom: {
|
|
364
|
+
enabled: true,
|
|
365
|
+
bot: {
|
|
366
|
+
primaryTransport: "ws",
|
|
367
|
+
ws: {
|
|
368
|
+
botId: "bot-1",
|
|
369
|
+
secret: "secret-1",
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
media: {
|
|
373
|
+
localRoots: ["/tmp/downloads"],
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
await wecomOutbound.sendMedia({
|
|
380
|
+
cfg,
|
|
381
|
+
to: "user:zhangsan",
|
|
382
|
+
mediaUrl: "/tmp/workspace-agent/01.png",
|
|
383
|
+
mediaLocalRoots: ["/tmp/workspace-agent"],
|
|
384
|
+
} as any);
|
|
385
|
+
|
|
386
|
+
expect(sendMedia).toHaveBeenCalledWith(
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
chatId: "zhangsan",
|
|
389
|
+
mediaUrl: "/tmp/workspace-agent/01.png",
|
|
390
|
+
mediaLocalRoots: expect.arrayContaining(["/tmp/workspace-agent", "/tmp/downloads"]),
|
|
391
|
+
text: undefined,
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("passes account-aware mediaMaxMb to Bot WS media sends", async () => {
|
|
397
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
398
|
+
const runtime = await import("./runtime.js");
|
|
399
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-limit" });
|
|
400
|
+
runtime.registerBotWsPushHandle(
|
|
401
|
+
"acct-ws",
|
|
402
|
+
createBotWsHandle({
|
|
403
|
+
sendMedia,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const cfg = {
|
|
408
|
+
agents: {
|
|
409
|
+
defaults: {
|
|
410
|
+
mediaMaxMb: 12,
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
channels: {
|
|
414
|
+
wecom: {
|
|
415
|
+
enabled: true,
|
|
416
|
+
mediaMaxMb: 24,
|
|
417
|
+
accounts: {
|
|
418
|
+
"acct-ws": {
|
|
419
|
+
enabled: true,
|
|
420
|
+
mediaMaxMb: 36,
|
|
421
|
+
bot: {
|
|
422
|
+
primaryTransport: "ws",
|
|
423
|
+
ws: {
|
|
424
|
+
botId: "bot-1",
|
|
425
|
+
secret: "secret-1",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
await wecomOutbound.sendMedia({
|
|
435
|
+
cfg,
|
|
436
|
+
accountId: "acct-ws",
|
|
437
|
+
to: "user:zhangsan",
|
|
438
|
+
mediaUrl: "https://example.com/media.png",
|
|
439
|
+
} as any);
|
|
440
|
+
|
|
441
|
+
expect(sendMedia).toHaveBeenCalledWith(
|
|
442
|
+
expect.objectContaining({
|
|
443
|
+
chatId: "zhangsan",
|
|
444
|
+
maxBytes: 36 * 1024 * 1024,
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not fall back to Agent media when Bot WS conversation media delivery fails", async () => {
|
|
450
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
451
|
+
const runtime = await import("./runtime.js");
|
|
452
|
+
const api = await import("./transport/agent-api/core.js");
|
|
453
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: false, error: "upload failed" });
|
|
454
|
+
runtime.registerBotWsPushHandle(
|
|
455
|
+
"default",
|
|
456
|
+
createBotWsHandle({
|
|
457
|
+
sendMedia,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
461
|
+
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
462
|
+
(api.sendMedia as any).mockClear();
|
|
463
|
+
vi.stubGlobal(
|
|
464
|
+
"fetch",
|
|
465
|
+
vi.fn().mockResolvedValue({
|
|
466
|
+
ok: true,
|
|
467
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
468
|
+
headers: new Headers({ "content-type": "image/png" }),
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const cfg = {
|
|
473
|
+
channels: {
|
|
474
|
+
wecom: {
|
|
475
|
+
enabled: true,
|
|
476
|
+
bot: {
|
|
477
|
+
primaryTransport: "ws",
|
|
478
|
+
ws: {
|
|
479
|
+
botId: "bot-1",
|
|
480
|
+
secret: "secret-1",
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
agent: {
|
|
484
|
+
corpId: "corp",
|
|
485
|
+
corpSecret: "secret",
|
|
486
|
+
agentId: 1000002,
|
|
487
|
+
token: "token",
|
|
488
|
+
encodingAESKey: "aes",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
await expect(
|
|
495
|
+
wecomOutbound.sendMedia({
|
|
496
|
+
cfg,
|
|
497
|
+
to: "user:zhangsan",
|
|
498
|
+
text: "caption",
|
|
499
|
+
mediaUrl: "https://example.com/media.png",
|
|
500
|
+
} as any),
|
|
501
|
+
).rejects.toThrow(/Bot WS media delivery failed/i);
|
|
502
|
+
|
|
503
|
+
expect(sendMedia).toHaveBeenCalledTimes(1);
|
|
504
|
+
expect(api.sendMedia).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("keeps explicit agent targets on the Agent media path", async () => {
|
|
508
|
+
const { wecomOutbound } = await import("./outbound.js");
|
|
509
|
+
const runtime = await import("./runtime.js");
|
|
510
|
+
const api = await import("./transport/agent-api/core.js");
|
|
511
|
+
const sendMedia = vi.fn().mockResolvedValue({ ok: true, messageId: "ws-media-1" });
|
|
512
|
+
runtime.registerBotWsPushHandle(
|
|
513
|
+
"default",
|
|
514
|
+
createBotWsHandle({
|
|
515
|
+
sendMedia,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
283
518
|
(api.uploadMedia as any).mockResolvedValue("media-1");
|
|
284
519
|
(api.sendMedia as any).mockResolvedValue(undefined);
|
|
285
520
|
(api.sendMedia as any).mockClear();
|
|
@@ -316,13 +551,13 @@ describe("wecomOutbound", () => {
|
|
|
316
551
|
|
|
317
552
|
await wecomOutbound.sendMedia({
|
|
318
553
|
cfg,
|
|
319
|
-
to: "user:zhangsan",
|
|
554
|
+
to: "wecom-agent:default:user:zhangsan",
|
|
320
555
|
text: "caption",
|
|
321
556
|
mediaUrl: "https://example.com/media.png",
|
|
322
557
|
} as any);
|
|
323
558
|
|
|
559
|
+
expect(sendMedia).not.toHaveBeenCalled();
|
|
324
560
|
expect(api.sendMedia).toHaveBeenCalledTimes(1);
|
|
325
|
-
expect(sendMarkdown).not.toHaveBeenCalled();
|
|
326
561
|
});
|
|
327
562
|
|
|
328
563
|
it("uses account-scoped agent config in matrix mode", async () => {
|