Phase 3 API 契约¶
目的:客户端 v0.10.95 已实现全部 sync 逻辑,但
enableCloudSync默认关闭(无云端时不发请求)。服务端按本契约实现后,用户开启 flag 即工作。 协议:HTTPS / JSON / Bearer token
0. 通用约定¶
0.1 Base URL(可在 settings 覆盖)¶
客户端通过 settings.cloudSyncBaseUrl 字段读取,留空走默认。
0.2 认证¶
所有请求需带:
Authorization: Bearer <session_token>
X-Client-Version: 0.10.95
X-Client-Id: <anonymous-uuid 或 logged-in-user-id>
session_token 来源:
1. 用户已通过现有 popup 登录 → 复用既有 token
2. 匿名用户 → 客户端启动时生成 anonymous-uuid(v4 UUID 持久化在 storage.local),首次调用 POST /anonymous/register 拿到匿名 token
0.3 通用响应包装¶
成功:
失败:
{
"success": false,
"code": "RATE_LIMIT" | "UNAUTHORIZED" | "VALIDATION" | "QUOTA_EXCEEDED" | "NOT_FOUND" | "INTERNAL",
"message": "用户可读的错误信息(可选)",
"retryAfter": 60
}
0.4 速率限制¶
| 操作 | 限制 |
|---|---|
| 上传类(POST upload/log) | 60 次/分钟/客户端 |
| 查询类(GET) | 600 次/分钟/客户端 |
| 拉取类(since/list) | 30 次/小时/客户端 |
超限返回 429 + retryAfter 秒数。
0.5 Idempotency(幂等)¶
所有 POST upload 请求带 Idempotency-Key: <uuid> 头。服务端去重,重复请求返回首次结果。客户端用 crypto.randomUUID() 生成。
1. 匿名注册 / 用户身份¶
POST /anonymous/register¶
首次使用时创建匿名身份。
Request:
Response 200:
{
"success": true,
"data": {
"anonymousUserId": "anon-abc123",
"token": "eyJhbGc...",
"tokenExpiresAt": 1719000000000
}
}
GET /me¶
当前用户信息(实名 OR 匿名)。
Response 200:
{
"success": true,
"data": {
"userId": "anon-abc123",
"isAnonymous": true,
"contributionBalance": 120,
"createdAt": 1716000000000,
"lastActiveAt": 1717000000000
}
}
2. ContactPool(contact 数据池 — 核心)¶
POST /contact-pool/upload¶
客户端抓到 contact 后批量上传(队列 debounce 30s 批量)。
Request:
{
"items": [
{
"urlHash": "sha256-of-normalized-url",
"normalizedUrl": "https://example.com/contact",
"domain": "example.com",
"emails": ["info@example.com"],
"phones": ["+1 234-567-8900"],
"socials": {
"facebook": "https://facebook.com/example",
"instagram": "",
"linkedin": "",
"twitter": "",
"youtube": "",
"whatsapp": ""
},
"scrapedAt": 1717000000000,
"scrapeMethod": "fetch" | "tab",
"clientVersion": "0.10.95"
}
]
}
Headers: Idempotency-Key: <uuid>
Response 200:
{
"success": true,
"data": {
"accepted": 95,
"rejected": 5,
"newRecords": 60,
"updatedRecords": 35,
"contributionEarned": 60,
"details": [
{ "urlHash": "...", "status": "accepted", "isNew": true },
{ "urlHash": "...", "status": "rejected", "reason": "spam" }
]
}
}
Validation:
- items.length ≤ 200 / 请求
- 每个 urlHash 必须是 64 字符十六进制
- 每个 email 通过 RFC 5321 基础格式校验(服务端 re-validate)
- normalizedUrl 长度 ≤ 2048
GET /contact-pool/query¶
批量查询命中(pipeline 抓前调用)。
Request (query string):
或 POST body(hashes 多时):
Request:
最多 100 hash / 请求。
Response 200:
{
"success": true,
"data": {
"hits": [
{
"urlHash": "sha256-1",
"emails": ["info@..."],
"phones": ["+1 ..."],
"socials": { ... },
"contributorCount": 5,
"lastVerifiedAt": 1717000000000,
"consensus": 0.95
}
],
"misses": ["sha256-2", "sha256-3"],
"queryCost": 2
}
}
queryCost = 命中时消耗的贡献度(每条命中 = 1 贡献度,未命中不扣)。
consensus = N 个客户端独立报告同结果的强度(0-1)。
GET /contact-pool/since¶
增量拉取(自某时间起更新过的记录 — 用于 pre-populate 本地缓存)。
Request:
domain 可选过滤。limit ≤ 5000。
Response 200:
{
"success": true,
"data": {
"items": [ /* 同 upload 格式 + consensus / contributorCount */ ],
"hasMore": true,
"nextSince": 1717100000000
}
}
3. DomainState 同步¶
POST /domain-state/upload¶
上传本地观察到的 domain state 批次(debounce 5min)。
Request:
{
"items": [
{
"domain": "example.com",
"state": "friendly" | "dead" | "antibot-soft" | "antibot-hard" | "cold" | "unknown",
"fetchTotal": 15,
"fetchOk": 12,
"tabTotal": 2,
"tabOk": 1,
"stateChangedAt": 1717000000000,
"lastSeen": 1717000000000
}
]
}
Response 200:
GET /domain-state/list¶
拉取云端权威 domain state(启动时 + 每小时)。
Request:
Response 200:
{
"success": true,
"data": {
"items": [
{
"domain": "example.com",
"state": "antibot-hard",
"consensus": 0.92,
"contributorCount": 15,
"updatedAt": 1717000000000,
"expiresAt": 1719000000000
}
],
"hasMore": false
}
}
4. Contribution Ledger(贡献度账本)¶
POST /contribution/log¶
实时上报本地行为。
Request:
{
"items": [
{
"localId": "ulid-or-uuid",
"action": "upload-new" | "upload-verify" | "query-hit" | "query-miss" | "reset-local",
"amount": 1,
"ref": "urlHash 或 domain",
"occurredAt": 1717000000000
}
]
}
amount 客户端建议值;服务端可校正(防作弊)。
Response 200:
{
"success": true,
"data": {
"accepted": [
{ "localId": "...", "serverId": "ledger-123", "amount": 1 }
],
"rejected": [
{ "localId": "...", "reason": "duplicate" }
],
"newBalance": 121
}
}
GET /contribution/balance¶
Response 200:
{
"success": true,
"data": {
"userId": "anon-abc123",
"balance": 120,
"lifetime": {
"earned": 200,
"consumed": 80
},
"updatedAt": 1717000000000
}
}
GET /contribution/history¶
Request:
Response 200:
{
"success": true,
"data": {
"items": [
{
"id": "ledger-123",
"action": "upload-new",
"amount": 1,
"ref": "sha256-...",
"occurredAt": 1717000000000,
"serverAckedAt": 1717000001000
}
],
"hasMore": false
}
}
5. 排行榜 / 统计(可选 — Phase 3.5)¶
GET /leaderboard¶
Request:
Response 200:
{
"success": true,
"data": {
"period": "week",
"items": [
{ "userId": "...", "rank": 1, "contribution": 850, "isMe": false },
{ "userId": "anon-abc123", "rank": 42, "contribution": 120, "isMe": true }
]
}
}
6. 健康检查¶
GET /health¶
无需 auth。客户端启动时 ping 确认服务可用。
Response 200:
{
"success": true,
"data": {
"status": "ok",
"version": "1.0.0",
"serverTime": 1717000000000,
"minClientVersion": "0.10.95"
}
}
minClientVersion 用于强制升级(旧版客户端收到 426 Upgrade Required + 此字段)。
7. 错误码总览¶
| HTTP | code | 含义 | 客户端处理 |
|---|---|---|---|
| 200 | (无 code) | OK | 正常 |
| 400 | VALIDATION | 请求格式错 | 不重试,日志记录 |
| 401 | UNAUTHORIZED | token 过期 | 重新 anonymous/register,retry |
| 402 | QUOTA_EXCEEDED | 余额不足(查询类) | 提示用户充值贡献度 |
| 403 | FORBIDDEN | 黑名单 / 滥用 | 停止 sync,提示 |
| 404 | NOT_FOUND | 资源不存在 | 不重试 |
| 409 | CONFLICT | Idempotency-Key 冲突(已处理) | 视为成功,直接 ack |
| 426 | UPGRADE_REQUIRED | 客户端版本过低 | 提示用户升级 |
| 429 | RATE_LIMIT | 速率限制 | 按 retryAfter 退避 |
| 500/502/503/504 | INTERNAL | 服务端临时错误 | 指数退避重试 |
8. 客户端调用模式(v0.10.95 已实现)¶
8.1 启动时¶
client.startup()
├─ ensureAuth() → POST /anonymous/register(首次)或刷新 token
├─ pingHealth() → GET /health(检查 minClientVersion)
└─ pullCloudState() → GET /domain-state/list?since=lastSyncAt
GET /contact-pool/since?since=lastSyncAt (limit 1000)
8.2 抓取前¶
pipeline.preCheck(url)
├─ localCache?.hit → 用本地
├─ POST /contact-pool/query (debounce 1s 批量) → 命中?
└─ both miss → 继续 pipeline
8.3 抓取后¶
pipeline.postExtract(url, contacts)
├─ writeContactPool(local)
├─ enqueue upload (queue.add)
└─ enqueue contribution log (queue.add)
queue.flush() — 每 30s(contact)/ 5min(domain-state)触发
├─ POST /contact-pool/upload (batch ≤ 200)
├─ POST /domain-state/upload (batch ≤ 100)
└─ POST /contribution/log (batch ≤ 200)
8.4 失败重试¶
- 网络错误 / 5xx:指数退避(2/4/8/16s,最多 5 次)
- 429:按
retryAfter等待 - 401:refresh token,retry 1 次
- 其他错误:本地队列保留,下次 flush 再试
- 队列满(1000 条):丢最旧,记 warning
9. 数据流图¶
┌────────────────────────────────────────────────────────────────┐
│ CLIENT (v0.10.95) │
│ │
│ 抓取 pipeline │
│ ├─ preCheck → localCache → POST /contact-pool/query ────┐ │
│ ├─ extract │ │
│ └─ postExtract │ │
│ ├─ jsstore.ContactPool insert ◀─────────────────┐ │ │
│ ├─ enqueue UPLOAD: /contact-pool/upload │ │ │
│ ├─ enqueue UPLOAD: /domain-state/upload │ │ │
│ └─ enqueue LOG: /contribution/log │ │ │
│ │ │ │
│ cloud-sync-orchestrator (alarm 30s/5min) │ │ │
│ ├─ flushContactPoolUploads ────────────────────────┐ │ │ │
│ ├─ flushDomainStateUploads │ │ │ │
│ ├─ flushContributionLogs │ │ │ │
│ └─ pullCloudState (hourly) │ │ │ │
│ ├─ GET /domain-state/list ◀──────────────────┼──┼──┼──┐ │
│ └─ GET /contact-pool/since ◀─────────────────┼──┼──┼──┤ │
│ │ │ │ │ │
└───────────────────────────────────────────────────────┼──┼──┼──┼──┘
│ │ │ │
┌───────────────────────────────────────────────────────┴──┴──┴──┴──┐
│ SERVER (待实现) │
│ │
│ POST /contact-pool/upload ─→ contact-pool table │
│ GET /contact-pool/query ─→ consensus 算法 │
│ GET /contact-pool/since ─→ 增量批量返回 │
│ │
│ POST /domain-state/upload ─→ domain-state aggregation table │
│ GET /domain-state/list ─→ 共识聚合后下发 │
│ │
│ POST /contribution/log ─→ ledger append-only │
│ GET /contribution/balance ─→ 实时余额 │
└──────────────────────────────────────────────────────────────────────┘
10. 服务端验收清单¶
服务端实现完毕后,按此清单测试:
- POST /anonymous/register 能拿到 token
- Authorization Bearer 验证生效(缺失或过期返 401)
- POST /contact-pool/upload 接受 200 条批量,返回 accepted/rejected
- Idempotency-Key 去重生效(同 key 重发返回首次结果)
- GET /contact-pool/query 批量 hashes 命中正确
- consensus 字段按 N 客户端独立报告计算
- GET /contact-pool/since 增量拉取分页正确(hasMore + nextSince)
- POST /domain-state/upload 接受批量
- GET /domain-state/list 按 since 增量
- POST /contribution/log append-only,重复 localId 返 duplicate
- GET /contribution/balance 实时返回当前余额
- Rate limit 60/min 上传、600/min 查询、30/h 拉取触发 429 + retryAfter
- 5xx 错误返 INTERNAL code(不是 stack trace)
- GET /health 无需 auth + 返 minClientVersion
11. 关键决策记录¶
- 匿名注册:v0.10.95 默认创建匿名 ID(不要求登录),降低使用门槛。后续可让用户绑定实名拿到更多权益
- Idempotency-Key:所有 POST upload 必带,避免网络重试导致重复写
- debounce 批量:减少请求次数 + 服务端 batch 处理效率
- 客户端建议 amount,服务端校正:防作弊(用户改本地代码刷贡献度)
- consensus 算法:服务端聚合 N 客户端报告,分歧大的不下发(低 consensus),分歧小的高置信度
- opt-in:客户端默认
enableCloudSync: false,用户主动开启 + 同意隐私条款后才开始 sync
12. 隐私 / 合规¶
- 上传数据 = 网页上公开可见的 contact 信息(不上传 cookies / personal data)
- 用户 opt-in 时 settings 必须明确说明"将与社区共享你抓到的 contact 数据"
- 提供
POST /contact-pool/delete-mine(未在上面列出,可选 Phase 3.6 增加)让用户删除自己贡献过的所有数据 - GDPR:匿名 ID 不算 PII;但用户登录后会关联实名,关联前的匿名贡献保留 OR 删除由用户选
相关¶
- SPEC-004-网站采集多阶段优化-云端协同 — 完整方案
- 云端同步架构 — 为什么 jsstore + 4 store 设计
- 域名状态机 — Phase 2 状态机
- 多阶段抓取pipeline — Phase 1 决策树