Files
weichen a9c9a9182e refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- 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 (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought 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)
2026-09-25 13:17:22 +00:00

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