@sunnoy/wecom 1.9.0 → 2.0.0
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 +391 -845
- package/image-processor.js +30 -27
- package/index.js +10 -43
- package/package.json +5 -5
- package/think-parser.js +51 -11
- package/wecom/accounts.js +323 -189
- package/wecom/channel-plugin.js +543 -750
- package/wecom/constants.js +57 -47
- package/wecom/dm-policy.js +91 -0
- package/wecom/group-policy.js +85 -0
- package/wecom/onboarding.js +117 -0
- package/wecom/runtime-telemetry.js +330 -0
- package/wecom/sandbox.js +60 -0
- package/wecom/state.js +33 -35
- package/wecom/workspace-template.js +62 -5
- package/wecom/ws-monitor.js +1487 -0
- package/wecom/ws-state.js +160 -0
- package/crypto.js +0 -135
- package/stream-manager.js +0 -358
- package/webhook.js +0 -469
- package/wecom/agent-inbound.js +0 -541
- package/wecom/http-handler-state.js +0 -23
- package/wecom/http-handler.js +0 -395
- package/wecom/inbound-processor.js +0 -562
- package/wecom/media.js +0 -192
- package/wecom/outbound-delivery.js +0 -435
- package/wecom/response-url.js +0 -33
- package/wecom/stream-utils.js +0 -163
- package/wecom/webhook-targets.js +0 -28
- package/wecom/xml-parser.js +0 -126
package/wecom/accounts.js
CHANGED
|
@@ -1,249 +1,304 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multi-account resolution layer.
|
|
3
|
-
*
|
|
4
|
-
* Design: dictionary-based — each key under `channels.wecom` is an account ID,
|
|
5
|
-
* and its value contains the full per-account config (token, encodingAesKey,
|
|
6
|
-
* agent, webhooks, etc.).
|
|
7
|
-
*
|
|
8
|
-
* Legacy single-account configs (where `token` exists directly under `wecom`)
|
|
9
|
-
* are auto-detected and treated as accountId = "default".
|
|
10
|
-
*
|
|
11
|
-
* ── Multi-account config ───────────────────────────────────────────
|
|
12
|
-
*
|
|
13
|
-
* channels:
|
|
14
|
-
* wecom:
|
|
15
|
-
* bot1:
|
|
16
|
-
* token: "bot-token-a"
|
|
17
|
-
* encodingAesKey: "..."
|
|
18
|
-
* agent:
|
|
19
|
-
* corpId: "ww1234"
|
|
20
|
-
* corpSecret: "secret-a"
|
|
21
|
-
* agentId: 1000001
|
|
22
|
-
* webhooks:
|
|
23
|
-
* ops-group: "key-xxx"
|
|
24
|
-
* bot2:
|
|
25
|
-
* token: "bot-token-b"
|
|
26
|
-
* encodingAesKey: "..."
|
|
27
|
-
* agent:
|
|
28
|
-
* corpId: "ww5678"
|
|
29
|
-
* corpSecret: "secret-b"
|
|
30
|
-
* agentId: 1000002
|
|
31
|
-
*
|
|
32
|
-
* ── Legacy single-account config (auto-detected, fully compatible) ─
|
|
33
|
-
*
|
|
34
|
-
* channels:
|
|
35
|
-
* wecom:
|
|
36
|
-
* token: "bot-token-a"
|
|
37
|
-
* encodingAesKey: "..."
|
|
38
|
-
* agent:
|
|
39
|
-
* corpId: "ww1234"
|
|
40
|
-
* corpSecret: "secret-a"
|
|
41
|
-
* agentId: 1000001
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
1
|
import { logger } from "../logger.js";
|
|
45
|
-
import { DEFAULT_ACCOUNT_ID } from "./constants.js";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, DEFAULT_WS_URL } from "./constants.js";
|
|
46
3
|
|
|
47
|
-
// Keys that belong to the top-level wecom config and are NOT account IDs.
|
|
48
4
|
const RESERVED_KEYS = new Set([
|
|
49
5
|
"enabled",
|
|
50
|
-
"token",
|
|
51
|
-
"encodingAesKey",
|
|
52
|
-
"agent",
|
|
53
|
-
"webhooks",
|
|
54
|
-
"webhookPath",
|
|
55
6
|
"name",
|
|
7
|
+
"botId",
|
|
8
|
+
"secret",
|
|
9
|
+
"websocketUrl",
|
|
10
|
+
"sendThinkingMessage",
|
|
11
|
+
"welcomeMessage",
|
|
56
12
|
"allowFrom",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
13
|
+
"dmPolicy",
|
|
14
|
+
"groupPolicy",
|
|
15
|
+
"groupAllowFrom",
|
|
16
|
+
"groups",
|
|
17
|
+
"commands",
|
|
18
|
+
"dynamicAgents",
|
|
19
|
+
"dm",
|
|
20
|
+
"groupChat",
|
|
21
|
+
"adminUsers",
|
|
22
|
+
"workspaceTemplate",
|
|
23
|
+
"agent",
|
|
24
|
+
"webhooks",
|
|
60
25
|
"network",
|
|
26
|
+
"defaultAccount",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SHARED_MULTI_ACCOUNT_KEYS = new Set([
|
|
30
|
+
"enabled",
|
|
31
|
+
"websocketUrl",
|
|
32
|
+
"sendThinkingMessage",
|
|
33
|
+
"welcomeMessage",
|
|
34
|
+
"allowFrom",
|
|
35
|
+
"dmPolicy",
|
|
36
|
+
"groupPolicy",
|
|
37
|
+
"groupAllowFrom",
|
|
38
|
+
"groups",
|
|
61
39
|
"commands",
|
|
62
40
|
"dynamicAgents",
|
|
63
41
|
"dm",
|
|
64
42
|
"groupChat",
|
|
65
43
|
"adminUsers",
|
|
66
44
|
"workspaceTemplate",
|
|
67
|
-
"
|
|
45
|
+
"agent",
|
|
46
|
+
"webhooks",
|
|
47
|
+
"network",
|
|
68
48
|
]);
|
|
69
49
|
|
|
70
|
-
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
52
|
+
}
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return typeof wecom?.token === "string";
|
|
54
|
+
function normalizeAccountKey(value) {
|
|
55
|
+
return String(value ?? "")
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[^a-z0-9_-]/g, "_");
|
|
78
59
|
}
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
function cloneValue(value) {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.map((entry) => cloneValue(entry));
|
|
64
|
+
}
|
|
65
|
+
if (!isPlainObject(value)) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
const cloned = {};
|
|
69
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
70
|
+
cloned[key] = cloneValue(entry);
|
|
71
|
+
}
|
|
72
|
+
return cloned;
|
|
73
|
+
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
75
|
+
function pruneEmptyObjects(value) {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.map((entry) => pruneEmptyObjects(entry));
|
|
78
|
+
}
|
|
79
|
+
if (!isPlainObject(value)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const next = {};
|
|
84
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
85
|
+
const pruned = pruneEmptyObjects(entry);
|
|
86
|
+
if (pruned === undefined) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (isPlainObject(pruned) && Object.keys(pruned).length === 0) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
next[key] = pruned;
|
|
93
|
+
}
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeConfig(base, override) {
|
|
98
|
+
const result = isPlainObject(base) ? cloneValue(base) : {};
|
|
99
|
+
for (const [key, value] of Object.entries(override ?? {})) {
|
|
100
|
+
if (value === undefined) {
|
|
101
|
+
delete result[key];
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
105
|
+
result[key] = mergeConfig(result[key], value);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
result[key] = cloneValue(value);
|
|
109
|
+
}
|
|
110
|
+
return pruneEmptyObjects(result);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getWecomConfig(cfg) {
|
|
114
|
+
return isPlainObject(cfg?.channels?.wecom) ? cfg.channels.wecom : {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getAccountEntries(wecom) {
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (const [key, value] of Object.entries(wecom ?? {})) {
|
|
120
|
+
if (RESERVED_KEYS.has(key) || !isPlainObject(value)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const accountId = normalizeAccountKey(key);
|
|
124
|
+
if (!accountId) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
entries.push({ key, accountId, value });
|
|
128
|
+
}
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasDictionaryAccounts(wecom) {
|
|
133
|
+
return getAccountEntries(wecom).length > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getSharedMultiAccountConfig(wecom) {
|
|
137
|
+
const shared = {};
|
|
138
|
+
for (const [key, value] of Object.entries(wecom ?? {})) {
|
|
139
|
+
if (SHARED_MULTI_ACCOUNT_KEYS.has(key)) {
|
|
140
|
+
shared[key] = cloneValue(value);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return shared;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function findEntryByAccountId(wecom, accountId) {
|
|
147
|
+
return getAccountEntries(wecom).find((entry) => entry.accountId === accountId) ?? null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildAccount(accountId, config, meta = {}) {
|
|
151
|
+
const safeConfig = isPlainObject(config) ? cloneValue(config) : {};
|
|
152
|
+
const agent = isPlainObject(safeConfig.agent) ? safeConfig.agent : {};
|
|
153
|
+
const botId = String(safeConfig.botId ?? "").trim();
|
|
154
|
+
const secret = String(safeConfig.secret ?? "").trim();
|
|
155
|
+
const websocketUrl = String(safeConfig.websocketUrl ?? DEFAULT_WS_URL).trim() || DEFAULT_WS_URL;
|
|
156
|
+
const enabled = safeConfig.enabled ?? Object.keys(safeConfig).length > 0;
|
|
157
|
+
const configured = Boolean(botId && secret);
|
|
158
|
+
const agentConfigured = Boolean(agent.corpId && agent.corpSecret && agent.agentId);
|
|
93
159
|
|
|
94
160
|
return {
|
|
95
161
|
accountId,
|
|
96
|
-
name:
|
|
97
|
-
enabled
|
|
98
|
-
configured
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
162
|
+
name: String(safeConfig.name ?? accountId ?? DEFAULT_ACCOUNT_ID).trim() || accountId,
|
|
163
|
+
enabled,
|
|
164
|
+
configured,
|
|
165
|
+
botId,
|
|
166
|
+
secret,
|
|
167
|
+
websocketUrl,
|
|
168
|
+
sendThinkingMessage: safeConfig.sendThinkingMessage !== false,
|
|
169
|
+
config: safeConfig,
|
|
170
|
+
configPath: meta.configPath ?? `channels.wecom.${accountId}`,
|
|
171
|
+
storageMode: meta.storageMode ?? "dictionary",
|
|
172
|
+
entryKey: meta.entryKey ?? accountId,
|
|
103
173
|
agentConfigured,
|
|
104
|
-
|
|
105
|
-
webhooksConfigured: Boolean(webhooks && Object.keys(webhooks).length > 0),
|
|
174
|
+
webhooksConfigured: isPlainObject(safeConfig.webhooks) && Object.keys(safeConfig.webhooks).length > 0,
|
|
106
175
|
agentCredentials: agentConfigured
|
|
107
|
-
? {
|
|
176
|
+
? {
|
|
177
|
+
corpId: String(agent.corpId),
|
|
178
|
+
corpSecret: String(agent.corpSecret),
|
|
179
|
+
agentId: agent.agentId,
|
|
180
|
+
}
|
|
108
181
|
: null,
|
|
109
182
|
};
|
|
110
183
|
}
|
|
111
184
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
185
|
+
function buildDisabledAccount(accountId) {
|
|
186
|
+
return buildAccount(
|
|
187
|
+
accountId,
|
|
188
|
+
{ enabled: false },
|
|
189
|
+
{ configPath: `channels.wecom.${accountId}`, storageMode: "dictionary", entryKey: accountId },
|
|
190
|
+
);
|
|
117
191
|
}
|
|
118
192
|
|
|
119
|
-
|
|
193
|
+
export function isDictionaryAccountConfig(cfg) {
|
|
194
|
+
return hasDictionaryAccounts(getWecomConfig(cfg));
|
|
195
|
+
}
|
|
120
196
|
|
|
121
|
-
/**
|
|
122
|
-
* List all configured account IDs.
|
|
123
|
-
* Returns `["default"]` for legacy single-account configs.
|
|
124
|
-
*/
|
|
125
197
|
export function listAccountIds(cfg) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
// Legacy single-account → just "default".
|
|
130
|
-
if (isLegacyConfig(wecom)) return [DEFAULT_ACCOUNT_ID];
|
|
131
|
-
|
|
132
|
-
// Dictionary mode — each non-reserved key is an account.
|
|
133
|
-
const ids = [];
|
|
134
|
-
for (const key of Object.keys(wecom)) {
|
|
135
|
-
if (RESERVED_KEYS.has(key)) continue;
|
|
136
|
-
const val = wecom[key];
|
|
137
|
-
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
138
|
-
const id = normalizeAccountKey(key);
|
|
139
|
-
if (id && !ids.includes(id)) ids.push(id);
|
|
140
|
-
}
|
|
198
|
+
const entries = getAccountEntries(getWecomConfig(cfg));
|
|
199
|
+
if (entries.length === 0) {
|
|
200
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
141
201
|
}
|
|
202
|
+
return [...new Set(entries.map((entry) => entry.accountId))].sort((left, right) => left.localeCompare(right));
|
|
203
|
+
}
|
|
142
204
|
|
|
143
|
-
|
|
144
|
-
|
|
205
|
+
export function resolveDefaultAccountId(cfg) {
|
|
206
|
+
const preferred = normalizeAccountKey(getWecomConfig(cfg)?.defaultAccount);
|
|
207
|
+
const ids = listAccountIds(cfg);
|
|
208
|
+
if (preferred && ids.includes(preferred)) {
|
|
209
|
+
return preferred;
|
|
145
210
|
}
|
|
146
|
-
|
|
211
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
212
|
+
return DEFAULT_ACCOUNT_ID;
|
|
213
|
+
}
|
|
214
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
147
215
|
}
|
|
148
216
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
export function resolveAccount(cfg, accountId) {
|
|
153
|
-
const wecom = cfg?.channels?.wecom;
|
|
154
|
-
if (!wecom) return null;
|
|
217
|
+
export function normalizeAccountId(accountId) {
|
|
218
|
+
return normalizeAccountKey(accountId) || DEFAULT_ACCOUNT_ID;
|
|
219
|
+
}
|
|
155
220
|
|
|
156
|
-
|
|
221
|
+
export function resolveAccount(cfg, accountId) {
|
|
222
|
+
const wecom = getWecomConfig(cfg);
|
|
223
|
+
const requestedId = normalizeAccountKey(accountId) || resolveDefaultAccountId(cfg);
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
logger.warn(`[accounts] legacy config does not have account "${resolvedId}"`);
|
|
162
|
-
return buildAccount(resolvedId, { enabled: false });
|
|
225
|
+
if (!hasDictionaryAccounts(wecom)) {
|
|
226
|
+
if (requestedId !== DEFAULT_ACCOUNT_ID) {
|
|
227
|
+
return buildDisabledAccount(requestedId);
|
|
163
228
|
}
|
|
164
|
-
return buildAccount(DEFAULT_ACCOUNT_ID, wecom
|
|
229
|
+
return buildAccount(DEFAULT_ACCOUNT_ID, wecom, {
|
|
230
|
+
configPath: "channels.wecom",
|
|
231
|
+
storageMode: "single",
|
|
232
|
+
entryKey: DEFAULT_ACCOUNT_ID,
|
|
233
|
+
});
|
|
165
234
|
}
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (normalizeAccountKey(key) === normalizedId) {
|
|
172
|
-
const val = wecom[key];
|
|
173
|
-
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
174
|
-
return buildAccount(normalizedId, val);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
236
|
+
const shared = getSharedMultiAccountConfig(wecom);
|
|
237
|
+
const entry = findEntryByAccountId(wecom, requestedId);
|
|
238
|
+
if (!entry) {
|
|
239
|
+
return buildDisabledAccount(requestedId);
|
|
177
240
|
}
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
return buildAccount(requestedId, mergeConfig(shared, entry.value), {
|
|
243
|
+
configPath: `channels.wecom.${entry.key}`,
|
|
244
|
+
storageMode: "dictionary",
|
|
245
|
+
entryKey: entry.key,
|
|
246
|
+
});
|
|
181
247
|
}
|
|
182
248
|
|
|
183
|
-
/**
|
|
184
|
-
* Resolve all accounts as a Map<accountId, account>.
|
|
185
|
-
*/
|
|
186
249
|
export function resolveAllAccounts(cfg) {
|
|
187
|
-
const ids = listAccountIds(cfg);
|
|
188
250
|
const accounts = new Map();
|
|
189
|
-
for (const
|
|
190
|
-
accounts.set(
|
|
251
|
+
for (const accountId of listAccountIds(cfg)) {
|
|
252
|
+
accounts.set(accountId, resolveAccount(cfg, accountId));
|
|
191
253
|
}
|
|
192
254
|
return accounts;
|
|
193
255
|
}
|
|
194
256
|
|
|
195
|
-
/**
|
|
196
|
-
* Extract Agent API credentials for a given accountId.
|
|
197
|
-
* Returns `{ corpId, corpSecret, agentId }` or null.
|
|
198
|
-
*/
|
|
199
257
|
export function resolveAgentConfigForAccount(cfg, accountId) {
|
|
258
|
+
return resolveAccount(cfg, accountId)?.agentCredentials ?? null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function resolveAllowFromForAccount(cfg, accountId) {
|
|
200
262
|
const account = resolveAccount(cfg, accountId);
|
|
201
|
-
|
|
263
|
+
const allowFrom = account?.config?.allowFrom;
|
|
264
|
+
return Array.isArray(allowFrom) ? allowFrom.map((entry) => String(entry)) : [];
|
|
202
265
|
}
|
|
203
266
|
|
|
204
|
-
/**
|
|
205
|
-
* Detect duplicate tokens / agentIds across accounts.
|
|
206
|
-
* Returns an array of conflict descriptions (empty = no conflicts).
|
|
207
|
-
*/
|
|
208
267
|
export function detectAccountConflicts(cfg) {
|
|
209
|
-
const accounts = resolveAllAccounts(cfg);
|
|
210
268
|
const conflicts = [];
|
|
269
|
+
const botOwners = new Map();
|
|
270
|
+
const agentOwners = new Map();
|
|
211
271
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (!account.enabled) continue;
|
|
272
|
+
for (const [accountId, account] of resolveAllAccounts(cfg)) {
|
|
273
|
+
if (!account.enabled) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
217
276
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (tokenOwners.has(key)) {
|
|
223
|
-
const owner = tokenOwners.get(key);
|
|
277
|
+
if (account.botId) {
|
|
278
|
+
const botKey = account.botId.toLowerCase();
|
|
279
|
+
if (botOwners.has(botKey)) {
|
|
280
|
+
const owner = botOwners.get(botKey);
|
|
224
281
|
conflicts.push({
|
|
225
|
-
type: "
|
|
226
|
-
accounts: [owner,
|
|
227
|
-
message: `账号 "${
|
|
282
|
+
type: "duplicate_bot_id",
|
|
283
|
+
accounts: [owner, accountId],
|
|
284
|
+
message: `账号 "${accountId}" 与 "${owner}" 使用了相同的 botId。`,
|
|
228
285
|
});
|
|
229
286
|
} else {
|
|
230
|
-
|
|
287
|
+
botOwners.set(botKey, accountId);
|
|
231
288
|
}
|
|
232
289
|
}
|
|
233
290
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (agentIdOwners.has(key)) {
|
|
239
|
-
const owner = agentIdOwners.get(key);
|
|
291
|
+
if (account.agentCredentials) {
|
|
292
|
+
const agentKey = `${account.agentCredentials.corpId}:${account.agentCredentials.agentId}`;
|
|
293
|
+
if (agentOwners.has(agentKey)) {
|
|
294
|
+
const owner = agentOwners.get(agentKey);
|
|
240
295
|
conflicts.push({
|
|
241
296
|
type: "duplicate_agent",
|
|
242
|
-
accounts: [owner,
|
|
243
|
-
message: `账号 "${
|
|
297
|
+
accounts: [owner, accountId],
|
|
298
|
+
message: `账号 "${accountId}" 与 "${owner}" 使用了相同的 Agent 配置 (${account.agentCredentials.corpId}/${account.agentCredentials.agentId})。`,
|
|
244
299
|
});
|
|
245
300
|
} else {
|
|
246
|
-
|
|
301
|
+
agentOwners.set(agentKey, accountId);
|
|
247
302
|
}
|
|
248
303
|
}
|
|
249
304
|
}
|
|
@@ -251,18 +306,97 @@ export function detectAccountConflicts(cfg) {
|
|
|
251
306
|
return conflicts;
|
|
252
307
|
}
|
|
253
308
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
309
|
+
export function updateAccountConfig(cfg, accountId, patch, options = {}) {
|
|
310
|
+
const normalizedId = normalizeAccountKey(accountId) || DEFAULT_ACCOUNT_ID;
|
|
311
|
+
const wecom = getWecomConfig(cfg);
|
|
312
|
+
const nextChannels = { ...(cfg?.channels ?? {}) };
|
|
313
|
+
|
|
314
|
+
if (normalizedId === DEFAULT_ACCOUNT_ID && !hasDictionaryAccounts(wecom) && options.forceDictionary !== true) {
|
|
315
|
+
nextChannels.wecom = mergeConfig(wecom, patch);
|
|
316
|
+
return { ...cfg, channels: nextChannels };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const nextWecom = { ...wecom };
|
|
320
|
+
const existingEntry = findEntryByAccountId(wecom, normalizedId);
|
|
321
|
+
const entryKey = existingEntry?.key ?? normalizedId;
|
|
322
|
+
const previousEntry = isPlainObject(nextWecom[entryKey]) ? nextWecom[entryKey] : {};
|
|
323
|
+
const nextEntry = mergeConfig(previousEntry, patch);
|
|
324
|
+
|
|
325
|
+
if (Object.keys(nextEntry).length > 0) {
|
|
326
|
+
nextWecom[entryKey] = nextEntry;
|
|
327
|
+
} else {
|
|
328
|
+
delete nextWecom[entryKey];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
nextChannels.wecom = nextWecom;
|
|
332
|
+
return { ...cfg, channels: nextChannels };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function setAccountConfig(cfg, accountId, patch, options = {}) {
|
|
336
|
+
return updateAccountConfig(cfg, accountId, patch, options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function setAccountEnabled({ cfg, accountId = DEFAULT_ACCOUNT_ID, enabled }) {
|
|
340
|
+
return updateAccountConfig(cfg, accountId, { enabled });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function deleteAccount({ cfg, accountId = DEFAULT_ACCOUNT_ID }) {
|
|
344
|
+
return deleteAccountConfig(cfg, accountId);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function clearAccountCredentials({ cfg, accountId = DEFAULT_ACCOUNT_ID }) {
|
|
348
|
+
return updateAccountConfig(
|
|
349
|
+
cfg,
|
|
350
|
+
accountId,
|
|
351
|
+
{
|
|
352
|
+
botId: "",
|
|
353
|
+
secret: "",
|
|
354
|
+
websocketUrl: undefined,
|
|
355
|
+
},
|
|
356
|
+
{ forceDictionary: accountId !== DEFAULT_ACCOUNT_ID },
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function resolveAccountBasePath(cfg, accountId) {
|
|
361
|
+
const account = resolveAccount(cfg, accountId);
|
|
362
|
+
return account?.configPath ?? "channels.wecom";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function deleteAccountConfig(cfg, accountId) {
|
|
366
|
+
const normalizedId = normalizeAccountKey(accountId) || DEFAULT_ACCOUNT_ID;
|
|
367
|
+
const wecom = getWecomConfig(cfg);
|
|
368
|
+
const nextChannels = { ...(cfg?.channels ?? {}) };
|
|
369
|
+
|
|
370
|
+
if (normalizedId === DEFAULT_ACCOUNT_ID && !hasDictionaryAccounts(wecom)) {
|
|
371
|
+
delete nextChannels.wecom;
|
|
372
|
+
return { ...cfg, channels: nextChannels };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const nextWecom = { ...wecom };
|
|
376
|
+
const existingEntry = findEntryByAccountId(wecom, normalizedId);
|
|
377
|
+
if (existingEntry) {
|
|
378
|
+
delete nextWecom[existingEntry.key];
|
|
379
|
+
}
|
|
380
|
+
nextChannels.wecom = nextWecom;
|
|
381
|
+
return { ...cfg, channels: nextChannels };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function describeAccount(account) {
|
|
385
|
+
return {
|
|
386
|
+
accountId: account.accountId,
|
|
387
|
+
name: account.name,
|
|
388
|
+
enabled: account.enabled,
|
|
389
|
+
configured: account.configured,
|
|
390
|
+
botId: account.botId,
|
|
391
|
+
websocketUrl: account.websocketUrl,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function logAccountConflicts(cfg) {
|
|
396
|
+
for (const conflict of detectAccountConflicts(cfg)) {
|
|
397
|
+
logger.error(`[wecom/accounts] ${conflict.message}`, {
|
|
398
|
+
type: conflict.type,
|
|
399
|
+
accounts: conflict.accounts,
|
|
400
|
+
});
|
|
266
401
|
}
|
|
267
|
-
return null;
|
|
268
402
|
}
|