104 HR Talent Search
Overview
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
CLI. For installation instructions, refer to the
field in the frontmatter.
Command Reference:
references/agent-browser.md
Architecture Principles (Important)
- 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 instead of , 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)
- Job Title: What position are you searching for? (e.g., Telemarketing, Sales, Engineer)
- Work Location: What work locations can candidates accept? (e.g., Taipei City, Banqiao District, New Taipei City)
- Hard Requirements: What are the necessary conditions? (e.g., work experience, education background, specific skills or certifications)
- Employment Status: Do you only want to view job seekers in specific employment statuses? (Default: No restriction)
- No restriction:
- Employed (seeking job change):
- Unemployed (available immediately):
Qualitative Criteria (For Post-Filter Evaluation)
- Ideal Candidate Profile: What traits or backgrounds are most valued for this position? What past experiences are particularly preferred?
- Red Flags: What situations need to be excluded? (e.g., frequent job changes, specific industry backgrounds, long employment gaps)
- 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 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
The browser only needs to log in; there is no need to use the UI to perform search or set filter criteria.
Steps
-
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
-
Get current status
bash
agent-browser --session-name 104-sourcing snapshot -i
Interpret the snapshot output to determine the current page status:
-
If already on vip.104.com.tw (Recruitment Management Platform) → directly proceed to Stage 3, no need to log in
-
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
-
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
-
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
Stage 3: API-only Search & Filter
After login is completed, use eval + fetch API for all operations, no longer operate the browser UI.
Known API Endpoints
| Purpose | Endpoint |
|---|
| 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
|
Employment Status Codes (empStatus Parameter)
| Value | Description |
|---|
| No restriction |
| Employed (seeking job change; is usually "Still employed") |
| Unemployed (already resigned, available immediately; shows duration of unemployment) |
City Codes
Complete code list can be found in
references/area.json, supporting Taiwan, mainland China, and overseas regions. County/city level codes end with three
s, district level codes end with serial numbers.
Common County/City Quick Reference:
| Location | city Parameter Value |
|---|
| Taipei City | |
| New Taipei City | |
| Keelung City | |
| Taoyuan City | |
| Hsinchu County/City | |
| Taichung City | |
| Tainan City | |
| Kaohsiung City | |
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...'
"
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:
must be obtained from the response of Page 1, and this value must be included in all subsequent page requests to ensure result consistency.
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...'
"
Wait and confirm:
bash
sleep 8 && agent-browser --session-name 104-sourcing eval "JSON.stringify({done:window._allDone, total:window._allCandidates.length})"
Step 3: Direct Filtering in JS, No Need to Call Individual Resume APIs
Each entry in the search results already contains the complete
, which can be directly filtered in memory:
bash
agent-browser --session-name 104-sourcing eval "
# 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+')'; })
};
})
})"
Reference for Candidate Fields Returned by Search API
| Field | Description |
|---|
| Candidate ID (used to call individual resume API) |
| Name |
| Age |
| Gender |
| Education background (including school name; use to get abbreviation) |
| Desired work location (multiple locations separated by 「、」) |
| Total work experience description |
| Desired job title category |
| Work experience array (including , , , , ) |
Step 4: Batch Save Qualified Candidates to Folder
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"
Present the results to the user in a table:
| # | Folder Name | folderNo |
|---|
| 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
to proceed to the next step.
Batch Save All Qualified Candidates
Important: Each API call can successfully save a maximum of
50 entries; entries exceeding this limit will fail silently (listed in
).
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
# Save in batches of 50 entries, use synchronous XHR to get direct results
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
to
,
... and so on to complete all batches.
Response Format Description:
- → All saved successfully
code: 6, type: dataDuplicated
→ Only appears when isDuplicate=-1; 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)
- → Number of candidates in single request exceeds platform limit
Description of saveResume POST Parameters:
| Parameter | Value | Description |
|---|
| | Operation type code (save from search page, fixed value) |
| | Target folder ID |
| | Source page (fixed value) |
| | Must use 0; will skip any candidates that have been saved in any folder under the account, resulting in a large number of missed candidates; will force save to the target folder |
contentInfo[searchEngine]
| | Candidate idNo, separated by commas, max 50 entries per batch |
| Empty | Snapshot ID (fixed empty for search page) |
Candidate Evaluation Report Format (Basic Version)
## Filter Results (Total N Qualified Candidates)
| # | Name | Age | Education | Desired Location | Total Work Experience | Relevant Experience |
|---|------|------|------|----------|----------|----------|
| 1 | Wang Xiaoming | 35 | University | Taipei City | 8~9 years | Telemarketing Supervisor@XX Company (3 years) |
...
**Recommended for Contact**: #1 Wang Xiaoming, #3 ...
**Recommended to Skip**: #2 ... (Reason: Location mismatch)
Step 3.5 (Optional): In-Depth Resume Analysis
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:
Batch Fetch Individual Resumes (50 Entries per Batch)
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
# 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"
Change
to
,
... to complete all batches.
Number of batches =
Math.ceil(qualified.length / 50)
, sleep 10 seconds between batches.
Merge Data and Write to Temporary File
After all batches are completed, merge
(basic data) with the collected individual resumes, and write to
.
json
[
{
"idNo": "...",
"name": "...",
"age": 28,
"expJobArr": [...],
"intro": "...",
"hopeSalary": "35,000~45,000",
"expCats": "教育業:2年, 金融業:1年"
},
...
]
Launch Sub-agents for Concurrent Analysis
Group candidates into groups of 50 entries, use the Task tool to
simultaneously launch multiple sub-agents (subagent_type:
):
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:
## In-Depth Filter Results (Total N Candidates, Sorted by Recommendation Score)
| # | Name | Score | Age | Desired Salary | Industry Background | Recommendation Reason | Concerns |
|---|------|------|------|----------|----------|----------|------|
| 1 | Wang Xiaoming | ★★★★★ | 28 | 40,000~50,000 | 2 years in education industry | Meets ideal background | None |
...
Notes
- eval only supports ES5: Do not use , 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
- contains HTML tags; use to clean it before analysis
- 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: 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; 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 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 is executed to switch pages, , , etc. will all be lost, and you need to re-fetch and filter