@@ -70,6 +70,10 @@ type InputPreparer struct {
7070 assetStore AssetStore
7171}
7272
73+ type assetCleanupStore interface {
74+ DeleteAsset (ctx context.Context , sessionID string , assetID string ) error
75+ }
76+
7377// NewInputPreparer 创建会话输入归一化组件。
7478func NewInputPreparer (store Store , assetStore AssetStore ) * InputPreparer {
7579 return & InputPreparer {
@@ -96,7 +100,7 @@ func (p *InputPreparer) Prepare(ctx context.Context, input PrepareInput) (Prepar
96100 }
97101
98102 sessionTitle := buildSessionTitle (trimmedText , len (input .Images ) > 0 )
99- session , sessionCreated , err := p .loadOrCreateSession (
103+ session , sessionCreated , pendingUpdate , err := p .loadOrCreateSession (
100104 ctx ,
101105 input .SessionID ,
102106 sessionTitle ,
@@ -117,6 +121,7 @@ func (p *InputPreparer) Prepare(ctx context.Context, input PrepareInput) (Prepar
117121 path := strings .TrimSpace (image .Path )
118122 if path == "" {
119123 p .rollbackCreatedSession (ctx , session .ID , sessionCreated )
124+ p .cleanupSavedAssets (ctx , session .ID , savedAssets )
120125 return PreparedInput {}, & AssetSaveError {
121126 SessionID : session .ID ,
122127 Index : index ,
@@ -129,6 +134,7 @@ func (p *InputPreparer) Prepare(ctx context.Context, input PrepareInput) (Prepar
129134 meta , err := p .saveImageAsset (ctx , session .ID , session .Workdir , path , mimeType )
130135 if err != nil {
131136 p .rollbackCreatedSession (ctx , session .ID , sessionCreated )
137+ p .cleanupSavedAssets (ctx , session .ID , savedAssets )
132138 return PreparedInput {}, & AssetSaveError {
133139 SessionID : session .ID ,
134140 Index : index ,
@@ -142,8 +148,14 @@ func (p *InputPreparer) Prepare(ctx context.Context, input PrepareInput) (Prepar
142148
143149 if err := providertypes .ValidateParts (parts ); err != nil {
144150 p .rollbackCreatedSession (ctx , session .ID , sessionCreated )
151+ p .cleanupSavedAssets (ctx , session .ID , savedAssets )
145152 return PreparedInput {}, fmt .Errorf ("session: normalize parts: %w" , err )
146153 }
154+ if err := p .persistSessionWorkdirUpdate (ctx , pendingUpdate ); err != nil {
155+ p .rollbackCreatedSession (ctx , session .ID , sessionCreated )
156+ p .cleanupSavedAssets (ctx , session .ID , savedAssets )
157+ return PreparedInput {}, err
158+ }
147159
148160 return PreparedInput {
149161 SessionID : session .ID ,
@@ -302,47 +314,53 @@ func resolveImagePath(workdir string, path string) (string, error) {
302314 return resolved , nil
303315}
304316
317+ // sessionWorkdirUpdate 描述已有会话 workdir 的待提交变更,确保 Prepare 成功后再落盘。
318+ type sessionWorkdirUpdate struct {
319+ session Session
320+ dirty bool
321+ }
322+
305323func (p * InputPreparer ) loadOrCreateSession (
306324 ctx context.Context ,
307325 sessionID string ,
308326 title string ,
309327 defaultWorkdir string ,
310328 requestedWorkdir string ,
311- ) (Session , bool , error ) {
329+ ) (Session , bool , sessionWorkdirUpdate , error ) {
312330 if strings .TrimSpace (sessionID ) == "" {
313331 sessionWorkdir , err := resolveWorkdirForInput (defaultWorkdir , "" , requestedWorkdir )
314332 if err != nil {
315- return Session {}, false , err
333+ return Session {}, false , sessionWorkdirUpdate {}, err
316334 }
317335 session := NewWithWorkdir (title , sessionWorkdir )
318336 if err := p .store .Save (ctx , & session ); err != nil {
319- return Session {}, false , err
337+ return Session {}, false , sessionWorkdirUpdate {}, err
320338 }
321- return session , true , nil
339+ return session , true , sessionWorkdirUpdate {}, nil
322340 }
323341
324342 session , err := p .store .Load (ctx , sessionID )
325343 if err != nil {
326- return Session {}, false , err
344+ return Session {}, false , sessionWorkdirUpdate {}, err
327345 }
328346 if strings .TrimSpace (requestedWorkdir ) == "" && strings .TrimSpace (session .Workdir ) != "" {
329- return session , false , nil
347+ return session , false , sessionWorkdirUpdate {}, nil
330348 }
331349
332350 resolved , err := resolveWorkdirForInput (defaultWorkdir , session .Workdir , requestedWorkdir )
333351 if err != nil {
334- return Session {}, false , err
352+ return Session {}, false , sessionWorkdirUpdate {}, err
335353 }
336354 if session .Workdir == resolved {
337- return session , false , nil
355+ return session , false , sessionWorkdirUpdate {}, nil
338356 }
339357
340358 session .Workdir = resolved
341359 session .UpdatedAt = time .Now ()
342- if err := p . store . Save ( ctx , & session ); err != nil {
343- return Session {}, false , err
344- }
345- return session , false , nil
360+ return session , false , sessionWorkdirUpdate {
361+ session : session ,
362+ dirty : true ,
363+ } , nil
346364}
347365
348366// rollbackCreatedSession 在本次 Prepare 新建会话后发生错误时回滚会话目录,避免残留孤儿会话。
@@ -356,6 +374,34 @@ func (p *InputPreparer) rollbackCreatedSession(ctx context.Context, sessionID st
356374 _ = p .store .DeleteSession (ctx , sessionID )
357375}
358376
377+ // persistSessionWorkdirUpdate 在 Prepare 其余步骤完成后统一提交会话 workdir 更新,避免失败时出现部分提交。
378+ func (p * InputPreparer ) persistSessionWorkdirUpdate (ctx context.Context , pending sessionWorkdirUpdate ) error {
379+ if ! pending .dirty {
380+ return nil
381+ }
382+ if err := p .store .Save (ctx , & pending .session ); err != nil {
383+ return err
384+ }
385+ return nil
386+ }
387+
388+ // cleanupSavedAssets 在 Prepare 失败时尽力回收已落盘的附件,减少 existing session 残留垃圾文件。
389+ func (p * InputPreparer ) cleanupSavedAssets (ctx context.Context , sessionID string , assets []AssetMeta ) {
390+ if len (assets ) == 0 || ctx .Err () != nil {
391+ return
392+ }
393+ cleanupStore , ok := p .assetStore .(assetCleanupStore )
394+ if ! ok {
395+ return
396+ }
397+ for _ , asset := range assets {
398+ if strings .TrimSpace (asset .ID ) == "" {
399+ continue
400+ }
401+ _ = cleanupStore .DeleteAsset (ctx , sessionID , asset .ID )
402+ }
403+ }
404+
359405func resolveWorkdirForInput (defaultWorkdir string , currentWorkdir string , requestedWorkdir string ) (string , error ) {
360406 base := EffectiveWorkdir (currentWorkdir , defaultWorkdir )
361407 if strings .TrimSpace (requestedWorkdir ) == "" {
0 commit comments