Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway (Epay-compatible + CDK stock), license/card system (per-app appid, domain binding), AI memory bank with built-in MCP server (/mcp, FTS5 search, signed read links), server SSH monitor + terminal + SFTP, proxy pool with protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL autocomplete + import/export), disk share, vault (AES-GCM + TOTP), bookmarks, dev tools. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
// memory_routes.go 记忆库对外协议端点:
|
||
// POST /mcp Streamable HTTP MCP(Bearer 令牌鉴权,令牌见 MemorySvc.McpInfo)
|
||
// GET /m 阅读链接索引(Bearer 鉴权——key 可枚举,不能裸奔)
|
||
// GET /m/* 记忆阅读链接(?sig= 签名鉴权,HMAC(key, 令牌) 签发,令牌重置即吊销)
|
||
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/subtle"
|
||
"net/http"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
|
||
"github.com/cyi-cc/fun"
|
||
"github.com/mark3labs/mcp-go/server"
|
||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||
|
||
"github.com/cyi-cc/cyi-box/backend/internal/memx"
|
||
"github.com/cyi-cc/cyi-box/backend/internal/store"
|
||
)
|
||
|
||
// requestBase 按当前请求 Host + X-Forwarded-Proto 推导站点基址,
|
||
// 反代换域名后生成的 /m/ 链接与 MCP 接入地址始终可点。
|
||
func requestBase(ctx *fasthttp.RequestCtx) string {
|
||
proto := string(ctx.Request.Header.Peek("X-Forwarded-Proto"))
|
||
if proto == "" {
|
||
proto = "http"
|
||
}
|
||
return proto + "://" + string(ctx.Host())
|
||
}
|
||
|
||
// McpHandler POST /mcp:Authorization Bearer 校验后交给 Streamable HTTP 服务。
|
||
// 令牌存 settings(mcp.token),首次访问懒生成。
|
||
func McpHandler(st *store.Store) fun.RouteHandler {
|
||
mcpSrv := server.NewStreamableHTTPServer(
|
||
memx.NewMCPServer(st),
|
||
server.WithStateLess(true),
|
||
server.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
|
||
proto := r.Header.Get("X-Forwarded-Proto")
|
||
if proto == "" {
|
||
proto = "http"
|
||
if r.TLS != nil {
|
||
proto = "https"
|
||
}
|
||
}
|
||
return memx.WithBase(ctx, proto+"://"+r.Host)
|
||
}),
|
||
)
|
||
h := fasthttpadaptor.NewFastHTTPHandler(mcpSrv)
|
||
return func(c *fun.RouteCtx) error {
|
||
tok, err := st.McpToken()
|
||
if err != nil {
|
||
c.RequestCtx.SetStatusCode(500)
|
||
c.RequestCtx.WriteString(err.Error())
|
||
return nil
|
||
}
|
||
auth := string(c.RequestCtx.Request.Header.Peek("Authorization"))
|
||
if subtle.ConstantTimeCompare([]byte(auth), []byte("Bearer "+tok)) != 1 {
|
||
c.RequestCtx.SetStatusCode(401)
|
||
c.RequestCtx.WriteString("unauthorized")
|
||
return nil
|
||
}
|
||
h(c.RequestCtx)
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// MemoryReadHandler GET /m/*:输出 markdown 原文;裸 /m/ 列出全部阅读链接。
|
||
func MemoryReadHandler(st *store.Store) fun.RouteHandler {
|
||
return func(c *fun.RouteCtx) error {
|
||
key := c.Wildcard
|
||
base := requestBase(c.RequestCtx)
|
||
if key == "" {
|
||
// 索引收进 Bearer 鉴权:key 形如 project/topic 可枚举,裸索引等于全库暴露
|
||
tok, err := st.McpToken()
|
||
if err != nil {
|
||
c.RequestCtx.SetStatusCode(500)
|
||
c.RequestCtx.WriteString(err.Error())
|
||
return nil
|
||
}
|
||
auth := string(c.RequestCtx.Request.Header.Peek("Authorization"))
|
||
if subtle.ConstantTimeCompare([]byte(auth), []byte("Bearer "+tok)) != 1 {
|
||
c.RequestCtx.SetStatusCode(401)
|
||
c.RequestCtx.WriteString("unauthorized")
|
||
return nil
|
||
}
|
||
ms, err := st.ListMemories("", "", 200)
|
||
if err != nil {
|
||
c.RequestCtx.SetStatusCode(500)
|
||
c.RequestCtx.WriteString(err.Error())
|
||
return nil
|
||
}
|
||
c.RequestCtx.SetContentType("text/plain; charset=utf-8")
|
||
for _, m := range ms {
|
||
readURL, _ := st.SignMemoryReadURL(base, m.Key)
|
||
line := readURL
|
||
if m.Project != "" {
|
||
line += "\t[" + m.Project + "]"
|
||
}
|
||
c.RequestCtx.WriteString(line + "\n")
|
||
}
|
||
return nil
|
||
}
|
||
if !st.CheckMemoryReadSig(key, string(c.RequestCtx.QueryArgs().Peek("sig"))) {
|
||
c.RequestCtx.SetStatusCode(403)
|
||
c.RequestCtx.WriteString("bad or missing sig")
|
||
return nil
|
||
}
|
||
m, err := st.GetMemory(key)
|
||
if err != nil {
|
||
c.RequestCtx.SetStatusCode(500)
|
||
c.RequestCtx.WriteString(err.Error())
|
||
return nil
|
||
}
|
||
if m == nil {
|
||
c.RequestCtx.SetStatusCode(404)
|
||
c.RequestCtx.WriteString("not found")
|
||
return nil
|
||
}
|
||
c.RequestCtx.SetContentType("text/markdown; charset=utf-8")
|
||
c.RequestCtx.WriteString(m.Content)
|
||
return nil
|
||
}
|
||
}
|