268 lines
9.2 KiB
Go
268 lines
9.2 KiB
Go
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
|
||
}
|