Files
weichen ce694baef1 feat(coordinator): P0 消除对话主链路假成功
claim/submit 经 DocResolver+确认卡后 PendingAction 真写 docgov;proposal 默认分支改澄清;workflow.switch 诚实拒绝(UI专属);workflow.run 确认后走 CreateWorkflowRun(ActionServices.Workflow 由合入时接线)。
2026-09-25 13:19:36 +00:00

167 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package coordinator
import (
"context"
"fmt"
"strings"
"chat-agent-system/internal/contracts/vocabulary"
)
// ResolvedDoc is a uniquely resolved controlled document.
type ResolvedDoc struct {
DocID string
Title string
State vocabulary.DocumentState
TaskKey string
}
// DocListItem is one candidate from the project document inventory.
type DocListItem struct {
DocID string
Title string
State vocabulary.DocumentState
TaskKey string
Aliases []string
}
// DocLister lists workflow documents for a project (typically ListWorkflowDocumentsByProject).
type DocLister func(ctx context.Context, projectID int64) ([]DocListItem, error)
// DocResolver normalizes target_doc references; never silently picks the first match.
type DocResolver interface {
ResolveDoc(ctx context.Context, projectID int64, ref, activeDocID string) (doc *ResolvedDoc, ok bool, candidates []string, err error)
}
// ListDocResolver resolves against a DocLister inventory.
type ListDocResolver struct {
List DocLister
}
// ResolveDoc matches by exact doc_id, then unique title/alias; empty/@active uses activeDocID.
// 0 or multiple title matches → ok=false with candidate labels (never default to first).
func (r *ListDocResolver) ResolveDoc(ctx context.Context, projectID int64, ref, activeDocID string) (*ResolvedDoc, bool, []string, error) {
if r == nil || r.List == nil {
return nil, false, nil, fmt.Errorf("doc resolver not configured")
}
ref = strings.TrimSpace(ref)
activeDocID = strings.TrimSpace(activeDocID)
if ref == "" || ref == "@active" || strings.EqualFold(ref, "当前受控文档") || ref == "当前文档" {
if activeDocID == "" {
items, err := r.List(ctx, projectID)
if err != nil {
return nil, false, nil, err
}
return nil, false, candidateLabels(items), nil
}
ref = activeDocID
}
items, err := r.List(ctx, projectID)
if err != nil {
return nil, false, nil, err
}
if len(items) == 0 {
return nil, false, nil, nil
}
for _, it := range items {
if strings.EqualFold(it.DocID, ref) {
return toResolved(it), true, nil, nil
}
}
normRef := normalizeDocLabel(ref)
var hits []DocListItem
for _, it := range items {
if normalizeDocLabel(it.Title) == normRef || normalizeDocLabel(it.TaskKey) == normRef {
hits = append(hits, it)
continue
}
for _, a := range it.Aliases {
if normalizeDocLabel(a) == normRef {
hits = append(hits, it)
break
}
}
// loose contains only when unique later
if strings.Contains(normalizeDocLabel(it.Title), normRef) || strings.Contains(normRef, normalizeDocLabel(it.Title)) {
if normalizeDocLabel(it.Title) != "" && normRef != "" {
already := false
for _, h := range hits {
if h.DocID == it.DocID {
already = true
break
}
}
if !already {
hits = append(hits, it)
}
}
}
}
if len(hits) == 1 {
return toResolved(hits[0]), true, nil, nil
}
if len(hits) == 0 {
return nil, false, candidateLabels(items), nil
}
return nil, false, candidateLabels(hits), nil
}
func toResolved(it DocListItem) *ResolvedDoc {
return &ResolvedDoc{DocID: it.DocID, Title: it.Title, State: it.State, TaskKey: it.TaskKey}
}
func candidateLabels(items []DocListItem) []string {
out := make([]string, 0, len(items))
for _, it := range items {
label := it.Title
if label == "" {
label = it.DocID
}
if it.DocID != "" && it.Title != "" {
label = fmt.Sprintf("%s(%s)", it.Title, it.DocID)
}
out = append(out, label)
}
return out
}
func normalizeDocLabel(s string) string {
s = strings.TrimSpace(s)
s = strings.Trim(s, "《》「」\"' ")
s = strings.ToLower(s)
s = strings.ReplaceAll(s, " ", "")
s = strings.ReplaceAll(s, " ", "")
return s
}
// PendingSupplementView is a read-only pending ledger row for dialogue answers.
type PendingSupplementView struct {
DocID string
DocTitle string
SourceSummary string
ImpactReason string
CreatedAt string
}
// PendingSupplementReader lists undigested locked-period supplements.
type PendingSupplementReader interface {
ListPendingSupplements(ctx context.Context, projectID int64, docID string) ([]PendingSupplementView, error)
}
// ReopenListItem is a pending reopen request shown for approve selection.
type ReopenListItem struct {
RequestID int64
DocTasks []string
Reason string
Status string
}
// ReopenLister lists pending reopen requests for a project (approve path).
type ReopenLister interface {
ListPendingReopens(ctx context.Context, projectID int64) ([]ReopenListItem, error)
}