feat(system): 完成客户端自定义与系统设置
新增管理端客户端自定义标签页、公开与管理端系统设置接口、文件存储设置持久化、数据库迁移、契约类型和 OpenAPI 文档。
This commit is contained in:
parent
229ed6a669
commit
0dc9df759b
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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 上传通道。
|
||||
|
||||
@ -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, "-", "_")
|
||||
|
||||
@ -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;
|
||||
@ -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<PlatformModel[]>([]);
|
||||
const [networkProxyConfig, setNetworkProxyConfig] = useState<GatewayNetworkProxyConfig | null>(null);
|
||||
const [clientCustomizationSettings, setClientCustomizationSettings] = useState<ClientCustomizationSettings | null>(null);
|
||||
const [fileStorageChannels, setFileStorageChannels] = useState<FileStorageChannel[]>([]);
|
||||
const [fileStorageSettings, setFileStorageSettings] = useState<FileStorageSettings | null>(null);
|
||||
const [providers, setProviders] = useState<CatalogProvider[]>([]);
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
@ -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<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/admin/system/client-customization/settings', { token });
|
||||
}
|
||||
|
||||
export async function updateClientCustomizationSettings(
|
||||
token: string,
|
||||
input: ClientCustomizationSettingsUpdateRequest,
|
||||
): Promise<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/admin/system/client-customization/settings', {
|
||||
body: input,
|
||||
method: 'PATCH',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPublicClientCustomization(): Promise<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/v1/public/client-customization', { auth: false });
|
||||
}
|
||||
|
||||
export async function createFileStorageChannel(
|
||||
token: string,
|
||||
input: FileStorageChannelUpsertRequest,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<void>;
|
||||
onSaveRuntimePolicySet: (input: RuntimePolicySetUpsertRequest, policySetId?: string) => Promise<void>;
|
||||
onSaveAccessRule: (input: GatewayAccessRuleUpsertRequest, ruleId?: string) => Promise<void>;
|
||||
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
|
||||
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
|
||||
@ -185,12 +187,14 @@ export function AdminPage(props: {
|
||||
{props.section === 'systemSettings' && (
|
||||
<SystemSettingsPanel
|
||||
channels={props.data.fileStorageChannels}
|
||||
clientCustomizationSettings={props.data.clientCustomizationSettings}
|
||||
settings={props.data.fileStorageSettings}
|
||||
message={props.operationMessage}
|
||||
state={props.state}
|
||||
onDeleteFileStorageChannel={props.onDeleteFileStorageChannel}
|
||||
onSaveFileStorageChannel={props.onSaveFileStorageChannel}
|
||||
onSaveFileStorageSettings={props.onSaveFileStorageSettings}
|
||||
onSaveClientCustomizationSettings={props.onSaveClientCustomizationSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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<void>;
|
||||
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
|
||||
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
|
||||
}) {
|
||||
@ -60,6 +69,7 @@ export function SystemSettingsPanel(props: {
|
||||
const [editingChannel, setEditingChannel] = useState<FileStorageChannel | null>(null);
|
||||
const [pendingDeleteChannel, setPendingDeleteChannel] = useState<FileStorageChannel | null>(null);
|
||||
const [form, setForm] = useState<FileStorageChannelForm>(() => defaultChannelForm());
|
||||
const [clientCustomizationForm, setClientCustomizationForm] = useState<ClientCustomizationForm>(() => 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 (
|
||||
<div className="pageStack">
|
||||
<Card>
|
||||
@ -136,7 +159,10 @@ export function SystemSettingsPanel(props: {
|
||||
{(props.message || localError) && <p className="formMessage">{localError || props.message}</p>}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
tabs={[{ value: 'fileStorage', label: '文件存储', icon: <Database size={15} /> }]}
|
||||
tabs={[
|
||||
{ value: 'fileStorage', label: '文件存储', icon: <Database size={15} /> },
|
||||
{ value: 'clientCustomization', label: '客户端自定义', icon: <Settings2 size={15} /> },
|
||||
]}
|
||||
onValueChange={setActiveTab}
|
||||
/>
|
||||
</CardContent>
|
||||
@ -216,6 +242,39 @@ export function SystemSettingsPanel(props: {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'clientCustomization' && (
|
||||
<section className="fileStoragePanel">
|
||||
<div className="fileStorageSettingsCard">
|
||||
<div>
|
||||
<strong>客户端自定义</strong>
|
||||
<span>配置公开展示用的客户端名称、平台名和图标路径;公开 API 无需鉴权读取。</span>
|
||||
</div>
|
||||
<div className="formGrid two spanTwo">
|
||||
<Label>
|
||||
客户端名
|
||||
<Input value={clientCustomizationForm.clientName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, clientName: event.target.value })} placeholder="EasyAI AI Gateway" />
|
||||
</Label>
|
||||
<Label>
|
||||
客户端英文名
|
||||
<Input value={clientCustomizationForm.clientEnglishName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, clientEnglishName: event.target.value })} placeholder="EasyAI AI Gateway" />
|
||||
</Label>
|
||||
<Label>
|
||||
平台名
|
||||
<Input value={clientCustomizationForm.platformName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, platformName: event.target.value })} placeholder="EasyAI" />
|
||||
</Label>
|
||||
<Label>
|
||||
Icon 路径/URL
|
||||
<Input value={clientCustomizationForm.iconPath} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, iconPath: event.target.value })} placeholder="/logo.svg 或 https://..." />
|
||||
</Label>
|
||||
</div>
|
||||
<Button type="button" onClick={saveClientCustomization} disabled={props.state === 'loading'}>
|
||||
<Save size={15} />
|
||||
保存客户端自定义
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<FormDialog
|
||||
ariaLabel={editingChannel ? '编辑文件存储渠道' : '新增文件存储渠道'}
|
||||
bodyClassName="fileStorageDialogBody"
|
||||
@ -308,6 +367,34 @@ export function SystemSettingsPanel(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function defaultClientCustomizationForm(): ClientCustomizationForm {
|
||||
return {
|
||||
clientEnglishName: 'EasyAI AI Gateway',
|
||||
clientName: 'EasyAI AI Gateway',
|
||||
iconPath: '',
|
||||
platformName: 'EasyAI',
|
||||
};
|
||||
}
|
||||
|
||||
function clientCustomizationSettingsToForm(settings: ClientCustomizationSettings | null): ClientCustomizationForm {
|
||||
const defaults = defaultClientCustomizationForm();
|
||||
return {
|
||||
clientEnglishName: settings?.clientEnglishName ?? defaults.clientEnglishName,
|
||||
clientName: settings?.clientName ?? defaults.clientName,
|
||||
iconPath: settings?.iconPath ?? defaults.iconPath,
|
||||
platformName: settings?.platformName ?? defaults.platformName,
|
||||
};
|
||||
}
|
||||
|
||||
function clientCustomizationFormToPayload(form: ClientCustomizationForm): ClientCustomizationSettingsUpdateRequest {
|
||||
return {
|
||||
clientEnglishName: form.clientEnglishName.trim(),
|
||||
clientName: form.clientName.trim(),
|
||||
iconPath: form.iconPath.trim(),
|
||||
platformName: form.platformName.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultChannelForm(channelKey = ''): FileStorageChannelForm {
|
||||
return {
|
||||
apiKey: '',
|
||||
|
||||
@ -933,6 +933,20 @@ export interface FileStorageSettingsUpdateRequest {
|
||||
resultUploadPolicy: 'default' | 'upload_all' | 'upload_none' | string;
|
||||
}
|
||||
|
||||
export interface ClientCustomizationSettings {
|
||||
clientName: string;
|
||||
clientEnglishName: string;
|
||||
platformName: string;
|
||||
iconPath: string;
|
||||
}
|
||||
|
||||
export interface ClientCustomizationSettingsUpdateRequest {
|
||||
clientName?: string;
|
||||
clientEnglishName?: string;
|
||||
platformName?: string;
|
||||
iconPath?: string;
|
||||
}
|
||||
|
||||
export interface GatewayTask {
|
||||
id: string;
|
||||
kind: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user