Skip to content

Releases: lza6/Aggregate-to-2api

v23.3.3 — CI 长期 flake 根治 + 退役域名清理

Choose a tag to compare

@lza6 lza6 released this 17 Oct 00:46

发布日期:2026-10-17
基线:v23.3.2 (8ae5018)
性质:缺陷修复批(无破坏性变更、无需迁移)


一句话

根治了持续制造 CI 噪声的唯一来源(test_ip_blocklist 的 TOCTOU flake),
并清除了散布在代码/模板/落地页里指向已退役域名的引用。


修复 1:test_ip_blocklist 的 flake 根治(CI 噪声的唯一持续来源)

根因:store 把「是否已建表」记成布尔标志,而连接用的是另一个属性

IPBlocklistStore 用 self._initialized(布尔)判断表是否建过,
但 _get_conn() 连的是 self._path。两者不绑定 —— 只要 _path 被改而
_initialized 仍为 True,写入就会落到未建表的库上:

sqlite3.OperationalError: no such table: ip_blocklist

触发路径有两条,都与"有没有等后台任务"无关:

  1. tests/test_ip_blocklist.py 的 fixture 换 _path 做每用例隔离 ——
    但只要期间任何协程在旧路径上跑了 init_schema()(把标志置回 True),
    换库后的写入就会落到新空库;
  2. api/request_guard.py:438 的 _sync_blocklist_cache 当时走裸 loop.create_task
    —— 裸 task 不被 background._BACKGROUND_TASKS 持有,所以即使调用方
    await background.await_pending() 也看不见它,等也等不到。

本机确定性复现(非碰运气,报错与 CI junit 逐字一致):

await store._write_lock.acquire()               # 占住写锁
t = asyncio.create_task(store.add_or_update(...))
await asyncio.sleep(0)                          # 让它越过 init_schema、卡在锁上
store._path = <新空库>                           # 换库
store._write_lock.release()
await t    # → sqlite3.OperationalError: no such table: ip_blocklist

两种症状是同一缺陷的两个落点:时序稍差时表现为 database is locked。
CI junit 里两种都出现过 —— 此前一直被当作"随机网络问题"。

注:v23.3.3 的第一版修复(仅 await_pending 排空后台任务)未中根因 ——
CI 在该提交上仍复现 no such table。本版才是根治。

修法(在 store 内加固,而非依赖调用方守规矩)

  • _initialized(布尔)→ _initialized_path(路径快照):
    短路检查变成「当前 _path 是否正是已建表的那条路径」。路径一换,检查自然失效 → 重走建表。
  • init_schema() 返回已建表的路径,全部 8 个调用点改用它开连接
    (conn = await self._get_conn(path))——「检查/建表用的路径」与「随后读写用的路径」
    是同一次读取,任何时刻换 _path 都不会让写入落到未建表的库上。
    _get_conn(path=None) 仍兼容无参调用。
  • api/request_guard.py:438 裸 create_task → background.spawn
    (同文件 _record_auto_block_violation 早已改走 spawn,这是漏改的同类点);
    顺带修掉「裸 task 可被 GC 静默回收 → 封禁缓存不同步」。
  • api/background.py 新增 await_pending(timeout):该模块此前只有
    spawn / pending_count,缺"排空"入口(先例:sse_events.await_pending_sse_tasks)。
    仍在 fixture 中用于文件级隔离(防在途写把上个用例的临时库写脏),
    但现在不再是 no-such-table 的依赖项。
  • fixture 里 _initialized = False → _initialized_path = None
    (前者在加固后已不是受控属性,会被当成普通实例属性静默创建,隔离形同失效)。
  • 两处 docstring 改为描述实际所做的事。

回归覆盖

tests/test_ip_blocklist.py::TestPathSwitchRobustness(3 用例):
直接换库自动建表 / 在途写跨越换库(原 TOCTOU 时序)/ 切库数据互不串。
已做变异验证:把 _initialized_path == path 退回布尔语义 → 2/3 红
(第 3 个是防「修成始终用同一个库」的对照项,布尔语义下仍成立属预期)。

此前 v7.6 的 test_auto_block_after_repeated_429 只靠「显式先建表」绕开同一缺陷,
未根治;本次才是根因消除。


修复 2:清除指向已退役域名 imagefree.tingfengai.art 的行为性引用

权威依据在仓库自身 —— deploy/README.deploy.md:3-6 明写该域名属「旧腾讯云 Docker 部署」,
当前生产为 https://imagefree.hwhcie.bond(Azure + systemd);项目 CLAUDE.md 同述。
故这不是域名分叉待拍板,而是行为性引用指向了死域名。

位置 影响
api/geo_ip.py 生成的代理订阅链接指向已下线站点 —— 用户复制过去不可用(且原为硬编码)
landing/src/components/SectionCode.vue / SectionFaq.vue 落地页给用户看的 curl 示例用死域名
README.md 接入方的 curl / ANTHROPIC_BASE_URL 示例 → 照抄必失败
deploy/.env.production.example 生产 CORS 模板白名单到不再服务的域名
landing/index.html / sitemap.xml / robots.txt canonical / og / twitter / JSON-LD / 站点地图 → SEO 与分享卡片失效
frontend/src/pages/ApiGuide.tsx 无 window 时的回退 base
scripts/block_abuser.ps1 运维脚本默认 base → 打到死主机
deploy/docs/cloudflare-cdn.md CDN 指南(前置域名错)

顺带消除硬编码:新增 IF_PUBLIC_BASE_URL

IF_BASE_URL 是上游(imagefree.net),不能复用为"自身公网地址"。
新增 IF_PUBLIC_BASE_URL(默认取当前生产域名):

  • settings_core.py 加字段(带 validation_alias)+ 取址 helper(懒导入 config 避循环依赖、去尾斜杠、取配置失败有兜底)
  • deploy/.env.example / .env.production.example 同步
  • 契约测试自动把关:TestEnvExampleSync(双向一致)+ TestEnvMappingContract(无 alias 必登记)

有意不改的(历史记录)

landing/public/release/* 与 docs/releases/archive/*、docs/reports/*、docs/prd/*、
docs/research/security-audit-v23.md —— 其中「线上实锤(匿名 curl https://imagefree.tingfengai.art)」
等描述的是当时确实发生的验证。改写会伪造历史,故保持原样。


判定为「不做」的设计项(附理由)

项 判定
requests.status 无 CHECK 约束 不做:加约束需表重建(破坏性迁移),且当前无实际缺陷 —— 状态值全部由代码写入并有白名单校验。收益 < 风险
删任务无级联清理(base64 / 幂等键 / DLQ / 向量) 不做:① base64 孤儿由 clean_base64_files(TTL) 按 mtime 兜底;② 幂等键指向已删任务是唯一"用户可见"的,但 IF_IDEMPOTENCY_ENABLED 默认 False → 默认配置下不可能发生;③ 向量孤儿已由 /v1/gallery/similar 的过滤兜住。属设计权衡而非缺陷

验证

ruff check api/ tests/ scripts/          → All checks passed!
ruff format --check                      → 3 files already formatted
pytest tests/test_background_tasks.py    → 5 passed(新增)
pytest tests/test_ip_blocklist.py        → 31 passed(含新增 TestPathSwitchRobustness 3 用例)
跨文件组合(test_request_guard*.py ×4 + test_ip_blocklist + test_auth_ip)
                                         → 130 passed
pytest tests/test_config_validate.py     → 25 passed(新字段过双向契约)
pytest tests/test_gallery_dedupe.py      → 14 passed
pytest tests/test_version_consistency.py → 5 passed
sync_version.py --check                  → 全链一致 23.3.3(10 文件)
_public_base_url() 实跑                   → 默认生产域名;设 env 后正确覆盖且去尾斜杠
npx tsc -b                               → exit 0
cd landing && npm run build              → 成功
node scripts/e2e_mobile_smoke.cjs        → 9 passed / 0 failed(真实 chromium)
CI run 38986559340 (5851c69)             → ✅ success(6/6 job;前一个提交 6cd7ac9 为 failure)

针对本修复的专项压测(原始 TOCTOU 时序,非碰运气)

in_flight(占住写锁 → 换库 → 放行)       → 300/300 ok
direct(直接换库后写入)                   → 300/300 ok
concurrent_mid_switch(120 协程并发中途换库)→ 120/120 ok

升级说明

无破坏性变更。新增可选配置 IF_PUBLIC_BASE_URL(不设则用默认生产域名)。
版本全链 10 文件 + 双 dist 已同步至 23.3.3。

v23.3.2 — CI 首次全绿:flaky 根因根治 + 进度条降级覆盖修复

Choose a tag to compare

@lza6 lza6 released this 16 Oct 11:06

v23.3.2 — CI 首次全绿:flaky 根因根治 + 进度条降级覆盖修复

发布日期:2026-10-16
基线:v23.3.1 (cf663ac)
性质:缺陷修复批(无新功能、无破坏性变更、无需迁移)


一句话

CI 六个 job 全部通过 —— 这是本仓库自 2026-09-08 以来第一次完整跑绿
(此前连续 11 次 failure)。同时修掉一个用户可见缺陷(进度条每 2 秒被抹回「运行中」)。

CI run 38887120926 → success
[OK] 前端 E2E + 自动化审计(e2e-smoke + a11y + perf + resp)
[OK] 代码检查(ruff)
[OK] 前端版本一致性门禁
[OK] 前端管理面板门禁(tsc + vitest + build)   ← 此前长期为红
[OK] 测试 (3.11, ubuntu-latest)                  ← 此前长期为红
[OK] Docker 构建验证

修复 1(核心):前端 flaky 的根因

机制

isolate: false 让同一 worker 内的多个测试文件共享模块实例,
而 vi.mock 的假模块是按文件注册的 —— 后跑文件里被缓存的组件闭包
仍指向上一个文件遗留的 mock 实例,本文件新注册的 mock 对它完全无效。

本机强制两文件同 worker 连跑的实测(--no-file-parallelism):

顺序 结果
GalleryAlbum → Gallery Gallery.test.tsx 4 用例全红:Unable to find an element with the alt text: test prompt(DOM 里其实是 GalleryAlbum 的 2 项数据)
Gallery → GalleryAlbum GalleryAlbum 7 用例全红:Cannot read properties of undefined (reading 'total')

这两个文件是全仓库唯一「同一组件 + 同一 vi.mock 目标」的组合
(其余 26 文件不受影响:单 worker 全量实测 2 failed | 26 passed,失败仅此一对)。

这就是「同一 commit 时红时绿」的机制:文件→worker 的分配取决于哪个 worker 先空闲 ——
4 vCPU 的 runner 上两个 Gallery 文件偶尔落进同一 worker → 后跑者必红;
本机 16 vCPU 几乎不会同组,所以本地长期复现不到。

修法:isolate: false → true,配套 maxWorkers: 2

isolate: true 下每个文件都要重启 worker 环境,启动成本显著上升,
而 vitest worker 启动有 60s 内部硬超时、非 ASCII 路径(2api目录)下这段本就偏慢:

配置 结果
默认并发 + isolate:true 28 errors / 0 tests(全部 worker 启动超时,套件完全不可用)
maxWorkers:2 + isolate:true 28 files / 300 passed(488s,稳定可跑)

曾试过的替代方案「关掉 isolate」能让默认并发跑起来,但那正是引入跨文件 mock 串味的原因 ——
所以不关隔离,改为降低并发让 worker 能在超时前起来。CI(ubuntu-latest,4 vCPU)
并发更低,此值同样安全。


修复 2:进度条每 2 秒被轮询抹回「运行中」(用户可见)

useTaskProgress 的轮询每 tick 无条件 apply(mapStatusToDetail(t.status)),
而 GET /v1/tasks/{id} 的响应(TaskInfo)不含 status_detail / progress
—— 二者只存在于 SSE 事件负载。于是:

  1. SSE 推 solving/30 → 阶段徽章「求解中」+ 定值进度条 30
  2. 2 秒后轮询返回 processing → 映射出 {null,null} → 被清回「运行中」+ 无定值进度条

用户看到的就是进度条每 2 秒跳一次。

实证来源:Tasks.test.tsx 失败时的 DOM dump —— aria-label="任务阶段:运行中"

  • progressbar 无 aria-valuenow(即 SSE 已推过精确值,却被轮询打回粗粒度)。

修法:轮询的职责是「探测终态 + 无 SSE 时兜底」,不是用粗粒度信息覆盖精确信息

// 状态变了要更新;状态没变且映射不出更细的阶段 → 不动现有精确值
if (mapped.statusDetail === null && lastAppliedStatus === t.status) return;

未做(记录边界):「刷新页面 / SSE 断开后只剩粗粒度阶段」是精度限制而非错误
(数据从未持久化)。要让刷新后也能看到阶段,需把 status_detail/progress 落库
并加进 TaskInfo —— 那是一次 API 契约变更(改 3 个发布点 + schema + 迁移),
属独立决策项,本批不做。


验证

npx tsc -b                                  → exit 0
NODE_OPTIONS=--max-old-space-size=4096 npx vitest run
  (CI 同款命令,不带任何 CLI 参数,完全读配置)
  → Test Files 28 passed | Tests 300 passed | 0 failed

pytest tests/test_shutdown_task_robustness.py tests/test_db_retention.py → 23 passed
pytest tests/test_version_consistency.py    → 5 passed
ruff check api/ tests/ scripts/             → All checks passed!

# CI 侧(决定性)
GitHub Actions run 38887120926 → success(6/6 job)

变异验证(新增用例的判别力):把「进度条降级覆盖」的修复摘掉 →
该用例单独失败、原 4 条照过(1 failed | 4 passed)→ 精确命中。


顺带修复

  • tests/test_shutdown_task_robustness.py:_MANAGERS/_IDS 被定义了两次(后者覆盖前者),
    删除重复。测试语义与断言强度不变。
  • .wolf/buglog.json 按项目协议记入 bug-025(isolate 串味)/ bug-026(轮询覆盖)。

升级说明

无破坏性变更。无需迁移。版本全链 10 文件 + 双 dist 已同步至 23.3.2。

v23.3.1 — CI 转绿:三层级联失败逐个击破

Choose a tag to compare

@lza6 lza6 released this 15 Oct 19:00

v23.3.1 — CI 转绿:三层级联失败逐个击破

发布日期:2026-10-16
基线:v23.3.0 (b37f8af)
性质:缺陷修复批(无新功能、无破坏性变更、无需迁移)


一句话

修掉 3 个让 CI 后端 job 持续红的缺陷,后端 job 已全步骤通过;
同时把"CI 失败是级联的"这一事实搞清楚并记录。


背景:CI 已经红了一个多月

本批开工时的实测(GitHub Actions API):

项 事实
总运行次数 462
最近 100 次 CI 54 failure + 1 cancelled、Deploy 45 failure → 成功 0 次
最后一次 success 2026-09-08 之前

CI 的失败是级联的 —— 前一步失败会让后续步骤 [skip],
所以必须逐层修、逐层看:

提交 后端 job 前端门禁 揭示/修复了什么
基线 ❌ 单元测试 ❌ Vitest 起点:2 个 job 红
d1b8ae7 ❌ 单元测试 ✅ DOM 泄漏修复在 CI 得到实证
26929d6 ❌(database is locked 已消失) ✅ DB 隔离修复生效
f862ce6 ⚠️ 单测 ✅,改由集成测试失败 ✅ shutdown 修复生效;集成步骤此前一直被 [skip]
e55e8fb ✅ 全步骤通过 ⚠️ 另一条 flake 后端 job 转绿

本批修复

P1 · stop() 冒泡异常,打断整个优雅关闭序列

api/provider_probe.py / free_proxy_fetcher.py / proxy_tracer.py 三处 stop() 都写成:

self.task.cancel()
try:
    await self.task
except asyncio.CancelledError:      # ← 只捕 CancelledError
    pass

await task 会重新抛出该任务的原始异常。 任务以任何非 CancelledError 终止时,
异常直接冒泡出 stop()。

关键区别在调用方式:

  • ProviderProbeManager.stop() 由 api/lifespan.py:177 直接 await
    → 异常打断整个优雅关闭序列,其后的 DB flush / SSE 关闭 / provider 收尾
    / worker 停止 / 代理号池停止全部被跳过
  • 另两处经 shutdown_phase 包装(asyncio.wait 从不取结果)→ 不致命,
    但留下 "Task exception was never retrieved" 噪声

CI 实证(run 38782001463 之前的 junit):

api/lifespan.py:177: in lifespan
    await provider_probe.stop()
api/provider_probe.py:62: in stop
    await self._task
RuntimeError: Task ... attached to a different loop

修法:三处补 except Exception + log.warning。关闭路径必须容错 ——
后台任务的失败不该让整个关机流程失败。

P1 · conftest.py 的 DB 隔离被外部注入绕过

if "IF_DB_FILE" not in os.environ:      # ← 信任边界搞反了
    os.environ["IF_DB_FILE"] = tempfile.mktemp(suffix=".db")

它信任外部注入的 IF_DB_FILE。CI 工作流设了 IF_DB_FILE: "data/test.db"
(ci.yml:75)→ 本段整个跳过 → 测试落到共享的文件库 → 写路径随后抛
sqlite3.OperationalError: database is locked(junit 指向 ip_blocklist_store.add_or_update)。

同一处也是「测试可污染真实库」风险的来源:宿主 shell / .env 里任何残留的
IF_DB_FILE 都会让测试直接读写真库。

修法:改无条件指向临时库;调试需要固定库文件时用专用变量
IF_TEST_DB_FILE 显式指定 —— 不与应用的 IF_DB_FILE 混用。

P1 · test_honor 首页断言缺前置条件守卫

test_index_linked_to_zanshang 无条件断言 GET / == 200,但 / 是条件挂载:

_LANDING_DIR = ... / "landing" / "dist"
if _LANDING_DIR.exists():          # ← 不构建就不挂载
    app.mount("/", ...)

后端 CI job 只装 Python 依赖、不构建前端 → landing/dist 不存在 → 404。
本地开发机通常已 build 过,所以长期看不出来。

修法:加前置条件守卫,未构建时 skip 并写明原因
(沿用 test_openapi_contract.py::test_landing_built_dist_version_matches_source 的既有模式)。


验证

ruff check api/ tests/ scripts/          → All checks passed!

# 关闭容错(新增,含变异验证)
pytest tests/test_shutdown_task_robustness.py        → 7 passed
  └ 三处修复同时摘掉 → 3 用例全红 ✅ 有判别力
pytest + test_free_proxy_fetcher + test_proxy_tracer → 67 passed

# DB 隔离(模拟 CI 注入 IF_DB_FILE)
pytest tests/test_ip_blocklist.py                     → 28 passed
  └ data/ci_sim.db 未被创建 ✅ 证明覆盖生效
6 文件回归(含全部原 CI 失败文件)                      → 84 passed

# test_honor 两条路径都实跑
有 landing/dist                                        → 3 passed
移走 landing/dist(模拟后端 CI job)                    → 2 passed + 1 skipped(带明确理由)

CI 侧实证(GitHub Actions,e55e8fb):后端 job
「单元测试」「启动 mock cf_solver 并运行集成测试」全部 ✅。


已知未闭环

前端 vitest 的一条 flake(非本批引入):

e55e8fb 上报 TestingLibraryElementError: Unable to find an element with the alt text: test prompt
(DOM 显示 gallery-album 已渲染、计数「2 张」,且只有一个根 div —— 说明 cleanup 有效)。
Gallery.test.tsx 全部用例只 mock 1 项,而 GalleryAlbum.test.tsx:67 恰好渲染 2 项
→ 判为 isolate:false 下跨文件 DOM 残留的残余症状。

但它在 d1b8ae7/26929d6/f862ce6 三次 CI 运行中均为 ✅,且本机(Node 25)无法复现
→ 属 flake。未在无法验证的前提下做投机修改。

后续若稳定复现,优先怀疑 Gallery.test.tsx 的 vi.useFakeTimers() 与
afterEach(cleanup) 的执行顺序(假定时器期间 unmount 可能不完整)。


升级说明

无破坏性变更。无需迁移。版本全链 10 文件 + 双 dist 已同步至 23.3.1。

v23.3.0 — 终局审计批:CI 真实性 + 静默失效修复

Choose a tag to compare

@lza6 lza6 released this 15 Oct 16:05

v23.3.0 — 终局审计批:CI 真实性 + 静默失效修复

发布日期:2026-10-15
基线:v23.2.0 (79a5374)
性质:缺陷修复批(无新功能、无破坏性变更、无需迁移)


一句话

本批修掉 5 个真缺陷(含 2 个静默失效、1 个让整条 CI 前端门禁持续红的系统性缺陷),
并发现 CI 已整体失败约一个月这一此前无人注意到的事实。


本批修复

P1 · api/bg_tasks.py — 定时清理循环每次中途抛异常,后半段从未执行

_engine._queue_db 中 _engine 是模块(from .worker import engine),
而 _queue_db 是 Engine 的实例属性 → 每次必抛 AttributeError。

异常被外层 except Exception 吞成 warning,且因抛在 try 块中部,
导致其后所有清理逻辑(DAG run TTL 清理、告警评估 evaluate)被长期跳过。

  • 修复:改从 api.meta 取 Engine 单例(与 lifespan 注入同一实例)+ 防御式 getattr
  • 回归:tests/test_db_retention.py 3 用例(AST 级断言——字符串匹配会被改名绕过)

P1 · 前端 Vitest 门禁持续红 — 测试套件系统性 DOM 泄漏

frontend/vite.config.ts 设了 isolate: false,并在注释中声称
「测试间通过 src/test/setup.ts 的 beforeEach 清理」——但该文件从未有任何清理逻辑。

CI 实证(annotations)多个测试文件同时报
TestingLibraryElementError: Found multiple elements with role "button"/"dialog"/"combobox"。

  • 修复:setup.ts 显式注册 afterEach(() => cleanup())
  • 配置注释改为与实现一致,并标注「isolate:false 与该 cleanup 是配套的,改一方须检查另一方」
  • ⚠ 本地无法判别:本机(Node 25)全套件 299 passed,且摘掉 cleanup 的变异测试本地也全绿 →
    复现不出 CI(Node 20)症状。此修复需推送后重跑 CI 确认。

P1 · 覆盖率门槛文档与 CI 不一致(70 vs 80)

ci.yml:53 / deploy.yml:30 为 --cov-fail-under=80,而开发文档写 70。
危害具体:按文档自测 70% 通过 → 推 CI 撞 80% 意外失败(根源是 v7.6 统一到 80 时文档未同步)。

  • 修复 4 处(仅 .claude/skills/ 权威副本;.agents/ 为未入库旧镜像,不动)

P2 · SSE / WebSocket 多订阅者场景误清回放缓冲

sse_events.py:246 与 ws_events.py:125 的连接收尾无条件调 clear_task。
同一任务被 2+ 客户端订阅时(多标签页、或重连旧连接尚未断开),先断开的一方会清掉
共享的事件缓冲 → 另一方之后用 Last-Event-ID 重连拿不到任何历史,且不报错(静默降级)。

  • 修复:新增 clear_task_if_idle,把「判空」与「清理」放进同一把锁(避免 check-then-act 竞态)
  • 回归 5 用例:行为 3 + 调用点契约 2(后者必要——第一版只测方法不测调用点,变异测试证明它是伪测试)

P2 · 环境变量映射契约的结构性盲区

既有校验用 regex 扫源码文本抓 validation_alias="IF_X" 字面量 →
没有 alias 的字段根本不进集合,故「字段存在但 env 永远读不到」这类缺陷对它完全不可见。
实证:Settings.user_agent 是 202 个字段中唯一漏挂 alias 的。

  • 修复:新增 TestEnvMappingContract(运行时 model_fields 反射 + NO_ENV_BY_DESIGN 显式登记表,5 用例)
  • 新增字段若忘记挂 alias → 测试立刻红,强制作者做显式决策

P2 · deploy/.env.example 注释指向错误的修复文件

注释称「改 UA 需改 api/config/http.py」,但那里是死副本
(HTTPSettings.from_settings() 会无条件用 s.user_agent 覆盖);
20 处生产代码实际消费的是 config.USER_AGENT(源自 api/config/settings_core.py)。
照注释去改,等于白改一场。

  • 修复:注释重写,指明权威文件 + 4 处需同步的位置 + 契约测试位置

P2 · skill 文档版本漂移

SKILL.md 写 v23.1.0,实际已是 v23.2.0(版本门禁只覆盖 10 个代码文件,不含文档)。

  • 修复:版本修正 + 新增门禁 test_dev_doc_version_not_drifted(漂移即红,不做自动改写)

环境 / 基建改善

  • 前端测试套件从"完全跑不起来"到 299/299:根因是 NODE_OPTIONS=--max-old-space-size=512
    太小导致 vitest worker 起不来。设 4096 后 28 files / 299 tests 全绿(139s)。
    排查顺序已固化进 skill:先堆内存 → 再路径 → 最后机器负载。
  • .claude/skills/imagefree-workflow/SKILL.md 新增「CI 现状」告警块 + 取 CI 失败原因的方法。

文档

  • 新增 docs/2026-10-15-audit-report.html(上下文 / 直觉 / 变更 / 未闭环项 / 10 题测验)
  • workflow_status.md 重建为事实台账(含 9 项"伪问题"判定记录,避免重复劳动)
  • .wolf/memory.md 追加本批 15 条可复用事实

验证(真实执行)

ruff check api/ tests/ scripts/          → All checks passed!
pytest tests/test_version_consistency.py → 5 passed
pytest tests/test_config_validate.py     → 25 passed
pytest tests/test_db_retention.py        → 16 passed
pytest tests/test_sse_events_unit.py     → 32 passed
pytest 受影响集合(5 文件)                → 83 passed, 1 skipped
frontend: NODE_OPTIONS=4096 npx vitest run → 28 files / 299 tests passed
frontend: npx tsc -b                       → exit 0
scripts/probe_route_methods.py             → 前端 31 路径全部存在,0 断链
scripts/e2e_mobile_smoke.cjs               → 9 passed / 0 failed(真实 chromium)
12 端点冒烟                                 → 全 200

变异验证 5 组:env 契约 / 文档版本门禁 / bg_tasks 回归 / SSE 调用点契约 均有判别力;
SSE 行为测试第一版无判别力 → 已补调用点契约。


已知未闭环(诚实披露)

项 原因
前端 CI 修复的确认 本机(Node 25)复现不出 CI(Node 20)症状,需推送后重跑 CI 确认
后端 CI 失败的具体原因 失败 job 仅给 exit code 1;日志下载需 admin 权限
全量覆盖率真实数字 本机全量被 --timeout=120 中断(test_tryingopen.py::no_sleep)
线上部署 主机 20.204.27.154 TCP 22/80/443/8100 全部超时(DNS 正常);按挂机处理

三项安全项:报告但未擅自修改(属生产策略决策,需授权):

  1. /v1/logs + /v1/logs/ws 匿名可读实时日志 —— 代码 docstring 明写「v7.7.8 公益只读开放」,属有意设计
  2. public_keymask() 经 /v1/chat/auth/status 回显 Key 前 12 字符(41%)—— 当前 IF_API_KEYS 为空故未触发
  3. POST /v1/agent/memory/observe 未授权写入 —— 匿名可写 agent 记忆(投毒面)

升级说明

无破坏性变更。无需迁移。版本全链 10 文件 + 双 dist 已同步至 23.3.0。

v23.2.0 — 加固批:CORS 安全修复 + 测试基建恢复

Choose a tag to compare

@lza6 lza6 released this 14 Oct 13:29

v23.2.0 — 加固批

本批以「终局审计 + 真实缺陷修复 + 验收能力补齐」为主题。核心是一条安全修复:
CORS 白名单从未生效。

🔒 安全修复

  • IF_CORS_ORIGINS 环境变量静默失效。api/main.py 挂载 CORSMiddleware 时读
    config.CORS_ORIGINS,而该值来自 constants.py 中一行硬编码字面量 "*"。
    Settings.__init__ 里的 global CORS_ORIGINS 拆包后只写 settings_core
    命名空间,与包命名空间不是同一个变量 —— 环境变量始终无效,主链路恒为
    allow_origins=["*"]。
    • 实测验证:白名单内 Origin 放行、白名单外无 access-control-allow-origin 头
    • 成因判定:既有缺陷,非本次拆分引入(旧版同样被模块级字面量覆盖)
    • 配 5 个回归测试锁死

🧪 测试基建恢复

  • 前端 vitest 此前完全不可用:npm test 输出 0 tests / 28 errors。根因是
    vitest 4 隔离 worker 在非 ASCII 路径(2api目录)下启动超时。
    → isolate:false + pool:'forks' + testTimeout:20000,现 28 files / 299 tests 全绿
  • 修 2 处测试失败:
    • test_http_pool_config::test_env_overrides —— config 拆包引入的真回归
      (importlib.reload 不重执行子模块 constants)
    • test_mcp_server::test_tools_list_whitelist —— 白名单停在 v16,未同步
      v18 P1-3 新增的 2 个工具
  • ruff 基线 27 → 0(含一处真缺陷 F821 Undefined name ModuleType)
  • 记录了本仓 pytest 失败详情被 conftest 的 os._exit 截断这一隐蔽问题

🎨 用户可见修复

  • 门户画廊错误文案泄漏:原本把 Unexpected token '<', "<!DOCTYPE "... 直接渲染给用户
    → 按失败类型归类中文文案
  • 管理端 topbar 375px 文字重叠、聊天页动作按钮组 375px 溢出

📋 验收能力补齐

  • 移动端 E2E 真跑通(此前脚本默认连 4590 但仓库无任何服务提供该端口,从未跑过)
    → 新增 scripts/serve_static.cjs,补 /admin 与深链回退路径
  • 压测复验:1→200 并发共 750/750 成功、0 个 5xx、进程存活;
    压力释放后延迟 1 次请求内回落(无连接池耗尽/内存泄漏残留)
  • 全量测试:2521+ passed / 0 failed

⚠️ 已知遗留(非本批引入)

  • test_skill_sediment.py 在特定文件组合下 teardown 报 attached to a different loop
    (跨文件事件循环污染)。已用实验证明与本批改动无关。

完整变更

6 主题提交 + 版本 bump,详见仓库 docs/2026-10-14-hardening-report.html(含 14 题测验)
与 workflow_status.md。

v23.1.0

Choose a tag to compare

@lza6 lza6 released this 28 Sep 16:42

v23.1.0 — 极限施压 + 防穿透 + 覆盖率猎杀

Spec-Kit 011 落地:010 US2「高并发极限施压探针」承诺未兑现 → 本轮真补。

极限施压(读路径)

  • scripts/probe_stress.py(新探针):5 只读端点 × 并发 1→200 阶梯压测
  • 200 并发 0 错误率、0 5xx、0 429、进程不崩;延迟双口径披露(本机 HTTP 排队噪声)
  • 报告:docs/research/stress-penetration-v23.md + stress-v23.json

防穿透

  • 畸形 JSON / 超长 prompt(200KB) / 5MB body → 全 422 不崩不 OOM
  • 生图限流 10→5×429 + Retry-After:60 真实生效
  • auto-block 403 实测(task-failure-burst 机制 = S-2 生产核对项实证)

覆盖率猎杀(T3/T4)

  • v23 修复模块基线仅 61.5%(log_buffer 43.2% / auth 65.9% / alerting 62.1%)
  • 补测后:auth 76.1% / alerting 95.4% / log_buffer 88.9%
  • 顺带修复真实 bug:LogBufferHandler attrs 键被 k[6:] 错剥(attr_ 是 5 字符)→ attr_custom 变 ustom,已修

独立审查(T6)

  • 首轮 NEEDS_CHANGES(F1 429 误判崩溃 P1 + F2-F8)→ 全部修复 → 复验 APPROVE
  • F1 探针崩溃阈值排除 429(优雅降级≠崩溃);F3 webhook caplog 断言;F4 存活 poll

验证

  • 36 补测全绿 + probe py_compile + ruff 0 error + 169 关键回归全绿
  • 版本全链 23.1.0 + landing dist 重建
  • HTML 报告增补 3 题(共 10 题测验)

🤖 Generated with Claude Code

v23.0.0

Choose a tag to compare

@lza6 lza6 released this 28 Sep 12:29

v23.0.0 — 终局闭环审计发布

7 节点终局审计(契约/SQL/安全/前端/solver/部署/独立审查)→ 修复 → 复验 → 发布。

安全

  • S-1 /v1/logs/ws 广播脱敏(redis 密码/webhook 签名不再经匿名 WS 泄露)+ redis/alerting 源头只记 host;_redact 补 redis:// 与含 @ 密码模式
  • S-3 chat/DAG 限流改受信代理解析真实客户端 IP(反代后不塌缩全站单桶)
  • S-4 POST /v1/tasks/{id}/cancel 补 per-IP 限流(防批量恶意取消)

SQL / 数据层

  • 5 个幂等补索引(status_dur/day/month/model/duration)
  • gallery 负向 NOT IN 弃索引 → GALLERY_VISIBLE_STATUSES 正向白名单(单一常量源)
  • 批量写失败回写带重试上限(_dead_letters 隔离,防头部阻塞/内存增长)

契约 / 前端

  • F1 画廊软删前端补管理 Key(后端强制 check_admin_key)+ 删除失败项保留可见
  • F2 聊天 429 retry_after_seconds 秒→分换算(提示真实生效)
  • Costs 预算预测无管理 Key 降级提示(不再永久骨架屏);Security 封禁确认;Tasks 空态 react-router

部署原生化(线上 systemd 无 Docker)

  • deploy/systemd/: prometheus/node_exporter/grafana/litestream 独立 unit + 生产配置 + 一键安装脚本
  • SOP/litestream-restore/README 同步 + 过时警示

验证

  • 后端 120+ 用例绿;前端 120+ 用例绿 + tsc 0;双 dist 重建
  • 真实 E2E:livez/healthz ok、openapi 23.0.0、models/stats/metrics(70)
  • solver 联邦实测分流 10/10、均时 0.18/0.27s
  • 独立审查 N7 APPROVE(2 P1 + 5 P2 已返工复验)

报告:docs/v23-terminal-report.html(含 7 题测验)· 审计:docs/research/*-v23.md

🤖 Generated with Claude Code

v22.1.5

Choose a tag to compare

@lza6 lza6 released this 28 Sep 03:42

听风AI v22.1.5(终局审计收尾)

内容

  • imagefree-workflow SKILL.md 更新至 v22.1.4(新工具集 uptime_probe/restore_drill/e2e_mobile_smoke/landing vitest + health-report 降级说明)
  • 独立审查 APPROVE 后修正 2 处注释级微瑕(ws_events 心跳快照语义 / health_report 超时窗口说明)

验证

  • 独立审查 v22.1.x 全批:APPROVE(82 passed + openapi 20 passed + 编译通过,无阻塞缺陷)
  • 前后端契约审计:前端 28 端点全匹配后端 96 路由,100% 对齐
  • 盲点扫描:无 TODO 欺骗/硬编码凭证/版本残留/日志泄漏

前置:v21.0.0 ~ v22.1.4(10 版本)

v22.1.4

Choose a tag to compare

@lza6 lza6 released this 28 Sep 02:56

听风AI v22.1.4 — P2 工程深化批

P2-2 画廊懒加载增强

  • Gallery.tsx 瀑布流 <img> 补 decoding="async"(loading="lazy" 已有)→ 降解码阻塞
  • Gallery 测试 10/10 通过,入 dist

P2-3 config 安全拆分

  • OTel 段抽离为独立模块 api/config/otlp.py(纯 os.getenv,不依赖 settings 单例)
  • __init__.py re-export 保持三类读点零破坏:包属性 config.OTEL_* / 显式 from api.config import OTEL_* / settings 批量导入
  • 剩余 800+ 行因强耦合 settings 单例 + monkeypatch 契约不硬拆(诚实标注:风险 > 收益)
  • config 契约测试 30 passed

P2-4 portal SEO 深化

  • JSON-LD softwareVersion 15.1.1 → 22.0.0(严重过时修复,与全链一致)
  • featureList 补 AI DAG 编排 / PWA 安装;加防漂移注释(sync_version 对齐)
  • canonical / og 核对完整

P2-5 成本预测确认已闭环

  • Costs 页已接 fetchCostForecast(projected_exceed_date / days_remaining / daily_avg_30d / disabled 四态齐全)——本项已在 v16.1 落地,确认无需重做

验证

  • config 契约 30 + 全量 34 passed + Gallery 10/10 + 双 dist 重建

前置:v21.0.0 ~ v22.1.3(9 版本)

v22.1.3

Choose a tag to compare

@lza6 lza6 released this 28 Sep 02:15

听风AI v22.1.3

SSE 心跳参数化(P1-3 扩展)

  • IF_SSE_HEARTBEAT_INTERVAL 配置装配(缺省 15s 向后兼容,0/非法值忽略)
  • set_heartbeat_interval() / heartbeat_interval() 动态读写;ws_events 同步动态读
  • 3 单测:默认 15s / 有效装配 / 非法忽略

health-report 邮箱源 import 阻塞降级(真实 E2E 修复 HIGH)

  • 发现:真实 E2E 契约卡 16c/16d——_collect_email_pool 同步 import email_pool 构建 9 源做域名探测(每源 6-10s 串行 45-90s),_safe await 后兜底无法中断同步 import → 拖垮 health-report
  • 修复:import 走 run_in_executor 线程 + wait_for(8s) 限时,超时降级 error 项;stats 主 loop 异步
  • 3 单测:超时降级(同步阻塞模拟)/ ok / 异常降级
  • 真实复测:受限网络下 health-report 200(8.4s,email_pool:error 超时降级,其余维度正常)——修复前 30s 挂起

验证

  • 相关 5 文件 71 passed + 1 skip 全绿
  • 实际运行:mock solver + uvicorn 起服成功、livez 200、health-report 限时返回

前置:v21.0.0 / v22.0.0 / v22.0.1 / v22.1.0 / v22.1.1 / v22.1.2