openapi: 3.0.3 info: title: Spark IM API version: 0.1.0 description: | 本页是 **HTTP REST**(Gin `127.0.0.1:8080`)。长连接 IM 是另一套 TCP,见文末。 ## 响应 ```json {"code":"0","msg":"ok","data":{}} ``` `code` 是字符串。`"0"` 成功,其它失败。失败时 `data` 为 `{}`。看 `code`,不要只看 HTTP 状态(多数接口 HTTP 200;`/readyz` 未就绪才是 503)。 ## 鉴权 1. `POST /v1/auth/login` 拿 `access_token` / `refresh_token`。测试号 `test01@spark.com`~`test10@spark.com`,`verify_code` 任意。 2. 其它接口:`Authorization: Bearer `。 3. 可选 `X-Device-ID` 必须等于登录时的 `device_id`,否则 `40101`。 4. 续期 `POST /v1/auth/token/refresh`(用 refresh,不必带 Bearer)。退出 `POST /v1/auth/logout`。 语言:`X-Language` 优先,否则 `Accept-Language`。取值 `zh-CN`(默认)/ `en` / `zh-TW`。 ## TCP(不是 Protobuf) 二进制 Frame 头 23 字节(Magic `SP` + Command + SequenceID + BodyLength),**Body 是上面同一套 JSON**。没有 `.proto`,不要按 Protobuf 解包。 公网 TLS 在 Nginx `:443` 终结;gnet 只收本机 PROXY 明文。心跳 / AUTH / GENERATE_MSG_ID 已通;发消息与同步还在接。 servers: - url: http://127.0.0.1:8080 description: loopback HTTP (not public) security: - bearerAuth: [] tags: - name: system - name: auth - name: users - name: friends - name: groups - name: conversations - name: messages - name: files - name: favorites 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: OpenAPI 文档页(Swagger UI) operationId: docsUI 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/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`)。 operationId: login 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: 当前用户资料 operationId: getMe 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/1940000000000000000/20260917/1940000000000000001.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.status 为 pending 或 accepted 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 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}/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: 修改群名称 / 头像 / 全员禁言 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: 改成员角色 / 禁言 / 群昵称 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/{uid}/…`(不过期);`group_avatar` 写入 `perm/group/{群号}/…`(需 `group_id`,群主/管理员); 聊天媒体写入 `chat/{yyyyMMdd}/{uid}/…`(桶生命周期过期)。 客户端 PUT 时必须带返回的 `headers`(至少 `Content-Type`)。消息与头像只存 `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/20260917/1/2.jpg upload_url: http://47.236.66.9:9000/spark/chat/20260917/1/2.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256 method: PUT headers: Content-Type: image/jpeg expires_in: 900 /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/{uid}/…`。 源对象已从 `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" components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: Access JWT from /v1/auth/login. Same token is used for TCP AUTH. schemas: Envelope: type: object required: [code, msg, data] properties: code: type: string description: 业务码,成功为 "0" example: "0" msg: type: string example: ok data: type: object additionalProperties: true 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: type: string enum: [ios, android, harmony, windows, macos, web] platform: type: string description: device_type 的兼容字段 enum: [ios, android, harmony, windows, macos, web] 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: type: integer enum: [0, 1, 2] 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 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: type: integer enum: [1, 2, 3, 4] description: 1=垃圾 2=骚扰 3=不实身份 4=其他 detail: type: string maxLength: 512 CreateUploadRequest: type: object required: [purpose, mime, size] properties: purpose: type: string enum: [avatar, group_avatar, chat_image, chat_voice, chat_video, chat_gif, chat_file] mime: type: string example: image/jpeg size: type: integer format: int64 minimum: 1 group_id: type: integer format: int64 minimum: 1 description: purpose=group_avatar 时必填,为自增群号 CreateDownloadRequest: type: object required: [object_key] properties: object_key: type: string maxLength: 512 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 InviteGroupMembersRequest: type: object required: [uids] properties: uids: type: array minItems: 1 items: type: integer format: int64 UpdateGroupMemberRequest: type: object properties: role: type: integer enum: [2, 3] description: 2=管理员 3=普通成员;群主只能通过转让变更 speak_muted: type: boolean nickname: type: string maxLength: 32 TransferGroupRequest: type: object required: [uid] properties: uid: type: integer format: int64 LoginEnvelope: type: object required: [code, msg, data] properties: code: type: string msg: 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 X-Trace-ID: schema: type: string X-Device-ID: schema: type: string description: Optional. Must match JWT did when present. X-App-Version: schema: type: string Accept-Language: schema: type: string enum: [zh-CN, en, zh-TW] example: zh-CN description: 用户语言。也可用 X-Language。缺省 zh-CN。 X-Language: schema: type: string enum: [zh-CN, en, zh-TW] 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 description: "Bearer " X-Device-ID: in: header name: X-Device-ID required: false schema: type: string X-App-Version: in: header name: X-App-Version required: false schema: type: string Accept-Language: in: header name: Accept-Language required: false schema: type: string enum: [zh-CN, en, zh-TW] description: 用户语言。缺省 zh-CN。 X-Language: in: header name: X-Language required: false schema: type: string enum: [zh-CN, en, zh-TW] description: 覆盖 Accept-Language。 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。 GroupID: in: path name: id required: true schema: type: integer format: int64 description: 自增群号(不是 conversation_id)。