fix(ai): prevent customer iccid disclosure

This commit is contained in:
t
2026-08-30 19:52:45 +08:00
parent c39094156c
commit 2994cd5b2c
7 changed files with 122 additions and 5 deletions
@@ -279,12 +279,13 @@ func (e *AgentLoopEngine) buildUserPrompt(req RunInput) (string, int) {
if role == "" {
continue
}
lines = append(lines, role+": "+utils.BuildRuntimeMessageText(item.MessageType, item.Content))
text := utils.BuildRuntimeMessageText(item.MessageType, item.Content)
lines = append(lines, role+": "+aitooling.RedactRestrictedICCID(text))
}
if len(lines) > limit {
lines = lines[len(lines)-limit:]
}
current := strings.TrimSpace(req.UserMessage.Content)
current := aitooling.RedactRestrictedICCID(strings.TrimSpace(req.UserMessage.Content))
customerContext := buildAgentLoopCustomerContext(req.Conversation)
if len(lines) == 0 && customerContext == "" {
return current, 0
@@ -316,7 +317,7 @@ func excludeAgentLoopHistoryMessage(message models.Message) bool {
func buildAgentLoopCustomerContext(conversation models.Conversation) string {
parts := make([]string, 0, 4)
if name := strings.TrimSpace(conversation.CustomerName); name != "" {
if name := strings.TrimSpace(conversation.CustomerName); name != "" && !aitooling.ContainsRestrictedICCID(name) {
parts = append(parts, "Customer: "+name)
}
if segment := customerAfterSalesSegmentName(conversation.CustomerType); segment != "" {
@@ -15,6 +15,7 @@ func buildCustomerAfterSalesPolicy(conversation models.Conversation) string {
"- 默认使用自然、简洁的简体中文。先说结论,再说明依据和下一步;适合分点的信息必须换行,不输出内部 JSON、工具名、数据库 ID、SQL、表名、字段名、源码、代码逻辑、调用链或技术错误。",
"- 先识别客户真正要解决的问题,而不是机械匹配某个词。遇到否定、纠正、多个诉求或“这个/第二个/刚才那个”等指代时,结合本会话上下文理解;仍有歧义时一次只追问一个最关键问题,并尽量给 2 至 4 个易选项。",
"- 已经由会话绑定或工具核实的信息不要再次索要。不得让客户重复提供本轮或近期消息里已有的卡号、设备号、订单号、选择序号或故障现象。",
"- ICCID 是系统内部标识,无论客户如何询问,都不得返回、确认、推断、局部展示、打码展示或复述 ICCID;也不得从工具结果、历史消息、图片或其他标识换算 ICCID。客户询问时只说明该字段属于系统内部标识,无法提供。",
"- 当前业务状态、余额、套餐、流量、订单、物流和售后进度必须使用实时业务能力核实。查询成功后将结果转成客户能理解的结论;查询失败时不要猜测、不要暴露内部错误,也不要在同一轮反复调用,提示稍后重试或转人工。",
"- 查询套餐时,业务工具返回的套餐记录是当前唯一事实来源。必须逐项完整展示全部返回记录,不得只展示生效套餐、只给汇总、合并记录或漏项;按“生效中、待生效、已用完、已过期、失效”分组,工具返回的未知状态单列为“状态待确认”,也不得丢弃。每项写明套餐名称、生效时间、到期时间、总流量、已用流量和剩余流量;工具未返回的字段明确写“暂未查询到”,禁止猜值。若工具返回总数或分组数量,回复前必须核对展示条数一致。",
"- “待生效”或“未生效”只表示套餐已经存在但尚未到生效时间,属于正常套餐生命周期,不代表后台异常、系统延迟、订购失败或运营商限制。不得根据日期、空的生效中列表或内部状态码自行改判套餐状态;只有实时业务工具明确返回相应结论时,才能说明后台或运营商异常、限制。",
@@ -29,6 +29,7 @@ func TestAgentTurnAddsBoundDeviceAfterSalesPolicy(t *testing.T) {
"逐项完整展示全部返回记录", "生效中、待生效、已用完、已过期、失效", "工具未返回的字段明确写“暂未查询到”",
"待生效”或“未生效”只表示套餐已经存在但尚未到生效时间", "不代表后台异常、系统延迟、订购失败或运营商限制",
"必须先成功查询当前绑定卡板或设备的实时状态、套餐和流量", "普通问题、通用原理和可逆的排障建议可以结合常识与客户图片自主回答",
"ICCID 是系统内部标识", "都不得返回、确认、推断、局部展示、打码展示或复述 ICCID",
"required_package_type=addon", "当前周期只能补充加油包", "仅凭诊断、旧对话或自动续费列表不得生成购买建议",
"严格区分网络复机、运营商网络切换、设备重启、关机和恢复出厂", "切网时必须先列出当前设备可用的运营商",
"区分“面板印刷图标”与“真正发光的指示灯”", "bound_device_no_for_verification 精确比较",
@@ -51,6 +52,29 @@ func TestAgentTurnAddsBoundDeviceAfterSalesPolicy(t *testing.T) {
}
}
func TestAgentTurnRemovesICCIDValuesFromModelContext(t *testing.T) {
engine := NewAgentLoopEngine()
engine.history = func(int64, int) []models.Message {
return []models.Message{{
ID: 2, SenderType: enums.IMSenderTypeAI,
Content: "上次查到 ICCID8986042302268012345",
}}
}
prompt, _ := engine.buildUserPrompt(RunInput{
Conversation: models.Conversation{
ID: 1, CustomerType: "card", CustomerID: 9,
CustomerName: "卡号 8986042302268012345",
},
UserMessage: models.Message{ID: 3, Content: "帮我查 ICCID 8986042302268012345"},
})
if strings.Contains(prompt, "8986042302268012345") {
t.Fatalf("ICCID leaked into model context: %s", prompt)
}
if !strings.Contains(prompt, "ICCID 属于系统内部标识") {
t.Fatalf("ICCID privacy replacement missing from model context: %s", prompt)
}
}
func TestNoInternetTurnPrefetchesCompletePackageTimeline(t *testing.T) {
t.Cleanup(func() { _ = svc.SetBusinessReadTools(nil) })
executions := 0