104-sourcing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

104 HR 人才搜尋

104 HR Talent Search

概覽

Overview

本 Skill 使用 agent-browser CLI 控制瀏覽器,自動登入 104 招募管理平台並搜尋合適人選。執行流程分三個階段:需求訪談 → 登入 → 純 API 搜尋篩選
依賴工具:本 Skill 需要
agent-browser
CLI,安裝方式見 frontmatter 的
compatibility
欄位。 指令參考references/agent-browser.md
This Skill uses the agent-browser CLI to control the browser, automatically log in to the 104 Recruitment Management Platform and search for suitable candidates. The execution process is divided into three stages: Requirement Interview → Login → API-only Search & Filter.
Dependent Tool: This Skill requires the
agent-browser
CLI. For installation instructions, refer to the
compatibility
field in the frontmatter. Command Reference: references/agent-browser.md

架構原則(重要)

Architecture Principles (Important)

  • 登入用 browser,其餘全用 API:瀏覽器只用來建立 session cookie,搜尋/篩選/評估全部走 API,速度快 100 倍以上
  • 所有 fetch 必須在主 session(104-sourcing)內呼叫:Subagent 的獨立 session 無法共用登入 cookie
  • eval 只能用 ES5 語法
    var
    不能用
    const/let
    ,不能用 arrow function,否則 eval 會報錯

  • Use browser only for login, all other operations via API: The browser is only used to establish session cookies; search/filter/evaluation are all done via API, which is over 100 times faster
  • All fetch requests must be called within the main session (104-sourcing): Independent sessions of Subagents cannot share login cookies
  • eval only supports ES5 syntax: Use
    var
    instead of
    const/let
    , do not use arrow functions, otherwise eval will throw errors

第一階段:需求訪談

Stage 1: Requirement Interview

在開啟瀏覽器前,必須先透過訪談了解招募需求,不得假設或跳過。訪談分兩層:
Before launching the browser, you must first understand the recruitment requirements through interviews, and you cannot make assumptions or skip this step. The interview is divided into two levels:

基本條件(硬性篩選用)

Basic Criteria (For Hard Filtering)

  1. 職位名稱:要搜尋什麼職務?(例:電銷、業務、工程師)
  2. 工作地點:候選人可接受的工作地點?(例:台北市、新北市板橋區)
  3. 硬性要求:有哪些必要條件?(例:工作年資、學歷、特定技能或證照)
  4. 待業狀況:是否只看特定就業狀態的求職者?(預設不限)
    • 不限:
      empStatus=0
    • 在職中(想跳槽):
      empStatus=1
    • 待業中(即可上班):
      empStatus=2
  1. Job Title: What position are you searching for? (e.g., Telemarketing, Sales, Engineer)
  2. Work Location: What work locations can candidates accept? (e.g., Taipei City, Banqiao District, New Taipei City)
  3. Hard Requirements: What are the necessary conditions? (e.g., work experience, education background, specific skills or certifications)
  4. Employment Status: Do you only want to view job seekers in specific employment statuses? (Default: No restriction)
    • No restriction:
      empStatus=0
    • Employed (seeking job change):
      empStatus=1
    • Unemployed (available immediately):
      empStatus=2

質性條件(篩選後評估用)

Qualitative Criteria (For Post-Filter Evaluation)

  1. 理想候選人樣貌:這個職位最重視什麼特質或背景?有哪些過往經歷特別加分?
  2. 地雷:有哪些狀況要排除?(例:頻繁換工作、特定產業背景、工作空窗過長)
  3. 加分項目:其他 nice-to-have?(例:具備特定軟體操作經驗、語言能力)
訪談結束後,整理並回覆「搜尋策略確認」:
  • 搜尋關鍵字:用於 API 的
    kws
    參數
  • 硬性篩選條件:JS 過濾邏輯的依據
  • 質性評估標準:看完候選人資料後打分的依據
用戶確認後,再進入第二階段。

  1. Ideal Candidate Profile: What traits or backgrounds are most valued for this position? What past experiences are particularly preferred?
  2. Red Flags: What situations need to be excluded? (e.g., frequent job changes, specific industry backgrounds, long employment gaps)
  3. Bonus Items: Other nice-to-have conditions? (e.g., experience with specific software, language proficiency)
After the interview, organize and reply with a "Search Strategy Confirmation":
  • Search Keywords: Used for the
    kws
    parameter in the API
  • Hard Filter Criteria: Basis for JS filtering logic
  • Qualitative Evaluation Standards: Basis for scoring after viewing candidate profiles
Proceed to Stage 2 only after user confirmation.

第二階段:啟動瀏覽器並確認登入狀態

Stage 2: Launch Browser and Verify Login Status

瀏覽器只需登入,不需要用 UI 操作搜尋或篩選條件。
The browser only needs to log in; there is no need to use the UI to perform search or set filter criteria.

步驟

Steps

  1. 直接開啟招募管理平台(帶現有 session,看是否已登入)
    bash
    agent-browser --session-name 104-sourcing open https://vip.104.com.tw/rms/index
  2. 取得目前狀態
    bash
    agent-browser --session-name 104-sourcing snapshot -i
    解讀 snapshot 輸出,判斷目前頁面狀態:
  3. 若已在 vip.104.com.tw(招募管理平台)→ 直接進入第三階段,不需要登入
  4. 若被導向登入頁 → 向用戶索取帳號密碼,注意兩種情況:
    • 完整登入(snapshot 有 email + password 兩個輸入欄):輸入帳號與密碼
    • 密碼重新驗證(snapshot 只有 password 輸入欄,無 email 欄):104 在 session 過期時有時只要求再輸入密碼,不需重新輸入 email
    bash
    agent-browser --session-name 104-sourcing fill @eX "{用戶提供的 email}"  # 僅完整登入時
    agent-browser --session-name 104-sourcing fill @eX "{用戶提供的密碼}"
    agent-browser --session-name 104-sourcing click @eX   # 登入按鈕
    agent-browser --session-name 104-sourcing snapshot -i
  5. 若出現 MFA(snapshot 中有 OTP 輸入)
    • 向用戶索取 6 位數 Email 驗證碼
    • agent-browser --session-name 104-sourcing fill @eX "驗證碼"
    • agent-browser --session-name 104-sourcing press Enter
  6. 確認已進入 vip.104.com.tw
    • 若出現重複登入對話框:snapshot 找「將目前帳號登出」→ click
    • 若出現廣告彈窗(screenshot 中看到「加強曝光」等促銷文字):snapshot 找關閉按鈕 → click

  1. Directly open the Recruitment Management Platform (with existing session to check if already logged in)
    bash
    agent-browser --session-name 104-sourcing open https://vip.104.com.tw/rms/index
  2. Get current status
    bash
    agent-browser --session-name 104-sourcing snapshot -i
    Interpret the snapshot output to determine the current page status:
  3. If already on vip.104.com.tw (Recruitment Management Platform) → directly proceed to Stage 3, no need to log in
  4. If redirected to login page → request account and password from the user, note two scenarios:
    • Full Login (snapshot shows both email and password input fields): Enter account and password
    • Password Re-verification (snapshot shows only password input field, no email field): 104 sometimes only requires re-entering the password when the session expires, no need to re-enter the email
    bash
    agent-browser --session-name 104-sourcing fill @eX "{user-provided email}"  # Only for full login
    agent-browser --session-name 104-sourcing fill @eX "{user-provided password}"
    agent-browser --session-name 104-sourcing click @eX   # Login button
    agent-browser --session-name 104-sourcing snapshot -i
  5. If MFA appears (snapshot shows OTP input field)
    • Request 6-digit email verification code from the user
    • agent-browser --session-name 104-sourcing fill @eX "verification code"
    • agent-browser --session-name 104-sourcing press Enter
  6. Confirm access to vip.104.com.tw
    • If duplicate login dialog appears: Find "Log out current account" in snapshot → click
    • If ad popup appears (promotional text like "Enhance Exposure" in screenshot): Find close button in snapshot → click

第三階段:純 API 搜尋與篩選

Stage 3: API-only Search & Filter

登入完成後,全程用 eval + fetch API,不再操作瀏覽器 UI。
After login is completed, use eval + fetch API for all operations, no longer operate the browser UI.

已知 API 端點

Known API Endpoints

用途端點
搜尋候選人列表(含工作經歷)
GET https://auth.vip.104.com.tw/api/search/searchResult
取得單一候選人完整履歷
GET https://auth.vip.104.com.tw/vipapi/resume/search/{idNo}?path_for_log=list_search
取得儲存資料夾列表(含 folderNo)
GET https://auth.vip.104.com.tw/api/resumeTools/getFolderList?source=search&ec=105
儲存候選人到資料夾(支援批次)
POST https://auth.vip.104.com.tw/api/resumeTools/saveResume
PurposeEndpoint
Search candidate list (including work experience)
GET https://auth.vip.104.com.tw/api/search/searchResult
Get full resume of a single candidate
GET https://auth.vip.104.com.tw/vipapi/resume/search/{idNo}?path_for_log=list_search
Get saved folder list (including folderNo)
GET https://auth.vip.104.com.tw/api/resumeTools/getFolderList?source=search&ec=105
Save candidates to folder (supports batch)
POST https://auth.vip.104.com.tw/api/resumeTools/saveResume

就業狀態代碼(empStatus 參數)

Employment Status Codes (empStatus Parameter)

說明
0
不限
1
在職中(想跳槽;
expJobArr[0].expEndDesc
通常為「仍在職」)
2
待業中(已離職,即可上班;
expEndDesc
顯示離職時長)
ValueDescription
0
No restriction
1
Employed (seeking job change;
expJobArr[0].expEndDesc
is usually "Still employed")
2
Unemployed (already resigned, available immediately;
expEndDesc
shows duration of unemployment)

城市代碼

City Codes

完整代碼清單見 references/area.json,支援台灣、大陸、海外地區。縣市層代碼末三位為
000
,行政區層末三位為流水號。
常用縣市快查:
地點city 參數值
台北市
6001001000
新北市
6001002000
基隆市
6001004000
桃園市
6001005000
新竹縣市
6001006000
台中市
6001008000
台南市
6001014000
高雄市
6001016000
Complete code list can be found in references/area.json, supporting Taiwan, mainland China, and overseas regions. County/city level codes end with three
0
s, district level codes end with serial numbers.
Common County/City Quick Reference:
Locationcity Parameter Value
Taipei City
6001001000
New Taipei City
6001002000
Keelung City
6001004000
Taoyuan City
6001005000
Hsinchu County/City
6001006000
Taichung City
6001008000
Tainan City
6001014000
Kaohsiung City
6001016000

步驟 1:呼叫搜尋 API 第 1 頁,取得總頁數與 fixedUpdateDate

Step 1: Call Search API for Page 1, get total pages and fixedUpdateDate

bash
agent-browser --session-name 104-sourcing eval "
fetch('https://auth.vip.104.com.tw/api/search/searchResult?contactPrivacy=0&kws=%E9%9B%BB%E9%8A%B7%E4%BA%BA%E6%89%8D&city=6001001000&workExpTimeType=all&sex=2&empStatus={0|1|2}&updateDateType=1&sortType=RANK&readStatus=all&plastActionDateType=1&page=1&ec=105', {credentials:'include'})
  .then(function(r){return r.json()})
  .then(function(d){
    window._fixedDate = d.result.fixedUpdateDate;
    window._totalPages = d.result.pageInfo.total_page;
    window._allCandidates = d.result.data;
    window._p1done = true;
  });
'fetching page 1...'
"
等待後確認:
bash
sleep 3 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._p1done, fixedDate:window._fixedDate, totalPages:window._totalPages, count:window._allCandidates&&window._allCandidates.length})"
重要
fixedUpdateDate
必須從第 1 頁回應取得,後續所有分頁都要帶這個值,確保結果一致性。
bash
agent-browser --session-name 104-sourcing eval "
fetch('https://auth.vip.104.com.tw/api/search/searchResult?contactPrivacy=0&kws=%E9%9B%BB%E9%8A%B7%E4%BA%BA%E6%89%8D&city=6001001000&workExpTimeType=all&sex=2&empStatus={0|1|2}&updateDateType=1&sortType=RANK&readStatus=all&plastActionDateType=1&page=1&ec=105', {credentials:'include'})
  .then(function(r){return r.json()})
  .then(function(d){
    window._fixedDate = d.result.fixedUpdateDate;
    window._totalPages = d.result.pageInfo.total_page;
    window._allCandidates = d.result.data;
    window._p1done = true;
  });
'fetching page 1...'
"
Wait and confirm:
bash
sleep 3 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._p1done, fixedDate:window._fixedDate, totalPages:window._totalPages, count:window._allCandidates&&window._allCandidates.length})"
Important:
fixedUpdateDate
must be obtained from the response of Page 1, and this value must be included in all subsequent page requests to ensure result consistency.

步驟 2:並發抓取所有剩餘頁

Step 2: Concurrent Fetch of All Remaining Pages

bash
agent-browser --session-name 104-sourcing eval "
var baseUrl = 'https://auth.vip.104.com.tw/api/search/searchResult?contactPrivacy=0&kws=%E9%9B%BB%E9%8A%B7%E4%BA%BA%E6%89%8D&city=6001001000&workExpTimeType=all&sex=2&empStatus={0|1|2}&updateDateType=1&sortType=RANK&readStatus=all&plastActionDateType=1&ec=105&fixed_update_date=';
var pages = [];
for(var i=2; i<=window._totalPages; i++) pages.push(i);
Promise.all(pages.map(function(p){
  return fetch(baseUrl+window._fixedDate+'&page='+p, {credentials:'include'})
    .then(function(r){return r.json()})
    .then(function(d){ window._allCandidates = window._allCandidates.concat(d.result.data); });
})).then(function(){ window._allDone = true; });
'fetching remaining pages...'
"
等待後確認:
bash
sleep 8 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._allDone, total:window._allCandidates.length})"
bash
agent-browser --session-name 104-sourcing eval "
var baseUrl = 'https://auth.vip.104.com.tw/api/search/searchResult?contactPrivacy=0&kws=%E9%9B%BB%E9%8A%B7%E4%BA%BA%E6%89%8D&city=6001001000&workExpTimeType=all&sex=2&empStatus={0|1|2}&updateDateType=1&sortType=RANK&readStatus=all&plastActionDateType=1&ec=105&fixed_update_date=';
var pages = [];
for(var i=2; i<=window._totalPages; i++) pages.push(i);
Promise.all(pages.map(function(p){
  return fetch(baseUrl+window._fixedDate+'&page='+p, {credentials:'include'})
    .then(function(r){return r.json()})
    .then(function(d){ window._allCandidates = window._allCandidates.concat(d.result.data); });
})).then(function(){ window._allDone = true; });
'fetching remaining pages...'
"
Wait and confirm:
bash
sleep 8 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._allDone, total:window._allCandidates.length})"

步驟 3:在 JS 中直接篩選,不需要呼叫個別履歷 API

Step 3: Direct Filtering in JS, No Need to Call Individual Resume APIs

搜尋結果的每筆資料已包含完整
expJobArr
,可直接在記憶體中篩選:
bash
agent-browser --session-name 104-sourcing eval "
Each entry in the search results already contains the complete
expJobArr
, which can be directly filtered in memory:
bash
agent-browser --session-name 104-sourcing eval "

範例:篩選職稱含「電銷」且年資 >= 1 年的候選人

Example: Filter candidates with job title containing 'Telemarketing' and work experience >= 1 year

var qualified = window._allCandidates.filter(function(c){ return c.expJobArr && c.expJobArr.some(function(e){ if((e.expTitle||'').indexOf('電銷') === -1) return false; var desc = e.expEndDesc || ''; if(desc.indexOf('仍在職') > -1) return true; var yr = desc.match(/(\d+)年/); return yr && parseInt(yr[1]) >= 1; }); }); window._qualified = qualified; JSON.stringify({ totalSearched: window._allCandidates.length, qualified: qualified.length, list: qualified.map(function(c){ return { idNo: c.idNo, name: c.userName, age: c.age, edu: c.eduDesc.split(' ')[0], city: c.wcityNoDesc, totalExp: c.expPeriodDesc, teleSalesJobs: c.expJobArr.filter(function(e){ return (e.expTitle||'').indexOf('電銷')>-1; }).map(function(e){ return e.expTitle+'@'+e.expFirm+'('+e.expEndDesc+')'; }) }; }) })"
undefined
var qualified = window._allCandidates.filter(function(c){ return c.expJobArr && c.expJobArr.some(function(e){ if((e.expTitle||'').indexOf('電銷') === -1) return false; var desc = e.expEndDesc || ''; if(desc.indexOf('仍在職') > -1) return true; var yr = desc.match(/(\d+)年/); return yr && parseInt(yr[1]) >= 1; }); }); window._qualified = qualified; JSON.stringify({ totalSearched: window._allCandidates.length, qualified: qualified.length, list: qualified.map(function(c){ return { idNo: c.idNo, name: c.userName, age: c.age, edu: c.eduDesc.split(' ')[0], city: c.wcityNoDesc, totalExp: c.expPeriodDesc, teleSalesJobs: c.expJobArr.filter(function(e){ return (e.expTitle||'').indexOf('電銷')>-1; }).map(function(e){ return e.expTitle+'@'+e.expFirm+'('+e.expEndDesc+')'; }) }; }) })"
undefined

搜尋 API 回傳的候選人欄位參考

Reference for Candidate Fields Returned by Search API

欄位說明
idNo
候選人 ID(用於呼叫個別履歷 API)
userName
姓名
age
年齡
sexDesc
性別
eduDesc
學歷(含學校名,可用
.split(' ')[0]
取簡稱)
wcityNoDesc
希望工作地點(多個以「、」分隔)
expPeriodDesc
總工作年資描述
titleCatDesc
希望職稱類別
expJobArr
工作經歷陣列(含
expTitle
,
expFirm
,
expJobNote
,
expEndDesc
,
expPeriod
FieldDescription
idNo
Candidate ID (used to call individual resume API)
userName
Name
age
Age
sexDesc
Gender
eduDesc
Education background (including school name; use
.split(' ')[0]
to get abbreviation)
wcityNoDesc
Desired work location (multiple locations separated by 「、」)
expPeriodDesc
Total work experience description
titleCatDesc
Desired job title category
expJobArr
Work experience array (including
expTitle
,
expFirm
,
expJobNote
,
expEndDesc
,
expPeriod
)

步驟 4:批次儲存合格候選人到資料夾

Step 4: Batch Save Qualified Candidates to Folder

篩選完成後,一次 API 呼叫即可批次儲存所有合格候選人,不需要 UI 操作。
After filtering is completed, you can batch save all qualified candidates with a single API call, no need for UI operations.

先取得資料夾列表,讓用戶選擇

First Get Folder List and Let User Select

bash
agent-browser --session-name 104-sourcing eval "fetch('https://auth.vip.104.com.tw/api/resumeTools/getFolderList?source=search&ec=105',{credentials:'include'}).then(function(r){return r.json()}).then(function(d){window._folders=JSON.stringify(d.result.folderList.map(function(f){return {name:f.name,folderNo:f.folderNo}}))});'fetching'"
sleep 2 && agent-browser --session-name 104-sourcing eval "window._folders"
取得結果後,以表格呈現給用戶:
#資料夾名稱folderNo
1(依 API 回應填入)...
詢問用戶:「請問要將 N 位候選人存入哪個資料夾?」,等用戶選擇後,以選定的
folderNo
執行下一步。
bash
agent-browser --session-name 104-sourcing eval "fetch('https://auth.vip.104.com.tw/api/resumeTools/getFolderList?source=search&ec=105',{credentials:'include'}).then(function(r){return r.json()}).then(function(d){window._folders=JSON.stringify(d.result.folderList.map(function(f){return {name:f.name,folderNo:f.folderNo}}))});'fetching'"
sleep 2 && agent-browser --session-name 104-sourcing eval "window._folders"
Present the results to the user in a table:
#Folder NamefolderNo
1(Fill in according to API response)...
Ask the user: "Which folder would you like to save the N candidates to?" After the user selects, use the selected
folderNo
to proceed to the next step.

批次儲存所有合格候選人

Batch Save All Qualified Candidates

重要:每次 API 呼叫最多成功儲存 50 筆,超過的會靜默失敗(在
params.fail
中)。 必須以 50 筆為單位分批送出,並使用同步 XHR 取得即時結果(async fetch + window 變數在頁面導航後會消失)。
bash
undefined
Important: Each API call can successfully save a maximum of 50 entries; entries exceeding this limit will fail silently (listed in
params.fail
). You must send requests in batches of 50 entries, and use synchronous XHR to get real-time results (async fetch + window variables will be lost after page navigation).
bash
undefined

以每批 50 筆為單位,用同步 XHR 儲存,直接取得結果

Save in batches of 50 entries, use synchronous XHR to get direct results

agent-browser --session-name 104-sourcing eval " var folderNo = '{用戶選擇的 folderNo}'; var batch = window._qualified.slice(0, 50).map(function(c){return c.idNo}); var body = 'rc=11012313&docNo='+folderNo+'&pageSource=search&isDuplicate=0&contentInfo%5Bsnapshot%5D=&contentInfo%5BsearchEngine%5D='+batch.join('%2C'); var xhr = new XMLHttpRequest(); xhr.open('POST','https://auth.vip.104.com.tw/api/resumeTools/saveResume',false); xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.withCredentials = true; xhr.send(body); xhr.responseText; "

將 `slice(0, 50)` 改為 `slice(50, 100)`、`slice(100, 150)`... 依此類推完成所有批次。

**回應格式說明:**
- `code: 0` → 全部儲存成功
- `code: 6, type: dataDuplicated` → **使用 isDuplicate=-1 時才會出現**;`params.id` 是成功儲存的 ID(非重複),其餘全被帳號層級的重複檢查跳過
- `code: 7, type: partialFail` → 部分失敗;`params.success.searchEngine[]` 是成功的,`params.fail.searchEngine[]` 是失敗的
- `code: 4, type: overStorage` → 資料夾已滿(上限 300 筆)
- `code: 5, type: overView` → 單次請求候選人數量超過平台上限

**saveResume POST 參數說明:**

| 參數 | 值 | 說明 |
|------|-----|------|
| `rc` | `11012313` | 操作類型碼(搜尋頁儲存,固定值) |
| `docNo` | `{folderNo}` | 目標資料夾 ID |
| `pageSource` | `search` | 來源頁面(固定值) |
| `isDuplicate` | `0` | **務必用 0**;`-1` 會跳過帳號內任何資料夾已存過的候選人,導致大量漏存 |
| `contentInfo[searchEngine]` | `{idNo1},{idNo2},...` | 候選人 idNo,逗號分隔,**每批最多 50 筆** |
| `contentInfo[snapshot]` | 空 | 快照 ID(搜尋頁固定留空) |
agent-browser --session-name 104-sourcing eval " var folderNo = '{folderNo selected by user}'; var batch = window._qualified.slice(0, 50).map(function(c){return c.idNo}); var body = 'rc=11012313&docNo='+folderNo+'&pageSource=search&isDuplicate=0&contentInfo%5Bsnapshot%5D=&contentInfo%5BsearchEngine%5D='+batch.join('%2C'); var xhr = new XMLHttpRequest(); xhr.open('POST','https://auth.vip.104.com.tw/api/resumeTools/saveResume',false); xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.withCredentials = true; xhr.send(body); xhr.responseText; "

Change `slice(0, 50)` to `slice(50, 100)`, `slice(100, 150)`... and so on to complete all batches.

**Response Format Description:**
- `code: 0` → All saved successfully
- `code: 6, type: dataDuplicated` → **Only appears when isDuplicate=-1**; `params.id` is the ID saved successfully (not duplicate), others are skipped by account-level duplicate check
- `code: 7, type: partialFail` → Partial failure; `params.success.searchEngine[]` are successful entries, `params.fail.searchEngine[]` are failed entries
- `code: 4, type: overStorage` → Folder is full (limit 300 entries)
- `code: 5, type: overView` → Number of candidates in single request exceeds platform limit

**Description of saveResume POST Parameters:**

| Parameter | Value | Description |
|------|-----|------|
| `rc` | `11012313` | Operation type code (save from search page, fixed value) |
| `docNo` | `{folderNo}` | Target folder ID |
| `pageSource` | `search` | Source page (fixed value) |
| `isDuplicate` | `0` | **Must use 0**; `-1` will skip any candidates that have been saved in any folder under the account, resulting in a large number of missed candidates; `0` will force save to the target folder |
| `contentInfo[searchEngine]` | `{idNo1},{idNo2},...` | Candidate idNo, separated by commas, **max 50 entries per batch** |
| `contentInfo[snapshot]` | Empty | Snapshot ID (fixed empty for search page) |

候選人評估報告格式(基本版)

Candidate Evaluation Report Format (Basic Version)

undefined
undefined

篩選結果(共 N 位符合條件)

Filter Results (Total N Qualified Candidates)

#姓名年齡學歷希望地點工作年資相關經歷
1王小明35大學台北市8~9年電銷主管@XX公司(3年)
...
建議聯絡:#1 王小明、#3 ... 建議略過:#2 ...(原因:地點不符)
undefined
#NameAgeEducationDesired LocationTotal Work ExperienceRelevant Experience
1Wang Xiaoming35UniversityTaipei City8~9 yearsTelemarketing Supervisor@XX Company (3 years)
...
Recommended for Contact: #1 Wang Xiaoming, #3 ... Recommended to Skip: #2 ... (Reason: Location mismatch)
undefined

步驟 3.5(選用):深度履歷分析

Step 3.5 (Optional): In-Depth Resume Analysis

顯示基本篩選結果後,詢問用戶是否進行深度分析
「目前根據搜尋列表資料篩出 N 位候選人。是否進行深度履歷分析?(額外讀取自我介紹、希望薪資、產業年資,耗時較長但評估更精準)」
用戶選擇「是」後,依序執行:
After presenting the basic filter results, ask the user if they want to perform in-depth analysis:
"Currently, N candidates have been filtered based on search list data. Would you like to perform in-depth resume analysis? (This will additionally read self-introduction, desired salary, and industry experience; it takes longer but provides more accurate evaluation)"
If the user selects "Yes", proceed in order:

分批 fetch 個別履歷(每批 50 筆)

Batch Fetch Individual Resumes (50 Entries per Batch)

以 50 筆為一批,批次間 sleep 5 秒,避免 rate limit。主 session 收集每批 JSON:
bash
undefined
Fetch in batches of 50 entries, sleep 5 seconds between batches to avoid rate limits. The main session collects JSON data for each batch:
bash
undefined

第 1 批(idNo 0~49)

Batch 1 (idNo 0~49)

agent-browser --session-name 104-sourcing eval " var ids = window._qualified.slice(0, 50).map(function(c){return c.idNo}); var results = {}; Promise.all(ids.map(function(id){ return fetch('https://auth.vip.104.com.tw/vipapi/resume/search/'+id+'?path_for_log=list_search',{credentials:'include'}) .then(function(r){return r.json()}) .then(function(d){ var res = d.data ? d.data.resume : null; if(!res) return; results[id] = { intro: res.intro ? res.intro.replace(/<[^>]+>/g,'') : '', hopeSalary: res.hopeSalaryDesc || '', expCats: res.expCatTimeDesc ? res.expCatTimeDesc.map(function(e){return e.expCatDesc+':'+e.expTimeDesc}).join(', ') : '' }; }); })).then(function(){ window._resumeBatch = JSON.stringify(results); }); 'fetching batch 1...' " sleep 10 && agent-browser --session-name 104-sourcing eval "window._resumeBatch"

每批取回 JSON 後,累積到主 session 的物件中(`let allDetails = {...allDetails, ...JSON.parse(batchJson)}`)。
將 `slice(0,50)` 改為 `slice(50,100)`, `slice(100,150)` ... 完成所有批次。

> 批次數量 = `Math.ceil(qualified.length / 50)`,每批 sleep 10 秒。
agent-browser --session-name 104-sourcing eval " var ids = window._qualified.slice(0, 50).map(function(c){return c.idNo}); var results = {}; Promise.all(ids.map(function(id){ return fetch('https://auth.vip.104.com.tw/vipapi/resume/search/'+id+'?path_for_log=list_search',{credentials:'include'}) .then(function(r){return r.json()}) .then(function(d){ var res = d.data ? d.data.resume : null; if(!res) return; results[id] = { intro: res.intro ? res.intro.replace(/<[^>]+>/g,'') : '', hopeSalary: res.hopeSalaryDesc || '', expCats: res.expCatTimeDesc ? res.expCatTimeDesc.map(function(e){return e.expCatDesc+':'+e.expTimeDesc}).join(', ') : '' }; }); })).then(function(){ window._resumeBatch = JSON.stringify(results); }); 'fetching batch 1...' " sleep 10 && agent-browser --session-name 104-sourcing eval "window._resumeBatch"

Change `slice(0,50)` to `slice(50,100)`, `slice(100,150)` ... to complete all batches.

> Number of batches = `Math.ceil(qualified.length / 50)`, sleep 10 seconds between batches.

合併資料並寫入暫存檔

Merge Data and Write to Temporary File

所有批次完成後,將
window._qualified
(基本資料)與累積的個別履歷合併,寫入暫存檔:
bash
undefined
After all batches are completed, merge
window._qualified
(basic data) with the collected individual resumes, and write to
/tmp/104_resumes.json
.
json
[
  {
    "idNo": "...",
    "name": "...",
    "age": 28,
    "expJobArr": [...],
    "intro": "...",
    "hopeSalary": "35,000~45,000",
    "expCats": "教育業:2年, 金融業:1年"
  },
  ...
]

主 session 中取出 _qualified 基本資料

Launch Sub-agents for Concurrent Analysis

agent-browser --session-name 104-sourcing eval "JSON.stringify(window._qualified)"

將兩份資料合併為:
```json
[
  {
    "idNo": "...",
    "name": "...",
    "age": 28,
    "expJobArr": [...],
    "intro": "...",
    "hopeSalary": "35,000~45,000",
    "expCats": "教育業:2年, 金融業:1年"
  },
  ...
]
寫入
/tmp/104_resumes.json
Group candidates into groups of 50 entries, use the Task tool to simultaneously launch multiple sub-agents (subagent_type:
general-purpose
):
Sample Task prompt (for each sub-agent):

Please read /tmp/104_resumes.json and analyze candidates from index {start} to {end} (0-indexed).

Recruitment Criteria:
- Position: {Job Title}
- Hard Requirements: {Criteria}
- Ideal Candidate: {Description}
- Red Flags: {Exclusion Criteria}
- Bonus Items: {nice-to-have}

For each candidate:
1. Score (1-5)
2. Recommendation Reason (one sentence)
3. Concerns (if any)

Return Format (JSON array):
[{"idNo":"...","score":4,"reason":"...","concern":"..."}]
After all sub-agents return results, the main session aggregates all scoring results, sorts them by score, and presents the in-depth evaluation report:
undefined

並發啟動 Sub-agents 分析

In-Depth Filter Results (Total N Candidates, Sorted by Recommendation Score)

將候選人分組,每組 50 筆,用 Task tool 同時啟動多個 sub-agents(subagent_type:
general-purpose
):
Task prompt 範例(每個 sub-agent):

請讀取 /tmp/104_resumes.json,分析第 {start} 到第 {end} 筆候選人(0-indexed)。

招募條件:
- 職位:{職位}
- 硬性要求:{條件}
- 理想候選人:{描述}
- 地雷:{排除條件}
- 加分:{nice-to-have}

請針對每位候選人:
1. 評分(1-5)
2. 推薦原因(一句話)
3. 疑慮(若有)

回傳格式(JSON array):
[{"idNo":"...","score":4,"reason":"...","concern":"..."}]
主 session 等所有 sub-agents 回傳後,彙整所有評分結果,以分數排序後呈現深度版評估報告:
undefined
#NameScoreAgeDesired SalaryIndustry BackgroundRecommendation ReasonConcerns
1Wang Xiaoming★★★★★2840,000~50,0002 years in education industryMeets ideal backgroundNone
...

---

深度篩選結果(共 N 位,按推薦分數排序)

Notes

#姓名評分年齡希望薪資產業背景推薦原因疑慮
1王小明★★★★★284~5萬教育業2年符合理想背景
...

---
  • eval only supports ES5: Do not use
    const/let
    , arrow functions, or template literals, otherwise SyntaxError will be thrown
  • API fetch requests must be called within the main session (104-sourcing): Independent sessions of Subagents do not have login cookies
  • expJobNote
    contains HTML tags; use
    .replace(/<[^>]+>/g, '')
    to clean it before analysis
  • fixedUpdateDate
    must be obtained from the response of Page 1 and used for all subsequent page requests
  • If the session expires (fetch returns "Not logged in"), re-execute the login process in Stage 2
  • Do not add "人才" (talent) to search keywords: The 104 search mechanism is not precise enough; adding "人才" will easily result in HR positions like "Talent Specialist" or "Talent Consultant", which interfere with results. Use only the job title instead (e.g., Telemarketing, Sales, Engineer)
  • Must use isDuplicate=0 for saveResume:
    -1
    is account-level duplicate check (candidates saved in any folder are considered duplicates), which will cause a large number of candidates to be skipped silently;
    0
    will force save to the target folder
  • Max 50 entries per batch: The actual maximum number of successfully saved entries per saveResume request is 50; entries exceeding this limit will be listed in
    params.fail
    without error prompts, so be sure to batch in units of 50
  • Use synchronous XHR instead of async fetch for saveResume: Async fetch requires additional sleep + reading window variables, which will be lost once page navigation occurs; synchronous XHR returns results directly and is more reliable
  • Page navigation will clear all window variables: As long as
    open
    is executed to switch pages,
    window._qualified
    ,
    window._allCandidates
    , etc. will all be lost, and you need to re-fetch and filter

注意事項

  • eval 只能用 ES5:不可用
    const/let
    、arrow function、template literal,否則報 SyntaxError
  • API fetch 必須在主 session(104-sourcing)內呼叫:Subagent 的獨立 session 沒有登入 cookie
  • expJobNote
    含 HTML 標籤,分析前用
    .replace(/<[^>]+>/g, '')
    清除
  • fixedUpdateDate
    必須從第 1 頁回應取得後,用於所有後續分頁請求
  • 若 session 失效(fetch 回傳「尚未登入」),重新執行第二階段登入流程
  • 搜尋關鍵字不要加「人才」兩字:104 搜尋機制不夠精準,加了「人才」後容易搜到「人才專員」「人才顧問」等人資職缺,反而干擾結果。直接用職位名稱即可(例如:電銷、業務、工程師)
  • saveResume 必須用 isDuplicate=0
    -1
    是帳號層級的重複檢查(只要曾存過任何資料夾就算重複),會導致大量候選人被靜默跳過;
    0
    才會強制儲存到目標資料夾
  • 每批最多 50 筆:saveResume 每次請求實際成功儲存上限為 50 筆,超過的會列在
    params.fail
    但不報錯,務必以 50 為單位分批
  • saveResume 用同步 XHR,不用 async fetch:async fetch 需要額外的 sleep + 讀取 window 變數,一旦中間導航頁面就會遺失;同步 XHR 直接回傳結果,更可靠
  • 頁面導航會清空所有 window 變數:只要執行
    open
    切換頁面,
    window._qualified
    window._allCandidates
    等全部消失,需重新抓取篩選