Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
247 lines
8.1 KiB
Go
247 lines
8.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/chat"
|
|
"chat-agent-system/internal/coordinator"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/projectinstance"
|
|
"chat-agent-system/internal/projectworkflow"
|
|
"chat-agent-system/internal/traceimpact"
|
|
)
|
|
|
|
// chatReopenAdapter implements chat.ReopenService and coordinator.ReopenLister.
|
|
type chatReopenAdapter struct {
|
|
trace *traceimpact.Service
|
|
gov *docgov.GovernanceEngine
|
|
inst *projectinstance.Service
|
|
pw *projectworkflow.Service
|
|
}
|
|
|
|
func (a *chatReopenAdapter) ListPendingReopens(ctx context.Context, projectID int64) ([]chat.ReopenRequestView, error) {
|
|
if a == nil || a.trace == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := a.trace.ListPendingReopens(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]chat.ReopenRequestView, 0, len(rows))
|
|
for _, r := range rows {
|
|
out = append(out, chat.ReopenRequestView{
|
|
ID: r.ID, DocTasks: r.DocTasks, Reason: r.Reason, Status: r.Status,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (a *chatReopenAdapter) ListPendingReopensForCoord(ctx context.Context, projectID int64) ([]coordinator.ReopenListItem, error) {
|
|
views, err := a.ListPendingReopens(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]coordinator.ReopenListItem, 0, len(views))
|
|
for _, v := range views {
|
|
out = append(out, coordinator.ReopenListItem{
|
|
RequestID: v.ID, DocTasks: v.DocTasks, Reason: v.Reason, Status: v.Status,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Ensure interface satisfaction for coordinator.ReopenLister via thin wrapper.
|
|
type coordReopenLister struct{ inner *chatReopenAdapter }
|
|
|
|
func (c coordReopenLister) ListPendingReopens(ctx context.Context, projectID int64) ([]coordinator.ReopenListItem, error) {
|
|
if c.inner == nil {
|
|
return nil, nil
|
|
}
|
|
return c.inner.ListPendingReopensForCoord(ctx, projectID)
|
|
}
|
|
|
|
func (a *chatReopenAdapter) RequestReopenByDoc(ctx context.Context, projectID int64, taskKey, reason string, userID int64) (chat.ReopenRequestView, error) {
|
|
if a == nil || a.trace == nil {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("reopen service unavailable")
|
|
}
|
|
taskKey = normalizeTaskKey(projectID, taskKey)
|
|
if taskKey == "" || strings.TrimSpace(reason) == "" {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("task_key and reason required")
|
|
}
|
|
creator := false
|
|
if a.pw != nil {
|
|
creator, _ = a.pw.IsProjectCreator(ctx, projectID, userID)
|
|
}
|
|
stageKey, instanceID, seats, err := a.resolveStageForTask(ctx, projectID, userID, taskKey)
|
|
if err != nil {
|
|
return chat.ReopenRequestView{}, err
|
|
}
|
|
if !traceimpact.MayRequestReopen(userID, creator, []string{taskKey}, seats) {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("无权申请重开:须为该文档职责人或项目创建人")
|
|
}
|
|
stored, err := a.trace.RequestReopen(ctx, traceimpact.ReopenRequest{
|
|
ProjectID: projectID, InstanceID: instanceID, StageKey: stageKey,
|
|
DocTasks: []string{taskKey}, Reason: reason, RequestedBy: userID,
|
|
}, creator)
|
|
if err != nil {
|
|
return chat.ReopenRequestView{}, err
|
|
}
|
|
return chat.ReopenRequestView{
|
|
ID: stored.ID, DocTasks: stored.DocTasks, Reason: stored.Reason, Status: stored.Status,
|
|
}, nil
|
|
}
|
|
|
|
func (a *chatReopenAdapter) ApproveReopenByID(ctx context.Context, projectID, requestID, userID int64) (chat.ReopenRequestView, error) {
|
|
if a == nil || a.trace == nil {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("reopen service unavailable")
|
|
}
|
|
existing, err := a.trace.GetReopen(ctx, requestID)
|
|
if err != nil {
|
|
return chat.ReopenRequestView{}, err
|
|
}
|
|
if existing.ProjectID != projectID {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("申请不属于该项目")
|
|
}
|
|
creator := false
|
|
if a.pw != nil {
|
|
creator, _ = a.pw.IsProjectCreator(ctx, projectID, userID)
|
|
}
|
|
if !a.mayApprove(ctx, projectID, userID, creator, existing) {
|
|
return chat.ReopenRequestView{}, fmt.Errorf("无权批准重开:须为文档批准人或(批准人缺位时)项目创建人")
|
|
}
|
|
stored, err := a.trace.ApproveReopen(ctx, requestID, userID)
|
|
if err != nil {
|
|
return chat.ReopenRequestView{}, err
|
|
}
|
|
return chat.ReopenRequestView{
|
|
ID: stored.ID, DocTasks: stored.DocTasks, Reason: stored.Reason, Status: stored.Status,
|
|
}, nil
|
|
}
|
|
|
|
func (a *chatReopenAdapter) mayApprove(ctx context.Context, projectID, userID int64, creator bool, req traceimpact.ReopenRequest) bool {
|
|
hasActiveApprover := false
|
|
userIsApprover := false
|
|
if a.gov != nil {
|
|
for _, taskKey := range req.DocTasks {
|
|
doc, err := a.gov.FindByProjectTaskKey(ctx, projectID, taskKey)
|
|
if err != nil || doc == nil || doc.ApproverID <= 0 {
|
|
continue
|
|
}
|
|
hasActiveApprover = true
|
|
if doc.ApproverID == userID {
|
|
userIsApprover = true
|
|
}
|
|
}
|
|
}
|
|
if userIsApprover {
|
|
return true
|
|
}
|
|
return !hasActiveApprover && creator
|
|
}
|
|
|
|
func (a *chatReopenAdapter) resolveStageForTask(ctx context.Context, projectID, userID int64, taskKey string) (stageKey string, instanceID int64, seats []traceimpact.DutySeat, err error) {
|
|
if a.inst == nil {
|
|
return "", 0, nil, fmt.Errorf("instance service unavailable")
|
|
}
|
|
settings, err := a.inst.GetSettings(ctx, projectID, userID, true)
|
|
if err != nil || settings == nil || settings.Plan == nil {
|
|
return "", 0, nil, fmt.Errorf("无法解析工作流阶段")
|
|
}
|
|
if settings.LiveInstance != nil {
|
|
instanceID = settings.LiveInstance.ID
|
|
}
|
|
for _, stage := range settings.Plan.Stages {
|
|
for _, task := range stage.DocTasks {
|
|
seats = append(seats, traceimpact.DutySeat{
|
|
TaskKey: task.TaskKey, AuthorID: task.Duties.AuthorUserID,
|
|
ReviewerID: task.Duties.ReviewerUserID, ApproverID: task.Duties.ApproverUserID,
|
|
})
|
|
if task.TaskKey == taskKey {
|
|
stageKey = stage.StageKey
|
|
}
|
|
}
|
|
}
|
|
if stageKey == "" {
|
|
return "", 0, nil, fmt.Errorf("文档任务 %s 不在当前工作流规划中", taskKey)
|
|
}
|
|
if instanceID == 0 {
|
|
return "", 0, nil, fmt.Errorf("项目尚无激活工作流实例")
|
|
}
|
|
return stageKey, instanceID, seats, nil
|
|
}
|
|
|
|
func normalizeTaskKey(projectID int64, ref string) string {
|
|
ref = strings.TrimSpace(ref)
|
|
prefix := fmt.Sprintf("doc_proj%d_", projectID)
|
|
if strings.HasPrefix(ref, prefix) {
|
|
return strings.TrimPrefix(ref, prefix)
|
|
}
|
|
return ref
|
|
}
|
|
|
|
// AdaptDocResolver builds a ListDocResolver from GovernanceEngine.
|
|
func AdaptDocResolver(gov *docgov.GovernanceEngine) coordinator.DocResolver {
|
|
if gov == nil {
|
|
return nil
|
|
}
|
|
return &coordinator.ListDocResolver{
|
|
List: func(ctx context.Context, projectID int64) ([]coordinator.DocListItem, error) {
|
|
rows, err := gov.ListWorkflowDocumentsByProject(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]coordinator.DocListItem, 0, len(rows))
|
|
for _, r := range rows {
|
|
out = append(out, coordinator.DocListItem{
|
|
DocID: r.DocID, Title: r.Title, State: r.LifecycleState, TaskKey: r.TaskKey,
|
|
Aliases: []string{r.TaskKey},
|
|
})
|
|
}
|
|
return out, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
type pendingSuppAdapter struct{ gov *docgov.GovernanceEngine }
|
|
|
|
func (p pendingSuppAdapter) ListPendingSupplements(ctx context.Context, projectID int64, docID string) ([]coordinator.PendingSupplementView, error) {
|
|
if p.gov == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := p.gov.ListPendingSupplementsByProject(ctx, projectID, docID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]coordinator.PendingSupplementView, 0, len(rows))
|
|
for _, r := range rows {
|
|
title := r.DocID
|
|
if doc, derr := p.gov.GetDocument(ctx, r.DocID); derr == nil && doc != nil && doc.Title != "" {
|
|
title = doc.Title
|
|
}
|
|
created := ""
|
|
if !r.CreatedAt.IsZero() {
|
|
created = r.CreatedAt.Format(time.RFC3339)
|
|
}
|
|
out = append(out, coordinator.PendingSupplementView{
|
|
DocID: r.DocID, DocTitle: title, SourceSummary: r.SourceSummary,
|
|
ImpactReason: r.ImpactReason, CreatedAt: created,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// WireDocDialogueTriggers injects DocResolver / pending supplements / reopen ports onto the coordinator and returns a chat ReopenService.
|
|
func (r *Runtime) WireDocDialogueTriggers() chat.ReopenService {
|
|
if r == nil || r.Engine == nil {
|
|
return nil
|
|
}
|
|
r.Engine.SetDocResolver(AdaptDocResolver(r.Gov))
|
|
r.Engine.SetPendingSupplementReader(pendingSuppAdapter{gov: r.Gov})
|
|
adapter := &chatReopenAdapter{trace: r.Trace, gov: r.Gov, inst: r.Inst, pw: r.PW}
|
|
r.Engine.SetReopenLister(coordReopenLister{inner: adapter})
|
|
return adapter
|
|
}
|