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)
|
||
}
|