Files
autoAiWorkSys/_doc/客户端待开发功能.md
张成 933f1618ca 1
2025-12-25 23:01:21 +08:00

28 KiB
Raw Permalink Blame History

客户端待开发功能文档

本文档说明客户端需要实现的功能所有操作通过MQTT接收服务端指令执行


一、客户端架构概览

1.1 技术栈

  • 自动化框架: Puppeteer / Playwright (推荐Playwright支持更好的反爬)
  • 运行环境: Node.js 18+
  • 通信协议: MQTT (mqtt://192.144.167.231:1883)
  • 消息格式: JSON
  • 浏览器: Chromium (无头模式/有头模式可切换)

1.2 核心模块

client/
├── src/
│   ├── mqtt/              # MQTT通信模块
│   │   ├── client.js      # MQTT客户端
│   │   └── handler.js     # 消息处理器
│   ├── platforms/         # 平台适配器
│   │   ├── boss/          # Boss直聘
│   │   │   ├── login.js   # 登录模块
│   │   │   ├── search.js  # 搜索模块 ⚠️ 待开发
│   │   │   ├── apply.js   # 投递模块 ⚠️ 待完善
│   │   │   ├── resume.js  # 简历模块 ⚠️ 待开发
│   │   │   ├── chat.js    # 聊天模块 ⚠️ 待完善
│   │   │   └── active.js  # 保活模块 ⚠️ 待开发
│   │   └── zhilian/       # 智联招聘 (同上)
│   ├── utils/             # 工具函数
│   │   ├── browser.js     # 浏览器管理
│   │   ├── antibot.js     # 反爬虫处理
│   │   └── logger.js      # 日志记录
│   └── index.js           # 入口文件
└── config/
    └── default.json       # 配置文件

1.3 工作流程

服务端 → MQTT → 客户端接收 → 路由到平台模块 → 执行操作 → 返回结果 → MQTT → 服务端

二、高优先级功能 (P0)

与投递直接相关的核心功能

2.1 增强搜索功能 ⚠️ 待开发

功能说明

实现Boss直聘完整的搜索功能支持所有筛选项和URL解析

MQTT指令

  • search_jobs_enhanced - 带完整筛选参数的搜索
  • search_by_url - 直接解析URL搜索

目标URL示例

https://www.zhipin.com/web/geek/jobs?city=101020100&query=%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88&experience=103&degree=203&salary=404

实现要点

1. URL参数解析

// platforms/boss/search.js

/**
 * 解析Boss直聘搜索URL
 * @param {string} url - 搜索URL
 * @returns {Object} 解析后的参数对象
 */
function parseSearchUrl(url) {
  const urlObj = new URL(url);
  const params = {};

  // 城市代码
  if (urlObj.searchParams.get('city')) {
    params.city = urlObj.searchParams.get('city');
  }

  // 搜索关键词
  if (urlObj.searchParams.get('query')) {
    params.query = decodeURIComponent(urlObj.searchParams.get('query'));
  }

  // 工作经验
  if (urlObj.searchParams.get('experience')) {
    params.experience = urlObj.searchParams.get('experience');
  }

  // 学历要求
  if (urlObj.searchParams.get('degree')) {
    params.degree = urlObj.searchParams.get('degree');
  }

  // 薪资范围
  if (urlObj.searchParams.get('salary')) {
    params.salary = urlObj.searchParams.get('salary');
  }

  // 公司规模
  if (urlObj.searchParams.get('scale')) {
    params.scale = urlObj.searchParams.get('scale');
  }

  // 融资阶段
  if (urlObj.searchParams.get('stage')) {
    params.stage = urlObj.searchParams.get('stage');
  }

  // 职位类型
  if (urlObj.searchParams.get('position')) {
    params.position = urlObj.searchParams.get('position');
  }

  // 行业领域
  if (urlObj.searchParams.get('industry')) {
    params.industry = urlObj.searchParams.get('industry');
  }

  return params;
}

/**
 * 构建搜索URL
 * @param {Object} params - 搜索参数
 * @returns {string} 完整的搜索URL
 */
function buildSearchUrl(params) {
  const baseUrl = 'https://www.zhipin.com/web/geek/jobs';
  const searchParams = new URLSearchParams();

  // 必填参数
  if (params.city) searchParams.set('city', params.city);
  if (params.query) searchParams.set('query', params.query);

  // 可选筛选参数
  if (params.experience) searchParams.set('experience', params.experience);
  if (params.degree) searchParams.set('degree', params.degree);
  if (params.salary) searchParams.set('salary', params.salary);
  if (params.scale) searchParams.set('scale', params.scale);
  if (params.stage) searchParams.set('stage', params.stage);
  if (params.position) searchParams.set('position', params.position);
  if (params.industry) searchParams.set('industry', params.industry);

  return `${baseUrl}?${searchParams.toString()}`;
}

2. 搜索执行

/**
 * 执行增强搜索
 * @param {Page} page - Playwright页面对象
 * @param {Object} params - 搜索参数
 * @returns {Promise<Array>} 职位列表
 */
async function searchJobsEnhanced(page, params) {
  const url = buildSearchUrl(params);

  // 访问搜索页面
  await page.goto(url, { waitUntil: 'networkidle' });

  // 等待职位列表加载
  await page.waitForSelector('.job-card-wrapper', { timeout: 10000 });

  // 反爬虫检测
  const isBlocked = await checkBotDetection(page);
  if (isBlocked) {
    throw new Error('检测到反爬虫,需要人工介入');
  }

  const jobs = [];
  let hasMore = true;
  let scrollCount = 0;
  const maxScrolls = params.maxResults ? Math.ceil(params.maxResults / 30) : 3;

  while (hasMore && scrollCount < maxScrolls) {
    // 提取当前页面职位
    const pageJobs = await extractJobsFromPage(page);
    jobs.push(...pageJobs);

    // 滚动加载更多
    hasMore = await scrollToLoadMore(page);
    scrollCount++;

    // 随机延迟,模拟人类行为
    await randomDelay(1000, 3000);
  }

  return jobs;
}

/**
 * 从页面提取职位信息
 * @param {Page} page - Playwright页面对象
 * @returns {Promise<Array>} 职位信息数组
 */
async function extractJobsFromPage(page) {
  return await page.$$eval('.job-card-wrapper', (cards) => {
    return cards.map(card => {
      const jobName = card.querySelector('.job-name')?.textContent.trim();
      const jobArea = card.querySelector('.job-area')?.textContent.trim();
      const salary = card.querySelector('.salary')?.textContent.trim();
      const companyName = card.querySelector('.company-name a')?.textContent.trim();
      const companyTag = Array.from(card.querySelectorAll('.company-tag-list li'))
        .map(li => li.textContent.trim());
      const bossInfo = card.querySelector('.info-public')?.textContent.trim();
      const jobLink = card.querySelector('.job-card-left')?.href;
      const jobId = jobLink?.match(/job_detail\/(\w+)\.html/)?.[1];

      // 提取标签(经验、学历)
      const tags = Array.from(card.querySelectorAll('.tag-list li'))
        .map(li => li.textContent.trim());

      return {
        jobId,
        jobName,
        jobArea,
        salary,
        companyName,
        companyTags: companyTag,
        experience: tags[0] || '',
        degree: tags[1] || '',
        bossInfo,
        jobLink,
        tags
      };
    });
  });
}

/**
 * 滚动加载更多职位
 * @param {Page} page - Playwright页面对象
 * @returns {Promise<boolean>} 是否还有更多内容
 */
async function scrollToLoadMore(page) {
  // 滚动到页面底部
  await page.evaluate(() => {
    window.scrollTo(0, document.body.scrollHeight);
  });

  // 等待新内容加载
  await page.waitForTimeout(2000);

  // 检查是否有"加载更多"按钮或已到底部
  const loadMoreBtn = await page.$('.load-more');
  if (loadMoreBtn) {
    await loadMoreBtn.click();
    await page.waitForTimeout(1500);
    return true;
  }

  // 检查是否显示"没有更多了"
  const noMore = await page.$('.no-more-tips');
  return !noMore;
}

3. 反爬虫处理

// utils/antibot.js

/**
 * 检测是否被反爬虫拦截
 * @param {Page} page - Playwright页面对象
 * @returns {Promise<boolean>} 是否被拦截
 */
async function checkBotDetection(page) {
  const indicators = [
    () => page.$('.captcha-container'),        // 验证码
    () => page.$('.security-check'),           // 安全检查
    () => page.content().then(c => c.includes('频繁访问')),
    () => page.content().then(c => c.includes('请稍后再试'))
  ];

  for (const check of indicators) {
    if (await check()) {
      return true;
    }
  }

  return false;
}

/**
 * 随机延迟
 * @param {number} min - 最小毫秒
 * @param {number} max - 最大毫秒
 */
async function randomDelay(min, max) {
  const delay = Math.floor(Math.random() * (max - min + 1)) + min;
  await new Promise(resolve => setTimeout(resolve, delay));
}

/**
 * 模拟人类鼠标移动
 * @param {Page} page - Playwright页面对象
 */
async function simulateHumanBehavior(page) {
  // 随机移动鼠标
  const randomX = Math.floor(Math.random() * 800) + 100;
  const randomY = Math.floor(Math.random() * 600) + 100;
  await page.mouse.move(randomX, randomY);

  // 随机滚动
  await page.evaluate(() => {
    const scrollAmount = Math.floor(Math.random() * 300) + 100;
    window.scrollBy(0, scrollAmount);
  });

  await randomDelay(500, 1500);
}

4. MQTT指令处理器

// mqtt/handler.js - 增强搜索指令处理

async function handleSearchJobsEnhanced(payload) {
  const { platform, params, taskId } = payload;

  try {
    // 获取平台对应的浏览器页面
    const page = await browserManager.getPage(platform);

    // 确保已登录
    await ensureLoggedIn(page, platform);

    // 执行搜索
    const searchModule = require(`../platforms/${platform}/search.js`);
    const jobs = await searchModule.searchJobsEnhanced(page, params);

    // 返回结果
    return {
      code: 0,
      message: '搜索成功',
      data: {
        total: jobs.length,
        jobs: jobs
      },
      taskId
    };
  } catch (error) {
    console.error('搜索失败:', error);
    return {
      code: -1,
      message: error.message,
      taskId
    };
  }
}

async function handleSearchByUrl(payload) {
  const { platform, url, taskId } = payload;

  try {
    const searchModule = require(`../platforms/${platform}/search.js`);

    // 解析URL
    const params = searchModule.parseSearchUrl(url);

    // 执行搜索
    const page = await browserManager.getPage(platform);
    await ensureLoggedIn(page, platform);
    const jobs = await searchModule.searchJobsEnhanced(page, params);

    return {
      code: 0,
      message: '搜索成功',
      data: {
        total: jobs.length,
        jobs: jobs,
        params: params  // 返回解析的参数
      },
      taskId
    };
  } catch (error) {
    console.error('URL搜索失败:', error);
    return {
      code: -1,
      message: error.message,
      taskId
    };
  }
}

参数映射表

工作经验 (experience)

  • 103: 应届生
  • 104: 1年以内
  • 105: 1-3年
  • 106: 3-5年
  • 107: 5-10年
  • 108: 10年以上

学历要求 (degree)

  • 203: 大专
  • 204: 本科
  • 205: 硕士
  • 206: 博士

薪资范围 (salary)

  • 402: 3K以下
  • 403: 3-5K
  • 404: 5-10K
  • 405: 10-15K
  • 406: 15-25K
  • 407: 25-35K
  • 408: 35-50K
  • 409: 50K以上

公司规模 (scale)

  • 301: 0-20人
  • 302: 20-99人
  • 303: 100-499人
  • 304: 500-999人
  • 305: 1000-9999人
  • 306: 10000人以上

融资阶段 (stage)

  • 801: 未融资
  • 802: 天使轮
  • 803: A轮
  • 804: B轮
  • 805: C轮
  • 806: D轮及以上
  • 807: 已上市
  • 808: 不需要融资

测试用例

// 测试用例1: 参数搜索
{
  action: 'search_jobs_enhanced',
  platform: 'boss',
  params: {
    city: '101020100',           // 上海
    query: '全栈工程师',
    experience: '106',           // 3-5年
    degree: '204',               // 本科
    salary: '406',               // 15-25K
    scale: '303',                // 100-499人
    maxResults: 100
  }
}

// 测试用例2: URL搜索
{
  action: 'search_by_url',
  platform: 'boss',
  url: 'https://www.zhipin.com/web/geek/jobs?city=101020100&query=%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88&experience=106&degree=204&salary=406'
}

2.2 批量投递功能 ⚠️ 待完善

功能说明

支持批量投递职位,带智能过滤和频率控制

MQTT指令

  • batch_apply_jobs - 批量投递

实现要点

// platforms/boss/apply.js

/**
 * 批量投递职位
 * @param {Page} page - Playwright页面对象
 * @param {Array} jobIds - 职位ID列表
 * @param {Object} options - 投递选项
 * @returns {Promise<Object>} 投递结果
 */
async function batchApplyJobs(page, jobIds, options = {}) {
  const {
    maxPerHour = 20,           // 每小时最多投递数
    delayMin = 10000,          // 最小间隔(ms)
    delayMax = 30000,          // 最大间隔(ms)
    skipApplied = true,        // 跳过已投递
    greetingMessage = ''       // 打招呼消息
  } = options;

  const results = {
    total: jobIds.length,
    success: 0,
    failed: 0,
    skipped: 0,
    details: []
  };

  let appliedThisHour = 0;
  let hourStartTime = Date.now();

  for (const jobId of jobIds) {
    // 频率控制
    if (appliedThisHour >= maxPerHour) {
      const elapsed = Date.now() - hourStartTime;
      if (elapsed < 3600000) {
        const waitTime = 3600000 - elapsed;
        console.log(`已达每小时投递上限,等待 ${waitTime/1000} 秒`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
        appliedThisHour = 0;
        hourStartTime = Date.now();
      }
    }

    try {
      // 检查是否已投递
      if (skipApplied) {
        const applied = await checkIfApplied(page, jobId);
        if (applied) {
          results.skipped++;
          results.details.push({ jobId, status: 'skipped', reason: '已投递' });
          continue;
        }
      }

      // 投递职位
      await applyJob(page, jobId, greetingMessage);

      results.success++;
      appliedThisHour++;
      results.details.push({ jobId, status: 'success' });

      // 随机延迟
      const delay = Math.floor(Math.random() * (delayMax - delayMin + 1)) + delayMin;
      await new Promise(resolve => setTimeout(resolve, delay));

    } catch (error) {
      results.failed++;
      results.details.push({
        jobId,
        status: 'failed',
        reason: error.message
      });
    }
  }

  return results;
}

/**
 * 检查职位是否已投递
 */
async function checkIfApplied(page, jobId) {
  await page.goto(`https://www.zhipin.com/job_detail/${jobId}.html`);
  await page.waitForTimeout(1000);

  // 查找"继续沟通"或"已沟通"按钮
  const appliedBtn = await page.$('.btn-applied, .btn-chatted');
  return !!appliedBtn;
}

/**
 * 投递单个职位
 */
async function applyJob(page, jobId, greetingMessage) {
  await page.goto(`https://www.zhipin.com/job_detail/${jobId}.html`);
  await page.waitForSelector('.btn-startchat', { timeout: 5000 });

  // 点击"立即沟通"
  await page.click('.btn-startchat');

  // 如果有打招呼消息
  if (greetingMessage) {
    await page.waitForSelector('.chat-input', { timeout: 3000 });
    await page.fill('.chat-input', greetingMessage);
    await page.click('.send-btn');
  }

  await page.waitForTimeout(1000);
}

2.3 简历刷新功能 ⚠️ 待开发

功能说明

定时刷新简历,提升曝光度

MQTT指令

  • refresh_resume - 刷新简历

实现要点

// platforms/boss/resume.js

/**
 * 刷新简历
 * @param {Page} page - Playwright页面对象
 * @returns {Promise<Object>} 刷新结果
 */
async function refreshResume(page) {
  try {
    // 进入简历管理页面
    await page.goto('https://www.zhipin.com/web/geek/profile', {
      waitUntil: 'networkidle'
    });

    // 查找刷新按钮
    const refreshBtn = await page.$('.refresh-resume-btn, .update-resume-btn');
    if (!refreshBtn) {
      throw new Error('未找到刷新按钮');
    }

    // 检查是否可刷新(可能有时间限制)
    const disabled = await refreshBtn.getAttribute('disabled');
    if (disabled) {
      // 获取下次可刷新时间
      const nextRefreshTime = await page.$eval('.next-refresh-time', el => el.textContent);
      throw new Error(`暂时无法刷新,下次可刷新时间: ${nextRefreshTime}`);
    }

    // 点击刷新
    await refreshBtn.click();
    await page.waitForTimeout(2000);

    // 确认刷新
    const confirmBtn = await page.$('.confirm-refresh');
    if (confirmBtn) {
      await confirmBtn.click();
      await page.waitForTimeout(1000);
    }

    return {
      success: true,
      message: '简历刷新成功',
      timestamp: new Date().toISOString()
    };
  } catch (error) {
    throw new Error(`简历刷新失败: ${error.message}`);
  }
}

/**
 * 获取简历刷新状态
 */
async function getResumeRefreshStatus(page) {
  await page.goto('https://www.zhipin.com/web/geek/profile');

  const status = await page.evaluate(() => {
    const refreshBtn = document.querySelector('.refresh-resume-btn');
    const isDisabled = refreshBtn?.hasAttribute('disabled');
    const nextTime = document.querySelector('.next-refresh-time')?.textContent;
    const lastRefreshTime = document.querySelector('.last-refresh-time')?.textContent;

    return {
      canRefresh: !isDisabled,
      nextRefreshTime: nextTime,
      lastRefreshTime: lastRefreshTime
    };
  });

  return status;
}

三、中优先级功能 (P1)

3.1 账号保活功能 ⚠️ 待开发

功能说明

模拟真实用户行为,防止账号被标记为机器人

MQTT指令

  • auto_active - 执行保活操作

实现要点

// platforms/boss/active.js

/**
 * 执行保活操作
 * @param {Page} page - Playwright页面对象
 * @param {Object} options - 保活选项
 */
async function performActiveActions(page, options = {}) {
  const {
    duration = 300000,         // 持续时间(ms) 默认5分钟
    actions = ['browse', 'search', 'view_company', 'scroll']
  } = options;

  const startTime = Date.now();
  const actionsPerformed = [];

  while (Date.now() - startTime < duration) {
    // 随机选择一个行为
    const action = actions[Math.floor(Math.random() * actions.length)];

    try {
      switch (action) {
        case 'browse':
          await browseJobList(page);
          break;
        case 'search':
          await performRandomSearch(page);
          break;
        case 'view_company':
          await viewRandomCompany(page);
          break;
        case 'scroll':
          await randomScroll(page);
          break;
      }

      actionsPerformed.push({
        action,
        timestamp: new Date().toISOString()
      });

      // 随机延迟
      await randomDelay(5000, 15000);

    } catch (error) {
      console.error(`保活操作失败: ${action}`, error);
    }
  }

  return {
    success: true,
    duration: Date.now() - startTime,
    actionsPerformed: actionsPerformed.length,
    details: actionsPerformed
  };
}

/**
 * 浏览职位列表
 */
async function browseJobList(page) {
  await page.goto('https://www.zhipin.com/web/geek/jobs', {
    waitUntil: 'networkidle'
  });

  // 随机滚动
  for (let i = 0; i < 3; i++) {
    await page.evaluate(() => {
      window.scrollBy(0, Math.random() * 500 + 200);
    });
    await randomDelay(1000, 3000);
  }
}

/**
 * 执行随机搜索
 */
async function performRandomSearch(page) {
  const keywords = ['前端', '后端', 'Java', 'Python', '产品经理', '运营'];
  const keyword = keywords[Math.floor(Math.random() * keywords.length)];

  await page.goto(`https://www.zhipin.com/web/geek/jobs?query=${encodeURIComponent(keyword)}`);
  await page.waitForTimeout(2000);

  // 随机点击一个职位查看
  const jobCards = await page.$$('.job-card-wrapper');
  if (jobCards.length > 0) {
    const randomIndex = Math.floor(Math.random() * Math.min(jobCards.length, 5));
    await jobCards[randomIndex].click();
    await page.waitForTimeout(3000);
  }
}

/**
 * 查看随机公司
 */
async function viewRandomCompany(page) {
  await page.goto('https://www.zhipin.com/web/geek/jobs');
  await page.waitForTimeout(2000);

  const companyLinks = await page.$$('.company-name a');
  if (companyLinks.length > 0) {
    const randomIndex = Math.floor(Math.random() * Math.min(companyLinks.length, 5));
    await companyLinks[randomIndex].click();
    await page.waitForTimeout(5000);
  }
}

/**
 * 随机滚动
 */
async function randomScroll(page) {
  const scrollTimes = Math.floor(Math.random() * 5) + 2;

  for (let i = 0; i < scrollTimes; i++) {
    await page.evaluate(() => {
      const direction = Math.random() > 0.5 ? 1 : -1;
      const distance = Math.random() * 300 + 100;
      window.scrollBy(0, direction * distance);
    });

    await randomDelay(800, 2000);
  }
}

3.2 智能聊天功能 ⚠️ 待完善

功能说明

自动回复HR消息提高响应率

MQTT指令

  • send_greeting - 发送打招呼消息
  • auto_reply_chat - 自动回复聊天

实现要点

// platforms/boss/chat.js

/**
 * 发送打招呼消息
 * @param {Page} page - Playwright页面对象
 * @param {string} bossId - HRID
 * @param {string} message - 消息内容
 */
async function sendGreeting(page, bossId, message) {
  // 进入聊天页面
  await page.goto(`https://www.zhipin.com/web/geek/chat?bosId=${bossId}`);
  await page.waitForSelector('.chat-input', { timeout: 5000 });

  // 输入消息
  await page.fill('.chat-input', message);
  await randomDelay(500, 1500);

  // 发送
  await page.click('.send-btn');
  await page.waitForTimeout(1000);

  return {
    success: true,
    bossId,
    message,
    timestamp: new Date().toISOString()
  };
}

/**
 * 自动回复聊天
 * @param {Page} page - Playwright页面对象
 * @param {Object} options - 回复选项
 */
async function autoReplyChat(page, options = {}) {
  const {
    checkInterval = 60000,    // 检查间隔(ms)
    replyDelay = 30000,       // 回复延迟(ms) 模拟思考时间
    maxReplies = 10           // 最多回复次数
  } = options;

  let repliedCount = 0;

  // 获取未读消息
  await page.goto('https://www.zhipin.com/web/geek/chat');
  await page.waitForTimeout(2000);

  const unreadChats = await page.$$('.chat-item.unread');

  for (const chat of unreadChats) {
    if (repliedCount >= maxReplies) break;

    try {
      // 点击进入聊天
      await chat.click();
      await page.waitForTimeout(1000);

      // 获取最后一条消息
      const lastMessage = await page.$eval('.message-list .message-item:last-child .message-text',
        el => el.textContent.trim()
      );

      // 调用服务端AI生成回复
      // 这里需要通过MQTT发送消息给服务端服务端调用AI生成回复后返回
      // 为了简化,这里暂时使用预设回复
      const reply = generateAutoReply(lastMessage);

      // 等待一段时间,模拟真人思考
      await randomDelay(replyDelay * 0.8, replyDelay * 1.2);

      // 发送回复
      await page.fill('.chat-input', reply);
      await page.click('.send-btn');
      await page.waitForTimeout(1000);

      repliedCount++;

    } catch (error) {
      console.error('自动回复失败:', error);
    }
  }

  return {
    success: true,
    totalUnread: unreadChats.length,
    replied: repliedCount
  };
}

/**
 * 生成自动回复简化版实际应调用服务端AI
 */
function generateAutoReply(message) {
  const lowerMessage = message.toLowerCase();

  if (lowerMessage.includes('面试') || lowerMessage.includes('interview')) {
    return '好的,感谢您的面试邀请!请问具体的面试时间和地点是什么?';
  }

  if (lowerMessage.includes('简历') || lowerMessage.includes('resume')) {
    return '您好,我已经投递了简历,请查收。如有任何问题,欢迎随时沟通!';
  }

  if (lowerMessage.includes('薪资') || lowerMessage.includes('salary')) {
    return '关于薪资,我的期望是根据岗位要求和市场情况来定的,具体可以详细沟通。';
  }

  // 默认回复
  return '收到,感谢您的消息!';
}

四、低优先级功能 (P2)

4.1 数据监控上报

/**
 * 上报性能数据
 */
async function reportMetrics(metrics) {
  const reportData = {
    action: 'report_metrics',
    metrics: {
      ...metrics,
      timestamp: new Date().toISOString(),
      deviceInfo: {
        platform: process.platform,
        memory: process.memoryUsage(),
        uptime: process.uptime()
      }
    }
  };

  await mqttClient.publish(`device/${sn_code}/metrics`, JSON.stringify(reportData));
}

4.2 截图上报

/**
 * 截图并上报
 */
async function captureAndReport(page, reason) {
  const screenshot = await page.screenshot({ fullPage: false });
  const base64 = screenshot.toString('base64');

  await mqttClient.publish(`device/${sn_code}/screenshot`, JSON.stringify({
    reason,
    image: base64,
    timestamp: new Date().toISOString()
  }));
}

五、开发规范

5.1 错误处理

所有函数必须包含try-catch并返回标准格式

{
  code: 0,           // 0成功-1失败
  message: '操作成功',
  data: {},          // 返回数据
  taskId: 'xxx',     // 任务ID
  timestamp: 'ISO时间'
}

5.2 日志记录

const logger = require('../utils/logger');

logger.info('操作成功', { action: 'search_jobs', jobCount: 50 });
logger.error('操作失败', { action: 'apply_job', error: error.message });
logger.warn('检测到异常', { type: 'bot_detection' });

5.3 配置管理

// config/default.json
{
  "mqtt": {
    "broker": "mqtt://192.144.167.231:1883",
    "username": "admin",
    "password": "public"
  },
  "browser": {
    "headless": false,
    "slowMo": 50,
    "defaultTimeout": 30000
  },
  "antibot": {
    "randomDelay": {
      "min": 1000,
      "max": 3000
    },
    "humanBehavior": true
  },
  "limits": {
    "maxApplyPerHour": 20,
    "maxApplyPerDay": 100,
    "maxSearchPerHour": 50
  }
}

六、测试计划

6.1 单元测试

为每个模块编写单元测试:

// tests/platforms/boss/search.test.js
describe('Boss搜索模块', () => {
  test('URL解析正确', () => {
    const url = 'https://www.zhipin.com/web/geek/jobs?city=101020100&query=全栈';
    const params = parseSearchUrl(url);
    expect(params.city).toBe('101020100');
    expect(params.query).toBe('全栈');
  });

  test('搜索执行成功', async () => {
    const jobs = await searchJobsEnhanced(page, {
      city: '101020100',
      query: '前端工程师'
    });
    expect(jobs.length).toBeGreaterThan(0);
  });
});

6.2 集成测试

测试完整的MQTT指令流程

// tests/integration/search.test.js
describe('搜索功能集成测试', () => {
  test('通过MQTT执行搜索', async () => {
    const response = await mqttClient.publishAndWait('test_device', {
      action: 'search_jobs_enhanced',
      platform: 'boss',
      params: {
        city: '101020100',
        query: '测试工程师'
      }
    });

    expect(response.code).toBe(0);
    expect(response.data.jobs).toBeDefined();
  });
});

七、部署和运维

7.1 环境要求

  • Node.js 18+
  • Chromium/Chrome 浏览器
  • 充足的内存建议2GB+
  • 稳定的网络连接

7.2 启动命令

# 开发模式
npm run dev

# 生产模式
npm run start

# 后台运行Linux
pm2 start src/index.js --name "job-client"

7.3 监控指标

  • 任务执行成功率
  • 平均响应时间
  • 反爬虫触发次数
  • 账号状态(正常/异常)

八、开发时间估算

功能模块 优先级 预计工时 备注
增强搜索功能 P0 3天 含URL解析和参数映射
批量投递功能 P0 2天 含频率控制
简历刷新功能 P0 1天 较简单
账号保活功能 P1 2天 需模拟多种行为
智能聊天功能 P1 3天 需AI对接
数据监控上报 P2 1天 辅助功能
总计 - 12天 约2.5周

九、风险和注意事项

9.1 反爬虫风险

  • 操作频率过高可能触发验证码
  • 建议设置合理的延迟和频率限制
  • 模拟真实用户行为(鼠标移动、随机滚动)

9.2 账号安全

  • 避免同时操作过多账号
  • 登录后保持Cookie有效期
  • 定期执行保活操作

9.3 性能问题

  • 浏览器实例占用较多内存
  • 建议实现浏览器实例池管理
  • 长时间运行需要定期重启

十、参考资料


文档版本: v1.0 最后更新: 2025-12-25 维护者: 开发团队