@yanhaidao/wecom 2.3.4 → 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.
- package/README.md +213 -339
- package/assets/03.bot.page.png +0 -0
- package/changelog/v2.3.9.md +22 -0
- package/compat-single-account.md +32 -2
- package/index.ts +5 -5
- package/package.json +8 -7
- package/src/agent/api-client.upload.test.ts +1 -2
- package/src/agent/handler.ts +82 -9
- package/src/agent/index.ts +1 -1
- package/src/app/account-runtime.ts +245 -0
- package/src/app/bootstrap.ts +29 -0
- package/src/app/index.ts +31 -0
- package/src/capability/agent/delivery-service.ts +79 -0
- package/src/capability/agent/fallback-policy.ts +13 -0
- package/src/capability/agent/index.ts +3 -0
- package/src/capability/agent/ingress-service.ts +38 -0
- package/src/capability/bot/dispatch-config.ts +47 -0
- package/src/capability/bot/fallback-delivery.ts +178 -0
- package/src/capability/bot/index.ts +1 -0
- package/src/capability/bot/local-path-delivery.ts +215 -0
- package/src/capability/bot/service.ts +56 -0
- package/src/capability/bot/stream-delivery.ts +379 -0
- package/src/capability/bot/stream-finalizer.ts +120 -0
- package/src/capability/bot/stream-orchestrator.ts +352 -0
- package/src/capability/bot/types.ts +8 -0
- package/src/capability/index.ts +2 -0
- package/src/channel.lifecycle.test.ts +9 -6
- package/src/channel.meta.test.ts +12 -0
- package/src/channel.ts +48 -21
- package/src/config/accounts.ts +223 -283
- package/src/config/derived-paths.test.ts +111 -0
- package/src/config/derived-paths.ts +41 -0
- package/src/config/index.ts +10 -12
- package/src/config/runtime-config.ts +46 -0
- package/src/config/schema.ts +59 -102
- package/src/domain/models.ts +7 -0
- package/src/domain/policies.ts +36 -0
- package/src/dynamic-agent.ts +6 -0
- package/src/gateway-monitor.ts +43 -93
- package/src/http.ts +23 -2
- package/src/monitor/limits.ts +7 -0
- package/src/monitor/state.ts +28 -508
- package/src/monitor.active.test.ts +3 -3
- package/src/monitor.integration.test.ts +0 -1
- package/src/monitor.ts +64 -2603
- package/src/monitor.webhook.test.ts +127 -42
- package/src/observability/audit-log.ts +48 -0
- package/src/observability/legacy-operational-event-store.ts +36 -0
- package/src/observability/raw-envelope-log.ts +28 -0
- package/src/observability/status-registry.ts +13 -0
- package/src/observability/transport-session-view.ts +14 -0
- package/src/onboarding.test.ts +219 -0
- package/src/onboarding.ts +88 -71
- package/src/outbound.test.ts +5 -5
- package/src/outbound.ts +18 -66
- package/src/runtime/dispatcher.ts +52 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/outbound-intent.ts +4 -0
- package/src/runtime/reply-orchestrator.test.ts +38 -0
- package/src/runtime/reply-orchestrator.ts +55 -0
- package/src/runtime/routing-bridge.ts +19 -0
- package/src/runtime/session-manager.ts +76 -0
- package/src/runtime.ts +7 -14
- package/src/shared/command-auth.ts +1 -17
- package/src/shared/media-service.ts +36 -0
- package/src/shared/media-types.ts +5 -0
- package/src/store/active-reply-store.ts +42 -0
- package/src/store/interfaces.ts +11 -0
- package/src/store/memory-store.ts +43 -0
- package/src/store/stream-batch-store.ts +350 -0
- package/src/target.ts +28 -0
- package/src/transport/agent-api/client.ts +44 -0
- package/src/transport/agent-api/core.ts +367 -0
- package/src/transport/agent-api/delivery.ts +41 -0
- package/src/transport/agent-api/media-upload.ts +11 -0
- package/src/transport/agent-api/reply.ts +39 -0
- package/src/transport/agent-callback/http-handler.ts +47 -0
- package/src/transport/agent-callback/inbound.ts +5 -0
- package/src/transport/agent-callback/reply.ts +13 -0
- package/src/transport/agent-callback/request-handler.ts +244 -0
- package/src/transport/agent-callback/session.ts +23 -0
- package/src/transport/bot-webhook/active-reply.ts +36 -0
- package/src/transport/bot-webhook/http-handler.ts +48 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
- package/src/transport/bot-webhook/inbound.ts +5 -0
- package/src/transport/bot-webhook/message-shape.ts +89 -0
- package/src/transport/bot-webhook/protocol.ts +148 -0
- package/src/transport/bot-webhook/reply.ts +15 -0
- package/src/transport/bot-webhook/request-handler.ts +394 -0
- package/src/transport/bot-webhook/session.ts +23 -0
- package/src/transport/bot-ws/inbound.ts +109 -0
- package/src/transport/bot-ws/reply.ts +48 -0
- package/src/transport/bot-ws/sdk-adapter.ts +180 -0
- package/src/transport/bot-ws/session.ts +28 -0
- package/src/transport/http/common.ts +109 -0
- package/src/transport/http/registry.ts +92 -0
- package/src/transport/http/request-handler.ts +84 -0
- package/src/transport/index.ts +14 -0
- package/src/types/account.ts +56 -91
- package/src/types/config.ts +59 -112
- package/src/types/constants.ts +20 -35
- package/src/types/events.ts +21 -0
- package/src/types/index.ts +14 -38
- package/src/types/legacy-stream.ts +50 -0
- package/src/types/runtime-context.ts +28 -0
- package/src/types/runtime.ts +161 -0
- package/src/agent/api-client.ts +0 -383
- 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
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WeCom 配置模块导出
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
export { WecomConfigSchema, type WecomConfigInput } from "./schema.js";
|
|
6
2
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
15
|
+
})
|
|
16
|
+
.optional();
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
})
|
|
24
|
+
})
|
|
25
|
+
.optional();
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
})
|
|
30
|
+
})
|
|
31
|
+
.optional();
|
|
60
32
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
})
|
|
36
|
+
})
|
|
37
|
+
.optional();
|
|
70
38
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
})
|
|
76
|
+
})
|
|
77
|
+
.optional();
|
|
114
78
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
})
|
|
85
|
+
})
|
|
86
|
+
.optional();
|
|
130
87
|
|
|
131
|
-
/** Matrix 账号条目 */
|
|
132
88
|
const accountSchema = z.object({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
89
|
+
enabled: z.boolean().optional(),
|
|
90
|
+
name: z.string().optional(),
|
|
91
|
+
bot: botSchema,
|
|
92
|
+
agent: agentSchema,
|
|
137
93
|
});
|
|
138
94
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
package/src/dynamic-agent.ts
CHANGED
|
@@ -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
|
*
|
package/src/gateway-monitor.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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:
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|