@botcord/daemon 0.2.59 → 0.2.61
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/dist/config.d.ts +4 -1
- package/dist/config.js +2 -2
- package/dist/cross-room.js +3 -1
- package/dist/daemon-config-map.js +6 -0
- package/dist/daemon.js +21 -1
- package/dist/gateway/channels/botcord.d.ts +7 -0
- package/dist/gateway/channels/botcord.js +3 -1
- package/dist/gateway/channels/feishu-registration.d.ts +35 -0
- package/dist/gateway/channels/feishu-registration.js +101 -0
- package/dist/gateway/channels/feishu.d.ts +16 -0
- package/dist/gateway/channels/feishu.js +459 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +2 -0
- package/dist/gateway/channels/login-session.d.ts +9 -1
- package/dist/gateway/channels/login-session.js +1 -1
- package/dist/gateway/dispatcher.js +7 -3
- package/dist/gateway/policy-resolver.d.ts +10 -6
- package/dist/gateway/types.d.ts +2 -1
- package/dist/gateway-control.d.ts +8 -1
- package/dist/gateway-control.js +171 -18
- package/dist/provision.js +7 -1
- package/package.json +2 -1
- package/src/__tests__/cross-room.test.ts +2 -0
- package/src/__tests__/gateway-control.test.ts +84 -0
- package/src/__tests__/policy-updated-handler.test.ts +5 -7
- package/src/__tests__/third-party-gateway.test.ts +28 -0
- package/src/config.ts +6 -3
- package/src/cross-room.ts +3 -1
- package/src/daemon-config-map.ts +3 -0
- package/src/daemon.ts +24 -3
- package/src/gateway/__tests__/botcord-channel.test.ts +77 -0
- package/src/gateway/__tests__/dispatcher.test.ts +14 -4
- package/src/gateway/__tests__/feishu-channel.test.ts +306 -0
- package/src/gateway/channels/botcord.ts +10 -1
- package/src/gateway/channels/feishu-registration.ts +155 -0
- package/src/gateway/channels/feishu.ts +554 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/login-session.ts +10 -2
- package/src/gateway/dispatcher.ts +7 -3
- package/src/gateway/policy-resolver.ts +19 -11
- package/src/gateway/types.ts +2 -1
- package/src/gateway-control.ts +188 -17
- package/src/provision.ts +13 -1
|
@@ -24,21 +24,26 @@
|
|
|
24
24
|
|
|
25
25
|
import type { AttentionPolicy } from "@botcord/protocol-core";
|
|
26
26
|
|
|
27
|
+
export type DaemonAttentionPolicy = Omit<AttentionPolicy, "mode"> & {
|
|
28
|
+
mode: AttentionPolicy["mode"] | "allowed_senders";
|
|
29
|
+
allowedSenderIds?: string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
/** Public surface — kept narrow so the dispatcher can mock easily in tests. */
|
|
28
33
|
export interface PolicyResolverLike {
|
|
29
|
-
resolve(agentId: string, roomId: string | null): Promise<
|
|
34
|
+
resolve(agentId: string, roomId: string | null): Promise<DaemonAttentionPolicy>;
|
|
30
35
|
invalidate(agentId: string, roomId?: string): void;
|
|
31
36
|
/**
|
|
32
37
|
* Install (or replace) the cached policy entry for an agent / room. Used
|
|
33
38
|
* by the `policy_updated` control-frame handler to apply embedded policy
|
|
34
39
|
* payloads without forcing a refetch.
|
|
35
40
|
*/
|
|
36
|
-
put(agentId: string, roomId: string | null, policy:
|
|
41
|
+
put(agentId: string, roomId: string | null, policy: DaemonAttentionPolicy): void;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface PolicyResolverOptions {
|
|
40
45
|
/** Fetcher for the per-agent default. Returning `undefined` means "no policy known"; the resolver falls back to `mode=always`. */
|
|
41
|
-
fetchGlobal: (agentId: string) => Promise<
|
|
46
|
+
fetchGlobal: (agentId: string) => Promise<DaemonAttentionPolicy | undefined>;
|
|
42
47
|
/**
|
|
43
48
|
* Optional per-room fetcher. PR2 supplies this; PR3 leaves it
|
|
44
49
|
* unimplemented and the resolver collapses to the global policy.
|
|
@@ -46,13 +51,13 @@ export interface PolicyResolverOptions {
|
|
|
46
51
|
fetchEffective?: (
|
|
47
52
|
agentId: string,
|
|
48
53
|
roomId: string,
|
|
49
|
-
) => Promise<
|
|
54
|
+
) => Promise<DaemonAttentionPolicy | undefined>;
|
|
50
55
|
/** Cache TTL in milliseconds. Defaults to 5 minutes. */
|
|
51
56
|
ttlMs?: number;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
interface Entry {
|
|
55
|
-
policy:
|
|
60
|
+
policy: DaemonAttentionPolicy;
|
|
56
61
|
expiresAt: number;
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -64,14 +69,17 @@ const FETCH_FAILED = Symbol("fetch_failed");
|
|
|
64
69
|
* lets the user mute a DM, but a stale cache from before a UX bug is cheap
|
|
65
70
|
* to defend against here.
|
|
66
71
|
*/
|
|
67
|
-
function maybeForceDm(
|
|
72
|
+
function maybeForceDm(
|
|
73
|
+
roomId: string | null,
|
|
74
|
+
policy: DaemonAttentionPolicy,
|
|
75
|
+
): DaemonAttentionPolicy {
|
|
68
76
|
if (roomId && roomId.startsWith("rm_dm_") && policy.mode !== "always") {
|
|
69
77
|
return { ...policy, mode: "always" };
|
|
70
78
|
}
|
|
71
79
|
return policy;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
function defaultPolicy():
|
|
82
|
+
function defaultPolicy(): DaemonAttentionPolicy {
|
|
75
83
|
return { mode: "always", keywords: [] };
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -87,7 +95,7 @@ export class PolicyResolver implements PolicyResolverLike {
|
|
|
87
95
|
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
async resolve(agentId: string, roomId: string | null): Promise<
|
|
98
|
+
async resolve(agentId: string, roomId: string | null): Promise<DaemonAttentionPolicy> {
|
|
91
99
|
const now = Date.now();
|
|
92
100
|
|
|
93
101
|
// 1. Per-room cache — populated either by a `policy_updated{room_id}`
|
|
@@ -132,8 +140,8 @@ export class PolicyResolver implements PolicyResolverLike {
|
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
private async safeFetch(
|
|
135
|
-
fn: () => Promise<
|
|
136
|
-
): Promise<
|
|
143
|
+
fn: () => Promise<DaemonAttentionPolicy | undefined>,
|
|
144
|
+
): Promise<DaemonAttentionPolicy | undefined | typeof FETCH_FAILED> {
|
|
137
145
|
try {
|
|
138
146
|
return await fn();
|
|
139
147
|
} catch {
|
|
@@ -157,7 +165,7 @@ export class PolicyResolver implements PolicyResolverLike {
|
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
|
|
160
|
-
put(agentId: string, roomId: string | null, policy:
|
|
168
|
+
put(agentId: string, roomId: string | null, policy: DaemonAttentionPolicy): void {
|
|
161
169
|
const key = cacheKey(agentId, roomId);
|
|
162
170
|
this.cache.set(key, {
|
|
163
171
|
policy: maybeForceDm(roomId, policy),
|
package/src/gateway/types.ts
CHANGED
|
@@ -187,6 +187,7 @@ export interface GatewayOutboundMessage {
|
|
|
187
187
|
accountId: string;
|
|
188
188
|
conversationId: string;
|
|
189
189
|
threadId?: string | null;
|
|
190
|
+
type?: "message" | "error";
|
|
190
191
|
text: string;
|
|
191
192
|
attachments?: GatewayOutboundAttachment[];
|
|
192
193
|
replyTo?: string | null;
|
|
@@ -209,7 +210,7 @@ export interface ChannelStatusSnapshot {
|
|
|
209
210
|
lastStopAt?: number;
|
|
210
211
|
lastError?: string | null;
|
|
211
212
|
/** Third-party provider id when this channel is not the built-in BotCord. */
|
|
212
|
-
provider?: "wechat" | "telegram";
|
|
213
|
+
provider?: "wechat" | "telegram" | "feishu";
|
|
213
214
|
/** Last time the adapter polled the upstream provider (ms epoch). */
|
|
214
215
|
lastPollAt?: number;
|
|
215
216
|
/** Last time the adapter accepted an inbound message (ms epoch). */
|
package/src/gateway-control.ts
CHANGED
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
getBotQrcode,
|
|
34
34
|
getQrcodeStatus,
|
|
35
35
|
} from "./gateway/channels/wechat-login.js";
|
|
36
|
+
import {
|
|
37
|
+
pollFeishuRegistration,
|
|
38
|
+
startFeishuRegistration,
|
|
39
|
+
type FeishuDomain,
|
|
40
|
+
} from "./gateway/channels/feishu-registration.js";
|
|
36
41
|
import { WECHAT_BASE_INFO, wechatHeaders } from "./gateway/channels/wechat-http.js";
|
|
37
42
|
import { assertSafeBaseUrl, UnsafeBaseUrlError } from "./gateway/channels/url-guard.js";
|
|
38
43
|
import { log as daemonLog } from "./log.js";
|
|
@@ -42,7 +47,7 @@ import type { FetchLike } from "./gateway/channels/http-types.js";
|
|
|
42
47
|
|
|
43
48
|
type AckBody = Omit<ControlAck, "id">;
|
|
44
49
|
|
|
45
|
-
type GatewayProvider = "telegram" | "wechat";
|
|
50
|
+
type GatewayProvider = "telegram" | "wechat" | "feishu";
|
|
46
51
|
|
|
47
52
|
interface GatewayProfileSummary {
|
|
48
53
|
id: string;
|
|
@@ -51,6 +56,9 @@ interface GatewayProfileSummary {
|
|
|
51
56
|
label?: string;
|
|
52
57
|
enabled: boolean;
|
|
53
58
|
baseUrl?: string;
|
|
59
|
+
appId?: string;
|
|
60
|
+
domain?: "feishu" | "lark";
|
|
61
|
+
userOpenId?: string;
|
|
54
62
|
allowedSenderIds?: string[];
|
|
55
63
|
allowedChatIds?: string[];
|
|
56
64
|
splitAt?: number;
|
|
@@ -81,6 +89,7 @@ interface UpsertGatewayParams {
|
|
|
81
89
|
};
|
|
82
90
|
settings?: {
|
|
83
91
|
baseUrl?: string;
|
|
92
|
+
domain?: "feishu" | "lark";
|
|
84
93
|
allowedSenderIds?: string[];
|
|
85
94
|
allowedChatIds?: string[];
|
|
86
95
|
splitAt?: number;
|
|
@@ -93,6 +102,9 @@ interface UpsertGatewayResult {
|
|
|
93
102
|
accountId: string;
|
|
94
103
|
enabled: boolean;
|
|
95
104
|
tokenPreview?: string;
|
|
105
|
+
appId?: string;
|
|
106
|
+
domain?: "feishu" | "lark";
|
|
107
|
+
userOpenId?: string;
|
|
96
108
|
status?: GatewayProfileSummary["status"];
|
|
97
109
|
}
|
|
98
110
|
|
|
@@ -123,6 +135,7 @@ interface GatewayLoginStartParams {
|
|
|
123
135
|
accountId: string;
|
|
124
136
|
gatewayId?: string;
|
|
125
137
|
baseUrl?: string;
|
|
138
|
+
domain?: "feishu" | "lark";
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
interface GatewayLoginStartResult {
|
|
@@ -141,6 +154,9 @@ interface GatewayLoginStatusParams {
|
|
|
141
154
|
interface GatewayLoginStatusResult {
|
|
142
155
|
status: "pending" | "scanned" | "confirmed" | "expired" | "failed";
|
|
143
156
|
baseUrl?: string;
|
|
157
|
+
appId?: string;
|
|
158
|
+
domain?: "feishu" | "lark";
|
|
159
|
+
userOpenId?: string;
|
|
144
160
|
tokenPreview?: string;
|
|
145
161
|
}
|
|
146
162
|
|
|
@@ -179,6 +195,10 @@ export interface GatewayControlContext {
|
|
|
179
195
|
getBotQrcode: typeof getBotQrcode;
|
|
180
196
|
getQrcodeStatus: typeof getQrcodeStatus;
|
|
181
197
|
};
|
|
198
|
+
feishuLoginClient?: {
|
|
199
|
+
startFeishuRegistration: typeof startFeishuRegistration;
|
|
200
|
+
pollFeishuRegistration: typeof pollFeishuRegistration;
|
|
201
|
+
};
|
|
182
202
|
/** Override the global fetch — used by `test_gateway` for Telegram getMe. */
|
|
183
203
|
fetchImpl?: FetchLike;
|
|
184
204
|
}
|
|
@@ -192,6 +212,8 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
192
212
|
const cfgIO = ctx.configIO ?? { load: loadConfig, save: saveConfig };
|
|
193
213
|
const sessions = ctx.loginSessions ?? new LoginSessionStore();
|
|
194
214
|
const wechatLogin = ctx.wechatLoginClient ?? { getBotQrcode, getQrcodeStatus };
|
|
215
|
+
const feishuLogin =
|
|
216
|
+
ctx.feishuLoginClient ?? { startFeishuRegistration, pollFeishuRegistration };
|
|
195
217
|
// W7: validate fetch availability at construction so a missing global is
|
|
196
218
|
// diagnosed at startup, not during the first control frame. Tests inject
|
|
197
219
|
// `ctx.fetchImpl` explicitly and bypass the global lookup entirely.
|
|
@@ -252,9 +274,13 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
// Provider-specific secret resolution.
|
|
255
|
-
let
|
|
277
|
+
let secretPayload: Record<string, unknown>;
|
|
278
|
+
let tokenPreviewSource: string | undefined;
|
|
279
|
+
let feishuAppId: string | undefined;
|
|
280
|
+
let feishuDomain: "feishu" | "lark" | undefined;
|
|
281
|
+
let feishuUserOpenId: string | undefined;
|
|
256
282
|
if (params.type === "telegram") {
|
|
257
|
-
botToken = params.secret?.botToken;
|
|
283
|
+
const botToken = params.secret?.botToken;
|
|
258
284
|
if (!botToken) {
|
|
259
285
|
// Allow updates that only flip enabled/whitelist — only require a
|
|
260
286
|
// token when none is on disk yet.
|
|
@@ -262,7 +288,11 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
262
288
|
if (!existing?.botToken) {
|
|
263
289
|
return badParams("upsert_gateway: telegram requires secret.botToken on first install");
|
|
264
290
|
}
|
|
265
|
-
|
|
291
|
+
secretPayload = { botToken: existing.botToken };
|
|
292
|
+
tokenPreviewSource = existing.botToken;
|
|
293
|
+
} else {
|
|
294
|
+
secretPayload = { botToken };
|
|
295
|
+
tokenPreviewSource = botToken;
|
|
266
296
|
}
|
|
267
297
|
} else if (params.type === "wechat") {
|
|
268
298
|
const loginId = params.loginId;
|
|
@@ -294,9 +324,46 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
294
324
|
error: { code: "login_unconfirmed", message: "wechat login session has no bot token yet" },
|
|
295
325
|
};
|
|
296
326
|
}
|
|
297
|
-
|
|
327
|
+
secretPayload = { botToken: session.botToken };
|
|
328
|
+
tokenPreviewSource = session.botToken;
|
|
298
329
|
// Bind the session to its eventual gateway id for forensic logging.
|
|
299
330
|
sessions.update(loginId, { gatewayId: params.id });
|
|
331
|
+
} else if (params.type === "feishu") {
|
|
332
|
+
const loginId = params.loginId;
|
|
333
|
+
if (!loginId) {
|
|
334
|
+
return badParams("upsert_gateway: feishu requires loginId");
|
|
335
|
+
}
|
|
336
|
+
const session = sessions.get(loginId);
|
|
337
|
+
if (!session) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
error: { code: "login_expired", message: `feishu login session "${loginId}" not found or expired` },
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (session.provider !== "feishu") {
|
|
344
|
+
return badParams(`upsert_gateway: login session provider "${session.provider}" != "feishu"`);
|
|
345
|
+
}
|
|
346
|
+
if (session.accountId !== params.accountId) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
error: {
|
|
350
|
+
code: "login_account_mismatch",
|
|
351
|
+
message: "feishu login session accountId does not match upsert request",
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (!session.appId || !session.appSecret) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
error: { code: "login_unconfirmed", message: "feishu login session has no app credentials yet" },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
secretPayload = { appSecret: session.appSecret };
|
|
362
|
+
tokenPreviewSource = session.appSecret;
|
|
363
|
+
feishuAppId = session.appId;
|
|
364
|
+
feishuDomain = session.domain ?? params.settings?.domain ?? "feishu";
|
|
365
|
+
feishuUserOpenId = session.userOpenId;
|
|
366
|
+
sessions.update(loginId, { gatewayId: params.id });
|
|
300
367
|
} else {
|
|
301
368
|
return badParams(`upsert_gateway: unknown provider "${(params as { type: string }).type}"`);
|
|
302
369
|
}
|
|
@@ -314,7 +381,7 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
314
381
|
|
|
315
382
|
// Persist secret first (so a config write that succeeds is never
|
|
316
383
|
// followed by a missing-secret crash). Atomic rename inside saveSecret.
|
|
317
|
-
const secretFile = saveGatewaySecret(params.id,
|
|
384
|
+
const secretFile = saveGatewaySecret(params.id, secretPayload);
|
|
318
385
|
|
|
319
386
|
// Update or insert the third-party gateway profile in config.
|
|
320
387
|
const enabled = params.enabled !== false;
|
|
@@ -328,6 +395,9 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
328
395
|
allowedSenderIds: params.settings?.allowedSenderIds,
|
|
329
396
|
allowedChatIds: params.settings?.allowedChatIds,
|
|
330
397
|
splitAt: params.settings?.splitAt,
|
|
398
|
+
appId: feishuAppId,
|
|
399
|
+
domain: feishuDomain,
|
|
400
|
+
userOpenId: feishuUserOpenId,
|
|
331
401
|
});
|
|
332
402
|
cfgIO.save(next);
|
|
333
403
|
|
|
@@ -340,7 +410,12 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
340
410
|
// best-effort
|
|
341
411
|
}
|
|
342
412
|
try {
|
|
343
|
-
await ctx.gateway.addChannel(
|
|
413
|
+
await ctx.gateway.addChannel(
|
|
414
|
+
buildChannelConfig(params, secretFile, {
|
|
415
|
+
...(feishuAppId ? { appId: feishuAppId } : {}),
|
|
416
|
+
...(feishuDomain ? { domain: feishuDomain } : {}),
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
344
419
|
} catch (addErr) {
|
|
345
420
|
const message = addErr instanceof Error ? addErr.message : String(addErr);
|
|
346
421
|
daemonLog.warn("upsert_gateway.addChannel failed", { id: params.id, error: message });
|
|
@@ -380,9 +455,14 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
380
455
|
allowedSenderIds: prevProfile.allowedSenderIds,
|
|
381
456
|
allowedChatIds: prevProfile.allowedChatIds,
|
|
382
457
|
splitAt: prevProfile.splitAt,
|
|
458
|
+
domain: prevProfile.domain,
|
|
383
459
|
},
|
|
384
460
|
},
|
|
385
461
|
secretFile,
|
|
462
|
+
{
|
|
463
|
+
...(prevProfile.appId ? { appId: prevProfile.appId } : {}),
|
|
464
|
+
...(prevProfile.domain ? { domain: prevProfile.domain } : {}),
|
|
465
|
+
},
|
|
386
466
|
),
|
|
387
467
|
);
|
|
388
468
|
}
|
|
@@ -417,7 +497,10 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
417
497
|
type: params.type,
|
|
418
498
|
accountId: params.accountId,
|
|
419
499
|
enabled,
|
|
420
|
-
tokenPreview: maskTokenPreview(
|
|
500
|
+
tokenPreview: maskTokenPreview(tokenPreviewSource),
|
|
501
|
+
...(feishuAppId ? { appId: feishuAppId } : {}),
|
|
502
|
+
...(feishuDomain ? { domain: feishuDomain } : {}),
|
|
503
|
+
...(feishuUserOpenId ? { userOpenId: feishuUserOpenId } : {}),
|
|
421
504
|
...(liveStatus ? { status: pickStatus(liveStatus) } : {}),
|
|
422
505
|
};
|
|
423
506
|
daemonLog.info("upsert_gateway applied", {
|
|
@@ -527,9 +610,8 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
527
610
|
}
|
|
528
611
|
}
|
|
529
612
|
|
|
530
|
-
// WeChat:
|
|
531
|
-
//
|
|
532
|
-
// is loaded and at least one poll succeeded.
|
|
613
|
+
// WeChat/Feishu: fall back to the adapter snapshot. `authorized === true`
|
|
614
|
+
// means the secret is loaded and the provider client started.
|
|
533
615
|
const snap = ctx.gateway.snapshot().channels[profile.id];
|
|
534
616
|
const result: TestGatewayResult = snap
|
|
535
617
|
? {
|
|
@@ -542,7 +624,7 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
542
624
|
},
|
|
543
625
|
...(snap.lastError ? { error: snap.lastError } : {}),
|
|
544
626
|
}
|
|
545
|
-
: { id: profile.id, ok: false, error:
|
|
627
|
+
: { id: profile.id, ok: false, error: `${profile.type} channel not running` };
|
|
546
628
|
return { ok: true, result };
|
|
547
629
|
}
|
|
548
630
|
|
|
@@ -554,9 +636,7 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
554
636
|
if (!params.accountId || typeof params.accountId !== "string") {
|
|
555
637
|
return badParams("gateway_login_start: accountId is required");
|
|
556
638
|
}
|
|
557
|
-
if (params.provider !== "wechat") {
|
|
558
|
-
// Telegram has no qrcode flow; surface a clear error so the dashboard
|
|
559
|
-
// can fall through to the token form.
|
|
639
|
+
if (params.provider !== "wechat" && params.provider !== "feishu") {
|
|
560
640
|
return badParams(`gateway_login_start: provider "${params.provider}" does not require login`);
|
|
561
641
|
}
|
|
562
642
|
// W1: SSRF guard — `baseUrl` flows directly into an authenticated fetch.
|
|
@@ -566,6 +646,38 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
566
646
|
if (urlErr instanceof UnsafeBaseUrlError) return badParams(urlErr.message);
|
|
567
647
|
throw urlErr;
|
|
568
648
|
}
|
|
649
|
+
if (params.provider === "feishu") {
|
|
650
|
+
const domain: FeishuDomain = params.domain === "lark" ? "lark" : "feishu";
|
|
651
|
+
try {
|
|
652
|
+
const r = await feishuLogin.startFeishuRegistration({ domain });
|
|
653
|
+
const loginId = mintLoginId("feishu");
|
|
654
|
+
const session = sessions.create({
|
|
655
|
+
loginId,
|
|
656
|
+
accountId: params.accountId,
|
|
657
|
+
...(params.gatewayId ? { gatewayId: params.gatewayId } : {}),
|
|
658
|
+
provider: "feishu",
|
|
659
|
+
qrcode: r.deviceCode,
|
|
660
|
+
qrcodeUrl: r.verificationUriComplete,
|
|
661
|
+
domain: r.domain,
|
|
662
|
+
});
|
|
663
|
+
const result: GatewayLoginStartResult = {
|
|
664
|
+
loginId,
|
|
665
|
+
qrcode: r.deviceCode,
|
|
666
|
+
qrcodeUrl: r.verificationUriComplete,
|
|
667
|
+
expiresAt: session.expiresAt,
|
|
668
|
+
};
|
|
669
|
+
daemonLog.info("gateway_login_start", { provider: "feishu", loginId, accountId: params.accountId });
|
|
670
|
+
return { ok: true, result };
|
|
671
|
+
} catch (err) {
|
|
672
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
673
|
+
daemonLog.warn("gateway_login_start.feishu failed", { error: message });
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
error: { code: "provider_unreachable", message },
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
569
681
|
const baseUrl = params.baseUrl ?? DEFAULT_WECHAT_BASE_URL;
|
|
570
682
|
let qrcode: string;
|
|
571
683
|
let qrcodeUrl: string | undefined;
|
|
@@ -640,8 +752,56 @@ export function createGatewayControl(ctx: GatewayControlContext) {
|
|
|
640
752
|
};
|
|
641
753
|
return { ok: true, result };
|
|
642
754
|
}
|
|
755
|
+
if (params.provider === "feishu") {
|
|
756
|
+
if (!session.qrcode) {
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
error: { code: "no_qrcode", message: "login session has no device code to poll" },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const probe = await feishuLogin.pollFeishuRegistration(session.qrcode, {
|
|
764
|
+
domain: session.domain ?? "feishu",
|
|
765
|
+
});
|
|
766
|
+
if (probe.status === "confirmed" && probe.appId && probe.appSecret) {
|
|
767
|
+
const tokenPreview = maskTokenPreview(probe.appSecret);
|
|
768
|
+
sessions.update(params.loginId, {
|
|
769
|
+
appId: probe.appId,
|
|
770
|
+
appSecret: probe.appSecret,
|
|
771
|
+
domain: probe.domain,
|
|
772
|
+
userOpenId: probe.userOpenId,
|
|
773
|
+
tokenPreview,
|
|
774
|
+
});
|
|
775
|
+
const result: GatewayLoginStatusResult = {
|
|
776
|
+
status: "confirmed",
|
|
777
|
+
appId: probe.appId,
|
|
778
|
+
domain: probe.domain,
|
|
779
|
+
userOpenId: probe.userOpenId,
|
|
780
|
+
tokenPreview,
|
|
781
|
+
};
|
|
782
|
+
return { ok: true, result };
|
|
783
|
+
}
|
|
784
|
+
const status =
|
|
785
|
+
probe.status === "denied"
|
|
786
|
+
? "failed"
|
|
787
|
+
: probe.status === "expired"
|
|
788
|
+
? "expired"
|
|
789
|
+
: probe.status === "failed"
|
|
790
|
+
? "failed"
|
|
791
|
+
: "pending";
|
|
792
|
+
const result: GatewayLoginStatusResult = { status, domain: probe.domain };
|
|
793
|
+
return { ok: true, result };
|
|
794
|
+
} catch (err) {
|
|
795
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
796
|
+
daemonLog.warn("gateway_login_status.feishu failed", { error: message });
|
|
797
|
+
return {
|
|
798
|
+
ok: false,
|
|
799
|
+
error: { code: "provider_unreachable", message },
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
643
804
|
if (params.provider !== "wechat") {
|
|
644
|
-
// Future provider hook — today only WeChat poll path exists.
|
|
645
805
|
return badParams(`gateway_login_status: provider "${params.provider}" not supported`);
|
|
646
806
|
}
|
|
647
807
|
if (!session.qrcode) {
|
|
@@ -789,7 +949,7 @@ function badParams(message: string): AckBody {
|
|
|
789
949
|
}
|
|
790
950
|
|
|
791
951
|
function isProvider(p: unknown): p is GatewayProvider {
|
|
792
|
-
return p === "telegram" || p === "wechat";
|
|
952
|
+
return p === "telegram" || p === "wechat" || p === "feishu";
|
|
793
953
|
}
|
|
794
954
|
|
|
795
955
|
function validateUpsertParams(p: UpsertGatewayParams): string | null {
|
|
@@ -810,6 +970,9 @@ function annotateProfile(
|
|
|
810
970
|
...(p.label !== undefined ? { label: p.label } : {}),
|
|
811
971
|
enabled: p.enabled !== false,
|
|
812
972
|
...(p.baseUrl !== undefined ? { baseUrl: p.baseUrl } : {}),
|
|
973
|
+
...(p.appId !== undefined ? { appId: p.appId } : {}),
|
|
974
|
+
...(p.domain !== undefined ? { domain: p.domain } : {}),
|
|
975
|
+
...(p.userOpenId !== undefined ? { userOpenId: p.userOpenId } : {}),
|
|
813
976
|
...(p.allowedSenderIds !== undefined ? { allowedSenderIds: p.allowedSenderIds } : {}),
|
|
814
977
|
...(p.allowedChatIds !== undefined ? { allowedChatIds: p.allowedChatIds } : {}),
|
|
815
978
|
...(p.splitAt !== undefined ? { splitAt: p.splitAt } : {}),
|
|
@@ -857,6 +1020,9 @@ function compactProfile(p: ThirdPartyGatewayProfile): ThirdPartyGatewayProfile {
|
|
|
857
1020
|
if (p.label !== undefined) out.label = p.label;
|
|
858
1021
|
if (p.enabled !== undefined) out.enabled = p.enabled;
|
|
859
1022
|
if (p.baseUrl !== undefined) out.baseUrl = p.baseUrl;
|
|
1023
|
+
if (p.appId !== undefined) out.appId = p.appId;
|
|
1024
|
+
if (p.domain !== undefined) out.domain = p.domain;
|
|
1025
|
+
if (p.userOpenId !== undefined) out.userOpenId = p.userOpenId;
|
|
860
1026
|
if (p.allowedSenderIds !== undefined) out.allowedSenderIds = p.allowedSenderIds;
|
|
861
1027
|
if (p.allowedChatIds !== undefined) out.allowedChatIds = p.allowedChatIds;
|
|
862
1028
|
if (p.splitAt !== undefined) out.splitAt = p.splitAt;
|
|
@@ -868,6 +1034,7 @@ function compactProfile(p: ThirdPartyGatewayProfile): ThirdPartyGatewayProfile {
|
|
|
868
1034
|
function buildChannelConfig(
|
|
869
1035
|
params: UpsertGatewayParams,
|
|
870
1036
|
secretFile: string,
|
|
1037
|
+
extra: { appId?: string; domain?: "feishu" | "lark" } = {},
|
|
871
1038
|
): GatewayChannelConfig {
|
|
872
1039
|
const ch: GatewayChannelConfig = {
|
|
873
1040
|
id: params.id,
|
|
@@ -878,6 +1045,10 @@ function buildChannelConfig(
|
|
|
878
1045
|
if (params.label !== undefined) ch.label = params.label;
|
|
879
1046
|
const s = params.settings ?? {};
|
|
880
1047
|
if (s.baseUrl !== undefined) ch.baseUrl = s.baseUrl;
|
|
1048
|
+
if (params.type === "feishu") {
|
|
1049
|
+
if (extra.appId) ch.appId = extra.appId;
|
|
1050
|
+
if (extra.domain) ch.domain = extra.domain;
|
|
1051
|
+
}
|
|
881
1052
|
if (s.allowedSenderIds !== undefined) ch.allowedSenderIds = s.allowedSenderIds;
|
|
882
1053
|
if (s.allowedChatIds !== undefined) ch.allowedChatIds = s.allowedChatIds;
|
|
883
1054
|
if (s.splitAt !== undefined) ch.splitAt = s.splitAt;
|
package/src/provision.ts
CHANGED
|
@@ -286,11 +286,23 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
286
286
|
const roomId = typeof params.room_id === "string" ? params.room_id : undefined;
|
|
287
287
|
if (params.policy) {
|
|
288
288
|
// Embedded policy payload — install directly to avoid a refetch.
|
|
289
|
+
const embeddedPolicy = params.policy as unknown as {
|
|
290
|
+
mode?: string;
|
|
291
|
+
allowedSenderIds?: unknown;
|
|
292
|
+
};
|
|
289
293
|
policyResolver.put(agentId, roomId ?? null, {
|
|
290
|
-
mode:
|
|
294
|
+
mode:
|
|
295
|
+
embeddedPolicy.mode === "allowed_senders"
|
|
296
|
+
? "allowed_senders"
|
|
297
|
+
: params.policy.mode,
|
|
291
298
|
keywords: Array.isArray(params.policy.keywords)
|
|
292
299
|
? params.policy.keywords.slice()
|
|
293
300
|
: [],
|
|
301
|
+
allowedSenderIds: Array.isArray(embeddedPolicy.allowedSenderIds)
|
|
302
|
+
? embeddedPolicy.allowedSenderIds.filter(
|
|
303
|
+
(id): id is string => typeof id === "string",
|
|
304
|
+
)
|
|
305
|
+
: [],
|
|
294
306
|
...(typeof params.policy.muted_until === "number"
|
|
295
307
|
? { muted_until: params.policy.muted_until }
|
|
296
308
|
: {}),
|