claim/submit 经 DocResolver+确认卡后 PendingAction 真写 docgov;proposal 默认分支改澄清;workflow.switch 诚实拒绝(UI专属);workflow.run 确认后走 CreateWorkflowRun(ActionServices.Workflow 由合入时接线)。
167 lines
4.5 KiB
Go
167 lines
4.5 KiB
Go
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)
|
||
}
|