ci / vet-and-test (pull_request) Successful in 1m10s
Why: The approved U5 scope needs the action half of the loop: an idempotent ledger (migration v24), a single-choke SubmitAction (plugin activation, current-revision, action whitelist, params-hash idempotency), dispatch through the command bus so DangerHigh lands on the existing inline confirm card with no Confirmed passthrough (the endpoint does not even bind the field), the button-only generative-ui.submit storing form data in the plugin KV ledger, and catalog discipline rejecting unknown action ids at present time. Evidence: go build/vet clean; full go test ./... green; surfacesvc/ genui/store suites also green under AGENT_PG_TEST_DSN; red matrix covers disabled/stale-revision/cross-owner/unknown-action/idempotent same-and-different-params/danger-high-await-confirm and the endpoint forged-confirmed case. Impact: Additive migration v24 + new endpoint/command; existing endpoints and command semantics untouched (B-class whitelist starts empty per the approved scope). Refs: U5 action-scope r2 (approved, Documents #71); UI #46/#47; Frontend #56; Dev task feature/generative-ui-u5
77 lines
3.0 KiB
Go
77 lines
3.0 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ---- Generative UI Surface 动作台账(提案 U5;迁移 v23)----
|
|
|
|
// SurfaceActionRecord 是一次动作提交的台账行。state: submitted(已分发)/
|
|
// awaiting_confirm(高危确认卡挂起,执行权威在命令总线审计)/ committed /
|
|
// rejected(分发被拒)。同 requestId 重放读此行返回原回执。
|
|
type SurfaceActionRecord struct {
|
|
Owner string
|
|
SurfaceID string
|
|
ActionID string
|
|
RequestID string
|
|
Revision int
|
|
ParamsHash string
|
|
State string
|
|
Receipt string
|
|
CardID string
|
|
CreatedAt string
|
|
}
|
|
|
|
// ErrIdempotencyConflict 同 requestId 但参数不同:拒绝(KTD6)。
|
|
var ErrIdempotencyConflict = errors.New("surface action: request id reused with different params")
|
|
|
|
// SaveSurfaceActionIfAbsent 幂等写入:首个提交插入成功(first=true);
|
|
// 已存在时返回既有行(first=false)。参数一致性由调用方比对 ParamsHash。
|
|
func (s *Store) SaveSurfaceActionIfAbsent(rec SurfaceActionRecord) (SurfaceActionRecord, bool, error) {
|
|
rec.CreatedAt = time.Now().UTC().Format(time.RFC3339)
|
|
res, err := s.db.Exec(s.q(`INSERT INTO surface_actions(owner,surface_id,action_id,request_id,revision,params_hash,state,receipt,card_id,created_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
ON CONFLICT(owner, surface_id, action_id, request_id) DO NOTHING`),
|
|
rec.Owner, rec.SurfaceID, rec.ActionID, rec.RequestID, rec.Revision,
|
|
rec.ParamsHash, rec.State, rec.Receipt, rec.CardID, rec.CreatedAt)
|
|
if err != nil {
|
|
return rec, false, err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
existing, err := s.GetSurfaceAction(rec.Owner, rec.SurfaceID, rec.ActionID, rec.RequestID)
|
|
if err != nil {
|
|
return rec, false, err
|
|
}
|
|
return existing, n == 1, nil // RowsAffected=1 → 本调用完成首插(时间戳同秒不可靠)
|
|
}
|
|
|
|
// GetSurfaceAction 读取台账行(不存在 → ErrNotFound)。
|
|
func (s *Store) GetSurfaceAction(owner, surfaceID, actionID, requestID string) (SurfaceActionRecord, error) {
|
|
var r SurfaceActionRecord
|
|
err := s.db.QueryRow(s.q(`SELECT owner,surface_id,action_id,request_id,revision,params_hash,state,receipt,card_id,created_at
|
|
FROM surface_actions WHERE owner=? AND surface_id=? AND action_id=? AND request_id=?`),
|
|
owner, surfaceID, actionID, requestID).
|
|
Scan(&r.Owner, &r.SurfaceID, &r.ActionID, &r.RequestID, &r.Revision, &r.ParamsHash, &r.State, &r.Receipt, &r.CardID, &r.CreatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return r, ErrNotFound
|
|
}
|
|
return r, err
|
|
}
|
|
|
|
// UpdateSurfaceActionState 回写分发结果(首个提交提交后的状态迁移)。
|
|
func (s *Store) UpdateSurfaceActionState(owner, surfaceID, actionID, requestID, state, receipt, cardID string) error {
|
|
res, err := s.db.Exec(s.q(`UPDATE surface_actions SET state=?, receipt=?, card_id=?
|
|
WHERE owner=? AND surface_id=? AND action_id=? AND request_id=?`),
|
|
state, receipt, cardID, owner, surfaceID, actionID, requestID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|