package runtime import ( "encoding/json" "fmt" "sort" "strconv" "strings" ) type verifiedAddonOption struct { Sequence int Name string Price string CanPurchase bool Recommended bool UnavailableReason string ReasonCode string } func cloneVerifiedToolResults(input map[string]string) map[string]string { if len(input) == 0 { return make(map[string]string) } result := make(map[string]string, len(input)) for code, value := range input { result[code] = value } return result } const ( cardPackageCatalogToolCode = "business/card_package_catalog" devicePackageCatalogToolCode = "business/device_package_catalog" ) type verifiedPackageCatalogDecision struct { RequiresAddon bool AddonOptions []verifiedAddonOption } // enforceVerifiedPackageReply is the final business safety boundary for // package recommendations. Model instructions remain useful for presentation, // but a probabilistic reply must never override the verified eligibility // result returned by the host system. // // Only the two package-catalog tools are authoritative here. Diagnosis and // unrelated tool payloads may contain similarly named fields, so recursively // searching every tool result would let stale or unrelated facts replace a // valid answer. Once the current-turn catalog says add-ons are mandatory, the // final answer is rendered deterministically instead of trying to recognise a // contradictory Chinese sentence after the fact. func enforceVerifiedPackageReply(reply string, toolResults map[string]string) string { decision, ok := resolveVerifiedPackageCatalogDecision(toolResults) if !ok || !decision.RequiresAddon { return reply } return buildVerifiedAddonReply(decision.AddonOptions) } func resolveVerifiedPackageCatalogDecision(toolResults map[string]string) (verifiedPackageCatalogDecision, bool) { var decision verifiedPackageCatalogDecision foundCatalog := false allRecognizedOptionsAreAddon := true recognizedOptionCount := 0 for _, code := range []string{cardPackageCatalogToolCode, devicePackageCatalogToolCode} { raw, exists := toolResults[code] if !exists || strings.TrimSpace(raw) == "" { continue } var value any decoder := json.NewDecoder(strings.NewReader(raw)) decoder.UseNumber() if decoder.Decode(&value) != nil { continue } foundCatalog = true collectPackageCatalogDecision(value, &decision, &recognizedOptionCount, &allRecognizedOptionsAreAddon) } if !foundCatalog || recognizedOptionCount == 0 { return verifiedPackageCatalogDecision{}, false } decision.RequiresAddon = decision.RequiresAddon || allRecognizedOptionsAreAddon sort.SliceStable(decision.AddonOptions, func(i, j int) bool { if decision.AddonOptions[i].Recommended != decision.AddonOptions[j].Recommended { return decision.AddonOptions[i].Recommended } if decision.AddonOptions[i].CanPurchase != decision.AddonOptions[j].CanPurchase { return decision.AddonOptions[i].CanPurchase } return decision.AddonOptions[i].Sequence < decision.AddonOptions[j].Sequence }) return decision, true } func collectPackageCatalogDecision( value any, decision *verifiedPackageCatalogDecision, recognizedOptionCount *int, allRecognizedOptionsAreAddon *bool, ) { switch typed := value.(type) { case []any: for _, item := range typed { collectPackageCatalogDecision(item, decision, recognizedOptionCount, allRecognizedOptionsAreAddon) } case map[string]any: if rawPackageType, exists := typed["package_type"]; exists { packageType := strings.ToLower(strings.TrimSpace(anyString(rawPackageType))) if packageType == "basic" || packageType == "addon" { *recognizedOptionCount++ if packageType != "addon" { *allRecognizedOptionsAreAddon = false return } option := verifiedAddonOption{ Sequence: anyInt(typed["sequence"]), Name: strings.TrimSpace(anyString(firstValue(typed, "name", "package_name", "title"))), Price: strings.TrimSpace(anyString(firstValue(typed, "price", "amount"))), CanPurchase: anyBool(typed["can_purchase"]), Recommended: anyBool(typed["recommended"]), UnavailableReason: strings.TrimSpace(anyString(typed["unavailable_reason"])), ReasonCode: strings.TrimSpace(anyString(typed["reason_code"])), } decision.AddonOptions = append(decision.AddonOptions, option) if option.Recommended { decision.RequiresAddon = true } return } } for _, item := range typed { collectPackageCatalogDecision(item, decision, recognizedOptionCount, allRecognizedOptionsAreAddon) } } } func buildVerifiedAddonReply(options []verifiedAddonOption) string { lines := []string{ "当前主套餐仍在有效期内,但本周期流量已经用完。", "待生效的基础套餐不会补充当前周期流量;当前只能购买加油包,不能再购买基础套餐来恢复本周期上网。", } purchasable := make([]verifiedAddonOption, 0, len(options)) for _, option := range options { if option.CanPurchase { purchasable = append(purchasable, option) } } if len(purchasable) > 0 { lines = append(lines, "", "当前可购买的加油包:") for i, option := range purchasable { sequence := option.Sequence if sequence <= 0 { sequence = i + 1 } label := strings.TrimSpace(option.Name) if label == "" { label = "加油包" } if option.Price != "" { label += " - ¥" + option.Price } lines = append(lines, fmt.Sprintf("%d. %s", sequence, label)) } lines = append(lines, "", "请回复加油包序号,我再为您进入下单确认。") return strings.Join(lines, "\n") } if hasInsufficientBalanceAddon(options) { lines = append(lines, "", "已查到加油包,但当前余额不足。请先充值余额,充值后再购买加油包。") return strings.Join(lines, "\n") } lines = append(lines, "", "当前暂未查到可购买的加油包,请稍后重新查询,或回复“人工客服”继续处理。") return strings.Join(lines, "\n") } func hasInsufficientBalanceAddon(options []verifiedAddonOption) bool { for _, option := range options { if strings.EqualFold(option.ReasonCode, "insufficient_balance") || strings.Contains(option.UnavailableReason, "余额不足") { return true } } return false } func firstValue(item map[string]any, keys ...string) any { for _, key := range keys { if value, exists := item[key]; exists { return value } } return nil } func anyString(value any) string { switch typed := value.(type) { case string: return typed case json.Number: return typed.String() case float64: return strconv.FormatFloat(typed, 'f', -1, 64) case nil: return "" default: return fmt.Sprint(typed) } } func anyInt(value any) int { parsed, _ := strconv.Atoi(anyString(value)) return parsed } func anyBool(value any) bool { switch typed := value.(type) { case bool: return typed case string: parsed, _ := strconv.ParseBool(typed) return parsed default: return false } }