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

739 lines
20 KiB
Go
Raw 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 (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math"
"sort"
"strings"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
// Canvas Layout Engine - 自动布局引擎
type CanvasLayoutEngine struct {
service *Service
}
func NewCanvasLayoutEngine(service *Service) *CanvasLayoutEngine {
return &CanvasLayoutEngine{service: service}
}
// ApplyLayout 应用布局算法
func (e *CanvasLayoutEngine) ApplyLayout(algorithm, direction string, spacing float64, nodes []model.CanvasNode) (map[string]*model.CanvasPosition, error) {
if algorithm == "auto" {
algorithm = e.detectBestAlgorithm(nodes)
}
switch algorithm {
case "tree":
return e.applyTreeLayout(nodes, direction, spacing)
case "grid":
return e.applyGridLayout(nodes, spacing)
case "force":
return e.applyForceLayout(nodes, spacing)
case "circular":
return e.applyCircularLayout(nodes, spacing)
case "hierarchical":
return e.applyHierarchicalLayout(nodes, direction, spacing)
default:
return nil, fmt.Errorf("unsupported layout algorithm: %s", algorithm)
}
}
// detectBestAlgorithm 检测最佳布局算法
func (e *CanvasLayoutEngine) detectBestAlgorithm(nodes []model.CanvasNode) string {
// 检查父子关系比例
withParent := 0
for _, node := range nodes {
if node.ParentID != nil {
withParent++
}
}
if len(nodes) > 0 && float64(withParent)/float64(len(nodes)) > 0.6 {
return "tree"
}
if len(nodes) > 20 {
return "force"
}
return "grid"
}
// applyTreeLayout 树形布局
func (e *CanvasLayoutEngine) applyTreeLayout(nodes []model.CanvasNode, direction string, spacing float64) (map[string]*model.CanvasPosition, error) {
positions := make(map[string]*model.CanvasPosition)
// 找出根节点
roots := make([]model.CanvasNode, 0)
nodeMap := make(map[string]model.CanvasNode)
for _, node := range nodes {
nodeMap[node.ID] = node
if node.ParentID == nil {
roots = append(roots, node)
}
}
if len(roots) == 0 {
return nil, fmt.Errorf("no root nodes found")
}
// 为每个根节点构建子树
currentY := 0.0
for _, root := range roots {
if direction == "horizontal" {
e.layoutTreeHorizontal(root, nodeMap, positions, 0, &currentY, spacing)
} else {
e.layoutTreeVertical(root, nodeMap, positions, 0, &currentY, spacing)
}
currentY += spacing * 2
}
return positions, nil
}
// layoutTreeVertical 垂直树形布局
func (e *CanvasLayoutEngine) layoutTreeVertical(node model.CanvasNode, nodeMap map[string]model.CanvasNode, positions map[string]*model.CanvasPosition, depth int, currentX *float64, spacing float64) {
x := *currentX
y := float64(depth) * spacing * 2
positions[node.ID] = &model.CanvasPosition{X: x, Y: y}
// 布局子节点
if len(node.Children) > 0 {
childX := x
for _, childID := range node.Children {
if child, ok := nodeMap[childID]; ok {
e.layoutTreeVertical(child, nodeMap, positions, depth+1, &childX, spacing)
childX += spacing
}
}
} else {
*currentX += spacing
}
}
// layoutTreeHorizontal 水平树形布局
func (e *CanvasLayoutEngine) layoutTreeHorizontal(node model.CanvasNode, nodeMap map[string]model.CanvasNode, positions map[string]*model.CanvasPosition, depth int, currentY *float64, spacing float64) {
x := float64(depth) * spacing * 2
y := *currentY
positions[node.ID] = &model.CanvasPosition{X: x, Y: y}
// 布局子节点
if len(node.Children) > 0 {
childY := y
for _, childID := range node.Children {
if child, ok := nodeMap[childID]; ok {
e.layoutTreeHorizontal(child, nodeMap, positions, depth+1, &childY, spacing)
childY += spacing
}
}
} else {
*currentY += spacing
}
}
// applyGridLayout 网格布局
func (e *CanvasLayoutEngine) applyGridLayout(nodes []model.CanvasNode, spacing float64) (map[string]*model.CanvasPosition, error) {
positions := make(map[string]*model.CanvasPosition)
// 计算网格维度
cols := int(math.Ceil(math.Sqrt(float64(len(nodes)))))
if cols == 0 {
cols = 1
}
for i, node := range nodes {
row := i / cols
col := i % cols
positions[node.ID] = &model.CanvasPosition{
X: float64(col) * spacing,
Y: float64(row) * spacing,
}
}
return positions, nil
}
// applyForceLayout 力导向布局(简化版)
func (e *CanvasLayoutEngine) applyForceLayout(nodes []model.CanvasNode, spacing float64) (map[string]*model.CanvasPosition, error) {
positions := make(map[string]*model.CanvasPosition)
// 初始随机分布
for i, node := range nodes {
angle := float64(i) * 2 * math.Pi / float64(len(nodes))
radius := spacing * 2
positions[node.ID] = &model.CanvasPosition{
X: radius * math.Cos(angle),
Y: radius * math.Sin(angle),
}
}
// 简化的力导向迭代(10次)
for iter := 0; iter < 10; iter++ {
forces := make(map[string]*model.CanvasPosition)
for id := range positions {
forces[id] = &model.CanvasPosition{X: 0, Y: 0}
}
// 计算斥力
for id1, pos1 := range positions {
for id2, pos2 := range positions {
if id1 != id2 {
dx := pos1.X - pos2.X
dy := pos1.Y - pos2.Y
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 1 {
dist = 1
}
force := spacing * spacing / dist
forces[id1].X += dx / dist * force
forces[id1].Y += dy / dist * force
}
}
}
// 计算引力(父子关系)
nodeMap := make(map[string]model.CanvasNode)
for _, node := range nodes {
nodeMap[node.ID] = node
}
for _, node := range nodes {
if node.ParentID != nil {
if parentPos, ok := positions[*node.ParentID]; ok {
if childPos, ok := positions[node.ID]; ok {
dx := parentPos.X - childPos.X
dy := parentPos.Y - childPos.Y
dist := math.Sqrt(dx*dx + dy*dy)
if dist > 0 {
force := dist * 0.1
forces[node.ID].X += dx / dist * force
forces[node.ID].Y += dy / dist * force
}
}
}
}
}
// 应用力
for id, force := range forces {
positions[id].X += force.X * 0.1
positions[id].Y += force.Y * 0.1
}
}
return positions, nil
}
// applyCircularLayout 环形布局
func (e *CanvasLayoutEngine) applyCircularLayout(nodes []model.CanvasNode, spacing float64) (map[string]*model.CanvasPosition, error) {
positions := make(map[string]*model.CanvasPosition)
if len(nodes) == 0 {
return positions, nil
}
radius := spacing * float64(len(nodes)) / (2 * math.Pi)
if radius < spacing {
radius = spacing
}
for i, node := range nodes {
angle := float64(i) * 2 * math.Pi / float64(len(nodes))
positions[node.ID] = &model.CanvasPosition{
X: radius * math.Cos(angle),
Y: radius * math.Sin(angle),
}
}
return positions, nil
}
// applyHierarchicalLayout 层级布局
func (e *CanvasLayoutEngine) applyHierarchicalLayout(nodes []model.CanvasNode, direction string, spacing float64) (map[string]*model.CanvasPosition, error) {
positions := make(map[string]*model.CanvasPosition)
// 计算每个节点的层级
levels := make(map[string]int)
nodeMap := make(map[string]model.CanvasNode)
for _, node := range nodes {
nodeMap[node.ID] = node
}
// 找根节点
roots := make([]string, 0)
for _, node := range nodes {
if node.ParentID == nil {
roots = append(roots, node.ID)
levels[node.ID] = 0
}
}
// BFS 计算层级
queue := make([]string, len(roots))
copy(queue, roots)
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
currentLevel := levels[current]
node := nodeMap[current]
for _, childID := range node.Children {
if _, visited := levels[childID]; !visited {
levels[childID] = currentLevel + 1
queue = append(queue, childID)
}
}
}
// 按层级分组
levelGroups := make(map[int][]string)
maxLevel := 0
for id, level := range levels {
levelGroups[level] = append(levelGroups[level], id)
if level > maxLevel {
maxLevel = level
}
}
// 布局每一层
if direction == "horizontal" {
for level := 0; level <= maxLevel; level++ {
nodeIDs := levelGroups[level]
x := float64(level) * spacing * 2
for i, nodeID := range nodeIDs {
y := float64(i) * spacing
positions[nodeID] = &model.CanvasPosition{X: x, Y: y}
}
}
} else {
for level := 0; level <= maxLevel; level++ {
nodeIDs := levelGroups[level]
y := float64(level) * spacing * 2
for i, nodeID := range nodeIDs {
x := float64(i) * spacing
positions[nodeID] = &model.CanvasPosition{X: x, Y: y}
}
}
}
return positions, nil
}
// Canvas Search Engine - 搜索引擎
type CanvasSearchEngine struct {
service *Service
}
type CanvasSearchQuery struct {
Query string
Type string
Category string
Tags []string
Limit int
}
type CanvasSearchResult struct {
Node model.CanvasNode `json:"node"`
Score float64 `json:"score"`
Highlight string `json:"highlight"`
}
func NewCanvasSearchEngine(service *Service) *CanvasSearchEngine {
return &CanvasSearchEngine{service: service}
}
// Search 执行搜索
func (e *CanvasSearchEngine) Search(userID, canvasID string, query CanvasSearchQuery) ([]CanvasSearchResult, error) {
// 获取所有节点
nodes, err := e.service.repo.CanvasNodes(userID, canvasID)
if err != nil {
return nil, fmt.Errorf("get nodes: %w", err)
}
results := make([]CanvasSearchResult, 0)
// 搜索和评分
for _, node := range nodes {
score := e.calculateScore(node, query)
if score > 0 {
highlight := e.generateHighlight(node, query)
results = append(results, CanvasSearchResult{
Node: node,
Score: score,
Highlight: highlight,
})
}
}
// 排序
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
// 限制结果数量
if query.Limit > 0 && len(results) > query.Limit {
results = results[:query.Limit]
}
return results, nil
}
// calculateScore 计算搜索得分
func (e *CanvasSearchEngine) calculateScore(node model.CanvasNode, query CanvasSearchQuery) float64 {
score := 0.0
// 类型匹配
if query.Type != "" && node.Type != query.Type {
return 0
}
// 关键词搜索
if query.Query != "" {
queryLower := strings.ToLower(query.Query)
contentLower := strings.ToLower(node.Content)
// 精确匹配
if strings.Contains(contentLower, queryLower) {
score += 10.0
// 标题匹配(前100字符)
if len(contentLower) > 100 && strings.Contains(contentLower[:100], queryLower) {
score += 5.0
}
// 完整词匹配
words := strings.Fields(contentLower)
for _, word := range words {
if word == queryLower {
score += 3.0
}
}
}
// 模糊匹配(分词)
queryWords := strings.Fields(queryLower)
for _, qWord := range queryWords {
if strings.Contains(contentLower, qWord) {
score += 1.0
}
}
}
// 分类匹配
if query.Category != "" {
ci := e.service.NewCanvasIntelligence()
nodeCategory := ci.categorizeNode(node)
if nodeCategory == query.Category {
score += 5.0
}
}
// 标签匹配
if len(query.Tags) > 0 && node.Metadata != nil {
if nodeTags, ok := node.Metadata["tags"].([]interface{}); ok {
matchedTags := 0
for _, qt := range query.Tags {
for _, nt := range nodeTags {
if ntStr, ok := nt.(string); ok && ntStr == qt {
matchedTags++
}
}
}
score += float64(matchedTags) * 3.0
}
}
return score
}
// generateHighlight 生成高亮摘要
func (e *CanvasSearchEngine) generateHighlight(node model.CanvasNode, query CanvasSearchQuery) string {
if query.Query == "" {
if len(node.Content) > 100 {
return node.Content[:100] + "..."
}
return node.Content
}
queryLower := strings.ToLower(query.Query)
contentLower := strings.ToLower(node.Content)
// 找到匹配位置
index := strings.Index(contentLower, queryLower)
if index == -1 {
// 没找到精确匹配,返回开头
if len(node.Content) > 100 {
return node.Content[:100] + "..."
}
return node.Content
}
// 提取匹配周围的文本
start := index - 50
if start < 0 {
start = 0
}
end := index + len(query.Query) + 50
if end > len(node.Content) {
end = len(node.Content)
}
highlight := ""
if start > 0 {
highlight += "..."
}
highlight += node.Content[start:end]
if end < len(node.Content) {
highlight += "..."
}
return highlight
}
// Canvas Event Handlers - 事件处理器
// handleMessageStart 处理消息开始事件
func (s *Service) handleMessageStart(userID, runID string, data map[string]any) (any, error) {
// 记录消息开始
slog.Debug("agent message_start", "run", runID)
// 更新运行状态
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return nil, err
}
state, err := cloudAgentDecode(run)
if err != nil {
return nil, err
}
// 标记正在生成响应
state.IsGenerating = true
if err := s.saveCloudAgentRuntimeState(userID, runID, &state); err != nil {
return nil, err
}
return map[string]any{"ok": true}, nil
}
// handleMessageDelta 处理消息增量事件
func (s *Service) handleMessageDelta(userID, runID string, data map[string]any) (any, error) {
delta, _ := data["delta"].(string)
messageID, _ := data["messageId"].(string)
slog.Debug("agent message_delta", "run", runID, "message_id", messageID, "len", len(delta))
// 推送实时增量到前端
if err := s.broadcastAgentEvent(userID, runID, "message_delta", map[string]any{
"delta": delta,
"messageId": messageID,
}); err != nil {
slog.Warn("agent message_delta broadcast failed", "run", runID, "error", err)
}
return map[string]any{"ok": true}, nil
}
// handleMessageEnd 处理消息结束事件(关键修复点)
func (s *Service) handleMessageEnd(userID, runID string, data map[string]any) (any, error) {
slog.Debug("agent message_end", "run", runID, "role", data["role"])
// 检查消息角色,防止将用户消息误认为助手消息
role, _ := data["role"].(string)
if role == "user" {
// 这是用户消息结束,不是助手响应,直接返回
slog.Debug("agent message_end ignored", "run", runID, "role", "user")
return map[string]any{"ok": true, "ignored": true, "reason": "user message"}, nil
}
// 只处理助手消息结束
if role != "assistant" {
slog.Debug("agent message_end ignored", "run", runID, "role", role)
return map[string]any{"ok": true, "ignored": true, "reason": fmt.Sprintf("non-assistant role: %s", role)}, nil
}
// 更新运行状态
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return nil, err
}
state, err := cloudAgentDecode(run)
if err != nil {
return nil, err
}
// 标记生成完成
state.IsGenerating = false
// 计数真实的助手响应
state.PiAssistantResponses++
slog.Debug("agent assistant response completed", "run", runID, "count", state.PiAssistantResponses)
if err := s.saveCloudAgentRuntimeState(userID, runID, &state); err != nil {
return nil, err
}
return map[string]any{"ok": true, "assistantResponses": state.PiAssistantResponses}, nil
}
// handleToolCall 处理工具调用事件
func (s *Service) handleToolCall(userID, runID string, data map[string]any) (any, error) {
toolName, _ := data["toolName"].(string)
slog.Debug("agent tool_call", "run", runID, "tool", toolName)
return map[string]any{"ok": true}, nil
}
// handleError 处理错误事件
func (s *Service) handleError(userID, runID string, data map[string]any) (any, error) {
errorMsg, _ := data["error"].(string)
slog.Warn("agent runtime error", "run", runID, "error", errorMsg)
// 更新运行状态为失败
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return nil, err
}
state, err := cloudAgentDecode(run)
if err != nil {
return nil, err
}
state.IsGenerating = false
state.LastError = errorMsg
if err := s.saveCloudAgentRuntimeState(userID, runID, &state); err != nil {
return nil, err
}
return map[string]any{"ok": true}, nil
}
// saveCloudAgentRuntimeState persists only the Pi control fields changed by an
// event handler, merging them into the latest revision before checkpointing.
func (s *Service) saveCloudAgentRuntimeState(userID, runID string, state *cloudAgentRuntime) error {
if state == nil {
return fmt.Errorf("missing Agent runtime state")
}
for attempt := 0; attempt < 4; attempt++ {
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return err
}
err = s.repo.MutateCloudAgent(userID, runID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
fresh, err := cloudAgentDecode(current)
if err != nil {
return err
}
fresh.IsGenerating = state.IsGenerating
fresh.LastError = state.LastError
fresh.PiAssistantResponses = state.PiAssistantResponses
return cloudAgentSave(current, &fresh)
})
if !errors.Is(err, repository.ErrCreationConflict) {
return err
}
}
return repository.ErrCreationConflict
}
func (s *Service) broadcastAgentEvent(userID, runID, kind string, payload map[string]any) error {
for attempt := 0; attempt < 4; attempt++ {
run, err := s.repo.CloudAgent(userID, runID)
if err != nil {
return err
}
err = s.repo.MutateCloudAgent(userID, runID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
state, err := cloudAgentDecode(current)
if err != nil {
return err
}
state.event(runID, kind, payload)
return cloudAgentSave(current, &state)
})
if !errors.Is(err, repository.ErrCreationConflict) {
return err
}
}
return repository.ErrCreationConflict
}
// updatePiRunWithStep 更新运行状态(添加步骤)
func (s *Service) updatePiRunWithStep(userID, runID string, revision int64, stepID string, state *cloudAgentRuntime) error {
return s.repo.MutateCloudAgent(userID, runID, revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
state.ActiveTaskID = stepID
return cloudAgentSave(current, state)
})
}
// routeStandardTool 路由标准工具(非画布工具)
func (s *Service) routeStandardTool(ctx context.Context, userID, runID, toolName string, payload map[string]json.RawMessage) (any, error) {
slog.Debug("agent routing standard tool", "run", runID, "tool", toolName)
// 将参数从 json.RawMessage 转换为 map[string]any
args := make(map[string]any)
for key, rawValue := range payload {
var value any
if err := json.Unmarshal(rawValue, &value); err != nil {
return nil, fmt.Errorf("decode standard tool parameter %q: %w", key, err)
}
args[key] = value
}
// 路由到对应的工具处理器
switch toolName {
case "read_file", "write_file", "edit_file", "delete_file":
return s.handleFileOperation(ctx, userID, runID, toolName, args)
case "list_directory", "search_files", "glob_files":
return s.handleFileSearch(ctx, userID, runID, toolName, args)
case "bash", "shell_command":
return s.handleShellCommand(ctx, userID, runID, args)
case "web_search", "fetch_url":
return s.handleWebOperation(ctx, userID, runID, toolName, args)
default:
return map[string]any{
"success": false,
"error": fmt.Sprintf("unsupported standard tool: %s", toolName),
}, nil
}
}
// handleFileOperation 处理文件操作工具
func (s *Service) handleFileOperation(ctx context.Context, userID, runID, toolName string, args map[string]any) (any, error) {
return nil, fmt.Errorf("standard tool %q is unavailable: file access is not configured", toolName)
}
// handleFileSearch 处理文件搜索工具
func (s *Service) handleFileSearch(ctx context.Context, userID, runID, toolName string, args map[string]any) (any, error) {
return nil, fmt.Errorf("standard tool %q is unavailable: file search is not configured", toolName)
}
// handleShellCommand 处理 Shell 命令工具
func (s *Service) handleShellCommand(ctx context.Context, userID, runID string, args map[string]any) (any, error) {
return nil, errors.New("standard tool shell execution is unavailable: no approval-backed executor is configured")
}
// handleWebOperation 处理 Web 操作工具
func (s *Service) handleWebOperation(ctx context.Context, userID, runID, toolName string, args map[string]any) (any, error) {
return nil, fmt.Errorf("standard tool %q is unavailable: web access is not configured", toolName)
}