Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3933817
feat: add CLI config foundation
May 18, 2026
4fff247
fix: harden config foundation
May 18, 2026
a58c061
feat: parse resume filenames
May 18, 2026
4510448
feat: read job requirements workbook
May 18, 2026
4c027a8
feat: extract resume text
May 18, 2026
b0bf700
fix: harden document extraction
May 19, 2026
8f1f6cb
feat: detect duplicate resume content
May 19, 2026
86dca2d
feat: validate model screening responses
May 19, 2026
a7f0aa6
feat: append screening results to workbook
May 19, 2026
7e67932
feat: orchestrate resume screening pipeline
May 19, 2026
904f961
docs: add resume screening usage
May 19, 2026
a0164e5
docs: add Chinese usage guide
May 19, 2026
2209007
fix: make package install reliable
May 19, 2026
a989a64
docs: add web interface design
May 19, 2026
c8805cb
docs: add web implementation plan
May 19, 2026
8eb9ec0
feat: add web config store
May 19, 2026
df4da68
test: cover web config path serialization
May 19, 2026
cbd788d
feat: add web precheck service
May 19, 2026
211b2e4
feat: emit pipeline progress events
May 19, 2026
cf3ab2f
feat: add web run manager
May 19, 2026
94febf6
feat: add local web api
May 19, 2026
d962a0f
feat: add web launcher command
May 19, 2026
ad712b8
feat: add react web wizard
May 19, 2026
781d57f
docs: explain web interface usage
May 19, 2026
d193b20
feat: scan nested files and unsupported formats
May 19, 2026
1b0082b
fix: use configured model and sanitize extracted text
May 19, 2026
fc19370
fix: route more resumes through model screening
May 20, 2026
ca6c807
fix: support tabular job requirement workbooks
May 20, 2026
f691112
fix: normalize configured local paths
May 23, 2026
13f97e5
feat: allow stopping active screening runs
May 23, 2026
61f712d
feat: add macOS desktop app packaging
Jun 1, 2026
cfbb4e3
docs: add screening workbench design
Jun 1, 2026
93e31a6
feat: add macOS dmg packaging
Jun 1, 2026
c3438d8
feat: refresh desktop UI with high contrast style
Jun 3, 2026
7ef3978
style: soften glass UI surfaces
Jun 3, 2026
8897dba
docs: design local qwen model integration
Jun 3, 2026
3e31af4
docs: plan local qwen model integration
Jun 3, 2026
2183ba8
feat: add local qwen model config
Jun 3, 2026
a964512
feat: add local qwen model manager
Jun 3, 2026
7e39edb
feat: add local model web api
Jun 3, 2026
e2638df
feat: integrate local qwen provider
Jun 3, 2026
7170ca7
feat: add local model configuration UI
Jun 3, 2026
1ca6ba0
feat: add cross-platform local runtime packaging
Jun 3, 2026
0f62123
ci: build windows portable package
Jun 4, 2026
8247588
fix: preserve windows paths in config
Jun 4, 2026
3dd6295
fix: support file model downloads on windows
Jun 4, 2026
a9f82f9
fix: preserve local runtime files in packages
Jun 4, 2026
70025a4
fix: copy local runtime into desktop packages
Jun 4, 2026
ced1cfa
Add Windows installer packaging workflow
Jun 10, 2026
705ba14
Fix Windows CI path assumptions
Jun 10, 2026
6b728be
Fix path separator assertion on Windows
Jun 10, 2026
82f77bc
Fix Windows local model connection errors
Jun 11, 2026
eed0b29
Fix local model environment simulation tests
Jun 11, 2026
ca7ee56
Bundle local model in offline Windows packages
Jun 11, 2026
2120db2
Fix Windows workflow script parameter splatting
Jun 11, 2026
4ffc7eb
Use ModelScope for bundled model downloads
Jun 11, 2026
9242b47
Add fallback sources for bundled model download
Jun 11, 2026
12b937e
Use 7-Zip for large Windows archives
Jun 11, 2026
320f464
Enable installer disk spanning for offline model bundle
Jun 11, 2026
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
165 changes: 165 additions & 0 deletions .github/workflows/build-windows-portable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Build Windows Packages

on:
workflow_dispatch:
inputs:
bundle_local_model:
description: "Build a full offline installer with the Qwen GGUF model bundled"
required: false
type: boolean
default: false
push:
branches:
- resume-screening-cli
paths:
- ".github/workflows/build-windows-portable.yml"
- "packaging/**"
- "scripts/build_windows_app.ps1"
- "src/**"
- "tests/**"
- "web/**"
- "pyproject.toml"

jobs:
build-windows-portable:
runs-on: windows-latest
timeout-minutes: 90
env:
DEFAULT_QWEN_MODEL_FILENAME: Qwen3.5-9B-Q4_K_M.gguf
DEFAULT_QWEN_MODEL_URLS: |
https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf?download=true
https://huggingface.co/lmstudio-community/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf?download=true
https://modelscope.cn/models/diodel/Qwen3.5-9B-Q4_K_M-GGUF/resolve/master/Qwen3.5-9B-Q4_K_M.gguf
BUNDLE_LOCAL_MODEL: ${{ github.event_name == 'workflow_dispatch' && inputs.bundle_local_model && '1' || '0' }}
WINDOWS_ARTIFACT_NAME: ${{ github.event_name == 'workflow_dispatch' && inputs.bundle_local_model && 'xiaoa-resume-screening-windows-x64-offline' || 'xiaoa-resume-screening-windows-x64' }}

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: "web/package-lock.json"

- name: Install Inno Setup
shell: pwsh
run: choco install innosetup --no-progress --yes

- name: Build llama.cpp server runtime
shell: pwsh
run: |
git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$env:RUNNER_TEMP\llama.cpp"
cmake -S "$env:RUNNER_TEMP\llama.cpp" -B "$env:RUNNER_TEMP\llama-build" -DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=ON
cmake --build "$env:RUNNER_TEMP\llama-build" --config Release --target llama-server --parallel 2
New-Item -ItemType Directory -Force packaging\runtime\windows-x64 | Out-Null
Copy-Item "$env:RUNNER_TEMP\llama-build\bin\Release\llama-server.exe" packaging\runtime\windows-x64\
Copy-Item "$env:RUNNER_TEMP\llama-build\bin\Release\*.dll" packaging\runtime\windows-x64\ -ErrorAction SilentlyContinue

- name: Download bundled Qwen model
if: env.BUNDLE_LOCAL_MODEL == '1'
shell: pwsh
run: |
$targetDir = "packaging\models\qwen"
New-Item -ItemType Directory -Force $targetDir | Out-Null
$target = Join-Path $targetDir $env:DEFAULT_QWEN_MODEL_FILENAME
if (-not (Test-Path $target)) {
$urls = $env:DEFAULT_QWEN_MODEL_URLS -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$errors = @()
foreach ($url in $urls) {
Write-Host "Downloading bundled Qwen model from $url"
if (Test-Path $target) {
Remove-Item $target -Force
}
curl.exe -L --fail --retry 5 --retry-delay 10 --retry-all-errors --connect-timeout 30 --max-time 2400 --output $target $url
if ($LASTEXITCODE -ne 0) {
$errors += "$url failed with curl exit code $LASTEXITCODE"
if (Test-Path $target) {
Remove-Item $target -Force
}
continue
}
$size = (Get-Item $target).Length
if ($size -ge 1000000000) {
break
}
$errors += "$url downloaded a too-small file: $size bytes"
Remove-Item $target -Force
}
if (-not (Test-Path $target)) {
throw "Failed to download bundled model. Attempts: $($errors -join '; ')"
}
}
$size = (Get-Item $target).Length
if ($size -lt 1000000000) {
throw "Downloaded model looks too small: $size bytes"
}
Write-Host "Bundled model ready: $target ($size bytes)"

- name: Run backend tests
run: python -m pip install ".[test,desktop]" && python -m pytest -q

- name: Build Windows portable and installer packages
shell: pwsh
run: |
$arguments = @{ Python = "python"; Installer = $true }
if ($env:BUNDLE_LOCAL_MODEL -eq "1") {
$arguments.BundleLocalModel = $true
}
.\scripts\build_windows_app.ps1 @arguments

- name: Verify Windows portable package
shell: pwsh
run: |
$runtime = "dist\小A简历筛选\_internal\packaging\runtime\windows-x64\llama-server.exe"
if (-not (Test-Path $runtime)) {
throw "Windows portable package is missing $runtime"
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zipPath = "dist\小A简历筛选-windows-x64.zip"
$zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
try {
$match = $zip.Entries | Where-Object { $_.FullName -like "*packaging/runtime/windows-x64/llama-server.exe" } | Select-Object -First 1
if (-not $match) {
throw "Windows portable zip is missing packaging/runtime/windows-x64/llama-server.exe"
}
if ($env:BUNDLE_LOCAL_MODEL -eq "1") {
$modelEntry = $zip.Entries | Where-Object { $_.FullName -like "*packaging/models/qwen/$env:DEFAULT_QWEN_MODEL_FILENAME" } | Select-Object -First 1
if (-not $modelEntry) {
throw "Windows portable zip is missing bundled local model"
}
}
} finally {
$zip.Dispose()
}
$setup = "dist\小A简历筛选-windows-x64-setup.exe"
if (-not (Test-Path $setup)) {
throw "Windows installer package is missing $setup"
}
if ($env:BUNDLE_LOCAL_MODEL -eq "1") {
$setupBins = Get-ChildItem -Path dist -Filter "小A简历筛选-windows-x64-setup*.bin"
if (-not $setupBins) {
throw "Windows offline installer is missing setup bin slices"
}
$model = "dist\小A简历筛选\_internal\packaging\models\qwen\$env:DEFAULT_QWEN_MODEL_FILENAME"
if (-not (Test-Path $model)) {
throw "Windows package is missing bundled local model $model"
}
}

- name: Upload Windows packages
uses: actions/upload-artifact@v4
with:
name: ${{ env.WINDOWS_ARTIFACT_NAME }}
path: |
dist/小A简历筛选-windows-x64.zip
dist/小A简历筛选-windows-x64-setup.exe
dist/小A简历筛选-windows-x64-setup*.bin
if-no-files-found: error
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
.scratch/
.worktrees/
.superpowers/
node_modules
__pycache__/
*.egg-info/
.DS_Store
data/
logs/
outputs/
build/
dist/
packaging/models/
*.spec.bak
.env
config.yaml
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Resume Screening CLI

## Setup

```bash
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install ".[test]"
cp config.example.yaml config.yaml
```

Edit `config.yaml` and set `model.base_url`, `model.api_key`, and `model.model`.

If the resume filename job name differs from the job workbook sheet name, add aliases:

```yaml
job_aliases:
后端开发工程师: "全栈"
后端开发实习岗: "全栈"
```

## Run

```bash
resume-screening run --config config.yaml
```

The tool appends rows to `/Users/mac/Downloads/小A科技(北京)组织招聘.xlsx`.
It rereads `/Users/mac/Downloads/小A自动化岗位说明书_副本.xlsx` on every run.
Historical rows are preserved.
Duplicate resume content is still appended and marked red.
The resume folder is scanned recursively. All visible regular files are processed; unsupported formats are appended as `待人工二筛` with an extraction note.
Supported filename formats include `【岗位_地点 薪资】姓名 工龄.pdf` and `岗位_地点_薪资_姓名_工龄.pdf`.
The job workbook can use the original one-sheet-per-job layout or a tabular layout where each row contains a `岗位名称`.

## Web Interface

Double-click `启动简历筛选网页版.command` from this project folder.

On first launch, the script creates or reuses `.venv`, installs the tool, creates `config.yaml` from `config.example.yaml` if needed, builds the React page if needed, and opens `http://127.0.0.1:8765`.

The web page has four steps:

1. Save model settings.
2. Confirm resume folder, job requirements workbook, result workbook paths, and optional job aliases.
3. Run a lightweight precheck.
4. Start screening and watch live progress, logs, and final statistics.

## Local Desktop App

Build a portable macOS app bundle:

```bash
source .venv/bin/activate
bash scripts/build_macos_app.sh
```

The outputs are `dist/小A简历筛选.app` and `dist/小A简历筛选-mac.zip`.

The app stores its config at `~/Library/Application Support/小A简历筛选/config.yaml`.
When copied to a new Mac, copy `小A简历筛选-mac.zip`, unzip it, launch the app, and reselect the local resume folder, job workbook, result workbook, and model API settings.
The app bundles the Python runtime and project dependencies. OCR still depends on a local `tesseract` installation if image or scanned-PDF recognition is needed.
The app is built for the architecture of the Mac that runs the build script. The current generated package is for Apple Silicon; build again on Intel Mac if an Intel-only Mac needs to run it.

Build Windows packages on Windows:

```powershell
.\scripts\build_windows_app.ps1 -Python python -Installer
```

The outputs are `dist\小A简历筛选-windows-x64.zip` and `dist\小A简历筛选-windows-x64-setup.exe`.
The installer checks WebView2 and can download it after the user confirms the selected installer task.
Qwen model weights are not bundled; the app checks the local model environment on first launch and downloads the GGUF model only after the user confirms in the app.

## Manual-Review Mode

By default, `model.fallback_to_local_when_unavailable: true` lets the web app switch from an unavailable custom OpenAI-compatible model to the bundled local Qwen model during precheck.

Set `model.allow_without_model: true` to process files without model calls.
Rows that require model judgment are classified as `待人工二筛`.
When `base_url`, `api_key`, and `model` are all set, the tool uses the model first even if fallback mode is enabled.
51 changes: 51 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
resume_dir: "/Users/mac/Downloads"
job_book: "/Users/mac/Downloads/小A自动化岗位说明书_副本.xlsx"
result_book: "/Users/mac/Downloads/小A科技(北京)组织招聘.xlsx"

job_aliases: {}
# 示例:
# job_aliases:
# 后端开发工程师: "全栈"
# 后端开发实习岗: "全栈"

job_profile_overrides: {}
# 预检页确认岗位画像后会自动写入:
# job_profile_overrides:
# 全栈: "自定义岗位核心画像..."

default_source_channel: ""
default_interviewer: ""
index_path: "data/processed_index.json"
ocr_command: "tesseract"

model:
provider: "openai-compatible"
base_url: ""
api_key: ""
model: ""
timeout_seconds: 60
temperature: 0.1
allow_without_model: false
fallback_to_local_when_unavailable: true
local:
runtime: "llama.cpp"
model_family: "qwen3.5"
model_display_name: "Qwen3.5 本地模型"
model_path: ""
manifest_path: "models/qwen/manifest.json"
host: "127.0.0.1"
port: 18080
context_size: 8192
threads: 0
gpu_layers: "auto"
auto_start: true
auto_download: false

screening:
score_pass: 7
score_excellent: 9
categories:
recommend: "推荐初试"
consider: "可考虑"
manual: "待人工二筛"
reject: "未通过"
Loading
Loading