diff --git a/Wavelet/docs/docs.go b/Wavelet/docs/docs.go index c31b463d..d553c93d 100644 --- a/Wavelet/docs/docs.go +++ b/Wavelet/docs/docs.go @@ -928,6 +928,231 @@ const docTemplate = `{ } } }, + "/api/v1/admin/logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取系统历史日志,cursor=0 获取最新日志,cursor\u003e0 获取更早日志", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取系统日志", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "日志游标,0=获取最新", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "default": 200, + "description": "每页条数", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.logsResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/access": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页并按照用户、接口路径、时间范围等维度检索 ClickHouse 用户访问日志列表(需要管理员权限,ClickHouse 未启用时报错)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取用户访问日志", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "用户名模糊搜索", + "name": "username", + "in": "query" + }, + { + "type": "string", + "description": "接口路径模糊搜索", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "起始时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "结束时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)", + "name": "end_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.accessLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "ClickHouse 未启用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/analytics": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "聚合统计最近 7 天的每日访问趋势、浏览器分布以及前 10 名最活跃用户排行(需要管理员权限,ClickHouse 未启用时报错)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取访问日志分析数据", + "responses": { + "200": { + "description": "分析统计数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.logsAnalyticsResponse" + } + } + } + ] + } + }, + "400": { + "description": "ClickHouse 未启用", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/ws": { + "get": { + "description": "通过 WebSocket 实时推送系统日志,需要管理员权限", + "tags": [ + "admin" + ], + "summary": "系统日志实时推送", + "responses": {} + } + }, "/api/v1/admin/push/channels": { "get": { "security": [ @@ -3076,6 +3301,362 @@ const docTemplate = `{ } } }, + "/api/v1/admin/uploads": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取系统上传的文件列表,支持文件名关键词、业务类型、扩展名、上传用户ID过滤", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件列表", + "parameters": [ + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(默认 20,最大 100)", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "文件名关键词(模糊匹配)", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "业务分类过滤", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "扩展名过滤", + "name": "extension", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "上传用户 ID", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.listFilesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/download/batch": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "传入多个文件 ID,后台实时将其打包压缩为 ZIP 流并输出,自动处理文件名重复冲突", + "consumes": [ + "application/json" + ], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "admin" + ], + "summary": "批量打包下载", + "parameters": [ + { + "description": "包含文件 ID 数组 of string 的请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.batchDownloadRequest" + } + } + ], + "responses": { + "200": { + "description": "成功下载打包后的 ZIP", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "打包失败", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/download/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据文件 ID 获取文件,以附件形式 (Attachment) 强制开启客户端浏览器下载", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "admin" + ], + "summary": "下载单文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "图片质量 (low, medium, high, origin),默认为 origin", + "name": "quality", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功下载文件", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "服务内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/stats": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统级的总文件数、占用大小、最近 7 天新增趋势、文件类型/格式分布等数据", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件统计数据", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.fileStatsResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/types": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回数据库中所有已上传文件实际拥有的业务类型列表", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件业务类型列表", + "responses": { + "200": { + "description": "业务类型列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/{id}": { + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将文件状态置为 deleted(软删除),不会立即清理底层存储对象", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "删除文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/admin/users": { "get": { "security": [ @@ -3528,50 +4109,6 @@ const docTemplate = `{ } } }, - "/api/v1/d/about": { - "get": { - "description": "返回 OpenFlare 关于页面文本,无需登录", - "produces": [ - "application/json" - ], - "tags": [ - "openflare-option" - ], - "summary": "获取关于信息", - "responses": { - "200": { - "description": "关于信息", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "type": "string" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, "/api/v1/d/access-logs": { "get": { "security": [ @@ -9089,272 +9626,6 @@ const docTemplate = `{ } } }, - "/api/v1/d/update/latest-release": { - "get": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限", - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "获取最新服务端发布版本", - "parameters": [ - { - "type": "string", - "description": "发布渠道(stable/preview)", - "name": "channel", - "in": "query" - } - ], - "responses": { - "200": { - "description": "最新发布信息", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/update.LatestReleaseView" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/logs/ws": { - "get": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "通过 WebSocket 推送升级进度快照,需要管理员权限", - "tags": [ - "openflare-update" - ], - "summary": "流式获取服务端升级日志", - "responses": { - "101": { - "description": "WebSocket 升级日志流", - "schema": { - "$ref": "#/definitions/update.StreamSnapshot" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/manual-upgrade": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "确认手动服务端升级", - "responses": { - "400": { - "description": "功能已禁用或参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/manual-upload": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "上传手动升级二进制", - "parameters": [ - { - "type": "file", - "description": "服务端二进制文件", - "name": "binary", - "in": "formData", - "required": true - } - ], - "responses": { - "400": { - "description": "功能已禁用或参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/upgrade": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "触发服务端自动升级", - "parameters": [ - { - "description": "升级参数", - "name": "request", - "in": "body", - "schema": { - "$ref": "#/definitions/update.upgradeRequest" - } - } - ], - "responses": { - "200": { - "description": "升级任务已调度", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/update.LatestReleaseView" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, "/api/v1/d/uptimekuma/sync": { "post": { "security": [ @@ -10957,6 +11228,274 @@ const docTemplate = `{ } } }, + "/api/v1/upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "支持各种类型的通用文件上传,支持自动文件类型检测、哈希计算与“秒传”去重", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "file", + "description": "要上传的文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "业务分类 (例如: avatar, attachment, doc,默认为 generic)", + "name": "type", + "in": "formData" + }, + { + "type": "string", + "description": "额外的 JSON 格式元数据", + "name": "metadata", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "上传成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Upload" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误或文件受限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/upload/my": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤", + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "获取我的文件列表", + "parameters": [ + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(默认 20,最大 100)", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "文件名关键词(模糊匹配)", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "业务分类过滤", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "扩展名过滤", + "name": "extension", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.listMyFilesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/upload/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新当前用户本人的文件名或访问权限模式 (AccessMode)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "更新我的文件信息", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新字段", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.updateMyFileRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Upload" + } + } + } + ] + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将当前用户本人的文件状态置为 deleted(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "删除我的文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/user-info": { "get": { "security": [ @@ -11563,6 +12102,65 @@ const docTemplate = `{ } } }, + "/f/{id}": { + "get": { + "description": "根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "upload" + ], + "summary": "获取已上传文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "图片质量 (low, medium, high, origin),默认为 origin", + "name": "quality", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取文件内容", + "schema": { + "type": "file" + } + }, + "400": { + "description": "文件 ID 格式错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件未找到", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "服务内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/robots.txt": { "get": { "description": "根据系统配置决定是否允许搜索引擎检索,并返回相应的 robots.txt 文件内容", @@ -12141,6 +12739,285 @@ const docTemplate = `{ } } }, + "handler.batchDownloadRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "handler.distributionItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "handler.fileStatsResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.distributionItem" + } + }, + "total_count": { + "type": "integer" + }, + "total_size": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.trendItem" + } + }, + "types": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.distributionItem" + } + } + } + }, + "handler.listFilesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Upload" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.listMyFilesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Upload" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.trendItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "handler.updateMyFileRequest": { + "type": "object", + "properties": { + "access_mode": { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + "file_name": { + "type": "string", + "maxLength": 255 + } + } + }, + "logger.LogEntry": { + "type": "object", + "properties": { + "data": { + "description": "一行日志原文(含换行符)", + "type": "string" + }, + "index": { + "description": "全局递增序号", + "type": "integer" + } + } + }, + "logs.accessLogItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "headers": { + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "ip": { + "type": "string" + }, + "latency": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "user_agent": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + }, + "username": { + "type": "string" + } + } + }, + "logs.accessLogsResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.accessLogItem" + } + }, + "total": { + "type": "integer" + } + } + }, + "logs.browserItem": { + "type": "object", + "properties": { + "browser": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + }, + "logs.logsAnalyticsResponse": { + "type": "object", + "properties": { + "browsers": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.browserItem" + } + }, + "top_users": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.topUserItem" + } + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.trendItem" + } + } + } + }, + "logs.logsResponse": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "lines": { + "type": "array", + "items": { + "$ref": "#/definitions/logger.LogEntry" + } + }, + "next_cursor": { + "description": "用于加载更早日志的 cursor", + "type": "integer" + } + } + }, + "logs.topUserItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + }, + "username": { + "type": "string" + } + } + }, + "logs.trendItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "string" + } + } + }, "model.AccessToken": { "type": "object", "properties": { @@ -12940,6 +13817,133 @@ const docTemplate = `{ } } }, + "model.Upload": { + "type": "object", + "properties": { + "access_mode": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "extension": { + "description": "文件后缀名 (不含点,如 png, pdf)", + "type": "string" + }, + "file_name": { + "description": "原始文件名 (例如: image.png)", + "type": "string" + }, + "file_path": { + "description": "文件相对路径 / S3 Key", + "type": "string" + }, + "file_size": { + "description": "文件大小(字节)", + "type": "integer" + }, + "hash": { + "description": "文件哈希 (SHA-256/MD5,可用于排重)", + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "metadata": { + "description": "业务扩展元数据", + "allOf": [ + { + "$ref": "#/definitions/model.UploadMetadata" + } + ] + }, + "mime_type": { + "description": "媒体类型 (MIME, 如 image/png)", + "type": "string" + }, + "status": { + "description": "状态", + "allOf": [ + { + "$ref": "#/definitions/model.UploadStatus" + } + ] + }, + "type": { + "description": "业务标识类型 (如 avatar, doc, attachment)", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + } + } + }, + "model.UploadMetadata": { + "type": "object", + "properties": { + "bucket": { + "description": "存储桶名称 (适用于 S3 等)", + "type": "string" + }, + "client_ip": { + "description": "上传者 IP", + "type": "string" + }, + "duration": { + "description": "音视频时长 (s)", + "type": "number" + }, + "extra": { + "description": "其它任意业务自定义元数据", + "type": "object", + "additionalProperties": {} + }, + "height": { + "description": "图像/视频高度 (px)", + "type": "integer" + }, + "original_mime": { + "description": "原始 MIME 类型", + "type": "string" + }, + "user_agent": { + "description": "上传者的 UA", + "type": "string" + }, + "width": { + "description": "图像/视频宽度 (px)", + "type": "integer" + } + } + }, + "model.UploadStatus": { + "type": "string", + "enum": [ + "pending", + "used", + "deleted" + ], + "x-enum-comments": { + "UploadStatusDeleted": "已删除", + "UploadStatusPending": "待使用", + "UploadStatusUsed": "已使用" + }, + "x-enum-descriptions": [ + "待使用", + "已使用", + "已删除" + ], + "x-enum-varnames": [ + "UploadStatusPending", + "UploadStatusUsed", + "UploadStatusDeleted" + ] + }, "node.AgentReleaseInfo": { "type": "object", "properties": { @@ -15405,89 +16409,6 @@ const docTemplate = `{ } } }, - "update.LatestReleaseView": { - "type": "object", - "properties": { - "body": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "current_version": { - "type": "string" - }, - "has_update": { - "type": "boolean" - }, - "html_url": { - "type": "string" - }, - "in_progress": { - "type": "boolean" - }, - "prerelease": { - "type": "boolean" - }, - "published_at": { - "type": "string" - }, - "tag_name": { - "type": "string" - }, - "upgrade_logs": { - "type": "array", - "items": { - "$ref": "#/definitions/update.UpgradeLogRecord" - } - }, - "upgrade_status": { - "type": "string" - }, - "upgrade_supported": { - "type": "boolean" - } - } - }, - "update.StreamSnapshot": { - "type": "object", - "properties": { - "in_progress": { - "type": "boolean" - }, - "upgrade_logs": { - "type": "array", - "items": { - "$ref": "#/definitions/update.UpgradeLogRecord" - } - }, - "upgrade_status": { - "type": "string" - } - } - }, - "update.UpgradeLogRecord": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "level": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "update.upgradeRequest": { - "type": "object", - "properties": { - "channel": { - "type": "string" - } - } - }, "updater.Status": { "type": "object", "properties": { diff --git a/Wavelet/docs/swagger.json b/Wavelet/docs/swagger.json index 10c0ada4..ade96ab2 100644 --- a/Wavelet/docs/swagger.json +++ b/Wavelet/docs/swagger.json @@ -921,6 +921,231 @@ } } }, + "/api/v1/admin/logs": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取系统历史日志,cursor=0 获取最新日志,cursor\u003e0 获取更早日志", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取系统日志", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "日志游标,0=获取最新", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "default": 200, + "description": "每页条数", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.logsResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/access": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页并按照用户、接口路径、时间范围等维度检索 ClickHouse 用户访问日志列表(需要管理员权限,ClickHouse 未启用时报错)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取用户访问日志", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页条数", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "用户名模糊搜索", + "name": "username", + "in": "query" + }, + { + "type": "string", + "description": "接口路径模糊搜索", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "起始时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "结束时间(RFC3339 或 YYYY-MM-DD HH:MM:SS)", + "name": "end_time", + "in": "query" + } + ], + "responses": { + "200": { + "description": "访问日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.accessLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "ClickHouse 未启用或参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/analytics": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "聚合统计最近 7 天的每日访问趋势、浏览器分布以及前 10 名最活跃用户排行(需要管理员权限,ClickHouse 未启用时报错)", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取访问日志分析数据", + "responses": { + "200": { + "description": "分析统计数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/logs.logsAnalyticsResponse" + } + } + } + ] + } + }, + "400": { + "description": "ClickHouse 未启用", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/logs/ws": { + "get": { + "description": "通过 WebSocket 实时推送系统日志,需要管理员权限", + "tags": [ + "admin" + ], + "summary": "系统日志实时推送", + "responses": {} + } + }, "/api/v1/admin/push/channels": { "get": { "security": [ @@ -3069,6 +3294,362 @@ } } }, + "/api/v1/admin/uploads": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取系统上传的文件列表,支持文件名关键词、业务类型、扩展名、上传用户ID过滤", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件列表", + "parameters": [ + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(默认 20,最大 100)", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "文件名关键词(模糊匹配)", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "业务分类过滤", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "扩展名过滤", + "name": "extension", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "上传用户 ID", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.listFilesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/download/batch": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "传入多个文件 ID,后台实时将其打包压缩为 ZIP 流并输出,自动处理文件名重复冲突", + "consumes": [ + "application/json" + ], + "produces": [ + "application/octet-stream" + ], + "tags": [ + "admin" + ], + "summary": "批量打包下载", + "parameters": [ + { + "description": "包含文件 ID 数组 of string 的请求体", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.batchDownloadRequest" + } + } + ], + "responses": { + "200": { + "description": "成功下载打包后的 ZIP", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "打包失败", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/download/{id}": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "根据文件 ID 获取文件,以附件形式 (Attachment) 强制开启客户端浏览器下载", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "admin" + ], + "summary": "下载单文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "图片质量 (low, medium, high, origin),默认为 origin", + "name": "quality", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功下载文件", + "schema": { + "type": "file" + } + }, + "400": { + "description": "参数错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "服务内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/stats": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回系统级的总文件数、占用大小、最近 7 天新增趋势、文件类型/格式分布等数据", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件统计数据", + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.fileStatsResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/types": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "返回数据库中所有已上传文件实际拥有的业务类型列表", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取文件业务类型列表", + "responses": { + "200": { + "description": "业务类型列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无管理员权限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/admin/uploads/{id}": { + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将文件状态置为 deleted(软删除),不会立即清理底层存储对象", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "删除文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/admin/users": { "get": { "security": [ @@ -3521,50 +4102,6 @@ } } }, - "/api/v1/d/about": { - "get": { - "description": "返回 OpenFlare 关于页面文本,无需登录", - "produces": [ - "application/json" - ], - "tags": [ - "openflare-option" - ], - "summary": "获取关于信息", - "responses": { - "200": { - "description": "关于信息", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "type": "string" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, "/api/v1/d/access-logs": { "get": { "security": [ @@ -9082,272 +9619,6 @@ } } }, - "/api/v1/d/update/latest-release": { - "get": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限", - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "获取最新服务端发布版本", - "parameters": [ - { - "type": "string", - "description": "发布渠道(stable/preview)", - "name": "channel", - "in": "query" - } - ], - "responses": { - "200": { - "description": "最新发布信息", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/update.LatestReleaseView" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/logs/ws": { - "get": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "通过 WebSocket 推送升级进度快照,需要管理员权限", - "tags": [ - "openflare-update" - ], - "summary": "流式获取服务端升级日志", - "responses": { - "101": { - "description": "WebSocket 升级日志流", - "schema": { - "$ref": "#/definitions/update.StreamSnapshot" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/manual-upgrade": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "确认手动服务端升级", - "responses": { - "400": { - "description": "功能已禁用或参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/manual-upload": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "上传手动升级二进制", - "parameters": [ - { - "type": "file", - "description": "服务端二进制文件", - "name": "binary", - "in": "formData", - "required": true - } - ], - "responses": { - "400": { - "description": "功能已禁用或参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, - "/api/v1/d/update/upgrade": { - "post": { - "security": [ - { - "SessionCookie": [] - } - ], - "description": "从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "openflare-update" - ], - "summary": "触发服务端自动升级", - "parameters": [ - { - "description": "升级参数", - "name": "request", - "in": "body", - "schema": { - "$ref": "#/definitions/update.upgradeRequest" - } - } - ], - "responses": { - "200": { - "description": "升级任务已调度", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.Any" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/update.LatestReleaseView" - } - } - } - ] - } - }, - "400": { - "description": "参数错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "401": { - "description": "未登录", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "403": { - "description": "无管理员权限", - "schema": { - "$ref": "#/definitions/response.Any" - } - }, - "500": { - "description": "内部错误", - "schema": { - "$ref": "#/definitions/response.Any" - } - } - } - } - }, "/api/v1/d/uptimekuma/sync": { "post": { "security": [ @@ -10950,6 +11221,274 @@ } } }, + "/api/v1/upload": { + "post": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "支持各种类型的通用文件上传,支持自动文件类型检测、哈希计算与“秒传”去重", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "file", + "description": "要上传的文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "业务分类 (例如: avatar, attachment, doc,默认为 generic)", + "name": "type", + "in": "formData" + }, + { + "type": "string", + "description": "额外的 JSON 格式元数据", + "name": "metadata", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "上传成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Upload" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误或文件受限", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/upload/my": { + "get": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤", + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "获取我的文件列表", + "parameters": [ + { + "type": "integer", + "description": "页码(默认 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(默认 20,最大 100)", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "文件名关键词(模糊匹配)", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "业务分类过滤", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "扩展名过滤", + "name": "extension", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.listMyFilesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, + "/api/v1/upload/{id}": { + "put": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "更新当前用户本人的文件名或访问权限模式 (AccessMode)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "更新我的文件信息", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新字段", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.updateMyFileRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Any" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Upload" + } + } + } + ] + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + }, + "delete": { + "security": [ + { + "SessionCookie": [] + } + ], + "description": "将当前用户本人的文件状态置为 deleted(软删除)", + "produces": [ + "application/json" + ], + "tags": [ + "upload" + ], + "summary": "删除我的文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "403": { + "description": "无权操作", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件不存在", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/api/v1/user-info": { "get": { "security": [ @@ -11556,6 +12095,65 @@ } } }, + "/f/{id}": { + "get": { + "description": "根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "upload" + ], + "summary": "获取已上传文件", + "parameters": [ + { + "type": "string", + "description": "文件 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "图片质量 (low, medium, high, origin),默认为 origin", + "name": "quality", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功获取文件内容", + "schema": { + "type": "file" + } + }, + "400": { + "description": "文件 ID 格式错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "401": { + "description": "未登录", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "404": { + "description": "文件未找到", + "schema": { + "$ref": "#/definitions/response.Any" + } + }, + "500": { + "description": "服务内部错误", + "schema": { + "$ref": "#/definitions/response.Any" + } + } + } + } + }, "/robots.txt": { "get": { "description": "根据系统配置决定是否允许搜索引擎检索,并返回相应的 robots.txt 文件内容", @@ -12134,6 +12732,285 @@ } } }, + "handler.batchDownloadRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "handler.distributionItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "handler.fileStatsResponse": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.distributionItem" + } + }, + "total_count": { + "type": "integer" + }, + "total_size": { + "type": "integer" + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.trendItem" + } + }, + "types": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.distributionItem" + } + } + } + }, + "handler.listFilesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Upload" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.listMyFilesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Upload" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.trendItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, + "handler.updateMyFileRequest": { + "type": "object", + "properties": { + "access_mode": { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + "file_name": { + "type": "string", + "maxLength": 255 + } + } + }, + "logger.LogEntry": { + "type": "object", + "properties": { + "data": { + "description": "一行日志原文(含换行符)", + "type": "string" + }, + "index": { + "description": "全局递增序号", + "type": "integer" + } + } + }, + "logs.accessLogItem": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "headers": { + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "ip": { + "type": "string" + }, + "latency": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "path": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "user_agent": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + }, + "username": { + "type": "string" + } + } + }, + "logs.accessLogsResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.accessLogItem" + } + }, + "total": { + "type": "integer" + } + } + }, + "logs.browserItem": { + "type": "object", + "properties": { + "browser": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + }, + "logs.logsAnalyticsResponse": { + "type": "object", + "properties": { + "browsers": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.browserItem" + } + }, + "top_users": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.topUserItem" + } + }, + "trend": { + "type": "array", + "items": { + "$ref": "#/definitions/logs.trendItem" + } + } + } + }, + "logs.logsResponse": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "lines": { + "type": "array", + "items": { + "$ref": "#/definitions/logger.LogEntry" + } + }, + "next_cursor": { + "description": "用于加载更早日志的 cursor", + "type": "integer" + } + } + }, + "logs.topUserItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + }, + "username": { + "type": "string" + } + } + }, + "logs.trendItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "string" + } + } + }, "model.AccessToken": { "type": "object", "properties": { @@ -12933,6 +13810,133 @@ } } }, + "model.Upload": { + "type": "object", + "properties": { + "access_mode": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "extension": { + "description": "文件后缀名 (不含点,如 png, pdf)", + "type": "string" + }, + "file_name": { + "description": "原始文件名 (例如: image.png)", + "type": "string" + }, + "file_path": { + "description": "文件相对路径 / S3 Key", + "type": "string" + }, + "file_size": { + "description": "文件大小(字节)", + "type": "integer" + }, + "hash": { + "description": "文件哈希 (SHA-256/MD5,可用于排重)", + "type": "string" + }, + "id": { + "type": "string", + "example": "0" + }, + "metadata": { + "description": "业务扩展元数据", + "allOf": [ + { + "$ref": "#/definitions/model.UploadMetadata" + } + ] + }, + "mime_type": { + "description": "媒体类型 (MIME, 如 image/png)", + "type": "string" + }, + "status": { + "description": "状态", + "allOf": [ + { + "$ref": "#/definitions/model.UploadStatus" + } + ] + }, + "type": { + "description": "业务标识类型 (如 avatar, doc, attachment)", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string", + "example": "0" + } + } + }, + "model.UploadMetadata": { + "type": "object", + "properties": { + "bucket": { + "description": "存储桶名称 (适用于 S3 等)", + "type": "string" + }, + "client_ip": { + "description": "上传者 IP", + "type": "string" + }, + "duration": { + "description": "音视频时长 (s)", + "type": "number" + }, + "extra": { + "description": "其它任意业务自定义元数据", + "type": "object", + "additionalProperties": {} + }, + "height": { + "description": "图像/视频高度 (px)", + "type": "integer" + }, + "original_mime": { + "description": "原始 MIME 类型", + "type": "string" + }, + "user_agent": { + "description": "上传者的 UA", + "type": "string" + }, + "width": { + "description": "图像/视频宽度 (px)", + "type": "integer" + } + } + }, + "model.UploadStatus": { + "type": "string", + "enum": [ + "pending", + "used", + "deleted" + ], + "x-enum-comments": { + "UploadStatusDeleted": "已删除", + "UploadStatusPending": "待使用", + "UploadStatusUsed": "已使用" + }, + "x-enum-descriptions": [ + "待使用", + "已使用", + "已删除" + ], + "x-enum-varnames": [ + "UploadStatusPending", + "UploadStatusUsed", + "UploadStatusDeleted" + ] + }, "node.AgentReleaseInfo": { "type": "object", "properties": { @@ -15398,89 +16402,6 @@ } } }, - "update.LatestReleaseView": { - "type": "object", - "properties": { - "body": { - "type": "string" - }, - "channel": { - "type": "string" - }, - "current_version": { - "type": "string" - }, - "has_update": { - "type": "boolean" - }, - "html_url": { - "type": "string" - }, - "in_progress": { - "type": "boolean" - }, - "prerelease": { - "type": "boolean" - }, - "published_at": { - "type": "string" - }, - "tag_name": { - "type": "string" - }, - "upgrade_logs": { - "type": "array", - "items": { - "$ref": "#/definitions/update.UpgradeLogRecord" - } - }, - "upgrade_status": { - "type": "string" - }, - "upgrade_supported": { - "type": "boolean" - } - } - }, - "update.StreamSnapshot": { - "type": "object", - "properties": { - "in_progress": { - "type": "boolean" - }, - "upgrade_logs": { - "type": "array", - "items": { - "$ref": "#/definitions/update.UpgradeLogRecord" - } - }, - "upgrade_status": { - "type": "string" - } - } - }, - "update.UpgradeLogRecord": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "level": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "update.upgradeRequest": { - "type": "object", - "properties": { - "channel": { - "type": "string" - } - } - }, "updater.Status": { "type": "object", "properties": { diff --git a/Wavelet/docs/swagger.yaml b/Wavelet/docs/swagger.yaml index 3abb81e5..a29730ad 100644 --- a/Wavelet/docs/swagger.yaml +++ b/Wavelet/docs/swagger.yaml @@ -367,6 +367,190 @@ definitions: token: type: string type: object + handler.batchDownloadRequest: + properties: + ids: + items: + type: string + minItems: 1 + type: array + required: + - ids + type: object + handler.distributionItem: + properties: + count: + type: integer + name: + type: string + size: + type: integer + type: object + handler.fileStatsResponse: + properties: + categories: + items: + $ref: '#/definitions/handler.distributionItem' + type: array + total_count: + type: integer + total_size: + type: integer + trend: + items: + $ref: '#/definitions/handler.trendItem' + type: array + types: + items: + $ref: '#/definitions/handler.distributionItem' + type: array + type: object + handler.listFilesResponse: + properties: + items: + items: + $ref: '#/definitions/model.Upload' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.listMyFilesResponse: + properties: + items: + items: + $ref: '#/definitions/model.Upload' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.trendItem: + properties: + count: + type: integer + date: + type: string + size: + type: integer + type: object + handler.updateMyFileRequest: + properties: + access_mode: + enum: + - 0 + - 1 + type: integer + file_name: + maxLength: 255 + type: string + type: object + logger.LogEntry: + properties: + data: + description: 一行日志原文(含换行符) + type: string + index: + description: 全局递增序号 + type: integer + type: object + logs.accessLogItem: + properties: + created_at: + type: string + headers: + type: string + id: + example: "0" + type: string + ip: + type: string + latency: + type: integer + method: + type: string + nickname: + type: string + path: + type: string + status: + type: integer + user_agent: + type: string + user_id: + example: "0" + type: string + username: + type: string + type: object + logs.accessLogsResponse: + properties: + list: + items: + $ref: '#/definitions/logs.accessLogItem' + type: array + total: + type: integer + type: object + logs.browserItem: + properties: + browser: + type: string + count: + type: integer + type: object + logs.logsAnalyticsResponse: + properties: + browsers: + items: + $ref: '#/definitions/logs.browserItem' + type: array + top_users: + items: + $ref: '#/definitions/logs.topUserItem' + type: array + trend: + items: + $ref: '#/definitions/logs.trendItem' + type: array + type: object + logs.logsResponse: + properties: + has_more: + type: boolean + lines: + items: + $ref: '#/definitions/logger.LogEntry' + type: array + next_cursor: + description: 用于加载更早日志的 cursor + type: integer + type: object + logs.topUserItem: + properties: + count: + type: integer + nickname: + type: string + user_id: + example: "0" + type: string + username: + type: string + type: object + logs.trendItem: + properties: + count: + type: integer + date: + type: string + type: object model.AccessToken: properties: created_at: @@ -900,6 +1084,96 @@ definitions: updated_at: type: string type: object + model.Upload: + properties: + access_mode: + type: integer + created_at: + type: string + extension: + description: 文件后缀名 (不含点,如 png, pdf) + type: string + file_name: + description: '原始文件名 (例如: image.png)' + type: string + file_path: + description: 文件相对路径 / S3 Key + type: string + file_size: + description: 文件大小(字节) + type: integer + hash: + description: 文件哈希 (SHA-256/MD5,可用于排重) + type: string + id: + example: "0" + type: string + metadata: + allOf: + - $ref: '#/definitions/model.UploadMetadata' + description: 业务扩展元数据 + mime_type: + description: 媒体类型 (MIME, 如 image/png) + type: string + status: + allOf: + - $ref: '#/definitions/model.UploadStatus' + description: 状态 + type: + description: 业务标识类型 (如 avatar, doc, attachment) + type: string + updated_at: + type: string + user_id: + example: "0" + type: string + type: object + model.UploadMetadata: + properties: + bucket: + description: 存储桶名称 (适用于 S3 等) + type: string + client_ip: + description: 上传者 IP + type: string + duration: + description: 音视频时长 (s) + type: number + extra: + additionalProperties: {} + description: 其它任意业务自定义元数据 + type: object + height: + description: 图像/视频高度 (px) + type: integer + original_mime: + description: 原始 MIME 类型 + type: string + user_agent: + description: 上传者的 UA + type: string + width: + description: 图像/视频宽度 (px) + type: integer + type: object + model.UploadStatus: + enum: + - pending + - used + - deleted + type: string + x-enum-comments: + UploadStatusDeleted: 已删除 + UploadStatusPending: 待使用 + UploadStatusUsed: 已使用 + x-enum-descriptions: + - 待使用 + - 已使用 + - 已删除 + x-enum-varnames: + - UploadStatusPending + - UploadStatusUsed + - UploadStatusDeleted node.AgentReleaseInfo: properties: body: @@ -2536,60 +2810,6 @@ definitions: matched: type: boolean type: object - update.LatestReleaseView: - properties: - body: - type: string - channel: - type: string - current_version: - type: string - has_update: - type: boolean - html_url: - type: string - in_progress: - type: boolean - prerelease: - type: boolean - published_at: - type: string - tag_name: - type: string - upgrade_logs: - items: - $ref: '#/definitions/update.UpgradeLogRecord' - type: array - upgrade_status: - type: string - upgrade_supported: - type: boolean - type: object - update.StreamSnapshot: - properties: - in_progress: - type: boolean - upgrade_logs: - items: - $ref: '#/definitions/update.UpgradeLogRecord' - type: array - upgrade_status: - type: string - type: object - update.UpgradeLogRecord: - properties: - created_at: - type: string - level: - type: string - message: - type: string - type: object - update.upgradeRequest: - properties: - channel: - type: string - type: object updater.Status: properties: asset_name: @@ -3621,6 +3841,143 @@ paths: summary: 获取数据库所有表名 tags: - admin + /api/v1/admin/logs: + get: + description: 分页获取系统历史日志,cursor=0 获取最新日志,cursor>0 获取更早日志 + parameters: + - default: 0 + description: 日志游标,0=获取最新 + in: query + name: cursor + type: integer + - default: 200 + description: 每页条数 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 日志列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/logs.logsResponse' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取系统日志 + tags: + - admin + /api/v1/admin/logs/access: + get: + description: 分页并按照用户、接口路径、时间范围等维度检索 ClickHouse 用户访问日志列表(需要管理员权限,ClickHouse 未启用时报错) + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 20 + description: 每页条数 + in: query + name: page_size + type: integer + - description: 用户名模糊搜索 + in: query + name: username + type: string + - description: 接口路径模糊搜索 + in: query + name: path + type: string + - description: 起始时间(RFC3339 或 YYYY-MM-DD HH:MM:SS) + in: query + name: start_time + type: string + - description: 结束时间(RFC3339 或 YYYY-MM-DD HH:MM:SS) + in: query + name: end_time + type: string + produces: + - application/json + responses: + "200": + description: 访问日志列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/logs.accessLogsResponse' + type: object + "400": + description: ClickHouse 未启用或参数错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取用户访问日志 + tags: + - admin + /api/v1/admin/logs/analytics: + get: + description: 聚合统计最近 7 天的每日访问趋势、浏览器分布以及前 10 名最活跃用户排行(需要管理员权限,ClickHouse 未启用时报错) + produces: + - application/json + responses: + "200": + description: 分析统计数据 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/logs.logsAnalyticsResponse' + type: object + "400": + description: ClickHouse 未启用 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取访问日志分析数据 + tags: + - admin + /api/v1/admin/logs/ws: + get: + description: 通过 WebSocket 实时推送系统日志,需要管理员权限 + responses: {} + summary: 系统日志实时推送 + tags: + - admin /api/v1/admin/push/channels: get: description: 返回系统配置的所有消息通道列表,需要管理员权限 @@ -4895,6 +5252,224 @@ paths: summary: 下载并应用应用更新 tags: - admin + /api/v1/admin/uploads: + get: + description: 分页获取系统上传的文件列表,支持文件名关键词、业务类型、扩展名、上传用户ID过滤 + parameters: + - description: 页码(默认 1) + in: query + name: page + type: integer + - description: 每页数量(默认 20,最大 100) + in: query + name: page_size + type: integer + - description: 文件名关键词(模糊匹配) + in: query + name: keyword + type: string + - description: 业务分类过滤 + in: query + name: type + type: string + - description: 扩展名过滤 + in: query + name: extension + type: string + - description: 上传用户 ID + format: int64 + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/handler.listFilesResponse' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取文件列表 + tags: + - admin + /api/v1/admin/uploads/{id}: + delete: + description: 将文件状态置为 deleted(软删除),不会立即清理底层存储对象 + parameters: + - description: 文件 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无权操作 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 文件不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除文件 + tags: + - admin + /api/v1/admin/uploads/download/{id}: + get: + description: 根据文件 ID 获取文件,以附件形式 (Attachment) 强制开启客户端浏览器下载 + parameters: + - description: 文件 ID + in: path + name: id + required: true + type: string + - description: 图片质量 (low, medium, high, origin),默认为 origin + in: query + name: quality + type: string + produces: + - application/octet-stream + responses: + "200": + description: 成功下载文件 + schema: + type: file + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 文件不存在 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 服务内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 下载单文件 + tags: + - admin + /api/v1/admin/uploads/download/batch: + post: + consumes: + - application/json + description: 传入多个文件 ID,后台实时将其打包压缩为 ZIP 流并输出,自动处理文件名重复冲突 + parameters: + - description: 包含文件 ID 数组 of string 的请求体 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.batchDownloadRequest' + produces: + - application/octet-stream + responses: + "200": + description: 成功下载打包后的 ZIP + schema: + type: file + "400": + description: 参数错误 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 打包失败 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 批量打包下载 + tags: + - admin + /api/v1/admin/uploads/stats: + get: + description: 返回系统级的总文件数、占用大小、最近 7 天新增趋势、文件类型/格式分布等数据 + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/handler.fileStatsResponse' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取文件统计数据 + tags: + - admin + /api/v1/admin/uploads/types: + get: + description: 返回数据库中所有已上传文件实际拥有的业务类型列表 + produces: + - application/json + responses: + "200": + description: 业务类型列表 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + items: + type: string + type: array + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无管理员权限 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取文件业务类型列表 + tags: + - admin /api/v1/admin/users: get: description: 分页返回用户列表,支持按用户 ID 和用户名筛选,需要管理员权限 @@ -5169,32 +5744,6 @@ paths: summary: Sample Hello API tags: - custom - /api/v1/d/about: - get: - description: 返回 OpenFlare 关于页面文本,无需登录 - produces: - - application/json - responses: - "200": - description: 关于信息 - schema: - allOf: - - $ref: '#/definitions/response.Any' - - properties: - data: - type: string - type: object - "400": - description: 参数错误 - schema: - $ref: '#/definitions/response.Any' - "500": - description: 内部错误 - schema: - $ref: '#/definitions/response.Any' - summary: 获取关于信息 - tags: - - openflare-option /api/v1/d/access-logs: get: description: 分页返回 OpenFlare 访问日志,支持按节点、IP、主机与路径筛选,需要管理员权限 @@ -8533,168 +9082,6 @@ paths: summary: 从文件导入 TLS 证书 tags: - openflare-tls - /api/v1/d/update/latest-release: - get: - description: 查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限 - parameters: - - description: 发布渠道(stable/preview) - in: query - name: channel - type: string - produces: - - application/json - responses: - "200": - description: 最新发布信息 - schema: - allOf: - - $ref: '#/definitions/response.Any' - - properties: - data: - $ref: '#/definitions/update.LatestReleaseView' - type: object - "400": - description: 参数错误 - schema: - $ref: '#/definitions/response.Any' - "401": - description: 未登录 - schema: - $ref: '#/definitions/response.Any' - "403": - description: 无管理员权限 - schema: - $ref: '#/definitions/response.Any' - "500": - description: 内部错误 - schema: - $ref: '#/definitions/response.Any' - security: - - SessionCookie: [] - summary: 获取最新服务端发布版本 - tags: - - openflare-update - /api/v1/d/update/logs/ws: - get: - description: 通过 WebSocket 推送升级进度快照,需要管理员权限 - responses: - "101": - description: WebSocket 升级日志流 - schema: - $ref: '#/definitions/update.StreamSnapshot' - "401": - description: 未登录 - schema: - $ref: '#/definitions/response.Any' - "403": - description: 无管理员权限 - schema: - $ref: '#/definitions/response.Any' - security: - - SessionCookie: [] - summary: 流式获取服务端升级日志 - tags: - - openflare-update - /api/v1/d/update/manual-upgrade: - post: - consumes: - - application/json - description: 确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限 - produces: - - application/json - responses: - "400": - description: 功能已禁用或参数错误 - schema: - $ref: '#/definitions/response.Any' - "401": - description: 未登录 - schema: - $ref: '#/definitions/response.Any' - "403": - description: 无管理员权限 - schema: - $ref: '#/definitions/response.Any' - security: - - SessionCookie: [] - summary: 确认手动服务端升级 - tags: - - openflare-update - /api/v1/d/update/manual-upload: - post: - consumes: - - multipart/form-data - description: 上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限 - parameters: - - description: 服务端二进制文件 - in: formData - name: binary - required: true - type: file - produces: - - application/json - responses: - "400": - description: 功能已禁用或参数错误 - schema: - $ref: '#/definitions/response.Any' - "401": - description: 未登录 - schema: - $ref: '#/definitions/response.Any' - "403": - description: 无管理员权限 - schema: - $ref: '#/definitions/response.Any' - security: - - SessionCookie: [] - summary: 上传手动升级二进制 - tags: - - openflare-update - /api/v1/d/update/upgrade: - post: - consumes: - - application/json - description: 从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限 - parameters: - - description: 升级参数 - in: body - name: request - schema: - $ref: '#/definitions/update.upgradeRequest' - produces: - - application/json - responses: - "200": - description: 升级任务已调度 - schema: - allOf: - - $ref: '#/definitions/response.Any' - - properties: - data: - $ref: '#/definitions/update.LatestReleaseView' - type: object - "400": - description: 参数错误 - schema: - $ref: '#/definitions/response.Any' - "401": - description: 未登录 - schema: - $ref: '#/definitions/response.Any' - "403": - description: 无管理员权限 - schema: - $ref: '#/definitions/response.Any' - "500": - description: 内部错误 - schema: - $ref: '#/definitions/response.Any' - security: - - SessionCookie: [] - summary: 触发服务端自动升级 - tags: - - openflare-update /api/v1/d/uptimekuma/sync: post: consumes: @@ -9662,6 +10049,169 @@ paths: summary: 获取当前登录用户信息 tags: - oauth + /api/v1/upload: + post: + consumes: + - multipart/form-data + description: 支持各种类型的通用文件上传,支持自动文件类型检测、哈希计算与“秒传”去重 + parameters: + - description: 要上传的文件 + in: formData + name: file + required: true + type: file + - description: '业务分类 (例如: avatar, attachment, doc,默认为 generic)' + in: formData + name: type + type: string + - description: 额外的 JSON 格式元数据 + in: formData + name: metadata + type: string + produces: + - application/json + responses: + "200": + description: 上传成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.Upload' + type: object + "400": + description: 请求参数错误或文件受限 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 上传文件 + tags: + - upload + /api/v1/upload/{id}: + delete: + description: 将当前用户本人的文件状态置为 deleted(软删除) + parameters: + - description: 文件 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/response.Any' + "403": + description: 无权操作 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 文件不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 删除我的文件 + tags: + - upload + put: + consumes: + - application/json + description: 更新当前用户本人的文件名或访问权限模式 (AccessMode) + parameters: + - description: 文件 ID + in: path + name: id + required: true + type: string + - description: 更新字段 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.updateMyFileRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/model.Upload' + type: object + "403": + description: 无权操作 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 文件不存在 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 更新我的文件信息 + tags: + - upload + /api/v1/upload/my: + get: + description: 分页获取当前登录用户上传的文件,支持文件名关键词、业务类型、扩展名过滤 + parameters: + - description: 页码(默认 1) + in: query + name: page + type: integer + - description: 每页数量(默认 20,最大 100) + in: query + name: page_size + type: integer + - description: 文件名关键词(模糊匹配) + in: query + name: keyword + type: string + - description: 业务分类过滤 + in: query + name: type + type: string + - description: 扩展名过滤 + in: query + name: extension + type: string + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Any' + - properties: + data: + $ref: '#/definitions/handler.listMyFilesResponse' + type: object + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + security: + - SessionCookie: [] + summary: 获取我的文件列表 + tags: + - upload /api/v1/user-info: get: description: 返回当前登录用户的基本信息及余额数据,需要登录。包括用户 ID、用户名、信任等级、各类余额信息等。 @@ -10018,6 +10568,45 @@ paths: summary: 发送邮箱验证码 tags: - user + /f/{id}: + get: + description: 根据文件 ID 获取并提供已上传的临时或正式文件,若配置了缓存则优先走本地缓存,否则从 S3 等后端存储读取并流式返回 + parameters: + - description: 文件 ID + in: path + name: id + required: true + type: string + - description: 图片质量 (low, medium, high, origin),默认为 origin + in: query + name: quality + type: string + produces: + - application/octet-stream + responses: + "200": + description: 成功获取文件内容 + schema: + type: file + "400": + description: 文件 ID 格式错误 + schema: + $ref: '#/definitions/response.Any' + "401": + description: 未登录 + schema: + $ref: '#/definitions/response.Any' + "404": + description: 文件未找到 + schema: + $ref: '#/definitions/response.Any' + "500": + description: 服务内部错误 + schema: + $ref: '#/definitions/response.Any' + summary: 获取已上传文件 + tags: + - upload /robots.txt: get: description: 根据系统配置决定是否允许搜索引擎检索,并返回相应的 robots.txt 文件内容 diff --git a/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx b/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx index 21834f2a..aac38819 100644 --- a/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx +++ b/Wavelet/frontend/app/(main)/admin/settings/components/openflare-ops.tsx @@ -27,9 +27,10 @@ import {Textarea} from "@/components/ui/textarea" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import type {DatabaseCleanupTarget} from "@/lib/services/openflare" -import {NodeService, OptionService, StatusService, UpdateService, UptimeKumaService,} from "@/lib/services/openflare" +import {AdminStatusService} from "@/lib/services/admin" +import {NodeService, OptionService, StatusService, UptimeKumaService,} from "@/lib/services/openflare" import {VersionUpgradeDialog} from "@/app/(main)/components/version-upgrade-dialog" -import {openflareLatestReleaseQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade" +import {adminUpdateStatusQueryKey, openflarePublicStatusQueryKey,} from "@/lib/hooks/use-openflare-server-upgrade" import { agentOptionEntries, @@ -102,8 +103,8 @@ export function OpenFlareOpsSettings() { }) const releaseQuery = useQuery({ - queryKey: openflareLatestReleaseQueryKey("stable"), - queryFn: () => UpdateService.getLatestRelease("stable"), + queryKey: adminUpdateStatusQueryKey, + queryFn: () => AdminStatusService.getUpdateStatus(), }) useEffect(() => { @@ -609,7 +610,7 @@ export function OpenFlareOpsSettings() {
版本信息 - 检查 GitHub Release、在线升级或手动上传服务端二进制包。 + 检查上游 GitHub Release 并升级当前服务。
- ) : null} - +

{update?.latest_version || '未检查'}

{canUpgrade ? ( ) : null} @@ -162,240 +123,101 @@ export function VersionUpgradeDialog({ ) : null} - - - 在线升级 - {canUpgrade ? 手动升级 : null} - {release ? 升级日志 : null} - + {!isInitialLoading && !releaseErrorMessage && !update ? ( +
+ 尚未检查更新,点击「检查更新」后展示 GitHub Release 信息。 +
+ ) : null} - - {!isInitialLoading && !releaseErrorMessage && !release ? ( -
- 尚未检查{selectedChannelLabel},点击「检查{selectedChannelLabel}」后展示 GitHub Release 信息。 + {update ? ( + + + GitHub Release · {update.latest_version} + + {update.published_at + ? `发布时间:${formatRelativeTime(update.published_at)} · ${formatDateTime(update.published_at)}` + : '未提供发布时间'} + + + +
+ + {update.update_available ? '发现新版本' : '已经是最新版本'} + + {update.prerelease ? ( + Preview 发布 + ) : ( + 正式发布 + )} + {!update.can_upgrade ? ( + 当前平台不支持自动升级 + ) : null}
- ) : null} - {release ? ( - - - - GitHub {selectedChannelLabel} · {release.tag_name} - - - {release.published_at - ? `发布时间:${formatRelativeTime(release.published_at)} · ${formatDateTime(release.published_at)}` - : '未提供发布时间'} - - - -
- - {release.has_update ? '发现新版本' : '已经是最新版本'} - - {release.prerelease ? ( - Preview 发布 - ) : ( - 正式发布 - )} - {!release.upgrade_supported ? ( - 当前平台不支持自动升级 - ) : null} - {release.in_progress ? ( - 升级任务执行中 - ) : null} -
+
+ + {update.release_notes || '暂无更新说明'} + +
-
- - {release.body || '暂无更新说明'} - -
+ {update.release_url ? ( + + 查看发布详情 + + + ) : null} - {release.html_url ? ( - - 查看发布详情 - - - ) : null} - - {canUpgrade ? ( -
+ {canUpgrade ? ( +
+ + -
- ) : null} - - - ) : null} - + + + + 升级到 {update.latest_version}? + + 服务将下载并校验 {update.asset_name},随后替换当前二进制并重启。请确保安装目录可写,且服务允许原地重启。 + + + + 取消 + 确认升级 + + + +
+ ) : null} - {canUpgrade ? ( - - - - 手动升级 - - 支持上传已编译好的当前平台 Server 可执行文件。 - - - -
- - { - setSelectedBinary(event.target.files?.[0] ?? null); - }} - /> -
- -
- {canConfirmManualUpgrade ? ( - - ) : ( - - )} -
- - {isUploadingBinary ? ( -
-
- {uploadPhaseLabel} - {uploadProgress}% -
- -
- ) : null} - - {manualErrorMessage ? ( -
- {manualErrorMessage} -
- ) : null} - - {!manualErrorMessage && manualStatusMessage ? ( -
- {manualStatusMessage} -
- ) : null} - - {uploadedBinary ? ( -
-
- - {uploadedBinary.ready_to_upgrade - ? '可确认升级' - : uploadedBinary.has_update - ? '待确认' - : '不可升级'} - - {!uploadedBinary.upgrade_supported ? ( - 当前版本不支持手动升级 - ) : null} -
- -
- - - - -
- -

- {uploadedBinary.comparison_message} -

-
- ) : ( -
- 尚未上传升级包 -
- )} -
-
-
- ) : null} - - {release ? ( - - - - ) : null} - + {!update.can_upgrade ? ( +

+ {update.current_version === 'dev' + ? '开发构建没有可比较的 Release 版本,不能执行自动升级。' + : update.update_available + ? '当前平台暂不支持自动替换二进制,请从 Release 页面手动升级。' + : '当前版本无需升级。'} +

+ ) : null} +
+
+ ) : null}
); -} - -function InfoCell({label, value}: {label: string; value: string}) { - return ( -
-

{label}

-

{value}

-
- ); -} - -export type {ReleaseChannel}; +} \ No newline at end of file diff --git a/Wavelet/frontend/lib/hooks/use-openflare-server-upgrade.ts b/Wavelet/frontend/lib/hooks/use-openflare-server-upgrade.ts index f49df76d..46f2e8bc 100644 --- a/Wavelet/frontend/lib/hooks/use-openflare-server-upgrade.ts +++ b/Wavelet/frontend/lib/hooks/use-openflare-server-upgrade.ts @@ -3,36 +3,13 @@ import {useMutation, useQuery} from '@tanstack/react-query'; import {useCallback, useEffect, useRef, useState} from 'react'; -import type { - LatestReleaseInfo, - ReleaseChannel, - UpgradeStreamSnapshot, - UploadedServerBinaryInfo, -} from '@/lib/services/openflare'; -import {StatusService, UpdateService} from '@/lib/services/openflare'; - -const UPGRADE_STATUS_POLL_INTERVAL = 3000; +import {AdminStatusService} from '@/lib/services/admin'; +import type {AppUpdateStatus} from '@/lib/services/admin/types'; +import {StatusService} from '@/lib/services/openflare'; export const openflarePublicStatusQueryKey = ['openflare', 'public-status'] as const; -export const openflareLatestReleaseQueryKey = (channel: ReleaseChannel = 'stable') => - ['openflare', 'latest-release', channel] as const; - -export function mergeReleaseWithUpgradeStream( - release: LatestReleaseInfo | null | undefined, - stream: UpgradeStreamSnapshot | null, -) { - if (!release || !stream) { - return release; - } - - return { - ...release, - in_progress: stream.in_progress, - upgrade_status: stream.upgrade_status, - upgrade_logs: stream.upgrade_logs, - }; -} +export const adminUpdateStatusQueryKey = ['admin', 'update'] as const; export function useOpenFlareServerUpgrade({ open, @@ -41,15 +18,8 @@ export function useOpenFlareServerUpgrade({ open: boolean; canUpgrade: boolean; }) { - const [selectedChannel, setSelectedChannel] = useState('stable'); const [feedback, setFeedback] = useState(null); - const [manualStatusMessage, setManualStatusMessage] = useState(null); - const [manualErrorMessage, setManualErrorMessage] = useState(null); - const [uploadedBinary, setUploadedBinary] = useState(null); - const [uploadProgress, setUploadProgress] = useState(0); - const [upgradeStream, setUpgradeStream] = useState(null); - const upgradeRefreshPendingRef = useRef(false); const upgradeReloadStartedRef = useRef(false); const upgradeReloadTimerRef = useRef(null); @@ -59,30 +29,11 @@ export function useOpenFlareServerUpgrade({ enabled: open, }); - const stableReleaseQuery = useQuery({ - queryKey: openflareLatestReleaseQueryKey('stable'), - queryFn: () => UpdateService.getLatestRelease('stable'), + const updateQuery = useQuery({ + queryKey: adminUpdateStatusQueryKey, + queryFn: () => AdminStatusService.getUpdateStatus(), enabled: open && canUpgrade, - refetchInterval: (query) => { - const release = query.state.data; - if (open && release?.in_progress) { - return UPGRADE_STATUS_POLL_INTERVAL; - } - return 60 * 60 * 1000; - }, - }); - - const previewReleaseQuery = useQuery({ - queryKey: openflareLatestReleaseQueryKey('preview'), - queryFn: () => UpdateService.getLatestRelease('preview'), - enabled: false, - refetchInterval: (query) => { - const release = query.state.data; - if (open && release?.in_progress) { - return UPGRADE_STATUS_POLL_INTERVAL; - } - return false; - }, + staleTime: 5 * 60 * 1000, }); const scheduleUpgradePageReload = useCallback(() => { @@ -113,236 +64,64 @@ export function useOpenFlareServerUpgrade({ }; }, []); - useEffect(() => { - if (!open || !canUpgrade) { - setUpgradeStream(null); - return; - } - - let closed = false; - let reconnectTimer: number | null = null; - let socket: WebSocket | null = null; - - const connect = () => { - if (closed) { - return; - } - - socket = UpdateService.createUpgradeLogsWebSocket(); - if (!socket) { - return; - } - - socket.onmessage = (event) => { - const snapshot = UpdateService.parseUpgradeStreamSnapshot(String(event.data)); - if (snapshot) { - if (snapshot.in_progress || snapshot.upgrade_status === 'succeeded') { - upgradeRefreshPendingRef.current = true; - } - if (snapshot.upgrade_status === 'failed') { - upgradeRefreshPendingRef.current = false; - } - setUpgradeStream(snapshot); - } - }; - - socket.onclose = () => { - if (closed) { - return; - } - if (upgradeRefreshPendingRef.current) { - scheduleUpgradePageReload(); - return; - } - reconnectTimer = window.setTimeout(connect, 1500); - }; - }; - - connect(); - - return () => { - closed = true; - if (reconnectTimer !== null) { - window.clearTimeout(reconnectTimer); - } - socket?.close(); - }; - }, [canUpgrade, open, scheduleUpgradePageReload]); - const upgradeMutation = useMutation({ - mutationFn: (channel: ReleaseChannel) => UpdateService.upgradeServer(channel), - onSuccess: (release) => { - upgradeRefreshPendingRef.current = true; - setUploadedBinary(null); - setManualStatusMessage(null); - setManualErrorMessage(null); - setFeedback( - `服务升级任务已启动,目标版本 ${release.tag_name}(${release.channel === 'preview' ? '预览版' : '正式版'})。页面可能短暂不可用。`, - ); - void stableReleaseQuery.refetch(); - if (release.channel === 'preview') { - void previewReleaseQuery.refetch(); - } + mutationFn: () => AdminStatusService.applyUpdate(), + onSuccess: () => { + scheduleUpgradePageReload(); + setFeedback('升级包已校验完成,服务正在重启。'); }, onError: (error) => { setFeedback(error instanceof Error ? error.message : '升级失败,请稍后重试。'); }, }); - const uploadBinaryMutation = useMutation({ - mutationFn: (binary: File) => - UpdateService.uploadServerBinary(binary, (progress) => { - setUploadProgress(progress); - }), - onSuccess: (candidate) => { - setUploadProgress(0); - setFeedback(null); - setManualErrorMessage(null); - setUploadedBinary(candidate); - setManualStatusMessage(candidate.comparison_message); - }, - onError: (error) => { - setUploadProgress(0); - setUploadedBinary(null); - setManualStatusMessage(null); - setManualErrorMessage( - error instanceof Error ? error.message : '上传升级包失败,请稍后重试。', - ); - }, - }); - - const confirmManualUpgradeMutation = useMutation({ - mutationFn: (uploadToken: string) => UpdateService.confirmManualServerUpgrade(uploadToken), - onSuccess: (candidate) => { - upgradeRefreshPendingRef.current = true; - setFeedback(null); - setManualErrorMessage(null); - setUploadedBinary(candidate); - setManualStatusMessage( - `手动升级任务已启动,目标版本 ${candidate.detected_version}。页面可能短暂不可用。`, - ); - void stableReleaseQuery.refetch(); - void previewReleaseQuery.refetch(); - }, - onError: (error) => { - setManualStatusMessage(null); - setManualErrorMessage( - error instanceof Error ? error.message : '确认手动升级失败,请稍后重试。', - ); - }, - }); - const resetTransientState = useCallback(() => { - setSelectedChannel('stable'); setFeedback(null); - setManualStatusMessage(null); - setManualErrorMessage(null); - setUploadedBinary(null); - setUploadProgress(0); - setUpgradeStream(null); - upgradeRefreshPendingRef.current = false; upgradeReloadStartedRef.current = false; }, []); const handleOpen = useCallback(() => { resetTransientState(); if (canUpgrade) { - void stableReleaseQuery.refetch(); + void updateQuery.refetch(); } - }, [canUpgrade, resetTransientState, stableReleaseQuery]); + }, [canUpgrade, resetTransientState, updateQuery]); const handleCheckRelease = useCallback(() => { setFeedback(null); if (!canUpgrade) { return; } - if (selectedChannel === 'preview') { - void previewReleaseQuery.refetch(); - return; - } - void stableReleaseQuery.refetch(); - }, [canUpgrade, previewReleaseQuery, selectedChannel, stableReleaseQuery]); - - const handleChannelChange = useCallback((channel: ReleaseChannel) => { - setSelectedChannel(channel); - setFeedback(null); - }, []); + void updateQuery.refetch(); + }, [canUpgrade, updateQuery]); const handleUpgrade = useCallback(() => { setFeedback(null); - setManualStatusMessage(null); - setManualErrorMessage(null); - upgradeMutation.mutate(selectedChannel); - }, [selectedChannel, upgradeMutation]); - - const handleUploadBinary = useCallback( - (binary: File) => { - setUploadProgress(0); - setManualStatusMessage(null); - setManualErrorMessage(null); - uploadBinaryMutation.mutate(binary); - }, - [uploadBinaryMutation], - ); - - const handleConfirmManualUpgrade = useCallback(() => { - if (!uploadedBinary?.upload_token) { - setManualStatusMessage(null); - setManualErrorMessage('请先上传并检查升级包。'); - return; - } - setFeedback(null); - setManualStatusMessage(null); - setManualErrorMessage(null); - confirmManualUpgradeMutation.mutate(uploadedBinary.upload_token); - }, [confirmManualUpgradeMutation, uploadedBinary?.upload_token]); - - const selectedRelease = - selectedChannel === 'preview' ? previewReleaseQuery.data : stableReleaseQuery.data; - const releaseWithStream = mergeReleaseWithUpgradeStream(selectedRelease, upgradeStream); - const selectedReleaseError = - selectedChannel === 'preview' ? previewReleaseQuery.error : stableReleaseQuery.error; - const isSelectedReleaseError = - selectedChannel === 'preview' ? previewReleaseQuery.isError : stableReleaseQuery.isError; - const isChecking = - selectedChannel === 'preview' - ? previewReleaseQuery.isFetching - : stableReleaseQuery.isFetching; - const isInitialLoading = - (selectedChannel === 'preview' - ? previewReleaseQuery.isLoading && !previewReleaseQuery.data - : stableReleaseQuery.isLoading && !stableReleaseQuery.data) && canUpgrade; + upgradeMutation.mutate(); + }, [upgradeMutation]); + const update = updateQuery.data; const releaseErrorMessage = feedback || - (isSelectedReleaseError - ? selectedReleaseError instanceof Error - ? selectedReleaseError.message + (updateQuery.isError + ? updateQuery.error instanceof Error + ? updateQuery.error.message : '版本检查失败,请稍后重试。' : undefined); - const currentVersion = statusQuery.data?.version || releaseWithStream?.current_version || 'unknown'; + const currentVersion = statusQuery.data?.version || update?.current_version || 'unknown'; return { currentVersion, - selectedChannel, - release: releaseWithStream, - uploadedBinary, + update, releaseErrorMessage, - manualStatusMessage, - manualErrorMessage, - isInitialLoading, - isChecking, + isInitialLoading: updateQuery.isLoading && !updateQuery.data && canUpgrade, + isChecking: updateQuery.isFetching, isUpgrading: upgradeMutation.isPending, - isUploadingBinary: uploadBinaryMutation.isPending, - uploadProgress, - isConfirmingManualUpgrade: confirmManualUpgradeMutation.isPending, - stableRelease: stableReleaseQuery.data, handleOpen, handleCheckRelease, - handleChannelChange, handleUpgrade, - handleUploadBinary, - handleConfirmManualUpgrade, }; -} \ No newline at end of file +} + +export type {AppUpdateStatus}; \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/index.ts b/Wavelet/frontend/lib/services/index.ts index 1bc67f3e..431e38ce 100644 --- a/Wavelet/frontend/lib/services/index.ts +++ b/Wavelet/frontend/lib/services/index.ts @@ -27,7 +27,6 @@ import {PushService} from './push'; import {AdminUploadService, UploadService} from './upload'; import {UserService} from './user'; import { - AboutService, AccessLogService, ApplyLogService, ConfigVersionService, @@ -40,7 +39,6 @@ import { ProxyRouteService, StatusService, TlsCertificateService, - UpdateService, UptimeKumaService, WafService, WebsiteService, @@ -62,7 +60,6 @@ const services = { adminUpload: AdminUploadService, dbManage: DbManageService, push: PushService, - openflareAbout: AboutService, openflareNode: NodeService, openflareProxyRoute: ProxyRouteService, openflareConfigVersion: ConfigVersionService, @@ -77,7 +74,6 @@ const services = { openflareAccessLog: AccessLogService, openflareOption: OptionService, openflareUptimeKuma: UptimeKumaService, - openflareUpdate: UpdateService, openflareStatus: StatusService, } as const; @@ -183,7 +179,6 @@ export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesReques // ==================== OpenFlare 业务服务导出 ==================== export { - AboutService, OpenFlareBaseService, NodeService, ProxyRouteService, @@ -199,7 +194,6 @@ export { AccessLogService, OptionService, StatusService, - UpdateService, UptimeKumaService, } from './openflare'; @@ -227,6 +221,5 @@ export type { OptionItem, GeoIPLookupResult, DatabaseCleanupResult, - LatestReleaseInfo, OpenFlarePublicStatus, } from './openflare'; diff --git a/Wavelet/frontend/lib/services/openflare/about.service.ts b/Wavelet/frontend/lib/services/openflare/about.service.ts deleted file mode 100644 index 3c11eeac..00000000 --- a/Wavelet/frontend/lib/services/openflare/about.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {OpenFlareBaseService} from './base.service'; - -export class AboutService extends OpenFlareBaseService { - static getAboutContent(): Promise { - return this.get('/about'); - } -} \ No newline at end of file diff --git a/Wavelet/frontend/lib/services/openflare/index.ts b/Wavelet/frontend/lib/services/openflare/index.ts index 71e8bef5..f1e29962 100644 --- a/Wavelet/frontend/lib/services/openflare/index.ts +++ b/Wavelet/frontend/lib/services/openflare/index.ts @@ -1,4 +1,3 @@ -export { AboutService } from './about.service'; export { OpenFlareBaseService } from './base.service'; export { NodeService } from './node.service'; @@ -15,7 +14,6 @@ export { OriginService } from './origin.service'; export { AccessLogService } from './access-log.service'; export { OptionService } from './option.service'; export { UptimeKumaService } from './uptimekuma.service'; -export { UpdateService } from './update.service'; export { StatusService } from './status.service'; export type { @@ -93,11 +91,7 @@ export type { DatabaseCleanupPayload, DatabaseCleanupResult, DatabaseCleanupTarget, - LatestReleaseInfo, OpenFlarePublicStatus, - UpgradeLogItem, - UpgradeStreamSnapshot, - UploadedServerBinaryInfo, OriginDetail, OriginItem, OriginMutationPayload, @@ -119,7 +113,6 @@ export type { TlsCertificateMutationPayload, } from './types'; -import {AboutService} from './about.service'; import {AccessLogService} from './access-log.service'; import {ApplyLogService} from './apply-log.service'; import {ConfigVersionService} from './config-version.service'; @@ -128,7 +121,6 @@ import {DnsAccountService} from './dns-account.service'; import {NodeService} from './node.service'; import {OptionService} from './option.service'; import {StatusService} from './status.service'; -import {UpdateService} from './update.service'; import {UptimeKumaService} from './uptimekuma.service'; import {OriginService} from './origin.service'; import {PagesService} from './pages.service'; @@ -138,7 +130,6 @@ import {WafService} from './waf.service'; import {WebsiteService} from './website.service'; export const openflareServices = { - about: AboutService, node: NodeService, proxyRoute: ProxyRouteService, configVersion: ConfigVersionService, @@ -153,6 +144,5 @@ export const openflareServices = { accessLog: AccessLogService, option: OptionService, uptimeKuma: UptimeKumaService, - update: UpdateService, status: StatusService, } as const; diff --git a/Wavelet/frontend/lib/services/openflare/types.ts b/Wavelet/frontend/lib/services/openflare/types.ts index b287fc09..fbd87362 100644 --- a/Wavelet/frontend/lib/services/openflare/types.ts +++ b/Wavelet/frontend/lib/services/openflare/types.ts @@ -696,45 +696,6 @@ export interface OpenFlarePublicStatus { system_name: string; } -export interface UpgradeLogItem { - level: string; - message: string; - created_at: string; -} - -export interface LatestReleaseInfo { - tag_name: string; - body: string; - html_url: string; - published_at: string; - channel: ReleaseChannel; - prerelease: boolean; - current_version: string; - has_update: boolean; - upgrade_supported: boolean; - in_progress: boolean; - upgrade_status: 'idle' | 'running' | 'succeeded' | 'failed' | string; - upgrade_logs: UpgradeLogItem[]; -} - -export interface UpgradeStreamSnapshot { - in_progress: boolean; - upgrade_status: 'idle' | 'running' | 'succeeded' | 'failed' | string; - upgrade_logs: UpgradeLogItem[]; -} - -export interface UploadedServerBinaryInfo { - upload_token: string; - file_name: string; - detected_version: string; - current_version: string; - has_update: boolean; - upgrade_supported: boolean; - ready_to_upgrade: boolean; - comparison_message: string; - uploaded_at: string; -} - export interface WAFRuleGroup { id: number; name: string; diff --git a/Wavelet/frontend/lib/services/openflare/update.service.ts b/Wavelet/frontend/lib/services/openflare/update.service.ts deleted file mode 100644 index be2e2c08..00000000 --- a/Wavelet/frontend/lib/services/openflare/update.service.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type {InternalAxiosRequestConfig} from 'axios'; - -import {getApiBaseUrl} from '@/lib/services/core/config'; -import {ApiErrorBase} from '@/lib/services/core/errors'; -import type {ApiResponse} from '@/lib/services/core'; - -import {OpenFlareBaseService} from './base.service'; -import type {LatestReleaseInfo, ReleaseChannel, UpgradeStreamSnapshot, UploadedServerBinaryInfo} from './types'; - -/** - * OpenFlare 服务端升级 API(`/api/v1/d/update/*`)。 - */ -export class UpdateService extends OpenFlareBaseService { - protected static override readonly basePath: string = '/api/v1/d/update'; - - private static getApiUrl(path: string): string { - const baseURL = getApiBaseUrl(); - const fullPath = this.getFullPath(path); - if (!baseURL) { - return fullPath; - } - return `${baseURL.replace(/\/$/, '')}${fullPath}`; - } - - static getLatestRelease(channel: ReleaseChannel = 'stable'): Promise { - return this.get('/latest-release', { channel }); - } - - static upgradeServer(channel: ReleaseChannel = 'stable'): Promise { - return this.post('/upgrade', { channel }); - } - - static uploadServerBinary( - binary: File, - onProgress?: (progress: number) => void, - ): Promise { - const formData = new FormData(); - formData.append('binary', binary); - - if (!onProgress) { - return this.post('/manual-upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - } as InternalAxiosRequestConfig); - } - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('POST', this.getApiUrl('/manual-upload')); - xhr.withCredentials = true; - - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable) { - onProgress(Math.round((event.loaded / event.total) * 100)); - } - }); - - xhr.addEventListener('load', () => { - let payload: ApiResponse | null = null; - try { - payload = JSON.parse( - xhr.responseText, - ) as ApiResponse; - } catch { - payload = null; - } - if (xhr.status < 200 || xhr.status >= 300) { - reject( - new ApiErrorBase( - payload?.message || `请求失败(${xhr.status})`, - undefined, - xhr.status, - ), - ); - return; - } - if (!payload) { - reject(new ApiErrorBase('响应格式无效', undefined, xhr.status)); - return; - } - resolve(payload.data); - }); - - xhr.addEventListener('error', () => { - reject(new ApiErrorBase('上传过程中网络连接中断,请检查网络后重试', undefined, 0)); - }); - - xhr.send(formData); - }); - } - - static confirmManualServerUpgrade(uploadToken: string): Promise { - return this.post('/manual-upgrade', { - upload_token: uploadToken, - }); - } - - static createUpgradeLogsWebSocket(): WebSocket | null { - if (typeof window === 'undefined') { - return null; - } - - const apiUrl = this.getApiUrl('/logs/ws'); - const resolvedUrl = apiUrl.startsWith('http://') - ? `ws://${apiUrl.slice('http://'.length)}` - : apiUrl.startsWith('https://') - ? `wss://${apiUrl.slice('https://'.length)}` - : `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${apiUrl}`; - - return new WebSocket(resolvedUrl); - } - - static parseUpgradeStreamSnapshot(rawMessage: string): UpgradeStreamSnapshot | null { - try { - const parsed = JSON.parse(rawMessage) as Partial; - if ( - typeof parsed.in_progress !== 'boolean' || - typeof parsed.upgrade_status !== 'string' || - !Array.isArray(parsed.upgrade_logs) - ) { - return null; - } - return { - in_progress: parsed.in_progress, - upgrade_status: parsed.upgrade_status, - upgrade_logs: parsed.upgrade_logs, - }; - } catch { - return null; - } - } -} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/option/logics.go b/Wavelet/internal/apps/openflare/option/logics.go index 2d4604dc..af62588e 100644 --- a/Wavelet/internal/apps/openflare/option/logics.go +++ b/Wavelet/internal/apps/openflare/option/logics.go @@ -146,13 +146,6 @@ func getNotice(ctx context.Context) (string, error) { return model.OptionValue("Notice"), nil } -func getAbout(ctx context.Context) (string, error) { - if err := EnsureInitialized(ctx); err != nil { - return "", err - } - return model.OptionValue("About"), nil -} - func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) { if err := EnsureInitialized(ctx); err != nil { return nil, err diff --git a/Wavelet/internal/apps/openflare/option/logics_test.go b/Wavelet/internal/apps/openflare/option/logics_test.go index b749f9a8..36e87c01 100644 --- a/Wavelet/internal/apps/openflare/option/logics_test.go +++ b/Wavelet/internal/apps/openflare/option/logics_test.go @@ -73,21 +73,16 @@ func TestUpdateOptionHotReloadsOptionMap(t *testing.T) { assert.Equal(t, "HotReloaded", model.SystemName) } -func TestGetNoticeAndAbout(t *testing.T) { +func TestGetNotice(t *testing.T) { cleanup := setupOptionTestDB(t) defer cleanup() ctx := context.Background() require.NoError(t, updateOption(ctx, model.OpenFlareOption{Key: "Notice", Value: "hello"})) - require.NoError(t, updateOption(ctx, model.OpenFlareOption{Key: "About", Value: "about-us"})) notice, err := getNotice(ctx) require.NoError(t, err) assert.Equal(t, "hello", notice) - - about, err := getAbout(ctx) - require.NoError(t, err) - assert.Equal(t, "about-us", about) } func TestLookupGeoIPDisabledProvider(t *testing.T) { diff --git a/Wavelet/internal/apps/openflare/option/routers.go b/Wavelet/internal/apps/openflare/option/routers.go index e846a2b5..d6052edb 100644 --- a/Wavelet/internal/apps/openflare/option/routers.go +++ b/Wavelet/internal/apps/openflare/option/routers.go @@ -50,24 +50,6 @@ func GetNoticeHandler(c *gin.Context) { c.JSON(http.StatusOK, response.OK(notice)) } -// getAboutHandler 获取关于信息。 -// @Summary 获取关于信息 -// @Description 返回 OpenFlare 关于页面文本,无需登录 -// @Tags openflare-option -// @Produce json -// @Success 200 {object} response.Any{data=string} "关于信息" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 500 {object} response.Any "内部错误" -// @Router /api/v1/d/about [get] -// GetAboutHandler returns the about content. -func GetAboutHandler(c *gin.Context) { - about, err := getAbout(c.Request.Context()) - if apiutil.AbortBadRequestOnError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(about)) -} - // listOptionsHandler 列出全部配置项。 // @Summary 列出 OpenFlare 配置项 // @Description 返回全部非敏感 OpenFlare 配置项,需要管理员权限 diff --git a/Wavelet/internal/apps/openflare/update/logics.go b/Wavelet/internal/apps/openflare/update/logics.go deleted file mode 100644 index 7eb1d4c8..00000000 --- a/Wavelet/internal/apps/openflare/update/logics.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package update - -import ( - "context" - "errors" - "fmt" - "runtime" - "strings" - "sync" - "time" - - "github.com/Rain-kl/Wavelet/internal/apps/admin/updater" - "github.com/Rain-kl/Wavelet/pkg/logger" -) - -const ( - channelStable = "stable" - channelPreview = "preview" -) - -// UpgradeLogRecord is a single upgrade log entry for the legacy update API. -type UpgradeLogRecord struct { - Level string `json:"level"` - Message string `json:"message"` - CreatedAt time.Time `json:"created_at"` -} - -// LatestReleaseView mirrors the legacy OpenFlare latest-release payload. -type LatestReleaseView struct { - TagName string `json:"tag_name"` - Body string `json:"body"` - HTMLURL string `json:"html_url"` - PublishedAt string `json:"published_at"` - Channel string `json:"channel"` - Prerelease bool `json:"prerelease"` - CurrentVersion string `json:"current_version"` - HasUpdate bool `json:"has_update"` - UpgradeSupported bool `json:"upgrade_supported"` - InProgress bool `json:"in_progress"` - UpgradeStatus string `json:"upgrade_status"` - UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"` -} - -// StreamSnapshot is pushed over the upgrade logs websocket. -type StreamSnapshot struct { - InProgress bool `json:"in_progress"` - UpgradeStatus string `json:"upgrade_status"` - UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"` -} - -type upgradeRequest struct { - Channel string `json:"channel"` -} - -var upgradeState struct { - sync.Mutex - inProgress bool - status string - logs []UpgradeLogRecord -} - -var upgradeSubscribers struct { - sync.Mutex - nextID int - listeners map[int]chan StreamSnapshot -} - -func init() { - upgradeSubscribers.listeners = make(map[int]chan StreamSnapshot) - upgradeState.status = "idle" -} - -func normalizeChannel(channel string) string { - switch strings.ToLower(strings.TrimSpace(channel)) { - case channelPreview: - return channelPreview - default: - return channelStable - } -} - -func isDevBuild(version string) bool { - version = strings.TrimSpace(version) - return version == "" || strings.EqualFold(version, "dev") -} - -func mapStatusToLatestRelease(status updater.Status, channel string) *LatestReleaseView { - inProgress, upgradeStatus, logs := snapshotUpgradeState() - view := &LatestReleaseView{ - TagName: status.LatestVersion, - Body: status.ReleaseNotes, - HTMLURL: status.ReleaseURL, - PublishedAt: status.PublishedAt, - Channel: channel, - Prerelease: status.Prerelease, - CurrentVersion: status.CurrentVersion, - HasUpdate: status.UpdateAvailable, - UpgradeSupported: !isDevBuild(status.CurrentVersion) && runtime.GOOS != "windows", - InProgress: inProgress || updater.IsUpgrading(), - UpgradeStatus: upgradeStatus, - UpgradeLogs: logs, - } - if channel == channelPreview && status.Prerelease { - view.HasUpdate = !isDevBuild(status.CurrentVersion) - } - return view -} - -// GetLatestRelease returns the newest upstream release for the requested channel. -func GetLatestRelease(ctx context.Context, channel string) (*LatestReleaseView, error) { - normalizedChannel := normalizeChannel(channel) - status, err := updater.GetStatus(ctx) - if err != nil { - return nil, err - } - return mapStatusToLatestRelease(status, normalizedChannel), nil -} - -// ScheduleUpgrade downloads the latest release and restarts with the staged binary. -func ScheduleUpgrade(ctx context.Context, channel string) (*LatestReleaseView, error) { - normalizedChannel := normalizeChannel(channel) - - upgradeState.Lock() - if upgradeState.inProgress || updater.IsUpgrading() { - upgradeState.Unlock() - return nil, errors.New("服务升级正在执行中,请稍后再试") - } - resetUpgradeLogsLocked() - upgradeState.inProgress = true - upgradeState.status = "running" - appendUpgradeLogLocked("info", fmt.Sprintf("Automatic upgrade scheduled for channel: %s.", normalizedChannel)) - upgradeState.Unlock() - broadcastUpgradeSnapshot() - - executable, stagedBinary, status, err := updater.PrepareUpgrade(ctx) - if err != nil { - recordUpgradeFailure(err) - return nil, err - } - - view := mapStatusToLatestRelease(status, normalizedChannel) - view.InProgress = true - view.UpgradeStatus = "running" - view.UpgradeLogs = snapshotUpgradeLogs() - - appendUpgradeLogLocked("info", fmt.Sprintf("Upgrade package prepared: %s.", status.LatestVersion)) - broadcastUpgradeSnapshot() - - go func() { - time.Sleep(time.Second) - if err := updater.ApplyPreparedUpgrade(executable, stagedBinary); err != nil { - updater.FinishUpgrade() - recordUpgradeFailure(err) - logger.ErrorF(context.Background(), "[Update] replace and restart failed: %v", err) - } - }() - - return view, nil -} - -// UploadManualBinary is disabled in the legacy OpenFlare server. -func UploadManualBinary() error { - return errors.New("手动升级功能已禁用") -} - -// ConfirmManualUpgrade is disabled in the legacy OpenFlare server. -func ConfirmManualUpgrade() error { - return errors.New("手动升级功能已禁用") -} - -// SubscribeUpgradeStream registers a listener for upgrade websocket snapshots. -func SubscribeUpgradeStream() (<-chan StreamSnapshot, func()) { - upgradeSubscribers.Lock() - defer upgradeSubscribers.Unlock() - - id := upgradeSubscribers.nextID - upgradeSubscribers.nextID++ - ch := make(chan StreamSnapshot, 1) - upgradeSubscribers.listeners[id] = ch - - unsubscribe := func() { - upgradeSubscribers.Lock() - defer upgradeSubscribers.Unlock() - if listener, ok := upgradeSubscribers.listeners[id]; ok { - delete(upgradeSubscribers.listeners, id) - close(listener) - } - } - - select { - case ch <- currentUpgradeSnapshot(): - default: - } - - return ch, unsubscribe -} - -func snapshotUpgradeState() (bool, string, []UpgradeLogRecord) { - upgradeState.Lock() - defer upgradeState.Unlock() - return upgradeState.inProgress || updater.IsUpgrading(), upgradeState.status, cloneUpgradeLogsLocked() -} - -func snapshotUpgradeLogs() []UpgradeLogRecord { - upgradeState.Lock() - defer upgradeState.Unlock() - return cloneUpgradeLogsLocked() -} - -func currentUpgradeSnapshot() StreamSnapshot { - inProgress, status, logs := snapshotUpgradeState() - return StreamSnapshot{ - InProgress: inProgress, - UpgradeStatus: status, - UpgradeLogs: logs, - } -} - -func resetUpgradeLogsLocked() { - upgradeState.logs = nil -} - -func appendUpgradeLogLocked(level, message string) { - upgradeState.logs = append(upgradeState.logs, UpgradeLogRecord{ - Level: level, - Message: message, - CreatedAt: time.Now().UTC(), - }) -} - -func cloneUpgradeLogsLocked() []UpgradeLogRecord { - if len(upgradeState.logs) == 0 { - return []UpgradeLogRecord{} - } - cloned := make([]UpgradeLogRecord, len(upgradeState.logs)) - copy(cloned, upgradeState.logs) - return cloned -} - -func recordUpgradeFailure(err error) { - upgradeState.Lock() - upgradeState.inProgress = false - upgradeState.status = "failed" - appendUpgradeLogLocked("error", err.Error()) - upgradeState.Unlock() - broadcastUpgradeSnapshot() -} - -func broadcastUpgradeSnapshot() { - snapshot := currentUpgradeSnapshot() - upgradeSubscribers.Lock() - defer upgradeSubscribers.Unlock() - for _, listener := range upgradeSubscribers.listeners { - select { - case listener <- snapshot: - default: - } - } -} diff --git a/Wavelet/internal/apps/openflare/update/routers.go b/Wavelet/internal/apps/openflare/update/routers.go deleted file mode 100644 index b78a8808..00000000 --- a/Wavelet/internal/apps/openflare/update/routers.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package update - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "time" - - "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" - "github.com/Rain-kl/Wavelet/internal/common/response" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" -) - -var upgradeLogsUpgrader = websocket.Upgrader{ - CheckOrigin: func(_ *http.Request) bool { return true }, -} - - -// GetLatestReleaseHandler 获取最新 GitHub 发布版本。 -// @Summary 获取最新服务端发布版本 -// @Description 查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限 -// @Tags openflare-update -// @Produce json -// @Security SessionCookie -// @Param channel query string false "发布渠道(stable/preview)" -// @Success 200 {object} response.Any{data=update.LatestReleaseView} "最新发布信息" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 403 {object} response.Any "无管理员权限" -// @Failure 500 {object} response.Any "内部错误" -// @Router /api/v1/d/update/latest-release [get] -func GetLatestReleaseHandler(c *gin.Context) { - release, err := GetLatestRelease(c.Request.Context(), c.Query("channel")) - if apiutil.AbortBadRequestOnError(c, err) { - return - } - c.JSON(http.StatusOK, response.OK(release)) -} - -// UpgradeServerHandler 调度自动升级任务。 -// @Summary 触发服务端自动升级 -// @Description 从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限 -// @Tags openflare-update -// @Accept json -// @Produce json -// @Security SessionCookie -// @Param request body update.upgradeRequest false "升级参数" -// @Success 200 {object} response.Any{data=update.LatestReleaseView} "升级任务已调度" -// @Failure 400 {object} response.Any "参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 403 {object} response.Any "无管理员权限" -// @Failure 500 {object} response.Any "内部错误" -// @Router /api/v1/d/update/upgrade [post] -func UpgradeServerHandler(c *gin.Context) { - var request upgradeRequest - if err := bindOptionalJSON(c.Request.Body, &request); err != nil { - response.AbortBadRequest(c, "无效的参数") - return - } - - release, err := ScheduleUpgrade(c.Request.Context(), request.Channel) - if apiutil.AbortBadRequestOnError(c, err) { - return - } - - c.JSON(http.StatusOK, response.OK(release)) -} - -// UploadManualServerBinaryHandler 上传手动升级二进制(已禁用)。 -// @Summary 上传手动升级二进制 -// @Description 上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限 -// @Tags openflare-update -// @Accept multipart/form-data -// @Produce json -// @Security SessionCookie -// @Param binary formData file true "服务端二进制文件" -// @Failure 400 {object} response.Any "功能已禁用或参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 403 {object} response.Any "无管理员权限" -// @Router /api/v1/d/update/manual-upload [post] -func UploadManualServerBinaryHandler(c *gin.Context) { - if apiutil.AbortBadRequestOnError(c, UploadManualBinary()) { - return - } -} - -// ConfirmManualServerUpgradeHandler 确认手动升级(已禁用)。 -// @Summary 确认手动服务端升级 -// @Description 确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限 -// @Tags openflare-update -// @Accept json -// @Produce json -// @Security SessionCookie -// @Failure 400 {object} response.Any "功能已禁用或参数错误" -// @Failure 401 {object} response.Any "未登录" -// @Failure 403 {object} response.Any "无管理员权限" -// @Router /api/v1/d/update/manual-upgrade [post] -func ConfirmManualServerUpgradeHandler(c *gin.Context) { - if apiutil.AbortBadRequestOnError(c, ConfirmManualUpgrade()) { - return - } -} - -// StreamServerUpgradeLogsHandler 通过 WebSocket 推送升级日志。 -// @Summary 流式获取服务端升级日志 -// @Description 通过 WebSocket 推送升级进度快照,需要管理员权限 -// @Tags openflare-update -// @Security SessionCookie -// @Success 101 {object} update.StreamSnapshot "WebSocket 升级日志流" -// @Failure 401 {object} response.Any "未登录" -// @Failure 403 {object} response.Any "无管理员权限" -// @Router /api/v1/d/update/logs/ws [get] -func StreamServerUpgradeLogsHandler(c *gin.Context) { - conn, err := upgradeLogsUpgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - return - } - defer func() { - _ = conn.Close() - }() - - updates, unsubscribe := SubscribeUpgradeStream() - defer unsubscribe() - - heartbeatTicker := time.NewTicker(15 * time.Second) - defer heartbeatTicker.Stop() - - for { - select { - case snapshot, ok := <-updates: - if !ok { - return - } - if err := conn.WriteJSON(snapshot); err != nil { - return - } - case <-heartbeatTicker.C: - if err := conn.WriteJSON(StreamSnapshot{}); err != nil { - return - } - case <-c.Request.Context().Done(): - return - } - } -} - -func bindOptionalJSON(body io.Reader, target any) error { - if err := json.NewDecoder(body).Decode(target); err != nil && !errors.Is(err, io.EOF) { - return err - } - return nil -} \ No newline at end of file diff --git a/Wavelet/internal/router/v1/openflare/register_option.go b/Wavelet/internal/router/v1/openflare/register_option.go index 9552f766..421ed345 100644 --- a/Wavelet/internal/router/v1/openflare/register_option.go +++ b/Wavelet/internal/router/v1/openflare/register_option.go @@ -12,7 +12,6 @@ import ( func registerOptionRoutes(apiGroup *gin.RouterGroup) { apiGroup.GET("/status", option.GetStatusHandler) apiGroup.GET("/notice", option.GetNoticeHandler) - apiGroup.GET("/about", option.GetAboutHandler) optionRoute := apiGroup.Group("/option") optionRoute.Use(apiutil.AdminMiddlewares()...) diff --git a/Wavelet/internal/router/v1/openflare/register_update.go b/Wavelet/internal/router/v1/openflare/register_update.go deleted file mode 100644 index 7d919cbf..00000000 --- a/Wavelet/internal/router/v1/openflare/register_update.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -package openflare - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil" - "github.com/Rain-kl/Wavelet/internal/apps/openflare/update" - "github.com/gin-gonic/gin" -) - -func registerUpdateRoutes(apiGroup *gin.RouterGroup) { - updateRoute := apiGroup.Group("/update") - updateRoute.Use(apiutil.AdminMiddlewares()...) - { - updateRoute.GET("/latest-release", update.GetLatestReleaseHandler) - updateRoute.GET("/logs/ws", update.StreamServerUpgradeLogsHandler) - updateRoute.POST("/manual-upload", update.UploadManualServerBinaryHandler) - updateRoute.POST("/manual-upgrade", update.ConfirmManualServerUpgradeHandler) - updateRoute.POST("/upgrade", update.UpgradeServerHandler) - } -} diff --git a/Wavelet/internal/router/v1/openflare/v1.go b/Wavelet/internal/router/v1/openflare/v1.go index e73853a5..148a82f3 100644 --- a/Wavelet/internal/router/v1/openflare/v1.go +++ b/Wavelet/internal/router/v1/openflare/v1.go @@ -22,5 +22,4 @@ func RegisterV1Routes(apiV1Router *gin.RouterGroup) { registerPagesRoutes(group) registerDashboardRoutes(group) registerObservabilityRoutes(group) - registerUpdateRoutes(group) } diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 2c98feb8..a2fbaafc 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -18,8 +18,10 @@ sidebar: false ### 移除 +- 移除 Wavelet About 界面相关代码:删除 `AboutService` 与 `GET /api/v1/d/about` 接口(页面从未实现,属冗余遗留)。 - 移除 Wavelet 中从旧系统迁移但未实装的全局 API / Web / 敏感接口限流选项(`GlobalApiRateLimit*`、`GlobalWebRateLimit*`、`CriticalRateLimit*`)及相关校验与数据库种子。 - 移除 `internal/apps/openflare/legacy/` 控制台兼容层、`compat/auth.go`(`OpenFlare-Token` JWT 桥接)及前端 `legacy-base.service.ts`;`openflare-server/web` 不再能对接当前 Wavelet 后端管理 API。 +- 移除旧 OpenFlare 服务升级兼容层(`/api/v1/openflare/update/*`);统一使用 Wavelet 框架 `admin/updater`(`/api/v1/admin/update`)。 ### 新增