fix(docgov): collapse legacy and published task keys in the document list

列表只返回发布键那一行;物化时若旧键行已在,就不再插入兄弟行,避免撞上唯一索引。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-21 06:37:10 +00:00
co-authored by Cursor
parent baca45ab9c
commit 51d4390bd6
3 changed files with 124 additions and 2 deletions
+59
View File
@@ -3,9 +3,14 @@ package docgov
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"chat-agent-system/internal/workflowdef"
"github.com/jackc/pgx/v5"
)
// TemplateDocSpec 是激活后要落到 document_records 的受控文档槽。
@@ -48,6 +53,12 @@ func (e *GovernanceEngine) MaterializeTemplateDocs(ctx context.Context, projectI
func (e *GovernanceEngine) upsertTemplateDoc(ctx context.Context, projectID int64, spec TemplateDocSpec) error {
taskKey := strings.TrimSpace(spec.TaskKey)
docID := ControlledDocID(projectID, taskKey)
if taken, err := e.hasCanonicalSibling(ctx, projectID, taskKey, docID); err != nil {
return err
} else if taken {
// 同一发布键已有另一行(常见于旧键 intake 仍是唯一数据)。不再插入 bd_* 兄弟行。
return nil
}
title := strings.TrimSpace(spec.Title)
if title == "" {
title = taskKey
@@ -155,3 +166,51 @@ func dutyDisplayName(userID int64, name string) string {
}
return ""
}
func siblingTaskKeys(taskKey string) []string {
canon := workflowdef.CanonicalTaskKey(taskKey)
legacy := workflowdef.ImplementationTaskKey(canon)
var out []string
if canon != "" && canon != taskKey {
out = append(out, canon)
}
if legacy != "" && legacy != taskKey && legacy != canon {
out = append(out, legacy)
}
return out
}
func (e *GovernanceEngine) hasCanonicalSibling(ctx context.Context, projectID int64, taskKey, docID string) (bool, error) {
siblings := siblingTaskKeys(taskKey)
if e == nil || len(siblings) == 0 {
return false, nil
}
if e.pool != nil {
var existing string
err := e.pool.QueryRow(ctx, `
SELECT doc_id FROM document_records
WHERE project_id = $1 AND task_key = ANY($2::text[]) AND doc_id <> $3
LIMIT 1
`, projectID, siblings, docID).Scan(&existing)
if errors.Is(err, pgx.ErrNoRows) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("docgov: lookup canonical sibling: %w", err)
}
return existing != "", nil
}
e.mu.RLock()
defer e.mu.RUnlock()
for _, doc := range e.memDocs {
if doc == nil || doc.ProjectID != projectID || doc.DocID == docID {
continue
}
for _, key := range siblings {
if doc.TaskKey == key {
return true, nil
}
}
}
return false, nil
}
+5 -2
View File
@@ -98,7 +98,10 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
}
out = append(out, item)
}
return out, rows.Err()
if err := rows.Err(); err != nil {
return nil, err
}
return collapseWorkflowDocuments(out), nil
}
e.mu.Lock()
@@ -130,7 +133,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
})
}
}
return out, nil
return collapseWorkflowDocuments(out), nil
}
// ReadDocumentContent 读取受控文档 output.md 并返回 typed ContentError。
+60
View File
@@ -0,0 +1,60 @@
package docgov
import (
"sort"
"chat-agent-system/internal/workflowdef"
)
// collapseWorkflowDocuments 把同一发布键的旧键行与新键行收成一行。
// 发布键优先;都是发布键时保留已脱离模板的那条;再按 doc_id 稳定选取。
func collapseWorkflowDocuments(docs []WorkflowDocumentSummary) []WorkflowDocumentSummary {
if len(docs) < 2 {
return docs
}
best := make(map[string]WorkflowDocumentSummary, len(docs))
for _, doc := range docs {
canon := workflowdef.CanonicalTaskKey(doc.TaskKey)
if canon == "" {
continue
}
prev, ok := best[canon]
if !ok {
best[canon] = doc
continue
}
best[canon] = preferWorkflowDocument(prev, doc)
}
out := make([]WorkflowDocumentSummary, 0, len(best))
for _, doc := range best {
out = append(out, doc)
}
sort.Slice(out, func(i, j int) bool {
if out[i].TaskKey == out[j].TaskKey {
return out[i].DocID < out[j].DocID
}
return out[i].TaskKey < out[j].TaskKey
})
return out
}
func preferWorkflowDocument(current, candidate WorkflowDocumentSummary) WorkflowDocumentSummary {
currentPublished := current.TaskKey == workflowdef.CanonicalTaskKey(current.TaskKey)
candidatePublished := candidate.TaskKey == workflowdef.CanonicalTaskKey(candidate.TaskKey)
if currentPublished != candidatePublished {
if candidatePublished {
return candidate
}
return current
}
if current.IsTemplateOnly != candidate.IsTemplateOnly {
if candidate.IsTemplateOnly {
return current
}
return candidate
}
if candidate.DocID < current.DocID {
return candidate
}
return current
}