Skip to content

Commit 20e024a

Browse files
feat(maimai): add talent search with multi-dimensional filters (#977)
* feat(maimai): add talent search with multi-dimensional filters Add maimai.cn talent search adapter with support for: - Keyword search (query) - Company filtering (multiple companies supported) - School filtering (with 985/211 options) - Location filtering (province/city) - Work experience and education level filters - Industry and position filters - Direct chat availability - Sort by relevance, activity, work years, or education Features: - Reuses Chrome login session for authentication - Extracts candidate info: name, job title, company, work history - Shows work years, education, age, active status - Displays skill tags and mutual friends count * fix docs and strategy for maimai adapter --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 01957bf commit 20e024a

4 files changed

Lines changed: 271 additions & 11 deletions

File tree

clis/maimai/search-talents.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* Maimai talent search - Browser cookie API.
3+
* Reuses Chrome login session to search for candidates on maimai.cn
4+
*/
5+
import { cli, Strategy } from '@jackwener/opencli/registry';
6+
7+
cli({
8+
site: 'maimai',
9+
name: 'search-talents',
10+
description: 'Search for candidates on Maimai with multi-dimensional filters',
11+
domain: 'maimai.cn',
12+
strategy: Strategy.COOKIE,
13+
browser: true,
14+
args: [
15+
{ name: 'query', positional: true, required: true, help: 'Search keyword (e.g., "Java", "产品经理")' },
16+
{ name: 'page', type: 'int', default: 0, help: 'Page number (0-based)' },
17+
{ name: 'size', type: 'int', default: 20, help: 'Results per page' },
18+
{ name: 'positions', help: 'Positions (e.g., "运营", "Java 开发工程师")' },
19+
{ name: 'companies', help: 'Companies, comma-separated (e.g., "百度", "字节跳动,阿里巴巴")' },
20+
{ name: 'schools', help: 'Schools, comma-separated (e.g., "北京大学", "清华大学,复旦大学")' },
21+
{ name: 'provinces', help: 'Provinces (e.g., "北京", "上海")' },
22+
{ name: 'cities', help: 'Cities (e.g., "北京市", "上海市")' },
23+
{ name: 'worktimes', help: 'Work years: 1=1-3y, 2=3-5y, 3=5-10y, 4=10+y' },
24+
{ name: 'degrees', help: 'Education: 1=大专,2=本科,3=硕士,4=博士,5=MBA' },
25+
{ name: 'professions', help: 'Industries: 01=互联网,02=金融,03=电子,04=通信' },
26+
{ name: 'is_211', type: 'int', help: '211 university: 0=any, 1=211' },
27+
{ name: 'is_985', type: 'int', help: '985 university: 0=any, 1=985' },
28+
{ name: 'sortby', type: 'int', default: 0, help: 'Sort: 0=relevance, 1=activity, 2=work_years, 3=education' },
29+
{ name: 'is_direct_chat', type: 'int', default: 0, help: 'Direct chat: 0=any, 1=available' },
30+
],
31+
columns: ['name', 'job_title', 'company', 'historical_companies', 'location', 'work_year', 'school', 'degree', 'active_status', 'age', 'tags', 'mutual_friends'],
32+
func: async (page, kwargs) => {
33+
const {
34+
query,
35+
page: pageNum = 0,
36+
size = 20,
37+
positions = '',
38+
companies = '',
39+
schools = '',
40+
provinces = '',
41+
cities = '',
42+
worktimes = '',
43+
degrees = '',
44+
professions = '',
45+
is_211 = 0,
46+
is_985 = 0,
47+
sortby = 0,
48+
is_direct_chat = 0,
49+
} = kwargs;
50+
51+
// Navigate to the search page
52+
await page.goto('https://maimai.cn/ent/talents/discover/search_v2', { waitUntil: 'networkidle' });
53+
await page.waitForTimeout(5000);
54+
55+
// Generate random session IDs
56+
const sessionid = 'b92d0fb5-f3fd-1f4b-fcdc-' + Math.random().toString(16).slice(2, 14);
57+
const deletesessionid = 'ae907d75-315c-8db7-2cc7-' + Math.random().toString(16).slice(2, 14);
58+
59+
const requestBody = {
60+
search: {
61+
page: pageNum,
62+
size: size,
63+
sessionid: sessionid,
64+
deletesessionid: deletesessionid,
65+
worktimes: worktimes,
66+
degrees: degrees,
67+
professions: professions,
68+
schools: schools,
69+
positions: positions,
70+
companyscope: 0,
71+
sortby: sortby,
72+
is_direct_chat: is_direct_chat,
73+
query: query,
74+
cities: cities,
75+
provinces: provinces,
76+
is_211: is_211,
77+
is_985: is_985,
78+
allcompanies: companies,
79+
},
80+
};
81+
82+
// Execute the search API call in browser context
83+
const data = await page.evaluate(async (body) => {
84+
// Get CSRF token from cookie or meta tag
85+
let csrftoken = document.cookie.split('; ')
86+
.find(row => row.startsWith('csrftoken='))
87+
?.split('=')[1] || '';
88+
89+
if (!csrftoken) {
90+
const meta = document.querySelector('meta[name="csrf-token"]');
91+
if (meta) csrftoken = meta.getAttribute('content') || '';
92+
}
93+
94+
const res = await fetch('https://maimai.cn/api/ent/discover/search?channel=www&data_version=3.0&version=1.0.0', {
95+
method: 'POST',
96+
headers: {
97+
'accept': '*/*',
98+
'content-type': 'text/plain;charset=UTF-8',
99+
'origin': 'https://maimai.cn',
100+
'referer': 'https://maimai.cn/ent/talents/discover/search_v2',
101+
'x-csrf-token': csrftoken,
102+
},
103+
credentials: 'include',
104+
body: JSON.stringify(body),
105+
});
106+
107+
const result = await res.json();
108+
109+
// Check login status
110+
if (res.status === 401 || res.status === 403 || result.error_code === 20002) {
111+
throw new Error('需要登录!请先在浏览器中访问 maimai.cn 并登录');
112+
}
113+
114+
if (result.code !== 200 && result.code !== 0) {
115+
throw new Error(result.message || result.error || 'API 请求失败');
116+
}
117+
118+
return result;
119+
}, requestBody);
120+
121+
// Extract talent list from response
122+
const talentList = data.data?.list || data.data?.talent_list || data.list || data.talent_list || [];
123+
124+
if (!talentList || talentList.length === 0) {
125+
return [{ error: '未找到匹配的候选人', query: query }];
126+
}
127+
128+
// Map to output format
129+
return talentList.map(item => {
130+
// Extract school info (first one)
131+
const schoolInfo = item.edu && item.edu.length > 0 ? item.edu[0] : {};
132+
133+
// Work years: use work_time field directly (e.g., "11 年", "10 年")
134+
const workYear = item.work_time || item.worktime || '';
135+
136+
// Extract all companies from work experience (deduplicated, excluding current company)
137+
const currentCompany = item.company || '';
138+
const historicalCompanies = (item.exp || [])
139+
.map(e => e.company)
140+
.filter(c => c && c.trim() !== '' && c !== currentCompany)
141+
.filter((c, i, arr) => arr.indexOf(c) === i)
142+
.join(' / ');
143+
144+
// Extract tags/skills from tag_list array
145+
const tags = (item.tag_list || item.tags || [])
146+
.filter(t => t && t.trim() !== '')
147+
.join(', ');
148+
149+
// Extract mutual friends count and list
150+
const mutualFriendsCount = item.friends_cnt || item.common_friends_count || 0;
151+
const mutualFriendsList = (item.friends || item.common_friends || [])
152+
.map(f => f.name || f.user_name || f)
153+
.slice(0, 3)
154+
.join(', ');
155+
156+
return {
157+
name: item.name || '',
158+
job_title: item.position || item.job_title || '',
159+
company: currentCompany,
160+
historical_companies: historicalCompanies,
161+
location: (item.province || '') + (item.city ? '·' + item.city : ''),
162+
work_year: workYear,
163+
school: schoolInfo.school || schoolInfo.hover?.name || '',
164+
degree: schoolInfo.sdegree || schoolInfo.hover?.school_level || '',
165+
active_status: item.active_state_v2 || item.active_state_v1 || item.active_state || '',
166+
age: item.age || '',
167+
tags: tags,
168+
mutual_friends: mutualFriendsCount > 0 ? `${mutualFriendsCount}${mutualFriendsList ? ' (' + mutualFriendsList + ')' : ''}` : '',
169+
};
170+
});
171+
},
172+
});

docs/adapters/browser/binance.md

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,51 @@
22

33
Access **Binance** market data from the terminal via the public API (no authentication required).
44

5+
**Mode**: 🌐 Public · **Domain**: `data-api.binance.vision`
6+
57
## Commands
68

79
| Command | Description |
810
|---------|-------------|
9-
| `opencli binance price SYMBOL` | Quick price check for a trading pair |
10-
| `opencli binance prices` | Latest prices for all trading pairs |
11-
| `opencli binance ticker` | 24h ticker statistics for top pairs by volume |
12-
| `opencli binance top` | Top trading pairs by 24h volume |
13-
| `opencli binance gainers` | Top gaining pairs by 24h price change |
14-
| `opencli binance losers` | Top losing pairs by 24h price change |
15-
| `opencli binance pairs` | List active trading pairs |
16-
| `opencli binance klines SYMBOL` | Candlestick/kline data for a trading pair |
17-
| `opencli binance depth SYMBOL` | Order book bid prices |
18-
| `opencli binance asks SYMBOL` | Order book ask prices |
19-
| `opencli binance trades SYMBOL` | Recent trades for a trading pair |
11+
| `opencli binance price` | Get 24h ticker stats for one symbol |
12+
| `opencli binance prices` | Get latest prices for all symbols |
13+
| `opencli binance ticker` | Get 24h ticker stats for all symbols |
14+
| `opencli binance pairs` | List exchange trading pairs |
15+
| `opencli binance trades` | Get recent trades for one symbol |
16+
| `opencli binance depth` | Get order-book depth for one symbol |
17+
| `opencli binance asks` | Show ask-side depth for one symbol |
18+
| `opencli binance klines` | Get candlestick data |
19+
| `opencli binance top` | Show top movers by volume |
20+
| `opencli binance gainers` | Show top gainers |
21+
| `opencli binance losers` | Show top losers |
22+
23+
## Usage Examples
24+
25+
```bash
26+
# One symbol, 24h stats
27+
opencli binance price BTCUSDT
28+
29+
# Latest prices for all pairs
30+
opencli binance prices
31+
32+
# Recent trades
33+
opencli binance trades BTCUSDT --limit 20
34+
35+
# Order-book depth
36+
opencli binance depth BTCUSDT --limit 20
37+
38+
# 1h candles
39+
opencli binance klines BTCUSDT --interval 1h --limit 50
40+
41+
# JSON output
42+
opencli binance top -f json
43+
```
44+
45+
## Prerequisites
46+
47+
- No browser required — uses Binance public market-data endpoints
48+
49+
## Notes
50+
51+
- Symbols use Binance market format such as `BTCUSDT` or `ETHUSDT`
52+
- Public market-data endpoints can still be rate-limited upstream; retry if you hit transient failures

docs/adapters/browser/maimai.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Maimai (脉脉)
2+
3+
**Mode**: 🔐 Browser · **Domain**: `maimai.cn`
4+
5+
## Commands
6+
7+
| Command | Description |
8+
|---------|-------------|
9+
| `opencli maimai search-talents` | Search Maimai talent profiles with keyword and structured filters |
10+
11+
## Usage Examples
12+
13+
```bash
14+
# Search by keyword
15+
opencli maimai search-talents Java
16+
17+
# Narrow by company and city
18+
opencli maimai search-talents 产品经理 --companies "阿里巴巴,字节跳动" --cities 北京市
19+
20+
# Filter by school, degree, and work years
21+
opencli maimai search-talents 算法 --schools "北京大学,清华大学" --degrees 3 --worktimes 3
22+
23+
# Prioritize recently active candidates
24+
opencli maimai search-talents 运营 --sortby 1 --is_direct_chat 1
25+
26+
# JSON output for downstream processing
27+
opencli maimai search-talents Java --size 10 -f json
28+
```
29+
30+
## Key Filters
31+
32+
- `--positions`: filter by role or title
33+
- `--companies`: comma-separated current or historical companies
34+
- `--schools`: comma-separated school names
35+
- `--provinces` / `--cities`: location filters
36+
- `--worktimes`: `1=1-3y`, `2=3-5y`, `3=5-10y`, `4=10+y`
37+
- `--degrees`: `1=大专`, `2=本科`, `3=硕士`, `4=博士`, `5=MBA`
38+
- `--professions`: industry codes such as `01=互联网`, `02=金融`
39+
- `--is_211` / `--is_985`: set to `1` to require those school tiers
40+
- `--sortby`: `0=relevance`, `1=activity`, `2=work_years`, `3=education`
41+
- `--is_direct_chat`: set to `1` to keep only candidates available for direct chat
42+
43+
## Prerequisites
44+
45+
- Chrome running and **logged into** `maimai.cn`
46+
- The logged-in browser should be able to open `https://maimai.cn/ent/talents/discover/search_v2`
47+
- [Browser Bridge extension](/guide/browser-bridge) installed
48+
49+
## Notes
50+
51+
- `page` is zero-based, so the first page is `--page 0`
52+
- Output includes current company plus a deduplicated `historical_companies` field from work experience
53+
- If the command reports login failure, first verify the same Chrome profile can access the talent search page directly

docs/adapters/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Run `opencli list` for the live registry.
3232
| **[chaoxing](./browser/chaoxing.md)** | `assignments` `exams` | 🔐 Browser |
3333
| **[grok](./browser/grok.md)** | `ask` | 🔐 Browser |
3434
| **[gemini](./browser/gemini.md)** | `new` `ask` `image` `deep-research` `deep-research-result` | 🔐 Browser |
35+
| **[maimai](./browser/maimai.md)** | `search-talents` | 🔐 Browser |
3536
| **[yuanbao](./browser/yuanbao.md)** | `new` `ask` | 🔐 Browser |
3637
| **[notebooklm](./browser/notebooklm.md)** | `status` `list` `open` `current` `get` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-get` `summary` | 🔐 Browser |
3738
| **[doubao](./browser/doubao.md)** | `status` `new` `send` `read` `ask` `history` `detail` `meeting-summary` `meeting-transcript` | 🔐 Browser |
@@ -76,6 +77,7 @@ Run `opencli list` for the live registry.
7677
| **[arxiv](./browser/arxiv.md)** | `search` `paper` | 🌐 Public |
7778
| **[paperreview](./browser/paperreview.md)** | `submit` `review` `feedback` | 🌐 Public |
7879
| **[barchart](./browser/barchart.md)** | `quote` `options` `greeks` `flow` | 🌐 Public |
80+
| **[binance](./browser/binance.md)** | `price` `prices` `ticker` `pairs` `trades` `depth` `asks` `klines` `top` `gainers` `losers` | 🌐 Public |
7981
| **[hf](./browser/hf.md)** | `top` | 🌐 Public |
8082
| **[sinafinance](./browser/sinafinance.md)** | `news` | 🌐 Public |
8183
| **[spotify](./browser/spotify.md)** | `auth` `status` `play` `pause` `next` `prev` `volume` `search` `queue` `shuffle` `repeat` | 🔑 OAuth API |

0 commit comments

Comments
 (0)