Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package actionutils
import (
"context"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/iwind/TeaGo/maps"
)
type ActionInterface interface {
RPC() *rpc.RPCClient
AdminContext() context.Context
ViewData() maps.Map
}

View File

@@ -0,0 +1,22 @@
package actionutils
import (
"github.com/TeaOSLab/EdgeAdmin/internal/csrf"
"github.com/iwind/TeaGo/actions"
"net/http"
)
type CSRF struct {
}
func (this *CSRF) BeforeAction(actionPtr actions.ActionWrapper, paramName string) (goNext bool) {
action := actionPtr.Object()
token := action.ParamString("csrfToken")
if !csrf.Validate(token) {
action.ResponseWriter.WriteHeader(http.StatusForbidden)
action.WriteString("表单已失效,请刷新页面后重试(001)")
return
}
return true
}

View File

@@ -0,0 +1,44 @@
package actionutils
// Menu 子菜单定义
type Menu struct {
Id string `json:"id"`
Name string `json:"name"`
Items []*MenuItem `json:"items"`
IsActive bool `json:"isActive"`
AlwaysActive bool `json:"alwaysActive"`
Index int `json:"index"`
CountNormalItems int `json:"countNormalItems"`
}
// NewMenu 获取新对象
func NewMenu() *Menu {
return &Menu{
Items: []*MenuItem{},
}
}
// Add 添加菜单项
func (this *Menu) Add(name string, subName string, url string, isActive bool) *MenuItem {
item := &MenuItem{
Name: name,
SubName: subName,
URL: url,
IsActive: isActive,
}
this.CountNormalItems++
this.Items = append(this.Items, item)
if isActive {
this.IsActive = true
}
return item
}
// AddSpecial 添加特殊菜单项,不计数
func (this *Menu) AddSpecial(name string, subName string, url string, isActive bool) *MenuItem {
item := this.Add(name, subName, url, isActive)
this.CountNormalItems--
return item
}

View File

@@ -0,0 +1,48 @@
package actionutils
import (
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/lists"
)
// MenuGroup 菜单分组
type MenuGroup struct {
Menus []*Menu `json:"menus"`
AlwaysMenu *Menu `json:"alwaysMenu"`
}
// NewMenuGroup 获取新菜单分组对象
func NewMenuGroup() *MenuGroup {
return &MenuGroup{
Menus: []*Menu{},
}
}
// FindMenu 查找菜单,如果找不到则自动创建
func (this *MenuGroup) FindMenu(menuId string, menuName string) *Menu {
for _, m := range this.Menus {
if m.Id == menuId {
return m
}
}
menu := NewMenu()
menu.Id = menuId
menu.Name = menuName
menu.Items = []*MenuItem{}
this.Menus = append(this.Menus, menu)
return menu
}
// Sort 排序
func (this *MenuGroup) Sort() {
lists.Sort(this.Menus, func(i int, j int) bool {
menu1 := this.Menus[i]
menu2 := this.Menus[j]
return menu1.Index < menu2.Index
})
}
// SetSubMenu 设置子菜单
func SetSubMenu(action actions.ActionWrapper, menu *MenuGroup) {
action.Object().Data["teaSubMenus"] = menu
}

View File

@@ -0,0 +1,14 @@
package actionutils
// MenuItem 菜单项
type MenuItem struct {
Id string `json:"id"`
Name string `json:"name"`
SubName string `json:"subName"` // 副标题
SupName string `json:"supName"` // 头标
URL string `json:"url"`
IsActive bool `json:"isActive"`
Icon string `json:"icon"`
IsSortable bool `json:"isSortable"`
SubColor string `json:"subColor"`
}

View File

@@ -0,0 +1,161 @@
package actionutils
import (
"fmt"
"github.com/iwind/TeaGo/actions"
"math"
"net/url"
"strings"
)
type Page struct {
Offset int64 // 开始位置
Size int64 // 每页显示数量
Current int64 // 当前页码
Max int64 // 最大页码
Total int64 // 总数量
Path string
Query url.Values
}
func NewActionPage(actionPtr actions.ActionWrapper, total int64, size int64) *Page {
action := actionPtr.Object()
currentPage := action.ParamInt64("page")
paramSize := action.ParamInt64("pageSize")
if paramSize > 0 {
size = paramSize
}
if size <= 0 {
size = 10
}
page := &Page{
Current: currentPage,
Total: total,
Size: size,
Path: action.Request.URL.Path,
Query: action.Request.URL.Query(),
}
page.calculate()
return page
}
func (this *Page) calculate() {
if this.Current < 1 {
this.Current = 1
}
if this.Size <= 0 {
this.Size = 10
}
this.Offset = this.Size * (this.Current - 1)
this.Max = int64(math.Ceil(float64(this.Total) / float64(this.Size)))
}
func (this *Page) AsHTML() string {
if this.Total <= this.Size {
return ""
}
result := []string{}
// 首页
if this.Max > 0 {
result = append(result, `<a href="`+this.composeURL(1)+`">首页</a>`)
} else {
result = append(result, `<a>首页</a>`)
}
// 上一页
if this.Current <= 1 {
result = append(result, `<a>上一页</a>`)
} else {
result = append(result, `<a href="`+this.composeURL(this.Current-1)+`">上一页</a>`)
}
// 中间页数
before5 := this.max(this.Current-5, 1)
after5 := this.min(before5+9, this.Max)
if before5 > 1 {
result = append(result, `<a>...</a>`)
}
for i := before5; i <= after5; i++ {
if i == this.Current {
result = append(result, `<a href="`+this.composeURL(i)+`" class="active">`+fmt.Sprintf("%d", i)+`</a>`)
} else {
result = append(result, `<a href="`+this.composeURL(i)+`">`+fmt.Sprintf("%d", i)+`</a>`)
}
}
if after5 < this.Max {
result = append(result, `<a>...</a>`)
}
// 下一页
if this.Current >= this.Max {
result = append(result, "<a>下一页</a>")
} else {
result = append(result, `<a href="`+this.composeURL(this.Current+1)+`">下一页</a>`)
}
// 尾页
if this.Max > 0 {
result = append(result, `<a href="`+this.composeURL(this.Max)+`">尾页</a>`)
} else {
result = append(result, `<a>尾页</a>`)
}
// 每页数
result = append(result, `<select class="ui dropdown" style="padding-top:0;padding-bottom:0;margin-left:1em;color:#666" onchange="ChangePageSize(this.value)">
<option value="10">[每页]</option>`+this.renderSizeOption(10)+
this.renderSizeOption(20)+
this.renderSizeOption(30)+
this.renderSizeOption(40)+
this.renderSizeOption(50)+
this.renderSizeOption(60)+
this.renderSizeOption(70)+
this.renderSizeOption(80)+
this.renderSizeOption(90)+
this.renderSizeOption(100)+`
</select>`)
return `<div class="page">` + strings.Join(result, "") + `</div>`
}
// IsLastPage 判断是否为最后一页
func (this *Page) IsLastPage() bool {
return this.Current == this.Max
}
func (this *Page) composeURL(page int64) string {
this.Query["page"] = []string{fmt.Sprintf("%d", page)}
return this.Path + "?" + this.Query.Encode()
}
func (this *Page) min(i, j int64) int64 {
if i < j {
return i
}
return j
}
func (this *Page) max(i, j int64) int64 {
if i < j {
return j
}
return i
}
func (this *Page) renderSizeOption(size int64) string {
o := `<option value="` + fmt.Sprintf("%d", size) + `"`
if size == this.Size {
o += ` selected="selected"`
}
o += `>` + fmt.Sprintf("%d", size) + `条</option>`
return o
}

View File

@@ -0,0 +1,40 @@
package actionutils
import (
"net/url"
"testing"
)
func TestNewActionPage(t *testing.T) {
page := &Page{
Current: 3,
Total: 105,
Size: 20,
Path: "/hello",
Query: url.Values{
"a": []string{"b"},
"c": []string{"d"},
"page": []string{"3"},
},
}
page.calculate()
t.Log(page.AsHTML())
//logs.PrintAsJSON(page, t)
}
func TestNewActionPage2(t *testing.T) {
page := &Page{
Current: 3,
Total: 105,
Size: 10,
Path: "/hello",
Query: url.Values{
"a": []string{"b"},
"c": []string{"d"},
"page": []string{"3"},
},
}
page.calculate()
t.Log(page.AsHTML())
//logs.PrintAsJSON(page, t)
}

View File

@@ -0,0 +1,182 @@
package actionutils
import (
"context"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/index/loginutils"
"github.com/TeaOSLab/EdgeCommon/pkg/langs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/dao"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"net/http"
"strconv"
)
type ParentAction struct {
actions.ActionObject
rpcClient *rpc.RPCClient
ctx context.Context
}
// Parent 可以调用自身的一个简便方法
func (this *ParentAction) Parent() *ParentAction {
return this
}
func (this *ParentAction) ErrorPage(err error) {
if err == nil {
return
}
// 日志
this.CreateLog(oplogs.LevelError, codes.AdminCommon_LogSystemError, err.Error())
if this.Request.Method == http.MethodGet {
FailPage(this, err)
} else {
Fail(this, err)
}
}
func (this *ParentAction) ErrorText(err string) {
this.ErrorPage(errors.New(err))
}
func (this *ParentAction) NotFound(name string, itemId int64) {
if itemId > 0 {
this.ErrorPage(errors.New(name + " id: '" + strconv.FormatInt(itemId, 10) + "' is not found"))
} else {
this.ErrorPage(errors.New(name + " is not found"))
}
}
func (this *ParentAction) NewPage(total int64, size ...int64) *Page {
if len(size) > 0 {
return NewActionPage(this, total, size[0])
}
var pageSize int64 = 10
adminConfig, err := configloaders.LoadAdminUIConfig()
if err == nil && adminConfig.DefaultPageSize > 0 {
pageSize = int64(adminConfig.DefaultPageSize)
}
return NewActionPage(this, total, pageSize)
}
func (this *ParentAction) Nav(mainMenu string, tab string, firstMenu string) {
this.Data["mainMenu"] = mainMenu
this.Data["mainTab"] = tab
this.Data["firstMenuItem"] = firstMenu
}
func (this *ParentAction) FirstMenu(menuItem string) {
this.Data["firstMenuItem"] = menuItem
}
func (this *ParentAction) SecondMenu(menuItem string) {
this.Data["secondMenuItem"] = menuItem
}
func (this *ParentAction) TinyMenu(menuItem string) {
this.Data["tinyMenuItem"] = menuItem
}
func (this *ParentAction) AdminId() int64 {
return this.Context.GetInt64(teaconst.SessionAdminId)
}
func (this *ParentAction) CreateLog(level string, messageCode langs.MessageCode, args ...any) {
var description = messageCode.For(this.LangCode())
var desc = fmt.Sprintf(description, args...)
if level == oplogs.LevelInfo {
if this.Code != 200 {
level = oplogs.LevelWarn
if len(this.Message) > 0 {
desc += " 失败:" + this.Message
}
}
}
err := dao.SharedLogDAO.CreateAdminLog(this.AdminContext(), level, this.Request.URL.Path, desc, loginutils.RemoteIP(&this.ActionObject), messageCode, args)
if err != nil {
utils.PrintError(err)
}
}
func (this *ParentAction) CreateLogInfo(messageCode langs.MessageCode, args ...any) {
this.CreateLog(oplogs.LevelInfo, messageCode, args...)
}
// RPC 获取RPC
func (this *ParentAction) RPC() *rpc.RPCClient {
if this.rpcClient != nil {
return this.rpcClient
}
// 所有集群
rpcClient, err := rpc.SharedRPC()
if err != nil {
logs.Fatal(err)
return nil
}
this.rpcClient = rpcClient
return rpcClient
}
// AdminContext 获取Context
// 每个请求的context都必须是一个新的实例
func (this *ParentAction) AdminContext() context.Context {
if this.rpcClient == nil {
rpcClient, err := rpc.SharedRPC()
if err != nil {
logs.Fatal(err)
return nil
}
this.rpcClient = rpcClient
}
this.ctx = this.rpcClient.Context(this.AdminId())
return this.ctx
}
// ViewData 视图里可以使用的数据
func (this *ParentAction) ViewData() maps.Map {
return this.Data
}
func (this *ParentAction) LangCode() string {
return configloaders.FindAdminLangForAction(this)
}
func (this *ParentAction) Lang(messageCode langs.MessageCode, args ...any) string {
return langs.Message(this.LangCode(), messageCode, args...)
}
func (this *ParentAction) FailLang(messageCode langs.MessageCode, args ...any) {
this.Fail(langs.Message(this.LangCode(), messageCode, args...))
}
func (this *ParentAction) FailFieldLang(field string, messageCode langs.MessageCode, args ...any) {
this.FailField(field, langs.Message(this.LangCode(), messageCode, args...))
}
func (this *ParentAction) FilterHTTPFamily() bool {
if this.Data.GetString("serverFamily") == "http" {
return false
}
this.ResponseWriter.WriteHeader(http.StatusNotFound)
_, _ = this.ResponseWriter.Write([]byte("page not found"))
return true
}

View File

@@ -0,0 +1,54 @@
package actionutils
import (
"github.com/iwind/TeaGo/actions"
)
type TabItem struct {
Name string `json:"name"`
SubName string `json:"subName"`
URL string `json:"url"`
Icon string `json:"icon"`
IsActive bool `json:"isActive"`
IsRight bool `json:"isRight"`
IsTitle bool `json:"isTitle"`
IsDisabled bool `json:"isDisabled"`
}
// Tabbar Tabbar定义
type Tabbar struct {
items []*TabItem
}
// NewTabbar 获取新对象
func NewTabbar() *Tabbar {
return &Tabbar{
items: []*TabItem{},
}
}
// Add 添加菜单项
func (this *Tabbar) Add(name string, subName string, url string, icon string, active bool) *TabItem {
var m = &TabItem{
Name: name,
SubName: subName,
URL: url,
Icon: icon,
IsActive: active,
IsRight: false,
IsTitle: false,
IsDisabled: false,
}
this.items = append(this.items, m)
return m
}
// Items 取得所有的Items
func (this *Tabbar) Items() []*TabItem {
return this.items
}
// SetTabbar 设置子菜单
func SetTabbar(action actions.ActionWrapper, tabbar *Tabbar) {
action.Object().Data["teaTabbar"] = tabbar.Items()
}

View File

@@ -0,0 +1,208 @@
package actionutils
import (
"encoding/json"
"errors"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
rpcerrors "github.com/TeaOSLab/EdgeCommon/pkg/rpc/errors"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/gosock/pkg/gosock"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
)
// Fail 提示服务器错误信息
func Fail(actionPtr actions.ActionWrapper, err error) {
if err == nil {
err = errors.New("unknown error")
}
var langCode = configloaders.FindAdminLangForAction(actionPtr)
var serverErrString = codes.AdminCommon_ServerError.For(langCode)
logs.Println("[" + reflect.TypeOf(actionPtr).String() + "]" + findStack(err.Error()))
_, _, isLocalAPI, issuesHTML := parseAPIErr(actionPtr, err)
if isLocalAPI && len(issuesHTML) > 0 {
actionPtr.Object().Fail(serverErrString + "" + err.Error() + ";最近一次错误提示:" + issuesHTML + "")
} else {
actionPtr.Object().Fail(serverErrString + "" + err.Error() + "")
}
}
// FailPage 提示页面错误信息
func FailPage(actionPtr actions.ActionWrapper, err error) {
if err == nil {
err = errors.New("unknown error")
}
var langCode = configloaders.FindAdminLangForAction(actionPtr)
var serverErrString = codes.AdminCommon_ServerError.For(langCode)
logs.Println("[" + reflect.TypeOf(actionPtr).String() + "]" + findStack(err.Error()))
actionPtr.Object().ResponseWriter.WriteHeader(http.StatusInternalServerError)
if len(actionPtr.Object().Request.Header.Get("X-Requested-With")) > 0 {
actionPtr.Object().WriteString(serverErrString)
} else {
apiNodeIsStarting, apiNodeProgress, _, issuesHTML := parseAPIErr(actionPtr, err)
var html = `<!DOCTYPE html>
<html>
<head>
<title>正在处理...</title>
<meta charset="UTF-8"/>
<style type="text/css">
hr { border-top: 1px #ccc solid; }
.red { color: red; }
</style>
</head>
<body>
<div style="background: #eee; border: 1px #ccc solid; padding: 10px; font-size: 12px; line-height: 1.8">
`
if apiNodeIsStarting { // API节点正在启动
html += "<div class=\"red\">API节点正在启动请耐心等待完成"
if len(apiNodeProgress) > 0 {
html += "" + apiNodeProgress + "(刷新当前页面查看最新状态)"
}
html += "</div>"
} else {
html += serverErrString + `
<div>可以通过查看 <strong><em>$安装目录/logs/run.log</em></strong> 日志文件查看具体的错误提示。</div>
<hr/>
<div class="red">Error: ` + err.Error() + `</div>`
if len(issuesHTML) > 0 {
html += ` <hr/>
<div class="red">` + issuesHTML + `</div>`
}
}
actionPtr.Object().WriteString(html + `
</div>
</body>
</html>`)
}
}
// MatchPath 判断动作的文件路径是否相当
func MatchPath(action *actions.ActionObject, path string) bool {
return action.Request.URL.Path == path
}
// FindParentAction 查找父级Action
func FindParentAction(actionPtr actions.ActionWrapper) *ParentAction {
action, ok := actionPtr.(interface {
Parent() *ParentAction
})
if ok {
return action.Parent()
}
return nil
}
func findStack(err string) string {
_, currentFilename, _, currentOk := runtime.Caller(1)
if currentOk {
for i := 1; i < 32; i++ {
_, filename, lineNo, ok := runtime.Caller(i)
if !ok {
break
}
if filename == currentFilename || filepath.Base(filename) == "parent_action.go" {
continue
}
goPath := os.Getenv("GOPATH")
if len(goPath) > 0 {
absGoPath, err := filepath.Abs(goPath)
if err == nil {
filename = strings.TrimPrefix(filename, absGoPath)[1:]
}
} else if strings.Contains(filename, "src") {
filename = filename[strings.Index(filename, "src"):]
}
err += "\n\t\t" + filename + ":" + fmt.Sprintf("%d", lineNo)
break
}
}
return err
}
// 分析API节点的错误信息
func parseAPIErr(action actions.ActionWrapper, err error) (apiNodeIsStarting bool, apiNodeProgress string, isLocalAPI bool, issuesHTML string) {
// 当前API终端地址
var apiEndpoints = []string{}
apiConfig, apiConfigErr := configs.LoadAPIConfig()
if apiConfigErr == nil && apiConfig != nil {
apiEndpoints = append(apiEndpoints, apiConfig.RPCEndpoints...)
}
var isRPCConnError bool
_, isRPCConnError = rpcerrors.HumanError(err, apiEndpoints, Tea.ConfigFile(configs.ConfigFileName))
if isRPCConnError {
// API节点是否正在启动
var sock = gosock.NewTmpSock("edge-api")
reply, err := sock.SendTimeout(&gosock.Command{
Code: "starting",
Params: nil,
}, 1*time.Second)
if err == nil && reply != nil {
var params = maps.NewMap(reply.Params)
if params.GetBool("isStarting") {
apiNodeIsStarting = true
var progressMap = params.GetMap("progress")
apiNodeProgress = progressMap.GetString("description")
}
}
}
// 本地的一些错误提示
if isRPCConnError {
host, _, hostErr := net.SplitHostPort(action.Object().Request.Host)
if hostErr == nil {
for _, endpoint := range apiEndpoints {
if strings.HasPrefix(endpoint, "http://"+host) || strings.HasPrefix(endpoint, "https://"+host) || strings.HasPrefix(endpoint, host) {
isLocalAPI = true
break
}
}
}
}
if isLocalAPI {
// 读取本地API节点的issues
issuesData, issuesErr := os.ReadFile(Tea.Root + "/edge-api/logs/issues.log")
if issuesErr == nil {
var issueMaps = []maps.Map{}
issuesErr = json.Unmarshal(issuesData, &issueMaps)
if issuesErr == nil && len(issueMaps) > 0 {
var issueMap = issueMaps[0]
issuesHTML = "本地API节点启动错误" + issueMap.GetString("message") + ",处理建议:" + issueMap.GetString("suggestion")
}
}
}
return
}