Files
Cyrene_v2/internal/services/gateway/session_interrupt.go
T
AskaEth 5494c31474
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway): interrupt 支持 scope(当前/全部),默认维持现状(协同 A2)
- 原状:interrupt{sessionId} 取消在飞回合**并清空 pending** ⇒ 客户端「打断模式」连发会被静默丢弃
- 修法:新增 scope 三态(与项目既有「省略=保留现状」口径一致)
  · 省略 或 "all" = 现状语义(取消当前 + 清 pending)
  · "current" = **只取消当前回合、保留 pending**(pending 继续按编排器规则续跑)
  · 非法值 → **定向回错误**(只回发送者,不广播),显式报错而非静默当默认
- 下行 session_interrupted 的 dropped 如实反映**真的被丢弃的条数**(scope=current 时通常为 0)
- 复核:gateway 包 build/vet/测试绿(新增 interrupt_scope_* 两组用例);后端 :10942 全程未停
2026-10-04 14:21:52 +08:00

95 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
// 本文件归属协同项 A2 的打断口径:`interrupt {sessionId, scope}` 决定一次
// 用户打断清到哪一层。它从 session_orchestrator.go 拆出,使编排器本体只留
// 消息状态机(Submit/pending/续跑),打断的选择与语义在这里回答 —— 也避免
// 编排器主文件越过「单文件 ≤500 行」的硬约束。
//
// 上游清单:`Cyrene-For-Android/devdocs/02-前后端协同事项.md`(A2.scope)。
// 客户端有「追加队列」与「打断」两种模式,若打断一律连队列一起清,用户选
// 「打断」时连发的多条消息会被静默丢弃,只回一个 dropped 数字。
// InterruptScope 选择一次 interrupt 清到哪一层(协同项 A2)。
type InterruptScope string
const (
// InterruptScopeAll 取消在飞回合并清空待处理队列。它是省略 scope 时的
// 默认口径,与历史行为逐字一致。
InterruptScopeAll InterruptScope = "all"
// InterruptScopeCurrent 只取消在飞回合,**保留**待处理队列:队列里的消息
// 按编排器正常的续跑规则继续执行,一条都不丢。
InterruptScopeCurrent InterruptScope = "current"
)
// Interrupt 取消在飞回合、清空待处理队列并把会话回到空闲(design/15 §3,
// 「停下/算了/换个话题」路径)。它是忙碌会话不必排空队列就能复位的唯一方式;
// 运行中 goroutine 的完成变成陈旧代次,不续跑任何东西。它返回被丢弃的
// 待处理回合数。
//
// 它把会话立即报成空闲并立即接受下一条消息(design/15 §3 的「立即
// 处理新消息」),但被取消的回合仍要收尾:下一个回合的启动被 session_turn_fence.go
// 的围栏挡在那次收尾之后,因此它绝不会读到被放弃回合那份仍可恢复的旧快照。
func (o *Orchestrator) Interrupt(sessionID string) int {
return o.InterruptScope(sessionID, InterruptScopeAll)
}
// InterruptScope 按 scope 中断一个会话(协同项 A2),返回真的被丢弃的
// 待处理回合数:
//
// - InterruptScopeAll:取消在飞回合、清空待处理队列并把会话回到空闲
// —— 省略 scope 时的历史语义,逐字不变;
// - InterruptScopeCurrent:只取消在飞回合,**保留**待处理队列。它不递增
// 代次,因此被取消回合的结算(completeTurn)仍属当代,会按正常续跑
// 规则弹出队列里的下一条消息、带上会话尾部上下文继续跑(design/15
// §2.2),返回 0 —— 真的没有消息被丢弃。
//
// 空字符串与省略同义(都是现状语义),使老客户端只发 {sessionId} 的
// 行为完全不变。
func (o *Orchestrator) InterruptScope(sessionID string, scope InterruptScope) int {
if scope == "" {
scope = InterruptScopeAll
}
sessionID = o.normalize(sessionID)
o.mu.Lock()
defer o.mu.Unlock()
return o.interruptLocked(sessionID, o.states[sessionID], scope)
}
// InterruptAll 取消每一个在飞会话并清空所有待处理队列。它在网关关闭时使用,
// 使没有后台回合比 hub 活得更久。它返回被丢弃的待处理回合总数。
func (o *Orchestrator) InterruptAll() int {
o.mu.Lock()
total := 0
for id, st := range o.states {
total += o.interruptLocked(id, st, InterruptScopeAll)
}
o.mu.Unlock()
return total
}
// interruptLocked 在状态锁下执行一次中断。scope 决定待处理队列的去留
// (见 InterruptScope):All 清队列、递增代次并把会话回到空闲;Current
// 只取消在飞回合,队列与代次原样留下,使被取消回合的结算仍会续跑队列。
// 它返回真的被丢弃的待处理回合数(Current 恒为 0)。
func (o *Orchestrator) interruptLocked(sessionID string, st *sessionState, scope InterruptScope) int {
if st == nil || st.mode == ModeIdle {
return 0
}
if st.cancel != nil {
st.cancel()
}
if scope == InterruptScopeCurrent {
// 只取消在途回合:不碰 pending、不递增代次。mode 如实报告为
// 当时的忙碌通道(回合仍在收尾、队列还在续跑),而不是 idle。
o.emitState(sessionID, EventSessionInterrupt, map[string]any{"sessionId": sessionID, "mode": st.mode, "dropped": 0})
return 0
}
dropped := len(st.pending)
st.pending = nil
st.gen++
st.mode = ModeIdle
st.cancel = nil
o.emitState(sessionID, EventSessionInterrupt, map[string]any{"sessionId": sessionID, "mode": ModeIdle, "dropped": dropped})
return dropped
}