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`、`GET /v1/music/playlist`、`GET /v1/music/settings` **不要**带。 | | `X-Device-ID` | Header | 可选 | 十进制 `device_id`,须等于登录响应 / JWT `did`(**服务端按物理指纹生成**,客户端勿自造)。不一致则 `40101`。 | | `X-App-ID` | Header | 可选 | 若传,须与 JWT **`aid`(app_id)** 一致。 | | `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`、`app_id` 塞进业务 JSON(登录 / 刷新除外:登录 Body 带 `app_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?q=`;`limit`;历史 `conversation_id`、`last_msg_id` | | Body | POST/PATCH 的业务字段 | 登录的 `email`/`verify_code`/`app_id`/`device_type`/`idfv` 或 `android_id`;发消息的 `content` | ### 调用顺序 1. (可选)`POST /v1/sms/codes` Header **`X-Language` 必填**(`zh-CN` / `en` / `zh-TW`),Body `{"purpose":1,"target":"邮箱或手机号"}`。封控见 [docs/sms.md](../../../docs/sms.md)(IP 桶 + 同 target 60s 冷却,均 `42902`)。测试号 `test01@spark.com`~`test100@spark.com` 可跳过发码。 2. `POST /v1/auth/login` Body 拿 `access_token` / `refresh_token`。**不要**传客户端自造 `device_id`;传 **`app_id`**、**`device_type`(1–6)** 与 **`idfv` / `android_id`**,由服务端生成 `device_id`。持久化响应里的 `device_id` 与 `app_id`。 3. 其它 HTTP:Header `Authorization: Bearer …`,可选 `X-Device-ID`(= JWT `did`)、`X-App-ID`(= JWT `aid`)、`X-Language`。 4. 续期:`POST /v1/auth/token/refresh`,**Body** `{"refresh_token"}`,不要带 Bearer;新 token 保留相同 `did` / `aid`。 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","app_id":1,"device_type":1,"idfv":"00000000-0000-0000-0000-000000000007","device_name":"iPhone"} ``` ```http GET /v1/users/me HTTP/1.1 Host: sparkapi.wyrs365.com Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... X-Device-ID: 7 X-App-ID: 1 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"};device_id 可选但须等于 JWT did;不必传 app_id(以 JWT aid 为准)同 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\",\"app_id\":1,\"device_type\":2,\"android_id\":\"YOUR_ANDROID_ID\"}"); 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","app_id":1,"device_type":1,"idfv":"00000000-0000-0000-0000-000000000007","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: 生产(IM / 音乐客户端) - url: http://127.0.0.1:8080 description: 本机开发 - url: https://popadmin.wyrs365.com description: popadmin(音乐管理 Admin API) - url: https://admin.fullstorynovel.com description: FullStoryNovel 小说 CMS / 公开 API - url: https://fullstorynovel.com description: FullStoryNovel 官网(公开 /api 同源时可切换) 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 - name: square - name: music description: | 运营音乐列表。客户端公开读 `GET /v1/music/playlist`、`GET /v1/music/settings`(无需登录,V3 envelope)。 - name: admin-music description: | popadmin 音乐管理:`/api/admin/spark/music*`(`/api/admin/pop/music*` 别名)。 Admin JWT;响应 `{success,message,data}`。Host 常用 `https://popadmin.wyrs365.com`。 - name: novels description: | FullStoryNovel 公开小说 API(App/官网)。生产基址见 servers 中 fullstorynovel。 响应多为直接 JSON(非 V3 envelope)。 - name: admin-novels description: | FullStoryNovel 小说 CMS(8095)。生产 `https://admin.fullstorynovel.com`。 CMS Bearer;或用 wyrs Admin JWT 调 `POST /api/admin/adopt-ops-session` 换取。 popadmin「小说内容管理」iframe 嵌入该后台。 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/sms/codes: post: tags: [sms] security: [] summary: 发送验证码 description: | Body:`purpose` 1=登录 2=注销;`target` 为邮箱或大陆 11 位手机号(服务端识别,无需 channel)。 **Header `X-Language` 必填**(不接受仅 Accept-Language),用于邮件主题/正文。 登录发码无需 Bearer;注销发码需 Bearer,且 `target` 须为当前绑定邮箱(不支持手机号注销发码)。 **封控(失败均为 envelope `42902`,HTTP 仍 200)** 1. **按客户端 IP**(`RealIP`,依赖网关 `X-Real-IP` / PROXY):令牌桶,速率 `rate_limit.auth_per_second`(默认 5/s),与 `POST /v1/auth/login`、`POST /v1/auth/token/refresh` **共用**同一 IP 桶。 2. **按 target**:同一「渠道 + purpose + 规范化 target」**60 秒内**只能成功触发一次发信/发短信;Redis 键 `sms:cd:*`(无 Redis 时为进程内内存)。 不同 purpose、不同邮箱/手机号互不影响。成功响应 `data.expires_in` 固定 **300**(码有效期秒数,与 60s 冷却无关)。 测试邮箱 `test01@spark.com`~`test100@spark.com` 不发信且不占用 target 冷却。详情 [docs/sms.md](../../../docs/sms.md)。 邮箱经 SMTP;手机走阿里云短信(按号段选运营商模板)。 operationId: sendVerificationCode parameters: - $ref: "#/components/parameters/X-Language" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SendVerificationCodeRequest" example: purpose: 1 target: ada@example.com responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/auth/login: post: tags: [auth] security: [] summary: 邮箱验证码登录并签发 JWT description: | 首次验证成功会创建用户(Snowflake uid、默认 spark_id、shortid 个人二维码票据,不含 uid)。 登录 UPSERT `user_devices` 与 `devices`,并追加 `user_login_logs`(含 app_id)。 返回 access_token 与 refresh_token;`device_id` 由服务端生成。测试账号任意验证码即可。 `device_type` 必填:整数 1–6(见 meta `device_type`);可选 `platform` 同枚举作别名。 **设备模型(Breaking,见 `docs/devices-client.md`)**: - Body **不要**再传客户端自造 `device_id`。 - **必填** `app_id`(整数,如 Spark IM=`1`)、`device_type`(整数 1–6,见 meta / `AppId` / `DeviceType`)。 - 物理指纹:ios/macos 必填 `idfv`;android/harmony 必填 `android_id`;windows/web 至少一个。 - 服务端按 `SHA256("{device_type}:{idfv|android_id}")` 派生 `device_id`(同机多 App **共用**同一 `device_id`)。 - JWT access/refresh 含 `sub`(user_id)、`did`(device_id)、**`aid`(app_id)**、`use`。 - 响应 `data` 含服务端 `device_id` 与 `app_id`;客户端须持久化后用于 Header / TCP。 - 可选 `platform`:与 `device_type` 同枚举,仅当未传 `device_type` 时作别名;可选 `device_name`。 首次验证成功会创建用户。测试账号任意验证码即可。 Header(可选):`X-Language` / `Accept-Language`、`X-App-Version`。不要带 `Authorization`。 Body:`email`、`verify_code`、`app_id`、`device_type`、`idfv`/`android_id`、可选 `device_name`。 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" app_id: 1 device_type: 1 idfv: "00000000-0000-0000-0000-000000000007" 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 app_id: 1 spark_id: s1a2b3c0 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)。 新 token 对保留相同 **`did` / `aid`**(与首次登录 App 绑定)。 不需要 access Bearer。按 RealIP 限流。Body 仅 `refresh_token`。 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: | 吊销当前 **`(user, device, app)`** 会话(默认 `triple`:各 App 独立 refresh,互不踢)。 若运维切到 `pair` 模式,同设备其它 App 的 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-App-ID`、`X-Language`。无 Body。 `avatar` 是 object_key,可匿名 GET `https://sparkr.wyrs365.com/spark/{avatar}`。 operationId: getMe parameters: - $ref: "#/components/parameters/X-Device-ID" - $ref: "#/components/parameters/X-App-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(8-16 位,字母开头)、nickname、avatar、gender(0/1/2)、birthday(YYYY-MM-DD)。未出现的字段不改。已有超范围 spark_id 可继续使用,改号须符合新规则。 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" delete: tags: [users] summary: 注销当前账号 description: | 吊销全部设备的 JWT 并踢掉 TCP。将 `users` 行迁入 `user_delete` 后删除原行(邮箱可重新注册)。 非测试账号需先 `POST /v1/sms/codes`(`purpose=2`,`target` 为绑定邮箱)发码,再在本接口提交 `verify_code`(遵守发码封控,见 docs/sms.md)。 同时清除好友关系、朋友圈、收藏、群成员(群主把群转给一名管理员,没有管理员则转给成员表第一位后退出;仅剩自己则解散)、会话成员、收件箱 Redis 键与 mailbox。 对方仍可见历史消息(不删 `messages`)。 operationId: deleteMe requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/DeleteAccountRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/friends/lookup: get: tags: [friends] summary: 查找用户 description: | 查询参数 `q`:邮箱则按邮箱查;纯数字按 uid 查;其余先按 spark_id,未命中再按 qrcode。 二维码为 shortid 个人扫描票据,不含 uid。对方已拉黑当前用户时返回 40401。 operationId: lookupFriend parameters: - in: query name: q required: true schema: { type: string } description: 邮箱 / uid / Spark账号 / 二维码 example: test02@spark.com 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,匿名 GET); `group_avatar` 写入 `perm/group/{群号}/{name}.{ext}`(需 `group_id`,群主/管理员,匿名 GET); 聊天按类型:`chat/image|voice|video|gif|file/{yyyyMMdd}/{name}.{ext}`(桶生命周期按前缀过期)。 朋友圈:`moment_image` / `moment_video` / `moment_cover` 写入 `perm/moment/{image|video|cover}/{yyyyMMdd}/{name}.{ext}`(不过期,删帖/换封面时删对象)。 广场:`square_image` / `square_video` 写入 `perm/square/{image|video}/{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/` 前缀。九宫格请用 `POST /v1/files/downloads/batch`。 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/files/downloads/batch: post: tags: [files] summary: 批量签发下载凭证 description: | 一次返回 1~9 个 Presigned GET,顺序与 `object_keys` 相同。朋友圈九宫格一次拿齐。 限流按请求计入,不按条数。文件仍直传 RustFS GET。 operationId: createDownloadCredentials requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDownloadBatchRequest" example: object_keys: - perm/moment/image/20260918/a.jpg - perm/moment/image/20260918/b.jpg responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" example: code: "0" msg: ok data: items: - object_key: perm/moment/image/20260918/a.jpg download_url: https://sparkr.wyrs365.com/spark/perm/moment/image/20260918/a.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256 method: GET expires_in: 900 - object_key: perm/moment/image/20260918/b.jpg method: GET /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。 浏览九宫格:`POST /v1/files/downloads/batch` 一次换 9 张 Presigned GET。 `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/square/posts: get: tags: [square] summary: 全站广场 Feed description: | `cursor` 为上一页最后一条 `post_id`,0 表示最新。仅 `visibility` 公开与不给谁看、且审核通过的帖; 读时按拉黑、黑名单过滤。评论随帖详情返回,无单独评论列表接口。 operationId: listSquareFeed 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: [square] summary: 发广场帖 description: | 内容与朋友圈相同。媒体先 `POST /v1/files`(`square_image` / `square_video`)再填 key。 `visibility`:0 全站公开 1 仅自己 2 白名单 3 黑名单。2/3 需 `audience_uids`(双向好友,最多 200)。 operationId: publishSquarePost requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublishMomentRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/posts/{id}: get: tags: [square] summary: 单条广场帖 operationId: getSquarePost parameters: - $ref: "#/components/parameters/SquarePostID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [square] summary: 删除自己的广场帖 operationId: deleteSquarePost parameters: - $ref: "#/components/parameters/SquarePostID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/posts/{id}/like: put: tags: [square] summary: 点赞 operationId: likeSquarePost parameters: - $ref: "#/components/parameters/SquarePostID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" delete: tags: [square] summary: 取消赞 operationId: unlikeSquarePost parameters: - $ref: "#/components/parameters/SquarePostID" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/posts/{id}/comments: post: tags: [square] summary: 评论或回复 operationId: commentSquarePost parameters: - $ref: "#/components/parameters/SquarePostID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MomentCommentRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/posts/{id}/comments/{cid}: delete: tags: [square] summary: 删除评论 operationId: deleteSquareComment parameters: - $ref: "#/components/parameters/SquarePostID" - name: cid in: path required: true schema: { type: integer, format: int64 } responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/users/{uid}/posts: get: tags: [square] summary: 某用户广场帖 description: 按当前浏览者与帖可见性、拉黑过滤;看自己可见私密与白名单帖。 operationId: listSquareUserPosts 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/square/notices: get: tags: [square] summary: 赞评通知列表 operationId: listSquareNotices 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/square/notices/unread: get: tags: [square] summary: 广场互动未读角标 operationId: squareNoticeUnread responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/square/notices/read: post: tags: [square] summary: 通知标已读 operationId: readSquareNotices requestBody: content: application/json: schema: $ref: "#/components/schemas/MomentNoticeReadRequest" responses: "200": description: 统一 envelope content: application/json: schema: $ref: "#/components/schemas/Envelope" /v1/music/playlist: get: tags: [music] summary: 音乐列表(公开) description: | 返回已上架曲目与全局「会员音乐免费试听秒数」。无需登录。 封面/音频为 sparkr 公网 URL(`https://sparkr.wyrs365.com/spark/perm/music/...`)。 客户端应按 `requireMember` 与 `memberPreviewSeconds` 控制试听。 operationId: getMusicPlaylist security: [] responses: "200": description: 统一 envelope,`data` 为 MusicPlaylist content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - type: object properties: data: $ref: "#/components/schemas/MusicPlaylist" /v1/music/settings: get: tags: [music] summary: 音乐全局配置(公开) description: 仅返回 `memberPreviewSeconds`(会员曲免费试听秒数)。无需登录。 operationId: getMusicSettings security: [] responses: "200": description: 统一 envelope content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - type: object properties: data: $ref: "#/components/schemas/MusicSettings" /api/admin/spark/music: get: tags: [admin-music] summary: 管理端曲目列表 description: | popadmin 同源调用。`data.items` 为全部曲目(含下架),`data.settings` 为全局试听配置。 别名:`GET /api/admin/pop/music`。 operationId: adminListMusic security: - bearerAuth: [] responses: "200": description: "AdminApiResponse:success、message、data(含 items 与 settings)" content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" post: tags: [admin-music] summary: 新建曲目 operationId: adminCreateMusic security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MusicUpsertRequest" responses: "200": description: data 为 MusicTrack(AdminApiResponse) content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" /api/admin/spark/music/settings: get: tags: [admin-music] summary: 读取全局试听时长 operationId: adminGetMusicSettings security: - bearerAuth: [] responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" put: tags: [admin-music] summary: 更新全局试听时长 description: Body `{"memberPreviewSeconds":0..3600}` operationId: adminPutMusicSettings security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MusicSettings" responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" /api/admin/spark/music/{id}: patch: tags: [admin-music] summary: 更新曲目元数据 operationId: adminUpdateMusic security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MusicUpsertRequest" responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" delete: tags: [admin-music] summary: 删除曲目(并尝试删除对象存储文件) operationId: adminDeleteMusic security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" /api/admin/spark/music/{id}/cover: post: tags: [admin-music] summary: 上传封面(multipart file) description: 字段名 `file`;jpg/png/webp/gif,≤5MB。写入 RustFS `perm/music/cover/...`。 operationId: adminUploadMusicCover security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" /api/admin/spark/music/{id}/audio: post: tags: [admin-music] summary: 上传 mp3(multipart file) description: 字段名 `file`;audio/mpeg,≤50MB。写入 RustFS `perm/music/audio/...`。 operationId: adminUploadMusicAudio security: - bearerAuth: [] parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": content: application/json: schema: $ref: "#/components/schemas/AdminApiResponse" # —— FullStoryNovel 公开小说 API(基址选 admin.fullstorynovel.com 或 fullstorynovel.com)—— /api/home: get: tags: [novels] summary: 首页聚合(精选/热门/新书/更新/分类) operationId: fsnHome servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] responses: "200": description: HomeDto 直接 JSON content: application/json: schema: $ref: "#/components/schemas/FsnHome" /api/genres: get: tags: [novels] summary: 分类列表 operationId: fsnGenres servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] parameters: - name: tree in: query schema: { type: boolean } description: true 时返回一级含 children 树 responses: "200": content: application/json: schema: type: array items: $ref: "#/components/schemas/FsnGenre" /api/novels: get: tags: [novels] summary: 小说分页搜索 operationId: fsnListNovels servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] parameters: - { name: status, in: query, schema: { type: string } } - { name: language, in: query, schema: { type: string } } - { name: genre, in: query, schema: { type: string }, description: 一级或二级 slug } - { name: q, in: query, schema: { type: string } } - { name: sort, in: query, schema: { type: string, default: popular } } - { name: page, in: query, schema: { type: integer, default: 0 } } - { name: size, in: query, schema: { type: integer, default: 24 } } responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnNovelPage" /api/novels/{slug}: get: tags: [novels] summary: 小说详情 operationId: fsnGetNovel servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] parameters: - name: slug in: path required: true schema: { type: string } responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnNovelCard" /api/novels/{slug}/chapters: get: tags: [novels] summary: 章节目录 operationId: fsnListChapters servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] parameters: - name: slug in: path required: true schema: { type: string } responses: "200": content: application/json: schema: type: array items: $ref: "#/components/schemas/FsnChapterMeta" /api/novels/{novelSlug}/chapters/{chapterSlug}: get: tags: [novels] summary: 章节正文 operationId: fsnGetChapter servers: - url: https://admin.fullstorynovel.com - url: https://fullstorynovel.com security: [] parameters: - { name: novelSlug, in: path, required: true, schema: { type: string } } - { name: chapterSlug, in: path, required: true, schema: { type: string } } responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnChapterDetail" # —— FullStoryNovel CMS 管理 API(基址 admin.fullstorynovel.com)—— /api/admin/adopt-ops-session: post: tags: [admin-novels] summary: 用 wyrs Admin JWT 换取 CMS 会话 description: | Header `Authorization: Bearer `(与 popadmin 登录同一 JWT)。 返回 CMS `token`;popadmin iframe 嵌入时用 postMessage 注入。 operationId: fsnAdoptOpsSession servers: - url: https://admin.fullstorynovel.com security: [] responses: "200": description: "CMS JSON:token、username、opsToken 等" content: application/json: schema: type: object properties: token: { type: string, description: FSN CMS Bearer } username: { type: string } opsToken: { type: string } expiresInHours: { type: integer } /api/admin/stats: get: tags: [admin-novels] summary: CMS 仪表盘计数 operationId: fsnAdminStats servers: - url: https://admin.fullstorynovel.com responses: "200": content: application/json: schema: type: object properties: novels: { type: integer, format: int64 } publishedNovels: { type: integer, format: int64 } chapters: { type: integer, format: int64 } genres: { type: integer, format: int64 } /api/admin/novels: get: tags: [admin-novels] summary: 管理端小说列表 operationId: fsnAdminListNovels servers: - url: https://admin.fullstorynovel.com responses: "200": content: application/json: schema: type: array items: $ref: "#/components/schemas/FsnAdminNovelCard" post: tags: [admin-novels] summary: 新建小说 operationId: fsnAdminCreateNovel servers: - url: https://admin.fullstorynovel.com requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnNovelUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnAdminNovelCard" /api/admin/novels/{id}: get: tags: [admin-novels] summary: 管理端小说详情 operationId: fsnAdminGetNovel servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnAdminNovelCard" put: tags: [admin-novels] summary: 更新小说(含上下架 published / 精选 featured) operationId: fsnAdminUpdateNovel servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnNovelUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnAdminNovelCard" delete: tags: [admin-novels] summary: 删除小说及章节 operationId: fsnAdminDeleteNovel servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: type: object properties: ok: { type: boolean } /api/admin/covers: post: tags: [admin-novels] summary: 上传封面 multipart operationId: fsnAdminUploadCover servers: - url: https://admin.fullstorynovel.com requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: { type: string, format: binary } responses: "200": description: 含 coverUrl content: application/json: schema: type: object properties: coverUrl: { type: string } /api/admin/novels/{id}/chapters: get: tags: [admin-novels] summary: 管理端章节列表 operationId: fsnAdminListChapters servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: type: array items: $ref: "#/components/schemas/FsnChapterMeta" post: tags: [admin-novels] summary: 新增章节 operationId: fsnAdminCreateChapter servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnChapterUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnChapterMeta" /api/admin/chapters/{id}: get: tags: [admin-novels] summary: 章节详情(含正文) operationId: fsnAdminGetChapter servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": description: Chapter 实体 JSON put: tags: [admin-novels] summary: 更新章节 operationId: fsnAdminUpdateChapter servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnChapterUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnChapterMeta" delete: tags: [admin-novels] summary: 删除章节 operationId: fsnAdminDeleteChapter servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: type: object properties: ok: { type: boolean } /api/admin/genres: get: tags: [admin-novels] summary: 管理端分类/标签 operationId: fsnAdminListGenres servers: - url: https://admin.fullstorynovel.com responses: "200": content: application/json: schema: type: array items: $ref: "#/components/schemas/FsnAdminGenre" post: tags: [admin-novels] summary: 新建分类或二级标签 operationId: fsnAdminCreateGenre servers: - url: https://admin.fullstorynovel.com requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnGenreUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnAdminGenre" /api/admin/genres/{id}: put: tags: [admin-novels] summary: 更新分类 operationId: fsnAdminUpdateGenre servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/FsnGenreUpsert" responses: "200": content: application/json: schema: $ref: "#/components/schemas/FsnAdminGenre" delete: tags: [admin-novels] summary: 删除分类 operationId: fsnAdminDeleteGenre servers: - url: https://admin.fullstorynovel.com parameters: - name: id in: path required: true schema: { type: integer, format: int64 } responses: "200": content: application/json: schema: type: object properties: ok: { type: boolean } /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: integer format: int32 enum: [1, 2, 3, 4, 5, 6] description: | 设备类型,见 meta `device_type`。1=ios 2=android 3=harmony 4=windows 5=macos 6=web。 指纹 payload 使用该数值。 AppId: type: integer enum: [1] description: | 登录 App 标识。写入 JWT `aid`。当前文档约定 Spark IM=`1`(以 `GET /v1/meta/enums` → `app_id` 为准;实现落地后枚举会扩展)。 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, square_image, square_video] SystemKind: type: string enum: [friend.became, system.welcome, 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: {} AdminApiResponse: type: object description: popadmin / Java 风格管理端响应(非 V3 envelope) properties: success: { type: boolean } message: { type: string } data: {} MusicTrack: type: object properties: id: { type: integer, format: int64 } title: { type: string } coverKey: { type: string } audioKey: { type: string } coverUrl: { type: string, description: sparkr 公网 URL } audioUrl: { type: string, description: sparkr 公网 URL } requireMember: { type: boolean, description: true=需会员 } sortOrder: { type: integer } enabled: { type: boolean } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } MusicSettings: type: object properties: memberPreviewSeconds: type: integer minimum: 0 maximum: 3600 description: 会员曲免费试听秒数(全局) MusicPlaylist: type: object properties: items: type: array items: $ref: "#/components/schemas/MusicTrack" memberPreviewSeconds: { type: integer } MusicUpsertRequest: type: object properties: title: { type: string } requireMember: { type: boolean } sortOrder: { type: integer } enabled: { type: boolean } FsnGenre: type: object properties: id: { type: integer, format: int64 } slug: { type: string } name: { type: string } description: { type: string } parentId: { type: integer, format: int64, nullable: true } parentSlug: { type: string, nullable: true } sortOrder: { type: integer } children: type: array items: $ref: "#/components/schemas/FsnGenre" FsnAdminGenre: allOf: - $ref: "#/components/schemas/FsnGenre" - type: object properties: createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } FsnNovelCard: type: object properties: id: { type: integer, format: int64 } slug: { type: string } title: { type: string } author: { type: string } synopsis: { type: string } coverUrl: { type: string } status: { type: string } language: { type: string } genres: { type: array, items: { type: string } } rating: { type: number } views: { type: integer, format: int64 } chapterCount: { type: integer } featured: { type: boolean } published: { type: boolean } latestChapterAt: { type: string, format: date-time, nullable: true } createdAt: { type: string, format: date-time } FsnAdminNovelCard: allOf: - $ref: "#/components/schemas/FsnNovelCard" - type: object properties: updatedAt: { type: string, format: date-time } FsnNovelPage: type: object properties: items: type: array items: $ref: "#/components/schemas/FsnNovelCard" page: { type: integer } size: { type: integer } total: { type: integer, format: int64 } totalPages: { type: integer } FsnHome: type: object properties: featured: type: array items: $ref: "#/components/schemas/FsnNovelCard" popular: type: array items: $ref: "#/components/schemas/FsnNovelCard" newReleases: type: array items: $ref: "#/components/schemas/FsnNovelCard" recentlyUpdated: type: array items: $ref: "#/components/schemas/FsnNovelCard" genres: type: array items: $ref: "#/components/schemas/FsnGenre" FsnChapterMeta: type: object properties: id: { type: integer, format: int64 } chapterNumber: { type: integer } slug: { type: string } title: { type: string } publishedAt: { type: string, format: date-time, nullable: true } FsnChapterDetail: type: object properties: id: { type: integer, format: int64 } novelId: { type: integer, format: int64 } novelSlug: { type: string } novelTitle: { type: string } chapterNumber: { type: integer } slug: { type: string } title: { type: string } content: { type: string } publishedAt: { type: string, format: date-time, nullable: true } prev: $ref: "#/components/schemas/FsnChapterMeta" next: $ref: "#/components/schemas/FsnChapterMeta" FsnNovelUpsert: type: object properties: title: { type: string } slug: { type: string } author: { type: string } synopsis: { type: string } coverUrl: { type: string } status: { type: string } language: { type: string } genres: { type: array, items: { type: string } } rating: { type: number } featured: { type: boolean } published: { type: boolean, description: 上下架 } FsnChapterUpsert: type: object properties: chapterNumber: { type: integer } title: { type: string } slug: { type: string } content: { type: string } published: { type: boolean } FsnGenreUpsert: type: object properties: name: { type: string } slug: { type: string } description: { type: string } parentId: { type: integer, format: int64, nullable: true } parentSlug: { type: string, nullable: true } sortOrder: { type: integer } 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" SendVerificationCodeRequest: type: object required: [purpose, target] properties: purpose: $ref: "#/components/schemas/VerificationPurpose" target: type: string description: 邮箱或大陆 11 位手机号(可带 +86) DeleteAccountRequest: type: object properties: verify_code: type: string description: 注销验证码(purpose=account_delete)。测试账号可省略。 VerificationPurpose: type: integer enum: [1, 2] description: 1=登录 2=注销账号 LoginRequest: type: object required: [email, verify_code, app_id, device_type] description: | Breaking:删除客户端 `device_id`;新增 `app_id`、整数 `device_type`、物理指纹字段。 指纹:`payload="{device_type}:{idfv|android_id}"` → `device_id=uint64(SHA256(HEX(SHA256(payload)))[0:8])`(非 0)。 properties: email: type: string format: email verify_code: type: string description: 邮箱验证码。测试账号 test01@spark.com~test100@spark.com 可任意填写。 app_id: type: integer minimum: 1 description: 当前客户端 App,见 meta `app_id` 枚举(JWT claim `aid`)。 device_type: $ref: "#/components/schemas/DeviceType" platform: type: integer format: int32 enum: [1, 2, 3, 4, 5, 6] description: 与 device_type 相同枚举;仅当未传 device_type 时可作别名 device_name: type: string maxLength: 128 idfv: type: string maxLength: 36 description: iOS / macOS 必填(物理设备标识,服务端据此生成 device_id)。 android_id: type: string maxLength: 20 description: Android / Harmony 必填。 LoginData: type: object required: [access_token, refresh_token, expires_in, refresh_expires_in, user_id, device_id, app_id, spark_id] properties: access_token: type: string description: JWT。claims 含 sub、did、aid、use=access refresh_token: type: string description: JWT。claims 含 sub、did、aid、use=refresh;刷新后 aid 不变 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(= JWT sub) device_id: type: integer format: int64 description: 服务端按物理指纹生成的设备 id(JWT `did`)。客户端持久化该值,勿自行生成。 app_id: type: integer description: 与 JWT claim `aid` 一致 spark_id: type: string description: Public handle, unique and user-changeable. New and updated values are 8-16 characters. RefreshRequest: type: object required: [refresh_token] properties: refresh_token: type: string PatchMeRequest: type: object properties: spark_id: type: string minLength: 8 maxLength: 16 pattern: "^[a-zA-Z][a-zA-Z0-9_]{7,15}$" nickname: type: string minLength: 1 maxLength: 64 avatar: type: string maxLength: 512 description: | 对象存储 key。仅 `perm/avatar/`、`perm/group/` 可匿名 GET(桶本身不公开)。 URL:`{public_endpoint}/spark/{avatar}`,Nginx 也可省略桶名 `{public_endpoint}/{avatar}`。 `chat/`、`perm/fav/`、`perm/moment/` 必须 Presigned GET。 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 CreateDownloadBatchRequest: type: object required: [object_keys] properties: object_keys: type: array minItems: 1 maxItems: 9 items: 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(服务端生成的 device_id)。 X-App-ID: schema: type: integer description: Optional. Must match JWT aid(app_id) 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,须等于登录响应 / JWT did(服务端按物理指纹生成)。不放 Body。 X-App-ID: in: header name: X-App-ID required: false schema: type: integer example: 1 description: Header。若传须等于 JWT aid(app_id)。不放业务 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。 SquarePostID: 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。