Skip to content

Commit 3669a89

Browse files
fix: fix social adapter bugs, sync docs, refactor boss common utils (#204)
* refactor(boss): extract common utilities, fix missing login detection - Add src/clis/boss/common.ts with shared helpers: - bossFetch(): unified XHR template with auto cookie-expiry detection (code 7/37) - navigateToChat()/navigateTo(): page navigation helpers - checkAuth()/assertOk(): centralized login state validation - fetchFriendList()/fetchRecommendList()/findFriendByUid(): data queries - clickCandidateInList()/typeAndSendMessage(): UI automation helpers - verbose(): conditional debug logging - Refactor all 14 boss adapters to use common.ts: - chatlist.ts: was missing cookie-expiry check (fixes #login-detect) - chatmsg.ts: was missing cookie-expiry check (fixes #login-detect) - Remaining 12 adapters: deduplicated XHR boilerplate and error handling - Fix execution.ts: skip redundant pre-navigation for TS adapters - TS adapters handle their own goto(), pre-navigating caused double page loads and could trigger duplicate login prompts - Pre-navigation preserved for YAML pipeline commands that need it Net reduction: ~730 lines of duplicated code across boss adapters. All 244 unit tests pass. * fix: fix social adapter bugs and sync docs with implementation Instagram: - Remove 6 non-existent commands from docs (like/unlike/comment/save/unsave/follow/unfollow) - Fix usage examples to use positional args Facebook: - Remove 6 non-existent commands from docs (friends/groups/memories/events/add-friend/join-group) - Fix search.yaml: URL encode query param, add missing url column - Fix feed.yaml: add English locale support for engagement regex patterns TikTok: - Fix save/unsave: replace broken data-e2e="undefined-icon" with bookmark-icon/collect-icon - Fix like/unlike: add state detection to prevent toggling (checks aria-label + computed color) - Fix notifications: rewrite nested setTimeout to async/await - Fix comment: add post-comment verification, throw on missing post button --------- Co-authored-by: Wing Huang <huangsen365@gmail.com>
1 parent eb0ccaf commit 3669a89

26 files changed

Lines changed: 594 additions & 1300 deletions

docs/adapters/browser/facebook.md

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,12 @@
1010
| `opencli facebook notifications` | Get recent notifications |
1111
| `opencli facebook feed` | Get news feed posts |
1212
| `opencli facebook search` | Search people, pages, posts |
13-
| `opencli facebook friends` | Friend suggestions |
14-
| `opencli facebook groups` | List your joined groups |
15-
| `opencli facebook memories` | On This Day memories |
16-
| `opencli facebook events` | Browse event categories |
17-
| `opencli facebook add-friend` | Send a friend request |
18-
| `opencli facebook join-group` | Join a group |
1913

2014
## Usage Examples
2115

2216
```bash
2317
# View a profile
24-
opencli facebook profile --username zuck
18+
opencli facebook profile zuck
2519

2620
# Get notifications
2721
opencli facebook notifications --limit 10
@@ -30,19 +24,10 @@ opencli facebook notifications --limit 10
3024
opencli facebook feed --limit 5
3125

3226
# Search
33-
opencli facebook search --query "OpenAI" --limit 5
34-
35-
# List your groups
36-
opencli facebook groups
37-
38-
# Send friend request
39-
opencli facebook add-friend --username someone
40-
41-
# Join a group
42-
opencli facebook join-group --group 123456789
27+
opencli facebook search "OpenAI" --limit 5
4328

4429
# JSON output
45-
opencli facebook profile --username zuck -f json
30+
opencli facebook profile zuck -f json
4631
```
4732

4833
## Prerequisites

docs/adapters/browser/instagram.md

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,31 @@
1313
| `opencli instagram followers` | List user's followers |
1414
| `opencli instagram following` | List user's following |
1515
| `opencli instagram saved` | Get your saved posts |
16-
| `opencli instagram like` | Like a post |
17-
| `opencli instagram unlike` | Unlike a post |
18-
| `opencli instagram comment` | Comment on a post |
19-
| `opencli instagram save` | Bookmark a post |
20-
| `opencli instagram unsave` | Remove bookmark |
21-
| `opencli instagram follow` | Follow a user |
22-
| `opencli instagram unfollow` | Unfollow a user |
2316

2417
## Usage Examples
2518

2619
```bash
2720
# View a user's profile
28-
opencli instagram profile --username nasa
21+
opencli instagram profile nasa
2922

3023
# Search users
31-
opencli instagram search --query nasa --limit 5
24+
opencli instagram search nasa --limit 5
3225

3326
# View a user's recent posts
34-
opencli instagram user --username nasa --limit 10
27+
opencli instagram user nasa --limit 10
3528

36-
# Like a user's most recent post
37-
opencli instagram like --username nasa --index 1
29+
# Discover trending posts
30+
opencli instagram explore --limit 20
3831

39-
# Comment on a post
40-
opencli instagram comment --username nasa --text "Amazing!" --index 1
32+
# List followers/following
33+
opencli instagram followers nasa --limit 20
34+
opencli instagram following nasa --limit 20
4135

42-
# Follow/unfollow
43-
opencli instagram follow --username nasa
44-
opencli instagram unfollow --username nasa
36+
# Get your saved posts
37+
opencli instagram saved --limit 10
4538

4639
# JSON output
47-
opencli instagram profile --username nasa -f json
40+
opencli instagram profile nasa -f json
4841
```
4942

5043
## Prerequisites

src/clis/boss/batchgreet.ts

Lines changed: 13 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
22
* BOSS直聘 batchgreet — batch greet recommended candidates.
3-
*
4-
* Combines recommend (greetRecSortList) + greet (UI automation).
5-
* Sends greeting messages to multiple candidates sequentially.
63
*/
74
import { cli, Strategy } from '../../registry.js';
8-
import type { IPage } from '../../types.js';
5+
import {
6+
requirePage, navigateToChat, fetchRecommendList,
7+
clickCandidateInList, typeAndSendMessage, verbose,
8+
} from './common.js';
99

1010
cli({
1111
site: 'boss',
@@ -20,44 +20,18 @@ cli({
2020
{ name: 'text', default: '', help: 'Custom greeting message (uses default if empty)' },
2121
],
2222
columns: ['name', 'status', 'detail'],
23-
func: async (page: IPage | null, kwargs) => {
24-
if (!page) throw new Error('Browser page required');
23+
func: async (page, kwargs) => {
24+
requirePage(page);
2525

2626
const filterJobId = kwargs['job-id'] || '';
2727
const limit = kwargs.limit || 5;
2828
const text = kwargs.text || '你好,请问您对这个职位感兴趣吗?';
2929

30-
if (process.env.OPENCLI_VERBOSE) {
31-
console.error(`[opencli:boss] Batch greeting up to ${limit} candidates...`);
32-
}
33-
34-
await page.goto('https://www.zhipin.com/web/chat/index');
35-
await page.wait({ time: 3 });
30+
verbose(`Batch greeting up to ${limit} candidates...`);
3631

37-
// Get recommended candidates
38-
const listData: any = await page.evaluate(`
39-
async () => {
40-
return new Promise((resolve, reject) => {
41-
const xhr = new XMLHttpRequest();
42-
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
43-
xhr.withCredentials = true;
44-
xhr.timeout = 15000;
45-
xhr.setRequestHeader('Accept', 'application/json');
46-
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
47-
xhr.onerror = () => reject(new Error('Network Error'));
48-
xhr.send();
49-
});
50-
}
51-
`);
32+
await navigateToChat(page, 3);
5233

53-
if (listData.code !== 0) {
54-
if (listData.code === 7 || listData.code === 37) {
55-
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
56-
}
57-
throw new Error(`获取推荐列表失败: ${listData.message}`);
58-
}
59-
60-
let candidates = listData.zpData?.friendList || [];
34+
let candidates = await fetchRecommendList(page);
6135
if (filterJobId) {
6236
candidates = candidates.filter((f: any) => f.encryptJobId === filterJobId);
6337
}
@@ -74,88 +48,21 @@ cli({
7448
const friendName = candidate.name || '候选人';
7549

7650
try {
77-
// Click on candidate
78-
const clicked: any = await page.evaluate(`
79-
async () => {
80-
const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
81-
if (item) {
82-
item.click();
83-
return { clicked: true };
84-
}
85-
const items = document.querySelectorAll('.geek-item');
86-
for (const el of items) {
87-
if (el.id && el.id.startsWith('_${numericUid}')) {
88-
el.click();
89-
return { clicked: true };
90-
}
91-
}
92-
return { clicked: false };
93-
}
94-
`);
95-
96-
if (!clicked.clicked) {
51+
const clicked = await clickCandidateInList(page, numericUid);
52+
if (!clicked) {
9753
results.push({ name: friendName, status: '❌ 跳过', detail: '在聊天列表中未找到' });
9854
continue;
9955
}
10056

10157
await page.wait({ time: 2 });
10258

103-
// Type message
104-
const typed: any = await page.evaluate(`
105-
async () => {
106-
const selectors = [
107-
'.chat-editor [contenteditable="true"]',
108-
'.chat-input [contenteditable="true"]',
109-
'[contenteditable="true"]',
110-
'textarea',
111-
];
112-
for (const sel of selectors) {
113-
const el = document.querySelector(sel);
114-
if (el && el.offsetParent !== null) {
115-
el.focus();
116-
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
117-
el.value = ${JSON.stringify(text)};
118-
el.dispatchEvent(new Event('input', { bubbles: true }));
119-
} else {
120-
el.textContent = '';
121-
el.focus();
122-
document.execCommand('insertText', false, ${JSON.stringify(text)});
123-
el.dispatchEvent(new Event('input', { bubbles: true }));
124-
}
125-
return { found: true };
126-
}
127-
}
128-
return { found: false };
129-
}
130-
`);
131-
132-
if (!typed.found) {
59+
const sent = await typeAndSendMessage(page, text);
60+
if (!sent) {
13361
results.push({ name: friendName, status: '❌ 失败', detail: '找不到消息输入框' });
13462
continue;
13563
}
13664

137-
await page.wait({ time: 0.5 });
138-
139-
// Click send
140-
const sent: any = await page.evaluate(`
141-
async () => {
142-
const btn = document.querySelector('.conversation-editor .submit')
143-
|| document.querySelector('.submit-content .submit')
144-
|| document.querySelector('.conversation-operate .submit');
145-
if (btn) {
146-
btn.click();
147-
return { clicked: true };
148-
}
149-
return { clicked: false };
150-
}
151-
`);
152-
153-
if (!sent.clicked) {
154-
await page.pressKey('Enter');
155-
}
156-
15765
await page.wait({ time: 1.5 });
158-
15966
results.push({ name: friendName, status: '✅ 已发送', detail: text });
16067
} catch (e: any) {
16168
results.push({ name: friendName, status: '❌ 失败', detail: e.message?.substring(0, 80) || '未知错误' });

src/clis/boss/chatlist.ts

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cli, Strategy } from '../../registry.js';
2-
import type { IPage } from '../../types.js';
2+
import { requirePage, navigateToChat, fetchFriendList } from './common.js';
33

44
cli({
55
site: 'boss',
@@ -14,31 +14,16 @@ cli({
1414
{ name: 'job-id', default: '0', help: 'Filter by job ID (0=all)' },
1515
],
1616
columns: ['name', 'job', 'last_msg', 'last_time', 'uid', 'security_id'],
17-
func: async (page: IPage | null, kwargs) => {
18-
if (!page) throw new Error('Browser page required');
19-
await page.goto('https://www.zhipin.com/web/chat/index');
20-
await page.wait({ time: 2 });
21-
const jobId = kwargs['job-id'] || '0';
22-
const pageNum = kwargs.page || 1;
23-
const limit = kwargs.limit || 20;
24-
const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${pageNum}&status=0&jobId=${jobId}`;
25-
const data: any = await page.evaluate(`
26-
async () => {
27-
return new Promise((resolve, reject) => {
28-
const xhr = new XMLHttpRequest();
29-
xhr.open('GET', '${targetUrl}', true);
30-
xhr.withCredentials = true;
31-
xhr.timeout = 15000;
32-
xhr.setRequestHeader('Accept', 'application/json');
33-
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
34-
xhr.onerror = () => reject(new Error('Network Error'));
35-
xhr.send();
36-
});
37-
}
38-
`);
39-
if (data.code !== 0) throw new Error(`API error: ${data.message} (code=${data.code})`);
40-
const friends = (data.zpData?.friendList || []).slice(0, limit);
41-
return friends.map((f: any) => ({
17+
func: async (page, kwargs) => {
18+
requirePage(page);
19+
await navigateToChat(page);
20+
21+
const friends = await fetchFriendList(page, {
22+
pageNum: kwargs.page || 1,
23+
jobId: kwargs['job-id'] || '0',
24+
});
25+
26+
return friends.slice(0, kwargs.limit || 20).map((f: any) => ({
4227
name: f.name || '',
4328
job: f.jobName || '',
4429
last_msg: f.lastMessageInfo?.text || '',

src/clis/boss/chatmsg.ts

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cli, Strategy } from '../../registry.js';
2-
import type { IPage } from '../../types.js';
2+
import { requirePage, navigateToChat, bossFetch, findFriendByUid } from './common.js';
33

44
cli({
55
site: 'boss',
@@ -13,48 +13,23 @@ cli({
1313
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
1414
],
1515
columns: ['from', 'type', 'text', 'time'],
16-
func: async (page: IPage | null, kwargs) => {
17-
if (!page) throw new Error('Browser page required');
18-
await page.goto('https://www.zhipin.com/web/chat/index');
19-
await page.wait({ time: 2 });
20-
const uid = kwargs.uid;
21-
const friendData: any = await page.evaluate(`
22-
async () => {
23-
return new Promise((resolve, reject) => {
24-
const xhr = new XMLHttpRequest();
25-
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
26-
xhr.withCredentials = true;
27-
xhr.timeout = 15000;
28-
xhr.setRequestHeader('Accept', 'application/json');
29-
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
30-
xhr.onerror = () => reject(new Error('Network Error'));
31-
xhr.send();
32-
});
33-
}
34-
`);
35-
if (friendData.code !== 0) throw new Error('获取好友列表失败');
36-
const friend = (friendData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
16+
func: async (page, kwargs) => {
17+
requirePage(page);
18+
await navigateToChat(page);
19+
20+
const friend = await findFriendByUid(page, kwargs.uid);
3721
if (!friend) throw new Error('未找到该候选人');
22+
3823
const gid = friend.uid;
3924
const securityId = encodeURIComponent(friend.securityId);
4025
const msgUrl = `https://www.zhipin.com/wapi/zpchat/boss/historyMsg?gid=${gid}&securityId=${securityId}&page=${kwargs.page}&c=20&src=0`;
41-
const msgData: any = await page.evaluate(`
42-
async () => {
43-
return new Promise((resolve, reject) => {
44-
const xhr = new XMLHttpRequest();
45-
xhr.open('GET', '${msgUrl}', true);
46-
xhr.withCredentials = true;
47-
xhr.timeout = 15000;
48-
xhr.setRequestHeader('Accept', 'application/json');
49-
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({raw: xhr.responseText.substring(0,500)}); } };
50-
xhr.onerror = () => reject(new Error('Network Error'));
51-
xhr.send();
52-
});
53-
}
54-
`);
55-
if (msgData.raw) throw new Error('Non-JSON: ' + msgData.raw);
56-
if (msgData.code !== 0) throw new Error('API error: ' + (msgData.message || msgData.code));
57-
const TYPE_MAP: Record<number, string> = {1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统', 6: '名片', 7: '语音', 8: '视频', 9: '表情'};
26+
27+
const msgData = await bossFetch(page, msgUrl);
28+
29+
const TYPE_MAP: Record<number, string> = {
30+
1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统',
31+
6: '名片', 7: '语音', 8: '视频', 9: '表情',
32+
};
5833
const messages = msgData.zpData?.messages || msgData.zpData?.historyMsgList || [];
5934
return messages.map((m: any) => {
6035
const fromObj = m.from || {};

0 commit comments

Comments
 (0)