diff --git a/apps/vscode-extension/src/extension.ts b/apps/vscode-extension/src/extension.ts index 45cb98f..9040e55 100644 --- a/apps/vscode-extension/src/extension.ts +++ b/apps/vscode-extension/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { fetchAtCoderProblem, fetchAtCoderTasks } from "./atcoder"; -import { CfError, ProxyError, LoginRequiredError, setSessionCookie, fetchSubStatus } from "./tools/fetch"; +import { CfError, ProxyError, LoginRequiredError, setSessionCookie, fetchSubStatus, fetchSubmitHistory } from "./tools/fetch"; import { fetchContest, signedUpContest } from "./tools/SignUpContest"; import { translateTextRaw } from "./tools/deepl"; import { runCommand } from "./tools/command"; @@ -240,6 +240,18 @@ export async function handleSubmitCode( } } +export async function handleFetchSubHistory(contest: string, send: (payload: Record) => void) { + send({ type: "loading", text: `正在获取 ${contest} 提交记录...` }); + try { + const submissions = await fetchSubmitHistory(contest); + send({ type: "submissionHistory", submissions }); + } catch (error) { + if (!handleErrorWithCfAndLogin(error, send)) { + send({ type: "error", text: error instanceof Error ? error.message : "获取提交记录失败" }); + } + } +} + async function pullSubmitStatu(contest: string, taskName: string, send: (payload: Record) => void,): Promise { const maxSetp = 15; for (let i = 1; i <= maxSetp; i++) { diff --git a/apps/vscode-extension/src/tools/command.ts b/apps/vscode-extension/src/tools/command.ts index 2437c34..3b4b5fa 100644 --- a/apps/vscode-extension/src/tools/command.ts +++ b/apps/vscode-extension/src/tools/command.ts @@ -10,19 +10,16 @@ import { handleRegistration, handleFetchSubmitPage, handleSubmitCode, + handleFetchSubHistory, } from "../extension"; const loadCommands = new Set(["loadContest", "loadProblem", "openBrowser"]); const deeplCommands = new Set(["translate", "setApiKey"]); const cookieCommands = new Set(["getCookie", "setCookie"]); const problemCommands = new Set(["registerContest", "copyMarkdown", "alert"]); -const submitCommands = new Set(["fetchSubmitPage", "submitCode"]); +const submitCommands = new Set(["fetchSubmitPage", "submitCode", "fetchSubmissionHistory", "fetchSubmissionDetail"]); -export async function runCommand( - message: IncomingMessage, - context: vscode.ExtensionContext, - sendToWebview: (payload: Record) => void, -) { +export async function runCommand(message: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record) => void,) { if (loadCommands.has(message.command!)) await runLoadCommand(message, sendToWebview); else if (deeplCommands.has(message.command!)) await runDeepL(message, context, sendToWebview); else if (cookieCommands.has(message.command!)) await runCookie(message, context, sendToWebview); @@ -41,16 +38,16 @@ async function runSubmit(command: IncomingMessage, context: vscode.ExtensionCont if (!command.contest) return false; await handleSubmitCode(command.contest, command.taskScreenName, command.languageId, command.sourceCode, sendToWebview); return true; + case "fetchSubmissionHistory": + if (!command.contest) return false; + await handleFetchSubHistory(command.contest, sendToWebview); + return true; default: return false; } } -async function runProblem( - command: IncomingMessage, - context: vscode.ExtensionContext, - sendToWebview: (payload: Record) => void, -): Promise { +async function runProblem(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record) => void,): Promise { switch (command.command) { case "registerContest": if (!command.contest) return false; @@ -71,11 +68,7 @@ async function runProblem( } } -async function runCookie( - command: IncomingMessage, - context: vscode.ExtensionContext, - sendToWebview: (payload: Record) => void, -): Promise { +async function runCookie(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record) => void,): Promise { switch (command.command) { case "getCookie": await handleGetCookie(context, sendToWebview); @@ -88,11 +81,7 @@ async function runCookie( } } -async function runDeepL( - command: IncomingMessage, - context: vscode.ExtensionContext, - sendToWebview: (payload: Record) => void, -): Promise { +async function runDeepL(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record) => void,): Promise { switch (command.command) { case "translate": await handleTranslate(command.payload, command.targetLang, context, sendToWebview); @@ -108,10 +97,7 @@ async function runDeepL( } } -async function runLoadCommand( - command: IncomingMessage, - sendToWebview: (payload: Record) => void, -): Promise { +async function runLoadCommand(command: IncomingMessage, sendToWebview: (payload: Record) => void,): Promise { switch (command.command) { case "loadContest": if (!command.contest) return false; diff --git a/apps/vscode-extension/src/tools/fetch.ts b/apps/vscode-extension/src/tools/fetch.ts index be27df1..a47ab71 100644 --- a/apps/vscode-extension/src/tools/fetch.ts +++ b/apps/vscode-extension/src/tools/fetch.ts @@ -4,6 +4,7 @@ import * as stream from "stream"; import * as zlib from "zlib"; import * as net from "net"; import * as tls from "tls"; +import { SubRecord } from "./types" export class CfError extends Error { @@ -279,7 +280,7 @@ function handleResponse( } export function fetchText(url: string): Promise { - const logPrefix = `[fetchText]`; + const logPrefix = `[fetchText]`; const savedProxy = saveProxyEnv(); @@ -368,4 +369,40 @@ export async function fetchSubStatus(contest: string): Promise { + const html = await fetchText(`https://atcoder.jp/contests/${contest}/submissions/me`); + const records: SubRecord[] = []; + const reg = /]*>([\s\S]*?)<\/tr>/gi; + let rowMatch: RegExpExecArray | null; + for (; (rowMatch = reg.exec(html)) !== null;) { + const rowHtml = rowMatch[1]; + const timeMatch = rowHtml.match(/]*class="text-center"[^>]*>([\s\S]*?)<\/td>/i); + if (!timeMatch) continue; + const time = timeMatch[1].trim().replace(/<[^>]+>/g, ""); + + const taskLinkMatch = rowHtml.match(/href="\/contests\/[^/]+\/tasks\/([^"#?]+)"[^>]*>([^<]+)]*class="text-center"[^>]*>[\s\S]*?<\/td>\s*]*>([^<]*)<\/td>/i); + const language = langMatch ? langMatch[1].trim() : ""; + + const scoreMatch = rowHtml.match(/]*class="text-right"[^>]*>\s*(\d+)\s*<\/td>/i); + const score = scoreMatch ? scoreMatch[1] : "0"; + + const statusMatch = rowHtml.match(/]*class=(["'])[^"']*\blabel\b[^"']*\1[^>]*>\s*([^<]+)\s*<\/span>/i); + if (!statusMatch) continue; + const status = statusMatch[2].trim(); + + const detailMatch = rowHtml.match(/]*href="\/contests\/[^/]+\/submissions\/(\d+)"[^>]*>/i); + if (!detailMatch) continue; + const id = detailMatch[1]; + + records.push({ id, time, task, taskScreenName, language, score, status }); + } + return records; +} + diff --git a/apps/vscode-extension/src/tools/types.ts b/apps/vscode-extension/src/tools/types.ts index ea83f41..965a9b8 100644 --- a/apps/vscode-extension/src/tools/types.ts +++ b/apps/vscode-extension/src/tools/types.ts @@ -15,9 +15,20 @@ export interface IncomingMessage { problem?: AtCoderProblem; } +export interface SubRecord { + id: string; + time: string; + task: string; + taskScreenName: string; // 题目标识 + language: string; // 编程语言 + score: string; // 分数 + status: string; +} + export interface SubStatus { label: string; value: string; url: string; status?: string; -} \ No newline at end of file +} + diff --git a/packages/webview/src/WebviewApp.tsx b/packages/webview/src/WebviewApp.tsx index 20007ae..bb11e5a 100644 --- a/packages/webview/src/WebviewApp.tsx +++ b/packages/webview/src/WebviewApp.tsx @@ -39,6 +39,9 @@ const WebviewApp: React.FC = ({ const [sourceCode, setSourceCode] = React.useState(""); const [submitResult, setSubmitResult] = React.useState(null); const [copiedSample, setCopiedSample] = React.useState>({}); + const [showSubmissionHistory, setShowSubmissionHistory] = React.useState(false); + const [submissionHistory, setSubmissionHistory] = React.useState>([]); + const [loadingHistory, setLoadingHistory] = React.useState(false); const loadContest = async (nextContest: string) => { setIsLoading(true); @@ -92,6 +95,12 @@ const WebviewApp: React.FC = ({ setTimeout(() => setCopiedSample(prev => ({ ...prev, [key]: false })), 1500); }; + const handleFetchSubmissionHistory = () => { + setLoadingHistory(true); + setStatus("正在获取提交记录..."); + vscode.postMessage({ command: "fetchSubmissionHistory", contest } as unknown as WebviewMessage); + }; + const handleSubmitCode = () => { if (!selectedSubmitTask || !selectedSubmitLanguage || !sourceCode.trim()) return; setSubmitResult(null); @@ -199,6 +208,13 @@ const WebviewApp: React.FC = ({ const statuses = message.statuses ?? {}; setTasks(prev => prev.map(t => ({ ...t, status: statuses[t.value] }))); } + if (message.type === "submissionHistory") { + const m = message as any; + setSubmissionHistory(m.submissions ?? []); + setLoadingHistory(false); + setStatus(`已获取 ${(m.submissions ?? []).length} 条提交记录`); + setShowSubmissionHistory(true); + } }; window.addEventListener("message", handleMessage); @@ -335,6 +351,22 @@ const WebviewApp: React.FC = ({ > 提交代码 + {Rated && (