# 候选人库

Version: 829db5245701f2f8

模块地址：https://dev.planahr.top/api/v1/candidate-library/tools；权限 candidates:read / candidates:write。写权限不包含读权限，均不扩大用户自身权限。

操作名：search、detail、contacts、notes、documentInfo、documents、resumeContent、businessInfo、suggestions、duplicates、create、update、note、changeStatus、changeHistory。工具名称前缀 candidate_library_ 不用于 POST 的 operation。

GET 加 ?operation=操作名 只取单项 schema；?view=summary 只取目录。POST 不带查询参数，使用 {operation,input} 和 Content-Type: application/json。

```bash
curl 'https://dev.planahr.top/api/v1/candidate-library/tools?operation=search' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

- 所有 ID 都从实际接口结果取得。候选人 ID 是字符串，岗位/客户 ID 是整数；成员使用 context 返回的 membership ID，不用姓名猜身份。
- 新建前 duplicates，疑似重复交用户判断，不自动合并。候选人库与脉脉筛选数据库独立。
- changeHistory({id,page?,pageSize?}) 分页查询候选人审批历史，需 candidates:read；只返回申请编号、发起人、来源、时间、状态和飞书入口，不返回修改前后值、原因、表单或联系方式。网页关闭已完成提示只影响当前用户当前浏览器，不删除记录、不修改审批状态，也不解除未完成审批的锁定。
- 修改主档：当前资料负责人直接 update；非负责人（包括管理员）使用同一 update 加必填 reason，发起飞书原生审批。需要 candidates:write；先用 candidates:read 读取 detail 的 version 和 permissions.canEdit/canRequestEdit。返回 editOutcome=approval_pending 只表示申请已接收，绝不表示资料已修改。重试原样复用 requestId；调用 changeStatus({id})（candidates:read）核对最终状态，建议不少于 15 秒一次。queued/pending 为审批中，review 需管理员核对且仍锁定；applied 才表示已写入，rejected/canceled/failed 不写入。423 CANDIDATE_APPROVAL_LOCKED 时不要重发或换 ID 绕过。审批人和知会人在飞书配置，Agent 不能指定或代批。审批中不能再编辑/上传附件，仍可追加备注和推进流程；系统负责人转交不改变已经发起的飞书审批。被退回仍按审批中处理，可在飞书继续处理或撤回。
- 同组织普通同事可读取所有候选人资料，不要求负责人/协作者关系，网页与 Agent 一致；主档修改遵循上条负责人/审批规则，附件上传仍校验原权限，不开放跨组织或作者私密流程笔记。note({id,content,requestId}) 允许同组织有效顾问追加候选人共享跟进备注，需 candidates:write，无需转交负责人；不能修改/删除已有备注，不能提交作者或来源。作者、时间、web/agent 来源由服务端记录，重试复用 requestId。note 回执不返回已有备注、电话或原始简历；读 notes 仍需 candidates:read + sensitive:notes。detail 只给摘要；contacts 需 candidates:read + sensitive:contacts，返回库内电话/微信/邮箱；businessInfo 需 sensitive:notes。documentInfo 与 GET /api/v1/candidate-library/{id}/document?download=0 或 1 需 candidates:read + sensitive:documents，仍用相同 Bearer。不要猜附件路径。
- 流程详情中的历史/备注同样需 sensitive:notes；旧连接不会自动获得新权限，确有业务需要才重新登录申请。电话/邮箱查重参数也需 sensitive:contacts。所有读取均留痕，敏感读取可能返回 429 / SENSITIVE_ACCESS_LIMIT；不要换会话或 Agent 绕过用户级限制。
- 完整履历：create/update 的 data 可选 rawResumeText（原样保留换行）、workExperiences、educationExperiences。工作段字段 company/title/startDate/endDate/current/description；教育段 school/degree/major/startDate/endDate/description。未知日期用 null 或省略，日期为 YYYY、YYYY-MM 或 YYYY-MM-DD。update 省略字段不变，显式 [] 清空对应经历数组；提交数组整体替换。detail 的 workExperienceSummary / educationExperienceSummary 只给基本字段，不给描述；resumeContent 读取同组织全文和完整经历，需 candidates:read + sensitive:documents。列表和写入回执不返回原文。历史字段不重解析、不改写；无法规范化的旧 experience/data 在敏感结果的 legacyContent 中保留。
- 业务字段：create/update.data 可选 currentSalary、expectedSalary（{min?,max?,currency:"CNY",period:"month"|"year",months?,basis?:"gross"|"net"|"unknown",negotiable?}，金额用实际单位，不填30k字符串）、jobSeekingStatus（open/considering/not_looking/unknown）、availability（{type:"date",date:"YYYY-MM-DD"} 或 {type:"notice",days:30} 或 {type:"negotiable"}）、sourceChannel（maimai/liepin/referral/cold_call/manual/other或真实自定义值）、departureReason、degreeLevel。省略保留，null清除；未知不填0或猜测。薪资/离职原因只由 businessInfo({id}) 返回，需 candidates:read + sensitive:notes，受审计；不会塞入普通摘要。suggestions({kind:"skill"|"company"|"title",query,limit}) 返回短标签建议。skills只放真实短标签，长句会保留进敏感履历原文，经历应填结构化数组。
- 脉脉关联：create/update 的 data.maimaiUid 填候选人的数字 UID 字符串（不是库内 id，也不是顾问账号 UID）。录入脉脉来源候选人时保留搜索/详情结果提供的确切 UID；不知道就留空，不按姓名猜测。省略字段保留原关联，空字符串解除关联。人工链接字段 maimaiProfileUrl 由服务器提取 UID，仅支持可确定 UID 的原生个人页链接，不保存链接/令牌；webuid、分享码不能当 UID。search/detail 返回 maimaiUid，网页可使用有权限的渠道打开个人页；Agent 业务接口不因此获得 Cookie 或浏览器控制权限。
- 附件上传不是 JSON 工具调用：POST /api/v1/candidate-library/{id}/documents，Authorization 仍为 Bearer，使用 multipart/form-data，唯一文件字段 file，其他字段 version（当前候选人版本）、kind（resume/screenshot/other，默认 resume）、requestId。需要 candidates:write + 对象编辑权限；上传人由登录身份确定，不能指定。不要把文件转成 Base64。GET tools?operation=documents 的 binaryUpload 给出元数据 inputSchema 与实际限制。默认每件 20 MiB，履历正文+经历合计 200000 字符（经历按 JSON 长度），配置可调整。支持 PDF/DOC/DOCX/PNG/JPG/JPEG，扩展名与文件签名须匹配。
- documents 分页列出同组织附件 documentId/name/type/bytes/sha256/kind/uploadedAt/uploadedBy，需 candidates:read + sensitive:documents。使用 GET /api/v1/candidate-library/{id}/documents/{documentId}?download=1 下载原件，download=0 预览 PDF/图片，或已生成的 Word PDF 预览。历史主附件 ID 为 legacy，旧 /{id}/document 仍可用，没历史主附件时返回最新上传附件。一个候选人可加多份；同一截图可对多个候选人分别上传，产生各自独立关联与权限。写入返回更新后的 version；重试保留原 version、requestId、文件名、kind 和原文件。409 VERSION_CONFLICT 先读最新版本后用新 requestId，IDEMPOTENCY_CONFLICT 不可盲重试；413 缩小内容，415 检查类型，503 使用原请求有限重试。
- 候选人 Word 简历（含 legacy）已接入后台 PDF 转换。documents/documentInfo 的 preview.status 为 queued/converting/ready/failed/evicted；ready 时 download=0 返回 PDF，download=1 永远返回原件。原生 PDF/图片不复制。上传回执是当时状态，后续以列表/可用性为准；不要重传原件或高速轮询。用户要求重试且 retryAvailable=true 时，用 binaryUpload.previewRetry 的 POST /{id}/documents/{documentId}/preview，JSON {}，需 candidates:read + sensitive:documents，无需编辑权；documentId 可为 legacy 或列表提供的 UUID。202 已入队，ready 返回 200。重建 PDF 不计新录入，不改变上传人、负责人、版本或原件。
- 为已有候选人补完整简历：不要重建候选人。先 documents 读取附件、version、permissions.canUpload 与 extractionAvailable；沿用 multipart 上传真实 kind=resume 的 PDF/DOC/DOCX，稳定 requestId，成功后原件已保存。自动解析与 PDF 预览是独立任务；上传本身不会补字段。GET tools?operation=documents 的 resumeExtraction 给出解析/读取/确认接口及 schema。下面路径均相对 /api/v1/candidate-library/{id}，使用同一 Bearer。
- POST /extractions JSON {documentId,requestId}，需要 candidates:read + candidates:write + sensitive:documents，202 返回 extractionId/status，queued/running 不等于已补齐。GET /extractions/{extractionId} 需要 candidates:read + sensitive:documents，最多每5秒查询且只在确需时；ready 才返回 proposed/current/version/evidence/warnings。GET /extractions 只列本人最近10条状态，关闭窗口后可续查；不能读取他人的解析结果。失败用新 requestId 重新解析同一 documentId，不重传；旧请求重试原样复用 ID。扫描/图片 PDF 可能没有可提取文字，应换文字版或手工补充，不伪造经历。
- 保存前让用户核对本人身份和字段差异，默认只选原来为空的字段，不默认覆盖已有内容。POST /extractions/{extractionId}/apply JSON {version,fields:["graduationSchool","workExperiences"],requestId,reason?}，需要上述读写及文档 scope；fields 只允许选本次 proposed 的真实字段，不能提交自定义 data，数组选中会整体替换，须核对完整内容。负责人直接保存，非负责人必填 reason 并走现有飞书审批，approval_pending 不等于保存。409 VERSION_CONFLICT 重新读结果与最新 current 再确认，不自动覆盖；423 等审批结束。解析不改电话/微信/邮箱、归属或创建时间，不新增录入计数。结果最长7天、缓存有界，expired 可重解析，原件不随结果过期删除。
- search 可选 company/title（同时提供时必须匹配同一段任职经历）、school、sourceChannel、jobSeekingStatus、hasDocument（yes/no）、createdFrom/createdTo（YYYY-MM-DD，北京时间含首尾日期）、degree（支持associate等及以上、unknown只匹配待补充）。期望薪资交集筛选用 expectedSalaryMin/expectedSalaryMax + salaryCurrency/ salaryPeriod（默认CNY/month），需额外 sensitive:notes 并受审计；不跨币种、周期换算，未知薪资不按0算。search列表只给附件是否存在，不给附件路径、薪资或全文。
- update 仅当前资料负责人直接保存；非负责人（包括管理员）必须提供 reason 并走飞书审批。先读 detail 的 version、owner、permissions.canEdit/canRequestEdit，只提交实际变化的 data 字段。不能用 update 改上传人、资料负责人或上传时间。审批回执不是资料保存回执：用 changeStatus 核对 applied 后才可向用户报告修改成功。403 业务权限不足时停止，不换凭证绕过；409 版本冲突须读取最新资料、重新核对修改，不自动覆盖。

以下仅为虚构模板；ID、version 必须从真实读取结果取得，写入 requestId 为每个逻辑动作生成且重试保持不变。

### 分页搜索候选人（只读）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "search",
  "input": {
    "query": "芯片",
    "page": 1,
    "pageSize": 20
  }
}
```

### 读取候选人详情和版本（只读）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "detail",
  "input": {
    "id": "1"
  }
}
```

### 检查可能重复的候选人（只读）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "duplicates",
  "input": {
    "name": "示例候选人"
  }
}
```

### 追加跟进备注（写入示例）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "note",
  "input": {
    "id": "1",
    "content": "示例：候选人希望先了解岗位职责。",
    "requestId": "example-note-001"
  }
}
```

### 负责人按版本修改候选人（写入示例）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "update",
  "input": {
    "id": "1",
    "version": 0,
    "data": {
      "city": "上海"
    },
    "requestId": "example-update-001"
  }
}
```

### 申请修改他人负责的资料（写入示例）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "update",
  "input": {
    "id": "1",
    "version": 0,
    "data": {
      "city": "上海"
    },
    "reason": "核实候选人目前常驻上海",
    "requestId": "example-approval-001"
  }
}
```

### 核对修改审批结果（只读）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "changeStatus",
  "input": {
    "id": "1"
  }
}
```

### 查看审批历史记录（只读）

POST /api/v1/candidate-library/tools

```json
{
  "operation": "changeHistory",
  "input": {
    "id": "1",
    "page": 1,
    "pageSize": 10
  }
}
```
