Long-running website monitoring: new cloud/resident.py layer on top of the automations claim/quota model (no task state-machine change). States running/paused/ended; pause preserves the content-hash cursor and stops polling, resume re-checks from the cursor, end is terminal (resume after end is 409, a new watch is required). Polling is a cheap fetch + hash comparison; only a real change creates a queued model task. Quota exhaustion pauses (quota_exceeded) instead of ending and keeps the cursor. Driver is the worker run_loop round boundary; server exposes create/list/get/update/pause/resume/end with audit. Console UI intentionally not wired this round (see verification summary for deviations).
10 KiB
10 KiB
常驻监听任务(resident watch)验证摘要
基底与交付物(代码就绪,local only)
- 工作区:
~/workspace/codex-app-server-resident-task - 分支:
feat-resident-task,基底origin/main=622c51e(Merge PR #274,2026-11-02 fetch 到的头,与任务书一致)。 - 主 checkout(
~/workspace/codex-app-server,feat-repo-picker,有脏文件)与其它现存 worktree 一律未动。 - 本轮提交前状态:
cloud/server.py+187 行、cloud/worker.py+31 行,新增cloud/resident.py、cloud/tests/test_resident.py、cloud/tests/test_resident_server.py、本摘要。 - 未做:未 push、未合并到 main、未跑
deploy.py、未部署、未建生产任务、未动生产/主机 env/系统 key/sub2api/生产网关/仓库可见性/GitHub App 权限、未杀生产容器。
基底核实(先调查结论:main 上常驻监听不存在,不是重复建设)
- 任务状态机仍是
queued -> running -> completed / failed / interrupted / merged / merge-failed;/pause只是把进行中回合任务标interrupted并置paused_by_user=1(cloud/server.py_pause_task),不是可续跑的常驻暂停态——本实现不复用它,另建常驻态。 - 云端自动化(
cloud/automations.py)定义层只有active / cancelled,含automations / automation_runs表与run_due_automation的 claim 抢单、配额门;服务端本身不起定时器,到期触发靠外部触发器。本实现复用它的 claim 口径与配额函数,不另起冲突语义。 - goal 循环(
host.pyrun_goal_loop,max_rounds=0不限轮次)结束条件是模型宣告达成,不是“一直监听外部变化”;定时自动化每次到期建一个新任务跑一轮就结束。两者都不是常驻监听,本摘要不把它们说成已实现。 - main 上与暂停相关的已有机制(MCP
pause_task工具、长任务中断自动恢复)针对的都是回合任务,不是常驻监听;greppause|resident在cloud/*.py与host.py中未发现常驻监听实现。
设计抉择
- 新层不改任务表:
cloud/resident.py(标准库 + 复用cloud.automations的_audit_row、_automation_quota_exceeded)新增resident_watches/resident_polls两张表,避免改动任务状态机牵连 worker 认领、sweep、自动恢复。 - 状态机:
running -> paused -> running;running / paused -> ended(终态)。ended后 pause / resume / end / update 一律 409,恢复已结束的监听必须新建任务(文档与测试双锁定,见下)。 - 暂停保留现场:暂停只改
status / paused_reason / next_poll_at,last_content_hash / last_poll_at / last_change_at / last_change_text不动;暂停期间due=False,驱动不轮询。恢复时next_poll_at=now立刻复查,用旧游标比对——暂停期间的变化在恢复后第一次轮询即报出,不丢。 - 驱动(明说):服务端本身不起定时器,与自动化同款结论。实际驱动是
cloud/worker.py的run_loop轮次边界调process_due_resident_watches()(新增方法,异常只记日志、不炸循环;self.resident_fetcher可注入假抓取供测试)。任何外部触发器也可直接调run_due_resident_watch / process_due_resident_watches,两者都走 claim 门。 - 防重复轮询:
claim_due_watch条件 UPDATE(status='running'且next_poll_at <= now),抢到才把last_poll_at / last_claimed_at写成 now 并预置next_poll_at=now+interval(崩在抓取中途时下个间隔仍重试,不因认领锁死永久停摆;完成后按成败重算)。 - 变化才花钱:轮询 = 廉价抓取 + 空白折叠后 sha256 比对,不跑模型。
baseline(首次,只建游标)、unchanged(不建任务)、changed(才建一个 queued 任务,prompt = 用户处理提示 + 变化摘录)三种产出都记resident_polls,可查。 - 配额边界:配额门只在“变化要建模型任务”那一刻过(复用自动化同款
_automation_quota_exceeded)。耗尽时不结束、不丢变化:转paused(paused_reason='quota_exceeded')、游标不推进、记changed_quota_paused;配额恢复后恢复,同一变化仍能报出。配额检查本身失败时按瞬时失败退避,不转暂停,游标同样不推进。 - 安全边界:间隔下限 60 秒(与自动化同款)、上限 30 天;抓取超时 1–60 秒(默认 10 秒)、读满 1MB 即断;失败退避 30 / 60 / 120……秒、上限 3600 秒;目标 URL 仅 http/https、无 userinfo、字面 localhost/私有/回环/链路本地 IP 拒绝。
- API(
cloud/server.py,权限与自动化同款:写create_task、读read,全部记审计):POST /projects/{id}/resident-watches(name / target_url / prompt 必填;interval_seconds 默认 3600;fetch_timeout_seconds 可选;创建不走配额门)GET /projects/{id}/resident-watches(附due_count)、GET /projects/{id}/resident-watches/{wid}(附最近轮询polls)PATCH /projects/{id}/resident-watches/{wid}(改名/提示/目标/间隔/超时;ended 409;改间隔不立即触发,下次轮询完成后按新间隔重算)POST /projects/{id}/resident-watches/{wid}/pause|resume|end- 状态查询返回常驻态、游标(
last_content_hash)、最后变化(last_change_at / last_change_text)、下次轮询(next_poll_at)、失败次数与暂停原因。
验收清单逐项结果
- 能新建常驻任务(目标 URL、轮询间隔、变化处理提示):✅ 模块与服务端测试覆盖;非空壳——新建即到期,驱动下一轮即可认领。
- 三态清晰(运行中/已暂停/已结束终态):✅ 状态机与 HTTP 409 语义双覆盖。
- 暂停保留现场、暂停期间停轮询、恢复从游标续跑:✅ 测试
test_pause_preserves_cursor_and_resume_reports_pause_period_change(暂停期间变了,恢复后第一次轮询即报出)。 - 结束终态、结束后不再静默重启、结束后恢复被拒绝:✅
ended后 due 查询只认running,pause / resume / end / update 一律 409(服务端错误文本含 terminal 并提示新建);测试覆盖。 - 真正有驱动、claim 防重复、心跳与 stale 回收:✅ 驱动为 worker
run_loop轮次边界(不是服务端定时器,明说);claim 单胜者测试覆盖;stale 口径为“认领预置 next_poll_at,崩后下个间隔重试”+ worker 既有每轮心跳/sweep(常驻监听本身不另起心跳表,见已知偏差)。 - 变化才花钱、每次轮询与变化可查:✅ baseline / unchanged 不建任务、changed 才建任务;
resident_polls逐轮记录。 - 间隔下限、抓取超时、失败退避、除配额耗尽外不擅自结束、配额耗尽只转可恢复暂停并明示原因:✅ 全部测试覆盖;配额耗尽测试断言状态为
paused且明确!= ended。 - 权限与审计沿用任务/自动化同款、pause / resume / end 记审计:✅ 服务端测试断言
resident_watch_created / paused / resumed / ended审计行;跨项目行一律 404(不泄露存在性);局外人写操作被拒。 - API 含暂停、恢复、结束与状态查询:✅ 见上;控制台 UI 本轮未接,列为已知偏差,不写成已完成。
真实测试数字
- 定向(改动后):
python3 -m unittest cloud.tests.test_resident cloud.tests.test_resident_server—— 13 项(模块 10 + 服务端 3),全部通过。 - 全量(CI 同款三条 discover,改动后、仅最后一次文案注释修订前跑出;该修订只改
update_watch的 docstring,不改任何代码逻辑,修订后定向 13 项与自动化相关 31 项重跑全部通过):discover -s tests:105 项,0 失败,0 跳过。discover -s cloud/tests:1544 项,0 失败,9 跳过。discover -s deploy/tests:44 项(单独重跑确认),0 失败。- 合计 1693 项、0 失败、9 跳过。失败未靠删断言、加 skip、改默认值蒙混;新增测试 13 项全部计入上述 cloud 套件。
- 新模块定向覆盖率(
coverage run --source=cloud.resident,仅定向 13 项):cloud/resident.py312 语句、85% 覆盖。未覆盖主因:默认真实 HTTP 抓取(default_fetch,单元测试一律注入假抓取,不联网)与个别异常分支(PG 方言补列、配额检查失败分支);本仓库 CI(.github/workflows/host-verify.yml)本身没有覆盖率门禁,故不报“覆盖率变化”——全量覆盖率基线未在本轮重算,不虚构数字。
已知偏差与未做事项
- 控制台 UI 未接:
cloud/console.html是打包产物,本轮没有常驻监听面板;用户现阶段只能走 API。接 UI 是后续一轮,不在本轮验收内。 - 变化判定 v1 较粗:空白折叠后全文 sha256,没有选择器/区域提取;动态页面(广告、时间戳、随机推荐)本身会抖动成“变化”并建任务。缓解在后续:加 CSS 选择器或正文提取口径、或变化阈值。
- SSRF 防护 v1 只挡字面 localhost/私有 IP,不做 DNS 解析后校验(解析后重定向到内网、DNS rebinding 不在覆盖内);
websearch.fetch_url的同款字面口径已对齐,但不要把 v1 当成完整 SSRF 方案。 - 常驻监听没有独立心跳表:stale 回收靠认领预置的
next_poll_at+ worker 既有心跳/sweep;驱动进程整体死亡时,已认领未完成的当次轮询要等到下个间隔才补跑(与自动化 claim 的同款取舍)。 - 未验证:真实外网抓取(单元测试不联网)、生产 PG 上的表迁移(
PRAGMA补列走自动化同款适配写法,但未在 PG 实测)、worker 常驻循环里的长时驱动(仅方法级测试,未跑真实run_loop长循环)、多 worker 并发抢单(仅条件 UPDATE 单胜者测试,未做双进程实测)。 - 未做:未 push、未合并、未部署;UI、选择器提取、DNS 级 SSRF、PG 实测、双进程抢单实测留待后续轮次。
Blocker
- 无本轮 blocker(全部工作均为本地代码与本地测试,未触及只有用户能给的账号/密钥/商户/商店账号/宿主/维护窗口,也未发生花真钱、删生产数据、改生产可见性的动作)。
- 上线前仍需用户侧决策(不是本轮 blocker,不擅自定):控制台 UI 是否随本批上、默认轮询间隔与抓取超时是否按本轮默认值(60 秒下限/10 秒超时)拍板为生产默认值、PG 实测与部署窗口。