diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json index 36237fa..6c87067 100644 --- a/apps/api/docs/swagger.json +++ b/apps/api/docs/swagger.json @@ -2415,6 +2415,110 @@ } } }, + "/api/admin/system/client-customization/settings": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "获取客户端自定义设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/store.ClientCustomizationSettings" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新客户端名称、英文名称、平台名和图标路径。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "更新客户端自定义设置", + "parameters": [ + { + "description": "客户端自定义设置", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/store.ClientCustomizationSettingsInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/store.ClientCustomizationSettings" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/admin/system/file-storage/channels": { "get": { "security": [ @@ -4455,8 +4559,7 @@ ], "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", "consumes": [ - "application/json", - "multipart/form-data" + "application/json" ], "produces": [ "application/json" @@ -5032,6 +5135,32 @@ } } }, + "/api/v1/public/client-customization": { + "get": { + "description": "无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "获取公开客户端自定义设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/store.ClientCustomizationSettings" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/v1/reranks": { "post": { "security": [ @@ -5539,6 +5668,64 @@ } } }, + "/api/v1/tasks/{taskID}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "取消异步任务", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskCancelResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/v1/tasks/{taskID}/events": { "get": { "security": [ @@ -5736,6 +5923,233 @@ } } }, + "/api/v1/voice_clone": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "创建或执行 AI 任务", + "parameters": [ + { + "type": "boolean", + "description": "true 时异步创建任务并返回 202", + "name": "X-Async", + "in": "header" + }, + { + "description": "AI 任务请求,字段随任务类型变化", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/httpapi.TaskRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.CompatibleResponse" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/httpapi.TaskAcceptedResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/voice_clone/voices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "列出当前用户克隆音色", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/voice_clone/voices/{voiceID}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "删除当前用户克隆音色", + "parameters": [ + { + "type": "string", + "description": "克隆音色记录 ID 或上游 voice_id", + "name": "voiceID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/workspace/desktop-config": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回桌面端需要的 Gateway Web 账单入口配置。", + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "获取桌面端配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.desktopConfigResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/workspace/tasks": { "get": { "security": [ @@ -5871,6 +6285,64 @@ } } }, + "/api/workspace/tasks/{taskID}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "取消异步任务", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskCancelResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/workspace/tasks/{taskID}/events": { "get": { "security": [ @@ -6362,8 +6834,7 @@ ], "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", "consumes": [ - "application/json", - "multipart/form-data" + "application/json" ], "produces": [ "application/json" @@ -7138,6 +7609,303 @@ } } }, + "/tasks": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "按当前用户列出任务,支持关键字、模型类型、时间范围和分页过滤。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "列出任务", + "parameters": [ + { + "type": "string", + "description": "搜索关键字,别名 query", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "模型类型,别名 type", + "name": "modelType", + "in": "query" + }, + { + "type": "string", + "description": "创建时间起点,支持 RFC3339 或日期格式,别名 from", + "name": "createdFrom", + "in": "query" + }, + { + "type": "string", + "description": "创建时间终点,支持 RFC3339 或日期格式,别名 to", + "name": "createdTo", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "每页数量,别名 limit", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/tasks/{taskID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回指定任务的请求、状态、输出和执行摘要。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "获取任务详情", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/store.GatewayTask" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/tasks/{taskID}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "取消异步任务", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskCancelResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/tasks/{taskID}/events": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。", + "produces": [ + "text/event-stream" + ], + "tags": [ + "tasks" + ], + "summary": "订阅任务事件", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Server-Sent Events,data 为 store.TaskEvent 或 TaskAcceptedEvent", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/tasks/{taskID}/param-preprocessing": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回指定任务在执行前的参数改写、校验或模板处理日志。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "获取任务参数预处理日志", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskParamPreprocessingLogListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/v1/chat/completions": { "post": { "security": [ @@ -7401,8 +8169,7 @@ ], "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", "consumes": [ - "application/json", - "multipart/form-data" + "application/json" ], "produces": [ "application/json" @@ -8043,6 +8810,456 @@ } } } + }, + "/v1/tasks/{taskID}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "取消异步任务", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.TaskCancelResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/v1/voice_clone": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "创建或执行 AI 任务", + "parameters": [ + { + "type": "boolean", + "description": "true 时异步创建任务并返回 202", + "name": "X-Async", + "in": "header" + }, + { + "description": "AI 任务请求,字段随任务类型变化", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/httpapi.TaskRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.CompatibleResponse" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/httpapi.TaskAcceptedResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/v1/voice_clone/voices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "列出当前用户克隆音色", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/v1/voice_clone/voices/{voiceID}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "删除当前用户克隆音色", + "parameters": [ + { + "type": "string", + "description": "克隆音色记录 ID 或上游 voice_id", + "name": "voiceID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/voice_clone": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tasks" + ], + "summary": "创建或执行 AI 任务", + "parameters": [ + { + "type": "boolean", + "description": "true 时异步创建任务并返回 202", + "name": "X-Async", + "in": "header" + }, + { + "description": "AI 任务请求,字段随任务类型变化", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/httpapi.TaskRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.CompatibleResponse" + } + }, + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/httpapi.TaskAcceptedResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "402": { + "description": "Payment Required", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/voice_clone/voices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "列出当前用户克隆音色", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/voice_clone/voices/{voiceID}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。", + "produces": [ + "application/json" + ], + "tags": [ + "voice-clone" + ], + "summary": "删除当前用户克隆音色", + "parameters": [ + { + "type": "string", + "description": "克隆音色记录 ID 或上游 voice_id", + "name": "voiceID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } } }, "definitions": { @@ -8252,16 +9469,31 @@ "type": "integer", "example": 8 }, + "input_tokens_details": { + "$ref": "#/definitions/httpapi.ChatInputTokensDetails" + }, "prompt_tokens": { "type": "integer", "example": 12 }, + "prompt_tokens_details": { + "$ref": "#/definitions/httpapi.ChatPromptTokensDetails" + }, "total_tokens": { "type": "integer", "example": 20 } } }, + "httpapi.ChatInputTokensDetails": { + "type": "object", + "properties": { + "cached_tokens": { + "type": "integer", + "example": 4 + } + } + }, "httpapi.ChatMessage": { "type": "object", "properties": { @@ -8275,6 +9507,15 @@ } } }, + "httpapi.ChatPromptTokensDetails": { + "type": "object", + "properties": { + "cached_tokens": { + "type": "integer", + "example": 4 + } + } + }, "httpapi.CompatibleResponse": { "type": "object", "properties": { @@ -8845,6 +10086,31 @@ } } }, + "httpapi.TaskCancelResponse": { + "type": "object", + "properties": { + "cancellable": { + "type": "boolean", + "example": false + }, + "cancelled": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "任务已提交上游,当前不可取消,请继续查询结果" + }, + "submitted": { + "type": "boolean", + "example": true + }, + "taskId": { + "type": "string", + "example": "9f4d8f3d-5f5f-4bb7-a4be-344a9f930e25" + } + } + }, "httpapi.TaskListResponse": { "type": "object", "properties": { @@ -9085,6 +10351,25 @@ } } }, + "httpapi.desktopBillingConfigResponse": { + "type": "object", + "properties": { + "gatewayBaseUrl": { + "type": "string" + }, + "gatewayBillingPath": { + "type": "string" + } + } + }, + "httpapi.desktopConfigResponse": { + "type": "object", + "properties": { + "billing": { + "$ref": "#/definitions/httpapi.desktopBillingConfigResponse" + } + } + }, "httpapi.updatePlatformDynamicPriorityRequest": { "type": "object", "properties": { @@ -9650,6 +10935,40 @@ } } }, + "store.ClientCustomizationSettings": { + "type": "object", + "properties": { + "clientEnglishName": { + "type": "string" + }, + "clientName": { + "type": "string" + }, + "iconPath": { + "type": "string" + }, + "platformName": { + "type": "string" + } + } + }, + "store.ClientCustomizationSettingsInput": { + "type": "object", + "properties": { + "clientEnglishName": { + "type": "string" + }, + "clientName": { + "type": "string" + }, + "iconPath": { + "type": "string" + }, + "platformName": { + "type": "string" + } + } + }, "store.CreateAPIKeyInput": { "type": "object", "properties": { @@ -9958,6 +11277,9 @@ "type": "array", "items": {} }, + "cancellable": { + "type": "boolean" + }, "conversationId": { "type": "string" }, @@ -9991,6 +11313,9 @@ "kind": { "type": "string" }, + "message": { + "type": "string" + }, "metrics": { "type": "object", "additionalProperties": {} @@ -10046,6 +11371,9 @@ "status": { "type": "string" }, + "submitted": { + "type": "boolean" + }, "tenantId": { "type": "string" }, @@ -11126,6 +12454,10 @@ "store.RunnerPolicy": { "type": "object", "properties": { + "cacheAffinityPolicy": { + "type": "object", + "additionalProperties": {} + }, "createdAt": { "type": "string" }, @@ -11172,6 +12504,10 @@ "store.RunnerPolicyInput": { "type": "object", "properties": { + "cacheAffinityPolicy": { + "type": "object", + "additionalProperties": {} + }, "description": { "type": "string" }, @@ -11568,4 +12904,4 @@ "in": "header" } } -} +} \ No newline at end of file diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml index 4a1dbc1..1d430f3 100644 --- a/apps/api/docs/swagger.yaml +++ b/apps/api/docs/swagger.yaml @@ -138,13 +138,23 @@ definitions: completion_tokens: example: 8 type: integer + input_tokens_details: + $ref: '#/definitions/httpapi.ChatInputTokensDetails' prompt_tokens: example: 12 type: integer + prompt_tokens_details: + $ref: '#/definitions/httpapi.ChatPromptTokensDetails' total_tokens: example: 20 type: integer type: object + httpapi.ChatInputTokensDetails: + properties: + cached_tokens: + example: 4 + type: integer + type: object httpapi.ChatMessage: properties: content: @@ -154,6 +164,12 @@ definitions: example: user type: string type: object + httpapi.ChatPromptTokensDetails: + properties: + cached_tokens: + example: 4 + type: integer + type: object httpapi.CompatibleResponse: properties: choices: @@ -534,6 +550,24 @@ definitions: example: 9f4d8f3d-5f5f-4bb7-a4be-344a9f930e25 type: string type: object + httpapi.TaskCancelResponse: + properties: + cancellable: + example: false + type: boolean + cancelled: + example: false + type: boolean + message: + example: 任务已提交上游,当前不可取消,请继续查询结果 + type: string + submitted: + example: true + type: boolean + taskId: + example: 9f4d8f3d-5f5f-4bb7-a4be-344a9f930e25 + type: string + type: object httpapi.TaskListResponse: properties: items: @@ -704,6 +738,18 @@ definitions: example: 42 type: integer type: object + httpapi.desktopBillingConfigResponse: + properties: + gatewayBaseUrl: + type: string + gatewayBillingPath: + type: string + type: object + httpapi.desktopConfigResponse: + properties: + billing: + $ref: '#/definitions/httpapi.desktopBillingConfigResponse' + type: object httpapi.updatePlatformDynamicPriorityRequest: properties: dynamicPriority: @@ -1090,6 +1136,28 @@ definitions: status: type: string type: object + store.ClientCustomizationSettings: + properties: + clientEnglishName: + type: string + clientName: + type: string + iconPath: + type: string + platformName: + type: string + type: object + store.ClientCustomizationSettingsInput: + properties: + clientEnglishName: + type: string + clientName: + type: string + iconPath: + type: string + platformName: + type: string + type: object store.CreateAPIKeyInput: properties: expiresAt: @@ -1299,6 +1367,8 @@ definitions: billings: items: {} type: array + cancellable: + type: boolean conversationId: type: string createdAt: @@ -1321,6 +1391,8 @@ definitions: type: string kind: type: string + message: + type: string metrics: additionalProperties: {} type: object @@ -1359,6 +1431,8 @@ definitions: type: string status: type: string + submitted: + type: boolean tenantId: type: string tenantKey: @@ -2086,6 +2160,9 @@ definitions: type: object store.RunnerPolicy: properties: + cacheAffinityPolicy: + additionalProperties: {} + type: object createdAt: type: string description: @@ -2118,6 +2195,9 @@ definitions: type: object store.RunnerPolicyInput: properties: + cacheAffinityPolicy: + additionalProperties: {} + type: object description: type: string failoverPolicy: @@ -3930,6 +4010,72 @@ paths: summary: 更新 Runner 策略 tags: - runtime + /api/admin/system/client-customization/settings: + get: + description: 返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/store.ClientCustomizationSettings' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 获取客户端自定义设置 + tags: + - system + patch: + consumes: + - application/json + description: 更新客户端名称、英文名称、平台名和图标路径。 + parameters: + - description: 客户端自定义设置 + in: body + name: body + required: true + schema: + $ref: '#/definitions/store.ClientCustomizationSettingsInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/store.ClientCustomizationSettings' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 更新客户端自定义设置 + tags: + - system /api/admin/system/file-storage/channels: get: description: 返回所有未删除的文件存储通道,用于管理上传与生成资源回传策略。 @@ -5238,7 +5384,6 @@ paths: post: consumes: - application/json - - multipart/form-data description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。 parameters: @@ -5611,6 +5756,23 @@ paths: summary: 列出目录供应商 tags: - catalog + /api/v1/public/client-customization: + get: + description: 无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/store.ClientCustomizationSettings' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + summary: 获取公开客户端自定义设置 + tags: + - system /api/v1/reranks: post: consumes: @@ -5942,6 +6104,43 @@ paths: summary: 获取任务详情 tags: - tasks + /api/v1/tasks/{taskID}/cancel: + post: + description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskCancelResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 取消异步任务 + tags: + - tasks /api/v1/tasks/{taskID}/events: get: description: 以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。 @@ -6069,6 +6268,152 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /api/v1/voice_clone: + post: + consumes: + - application/json + description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible + 路径同步返回兼容响应或 SSE 流。 + parameters: + - description: true 时异步创建任务并返回 202 + in: header + name: X-Async + type: boolean + - description: AI 任务请求,字段随任务类型变化 + in: body + name: input + required: true + schema: + $ref: '#/definitions/httpapi.TaskRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.CompatibleResponse' + "202": + description: Accepted + schema: + $ref: '#/definitions/httpapi.TaskAcceptedResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "402": + description: Payment Required + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 创建或执行 AI 任务 + tags: + - tasks + /api/v1/voice_clone/voices: + get: + description: 返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 列出当前用户克隆音色 + tags: + - voice-clone + /api/v1/voice_clone/voices/{voiceID}: + delete: + description: 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。 + parameters: + - description: 克隆音色记录 ID 或上游 voice_id + in: path + name: voiceID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 删除当前用户克隆音色 + tags: + - voice-clone + /api/workspace/desktop-config: + get: + description: 返回桌面端需要的 Gateway Web 账单入口配置。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.desktopConfigResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 获取桌面端配置 + tags: + - workspace /api/workspace/tasks: get: description: 按当前用户列出任务,支持关键字、模型类型、时间范围和分页过滤。 @@ -6156,6 +6501,43 @@ paths: summary: 获取任务详情 tags: - tasks + /api/workspace/tasks/{taskID}/cancel: + post: + description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskCancelResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 取消异步任务 + tags: + - tasks /api/workspace/tasks/{taskID}/events: get: description: 以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。 @@ -6471,7 +6853,6 @@ paths: post: consumes: - application/json - - multipart/form-data description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。 parameters: @@ -6983,6 +7364,196 @@ paths: summary: 获取本地上传资源 tags: - static + /tasks: + get: + description: 按当前用户列出任务,支持关键字、模型类型、时间范围和分页过滤。 + parameters: + - description: 搜索关键字,别名 query + in: query + name: q + type: string + - description: 模型类型,别名 type + in: query + name: modelType + type: string + - description: 创建时间起点,支持 RFC3339 或日期格式,别名 from + in: query + name: createdFrom + type: string + - description: 创建时间终点,支持 RFC3339 或日期格式,别名 to + in: query + name: createdTo + type: string + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 50 + description: 每页数量,别名 limit + in: query + name: pageSize + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 列出任务 + tags: + - tasks + /tasks/{taskID}: + get: + description: 返回指定任务的请求、状态、输出和执行摘要。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/store.GatewayTask' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 获取任务详情 + tags: + - tasks + /tasks/{taskID}/cancel: + post: + description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskCancelResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 取消异步任务 + tags: + - tasks + /tasks/{taskID}/events: + get: + description: 以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - text/event-stream + responses: + "200": + description: Server-Sent Events,data 为 store.TaskEvent 或 TaskAcceptedEvent + schema: + type: string + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 订阅任务事件 + tags: + - tasks + /tasks/{taskID}/param-preprocessing: + get: + description: 返回指定任务在执行前的参数改写、校验或模板处理日志。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskParamPreprocessingLogListResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 获取任务参数预处理日志 + tags: + - tasks /v1/chat/completions: post: consumes: @@ -7153,7 +7724,6 @@ paths: post: consumes: - application/json - - multipart/form-data description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。 parameters: @@ -7577,6 +8147,297 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /v1/tasks/{taskID}/cancel: + post: + description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.TaskCancelResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 取消异步任务 + tags: + - tasks + /v1/voice_clone: + post: + consumes: + - application/json + description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible + 路径同步返回兼容响应或 SSE 流。 + parameters: + - description: true 时异步创建任务并返回 202 + in: header + name: X-Async + type: boolean + - description: AI 任务请求,字段随任务类型变化 + in: body + name: input + required: true + schema: + $ref: '#/definitions/httpapi.TaskRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.CompatibleResponse' + "202": + description: Accepted + schema: + $ref: '#/definitions/httpapi.TaskAcceptedResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "402": + description: Payment Required + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 创建或执行 AI 任务 + tags: + - tasks + /v1/voice_clone/voices: + get: + description: 返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 列出当前用户克隆音色 + tags: + - voice-clone + /v1/voice_clone/voices/{voiceID}: + delete: + description: 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。 + parameters: + - description: 克隆音色记录 ID 或上游 voice_id + in: path + name: voiceID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 删除当前用户克隆音色 + tags: + - voice-clone + /voice_clone: + post: + consumes: + - application/json + description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible + 路径同步返回兼容响应或 SSE 流。 + parameters: + - description: true 时异步创建任务并返回 202 + in: header + name: X-Async + type: boolean + - description: AI 任务请求,字段随任务类型变化 + in: body + name: input + required: true + schema: + $ref: '#/definitions/httpapi.TaskRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.CompatibleResponse' + "202": + description: Accepted + schema: + $ref: '#/definitions/httpapi.TaskAcceptedResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "402": + description: Payment Required + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 创建或执行 AI 任务 + tags: + - tasks + /voice_clone/voices: + get: + description: 返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 列出当前用户克隆音色 + tags: + - voice-clone + /voice_clone/voices/{voiceID}: + delete: + description: 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。 + parameters: + - description: 克隆音色记录 ID 或上游 voice_id + in: path + name: voiceID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 删除当前用户克隆音色 + tags: + - voice-clone schemes: - http - https diff --git a/apps/api/internal/httpapi/server.go b/apps/api/internal/httpapi/server.go index 124a571..922c750 100644 --- a/apps/api/internal/httpapi/server.go +++ b/apps/api/internal/httpapi/server.go @@ -52,6 +52,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me))) mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders))) mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels))) + mux.Handle("GET /api/v1/public/client-customization", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.getPublicClientCustomizationSettings))) mux.Handle("GET /api/admin/catalog/providers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders))) mux.Handle("POST /api/admin/catalog/providers", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider))) mux.Handle("PATCH /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider))) @@ -115,6 +116,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("GET /api/admin/config/network-proxy", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getNetworkProxyConfig))) mux.Handle("GET /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getFileStorageSettings))) mux.Handle("PATCH /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageSettings))) + mux.Handle("GET /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getClientCustomizationSettings))) + mux.Handle("PATCH /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateClientCustomizationSettings))) mux.Handle("GET /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listFileStorageChannels))) mux.Handle("POST /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createFileStorageChannel))) mux.Handle("PATCH /api/admin/system/file-storage/channels/{channelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageChannel))) diff --git a/apps/api/internal/httpapi/system_settings_handlers.go b/apps/api/internal/httpapi/system_settings_handlers.go index a497c7e..2abdc7c 100644 --- a/apps/api/internal/httpapi/system_settings_handlers.go +++ b/apps/api/internal/httpapi/system_settings_handlers.go @@ -83,6 +83,76 @@ func (s *Server) updateFileStorageSettings(w http.ResponseWriter, r *http.Reques writeJSON(w, http.StatusOK, settings) } +// getClientCustomizationSettings godoc +// @Summary 获取客户端自定义设置 +// @Description 返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。 +// @Tags system +// @Produce json +// @Security BearerAuth +// @Success 200 {object} store.ClientCustomizationSettings +// @Failure 401 {object} ErrorEnvelope +// @Failure 403 {object} ErrorEnvelope +// @Failure 500 {object} ErrorEnvelope +// @Router /api/admin/system/client-customization/settings [get] +func (s *Server) getClientCustomizationSettings(w http.ResponseWriter, r *http.Request) { + s.writeClientCustomizationSettings(w, r) +} + +// getPublicClientCustomizationSettings godoc +// @Summary 获取公开客户端自定义设置 +// @Description 无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。 +// @Tags system +// @Produce json +// @Success 200 {object} store.ClientCustomizationSettings +// @Failure 500 {object} ErrorEnvelope +// @Router /api/v1/public/client-customization [get] +func (s *Server) getPublicClientCustomizationSettings(w http.ResponseWriter, r *http.Request) { + s.writeClientCustomizationSettings(w, r) +} + +func (s *Server) writeClientCustomizationSettings(w http.ResponseWriter, r *http.Request) { + settings, err := s.store.GetClientCustomizationSettings(r.Context()) + if err != nil { + if store.IsUndefinedDatabaseObject(err) { + writeJSON(w, http.StatusOK, store.DefaultClientCustomizationSettings()) + return + } + s.logger.Error("get client customization settings failed", "error", err) + writeError(w, http.StatusInternalServerError, "get client customization settings failed") + return + } + writeJSON(w, http.StatusOK, settings) +} + +// updateClientCustomizationSettings godoc +// @Summary 更新客户端自定义设置 +// @Description 更新客户端名称、英文名称、平台名和图标路径。 +// @Tags system +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body store.ClientCustomizationSettingsInput true "客户端自定义设置" +// @Success 200 {object} store.ClientCustomizationSettings +// @Failure 400 {object} ErrorEnvelope +// @Failure 401 {object} ErrorEnvelope +// @Failure 403 {object} ErrorEnvelope +// @Failure 500 {object} ErrorEnvelope +// @Router /api/admin/system/client-customization/settings [patch] +func (s *Server) updateClientCustomizationSettings(w http.ResponseWriter, r *http.Request) { + var input store.ClientCustomizationSettingsInput + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeError(w, http.StatusBadRequest, "invalid json body") + return + } + settings, err := s.store.UpdateClientCustomizationSettings(r.Context(), input) + if err != nil { + s.logger.Error("update client customization settings failed", "error", err) + writeError(w, http.StatusInternalServerError, "update client customization settings failed") + return + } + writeJSON(w, http.StatusOK, settings) +} + // createFileStorageChannel godoc // @Summary 创建文件存储通道 // @Description 创建文件存储通道,当前主要用于配置 server-main OpenAPI 上传通道。 diff --git a/apps/api/internal/store/file_storage_channels.go b/apps/api/internal/store/file_storage_channels.go index 64276f9..479a4f5 100644 --- a/apps/api/internal/store/file_storage_channels.go +++ b/apps/api/internal/store/file_storage_channels.go @@ -24,6 +24,7 @@ const ( ) const SystemSettingFileStorage = "file_storage" +const SystemSettingClientCustomization = "client_customization" const fileStorageChannelColumns = ` id::text, channel_key, name, provider, COALESCE(upload_url, ''), credentials, @@ -72,6 +73,20 @@ type FileStorageSettingsInput struct { ResultUploadPolicy string `json:"resultUploadPolicy"` } +type ClientCustomizationSettings struct { + ClientName string `json:"clientName"` + ClientEnglishName string `json:"clientEnglishName"` + PlatformName string `json:"platformName"` + IconPath string `json:"iconPath"` +} + +type ClientCustomizationSettingsInput struct { + ClientName *string `json:"clientName"` + ClientEnglishName *string `json:"clientEnglishName"` + PlatformName *string `json:"platformName"` + IconPath *string `json:"iconPath"` +} + type fileStorageChannelScanner interface { Scan(dest ...any) error } @@ -348,6 +363,15 @@ func DefaultFileStorageSettings() FileStorageSettings { return FileStorageSettings{ResultUploadPolicy: FileStorageResultUploadPolicyDefault} } +func DefaultClientCustomizationSettings() ClientCustomizationSettings { + return ClientCustomizationSettings{ + ClientName: "EasyAI AI Gateway", + ClientEnglishName: "EasyAI AI Gateway", + PlatformName: "EasyAI", + IconPath: "", + } +} + func (s *Store) GetFileStorageSettings(ctx context.Context) (FileStorageSettings, error) { var value []byte err := s.pool.QueryRow(ctx, ` @@ -380,13 +404,53 @@ func (s *Store) UpdateFileStorageSettings(ctx context.Context, input FileStorage return fileStorageSettingsFromValue(decodeObject(saved)), nil } +func (s *Store) GetClientCustomizationSettings(ctx context.Context) (ClientCustomizationSettings, error) { + var value []byte + err := s.pool.QueryRow(ctx, ` +SELECT value +FROM system_settings +WHERE setting_key = $1`, SystemSettingClientCustomization).Scan(&value) + if err != nil { + if IsNotFound(err) { + return DefaultClientCustomizationSettings(), nil + } + return ClientCustomizationSettings{}, err + } + return clientCustomizationSettingsFromValue(decodeObject(value)), nil +} + +func (s *Store) UpdateClientCustomizationSettings(ctx context.Context, input ClientCustomizationSettingsInput) (ClientCustomizationSettings, error) { + current, err := s.GetClientCustomizationSettings(ctx) + if err != nil && !IsUndefinedDatabaseObject(err) { + return ClientCustomizationSettings{}, err + } + settings := normalizeClientCustomizationSettings(input, current) + value, _ := json.Marshal(settings) + var saved []byte + err = s.upsertSystemSetting(ctx, SystemSettingClientCustomization, value, &saved) + if err != nil && IsUndefinedDatabaseObject(err) { + if ensureErr := s.ensureSystemSettingsTable(ctx); ensureErr != nil { + return ClientCustomizationSettings{}, ensureErr + } + err = s.upsertSystemSetting(ctx, SystemSettingClientCustomization, value, &saved) + } + if err != nil { + return ClientCustomizationSettings{}, err + } + return clientCustomizationSettingsFromValue(decodeObject(saved)), nil +} + func (s *Store) upsertFileStorageSettings(ctx context.Context, value []byte, saved *[]byte) error { + return s.upsertSystemSetting(ctx, SystemSettingFileStorage, value, saved) +} + +func (s *Store) upsertSystemSetting(ctx context.Context, settingKey string, value []byte, saved *[]byte) error { return s.pool.QueryRow(ctx, ` INSERT INTO system_settings (setting_key, value) VALUES ($1, $2) ON CONFLICT (setting_key) DO UPDATE SET value = EXCLUDED.value, updated_at = now() -RETURNING value`, SystemSettingFileStorage, value).Scan(saved) +RETURNING value`, settingKey, value).Scan(saved) } func (s *Store) ensureSystemSettingsTable(ctx context.Context) error { @@ -409,6 +473,53 @@ func fileStorageSettingsFromValue(value map[string]any) FileStorageSettings { return settings } +func clientCustomizationSettingsFromValue(value map[string]any) ClientCustomizationSettings { + settings := DefaultClientCustomizationSettings() + if value == nil { + return settings + } + if clientName := stringFromAny(value["clientName"]); clientName != "" { + settings.ClientName = clientName + } + if clientEnglishName := stringFromAny(value["clientEnglishName"]); clientEnglishName != "" { + settings.ClientEnglishName = clientEnglishName + } + if platformName := stringFromAny(value["platformName"]); platformName != "" { + settings.PlatformName = platformName + } + settings.IconPath = stringFromAny(value["iconPath"]) + return settings +} + +func normalizeClientCustomizationSettings(input ClientCustomizationSettingsInput, current ClientCustomizationSettings) ClientCustomizationSettings { + settings := current + if settings.ClientName == "" && settings.ClientEnglishName == "" && settings.PlatformName == "" { + settings = DefaultClientCustomizationSettings() + } + if input.ClientName != nil { + settings.ClientName = strings.TrimSpace(*input.ClientName) + } + if input.ClientEnglishName != nil { + settings.ClientEnglishName = strings.TrimSpace(*input.ClientEnglishName) + } + if input.PlatformName != nil { + settings.PlatformName = strings.TrimSpace(*input.PlatformName) + } + if input.IconPath != nil { + settings.IconPath = strings.TrimSpace(*input.IconPath) + } + if settings.ClientName == "" { + settings.ClientName = DefaultClientCustomizationSettings().ClientName + } + if settings.ClientEnglishName == "" { + settings.ClientEnglishName = DefaultClientCustomizationSettings().ClientEnglishName + } + if settings.PlatformName == "" { + settings.PlatformName = DefaultClientCustomizationSettings().PlatformName + } + return settings +} + func NormalizeFileStorageResultUploadPolicy(policy string) string { normalized := strings.ToLower(strings.TrimSpace(policy)) normalized = strings.ReplaceAll(normalized, "-", "_") diff --git a/apps/api/migrations/0057_client_customization_settings.sql b/apps/api/migrations/0057_client_customization_settings.sql new file mode 100644 index 0000000..2e73062 --- /dev/null +++ b/apps/api/migrations/0057_client_customization_settings.sql @@ -0,0 +1,6 @@ +INSERT INTO system_settings (setting_key, value) +VALUES ( + 'client_customization', + '{"clientName":"EasyAI AI Gateway","clientEnglishName":"EasyAI AI Gateway","platformName":"EasyAI","iconPath":""}'::jsonb +) +ON CONFLICT (setting_key) DO NOTHING; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e44ccc8..0dc778e 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react'; import type { BaseModelCatalogItem, CatalogProvider, + ClientCustomizationSettings, + ClientCustomizationSettingsUpdateRequest, FileStorageChannel, FileStorageSettings, FileStorageSettingsUpdateRequest, @@ -52,6 +54,7 @@ import { deleteTenant, deleteUserGroup, GatewayApiError, + getClientCustomizationSettings, getHealth, listFileStorageChannels, getCurrentUser, @@ -92,6 +95,7 @@ import { type HealthResponse, updateAccessRule, updateApiKeyScopes, + updateClientCustomizationSettings, updateFileStorageChannel, updateFileStorageSettings, updateGatewayUser, @@ -153,6 +157,7 @@ type DataKey = | 'playgroundModels' | 'modelCatalog' | 'networkProxyConfig' + | 'clientCustomizationSettings' | 'fileStorageChannels' | 'fileStorageSettings' | 'platforms' @@ -201,6 +206,7 @@ export function App() { }); const [playgroundModels, setPlaygroundModels] = useState([]); const [networkProxyConfig, setNetworkProxyConfig] = useState(null); + const [clientCustomizationSettings, setClientCustomizationSettings] = useState(null); const [fileStorageChannels, setFileStorageChannels] = useState([]); const [fileStorageSettings, setFileStorageSettings] = useState(null); const [providers, setProviders] = useState([]); @@ -321,6 +327,7 @@ export function App() { auditLogs, apiKeys, baseModels, + clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, @@ -344,7 +351,7 @@ export function App() { users, walletAccounts, walletTransactions, - }), [accessRules, apiKeys, auditLogs, baseModels, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]); + }), [accessRules, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]); async function refresh(nextToken = token) { await ensureRouteData(nextToken, true); @@ -424,6 +431,9 @@ export function App() { case 'networkProxyConfig': setNetworkProxyConfig(await getNetworkProxyConfig(nextToken)); return; + case 'clientCustomizationSettings': + setClientCustomizationSettings(await getClientCustomizationSettings(nextToken)); + return; case 'fileStorageChannels': setFileStorageChannels((await listFileStorageChannels(nextToken)).items); return; @@ -952,6 +962,21 @@ export function App() { } } + async function saveClientCustomizationSettings(input: ClientCustomizationSettingsUpdateRequest) { + setCoreState('loading'); + setCoreMessage(''); + try { + const settings = await updateClientCustomizationSettings(token, input); + setClientCustomizationSettings(settings); + setCoreState('ready'); + setCoreMessage('客户端自定义设置已更新。'); + } catch (err) { + setCoreState('error'); + setCoreMessage(err instanceof Error ? err.message : '更新客户端自定义设置失败'); + throw err; + } + } + async function removeFileStorageChannel(channelId: string) { setCoreState('loading'); setCoreMessage(''); @@ -1233,6 +1258,7 @@ export function App() { onSaveAccessRule={saveAccessRule} onSaveFileStorageChannel={saveFileStorageChannel} onSaveFileStorageSettings={saveFileStorageSettings} + onSaveClientCustomizationSettings={saveClientCustomizationSettings} onSaveTenant={saveTenant} onSaveUser={saveUser} onRechargeUserWalletBalance={rechargeUserWallet} @@ -1452,7 +1478,7 @@ function dataKeysForRoute( case 'accessRules': return ['accessRules', 'userGroups', 'platforms', 'models']; case 'systemSettings': - return ['fileStorageSettings', 'fileStorageChannels']; + return ['clientCustomizationSettings', 'fileStorageSettings', 'fileStorageChannels']; default: return []; } diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts index 6846797..3a59d69 100644 --- a/apps/web/src/api.ts +++ b/apps/web/src/api.ts @@ -5,6 +5,8 @@ import type { BaseModelUpsertRequest, CatalogProvider, CatalogProviderUpsertRequest, + ClientCustomizationSettings, + ClientCustomizationSettingsUpdateRequest, CreatedGatewayApiKey, FileStorageChannel, FileStorageSettings, @@ -974,6 +976,25 @@ export async function updateFileStorageSettings( }); } +export async function getClientCustomizationSettings(token: string): Promise { + return request('/api/admin/system/client-customization/settings', { token }); +} + +export async function updateClientCustomizationSettings( + token: string, + input: ClientCustomizationSettingsUpdateRequest, +): Promise { + return request('/api/admin/system/client-customization/settings', { + body: input, + method: 'PATCH', + token, + }); +} + +export async function getPublicClientCustomization(): Promise { + return request('/api/v1/public/client-customization', { auth: false }); +} + export async function createFileStorageChannel( token: string, input: FileStorageChannelUpsertRequest, diff --git a/apps/web/src/app-state.ts b/apps/web/src/app-state.ts index e01ef9e..a45c190 100644 --- a/apps/web/src/app-state.ts +++ b/apps/web/src/app-state.ts @@ -2,6 +2,7 @@ import type { AuthUser, BaseModelCatalogItem, CatalogProvider, + ClientCustomizationSettings, FileStorageChannel, FileStorageSettings, GatewayAccessRule, @@ -32,6 +33,7 @@ export interface ConsoleData { baseModels: BaseModelCatalogItem[]; currentUser: AuthUser | null; currentUserGroups: UserGroup[]; + clientCustomizationSettings: ClientCustomizationSettings | null; fileStorageChannels: FileStorageChannel[]; fileStorageSettings: FileStorageSettings | null; modelCatalog: ModelCatalogResponse; diff --git a/apps/web/src/pages/AdminPage.tsx b/apps/web/src/pages/AdminPage.tsx index 6e31ae8..829fe3e 100644 --- a/apps/web/src/pages/AdminPage.tsx +++ b/apps/web/src/pages/AdminPage.tsx @@ -3,6 +3,7 @@ import { Boxes, Building2, Gauge, History, KeyRound, Route, ServerCog, Settings, import type { BaseModelUpsertRequest, CatalogProviderUpsertRequest, + ClientCustomizationSettingsUpdateRequest, FileStorageChannelUpsertRequest, FileStorageSettingsUpdateRequest, GatewayAccessRuleBatchRequest, @@ -78,6 +79,7 @@ export function AdminPage(props: { onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise; onSaveRuntimePolicySet: (input: RuntimePolicySetUpsertRequest, policySetId?: string) => Promise; onSaveAccessRule: (input: GatewayAccessRuleUpsertRequest, ruleId?: string) => Promise; + onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise; onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise; onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise; onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise; @@ -185,12 +187,14 @@ export function AdminPage(props: { {props.section === 'systemSettings' && ( )} diff --git a/apps/web/src/pages/admin/SystemSettingsPanel.tsx b/apps/web/src/pages/admin/SystemSettingsPanel.tsx index cbc04c9..93bc7dc 100644 --- a/apps/web/src/pages/admin/SystemSettingsPanel.tsx +++ b/apps/web/src/pages/admin/SystemSettingsPanel.tsx @@ -1,10 +1,17 @@ import { useEffect, useState, type FormEvent } from 'react'; -import { Database, Pencil, Plus, RotateCcw, Save, ServerCog, Trash2 } from 'lucide-react'; -import type { FileStorageChannel, FileStorageChannelUpsertRequest, FileStorageSettings, FileStorageSettingsUpdateRequest } from '@easyai-ai-gateway/contracts'; +import { Database, Pencil, Plus, RotateCcw, Save, ServerCog, Settings2, Trash2 } from 'lucide-react'; +import type { ClientCustomizationSettings, ClientCustomizationSettingsUpdateRequest, FileStorageChannel, FileStorageChannelUpsertRequest, FileStorageSettings, FileStorageSettingsUpdateRequest } from '@easyai-ai-gateway/contracts'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, FormDialog, Input, Label, Select, Tabs, Textarea } from '../../components/ui'; import type { LoadState } from '../../types'; -type SystemSettingsTab = 'fileStorage'; +type SystemSettingsTab = 'fileStorage' | 'clientCustomization'; + +type ClientCustomizationForm = { + clientEnglishName: string; + clientName: string; + iconPath: string; + platformName: string; +}; type FileStorageChannelForm = { apiKey: string; @@ -48,10 +55,12 @@ const resultUploadPolicyOptions = [ export function SystemSettingsPanel(props: { channels: FileStorageChannel[]; + clientCustomizationSettings: ClientCustomizationSettings | null; message: string; settings: FileStorageSettings | null; state: LoadState; onDeleteFileStorageChannel: (channelId: string) => Promise; + onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise; onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise; onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise; }) { @@ -60,6 +69,7 @@ export function SystemSettingsPanel(props: { const [editingChannel, setEditingChannel] = useState(null); const [pendingDeleteChannel, setPendingDeleteChannel] = useState(null); const [form, setForm] = useState(() => defaultChannelForm()); + const [clientCustomizationForm, setClientCustomizationForm] = useState(() => clientCustomizationSettingsToForm(props.clientCustomizationSettings)); const [settingsPolicy, setSettingsPolicy] = useState(() => normalizeResultUploadPolicy(props.settings?.resultUploadPolicy)); const [localError, setLocalError] = useState(''); @@ -67,6 +77,10 @@ export function SystemSettingsPanel(props: { setSettingsPolicy(normalizeResultUploadPolicy(props.settings?.resultUploadPolicy)); }, [props.settings?.resultUploadPolicy]); + useEffect(() => { + setClientCustomizationForm(clientCustomizationSettingsToForm(props.clientCustomizationSettings)); + }, [props.clientCustomizationSettings]); + function openCreateDialog() { setEditingChannel(null); setForm(defaultChannelForm(`server-main-${Date.now().toString(36)}`)); @@ -122,6 +136,15 @@ export function SystemSettingsPanel(props: { } } + async function saveClientCustomization() { + setLocalError(''); + try { + await props.onSaveClientCustomizationSettings(clientCustomizationFormToPayload(clientCustomizationForm)); + } catch (err) { + setLocalError(err instanceof Error ? err.message : '客户端自定义设置保存失败'); + } + } + return (
@@ -136,7 +159,10 @@ export function SystemSettingsPanel(props: { {(props.message || localError) &&

{localError || props.message}

} }]} + tabs={[ + { value: 'fileStorage', label: '文件存储', icon: }, + { value: 'clientCustomization', label: '客户端自定义', icon: }, + ]} onValueChange={setActiveTab} /> @@ -216,6 +242,39 @@ export function SystemSettingsPanel(props: { )} + {activeTab === 'clientCustomization' && ( +
+
+
+ 客户端自定义 + 配置公开展示用的客户端名称、平台名和图标路径;公开 API 无需鉴权读取。 +
+
+ + + + +
+ +
+
+ )} +