跳转到内容

配置文件在哪里:默认配置 + 用户配置目录

Job Radar 的所有设置都是 YAML,分两层:

层 位置(环境变量,默认值) 谁改 进 git
默认配置 JOB_RADAR_DEFAULTS_DIR,默认 config/ 只随产品版本变 是(随产品发布)
用户配置 JOB_RADAR_CONFIG_DIR,默认 data/config/ 你、网页(/settings、/interests、/sources/registry)、pnpm config:* 否(data/ 被忽略)
画像 JOB_RADAR_PROFILE_DIR,默认 data/profile/ 你、网页 /me 否

相对路径按项目目录解析(不是当前目录)。Docker 里 web 和 runner 的挂载和变量写在 compose.yaml(web:./config:/app/config:ro、./data/config:/app/data/config;runner 两个都只读);宿主机的 ai-worker 由 scripts/ops/install-ai-worker.sh 把三个目录显式写进 launchd 的 plist。

  • 用户目录里有这个文件就读它,没有就读默认目录的。两层不合并:用户目录里的文件是整份生效的。
  • 网页保存永远写用户目录;第一次保存一个只在默认目录里的文件时,以默认文件为底稿生成(默认文件的注释会带过去)。默认目录从不被写。保存时,文件里还没有的键如果取值就是 schema 的默认值,也不会写进去(否则会把默认值固定住,以后产品更新改了默认值你也拿不到);文件里已有的键会保留,与默认值不同的取值才会写入。
  • 热加载同时盯两层:用户文件新出现、被删除(退回默认)、默认文件变了(你没有自己那份时)都在约 5 秒内生效。runner 和 ai-worker 盯所有配置文件(包括 me.yaml、preferences.yaml),并在每轮周期和每个请求开始时再读一遍,所以改动(比如在设置向导或 /me 里保存的市场)在下一轮周期就生效,即使它紧接着就开始,也不用重启。
  • pnpm config:check 逐个文件写出读的是哪一层:(user)、(default)、(user override of a shipped file)。
类 文件 首次初始化 产品升级时
user schedule、search、sources、ai、notify、interests、ui、breakdown、resume、insights、language、setup、salary(salary.zh.md) 从默认目录复制到用户目录,之后归你 不会自动改你的文件;格式变化由迁移(config_version)带过去
shipped models(模型目录和价格)、pipeline(解析规则、城市和技能表)、signal-presets(/interests“新增类别”对话框的预设,见 signals.zh.md) 不复制,一直读默认目录的 跟着产品更新

产品升级新增的 user 文件会在下次启动时从默认目录复制过来;如果用户目录里本来就有配置(老安装),可以改为写入保持原有行为的版本 (language.yaml:中文,见 languages.zh.md)。初始化向导用同一个“老安装”判断。

想改 shipped 文件:把它复制到用户目录再改(整份覆盖)。代价是产品以后对这个文件的更新不再生效:pnpm config:check 和 doctor 的 config.layers 会一直提示“你覆盖了 models.yaml”。想重新跟随产品:删掉用户目录里那份。

市场包(config/packs/<id>/:国家、城市、本地语言规则)也按 shipped 处理:data/config/packs/<id>/ 里的副本整包替换发布的那份。见 market-packs.zh.md。

终端窗口
pnpm config:init # 建用户目录,复制缺的 user 文件和画像模板;已有的不动(可重复运行)
pnpm config:init --from <目录> # 从别的目录复制(旧安装迁移用),并复制与默认内容不同的 shipped 文件
pnpm config:migrate # 列出要迁移的文件和 diff,不写
pnpm config:migrate --write # 迁移:先把要改的原文件备份到 data/config/.backup/<时间>/,再写
pnpm config:check # 校验两层(默认目录的文件即使被覆盖也要合法),写出每个文件读的是哪层
pnpm config:example # 列出示例配置;`pnpm config:example <id> [--dry-run]` 套用其中一个(见下文)

web 和 runner 启动时也会自动做一次 config:init(缺什么补什么;runner 在容器里对用户目录只读,目录已满时不需要写);scripts/ops/deploy.sh 第 4 步依次运行 config:init → config:migrate --write → config:check。

setup.yaml 记录首次设置的状态(completed_at、completed_by、steps),由设置向导和 pnpm run setup 写入。全新安装时它是 “未完成”(打开任何页面都进入 /setup,runner 不跑定时轮次)。config:init(以及 web / runner 启动时)发现用户目录里已经有配置 文件、却没有 setup.yaml 时,会写入一份已完成的(completed_by: existing_install),所以老安装永远不会看到向导。读不了的文件按 已完成处理。见 setup.zh.md。

随产品发布的默认配置是刻意中性的:一组占位关键词(targets: [Software Engineer]),没有公司招聘页(ats_boards: []), 招聘页通道没有额外的职位名词(include_terms: []),时区 America/New_York,画像模板里是一个美国的示例市场。设置向导 (或 pnpm run setup)会用你的回答替换它们。

现成的配置以示例的形式放在 config/examples/<id>/:每个目录有一个 example.yaml(名称、说明)和几个 user 文件的 局部副本,只写它要设的键。不套用就不会被读取:

终端窗口
pnpm config:example # 列出
pnpm config:example ai-engineering-europe --dry-run # 只看会改哪些设置
pnpm config:example ai-engineering-europe # 套用:先把要改的文件备份到 data/config/.backup/<时间>/,再写

套用时把每个文件合并进你在 data/config/ 里的那份(注释保留):映射按键合并,列表整个替换;ats_boards 例外,只补上你 还没有的招聘页;keyword_groups 只有还是发布时的占位内容时才整组替换,否则把示例的关键词组加在你的旁边。会替换掉你已有条目的列表会先给出警告(–dry-run 也会)。重复套用不会再改。

示例 设什么
ai-engineering-europe search.yaml:AI / LLM / ML 工程、Forward Deployed 和 AI 解决方案岗位的关键词组;sources.yaml:招聘网站和公司招聘页通道的 AI 职位名词,十家示例公司的招聘页(Greenhouse、Lever、Ashby、Personio、SmartRecruiters);ai.yaml:这些职位名的翻译词表。市场、城市和时区不在里面:在向导里选(这个示例是按德国市场包、Berlin、Europe/Berlin 做的)。

想用同样的方式分享你自己的配置:在旁边放一个目录,写好 example.yaml 和局部文件即可。

有些值决定宿主机运行哪个程序,或者凭据发往哪里。runner 和宿主机上的 ai-worker 带着 .env 里的全部密钥运行,所以 web (设置表单、高级编辑器、设置向导、来源登记)不能改这些键,只能在用户配置目录的文件里手动改:

文件 受保护的键
ai.yaml providers.*.command(claude / codex 可执行文件)、providers.*.base_url(DeepSeek、MiMo)、providers.codex_cli.config_overrides、providers.codex_cli.disable_features、providers.codex_cli.trim_context
notify.yaml telegram.api_base_url
sources.yaml 所有 base_url、api_base_url、eu_api_base_url,jobspy.url,jobspy.proxies,sources.gmail.host / port / secure,linkedin.session.site.auth_file,linkedin.apify.actor_id
resume.yaml claude_command、claude_args、resume_kit_path、master_resume_path、pdf_dir、jd_export_dir、jd_export_host_dir
ui.yaml web_access 下的全部键(登录限制,见网页访问)
  • web 保存时,如果受保护键的值既不同于文件里的值,也不同于出厂默认值,就拒绝保存。写回文件里已有的值或出厂默认值(例如“恢复默认”,或旧文件还没有的键)是允许的。
  • 设置表单里这些字段是只读的,带锁定说明;高级编辑器在文本上方列出它们,改动会在对应行标成问题(“受保护的键”),保存按钮不可用。
  • 如果确实要让 web 改(例如只有你自己能访问 web 的机器),在 .env 里设 JOB_RADAR_ALLOW_WEB_EDIT_PROTECTED=1 并重启 web(docker compose up -d web)。表单里的字段仍显示为锁定,请用高级编辑器改。
  • 清单在 src/lib/config/protected-keys.ts。

上面的锁定是 web 这一侧。ai-worker 和 runner 也不信任文件(它可能是手动改的,或者设了 JOB_RADAR_ALLOW_WEB_EDIT_PROTECTED=1),所以有几个值在使用的地方再检查一次,不管文件里写了什么(出厂值总能通过):

  • ai.yaml providers.claude_cli.command / providers.codex_cli.command 必须是 claude / codex 这个可执行文件(裸名字或绝对路径);providers.codex_cli.config_overrides 只保留设为 false 的裁剪上下文开关;不管 disable_features 和 trim_context 怎么写,codex 的工具限制始终生效。
  • 接收凭据的服务(DeepSeek、MiMo、Telegram、JSearch、Tavily、Brave、Apify)的 base_url / api_base_url 必须指向厂商自己的主机,sources.gmail 必须是带 TLS 的 imap.gmail.com:993,除非 .env 里设了 JOB_RADAR_ALLOW_CUSTOM_ENDPOINTS=1。带凭据的请求不跟随重定向。
  • claude / codex 的模型名必须是普通名字(没有空格,不以 - 开头)。

两个放行开关互不相干:JOB_RADAR_ALLOW_WEB_EDIT_PROTECTED=1 只让 web 能写这些键,JOB_RADAR_ALLOW_CUSTOM_ENDPOINTS=1 让进程把凭据发往你自己的主机。检查不通过时会怎样,见 ai-providers.zh.md;doctor 的 config.guards 会列出每个被忽略或拒绝的值。

哪些主机名能访问 web(JOB_RADAR_ALLOWED_HOSTS)

Section titled “哪些主机名能访问 web(JOB_RADAR_ALLOWED_HOSTS)”

web 只响应发往已知主机名的请求,用来防 DNS rebinding:别的网站把自己的域名重新解析到 127.0.0.1 后,它的页面就能像仪表盘自己一样访问仪表盘。不用任何设置就允许的:

  • localhost、127.0.0.1、[::1](任意端口);
  • *.ts.net(Tailscale:tailscale serve、MagicDNS)。

这个清单不取自 web 能写的任何配置文件:notify.yaml: public_base_url(Telegram 消息里的链接)不会加入它的主机名。Host、X-Forwarded-Host 或 Origin 是其他名字的请求会得到 421 Misdirected Request 和一段简短说明。会改动东西的请求(表单、Server Action,GET / HEAD 以外的一切)还必须来自它发往的同一个地址:另一个允许的主机上的页面,例如 tailnet 里的另一台机器、localhost:5173 上的另一个程序,算另一个网站,同样拒绝。除请求自己的主机外,只接受 JOB_RADAR_ALLOWED_HOSTS 里列出的来源。如果你用别的名字访问 web(Caddy、nginx 等反向代理,局域网主机名或 IP),把它加进 .env 并重启 web:

终端窗口
JOB_RADAR_ALLOWED_HOSTS=jobs.example.com,*.home.example,192.168.1.20

用逗号分隔主机名或 IP 地址(IPv6 加方括号);*.example.com 允许 example.com 下的所有名字(*. 后面至少要两段,*.com 会被忽略);name:port 只允许那个端口。

反向代理必须把公开的主机名传下去,放在 Host 或 X-Forwarded-Host 里(Caddy 默认这样做;nginx 用 proxy_set_header Host $host; 或 proxy_set_header X-Forwarded-Host $host;)。否则表单和操作会被拒绝,因为浏览器的来源和 web 看到的地址对不上。Docker 镜像里 Server Actions 的来源清单在构建镜像时就固定了;传下主机名就不需要它。

网页访问:可选口令、监听地址和端口

Section titled “网页访问:可选口令、监听地址和端口”

默认情况下 web 只监听 127.0.0.1:3000,没有登录:在你自己的机器上别人访问不到,手机通过 tailscale serve 进来。口令是可选的。只要除你之外还有人能打开面板(共享的 tailnet 里经 tailscale serve、局域网、反向代理之后),就设一个。

终端窗口
pnpm web:password # 输入两次(不回显),打印一行
# JOB_RADAR_WEB_PASSWORD_HASH=scrypt:131072:8:1:... -> 贴进 .env
docker compose up -d web # 创建容器时 web 读取 .env

变量(都在 .env 里,从不放在 web 能写的文件里):

变量 默认 含义
JOB_RADAR_WEB_PASSWORD_HASH 空 pnpm web:password 生成的 scrypt 哈希(推荐)。空 = 不登录。
JOB_RADAR_WEB_PASSWORD 空 简易写法:明文口令。两者都设时哈希生效。doctor 会建议改用哈希。
JOB_RADAR_WEB_BIND 127.0.0.1(compose) compose 在宿主机上发布 web 的地址,也是 pnpm start / pnpm dev 监听的地址。0.0.0.0(或局域网地址)会向网络开放:先设口令,否则 doctor 的 compose.web 失败。在 compose 之外不设 = 监听所有网卡(本机也不免登录,见下文)。
JOB_RADAR_WEB_PORT 3000 web 在宿主机上的端口(第二个实例要用自己的端口,见 multiple-instances.zh.md)。doctor 会跟随这两个变量。
JOB_RADAR_SESSION_SECRET 自动生成 签名会话 cookie 的密钥。不设时,首次使用会把 32 个随机字节写进 data/config/.web-session-secret(权限 0600)。只有要在多台机器间共享会话时才设(至少 32 个字符)。
JOB_RADAR_WEB_LOCAL_LOGIN 空 1:本机发出的请求也要口令(见下文)。web 能从本机以外访问时推荐设置。

设了口令之后:

  • 没有会话的访客打开页面会跳到 /login,登录后回到原页面。在此之前其他请求(API、表单、后台请求)一律返回 401。/api/health 始终开放,供健康检查使用。
  • 会话有效期为 ui.yaml: web_access.session_days(30 天);cookie 为 HttpOnly、SameSite=Lax。经 https 到达时(tailscale serve 就是 https)cookie 名为 __Host-jr_session 并带 Secure(浏览器只经 https 把它发给这一个主机);明文 http 下名为 jr_session。这个 cookie 名出现之前经 https 建立的会话不会沿用:升级到那个版本后,经 https 访问的每台设备都要再登录一次。退出登录按钮在侧栏底部(手机上在菜单里)。
  • 结束所有会话(手机丢了、用过别人的浏览器):设置 → 已登录的浏览器 → 退出所有设备会一次让所有浏览器退出登录,包括当前这个。它把 data/config/.web-session-generation 里的计数加一;之前发出的会话从此都被拒绝,其他不变。改口令也会让所有会话失效;删掉 data/config/.web-session-secret(或改 JOB_RADAR_SESSION_SECRET)再 docker compose up -d web 也一样。
  • 失败的登录在校验口令之前就会被限制:同一来源在 login_window_minutes(15)分钟内失败 web_access.login_max_failures(5)次,锁定 login_lockout_minutes(15)分钟。来源取 X-Forwarded-For 的最后一个地址,即最近一跳看到的地址(tailscale serve 会把它设成设备的 tailnet 地址)。窗口内所有来源合计失败达到 global_max_failures(30)次后,之后每失败一次,所有登录都要等待:先等 global_delay_seconds(2)秒,每次翻倍,最多 login_lockout_minutes。仍持有有效会话的浏览器不受这个等待影响。同时最多进行 max_concurrent_logins(4)次校验(每次会短暂占用约 128 MB 内存)。web 日志会记下每次锁定和每次开始等待,但从不记录口令或地址。min_password_length(10)是 pnpm web:password 接受的最短口令。这些键受保护:在 data/config/ui.yaml 里手动改。
  • 从别的机器经明文 http 登录时,登录页会显示警告:口令会不加密地经过网络。请用 tailscale serve(https)或 https 反向代理。
  • 读不懂的哈希(复制不全、旧格式)不会关闭登录:谁都登录不了,登录页提示“现在无法登录”,doctor 的 web.access 检查失败。重新运行 pnpm web:password。

**本机访问免口令。**请求点名本机(Host 和 X-Forwarded-Host 是 localhost、127.0.0.1 或 [::1])、转发的客户端地址是回环地址(在 compose 里私有地址也算:那是 Docker 网关)、不是经代理以 https 进来的、并且不带任何只有代理才会加的请求头时,可以跳过登录,所以本机上的浏览器不需要口令。这些请求头客户端都能自己伪造,所以只有 JOB_RADAR_WEB_BIND 设成回环地址、也就是只有本机能连上时,这项豁免才开启:

  • compose 总会设置它(不改就是 127.0.0.1),并把端口发布在那里;
  • pnpm start 和 pnpm dev 让服务器监听 JOB_RADAR_WEB_BIND(传了 -H <地址> 时用那个地址)。两者都没有时监听所有网卡,这时每个请求都要口令,http://localhost:3000 也一样。要保留豁免,在 .env 里设 JOB_RADAR_WEB_BIND=127.0.0.1;设了口令、web 不在 compose 里运行且这个变量没设时,pnpm run doctor 会给出警告(web.access);
  • 这里不支持直接运行 next start:它不理会 JOB_RADAR_WEB_BIND,监听所有网卡,.env 里写着 JOB_RADAR_WEB_BIND=127.0.0.1 时网络上任何人都能自称 localhost。请用 pnpm start。它通过开发依赖 tsx 启动 Next:要在用普通 pnpm install 安装的检出里运行,不能是 pnpm install --prod。

只要不是回环地址,豁免就关闭。

**规则:JOB_RADAR_WEB_BIND 必须写 web 实际发布的地址。**web 只通过它知道自己能被网络访问。用别的方式发布(改 compose.yaml 的 ports:、compose override、端口转发)而这个变量还是没设或 127.0.0.1,本机免登录就仍然开着;doctor 的 compose.web 检查会失败并提示“本机免登录仍然开着”。把 JOB_RADAR_WEB_BIND 设成发布的地址(并设口令)。

剩余风险:本机上任何把连接转给 web 的东西,在 web 看来都是本机。包括同一 compose 网络里的其他容器、单纯的 TCP 转发(tailscale serve --tcp、socat、ssh -R、路由器或 VPN 转发到这台机器的端口),以及本机上被你配置成发送 Host: 127.0.0.1 且不带转发头的反向代理。正常 https 模式的 tailscale serve 是安全的:它会加转发头,经它进来的访客总要口令。web 能从本机以外的任何地方访问时,设 JOB_RADAR_WEB_LOCAL_LOGIN=1:所有请求都要口令,宿主机上也登录一次即可。

没设口令时,只要面板是从别的机器打开的(或监听了回环以外的地址),每页顶部都会有红色横幅提示,doctor 的 web.access 检查也会给出警告。

web 和 runner 以 0600(仅属主)写配置文件:data/config/*.yaml 里可能有 token,data/profile/ 里是你的个人画像。Linux 上容器以 PUID:PGID(默认 1000:1000)运行,这个用户必须拥有 data/;见 host-setup.zh.md。macOS 上的 Docker Desktop 会自己映射属主。

两个浏览器标签页(或手机和电脑)同时编辑同一个设置表单或个人画像时,第二次保存会被拒绝(文件已在别处改过),表单显示已保存的值,而不是覆盖第一次的保存。web 每次保存配置文件,还会把之前的文本留在 data/config/.backup/editor/(高级编辑器的备份,每个文件保留 ui.yaml: config_editor.backups_keep 份)。

  • 每份配置可以在顶层写 config_version: <整数>,不写 = 1。
  • 代码对每份文件有一个当前版本(迁移步骤 src/lib/config/migrations/<文件>/NNN-*.ts 最后一步的 to)。
  • 文件比代码旧:读取时先在内存里迁移再校验,照常工作;doctor 提示运行 pnpm config:migrate --write 把迁移写回(注释保留,原文件进 .backup/)。
  • 文件比代码新(代码回退了):明确报错(ConfigError,doctor ❌),不会猜。升级代码,或从 data/config/.backup/<时间>/ 拿回迁移前的文件。
  • 默认目录的文件随产品发布,永远是当前版本(config:check 检查)。

写迁移(开发者):在 src/lib/config/migrations/<文件名>/NNN-slug.ts 导出一个 { from, to, description?, migrate(doc) }(to = from + 1,migrate 原地改 yaml 的 Document,保留注释,不做 I/O),在 migrations/index.ts 的 CONFIG_MIGRATIONS 里按顺序登记,同时把默认目录里那份文件改成新格式并写上新的 config_version。契约见 src/lib/config/migrations/types.ts。

关注岗位复查(pipeline.yaml + ui.yaml)

Section titled “关注岗位复查(pipeline.yaml + ui.yaml)”

维护步骤会复查收藏和跟进中岗位的原帖页面(见 usage.zh.md“已关闭的岗位”)。相关的键在两个文件里;两段都可以缺省(旧文件),缺省时用下面的默认值。

文件 键 默认 含义
pipeline.yaml(shipped) lifecycle.watch_recheck.interval_hours 72 同一个关注岗位最多每这么多小时复查一次
lifecycle.watch_recheck.max_per_cycle 10 每轮最多复查几个岗位
lifecycle.watch_recheck.per_host_min_interval_ms 3000 对同一网站两次请求的最小间隔(含 robots.txt)
lifecycle.watch_recheck.timeout_ms 10000 单次请求超时
lifecycle.watch_recheck.max_body_bytes 1048576 页面最多读这么多字节,其余不下载
lifecycle.watch_recheck.user_agent JobRadar/2 (self-hosted job tracker; link check) 复查和“可能已关闭”链接检查都带它;第一个词是 robots.txt 里匹配的产品名
ui.yaml(user) job_checks.closed_recheck.enabled true 关掉后不再复查(“可能已关闭”的链接检查照常)
job_checks.closed_recheck.extra_phrases [] 你自己的短语(正则,不区分大小写),与市场包的 closed_posting.phrases 合并。只接受安全的子集:文字、字符类、多选 (a|b)、\b、可选部分 ?(最多 8 个)和最多 5 次的 {n,m},长度不超过 200;不允许 *、+、{n,} 和反向引用。写错或不安全的正则 pnpm config:check 会报出路径。只搜页面可见文字的前 200,000 个字符,每页最多 250 毫秒(超时的页面记为“未查”)

ui.yaml 这一段在 YAML 或高级编辑器里改(没有表单)。旧版本不认识它:回退前先删掉。

每一份备份,不管是 runner 每天做的(backup.config_archive,默认开)还是 scripts/ops/backup.sh 做的,都会在数据库 dump 旁边留一份 ….config.tgz(data/config + data/profile,不含 .backup/ 和登录密钥 .web-session-secret)。schedule.yaml 的 backup 段还有 pre_migrate(数据库迁移前先备份)、keep_system(每一类安全备份保留几份)和 keep_old_databases(恢复时被替换的旧库保留几个)。.env 和 .auth/ 不在任何备份里。见 backup-restore.zh.md,包括“恢复配置”。

旧版本的线上配置就是 git 里的 config/。升级到分层配置时,scripts/ops/deploy.sh 第 4 步看到 data/config/ 是空的,pnpm config:init 就把当时 config/ 里的全部 user 文件复制过去(此时默认配置就是旧线上配置,和 pnpm config:init --from config 结果相同)。之后网页只写 data/config/,config/ 回到“产品默认值”的角色,不会再让 checkout 变脏。详见 deploy.zh.md 第 4、5 节。

本页同步自产品库 Joevonlong/job-radar@2947f47 的 docs/。Job Radar 与任何招聘网站均无关联。代码采用 AGPL-3.0-only 许可。