# 复制为 .env 后填入真实值:cp .env.example .env # .env 已被 .gitignore 排除,不会进仓库。 # 也可以直接用环境变量传入 —— 环境变量优先级高于 .env。 # ── 邮箱源选择(可插拔,见 src/mailbox.py)──────────────────────── # worker(默认)| imap | chatai | remail。 # **默认值刻意保持 worker** ⇒ 行为与加这个开关之前完全一致。 # # 为什么要可插拔:整链有两道**互相独立**的坎,早期没有任何一个邮箱源同时过得了 —— # · 本项目 CF Worker 自有域名:过不了域名门(A0232),但读信是现成的 # · gmail / outlook / qq / 定制域名:过得去域名门,但读信要 app password # · chatai 读信页(供应商 outlook 账号池):**两道都过**,但账号会过期 # 前两条各解决一半;第三条是当前唯一跑得通的,代价是"探活后再领用"。 # 背景与实测数据见 docs/protocol.md「两道坎互相独立」。 # IR_MAILBOX_KIND=worker # ── CF Worker 临时邮箱(MAILBOX_KIND=worker 时必填)─────────────── # Worker 的 Admin Token。缺失时 run.py 会在启动阶段直接报错并提示本文件。 IR_WORKER_ADMIN_TOKEN= # ── 其余项有实测默认值,通常不用改 ─────────────────────────────── # IR_WORKER_BASE=https://..workers.dev # IR_WORKER_DOMAIN= # # ── IMAP 邮箱源(MAILBOX_KIND=imap 时必填凭据)──────────────────── # 凭据文件每行 `邮箱----密码`(第三段「恢复邮箱」可有可无)。 # 🔴 这个文件**含明文密码,不进仓库**:放在 .workbuddy-ai/ 下,或用绝对路径 # 指向仓库外(.gitignore 对仓库内任意路径都拦不住"你手动 add")。 # ⚠ 路径写错会在**启动阶段**报出来,不会跑到一半才炸(config.validate)。 # IR_IMAP_CREDENTIALS= # # 下面几项有可用默认值(Gmail 的 IMAP 入口固定是 imap.gmail.com:993/SSL)。 # ⚠ 供应商给的是"定制域名邮箱"时,其邮件多半也托管在 Google # (MX 指向 smtp.google.com),此时默认值照样对 —— 换个域名而已。 # IR_IMAP_HOST=imap.gmail.com # IR_IMAP_PORT=993 # IR_IMAP_FOLDER=INBOX # IR_IMAP_TIMEOUT=30 # # 🔴 IMAP 的密码**必须是 Google app password**(固定 16 位纯小写字母)。 # 拿账号常规密码去登会得到 `[ALERT] Invalid credentials`; # 账号开了两步验证时会得到 `[ALERT] Application-specific password required` # —— 后者说明**密码是对的**,只差 app password。两者别混为一谈。 # ⚠ 「已领用」状态文件的路径覆盖(默认 .workbuddy-ai/state/imap_used.json)。 # 它记的是"哪些地址已经被领过",**跨运行保留** —— 删了会从池头重新领。 # IR_IMAP_USED_STATE= # # ── chatai.codes 读信页(MAILBOX_KIND=chatai 时必填账号文件)────── # 供应商给的 outlook 账号池,每行 4 段: # 邮箱----密码----clientId----refreshToken # 🔴 同样**含凭据,不进仓库**:放 .workbuddy-ai/ 下,或用绝对路径指向仓库外。 # ⚠ 路径写错会在**启动阶段**报出来(config.validate),不会跑到一半才炸。 # IR_CHATAI_ACCOUNTS= # # 读信页地址。公开服务、不含凭据,所以有默认值。 # IR_CHATAI_BASE=https://mail.chatai.codes # # 单次读信请求超时(秒)。比 REQUEST_TIMEOUT 长得多:读信页要替我们拿 # Microsoft access_token 再拉 Graph,首次(含 refresh_token 交换)可能 10s 以上。 # IR_CHATAI_TIMEOUT=60 # # ⚠ 「已领用 / 已失效」状态文件的路径覆盖 # (默认 .workbuddy-ai/state/chatai_used.json)。它记两样东西: # · `used` = 已领出的地址(跨运行保留;删了会从池头重新领) # · `dead` = refreshToken 已失效的地址(**negative cache**) # 🔴 删掉它 = 下一轮把整池死号重探一遍(实测 31 条约 1 分钟)。 # IR_CHATAI_STATE= # # ── Remail 接码平台(IR_MAILBOX_KIND=remail 时必填 API Key)──────── # 平台:https://remail.aishop6.com 。按**订单**买邮箱,取件走 /v1/pickup。 # 🔴 权威文档是 /openapi.json(OpenAPI 3.0.3)—— /docs 只是 814 B 的 SPA 空壳。 # 🔴 必须指向**带 OpenXLab 邮件规则的私有项目**:默认 170 = OpenXLab # (accessType=private,mailRuleCount=4,platform=sso.openxlab.org.cn)。 # 用公共项目(chatgpt / sub2api 等)下单时 /v1/pickup 恒 items=[]。 # **凭据不进仓库**,默认空、由 validate() 在启动阶段报错。 # IR_REMAIL_API_KEY=rk-... # # ⚠ 下面几项有实测默认值,通常不用改: # IR_REMAIL_PROJECT_ID=170 # OpenXLab 私有项目 # IR_REMAIL_SUFFIX=outlook.com # 实测过 OpenXLab 域名门 # IR_REMAIL_SUPPLY=private_first # 服务端默认;别改回 public_only # IR_REMAIL_SERVICE_MODE=purchase # 长效购买(10 积分/个,60min 收信窗口) # IR_REMAIL_BASE=https://remail.aishop6.com # IR_REMAIL_TIMEOUT=60 # # IR_CHROME_PATH=C:\Program Files\Google\Chrome\Application\chrome.exe # IR_CHAT_API_BASE=https://discovery-api.intern-ai.org.cn/v1 # # 注册时用的应用身份(来自 discovery 活动页跳转链接)。 # 分类:**公开 app id,不是秘密** —— 每次请求都出现在 URL / 请求体里, # 目标站本来就看得见,也不授予额外权限,所以代码里有默认值。 # 只有换活动 / 换站点时才需要在这里覆盖。 # IR_CLIENT_ID= # IR_SOURCE= # ── 行为旋钮(改动前先读 README「可调环境变量」)───────────────── # ⚠ 这几项都验证过"调了不会更快",别当成性能旋钮: # IR_MICRO_BUDGET=45 # 微移动预算(秒)。调小会稳定落到更慢的验证码通路。 # IR_TYPE_DELAY_LO=45 # 逐字输入按键间隔下限(毫秒) # IR_TYPE_DELAY_HI=110 # 逐字输入按键间隔上限(毫秒) # IR_NO_MICRO_MOVE=1 # 关闭微移动 —— 仅对照实验用,生产环境勿开 # IR_PREWARM_MS=0 # goto 后闲置 N 毫秒再操作 —— 仅对照实验用 # # 🔴 上面这 **5 个**键都由 `src/browser/constants.py` 在**导入时**用 `os.getenv` # 定值。真要在 `.env` 里启用它们,必须先确认 `import common.config` 早于 # `src.browser` —— 否则 `.env` 还没进 `os.environ`,这 5 个 `os.getenv` # 会**静默**取默认值(不报错、不告警,只是配置不生效)。 # 生产路径(run.py / src/pipeline.py)的导入顺序是对的;单独 # `import src.browser.constants` 没有这个保护。详见该模块 docstring。 # ── 注册配额保护(本地累计计数)────────────────────────────────── # `register/byEmail` 有**两层**限制:瞬时速率(由代码里的 REG_MIN_INTERVAL # 闸门控制)+ **累计配额**(约 40 个后 B0000,闸门完全无效)。 # 下面两项是第二层的本地保护:接近上限就主动停,而不是撞上去。 # # ⚠ 这是本地保护,不是权威计量 —— 换机器 / 删 state 文件都会重置。 # 实测恢复窗口 **> 8.6h**(14:05 最后成功 → 22:39 仍 B0000), # 所以窗口取 24h 作保守上界。封禁是 **IP 维度**:换发信域名无效(已实测)。 # IR_REG_QUOTA_MAX=40 # 滚动窗口内的成功注册上限 # IR_REG_QUOTA_WINDOW_H=24 # 滚动窗口长度(小时) # IR_QUOTA_STATE= # 计数文件路径覆盖(默认 .workbuddy-ai/state/register_quota.jsonl) # # 确信服务端已恢复但本地计数没滑出窗口时:run.py 加 --ignore-quota # ── 出口代理(换 IP 绕过 IP 维度的注册封禁)─────────────────────── # 注册封禁是 **IP 维度**,所以换出口 IP 是对症的解法。 # 实测本机默认出口是**机房** IP(NTT / 天风通信,洛杉矶),信誉差 —— # 换成住宅 IP 才有意义。 # # 格式(与常见代理商控制台一致): # IR_PROXY=host:port:user:pass # 单条 # IR_PROXY=http://user:pass@host:port # 也接受完整 URL # IR_PROXY=socks5://user:pass@host:port # socks5 需先装 PySocks # # 只作用于**被封的那一侧**(sso / discovery)。邮箱 Worker 默认直连 —— # 收信轮询是整链瓶颈,绕道代理只会更慢,而 Cloudflare 不关心我们的出口 IP。 # 确实要让邮箱也走代理时:IR_PROXY_MAIL=1 # # 🔴 选代理前**先跑探测器**,别拿通用站点测: # python tools/probes/probe_proxy.py host:port:user:pass # 实测踩过的三个坑(细节见该工具 docstring): # 1. TCP 连通性完全不能作判据 —— 本机有 Clash TUN,连美国 IP 也会 # 显示 0.02s "连通"(其实是连到了本地虚拟网卡)。 # 2. 别只看状态码 —— 代理的域名 ACL 拒绝时返回 403,正文才是判据: # `errorMsg: :80 not accessible` # 3. 必须用**真实目标域名**试 —— 有代理商能通 google/baidu, # 却精确屏蔽了 `openxlab.org.cn` 和 `intern-ai.org.cn`。 # IR_PROXY= # IR_PROXY_MAIL= # ── 槽位代理池(规模化:一次注册多个账号,每个走不同出口 IP)──────── # 🔴 为什么需要它:上面那条 `IR_PROXY` 是**一条**出口 —— 整个批次共用同一个 # IP,`workers` 调到几都一样会撞上 IP 维度累计配额(`B0000`)。 # 真正的约束是"**有多少个不同出口 IP**",不是本地并发度。 # # 设计参考 `asz798838958/aBaiFreeGPT`:mihomo 里一个节点 = 一条 listener = # 一个本地端口 = 一个固定出口 IP;程序侧按**租约**把端口分配给 worker。 # # 三步起池(都在本机跑,不需要额外服务): # # 1) 生成槽位配置(会顺带写出 slots.txt): # python tools/ops/gen_mihomo_slots.py --sub <订阅名> --slots 6 --filter 美国 # → .workbuddy-ai/proxypool/config.yaml + slots.txt # # 2) 起独立 mihomo 实例(不要动 Clash Verge —— 它把 TCP 控制器关了, # 而且改它的配置会影响你上网): # "<你的 mihomo 可执行文件>" \ # -d .workbuddy-ai/proxypool -f .workbuddy-ai/proxypool/config.yaml # # 用 `proxypool_ctl.py start` 的话,内核路径从这里读(别写进源码 —— # 绝对路径会暴露本机目录结构,而且每个人装的位置不同): # IR_MIHOMO_EXE=<你的 mihomo 可执行文件绝对路径> # # 3) 先探测再跑(**必做**,见下): # python tools/probes/probe_slots.py # # 🔴 **不同节点名 ≠ 不同出口 IP**。实测 6 个槽位(6 个不同节点名)只得到 # **4 个**不同出口 IP —— 同机房的多台机器常常共用同一个 NAT 出口。 # `probe_slots.py` 会把去重后的真实出口数报出来,**那个数才是并发上限**。 # 配 6 个槽位就往 workers=6 冲,只会让多出来的槽位在同一个出口上排队, # 反而更快撞穿那个 IP 的配额。 # # 槽位清单来源(文件优先;槽位多时用文件,别把 50 个 URL 塞进环境变量): # IR_PROXY_SLOTS_FILE=.workbuddy-ai/proxypool/slots.txt # IR_PROXY_SLOTS=http://127.0.0.1:7901,http://127.0.0.1:7902 # # 一个槽位被判"这个出口被目标站点封了"后,冷却多久才重新启用(秒)。 # 默认 120,与参考实现的 MIHOMO_NODE_COOLDOWN_SECONDS 同值。 # 🔴 冷却而不是永久拉黑:永久拉黑会让池子越跑越小,最后全不可用。 # ⚠ 这是**基础**冷却:同一槽位反复被封时按 2 的幂退避(见下一条)。 # IR_PROXY_COOLDOWN=120 # # 🔴 反复被封时冷却退避的**封顶**(秒),默认 21600(6 小时)。 # # 为什么需要退避:基础冷却 120s,而服务端配额窗口是 **24h** —— 差 720 倍。 # 而封禁**不写**配额台账(只有注册成功才记),于是 120s 后本地计数没涨、 # 运行中的配额过滤器照样放行 ⇒ 这个已被封的出口被重新租出去,再打一次 # 注定失败的请求 —— **每 120s 一次,一天约 720 次**,每次都在加深封禁。 # # 退避序列(base=120 / cap=21600): # 120 → 240 → 480 → 960 → 1920 → 3840 → 7680 → 15360 → 21600(封顶) # 即"一天 720 次"压到"一天 6 次左右"。 # # ⚠ 封顶取 6h 而不是 24h:这是没有证据时的猜测,不能猜得太激进。 # 取 24h 等于"封一次就整天不用它",而单次 B0000 可能是服务端抖动。 # IR_PROXY_COOLDOWN_MAX=21600 # # 全池都在冷却时,一个任务最多等多久(秒),超时按失败记账。 # 🔴 别设 0(全冷却瞬间变一片假失败),也别设无穷(批量会永远挂着看不出卡死)。 # IR_PROXY_SLOT_TIMEOUT=240 # # 起飞前是否做槽位端口连通检查(1 开 / 0 关,默认开)。 # 🔴 为什么默认开:槽位实例是**独立的前台进程**,很容易"配置还留着、进程已经 # 没了"。那种状态下每条注册记录都会以**代理连接错误**收场,而在这个项目里 # "注册全失败"最容易被误读成"换 IP 也不行 / 还在封" —— 结论完全错。 # ⚠ 全部端口都连不上时**直接拒绝开跑**(不是静默退回单代理),就是这个理由。 # IR_PROXY_PREFLIGHT=1 # # 池子状态文件路径覆盖(默认 .workbuddy-ai/state/proxypool.json)。 # 🔴 它存的是"哪些出口在冷却、被封过几次",**跨运行保留** —— 别删它, # 删了退避序列会从第一档重来(症状:明明封过很多次的出口又被立刻租出去)。 # 不含账密(键用 `host:port`),但仍属运行态,落在被 .gitignore 排除的目录下。 # 与 `IR_QUOTA_STATE` 不同,这个键在**调用时**读,不受导入顺序影响。 # IR_PROXY_STATE= # # 解盾 cookie(`acw_sc__v2`)的持久化位置覆盖(默认 .workbuddy-ai/state/waf_cookies.json)。 # 🔴 它是**凭据**(能在对应出口上换取 WAF 放行),只是 WAF 的 cookie、不是账号 # 凭据;但仍然落在被 .gitignore 排除的目录下,绝不进仓库。 # 跨 `run.py` 进程复用 ⇒ 同一出口不必每批重解一次盾(实测解盾单价 ~8.8s, # 一批 12 账号固定要 5 次,约 44s/批)。 # cookie 失效时服务端照常回挑战页,会重解并覆盖 —— **自愈,不需要手工清理**。 # IR_WAF_STATE= # # 槽位端口 -> 出口 IP。格式:`端口=IP`,逗号分隔。 # 🔴 配额记账**必须按出口 IP**,不能按槽位位置号 —— 位置号会随 slots.txt # 增删条目整体平移,既有记录会静默错配到别的 IP 头上(实测踩过)。 # 🔴 这张表现在还承担第二个作用:**同出口 IP 互斥**。填了它之后,池子会 # 保证"同一时刻一个出口 IP 只有一个租约在外"。不填则退回按槽位分配, # 同 IP 的两个槽位可能被同时租出(服务端看到同一 IP 上 2 路并发)。 # ⚠ 因此**填了这张表,并发上限就变成"不同出口 IP 的个数",不是槽位数** —— # 实测 6 个槽位只对应 4 个出口,所以并发上限是 4。`describe()` 会打印出来。 # 🔴 这张表**不进仓库**:出口 IP 属于基础设施标识,公开可被搜索, # 节点容易被盯上。所以只在 .env 里配。 # 🔴 换订阅/换节点/换机房后必须重测并重填: # python tools/probes/probe_slots.py # 量出真实出口 IP # python tools/data/migrate_quota_scope.py --apply # 迁移台账(旧记录挂在旧 IP 名下) # 🔴 端口漏配会**直接报错拒绝开跑**(不退回位置号)—— 静默错配会让某个 IP # 偷偷超限、真被目标站封,比跑不起来危险得多。 # IR_SLOT_EGRESS_IPS=7901=203.0.113.7,7902=203.0.113.8 # 🔴 槽位模式下**本地配额计数按出口分开算**(src/quota.py 的 scope)。 # 老记录(没有 scope 字段)属于换出口之前的那个 IP,不会拿来拦新出口 —— # 所以配了槽位池之后,`本地配额:53/40 已用尽` 这类提示可以直接忽略。