@@ -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 ,
@@ -278,47 +290,53 @@ func resolveImagePath(workdir string, path string) (string, error) {
278290 return resolved , nil
279291}
280292
293+ // sessionWorkdirUpdate 描述已有会话 workdir 的待提交变更,确保 Prepare 成功后再落盘。
294+ type sessionWorkdirUpdate struct {
295+ session Session
296+ dirty bool
297+ }
298+
281299func (p * InputPreparer ) loadOrCreateSession (
282300 ctx context.Context ,
283301 sessionID string ,
284302 title string ,
285303 defaultWorkdir string ,
286304 requestedWorkdir string ,
287- ) (Session , bool , error ) {
305+ ) (Session , bool , sessionWorkdirUpdate , error ) {
288306 if strings .TrimSpace (sessionID ) == "" {
289307 sessionWorkdir , err := resolveWorkdirForInput (defaultWorkdir , "" , requestedWorkdir )
290308 if err != nil {
291- return Session {}, false , err
309+ return Session {}, false , sessionWorkdirUpdate {}, err
292310 }
293311 session := NewWithWorkdir (title , sessionWorkdir )
294312 if err := p .store .Save (ctx , & session ); err != nil {
295- return Session {}, false , err
313+ return Session {}, false , sessionWorkdirUpdate {}, err
296314 }
297- return session , true , nil
315+ return session , true , sessionWorkdirUpdate {}, nil
298316 }
299317
300318 session , err := p .store .Load (ctx , sessionID )
301319 if err != nil {
302- return Session {}, false , err
320+ return Session {}, false , sessionWorkdirUpdate {}, err
303321 }
304322 if strings .TrimSpace (requestedWorkdir ) == "" && strings .TrimSpace (session .Workdir ) != "" {
305- return session , false , nil
323+ return session , false , sessionWorkdirUpdate {}, nil
306324 }
307325
308326 resolved , err := resolveWorkdirForInput (defaultWorkdir , session .Workdir , requestedWorkdir )
309327 if err != nil {
310- return Session {}, false , err
328+ return Session {}, false , sessionWorkdirUpdate {}, err
311329 }
312330 if session .Workdir == resolved {
313- return session , false , nil
331+ return session , false , sessionWorkdirUpdate {}, nil
314332 }
315333
316334 session .Workdir = resolved
317335 session .UpdatedAt = time .Now ()
318- if err := p . store . Save ( ctx , & session ); err != nil {
319- return Session {}, false , err
320- }
321- return session , false , nil
336+ return session , false , sessionWorkdirUpdate {
337+ session : session ,
338+ dirty : true ,
339+ } , nil
322340}
323341
324342// rollbackCreatedSession 在本次 Prepare 新建会话后发生错误时回滚会话目录,避免残留孤儿会话。
@@ -332,6 +350,34 @@ func (p *InputPreparer) rollbackCreatedSession(ctx context.Context, sessionID st
332350 _ = p .store .DeleteSession (ctx , sessionID )
333351}
334352
353+ // persistSessionWorkdirUpdate 在 Prepare 其余步骤完成后统一提交会话 workdir 更新,避免失败时出现部分提交。
354+ func (p * InputPreparer ) persistSessionWorkdirUpdate (ctx context.Context , pending sessionWorkdirUpdate ) error {
355+ if ! pending .dirty {
356+ return nil
357+ }
358+ if err := p .store .Save (ctx , & pending .session ); err != nil {
359+ return err
360+ }
361+ return nil
362+ }
363+
364+ // cleanupSavedAssets 在 Prepare 失败时尽力回收已落盘的附件,减少 existing session 残留垃圾文件。
365+ func (p * InputPreparer ) cleanupSavedAssets (ctx context.Context , sessionID string , assets []AssetMeta ) {
366+ if len (assets ) == 0 || ctx .Err () != nil {
367+ return
368+ }
369+ cleanupStore , ok := p .assetStore .(assetCleanupStore )
370+ if ! ok {
371+ return
372+ }
373+ for _ , asset := range assets {
374+ if strings .TrimSpace (asset .ID ) == "" {
375+ continue
376+ }
377+ _ = cleanupStore .DeleteAsset (ctx , sessionID , asset .ID )
378+ }
379+ }
380+
335381func resolveWorkdirForInput (defaultWorkdir string , currentWorkdir string , requestedWorkdir string ) (string , error ) {
336382 base := EffectiveWorkdir (currentWorkdir , defaultWorkdir )
337383 if strings .TrimSpace (requestedWorkdir ) == "" {
0 commit comments