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

442 lines
38 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.
# 帮道 MVP 本地启动
## 当前仓库
- 后端:`backend`,来自 `dromara/RuoYi-Vue-Plus` 的 `5.X` 分支
- 管理端前端:`frontend`,来自 `CrazyLionCat/plus-ui` 的 `5.X` 分支
- 用户侧前端:`mobile-uni`,uni-app Vue3 H5 + App-Plus 工程;旧 `mobile` 仅保留为 MVP 演示兜底
## 本地入口与依赖端口
- 管理端前端:`https://wygj-admin.localhost/`
- uni-app 用户侧:`https://wygj-mobile-uni.localhost/h5/`
- 后端:`https://wygj-api.localhost/`
- 固定端口仅用于显式 fallback:`PORTLESS=0 ./scripts/dev.sh` 时后端回退 `http://127.0.0.1:8080/`,旧移动端回退 `http://127.0.0.1:5174/h5/user`
- MySQL:`127.0.0.1:13306`,数据库 `ry-vue`,账号 `root/root`
- Redis:`127.0.0.1:16379`,密码 `ruoyi123`
- MinIO API:`http://127.0.0.1:9000`,Console:`http://127.0.0.1:9001`,账号 `ruoyi / ruoyi123`,默认 bucket `ruoyi`
- Qdrant REST:`http://127.0.0.1:6333`,默认 collection `aihr_knowledge`
- 服务端资料导入根目录:`./.data/import`
## portless 本地入口
本机开发测试优先用 portless 访问稳定域名,避免继续记固定端口:
- uni-app 用户侧:`https://wygj-mobile-uni.localhost/h5/`
- 管理端:`https://wygj-admin.localhost/`
- 后端 API:`https://wygj-api.localhost`
Linux/macOS 或已完整配置的 Linux shell 可从根目录一次性启动本地依赖、后端、管理端和 uni-app 用户侧:
```bash
./scripts/dev.sh
```
`./scripts/dev.sh` 默认通过 portless 启动后端、管理端和 `mobile-uni`,并由 Docker 启动 MySQL/Redis/MinIO/Qdrant。管理端 Vite 代理读取 `frontend/.env.development` 的 `VITE_APP_PROXY_TARGET=https://wygj-api.localhost`,`mobile-uni` Vite 代理默认指向 `https://wygj-api.localhost`,都不依赖固定 `8080`。如需单独启动后端,可执行:
```bash
portless run --name wygj-api ./scripts/dev-backend.sh
```
Windows 10/11 + Docker Desktop 不要从 PowerShell 直接执行 `./scripts/dev.sh`:Windows 的 `bash` 可能解析到 WSL,而 WSL 的 Node、Docker 集成和检出文件换行符未必满足脚本要求。使用本文件的 [Windows 10/11 + Docker Desktop](#windows-docker-desktop) 流程。
本地启动默认关闭管理端图形验证码;需要专门验证验证码链路时设置 `AIHR_DEV_CAPTCHA_ENABLED=true`。生产启动不使用该开发开关,验证码保持开启。
重复启动或切换分支时优先使用 `./scripts/dev.sh restart`,停止使用 `./scripts/dev.sh stop`。脚本会递归终止 portless 包装进程及其 Java/Vite 子进程,避免旧后端脱离后继续占用 MySQL 连接池;进程树回归可用 `bash scripts/tests/process-tree.test.sh` 验证。
旧 `mobile/` 只在显式 `PORTLESS=0 ./scripts/dev.sh` 时作为固定端口兜底启动;后端开发测试优先看 `wygj-api.localhost`,管理端开发测试优先看 `wygj-admin.localhost`,新用户侧开发测试优先看 `wygj-mobile-uni.localhost`。
原生 Android/iOS 资源使用 `npm --prefix mobile-uni run build:app` 构建;它不生成签名 APK/IPA,也不适用本文件的 H5 `rsync` 发布流程。自定义基座、协议链接、签名和真机门禁见 [MOBILE_APP_PACKAGING.md](MOBILE_APP_PACKAGING.md)。
Qdrant 默认本地无需配置;远端或自定义 collection 可用 `AIHR_QDRANT_URL`、`AIHR_QDRANT_COLLECTION`、`AIHR_QDRANT_API_KEY` 覆盖。服务端资料导入根目录可用 `AIHR_IMPORT_ROOT` 或 `-Daihr.import.root` 覆盖。组织人员同步可用 `AIHR_ORG_SYNC_BASE_URL` 指向外部开放平台 `/api/open/v1` 前缀,并配置 `AIHR_ORG_SYNC_ACCESS_TOKEN` 或 `AIHR_ORG_SYNC_CLIENT_ID`/`AIHR_ORG_SYNC_CLIENT_SECRET`;业务请求会用 client secret 生成 HMAC-SHA256 hex 签名。移动端手机号登录的短信模板 ID、阿里云 AccessKey、Secret 和短信签名都通过环境变量注入;本地放根目录 `.env.local`,`scripts/dev-backend.sh` 会自动加载。若开放平台凭证放在 `backend/.env`,启动脚本也会加载该文件,并把 `client_id`/`client_secret` 映射为组织同步实际读取的 `AIHR_ORG_SYNC_CLIENT_ID`/`AIHR_ORG_SYNC_CLIENT_SECRET`。
组织同步写入前先运行只读预检:`node scripts/verify-demo-questions.mjs --org-dry-run`。dry-run 不检查或变更本地快照表结构,也不写数据库;输出只包含人数、手机号覆盖、脱敏数、疑似乱码数和警告,不输出员工姓名。2026-07-25 只读预检返回 3424 名员工、3398 个脱敏手机号和 1 条重复项目成员关系;当前上游优先字段 `employee_number` 仅匹配生产既有主体 `7/3417`,稳定 `employee_id` 匹配 `3417/3417`。全量快照仍存在脱敏手机号和重复关系,不得执行非 dry-run 请求,也不得用 `allowPartialReplace=true` 绕过。
员工岗位等字段的日常变化不要执行不安全的全量覆盖;部署支持增量同步的后端后,使用受 `superadmin/hr_operator` 保护的 `POST /api/aihr/org/sync-changes`。首次传 `{"dryRun":true,"sinceTime":"YYYY-MM-DD HH:mm:ss"}` 预检,再以相同 `sinceTime` 和 `dryRun:false` 写入;接口主动拉 `/sync/changes` 并按稳定 `employee.id` 回源详情,只替换命中的员工,脱敏手机号保留本地有效值,任职快照拉取失败则拒绝写库。
正式试点预检必须指定当前批次租户和时间窗,例如:`AIHR_PILOT_TENANT_ID=000000 AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 AIHR_PILOT_STRICT=true ./scripts/demo-check.sh`。脚本只接受安全租户编号和 `YYYY-MM-DD` 日期,只统计目标租户窗口内完成的训练、校准和 SOP 评审;人员先按唯一手机号映射到在职组织快照,完训口径为每人至少 10 次已完成对练。
服务端正式试点 CSV 也必须显式指定同一批次日期;当前管理端不展示个人训练或试点导出入口。可用受角色保护的真实 API 验证其日期拒绝、CSV 契约和正式身份范围:`node scripts/verify-demo-questions.mjs --pilot-export --start-date=2026-07-07 --end-date=2026-07-10`。
本地 `.env.local` 示例:
```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助手
# 演示兜底:非空则不真发短信,验证码固定为该值;dev 默认 123456
AIHR_SMS_DEV_FIXED_CODE=123456
# prod 默认 false;试点期仅在明确接受固定码风险时与上一项同时开启
AIHR_SMS_PROD_FIXED_CODE_ENABLED=false
AIHR_ORG_SYNC_BASE_URL=https://wuye.meihe.cc/api/open/v1
AIHR_ORG_SYNC_ACCESS_TOKEN=
AIHR_ORG_SYNC_CLIENT_ID=
AIHR_ORG_SYNC_CLIENT_SECRET=
AIHR_ORG_SYNC_SIGNING_SECRET=
# 若放在 backend/.env,也可沿用开放平台字段名:
# client_id=dn_xxxxxx
# client_secret=dns_xxxxxx
# 试点成本闸门:超预算时改 false,chat/asr/tts/vision 会停止外发并走现有兜底
AIHR_AI_RUNTIME_ENABLED=true
AIHR_AI_CHAT_ENABLED=true
AIHR_AI_SPEECH_ENABLED=true
```
全网检索访问密钥使用数据库表 `aihr_web_ai_secret` 中自动生成的主密钥加密,不依赖环境变量,也不通过通用参数接口回显。数据库迁移、备份和恢复必须同时保留该表;丢失或修改主密钥后需要重新保存提供方密钥。全网检索提供方只接受公网 HTTPS 地址,且每次修改地址或密钥后都必须重新连接测试,测试成功后才能启用。
`application-dev.yml` 只保留占位和默认值,不提交真实短信密钥。dev 环境不配阿里云短信也能登录移动端:验证码固定 `123456`。
<a id="windows-docker-desktop"></a>
## Windows 10/11 + Docker Desktop
Windows 推荐让 Docker Desktop 承载 MySQL、Redis、MinIO 和 Qdrant,Java/Vite/portless 仍作为 Windows 进程运行;无需在 Windows 单独安装数据库或 Redis 服务。基础环境要求:
- Docker Desktop 已启动,`docker info` 可连接 daemon
- Git for Windows(使用其 Git Bash 和 OpenSSL)
- JDK 17、Maven
- Node.js 24+、npm、全局 `portless`
- 可选:测试视频解析时另装 `ffmpeg` 和 `ffprobe`
### 1. PowerShell 会话准备
在仓库根目录打开 PowerShell。下面通过 `git.exe` 自动定位 Git for Windows,不依赖仓库或 Git 的固定安装盘符:
```powershell
$gitRoot = Split-Path (Split-Path (Get-Command git).Source -Parent) -Parent
$gitBash = Join-Path $gitRoot 'bin\bash.exe'
$env:Path = "$(Join-Path $gitRoot 'usr\bin');$(Join-Path $gitRoot 'bin');$env:Path"
docker info
docker compose -f docker-compose.dev.yml config --quiet
java -version
mvn -version
node --version
portless --version
portless doctor
```
若 `portless doctor` 仅提示代理未启动,属于首启正常状态;若提示找不到 OpenSSL,确认本会话已执行上面的 `$env:Path` 设置。首次使用 HTTPS 本地域名时执行一次:
```powershell
portless trust
```
公司代理、VPN 或 TUN 软件可能把 `*.localhost` 在 Node.js 中解析到 `198.18.0.0/15`,导致 Vite 页面能打开、`/dev-api` 却返回 `500`。下面的 Windows 流程先让 portless 为后端分配内部端口,再让两个 Vite 代理从 `portless list` 读取该端口并直连 `127.0.0.1`,因此不依赖 Node.js 对 `*.localhost` 的解析,也不要求 `8080` 空闲;浏览器入口仍使用 portless HTTPS 域名。若浏览器本身也无法解析这些域名,可在路由启动后,用**管理员 PowerShell** 执行一次 `portless hosts sync`。
### 2. 首次初始化
首次检出时安装两个当前前端工程的锁定依赖:
```powershell
npm --prefix frontend ci
npm --prefix mobile-uni ci
```
首次创建本地测试库时,通过 Git Bash 执行初始化脚本:
```powershell
& $gitBash ./scripts/reset-dev-db.sh
```
该脚本会在 Docker Desktop 中启动 `wygj-mysql`、`wygj-redis`、`wygj-minio`、`wygj-qdrant`,并**删除后重建**本地 `ry-vue`、导入若依与 AIHR schema/seed。仅首次初始化或明确需要清空本地测试数据时使用。
后续日常启动依赖容器不重建数据:
```powershell
docker compose -f docker-compose.dev.yml up -d mysql redis qdrant minio minio-init
```
基础开发不要求 `.env.local`:dev 短信验证码默认固定为 `123456`,模型调用失败仍保留 seed fallback。真实短信、外部组织同步或外部 AI 模型联调时,再按前文示例创建被 Git 忽略的 `.env.local`。
### 3. 启动后端
新开一个 PowerShell,重复“PowerShell 会话准备”中的 `$gitRoot`、`$gitBash` 和 `$env:Path` 三行,然后执行:
```powershell
portless run --name wygj-api --force bash ./scripts/dev-backend.sh
```
首次启动没有 `backend/ruoyi-admin/target/ruoyi-admin.jar` 时,脚本会先执行 Maven 打包;保持该终端运行。portless 分配的内部端口是动态的,对外入口始终是 `https://wygj-api.localhost/`。
### 4. 启动管理端与 mobile-uni
管理端和 `mobile-uni` 各使用一个 PowerShell,两个终端都先重复“PowerShell 会话准备”中的变量设置。
管理端终端在仓库根目录执行:
```powershell
$apiRoute = (portless list | Select-String 'https://wygj-api\.localhost').Line
$apiPort = [regex]::Match($apiRoute, 'localhost:(\d+)').Groups[1].Value
if (-not $apiPort) { throw 'wygj-api portless route not found' }
$env:VITE_APP_PROXY_TARGET = "http://127.0.0.1:$apiPort"
& $gitBash ./scripts/dev-frontend.sh
```
`mobile-uni` 终端在仓库根目录执行:
```powershell
$apiRoute = (portless list | Select-String 'https://wygj-api\.localhost').Line
$apiPort = [regex]::Match($apiRoute, 'localhost:(\d+)').Groups[1].Value
if (-not $apiPort) { throw 'wygj-api portless route not found' }
Set-Location .\mobile-uni
$env:VITE_PROXY_TARGET = "http://127.0.0.1:$apiPort"
portless run --name wygj-mobile-uni --force npm run dev
```
本机 portless `0.15.4` 在没有根 `package.json` workspace 时不会仅凭 `portless.json` 批量启动两个前端,因此不要用根目录裸命令 `portless` 代替上述两个入口。后端重新启动后内部端口可能变化,此时也要重新启动两个前端以刷新代理目标。保持三个应用终端运行,访问:
- 管理端:`https://wygj-admin.localhost/`
- 用户侧:`https://wygj-mobile-uni.localhost/h5/`
- 后端:`https://wygj-api.localhost/`
### 5. 冒烟与测试
```powershell
$gitBash = Join-Path $gitRoot 'bin\bash.exe'
docker compose -f docker-compose.dev.yml ps
portless list
curl.exe -k -fsS https://wygj-admin.localhost/dev-api/auth/tenant/list
curl.exe -k -fsS https://wygj-admin.localhost/dev-api/auth/code
curl.exe -k -fsS https://wygj-mobile-uni.localhost/dev-api/api/aihr/mobile/home/user
docker exec wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 -N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='ry-vue';"
docker exec wygj-redis redis-cli -a ruoyi123 ping
curl.exe -fsS http://127.0.0.1:9000/minio/health/live
curl.exe -k -fsS http://127.0.0.1:6333/
npm --prefix mobile-uni run test:unit
npm --prefix mobile-uni run typecheck
npm --prefix frontend run build:dev
mvn -f backend/pom.xml -pl ruoyi-modules/ruoyi-aihr -am -DskipTests=false test
& $gitBash ./scripts/demo-check.sh
```
### 6. 停止
先在后端、管理端和 `mobile-uni` 三个终端按 `Ctrl+C`。需要同时停止代理和依赖容器时执行:
```powershell
portless proxy stop
docker compose -f docker-compose.dev.yml stop
```
日常停止不需要 `docker compose down -v`;当前数据库等数据使用仓库 `.data/` bind mount,只有明确重置时才运行 `reset-dev-db.sh`,不要手工删除 `.data/mysql` 等目录。
## 默认登录
- 租户:`000000`
- 管理员:`admin / admin123`
- 测试账号:`test / 666666`、`test1 / 666666`
## 品牌资源
- 页面标题:`帮道`
- 侧栏 Logo:`frontend/src/assets/logo/logo.png`
## 已验证的基础链路
- MySQL、Redis 与 MinIO 容器健康检查通过
- Qdrant 容器随本地开发编排启动,供 SOP 知识库向量召回使用
- `ry_vue_5.X.sql`、`ry_job.sql`、`ry_workflow.sql` 已导入
- `aihr_knowledge_mysql8.sql`、`aihr_model_mysql8.sql` 已导入;本地库含住宅 SOP seed 片段与模型配置表
- `aihr_practice_mysql8.sql` 已导入;移动端员工训练记录落 `aihr_practice_session`,用于训练历史、主管待复盘列表和能力画像聚合
- `aihr_interview_result_mysql8.sql` 已纳入 reset 脚本;AI 面试评分完成后结果落 `aihr_interview_result`
- `aihr_candidate_material_mysql8.sql` 已纳入;候选人端补充资料文件写 `sys_oss`/MinIO,关系落 `aihr_candidate_material`
- `aihr_org_snapshot_mysql8.sql` 已纳入 reset 脚本;组织人员本地 seed(2 个住宅项目 22 人,项目经理/主管/一线三层)支撑演示,外部开放组织系统配置完成后用 `POST /api/aihr/org/sync` 拉取 `company/department/employee` 快照。2026-07-25 生产快照为 3417 名员工、2938 人在职、3392 个手机号映射;在职姓名已按稳定 `employee_id` 定向补齐,本次未做全量覆盖。
- 源码 `application-prod.yml` 默认关闭 `aihr.org-sync.store-display-fields`;生产当前通过外部配置显式开启,用于主管团队、派发和考试对象显示已授权姓名。关闭该开关会让接口回退为编号/“员工”,修改前必须确认隐私口径与业务验收影响。项目范围、岗位和外部主体 ID仍用于权限与身份映射。
- 本地组织 seed 带演示手机号;从 `aihr_org_snapshot` 按角色查询测试账号,分别验证员工端岗位识别和主管端项目范围,不在文档保存具体号码
- 生产移动端回归同样由执行者从 `aihr_org_snapshot` 只读选择账号,不等待人工提供手机号:限定目标岗位、`employment_status='active'`、11 位 `person_phone`,并确认手机号与 `ext_party_id` 双向唯一。账号值只注入当前浏览器进程,不输出到终端、截图、报告或仓库。试点固定码开启时,登录顺序仍为先请求 `/resource/sms/code`、再提交固定码;直接提交固定码会得到 `Captcha invalid`,不应误判为账号或功能故障。
- SOP 知识库支持 `.txt/.md/.markdown/.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx` 上传到 MinIO 后解析入库,接口为 `POST /api/knowledge/doc/upload`,单文件上限 100MB;管理端上传请求单独放宽到 180s,PDF 解析/归类/向量化较慢时不要改全局 axios 超时。
- 资料处理中心异步接口支持 `.zip` 和视频单文件 ≤500MB;生产 Spring/Undertow multipart 配置必须保持 `max-file-size=500MB`、`max-request-size=520MB`(源文件 `backend/ruoyi-admin/src/main/resources/application-prod.yml`,线上外置 `/opt/wygj/config/application-prod.yml`),修改后重启 `wygj-aihr.service`。非视频/ZIP 文件仍由队列限制为 100MB;ZIP 最多 1000 个子文件、解压总量 ≤2GB,不支持嵌套 ZIP。
- 视频(`.mp4/.mov/.avi/.mkv/.webm/.m4v`,≤500MB、≤60 分钟)只走资料处理中心批量导入(异步队列):ffmpeg 抽音轨分段调 asr 转写 + 抽关键帧调 vision 提取画面文字,合并后归类切片入库,片段带 `[mm:ss]` 时间戳;**依赖服务器安装 ffmpeg/ffprobe**(macOS `brew install ffmpeg`,Linux `apt install ffmpeg`)
- 服务端目录导入接口为 `POST /api/knowledge/doc/import-local-task`,只读取导入根目录下的相对目录,后台逐文件复用同一上传解析链路;`POST /api/knowledge/doc/import-tasks/{id}/cancel` 可取消运行中任务;`POST /api/knowledge/doc/import-local` 保留为同步调试接口。这组接口仅供运维/调试,不在资料处理页提供入口。
- 启用 `aihr_model_config.category='vector'` 的模型配置后,上传会同步写入片段 embedding,并尽力 upsert 到 Qdrant;模型配置页可查看 Qdrant 维度、点数和片段向量数,并在维度不一致时重建索引;未配置或 Qdrant 不可用时只走 MySQL Fulltext/seed fallback,不影响检索。
- 后端 `ruoyi-admin` 已完成 Maven 打包,并通过 portless 暴露为 `https://wygj-api.localhost/`
- AIHR 真跑测试必须显式关闭父 POM 的默认跳测:`mvn -f backend/pom.xml -pl ruoyi-modules/ruoyi-aihr -am -DskipTests=false test`;普通 `mvn ... test` 只适合编译/打包检查。
- 前端依赖已安装,Vite 已通过 portless 暴露为 `https://wygj-admin.localhost/`
- 当前用户侧由 `./scripts/dev.sh` 通过 portless 启动 `mobile-uni`,入口 `https://wygj-mobile-uni.localhost/h5/`;旧移动端兜底仅在 `PORTLESS=0` 调试时启动到 `5174`
- 移动端手机号登录页已接 `/resource/sms/code` 与 `/auth/mobile/sms-login`;未配置真实 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET`、`ALIYUN_SMS_SIGN_NAME`、`AIHR_SMS_LOGIN_TEMPLATE_ID` 时不会发送阿里云短信;手机号不存在时会自动注册为 `app_user`
- 移动端员工端“开始训练”已复用 `/api/train/practice/start`、`/turn`、`/finish`;移动端请求需带登录返回的 `Authorization` 与 `clientid`,服务端会强制当前 APP 身份和 `mode=mobile`,完成后才会改变主管端完训人数、“待复盘对练”计数、复盘列表、复盘详情、员工训练历史和能力画像。管理端训练看板的同一路径只允许 `superadmin/hr_operator` 做服务端绑定的 `mode=preview` 运营预览;预览不会冒用员工身份,也不计入上述统计、复盘或试点。
- 移动端候选人端“开始面试/面试练习”已复用 `/api/recruit/interview/start`、`/answer`、`/finish`;“补充资料”需带移动端 `Authorization` 与 `clientid`,上传后写 `sys_oss` 和 `aihr_candidate_material`,管理端 `/recruit/interview` 可审核为 `已通过/已驳回`
- `/dev-api/auth/tenant/list` 与 `/dev-api/auth/code` 已通过前端代理返回 `200`
- `000000 / admin / admin123` 已通过真实加密登录接口返回 `access_token`
Qdrant 单独检查:
```bash
curl -fsS http://127.0.0.1:6333/
```
登录后可检查向量索引状态:
```bash
TOKEN=<登录后 access_token>
curl -fsS https://wygj-api.localhost/api/knowledge/doc/vector-index-status -H "Authorization: Bearer $TOKEN"
```
## 线上管理端静态发布
线上根站由 Caddy 从服务器 `/opt/wygj/www` 提供;同一目录下的 `/h5` 是移动端静态资源。只改管理端静态资源时无需重启后端,但必须先备份整个目录,并且同步根目录时不能使用 `--delete`,以免删除移动端资源。
```bash
npm --prefix frontend run build:prod
ssh YCWY 'ts=$(date +%Y%m%d%H%M%S); mkdir -p /opt/wygj/backups; cp -a /opt/wygj/www /opt/wygj/backups/www-$ts; echo /opt/wygj/backups/www-$ts'
rsync -az frontend/dist/ YCWY:/opt/wygj/www/
curl -k -s https://peilian.njzhmj.top/ | sed -n '1,20p'
```
发布后以管理员真实会话回归受影响页面;如需核对指定资源,可比较本地构建文件和远端对应文件的 SHA-256。该流程不发布后端、不迁移数据库,也不覆盖 `/opt/wygj/www/h5`。
## 线上移动端静态发布
线上 `peilian.njzhmj.top` 由 Caddy 服务,`/h5*` 映射到服务器 `/opt/wygj/www/h5`,并 fallback 到 `/h5/index.html`。只改移动端静态资源时无需重启后端。
```bash
npm --prefix mobile-uni run build:h5
ssh YCWY 'ts=$(date +%Y%m%d%H%M%S); mkdir -p /opt/wygj/backups; cp -a /opt/wygj/www/h5 /opt/wygj/backups/h5-$ts; echo /opt/wygj/backups/h5-$ts'
rsync -az --delete mobile-uni/dist/build/h5/ YCWY:/opt/wygj/www/h5/
curl -k -s https://peilian.njzhmj.top/h5/ | sed -n '1,20p'
```
发布前先运行只读产物检查,记录 commit、管理端/H5/后端 hash;它不会连接 SSH、同步文件或重启服务:
```bash
./scripts/release-preflight.sh
RELEASE_REMOTE_URL=https://peilian.njzhmj.top ./scripts/release-preflight.sh
# 发布后核验线上主资源是否与当前本地产物一致
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true ./scripts/release-preflight.sh
# 定向后端发布后只核对后端与 schema,不要求本轮未发布的管理端/H5 与本地一致
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh
# 8 月 1 日最终业务门禁:正式内容签认、固定码模式、五渠道主处理人/替补和生产场景快照,只读且失败关闭
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh
# 验证冻结提交生成的指定 JAR;提交必须是当前 HEAD 的祖先,且产物时间不得早于该提交
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_ARTIFACT_COMMIT=<release-commit> RELEASE_LOCAL_BACKEND_PATH=<release-jar> ./scripts/release-preflight.sh
# 8 月 1 日最终发布后同时核对线上静态资源、后端包、必需表和业务就绪状态
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_VERIFY_AUG1_READINESS=true ./scripts/release-preflight.sh
```
四个远端核验开关可以按实际发布单元独立组合,但只要启用任一开关就必须提供 `RELEASE_REMOTE_URL`。定向模式只要求对应的本地产物保持新鲜:静态核验要求管理端/H5,后端核验要求后端 JAR,单独 schema 或 8 月 1 日业务就绪核验不要求无关构建产物。默认以当前 `HEAD` 判断产物时间;冻结 RC 应同时设置 `RELEASE_ARTIFACT_COMMIT` 和 `RELEASE_LOCAL_BACKEND_PATH`,前者必须是当前 `HEAD` 的祖先,后者必须指向实际准备发布的 JAR,避免后续文档提交误伤冻结物或误用 `target` 下的其他构建。`RELEASE_VERIFY_REMOTE_BACKEND=true` 会打印整包 jar SHA-256 和 `ruoyi-aihr` 模块内容 SHA-256,实际匹配以模块内容 hash 为准;它不再隐式要求管理端/H5 静态资源匹配。同时启用前三项只能证明完整包匹配;8 月 1 日最终 Go 还必须增加 `RELEASE_VERIFY_AUG1_READINESS=true`。该门禁先严格校验 `docs/content-candidates/aug1-release-approval.json`:5/5 场景、30/30 问题证据及五类负责人均签认后,才继续只读检查生产固定码模式、五个直通角色各至少两名有效处理人,以及五个已发布场景的版本、内容哈希、审核人和 SOP 引用。它不请求验证码、不发送短信、不登录、不输出账号或处理人身份,也不写数据库。远端后端默认核对 `/opt/wygj/app/ruoyi-admin.jar`,如发布路径不同可通过 `RELEASE_REMOTE_BACKEND_PATH` 覆盖;路径必须是安全的绝对路径。开启远端后端或 schema 核验时,预检还会只读检查 `wygj-aihr.service` 及其 `EnvironmentFile` 的有效配置,`AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP` 必须为 `false` 或未设置;无法读取引用的环境文件会失败关闭,而不会把本机环境变量当作线上证据。
Android 人工验收每完成一个步骤,用 `.\scripts\capture-android-acceptance.ps1 -Serial <adb-serial> -Stage <stage>` 保存当前状态;脚本只读取包版本、前台 Activity、权限、UI、截图和退出记录,不执行安装、启动、清数据、授权或点击。证据仅写入已忽略的 `output/aug1-rc/android-acceptance/`,详细口径见 `docs/MOBILE_APP_DEVICE_ACCEPTANCE.md`。
带 `RELEASE_REMOTE_URL` 时,预检同时校验租户接口 JSON 的业务 `code=200`;HTTP 200 但业务返回 401/405 会判定失败。
发布时必须保留 preflight 输出、远端备份目录和发布后浏览器回归结果;回滚优先使用对应备份目录恢复,再重启后端服务,不能直接覆盖当前线上目录而不留证据。
### 定向后端发布与回滚
8 月 1 日独立 APK 如只需补齐候选后端语义,使用固定拓扑的 `release-backend.sh`,不要手工 `scp` 后直接覆盖。脚本只允许 `YCWY:/opt/wygj/app/ruoyi-admin.jar` 和 `wygj-aihr.service`,默认 `plan` 与 `rollback-plan` 只读;`deploy`/`rollback` 必须使用只读计划输出的完整 SHA-256 授权串并保持 Git 工作区干净。计划阶段会核对冻结提交、JAR 哈希与 AIHR 模块、生产目标/服务/空间和 schema;部署阶段先保留旧 JAR 与清单,再在同目录校验并原子切换、重启、执行后端 + schema 预检,任何激活或发布后预检失败都会尝试恢复旧 JAR。回滚前还会额外备份当时正在运行的 JAR。脚本不删除备份,不发布管理端/H5,不执行数据库迁移。
先只读生成计划:
```bash
./scripts/release-backend.sh plan \
--artifact backend/ruoyi-admin/target/ruoyi-admin.jar \
--artifact-commit af8af2f6dd169e4616a4ca0ca724e8b809502ab3 \
--expected-sha256 93ad272118f6a28f88c2bfa305f5c450e163cbe22577ff5be3e85b3bd3958dad
```
只有负责人明确授权后,才能把计划输出的 `approval_token` 原样传给 `deploy`;仅运行 `plan` 不构成发布授权:
```bash
./scripts/release-backend.sh deploy \
--artifact backend/ruoyi-admin/target/ruoyi-admin.jar \
--artifact-commit af8af2f6dd169e4616a4ca0ca724e8b809502ab3 \
--expected-sha256 93ad272118f6a28f88c2bfa305f5c450e163cbe22577ff5be3e85b3bd3958dad \
--approval 'DEPLOY_BACKEND:<完整候选JAR-SHA256>'
```
发布成功会输出 `rollback_backup`、`rollback_sha256` 和回滚授权串。先只读核对备份,再在获得单独回滚授权后执行:
```bash
./scripts/release-backend.sh rollback-plan \
--backup-dir '<deploy输出的rollback_backup>' \
--expected-sha256 '<deploy输出的rollback_sha256>'
./scripts/release-backend.sh rollback \
--backup-dir '<deploy输出的rollback_backup>' \
--expected-sha256 '<deploy输出的rollback_sha256>' \
--approval 'ROLLBACK_BACKEND:<完整备份JAR-SHA256>'
```
发布前后均可重复运行下列无登录安全探针。它只对 APK 依赖的 20 个入口发送 GET:1 个公开首页必须返回业务 `200`,6 个鉴权 GET 必须返回 `401`,13 个仅允许 POST 的入口用 GET 必须返回 `405`。`/api/knowledge/answer-feedback` 同时存在员工 POST 和运营 GET,因此归入鉴权 GET。脚本明确排除短信验证码入口,不登录、不上传、不提交业务请求;通过只证明路由存在且无登录访问失败关闭,不能替代认证态业务验收:
```bash
./scripts/verify-aug1-production-api-readonly.sh
```
冻结运行时产物后,如发布/回滚或验收工具继续修正,不要重编同版本 APK/JAR 冒充新运行时。当前打包器默认使用 `af8af2f6` 的 `0.1.13 (113)` 统一 RC 证据,并把当前干净 `HEAD` 作为 operations commit;新包名同时包含两者短哈希,旧 ZIP 不覆盖:
```powershell
.\scripts\package-aug1-release.ps1 `
-BackendJar .\backend\ruoyi-admin\target\ruoyi-admin.jar
```
打包器重新校验冻结证据、APK/JAR/内容哈希和提交祖先关系,按固定顺序与时间戳生成 20 个文件的 ZIP,并附带发布、预检、无登录 API 探针、固定码认证复验、内容候选与正式签认校验、生产业务就绪门禁、机器签认清单、业务与五通道签认单、Android 取证及最新 Go/No-Go 的审计快照。ZIP 内 operations 脚本用于交付审计;实际执行仍须从对应 operations commit 的干净仓库运行。固定码认证脚本只有显式传入 `--execute` 才运行,沿用生产现有固定码配置,不启用或发送真实短信,也不提交业务记录;最终业务就绪脚本同样只读,签认不足时在连接生产前就失败关闭。
## MVP 页面验证
登录后侧栏应只展示以下入口:
- 首页:`/index`
- AI面试:`/recruit/interview`
- 三角色对练:`/train/practice`
- 案例沉淀:`/knowledge/cases`
- SOP知识库:`/knowledge/sop`
- 资料处理:`/knowledge/processing`
- 系统设置-模型配置:`/system/model`
若依默认菜单如“系统管理 / 租户管理 / 系统监控 / 系统工具 / 测试菜单”在当前 MVP 阶段应保持隐藏。
## MVP 演示流验证
当前已跑通管理端五个本地演示流和移动端员工训练闭环:
- AI面试:进入 `/recruit/interview`,点击“生成题目” → 输入真实回答或“填满参考回答” → “完成评分”,应看到新增面试记录;配置 chat 模型时题目和评分都来自真实模型,未配置时使用本地 Rubric。
- 三角色对练:进入 `/train/practice`,点击“开始对练” → 完成两轮真实或参考回复 → “结束并评分”,应看到“已完成闭环”“导师改写”和新增对练记录。
- 案例沉淀:进入 `/knowledge/cases`,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。
- SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。
- 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;页面只保留“批量导入”和“刷新”,不应出现“选择目录”“服务端导入”或目录导入任务面板。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;支持多文件和 ZIP,暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。
- uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。每日三题正式按组织快照 `hire_date` 判断入职三个月窗口;本地 Demo 若快照尚无该字段,仅由 `dev/local` profile 且 `application-dev.yml` 的 `aihr.practice.allow-legacy-daily-drill-fallback=true` 启用训练次数回退,生产 profile 即使误传环境变量也强制关闭。
- 工作助手确认式统一采集:`/h5/#/pages/user/sop/index` 一级入口只保留“工作助手 / 查全网”,既有文字、ASR、媒体、数据工具和 30 分钟/最近 6 轮短会话链保持不变;显式“记一下/帮我记/保存一下”返回 `DRAFT` 确认卡,位置缺失只作可选提示。确认后写 `aihr_assistant_capture`:`PRIVATE/NOT_REQUIRED` 仅本人可见,`COMPANY/PENDING` 只表示待流转;旧项目记录继续使用 `aihr_service_memory/version`。`/h5/#/pages/user/assistant/memories` 展示待确认候选和本人已确认记录;不依赖新的 Qdrant collection,提醒默认关闭。
- 上一条只验证当前已部署的确认式采集;项目名称下拉、项目化会话、原始来源绑定、今日工作成果和主管按日汇总仍属[后续迭代](工作助手与今日工作成果迭代计划-20260721.md),不得在本地回归结果中写成已完成。
- 移动端登录后若组织快照接口返回 `401/403`,只降级为手动岗位确认,不应清除手机号登录态;岗位确认页仍需允许员工选择“生活顾问”后继续进入业务页。
- uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。
- uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。
- 真 LLM 激活:在 `/system/model` 给供应商填 api_host/api_key 并启用 `category=chat` 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 `category=asr/tts` 语音路径生效。阿里云短音频 ASR 使用 `provider_code=dashscope`、`model_name=qwen3-asr-flash` 和北京工作空间的 `/compatible-mode/v1` Base URL;现有 `backend/.env` 中同一工作空间的 `AIHR_QWEN_REALTIME_ENDPOINT` / `AIHR_QWEN_REALTIME_API_KEY` 可作为本地模型管理的配置来源,但应用不会自动把私密变量导入模型表。密钥只通过模型管理或本地私密配置提供,不写入文档、SQL 或受版本控制配置。SiliconFlow SenseVoice/CosyVoice2 仍作为兼容供应商。未配置时全链路自动回退 seed。
演示前可先跑最小预检:
```bash
./scripts/demo-check.sh
bash scripts/tests/aihr-schema-migrations.test.sh
node --test mobile-uni/tests/personal-assistant.test.mjs
API_BASE=https://wygj-api.localhost ./scripts/personal-assistant-smoke.sh
# 登录后只读核验:TOKEN=<mobile-access-token> API_BASE=https://wygj-api.localhost ./scripts/personal-assistant-smoke.sh
```
工作助手记忆本地人工回归至少覆盖:登录后说“帮我记一下三栋 3203 需要保洁服务”,即使缺少单元也应看到 `DRAFT` 卡;“暂不保存”不产生正式记录;“仅自己保存”后只由本人召回;“提交公司处理”只显示 `PENDING`,不得显示已送达;同一会话重复描述更新原卡,“另外记一条”才新建;双击确认不得重复写入。再用另一账号验证个人记录不可见,并验证按精确房号查询不会混入同楼栋其他房号。
完整演示脚本与录屏兜底见 [DEMO_ACCEPTANCE.md](DEMO_ACCEPTANCE.md)。
# 个人 AI 助理本地验证
个人知识使用独立 Qdrant collection,默认 `aihr_personal_knowledge`,payload 强制包含 `tenant_id`、`owner_user_id`、`item_id` 与 `captured_at`。对象存储固定使用 `personal-minio` 配置和私有 bucket `ruoyi-personal`,不修改企业资料使用的默认 `minio/ruoyi`。MySQL 表和专用 OSS 配置由 `backend/script/sql/aihr_personal_knowledge_mysql8.sql` 初始化;旧开发库先运行 `COMPOSE_PROJECT_NAME=wygj ./scripts/reset-dev-db.sh`。
企业范围问答另外依赖 `aihr_org_snapshot` 与 `aihr_knowledge_acl`。reset 会给企业 SOP `1001/1002/1003` 写入“一线生活顾问”POSITION ACL;本地物业管家 seed 账号由服务端按固定别名映射到生活顾问后获得 allowlist。普通新注册手机号没有组织快照,企业范围必须返回无权限。不要用手工手机号、客户端岗位参数或 TENANT ACL 绕过该默认拒绝;开放组织系统只负责刷新阶段二快照,北森实时组织/任职仍属阶段三。
空间与处理配额通过 `aihr.personal.*` 配置覆盖,包括空间字节配额、资料数量、单文件大小、抓取字节/超时、worker 与 cleanup 批量大小。不要把个人 collection 改回企业 `aihr_knowledge`。
启动依赖与后端后运行真实隔离 smoke:
```bash
./scripts/dev.sh
./scripts/personal-assistant-smoke.sh
```
脚本每次生成唯一 smoke 手机号与 run marker,通过开发短信登录创建 A/B,并真实采集 TEXT、由 macOS `cupsfilter` 生成的可检索 PDF、公开网页 `https://example.com/`。它会验证三类资料 READY/检索/引用、owner 隔离、私有 OSS 匿名 403、SSRF、幂等删除,以及 MySQL/MinIO/Qdrant 零残留;退出时按本次临时用户清理其全部个人会话/消息,并按 user/item/OSS/job ID 与 run marker 回查清理。公开网页可用 `AIHR_PERSONAL_SMOKE_PUBLIC_URL` 覆盖,页面检索词可用 `AIHR_PERSONAL_SMOKE_PUBLIC_QUERY` 覆盖;客户端只检查 URL 语法,不预先跟随重定向,DNS、逐跳 SSRF 校验和最终 READY 状态以后端为准,公网不可达会明确失败。需要为重定向目标做精确断言时可设置 `AIHR_PERSONAL_SMOKE_EXPECTED_PUBLIC_URL`。脚本不会输出 token。`./scripts/personal-assistant-smoke.sh --signal-self-test` 可单独验证 INT/TERM 分别返回 130/143。
个人网页采集默认从 `/etc/resolv.conf` 读取最多 4 个 DNS resolver,并使用有 socket deadline 的原生 UDP 查询;如运行环境的 resolver 配置不可用,可通过 `AIHR_PERSONAL_DNS_SERVERS=223.5.5.5,1.1.1.1` 显式覆盖。配置项只接受数字 IP,不会递归解析 DNS 服务器名称。