11import { createAgentAdapter } from "@/agents/adapter" ;
22import type { OpenCodeMessageContext } from "@/agents/types" ;
3+ import * as Lark from "@larksuiteoapi/node-sdk" ;
34import {
45 getChannelSystemMessage ,
56 getGitHubInfoForUser ,
@@ -52,6 +53,7 @@ type LarkBotInfoResponse = {
5253const tenantTokenCache = new Map < string , { token : string ; expiresAt : number } > ( ) ;
5354const botOpenIdCache = new Map < string , string > ( ) ;
5455const sentMessageThreadMap = new Map < string , { channelId : string ; threadId : string } > ( ) ;
56+ const wsClientRegistry = new Map < string , unknown > ( ) ;
5557
5658function getLarkCredentialsForChannel ( channelId : string ) : LarkCredentials | null {
5759 const channel = channelId . trim ( ) ;
@@ -444,42 +446,37 @@ type LarkIncomingEnvelope = {
444446 } ;
445447} ;
446448
447- export async function handleLarkEventPayload ( payload : unknown ) : Promise < { status : number ; body : Record < string , unknown > } > {
448- if ( ! payload || typeof payload !== "object" ) {
449- return { status : 400 , body : { ok : false , error : "Invalid payload" } } ;
450- }
451-
452- const envelope = payload as LarkIncomingEnvelope ;
453- if ( envelope . type === "url_verification" && typeof envelope . challenge === "string" ) {
454- return { status : 200 , body : { challenge : envelope . challenge } } ;
455- }
449+ type LarkIncomingEvent = NonNullable < LarkIncomingEnvelope [ "event" ] > ;
456450
457- if ( envelope . header ?. event_type !== "im.message.receive_v1" ) {
458- return { status : 200 , body : { code : 0 } } ;
459- }
451+ function isLarkLongConnectionEnabled ( ) : boolean {
452+ const raw = process . env . LARK_LONG_CONNECTION ?. trim ( ) . toLowerCase ( ) ;
453+ if ( ! raw ) return true ;
454+ return ! [ "0" , "false" , "off" , "no" ] . includes ( raw ) ;
455+ }
460456
461- const message = envelope . event ?. message ;
462- const senderOpenId = envelope . event ?. sender ?. sender_id ?. open_id ?. trim ( ) || "" ;
457+ async function processLarkIncomingEvent ( event : LarkIncomingEvent ) : Promise < void > {
458+ const message = event . message ;
459+ const senderOpenId = event . sender ?. sender_id ?. open_id ?. trim ( ) || "" ;
463460 const channelId = message ?. chat_id ?. trim ( ) || "" ;
464461 const messageId = message ?. message_id ?. trim ( ) || "" ;
465462 const threadId = message ?. root_id ?. trim ( ) || message ?. parent_id ?. trim ( ) || messageId ;
466463 const isThreadReply = Boolean ( message ?. root_id || message ?. parent_id ) ;
467464
468465 if ( ! channelId || ! messageId || ! threadId || ! senderOpenId ) {
469- return { status : 200 , body : { code : 0 } } ;
466+ return ;
470467 }
471468
472469 if ( ! isAuthorizedLarkChannel ( channelId ) ) {
473- return { status : 200 , body : { code : 0 } } ;
470+ return ;
474471 }
475472
476473 const botOpenId = await getBotOpenIdForChannel ( channelId ) ;
477474 if ( botOpenId && senderOpenId === botOpenId ) {
478- return { status : 200 , body : { code : 0 } } ;
475+ return ;
479476 }
480477
481478 if ( message ?. message_type !== "text" ) {
482- return { status : 200 , body : { code : 0 } } ;
479+ return ;
483480 }
484481
485482 const mentions = parseMentionedOpenIds ( message ?. mentions ) ;
@@ -490,27 +487,27 @@ export async function handleLarkEventPayload(payload: unknown): Promise<{ status
490487
491488 if ( isSettingsCommand ( text ) ) {
492489 await sendSettingsCard ( channelId , threadId ) ;
493- return { status : 200 , body : { code : 0 } } ;
490+ return ;
494491 }
495492
496493 if ( isThreadReply ) {
497494 if ( ! isMentioned && ! active ) {
498- return { status : 200 , body : { code : 0 } } ;
495+ return ;
499496 }
500497 } else if ( ! isMentioned ) {
501- return { status : 200 , body : { code : 0 } } ;
498+ return ;
502499 }
503500
504501 if ( ! text ) {
505- return { status : 200 , body : { code : 0 } } ;
502+ return ;
506503 }
507504
508505 if ( isStopCommand ( text ) ) {
509506 const stopped = await coreRuntime . handleStopCommand ( channelId , threadId ) ;
510507 if ( stopped ) {
511508 await sendMessage ( channelId , threadId , "Request stopped." , true ) ;
512509 }
513- return { status : 200 , body : { code : 0 } } ;
510+ return ;
514511 }
515512
516513 markThreadActive ( channelId , threadId ) ;
@@ -524,6 +521,98 @@ export async function handleLarkEventPayload(payload: unknown): Promise<{ status
524521 } ,
525522 text
526523 ) ;
524+ }
525+
526+ async function startLarkLongConnections ( reason : string ) : Promise < void > {
527+ if ( ! isLarkLongConnectionEnabled ( ) ) {
528+ log . debug ( "Lark long connection disabled" , { reason } ) ;
529+ return ;
530+ }
531+
532+ const workspaces = getLarkAppCredentials ( ) ;
533+ const uniqueCredentials = new Map < string , { appId : string ; appSecret : string } > ( ) ;
534+ for ( const workspace of workspaces ) {
535+ if ( ! uniqueCredentials . has ( workspace . appId ) ) {
536+ uniqueCredentials . set ( workspace . appId , {
537+ appId : workspace . appId ,
538+ appSecret : workspace . appSecret ,
539+ } ) ;
540+ }
541+ }
542+
543+ for ( const [ appId , creds ] of uniqueCredentials . entries ( ) ) {
544+ if ( wsClientRegistry . has ( appId ) ) {
545+ continue ;
546+ }
547+
548+ const eventDispatcher = new Lark . EventDispatcher ( { } ) . register ( {
549+ "im.message.receive_v1" : async ( data : unknown ) => {
550+ try {
551+ await processLarkIncomingEvent ( data as LarkIncomingEvent ) ;
552+ } catch ( error ) {
553+ log . warn ( "Failed to handle Lark long-connection message event" , {
554+ appId,
555+ error : String ( error ) ,
556+ } ) ;
557+ }
558+ } ,
559+ } ) ;
560+
561+ const wsClient = new Lark . WSClient ( {
562+ appId : creds . appId ,
563+ appSecret : creds . appSecret ,
564+ domain : Lark . Domain . Feishu ,
565+ loggerLevel : Lark . LoggerLevel . warn ,
566+ } ) ;
567+
568+ await Promise . resolve (
569+ wsClient . start ( {
570+ eventDispatcher,
571+ } )
572+ ) ;
573+
574+ wsClientRegistry . set ( appId , wsClient ) ;
575+ log . info ( "Lark long connection started" , { appId } ) ;
576+ }
577+ }
578+
579+ async function stopLarkLongConnections ( reason : string ) : Promise < void > {
580+ const entries = Array . from ( wsClientRegistry . entries ( ) ) ;
581+ wsClientRegistry . clear ( ) ;
582+ for ( const [ appId , client ] of entries ) {
583+ try {
584+ const wsClient = client as { stop ?: ( ) => unknown | Promise < unknown > } ;
585+ if ( typeof wsClient . stop === "function" ) {
586+ await Promise . resolve ( wsClient . stop ( ) ) ;
587+ }
588+ log . info ( "Lark long connection stopped" , { appId, reason } ) ;
589+ } catch ( error ) {
590+ log . warn ( "Failed to stop Lark long connection" , {
591+ appId,
592+ reason,
593+ error : String ( error ) ,
594+ } ) ;
595+ }
596+ }
597+ }
598+
599+ export async function handleLarkEventPayload ( payload : unknown ) : Promise < { status : number ; body : Record < string , unknown > } > {
600+ if ( ! payload || typeof payload !== "object" ) {
601+ return { status : 400 , body : { ok : false , error : "Invalid payload" } } ;
602+ }
603+
604+ const envelope = payload as LarkIncomingEnvelope ;
605+ if ( envelope . type === "url_verification" && typeof envelope . challenge === "string" ) {
606+ return { status : 200 , body : { challenge : envelope . challenge } } ;
607+ }
608+
609+ if ( envelope . header ?. event_type !== "im.message.receive_v1" ) {
610+ return { status : 200 , body : { code : 0 } } ;
611+ }
612+
613+ if ( envelope . event ) {
614+ await processLarkIncomingEvent ( envelope . event ) ;
615+ }
527616
528617 return { status : 200 , body : { code : 0 } } ;
529618}
@@ -543,12 +632,14 @@ export async function startLarkRuntime(reason: string): Promise<boolean> {
543632 reason,
544633 workspaceCount : workspaces . length ,
545634 } ) ;
635+ await startLarkLongConnections ( reason ) ;
546636 return true ;
547637}
548638
549639export async function stopLarkRuntime ( reason : string ) : Promise < void > {
550640 if ( ! larkRuntimeStarted ) return ;
551641 larkRuntimeStarted = false ;
642+ await stopLarkLongConnections ( reason ) ;
552643 tenantTokenCache . clear ( ) ;
553644 botOpenIdCache . clear ( ) ;
554645 sentMessageThreadMap . clear ( ) ;
0 commit comments