- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go - backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline44161d2areproduced DATA RACE; now green under -race) - goose 00071 duplicate (dialogue merge1da17a28brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration - task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
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
|
|
}
|
|
}
|