739 lines
20 KiB
Go
739 lines
20 KiB
Go
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, ¤tY, spacing)
|
|
} else {
|
|
e.layoutTreeVertical(root, nodeMap, positions, 0, ¤tY, 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)
|
|
}
|