commit 17cdb7ad0d1ee18cee68abe289f42250b2a624bf Author: lan Date: Fri Mar 13 20:43:44 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f32ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +converter +converter.exe diff --git a/auth/login.go b/auth/login.go new file mode 100644 index 0000000..15ab49c --- /dev/null +++ b/auth/login.go @@ -0,0 +1,68 @@ +package auth + +import ( + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + + "jwts/client" + "jwts/config" +) + +// 登录 +func Login(captchaCode string) (bool, error) { + return LoginWithClient(client.Client, captchaCode, config.Username, config.Password) +} + +// LoginWithClient 使用指定的 HTTP 客户端和凭据登录 +func LoginWithClient(httpClient *http.Client, captchaCode, username, password string) (bool, error) { + fmt.Println("[4] 执行登录...") + + data := fmt.Sprintf("usercode=%s&password=%s&code=%s", username, password, captchaCode) + req, err := http.NewRequest("POST", config.BaseURL+"/login", strings.NewReader(data)) + if err != nil { + return false, err + } + + req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded") + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + if client.CheckLoginSuccess(string(body)) { + fmt.Println(" ✓ 登录成功!") + + // 提取用户名 + nameMatch := regexp.MustCompile(`(\w+)同学`).FindStringSubmatch(string(body)) + if len(nameMatch) > 1 { + fmt.Printf(" 欢迎, %s!\n", nameMatch[1]) + } + + // 保存登录后的页面 + if err := os.WriteFile("login_result.html", body, 0644); err != nil { + fmt.Printf(" 警告: 无法保存登录页面: %v\n", err) + } else { + fmt.Println(" 登录后页面已保存到: login_result.html") + } + + return true, nil + } + + fmt.Println(" ✗ 登录失败") + if client.IsCaptchaError(string(body)) { + fmt.Println(" 错误: 验证码错误") + } + + return false, nil +} diff --git a/captcha/recognize.go b/captcha/recognize.go new file mode 100644 index 0000000..0546397 --- /dev/null +++ b/captcha/recognize.go @@ -0,0 +1,72 @@ +package captcha + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + + "jwts/client" + "jwts/config" + + "github.com/PuerkitoBio/goquery" +) + +// 保存验证码图片 +func SaveCaptchaImage(httpClient *http.Client) ([]byte, error) { + fmt.Println("[2] 获取验证码...") + + req, err := http.NewRequest("GET", config.BaseURL+"/captchaImage", nil) + if err != nil { + return nil, err + } + req.Header = client.GetHeaders() + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if err := os.WriteFile("captcha.png", body, 0644); err != nil { + fmt.Printf(" 警告: 无法保存验证码图片: %v\n", err) + } else { + fmt.Println(" 验证码已保存到: captcha.png") + } + + return body, nil +} + +// 识别验证码 - 使用固定验证码用于测试 +func Recognize(imagePath string) (string, error) { + fmt.Println("[3] 使用固定验证码...") + // 固定验证码用于测试 + return "....", nil +} + +// 获取并识别验证码 +func GetAndRecognize(client *http.Client) (string, error) { + _, err := SaveCaptchaImage(client) + if err != nil { + return "", err + } + + captchaCode, err := Recognize("captcha.png") + if err != nil { + return "", err + } + + fmt.Printf(" 识别结果: %s\n", captchaCode) + return captchaCode, nil +} + +// 用于解析HTML(登录页面) +func ParseHTML(html string) (*goquery.Document, error) { + return goquery.NewDocumentFromReader(strings.NewReader(html)) +} diff --git a/client/http.go b/client/http.go new file mode 100644 index 0000000..5a15ebc --- /dev/null +++ b/client/http.go @@ -0,0 +1,89 @@ +package client + +import ( + "fmt" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + + "jwts/config" +) + +// 全局HTTP客户端 +var Client *http.Client + +// 初始化HTTP客户端 +func InitClient() { + jar, err := cookiejar.New(nil) + if err != nil { + log.Fatalf("Failed to create cookie jar: %v", err) + } + Client = &http.Client{ + Jar: jar, + } +} + +// 设置请求头 +func GetHeaders() http.Header { + return http.Header{ + "User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}, + "Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}, + "Accept-Language": []string{"zh-CN,zh;q=0.9,en;q=0.8"}, + } +} + +// 获取带Content-Type的请求头 +func GetHeadersWithContentType(contentType string) http.Header { + headers := GetHeaders() + headers["Content-Type"] = []string{contentType} + return headers +} + +// 步骤1: 获取初始Cookie +func GetInitialCookies() error { + fmt.Println("[1] 访问登录页面获取初始Cookie...") + + req, err := http.NewRequest("GET", config.BaseURL+"/loginNOCAS", nil) + if err != nil { + return err + } + req.Header = GetHeaders() + + resp, err := Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + fmt.Printf(" 登录页面状态码: %d\n", resp.StatusCode) + PrintCookies() + + return nil +} + +// 打印Cookies +func PrintCookies() { + fmt.Printf(" 当前Cookies: ") + for _, cookie := range Client.Jar.Cookies(parseURL(config.BaseURL)) { + fmt.Printf("%s=%s; ", cookie.Name, cookie.Value) + } + fmt.Println() +} + +// 解析URL +func parseURL(rawURL string) *url.URL { + u, _ := url.Parse(rawURL) + return u +} + +// 检查登录是否成功 +func CheckLoginSuccess(body string) bool { + return strings.Contains(body, "本科教学管理服务系统") +} + +// 检查是否验证码错误 +func IsCaptchaError(body string) bool { + return strings.Contains(body, "验证码") +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..aa53ace --- /dev/null +++ b/config/config.go @@ -0,0 +1,23 @@ +package config + +import ( + "os" +) + +// 配置变量 +var ( + BaseURL = getEnv("JWTS_BASE_URL", "http://jwts.hitwh.edu.cn") + Username = getEnv("JWTS_USERNAME", "") + Password = getEnv("JWTS_PASSWORD", "") + Semester = getEnv("JWTS_SEMESTER", "2025-20262") + TWFID = getEnv("JWTS_TWFID", "") +) + +// getEnv 从环境变量获取配置,提供默认值 +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..38b3021 --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module jwts + +go 1.25.7 + +require ( + github.com/PuerkitoBio/goquery v1.11.0 + github.com/gin-gonic/gin v1.12.0 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.10 +) + +require ( + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ac4e6fe --- /dev/null +++ b/go.sum @@ -0,0 +1,180 @@ +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grkb.html b/grkb.html new file mode 100644 index 0000000..3945694 --- /dev/null +++ b/grkb.html @@ -0,0 +1,303 @@ + + + + + + + + + + + + +学生个人课表查询 + + + + + + + + + + + + + + + + + + + + + +
+
+
当前位置:学生选课 >> 个人课表查询
+
+
+
+ + + + + + + + + + + + + + + + + +
学年学期: + + + + + + + +
+
+
+
课表确认时间:~
+
+ +
+ +
+
2026春季学期(2023210517)胡浩烨课表(2026-03-13 16:27:14)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 星期一星期二星期三星期四星期五星期六星期日
上午1-2点集拓扑学
李鹏程[2-8]周,[9]周
N楼-335
微分几何
李鹏程[10]周N楼-335
点集拓扑学
李鹏程[2-8]周,[9]周
N楼-335
泛函分析基础
蒋心蕊[11]周,[12]周,[9,10]周
N楼-335
微分几何
李鹏程[2-8]周N楼-335
微分几何
李鹏程[2-8]周,[9]周
N楼-335
泛函分析基础
蒋心蕊[11]周,[2-8]周,[9,10]周
N楼-335
  
上午3-4泛函分析基础
蒋心蕊[11]周,[12]周,[2-8]周,[9,10]周
N楼-335
 泛函分析基础
蒋心蕊[2-8]周N楼-335
体育(6)(定向运动)
张晓秋[9-16]周
 微分几何
李鹏程[2-8]周N楼-335,[9]周N楼-331
  
下午5-6    数理统计
黄春茂[10,11]周研究院-中405
  
下午7-8数理统计
黄春茂[1-9]周,[10]周
N楼-116
数理统计
黄春茂[10,11]周,[12]周
研究院-中405
数理统计
黄春茂[1-9]周N楼-116
数理统计
黄春茂[10,11]周,[12]周
研究院-中405
形势与政策(3)
田金花[5-8]周N楼-116
数理统计
黄春茂[1-9]周N楼-116
  
晚上9-10       
晚上11-12       
其它课程: 生产实习◇马强◇1◇' +
+
+
+
+ + +
+
+ 注意:“个人课表查询”是按照您的选课进行显示,请仔细与“班级推荐课表查询”页面进行对比,如发现个人课表中缺少本班级的推荐课程(特别是必修课),则说明您可能存在漏选情况,请认真核实,如有需要请及时进行补选。 +
+
+
+
+ + + + + + + + + +
+ + + + + + + + diff --git a/grpc/client.go b/grpc/client.go new file mode 100644 index 0000000..e571871 --- /dev/null +++ b/grpc/client.go @@ -0,0 +1,205 @@ +package grpc + +import ( + "context" + "fmt" + "io" + "log" + "sync" + "time" + + pb "jwts/proto/runner" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// Client gRPC 客户端 +type Client struct { + serverAddr string + runnerID string + version string + conn *grpc.ClientConn + stream pb.RunnerHub_ConnectClient + mu sync.Mutex + handler *TaskHandler + + // 配置 + heartbeatInterval time.Duration +} + +// NewClient 创建新的 gRPC 客户端 +func NewClient(serverAddr, runnerID, version string) *Client { + return &Client{ + serverAddr: serverAddr, + runnerID: runnerID, + version: version, + heartbeatInterval: 30 * time.Second, + } +} + +// SetHandler 设置任务处理器 +func (c *Client) SetHandler(handler *TaskHandler) { + c.handler = handler +} + +// Connect 连接到 gRPC 服务器 +func (c *Client) Connect(ctx context.Context) error { + log.Printf("正在连接到 gRPC 服务器: %s", c.serverAddr) + + conn, err := grpc.Dial(c.serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return fmt.Errorf("连接失败: %w", err) + } + c.conn = conn + + client := pb.NewRunnerHubClient(conn) + stream, err := client.Connect(ctx) + if err != nil { + return fmt.Errorf("创建流失败: %w", err) + } + c.stream = stream + + // 发送注册请求 + if err := c.register(); err != nil { + return err + } + + log.Println("gRPC 连接成功") + return nil +} + +// register 发送注册请求 +func (c *Client) register() error { + msg := &pb.StreamMessage{ + Message: &pb.StreamMessage_Register{ + Register: &pb.RegisterRequest{ + RunnerId: c.runnerID, + Version: c.version, + Capabilities: map[string]string{ + "TASK_TYPE_GET_SCHEDULE": "true", + "TASK_TYPE_LOGIN": "true", + }, + }, + }, + } + return c.stream.Send(msg) +} + +// Run 运行主循环,接收消息 +func (c *Client) Run(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + msg, err := c.stream.Recv() + if err == io.EOF { + log.Println("服务器关闭了连接") + return nil + } + if err != nil { + return fmt.Errorf("接收消息失败: %w", err) + } + + c.handleMessage(msg) + } + } +} + +// handleMessage 处理接收到的消息 +func (c *Client) handleMessage(msg *pb.StreamMessage) { + switch m := msg.Message.(type) { + case *pb.StreamMessage_RegisterResponse: + log.Printf("注册成功: %s, 会话ID: %s, 心跳间隔: %d秒", + m.RegisterResponse.Message, + m.RegisterResponse.SessionId, + m.RegisterResponse.HeartbeatInterval) + if m.RegisterResponse.HeartbeatInterval > 0 { + c.heartbeatInterval = time.Duration(m.RegisterResponse.HeartbeatInterval) * time.Second + } + case *pb.StreamMessage_HeartbeatAck: + // 心跳确认,可以用于计算延迟 + log.Printf("心跳确认,延迟: %dms", m.HeartbeatAck.Latency) + case *pb.StreamMessage_Task: + log.Printf("收到任务: %s, 类型: %v", m.Task.TaskId, m.Task.Type) + go c.handleTask(m.Task) + case *pb.StreamMessage_TaskCancel: + log.Printf("收到取消任务请求: %s, 原因: %s", m.TaskCancel.TaskId, m.TaskCancel.Reason) + if c.handler != nil { + c.handler.CancelTask(m.TaskCancel.TaskId) + } + default: + log.Printf("收到未知消息类型: %T", msg.Message) + } +} + +// handleTask 处理任务 +func (c *Client) handleTask(task *pb.Task) { + if c.handler == nil { + log.Println("错误: 未设置任务处理器") + return + } + + result := c.handler.Execute(task) + + c.mu.Lock() + err := c.stream.Send(&pb.StreamMessage{ + Message: &pb.StreamMessage_Result{ + Result: result, + }, + }) + c.mu.Unlock() + + if err != nil { + log.Printf("发送任务结果失败: %v", err) + } else { + log.Printf("任务结果已发送: %s, 状态: %v", result.TaskId, result.Status) + } +} + +// sendHeartbeat 发送心跳 +func (c *Client) sendHeartbeat() error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.stream == nil { + return fmt.Errorf("流未连接") + } + + return c.stream.Send(&pb.StreamMessage{ + Message: &pb.StreamMessage_Heartbeat{ + Heartbeat: &pb.Heartbeat{ + Timestamp: time.Now().UnixMilli(), + }, + }, + }) +} + +// HeartbeatLoop 心跳循环 +func (c *Client) HeartbeatLoop(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := c.sendHeartbeat(); err != nil { + log.Printf("发送心跳失败: %v", err) + } + } + } +} + +// Close 关闭连接 +func (c *Client) Close() error { + if c.stream != nil { + c.stream.CloseSend() + } + if c.conn != nil { + return c.conn.Close() + } + return nil +} diff --git a/grpc/handler.go b/grpc/handler.go new file mode 100644 index 0000000..bc5cd56 --- /dev/null +++ b/grpc/handler.go @@ -0,0 +1,305 @@ +package grpc + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + "sync" + "time" + + pb "jwts/proto/runner" + + "jwts/auth" + "jwts/captcha" + "jwts/config" + "jwts/models" + "jwts/parser" +) + +// TaskHandler 任务处理器 +type TaskHandler struct { + cancelledTasks map[string]bool + mu sync.RWMutex +} + +// NewTaskHandler 创建新的任务处理器 +func NewTaskHandler() *TaskHandler { + return &TaskHandler{ + cancelledTasks: make(map[string]bool), + } +} + +// Execute 执行任务 +func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult { + result := &pb.TaskResult{ + TaskId: task.TaskId, + CompletedAt: time.Now().UnixMilli(), + } + + // 检查任务是否已取消 + if h.IsCancelled(task.TaskId) { + result.Status = pb.TaskStatus_TASK_STATUS_CANCELLED + result.ErrorMessage = "任务已取消" + return result + } + + switch task.Type { + case pb.TaskType_TASK_TYPE_GET_SCHEDULE: + data, err := h.getSchedule(task.Payload) + if err != nil { + result.Status = pb.TaskStatus_TASK_STATUS_FAILED + result.ErrorMessage = err.Error() + } else { + result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS + result.Data = data + } + case pb.TaskType_TASK_TYPE_LOGIN: + data, err := h.login(task.Payload) + if err != nil { + result.Status = pb.TaskStatus_TASK_STATUS_FAILED + result.ErrorMessage = err.Error() + } else { + result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS + result.Data = data + } + default: + result.Status = pb.TaskStatus_TASK_STATUS_FAILED + result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type) + } + + return result +} + +// CancelTask 取消任务 +func (h *TaskHandler) CancelTask(taskID string) { + h.mu.Lock() + defer h.mu.Unlock() + h.cancelledTasks[taskID] = true + log.Printf("任务已标记为取消: %s", taskID) +} + +// IsCancelled 检查任务是否已取消 +func (h *TaskHandler) IsCancelled(taskID string) bool { + h.mu.RLock() + defer h.mu.RUnlock() + return h.cancelledTasks[taskID] +} + +// getSchedule 获取课表 +func (h *TaskHandler) getSchedule(payload []byte) ([]byte, error) { + var req pb.GetSchedulePayload + if err := json.Unmarshal(payload, &req); err != nil { + return nil, fmt.Errorf("解析任务载荷失败: %w", err) + } + + semester := strings.TrimSpace(req.Semester) + if semester == "" { + semester = config.Semester + } + + log.Printf("获取课表: 用户=%s, 学期=%s", req.Username, semester) + + // 创建新的 HTTP 客户端 + jar, err := cookiejar.New(nil) + if err != nil { + return nil, fmt.Errorf("创建 cookie jar 失败: %w", err) + } + httpClient := &http.Client{ + Jar: jar, + } + + // 设置 TWFID cookie (使用默认配置) + setTWFIDCookie(httpClient, config.BaseURL, "") + + // 步骤1: 获取初始 Cookie + if err := getInitialCookies(httpClient); err != nil { + return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err) + } + + // 重试登录 + maxRetries := 3 + var loginSuccess bool + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("尝试登录 (第%d次)...", attempt) + + // 获取并识别验证码 + captchaCode, err := captcha.GetAndRecognize(httpClient) + if err != nil { + log.Printf(" 验证码获取失败: %v", err) + continue + } + + // 登录 + success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password) + if err != nil { + log.Printf(" 登录失败: %v", err) + continue + } + + if success { + loginSuccess = true + break + } + + log.Println(" 登录失败,3秒后重试...") + time.Sleep(3 * time.Second) + } + + if !loginSuccess { + return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries) + } + + // 获取课表 + courses, err := parser.FetchScheduleWithClient(httpClient, semester) + if err != nil { + return nil, fmt.Errorf("获取课表失败: %w", err) + } + + // 构建结果 + result := &pb.ScheduleResultData{ + StudentId: req.Username, + Semester: semester, + Courses: convertCourses(courses), + } + + return json.Marshal(result) +} + +// login 登录验证 +func (h *TaskHandler) login(payload []byte) ([]byte, error) { + var req pb.LoginPayload + if err := json.Unmarshal(payload, &req); err != nil { + return nil, fmt.Errorf("解析任务载荷失败: %w", err) + } + + log.Printf("登录验证: 用户=%s", req.Username) + + // 创建新的 HTTP 客户端 + jar, err := cookiejar.New(nil) + if err != nil { + return nil, fmt.Errorf("创建 cookie jar 失败: %w", err) + } + httpClient := &http.Client{ + Jar: jar, + } + + // 设置 TWFID cookie + setTWFIDCookie(httpClient, config.BaseURL, req.Twfid) + + // 步骤1: 获取初始 Cookie + if err := getInitialCookies(httpClient); err != nil { + return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err) + } + + // 重试登录 + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("尝试登录 (第%d次)...", attempt) + + // 获取并识别验证码 + captchaCode, err := captcha.GetAndRecognize(httpClient) + if err != nil { + log.Printf(" 验证码获取失败: %v", err) + continue + } + + // 登录 + success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password) + if err != nil { + log.Printf(" 登录失败: %v", err) + continue + } + + if success { + return json.Marshal(map[string]interface{}{ + "success": true, + "message": "登录成功", + }) + } + + log.Println(" 登录失败,3秒后重试...") + time.Sleep(3 * time.Second) + } + + return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries) +} + +// getInitialCookies 获取初始 Cookie +func getInitialCookies(httpClient *http.Client) error { + log.Println("[1] 访问登录页面获取初始 Cookie...") + + req, err := http.NewRequest("GET", config.BaseURL+"/loginNOCAS", nil) + if err != nil { + return err + } + req.Header = getHeaders() + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + log.Printf(" 登录页面状态码: %d", resp.StatusCode) + return nil +} + +// loginWithCredentials 使用凭据登录 +func loginWithCredentials(httpClient *http.Client, captchaCode, username, password string) (bool, error) { + return auth.LoginWithClient(httpClient, captchaCode, username, password) +} + +// setTWFIDCookie 设置 TWFID Cookie +func setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) { + if twfid == "" { + twfid = config.TWFID + } + u, _ := url.Parse(baseURL) + httpClient.Jar.SetCookies(u, []*http.Cookie{ + {Name: "TWFID", Value: twfid}, + }) +} + +// getHeaders 获取请求头 +func getHeaders() http.Header { + return http.Header{ + "User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}, + "Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}, + "Accept-Language": []string{"zh-CN,zh;q=0.9,en;q=0.8"}, + } +} + +// convertCourses 转换课程格式 +func convertCourses(courses []models.Course) []*pb.Course { + var result []*pb.Course + for _, c := range courses { + var schedule []*pb.ScheduleTime + for _, s := range c.Schedule { + schedule = append(schedule, &pb.ScheduleTime{ + Day: s.Day, + Sections: convertToInt32(s.Section), + Weeks: convertToInt32(s.Weeks), + }) + } + result = append(result, &pb.Course{ + Name: c.Name, + Teacher: c.Teacher, + Room: c.Room, + Schedule: schedule, + }) + } + return result +} + +// convertToInt32 将 []int 转换为 []int32 +func convertToInt32(arr []int) []int32 { + result := make([]int32, len(arr)) + for i, v := range arr { + result[i] = int32(v) + } + return result +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..bab6fc9 --- /dev/null +++ b/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "jwts/auth" + "jwts/captcha" + "jwts/client" + "jwts/config" + grpcClient "jwts/grpc" + "jwts/parser" + + "github.com/gin-gonic/gin" +) + +func main() { + mode := flag.String("mode", "http", "运行模式: http 或 grpc") + grpcAddr := flag.String("grpc-addr", "localhost:50051", "gRPC 服务器地址") + flag.Parse() + + if *mode == "grpc" { + runGRPCClient(*grpcAddr) + } else { + runHTTPServer() + } +} + +// runGRPCClient 运行 gRPC 客户端模式 +func runGRPCClient(serverAddr string) { + runnerID := os.Getenv("RUNNER_ID") + if runnerID == "" { + hostname, _ := os.Hostname() + runnerID = "runner-" + hostname + } + + version := os.Getenv("RUNNER_VERSION") + if version == "" { + version = "1.0.0" + } + + log.Printf("启动 gRPC 客户端, Runner ID: %s, 版本: %s", runnerID, version) + + client := grpcClient.NewClient(serverAddr, runnerID, version) + handler := grpcClient.NewTaskHandler() + client.SetHandler(handler) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // 处理信号 + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + if err := client.Connect(ctx); err != nil { + log.Printf("连接失败: %v", err) + os.Exit(1) + } + + go func() { + <-sigChan + log.Println("正在关闭...") + client.Close() + cancel() + }() + + // 启动心跳 + go client.HeartbeatLoop(ctx, 30*time.Second) + + // 运行主循环 + if err := client.Run(ctx); err != nil { + log.Printf("客户端错误: %v", err) + } +} + +// runHTTPServer 运行 HTTP 服务器模式 +func runHTTPServer() { + // 初始化客户端 + client.InitClient() + + r := gin.Default() + + // 健康检查接口 + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + + // 任务处理接口 + r.POST("/task", func(c *gin.Context) { + // 这里可以根据请求体参数决定执行什么任务 + // 目前默认执行原有的登录和获取课表逻辑 + if config.Username == "" || config.Password == "" { + c.JSON(http.StatusInternalServerError, gin.H{"error": "环境变量未设置"}) + return + } + + // 执行任务 + err := runTask() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "任务执行成功"}) + }) + + // 启动服务 + log.Println("Server starting on :8080") + if err := r.Run(":8080"); err != nil { + log.Fatalf("Failed to run server: %v", err) + } +} + +func runTask() error { + // 步骤1: 获取初始Cookie + err := client.GetInitialCookies() + if err != nil { + log.Printf("获取Cookie失败: %v", err) + return err + } + + // 重试登录 + maxRetries := 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("尝试登录 (第%d次)...", attempt) + + // 获取并识别验证码 + captchaCode, err := captcha.GetAndRecognize(client.Client) + if err != nil { + log.Printf(" 验证码获取失败: %v", err) + continue + } + + // 登录 + success, err := auth.Login(captchaCode) + if err != nil { + log.Printf(" 登录失败: %v", err) + continue + } + + if success { + // 获取课表 + _, err = parser.FetchSchedule(client.Client) + if err != nil { + log.Printf("获取课表失败: %v", err) + } + return nil + } + + log.Println(" 登录失败,3秒后重试...") + } + + return nil +} diff --git a/models/api.go b/models/api.go new file mode 100644 index 0000000..d2c2b19 --- /dev/null +++ b/models/api.go @@ -0,0 +1,36 @@ +package models + +// APIRequest API请求结构 +type APIRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + MaxTokens int `json:"max_tokens"` +} + +// Message 消息结构 +type Message struct { + Role string `json:"role"` + Content interface{} `json:"content"` +} + +// ContentItem 内容项 +type ContentItem struct { + Type string `json:"type,omitempty"` + Text string `json:"text,omitempty"` + ImageURL *ImageURL `json:"image_url,omitempty"` +} + +// ImageURL 图片URL +type ImageURL struct { + URL string `json:"url"` +} + +// APIResponse API响应结构 +type APIResponse struct { + Choices []Choice `json:"choices"` +} + +// Choice 选择 +type Choice struct { + Message Message `json:"message"` +} diff --git a/models/course.go b/models/course.go new file mode 100644 index 0000000..737c090 --- /dev/null +++ b/models/course.go @@ -0,0 +1,16 @@ +package models + +// Course 课程结构 +type Course struct { + Name string `json:"name"` + Teacher string `json:"teacher"` + Room string `json:"room"` + Schedule []Schedule `json:"schedule"` +} + +// Schedule 上课时间 +type Schedule struct { + Day string `json:"day"` + Section []int `json:"section"` + Weeks []int `json:"weeks"` +} diff --git a/parser/schedule.go b/parser/schedule.go new file mode 100644 index 0000000..a49d8a1 --- /dev/null +++ b/parser/schedule.go @@ -0,0 +1,531 @@ +package parser + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "sort" + "strconv" + "strings" + + "jwts/client" + "jwts/config" + "jwts/models" + + "github.com/PuerkitoBio/goquery" +) + +var weekTokenRegexp = regexp.MustCompile(`\[?\d+(?:[-,,]\d+)*\]?周`) + +type weekRoomEntry struct { + weeksStr string + room string +} + +// 获取课表 +func FetchSchedule(httpClient *http.Client) ([]models.Course, error) { + return FetchScheduleWithClient(httpClient, config.Semester) +} + +// FetchScheduleWithClient 使用指定的 HTTP 客户端和学期获取课表 +func FetchScheduleWithClient(httpClient *http.Client, semester string) ([]models.Course, error) { + fmt.Println("[5] 获取课表数据...") + + // POST请求获取课表 + data := fmt.Sprintf("xnxq=%s", semester) + req, err := http.NewRequest("POST", config.BaseURL+"/kbcx/queryGrkb", strings.NewReader(data)) + if err != nil { + return nil, err + } + + req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded") + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if err := os.WriteFile("grkb.html", body, 0644); err != nil { + fmt.Printf(" 警告: 无法保存课表页面: %v\n", err) + } else { + fmt.Println(" 课表页面已保存到: grkb.html") + } + + // 解析课表 + courses, err := ParseScheduleHTML(string(body)) + if err != nil { + return nil, err + } + + fmt.Printf(" 共提取 %d 门课程\n", len(courses)) + + // 输出到JSON + jsonData, err := os.Create("schedule_result.json") + if err != nil { + return nil, err + } + defer jsonData.Close() + + encoder := json.NewEncoder(jsonData) + encoder.SetIndent("", " ") + err = encoder.Encode(courses) + if err != nil { + return nil, err + } + fmt.Println(" JSON结果已保存到: schedule_result.json") + + // 打印课程信息 + for i, course := range courses { + fmt.Printf(" 课程 %d: %s - %s\n", i+1, course.Name, course.Teacher) + } + + return courses, nil +} + +// 解析课表HTML +func ParseScheduleHTML(html string) ([]models.Course, error) { + // 创建一个带Reader的Document + reader := strings.NewReader(html) + doc, err := goquery.NewDocumentFromReader(reader) + if err != nil { + return nil, err + } + + courseMap := make(map[string]*models.Course) + + // 找到课表table + doc.Find("table").Each(func(tableIdx int, table *goquery.Selection) { + class, _ := table.Attr("class") + if !strings.Contains(class, "addlist") { + return + } + + table.Find("tr").Each(func(trIdx int, tr *goquery.Selection) { + tds := tr.Find("td") + if tds.Length() < 3 { + return + } + + // 第一个td是上午/下午/晚上,第二个是节次 + section := tds.Eq(1).Text() + if section == "" { + return + } + + // 从第3个td开始是星期一到星期日 (索引2-8) + for dayIndex := 2; dayIndex <= 8; dayIndex++ { + if dayIndex >= tds.Length() { + break + } + dayTd := tds.Eq(dayIndex) + + // 获取HTML内容 + cellHTML, err := dayTd.Html() + if err != nil || cellHTML == "" || cellHTML == " " { + continue + } + + // 跳过  + if strings.TrimSpace(cellHTML) == " " { + continue + } + + day := getDayName(dayIndex - 1) + + // 解析课程信息 + parseCourseCellHTML(cellHTML, day, section, courseMap) + } + }) + }) + + // 将map转为slice + var courses []models.Course + for _, course := range courseMap { + courses = append(courses, *course) + } + + return courses, nil +} + +// 解析课程单元格HTML +func parseCourseCellHTML(cellHTML, day, section string, courseMap map[string]*models.Course) { + + // 检查是否有多个课程(用
分割) + if strings.Contains(cellHTML, "
") || strings.Contains(cellHTML, "
") || strings.Contains(cellHTML, "
") { + // 分割成多个课程 + // 先统一替换 + cellHTML = strings.ReplaceAll(cellHTML, "
", "|SPLIT|") + cellHTML = strings.ReplaceAll(cellHTML, "
", "|SPLIT|") + cellHTML = strings.ReplaceAll(cellHTML, "
", "|SPLIT|") + + courses := strings.Split(cellHTML, "|SPLIT|") + + var courseName string + for i, part := range courses { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + beforeWeek, weekEntries := extractWeekEntries(part) + if len(weekEntries) == 0 { + // 没有周数信息,这是课程名(教室会在周数后面或下一个部分) + courseName = part + continue + } + + // 有周数信息,说明这行包含老师+周数+地点 + // 需要结合之前的课程名 + if courseName == "" { + courseName = part + } + + // 先取片段中的第一个有效教室作为回退值 + fallbackRoom := "" + for _, entry := range weekEntries { + if entry.room != "" { + fallbackRoom = entry.room + break + } + } + + // 如果片段里没有教室,查看下一个部分是否是教室 + if fallbackRoom == "" { + // 找下一个部分 + for j := i + 1; j < len(courses); j++ { + nextPart := strings.TrimSpace(courses[j]) + if nextPart == "" { + continue + } + // 检查下一个部分是否是教室(包含楼/室/厅/院) + if strings.Contains(nextPart, "楼") || strings.Contains(nextPart, "室") || strings.Contains(nextPart, "厅") || strings.Contains(nextPart, "院") { + fallbackRoom = nextPart + break + } + // 如果下一个部分不包含周数信息,说明是课程名,停止查找 + if !regexp.MustCompile(`\d+.*周`).MatchString(nextPart) { + break + } + } + } + + // 解析老师(在beforeWeek中) + teacher := "" + // 去掉课程名部分 + if strings.HasPrefix(beforeWeek, courseName) { + teacher = strings.TrimSpace(beforeWeek[len(courseName):]) + } else { + // 老师可能直接在整个字符串中 + teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`) + teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek) + if teacherMatch != nil { + teacher = teacherMatch[1] + } + } + + // 解析节次 + sectionNums := parseSection(section) + + // 每个“周次-地点”片段分别入库,避免不同周次地点混淆 + for _, entry := range weekEntries { + room := strings.TrimSpace(entry.room) + if room == "" { + room = fallbackRoom + } + weeks := parseWeeks(entry.weeksStr) + if len(weeks) == 0 { + continue + } + + // 使用课程名+老师+教室作为 key,避免不同地点被错误合并 + key := buildCourseKey(courseName, teacher, room) + + // 检查是否已存在 + if existing, exists := courseMap[key]; exists { + // 添加新的上课时间 + existing.Schedule = append(existing.Schedule, models.Schedule{ + Day: day, + Section: sectionNums, + Weeks: weeks, + }) + if existing.Room == "" && room != "" { + existing.Room = room + } + } else { + // 创建新课程 + courseMap[key] = &models.Course{ + Name: courseName, + Teacher: teacher, + Room: room, + Schedule: []models.Schedule{ + { + Day: day, + Section: sectionNums, + Weeks: weeks, + }, + }, + } + } + } + + // 这个课程处理完后,重置courseName + courseName = "" + } + } else { + // 只有一个课程,直接解析 + line := strings.TrimSpace(cellHTML) + + beforeWeek, weekEntries := extractWeekEntries(line) + if len(weekEntries) == 0 { + return + } + + fallbackRoom := "" + for _, entry := range weekEntries { + if entry.room != "" { + fallbackRoom = entry.room + break + } + } + + // 从beforeWeek中提取老师和课程名 + teacherPattern := regexp.MustCompile(`([一-龥]{2,4})$`) + teacherMatch := teacherPattern.FindStringSubmatch(beforeWeek) + + var courseName, teacher string + if teacherMatch != nil { + teacher = teacherMatch[1] + courseName = strings.TrimSpace(beforeWeek[:len(beforeWeek)-len(teacher)]) + } else { + courseName = beforeWeek + } + + if courseName == "" { + return + } + + // 解析节次 + sectionNums := parseSection(section) + + for _, entry := range weekEntries { + room := strings.TrimSpace(entry.room) + if room == "" { + room = fallbackRoom + } + weeks := parseWeeks(entry.weeksStr) + if len(weeks) == 0 { + continue + } + + // 使用课程名+老师+教室作为 key,避免不同地点被错误合并 + key := buildCourseKey(courseName, teacher, room) + + if existing, exists := courseMap[key]; exists { + existing.Schedule = append(existing.Schedule, models.Schedule{ + Day: day, + Section: sectionNums, + Weeks: weeks, + }) + if existing.Room == "" && room != "" { + existing.Room = room + } + } else { + courseMap[key] = &models.Course{ + Name: courseName, + Teacher: teacher, + Room: room, + Schedule: []models.Schedule{ + { + Day: day, + Section: sectionNums, + Weeks: weeks, + }, + }, + } + } + } + } +} + +func buildCourseKey(name, teacher, room string) string { + return strings.TrimSpace(name) + "|" + strings.TrimSpace(teacher) + "|" + strings.TrimSpace(room) +} + +// extractWeekInfo 提取一段文本中的完整周次片段 +// 例如:于战华[15]周,[2-14]周 -> before=于战华, weeks=[15]周,[2-14]周, after= +func extractWeekInfo(text string) (beforeWeek, weeksPart, afterWeek string, ok bool) { + indexes := weekTokenRegexp.FindAllStringIndex(text, -1) + if len(indexes) == 0 { + return "", "", "", false + } + + firstStart := indexes[0][0] + beforeWeek = strings.TrimSpace(text[:firstStart]) + + parts := weekTokenRegexp.FindAllString(text, -1) + weeksPart = strings.Join(parts, ",") + + // 周次后(含中间)非周次内容作为教室候选 + tail := strings.TrimSpace(text[firstStart:]) + afterWeek = strings.TrimSpace(weekTokenRegexp.ReplaceAllString(tail, "")) + afterWeek = strings.Trim(afterWeek, ",, ") + return beforeWeek, weeksPart, afterWeek, true +} + +// extractWeekEntries 将一段文本拆成多个“周次-地点”片段 +// 例如:李鹏程[2-8]周N楼-335,[9]周N楼-331 +// 会拆成: +// 1) weeks=[2-8]周 room=N楼-335 +// 2) weeks=[9]周 room=N楼-331 +func extractWeekEntries(text string) (beforeWeek string, entries []weekRoomEntry) { + indexes := weekTokenRegexp.FindAllStringIndex(text, -1) + if len(indexes) == 0 { + return "", nil + } + + firstStart := indexes[0][0] + beforeWeek = strings.TrimSpace(text[:firstStart]) + + for i, idx := range indexes { + start := idx[0] + end := idx[1] + nextStart := len(text) + if i+1 < len(indexes) { + nextStart = indexes[i+1][0] + } + + weeksStr := strings.TrimSpace(text[start:end]) + room := strings.TrimSpace(text[end:nextStart]) + room = weekTokenRegexp.ReplaceAllString(room, "") + room = strings.Trim(room, ",, ") + room = strings.TrimSpace(room) + + entries = append(entries, weekRoomEntry{ + weeksStr: weeksStr, + room: room, + }) + } + + return beforeWeek, entries +} + +func getDayName(index int) string { + // 返回0-6,0=周一,6=周日 + days := []string{"0", "1", "2", "3", "4", "5", "6"} + if index >= 1 && index <= 7 { + return days[index-1] + } + return "" +} + +func parseSection(section string) []int { + // 格式: 1-2 或 3-4 + re := regexp.MustCompile(`(\d+)-(\d+)`) + match := re.FindStringSubmatch(section) + if match != nil { + start, _ := strconv.Atoi(match[1]) + end, _ := strconv.Atoi(match[2]) + var nums []int + for i := start; i <= end; i++ { + nums = append(nums, i) + } + return nums + } + + // 单独数字 + num, err := strconv.Atoi(section) + if err == nil { + return []int{num} + } + + return []int{} +} + +func parseWeeks(weeksStr string) []int { + if weeksStr == "" { + return []int{} + } + + // 统一格式:去掉括号/周/空白,统一中文分隔符 + raw := strings.TrimSpace(weeksStr) + oddOnly := strings.Contains(raw, "单") + evenOnly := strings.Contains(raw, "双") + + replacer := strings.NewReplacer( + "[", "", "]", "", + "(", "", ")", "", + "(", "", ")", "", + "周", "", + ",", ",", + "、", ",", + " ", "", + ) + normalized := replacer.Replace(raw) + if normalized == "" { + return []int{} + } + + weekSet := make(map[int]struct{}) + for _, part := range strings.Split(normalized, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + // 区间格式:1-8 + if strings.Contains(part, "-") { + rangeParts := strings.SplitN(part, "-", 2) + if len(rangeParts) != 2 { + continue + } + start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0])) + end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1])) + if err1 != nil || err2 != nil || start > end { + continue + } + for i := start; i <= end; i++ { + if oddOnly && i%2 == 0 { + continue + } + if evenOnly && i%2 != 0 { + continue + } + weekSet[i] = struct{}{} + } + continue + } + + // 单周次:例如 9 + num, err := strconv.Atoi(part) + if err != nil { + continue + } + if oddOnly && num%2 == 0 { + continue + } + if evenOnly && num%2 != 0 { + continue + } + weekSet[num] = struct{}{} + } + + if len(weekSet) == 0 { + return []int{} + } + + weeks := make([]int, 0, len(weekSet)) + for w := range weekSet { + weeks = append(weeks, w) + } + sort.Ints(weeks) + return weeks +} diff --git a/parser/schedule_test.go b/parser/schedule_test.go new file mode 100644 index 0000000..91df08d --- /dev/null +++ b/parser/schedule_test.go @@ -0,0 +1,234 @@ +package parser + +import ( + "os" + "path/filepath" + "testing" + + "jwts/models" +) + +func TestParseScheduleHTML_FromTestHTML_MergedWeeks(t *testing.T) { + htmlPath := filepath.Join("..", "test.html") + body, err := os.ReadFile(htmlPath) + if err != nil { + t.Fatalf("读取 test.html 失败: %v", err) + } + + courses, err := ParseScheduleHTML(string(body)) + if err != nil { + t.Fatalf("解析课表失败: %v", err) + } + + checkCourseHasWeek(t, courses, "大学物理X(1)", "王小赛", 1) + checkCourseHasWeek(t, courses, "大学物理X(1)", "王小赛", 16) + checkCourseHasWeek(t, courses, "数学分析(2)", "于战华", 2) + checkCourseHasWeek(t, courses, "数学分析(2)", "于战华", 15) + checkCourseHasRooms(t, courses, "中国近现代史纲要", "苏克", []string{"H楼-447", "H楼-434"}) +} + +func TestParseScheduleHTML_FromGrkbHTML_ChineseCommaWeeks(t *testing.T) { + htmlPath := filepath.Join("..", "grkb.html") + body, err := os.ReadFile(htmlPath) + if err != nil { + t.Fatalf("读取 grkb.html 失败: %v", err) + } + + courses, err := ParseScheduleHTML(string(body)) + if err != nil { + t.Fatalf("解析课表失败: %v", err) + } + + checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 10) + checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 11) + checkCourseHasWeek(t, courses, "数理统计", "黄春茂", 12) + checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 2) + checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 8) + checkCourseHasWeek(t, courses, "微分几何", "李鹏程", 9) + checkCourseRoomForWeek(t, courses, "微分几何", "李鹏程", 9, "N楼-331") + checkCourseRoomForWeek(t, courses, "微分几何", "李鹏程", 8, "N楼-335") +} + +func checkCourseWeeks( + t *testing.T, + courses []models.Course, + name, teacher, day string, + section []int, + wantStart, wantEnd int, +) { + t.Helper() + + for _, c := range courses { + if c.Name != name || c.Teacher != teacher { + continue + } + for _, s := range c.Schedule { + if s.Day != day || !sameInts(s.Section, section) { + continue + } + if len(s.Weeks) == 0 { + t.Fatalf("%s/%s 周次为空", name, teacher) + } + if s.Weeks[0] != wantStart || s.Weeks[len(s.Weeks)-1] != wantEnd { + t.Fatalf("%s/%s 周次范围错误: got=%v want=%d..%d", name, teacher, s.Weeks, wantStart, wantEnd) + } + return + } + } + + t.Fatalf("未找到课程: %s/%s day=%s section=%v", name, teacher, day, section) +} + +func sameInts(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func checkScheduleContainsWeeks( + t *testing.T, + courses []models.Course, + name, teacher, day string, + section []int, + wantWeeks []int, +) { + t.Helper() + + for _, c := range courses { + if c.Name != name || c.Teacher != teacher { + continue + } + for _, s := range c.Schedule { + if s.Day != day || !sameInts(s.Section, section) { + continue + } + if len(s.Weeks) == 0 { + t.Fatalf("%s/%s 周次为空", name, teacher) + } + for _, w := range wantWeeks { + if !containsInt(s.Weeks, w) { + t.Fatalf("%s/%s 周次缺失: want include=%d got=%v", name, teacher, w, s.Weeks) + } + } + return + } + } + + t.Fatalf("未找到课程: %s/%s day=%s section=%v", name, teacher, day, section) +} + +func containsInt(arr []int, target int) bool { + for _, v := range arr { + if v == target { + return true + } + } + return false +} + +func checkCourseHasWeek( + t *testing.T, + courses []models.Course, + name, teacher string, + week int, +) { + t.Helper() + + for _, c := range courses { + if c.Name != name || c.Teacher != teacher { + continue + } + for _, s := range c.Schedule { + if containsInt(s.Weeks, week) { + return + } + } + } + + t.Fatalf("%s/%s 缺少第%d周", name, teacher, week) +} + +func checkCourseRoomForWeek( + t *testing.T, + courses []models.Course, + name, teacher string, + week int, + wantRoom string, +) { + t.Helper() + + for _, c := range courses { + if c.Name != name || c.Teacher != teacher { + continue + } + for _, s := range c.Schedule { + if containsInt(s.Weeks, week) && c.Room == wantRoom { + return + } + } + } + + t.Fatalf("%s/%s 第%d周未匹配到地点 %s", name, teacher, week, wantRoom) +} + +func checkCourseHasRooms( + t *testing.T, + courses []models.Course, + name, teacher string, + wantRooms []string, +) { + t.Helper() + + roomSet := map[string]bool{} + for _, c := range courses { + if c.Name == name && c.Teacher == teacher { + roomSet[c.Room] = true + } + } + + for _, room := range wantRooms { + if !roomSet[room] { + t.Fatalf("%s/%s 缺少地点: %s, 当前地点集合: %v", name, teacher, room, roomSet) + } + } +} + +func checkCourseAnyScheduleContainsWeeks( + t *testing.T, + courses []models.Course, + name, teacher string, + wantWeeks []int, +) { + t.Helper() + + foundCourse := false + for _, c := range courses { + if c.Name != name || c.Teacher != teacher { + continue + } + foundCourse = true + for _, s := range c.Schedule { + allFound := true + for _, w := range wantWeeks { + if !containsInt(s.Weeks, w) { + allFound = false + break + } + } + if allFound { + return + } + } + } + + if !foundCourse { + t.Fatalf("未找到课程: %s/%s", name, teacher) + } + t.Fatalf("%s/%s 所有排课都不包含预期周次: want=%v", name, teacher, wantWeeks) +} diff --git a/proto/runner/runner.pb.go b/proto/runner/runner.pb.go new file mode 100644 index 0000000..8b821a2 --- /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\x1aZ\x18jwts/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..33f35b8 --- /dev/null +++ b/proto/runner/runner.proto @@ -0,0 +1,224 @@ +syntax = "proto3"; + +package runner; + +option go_package = "jwts/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..72b7c8d --- /dev/null +++ b/proto/runner/runner_grpc.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - 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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +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. +// +// RunnerHub 服务 - 由 Backend 服务器实现 +// Runner 作为客户端主动连接,建立双向流通信 +type RunnerHubClient interface { + // Connect 建立双向流连接 + // Runner 通过此连接注册、接收任务、返回结果 + Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], 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) (grpc.BidiStreamingClient[StreamMessage, StreamMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &RunnerHub_ServiceDesc.Streams[0], RunnerHub_Connect_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[StreamMessage, StreamMessage]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type RunnerHub_ConnectClient = grpc.BidiStreamingClient[StreamMessage, StreamMessage] + +// RunnerHubServer is the server API for RunnerHub service. +// All implementations must embed UnimplementedRunnerHubServer +// for forward compatibility. +// +// RunnerHub 服务 - 由 Backend 服务器实现 +// Runner 作为客户端主动连接,建立双向流通信 +type RunnerHubServer interface { + // Connect 建立双向流连接 + // Runner 通过此连接注册、接收任务、返回结果 + Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error + mustEmbedUnimplementedRunnerHubServer() +} + +// UnimplementedRunnerHubServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRunnerHubServer struct{} + +func (UnimplementedRunnerHubServer) Connect(grpc.BidiStreamingServer[StreamMessage, StreamMessage]) error { + return status.Error(codes.Unimplemented, "method Connect not implemented") +} +func (UnimplementedRunnerHubServer) mustEmbedUnimplementedRunnerHubServer() {} +func (UnimplementedRunnerHubServer) testEmbeddedByValue() {} + +// 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) { + // If the following call panics, it indicates UnimplementedRunnerHubServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RunnerHub_ServiceDesc, srv) +} + +func _RunnerHub_Connect_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(RunnerHubServer).Connect(&grpc.GenericServerStream[StreamMessage, StreamMessage]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type RunnerHub_ConnectServer = grpc.BidiStreamingServer[StreamMessage, StreamMessage] + +// 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", +} diff --git a/test.html b/test.html new file mode 100644 index 0000000..2b844d2 --- /dev/null +++ b/test.html @@ -0,0 +1,291 @@ + + + + + + + + + + + + +学生个人课表查询 + + + + + + + + + + + + + + + + + + + + + +
+
+
当前位置:学生选课 >> 个人课表查询
+
+
+
+ + + + + + + + + + + + + + + + + +
学年学期: + + + + + + + +
+
+
+
课表确认时间:~
+
+ +
+ +
+
2026春季学期(2025211232)宋长霖课表(2026-03-13 15:40:22)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 星期一星期二星期三星期四星期五星期六星期日
上午1-2中国近现代史纲要
苏克[1-16]周H楼-447
体育(2)(高尔夫)
毕剑峰[1-16]周
计算与智能程序设计
陈浩[9-12]周N楼-118
形势与政策(1)
刘文颖[1-4]周H楼-463
中国近现代史纲要
苏克[9-12]周H楼-434
计算与智能程序设计
陈浩[9-12]周N楼-118
  
上午3-4数学分析(2)
于战华[15]周,[2-14]周
M楼-303
学术英语读写(2025版大一下)
苏敏[1-17]周N楼-255
数学分析(2)
于战华[2-14]周M楼-303
数学分析(2)
于战华[2-14]周M楼-303
悦己人生
黄靖涵[1-8]周大活-北301
学术英语读写(2025版大一下)
苏敏[9]周N楼-255
 
下午5-6写作与沟通
侯薇[1-8]周N楼-208
爱情与婚姻
师韵茗[1-8]周M楼-201
外汇交易原理与实务
王继东[9-16]周M楼-106
     
下午7-8大学物理X(1)
王小赛[1-8]周,[9-16]周
M楼-307
电路与电子技术(1)
菅维乐[1-8]周,[9-12]周
M楼-205
大学物理X(1)
王小赛[1-8]周,[9-16]周
M楼-307
电路与电子技术(1)
菅维乐[1-8]周,[9-12]周
M楼-205
大学物理X(1)
王小赛[1-8]周M楼-307
  
晚上9-10       
晚上11-12       
其它课程: 新型储能电源前沿与进展◇李旭东◇0◇2' 大学物理实验x(1)◇李盛凤◇1-16◇847290108334833812 APP群号' +
+
+
+
+ + +
+
+ 注意:“个人课表查询”是按照您的选课进行显示,请仔细与“班级推荐课表查询”页面进行对比,如发现个人课表中缺少本班级的推荐课程(特别是必修课),则说明您可能存在漏选情况,请认真核实,如有需要请及时进行补选。 +
+
+
+
+ + + + + + + + + +
+ + + + + + + +