## 详细信息 - 升级项目版本号到 0.1.2 - 增强刷课稳定性(失败重试、心跳检测、状态自动纠正) - 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选 - 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转
333 lines
8.3 KiB
TypeScript
333 lines
8.3 KiB
TypeScript
import type { Accessor } from "solid-js";
|
|
import http, { createHttpClient, type HttpClient } from "~/service/http";
|
|
import type { CourseType } from "~/types/Course";
|
|
import type { userInfoType } from "~/types/Userinfo";
|
|
|
|
export type CourseKind = "run" | "finish" | "sign";
|
|
|
|
export type RecordType = "" | "/work" | "/exam" | "/discuss";
|
|
|
|
export type StudyStatus = 1 | 2 | 3;
|
|
|
|
type ApiResponse<T> = {
|
|
code: number;
|
|
message: string;
|
|
data: T;
|
|
};
|
|
|
|
export type LoginReq = {
|
|
username: string;
|
|
password: string;
|
|
token: string;
|
|
status: CourseKind;
|
|
host: string;
|
|
};
|
|
|
|
export type LoginData = {
|
|
courses?: CourseType[];
|
|
session_id: string;
|
|
user: userInfoType;
|
|
};
|
|
|
|
export type LoginRes = ApiResponse<LoginData>;
|
|
|
|
export type RecordItem = {
|
|
id: string;
|
|
name: string;
|
|
type: string | null;
|
|
chapterId: string;
|
|
courseId: string;
|
|
videoFile: string;
|
|
videoDuration: string;
|
|
votingPath: string | null;
|
|
tabVideo: string;
|
|
tabFile: string;
|
|
tabVote: string;
|
|
tabWork: string;
|
|
tabExam: string;
|
|
sort: string;
|
|
videoMode: string;
|
|
localFile: string;
|
|
schoolId: string;
|
|
lock: string;
|
|
unlockTime: string;
|
|
bid: string;
|
|
duration: string;
|
|
progress: string;
|
|
state: string;
|
|
viewCount: string;
|
|
finalTime: string;
|
|
error: number;
|
|
errorMessage: string;
|
|
beginTime: string;
|
|
viewedDuration: string;
|
|
url: string;
|
|
};
|
|
|
|
export type PageInfo = {
|
|
keyName: string;
|
|
page: number;
|
|
pageCount: number;
|
|
recordsCount: number;
|
|
onlyCount: number;
|
|
pageSize: number;
|
|
};
|
|
|
|
export type RecordData = {
|
|
list: RecordItem[];
|
|
page_info: PageInfo;
|
|
};
|
|
|
|
export type RecordRes = ApiResponse<RecordData>;
|
|
|
|
export type StudyData = {
|
|
state: number;
|
|
studyId: number;
|
|
status: boolean;
|
|
msg: string;
|
|
};
|
|
|
|
export type StudyRes = ApiResponse<StudyData>;
|
|
|
|
export type LogoutRes = {
|
|
code: number;
|
|
message: string;
|
|
};
|
|
|
|
export type HostItem = {
|
|
host: string;
|
|
name: string;
|
|
};
|
|
|
|
export type HostRes = ApiResponse<{
|
|
list: HostItem[];
|
|
}>;
|
|
|
|
export type VersionData = {
|
|
BuildAt: string;
|
|
GitAuthor: string;
|
|
GitCommit: string;
|
|
GitEmail: string;
|
|
Version: string;
|
|
};
|
|
|
|
export type VersionRes = ApiResponse<VersionData>;
|
|
|
|
export type RecordReq = {
|
|
course_id: string;
|
|
page: number;
|
|
record_type?: RecordType;
|
|
};
|
|
|
|
export type CourseReq = {
|
|
status: CourseKind;
|
|
};
|
|
|
|
export type CourseData = {
|
|
courses: CourseType[];
|
|
};
|
|
|
|
export type CourseRes = ApiResponse<CourseData>;
|
|
|
|
export type StudyReq = {
|
|
node_id: string;
|
|
study_id: string;
|
|
study_time: string;
|
|
status: StudyStatus;
|
|
};
|
|
|
|
export type StudyRunnerItem = {
|
|
nodeId: string;
|
|
name: string;
|
|
currentTime: number;
|
|
totalTime: number;
|
|
progress: string;
|
|
completed: boolean;
|
|
};
|
|
|
|
export type StudyRunnerPayload = {
|
|
accountId: string;
|
|
courseId: number;
|
|
intervalSeconds: number;
|
|
items: StudyRunnerItem[];
|
|
isRunningStudy: Accessor<boolean>;
|
|
setIsRunningStudy: () => void;
|
|
client: WkClient;
|
|
onLog?: (message: string, accoundID: string) => void;
|
|
};
|
|
|
|
export type WkClient = {
|
|
courseApi: (payload: CourseReq) => Promise<CourseRes>;
|
|
recordApi: (payload: RecordReq) => Promise<RecordRes>;
|
|
studyApi: (payload: StudyReq) => Promise<StudyRes>;
|
|
logoutApi: () => Promise<LogoutRes>;
|
|
};
|
|
|
|
export const loginApi = async (payload: LoginReq) => {
|
|
const res = await http.post<LoginRes>("/api/login", payload);
|
|
return res;
|
|
};
|
|
|
|
const createWkClientFromHttp = (client: HttpClient): WkClient => ({
|
|
courseApi(payload) {
|
|
return client.post<CourseRes>("/api/v2/course", payload);
|
|
},
|
|
recordApi(payload) {
|
|
return client.post<RecordRes>("/api/v2/record", payload);
|
|
},
|
|
studyApi(payload) {
|
|
return client.post<StudyRes>("/api/v2/study", payload);
|
|
},
|
|
logoutApi() {
|
|
return client.post<LogoutRes>("/api/v2/logout");
|
|
},
|
|
});
|
|
|
|
export const createWkClient = (
|
|
resolveSessionId: () => string | undefined,
|
|
): WkClient => {
|
|
return createWkClientFromHttp(createHttpClient(resolveSessionId));
|
|
};
|
|
|
|
const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
|
|
|
export const runStudyQueue = async (_payload: StudyRunnerPayload) => {
|
|
const stopFlag = _payload.isRunningStudy;
|
|
const stepSeconds = Math.max(1, Math.floor(_payload.intervalSeconds || 5));
|
|
const sleepMs = stepSeconds * 1000;
|
|
const maxRetryCount = 3;
|
|
const maxStudySubmitRetry = 3;
|
|
const studySubmitRetrySleepMs = 5000;
|
|
|
|
for (const item of _payload.items) {
|
|
const learnedTime = Math.max(0, Math.floor(item.currentTime || 0));
|
|
const rawTotalTime = Math.max(0, Math.floor(item.totalTime || 0));
|
|
const totalTime = Math.max(0, rawTotalTime - learnedTime);
|
|
let currentTime = 0;
|
|
let count = 0;
|
|
let study_id = 0;
|
|
let retryCount = 0;
|
|
|
|
if (totalTime <= 0) {
|
|
_payload.onLog?.(`跳过已完成节点: ${item.name}`, _payload.accountId);
|
|
continue;
|
|
}
|
|
|
|
while (currentTime <= totalTime) {
|
|
if (!stopFlag()) {
|
|
_payload.onLog?.("⛔ 已手动停止", _payload.accountId);
|
|
return;
|
|
}
|
|
const message = `[${item.name}]: ${currentTime}/${totalTime}`;
|
|
_payload.onLog?.(message, _payload.accountId);
|
|
|
|
try {
|
|
const resp = await _payload.client.studyApi({
|
|
node_id: item.nodeId,
|
|
study_id: String(study_id),
|
|
study_time: String(currentTime),
|
|
status: count === 0 ? 1 : currentTime >= totalTime ? 3 : 2,
|
|
});
|
|
const submitMessage = String(resp.data.msg ?? "");
|
|
const isSubmitFailed =
|
|
submitMessage.includes("提交学时失败") || resp.data.status === false;
|
|
|
|
if (isSubmitFailed) {
|
|
let submitRetry = 0;
|
|
let fixedResp = resp;
|
|
|
|
while (submitRetry < maxStudySubmitRetry) {
|
|
submitRetry += 1;
|
|
_payload.onLog?.(
|
|
`⚠️ 提交学时失败,${submitRetry}/${maxStudySubmitRetry} 次重试后再提交: ${item.name}`,
|
|
_payload.accountId,
|
|
);
|
|
await sleep(studySubmitRetrySleepMs);
|
|
|
|
if (!stopFlag()) {
|
|
_payload.onLog?.("⛔ 已手动停止", _payload.accountId);
|
|
return;
|
|
}
|
|
|
|
fixedResp = await _payload.client.studyApi({
|
|
node_id: item.nodeId,
|
|
study_id: String(study_id),
|
|
study_time: String(currentTime),
|
|
status: count === 0 ? 1 : currentTime >= totalTime ? 3 : 2,
|
|
});
|
|
|
|
const nextMsg = String(fixedResp.data.msg ?? "");
|
|
const nextFailed =
|
|
nextMsg.includes("提交学时失败") || fixedResp.data.status === false;
|
|
if (!nextFailed) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const finalMsg = String(fixedResp.data.msg ?? "");
|
|
const stillFailed =
|
|
finalMsg.includes("提交学时失败") || fixedResp.data.status === false;
|
|
if (stillFailed) {
|
|
_payload.onLog?.(
|
|
`⛔ 提交学时连续重试 ${maxStudySubmitRetry} 次仍失败,停止刷课: ${item.name}`,
|
|
_payload.accountId,
|
|
);
|
|
_payload.setIsRunningStudy();
|
|
return;
|
|
}
|
|
|
|
study_id = fixedResp.data.studyId;
|
|
retryCount = 0;
|
|
|
|
if (currentTime === totalTime) {
|
|
break;
|
|
}
|
|
|
|
currentTime = Math.min(currentTime + stepSeconds, totalTime);
|
|
count++;
|
|
await sleep(sleepMs);
|
|
continue;
|
|
}
|
|
|
|
if (resp.data.state != 0) {
|
|
_payload.onLog?.(`⛔ 自动停止: ${resp.data.msg}`, _payload.accountId);
|
|
_payload.setIsRunningStudy();
|
|
return;
|
|
}
|
|
|
|
study_id = resp.data.studyId;
|
|
retryCount = 0;
|
|
|
|
if (currentTime === totalTime) {
|
|
break;
|
|
}
|
|
|
|
currentTime = Math.min(currentTime + stepSeconds, totalTime);
|
|
count++;
|
|
} catch (error) {
|
|
retryCount += 1;
|
|
const errorMessage = `请求失败: ${error instanceof Error ? error.message : String(error)}`;
|
|
_payload.onLog?.(errorMessage, _payload.accountId);
|
|
|
|
if (retryCount >= maxRetryCount) {
|
|
_payload.onLog?.(
|
|
`⚠️ 连续失败 ${maxRetryCount} 次,跳过节点: ${item.name}`,
|
|
_payload.accountId,
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
await sleep(sleepMs);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const hostApi = async () => {
|
|
return await http.get<HostRes>("/api/v1/host");
|
|
};
|
|
|
|
export const versionApi = async () => {
|
|
return await http.get<VersionRes>("/api/version");
|
|
};
|