@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.
Files changed (43) hide show
  1. package/dist/config.d.ts +4 -1
  2. package/dist/config.js +2 -2
  3. package/dist/cross-room.js +3 -1
  4. package/dist/daemon-config-map.js +6 -0
  5. package/dist/daemon.js +21 -1
  6. package/dist/gateway/channels/botcord.d.ts +7 -0
  7. package/dist/gateway/channels/botcord.js +3 -1
  8. package/dist/gateway/channels/feishu-registration.d.ts +35 -0
  9. package/dist/gateway/channels/feishu-registration.js +101 -0
  10. package/dist/gateway/channels/feishu.d.ts +16 -0
  11. package/dist/gateway/channels/feishu.js +459 -0
  12. package/dist/gateway/channels/index.d.ts +2 -0
  13. package/dist/gateway/channels/index.js +2 -0
  14. package/dist/gateway/channels/login-session.d.ts +9 -1
  15. package/dist/gateway/channels/login-session.js +1 -1
  16. package/dist/gateway/dispatcher.js +7 -3
  17. package/dist/gateway/policy-resolver.d.ts +10 -6
  18. package/dist/gateway/types.d.ts +2 -1
  19. package/dist/gateway-control.d.ts +8 -1
  20. package/dist/gateway-control.js +171 -18
  21. package/dist/provision.js +7 -1
  22. package/package.json +2 -1
  23. package/src/__tests__/cross-room.test.ts +2 -0
  24. package/src/__tests__/gateway-control.test.ts +84 -0
  25. package/src/__tests__/policy-updated-handler.test.ts +5 -7
  26. package/src/__tests__/third-party-gateway.test.ts +28 -0
  27. package/src/config.ts +6 -3
  28. package/src/cross-room.ts +3 -1
  29. package/src/daemon-config-map.ts +3 -0
  30. package/src/daemon.ts +24 -3
  31. package/src/gateway/__tests__/botcord-channel.test.ts +77 -0
  32. package/src/gateway/__tests__/dispatcher.test.ts +14 -4
  33. package/src/gateway/__tests__/feishu-channel.test.ts +306 -0
  34. package/src/gateway/channels/botcord.ts +10 -1
  35. package/src/gateway/channels/feishu-registration.ts +155 -0
  36. package/src/gateway/channels/feishu.ts +554 -0
  37. package/src/gateway/channels/index.ts +6 -0
  38. package/src/gateway/channels/login-session.ts +10 -2
  39. package/src/gateway/dispatcher.ts +7 -3
  40. package/src/gateway/policy-resolver.ts +19 -11
  41. package/src/gateway/types.ts +2 -1
  42. package/src/gateway-control.ts +188 -17
  43. 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<AttentionPolicy>;
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: AttentionPolicy): void;
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<AttentionPolicy | undefined>;
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<AttentionPolicy | undefined>;
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: AttentionPolicy;
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(roomId: string | null, policy: AttentionPolicy): AttentionPolicy {
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(): AttentionPolicy {
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<AttentionPolicy> {
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<AttentionPolicy | undefined>,
136
- ): Promise<AttentionPolicy | undefined | typeof FETCH_FAILED> {
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: AttentionPolicy): void {
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),
@@ -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). */
@@ -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 botToken: string | undefined;
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
- botToken = existing.botToken;
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
- botToken = session.botToken;
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, { botToken });
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(buildChannelConfig(params, secretFile));
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(botToken),
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: iLink has no no-side-effect probe today. Fall back to the
531
- // adapter's last poll snapshot. `authorized === true` means the secret
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: "wechat channel not running" };
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: params.policy.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
  : {}),