后端: 1. 主动调度预览确认主链路落地——新增主动调度数据模型、DAO 与事件契约;接入 dry-run pipeline 与任务触发的 job upsert/cancel;新增 preview 查询与 confirm API,支持 apply_id 幂等确认并同步写入 task_pool 日程 2. 同步更新主动调度实施文档的阶段状态与验收记录 前端: 3. AssistantPanel 脚本层继续解耦——私有类型迁移到独立类型文件,并抽离会话、工具轨迹、思考摘要、任务表单等纯函数辅助逻辑;保持助手面板模板与样式不变,降低表现层回归风险
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package apply
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"strings"
|
||
)
|
||
|
||
type RequestHash struct {
|
||
BodyHash string
|
||
ApplyHash string
|
||
ApplyID string
|
||
}
|
||
|
||
// BuildConfirmRequestHash 计算 confirm 请求的幂等摘要。
|
||
//
|
||
// 职责边界:
|
||
// 1. body_hash 只覆盖一次确认动作中真正影响 apply 的 body 字段;
|
||
// 2. apply_hash 按 preview_id + idempotency_key + body_hash 计算,满足同 key 不同内容可识别;
|
||
// 3. 不查询历史记录,是否冲突由 DetectIdempotencyConflict 或接入层唯一约束判断。
|
||
func BuildConfirmRequestHash(previewID string, req ConfirmRequest) (RequestHash, error) {
|
||
previewID = strings.TrimSpace(firstNonEmpty(req.PreviewID, previewID))
|
||
idempotencyKey := strings.TrimSpace(req.IdempotencyKey)
|
||
if previewID == "" {
|
||
return RequestHash{}, newApplyError(ErrorCodeInvalidRequest, "preview_id 不能为空", nil)
|
||
}
|
||
if idempotencyKey == "" {
|
||
return RequestHash{}, newApplyError(ErrorCodeInvalidRequest, "idempotency_key 不能为空", nil)
|
||
}
|
||
|
||
bodyHash, err := hashJSON(confirmRequestBodyForHash{
|
||
CandidateID: strings.TrimSpace(req.CandidateID),
|
||
Action: normalizeConfirmAction(req.Action),
|
||
EditedChanges: req.EditedChanges,
|
||
IdempotencyKey: idempotencyKey,
|
||
})
|
||
if err != nil {
|
||
return RequestHash{}, err
|
||
}
|
||
|
||
applyHash := sha256Text(previewID + "\n" + idempotencyKey + "\n" + bodyHash)
|
||
applyID := "asap_" + applyHash[:24]
|
||
return RequestHash{
|
||
BodyHash: bodyHash,
|
||
ApplyHash: applyHash,
|
||
ApplyID: applyID,
|
||
}, nil
|
||
}
|
||
|
||
// BuildNormalizedChangesHash 计算转换后 changes 的稳定摘要。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只用于审计和幂等辅助,不替代正式 DB 重校验;
|
||
// 2. 输入应是 NormalizeChanges 后的结果,避免相同语义因顺序不同得到不同摘要;
|
||
// 3. 序列化失败会返回 invalid_request,调用方应拒绝本次 confirm。
|
||
func BuildNormalizedChangesHash(changes []ApplyChange) (string, error) {
|
||
return hashJSON(changes)
|
||
}
|
||
|
||
type confirmRequestBodyForHash struct {
|
||
CandidateID string `json:"candidate_id"`
|
||
Action ConfirmAction `json:"action"`
|
||
EditedChanges []ApplyChange `json:"edited_changes,omitempty"`
|
||
IdempotencyKey string `json:"idempotency_key"`
|
||
}
|
||
|
||
func hashJSON(value any) (string, error) {
|
||
raw, err := json.Marshal(value)
|
||
if err != nil {
|
||
return "", newApplyError(ErrorCodeInvalidRequest, "请求体无法生成稳定摘要", err)
|
||
}
|
||
return sha256Bytes(raw), nil
|
||
}
|
||
|
||
func sha256Text(text string) string {
|
||
return sha256Bytes([]byte(text))
|
||
}
|
||
|
||
func sha256Bytes(raw []byte) string {
|
||
sum := sha256.Sum256(raw)
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
func normalizeConfirmAction(action ConfirmAction) ConfirmAction {
|
||
if action == "" {
|
||
return ConfirmActionConfirm
|
||
}
|
||
return action
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|