fix(*): 支付与Agent运行 - 修复商品策略并保证审批恢复稳定

This commit is contained in:
ddcat666
2026-09-29 11:07:11 +08:00
parent 37bb4d7242
commit 3dd84ff25b
17 changed files with 1836 additions and 226 deletions

View File

@@ -2454,80 +2454,119 @@ func (s *Service) DecideCloudAgentApproval(userID, id, approvalID, decision, rea
return creationConflict("审批不存在或已过期")
}
// 只在记录审批决定的这次写入期间持锁:随后的工具执行(advanceCloudAgentTool)
// 会自己获取 storageMu,持锁到函数返回会造成自锁。
s.storageMu.Lock()
err = s.repo.MutateCloudAgent(userID, id, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
if settings != nil {
if err := s.updateCloudAgentMediaApproval(repo, run, &state, *settings); err != nil {
// 会自己获取 storageMu,持锁到函数返回会造成自锁。旧 Pi 会话退出前仍可能
// 写入最后一个事件,因此审批决定采用重读后 CAS 重试。
for attempt := 0; attempt < 8; attempt++ {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * 20 * time.Millisecond)
run, err = s.repo.CloudAgent(userID, id)
if err != nil {
return err
}
}
state.Approval.Decision = decision
state.Approval.Reason = reason
state.Decisions[approvalID] = decision
if settings != nil {
if state.DecisionSettings == nil {
state.DecisionSettings = map[string]string{}
}
state.DecisionSettings[approvalID] = creationHash(settings)
}
if decision == "approve" && state.Approval.Prepared != nil {
if state.DecisionPreparedHashes == nil {
state.DecisionPreparedHashes = map[string]string{}
}
state.DecisionPreparedHashes[approvalID] = state.Approval.Prepared.Hash
}
if decision == "reject" {
// Rejection is a user control-plane decision, not a failed tool
// invocation. Make it terminal before the scheduler can advance the
// pending call; no tool result, canvas mutation, generation task or
// follow-up model request may be produced from this decision.
current.Status = "rejected"
current.FailureMessage = ""
state.Approval = nil
state.event(id, "approval_decided", map[string]any{
"approvalId": approvalID,
"decision": decision,
"reason": reason,
"text": "已拒绝本次生成,草稿节点仍保留在画布中;未提交任务、未产生扣费。你可以继续编辑后重新申请。",
})
if err := repo.ReleaseCloudAgentResourceLeases(userID, approvalID); err != nil {
state, err = cloudAgentDecodeForExecution(run)
if err != nil {
return err
}
if previous, ok := state.Decisions[approvalID]; ok {
if previous == decision && (settings == nil || state.DecisionSettings[approvalID] == creationHash(settings)) {
return nil
}
return creationConflict("该审批已有不同决定")
}
if run.Status != "waiting_approval" || state.Approval == nil || state.Approval.ID != approvalID {
return creationConflict("审批不存在或已过期")
}
}
s.storageMu.Lock()
err = s.repo.MutateCloudAgent(userID, id, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
if settings != nil {
if err := s.updateCloudAgentMediaApproval(repo, run, &state, *settings); err != nil {
return err
}
}
state.Approval.Decision = decision
state.Approval.Reason = reason
state.Decisions[approvalID] = decision
if settings != nil {
if state.DecisionSettings == nil {
state.DecisionSettings = map[string]string{}
}
state.DecisionSettings[approvalID] = creationHash(settings)
}
if decision == "approve" && state.Approval.Prepared != nil {
if state.DecisionPreparedHashes == nil {
state.DecisionPreparedHashes = map[string]string{}
}
state.DecisionPreparedHashes[approvalID] = state.Approval.Prepared.Hash
}
if decision == "reject" {
// Rejection is a user control-plane decision, not a failed tool
// invocation. Make it terminal before the scheduler can advance the
// pending call; no tool result, canvas mutation, generation task or
// follow-up model request may be produced from this decision.
current.Status = "rejected"
current.FailureMessage = ""
state.Approval = nil
state.event(id, "approval_decided", map[string]any{
"approvalId": approvalID,
"decision": decision,
"reason": reason,
"text": "已拒绝本次生成,草稿节点仍保留在画布中;未提交任务、未产生扣费。你可以继续编辑后重新申请。",
})
if err := repo.ReleaseCloudAgentResourceLeases(userID, approvalID); err != nil {
return err
}
return cloudAgentSave(current, &state)
}
current.Status = "running"
payload := map[string]any{"approvalId": approvalID, "decision": decision, "arguments": json.RawMessage(state.Approval.Call.Function.Arguments), "preview": state.Approval.Preview, "modelName": state.Approval.ModelName}
if state.Approval.Prepared != nil {
payload["preparedHash"] = state.Approval.Prepared.Hash
payload["generationId"] = state.Approval.Prepared.GenerationID
}
state.event(id, "approval_decided", payload)
return cloudAgentSave(current, &state)
})
s.storageMu.Unlock()
if !errors.Is(err, repository.ErrCreationConflict) {
break
}
current.Status = "running"
payload := map[string]any{"approvalId": approvalID, "decision": decision, "arguments": json.RawMessage(state.Approval.Call.Function.Arguments), "preview": state.Approval.Preview, "modelName": state.Approval.ModelName}
if state.Approval.Prepared != nil {
payload["preparedHash"] = state.Approval.Prepared.Hash
payload["generationId"] = state.Approval.Prepared.GenerationID
}
state.event(id, "approval_decided", payload)
return cloudAgentSave(current, &state)
})
s.storageMu.Unlock()
}
if err != nil || decision != "approve" {
return err
}
// Approval only releases the paused tool. Execute it once in the Go business
// executor, then give the durable result back to Pi; Go never asks a model
// what to do next.
latest, err := s.repo.CloudAgent(userID, id)
if err != nil {
return err
var mediaTaskID string
for attempt := 0; attempt < 8; attempt++ {
if attempt > 0 {
time.Sleep(time.Duration(attempt) * 20 * time.Millisecond)
}
latest, err := s.repo.CloudAgent(userID, id)
if err != nil {
return err
}
state, err = cloudAgentDecode(latest)
if err != nil {
return err
}
err = s.advanceCloudAgentTool(latest, &state)
if !errors.Is(err, repository.ErrCreationConflict) {
if err != nil {
return err
}
mediaTaskID = state.MediaTaskID
break
}
if attempt == 7 {
return err
}
}
state, err = cloudAgentDecode(latest)
if err != nil {
return err
}
if err = s.advanceCloudAgentTool(latest, &state); err != nil {
return err
}
if state.MediaTaskID == "" {
if mediaTaskID == "" {
return s.resumeCloudAgentAfterApproval(userID, id, &state)
}
// 媒体生成可能要几分钟:审批请求立即返回,等待与回写在后台完成后再恢复运行。
mediaTaskID := state.MediaTaskID
s.startApprovedCloudAgentMediaWaiter(userID, id, mediaTaskID)
return nil
}
@@ -2564,7 +2603,7 @@ func (s *Service) resumeCloudAgentAfterApproval(userID, id string, state *cloudA
if err := s.saveCloudAgentPiResumePrompt(userID, id, state.PiResumePrompt); err != nil {
return err
}
s.startCloudAgentPi(userID, id)
s.resumeCloudAgentPi(userID, id)
return nil
}