API 参考

HTTP 路由

PlainApp 在 POST /graphql 的 GraphQL API 之外还提供了一小组 HTTP 端点。这些端点处理文件服务、分片上传、zip 流式传输、DLNA 投屏、WebSocket 事件和系统控制。所有文件/上传/zip 端点都需要 c-id 头(通常还需要加密的 id 查询参数)。

类型定义

bash
# Common headers for protected HTTP endpoints:
c-id: <client-id>
# /fs, /proxyfs, /zip/* use an encrypted "id" query parameter produced
# by the GraphQL files()/fileIds() queries. The id is opaque to the client.

# Upload endpoints additionally require an encrypted "info" multipart part:
#   info = chaCha20Encrypt(token, json({ dir, size, replace, isAppFile }))
#   file = <raw bytes>
# Chunked uploads use:
#   info = chaCha20Encrypt(token, json({ fileId, index, size }))

# Status codes follow HTTP semantics:
#   200 OK              – success
#   201 Created         – upload stored
#   204 No Content      – init pending password
#   400 Bad Request     – missing/invalid params
#   401 Unauthorized    – bad token
#   403 Forbidden       – expired/invalid encrypted id
#   404 Not Found       – file/entry missing
#   410 Gone            – server shutting down
#   429 Too Many Requests – concurrent zip limit hit

操作

http

POST /graphql

主要 GraphQL 端点。在此发送所有 GraphQL 查询和变更,需携带 c-id + Authorization 头。/peer_graphql 端点是已配对设备之间使用的点对点等价端点。

bash
curl --request POST \
  --url http://192.168.1.100:8080/graphql \
  --header 'c-id: <client-id>' \
  --header 'Authorization: Bearer <api-token>' \
  --header 'Content-Type: application/json' \
  --data '{"query":"{ app { battery deviceName } }"}'
http

GET /fs

提供文件、content:// URI 或包图标服务。id 查询参数是由 files()/fileIds() 返回的加密字符串。支持缩略图生成(?w=&h=&cc=)、用于低吞吐量传输的字节范围(?offset=&length=)、HEIF→PNG 转换、3gp→MP4 转码,以及带 Content-Disposition 附件头的下载模式(dl=1)。

bash
# Original (full size, inline)
curl http://192.168.1.100:8080/fs?id=<encrypted-id> -o file.jpg

# Thumbnail (center-cropped to 200x200)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&w=200&h=200' -o thumb.jpg

# Download (attachment)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&dl=1' -OJ

# Byte range (BLE chunked download)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&offset=0&length=4096' \
  -o chunk.bin
http

GET /proxyfs

代理 peer HTTP URL。id 查询参数解密为已配对 peer 设备上的完整 http(s) URL;服务器将上游响应流式传回。用于通过 Wi-Fi Aware 进行点对点文件下载。

bash
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpg
http

POST /upload

通过 multipart/form-data 上传单个文件。"info" 部分(ChaCha20 加密的 JSON)必须位于 "file" 部分之前。当 isAppFile=true 时,字节被导入内容寻址的 AppFileStore(按哈希去重);否则文件被写入 info.dir/<fileName>。返回最终文件名(在避免覆盖时可能带有 "(1)" 后缀)。

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/local.jpg'
http

POST /upload_chunk

上传可恢复分片上传中的一个分片。每个分片写入磁盘上的 upload_tmp/{fileId}/chunk_{index}。返回 "<index>:<savedSize>"。所有分片到达后,调用 mergeChunks GraphQL 变更以组装最终文件。

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload_chunk \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-chunk-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/chunk_0.bin'
http

GET /zip/dir

将单个目录作为 zip 归档流式传输。id 查询参数解密为目录路径。设备上同一时间只运行一个 zip 操作——并发请求会收到 HTTP 429。

bash
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
  -o folder.zip
http

GET /zip/files

将多个文件(或媒体搜索结果)作为单个 zip 流式传输。id 解密为 { type, query, id, name }。对于 FILE 类型,文件列表由 GraphQL files() 查询存储在 TempHelper 下的 request.id 中;对于媒体类型,服务器运行 searchZipItems(type, query, id)。

bash
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
  -o selection.zip
http

GET /media/{id}

DLNA 媒体端点。将先前注册的媒体路径(通过 UrlHelper.getMediaHttpUrl 注册)提供给电视 / DLNA 渲染器。URL 源被代理,content:// URI 被流式传输,图片按原样提供,所有其他文件以 DLNA 特定头 + HTTP 206 范围支持提供,以便渲染器接受流。

bash
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4
http

NOTIFY /callback/cast

DLNA 渲染器回调。接收渲染器的事件 NOTIFY XML 并更新 CastPlayer 状态。在 TransportState=STOPPED(无 AVTransportURIMetaData)时,播放器自动前进到播放列表的下一项。还解析 RelTime / TrackDuration 用于位置更新。

bash
# Sent by the DLNA renderer (not by the client):
NOTIFY /callback/cast HTTP/1.1
Content-Type: text/xml

<?xml ...><e:propertyset>...TransportState val="PLAYING"...</e:propertyset>
http

GET /health

无需认证的健康检查端点。以纯文本形式返回应用包名。用于验证 HTTP 服务器是否可达。

bash
curl http://192.168.1.100:8080/health
http

GET /shutdown

关闭 HTTP 服务器。仅可从 localhost 调用——远程请求收到 HTTP 403。关闭所有 WebSocket 会话,清除在线客户端集合,并释放 HTTP 服务器。

bash
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdown
http

POST /init

初始化客户端会话。需要 c-id 头。如果存在有效的加密请求体(用缓存的 token 解密),会话被视为已认证,服务器响应 200。否则:如果未设置密码,服务器响应一个全新重置的密码(200);如果已设置密码,响应 204 No Content,客户端必须通过 WebSocket 登录流程进行认证。

bash
curl --request POST \
  --url http://192.168.1.100:8080/init \
  --header 'c-id: <client-id>' \
  --data-binary '<encrypted-token-bytes>'
http

WS /

主 WebSocket 端点。使用 ?cid=<client-id> 注册会话;第一个二进制帧必须是加密的时间戳(或使用 ?auth=1 时为包含密码的加密 AuthRequest)。认证后,服务器通过此 socket 推送加密事件帧(通知、聊天更新等)。

bash
# Browser / JS example
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>')
ws.binaryType = 'arraybuffer'
ws.send(encryptWithToken(token, Date.now().toString()))

// Login variant:
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>&auth=1')
ws.send(encryptWithPassword(password, JSON.stringify({ password: hash })))
http

WS /status

Peer 在线状态 WebSocket。使用 ?cid=<peer-id> 保持已配对 peer 标记为在线。第一个二进制帧必须是 PeerChatParser 加密的请求;成功时服务器发送 "ok" 文本并将 peer 标记为在线,直到 socket 关闭。

bash
# Peer-to-peer keepalive (typically opened automatically by the chat
# client after pairing — not usually invoked manually)
const ws = new WebSocket('ws://192.168.1.100:8080/status?cid=<peer-id>')
ws.send(peerEncryptedFrame)