mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-08-31 00:50:02 +08:00
fix: filter menus by deployment capabilities (#2674)
* fix: filter menus by deployment capabilities * fix: address deployment capabilities review feedback Restore Lite organizations hiding when the capabilities probe fails, align embed capability detection with route registration, move the handler into SystemHandler for Swagger/docs/client sync, and add cross-layer capability key contract tests. * style: gofmt deployment capabilities files Fix App CI formatting check so the vet/test step can run. --------- Co-authored-by: wizardchen <wizardchen@tencent.com>
This commit is contained in:
@@ -63,6 +63,18 @@ type StorageCheckResponse struct {
|
||||
BucketCreated bool `json:"bucket_created,omitempty"`
|
||||
}
|
||||
|
||||
// DeploymentCapability describes whether a deployment exposes a feature route.
|
||||
type DeploymentCapability struct {
|
||||
Supported bool `json:"supported"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// DeploymentCapabilitiesData is the payload of GET /system/capabilities.
|
||||
type DeploymentCapabilitiesData struct {
|
||||
Edition string `json:"edition"`
|
||||
Capabilities map[string]DeploymentCapability `json:"capabilities"`
|
||||
}
|
||||
|
||||
// GetSystemInfo gets system version and configuration information
|
||||
func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/system/info", nil, nil)
|
||||
@@ -79,6 +91,22 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetDeploymentCapabilities returns the deployment feature snapshot for SPA menu gating.
|
||||
func (c *Client) GetDeploymentCapabilities(ctx context.Context) (*DeploymentCapabilitiesData, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/system/capabilities", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Data *DeploymentCapabilitiesData `json:"data"`
|
||||
}
|
||||
if err := parseResponse(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// ListParserEngines lists available document parser engines
|
||||
func (c *Client) ListParserEngines(ctx context.Context) ([]ParserEngine, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/system/parser-engines", nil, nil)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | --------------------------------- | ---------------------- |
|
||||
| GET | `/system/capabilities` | 获取部署能力清单 |
|
||||
| GET | `/system/info` | 获取系统信息 |
|
||||
| GET | `/system/parser-engines` | 获取解析引擎列表 |
|
||||
| POST | `/system/parser-engines/check` | 检查解析引擎可用性 |
|
||||
@@ -11,6 +12,57 @@
|
||||
| GET | `/system/storage-engine-status` | 获取存储引擎状态 |
|
||||
| POST | `/system/storage-engine-check` | 检查存储引擎连通性 |
|
||||
|
||||
## GET `/system/capabilities` - 获取部署能力清单
|
||||
|
||||
返回当前部署版本,以及各功能模块是否已在后端注册对应路由。`supported: false` 表示 SPA 应隐藏相关入口;字段缺失或接口不可用时不应据此清空整个菜单(fail-open),但 Lite 版会始终将 `organizations` 标记为不支持。
|
||||
|
||||
**权限**:Viewer+(租户成员);任意有效 API Key 可读(`apiKeyAny`)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/capabilities' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"edition": "standard",
|
||||
"capabilities": {
|
||||
"organizations": { "supported": true },
|
||||
"agents": { "supported": true },
|
||||
"integrations.im": { "supported": true },
|
||||
"integrations.embed": { "supported": false, "reason": "route_not_registered" },
|
||||
"integrations.api": { "supported": true },
|
||||
"settings.mcp": { "supported": true },
|
||||
"settings.websearch": { "supported": true },
|
||||
"settings.vectorstore": { "supported": true },
|
||||
"settings.storage": { "supported": true },
|
||||
"settings.sandbox": { "supported": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Lite 版示例(共享空间不可用):
|
||||
|
||||
```json
|
||||
{
|
||||
"capabilities": {
|
||||
"organizations": {
|
||||
"supported": false,
|
||||
"reason": "not_supported_in_lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/system/info` - 获取系统信息
|
||||
|
||||
**请求**:
|
||||
|
||||
+706
@@ -1161,6 +1161,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/start": {
|
||||
"get": {
|
||||
"description": "与 /auth/oidc/url 不同,此端点直接 302 重定向到 OIDC Provider 的授权页,\n无需前端 JS 介入。适用于外部平台(如企业门户)直接给出一个链接即可\n触发 OIDC 授权码流程,借助 IdP 的 SSO session 实现免再次输密码。",
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "发起 OIDC 登录(直接 302)",
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/url": {
|
||||
"get": {
|
||||
"description": "根据后端OIDC配置生成第三方登录跳转地址",
|
||||
@@ -10605,6 +10619,329 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "List workspace sandbox backend configs with credentials masked.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "List sandbox configs",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox configs and defaults",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Create a named workspace sandbox backend config. Credentials are masked in the response.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Create sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sandbox backend config",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.sandboxConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve a workspace sandbox backend config with credentials masked.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Get sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Sandbox config not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update a sandbox backend config. Identity-field changes are refused while the config owns live or paused sandboxes.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Update sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Updated sandbox config",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.sandboxConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Sandbox config not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Live sandboxes or unverifiable inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"423": {
|
||||
"description": "Sandbox config is being modified by another request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Soft-delete a sandbox backend config. force=true only overrides unverifiable provider inventory, never confirmed live sandboxes.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Delete sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Force delete when inventory is unverifiable",
|
||||
"name": "force",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deletion success",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Live sandboxes or unverifiable inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs/{id}/sandboxes": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Return live/paused sandbox inventory and affected agent names for one config.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Inspect sandbox config inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -11139,6 +11476,57 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/artifacts": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "返回本会话中所有 assistant 消息产生的技能产物元数据(不含 URL)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"会话"
|
||||
],
|
||||
"summary": "列出会话生成的产物文件",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "会话ID",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/artifacts": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/artifacts/{index}/download": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/suggestions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -12696,6 +13084,27 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/capabilities": {
|
||||
"get": {
|
||||
"description": "返回当前部署版本及实际注册的后端路由所对应的功能能力;仅 supported=false 表示入口应隐藏",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "获取部署能力清单",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "标准 code/msg/data 包装,data 为 DeploymentCapabilitiesData",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/docreader/reconnect": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -12799,6 +13208,40 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/sandbox-check": {
|
||||
"post": {
|
||||
"description": "使用当前填写的参数测试沙箱后端,不保存配置;deep=true 会执行临时脚本,远端后端还会创建并销毁一个沙箱",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "测试沙箱连通性",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "沙箱配置",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/storage-engine-check": {
|
||||
"post": {
|
||||
"description": "使用当前填写的参数测试 MinIO/COS 连通性,不保存配置",
|
||||
@@ -15763,6 +16206,34 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"description": "加密",
|
||||
"type": "string"
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"cube_sandbox_ttl_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"http_timeout_sec": {
|
||||
"description": "HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.\n0 means use the built-in default (30s), never the deployment's value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"proxy_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.CustomAgentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15969,6 +16440,10 @@ const docTemplate = `{
|
||||
"description": "Rewrite prompt user message template",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_config_id": {
|
||||
"description": "===== Sandbox Settings =====\nSandboxConfigID selects which workspace sandbox config this agent's\nskill scripts run on. Empty means sandbox execution is disabled.\n\nThis references the LOGICAL config, never a specific revision: keeping\nthe indirection here is what would let credential rotation happen\nwithout re-pointing every agent (see the spec's §4.8).",
|
||||
"type": "string"
|
||||
},
|
||||
"selected_skills": {
|
||||
"description": "Selected skill names (only used when SkillsSelectionMode is \"selected\")",
|
||||
"type": "array",
|
||||
@@ -16139,6 +16614,43 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"description": "加密",
|
||||
"type": "string"
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"e2b_sandbox_ttl_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"http_timeout_sec": {
|
||||
"description": "HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.\n0 means use the built-in default (30s), never the deployment's value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"proxy_url": {
|
||||
"description": "ProxyURL is the data-plane gateway that fronts envd. E2B Cloud resolves\n\"\u003cport\u003e-\u003csandboxID\u003e.\u003csandbox_domain\u003e\" through public DNS and TLS, so it\nneeds no value here. Self-hosted E2B-compatible control planes usually\nserve every sandbox from one gateway address and expect the sandbox\nauthority in the Host header; setting this makes WeKnora dial the\ngateway directly instead of requiring wildcard DNS and a certificate\nfor the sandbox domain. An \"http://\" gateway also downgrades the\ndata-plane scheme, which the E2B SDK otherwise pins to https.",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.EmbeddingParameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17659,6 +18171,13 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.AgentStep"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"description": "Skill-generated files produced during this assistant turn (assistant messages only).\nPopulated by ArtifactCollector after the sandbox finishes, referenced by the\nartifact download endpoint. Empty for user messages and turns without skills.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MessageArtifact"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"description": "Attached files (documents, audio, etc., for user messages)",
|
||||
"type": "array",
|
||||
@@ -17745,6 +18264,39 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MessageArtifact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "When WeKnora persisted the blob",
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"description": "Original filename inside the sandbox",
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"description": "File size in bytes",
|
||||
"type": "integer"
|
||||
},
|
||||
"file_type": {
|
||||
"description": "File extension (e.g., \".pptx\", \".pdf\")",
|
||||
"type": "string"
|
||||
},
|
||||
"mod_time": {
|
||||
"description": "Sandbox-side modification time (used for diff)",
|
||||
"type": "string"
|
||||
},
|
||||
"source_path": {
|
||||
"description": "Absolute path inside the sandbox (used for diff)",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Storage URL (provider://path); persisted, not sent to client",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MessageAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19050,6 +19602,10 @@ const docTemplate = `{
|
||||
"description": "PinnedAt records when the session was pinned; nil when not pinned.",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_config_id": {
|
||||
"description": "SandboxConfigID pins which sandbox config this session's CURRENT live\nsandbox was created on. Empty means no live sandbox;\nSandboxConfigIDGlobalDefault means the deployment-wide default config.\n\nThis is an ephemeral pin that dies with the sandbox, not a permanent\nowner: sessions outlive sandboxes by months, so treating it as\npermanent would make \"no session references this config\" never true.",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Workspace ID",
|
||||
"type": "integer"
|
||||
@@ -19611,6 +20167,47 @@ const docTemplate = `{
|
||||
"TenantRoleViewer"
|
||||
]
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_private_endpoints": {
|
||||
"description": "AllowPrivateEndpoints permits this workspace config to reach RFC1918 or\nloopback cluster endpoints. Link-local/cloud-metadata addresses remain\nblocked. It is explicit in the UI instead of hidden in process env.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cube": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig"
|
||||
},
|
||||
"default_timeout_sec": {
|
||||
"description": "DefaultTimeoutSec is the per-execution timeout in seconds. 0 uses the\nprogram's built-in default.",
|
||||
"type": "integer"
|
||||
},
|
||||
"docker": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig"
|
||||
},
|
||||
"e2b": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig"
|
||||
},
|
||||
"env_vars": {
|
||||
"description": "EnvVars are additional environment variables injected into every\nsandbox created for this tenant. 🔒 Values are encrypted at rest.\nThese become visible to all scripts running in the tenant's\nsandboxes — do not place secrets here that scripts must not access.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sandbox_type": {
|
||||
"description": "SandboxType selects the sandbox backend. Named configs may use \"cube\",\n\"e2b\", \"docker\", or \"local\". \"disabled\" is reserved for the hidden\nworkspace policy row.",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_mount": {
|
||||
"description": "VolumeMount configures an optional shared volume mounted into every\nsandbox created for this tenant. Currently used for tenant-installed\nskills, but the configuration itself is skill-agnostic and can serve\nany volume-mount use case (shared datasets, pre-installed toolchains,\netc.).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.VolumeMountConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.ToolCall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19902,6 +20499,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.VolumeMountConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enabled toggles the volume mount for this tenant.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"mount_path": {
|
||||
"description": "MountPath is the sandbox-internal path where the volume is mounted.\nDefault: /weknora/tenant/skills (customizable per use case).",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"description": "Provider identifies the volume backend. Currently \"e2b\" or \"cube\".",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_id": {
|
||||
"description": "VolumeID is the provider-specific volume identifier, populated after\nEnsureVolume / CreateVolume succeeds.",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_name": {
|
||||
"description": "VolumeName is the human-readable volume name, e.g.\n\"weknora-tenant-\u003cid\u003e-skills\".",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_owner_fingerprint": {
|
||||
"description": "VolumeOwnerFingerprint = sha256(provider + APIKey + APIURL).\nUsed to detect when the tenant switched to a different backend or\nAPI key, at which point the volume is no longer reachable and must\nbe recreated.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.WeKnoraCloudCredentials": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -20056,6 +20682,7 @@ const docTemplate = `{
|
||||
"searxng",
|
||||
"keenable",
|
||||
"zhipu",
|
||||
"exa",
|
||||
"metaso"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
@@ -20068,6 +20695,7 @@ const docTemplate = `{
|
||||
"WebSearchProviderTypeSearxng",
|
||||
"WebSearchProviderTypeKeenable",
|
||||
"WebSearchProviderTypeZhipu",
|
||||
"WebSearchProviderTypeExa",
|
||||
"WebSearchProviderTypeMetaso"
|
||||
]
|
||||
},
|
||||
@@ -21863,6 +22491,67 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"latency_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"description": "Message carries free-form provider detail for an executed probe.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason is a stable code explaining why a probe was skipped. It exists so\nthe UI can phrase the skip in the operator's language instead of echoing\na server-side sentence.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig"
|
||||
},
|
||||
"config_id": {
|
||||
"description": "ConfigID lets an edit form test stored credentials while overriding only\nthe fields the admin changed in the drawer.",
|
||||
"type": "string"
|
||||
},
|
||||
"deep": {
|
||||
"description": "Deep additionally runs a throwaway script. For remote backends this also\ncreates and destroys one sandbox, which is the only way to validate the\ntemplate ID, data plane, in-sandbox execution, and outbound egress. It may\nconsume real sandbox time, so it is opt-in.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckItem"
|
||||
}
|
||||
},
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SearchMessagesRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -22369,6 +23058,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.sandboxConfigRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.storageBackendRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -1154,6 +1154,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/start": {
|
||||
"get": {
|
||||
"description": "与 /auth/oidc/url 不同,此端点直接 302 重定向到 OIDC Provider 的授权页,\n无需前端 JS 介入。适用于外部平台(如企业门户)直接给出一个链接即可\n触发 OIDC 授权码流程,借助 IdP 的 SSO session 实现免再次输密码。",
|
||||
"tags": [
|
||||
"认证"
|
||||
],
|
||||
"summary": "发起 OIDC 登录(直接 302)",
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/oidc/url": {
|
||||
"get": {
|
||||
"description": "根据后端OIDC配置生成第三方登录跳转地址",
|
||||
@@ -10598,6 +10612,329 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "List workspace sandbox backend configs with credentials masked.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "List sandbox configs",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox configs and defaults",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Create a named workspace sandbox backend config. Credentials are masked in the response.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Create sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Sandbox backend config",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.sandboxConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Retrieve a workspace sandbox backend config with credentials masked.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Get sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Sandbox config not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Update a sandbox backend config. Identity-field changes are refused while the config owns live or paused sandboxes.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Update sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Updated sandbox config",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.sandboxConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated sandbox config",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or validation failure",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Sandbox config not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Live sandboxes or unverifiable inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"423": {
|
||||
"description": "Sandbox config is being modified by another request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Soft-delete a sandbox backend config. force=true only overrides unverifiable provider inventory, never confirmed live sandboxes.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Delete sandbox config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Force delete when inventory is unverifiable",
|
||||
"name": "force",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deletion success",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Live sandboxes or unverifiable inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sandbox-configs/{id}/sandboxes": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Return live/paused sandbox inventory and affected agent names for one config.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"SandboxConfig"
|
||||
],
|
||||
"summary": "Inspect sandbox config inventory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sandbox config ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sandbox inventory",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -11132,6 +11469,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/artifacts": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "返回本会话中所有 assistant 消息产生的技能产物元数据(不含 URL)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"会话"
|
||||
],
|
||||
"summary": "列出会话生成的产物文件",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "会话ID",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/artifacts": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/artifacts/{index}/download": {
|
||||
"get": {
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/sessions/{session_id}/messages/{message_id}/suggestions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -12689,6 +13077,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/capabilities": {
|
||||
"get": {
|
||||
"description": "返回当前部署版本及实际注册的后端路由所对应的功能能力;仅 supported=false 表示入口应隐藏",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "获取部署能力清单",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "标准 code/msg/data 包装,data 为 DeploymentCapabilitiesData",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/docreader/reconnect": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -12792,6 +13201,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/sandbox-check": {
|
||||
"post": {
|
||||
"description": "使用当前填写的参数测试沙箱后端,不保存配置;deep=true 会执行临时脚本,远端后端还会创建并销毁一个沙箱",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"系统"
|
||||
],
|
||||
"summary": "测试沙箱连通性",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "沙箱配置",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/system/storage-engine-check": {
|
||||
"post": {
|
||||
"description": "使用当前填写的参数测试 MinIO/COS 连通性,不保存配置",
|
||||
@@ -15756,6 +16199,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"description": "加密",
|
||||
"type": "string"
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"cube_sandbox_ttl_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"http_timeout_sec": {
|
||||
"description": "HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.\n0 means use the built-in default (30s), never the deployment's value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"proxy_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.CustomAgentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15962,6 +16433,10 @@
|
||||
"description": "Rewrite prompt user message template",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_config_id": {
|
||||
"description": "===== Sandbox Settings =====\nSandboxConfigID selects which workspace sandbox config this agent's\nskill scripts run on. Empty means sandbox execution is disabled.\n\nThis references the LOGICAL config, never a specific revision: keeping\nthe indirection here is what would let credential rotation happen\nwithout re-pointing every agent (see the spec's §4.8).",
|
||||
"type": "string"
|
||||
},
|
||||
"selected_skills": {
|
||||
"description": "Selected skill names (only used when SkillsSelectionMode is \"selected\")",
|
||||
"type": "array",
|
||||
@@ -16132,6 +16607,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"description": "加密",
|
||||
"type": "string"
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"e2b_sandbox_ttl_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"http_timeout_sec": {
|
||||
"description": "HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.\n0 means use the built-in default (30s), never the deployment's value.",
|
||||
"type": "integer"
|
||||
},
|
||||
"proxy_url": {
|
||||
"description": "ProxyURL is the data-plane gateway that fronts envd. E2B Cloud resolves\n\"\u003cport\u003e-\u003csandboxID\u003e.\u003csandbox_domain\u003e\" through public DNS and TLS, so it\nneeds no value here. Self-hosted E2B-compatible control planes usually\nserve every sandbox from one gateway address and expect the sandbox\nauthority in the Host header; setting this makes WeKnora dial the\ngateway directly instead of requiring wildcard DNS and a certificate\nfor the sandbox domain. An \"http://\" gateway also downgrades the\ndata-plane scheme, which the E2B SDK otherwise pins to https.",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.EmbeddingParameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17652,6 +18164,13 @@
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.AgentStep"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"description": "Skill-generated files produced during this assistant turn (assistant messages only).\nPopulated by ArtifactCollector after the sandbox finishes, referenced by the\nartifact download endpoint. Empty for user messages and turns without skills.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MessageArtifact"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"description": "Attached files (documents, audio, etc., for user messages)",
|
||||
"type": "array",
|
||||
@@ -17738,6 +18257,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MessageArtifact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"description": "When WeKnora persisted the blob",
|
||||
"type": "string"
|
||||
},
|
||||
"file_name": {
|
||||
"description": "Original filename inside the sandbox",
|
||||
"type": "string"
|
||||
},
|
||||
"file_size": {
|
||||
"description": "File size in bytes",
|
||||
"type": "integer"
|
||||
},
|
||||
"file_type": {
|
||||
"description": "File extension (e.g., \".pptx\", \".pdf\")",
|
||||
"type": "string"
|
||||
},
|
||||
"mod_time": {
|
||||
"description": "Sandbox-side modification time (used for diff)",
|
||||
"type": "string"
|
||||
},
|
||||
"source_path": {
|
||||
"description": "Absolute path inside the sandbox (used for diff)",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "Storage URL (provider://path); persisted, not sent to client",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MessageAttachment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19043,6 +19595,10 @@
|
||||
"description": "PinnedAt records when the session was pinned; nil when not pinned.",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_config_id": {
|
||||
"description": "SandboxConfigID pins which sandbox config this session's CURRENT live\nsandbox was created on. Empty means no live sandbox;\nSandboxConfigIDGlobalDefault means the deployment-wide default config.\n\nThis is an ephemeral pin that dies with the sandbox, not a permanent\nowner: sessions outlive sandboxes by months, so treating it as\npermanent would make \"no session references this config\" never true.",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Workspace ID",
|
||||
"type": "integer"
|
||||
@@ -19604,6 +20160,47 @@
|
||||
"TenantRoleViewer"
|
||||
]
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow_private_endpoints": {
|
||||
"description": "AllowPrivateEndpoints permits this workspace config to reach RFC1918 or\nloopback cluster endpoints. Link-local/cloud-metadata addresses remain\nblocked. It is explicit in the UI instead of hidden in process env.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cube": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig"
|
||||
},
|
||||
"default_timeout_sec": {
|
||||
"description": "DefaultTimeoutSec is the per-execution timeout in seconds. 0 uses the\nprogram's built-in default.",
|
||||
"type": "integer"
|
||||
},
|
||||
"docker": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig"
|
||||
},
|
||||
"e2b": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig"
|
||||
},
|
||||
"env_vars": {
|
||||
"description": "EnvVars are additional environment variables injected into every\nsandbox created for this tenant. 🔒 Values are encrypted at rest.\nThese become visible to all scripts running in the tenant's\nsandboxes — do not place secrets here that scripts must not access.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sandbox_type": {
|
||||
"description": "SandboxType selects the sandbox backend. Named configs may use \"cube\",\n\"e2b\", \"docker\", or \"local\". \"disabled\" is reserved for the hidden\nworkspace policy row.",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_mount": {
|
||||
"description": "VolumeMount configures an optional shared volume mounted into every\nsandbox created for this tenant. Currently used for tenant-installed\nskills, but the configuration itself is skill-agnostic and can serve\nany volume-mount use case (shared datasets, pre-installed toolchains,\netc.).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.VolumeMountConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.ToolCall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19895,6 +20492,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.VolumeMountConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enabled toggles the volume mount for this tenant.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"mount_path": {
|
||||
"description": "MountPath is the sandbox-internal path where the volume is mounted.\nDefault: /weknora/tenant/skills (customizable per use case).",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"description": "Provider identifies the volume backend. Currently \"e2b\" or \"cube\".",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_id": {
|
||||
"description": "VolumeID is the provider-specific volume identifier, populated after\nEnsureVolume / CreateVolume succeeds.",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_name": {
|
||||
"description": "VolumeName is the human-readable volume name, e.g.\n\"weknora-tenant-\u003cid\u003e-skills\".",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_owner_fingerprint": {
|
||||
"description": "VolumeOwnerFingerprint = sha256(provider + APIKey + APIURL).\nUsed to detect when the tenant switched to a different backend or\nAPI key, at which point the volume is no longer reachable and must\nbe recreated.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.WeKnoraCloudCredentials": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -20049,6 +20675,7 @@
|
||||
"searxng",
|
||||
"keenable",
|
||||
"zhipu",
|
||||
"exa",
|
||||
"metaso"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
@@ -20061,6 +20688,7 @@
|
||||
"WebSearchProviderTypeSearxng",
|
||||
"WebSearchProviderTypeKeenable",
|
||||
"WebSearchProviderTypeZhipu",
|
||||
"WebSearchProviderTypeExa",
|
||||
"WebSearchProviderTypeMetaso"
|
||||
]
|
||||
},
|
||||
@@ -21856,6 +22484,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"latency_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"description": "Message carries free-form provider detail for an executed probe.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason is a stable code explaining why a probe was skipped. It exists so\nthe UI can phrase the skip in the operator's language instead of echoing\na server-side sentence.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig"
|
||||
},
|
||||
"config_id": {
|
||||
"description": "ConfigID lets an edit form test stored credentials while overriding only\nthe fields the admin changed in the drawer.",
|
||||
"type": "string"
|
||||
},
|
||||
"deep": {
|
||||
"description": "Deep additionally runs a throwaway script. For remote backends this also\ncreates and destroys one sandbox, which is the only way to validate the\ntemplate ID, data plane, in-sandbox execution, and outbound egress. It may\nconsume real sandbox time, so it is opt-in.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SandboxCheckResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/internal_handler.SandboxCheckItem"
|
||||
}
|
||||
},
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.SearchMessagesRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -22362,6 +23051,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.sandboxConfigRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.storageBackendRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -672,6 +672,27 @@ definitions:
|
||||
weknoracloud:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.WeKnoraCloudCredentials'
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig:
|
||||
properties:
|
||||
api_key:
|
||||
description: 加密
|
||||
type: string
|
||||
api_url:
|
||||
type: string
|
||||
cube_sandbox_ttl_seconds:
|
||||
type: integer
|
||||
http_timeout_sec:
|
||||
description: |-
|
||||
HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.
|
||||
0 means use the built-in default (30s), never the deployment's value.
|
||||
type: integer
|
||||
proxy_url:
|
||||
type: string
|
||||
sandbox_domain:
|
||||
type: string
|
||||
template_id:
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.CustomAgentConfig:
|
||||
properties:
|
||||
agent_mode:
|
||||
@@ -895,6 +916,16 @@ definitions:
|
||||
rewrite_prompt_user:
|
||||
description: Rewrite prompt user message template
|
||||
type: string
|
||||
sandbox_config_id:
|
||||
description: |-
|
||||
===== Sandbox Settings =====
|
||||
SandboxConfigID selects which workspace sandbox config this agent's
|
||||
skill scripts run on. Empty means sandbox execution is disabled.
|
||||
|
||||
This references the LOGICAL config, never a specific revision: keeping
|
||||
the indirection here is what would let credential rotation happen
|
||||
without re-pointing every agent (see the spec's §4.8).
|
||||
type: string
|
||||
selected_skills:
|
||||
description: Selected skill names (only used when SkillsSelectionMode is "selected")
|
||||
items:
|
||||
@@ -1035,6 +1066,41 @@ definitions:
|
||||
description: Last update timestamp
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig:
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig:
|
||||
properties:
|
||||
api_key:
|
||||
description: 加密
|
||||
type: string
|
||||
api_url:
|
||||
type: string
|
||||
e2b_sandbox_ttl_seconds:
|
||||
type: integer
|
||||
http_timeout_sec:
|
||||
description: |-
|
||||
HTTPTimeoutSec bounds each HTTP call to the sandbox control plane.
|
||||
0 means use the built-in default (30s), never the deployment's value.
|
||||
type: integer
|
||||
proxy_url:
|
||||
description: |-
|
||||
ProxyURL is the data-plane gateway that fronts envd. E2B Cloud resolves
|
||||
"<port>-<sandboxID>.<sandbox_domain>" through public DNS and TLS, so it
|
||||
needs no value here. Self-hosted E2B-compatible control planes usually
|
||||
serve every sandbox from one gateway address and expect the sandbox
|
||||
authority in the Host header; setting this makes WeKnora dial the
|
||||
gateway directly instead of requiring wildcard DNS and a certificate
|
||||
for the sandbox domain. An "http://" gateway also downgrades the
|
||||
data-plane scheme, which the E2B SDK otherwise pins to https.
|
||||
type: string
|
||||
sandbox_domain:
|
||||
type: string
|
||||
template_id:
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.EmbeddingParameters:
|
||||
properties:
|
||||
dimension:
|
||||
@@ -2160,6 +2226,14 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.AgentStep'
|
||||
type: array
|
||||
artifacts:
|
||||
description: |-
|
||||
Skill-generated files produced during this assistant turn (assistant messages only).
|
||||
Populated by ArtifactCollector after the sandbox finishes, referenced by the
|
||||
artifact download endpoint. Empty for user messages and turns without skills.
|
||||
items:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.MessageArtifact'
|
||||
type: array
|
||||
attachments:
|
||||
description: Attached files (documents, audio, etc., for user messages)
|
||||
items:
|
||||
@@ -2229,6 +2303,30 @@ definitions:
|
||||
description: Last update timestamp
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.MessageArtifact:
|
||||
properties:
|
||||
created_at:
|
||||
description: When WeKnora persisted the blob
|
||||
type: string
|
||||
file_name:
|
||||
description: Original filename inside the sandbox
|
||||
type: string
|
||||
file_size:
|
||||
description: File size in bytes
|
||||
type: integer
|
||||
file_type:
|
||||
description: File extension (e.g., ".pptx", ".pdf")
|
||||
type: string
|
||||
mod_time:
|
||||
description: Sandbox-side modification time (used for diff)
|
||||
type: string
|
||||
source_path:
|
||||
description: Absolute path inside the sandbox (used for diff)
|
||||
type: string
|
||||
url:
|
||||
description: Storage URL (provider://path); persisted, not sent to client
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.MessageAttachment:
|
||||
properties:
|
||||
content:
|
||||
@@ -3227,6 +3325,16 @@ definitions:
|
||||
pinned_at:
|
||||
description: PinnedAt records when the session was pinned; nil when not pinned.
|
||||
type: string
|
||||
sandbox_config_id:
|
||||
description: |-
|
||||
SandboxConfigID pins which sandbox config this session's CURRENT live
|
||||
sandbox was created on. Empty means no live sandbox;
|
||||
SandboxConfigIDGlobalDefault means the deployment-wide default config.
|
||||
|
||||
This is an ephemeral pin that dies with the sandbox, not a permanent
|
||||
owner: sessions outlive sandboxes by months, so treating it as
|
||||
permanent would make "no session references this config" never true.
|
||||
type: string
|
||||
tenant_id:
|
||||
description: Workspace ID
|
||||
type: integer
|
||||
@@ -3634,6 +3742,49 @@ definitions:
|
||||
- TenantRoleAdmin
|
||||
- TenantRoleContributor
|
||||
- TenantRoleViewer
|
||||
github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig:
|
||||
properties:
|
||||
allow_private_endpoints:
|
||||
description: |-
|
||||
AllowPrivateEndpoints permits this workspace config to reach RFC1918 or
|
||||
loopback cluster endpoints. Link-local/cloud-metadata addresses remain
|
||||
blocked. It is explicit in the UI instead of hidden in process env.
|
||||
type: boolean
|
||||
cube:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.CubeSandboxConfig'
|
||||
default_timeout_sec:
|
||||
description: |-
|
||||
DefaultTimeoutSec is the per-execution timeout in seconds. 0 uses the
|
||||
program's built-in default.
|
||||
type: integer
|
||||
docker:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.DockerSandboxConfig'
|
||||
e2b:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.E2BSandboxConfig'
|
||||
env_vars:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: "EnvVars are additional environment variables injected into every\nsandbox
|
||||
created for this tenant. \U0001F512 Values are encrypted at rest.\nThese
|
||||
become visible to all scripts running in the tenant's\nsandboxes — do not
|
||||
place secrets here that scripts must not access."
|
||||
type: object
|
||||
sandbox_type:
|
||||
description: |-
|
||||
SandboxType selects the sandbox backend. Named configs may use "cube",
|
||||
"e2b", "docker", or "local". "disabled" is reserved for the hidden
|
||||
workspace policy row.
|
||||
type: string
|
||||
volume_mount:
|
||||
allOf:
|
||||
- $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.VolumeMountConfig'
|
||||
description: |-
|
||||
VolumeMount configures an optional shared volume mounted into every
|
||||
sandbox created for this tenant. Currently used for tenant-installed
|
||||
skills, but the configuration itself is skill-agnostic and can serve
|
||||
any volume-mount use case (shared datasets, pre-installed toolchains,
|
||||
etc.).
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.ToolCall:
|
||||
properties:
|
||||
args:
|
||||
@@ -3859,6 +4010,37 @@ definitions:
|
||||
Model Name
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.VolumeMountConfig:
|
||||
properties:
|
||||
enabled:
|
||||
description: Enabled toggles the volume mount for this tenant.
|
||||
type: boolean
|
||||
mount_path:
|
||||
description: |-
|
||||
MountPath is the sandbox-internal path where the volume is mounted.
|
||||
Default: /weknora/tenant/skills (customizable per use case).
|
||||
type: string
|
||||
provider:
|
||||
description: Provider identifies the volume backend. Currently "e2b" or "cube".
|
||||
type: string
|
||||
volume_id:
|
||||
description: |-
|
||||
VolumeID is the provider-specific volume identifier, populated after
|
||||
EnsureVolume / CreateVolume succeeds.
|
||||
type: string
|
||||
volume_name:
|
||||
description: |-
|
||||
VolumeName is the human-readable volume name, e.g.
|
||||
"weknora-tenant-<id>-skills".
|
||||
type: string
|
||||
volume_owner_fingerprint:
|
||||
description: |-
|
||||
VolumeOwnerFingerprint = sha256(provider + APIKey + APIURL).
|
||||
Used to detect when the tenant switched to a different backend or
|
||||
API key, at which point the volume is no longer reachable and must
|
||||
be recreated.
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.WeKnoraCloudCredentials:
|
||||
properties:
|
||||
app_id:
|
||||
@@ -3974,6 +4156,7 @@ definitions:
|
||||
- searxng
|
||||
- keenable
|
||||
- zhipu
|
||||
- exa
|
||||
- metaso
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
@@ -3986,6 +4169,7 @@ definitions:
|
||||
- WebSearchProviderTypeSearxng
|
||||
- WebSearchProviderTypeKeenable
|
||||
- WebSearchProviderTypeZhipu
|
||||
- WebSearchProviderTypeExa
|
||||
- WebSearchProviderTypeMetaso
|
||||
github_com_Tencent_WeKnora_internal_types.WikiConfig:
|
||||
properties:
|
||||
@@ -5295,6 +5479,56 @@ definitions:
|
||||
description: Active / ClusterCapacity
|
||||
type: number
|
||||
type: object
|
||||
internal_handler.SandboxCheckItem:
|
||||
properties:
|
||||
latency_ms:
|
||||
type: integer
|
||||
message:
|
||||
description: Message carries free-form provider detail for an executed probe.
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
ok:
|
||||
type: boolean
|
||||
reason:
|
||||
description: |-
|
||||
Reason is a stable code explaining why a probe was skipped. It exists so
|
||||
the UI can phrase the skip in the operator's language instead of echoing
|
||||
a server-side sentence.
|
||||
type: string
|
||||
type: object
|
||||
internal_handler.SandboxCheckRequest:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig'
|
||||
config_id:
|
||||
description: |-
|
||||
ConfigID lets an edit form test stored credentials while overriding only
|
||||
the fields the admin changed in the drawer.
|
||||
type: string
|
||||
deep:
|
||||
description: |-
|
||||
Deep additionally runs a throwaway script. For remote backends this also
|
||||
creates and destroys one sandbox, which is the only way to validate the
|
||||
template ID, data plane, in-sandbox execution, and outbound egress. It may
|
||||
consume real sandbox time, so it is opt-in.
|
||||
type: boolean
|
||||
type: object
|
||||
internal_handler.SandboxCheckResponse:
|
||||
properties:
|
||||
capabilities:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
type: object
|
||||
checks:
|
||||
items:
|
||||
$ref: '#/definitions/internal_handler.SandboxCheckItem'
|
||||
type: array
|
||||
ok:
|
||||
type: boolean
|
||||
provider:
|
||||
type: string
|
||||
type: object
|
||||
internal_handler.SearchMessagesRequest:
|
||||
properties:
|
||||
limit:
|
||||
@@ -5633,6 +5867,17 @@ definitions:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
internal_handler.sandboxConfigRequest:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.TenantSandboxConfig'
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
internal_handler.storageBackendRequest:
|
||||
properties:
|
||||
config:
|
||||
@@ -6608,6 +6853,18 @@ paths:
|
||||
summary: 获取OIDC登录配置
|
||||
tags:
|
||||
- 认证
|
||||
/auth/oidc/start:
|
||||
get:
|
||||
description: |-
|
||||
与 /auth/oidc/url 不同,此端点直接 302 重定向到 OIDC Provider 的授权页,
|
||||
无需前端 JS 介入。适用于外部平台(如企业门户)直接给出一个链接即可
|
||||
触发 OIDC 授权码流程,借助 IdP 的 SSO session 实现免再次输密码。
|
||||
responses:
|
||||
"302":
|
||||
description: Found
|
||||
summary: 发起 OIDC 登录(直接 302)
|
||||
tags:
|
||||
- 认证
|
||||
/auth/oidc/url:
|
||||
get:
|
||||
consumes:
|
||||
@@ -12626,6 +12883,215 @@ paths:
|
||||
summary: 搜索可加入的空间
|
||||
tags:
|
||||
- 组织管理
|
||||
/sandbox-configs:
|
||||
get:
|
||||
description: List workspace sandbox backend configs with credentials masked.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox configs and defaults
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: List sandbox configs
|
||||
tags:
|
||||
- SandboxConfig
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a named workspace sandbox backend config. Credentials are
|
||||
masked in the response.
|
||||
parameters:
|
||||
- description: Sandbox backend config
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.sandboxConfigRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created sandbox config
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request or validation failure
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: Create sandbox config
|
||||
tags:
|
||||
- SandboxConfig
|
||||
/sandbox-configs/{id}:
|
||||
delete:
|
||||
description: Soft-delete a sandbox backend config. force=true only overrides
|
||||
unverifiable provider inventory, never confirmed live sandboxes.
|
||||
parameters:
|
||||
- description: Sandbox config ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Force delete when inventory is unverifiable
|
||||
in: query
|
||||
name: force
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Deletion success
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"409":
|
||||
description: Live sandboxes or unverifiable inventory
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: Delete sandbox config
|
||||
tags:
|
||||
- SandboxConfig
|
||||
get:
|
||||
description: Retrieve a workspace sandbox backend config with credentials masked.
|
||||
parameters:
|
||||
- description: Sandbox config ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox config
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: Sandbox config not found
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: Get sandbox config
|
||||
tags:
|
||||
- SandboxConfig
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update a sandbox backend config. Identity-field changes are refused
|
||||
while the config owns live or paused sandboxes.
|
||||
parameters:
|
||||
- description: Sandbox config ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Updated sandbox config
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.sandboxConfigRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated sandbox config
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request or validation failure
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: Sandbox config not found
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
"409":
|
||||
description: Live sandboxes or unverifiable inventory
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"423":
|
||||
description: Sandbox config is being modified by another request
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: Update sandbox config
|
||||
tags:
|
||||
- SandboxConfig
|
||||
/sandbox-configs/{id}/sandboxes:
|
||||
get:
|
||||
description: Return live/paused sandbox inventory and affected agent names for
|
||||
one config.
|
||||
parameters:
|
||||
- description: Sandbox config ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Sandbox inventory
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: Inspect sandbox config inventory
|
||||
tags:
|
||||
- SandboxConfig
|
||||
/sessions:
|
||||
get:
|
||||
consumes:
|
||||
@@ -12851,6 +13317,38 @@ paths:
|
||||
summary: 取消置顶会话
|
||||
tags:
|
||||
- 会话
|
||||
/sessions/{session_id}/artifacts:
|
||||
get:
|
||||
description: 返回本会话中所有 assistant 消息产生的技能产物元数据(不含 URL)
|
||||
parameters:
|
||||
- description: 会话ID
|
||||
in: path
|
||||
name: session_id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 列出会话生成的产物文件
|
||||
tags:
|
||||
- 会话
|
||||
/sessions/{session_id}/messages/{message_id}/artifacts:
|
||||
get:
|
||||
responses: {}
|
||||
/sessions/{session_id}/messages/{message_id}/artifacts/{index}/download:
|
||||
get:
|
||||
responses: {}
|
||||
/sessions/{session_id}/messages/{message_id}/suggestions:
|
||||
get:
|
||||
parameters:
|
||||
@@ -14011,6 +14509,20 @@ paths:
|
||||
summary: Reset another user's password
|
||||
tags:
|
||||
- System Admin
|
||||
/system/capabilities:
|
||||
get:
|
||||
description: 返回当前部署版本及实际注册的后端路由所对应的功能能力;仅 supported=false 表示入口应隐藏
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 标准 code/msg/data 包装,data 为 DeploymentCapabilitiesData
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 获取部署能力清单
|
||||
tags:
|
||||
- 系统
|
||||
/system/docreader/reconnect:
|
||||
post:
|
||||
consumes:
|
||||
@@ -14077,6 +14589,28 @@ paths:
|
||||
summary: 使用当前参数检测解析引擎可用性
|
||||
tags:
|
||||
- 系统
|
||||
/system/sandbox-check:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用当前填写的参数测试沙箱后端,不保存配置;deep=true 会执行临时脚本,远端后端还会创建并销毁一个沙箱
|
||||
parameters:
|
||||
- description: 沙箱配置
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.SandboxCheckRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.SandboxCheckResponse'
|
||||
summary: 测试沙箱连通性
|
||||
tags:
|
||||
- 系统
|
||||
/system/storage-engine-check:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -45,6 +45,20 @@ export interface SystemInfo {
|
||||
uptime_seconds?: number
|
||||
}
|
||||
|
||||
export interface DeploymentCapability {
|
||||
supported: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface DeploymentCapabilitiesResponse {
|
||||
edition: string
|
||||
capabilities: Record<string, DeploymentCapability>
|
||||
}
|
||||
|
||||
export function getDeploymentCapabilities(): Promise<{ data: DeploymentCapabilitiesResponse }> {
|
||||
return get('/api/v1/system/capabilities')
|
||||
}
|
||||
|
||||
export interface PlaceholderDefinition {
|
||||
name: string
|
||||
label: string
|
||||
|
||||
@@ -165,6 +165,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCommandPaletteStore } from '@/stores/commandPalette'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDeploymentCapabilitiesStore } from '@/stores/deploymentCapabilities'
|
||||
import { useCmdkSearch, type CmdkFileGroup, type CmdkChunk, type CmdkMsgGroup } from './GlobalCommandPalette/useSearch'
|
||||
import { highlightText } from './GlobalCommandPalette/useHighlight'
|
||||
import { useStartChat } from './GlobalCommandPalette/useStartChat'
|
||||
@@ -179,6 +180,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const commandPaletteStore = useCommandPaletteStore()
|
||||
const authStore = useAuthStore()
|
||||
const deploymentCapabilities = useDeploymentCapabilitiesStore()
|
||||
const { open, initialQuery, recentQueries } = storeToRefs(commandPaletteStore)
|
||||
const { startChat } = useStartChat()
|
||||
|
||||
@@ -200,6 +202,7 @@ const {
|
||||
clearResults,
|
||||
} = useCmdkSearch({
|
||||
lockedKbIds: () => (activeKbScope.value ? [activeKbScope.value.id] : []),
|
||||
agentsEnabled: () => deploymentCapabilities.isSupported('agents'),
|
||||
})
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
@@ -256,11 +259,15 @@ const allCommands = computed(() => {
|
||||
t,
|
||||
close: () => commandPaletteStore.closePalette(),
|
||||
})
|
||||
// 共享空间入口与侧栏菜单保持一致:viewer / contributor 看不到。
|
||||
if (!authStore.hasRole('admin')) {
|
||||
return cmds.filter((c) => c.id !== 'open-organizations')
|
||||
}
|
||||
return cmds
|
||||
return cmds.filter((command) => {
|
||||
if (command.id === 'open-agents') {
|
||||
return deploymentCapabilities.isSupported('agents')
|
||||
}
|
||||
if (command.id === 'open-organizations') {
|
||||
return authStore.hasRole('admin') && deploymentCapabilities.isSupported('organizations')
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const filteredCommands = computed(() => filterCommands(allCommands.value, query.value))
|
||||
|
||||
@@ -74,6 +74,8 @@ export function useCmdkSearch(options: {
|
||||
chunkLimit?: number
|
||||
/** Debounce delay in ms. */
|
||||
debounceMs?: number
|
||||
/** 当前部署是否提供智能体路由;不提供时不发起预加载请求。 */
|
||||
agentsEnabled?: () => boolean
|
||||
}) {
|
||||
const debounceMs = options.debounceMs ?? 350
|
||||
const query = ref('')
|
||||
@@ -149,6 +151,7 @@ export function useCmdkSearch(options: {
|
||||
// Agents (own + shared). Lazily loaded & cached; no backend search endpoint
|
||||
// exists so we always filter client-side.
|
||||
const ensureAgents = async (): Promise<void> => {
|
||||
if (options.agentsEnabled?.() === false) return
|
||||
if (agentsLoaded.value) return
|
||||
if (agentsLoadingPromise) return agentsLoadingPromise
|
||||
agentsLoadingPromise = (async () => {
|
||||
@@ -188,6 +191,7 @@ export function useCmdkSearch(options: {
|
||||
}
|
||||
|
||||
const agentMatches = computed<CmdkAgent[]>(() => {
|
||||
if (options.agentsEnabled?.() === false) return []
|
||||
const q = query.value.trim().toLowerCase()
|
||||
if (!q) return []
|
||||
return agents.value
|
||||
@@ -350,7 +354,9 @@ export function useCmdkSearch(options: {
|
||||
// keystroke. Sessions come from the menuStore (already populated by the
|
||||
// sidebar) so no extra fetch needed here.
|
||||
ensureKbs()
|
||||
ensureAgents()
|
||||
if (options.agentsEnabled?.() !== false) {
|
||||
ensureAgents()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -247,6 +247,7 @@ import {
|
||||
import { logout as logoutApi } from '@/api/auth';
|
||||
import { useMenuStore } from '@/stores/menu';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useDeploymentCapabilitiesStore } from '@/stores/deploymentCapabilities';
|
||||
import { useOrganizationStore } from '@/stores/organization';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
import { useCommandPaletteStore } from '@/stores/commandPalette';
|
||||
@@ -286,6 +287,7 @@ const platformLogo = (p: string): string => (p ? PLATFORM_LOGO[p] || '' : '');
|
||||
const { t } = useI18n();
|
||||
const usemenuStore = useMenuStore();
|
||||
const authStore = useAuthStore();
|
||||
const deploymentCapabilities = useDeploymentCapabilitiesStore();
|
||||
const orgStore = useOrganizationStore();
|
||||
const uiStore = useUIStore();
|
||||
const commandPaletteStore = useCommandPaletteStore();
|
||||
@@ -992,7 +994,7 @@ onMounted(async () => {
|
||||
await syncActiveBucketFromChat(initialChatId);
|
||||
}
|
||||
// 若组织列表未加载则拉取一次,用于侧栏「待审批」角标
|
||||
if (orgStore.organizations.length === 0) {
|
||||
if (deploymentCapabilities.isSupported('organizations') && orgStore.organizations.length === 0) {
|
||||
orgStore.fetchOrganizations();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
SETTINGS_SECTION_CAPABILITY,
|
||||
isDeploymentCapabilitySupported,
|
||||
type DeploymentCapabilityMap,
|
||||
} from './deploymentCapabilities'
|
||||
|
||||
test('capability filtering is fail-open unless backend explicitly disables a feature', () => {
|
||||
assert.equal(isDeploymentCapabilitySupported({}, 'organizations'), true)
|
||||
|
||||
const capabilities: DeploymentCapabilityMap = {
|
||||
organizations: { supported: false, reason: 'not_supported_in_lite' },
|
||||
agents: { supported: true },
|
||||
}
|
||||
assert.equal(isDeploymentCapabilitySupported(capabilities, 'organizations'), false)
|
||||
assert.equal(isDeploymentCapabilitySupported(capabilities, 'agents'), true)
|
||||
})
|
||||
|
||||
test('organizations stay hidden in lite even when capabilities fail open', () => {
|
||||
assert.equal(
|
||||
isDeploymentCapabilitySupported({}, 'organizations', { liteMode: true }),
|
||||
false,
|
||||
)
|
||||
assert.equal(
|
||||
isDeploymentCapabilitySupported({}, 'organizations', { edition: 'lite' }),
|
||||
false,
|
||||
)
|
||||
assert.equal(
|
||||
isDeploymentCapabilitySupported({}, 'agents', { liteMode: true }),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('only route-backed settings sections require deployment capabilities', () => {
|
||||
assert.equal(SETTINGS_SECTION_CAPABILITY.mcp, 'settings.mcp')
|
||||
assert.equal(SETTINGS_SECTION_CAPABILITY.storage, 'settings.storage')
|
||||
assert.equal(SETTINGS_SECTION_CAPABILITY.parser, undefined)
|
||||
assert.equal(SETTINGS_SECTION_CAPABILITY['runtime-queues'], undefined)
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
export const DEPLOYMENT_CAPABILITY_KEYS = [
|
||||
'organizations',
|
||||
'agents',
|
||||
'integrations.im',
|
||||
'integrations.embed',
|
||||
'integrations.api',
|
||||
'settings.mcp',
|
||||
'settings.websearch',
|
||||
'settings.vectorstore',
|
||||
'settings.storage',
|
||||
'settings.sandbox',
|
||||
] as const
|
||||
|
||||
export type DeploymentCapabilityKey = typeof DEPLOYMENT_CAPABILITY_KEYS[number]
|
||||
|
||||
export interface DeploymentCapability {
|
||||
supported: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export type DeploymentCapabilityMap = Partial<Record<DeploymentCapabilityKey, DeploymentCapability>>
|
||||
|
||||
/**
|
||||
* 能力接口失败或旧版后端没有返回某个键时保持可见,避免一次探测失败把整个菜单清空。
|
||||
* 只有后端明确返回 supported: false 时才隐藏入口。
|
||||
*/
|
||||
export function isDeploymentCapabilitySupported(
|
||||
capabilities: DeploymentCapabilityMap,
|
||||
key?: DeploymentCapabilityKey,
|
||||
options?: { liteMode?: boolean; edition?: string },
|
||||
): boolean {
|
||||
if (!key) return true
|
||||
if (key === 'organizations') {
|
||||
const isLite =
|
||||
options?.liteMode === true ||
|
||||
options?.edition?.trim().toLowerCase() === 'lite'
|
||||
if (isLite) return false
|
||||
}
|
||||
return capabilities[key]?.supported !== false
|
||||
}
|
||||
|
||||
export const SETTINGS_SECTION_CAPABILITY: Partial<Record<string, DeploymentCapabilityKey>> = {
|
||||
websearch: 'settings.websearch',
|
||||
vectorstore: 'settings.vectorstore',
|
||||
storage: 'settings.storage',
|
||||
sandbox: 'settings.sandbox',
|
||||
mcp: 'settings.mcp',
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DeploymentCapabilityKey } from './deploymentCapabilities'
|
||||
|
||||
export const CHROME_EXTENSION_URL =
|
||||
'https://chromewebstore.google.com/detail/jpemjbopikggjlmikmclgbmkhhopjdgd?utm_source=item-share-cb'
|
||||
|
||||
@@ -14,6 +16,12 @@ export const INTEGRATION_TAB_MIN_ROLE: Partial<Record<IntegrationTab, Integratio
|
||||
api: 'owner',
|
||||
}
|
||||
|
||||
export const INTEGRATION_TAB_CAPABILITY: Partial<Record<IntegrationTab, DeploymentCapabilityKey>> = {
|
||||
im: 'integrations.im',
|
||||
embed: 'integrations.embed',
|
||||
api: 'integrations.api',
|
||||
}
|
||||
|
||||
export type IntegrationPreviewIcon =
|
||||
| { type: 'icon'; name: string }
|
||||
| { type: 'emoji'; value: string }
|
||||
|
||||
@@ -1260,6 +1260,7 @@ export default {
|
||||
title: 'Insufficient permissions',
|
||||
desc: 'Your role can\'t access this settings page. Ask an admin of this workspace to grant the required role.'
|
||||
},
|
||||
capabilityUnavailable: 'This feature is not supported by the current deployment. You have been returned to an available page.',
|
||||
weknoraCloud: {
|
||||
title: 'WeKnora Cloud',
|
||||
description: 'Configure WeKnora Cloud APPID and APPSECRET credentials. Credentials are used for model services and document parsing engine.',
|
||||
|
||||
@@ -5293,6 +5293,7 @@ export default {
|
||||
title: '권한 없음',
|
||||
desc: '현재 역할로는 이 설정 페이지에 접근할 수 없습니다. 이 워크스페이스의 관리자에게 필요한 역할을 요청하세요.'
|
||||
},
|
||||
capabilityUnavailable: '현재 배포에서는 이 기능을 지원하지 않습니다. 사용 가능한 페이지로 돌아갔습니다.',
|
||||
navGroups: {
|
||||
account: '계정',
|
||||
workspace: '공간',
|
||||
|
||||
@@ -5293,6 +5293,7 @@ export default {
|
||||
title: 'Недостаточно прав',
|
||||
desc: 'Ваша роль не позволяет открыть этот раздел настроек. Обратитесь к администратору пространства, чтобы запросить нужную роль.'
|
||||
},
|
||||
capabilityUnavailable: 'Эта функция не поддерживается в текущем развёртывании. Выполнен переход на доступную страницу.',
|
||||
navGroups: {
|
||||
account: 'Аккаунт',
|
||||
workspace: 'Пространство',
|
||||
|
||||
@@ -5295,6 +5295,7 @@ export default {
|
||||
title: '权限不足',
|
||||
desc: '你当前的角色无权访问此设置项。请联系本空间的管理员获取所需角色。'
|
||||
},
|
||||
capabilityUnavailable: '当前部署不支持此功能,已返回可用页面。',
|
||||
navGroups: {
|
||||
account: '账户',
|
||||
workspace: '空间',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDeploymentCapabilitiesStore } from '@/stores/deploymentCapabilities'
|
||||
import { autoSetup, getCurrentUser, userInfoFromApi } from '@/api/auth'
|
||||
import type { DeploymentCapabilityKey } from '@/config/deploymentCapabilities'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
/** Lite /桌面 WebView 硬刷新时可能只打开 `/`,用 session 记住上次页面以便恢复 */
|
||||
const LITE_LAST_PATH_KEY = 'weknora_lite_last_path'
|
||||
@@ -132,7 +136,7 @@ const router = createRouter({
|
||||
path: "agents",
|
||||
name: "agentList",
|
||||
component: () => import("../views/agent/AgentList.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
meta: { requiresInit: true, requiresAuth: true, requiredCapability: 'agents' }
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
@@ -167,7 +171,7 @@ const router = createRouter({
|
||||
path: "organizations",
|
||||
name: "organizationList",
|
||||
component: () => import("../views/organization/OrganizationList.vue"),
|
||||
meta: { requiresInit: true, requiresAuth: true }
|
||||
meta: { requiresInit: true, requiresAuth: true, requiredCapability: 'organizations' }
|
||||
},
|
||||
// Compatibility redirects for /platform/system/* URLs. System
|
||||
// administration surfaces live as dedicated sections inside the
|
||||
@@ -387,6 +391,17 @@ router.beforeEach(async (to, from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 部署能力只描述“后端是否提供该功能”,不反映服务健康或是否已配置。
|
||||
// 探测失败时 Store 会 fail-open,真正的权限和可用性仍由后端接口校验。
|
||||
const deploymentCapabilities = useDeploymentCapabilitiesStore()
|
||||
await deploymentCapabilities.ensureLoaded()
|
||||
const requiredCapability = to.meta.requiredCapability as DeploymentCapabilityKey | undefined
|
||||
if (requiredCapability && !deploymentCapabilities.isSupported(requiredCapability)) {
|
||||
MessagePlugin.warning(i18n.global.t('settings.capabilityUnavailable'))
|
||||
next('/platform/knowledge-bases')
|
||||
return
|
||||
}
|
||||
|
||||
// SystemAdmin gate — checked AFTER auth so a non-admin who's logged
|
||||
// out gets redirected to /login first (consistent with how the rest
|
||||
// of the auth flow works), and only an authenticated non-admin sees
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getDeploymentCapabilities } from '@/api/system'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
isDeploymentCapabilitySupported,
|
||||
type DeploymentCapabilityKey,
|
||||
type DeploymentCapabilityMap,
|
||||
} from '@/config/deploymentCapabilities'
|
||||
|
||||
export const useDeploymentCapabilitiesStore = defineStore('deploymentCapabilities', () => {
|
||||
const edition = ref('')
|
||||
const capabilities = ref<DeploymentCapabilityMap>({})
|
||||
const loaded = ref(false)
|
||||
const loadError = ref('')
|
||||
let loadingPromise: Promise<void> | null = null
|
||||
|
||||
const ensureLoaded = async (force = false): Promise<void> => {
|
||||
if (loaded.value && !force) return
|
||||
if (loadingPromise) return loadingPromise
|
||||
|
||||
loadingPromise = (async () => {
|
||||
try {
|
||||
const response = await getDeploymentCapabilities()
|
||||
edition.value = response.data?.edition || ''
|
||||
capabilities.value = response.data?.capabilities || {}
|
||||
loadError.value = ''
|
||||
} catch (error) {
|
||||
// 能力探测失败时保持 fail-open;权限仍由后端路由最终校验。
|
||||
capabilities.value = {}
|
||||
loadError.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
loaded.value = true
|
||||
loadingPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return loadingPromise
|
||||
}
|
||||
|
||||
const isSupported = (key?: DeploymentCapabilityKey) => {
|
||||
const authStore = useAuthStore()
|
||||
return isDeploymentCapabilitySupported(capabilities.value, key, {
|
||||
liteMode: authStore.isLiteMode,
|
||||
edition: edition.value,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
edition,
|
||||
capabilities,
|
||||
loaded,
|
||||
loadError,
|
||||
ensureLoaded,
|
||||
isSupported,
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,8 @@ import { reactive, ref, computed, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import i18n from '@/i18n'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDeploymentCapabilitiesStore } from '@/stores/deploymentCapabilities'
|
||||
import type { DeploymentCapabilityKey } from '@/config/deploymentCapabilities'
|
||||
|
||||
type MenuChild = Record<string, any>
|
||||
|
||||
@@ -12,6 +14,7 @@ interface MenuItem {
|
||||
path: string
|
||||
childrenPath?: string
|
||||
children?: MenuChild[]
|
||||
requiredCapability?: DeploymentCapabilityKey
|
||||
}
|
||||
|
||||
const createMenuChildren = () => reactive<MenuChild[]>([])
|
||||
@@ -27,8 +30,8 @@ export const useMenuStore = defineStore('menuStore', () => {
|
||||
children: createMenuChildren()
|
||||
},
|
||||
{ title: '', titleKey: 'menu.knowledgeBase', icon: 'zhishiku', path: 'knowledge-bases' },
|
||||
{ title: '', titleKey: 'menu.agents', icon: 'agent', path: 'agents' },
|
||||
{ title: '', titleKey: 'menu.organizations', icon: 'organization', path: 'organizations' },
|
||||
{ title: '', titleKey: 'menu.agents', icon: 'agent', path: 'agents', requiredCapability: 'agents' },
|
||||
{ title: '', titleKey: 'menu.organizations', icon: 'organization', path: 'organizations', requiredCapability: 'organizations' },
|
||||
{ title: '', titleKey: 'menu.settings', icon: 'setting', path: 'settings' },
|
||||
{ title: '', titleKey: 'menu.logout', icon: 'logout', path: 'logout' }
|
||||
])
|
||||
@@ -65,6 +68,7 @@ export const useMenuStore = defineStore('menuStore', () => {
|
||||
// 入口在侧栏只会徒增噪音;后端 RBAC 才是权限的最终来源(见 middleware/rbac.go)。
|
||||
const visibleMenuArr = computed(() => {
|
||||
const authStore = useAuthStore()
|
||||
const deploymentCapabilities = useDeploymentCapabilitiesStore()
|
||||
return menuArr.filter(item => {
|
||||
if (authStore.isLiteMode && liteHiddenPaths.has(item.path)) {
|
||||
return false
|
||||
@@ -72,6 +76,9 @@ export const useMenuStore = defineStore('menuStore', () => {
|
||||
if (item.path === 'organizations' && !authStore.hasRole('admin')) {
|
||||
return false
|
||||
}
|
||||
if (!deploymentCapabilities.isSupported(item.requiredCapability)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -207,7 +207,9 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useDeploymentCapabilitiesStore } from '@/stores/deploymentCapabilities'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import SystemInfo from './SystemInfo.vue'
|
||||
import TenantInfo from './TenantInfo.vue'
|
||||
import UserProfile from './UserProfile.vue'
|
||||
@@ -232,6 +234,7 @@ import SystemAuditLog from '@/views/system/SystemAuditLog.vue'
|
||||
import IntegrationSettingsSection from '@/views/integrations/IntegrationSettingsSection.vue'
|
||||
import {
|
||||
INTEGRATION_PREVIEW_ITEMS,
|
||||
INTEGRATION_TAB_CAPABILITY,
|
||||
INTEGRATION_TAB_MIN_ROLE,
|
||||
INTEGRATION_TABS,
|
||||
type IntegrationTab,
|
||||
@@ -240,11 +243,13 @@ import {
|
||||
SETTINGS_SECTION_MIN_ROLE,
|
||||
SYSTEM_ADMIN_SETTINGS_SECTIONS,
|
||||
} from '@/config/settingsAccess'
|
||||
import { SETTINGS_SECTION_CAPABILITY } from '@/config/deploymentCapabilities'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const uiStore = useUIStore()
|
||||
const authStore = useAuthStore()
|
||||
const deploymentCapabilities = useDeploymentCapabilitiesStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const currentSection = ref<string>('general')
|
||||
@@ -310,6 +315,15 @@ const normalizeSettingsSection = (section: string) => {
|
||||
return section
|
||||
}
|
||||
|
||||
const isSectionSupported = (key: string): boolean => {
|
||||
if (isIntegrationSection(key)) {
|
||||
return deploymentCapabilities.isSupported(
|
||||
INTEGRATION_TAB_CAPABILITY[integrationTabFromSection(key)],
|
||||
)
|
||||
}
|
||||
return deploymentCapabilities.isSupported(SETTINGS_SECTION_CAPABILITY[key])
|
||||
}
|
||||
|
||||
const canSeeSection = (key: string): boolean => {
|
||||
if (isIntegrationSection(key)) {
|
||||
const min = INTEGRATION_TAB_MIN_ROLE[integrationTabFromSection(key)]
|
||||
@@ -367,7 +381,7 @@ const navItems = computed(() => {
|
||||
if (!authStore.currentTenantRole && !authStore.canAccessAllTenants) {
|
||||
return [] as NavItem[]
|
||||
}
|
||||
return all.filter((it) => canSeeSection(it.key))
|
||||
return all.filter((it) => canSeeSection(it.key) && isSectionSupported(it.key))
|
||||
})
|
||||
|
||||
const navGroups = computed<NavGroup[]>(() => {
|
||||
@@ -506,6 +520,12 @@ const handleClose = () => {
|
||||
watch(() => uiStore.settingsInitialSection, (section) => {
|
||||
if (section && visible.value) {
|
||||
const normalizedSection = normalizeSettingsSection(section)
|
||||
if (deploymentCapabilities.loaded && !isSectionSupported(normalizedSection)) {
|
||||
MessagePlugin.warning(t('settings.capabilityUnavailable'))
|
||||
currentSection.value = navItems.value[0]?.key || 'general'
|
||||
currentSubSection.value = ''
|
||||
return
|
||||
}
|
||||
currentSection.value = normalizedSection
|
||||
const navItem = (navItems.value as any[]).find((item) => item.key === normalizedSection)
|
||||
if (navItem && navItem.children && navItem.children.length > 0) {
|
||||
@@ -528,10 +548,23 @@ watch(() => uiStore.settingsInitialSection, (section) => {
|
||||
}, { immediate: true })
|
||||
|
||||
watch(
|
||||
() => [visible.value, route.query.section],
|
||||
([isVisible, section]) => {
|
||||
() => [visible.value, route.query.section, deploymentCapabilities.loaded] as const,
|
||||
([isVisible, section, capabilitiesLoaded]) => {
|
||||
if (!isVisible || typeof section !== 'string') return
|
||||
currentSection.value = normalizeSettingsSection(section)
|
||||
const normalizedSection = normalizeSettingsSection(section)
|
||||
if (capabilitiesLoaded && !isSectionSupported(normalizedSection)) {
|
||||
MessagePlugin.warning(t('settings.capabilityUnavailable'))
|
||||
currentSection.value = navItems.value[0]?.key || 'general'
|
||||
currentSubSection.value = ''
|
||||
if (route.path === '/platform/settings') {
|
||||
const query = { ...route.query }
|
||||
delete query.section
|
||||
delete query.tab
|
||||
void router.replace({ path: route.path, query })
|
||||
}
|
||||
return
|
||||
}
|
||||
currentSection.value = normalizedSection
|
||||
currentSubSection.value = ''
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -558,6 +591,12 @@ const handleSettingsNav = (e: CustomEvent) => {
|
||||
const { section, subsection } = e.detail
|
||||
if (section) {
|
||||
const normalizedSection = normalizeSettingsSection(section)
|
||||
if (deploymentCapabilities.loaded && !isSectionSupported(normalizedSection)) {
|
||||
MessagePlugin.warning(t('settings.capabilityUnavailable'))
|
||||
currentSection.value = navItems.value[0]?.key || 'general'
|
||||
currentSubSection.value = ''
|
||||
return
|
||||
}
|
||||
currentSection.value = normalizedSection
|
||||
// 如果有子菜单,自动展开
|
||||
const navItem = (navItems.value as any[]).find((item: any) => item.key === normalizedSection)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DeploymentCapabilityKeys is the canonical capability key list shared with
|
||||
// frontend/src/config/deploymentCapabilities.ts — keep both in sync.
|
||||
var DeploymentCapabilityKeys = []string{
|
||||
"organizations",
|
||||
"agents",
|
||||
"integrations.im",
|
||||
"integrations.embed",
|
||||
"integrations.api",
|
||||
"settings.mcp",
|
||||
"settings.websearch",
|
||||
"settings.vectorstore",
|
||||
"settings.storage",
|
||||
"settings.sandbox",
|
||||
}
|
||||
|
||||
// DeploymentCapability describes whether a deployment exposes a feature route.
|
||||
type DeploymentCapability struct {
|
||||
Supported bool `json:"supported"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// DeploymentCapabilitiesData is returned by GET /system/capabilities.
|
||||
type DeploymentCapabilitiesData struct {
|
||||
Edition string `json:"edition"`
|
||||
Capabilities map[string]DeploymentCapability `json:"capabilities"`
|
||||
}
|
||||
|
||||
// DeploymentFeatureAvailability mirrors injected backend handlers/services.
|
||||
type DeploymentFeatureAvailability struct {
|
||||
Organizations bool
|
||||
Agents bool
|
||||
IM bool
|
||||
Embed bool
|
||||
API bool
|
||||
MCP bool
|
||||
WebSearch bool
|
||||
VectorStore bool
|
||||
Storage bool
|
||||
Sandbox bool
|
||||
}
|
||||
|
||||
func supportedDeploymentCapability(supported bool) DeploymentCapability {
|
||||
if supported {
|
||||
return DeploymentCapability{Supported: true}
|
||||
}
|
||||
return DeploymentCapability{Supported: false, Reason: "route_not_registered"}
|
||||
}
|
||||
|
||||
// BuildDeploymentCapabilities derives the deployment capability snapshot.
|
||||
func BuildDeploymentCapabilities(
|
||||
edition string,
|
||||
available DeploymentFeatureAvailability,
|
||||
) DeploymentCapabilitiesData {
|
||||
isLite := strings.EqualFold(strings.TrimSpace(edition), "lite")
|
||||
organizations := supportedDeploymentCapability(available.Organizations && !isLite)
|
||||
if isLite {
|
||||
organizations.Reason = "not_supported_in_lite"
|
||||
}
|
||||
|
||||
return DeploymentCapabilitiesData{
|
||||
Edition: edition,
|
||||
Capabilities: map[string]DeploymentCapability{
|
||||
"organizations": organizations,
|
||||
"agents": supportedDeploymentCapability(available.Agents),
|
||||
"integrations.im": supportedDeploymentCapability(available.IM),
|
||||
"integrations.embed": supportedDeploymentCapability(available.Embed),
|
||||
"integrations.api": supportedDeploymentCapability(available.API),
|
||||
"settings.mcp": supportedDeploymentCapability(available.MCP),
|
||||
"settings.websearch": supportedDeploymentCapability(available.WebSearch),
|
||||
"settings.vectorstore": supportedDeploymentCapability(available.VectorStore),
|
||||
"settings.storage": supportedDeploymentCapability(available.Storage),
|
||||
"settings.sandbox": supportedDeploymentCapability(available.Sandbox),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BindDeploymentCapabilities stores the startup snapshot used by GetDeploymentCapabilities.
|
||||
func (h *SystemHandler) BindDeploymentCapabilities(data DeploymentCapabilitiesData) {
|
||||
h.deploymentCapabilities = data
|
||||
}
|
||||
|
||||
// GetDeploymentCapabilities godoc
|
||||
// @Summary 获取部署能力清单
|
||||
// @Description 返回当前部署版本及实际注册的后端路由所对应的功能能力;仅 supported=false 表示入口应隐藏
|
||||
// @Tags 系统
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "标准 code/msg/data 包装,data 为 DeploymentCapabilitiesData"
|
||||
// @Router /system/capabilities [get]
|
||||
func (h *SystemHandler) GetDeploymentCapabilities(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": h.deploymentCapabilities,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeploymentCapabilityKeysMatchFrontend(t *testing.T) {
|
||||
frontendKeys, err := readFrontendDeploymentCapabilityKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("read frontend capability keys: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(DeploymentCapabilityKeys, frontendKeys) {
|
||||
t.Fatalf("backend keys = %#v, frontend keys = %#v", DeploymentCapabilityKeys, frontendKeys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeploymentCapabilitiesIncludesAllKeys(t *testing.T) {
|
||||
result := BuildDeploymentCapabilities("standard", DeploymentFeatureAvailability{
|
||||
Organizations: true,
|
||||
Agents: true,
|
||||
IM: true,
|
||||
Embed: true,
|
||||
API: true,
|
||||
MCP: true,
|
||||
WebSearch: true,
|
||||
VectorStore: true,
|
||||
Storage: true,
|
||||
Sandbox: true,
|
||||
})
|
||||
|
||||
for _, key := range DeploymentCapabilityKeys {
|
||||
if _, ok := result.Capabilities[key]; !ok {
|
||||
t.Fatalf("missing capability key %q", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFrontendDeploymentCapabilityKeys() ([]string, error) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
repoRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", ".."))
|
||||
frontendPath := filepath.Join(repoRoot, "frontend", "src", "config", "deploymentCapabilities.ts")
|
||||
content, err := os.ReadFile(frontendPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`(?s)export const DEPLOYMENT_CAPABILITY_KEYS = \[(.*?)\]`)
|
||||
match := re.FindSubmatch(content)
|
||||
if len(match) < 2 {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for _, line := range strings.Split(string(match[1]), "\n") {
|
||||
line = strings.TrimSpace(strings.TrimRight(line, ","))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
line = strings.Trim(line, `'`)
|
||||
keys = append(keys, line)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
@@ -63,6 +63,8 @@ type SystemHandler struct {
|
||||
// unit tests, in which case only the legacy config is consulted.
|
||||
storageBackendRepo interfaces.StorageBackendRepository
|
||||
sandboxConfigSvc sandboxConfigService
|
||||
// startup snapshot for GET /system/capabilities; bound in router.NewRouter.
|
||||
deploymentCapabilities DeploymentCapabilitiesData
|
||||
}
|
||||
|
||||
// NewSystemHandler creates a new system handler
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
import "github.com/Tencent/WeKnora/internal/handler"
|
||||
|
||||
func deploymentCapabilitiesFromRouter(params RouterParams) handler.DeploymentCapabilitiesData {
|
||||
return handler.BuildDeploymentCapabilities(handler.Edition, handler.DeploymentFeatureAvailability{
|
||||
Organizations: params.OrganizationHandler != nil,
|
||||
Agents: params.CustomAgentHandler != nil,
|
||||
IM: params.IMHandler != nil,
|
||||
// Match RegisterEmbedChannelRoutes: management routes depend on handler only.
|
||||
Embed: params.EmbedChannelHandler != nil,
|
||||
API: params.TenantHandler != nil && params.TenantAPIKeyService != nil,
|
||||
MCP: params.MCPServiceHandler != nil && params.MCPCredentialsHandler != nil && params.MCPOAuthHandler != nil,
|
||||
WebSearch: params.WebSearchHandler != nil && params.WebSearchProviderHandler != nil && params.WebSearchCredentialsHandler != nil,
|
||||
VectorStore: params.VectorStoreHandler != nil,
|
||||
Storage: params.StorageBackendHandler != nil,
|
||||
Sandbox: params.SandboxConfigHandler != nil,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/handler"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func allDeploymentFeaturesAvailable() handler.DeploymentFeatureAvailability {
|
||||
return handler.DeploymentFeatureAvailability{
|
||||
Organizations: true,
|
||||
Agents: true,
|
||||
IM: true,
|
||||
Embed: true,
|
||||
API: true,
|
||||
MCP: true,
|
||||
WebSearch: true,
|
||||
VectorStore: true,
|
||||
Storage: true,
|
||||
Sandbox: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeploymentCapabilitiesHidesOrganizationsInLite(t *testing.T) {
|
||||
result := handler.BuildDeploymentCapabilities("lite", allDeploymentFeaturesAvailable())
|
||||
|
||||
organization := result.Capabilities["organizations"]
|
||||
if organization.Supported {
|
||||
t.Fatal("organizations should be unsupported in lite edition")
|
||||
}
|
||||
if organization.Reason != "not_supported_in_lite" {
|
||||
t.Fatalf("organization reason = %q, want not_supported_in_lite", organization.Reason)
|
||||
}
|
||||
if !result.Capabilities["agents"].Supported {
|
||||
t.Fatal("agents should remain supported in lite edition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDeploymentCapabilitiesReflectsMissingRoutes(t *testing.T) {
|
||||
available := allDeploymentFeaturesAvailable()
|
||||
available.Embed = false
|
||||
available.MCP = false
|
||||
|
||||
result := handler.BuildDeploymentCapabilities("standard", available)
|
||||
|
||||
for _, key := range []string{"integrations.embed", "settings.mcp"} {
|
||||
capability := result.Capabilities[key]
|
||||
if capability.Supported {
|
||||
t.Fatalf("%s should be unsupported", key)
|
||||
}
|
||||
if capability.Reason != "route_not_registered" {
|
||||
t.Fatalf("%s reason = %q, want route_not_registered", key, capability.Reason)
|
||||
}
|
||||
}
|
||||
if !result.Capabilities["settings.storage"].Supported {
|
||||
t.Fatal("an available route should remain supported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeploymentCapabilitiesHandlerReturnsSnapshot(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
want := handler.BuildDeploymentCapabilities("standard", allDeploymentFeaturesAvailable())
|
||||
systemHandler := &handler.SystemHandler{}
|
||||
systemHandler.BindDeploymentCapabilities(want)
|
||||
engine.GET("/capabilities", systemHandler.GetDeploymentCapabilities)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/capabilities", nil)
|
||||
engine.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
||||
}
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Data handler.DeploymentCapabilitiesData `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if body.Code != 0 || body.Data.Edition != "standard" {
|
||||
t.Fatalf("response = %#v", body)
|
||||
}
|
||||
if !body.Data.Capabilities["integrations.embed"].Supported {
|
||||
t.Fatal("embed capability should be returned")
|
||||
}
|
||||
}
|
||||
@@ -267,6 +267,7 @@ func NewRouter(params RouterParams) *gin.Engine {
|
||||
RegisterSandboxConfigRoutes(v1, params.SandboxConfigHandler, rbacGuards)
|
||||
RegisterEvaluationRoutes(v1, params.EvaluationHandler, rbacGuards)
|
||||
RegisterInitializationRoutes(v1, params.InitializationHandler, rbacGuards)
|
||||
params.SystemHandler.BindDeploymentCapabilities(deploymentCapabilitiesFromRouter(params))
|
||||
RegisterSystemRoutes(v1, params.SystemHandler, rbacGuards)
|
||||
RegisterSystemAdminRoutes(v1, params.SystemHandler, params.AuditLogHandler, rbacGuards)
|
||||
RegisterMCPServiceRoutes(v1, params.MCPServiceHandler, params.MCPCredentialsHandler, params.MCPOAuthHandler, rbacGuards)
|
||||
|
||||
@@ -348,6 +348,11 @@ func TestTenantInfrastructureRoutesDeclareSpecificCapabilities(t *testing.T) {
|
||||
RegisterDataSourceRoutes(v1, &handler.DataSourceHandler{}, &handler.DataSourceCredentialsHandler{}, g)
|
||||
RegisterWeKnoraCloudRoutes(v1, &handler.WeKnoraCloudHandler{}, g)
|
||||
|
||||
capabilitiesPolicy := mustLookupAPIKeyPolicy(t, g, http.MethodGet, "/api/v1/system/capabilities")
|
||||
if capabilitiesPolicy.RequireFullAccess || len(capabilitiesPolicy.Capabilities) != 0 {
|
||||
t.Fatalf("system capabilities should be readable by any valid API key: %#v", capabilitiesPolicy)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
|
||||
@@ -214,9 +214,14 @@ func RegisterAuthRoutes(r *gin.RouterGroup, handler *handler.AuthHandler, g *rba
|
||||
// reachable". The /*-check / /reconnect endpoints actively probe
|
||||
// remote services with tenant credentials and could trigger network
|
||||
// fanout, so they're Admin+.
|
||||
func RegisterSystemRoutes(r *gin.RouterGroup, handler *handler.SystemHandler, g *rbacGuards) {
|
||||
func RegisterSystemRoutes(
|
||||
r *gin.RouterGroup,
|
||||
handler *handler.SystemHandler,
|
||||
g *rbacGuards,
|
||||
) {
|
||||
systemRoutes := g.apiKeyGroup(r.Group("/system"), apiKeyManageVectorStores(apiKeyFullAccess()))
|
||||
{
|
||||
systemRoutes.With(apiKeyAny()).GET("/capabilities", g.Viewer(), handler.GetDeploymentCapabilities)
|
||||
systemRoutes.GET("/info", g.Viewer(), handler.GetSystemInfo)
|
||||
systemRoutes.GET("/parser-engines", g.Viewer(), handler.ListParserEngines)
|
||||
systemRoutes.POST("/parser-engines/check", g.Admin(), handler.CheckParserEngines)
|
||||
|
||||
Reference in New Issue
Block a user