Skip to content

Commit 251ccd1

Browse files
committed
fix(feishuadapter): cleanup stale sent approval cards
1 parent 542951c commit 251ccd1

2 files changed

Lines changed: 118 additions & 9 deletions

File tree

internal/feishuadapter/adapter.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1139,14 +1139,20 @@ func (a *Adapter) executeApprovalOutbox(ctx context.Context, ops []approvalOutbo
11391139
var cardID string
11401140
cardID, err = a.messenger.SendPermissionCard(ctx, op.ChatID, op.PendingCard)
11411141
if err == nil && strings.TrimSpace(cardID) != "" {
1142+
normalizedCardID := strings.TrimSpace(cardID)
1143+
shouldAttach := false
11421144
a.mu.Lock()
11431145
fsm := a.approvalFSMByRun[op.RunKey]
11441146
if fsm != nil && fsm.Generation == op.Generation && fsm.Version == op.Version {
1145-
fsm.CardID = strings.TrimSpace(cardID)
1147+
shouldAttach = true
1148+
fsm.CardID = normalizedCardID
11461149
a.approvalCardRunIndex[fsm.CardID] = op.RunKey
11471150
a.rememberRunPermissionCardLocked(op.RunKey, fsm.CardID)
11481151
}
11491152
a.mu.Unlock()
1153+
if !shouldAttach {
1154+
a.cleanupStalePermissionCard(op, normalizedCardID)
1155+
}
11501156
}
11511157
case approvalOutboxUpdatePendingCard:
11521158
if strings.TrimSpace(op.CardID) == "" {
@@ -1168,6 +1174,38 @@ func (a *Adapter) executeApprovalOutbox(ctx context.Context, ops []approvalOutbo
11681174
}
11691175
}
11701176

1177+
// cleanupStalePermissionCard 在发送审批卡后若发现版本已过期,则立即回收该游离卡片。
1178+
func (a *Adapter) cleanupStalePermissionCard(op approvalOutboxOperation, cardID string) {
1179+
normalizedCardID := strings.TrimSpace(cardID)
1180+
if normalizedCardID == "" {
1181+
return
1182+
}
1183+
timeout := a.cfg.RequestTimeout
1184+
if timeout <= 0 {
1185+
timeout = 3 * time.Second
1186+
}
1187+
callCtx, cancel := context.WithTimeout(context.Background(), timeout)
1188+
defer cancel()
1189+
if err := a.messenger.DeleteMessage(callCtx, normalizedCardID); err != nil {
1190+
a.safeLog(
1191+
"cleanup stale approval card failed kind=%s run_key=%s request_id=%s card_id=%s err=%v",
1192+
op.Kind,
1193+
op.RunKey,
1194+
op.RequestID,
1195+
normalizedCardID,
1196+
err,
1197+
)
1198+
return
1199+
}
1200+
a.safeLog(
1201+
"cleanup stale approval card success kind=%s run_key=%s request_id=%s card_id=%s",
1202+
op.Kind,
1203+
op.RunKey,
1204+
op.RequestID,
1205+
normalizedCardID,
1206+
)
1207+
}
1208+
11711209
// shouldExecuteApprovalOutbox 在发送副作用前进行代际/版本栅栏校验,避免旧 outbox 覆写新状态。
11721210
func (a *Adapter) shouldExecuteApprovalOutbox(op approvalOutboxOperation) bool {
11731211
currentGeneration, currentVersion, ok := a.snapshotApprovalFSMVersion(op.RunKey)

internal/feishuadapter/adapter_test.go

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,14 @@ type sentMessage struct {
137137
}
138138

139139
type fakeMessenger struct {
140-
mu sync.Mutex
141-
messages []sentMessage
142-
nextID int
143-
sendTextErr error
144-
sendCardErr error
145-
updateCardErr error
146-
deleteCardErr error
140+
mu sync.Mutex
141+
messages []sentMessage
142+
nextID int
143+
sendTextErr error
144+
sendCardErr error
145+
updateCardErr error
146+
deleteCardErr error
147+
sendPermissionCardHook func(cardID string, payload PermissionCardPayload)
147148
}
148149

149150
func (m *fakeMessenger) SendText(_ context.Context, chatID string, text string) error {
@@ -155,10 +156,14 @@ func (m *fakeMessenger) SendText(_ context.Context, chatID string, text string)
155156

156157
func (m *fakeMessenger) SendPermissionCard(_ context.Context, chatID string, payload PermissionCardPayload) (string, error) {
157158
m.mu.Lock()
158-
defer m.mu.Unlock()
159159
m.nextID++
160160
cardID := fmt.Sprintf("perm-card-%d", m.nextID)
161161
m.messages = append(m.messages, sentMessage{chatID: chatID, kind: "card", card: payload, cardID: cardID})
162+
hook := m.sendPermissionCardHook
163+
m.mu.Unlock()
164+
if hook != nil {
165+
hook(cardID, payload)
166+
}
162167
return cardID, nil
163168
}
164169

@@ -2311,6 +2316,72 @@ func TestApprovalOutboxPreflightDropsStaleOperation(t *testing.T) {
23112316
}
23122317
}
23132318

2319+
func TestApprovalOutboxSendCardCleanupWhenVersionAdvancedDuringSend(t *testing.T) {
2320+
adapter := newTestAdapter(t)
2321+
sessionID := "session-outbox-race-send"
2322+
runID := "run-outbox-race-send"
2323+
runKey := runBindingKey(sessionID, runID)
2324+
adapter.trackSession(sessionID, runID, "chat-outbox-race-send", "outbox race send task")
2325+
2326+
var createdCardID string
2327+
var hookOnce sync.Once
2328+
messenger := adapterTestMessenger(adapter)
2329+
messenger.mu.Lock()
2330+
messenger.sendPermissionCardHook = func(cardID string, _ PermissionCardPayload) {
2331+
createdCardID = strings.TrimSpace(cardID)
2332+
hookOnce.Do(func() {
2333+
adapter.mu.Lock()
2334+
if fsm := adapter.approvalFSMByRun[runKey]; fsm != nil {
2335+
fsm.Version++
2336+
}
2337+
adapter.mu.Unlock()
2338+
})
2339+
}
2340+
messenger.mu.Unlock()
2341+
2342+
adapter.processPermissionRequested(
2343+
context.Background(),
2344+
sessionID,
2345+
runID,
2346+
"chat-outbox-race-send",
2347+
"perm-outbox-race-send",
2348+
"filesystem_write_file",
2349+
"write_file",
2350+
"outbox-race-send.txt",
2351+
"需要审批",
2352+
)
2353+
if strings.TrimSpace(createdCardID) == "" {
2354+
t.Fatal("expected permission card to be sent")
2355+
}
2356+
2357+
msgs := adapterTestMessenger(adapter).snapshot()
2358+
foundDelete := false
2359+
for _, message := range msgs {
2360+
if message.kind == "delete_card" && message.chatID == createdCardID {
2361+
foundDelete = true
2362+
break
2363+
}
2364+
}
2365+
if !foundDelete {
2366+
t.Fatalf("expected stale sent permission card to be deleted, msgs=%#v", msgs)
2367+
}
2368+
2369+
adapter.mu.RLock()
2370+
fsm := adapter.approvalFSMByRun[runKey]
2371+
storedCardID := ""
2372+
if fsm != nil {
2373+
storedCardID = strings.TrimSpace(fsm.CardID)
2374+
}
2375+
_, indexed := adapter.approvalCardRunIndex[createdCardID]
2376+
adapter.mu.RUnlock()
2377+
if storedCardID != "" {
2378+
t.Fatalf("stale card should not be attached to fsm, got %q", storedCardID)
2379+
}
2380+
if indexed {
2381+
t.Fatalf("stale card should not remain in approvalCardRunIndex: %s", createdCardID)
2382+
}
2383+
}
2384+
23142385
func TestTryHandleTextPermissionHandlesAskUserAnswerAndSkip(t *testing.T) {
23152386
adapter := newTestAdapter(t)
23162387
sessionID := BuildSessionID("chat-ask-text-cmd")

0 commit comments

Comments
 (0)