← Back to skills

Domain skill

BOSS-zhipin

Markdown synced from browser-harness domain skills.

Agent prompt

Use this skill

Copy this prompt into your coding agent to make it enable browser-harness domain skills and read this exact domain folder before automating.

Set up https://github.com/browser-use/browser-harness for me if it is not already installed. If setup is needed, read `install.md` first to install and connect it to my real browser. Then read `SKILL.md` for normal usage and always read `helpers.py` because that is where the browser-harness functions are.

Enable domain skills if they are not already enabled by setting `BH_DOMAIN_SKILLS=1` for browser-harness. Use the `BOSS-zhipin` domain skill from `agent-workspace/domain-skills/BOSS-zhipin/`. Read every markdown file for this domain before inventing an approach:
- agent-workspace/domain-skills/BOSS-zhipin/chat.md
- agent-workspace/domain-skills/BOSS-zhipin/job-search.md
- agent-workspace/domain-skills/BOSS-zhipin/navigation.md

Use those domain-skill notes to complete my task for `BOSS-zhipin` in my real browser. When you open a setup, verification, or task tab, activate it so I can see the active browser tab.

Skill contents

What the agent will read

Chat & Messaging

chat.md

Source
  • Field-tested against zhipin.com on 2026-05-01. Login required. Messages are loaded via WebSocket + REST API.
  • IMPORTANT: Never send messages without the user's explicit permission. This skill documents the read/retrieval mechanics only.
  • ---
  • BOSS直聘 uses a hybrid messaging architecture:
Show full markdown

Field-tested against zhipin.com on 2026-05-01. Login required. Messages are loaded via WebSocket + REST API.

IMPORTANT: Never send messages without the user's explicit permission. This skill documents the read/retrieval mechanics only.


Architecture

BOSS直聘 uses a hybrid messaging architecture:

  • Conversation list — loaded via WebSocket (ws6.zhipin.com) on page load, NOT via REST
  • Message history — REST API /wapi/zpchat/geek/historyMsg
  • Real-time messages — WebSocket push from ws6.zhipin.com

Chat Page (/web/geek/chat)

Page Structure

code
Left panel:
  .chat-user.v2                  — filter bar + search input
    .label-list > ul
      li.selected                — active filter tab
      li                         — "未读(N)" shows count badge in <i>
      li > .ui-dropmenu          — "更多" dropdown (仅沟通/有交换/有面试/不感兴趣)
      li.filter-item             — "AI筛选" dropdown with natural language input
    .boss-search-input           — contact search (placeholder: "搜索30天内的联系人")
  .user-list
    .user-list-content
      .friend-content-warp
        .friend-content           — conversation item (click to open)
          .friend-content.friend-top  — pinned/top conversation

Right panel (visible after clicking a conversation):
  .chat-record                   — message history container
    .message-item.item-myself    — message sent by user
      .item-time > .time         — timestamp
      .message-content > .text   — message body
      .message-status.status-read — read receipt ("已读")
    .message-item.item-friend    — message from recruiter
      .item-time > .time
      .message-content > .text

Filter Tabs

Top-level tabs (.chat-user.v2 .label-list li):

TabDescriptionClass
全部All conversations (default)li.selected when active
未读(N)Unread conversations, badge shows count<i> in label shows count
新招呼New greetings from recruitersBadge indicator via <i class="badge">
更多 ▾Dropdown with extra filters.ui-dropmenu

"更多" dropdown (.more-label li):

OptionDescription
仅沟通Conversations with messages exchanged
有交换Conversations with file/contact exchange
有面试Conversations with interview invitations
不感兴趣Conversations marked "not interested"

"AI筛选" (.filter-item > .ui-dropmenu): Opens a panel with a <textarea> for natural language filter input (e.g. "后端开发 上海 高薪").

Clicking Filter Tabs

python
def click_filter(label_text):
    """Click a filter tab by its text label."""
    js(f"""
    (function() {{
        var labels = document.querySelectorAll('.chat-user .label-list li .label-name');
        for (var i = 0; i < labels.length; i++) {{
            if (labels[i].textContent.trim().indexOf('{label_text}') === 0) {{
                labels[i].closest('li').click();
                return true;
            }}
        }}
        return false;
    }})()
    """)
    wait(1)

def click_more_filter(label_text):
    """Click an option inside the '更多' dropdown."""
    # First open the dropdown
    click_filter("更多")
    wait(0.5)
    js(f"""
    (function() {{
        var items = document.querySelectorAll('.more-label li span');
        for (var i = 0; i < items.length; i++) {{
            if (items[i].textContent.trim() === '{label_text}') {{
                items[i].closest('li').click();
                return true;
            }}
        }}
        return false;
    }})()
    """)
    wait(1)

Conversation Item (DOM)

Each .friend-content contains:

  • Timestamp (e.g. "04月13日", "昨天")
  • Recruiter name (e.g. "刘女士")
  • Company name (e.g. "Soul App")
  • Recruiter title (e.g. "招聘专家")
  • Last message preview
  • Unread count badge (numeric)

Read Conversation List (DOM)

python
def get_conversations():
    raw = js("""
    (function() {
        var items = document.querySelectorAll('.friend-content');
        var results = [];
        for (var i = 0; i < items.length; i++) {
            var el = items[i];
            var text = el.textContent;
            var badge = el.querySelector('[class*="badge"], [class*="unread"], [class*="count"]');
            var unread = badge ? parseInt(badge.textContent) || 0 : 0;
            results.push({
                text: text.trim().substring(0, 150),
                is_top: el.classList.contains('friend-top'),
                unread: unread
            });
        }
        return JSON.stringify(results);
    })()
    """)
    return json.loads(raw)

Open a Conversation

Click the .friend-content element:

python
def open_conversation(index=0):
    js(f"document.querySelectorAll('.friend-content')[{index}].click()")
    wait(2)

API: Message History

code
GET /wapi/zpchat/geek/historyMsg?bossId={bossId}&maxMsgId=0&c=20&page=1&src=0

Parameters

ParamDescription
bossIdRecruiter ID from conversation (format: 9c833990a839f1251Hx92du5GA~~)
maxMsgIdPagination cursor. 0 for first page, then use the smallest mid from previous page
cCount per page (default 20)
pagePage number
srcSource (0 for web)

The bossId can be found in performance entries after clicking a conversation, or extracted from the WebSocket connection data on page load.

Response (zpData.messages[])

Each message has:

python
{
    "mid": 337069469603329,              # message ID (numeric, for pagination)
    "type": 3,                           # 3=regular message, 4=system message
    "received": true,                    # whether you received it
    "body": {
        "type": 1,                       # 1=text, 8=job card
        "text": "message text here...",  # present when body.type=1
        "jobDesc": { ... }               # present when body.type=8
    },
    "from": {
        "uid": 502838021,                # sender user ID
        "name": "张女士",
        "avatar": "https://img.bosszhipin.com/..."
    },
    "to": {
        "uid": 680839465                 # recipient user ID
    }
}

Message Body Types

body.typeMeaningFields
1Plain textbody.text
8Job description cardbody.jobDesc (title, salary, company, boss, city, experience, education), body.headTitle
16System notification(file received, etc.)

Job Card Messages (body.type=8)

python
{
    "body": {
        "type": 8,
        "headTitle": "您正在与Boss刘女士直接沟通如下职位",
        "jobDesc": {
            "title": "AI Agent工程师",
            "salary": "35-60K·16薪",         # REAL salary — not font-encoded
            "company": "Soul App",
            "city": "上海 浦东新区 金桥",
            "experience": "经验不限",
            "education": "硕士",
            "stage": "D轮及以上",
            "positionCategory": "算法工程师",
            "boss": {
                "uid": 3872648,
                "name": "刘女士",
                "avatar": "https://img.bosszhipin.com/..."
            },
            "bossTitle": "招聘专家",
            "jobId": 509933581
        }
    }
}

Fetch Message History

python
def fetch_messages(boss_id, page=1, count=20):
    raw = js(f"""
    (async function() {{
        var url = '/wapi/zpchat/geek/historyMsg?bossId={boss_id}&maxMsgId=0&c={count}&page={page}&src=0';
        var r = await fetch(url);
        var d = await r.json();
        if (d.code !== 0 || !d.zpData) {{
            return JSON.stringify({{code: d.code, hasMore: false, count: 0, messages: [], error: d.msg || 'API error'}});
        }}
        var msgs = d.zpData.messages || [];
        return JSON.stringify({{
            code: d.code,
            hasMore: d.zpData.hasMore,
            count: msgs.length,
            messages: msgs.map(function(m) {{
                var b = m.body || {{}};
                return {{
                    mid: m.mid,
                    type: m.type,
                    body_type: b.type,
                    text: b.text || null,
                    job: b.jobDesc ? {{
                        title: b.jobDesc.title,
                        salary: b.jobDesc.salary,
                        company: b.jobDesc.company,
                        city: b.jobDesc.city,
                        boss_name: (b.jobDesc.boss || {{}}).name,
                        job_id: b.jobDesc.jobId
                    }} : null,
                    from_name: (m.from || {{}}).name,
                    from_uid: (m.from || {{}}).uid,
                    received: m.received
                }};
            }})
        }});
    }})()
    """)
    return json.loads(raw)

Pagination

Use maxMsgId (not page) for efficient pagination. Set maxMsgId to the smallest mid from the previous batch:

python
def fetch_all_messages(boss_id):
    all_msgs = []
    max_msg_id = 0
    while True:
        raw = js(f"""
        (async function() {{
            var r = await fetch('/wapi/zpchat/geek/historyMsg?bossId={boss_id}&maxMsgId={max_msg_id}&c=20&page=1&src=0');
            var d = await r.json();
            if (d.code !== 0 || !d.zpData) {{
                return JSON.stringify({{messages: [], hasMore: false}});
            }}
            return JSON.stringify(d.zpData);
        }})()
        """)
        data = json.loads(raw)
        msgs = data.get("messages", [])
        if not msgs:
            break
        all_msgs.extend(msgs)
        if not data.get("hasMore"):
            break
        max_msg_id = msgs[-1]["mid"]  # smallest mid
        wait(0.5)
    return all_msgs

Messages Read from DOM (after opening a conversation)

python
def read_messages_dom():
    raw = js("""
    (function() {
        var items = document.querySelectorAll('.message-item');
        var results = [];
        for (var i = 0; i < items.length; i++) {
            var el = items[i];
            var timeEl = el.querySelector('.time');
            var textEl = el.querySelector('.text');
            var statusEl = el.querySelector('.message-status');
            results.push({
                from_me: el.classList.contains('item-myself'),
                time: timeEl ? timeEl.textContent.trim() : '',
                text: textEl ? textEl.textContent.trim().substring(0, 300) : '',
                status: statusEl ? statusEl.textContent.trim() : ''
            });
        }
        return JSON.stringify(results);
    })()
    """)
    return json.loads(raw)

Extracting bossId from the Page

The bossId is embedded in WebSocket payloads and API calls. To discover it after clicking a conversation:

python
def get_current_boss_id():
    return js("""
    (function() {
        var entries = performance.getEntriesByType('resource');
        for (var i = entries.length - 1; i >= 0; i--) {
            var url = entries[i].name;
            if (url.indexOf('/wapi/zpchat/geek/historyMsg') === -1) continue;
            var match = url.match(/bossId=([^&]+)/);
            if (match) return match[1];
        }
        return null;
    })()
    """)

Navigating from Job Detail to Chat

Opening a job detail page and clicking "立即沟通" initiates a conversation with that job's recruiter. The API needed:

  1. Navigate to /job_detail/{JOB_ID}.html
  2. Find the chat button (.btn-startchat) element
  3. The button's href or click handler contains the bossId and securityId

Gotchas

  • Conversation list is WebSocket-loaded — no REST API for the list. Use DOM extraction (.friend-content) or monitor WebSocket frames to get the initial conversation list.
  • Message history uses bossId, not encryptBossId — the bossId format is "9c833990a839f1251Hx92du5GA~~" (trailing ~~), different from the job list's encryptBossId.
  • maxMsgId pagination — use the smallest mid from the current batch for the next page, not page parameter.
  • Job cards in messages have real salarybody.jobDesc.salary returns "35-60K·16薪" unlike the DOM which uses font-encoded digits.
  • System messages (type=4) — these include read receipts, file transfers ("对方已同意,您的附件简历已发送给对方"), and competitor analysis cards.
  • After clicking a conversation, wait(2) — the message history needs time to render.
  • item-myself vs item-friend — user messages have item-myself class, recruiter messages have item-friend.
  • Contact search input.boss-search-input searches within 30 days of contacts, not a general message compose box.