From c561a0bb0cfb5d61a7ef18d3e39d43a199c43e9f Mon Sep 17 00:00:00 2001 From: lan Date: Fri, 13 Mar 2026 20:40:20 +0800 Subject: [PATCH] feat(grpc): add gRPC server infrastructure and schedule sync service Add gRPC server support with configurable TLS, environment-based settings, and Wire injection. Implement schedule synchronization service with task runner integration for external course data fetching. - Add gRPC configuration with env var overrides (APP_GRPC_ENABLED, APP_GRPC_PORT, etc.) - Create gRPC server infrastructure with runner task management - Implement ScheduleSyncService for course data synchronization - Add sync endpoint to schedule handler for external system integration - Update repository with context-aware batch delete operation - Wire all dependencies including zap logger for structured logging --- configs/config.yaml | 11 + go.mod | 5 +- go.sum | 7 + internal/config/config.go | 13 + internal/config/grpc.go | 17 + internal/grpc/runner/hub.go | 299 ++++ internal/grpc/runner/server.go | 145 ++ internal/grpc/runner/task_manager.go | 235 +++ internal/handler/schedule_handler.go | 49 +- internal/repository/schedule_repo.go | 7 + internal/router/router.go | 1 + internal/service/schedule_sync_service.go | 192 +++ internal/wire/grpc.go | 25 + internal/wire/handler.go | 10 +- internal/wire/infrastructure.go | 13 + internal/wire/provider_set.go | 1 + internal/wire/service.go | 12 + proto/runner/runner.pb.go | 1835 +++++++++++++++++++++ proto/runner/runner.proto | 224 +++ proto/runner/runner_grpc.pb.go | 145 ++ 20 files changed, 3241 insertions(+), 5 deletions(-) create mode 100644 internal/config/grpc.go create mode 100644 internal/grpc/runner/hub.go create mode 100644 internal/grpc/runner/server.go create mode 100644 internal/grpc/runner/task_manager.go create mode 100644 internal/service/schedule_sync_service.go create mode 100644 internal/wire/grpc.go create mode 100644 proto/runner/runner.pb.go create mode 100644 proto/runner/runner.proto create mode 100644 proto/runner/runner_grpc.pb.go diff --git a/configs/config.yaml b/configs/config.yaml index 0b68bc2..e819a51 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -190,3 +190,14 @@ conversation_cache: batch_threshold: 100 # 条数阈值 batch_max_size: 500 # 单次最大批量 buffer_max_size: 10000 # 写缓冲最大条数 + +# gRPC 服务器配置 +# 环境变量: +# APP_GRPC_ENABLED, APP_GRPC_PORT +# APP_GRPC_TLS_ENABLED, APP_GRPC_TLS_CERT_FILE, APP_GRPC_TLS_KEY_FILE +grpc: + enabled: true + port: 50051 + tls_enabled: false + tls_cert_file: "" + tls_key_file: "" diff --git a/go.mod b/go.mod index b648e4f..c8606c7 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,8 @@ require ( go.uber.org/zap v1.26.0 golang.org/x/crypto v0.26.0 golang.org/x/image v0.24.0 + google.golang.org/grpc v1.59.0 + google.golang.org/protobuf v1.31.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/postgres v1.5.4 gorm.io/driver/sqlite v1.5.4 @@ -38,6 +40,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -79,7 +82,7 @@ require ( golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.22.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e47f2cd..75168ec 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1 github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -192,7 +194,12 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/internal/config/config.go b/internal/config/config.go index 074bf00..b1145e5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,6 +25,7 @@ type Config struct { OpenAI OpenAIConfig `mapstructure:"openai"` Email EmailConfig `mapstructure:"email"` ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"` + GRPC GRPCConfig `mapstructure:"grpc"` } // Load 加载配置文件 @@ -127,6 +128,12 @@ func Load(configPath string) (*Config, error) { viper.SetDefault("conversation_cache.batch_threshold", 100) viper.SetDefault("conversation_cache.batch_max_size", 500) viper.SetDefault("conversation_cache.buffer_max_size", 10000) + // GRPC 默认值 + viper.SetDefault("grpc.enabled", true) + viper.SetDefault("grpc.port", 50051) + viper.SetDefault("grpc.tls_enabled", false) + viper.SetDefault("grpc.tls_cert_file", "") + viper.SetDefault("grpc.tls_key_file", "") if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config: %w", err) @@ -202,6 +209,12 @@ func Load(configPath string) (*Config, error) { cfg.Email.UseTLS, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_USE_TLS", fmt.Sprintf("%t", cfg.Email.UseTLS))) cfg.Email.InsecureSkipVerify, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_INSECURE_SKIP_VERIFY", fmt.Sprintf("%t", cfg.Email.InsecureSkipVerify))) cfg.Email.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_TIMEOUT", fmt.Sprintf("%d", cfg.Email.Timeout))) + // GRPC 环境变量覆盖 + cfg.GRPC.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_ENABLED", fmt.Sprintf("%t", cfg.GRPC.Enabled))) + cfg.GRPC.Port, _ = strconv.Atoi(getEnvOrDefault("APP_GRPC_PORT", fmt.Sprintf("%d", cfg.GRPC.Port))) + cfg.GRPC.TLSEnabled, _ = strconv.ParseBool(getEnvOrDefault("APP_GRPC_TLS_ENABLED", fmt.Sprintf("%t", cfg.GRPC.TLSEnabled))) + cfg.GRPC.TLSCertFile = getEnvOrDefault("APP_GRPC_TLS_CERT_FILE", cfg.GRPC.TLSCertFile) + cfg.GRPC.TLSKeyFile = getEnvOrDefault("APP_GRPC_TLS_KEY_FILE", cfg.GRPC.TLSKeyFile) return &cfg, nil } diff --git a/internal/config/grpc.go b/internal/config/grpc.go new file mode 100644 index 0000000..f285bc2 --- /dev/null +++ b/internal/config/grpc.go @@ -0,0 +1,17 @@ +package config + +import "fmt" + +// GRPCConfig gRPC 服务器配置 +type GRPCConfig struct { + Enabled bool `mapstructure:"enabled"` + Port int `mapstructure:"port"` + TLSEnabled bool `mapstructure:"tls_enabled"` + TLSCertFile string `mapstructure:"tls_cert_file"` + TLSKeyFile string `mapstructure:"tls_key_file"` +} + +// Address 返回 gRPC 服务器监听地址 +func (c *GRPCConfig) Address() string { + return fmt.Sprintf(":%d", c.Port) +} diff --git a/internal/grpc/runner/hub.go b/internal/grpc/runner/hub.go new file mode 100644 index 0000000..0ea6196 --- /dev/null +++ b/internal/grpc/runner/hub.go @@ -0,0 +1,299 @@ +package runner + +import ( + "context" + "fmt" + "sync" + "time" + + "carrot_bbs/proto/runner" + + "go.uber.org/zap" +) + +// RunnerConnection 表示一个已连接的 Runner +type RunnerConnection struct { + ID string + Version string + Capabilities map[string]string + Stream runner.RunnerHub_ConnectServer + LastSeen time.Time + sendChan chan *runner.StreamMessage + logger *zap.Logger +} + +// RunnerHub 管理 Runner 连接和任务分发 +type RunnerHub struct { + runner.UnimplementedRunnerHubServer // 嵌入以实现 gRPC 接口 + + mu sync.RWMutex + runners map[string]*RunnerConnection + taskManager *TaskManager + registerChan chan *RunnerConnection + unregisterChan chan string + logger *zap.Logger +} + +// NewRunnerHub 创建新的 RunnerHub +func NewRunnerHub(taskManager *TaskManager, logger *zap.Logger) *RunnerHub { + return &RunnerHub{ + runners: make(map[string]*RunnerConnection), + taskManager: taskManager, + registerChan: make(chan *RunnerConnection, 100), + unregisterChan: make(chan string, 100), + logger: logger, + } +} + +// Connect 实现 RunnerHub 服务的 Connect 方法 +func (h *RunnerHub) Connect(stream runner.RunnerHub_ConnectServer) error { + var conn *RunnerConnection + ctx := stream.Context() + + defer func() { + if conn != nil { + h.unregisterRunner(conn.ID) + } + }() + + // 启动发送协程 + sendDone := make(chan struct{}) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-sendDone: + return nil + default: + msg, err := stream.Recv() + if err != nil { + h.logger.Debug("Stream receive error", zap.Error(err)) + return err + } + + switch m := msg.Message.(type) { + case *runner.StreamMessage_Register: + conn = h.handleRegister(m.Register, stream) + if conn != nil { + // 启动发送协程 + go h.sendLoop(conn, sendDone) + } + case *runner.StreamMessage_Heartbeat: + h.handleHeartbeat(conn, m.Heartbeat) + case *runner.StreamMessage_Result: + h.handleTaskResult(m.Result) + } + } + } +} + +// sendLoop 处理向 Runner 发送消息 +func (h *RunnerHub) sendLoop(conn *RunnerConnection, done chan struct{}) { + for { + select { + case msg, ok := <-conn.sendChan: + if !ok { + // Channel 已关闭 + return + } + if msg == nil { + // 忽略 nil 消息 + continue + } + if err := conn.Stream.Send(msg); err != nil { + h.logger.Error("Failed to send message to runner", + zap.String("runner_id", conn.ID), + zap.Error(err)) + return + } + case <-done: + return + } + } +} + +func (h *RunnerHub) handleRegister(req *runner.RegisterRequest, stream runner.RunnerHub_ConnectServer) *RunnerConnection { + h.logger.Info("Runner registering", + zap.String("runner_id", req.RunnerId), + zap.String("version", req.Version), + zap.Any("capabilities", req.Capabilities)) + + conn := &RunnerConnection{ + ID: req.RunnerId, + Version: req.Version, + Capabilities: req.Capabilities, + Stream: stream, + LastSeen: time.Now(), + sendChan: make(chan *runner.StreamMessage, 100), + logger: h.logger, + } + + h.mu.Lock() + // 如果已存在旧连接,先关闭 + if oldConn, ok := h.runners[req.RunnerId]; ok { + close(oldConn.sendChan) + } + h.runners[req.RunnerId] = conn + h.mu.Unlock() + + // 发送注册响应 + if err := conn.send(&runner.StreamMessage{ + Message: &runner.StreamMessage_RegisterResponse{ + RegisterResponse: &runner.RegisterResponse{ + Success: true, + SessionId: conn.ID, + Message: "Registered successfully", + HeartbeatInterval: 30, + }, + }, + }); err != nil { + h.logger.Error("Failed to send register response", zap.Error(err)) + } + + h.logger.Info("Runner registered successfully", zap.String("runner_id", req.RunnerId)) + return conn +} + +func (h *RunnerHub) handleHeartbeat(conn *RunnerConnection, hb *runner.Heartbeat) { + if conn == nil { + return + } + conn.LastSeen = time.Now() + + if err := conn.send(&runner.StreamMessage{ + Message: &runner.StreamMessage_HeartbeatAck{ + HeartbeatAck: &runner.HeartbeatAck{ + Timestamp: time.Now().Unix(), + }, + }, + }); err != nil { + h.logger.Debug("Failed to send heartbeat ack", + zap.String("runner_id", conn.ID), + zap.Error(err)) + } +} + +func (h *RunnerHub) handleTaskResult(result *runner.TaskResult) { + h.logger.Debug("Received task result", + zap.String("task_id", result.TaskId), + zap.String("status", result.Status.String())) + + if h.taskManager != nil { + h.taskManager.HandleResult(result) + } +} + +// unregisterRunner 注销 Runner +func (h *RunnerHub) unregisterRunner(runnerID string) { + h.mu.Lock() + if conn, ok := h.runners[runnerID]; ok { + close(conn.sendChan) + delete(h.runners, runnerID) + } + h.mu.Unlock() + + h.logger.Info("Runner unregistered", zap.String("runner_id", runnerID)) +} + +// SendTask 向指定 Runner 发送任务 +func (h *RunnerHub) SendTask(runnerID string, task *runner.Task) error { + h.mu.RLock() + conn, ok := h.runners[runnerID] + h.mu.RUnlock() + + if !ok { + return fmt.Errorf("runner not found: %s", runnerID) + } + + return conn.send(&runner.StreamMessage{ + Message: &runner.StreamMessage_Task{ + Task: task, + }, + }) +} + +// GetAvailableRunner 获取支持指定任务类型的可用 Runner +func (h *RunnerHub) GetAvailableRunner(taskType runner.TaskType) (*RunnerConnection, error) { + h.mu.RLock() + defer h.mu.RUnlock() + + taskTypeStr := taskType.String() + for _, conn := range h.runners { + for capKey, capValue := range conn.Capabilities { + // 支持两种格式: + // 1. key = "TASK_TYPE_GET_SCHEDULE", value = "true" + // 2. key = "schedule", value = "TASK_TYPE_GET_SCHEDULE" + if capKey == taskTypeStr && capValue == "true" { + return conn, nil + } + if capValue == taskTypeStr { + return conn, nil + } + } + } + return nil, fmt.Errorf("no available runner for task type: %s", taskType) +} + +// GetAllRunners 获取所有已连接的 Runner 信息 +func (h *RunnerHub) GetAllRunners() []map[string]interface{} { + h.mu.RLock() + defer h.mu.RUnlock() + + result := make([]map[string]interface{}, 0, len(h.runners)) + for id, conn := range h.runners { + result = append(result, map[string]interface{}{ + "id": id, + "version": conn.Version, + "capabilities": conn.Capabilities, + "last_seen": conn.LastSeen, + }) + } + return result +} + +// SetTaskManager 设置任务管理器(用于解决循环依赖) +func (h *RunnerHub) SetTaskManager(tm *TaskManager) { + h.taskManager = tm +} + +// send 向 RunnerConnection 发送消息 +func (c *RunnerConnection) send(msg *runner.StreamMessage) error { + select { + case c.sendChan <- msg: + return nil + default: + return fmt.Errorf("send buffer full for runner: %s", c.ID) + } +} + +// CheckStaleRunners 检查并清理超时的 Runner 连接 +func (h *RunnerHub) CheckStaleRunners(timeout time.Duration) { + h.mu.Lock() + defer h.mu.Unlock() + + now := time.Now() + for id, conn := range h.runners { + if now.Sub(conn.LastSeen) > timeout { + h.logger.Warn("Runner timeout, removing", + zap.String("runner_id", id), + zap.Time("last_seen", conn.LastSeen)) + close(conn.sendChan) + delete(h.runners, id) + } + } +} + +// Shutdown 关闭所有 Runner 连接 +func (h *RunnerHub) Shutdown(ctx context.Context) error { + h.mu.Lock() + defer h.mu.Unlock() + + for id, conn := range h.runners { + h.logger.Info("Closing runner connection", zap.String("runner_id", id)) + close(conn.sendChan) + delete(h.runners, id) + } + + return nil +} diff --git a/internal/grpc/runner/server.go b/internal/grpc/runner/server.go new file mode 100644 index 0000000..9b0f7d3 --- /dev/null +++ b/internal/grpc/runner/server.go @@ -0,0 +1,145 @@ +package runner + +import ( + "context" + "fmt" + "net" + "time" + + "carrot_bbs/internal/config" + "carrot_bbs/proto/runner" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Server gRPC 服务器 +type Server struct { + cfg *config.GRPCConfig + logger *zap.Logger + hub *RunnerHub + grpcSrv *grpc.Server +} + +// NewServer 创建新的 gRPC 服务器 +func NewServer(cfg *config.GRPCConfig, logger *zap.Logger) *Server { + // 创建 TaskManager 和 RunnerHub,解决循环依赖 + taskManager := NewTaskManager(nil, 60*time.Second, logger) + hub := NewRunnerHub(taskManager, logger) + taskManager.SetHub(hub) + + return &Server{ + cfg: cfg, + logger: logger, + hub: hub, + } +} + +// Start 启动 gRPC 服务器 +func (s *Server) Start() error { + if !s.cfg.Enabled { + s.logger.Info("gRPC server is disabled") + return nil + } + + lis, err := net.Listen("tcp", s.cfg.Address()) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", s.cfg.Address(), err) + } + + // 配置 gRPC 服务器选项 + var opts []grpc.ServerOption + + // 配置 TLS + if s.cfg.TLSEnabled { + if s.cfg.TLSCertFile == "" || s.cfg.TLSKeyFile == "" { + return fmt.Errorf("TLS enabled but cert/key files not specified") + } + creds, err := credentials.NewServerTLSFromFile(s.cfg.TLSCertFile, s.cfg.TLSKeyFile) + if err != nil { + return fmt.Errorf("failed to load TLS credentials: %w", err) + } + opts = append(opts, grpc.Creds(creds)) + s.logger.Info("gRPC server TLS enabled") + } else { + s.logger.Warn("gRPC server running without TLS") + } + + s.grpcSrv = grpc.NewServer(opts...) + runner.RegisterRunnerHubServer(s.grpcSrv, s.hub) + + s.logger.Info("gRPC server starting", zap.String("address", s.cfg.Address())) + + go func() { + if err := s.grpcSrv.Serve(lis); err != nil { + s.logger.Error("gRPC server error", zap.Error(err)) + } + }() + + // 启动后台任务清理协程 + go s.backgroundCleanup() + + return nil +} + +// backgroundCleanup 后台清理任务 +func (s *Server) backgroundCleanup() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for range ticker.C { + // 清理超时的 Runner 连接 + s.hub.CheckStaleRunners(90 * time.Second) + } +} + +// Stop 停止 gRPC 服务器 +func (s *Server) Stop() { + if s.grpcSrv != nil { + s.logger.Info("Stopping gRPC server") + s.grpcSrv.GracefulStop() + } +} + +// GetHub 获取 RunnerHub +func (s *Server) GetHub() *RunnerHub { + return s.hub +} + +// GetTaskManager 获取 TaskManager +func (s *Server) GetTaskManager() *TaskManager { + return s.hub.taskManager +} + +// Shutdown 优雅关闭 +func (s *Server) Shutdown(ctx context.Context) error { + if s.grpcSrv == nil { + return nil + } + + done := make(chan struct{}) + go func() { + s.grpcSrv.GracefulStop() + close(done) + }() + + select { + case <-done: + s.logger.Info("gRPC server stopped gracefully") + return nil + case <-ctx.Done(): + s.logger.Warn("gRPC server shutdown timeout, forcing stop") + s.grpcSrv.Stop() + return ctx.Err() + } +} + +// GetClientCredentials 获取客户端凭证配置 +func GetClientCredentials(cfg *config.GRPCConfig) (credentials.TransportCredentials, error) { + if cfg.TLSEnabled { + return credentials.NewClientTLSFromFile(cfg.TLSCertFile, "") + } + return insecure.NewCredentials(), nil +} diff --git a/internal/grpc/runner/task_manager.go b/internal/grpc/runner/task_manager.go new file mode 100644 index 0000000..92a8a09 --- /dev/null +++ b/internal/grpc/runner/task_manager.go @@ -0,0 +1,235 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "time" + + "carrot_bbs/proto/runner" + + "github.com/google/uuid" + "go.uber.org/zap" +) + +// TaskCallback 任务完成回调函数类型 +type TaskCallback func(result *runner.TaskResult) + +// PendingTask 表示一个等待处理的任务 +type PendingTask struct { + Task *runner.Task + Callback TaskCallback + CreatedAt time.Time +} + +// TaskManager 管理任务的生命周期 +type TaskManager struct { + mu sync.RWMutex + pending map[string]*PendingTask + hub *RunnerHub + timeout time.Duration + logger *zap.Logger +} + +// NewTaskManager 创建新的任务管理器 +func NewTaskManager(hub *RunnerHub, timeout time.Duration, logger *zap.Logger) *TaskManager { + return &TaskManager{ + pending: make(map[string]*PendingTask), + hub: hub, + timeout: timeout, + logger: logger, + } +} + +// SubmitTask 提交任务并等待结果 +func (tm *TaskManager) SubmitTask(ctx context.Context, taskType runner.TaskType, payload interface{}, callback TaskCallback) (*runner.Task, error) { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal payload: %w", err) + } + + task := &runner.Task{ + TaskId: uuid.New().String(), + Type: taskType, + CreatedAt: time.Now().Unix(), + TimeoutSeconds: int32(tm.timeout.Seconds()), + Payload: payloadBytes, + } + + tm.mu.Lock() + tm.pending[task.TaskId] = &PendingTask{ + Task: task, + Callback: callback, + CreatedAt: time.Now(), + } + tm.mu.Unlock() + + tm.logger.Debug("Task submitted", + zap.String("task_id", task.TaskId), + zap.String("type", taskType.String())) + + // 获取可用 Runner + conn, err := tm.hub.GetAvailableRunner(taskType) + if err != nil { + tm.cleanupTask(task.TaskId) + return nil, fmt.Errorf("no available runner: %w", err) + } + + // 发送任务 + if err := tm.hub.SendTask(conn.ID, task); err != nil { + tm.cleanupTask(task.TaskId) + return nil, fmt.Errorf("failed to send task: %w", err) + } + + // 启动超时检查 + go tm.waitForTimeout(task.TaskId) + + return task, nil +} + +// SubmitTaskSync 同步提交任务并等待结果 +func (tm *TaskManager) SubmitTaskSync(ctx context.Context, taskType runner.TaskType, payload interface{}) (*runner.TaskResult, error) { + resultChan := make(chan *runner.TaskResult, 1) + + task, err := tm.SubmitTask(ctx, taskType, payload, func(result *runner.TaskResult) { + select { + case resultChan <- result: + default: + } + }) + if err != nil { + return nil, err + } + + select { + case result := <-resultChan: + return result, nil + case <-ctx.Done(): + tm.cleanupTask(task.TaskId) + return nil, ctx.Err() + case <-time.After(tm.timeout): + tm.cleanupTask(task.TaskId) + return nil, fmt.Errorf("task timeout") + } +} + +// HandleResult 处理任务结果 +func (tm *TaskManager) HandleResult(result *runner.TaskResult) { + tm.mu.Lock() + pending, ok := tm.pending[result.TaskId] + if ok { + delete(tm.pending, result.TaskId) + } + tm.mu.Unlock() + + if !ok { + tm.logger.Warn("Received result for unknown task", + zap.String("task_id", result.TaskId)) + return + } + + tm.logger.Debug("Task completed", + zap.String("task_id", result.TaskId), + zap.String("status", result.Status.String())) + + if pending.Callback != nil { + pending.Callback(result) + } +} + +// waitForTimeout 等待任务超时 +func (tm *TaskManager) waitForTimeout(taskID string) { + time.Sleep(tm.timeout) + + tm.mu.Lock() + defer tm.mu.Unlock() + + if pending, ok := tm.pending[taskID]; ok { + tm.logger.Warn("Task timeout", + zap.String("task_id", taskID), + zap.String("type", pending.Task.Type.String())) + + delete(tm.pending, taskID) + + // 调用回调,返回超时结果 + if pending.Callback != nil { + pending.Callback(&runner.TaskResult{ + TaskId: taskID, + Status: runner.TaskStatus_TASK_STATUS_TIMEOUT, + ErrorMessage: "task timeout", + CompletedAt: time.Now().Unix(), + }) + } + } +} + +// cleanupTask 清理任务 +func (tm *TaskManager) cleanupTask(taskID string) { + tm.mu.Lock() + defer tm.mu.Unlock() + delete(tm.pending, taskID) +} + +// CancelTask 取消任务 +func (tm *TaskManager) CancelTask(taskID string) error { + tm.mu.Lock() + defer tm.mu.Unlock() + + if pending, ok := tm.pending[taskID]; ok { + tm.logger.Debug("Task cancelled", zap.String("task_id", taskID)) + + delete(tm.pending, taskID) + + // 调用回调,返回取消结果 + if pending.Callback != nil { + pending.Callback(&runner.TaskResult{ + TaskId: taskID, + Status: runner.TaskStatus_TASK_STATUS_CANCELLED, + ErrorMessage: "task cancelled", + CompletedAt: time.Now().Unix(), + }) + } + return nil + } + + return fmt.Errorf("task not found: %s", taskID) +} + +// GetPendingTaskCount 获取等待处理的任务数量 +func (tm *TaskManager) GetPendingTaskCount() int { + tm.mu.RLock() + defer tm.mu.RUnlock() + return len(tm.pending) +} + +// SetHub 设置 RunnerHub(用于解决循环依赖) +func (tm *TaskManager) SetHub(hub *RunnerHub) { + tm.hub = hub +} + +// CleanupStaleTasks 清理超时的任务 +func (tm *TaskManager) CleanupStaleTasks() { + tm.mu.Lock() + defer tm.mu.Unlock() + + now := time.Now() + for taskID, pending := range tm.pending { + if now.Sub(pending.CreatedAt) > tm.timeout { + tm.logger.Warn("Cleaning up stale task", + zap.String("task_id", taskID), + zap.String("type", pending.Task.Type.String())) + + delete(tm.pending, taskID) + + if pending.Callback != nil { + pending.Callback(&runner.TaskResult{ + TaskId: taskID, + Status: runner.TaskStatus_TASK_STATUS_TIMEOUT, + ErrorMessage: "task timeout", + CompletedAt: now.Unix(), + }) + } + } + } +} diff --git a/internal/handler/schedule_handler.go b/internal/handler/schedule_handler.go index a459482..2061fca 100644 --- a/internal/handler/schedule_handler.go +++ b/internal/handler/schedule_handler.go @@ -10,11 +10,15 @@ import ( ) type ScheduleHandler struct { - scheduleService service.ScheduleService + scheduleService service.ScheduleService + scheduleSyncService service.ScheduleSyncService } -func NewScheduleHandler(scheduleService service.ScheduleService) *ScheduleHandler { - return &ScheduleHandler{scheduleService: scheduleService} +func NewScheduleHandler(scheduleService service.ScheduleService, scheduleSyncService service.ScheduleSyncService) *ScheduleHandler { + return &ScheduleHandler{ + scheduleService: scheduleService, + scheduleSyncService: scheduleSyncService, + } } type createScheduleCourseRequest struct { @@ -138,3 +142,42 @@ func (h *ScheduleHandler) DeleteCourse(c *gin.Context) { } response.SuccessWithMessage(c, "course deleted", nil) } + +// syncScheduleRequest 同步课表请求 +type syncScheduleRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Semester string `json:"semester"` +} + +// SyncSchedule 同步课表接口 +func (h *ScheduleHandler) SyncSchedule(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + response.Unauthorized(c, "") + return + } + + var req syncScheduleRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "invalid request: "+err.Error()) + return + } + + result, err := h.scheduleSyncService.SyncUserSchedule(c.Request.Context(), userID, &service.SyncScheduleRequest{ + Username: req.Username, + Password: req.Password, + Semester: req.Semester, + }) + if err != nil { + response.HandleError(c, err, "failed to sync schedule") + return + } + + if !result.Success { + response.Success(c, result) + return + } + + response.SuccessWithMessage(c, result.Message, result) +} diff --git a/internal/repository/schedule_repo.go b/internal/repository/schedule_repo.go index 1de19f7..97f021a 100644 --- a/internal/repository/schedule_repo.go +++ b/internal/repository/schedule_repo.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "carrot_bbs/internal/model" "gorm.io/gorm" @@ -12,6 +14,7 @@ type ScheduleRepository interface { Create(course *model.ScheduleCourse) error Update(course *model.ScheduleCourse) error DeleteByID(id string) error + DeleteByUserID(ctx context.Context, userID string) error ExistsColorByUser(userID, color, excludeID string) (bool, error) } @@ -52,6 +55,10 @@ func (r *scheduleRepository) DeleteByID(id string) error { return r.db.Delete(&model.ScheduleCourse{}, "id = ?", id).Error } +func (r *scheduleRepository) DeleteByUserID(ctx context.Context, userID string) error { + return r.db.WithContext(ctx).Delete(&model.ScheduleCourse{}, "user_id = ?", userID).Error +} + func (r *scheduleRepository) ExistsColorByUser(userID, color, excludeID string) (bool, error) { var count int64 query := r.db.Model(&model.ScheduleCourse{}). diff --git a/internal/router/router.go b/internal/router/router.go index b7017ad..3afb9d4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -170,6 +170,7 @@ func (r *Router) setupRoutes() { { schedule.GET("/courses", r.scheduleHandler.ListCourses) schedule.POST("/courses", r.scheduleHandler.CreateCourse) + schedule.POST("/sync", r.scheduleHandler.SyncSchedule) // 同步课表 schedule.PUT("/courses/:id", r.scheduleHandler.UpdateCourse) schedule.DELETE("/courses/:id", r.scheduleHandler.DeleteCourse) } diff --git a/internal/service/schedule_sync_service.go b/internal/service/schedule_sync_service.go new file mode 100644 index 0000000..1f9c33e --- /dev/null +++ b/internal/service/schedule_sync_service.go @@ -0,0 +1,192 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + + "carrot_bbs/internal/grpc/runner" + "carrot_bbs/internal/model" + "carrot_bbs/internal/repository" + pb "carrot_bbs/proto/runner" + + "github.com/google/uuid" + "go.uber.org/zap" +) + +// SyncScheduleRequest 同步课表请求 +type SyncScheduleRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Semester string `json:"semester"` // 可选,默认当前学期 +} + +// SyncScheduleResult 同步课表结果 +type SyncScheduleResult struct { + Success bool `json:"success"` + Message string `json:"message"` + SyncedCount int `json:"synced_count"` + ErrorMessage string `json:"error_message,omitempty"` +} + +// ScheduleSyncService 课表同步服务接口 +type ScheduleSyncService interface { + SyncUserSchedule(ctx context.Context, userID string, req *SyncScheduleRequest) (*SyncScheduleResult, error) +} + +type scheduleSyncService struct { + taskManager *runner.TaskManager + scheduleRepo repository.ScheduleRepository + logger *zap.Logger +} + +// NewScheduleSyncService 创建课表同步服务 +func NewScheduleSyncService( + taskManager *runner.TaskManager, + scheduleRepo repository.ScheduleRepository, + logger *zap.Logger, +) ScheduleSyncService { + return &scheduleSyncService{ + taskManager: taskManager, + scheduleRepo: scheduleRepo, + logger: logger, + } +} + +// SyncUserSchedule 同步用户课表 +func (s *scheduleSyncService) SyncUserSchedule(ctx context.Context, userID string, req *SyncScheduleRequest) (*SyncScheduleResult, error) { + // 构建任务载荷 + payload := &pb.GetSchedulePayload{ + Username: req.Username, + Password: req.Password, + Semester: req.Semester, + } + + // 同步提交任务并等待结果 + taskResult, err := s.taskManager.SubmitTaskSync(ctx, pb.TaskType_TASK_TYPE_GET_SCHEDULE, payload) + if err != nil { + s.logger.Error("failed to submit sync task", + zap.String("user_id", userID), + zap.Error(err)) + return nil, fmt.Errorf("failed to submit task: %w", err) + } + + if taskResult.Status != pb.TaskStatus_TASK_STATUS_SUCCESS { + return &SyncScheduleResult{ + Success: false, + Message: "课表获取失败", + ErrorMessage: taskResult.ErrorMessage, + }, nil + } + + // 解析结果数据 + var resultData pb.ScheduleResultData + if err := json.Unmarshal(taskResult.Data, &resultData); err != nil { + s.logger.Error("failed to parse schedule result", + zap.String("user_id", userID), + zap.Error(err)) + return nil, fmt.Errorf("failed to parse result: %w", err) + } + + // 转换并保存课表 + courses := s.convertCourses(&resultData) + if err := s.saveCourses(ctx, userID, courses); err != nil { + s.logger.Error("failed to save courses", + zap.String("user_id", userID), + zap.Error(err)) + return nil, fmt.Errorf("failed to save courses: %w", err) + } + + s.logger.Info("schedule sync completed", + zap.String("user_id", userID), + zap.Int("course_count", len(courses))) + + return &SyncScheduleResult{ + Success: true, + Message: "课表同步成功", + SyncedCount: len(courses), + }, nil +} + +// convertCourses 转换 converter 课表数据为 backend 模型 +func (s *scheduleSyncService) convertCourses(data *pb.ScheduleResultData) []*model.ScheduleCourse { + var courses []*model.ScheduleCourse + + for _, c := range data.Courses { + for _, st := range c.Schedule { + dayOfWeek := 0 + fmt.Sscanf(st.Day, "%d", &dayOfWeek) + + startSection := 1 + endSection := 1 + if len(st.Sections) > 0 { + startSection = int(st.Sections[0]) + endSection = int(st.Sections[len(st.Sections)-1]) + } + + // 转换周次数组 + var weeks []int + for _, w := range st.Weeks { + weeks = append(weeks, int(w)) + } + weeksJSON, _ := json.Marshal(weeks) + + course := &model.ScheduleCourse{ + ID: uuid.New().String(), + Name: c.Name, + Teacher: c.Teacher, + Location: c.Room, + DayOfWeek: dayOfWeek, + StartSection: startSection, + EndSection: endSection, + Weeks: string(weeksJSON), + Color: generateCourseColor(c.Name), + } + courses = append(courses, course) + } + } + + return courses +} + +// saveCourses 保存课表到数据库 +func (s *scheduleSyncService) saveCourses(ctx context.Context, userID string, courses []*model.ScheduleCourse) error { + // 先删除用户旧课表 + if err := s.scheduleRepo.DeleteByUserID(ctx, userID); err != nil { + return fmt.Errorf("failed to delete old courses: %w", err) + } + + // 设置用户 ID 并保存 + for _, c := range courses { + c.UserID = userID + if err := s.scheduleRepo.Create(c); err != nil { + return fmt.Errorf("failed to create course: %w", err) + } + } + + return nil +} + +// generateCourseColor 根据课程名生成颜色 +func generateCourseColor(name string) string { + // 预定义的课程颜色列表 + colors := []string{ + "#3498DB", // 蓝色 + "#2ECC71", // 绿色 + "#E74C3C", // 红色 + "#F39C12", // 橙色 + "#9B59B6", // 紫色 + "#1ABC9C", // 青色 + "#E67E22", // 深橙色 + "#34495E", // 深灰色 + "#16A085", // 深青色 + "#27AE60", // 深绿色 + } + + // 根据课程名哈希选择颜色 + hash := 0 + for _, c := range name { + hash = (hash + int(c)) % len(colors) + } + return colors[hash] +} diff --git a/internal/wire/grpc.go b/internal/wire/grpc.go new file mode 100644 index 0000000..40af63f --- /dev/null +++ b/internal/wire/grpc.go @@ -0,0 +1,25 @@ +package wire + +import ( + "carrot_bbs/internal/config" + "carrot_bbs/internal/grpc/runner" + + "github.com/google/wire" + "go.uber.org/zap" +) + +// GRPCSet gRPC 相关的 Provider Set +var GRPCSet = wire.NewSet( + ProvideGRPCServer, + ProvideTaskManager, +) + +// ProvideGRPCServer 提供 gRPC 服务器 +func ProvideGRPCServer(cfg *config.Config, logger *zap.Logger) *runner.Server { + return runner.NewServer(&cfg.GRPC, logger) +} + +// ProvideTaskManager 提供 TaskManager +func ProvideTaskManager(server *runner.Server) *runner.TaskManager { + return server.GetTaskManager() +} diff --git a/internal/wire/handler.go b/internal/wire/handler.go index 9616411..ce7d6f3 100644 --- a/internal/wire/handler.go +++ b/internal/wire/handler.go @@ -22,7 +22,6 @@ var HandlerSet = wire.NewSet( handler.NewPushHandler, handler.NewStickerHandler, handler.NewVoteHandler, - handler.NewScheduleHandler, // 需要特殊处理的 Handler ProvidePostHandler, @@ -30,6 +29,7 @@ var HandlerSet = wire.NewSet( ProvideSystemMessageHandler, ProvideGroupHandler, ProvideGorseHandler, + ProvideScheduleHandler, ) // ProvidePostHandler 提供帖子处理器 @@ -72,3 +72,11 @@ func ProvideGroupHandler( func ProvideGorseHandler(cfg *config.Config, db *gorm.DB) *handler.GorseHandler { return handler.NewGorseHandler(cfg.Gorse, db) } + +// ProvideScheduleHandler 提供课表处理器 +func ProvideScheduleHandler( + scheduleService service.ScheduleService, + scheduleSyncService service.ScheduleSyncService, +) *handler.ScheduleHandler { + return handler.NewScheduleHandler(scheduleService, scheduleSyncService) +} diff --git a/internal/wire/infrastructure.go b/internal/wire/infrastructure.go index 908b9b4..3fbf8f7 100644 --- a/internal/wire/infrastructure.go +++ b/internal/wire/infrastructure.go @@ -16,6 +16,7 @@ import ( "carrot_bbs/internal/repository" "github.com/google/wire" + "go.uber.org/zap" "gorm.io/gorm" ) @@ -24,6 +25,9 @@ var InfrastructureSet = wire.NewSet( // 配置 ProvideConfig, + // 日志 + ProvideLogger, + // 数据库 ProvideDB, @@ -119,3 +123,12 @@ func ProvideS3Client(cfg *config.Config) (*s3.Client, error) { func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager { return repository.NewTransactionManager(db) } + +// ProvideLogger 提供 Zap Logger +func ProvideLogger() *zap.Logger { + logger, err := zap.NewProduction() + if err != nil { + log.Fatalf("Failed to create logger: %v", err) + } + return logger +} diff --git a/internal/wire/provider_set.go b/internal/wire/provider_set.go index f9d60ec..55ba0d3 100644 --- a/internal/wire/provider_set.go +++ b/internal/wire/provider_set.go @@ -8,4 +8,5 @@ var AllSet = wire.NewSet( RepositorySet, ServiceSet, HandlerSet, + GRPCSet, ) diff --git a/internal/wire/service.go b/internal/wire/service.go index 45f0d8e..101dc81 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -3,6 +3,7 @@ package wire import ( "carrot_bbs/internal/cache" "carrot_bbs/internal/config" + "carrot_bbs/internal/grpc/runner" "carrot_bbs/internal/pkg/email" "carrot_bbs/internal/pkg/gorse" "carrot_bbs/internal/pkg/openai" @@ -12,6 +13,7 @@ import ( "carrot_bbs/internal/service" "github.com/google/wire" + "go.uber.org/zap" "gorm.io/gorm" ) @@ -36,6 +38,7 @@ var ServiceSet = wire.NewSet( ProvideVoteService, ProvideChatService, ProvideScheduleService, + ProvideScheduleSyncService, ProvideGroupService, ProvideUploadService, ) @@ -168,6 +171,15 @@ func ProvideScheduleService( return service.NewScheduleService(scheduleRepo) } +// ProvideScheduleSyncService 提供课表同步服务 +func ProvideScheduleSyncService( + taskManager *runner.TaskManager, + scheduleRepo repository.ScheduleRepository, + logger *zap.Logger, +) service.ScheduleSyncService { + return service.NewScheduleSyncService(taskManager, scheduleRepo, logger) +} + // ProvideGroupService 提供群组服务 func ProvideGroupService( db *gorm.DB, diff --git a/proto/runner/runner.pb.go b/proto/runner/runner.pb.go new file mode 100644 index 0000000..b9e40ce --- /dev/null +++ b/proto/runner/runner.pb.go @@ -0,0 +1,1835 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.1 +// source: proto/runner/runner.proto + +package runner + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// TaskType 任务类型枚举 +type TaskType int32 + +const ( + TaskType_TASK_TYPE_UNKNOWN TaskType = 0 // 未知类型 + TaskType_TASK_TYPE_LOGIN TaskType = 1 // 登录验证 + TaskType_TASK_TYPE_GET_SCHEDULE TaskType = 2 // 获取课表 + TaskType_TASK_TYPE_GET_GRADES TaskType = 3 // 获取成绩 + TaskType_TASK_TYPE_GET_EXAMS TaskType = 4 // 获取考试安排 + TaskType_TASK_TYPE_GET_USER_INFO TaskType = 5 // 获取用户信息 +) + +// Enum value maps for TaskType. +var ( + TaskType_name = map[int32]string{ + 0: "TASK_TYPE_UNKNOWN", + 1: "TASK_TYPE_LOGIN", + 2: "TASK_TYPE_GET_SCHEDULE", + 3: "TASK_TYPE_GET_GRADES", + 4: "TASK_TYPE_GET_EXAMS", + 5: "TASK_TYPE_GET_USER_INFO", + } + TaskType_value = map[string]int32{ + "TASK_TYPE_UNKNOWN": 0, + "TASK_TYPE_LOGIN": 1, + "TASK_TYPE_GET_SCHEDULE": 2, + "TASK_TYPE_GET_GRADES": 3, + "TASK_TYPE_GET_EXAMS": 4, + "TASK_TYPE_GET_USER_INFO": 5, + } +) + +func (x TaskType) Enum() *TaskType { + p := new(TaskType) + *p = x + return p +} + +func (x TaskType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TaskType) Descriptor() protoreflect.EnumDescriptor { + return file_proto_runner_runner_proto_enumTypes[0].Descriptor() +} + +func (TaskType) Type() protoreflect.EnumType { + return &file_proto_runner_runner_proto_enumTypes[0] +} + +func (x TaskType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TaskType.Descriptor instead. +func (TaskType) EnumDescriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{0} +} + +// TaskStatus 任务状态枚举 +type TaskStatus int32 + +const ( + TaskStatus_TASK_STATUS_UNKNOWN TaskStatus = 0 // 未知状态 + TaskStatus_TASK_STATUS_SUCCESS TaskStatus = 1 // 成功 + TaskStatus_TASK_STATUS_FAILED TaskStatus = 2 // 失败 + TaskStatus_TASK_STATUS_TIMEOUT TaskStatus = 3 // 超时 + TaskStatus_TASK_STATUS_CANCELLED TaskStatus = 4 // 已取消 +) + +// Enum value maps for TaskStatus. +var ( + TaskStatus_name = map[int32]string{ + 0: "TASK_STATUS_UNKNOWN", + 1: "TASK_STATUS_SUCCESS", + 2: "TASK_STATUS_FAILED", + 3: "TASK_STATUS_TIMEOUT", + 4: "TASK_STATUS_CANCELLED", + } + TaskStatus_value = map[string]int32{ + "TASK_STATUS_UNKNOWN": 0, + "TASK_STATUS_SUCCESS": 1, + "TASK_STATUS_FAILED": 2, + "TASK_STATUS_TIMEOUT": 3, + "TASK_STATUS_CANCELLED": 4, + } +) + +func (x TaskStatus) Enum() *TaskStatus { + p := new(TaskStatus) + *p = x + return p +} + +func (x TaskStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TaskStatus) Descriptor() protoreflect.EnumDescriptor { + return file_proto_runner_runner_proto_enumTypes[1].Descriptor() +} + +func (TaskStatus) Type() protoreflect.EnumType { + return &file_proto_runner_runner_proto_enumTypes[1] +} + +func (x TaskStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TaskStatus.Descriptor instead. +func (TaskStatus) EnumDescriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{1} +} + +// StreamMessage 双向流消息包装 +type StreamMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Message: + // + // *StreamMessage_Register + // *StreamMessage_RegisterResponse + // *StreamMessage_Heartbeat + // *StreamMessage_HeartbeatAck + // *StreamMessage_Task + // *StreamMessage_Result + // *StreamMessage_TaskCancel + Message isStreamMessage_Message `protobuf_oneof:"message"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamMessage) Reset() { + *x = StreamMessage{} + mi := &file_proto_runner_runner_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamMessage) ProtoMessage() {} + +func (x *StreamMessage) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamMessage.ProtoReflect.Descriptor instead. +func (*StreamMessage) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{0} +} + +func (x *StreamMessage) GetMessage() isStreamMessage_Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *StreamMessage) GetRegister() *RegisterRequest { + if x != nil { + if x, ok := x.Message.(*StreamMessage_Register); ok { + return x.Register + } + } + return nil +} + +func (x *StreamMessage) GetRegisterResponse() *RegisterResponse { + if x != nil { + if x, ok := x.Message.(*StreamMessage_RegisterResponse); ok { + return x.RegisterResponse + } + } + return nil +} + +func (x *StreamMessage) GetHeartbeat() *Heartbeat { + if x != nil { + if x, ok := x.Message.(*StreamMessage_Heartbeat); ok { + return x.Heartbeat + } + } + return nil +} + +func (x *StreamMessage) GetHeartbeatAck() *HeartbeatAck { + if x != nil { + if x, ok := x.Message.(*StreamMessage_HeartbeatAck); ok { + return x.HeartbeatAck + } + } + return nil +} + +func (x *StreamMessage) GetTask() *Task { + if x != nil { + if x, ok := x.Message.(*StreamMessage_Task); ok { + return x.Task + } + } + return nil +} + +func (x *StreamMessage) GetResult() *TaskResult { + if x != nil { + if x, ok := x.Message.(*StreamMessage_Result); ok { + return x.Result + } + } + return nil +} + +func (x *StreamMessage) GetTaskCancel() *TaskCancel { + if x != nil { + if x, ok := x.Message.(*StreamMessage_TaskCancel); ok { + return x.TaskCancel + } + } + return nil +} + +type isStreamMessage_Message interface { + isStreamMessage_Message() +} + +type StreamMessage_Register struct { + Register *RegisterRequest `protobuf:"bytes,1,opt,name=register,proto3,oneof"` // Runner 注册请求 +} + +type StreamMessage_RegisterResponse struct { + RegisterResponse *RegisterResponse `protobuf:"bytes,2,opt,name=register_response,json=registerResponse,proto3,oneof"` // 注册响应 +} + +type StreamMessage_Heartbeat struct { + Heartbeat *Heartbeat `protobuf:"bytes,3,opt,name=heartbeat,proto3,oneof"` // 心跳请求 +} + +type StreamMessage_HeartbeatAck struct { + HeartbeatAck *HeartbeatAck `protobuf:"bytes,4,opt,name=heartbeat_ack,json=heartbeatAck,proto3,oneof"` // 心跳响应 +} + +type StreamMessage_Task struct { + Task *Task `protobuf:"bytes,5,opt,name=task,proto3,oneof"` // 下发的任务 +} + +type StreamMessage_Result struct { + Result *TaskResult `protobuf:"bytes,6,opt,name=result,proto3,oneof"` // 任务结果 +} + +type StreamMessage_TaskCancel struct { + TaskCancel *TaskCancel `protobuf:"bytes,7,opt,name=task_cancel,json=taskCancel,proto3,oneof"` // 取消任务 +} + +func (*StreamMessage_Register) isStreamMessage_Message() {} + +func (*StreamMessage_RegisterResponse) isStreamMessage_Message() {} + +func (*StreamMessage_Heartbeat) isStreamMessage_Message() {} + +func (*StreamMessage_HeartbeatAck) isStreamMessage_Message() {} + +func (*StreamMessage_Task) isStreamMessage_Message() {} + +func (*StreamMessage_Result) isStreamMessage_Message() {} + +func (*StreamMessage_TaskCancel) isStreamMessage_Message() {} + +// RegisterRequest Runner 注册请求 +type RegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunnerId string `protobuf:"bytes,1,opt,name=runner_id,json=runnerId,proto3" json:"runner_id,omitempty"` // Runner 唯一标识 (UUID) + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` // Runner 版本号 + Capabilities map[string]string `protobuf:"bytes,3,rep,name=capabilities,proto3" json:"capabilities,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // 能力标签 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterRequest) Reset() { + *x = RegisterRequest{} + mi := &file_proto_runner_runner_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterRequest) ProtoMessage() {} + +func (x *RegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead. +func (*RegisterRequest) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{1} +} + +func (x *RegisterRequest) GetRunnerId() string { + if x != nil { + return x.RunnerId + } + return "" +} + +func (x *RegisterRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *RegisterRequest) GetCapabilities() map[string]string { + if x != nil { + return x.Capabilities + } + return nil +} + +// RegisterResponse 注册响应 +type RegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` // 是否成功 + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // 会话ID,用于标识本次连接 + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` // 消息说明 + HeartbeatInterval int32 `protobuf:"varint,4,opt,name=heartbeat_interval,json=heartbeatInterval,proto3" json:"heartbeat_interval,omitempty"` // 心跳间隔 (秒) + TaskTimeout int32 `protobuf:"varint,5,opt,name=task_timeout,json=taskTimeout,proto3" json:"task_timeout,omitempty"` // 默认任务超时时间 (秒) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterResponse) Reset() { + *x = RegisterResponse{} + mi := &file_proto_runner_runner_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterResponse) ProtoMessage() {} + +func (x *RegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead. +func (*RegisterResponse) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{2} +} + +func (x *RegisterResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *RegisterResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *RegisterResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *RegisterResponse) GetHeartbeatInterval() int32 { + if x != nil { + return x.HeartbeatInterval + } + return 0 +} + +func (x *RegisterResponse) GetTaskTimeout() int32 { + if x != nil { + return x.TaskTimeout + } + return 0 +} + +// Heartbeat 心跳请求 +type Heartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // 客户端时间戳 (毫秒) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Heartbeat) Reset() { + *x = Heartbeat{} + mi := &file_proto_runner_runner_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Heartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Heartbeat) ProtoMessage() {} + +func (x *Heartbeat) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Heartbeat.ProtoReflect.Descriptor instead. +func (*Heartbeat) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{3} +} + +func (x *Heartbeat) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// HeartbeatAck 心跳响应 +type HeartbeatAck struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // 服务端时间戳 (毫秒) + Latency int64 `protobuf:"varint,2,opt,name=latency,proto3" json:"latency,omitempty"` // 往返延迟 (毫秒) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeartbeatAck) Reset() { + *x = HeartbeatAck{} + mi := &file_proto_runner_runner_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeartbeatAck) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatAck) ProtoMessage() {} + +func (x *HeartbeatAck) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatAck.ProtoReflect.Descriptor instead. +func (*HeartbeatAck) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{4} +} + +func (x *HeartbeatAck) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *HeartbeatAck) GetLatency() int64 { + if x != nil { + return x.Latency + } + return 0 +} + +// Task 任务定义 +type Task struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` // 任务唯一ID (UUID) + Type TaskType `protobuf:"varint,2,opt,name=type,proto3,enum=runner.TaskType" json:"type,omitempty"` // 任务类型 + CreatedAt int64 `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // 创建时间戳 (毫秒) + TimeoutSeconds int32 `protobuf:"varint,4,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` // 超时时间 (秒) + Payload []byte `protobuf:"bytes,5,opt,name=payload,proto3" json:"payload,omitempty"` // 任务载荷 (JSON 编码) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Task) Reset() { + *x = Task{} + mi := &file_proto_runner_runner_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Task) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Task) ProtoMessage() {} + +func (x *Task) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Task.ProtoReflect.Descriptor instead. +func (*Task) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{5} +} + +func (x *Task) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *Task) GetType() TaskType { + if x != nil { + return x.Type + } + return TaskType_TASK_TYPE_UNKNOWN +} + +func (x *Task) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Task) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +func (x *Task) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +// TaskCancel 取消任务 +type TaskCancel struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` // 要取消的任务ID + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` // 取消原因 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskCancel) Reset() { + *x = TaskCancel{} + mi := &file_proto_runner_runner_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskCancel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskCancel) ProtoMessage() {} + +func (x *TaskCancel) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskCancel.ProtoReflect.Descriptor instead. +func (*TaskCancel) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{6} +} + +func (x *TaskCancel) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskCancel) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// TaskResult 任务结果 +type TaskResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` // 任务ID + Status TaskStatus `protobuf:"varint,2,opt,name=status,proto3,enum=runner.TaskStatus" json:"status,omitempty"` // 执行状态 + CompletedAt int64 `protobuf:"varint,3,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` // 完成时间戳 (毫秒) + Data []byte `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` // 结果数据 (JSON 编码) + ErrorCode string `protobuf:"bytes,5,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // 错误码 + ErrorMessage string `protobuf:"bytes,6,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` // 错误消息 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskResult) Reset() { + *x = TaskResult{} + mi := &file_proto_runner_runner_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskResult) ProtoMessage() {} + +func (x *TaskResult) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskResult.ProtoReflect.Descriptor instead. +func (*TaskResult) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{7} +} + +func (x *TaskResult) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskResult) GetStatus() TaskStatus { + if x != nil { + return x.Status + } + return TaskStatus_TASK_STATUS_UNKNOWN +} + +func (x *TaskResult) GetCompletedAt() int64 { + if x != nil { + return x.CompletedAt + } + return 0 +} + +func (x *TaskResult) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *TaskResult) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *TaskResult) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +// LoginPayload 登录任务载荷 +type LoginPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号 + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码 + Twfid string `protobuf:"bytes,3,opt,name=twfid,proto3" json:"twfid,omitempty"` // 初始 Cookie (可选) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginPayload) Reset() { + *x = LoginPayload{} + mi := &file_proto_runner_runner_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginPayload) ProtoMessage() {} + +func (x *LoginPayload) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginPayload.ProtoReflect.Descriptor instead. +func (*LoginPayload) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{8} +} + +func (x *LoginPayload) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *LoginPayload) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *LoginPayload) GetTwfid() string { + if x != nil { + return x.Twfid + } + return "" +} + +// GetSchedulePayload 获取课表任务载荷 +type GetSchedulePayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号 + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码 + Semester string `protobuf:"bytes,3,opt,name=semester,proto3" json:"semester,omitempty"` // 学期,如 "2025-20262" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSchedulePayload) Reset() { + *x = GetSchedulePayload{} + mi := &file_proto_runner_runner_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSchedulePayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSchedulePayload) ProtoMessage() {} + +func (x *GetSchedulePayload) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSchedulePayload.ProtoReflect.Descriptor instead. +func (*GetSchedulePayload) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{9} +} + +func (x *GetSchedulePayload) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GetSchedulePayload) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *GetSchedulePayload) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +// GetGradesPayload 获取成绩任务载荷 +type GetGradesPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号 + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码 + Semester string `protobuf:"bytes,3,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 (可选,为空则获取全部) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGradesPayload) Reset() { + *x = GetGradesPayload{} + mi := &file_proto_runner_runner_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGradesPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGradesPayload) ProtoMessage() {} + +func (x *GetGradesPayload) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGradesPayload.ProtoReflect.Descriptor instead. +func (*GetGradesPayload) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{10} +} + +func (x *GetGradesPayload) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GetGradesPayload) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *GetGradesPayload) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +// GetExamsPayload 获取考试安排任务载荷 +type GetExamsPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号 + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码 + Semester string `protobuf:"bytes,3,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetExamsPayload) Reset() { + *x = GetExamsPayload{} + mi := &file_proto_runner_runner_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetExamsPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExamsPayload) ProtoMessage() {} + +func (x *GetExamsPayload) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExamsPayload.ProtoReflect.Descriptor instead. +func (*GetExamsPayload) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{11} +} + +func (x *GetExamsPayload) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GetExamsPayload) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *GetExamsPayload) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +// ScheduleResultData 课表结果数据 +type ScheduleResultData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Semester string `protobuf:"bytes,1,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 + StudentId string `protobuf:"bytes,2,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号 + StudentName string `protobuf:"bytes,3,opt,name=student_name,json=studentName,proto3" json:"student_name,omitempty"` // 学生姓名 + Courses []*Course `protobuf:"bytes,4,rep,name=courses,proto3" json:"courses,omitempty"` // 课程列表 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScheduleResultData) Reset() { + *x = ScheduleResultData{} + mi := &file_proto_runner_runner_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScheduleResultData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleResultData) ProtoMessage() {} + +func (x *ScheduleResultData) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleResultData.ProtoReflect.Descriptor instead. +func (*ScheduleResultData) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{12} +} + +func (x *ScheduleResultData) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +func (x *ScheduleResultData) GetStudentId() string { + if x != nil { + return x.StudentId + } + return "" +} + +func (x *ScheduleResultData) GetStudentName() string { + if x != nil { + return x.StudentName + } + return "" +} + +func (x *ScheduleResultData) GetCourses() []*Course { + if x != nil { + return x.Courses + } + return nil +} + +// Course 课程信息 +type Course struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` // 课程代码 + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 课程名称 + Teacher string `protobuf:"bytes,3,opt,name=teacher,proto3" json:"teacher,omitempty"` // 授课教师 + Room string `protobuf:"bytes,4,opt,name=room,proto3" json:"room,omitempty"` // 教室 + Credit float32 `protobuf:"fixed32,5,opt,name=credit,proto3" json:"credit,omitempty"` // 学分 + Schedule []*ScheduleTime `protobuf:"bytes,6,rep,name=schedule,proto3" json:"schedule,omitempty"` // 上课时间安排 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Course) Reset() { + *x = Course{} + mi := &file_proto_runner_runner_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Course) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Course) ProtoMessage() {} + +func (x *Course) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Course.ProtoReflect.Descriptor instead. +func (*Course) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{13} +} + +func (x *Course) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *Course) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Course) GetTeacher() string { + if x != nil { + return x.Teacher + } + return "" +} + +func (x *Course) GetRoom() string { + if x != nil { + return x.Room + } + return "" +} + +func (x *Course) GetCredit() float32 { + if x != nil { + return x.Credit + } + return 0 +} + +func (x *Course) GetSchedule() []*ScheduleTime { + if x != nil { + return x.Schedule + } + return nil +} + +// ScheduleTime 上课时间安排 +type ScheduleTime struct { + state protoimpl.MessageState `protogen:"open.v1"` + Day string `protobuf:"bytes,1,opt,name=day,proto3" json:"day,omitempty"` // 星期几,"0"-"6" 表示周日到周六 + Sections []int32 `protobuf:"varint,2,rep,packed,name=sections,proto3" json:"sections,omitempty"` // 节次,如 [1, 2, 3] + Weeks []int32 `protobuf:"varint,3,rep,packed,name=weeks,proto3" json:"weeks,omitempty"` // 周次,如 [1, 2, 3, 4, 5] + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScheduleTime) Reset() { + *x = ScheduleTime{} + mi := &file_proto_runner_runner_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ScheduleTime) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleTime) ProtoMessage() {} + +func (x *ScheduleTime) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScheduleTime.ProtoReflect.Descriptor instead. +func (*ScheduleTime) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{14} +} + +func (x *ScheduleTime) GetDay() string { + if x != nil { + return x.Day + } + return "" +} + +func (x *ScheduleTime) GetSections() []int32 { + if x != nil { + return x.Sections + } + return nil +} + +func (x *ScheduleTime) GetWeeks() []int32 { + if x != nil { + return x.Weeks + } + return nil +} + +// GradesResultData 成绩结果数据 +type GradesResultData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Semester string `protobuf:"bytes,1,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 + StudentId string `protobuf:"bytes,2,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号 + Grades []*Grade `protobuf:"bytes,3,rep,name=grades,proto3" json:"grades,omitempty"` // 成绩列表 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GradesResultData) Reset() { + *x = GradesResultData{} + mi := &file_proto_runner_runner_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GradesResultData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GradesResultData) ProtoMessage() {} + +func (x *GradesResultData) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GradesResultData.ProtoReflect.Descriptor instead. +func (*GradesResultData) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{15} +} + +func (x *GradesResultData) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +func (x *GradesResultData) GetStudentId() string { + if x != nil { + return x.StudentId + } + return "" +} + +func (x *GradesResultData) GetGrades() []*Grade { + if x != nil { + return x.Grades + } + return nil +} + +// Grade 成绩信息 +type Grade struct { + state protoimpl.MessageState `protogen:"open.v1"` + CourseCode string `protobuf:"bytes,1,opt,name=course_code,json=courseCode,proto3" json:"course_code,omitempty"` // 课程代码 + CourseName string `protobuf:"bytes,2,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称 + Credit float32 `protobuf:"fixed32,3,opt,name=credit,proto3" json:"credit,omitempty"` // 学分 + Grade string `protobuf:"bytes,4,opt,name=grade,proto3" json:"grade,omitempty"` // 成绩 (可能是等级或分数) + GradePoint float32 `protobuf:"fixed32,5,opt,name=grade_point,json=gradePoint,proto3" json:"grade_point,omitempty"` // 绩点 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Grade) Reset() { + *x = Grade{} + mi := &file_proto_runner_runner_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Grade) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Grade) ProtoMessage() {} + +func (x *Grade) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Grade.ProtoReflect.Descriptor instead. +func (*Grade) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{16} +} + +func (x *Grade) GetCourseCode() string { + if x != nil { + return x.CourseCode + } + return "" +} + +func (x *Grade) GetCourseName() string { + if x != nil { + return x.CourseName + } + return "" +} + +func (x *Grade) GetCredit() float32 { + if x != nil { + return x.Credit + } + return 0 +} + +func (x *Grade) GetGrade() string { + if x != nil { + return x.Grade + } + return "" +} + +func (x *Grade) GetGradePoint() float32 { + if x != nil { + return x.GradePoint + } + return 0 +} + +// ExamsResultData 考试安排结果数据 +type ExamsResultData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Semester string `protobuf:"bytes,1,opt,name=semester,proto3" json:"semester,omitempty"` // 学期 + StudentId string `protobuf:"bytes,2,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号 + Exams []*Exam `protobuf:"bytes,3,rep,name=exams,proto3" json:"exams,omitempty"` // 考试列表 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExamsResultData) Reset() { + *x = ExamsResultData{} + mi := &file_proto_runner_runner_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExamsResultData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExamsResultData) ProtoMessage() {} + +func (x *ExamsResultData) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExamsResultData.ProtoReflect.Descriptor instead. +func (*ExamsResultData) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{17} +} + +func (x *ExamsResultData) GetSemester() string { + if x != nil { + return x.Semester + } + return "" +} + +func (x *ExamsResultData) GetStudentId() string { + if x != nil { + return x.StudentId + } + return "" +} + +func (x *ExamsResultData) GetExams() []*Exam { + if x != nil { + return x.Exams + } + return nil +} + +// Exam 考试信息 +type Exam struct { + state protoimpl.MessageState `protogen:"open.v1"` + CourseCode string `protobuf:"bytes,1,opt,name=course_code,json=courseCode,proto3" json:"course_code,omitempty"` // 课程代码 + CourseName string `protobuf:"bytes,2,opt,name=course_name,json=courseName,proto3" json:"course_name,omitempty"` // 课程名称 + ExamType string `protobuf:"bytes,3,opt,name=exam_type,json=examType,proto3" json:"exam_type,omitempty"` // 考试类型 (期末/期中/补考) + Date string `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` // 考试日期 (YYYY-MM-DD) + Time string `protobuf:"bytes,5,opt,name=time,proto3" json:"time,omitempty"` // 考试时间 (HH:MM-HH:MM) + Room string `protobuf:"bytes,6,opt,name=room,proto3" json:"room,omitempty"` // 考场 + Seat string `protobuf:"bytes,7,opt,name=seat,proto3" json:"seat,omitempty"` // 座位号 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Exam) Reset() { + *x = Exam{} + mi := &file_proto_runner_runner_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Exam) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Exam) ProtoMessage() {} + +func (x *Exam) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Exam.ProtoReflect.Descriptor instead. +func (*Exam) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{18} +} + +func (x *Exam) GetCourseCode() string { + if x != nil { + return x.CourseCode + } + return "" +} + +func (x *Exam) GetCourseName() string { + if x != nil { + return x.CourseName + } + return "" +} + +func (x *Exam) GetExamType() string { + if x != nil { + return x.ExamType + } + return "" +} + +func (x *Exam) GetDate() string { + if x != nil { + return x.Date + } + return "" +} + +func (x *Exam) GetTime() string { + if x != nil { + return x.Time + } + return "" +} + +func (x *Exam) GetRoom() string { + if x != nil { + return x.Room + } + return "" +} + +func (x *Exam) GetSeat() string { + if x != nil { + return x.Seat + } + return "" +} + +// UserInfoResultData 用户信息结果数据 +type UserInfoResultData struct { + state protoimpl.MessageState `protogen:"open.v1"` + StudentId string `protobuf:"bytes,1,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号 + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 姓名 + Gender string `protobuf:"bytes,3,opt,name=gender,proto3" json:"gender,omitempty"` // 性别 + College string `protobuf:"bytes,4,opt,name=college,proto3" json:"college,omitempty"` // 学院 + Major string `protobuf:"bytes,5,opt,name=major,proto3" json:"major,omitempty"` // 专业 + ClassName string `protobuf:"bytes,6,opt,name=class_name,json=className,proto3" json:"class_name,omitempty"` // 班级 + Grade string `protobuf:"bytes,7,opt,name=grade,proto3" json:"grade,omitempty"` // 年级 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserInfoResultData) Reset() { + *x = UserInfoResultData{} + mi := &file_proto_runner_runner_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserInfoResultData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserInfoResultData) ProtoMessage() {} + +func (x *UserInfoResultData) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserInfoResultData.ProtoReflect.Descriptor instead. +func (*UserInfoResultData) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{19} +} + +func (x *UserInfoResultData) GetStudentId() string { + if x != nil { + return x.StudentId + } + return "" +} + +func (x *UserInfoResultData) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserInfoResultData) GetGender() string { + if x != nil { + return x.Gender + } + return "" +} + +func (x *UserInfoResultData) GetCollege() string { + if x != nil { + return x.College + } + return "" +} + +func (x *UserInfoResultData) GetMajor() string { + if x != nil { + return x.Major + } + return "" +} + +func (x *UserInfoResultData) GetClassName() string { + if x != nil { + return x.ClassName + } + return "" +} + +func (x *UserInfoResultData) GetGrade() string { + if x != nil { + return x.Grade + } + return "" +} + +var File_proto_runner_runner_proto protoreflect.FileDescriptor + +const file_proto_runner_runner_proto_rawDesc = "" + + "\n" + + "\x19proto/runner/runner.proto\x12\x06runner\"\x93\x03\n" + + "\rStreamMessage\x125\n" + + "\bregister\x18\x01 \x01(\v2\x17.runner.RegisterRequestH\x00R\bregister\x12G\n" + + "\x11register_response\x18\x02 \x01(\v2\x18.runner.RegisterResponseH\x00R\x10registerResponse\x121\n" + + "\theartbeat\x18\x03 \x01(\v2\x11.runner.HeartbeatH\x00R\theartbeat\x12;\n" + + "\rheartbeat_ack\x18\x04 \x01(\v2\x14.runner.HeartbeatAckH\x00R\fheartbeatAck\x12\"\n" + + "\x04task\x18\x05 \x01(\v2\f.runner.TaskH\x00R\x04task\x12,\n" + + "\x06result\x18\x06 \x01(\v2\x12.runner.TaskResultH\x00R\x06result\x125\n" + + "\vtask_cancel\x18\a \x01(\v2\x12.runner.TaskCancelH\x00R\n" + + "taskCancelB\t\n" + + "\amessage\"\xd8\x01\n" + + "\x0fRegisterRequest\x12\x1b\n" + + "\trunner_id\x18\x01 \x01(\tR\brunnerId\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12M\n" + + "\fcapabilities\x18\x03 \x03(\v2).runner.RegisterRequest.CapabilitiesEntryR\fcapabilities\x1a?\n" + + "\x11CapabilitiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb7\x01\n" + + "\x10RegisterResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\tR\tsessionId\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x12-\n" + + "\x12heartbeat_interval\x18\x04 \x01(\x05R\x11heartbeatInterval\x12!\n" + + "\ftask_timeout\x18\x05 \x01(\x05R\vtaskTimeout\")\n" + + "\tHeartbeat\x12\x1c\n" + + "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"F\n" + + "\fHeartbeatAck\x12\x1c\n" + + "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\x12\x18\n" + + "\alatency\x18\x02 \x01(\x03R\alatency\"\xa7\x01\n" + + "\x04Task\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12$\n" + + "\x04type\x18\x02 \x01(\x0e2\x10.runner.TaskTypeR\x04type\x12\x1d\n" + + "\n" + + "created_at\x18\x03 \x01(\x03R\tcreatedAt\x12'\n" + + "\x0ftimeout_seconds\x18\x04 \x01(\x05R\x0etimeoutSeconds\x12\x18\n" + + "\apayload\x18\x05 \x01(\fR\apayload\"=\n" + + "\n" + + "TaskCancel\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"\xcc\x01\n" + + "\n" + + "TaskResult\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12*\n" + + "\x06status\x18\x02 \x01(\x0e2\x12.runner.TaskStatusR\x06status\x12!\n" + + "\fcompleted_at\x18\x03 \x01(\x03R\vcompletedAt\x12\x12\n" + + "\x04data\x18\x04 \x01(\fR\x04data\x12\x1d\n" + + "\n" + + "error_code\x18\x05 \x01(\tR\terrorCode\x12#\n" + + "\rerror_message\x18\x06 \x01(\tR\ferrorMessage\"\\\n" + + "\fLoginPayload\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x14\n" + + "\x05twfid\x18\x03 \x01(\tR\x05twfid\"h\n" + + "\x12GetSchedulePayload\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1a\n" + + "\bsemester\x18\x03 \x01(\tR\bsemester\"f\n" + + "\x10GetGradesPayload\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1a\n" + + "\bsemester\x18\x03 \x01(\tR\bsemester\"e\n" + + "\x0fGetExamsPayload\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1a\n" + + "\bsemester\x18\x03 \x01(\tR\bsemester\"\x9c\x01\n" + + "\x12ScheduleResultData\x12\x1a\n" + + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + + "\n" + + "student_id\x18\x02 \x01(\tR\tstudentId\x12!\n" + + "\fstudent_name\x18\x03 \x01(\tR\vstudentName\x12(\n" + + "\acourses\x18\x04 \x03(\v2\x0e.runner.CourseR\acourses\"\xa8\x01\n" + + "\x06Course\x12\x12\n" + + "\x04code\x18\x01 \x01(\tR\x04code\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\ateacher\x18\x03 \x01(\tR\ateacher\x12\x12\n" + + "\x04room\x18\x04 \x01(\tR\x04room\x12\x16\n" + + "\x06credit\x18\x05 \x01(\x02R\x06credit\x120\n" + + "\bschedule\x18\x06 \x03(\v2\x14.runner.ScheduleTimeR\bschedule\"R\n" + + "\fScheduleTime\x12\x10\n" + + "\x03day\x18\x01 \x01(\tR\x03day\x12\x1a\n" + + "\bsections\x18\x02 \x03(\x05R\bsections\x12\x14\n" + + "\x05weeks\x18\x03 \x03(\x05R\x05weeks\"t\n" + + "\x10GradesResultData\x12\x1a\n" + + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + + "\n" + + "student_id\x18\x02 \x01(\tR\tstudentId\x12%\n" + + "\x06grades\x18\x03 \x03(\v2\r.runner.GradeR\x06grades\"\x98\x01\n" + + "\x05Grade\x12\x1f\n" + + "\vcourse_code\x18\x01 \x01(\tR\n" + + "courseCode\x12\x1f\n" + + "\vcourse_name\x18\x02 \x01(\tR\n" + + "courseName\x12\x16\n" + + "\x06credit\x18\x03 \x01(\x02R\x06credit\x12\x14\n" + + "\x05grade\x18\x04 \x01(\tR\x05grade\x12\x1f\n" + + "\vgrade_point\x18\x05 \x01(\x02R\n" + + "gradePoint\"p\n" + + "\x0fExamsResultData\x12\x1a\n" + + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + + "\n" + + "student_id\x18\x02 \x01(\tR\tstudentId\x12\"\n" + + "\x05exams\x18\x03 \x03(\v2\f.runner.ExamR\x05exams\"\xb5\x01\n" + + "\x04Exam\x12\x1f\n" + + "\vcourse_code\x18\x01 \x01(\tR\n" + + "courseCode\x12\x1f\n" + + "\vcourse_name\x18\x02 \x01(\tR\n" + + "courseName\x12\x1b\n" + + "\texam_type\x18\x03 \x01(\tR\bexamType\x12\x12\n" + + "\x04date\x18\x04 \x01(\tR\x04date\x12\x12\n" + + "\x04time\x18\x05 \x01(\tR\x04time\x12\x12\n" + + "\x04room\x18\x06 \x01(\tR\x04room\x12\x12\n" + + "\x04seat\x18\a \x01(\tR\x04seat\"\xc4\x01\n" + + "\x12UserInfoResultData\x12\x1d\n" + + "\n" + + "student_id\x18\x01 \x01(\tR\tstudentId\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + + "\x06gender\x18\x03 \x01(\tR\x06gender\x12\x18\n" + + "\acollege\x18\x04 \x01(\tR\acollege\x12\x14\n" + + "\x05major\x18\x05 \x01(\tR\x05major\x12\x1d\n" + + "\n" + + "class_name\x18\x06 \x01(\tR\tclassName\x12\x14\n" + + "\x05grade\x18\a \x01(\tR\x05grade*\xa2\x01\n" + + "\bTaskType\x12\x15\n" + + "\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x13\n" + + "\x0fTASK_TYPE_LOGIN\x10\x01\x12\x1a\n" + + "\x16TASK_TYPE_GET_SCHEDULE\x10\x02\x12\x18\n" + + "\x14TASK_TYPE_GET_GRADES\x10\x03\x12\x17\n" + + "\x13TASK_TYPE_GET_EXAMS\x10\x04\x12\x1b\n" + + "\x17TASK_TYPE_GET_USER_INFO\x10\x05*\x8a\x01\n" + + "\n" + + "TaskStatus\x12\x17\n" + + "\x13TASK_STATUS_UNKNOWN\x10\x00\x12\x17\n" + + "\x13TASK_STATUS_SUCCESS\x10\x01\x12\x16\n" + + "\x12TASK_STATUS_FAILED\x10\x02\x12\x17\n" + + "\x13TASK_STATUS_TIMEOUT\x10\x03\x12\x19\n" + + "\x15TASK_STATUS_CANCELLED\x10\x042H\n" + + "\tRunnerHub\x12;\n" + + "\aConnect\x12\x15.runner.StreamMessage\x1a\x15.runner.StreamMessage(\x010\x01B Z\x1ecarrot_bbs/proto/runner;runnerb\x06proto3" + +var ( + file_proto_runner_runner_proto_rawDescOnce sync.Once + file_proto_runner_runner_proto_rawDescData []byte +) + +func file_proto_runner_runner_proto_rawDescGZIP() []byte { + file_proto_runner_runner_proto_rawDescOnce.Do(func() { + file_proto_runner_runner_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc))) + }) + return file_proto_runner_runner_proto_rawDescData +} + +var file_proto_runner_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_proto_runner_runner_proto_goTypes = []any{ + (TaskType)(0), // 0: runner.TaskType + (TaskStatus)(0), // 1: runner.TaskStatus + (*StreamMessage)(nil), // 2: runner.StreamMessage + (*RegisterRequest)(nil), // 3: runner.RegisterRequest + (*RegisterResponse)(nil), // 4: runner.RegisterResponse + (*Heartbeat)(nil), // 5: runner.Heartbeat + (*HeartbeatAck)(nil), // 6: runner.HeartbeatAck + (*Task)(nil), // 7: runner.Task + (*TaskCancel)(nil), // 8: runner.TaskCancel + (*TaskResult)(nil), // 9: runner.TaskResult + (*LoginPayload)(nil), // 10: runner.LoginPayload + (*GetSchedulePayload)(nil), // 11: runner.GetSchedulePayload + (*GetGradesPayload)(nil), // 12: runner.GetGradesPayload + (*GetExamsPayload)(nil), // 13: runner.GetExamsPayload + (*ScheduleResultData)(nil), // 14: runner.ScheduleResultData + (*Course)(nil), // 15: runner.Course + (*ScheduleTime)(nil), // 16: runner.ScheduleTime + (*GradesResultData)(nil), // 17: runner.GradesResultData + (*Grade)(nil), // 18: runner.Grade + (*ExamsResultData)(nil), // 19: runner.ExamsResultData + (*Exam)(nil), // 20: runner.Exam + (*UserInfoResultData)(nil), // 21: runner.UserInfoResultData + nil, // 22: runner.RegisterRequest.CapabilitiesEntry +} +var file_proto_runner_runner_proto_depIdxs = []int32{ + 3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest + 4, // 1: runner.StreamMessage.register_response:type_name -> runner.RegisterResponse + 5, // 2: runner.StreamMessage.heartbeat:type_name -> runner.Heartbeat + 6, // 3: runner.StreamMessage.heartbeat_ack:type_name -> runner.HeartbeatAck + 7, // 4: runner.StreamMessage.task:type_name -> runner.Task + 9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult + 8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel + 22, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry + 0, // 8: runner.Task.type:type_name -> runner.TaskType + 1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus + 15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course + 16, // 11: runner.Course.schedule:type_name -> runner.ScheduleTime + 18, // 12: runner.GradesResultData.grades:type_name -> runner.Grade + 20, // 13: runner.ExamsResultData.exams:type_name -> runner.Exam + 2, // 14: runner.RunnerHub.Connect:input_type -> runner.StreamMessage + 2, // 15: runner.RunnerHub.Connect:output_type -> runner.StreamMessage + 15, // [15:16] is the sub-list for method output_type + 14, // [14:15] is the sub-list for method input_type + 14, // [14:14] is the sub-list for extension type_name + 14, // [14:14] is the sub-list for extension extendee + 0, // [0:14] is the sub-list for field type_name +} + +func init() { file_proto_runner_runner_proto_init() } +func file_proto_runner_runner_proto_init() { + if File_proto_runner_runner_proto != nil { + return + } + file_proto_runner_runner_proto_msgTypes[0].OneofWrappers = []any{ + (*StreamMessage_Register)(nil), + (*StreamMessage_RegisterResponse)(nil), + (*StreamMessage_Heartbeat)(nil), + (*StreamMessage_HeartbeatAck)(nil), + (*StreamMessage_Task)(nil), + (*StreamMessage_Result)(nil), + (*StreamMessage_TaskCancel)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)), + NumEnums: 2, + NumMessages: 21, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_runner_runner_proto_goTypes, + DependencyIndexes: file_proto_runner_runner_proto_depIdxs, + EnumInfos: file_proto_runner_runner_proto_enumTypes, + MessageInfos: file_proto_runner_runner_proto_msgTypes, + }.Build() + File_proto_runner_runner_proto = out.File + file_proto_runner_runner_proto_goTypes = nil + file_proto_runner_runner_proto_depIdxs = nil +} diff --git a/proto/runner/runner.proto b/proto/runner/runner.proto new file mode 100644 index 0000000..fed7a23 --- /dev/null +++ b/proto/runner/runner.proto @@ -0,0 +1,224 @@ +syntax = "proto3"; + +package runner; + +option go_package = "carrot_bbs/proto/runner;runner"; + +// ============================================================ +// 服务定义 +// ============================================================ + +// RunnerHub 服务 - 由 Backend 服务器实现 +// Runner 作为客户端主动连接,建立双向流通信 +service RunnerHub { + // Connect 建立双向流连接 + // Runner 通过此连接注册、接收任务、返回结果 + rpc Connect(stream StreamMessage) returns (stream StreamMessage); +} + +// ============================================================ +// 流消息包装 - 所有消息通过 oneof 包装 +// ============================================================ + +// StreamMessage 双向流消息包装 +message StreamMessage { + oneof message { + RegisterRequest register = 1; // Runner 注册请求 + RegisterResponse register_response = 2; // 注册响应 + Heartbeat heartbeat = 3; // 心跳请求 + HeartbeatAck heartbeat_ack = 4; // 心跳响应 + Task task = 5; // 下发的任务 + TaskResult result = 6; // 任务结果 + TaskCancel task_cancel = 7; // 取消任务 + } +} + +// ============================================================ +// 注册相关消息 +// ============================================================ + +// RegisterRequest Runner 注册请求 +message RegisterRequest { + string runner_id = 1; // Runner 唯一标识 (UUID) + string version = 2; // Runner 版本号 + map capabilities = 3; // 能力标签 + // 例如: {"login": "true", "schedule": "true", "grades": "true"} +} + +// RegisterResponse 注册响应 +message RegisterResponse { + bool success = 1; // 是否成功 + string session_id = 2; // 会话ID,用于标识本次连接 + string message = 3; // 消息说明 + int32 heartbeat_interval = 4; // 心跳间隔 (秒) + int32 task_timeout = 5; // 默认任务超时时间 (秒) +} + +// ============================================================ +// 心跳消息 +// ============================================================ + +// Heartbeat 心跳请求 +message Heartbeat { + int64 timestamp = 1; // 客户端时间戳 (毫秒) +} + +// HeartbeatAck 心跳响应 +message HeartbeatAck { + int64 timestamp = 1; // 服务端时间戳 (毫秒) + int64 latency = 2; // 往返延迟 (毫秒) +} + +// ============================================================ +// 任务定义 +// ============================================================ + +// TaskType 任务类型枚举 +enum TaskType { + TASK_TYPE_UNKNOWN = 0; // 未知类型 + TASK_TYPE_LOGIN = 1; // 登录验证 + TASK_TYPE_GET_SCHEDULE = 2; // 获取课表 + TASK_TYPE_GET_GRADES = 3; // 获取成绩 + TASK_TYPE_GET_EXAMS = 4; // 获取考试安排 + TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息 +} + +// TaskStatus 任务状态枚举 +enum TaskStatus { + TASK_STATUS_UNKNOWN = 0; // 未知状态 + TASK_STATUS_SUCCESS = 1; // 成功 + TASK_STATUS_FAILED = 2; // 失败 + TASK_STATUS_TIMEOUT = 3; // 超时 + TASK_STATUS_CANCELLED = 4; // 已取消 +} + +// Task 任务定义 +message Task { + string task_id = 1; // 任务唯一ID (UUID) + TaskType type = 2; // 任务类型 + int64 created_at = 3; // 创建时间戳 (毫秒) + int32 timeout_seconds = 4; // 超时时间 (秒) + bytes payload = 5; // 任务载荷 (JSON 编码) +} + +// TaskCancel 取消任务 +message TaskCancel { + string task_id = 1; // 要取消的任务ID + string reason = 2; // 取消原因 +} + +// TaskResult 任务结果 +message TaskResult { + string task_id = 1; // 任务ID + TaskStatus status = 2; // 执行状态 + int64 completed_at = 3; // 完成时间戳 (毫秒) + bytes data = 4; // 结果数据 (JSON 编码) + string error_code = 5; // 错误码 + string error_message = 6; // 错误消息 +} + +// ============================================================ +// 任务载荷定义 - 用于 JSON 序列化到 bytes +// ============================================================ + +// LoginPayload 登录任务载荷 +message LoginPayload { + string username = 1; // 学号 + string password = 2; // 密码 + string twfid = 3; // 初始 Cookie (可选) +} + +// GetSchedulePayload 获取课表任务载荷 +message GetSchedulePayload { + string username = 1; // 学号 + string password = 2; // 密码 + string semester = 3; // 学期,如 "2025-20262" +} + +// GetGradesPayload 获取成绩任务载荷 +message GetGradesPayload { + string username = 1; // 学号 + string password = 2; // 密码 + string semester = 3; // 学期 (可选,为空则获取全部) +} + +// GetExamsPayload 获取考试安排任务载荷 +message GetExamsPayload { + string username = 1; // 学号 + string password = 2; // 密码 + string semester = 3; // 学期 +} + +// ============================================================ +// 结果数据定义 - 用于 JSON 序列化到 bytes +// ============================================================ + +// ScheduleResultData 课表结果数据 +message ScheduleResultData { + string semester = 1; // 学期 + string student_id = 2; // 学号 + string student_name = 3; // 学生姓名 + repeated Course courses = 4; // 课程列表 +} + +// Course 课程信息 +message Course { + string code = 1; // 课程代码 + string name = 2; // 课程名称 + string teacher = 3; // 授课教师 + string room = 4; // 教室 + float credit = 5; // 学分 + repeated ScheduleTime schedule = 6; // 上课时间安排 +} + +// ScheduleTime 上课时间安排 +message ScheduleTime { + string day = 1; // 星期几,"0"-"6" 表示周日到周六 + repeated int32 sections = 2; // 节次,如 [1, 2, 3] + repeated int32 weeks = 3; // 周次,如 [1, 2, 3, 4, 5] +} + +// GradesResultData 成绩结果数据 +message GradesResultData { + string semester = 1; // 学期 + string student_id = 2; // 学号 + repeated Grade grades = 3; // 成绩列表 +} + +// Grade 成绩信息 +message Grade { + string course_code = 1; // 课程代码 + string course_name = 2; // 课程名称 + float credit = 3; // 学分 + string grade = 4; // 成绩 (可能是等级或分数) + float grade_point = 5; // 绩点 +} + +// ExamsResultData 考试安排结果数据 +message ExamsResultData { + string semester = 1; // 学期 + string student_id = 2; // 学号 + repeated Exam exams = 3; // 考试列表 +} + +// Exam 考试信息 +message Exam { + string course_code = 1; // 课程代码 + string course_name = 2; // 课程名称 + string exam_type = 3; // 考试类型 (期末/期中/补考) + string date = 4; // 考试日期 (YYYY-MM-DD) + string time = 5; // 考试时间 (HH:MM-HH:MM) + string room = 6; // 考场 + string seat = 7; // 座位号 +} + +// UserInfoResultData 用户信息结果数据 +message UserInfoResultData { + string student_id = 1; // 学号 + string name = 2; // 姓名 + string gender = 3; // 性别 + string college = 4; // 学院 + string major = 5; // 专业 + string class_name = 6; // 班级 + string grade = 7; // 年级 +} diff --git a/proto/runner/runner_grpc.pb.go b/proto/runner/runner_grpc.pb.go new file mode 100644 index 0000000..c9a00a2 --- /dev/null +++ b/proto/runner/runner_grpc.pb.go @@ -0,0 +1,145 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v6.33.1 +// source: proto/runner/runner.proto + +package runner + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + RunnerHub_Connect_FullMethodName = "/runner.RunnerHub/Connect" +) + +// RunnerHubClient is the client API for RunnerHub service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RunnerHubClient interface { + // Connect 建立双向流连接 + // Runner 通过此连接注册、接收任务、返回结果 + Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error) +} + +type runnerHubClient struct { + cc grpc.ClientConnInterface +} + +func NewRunnerHubClient(cc grpc.ClientConnInterface) RunnerHubClient { + return &runnerHubClient{cc} +} + +func (c *runnerHubClient) Connect(ctx context.Context, opts ...grpc.CallOption) (RunnerHub_ConnectClient, error) { + stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &runnerHubConnectClient{stream} + return x, nil +} + +type RunnerHub_ConnectClient interface { + Send(*StreamMessage) error + Recv() (*StreamMessage, error) + grpc.ClientStream +} + +type runnerHubConnectClient struct { + grpc.ClientStream +} + +func (x *runnerHubConnectClient) Send(m *StreamMessage) error { + return x.ClientStream.SendMsg(m) +} + +func (x *runnerHubConnectClient) Recv() (*StreamMessage, error) { + m := new(StreamMessage) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// RunnerHubServer is the server API for RunnerHub service. +// All implementations must embed UnimplementedRunnerHubServer +// for forward compatibility +type RunnerHubServer interface { + // Connect 建立双向流连接 + // Runner 通过此连接注册、接收任务、返回结果 + Connect(RunnerHub_ConnectServer) error + mustEmbedUnimplementedRunnerHubServer() +} + +// UnimplementedRunnerHubServer must be embedded to have forward compatible implementations. +type UnimplementedRunnerHubServer struct { +} + +func (UnimplementedRunnerHubServer) Connect(RunnerHub_ConnectServer) error { + return status.Errorf(codes.Unimplemented, "method Connect not implemented") +} +func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {} + +// UnsafeRunnerHubServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RunnerHubServer will +// result in compilation errors. +type UnsafeRunnerHubServer interface { + mustEmbedUnimplementedRunnerHubServer() +} + +func RegisterRunnerHubServer(s grpc.ServiceRegistrar, srv RunnerHubServer) { + s.RegisterService(&RunnerHub_ServiceDesc, srv) +} + +func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(RunnerHubServer).Connect(&runnerHubConnectServer{stream}) +} + +type RunnerHub_ConnectServer interface { + Send(*StreamMessage) error + Recv() (*StreamMessage, error) + grpc.ServerStream +} + +type runnerHubConnectServer struct { + grpc.ServerStream +} + +func (x *runnerHubConnectServer) Send(m *StreamMessage) error { + return x.ServerStream.SendMsg(m) +} + +func (x *runnerHubConnectServer) Recv() (*StreamMessage, error) { + m := new(StreamMessage) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// RunnerHub_ServiceDesc is the grpc.ServiceDesc for RunnerHub service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RunnerHub_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "runner.RunnerHub", + HandlerType: (*RunnerHubServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Connect", + Handler: _RunnerHub_Connect_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "proto/runner/runner.proto", +}