后端接入 AI 模型并支持测试账号托管回复

按协议而不是按厂商做适配,与现有短信、对象存储的做法一致:openai 兼容格式
一个适配器即可覆盖 DeepSeek、通义、智谱、火山方舟、Ollama 等,新增厂商通常
只需在后台加一条模型记录;anthropic、gemini、webhook、debug 各一个适配器。
密钥沿用 integration.go 的 AES-GCM 加密存储。

测试账号托管的回复走 persistMessage 同一条落库和推送路径,因此未读数、
WebSocket 推送和会话排序全部复用现有逻辑,客户端无需改动。为此把
persistMessage 抽出 persistMessageContext,因为 worker 没有 *http.Request。

任务队列用数据库表而非内存:重启不丢回复,多实例不重复消费。领取用 UPDATE
打 claim_token 再回读,没有用 SELECT ... FOR UPDATE SKIP LOCKED——生产存在
MySQL 5.7 环境,那里该语法无法解析。同一会话同时只允许一个待处理任务
(pending_key 生成列 + 唯一键),所以用户连发多条消息只会得到一条回复。

闸门:仅 is_test=1 且在允许批次内的账号生效,托管账号之间不互相触发,三层
配额,调用失败默认静默。会话内是否标注 AI 身份由 ai.disclose_in_chat 控制
并默认开启,关闭前需确认所在地区的监管要求。

app.go、integration.go、im.go 三个文件同时包含本次改动之前工作区里就已存在
的未提交修改,一并带入。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-03 08:31:04 +08:00
co-authored by Claude Opus 5
parent 334890171e
commit a024d59827
14 changed files with 3239 additions and 146 deletions
+118 -65
View File
@@ -97,6 +97,23 @@ func New(config Config) (*App, error) {
func (a *App) Close() { _ = a.db.Close() }
func (a *App) Run() {
workerContext, stopWorkers := context.WithCancel(context.Background())
defer stopWorkers()
go func() {
a.processDueAccountClosures(workerContext)
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-workerContext.Done():
return
case <-ticker.C:
a.processDueAccountClosures(workerContext)
}
}
}()
go a.runAIReplyWorker(workerContext)
go a.runAIMaintenance(workerContext)
server := rest.MustNewServer(rest.RestConf{
Host: a.config.Host,
Port: a.config.Port,
@@ -156,6 +173,7 @@ func (a *App) routes() []rest.Route {
{Method: http.MethodPost, Path: "/api/v1/auth/login/sms", Handler: a.loginSMS},
{Method: http.MethodGet, Path: "/api/v1/auth/oauth/providers", Handler: a.userOAuthProviders},
{Method: http.MethodPost, Path: "/api/v1/auth/oauth/start", Handler: a.userOAuthStart},
{Method: http.MethodPost, Path: "/api/v1/auth/oauth/native", Handler: a.userOAuthNative},
{Method: http.MethodGet, Path: "/api/v1/auth/oauth/callback", Handler: a.oauthCallback},
{Method: http.MethodPost, Path: "/api/v1/auth/oauth/exchange", Handler: a.userOAuthExchange},
{Method: http.MethodPost, Path: "/api/v1/auth/oauth/link", Handler: a.userOAuthLink},
@@ -169,6 +187,7 @@ func (a *App) routes() []rest.Route {
{Method: http.MethodHead, Path: "/uploads/:name", Handler: a.serveMedia},
{Method: http.MethodPost, Path: "/admin/v1/auth/login", Handler: a.adminLogin},
{Method: http.MethodPost, Path: "/admin/v1/auth/refresh", Handler: a.adminRefresh},
{Method: http.MethodPost, Path: "/admin/v1/auth/logout", Handler: a.adminLogout},
{Method: http.MethodGet, Path: "/admin/v1/auth/oauth/providers", Handler: a.adminOAuthProviders},
{Method: http.MethodPost, Path: "/admin/v1/auth/oauth/start", Handler: a.adminOAuthStart},
{Method: http.MethodGet, Path: "/admin/v1/auth/oauth/callback", Handler: a.oauthCallback},
@@ -187,13 +206,32 @@ func (a *App) userRoutes() []rest.Route {
{Method: http.MethodGet, Path: "/api/v1/me", Handler: auth(a.me)},
{Method: http.MethodGet, Path: "/api/v1/me/profile", Handler: auth(a.me)},
{Method: http.MethodPatch, Path: "/api/v1/me/profile", Handler: auth(a.updateProfile)},
{Method: http.MethodPut, Path: "/api/v1/me/password", Handler: auth(a.changeUserPassword)},
{Method: http.MethodPut, Path: "/api/v1/me/phone", Handler: auth(a.changeUserPhone)},
{Method: http.MethodGet, Path: "/api/v1/me/devices", Handler: auth(a.myDevices)},
{Method: http.MethodDelete, Path: "/api/v1/me/devices/:id", Handler: auth(a.revokeDevice)},
{Method: http.MethodGet, Path: "/api/v1/me/blocks", Handler: auth(a.blockedUsers)},
{Method: http.MethodGet, Path: "/api/v1/me/notification-settings", Handler: auth(a.notificationSettings)},
{Method: http.MethodPut, Path: "/api/v1/me/notification-settings", Handler: auth(a.updateNotificationSettings)},
{Method: http.MethodPost, Path: "/api/v1/me/push-tokens", Handler: auth(a.registerPushToken)},
{Method: http.MethodDelete, Path: "/api/v1/me/push-tokens", Handler: auth(a.deletePushToken)},
{Method: http.MethodGet, Path: "/api/v1/me/feedback", Handler: auth(a.feedback)},
{Method: http.MethodPost, Path: "/api/v1/me/feedback", Handler: auth(a.feedback)},
{Method: http.MethodGet, Path: "/api/v1/me/account-closure", Handler: auth(a.accountClosure)},
{Method: http.MethodPost, Path: "/api/v1/me/account-closure", Handler: auth(a.accountClosure)},
{Method: http.MethodDelete, Path: "/api/v1/me/account-closure", Handler: auth(a.accountClosure)},
{Method: http.MethodGet, Path: "/api/v1/me/data-export", Handler: auth(a.exportMyData)},
{Method: http.MethodPost, Path: "/api/v1/me/consents", Handler: auth(a.recordConsent)},
{Method: http.MethodGet, Path: "/api/v1/users/search", Handler: auth(a.searchUsers)},
{Method: http.MethodGet, Path: "/api/v1/tags", Handler: auth(a.availableTags)},
{Method: http.MethodGet, Path: "/api/v1/users/:id", Handler: auth(a.userProfile)},
{Method: http.MethodGet, Path: "/api/v1/me/following", Handler: auth(a.followingList)},
{Method: http.MethodGet, Path: "/api/v1/me/followers", Handler: auth(a.followerList)},
{Method: http.MethodGet, Path: "/api/v1/me/visitors", Handler: auth(a.visitorList)},
{Method: http.MethodGet, Path: "/api/v1/me/privacy", Handler: auth(a.getPrivacy)},
{Method: http.MethodPut, Path: "/api/v1/me/privacy", Handler: auth(a.updatePrivacy)},
{Method: http.MethodGet, Path: "/api/v1/me/verification", Handler: auth(a.myVerification)},
{Method: http.MethodPost, Path: "/api/v1/me/verification", Handler: auth(a.submitVerification)},
{Method: http.MethodGet, Path: "/api/v1/discover/recommendations", Handler: auth(a.discover)},
{Method: http.MethodGet, Path: "/api/v1/nearby/users", Handler: auth(a.nearby)},
{Method: http.MethodPut, Path: "/api/v1/location", Handler: auth(a.updateLocation)},
@@ -207,76 +245,114 @@ func (a *App) userRoutes() []rest.Route {
{Method: http.MethodGet, Path: "/api/v1/posts/:id", Handler: auth(a.postDetail)},
{Method: http.MethodGet, Path: "/api/v1/users/:id/posts", Handler: auth(a.userPosts)},
{Method: http.MethodPost, Path: "/api/v1/posts", Handler: auth(a.createPost)},
{Method: http.MethodPatch, Path: "/api/v1/posts/:id", Handler: auth(a.updatePost)},
{Method: http.MethodDelete, Path: "/api/v1/posts/:id", Handler: auth(a.deleteOwnPost)},
{Method: http.MethodPost, Path: "/api/v1/media/upload", Handler: auth(a.uploadMedia)},
{Method: http.MethodPost, Path: "/api/v1/posts/:id/like", Handler: auth(a.likePost)},
{Method: http.MethodDelete, Path: "/api/v1/posts/:id/like", Handler: auth(a.unlikePost)},
{Method: http.MethodGet, Path: "/api/v1/posts/:id/comments", Handler: auth(a.comments)},
{Method: http.MethodPost, Path: "/api/v1/posts/:id/comments", Handler: auth(a.createComment)},
{Method: http.MethodDelete, Path: "/api/v1/comments/:id", Handler: auth(a.deleteOwnComment)},
{Method: http.MethodPost, Path: "/api/v1/im/conversations/direct", Handler: auth(a.directConversation)},
{Method: http.MethodGet, Path: "/api/v1/im/conversations", Handler: auth(a.conversations)},
{Method: http.MethodGet, Path: "/api/v1/im/conversations/:id/messages", Handler: auth(a.messages)},
{Method: http.MethodPost, Path: "/api/v1/im/conversations/:id/messages", Handler: auth(a.sendMessageHTTP)},
{Method: http.MethodPost, Path: "/api/v1/im/messages/:id/recall", Handler: auth(a.recallMessage)},
{Method: http.MethodPatch, Path: "/api/v1/im/conversations/:id/settings", Handler: auth(a.conversationSettings)},
{Method: http.MethodGet, Path: "/api/v1/membership/status", Handler: auth(a.membershipStatus)},
{Method: http.MethodPost, Path: "/api/v1/orders", Handler: auth(a.createOrder)},
{Method: http.MethodPost, Path: "/api/v1/orders/:id/pay", Handler: auth(a.payOrder)},
{Method: http.MethodGet, Path: "/api/v1/orders/:id", Handler: auth(a.orderStatus)},
{Method: http.MethodPost, Path: "/api/v1/orders/:id/close", Handler: auth(a.closeOwnOrder)},
{Method: http.MethodPost, Path: "/api/v1/orders/:id/refund", Handler: auth(a.requestOrderRefund)},
{Method: http.MethodGet, Path: "/api/v1/me/orders", Handler: auth(a.myOrders)},
{Method: http.MethodGet, Path: "/api/v1/notifications", Handler: auth(a.notifications)},
{Method: http.MethodPost, Path: "/api/v1/notifications/read-all", Handler: auth(a.readAllNotifications)},
{Method: http.MethodPost, Path: "/api/v1/notifications/:id/read", Handler: auth(a.readNotification)},
{Method: http.MethodPost, Path: "/api/v1/reports", Handler: auth(a.createReport)},
{Method: http.MethodGet, Path: "/api/v1/me/reports", Handler: auth(a.myReports)},
{Method: http.MethodGet, Path: "/ws", Handler: a.websocket},
}
}
func (a *App) adminRoutes() []rest.Route {
auth := func(next http.HandlerFunc) http.HandlerFunc { return a.requireAuth("admin", next) }
permit := func(permission string, next http.HandlerFunc) http.HandlerFunc {
return a.requireAdminPermission(permission, next)
}
return []rest.Route{
{Method: http.MethodPost, Path: "/admin/v1/auth/logout", Handler: auth(a.adminLogout)},
{Method: http.MethodPut, Path: "/admin/v1/me/password", Handler: auth(a.adminChangePassword)},
{Method: http.MethodGet, Path: "/admin/v1/auth/codes", Handler: auth(a.adminCodes)},
{Method: http.MethodGet, Path: "/admin/v1/user/info", Handler: auth(a.adminInfo)},
{Method: http.MethodGet, Path: "/admin/v1/dashboard/overview", Handler: auth(a.dashboard)},
{Method: http.MethodGet, Path: "/admin/v1/users", Handler: auth(a.adminUsers)},
{Method: http.MethodGet, Path: "/admin/v1/users/:id", Handler: auth(a.adminUserDetail)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/profile", Handler: auth(a.adminUpdateUserProfile)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/verification", Handler: auth(a.adminUpdateVerification)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/membership", Handler: auth(a.adminUpdateMembership)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/password-reset", Handler: auth(a.adminResetUserPassword)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/force-logout", Handler: auth(a.adminForceLogout)},
{Method: http.MethodGet, Path: "/admin/v1/users/:id/sanctions", Handler: auth(a.adminUserSanctions)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/sanctions", Handler: auth(a.adminUserSanctions)},
{Method: http.MethodPost, Path: "/admin/v1/sanctions/:id/revoke", Handler: auth(a.adminRevokeSanction)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/freeze", Handler: auth(a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/unfreeze", Handler: auth(a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/ban", Handler: auth(a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/unban", Handler: auth(a.adminUserStatus)},
{Method: http.MethodGet, Path: "/admin/v1/posts", Handler: auth(a.adminPosts)},
{Method: http.MethodGet, Path: "/admin/v1/posts/:id", Handler: auth(a.adminPostDetail)},
{Method: http.MethodDelete, Path: "/admin/v1/posts/:id", Handler: auth(a.adminDeletePost)},
{Method: http.MethodGet, Path: "/admin/v1/reports", Handler: auth(a.adminReports)},
{Method: http.MethodPost, Path: "/admin/v1/reports/:id/handle", Handler: auth(a.adminHandleReport)},
{Method: http.MethodGet, Path: "/admin/v1/risk/users", Handler: auth(a.adminRiskUsers)},
{Method: http.MethodGet, Path: "/admin/v1/risk/events", Handler: auth(a.adminRiskEvents)},
{Method: http.MethodGet, Path: "/admin/v1/membership/plans", Handler: auth(a.adminPlans)},
{Method: http.MethodPost, Path: "/admin/v1/membership/plans", Handler: auth(a.adminCreatePlan)},
{Method: http.MethodPatch, Path: "/admin/v1/membership/plans/:id", Handler: auth(a.adminUpdatePlan)},
{Method: http.MethodPut, Path: "/admin/v1/membership/plans/:id", Handler: auth(a.adminUpdatePlan)},
{Method: http.MethodDelete, Path: "/admin/v1/membership/plans/:id", Handler: auth(a.adminDeletePlan)},
{Method: http.MethodGet, Path: "/admin/v1/orders", Handler: auth(a.adminOrders)},
{Method: http.MethodPut, Path: "/admin/v1/orders/:id", Handler: auth(a.adminUpdateOrder)},
{Method: http.MethodDelete, Path: "/admin/v1/orders/:id", Handler: auth(a.adminDeleteOrder)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/pay", Handler: auth(a.adminMarkOrderPaid)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/close", Handler: auth(a.adminCloseOrder)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/refund", Handler: auth(a.adminRefund)},
{Method: http.MethodGet, Path: "/admin/v1/messages", Handler: auth(a.adminMessages)},
{Method: http.MethodGet, Path: "/admin/v1/system/configs", Handler: auth(a.adminConfigs)},
{Method: http.MethodPatch, Path: "/admin/v1/system/configs/:key", Handler: auth(a.adminUpdateConfig)},
{Method: http.MethodPut, Path: "/admin/v1/system/configs/:key", Handler: auth(a.adminUpdateConfig)},
{Method: http.MethodGet, Path: "/admin/v1/integrations/:group", Handler: auth(a.adminIntegration)},
{Method: http.MethodPut, Path: "/admin/v1/integrations/:group", Handler: auth(a.adminUpdateIntegration)},
{Method: http.MethodPost, Path: "/admin/v1/integrations/:group/test", Handler: auth(a.adminTestIntegration)},
{Method: http.MethodGet, Path: "/admin/v1/audit-logs", Handler: auth(a.adminAuditLogs)},
{Method: http.MethodGet, Path: "/admin/v1/dashboard/overview", Handler: permit("dashboard:view", a.dashboard)},
{Method: http.MethodGet, Path: "/admin/v1/users", Handler: permit("users:view", a.adminUsers)},
{Method: http.MethodPost, Path: "/admin/v1/users", Handler: permit("users:create", a.adminCreateUser)},
{Method: http.MethodGet, Path: "/admin/v1/users/:id", Handler: permit("users:view", a.adminUserDetail)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/profile", Handler: permit("users:manage", a.adminUpdateUserProfile)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/verification", Handler: permit("verification:manage", a.adminUpdateVerification)},
{Method: http.MethodPut, Path: "/admin/v1/users/:id/membership", Handler: permit("users:manage", a.adminUpdateMembership)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/password-reset", Handler: permit("users:security", a.adminResetUserPassword)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/force-logout", Handler: permit("users:security", a.adminForceLogout)},
{Method: http.MethodGet, Path: "/admin/v1/users/:id/sanctions", Handler: permit("violations:manage", a.adminUserSanctions)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/sanctions", Handler: permit("violations:manage", a.adminUserSanctions)},
{Method: http.MethodPost, Path: "/admin/v1/sanctions/:id/revoke", Handler: permit("violations:manage", a.adminRevokeSanction)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/freeze", Handler: permit("violations:manage", a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/unfreeze", Handler: permit("violations:manage", a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/ban", Handler: permit("violations:manage", a.adminUserStatus)},
{Method: http.MethodPost, Path: "/admin/v1/users/:id/unban", Handler: permit("violations:manage", a.adminUserStatus)},
{Method: http.MethodGet, Path: "/admin/v1/posts", Handler: permit("content:view", a.adminPosts)},
{Method: http.MethodGet, Path: "/admin/v1/posts/:id", Handler: permit("content:view", a.adminPostDetail)},
{Method: http.MethodDelete, Path: "/admin/v1/posts/:id", Handler: permit("content:manage", a.adminDeletePost)},
{Method: http.MethodGet, Path: "/admin/v1/reports", Handler: permit("reports:handle", a.adminReports)},
{Method: http.MethodPost, Path: "/admin/v1/reports/:id/handle", Handler: permit("reports:handle", a.adminHandleReport)},
{Method: http.MethodGet, Path: "/admin/v1/risk/users", Handler: permit("risk:view", a.adminRiskUsers)},
{Method: http.MethodGet, Path: "/admin/v1/risk/events", Handler: permit("risk:view", a.adminRiskEvents)},
{Method: http.MethodGet, Path: "/admin/v1/membership/plans", Handler: permit("membership:manage", a.adminPlans)},
{Method: http.MethodPost, Path: "/admin/v1/membership/plans", Handler: permit("membership:manage", a.adminCreatePlan)},
{Method: http.MethodPatch, Path: "/admin/v1/membership/plans/:id", Handler: permit("membership:manage", a.adminUpdatePlan)},
{Method: http.MethodPut, Path: "/admin/v1/membership/plans/:id", Handler: permit("membership:manage", a.adminUpdatePlan)},
{Method: http.MethodDelete, Path: "/admin/v1/membership/plans/:id", Handler: permit("membership:manage", a.adminDeletePlan)},
{Method: http.MethodGet, Path: "/admin/v1/orders", Handler: permit("orders:view", a.adminOrders)},
{Method: http.MethodPut, Path: "/admin/v1/orders/:id", Handler: permit("orders:manage", a.adminUpdateOrder)},
{Method: http.MethodDelete, Path: "/admin/v1/orders/:id", Handler: permit("orders:manage", a.adminDeleteOrder)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/pay", Handler: permit("orders:manage", a.adminMarkOrderPaid)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/close", Handler: permit("orders:manage", a.adminCloseOrder)},
{Method: http.MethodPost, Path: "/admin/v1/orders/:id/refund", Handler: permit("orders:manage", a.adminRefund)},
{Method: http.MethodGet, Path: "/admin/v1/messages", Handler: permit("messages:view", a.adminMessages)},
{Method: http.MethodPost, Path: "/admin/v1/messages/:id/moderate", Handler: permit("messages:manage", a.adminModerateMessage)},
{Method: http.MethodGet, Path: "/admin/v1/client-feedback", Handler: permit("users:view", a.adminFeedback)},
{Method: http.MethodPut, Path: "/admin/v1/client-feedback/:id", Handler: permit("users:manage", a.adminFeedback)},
{Method: http.MethodGet, Path: "/admin/v1/account-closures", Handler: permit("users:view", a.adminAccountClosures)},
{Method: http.MethodPut, Path: "/admin/v1/account-closures/:id", Handler: permit("users:manage", a.adminAccountClosures)},
{Method: http.MethodGet, Path: "/admin/v1/app-versions", Handler: permit("system:manage", a.adminAppVersions)},
{Method: http.MethodPost, Path: "/admin/v1/app-versions", Handler: permit("system:manage", a.adminAppVersions)},
{Method: http.MethodPut, Path: "/admin/v1/app-versions/:id", Handler: permit("system:manage", a.adminAppVersions)},
{Method: http.MethodDelete, Path: "/admin/v1/app-versions/:id", Handler: permit("system:manage", a.adminAppVersions)},
{Method: http.MethodGet, Path: "/admin/v1/system/configs", Handler: permit("system:manage", a.adminConfigs)},
{Method: http.MethodPatch, Path: "/admin/v1/system/configs/:key", Handler: permit("system:manage", a.adminUpdateConfig)},
{Method: http.MethodPut, Path: "/admin/v1/system/configs/:key", Handler: permit("system:manage", a.adminUpdateConfig)},
{Method: http.MethodGet, Path: "/admin/v1/integrations/:group", Handler: permit("system:manage", a.adminIntegration)},
{Method: http.MethodPut, Path: "/admin/v1/integrations/:group", Handler: permit("system:manage", a.adminUpdateIntegration)},
{Method: http.MethodPost, Path: "/admin/v1/integrations/:group/test", Handler: permit("system:manage", a.adminTestIntegration)},
{Method: http.MethodGet, Path: "/admin/v1/ai/models", Handler: permit("system:manage", a.adminAIModels)},
{Method: http.MethodPost, Path: "/admin/v1/ai/models", Handler: permit("system:manage", a.adminCreateAIModel)},
{Method: http.MethodPut, Path: "/admin/v1/ai/models/:id", Handler: permit("system:manage", a.adminUpdateAIModel)},
{Method: http.MethodDelete, Path: "/admin/v1/ai/models/:id", Handler: permit("system:manage", a.adminDeleteAIModel)},
{Method: http.MethodPost, Path: "/admin/v1/ai/models/:id/test", Handler: permit("system:manage", a.adminTestAIModel)},
{Method: http.MethodGet, Path: "/admin/v1/ai/agents", Handler: permit("system:manage", a.adminAIAgents)},
{Method: http.MethodPut, Path: "/admin/v1/ai/agents", Handler: permit("system:manage", a.adminUpdateAIAgents)},
{Method: http.MethodGet, Path: "/admin/v1/ai/reply-jobs", Handler: permit("system:manage", a.adminAIReplyJobs)},
{Method: http.MethodGet, Path: "/admin/v1/ai/logs", Handler: permit("system:manage", a.adminAILogs)},
{Method: http.MethodGet, Path: "/admin/v1/ai/usage", Handler: permit("system:manage", a.adminAIUsage)},
{Method: http.MethodGet, Path: "/admin/v1/audit-logs", Handler: permit("system:manage", a.adminAuditLogs)},
{Method: http.MethodGet, Path: "/admin/v1/admin-users", Handler: permit("system:manage", a.adminAccounts)},
{Method: http.MethodPost, Path: "/admin/v1/admin-users", Handler: permit("system:manage", a.adminCreateAccount)},
{Method: http.MethodPut, Path: "/admin/v1/admin-users/:id", Handler: permit("system:manage", a.adminUpdateAccount)},
{Method: http.MethodGet, Path: "/admin/v1/admin-roles", Handler: permit("system:manage", a.adminRoles)},
{Method: http.MethodPost, Path: "/admin/v1/admin-roles", Handler: permit("system:manage", a.adminCreateRole)},
{Method: http.MethodPut, Path: "/admin/v1/admin-roles/:id", Handler: permit("system:manage", a.adminUpdateRole)},
{Method: http.MethodDelete, Path: "/admin/v1/admin-roles/:id", Handler: permit("system:manage", a.adminDeleteRole)},
{Method: http.MethodGet, Path: "/admin/v1/admin-permissions", Handler: permit("system:manage", a.adminPermissions)},
}
}
@@ -394,32 +470,9 @@ func validateConfig(config Config) error {
return fmt.Errorf("生产环境来源必须使用 HTTPS: %s", origin)
}
}
if config.BootstrapAdminPassword != "" && !strongAdminPassword(config.BootstrapAdminPassword) {
return fmt.Errorf("IM_BOOTSTRAP_ADMIN_PASSWORD 至少 12 位,且必须包含大小写字母、数字和特殊字符")
}
return nil
}
func strongAdminPassword(value string) bool {
if len(value) < 12 {
return false
}
var lower, upper, digit, special bool
for _, char := range value {
switch {
case char >= 'a' && char <= 'z':
lower = true
case char >= 'A' && char <= 'Z':
upper = true
case char >= '0' && char <= '9':
digit = true
default:
special = true
}
}
return lower && upper && digit && special
}
func (a *App) originAllowed(origin string) bool {
origin = strings.TrimRight(strings.TrimSpace(origin), "/")
if origin == "" {