"""
CANNJudge 算子提交辅助工具
提供命令行接口完成 CANNJudge 算子竞赛的完整流程。
"""
import argparse
import requests
import json
import time
import os
import zipfile
import tempfile
import base64
from pathlib import Path
try:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
def decrypt_password(ciphertext_b64: str, private_key_path: str = "private.pem") -> str:
"""用 RSA 私钥解密密码密文。返回的明文密码仅供内部调用登录 API,禁止输出到对话/日志/文件。"""
if not HAS_CRYPTO:
raise ImportError("需要 pycryptodome 库: pip install pycryptodome")
with open(private_key_path, "r") as f:
private_key_pem = f.read()
rsa_key = RSA.importKey(private_key_pem)
cipher = PKCS1_v1_5.new(rsa_key)
ciphertext = base64.b64decode(ciphertext_b64)
plaintext = cipher.decrypt(ciphertext, sentinel=None)
return plaintext.decode()
class CANNJudgeClient:
"""CANNJudge API 客户端"""
BASE_URL = "https://cannjudge.cn"
def __init__(self):
self.session = requests.Session()
self.user_id = None
self.user_info = None
def login(self, email: str, password: str) -> dict:
"""登录 CANNJudge(password 为明文密码)"""
resp = self.session.post(
f"{self.BASE_URL}/api/users/login",
json={"email": email, "password": password},
timeout=30
)
resp.raise_for_status()
self.user_info = resp.json()
self.user_id = self.user_info["_id"]
return self.user_info
def login_with_ciphertext(self, email: str, ciphertext: str,
private_key_path: str = "private.pem") -> dict:
"""登录 CANNJudge(ciphertext 为 RSA 加密后的密码密文,服务器用私钥解密)。
解密后的明文密码仅供内部调用登录 API,禁止输出到对话/日志/文件。"""
password = decrypt_password(ciphertext, private_key_path)
result = self.login(email, password)
del password
return result
def get_problem(self, problem_id: str) -> dict:
"""获取题目信息"""
resp = self.session.get(
f"{self.BASE_URL}/api/problems/{problem_id}",
timeout=30
)
resp.raise_for_status()
return resp.json()
def download_package(self, problem_id: str, output_dir: str = None) -> str:
"""下载工程模板"""
resp = self.session.get(
f"{self.BASE_URL}/api/problems/{problem_id}/package",
params={"userId": self.user_id},
timeout=60
)
resp.raise_for_status()
if output_dir is None:
output_dir = tempfile.mkdtemp()
zip_path = os.path.join(output_dir, "project.zip")
with open(zip_path, "wb") as f:
f.write(resp.content)
extract_dir = os.path.join(output_dir, "project")
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(extract_dir)
return extract_dir
def submit(self, problem_id: str,
tiling_h: str, tiling_key_h: str,
host_cpp: str, kernel_cpp: str) -> str:
"""提交代码"""
payload = {
"problemId": problem_id,
"userId": self.user_id,
"tiling_h": tiling_h,
"tiling_key_h": tiling_key_h,
"host_cpp": host_cpp,
"kernel_cpp": kernel_cpp
}
resp = self.session.post(
f"{self.BASE_URL}/api/submissions/submit",
json=payload,
timeout=30
)
resp.raise_for_status()
result = resp.json()
return result["data"]["submissionId"]
def get_submission(self, submission_id: str) -> dict:
"""查询提交结果"""
resp = self.session.get(
f"{self.BASE_URL}/api/submissions/{submission_id}",
timeout=30
)
resp.raise_for_status()
return resp.json()
def wait_for_result(self, submission_id: str,
max_wait: int = 120, interval: int = 3) -> dict:
"""等待提交完成并返回结果"""
for i in range(max_wait // interval):
time.sleep(interval)
result = self.get_submission(submission_id)
status = result.get("status")
if status in ["Accepted", "Wrong Answer",
"Compile Error", "Runtime Error",
"Time Limit Exceeded"]:
return result
return self.get_submission(submission_id)
def get_rankings(self, problem_id: str) -> list:
"""获取排行榜"""
resp = self.session.get(
f"{self.BASE_URL}/api/submissions/problem/{problem_id}/latest",
timeout=30
)
resp.raise_for_status()
return resp.json()
def print_submission_result(result: dict):
"""打印提交结果"""
status = result.get("status", "Unknown")
print(f"\n{'='*60}")
print(f"状态: {status}")
print(f"{'='*60}")
if "result" in result:
print("\n测试用例结果:")
for idx, tc in enumerate(result["result"]):
tc_status = tc.get("testcase_status", "Unknown")
precision = tc.get("precision_ratio", "N/A")
time_ms = tc.get("time", "N/A")
print(f" Case {idx+1}: {tc_status}")
print(f" precision: {precision}, time: {time_ms}ms")
def main():
parser = argparse.ArgumentParser(
description="CANNJudge 算子提交辅助工具"
)
subparsers = parser.add_subparsers(dest="command", help="命令")
login_parser = subparsers.add_parser("login", help="登录 CANNJudge")
login_parser.add_argument("--email", required=True, help="登录邮箱")
login_parser.add_argument("--password", help="明文密码(不推荐,优先使用 --ciphertext)")
login_parser.add_argument("--ciphertext", help="RSA 加密后的密码密文(推荐)")
login_parser.add_argument("--private-key", default="private.pem", help="RSA 私钥路径")
download_parser = subparsers.add_parser("download", help="下载工程模板")
download_parser.add_argument("--problem-id", required=True, help="题目ID")
download_parser.add_argument("--output", help="输出目录")
submit_parser = subparsers.add_parser("submit", help="提交代码")
submit_parser.add_argument("--problem-id", required=True, help="题目ID")
submit_parser.add_argument("--project-dir", required=True, help="工程目录")
query_parser = subparsers.add_parser("query", help="查询提交结果")
query_parser.add_argument("--submission-id", required=True, help="提交ID")
rank_parser = subparsers.add_parser("rank", help="查看排行榜")
rank_parser.add_argument("--problem-id", required=True, help="题目ID")
args = parser.parse_args()
client = CANNJudgeClient()
email = os.environ.get("CANNJUDGE_EMAIL", "")
password = os.environ.get("CANNJUDGE_PASSWORD", "")
if email and password:
print("使用环境变量中的登录信息...")
client.login(email, password)
if args.command == "login":
if args.ciphertext:
result = client.login_with_ciphertext(args.email, args.ciphertext, args.private_key)
elif args.password:
result = client.login(args.email, args.password)
else:
print("错误: 请提供 --ciphertext(推荐)或 --password")
return
print(f"登录成功!")
print(f"用户ID: {result['_id']}")
print(f"昵称: {result.get('nickname', 'N/A')}")
elif args.command == "download":
if not client.user_id:
print("错误: 请先登录")
return
print(f"下载题目 {args.problem_id} 的工程模板...")
extract_dir = client.download_package(args.problem_id, args.output)
print(f"工程已解压到: {extract_dir}")
elif args.command == "submit":
if not client.user_id:
print("错误: 请先登录")
return
project_dir = Path(args.project_dir)
code_dir = project_dir / "code" if (project_dir / "code").exists() else project_dir
tiling_h_path = list(code_dir.glob("op_kernel/*_tiling.h"))
tiling_key_h_path = list(code_dir.glob("op_kernel/tiling_key_*.h"))
host_cpp_path = list(code_dir.glob("op_host/*.cpp"))
kernel_cpp_path = list(code_dir.glob("op_kernel/*.cpp"))
kernel_cpp_path = [p for p in kernel_cpp_path if "_tiling" not in p.name]
if not all([tiling_h_path, host_cpp_path, kernel_cpp_path]):
print("错误: 找不到必要的代码文件")
return
tiling_h = tiling_h_path[0].read_text()
tiling_key_h = tiling_key_h_path[0].read_text() if tiling_key_h_path else ""
host_cpp = host_cpp_path[0].read_text()
kernel_cpp = kernel_cpp_path[0].read_text()
print(f"提交代码到题目 {args.problem_id}...")
submission_id = client.submit(
args.problem_id, tiling_h, tiling_key_h, host_cpp, kernel_cpp
)
print(f"提交成功! Submission ID: {submission_id}")
print("\n等待结果...")
result = client.wait_for_result(submission_id)
print_submission_result(result)
elif args.command == "query":
if not client.user_id:
print("错误: 请先登录")
return
result = client.get_submission(args.submission_id)
print_submission_result(result)
elif args.command == "rank":
if not client.user_id:
print("错误: 请先登录")
return
rankings = client.get_rankings(args.problem_id)
print(f"\n题目 {args.problem_id} 排行榜:")
print("=" * 60)
sorted_rankings = sorted(
rankings,
key=lambda x: (x.get("status") == "Accepted", x.get("score", 0)),
reverse=True
)
for i, item in enumerate(sorted_rankings[:20]):
user_id = item.get("user_id", "unknown")
status = item.get("status", "unknown")
score = item.get("score", "N/A")
print(f"{i+1:3d}. {user_id:24s} | {status:15s} | score: {score}")
else:
parser.print_help()
if __name__ == "__main__":
main()