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:
2026-10-01 01:58:47 +08:00
co-authored by Devin
commit 834ecacdb9
326 changed files with 27813 additions and 0 deletions
+272
View File
@@ -0,0 +1,272 @@
// mem.go 共享记忆库门面:key 形态 "<project>/<topic>",写覆盖即 revision+1。
// 同一套数据同时服务后台管理页(MemorySvc)与对外 MCP 协议端点(internal/memx)。
package store
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/cyi-cc/cyi-box/backend/internal/db"
)
// memFTSDDL 全文索引:外挂内容表(不复制数据)+ 增删改触发器同步。
// trigram 分词器:中英文都能做子串匹配(代价是 <3 字符的词无法索引,搜索侧兜底)
const memFTSDDL = `
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
k, content, project, tags,
content='memories', content_rowid='rowid',
tokenize='trigram'
);
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
INSERT INTO memories_fts(rowid, k, content, project, tags)
VALUES (new.rowid, new.k, new.content, new.project, new.tags);
END;
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, k, content, project, tags)
VALUES ('delete', old.rowid, old.k, old.content, old.project, old.tags);
END;
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, k, content, project, tags)
VALUES ('delete', old.rowid, old.k, old.content, old.project, old.tags);
INSERT INTO memories_fts(rowid, k, content, project, tags)
VALUES (new.rowid, new.k, new.content, new.project, new.tags);
END;`
// Memory 一条持久记忆;Tags 经 JSON 数组落库
type Memory struct {
Key string
Content string
Project string
Tags []string
WrittenBy string
Revision int64
CreatedAt int64
UpdatedAt int64
}
func memoryFromRow(r db.Memory) Memory {
m := Memory{
Key: r.K, Content: r.Content, Project: r.Project, WrittenBy: r.WrittenBy,
Revision: r.Revision, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
}
_ = json.Unmarshal([]byte(r.Tags), &m.Tags)
if m.Tags == nil {
m.Tags = []string{}
}
return m
}
// SaveMemory 覆盖式写入:同 key 保留 created_at、revision 自增
func (s *Store) SaveMemory(m *Memory) error {
now := time.Now().Unix()
tags, _ := json.Marshal(m.Tags)
if err := s.q.UpsertMemory(storeCtx, db.UpsertMemoryParams{
K: m.Key, Content: m.Content, Project: m.Project, Tags: string(tags),
WrittenBy: m.WrittenBy, CreatedAt: now, UpdatedAt: now,
}); err != nil {
return err
}
row, err := s.q.GetMemory(storeCtx, m.Key)
if err != nil {
return err
}
m.Revision, m.CreatedAt, m.UpdatedAt = row.Revision, row.CreatedAt, row.UpdatedAt
return nil
}
func (s *Store) GetMemory(key string) (*Memory, error) {
row, err := s.q.GetMemory(storeCtx, key)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
m := memoryFromRow(row)
return &m, nil
}
func (s *Store) DeleteMemory(key string) (bool, error) {
n, err := s.q.DeleteMemory(storeCtx, key)
return n > 0, err
}
// ListMemories project/tag 可空过滤,limit<=0 取默认 50,上限 500
func (s *Store) ListMemories(project, tag string, limit int64) ([]Memory, error) {
if limit <= 0 {
limit = 50
}
if limit > 500 {
limit = 500
}
rows, err := s.q.ListMemories(storeCtx, db.ListMemoriesParams{
Column1: project, Project: project,
Column3: tag, Column4: optStr(tag), Limit: limit,
})
if err != nil {
return nil, err
}
out := make([]Memory, 0, len(rows))
for _, r := range rows {
out = append(out, memoryFromRow(r))
}
return out, nil
}
// SearchMemories 每个空白分隔的词都要命中(AND 语义)。
// ≥3 字符的词走 FTS5 trigram 索引(按相关度排序);出现短词或索引异常时退回内存 LIKE。
func (s *Store) SearchMemories(query, project string, limit int64) ([]Memory, error) {
terms := strings.Fields(query)
if len(terms) == 0 {
return s.ListMemories(project, "", limit)
}
if limit <= 0 {
limit = 50
}
canFTS := true
for _, t := range terms {
if len([]rune(t)) < 3 {
canFTS = false
break
}
}
if canFTS {
if ms, err := s.searchMemoriesFTS(terms, project, limit); err == nil {
return ms, nil
}
}
return s.searchMemoriesLike(terms, project, limit)
}
// searchMemoriesFTS 索引命中:词项加引号消除 MATCH 语法注入面
func (s *Store) searchMemoriesFTS(terms []string, project string, limit int64) ([]Memory, error) {
var b strings.Builder
for i, t := range terms {
if i > 0 {
b.WriteString(" AND ")
}
b.WriteString(`"` + strings.ReplaceAll(t, `"`, `""`) + `"`)
}
rows, err := s.DB.Query(`SELECT m.k, m.content, m.project, m.tags, m.written_by, m.revision, m.created_at, m.updated_at
FROM memories_fts f JOIN memories m ON m.rowid = f.rowid
WHERE f MATCH ? AND (? = '' OR m.project = ?)
ORDER BY f.rank LIMIT ?`, b.String(), project, project, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Memory
for rows.Next() {
var m Memory
var tags string
if err := rows.Scan(&m.Key, &m.Content, &m.Project, &tags, &m.WrittenBy, &m.Revision, &m.CreatedAt, &m.UpdatedAt); err != nil {
return nil, err
}
_ = json.Unmarshal([]byte(tags), &m.Tags)
if m.Tags == nil {
m.Tags = []string{}
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *Store) searchMemoriesLike(terms []string, project string, limit int64) ([]Memory, error) {
all, err := s.ListMemories(project, "", 500)
if err != nil {
return nil, err
}
var out []Memory
for _, m := range all {
hay := strings.ToLower(m.Key + "\n" + m.Project + "\n" + m.Content)
ok := true
for _, t := range terms {
if !strings.Contains(hay, strings.ToLower(t)) {
ok = false
break
}
}
if ok {
out = append(out, m)
}
if int64(len(out)) >= limit {
break
}
}
return out, nil
}
type MemoryProject struct {
Name string
Count int64
}
func (s *Store) MemoryProjects() ([]MemoryProject, error) {
rows, err := s.q.MemoryProjects(storeCtx)
if err != nil {
return nil, err
}
out := make([]MemoryProject, 0, len(rows))
for _, r := range rows {
out = append(out, MemoryProject{Name: r.Name, Count: r.Cnt})
}
return out, nil
}
const mcpTokenKey = "mcp.token"
// McpToken 读取对外 MCP 端点的 Bearer 令牌;不存在则生成一个并落库
func (s *Store) McpToken() (string, error) {
if v, err := s.GetSetting(mcpTokenKey); err == nil && v != "" {
return v, nil
}
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("store: gen mcp token failed: %w", err)
}
tok := hex.EncodeToString(b)
if err := s.SetSetting(mcpTokenKey, tok); err != nil {
return "", err
}
return tok, nil
}
// SignMemoryReadURL 签发 /m/<key> 阅读链接:?sig=HMAC(token, "read:"+key)[:24]
// 与 MCP 共用同一令牌——重置令牌即吊销全部已发链接
func (s *Store) SignMemoryReadURL(base, key string) (string, error) {
tok, err := s.McpToken()
if err != nil {
return "", err
}
return base + "/m/" + key + "?sig=" + memReadSig(tok, key), nil
}
// CheckMemoryReadSig 校验阅读链接签名
func (s *Store) CheckMemoryReadSig(key, sig string) bool {
tok, err := s.McpToken()
if err != nil || sig == "" {
return false
}
return hmac.Equal([]byte(sig), []byte(memReadSig(tok, key)))
}
func memReadSig(token, key string) string {
h := hmac.New(sha256.New, []byte(token))
h.Write([]byte("read:" + key))
return hex.EncodeToString(h.Sum(nil))[:24]
}
func (s *Store) ResetMcpToken() (string, error) {
if err := s.SetSetting(mcpTokenKey, ""); err != nil {
return "", err
}
return s.McpToken()
}