package server import ( "errors" "net/http" "agent-system-backend/internal/cmdbus" "agent-system-backend/internal/store" ) // commandOperationQuery 把"已记录 operation 的状态查询"落在既有的 owner+project // 作用域审计查询上(store.ListProjectCommandAudits),不新建通用幂等平台:不加表、 // 不加索引、不引入新的重试语义。命令回执(审计信封)本就是宿主侧的操作记录, // handler 自己事务/发件箱里的领域操作表(如文档投递 document_render_deliveries) // 仍由各自领域查询负责。 // // 诚实边界: // - 只有成功落下审计信封的 operation 查得到。审计不可用时总线回 unknown + // operationID,但那一行没有落库:这里返回"无记录",绝不推断"未执行"。 // - 扫描有界(最近若干页):超出窗口的旧操作返回无记录,而不是全表扫描。 // - project_id 是追加时按可信会话快照下来的归属;改绑/删除会话不改写历史归属, // 与既有项目审计查询同一口径。 type commandOperationQuery struct{ st *store.Store } const ( operationLookupPageSize = 100 operationLookupMaxPages = 20 // 有界扫描:最近 2000 行,超窗即"无记录" ) func (q commandOperationQuery) LookupOperation(owner, projectID, operationID string) (cmdbus.OperationRecord, bool, error) { if q.st == nil || owner == "" || projectID == "" || operationID == "" { return cmdbus.OperationRecord{}, false, nil } var before, cutoff int64 for page := 0; page < operationLookupMaxPages; page++ { rows, resolvedCutoff, next, err := q.st.ListProjectCommandAudits(owner, projectID, before, cutoff, operationLookupPageSize) if err != nil { return cmdbus.OperationRecord{}, false, err } cutoff = resolvedCutoff // 首帧固定游标,后续页在同一提交前缀内翻页 for _, row := range rows { if row.Envelope.OperationID != operationID { continue } return cmdbus.OperationRecord{ OperationID: row.Envelope.OperationID, CommandID: row.Envelope.CommandID, State: row.Envelope.State, Time: row.Envelope.Time, }, true, nil } if next == 0 { return cmdbus.OperationRecord{}, false, nil } before = next } return cmdbus.OperationRecord{}, false, nil } // getCommandOperation 是 unknown 回执的对账入口(R7/KTD6): // GET /api/commands/operations/{operationId}?projectId=… // // 授权复用既有口径:owner 只取自认证上下文(不读请求参数),projectId 仅用于 // 收窄到该 owner 名下的项目——与 /api/commands/audit 同源,跨租户天然不可见。 // 查不到一律 404:HTTP 层的"没有记录"同样不得被读成"未执行"。 func (s *Server) getCommandOperation(w http.ResponseWriter, r *http.Request) { if s.Bus == nil { http.Error(w, "commands disabled", http.StatusServiceUnavailable) return } record, err := s.Bus.LookupOperation(s.owner(r), r.URL.Query().Get("projectId"), r.PathValue("operationId")) switch { case errors.Is(err, cmdbus.ErrOperationScope): http.Error(w, "projectId and operationId are required", http.StatusBadRequest) return case errors.Is(err, cmdbus.ErrOperationQueryUnavailable): http.Error(w, "operation query unavailable", http.StatusServiceUnavailable) return case errors.Is(err, cmdbus.ErrOperationNotRecorded): http.Error(w, "no operation record in this scope", http.StatusNotFound) return case err != nil: writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"operation": record}) }