feat(grpc): add support for fetching empty classrooms
All checks were successful
Build / build (push) Successful in 1m31s

Implement the `TASK_TYPE_GET_EMPTY_CLASSROOM` task type to allow users to fetch available classroom information.

- Add `TASK_TYPE_GET_EMPTY_CLASSROOM` to the `TaskType` enum in protobuf.
- Define `GetEmptyClassroomPayload` and `EmptyClassroomResultData` messages.
- Implement `getEmptyClassrooms`
This commit is contained in:
2026-06-02 13:59:30 +08:00
parent d64f2fa20b
commit ee5c36f3ac
7 changed files with 663 additions and 53 deletions

View File

@@ -77,10 +77,11 @@ func (c *Client) register() error {
RunnerId: c.runnerID, RunnerId: c.runnerID,
Version: c.version, Version: c.version,
Capabilities: map[string]string{ Capabilities: map[string]string{
"TASK_TYPE_GET_SCHEDULE": "true", "TASK_TYPE_GET_SCHEDULE": "true",
"TASK_TYPE_GET_GRADES": "true", "TASK_TYPE_GET_GRADES": "true",
"TASK_TYPE_GET_EXAMS": "true", "TASK_TYPE_GET_EXAMS": "true",
"TASK_TYPE_LOGIN": "true", "TASK_TYPE_LOGIN": "true",
"TASK_TYPE_GET_EMPTY_CLASSROOM": "true",
}, },
}, },
}, },

View File

@@ -58,6 +58,8 @@ func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
data, err = h.getExams(context.Background(), task.Payload) data, err = h.getExams(context.Background(), task.Payload)
case pb.TaskType_TASK_TYPE_LOGIN: case pb.TaskType_TASK_TYPE_LOGIN:
data, err = h.login(context.Background(), task.Payload) data, err = h.login(context.Background(), task.Payload)
case pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM:
data, err = h.getEmptyClassrooms(context.Background(), task.Payload)
default: default:
result.Status = pb.TaskStatus_TASK_STATUS_FAILED result.Status = pb.TaskStatus_TASK_STATUS_FAILED
result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type) result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type)
@@ -383,3 +385,55 @@ func convertExams(exams []parser.ExamEntry) []*pb.Exam {
} }
return result return result
} }
func (h *TaskHandler) getEmptyClassrooms(ctx context.Context, payload []byte) ([]byte, error) {
var req pb.GetEmptyClassroomPayload
if err := json.Unmarshal(payload, &req); err != nil {
return nil, apperr.New("parse_payload", err, "解析任务载荷失败")
}
log := slog.With("username", req.Username, "semester", req.Semester)
log.Info("获取空教室任务")
httpClient, err := createHTTPClient()
if err != nil {
return nil, err
}
cfg := config.Get()
setTWFIDCookie(httpClient, cfg.BaseURL, "")
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
return nil, err
}
classrooms, err := parser.FetchEmptyClassrooms(httpClient, req.Semester, req.WeekStart, req.WeekEnd, req.CampusCode)
if err != nil {
return nil, apperr.New("fetch_empty_classrooms", err, "获取空教室失败")
}
pbClassrooms := convertEmptyClassrooms(classrooms)
result := &pb.EmptyClassroomResultData{
StudentId: req.Username,
Semester: req.Semester,
Classrooms: pbClassrooms,
}
log.Info("空教室获取成功", "classroom_count", len(pbClassrooms))
return json.Marshal(result)
}
func convertEmptyClassrooms(entries []parser.EmptyClassroomEntry) []*pb.EmptyClassroomEntry {
result := make([]*pb.EmptyClassroomEntry, 0, len(entries))
for _, e := range entries {
result = append(result, &pb.EmptyClassroomEntry{
Classroom: e.Classroom,
Weekday: e.Weekday,
Periods: e.Periods,
Term: e.Term,
Weeks: e.Weeks,
})
}
return result
}

256
parser/empty_classroom.go Normal file
View File

@@ -0,0 +1,256 @@
package parser
import (
"fmt"
"net/http"
"strings"
"schedule_converter/client"
"schedule_converter/config"
"github.com/PuerkitoBio/goquery"
)
type EmptyClassroomEntry struct {
Classroom string `json:"classroom"`
Weekday string `json:"weekday"`
Periods string `json:"periods"`
Term string `json:"term"`
Weeks string `json:"weeks"`
}
type BuildingInfo struct {
DM string `json:"dm"`
MC string `json:"mc"`
}
type VenueInfo struct {
DM string `json:"dm"`
MC string `json:"mc"`
}
func FetchEmptyClassrooms(httpClient *http.Client, semester, weekStart, weekEnd, campusCode string) ([]EmptyClassroomEntry, error) {
url := config.BaseURL + "/kjscx/queryKjs"
fmt.Printf("[空教室] 正在获取空教室数据 (学期=%s, 周次=%s-%s)...\n", semester, weekStart, weekEnd)
// First GET to establish session
resp, err := httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("访问空教室页面失败: %w", err)
}
resp.Body.Close()
// POST with query parameters
data := fmt.Sprintf("pageXnxq=%s&pageZc1=%s&pageZc2=%s&pageXiaoqu=%s",
semester, weekStart, weekEnd, campusCode)
req, err := http.NewRequest("POST", url, strings.NewReader(data))
if err != nil {
return nil, err
}
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
req.Header.Set("Referer", url)
resp, err = httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("查询空教室失败: %w", err)
}
defer resp.Body.Close()
html, err := decodeResponseBody(resp)
if err != nil {
return nil, err
}
entries, err := ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd)
if err != nil {
return nil, err
}
fmt.Printf("[空教室] 共获取 %d 条空教室记录\n", len(entries))
return entries, nil
}
func ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]EmptyClassroomEntry, error) {
if html == "" {
return nil, nil
}
// Clean nbsp entities
cleaned := strings.ReplaceAll(html, "&nbsp", "")
cleaned = strings.ReplaceAll(cleaned, "&", "&")
doc, err := goquery.NewDocumentFromReader(strings.NewReader(cleaned))
if err != nil {
return nil, err
}
dayNames := [7]string{"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"}
periodLabels := [6]string{"1,2", "3,4", "5,6", "7,8", "9,10", "11,12"}
var classrooms []EmptyClassroomEntry
doc.Find("table").Each(func(_ int, table *goquery.Selection) {
rows := table.Find("tr")
if rows.Length() < 3 {
return
}
// Check if this table has "星期一" in header
headerText := ""
table.Find("th").Each(func(_ int, th *goquery.Selection) {
headerText += th.Text()
})
if !strings.Contains(headerText, "星期一") {
return
}
// Parse period row (second row) to map column index to (day, period)
periodRow := rows.Eq(1)
periodCells := periodRow.Find("td")
periodCellTexts := make([]string, periodCells.Length())
periodCells.Each(func(i int, cell *goquery.Selection) {
periodCellTexts[i] = strings.TrimSpace(cell.Text())
})
colToDayPeriod := make(map[int][2]string)
colIdx := 1
for dayIdx := 0; dayIdx < 7; dayIdx++ {
for periodIdx := 0; periodIdx < 6; periodIdx++ {
var periodText string
if colIdx < len(periodCellTexts) {
periodText = periodCellTexts[colIdx]
} else {
periodText = periodLabels[periodIdx]
}
colToDayPeriod[colIdx] = [2]string{dayNames[dayIdx], periodText}
colIdx++
}
}
// Data rows start from row 2+
rows.FilterFunction(func(_ int, row *goquery.Selection) bool {
return row.Index() >= 2
}).Each(func(_ int, row *goquery.Selection) {
cells := row.Find("td")
if cells.Length() < 2 {
return
}
classroomName := strings.TrimSpace(cells.First().Text())
if classroomName == "" || len(classroomName) < 2 {
return
}
var slots []string
var weekdays []string
weekdaySet := make(map[string]bool)
for colIdx := 1; colIdx < cells.Length(); colIdx++ {
cell := cells.Eq(colIdx)
// Check if cell has kjs_icon div (indicates occupied, not empty)
hasIcon := false
cell.Find("div").Each(func(_ int, div *goquery.Selection) {
if classAttr, exists := div.Attr("class"); exists && strings.Contains(classAttr, "kjs_icon") {
hasIcon = true
}
})
if !hasIcon && colToDayPeriod[colIdx][0] != "" {
dayName := colToDayPeriod[colIdx][0]
periodText := colToDayPeriod[colIdx][1]
var slot string
if periodText != "" {
slot = fmt.Sprintf("%s 第%s节", dayName, periodText)
} else {
slot = dayName
}
slots = append(slots, slot)
if !weekdaySet[dayName] {
weekdaySet[dayName] = true
weekdays = append(weekdays, dayName)
}
}
}
if len(slots) > 0 {
weekStr := ""
if weekStart != "" && weekEnd != "" {
weekStr = fmt.Sprintf("%s-%s周", weekStart, weekEnd)
}
entry := EmptyClassroomEntry{
Classroom: classroomName,
Weekday: strings.Join(weekdays, ", "),
Periods: strings.Join(slots, "\n"),
Term: semester,
Weeks: weekStr,
}
classrooms = append(classrooms, entry)
}
})
})
return classrooms, nil
}
func FetchBuildings(httpClient *http.Client, campusCode string) ([]BuildingInfo, error) {
url := config.BaseURL + "/kjscx/queryJxlListBySjid"
fmt.Printf("[空教室] 正在获取楼号列表 (校区=%s)...\n", campusCode)
data := fmt.Sprintf("id=%s", campusCode)
req, err := http.NewRequest("POST", url, strings.NewReader(data))
if err != nil {
return nil, err
}
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
req.Header.Set("Referer", config.BaseURL+"/kjscx/queryKjs")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("获取楼号列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("获取楼号列表失败, 状态码: %d", resp.StatusCode)
}
var buildings []BuildingInfo
if err := decodeJSONResponse(resp, &buildings); err != nil {
return nil, fmt.Errorf("解析楼号列表失败: %w", err)
}
return buildings, nil
}
func FetchVenues(httpClient *http.Client, buildingCode string) ([]VenueInfo, error) {
url := config.BaseURL + "/kjscx/queryJxcdListBySjid"
fmt.Printf("[空教室] 正在获取场地列表 (楼号=%s)...\n", buildingCode)
data := fmt.Sprintf("id=%s", buildingCode)
req, err := http.NewRequest("POST", url, strings.NewReader(data))
if err != nil {
return nil, err
}
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
req.Header.Set("Referer", config.BaseURL+"/kjscx/queryKjs")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("获取场地列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("获取场地列表失败, 状态码: %d", resp.StatusCode)
}
var venues []VenueInfo
if err := decodeJSONResponse(resp, &venues); err != nil {
return nil, fmt.Errorf("解析场地列表失败: %w", err)
}
return venues, nil
}

View File

@@ -2,6 +2,7 @@ package parser
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"net/http" "net/http"
"strings" "strings"
@@ -72,3 +73,11 @@ func isValidUTF8(data []byte) bool {
} }
return true return true
} }
func decodeJSONResponse(resp *http.Response, v any) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.NewDecoder(bytes.NewReader(body)).Decode(v)
}

View File

@@ -25,12 +25,13 @@ const (
type TaskType int32 type TaskType int32
const ( const (
TaskType_TASK_TYPE_UNKNOWN TaskType = 0 // 未知类型 TaskType_TASK_TYPE_UNKNOWN TaskType = 0 // 未知类型
TaskType_TASK_TYPE_LOGIN TaskType = 1 // 登录验证 TaskType_TASK_TYPE_LOGIN TaskType = 1 // 登录验证
TaskType_TASK_TYPE_GET_SCHEDULE TaskType = 2 // 获取课表 TaskType_TASK_TYPE_GET_SCHEDULE TaskType = 2 // 获取课表
TaskType_TASK_TYPE_GET_GRADES TaskType = 3 // 获取成绩 TaskType_TASK_TYPE_GET_GRADES TaskType = 3 // 获取成绩
TaskType_TASK_TYPE_GET_EXAMS TaskType = 4 // 获取考试安排 TaskType_TASK_TYPE_GET_EXAMS TaskType = 4 // 获取考试安排
TaskType_TASK_TYPE_GET_USER_INFO TaskType = 5 // 获取用户信息 TaskType_TASK_TYPE_GET_USER_INFO TaskType = 5 // 获取用户信息
TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM TaskType = 6 // 获取空教室
) )
// Enum value maps for TaskType. // Enum value maps for TaskType.
@@ -42,14 +43,16 @@ var (
3: "TASK_TYPE_GET_GRADES", 3: "TASK_TYPE_GET_GRADES",
4: "TASK_TYPE_GET_EXAMS", 4: "TASK_TYPE_GET_EXAMS",
5: "TASK_TYPE_GET_USER_INFO", 5: "TASK_TYPE_GET_USER_INFO",
6: "TASK_TYPE_GET_EMPTY_CLASSROOM",
} }
TaskType_value = map[string]int32{ TaskType_value = map[string]int32{
"TASK_TYPE_UNKNOWN": 0, "TASK_TYPE_UNKNOWN": 0,
"TASK_TYPE_LOGIN": 1, "TASK_TYPE_LOGIN": 1,
"TASK_TYPE_GET_SCHEDULE": 2, "TASK_TYPE_GET_SCHEDULE": 2,
"TASK_TYPE_GET_GRADES": 3, "TASK_TYPE_GET_GRADES": 3,
"TASK_TYPE_GET_EXAMS": 4, "TASK_TYPE_GET_EXAMS": 4,
"TASK_TYPE_GET_USER_INFO": 5, "TASK_TYPE_GET_USER_INFO": 5,
"TASK_TYPE_GET_EMPTY_CLASSROOM": 6,
} }
) )
@@ -1711,6 +1714,237 @@ func (x *UserInfoResultData) GetGrade() string {
return "" return ""
} }
// GetEmptyClassroomPayload 获取空教室任务载荷
type GetEmptyClassroomPayload 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"` // 学期
WeekStart string `protobuf:"bytes,4,opt,name=week_start,json=weekStart,proto3" json:"week_start,omitempty"` // 起始周次
WeekEnd string `protobuf:"bytes,5,opt,name=week_end,json=weekEnd,proto3" json:"week_end,omitempty"` // 结束周次
CampusCode string `protobuf:"bytes,6,opt,name=campus_code,json=campusCode,proto3" json:"campus_code,omitempty"` // 校区代码
BuildingCode string `protobuf:"bytes,7,opt,name=building_code,json=buildingCode,proto3" json:"building_code,omitempty"` // 楼号代码(可选)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetEmptyClassroomPayload) Reset() {
*x = GetEmptyClassroomPayload{}
mi := &file_proto_runner_runner_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetEmptyClassroomPayload) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetEmptyClassroomPayload) ProtoMessage() {}
func (x *GetEmptyClassroomPayload) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[21]
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 GetEmptyClassroomPayload.ProtoReflect.Descriptor instead.
func (*GetEmptyClassroomPayload) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{21}
}
func (x *GetEmptyClassroomPayload) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *GetEmptyClassroomPayload) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *GetEmptyClassroomPayload) GetSemester() string {
if x != nil {
return x.Semester
}
return ""
}
func (x *GetEmptyClassroomPayload) GetWeekStart() string {
if x != nil {
return x.WeekStart
}
return ""
}
func (x *GetEmptyClassroomPayload) GetWeekEnd() string {
if x != nil {
return x.WeekEnd
}
return ""
}
func (x *GetEmptyClassroomPayload) GetCampusCode() string {
if x != nil {
return x.CampusCode
}
return ""
}
func (x *GetEmptyClassroomPayload) GetBuildingCode() string {
if x != nil {
return x.BuildingCode
}
return ""
}
// EmptyClassroomResultData 空教室结果数据
type EmptyClassroomResultData 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"` // 学号
Classrooms []*EmptyClassroomEntry `protobuf:"bytes,3,rep,name=classrooms,proto3" json:"classrooms,omitempty"` // 空教室列表
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EmptyClassroomResultData) Reset() {
*x = EmptyClassroomResultData{}
mi := &file_proto_runner_runner_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyClassroomResultData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyClassroomResultData) ProtoMessage() {}
func (x *EmptyClassroomResultData) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[22]
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 EmptyClassroomResultData.ProtoReflect.Descriptor instead.
func (*EmptyClassroomResultData) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{22}
}
func (x *EmptyClassroomResultData) GetSemester() string {
if x != nil {
return x.Semester
}
return ""
}
func (x *EmptyClassroomResultData) GetStudentId() string {
if x != nil {
return x.StudentId
}
return ""
}
func (x *EmptyClassroomResultData) GetClassrooms() []*EmptyClassroomEntry {
if x != nil {
return x.Classrooms
}
return nil
}
// EmptyClassroomEntry 空教室条目
type EmptyClassroomEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Classroom string `protobuf:"bytes,1,opt,name=classroom,proto3" json:"classroom,omitempty"` // 教室名称
Weekday string `protobuf:"bytes,2,opt,name=weekday,proto3" json:"weekday,omitempty"` // 星期几
Periods string `protobuf:"bytes,3,opt,name=periods,proto3" json:"periods,omitempty"` // 可用节次
Term string `protobuf:"bytes,4,opt,name=term,proto3" json:"term,omitempty"` // 学期描述
Weeks string `protobuf:"bytes,5,opt,name=weeks,proto3" json:"weeks,omitempty"` // 周次范围
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EmptyClassroomEntry) Reset() {
*x = EmptyClassroomEntry{}
mi := &file_proto_runner_runner_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EmptyClassroomEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EmptyClassroomEntry) ProtoMessage() {}
func (x *EmptyClassroomEntry) ProtoReflect() protoreflect.Message {
mi := &file_proto_runner_runner_proto_msgTypes[23]
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 EmptyClassroomEntry.ProtoReflect.Descriptor instead.
func (*EmptyClassroomEntry) Descriptor() ([]byte, []int) {
return file_proto_runner_runner_proto_rawDescGZIP(), []int{23}
}
func (x *EmptyClassroomEntry) GetClassroom() string {
if x != nil {
return x.Classroom
}
return ""
}
func (x *EmptyClassroomEntry) GetWeekday() string {
if x != nil {
return x.Weekday
}
return ""
}
func (x *EmptyClassroomEntry) GetPeriods() string {
if x != nil {
return x.Periods
}
return ""
}
func (x *EmptyClassroomEntry) GetTerm() string {
if x != nil {
return x.Term
}
return ""
}
func (x *EmptyClassroomEntry) GetWeeks() string {
if x != nil {
return x.Weeks
}
return ""
}
var File_proto_runner_runner_proto protoreflect.FileDescriptor var File_proto_runner_runner_proto protoreflect.FileDescriptor
const file_proto_runner_runner_proto_rawDesc = "" + const file_proto_runner_runner_proto_rawDesc = "" +
@@ -1849,14 +2083,38 @@ const file_proto_runner_runner_proto_rawDesc = "" +
"\x05major\x18\x05 \x01(\tR\x05major\x12\x1d\n" + "\x05major\x18\x05 \x01(\tR\x05major\x12\x1d\n" +
"\n" + "\n" +
"class_name\x18\x06 \x01(\tR\tclassName\x12\x14\n" + "class_name\x18\x06 \x01(\tR\tclassName\x12\x14\n" +
"\x05grade\x18\a \x01(\tR\x05grade*\xa2\x01\n" + "\x05grade\x18\a \x01(\tR\x05grade\"\xee\x01\n" +
"\x18GetEmptyClassroomPayload\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\x12\x1d\n" +
"\n" +
"week_start\x18\x04 \x01(\tR\tweekStart\x12\x19\n" +
"\bweek_end\x18\x05 \x01(\tR\aweekEnd\x12\x1f\n" +
"\vcampus_code\x18\x06 \x01(\tR\n" +
"campusCode\x12#\n" +
"\rbuilding_code\x18\a \x01(\tR\fbuildingCode\"\x92\x01\n" +
"\x18EmptyClassroomResultData\x12\x1a\n" +
"\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" +
"\n" +
"student_id\x18\x02 \x01(\tR\tstudentId\x12;\n" +
"\n" +
"classrooms\x18\x03 \x03(\v2\x1b.runner.EmptyClassroomEntryR\n" +
"classrooms\"\x91\x01\n" +
"\x13EmptyClassroomEntry\x12\x1c\n" +
"\tclassroom\x18\x01 \x01(\tR\tclassroom\x12\x18\n" +
"\aweekday\x18\x02 \x01(\tR\aweekday\x12\x18\n" +
"\aperiods\x18\x03 \x01(\tR\aperiods\x12\x12\n" +
"\x04term\x18\x04 \x01(\tR\x04term\x12\x14\n" +
"\x05weeks\x18\x05 \x01(\tR\x05weeks*\xc5\x01\n" +
"\bTaskType\x12\x15\n" + "\bTaskType\x12\x15\n" +
"\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x13\n" + "\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x13\n" +
"\x0fTASK_TYPE_LOGIN\x10\x01\x12\x1a\n" + "\x0fTASK_TYPE_LOGIN\x10\x01\x12\x1a\n" +
"\x16TASK_TYPE_GET_SCHEDULE\x10\x02\x12\x18\n" + "\x16TASK_TYPE_GET_SCHEDULE\x10\x02\x12\x18\n" +
"\x14TASK_TYPE_GET_GRADES\x10\x03\x12\x17\n" + "\x14TASK_TYPE_GET_GRADES\x10\x03\x12\x17\n" +
"\x13TASK_TYPE_GET_EXAMS\x10\x04\x12\x1b\n" + "\x13TASK_TYPE_GET_EXAMS\x10\x04\x12\x1b\n" +
"\x17TASK_TYPE_GET_USER_INFO\x10\x05*\x8a\x01\n" + "\x17TASK_TYPE_GET_USER_INFO\x10\x05\x12!\n" +
"\x1dTASK_TYPE_GET_EMPTY_CLASSROOM\x10\x06*\x8a\x01\n" +
"\n" + "\n" +
"TaskStatus\x12\x17\n" + "TaskStatus\x12\x17\n" +
"\x13TASK_STATUS_UNKNOWN\x10\x00\x12\x17\n" + "\x13TASK_STATUS_UNKNOWN\x10\x00\x12\x17\n" +
@@ -1880,32 +2138,35 @@ func file_proto_runner_runner_proto_rawDescGZIP() []byte {
} }
var file_proto_runner_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_proto_runner_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
var file_proto_runner_runner_proto_goTypes = []any{ var file_proto_runner_runner_proto_goTypes = []any{
(TaskType)(0), // 0: runner.TaskType (TaskType)(0), // 0: runner.TaskType
(TaskStatus)(0), // 1: runner.TaskStatus (TaskStatus)(0), // 1: runner.TaskStatus
(*StreamMessage)(nil), // 2: runner.StreamMessage (*StreamMessage)(nil), // 2: runner.StreamMessage
(*RegisterRequest)(nil), // 3: runner.RegisterRequest (*RegisterRequest)(nil), // 3: runner.RegisterRequest
(*RegisterResponse)(nil), // 4: runner.RegisterResponse (*RegisterResponse)(nil), // 4: runner.RegisterResponse
(*Heartbeat)(nil), // 5: runner.Heartbeat (*Heartbeat)(nil), // 5: runner.Heartbeat
(*HeartbeatAck)(nil), // 6: runner.HeartbeatAck (*HeartbeatAck)(nil), // 6: runner.HeartbeatAck
(*Task)(nil), // 7: runner.Task (*Task)(nil), // 7: runner.Task
(*TaskCancel)(nil), // 8: runner.TaskCancel (*TaskCancel)(nil), // 8: runner.TaskCancel
(*TaskResult)(nil), // 9: runner.TaskResult (*TaskResult)(nil), // 9: runner.TaskResult
(*LoginPayload)(nil), // 10: runner.LoginPayload (*LoginPayload)(nil), // 10: runner.LoginPayload
(*GetSchedulePayload)(nil), // 11: runner.GetSchedulePayload (*GetSchedulePayload)(nil), // 11: runner.GetSchedulePayload
(*GetGradesPayload)(nil), // 12: runner.GetGradesPayload (*GetGradesPayload)(nil), // 12: runner.GetGradesPayload
(*GetExamsPayload)(nil), // 13: runner.GetExamsPayload (*GetExamsPayload)(nil), // 13: runner.GetExamsPayload
(*ScheduleResultData)(nil), // 14: runner.ScheduleResultData (*ScheduleResultData)(nil), // 14: runner.ScheduleResultData
(*Course)(nil), // 15: runner.Course (*Course)(nil), // 15: runner.Course
(*ScheduleTime)(nil), // 16: runner.ScheduleTime (*ScheduleTime)(nil), // 16: runner.ScheduleTime
(*GpaSummary)(nil), // 17: runner.GpaSummary (*GpaSummary)(nil), // 17: runner.GpaSummary
(*GradesResultData)(nil), // 18: runner.GradesResultData (*GradesResultData)(nil), // 18: runner.GradesResultData
(*Grade)(nil), // 19: runner.Grade (*Grade)(nil), // 19: runner.Grade
(*ExamsResultData)(nil), // 20: runner.ExamsResultData (*ExamsResultData)(nil), // 20: runner.ExamsResultData
(*Exam)(nil), // 21: runner.Exam (*Exam)(nil), // 21: runner.Exam
(*UserInfoResultData)(nil), // 22: runner.UserInfoResultData (*UserInfoResultData)(nil), // 22: runner.UserInfoResultData
nil, // 23: runner.RegisterRequest.CapabilitiesEntry (*GetEmptyClassroomPayload)(nil), // 23: runner.GetEmptyClassroomPayload
(*EmptyClassroomResultData)(nil), // 24: runner.EmptyClassroomResultData
(*EmptyClassroomEntry)(nil), // 25: runner.EmptyClassroomEntry
nil, // 26: runner.RegisterRequest.CapabilitiesEntry
} }
var file_proto_runner_runner_proto_depIdxs = []int32{ var file_proto_runner_runner_proto_depIdxs = []int32{
3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest 3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest
@@ -1915,7 +2176,7 @@ var file_proto_runner_runner_proto_depIdxs = []int32{
7, // 4: runner.StreamMessage.task:type_name -> runner.Task 7, // 4: runner.StreamMessage.task:type_name -> runner.Task
9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult 9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult
8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel 8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel
23, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry 26, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry
0, // 8: runner.Task.type:type_name -> runner.TaskType 0, // 8: runner.Task.type:type_name -> runner.TaskType
1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus 1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus
15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course 15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course
@@ -1923,13 +2184,14 @@ var file_proto_runner_runner_proto_depIdxs = []int32{
19, // 12: runner.GradesResultData.grades:type_name -> runner.Grade 19, // 12: runner.GradesResultData.grades:type_name -> runner.Grade
17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary 17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary
21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam 21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam
2, // 15: runner.RunnerHub.Connect:input_type -> runner.StreamMessage 25, // 15: runner.EmptyClassroomResultData.classrooms:type_name -> runner.EmptyClassroomEntry
2, // 16: runner.RunnerHub.Connect:output_type -> runner.StreamMessage 2, // 16: runner.RunnerHub.Connect:input_type -> runner.StreamMessage
16, // [16:17] is the sub-list for method output_type 2, // 17: runner.RunnerHub.Connect:output_type -> runner.StreamMessage
15, // [15:16] is the sub-list for method input_type 17, // [17:18] is the sub-list for method output_type
15, // [15:15] is the sub-list for extension type_name 16, // [16:17] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension extendee 16, // [16:16] is the sub-list for extension type_name
0, // [0:15] is the sub-list for field type_name 16, // [16:16] is the sub-list for extension extendee
0, // [0:16] is the sub-list for field type_name
} }
func init() { file_proto_runner_runner_proto_init() } func init() { file_proto_runner_runner_proto_init() }
@@ -1952,7 +2214,7 @@ func file_proto_runner_runner_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 22, NumMessages: 25,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -81,6 +81,7 @@ enum TaskType {
TASK_TYPE_GET_GRADES = 3; // 获取成绩 TASK_TYPE_GET_GRADES = 3; // 获取成绩
TASK_TYPE_GET_EXAMS = 4; // 获取考试安排 TASK_TYPE_GET_EXAMS = 4; // 获取考试安排
TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息 TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息
TASK_TYPE_GET_EMPTY_CLASSROOM = 6; // 获取空教室
} }
// TaskStatus 任务状态枚举 // TaskStatus 任务状态枚举
@@ -236,3 +237,30 @@ message UserInfoResultData {
string class_name = 6; // 班级 string class_name = 6; // 班级
string grade = 7; // 年级 string grade = 7; // 年级
} }
// GetEmptyClassroomPayload 获取空教室任务载荷
message GetEmptyClassroomPayload {
string username = 1; // 学号
string password = 2; // 密码
string semester = 3; // 学期
string week_start = 4; // 起始周次
string week_end = 5; // 结束周次
string campus_code = 6; // 校区代码
string building_code = 7; // 楼号代码(可选)
}
// EmptyClassroomResultData 空教室结果数据
message EmptyClassroomResultData {
string semester = 1; // 学期
string student_id = 2; // 学号
repeated EmptyClassroomEntry classrooms = 3; // 空教室列表
}
// EmptyClassroomEntry 空教室条目
message EmptyClassroomEntry {
string classroom = 1; // 教室名称
string weekday = 2; // 星期几
string periods = 3; // 可用节次
string term = 4; // 学期描述
string weeks = 5; // 周次范围
}

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.1 // - protoc-gen-go-grpc v1.6.2
// - protoc v7.34.1 // - protoc v7.34.1
// source: proto/runner/runner.proto // source: proto/runner/runner.proto