@yanhaidao/wecom 2.3.3 → 2.3.9

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 (111) hide show
  1. package/.github/workflows/release.yml +69 -1
  2. package/README.md +213 -337
  3. package/assets/03.bot.page.png +0 -0
  4. package/changelog/v2.3.4.md +20 -0
  5. package/changelog/v2.3.9.md +22 -0
  6. package/compat-single-account.md +32 -2
  7. package/index.test.ts +34 -0
  8. package/index.ts +15 -7
  9. package/package.json +8 -7
  10. package/src/agent/api-client.upload.test.ts +1 -2
  11. package/src/agent/handler.ts +82 -9
  12. package/src/agent/index.ts +1 -1
  13. package/src/app/account-runtime.ts +245 -0
  14. package/src/app/bootstrap.ts +29 -0
  15. package/src/app/index.ts +31 -0
  16. package/src/capability/agent/delivery-service.ts +79 -0
  17. package/src/capability/agent/fallback-policy.ts +13 -0
  18. package/src/capability/agent/index.ts +3 -0
  19. package/src/capability/agent/ingress-service.ts +38 -0
  20. package/src/capability/bot/dispatch-config.ts +47 -0
  21. package/src/capability/bot/fallback-delivery.ts +178 -0
  22. package/src/capability/bot/index.ts +1 -0
  23. package/src/capability/bot/local-path-delivery.ts +215 -0
  24. package/src/capability/bot/service.ts +56 -0
  25. package/src/capability/bot/stream-delivery.ts +379 -0
  26. package/src/capability/bot/stream-finalizer.ts +120 -0
  27. package/src/capability/bot/stream-orchestrator.ts +352 -0
  28. package/src/capability/bot/types.ts +8 -0
  29. package/src/capability/index.ts +2 -0
  30. package/src/channel.lifecycle.test.ts +9 -6
  31. package/src/channel.meta.test.ts +12 -0
  32. package/src/channel.ts +48 -21
  33. package/src/config/accounts.ts +223 -283
  34. package/src/config/derived-paths.test.ts +111 -0
  35. package/src/config/derived-paths.ts +41 -0
  36. package/src/config/index.ts +10 -12
  37. package/src/config/runtime-config.ts +46 -0
  38. package/src/config/schema.ts +59 -102
  39. package/src/domain/models.ts +7 -0
  40. package/src/domain/policies.ts +36 -0
  41. package/src/dynamic-agent.ts +6 -0
  42. package/src/gateway-monitor.ts +43 -93
  43. package/src/http.ts +23 -2
  44. package/src/monitor/limits.ts +7 -0
  45. package/src/monitor/state.ts +28 -508
  46. package/src/monitor.active.test.ts +3 -3
  47. package/src/monitor.integration.test.ts +0 -1
  48. package/src/monitor.ts +64 -2603
  49. package/src/monitor.webhook.test.ts +127 -42
  50. package/src/observability/audit-log.ts +48 -0
  51. package/src/observability/legacy-operational-event-store.ts +36 -0
  52. package/src/observability/raw-envelope-log.ts +28 -0
  53. package/src/observability/status-registry.ts +13 -0
  54. package/src/observability/transport-session-view.ts +14 -0
  55. package/src/onboarding.test.ts +219 -0
  56. package/src/onboarding.ts +88 -71
  57. package/src/outbound.test.ts +5 -5
  58. package/src/outbound.ts +18 -66
  59. package/src/runtime/dispatcher.ts +52 -0
  60. package/src/runtime/index.ts +4 -0
  61. package/src/runtime/outbound-intent.ts +4 -0
  62. package/src/runtime/reply-orchestrator.test.ts +38 -0
  63. package/src/runtime/reply-orchestrator.ts +55 -0
  64. package/src/runtime/routing-bridge.ts +19 -0
  65. package/src/runtime/session-manager.ts +76 -0
  66. package/src/runtime.ts +7 -14
  67. package/src/shared/command-auth.ts +1 -17
  68. package/src/shared/media-service.ts +36 -0
  69. package/src/shared/media-types.ts +5 -0
  70. package/src/store/active-reply-store.ts +42 -0
  71. package/src/store/interfaces.ts +11 -0
  72. package/src/store/memory-store.ts +43 -0
  73. package/src/store/stream-batch-store.ts +350 -0
  74. package/src/target.ts +28 -0
  75. package/src/transport/agent-api/client.ts +44 -0
  76. package/src/transport/agent-api/core.ts +367 -0
  77. package/src/transport/agent-api/delivery.ts +41 -0
  78. package/src/transport/agent-api/media-upload.ts +11 -0
  79. package/src/transport/agent-api/reply.ts +39 -0
  80. package/src/transport/agent-callback/http-handler.ts +47 -0
  81. package/src/transport/agent-callback/inbound.ts +5 -0
  82. package/src/transport/agent-callback/reply.ts +13 -0
  83. package/src/transport/agent-callback/request-handler.ts +244 -0
  84. package/src/transport/agent-callback/session.ts +23 -0
  85. package/src/transport/bot-webhook/active-reply.ts +36 -0
  86. package/src/transport/bot-webhook/http-handler.ts +48 -0
  87. package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
  88. package/src/transport/bot-webhook/inbound.ts +5 -0
  89. package/src/transport/bot-webhook/message-shape.ts +89 -0
  90. package/src/transport/bot-webhook/protocol.ts +148 -0
  91. package/src/transport/bot-webhook/reply.ts +15 -0
  92. package/src/transport/bot-webhook/request-handler.ts +394 -0
  93. package/src/transport/bot-webhook/session.ts +23 -0
  94. package/src/transport/bot-ws/inbound.ts +109 -0
  95. package/src/transport/bot-ws/reply.ts +48 -0
  96. package/src/transport/bot-ws/sdk-adapter.ts +180 -0
  97. package/src/transport/bot-ws/session.ts +28 -0
  98. package/src/transport/http/common.ts +109 -0
  99. package/src/transport/http/registry.ts +92 -0
  100. package/src/transport/http/request-handler.ts +84 -0
  101. package/src/transport/index.ts +14 -0
  102. package/src/types/account.ts +56 -91
  103. package/src/types/config.ts +59 -112
  104. package/src/types/constants.ts +20 -35
  105. package/src/types/events.ts +21 -0
  106. package/src/types/index.ts +14 -38
  107. package/src/types/legacy-stream.ts +50 -0
  108. package/src/types/runtime-context.ts +28 -0
  109. package/src/types/runtime.ts +161 -0
  110. package/src/agent/api-client.ts +0 -383
  111. package/src/monitor/types.ts +0 -136
@@ -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,99 @@
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
70
  corpSecret: z.string(),
108
- agentId: z.union([z.string(), z.number()]).optional(),
71
+ agentId: z.union([z.number(), z.string()]).optional(),
109
72
  token: z.string(),
110
73
  encodingAESKey: z.string(),
111
74
  welcomeText: z.string().optional(),
112
75
  dm: dmSchema,
113
- }).optional();
76
+ })
77
+ .optional();
114
78
 
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({
79
+ const dynamicAgentsSchema = z
80
+ .object({
125
81
  enabled: z.boolean().optional(),
126
82
  dmCreateAgent: z.boolean().optional(),
127
83
  groupEnabled: z.boolean().optional(),
128
84
  adminUsers: z.array(z.string()).optional(),
129
- }).optional();
85
+ })
86
+ .optional();
130
87
 
131
- /** Matrix 账号条目 */
132
88
  const accountSchema = z.object({
133
- enabled: z.boolean().optional(),
134
- name: z.string().optional(),
135
- bot: botSchema,
136
- agent: agentSchema,
89
+ enabled: z.boolean().optional(),
90
+ name: z.string().optional(),
91
+ bot: botSchema,
92
+ agent: agentSchema,
137
93
  });
138
94
 
139
- /** 顶层 WeCom 配置 Schema */
140
- export const WecomConfigSchema = bindToJsonSchema(z.object({
95
+ export const WecomConfigSchema = bindToJsonSchema(
96
+ z.object({
141
97
  enabled: z.boolean().optional(),
142
98
  bot: botSchema,
143
99
  agent: agentSchema,
@@ -147,6 +103,7 @@ export const WecomConfigSchema = bindToJsonSchema(z.object({
147
103
  network: networkSchema,
148
104
  routing: routingSchema,
149
105
  dynamicAgents: dynamicAgentsSchema,
150
- }));
106
+ }),
107
+ );
151
108
 
152
109
  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
  *
@@ -1,18 +1,20 @@
1
1
  import type {
2
2
  ChannelGatewayContext,
3
3
  OpenClawConfig,
4
- PluginRuntime,
5
4
  } from "openclaw/plugin-sdk";
6
5
 
7
6
  import {
8
- detectMode,
9
7
  listWecomAccountIds,
8
+ resolveDerivedPathSummary,
10
9
  resolveWecomAccount,
11
10
  resolveWecomAccountConflict,
12
11
  } from "./config/index.js";
13
- import { registerAgentWebhookTarget, registerWecomWebhookTarget } from "./monitor.js";
12
+ import { createAccountRuntime } from "./app/bootstrap.js";
13
+ import { registerAccountRuntime, unregisterAccountRuntime } from "./app/index.js";
14
14
  import type { ResolvedWecomAccount, WecomConfig } from "./types/index.js";
15
- import { WEBHOOK_PATHS } from "./types/constants.js";
15
+ import { WecomBotCapabilityService } from "./capability/bot/index.js";
16
+ import { WecomAgentIngressService } from "./capability/agent/index.js";
17
+ import type { WecomRuntimeEnv } from "./types/runtime-context.js";
16
18
 
17
19
  type AccountRouteRegistryItem = {
18
20
  botPaths: string[];
@@ -75,30 +77,6 @@ function waitForAbortSignal(abortSignal: AbortSignal): Promise<void> {
75
77
  });
76
78
  }
77
79
 
78
- function uniquePaths(paths: string[]): string[] {
79
- return Array.from(new Set(paths.map((path) => path.trim()).filter(Boolean)));
80
- }
81
-
82
- function resolveBotRegistrationPaths(params: { accountId: string; matrixMode: boolean }): string[] {
83
- if (params.matrixMode) {
84
- return uniquePaths([
85
- `${WEBHOOK_PATHS.BOT_PLUGIN}/${params.accountId}`,
86
- `${WEBHOOK_PATHS.BOT_ALT}/${params.accountId}`,
87
- ]);
88
- }
89
- return uniquePaths([WEBHOOK_PATHS.BOT_PLUGIN, WEBHOOK_PATHS.BOT, WEBHOOK_PATHS.BOT_ALT]);
90
- }
91
-
92
- function resolveAgentRegistrationPaths(params: { accountId: string; matrixMode: boolean }): string[] {
93
- if (params.matrixMode) {
94
- return uniquePaths([
95
- `${WEBHOOK_PATHS.AGENT_PLUGIN}/${params.accountId}`,
96
- `${WEBHOOK_PATHS.AGENT}/${params.accountId}`,
97
- ]);
98
- }
99
- return uniquePaths([WEBHOOK_PATHS.AGENT_PLUGIN, WEBHOOK_PATHS.AGENT]);
100
- }
101
-
102
80
  /**
103
81
  * Keeps WeCom webhook targets registered for the account lifecycle.
104
82
  * The promise only settles after gateway abort/reload signals shutdown.
@@ -122,29 +100,11 @@ export async function monitorWecomProvider(
122
100
  });
123
101
  throw new Error(conflict.message);
124
102
  }
125
- const mode = detectMode(cfg.channels?.wecom as WecomConfig | undefined);
126
- const matrixMode = mode === "matrix";
127
103
  const bot = account.bot;
128
104
  const agent = account.agent;
129
105
  const botConfigured = Boolean(bot?.configured);
130
106
  const agentConfigured = Boolean(agent?.configured);
131
107
 
132
- if (mode === "legacy" && (botConfigured || agentConfigured)) {
133
- if (agentConfigured && !botConfigured) {
134
- ctx.log?.warn(
135
- `[${account.accountId}] 检测到仍在使用单 Agent 兼容模式。建议尽快升级为多账号模式:` +
136
- `将 channels.wecom.agent 迁移到 channels.wecom.accounts.<accountId>.agent,` +
137
- `并设置 channels.wecom.defaultAccount。`,
138
- );
139
- } else {
140
- ctx.log?.warn(
141
- `[${account.accountId}] 检测到仍在使用单账号兼容模式。建议尽快升级为多账号模式:` +
142
- `将 channels.wecom.bot/agent 迁移到 channels.wecom.accounts.<accountId>.bot/agent,` +
143
- `并设置 channels.wecom.defaultAccount。`,
144
- );
145
- }
146
- }
147
-
148
108
  if (!botConfigured && !agentConfigured) {
149
109
  ctx.log?.warn(`[${account.accountId}] wecom not configured; channel is idle`);
150
110
  ctx.setStatus({ accountId: account.accountId, running: false, configured: false });
@@ -152,50 +112,38 @@ export async function monitorWecomProvider(
152
112
  return;
153
113
  }
154
114
 
155
- const unregisters: Array<() => void> = [];
115
+ const accountRuntime = createAccountRuntime(ctx);
116
+ registerAccountRuntime(accountRuntime);
156
117
  const botPaths: string[] = [];
157
118
  const agentPaths: string[] = [];
119
+ const runtimeEnv: WecomRuntimeEnv = {
120
+ log: (message) => ctx.log?.info(message),
121
+ error: (message) => ctx.log?.error(message),
122
+ };
123
+ const botService = new WecomBotCapabilityService(
124
+ accountRuntime,
125
+ cfg,
126
+ runtimeEnv,
127
+ );
128
+ const agentIngress = new WecomAgentIngressService(accountRuntime, cfg, runtimeEnv);
158
129
  try {
159
- if (bot && botConfigured) {
160
- const paths = resolveBotRegistrationPaths({
161
- accountId: account.accountId,
162
- matrixMode,
163
- });
164
- for (const path of paths) {
165
- unregisters.push(
166
- registerWecomWebhookTarget({
167
- account: bot,
168
- config: cfg,
169
- runtime: ctx.runtime,
170
- // The HTTP handler resolves the active PluginRuntime via getWecomRuntime().
171
- // The stored target only needs to be decrypt/verify-capable.
172
- core: {} as PluginRuntime,
173
- path,
174
- statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
175
- }),
176
- );
177
- }
178
- botPaths.push(...paths);
179
- ctx.log?.info(`[${account.accountId}] wecom bot webhook registered at ${paths.join(", ")}`);
130
+ ctx.log?.info(
131
+ `[${account.accountId}] wecom runtime start bot=${bot?.primaryTransport ?? "disabled"} agent=${agentConfigured ? "callback/api" : "disabled"}`,
132
+ );
133
+ const botRegistration = botService.start();
134
+ if (botRegistration) {
135
+ botPaths.push(...botRegistration.descriptors);
136
+ ctx.log?.info(
137
+ `[${account.accountId}] wecom bot ${botRegistration.transport} started: ${botRegistration.descriptors.join(", ")}`,
138
+ );
180
139
  }
181
140
 
182
- if (agent && agentConfigured) {
183
- const paths = resolveAgentRegistrationPaths({
184
- accountId: account.accountId,
185
- matrixMode,
186
- });
187
- for (const path of paths) {
188
- unregisters.push(
189
- registerAgentWebhookTarget({
190
- agent,
191
- config: cfg,
192
- runtime: ctx.runtime,
193
- path,
194
- }),
195
- );
196
- }
197
- agentPaths.push(...paths);
198
- ctx.log?.info(`[${account.accountId}] wecom agent webhook registered at ${paths.join(", ")}`);
141
+ const agentRegistration = agentIngress.start();
142
+ if (agentRegistration) {
143
+ agentPaths.push(...agentRegistration.descriptors);
144
+ ctx.log?.info(
145
+ `[${account.accountId}] wecom agent ${agentRegistration.transport} started: ${agentRegistration.descriptors.join(", ")}`,
146
+ );
199
147
  }
200
148
 
201
149
  accountRouteRegistry.set(account.accountId, { botPaths, agentPaths });
@@ -207,25 +155,27 @@ export async function monitorWecomProvider(
207
155
  }
208
156
 
209
157
  ctx.setStatus({
210
- accountId: account.accountId,
211
158
  running: true,
212
159
  configured: true,
213
- webhookPath: botConfigured
214
- ? (botPaths[0] ?? WEBHOOK_PATHS.BOT_PLUGIN)
215
- : (agentPaths[0] ?? WEBHOOK_PATHS.AGENT_PLUGIN),
160
+ webhookPath: botPaths[0] ?? agentPaths[0] ?? null,
216
161
  lastStartAt: Date.now(),
162
+ ...accountRuntime.buildRuntimeStatus(),
217
163
  });
164
+ ctx.log?.info(
165
+ `[${account.accountId}] runtime status health=${accountRuntime.buildRuntimeStatus().health} transports=${(accountRuntime.buildRuntimeStatus().transportSessions ?? []).join(" | ") || "none"}`,
166
+ );
218
167
 
219
168
  await waitForAbortSignal(ctx.abortSignal);
220
169
  } finally {
221
- for (const unregister of unregisters) {
222
- unregister();
223
- }
170
+ botService.stop();
171
+ agentIngress.stop();
224
172
  accountRouteRegistry.delete(account.accountId);
173
+ unregisterAccountRuntime(account.accountId);
225
174
  ctx.setStatus({
226
- accountId: account.accountId,
227
175
  running: false,
228
176
  lastStopAt: Date.now(),
177
+ ...accountRuntime.buildRuntimeStatus(),
229
178
  });
179
+ ctx.log?.info(`[${account.accountId}] wecom runtime stopped`);
230
180
  }
231
181
  }
package/src/http.ts CHANGED
@@ -18,6 +18,15 @@ function getProxyDispatcher(proxyUrl: string): ProxyDispatcher {
18
18
  return created;
19
19
  }
20
20
 
21
+ function summarizeHttpTarget(input: string | URL): string {
22
+ try {
23
+ const url = typeof input === "string" ? new URL(input) : input;
24
+ return `${url.origin}${url.pathname}`;
25
+ } catch {
26
+ return String(input);
27
+ }
28
+ }
29
+
21
30
  function mergeAbortSignal(params: {
22
31
  signal?: AbortSignal;
23
32
  timeoutMs?: number;
@@ -54,6 +63,9 @@ export type WecomHttpOptions = {
54
63
  export async function wecomFetch(input: string | URL, init?: RequestInit, opts?: WecomHttpOptions): Promise<Response> {
55
64
  const proxyUrl = opts?.proxyUrl?.trim() ?? "";
56
65
  const dispatcher = proxyUrl ? getProxyDispatcher(proxyUrl) : undefined;
66
+ const startedAt = Date.now();
67
+ const method = (init?.method ?? "GET").toUpperCase();
68
+ const target = summarizeHttpTarget(input);
57
69
 
58
70
  const initSignal = init?.signal ?? undefined;
59
71
  const signal = mergeAbortSignal({ signal: opts?.signal ?? initSignal, timeoutMs: opts?.timeoutMs });
@@ -71,11 +83,20 @@ export async function wecomFetch(input: string | URL, init?: RequestInit, opts?:
71
83
  };
72
84
 
73
85
  try {
74
- return await undiciFetch(input, nextInit as Parameters<typeof undiciFetch>[1]) as unknown as Response;
86
+ console.log(
87
+ `[wecom-http] request method=${method} target=${target} proxy=${proxyUrl || "none"} timeoutMs=${String(opts?.timeoutMs ?? "none")}`,
88
+ );
89
+ const response = await undiciFetch(input, nextInit as Parameters<typeof undiciFetch>[1]) as unknown as Response;
90
+ console.log(
91
+ `[wecom-http] response method=${method} target=${target} status=${response.status} durationMs=${Date.now() - startedAt}`,
92
+ );
93
+ return response;
75
94
  } catch (err: unknown) {
76
95
  if (err instanceof Error && err.name === "TypeError" && err.message === "fetch failed") {
77
96
  const cause = (err as any).cause;
78
- console.error(`[wecom-http] fetch failed: ${input} (proxy: ${proxyUrl || "none"})${cause ? ` - cause: ${String(cause)}` : ""}`);
97
+ console.error(
98
+ `[wecom-http] fetch failed method=${method} target=${target} durationMs=${Date.now() - startedAt} proxy=${proxyUrl || "none"}${cause ? ` cause=${String(cause)}` : ""}`,
99
+ );
79
100
  }
80
101
  throw err;
81
102
  }
@@ -0,0 +1,7 @@
1
+ export const LIMITS = {
2
+ STREAM_TTL_MS: 10 * 60 * 1000,
3
+ ACTIVE_REPLY_TTL_MS: 60 * 60 * 1000,
4
+ DEFAULT_DEBOUNCE_MS: 500,
5
+ STREAM_MAX_BYTES: 20_480,
6
+ REQUEST_TIMEOUT_MS: 15_000,
7
+ };