Files
wk-frontend/src/service/wk.ts
zhilv 58555c5043 feat(release): bump version to 0.1.2
## 详细信息
- 升级项目版本号到 0.1.2
- 增强刷课稳定性(失败重试、心跳检测、状态自动纠正)
- 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选
- 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转
2026-04-02 22:33:04 +08:00

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");
};