- 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 自动识别为原生公式
138 lines
3.8 KiB
Go
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
|
|
}
|