@@ -697,6 +697,66 @@ func (b *gatewayRuntimePortBridge) CreateSession(ctx context.Context, input gate
697697 return strings .TrimSpace (session .ID ), nil
698698}
699699
700+ // SaveSessionAsset 将浏览器上传的附件保存到当前工作区的 session asset store。
701+ func (b * gatewayRuntimePortBridge ) SaveSessionAsset (
702+ ctx context.Context ,
703+ input gateway.SaveSessionAssetInput ,
704+ ) (gateway.SessionAssetMeta , error ) {
705+ if err := b .ensureRuntimeAccess (input .SubjectID ); err != nil {
706+ return gateway.SessionAssetMeta {}, err
707+ }
708+ sessionID := strings .TrimSpace (input .SessionID )
709+ if sessionID == "" {
710+ return gateway.SessionAssetMeta {}, gateway .ErrRuntimeResourceNotFound
711+ }
712+ assetStore , ok := b .sessionStore .(agentsession.AssetStore )
713+ if ! ok || assetStore == nil {
714+ return gateway.SessionAssetMeta {}, fmt .Errorf ("gateway runtime bridge: session asset store is unavailable" )
715+ }
716+ meta , err := assetStore .SaveAsset (ctx , sessionID , input .Reader , strings .TrimSpace (input .MimeType ))
717+ if err != nil {
718+ return gateway.SessionAssetMeta {}, err
719+ }
720+ return gateway.SessionAssetMeta {
721+ SessionID : sessionID ,
722+ AssetID : strings .TrimSpace (meta .ID ),
723+ MimeType : strings .TrimSpace (meta .MimeType ),
724+ Size : meta .Size ,
725+ }, nil
726+ }
727+
728+ // OpenSessionAsset 打开当前工作区的会话附件,供 Gateway HTTP 读取端点流式返回。
729+ func (b * gatewayRuntimePortBridge ) OpenSessionAsset (
730+ ctx context.Context ,
731+ input gateway.OpenSessionAssetInput ,
732+ ) (gateway.OpenSessionAssetResult , error ) {
733+ if err := b .ensureRuntimeAccess (input .SubjectID ); err != nil {
734+ return gateway.OpenSessionAssetResult {}, err
735+ }
736+ sessionID := strings .TrimSpace (input .SessionID )
737+ assetID := strings .TrimSpace (input .AssetID )
738+ if sessionID == "" || assetID == "" {
739+ return gateway.OpenSessionAssetResult {}, gateway .ErrRuntimeResourceNotFound
740+ }
741+ assetStore , ok := b .sessionStore .(agentsession.AssetStore )
742+ if ! ok || assetStore == nil {
743+ return gateway.OpenSessionAssetResult {}, fmt .Errorf ("gateway runtime bridge: session asset store is unavailable" )
744+ }
745+ reader , meta , err := assetStore .Open (ctx , sessionID , assetID )
746+ if err != nil {
747+ return gateway.OpenSessionAssetResult {}, err
748+ }
749+ return gateway.OpenSessionAssetResult {
750+ Reader : reader ,
751+ Meta : gateway.SessionAssetMeta {
752+ SessionID : sessionID ,
753+ AssetID : strings .TrimSpace (meta .ID ),
754+ MimeType : strings .TrimSpace (meta .MimeType ),
755+ Size : meta .Size ,
756+ },
757+ }, nil
758+ }
759+
700760// DeleteSession 删除/归档指定会话。
701761func (b * gatewayRuntimePortBridge ) DeleteSession (ctx context.Context , input gateway.DeleteSessionInput ) (bool , error ) {
702762 if err := b .ensureRuntimeAccess (input .SubjectID ); err != nil {
@@ -1684,11 +1744,13 @@ func convertGatewayRunInput(input gateway.RunInput) agentruntime.PrepareInput {
16841744 continue
16851745 }
16861746 path := strings .TrimSpace (part .Media .URI )
1687- if path == "" {
1747+ assetID := strings .TrimSpace (part .Media .AssetID )
1748+ if path == "" && assetID == "" {
16881749 continue
16891750 }
16901751 images = append (images , agentruntime.UserImageInput {
16911752 Path : path ,
1753+ AssetID : assetID ,
16921754 MimeType : strings .TrimSpace (part .Media .MimeType ),
16931755 })
16941756 }
@@ -1867,6 +1929,7 @@ func convertSessionMessages(messages []providertypes.Message) []gateway.SessionM
18671929 convertedMessage := gateway.SessionMessage {
18681930 Role : strings .TrimSpace (message .Role ),
18691931 Content : renderSessionMessageContent (message .Parts ),
1932+ Parts : convertProviderContentParts (message .Parts ),
18701933 ToolCallID : strings .TrimSpace (message .ToolCallID ),
18711934 IsError : message .IsError ,
18721935 }
@@ -1885,6 +1948,52 @@ func convertSessionMessages(messages []providertypes.Message) []gateway.SessionM
18851948 return converted
18861949}
18871950
1951+ // convertProviderContentParts 将 provider 通用内容分片转换为 Gateway 会话快照分片。
1952+ func convertProviderContentParts (parts []providertypes.ContentPart ) []gateway.InputPart {
1953+ if len (parts ) == 0 {
1954+ return nil
1955+ }
1956+ converted := make ([]gateway.InputPart , 0 , len (parts ))
1957+ for _ , part := range parts {
1958+ switch part .Kind {
1959+ case providertypes .ContentPartText :
1960+ if text := strings .TrimSpace (part .Text ); text != "" {
1961+ converted = append (converted , gateway.InputPart {
1962+ Type : gateway .InputPartTypeText ,
1963+ Text : text ,
1964+ })
1965+ }
1966+ case providertypes .ContentPartImage :
1967+ if part .Image == nil {
1968+ continue
1969+ }
1970+ switch part .Image .SourceType {
1971+ case providertypes .ImageSourceSessionAsset :
1972+ if part .Image .Asset == nil || strings .TrimSpace (part .Image .Asset .ID ) == "" {
1973+ continue
1974+ }
1975+ converted = append (converted , gateway.InputPart {
1976+ Type : gateway .InputPartTypeImage ,
1977+ Media : & gateway.Media {
1978+ AssetID : strings .TrimSpace (part .Image .Asset .ID ),
1979+ MimeType : strings .TrimSpace (part .Image .Asset .MimeType ),
1980+ },
1981+ })
1982+ case providertypes .ImageSourceRemote :
1983+ if url := strings .TrimSpace (part .Image .URL ); url != "" {
1984+ converted = append (converted , gateway.InputPart {
1985+ Type : gateway .InputPartTypeImage ,
1986+ Media : & gateway.Media {
1987+ URI : url ,
1988+ },
1989+ })
1990+ }
1991+ }
1992+ }
1993+ }
1994+ return converted
1995+ }
1996+
18881997// convertRuntimePlanTodoItem 将 session 计划中的 legacy todo 项映射为 gateway 展示结构。
18891998func convertRuntimePlanTodoItem (item agentsession.TodoItem ) gateway.PlanTodoItem {
18901999 required := false
0 commit comments