@@ -446,6 +446,47 @@ pub fn build_mcp_state(services: &AppServices) -> McpRouterState {
446446 }
447447}
448448
449+ fn build_channel_settings_service (
450+ services : & AppServices ,
451+ ) -> Arc < aionui_channel:: channel_settings:: ChannelSettingsService > {
452+ let pref_repo: Arc < dyn aionui_db:: IClientPreferenceRepository > =
453+ Arc :: new ( SqliteClientPreferenceRepository :: new ( services. database . pool ( ) . clone ( ) ) ) ;
454+
455+ Arc :: new (
456+ aionui_channel:: channel_settings:: ChannelSettingsService :: new ( pref_repo)
457+ . with_agent_metadata_repo ( Arc :: new ( SqliteAgentMetadataRepository :: new (
458+ services. database . pool ( ) . clone ( ) ,
459+ ) ) )
460+ . with_assistant_repos (
461+ Arc :: new ( SqliteAssistantDefinitionRepository :: new (
462+ services. database . pool ( ) . clone ( ) ,
463+ ) ) ,
464+ Arc :: new ( SqliteAssistantOverlayRepository :: new ( services. database . pool ( ) . clone ( ) ) ) ,
465+ ) ,
466+ )
467+ }
468+
469+ async fn build_channel_message_service (
470+ services : & AppServices ,
471+ channel_settings : Arc < aionui_channel:: channel_settings:: ChannelSettingsService > ,
472+ ) -> Arc < aionui_channel:: message_service:: ChannelMessageService > {
473+ let owner_user_id = services
474+ . user_repo
475+ . get_primary_webui_user ( )
476+ . await
477+ . ok ( )
478+ . flatten ( )
479+ . map ( |u| u. id )
480+ . unwrap_or_else ( || "system_default_user" . to_string ( ) ) ;
481+
482+ Arc :: new ( aionui_channel:: message_service:: ChannelMessageService :: new (
483+ Arc :: new ( services. conversation_service . clone ( ) ) ,
484+ services. worker_task_manager . clone ( ) ,
485+ channel_settings,
486+ owner_user_id,
487+ ) )
488+ }
489+
449490/// Build the default `ChannelRouterState` and orchestrator components.
450491pub async fn build_channel_state (
451492 services : & AppServices ,
@@ -476,22 +517,8 @@ pub async fn build_channel_state(
476517 let plugin_factory: Arc < aionui_channel:: manager:: PluginFactory > =
477518 Arc :: new ( Box :: new ( aionui_channel:: plugins:: create_plugin) ) ;
478519
479- // Build channel settings service for per-plugin agent/model configuration
480- let pref_pool = services. database . pool ( ) . clone ( ) ;
481- let pref_repo: Arc < dyn aionui_db:: IClientPreferenceRepository > =
482- Arc :: new ( SqliteClientPreferenceRepository :: new ( pref_pool) ) ;
483- let channel_settings = Arc :: new (
484- aionui_channel:: channel_settings:: ChannelSettingsService :: new ( pref_repo)
485- . with_agent_metadata_repo ( Arc :: new ( SqliteAgentMetadataRepository :: new (
486- services. database . pool ( ) . clone ( ) ,
487- ) ) )
488- . with_assistant_repos (
489- Arc :: new ( SqliteAssistantDefinitionRepository :: new (
490- services. database . pool ( ) . clone ( ) ,
491- ) ) ,
492- Arc :: new ( SqliteAssistantOverlayRepository :: new ( services. database . pool ( ) . clone ( ) ) ) ,
493- ) ,
494- ) ;
520+ // Build channel settings service for per-plugin agent/model configuration.
521+ let channel_settings = build_channel_settings_service ( services) ;
495522
496523 // Build orchestrator dependencies
497524 let action_executor = Arc :: new ( aionui_channel:: action:: ActionExecutor :: new (
@@ -500,53 +527,7 @@ pub async fn build_channel_state(
500527 Arc :: clone ( & channel_settings) ,
501528 ) ) ;
502529
503- let conv_repo: Arc < dyn aionui_db:: IConversationRepository > = Arc :: new (
504- aionui_db:: SqliteConversationRepository :: new ( services. database . pool ( ) . clone ( ) ) ,
505- ) ;
506- let skill_resolver = Arc :: new ( aionui_conversation:: skill_resolver:: ExtensionSkillResolver :: new (
507- services. skill_paths . clone ( ) ,
508- services. skill_repo . clone ( ) ,
509- ) ) ;
510- let agent_metadata_repo: Arc < dyn aionui_db:: IAgentMetadataRepository > = Arc :: new (
511- aionui_db:: SqliteAgentMetadataRepository :: new ( services. database . pool ( ) . clone ( ) ) ,
512- ) ;
513- let acp_session_repo: Arc < dyn aionui_db:: IAcpSessionRepository > = Arc :: new (
514- aionui_db:: SqliteAcpSessionRepository :: new ( services. database . pool ( ) . clone ( ) ) ,
515- ) ;
516- let conversation_svc = Arc :: new (
517- ConversationService :: new (
518- services. work_dir . clone ( ) ,
519- services. event_bus . clone ( ) ,
520- skill_resolver,
521- services. worker_task_manager . clone ( ) ,
522- conv_repo,
523- agent_metadata_repo,
524- acp_session_repo,
525- )
526- . with_runtime_state ( services. conversation_runtime_state . clone ( ) ) ,
527- ) ;
528- conversation_svc. with_mcp_server_repo ( Arc :: new ( aionui_db:: SqliteMcpServerRepository :: new (
529- services. database . pool ( ) . clone ( ) ,
530- ) ) ) ;
531- if let Some ( hook) = services. task_manager_delete_hook . clone ( ) {
532- conversation_svc. with_delete_hook ( hook) ;
533- }
534-
535- let owner_user_id = services
536- . user_repo
537- . get_primary_webui_user ( )
538- . await
539- . ok ( )
540- . flatten ( )
541- . map ( |u| u. id )
542- . unwrap_or_else ( || "system_default_user" . to_string ( ) ) ;
543-
544- let message_service = Arc :: new ( aionui_channel:: message_service:: ChannelMessageService :: new (
545- conversation_svc,
546- services. worker_task_manager . clone ( ) ,
547- Arc :: clone ( & channel_settings) ,
548- owner_user_id,
549- ) ) ;
530+ let message_service = build_channel_message_service ( services, Arc :: clone ( & channel_settings) ) . await ;
550531
551532 let orchestrator = aionui_channel:: orchestrator:: ChannelOrchestrator :: new (
552533 action_executor,
@@ -807,9 +788,189 @@ pub fn build_ws_state(services: &AppServices) -> WsHandlerState {
807788mod tests {
808789 use super :: * ;
809790
791+ use std:: sync:: Arc ;
792+
810793 use crate :: AppConfig ;
794+ use aionui_ai_agent:: types:: { BuildTaskOptions , SendMessageData } ;
795+ use aionui_ai_agent:: {
796+ AgentError , AgentInstance , AgentSendError , AgentStreamEvent , IAgentTask , IMockAgent , IWorkerTaskManager ,
797+ WorkerTaskManagerImpl ,
798+ } ;
799+ use aionui_channel:: types:: PluginType ;
800+ use aionui_common:: { AgentKillReason , AgentType , ConversationStatus , TimestampMs } ;
801+ use aionui_db:: models:: { AssistantSessionRow , UpsertAssistantDefinitionParams } ;
802+ use aionui_db:: {
803+ IAssistantDefinitionRepository , IClientPreferenceRepository , IConversationRepository ,
804+ SqliteAssistantDefinitionRepository , SqliteClientPreferenceRepository , SqliteConversationRepository ,
805+ } ;
811806 use aionui_extension:: { ExtensionSource , ScanPath } ;
812807
808+ struct ChannelStateNoopAgent {
809+ conversation_id : String ,
810+ workspace : String ,
811+ }
812+
813+ #[ async_trait:: async_trait]
814+ impl IAgentTask for ChannelStateNoopAgent {
815+ fn agent_type ( & self ) -> AgentType {
816+ AgentType :: Aionrs
817+ }
818+
819+ fn conversation_id ( & self ) -> & str {
820+ & self . conversation_id
821+ }
822+
823+ fn workspace ( & self ) -> & str {
824+ & self . workspace
825+ }
826+
827+ fn status ( & self ) -> Option < ConversationStatus > {
828+ Some ( ConversationStatus :: Finished )
829+ }
830+
831+ fn last_activity_at ( & self ) -> TimestampMs {
832+ 0
833+ }
834+
835+ fn subscribe ( & self ) -> tokio:: sync:: broadcast:: Receiver < AgentStreamEvent > {
836+ let ( tx, _) = tokio:: sync:: broadcast:: channel ( 1 ) ;
837+ tx. subscribe ( )
838+ }
839+
840+ async fn send_message ( & self , _data : SendMessageData ) -> Result < ( ) , AgentSendError > {
841+ Ok ( ( ) )
842+ }
843+
844+ async fn cancel ( & self ) -> Result < ( ) , AgentError > {
845+ Ok ( ( ) )
846+ }
847+
848+ fn kill ( & self , _reason : Option < AgentKillReason > ) -> Result < ( ) , AgentError > {
849+ Ok ( ( ) )
850+ }
851+ }
852+
853+ impl IMockAgent for ChannelStateNoopAgent { }
854+
855+ fn mock_worker_task_manager ( ) -> Arc < dyn IWorkerTaskManager > {
856+ let factory = Arc :: new ( |opts : BuildTaskOptions | {
857+ Box :: pin ( async move {
858+ Ok ( AgentInstance :: Mock ( Arc :: new ( ChannelStateNoopAgent {
859+ conversation_id : opts. conversation_id ( ) . to_owned ( ) ,
860+ workspace : opts. context . workspace . path ,
861+ } ) ) )
862+ } ) as futures_util:: future:: BoxFuture < ' static , Result < AgentInstance , AgentError > >
863+ } ) ;
864+
865+ Arc :: new ( WorkerTaskManagerImpl :: new ( factory) )
866+ }
867+
868+ fn channel_state_assistant_definition ( ) -> UpsertAssistantDefinitionParams < ' static > {
869+ UpsertAssistantDefinitionParams {
870+ id : "asstdef-channel-state-aionrs" ,
871+ assistant_id : "bare-channel-aionrs" ,
872+ source : "generated" ,
873+ owner_type : "system" ,
874+ source_ref : Some ( "bare-channel-aionrs" ) ,
875+ source_version : None ,
876+ source_hash : None ,
877+ name : "Bare Channel Aionrs" ,
878+ name_i18n : "{}" ,
879+ description : Some ( "Channel state regression assistant" ) ,
880+ description_i18n : "{}" ,
881+ avatar_type : "emoji" ,
882+ avatar_value : Some ( "A" ) ,
883+ agent_id : "632f31d2" ,
884+ rule_resource_type : "inline" ,
885+ rule_resource_ref : None ,
886+ rule_inline_content : Some ( "" ) ,
887+ recommended_prompts : "[]" ,
888+ recommended_prompts_i18n : "{}" ,
889+ default_model_mode : "auto" ,
890+ default_model_value : None ,
891+ default_permission_mode : "auto" ,
892+ default_permission_value : None ,
893+ default_skills_mode : "auto" ,
894+ default_skill_ids : "[]" ,
895+ custom_skill_names : "[]" ,
896+ default_disabled_builtin_skill_ids : "[]" ,
897+ default_mcps_mode : "auto" ,
898+ default_mcp_ids : "[]" ,
899+ }
900+ }
901+
902+ #[ tokio:: test]
903+ async fn build_channel_message_service_uses_app_conversation_service_for_assistant_bindings ( ) {
904+ let db = aionui_db:: init_database_memory ( ) . await . unwrap ( ) ;
905+ let services = AppServices :: from_config ( db, & AppConfig :: default ( ) )
906+ . await
907+ . unwrap ( )
908+ . with_worker_task_manager ( mock_worker_task_manager ( ) ) ;
909+
910+ let pool = services. database . pool ( ) . clone ( ) ;
911+ let definition_repo = SqliteAssistantDefinitionRepository :: new ( pool. clone ( ) ) ;
912+ definition_repo
913+ . upsert ( & channel_state_assistant_definition ( ) )
914+ . await
915+ . unwrap ( ) ;
916+
917+ let pref_repo = SqliteClientPreferenceRepository :: new ( pool. clone ( ) ) ;
918+ pref_repo
919+ . upsert_batch ( & [ (
920+ "assistant.weixin.agent" ,
921+ r#"{"assistant_id":"bare-channel-aionrs","name":"Weixin Aionrs"}"# ,
922+ ) ] )
923+ . await
924+ . unwrap ( ) ;
925+
926+ let settings = build_channel_settings_service ( & services) ;
927+ let message_service = build_channel_message_service ( & services, settings) . await ;
928+ let session = AssistantSessionRow {
929+ id : "session-channel-state" . to_owned ( ) ,
930+ user_id : "channel-user-state" . to_owned ( ) ,
931+ agent_type : "aionrs" . to_owned ( ) ,
932+ conversation_id : None ,
933+ workspace : None ,
934+ chat_id : Some ( "wx-chat-state" . to_owned ( ) ) ,
935+ created_at : 1 ,
936+ last_activity : 1 ,
937+ } ;
938+
939+ let first = message_service
940+ . send_to_agent ( & session, "hello" , PluginType :: Weixin )
941+ . await
942+ . unwrap ( ) ;
943+
944+ let conversation_repo = SqliteConversationRepository :: new ( pool) ;
945+ let snapshot = conversation_repo
946+ . get_assistant_snapshot ( & first. conversation_id )
947+ . await
948+ . unwrap ( )
949+ . expect ( "channel-created conversation should persist assistant snapshot" ) ;
950+ let conversation = conversation_repo
951+ . get ( & first. conversation_id )
952+ . await
953+ . unwrap ( )
954+ . expect ( "channel-created conversation should be persisted" ) ;
955+
956+ assert_eq ! ( snapshot. assistant_id, "bare-channel-aionrs" ) ;
957+ assert_eq ! ( snapshot. agent_id, "632f31d2" ) ;
958+ assert_eq ! ( conversation. r#type, AgentType :: Aionrs . serde_name( ) ) ;
959+ assert_eq ! ( conversation. name, "Weixin Aionrs" ) ;
960+
961+ let second_session = AssistantSessionRow {
962+ conversation_id : Some ( first. conversation_id . clone ( ) ) ,
963+ ..session
964+ } ;
965+ let second = message_service
966+ . send_to_agent ( & second_session, "again" , PluginType :: Weixin )
967+ . await
968+ . unwrap ( ) ;
969+ assert_eq ! ( second. conversation_id, first. conversation_id) ;
970+
971+ services. database . close ( ) . await ;
972+ }
973+
813974 #[ tokio:: test]
814975 async fn build_extension_states_uses_host_app_version_for_engine_filtering ( ) {
815976 let tmp = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
0 commit comments