feat(cloud-agent): 云端 Agent - 完善运行时与资源持久化

This commit is contained in:
ddcat666
2026-09-26 17:05:36 +08:00
parent 4613cca38c
commit f4480b129b
40 changed files with 1739 additions and 440 deletions

View File

@@ -2,12 +2,104 @@ package app
import (
"encoding/json"
"fmt"
"strings"
"infinite-canvas/backend/internal/model"
)
// Keep cross-field validation server-side rather than adding provider-specific
// root schema combinators. The same contract is advertised by both media tools.
const cloudAgentModelSelectionDescription = "模型选择必填:将 model_list 返回的一种 selection 展开到顶层参数,必须提供非空 logicalModelId,或同时提供非空 channelId 和 channelModelKey,二者互斥。未使用的选择字段省略或传空字符串,不得传 null 或仅含空白的字符串。缺失、混用或不完整均在提交前拒绝,不会自动选择或切换模型。"
const cloudAgentModelSelectionDescription = "模型选择:复制 model_list 的 selection 到顶层。完全省略模型字段时,仅使用当前项目对应能力的可用默认模型。显式选择必须提供 logicalModelId,或同时提供 channelId 与 channelModelKey,二者互斥。未使用字段省略或传空字符串,不得传 null/空白;无可用默认、混用或不完整均拒绝,不会随机选模。"
// applyCloudAgentProjectDefaultModel only fills a selection when the Agent did
// not send any model-selection field at all. An explicitly empty, partial, or
// null selection remains invalid so a malformed tool call cannot silently turn
// into a different model.
func (s *Service) applyCloudAgentProjectDefaultModel(run *model.CloudAgentExecution, state *cloudAgentRuntime, raw string, a *cloudAgentMediaArgs) (bool, error) {
if s == nil || s.repo == nil || run == nil || state == nil || a == nil || cloudAgentModelSelectionProvided(raw) {
return false, nil
}
a.Mode = strings.ToLower(strings.TrimSpace(a.Mode))
if a.Mode != "image" && a.Mode != "video" || strings.TrimSpace(state.Request.CanvasID) == "" {
return false, nil
}
canvas, err := s.repo.CanvasProjectForUser(run.UserID, state.Request.CanvasID)
if err != nil {
return false, err
}
if strings.TrimSpace(canvas.ProjectID) == "" {
return false, nil
}
project, err := s.repo.ProjectForUser(run.UserID, canvas.ProjectID)
if err != nil {
return false, err
}
defaultModel := strings.TrimSpace(project.DefaultImageModel)
if a.Mode == "video" {
defaultModel = strings.TrimSpace(project.DefaultVideoModel)
}
if defaultModel == "" {
return false, nil
}
if channelID, channelModelKey, ok := splitCloudAgentChannelModel(defaultModel); ok {
catalog, catalogErr := s.ModelCatalog(nil)
if catalogErr != nil {
return false, catalogErr
}
for _, channel := range catalog.Channels {
if channel.ID != channelID {
continue
}
for _, channelModel := range channel.Models {
if channelModel.ModelKey == channelModelKey && channelModel.Available && normalizeCapability(channelModel.Capability) == a.Mode {
a.ChannelID, a.ChannelModelKey = channelID, channelModelKey
return true, nil
}
}
}
return false, fmt.Errorf("项目默认%s模型不可用或能力不匹配,请在项目设置中重新选择模型", cloudAgentModeLabel(a.Mode))
}
logicalModels, err := s.PublicLogicalModels(nil)
if err != nil {
return false, err
}
for _, logicalModel := range logicalModels {
if logicalModel.ID == defaultModel && logicalModel.Available && normalizeCapability(logicalModel.Capability) == a.Mode {
a.LogicalModelID = defaultModel
return true, nil
}
}
return false, fmt.Errorf("项目默认%s模型不可用或能力不匹配,请在项目设置中重新选择模型", cloudAgentModeLabel(a.Mode))
}
func cloudAgentModelSelectionProvided(raw string) bool {
var fields map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &fields); err != nil || fields == nil {
return true
}
for _, field := range []string{"logicalModelId", "channelId", "channelModelKey"} {
if _, ok := fields[field]; ok {
return true
}
}
return false
}
func splitCloudAgentChannelModel(value string) (string, string, bool) {
channelID, channelModelKey, ok := strings.Cut(strings.TrimSpace(value), "::")
channelID, channelModelKey = strings.TrimSpace(channelID), strings.TrimSpace(channelModelKey)
return channelID, channelModelKey, ok && channelID != "" && channelModelKey != ""
}
func cloudAgentModeLabel(mode string) string {
if mode == "video" {
return "视频"
}
return "图片"
}
func validateCloudAgentModelSelection(raw string, a cloudAgentMediaArgs) error {
// Go's JSON decoder accepts null for string fields; the tool contract does
@@ -33,7 +125,7 @@ func validateCloudAgentModelSelection(raw string, a cloudAgentMediaArgs) error {
case a.LogicalModelID != "":
return nil
case a.ChannelID == "" && a.ChannelModelKey == "":
return cloudAgentFieldError("logicalModelId", "required", "缺少模型选择:必须复制 model_list 的 logicalModelId 或完整的 channelId/channelModelKey,不会自动选择模型")
return cloudAgentFieldError("logicalModelId", "required", "缺少模型选择:请复制 model_list 的 logicalModelId 或完整的 channelId/channelModelKey;若项目未配置当前能力的可用默认模型,不能自动猜测")
case a.ChannelID == "":
return cloudAgentFieldError("channelId", "required", "系统渠道模型选择不完整:缺少 channelId,请复制 model_list 的完整 selection")
case a.ChannelModelKey == "":