Skill: wangyapersonal/liepin-assistant (version 1.0.11)

--- name: liepin-assistant description: | 猎聘求职助手，封装 Liepin MCP 服务。用于搜索职位、查看 JD、投递简历、管理简历。 **触发场景**： (1) 用户提到"猎聘"、"liepin"、"liepin求职"、"猎聘求职"、"猎聘助手"、"liepin助手"、"找工作"、"搜职位"、"投简历"、"查看简历" (2) 用户提供猎聘 token 并要求配置 (3) 用户要搜索职位、查看 JD、投递岗位 **必须配置凭证**。凭证获取：https://www.liepin.com/mcp/server → 登录 → 生成凭证。设置环境变量 LIEPIN_TOKEN（推荐）或运行 set-token.js 将 token 写入 config.json。 --- # 猎聘求职 ## 快速使用 1. 配置 Token：用户发送「设置猎聘token xxx」 2. 搜索职位：说「搜前端职位」 3. 投递简历：确认后说「投递这个职位」 ## Token 配置 ### 方式一（推荐）：环境变量 **最安全**，token 不落地，只存在于当前进程环境： ```bash export LIEPIN_TOKEN=<你的token> ``` ### 方式二：配置文件 如果环境变量未设置，则回退到 config.json： ```bash node scripts/set-token.js <token> ``` 查看当前 token 状态： ```bash node scripts/set-token.js --show ``` **清除已保存的 token：** ```bash node scripts/set-token.js --clear ``` Token 获取方式：打开 https://www.liepin.com/mcp/server → 登录 → 生成凭证 → 复制 token（有效期 90 天） --- ## 工作流程 ### 1. 搜索职位 关键词、地点均可选；jobKind 通常为 "2"（正式职位）： ```bash node scripts/liepin-mcp.js user-search-job '{"jobName":"AI","address":"北京"}' ``` ### 2. 投递职位 **jobId 必须是数字类型**（从搜索结果的职位ID获取），jobKind 从搜索结果获取（通常为 "2"）： ```bash node scripts/liepin-mcp.js user-apply-job '{"jobId":81543059,"jobKind":"2"}' ``` ### 3. 查看我的简历 ```bash node scripts/liepin-mcp.js my-resume '{}' ``` ### 4. 补充简历信息 修改简历各项（基本信息、工作经历、教育经历、项目经历、求职期望、自我评价）： ```bash # 修改基本信息 node scripts/liepin-mcp.js modify-resume-base-info '{"realName":"姓名","sex":"男","birthday":"19950101"}' # 添加工作经历 node scripts/liepin-mcp.js add-work-exp '{"compName":"公司名","rwTitle":"职位名称","workStart":"202001","workEnd":"202312"}' ``` --- ## 注意事项 - Token 有效期 90 天，过期后重新生成并配置 - 频率限制 60 次/分钟，搜索/投递共用配额 - 每次投递前先展示职位详情，用户确认后再投 - jobId 必须是**数字类型**（不是字符串），jobKind 为字符串 - 所有操作记录可在猎聘 App 查看 - **优先读取环境变量 `LIEPIN_TOKEN`**，未设置则回退到 config.json ## 错误排查 | 现象 | 原因 | 方案 | |-----|------|-----| | "token not configured" | 未设置过 token | 用户发送"设置猎聘token xxx" | | "Request failed" | 网络问题 | 等待几秒重试 | | errCode != 0 | 业务错误 | 查看返回的具体错误信息 |

path: references/api.md
content: # 猎聘 MCP API 参考

## 基本信息

- **MCP 端点**: `https://open-agent.liepin.com/mcp/user`
- **认证**: Header `x-user-[REDACTED_SECRET]`
- **协议**: MCP (Model Context Protocol) over HTTPS，JSON-RPC 2.0
- **调用方式**: 所有工具通过 `tools/call` 方法调用，传入 `name` 和 `arguments`
- **频率限制**: 60 次/分钟（含搜索、查看、简历更新、投递）
- **Token 有效期**: 90 天

## 获取 Token

1. 打开 https://www.liepin.com/mcp/server 并登录猎聘
2. 点击「生成凭证」获取 `x-user-token` 值
3. 发送给我：`设置猎聘token <token值>`

## MCP 方法（统一通过 tools/call 调用）

### user-search-job

搜索职位。

```bash
node scripts/liepin-mcp.js user-search-job '{"jobName":"AI","address":"北京","page":0}'
```

| 参数 | 类型 | 说明 |
|-----|------|-----|
| jobName | string | 职位名称关键词 |
| address | string | 工作地点 |
| workExperience | string | 工作经验要求 |
| eduLevel | string | 学历要求 |
| compNature | string | 公司性质（国企、外企、民营） |
| salaryFloor | string | 薪资下限 |
| salaryCap | string | 薪资上限 |
| salaryKind | string | 薪资类型（月薪/年薪） |
| companyName | string | 公司名称 |
| page | number | 分页页码（0表示第1页） |

---

### user-apply-job

投递职位。**jobId 和 jobKind 均为必填，且 jobId 必须是数字类型**。

```bash
node scripts/liepin-mcp.js user-apply-job '{"jobId":81543059,"jobKind":"2"}'
```

| 参数 | 类型 | 说明 |
|-----|------|-----|
| jobId | number | 职位ID（**数字类型**，从搜索结果获取） |
| jobKind | string | 职位类型（从搜索结果获取，通常为 "2"） |

---

### my-resume

获取当前用户的简历原始内容。

```bash
node scripts/liepin-mcp.js my-resume '{}'
```

---

### add-work-exp / add-edu-exp / add-project-exp

添加工作/教育/项目经历。

```bash
node scripts/liepin-mcp.js add-work-exp '{"compName":"公司名","rwTitle":"职位名称","workStart":"202001","workEnd":"202312"}'
```

---

### modify-resume-base-info

修改简历基本信息。

```bash
node scripts/liepin-mcp.js modify-resume-base-info '{"realName":"姓名","sex":"男","birthday":"19950101","cityCode":"北京","nowWorkStatus":"1"}'
```

---

### modify-job-want / add-job-want

修改/添加求职期望。

```bash
node scripts/liepin-mcp.js modify-job-want '{"id":123,"wantSalaryLow":20000,"wantSalaryHigh":40000}'
```

---

## 内部实现

MCP 客户端（`liepin-mcp.js`）统一使用 `tools/call` 协议：

```json
{
  "jsonrpc": "2.0",
  "id": <timestamp>,
  "method": "tools/call",
  "params": {
    "name": "<toolName>",
    "arguments": { ... }
  }
}
```

## 错误处理

| 错误信息 | 原因 | 处理 |
|---------|------|------|
| token not configured | 未设置猎聘 token | 用户发送"设置猎聘token xxx" |
| Request failed | 网络错误 | 重试 |
| errCode != 0 | 业务错误 | 查看返回的 errCode |


path: scripts/liepin-mcp.js
content: #!/usr/bin/env node
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');

const MCP_ENDPOINT = 'https://open-agent.liepin.com/mcp/user';
const TOKEN_ENV_VAR = 'LIEPIN_TOKEN';

function findSkillDir() {
  if (process.env.LIEPIN_SKILL_DIR) return process.env.LIEPIN_SKILL_DIR;
  var scriptDir = path.dirname(__filename);
  var skillRoot = path.dirname(scriptDir);
  var name = path.basename(skillRoot);
  if (name === 'liepin-assistant' || name === 'liepin') return skillRoot;
  var home = os.homedir();
  var defaults = [
    path.join(home, '.openclaw', 'workspace', 'skills', 'liepin-assistant'),
    path.join(home, '.openclaw', 'workspace', 'skills', 'liepin'),
    path.join(home, '.openclaw', 'skills', 'liepin-assistant'),
    path.join(home, '.openclaw', 'skills', 'liepin'),
  ];
  for (var i = 0; i < defaults.length; i++) {
    if (fs.existsSync(path.join(defaults[i], 'SKILL.md'))) return defaults[i];
  }
  return skillRoot;
}

function loadConfig() {
  // 1. 优先从环境变量读取
  if (process.env[TOKEN_ENV_VAR]) {
    return { [REDACTED_SECRET] };
  }

  // 2. 降级到 config.json
  var skillDir = findSkillDir();
  var configPath = path.join(skillDir, 'config.json');
  if (!fs.existsSync(configPath)) {
    console.error('Error: token not configured. Set LIEPIN_TOKEN env var, or run "set-liepin-token <your-token>" first.');
    process.exit(1);
  }
  var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  if (!config.token) {
    console.error('Error: token is empty. Please configure again.');
    process.exit(1);
  }
  return config;
}

function mcpRequest(toolName, arguments_) {
  return new Promise(function(resolve, reject) {
    var config = loadConfig();
    var id = Date.now();

    var bodyObj = {
      jsonrpc: '2.0',
      id: id,
      method: 'tools/call',
      params: {
        name: toolName,
        arguments: arguments_ || {}
      }
    };
    var body = JSON.stringify(bodyObj);

    var url = new URL(MCP_ENDPOINT);
    var options = {
      hostname: url.hostname,
      port: url.port || 443,
      path: url.pathname,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        'x-user-token': config.token,
        'Accept': 'application/json, text/event-stream'
      },
      timeout: 30000
    };

    var protocol = url.protocol === 'https:' ? https : http;
    var req = protocol.request(options, function(res) {
      var chunks = [];
      res.on('data', function(chunk) { chunks.push(chunk); });
      res.on('end', function() {
        var raw = chunks.join('').trim();
        if (!raw) { resolve({ result: null }); return; }

        var lines = raw.split('\n');
        for (var i = 0; i < lines.length; i++) {
          var trimmed = lines[i].trim();
          if (trimmed.startsWith('data:')) {
            var jsonStr = trimmed.slice(5).trim();
            try {
              var obj = JSON.parse(jsonStr);
              if (obj.result !== undefined) { resolve(obj); return; }
              if (obj.error) { resolve(obj); return; }
            } catch (e) {}
          }
        }

        if (raw.slice(0, 5).toLowerCase() === '<html' || raw.includes('<!DOCTYPE') || raw.includes('<!doctype')) {
          reject(new Error('Server returned HTML (possibly login page or 404). Check: (1) Token is valid; (2) API endpoint is correct.'));
          return;
        }

        try { resolve(JSON.parse(raw)); return; } catch (e) {}

        for (var j = 0; j < lines.length; j++) {
          var line = lines[j].trim();
          if (line && !line.startsWith('data:')) {
            try { resolve(JSON.parse(line)); return; } catch (e) {}
          }
        }
        reject(new Error('Failed to parse response: ' + raw.slice(0, 200)));
      });
    });

    req.on('error', function(e) { reject(e); });
    req.on('timeout', function() { req.destroy(); reject(new Error('Request timeout')); });
    req.write(body);
    req.end();
  });
}

var args = process.argv.slice(2);
if (args.length === 0) {
  console.error('Usage: node liepin-mcp.js <toolName> [paramsJson]');
  console.error('Example: node liepin-mcp.js user-search-job \'{"jobName":"AI","address":"北京"}\'');
  console.error('         node liepin-mcp.js user-apply-job \'{"jobId":81543059,"jobKind":"2"}\'');
  process.exit(1);
}

var toolName = args[0];
var toolArgs = {};
if (args.length > 1) {
  try { toolArgs = JSON.parse(args[1]); }
  catch (e) {
    console.error('Invalid params JSON: ' + args[1]);
    process.exit(1);
  }
}

mcpRequest(toolName, toolArgs)
  .then(function(result) {
    if (result.error) {
      console.error('MCP Error: ' + result.error.message + ' (code: ' + result.error.code + ')');
      process.exit(1);
    }
    if (result.result && result.result.content) {
      var text = result.result.content[0].text;
      try {
        var parsed = JSON.parse(text);
        console.log(JSON.stringify(parsed, null, 2));
      } catch (e) {
        console.log(text);
      }
    } else {
      console.log(JSON.stringify(result, null, 2));
    }
  })
  .catch(function(err) {
    console.error('Request failed: ' + err.message);
    process.exit(1);
  });


path: scripts/set-token.js
content: #!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var os = require('os');

var TOKEN_ENV_VAR = 'LIEPIN_TOKEN';

function findSkillDir() {
  if (process.env.LIEPIN_SKILL_DIR) return process.env.LIEPIN_SKILL_DIR;
  var scriptDir = path.dirname(__filename);
  var skillRoot = path.dirname(scriptDir);
  var name = path.basename(skillRoot);
  if (name === 'liepin-assistant' || name === 'liepin') return skillRoot;
  var home = os.homedir();
  var defaults = [
    path.join(home, '.openclaw', 'workspace', 'skills', 'liepin-assistant'),
    path.join(home, '.openclaw', 'workspace', 'skills', 'liepin'),
    path.join(home, '.openclaw', 'skills', 'liepin-assistant'),
    path.join(home, '.openclaw', 'skills', 'liepin'),
  ];
  for (var i = 0; i < defaults.length; i++) {
    if (fs.existsSync(path.join(defaults[i], 'SKILL.md'))) return defaults[i];
  }
  return skillRoot;
}

var skillDir = findSkillDir();
var configPath = path.join(skillDir, 'config.json');

function showStatus() {
  // 优先检查环境变量
  if (process.env[TOKEN_ENV_VAR]) {
    var t = process.env[TOKEN_ENV_VAR];
    console.log('Token:', t.slice(0, 4) + '****' + t.slice(-4), '(from env LIEPIN_TOKEN)');
    return;
  }
  if (!fs.existsSync(configPath)) {
    console.log('Token: 未设置 (env LIEPIN_TOKEN 未设置，config.json 也不存在)');
  } else {
    var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    var t = config.token || '';
    if (!t) console.log('Token: 未设置');
    else console.log('Token:', t.slice(0, 4) + '****' + t.slice(-4), '(from config.json)');
  }
}

var args = process.argv.slice(2);
if (args.length === 0 || args[0] === '--show') {
  showStatus();
} else if (args[0] === '--clear') {
  if (fs.existsSync(configPath)) { fs.unlinkSync(configPath); console.log('已清除 config.json'); }
  else console.log('没有 config.json 可清除');
} else {
  var dir = path.dirname(configPath);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(configPath, JSON.stringify({ [REDACTED_SECRET] }, null, 2));
  console.log('已保存到 config.json');
  console.log('Tip: 也可以设置环境变量 LIEPIN_TOKEN 更安全');
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)