| 56 | |
| 57 | /** Feishu/Lark long-connection bridge backed by the official Channel API. */ |
| 58 | export class FeishuBotBridge extends BaseBotAdapter implements SendCapable { |
| 59 | private channel: LarkChannel | null = null; |
| 60 | private unsubscribe: Array<() => void> = []; |
| 61 | private explicitlyStopped = false; |
| 62 | |
| 63 | constructor(settings: BotChannelSettings) { |
| 64 | super('feishu', settings); |
| 65 | } |
| 66 | |
| 67 | async start(): Promise<void> { |
| 68 | if (this.running) return; |
| 69 | if (!this.settings.enabled) { |
| 70 | this.reason = 'disabled'; |
| 71 | this.readiness = 'scaffolded'; |
| 72 | return; |
| 73 | } |
| 74 | const appId = this.settings.appId?.trim() ?? ''; |
| 75 | const appSecret = this.settings.appSecret?.trim() || this.settings.token.trim(); |
| 76 | if (!appId || !appSecret) { |
| 77 | this.reason = 'missing-feishu-credentials'; |
| 78 | this.readiness = 'scaffolded'; |
| 79 | this.emitStatusChange(); |
| 80 | return; |
| 81 | } |
| 82 | |
| 83 | this.explicitlyStopped = false; |
| 84 | const isLark = this.settings.domain?.trim() === 'larksuite.com'; |
| 85 | const channel = createLarkChannel({ |
| 86 | appId, |
| 87 | appSecret, |
| 88 | domain: isLark ? Domain.Lark : Domain.Feishu, |
| 89 | transport: 'websocket', |
| 90 | source: 'maka', |
| 91 | loggerLevel: LoggerLevel.error, |
| 92 | handshakeTimeoutMs: HANDSHAKE_TIMEOUT_MS, |
| 93 | policy: { |
| 94 | dmMode: this.settings.allowedUserIds?.length ? 'allowlist' : 'open', |
| 95 | dmAllowlist: [...(this.settings.allowedUserIds ?? [])], |
| 96 | requireMention: false, |
| 97 | }, |
| 98 | }); |
| 99 | this.channel = channel; |
| 100 | this.wire(channel, appId); |
| 101 | |
| 102 | try { |
| 103 | await channel.connect(); |
| 104 | if (this.explicitlyStopped || this.channel !== channel) return; |
| 105 | this.startedAt = Date.now(); |
| 106 | this.markConnected(channel, appId); |
| 107 | } catch (error) { |
| 108 | if (this.explicitlyStopped || this.channel !== channel) return; |
| 109 | this.running = false; |
| 110 | this.reason = generalizedErrorMessage(error); |
| 111 | this.readiness = 'configured'; |
| 112 | this.emitStatusChange(); |
| 113 | await this.disconnectChannel(channel); |
| 114 | } |
| 115 | } |
nothing calls this directly
no outgoing calls
no test coverage detected