@yanhaidao/wecom 2.3.4 → 2.3.10

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.
Files changed (110) hide show
  1. package/README.md +222 -335
  2. package/assets/03.bot.page.png +0 -0
  3. package/changelog/v2.3.10.md +17 -0
  4. package/changelog/v2.3.9.md +22 -0
  5. package/compat-single-account.md +34 -4
  6. package/index.ts +5 -5
  7. package/package.json +8 -7
  8. package/src/agent/api-client.upload.test.ts +1 -2
  9. package/src/agent/handler.ts +82 -9
  10. package/src/agent/index.ts +1 -1
  11. package/src/app/account-runtime.ts +245 -0
  12. package/src/app/bootstrap.ts +29 -0
  13. package/src/app/index.ts +31 -0
  14. package/src/capability/agent/delivery-service.ts +79 -0
  15. package/src/capability/agent/fallback-policy.ts +13 -0
  16. package/src/capability/agent/index.ts +3 -0
  17. package/src/capability/agent/ingress-service.ts +38 -0
  18. package/src/capability/bot/dispatch-config.ts +47 -0
  19. package/src/capability/bot/fallback-delivery.ts +178 -0
  20. package/src/capability/bot/index.ts +1 -0
  21. package/src/capability/bot/local-path-delivery.ts +215 -0
  22. package/src/capability/bot/service.ts +56 -0
  23. package/src/capability/bot/stream-delivery.ts +379 -0
  24. package/src/capability/bot/stream-finalizer.ts +120 -0
  25. package/src/capability/bot/stream-orchestrator.ts +352 -0
  26. package/src/capability/bot/types.ts +8 -0
  27. package/src/capability/index.ts +2 -0
  28. package/src/channel.lifecycle.test.ts +9 -6
  29. package/src/channel.meta.test.ts +12 -0
  30. package/src/channel.ts +48 -21
  31. package/src/config/accounts.resolve.test.ts +39 -2
  32. package/src/config/accounts.ts +242 -280
  33. package/src/config/derived-paths.test.ts +111 -0
  34. package/src/config/derived-paths.ts +41 -0
  35. package/src/config/index.ts +10 -12
  36. package/src/config/runtime-config.ts +46 -0
  37. package/src/config/schema.ts +65 -103
  38. package/src/domain/models.ts +7 -0
  39. package/src/domain/policies.ts +36 -0
  40. package/src/dynamic-agent.ts +6 -0
  41. package/src/gateway-monitor.ts +43 -93
  42. package/src/http.ts +23 -2
  43. package/src/monitor/limits.ts +7 -0
  44. package/src/monitor/state.ts +28 -508
  45. package/src/monitor.active.test.ts +3 -3
  46. package/src/monitor.integration.test.ts +0 -1
  47. package/src/monitor.ts +64 -2603
  48. package/src/monitor.webhook.test.ts +127 -42
  49. package/src/observability/audit-log.ts +48 -0
  50. package/src/observability/legacy-operational-event-store.ts +36 -0
  51. package/src/observability/raw-envelope-log.ts +28 -0
  52. package/src/observability/status-registry.ts +13 -0
  53. package/src/observability/transport-session-view.ts +14 -0
  54. package/src/onboarding.test.ts +268 -0
  55. package/src/onboarding.ts +95 -78
  56. package/src/outbound.test.ts +5 -5
  57. package/src/outbound.ts +18 -66
  58. package/src/runtime/dispatcher.ts +52 -0
  59. package/src/runtime/index.ts +4 -0
  60. package/src/runtime/outbound-intent.ts +4 -0
  61. package/src/runtime/reply-orchestrator.test.ts +38 -0
  62. package/src/runtime/reply-orchestrator.ts +55 -0
  63. package/src/runtime/routing-bridge.ts +19 -0
  64. package/src/runtime/session-manager.ts +76 -0
  65. package/src/runtime.ts +7 -14
  66. package/src/shared/command-auth.ts +1 -17
  67. package/src/shared/media-service.ts +36 -0
  68. package/src/shared/media-types.ts +5 -0
  69. package/src/store/active-reply-store.ts +42 -0
  70. package/src/store/interfaces.ts +11 -0
  71. package/src/store/memory-store.ts +43 -0
  72. package/src/store/stream-batch-store.ts +350 -0
  73. package/src/target.ts +28 -0
  74. package/src/transport/agent-api/client.ts +44 -0
  75. package/src/transport/agent-api/core.ts +367 -0
  76. package/src/transport/agent-api/delivery.ts +41 -0
  77. package/src/transport/agent-api/media-upload.ts +11 -0
  78. package/src/transport/agent-api/reply.ts +39 -0
  79. package/src/transport/agent-callback/http-handler.ts +47 -0
  80. package/src/transport/agent-callback/inbound.ts +5 -0
  81. package/src/transport/agent-callback/reply.ts +13 -0
  82. package/src/transport/agent-callback/request-handler.ts +244 -0
  83. package/src/transport/agent-callback/session.ts +23 -0
  84. package/src/transport/bot-webhook/active-reply.ts +36 -0
  85. package/src/transport/bot-webhook/http-handler.ts +48 -0
  86. package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
  87. package/src/transport/bot-webhook/inbound.ts +5 -0
  88. package/src/transport/bot-webhook/message-shape.ts +89 -0
  89. package/src/transport/bot-webhook/protocol.ts +148 -0
  90. package/src/transport/bot-webhook/reply.ts +15 -0
  91. package/src/transport/bot-webhook/request-handler.ts +394 -0
  92. package/src/transport/bot-webhook/session.ts +23 -0
  93. package/src/transport/bot-ws/inbound.ts +108 -0
  94. package/src/transport/bot-ws/reply.ts +63 -0
  95. package/src/transport/bot-ws/sdk-adapter.ts +180 -0
  96. package/src/transport/bot-ws/session.ts +28 -0
  97. package/src/transport/http/common.ts +109 -0
  98. package/src/transport/http/registry.ts +92 -0
  99. package/src/transport/http/request-handler.ts +84 -0
  100. package/src/transport/index.ts +14 -0
  101. package/src/types/account.ts +56 -91
  102. package/src/types/config.ts +64 -112
  103. package/src/types/constants.ts +20 -35
  104. package/src/types/events.ts +21 -0
  105. package/src/types/index.ts +14 -38
  106. package/src/types/legacy-stream.ts +50 -0
  107. package/src/types/runtime-context.ts +28 -0
  108. package/src/types/runtime.ts +161 -0
  109. package/src/agent/api-client.ts +0 -383
  110. package/src/monitor/types.ts +0 -136
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
4
+
5
+ import { resolveDerivedPathSummary } from "./derived-paths.js";
6
+ import {
7
+ hasMatrixExplicitRoutesRegistered,
8
+ registerAgentWebhookTarget,
9
+ registerWecomWebhookTarget,
10
+ } from "../transport/http/registry.js";
11
+ import type { ResolvedAgentAccount, ResolvedBotAccount } from "../types/index.js";
12
+
13
+ function createBotAccount(accountId: string): ResolvedBotAccount {
14
+ return {
15
+ accountId,
16
+ configured: true,
17
+ primaryTransport: "webhook",
18
+ wsConfigured: false,
19
+ webhookConfigured: true,
20
+ config: {} as ResolvedBotAccount["config"],
21
+ token: "token",
22
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
23
+ receiveId: "",
24
+ botId: "",
25
+ secret: "",
26
+ webhook: {
27
+ token: "token",
28
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
29
+ receiveId: "",
30
+ },
31
+ };
32
+ }
33
+
34
+ function createAgentAccount(accountId: string): ResolvedAgentAccount {
35
+ return {
36
+ accountId,
37
+ configured: true,
38
+ callbackConfigured: true,
39
+ apiConfigured: true,
40
+ corpId: `corp-${accountId}`,
41
+ corpSecret: `secret-${accountId}`,
42
+ agentId: 1001,
43
+ token: "token",
44
+ encodingAESKey: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
45
+ config: {} as ResolvedAgentAccount["config"],
46
+ };
47
+ }
48
+
49
+ const emptyConfig = {} as OpenClawConfig;
50
+ const emptyCore = {} as PluginRuntime;
51
+
52
+ describe("resolveDerivedPathSummary", () => {
53
+ it("registers scoped aliases first for the default account", () => {
54
+ expect(resolveDerivedPathSummary("default")).toEqual({
55
+ botWebhook: [
56
+ "/plugins/wecom/bot/default",
57
+ "/wecom/bot/default",
58
+ "/plugins/wecom/bot",
59
+ "/wecom",
60
+ "/wecom/bot",
61
+ ],
62
+ agentCallback: [
63
+ "/plugins/wecom/agent/default",
64
+ "/wecom/agent/default",
65
+ "/plugins/wecom/agent",
66
+ "/wecom/agent",
67
+ ],
68
+ });
69
+ });
70
+ });
71
+
72
+ describe("hasMatrixExplicitRoutesRegistered", () => {
73
+ it("ignores default-account scoped aliases", () => {
74
+ const unregisterBot = registerWecomWebhookTarget({
75
+ account: createBotAccount("default"),
76
+ config: emptyConfig,
77
+ runtime: {},
78
+ core: emptyCore,
79
+ path: "/plugins/wecom/bot/default",
80
+ });
81
+ const unregisterAgent = registerAgentWebhookTarget({
82
+ agent: createAgentAccount("default"),
83
+ config: emptyConfig,
84
+ runtimeEnv: {},
85
+ path: "/plugins/wecom/agent/default",
86
+ });
87
+
88
+ try {
89
+ expect(hasMatrixExplicitRoutesRegistered()).toBe(false);
90
+ } finally {
91
+ unregisterAgent();
92
+ unregisterBot();
93
+ }
94
+ });
95
+
96
+ it("detects non-default explicit account routes", () => {
97
+ const unregister = registerWecomWebhookTarget({
98
+ account: createBotAccount("acct-a"),
99
+ config: emptyConfig,
100
+ runtime: {},
101
+ core: emptyCore,
102
+ path: "/plugins/wecom/bot/acct-a",
103
+ });
104
+
105
+ try {
106
+ expect(hasMatrixExplicitRoutesRegistered()).toBe(true);
107
+ } finally {
108
+ unregister();
109
+ }
110
+ });
111
+ });
@@ -0,0 +1,41 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "./accounts.js";
2
+ import type { WecomTransportKind } from "../types/runtime.js";
3
+ import { WEBHOOK_PATHS } from "../types/constants.js";
4
+
5
+ export function resolveDerivedPath(params: {
6
+ accountId: string;
7
+ transport: Extract<WecomTransportKind, "bot-webhook" | "agent-callback">;
8
+ includeLegacy?: boolean;
9
+ }): string[] {
10
+ const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
11
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
12
+ if (params.transport === "bot-webhook") {
13
+ return isDefault
14
+ ? [
15
+ `${WEBHOOK_PATHS.BOT_PLUGIN}/${accountId}`,
16
+ `${WEBHOOK_PATHS.BOT}/${accountId}`,
17
+ WEBHOOK_PATHS.BOT_PLUGIN,
18
+ WEBHOOK_PATHS.BOT_ALT,
19
+ WEBHOOK_PATHS.BOT,
20
+ ]
21
+ : [`${WEBHOOK_PATHS.BOT_PLUGIN}/${accountId}`];
22
+ }
23
+ return isDefault
24
+ ? [
25
+ `${WEBHOOK_PATHS.AGENT_PLUGIN}/${accountId}`,
26
+ `${WEBHOOK_PATHS.AGENT}/${accountId}`,
27
+ WEBHOOK_PATHS.AGENT_PLUGIN,
28
+ WEBHOOK_PATHS.AGENT,
29
+ ]
30
+ : [`${WEBHOOK_PATHS.AGENT_PLUGIN}/${accountId}`];
31
+ }
32
+
33
+ export function resolveDerivedPathSummary(accountId: string): {
34
+ botWebhook: string[];
35
+ agentCallback: string[];
36
+ } {
37
+ return {
38
+ botWebhook: resolveDerivedPath({ accountId, transport: "bot-webhook" }),
39
+ agentCallback: resolveDerivedPath({ accountId, transport: "agent-callback" }),
40
+ };
41
+ }
@@ -1,18 +1,16 @@
1
- /**
2
- * WeCom 配置模块导出
3
- */
4
-
5
1
  export { WecomConfigSchema, type WecomConfigInput } from "./schema.js";
6
2
  export {
7
- DEFAULT_ACCOUNT_ID,
8
- detectMode,
9
- listWecomAccountIds,
10
- resolveDefaultWecomAccountId,
11
- resolveWecomAccount,
12
- resolveWecomAccountConflict,
13
- resolveWecomAccounts,
14
- isWecomEnabled,
3
+ DEFAULT_ACCOUNT_ID,
4
+ detectMode,
5
+ listWecomAccountIds,
6
+ resolveDefaultWecomAccountId,
7
+ resolveWecomAccount,
8
+ resolveWecomAccountConflict,
9
+ resolveWecomAccounts,
10
+ isWecomEnabled,
15
11
  } from "./accounts.js";
12
+ export { resolveWecomRuntimeAccount, resolveWecomRuntimeConfig, type ResolvedRuntimeAccount, type ResolvedRuntimeConfig } from "./runtime-config.js";
13
+ export { resolveDerivedPath, resolveDerivedPathSummary } from "./derived-paths.js";
16
14
  export { resolveWecomEgressProxyUrl, resolveWecomEgressProxyUrlFromNetwork } from "./network.js";
17
15
  export { DEFAULT_WECOM_MEDIA_MAX_BYTES, resolveWecomMediaMaxBytes } from "./media.js";
18
16
  export { resolveWecomFailClosedOnDefaultRoute, shouldRejectWecomDefaultRoute } from "./routing.js";
@@ -0,0 +1,46 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+
3
+ import { resolveDerivedPathSummary } from "./derived-paths.js";
4
+ import { DEFAULT_ACCOUNT_ID, resolveWecomAccount, resolveWecomAccounts } from "./accounts.js";
5
+ import type { ResolvedWecomAccount, WecomConfig } from "../types/index.js";
6
+
7
+ export type ResolvedRuntimeAccount = {
8
+ account: ResolvedWecomAccount;
9
+ derivedPaths: ReturnType<typeof resolveDerivedPathSummary>;
10
+ };
11
+
12
+ export type ResolvedRuntimeConfig = {
13
+ raw: WecomConfig | undefined;
14
+ defaultAccountId: string;
15
+ accounts: Record<string, ResolvedRuntimeAccount>;
16
+ };
17
+
18
+ export function resolveWecomRuntimeConfig(cfg: OpenClawConfig): ResolvedRuntimeConfig {
19
+ const raw = cfg.channels?.wecom as WecomConfig | undefined;
20
+ const resolved = resolveWecomAccounts(cfg);
21
+ const accounts = Object.fromEntries(
22
+ Object.entries(resolved.accounts).map(([accountId, account]) => [
23
+ accountId,
24
+ {
25
+ account,
26
+ derivedPaths: resolveDerivedPathSummary(accountId),
27
+ },
28
+ ]),
29
+ );
30
+ return {
31
+ raw,
32
+ defaultAccountId: resolved.defaultAccountId || DEFAULT_ACCOUNT_ID,
33
+ accounts,
34
+ };
35
+ }
36
+
37
+ export function resolveWecomRuntimeAccount(params: {
38
+ cfg: OpenClawConfig;
39
+ accountId?: string | null;
40
+ }): ResolvedRuntimeAccount {
41
+ const account = resolveWecomAccount(params);
42
+ return {
43
+ account,
44
+ derivedPaths: resolveDerivedPathSummary(account.accountId),
45
+ };
46
+ }
@@ -1,143 +1,104 @@
1
- /**
2
- * WeCom 配置 Schema (Zod)
3
- */
4
-
5
1
  import { z } from "zod";
6
2
 
7
3
  function bindToJsonSchema<T extends z.ZodTypeAny>(schema: T): T {
8
- const anySchema = schema as unknown as { toJSONSchema?: (...args: any[]) => unknown };
9
- if (typeof anySchema.toJSONSchema === "function") {
10
- anySchema.toJSONSchema = anySchema.toJSONSchema.bind(schema) as any;
11
- }
12
- return schema;
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;
13
9
  }
14
10
 
15
- /**
16
- * **dmSchema (单聊配置)**
17
- *
18
- * 控制单聊行为(如允许名单、策略)。
19
- * @property enabled - 是否启用单聊 [默认: true]
20
- * @property policy - 访问策略: "pairing" (需配对, 默认), "allowlist" (仅在名单), "open" (所有人), "disabled" (禁用)
21
- * @property allowFrom - 允许的用户ID或群ID列表 (仅当 policy="allowlist" 时生效)
22
- */
23
- const dmSchema = z.object({
24
- enabled: z.boolean().optional(),
11
+ const dmSchema = z
12
+ .object({
25
13
  policy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
26
14
  allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
27
- }).optional();
15
+ })
16
+ .optional();
28
17
 
29
- /**
30
- * **mediaSchema (媒体处理配置)**
31
- *
32
- * 控制媒体文件的下载和缓存行为。
33
- * @property tempDir - 临时文件下载目录
34
- * @property retentionHours - 临时文件保留时间(小时)
35
- * @property cleanupOnStart - 启动时是否自动清理旧文件
36
- * @property maxBytes - 允许下载的最大字节数
37
- */
38
- const mediaSchema = z.object({
18
+ const mediaSchema = z
19
+ .object({
39
20
  tempDir: z.string().optional(),
40
21
  retentionHours: z.number().optional(),
41
22
  cleanupOnStart: z.boolean().optional(),
42
23
  maxBytes: z.number().optional(),
43
- }).optional();
24
+ })
25
+ .optional();
44
26
 
45
- /**
46
- * **networkSchema (网络配置)**
47
- *
48
- * 控制 HTTP 请求行为,特别是出站代理。
49
- * @property timeoutMs - 请求超时时间 (毫秒)
50
- * @property retries - 重试次数
51
- * @property retryDelayMs - 重试间隔 (毫秒)
52
- * @property egressProxyUrl - 出站 HTTP 代理 (如 "http://127.0.0.1:7890")
53
- */
54
- const networkSchema = z.object({
55
- timeoutMs: z.number().optional(),
56
- retries: z.number().optional(),
57
- retryDelayMs: z.number().optional(),
27
+ const networkSchema = z
28
+ .object({
58
29
  egressProxyUrl: z.string().optional(),
59
- }).optional();
30
+ })
31
+ .optional();
60
32
 
61
- /**
62
- * **routingSchema (路由策略配置)**
63
- *
64
- * 控制未命中 bindings 时的回退行为。
65
- * @property failClosedOnDefaultRoute - true=拒绝 default 回退,false=允许回退默认 agent
66
- */
67
- const routingSchema = z.object({
33
+ const routingSchema = z
34
+ .object({
68
35
  failClosedOnDefaultRoute: z.boolean().optional(),
69
- }).optional();
36
+ })
37
+ .optional();
70
38
 
71
- /**
72
- * **botSchema (Bot 模式配置)**
73
- *
74
- * 用于配置企业微信内部机器人 (Webhook 模式)
75
- * @property token - 企业微信后台设置的 Token
76
- * @property encodingAESKey - 企业微信后台设置的 EncodingAESKey
77
- * @property receiveId - (可选) 接收者ID,通常不用填
78
- * @property streamPlaceholderContent - (可选) 流式响应中的占位符,默认为 "Thinking..."或空
79
- * @property welcomeText - (可选) 用户首次对话时的欢迎语
80
- * @property dm - 单聊策略覆盖配置
81
- */
82
- const botSchema = z.object({
83
- aibotid: z.string().optional(),
39
+ const botWsSchema = z
40
+ .object({
41
+ botId: z.string(),
42
+ secret: z.string(),
43
+ })
44
+ .optional();
45
+
46
+ const botWebhookSchema = z
47
+ .object({
84
48
  token: z.string(),
85
49
  encodingAESKey: z.string(),
86
- botIds: z.array(z.string()).optional(),
87
50
  receiveId: z.string().optional(),
51
+ })
52
+ .optional();
53
+
54
+ const botSchema = z
55
+ .object({
56
+ primaryTransport: z.enum(["ws", "webhook"]).optional(),
88
57
  streamPlaceholderContent: z.string().optional(),
89
58
  welcomeText: z.string().optional(),
90
59
  dm: dmSchema,
91
- }).optional();
60
+ aibotid: z.string().optional(),
61
+ botIds: z.array(z.string()).optional(),
62
+ ws: botWsSchema,
63
+ webhook: botWebhookSchema,
64
+ })
65
+ .optional();
92
66
 
93
- /**
94
- * **agentSchema (Agent 模式配置)**
95
- *
96
- * 用于配置企业微信自建应用 (Agent)。
97
- * @property corpId - 企业 ID (CorpID)
98
- * @property corpSecret - 应用 Secret
99
- * @property agentId - 应用 AgentId (数字,可选)
100
- * @property token - 回调配置 Token
101
- * @property encodingAESKey - 回调配置 EncodingAESKey
102
- * @property welcomeText - (可选) 欢迎语
103
- * @property dm - 单聊策略覆盖配置
104
- */
105
- const agentSchema = z.object({
67
+ const agentSchema = z
68
+ .object({
106
69
  corpId: z.string(),
107
- corpSecret: z.string(),
108
- agentId: z.union([z.string(), z.number()]).optional(),
70
+ agentSecret: z.string().optional(),
71
+ corpSecret: z.string().optional(),
72
+ agentId: z.union([z.number(), z.string()]).optional(),
109
73
  token: z.string(),
110
74
  encodingAESKey: z.string(),
111
75
  welcomeText: z.string().optional(),
112
76
  dm: dmSchema,
113
- }).optional();
77
+ })
78
+ .refine((value) => Boolean(value.agentSecret?.trim() || value.corpSecret?.trim()), {
79
+ path: ["agentSecret"],
80
+ message: "agentSecret 不能为空",
81
+ })
82
+ .optional();
114
83
 
115
- /**
116
- * **dynamicAgentsSchema (动态 Agent 配置)**
117
- *
118
- * 控制是否按用户/群组自动创建独立 Agent 实例。
119
- * @property enabled - 是否启用动态 Agent
120
- * @property dmCreateAgent - 私聊是否为每个用户创建独立 Agent
121
- * @property groupEnabled - 群聊是否启用动态 Agent
122
- * @property adminUsers - 管理员列表(绕过动态路由)
123
- */
124
- const dynamicAgentsSchema = z.object({
84
+ const dynamicAgentsSchema = z
85
+ .object({
125
86
  enabled: z.boolean().optional(),
126
87
  dmCreateAgent: z.boolean().optional(),
127
88
  groupEnabled: z.boolean().optional(),
128
89
  adminUsers: z.array(z.string()).optional(),
129
- }).optional();
90
+ })
91
+ .optional();
130
92
 
131
- /** Matrix 账号条目 */
132
93
  const accountSchema = z.object({
133
- enabled: z.boolean().optional(),
134
- name: z.string().optional(),
135
- bot: botSchema,
136
- agent: agentSchema,
94
+ enabled: z.boolean().optional(),
95
+ name: z.string().optional(),
96
+ bot: botSchema,
97
+ agent: agentSchema,
137
98
  });
138
99
 
139
- /** 顶层 WeCom 配置 Schema */
140
- export const WecomConfigSchema = bindToJsonSchema(z.object({
100
+ export const WecomConfigSchema = bindToJsonSchema(
101
+ z.object({
141
102
  enabled: z.boolean().optional(),
142
103
  bot: botSchema,
143
104
  agent: agentSchema,
@@ -147,6 +108,7 @@ export const WecomConfigSchema = bindToJsonSchema(z.object({
147
108
  network: networkSchema,
148
109
  routing: routingSchema,
149
110
  dynamicAgents: dynamicAgentsSchema,
150
- }));
111
+ }),
112
+ );
151
113
 
152
114
  export type WecomConfigInput = z.infer<typeof WecomConfigSchema>;
@@ -0,0 +1,7 @@
1
+ import type { DeliveryTask, RawFrameReference, ReplyContext, TransportSessionSnapshot, UnifiedInboundEvent } from "../types/index.js";
2
+
3
+ export type WecomConversation = UnifiedInboundEvent["conversation"];
4
+ export type WecomRawEnvelope = RawFrameReference;
5
+ export type WecomReplyContext = ReplyContext;
6
+ export type WecomTransportSession = TransportSessionSnapshot;
7
+ export type WecomDeliveryTask = DeliveryTask;
@@ -0,0 +1,36 @@
1
+ import type { ResolvedBotAccount, UnifiedInboundEvent } from "../types/index.js";
2
+
3
+ export function assertBotPrimaryTransport(account: ResolvedBotAccount): void {
4
+ if (account.primaryTransport === "ws" && !account.wsConfigured) {
5
+ throw new Error(`WeCom bot account "${account.accountId}" is missing bot.ws credentials.`);
6
+ }
7
+ if (account.primaryTransport === "webhook" && !account.webhookConfigured) {
8
+ throw new Error(`WeCom bot account "${account.accountId}" is missing bot.webhook credentials.`);
9
+ }
10
+ }
11
+
12
+ export function buildDedupKey(event: UnifiedInboundEvent): string {
13
+ return `${event.accountId}:${event.transport}:${event.messageId}`;
14
+ }
15
+
16
+ export function resolveConversationKey(event: UnifiedInboundEvent): string {
17
+ const conversation = event.conversation;
18
+ return [event.accountId, conversation.peerKind, conversation.peerId, conversation.senderId].join(":");
19
+ }
20
+
21
+ export function normalizeWecomAllowFromEntry(raw: string): string {
22
+ return raw
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/^wecom:/, "")
26
+ .replace(/^user:/, "")
27
+ .replace(/^userid:/, "");
28
+ }
29
+
30
+ export function isWecomSenderAllowed(senderUserId: string, allowFrom: string[]): boolean {
31
+ const list = allowFrom.map((entry) => normalizeWecomAllowFromEntry(entry)).filter(Boolean);
32
+ if (list.includes("*")) return true;
33
+ const normalizedSender = normalizeWecomAllowFromEntry(senderUserId);
34
+ if (!normalizedSender) return false;
35
+ return list.includes(normalizedSender);
36
+ }
@@ -48,6 +48,12 @@ export function generateAgentId(chatType: "dm" | "group", peerId: string, accoun
48
48
  return `wecom-${sanitizedAccountId}-${chatType}-${sanitizedPeer}`;
49
49
  }
50
50
 
51
+ export function buildAgentSessionTarget(userId: string, accountId?: string): string {
52
+ const normalizedUserId = String(userId).trim();
53
+ const sanitizedAccountId = sanitizeDynamicIdPart(accountId ?? "default") || "default";
54
+ return `wecom-agent:${sanitizedAccountId}:${normalizedUserId}`;
55
+ }
56
+
51
57
  /**
52
58
  * **shouldUseDynamicAgent (检查是否使用动态 Agent)**
53
59
  *