server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物
This commit is contained in:
@@ -1,24 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"everypublish/server/internal/api/response"
|
||||
"everypublish/server/internal/credentials"
|
||||
"everypublish/server/internal/models"
|
||||
"everypublish/server/internal/platform"
|
||||
"everypublish/server/internal/ws"
|
||||
"everypublish/shared/proto"
|
||||
)
|
||||
|
||||
// AccountHandler 账号台账处理器(凭据仅在 Agent 本机保险库,服务器零凭据)
|
||||
// AccountHandler 账号台账处理器(Web-only 本机执行器)。
|
||||
type AccountHandler struct {
|
||||
DB *gorm.DB
|
||||
Hub *ws.Hub
|
||||
DB *gorm.DB
|
||||
Hub *ws.Hub
|
||||
Registry *platform.Registry
|
||||
Credentials *credentials.Store
|
||||
ExecutorMode string
|
||||
PollInterval time.Duration
|
||||
pollMu sync.Mutex
|
||||
pollCancels map[uint64]context.CancelFunc
|
||||
pollGeneration map[uint64]uint64
|
||||
pollSeq uint64
|
||||
}
|
||||
|
||||
var platforms = map[string]bool{
|
||||
@@ -27,10 +39,15 @@ var platforms = map[string]bool{
|
||||
}
|
||||
|
||||
type accountReq struct {
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
AgentDeviceID uint64 `json:"agentDeviceId"`
|
||||
AccountName string `json:"accountName"`
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
AccountName string `json:"accountName"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
IPProfile string `json:"ipProfile"`
|
||||
}
|
||||
|
||||
type credentialsReq struct {
|
||||
Cookies string `json:"cookies" binding:"required"`
|
||||
}
|
||||
|
||||
// List 账号台账(筛选 platform/status)
|
||||
@@ -57,7 +74,7 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
response.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
||||
}
|
||||
|
||||
// Create 新增账号(初始 unbound;凭据只登记元数据)
|
||||
// Create 新增账号(初始 unbound;V1 凭据由本机 Web executor 管理)
|
||||
func (h *AccountHandler) Create(c *gin.Context) {
|
||||
var req accountReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -69,12 +86,14 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
account := models.Account{
|
||||
WorkspaceID: c.GetUint64("wsid"),
|
||||
Platform: req.Platform,
|
||||
Remark: req.Remark,
|
||||
AgentDeviceID: req.AgentDeviceID,
|
||||
Status: "unbound",
|
||||
Health: 100,
|
||||
WorkspaceID: c.GetUint64("wsid"),
|
||||
Platform: req.Platform,
|
||||
AccountName: req.AccountName,
|
||||
Remark: req.Remark,
|
||||
AvatarURL: req.AvatarURL,
|
||||
IPProfile: req.IPProfile,
|
||||
Status: "unbound",
|
||||
Health: 100,
|
||||
}
|
||||
if err := h.DB.Create(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "创建失败")
|
||||
@@ -91,19 +110,26 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var req accountReq
|
||||
var req struct {
|
||||
Remark string `json:"remark"`
|
||||
AccountName string `json:"accountName"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
IPProfile string `json:"ipProfile"`
|
||||
}
|
||||
if err = c.ShouldBindJSON(&req); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{"remark": req.Remark}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{"remark": req.Remark, "avatar_url": req.AvatarURL, "ip_profile": req.IPProfile}
|
||||
if req.AccountName != "" {
|
||||
updates["account_name"] = req.AccountName
|
||||
}
|
||||
if req.AgentDeviceID > 0 {
|
||||
updates["agent_device_id"] = req.AgentDeviceID
|
||||
}
|
||||
if err = h.DB.Model(&models.Account{}).Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).Updates(updates).Error; err != nil {
|
||||
if err = h.DB.Model(&account).Updates(updates).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "更新失败")
|
||||
return
|
||||
}
|
||||
@@ -131,11 +157,14 @@ func (h *AccountHandler) Delete(c *gin.Context) {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "删除失败")
|
||||
return
|
||||
}
|
||||
if h.Credentials != nil {
|
||||
_ = h.Credentials.Delete(account.WorkspaceID, account.ID)
|
||||
}
|
||||
response.Audit(c, "account.delete", "account:"+itoa(id), nil)
|
||||
response.OK(c, nil)
|
||||
}
|
||||
|
||||
// Bind 发起绑定:创建挑战记录(pending),等待客户端扫码(D4 WSS 联动)
|
||||
// Bind 发起绑定:创建网页端挑战,由当前服务器上的 Web adapter 继续处理。
|
||||
func (h *AccountHandler) Bind(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -147,19 +176,50 @@ func (h *AccountHandler) Bind(c *gin.Context) {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if account.Status == "active" {
|
||||
response.Fail(c, http.StatusConflict, 3002, "账号已绑定")
|
||||
return
|
||||
if account.Status == "binding" {
|
||||
var existing models.Challenge
|
||||
if findErr := h.DB.Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Order("id desc").First(&existing).Error; findErr == nil {
|
||||
response.OK(c, gin.H{"challengeId": existing.ID, "account": account.ID, "status": "binding", "qrUrl": existing.QRURL, "qrToken": existing.QRToken, "prompt": existing.Prompt, "expiresAt": existing.ExpiresAt})
|
||||
return
|
||||
}
|
||||
}
|
||||
var staleChallenges []models.Challenge
|
||||
_ = h.DB.Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Find(&staleChallenges).Error
|
||||
for _, stale := range staleChallenges {
|
||||
h.cancelLoginPoll(stale.ID)
|
||||
}
|
||||
if len(staleChallenges) > 0 {
|
||||
_ = h.DB.Model(&models.Challenge{}).Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Update("status", "suspended").Error
|
||||
}
|
||||
// A registered adapter owns the login session. The browser receives one QR
|
||||
// URL and the server polls it; no second client-side pairing code is needed.
|
||||
var adapter platform.Adapter
|
||||
if h.ExecutorMode != "mock" && h.Registry != nil {
|
||||
adapter = h.Registry.Get(account.Platform)
|
||||
}
|
||||
var session platform.LoginSession
|
||||
if adapter != nil {
|
||||
var beginErr error
|
||||
session, beginErr = adapter.BeginLogin(c.Request.Context())
|
||||
if beginErr != nil {
|
||||
response.Fail(c, http.StatusBadGateway, 2007, adapterErrorMessage(beginErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
challenge := models.Challenge{
|
||||
WorkspaceID: c.GetUint64("wsid"),
|
||||
AccountID: account.ID,
|
||||
Platform: account.Platform,
|
||||
Kind: "pending",
|
||||
Kind: "qr",
|
||||
Status: "active",
|
||||
Prompt: "等待客户端发起扫码,稍后此处展示二维码",
|
||||
Prompt: "请在网页中完成 " + account.Platform + " 登录",
|
||||
QRToken: session.Token,
|
||||
QRURL: session.URL,
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute),
|
||||
}
|
||||
if adapter == nil {
|
||||
challenge.QRURL = "mock://everypublish/login/" + strconv.FormatUint(account.ID, 10)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err = tx.Create(&challenge).Error; err != nil {
|
||||
return err
|
||||
@@ -170,26 +230,253 @@ func (h *AccountHandler) Bind(c *gin.Context) {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "发起绑定失败")
|
||||
return
|
||||
}
|
||||
// 定向下发:优先账号指定设备,否则工作空间首个在线设备
|
||||
if h.Hub != nil {
|
||||
target := account.AgentDeviceID
|
||||
if target == 0 || !h.Hub.AgentOnline(target) {
|
||||
if online := h.Hub.OnlineDevices(c.GetUint64("wsid")); len(online) > 0 {
|
||||
target = online[0]
|
||||
}
|
||||
}
|
||||
if target != 0 {
|
||||
env := proto.NewEnvelope(uuid.NewString(), proto.TypeChallenge, proto.Challenge{
|
||||
ChallengeID: strconv.FormatUint(challenge.ID, 10),
|
||||
AccountID: strconv.FormatUint(account.ID, 10),
|
||||
Platform: account.Platform,
|
||||
Kind: "qr",
|
||||
Prompt: "请使用手机客户端扫码登录 " + account.Platform,
|
||||
ExpiresAt: challenge.ExpiresAt.UnixMilli(),
|
||||
})
|
||||
_ = h.Hub.SendToAgent(target, env)
|
||||
}
|
||||
h.Hub.BroadcastWS(c.GetUint64("wsid"), "challenge.status", gin.H{
|
||||
"challengeId": challenge.ID, "accountId": account.ID, "status": challenge.Status,
|
||||
"kind": challenge.Kind, "platform": challenge.Platform, "qrUrl": challenge.QRURL,
|
||||
"qrToken": challenge.QRToken, "prompt": challenge.Prompt, "expiresAt": challenge.ExpiresAt,
|
||||
})
|
||||
}
|
||||
response.Audit(c, "account.bind", "account:"+itoa(account.ID), gin.H{"challengeId": challenge.ID})
|
||||
response.OK(c, gin.H{"challengeId": challenge.ID, "account": account.ID, "status": "binding"})
|
||||
if adapter != nil {
|
||||
h.StartLoginPoll(challenge, account, c.GetUint64("uid"), adapter)
|
||||
}
|
||||
response.OK(c, gin.H{"challengeId": challenge.ID, "account": account.ID, "status": "binding", "qrUrl": challenge.QRURL, "qrToken": challenge.QRToken, "prompt": challenge.Prompt, "expiresAt": challenge.ExpiresAt})
|
||||
}
|
||||
|
||||
// ImportCredentials accepts a user-supplied platform cookie string. The value
|
||||
// is written directly to the encrypted adapter store and never returned or
|
||||
// included in audit details.
|
||||
func (h *AccountHandler) ImportCredentials(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var req credentialsReq
|
||||
if err = c.ShouldBindJSON(&req); err != nil || len(strings.TrimSpace(req.Cookies)) < 8 || len(req.Cookies) > 64*1024 {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "凭据格式不合法")
|
||||
return
|
||||
}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if h.ExecutorMode == "mock" || h.Registry == nil {
|
||||
response.Fail(c, http.StatusConflict, 3006, "当前 mock 模式不支持真实凭据导入")
|
||||
return
|
||||
}
|
||||
adapter := h.Registry.Get(account.Platform)
|
||||
if adapter == nil {
|
||||
response.Fail(c, http.StatusConflict, 3006, "该平台尚未接入真实适配器")
|
||||
return
|
||||
}
|
||||
if err = adapter.SaveCredentials(account.WorkspaceID, account.ID, strings.TrimSpace(req.Cookies)); err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, adapterErrorMessage(err))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if err = h.DB.Model(&account).Updates(map[string]interface{}{"status": "active", "last_active_at": &now, "last_error": ""}).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "账号状态更新失败")
|
||||
return
|
||||
}
|
||||
var activeChallenges []models.Challenge
|
||||
_ = h.DB.Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Find(&activeChallenges).Error
|
||||
for _, challenge := range activeChallenges {
|
||||
h.cancelLoginPoll(challenge.ID)
|
||||
}
|
||||
_ = h.DB.Model(&models.Challenge{}).Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Update("status", "suspended").Error
|
||||
_ = Notify(h.DB, account.WorkspaceID, []uint64{c.GetUint64("uid")}, "challenge", "账号凭据已导入", "账号已恢复可用")
|
||||
if h.Hub != nil {
|
||||
h.Hub.BroadcastWS(account.WorkspaceID, "account.status", gin.H{"accountId": account.ID, "status": "active", "platform": account.Platform, "prompt": "凭据已导入"})
|
||||
}
|
||||
response.Audit(c, "account.credentials_import", "account:"+itoa(account.ID), gin.H{"platform": account.Platform})
|
||||
response.OK(c, gin.H{"accountId": account.ID, "status": "active", "lastActiveAt": now})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Check(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if h.Registry == nil || h.ExecutorMode == "mock" {
|
||||
response.Fail(c, http.StatusConflict, 3006, "当前模式不支持真实账号检查")
|
||||
return
|
||||
}
|
||||
checker, ok := h.Registry.Get(account.Platform).(platform.CredentialChecker)
|
||||
if !ok {
|
||||
response.Fail(c, http.StatusConflict, 3006, "该平台尚未提供账号检查")
|
||||
return
|
||||
}
|
||||
if err = checker.CheckCredentials(c.Request.Context(), account.WorkspaceID, account.ID); err != nil {
|
||||
message := adapterErrorMessage(err)
|
||||
_ = h.DB.Model(&account).Updates(map[string]interface{}{"status": "expired", "last_error": message}).Error
|
||||
response.Fail(c, http.StatusBadGateway, 2007, message)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_ = h.DB.Model(&account).Updates(map[string]interface{}{"status": "active", "last_active_at": &now, "last_error": ""}).Error
|
||||
response.Audit(c, "account.credentials_check", "account:"+itoa(account.ID), gin.H{"platform": account.Platform})
|
||||
response.OK(c, gin.H{"accountId": account.ID, "status": "active", "lastActiveAt": now})
|
||||
}
|
||||
|
||||
// Unbind 解绑账号,清除本机执行器关联但保留账号台账记录。
|
||||
func (h *AccountHandler) Unbind(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
|
||||
return
|
||||
}
|
||||
var account models.Account
|
||||
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&account).Error; err != nil {
|
||||
response.Fail(c, http.StatusNotFound, 1004, "账号不存在")
|
||||
return
|
||||
}
|
||||
if err = h.DB.Model(&account).Updates(map[string]interface{}{
|
||||
"status": "unbound", "account_name": "", "last_error": "",
|
||||
}).Error; err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 5000, "解绑失败")
|
||||
return
|
||||
}
|
||||
_ = h.DB.Model(&models.Challenge{}).Where("account_id = ? and workspace_id = ? and status = ?", account.ID, account.WorkspaceID, "active").Update("status", "suspended").Error
|
||||
if h.Credentials != nil {
|
||||
_ = h.Credentials.Delete(account.WorkspaceID, account.ID)
|
||||
}
|
||||
response.Audit(c, "account.unbind", "account:"+itoa(id), nil)
|
||||
response.OK(c, nil)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) pollLogin(ctx context.Context, challengeID uint64, account models.Account, userID uint64, adapter platform.Adapter) {
|
||||
interval := h.PollInterval
|
||||
if interval <= 0 {
|
||||
interval = 2 * time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
var challenge models.Challenge
|
||||
if err := h.DB.Where("id = ? and workspace_id = ?", challengeID, account.WorkspaceID).First(&challenge).Error; err != nil || challenge.Status != "active" {
|
||||
return
|
||||
}
|
||||
if time.Now().After(challenge.ExpiresAt) {
|
||||
h.finishLogin(challenge, account, userID, "expired", "登录二维码已过期")
|
||||
return
|
||||
}
|
||||
poll, err := adapter.PollLogin(ctx, challenge.QRToken)
|
||||
if err == nil {
|
||||
switch poll.State {
|
||||
case platform.LoginPending, platform.LoginScanned:
|
||||
prompt := poll.Prompt
|
||||
if prompt == "" {
|
||||
prompt = challenge.Prompt
|
||||
}
|
||||
_ = h.DB.Model(&challenge).Update("prompt", prompt).Error
|
||||
h.broadcastChallenge(challenge, string(poll.State), prompt)
|
||||
case platform.LoginConfirmed:
|
||||
if saveErr := adapter.SaveCredentials(account.WorkspaceID, account.ID, poll.Cookies); saveErr != nil {
|
||||
h.finishLogin(challenge, account, userID, "suspended", adapterErrorMessage(saveErr))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if h.DB.Model(&challenge).Updates(map[string]interface{}{"status": "solved", "solved_at": &now, "prompt": "登录成功"}).Error != nil {
|
||||
return
|
||||
}
|
||||
_ = h.DB.Model(&models.Account{}).Where("id = ? and workspace_id = ?", account.ID, account.WorkspaceID).Updates(map[string]interface{}{"status": "active", "last_active_at": &now, "last_error": ""}).Error
|
||||
_ = Notify(h.DB, account.WorkspaceID, []uint64{userID}, "challenge", "登录挑战已完成", "账号已恢复可用")
|
||||
h.broadcastChallenge(challenge, "solved", "登录成功")
|
||||
return
|
||||
}
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
||||
h.finishLogin(challenge, account, userID, "expired", "登录等待已超时")
|
||||
return
|
||||
} else if platform.CategoryOf(err) != platform.Network {
|
||||
h.finishLogin(challenge, account, userID, "suspended", adapterErrorMessage(err))
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
h.finishLogin(challenge, account, userID, "expired", "登录等待已超时")
|
||||
}
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartLoginPoll is shared with challenge resend so every real QR session has
|
||||
// exactly one backend worker responsible for polling and credential persistence.
|
||||
func (h *AccountHandler) StartLoginPoll(challenge models.Challenge, account models.Account, userID uint64, adapter platform.Adapter) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
h.pollMu.Lock()
|
||||
if h.pollCancels == nil {
|
||||
h.pollCancels = make(map[uint64]context.CancelFunc)
|
||||
}
|
||||
if h.pollGeneration == nil {
|
||||
h.pollGeneration = make(map[uint64]uint64)
|
||||
}
|
||||
if old := h.pollCancels[challenge.ID]; old != nil {
|
||||
old()
|
||||
}
|
||||
h.pollSeq++
|
||||
generation := h.pollSeq
|
||||
h.pollCancels[challenge.ID] = cancel
|
||||
h.pollGeneration[challenge.ID] = generation
|
||||
h.pollMu.Unlock()
|
||||
go func() {
|
||||
h.pollLogin(ctx, challenge.ID, account, userID, adapter)
|
||||
h.pollMu.Lock()
|
||||
if h.pollGeneration[challenge.ID] == generation {
|
||||
delete(h.pollCancels, challenge.ID)
|
||||
delete(h.pollGeneration, challenge.ID)
|
||||
}
|
||||
h.pollMu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *AccountHandler) cancelLoginPoll(challengeID uint64) {
|
||||
h.pollMu.Lock()
|
||||
cancel := h.pollCancels[challengeID]
|
||||
h.pollMu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) finishLogin(challenge models.Challenge, account models.Account, userID uint64, status, message string) {
|
||||
_ = h.DB.Model(&challenge).Updates(map[string]interface{}{"status": status, "prompt": message}).Error
|
||||
accountStatus := "expired"
|
||||
if status == "suspended" {
|
||||
accountStatus = "suspended"
|
||||
}
|
||||
_ = h.DB.Model(&models.Account{}).Where("id = ? and workspace_id = ?", account.ID, account.WorkspaceID).Updates(map[string]interface{}{"status": accountStatus, "last_error": message}).Error
|
||||
_ = Notify(h.DB, account.WorkspaceID, []uint64{userID}, "challenge", "登录需要处理", message)
|
||||
h.broadcastChallenge(challenge, status, message)
|
||||
}
|
||||
|
||||
func (h *AccountHandler) broadcastChallenge(challenge models.Challenge, status, prompt string) {
|
||||
if h.Hub == nil {
|
||||
return
|
||||
}
|
||||
h.Hub.BroadcastWS(challenge.WorkspaceID, "challenge.status", gin.H{
|
||||
"challengeId": challenge.ID, "accountId": challenge.AccountID, "status": status,
|
||||
"kind": challenge.Kind, "platform": challenge.Platform, "prompt": prompt,
|
||||
})
|
||||
}
|
||||
|
||||
func adapterErrorMessage(err error) string {
|
||||
var pe *platform.Error
|
||||
if errors.As(err, &pe) && strings.TrimSpace(pe.Message) != "" {
|
||||
return pe.Message
|
||||
}
|
||||
return "平台登录暂时不可用,请稍后重试"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user