diff --git a/docs/api/README.md b/docs/api/README.md index 09e49911..11ed509e 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -89,5 +89,6 @@ WeKnora API 按功能分为以下几类: | Skills | 预装智能体技能 | [skill.md](./skill.md) | | 网络搜索 | 网络搜索服务商 | [web-search.md](./web-search.md) | | 向量存储 | 向量数据库连接管理 | [vector-store.md](./vector-store.md) | +| 存储后端 | 对象/文件存储实例(多实例)管理 | [storage-backend.md](./storage-backend.md) | | IM 渠道 | 企业微信 / 飞书 / Slack 等 IM 平台对接,含渠道 CRUD 与回调 | [../IM集成开发文档.md](../IM集成开发文档.md) | | 数据源导入 | 飞书 / 企微 / Notion / Confluence 等外部数据源接入与同步 | [../数据源导入开发文档.md](../数据源导入开发文档.md) | diff --git a/docs/api/storage-backend.md b/docs/api/storage-backend.md new file mode 100644 index 00000000..29657e21 --- /dev/null +++ b/docs/api/storage-backend.md @@ -0,0 +1,341 @@ +# Storage Backend API + +[返回目录](./README.md) + +存储后端(StorageBackend)API 用于管理空间的对象/文件存储实例。一个空间可以注册多个存储实例(`local`、`minio`、`cos`、`tos`、`s3`、`oss`、`ks3`、`obs`),并将不同知识库绑定到不同实例;空间维度还有一个默认实例(`default_storage_backend_id`),未显式绑定的新知识库使用该默认实例。 + +接口同时管理用户创建的实例(`source: "user"`)以及从环境变量快照生成的只读实例(`source: "env"`)。存储后端 CRUD 需要 **Admin+** 角色;对 API Key 需具备 `manage_storage_backends` capability(或 full-access)。 + +| 方法 | 路径 | 描述 | 最低权限 | +| ------ | --------------------------------- | -------------------------------------- | -------- | +| GET | `/storage-backends/types` | 获取 `STORAGE_ALLOW_LIST` 允许的存储类型 | Viewer+ | +| POST | `/storage-backends/test` | 使用原始配置测试连通性(不落库) | Admin+ | +| POST | `/storage-backends` | 创建存储实例 | Admin+ | +| GET | `/storage-backends` | 获取存储实例列表 | Viewer+ | +| GET | `/storage-backends/:id` | 获取存储实例详情 | Viewer+ | +| PUT | `/storage-backends/:id` | 更新存储实例(名称/凭据/状态) | Admin+ | +| DELETE | `/storage-backends/:id` | 删除存储实例(软删除) | Admin+ | +| POST | `/storage-backends/:id/test` | 测试已保存实例的连通性 | Admin+ | +| PUT | `/storage-backends/:id/default` | 设为空间默认存储实例 | Admin+ | + +> 所有响应中的敏感字段(`access_key_id`、`secret_access_key`)都会被掩码。更新时若提交掩码占位符,则保留库中原有的真实凭据,不会被占位符覆盖。 + +## 存储配置字段(`config`) + +不同 provider 使用同一套归一化的配置对象,按 provider 取用其中的子集: + +| 字段 | 类型 | 说明 | +| ------------------- | ------- | ------------------------------------------------------------- | +| mode | string | MinIO 模式:`docker`(复用环境变量凭据)或 `remote` | +| endpoint | string | 对象存储 endpoint(COS 使用 region,不需要 endpoint) | +| region | string | 区域 | +| access_key_id | string | 访问密钥 ID(COS 对应 SecretID);响应中掩码 | +| secret_access_key | string | 访问密钥 Secret(COS 对应 SecretKey);响应中掩码 | +| bucket_name | string | Bucket 名称 | +| path_prefix | string | 对象前缀,必须为相对路径,禁止 `/` 开头或 `..` 上跳 | +| app_id | string | 腾讯云 COS AppID | +| use_ssl | boolean | 是否使用 SSL | +| force_path_style | boolean | S3 是否使用 path-style 寻址 | +| use_temp_bucket | boolean | OSS 是否使用临时 bucket | +| temp_bucket_name | string | 临时 bucket 名称 | +| temp_region | string | 临时 bucket 区域 | + +> `endpoint`、`region`、`bucket_name`、`path_prefix` 决定对象的物理位置,**创建后不可变更**(更新时会被拒绝);如需迁移请使用存储迁移流程。凭据可通过更新单独轮换。 + +## GET `/storage-backends/types` - 获取允许的存储类型 + +返回 `STORAGE_ALLOW_LIST` 允许的 provider 列表,可用于前端动态表单生成。 + +**请求**: + +```curl +curl --location 'http://localhost:8080/api/v1/storage-backends/types' \ +--header 'X-API-Key: sk-xxxxx' +``` + +**响应**: + +```json +{ + "success": true, + "data": ["local", "minio", "cos", "s3"] +} +``` + +## POST `/storage-backends/test` - 使用原始配置测试连通性 + +用前端表单中尚未保存的配置执行一次连通性测试,不会写入数据库。 + +**参数说明(请求体)**: + +| 字段 | 类型 | 必填 | 说明 | +| -------- | ------ | ---- | ------------------------------------------- | +| name | string | 是 | 实例显示名 | +| provider | string | 是 | 存储类型,取自 `/storage-backends/types` | +| config | object | 否 | 该 provider 对应的存储配置字段 | + +**请求**: + +```curl +curl --location --request POST 'http://localhost:8080/api/v1/storage-backends/test' \ +--header 'X-API-Key: sk-xxxxx' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "s3-hot", + "provider": "s3", + "config": { + "endpoint": "https://s3.example.com", + "region": "ap-test-1", + "access_key_id": "AKID", + "secret_access_key": "SECRET", + "bucket_name": "weknora" + } +}' +``` + +**响应(成功)**: + +```json +{ + "success": true +} +``` + +**响应(失败)**: + +```json +{ + "success": false, + "error": "连接被拒绝,请确认服务已启动且端口正确" +} +``` + +> 测试失败时 HTTP 状态码仍为 `200`,错误信息通过 `success: false` + `error` 返回;`error` 已脱敏,不会泄漏内部主机名、IP、端口或 TLS 细节。 + +## POST `/storage-backends` - 创建存储实例 + +为当前空间创建一个新的存储实例。创建前会先校验配置、执行 SSRF 校验(本地存储与 docker 模式 MinIO 除外),并执行一次连通性测试;任一环节失败都会返回 `400`。同一空间内实例名称不允许重复。 + +**参数说明(请求体)**: + +| 字段 | 类型 | 必填 | 说明 | +| -------- | ------ | ---- | ------------------------------------------- | +| name | string | 是 | 实例显示名(空间内唯一) | +| provider | string | 是 | 存储类型,取自 `/storage-backends/types` | +| config | object | 否 | 该 provider 对应的存储配置字段 | +| status | string | 否 | `active`(默认)或 `disabled` | + +**请求**: + +```curl +curl --location 'http://localhost:8080/api/v1/storage-backends' \ +--header 'X-API-Key: sk-xxxxx' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "s3-hot", + "provider": "s3", + "config": { + "endpoint": "https://s3.example.com", + "region": "ap-test-1", + "access_key_id": "AKID", + "secret_access_key": "SECRET", + "bucket_name": "weknora", + "path_prefix": "prod" + } +}' +``` + +**响应** (201): + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "s3-hot", + "provider": "s3", + "config": { + "endpoint": "https://s3.example.com", + "region": "ap-test-1", + "access_key_id": "***", + "secret_access_key": "***", + "bucket_name": "weknora", + "path_prefix": "prod" + }, + "source": "user", + "status": "active", + "legacy_alias": false, + "created_at": "2026-07-15T10:00:00Z", + "updated_at": "2026-07-15T10:00:00Z" + } +} +``` + +## GET `/storage-backends` - 获取存储实例列表 + +返回当前空间的所有存储实例(凭据已掩码),并在顶层返回空间默认实例 id。 + +**请求**: + +```curl +curl --location 'http://localhost:8080/api/v1/storage-backends' \ +--header 'X-API-Key: sk-xxxxx' +``` + +**响应**: + +```json +{ + "success": true, + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "s3-hot", + "provider": "s3", + "config": { "endpoint": "https://s3.example.com", "access_key_id": "***", "secret_access_key": "***", "bucket_name": "weknora" }, + "source": "user", + "status": "active", + "legacy_alias": false + } + ], + "default_storage_backend_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +## GET `/storage-backends/:id` - 获取存储实例详情 + +根据 ID 获取当前空间下的单个存储实例,凭据已掩码。 + +**路径参数**: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ------------- | +| id | string | 是 | 存储实例 ID | + +**请求**: + +```curl +curl --location 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \ +--header 'X-API-Key: sk-xxxxx' +``` + +## PUT `/storage-backends/:id` - 更新存储实例 + +更新实例的可变字段(`name`、凭据、`status`)。`provider` 与物理位置字段(`endpoint`、`region`、`bucket_name`、`path_prefix`)不可变更,尝试更改会返回 `400`。环境变量来源(`source: "env"`)的实例只读,不可更新。更新同样会执行校验与连通性测试。 + +> 若 `access_key_id` / `secret_access_key` 提交为掩码占位符(`***`),则保留库中原有真实凭据。 + +**禁用保护**:将当前为默认实例、或仍有知识库绑定的实例改为 `disabled` 会被拒绝(`400`)。 + +**路径参数**: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ------------- | +| id | string | 是 | 存储实例 ID | + +**请求**: + +```curl +curl --location --request PUT 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \ +--header 'X-API-Key: sk-xxxxx' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "s3-hot-renamed", + "provider": "s3", + "config": { + "access_key_id": "***", + "secret_access_key": "NEW_SECRET" + } +}' +``` + +## DELETE `/storage-backends/:id` - 删除存储实例 + +对存储实例执行软删除。以下情况删除会被拒绝(`400`):实例是空间默认实例、仍有知识库绑定、环境变量来源(只读)、或为 legacy 别名(旧文件路径可能仍引用它)。删除在事务中执行;PostgreSQL 上对目标行加行锁以避免并发绑定竞态。 + +**路径参数**: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ------------- | +| id | string | 是 | 存储实例 ID | + +**请求**: + +```curl +curl --location --request DELETE 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \ +--header 'X-API-Key: sk-xxxxx' +``` + +**响应(成功)**: + +```json +{ + "success": true +} +``` + +## POST `/storage-backends/:id/test` - 测试已保存实例的连通性 + +对已保存的存储实例用其存储的凭据执行一次连通性测试。 + +**路径参数**: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ------------- | +| id | string | 是 | 存储实例 ID | + +**请求**: + +```curl +curl --location --request POST 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000/test' \ +--header 'X-API-Key: sk-xxxxx' +``` + +**响应(成功)**: + +```json +{ + "success": true +} +``` + +> 与 `/storage-backends/test` 一致,测试失败时 HTTP 状态码仍为 `200`,错误经脱敏后通过 `success: false` + `error` 返回。 + +## PUT `/storage-backends/:id/default` - 设为空间默认实例 + +将某个存储实例标记为空间默认实例。仅 `active` 状态的实例可以设为默认。未显式绑定存储实例的新知识库将使用默认实例。 + +**路径参数**: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ------ | ---- | ------------- | +| id | string | 是 | 存储实例 ID | + +**请求**: + +```curl +curl --location --request PUT 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000/default' \ +--header 'X-API-Key: sk-xxxxx' +``` + +**响应**: + +```json +{ + "success": true +} +``` + +## 环境变量存储实例 + +通过 `STORAGE_TYPE` 等环境变量配置的存储会以只读实例(`source: "env"`、`legacy_alias: true`)形式参与实例解析,使 env-only 部署与用户管理的实例走同一套解析路径。这类实例在每次启动时按环境变量刷新,且不可通过 API 更新或删除。 + +## 错误码 + +| HTTP 状态码 | 含义 | +| ----------- | ---------------------------------------------------------------- | +| 400 | 请求参数错误、校验失败、SSRF 校验失败、连通性测试失败、尝试更改不可变字段、修改只读实例、删除受保护实例、禁用被引用实例、设为默认时实例非 active | +| 401 | 未认证(缺少空间上下文或 API Key) | +| 403 | 权限不足(需 Admin+ 或 API Key `manage_storage_backends` capability) | +| 404 | 存储实例不存在 | +| 409 | 同名存储实例已存在 | diff --git a/docs/docs.go b/docs/docs.go index c9c7ce22..3c3cba25 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -11077,6 +11077,468 @@ const docTemplate = `{ } } }, + "/storage-backends": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "List all storage backend instances for the current workspace, with credentials masked. The workspace default backend id is returned alongside the list.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "List storage backends", + "responses": { + "200": { + "description": "List of storage backends and default_storage_backend_id", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Register a new object/file storage instance for the current workspace. The configuration is validated and a connectivity test is run before the backend is persisted.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Create storage backend", + "parameters": [ + { + "description": "Storage backend configuration", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "201": { + "description": "Created storage backend", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Invalid request, validation, or connectivity test failure", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "409": { + "description": "A storage backend with this name already exists", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/test": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Test connectivity for the provided storage configuration without persisting it. Returns success=false with a sanitized error message on failure (the HTTP status stays 200).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Test storage backend with raw config", + "parameters": [ + { + "description": "Storage backend configuration to test", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "200": { + "description": "Connectivity test result (success, error)", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Invalid request or validation error", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/storage-backends/types": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Return the storage provider types allowed by STORAGE_ALLOW_LIST for UI form generation (e.g. local, minio, cos, tos, s3, oss, ks3, obs).", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "List allowed storage provider types", + "responses": { + "200": { + "description": "List of allowed storage provider types", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/storage-backends/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieve a single storage backend by ID for the current workspace. Credentials are masked.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Get storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Storage backend details", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Update a storage backend's mutable fields (name, credentials, status). Provider and physical location (endpoint, region, bucket, path prefix) are immutable; use storage migration to move data. Environment-sourced backends are read-only. Redacted secret placeholders preserve the stored credentials.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Update storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated storage backend fields", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated storage backend", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Immutable field change, read-only backend, validation, or connectivity failure", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Soft-delete a storage backend. A backend that is the workspace default, still bound to knowledge bases, environment-sourced, or a legacy alias cannot be deleted.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Delete storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Deletion success", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Backend is default, bound, read-only, or legacy alias", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/{id}/default": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Mark a storage backend as the workspace default. Only an active backend can become the default. New knowledge bases without an explicit binding use the default.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Set default storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Default set successfully", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Backend is not active", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/{id}/test": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Test connectivity of an existing saved storage backend using its stored credentials. Returns success=false with a sanitized error message on failure (the HTTP status stays 200).", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Test storage backend by ID", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Connectivity test result (success, error)", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, "/system/admin/audit-log": { "get": { "security": [ @@ -14387,6 +14849,12 @@ const docTemplate = `{ }, "secret_key": { "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" } } }, @@ -15718,6 +16186,10 @@ const docTemplate = `{ "description": "ShareCount indicates the number of organizations this knowledge base is shared with (not stored in database)", "type": "integer" }, + "storage_backend_id": { + "description": "StorageBackendID binds this KB to one concrete storage instance. The\nlegacy provider field remains readable during migration only.", + "type": "string" + }, "storage_config": { "description": "Deprecated: legacy COS config column. Kept for backward compatibility with old data.", "allOf": [ @@ -17888,6 +18360,50 @@ const docTemplate = `{ } } }, + "github_com_Tencent_WeKnora_internal_types.StorageBackendConfig": { + "type": "object", + "properties": { + "access_key_id": { + "type": "string" + }, + "app_id": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "force_path_style": { + "type": "boolean" + }, + "mode": { + "type": "string" + }, + "path_prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret_access_key": { + "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" + }, + "use_ssl": { + "type": "boolean" + }, + "use_temp_bucket": { + "type": "boolean" + } + } + }, "github_com_Tencent_WeKnora_internal_types.StorageConfig": { "type": "object", "properties": { @@ -18159,6 +18675,12 @@ const docTemplate = `{ }, "secret_key": { "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" } } }, @@ -18197,6 +18719,10 @@ const docTemplate = `{ } ] }, + "default_storage_backend_id": { + "description": "DefaultStorageBackendID is the workspace default concrete storage instance.", + "type": "string" + }, "deleted_at": { "description": "Deletion time", "allOf": [ @@ -19993,6 +20519,9 @@ const docTemplate = `{ } } }, + "storageBackendId": { + "type": "string" + }, "storageProvider": { "description": "存储引擎选择(\"local\" | \"minio\" | \"cos\"),影响文档上传与文档内图片存储,参数从全局设置读取", "type": "string" @@ -20516,7 +21045,7 @@ const docTemplate = `{ "type": "string" }, "name": { - "description": "\"local\", \"minio\", \"cos\", \"tos\", \"s3\", \"oss\", \"ks3\"", + "description": "\"local\", \"minio\", \"cos\", \"tos\", \"s3\", \"oss\", \"ks3\", \"obs\"", "type": "string" } } @@ -20923,6 +21452,27 @@ const docTemplate = `{ } } }, + "internal_handler.storageBackendRequest": { + "type": "object", + "required": [ + "name", + "provider" + ], + "properties": { + "config": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.StorageBackendConfig" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "internal_handler.updateLastFAQImportResultDisplayStatusRequest": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 484f857d..5e7dc30c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -11070,6 +11070,468 @@ } } }, + "/storage-backends": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "List all storage backend instances for the current workspace, with credentials masked. The workspace default backend id is returned alongside the list.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "List storage backends", + "responses": { + "200": { + "description": "List of storage backends and default_storage_backend_id", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Register a new object/file storage instance for the current workspace. The configuration is validated and a connectivity test is run before the backend is persisted.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Create storage backend", + "parameters": [ + { + "description": "Storage backend configuration", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "201": { + "description": "Created storage backend", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Invalid request, validation, or connectivity test failure", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "409": { + "description": "A storage backend with this name already exists", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/test": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Test connectivity for the provided storage configuration without persisting it. Returns success=false with a sanitized error message on failure (the HTTP status stays 200).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Test storage backend with raw config", + "parameters": [ + { + "description": "Storage backend configuration to test", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "200": { + "description": "Connectivity test result (success, error)", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Invalid request or validation error", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/storage-backends/types": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Return the storage provider types allowed by STORAGE_ALLOW_LIST for UI form generation (e.g. local, minio, cos, tos, s3, oss, ks3, obs).", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "List allowed storage provider types", + "responses": { + "200": { + "description": "List of allowed storage provider types", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/storage-backends/{id}": { + "get": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieve a single storage backend by ID for the current workspace. Credentials are masked.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Get storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Storage backend details", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Update a storage backend's mutable fields (name, credentials, status). Provider and physical location (endpoint, region, bucket, path prefix) are immutable; use storage migration to move data. Environment-sourced backends are read-only. Redacted secret placeholders preserve the stored credentials.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Update storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated storage backend fields", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.storageBackendRequest" + } + } + ], + "responses": { + "200": { + "description": "Updated storage backend", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Immutable field change, read-only backend, validation, or connectivity failure", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Soft-delete a storage backend. A backend that is the workspace default, still bound to knowledge bases, environment-sourced, or a legacy alias cannot be deleted.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Delete storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Deletion success", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Backend is default, bound, read-only, or legacy alias", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/{id}/default": { + "put": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Mark a storage backend as the workspace default. Only an active backend can become the default. New knowledge bases without an explicit binding use the default.", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Set default storage backend", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Default set successfully", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Backend is not active", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, + "/storage-backends/{id}/test": { + "post": { + "security": [ + { + "Bearer": [] + }, + { + "ApiKeyAuth": [] + } + ], + "description": "Test connectivity of an existing saved storage backend using its stored credentials. Returns success=false with a sanitized error message on failure (the HTTP status stays 200).", + "produces": [ + "application/json" + ], + "tags": [ + "StorageBackend" + ], + "summary": "Test storage backend by ID", + "parameters": [ + { + "type": "string", + "description": "Storage backend ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Connectivity test result (success, error)", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Storage backend not found", + "schema": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError" + } + } + } + } + }, "/system/admin/audit-log": { "get": { "security": [ @@ -14380,6 +14842,12 @@ }, "secret_key": { "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" } } }, @@ -15711,6 +16179,10 @@ "description": "ShareCount indicates the number of organizations this knowledge base is shared with (not stored in database)", "type": "integer" }, + "storage_backend_id": { + "description": "StorageBackendID binds this KB to one concrete storage instance. The\nlegacy provider field remains readable during migration only.", + "type": "string" + }, "storage_config": { "description": "Deprecated: legacy COS config column. Kept for backward compatibility with old data.", "allOf": [ @@ -17881,6 +18353,50 @@ } } }, + "github_com_Tencent_WeKnora_internal_types.StorageBackendConfig": { + "type": "object", + "properties": { + "access_key_id": { + "type": "string" + }, + "app_id": { + "type": "string" + }, + "bucket_name": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "force_path_style": { + "type": "boolean" + }, + "mode": { + "type": "string" + }, + "path_prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret_access_key": { + "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" + }, + "use_ssl": { + "type": "boolean" + }, + "use_temp_bucket": { + "type": "boolean" + } + } + }, "github_com_Tencent_WeKnora_internal_types.StorageConfig": { "type": "object", "properties": { @@ -18152,6 +18668,12 @@ }, "secret_key": { "type": "string" + }, + "temp_bucket_name": { + "type": "string" + }, + "temp_region": { + "type": "string" } } }, @@ -18190,6 +18712,10 @@ } ] }, + "default_storage_backend_id": { + "description": "DefaultStorageBackendID is the workspace default concrete storage instance.", + "type": "string" + }, "deleted_at": { "description": "Deletion time", "allOf": [ @@ -19986,6 +20512,9 @@ } } }, + "storageBackendId": { + "type": "string" + }, "storageProvider": { "description": "存储引擎选择(\"local\" | \"minio\" | \"cos\"),影响文档上传与文档内图片存储,参数从全局设置读取", "type": "string" @@ -20509,7 +21038,7 @@ "type": "string" }, "name": { - "description": "\"local\", \"minio\", \"cos\", \"tos\", \"s3\", \"oss\", \"ks3\"", + "description": "\"local\", \"minio\", \"cos\", \"tos\", \"s3\", \"oss\", \"ks3\", \"obs\"", "type": "string" } } @@ -20916,6 +21445,27 @@ } } }, + "internal_handler.storageBackendRequest": { + "type": "object", + "required": [ + "name", + "provider" + ], + "properties": { + "config": { + "$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.StorageBackendConfig" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "internal_handler.updateLastFAQImportResultDisplayStatusRequest": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f09a6eff..8bb232a1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -377,6 +377,10 @@ definitions: type: string secret_key: type: string + temp_bucket_name: + type: string + temp_region: + type: string type: object github_com_Tencent_WeKnora_internal_types.ChatHistoryConfig: properties: @@ -1461,6 +1465,11 @@ definitions: description: ShareCount indicates the number of organizations this knowledge base is shared with (not stored in database) type: integer + storage_backend_id: + description: |- + StorageBackendID binds this KB to one concrete storage instance. The + legacy provider field remains readable during migration only. + type: string storage_config: allOf: - $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.StorageConfig' @@ -3080,6 +3089,35 @@ definitions: mode: type: string type: object + github_com_Tencent_WeKnora_internal_types.StorageBackendConfig: + properties: + access_key_id: + type: string + app_id: + type: string + bucket_name: + type: string + endpoint: + type: string + force_path_style: + type: boolean + mode: + type: string + path_prefix: + type: string + region: + type: string + secret_access_key: + type: string + temp_bucket_name: + type: string + temp_region: + type: string + use_ssl: + type: boolean + use_temp_bucket: + type: boolean + type: object github_com_Tencent_WeKnora_internal_types.StorageConfig: properties: app_id: @@ -3292,6 +3330,10 @@ definitions: type: string secret_key: type: string + temp_bucket_name: + type: string + temp_region: + type: string type: object github_com_Tencent_WeKnora_internal_types.Tenant: properties: @@ -3316,6 +3358,10 @@ definitions: - $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.CredentialsConfig' description: 'Credentials config: third-party provider credentials (e.g. WeKnoraCloud AppID/AppSecret)' + default_storage_backend_id: + description: DefaultStorageBackendID is the workspace default concrete storage + instance. + type: string deleted_at: allOf: - $ref: '#/definitions/gorm.DeletedAt' @@ -4651,6 +4697,8 @@ definitions: questionCount: type: integer type: object + storageBackendId: + type: string storageProvider: description: 存储引擎选择("local" | "minio" | "cos"),影响文档上传与文档内图片存储,参数从全局设置读取 type: string @@ -5024,7 +5072,7 @@ definitions: description: short description for UI type: string name: - description: '"local", "minio", "cos", "tos", "s3", "oss", "ks3"' + description: '"local", "minio", "cos", "tos", "s3", "oss", "ks3", "obs"' type: string type: object internal_handler.SuggestionEventRequest: @@ -5295,6 +5343,20 @@ definitions: type: string type: array type: object + internal_handler.storageBackendRequest: + properties: + config: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.StorageBackendConfig' + name: + type: string + provider: + type: string + status: + type: string + required: + - name + - provider + type: object internal_handler.updateLastFAQImportResultDisplayStatusRequest: properties: display_status: @@ -12553,6 +12615,314 @@ paths: summary: 获取预装Skills列表 tags: - Skills + /storage-backends: + get: + description: List all storage backend instances for the current workspace, with + credentials masked. The workspace default backend id is returned alongside + the list. + produces: + - application/json + responses: + "200": + description: List of storage backends and default_storage_backend_id + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: List storage backends + tags: + - StorageBackend + post: + consumes: + - application/json + description: Register a new object/file storage instance for the current workspace. + The configuration is validated and a connectivity test is run before the backend + is persisted. + parameters: + - description: Storage backend configuration + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.storageBackendRequest' + produces: + - application/json + responses: + "201": + description: Created storage backend + schema: + additionalProperties: true + type: object + "400": + description: Invalid request, validation, or connectivity test failure + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "409": + description: A storage backend with this name already exists + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Create storage backend + tags: + - StorageBackend + /storage-backends/{id}: + delete: + description: Soft-delete a storage backend. A backend that is the workspace + default, still bound to knowledge bases, environment-sourced, or a legacy + alias cannot be deleted. + parameters: + - description: Storage backend ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Deletion success + schema: + additionalProperties: true + type: object + "400": + description: Backend is default, bound, read-only, or legacy alias + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "404": + description: Storage backend not found + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Delete storage backend + tags: + - StorageBackend + get: + description: Retrieve a single storage backend by ID for the current workspace. + Credentials are masked. + parameters: + - description: Storage backend ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Storage backend details + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "404": + description: Storage backend not found + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Get storage backend + tags: + - StorageBackend + put: + consumes: + - application/json + description: Update a storage backend's mutable fields (name, credentials, status). + Provider and physical location (endpoint, region, bucket, path prefix) are + immutable; use storage migration to move data. Environment-sourced backends + are read-only. Redacted secret placeholders preserve the stored credentials. + parameters: + - description: Storage backend ID + in: path + name: id + required: true + type: string + - description: Updated storage backend fields + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.storageBackendRequest' + produces: + - application/json + responses: + "200": + description: Updated storage backend + schema: + additionalProperties: true + type: object + "400": + description: Immutable field change, read-only backend, validation, or connectivity + failure + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "404": + description: Storage backend not found + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Update storage backend + tags: + - StorageBackend + /storage-backends/{id}/default: + put: + description: Mark a storage backend as the workspace default. Only an active + backend can become the default. New knowledge bases without an explicit binding + use the default. + parameters: + - description: Storage backend ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Default set successfully + schema: + additionalProperties: true + type: object + "400": + description: Backend is not active + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "404": + description: Storage backend not found + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Set default storage backend + tags: + - StorageBackend + /storage-backends/{id}/test: + post: + description: Test connectivity of an existing saved storage backend using its + stored credentials. Returns success=false with a sanitized error message on + failure (the HTTP status stays 200). + parameters: + - description: Storage backend ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Connectivity test result (success, error) + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + "404": + description: Storage backend not found + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Test storage backend by ID + tags: + - StorageBackend + /storage-backends/test: + post: + consumes: + - application/json + description: Test connectivity for the provided storage configuration without + persisting it. Returns success=false with a sanitized error message on failure + (the HTTP status stays 200). + parameters: + - description: Storage backend configuration to test + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.storageBackendRequest' + produces: + - application/json + responses: + "200": + description: Connectivity test result (success, error) + schema: + additionalProperties: true + type: object + "400": + description: Invalid request or validation error + schema: + $ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError' + "401": + description: Unauthorized + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: Test storage backend with raw config + tags: + - StorageBackend + /storage-backends/types: + get: + description: Return the storage provider types allowed by STORAGE_ALLOW_LIST + for UI form generation (e.g. local, minio, cos, tos, s3, oss, ks3, obs). + produces: + - application/json + responses: + "200": + description: List of allowed storage provider types + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + - ApiKeyAuth: [] + summary: List allowed storage provider types + tags: + - StorageBackend /system/admin/audit-log: get: description: 返回 system-scope(tenant_id=0)的审计事件,覆盖 system.setting_changed / system.admin_promoted diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index b6fc9460..3709554f 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1371,6 +1371,49 @@ export default { paddleocrVlEndpointHint: 'Base URL of the full PaddleOCR-VL pipeline service; no /layout-parsing suffix needed', paddleocrVlCloudTokenPlaceholder: 'PaddleOCR-VL AI Studio Token', }, + storageBackend: { + description: 'Manage the storage instances used for files and images; multiple instances of the same type can be configured.', + empty: 'No storage instances configured yet', + defaultTag: 'Default', + add: 'Add storage instance', + editTitle: 'Edit storage instance', + createTitle: 'Add storage instance', + editSubtitle: 'Update the connection configuration of this storage instance.', + createSubtitle: 'Add a new storage instance for files and images.', + basicSection: 'Basic information', + nameLabel: 'Name', + namePlaceholder: 'e.g. Production COS, Archive COS', + providerLabel: 'Storage type', + modeLabel: 'Deployment mode', + modeRemote: 'Remote instance', + modeEnv: 'Environment variables', + connectionSection: 'Connection settings', + optionalPlaceholder: 'Optional', + advancedSection: 'Advanced options', + pathPrefixLabel: 'Path prefix', + useSslDesc: 'Access MinIO over HTTPS', + forcePathStyleDesc: 'Use path style', + useTempBucketDesc: 'Use temporary bucket', + tempBucketLabel: 'Temporary bucket', + tempBucketPlaceholder: 'Optional, for temporary files', + tempRegionLabel: 'Temporary bucket region', + tempRegionPlaceholder: 'Leave empty to use the main region', + testConnection: 'Test connection', + localStorage: 'Local storage', + setDefault: 'Set as default', + edit: 'Edit', + delete: 'Delete', + testSuccess: 'Connection succeeded', + testFailed: 'Connection failed', + nameRequired: 'Please enter a name', + saveSuccess: 'Saved successfully', + saveFailed: 'Failed to save', + defaultUpdated: 'Default storage updated', + deleteTitle: 'Delete storage instance', + deleteConfirm: 'Are you sure you want to delete "{name}"?', + deleted: 'Deleted', + deleteFailed: 'Failed to delete', + }, storage: { title: 'Storage Engine', description: 'Configure document and image storage. Set engine parameters here; knowledge bases only select which engine to use.', @@ -5327,6 +5370,13 @@ export default { storage: { title: 'Storage Engine', description: 'Select the file storage engine. This affects how uploaded documents and images within documents are stored. Parameters are configured in global settings.', + selectDescription: 'Select the specific storage instance to bind this knowledge base to.', + defaultTag: 'Default', + instanceLabel: 'Storage instance', + instanceDesc: 'The same storage type can have multiple instances with different endpoints, buckets, or credentials.', + migrateHint: 'This knowledge base already contains files; use the storage migration flow to switch instances.', + manageInstances: 'Manage storage instances', + localStorage: 'Local storage', loading: 'Loading...', engineLabel: 'Storage Engine', engineDesc: 'Select the storage engine for this knowledge base. The corresponding engine must be configured in global settings.', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index ad1e75c9..fc43cc21 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -1229,6 +1229,49 @@ export default { paddleocrVlEndpointHint: 'PaddleOCR-VL 전체 서비스(pipeline) 주소를 입력하세요. /layout-parsing 접미사는 불필요합니다', paddleocrVlCloudTokenPlaceholder: 'PaddleOCR-VL AI Studio Token', }, + storageBackend: { + description: '파일과 이미지에 사용하는 스토리지 인스턴스를 관리합니다. 같은 유형의 인스턴스를 여러 개 구성할 수 있습니다.', + empty: '아직 구성된 스토리지 인스턴스가 없습니다', + defaultTag: '기본', + add: '스토리지 인스턴스 추가', + editTitle: '스토리지 인스턴스 편집', + createTitle: '스토리지 인스턴스 추가', + editSubtitle: '이 스토리지 인스턴스의 연결 구성을 수정합니다.', + createSubtitle: '파일과 이미지를 위한 새 스토리지 인스턴스를 추가합니다.', + basicSection: '기본 정보', + nameLabel: '이름', + namePlaceholder: '예: 프로덕션 COS, 아카이브 COS', + providerLabel: '스토리지 유형', + modeLabel: '배포 모드', + modeRemote: '원격 인스턴스', + modeEnv: '환경 변수', + connectionSection: '연결 설정', + optionalPlaceholder: '선택 사항', + advancedSection: '고급 옵션', + pathPrefixLabel: '경로 접두사', + useSslDesc: 'HTTPS로 MinIO 접속', + forcePathStyleDesc: 'Path Style 사용', + useTempBucketDesc: '임시 버킷 사용', + tempBucketLabel: '임시 버킷', + tempBucketPlaceholder: '선택 사항, 임시 파일용', + tempRegionLabel: '임시 버킷 Region', + tempRegionPlaceholder: '비워 두면 기본 Region을 사용합니다', + testConnection: '연결 테스트', + localStorage: '로컬 스토리지', + setDefault: '기본으로 설정', + edit: '편집', + delete: '삭제', + testSuccess: '연결에 성공했습니다', + testFailed: '연결에 실패했습니다', + nameRequired: '이름을 입력하세요', + saveSuccess: '저장되었습니다', + saveFailed: '저장하지 못했습니다', + defaultUpdated: '기본 스토리지가 업데이트되었습니다', + deleteTitle: '스토리지 인스턴스 삭제', + deleteConfirm: '"{name}"을(를) 삭제하시겠습니까?', + deleted: '삭제되었습니다', + deleteFailed: '삭제하지 못했습니다', + }, storage: { title: '스토리지 엔진', description: '문서 및 이미지 저장 방식을 구성합니다. 엔진 파라미터를 설정하면 지식베이스에서 사용할 엔진만 선택합니다.', @@ -5354,6 +5397,13 @@ export default { storage: { title: '스토리지 엔진', description: '파일 스토리지 엔진을 선택합니다. 문서 업로드 및 문서 내 이미지 저장 방식에 영향을 미칩니다. 파라미터는 전역 설정에서 구성합니다.', + selectDescription: '이 지식베이스를 바인딩할 구체적인 스토리지 인스턴스를 선택하세요.', + defaultTag: '기본', + instanceLabel: '스토리지 인스턴스', + instanceDesc: '같은 스토리지 유형에 서로 다른 Endpoint, Bucket 또는 자격 증명을 가진 여러 인스턴스를 구성할 수 있습니다.', + migrateHint: '이 지식베이스에는 이미 파일이 있어 스토리지 마이그레이션 절차를 통해서만 인스턴스를 변경할 수 있습니다.', + manageInstances: '스토리지 인스턴스 관리', + localStorage: '로컬 스토리지', loading: '로딩 중...', engineLabel: '스토리지 엔진', engineDesc: '이 지식베이스에 사용할 스토리지 엔진을 선택하세요. 전역 설정에서 해당 엔진이 구성되어 있어야 합니다.', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 461924b4..e7fa13a3 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -1280,6 +1280,49 @@ export default { paddleocrVlEndpointHint: 'Адрес полного сервиса PaddleOCR-VL (pipeline); суффикс /layout-parsing не требуется', paddleocrVlCloudTokenPlaceholder: 'Токен PaddleOCR-VL AI Studio' }, + storageBackend: { + description: 'Управляйте экземплярами хранилища для файлов и изображений; можно настроить несколько экземпляров одного типа.', + empty: 'Экземпляры хранилища ещё не настроены', + defaultTag: 'По умолчанию', + add: 'Добавить экземпляр хранилища', + editTitle: 'Изменить экземпляр хранилища', + createTitle: 'Добавить экземпляр хранилища', + editSubtitle: 'Измените настройки подключения этого экземпляра хранилища.', + createSubtitle: 'Добавьте новый экземпляр хранилища для файлов и изображений.', + basicSection: 'Основная информация', + nameLabel: 'Название', + namePlaceholder: 'напр. Production COS, Archive COS', + providerLabel: 'Тип хранилища', + modeLabel: 'Режим развёртывания', + modeRemote: 'Удалённый экземпляр', + modeEnv: 'Переменные окружения', + connectionSection: 'Настройки подключения', + optionalPlaceholder: 'Необязательно', + advancedSection: 'Дополнительные параметры', + pathPrefixLabel: 'Префикс пути', + useSslDesc: 'Доступ к MinIO по HTTPS', + forcePathStyleDesc: 'Использовать path style', + useTempBucketDesc: 'Использовать временный бакет', + tempBucketLabel: 'Временный бакет', + tempBucketPlaceholder: 'Необязательно, для временных файлов', + tempRegionLabel: 'Регион временного бакета', + tempRegionPlaceholder: 'Оставьте пустым, чтобы использовать основной регион', + testConnection: 'Проверить подключение', + localStorage: 'Локальное хранилище', + setDefault: 'Сделать по умолчанию', + edit: 'Изменить', + delete: 'Удалить', + testSuccess: 'Подключение успешно', + testFailed: 'Не удалось подключиться', + nameRequired: 'Введите название', + saveSuccess: 'Успешно сохранено', + saveFailed: 'Не удалось сохранить', + defaultUpdated: 'Хранилище по умолчанию обновлено', + deleteTitle: 'Удалить экземпляр хранилища', + deleteConfirm: 'Вы уверены, что хотите удалить «{name}»?', + deleted: 'Удалено', + deleteFailed: 'Не удалось удалить', + }, storage: { title: 'Хранилище', description: 'Настройте хранение документов и изображений. Здесь задаются параметры хранилищ; в базе знаний выбирается только тип хранилища.', @@ -4854,6 +4897,13 @@ export default { storage: { title: 'Хранилище', description: 'Выберите хранилище файлов. Это влияет на способ хранения загруженных документов и изображений в документах. Параметры настраиваются в глобальных настройках.', + selectDescription: 'Выберите конкретный экземпляр хранилища для привязки этой базы знаний.', + defaultTag: 'По умолчанию', + instanceLabel: 'Экземпляр хранилища', + instanceDesc: 'Для одного типа хранилища можно настроить несколько экземпляров с разными endpoint, бакетами или учётными данными.', + migrateHint: 'В этой базе знаний уже есть файлы; сменить экземпляр можно только через процедуру миграции хранилища.', + manageInstances: 'Управление экземплярами хранилища', + localStorage: 'Локальное хранилище', loading: 'Загрузка...', engineLabel: 'Хранилище', engineDesc: 'Выберите хранилище для этой базы знаний. Соответствующее хранилище должно быть настроено в глобальных настройках.', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index ca8fbd8f..c3ef68ac 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1237,6 +1237,49 @@ export default { paddleocrVlEndpointHint: "填写 PaddleOCR-VL 完整服务(pipeline)地址,无需 /layout-parsing 后缀", paddleocrVlCloudTokenPlaceholder: "PaddleOCR-VL 飞桨星河社区 Token", }, + storageBackend: { + description: "管理文件与图片使用的存储实例;同一种类型可以配置多个实例。", + empty: "尚未配置存储实例", + defaultTag: "默认", + add: "添加存储实例", + editTitle: "编辑存储实例", + createTitle: "添加存储实例", + editSubtitle: "修改此存储实例的连接配置。", + createSubtitle: "为文件与图片新增一个存储实例。", + basicSection: "基本信息", + nameLabel: "名称", + namePlaceholder: "例如:生产 COS、归档 COS", + providerLabel: "存储类型", + modeLabel: "部署模式", + modeRemote: "远程实例", + modeEnv: "环境变量", + connectionSection: "连接配置", + optionalPlaceholder: "可选", + advancedSection: "高级选项", + pathPrefixLabel: "路径前缀", + useSslDesc: "使用 HTTPS 访问 MinIO", + forcePathStyleDesc: "使用 Path Style", + useTempBucketDesc: "使用临时桶", + tempBucketLabel: "临时桶", + tempBucketPlaceholder: "可选,用于临时文件", + tempRegionLabel: "临时桶 Region", + tempRegionPlaceholder: "留空时使用主 Region", + testConnection: "测试连接", + localStorage: "本地存储", + setDefault: "设为默认", + edit: "编辑", + delete: "删除", + testSuccess: "连接成功", + testFailed: "连接失败", + nameRequired: "请输入名称", + saveSuccess: "保存成功", + saveFailed: "保存失败", + defaultUpdated: "默认存储已更新", + deleteTitle: "删除存储实例", + deleteConfirm: "确定删除“{name}”吗?", + deleted: "已删除", + deleteFailed: "删除失败", + }, storage: { title: "存储引擎", description: "配置文档与图片的存储方式。此处设置各引擎参数,知识库中仅选择使用哪个引擎。", @@ -5352,6 +5395,13 @@ export default { storage: { title: "存储引擎", description: "选择文件存储引擎,影响文档上传存储和文档中图片的存储方式。参数在全局设置中配置。", + selectDescription: "选择此知识库绑定的具体存储实例。", + defaultTag: "默认", + instanceLabel: "存储实例", + instanceDesc: "同一种存储类型可以配置多个不同 Endpoint、Bucket 或凭据的实例。", + migrateHint: "知识库中已有文件,需通过存储迁移流程才能更换实例。", + manageInstances: "管理存储实例", + localStorage: "本地存储", loading: "加载中...", engineLabel: "存储引擎", engineDesc: "选择该知识库使用的存储引擎,需在全局设置中已配置对应引擎。", diff --git a/frontend/src/views/knowledge/settings/KBStorageSettings.vue b/frontend/src/views/knowledge/settings/KBStorageSettings.vue index e65d1bdf..bf50e026 100644 --- a/frontend/src/views/knowledge/settings/KBStorageSettings.vue +++ b/frontend/src/views/knowledge/settings/KBStorageSettings.vue @@ -2,14 +2,14 @@

{{ $t('kbSettings.storage.title') }}

-

选择此知识库绑定的具体存储实例。

+

{{ $t('kbSettings.storage.selectDescription') }}

-
加载中...
+
{{ $t('kbSettings.storage.loading') }}
- -

同一种存储类型可以配置多个不同 Endpoint、Bucket 或凭据的实例。

+ +

{{ $t('kbSettings.storage.instanceDesc') }}

@@ -17,13 +17,13 @@ {{ backend.name }} {{ backend.provider.toUpperCase() }} - 默认 + {{ $t('kbSettings.storage.defaultTag') }} -

知识库中已有文件,需通过存储迁移流程才能更换实例。

-

{{ selected.config.endpoint || selected.config.bucket_name || selected.config.path_prefix || '本地存储' }}

- 管理存储实例 +

{{ $t('kbSettings.storage.migrateHint') }}

+

{{ selected.config.endpoint || selected.config.bucket_name || selected.config.path_prefix || $t('kbSettings.storage.localStorage') }}

+ {{ $t('kbSettings.storage.manageInstances') }}
diff --git a/frontend/src/views/settings/StorageBackendSettings.vue b/frontend/src/views/settings/StorageBackendSettings.vue index 9879706c..4c5fb971 100644 --- a/frontend/src/views/settings/StorageBackendSettings.vue +++ b/frontend/src/views/settings/StorageBackendSettings.vue @@ -4,7 +4,7 @@

{{ t('settings.storage.title') }}

-

管理文件与图片使用的存储实例;同一种类型可以配置多个实例。

+

{{ t('settings.storageBackend.description') }}

@@ -12,7 +12,7 @@

{{ backend.name }}

- 默认 + {{ t('settings.storageBackend.defaultTag') }}
{{ providerInitial(form.provider) }}
-

基本信息

+

{{ t('settings.storageBackend.basicSection') }}

- - + +
- +
- +
-

连接配置

+

{{ t('settings.storageBackend.connectionSection') }}

- +
-

高级选项

+

{{ t('settings.storageBackend.advancedSection') }}

- +
- 使用 HTTPS 访问 MinIO + {{ t('settings.storageBackend.useSslDesc') }}
- 使用 Path Style + {{ t('settings.storageBackend.forcePathStyleDesc') }}
- 使用临时桶 + {{ t('settings.storageBackend.useTempBucketDesc') }}
@@ -237,7 +237,7 @@ - 测试连接 + {{ t('settings.storageBackend.testConnection') }}
@@ -293,7 +293,7 @@ const monoLogoStyle = computed((): Record => { }) function backendMeta(backend: StorageBackend): string { - return backend.config.endpoint || backend.config.bucket_name || backend.config.path_prefix || '本地存储' + return backend.config.endpoint || backend.config.bucket_name || backend.config.path_prefix || t('settings.storageBackend.localStorage') } const canEdit = (backend: StorageBackend) => authStore.hasRole('admin') && backend.source !== 'env' @@ -304,10 +304,10 @@ const hasActions = (_backend: StorageBackend) => true function getBackendOptions(backend: StorageBackend) { const options: { content: string; value: string; theme?: string }[] = [] - options.push({ content: '测试连接', value: 'test' }) - if (canSetDefault(backend)) options.push({ content: '设为默认', value: 'default' }) - if (canEdit(backend)) options.push({ content: '编辑', value: 'edit' }) - if (canDelete(backend)) options.push({ content: '删除', value: 'delete', theme: 'error' }) + options.push({ content: t('settings.storageBackend.testConnection'), value: 'test' }) + if (canSetDefault(backend)) options.push({ content: t('settings.storageBackend.setDefault'), value: 'default' }) + if (canEdit(backend)) options.push({ content: t('settings.storageBackend.edit'), value: 'edit' }) + if (canDelete(backend)) options.push({ content: t('settings.storageBackend.delete'), value: 'delete', theme: 'error' }) return options } @@ -340,23 +340,23 @@ async function testRaw() { rawTestResult.value = null try { const r: any = editing.value ? await testStorageBackendByID(editing.value.id) : await testStorageBackend(form) - if (r.success) { rawTestResult.value = 'ok'; MessagePlugin.success('连接成功') } - else { rawTestResult.value = 'error'; MessagePlugin.error(r.error || '连接失败') } + if (r.success) { rawTestResult.value = 'ok'; MessagePlugin.success(t('settings.storageBackend.testSuccess')) } + else { rawTestResult.value = 'error'; MessagePlugin.error(r.error || t('settings.storageBackend.testFailed')) } } finally { testing.value = false } } -async function testSaved(backend: StorageBackend) { const r: any = await testStorageBackendByID(backend.id); r.success ? MessagePlugin.success('连接成功') : MessagePlugin.error(r.error || '连接失败') } +async function testSaved(backend: StorageBackend) { const r: any = await testStorageBackendByID(backend.id); r.success ? MessagePlugin.success(t('settings.storageBackend.testSuccess')) : MessagePlugin.error(r.error || t('settings.storageBackend.testFailed')) } async function save() { - if (!form.name.trim()) { MessagePlugin.warning('请输入名称'); return } + if (!form.name.trim()) { MessagePlugin.warning(t('settings.storageBackend.nameRequired')); return } saving.value = true try { const payload = { name: form.name.trim(), provider: form.provider, config: { ...form.config } } if (editing.value) await updateStorageBackend(editing.value.id, payload); else await createStorageBackend(payload) - MessagePlugin.success('保存成功'); visible.value = false; await load() - } catch (e: any) { MessagePlugin.error(e?.message || '保存失败') } finally { saving.value = false } + MessagePlugin.success(t('settings.storageBackend.saveSuccess')); visible.value = false; await load() + } catch (e: any) { MessagePlugin.error(e?.message || t('settings.storageBackend.saveFailed')) } finally { saving.value = false } } -async function makeDefault(backend: StorageBackend) { await setDefaultStorageBackend(backend.id); defaultID.value = backend.id; MessagePlugin.success('默认存储已更新') } +async function makeDefault(backend: StorageBackend) { await setDefaultStorageBackend(backend.id); defaultID.value = backend.id; MessagePlugin.success(t('settings.storageBackend.defaultUpdated')) } function remove(backend: StorageBackend) { - const dialog = DialogPlugin.confirm({ header: '删除存储实例', body: `确定删除“${backend.name}”吗?`, onConfirm: async () => { dialog.destroy(); try { await deleteStorageBackend(backend.id); await load(); MessagePlugin.success('已删除') } catch (e: any) { MessagePlugin.error(e?.message || '删除失败') } }, onCancel: () => dialog.destroy() }) + const dialog = DialogPlugin.confirm({ header: t('settings.storageBackend.deleteTitle'), body: t('settings.storageBackend.deleteConfirm', { name: backend.name }), onConfirm: async () => { dialog.destroy(); try { await deleteStorageBackend(backend.id); await load(); MessagePlugin.success(t('settings.storageBackend.deleted')) } catch (e: any) { MessagePlugin.error(e?.message || t('settings.storageBackend.deleteFailed')) } }, onCancel: () => dialog.destroy() }) } onMounted(load) diff --git a/internal/handler/storagebackend.go b/internal/handler/storagebackend.go index eecd0f22..0df782d6 100644 --- a/internal/handler/storagebackend.go +++ b/internal/handler/storagebackend.go @@ -41,6 +41,16 @@ type storageBackendRequest struct { func storageTenantID(c *gin.Context) uint64 { return c.GetUint64(types.TenantIDContextKey.String()) } +// List godoc +// @Summary List storage backends +// @Description List all storage backend instances for the current workspace, with credentials masked. The workspace default backend id is returned alongside the list. +// @Tags StorageBackend +// @Produce json +// @Success 200 {object} map[string]interface{} "List of storage backends and default_storage_backend_id" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends [get] func (h *StorageBackendHandler) List(c *gin.Context) { tenantID := storageTenantID(c) backends, err := h.repo.List(c.Request.Context(), tenantID) @@ -60,6 +70,18 @@ func (h *StorageBackendHandler) List(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": result, "default_storage_backend_id": defaultID}) } +// Get godoc +// @Summary Get storage backend +// @Description Retrieve a single storage backend by ID for the current workspace. Credentials are masked. +// @Tags StorageBackend +// @Produce json +// @Param id path string true "Storage backend ID" +// @Success 200 {object} map[string]interface{} "Storage backend details" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 404 {object} apperrors.AppError "Storage backend not found" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/{id} [get] func (h *StorageBackendHandler) Get(c *gin.Context) { backend, err := h.repo.GetByID(c.Request.Context(), storageTenantID(c), c.Param("id")) if err != nil { @@ -73,6 +95,20 @@ func (h *StorageBackendHandler) Get(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": types.NewStorageBackendResponse(backend)}) } +// Create godoc +// @Summary Create storage backend +// @Description Register a new object/file storage instance for the current workspace. The configuration is validated and a connectivity test is run before the backend is persisted. +// @Tags StorageBackend +// @Accept json +// @Produce json +// @Param request body storageBackendRequest true "Storage backend configuration" +// @Success 201 {object} map[string]interface{} "Created storage backend" +// @Failure 400 {object} apperrors.AppError "Invalid request, validation, or connectivity test failure" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 409 {object} apperrors.AppError "A storage backend with this name already exists" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends [post] func (h *StorageBackendHandler) Create(c *gin.Context) { var req storageBackendRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -87,6 +123,21 @@ func (h *StorageBackendHandler) Create(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"success": true, "data": types.NewStorageBackendResponse(backend)}) } +// Update godoc +// @Summary Update storage backend +// @Description Update a storage backend's mutable fields (name, credentials, status). Provider and physical location (endpoint, region, bucket, path prefix) are immutable; use storage migration to move data. Environment-sourced backends are read-only. Redacted secret placeholders preserve the stored credentials. +// @Tags StorageBackend +// @Accept json +// @Produce json +// @Param id path string true "Storage backend ID" +// @Param request body storageBackendRequest true "Updated storage backend fields" +// @Success 200 {object} map[string]interface{} "Updated storage backend" +// @Failure 400 {object} apperrors.AppError "Immutable field change, read-only backend, validation, or connectivity failure" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 404 {object} apperrors.AppError "Storage backend not found" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/{id} [put] func (h *StorageBackendHandler) Update(c *gin.Context) { var req storageBackendRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -106,6 +157,19 @@ func (h *StorageBackendHandler) Update(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": types.NewStorageBackendResponse(updated)}) } +// Delete godoc +// @Summary Delete storage backend +// @Description Soft-delete a storage backend. A backend that is the workspace default, still bound to knowledge bases, environment-sourced, or a legacy alias cannot be deleted. +// @Tags StorageBackend +// @Produce json +// @Param id path string true "Storage backend ID" +// @Success 200 {object} map[string]interface{} "Deletion success" +// @Failure 400 {object} apperrors.AppError "Backend is default, bound, read-only, or legacy alias" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 404 {object} apperrors.AppError "Storage backend not found" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/{id} [delete] func (h *StorageBackendHandler) Delete(c *gin.Context) { if err := h.service.Delete(c.Request.Context(), storageTenantID(c), c.Param("id")); err != nil { c.Error(err) @@ -114,6 +178,19 @@ func (h *StorageBackendHandler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +// SetDefault godoc +// @Summary Set default storage backend +// @Description Mark a storage backend as the workspace default. Only an active backend can become the default. New knowledge bases without an explicit binding use the default. +// @Tags StorageBackend +// @Produce json +// @Param id path string true "Storage backend ID" +// @Success 200 {object} map[string]interface{} "Default set successfully" +// @Failure 400 {object} apperrors.AppError "Backend is not active" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 404 {object} apperrors.AppError "Storage backend not found" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/{id}/default [put] func (h *StorageBackendHandler) SetDefault(c *gin.Context) { if err := h.service.SetDefault(c.Request.Context(), storageTenantID(c), c.Param("id")); err != nil { c.Error(err) @@ -122,6 +199,19 @@ func (h *StorageBackendHandler) SetDefault(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +// TestRaw godoc +// @Summary Test storage backend with raw config +// @Description Test connectivity for the provided storage configuration without persisting it. Returns success=false with a sanitized error message on failure (the HTTP status stays 200). +// @Tags StorageBackend +// @Accept json +// @Produce json +// @Param request body storageBackendRequest true "Storage backend configuration to test" +// @Success 200 {object} map[string]interface{} "Connectivity test result (success, error)" +// @Failure 400 {object} apperrors.AppError "Invalid request or validation error" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/test [post] func (h *StorageBackendHandler) TestRaw(c *gin.Context) { var req storageBackendRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -140,6 +230,18 @@ func (h *StorageBackendHandler) TestRaw(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +// TestByID godoc +// @Summary Test storage backend by ID +// @Description Test connectivity of an existing saved storage backend using its stored credentials. Returns success=false with a sanitized error message on failure (the HTTP status stays 200). +// @Tags StorageBackend +// @Produce json +// @Param id path string true "Storage backend ID" +// @Success 200 {object} map[string]interface{} "Connectivity test result (success, error)" +// @Failure 401 {object} map[string]interface{} "Unauthorized" +// @Failure 404 {object} apperrors.AppError "Storage backend not found" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/{id}/test [post] func (h *StorageBackendHandler) TestByID(c *gin.Context) { backend, err := h.repo.GetByID(c.Request.Context(), storageTenantID(c), c.Param("id")) if err != nil { @@ -157,6 +259,15 @@ func (h *StorageBackendHandler) TestByID(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +// Types godoc +// @Summary List allowed storage provider types +// @Description Return the storage provider types allowed by STORAGE_ALLOW_LIST for UI form generation (e.g. local, minio, cos, tos, s3, oss, ks3, obs). +// @Tags StorageBackend +// @Produce json +// @Success 200 {object} map[string]interface{} "List of allowed storage provider types" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /storage-backends/types [get] func (h *StorageBackendHandler) Types(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "data": storageallowlist.AllowedList()}) }