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
4 changes: 2 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Entry: `packages/webview/src/index.tsx` → `WebviewApp.tsx`. Uses Tailwind CSS

- 4-space indent, no `export default`, prefer `interface` over `type`
- No `any` — use `unknown` instead
- Functions max 120 lines (CI enforces 130 hard limit)
- Functions max 80 lines (CI enforces 80 hard limit)
- Import order: external deps → `@template/*` packages → relative paths
- Component files: PascalCase. Tool/utility files: camelCase
- Error handling: use `CfError`/`ProxyError`/`LoginRequiredError`, never bare `throw new Error()`
Expand All @@ -64,7 +64,7 @@ Entry: `packages/webview/src/index.tsx` → `WebviewApp.tsx`. Uses Tailwind CSS

## CI

PR to `main` triggers: lint → build → test → function length check (>130 lines fails). Results posted as PR comment.
PR to `main` triggers: lint → build → test → function length check (>80 lines fails). Results posted as PR comment.

## Git Branch Rules (Claude MUST follow)

Expand Down
136 changes: 92 additions & 44 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ on:
permissions:
contents: read
pull-requests: write
actions: write

jobs:
review:
name: Code Review
check:
name: Code Checks
runs-on: ubuntu-latest
timeout-minutes: 15

Expand Down Expand Up @@ -48,67 +49,114 @@ jobs:
continue-on-error: true
shell: bash
run: |
overages=""
while IFS= read -r file; do
if [ -f "$file" ]; then
results=$(node -e "
var fs=require('fs');
var lines=fs.readFileSync('$file','utf-8').split('\n');
var start=-1,name='';
for(var i=0;i<lines.length;i++){
var m=lines[i].match(/^\s*(export\s+)?(async\s+)?function\s+(\w+)/);
if(m){
if(start>=0){
var cnt=i-start;
if(cnt>130)console.log(name+' in $file ('+cnt+' lines, line '+(start+1)+')');
}
start=i;name=m[3];
}
}
")
if [ -n "$results" ]; then overages="$overages"$'\n'"$results"; fi
fi
done < <(find apps packages -name "*.ts" -o -name "*.tsx")
echo "$overages" | tee linelength_output.txt
if [ -z "$overages" ]; then echo "passed=true" >> $GITHUB_OUTPUT; else echo "passed=false" >> $GITHUB_OUTPUT; fi

- env:
GH_TOKEN: ${{ github.token }}
BUILD: ${{ steps.build.outcome }}
LINT: ${{ steps.lint.outcome }}
TEST: ${{ steps.test.outcome }}
LL: ${{ steps.linelength.outputs.passed }}
set -o pipefail
node scripts/check-functions.mjs 2>&1 | tee linelength_output.txt
status=$?
if [ "$status" -eq 0 ]; then echo "passed=true" >> $GITHUB_OUTPUT; else echo "passed=false" >> $GITHUB_OUTPUT; fi

- name: Collect results & logs
shell: bash
run: |
mkdir -p artifact/logs
{
echo "lint=${{ steps.lint.outcome }}"
echo "build=${{ steps.build.outcome }}"
echo "test=${{ steps.test.outcome }}"
echo "ll=${{ steps.linelength.outputs.passed }}"
} > artifact/results.txt
for f in lint build test; do
if [ -f "${f}_output.txt" ]; then tail -n 40 "${f}_output.txt" > "artifact/logs/${f}_output.txt"; fi
done
if [ -f linelength_output.txt ]; then cp linelength_output.txt artifact/logs/linelength_output.txt; fi

- uses: actions/upload-artifact@v4
with:
name: check-results
path: artifact/
retention-days: 1

- name: Fail if any check failed
if: >-
steps.lint.outcome == 'failure' ||
steps.build.outcome == 'failure' ||
steps.test.outcome == 'failure' ||
steps.linelength.outputs.passed != 'true'
run: exit 1

report:
name: Post PR Report
needs: check
if: always()
runs-on: ubuntu-latest

steps:
- uses: actions/download-artifact@v4
with:
name: check-results
path: ./results

- name: Build report
shell: bash
run: |
. ./results/results.txt

echo "## PR 审查报告" > comment.md
echo "" >> comment.md
echo "### 检查结果" >> comment.md
echo "" >> comment.md
echo "| 检查项 | 状态 |" >> comment.md
echo "|--------|------|" >> comment.md

if [ "$BUILD" = "success" ]; then echo "| \`pnpm build\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm build\` | :x: |" >> comment.md; fi
if [ "$LINT" = "success" ]; then echo "| \`pnpm lint\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm lint\` | :x: |" >> comment.md; fi
if [ "$TEST" = "success" ]; then echo "| \`pnpm test\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm test\` | :x: |" >> comment.md; fi
if [ "$LL" = "true" ]; then echo "| 函数 <= 130 行 | :white_check_mark: |" >> comment.md; else echo "| 函数 <= 130 行 | :x: |" >> comment.md; fi
if [ "$lint" = "success" ]; then echo "| \`pnpm lint\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm lint\` | :x: |" >> comment.md; fi
if [ "$build" = "success" ]; then echo "| \`pnpm build\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm build\` | :x: |" >> comment.md; fi
if [ "$test" = "success" ]; then echo "| \`pnpm test:unit\` | :white_check_mark: |" >> comment.md; else echo "| \`pnpm test:unit\` | :x: |" >> comment.md; fi
if [ "$ll" = "true" ]; then echo "| 函数 ≤ 80/ tsx ≤ 600 行 | :white_check_mark: |" >> comment.md; else echo "| 函数 ≤ 80 行 / tsx ≤ 600 行 | :x: |" >> comment.md; fi

echo "" >> comment.md
echo "### 未通过的检查" >> comment.md
echo "" >> comment.md

any_fail=false
if [ "$BUILD" != "success" ]; then echo "- [ ] \`pnpm build\`" >> comment.md; any_fail=true; fi
if [ "$LINT" != "success" ]; then echo "- [ ] \`pnpm lint\`" >> comment.md; any_fail=true; fi
if [ "$TEST" != "success" ]; then echo "- [ ] \`pnpm test\`" >> comment.md; any_fail=true; fi
if [ "$LL" != "true" ]; then echo "- [ ] 函数长度不超过 130 行" >> comment.md; any_fail=true; fi

if [ "$any_fail" = false ]; then echo "全部通过,无需修改。" >> comment.md; fi
if [ "$lint" != "success" ]; then echo "- [ ] \`pnpm lint\`" >> comment.md; any_fail=true; fi
if [ "$build" != "success" ]; then echo "- [ ] \`pnpm build\`" >> comment.md; any_fail=true; fi
if [ "$test" != "success" ]; then echo "- [ ] \`pnpm test:unit\`" >> comment.md; any_fail=true; fi
if [ "$ll" != "true" ]; then echo "- [ ] 函数 > 80 行 或 tsx 文件 > 600 行" >> comment.md; any_fail=true; fi

if [ "$any_fail" = false ]; then
echo "全部通过,无需修改。" >> comment.md
else
echo "" >> comment.md
echo "### 失败详情" >> comment.md
echo "" >> comment.md

append_log() {
local label="$1" logfile="$2" full="$3"
if [ -f "$logfile" ] && [ -s "$logfile" ]; then
echo "<details><summary>$label</summary>" >> comment.md
echo "" >> comment.md
echo '```' >> comment.md
if [ "$full" = "true" ]; then cat "$logfile" >> comment.md; else tail -n 40 "$logfile" >> comment.md; fi
echo '```' >> comment.md
echo "</details>" >> comment.md
echo "" >> comment.md
fi
}

[ "$lint" != "success" ] && append_log "\`pnpm lint\`" "results/logs/lint_output.txt" "false"
[ "$build" != "success" ] && append_log "\`pnpm build\`" "results/logs/build_output.txt" "false"
[ "$test" != "success" ] && append_log "\`pnpm test:unit\`" "results/logs/test_output.txt" "false"
[ "$ll" != "true" ] && append_log "行数限制(超限清单)" "results/logs/linelength_output.txt" "true"
fi

echo "" >> comment.md
echo "---" >> comment.md
echo "" >> comment.md
echo "*自动生成 by PR Review workflow*" >> comment.md

- name: Post comment
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
gh pr comment ${{ github.event.pull_request.number }} --body "$(cat comment.md)"

if [ "$any_fail" = true ]; then exit 1; fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.turbo/
temp/
out/
out-test/
src-tauri/target
**/.env
**/.pnpm-store
Expand Down
110 changes: 66 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,23 @@

<!-- toc -->

[特性](#特性) • [项目结构](#项目结构) • [快速开始](#快速开始) • [开发指南](#开发指南) • [常见问题](#常见问题) • [许可证](#许可证)
[特性](#特性) • [项目结构](#项目结构) • [快速开始](#快速开始) • [开发指南](#开发指南) • [发布](#发布) • [常见问题](#常见问题) • [许可证](#许可证)

<!-- tocstop -->

VS Code 插件,支持浏览 AtCoder 竞赛题目、LaTeX 数学公式渲染、DeepL 翻译、评级/非评级报名。
VS Code 插件,支持浏览 AtCoder 竞赛题目、LaTeX 数学公式渲染、DeepL 翻译、提交代码与判题跟踪、评级/非评级报名、榜单与提交详情查看

## 特性

- 📋 **题目浏览** — 输入比赛代号,一键加载所有题目列表
- 📋 **题目浏览** — 输入比赛代号,一键加载所有题目列表与题面
- 📐 **LaTeX 渲染** — 服务端 KaTeX 预渲染,WebView 直接展示
- 🌐 **DeepL 翻译** — 选中题面段落,自动翻译为中文
- 📝 **报名比赛** — 支持评级(Rated)/ 非评级报名
- 🌐 **DeepL 翻译** — 题面段落自动翻译为中文(免费接口 / 官方 API 双模式)
- 📝 **报名比赛** — 支持评级(Rated)/ 非评级报名,自动处理 CSRF 与多步骤表单
- 📤 **提交代码** — 内置提交面板,提交后自动轮询判题状态
- 🧾 **提交历史 & 详情** — 查看个人提交列表、代码、逐测试点判定(Judge Result)
- 🏆 **实时榜单** — 读取 Standings JSON,查看排名与得分
- 🏠 **首页比赛列表** — 侧边栏展示进行中 / 即将开始 / 最近 / 每日比赛
- 🔗 **CPH 导出** — 一键导入 Competitive Programming Helper
- 🔑 **Cookie 登录** — 安全存储 REVEL_SESSION,自动注入请求
- 🎨 **主题适配** — WebView 自动跟随 VS Code 主题

Expand All @@ -34,43 +39,57 @@ VS Code 插件,支持浏览 AtCoder 竞赛题目、LaTeX 数学公式渲染、
```
.
├── apps/
│ └── vscode-extension/ # 插件主程序
│ └── vscode-extension/ # 插件主程序(webpack 打包)
│ ├── src/
│ │ ├── extension.ts # 插件入口,消息路由,handler
│ │ ├── extension.ts # 插件入口,激活 & 面板创建
│ │ ├── viewProvider.ts # 侧边栏 WebviewView Provider
│ │ ├── atcoder.ts # AtCoder 爬虫 & 题面解析
│ │ ├── tools/
│ │ │ ├── fetch.ts # HTTP 客户端,CF/Proxy/Login 错误类
│ │ │ ├── submit.ts # 提交代码(fetchSubmitPage / submitCode)
│ │ │ ├── command.ts # 消息分发路由
│ │ │ ├── types.ts # IncomingMessage / SubStatus
│ │ │ ├── deepl.ts # DeepL 翻译
│ │ │ ├── copy.ts # Markdown 复制
│ │ │ └── SignUpContest.ts # 报名逻辑
│ │ ├── atcoder.test.ts # 单元测试
│ ├── webpack.config.js # Webpack 构建
│ ├── package.json # 扩展清单
│ ├── img.png # 扩展图标
│ └── LICENSE.txt # MIT 许可证
│ │ ├── atcoder.test.ts # 单元测试(mocha)
│ │ └── tools/
│ │ ├── command.ts # 消息分发路由
│ │ ├── handle.ts # 各命令 handler
│ │ ├── fetch.ts # HTTP 客户端(代理绕过 / CF 挑战 / 登录检测)
│ │ ├── webview.ts # WebView HTML 模板与全局变量注入
│ │ ├── submit.ts # 提交代码(fetchSubmitPage / submitCode)
│ │ ├── submission.ts # 提交详情 & Judge Result 解析
│ │ ├── SignUpContest.ts # 报名逻辑(Rated / 非 Rated)
│ │ ├── standings.ts # 榜单 JSON 拉取
│ │ ├── homepage.ts # 首页比赛列表解析
│ │ ├── deepl.ts # DeepL 翻译(免费 / API)
│ │ ├── cph.ts # CPH 问题构建与导出
│ │ ├── copy.ts # Markdown 复制
│ │ ├── types.ts # IncomingMessage / 提交记录类型
│ │ └── i18n/ # zh / en 文案
│ ├── webpack.config.js # 构建 extension.js 与 webview.js
│ ├── package.json # 扩展清单(atc-helper v1.1.2)
│ └── img.png # 扩展图标
├── packages/
│ ├── core/ # @template/core — 核心工具
│ ├── ui/ # @template/ui — React 组件库
│ └── webview/ # @template/webview — WebView 前端
│ ├── core/ # @template/core — MessageBus / StateManager
│ ├── ui/ # @template/ui — React 组件库(Button/Card/Input/Spinner)
│ └── webview/ # @template/webview — WebView 前端(React + Tailwind + KaTeX)
│ └── src/
│ ├── WebviewApp.tsx # 主应用(题目/提交/状态/复制)
│ ├── index.tsx # 入口,按 mode 分发四种应用
│ ├── WebviewApp.tsx # 编辑器主视图
│ ├── SidebarApp.tsx # 侧边栏(比赛列表 / 提交历史 / 登录)
│ ├── ContestApp.tsx # 单场比赛面板(Info/Task/Submit/Rating)
│ ├── SubmissionDetailApp.tsx # 提交详情页
│ ├── VSCodeProvider.tsx # VS Code API 上下文
│ ├── types.ts # WebviewMessage / SubmitResult
│ ├── components/
│ │ └── HtmlContent.tsx # HTML 渲染 & 翻译块
│ ├── types.ts # WebviewMessage / SubmissionDetail
│ ├── hooks/ # useWebviewMessage / useTranslation
│ ├── components/ # ProblemView / HtmlContent / SubmitPanel 等
│ ├── utils/ # format / status 工具
│ ├── i18n/ # zh / en
│ └── styles.css # Tailwind + KaTeX
├── docs/
│ ├── images/
│ │ └── img.png # 扩展图标源文件
│ └── Standard.md # 编码规范
├── scripts/
│ └── check-functions.mjs # 行数检查(ts 函数 ≤80 行 / tsx 文件 ≤600 行)
├── .github/workflows/
│ └── pr-review.yml # PR CI:lint + build + test
│ └── pr-review.yml # PR CI:lint + build + test + 行数检查
├── release/
│ └── extension.vsix # 打包输出
Expand All @@ -83,8 +102,8 @@ VS Code 插件,支持浏览 AtCoder 竞赛题目、LaTeX 数学公式渲染、

| 目录 | 职责 |
|------|------|
| `apps/vscode-extension` | 扩展入口、消息路由、AtCoder 通信、提交、报名 |
| `packages/core` | 核心业务逻辑 |
| `apps/vscode-extension` | 插件入口、消息路由、AtCoder 爬虫、HTTP 工具、提交与报名 |
| `packages/core` | 核心业务逻辑(MessageBus, StateManager) |
| `packages/ui` | React 组件库(Button, Card, Input, Spinner) |
| `packages/webview` | WebView 前端应用(React + Tailwind + KaTeX) |
| `docs/Standard.md` | 编码规范 |
Expand All @@ -100,36 +119,36 @@ pnpm install
### 开发

```bash
# 启动监听模式(自动编译
# 启动监听模式(自动编译扩展与 WebView
pnpm dev
```

### 调试

1. VS Code 中按 `F5`(已预配置 launch.json)
2. 新窗口中 `Ctrl+Shift+P` → `Show WebView`
3. 输入比赛代号(如 `abc345`)→ 加载题目 → 浏览/翻译/报名
2. 侧边栏点击 AtCoder Helper 图标打开比赛列表,或 `Ctrl+Shift+P` → `AtCoder Helper (Editor)`
3. 输入比赛代号(如 `abc345`)→ 加载题目 → 浏览 / 翻译 / 报名 / 提交

### 设置 Cookie(用于报名 & 登录后题目)

1. 浏览器登录 https://atcoder.jp
2. F12 → Application → Cookies → 复制 `REVEL_SESSION` 的 Value
3. VS Code 中 `Ctrl+Shift+P` → `Set AtCoder Login Cookie`(或在侧边栏底部登录区域粘贴保存)

### 设置 DeepL API Key(用于翻译
### 设置 DeepL API Key(用于官方 API 翻译模式

`Ctrl+Shift+P` → `Set DeepL API Key`
`Ctrl+Shift+P` → `Set DeepL API Key`(免费模式无需配置)

## 开发指南

### 脚本

```bash
pnpm build # 编译所有包
pnpm dev # 开发监听模式
pnpm lint # ESLint 检查
pnpm build # 编译所有包(turbo build)
pnpm dev # 开发监听模式(turbo dev)
pnpm lint # ESLint 检查(turbo lint)
pnpm format # Prettier 格式化
pnpm test:unit # 运行单元测试(apps/vscode-extension)
pnpm test # 运行单元测试(turbo test,apps/vscode-extension)
pnpm clean # 清理构建产物
```

Expand All @@ -139,7 +158,7 @@ pnpm clean # 清理构建产物

- 文件名:组件 PascalCase,工具 camelCase
- TypeScript:优先 `interface`,避免 `any`
- 函数不超过 **120 行**(可适当放宽至 130
- ts 函数不超过 **80 行**;tsx 文件不超过 **600 行**(CI 用 `scripts/check-functions.mjs` 检查
- 导入顺序:外部 → 内部包 → 相对路径

## 发布
Expand All @@ -154,16 +173,19 @@ pnpm package

**WebView 不显示?**
- 确保已运行 `pnpm build`
- 检查 `dist/webview.js` 是否存在
- 检查 `dist/extension.js` 与 `dist/webview.js` 是否存在

**报名失败?**
**报名 / 提交失败?**
- 确认已设置 AtCoder Cookie
- Cookie 可能过期,重新登录获取

**LaTeX 不渲染?**
- KaTeX 在扩展端预渲染,WebView 只需 CSS
- 检查控制台是否有字体加载错误

**Cloudflare 挑战?**
- 扩展会提示在浏览器中打开对应页面完成人机验证后重试

## 许可证

MIT
Loading
Loading