@@ -7,8 +7,7 @@ import { randomUUID } from 'crypto';
77import { execFile } from 'child_process' ;
88import { promisify } from 'util' ;
99import { z } from 'zod' ;
10- import { WebSocket , WebSocketServer } from 'ws' ;
11- import { AgentToolService , SkillService , ModelProvider , listThreadIds , isEmptyContent , resizeImageIfNeeded , setMaxImageSize , type StoredMessage , type MessageContent } from "scorpio.ai" ;
10+ import { AgentToolService , SkillService , ModelProvider , listThreadIds , setMaxImageSize , type StoredMessage } from "scorpio.ai" ;
1211import { config , isDev , isValidAgentId } from '../Core/Config' ;
1312import { AgentRunner } from '../Agent/AgentRunner' ;
1413import { ACPAgentPool } from '../Agent/ACPAgentPool' ;
@@ -25,9 +24,10 @@ import { sessionManager } from '../Session/SessionManager';
2524import { schedulerService } from '../Scheduler/SchedulerService' ;
2625import { heartbeatService } from '../Heartbeat/HeartbeatService' ;
2726import { channelManager } from '../Channel/ChannelManager' ;
28- import { WsCommandType , WEB_CHANNEL_ID , WEB_CHANNEL_TYPE } from 'sbot.commons' ;
27+ import { WEB_CHANNEL_ID } from 'sbot.commons' ;
2928import { getModelMeta , getKnownModels } from './modelCatalog' ;
3029import { FsApi } from './FsApi' ;
30+ import { webService } from '../Channel/web/WebService' ;
3131
3232const logger = LoggerService . getLogger ( 'HttpServer.ts' ) ;
3333const execFileAsync = promisify ( execFile ) ;
@@ -317,57 +317,6 @@ function buildPromptTree(dir: string, basePath = '', userBaseDir = ''): PromptNo
317317 return result ;
318318}
319319
320- // ===== 附件处理 =====
321- type AttachmentInput = { name : string ; dataUrl ?: string ; content ?: string } ;
322- type ContentPartInput = { type : 'text' ; text : string } | { type : 'image' ; dataUrl : string } ;
323-
324- function isImageDataUrl ( dataUrl : string ) : boolean {
325- return / ^ d a t a : i m a g e \/ / . test ( dataUrl ) ;
326- }
327-
328- /**
329- * Build MessageContent from ordered parts (interleaved text/image) + file attachments.
330- * Parts preserve the interleaved order from the editor.
331- * File attachments (non-inline) are appended at the end.
332- */
333- async function processMessage ( parts : ContentPartInput [ ] , attachments : AttachmentInput [ ] | undefined , uploadDir : string ) : Promise < MessageContent > {
334- const msgParts : Array < { type : string ; text ?: string ; [ key : string ] : any } > = [ ] ;
335- let hasImage = false ;
336-
337- for ( const p of parts ) {
338- if ( p . type === 'text' ) {
339- msgParts . push ( { type : 'text' , text : p . text } ) ;
340- } else if ( p . type === 'image' && p . dataUrl ) {
341- const url = await resizeImageIfNeeded ( p . dataUrl ) ;
342- msgParts . push ( { type : 'image_url' , image_url : { url } } ) ;
343- hasImage = true ;
344- }
345- }
346-
347- // Append file attachments (non-inline files from the attachment picker)
348- if ( attachments ?. length ) {
349- for ( const att of attachments ) {
350- if ( att . dataUrl && isImageDataUrl ( att . dataUrl ) ) {
351- const url = await resizeImageIfNeeded ( att . dataUrl ) ;
352- msgParts . push ( { type : 'image_url' , image_url : { url } } ) ;
353- hasImage = true ;
354- } else if ( att . dataUrl ) {
355- const filePath = path . join ( uploadDir , `${ randomUUID ( ) } -${ att . name } ` ) ;
356- fs . writeFileSync ( filePath , Buffer . from ( att . dataUrl . replace ( / ^ d a t a : [ ^ ; ] + ; b a s e 6 4 , / , '' ) , 'base64' ) ) ;
357- msgParts . push ( { type : 'text' , text : `[file: ${ att . name } ](${ filePath } )` } ) ;
358- } else if ( att . content != null ) {
359- const filePath = path . join ( uploadDir , `${ randomUUID ( ) } -${ att . name } ` ) ;
360- fs . writeFileSync ( filePath , att . content ) ;
361- msgParts . push ( { type : 'text' , text : `[file: ${ att . name } ](${ filePath } )` } ) ;
362- }
363- }
364- }
365-
366- if ( msgParts . length === 0 ) return '' ;
367- if ( ! hasImage ) return msgParts . map ( p => p . text ! ) . join ( '\n' ) ;
368- return msgParts ;
369- }
370-
371320// ===== Skills 辅助函数 =====
372321function listSkills ( skillsDir : string ) {
373322 if ( ! fs . existsSync ( skillsDir ) ) return [ ] ;
@@ -422,23 +371,14 @@ function api(fn: (req: Request, res: Response) => any) {
422371class HttpServer {
423372 private readonly skillHubService = new SkillHubService ( ) ;
424373 private readonly agentStoreService = new AgentStoreService ( ) ;
425- private readonly wsClients = new Set < WebSocket > ( ) ;
426374 private server ?: http . Server ;
427375
428- broadcastToWs ( data : string ) : void {
429- for ( const ws of this . wsClients ) {
430- if ( ws . readyState === WebSocket . OPEN ) ws . send ( data ) ;
431- }
432- }
433-
434376 async shutdown ( ) : Promise < void > {
435377 logger . info ( 'Shutting down services...' ) ;
436378 try {
437379 schedulerService . stopAll ( ) ;
438380 await ACPAgentPool . getInstance ( ) . disposeAll ( ) ;
439381 await channelManager . dispose ( ) ;
440- for ( const ws of this . wsClients ) ws . close ( ) ;
441- this . wsClients . clear ( ) ;
442382 if ( this . server ) {
443383 await new Promise < void > ( ( resolve , reject ) =>
444384 this . server ! . close ( err => err ? reject ( err ) : resolve ( ) ) ,
@@ -499,45 +439,11 @@ class HttpServer {
499439 this . registerUserRoutes ( app ) ;
500440 this . registerChatRoutes ( app ) ;
501441
502- // HTTP + WebSocket 服务
442+ // HTTP + WebSocket 服务:把 ws 升级路径与 web channel 运行时交给 WebService,
443+ // 然后注册到 channelManager,让消息出路与 dispose 生命周期与其他 channel 对齐
503444 const server = this . server = http . createServer ( app ) ;
504-
505- const wss = new WebSocketServer ( { server, path : '/ws/chat' } ) ;
506- wss . on ( 'connection' , ( ws ) => {
507- this . wsClients . add ( ws ) ;
508- ws . on ( 'close' , ( ) => { this . wsClients . delete ( ws ) ; } ) ;
509- ws . on ( 'message' , async ( data ) => {
510- try {
511- const msg = JSON . parse ( data . toString ( ) ) as { type ?: string ; [ key : string ] : any } ;
512- const sid = msg . sessionId as string | undefined ;
513- if ( ! sid ) throw new Error ( 'sessionId is required' ) ;
514- // 与 channel plugin 路径对齐:先 ensure session+profile,再交给 sessionManager
515- const { session, profile } = await ensureChannelSession ( WEB_CHANNEL_ID , sid ) ;
516- const threadId = String ( profile . id ) ;
517- switch ( msg . type ) {
518- case WsCommandType . Query : {
519- const enriched = await processMessage ( msg . parts ?? [ ] , msg . attachments , uploadDir ) ;
520- if ( isEmptyContent ( enriched ) ) break ;
521- sessionManager . onReceiveChannelMessage ( threadId , enriched , {
522- channelType : WEB_CHANNEL_TYPE ,
523- channelId : WEB_CHANNEL_ID ,
524- dbSessionId : session . id ,
525- sessionId : sid ,
526- } ) ;
527- break ;
528- }
529- case WsCommandType . Approval :
530- case WsCommandType . Ask :
531- case WsCommandType . Abort : {
532- sessionManager . onTriggerChannelAction ( threadId , msg . type ! , msg ) . catch ( e => logger . error ( `ws trigger error: ${ e ?. message ?? e } ` ) ) ;
533- break ;
534- }
535- }
536- } catch ( e : any ) {
537- logger . error ( `ws message error: ${ e ?. message ?? e } ` ) ;
538- }
539- } ) ;
540- } ) ;
445+ webService . attach ( server , uploadDir ) ;
446+ channelManager . registerService ( WEB_CHANNEL_ID , webService ) ;
541447
542448 server . listen ( port , ( ) => {
543449 logger . info ( `HTTP server started, admin UI available at: http://127.0.0.1:${ port } ` ) ;
0 commit comments