@@ -145,6 +145,7 @@ function larkErrorMessage(cause: unknown, fallback: string, t: WorkspaceTranslat
145145 lark_app_required : t ( "lark.error.appRequired" ) ,
146146 invalid_lark_app : t ( "lark.error.invalidApp" ) ,
147147 lark_group_lookup_failed : t ( "lark.error.groupLookup" ) ,
148+ goal_topic_agent_conflict : t ( "lark.error.agentConflict" ) ,
148149 provider_api_failed : t ( "lark.error.provider" ) ,
149150 } ;
150151 return messages [ code ] ?? cause . message ;
@@ -186,10 +187,11 @@ export function LarkSettingsPage({
186187 const [ ingressMode , setIngressMode ] = useState < LarkIngressMode > ( "async_inbox" ) ;
187188 const [ replyMode , setReplyMode ] = useState < LarkReplyMode > ( "topic_reply" ) ;
188189 const [ agentId , setAgentId ] = useState ( "" ) ;
190+ const [ connectAllAgents , setConnectAllAgents ] = useState ( false ) ;
189191 const [ connecting , setConnecting ] = useState ( false ) ;
190192 const [ connectError , setConnectError ] = useState < string | null > ( null ) ;
191193 const [ editingGoalId , setEditingGoalId ] = useState < string | null > ( null ) ;
192- const [ disconnectGoalId , setDisconnectGoalId ] = useState < string | null > ( null ) ;
194+ const [ disconnectConnectionId , setDisconnectConnectionId ] = useState < string | null > ( null ) ;
193195 const [ setupOpen , setSetupOpen ] = useState ( false ) ;
194196 const [ setupAppRef , setSetupAppRef ] = useState ( "loopx-workspace-bot" ) ;
195197 const [ setupBrand , setSetupBrand ] = useState < "feishu" | "lark" > ( "feishu" ) ;
@@ -299,6 +301,19 @@ export function LarkSettingsPage({
299301 } , [ setupOpen , setupSnapshot ] ) ;
300302
301303 const selectedGoal = goals . find ( ( goal ) => goal . goalId === goalId ) ;
304+ const fallbackGoalAgents = selectedGoal ?. agentId
305+ ? [ { agentId : selectedGoal . agentId , label : selectedGoal . agentLabel ?? selectedGoal . agentId } ]
306+ : [ ] ;
307+ const goalAgents = selectedGoal ?. agentLanes ?. length
308+ ? selectedGoal . agentLanes
309+ : fallbackGoalAgents ;
310+ const selectedAgentAvailable = goalAgents . some ( ( agent ) => agent . agentId === agentId ) ;
311+ let targetAgentIds : string [ ] = [ ] ;
312+ if ( connectAllAgents ) targetAgentIds = goalAgents . map ( ( agent ) => agent . agentId ) ;
313+ else if ( selectedAgentAvailable ) targetAgentIds = [ agentId ] ;
314+ let connectActionLabel = t ( "lark.connect" ) ;
315+ if ( editingGoalId ) connectActionLabel = t ( "lark.saveConnection" ) ;
316+ else if ( connectAllAgents ) connectActionLabel = t ( "lark.connectAllAgentsAction" , { count : targetAgentIds . length } ) ;
302317 const selectedApp = apps . find ( ( app ) => app . app_ref === appRef ) ;
303318 const selectedChat = chats . find ( ( chat ) => chat . chat_id === chatId ) ;
304319 const filteredConnections = useMemo ( ( ) => {
@@ -317,6 +332,7 @@ export function LarkSettingsPage({
317332 setEditingGoalId ( null ) ;
318333 setGoalId ( nextGoal ?. goalId ?? "" ) ;
319334 setAgentId ( nextGoal ?. agentId ?? "" ) ;
335+ setConnectAllAgents ( false ) ;
320336 setCaptureScope ( "addressed_only" ) ;
321337 setIngressMode ( "async_inbox" ) ;
322338 setReplyMode ( "topic_reply" ) ;
@@ -330,6 +346,7 @@ export function LarkSettingsPage({
330346 setAppRef ( connection . app_ref ) ;
331347 setGoalId ( connection . goal_id ) ;
332348 setAgentId ( connection . agent_id ?? goals . find ( ( goal ) => goal . goalId === connection . goal_id ) ?. agentId ?? "" ) ;
349+ setConnectAllAgents ( false ) ;
333350 setCaptureScope ( connection . capture_scope ) ;
334351 setIngressMode ( connection . ingress_mode === "direct_session" ? "session_queue" : connection . ingress_mode ) ;
335352 setReplyMode ( connection . reply_mode ) ;
@@ -376,12 +393,11 @@ export function LarkSettingsPage({
376393 }
377394
378395 async function connect ( ) {
379- if ( ! appRef || ! goalId || ! selectedChat || connecting ) return ;
396+ if ( ! appRef || ! goalId || ! selectedChat || targetAgentIds . length === 0 || connecting ) return ;
380397 setConnecting ( true ) ;
381398 setConnectError ( null ) ;
382399 try {
383400 const input = {
384- agentId,
385401 appRef,
386402 captureScope,
387403 chatId : selectedChat . chat_id ,
@@ -391,10 +407,14 @@ export function LarkSettingsPage({
391407 ingressMode,
392408 replyMode,
393409 } as const ;
394- const preview = await connectLarkGoalTopic ( { ...input , execute : false } ) ;
395- if ( ! preview . ok ) throw new ChatApiError ( preview . public_summary ?? preview . blocker ?? t ( "lark.error.bindPreview" ) , { error_code : preview . blocker ?? "provider_api_failed" } ) ;
396- const result = await connectLarkGoalTopic ( { ...input , execute : true } ) ;
397- if ( ! result . ok ) throw new ChatApiError ( result . public_summary ?? result . blocker ?? t ( "lark.error.bind" ) , { error_code : result . blocker ?? "provider_api_failed" } ) ;
410+ for ( const targetAgentId of targetAgentIds ) {
411+ const preview = await connectLarkGoalTopic ( { ...input , agentId : targetAgentId , execute : false } ) ;
412+ if ( ! preview . ok ) throw new ChatApiError ( preview . public_summary ?? preview . blocker ?? t ( "lark.error.bindPreview" ) , { error_code : preview . blocker ?? "provider_api_failed" } ) ;
413+ }
414+ for ( const targetAgentId of targetAgentIds ) {
415+ const result = await connectLarkGoalTopic ( { ...input , agentId : targetAgentId , execute : true } ) ;
416+ if ( ! result . ok ) throw new ChatApiError ( result . public_summary ?? result . blocker ?? t ( "lark.error.bind" ) , { error_code : result . blocker ?? "provider_api_failed" } ) ;
417+ }
398418 setModalOpen ( false ) ;
399419 await refresh ( ) ;
400420 onChanged ?.( ) ;
@@ -405,14 +425,14 @@ export function LarkSettingsPage({
405425 }
406426 }
407427
408- async function disconnect ( goal : string ) {
409- if ( disconnectGoalId !== goal ) {
410- setDisconnectGoalId ( goal ) ;
428+ async function disconnect ( goal : string , connectionId : string ) {
429+ if ( disconnectConnectionId !== connectionId ) {
430+ setDisconnectConnectionId ( connectionId ) ;
411431 return ;
412432 }
413433 try {
414- await disconnectLarkGoalTopic ( goal ) ;
415- setDisconnectGoalId ( null ) ;
434+ await disconnectLarkGoalTopic ( goal , connectionId ) ;
435+ setDisconnectConnectionId ( null ) ;
416436 await refresh ( ) ;
417437 onChanged ?.( ) ;
418438 } catch ( cause ) {
@@ -466,7 +486,7 @@ export function LarkSettingsPage({
466486 < div className = "personal-lark-table" role = "table" aria-label = { t ( "lark.goalTopicConnections" ) } >
467487 < div className = "personal-lark-table-head" role = "row" > < span > { t ( "lark.connection" ) } </ span > < span > { t ( "common.goal" ) } </ span > < span > { t ( "lark.capture" ) } </ span > < span > { t ( "lark.processing" ) } </ span > < span > { t ( "common.actions" ) } </ span > </ div >
468488 { filteredConnections . map ( ( connection ) => (
469- < div className = "personal-lark-table-row" key = { connection . goal_id } role = "row" >
489+ < div className = "personal-lark-table-row" key = { connection . connection_id } role = "row" >
470490 < span >
471491 < strong > { connection . chat_name } </ strong >
472492 < small > { connection . app_label } · { larkConnectionHealth ( connection , t ) . label } </ small >
@@ -483,7 +503,7 @@ export function LarkSettingsPage({
483503 < span > < strong > { ingressPresentation ( connection . ingress_mode , t ) . label } </ strong > < small > { connection . agent_id ?? ingressPresentation ( connection . ingress_mode , t ) . detail } </ small > </ span >
484504 < span className = "personal-lark-row-actions" >
485505 < button aria-label = { t ( "lark.settingsConfigure" , { goal : connection . goal_title } ) } onClick = { ( ) => openConnectionEditor ( connection ) } type = "button" > < Settings2 size = { 15 } /> </ button >
486- < button aria-label = { t ( "lark.settingsDisconnect" , { goal : connection . goal_title } ) } className = { disconnectGoalId === connection . goal_id ? "is-confirm" : "" } onClick = { ( ) => void disconnect ( connection . goal_id ) } type = "button" > < Unlink size = { 15 } /> { disconnectGoalId === connection . goal_id ? t ( "common.confirm" ) : null } </ button >
506+ < button aria-label = { t ( "lark.settingsDisconnect" , { goal : connection . goal_title } ) } className = { disconnectConnectionId === connection . connection_id ? "is-confirm" : "" } onClick = { ( ) => void disconnect ( connection . goal_id , connection . connection_id ) } type = "button" > < Unlink size = { 15 } /> { disconnectConnectionId === connection . connection_id ? t ( "common.confirm" ) : null } </ button >
487507 </ span >
488508 </ div >
489509 ) ) }
@@ -511,11 +531,16 @@ export function LarkSettingsPage({
511531 < label > < span > { t ( "lark.topicPreview" ) } </ span > < div className = "personal-lark-topic-preview" > < MessageSquareText size = { 15 } /> # { selectedGoal ?. title ?? selectedGoal ?. goalId ?? "Goal" } </ div > </ label >
512532 < label > < span > { t ( "lark.captureScope" ) } </ span > < select aria-label = { t ( "lark.captureScope" ) } onChange = { ( event ) => setCaptureScope ( event . target . value as LarkCaptureScope ) } value = { captureScope } > < option value = "addressed_only" > { t ( "lark.captureAddressed" ) } </ option > < option value = "configured_chat_all" > { t ( "lark.captureAll" ) } </ option > </ select > < small > { t ( "lark.captureScopeDescription" ) } </ small > </ label >
513533 < fieldset aria-label = { t ( "lark.agentIngress" ) } className = "personal-lark-ingress" > < legend > { t ( "lark.agentIngress" ) } </ legend > < div > { ( [ "live_steering" , "session_queue" , "async_inbox" ] as const ) . map ( ( mode ) => { const presentation = ingressPresentation ( mode , t ) ; return < label className = { ingressMode === mode ? "is-active" : "" } key = { mode } > < input aria-label = { presentation . label } checked = { ingressMode === mode } name = "lark-agent-ingress" onChange = { ( ) => setIngressMode ( mode ) } type = "radio" value = { mode } /> < span > < strong > { presentation . label } </ strong > < small > { presentation . detail } </ small > </ span > </ label > ; } ) } </ div > </ fieldset >
514- < label > < span > { t ( "lark.targetAgent" ) } </ span > < select aria-label = { t ( "lark.targetAgent" ) } onChange = { ( event ) => setAgentId ( event . target . value ) } value = { agentId } > < option value = { selectedGoal ?. agentId ?? "" } > { selectedGoal ?. agentLabel ?? selectedGoal ?. agentId ?? t ( "lark.noAgentConfigured" ) } </ option > </ select > < small > { t ( "lark.targetAgentDescription" ) } </ small > </ label >
534+ < label > < span > { t ( "lark.targetAgent" ) } </ span > < select aria-label = { t ( "lark.targetAgent" ) } onChange = { ( event ) => setAgentId ( event . target . value ) } value = { agentId } >
535+ { ! selectedAgentAvailable ? < option disabled value = { agentId } > { agentId ? t ( "lark.agentUnavailable" , { agent : agentId } ) : t ( "lark.noAgentConfigured" ) } </ option > : null }
536+ { goalAgents . map ( ( agent ) => < option key = { agent . agentId } value = { agent . agentId } > { agent . label === agent . agentId ? agent . agentId : `${ agent . label } · ${ agent . agentId } ` } </ option > ) }
537+ </ select > < small > { t ( "lark.targetAgentDescription" ) } </ small > </ label >
538+ { ! editingGoalId && goalAgents . length > 1 ? < label aria-label = { t ( "lark.connectAllAgents" ) } className = "personal-lark-check" > < input checked = { connectAllAgents } onChange = { ( event ) => setConnectAllAgents ( event . target . checked ) } type = "checkbox" /> < span > < strong > { t ( "lark.connectAllAgents" ) } </ strong > < small > { t ( "lark.connectAllAgentsDescription" , { count : goalAgents . length } ) } </ small > </ span > </ label > : null }
539+ { targetAgentIds . length === 0 ? < p className = "personal-notification-error" role = "alert" > { t ( "lark.selectRegisteredAgent" ) } </ p > : null }
515540 < label > < span > { t ( "lark.replyMode" ) } </ span > < select aria-label = { t ( "lark.replyMode" ) } onChange = { ( event ) => setReplyMode ( event . target . value as LarkReplyMode ) } value = { replyMode } > < option value = "topic_reply" > { t ( "lark.topicReply" ) } </ option > </ select > < small > { t ( "lark.replyModeDescription" ) } </ small > </ label >
516541 < p className = "personal-lark-cardinality" > < Check size = { 15 } /> { t ( "lark.cardinality" ) } </ p >
517- { connectError ? < p className = "personal-notification-error" > { connectError } </ p > : null }
518- < footer > < button className = "personal-secondary-action" onClick = { ( ) => setModalOpen ( false ) } type = "button" > { t ( "lark.cancel" ) } </ button > < button className = "personal-primary-action" disabled = { loading || ! appRef || ! selectedApp ?. reply_ready || ! goalId || ! chatId || ! agentId || connecting } onClick = { ( ) => void connect ( ) } type = "button" > { connecting ? < Loader2 className = "is-spinning" size = { 15 } /> : null } { editingGoalId ? t ( "lark.saveConnection" ) : t ( "lark.connect" ) } </ button > </ footer >
542+ { connectError ? < p className = "personal-notification-error" role = "alert" > { connectError } </ p > : null }
543+ < footer > < button className = "personal-secondary-action" onClick = { ( ) => setModalOpen ( false ) } type = "button" > { t ( "lark.cancel" ) } </ button > < button className = "personal-primary-action" disabled = { loading || ! appRef || ! selectedApp ?. reply_ready || ! goalId || ! chatId || targetAgentIds . length === 0 || connecting } onClick = { ( ) => void connect ( ) } type = "button" > { connecting ? < Loader2 className = "is-spinning" size = { 15 } /> : null } { connectActionLabel } </ button > </ footer >
519544 </ section >
520545 </ div >
521546 ) : null }
0 commit comments