Files
Agent-System/internal/app/trace_bridge.go
T
weichen 2e56ba9719 feat(docgov): complete review lifecycle gaps for lock/notify/comment/reopen
Defer locked-period supplements to a pending ledger, route duty-holder
notifications, persist stage-scoped comments, and prefer Approver for reopen.
2026-09-25 20:08:56 +00:00

181 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
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 app
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"chat-agent-system/internal/app/docnotify"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/traceimpact"
)
type traceRecheck struct{ svc *traceimpact.Service }
func (t traceRecheck) StageNeedsAck(ctx context.Context, projectID int64, stageKey string) (bool, error) {
if t.svc == nil {
return false, nil
}
return t.svc.NeedsRecheckAck(ctx, projectID, stageKey)
}
func (t traceRecheck) Acknowledge(ctx context.Context, projectID int64, stageKey string, userID int64) error {
if t.svc == nil {
return nil
}
return t.svc.AcknowledgeRecheck(ctx, projectID, stageKey, userID)
}
type traceStage struct{ inst *projectinstance.Service }
func (t traceStage) ReopenCompletedStage(ctx context.Context, instanceID int64, stageKey string, docTaskKeys []string, operatorID int64) error {
if t.inst == nil {
return fmt.Errorf("trace: instance service unavailable")
}
return t.inst.ReopenStage(ctx, instanceID, stageKey, docTaskKeys, operatorID)
}
type traceUnlock struct{ gov *docgov.GovernanceEngine }
func (t traceUnlock) ReopenApprovedDocs(ctx context.Context, projectID int64, docTaskKeys []string, actorID int64, reason string) error {
if t.gov == nil {
return nil
}
actorName := "reopen"
if actorID > 0 {
actorName = fmt.Sprintf("user:%d", actorID)
}
for _, key := range docTaskKeys {
docID := fmt.Sprintf("doc_proj%d_%s", projectID, key)
_, err := t.gov.ReopenForRevision(ctx, docID, actorID, actorName, reason)
if err == nil || errors.Is(err, vocabulary.ErrDocumentNotFound) {
continue
}
if errors.Is(err, vocabulary.ErrInvalidStateTransition) {
continue
}
return err
}
return nil
}
type traceChat struct{ svc *traceimpact.Service }
func (t traceChat) AnalyzeImpact(ctx context.Context, projectID, userID int64, text string) (string, error) {
if t.svc == nil {
return "", fmt.Errorf("trace: unavailable")
}
ctx = traceimpact.WithActor(ctx, userID)
rep, err := t.svc.Analyze(ctx, projectID, traceimpact.DocRef{DocID: "chat", Markdown: text, Visible: true}, nil)
if err != nil {
return "", err
}
return formatImpactReply(rep), nil
}
func formatImpactReply(rep *traceimpact.Report) string {
if rep == nil || len(rep.Impacts) == 0 {
if rep.Summary != "" {
return rep.Summary
}
return "目前没有已确认的影响。可以说「分析影响」并带上条目编号,例如 UR-001。"
}
var b strings.Builder
if rep.Summary != "" {
b.WriteString(rep.Summary)
b.WriteString("。")
}
limit := len(rep.Impacts)
if limit > 8 {
limit = 8
}
for i := 0; i < limit; i++ {
imp := rep.Impacts[i]
label := imp.ItemKey
if label == "" {
label = imp.DocID
}
if label == "" {
continue
}
b.WriteString("\n- ")
b.WriteString(label)
if imp.Reason != "" {
b.WriteString(":")
b.WriteString(imp.Reason)
}
}
if len(rep.Impacts) > limit {
fmt.Fprintf(&b, "\n另有 %d 条未展开。", len(rep.Impacts)-limit)
}
return b.String()
}
func projectIDFromControlledDoc(docID string) int64 {
rest := strings.TrimPrefix(docID, "doc_proj")
if rest == docID {
return 0
}
i := strings.Index(rest, "_")
if i <= 0 {
return 0
}
n, err := strconv.ParseInt(rest[:i], 10, 64)
if err != nil {
return 0
}
return n
}
func bindRecheckChat(svc *traceimpact.Service, chat *ctxmgr.ContextManager) {
if svc == nil || chat == nil {
return
}
svc.SetRecheckHook(func(ctx context.Context, mark traceimpact.RecheckMark) {
userID := traceimpact.ActorID(ctx)
reason := mark.Reason
if reason == "" {
reason = mark.SourceDoc
}
content := fmt.Sprintf("阶段 %s 待复核(%s)。需要细节可以说「分析影响」。", mark.StageKey, reason)
card := map[string]any{
"cardId": fmt.Sprintf("recheck-%d", mark.ID),
"cardType": "RECHECK_NOTICE",
"title": "阶段待复核",
"proposedContent": content,
"status": "active",
"ttlTurns": 8,
"actions": []any{
map[string]any{
"actionId": "recheck_no_change",
"label": "无需变更",
"style": "primary",
"payload": map[string]string{
"project_id": strconv.FormatInt(mark.ProjectID, 10),
"mark_id": strconv.FormatInt(mark.ID, 10),
},
},
},
}
_ = docnotify.PostProjectNotice(ctx, chat, mark.ProjectID, userID, content, map[string]any{
"interactive_card": card,
})
})
}
func (t traceChat) ProposeRevision(ctx context.Context, projectID, userID int64, text string, confirmed bool) (string, error) {
if t.svc == nil {
return "", fmt.Errorf("trace: unavailable")
}
id, err := t.svc.ProposeRevision(ctx, projectID, userID, text, confirmed)
if errors.Is(err, traceimpact.ErrNeedsConfirm) {
return "", coordinator.ErrImpactNeedsConfirm
}
return id, err
}