Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_profile.go

268 lines
9.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
"time"
"unicode"
"unicode/utf8"
"yingce/backend/internal/kernel"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
"gorm.io/gorm"
)
const cloudAgentProfileMaxRunes = 12000
type AgentProfileRequest struct {
Scope string `json:"scope"`
ProjectID string `json:"projectId,omitempty"`
CanvasID string `json:"canvasId,omitempty"`
Content string `json:"content"`
Revision int64 `json:"revision"`
}
type AgentProfileLayer struct {
Scope string `json:"scope"`
ProjectID string `json:"projectId,omitempty"`
CanvasID string `json:"canvasId,omitempty"`
Content string `json:"content"`
Revision int64 `json:"revision"`
Hash string `json:"hash"`
}
type AgentProfileView struct {
Revision string `json:"revision"`
Hash string `json:"hash"`
Layers []AgentProfileLayer `json:"layers"`
}
func validateAgentProfileContent(content string) error {
if !utf8.ValidString(content) || utf8.RuneCountInString(content) > cloudAgentProfileMaxRunes {
return BadAuthRequest("Agent 偏好文档必须是有效 UTF-8,且不超过 12000 个字符")
}
for _, r := range content {
if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' {
return BadAuthRequest("Agent 偏好文档不能包含控制字符")
}
}
return nil
}
func validateAgentProfileScope(req AgentProfileRequest) error {
if req.Scope != model.AgentProfileScopeUser && req.Scope != model.AgentProfileScopeProject && req.Scope != model.AgentProfileScopeCanvas {
return BadAuthRequest("无效的 Agent 偏好作用域")
}
if req.Scope == model.AgentProfileScopeUser && (req.ProjectID != "" || req.CanvasID != "") {
return BadAuthRequest("用户偏好不能带项目或画布 ID")
}
if req.Scope == model.AgentProfileScopeProject && (req.ProjectID == "" || req.CanvasID != "") {
return BadAuthRequest("项目偏好需要 projectId,且不能带 canvasId")
}
if req.Scope == model.AgentProfileScopeCanvas && req.CanvasID == "" {
return BadAuthRequest("画布偏好缺少 canvasId")
}
if req.ProjectID != "" {
if err := validateCloudAgentID(req.ProjectID, "项目 ID", 80); err != nil {
return err
}
}
if req.CanvasID != "" {
if err := validateCloudAgentID(req.CanvasID, "画布 ID", 80); err != nil {
return err
}
}
if req.Revision < 0 {
return BadAuthRequest("偏好 revision 无效")
}
return validateAgentProfileContent(req.Content)
}
func agentProfileHash(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
}
func agentProfileRevision(layers []AgentProfileLayer) string {
var b strings.Builder
for _, layer := range layers {
b.WriteString(layer.Scope)
b.WriteByte(0)
b.WriteString(layer.ProjectID)
b.WriteByte(0)
b.WriteString(layer.CanvasID)
b.WriteByte(0)
b.WriteString(layer.Hash)
b.WriteByte(0)
}
return agentProfileHash(b.String())
}
func (s *Service) UpdateCloudAgentProfile(userID string, req AgentProfileRequest) (AgentProfileView, error) {
if userID == "" {
return AgentProfileView{}, kernel.Unauthorized("请先登录")
}
if err := validateAgentProfileScope(req); err != nil {
return AgentProfileView{}, err
}
projectID, canvasID := req.ProjectID, req.CanvasID
if req.Scope == model.AgentProfileScopeProject {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return AgentProfileView{}, err
}
}
if req.Scope == model.AgentProfileScopeCanvas {
canvas, err := s.repo.CanvasProjectForUser(userID, canvasID)
if err != nil {
return AgentProfileView{}, err
}
if projectID != "" && canvas.ProjectID != projectID {
return AgentProfileView{}, errors.New("画布不属于指定项目")
}
projectID = canvas.ProjectID
}
if req.Scope != model.AgentProfileScopeCanvas {
canvasID = ""
}
content := strings.TrimSpace(req.Content)
profile := &model.AgentProfile{
ID: newID(), UserID: userID, Scope: req.Scope, ProjectID: projectID, CanvasID: canvasID,
Content: content, Revision: 1, Hash: agentProfileHash(content), UpdatedAt: time.Now(),
}
if err := s.repo.SaveAgentProfile(profile, req.Revision); err != nil {
if errors.Is(err, repository.ErrTaskStateConflict) {
return AgentProfileView{}, creationConflict("Agent 偏好已变化,请重新读取后保存")
}
return AgentProfileView{}, err
}
return s.cloudAgentProfileView(userID, projectID, canvasID)
}
// CloudAgentProfile returns the effective user/project/canvas preferences for a canvas.
func (s *Service) CloudAgentProfile(userID, canvasID string) (AgentProfileView, error) {
return s.cloudAgentProfileView(userID, "", canvasID)
}
// CloudAgentProfileForScope returns the effective preference layers for the requested scope.
// The returned revision/hash identify the exact merged document that must be fixed into a run.
func (s *Service) CloudAgentProfileForScope(userID, projectID, canvasID string) (AgentProfileView, error) {
return s.cloudAgentProfileView(userID, projectID, canvasID)
}
func (s *Service) cloudAgentProfileView(userID, projectID, canvasID string) (AgentProfileView, error) {
if userID == "" {
return AgentProfileView{}, kernel.Unauthorized("请先登录")
}
if projectID != "" {
if _, err := s.repo.ProjectForUser(userID, projectID); err != nil {
return AgentProfileView{}, err
}
}
if canvasID != "" {
canvas, err := s.repo.CanvasProjectForUser(userID, canvasID)
if err != nil {
return AgentProfileView{}, err
}
if projectID != "" && canvas.ProjectID != projectID {
return AgentProfileView{}, errors.New("画布不属于指定项目")
}
projectID = canvas.ProjectID
}
layers := []AgentProfileLayer{}
appendProfile := func(profile *model.AgentProfile, scope string) {
if profile == nil {
return
}
layers = append(layers, AgentProfileLayer{Scope: scope, ProjectID: profile.ProjectID, CanvasID: profile.CanvasID, Content: profile.Content, Revision: profile.Revision, Hash: profile.Hash})
}
user, err := s.repo.AgentProfile(userID, model.AgentProfileScopeUser, "", "")
if err == nil {
appendProfile(user, model.AgentProfileScopeUser)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return AgentProfileView{}, err
}
if projectID != "" {
project, err := s.repo.AgentProfile(userID, model.AgentProfileScopeProject, projectID, "")
if err == nil {
appendProfile(project, model.AgentProfileScopeProject)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return AgentProfileView{}, err
}
}
if canvasID != "" {
canvasProfile, err := s.repo.AgentProfile(userID, model.AgentProfileScopeCanvas, projectID, canvasID)
if err == nil {
appendProfile(canvasProfile, model.AgentProfileScopeCanvas)
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return AgentProfileView{}, err
}
}
text := agentProfileText(layers)
return AgentProfileView{Revision: agentProfileRevision(layers), Hash: agentProfileHash(text), Layers: layers}, nil
}
func agentProfileText(layers []AgentProfileLayer) string {
var text strings.Builder
for _, layer := range layers {
if strings.TrimSpace(layer.Content) == "" {
continue
}
text.WriteString("\n## ")
text.WriteString(layer.Scope)
text.WriteString(" profile\n")
text.WriteString(layer.Content)
text.WriteByte('\n')
}
return strings.TrimSpace(text.String())
}
func (s *Service) cloudAgentProfileSnapshot(userID, canvasID string) (cloudAgentProfileSnapshot, error) {
view, err := s.CloudAgentProfile(userID, canvasID)
if err != nil {
return cloudAgentProfileSnapshot{}, err
}
layers := make([]AgentProfileLayer, len(view.Layers))
copy(layers, view.Layers)
return cloudAgentProfileSnapshot{Revision: view.Revision, Hash: view.Hash, Layers: layers}, nil
}
func validateCloudAgentProfileSnapshot(profile cloudAgentProfileSnapshot, policy cloudAgentPolicySnapshot) error {
if profile.Revision != policy.ProfileRevision || profile.Hash != policy.ProfileHash {
return errors.New("Agent runtime profile does not match admitted policy")
}
order := map[string]int{model.AgentProfileScopeUser: 0, model.AgentProfileScopeProject: 1, model.AgentProfileScopeCanvas: 2}
last, seen := -1, map[string]bool{}
for _, layer := range profile.Layers {
position, ok := order[layer.Scope]
if !ok || seen[layer.Scope] || position <= last || layer.Revision <= 0 {
return errors.New("Agent runtime profile layers are invalid")
}
switch layer.Scope {
case model.AgentProfileScopeUser:
if layer.ProjectID != "" || layer.CanvasID != "" {
return errors.New("Agent runtime user profile scope is invalid")
}
case model.AgentProfileScopeProject:
if layer.ProjectID == "" || layer.CanvasID != "" {
return errors.New("Agent runtime project profile scope is invalid")
}
case model.AgentProfileScopeCanvas:
if layer.ProjectID == "" || layer.CanvasID == "" {
return errors.New("Agent runtime canvas profile scope is invalid")
}
}
if err := validateAgentProfileContent(layer.Content); err != nil || layer.Hash != agentProfileHash(layer.Content) {
return errors.New("Agent runtime profile content is invalid")
}
seen[layer.Scope], last = true, position
}
if profile.Revision != agentProfileRevision(profile.Layers) || profile.Hash != agentProfileHash(agentProfileText(profile.Layers)) {
return errors.New("Agent runtime profile snapshot is inconsistent")
}
return nil
}