跳转至

Phase 3 API 契约

目的:客户端 v0.10.95 已实现全部 sync 逻辑,但 enableCloudSync 默认关闭(无云端时不发请求)。服务端按本契约实现后,用户开启 flag 即工作。 协议:HTTPS / JSON / Bearer token

0. 通用约定

0.1 Base URL(可在 settings 覆盖)

默认: https://cloud.laifaxin.com/api/v1

客户端通过 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": true,
  "data": { ... },
  "serverTime": 1717000000000
}

失败

{
  "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:

{
  "clientId": "uuid-v4-from-client",
  "platform": "chrome-extension",
  "version": "0.10.95"
}

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):

GET /contact-pool/query?hashes=sha256-1,sha256-2,sha256-3

或 POST body(hashes 多时):

Request:

{ "hashes": ["sha256-1", "sha256-2", "..."] }

最多 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:

GET /contact-pool/since?since=1717000000000&limit=1000&domain=example.com

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:

{
  "success": true,
  "data": {
    "accepted": 100,
    "contributionEarned": 10
  }
}

GET /domain-state/list

拉取云端权威 domain state(启动时 + 每小时)。

Request:

GET /domain-state/list?since=1717000000000&limit=500

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:

GET /contribution/history?limit=50&before=1717000000000

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:

GET /leaderboard?period=week|month|all&limit=100

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 删除由用户选

相关