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
+105
View File
@@ -0,0 +1,105 @@
package service
import (
"encoding/json"
"strings"
"time"
"github.com/cyi-cc/fun"
"github.com/valyala/fasthttp"
"github.com/cyi-cc/cyi-box/backend/internal/dto"
)
const (
webReqTimeout = 30 * time.Second
webRespMaxBody = 2 << 20 // 响应体截断上限 2MB
webReqMaxBody = 1 << 20 // 请求体上限 1MB
webMaxRedirect = 5
webMaxHeaderPairs = 64
)
// fun autowired 只处理导出字段,client 用包级单例复用
var webClient = &fasthttp.Client{
Name: "cyi-box-webreq",
MaxConnsPerHost: 16,
ReadTimeout: webReqTimeout,
WriteTimeout: webReqTimeout,
MaxConnWaitTimeout: 5 * time.Second,
}
// WebSvc HTTP 请求代理(Postman 工具):服务端代发以绕开浏览器 CORS。
// 只放行 http/https;headersJson 为 {"k":"v"} 对象。
type WebSvc struct {
fun.Ctx
}
func (s *WebSvc) Fetch(d dto.WebFetchDto) (dto.WebFetchView, error) {
method := strings.ToUpper(strings.TrimSpace(d.Method))
switch method {
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS":
default:
return dto.WebFetchView{}, fun.Error(4001, "不支持的请求方法: "+method)
}
url := strings.TrimSpace(d.Url)
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return dto.WebFetchView{}, fun.Error(4001, "URL 必须以 http:// 或 https:// 开头")
}
if len(d.Body) > webReqMaxBody {
return dto.WebFetchView{}, fun.Error(4001, "请求体超过 1MB 上限")
}
var headers map[string]string
if strings.TrimSpace(d.HeadersJson) != "" {
if err := json.Unmarshal([]byte(d.HeadersJson), &headers); err != nil {
return dto.WebFetchView{}, fun.Error(4001, "请求头 JSON 解析失败: "+err.Error())
}
if len(headers) > webMaxHeaderPairs {
return dto.WebFetchView{}, fun.Error(4001, "请求头数量超过上限")
}
}
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.Header.SetMethod(method)
req.SetRequestURI(url)
for k, v := range headers {
k = strings.TrimSpace(k)
if k == "" || strings.EqualFold(k, "Host") || strings.EqualFold(k, "Content-Length") {
continue
}
req.Header.Set(k, v)
}
if d.Body != "" {
req.SetBodyString(d.Body)
}
start := time.Now()
err := webClient.DoRedirects(req, resp, webMaxRedirect)
elapsed := time.Since(start).Milliseconds()
if err != nil {
return dto.WebFetchView{}, fun.Error(4002, "请求失败: "+err.Error())
}
view := dto.WebFetchView{
Status: int64(resp.StatusCode()),
ElapsedMs: elapsed,
}
respHeaders := map[string]string{}
resp.Header.VisitAll(func(k, v []byte) {
respHeaders[string(k)] = string(v)
})
if b, e := json.Marshal(respHeaders); e == nil {
view.HeadersJson = string(b)
}
body := resp.Body()
if len(body) > webRespMaxBody {
body = body[:webRespMaxBody]
view.Truncated = true
}
view.Body = string(body)
return view, nil
}