Why: An interrupted import cleanup removed the cmdbus core import while two human-decision files used an explicit alias. Evidence: go test ./internal/cmdbus ./internal/server passes. Impact: The backend worktree is buildable and consistently names the core package without behavior changes. Refs: W4b human decision verifier
99 lines
3.1 KiB
Go
99 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
core "agent-system-core"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"agent-system-backend/internal/cmdbus"
|
|
"agent-system-backend/internal/store"
|
|
)
|
|
|
|
// resolveHumanDecision handles a card issued by HumanDecisionVerifier.
|
|
// Ordinary HITL cards return handled=false and keep the existing run path.
|
|
func (s *Server) resolveHumanDecision(w http.ResponseWriter, r *http.Request, resp core.HITLResponse) (bool, error) {
|
|
rec, err := s.st.HumanDecision(string(resp.CardID))
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
decision := resp.ChoiceID
|
|
if decision == "" {
|
|
switch strings.TrimSpace(resp.FreeText) {
|
|
case "批准", "接受", "同意":
|
|
pending, perr := s.st.PendingHumanDecisions(s.owner(r), time.Now())
|
|
if perr != nil {
|
|
return true, perr
|
|
}
|
|
if len(pending) != 1 || pending[0].ID != rec.ID {
|
|
return true, errors.New("human decision text matches more than one pending card")
|
|
}
|
|
decision = "accept"
|
|
default:
|
|
return true, errors.New("human decision requires an explicit choice")
|
|
}
|
|
}
|
|
answered, err := s.st.RespondHumanDecision(s.owner(r), rec.ID, decision, time.Now())
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
out := map[string]any{
|
|
"resolved": true, "proofId": answered.ProofID, "decision": answered.Decision,
|
|
"proposalId": answered.ProposalID, "proposalRevision": answered.ProposalRevision,
|
|
}
|
|
if s.Bus != nil && answered.CommandID != "" {
|
|
follow := s.Bus.Dispatch(r.Context(), cmdbus.Request{
|
|
ID: answered.CommandID,
|
|
Params: map[string]any{
|
|
"projectId": answered.ProjectID, "proposalId": answered.ProposalID,
|
|
"proposalRevision": answered.ProposalRevision, "payloadHash": answered.PayloadHash,
|
|
"decision": answered.Decision,
|
|
},
|
|
Entrance: "card", Actor: answered.HumanActor, Owner: s.owner(r),
|
|
ConversationID: answered.ConversationID,
|
|
HumanGrant: &cmdbus.HumanGrant{
|
|
CardID: answered.ID, ProofID: answered.ProofID,
|
|
Actor: answered.HumanActor, Decision: answered.Decision,
|
|
},
|
|
})
|
|
out["receipt"] = follow.Receipt
|
|
if follow.Payload != nil {
|
|
out["payload"] = follow.Payload
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
return true, nil
|
|
}
|
|
|
|
func (s *Server) sessionHumanGrant(actor, commandID string, params map[string]any) (*cmdbus.HumanGrant, bool) {
|
|
if commandID != "rdm.structure.decide" || s.st == nil {
|
|
return nil, false
|
|
}
|
|
project, _ := params["projectId"].(string)
|
|
proposal, _ := params["proposalId"].(string)
|
|
hash, _ := params["payloadHash"].(string)
|
|
decision, _ := params["decision"].(string)
|
|
revision := 0
|
|
switch v := params["proposalRevision"].(type) {
|
|
case float64:
|
|
revision = int(v)
|
|
case int:
|
|
revision = v
|
|
case string:
|
|
revision, _ = strconv.Atoi(v)
|
|
}
|
|
if project == "" || proposal == "" || hash == "" || decision == "" || revision < 1 {
|
|
return nil, false
|
|
}
|
|
rec, err := s.st.SessionHumanGrant(actor, commandID, project, proposal, hash, decision, revision, time.Now())
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
return &cmdbus.HumanGrant{CardID: rec.ID, ProofID: rec.ProofID, Actor: rec.HumanActor, Decision: rec.Decision}, true
|
|
}
|