Files
Agent-System/internal/intent/recognizer_helpers.go
T
weichen 7bdb045272 fix(intent): 收紧安全红线正则动词与桥接,消除正常撰写请求误拦
裸单字「改/换」加无界桥接会把「整改/角色」等正常撰写句误判为
SECURITY_FORBIDDEN。改为复合动词 + 短窗不跨标点桥接,受控名词须与写动词共现;补误报负例回归。
2026-09-25 12:53:08 +00:00

209 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"regexp"
"strings"
"unicode/utf8"
)
// isSecurityViolation 检查是否命中敏感安全与权限红线。
// 中文无词边界:动词仅用复合形(改成/改为/换成/重置/设为/修改等),桥接限制在不跨子句标点的短窗;
// 受控资产名词(系统配置/知识库连接等)须与写动词共现,避免正常撰写/排查误拦。
func isSecurityViolation(input string) bool {
lower := strings.ToLower(input)
patterns := []string{
"修改身份", "身份修改", "修改职位", "职位修改", "修改岗位", "岗位修改",
"修改姓名", "姓名修改", "修改名字", "名字修改",
"修改部门", "部门修改",
"修改资质", "资质修改",
"改成批准人", "改成审核人", "改成编制人",
"工号改成", "修改工号", "工号修改",
"修改设置", "修改配置", "修改系统设置", "修改系统配置",
"修改网关", "修改网关配置", "修改网关密钥", "修改大模型网关", "修改模型网关",
"修改密码", "重置密码", "提升权限", "越权",
"修改向量库", "修改向量库连接", "修改向量库配置",
"修改知识库连接", "修改知识库配置",
}
for _, p := range patterns {
if strings.Contains(lower, p) {
return true
}
}
// 桥接窗:不跨子句标点(含斜杠),避免「整改/角色」类远距误配。
const bridge = `[一-鿿A-Za-z0-9_、  ]{0,16}`
secRegexes := []string{
`把` + bridge + `(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质)` + bridge + `(?:修改|改成|改为|换成|换为|重置|设为|变为)`,
`把` + bridge + `(?:向量库连接|向量库|知识库连接|大模型网关|模型网关|网关密钥|网关|密钥|apikey|api_key)` + bridge + `(?:修改|改成|改为|换成|换为|重置|设为|变为)`,
`(?:修改|改成|改为|换成|换为|更换|重置|设为)` + bridge + `(?:我的)?(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质|网关|向量库|知识库连接|密钥|apikey|api_key)`,
`(?:修改|改成|改为|换成|换为|更换|重置|设为)` + bridge + `(?:向量库连接|向量库|知识库连接|大模型网关密钥|网关密钥)`,
}
for _, r := range secRegexes {
if matched, _ := regexp.MatchString(r, input); matched {
return true
}
}
return false
}
// isPureUICommand 检查是否为纯前端UI视窗布局与主题控制指令
func isPureUICommand(input string) bool {
patterns := []string{
"打开工具栏", "关闭工具栏",
"折叠侧边栏", "展开侧边栏", "隐藏侧边栏", "显示侧边栏",
"折叠左侧栏", "展开左侧栏", "折叠右侧栏", "展开右侧栏",
"折叠工作台", "展开工作台",
"切换全屏", "全屏显示", "退出全屏",
"打开控制台", "关闭控制台",
"切换主题", "换成深色", "换成浅色",
"打开抽屉", "展开抽屉", "关闭抽屉",
"深色模式", "浅色模式", "深色主题", "浅色主题",
}
for _, p := range patterns {
if strings.Contains(input, p) {
return true
}
}
return false
}
// resolveTargetDoc 优先从输入中提取显式受控文档名称,提取不到时回退至上下文的 ActiveDocID
func resolveTargetDoc(input string, userCtx *ContextSnapshot) string {
if doc := extractDocName(input); doc != "" {
return doc
}
if userCtx != nil && userCtx.ActiveDocID != "" {
return userCtx.ActiveDocID
}
return ""
}
// isRejectReasonQuery 判定是否为询问驳回/退回原因(而非发起驳回执行)
func isRejectReasonQuery(input string) bool {
markers := []string{
"原因", "为何", "为什么", "怎么回事", "什么情况", "什么意见",
"哪点", "哪些问题", "整改", "意见是什么", "理由", "怎么改", "如何修改",
"查一下驳回", "看看驳回",
}
for _, m := range markers {
if strings.Contains(input, m) {
return true
}
}
return strings.Contains(input, "被驳回")
}
// extractFactSlots 从文本中提取结构化事实槽位 (支持补丁合并与补充信息)
func extractFactSlots(input string) map[string]string {
slots := make(map[string]string)
marketPrefixes := []string{
"目标市场不仅是", "目标市场不仅有", "目标市场还包括", "目标市场确定为", "目标市场确定是",
"目标市场改成", "目标市场改为", "目标市场是", "目标市场为",
"市场改成", "市场改为", "市场是", "市场为", "目标市场除了国内还想考虑",
}
for _, p := range marketPrefixes {
if idx := strings.Index(input, p); idx != -1 {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["target_market"] = val
slots[SlotFactKey] = "target_market"
slots[SlotFactValue] = val
break
}
}
}
budgetPrefixes := []string{"预算改成", "预算改为", "预算是", "预算为", "预算只有", "预算大概", "预算"}
for _, p := range budgetPrefixes {
if idx := strings.Index(input, p); idx != -1 && slots["budget"] == "" {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["budget"] = val
slots[SlotFactKey] = "budget"
slots[SlotFactValue] = val
break
}
}
}
usePrefixes := []string{"用途是", "用途为", "预期用途是", "场景是", "场景为"}
for _, p := range usePrefixes {
if idx := strings.Index(input, p); idx != -1 && slots["intended_use"] == "" {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["intended_use"] = val
slots[SlotFactKey] = "intended_use"
slots[SlotFactValue] = val
break
}
}
}
return slots
}
func isGreetingOrHelp(input string) bool {
greetings := []string{"你好", "您好", "早上好", "下午好", "晚上好", "早啊", "在吗", "hi", "hello", "你是谁"}
for _, g := range greetings {
if strings.Contains(strings.ToLower(input), g) {
return true
}
}
return false
}
func sortCandidates(c []CandidateIntent) {
for i := 0; i < len(c)-1; i++ {
for j := i + 1; j < len(c); j++ {
if c[j].Confidence > c[i].Confidence {
c[i], c[j] = c[j], c[i]
}
}
}
}
// IsSmallTalkOrGeneralQuestion 判定是否为通用非业务问句(闲聊/常识提问)。
func IsSmallTalkOrGeneralQuestion(input string) bool {
trimmed := strings.TrimSpace(input)
if trimmed == "" || utf8.RuneCountInString(trimmed) < 3 {
return false
}
if hasStrongBusinessLexicon(trimmed) {
return false
}
questionMarkers := []string{
"怎么样", "好吗", "什么是", "为什么", "怎么", "哪些", "多少",
"吗", "呢", "?", "?",
}
for _, m := range questionMarkers {
if strings.Contains(trimmed, m) {
return true
}
}
return false
}
// HasStrongBusinessLexicon 是否含强业务词典(项目/法规/文档治理等),供闲聊与实时检索判定复用。
func HasStrongBusinessLexicon(input string) bool {
return hasStrongBusinessLexicon(input)
}
func hasStrongBusinessLexicon(input string) bool {
markers := []string{
"项目", "阶段", "推进", "法规", "注册", "文档", "审核", "批准", "驳回",
"报价", "需求", "型检", "成熟度", "工作流", "交付", "立项", "送审", "认领",
"方案成型", "V0.1", "V0.5", "V1.0", "v0.1", "v0.5", "v1.0",
"进度", "风险", "预算", "目标市场", "受控",
}
for _, m := range markers {
if strings.Contains(input, m) {
return true
}
}
return false
}