Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"chat-agent-system/internal/auth"
|
|
"chat-agent-system/internal/chat"
|
|
"chat-agent-system/internal/coordinator"
|
|
"chat-agent-system/internal/docgov"
|
|
)
|
|
|
|
// bindSessionDocumentSummaryReader 将正文访问绑定到已认证的用户与项目。
|
|
// 读取前先应用与文档列表/正文 API 相同的阶段权限过滤。
|
|
func (s *Server) bindSessionDocumentSummaryReader(sess *chat.Session, user *auth.UserAccount) {
|
|
if sess == nil || sess.UserCtx == nil {
|
|
return
|
|
}
|
|
sess.UserCtx.DocumentSummaryReader = nil
|
|
projectID := sess.ProjectID
|
|
userID := int64(0)
|
|
if user != nil {
|
|
userID = user.ID
|
|
}
|
|
if projectID <= 0 || userID <= 0 || s.gov == nil {
|
|
return
|
|
}
|
|
|
|
sess.UserCtx.DocumentSummaryReader = func(ctx context.Context, target string) (*coordinator.ReadableDocument, error) {
|
|
if s.authorizer == nil {
|
|
return nil, fmt.Errorf("project authorization unavailable")
|
|
}
|
|
if err := s.authorizer.RequireProjectAccessOrAdmin(ctx, projectID, userID, s.isSystemAdminUser(ctx, user)); err != nil {
|
|
return nil, err
|
|
}
|
|
docs, err := s.gov.ListWorkflowDocumentsByProject(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
docs = s.filterWorkflowDocsByStageAccess(ctx, projectID, userID, docs)
|
|
doc, err := selectReadableWorkflowDocument(docs, target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content, err := s.loadControlledDocumentContent(ctx, doc.DocID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &coordinator.ReadableDocument{
|
|
DocID: doc.DocID,
|
|
Title: doc.Title,
|
|
TaskKey: doc.TaskKey,
|
|
LifecycleState: string(doc.LifecycleState),
|
|
Version: doc.Version,
|
|
Markdown: content.Markdown,
|
|
IsTemplateOnly: doc.IsTemplateOnly || content.IsTemplateOnly,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func selectReadableWorkflowDocument(docs []docgov.WorkflowDocumentSummary, target string) (*docgov.WorkflowDocumentSummary, error) {
|
|
if len(docs) == 0 {
|
|
return nil, fmt.Errorf("no readable workflow documents")
|
|
}
|
|
targetKey := normalizeDocumentLookupKey(target)
|
|
if targetKey == "" {
|
|
if len(docs) == 1 {
|
|
return &docs[0], nil
|
|
}
|
|
return nil, fmt.Errorf("document target required")
|
|
}
|
|
|
|
for i := range docs {
|
|
if target == docs[i].DocID || target == docs[i].TaskKey || target == docs[i].Title {
|
|
return &docs[i], nil
|
|
}
|
|
}
|
|
for i := range docs {
|
|
keys := []string{
|
|
normalizeDocumentLookupKey(docs[i].DocID),
|
|
normalizeDocumentLookupKey(docs[i].TaskKey),
|
|
normalizeDocumentLookupKey(docs[i].Title),
|
|
}
|
|
for _, key := range keys {
|
|
if key != "" && (key == targetKey || strings.Contains(key, targetKey) || strings.Contains(targetKey, key)) {
|
|
return &docs[i], nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("document not found in readable scope")
|
|
}
|
|
|
|
func normalizeDocumentLookupKey(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
value = strings.NewReplacer("收集", "", "采集", "", "正文", "", "文档", "", "《", "", "》", "").Replace(value)
|
|
return strings.Map(func(r rune) rune {
|
|
if unicode.IsSpace(r) || unicode.IsPunct(r) {
|
|
return -1
|
|
}
|
|
return r
|
|
}, value)
|
|
}
|