18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
226 lines
6.9 KiB
Go
226 lines
6.9 KiB
Go
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
|
|
}
|
|
}
|