feat(gateway): 忙时并行帧层(turn_begin + steer_ack.turnId + interrupt.turnId)
新下行帧 turn_begin:编排器每启动一路恰好一帧(单路 kind:"turn" / 并行路 "parallel"),
data{kind, messageIds, leg, activeTurns};角色门 fail-closed 仅 owner+admin,未进 frameReachesAll。
steer_ack 增帧壳 turnId(与该路 turn_begin 同源)—— 并行下没有它配不上正确的 turn.end。
上行 interrupt.turnId(仅 scope:"current" 生效):精确停某一路;目标不存在 ⇒ 定向
{type:"error",data:{code:"bad_request"}},不广播、不动任何会话;省略 turnId = v0.65 原口径(全部在途路)。
帧序按实现:session_busy / pending_drain → message_persisted(user) → turn_begin
(规范草案 §2.9 的时序图需按实现更正:messageIds 必须是已落库的权威 id,故只能在落库之后)。
测试:编排器侧 + 真 WS/JWT 端到端;变异 5 条(每路一帧 / 帧壳 turnId / 目标化打断 / 取消筛选)。
Docs 待升 v0.66(由监工登记)。
This commit is contained in:
1 parent
e5cc9c6054
commit
c0dc435770
13 files changed
+800
-45
No files matched your search
@@ -103,7 +103,7 @@ func (h *hub) streamChat(ctx context.Context, payload any) SubmitResult {
|
||||
func (h *hub) runTurn(ctx context.Context, sessionID string, msgs []model.Message, env loop.SessionEnv) (*loop.Result, error) {
|
||||
// 该路的 turnId 优先取 ctx 上已有的那个(design/15 §2.5.9 的 ⚠️ 必改项):
|
||||
// 编排器在**启动该路之前**就生成了它,并把同一份 id 交给 runTurn —— 因为它
|
||||
// 发出(后续切片的)`turn_begin` 帧时必须报出这个 id,而客户端按 turnId
|
||||
// 发出 `turn_begin` 帧时必须报出这个 id,而客户端按 turnId
|
||||
// 两级分桶。若这里再自己生成一个,两处 id 不同,分桶会立即失配。
|
||||
//
|
||||
// 未挂载(子代理 / 后台思考 / 未接编排器的旧回退路径)时才新铸一个,因此
|
||||
@@ -149,6 +149,14 @@ type interruptRequest struct {
|
||||
// 其余取值一律显式拒绝 —— 客户端以为队列保住了、实际被清空,
|
||||
// 就是静默丢消息,正是 A2 要修掉的那类缺陷。
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// TurnID 是「只停这一路」的目标(忙时并行规范草案 §2.3):**只在
|
||||
// scope:"current" 时生效**;省略 scope 或 scope:"all" 时按契约**忽略**它
|
||||
// ——那两个取值的语义本就是「全部在途路 + 清 pending」。
|
||||
//
|
||||
// 指向不存在的路(已被收尾 / 已被取消 / id 写错)⇒ 定向回
|
||||
// `{type:"error",data:{code:"bad_request"}}`,**绝不**静默降级成「停全部」:
|
||||
// 那会把用户「只停这一路」的意图执行成「停掉所有路」。
|
||||
TurnID string `json:"turnId,omitempty"`
|
||||
}
|
||||
|
||||
// parseInterruptScope 把上行 scope 解析为取值域内的口径。省略、空串与
|
||||
@@ -178,6 +186,10 @@ func parseInterruptScope(raw string) (InterruptScope, error) {
|
||||
// 分发到此(design/11 §7.2 B)。
|
||||
//
|
||||
// 缺 sessionId 时是空操作(既有行为,本次不改)。
|
||||
//
|
||||
// 目标化打断(忙时并行规范草案 §2.3):`scope:"current"` + `turnId` ⇒ 只停那
|
||||
// 一路(InterruptTurn)。其它 scope 下 turnId 按契约**忽略**(它们的语义本就是
|
||||
// 「全部在途路 + 清队列」),这里只留一条 Debug 让「发了但没生效」可被追溯。
|
||||
func (c *wsConn) handleInterrupt(payload json.RawMessage) {
|
||||
if c.h == nil {
|
||||
return
|
||||
@@ -193,9 +205,27 @@ func (c *wsConn) handleInterrupt(payload json.RawMessage) {
|
||||
if c.h.orchestrator == nil || req.SessionID == "" {
|
||||
return
|
||||
}
|
||||
if req.TurnID != "" && scope != InterruptScopeCurrent {
|
||||
c.h.log.Debug("ws interrupt turnId ignored", "session", req.SessionID, "scope", scope)
|
||||
}
|
||||
if req.TurnID != "" && scope == InterruptScopeCurrent {
|
||||
c.interruptOneTurn(req.SessionID, req.TurnID)
|
||||
return
|
||||
}
|
||||
c.h.orchestrator.InterruptScope(req.SessionID, scope)
|
||||
}
|
||||
|
||||
// interruptOneTurn 执行一次「只停这一路」并如实回报结果:目标不存在时定向回
|
||||
// 发送者一条 {type:"error", data:{code:"bad_request"}}(只到发送者、不广播、也
|
||||
// 不动任何会话),**绝不**静默降级成「停掉全部在途路」。
|
||||
func (c *wsConn) interruptOneTurn(sessionID, turnID string) {
|
||||
if c.h.orchestrator.InterruptTurn(sessionID, turnID) {
|
||||
return
|
||||
}
|
||||
c.h.log.Debug("ws interrupt turn not found", "session", sessionID, "turn", turnID)
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"code": wsBadRequestCode}})
|
||||
}
|
||||
|
||||
// broadcastStream 把流式的 model.StreamEvent 映射为其下行 WS 事件类型,
|
||||
// 并经 hub 的帧门扇出(design/11 §7.1):每一个结构化流帧都携带模型
|
||||
// 输出,因此只到达 owner + admin。它跑在 bus 订阅 worker 上;seq 原子
|
||||
|
||||
Reference in new issue
Block a user