feat(agent): 云端创作助手 - 增加创作锚点、媒体准入与结构化分镜能力

This commit is contained in:
ddCat
2026-09-15 13:37:21 +08:00
parent 40aca20f4d
commit be9b22a697
22 changed files with 1686 additions and 60 deletions

View File

@@ -50,6 +50,7 @@ type cloudAgentRuntime struct {
Policy cloudAgentPolicySnapshot `json:"policy"`
ParentID string `json:"parentId,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
CreativeAnchor cloudAgentCreativeAnchor `json:"creativeAnchor,omitempty"`
TextHistory []providerTextMessage `json:"textHistory,omitempty"`
Skills []cloudAgentSkill `json:"skills"`
SkillReads map[string]bool `json:"skillReads,omitempty"`
@@ -80,7 +81,7 @@ func (s *Service) ensureCloudAgentExecution(task *model.Task, initial cloudAgent
if err := json.Unmarshal([]byte(task.InputJSON), &input); err != nil {
return err
}
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: input.Requests.Canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Events: []CloudAgentEvent{}}
state := cloudAgentRuntime{Request: initial.Request, Policy: initial.Policy, ParentID: initial.ParentID, Fingerprint: initial.Fingerprint, CreativeAnchor: initial.CreativeAnchor, TextHistory: input.TextHistory, Skills: initial.Skills, Profile: initial.Profile, Canonical: input.Requests.Canonical, ActiveTaskID: task.ID, TaskIDs: []string{task.ID}, Step: 1, Decisions: map[string]string{}, Events: []CloudAgentEvent{}}
if len(initial.Skills) > 0 {
state.event(task.ID, "tool_completed", map[string]any{"toolName": "skills_load", "text": fmt.Sprintf("已启用 %d 个技能,正文将按需读取", len(initial.Skills))})
}
@@ -356,7 +357,7 @@ func (s *Service) cloudAgentExecutionOutput(task *model.Task, initial cloudAgent
// A terminal failed run must remain readable even if its durable runtime
// blob was damaged. Do not invent permissions or approval state; expose
// only the identity available from the original task input.
state = cloudAgentRuntime{Request: initial.Request, ParentID: initial.ParentID, Skills: initial.Skills, Profile: initial.Profile, TaskIDs: []string{task.ID}, Events: []CloudAgentEvent{}}
state = cloudAgentRuntime{Request: initial.Request, ParentID: initial.ParentID, CreativeAnchor: initial.CreativeAnchor, Skills: initial.Skills, Profile: initial.Profile, TaskIDs: []string{task.ID}, Events: []CloudAgentEvent{}}
}
out := agentRunOutput(task, initial)
out.Status = run.Status
@@ -850,20 +851,39 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
state.Calls[state.CallIndex] = call
preview = cloudAgentMediaApprovalPreview(plan, modelName)
} else {
canvasPlan, err := prepareCloudAgentCanvasMutation(repo, run.UserID, state.Request.CanvasID, call)
if err != nil {
var mutationErr error
switch call.Function.Name {
case "canvas_create_storyboard":
storyboardPlan, err := prepareCloudAgentStoryboardCreate(repo, run.UserID, state.Request.CanvasID, call)
mutationErr = err
if err == nil {
preview = storyboardPlan.Preview
}
case "canvas_edit_storyboard":
storyboardPlan, err := prepareCloudAgentStoryboardEdit(repo, run.UserID, state.Request.CanvasID, call)
mutationErr = err
if err == nil {
preview = storyboardPlan.Preview
}
default:
canvasPlan, err := prepareCloudAgentCanvasMutation(repo, run.UserID, state.Request.CanvasID, call)
mutationErr = err
if err == nil {
preview = canvasPlan.Preview
}
}
if mutationErr != nil {
var argumentErr *cloudAgentArgumentError
if errors.As(err, &argumentErr) {
cloudAgentToolResult(run.ID, state, call, nil, err)
if errors.As(mutationErr, &argumentErr) {
cloudAgentToolResult(run.ID, state, call, nil, mutationErr)
return cloudAgentSave(current, state)
}
var appErr *AppError
if errors.As(err, &appErr) && appErr != nil {
return failCloudAgentAdmission(current, state, run.ID, err)
if errors.As(mutationErr, &appErr) && appErr != nil {
return failCloudAgentAdmission(current, state, run.ID, mutationErr)
}
return err
return mutationErr
}
preview = canvasPlan.Preview
}
state.Approval = &cloudAgentApproval{ID: fmt.Sprintf("%s-%d-%d", run.ID, state.Step, state.CallIndex), Call: call, CallHash: cloudAgentApprovalCallHash(call), Preview: preview, ModelName: modelName}
current.Status = "waiting_approval"
@@ -879,12 +899,13 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
return s.terminateCloudAgent(run, "Agent 运行策略不可用,本轮已停止")
}
var modelList any
var modelListErr error
if allowed && call.Function.Name == "model_list" {
models, e := s.cloudAgentModelList()
if e != nil {
return s.terminateCloudAgent(run, "Agent 模型目录不可用,本轮已停止")
intent, e := s.cloudAgentModelIntent(run.UserID, state.Request.CanvasID, call.Function.Arguments)
modelListErr = e
if e == nil {
modelList, modelListErr = s.cloudAgentModelList(intent)
}
modelList = models
}
// Skill reads use the domain repository and filesystem, not the checkpoint
// transaction's connection. Read first to avoid nesting DB reads on SQLite.
@@ -901,12 +922,12 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
switch {
case !allowed:
toolErr = BadAuthRequest("工具未获本轮权限授权")
case state.Approval != nil && state.Approval.Decision == "reject":
toolErr = BadAuthRequest("用户拒绝本次操作:" + state.Approval.Reason)
case call.Function.Name == "canvas_apply_ops":
result, toolErr = applyCloudAgentCanvas(repo, run.UserID, state.Request.CanvasID, call, policy, cloudAgentCanvasEventRecorder(run.ID, state))
case call.Function.Name == "canvas_create_storyboard", call.Function.Name == "canvas_edit_storyboard":
result, toolErr = applyCloudAgentStoryboardMutation(repo, run.UserID, state.Request.CanvasID, call, policy, cloudAgentCanvasEventRecorder(run.ID, state))
case call.Function.Name == "model_list":
result = modelList
result, toolErr = modelList, modelListErr
case call.Function.Name == "skill_read_file":
result, toolErr = skillResult, skillErr
default:
@@ -1140,6 +1161,22 @@ func (s *Service) DecideCloudAgentApproval(userID, id, approvalID, decision, rea
state.Approval.Decision = decision
state.Approval.Reason = reason
state.Decisions[approvalID] = decision
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": "已拒绝本次操作,未写入画布。你可以告诉 Agent 修改方向后重新申请。",
})
return cloudAgentSave(current, &state)
}
current.Status = "running"
state.event(id, "approval_decided", map[string]any{"approvalId": approvalID, "decision": decision})
return cloudAgentSave(current, &state)