openapi: 3.0.3 info: title: Spark IM API version: 0.1.0 description: | 本规范描述 **HTTP REST**。即时消息长连接是另一套 **TCP Frame**(TLS + 23 字节头 + JSON Body),见文末「TCP 协议」。没有 Protobuf。 ## 环境与地址 公网只开放 `443`。Nginx 按 TLS SNI 分流:HTTP 证书打到 `sparkapi.wyrs365.com`,IM 证书打到 `sparkim.wyrs365.com`,对象存储(RustFS S3)打到 `sparkr.wyrs365.com`。进程本身只绑本机。 | 环境 | 用途 | HTTP Base URL | IM 长连接 | | --- | --- | --- | --- | | 生产 | App / 联调 | `https://sparkapi.wyrs365.com` | `sparkim.wyrs365.com:443`(TLS,SNI=`sparkim.wyrs365.com`) | | 本机开发 | 直连 Gin / gnet | `http://127.0.0.1:8080` | `127.0.0.1:8090`(明文,无 TLS) | Swagger 右上角 **Servers** 切换 HTTP 环境;Try it out 打的是 HTTP,不会建 IM 长连接。 文档: 1. HTTP API / Swagger:`/docs` 2. 枚举目录:`/docs/enums` 3. 错误码:`/docs/errors` --- ## 统一响应(HTTP 与 TCP Body 同一套) 所有业务响应 Body 都是: ```json {"code":"0","msg":"ok","request_id":"","data":{}} ``` | 字段 | 类型 | 规则 | | --- | --- | --- | | `code` | **字符串** | `"0"` 成功;其它失败。不要用数字 `0` 比较。全表见 [错误码](/docs/errors)。 | | `msg` | 字符串 | 给人看的文案,随请求语言变化(`zh-CN` / `en` / `zh-TW`)。成功一般为 `ok`。 | | `request_id` | 字符串 | 服务端生成,11 位 base62(`0-9A-Za-z`)。客户端不用传;从响应体读取。与 Header `X-Request-ID` 相同。 | | `data` | object / array | 成功时为业务对象;失败时固定 `{}`。字段缺失当空对象,不要当 `null`。 | **判定成功只看 `code === "0"`。** 不要只看 HTTP 状态码。 | 场景 | HTTP 状态 | envelope | | --- | --- | --- | | 业务成功 / 业务失败(参数、未登录、无权限等) | **200** | `code` 为 `"0"` 或 `"40001"` / `"40101"` / `"40301"` 等 | | `/readyz` 未就绪 | **503** | `code`=`"50301"`,`data` 仍有探活字段 | | panic / 未捕获 | 200 | `code`=`"50001"` | Snowflake(`uid` / `msg_id` / `conversation_id`)是 int64。Android/iOS/Java 用 `long`;JavaScript 不要用 `Number` 接,用字符串或 BigInt。 --- ## HTTP 请求:Header / Path / Query / Body ``` METHOD {base}{path}?{query} Header: 鉴权、语言、设备、追踪 Body: 仅 POST / PATCH 的 JSON(GET / DELETE 无 Body) ``` `Content-Type`:有 Body 时必须是 `application/json`。 ### Header(客户端可设) | Header | 放哪 | 必填 | 说明 | | --- | --- | --- | --- | | `Authorization` | Header | 需登录的接口必填 | `Bearer `。登录、发码、刷新、`/healthz`、`/readyz`、`/docs`、`/openapi.yaml`、`GET /v1/meta/enums` **不要**带。 | | `X-Device-ID` | Header | 可选 | 十进制设备 id,必须等于登录时 Body 里的 `device_id`(也等于 JWT `did`),否则 `40101`。 | | `X-Language` | Header | 可选 | `zh-CN` / `en` / `zh-TW`。优先于 `Accept-Language`。缺省 `zh-CN`。决定 `msg` 语言。 | | `Accept-Language` | Header | 可选 | 无 `X-Language` 时使用。 | | `X-App-Version` | Header | 可选 | 客户端版本,写入请求上下文。 | | `Content-Type` | Header | 有 Body 时必填 | `application/json`。 | **不要**把 token、语言、device_id 塞进业务 JSON(登录/刷新除外:登录 Body 带 `device_id`,刷新 Body 带 `refresh_token`)。 网关写入、客户端不要发:`X-Real-IP`、`X-Forwarded-For`(限流/审计用,见 C6)。 ### 响应 Header | Header | 说明 | | --- | --- | | `X-Request-ID` | 与响应体 `request_id` 相同,服务端生成,客户端不用传 | | `Content-Type` | `application/json; charset=utf-8` | ### Path / Query / Body | 位置 | 放什么 | 例子 | | --- | --- | --- | | Path | 资源 id | `/v1/friends/{uid}` 的 `uid`;`/v1/groups/{id}` 的群号;`/v1/messages/{id}` 的 MsgId | | Query | GET 筛选 / 分页 | `GET /v1/friends/lookup?email=`;`limit`;历史 `conversation_id`、`last_msg_id` | | Body | POST/PATCH 的业务字段 | 登录的 `email`/`verify_code`/`device_id`;发消息的 `content` | ### 调用顺序 1. (可选)`POST /v1/auth/email-codes` Body `{"email"}`。测试号 `test01@spark.com`~`test10@spark.com` 可跳过,登录任意 `verify_code`。 2. `POST /v1/auth/login` Body 拿 `access_token` / `refresh_token`。`device_id` 由客户端生成并持久化,不要每次换。 3. 其它 HTTP:Header `Authorization: Bearer …`,可选 `X-Device-ID`、`X-Language`。 4. 续期:`POST /v1/auth/token/refresh`,**Body** `{"refresh_token"}`,不要带 Bearer。 5. App 在线发消息、收推送、增量同步走 **TCP**,不要用 `POST /v1/messages` 当主路径。 请求示例(生产): ```http POST /v1/auth/login HTTP/1.1 Host: sparkapi.wyrs365.com Content-Type: application/json X-Language: zh-CN X-App-Version: 1.0.0 {"email":"test01@spark.com","verify_code":"000000","device_id":7,"device_type":"ios","device_name":"iPhone"} ``` ```http GET /v1/users/me HTTP/1.1 Host: sparkapi.wyrs365.com Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... X-Device-ID: 7 X-Language: zh-CN ``` ```http POST /v1/messages HTTP/1.1 Host: sparkapi.wyrs365.com Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... Content-Type: application/json X-Device-ID: 7 X-Language: zh-CN {"peer_id":1940000000000000001,"client_msg_id":"c-1","content":{"t":1,"text":{"body":"hi"}}} ``` 成功:`{"code":"0","msg":"ok","request_id":"...","data":{...}}`。失败:`{"code":"40101","msg":"未授权","request_id":"...","data":{}}`。 --- ## TCP 协议(生产 `sparkim.wyrs365.com:443`) 不是 WebSocket、不是 HTTP、不是 Protobuf。客户端对 `sparkim.wyrs365.com:443` 做 **TLS**(SNI 必须是该域名),握手完成后在 TLS 上写二进制 Frame。本机开发可直连 `127.0.0.1:8090` 明文。 心跳 30s 一次;空闲 120s 服务端断开。Frame 最大 64KB。媒体字节走 HTTP 上传,消息里只带 object key。 ### Frame(大端,固定 23 字节头) ``` offset 0 Magic uint16 0x5350(ASCII "SP") offset 2 Version uint8 1 offset 3 Command uint16 offset 5 Flags uint16 bit0=1 表示 Body 是 gzip offset 7 RequestID uint64 客户端追踪,可 0;响应原样带回 offset 15 SequenceID uint32 请求/响应配对;PUSH 推送用它做 PUSH_ACK offset 19 BodyLength uint32 Body 字节数(压缩后) offset 23 Body bytes UTF-8 JSON;Flags bit0 则先 gzip 再当 Body ``` 读包:先读 23 字节,按 `BodyLength` 再读 Body。`Magic` 不对或总长超过 64KB 应断开。 压缩:Body ≥ 512B 且 gzip 更小时可设 Flags bit0。未压缩 Flags=0。解压后仍不得超过 64KB。 ### Command
值名方向请求 Body响应 Command / Body
1AUTH客户端 → 服务端不是 envelope:{"token","device_id","app_version","language"}同 1,envelope,data={"user_id","device_id"}
2HEARTBEAT客户端 → 服务端空 Body同 2,envelope data={}
3GENERATE_MSG_ID客户端 → 服务端空同 3,data.msg_id
4SEND_MESSAGE客户端 → 服务端不是 envelope:与 HTTP POST /v1/messages 同一 JSON5 SEND_ACK,data={"msg_id","conversation_id","client_msg_id","server_time"}
5SEND_ACK服务端 → 客户端—仅作 4 的应答
6MESSAGE服务端 → 客户端—envelope,data 为消息 Item(msg_id / content / …)
7PUSH_ACK客户端 → 服务端空;SequenceID=推送帧的 SequenceID无业务响应
9RECALL客户端 → 服务端{"msg_id"}同 9,envelope
10SYNC_INBOX客户端 → 服务端{"mailbox_seq","dialog_score","limit"},均可 0同 10,data.dialogs / mailbox / 游标
12SYNC_MESSAGES客户端 → 服务端{"conversation_id","last_msg_id","limit"}同 12,data.items / has_more
14KICK服务端 → 客户端—同设备顶号,data.reason
15DELETE客户端 → 服务端{"msg_id"}同 15
除 AUTH 外,业务命令须先 AUTH,否则 `40102`。HEARTBEAT / PUSH_ACK 可以未登录。 **重要:** AUTH、SEND、SYNC、撤回/删除的 **请求 Body 是业务 JSON**,不要包 `{code,msg,request_id,data}`。服务端应答和 MESSAGE 推送的 Body 才是统一 envelope。失败时 Command 仍为请求 Command(发送失败不是 SEND_ACK),`code`≠`"0"`。 发送成功条件(C5):MySQL COMMIT **并且** 入队 `deliver:queue` 后才 SEND_ACK。未收到 ACK 用同一 `client_msg_id` 重试。发送端其它设备会收到 MESSAGE;本设备不回推。 上线增量:先 `SYNC_INBOX`(会话列表/未读 + 撤回删除邮箱),再对脏会话 `SYNC_MESSAGES`(`last_msg_id` 之后)。成功在线 MESSAGE 不进邮箱。
Java 示例(点击展开,生产域名) ```java import javax.net.ssl.*; import java.io.*; import java.net.*; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; public class SparkImClient { static final String API = "https://sparkapi.wyrs365.com"; static final String IM_HOST = "sparkim.wyrs365.com"; static final int IM_PORT = 443; static final int HDR = 23; public static void main(String[] args) throws Exception { String login = httpPost(API + "/v1/auth/login", "{\"email\":\"test01@spark.com\",\"verify_code\":\"000000\",\"device_id\":7,\"device_type\":\"android\"}"); String token = jsonString(login, "access_token"); // 从 data.access_token 取出 SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket s = (SSLSocket) f.createSocket(); SSLParameters p = s.getSSLParameters(); p.setServerNames(List.of(new SNIHostName(IM_HOST))); s.setSSLParameters(p); s.connect(new InetSocketAddress(IM_HOST, IM_PORT), 10_000); s.startHandshake(); DataOutputStream out = new DataOutputStream(s.getOutputStream()); DataInputStream in = new DataInputStream(s.getInputStream()); write(out, 1, 1, "{\"token\":\"" + token + "\",\"device_id\":7,\"app_version\":\"1.0.0\",\"language\":\"zh-CN\"}"); System.out.println("AUTH " + read(in)); write(out, 2, 2, ""); System.out.println("HB " + read(in)); write(out, 4, 3, "{\"peer_id\":1,\"client_msg_id\":\"c1\",\"content\":{\"t\":1,\"text\":{\"body\":\"hi\"}}}"); System.out.println("ACK " + read(in)); // Command=5 } static void write(DataOutputStream out, int cmd, int seq, String body) throws IOException { byte[] b = body.getBytes(StandardCharsets.UTF_8); ByteBuffer buf = ByteBuffer.allocate(HDR + b.length); buf.putShort((short) 0x5350).put((byte) 1).putShort((short) cmd).putShort((short) 0) .putLong(0).putInt(seq).putInt(b.length).put(b); out.write(buf.array()); out.flush(); } static String read(DataInputStream in) throws IOException { byte[] h = in.readNBytes(HDR); ByteBuffer buf = ByteBuffer.wrap(h); if (buf.getShort() != 0x5350) throw new IOException("magic"); buf.get(); int cmd = buf.getShort() & 0xffff; int flags = buf.getShort() & 0xffff; buf.getLong(); int seq = buf.getInt(); int n = buf.getInt(); byte[] body = in.readNBytes(n); if ((flags & 1) != 0) { try (java.util.zip.GZIPInputStream gz = new java.util.zip.GZIPInputStream(new ByteArrayInputStream(body))) { body = gz.readAllBytes(); } } return "cmd=" + cmd + " seq=" + seq + " " + new String(body, StandardCharsets.UTF_8); } static String httpPost(String url, String json) throws IOException { HttpURLConnection c = (HttpURLConnection) URI.create(url).toURL().openConnection(); c.setRequestMethod("POST"); c.setDoOutput(true); c.setRequestProperty("Content-Type", "application/json"); c.setRequestProperty("X-Language", "zh-CN"); try (OutputStream os = c.getOutputStream()) { os.write(json.getBytes(StandardCharsets.UTF_8)); } try (InputStream is = c.getInputStream()) { return new String(is.readAllBytes(), StandardCharsets.UTF_8); } } static String jsonString(String env, String key) { int i = env.indexOf("\"" + key + "\""); int a = env.indexOf('"', env.indexOf(':', i) + 1) + 1; return env.substring(a, env.indexOf('"', a)); } } ```
iOS (Swift) 示例(点击展开,生产域名) ```swift import Foundation import Network import Compression let api = URL(string: "https://sparkapi.wyrs365.com")! let imHost = NWEndpoint.Host("sparkim.wyrs365.com") let imPort = NWEndpoint.Port(443)! struct Envelope: Decodable { let code: String; let msg: String; let request_id: String } struct LoginEnv: Decodable { struct Data: Decodable { let access_token: String } let code: String; let data: Data } func login() throws -> String { var req = URLRequest(url: api.appendingPathComponent("/v1/auth/login")) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("zh-CN", forHTTPHeaderField: "X-Language") req.httpBody = """ {"email":"test01@spark.com","verify_code":"000000","device_id":7,"device_type":"ios","device_name":"iPhone"} """.data(using: .utf8) let data = try URLSession.shared.send(req) return try JSONDecoder().decode(LoginEnv.self, from: data).data.access_token } func frame(cmd: UInt16, seq: UInt32, body: Data) -> Data { var d = Data(capacity: 23 + body.count) func u16(_ v: UInt16) { d.append(UInt8(v >> 8)); d.append(UInt8(v & 0xff)) } func u32(_ v: UInt32) { d.append(UInt8(v >> 24)); d.append(UInt8(v >> 16 & 0xff)) d.append(UInt8(v >> 8 & 0xff)); d.append(UInt8(v & 0xff)) } u16(0x5350); d.append(1); u16(cmd); u16(0) d.append(contentsOf: [UInt8](repeating: 0, count: 8)); u32(seq); u32(UInt32(body.count)); d.append(body) return d } // TLS:NWParameters.tls,SNI 随 Host 自动带 sparkim.wyrs365.com let conn = NWConnection(host: imHost, port: imPort, using: .tls) conn.start(queue: .main) // 连接 ready 后: // let token = try login() // let auth = #"{"token":"\#(token)","device_id":7,"app_version":"1.0.0","language":"zh-CN"}"# // conn.send(content: frame(cmd: 1, seq: 1, body: Data(auth.utf8)), completion: .idempotent) // 收包:先 23 字节,再 BodyLength;Flags & 1 则 gunzip。看 JSON code == "0"。 // HEARTBEAT cmd=2;SEND_MESSAGE cmd=4,应答 cmd=5;推送 cmd=6 后发 PUSH_ACK cmd=7。 ```
本机 TCP 把 Host/Port 换成 `127.0.0.1:8090`,并用 **明文 TCP**(不要 TLS)。HTTP 仍用 `http://127.0.0.1:8080`。 仓库可运行示例:`docs/clients/SparkImClient.java`、`docs/clients/SparkImClient.swift`。 servers: - url: https://sparkapi.wyrs365.com description: 生产 - url: http://127.0.0.1:8080 description: 本机开发 security: - bearerAuth: [] tags: - name: enums description: "[打开枚举页](/docs/enums)" externalDocs: description: 打开枚举页 url: /docs/enums - name: errors description: "[打开错误码页](/docs/errors)" externalDocs: description: 打开错误码页 url: /docs/errors - name: system - name: meta description: 客户端枚举 JSON。`GET /v1/meta/enums`,无需登录。 - name: auth - name: users - name: friends - name: groups - name: conversations - name: messages - name: files - name: favorites - name: moments paths: /healthz: get: tags: [system] security: [] summary: 进程探活 operationId: healthz responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: status: ok /readyz: get: tags: [system] security: [] summary: 就绪探活(MySQL / Redis / 是否接流量) description: | 进程存活用 `/healthz`(始终 200)。 `/readyz` 在未接流量、依赖不可用或关停中返回 HTTP 503,`code=50301`。 Nginx / 编排应摘流看本接口,不要看 `/healthz`。 operationId: readyz responses: "200": description: 已就绪 content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: status: ok accepting: true mysql: ok redis: ok taskpool: ok "503": description: 未就绪 content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "50301" msg: 服务不可用 data: status: not_ready accepting: false mysql: skipped redis: skipped taskpool: ok /docs: get: tags: [system] security: [] summary: HTTP API 文档页(Swagger UI) description: | 文档入口: 1. 本页 `/docs` — 接口(Servers 切换环境) 2. `/docs/enums` — 枚举,左侧目录可跳转 3. `/docs/errors` — 错误码(envelope.code) operationId: docsUI responses: "200": description: HTML content: text/html: schema: type: string /docs/enums: get: tags: [enums] security: [] summary: 枚举目录页 description: | 拉取 `GET /v1/meta/enums`,按集合列出 value / name / label。 左侧有序列表点击跳到对应集合(`#enum-{set}`,例如 `/docs/enums#enum-message_type`)。 错误码见 `/docs/errors`。 operationId: docsEnums responses: "200": description: HTML content: text/html: schema: type: string /docs/errors: get: tags: [errors] security: [] summary: 错误码页 description: | envelope.code 全表,与 HTTP/TCP 同一套。`code` 为字符串,`"0"` 成功。 左侧点击跳到对应码(`#code-40101`)。数据来自 `GET /v1/meta/enums` 的 `envelope_code`。 operationId: docsErrors responses: "200": description: HTML content: text/html: schema: type: string /openapi.yaml: get: tags: [system] security: [] summary: OpenAPI 规范原文 operationId: openAPISpec responses: "200": description: YAML content: application/yaml: schema: type: string /v1/meta/enums: get: tags: [meta] security: [] summary: 类型 / 状态枚举目录 description: | 客户端接入用。无需登录。`label` 随 `X-Language` / `Accept-Language` 变化。 `value` 与 JSON/TCP/SQL 落库值一致(数字集合为 number,字符串集合为 string)。 `name` 是稳定英文标识,不随语言变化。人读目录:`/docs/enums`(类型/状态)、`/docs/errors`(envelope.code)。 operationId: listEnums responses: "200": description: data 为 EnumCatalog content: application/json: schema: $ref: "#/components/schemas/EnumsEnvelope" /v1/auth/email-codes: post: tags: [auth] security: [] summary: 发送邮箱登录验证码 description: | 验证码 5 分钟有效,同一邮箱 60 秒内只能发一次。 测试账号 `test01@spark.com`~`test10@spark.com` 无需发码,登录时任意填写 `verify_code`。 当前不发送真实邮件,验证码写入 Redis(无 Redis 时为进程内存)。 operationId: sendEmailCode requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SendEmailCodeRequest" example: email: ada@example.com responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" examples: ok: value: code: "0" msg: ok data: expires_in: 300 /v1/auth/login: post: tags: [auth] security: [] summary: 邮箱验证码登录并签发 JWT description: | 首次验证成功会创建用户(Snowflake uid、默认 spark_id、shortid 个人二维码票据,不含 uid)。 登录写入/更新 `user_devices`(device_type 存在 `platform` 列、device_name、app_version、language、last_ip),并追加 `user_login_logs`。 返回 access_token 与 refresh_token。测试账号任意验证码即可。 `device_type` 必填:ios / android / harmony / windows / macos / web(也接受旧字段 `platform`)。 Header(可选):`X-Language` / `Accept-Language`、`X-App-Version`。不要带 `Authorization`。 Body(JSON):`email`、`verify_code`、`device_id`、`device_type`、`device_name`。`device_id` 只在登录 Body,之后改走 Header `X-Device-ID` 与 TCP AUTH。 operationId: login parameters: - $ref: "#/components/parameters/X-Language" - $ref: "#/components/parameters/Accept-Language" - $ref: "#/components/parameters/X-App-Version" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" example: email: test01@spark.com verify_code: "000000" device_id: 7 device_type: ios device_name: iPhone responses: "200": description: 统一 envelope。成功时 data 为 LoginData;失败时 data 为 {} content: application/json: schema: $ref: "#/components/schemas/LoginEnvelope" examples: ok: value: code: "0" msg: ok data: access_token: eyJhbGciOiJIUzI1NiJ9... refresh_token: eyJhbGciOiJIUzI1NiJ9... expires_in: 604800 refresh_expires_in: 2592000 user_id: 1940000000000000000 device_id: 7 spark_id: s1a2b3c unauthorized: value: code: "40101" msg: 未授权 data: {} invalid: value: code: "40001" msg: 请填写验证码 data: {} /v1/auth/token/refresh: post: tags: [auth] security: [] summary: 用 refresh_token 续期 description: | 校验 refresh_token 后签发新的 access/refresh,旧一对立即失效(jti denylist)。 不需要 access Bearer。按 RealIP 限流。 operationId: refreshToken requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RefreshRequest" example: refresh_token: eyJhbGciOiJIUzI1NiJ9... responses: "200": description: 与登录相同的 LoginData content: application/json: schema: $ref: "#/components/schemas/LoginEnvelope" /v1/auth/logout: post: tags: [auth] summary: 退出当前设备 description: 吊销该设备的 access/refresh,并踢掉同 device_id 的 TCP 连接。 operationId: logout responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/users/me: get: tags: [users] summary: 当前用户资料 description: | Header:`Authorization: Bearer `(必填),可选 `X-Device-ID`、`X-Language`。无 Body。 operationId: getMe parameters: - $ref: "#/components/parameters/X-Device-ID" - $ref: "#/components/parameters/X-Language" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: uid: 1940000000000000000 spark_id: alice_ok nickname: test01 avatar: perm/avatar/20260917/3H8kPq2mN1Rabcde.png qrcode: "-NDveu-9Q" birthday: "" gender: 0 status: 1 last_login_time: 1726550000000 last_active_time: 1726550000000 create_time: 1726550000000 update_time: 1726550000000 patch: tags: [users] summary: 修改资料 description: 可改 spark_id、nickname、avatar、gender(0/1/2)、birthday(YYYY-MM-DD)。未出现的字段不改。 operationId: patchMe requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatchMeRequest" example: nickname: Ada gender: 2 birthday: "1994-05-01" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/lookup: get: tags: [friends] summary: 按邮箱 / Spark账号 / 二维码查找用户 description: | 查询参数 `email`、`spark_id`、`qrcode`、`uid` 四选一。 二维码为 shortid 个人扫描票据,不含 uid。对方已拉黑当前用户时返回 40401。 operationId: lookupFriend parameters: - in: query name: email schema: { type: string, format: email } - in: query name: spark_id schema: { type: string } - in: query name: qrcode schema: { type: string, example: "-NDveu-9Q" } - in: query name: uid schema: { type: integer, format: int64 } responses: "200": description: 统一 envelope,data 为好友卡片(含 relation:0无 1好友 2已拉黑) content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/requests: get: tags: [friends] summary: 收到的待处理好友申请 operationId: listFriendRequests parameters: - in: query name: limit schema: { type: integer, default: 20, maximum: 100 } responses: "200": description: data.items content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [friends] summary: 发起好友申请 description: | 通过扫描二维码、搜索邮箱或 spark_id 发起。可填申请理由与备注。 同一对用户只保留一条申请,重复发起覆盖理由/备注,不新增行。 若对方已有待处理申请指向自己,则自动成为双向好友并写入一条系统消息。 不支持单向好友。 operationId: applyFriend requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FriendApplyRequest" example: email: test02@spark.com reason: 我是同事 remark: 同事 responses: "200": description: data 见 FriendApplyData。status 为 1 待处理或 2 已同意(与 friend_requests.status 同值,不是字符串) content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends: get: tags: [friends] summary: 好友列表 operationId: listFriends parameters: - in: query name: limit schema: { type: integer, default: 20, maximum: 100 } responses: "200": description: data.items,置顶好友排在前面;每项含 remark、blocked、pinned content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [friends] summary: 同意好友申请 description: 成为双向好友后自动在单聊中插入一条 `kind=friend.became` 系统消息并入队 deliver。 operationId: acceptFriend requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FriendAcceptRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/{uid}: get: tags: [friends] summary: 查看好友资料 operationId: getFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [friends] summary: 删除好友 description: 解除双向关系,删除双方各一条好友边。 operationId: deleteFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/{uid}/remark: put: tags: [friends] summary: 设置好友备注 operationId: setFriendRemark parameters: - $ref: "#/components/parameters/FriendUID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FriendRemarkRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/{uid}/pin: put: tags: [friends] summary: 置顶好友会话 description: 仅写入自己这一侧的好友边 is_pinned,不影响对方。 operationId: pinFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [friends] summary: 取消置顶 operationId: unpinFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/{uid}/block: put: tags: [friends] summary: 拉黑 description: | 单向拉黑,写在自己这一侧的好友边上,不解除好友关系。 任一侧拉黑后双方都不能发消息。被拉黑一方再申请时视为用户不存在。 operationId: blockFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [friends] summary: 取消拉黑 operationId: unblockFriend parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/{uid}/reports: post: tags: [friends] summary: 投诉好友 operationId: reportFriend parameters: - $ref: "#/components/parameters/FriendUID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FriendReportRequest" example: reason: 1 detail: spam responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/groups: get: tags: [groups] summary: 我加入的群 operationId: listGroups parameters: - in: query name: limit schema: { type: integer, default: 20, maximum: 100 } responses: "200": description: data.items,id 为自增群号,conversation_id 为 Snowflake 会话 id content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [groups] summary: 创建群 description: | `id` 为表内自增群号。发消息、同步使用返回的 `conversation_id`(Snowflake)。 只能拉好友入群。群主无法直接退群,需先转让。 建群成功后服务端按实际人数拼接成员已有头像(注册默认或自定义),最多 9 人;两人就是并排两格,不会凑九宫格。 成员头像拉不到时用字母占位图,整张拼图失败则改用群名默认头像,`avatar` 不会为空。 写入 `perm/group/{群号}/`。之后可用 `purpose=group_avatar` 直传自定义头像再 PATCH。 operationId: createGroup requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGroupRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/groups/{id}: get: tags: [groups] summary: 群资料 operationId: getGroup parameters: - $ref: "#/components/parameters/GroupID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" patch: tags: [groups] summary: 修改群名称 / 头像 / 公告 / 全员禁言 description: 仅群主和管理员。`announcement` 为空字符串表示清空公告。 operationId: updateGroup parameters: - $ref: "#/components/parameters/GroupID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateGroupRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [groups] summary: 解散群(仅群主) operationId: dissolveGroup parameters: - $ref: "#/components/parameters/GroupID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/groups/{id}/members: get: tags: [groups] summary: 群成员 operationId: listGroupMembers parameters: - $ref: "#/components/parameters/GroupID" - in: query name: limit schema: { type: integer, default: 200, maximum: 200 } responses: "200": description: data.items content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [groups] summary: 邀请好友入群 operationId: inviteGroupMembers parameters: - $ref: "#/components/parameters/GroupID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InviteGroupMembersRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/groups/{id}/members/{uid}: patch: tags: [groups] summary: 设管理员、禁言、群昵称、群备注 description: | `role` 仅群主可改(2=管理员 3=普通成员)。`speak_muted` 群主/管理员可对成员设置;管理员不能禁言其他管理员。 `nickname` 是我在群里的昵称(自己可改;管理员也可改他人)。`remark` 是群备注,仅自己可改。 operationId: updateGroupMember parameters: - $ref: "#/components/parameters/GroupID" - $ref: "#/components/parameters/FriendUID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateGroupMemberRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [groups] summary: 踢人,或 uid 为自己时退群 operationId: removeGroupMember parameters: - $ref: "#/components/parameters/GroupID" - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/groups/{id}/transfer: post: tags: [groups] summary: 转让群主 operationId: transferGroup parameters: - $ref: "#/components/parameters/GroupID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TransferGroupRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/files: post: tags: [files] summary: 签发对象存储直传凭证 description: | 客户端只传 `purpose` / `mime` / `size`,服务端生成 object_key 并签发短时 Presigned PUT。 文件直传 RustFS,不经过 Gin。 `avatar` 写入 `perm/avatar/{yyyyMMdd}/{name}.{ext}`(不过期,路径不含 uid); `group_avatar` 写入 `perm/group/{群号}/{name}.{ext}`(需 `group_id`,群主/管理员); 聊天按类型:`chat/image|voice|video|gif|file/{yyyyMMdd}/{name}.{ext}`(桶生命周期按前缀过期)。 朋友圈:`moment_image` / `moment_video` / `moment_cover` 写入 `perm/moment/{image|video|cover}/{yyyyMMdd}/{name}.{ext}`(不过期,删帖/换封面时删对象)。 `{name}` 为服务端 16 位 base62 随机串,不占用 Snowflake。 发图片:客户端压成 JPEG/PNG 后 `POST /v1/files/batch` 直传缩略图、压缩大图、原图(未勾选原图只传前两项)。 服务端不转码。iOS HEIC、Android WebP 须客户端先转 JPEG/PNG,两端才能互看。 客户端 PUT 必须原样带返回的 `headers`。消息只存 object_key。 operationId: createUploadCredential requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUploadRequest" example: purpose: chat_image mime: image/jpeg size: 245760 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: object_key: chat/image/20260917/3H8kPq2mN1Rabcde.jpg upload_url: https://sparkr.wyrs365.com/spark/chat/image/20260917/3H8kPq2mN1Rabcde.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256 method: PUT headers: Content-Type: image/jpeg Content-Length: "245760" If-None-Match: "*" expires_in: 900 /v1/files/batch: post: tags: [files] summary: 批量签发直传凭证 description: | 一次返回 1~3 个 Presigned PUT,顺序与 `items` 相同。聊天图片请按「缩略图、压缩大图、原图」排列;未勾选原图只传前两项。 mime 仅 `image/jpeg` / `image/png`(HEIC、WebP 请客户端先转)。限流按条数计入 `upload_per_second`。文件仍直传 RustFS。 operationId: createUploadCredentials requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUploadBatchRequest" example: items: - purpose: chat_image mime: image/jpeg size: 8192 - purpose: chat_image mime: image/jpeg size: 245760 - purpose: chat_image mime: image/jpeg size: 3500000 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: items: - object_key: chat/image/20260917/3H8kPq2mN1Rabcde.jpg method: PUT headers: Content-Type: image/jpeg Content-Length: "8192" If-None-Match: "*" - object_key: chat/image/20260917/4kPq2mN1RabcdeXY.jpg method: PUT - object_key: chat/image/20260917/5mN1RabcdeXY12ab.jpg method: PUT /v1/files/downloads: post: tags: [files] summary: 签发对象存储下载凭证 description: 已登录用户用 object_key 换取短时 Presigned GET。key 必须是 `perm/` 或 `chat/` 前缀。 operationId: createDownloadCredential requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDownloadRequest" example: object_key: chat/20260917/1/2.jpg responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/favorites: get: tags: [favorites] summary: 收藏列表 operationId: listFavorites parameters: - in: query name: limit schema: type: integer default: 20 maximum: 100 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [favorites] summary: 收藏一条消息 description: | 会话成员可将消息收入收藏。图片/语音/视频/文件会 CopyObject 到 `perm/fav/{yyyyMMdd}/{name}.{ext}`。 源对象已从 `chat/` 过期删除则失败(`code=40402`),不提供本地补传。 同一 `msg_id` 重复收藏返回已有记录。撤回不影响已收藏快照。 operationId: createFavorite requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateFavoriteRequest" example: msg_id: 1 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/favorites/{id}: delete: tags: [favorites] summary: 取消收藏 operationId: deleteFavorite parameters: - $ref: "#/components/parameters/FavoriteID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments: get: tags: [moments] summary: 好友朋友圈 Feed description: | `cursor` 为上一页最后一条 `post_id`,0 表示最新。读时按好友、拉黑、不看他/不让他看、可见范围过滤。 不写会话 inbox。 operationId: listMomentFeed parameters: - in: query name: cursor schema: { type: integer, format: int64, default: 0 } - in: query name: limit schema: { type: integer, default: 20, maximum: 50 } responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [moments] summary: 发朋友圈 description: | 纯文,或最多 9 张图,或 1 条视频(图/视频互斥)。媒体先 `POST /v1/files`(`moment_image` / `moment_video`)再填 key。 `visibility`:0 好友公开 1 私密 2 白名单 3 黑名单。2/3 需 `audience_uids`(好友,最多 200)。 operationId: publishMoment requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublishMomentRequest" example: text: hello visibility: 0 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/unread: get: tags: [moments] summary: 发现页红点 operationId: momentUnread responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/read: put: tags: [moments] summary: 更新发现页已读水位 operationId: momentMarkRead requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MomentReadRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/notices: get: tags: [moments] summary: 赞评通知列表 operationId: listMomentNotices parameters: - in: query name: cursor schema: { type: integer, format: int64, default: 0 } - in: query name: limit schema: { type: integer, default: 20, maximum: 50 } responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/notices/read: post: tags: [moments] summary: 通知标已读 operationId: readMomentNotices requestBody: content: application/json: schema: $ref: "#/components/schemas/MomentNoticeReadRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/settings: get: tags: [moments] summary: 朋友圈设置 operationId: getMomentSettings responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" patch: tags: [moments] summary: 改封面或可见天数 operationId: patchMomentSettings requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PatchMomentSettingsRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/settings/hides/{uid}: put: tags: [moments] summary: 不看他 / 不让他看 operationId: putMomentHide parameters: - $ref: "#/components/parameters/FriendUID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MomentHideRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [moments] summary: 清除对该好友的隐藏 operationId: deleteMomentHide parameters: - $ref: "#/components/parameters/FriendUID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/users/{uid}: get: tags: [moments] summary: 某人朋友圈相册 description: 非好友返回 404。看自己不过可见天数,能看到私密帖。 operationId: listMomentAlbum parameters: - $ref: "#/components/parameters/FriendUID" - in: query name: cursor schema: { type: integer, format: int64, default: 0 } - in: query name: limit schema: { type: integer, default: 20, maximum: 50 } responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/{id}: get: tags: [moments] summary: 单条动态 operationId: getMoment parameters: - $ref: "#/components/parameters/MomentID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [moments] summary: 删除自己的动态 operationId: deleteMoment parameters: - $ref: "#/components/parameters/MomentID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/{id}/like: put: tags: [moments] summary: 点赞 operationId: likeMoment parameters: - $ref: "#/components/parameters/MomentID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [moments] summary: 取消赞 operationId: unlikeMoment parameters: - $ref: "#/components/parameters/MomentID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/{id}/comments: post: tags: [moments] summary: 评论或回复 operationId: commentMoment parameters: - $ref: "#/components/parameters/MomentID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MomentCommentRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/moments/{id}/comments/{cid}: delete: tags: [moments] summary: 删除评论 operationId: deleteMomentComment parameters: - $ref: "#/components/parameters/MomentID" - name: cid in: path required: true schema: { type: integer, format: int64 } responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/conversations: get: tags: [conversations] summary: 会话列表增量(离线后上线) description: | 与 TCP `SYNC_INBOX` 同一 Application。`dialog_score` 是 Redis 会话 ZSET 游标(score=MsgId), `mailbox_seq` 是撤回/删除等控制事件游标。成功在线推送的新消息不进 mailbox,正文用 `GET /v1/messages?last_msg_id=` 或 `SYNC_MESSAGES` 按 MsgId 补齐。 Header:`Authorization`(必填),可选 `X-Device-ID`、`X-Language`。 Query:`dialog_score`、`mailbox_seq`、`limit`。无 Body。 operationId: syncInbox parameters: - $ref: "#/components/parameters/X-Device-ID" - $ref: "#/components/parameters/X-Language" - in: query name: dialog_score required: false schema: type: integer format: int64 default: 0 - in: query name: mailbox_seq required: false schema: type: integer format: int64 default: 0 - in: query name: limit required: false schema: type: integer default: 200 maximum: 200 responses: "200": description: data 含 dialogs / mailbox / dialog_score / mailbox_seq / has_more content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/messages: get: tags: [messages] summary: 会话历史 description: | `id > last_msg_id` 升序,最多 200 条,`has_more` 表示还有更新消息。 私聊读 `private_message`(含 `receiver_id`),群聊读 `group_message`(含 `group_id` 群号)。 不含当前用户已删除的消息;已撤回的消息 `status=2` 仍返回。 长连接优先 `SYNC_MESSAGES`。 Header:`Authorization`(必填),可选 `X-Device-ID`、`X-Language`。 Query:`conversation_id`、`last_msg_id`、`limit`。无 Body。 operationId: listMessages parameters: - $ref: "#/components/parameters/X-Device-ID" - $ref: "#/components/parameters/X-Language" - in: query name: conversation_id required: true schema: type: integer format: int64 - in: query name: last_msg_id required: false schema: type: integer format: int64 default: 0 - in: query name: limit required: false schema: type: integer default: 200 maximum: 200 responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" post: tags: [messages] summary: 发消息(调试/非长连接;App 在线发送用 TCP) description: | **客户端在线发消息走 TCP `SEND_MESSAGE`(Command=4)**,不是本接口。对方实时收消息走 TCP `MESSAGE` 推送。 本接口与 TCP 调用同一套 Application(落库 + deliver 入队),给调试、单测、尚未接 TCP 的页面用。HTTP 发完不会变成长连接,也收不到实时推送。 `conversation_id` 与 `peer_id` 至少填一个。单聊写入 `private_message`,群聊写入 `group_message`。 回复在 `content.reply_to`。群聊 `content.at_uids` 可@成员;`content.at_all` 仅群主/管理员可@所有人。 成功 data 含 `msg_id`、`conversation_id`、`client_msg_id`、`server_time`。 ACK 仅在 MySQL COMMIT 且 deliver 入队成功后返回(与 TCP `SEND_ACK` 同一条件)。 Header:`Authorization: Bearer `(必填)、可选 `X-Device-ID`、`X-Language`、`X-App-Version`。 Body(JSON):`conversation_id` 或 `peer_id`、`client_msg_id`、`content`。token / device_id / 语言不放 Body。 operationId: sendMessage parameters: - $ref: "#/components/parameters/X-Device-ID" - $ref: "#/components/parameters/X-Language" - $ref: "#/components/parameters/X-App-Version" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SendMessageRequest" example: peer_id: 2 client_msg_id: "c-1" content: t: 1 reply_to: 0 text: body: hello responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/messages/{id}/recall: post: tags: [messages] summary: 撤回消息 description: 仅发送者、2 分钟内。全员可见 `status=2`,经 deliver 下发 `RECALL_MESSAGE`。 operationId: recallMessage parameters: - $ref: "#/components/parameters/MessageID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/messages/{id}: delete: tags: [messages] summary: 删除消息(仅自己) description: 写入 `message_deletes`,对端历史不变。其他设备经 `DELETE_MESSAGE` 同步隐藏。 operationId: deleteMessage parameters: - $ref: "#/components/parameters/MessageID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | HTTP Header:`Authorization: Bearer `。 `access_token` 来自 `POST /v1/auth/login` 的 `data.access_token`。TCP AUTH Body 的 `token` 用同一值(不是 Header)。 schemas: Gender: type: integer enum: [0, 1, 2] description: users.gender。0未知 1男 2女 UserStatus: type: integer enum: [0, 1] description: users.status。0停用 1正常 ConversationType: type: integer enum: [1, 2] description: conversations.type。1单聊 2群聊 MessageKind: type: integer enum: [1, 2] description: 消息分表,与 ConversationType 同值。1=private_message 2=group_message MessageType: type: integer enum: [1, 2, 3, 4, 5, 6, 7, 8, 9] description: messages.message_type / content.t。1文本 2图片 3语音 4视频 5表情 6GIF 7文件 8位置 9系统 MessageStatus: type: integer enum: [1, 2, 3] description: private_message/group_message.status。1正常 2已撤回 3审核隐藏 RelationKind: type: integer enum: [0, 1, 2] description: 查看者视角 JSON relation(不是 friend_relations.status)。0无 1好友 2已拉黑 FriendRequestStatus: type: integer enum: [1, 2, 3] description: friend_requests.status,申请结果 status 同此。1待处理 2已同意 3已拒绝 FriendSource: type: integer enum: [0, 1, 2, 3] description: 好友添加来源。0未知 1二维码 2邮箱 3spark_id ReportReason: type: integer enum: [1, 2, 3, 4] description: friend_reports.reason。1垃圾 2骚扰 3不实身份 4其他 GroupStatus: type: integer enum: [1, 2] description: groups.status。1正常 2已解散 GroupRole: type: integer enum: [1, 2, 3] description: group_members.role。1群主 2管理员 3成员。PATCH 成员只能设 2 或 3 MailboxEventType: type: integer format: int16 enum: [1, 2, 3] description: mailbox_events.event_type。1漏推消息 2撤回 3删除 DeviceType: type: string enum: [ios, android, harmony, windows, macos, web] Language: type: string enum: [zh-CN, en, zh-TW] description: 缺省 zh-CN UploadPurpose: type: string enum: [avatar, group_avatar, chat_image, chat_voice, chat_video, chat_gif, chat_file, moment_image, moment_video, moment_cover] SystemKind: type: string enum: [friend.became, group.created, group.invite, group.leave, group.kick, group.dissolve, group.transfer] description: content.system.kind,服务端系统消息 MomentKind: type: integer enum: [1, 2, 3] description: moment_posts.kind。1纯文 2图 3视频 MomentVisibility: type: integer enum: [0, 1, 2, 3] description: moment_posts.visibility。0好友公开 1私密 2白名单 3黑名单 MomentStatus: type: integer enum: [0, 1] description: moment_posts.status。0正常 1已删 MomentNoticeKind: type: integer enum: [1, 2, 3] description: moment_notices.kind。1赞 2评论 3回复 MomentVisibleDays: type: integer enum: [0, 3, 30, 180] description: moment_settings.visible_days。0全部 3/30/180 天 PublishMomentRequest: type: object properties: text: type: string maxLength: 1000 images: type: array maxItems: 9 items: $ref: "#/components/schemas/MomentImage" video: $ref: "#/components/schemas/MomentVideo" visibility: $ref: "#/components/schemas/MomentVisibility" audience_uids: type: array maxItems: 200 items: type: integer format: int64 poi: $ref: "#/components/schemas/MomentPOI" MomentImage: type: object required: [key] properties: key: { type: string } mime: { type: string } w: { type: integer } h: { type: integer } size: { type: integer, format: int64 } thumb_key: { type: string } orig_key: { type: string } MomentVideo: type: object required: [key, duration_ms] properties: key: { type: string } mime: { type: string } w: { type: integer } h: { type: integer } duration_ms: { type: integer, maximum: 30000 } size: { type: integer, format: int64 } cover_key: { type: string } MomentPOI: type: object properties: lat: { type: number, format: double } lng: { type: number, format: double } name: { type: string } address: { type: string } MomentReadRequest: type: object required: [post_id] properties: post_id: { type: integer, format: int64 } MomentNoticeReadRequest: type: object properties: cursor: { type: integer, format: int64 } PatchMomentSettingsRequest: type: object properties: cover_key: { type: string } visible_days: $ref: "#/components/schemas/MomentVisibleDays" MomentHideRequest: type: object properties: hide_him: { type: boolean } hide_me: { type: boolean } MomentCommentRequest: type: object required: [text] properties: text: { type: string, maxLength: 500 } reply_to_id: { type: integer, format: int64 } TcpCommand: type: integer enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] description: | Frame.Command:1 AUTH 2 HEARTBEAT 3 GENERATE_MSG_ID 4 SEND_MESSAGE 5 SEND_ACK 6 MESSAGE 7 PUSH_ACK 8 READ 9 RECALL 10 SYNC_INBOX 11 SYNC_INBOX_FINISH 12 SYNC_MESSAGES 13 SYNC_MESSAGES_FINISH 14 KICK 15 DELETE_MESSAGE EnvelopeCode: type: string enum: ["0", "40001", "40002", "40003", "40004", "40101", "40102", "40301", "40401", "40402", "40901", "42901", "42902", "42903", "49901", "50001", "50002", "50003", "50301", "50401"] description: envelope.code。成功为 "0" Envelope: type: object required: [code, msg, request_id, data] description: | HTTP 与 TCP 应答 Body 的统一信封。业务成败看 `code` 字符串,不看 HTTP 状态(除 `/readyz` 503)。 失败时 `data` 为 `{}`。成功时 `data` 为对象或列表,不会是 `null`。 `request_id` 由服务端生成,客户端不用传;与 Header `X-Request-ID` 相同。 properties: code: allOf: - $ref: "#/components/schemas/EnvelopeCode" description: 字符串。`"0"` 成功,其它失败。不要用 JSON number。 example: "0" msg: type: string description: 展示文案,随 `X-Language` / `Accept-Language` / TCP AUTH `language` 变化。 example: ok request_id: type: string description: 服务端生成的本次请求 id(11 位 base62,`0-9A-Za-z`)。客户端不用传,从响应体读取。 example: 3H8kPq2mN1R data: type: object additionalProperties: true description: 成功为业务对象;失败为 {} default: {} EnumsEnvelope: type: object required: [code, msg, request_id, data] properties: code: $ref: "#/components/schemas/EnvelopeCode" msg: type: string request_id: type: string data: $ref: "#/components/schemas/EnumCatalog" EnumItem: type: object required: [value, name, label] properties: value: description: 与 JSON/TCP/SQL 一致。数字集合为 integer,字符串集合为 string。 oneOf: - type: integer - type: string name: type: string description: 稳定英文标识,不随语言变化 label: type: string description: 当前请求语言下的展示文案 EnumCatalog: type: object properties: gender: type: array items: $ref: "#/components/schemas/EnumItem" user_status: type: array items: $ref: "#/components/schemas/EnumItem" conversation_type: type: array items: $ref: "#/components/schemas/EnumItem" message_kind: type: array items: $ref: "#/components/schemas/EnumItem" message_type: type: array items: $ref: "#/components/schemas/EnumItem" message_status: type: array items: $ref: "#/components/schemas/EnumItem" relation_kind: type: array items: $ref: "#/components/schemas/EnumItem" friend_request_status: type: array items: $ref: "#/components/schemas/EnumItem" friend_source: type: array items: $ref: "#/components/schemas/EnumItem" report_reason: type: array items: $ref: "#/components/schemas/EnumItem" group_status: type: array items: $ref: "#/components/schemas/EnumItem" group_role: type: array items: $ref: "#/components/schemas/EnumItem" mailbox_event_type: type: array items: $ref: "#/components/schemas/EnumItem" tcp_command: type: array items: $ref: "#/components/schemas/EnumItem" envelope_code: type: array items: $ref: "#/components/schemas/EnumItem" device_type: type: array items: $ref: "#/components/schemas/EnumItem" language: type: array items: $ref: "#/components/schemas/EnumItem" upload_purpose: type: array items: $ref: "#/components/schemas/EnumItem" system_kind: type: array items: $ref: "#/components/schemas/EnumItem" SendEmailCodeRequest: type: object required: [email] properties: email: type: string format: email LoginRequest: type: object required: [email, verify_code, device_id] properties: email: type: string format: email verify_code: type: string description: 邮箱验证码。测试账号 test01@spark.com~test10@spark.com 可任意填写。 device_id: type: integer format: int64 minimum: 1 device_type: $ref: "#/components/schemas/DeviceType" platform: allOf: - $ref: "#/components/schemas/DeviceType" description: device_type 的兼容字段 device_name: type: string maxLength: 128 LoginData: type: object required: [access_token, refresh_token, expires_in, refresh_expires_in, user_id, device_id, spark_id] properties: access_token: type: string refresh_token: type: string expires_in: type: integer format: int64 description: access TTL in seconds (default 604800 = 7 days) example: 604800 refresh_expires_in: type: integer format: int64 description: refresh TTL in seconds (default 2592000 = 30 days) example: 2592000 user_id: type: integer format: int64 description: Snowflake user id device_id: type: integer format: int64 spark_id: type: string description: Public handle, unique and user-changeable RefreshRequest: type: object required: [refresh_token] properties: refresh_token: type: string PatchMeRequest: type: object properties: spark_id: type: string minLength: 6 maxLength: 32 pattern: "^[a-zA-Z][a-zA-Z0-9_]{5,31}$" nickname: type: string minLength: 1 maxLength: 64 avatar: type: string maxLength: 512 gender: $ref: "#/components/schemas/Gender" birthday: type: string description: YYYY-MM-DD;空字符串表示清除 example: "1994-05-01" FriendApplyRequest: type: object properties: uid: type: integer format: int64 email: type: string format: email spark_id: type: string qrcode: type: string description: 个人二维码扫描票据(shortid),不含 uid reason: type: string maxLength: 128 remark: type: string maxLength: 64 FriendApplyData: type: object required: [status, uid] properties: status: $ref: "#/components/schemas/FriendRequestStatus" request_id: type: integer format: int64 description: status=1 时返回,待处理申请 id uid: type: integer format: int64 description: 对方 Snowflake uid FriendAcceptRequest: type: object required: [request_id] properties: request_id: type: integer format: int64 remark: type: string maxLength: 64 FriendRemarkRequest: type: object properties: remark: type: string maxLength: 64 FriendReportRequest: type: object required: [reason] properties: reason: $ref: "#/components/schemas/ReportReason" detail: type: string maxLength: 512 CreateUploadRequest: type: object required: [purpose, mime, size] properties: purpose: $ref: "#/components/schemas/UploadPurpose" mime: type: string example: image/jpeg description: | avatar / group_avatar / chat_image 仅 image/jpeg、image/jpg、image/png。 iOS HEIC、Android WebP 须客户端转成 JPEG/PNG 再申请凭证。 size: type: integer format: int64 minimum: 1 group_id: type: integer format: int64 minimum: 1 description: purpose=group_avatar 时必填,为自增群号 CreateUploadBatchRequest: type: object required: [items] properties: items: type: array minItems: 1 maxItems: 3 items: $ref: "#/components/schemas/CreateUploadRequest" CreateDownloadRequest: type: object required: [object_key] properties: object_key: type: string maxLength: 512 SendMessageRequest: type: object required: [client_msg_id, content] properties: conversation_id: type: integer format: int64 peer_id: type: integer format: int64 description: 单聊对方 uid;与 conversation_id 至少填一个 client_msg_id: type: string maxLength: 64 msg_id: type: integer format: int64 description: 可选,须为 ±5 分钟内的 Snowflake content: type: object required: [t] properties: t: $ref: "#/components/schemas/MessageType" reply_to: type: integer format: int64 description: 被回复消息 MsgId text: type: object properties: body: type: string image: type: object description: | t=2。点开看 `key`(压缩预览);气泡用 `thumb_key`;`orig_key` 有则显示「查看原图」。 三档由客户端压成 JPEG/PNG 后 `POST /v1/files/batch` 直传。服务端不转码;不要传 HEIC/WebP。 required: [key] properties: key: type: string description: 点开查看用的压缩图 object_key thumb_key: type: string description: 气泡缩略图,可省略 orig_key: type: string description: 原图 object_key;空则没有「查看原图」 orig_size: type: integer format: int64 description: 原图像素文件大小,供「查看原图 3.2MB」按钮 orig_w: type: integer orig_h: type: integer mime: type: string w: type: integer h: type: integer size: type: integer format: int64 at_all: type: boolean description: "@所有人,仅群主/管理员" at_uids: type: array maxItems: 50 items: type: integer format: int64 description: "@的群成员 uid" system: type: object properties: kind: $ref: "#/components/schemas/SystemKind" body: type: string CreateFavoriteRequest: type: object required: [msg_id] properties: msg_id: type: integer format: int64 minimum: 1 CreateGroupRequest: type: object required: [name, member_uids] properties: name: type: string minLength: 1 maxLength: 32 member_uids: type: array minItems: 1 maxItems: 199 items: type: integer format: int64 UpdateGroupRequest: type: object properties: name: type: string minLength: 1 maxLength: 32 avatar: type: string muted_all: type: boolean announcement: type: string maxLength: 1024 InviteGroupMembersRequest: type: object required: [uids] properties: uids: type: array minItems: 1 items: type: integer format: int64 UpdateGroupMemberRequest: type: object properties: role: allOf: - $ref: "#/components/schemas/GroupRole" description: 仅 2=管理员 3=普通成员;群主只能通过转让变更 speak_muted: type: boolean nickname: type: string maxLength: 32 remark: type: string maxLength: 32 description: 对该群的备注,仅自己可改 TransferGroupRequest: type: object required: [uid] properties: uid: type: integer format: int64 LoginEnvelope: type: object required: [code, msg, request_id, data] properties: code: type: string msg: type: string request_id: type: string data: $ref: "#/components/schemas/LoginData" AuthData: type: object properties: user_id: type: integer format: int64 device_id: type: integer format: int64 MsgIDData: type: object properties: msg_id: type: integer format: int64 headers: X-Request-ID: schema: type: string description: 服务端生成的本次请求 id。与响应体 `request_id` 相同。客户端不用传。 X-Device-ID: schema: type: string description: Optional. Must match JWT did when present. X-App-Version: schema: type: string Accept-Language: schema: $ref: "#/components/schemas/Language" example: zh-CN description: 用户语言。也可用 X-Language。缺省 zh-CN。 X-Language: schema: $ref: "#/components/schemas/Language" description: 覆盖 Accept-Language 的简洁语言标签。 X-Real-IP: schema: type: string description: Nginx PROXY / SNI 写入的客户端真实 IP,进入 reqctx.RealIP。 parameters: Authorization: in: header name: Authorization required: true schema: type: string example: Bearer eyJhbGciOiJIUzI1NiJ9... description: Header。`Bearer `。登录/发码/刷新/探活/文档/枚举不要带。 X-Device-ID: in: header name: X-Device-ID required: false schema: type: string example: "7" description: Header。十进制 device_id,须等于登录 Body 的 device_id / JWT did。不放 Body。 X-App-Version: in: header name: X-App-Version required: false schema: type: string example: 1.0.0 description: Header。客户端版本。登录时也可在 Body 带 app_version;此后用本 Header。 Accept-Language: in: header name: Accept-Language required: false schema: $ref: "#/components/schemas/Language" description: Header。用户语言。缺省 zh-CN。无 X-Language 时生效。 X-Language: in: header name: X-Language required: false schema: $ref: "#/components/schemas/Language" description: Header。覆盖 Accept-Language。决定 envelope.msg 语言。不放 Body。 FriendUID: in: path name: uid required: true schema: type: integer format: int64 description: 对方 Snowflake uid。 FavoriteID: in: path name: id required: true schema: type: integer format: int64 description: 收藏 Snowflake id。 MomentID: in: path name: id required: true schema: type: integer format: int64 description: 朋友圈 Snowflake post_id。 GroupID: in: path name: id required: true schema: type: integer format: int64 description: 自增群号(不是 conversation_id)。 MessageID: in: path name: id required: true schema: type: integer format: int64 description: Snowflake MsgId。