Files

308 lines
10 KiB
Go

package coordinator
import (
"fmt"
"strings"
"time"
"chat-agent-system/internal/intent"
)
// RejectUtteranceIntent 驳回相关话语意图
type RejectUtteranceIntent string
const (
RejectUtteranceQuery RejectUtteranceIntent = "QUERY_REJECT_REASON"
RejectUtteranceExecute RejectUtteranceIntent = "EXECUTE_REJECT"
RejectUtteranceOther RejectUtteranceIntent = "OTHER"
)
// ClassifyRejectUtterance 区分询问驳回原因与发起驳回执行
func ClassifyRejectUtterance(text string) RejectUtteranceIntent {
t := strings.TrimSpace(text)
if t == "" {
return RejectUtteranceOther
}
if !strings.Contains(t, "驳回") && !strings.Contains(t, "退回") {
return RejectUtteranceOther
}
if strings.Contains(t, "被驳回") {
return RejectUtteranceQuery
}
executeMarkers := []string{
"请驳回", "帮我驳回", "把它驳回", "把这份", "把该",
"审核驳回", "批准驳回", "终审驳回", "正式驳回", "执行驳回", "确认驳回",
"驳回这份", "驳回该", "驳回报告", "驳回文档", "予以驳回", "决定驳回",
"我要驳回", "我要退回", "退回编制", "退回修改",
}
for _, m := range executeMarkers {
if strings.Contains(t, m) {
return RejectUtteranceExecute
}
}
if strings.Contains(t, "把") && (strings.Contains(t, "驳回") || strings.Contains(t, "退回")) {
return RejectUtteranceExecute
}
if strings.Contains(t, "将") && (strings.Contains(t, "驳回") || strings.Contains(t, "退回")) {
return RejectUtteranceExecute
}
queryMarkers := []string{
"原因", "为何", "为什么", "怎么回事", "什么情况", "什么意见",
"哪点", "哪些问题", "整改", "意见是什么", "理由", "怎么改", "如何修改",
"查一下驳回", "看看驳回",
}
for _, m := range queryMarkers {
if strings.Contains(t, m) {
return RejectUtteranceQuery
}
}
return RejectUtteranceOther
}
// DocGovCardResolution 角色适配卡片决议
type DocGovCardResolution struct {
Kind string // reject_confirm_review | reject_confirm_final | reject_reason_info | deny_text
Reply string
DenyReason string
Card *intent.InteractiveCardPayload
Expectation *intent.PendingExpectation
}
func roleLabelCN(role string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "author":
return "编制负责人"
case "reviewer":
return "审核负责人"
case "approver":
return "批准负责人"
case "viewer":
return "查阅者"
default:
if role == "" {
return "未指定"
}
return role
}
}
func defaultRejectReasonText(reason string) string {
r := strings.TrimSpace(reason)
if r == "" {
return "(暂未检索到结构化驳回批注,请打开文档批注或联系审核人确认。)"
}
return r
}
func buildRejectReasonInfoCard(docID, docTitle, reason string, includeRevision bool) *intent.InteractiveCardPayload {
actions := []intent.CardAction{
{
ActionID: "ack_reject_reason",
Label: "知道了",
Style: "primary",
IsAffirmativeDefault: true,
Payload: map[string]string{"target_doc": docID},
},
}
if includeRevision {
actions = append(actions, intent.CardAction{
ActionID: "start_revision",
Label: "开始按意见修订",
Style: "subtle",
Payload: map[string]string{"target_doc": docID},
})
}
return &intent.InteractiveCardPayload{
CardID: fmt.Sprintf("card_rej_reason_%d", time.Now().UnixNano()),
CardType: "REMINDER_NOTICE",
Title: "驳回原因说明",
TargetDocID: docID,
TargetDocTitle: fmt.Sprintf("《%s》", docTitle),
ProposedContent: reason,
ImpactRisk: "本卡片为查询说明,不会变更文档生命周期状态",
CurrentStatus: "active",
TTLTurns: 5,
CreatedAt: time.Now(),
Actions: actions,
}
}
func buildRejectConfirmCard(docID, docTitle, section, reason string, final bool) (*intent.InteractiveCardPayload, *intent.PendingExpectation) {
title := "审核驳回与返工确认"
if final {
title = "终审驳回确认"
}
sectionTitle := "概述与通用技术要求"
if section != "" {
sectionTitle = fmt.Sprintf("%s 专项技术指标与合规核验", section)
}
proposed := defaultRejectReasonText(reason)
if reason == "" {
proposed = "请补充关键技术指标论证与合规强标依据"
}
card := &intent.InteractiveCardPayload{
CardID: fmt.Sprintf("card_rej_%d", time.Now().UnixNano()),
CardType: "CONFIRMATION",
Title: title,
TargetDocID: docID,
TargetDocTitle: fmt.Sprintf("《%s》", docTitle),
TargetSectionID: section,
SectionTitle: sectionTitle,
ProposedContent: proposed,
ImpactRisk: "驳回将使文档退回至编制中(drafting),并向编制人派发修订批注",
CurrentStatus: "active",
TTLTurns: 3,
CreatedAt: time.Now(),
Actions: []intent.CardAction{
{
ActionID: "confirm_reject",
Label: "确认驳回退回编制",
Style: "danger",
IsAffirmativeDefault: true,
Payload: map[string]string{
"target_doc": docID,
"reject_reason": proposed,
"section": section,
"target_action": "confirm_reject",
},
},
{
ActionID: "cancel",
Label: "放弃驳回",
Style: "subtle",
Payload: map[string]string{"action": "cancel"},
},
},
}
exp := &intent.PendingExpectation{
ExpectationID: "exp_" + card.CardID,
ExpectedIntent: intent.IntentDocGovProposalDecide,
TargetAction: "confirm_reject",
Prompt: "请确认是否执行驳回操作",
AffirmativeWord: "确认",
DeclineWord: "取消",
ExpiresAt: time.Now().Add(5 * time.Minute),
Payload: card.Actions[0].Payload,
AllowAffirmative: true,
CreatedAt: time.Now(),
DefaultOption: &intent.ExpectationOption{
OptionID: "confirm_reject",
Title: "确认驳回退回编制",
Intent: intent.IntentDocGovProposalDecide,
Slots: card.Actions[0].Payload,
},
}
return card, exp
}
// ResolveRejectCard 按角色×状态×意图解析驳回相关卡片
func ResolveRejectCard(role, lifecycleState, docID, docTitle, section, reason string, utterance RejectUtteranceIntent) DocGovCardResolution {
role = strings.ToLower(strings.TrimSpace(role))
state := strings.ToLower(strings.TrimSpace(lifecycleState))
if docID == "" {
docID = "当前受控文档"
}
if docTitle == "" {
docTitle = docID
}
reasonText := defaultRejectReasonText(reason)
switch utterance {
case RejectUtteranceQuery:
if role == "viewer" {
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "VIEWER_QUERY_ONLY",
Reply: fmt.Sprintf("关于《%s》的驳回情况:%s\n\n您当前为查阅者,可查看说明但无法执行编制修订或审签流转。", docTitle, reasonText),
}
}
card := buildRejectReasonInfoCard(docID, docTitle, reasonText, role == "author" || role == "")
return DocGovCardResolution{
Kind: "reject_reason_info",
Reply: fmt.Sprintf("关于《%s》的驳回说明如下。本说明不会变更文档状态:\n\n%s", docTitle, reasonText),
Card: card,
}
case RejectUtteranceExecute:
if role == "reviewer" && (state == "reviewing" || state == "") {
card, exp := buildRejectConfirmCard(docID, docTitle, section, reason, false)
return DocGovCardResolution{
Kind: "reject_confirm_review",
Reply: fmt.Sprintf("已为您起草针对《%s》的审核驳回流转意见。请审阅下方卡片中的拟注入批注,确认无误后点击「确认驳回退回编制」或回复「确认」。", docTitle),
Card: card,
Expectation: exp,
}
}
if role == "approver" && (state == "approving" || state == "") {
card, exp := buildRejectConfirmCard(docID, docTitle, section, reason, true)
return DocGovCardResolution{
Kind: "reject_confirm_final",
Reply: fmt.Sprintf("已为您起草针对《%s》的终审驳回流转意见。请审阅下方卡片,确认无误后点击「确认驳回退回编制」或回复「确认」。", docTitle),
Card: card,
Expectation: exp,
}
}
if role == "author" || role == "viewer" {
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "UNAUTHORIZED_REJECT_ROLE",
Reply: fmt.Sprintf(
"您当前在《%s》中的身份为「%s」,不具备审核/终审驳回权限。若需了解驳回原因,可直接询问「被驳回的原因是什么」。",
docTitle, roleLabelCN(role),
),
}
}
if role == "reviewer" {
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "INVALID_STATE_FOR_REVIEW_REJECT",
Reply: fmt.Sprintf("当前受控文档《%s》处于【%s】阶段,非待审核(reviewing)状态,无法执行审核驳回。", docTitle, state),
}
}
if role == "approver" {
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "INVALID_STATE_FOR_FINAL_REJECT",
Reply: fmt.Sprintf("当前受控文档《%s》处于【%s】阶段,非终审(approving)状态,无法执行终审驳回。", docTitle, state),
}
}
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "UNAUTHORIZED_OR_INVALID_STATE",
Reply: fmt.Sprintf("无法为当前身份「%s」在状态【%s】下执行驳回。", roleLabelCN(role), state),
}
default:
if role == "author" || role == "viewer" || role == "" {
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "NON_EXECUTOR_AMBIGUOUS",
Reply: fmt.Sprintf(
"您当前是「%s」。若询问《%s》被驳回的原因,可直接说明;系统不会在此身份下发起审核驳回确认。\n\n已知驳回意见摘要:%s",
roleLabelCN(role), docTitle, reasonText,
),
}
}
return DocGovCardResolution{
Kind: "deny_text",
DenyReason: "AMBIGUOUS_REJECT_UTTERANCE",
Reply: fmt.Sprintf("您提到了「驳回」。若要执行驳回,请明确说明驳回意见;若只是查询原因,请直接询问「被驳回的原因是什么」。"),
}
}
}
// effectiveDocRole 从用户上下文推导文档级角色
func effectiveDocRole(userCtx *UserProjectContext) string {
if userCtx == nil {
return ""
}
if userCtx.User.ProjectRole != "" {
return strings.ToLower(strings.TrimSpace(userCtx.User.ProjectRole))
}
return strings.ToLower(strings.TrimSpace(userCtx.User.Role))
}