Files
ai-gateway-go/internal/identity/http.go
T
LLMGuardX Dev e31cc54b8e 0.11.2: 旗舰版第三轮完善(通用聊天/企微钉钉飞书扫码登录/个人安全策略)
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时
  API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计;
  会话哈希链完整性 + busy 租约防并发,失败不落库。
- 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置
  (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示;
  one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点
  固定公网 URL 复用 public-only 拨号。
- 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/
  扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信
  (新增 security 类别),会话索引只存令牌摘要并惰性清理。
- 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失;
  全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
2026-08-13 12:53:38 +08:00

592 lines
29 KiB
Go

package identity
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"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.mux.HandleFunc("GET /api/v1/admin/login-logs", handler.loginLogs(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/sessions", handler.listSessions)
handler.mux.HandleFunc("POST /api/v1/portal/sessions/{id}/revoke", handler.revokeSession)
handler.mux.HandleFunc("GET /api/v1/portal/security/prefs", handler.securityPrefs)
handler.mux.HandleFunc("PUT /api/v1/portal/security/prefs", handler.setSecurityPrefs)
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()
handler.registerSocial()
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) loginLogs(kind Kind) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
account, err := h.service.Authenticate(request.Context(), KindAdmin, request.Header.Get("Authorization"))
if err != nil {
apiresponse.Error(writer, http.StatusUnauthorized, "登录状态无效或已过期")
return
}
if !HasPermission(account, PermissionAuditRead) {
apiresponse.Error(writer, http.StatusForbidden, "缺少审计日志查看权限")
return
}
login := strings.TrimSpace(request.URL.Query().Get("login"))
limit := 50
if value := request.URL.Query().Get("limit"); value != "" {
if parsed, parseErr := strconv.Atoi(value); parseErr == nil {
limit = parsed
}
}
logs, err := h.service.ListLoginLogs(request.Context(), kind, login, limit)
if err != nil {
apiresponse.Error(writer, http.StatusServiceUnavailable, "登录记录查询失败")
return
}
apiresponse.OK(writer, logs)
}
}
func (h *HTTPHandler) login(kind Kind) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
// 防爆破:按 IP 的滑动窗口限流,超限返回 429(与账号锁定叠加)。
if !h.service.AllowLogin(request.Context(), h.service.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.LoginWithMeta(request.Context(), kind, login, input.Password, SessionMeta{IP: h.service.ClientIP(request), UserAgent: request.UserAgent()})
if err != nil {
// 登录失败审计(429 限流在 AllowLogin 阶段已拦截,此处都是真实失败)。
_ = h.service.RecordLoginLog(request.Context(), kind, login, false, h.service.ClientIP(request), request.UserAgent(), loginFailureReason(err))
h.writeIdentityError(writer, err)
return
}
// 登录成功(含进入 TOTP 挑战阶段)。
_ = h.service.RecordLoginLog(request.Context(), kind, login, true, h.service.ClientIP(request), request.UserAgent(), "success")
apiresponse.OK(writer, map[string]any{
"token": result.Token, "refreshToken": "", "require_totp": result.RequireTOTP,
"temp_token": result.TempToken,
})
}
}
// loginFailureReason 把登录错误归一化为审计用原因码。
func loginFailureReason(err error) string {
switch {
case errors.Is(err, ErrInvalidCredentials):
return "invalid_credentials"
case errors.Is(err, ErrAccountDisabled):
return "account_disabled"
case errors.Is(err, ErrInvalidTOTP):
return "invalid_totp"
case errors.Is(err, ErrTOTPNotEnabled), errors.Is(err, ErrTOTPSetupRequired):
return "totp_not_configured"
case errors.Is(err, ErrUnavailable):
return "service_unavailable"
default:
var locked LockedError
if errors.As(err, &locked) {
return "account_locked"
}
return "unknown"
}
}
// listSessions 我的登录设备列表(当前会话标记 current)。
func (h *HTTPHandler) listSessions(w http.ResponseWriter, r *http.Request) {
account, ok := h.requireAccount(w, r, KindPortal)
if !ok {
return
}
items, err := h.service.ListSessions(r.Context(), KindPortal, account.ID, r.Header.Get("Authorization"))
if err != nil {
h.writeIdentityError(w, err)
return
}
apiresponse.OK(w, items)
}
// revokeSession 吊销指定登录设备(不允许吊销当前会话)。
func (h *HTTPHandler) revokeSession(w http.ResponseWriter, r *http.Request) {
account, ok := h.requireAccount(w, r, KindPortal)
if !ok {
return
}
if err := h.service.RevokeSession(r.Context(), KindPortal, account.ID, r.PathValue("id"), r.Header.Get("Authorization")); err != nil {
h.writeIdentityError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"revoked": true})
}
// securityPrefs 我的安全偏好。
func (h *HTTPHandler) securityPrefs(w http.ResponseWriter, r *http.Request) {
account, ok := h.requireAccount(w, r, KindPortal)
if !ok {
return
}
loginNotify, err := h.service.SecurityPrefs(r.Context(), account.ID)
if err != nil {
h.writeIdentityError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"login_notify": loginNotify})
}
// setSecurityPrefs 更新安全偏好。
func (h *HTTPHandler) setSecurityPrefs(w http.ResponseWriter, r *http.Request) {
account, ok := h.requireAccount(w, r, KindPortal)
if !ok {
return
}
var input struct {
LoginNotify bool `json:"login_notify"`
}
if !decodeJSON(w, r, &input) {
return
}
if err := h.service.SetSecurityPrefs(r.Context(), account.ID, input.LoginNotify); err != nil {
h.writeIdentityError(w, err)
return
}
apiresponse.OK(w, map[string]bool{"login_notify": input.LoginNotify})
}
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(), h.service.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.CompleteTOTPLoginWithMeta(request.Context(), kind, input.TempToken, input.Code, input.BackupCode, SessionMeta{IP: h.service.ClientIP(request), UserAgent: request.UserAgent()})
if err != nil {
_ = h.service.RecordLoginLog(request.Context(), kind, "", false, h.service.ClientIP(request), request.UserAgent(), loginFailureReason(err))
h.writeIdentityError(writer, err)
return
}
_ = h.service.RecordLoginLog(request.Context(), kind, result.Account.Login, true, h.service.ClientIP(request), request.UserAgent(), "success")
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, ErrRevokeCurrentSession):
apiresponse.Error(writer, http.StatusBadRequest, "不能吊销当前登录的会话")
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})
}
// 安全与审计:审计用量、内容策略、模型治理、Trace、会话与节点。
securityChildren := make([]map[string]any, 0, 6)
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 HasPermission(account, PermissionTraceRead) {
securityChildren = append(securityChildren, map[string]any{"name": "Traces", "path": "traces", "component": "/gateway/traces", "meta": map[string]any{"title": "LLM Trace"}})
securityChildren = append(securityChildren, map[string]any{"name": "AgentSessions", "path": "agent-sessions", "component": "/gateway/agent-sessions", "meta": map[string]any{"title": "智能体会话"}})
}
if HasPermission(account, PermissionAgentNodeRead) || HasPermission(account, PermissionAgentNodeManage) {
securityChildren = append(securityChildren, map[string]any{"name": "AgentNodes", "path": "agent-nodes", "component": "/gateway/agent-nodes", "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})
}
if HasPermission(account, PermissionUsageRead) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Reports", "path": "reports", "component": "/gateway/reports", "meta": map[string]any{"title": "企业报表"}})
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Tenants", "path": "tenants", "component": "/gateway/tenants", "meta": map[string]any{"title": "租户概览"}})
}
if HasPermission(account, PermissionNotificationRead) || HasPermission(account, PermissionNotificationManage) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Channels", "path": "channels", "component": "/gateway/channels", "meta": map[string]any{"title": "渠道管理"}})
}
// 系统管理:账号权限、事件投递与通知。
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 HasPermission(account, PermissionInboxRead) || HasPermission(account, PermissionInboxManage) {
systemChildren = append(systemChildren, map[string]any{"name": "Inbox", "path": "inbox", "component": "/gateway/inbox", "meta": map[string]any{"title": "站内消息"}})
}
if HasPermission(account, PermissionScheduledTaskRead) || HasPermission(account, PermissionScheduledTaskManage) {
systemChildren = append(systemChildren, map[string]any{"name": "ScheduledTasks", "path": "scheduled-tasks", "component": "/gateway/scheduled-tasks", "meta": map[string]any{"title": "定时任务"}})
}
if HasPermission(account, PermissionAuditRead) {
systemChildren = append(systemChildren, map[string]any{"name": "LoginLogs", "path": "login-logs", "component": "/system/login-logs", "meta": map[string]any{"title": "登录记录"}})
}
if HasPermission(account, PermissionSystemManage) {
systemChildren = append(systemChildren, map[string]any{"name": "Assistant", "path": "assistant", "component": "/system/assistant", "meta": map[string]any{"title": "AI 助手"}})
systemChildren = append(systemChildren, map[string]any{"name": "License", "path": "license", "component": "/system/license", "meta": map[string]any{"title": "License 授权"}})
}
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": "PortalChat", "path": "chat", "component": "/portal/chat", "meta": map[string]any{"title": "通用聊天", "fixedTab": true}},
{"name": "PortalCatalog", "path": "catalog", "component": "/portal/catalog", "meta": map[string]any{"title": "资产目录"}},
{"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": "文件仓库"}},
{"name": "PortalInbox", "path": "inbox", "component": "/portal/inbox", "meta": map[string]any{"title": "站内消息"}},
{"name": "PortalScheduledTasks", "path": "scheduled-tasks", "component": "/portal/scheduled-tasks", "meta": map[string]any{"title": "定时任务"}},
{"name": "PortalMemories", "path": "memories", "component": "/portal/memories", "meta": map[string]any{"title": "记忆管理"}},
{"name": "PortalEnvVars", "path": "env-vars", "component": "/portal/env-vars", "meta": map[string]any{"title": "环境变量"}},
{"name": "PortalLoginLogs", "path": "login-logs", "component": "/portal/login-logs", "meta": map[string]any{"title": "登录记录"}},
{"name": "PortalSecurity", "path": "security", "component": "/portal/security", "meta": map[string]any{"title": "账号安全"}},
}},
}
}