点击图形
@ -41,7 +41,7 @@ func (c *CaptchaApi) VerifyCaptcha(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
resp, err := c.Client.VerifyCaptcha(ctx, req)
|
resp, err := c.Client.VerifyCaptcha(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "x", req.GetX(), "y", req.GetY())
|
log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "clickCount", len(req.GetClickPoints()))
|
||||||
apiresp.GinError(ctx, err)
|
apiresp.GinError(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import (
|
|||||||
pbAuth "github.com/openimsdk/protocol/auth"
|
pbAuth "github.com/openimsdk/protocol/auth"
|
||||||
pbcaptcha "github.com/openimsdk/protocol/captcha"
|
pbcaptcha "github.com/openimsdk/protocol/captcha"
|
||||||
"github.com/openimsdk/protocol/conversation"
|
"github.com/openimsdk/protocol/conversation"
|
||||||
|
pbcrypto "github.com/openimsdk/protocol/crypto"
|
||||||
"github.com/openimsdk/protocol/group"
|
"github.com/openimsdk/protocol/group"
|
||||||
"github.com/openimsdk/protocol/msg"
|
"github.com/openimsdk/protocol/msg"
|
||||||
"github.com/openimsdk/protocol/relation"
|
"github.com/openimsdk/protocol/relation"
|
||||||
pbcrypto "github.com/openimsdk/protocol/crypto"
|
|
||||||
"github.com/openimsdk/protocol/rtc"
|
"github.com/openimsdk/protocol/rtc"
|
||||||
"github.com/openimsdk/protocol/third"
|
"github.com/openimsdk/protocol/third"
|
||||||
"github.com/openimsdk/protocol/user"
|
"github.com/openimsdk/protocol/user"
|
||||||
@ -334,8 +334,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
phoneGroup := r.Group("/phone")
|
phoneGroup := r.Group("/phone")
|
||||||
phoneGroup.POST("/get_sn_info", phoneSN.GetSNInfo)
|
phoneGroup.POST("/get_sn_info", phoneSN.GetSNInfo)
|
||||||
phoneGroup.POST("/set_sn_info", phoneSN.SetSNInfo)
|
phoneGroup.POST("/set_sn_info", phoneSN.SetSNInfo)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
rc := NewRtcApi(rtc.NewRtcServiceClient(rtcConn))
|
rc := NewRtcApi(rtc.NewRtcServiceClient(rtcConn))
|
||||||
rtcGroup := r.Group("/rtc")
|
rtcGroup := r.Group("/rtc")
|
||||||
rtcGroup.POST("/signal_message_assemble", rc.SignalMessageAssemble)
|
rtcGroup.POST("/signal_message_assemble", rc.SignalMessageAssemble)
|
||||||
|
|||||||
@ -188,6 +188,9 @@ func (g *GrpcHandler) SendSignalMessage(ctx context.Context, data *Req) ([]byte,
|
|||||||
}
|
}
|
||||||
assembleReq.SignalReq = &signalReq
|
assembleReq.SignalReq = &signalReq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.ZDebug(ctx, "SendSignalMessage", "assembleReq", assembleReq)
|
||||||
|
|
||||||
resp, err := g.rtcClient.RtcServiceClient.SignalMessageAssemble(ctx, &assembleReq)
|
resp, err := g.rtcClient.RtcServiceClient.SignalMessageAssemble(ctx, &assembleReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "SendSignalMessage", err, "r", err.Error())
|
log.ZError(ctx, "SendSignalMessage", err, "r", err.Error())
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package captcha
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,10 +18,17 @@ import (
|
|||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/wenlng/go-captcha/v2/base/option"
|
"github.com/wenlng/go-captcha/v2/click"
|
||||||
"github.com/wenlng/go-captcha/v2/slide"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// alphanumChars is the character pool for the click captcha.
|
||||||
|
// Visually ambiguous characters (I, O, 0, 1, l) are excluded.
|
||||||
|
var alphanumChars = []string{
|
||||||
|
"A", "B", "C", "D", "E", "F", "G", "H", "J", "K",
|
||||||
|
"L", "M", "N", "P", "Q", "R", "S", "T", "U", "V",
|
||||||
|
"W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
RpcConfig config.Captcha
|
RpcConfig config.Captcha
|
||||||
MongodbConfig config.Mongo
|
MongodbConfig config.Mongo
|
||||||
@ -31,14 +39,14 @@ type Config struct {
|
|||||||
type server struct {
|
type server struct {
|
||||||
pbcaptcha.UnimplementedCaptchaServer
|
pbcaptcha.UnimplementedCaptchaServer
|
||||||
conf config.Captcha
|
conf config.Captcha
|
||||||
capt slide.Captcha
|
capt click.Captcha
|
||||||
collection *mongo.Collection
|
collection *mongo.Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captchaDoc is the MongoDB document that stores the verification answer.
|
||||||
type captchaDoc struct {
|
type captchaDoc struct {
|
||||||
CaptchaID string `bson:"captcha_id"`
|
CaptchaID string `bson:"captcha_id"`
|
||||||
X int `bson:"x"`
|
DotsJSON string `bson:"dots_json"` // JSON-encoded map[int]*click.Dot (answer dots)
|
||||||
Y int `bson:"y"`
|
|
||||||
ExpiredAt time.Time `bson:"expired_at"`
|
ExpiredAt time.Time `bson:"expired_at"`
|
||||||
CreateTime time.Time `bson:"create_time"`
|
CreateTime time.Time `bson:"create_time"`
|
||||||
VerifyTime time.Time `bson:"verify_time,omitempty"`
|
VerifyTime time.Time `bson:"verify_time,omitempty"`
|
||||||
@ -66,17 +74,15 @@ func Start(ctx context.Context, cfg *Config, _ discovery.SvcDiscoveryRegistry, g
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resources, err := loadResources()
|
capt, err := buildClickCaptcha()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "captcha load resources failed", err)
|
log.ZError(ctx, "captcha build click captcha failed", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := slide.NewBuilder()
|
|
||||||
builder.SetResources(resources...)
|
|
||||||
s := &server{
|
s := &server{
|
||||||
conf: cfg.RpcConfig,
|
conf: cfg.RpcConfig,
|
||||||
capt: builder.Make(),
|
capt: capt,
|
||||||
collection: collection,
|
collection: collection,
|
||||||
}
|
}
|
||||||
if s.conf.ExpireSeconds <= 0 {
|
if s.conf.ExpireSeconds <= 0 {
|
||||||
@ -95,24 +101,31 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc
|
|||||||
log.ZError(ctx, "captcha generate failed", err)
|
log.ZError(ctx, "captcha generate failed", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
block := captData.GetData()
|
|
||||||
masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(option.QualityNone)
|
dots := captData.GetData() // answer dots: map[int]*click.Dot
|
||||||
|
masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "captcha encode master image failed", err)
|
log.ZError(ctx, "captcha encode master image failed", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tileImage, err := captData.GetTileImage().ToBase64Data()
|
thumbImage, err := captData.GetThumbImage().ToBase64Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ZError(ctx, "captcha encode tile image failed", err)
|
log.ZError(ctx, "captcha encode thumb image failed", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dotsJSON, err := json.Marshal(dots)
|
||||||
|
if err != nil {
|
||||||
|
log.ZError(ctx, "captcha marshal dots failed", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
id := uuid.NewString()
|
id := uuid.NewString()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expiredAt := now.Add(time.Duration(s.conf.ExpireSeconds) * time.Second)
|
expiredAt := now.Add(time.Duration(s.conf.ExpireSeconds) * time.Second)
|
||||||
_, err = s.collection.InsertOne(ctx, captchaDoc{
|
_, err = s.collection.InsertOne(ctx, captchaDoc{
|
||||||
CaptchaID: id,
|
CaptchaID: id,
|
||||||
X: block.X,
|
DotsJSON: string(dotsJSON),
|
||||||
Y: block.Y,
|
|
||||||
ExpiredAt: expiredAt,
|
ExpiredAt: expiredAt,
|
||||||
CreateTime: now,
|
CreateTime: now,
|
||||||
})
|
})
|
||||||
@ -120,26 +133,26 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc
|
|||||||
log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id)
|
log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.ZDebug(ctx, "captcha generated", "captchaID", id, "dotCount", len(dots), "expireAt", expiredAt.Unix())
|
||||||
return &pbcaptcha.GenerateCaptchaResp{
|
return &pbcaptcha.GenerateCaptchaResp{
|
||||||
CaptchaID: id,
|
CaptchaID: id,
|
||||||
MasterImage: masterImage,
|
MasterImage: masterImage,
|
||||||
TileImage: tileImage,
|
ThumbImage: thumbImage,
|
||||||
TileY: int32(block.DY),
|
|
||||||
ExpireAt: expiredAt.Unix(),
|
ExpireAt: expiredAt.Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) {
|
func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) {
|
||||||
|
log.ZDebug(ctx, "captcha verify request", "captchaID", req.CaptchaID, "clickCount", len(req.ClickPoints))
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
filter := bson.M{
|
filter := bson.M{
|
||||||
"captcha_id": req.CaptchaID,
|
"captcha_id": req.CaptchaID,
|
||||||
"verify_time": bson.M{"$exists": false},
|
"verify_time": bson.M{"$exists": false},
|
||||||
}
|
}
|
||||||
update := bson.M{
|
update := bson.M{"$set": bson.M{"verify_time": now}}
|
||||||
"$set": bson.M{
|
|
||||||
"verify_time": now,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var doc captchaDoc
|
var doc captchaDoc
|
||||||
err := s.collection.FindOneAndUpdate(
|
err := s.collection.FindOneAndUpdate(
|
||||||
ctx,
|
ctx,
|
||||||
@ -159,9 +172,41 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha
|
|||||||
log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix())
|
log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix())
|
||||||
return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID)
|
return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID)
|
||||||
}
|
}
|
||||||
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
|
|
||||||
|
// Unmarshal the stored answer dots.
|
||||||
|
var answerDots map[int]*click.Dot
|
||||||
|
if err := json.Unmarshal([]byte(doc.DotsJSON), &answerDots); err != nil {
|
||||||
|
log.ZError(ctx, "captcha unmarshal dots failed", err, "captchaID", req.CaptchaID)
|
||||||
|
return nil, servererrs.ErrDatabase.WrapMsg("internal captcha data error")
|
||||||
|
}
|
||||||
|
|
||||||
|
success := validateClickPoints(req.ClickPoints, answerDots, s.conf.VerifyPadding)
|
||||||
if !success {
|
if !success {
|
||||||
log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y)
|
log.ZError(ctx, "captcha validate failed", nil,
|
||||||
|
"captchaID", req.CaptchaID,
|
||||||
|
"clickCount", len(req.ClickPoints),
|
||||||
|
"answerCount", len(answerDots),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.ZDebug(ctx, "captcha validate success", "captchaID", req.CaptchaID)
|
||||||
}
|
}
|
||||||
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateClickPoints checks that each user click point falls within the
|
||||||
|
// bounding box of the corresponding answer dot (in order).
|
||||||
|
func validateClickPoints(points []*pbcaptcha.ClickPoint, dots map[int]*click.Dot, padding int) bool {
|
||||||
|
if len(points) != len(dots) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, pt := range points {
|
||||||
|
dot, ok := dots[i]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !click.Validate(int(pt.X), int(pt.Y), dot.X, dot.Y, dot.Width, dot.Height, padding) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@ -2,12 +2,7 @@ package captcha
|
|||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
// resourceFS embeds background images and tile images at compile time.
|
// resourceFS embeds background images for the click captcha at compile time.
|
||||||
// Background images come from go-captcha-resources (sourcedata/images/image-{1..5}).
|
|
||||||
// Tile images come from go-captcha-resources (sourcedata/tiles/tile-{1..4}):
|
|
||||||
// overlay.png → GraphImage.OverlayImage
|
|
||||||
// shadow.png → GraphImage.ShadowImage
|
|
||||||
// mask.png → GraphImage.MaskImage
|
|
||||||
//
|
//
|
||||||
//go:embed resources/images/*.jpg resources/tiles/*/*.png
|
//go:embed resources/images/*.jpg
|
||||||
var resourceFS embed.FS
|
var resourceFS embed.FS
|
||||||
|
|||||||
@ -6,24 +6,42 @@ import (
|
|||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"github.com/wenlng/go-captcha/v2/slide"
|
"github.com/golang/freetype"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wenlng/go-captcha/v2/base/option"
|
||||||
|
"github.com/wenlng/go-captcha/v2/click"
|
||||||
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadResources reads the embedded files and returns slide.Resource options
|
// buildClickCaptcha constructs a click.Captcha instance configured with
|
||||||
// ready to be passed to slide.NewBuilder().SetResources(...).
|
// alphanumeric characters, a bundled Go font, and the embedded background images.
|
||||||
func loadResources() ([]slide.Resource, error) {
|
func buildClickCaptcha() (click.Captcha, error) {
|
||||||
|
font, err := loadGoRegularFont()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load font: %w", err)
|
||||||
|
}
|
||||||
backgrounds, err := loadBackgrounds()
|
backgrounds, err := loadBackgrounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load captcha backgrounds: %w", err)
|
return nil, fmt.Errorf("load captcha backgrounds: %w", err)
|
||||||
}
|
}
|
||||||
graphImages, err := loadGraphImages()
|
|
||||||
if err != nil {
|
builder := click.NewBuilder(
|
||||||
return nil, fmt.Errorf("load captcha graph images: %w", err)
|
click.WithRangeLen(option.RangeVal{Min: 6, Max: 8}),
|
||||||
}
|
click.WithRangeVerifyLen(option.RangeVal{Min: 3, Max: 4}),
|
||||||
return []slide.Resource{
|
click.WithRangeSize(option.RangeVal{Min: 26, Max: 34}),
|
||||||
slide.WithBackgrounds(backgrounds),
|
click.WithDisplayShadow(true),
|
||||||
slide.WithGraphImages(graphImages),
|
)
|
||||||
}, nil
|
builder.SetResources(
|
||||||
|
click.WithChars(alphanumChars),
|
||||||
|
click.WithFonts([]*truetype.Font{font}),
|
||||||
|
click.WithBackgrounds(backgrounds),
|
||||||
|
)
|
||||||
|
return builder.Make(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadGoRegularFont parses the bundled Go Regular TTF font.
|
||||||
|
func loadGoRegularFont() (*truetype.Font, error) {
|
||||||
|
return freetype.ParseFont(goregular.TTF)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadBackgrounds decodes the embedded JPEG background images.
|
// loadBackgrounds decodes the embedded JPEG background images.
|
||||||
@ -41,32 +59,6 @@ func loadBackgrounds() ([]image.Image, error) {
|
|||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadGraphImages decodes the 4 sets of tile overlay/shadow/mask PNG images.
|
|
||||||
func loadGraphImages() ([]*slide.GraphImage, error) {
|
|
||||||
const count = 4
|
|
||||||
graphs := make([]*slide.GraphImage, 0, count)
|
|
||||||
for i := 1; i <= count; i++ {
|
|
||||||
overlay, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/overlay.png", i))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode tile-%d overlay: %w", i, err)
|
|
||||||
}
|
|
||||||
shadow, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/shadow.png", i))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode tile-%d shadow: %w", i, err)
|
|
||||||
}
|
|
||||||
mask, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/mask.png", i))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode tile-%d mask: %w", i, err)
|
|
||||||
}
|
|
||||||
graphs = append(graphs, &slide.GraphImage{
|
|
||||||
OverlayImage: overlay,
|
|
||||||
ShadowImage: shadow,
|
|
||||||
MaskImage: mask,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return graphs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeEmbedImage(path string) (image.Image, error) {
|
func decodeEmbedImage(path string) (image.Image, error) {
|
||||||
f, err := resourceFS.Open(path)
|
f, err := resourceFS.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 12 KiB |
@ -48,7 +48,7 @@ func (s *rtcServer) SignalMessageAssemble(ctx context.Context, req *rtc.SignalMe
|
|||||||
)
|
)
|
||||||
switch payload := req.SignalReq.Payload.(type) {
|
switch payload := req.SignalReq.Payload.(type) {
|
||||||
case *rtc.SignalReq_Invite:
|
case *rtc.SignalReq_Invite:
|
||||||
log.ZInfo(ctx, "SignalMessageAssemble", "payload", payload.Invite)
|
log.ZDebug(ctx, "SignalMessageAssemble", "payload", payload.Invite)
|
||||||
r, err := s.handleInvite(ctx, payload.Invite, req.SignalReq)
|
r, err := s.handleInvite(ctx, payload.Invite, req.SignalReq)
|
||||||
resp.Payload = &rtc.SignalResp_Invite{Invite: r}
|
resp.Payload = &rtc.SignalResp_Invite{Invite: r}
|
||||||
respErr = err
|
respErr = err
|
||||||
|
|||||||
1
openim-sdk-core
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../openim-sdk-core-origin
|
||||||
2
protocol
@ -1 +1 @@
|
|||||||
Subproject commit 9f0b38eb5c5015da3969d6711a140c3ba12956bb
|
Subproject commit ed16bd0c4049d722e7b605c3f314ee661b9bc4e1
|
||||||
@ -13,6 +13,7 @@ serviceBinaries:
|
|||||||
openim-rpc-msg: 1
|
openim-rpc-msg: 1
|
||||||
openim-rpc-rtc: 1
|
openim-rpc-rtc: 1
|
||||||
openim-rpc-third: 1
|
openim-rpc-third: 1
|
||||||
|
openim-rpc-crypto: 1
|
||||||
toolBinaries:
|
toolBinaries:
|
||||||
- check-free-memory
|
- check-free-memory
|
||||||
- check-component
|
- check-component
|
||||||
|
|||||||