Files
Agent-System/internal/replyexport/policy.go
T
WeiCHEN 1fc7d0d01f feat(export): 实现单次智能体回复导出、私密文档保存与隐私防泄露全套闭环 (Phase 0 - Phase 2)
- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API
- Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制
- Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动
- 专项修复:
  1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表
  2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply)
  3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
2026-09-13 02:50:21 +00:00

138 lines
3.8 KiB
Go

package replyexport
import (
"strings"
)
// PolicyOptions 传递判定所需上下文
type PolicyOptions struct {
Role string
Status string // pending, streaming_status, auto_retrying, completed, failed, cancelled
Content string
HasInteractiveCard bool
HasError bool
IsCancelled bool
IsRestricted bool // 内容合规策略是否禁止导出
Revision int
}
// EvaluatePolicy 权威判定一条消息的内容类型与导出能力
func EvaluatePolicy(opts PolicyOptions) (ContentKind, MessageExportCapability) {
revision := opts.Revision
if revision <= 0 {
revision = 1
}
normContent := strings.TrimSpace(opts.Content)
hash := ComputeSha256(normContent)
cap := MessageExportCapability{
Policy: PolicyNone,
Formats: []string{},
BodySelector: "content_only",
ContentRevision: revision,
ContentSha256: hash,
}
// 1. 非助手回复不可导出
if opts.Role != "assistant" {
cap.ReasonCode = ReasonNotAssistant
return KindConversationalText, cap
}
// 2. 异常或已取消状态
if opts.HasError || opts.IsCancelled || opts.Status == "failed" || opts.Status == "cancelled" {
cap.ReasonCode = ReasonErrorOrCancelled
return KindErrorNotice, cap
}
// 3. 流式中或未完成
if opts.Status == "pending" || opts.Status == "streaming_status" || opts.Status == "auto_retrying" {
cap.ReasonCode = ReasonNotCompleted
return KindStatusUpdate, cap
}
// 4. 空正文
if normContent == "" {
if opts.HasInteractiveCard {
cap.ReasonCode = ReasonCardOnly
return KindActionCard, cap
}
cap.ReasonCode = ReasonEmptyBody
return KindStatusUpdate, cap
}
// 5. 内容合规策略拦截
if opts.IsRestricted {
cap.ReasonCode = ReasonContentRestricted
return KindDocumentText, cap
}
// 6. 纯交互卡片与短依赖引导语
if opts.HasInteractiveCard {
// 检查正文是否具备独立阅读价值
isDependentPrompt := isDependentCardPrompt(normContent)
if isDependentPrompt {
cap.ReasonCode = ReasonCardOnly
return KindActionCard, cap
}
// 正文具备独立性,允许导出,且明确仅导出 content
cap.Policy = PolicyFull
cap.Formats = FullFormats()
return KindDocumentText, cap
}
// 7. 极短寒暄或瞬时确认答复:开放 copy_only,不保存为系统文档
if isCasualOrVeryShort(normContent) {
cap.Policy = PolicyCopyOnly
cap.Formats = CopyOnlyFormats()
return KindConversationalText, cap
}
// 8. 常规具备独立结构的助手正文
cap.Policy = PolicyFull
cap.Formats = FullFormats()
return KindDocumentText, cap
}
// isDependentCardPrompt 判断正文是否仅仅是依赖卡片的简短引导语
func isDependentCardPrompt(content string) bool {
runes := []rune(content)
// 超过 80 字符且包含结构化 Markdown 标记的,通常为完整方案说明
if len(runes) > 80 && (strings.Contains(content, "#") || strings.Contains(content, "- ") || strings.Contains(content, "|")) {
return false
}
// 短于 50 字符且匹配经典卡片引导语模式
if len(runes) <= 50 {
lower := strings.ToLower(content)
patterns := []string{
"请确认", "请选择", "请核对", "点击下方", "操作如下",
"是否继续", "请决策", "请审批", "已为您生成以下操作卡",
}
for _, p := range patterns {
if strings.Contains(lower, p) {
return true
}
}
}
return false
}
// isCasualOrVeryShort 判断是否属于无长久沉淀价值的极短寒暄
func isCasualOrVeryShort(content string) bool {
runes := []rune(content)
if len(runes) <= 12 {
shortGreetings := []string{
"好的", "没问题", "收到", "您好", "在的", "请稍候", "不用谢", "再见", "明白",
}
for _, g := range shortGreetings {
if strings.Contains(content, g) {
return true
}
}
}
return false
}