Files
sundynix-agentix/sundynix-gateway/internal/router/router.go
T
Blizzard 0edfc948ba feat(memory): P2 读路径打分 + 桌面端记忆面板(看/改/删)
读路径(Generative Agents 公式的 Recency+Importance 两项,Relevance 待 P3):
- memory.Get 改为 rankProfiles:Score=0.4·Recency+0.6·Importance,按分降序、截断 top-30。
  Recency=0.98^天 指数衰减;未评分行用兜底 importance=5(不被不公平遗忘)。纯函数 + 单测。
- 新增 Store.List(结构化、不截断)+ memory_list 工具。

桌面端记忆面板:
- gateway GET /memory(列表) + DELETE /memory?key=(软删),受保护组。
- api listMemory/deleteMemory;MemoryView 右侧从占位 → 真列表:按分排序展示
  key/value/重要度/最近时间,可内联编辑(PUT)与删除(软删);左侧登记后自动刷新。

实测:PUT 两条 → GET 返回带 importance/last_seen 的有序列表(较新者靠前)→ DELETE 一条
(软删)→ 再 GET 已消失。rank/recency/兜底 importance 单测过;前端 tsc+构建通过。

至此记忆:召回(打分) + 历史 + 自动对账(P1) + 用户可管控(面板) 闭环。Relevance(Milvus) 留 P3。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:48:11 +08:00

108 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package router 装配 Gin 统一接入层的路由与中间件。
package router
import (
"os"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sundynix/sundynix-gateway/internal/blob"
"github.com/sundynix/sundynix-gateway/internal/handler"
"github.com/sundynix/sundynix-gateway/internal/middleware"
"github.com/sundynix/sundynix-gateway/internal/nats"
"github.com/sundynix/sundynix-gateway/internal/store"
)
// New 构建带有 Guardrail / 限流中间件的 Gin 引擎。
func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.Store) *gin.Engine {
r := gin.New()
r.Use(gin.Recovery()) // panic 兜底
r.Use(middleware.RequestID()) // 生成/透传 X-Request-ID(日志关联)
r.Use(middleware.Observe()) // Prometheus 指标 + 结构化访问日志(替代 gin 默认文本日志)
r.Use(cors()) // 桌面端/浏览器跨源访问
r.Use(middleware.RateLimit(cache))
r.Use(middleware.Auth()) // 解析 Bearer JWT,注入已验证 userID(非阻断)
r.Use(middleware.Guardrail()) // Harness: Input Guardrail
h := handler.New(db, cache, bus, blobStore)
// 可观测性根端点:Prometheus 抓取 + k8s 存活/就绪探针(不挂业务中间件鉴权)。
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
r.GET("/healthz", h.Healthz)
r.GET("/readyz", h.Readyz)
api := r.Group("/api/v1")
{
// —— 公开:鉴权端点 / 健康 / 按 task_id 寻址的 SSE 与导出(EventSource/下载无法带 Bearer)——
api.POST("/auth/register", h.Register) // 注册 + 签发 JWT
api.POST("/auth/login", h.Login) // 登录 + 签发 JWT
api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401)
api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯)
api.GET("/tasks/:id/stream", h.StreamTask) // SSE 回流 Token Streamtask_id 寻址)
api.GET("/tasks/:id/exec", h.StreamExec) // SSE 回流执行轨迹(task_id 寻址)
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSEjob_id 寻址)
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址)
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx
// —— 受保护:owner 作用域业务,必须携带有效 JWT ——
p := api.Group("", middleware.RequireAuth())
{
p.POST("/tasks", h.SubmitTask) // 解析 DSL 并 Publish 到 NATS(带已验证 uid
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert
p.GET("/memory", h.ListMemory) // 列出当前用户偏好(记忆面板)
p.DELETE("/memory", h.DeleteMemory) // 软删一条偏好(?key=
p.GET("/kb/list", h.KbList) // 当前用户的知识库列表(owner 隔离)
p.POST("/kb/create", h.KbCreate) // 新建知识库
p.POST("/kb/ingest", h.KbIngest) // 文本入库
p.POST("/kb/ingest_file", h.KbIngestFile) // 文件入库
p.POST("/kb/search", h.KbSearch) // 检索台
p.GET("/kb/vault", h.KbVault) // 文库列表
p.GET("/kb/doc", h.KbDoc) // 取单篇文档
p.GET("/kb/links", h.KbLinks) // 某库双链
p.POST("/kb/note", h.KbSaveNote) // 新建/编辑笔记
p.GET("/kb/graph", h.KbGraph) // 知识图谱三元组
p.GET("/agents", h.AgentList) // 我的编排列表(owner 隔离)
p.POST("/agents", h.AgentSave) // 保存/更新编排
p.DELETE("/agents", h.AgentDelete) // 删除编排
p.POST("/reports", h.GenerateReport) // 报告生成
p.GET("/billing", h.Billing)
}
// 运维控制面:LLM 模型配置(含 API 密钥管理)—— 必须管理员(RequireAdmin)。
admin := api.Group("/admin", middleware.RequireAdmin())
{
admin.GET("/models", h.ListModels)
admin.POST("/models", h.SaveModel)
admin.POST("/models/:id/active", h.SetActiveModel)
admin.DELETE("/models/:id", h.DeleteModel)
admin.POST("/models/test", h.TestModel)
admin.GET("/pricing", h.ListPricing) // 各模型计价(token↔真钱)
admin.PUT("/pricing", h.SavePricing) // 设置某模型输入/输出单价
}
}
return r
}
// cors 控制跨源访问。允许来源经 CORS_ALLOW_ORIGIN 配置(缺省 "*" 仅供开发;
// 生产应设为具体源,如 https://app.example.com)。Vary 保证按 Origin 正确缓存。
func cors() gin.HandlerFunc {
origin := "*"
if v := os.Getenv("CORS_ALLOW_ORIGIN"); v != "" {
origin = v
}
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", origin)
if origin != "*" {
c.Header("Vary", "Origin")
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-ID, X-User-ID")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}