Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions docs/adapters/browser/facebook.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
| `opencli facebook notifications` | Get recent notifications |
| `opencli facebook feed` | Get news feed posts |
| `opencli facebook search` | Search people, pages, posts |
| `opencli facebook friends` | Friend suggestions |
| `opencli facebook groups` | List your joined groups |
| `opencli facebook memories` | On This Day memories |
| `opencli facebook events` | Browse event categories |
| `opencli facebook add-friend` | Send a friend request |
| `opencli facebook join-group` | Join a group |

## Usage Examples

```bash
# View a profile
opencli facebook profile --username zuck
opencli facebook profile zuck

# Get notifications
opencli facebook notifications --limit 10
Expand All @@ -30,19 +24,10 @@ opencli facebook notifications --limit 10
opencli facebook feed --limit 5

# Search
opencli facebook search --query "OpenAI" --limit 5

# List your groups
opencli facebook groups

# Send friend request
opencli facebook add-friend --username someone

# Join a group
opencli facebook join-group --group 123456789
opencli facebook search "OpenAI" --limit 5

# JSON output
opencli facebook profile --username zuck -f json
opencli facebook profile zuck -f json
```

## Prerequisites
Expand Down
29 changes: 11 additions & 18 deletions docs/adapters/browser/instagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,31 @@
| `opencli instagram followers` | List user's followers |
| `opencli instagram following` | List user's following |
| `opencli instagram saved` | Get your saved posts |
| `opencli instagram like` | Like a post |
| `opencli instagram unlike` | Unlike a post |
| `opencli instagram comment` | Comment on a post |
| `opencli instagram save` | Bookmark a post |
| `opencli instagram unsave` | Remove bookmark |
| `opencli instagram follow` | Follow a user |
| `opencli instagram unfollow` | Unfollow a user |

## Usage Examples

```bash
# View a user's profile
opencli instagram profile --username nasa
opencli instagram profile nasa

# Search users
opencli instagram search --query nasa --limit 5
opencli instagram search nasa --limit 5

# View a user's recent posts
opencli instagram user --username nasa --limit 10
opencli instagram user nasa --limit 10

# Like a user's most recent post
opencli instagram like --username nasa --index 1
# Discover trending posts
opencli instagram explore --limit 20

# Comment on a post
opencli instagram comment --username nasa --text "Amazing!" --index 1
# List followers/following
opencli instagram followers nasa --limit 20
opencli instagram following nasa --limit 20

# Follow/unfollow
opencli instagram follow --username nasa
opencli instagram unfollow --username nasa
# Get your saved posts
opencli instagram saved --limit 10

# JSON output
opencli instagram profile --username nasa -f json
opencli instagram profile nasa -f json
```

## Prerequisites
Expand Down
119 changes: 13 additions & 106 deletions src/clis/boss/batchgreet.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* BOSS直聘 batchgreet — batch greet recommended candidates.
*
* Combines recommend (greetRecSortList) + greet (UI automation).
* Sends greeting messages to multiple candidates sequentially.
*/
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
import {
requirePage, navigateToChat, fetchRecommendList,
clickCandidateInList, typeAndSendMessage, verbose,
} from './common.js';

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

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

if (process.env.OPENCLI_VERBOSE) {
console.error(`[opencli:boss] Batch greeting up to ${limit} candidates...`);
}

await page.goto('https://www.zhipin.com/web/chat/index');
await page.wait({ time: 3 });
verbose(`Batch greeting up to ${limit} candidates...`);

// Get recommended candidates
const listData: any = await page.evaluate(`
async () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
xhr.withCredentials = true;
xhr.timeout = 15000;
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send();
});
}
`);
await navigateToChat(page, 3);

if (listData.code !== 0) {
if (listData.code === 7 || listData.code === 37) {
throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
}
throw new Error(`获取推荐列表失败: ${listData.message}`);
}

let candidates = listData.zpData?.friendList || [];
let candidates = await fetchRecommendList(page);
if (filterJobId) {
candidates = candidates.filter((f: any) => f.encryptJobId === filterJobId);
}
Expand All @@ -74,88 +48,21 @@ cli({
const friendName = candidate.name || '候选人';

try {
// Click on candidate
const clicked: any = await page.evaluate(`
async () => {
const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
if (item) {
item.click();
return { clicked: true };
}
const items = document.querySelectorAll('.geek-item');
for (const el of items) {
if (el.id && el.id.startsWith('_${numericUid}')) {
el.click();
return { clicked: true };
}
}
return { clicked: false };
}
`);

if (!clicked.clicked) {
const clicked = await clickCandidateInList(page, numericUid);
if (!clicked) {
results.push({ name: friendName, status: '❌ 跳过', detail: '在聊天列表中未找到' });
continue;
}

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

// Type message
const typed: any = await page.evaluate(`
async () => {
const selectors = [
'.chat-editor [contenteditable="true"]',
'.chat-input [contenteditable="true"]',
'[contenteditable="true"]',
'textarea',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
el.focus();
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
el.value = ${JSON.stringify(text)};
el.dispatchEvent(new Event('input', { bubbles: true }));
} else {
el.textContent = '';
el.focus();
document.execCommand('insertText', false, ${JSON.stringify(text)});
el.dispatchEvent(new Event('input', { bubbles: true }));
}
return { found: true };
}
}
return { found: false };
}
`);

if (!typed.found) {
const sent = await typeAndSendMessage(page, text);
if (!sent) {
results.push({ name: friendName, status: '❌ 失败', detail: '找不到消息输入框' });
continue;
}

await page.wait({ time: 0.5 });

// Click send
const sent: any = await page.evaluate(`
async () => {
const btn = document.querySelector('.conversation-editor .submit')
|| document.querySelector('.submit-content .submit')
|| document.querySelector('.conversation-operate .submit');
if (btn) {
btn.click();
return { clicked: true };
}
return { clicked: false };
}
`);

if (!sent.clicked) {
await page.pressKey('Enter');
}

await page.wait({ time: 1.5 });

results.push({ name: friendName, status: '✅ 已发送', detail: text });
} catch (e: any) {
results.push({ name: friendName, status: '❌ 失败', detail: e.message?.substring(0, 80) || '未知错误' });
Expand Down
37 changes: 11 additions & 26 deletions src/clis/boss/chatlist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
import { requirePage, navigateToChat, fetchFriendList } from './common.js';

cli({
site: 'boss',
Expand All @@ -14,31 +14,16 @@ cli({
{ name: 'job-id', default: '0', help: 'Filter by job ID (0=all)' },
],
columns: ['name', 'job', 'last_msg', 'last_time', 'uid', 'security_id'],
func: async (page: IPage | null, kwargs) => {
if (!page) throw new Error('Browser page required');
await page.goto('https://www.zhipin.com/web/chat/index');
await page.wait({ time: 2 });
const jobId = kwargs['job-id'] || '0';
const pageNum = kwargs.page || 1;
const limit = kwargs.limit || 20;
const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${pageNum}&status=0&jobId=${jobId}`;
const data: any = await page.evaluate(`
async () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '${targetUrl}', true);
xhr.withCredentials = true;
xhr.timeout = 15000;
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send();
});
}
`);
if (data.code !== 0) throw new Error(`API error: ${data.message} (code=${data.code})`);
const friends = (data.zpData?.friendList || []).slice(0, limit);
return friends.map((f: any) => ({
func: async (page, kwargs) => {
requirePage(page);
await navigateToChat(page);

const friends = await fetchFriendList(page, {
pageNum: kwargs.page || 1,
jobId: kwargs['job-id'] || '0',
});

return friends.slice(0, kwargs.limit || 20).map((f: any) => ({
name: f.name || '',
job: f.jobName || '',
last_msg: f.lastMessageInfo?.text || '',
Expand Down
53 changes: 14 additions & 39 deletions src/clis/boss/chatmsg.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cli, Strategy } from '../../registry.js';
import type { IPage } from '../../types.js';
import { requirePage, navigateToChat, bossFetch, findFriendByUid } from './common.js';

cli({
site: 'boss',
Expand All @@ -13,48 +13,23 @@ cli({
{ name: 'page', type: 'int', default: 1, help: 'Page number' },
],
columns: ['from', 'type', 'text', 'time'],
func: async (page: IPage | null, kwargs) => {
if (!page) throw new Error('Browser page required');
await page.goto('https://www.zhipin.com/web/chat/index');
await page.wait({ time: 2 });
const uid = kwargs.uid;
const friendData: any = await page.evaluate(`
async () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
xhr.withCredentials = true;
xhr.timeout = 15000;
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send();
});
}
`);
if (friendData.code !== 0) throw new Error('获取好友列表失败');
const friend = (friendData.zpData?.friendList || []).find((f: any) => f.encryptUid === uid);
func: async (page, kwargs) => {
requirePage(page);
await navigateToChat(page);

const friend = await findFriendByUid(page, kwargs.uid);
if (!friend) throw new Error('未找到该候选人');

const gid = friend.uid;
const securityId = encodeURIComponent(friend.securityId);
const msgUrl = `https://www.zhipin.com/wapi/zpchat/boss/historyMsg?gid=${gid}&securityId=${securityId}&page=${kwargs.page}&c=20&src=0`;
const msgData: any = await page.evaluate(`
async () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '${msgUrl}', true);
xhr.withCredentials = true;
xhr.timeout = 15000;
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({raw: xhr.responseText.substring(0,500)}); } };
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send();
});
}
`);
if (msgData.raw) throw new Error('Non-JSON: ' + msgData.raw);
if (msgData.code !== 0) throw new Error('API error: ' + (msgData.message || msgData.code));
const TYPE_MAP: Record<number, string> = {1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统', 6: '名片', 7: '语音', 8: '视频', 9: '表情'};

const msgData = await bossFetch(page, msgUrl);

const TYPE_MAP: Record<number, string> = {
1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统',
6: '名片', 7: '语音', 8: '视频', 9: '表情',
};
const messages = msgData.zpData?.messages || msgData.zpData?.historyMsgList || [];
return messages.map((m: any) => {
const fromObj = m.from || {};
Expand Down
Loading
Loading