feat: 数据上报

This commit is contained in:
yokowu
2025-07-21 16:45:39 +08:00
parent d86dcd945c
commit 8f9041b84a
15 changed files with 383 additions and 1 deletions

View File

@@ -87,6 +87,18 @@ jobs:
VERSION=${GITHUB_REF#refs/tags/}
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- name: Get build time
id: get_build_time
run: |
BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo "BUILD_TIME=${BUILD_TIME}" >> $GITHUB_OUTPUT
- name: Get git commit
id: get_git_commit
run: |
GIT_COMMIT=$(git rev-parse HEAD)
echo "GIT_COMMIT=${GIT_COMMIT}" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -114,5 +126,8 @@ jobs:
GOCACHE=/tmp/go-build
GOMODCACHE=/tmp/go-mod
REPO_COMMIT=${{ github.sha }}
VERSION=${{ steps.get_version.outputs.VERSION }}
BUILD_TIME=${{ steps.get_build_time.outputs.BUILD_TIME }}
GIT_COMMIT=${{ steps.get_git_commit.outputs.GIT_COMMIT }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -4,6 +4,9 @@ OUTPUT=type=docker,dest=${HOME}/tmp/monkeycode_server.tar
GOCACHE=${HOME}/.cache/go-build
GOMODCACHE=${HOME}/go/pkg/mod
REGISTRY=monkeycode.chaitin.cn/monkeycode
VERSION=dev-${shell git rev-parse HEAD}
BUILD_TIME=${shell date -u +"%Y-%m-%dT%H:%M:%SZ"}
GIT_COMMIT=${shell git rev-parse HEAD}
# make build PLATFORM= TAG= OUTPUT= GOCACHE=
image:
@@ -12,6 +15,9 @@ image:
--build-arg GOCACHE=${GOCACHE} \
--build-arg GOMODCACHE=${GOMODCACHE} \
--build-arg REPO_COMMIT=$(shell git rev-parse HEAD) \
--build-arg VERSION=${VERSION} \
--build-arg BUILD_TIME=${BUILD_TIME} \
--build-arg GIT_COMMIT=${GIT_COMMIT} \
--platform ${PLATFORM} \
--tag ${REGISTRY}/backend:${TAG} \
--output ${OUTPUT} \

View File

@@ -9,10 +9,17 @@ RUN --mount=type=cache,target=${GOMODCACHE} \
go mod download
ARG TARGETOS TARGETARCH GOCACHE
ARG VERSION
ARG BUILD_TIME
ARG GIT_COMMIT
RUN --mount=type=bind,target=. \
--mount=type=cache,target=${GOMODCACHE} \
--mount=type=cache,target=${GOCACHE} \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/main cmd/server/main.go cmd/server/wire_gen.go
GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build \
-ldflags "-w -s -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.Version=${VERSION}' -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.BuildTime=${BUILD_TIME}' -X 'github.com/chaitin/MonkeyCode/backend/pkg/version.GitCommit=${GIT_COMMIT}'" \
-o /out/main \
cmd/server/main.go cmd/server/wire_gen.go
FROM alpine:3.21 as binary

View File

@@ -24,6 +24,7 @@ func main() {
panic(err)
}
s.version.Print()
s.logger.With("config", s.config).Debug("config")
if s.config.Debug {
@@ -40,6 +41,10 @@ func main() {
panic(err)
}
if err := s.report.ReportInstallation(); err != nil {
panic(err)
}
svc := service.NewService(service.WithPprof())
svc.Add(s)
if err := svc.Run(); err != nil {

View File

@@ -17,6 +17,8 @@ import (
v1 "github.com/chaitin/MonkeyCode/backend/internal/model/handler/http/v1"
openaiV1 "github.com/chaitin/MonkeyCode/backend/internal/openai/handler/v1"
userV1 "github.com/chaitin/MonkeyCode/backend/internal/user/handler/v1"
"github.com/chaitin/MonkeyCode/backend/pkg/report"
"github.com/chaitin/MonkeyCode/backend/pkg/version"
)
type Server struct {
@@ -29,6 +31,8 @@ type Server struct {
userV1 *userV1.UserHandler
dashboardV1 *dashv1.DashboardHandler
billingV1 *billingv1.BillingHandler
version *version.VersionInfo
report *report.Reporter
}
func newServer() (*Server, error) {

View File

@@ -34,8 +34,10 @@ import (
"github.com/chaitin/MonkeyCode/backend/pkg"
"github.com/chaitin/MonkeyCode/backend/pkg/ipdb"
"github.com/chaitin/MonkeyCode/backend/pkg/logger"
"github.com/chaitin/MonkeyCode/backend/pkg/report"
"github.com/chaitin/MonkeyCode/backend/pkg/session"
"github.com/chaitin/MonkeyCode/backend/pkg/store"
"github.com/chaitin/MonkeyCode/backend/pkg/version"
"log/slog"
)
@@ -82,6 +84,8 @@ func newServer() (*Server, error) {
billingRepo := repo7.NewBillingRepo(client)
billingUsecase := usecase6.NewBillingUsecase(billingRepo)
billingHandler := v1_5.NewBillingHandler(web, billingUsecase, authMiddleware, activeMiddleware)
versionInfo := version.NewVersionInfo()
reporter := report.NewReport(slogLogger, configConfig, versionInfo)
server := &Server{
config: configConfig,
web: web,
@@ -92,6 +96,8 @@ func newServer() (*Server, error) {
userV1: userHandler,
dashboardV1: dashboardHandler,
billingV1: billingHandler,
version: versionInfo,
report: reporter,
}
return server, nil
}
@@ -108,4 +114,6 @@ type Server struct {
userV1 *v1_3.UserHandler
dashboardV1 *v1_4.DashboardHandler
billingV1 *v1_5.BillingHandler
version *version.VersionInfo
report *report.Reporter
}

View File

@@ -67,6 +67,10 @@ type Config struct {
LimitSecond int `mapstructure:"limit_second"`
Limit int `mapstructure:"limit"`
} `mapstructure:"extension"`
DataReport struct {
Key string `mapstructure:"key"`
} `mapstructure:"data_report"`
}
func Init() (*Config, error) {
@@ -103,6 +107,7 @@ func Init() (*Config, error) {
v.SetDefault("extension.baseurl", "https://release.baizhi.cloud")
v.SetDefault("extension.limit", 1)
v.SetDefault("extension.limit_second", 10)
v.SetDefault("data_report.key", "")
c := Config{}
if err := v.Unmarshal(&c); err != nil {

View File

@@ -24,6 +24,7 @@ import (
userV1 "github.com/chaitin/MonkeyCode/backend/internal/user/handler/v1"
userrepo "github.com/chaitin/MonkeyCode/backend/internal/user/repo"
userusecase "github.com/chaitin/MonkeyCode/backend/internal/user/usecase"
"github.com/chaitin/MonkeyCode/backend/pkg/version"
)
var Provider = wire.NewSet(
@@ -50,4 +51,5 @@ var Provider = wire.NewSet(
billingusecase.NewBillingUsecase,
erepo.NewExtensionRepo,
eusecase.NewExtensionUsecase,
version.NewVersionInfo,
)

60
backend/pkg/aes/aes.go Normal file
View File

@@ -0,0 +1,60 @@
package aes
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
)
func Encrypt(key []byte, plaintext string) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func Decrypt(key []byte, ciphertext string) (string, error) {
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, text := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, text, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}

View File

@@ -0,0 +1,102 @@
package machine
import (
"crypto/md5"
"crypto/sha256"
"fmt"
"net"
"os"
"runtime"
"sort"
"strings"
)
type MachineInfo struct {
Hostname string `json:"hostname"`
MACAddresses []string `json:"mac_addresses"`
OSType string `json:"os_type"`
OSRelease string `json:"os_release"`
CPUInfo string `json:"cpu_info"`
}
func GetMachineInfo() (*MachineInfo, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("failed to get hostname: %w", err)
}
macAddresses, err := getMACAddresses()
if err != nil {
return nil, fmt.Errorf("failed to get MAC addresses: %w", err)
}
return &MachineInfo{
Hostname: hostname,
MACAddresses: macAddresses,
OSType: runtime.GOOS,
OSRelease: getOSRelease(),
CPUInfo: getCPUInfo(),
}, nil
}
func GenerateMachineID() (string, error) {
machineInfo, err := GetMachineInfo()
if err != nil {
return "", err
}
var parts []string
parts = append(parts, machineInfo.Hostname)
parts = append(parts, strings.Join(machineInfo.MACAddresses, ","))
parts = append(parts, machineInfo.OSType)
parts = append(parts, machineInfo.OSRelease)
parts = append(parts, machineInfo.CPUInfo)
combined := strings.Join(parts, "|")
hash := sha256.Sum256([]byte(combined))
return fmt.Sprintf("%x", hash), nil
}
func GenerateShortMachineID() (string, error) {
machineInfo, err := GetMachineInfo()
if err != nil {
return "", err
}
var parts []string
parts = append(parts, machineInfo.Hostname)
if len(machineInfo.MACAddresses) > 0 {
parts = append(parts, machineInfo.MACAddresses[0])
}
parts = append(parts, machineInfo.OSType)
combined := strings.Join(parts, "|")
hash := md5.Sum([]byte(combined))
return fmt.Sprintf("%x", hash), nil
}
func getMACAddresses() ([]string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
var macAddresses []string
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 || len(iface.HardwareAddr) == 0 {
continue
}
macAddresses = append(macAddresses, iface.HardwareAddr.String())
}
sort.Strings(macAddresses)
return macAddresses, nil
}
func getOSRelease() string {
return runtime.GOARCH
}
func getCPUInfo() string {
return fmt.Sprintf("%s_%d", runtime.GOARCH, runtime.NumCPU())
}

View File

@@ -0,0 +1,13 @@
package machine
import (
"testing"
)
func TestGenerateShortMachineID(t *testing.T) {
machineID, err := GenerateMachineID()
if err != nil {
t.Fatal(err)
}
t.Log(machineID)
}

View File

@@ -13,6 +13,7 @@ import (
mid "github.com/chaitin/MonkeyCode/backend/internal/middleware"
"github.com/chaitin/MonkeyCode/backend/pkg/ipdb"
"github.com/chaitin/MonkeyCode/backend/pkg/logger"
"github.com/chaitin/MonkeyCode/backend/pkg/report"
"github.com/chaitin/MonkeyCode/backend/pkg/session"
"github.com/chaitin/MonkeyCode/backend/pkg/store"
)
@@ -24,6 +25,7 @@ var Provider = wire.NewSet(
store.NewRedisCli,
session.NewSession,
ipdb.NewIPDB,
report.NewReport,
)
func NewWeb(cfg *config.Config) *web.Web {

View File

@@ -0,0 +1,111 @@
package report
import (
"encoding/json"
"log/slog"
"net/url"
"os"
"time"
"github.com/google/uuid"
"github.com/chaitin/MonkeyCode/backend/config"
"github.com/chaitin/MonkeyCode/backend/pkg/aes"
"github.com/chaitin/MonkeyCode/backend/pkg/machine"
"github.com/chaitin/MonkeyCode/backend/pkg/request"
"github.com/chaitin/MonkeyCode/backend/pkg/version"
)
type Reporter struct {
client *request.Client
version *version.VersionInfo
logger *slog.Logger
IDFile string
machineID string
cfg *config.Config
}
func NewReport(logger *slog.Logger, cfg *config.Config, version *version.VersionInfo) *Reporter {
raw := "https://baizhi.cloud"
u, _ := url.Parse(raw)
client := request.NewClient(u.Scheme, u.Host, 30*time.Second)
r := &Reporter{
client: client,
logger: logger.With("module", "reporter"),
IDFile: "/app/static/.machine_id",
cfg: cfg,
version: version,
}
if _, err := r.readMachineID(); err != nil {
r.logger.With("error", err).Warn("read machine id file failed")
}
return r
}
func (r *Reporter) readMachineID() (string, error) {
data, err := os.ReadFile(r.IDFile)
if err != nil {
return "", err
}
r.machineID = string(data)
return r.machineID, nil
}
func (r *Reporter) report(data any) error {
b, err := json.Marshal(data)
if err != nil {
return err
}
encrypt, err := aes.Encrypt([]byte(r.cfg.DataReport.Key), string(b))
if err != nil {
return err
}
req := map[string]any{
"index": "monkeycode-installation",
"id": uuid.NewString(),
"data": encrypt,
}
if _, err := request.Post[map[string]any](r.client, "/api/public/data/report", req); err != nil {
r.logger.With("error", err).Warn("report installation failed")
return err
}
return nil
}
func (r *Reporter) ReportInstallation() error {
if r.machineID != "" {
return nil
}
id, err := machine.GenerateMachineID()
if err != nil {
r.logger.With("error", err).Warn("generate machine id failed")
return err
}
r.machineID = id
f, err := os.Create(r.IDFile)
if err != nil {
r.logger.With("error", err).Warn("create machine id file failed")
return err
}
defer f.Close()
_, err = f.WriteString(id)
if err != nil {
r.logger.With("error", err).Warn("write machine id file failed")
return err
}
return r.report(InstallData{
MachineID: id,
Version: r.version.Version(),
Timestamp: time.Now().Format(time.RFC3339),
Type: "installation",
})
}

View File

@@ -0,0 +1,8 @@
package report
type InstallData struct {
MachineID string `json:"machine_id"`
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Type string `json:"type"`
}

View File

@@ -0,0 +1,34 @@
package version
import "fmt"
var (
Version = "v0.0.0"
BuildTime = ""
GitCommit = ""
)
type VersionInfo struct{}
func NewVersionInfo() *VersionInfo {
return &VersionInfo{}
}
func (v *VersionInfo) Print() {
fmt.Printf("🚀 Starting MonkeyCode Server\n")
fmt.Printf("📦 Version: %s\n", Version)
fmt.Printf("⏰ BuildTime: %s\n", BuildTime)
fmt.Printf("📝 GitCommit: %s\n", GitCommit)
}
func (v *VersionInfo) Version() string {
return Version
}
func (v *VersionInfo) BuildTime() string {
return BuildTime
}
func (v *VersionInfo) GitCommit() string {
return GitCommit
}