Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"commands": [
{
"command": "extension.showWebview",
"title": "AtCoder Helper(编辑器)"
"title": "AtCoder Helper (Editor)"
},
{
"command": "extension.setDeeplApiKey",
Expand Down
3 changes: 3 additions & 0 deletions apps/vscode-extension/package.nls.zh-cn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extension.showWebview": "AtCoder Helper(编辑器)"
}
30 changes: 16 additions & 14 deletions apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { runCommand } from "./tools/command";
import { IncomingMessage } from "./tools/types";
import { getWebviewContent } from "./tools/webview";
import { AtCoderViewProvider } from "./viewProvider";
import { init, t } from "./tools/i18n";

let sidebarViewProvider: AtCoderViewProvider | undefined;

Expand Down Expand Up @@ -39,61 +40,61 @@ export async function pullSubmitStatu(contest: string, taskName: string, send: (
send({ type: "statusUpdate", statuses: Object.fromEntries(statusMap) });
const status = statusMap.get(taskName);
if (status && judgeStatus.has(status)) {
send({ type: "update", text: `评测结果: ${status}` });
send({ type: "update", text: t("ext.judgeResult", { status }) });
return;
}
} catch {
//单次轮询失败,直接下一次
}
}
send({ type: "update", text: "评测超时,请稍后手动刷新查看结果" });
send({ type: "update", text: t("ext.judgeTimeout") });
}

function registerSetDeeplApiKey(context: vscode.ExtensionContext) {
return vscode.commands.registerCommand("extension.setDeeplApiKey", async () => {
const key = await vscode.window.showInputBox({
prompt: "请输入 DeepL API Key",
prompt: t("ext.promptDeeplKey"),
password: true,
placeHolder: "例如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx",
placeHolder: t("ext.placeholderDeeplKey"),
ignoreFocusOut: true,
});
if (key?.trim()) {
await context.secrets.store("deeplApiKey", key.trim());
vscode.window.showInformationMessage("DeepL API Key 已保存");
vscode.window.showInformationMessage(t("ext.deeplKeySaved"));
}
});
}

function registerSetAtCoderCookie(context: vscode.ExtensionContext) {
return vscode.commands.registerCommand("extension.setAtCoderCookie", async () => {
const cookie = await vscode.window.showInputBox({
prompt: "粘贴 AtCoder 的 Cookie(仅需 REVEL_SESSION)",
prompt: t("ext.promptCookie"),
password: true,
placeHolder: "REVEL_SESSION=abcdef1234567890abcdef1234567890",
placeHolder: t("ext.placeholderCookie"),
ignoreFocusOut: true,
});
if (!cookie?.trim()) return;
const trimmed = cookie.trim();
if (!trimmed.startsWith("REVEL_SESSION=")) {
const fix = `REVEL_SESSION=${trimmed}`;
const choice = await vscode.window.showWarningMessage(
`Cookie 格式似乎不正确,是否添加 REVEL_SESSION= 前缀?`,
t("ext.cookieFormatWarn"),
{ modal: false },
"自动修复",
"取消"
t("ext.autoFix"),
t("ext.cancel")
);
if (choice === "自动修复") {
if (choice === t("ext.autoFix")) {
await context.secrets.store("atcoderCookie", fix);
setSessionCookie(fix);
notifyCookieChanged(true);
vscode.window.showInformationMessage("AtCoder Cookie 已保存并自动修复格式");
vscode.window.showInformationMessage(t("ext.cookieSavedFixed"));
}
return;
}
await context.secrets.store("atcoderCookie", trimmed);
setSessionCookie(trimmed);
notifyCookieChanged(true);
vscode.window.showInformationMessage("AtCoder Cookie 已保存");
vscode.window.showInformationMessage(t("ext.cookieSaved"));
});
}

Expand Down Expand Up @@ -168,7 +169,7 @@ export function openContestPanel(context: vscode.ExtensionContext, contest: stri
export function openSubmissionPanel(context: vscode.ExtensionContext, contest: string, id: string) {
const panel = vscode.window.createWebviewPanel(
"atcoderSubmission",
`提交 ${id} - ${contest}`,
t("ext.submissionPanelTitle", { id, contest }),
vscode.ViewColumn.One,
{
enableScripts: true,
Expand Down Expand Up @@ -201,6 +202,7 @@ export function openSubmissionPanel(context: vscode.ExtensionContext, contest: s

export async function activate(context: vscode.ExtensionContext) {
log.info("Extension is now active!");
init(vscode.env.language);

setStaleCookieHandler(() => {
// vscode.window.showWarningMessage(
Expand Down
17 changes: 9 additions & 8 deletions apps/vscode-extension/src/tools/SignUpContest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchText, fetchTextPost, CfError, LoginRequiredError, ProxyError } from "./fetch";
import { t } from "./i18n";

export interface ContestPage {
contest: string;
Expand Down Expand Up @@ -113,20 +114,20 @@ function buildFormBody(fields: FormField[]): string {

function parseRegistrationResult(html: string): RegistrationResult {
const isSigned = /Unregister|registered/i.test(html);
if (isSigned) return { success: true, message: "报名成功!" };
if (isSigned) return { success: true, message: t("register.success") };
const successMatch = html.match(
/<div[^>]*class="[^"]*alert-success[^"]*"[^>]*>([\s\S]*?)<\/div>/i
);
if (successMatch) {
const msg = successMatch[1].replace(/<[^>]+>/g, "").trim();
return { success: true, message: msg || "报名成功!" };
return { success: true, message: msg || t("register.success") };
}
const errMatch = html.match(
/<div[^>]*class="[^"]*(?:alert-danger|alert-error)[^"]*"[^>]*>([\s\S]*?)<\/div>/i
);
if (errMatch) {
const msg = errMatch[1].replace(/<[^>]+>/g, "").trim();
return { success: false, message: msg || "报名失败" };
return { success: false, message: msg || t("register.failed") };
}
return { success: false, message: "" };
}
Expand All @@ -151,7 +152,7 @@ async function completeRatedRegistration(
`<form[^>]*action="[^"]*${contest}\\/rated_register"[^>]*>([\\s\\S]*?)<\\/form>`,
"i"
);
const fallback: RegistrationResult = { success: false, message: "报名失败,请检查 Cookie 是否有效" };
const fallback: RegistrationResult = { success: false, message: t("register.failedBadCookie") };
const step2Form = stepHtml.match(now)?.[1];
if (!step2Form) {
const result = parseRegistrationResult(stepHtml);
Expand Down Expand Up @@ -185,7 +186,7 @@ async function registerFormBased(contest: string, formHtml: string, rated: boole
if (result.message) return result;
return {
success: false,
message: "报名未成功:注册页返回校验结果,请确认表单必填信息(如姓名、邮箱、居住地等)填写完整",
message: t("register.formIncomplete"),
};
}
return await completeRatedRegistration(contest, responseHtml, rated);
Expand All @@ -196,11 +197,11 @@ export async function signedUpContest(contest: string, csrfToken: string, rated?
try {
const registerHtml = await fetchText(registerUrl);
if (/Unregister|already registered/i.test(registerHtml)) {
return { success: true, message: "已报名" };
return { success: true, message: t("register.alreadyDone") };
}
const formHtml = extractFormHtml(registerHtml, contest);
if (!formHtml) {
return { success: false, message: "报名已截止或无法获取报名信息" };
return { success: false, message: t("register.closed") };
}
const freshCsrfMatch = formHtml.match(/name="csrf_token"[^>]*value="([^"]*)"/i);
const freshCsrf = freshCsrfMatch ? freshCsrfMatch[1] : csrfToken;
Expand All @@ -212,6 +213,6 @@ export async function signedUpContest(contest: string, csrfToken: string, rated?
if (error instanceof CfError || error instanceof LoginRequiredError || error instanceof ProxyError) {
throw error;
}
return { success: false, message: error instanceof Error ? error.message : "报名请求失败" };
return { success: false, message: error instanceof Error ? error.message : t("register.requestFailed") };
}
}
7 changes: 4 additions & 3 deletions apps/vscode-extension/src/tools/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as vscode from "vscode";
import { copyMarkdown } from "./copy";
import { IncomingMessage } from "./types";
import { openContestPanel, openSubmissionPanel } from "../extension"
import { t } from "./i18n";
import {
handleContestLoad,
handleProblemLoad,
Expand Down Expand Up @@ -32,7 +33,7 @@ export async function runCommand(message: IncomingMessage, context: vscode.Exten
else if (problemCommands.has(message.command!)) await runProblem(message, context, sendToWebview);
else if (submitCommands.has(message.command!)) await runSubmit(message, context, sendToWebview);
else if (contestCommands.has(message.command!)) await runContest(message, context, sendToWebview);
else throw new Error("unknown command");
else throw new Error(t("cmd.unknown"));
}

async function runContest(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
Expand Down Expand Up @@ -89,7 +90,7 @@ async function runProblem(command: IncomingMessage, context: vscode.ExtensionCon
case "copyMarkdown":
if (command.problem) {
await vscode.env.clipboard.writeText(copyMarkdown(command.problem));
sendToWebview({ type: "update", text: "已复制到剪贴板" });
sendToWebview({ type: "update", text: t("cmd.copied") });
}
return true;
case "alert":
Expand Down Expand Up @@ -127,7 +128,7 @@ async function runDeepL(command: IncomingMessage, context: vscode.ExtensionConte
case "setApiKey":
if (command.text?.trim()) {
await context.secrets.store("deeplApiKey", command.text.trim());
vscode.window.showInformationMessage("DeepL API Key 已保存");
vscode.window.showInformationMessage(t("ext.deeplKeySaved"));
}
return true;
default:
Expand Down
15 changes: 8 additions & 7 deletions apps/vscode-extension/src/tools/copy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AtCoderProblem } from "../atcoder";
import { t } from "./i18n";

function decodeEntities(text: string): string {
return text
Expand Down Expand Up @@ -57,36 +58,36 @@ export function copyMarkdown(problem: AtCoderProblem): string {
parts.push("");

if (problem.statement) {
parts.push("### 题目描述");
parts.push(t("md.statement"));
parts.push(htmlToText(problem.statement));
parts.push("");
}

if (problem.constraints) {
parts.push("### 约束");
parts.push(t("md.constraints"));
parts.push(htmlToText(problem.constraints));
parts.push("");
}

if (problem.inputFormat) {
parts.push("### 输入格式");
parts.push(t("md.inputFormat"));
parts.push(htmlToText(problem.inputFormat));
parts.push("");
}

if (problem.outputFormat) {
parts.push("### 输出格式");
parts.push(t("md.outputFormat"));
parts.push(htmlToText(problem.outputFormat));
parts.push("");
}

if (problem.samples && problem.samples.length > 0) {
for (const sample of problem.samples) {
parts.push(`### 样例 ${sample.index}`);
parts.push("输入");
parts.push(t("md.sample", { index: sample.index }));
parts.push(t("md.inputLabel"));
parts.push("```\n" + sample.input + "\n```");
parts.push("");
parts.push("输出");
parts.push(t("md.outputLabel"));
parts.push("```\n" + sample.output + "\n```");
parts.push("");
}
Expand Down
10 changes: 4 additions & 6 deletions apps/vscode-extension/src/tools/cph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as http from "http";
import { AtCoderProblem } from "../atcoder";
import { t } from "./i18n";

export interface CphTestCase {
input: string;
Expand All @@ -21,10 +22,7 @@ export interface CphProblem {

export class CphNotRunningError extends Error {
constructor() {
super(
"未检测到 CPH 插件(localhost:27121 无响应)。\n" +
"请安装并启用 Competitive Programming Helper 扩展后重试。"
);
super(t("cph.notRunning"));
this.name = "CphNotRunningError";
}
}
Expand Down Expand Up @@ -66,7 +64,7 @@ export function sendToCph(problem: CphProblem): Promise<void> {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve();
} else {
reject(new Error(`CPH 返回状态码 ${res.statusCode}`));
reject(new Error(t("cph.httpError", { status: res.statusCode })));
}
});
}
Expand All @@ -75,7 +73,7 @@ export function sendToCph(problem: CphProblem): Promise<void> {
if ((err as NodeJS.ErrnoException).code === "ECONNREFUSED") {
reject(new CphNotRunningError());
} else {
reject(new Error(`连接 CPH 失败: ${err.message}`));
reject(new Error(t("cph.connectionFailed", { msg: err.message })));
}
});
req.write(body);
Expand Down
17 changes: 9 additions & 8 deletions apps/vscode-extension/src/tools/deepl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as https from "https";
import { t } from "./i18n";

let freeDeeplID = 1;

Expand Down Expand Up @@ -56,27 +57,27 @@ export async function translateTextFree(text: string, lang: string): Promise<str
res.on("end", () => {
settle(() => {
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(res.statusCode === 429 ? "翻译请求过于频繁,请稍后再试" : `翻译接口错误 (${res.statusCode})`));
reject(new Error(res.statusCode === 429 ? t("deepl.tooFrequent") : t("deepl.httpError", { status: res.statusCode })));
return;
}
try {
const json = JSON.parse(data);
if (json?.result?.texts?.[0]?.text) {
resolve(json.result.texts[0].text);
} else {
reject(new Error("翻译接口返回异常"));
reject(new Error(t("deepl.badResponse")));
}
} catch {
reject(new Error("翻译接口返回异常"));
reject(new Error(t("deepl.badResponse")));
}
});
});
}
);

req.setTimeout(20000, () => req.destroy(new Error("翻译请求超时")));
req.setTimeout(20000, () => req.destroy(new Error(t("deepl.timeout"))));
req.on("error", (err: Error) =>
settle(() => reject(new Error(err.message === "翻译请求超时" ? "翻译请求超时" : `翻译请求失败: ${err.message}`)))
settle(() => reject(new Error(err.message === t("deepl.timeout") ? t("deepl.timeout") : t("deepl.failed", { msg: err.message }))))
);
req.write(postData);
req.end();
Expand Down Expand Up @@ -104,17 +105,17 @@ export function translateTextRaw(text: string, targetLang: string, apiKey: strin
try {
const json = JSON.parse(data);
if (res.statusCode && res.statusCode >= 400) {
reject(new Error(json.message || `翻译接口错误 (${res.statusCode})`));
reject(new Error(json.message || t("deepl.httpError", { status: res.statusCode })));
return;
}
resolve(json.translations?.[0]?.text ?? text);
} catch {
reject(new Error("翻译接口返回异常"));
reject(new Error(t("deepl.badResponse")));
}
});
}
);
req.on("error", () => reject(new Error("翻译请求失败")));
req.on("error", () => reject(new Error(t("deepl.failedSimple"))));
req.write(params.toString());
req.end();
});
Expand Down
Loading
Loading