308 lines
10 KiB
Go
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))
|
|
}
|