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.
177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
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
|
||
}
|