内容
# job-engine-spring
`job-engine-spring` 是一个仅限本地的模型上下文协议(MCP)服务器,用于存储规范化的候选人资料和工作岗位,生成可解释的候选人-工作岗位匹配报告,并生成简历文档。它是使用 Java 25、Spring Boot、Spring AI、PostgreSQL 和 Flyway 构建的。
该项目是一个 MCP 后端,而不是托管服务、Web UI、公共 REST API 或工作板爬虫。正常操作使用 Streamable HTTP,仅在主机环回上发布。PostgreSQL 对 Docker Compose 来说是私有的,隔离的 STDIO 仅存在于 CI、包验证和诊断中。
## 快速开始:选择路径
### 先决条件
- Git
- Docker Engine 或带有 Docker Compose v2 的 Docker Desktop
- 用于捆绑的 MCP 烟雾客户端的 Python 3
- 直接在主机上运行 Maven 时仅需 Java 25
### 获取部署文件
两种路径都需要此检出用于 Compose、部署脚本、环境模板和烟雾客户端。发布的发布路径 **不** 构建源代码,**不** 需要主机 Java 或 Maven。
```bash
git clone https://github.com/JonHHH09/job-engine-spring.git
cd job-engine-spring
cp .env.example .env
```
### 运行发布的版本(推荐给用户)
使用防护助手部署选定的已发布镜像。对于当前的已验证公共版本,请使用 [`v0.1.22`](https://github.com/JonHHH09/job-engine-spring/releases/tag/v0.1.22):
```bash
./scripts/run-release-mcp-http.sh ghcr.io/jonhhh09/job-engine-spring:v0.1.22
```
对于不可变的部署,请传递完整的已发布镜像摘要而不是标签。助手仅接受 `ghcr.io/jonhhh09/job-engine-spring:v<semver>` 标签或完整的摘要,拉取镜像并重新创建持久性服务而无需构建。
### 从源代码构建(用于开发)
在更改应用程序代码或测试本地源代码更改时使用此路径:
```bash
docker compose up -d --build --wait postgres mcp
```
### 验证和连接
在任一部署路径后,通过以下命令验证持久性服务:
```bash
python3 scripts/smoke-mcp-http.py
```
烟雾命令初始化 MCP 会话,发现工具表面,调用 `health`,如果数据库未就绪则失败。支持的端点是:
```text
http://127.0.0.1:8080/mcp
```
不要在非环回主机地址上发布 MCP 端口,也不要发布 PostgreSQL。`.env.example` 中的值是综合本地开发默认值;保留复制的 `.env` 文件私有,并在将其用于除隔离本地开发之外的任何环境之前替换其密码。
使用上面的 URL 配置名为 `job-engine-spring` 的 Streamable HTTP MCP 服务器。对于 Hermes Agent:
```yaml
mcp_servers:
job-engine-spring:
url: http://127.0.0.1:8080/mcp
enabled: true
timeout: 120
connect_timeout: 60
```
重新加载客户端的 MCP 连接,然后调用 `health`。成功的响应报告数据库准备就绪以及清理队列计数、过期完成行计数、最旧到期年龄和重复失败状态,而不返回凭证、文件路径、时间戳或失败详细信息。只要任何完成的行超过配置的保留窗口,清理健康状况就会降低。
已完成生成的简历清理审计行保留 30 天,然后每天删除。每次运行最多可排出 10 批 1,000 行,默认容量为 10,000 行/次,并会在短批次或空批次后提前停止。操作员可以调整保留窗口、批大小、每次运行的最大批次数、计划、五分钟最旧到期健康阈值和三尝试重复失败阈值,使用 `JOB_ENGINE_CLEANUP_*` 环境变量,如 `application.yaml` 中所述。当完成的行吞吐量可以超过配置的每次运行容量时,增加批大小、最大批次或计划频率。清理警告和错误使用专用的 stderr 日志,因此 STDIO stdout 仅用于 MCP JSON-RPC。
### 第一个实用工作流程
对于第一个工作岗位插入,使用合成文本;普通的 hostname 工作板网址由于 URL 提取限制在直接链接摄取方面故意不支持。
调用 `add_job_from_text` 并提供类似以下内容:
```text
Senior Java Engineer at Example Corp in Berlin. Build local backend services
with Java 25, Spring Boot, PostgreSQL, Docker, and MCP. Five years of backend
experience required. Full-time.
```
可选地提供结构化字段,如职称、公司、地点、技能、资历和就业类型。然后调用 `list_jobs` 或 `search_jobs` 以读取规范化的结果。在评估项目时,不要使用真实简历、私人申请、凭证或生产数据。
### 持久性、源代码更新和发布升级
PostgreSQL 数据在 Compose 管理的 `postgres-data` 卷中持久存在。
停止容器同时保留数据:
```bash
docker compose down
```
在不停用数据库卷的情况下更新源代码构建的检出:
```bash
git pull --ff-only
docker compose up -d --build --force-recreate --wait postgres mcp
python3 scripts/smoke-mcp-http.py
```
在进行重大源代码或发布升级之前,创建事务一致的备份:
```bash
./scripts/postgres-backup.sh
```
要安全地升级发布的版本,请选择标签或不可变的摘要,然后部署、进行烟雾测试并重新加载 MCP 客户端:
```bash
./scripts/postgres-backup.sh
./scripts/run-release-mcp-http.sh ghcr.io/jonhhh09/job-engine-spring:v0.1.22
python3 scripts/smoke-mcp-http.py
# Reload your MCP client connections (for Hermes Agent: /reload-mcp).
```
单独拉取镜像永远不会更改正在运行的容器。镜像回滚 **不是** 数据库回滚:切勿使用较旧的镜像来逆转 Flyway 迁移。在需要数据库恢复时,请使用 [PostgreSQL 备份和恢复](#postgresql-备份和恢复) 中的显式、可丢弃的目标恢复过程。
破坏性地卸载本地容器 **并删除 PostgreSQL 卷**:
```bash
docker compose down -v
```
该最终命令永久删除本地应用程序数据。请参阅 [PostgreSQL 备份和恢复](#postgresql-备份和恢复) 在使用您需要保留的数据之前。
## 项目状态和非目标
- 服务器正在积极开发中;MCP 模式和持久数据合约可能在 1.0 版本之前的版本之间发生变化。
- 仅最新的发布版本接收安全修复。
- 支持的网络模型仅限本地。禁止将 MCP 或 PostgreSQL 公开给其他机器。
- 提供者支持的丰富功能是可选的。确定性存储、搜索和匹配独立可用。
- 直接主机名 URL 摄取功能故意不可用;使用 `add_job_from_text` 获取普通的工作板内容。
请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 获取开发和拉取请求要求,[SUPPORT.md](SUPPORT.md) 获取问题路由,[SECURITY.md](SECURITY.md) 获取私人漏洞报告,[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) 获取社区期望。
## 持久匹配分析
该服务在 `match` PostgreSQL 模式中持久存储不可变的、可解释的候选人-工作岗位匹配报告。确定性 `deterministic-v1` 基线使用技术(40)、经验/资历(25)、领域(15)、交付(10)和硬性要求(10)组件。缺少结构化证据时,证据为 `UNKNOWN`,降低置信度,永远不会被非结构化领域或交付推断取代。分数根据已知证据标准化,因此已知证据可以产生 `STRONG_MATCH`,而置信度仍然较低。工作岗位技能的 `required` 属性本身并不是阻塞证据。
咨询审查单独存储,不会取代基线。审查摘要仅使用 `review_consistent`、`score_adjustment`、`outcome_adjustment` 或 `evidence_defect_identified`;拒绝自由格式摘要。咨询证据必须准确重用确定性基线中的事实或使用 `structured_evidence_missing`、`structured_evidence_incorrect`、`requirement_provenance_missing` 或 `outcome_calibration_issue` 中的一项。分歧政策 `divergence-v1` 为整体增量至少 15、结果或阻塞不匹配、组件增量至少 40% 的可用积分或明确改变结果的证据缺陷而创建其唯一指纹。该指纹使用报告、持久策略版本、排序原因分类和规范结果更改的缺陷代码 - 而不是审查身份、审查者、模型或摘要。
MCP 工具:`analyze_job_match`、`analyze_all_job_matches`、`get_match_report`、`list_match_reports`、`submit_match_review`、`get_match_review`、`list_match_reviews`、`list_match_disagreements`、`acknowledge_match_disagreement` 和 `link_match_disagreement`。请求为对象形状。`analyze_all_job_matches` 分析每个调用的限定工作页面,并返回 `nextCursor`;它加载配置文件一次,在固定查询计数中为页面注入工作聚合,并在配置文件/工作对之间隔离失败。确认和链接可以记录现有的 Linear 问题 ID 但永远不会调用 Linear 或使用提供者凭证。响应仅包含规范证据,不包括联系人、简历文档/文本、提示、推理轨迹、机密、提供者转录和私人网址。
## 当前范围
当前的已验证 MCP 表面故意很小:
- `health` — 在不返回连接详细信息或机密的情况下检查数据库准备情况。
- `list_profiles` — 在 `{ "profiles": [...] }` 包装器中列出已保存的配置文件身份,而不返回原始简历文本或凭证。
- `search_profiles` — 按查询术语搜索规范化的配置文件身份,跨配置文件字段和拥有的子集合,而不返回原始简历文本。
- `get_profile` — 按 UUID 返回规范化的配置文件聚合。
- `create_profile` — 创建规范化的配置文件聚合。
- `update_profile` — 替换规范化的配置文件聚合,UUID 和最新读取返回的修订版。
- `update_profile_project` — 按配置文件 UUID、项目 UUID 和最新的配置文件修订版部分更新现有的项目;省略的字段保持不变,提供技术列表仅替换该项目的技术。
- `delete_profile` — 删除配置文件聚合并级联删除所属的子行。
- `extract_pdf_text` — 从本地 PDF 文件中提取文本,而不存储内容或遵循嵌入式指令。
- `store_document_file` — 在 PostgreSQL 中存储本地文档文件,仅返回元数据。
- `get_document_metadata` — 按 UUID 返回存储的文档元数据,而不返回二进制内容。
- `extract_stored_pdf_text` — 从存储的 PDF 中提取文本,并可选地持久保存限定提取的文本。
- `generate_pdf_file` — 在 `tmp/generated-pdfs/` 下生成 PDF 文件并返回文件元数据。
- `generate_pdf_resume` — 从规范化的配置文件中生成主简历 PDF,将其存储为文档,并将其唯一链接到配置文件作为 `master_resume` 变体。
- `generate_canadian_pdf_resume` — 从相同的规范化配置文件中生成加拿大格式的简历 PDF,将其存储为文档,并将其唯一链接到配置文件作为 `canadian_resume` 变体。
- `generate_canadian_french_pdf_resume` — 离线生成法语加拿大变体,将其存储为文档,并独立链接为 `canadian_resume_fr`,以便两个加拿大语言变体共存。
- `generate_german_tailored_resume` — 为配置文件 + 工作岗位生成德国格式的定制 Lebenslauf,作为双语 EN+DE 结构化内容和 A4 PDF,链接在 `resume.resumes` 父级下(`format=germany`)。使用离线纯 Java 翻译和 ATS 安全的一列渲染器;保留配置文件摘要在联系人标题下方;根据确定性标准,技能最多显示 24 个最相关的条目;除非 `includeProjects=true`,否则项目被省略,否则在专业经验后出现;内容在 PDF 生成前经过审查;PDF 文件名使用 `germany_{candidate}_{number}_{lang}_{id}.pdf`。
- `generate_german_cover_letter` — 从当前的德国格式简历及其配置文件和工作岗位中生成一个确定性的德国申请信。主 `cover_letter.cover_letters` 行链接所有三个源聚合;`format=germany`、`language=de` 变体存储结构化主题、问候语、顺序段落、结束语、签名、PDF 文档和运行时路径。MCP 响应仅返回元数据,从不返回信件正文。
- `ingest_profile_from_stored_pdf` — 从存储的 PDF 提取中填充规范化的配置文件架构,并将配置文件链接到该提取。
- `get_profile_pdf_source` — 返回配置文件的唯一 PDF 提取源链接。
- `list_jobs` — 列出存储的工作岗位,不返回源摄取的原始文本。
- `search_jobs` — 按标题、公司、地点、描述、工作经验要求、资历、就业类型和所需技能搜索存储的工作岗位。
- `get_job` — 按 UUID 返回存储的工作岗位聚合,包括规范化的技能和插入出处。
- `update_job` — 使用最新读取返回的修订版部分更新存储的工作岗位;省略的字段保留当前值,而提供的技能列表替换现有的技能。
- `delete_job` — 按 UUID 硬删除存储的工作岗位。
- `add_job_from_text` — 从粘贴的文本和可选的结构化字段插入工作岗位;源文本被哈希处理以确保幂等性,不以方法特定的表格存储原始文本。
- `add_job_from_link` — 通过获取页面内容并将提取的页面文本与可选的结构化字段相结合,从公共 IP 字面 HTTP(S) URL 插入工作岗位。主机名 URL 被拒绝,直到 HTTP 客户端可以将验证绑定到它实际连接的地址。如果页面获取被阻止,返回 HTTP 4xx/5xx,或解析为机器人/安全检查内容,则工具拒绝插入,而是使用 `add_job_from_text` 获取普通的工作板网址。调用者的完整检索 URL 仅用于获取本身,从不持久或返回 MCP。
- `analyze_job_link` — 获取工作岗位网址,调用配置的 Hermes 分析提供者,持久存储仅限红色的 URL 出处和结构化的 Hermes 响应在 `job_schema.job_analysis_runs` 中,并返回分析运行报告,而不创建工作岗位。
- `add_job_from_analysis` — 通过从 `job_schema.job_analysis_runs` 读取先前存储的 Hermes 分析运行,创建或重用规范化工作岗位。
应用程序以 MCP 为主。除非明确需要 REST 兼容性,否则不要添加 REST 控制器。
## 架构
该项目遵循基于 Spring Boot 的六边形布局:
- `domain` — 纯 Java 记录/值对象,无 Spring、MCP、JDBC 或持久性注解。
- `application` — 用例、端口、事务和安全的应用错误。
- `adapter/in/mcp` — 薄的 Spring AI MCP 工具适配器。
- `adapter/out/postgres` — PostgreSQL/JDBC 适配器在应用程序端口后面。
Flyway 在 `src/main/resources/db/migration` 下拥有模式创建和演化。 将应用的 `V*__*.sql` 迁移视为不可变;为模式更改添加新的版本化迁移。
## 配置
通过环境变量或安全的本地默认值配置 PostgreSQL 连接:
设置 `JOB_ENGINE_CURSOR_HMAC_SECRET` 至至少 32 个高熵字符,当分页游标必须在进程重启后保持有效时。如果省略,服务器会生成一个临时的进程本地密钥,现有的游标在重启时过期。
```yaml
spring:
main:
web-application-type: servlet
datasource:
url: ${JOB_ENGINE_POSTGRES_URL:jdbc:postgresql://localhost:5432/job_engine}
username: ${JOB_ENGINE_POSTGRES_USER:}
password: ${JOB_ENGINE_POSTGRES_PASSWORD:}
job-engine:
document:
import-root: ${JOB_ENGINE_DOCUMENT_IMPORT_ROOT:tmp/imports}
```
永远不要提交真实的凭证、私人简历数据、API 密钥、生产连接详细信息或机器本地绝对路径。 配置路径不得硬编码;使用环境占位符、文档的安全相对默认值或调用者提供的运行时设置。
MCP 仅限本地操作。正常操作使用仅在主机环回接口上发布的持久 Streamable HTTP MCP 容器。 `McpLocalOnlyStartupGuard` 接受环回 HTTP 和显式标记的容器运行时,同时拒绝不安全的传输/绑定组合。 工具模式不携带每次调用的访问令牌;安全边界是仅限环回的 Docker 发布加上本地 OS 边界。 STDIO 仅通过 `stdio` Spring 配置文件用于 CI、发布验证和隔离的工程诊断。 本地文件导入用于 PDF 提取/存储,默认情况下限制在 `job-engine.document.import-root`;生成的简历 PDF 跳过导入根检查,因为它们在 `tmp/generated-pdfs/` 下生成。 工作岗位 URL 获取是 SSRF 加强的:重定向不会被跟随,本地/私有/元数据/用户信息目标在发送前被拒绝,只有公共 IP 字面主机被接受,因此验证的地址是连接使用的地址。 主机名允许列表配置被故意删除,因为在 `HttpClient` 连接之前解析主机名不会防止 DNS 重新绑定/TOCTOU 攻击。
## 本地持久化 MCP 部署
正常的本地部署是一个持久化的 Spring Boot Streamable HTTP MCP 容器。Docker 管理应用程序的生命周期,Hermes 连接到 `http://127.0.0.1:8080/mcp`,因此重新连接客户端不会替换应用程序容器。
使用以下命令构建和验证本地 jar:
```bash
./scripts/rebuild-local-mcp-jar.sh
```
该脚本运行 `./mvnw test`,打包 `target/job-engine-spring-0.0.1-SNAPSHOT.jar`,然后在 Hermes CLI 可用时运行 `hermes mcp test job-engine-spring`。要跳过单元测试,可以运行:
```bash
RUN_TESTS=false ./scripts/rebuild-local-mcp-jar.sh
```
重新构建 jar 或拉取镜像只会更新磁盘上的工件。要部署它,请重新创建持久化服务,然后在活动的 Hermes 会话中运行 `/reload-mcp`。如果工具名称、参数模式、提示或资源发生变化,请在重新加载后启动一个新的 Hermes 会话,并使用 `/reset` 以使代理上下文中的工具模式保持最新。
`scripts/restart-local-mcp-server.sh` 被保留为本地维护助手,因为它停止了过时的匹配本地 jar 子进程,重新构建 jar 而不递归运行 MCP 烟雾测试,并且可选地在前台 STDIO 模式下运行服务器。它仅限于本地,不会暴露 HTTP 守护进程。
## 本地容器化 MCP 开发详情
`compose.yaml` 启动私有 PostgreSQL 和持久化 MCP 服务。PostgreSQL 没有主机端口。MCP 只发布容器端口 8080 作为 `127.0.0.1:${JOB_ENGINE_MCP_PORT:-8080}`,并使用 `restart: unless-stopped`。
源代码开发快速入门构建并启动此服务。当迭代已经运行时,使用以下命令重新构建本地镜像:
```bash
docker compose build mcp
```
然后重新创建持久化源构建服务并重新运行共享烟雾检查:
```bash
docker compose up -d --wait postgres mcp
python3 scripts/smoke-mcp-http.py
```
更改部署的源镜像后,重新加载 MCP 客户端(对于 Hermes Agent:`/reload-mcp`)。如果工具名称、参数模式、提示或资源发生变化,请在重新加载后启动一个新的 Hermes 会话,并使用 `/reset` 以使代理上下文中的工具模式保持最新。
### 隔离 STDIO 验证
遗留的 `scripts/run-local-mcp-container.sh` 启动器显式激活 `stdio` 配置文件。它被保留用于 CI 和发布包检查。交互式诊断必须使用唯一名称启动器,以免删除持久化 Compose `mcp` 服务。
对于诊断或并行烟雾测试,当 Hermes 连接时,请使用安全的诊断启动器(默认情况下为唯一容器名称):
```bash
./scripts/run-mcp-stdio-diag.sh
MCP_CONTAINER_BUILD=never python3 scripts/smoke-mcp-stdio.py -- ./scripts/run-mcp-stdio-diag.sh
```
当 Hermes 报告 MCP 不可达时:
1. 检查 `docker compose ps mcp postgres` 和持久化服务健康状况。
2. 运行 `python3 scripts/smoke-mcp-http.py` 以独立验证传输。
3. 运行 `/reload-mcp` 一次,并在 CRUD 工具之前调用原生 `health`。
4. 仅在发布/工程诊断时使用 `run-mcp-stdio-diag.sh`。
主机可见的本地文件导入从 `tmp/imports/` 挂载到容器中作为只读文件,生成的 PDF 通过 `tmp/generated-pdfs/` 挂载。导入根目录之外的路径(例如 `~/.hermes/cache/documents/` 下的 Hermes 文档缓存)在 MCP 容器内不可见;在 `store_document_file` 之前,请将文件复制到 `tmp/imports/`。永远不要更改 MCP 主机绑定回环或发布数据库端口而不具备单独的身份验证网络架构决策。
使用以下命令对容器化 MCP 传输进行烟雾测试:
```bash
# 正常的持久化传输:
python3 scripts/smoke-mcp-http.py
# 显式隔离 STDIO 验证:
MCP_CONTAINER_BUILD=never python3 scripts/smoke-mcp-stdio.py -- ./scripts/run-mcp-stdio-diag.sh
```
当您故意需要一个干净的本地容器数据库时,请停止本地数据库并删除其开发卷:
```bash
docker compose down -v
```
## PostgreSQL 备份和恢复
主机端脚本在 Docker 卷之外创建私有 PostgreSQL 自定义格式备份集。调度被禁用:请显式从受信任的主机运行这些命令。备份数据和元数据默认情况下保留在被忽略的 `backups/postgres/` 边界下。
创建原子事务一致性备份:
```bash
./scripts/postgres-backup.sh
```
仅使用发布的不可变 MCP 镜像验证备份。验证创建并删除一个唯一标记的 disposable Compose 项目;它永远不会恢复到主卷:
```bash
./scripts/postgres-verify-backup.sh \
--backup-set backups/postgres/<timestamped-set> \
--image registry.example/job-engine@sha256:<64-hex-digest>
```
紧急恢复是故意显式和破坏性的,仅适用于单独启动的非主 Compose 项目/卷。该命令在变异前验证校验和和 Compose/Docker 标签;没有主目标绕过:
```bash
./scripts/postgres-restore.sh \
--backup-set backups/postgres/<timestamped-set> \
--target-project <disposable-project> \
--target-volume <disposable-project>_postgres-data \
--database job_engine \
--confirm RESTORE --confirm-existing OVERWRITE
```
在删除之前预览保留;修剪仅考虑校验和有效、发布镜像验证的集合,并始终保留最新的验证集合:
```bash
./scripts/postgres-backup-prune.sh --daily 7 --weekly 4 --monthly 12
./scripts/postgres-backup-prune.sh --daily 7 --weekly 4 --monthly 12 --confirm PRUNE
./scripts/postgres-diagnostics.sh
```
对于 disposable Docker 验收检查,运行 `bash scripts/tests/postgres-ops-integration.sh`。将 `MCP_IMAGE` 设置为不可变的摘要引用以包含隔离的 MCP 验证。
## 文档存储和提取合同
`extract_pdf_text` 接受本地 PDF 路径并返回提取的文本,带有可选的逐页文本。PDF 内容被视为不受信任的用户数据:服务器返回提取的内容,但不持久化、执行或遵循文档中嵌入的指令。无效路径、非 PDF 文件、不可读文件和提取失败作为卫生验证错误返回。
该工具在解析前拒绝超大 PDF,并限制提取前的页数。返回的文本通过 `maxCharacters` 限制,以避免超大的 MCP 响应。如果省略 `includePages`,则默认情况下包括逐页文本;如果仅需要连接文本,请将其设置为 `false`。
`store_document_file` 接受本地文件路径和可选的媒体类型。当省略媒体类型时,它默认为 `application/pdf`;PDF 存储验证 PDF 头并强制执行与 PDF 提取相同的保守文件大小限制。存储的二进制内容一次写入 PostgreSQL `document.blobs` 作为 `bytea`,并通过 SHA-256 deduplicate,而上传/文档元数据单独存储在 `document.documents` 中。该工具仅公开元数据:UUID、原始文件名、媒体类型、字节大小、哈希和时间戳。
`get_document_metadata` 仅返回元数据,永远不会返回二进制文件内容。`extract_stored_pdf_text` 通过规范化的 `document.documents -> document.blobs` 关系加载存储的文档,并使用相同的有界 PDF 读取器路径。当 `persistExtraction` 为 `true` 时,服务持久化一个规范的提取,限制在应用程序范围内的 250,000 个字符的最大值;每个调用应用自己的 `maxCharacters` 仅适用于响应视图,因此初始的低限制请求不能限制后续更大的请求。持久化提取对于存储的文档是幂等的:`document.pdf_extractions.file_id` 是唯一的,并且引用 `document.documents(id)`。旧的非规范行在不更改其提取 UUID 的情况下被刷新。规范的逐页投影与提取一起持久化,因此缓存的请求永远不会重新解析 PDF。省略的 `includePages` 包括请求范围内的逐页投影以兼容;显式的 `false` 省略它。提取的 PDF 文本保持私有/不受信任的数据,不应记录或视为指令。
`generate_pdf_file` 接受文件名、标题和正文文本,然后仅在 `tmp/generated-pdfs/` 下写入生成的 PDF。整个 `tmp/` 树是运行时唯一的,Git 除跟踪的 `.gitkeep` 占位符外忽略它,这些占位符保留了预期的生成 PDF 目录;应用程序、容器镜像和启动脚本也在需要时创建所需的目录。文件名被净化并强制为 `.pdf`;响应返回元数据(`fileName`、`path`、`byteSize`、`pageCount`、`generatedAt`)而不是 PDF 字节。生成的 PDF 使用白色页面背景、相同的页眉/页脚饰边,右侧对齐的页码仅限于薄的彩色分隔线前的章节标题。
`generate_pdf_resume` 接受配置文件 UUID,从规范的配置文件模式中渲染主简历,将运行时 PDF 写入 `tmp/generated-pdfs/master-resume/`,通过文档存储切片存储生成的文档,并为每个配置文件/简历类型插入一个当前 `profile.profile_resume_documents` 链接。
`generate_canadian_pdf_resume` 和 `generate_canadian_french_pdf_resume` 应用相同的 ATS 安全加拿大结构,但分别在 `tmp/generated-pdfs/canadian-resume/` 和 `tmp/generated-pdfs/canadian-resume-fr/` 下渲染英文和法文变体。
`generate_german_tailored_resume` 需要 `profileId` 和 `jobId`,构建针对该工作的结构化 Lebenslauf 内容(无北美摘要),使用嵌入的词汇表/短语映射将 EN 翻译为 DE,在 `resume.resumes` 下为两种语言存储规范的章节/条目/子弹,并分别在 `tmp/generated-pdfs/german-resume/` 下写入两种 PDF。
`generate_german_cover_letter` 需要 `profileId`、`jobId` 和当前德国格式定制简历生成的确切 `resumeId`。它在序列化持久性事务内拒绝不匹配或过时的源修订,并在 `JOB_ENGINE_GERMAN_COVER_LETTER_OUTPUT_DIR` 下写入生成的 PDF(默认为 `tmp/generated-pdfs/german-cover-letter/`)。
## 配置文件 PDF 提取合同
`ingest_profile_from_stored_pdf` 协调存储文档的提取、保守的配置文件字段提取、规范的配置文件持久性和来源链接。它始终使用 `persistExtraction=true` 提取存储的 PDF,然后通过 `ProfileService` 创建或更新配置文件,以便配置文件验证/规范化仍然是单一的写入门。
凭证链是一对一的:
```text
document.blobs.sha256 UNIQUE
-> document.documents.blob_id
-> document.pdf_extractions.file_id UNIQUE
-> profile.profile_pdf_sources.pdf_extraction_id UNIQUE
-> profile.profile_pdf_sources.profile_id UNIQUE
-> profile.profiles + profile-owned child tables
```
如果为同一个存储的 PDF 重新运行提取,服务会返回现有的配置文件/来源链接,并且不会复制文件、提取、配置文件或配置文件来源行。如果相同的 PDF 字节作为不同的文档行再次存储,提取会通过 `document.blobs.sha256` 解析先前的来源链接并返回现有的配置文件/来源链接,而不是创建另一个配置文件。对于更改/重新导出的 PDF,如果它们不共享相同的 SHA-256,提取会检查强提取的标识字段,然后在创建新配置文件之前:规范的电子邮件和规范的链接。强匹配返回非变异的结果状态(`DUPLICATE_PROFILE_CANDIDATE` 或 `AMBIGUOUS_PROFILE_CANDIDATES`)以及候选配置文件 ID、匹配字段和推荐的重新运行操作。成功的变异/重用路径也返回显式状态:`CREATED_PROFILE`、`UPDATED_PROFILE` 或 `REUSED_EXISTING_SOURCE`。提取工具返回配置文件/文档/提取/来源 ID 和计数;它不返回原始提取的简历文本。
确定性回退提取器是节意识的:它从 `Summary`/`Profile` 节中读取摘要文本,仅从识别的技能/技术节中提取技术技能,仅从语言节中提取人类语言,从它们自己的节中提取用管道分隔的教育/经验/项目条目,并规范化明显的 LinkedIn/GitHub 链接,通过删除查询字符串、片段和尾部斜杠。结构化提取保持保守,并将 PDF 文本视为不受信任的数据。
## 配置文件写入合约
MCP 成功响应使用对象形状的 `structuredContent`,因此拒绝原始顶级数组的客户端可以一致地验证响应。例如,`list_profiles` 返回 `{ "profiles": [...] }` 而不是一个裸数组。
`create_profile` 和 `update_profile` 在持久化之前在应用层验证配置文件写入请求。`update_profile` 需要最新的返回 `revision`;过时的写入返回一个净化的 `conflict`,而不会替换任何配置文件子项。`update_profile_project` 还需要最新的配置文件修订版,并且只更新一个现有的项目;省略的项目字段被保留,而提供的技术列表仅替换该项目的技术。它返回持久化的项目和新的配置文件修订版,而不暴露原始的简历文本。无效的有效载荷被拒绝,并带有 `validation_error` 应用异常和仅包含无效字段路径和原因的安全详细信息。配置文件 MCP 适配器将应用和意外的故障映射到 MCP `CallToolResult` 错误,并使用净化的结构化内容而不是泄漏原始异常文本。
`search_profiles` 由归一化的配置文件聚合上的确定性 `ProfileSearchService` 支持。`SearchTextNormalizer` 是规范化查询/写入的规范所有者:它应用 Unicode NFKD 分解,移除组合标记,使用 `Locale.ROOT` 小写,并为 Java 证据评分和持久化搜索帖子标记一次。搜索查询仅限于 256 个 Unicode 字符和 16 个不同的可搜索项。PostgreSQL 读取每个查询令牌的限定数量的索引帖子,排名最多 500 个候选者,然后在固定的一组集合查询中加载这些聚合。响应公开 `matchedCount`、`hasMore` 和 `returnedCount`;`matchedCount` 是一个下限,当发布或候选工作达到其限制时,兼容性 `totalMatches` 为 `null`,因为精确的语料库范围计数需要无限工作。结果仅包含配置文件标识、分数和匹配的字段名称,而不是原始简历文本、描述或私有文档内容。
在验证后,配置文件写入在持久化之前被规范化:必需文本被修剪,电子邮件和规范化键被小写,空白可选文本变为 `null`,而 `null` 子集合变为空列表。PostgreSQL 还通过 Flyway 管理的索引和 `NOT VALID` 检查约束强制规范唯一性和未来写入规范表单的配置文件电子邮件和规范配置文件子键。持久化约束失败被映射到净化的验证错误。
当前验证的示例包括:
- 非空白 `fullName` 和电子邮件
- 基本电子邮件形状
- 必需的联系、链接、技能、语言和项目技术字段
- 非负的显示顺序值
- 教育/经验日期范围
- 复制规范的技能、语言、链接、联系人、教育条目、经验、项目和项目技术,位于一个请求中
数据库约束仍然保护持久性完整性,但预期的请求问题应该在 PostgreSQL 约束错误之前失败。
## 工作岗位插入合约
工作存储使用 Flyway 管理的 `job_schema` 模式。规范的工作字段位于 `job_schema.jobs` 中,必需/规范化的技能位于 `job_schema.job_skills` 中,方法特定的来源通过插入路径分离:
- `job_schema.job_text_ingestions` 用于过去的文本插入元数据,按规范源文本的 SHA-256 哈希键控。
- `job_schema.job_link_ingestions` 用于 URL 插入元数据,按规范 URL 键控并存储获取状态/标题来源。
- `job_schema.job_analysis_runs` 用于 URL 分析来源:有界结构化输入,持久化结构化 Hermes 响应,验证状态/错误,响应哈希和规范的工作 ID,一旦分析被接受。
每个存储的工作都有一个确切的来源,它必须与 `job_schema.jobs.source_method` 匹配:基于文本的工作需要一个 `job_text_ingestions` 行,并且没有 `job_link_ingestions` 行,而基于链接的工作则相反。该不变式在领域聚合、PostgreSQL 存储库写入路径和 Flyway 迁移 `V14__enforce_job_source_provenance.sql` 中得到强制执行,在 V13 安全 URL 身份迁移之后。
`add_job_from_text` 和 `add_job_from_link` 是幂等的,但链接身份故意分为三种形式。完整的检索 URL 是短暂的,仅用于出站获取。持久化/MCP 可见的 `url` 是一个编辑后的安全显示 URL,移除了用户信息、查询和片段。持久化的 `normalized_url` 是用于重复数据删除的规范链接身份,它仅保留识别的 ATS 主机的受限发布标识符(Indeed 上的 `jk` 和 Greenhouse 上的 `gh_jid`)。诸如 `gh_src`、推荐/会话值、未识别的主机查询参数和畸形或超大标识符等跟踪值始终被丢弃。链接提交首先检查 `normalized_url`,然后是规范的工作指纹,该指纹现在包括基于链接的工作的规范链接身份,以便不同的 ATS 发布 ID 不会崩溃。文本提交仍然以 `input_text_hash` 和内容指纹为键。直接链接路径故意限制为公共 IP 字面 URL;使用粘贴文本进行普通主机名的工作板。应用层从提供的/获取的文本中推导出保守的标题、必需的技能和经验要求,但在显式字段缺失时,调用者可以用更好的数据覆盖结构化字段。`update_job` 需要最新的返回 `revision`,保留省略的字段,在标题/公司/位置/描述更改时重新计算规范指纹,仅在提供技能列表时替换技能,并保留插入来源;过时的写入在子项替换之前返回一个净化的 `conflict`。`delete_job` 删除规范行并通过 Flyway 管理的模式级联删除拥有的技能/来源行。
分段 Hermes URL 流被故意拆分。`analyze_job_link` 验证 URL,获取有界页面内容,使用调用者提供的 URL,持久存储安全显示 URL 加上规范链接身份,仅发送这些编辑后的表单到提供者,存储结构化的 Hermes 响应在 `job_schema.job_analysis_runs` 中,并返回分析运行报告,而不写入规范的工作。`add_job_from_analysis` 然后加载存储的分析行,验证/规范持久响应,在 Java 中拒绝弱 URL 仅或 JavaScript 外壳分析,然后创建或重用规范的工作。直到配置了实时 Hermes CLI 适配器,默认分析提供者记录安全失败的分析,而不是假装富集成功。
工作搜索使用与配置文件搜索相同的规范 Unicode 标准化器、256 个字符/16 项查询限制和索引的加权帖子设计。它确定性地排名标题/公司/位置/描述/经验/就业/资历/技能,填充最多 500 个候选者在一个固定集合的查询中,并返回匹配的字段名称和分数。它的计数元数据遵循相同的 `matchedCount`/`hasMore` 合约和可空的兼容性 `totalMatches`。分数是内部搜索元数据,而不是面向申请人的简历内容。
## 有界列表分页
`list_jobs`、`list_profiles`、`analyze_all_job_matches`、`list_match_reports`、`list_match_reviews` 和 `list_match_disagreements` 接受 `limit`(默认为 20,最大 100)以及不透明的版本控制 `cursor`。匹配审查和分歧列表可以按 `reportId` 过滤;省略它会列出所有报告。光标嵌入了不可变的 `created_at, id` 键集、第一个页面的数据库时间水印和 SHA-256 指纹的工具范围和过滤器;HMAC-SHA-256 信封验证了完整的有效载荷。删除前一页的锚点或更新可变的分歧状态不会使遍历无效;其不可变的 `created_at` 比第一页的水印晚的行被故意排除。这是有限的水印遍历,而不是跨请求的 PostgreSQL MVCC 快照:一个事务在第一页开始并在之后提交,仍然可以贡献一行,当其 `created_at` 在或之前的水印时。超大、畸形、篡改、跨工具和更改过滤器的光标返回一个净化的 `validation_error`。光标是 API 令牌,客户端不得解析或制造它们。遗留请求省略新的分页字段,保留有限的默认页面,空请求对象对 `list_jobs` 和 `list_profiles` 仍然有效。
文档工具仍然是点操作(`store_document_file`、`get_document_metadata` 和按 UUID 提取/生成);没有文档列表快照 API 或残留的无限文档列表。此更改中没有添加文档列表;如果以后添加,必须采用相同的 opaque 快照/键集策略,而不是公开原始 UUID 锚点。
## 验证
在开发配置文件或文档行为时运行专注的单元测试:
```bash
./mvnw -q -Dtest=ProfileWriteValidatorTests,ProfileWriteCanonicalizerTests,ProfileServiceTests,ProfileMcpAdapterTests test
./mvnw -q -Dtest=PdfTextExtractionServiceTests,DocumentStorageServiceTests,PdfGenerationServiceTests,GeneratePdfResumeServiceTests,OfflineFrenchResumeTranslatorTests,DocumentMcpAdapterTests,DocumentPdfGenerateResumeMcpTests,DocumentCaPdfGenerateResumeMcpTests,DocumentCanadianFrenchPdfGenerateResumeMcpTests test
./mvnw -q -Dtest=ProfilePdfIngestionServiceTests,ProfilePdfIngestionMcpAdapterTests test
./mvnw -q -Dtest=JobServiceTests,JobAnalysisServiceTests,JobMcpAdapterTests,HttpJobLinkContentFetcherTests test
./mvnw -q -Dtest=PostgresProfileSearchIntegrationTests,PostgresJobSearchIntegrationTests,PostgresJobRepositoryIntegrationTests test
```
在声明非数据库完成之前运行单元测试套件:
```bash
./mvnw test
```
在 Docker/Testcontainers 可用时,运行 Docker 支持的 PostgreSQL 集成测试和 JaCoCo 覆盖率门控:
```bash
./mvnw -Pintegration-tests verify
```
GitHub Actions 管道在 `development` 的拉取请求上运行快速验证和单元测试。针对 `master` 的功能/修复拉取请求还运行 Docker 支持的集成/覆盖率门控、容器化 MCP STDIO 烟雾测试和 Qodana。`development` → `master` 促销 PR 是一个特殊情况,其候选树必须与 `origin/development` 完全匹配;相同的促销防护措施在直接推送到 `master` 时作为深度防御措施运行。受信任的推送到 `development` 和手动 CI 运行也保留了重大的集成和容器烟雾门控。成功合并同一个存储库的 `development` → `master` 拉取请求会自动启动发布工作流;直接推送、分叉拉取请求、其他源分支和关闭但未合并的拉取请求无法发布。工作流确认合并提交仍然是当前的 `origin/master` 提示,并且与合并的促销拉取请求上记录的不可变头部 SHA 树身份相同,因此后续对 `development` 的合法更改不会使发布无效。然后,它将最高稳定 `vMAJOR.MINOR.PATCH` 标签(忽略预发布标签)的补丁组件递增。重新运行的重用稳定标签仅当它已经指向确切的验证源提交时。自动促销共享一个序列化并发组,其 `cancel-in-progress: false`;GitHub 仅保留每个组的一个待处理运行,因此快速重叠的促销可能会合并,并且只有最新的待处理促销会被尝试。过时的运行在它们的源不再是当前的 `master` 提示时关闭。手动干燥运行使用每个运行的组,不能取代自动促销。
该工作流运行干净的完整验证 (`./mvnw clean -Drevision=<version> -Pintegration-tests verify`),复制经过验证的 Maven jar 到版本发布工件,构建并烟雾测试 `linux/amd64` 和 `linux/arm64` 发布镜像,使用相同的经过验证的 jar 记录和传输每个确切的烟雾测试平台镜像,并为 jar 和每个镜像生成 CycloneDX 和 SPDX SBOM。 在发布前,它重新检查验证的源代码与当前的 `master`,创建并推送注释的发布标签幂等,并发布而不依赖于 bot 创建的标签推送来触发另一个工作流。 发布作业在编写 GHCR 多平台镜像列表之前验证每个加载的镜像 ID 和对应的注册表配置摘要。 稳定版本也发布 `:latest`;工作流验证两个必需平台,使用密钥 Cosign 签名和 attestation 镜像列表的不可变摘要,然后上传 jar、校验和、每个平台的 SBOM、镜像 ID/摘要和验证报告作为发布工件。 Docker Desktop 在 macOS 和 Windows 上自动选择匹配的 Linux 架构;该项目不发布 `darwin/*` 或 Windows 容器变体。
容器输入在 `Dockerfile`、`Dockerfile.release` 和 `compose.yaml` 中被 digest-pinned。 `.github/dependabot.yml` 每周跟踪 GitHub Actions 和 Docker 引用,以便可以有意刷新固定的基数和固定的工作流操作,而不是无声地漂移。
发布是促销驱动的;不要在正常发布路径期间手动创建或推送发布标签。 在其所需检查通过后合并审查的 `development` → `master` 拉取请求,成功的合并事件会自动执行下一个稳定补丁发布。
为了在不发布任何内容的情况下安全地演练发布流程,请从分支或 `development` 手动运行 `.github/workflows/release.yml`。 工作流调度路径执行干净的 Maven 验证、验证的 jar 打包、构建并烟雾测试两个 Linux 发布架构,并生成 jar 加每个平台镜像的 SBOM。 它还使用 `SHA256SUMS` 演练无密钥 Cosign 签名和 SLSA 来源声明,然后使用预期的 GitHub Actions OIDC 工作流标识验证两者。 手动调度始终保持干燥运行:它从不创建标签或 GitHub 发布,也从不发布 GHCR 包。 下载干燥运行工件并在本地使用以下命令验证 JAR 校验和:
```bash
sha256sum -c SHA256SUMS
```
Maven Wrapper 现在为固定的 Apache Maven 3.9.16 分发记录 `distributionSha256Sum`。 更新 `.mvn/wrapper/maven-wrapper.properties` 时,请从 Apache 下载确切的替换存档,验证其上游校验和/签名,本地计算其 SHA-256,然后一起更新 `distributionUrl` 和 `distributionSha256Sum`。
## 许可证
`job-engine-spring` 在 [Apache License 2.0](LICENSE) 下获得许可。第三方依赖关系仍然受其各自许可证的约束。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器