بنقرة واحدة
104-sourcing
自動登入 104 招募管理平台,依據人資需求搜尋並篩選合適候選人。使用時機:當人資需要在 104 平台搜尋求職者、篩選條件、查看 Candidate 資料時。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
自動登入 104 招募管理平台,依據人資需求搜尋並篩選合適候選人。使用時機:當人資需要在 104 平台搜尋求職者、篩選條件、查看 Candidate 資料時。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | 104-sourcing |
| description | 自動登入 104 招募管理平台,依據人資需求搜尋並篩選合適候選人。使用時機:當人資需要在 104 平台搜尋求職者、篩選條件、查看 Candidate 資料時。 |
| compatibility | 需要 agent-browser 已全域安裝(npm install -g agent-browser && agent-browser install) |
| metadata | {"author":"orangeapple","version":"5.0"} |
本 Skill 使用 agent-browser CLI 控制瀏覽器,自動登入 104 招募管理平台並搜尋合適人選。執行流程分三個階段:需求訪談 → 登入 → 純 API 搜尋篩選。
依賴工具:本 Skill 需要
agent-browserCLI,安裝方式見 frontmatter 的compatibility欄位。 指令參考:references/agent-browser.md
var 不能用 const/let,不能用 arrow function,否則 eval 會報錯在開啟瀏覽器前,必須先透過訪談了解招募需求,不得假設或跳過。訪談分兩層:
empStatus=0empStatus=1empStatus=2訪談結束後,整理並回覆「搜尋策略確認」:
kws 參數用戶確認後,再進入第二階段。
瀏覽器只需登入,不需要用 UI 操作搜尋或篩選條件。
直接開啟招募管理平台(帶現有 session,看是否已登入)
agent-browser --session-name 104-sourcing open https://vip.104.com.tw/rms/index
取得目前狀態
agent-browser --session-name 104-sourcing snapshot -i
解讀 snapshot 輸出,判斷目前頁面狀態:
若已在 vip.104.com.tw(招募管理平台)→ 直接進入第三階段,不需要登入
若被導向登入頁 → 向用戶索取帳號密碼,注意兩種情況:
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
若出現 MFA(snapshot 中有 OTP 輸入)
agent-browser --session-name 104-sourcing fill @eX "驗證碼"agent-browser --session-name 104-sourcing press Enter確認已進入 vip.104.com.tw
登入完成後,全程用 eval + fetch API,不再操作瀏覽器 UI。
| 用途 | 端點 |
|---|---|
| 搜尋候選人列表(含工作經歷) | 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 |
| 值 | 說明 |
|---|---|
0 | 不限 |
1 | 在職中(想跳槽;expJobArr[0].expEndDesc 通常為「仍在職」) |
2 | 待業中(已離職,即可上班;expEndDesc 顯示離職時長) |
完整代碼清單見 references/area.json,支援台灣、大陸、海外地區。縣市層代碼末三位為 000,行政區層末三位為流水號。
常用縣市快查:
| 地點 | city 參數值 |
|---|---|
| 台北市 | 6001001000 |
| 新北市 | 6001002000 |
| 基隆市 | 6001004000 |
| 桃園市 | 6001005000 |
| 新竹縣市 | 6001006000 |
| 台中市 | 6001008000 |
| 台南市 | 6001014000 |
| 高雄市 | 6001016000 |
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...'
"
等待後確認:
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 頁回應取得,後續所有分頁都要帶這個值,確保結果一致性。
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...'
"
等待後確認:
sleep 8 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._allDone, total:window._allCandidates.length})"
搜尋結果的每筆資料已包含完整 expJobArr,可直接在記憶體中篩選:
agent-browser --session-name 104-sourcing eval "
# 範例:篩選職稱含「電銷」且年資 >= 1 年的候選人
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+')'; })
};
})
})"
| 欄位 | 說明 |
|---|---|
idNo | 候選人 ID(用於呼叫個別履歷 API) |
userName | 姓名 |
age | 年齡 |
sexDesc | 性別 |
eduDesc | 學歷(含學校名,可用 .split(' ')[0] 取簡稱) |
wcityNoDesc | 希望工作地點(多個以「、」分隔) |
expPeriodDesc | 總工作年資描述 |
titleCatDesc | 希望職稱類別 |
expJobArr | 工作經歷陣列(含 expTitle, expFirm, expJobNote, expEndDesc, expPeriod) |
篩選完成後,一次 API 呼叫即可批次儲存所有合格候選人,不需要 UI 操作。
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 執行下一步。
重要:每次 API 呼叫最多成功儲存 50 筆,超過的會靜默失敗(在
params.fail中)。 必須以 50 筆為單位分批送出,並使用同步 XHR 取得即時結果(async fetch + window 變數在頁面導航後會消失)。
# 以每批 50 筆為單位,用同步 XHR 儲存,直接取得結果
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(搜尋頁固定留空) |
## 篩選結果(共 N 位符合條件)
| # | 姓名 | 年齡 | 學歷 | 希望地點 | 工作年資 | 相關經歷 |
|---|------|------|------|----------|----------|----------|
| 1 | 王小明 | 35 | 大學 | 台北市 | 8~9年 | 電銷主管@XX公司(3年) |
...
**建議聯絡**:#1 王小明、#3 ...
**建議略過**:#2 ...(原因:地點不符)
顯示基本篩選結果後,詢問用戶是否進行深度分析:
「目前根據搜尋列表資料篩出 N 位候選人。是否進行深度履歷分析?(額外讀取自我介紹、希望薪資、產業年資,耗時較長但評估更精準)」
用戶選擇「是」後,依序執行:
以 50 筆為一批,批次間 sleep 5 秒,避免 rate limit。主 session 收集每批 JSON:
# 第 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 秒。
所有批次完成後,將 window._qualified(基本資料)與累積的個別履歷合併,寫入暫存檔:
# 主 session 中取出 _qualified 基本資料
agent-browser --session-name 104-sourcing eval "JSON.stringify(window._qualified)"
將兩份資料合併為:
[
{
"idNo": "...",
"name": "...",
"age": 28,
"expJobArr": [...],
"intro": "...",
"hopeSalary": "35,000~45,000",
"expCats": "教育業:2年, 金融業:1年"
},
...
]
寫入 /tmp/104_resumes.json。
將候選人分組,每組 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 回傳後,彙整所有評分結果,以分數排序後呈現深度版評估報告:
## 深度篩選結果(共 N 位,按推薦分數排序)
| # | 姓名 | 評分 | 年齡 | 希望薪資 | 產業背景 | 推薦原因 | 疑慮 |
|---|------|------|------|----------|----------|----------|------|
| 1 | 王小明 | ★★★★★ | 28 | 4~5萬 | 教育業2年 | 符合理想背景 | 無 |
...
const/let、arrow function、template literal,否則報 SyntaxErrorexpJobNote 含 HTML 標籤,分析前用 .replace(/<[^>]+>/g, '') 清除fixedUpdateDate 必須從第 1 頁回應取得後,用於所有後續分頁請求-1 是帳號層級的重複檢查(只要曾存過任何資料夾就算重複),會導致大量候選人被靜默跳過;0 才會強制儲存到目標資料夾params.fail 但不報錯,務必以 50 為單位分批open 切換頁面,window._qualified、window._allCandidates 等全部消失,需重新抓取篩選