HTTP API 文档

CVKShadowKit 备份控制接口
← 返回控制台
所有接口部署在设备本机的局域网 HTTP 服务上,监听端口 45578~45599。 响应均为 application/json(下载类除外),已开启 Access-Control-Allow-Origin: *,并支持 OPTIONS 预检。 路径相对服务根,例如 http://<设备IP>:<端口>/api/overview。 POST 接口请用 Content-Type: application/json 提交(普通请求体上限 64KB,/api/upload 走流式上限 4GB);方法不匹配返回 405,未知路径返回 404

总览

GET/api/overview

一次性返回当前生效环境、偏好键值、全部环境名、导出/导入目录文件列表,以及三个目录的绝对路径。Web 控制台首页即用它。

响应示例
{
  "current": "原始机器",
  "storedPreference": "原始机器",
  "environments": ["原始机器", "20260610022719", "..."],
  "exports": [{ "name": "原始机器.tar", "size": 30279680 }],
  "imports": [{ "name": "aaa.tar", "size": 1024 }],
  "paths": {
    "data":   "/var/mobile/Media/CVKShadowKit/data",
    "export": "/var/mobile/Media/CVKShadowKit/export",
    "import": "/var/mobile/Media/CVKShadowKit/import"
  }
}

环境

GET/api/env

返回 current / storedPreference / environments

响应
{ "current": "原始机器", "storedPreference": "原始机器",
  "environments": ["原始机器", "20260610022719", "..."] }
GET/api/env/current

仅返回当前生效环境名。

响应
{ "current": "原始机器" }
GET/api/env/all

仅返回全部环境名数组。

响应
{ "environments": ["原始机器", "20260610022719", "..."] }
GET/api/env/exists?name=<环境名>

判断指定环境是否存在。name(亦可 record/environment)必填;按目录实际名区分大小写匹配,未命中时再给出大小写不敏感的命中名以便纠正。

响应
{ "name": "20260610022719", "exists": true, "caseInsensitiveMatch": null }
// 不存在但大小写不同:{ "name": "Env1", "exists": false, "caseInsensitiveMatch": "env1" }
GET/api/records

仅返回环境名数组(旧版兼容)。

["原始机器", "20260610022719", "..."]
POST/api/env/create

新建一个以时间戳命名的环境目录并选中,随后对其做一次快照。请求体可为空。

响应
{ "created": "20260610213000", "current": "...", "environments": [...] }
POST/api/env/switch

切换生效环境:先备份当前环境 → 选中目标 → 终止相关进程并清理、再从记录恢复。默认切换后会自动刷新一次 IP 定位;如需跳过,加参数 skipIpRefresh

请求体
{ "name": "环境目录名" }                        // 默认切换后刷新 IP
{ "name": "环境目录名", "skipIpRefresh": true } // 切换后不刷新 IP
// 也支持查询参数 ?name=环境目录名&skipIpRefresh=1
// 等价写法:refreshIp=false(同样跳过刷新)
响应
{ "switchedTo": "...", "snapshotRecord": "...", "restoreTriggered": true,
  "ipRefreshed": true, "current": "...", "environments": [...] }
POST/api/env/rename

重命名环境目录。不带 old 时默认重命名「当前激活环境」。不能重命名默认环境(原始机器)。若被改的是当前选中环境,会同步更新选中偏好。

请求体
{ "name": "新环境名" }                 // 重命名当前激活环境
{ "name": "新环境名", "old": "旧环境名" } // 重命名指定环境
// 也支持查询参数 ?name=新名&old=旧名
响应
{ "renamedFrom": "20260610213000", "renamedTo": "新环境名",
  "current": "新环境名", "environments": [...] }
错误
error状态码说明
name_required400未提供新名
default_immutable400不能重命名「原始机器」
invalid_name400新名非法(含非法字符等)
same_name400新旧名相同
name_exists400目标名已存在(大小写不敏感)
src_missing400旧环境目录不存在
move_failed500目录移动失败
POST/api/newdevice

一键新机:备份当前环境 → 新建时间戳空白环境并选中 → 清理真机数据(不恢复)。注入目标取持久化预设列表。请求体可为空。清理完成后默认会自动对新环境触发一次 IP 定位(与首页一键新机一致),把伪装 GPS 写入环境并刷新系统模拟定位(定位真正在系统中生效);请求同步阻塞至定位完成(最长约 35s),结果里带回 ipLocate如需新机后不刷新 IP,加参数 skipIpRefresh(此时接口不阻塞、秒回,ipLocatenull)。

请求(可选,不传即默认刷新)
{ "skipIpRefresh": true }   // 新机后不刷新 IP
// 也支持查询参数 ?skipIpRefresh=1
// 等价写法:refreshIp=false(同样跳过刷新)
响应
{ "created": "20260610213000", "snapshotRecord": "...",
  "ipRefreshed": true,
  "ipLocate": { "ok": true, "located": true, "ip": "...",
                "city": "...", "region": "...", "lat": 36.17, "lon": -115.07 },
  "current": "...", "environments": [...] }
// skipIpRefresh 时: "ipRefreshed": false, "ipLocate": null
GET/api/ipinfo

等价于首页「刷新 IP」按钮的完整动作(不是只读查询):对当前激活环境触发一次真实 IP 定位——查询出口 IP、把伪装 GPS 写入环境并刷新系统模拟定位(定位真正在系统中生效),首页同步显示「定位中…」。请求会同步阻塞至定位完成(含双接口轮换+交叉校验,最长约 35s),再返回刚定位到的结果。

响应
{ "ok": true, "located": true, "ip": "1.2.3.4",
  "city": "...", "region": "...", "region_code": "NV",
  "country": "...", "country_code": "US",
  "lat": 37.77, "lon": -122.41, "timezone": "...", "zip": "..." }
失败
{ "ok": false, "error": "ip_locate_failed" }   // 定位超时/全部接口失败
{ "ok": false, "error": "ip_locate_no_result" } // 定位成功但读回结果为空
GETPOST/api/ipscore

设备出口 IP 信用分(与首页「IP 刷新成功后」调用的 Radar trackVerified 打分同一套)。默认会重新探测当前出口并阻塞等待结果(最长约 20s),分数 0–100,越高越干净。?cached=1 只读本地已缓存分、不打 Radar。别名:/api/radar/ipscore

查询参数
?cached=1   // 只读缓存,不探测;缺省或不传 = 重新探测
响应(探测成功)
{ "ok": true, "score": 85, "cached": false, "source": "radar_trackVerified",
  "ip": "1.2.3.4", "locatedIp": "1.2.3.4",
  "lat": 37.77, "lon": -122.41,
  "city": "...", "region": "...", "country_code": "US",
  "passed": true,
  "fraud": { "proxy": false, "mocked": false, "privacy": { "vpn": false, ... }, "asn": { ... } },
  "privacy": { "vpn": false, "proxy": false, "tor": false, ... },
  "asn": { "asn": "...", "type": "isp", ... },
  "failureReasons": [], "warningReasons": [] }
失败 / 无缓存
{ "ok": false, "score": -1, "cached": false, "error": "probe_failed" }
{ "ok": false, "score": -1, "cached": false, "error": "timeout" }
{ "ok": false, "score": -1, "cached": true,  "error": "no_cached_score" }
GETPOST/api/consistency

人设 / 网络一致性检查(独立方法 CVKNetworkConsistencyCheckRun)。对比:环境 sticky 出口国家、系统时区、伪装 GPS 与现场出口(默认只读探测,不写 GPS)、隧道网卡;并默认做 STUN 反射 IP≈WebRTC系统 DNS 探测。别名:/api/net/consistency

查询 / JSON 参数
?probe=1     // 缺省:现场探测出口地理(只读)
?probe=0     // 只用本地 sticky / 上次定位
?webrtc=1    // 缺省:UDP STUN → 反射公网 IP,与 HTTP 出口比对
?dns=1       // 缺省:读系统 DNS,对照出口国家
?radar=1     // 附带 Radar 出口分(阻塞)
?timeout=12  // 出口探测超时秒数
响应(节选)
{ "ok": true, "level": "green", "summary": "一致性良好",
  "checks": [
    { "id": "country", "title": "出口国家", "level": "green", "ok": true,
      "detail": "出口国家与环境记录一致", "expected": "US", "observed": "US" },
    { "id": "webrtc", "title": "WebRTC(STUN)", "level": "green", "ok": true,
      "detail": "STUN 反射 IP 与 HTTP 出口一致(…)" },
    { "id": "dns", "title": "DNS", "level": "green", "ok": true,
      "detail": "存在私网 DNS + 隧道网卡,较像隧道内解析" }
  ],
  "expected": { "environment": "...", "country_code": "US", "timezone": "...", ... },
  "observed": { "ip": "...", "country_code": "US", "vpn_ifaces": ["utun4"],
                "webrtc_srflx_ip": "...", "dns_servers": ["10.x.x.x"] },
  "webrtc": { "status": "ok", "srflx_ip": "...", "match": "same", "server": "stun.l.google.com" },
  "dns": { "status": "ok", "verdict": "private_with_tunnel", "servers": [ … ] } }
判定
level=green  全项通过
level=yellow 有警告(IP 已变 / 无隧道网卡 / STUN 失败 / 公共 DNS 等)
level=red    国家/时区不一致、STUN≠HTTP 出口、DNS 国家旁路,或出口探测失败
GETPOST/api/pp/weblogin

特例接口(仅 PayPal)。在当前激活环境里换取网页端免密登录地址,重建并发起 token_to_code 请求。
三级取参(按优先级自动回退):
keychain:实时 token_storage/PFAuthTokenService 里 App 当前在用的 UserAccessToken(A23AA)+idToken(随会话刷新、长效,推荐)+ 最近一条 proxy-auth/token 抓包的设备指纹重建 body;
proxy-auth/token 抓包:最近一条响应里的 UserAccessToken+idToken(仅 ~30 分钟)+ 同包指纹重建 body;
token_to_code 抓包:直接重放最近一条 token_to_code 抓包(整包复用 body+头,仅换 paypal-request-id)。
设备指纹(riskData/appInfo/deviceInfo/firstPartyClientId)与请求头(CMID/FPTI/ConsumerApp-Context/UA)取自来源抓包;channel=Web、tenantName=PayPal、redirectUri 为 token_to_code 固定值(可用 query/body 的 returnUrl 覆盖)。返回仅三个字段。

响应
{ "ok": true,
  "auth_url": "https://www.paypal.com/signin?intent=nativeWebSSO&...",
  "returnUrl": "" }

{ "ok": false, "auth_url": "", "returnUrl": "",
  "error": "no_source" }   // 失败时多带 error:paypal_container_not_found / no_source / request_failed / paypal_http_401 / no_auth_url 等
POST/api/pp/addmail

特例接口(仅 PayPal)。批量添加备用邮箱:对每个地址发起 update-account-profile,并强制 mandatory_email_confirmation=false(未验证邮箱也会出现在 profile 列表)。
凭据优先读 keychain UserAccessToken(A23AA);会话头取自最近 profile 抓包或 cvk_weblogin_params.jsonprofileHeaders(需先在 PayPal App 打开过资料页一次)。

请求体
{ "emails": [ "backup1@example.com", "backup2@example.com" ] }
响应
{ "ok": true,
  "results": [
    { "email": "backup1@example.com", "ok": true, "http_status": 200,
      "unique_id": "TJY3UWJANW3UE", "confirmed": false, "closure_id": "..." },
    { "email": "bad@x.com", "ok": false, "http_status": 403, "error": "PermissionDenied" }
  ] }

{ "ok": false, "error": "no_token", "results": [] }
{ "ok": false, "error": "no_profile_session", "hint": "...", "results": [] }
GET/api/spoof

读取某环境 env.plist 内的伪装数据(伪装了哪些项、各项的值)。?record= 缺省为当前激活环境。

响应
{ "record": "...", "isActive": true, "count": 12, "enabledCount": 9,
  "fields": [ { "key": "_pinnedDeviceModel", "label": "设备型号",
               "value": "iPhone14,3", "enabled": true }, ... ] }

网络拦截规则

GET/api/env/intercept/list?name=<环境>

列出指定环境的网络拦截/参数替换规则。name 缺省取当前激活环境。

响应
{ "environment": "...", "count": 2,
  "rules": [ { "url": "api.example.com/login", "key": "deviceId", "value": "替换值" }, ... ] }
POST/api/env/intercept/add

给指定环境添加拦截规则,支持单条或批量。name 缺省取当前激活环境;每条 url 必填,key/value 可空。

请求体(单条)
{ "name": "环境名", "url": "api.example.com/login", "key": "deviceId", "value": "替换值" }
// 也支持 query:?name=&url=&key=&value=
请求体(批量)
{ "name": "环境名", "rules": [ { "url": "api.paypal.com/graphql", "params": { "firstName": "Raji", "lastName": "Ramakrishnan", "isPartial": false } } ] }
// params 值请用 JSON 类型:布尔写 false/true(不要 ""),数字写 123。
// 或旧格式 { "url": "...", "key": "...", "value": "..." };也可直接 POST 数组,环境名取 ?name= 或当前激活环境
响应
{ "environment": "...", "added": 1, "count": 3, "rules": [...] }
错误
error说明
url_required单条缺 url
no_valid_rules批量里没有有效规则
unknown_environment环境不存在

Keychain

GET/api/keychain

实时枚举「当前激活环境(即此刻真机)」的 Keychain 条目,经 cvkkcd 跨组实时读取,不读备份文件。目标 App 取持久化的注入预设列表。

响应
{ "record": "...", "count": 8, "targets": ["com.xxx.app", ...],
  "items": [ { "agrp": "...", "class": "genp", "classLabel": "通用密码",
               "account": "...", "service": "...", "label": "...",
               "isSE": false, "bytes": 64, "kind": "...", "value": "...",
               "_raw": "" }, ... ] }
// 无注入目标时:{ ..., "error": "no_injection_targets" }
GET/api/keychain/exclude

读取 key 过滤名单:命中的 kSecClassKey 条目在备份/恢复时都会跳过。

响应
{ "needles": ["...", "..."] }
POST/api/keychain/exclude

覆盖写入 key 过滤名单。

请求体
{ "needles": ["needle1", "needle2"] }   // 也可直接 POST 一个数组
响应
{ "ok": true, "needles": [...], "error": null }
POST/api/keychain/delete

实时删除单条 keychain 条目。raw 取自 /api/keychain 列表行的 _raw

请求体
{ "raw": "<一条 cvksd1 jsonl>" }
响应
{ "ok": true, "deleted": 1 }

导出 / 导入

POST/api/export

把指定环境分别打包成 <环境名>.tar 写入导出目录(进程内 USTAR 打包)。

请求体
{ "names": ["env1", "env2"] }   // 也支持 ?name=env 或 {"name":"env"}
响应
{ "exported": ["env1"], "failed": ["env2 (原因)"],
  "exportPath": "/var/mobile/Media/CVKShadowKit/export",
  "exports": [{ "name": "env1.tar", "size": 12345 }] }
POST/api/delete

删除指定环境整棵目录。拒绝删除默认环境(原始机器)与当前激活环境。

请求体
{ "names": ["env1", "env2"] }   // 或查询参数 ?names=env1,env2
响应
{ "deleted": ["env1"], "failed": ["env2 (...)"], "current": "...", "environments": [...] }
POST/api/import

扫描导入目录下所有 .tar / .tar.gz / .tgz,逐个解压到环境目录(如 aaa.tardata/aaa/),同名旧环境会被覆盖。请求体为空。

响应
{ "imported": ["aaa"], "failed": [], "importPath": "...", "environments": [...] }
POST/api/upload?name=<文件名>.(tar|tar.gz|tgz)

上传单个归档(.tar / .tar.gz / .tgz)并立即解压成环境。请求体为原始归档字节(非 multipart),文件名取查询参数 name。支持大文件流式接收。

示例(curl)
curl -X POST --data-binary @aaa.tar \
  "http://<设备IP>:<端口>/api/upload?name=aaa.tar"
响应
{ "saved": "aaa.tar", "bytes": 1024, "imported": "aaa", "ok": true,
  "error": null, "environments": [...] }
POST/api/bundle

为某环境构建/查找匹配的导出归档(用于下载前拿到归档名与体积)。

请求体
{ "name": "环境名" }
响应
{ "bundle": "环境名.tar", "size": 12345, "members": ["..."] }
// 无匹配时:{ "error": "no_match" }
POST/api/bundledelete

删除导出目录中的某个归档文件。

请求体
{ "name": "环境名.tar" }
响应
{ "ok": true, "error": null }
POST/api/remoteimport

从另一台运行本服务的设备直接拉取并导入指定环境(设备到设备迁移)。

请求体
{ "ip": "192.168.x.x", "name": "环境名" }
响应
{ "ok": true, "host": "192.168.x.x", "imported": ["环境名"],
  "failed": [], "environments": [...] }
// 失败:{ "ok": false, "error": "...", "conflict_env": "...", ... }
GET/api/remoteimport/progress

查询正在进行的远程导入进度快照(供前端轮询)。

文件浏览 / 下载

GET/api/fs?path=<相对路径>

/rootfs/var/mobile/Media/CVKShadowKit 为根列目录(可见 data/export/import)。path 缺省为根;目录在前、按名排序。

响应
{ "path": "data", "items": [ { "name": "原始机器", "isDirectory": true, "size": 0 }, ... ] }
GET/api/fsfile?path=<相对路径>

下载浏览根目录下的某个文件(带越界防护,application/octet-stream)。

GET/api/list?record=<环境>&path=<相对路径>

列出某环境目录下的文件/子目录。path 可省略(即环境根)。

响应
[ { "name": "appData", "isDirectory": true, "size": 0 },
  { "name": "env.plist", "isDirectory": false, "size": 512 } ]
GET/api/download?record=<环境>&path=<相对路径>

下载环境目录中的某个文件(application/octet-stream,带 Content-Disposition)。

GET/api/dirfile?kind=export|import&name=<文件名>.tar

下载导出目录或导入目录中的 .tar 文件。

参数
参数说明
kindexportimport
name目标 .tar 文件名(不含路径)

粘贴板

GET/api/clipboard

读取手机系统粘贴板当前文本。App 在后台时可能返回空串,需把 App 置于前台再读。

响应示例
{
  "ok": true,
  "text": "hello",
  "length": 5,
  "changeCount": 12
}
POST/api/clipboard

写入手机系统粘贴板。JSON {"text":"..."} 或 raw UTF-8 纯文本;受通用 64KB body 上限约束。

请求示例
{ "text": "从电脑写入手机" }
响应示例
{
  "ok": true,
  "length": 8,
  "changeCount": 13
}