Files
Agent-System/internal/app/trace_bridge.go
T
WeiCHEN 381b56dafc refactor(docgov): converge shared lifecycle vocab into contracts
Introduce internal/contracts/vocabulary for state/role/error sentinels,
alias through docgov, and retarget hot production consumers so
coordinator no longer imports docgov for vocabulary-only use.
2026-09-22 21:44:57 +08:00

177 lines
4.8 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
}
for _, key := range docTaskKeys {
docID := fmt.Sprintf("doc_proj%d_%s", projectID, key)
_, err := t.gov.ReopenForRevision(ctx, docID, actorID, "creator", 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
}