You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
4.6 KiB
134 lines
4.6 KiB
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",
|
|
}
|
|
|