jobscout 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,9 +29,11 @@ crawler được sinh tại lúc chạy và lưu ở `data/crawlers/<domain>.py`
29
29
 
30
30
  **Quy trình cho `platform` được giao:**
31
31
  1. Nếu `data/crawlers/<domain>.py` **đã tồn tại** → chạy thẳng (xem CLI ở `crawlers/README.md`),
32
- truyền `--today <run-id date>`. Nếu nó lỗi/validate fail (board đổi layout) → sinh lại bước 2.
32
+ truyền `--today <run-id date>`. Nếu nó lỗi/validate fail (board đổi layout) → sinh lại (bước 2).
33
33
  2. Nếu **chưa có** và board **được phép crawl** → gọi skill **`crawler-builder`** để WebFetch một tin
34
- mẫu, viết crawler, validate khớp ground-truth, rồi chạy nó.
34
+ mẫu, viết crawler, validate khớp ground-truth, rồi chạy nó. **Chỉ dựng ở `mode=fetch`** (khi đã
35
+ biết URL cụ thể để lấy mẫu và board đáng để đầu tư); `mode=search` **không** dựng — chưa có thì
36
+ dùng thẳng `WebSearch`.
35
37
  3. Board **bị cấm** (LinkedIn, TopCV) hoặc **nghi ngờ** không được phép → **không** sinh crawler,
36
38
  fallback thẳng `WebSearch`/`WebFetch` mô tả bên dưới.
37
39
 
@@ -42,10 +44,12 @@ Mục tiêu: trả về danh sách **đã lọc gọn** của nền tảng này
42
44
  xếp hạng rẻ. **Không** `WebFetch` ở chế độ này. Snippet chỉ dùng để tính relevance/ước tuổi tin
43
45
  **trong context của collector** rồi bỏ đi — **KHÔNG ghi snippet vào file** (tiết kiệm token).
44
46
 
45
- **0. Thử crawler trước (Tier 1):** dùng/sinh `data/crawlers/<domain>.py` (xem mục Tier 1), rồi `Bash`:
47
+ **0. Thử crawler trước (Tier 1) — CHỈ nếu đã có sẵn:** nếu `data/crawlers/<domain>.py` tồn tại (run
48
+ trước đã dựng), `Bash`:
46
49
  `python data/crawlers/<domain>.py --mode search --query "<query song ngữ>" --max <fetch_count> --today <run-id date>`.
47
50
  Nếu exit 0 → lấy mảng `candidates` trong JSON trả về, ghi thẳng ra `out_path` (đã đúng khuôn
48
- `{url,title,platform,relevance,posted_days}`), xong. Nếu board bị cấm/nghi ngờ làm tiếp bằng WebSearch:
51
+ `{url,title,platform,relevance,posted_days}`), xong. **Chưa crawler thì KHÔNG dựng pha search**
52
+ (để dành pha fetch) → làm tiếp bằng WebSearch:
49
53
 
50
54
  1. Từ `target.desired_roles`, `desired_level`, `locations`, top skills → tạo vài biến thể query **song ngữ**, tất cả `site:<platform>`.
51
55
  2. `WebSearch` từng query, gom URL + snippet + tiêu đề. Khử trùng theo URL chuẩn hóa.
@@ -20,7 +20,12 @@ Dùng `<run-id>` duy nhất theo mẫu `<candidate-slug>-<YYYYMMDD-HHMMSS>-<suff
20
20
  ## Bước 2 — Collector (2 pha: search song song → chọn toàn cục → fetch song song)
21
21
  Mục tiêu: **chỉ tốn tối đa `fetch_count` (≤20) lần fetch** nhưng ngân sách fetch được dồn cho các URL **tốt nhất trên toàn bộ nền tảng**, không chia đều — để không bỏ sót nền tảng giàu job.
22
22
 
23
- **Pha 2a Search (rẻ, song song).** Với mỗi nền tảng trong `platforms` (đã chốt ở Bước 1), **spawn một `job-collector` `mode=search` song song trong CÙNG một message**, truyền `fetch_count`. Mỗi cái chỉ `WebSearch` (không fetch) và **lọc gọn ngay tại nguồn**: bỏ tin đăng **≥ 1 tháng**, bỏ link không phải JD / hết hạn / relevance thấp, cap top-`fetch_count` theo relevance, rồi ghi bản **gọn (KHÔNG snippet)** ra `data/jobs/<run-id>.<platform-slug>.candidates.json` (`{url, title, platform, relevance, posted_days}`).
23
+ **Hai tầng thu thập (mỗi `job-collector` tự xử ).**
24
+ - **Tier 1 — crawler sinh tại chỗ:** script Python (chỉ stdlib, import `crawlers/base.py`) bóc job **ngoài context** → gần như 0 token. Nếu `data/crawlers/<domain>.py` đã có (từ run trước) → chạy thẳng. Nếu chưa và board **được phép crawl** → dùng skill **`crawler-builder`** dựng nó (**một lần, ở pha fetch** — xem 2c), validate khớp tin mẫu rồi mới dùng.
25
+ - **Tier 2 — WebSearch/WebFetch của host:** fallback cho board **bị cấm** (LinkedIn robots+ToS, TopCV ToS) hoặc khi build/chạy crawler fail.
26
+ - Crawler sinh ra **lưu ở `data/crawlers/` và tái dùng xuyên run** (đã gitignore); các run sau near-zero token.
27
+
28
+ **Pha 2a — Search (rẻ, song song).** Với mỗi nền tảng trong `platforms` (đã chốt ở Bước 1), **spawn một `job-collector` `mode=search` song song trong CÙNG một message**, truyền `fetch_count`. Mỗi cái gom URL ứng viên **không fetch JD**: nếu domain **đã có** crawler Tier-1 thì chạy `--mode search` (một request rẻ), ngược lại `WebSearch`. **Không dựng crawler ở pha này** — để dành pha fetch, chỉ dựng cho board thắng global-select. Rồi **lọc gọn ngay tại nguồn**: bỏ tin đăng **≥ 1 tháng**, bỏ link không phải JD / hết hạn / relevance thấp, cap top-`fetch_count` theo relevance, rồi ghi bản **gọn (KHÔNG snippet)** ra `data/jobs/<run-id>.<platform-slug>.candidates.json` (`{url, title, platform, relevance, posted_days}`).
24
29
 
25
30
  **Pha 2b — Chọn toàn cục + kiểm tra cache (điều phối viên, không spawn).**
26
31
  1. Đọc tất cả file `*.candidates.json` (gọn, nhẹ token), gộp, khử trùng theo URL chuẩn hóa.
@@ -28,7 +33,7 @@ Mục tiêu: **chỉ tốn tối đa `fetch_count` (≤20) lần fetch** nhưng
28
33
  3. **Kiểm tra cache:** Glob `data/jobs/*.json` (các run trước, loại file `*.candidates.json` và file gộp run hiện tại). Với mỗi URL trong danh sách đã chọn, chuẩn hóa URL rồi tra trong tất cả job đã có (`url` hoặc `id`). Nếu tìm thấy **và** `posted_date` của job cached cho thấy tin **còn trong vòng 30 ngày** → đánh dấu `cached = true`, dùng lại bản đó (không fetch lại). URL còn lại (`cached = false`) → đưa vào danh sách fetch thật.
29
34
  4. **Thông báo ngắn** cho người dùng: "Tìm thấy X job từ cache (skip fetch), sẽ fetch Y URL mới."
30
35
 
31
- **Pha 2c — Fetch (song song, chỉ URL chưa có cache).** Lấy danh sách URL `cached = false` từ pha 2b. Nhóm theo nền tảng; với mỗi nhóm không rỗng, **spawn một `job-collector` `mode=fetch` song song**, truyền `urls` của nhóm đó, ghi `data/jobs/<run-id>.<platform-slug>.json`. Nếu tất cả URL đều có cache → bỏ qua pha này hoàn toàn.
36
+ **Pha 2c — Fetch (song song, chỉ URL chưa có cache).** Lấy danh sách URL `cached = false` từ pha 2b. Nhóm theo nền tảng; với mỗi nhóm không rỗng, **spawn một `job-collector` `mode=fetch` song song**, truyền `urls` của nhóm đó, ghi `data/jobs/<run-id>.<platform-slug>.json`. Mỗi collector thử **Tier 1** trước: tái dùng `data/crawlers/<domain>.py` nếu có, hoặc — với board **được phép** — gọi `crawler-builder` dựng **một lần** từ một URL mẫu trong nhóm, validate khớp ground-truth rồi batch-fetch cả nhóm bằng script đó; board **bị cấm**/build fail → `WebFetch` từng URL (Tier 2). Vì mỗi domain chỉ có **một** collector fetch → không có hai tiến trình cùng dựng một crawler. Nếu tất cả URL đều có cache → bỏ qua pha này hoàn toàn.
32
37
 
33
38
  **Pha 2d — Gộp pool.** Gộp hai nguồn:
34
39
  - Job fetch mới: đọc mọi `data/jobs/<run-id>.<platform-slug>.json`.
@@ -54,5 +59,6 @@ Khử trùng toàn bộ theo `id`/URL, ghi `data/jobs/<run-id>.json`. Nếu sau
54
59
 
55
60
  ## Nguyên tắc
56
61
  - Không bịa dữ liệu job/CV. Thiếu → unknown, hạ confidence.
57
- - Tôn trọng robots/ToS khi scrape; không vượt anti-bot/CAPTCHA.
62
+ - Tôn trọng robots/ToS khi scrape; không vượt anti-bot/CAPTCHA. **LinkedIn/TopCV: không dựng crawler** — chỉ Tier 2 (WebSearch, để user tự mở link).
63
+ - Crawler Tier-1 chỉ lấy **dữ kiện + link**, `description` ≤ 60 từ — **không** republish nguyên văn JD, **không** lưu dữ liệu cá nhân HR/ứng viên.
58
64
  - Không tự nộp hồ sơ thay người dùng.
@@ -11,8 +11,8 @@ Nhận đường dẫn `profile.json`, `run_id` duy nhất và đường dẫn o
11
11
 
12
12
  - **Tier 1 — crawler sinh tại chỗ (thử trước):** dùng skill `crawler-builder` để sinh
13
13
  `data/crawlers/<domain>.py` (Python stdlib, import `crawlers/base.py`) bóc job ngoài context, tiết
14
- kiệm token. Đã có file cho board → chạy thẳng; chưa có + board được phép → sinh mới; board bị cấm →
15
- bỏ qua Tier 1. Hợp đồng CLI: `crawlers/README.md`.
14
+ kiệm token. Đã có file cho board → chạy thẳng; chưa có + board được phép → **sinh mới pha fetch**
15
+ (pha search chưa có thì dùng thẳng WebSearch); board bị cấm → bỏ qua Tier 1. Hợp đồng CLI: `crawlers/README.md`.
16
16
  - search: `python data/crawlers/<domain>.py --mode search --query "<song ngữ>" --max <N> --today <run-id>`
17
17
  - fetch: `python data/crawlers/<domain>.py --mode fetch --urls-file <urls.json|-> --out data/jobs/<run_id>.<domain>.json --today <run-id>`
18
18
  - **Tier 2 — web search/fetch** của host cho board bị cấm/nghi ngờ hoặc crawler chưa dựng được. Chỉ giữ job đọc được JD và chưa có bằng chứng hết hạn.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobscout",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "AI job matching — tìm và xếp hạng việc làm phù hợp CV, song ngữ Việt–Anh. Chạy trên Claude Code và Codex CLI.",
5
5
  "bin": {
6
6
  "jobscout": "bin/cli.mjs"