package comm import ( "fmt" "strings" "time" "gorm.io/gorm" ) /* 记忆项的**共享查询层**。 放在 comm 是为了解决一个具体问题:`mcp` 是独立进程、独立服务,它的工具从不 RpcCall、只裸查 MySQL(见 tool_get_user_tasks 的 `mysql.Table(...).Where("uid = ?", uid)`)。如果 memory 的过滤条件、分类白名单、 排序口径在 home 和 mcp 里各写一遍,改了一边另一边不会报错,只会让 EMAI 答的 和 App 里显示的对不上——而且没人会发现。 所以两边都调这里。**新增过滤条件只改这一个文件。** ⚠️ 这里只放「读」。写操作(新增/改/删)仍只在 home 的 memory 模块里, MCP 侧的写工具经它的 HTTP 接口不了——那是刻意的:写路径的校验、幂等、 提醒重算都在 memory 模块内,复制一份必然漂移。 */ // MemoryQuery 记忆项查询条件。零值表示不限制。 type MemoryQuery struct { Uid string StartDate string // YYYY-MM-DD,含 EndDate string // YYYY-MM-DD,含 Categories []string States []int32 Limit int Offset int } // ApplyMemoryQuery 把条件套到一个已经 Table(memory_item) 过的 *gorm.DB 上。 // // ⚠️ **uid 一定会被拼进去**,即使 q.Uid 为空也会拼一个不可能命中的条件—— // 少一个 uid 条件就是把全库的记忆项返回给调用方,而这个函数有两个调用方, // 其中一个(MCP)是公网可达的。宁可返回空也不要返回别人的数据。 func ApplyMemoryQuery(tx *gorm.DB, q MemoryQuery) *gorm.DB { if strings.TrimSpace(q.Uid) == "" { return tx.Where("1 = 0") } tx = tx.Where("uid = ?", q.Uid) if q.StartDate != "" { tx = tx.Where("happen_date >= ?", q.StartDate) } if q.EndDate != "" { tx = tx.Where("happen_date <= ?", q.EndDate) } if len(q.Categories) > 0 { tx = tx.Where("category in ?", q.Categories) } if len(q.States) > 0 { tx = tx.Where("state in ?", q.States) } return tx } // OrderMemoryItems 统一排序口径:先日期,再时刻(无时刻的排当天最后),最后 id。 // // 两边必须一致,否则「App 里第一条」和「EMAI 说的第一条」会是不同的东西。 func OrderMemoryItems(tx *gorm.DB) *gorm.DB { return tx.Order("happen_date asc"). Order("happen_time = '' asc"). Order("happen_time asc"). Order("id asc") } // NormalizeMemoryCategories 过滤掉不在白名单里的分类。 // // 返回 (合法分类, 第一个非法分类)。全部合法时第二个返回值为空串。 // 调用方决定是拒绝还是忽略——MCP 侧倾向忽略(模型填错分类不该让整次查询失败), // HTTP 接口侧倾向拒绝(客户端填错是 bug,早点暴露)。 func NormalizeMemoryCategories(in []string) ([]string, string) { out := make([]string, 0, len(in)) bad := "" for _, c := range in { c = strings.ToLower(strings.TrimSpace(c)) if c == "" { continue } if !IsValidMemoryCategory(c) { if bad == "" { bad = c } continue } out = append(out, c) } return out, bad } // MemoryRelativeRange 把「最近 N 天 / 本周 / 本月」这类相对说法换算成日期区间。 // // 给 MCP 工具用:模型问「我这个月花了多少」时不该自己算日期, // 它对「今天几号」的认知来自对话上下文,很不可靠。 func MemoryRelativeRange(kind string, now time.Time) (start, end string, err error) { switch strings.ToLower(strings.TrimSpace(kind)) { case "today": d := FormatMemoryDate(now) return d, d, nil case "yesterday": d := FormatMemoryDate(now.AddDate(0, 0, -1)) return d, d, nil case "this_week": s, e := MemoryWeekRange(now) return FormatMemoryDate(s), FormatMemoryDate(e), nil case "last_week": s, e := MemoryWeekRange(now.AddDate(0, 0, -7)) return FormatMemoryDate(s), FormatMemoryDate(e), nil case "this_month": s, e := MemoryMonthRange(now) return FormatMemoryDate(s), FormatMemoryDate(e), nil case "last_month": prev := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, 0, -1) s, e := MemoryMonthRange(prev) return FormatMemoryDate(s), FormatMemoryDate(e), nil case "next_7_days": return FormatMemoryDate(now), FormatMemoryDate(now.AddDate(0, 0, 7)), nil case "last_7_days": return FormatMemoryDate(now.AddDate(0, 0, -7)), FormatMemoryDate(now), nil } return "", "", fmt.Errorf("不支持的时间范围: %s", kind) } // MemoryRelativeRanges 支持的相对范围,给工具描述用 var MemoryRelativeRanges = []string{ "today", "yesterday", "this_week", "last_week", "this_month", "last_month", "next_7_days", "last_7_days", }