-
Notifications
You must be signed in to change notification settings - Fork 0
Development
git clone https://github.com/yuanqimanong/IPClick.git
cd IPClick
uv sync --all-extras --all-groups不用 uv 的话:
pip install -e ".[niquests,playwright]"
pip install ruff basedpyright pytest pytest-cov pytest-asyncio提交前跑这四条——CI 跑的是完全一样的四条:
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run basedpyright src/
uv run pytest- basedpyright 是 strict 模式,warning 也算失败。 它的退出码在有 warning 时同样是 1,所以本地和 CI 的判定是一致的。
-
ruff format 不能碰生成文件 ——
task_pb2.py/task_pb2.pyi已在排除列表里。 漏掉的话 format 会改写它们,之后每次重新生成 protobuf 都产生一堆无意义 diff。
覆盖率:
uv run pytest --cov=ipclick --cov-report=term-missing# 改完 src/ipclick/dto/proto/task.proto 之后
uv run python src/ipclick/dto/proto/generate.py这个脚本调 protoc,并把生成代码里的顶层导入改成包内相对导入。
CI 会验证生成产物是最新的——改了 .proto 不重新生成会直接失败。
| 做法 | 允许? |
|---|---|
| 加新字段(新字段号) | ✅ |
字段 string → bytes
|
✅ wire 兼容,编码完全相同 |
| 删字段 | ✅ 但字段号必须进 reserved,永不复用 |
| 删枚举值 | ❌ 标 deprecated = true 保留 |
| 改字段号 | ❌ |
| 改字段类型(除上面那条) | ❌ |
枚举值标 deprecated 而不是删掉,是为了让旧客户端传 HTTPX 时能得到
"已移除,请改用 niquests"这样一句有用的话,而不是"未知枚举值"。
需要区分"没传"和"传了默认值"时用 optional(显式 presence)——
verify 默认值那个 bug 就是这个区别没处理好造成的。
src/ipclick/
├── adapters/ # 适配器:curl_cffi / niquests / 四个浏览器引擎
│ ├── base.py 重试、脚本规范化、错误分类
│ ├── registry.py 名字 → 实现,以及"没装"与"已移除"两张提示表
│ ├── browser_adapter.py 浏览器适配器与实例复用
│ └── browser_engines.py 引擎安装状态检测(三态)
├── cluster/ # 集群
│ ├── node.py 节点池、负载均衡、健康探测
│ ├── forwarder.py 服务端转发
│ └── tokens.py 共享密钥派生每节点令牌
├── config_loader/ # 配置加载与写回
├── dto/ # 数据模型与 protobuf
├── services/ # gRPC 服务实现
├── web/ # Web 管理端
│ ├── server.py HTTP、会话、CSRF
│ ├── pages.py 页面业务逻辑
│ ├── editable.py 可改配置的白名单
│ └── templates.py 渲染
├── compression.py # 请求压缩策略
├── limiter.py # 按 host 的并发与速率闸门
├── trace.py # 链路记录与 SQLite
├── sdk.py / aio.py # 同步 / 异步客户端
└── server.py # 服务端入口
web/server.py 和 web/pages.py 是刻意分开的:前者管 HTTP(路由、会话、CSRF、响应头),
后者管"页面展示什么、提交上来怎么处理"。混在一起的话,每加一页都得往 HTTP 处理器里塞
一段业务代码,很快就没法看了。
约 1050 个测试。
uv run pytest # 全部
uv run pytest tests/test_trace.py # 单个文件
uv run pytest -k "cluster" # 按名字筛
uv run pytest -x -q # 第一个失败就停写测试的几条约定:
- 测行为,不测实现。 断言"配错了会报错",而不是"调用了某个内部函数"。
- 一个测试一件事,名字说清楚断言的是什么。
- 随机/时间相关的测试要真的稳。 压缩启发式那个测试早期是个 5% 概率翻车的硬币—— 阈值 10% 撞上随机字节的期望控制字符占比 10.9%。发现这种"偶尔红"的测试, 修的是判据本身,不是加重试。
这几条踩过不止一次:
-
pkill -f "xxx"会匹配到它自己的命令行。 用ps -eo pid,etimes,cmd | grep "[x]xx"拿 PID 再 kill;找监听进程用ss -lptn "sport = :9527"。 -
handle_error是socketserver.BaseServer的方法,不是 handler 的。 写在 handler 上是个静默的 no-op。 -
SQLite 的
auto_vacuumPRAGMA 只在建库时生效,已有的库改不了(要VACUUM重建)。 - schema 迁移必须在建索引之前跑,否则新索引引用的列还不存在。
Conventional Commits:
feat(cluster): 支持服务端转发
fix(browser): 复用浏览器实例前检查连接存活
docs: 补充集群鉴权说明
refactor(web)!: 拆分 HTTP 层与页面业务逻辑
! 表示破坏性变更,正文里要写清楚怎么迁移。
- 更新
CHANGELOG.md—— 破坏性变更单独一节,写明迁移方法 - 改
pyproject.toml的version - 四条门禁全绿
- 合并到
master - 打标签并推:
git tag -a v0.3.0 -m "v0.3.0"
git push origin v0.3.0推标签会触发 release.yml:
build ──→ publish-pypi ──→ github-release
│ │
│ └─ environment: pypi,配了必需审批人,
│ 上传前停下来等人工确认
│
└─ 重跑全部门禁 + uv build + 校验标签与 pyproject 版本号一致
两处刻意的设计:
- PyPI 上传前有人工审批门。 PyPI 同一版本号永远不能重传,只能 yank—— 这一步不可撤销,值得停一下。
-
不设
skip-existing。 正式发布时重复版本号应当失败并让人知道, 而不是静默跳过让你以为发出去了。
CI(ci.yml)在 Python 3.11 / 3.12 / 3.13 上跑门禁,另外还会:
构建 wheel 与 sdist、twine check、验证包数据都打进去了、构建 Docker 镜像并冒烟测试、
验证 protobuf 生成产物是最新的。
- 从
master开分支 - 写代码 + 测试
- 四条门禁全绿
- Conventional Commits
- PR 说明里写清楚为什么,不只是改了什么
改了行为的话记得同步:CHANGELOG.md、README.md、
configs/default_config.toml 里的注释(那是配置项的唯一权威来源)、以及本 wiki。
代码里的注释解释的是为什么这么写,尤其是"为什么不用那个看起来更自然的写法"。 一个默认值的选择、一处不重试的判断、一个显式关掉的 gRPC 选项——这些不写下来, 下一个人(包括三个月后的你)会把它改回去,然后重新踩一遍。