@@ -53,6 +53,7 @@ type Server struct {
5353 onToolsCatalog func () interface {}
5454 whatsAppBridge * channels.WhatsAppBridgeService
5555 whatsAppBase string
56+ weixinChannel * channels.WeixinChannel
5657 oauthFlowMu sync.Mutex
5758 oauthFlows map [string ]* providers.OAuthPendingFlow
5859 extraRoutesMu sync.RWMutex
@@ -109,6 +110,10 @@ func (s *Server) SetWhatsAppBridge(service *channels.WhatsAppBridgeService, base
109110 s .whatsAppBase = strings .TrimSpace (basePath )
110111}
111112
113+ func (s * Server ) SetWeixinChannel (ch * channels.WeixinChannel ) {
114+ s .weixinChannel = ch
115+ }
116+
112117func (s * Server ) handleWhatsAppBridgeWS (w http.ResponseWriter , r * http.Request ) {
113118 if s .whatsAppBridge == nil {
114119 http .Error (w , "whatsapp bridge unavailable" , http .StatusServiceUnavailable )
@@ -190,6 +195,12 @@ func (s *Server) Start(ctx context.Context) error {
190195 mux .HandleFunc ("/api/whatsapp/status" , s .handleWebUIWhatsAppStatus )
191196 mux .HandleFunc ("/api/whatsapp/logout" , s .handleWebUIWhatsAppLogout )
192197 mux .HandleFunc ("/api/whatsapp/qr.svg" , s .handleWebUIWhatsAppQR )
198+ mux .HandleFunc ("/api/weixin/status" , s .handleWebUIWeixinStatus )
199+ mux .HandleFunc ("/api/weixin/login/start" , s .handleWebUIWeixinLoginStart )
200+ mux .HandleFunc ("/api/weixin/login/cancel" , s .handleWebUIWeixinLoginCancel )
201+ mux .HandleFunc ("/api/weixin/qr.svg" , s .handleWebUIWeixinQR )
202+ mux .HandleFunc ("/api/weixin/accounts/remove" , s .handleWebUIWeixinAccountRemove )
203+ mux .HandleFunc ("/api/weixin/accounts/default" , s .handleWebUIWeixinAccountDefault )
193204 mux .HandleFunc ("/api/upload" , s .handleWebUIUpload )
194205 mux .HandleFunc ("/api/cron" , s .handleWebUICron )
195206 mux .HandleFunc ("/api/skills" , s .handleWebUISkills )
@@ -1318,6 +1329,227 @@ func (s *Server) webUIWhatsAppStatusPayload(ctx context.Context) (map[string]int
13181329 }, http .StatusOK
13191330}
13201331
1332+ func (s * Server ) handleWebUIWeixinStatus (w http.ResponseWriter , r * http.Request ) {
1333+ if ! s .checkAuth (r ) {
1334+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1335+ return
1336+ }
1337+ if r .Method != http .MethodGet {
1338+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1339+ return
1340+ }
1341+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1342+ writeJSONStatus (w , code , payload )
1343+ }
1344+
1345+ func (s * Server ) handleWebUIWeixinLoginStart (w http.ResponseWriter , r * http.Request ) {
1346+ if ! s .checkAuth (r ) {
1347+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1348+ return
1349+ }
1350+ if r .Method != http .MethodPost {
1351+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1352+ return
1353+ }
1354+ if s .weixinChannel == nil {
1355+ http .Error (w , "weixin channel unavailable" , http .StatusServiceUnavailable )
1356+ return
1357+ }
1358+ if _ , err := s .weixinChannel .StartLogin (r .Context ()); err != nil {
1359+ http .Error (w , err .Error (), http .StatusBadGateway )
1360+ return
1361+ }
1362+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1363+ writeJSONStatus (w , code , payload )
1364+ }
1365+
1366+ func (s * Server ) handleWebUIWeixinLoginCancel (w http.ResponseWriter , r * http.Request ) {
1367+ if ! s .checkAuth (r ) {
1368+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1369+ return
1370+ }
1371+ if r .Method != http .MethodPost {
1372+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1373+ return
1374+ }
1375+ if s .weixinChannel == nil {
1376+ http .Error (w , "weixin channel unavailable" , http .StatusServiceUnavailable )
1377+ return
1378+ }
1379+ var body struct {
1380+ LoginID string `json:"login_id"`
1381+ }
1382+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
1383+ http .Error (w , "invalid json body" , http .StatusBadRequest )
1384+ return
1385+ }
1386+ if ! s .weixinChannel .CancelPendingLogin (body .LoginID ) {
1387+ http .Error (w , "login_id not found" , http .StatusNotFound )
1388+ return
1389+ }
1390+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1391+ writeJSONStatus (w , code , payload )
1392+ }
1393+
1394+ func (s * Server ) handleWebUIWeixinQR (w http.ResponseWriter , r * http.Request ) {
1395+ if ! s .checkAuth (r ) {
1396+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1397+ return
1398+ }
1399+ if r .Method != http .MethodGet {
1400+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1401+ return
1402+ }
1403+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1404+ if code != http .StatusOK {
1405+ http .Error (w , "qr unavailable" , http .StatusNotFound )
1406+ return
1407+ }
1408+ qrCode := ""
1409+ loginID := strings .TrimSpace (r .URL .Query ().Get ("login_id" ))
1410+ if loginID != "" && s .weixinChannel != nil {
1411+ if pending := s .weixinChannel .PendingLoginByID (loginID ); pending != nil {
1412+ qrCode = fallbackString (pending .QRCodeImgContent , pending .QRCode )
1413+ }
1414+ }
1415+ if qrCode == "" {
1416+ pendingItems , _ := payload ["pending_logins" ].([]interface {})
1417+ if len (pendingItems ) > 0 {
1418+ if pending , ok := pendingItems [0 ].(map [string ]interface {}); ok {
1419+ qrCode = fallbackString (stringFromMap (pending , "qr_code_img_content" ), stringFromMap (pending , "qr_code" ))
1420+ }
1421+ }
1422+ }
1423+ if strings .TrimSpace (qrCode ) == "" {
1424+ http .Error (w , "qr unavailable" , http .StatusNotFound )
1425+ return
1426+ }
1427+ qrImage , err := qr .Encode (strings .TrimSpace (qrCode ), qr .M )
1428+ if err != nil {
1429+ http .Error (w , err .Error (), http .StatusBadGateway )
1430+ return
1431+ }
1432+ w .Header ().Set ("Content-Type" , "image/svg+xml" )
1433+ _ , _ = io .WriteString (w , renderQRCodeSVG (qrImage , 8 , 24 ))
1434+ }
1435+
1436+ func (s * Server ) handleWebUIWeixinAccountRemove (w http.ResponseWriter , r * http.Request ) {
1437+ if ! s .checkAuth (r ) {
1438+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1439+ return
1440+ }
1441+ if r .Method != http .MethodPost {
1442+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1443+ return
1444+ }
1445+ if s .weixinChannel == nil {
1446+ http .Error (w , "weixin channel unavailable" , http .StatusServiceUnavailable )
1447+ return
1448+ }
1449+ var body struct {
1450+ BotID string `json:"bot_id"`
1451+ }
1452+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
1453+ http .Error (w , "invalid json body" , http .StatusBadRequest )
1454+ return
1455+ }
1456+ if err := s .weixinChannel .RemoveAccount (body .BotID ); err != nil {
1457+ http .Error (w , err .Error (), http .StatusBadRequest )
1458+ return
1459+ }
1460+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1461+ writeJSONStatus (w , code , payload )
1462+ }
1463+
1464+ func (s * Server ) handleWebUIWeixinAccountDefault (w http.ResponseWriter , r * http.Request ) {
1465+ if ! s .checkAuth (r ) {
1466+ http .Error (w , "unauthorized" , http .StatusUnauthorized )
1467+ return
1468+ }
1469+ if r .Method != http .MethodPost {
1470+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
1471+ return
1472+ }
1473+ if s .weixinChannel == nil {
1474+ http .Error (w , "weixin channel unavailable" , http .StatusServiceUnavailable )
1475+ return
1476+ }
1477+ var body struct {
1478+ BotID string `json:"bot_id"`
1479+ }
1480+ if err := json .NewDecoder (r .Body ).Decode (& body ); err != nil {
1481+ http .Error (w , "invalid json body" , http .StatusBadRequest )
1482+ return
1483+ }
1484+ if err := s .weixinChannel .SetDefaultAccount (body .BotID ); err != nil {
1485+ http .Error (w , err .Error (), http .StatusBadRequest )
1486+ return
1487+ }
1488+ payload , code := s .webUIWeixinStatusPayload (r .Context ())
1489+ writeJSONStatus (w , code , payload )
1490+ }
1491+
1492+ func (s * Server ) webUIWeixinStatusPayload (ctx context.Context ) (map [string ]interface {}, int ) {
1493+ cfg , err := s .loadConfig ()
1494+ if err != nil {
1495+ return map [string ]interface {}{
1496+ "ok" : false ,
1497+ "error" : err .Error (),
1498+ }, http .StatusInternalServerError
1499+ }
1500+ weixinCfg := cfg .Channels .Weixin
1501+ if s .weixinChannel == nil {
1502+ return map [string ]interface {}{
1503+ "ok" : false ,
1504+ "enabled" : weixinCfg .Enabled ,
1505+ "base_url" : weixinCfg .BaseURL ,
1506+ "error" : "weixin channel unavailable" ,
1507+ }, http .StatusOK
1508+ }
1509+ pendingLogins , err := s .weixinChannel .RefreshLoginStatuses (ctx )
1510+ if err != nil {
1511+ return map [string ]interface {}{
1512+ "ok" : false ,
1513+ "enabled" : weixinCfg .Enabled ,
1514+ "base_url" : weixinCfg .BaseURL ,
1515+ "error" : err .Error (),
1516+ }, http .StatusOK
1517+ }
1518+ accounts := s .weixinChannel .ListAccounts ()
1519+ pendingPayload := make ([]map [string ]interface {}, 0 , len (pendingLogins ))
1520+ for _ , pending := range pendingLogins {
1521+ pendingPayload = append (pendingPayload , map [string ]interface {}{
1522+ "login_id" : pendingString (pending , "login_id" ),
1523+ "qr_code" : pendingString (pending , "qr_code" ),
1524+ "qr_code_img_content" : pendingString (pending , "qr_code_img_content" ),
1525+ "status" : pendingString (pending , "status" ),
1526+ "last_error" : pendingString (pending , "last_error" ),
1527+ "updated_at" : pendingString (pending , "updated_at" ),
1528+ "qr_available" : pending != nil && strings .TrimSpace (fallbackString (pending .QRCodeImgContent , pending .QRCode )) != "" ,
1529+ })
1530+ }
1531+ var firstPending * channels.WeixinPendingLogin
1532+ if len (pendingLogins ) > 0 {
1533+ firstPending = pendingLogins [0 ]
1534+ }
1535+ return map [string ]interface {}{
1536+ "ok" : true ,
1537+ "enabled" : weixinCfg .Enabled ,
1538+ "base_url" : fallbackString (weixinCfg .BaseURL , "https://ilinkai.weixin.qq.com" ),
1539+ "pending_logins" : pendingPayload ,
1540+ "pending_login" : map [string ]interface {}{
1541+ "login_id" : pendingString (firstPending , "login_id" ),
1542+ "qr_code" : pendingString (firstPending , "qr_code" ),
1543+ "qr_code_img_content" : pendingString (firstPending , "qr_code_img_content" ),
1544+ "status" : pendingString (firstPending , "status" ),
1545+ "last_error" : pendingString (firstPending , "last_error" ),
1546+ "updated_at" : pendingString (firstPending , "updated_at" ),
1547+ "qr_available" : firstPending != nil && strings .TrimSpace (fallbackString (firstPending .QRCodeImgContent , firstPending .QRCode )) != "" ,
1548+ },
1549+ "accounts" : accounts ,
1550+ }, http .StatusOK
1551+ }
1552+
13211553func (s * Server ) loadConfig () (* cfgpkg.Config , error ) {
13221554 configPath := strings .TrimSpace (s .configPath )
13231555 if configPath == "" {
@@ -1768,6 +2000,28 @@ func fallbackString(value, fallback string) string {
17682000 return strings .TrimSpace (fallback )
17692001}
17702002
2003+ func pendingString (item * channels.WeixinPendingLogin , key string ) string {
2004+ if item == nil {
2005+ return ""
2006+ }
2007+ switch strings .TrimSpace (key ) {
2008+ case "login_id" :
2009+ return strings .TrimSpace (item .LoginID )
2010+ case "qr_code" :
2011+ return strings .TrimSpace (item .QRCode )
2012+ case "qr_code_img_content" :
2013+ return strings .TrimSpace (item .QRCodeImgContent )
2014+ case "status" :
2015+ return strings .TrimSpace (item .Status )
2016+ case "last_error" :
2017+ return strings .TrimSpace (item .LastError )
2018+ case "updated_at" :
2019+ return strings .TrimSpace (item .UpdatedAt )
2020+ default :
2021+ return ""
2022+ }
2023+ }
2024+
17712025func (s * Server ) handleWebUICron (w http.ResponseWriter , r * http.Request ) {
17722026 if ! s .checkAuth (r ) {
17732027 http .Error (w , "unauthorized" , http .StatusUnauthorized )
0 commit comments