package server import ( "context" "fmt" "net/http" "strconv" "strings" "chat-agent-system/internal/projectworkflow" ) // filterLineageEdges 丢掉不属于该项目,或任一端文档对当前主体不可见的边。 // 不可见端整行省略,避免通过 ID、标题或数量泄漏。 func filterLineageEdges(rows []*projectworkflow.DocumentLineage, projectID int64, visible func(docID string) bool) []*projectworkflow.DocumentLineage { out := make([]*projectworkflow.DocumentLineage, 0, len(rows)) for _, row := range rows { if row == nil || row.ProjectID != projectID { continue } if visible == nil || !visible(row.DownstreamDocID) || !visible(row.UpstreamDocID) { continue } out = append(out, row) } return out } func writeEmptyLineage(w http.ResponseWriter) { writeJSONResponse(w, http.StatusOK, []*projectworkflow.DocumentLineage{}) } // lineageDocVisible 判断 docID 是否属于 projectID,且当前用户可查看其任务。 // 无法证明归属时返回 false,调用方应给出与“没有血缘”相同的空结果。 func (s *Server) lineageDocVisible(ctx context.Context, projectID, userID int64, docID string) bool { docID = strings.TrimSpace(docID) if s == nil || projectID <= 0 || docID == "" { return false } taskKey, placed, ok := s.lineageDocPlacement(ctx, projectID, docID) if !ok || !placed { return false } if taskKey == "" { return true } return s.allowWorkflowDocTaskAccess(ctx, projectID, userID, taskKey) } func (s *Server) lineageDocPlacement(ctx context.Context, projectID int64, docID string) (taskKey string, placed bool, ok bool) { if s.gov() != nil { doc, err := s.gov().GetDocument(ctx, docID) if err != nil { if status, mapped := domainHTTPStatus(err); !(mapped && status == http.StatusNotFound) { return "", false, false } } if err == nil && doc != nil { if doc.ProjectID != projectID { return "", false, true } return doc.TaskKey, true, true } } if pid, task, parsed := parseCanonicalWorkflowDocID(docID); parsed { if pid != projectID { return "", false, true } return task, true, true } if pid, parsed := parseHyphenProjectDocID(docID); parsed { if pid != projectID { return "", false, true } return "", true, true } return "", false, true } func parseHyphenProjectDocID(docID string) (int64, bool) { const prefix = "doc-proj" if !strings.HasPrefix(docID, prefix) { return 0, false } rest := strings.TrimPrefix(docID, prefix) idx := strings.IndexByte(rest, '-') if idx <= 0 || idx >= len(rest)-1 { return 0, false } projectID, err := strconv.ParseInt(rest[:idx], 10, 64) if err != nil || projectID <= 0 { return 0, false } return projectID, true } // lineageTestManagerAlias 仅在两种 test_manager 文档 ID 写法之间互换,不把任意空查询改写成别的文档。 func lineageTestManagerAlias(projectID int64, docID string) (string, bool) { canonical := fmt.Sprintf("doc_proj%d_test_manager", projectID) hyphen := fmt.Sprintf("doc-proj%d-test_manager", projectID) switch docID { case canonical: return hyphen, true case hyphen: return canonical, true default: return "", false } }