Files
weichenandCursor dba8139d56 feat(workflow): namespace task keys without breaking old instances
Published manifests now use bd_/pm_/tm_/sw_ prefixes so workflows cannot share a doc id. Frozen instances keep the old keys; lookup, templates, and duty checks treat the two names as the same task.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 10:50:25 +00:00

87 lines
2.5 KiB
Go

package bizdev
import (
"context"
"fmt"
"regexp"
"strings"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/workflowdef"
)
var (
legacyDocPPattern = regexp.MustCompile(`^doc-p(\d+)-(.+)$`)
legacyDocProjHyphenPattern = regexp.MustCompile(`^doc-proj(\d+)-(.+)$`)
)
// normalizeDocID 将 legacy 本地 ID 映射为服务端权威 doc_id。
func normalizeDocID(docID string, projectID int64, taskKey string) string {
docID = strings.TrimSpace(docID)
if docID == "" {
return canonicalDocID(projectID, taskKey)
}
if m := legacyDocPPattern.FindStringSubmatch(docID); len(m) == 3 {
return fmt.Sprintf("doc_proj%s_%s", m[1], m[2])
}
if m := legacyDocProjHyphenPattern.FindStringSubmatch(docID); len(m) == 3 {
return fmt.Sprintf("doc_proj%s_%s", m[1], m[2])
}
return docID
}
func canonicalDocID(projectID int64, taskKey string) string {
if projectID <= 0 || taskKey == "" {
return ""
}
return fmt.Sprintf("doc_proj%d_%s", projectID, taskKey)
}
// ResolveAndValidateDocID 解析权威 doc_id;若客户端传入则校验归属。
func (s *Service) ResolveAndValidateDocID(ctx context.Context, req TaskRunRequest) (string, error) {
rawDocID := strings.TrimSpace(req.DocID)
docID := normalizeDocID(rawDocID, req.ProjectID, req.TaskKey)
if docID == "" {
return "", nil
}
if s.docGov == nil {
return docID, nil
}
doc, err := lookupDocument(ctx, s, docID)
if err != nil {
// 尚未建档:与空 doc_id 行为一致,交由 CreateOrGetDocument 创建
if fallback := canonicalDocID(req.ProjectID, req.TaskKey); fallback != "" {
return fallback, nil
}
lookupID := rawDocID
if lookupID == "" {
lookupID = docID
}
return "", fmt.Errorf("doc_id not found: %s", lookupID)
}
if req.ProjectID > 0 && doc.ProjectID != req.ProjectID {
return "", fmt.Errorf("doc_id %s does not belong to project %d", docID, req.ProjectID)
}
// 已注册文档的 task_key 以账本为准。旧键与新键是同一任务,不算改写。
if req.TaskKey != "" && doc.TaskKey != "" && !workflowdef.SameTaskKey(doc.TaskKey, req.TaskKey) {
return "", fmt.Errorf("doc_id %s task_key mismatch: expected %s got %s", docID, req.TaskKey, doc.TaskKey)
}
return doc.DocID, nil
}
func lookupDocument(ctx context.Context, s *Service, docID string) (*docgov.DocumentRecord, error) {
doc, err := s.docGov.GetDocument(ctx, docID)
if err == nil {
return doc, nil
}
for _, alt := range workflowdef.AlternateDocIDs(docID) {
altDoc, altErr := s.docGov.GetDocument(ctx, alt)
if altErr == nil {
return altDoc, nil
}
}
return nil, err
}