内容
# LinkedOut
**工作机会的 Tinder** — 一个由 AI 驱动的工作机会发现应用,从 5 个远程工作机会板中获取列表,根据您的个人资料对它们进行 LLM 评分,并让您在 iOS 上浏览最佳匹配。
<p align="center">
<img src="https://img.shields.io/badge/SwiftUI-iOS_17+-blue?logo=swift" />
<img src="https://img.shields.io/badge/FastAPI-Python_3.12-green?logo=fastapi" />
<img src="https://img.shields.io/badge/LLM-Gemini_%7C_OpenAI-orange" />
<img src="https://img.shields.io/badge/Deploy-Render-purple?logo=render" />
</p>
---
## 工作原理
```
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌───────────┐
│ 5 个工作机会 API │────▶│ 去重 + │────▶│ LLM 评分 │────▶│ iOS 应用 │
│ (Remotive, │ │ 提取 │ │ (Gemini / │ │ 滑动用户界面 │
│ Himalayas, │ │ 流水线 │ │ OpenAI / │ │ + 地图 │
│ HN, Jobicy, │ │ │ │ 备用) │ │ │
│ RemoteOK) │ └──────────────┘ └──────────────┘ └───────────┘
└─────────────┘
```
1. **获取** — 后端定时(或手动触发)从 5 个远程工作机会板拉取列表
2. **去重** — 3 层 URL 去重防止同一列表出现两次
3. **评分** — 每个列表通过反谄媚提示、Why 矩阵和可配置的惩罚/提升权重运行 LLM 评分流水线
4. **发现** — iOS 应用将评分工作机会呈现为可滑动的卡片。向右滑动申请,向左滑动拒绝,向上滑动保存以备后用
5. **跟踪** — 已申请和保存的工作机会存放在单独的列表中,带有备注和状态跟踪
---
## 目录
- [LinkedOut](#linkedout)
- [工作原理](#工作原理)
- [目录](#目录)
- [功能](#功能)
- [架构](#架构)
- [技术栈](#技术栈)
- [开始使用](#开始使用)
- [先决条件](#先决条件)
- [本地开发(Docker)](#本地开发docker)
- [部署到树莓派(Tailscale)](#部署到树莓派tailscale)
- [部署到 Render](#部署到render)
- [iOS 应用设置](#ios应用设置)
- [配置](#配置)
- [环境变量](#环境变量)
- [评分权重和预设](#评分权重和预设)
- [评分引擎](#评分引擎)
- [两层流水线](#两层流水线)
- [Why 矩阵](#why矩阵)
- [评分校准](#评分校准)
- [惩罚和提升](#惩罚和提升)
- [LLM 备用链](#llm备用链)
- [工作机会来源](#工作机会来源)
- [去重](#去重)
- [位置智能](#位置智能)
- [iOS 应用](#ios应用)
- [标签](#标签)
- [滑动机制](#滑动机制)
- [服务器发现](#服务器发现)
- [设置](#设置)
- [API 参考](#api参考)
- [健康检查](#健康检查)
- [身份验证](#身份验证)
- [个人资料](#个人资料)
- [工作机会](#工作机会)
- [评分和提取](#评分和提取)
- [偏好](#偏好)
- [LinkedIn 社交](#linkedin社交)
- [Notion 同步](#notion同步)
- [遥测](#遥测)
- [开发(仅调试模式)](#开发仅调试模式)
- [数据模型](#数据模型)
- [JobPayload](#jobpayload)
- [UserPreferences](#userpreferences)
- [项目结构](#项目结构)
- [许可证](#许可证)
---
## 功能
- **5 个工作机会来源** 聚合和去重(Remotive, Himalayas, HN Who's Hiring, Jobicy, RemoteOK)
- **LLM 驱动的评分** 采用 Gemini Flash 预处理 + Gemini Pro 全分析,OpenAI 备用
- **动态 AI 求职者角色** — 在 iOS 应用中编辑您的专业个人资料,以重塑 AI 评估提示,并立即重新评分所有待处理的工作机会
- **会话持久性** — OAuth 会话在 Docker 重建中存活;iOS 自动将缓存的个人资料恢复到后端无缝
- **嵌入式 MCP 服务器** — 包括一个内部 FastMCP 服务器,将您的 LinkedIn 个人资料和工作机会流水线状态直接连接到 Claude Desktop
- **Notion 同步** — 与 Notion 数据库的双向同步(推送工作机会,拉取状态更改,iOS 运行时配置)
- **Why 矩阵** — 每个工作机会的结构化 `logic_fit`、`domain_leverage`、`risk_reward` 评估
- **Tinder 式滑动用户界面** — 滑动右侧(申请),左侧(拒绝),向上(保存),可撤销
- **列表/卡片切换** — 在滑动卡片和带有丰富工作机会行的可滚动列表之间切换(评分环,标签,技术栈,适配原因,红旗,AI 推介)
- **交互式地图视图** — 所有工作机会都在 Apple 地图中绘制,带有颜色编码的评分大头针
- **统一的 "您" 中心** — 可点击的流水线仪表盘(队列中,已申请,已保存,已通过)直接导航到每个工作机会列表
- **可配置的评分权重** — 3 个预设(宽松/平衡/严格)+ 每滑块微调,实时同步指示器
- **多位置偏好** — 根据多个首选城市对工作机会进行评分
- **LinkedIn OAuth** — 身份验证并与您的 LinkedIn 个人资料分享申请
- **申请跟踪** — 备注,状态流水线(新 → 已申请 → 电话筛选 → 面试 → Offer)
- **自动服务器发现** — iOS 应用在健康时保持选定的后端,然后在 Raspberry Pi 上通过 Tailscale 降级 → 本地 Docker → Render
- **3 层 URL 去重** — 获取时,提取时和存储时保护免受重复
- **并发安全的提取** — `asyncio.Lock` 防止重叠的周期性和手动提取周期
- **智能内容感知修剪器** — 3 级工作机会新鲜度系统:HTTP HEAD 检查,页面内容扫描 "position filled" 信号,HN 线程分析通过 Algolia API + Gemini Flash LLM 评估
- **3 种排序模式** — 最佳匹配(评分),最新(日期),最高薪酬(工资)具有确定性的多键破坏器
- **自动分页获取** — iOS 以 100 项分页获取工作机会,与任何后端限制一起工作
- **工作机会淘汰保护** — 高显性工作机会(评分 ≥ 0.60)永不过期,扩展 30 天 TTL,已查看 URL 作为带时间戳的字典,30 天过期
---
## 架构
```
LinkedOut/
├── backend/ # FastAPI Python 后端
│ ├── main.py # API 端点 + 提取编排
│ ├── scoring_engine.py # LLM 评分流水线(Gemini/OpenAI/本地)
│ ├── job_fetcher.py # 5 个异步工作机会来源提取器
│ ├── job_store.py # JSON 文件支持的持久存储 + 去重
│ ├── location_mapper.py # 5 级位置分类
│ ├── models.py # Pydantic 模型(JobPayload, UserPreferences 等)
│ ├── config.py # pydantic-settings 环境配置
│ ├── linkedin_api.py # LinkedIn API 客户端(个人资料,分享)
│ ├── linkedin_oauth.py # OAuth 2.0 流程 + 会话持久性
│ ├── notion_sync.py # 双向 Notion 数据库同步
│ ├── mcp_server.py # FastMCP 服务器用于 Claude Desktop
│ ├── Dockerfile # Python 3.12-slim + uvicorn
│ └── requirements.txt # 依赖项
├── LinkedOut/ # SwiftUI iOS 应用
│ ├── LinkedOutApp.swift # 应用入口点(@main)
│ ├── ContentView.swift # 身份验证门控根视图
│ ├── Models/ # 与后端匹配的可编码结构体
│ ├── ViewModels/ # AuthViewModel, JobsViewModel
│ ├── Views/ # 所有 SwiftUI 视图(22 个文件)
│ ├── Network/ # APIClient(actor),ServerDiscovery
│ └── Utils/ # ScoreRing, SwipeHintOverlay, LocationGeocoder, ApplicationTracker
├── LinkedOut.xcodeproj/ # Xcode 项目
├── docker-compose.yml # 本地开发:端口 8443,卷挂载
├── render.yaml # Render 蓝图(一键部署)
└── docs/ # LinkedIn API 参考文档
```
### 技术栈
| 层 | 技术 |
| --------------- | --------------------------------------------------------------------------------- |
| **iOS 应用** | SwiftUI,iOS 26.2+,MapKit,WebKit,Foundation 模型 |
| **后端** | FastAPI,Python 3.12,Pydantic v2,uvicorn |
| **LLM 评分** | Google Gemini(主要),OpenAI(备用) |
| **工作机会 API** | Remotive,Himalayas,HN Algolia,Jobicy,RemoteOK |
| **身份验证** | LinkedIn OAuth 2.0 |
| **存储** | JSON 文件支持(job_store.json,seen_urls.json,user_prefs.json,sessions.json) |
| **部署** | Docker 在 Raspberry Pi 上通过 Tailscale 或 Render |
---
## 开始使用
### 先决条件
- **Xcode 26+** (用于 iOS 应用)
- **Docker** (用于后端)
- **API 密钥**:至少一个 Google Gemini 或 OpenAI
- **LinkedIn 开发者应用** (可选,用于 OAuth)
### 本地开发(Docker)
```bash
# 1. 克隆
git clone https://github.com/Gunnarguy/LinkedOut.git
cd LinkedOut
# 2. 配置环境
cp backend/.env.example backend/.env
# 编辑 backend/.env 您的 API 密钥
# 3. 构建和运行
docker compose up --build -d
# 4. 验证
curl http://localhost:8443/health
# → {"status": "ok", "store": {"pending": 0, ...}}
# 5. 触发首次工作机会提取
curl -X POST http://localhost:8443/api/ingest/refresh
```
后端在 **端口 8443** 上运行,带有 `./data/` 持久化工作机会数据。
### 部署到树莓派(Tailscale)
如果您想将后端从 Mac 上移开,此仓库包含一个 Pi 部署脚本,将后端同步到可通过 Tailscale 访问的树莓派并在那里启动 Docker。
```bash
# 首次部署或正常代码更新
./deploy-to-pi.sh
# 故意覆盖 Pi 的环境或从本地副本持久化数据
./deploy-to-pi.sh --sync-env --sync-data
```
它的作用:
- 将 `backend/` 和 `docker-compose.yml` 同步到 Pi 上的 `~/linkedout`
- 默认保留 Pi 的活动 `data/` 和 `backend/.env`
- 如果 Pi 副本不存在,则自动播种 `data/` 和 `backend/.env`
- 使用 `docker compose up --build -d` 重建并重启后端
- 在 Pi 和通过 Pi 的 Tailscale MagicDNS URL 验证健康状况
操作说明:
- Pi 后端 URL 为 `http://gunzino.taildb93d4.ts.net:8443`
- iPhone 和 iPad 客户端需要启用 Tailscale 才能直接访问 Pi 后端
- Tailscale `Serve` 是可选的。如果禁用,应用仍然可以通过原始 tailnet HTTP 工作
- 保持 `LINKEDIN_REDIRECT_URI` 指向在 LinkedIn 中注册的稳定 HTTPS 回调,除非您稍后添加 Pi 端 HTTPS 前门
### 部署到 Render
1. 复刻此仓库
2. 连接到 [Render](https://render.com) → **新建蓝图实例** → 选择您的复刻
3. 在 Render 仪表盘中设置秘密环境变量:
- `LINKEDIN_CLIENT_ID`
- `LINKEDIN_CLIENT_SECRET`
- `GEMINI_API_KEY`
- `OPENAI_API_KEY` (可选备用)
4. 部署 — Render 使用 `render.yaml` 配置所有其他内容
> **注意:** Render 的免费层使用临时磁盘。工作机会数据在运行时保存在内存中,但在重新部署时重置。3 层去重系统优雅地处理此问题。
### iOS 应用设置
1. 在 Xcode 中打开 `LinkedOut.xcodeproj`
2. 在 Signing & Capabilities 中选择您的开发团队
3. 在模拟器或设备(iOS 26.2+)上构建并运行
4. 应用通过 `ServerDiscovery` 自动发现后端:
- 如果仍然健康,则保持当前后端
- 降级到通过 Tailscale 的 Raspberry Pi → 本地 Docker → Render
- 缓存结果 5 分钟
- 在网络错误时重新发现
---
## MCP 服务器集成
LinkedOut 包含一个 **FastMCP** 服务器,将您的求职流水线和 LinkedIn OAuth 会话直接暴露给 Claude Desktop(或其他 MCP 兼容客户端)。这会创建一个专用 AI 代理,能够审查待处理的工作机会,在 LinkedIn 上发布,返回个人洞察,并触发提取工作流。
### Claude Desktop 设置
将以下内容添加到您的 Claude Desktop `claude_desktop_config.json` 中:
```json
{
"mcpServers": {
"linkedout": {
"command": "docker",
"args": [
"exec",
"-i",
"linkedout-backend-1",
"python",
"-m",
"mcp_server"
]
}
}
}
```
_注意:根据您的 Docker 容器命名调整 `linkedout-backend-1`。命令假设您通过 `docker compose up` 在 LinkedOut 目录中运行 docker。_
可用工具:
- `get_linkedin_auth_status` / `get_my_linkedin_profile`
- `post_linkedin_update`
- `get_linkedout_pipeline_stats`
- `query_pending_jobs` / `get_job_details` / `action_job`
- `trigger_linkedout_ingest`
- `get_saved_jobs_to_share`
---
## 配置
### 环境变量
从示例模板创建 `backend/.env`。所有具有默认值的变量都是可选的。
| 变量 | 必需 | 默认 | 描述 |
| ------------------------ | ---------- | -------------------------------- | ------------------------------------- |
| `LINKEDIN_CLIENT_ID` | 用于 OAuth | `""` | LinkedIn 应用客户端 ID |
| `LINKEDIN_CLIENT_SECRET` | 用于 OAuth | `""` | LinkedIn 应用密钥 |
| `LINKEDIN_REDIRECT_URI` | 用于 OAuth | Render 回调 URL | OAuth 重定向 URI |
| `LLM_PROVIDER` | 否 | `gemini` | 主要 LLM:`"gemini"` 或 `"openai"` |
| `GEMINI_API_KEY` | 用于 Gemini | `""` | Google Gemini API 密钥 |
| `GEMINI_MODEL` | 否 | `gemini-3.1-pro-preview` | 完整评分模型 |
| `GEMINI_FLASH_MODEL` | 否 | `gemini-3-flash-preview` | 预处理(快速)模型 |
| `OPENAI_API_KEY` | 用于 OpenAI | `""` | OpenAI API 密钥 |
| `OPENAI_MODEL` | 否 | `gpt-5.4` | OpenAI 模型名称 |
| `SECRET_KEY` | 否 | 自动生成 | 令牌签名密钥 |
| `MIN_SALARY` | 否 | `90000` | 最低工资过滤器 |
| `REQUIRE_REMOTE` | 否 | `true` | 远程过滤器 |
| `DEBUG` | 否 | `true`(本地),`false`(Render) | 启用开发端点 |
### 评分权重和预设
权重可从 iOS 设置选项卡或通过 `PUT /api/preferences` 配置。三个预设随附:
| 权重 | 放松 🌊 | 平衡 ⚖️ | 严格 🎯 |
| ---------------------------- | ---------- | ----------- | --------- |
| **评分截止** | 0.20 | 0.35 | 0.50 |
| **堆栈不匹配** | –0.10 | –0.20 | –0.35 |
| **"欢迎开发者" 提升** | +0.15 | +0.10 | +0.05 |
| **优先考虑投资组合** | +0.15 | +0.10 | +0.05 |
| **附近惩罚** | –0.01 | –0.03 | –0.05 |
| **区域惩罚** | –0.03 | –0.08 | –0.15 |
| **重新安置惩罚** | –0.05 | –0.15 | –0.25 |
| **国际惩罚** | –0.10 | –0.25 | –0.35 |
| **经验惩罚** | –0.05 | –0.10 | –0.20 |
| **证书惩罚** | –0.05 | –0.15 | –0.25 |
---
## 评分引擎
### 两层流水线
每个原始列表都经过两个阶段:
1. **预处理** (Gemini Flash,快速)— 快速通过/失败过滤器。约 40% 的列表存活下来。在花费代币进行全面分析之前,拒绝明显的不匹配项(错误级别,相关领域,硬资格门槛)。
2. **全面评分** (Gemini Pro)— 深度分析,生成 Why 矩阵,评分,封面信草稿,公司情报,红旗和适配原因。温度 **0.3** 以确保事实一致性。提示使用第二人称语音("您的技能", "您建立")并且封面信草稿避免公司填充 — 自信的同行语气,无"兴奋/惊讶/热情"。
### Why 矩阵
每个评分工作都有四个结构化字段:
| 字段 | 目的 | 长度 |
| --------------------- | --------------------------------------------------------------------------------- | ------------- |
| **`logic_fit`** | 该角色的日常工作如何映射到您实际的工作 | 2–3 句子 |
| **`domain_leverage`** | 您相对于典型申请者有何优势 | 2–3 句子 |
| **`risk_reward`** | 现实的摩擦和上升 | 2–3 句子 |
| **`red_flags`** | 每个工作都有至少一个。硬性要求,模糊产品,资格信号 | 1–5 个项目 |
… (剩余部分未翻译,以保持一致性)
### 评分校准
评分按照现实的分布进行校准,不会夸大:
| 范围 | 工作岗位比例 | 含义 |
| ---------------- | --------- | --------------------------------------------------------------------------------------- |
| **0.85–1.00** | 约5% | 清晰描述您, 公司明确看重作品而非证书 |
| **0.70–0.84** | 约15% | 强烈匹配,需要 minimal 说服 |
| **0.55–0.69** | 约30% | 不错的机会,但有现实摩擦。 有趣但不明确的匹配 |
| **0.40–0.54** | 约30% | 需要大量说服。技术栈或经验差距 |
| **低于门槛** | 约20% | 拒绝。企业,传统,严格的HR,硬性学位要求 |
**锚点分数:0.55** — 具有有趣的使命和AI相关性但没有明确的作品信号的工作。
**评分降低**:`if raw_score > 0.55: deflated = 0.55 + (raw_score - 0.55) * 0.78`
### 惩罚和提升
在基础LLM评估的基础上应用:
**地点** (相对于首选地点):
- 远程 / 在首选城市:+0
- 附近(同一州,约1–2小时车程):`nearby_penalty`
- 区域(邻近州):`regional_penalty`
- 跨国搬迁:`relocation_penalty`
- 国际:`international_penalty`
**技术栈摩擦** — 决定性因素:
- 不熟悉的技术栈中的硬性要求:`convincing_penalty`
- “任何现代框架” / “我们重视建设者”:`convincing_boost`
- 明确重视已交付产品 / 作品优先:`portfolio_boost`
**经验现实**:
- “1–3年” 或 “任何”: +0
- “5+年” 且有灵活性:`experience_penalty`
- 知名精英/选择性(FAANG,Jane Street):`credential_penalty`
**行业乘数**:
- 健康科技 / 医疗科技 / 临床AI:+0.08 (领域区分)
- 开发者/人工智能工具:+0.05
### LLM备用链
当主要LLM速率限制或不可用时:
```
Gemini Flash(分诊)→ Gemini Pro(深度评分)→ OpenAI GPT(备用)
```
系统使用Gemini Flash 2.5快速丢弃不合适的职位,保留更长的延迟Gemini Pro上下文窗口,严格用于合法的潜在匹配。
---
## 工作来源
异步获取并去重五个远程工作板:
| 来源 | API | 提供内容 | 查询 |
| ------------------- | ------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------- |
| **Remotive** | `remotive.com/api/remote-jobs` | 远程优先的工作 | 8个搜索查询(AI工程师、创始工程师、iOS工程师等) |
| **Himalayas** | `himalayas.app/jobs/api` | 远程优先的工作 | 6个搜索查询 |
| **HN Who's Hiring** | `hn.algolia.com/api/v1` | Hacker News上的每月招聘主题 | 查找最新主题 → 获取最多300条评论 → 通过AI/产品/iOS关键词过滤 |
| **Jobicy** | `jobicy.com/api/v2/remote-jobs` | 按标签分类的远程工作 | 7个标签(ai、python、javascript、ios、react、devops、data-science) |
| **RemoteOK** | `remoteok.com/api` | 远程工作聚合器 | 排名前100的列表 |
### 去重
三层防止重复工作:
1. **获取时**: `fetch_all_sources()` 通过URL和公司+职位名称去重所有来源
2. **摄入时**: `_run_ingest_cycle()` 检查`seen_urls`(持久化)和`store.has_url()`(内存索引跨所有桶),以及批次内去重
3. **存储时**: `add_pending()` 拒绝任何已存在于任何桶中的URL(待处理、已应用、已保存、已拒绝)。启动时, `_dedup_on_load()` 清理任何现有的重复项
---
## 工作新鲜度和智能修剪
后台任务每**2小时**运行一次,检查待处理的工作是否仍然有效。修剪器使用保守的3层方法避免假阳性:
### 层1:无效链接检测(HTTP HEAD)
向每个工作的源URL发送HEAD请求。如果服务器返回 **404** 或 **410**,则工作被修剪。LinkedIn和HN URL被跳过(它们总是返回200)。
### 层2:页面内容扫描
对于非HN、非LinkedIn URL,获取页面前 **20KB** 并扫描填充/关闭信号:
- “该职位已填补”
- “不再接受申请”
- “该职位已过期”
- “该职位不再活跃”
- “申请已关闭”
仅扫描 `text/html` 和 `text/plain` 内容类型。瞬时HTTP错误(4xx/5xx)不会触发删除。
### 层3:HN线程分析(Hacker News特定)
对于HN “Who's Hiring” 评论网址:
1. 从URL中提取评论ID
2. 通过 **Algolia HN API** (`hn.algolia.com/api/v1/items/{id}`)重新获取评论
3. 检查 **OP(原始海报)回复** 中的填充语言(“职位已填补”,“不再招聘”,“招聘已关闭”)
4. 如果OP未确认但 **3+ 回复** 包含暗示性关键词 → 调用 **Gemini Flash** 作为LLM裁判
5. LLM提示是保守的:仅在有强烈明确证据的情况下标记“已填补”
所有检查的速率限制为 **每秒1个请求**。每个循环最多检查500个工作。
---
## iOS排序
iOS应用支持 **3种排序模式**,可通过卡片堆栈和列表视图中的下拉菜单选择:
| 模式 | 主要键 | 辅助键1 | 辅助键2 | 图标 | 颜色 |
| --------------- | ----------------- | ----------------- | ---------------- | ------ | ------ |
| **最佳匹配** | `builder_score` ↓ | `posted_at` ↓ | `role_title` A→Z | 星星 | 蓝色 |
| **最新** | `posted_at` ↓ | `builder_score` ↓ | `role_title` A→Z | 时钟 | 橙色 |
| **最高薪酬** | `salary_floor` ↓ | `builder_score` ↓ | `role_title` A→Z | 美元 | 绿色 |
排序模式在 `CardStackView` 和 `PendingJobsListView` 之间共享,通过 `JobsViewModel`。所有排序都使用确定性多键比较器,因此在渲染之间不会改变顺序。
---
## 工作岗位保护机制
几种机制防止优秀工作岗位丢失:
| 保护机制 | 详情 |
| --------------------------- | --------------------------------------------------------------------- |
| **高显著性阈值** | `builder_score ≥ 0.60` 的工作 **永不过期**,即使30天后 |
| **扩展TTL** | 标准工作在 **30天** 后过期(之前为14天) |
| **已查看URL过期** | `seen_urls` 条目在 **30天** 后过期,以便工作可以重新评分 |
| **时间戳已查看URL** | `seen_urls.json` 存储 `{url: ISO_timestamp}` 而不是扁平列表 |
| **最低评分阈值** | 只有评分低于 `0.20` 的工作在摄入过程中被静默丢弃 |
---
## 地理智能
一个5层分类系统根据工作岗位位置与您首选位置的相对关系进行评分:
| 层级 | 名称 | 示例(来自密歇根州卡拉马祖) | 默认惩罚 |
| ----- | ------------- | --------------------------------------------- | --------------- |
| **0** | 家乡 | 远程,或卡拉马祖的工作 | +0 |
| **1** | 附近 | 大 Rapids,Lansing,底特律(同一州) | –0.03 |
| **2** | 区域 | 俄亥俄州,印第安纳州,威斯康星州(邻近州) | –0.08 |
| **3** | 美国其他地区 | 加利福尼亚州,德克萨斯州等 | –0.15 |
| **4** | 国际 | 伦敦,柏林,多伦多等 | –0.25 |
所有50个美国州都有其邻居映射。系统包括密歇根州特定的大都市地区(Grand Rapids,Battle Creek,Lansing,Jackson等)以进行精细的附近检测。
**多位置支持**:如果您有 `preferred_locations: ["Kalamazoo, Michigan", "New York, New York"]`,则纽约的工作岗位获得第0层(家乡)甚至尽管它距离卡拉马祖很远。系统返回所有首选位置中最佳的层级。
---
## iOS应用
### 标签
应用有5个标签:
| 标签 | 视图 | 描述 |
| ------------ | ----------------- | ------------------------------------------------------------------- |
| **发现** | `CardStackView` | 可滑动的卡片堆栈或待处理工作列表,带有列表/卡片切换 |
| **地图** | `JobMapView` | 带有所有工作岗位的Apple Maps(绿色 = 现场,蓝色 = 远程总部) |
| **已申请** | `AppliedJobsView` | 您右滑的工作岗位,带有状态跟踪 |
| **已保存** | `SavedJobsView` | 已添加书签的工作岗位(向上滑动) |
| **您** | `YourHubView` | 统一仪表板 — 可点击的流程统计数据,个人资料和设置 |
### 滑动机制
类似Tinder的卡片手势,带有物理动画:
| 手势 | 阈值 | 操作 |
| --------------------------- | ----------------- | --------- |
| **向右滑动** | >120px 水平 | 申请 |
| **向左滑动** | >120px 水平 | 拒绝 |
| **向上滑动** | >120px 垂直 | 保存 |
| **在阈值以下释放** | <120px | 回弹 |
- **卡片旋转**:`atan(offset.width / 20)` 度,当您拖动时
- **动画**:`easeOut(duration: 0.3)` 在释放时
- **堆栈深度**:可见的顶部3张卡片(按1.0、0.96、0.92缩放)
- **撤销**:恢复最后的滑动操作
- **排序选择器**:带有3种模式的菜单下拉列表(最佳匹配/最新/最高薪酬) — 与列表视图共享
- **自动分页**:以100个项目为单位获取所有工作岗位 — 无缝加载100+工作岗位
### 服务器发现
应用在启动时通过按优先级顺序探测这些候选者来自动发现后端:
1. `https://linkedout-backend-9q4t.onrender.com` (Render云)
2. `http://Gunnars-Brain-Extension.local:8443` (本地Docker via mDNS)
3. `http://10.0.0.175:8443` (LAN IP)
4. `http://localhost:8443` (模拟器)
探测请求访问 `/health`,超时时间为2秒。结果缓存5分钟,网络错误时失效。设置选项卡允许手动覆盖。
### 设置
设置视图(可从“You”中心访问)提供对评分行为的完全控制:
- **快速预设**:宽松 🌊, 平衡 ⚖️, 严格 🎯 — 一键切换
- **评分模拟器**:显示当前权重将如何评分示例工作岗位
- **基础**:最大资历等级,最低薪酬($0–$300k),仅限远程切换
- **可接受的位置**:添加/删除首选城市(多位置)
- **首选角色**:自定义搜索哪些角色标题
- **排除关键词**:阻止特定关键词(Staff, Principal, Director等)
- **高级权重滑块**:单独微调每个惩罚和提升
- **后端同步**:显示连接状态 + 手动服务器URL覆盖
- **同步指示器**:工具栏云徽章(空闲 → 同步中 → 同步完成 → 失败)+ 浮动“已保存和同步”提示成功保存时
- **重置为默认值**:一键恢复所有设置
所有设置通过 `@AppStorage`(UserDefaults)本地持久化 — 它们在后端离线时仍然存活。当连接时,通过 `PUT /api/preferences` 将设置同步到后端。
---
## API参考
### 健康
| 方法 | 路径 | 描述 |
| ------ | --------- | --------------------------- |
| GET | `/health` | 服务器健康 + 存储统计 |
### 认证
| 方法 | 路径 | 描述 |
| ------ | -------------------------- | ----------------------------------------- |
| GET | `/auth/login` | 获取LinkedIn OAuth URL |
| GET | `/auth/callback` | OAuth回调(重定向到应用) |
| POST | `/auth/token` | 交换身份验证代码为个人资料 |
| GET | `/auth/status/{person_id}` | 检查会话有效性 |
| POST | `/auth/restore` | 恢复缓存的个人资料到后端会话 |
### 个人资料
| 方法 | 路径 | 描述 |
| ------ | --------------------- | ------------------------------------ |
| GET | `/api/profile/resume` | 获取完整的LinkedIn个人资料 + 简历 |
### 工作
| 方法 | 路径 | 描述 |
| ------ | --------------------------- | --------------------------------------- |
| GET | `/api/jobs/pending` | 待处理的工作(查询:`limit`, `offset`) |
| GET | `/api/jobs/{job_id}` | 按ID获取单个工作 |
| GET | `/api/jobs/applied` | 所有已申请的工作 |
| GET | `/api/jobs/saved` | 所有已保存的工作 |
| GET | `/api/jobs/rejected` | 所有已拒绝/已通过的工作 |
| GET | `/api/jobs/stats` | 流程统计数据 |
| POST | `/api/jobs/action` | 申请/拒绝/保存工作 |
| POST | `/api/jobs/undo` | 撤销最后的操作 |
| POST | `/api/jobs/import` | 批量导入预评分工作 |
| PUT | `/api/jobs/{job_id}/notes` | 更新工作笔记 |
| PUT | `/api/jobs/{job_id}/status` | 更新申请状态 |
### 评分和摄入
| 方法 | 路径 | 描述 |
| ------ | -------------------------- | ---------------------------------------- |
| POST | `/api/score` | 评分单个列表 |
| POST | `/api/score/batch` | 评分一批列表 |
| POST | `/api/jobs/rescore` | 重新评分所有待处理的工作(非阻塞) |
| GET | `/api/jobs/rescore/status` | 重新评分进度 |
| POST | `/api/ingest/refresh` | 触发后台摄入 |
| GET | `/api/ingest/status` | 检查摄入状态 |
### 首选项
| 方法 | 路径 | 描述 |
| ------ | ------------------ | ----------------------- |
| GET | `/api/preferences` | 获取当前首选项 |
| PUT | `/api/preferences` | 更新首选项 |
### LinkedIn 社交
| 方法 | 路径 | 描述 |
| ------ | ---------------------------- | -------------------------------- |
| POST | `/api/share` | 在LinkedIn上分享工作岗位为文章 |
| POST | `/api/share/post` | 在LinkedIn上发布自由文本 |
| POST | `/api/share/media` | 发布带有图像附件 |
| POST | `/api/share/document` | 发布带有文档附件 |
| POST | `/api/share/reshare` | 转发现有的LinkedIn帖子 |
| GET | `/api/linkedin/posts` | 获取用户的LinkedIn帖子 |
| POST | `/api/linkedin/comments` | 添加评论到帖子 |
| POST | `/api/linkedin/reactions` | 添加反应到帖子 |
| DELETE | `/api/linkedin/reactions` | 删除反应 |
| GET | `/api/linkedin/capabilities` | 可用的LinkedIn API功能 |
### Notion 同步
| 方法 | 路径 | 描述 |
| ------ | ---------------------------- | ------------------------------------------ |
| POST | `/api/notion/configure` | 运行时Notion设置(令牌 + 数据库ID) |
| GET | `/api/notion/status` | Notion集成状态 + 架构 |
| GET | `/api/notion/schema` | Notion数据库架构 |
| POST | `/api/notion/sync` | 双向同步(非阻塞) |
| POST | `/api/notion/push` | 将所有工作岗位推送到Notion |
| POST | `/api/notion/pull` | 从Notion拉取更改 |
| GET | `/api/notion/jobs` | 列出所有Notion数据库条目 |
| GET | `/api/notion/jobs/{page_id}` | 获取单个Notion页面 |
| PATCH | `/api/notion/jobs/{page_id}` | 更新Notion页面属性 |
| DELETE | `/api/notion/jobs/{page_id}` | 存档Notion页面 |
| POST | `/api/notion/jobs` | 创建新的Notion页面 |
### 遥测
| 方法 | 路径 | 描述 |
| ------ | ---------------- | ------------------ |
| GET | `/api/telemetry` | 遥测快照 |
### 开发(仅调试模式)
| 方法 | 路径 | 描述 |
| ------ | ------------------------------- | ------------------------------------------ |
| POST | `/api/dev/seed` | 播种模拟工作岗位 |
| POST | `/api/dev/reset-seen` | 清除已查看的URL |
| POST | `/api/dev/clear-pending` | 清除待处理队列 |
| POST | `/api/dev/purge-keyword-scored` | 清除按关键词评分的工作岗位 |
| GET | `/api/dev/logs` | 最近的日志行(查询:`n`,默认为500) |
---
## 数据模型
### 工作负载
后端和 iOS 之间传递的核心工作对象:
```
id UUID 字符串
company_name 公司名称
role_title 工作标题
salary_floor 最低薪水(0 = 未知)
salary_max 最高薪水(0 = 未知)
is_remote 远程标志
builder_score 0.0–1.0 匹配分数
location 地点字符串
posted_at 发布时间(可为空)
source_url 原始列表 URL
apply_url 直接申请链接
tags 技术/行业标签(最多 5 个)
# AI 生成智能
ai_pitch_summary 3 点摘要(Markdown)
drafted_cover_letter 约 150 字求职信
description 完整工作描述
company_description 关于公司
company_size 例如“10-50”,“500+”
company_stage 例如“种子期”,“A 轮”,“上市公司”
company_url 公司网站
requirements 关键要求(列表)
nice_to_haves 期望条件(列表)
tech_stack 提到的技术(列表)
experience_level 例如“入门级”,“中级”,“高级”
job_type 例如“全职”,“合同工”
benefits 福利(列表)
# 为什么是矩阵
logic_fit 角色如何与您的技能匹配
domain_leverage 您的优势
risk_reward 现实的摩擦 + 收益
red_flags 关注点(列表,始终 ≥1)
# 匹配信号
fit_reasons 2–4 个简短的匹配原因
dealbreaker_warnings 0–3 个需要说服的警告
# Notion 交叉引用
notion_page_id 链接的 Notion 页面 ID(可为空)
# 用户字段
notes 个人笔记
application_status “新” | “已申请” | “电话面试” | “面试” | “offer” | “已拒绝”
```
### 用户偏好
```
min_salary 最低薪水筛选(默认值:90000)
require_remote 仅远程切换(默认值:false)
preferred_roles 搜索的角色标题
excluded_keywords 阻止的关键词
location_preference “远程”(默认值)
preferred_locations ["卡拉马祖,密歇根州"] (多城市)
# 评分权重
score_cutoff 拒绝低于此值(默认值:0.35)
convincing_penalty 堆栈不匹配(默认值:-0.20)
convincing_boost “欢迎建设者”(默认值:+0.10)
portfolio_boost 基于投资组合的信号(默认值:+0.10)
nearby_penalty 同一州(默认值:-0.03)
regional_penalty 邻近州(默认值:-0.08)
relocation_penalty 全美搬迁(默认值:-0.15)
international_penalty 国际(默认值:-0.25)
experience_penalty 需要 5+ 年经验(默认值:-0.10)
credential_penalty FAANG/僵化(默认值:-0.15)
max_seniority_level “初级” | “中级” | “高级” | “任意”
```
---
## 项目结构
```
LinkedOut/
├── backend/
│ ├── main.py # FastAPI 应用,所有端点,摄取编排
│ ├── scoring_engine.py # LLM 评分(三级 + 完整 + 备用链)
│ ├── job_fetcher.py # 5 个异步工作源获取器
│ ├── job_store.py # JSON 支持的存储,4 个桶 + 去重
│ ├── location_mapper.py # 5 级地理分类,50 个州邻接图
│ ├── models.py # Pydantic 模型(JobPayload, UserPreferences 等)
│ ├── config.py # 环境配置(pydantic-settings)
│ ├── linkedin_api.py # LinkedIn 配置文件 + 分享 API
│ ├── linkedin_oauth.py # OAuth 2.0 流程 + 会话持久化
│ ├── notion_sync.py # 双向 Notion 数据库同步
│ ├── mcp_server.py # FastMCP 服务器,用于 Claude Desktop
│ ├── Dockerfile # Python 3.12-slim 容器
│ ├── requirements.txt # fastapi, uvicorn, pydantic, httpx, google-genai, openai
│ └── .env.example # 环境变量模板
├── LinkedOut/
│ ├── LinkedOutApp.swift # @main 入口,注入 ViewModels
│ ├── ContentView.swift # 认证门户 → MainTabView 或 LoginView
│ ├── Models/
│ │ ├── JobPayload.swift # 可编码工作模型(40+ 个字段)
│ │ ├── UserPreferences.swift # 可编码偏好模型
│ │ └── UserProfile.swift # LinkedIn 配置文件 + 认证模型
│ ├── ViewModels/
│ │ ├── AuthViewModel.swift # LinkedIn 认证状态
│ │ └── JobsViewModel.swift # 工作数据,操作,缓存
│ ├── Views/
│ │ ├── MainTabView.swift # 5 个标签容器(Discover,Map,Applied,Saved,You)
│ │ ├── CardStackView.swift # Tinder 式滑动 UI + 列表/卡片切换
│ │ ├── JobCardView.swift # 单个工作卡片
│ │ ├── JobDetailView.swift # 完整工作详情(可滚动)
│ │ ├── JobMapView.swift # MapKit 工作图钉
│ │ ├── AppliedJobsView.swift # 已申请 + 已保存列表(增强的 JobListRow)
│ │ ├── YourHubView.swift # 统一仪表盘:统计数据,配置文件,设置
│ │ ├── PendingJobsListView.swift # 待处理队列列表(来自统计卡片)
│ │ ├── RejectedJobsView.swift # 已拒绝工作列表
│ │ ├── ResumeView.swift # LinkedIn 配置文件显示
│ │ ├── SettingsView.swift # 偏好设置 + 权重调整 + 同步指示器
│ │ ├── LoginView.swift # OAuth 登录流程
│ │ ├── OAuthWebView.swift # WKWebView,用于 LinkedIn 认证
│ │ ├── FilterSheet.swift # 工作筛选控件
│ │ ├── ApplyReviewSheet.swift # 申请确认表格
│ │ ├── ComposePostView.swift # LinkedIn 文章撰写
│ │ ├── ShareSheetView.swift # iOS 分享表格包装器
│ │ ├── SafariView.swift # 应用内 Safari 浏览器
│ │ ├── NotionDatabaseView.swift # Notion 数据库浏览器
│ │ ├── TelemetryView.swift # 后端遥测仪表盘
│ │ ├── OnboardingOverlay.swift # 首发教程叠加层
│ │ ├── ErrorBanner.swift # 错误 + 信息横幅
│ │ └── SwipeHintOverlay.swift # 视觉滑动方向提示
│ ├── Network/
│ │ ├── APIClient.swift # 基于 Actor 的 HTTP 客户端
│ │ └── ServerDiscovery.swift # 自动发现后端 URL
│ └── Utils/
│ ├── LocationGeocoder.swift # 带缓存的 CLGeocoder
│ ├── ScoreRing.swift # 环形得分指示器
│ ├── ApplicationTracker.swift # 申请状态跟踪
│ └── SwipeHintOverlay.swift # 视觉滑动方向提示
├── docker-compose.yml # 本地开发:后端 + 卷挂载
├── render.yaml # Render Blueprint 配置
├── .gitignore # 忽略 .env, data/, __pycache__, xcuserdata/
└── docs/ # LinkedIn API 参考文档
```
---
## 后台任务
三个定期任务在后端运行:
| 任务 | 间隔 | 描述 |
| ------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **定期摄取** | 6 小时 | 从所有 5 个工作 API 获取,重复数据删除,评分,存储。同时在每个循环中运行 `expire_old_jobs()` 和 `expire_stale_seen_urls()` |
| **智能修剪器** | 2 小时 | 3 级工作新鲜度:HEAD 检查 → 页面内容扫描 → HN 线程分析。速率限制为 1 请求/秒 |
| **保持活动** | 10 分钟 | 自我 ping `/health` 以防止 Render 休眠实例 |
所有任务在应用程序启动时通过 FastAPI `lifespan` 上下文管理器启动,并在关闭时正常取消。
---
## 路线图
- [x] 5 个工作源聚合 + 3 级重复数据删除
- [x] 为什么矩阵的 LLM 评分 + 反媚俗提示
- [x] Tinder 式滑动 UI + 卡片/列表切换
- [x] LinkedIn OAuth + 会话持久化
- [x] 双向 Notion 同步
- [x] 用于 Claude Desktop 的 MCP 服务器
- [x] 动态 AI 候选人角色 + 重新评分流程
- [x] 智能内容感知工作修剪器(3 级:HEAD + 页面扫描 + HN 线程 LLM)
- [x] 3 种模式排序系统(最佳匹配/最新/最高薪水)
- [x] 自动分页 iOS 获取(100 项页面)
- [x] 工作淘汰保护(高显著性阈值,30 天 TTL,已见 URL 过期)
- [ ] 高评分新工作推送通知
- [ ] 自动申请工作流(一键申请提交)
- [ ] 面试准备 — 每家公司 LLM 生成的准备笔记
- [ ] 分析仪表盘 — 评分分布,申请率,来源质量
---
## 许可
个人项目。目前不允许重新分发。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器