Files
prop-ai-hr/docs/API_INTEGRATION.md
T

223 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端 API 对接指南
本文维护当前前后端业务接口和安全边界;旧演示流继续保留降级链,但不能作为生产或试点通过证据。
## 第一阶段范围
| 页面 | 后端接口 | 处理 |
|---|---|---|
| AI面试 `/recruit/interview` | `POST /api/recruit/interview/start`、`/answer`、`/finish`、`GET /records`、`POST /records/{sessionId}/review` | 已接入真实模型优先链路:配置 chat 模型后 `/start` 动态生成面试题,`/finish` 按前端提交的真实回答做结构化评分;未配置模型时使用本地 Rubric 估分,不返回固定候选人分数。AI 分数只作辅助参考,HR/管理员可提交 `reviewedScore/reviewNote` 完成人工复核;记录同时保留 AI 分、人工复核分、最终采用分和复核状态 |
| 候选人入职主体关联 `/recruit/interview` | `GET/POST /api/recruit/interview/candidate-links` | HR/管理员在当前租户范围内把本地候选人 ID 关联到已同步的在职 `ext_party_id`;只保存外部主体 ID,不复制姓名、部门等组织字段,重复关联同一主体幂等,候选人更换主体或同一主体已关联其他候选人会拒绝 |
| 三角色对练 `/train/practice` | `POST /api/train/practice/start`、`/turn`、`/finish` | 已接入编排 API;数据库启用 chat 模型后,`/turn` 客户回复按人设走真 LLM 生成(seed 剧本作剧情锚点),`/finish` 走单次 temperature=0 结构化评分(4 维分+导师改写+点评);移动端 `turn/finish` 校验当前手机号与启动会话归属,未知或他人会话拒绝;模型未配置或调用失败自动回退 seed,契约不变 |
| 正式试点数据导出 | `GET /api/train/practice/export?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD` | 起止日期必填且包含结束日;只统计窗口内能通过唯一手机号或外部 ID 映射到在职组织快照的正式会话,排除重复手机号和身份碰撞。完训定义为每人至少 10 次,校准必须关联同一窗口内正式会话;CSV 同时给出校准命中数、SOP 可用数、满意度响应数/平均分,以及明细级 AI 分、人工校准分、校准人、校准时间、最终采用分、满意度分和意见,避免用四舍五入后的比率反推门禁状态;汇总和明细均携带正式人员及项目口径,不混入历史 seed/开发身份 |
| 对练语音 | `POST /api/ai/asr`(multipart 字段 `file`,≤5MB)、`POST /api/ai/tts`(JSON `{text≤300字, voice?, voiceProfile?:{role,voice?,speed?,emotion?}}`,旧 `voice` 兼容;成功返回 `ossId`,客户端播放地址为受控的内联 `data:audio/*`,不返回原始 OSS URL)、`GET /api/aihr/mobile/oss/{ossId}` | 走 OpenAI-compatible audio 接口;生产 SiliconFlow CosyVoice2 按角色映射老师傅/业主/面试官音色,业主对练再按已有情绪分切换平静/严肃/强烈语气和语速,设备语音降级同步调整语速与音高;模型管理需启用 `category=asr/tts` 配置;移动端优先用浏览器录音,`getUserMedia/MediaRecorder` 不可用或麦克风权限失败时,用 `audio/*` file input 选择/录制音频后继续调同一 ASR 接口;ASR/TTS 未配置或失败返回 fail,前端降级设备语音或文本;训练/每日题录音只通过 `audioOssId` 走受保护下载,历史客户端传入的 HTTP `audioUrl` 不再回显;TTS 成功音频仍写入 `sys_oss` 留痕,同时用内联 data URL 保持旧客户端可播放;音频下载按员工本人或主管项目范围授权,系统管理端保持后台访问 |
| 案例沉淀 `/knowledge/cases` | `GET /api/knowledge/case/capabilities`、`POST /api/knowledge/case/upload`、`/organize`、`/curate`、`GET /records`、`GET /records/{caseId}`、`POST /records/{caseId}/review`、`GET /records/{caseId}/media` | `/capabilities` 返回服务端判定的案例提交/查看能力,移动端不再向普通员工展示无权提交的素材表单;`/upload` 改为 multipart 真实语音上传并走 ASR,服务端只接受 MP3/WAV/M4A/WebM/OGG/AAC/FLAC,成功后原始音频写入 `sys_oss`,案例记录只保存 `mediaOssId` 供受保护媒体接口读取,不向客户端回传原始 `mediaUrl`;`/organize` 用真实转写调 chat 模型整理案例,未配置模型时按真实 transcript 本地结构化,并从背景外的真实摘要项提取学习点;APP 用户的项目范围从 `aihr_org_snapshot` 登录身份解析,上传、整理、入库、列表和详情均按项目范围校验,未完成正式组织映射时安全拒绝,不接受前端伪造项目范围;员工列表/详情只返回 `已入库` 案例,管理端系统用户保留全局运营视图;移动端和管理端案例详情通过受保护媒体接口回放原始音频,主管/项目负责人可提交脱敏点评;预渲染视频样片仍待正式媒体资产接入 |
| 案例媒体安全 | `GET /api/knowledge/case/records/{caseId}/media` | 案例详情只返回 `mediaOssId`,不返回原始 `sys_oss.url`;媒体下载会重复执行登录、后台角色或 APP 项目范围校验,再由服务端流式读取 OSS。管理端与 `mobile-uni` 通过鉴权 blob/temp 文件播放,关闭详情页时释放本地对象 URL |
| 问师傅与 SOP 知识库 `/pages/user/sop/index`、`/knowledge/sop` | `POST /api/knowledge/query`、`POST /api/knowledge/query-media`、`POST /api/knowledge/answer-feedback`、`GET /api/knowledge/position-sop`、`GET /api/aihr/mobile/onboard/tasks`、`POST /api/aihr/mobile/onboard/tasks/{id}/complete`、`GET /api/aihr/mobile/qualification`、`POST /api/knowledge/doc/upload` | 当前文字/现场附件查询已走统一知识空间授权和 MySQL Fulltext + Qdrant 混合召回;搜索返回 `reviewId` 与 `promptVersion`,员工反馈回传并保存该评审批次。现状仍是每次只提交当前问题的单轮 RAG,页面消息列表不等于服务端对话记忆;多轮指代、意图路由和原文件/视频交付按 [专项设计](20260718/问师傅多轮会话与原始资料交付设计.md) 实施。员工学习页按当前 APP 身份读取正式岗前/入职任务,资格证据无正式数据时明确返回 `NOT_CONFIGURED`;`position-sop` 保留一期生活顾问学习导航语义 |
| 资料处理 `/knowledge/processing` | `GET /api/knowledge/processing/overview`、`POST /api/knowledge/doc/upload-async`、`GET /api/knowledge/doc/upload-items`、`POST /api/knowledge/doc/upload-items/{id}/retry` | 已接入解析任务状态聚合;页面只保留“批量导入”,接口暂存+入队即秒回,后台 worker(并发 2)逐条解析/归类/向量化;ZIP 在 worker 内安全解压后把支持的子文件继续入同一批次队列,页面按批次轮询进度、失败可单文件重试;不提供浏览器目录选择或服务端目录导入入口 |
| 组织人员同步 | `POST /api/aihr/org/sync` | 从开放组织同步系统的 `/api/open/v1/sync/snapshot` 拉取 `company/department/employee` 快照,分页参数使用 `limit`;员工手机号只落 `person_phone` 用于移动端身份映射,不在组织列表响应暴露;岗位识别 `position/job_title/post/job_name/role/title` 等字段。`dryRun` 必须显式传入 `true`(预检)或 `false`(写入),省略或传 `null` 直接拒绝,避免空请求意外写库;默认 `replaceExisting=true`,写入前必须先用 `{"dryRun":true}`;dry-run 不访问本地快照表、不执行 DDL/写库,返回 `phoneLinked/maskedPhone/suspectText/warnings` 且 `syncedCount=0`。非 dry-run 覆盖写入遇到员工被跳过、手机号不完整、脱敏手机号或疑似乱码时默认拒绝,只有确认 dry-run 结果后显式传 `allowPartialReplace=true` 才允许覆盖;重复 `ext_party_id` 始终拒绝写入,因为数据库唯一键会折叠重复身份;`replaceExisting=false` 不触发不完整快照覆盖闸门,但仍拒绝重复身份。2026-07-15 线上已用 `/api/open/v1` 前缀完成配置、dry-run 和覆盖同步:开放平台返回公司 17、部门 963、员工 3417;生产 `aihr_org_snapshot` 为 3417 行,`phoneLinked=3392`、`maskedPhone=25`、`suspectText=0`,在职 2943、离职 474。当前剩余 25 人手机号不可用属于上游数据质量问题;功能发布不再被组织同步能力阻塞 |
| 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`)。prod 默认关闭,试点期只有同时设置 `AIHR_SMS_DEV_FIXED_CODE` 与 `AIHR_SMS_PROD_FIXED_CODE_ENABLED=true` 才启用固定码。 |
| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 未登录请求只返回不读取租户业务统计的公开首屏 seed;已登录移动端请求自动携带 `Authorization/clientid`,才返回员工/主管真实统计;移动端本地 fallback 保演示 |
| 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后按手机号匹配组织快照;`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端;接口失败回退员工端 |
| 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;训练完成后提交 `POST /api/aihr/mobile/practice/satisfaction`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。满意度接口只接受本人已完成训练的 1-5 分,意见脱敏后落库,未填写不补默认值。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管 `/practice/team` 在同一项目权限范围内额外返回 `mistakes` 聚合,按场景/归因统计次数、影响人数、平均分和最近发生时间;普通员工返回“无主管权限”。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度。每日三题正式只对 `hire_date` 在当前日期前三个月内的在职员工派发;没有入职日期时不使用训练次数推断,开发 Demo 的旧回退只有在 `dev/local` profile 且由 `aihr.practice.allow-legacy-daily-drill-fallback` 显式开启时生效,生产 profile 强制关闭。 |
| 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;面试支持文字作答和复用 `/api/ai/asr` 的录音转文字/选音频,`/answer` 可带可选 `answerAudioOssId`,服务端只接受当前候选人名下处于 `staged/bound` 状态的音频并将 `ossId` 写入问题快照,不向客户端暴露原始 OSS URL;`/answer` 与 `/finish` 还会校验当前 APP 手机号与启动会话的候选人 ID 一致,未知或他人会话直接拒绝;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 |
| 每日题反馈与常见难题 | `POST /api/aihr/mobile/practice/assignments/{id}/feedback`、`GET /api/aihr/mobile/practice/difficulties`、`POST /api/aihr/mobile/practice/difficulties/assign` | 反馈区分题目有用性与答案正确性;主管/管理端按岗位、时间、次数、人数和均分聚合,并可基于难题创建专项;身份和项目范围由服务端解析 |
| 岗位考试 | 员工 `GET /api/aihr/mobile/exams`、`GET /exams/{id}`、`POST /exams/{id}/submit`;主管 `GET/POST /exams/supervisor`、`GET/PUT /exams/supervisor/{id}`、`POST /exams/supervisor/{id}/publish`、`GET /exams/supervisor/{id}/results` | 主管按项目范围选择真实员工、组卷、发布和查成绩;员工只读本人目标考试。发布和提交接受请求 ID 并保持幂等,不得重复生成成绩或奖励 |
| 开放问题榜与积分 | 移动端 `/api/aihr/community/questions`、`questions/{id}`、`questions/{id}/answers`、`questions/{id}/best-answer`、`points/me`;后台 `/api/aihr/question-admin/**`、`/api/aihr/incentive-admin/rules` | 问题、答案、最佳答案和奖励全程按租户/身份授权;重复选优不重复入账。奖励只有积分与学习学分,不代表现金或提现 |
| 全网 AI | 员工 `GET /api/aihr/web-ai/capabilities`、`POST /api/aihr/web-ai/query`;提供方管理 `/api/aihr/web-search/providers/**` | 与企业知识问答分入口和来源。仅接受公网 HTTPS 提供方,地址或密钥变更后必须重新连接测试;未配置/未启用时返回明确不可用,不生成假答案 |
## 后端落点
- 业务模块:`backend/ruoyi-modules/ruoyi-aihr`
- 注册模块:`backend/ruoyi-modules/pom.xml`
- 接入启动包:`backend/ruoyi-admin/pom.xml`
- 包名建议:`org.dromara.aihr`
- Controller 返回统一用 `org.dromara.common.core.domain.R`
当前管理端 AI 面试和案例沉淀已改为真实模型/ASR 优先;移动端首页和部分演示态数据仍保留 fallback。知识库、模型能力、文档解析、RAG、chat 按 [ruoyi-ai 能力分片迁移计划](RUOYI_AI_INCREMENTAL_MIGRATION.md) 逐片引入;知识库 DDL 与住宅类 SOP seed 在 `backend/script/sql/aihr_knowledge_mysql8.sql`,模型 DDL 在 `backend/script/sql/aihr_model_mysql8.sql`,训练记录 DDL 在 `backend/script/sql/aihr_practice_mysql8.sql`,AI 面试结果 DDL 在 `backend/script/sql/aihr_interview_result_mysql8.sql`,候选人资料 DDL 在 `backend/script/sql/aihr_candidate_material_mysql8.sql`,组织人员快照表在 `backend/script/sql/aihr_org_snapshot_mysql8.sql`,本地 reset 带 2 个住宅项目 22 人 seed;配置开放组织同步系统后用 `POST /api/aihr/org/sync` 覆盖为外部快照。
直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer <access_token>`;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode, tenantId }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端首页接口 `GET /api/aihr/mobile/home/{role}` 仍保留 `@SaIgnore` 以保证未登录首屏可用,但匿名分支只返回不读取业务统计的公开 seed;登录后的 `mobile-uni` 会自动携带移动端 token,服务端才返回真实员工/主管统计。
阿里云短信复用 RuoYi 的 `sms.blends.config1`。本地开发把真实短信参数放根目录 `.env.local` 或外部环境变量,`scripts/dev-backend.sh` 会自动加载;`application-dev.yml` / `application-prod.yml` 只保留占位,不写真实密钥。
```bash
AIHR_SMS_LOGIN_TEMPLATE_ID=SMS_xxxxxx
ALIYUN_SMS_ACCESS_KEY_ID=xxx
ALIYUN_SMS_ACCESS_KEY_SECRET=xxx
ALIYUN_SMS_SIGN_NAME=物业AI助手
```
候选人资料上传最小后端边界:
| 能力 | 后端接口 | 处理 |
|---|---|---|
| 候选资料上传 | `POST /api/aihr/mobile/candidate/materials` | `multipart/form-data` 字段 `file`,参数 `candidateId/candidateName/materialType`;需移动端登录返回的 `Authorization` 与 `clientid`;APP 用户场景后端强制使用登录手机号和派生候选人名,不信任前端传入身份;服务端与移动端共同限制 PDF/Word/JPG/PNG/WebP,文件名会去掉路径和控制字符;文件用 `ISysOssService` 写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,状态默认 `待审核`,单文件 20MB |
| 候选资料列表 | `GET /api/aihr/mobile/candidate/materials?candidateId=` | 返回前 20 条 `{id, ossId, fileName, materialType, fileSize, status, time}`,供 `/h5/candidate` 的“已提交资料”展示;APP 用户场景忽略传入 `candidateId`,只返回登录手机号对应资料 |
| HR 资料审核 | `GET /api/aihr/hr/candidate/materials?status=`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 管理端 `/recruit/interview` 展示前 50 条,可按状态筛选;审核请求体 `{status}`,只接受 `待审核/已通过/已驳回`;接口只允许管理端超级管理员或 `hr_operator` 角色访问 |
SOP 文档上传第三片已经落最小后端边界:
| 能力 | 后端接口 | 处理 |
|---|---|---|
| 文档上传解析 | `POST /api/knowledge/doc/upload` | `multipart/form-data`,字段 `file` 和 `category`;支持 `.txt/.md/.markdown/.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx` 及图片 `.jpg/.jpeg/.png/.gif/.webp/.bmp`、100MB 内;先写 `sys_oss`,再绑定 `aihr_knowledge_attach.oss_id` 并切分写入 `aihr_knowledge_fragment`;管理端该接口 timeout 为 180s,避免 PDF 同步解析/归类/向量化接近默认 50s 时被客户端断开 |
| 图片视觉 OCR | 同一上传/导入链路 | 上传图片时,若启用了 `aihr_model_config.category='vision'`(无则回退 `category='chat'`)的模型,会把图片编码为 base64 data URL 调用该供应商 OpenAI-compatible `/chat/completions` 提取文字,识别结果按普通正文切分写入 fragment;无可用视觉模型或 OCR 无结果时按「待处理」落库(attach status=0、0 片段),不算失败,启用视觉模型后重新上传同名文件即可解析。不依赖 Tesseract,识别质量由所配置视觉模型决定 |
| 智能归类与标签 | 同一上传/导入链路 | `category=__auto__` 时,解析正文后优先调用已启用的 `aihr_model_config.category='chat'` 模型生成分类、摘要、标签和归类理由,温度固定为 `0`;无模型或调用失败时按文件名/正文关键词兜底。最终分类写入 `aihr_knowledge_info/attach`,摘要和标签写入 `sys_oss.ext1` |
| 重复资料处理 | 同一上传/导入链路 | 上传时计算原始文件 `aihrFileSha256`、解析文本 `aihrTextSha256` 和 `md5` 写入 `sys_oss.ext1`;重复判断优先按文件 SHA-256,其次按文本 SHA-256,最后用同名同大小兼容旧数据。命中重复时复用原附件并迁移到最新分类,删除其他重复附件和旧 fragment |
| 归类稳定性 | 同一上传/导入链路 | 命中重复资料时优先复用已有 `sys_oss.ext1` 里的分类、摘要和标签;只有旧资料没有存过模型结果时才重新分析,避免同文件因重复上传或更换模型导致标签漂移 |
| 文档替换 | 同一 `category + fileName` 再上传 | 复用原 `doc_id`,删除旧 fragment 后重写新 fragment,避免重复文档堆积 |
| 片段向量化 | 同一上传接口内机会性执行 | 若 `aihr_model_config.category='vector'` 且启用,会优先调用配置供应商的 OpenAI-compatible `/embeddings`,把返回向量写入 `aihr_knowledge_fragment.embedding_json`;配置缺失或外部接口失败时使用本地确定性 `local-hash-v1` 兜底,保证 MVP 链路仍有真实向量数据 |
| 缺失向量补跑 | `POST /api/knowledge/doc/vectorize-missing` | 扫描缺少 `embedding_json` 的片段,复用同一向量化与 Qdrant 入库链路,适合模型配置变更后或历史资料补齐向量 |
| 向量库状态与重建 | `GET /api/knowledge/doc/vector-index-status`、`POST /api/knowledge/doc/rebuild-vector-index` | 模型配置页展示当前 vector 模型维度、Qdrant collection 维度、点数和片段向量数;维度不一致时可一键清空旧 embedding、删除 collection 并按当前模型重建 |
| Qdrant 向量索引 | 同一上传接口内机会性执行 | embedding 写入 MySQL 后尽力 upsert 到 Qdrant;同名文档替换会尽力删除旧 points;Qdrant 不可用不影响上传和 MySQL 检索;外部 embedding 成功但 collection 维度不一致时不静默降级为 `local-hash-v1`,通过状态接口和重建入口处理 |
| 混合检索 | `POST /api/knowledge/search` | 先跑中文关键词 `LIKE` 打分和 MySQL Fulltext,再生成 query embedding 走 Qdrant,RRF 融合后交给 `category='rerank'` 模型(如硅基流动 bge-reranker-v2-m3)按语义相关性重排;rerank 未配置或失败保持 RRF 顺序,Qdrant 或外部向量接口不可用时保留关键词/全文检索。命中后若 chat 模型可用,自动生成结构化轻概要写入响应 `answer/keyPoints/cautions`(≤60字直答+3-6要点+0-3注意;temperature=0,15s 超时,硬约束不得编造、答不了明示无依据);概要失败回退片段原文拼接,检索不报错 |
| 检索总结卡 | `POST /api/knowledge/summary-card`(JSON `{queryText, category}`) | 按需生成信息图式总结卡 `{title, steps[], objections[], scripts[], reminders[]}`:复用检索命中片段(最多5条)调 chat 模型,temperature=0、30s 超时;无命中/无 chat 模型/生成失败均抛明确错误供前端重试,不兜假数据。移动端查SOP结果页「生成总结卡」按钮触发,底部弹层展示并可 TTS 朗读 |
| 解析状态聚合 | `GET /api/knowledge/processing/overview` | 聚合 `aihr_knowledge_attach.status`、fragment 数、embedding 数、`sys_oss.ext1.fileSize`,生成资料处理页指标、分类、任务、链路和事件列表 |
| 服务端目录导入(运维) | `POST /api/knowledge/doc/import-local` | JSON `{ directory, category, limit }`;`directory` 只能是 `AIHR_IMPORT_ROOT` / `aihr.import.root` 下的相对目录,默认根目录为 `./.data/import`;逐文件复用上传解析链路,同步执行,仅保留给运维/调试,不在资料处理页暴露 |
| 服务端导入任务(运维) | `POST /api/knowledge/doc/import-local-task`、`GET /api/knowledge/doc/import-tasks`、`POST /api/knowledge/doc/import-tasks/{id}/cancel` | 启动后台目录导入并返回任务;任务写入 `aihr_knowledge_import_task`,调用方可查询总数、成功数、失败数、当前文件和进度,运行中任务可取消;仅保留给运维/调试,不在资料处理页暴露 |
| 批量异步上传 | `POST /api/knowledge/doc/upload-async`(multipart `file`+`category`+`batchId`)、`GET /api/knowledge/doc/upload-items?batchId=`、`POST /api/knowledge/doc/upload-items/{id}/retry` | 上传只做暂存(`aihr.upload.staging`,默认 `./.data/staging`)+ 写入 `aihr_knowledge_upload_item`(0待处理/1处理中/2完成/3失败)即返回;支持 ZIP ≤500MB,worker 限制最多 1000 个子文件、解压总量 ≤2GB,拒绝嵌套 ZIP/不安全路径并忽略未知格式;ZIP 文件名优先按 UTF-8 解码,旧版中文 Windows ZIP 自动回退 GBK,仍无法解压时返回安全提示;同名子文件自动追加编号并以单条批量写入入队,避免目录扁平化覆盖和半批次入队;完成/失败状态 CAS 写入,卡住 30 分钟(视频 120 分钟)由清扫重置,失败暂存文件保留 72h 供重试;同步接口 `POST /api/knowledge/doc/upload` 不支持 ZIP,保留给 SOP 页单文件即时预览 |
| 视频解析 | 走批量异步上传,支持 `.mp4/.mov/.avi/.mkv/.webm/.m4v`,单文件 ≤500MB、时长 ≤60 分钟 | 依赖服务器 ffmpeg/ffprobe;抽音轨按 5 分钟分段调 asr 模型转写(`[mm:ss]` 时间戳),按 30s 间隔(≤40 帧)抽关键帧调 vision 模型提取画面文字(相邻重复画面去重);两类文本合并后走归类/切片/向量化;无 asr 且无 vision 结果时按「待处理」落库不报错;视频同时只加工 1 个 |
模型能力第二片已经落最小后端边界:
| 能力 | 后端接口 | 处理 |
|---|---|---|
| 模型供应商 | `GET /api/aihr/model/providers`、`POST /api/aihr/model/providers`、`PUT /api/aihr/model/providers/{providerCode}`、`PATCH /api/aihr/model/providers/{providerCode}/status` | 优先返回 `aihr_model_provider`,支持新增、编辑、启停;缺表或空表时返回 seed 清单 |
| 模型配置 | `GET /api/aihr/model/configs`、`POST /api/aihr/model/configs`、`PUT /api/aihr/model/configs/{id}`、`PATCH /api/aihr/model/configs/{id}/enabled` | 优先返回 `aihr_model_config`,支持新增、编辑、启停,并计算是否已具备 URL/Key;`category` 支持 `chat/vector/rerank/asr/tts/vision` |
| 模型探针 | `POST /api/aihr/model/chat` | 数据库配置后走 OpenAI-compatible `/chat/completions`,否则 seed fallback;请求携带的模型名在库中不存在时(如前端 seed 占位行 `gpt-4o-mini`),自动回退到默认已启用的 chat 配置 |
本阶段不修改 `.env`,也不自动执行模型 SQL。API Key 通过模型配置页面写入数据库:`api_key` 可放在 `aihr_model_provider` 作为供应商默认值,也可放在 `aihr_model_config` 覆盖单个模型;接口响应只返回 `configured/apiKeyConfigured`,不返回密钥明文。
Qdrant 本地默认值可不配;需要覆盖时用 JVM property 或环境变量:
| 配置 | 默认值 | 用途 |
|---|---|---|
| `AIHR_QDRANT_URL` / `-Daihr.qdrant.url` | `http://127.0.0.1:6333` | Qdrant REST 地址 |
| `AIHR_QDRANT_COLLECTION` / `-Daihr.qdrant.collection` | `aihr_knowledge` | 知识库向量 collection |
| `AIHR_QDRANT_API_KEY` / `-Daihr.qdrant.apiKey` | 空 | 远端 Qdrant API Key,本地不用 |
| `AIHR_IMPORT_ROOT` / `-Daihr.import.root` | `./.data/import` | 服务端资料目录导入根目录 |
组织人员同步配置:
| 配置 | 默认值 | 用途 |
|---|---|---|
| `AIHR_ORG_SYNC_BASE_URL` / `-Daihr.org-sync.base-url` | 空 | 外部开放平台 `/api/open/v1` 前缀,例如 `https://wuye.meihe.cc/api/open/v1` |
| `AIHR_ORG_SYNC_ACCESS_TOKEN` / `-Daihr.org-sync.access-token` | 空 | 已有 Bearer token;为空时尝试用 client credentials 换 token |
| `AIHR_ORG_SYNC_CLIENT_ID` / `-Daihr.org-sync.client-id` | 空 | 开放平台应用 `client_id`,也用于签名头 `X-Client-Id` |
| `AIHR_ORG_SYNC_CLIENT_SECRET` / `-Daihr.org-sync.client-secret` | 空 | `POST /auth/token` 换取访问令牌 |
| `AIHR_ORG_SYNC_SIGNING_SECRET` / `-Daihr.org-sync.signing-secret` | 空 | 可选 HMAC-SHA256 签名密钥;为空时使用 `AIHR_ORG_SYNC_CLIENT_SECRET`,业务请求带 `X-Client-Secret/X-Timestamp/X-Nonce/X-Signature`,签名 path 必须包含 `/api/open/v1` |
本地启动脚本会先加载根目录 `.env.local`,再加载 `backend/.env`。如果 `backend/.env` 沿用开放平台字段名 `client_id` / `client_secret`,脚本会映射为 `AIHR_ORG_SYNC_CLIENT_ID` / `AIHR_ORG_SYNC_CLIENT_SECRET`。
```bash
TOKEN=<登录后 access_token>
API_BASE=${API_BASE:-https://wygj-api.localhost}
curl -fsS "$API_BASE/api/aihr/org/sync" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"dryRun":true}'
curl -fsS "$API_BASE/api/aihr/org/sync" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"dryRun":false,"replaceExisting":true,"allowPartialReplace":false}'
# 仅在 dry-run 已人工确认允许不完整快照覆盖时使用
curl -fsS "$API_BASE/api/aihr/org/sync" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"dryRun":false,"replaceExisting":true,"allowPartialReplace":true}'
```
## 前端落点
- 管理端前端:`frontend/`,只承载后台管理、配置、审核、查看结果等页面。
- AI 面试 API 文件:`frontend/src/api/aihr/interview.ts`
- AI 面试页面:`frontend/src/views/recruit/interview.vue`
- 三角色对练 API 文件:`frontend/src/api/aihr/practice.ts`
- 三角色对练页面:`frontend/src/views/train/practice.vue`
- 案例沉淀 API 文件:`frontend/src/api/aihr/case.ts`
- 案例沉淀页面:`frontend/src/views/knowledge/cases.vue`
- SOP 知识库 API 文件:`frontend/src/api/aihr/sop.ts`
- SOP 知识库页面:`frontend/src/views/knowledge/sop.vue`
- 资料处理 API 文件:`frontend/src/api/aihr/processing.ts`
- 资料处理页面:`frontend/src/views/knowledge/processing.vue`
- 模型配置 API 文件:`frontend/src/api/aihr/model.ts`
- 模型配置页面:`frontend/src/views/system/model/index.vue`
- 用户侧工程:`mobile-uni/`,路由为 `/h5/#/pages/user/today/index`、`/h5/#/pages/user/practice/index`、`/h5/#/pages/user/sop/index`、`/h5/#/pages/user/profile/index`、`/h5/#/pages/candidate/index/index`、`/h5/#/pages/supervisor/index/index`。未登录先走手机号短信登录,登录后 API 优先请求真实业务接口,不复用后台页面路由。语音输入不要新增移动端专用接口,浏览器录音和文件选择兜底都复用 `/api/ai/asr`。
- 员工端训练:底部“练”进入 `/h5/#/pages/user/practice/index`,展示今日任务、每日三题、专项训练、训练前预习卡和训练对话;每日三题支持文字或录音转写,作答接口 `/api/aihr/mobile/practice/assignments/{id}/answer` 可携带 `answer/audioUrl/audioOssId`,服务端只持久化可安全回放的内联 data URL,OSS 音频通过 `audioOssId` 受保护下载;完成后记录写入 `aihr_practice_session`,员工端可提交 1-5 分训练体验和可选意见,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。能力画像按 BRD 五维返回任务完成度、话术规范性、情绪管理能力、响应时效、增值转化潜力;暂无工单/时效数据的维度返回 `null`,前端显示“待采集”,不把训练次数当正式绩效指标。
- 候选人端:`/h5/#/pages/candidate/index/index` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。
- 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。
## 验收
```bash
./scripts/demo-check.sh
npm --prefix mobile-uni run build:h5
API_BASE=${API_BASE:-https://wygj-api.localhost}
curl -fsS "$API_BASE/api/aihr/mobile/home/user"
npm --prefix frontend run lint:eslint -- src/api/aihr/interview.ts src/views/recruit/interview.vue src/api/aihr/practice.ts src/views/train/practice.vue src/api/aihr/case.ts src/views/knowledge/cases.vue src/api/aihr/sop.ts src/views/knowledge/sop.vue
mvn -f backend/pom.xml -pl ruoyi-admin -am -DskipTests package
```
模型能力可单独 smoke:
```bash
TOKEN=<登录后 access_token>
API_BASE=${API_BASE:-https://wygj-api.localhost}
curl -fsS "$API_BASE/api/aihr/model/providers" -H "Authorization: Bearer $TOKEN"
curl -fsS "$API_BASE/api/aihr/model/configs" -H "Authorization: Bearer $TOKEN"
curl -fsS -X POST "$API_BASE/api/aihr/model/chat" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"prompt":"物业管家处理漏水投诉第一步是什么?"}'
```
Qdrant 和向量索引可单独 smoke;重建接口会清空旧 embedding 并重建 collection,只用于本地验证或确认维度不一致后的修复:
```bash
TOKEN=<登录后 access_token>
API_BASE=${API_BASE:-https://wygj-api.localhost}
curl -fsS http://127.0.0.1:6333/
curl -fsS "$API_BASE/api/knowledge/doc/vector-index-status" -H "Authorization: Bearer $TOKEN"
curl -fsS -X POST "$API_BASE/api/knowledge/doc/rebuild-vector-index" -H "Authorization: Bearer $TOKEN"
```
浏览器验收仍按 [DEMO_ACCEPTANCE.md](DEMO_ACCEPTANCE.md) 的四条关键路径执行。
## 多租户知识空间统一问答
内部员工端、主管端和管理端统一调用:
```http
POST /api/knowledge/query
Authorization: Bearer <session-token>
Content-Type: application/json
{
"queryText": "漏水投诉第一步怎么处理?",
"spaceCodes": ["yc_property_sop"],
"limit": 5
}
```
内部有效空间由服务端按当前租户、SESSION 应用绑定和用户/角色授权求交集;请求体中的 `tenantId/userId/extPartyId` 不参与身份判断。员工本人和主管团队概况只允许使用固定数据工具:
```json
{"queryText":"我的训练概况","toolCode":"MY_PRACTICE_SUMMARY"}
{"queryText":"团队训练概况","toolCode":"TEAM_PRACTICE_SUMMARY"}
```
外部应用使用独立入口,不能调用数据工具:
```http
POST /api/open/knowledge/query
Authorization: Bearer <api-token>
Content-Type: application/json
{"queryText":"你们提供哪些服务?","spaceCodes":["mt_customer_service"],"limit":5}
```
成功响应包含 `requestId`、`answer`、`citations`、`usedSpaceCodes`、`noEvidence` 和兼容 `legacy` 数据。引用携带 `spaceCode/sourceType/docId/title/snippet/fragmentId`;客户端不得把无引用回答包装成有知识依据的正式答案。
空间、授权和应用管理接口统一位于 `/api/knowledge/admin`:`spaces`、`spaces/{id}/grants`、`spaces/{id}/documents`、`apps`、`apps/{id}/spaces`、`apps/{id}/rotate-token`。`DELETE /spaces/{id}/documents/{attachId}` 只解绑当前空间成员;其他空间仍引用同一 OSS 时原文件不会删除。仅知识平台管理角色可调用,API_TOKEN 创建或轮换后只返回一次明文。完整初始化、令牌保管、legacy 迁移与回滚步骤见 [KNOWLEDGE_PLATFORM_RUNBOOK.md](KNOWLEDGE_PLATFORM_RUNBOOK.md)。