6708c226a5
- 迁移 000023 gateway.file_objects(personal/system 归属隔离 + 部分索引) - internal/platform/storage:minio-go 适配(端点 scheme 剥离、流式 PutObject/Open/Delete) - internal/workbench/files.go:FileService(sha256 校验、PutObject-then-insert 回滚、delete 先删行再删对象) - admin /api/v1/admin/files + portal /api/v1/portal/files 处理器(流式上传下载、Content-Disposition) - RBAC file:read/file:manage;菜单加文件管理 + 门户文件仓库 - compose 增 minio 服务(S3_* anchor、不暴露端口);nginx client_max_body_size 32m→256m - 管理端文件管理页 + 门户个人文件仓;集成测试 TestFileObjectLifecycle 连真 MinIO 通过 - healthz object_storage:true;README/PRODUCTION/进展文档同步 Co-Authored-By: Claude <noreply@anthropic.com>
434 lines
20 KiB
Go
434 lines
20 KiB
Go
package identity
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"aigateway.local/core/internal/platform/apiresponse"
|
|
"aigateway.local/core/internal/platform/cryptox"
|
|
)
|
|
|
|
type HTTPHandler struct {
|
|
service *Service
|
|
mux *http.ServeMux
|
|
}
|
|
|
|
type loginRequest struct {
|
|
UserName string `json:"userName"`
|
|
Username string `json:"username"`
|
|
Account string `json:"account"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type totpLoginRequest struct {
|
|
TempToken string `json:"temp_token"`
|
|
Code string `json:"code"`
|
|
BackupCode string `json:"backup_code"`
|
|
}
|
|
|
|
type passwordRequest struct {
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type factorRequest struct {
|
|
Password string `json:"password"`
|
|
Code string `json:"code"`
|
|
BackupCode string `json:"backup_code"`
|
|
}
|
|
|
|
func NewHTTPHandler(service *Service) *HTTPHandler {
|
|
handler := &HTTPHandler{service: service, mux: http.NewServeMux()}
|
|
handler.mux.HandleFunc("POST /api/v1/admin/login", handler.login(KindAdmin))
|
|
handler.registerTOTP(KindAdmin, "/api/v1/admin")
|
|
handler.mux.HandleFunc("GET /api/v1/admin/whoami", handler.whoami(KindAdmin))
|
|
handler.mux.HandleFunc("POST /api/v1/admin/password", handler.changePassword(KindAdmin))
|
|
handler.mux.HandleFunc("POST /api/v1/admin/logout", handler.logout)
|
|
handler.mux.HandleFunc("GET /api/v1/admin/menus", handler.menus(KindAdmin))
|
|
handler.mux.HandleFunc("POST /api/v1/portal/login", handler.login(KindPortal))
|
|
handler.registerTOTP(KindPortal, "/api/v1/portal")
|
|
handler.mux.HandleFunc("GET /api/v1/portal/me", handler.whoami(KindPortal))
|
|
handler.mux.HandleFunc("POST /api/v1/portal/logout", handler.logout)
|
|
handler.mux.HandleFunc("GET /api/v1/portal/menus", handler.menus(KindPortal))
|
|
handler.registerOIDC()
|
|
return handler
|
|
}
|
|
|
|
func (h *HTTPHandler) changePassword(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input struct {
|
|
OldPassword string `json:"old_password"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if !decodeJSON(writer, request, &input) {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "请求格式无效")
|
|
return
|
|
}
|
|
if err := h.service.ChangePassword(request.Context(), account, input.OldPassword, input.NewPassword); err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]bool{"changed": true})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
h.mux.ServeHTTP(writer, request)
|
|
}
|
|
|
|
func (h *HTTPHandler) login(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
// 防爆破:按 IP 的滑动窗口限流,超限返回 429(与账号锁定叠加)。
|
|
if !h.service.AllowLogin(request.Context(), ClientIP(request)) {
|
|
apiresponse.Error(writer, http.StatusTooManyRequests, "登录尝试过于频繁,请稍后再试")
|
|
return
|
|
}
|
|
var input loginRequest
|
|
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&input); err != nil {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "请求格式无效")
|
|
return
|
|
}
|
|
login := strings.TrimSpace(input.UserName)
|
|
if login == "" {
|
|
login = strings.TrimSpace(input.Username)
|
|
}
|
|
if login == "" {
|
|
login = strings.TrimSpace(input.Account)
|
|
}
|
|
if login == "" || len(login) > 128 || len(input.Password) < 1 || len(input.Password) > 1024 {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "账号或口令格式无效")
|
|
return
|
|
}
|
|
result, err := h.service.Login(request.Context(), kind, login, input.Password)
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]any{
|
|
"token": result.Token, "refreshToken": "", "require_totp": result.RequireTOTP,
|
|
"temp_token": result.TempToken,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) registerTOTP(kind Kind, prefix string) {
|
|
h.mux.HandleFunc("POST "+prefix+"/login/totp", h.completeTOTPLogin(kind))
|
|
h.mux.HandleFunc("GET "+prefix+"/totp/status", h.totpStatus(kind))
|
|
h.mux.HandleFunc("POST "+prefix+"/totp/setup", h.setupTOTP(kind))
|
|
h.mux.HandleFunc("POST "+prefix+"/totp/confirm", h.confirmTOTP(kind))
|
|
h.mux.HandleFunc("POST "+prefix+"/totp/disable", h.disableTOTP(kind))
|
|
h.mux.HandleFunc("POST "+prefix+"/totp/backup-codes/regenerate", h.regenerateBackupCodes(kind))
|
|
}
|
|
|
|
func (h *HTTPHandler) completeTOTPLogin(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
// 防爆破:TOTP 完成端点同样按 IP 限流。
|
|
if !h.service.AllowLogin(request.Context(), ClientIP(request)) {
|
|
apiresponse.Error(writer, http.StatusTooManyRequests, "登录尝试过于频繁,请稍后再试")
|
|
return
|
|
}
|
|
var input totpLoginRequest
|
|
if !decodeJSON(writer, request, &input) || strings.TrimSpace(input.TempToken) == "" || (strings.TrimSpace(input.Code) == "" && strings.TrimSpace(input.BackupCode) == "") {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "请输入动态验证码或备用码")
|
|
return
|
|
}
|
|
result, err := h.service.CompleteTOTPLogin(request.Context(), kind, input.TempToken, input.Code, input.BackupCode)
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]any{"token": result.Token, "refreshToken": "", "require_totp": false})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) totpStatus(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]bool{"enabled": account.TOTPEnabled, "setup_pending": !account.TOTPEnabled && len(account.EncryptedTOTPSecret) > 0})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) setupTOTP(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input passwordRequest
|
|
if !decodeJSON(writer, request, &input) || input.Password == "" {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "请输入当前口令")
|
|
return
|
|
}
|
|
result, err := h.service.SetupTOTP(request.Context(), account, input.Password)
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]string{"secret": result.Secret, "provisioning_uri": result.ProvisioningURI})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) confirmTOTP(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input factorRequest
|
|
if !decodeJSON(writer, request, &input) || strings.TrimSpace(input.Code) == "" {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "请输入动态验证码")
|
|
return
|
|
}
|
|
codes, err := h.service.ConfirmTOTP(request.Context(), account, input.Code)
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]any{"enabled": true, "backup_codes": codes})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) disableTOTP(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input factorRequest
|
|
if !decodeJSON(writer, request, &input) || input.Password == "" {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "当前口令和验证因子不能为空")
|
|
return
|
|
}
|
|
if err := h.service.DisableTOTP(request.Context(), account, input.Password, input.Code, input.BackupCode); err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]bool{"enabled": false})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) regenerateBackupCodes(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, ok := h.requireAccount(writer, request, kind)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input factorRequest
|
|
if !decodeJSON(writer, request, &input) || input.Password == "" {
|
|
apiresponse.Error(writer, http.StatusBadRequest, "当前口令和验证因子不能为空")
|
|
return
|
|
}
|
|
codes, err := h.service.RegenerateBackupCodes(request.Context(), account, input.Password, input.Code, input.BackupCode)
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]any{"backup_codes": codes})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) requireAccount(writer http.ResponseWriter, request *http.Request, kind Kind) (Account, bool) {
|
|
account, err := h.service.Authenticate(request.Context(), kind, request.Header.Get("Authorization"))
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return Account{}, false
|
|
}
|
|
return account, true
|
|
}
|
|
|
|
func decodeJSON(writer http.ResponseWriter, request *http.Request, target any) bool {
|
|
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
|
decoder.DisallowUnknownFields()
|
|
return decoder.Decode(target) == nil
|
|
}
|
|
|
|
func (h *HTTPHandler) whoami(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, err := h.service.Authenticate(request.Context(), kind, request.Header.Get("Authorization"))
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
roles := []string{"R_USER"}
|
|
if kind == KindAdmin {
|
|
roles = []string{"R_ADMIN"}
|
|
if account.Role == "superadmin" {
|
|
roles = []string{"R_SUPER"}
|
|
}
|
|
}
|
|
permissions := EffectivePermissions(account)
|
|
apiresponse.OK(writer, map[string]any{
|
|
"userId": account.ID, "userName": account.Login,
|
|
"displayName": account.DisplayName, "email": "",
|
|
"roles": roles, "buttons": permissions, "permissions": permissions, "role": account.Role,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) logout(writer http.ResponseWriter, request *http.Request) {
|
|
if err := h.service.Logout(request.Context(), request.Header.Get("Authorization")); err != nil && !errors.Is(err, ErrInvalidSession) {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
apiresponse.OK(writer, map[string]bool{"ok": true})
|
|
}
|
|
|
|
func (h *HTTPHandler) menus(kind Kind) http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
account, err := h.service.Authenticate(request.Context(), kind, request.Header.Get("Authorization"))
|
|
if err != nil {
|
|
h.writeIdentityError(writer, err)
|
|
return
|
|
}
|
|
if kind == KindAdmin {
|
|
apiresponse.OK(writer, adminMenus(account))
|
|
return
|
|
}
|
|
apiresponse.OK(writer, portalMenus())
|
|
}
|
|
}
|
|
|
|
func (h *HTTPHandler) writeIdentityError(writer http.ResponseWriter, err error) {
|
|
var locked LockedError
|
|
switch {
|
|
case errors.As(err, &locked):
|
|
minutes := int(time.Until(locked.Until).Minutes()) + 1
|
|
apiresponse.Error(writer, http.StatusTooManyRequests, "账号已锁定,请在 "+(time.Duration(minutes)*time.Minute).String()+" 后重试")
|
|
case errors.Is(err, ErrInvalidCredentials):
|
|
apiresponse.Error(writer, http.StatusUnauthorized, "账号或口令错误")
|
|
case errors.Is(err, ErrInvalidSession), errors.Is(err, ErrNotFound):
|
|
apiresponse.Error(writer, http.StatusUnauthorized, "登录状态无效或已过期")
|
|
case errors.Is(err, ErrAccountDisabled):
|
|
apiresponse.Error(writer, http.StatusForbidden, "账号已被停用")
|
|
case errors.Is(err, ErrInvalidTOTP):
|
|
apiresponse.Error(writer, http.StatusUnauthorized, "动态验证码无效、已使用或备用码无效")
|
|
case errors.Is(err, ErrTOTPAlreadyEnabled):
|
|
apiresponse.Error(writer, http.StatusConflict, "两步验证已经启用")
|
|
case errors.Is(err, ErrTOTPNotEnabled), errors.Is(err, ErrTOTPSetupRequired):
|
|
apiresponse.Error(writer, http.StatusConflict, "两步验证尚未完成配置")
|
|
case errors.Is(err, cryptox.ErrKeyUnavailable):
|
|
apiresponse.Error(writer, http.StatusServiceUnavailable, "两步验证加密密钥不可用")
|
|
case errors.Is(err, ErrUnavailable):
|
|
apiresponse.Error(writer, http.StatusServiceUnavailable, "身份服务暂不可用")
|
|
default:
|
|
apiresponse.Error(writer, http.StatusInternalServerError, "身份服务处理失败")
|
|
}
|
|
}
|
|
|
|
func adminMenus(account Account) []map[string]any {
|
|
// 运行概览:首页仪表盘(叶子菜单,避免出现「运行概览>运行概览」同级冗余)。
|
|
menus := []map[string]any{
|
|
{"name": "Dashboard", "path": "/dashboard/console", "component": "/dashboard/console", "meta": map[string]any{"title": "运行概览", "icon": "ri:pie-chart-line", "fixedTab": true}},
|
|
}
|
|
|
|
// 网关接入:上游供应商、路由与凭据。
|
|
gatewayChildren := make([]map[string]any, 0, 4)
|
|
if HasPermission(account, PermissionProviderRead) || HasPermission(account, PermissionProviderManage) {
|
|
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Providers", "path": "providers", "component": "/gateway/providers", "meta": map[string]any{"title": "模型供应商"}})
|
|
gatewayChildren = append(gatewayChildren, map[string]any{"name": "ModelRoutes", "path": "model-routes", "component": "/gateway/model-routes", "meta": map[string]any{"title": "模型路由"}})
|
|
}
|
|
if HasPermission(account, PermissionAPIKeyRead) || HasPermission(account, PermissionAPIKeyManage) {
|
|
gatewayChildren = append(gatewayChildren, map[string]any{"name": "APIKeys", "path": "api-keys", "component": "/gateway/api-keys", "meta": map[string]any{"title": "API Key"}})
|
|
}
|
|
if HasPermission(account, PermissionPricingRead) || HasPermission(account, PermissionPricingManage) {
|
|
gatewayChildren = append(gatewayChildren, map[string]any{"name": "ModelPrices", "path": "model-prices", "component": "/gateway/model-prices", "meta": map[string]any{"title": "模型价格"}})
|
|
}
|
|
if len(gatewayChildren) > 0 {
|
|
menus = append(menus, map[string]any{"name": "Gateway", "path": "/gateway", "component": "/index/index", "meta": map[string]any{"title": "网关接入", "icon": "ri:router-line"}, "children": gatewayChildren})
|
|
}
|
|
|
|
// 安全与审计:审计用量、内容策略与模型治理。
|
|
securityChildren := make([]map[string]any, 0, 3)
|
|
if HasPermission(account, PermissionAuditRead) || HasPermission(account, PermissionUsageRead) {
|
|
securityChildren = append(securityChildren, map[string]any{"name": "AuditUsage", "path": "audit-usage", "component": "/gateway/audit-usage", "meta": map[string]any{"title": "审计与用量"}})
|
|
}
|
|
if HasPermission(account, PermissionContentPolicyRead) || HasPermission(account, PermissionContentPolicyManage) {
|
|
securityChildren = append(securityChildren, map[string]any{"name": "ContentPolicies", "path": "content-policies", "component": "/gateway/content-policies", "meta": map[string]any{"title": "内容策略"}})
|
|
}
|
|
if HasPermission(account, PermissionKnowledgeRead) || HasPermission(account, PermissionKnowledgeManage) {
|
|
securityChildren = append(securityChildren, map[string]any{"name": "Governance", "path": "governance", "component": "/gateway/governance", "meta": map[string]any{"title": "模型治理"}})
|
|
}
|
|
if len(securityChildren) > 0 {
|
|
menus = append(menus, map[string]any{"name": "Security", "path": "/security", "component": "/index/index", "meta": map[string]any{"title": "安全与审计", "icon": "ri:shield-check-line"}, "children": securityChildren})
|
|
}
|
|
|
|
// AI 资产:Prompt、知识库、工具与 AI 应用。
|
|
assetsChildren := make([]map[string]any, 0, 4)
|
|
if HasPermission(account, PermissionPromptRead) || HasPermission(account, PermissionPromptManage) {
|
|
assetsChildren = append(assetsChildren, map[string]any{"name": "Prompts", "path": "prompts", "component": "/gateway/prompts", "meta": map[string]any{"title": "Prompt 资产"}})
|
|
}
|
|
if HasPermission(account, PermissionKnowledgeRead) || HasPermission(account, PermissionKnowledgeManage) {
|
|
assetsChildren = append(assetsChildren, map[string]any{"name": "Knowledge", "path": "knowledge", "component": "/gateway/knowledge", "meta": map[string]any{"title": "知识库"}})
|
|
}
|
|
if HasPermission(account, PermissionToolRead) || HasPermission(account, PermissionToolManage) {
|
|
assetsChildren = append(assetsChildren, map[string]any{"name": "Tools", "path": "tools", "component": "/gateway/tools", "meta": map[string]any{"title": "工具中心"}})
|
|
}
|
|
if HasPermission(account, PermissionApplicationRead) || HasPermission(account, PermissionApplicationManage) {
|
|
assetsChildren = append(assetsChildren, map[string]any{"name": "Applications", "path": "applications", "component": "/gateway/applications", "meta": map[string]any{"title": "AI 应用"}})
|
|
}
|
|
if HasPermission(account, PermissionFileRead) || HasPermission(account, PermissionFileManage) {
|
|
assetsChildren = append(assetsChildren, map[string]any{"name": "Files", "path": "files", "component": "/gateway/files", "meta": map[string]any{"title": "文件管理"}})
|
|
}
|
|
if len(assetsChildren) > 0 {
|
|
menus = append(menus, map[string]any{"name": "Assets", "path": "/assets", "component": "/index/index", "meta": map[string]any{"title": "AI 资产", "icon": "ri:box-3-line"}, "children": assetsChildren})
|
|
}
|
|
|
|
// 资源市场:MCP 服务器、Skills 与数字员工(旗舰版资源市场)。
|
|
marketChildren := make([]map[string]any, 0, 4)
|
|
if HasPermission(account, PermissionMarketplaceRead) || HasPermission(account, PermissionMarketplaceManage) {
|
|
marketChildren = append(marketChildren, map[string]any{"name": "Marketplace", "path": "overview", "component": "/gateway/marketplace", "meta": map[string]any{"title": "市场总览"}})
|
|
}
|
|
if HasPermission(account, PermissionMCPServerRead) || HasPermission(account, PermissionMCPServerManage) {
|
|
marketChildren = append(marketChildren, map[string]any{"name": "MCPServers", "path": "mcp-servers", "component": "/gateway/mcp-servers", "meta": map[string]any{"title": "MCP 服务器"}})
|
|
}
|
|
if HasPermission(account, PermissionSkillRead) || HasPermission(account, PermissionSkillManage) {
|
|
marketChildren = append(marketChildren, map[string]any{"name": "Skills", "path": "skills", "component": "/gateway/skills", "meta": map[string]any{"title": "Skills 技能"}})
|
|
}
|
|
if HasPermission(account, PermissionDigitalEmployeeRead) || HasPermission(account, PermissionDigitalEmployeeManage) {
|
|
marketChildren = append(marketChildren, map[string]any{"name": "DigitalEmployees", "path": "digital-employees", "component": "/gateway/digital-employees", "meta": map[string]any{"title": "数字员工"}})
|
|
}
|
|
if len(marketChildren) > 0 {
|
|
menus = append(menus, map[string]any{"name": "ResourceMarket", "path": "/resource-market", "component": "/index/index", "meta": map[string]any{"title": "资源市场", "icon": "ri:store-3-line"}, "children": marketChildren})
|
|
}
|
|
|
|
// 系统管理:账号权限、事件投递与通知。
|
|
systemChildren := make([]map[string]any, 0, 3)
|
|
if HasPermission(account, PermissionIdentityManage) {
|
|
systemChildren = append(systemChildren, map[string]any{"name": "User", "path": "user", "component": "/system/user", "meta": map[string]any{"title": "账号与权限"}})
|
|
}
|
|
if HasPermission(account, PermissionOutboxRead) || HasPermission(account, PermissionOutboxManage) {
|
|
systemChildren = append(systemChildren, map[string]any{"name": "Outbox", "path": "outbox", "component": "/gateway/outbox", "meta": map[string]any{"title": "事件投递"}})
|
|
}
|
|
if HasPermission(account, PermissionNotificationRead) || HasPermission(account, PermissionNotificationManage) {
|
|
systemChildren = append(systemChildren, map[string]any{"name": "Notifications", "path": "notifications", "component": "/gateway/notifications", "meta": map[string]any{"title": "通知中心"}})
|
|
}
|
|
if len(systemChildren) > 0 {
|
|
menus = append(menus, map[string]any{"name": "System", "path": "/system", "component": "/index/index", "meta": map[string]any{"title": "系统管理", "icon": "ri:user-3-line"}, "children": systemChildren})
|
|
}
|
|
return menus
|
|
}
|
|
|
|
func portalMenus() []map[string]any {
|
|
return []map[string]any{
|
|
{"name": "Portal", "path": "/portal", "component": "/index/index", "meta": map[string]any{"title": "AI 工作台", "icon": "ri:sparkling-line"}, "children": []map[string]any{
|
|
{"name": "PortalCatalog", "path": "catalog", "component": "/portal/catalog", "meta": map[string]any{"title": "资产目录", "fixedTab": true}},
|
|
{"name": "PortalMarketplace", "path": "marketplace", "component": "/portal/marketplace", "meta": map[string]any{"title": "资源市场"}},
|
|
{"name": "PortalPrompts", "path": "prompts", "component": "/portal/prompts", "meta": map[string]any{"title": "Prompt 广场"}},
|
|
{"name": "PortalUsage", "path": "usage", "component": "/portal/usage", "meta": map[string]any{"title": "我的用量"}},
|
|
{"name": "PortalAccess", "path": "access", "component": "/portal/access", "meta": map[string]any{"title": "模型权限"}},
|
|
{"name": "PortalFiles", "path": "files", "component": "/portal/files", "meta": map[string]any{"title": "文件仓库"}},
|
|
}},
|
|
}
|
|
}
|