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

748 lines
21 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"
"fmt"
"log"
"sort"
"strings"
"time"
"yingce/backend/internal/model"
)
// generateIntelligence 生成智能洞察
func (ci *CanvasIntelligence) generateIntelligence(nodes []model.CanvasNode, snapshot CanvasSnapshot) CanvasIntelligenceInsights {
return CanvasIntelligenceInsights{
Summary: ci.generateSummary(nodes, snapshot),
Suggestions: ci.generateSuggestions(nodes, snapshot),
Patterns: ci.recognizePatterns(nodes, snapshot),
Issues: ci.diagnoseIssues(nodes, snapshot),
Trends: ci.analyzeTrends(nodes),
}
}
// generateSummary 生成画布总结
func (ci *CanvasIntelligence) generateSummary(nodes []model.CanvasNode, snapshot CanvasSnapshot) CanvasSummary {
// 提取主要主题
mainTopics := ci.extractMainTopics(nodes)
// 评估进度
progress := ci.assessProgress(nodes)
// 计算完整性
completeness := ci.calculateCompleteness(nodes, snapshot)
// 生成一句话总结
oneSentence := ci.generateOneSentenceSummary(nodes, snapshot, mainTopics)
return CanvasSummary{
OneSentence: oneSentence,
MainTopics: mainTopics,
Progress: progress,
Completeness: completeness,
}
}
// extractMainTopics 提取主要主题
func (ci *CanvasIntelligence) extractMainTopics(nodes []model.CanvasNode) []string {
// 收集所有关键词
allKeywords := make(map[string]int)
for _, node := range nodes {
keywords := ci.extractKeywords(node.Content)
for _, kw := range keywords {
allKeywords[kw]++
}
}
// 排序取前3个
type kv struct {
Key string
Value int
}
var sorted []kv
for k, v := range allKeywords {
sorted = append(sorted, kv{k, v})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Value > sorted[j].Value
})
topics := make([]string, 0, 3)
for i := 0; i < len(sorted) && i < 3; i++ {
topics = append(topics, sorted[i].Key)
}
return topics
}
// assessProgress 评估进度
func (ci *CanvasIntelligence) assessProgress(nodes []model.CanvasNode) string {
if len(nodes) < 5 {
return "early"
}
// 统计完成状态
completed := 0
total := 0
for _, node := range nodes {
category := ci.categorizeNode(node)
if category == "task" {
total++
sentiment := ci.analyzeSentiment(node.Content)
if sentiment == "positive" {
completed++
}
}
}
if total == 0 {
// 没有任务节点,根据节点数量判断
if len(nodes) < 10 {
return "early"
} else if len(nodes) < 30 {
return "mid"
} else {
return "mature"
}
}
completionRate := float64(completed) / float64(total)
if completionRate < 0.3 {
return "early"
} else if completionRate < 0.7 {
return "mid"
} else {
return "mature"
}
}
// calculateCompleteness 计算完整性
func (ci *CanvasIntelligence) calculateCompleteness(nodes []model.CanvasNode, snapshot CanvasSnapshot) float64 {
score := 0.0
// 1. 节点数量(20%)
if len(nodes) > 0 {
score += 0.2
}
// 2. 结构完整性(20%)
if len(snapshot.Hierarchy.RootNodes) > 0 {
score += 0.2
}
// 3. 内容质量(30%)
contentScore := 0.0
for _, node := range nodes {
if len(node.Content) > 50 {
contentScore += 1.0
}
}
if len(nodes) > 0 {
contentScore /= float64(len(nodes))
score += contentScore * 0.3
}
// 4. 关系完整性(30%)
if len(snapshot.Relationships) > 0 {
relationshipRatio := float64(len(snapshot.Relationships)) / float64(len(nodes))
if relationshipRatio > 1 {
relationshipRatio = 1
}
score += relationshipRatio * 0.3
}
return score
}
// generateOneSentenceSummary 生成一句话总结
func (ci *CanvasIntelligence) generateOneSentenceSummary(nodes []model.CanvasNode, snapshot CanvasSnapshot, mainTopics []string) string {
nodeCount := len(nodes)
progress := ci.assessProgress(nodes)
topicsStr := "various topics"
if len(mainTopics) > 0 {
topicsStr = strings.Join(mainTopics, ", ")
}
progressMap := map[string]string{
"early": "刚开始",
"mid": "进行中",
"mature": "接近完成",
}
return fmt.Sprintf("包含 %d 个节点的画布,主题:%s,状态:%s", nodeCount, topicsStr, progressMap[progress])
}
// generateSuggestions 生成行动建议
func (ci *CanvasIntelligence) generateSuggestions(nodes []model.CanvasNode, snapshot CanvasSnapshot) []ActionSuggestion {
suggestions := make([]ActionSuggestion, 0)
// 1. 孤立节点建议
orphans := ci.findOrphanNodes(nodes, snapshot)
if len(orphans) > 0 {
suggestions = append(suggestions, ActionSuggestion{
Priority: 3,
Action: "连接孤立节点",
Reason: fmt.Sprintf("发现 %d 个孤立节点,建立关联可以增强结构", len(orphans)),
Impact: "提升画布结构完整性",
})
}
// 2. 布局优化建议
if snapshot.Layout.Density > 0.8 {
suggestions = append(suggestions, ActionSuggestion{
Priority: 2,
Action: "优化布局密度",
Reason: "当前布局密度过高,可能影响可读性",
Impact: "改善视觉体验和导航效率",
})
}
// 3. 内容完善建议
emptyNodes := ci.findEmptyOrShortNodes(nodes)
if len(emptyNodes) > 0 {
suggestions = append(suggestions, ActionSuggestion{
Priority: 4,
Action: "完善节点内容",
Reason: fmt.Sprintf("发现 %d 个内容较少的节点", len(emptyNodes)),
Impact: "提高画布信息密度",
})
}
// 4. 分类整理建议
if len(nodes) > 20 && len(snapshot.Layout.Clusters) < 3 {
suggestions = append(suggestions, ActionSuggestion{
Priority: 3,
Action: "添加分类节点",
Reason: "节点数量较多但缺少明确分类",
Impact: "改善信息架构和可维护性",
})
}
// 5. 任务跟进建议
incompleteTasks := ci.findIncompleteTasks(nodes)
if len(incompleteTasks) > 0 {
suggestions = append(suggestions, ActionSuggestion{
Priority: 5,
Action: "跟进待办任务",
Reason: fmt.Sprintf("有 %d 个待办任务需要处理", len(incompleteTasks)),
Impact: "推进工作进度",
})
}
// 按优先级排序
sort.Slice(suggestions, func(i, j int) bool {
return suggestions[i].Priority > suggestions[j].Priority
})
return suggestions
}
// recognizePatterns 识别模式
func (ci *CanvasIntelligence) recognizePatterns(nodes []model.CanvasNode, snapshot CanvasSnapshot) []PatternRecognition {
patterns := make([]PatternRecognition, 0)
// 1. 思维导图模式
if ci.isMindMapPattern(snapshot) {
patterns = append(patterns, PatternRecognition{
Pattern: "mindmap",
Confidence: 0.85,
Evidence: snapshot.Hierarchy.RootNodes,
Description: "检测到思维导图结构:以中心主题向外扩展",
})
}
// 2. 看板模式
if ci.isKanbanPattern(snapshot) {
patterns = append(patterns, PatternRecognition{
Pattern: "kanban",
Confidence: 0.75,
Evidence: []string{},
Description: "检测到看板模式:按阶段组织的任务流",
})
}
// 3. 时间线模式
if ci.isTimelinePattern(nodes, snapshot) {
patterns = append(patterns, PatternRecognition{
Pattern: "timeline",
Confidence: 0.7,
Evidence: []string{},
Description: "检测到时间线模式:按时间顺序排列的事件",
})
}
// 4. 层级结构模式
if snapshot.Hierarchy.MaxDepth >= 3 {
patterns = append(patterns, PatternRecognition{
Pattern: "hierarchy",
Confidence: 0.8,
Evidence: snapshot.Hierarchy.RootNodes,
Description: fmt.Sprintf("检测到层级结构:最大深度 %d 层", snapshot.Hierarchy.MaxDepth),
})
}
// 5. 协作模式
if ci.isCollaborativePattern(nodes) {
patterns = append(patterns, PatternRecognition{
Pattern: "collaborative",
Confidence: 0.65,
Evidence: []string{},
Description: "检测到协作模式:多人参与编辑",
})
}
return patterns
}
// diagnoseIssues 诊断问题
func (ci *CanvasIntelligence) diagnoseIssues(nodes []model.CanvasNode, snapshot CanvasSnapshot) []CanvasIssue {
issues := make([]CanvasIssue, 0)
// 1. 孤立节点
orphans := ci.findOrphanNodes(nodes, snapshot)
if len(orphans) > 0 {
issues = append(issues, CanvasIssue{
Severity: "medium",
Type: "orphan",
Description: fmt.Sprintf("发现 %d 个孤立节点,没有与其他节点建立关系", len(orphans)),
AffectedNodes: orphans,
FixSuggestion: "使用 canvas_relationship_create 工具建立关联",
})
}
// 2. 重复内容
duplicates := ci.findDuplicateNodes(nodes)
if len(duplicates) > 0 {
issues = append(issues, CanvasIssue{
Severity: "low",
Type: "duplicate",
Description: fmt.Sprintf("发现 %d 组可能重复的节点", len(duplicates)),
AffectedNodes: ci.flattenDuplicates(duplicates),
FixSuggestion: "合并或删除重复节点",
})
}
// 3. 空节点
emptyNodes := ci.findEmptyOrShortNodes(nodes)
if len(emptyNodes) > 3 {
issues = append(issues, CanvasIssue{
Severity: "low",
Type: "incomplete",
Description: fmt.Sprintf("发现 %d 个内容较少的节点", len(emptyNodes)),
AffectedNodes: emptyNodes,
FixSuggestion: "补充节点内容或删除无用节点",
})
}
// 4. 布局问题
if snapshot.Layout.Density > 0.9 {
issues = append(issues, CanvasIssue{
Severity: "medium",
Type: "layout",
Description: "节点密度过高,可能影响可读性",
AffectedNodes: []string{},
FixSuggestion: "使用 canvas_layout 工具重新布局",
})
}
// 5. 深度过深
if snapshot.Hierarchy.MaxDepth > 5 {
issues = append(issues, CanvasIssue{
Severity: "low",
Type: "hierarchy",
Description: fmt.Sprintf("层级深度 %d 过深,可能难以导航", snapshot.Hierarchy.MaxDepth),
AffectedNodes: []string{},
FixSuggestion: "考虑扁平化结构或添加中间层级",
})
}
return issues
}
// analyzeTrends 分析趋势
func (ci *CanvasIntelligence) analyzeTrends(nodes []model.CanvasNode) TrendAnalysis {
// 计算最近活动
recentActivity := ci.calculateRecentActivity(nodes)
// 计算增长率(简化实现:基于节点创建时间)
growthRate := ci.calculateGrowthRate(nodes)
// 识别热门区域
hotAreas := ci.identifyHotAreas(nodes)
// 协作地图
collaborationMap := ci.buildCollaborationMap(nodes)
return TrendAnalysis{
RecentActivity: recentActivity,
GrowthRate: growthRate,
HotAreas: hotAreas,
CollaborationMap: collaborationMap,
}
}
// 辅助方法
func (ci *CanvasIntelligence) findOrphanNodes(nodes []model.CanvasNode, snapshot CanvasSnapshot) []string {
connected := make(map[string]bool)
// 标记有关系的节点
for _, rel := range snapshot.Relationships {
connected[rel.From] = true
connected[rel.To] = true
}
// 找出孤立节点
orphans := make([]string, 0)
for _, node := range nodes {
if !connected[node.ID] && node.ParentID == nil && len(node.Children) == 0 {
orphans = append(orphans, node.ID)
}
}
return orphans
}
func (ci *CanvasIntelligence) findDuplicateNodes(nodes []model.CanvasNode) [][]string {
// 简化实现:基于内容哈希
contentMap := make(map[string][]string)
for _, node := range nodes {
if len(node.Content) < 10 {
continue
}
hash := hashContentSHA256(node.Content)
contentMap[hash] = append(contentMap[hash], node.ID)
}
duplicates := make([][]string, 0)
for _, ids := range contentMap {
if len(ids) > 1 {
duplicates = append(duplicates, ids)
}
}
return duplicates
}
func (ci *CanvasIntelligence) flattenDuplicates(duplicates [][]string) []string {
result := make([]string, 0)
for _, group := range duplicates {
result = append(result, group...)
}
return result
}
func (ci *CanvasIntelligence) findEmptyOrShortNodes(nodes []model.CanvasNode) []string {
result := make([]string, 0)
for _, node := range nodes {
if len(strings.TrimSpace(node.Content)) < 10 {
result = append(result, node.ID)
}
}
return result
}
func (ci *CanvasIntelligence) findIncompleteTasks(nodes []model.CanvasNode) []string {
result := make([]string, 0)
for _, node := range nodes {
category := ci.categorizeNode(node)
sentiment := ci.analyzeSentiment(node.Content)
if category == "task" && sentiment == "todo" {
result = append(result, node.ID)
}
}
return result
}
func (ci *CanvasIntelligence) isMindMapPattern(snapshot CanvasSnapshot) bool {
// 思维导图特征:单一根节点,多层级
return len(snapshot.Hierarchy.RootNodes) == 1 && snapshot.Hierarchy.MaxDepth >= 2
}
func (ci *CanvasIntelligence) isKanbanPattern(snapshot CanvasSnapshot) bool {
// 看板特征:多个根节点(列),每个根节点下有多个子节点(卡片)
if len(snapshot.Hierarchy.RootNodes) < 2 {
return false
}
// 检查每个根节点是否有子节点
for _, rootID := range snapshot.Hierarchy.RootNodes {
if treeNode, ok := snapshot.Hierarchy.Tree[rootID]; ok {
if len(treeNode.Children) == 0 {
return false
}
}
}
return true
}
func (ci *CanvasIntelligence) isTimelinePattern(nodes []model.CanvasNode, snapshot CanvasSnapshot) bool {
// 时间线特征:节点按时间顺序排列(X轴)
if len(nodes) < 3 {
return false
}
// 检查节点是否按X坐标排序
positions := make([]float64, 0)
timestamps := make([]int64, 0)
for _, node := range nodes {
if node.Position != nil {
positions = append(positions, node.Position.X)
timestamps = append(timestamps, node.CreatedAt.Unix())
}
}
if len(positions) < 3 {
return false
}
// 检查位置和时间是否相关
posIncreasing := true
timeIncreasing := true
for i := 1; i < len(positions); i++ {
if positions[i] < positions[i-1] {
posIncreasing = false
}
if timestamps[i] < timestamps[i-1] {
timeIncreasing = false
}
}
return posIncreasing && timeIncreasing
}
func (ci *CanvasIntelligence) isCollaborativePattern(nodes []model.CanvasNode) bool {
// 协作特征:多个不同的更新者
updaters := make(map[string]bool)
for _, node := range nodes {
if node.Metadata != nil {
if updatedBy, ok := node.Metadata["updatedBy"].(string); ok && updatedBy != "" {
updaters[updatedBy] = true
}
}
}
return len(updaters) >= 2
}
func (ci *CanvasIntelligence) calculateRecentActivity(nodes []model.CanvasNode) ActivityLevel {
now := time.Now()
recentUpdates := 0
for _, node := range nodes {
if now.Sub(node.UpdatedAt) < 1*time.Hour {
recentUpdates++
}
}
level := "low"
if recentUpdates > 10 {
level = "high"
} else if recentUpdates > 3 {
level = "medium"
}
lastUpdated := int64(0)
for _, node := range nodes {
if node.UpdatedAt.Unix() > lastUpdated {
lastUpdated = node.UpdatedAt.Unix()
}
}
return ActivityLevel{
Level: level,
LastUpdated: lastUpdated,
UpdateCount: recentUpdates,
}
}
func (ci *CanvasIntelligence) calculateGrowthRate(nodes []model.CanvasNode) float64 {
if len(nodes) == 0 {
return 0
}
// 简化实现:计算最近1天的增长
now := time.Now()
recentNodes := 0
for _, node := range nodes {
if now.Sub(node.CreatedAt) < 24*time.Hour {
recentNodes++
}
}
return float64(recentNodes) / float64(len(nodes))
}
func (ci *CanvasIntelligence) identifyHotAreas(nodes []model.CanvasNode) []string {
// 简化实现:找出最近更新最频繁的区域(聚类)
recentNodes := make([]model.CanvasNode, 0)
now := time.Now()
for _, node := range nodes {
if node.Position != nil && now.Sub(node.UpdatedAt) < 1*time.Hour {
recentNodes = append(recentNodes, node)
}
}
if len(recentNodes) == 0 {
return []string{}
}
for index := range recentNodes {
if recentNodes[index].Position == nil {
recentNodes[index].Position = &model.CanvasPosition{}
}
}
// 使用聚类找出热门区域
clusterCount := 3
if len(recentNodes) < clusterCount {
clusterCount = len(recentNodes)
}
clusters := ci.simpleKMeans(recentNodes, clusterCount)
hotAreas := make([]string, 0)
for _, cluster := range clusters {
if len(cluster.NodeIDs) > 0 {
hotAreas = append(hotAreas, cluster.Label)
}
}
return hotAreas
}
func (ci *CanvasIntelligence) buildCollaborationMap(nodes []model.CanvasNode) map[string]int {
collaborationMap := make(map[string]int)
for _, node := range nodes {
if node.Metadata != nil {
if updatedBy, ok := node.Metadata["updatedBy"].(string); ok && updatedBy != "" {
collaborationMap[updatedBy]++
}
}
}
return collaborationMap
}
// buildCapabilityMatrix 构建能力矩阵
func (ci *CanvasIntelligence) buildCapabilityMatrix(userID, canvasID string) CanvasCapabilityMatrix {
return CanvasCapabilityMatrix{
SupportedNodeTypes: ci.getSupportedNodeTypes(),
Permissions: ci.getCanvasPermissions(userID, canvasID),
Limits: ci.getCanvasLimits(),
Features: ci.getFeatures(),
Extensions: ci.getExtensions(),
}
}
func (ci *CanvasIntelligence) getSupportedNodeTypes() []NodeTypeCapability {
return []NodeTypeCapability{
{Type: "text", DisplayName: "文本", Description: "纯文本内容", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
{Type: "markdown", DisplayName: "Markdown", Description: "Markdown格式文本", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
{Type: "code", DisplayName: "代码", Description: "代码片段", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
{Type: "image", DisplayName: "图片", Description: "图片节点", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
{Type: "link", DisplayName: "链接", Description: "外部链接", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
{Type: "diagram", DisplayName: "图表", Description: "图表节点", Creatable: true, Operations: []string{"read", "update", "delete", "move"}},
{Type: "mindmap", DisplayName: "思维导图", Description: "思维导图节点", Creatable: true, Operations: []string{"read", "update", "delete", "move"}},
{Type: "task", DisplayName: "任务", Description: "待办任务", Creatable: true, Operations: []string{"read", "update", "delete", "move", "duplicate"}},
}
}
func (ci *CanvasIntelligence) getCanvasPermissions(userID, canvasID string) CanvasPermissions {
// CanvasProjectForUser uses the persisted canvas store and enforces the same
// owner scope as every other Agent canvas operation.
if _, err := ci.service.repo.CanvasProjectForUser(userID, canvasID); err != nil {
log.Printf("[Agent] failed to get canvas permissions for %s: %v", canvasID, err)
return CanvasPermissions{
CanRead: true,
CanCreate: false,
CanUpdate: false,
CanDelete: false,
CanMove: false,
CanShare: false,
RequireApproval: []string{"canvas_node_create", "canvas_node_update", "canvas_node_delete", "canvas_bulk_operation"},
}
}
return CanvasPermissions{CanRead: true, CanCreate: true, CanUpdate: true, CanDelete: true, CanMove: true, CanShare: true, RequireApproval: []string{}}
}
func (ci *CanvasIntelligence) getCanvasLimits() CanvasLimits {
return CanvasLimits{
MaxNodes: 10000,
MaxDepth: 10,
MaxContentSize: 1024 * 1024, // 1MB
MaxBatchSize: 50,
}
}
func (ci *CanvasIntelligence) getFeatures() map[string]bool {
return map[string]bool{
"real_time_collaboration": true,
"version_history": true,
"ai_suggestions": true,
"auto_layout": true,
"search": true,
"export": true,
"templates": true,
}
}
func (ci *CanvasIntelligence) getExtensions() []ExtensionPoint {
return []ExtensionPoint{
{Name: "custom_node_types", Type: "plugin", Enabled: true, Description: "支持自定义节点类型"},
{Name: "webhooks", Type: "hook", Enabled: true, Description: "画布事件webhook"},
{Name: "integrations", Type: "plugin", Enabled: true, Description: "第三方集成"},
}
}
// getRealtimeState 获取实时状态
func (ci *CanvasIntelligence) getRealtimeState(ctx context.Context, document map[string]any) CanvasRealtimeState {
// Realtime presence is optional. Keep the Agent request scoped to the saved
// document instead of querying the unpersisted Canvas extension model.
state := CanvasRealtimeState{
OnlineUsers: []string{},
ActiveCursors: make(map[string]Position),
LockedNodes: []string{},
PendingOperations: 0,
SyncStatus: "synced",
}
metadata, ok := document["metadata"].(map[string]any)
if !ok {
return state
}
if onlineUsers, ok := metadata["onlineUsers"].([]any); ok {
for _, user := range onlineUsers {
if userStr, ok := user.(string); ok {
state.OnlineUsers = append(state.OnlineUsers, userStr)
}
}
}
if cursors, ok := metadata["activeCursors"].(map[string]any); ok {
for userID, cursorData := range cursors {
if cursorMap, ok := cursorData.(map[string]any); ok {
state.ActiveCursors[userID] = Position{X: numberValue(cursorMap["x"], 0), Y: numberValue(cursorMap["y"], 0)}
}
}
}
if lockedNodes, ok := metadata["lockedNodes"].([]any); ok {
for _, node := range lockedNodes {
if nodeStr, ok := node.(string); ok {
state.LockedNodes = append(state.LockedNodes, nodeStr)
}
}
}
if syncStatus, ok := metadata["syncStatus"].(string); ok {
state.SyncStatus = syncStatus
}
return state
}