cyi-box: Memphis-style toolbox admin platform
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>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user