diff --git a/grpc/client.go b/grpc/client.go index b053e85..c798dbd 100644 --- a/grpc/client.go +++ b/grpc/client.go @@ -77,10 +77,11 @@ func (c *Client) register() error { RunnerId: c.runnerID, Version: c.version, Capabilities: map[string]string{ - "TASK_TYPE_GET_SCHEDULE": "true", - "TASK_TYPE_GET_GRADES": "true", - "TASK_TYPE_GET_EXAMS": "true", - "TASK_TYPE_LOGIN": "true", + "TASK_TYPE_GET_SCHEDULE": "true", + "TASK_TYPE_GET_GRADES": "true", + "TASK_TYPE_GET_EXAMS": "true", + "TASK_TYPE_LOGIN": "true", + "TASK_TYPE_GET_EMPTY_CLASSROOM": "true", }, }, }, diff --git a/grpc/handler.go b/grpc/handler.go index 425088b..27e5746 100644 --- a/grpc/handler.go +++ b/grpc/handler.go @@ -58,6 +58,8 @@ func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult { data, err = h.getExams(context.Background(), task.Payload) case pb.TaskType_TASK_TYPE_LOGIN: 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: result.Status = pb.TaskStatus_TASK_STATUS_FAILED result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type) @@ -383,3 +385,55 @@ func convertExams(exams []parser.ExamEntry) []*pb.Exam { } 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 +} diff --git a/parser/empty_classroom.go b/parser/empty_classroom.go new file mode 100644 index 0000000..8b5b01d --- /dev/null +++ b/parser/empty_classroom.go @@ -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, " ", "") + 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 +} \ No newline at end of file diff --git a/parser/encoding.go b/parser/encoding.go index 5c47cf8..85b56ed 100644 --- a/parser/encoding.go +++ b/parser/encoding.go @@ -2,6 +2,7 @@ package parser import ( "bytes" + "encoding/json" "io" "net/http" "strings" @@ -71,4 +72,12 @@ func isValidUTF8(data []byte) bool { } } 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) } \ No newline at end of file diff --git a/proto/runner/runner.pb.go b/proto/runner/runner.pb.go index 53e154a..dff09e9 100644 --- a/proto/runner/runner.pb.go +++ b/proto/runner/runner.pb.go @@ -25,12 +25,13 @@ const ( 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 // 获取用户信息 + 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 // 获取用户信息 + TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM TaskType = 6 // 获取空教室 ) // Enum value maps for TaskType. @@ -42,14 +43,16 @@ var ( 3: "TASK_TYPE_GET_GRADES", 4: "TASK_TYPE_GET_EXAMS", 5: "TASK_TYPE_GET_USER_INFO", + 6: "TASK_TYPE_GET_EMPTY_CLASSROOM", } 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, + "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, + "TASK_TYPE_GET_EMPTY_CLASSROOM": 6, } ) @@ -1711,6 +1714,237 @@ func (x *UserInfoResultData) GetGrade() string { 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 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" + "\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" + "\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" + + "\x17TASK_TYPE_GET_USER_INFO\x10\x05\x12!\n" + + "\x1dTASK_TYPE_GET_EMPTY_CLASSROOM\x10\x06*\x8a\x01\n" + "\n" + "TaskStatus\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_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 25) 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 - (*GpaSummary)(nil), // 17: runner.GpaSummary - (*GradesResultData)(nil), // 18: runner.GradesResultData - (*Grade)(nil), // 19: runner.Grade - (*ExamsResultData)(nil), // 20: runner.ExamsResultData - (*Exam)(nil), // 21: runner.Exam - (*UserInfoResultData)(nil), // 22: runner.UserInfoResultData - nil, // 23: runner.RegisterRequest.CapabilitiesEntry + (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 + (*GpaSummary)(nil), // 17: runner.GpaSummary + (*GradesResultData)(nil), // 18: runner.GradesResultData + (*Grade)(nil), // 19: runner.Grade + (*ExamsResultData)(nil), // 20: runner.ExamsResultData + (*Exam)(nil), // 21: runner.Exam + (*UserInfoResultData)(nil), // 22: runner.UserInfoResultData + (*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{ 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 9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult 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 1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus 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 17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary 21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam - 2, // 15: runner.RunnerHub.Connect:input_type -> runner.StreamMessage - 2, // 16: runner.RunnerHub.Connect:output_type -> runner.StreamMessage - 16, // [16:17] is the sub-list for method output_type - 15, // [15:16] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 25, // 15: runner.EmptyClassroomResultData.classrooms:type_name -> runner.EmptyClassroomEntry + 2, // 16: runner.RunnerHub.Connect:input_type -> runner.StreamMessage + 2, // 17: runner.RunnerHub.Connect:output_type -> runner.StreamMessage + 17, // [17:18] is the sub-list for method output_type + 16, // [16:17] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension 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() } @@ -1952,7 +2214,7 @@ func file_proto_runner_runner_proto_init() { 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: 22, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/runner/runner.proto b/proto/runner/runner.proto index 415d6d7..7e51a55 100644 --- a/proto/runner/runner.proto +++ b/proto/runner/runner.proto @@ -81,6 +81,7 @@ enum TaskType { TASK_TYPE_GET_GRADES = 3; // 获取成绩 TASK_TYPE_GET_EXAMS = 4; // 获取考试安排 TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息 + TASK_TYPE_GET_EMPTY_CLASSROOM = 6; // 获取空教室 } // TaskStatus 任务状态枚举 @@ -236,3 +237,30 @@ message UserInfoResultData { string class_name = 6; // 班级 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; // 周次范围 +} diff --git a/proto/runner/runner_grpc.pb.go b/proto/runner/runner_grpc.pb.go index 3adf82f..435ab41 100644 --- a/proto/runner/runner_grpc.pb.go +++ b/proto/runner/runner_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v7.34.1 // source: proto/runner/runner.proto