fix(knowledge): harden multi-tag data integrity and UX follow-ups
Clear tag relations on move/delete, validate tags on create, batch-load tags, restore KB-scoped reference counts, dedupe SetKnowledgeTags, fix filter double-fetch and ResizeObserver leaks, and regenerate Swagger for tag_ids.
This commit is contained in:
+418
-12
@@ -2081,9 +2081,65 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/datasource/{id}/resource-ancestors": {
|
||||
"post": {
|
||||
"description": "Resolve the ancestor ExternalIDs that must be expanded to reveal the given (possibly deeply nested) resources in a lazily-loaded picker. Used to restore an existing selection when editing a data source.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"DataSource"
|
||||
],
|
||||
"summary": "Resolve resource ancestors",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Data source ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Resource IDs to resolve",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.resolveAncestorsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/datasource/{id}/resources": {
|
||||
"get": {
|
||||
"description": "List resources available for sync in the external system",
|
||||
"description": "List resources available for sync in the external system. Pass parent_id to lazily load the direct children of a resource (used for large hierarchical sources such as Feishu wiki).",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -2098,6 +2154,12 @@ const docTemplate = `{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Parent resource ExternalID; empty lists the top level",
|
||||
"name": "parent_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4667,8 +4729,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "标签ID筛选",
|
||||
"name": "tag_id",
|
||||
"description": "标签ID筛选,逗号分隔(OR语义)",
|
||||
"name": "tag_ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -4830,6 +4892,12 @@ const docTemplate = `{
|
||||
"name": "enable_multimodel",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "分类ID列表,逗号分隔",
|
||||
"name": "tag_ids",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "处理配置JSON(KnowledgeProcessOverrides)",
|
||||
@@ -4963,8 +5031,11 @@ const docTemplate = `{
|
||||
"file_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"type": "string"
|
||||
"tag_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -5668,6 +5739,61 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/batch-reparse": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按 ID 列表批量重新解析单个知识库下的多个知识条目",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"知识管理"
|
||||
],
|
||||
"summary": "批量重新解析知识",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "批量重解析请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.batchReparseKnowledgeRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "任务已提交",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/image/{id}/{chunk_id}": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -5893,7 +6019,7 @@ const docTemplate = `{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Search knowledge files by keyword. When agent_id is set (shared agent), scope is the agent's configured knowledge bases.",
|
||||
"description": "Search knowledge files by keyword. Pass recent=true without a keyword to browse recent files. When agent_id is set (shared agent), scope is the agent's configured knowledge bases.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -5934,6 +6060,12 @@ const docTemplate = `{
|
||||
"description": "Shared agent ID (search within agent's KB scope)",
|
||||
"name": "agent_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Return recent files when keyword is empty",
|
||||
"name": "recent",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -7459,6 +7591,40 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/oauth/callback": {
|
||||
"get": {
|
||||
"description": "接收授权服务器回调并完成 code 交换,随后重定向回前端",
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "MCP OAuth 回调",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "授权码",
|
||||
"name": "code",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "状态参数",
|
||||
"name": "state",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "授权错误码",
|
||||
"name": "error",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -7721,6 +7887,126 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/authorize-url": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "对使用 OAuth 的 MCP 服务执行发现与动态客户端注册,返回浏览器应跳转的授权地址(当前用户维度)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "发起 MCP OAuth 授权",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "{redirect_uri: string, frontend_redirect?: string}",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{authorization_url: string}",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/status": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "返回当前用户对指定 MCP 服务是否已完成 OAuth 授权",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "查询 MCP OAuth 授权状态",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{authorized: bool}",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/token": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "删除当前用户对指定 MCP 服务的 OAuth 令牌",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "撤销 MCP OAuth 授权",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/resources": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -14370,7 +14656,7 @@ const docTemplate = `{
|
||||
"type": "integer"
|
||||
},
|
||||
"replica_number": {
|
||||
"description": "Milvus: in-memory replica count (LoadCollection)",
|
||||
"description": "Milvus LoadCollection / Tencent VectorDB CreateCollection replicas",
|
||||
"type": "integer"
|
||||
},
|
||||
"replication_factor": {
|
||||
@@ -14620,9 +14906,12 @@ const docTemplate = `{
|
||||
"description": "Summary status for async summary generation",
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"description": "Optional tag ID for categorization within a knowledge base",
|
||||
"type": "string"
|
||||
"tags": {
|
||||
"description": "Tags holds the tags associated with this knowledge (populated on query, not persisted directly).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeTag"
|
||||
}
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Tenant ID",
|
||||
@@ -14991,6 +15280,13 @@ const docTemplate = `{
|
||||
"graph_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parser_engine_overrides": {
|
||||
"description": "ParserEngineOverrides passes key-value configuration to docreader parsers\n(e.g. pdf_force_scanned=true). Merged with tenant-level overrides in the\nparse pipeline; per-upload values take priority on conflict.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parser_engine_rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -15005,6 +15301,47 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.KnowledgeTag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Optional display color",
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"description": "Creation time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier of the tag (UUID)",
|
||||
"type": "string"
|
||||
},
|
||||
"knowledge_base_id": {
|
||||
"description": "Knowledge base ID that this tag belongs to",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Tag name, unique within the same knowledge base",
|
||||
"type": "string"
|
||||
},
|
||||
"seq_id": {
|
||||
"description": "SeqID is an auto-increment integer ID for external API usage",
|
||||
"type": "integer"
|
||||
},
|
||||
"sort_order": {
|
||||
"description": "Sort order within the same knowledge base",
|
||||
"type": "integer"
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Tenant ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"description": "Last updated time",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.ListMembersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15137,17 +15474,51 @@ const docTemplate = `{
|
||||
"api_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_server_metadata_url": {
|
||||
"description": "AuthServerMetadataURL optionally pins the OAuth authorization server\nmetadata URL. When empty, the server is discovered automatically from\nthe MCP URL (RFC 9728 / RFC 8414).",
|
||||
"type": "string"
|
||||
},
|
||||
"auth_type": {
|
||||
"description": "AuthType selects the authentication strategy. Empty (\"\") is treated as\nnone for backward compatibility with rows that pre-date this field.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MCPAuthType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"custom_headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Scopes are the OAuth scopes requested during authorization. Optional.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MCPAuthType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"",
|
||||
"api_key",
|
||||
"bearer",
|
||||
"oauth"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"MCPAuthNone",
|
||||
"MCPAuthAPIKey",
|
||||
"MCPAuthBearer",
|
||||
"MCPAuthOAuth"
|
||||
]
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MCPEnvVars": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -15279,8 +15650,11 @@ const docTemplate = `{
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"type": "string"
|
||||
"tag_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -19275,6 +19649,27 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.batchReparseKnowledgeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ids",
|
||||
"kb_id"
|
||||
],
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"process_config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeProcessOverrides"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.createInvitationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -19376,6 +19771,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.resolveAncestorsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.updateLastFAQImportResultDisplayStatusRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
+418
-12
@@ -2074,9 +2074,65 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/datasource/{id}/resource-ancestors": {
|
||||
"post": {
|
||||
"description": "Resolve the ancestor ExternalIDs that must be expanded to reveal the given (possibly deeply nested) resources in a lazily-loaded picker. Used to restore an existing selection when editing a data source.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"DataSource"
|
||||
],
|
||||
"summary": "Resolve resource ancestors",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Data source ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Resource IDs to resolve",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.resolveAncestorsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/datasource/{id}/resources": {
|
||||
"get": {
|
||||
"description": "List resources available for sync in the external system",
|
||||
"description": "List resources available for sync in the external system. Pass parent_id to lazily load the direct children of a resource (used for large hierarchical sources such as Feishu wiki).",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -2091,6 +2147,12 @@
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Parent resource ExternalID; empty lists the top level",
|
||||
"name": "parent_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4660,8 +4722,8 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "标签ID筛选",
|
||||
"name": "tag_id",
|
||||
"description": "标签ID筛选,逗号分隔(OR语义)",
|
||||
"name": "tag_ids",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
@@ -4823,6 +4885,12 @@
|
||||
"name": "enable_multimodel",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "分类ID列表,逗号分隔",
|
||||
"name": "tag_ids",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "处理配置JSON(KnowledgeProcessOverrides)",
|
||||
@@ -4956,8 +5024,11 @@
|
||||
"file_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"type": "string"
|
||||
"tag_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -5661,6 +5732,61 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/batch-reparse": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
},
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按 ID 列表批量重新解析单个知识库下的多个知识条目",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"知识管理"
|
||||
],
|
||||
"summary": "批量重新解析知识",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "批量重解析请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handler.batchReparseKnowledgeRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "任务已提交",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "权限不足",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/knowledge/image/{id}/{chunk_id}": {
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -5886,7 +6012,7 @@
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Search knowledge files by keyword. When agent_id is set (shared agent), scope is the agent's configured knowledge bases.",
|
||||
"description": "Search knowledge files by keyword. Pass recent=true without a keyword to browse recent files. When agent_id is set (shared agent), scope is the agent's configured knowledge bases.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -5927,6 +6053,12 @@
|
||||
"description": "Shared agent ID (search within agent's KB scope)",
|
||||
"name": "agent_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Return recent files when keyword is empty",
|
||||
"name": "recent",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -7452,6 +7584,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/oauth/callback": {
|
||||
"get": {
|
||||
"description": "接收授权服务器回调并完成 code 交换,随后重定向回前端",
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "MCP OAuth 回调",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "授权码",
|
||||
"name": "code",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "状态参数",
|
||||
"name": "state",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "授权错误码",
|
||||
"name": "error",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -7714,6 +7880,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/authorize-url": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "对使用 OAuth 的 MCP 服务执行发现与动态客户端注册,返回浏览器应跳转的授权地址(当前用户维度)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "发起 MCP OAuth 授权",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "{redirect_uri: string, frontend_redirect?: string}",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{authorization_url: string}",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/status": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "返回当前用户对指定 MCP 服务是否已完成 OAuth 授权",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "查询 MCP OAuth 授权状态",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{authorized: bool}",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/oauth/token": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "删除当前用户对指定 MCP 服务的 OAuth 令牌",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"MCP服务"
|
||||
],
|
||||
"summary": "撤销 MCP OAuth 授权",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "MCP 服务 ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/mcp-services/{id}/resources": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -14363,7 +14649,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"replica_number": {
|
||||
"description": "Milvus: in-memory replica count (LoadCollection)",
|
||||
"description": "Milvus LoadCollection / Tencent VectorDB CreateCollection replicas",
|
||||
"type": "integer"
|
||||
},
|
||||
"replication_factor": {
|
||||
@@ -14613,9 +14899,12 @@
|
||||
"description": "Summary status for async summary generation",
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"description": "Optional tag ID for categorization within a knowledge base",
|
||||
"type": "string"
|
||||
"tags": {
|
||||
"description": "Tags holds the tags associated with this knowledge (populated on query, not persisted directly).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeTag"
|
||||
}
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Tenant ID",
|
||||
@@ -14984,6 +15273,13 @@
|
||||
"graph_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"parser_engine_overrides": {
|
||||
"description": "ParserEngineOverrides passes key-value configuration to docreader parsers\n(e.g. pdf_force_scanned=true). Merged with tenant-level overrides in the\nparse pipeline; per-upload values take priority on conflict.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parser_engine_rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -14998,6 +15294,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.KnowledgeTag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Optional display color",
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"description": "Creation time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier of the tag (UUID)",
|
||||
"type": "string"
|
||||
},
|
||||
"knowledge_base_id": {
|
||||
"description": "Knowledge base ID that this tag belongs to",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Tag name, unique within the same knowledge base",
|
||||
"type": "string"
|
||||
},
|
||||
"seq_id": {
|
||||
"description": "SeqID is an auto-increment integer ID for external API usage",
|
||||
"type": "integer"
|
||||
},
|
||||
"sort_order": {
|
||||
"description": "Sort order within the same knowledge base",
|
||||
"type": "integer"
|
||||
},
|
||||
"tenant_id": {
|
||||
"description": "Tenant ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"description": "Last updated time",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.ListMembersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15130,17 +15467,51 @@
|
||||
"api_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_server_metadata_url": {
|
||||
"description": "AuthServerMetadataURL optionally pins the OAuth authorization server\nmetadata URL. When empty, the server is discovered automatically from\nthe MCP URL (RFC 9728 / RFC 8414).",
|
||||
"type": "string"
|
||||
},
|
||||
"auth_type": {
|
||||
"description": "AuthType selects the authentication strategy. Empty (\"\") is treated as\nnone for backward compatibility with rows that pre-date this field.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.MCPAuthType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"custom_headers": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"scopes": {
|
||||
"description": "Scopes are the OAuth scopes requested during authorization. Optional.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MCPAuthType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"",
|
||||
"api_key",
|
||||
"bearer",
|
||||
"oauth"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"MCPAuthNone",
|
||||
"MCPAuthAPIKey",
|
||||
"MCPAuthBearer",
|
||||
"MCPAuthOAuth"
|
||||
]
|
||||
},
|
||||
"github_com_Tencent_WeKnora_internal_types.MCPEnvVars": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -15272,8 +15643,11 @@
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_id": {
|
||||
"type": "string"
|
||||
"tag_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -19268,6 +19642,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.batchReparseKnowledgeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ids",
|
||||
"kb_id"
|
||||
],
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"process_config": {
|
||||
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeProcessOverrides"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.createInvitationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -19369,6 +19764,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.resolveAncestorsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handler.updateLastFAQImportResultDisplayStatusRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
+296
-15
@@ -1071,7 +1071,7 @@ definitions:
|
||||
description: ES, OpenSearch
|
||||
type: integer
|
||||
replica_number:
|
||||
description: 'Milvus: in-memory replica count (LoadCollection)'
|
||||
description: Milvus LoadCollection / Tencent VectorDB CreateCollection replicas
|
||||
type: integer
|
||||
replication_factor:
|
||||
description: 'Qdrant, Weaviate: number of replicas'
|
||||
@@ -1257,9 +1257,12 @@ definitions:
|
||||
summary_status:
|
||||
description: Summary status for async summary generation
|
||||
type: string
|
||||
tag_id:
|
||||
description: Optional tag ID for categorization within a knowledge base
|
||||
type: string
|
||||
tags:
|
||||
description: Tags holds the tags associated with this knowledge (populated
|
||||
on query, not persisted directly).
|
||||
items:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeTag'
|
||||
type: array
|
||||
tenant_id:
|
||||
description: Tenant ID
|
||||
type: integer
|
||||
@@ -1529,6 +1532,14 @@ definitions:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.ExtractConfig'
|
||||
graph_enabled:
|
||||
type: boolean
|
||||
parser_engine_overrides:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
ParserEngineOverrides passes key-value configuration to docreader parsers
|
||||
(e.g. pdf_force_scanned=true). Merged with tenant-level overrides in the
|
||||
parse pipeline; per-upload values take priority on conflict.
|
||||
type: object
|
||||
parser_engine_rules:
|
||||
items:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.ParserEngineRule'
|
||||
@@ -1538,6 +1549,36 @@ definitions:
|
||||
vlm_config:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.VLMConfig'
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.KnowledgeTag:
|
||||
properties:
|
||||
color:
|
||||
description: Optional display color
|
||||
type: string
|
||||
created_at:
|
||||
description: Creation time
|
||||
type: string
|
||||
id:
|
||||
description: Unique identifier of the tag (UUID)
|
||||
type: string
|
||||
knowledge_base_id:
|
||||
description: Knowledge base ID that this tag belongs to
|
||||
type: string
|
||||
name:
|
||||
description: Tag name, unique within the same knowledge base
|
||||
type: string
|
||||
seq_id:
|
||||
description: SeqID is an auto-increment integer ID for external API usage
|
||||
type: integer
|
||||
sort_order:
|
||||
description: Sort order within the same knowledge base
|
||||
type: integer
|
||||
tenant_id:
|
||||
description: Tenant ID
|
||||
type: integer
|
||||
updated_at:
|
||||
description: Last updated time
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.ListMembersResponse:
|
||||
properties:
|
||||
members:
|
||||
@@ -1633,13 +1674,42 @@ definitions:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
auth_server_metadata_url:
|
||||
description: |-
|
||||
AuthServerMetadataURL optionally pins the OAuth authorization server
|
||||
metadata URL. When empty, the server is discovered automatically from
|
||||
the MCP URL (RFC 9728 / RFC 8414).
|
||||
type: string
|
||||
auth_type:
|
||||
allOf:
|
||||
- $ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.MCPAuthType'
|
||||
description: |-
|
||||
AuthType selects the authentication strategy. Empty ("") is treated as
|
||||
none for backward compatibility with rows that pre-date this field.
|
||||
custom_headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
scopes:
|
||||
description: Scopes are the OAuth scopes requested during authorization. Optional.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
github_com_Tencent_WeKnora_internal_types.MCPAuthType:
|
||||
enum:
|
||||
- ""
|
||||
- api_key
|
||||
- bearer
|
||||
- oauth
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- MCPAuthNone
|
||||
- MCPAuthAPIKey
|
||||
- MCPAuthBearer
|
||||
- MCPAuthOAuth
|
||||
github_com_Tencent_WeKnora_internal_types.MCPEnvVars:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -1728,8 +1798,10 @@ definitions:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeProcessOverrides'
|
||||
status:
|
||||
type: string
|
||||
tag_id:
|
||||
type: string
|
||||
tag_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
@@ -4685,6 +4757,20 @@ definitions:
|
||||
success:
|
||||
type: boolean
|
||||
type: object
|
||||
internal_handler.batchReparseKnowledgeRequest:
|
||||
properties:
|
||||
ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
kb_id:
|
||||
type: string
|
||||
process_config:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_types.KnowledgeProcessOverrides'
|
||||
required:
|
||||
- ids
|
||||
- kb_id
|
||||
type: object
|
||||
internal_handler.createInvitationRequest:
|
||||
properties:
|
||||
email:
|
||||
@@ -4753,6 +4839,13 @@ definitions:
|
||||
- token
|
||||
- username
|
||||
type: object
|
||||
internal_handler.resolveAncestorsRequest:
|
||||
properties:
|
||||
resource_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
internal_handler.updateLastFAQImportResultDisplayStatusRequest:
|
||||
properties:
|
||||
display_status:
|
||||
@@ -6209,15 +6302,60 @@ paths:
|
||||
summary: Pause data source
|
||||
tags:
|
||||
- DataSource
|
||||
/datasource/{id}/resources:
|
||||
get:
|
||||
description: List resources available for sync in the external system
|
||||
/datasource/{id}/resource-ancestors:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Resolve the ancestor ExternalIDs that must be expanded to reveal
|
||||
the given (possibly deeply nested) resources in a lazily-loaded picker. Used
|
||||
to restore an existing selection when editing a data source.
|
||||
parameters:
|
||||
- description: Data source ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Resource IDs to resolve
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.resolveAncestorsRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Resolve resource ancestors
|
||||
tags:
|
||||
- DataSource
|
||||
/datasource/{id}/resources:
|
||||
get:
|
||||
description: List resources available for sync in the external system. Pass
|
||||
parent_id to lazily load the direct children of a resource (used for large
|
||||
hierarchical sources such as Feishu wiki).
|
||||
parameters:
|
||||
- description: Data source ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Parent resource ExternalID; empty lists the top level
|
||||
in: query
|
||||
name: parent_id
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -7875,9 +8013,9 @@ paths:
|
||||
in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
- description: 标签ID筛选
|
||||
- description: 标签ID筛选,逗号分隔(OR语义)
|
||||
in: query
|
||||
name: tag_id
|
||||
name: tag_ids
|
||||
type: string
|
||||
- description: 关键词搜索
|
||||
in: query
|
||||
@@ -7950,6 +8088,10 @@ paths:
|
||||
in: formData
|
||||
name: enable_multimodel
|
||||
type: boolean
|
||||
- description: 分类ID列表,逗号分隔
|
||||
in: formData
|
||||
name: tag_ids
|
||||
type: string
|
||||
- description: 处理配置JSON(KnowledgeProcessOverrides)
|
||||
in: formData
|
||||
name: process_config
|
||||
@@ -8035,8 +8177,10 @@ paths:
|
||||
type: string
|
||||
file_type:
|
||||
type: string
|
||||
tag_id:
|
||||
type: string
|
||||
tag_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
title:
|
||||
type: string
|
||||
url:
|
||||
@@ -8779,6 +8923,40 @@ paths:
|
||||
summary: 批量删除知识
|
||||
tags:
|
||||
- 知识管理
|
||||
/knowledge/batch-reparse:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 按 ID 列表批量重新解析单个知识库下的多个知识条目
|
||||
parameters:
|
||||
- description: 批量重解析请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handler.batchReparseKnowledgeRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 任务已提交
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
"403":
|
||||
description: 权限不足
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
security:
|
||||
- Bearer: []
|
||||
- ApiKeyAuth: []
|
||||
summary: 批量重新解析知识
|
||||
tags:
|
||||
- 知识管理
|
||||
/knowledge/image/{id}/{chunk_id}:
|
||||
put:
|
||||
consumes:
|
||||
@@ -8916,8 +9094,9 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Search knowledge files by keyword. When agent_id is set (shared
|
||||
agent), scope is the agent's configured knowledge bases.
|
||||
description: Search knowledge files by keyword. Pass recent=true without a keyword
|
||||
to browse recent files. When agent_id is set (shared agent), scope is the
|
||||
agent's configured knowledge bases.
|
||||
parameters:
|
||||
- description: Keyword to search
|
||||
in: query
|
||||
@@ -8939,6 +9118,10 @@ paths:
|
||||
in: query
|
||||
name: agent_id
|
||||
type: string
|
||||
- description: Return recent files when keyword is empty
|
||||
in: query
|
||||
name: recent
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -9860,6 +10043,82 @@ paths:
|
||||
summary: 移除 MCP 服务的单个凭据字段
|
||||
tags:
|
||||
- MCP服务
|
||||
/mcp-services/{id}/oauth/authorize-url:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 对使用 OAuth 的 MCP 服务执行发现与动态客户端注册,返回浏览器应跳转的授权地址(当前用户维度)
|
||||
parameters:
|
||||
- description: MCP 服务 ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: '{redirect_uri: string, frontend_redirect?: string}'
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: '{authorization_url: string}'
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 发起 MCP OAuth 授权
|
||||
tags:
|
||||
- MCP服务
|
||||
/mcp-services/{id}/oauth/status:
|
||||
get:
|
||||
description: 返回当前用户对指定 MCP 服务是否已完成 OAuth 授权
|
||||
parameters:
|
||||
- description: MCP 服务 ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: '{authorized: bool}'
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 查询 MCP OAuth 授权状态
|
||||
tags:
|
||||
- MCP服务
|
||||
/mcp-services/{id}/oauth/token:
|
||||
delete:
|
||||
description: 删除当前用户对指定 MCP 服务的 OAuth 令牌
|
||||
parameters:
|
||||
- description: MCP 服务 ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 撤销 MCP OAuth 授权
|
||||
tags:
|
||||
- MCP服务
|
||||
/mcp-services/{id}/resources:
|
||||
get:
|
||||
consumes:
|
||||
@@ -9992,6 +10251,28 @@ paths:
|
||||
summary: 获取MCP服务工具列表
|
||||
tags:
|
||||
- MCP服务
|
||||
/mcp-services/oauth/callback:
|
||||
get:
|
||||
description: 接收授权服务器回调并完成 code 交换,随后重定向回前端
|
||||
parameters:
|
||||
- description: 授权码
|
||||
in: query
|
||||
name: code
|
||||
type: string
|
||||
- description: 状态参数
|
||||
in: query
|
||||
name: state
|
||||
type: string
|
||||
- description: 授权错误码
|
||||
in: query
|
||||
name: error
|
||||
type: string
|
||||
responses:
|
||||
"302":
|
||||
description: Found
|
||||
summary: MCP OAuth 回调
|
||||
tags:
|
||||
- MCP服务
|
||||
/me/invitations:
|
||||
get:
|
||||
description: 返回当前登录用户的待接受邀请(默认仅 pending),用于头像入口和 /invitations 收件箱页。
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { onBeforeUnmount, reactive } from 'vue';
|
||||
|
||||
const TAG_EST_WIDTH = 82;
|
||||
const TAG_OVERFLOW_MIN = 32;
|
||||
|
||||
export function useTagChipsOverflow(datasetKey: 'tagItemId' | 'listTagItemId') {
|
||||
const tagVisibleLimit = reactive<Record<string, number>>({});
|
||||
const tagItemTotalMap = new Map<string, number>();
|
||||
const observedElements = new WeakSet<Element>();
|
||||
let tagChipsRO: ResizeObserver | null = null;
|
||||
|
||||
const computeLimit = (width: number, total: number) => {
|
||||
if (total <= 0) return 99;
|
||||
const maxFit = Math.floor((width - TAG_OVERFLOW_MIN) / TAG_EST_WIDTH);
|
||||
const limit = Math.max(1, Math.min(maxFit, total));
|
||||
return limit >= total ? 99 : limit;
|
||||
};
|
||||
|
||||
const ensureObserver = () => {
|
||||
if (tagChipsRO) return;
|
||||
tagChipsRO = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const target = entry.target as HTMLElement;
|
||||
const id = target.dataset[datasetKey];
|
||||
if (!id) continue;
|
||||
const total = tagItemTotalMap.get(id) ?? 0;
|
||||
tagVisibleLimit[id] = computeLimit(entry.contentRect.width, total);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function setupTagChipsObserver(el: Element | null, itemId: string, totalCount: number) {
|
||||
if (!el) return;
|
||||
const htmlEl = el as HTMLElement;
|
||||
htmlEl.dataset[datasetKey] = itemId;
|
||||
tagItemTotalMap.set(itemId, totalCount);
|
||||
ensureObserver();
|
||||
if (!observedElements.has(htmlEl)) {
|
||||
observedElements.add(htmlEl);
|
||||
tagChipsRO!.observe(htmlEl);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
tagVisibleLimit[itemId] = computeLimit(htmlEl.clientWidth, totalCount);
|
||||
});
|
||||
}
|
||||
|
||||
function getTagLimit(itemId: string): number {
|
||||
return tagVisibleLimit[itemId] ?? 99;
|
||||
}
|
||||
|
||||
function hasTagOverflow(itemId: string, total: number): boolean {
|
||||
return total > getTagLimit(itemId);
|
||||
}
|
||||
|
||||
function getOverflowCount(itemId: string, total: number): number {
|
||||
return Math.max(0, total - getTagLimit(itemId));
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
tagChipsRO?.disconnect();
|
||||
tagChipsRO = null;
|
||||
});
|
||||
|
||||
return {
|
||||
setupTagChipsObserver,
|
||||
getTagLimit,
|
||||
hasTagOverflow,
|
||||
getOverflowCount,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatFileSize, getFileIcon } from '@/utils/files';
|
||||
import { useTagChipsOverflow } from '@/composables/useTagChipsOverflow';
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
@@ -43,57 +44,12 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 动态标签可见数量(根据列宽自适应)
|
||||
const TAG_EST_WIDTH = 82;
|
||||
const TAG_OVERFLOW_MIN = 32;
|
||||
const tagVisibleLimit = reactive<Record<string, number>>({});
|
||||
const tagItemTotalMap = new Map<string, number>();
|
||||
let tagChipsRO: ResizeObserver | null = null;
|
||||
|
||||
function setupTagChipsObserver(el: Element | null, itemId: string, totalCount: number) {
|
||||
if (!el) return;
|
||||
const htmlEl = el as HTMLElement;
|
||||
htmlEl.dataset.listTagItemId = itemId;
|
||||
tagItemTotalMap.set(itemId, totalCount);
|
||||
if (!tagChipsRO) {
|
||||
tagChipsRO = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const target = entry.target as HTMLElement;
|
||||
const id = target.dataset.listTagItemId;
|
||||
if (!id) continue;
|
||||
const w = entry.contentRect.width;
|
||||
const total = tagItemTotalMap.get(id) ?? 0;
|
||||
if (total <= 0) { tagVisibleLimit[id] = 99; continue; }
|
||||
const maxFit = Math.floor((w - TAG_OVERFLOW_MIN) / TAG_EST_WIDTH);
|
||||
const limit = Math.max(1, Math.min(maxFit, total));
|
||||
tagVisibleLimit[id] = limit >= total ? 99 : limit;
|
||||
}
|
||||
});
|
||||
}
|
||||
tagChipsRO.observe(htmlEl);
|
||||
requestAnimationFrame(() => {
|
||||
const w = htmlEl.clientWidth;
|
||||
const total = totalCount;
|
||||
if (total <= 0) { tagVisibleLimit[itemId] = 99; return; }
|
||||
const maxFit = Math.floor((w - TAG_OVERFLOW_MIN) / TAG_EST_WIDTH);
|
||||
const limit = Math.max(1, Math.min(maxFit, total));
|
||||
tagVisibleLimit[itemId] = limit >= total ? 99 : limit;
|
||||
});
|
||||
}
|
||||
|
||||
function getTagLimit(itemId: string): number {
|
||||
return tagVisibleLimit[itemId] ?? 99;
|
||||
}
|
||||
|
||||
function hasTagOverflow(itemId: string, total: number): boolean {
|
||||
const limit = tagVisibleLimit[itemId] ?? 99;
|
||||
return total > limit;
|
||||
}
|
||||
|
||||
function getOverflowCount(itemId: string, total: number): number {
|
||||
const limit = tagVisibleLimit[itemId] ?? 99;
|
||||
return Math.max(0, total - limit);
|
||||
}
|
||||
const {
|
||||
setupTagChipsObserver,
|
||||
getTagLimit,
|
||||
hasTagOverflow,
|
||||
getOverflowCount,
|
||||
} = useTagChipsOverflow('listTagItemId');
|
||||
|
||||
const tagMap = computed(() => {
|
||||
const map: Record<string, Tag> = {};
|
||||
@@ -246,15 +202,8 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
<div ref="stickySentinel" class="doc-list-sticky-sentinel" aria-hidden="true"></div>
|
||||
<div class="doc-list-header" :class="{ 'is-stuck': headerStuck }" role="row">
|
||||
<div class="cell cell-check" role="columnheader" @click.stop>
|
||||
<t-checkbox
|
||||
class="doc-list-check"
|
||||
size="small"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
:disabled="!items.length"
|
||||
:title="t('knowledgeBase.selectAll')"
|
||||
@change="onHeaderCheckboxChange"
|
||||
/>
|
||||
<t-checkbox class="doc-list-check" size="small" :checked="allSelected" :indeterminate="someSelected"
|
||||
:disabled="!items.length" :title="t('knowledgeBase.selectAll')" @change="onHeaderCheckboxChange" />
|
||||
</div>
|
||||
<div class="cell cell-name" role="columnheader">{{ t('knowledgeBase.columnName') }}</div>
|
||||
<div class="cell cell-tag" role="columnheader">{{ t('knowledgeBase.columnTag') }}</div>
|
||||
@@ -266,23 +215,12 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
</div>
|
||||
|
||||
<div class="doc-list-body">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="doc-list-row"
|
||||
:class="{ selected: selectedIds.has(item.id), 'menu-open': moreOpen === item.id }"
|
||||
:data-select-id="item.id"
|
||||
role="row"
|
||||
@click="emit('open', item)"
|
||||
>
|
||||
<div v-for="item in items" :key="item.id" class="doc-list-row"
|
||||
:class="{ selected: selectedIds.has(item.id), 'menu-open': moreOpen === item.id }" :data-select-id="item.id"
|
||||
role="row" @click="emit('open', item)">
|
||||
<div class="cell cell-check" @click.stop>
|
||||
<t-checkbox
|
||||
class="doc-list-check"
|
||||
size="small"
|
||||
:checked="selectedIds.has(item.id)"
|
||||
:title="item.file_name"
|
||||
@change="(c, ctx) => onRowCheckboxChange(item, c, ctx)"
|
||||
/>
|
||||
<t-checkbox class="doc-list-check" size="small" :checked="selectedIds.has(item.id)" :title="item.file_name"
|
||||
@change="(c, ctx) => onRowCheckboxChange(item, c, ctx)" />
|
||||
</div>
|
||||
|
||||
<div class="cell cell-name">
|
||||
@@ -291,54 +229,28 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
</span>
|
||||
<div class="row-file-text">
|
||||
<span class="row-file-name" :title="item.file_name">{{ item.file_name }}</span>
|
||||
<span
|
||||
v-if="item.description"
|
||||
class="row-file-desc"
|
||||
:title="item.description"
|
||||
>{{ item.description }}</span>
|
||||
<span v-if="item.description" class="row-file-desc" :title="item.description">{{ item.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="cell cell-tag">
|
||||
<template v-if="item.tags && item.tags.length > 0">
|
||||
<t-tooltip
|
||||
v-if="hasTagOverflow(item.id, item.tags.length)"
|
||||
:content="item.tags.map((t: any) => t.name).join(', ')"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
class="row-tag-chips"
|
||||
:ref="(el: any) => setupTagChipsObserver(el, item.id, item.tags.length)"
|
||||
:class="{ 'is-clickable': canEdit }"
|
||||
@click.stop="canEdit && emit('tag-edit', item)"
|
||||
>
|
||||
<t-tag
|
||||
v-for="tag in item.tags.slice(0, getTagLimit(item.id))"
|
||||
:key="tag.id"
|
||||
size="small"
|
||||
variant="light-outline"
|
||||
class="row-tag"
|
||||
>
|
||||
<t-tooltip v-if="hasTagOverflow(item.id, item.tags.length)"
|
||||
:content="item.tags.map((t: any) => t.name).join(', ')" placement="top">
|
||||
<div class="row-tag-chips" :ref="(el: any) => setupTagChipsObserver(el, item.id, item.tags.length)"
|
||||
:class="{ 'is-clickable': canEdit }" @click.stop="canEdit && emit('tag-edit', item)">
|
||||
<t-tag v-for="tag in item.tags.slice(0, getTagLimit(item.id))" :key="tag.id" size="small"
|
||||
variant="light-outline" class="row-tag">
|
||||
{{ tag.name }}
|
||||
</t-tag>
|
||||
<span class="row-tag-overflow">+{{ getOverflowCount(item.id, item.tags.length) }}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
<div
|
||||
v-else
|
||||
class="row-tag-chips"
|
||||
:ref="(el: any) => setupTagChipsObserver(el, item.id, item.tags.length)"
|
||||
:class="{ 'is-clickable': canEdit }"
|
||||
@click.stop="canEdit && emit('tag-edit', item)"
|
||||
>
|
||||
<t-tag
|
||||
v-for="tag in item.tags.slice(0, getTagLimit(item.id))"
|
||||
:key="tag.id"
|
||||
size="small"
|
||||
variant="light-outline"
|
||||
class="row-tag"
|
||||
>
|
||||
<div v-else class="row-tag-chips" :ref="(el: any) => setupTagChipsObserver(el, item.id, item.tags.length)"
|
||||
:class="{ 'is-clickable': canEdit }" @click.stop="canEdit && emit('tag-edit', item)">
|
||||
<t-tag v-for="tag in item.tags.slice(0, getTagLimit(item.id))" :key="tag.id" size="small"
|
||||
variant="light-outline" class="row-tag">
|
||||
{{ tag.name }}
|
||||
</t-tag>
|
||||
</div>
|
||||
@@ -359,18 +271,11 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
|
||||
<div class="cell cell-status">
|
||||
<template v-if="statusByRow.get(item.id) as StatusInfo | undefined">
|
||||
<t-tag
|
||||
v-if="statusByRow.get(item.id)!.label !== '--'"
|
||||
size="small"
|
||||
:theme="statusByRow.get(item.id)!.theme"
|
||||
variant="light-outline"
|
||||
class="row-status-tag"
|
||||
>
|
||||
<t-tag v-if="statusByRow.get(item.id)!.label !== '--'" size="small" :theme="statusByRow.get(item.id)!.theme"
|
||||
variant="light-outline" class="row-status-tag">
|
||||
<template v-if="statusByRow.get(item.id)!.icon" #icon>
|
||||
<t-icon
|
||||
:name="statusByRow.get(item.id)!.icon!"
|
||||
:class="{ 'icon-spin': statusByRow.get(item.id)!.spin }"
|
||||
/>
|
||||
<t-icon :name="statusByRow.get(item.id)!.icon!"
|
||||
:class="{ 'icon-spin': statusByRow.get(item.id)!.spin }" />
|
||||
</template>
|
||||
{{ statusByRow.get(item.id)!.label }}
|
||||
</t-tag>
|
||||
@@ -383,56 +288,37 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
</div>
|
||||
|
||||
<div class="cell cell-actions" v-if="canEdit" @click.stop>
|
||||
<t-popup
|
||||
placement="bottom-right"
|
||||
trigger="click"
|
||||
destroy-on-close
|
||||
:on-visible-change="(v: boolean) => onMoreVisible(item.id, v)"
|
||||
>
|
||||
<button class="row-more-btn" :class="{ active: moreOpen === item.id }" type="button" :aria-label="t('knowledgeBase.columnActions')">
|
||||
<t-popup placement="bottom-right" trigger="click" destroy-on-close
|
||||
:on-visible-change="(v: boolean) => onMoreVisible(item.id, v)">
|
||||
<button class="row-more-btn" :class="{ active: moreOpen === item.id }" type="button"
|
||||
:aria-label="t('knowledgeBase.columnActions')">
|
||||
<t-icon name="more" size="16px" />
|
||||
</button>
|
||||
<template #content>
|
||||
<div class="row-menu">
|
||||
<div
|
||||
v-if="item.type === 'manual'"
|
||||
class="row-menu-item"
|
||||
@click.stop="handleAction('edit', item)"
|
||||
>
|
||||
<div v-if="item.type === 'manual'" class="row-menu-item" @click.stop="handleAction('edit', item)">
|
||||
<t-icon class="icon" name="edit" />
|
||||
<span>{{ t('knowledgeBase.editDocument') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isParseInFlight(item)"
|
||||
class="row-menu-item"
|
||||
@click.stop="handleAction('reparse', item)"
|
||||
>
|
||||
<div v-if="isParseInFlight(item)" class="row-menu-item" @click.stop="handleAction('reparse', item)">
|
||||
<t-icon class="icon" name="refresh" />
|
||||
<span>{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||||
</div>
|
||||
<t-popconfirm
|
||||
v-else
|
||||
theme="warning"
|
||||
<t-popconfirm v-else theme="warning"
|
||||
:content="t('knowledgeBase.rebuildConfirm', { fileName: item.file_name || '' })"
|
||||
:confirm-btn="{ content: t('common.confirm'), theme: 'primary' }"
|
||||
:cancel-btn="{ content: t('common.cancel') }"
|
||||
placement="left"
|
||||
@confirm="handleAction('reparse', item)"
|
||||
>
|
||||
:cancel-btn="{ content: t('common.cancel') }" placement="left"
|
||||
@confirm="handleAction('reparse', item)">
|
||||
<div class="row-menu-item" @click.stop>
|
||||
<t-icon class="icon" name="refresh" />
|
||||
<span>{{ t('knowledgeBase.rebuildDocument') }}</span>
|
||||
</div>
|
||||
</t-popconfirm>
|
||||
<t-popconfirm
|
||||
v-if="canCancelParse(item)"
|
||||
theme="warning"
|
||||
<t-popconfirm v-if="canCancelParse(item)" theme="warning"
|
||||
:content="t('knowledgeBase.cancelParseConfirmBody', { title: item.file_name || item.id })"
|
||||
:confirm-btn="{ content: t('knowledgeBase.cancelParse'), theme: 'danger' }"
|
||||
:cancel-btn="{ content: t('common.cancel') }"
|
||||
placement="left"
|
||||
@confirm="handleAction('cancel-parse', item)"
|
||||
>
|
||||
:cancel-btn="{ content: t('common.cancel') }" placement="left"
|
||||
@confirm="handleAction('cancel-parse', item)">
|
||||
<div class="row-menu-item danger" @click.stop>
|
||||
<t-icon class="icon" name="close-circle" />
|
||||
<span>{{ t('knowledgeBase.cancelParse') }}</span>
|
||||
@@ -442,14 +328,11 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
<t-icon class="icon" name="swap" />
|
||||
<span>{{ t('knowledgeBase.moveDocument') }}</span>
|
||||
</div>
|
||||
<t-popconfirm
|
||||
theme="warning"
|
||||
<t-popconfirm theme="warning"
|
||||
:content="t('knowledgeBase.confirmDeleteDocument', { fileName: item.file_name || '' })"
|
||||
:confirm-btn="{ content: t('knowledgeBase.confirmDelete'), theme: 'danger' }"
|
||||
:cancel-btn="{ content: t('common.cancel') }"
|
||||
placement="left"
|
||||
@confirm="handleAction('delete', item)"
|
||||
>
|
||||
:cancel-btn="{ content: t('common.cancel') }" placement="left"
|
||||
@confirm="handleAction('delete', item)">
|
||||
<div class="row-menu-item danger" @click.stop>
|
||||
<t-icon class="icon" name="delete" />
|
||||
<span>{{ t('knowledgeBase.deleteDocument') }}</span>
|
||||
@@ -470,6 +353,7 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -493,14 +377,14 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
.doc-list-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
44px // checkbox
|
||||
minmax(260px, 2.6fr) // name
|
||||
minmax(100px, 0.9fr) // tag
|
||||
minmax(96px, 0.8fr) // source
|
||||
96px // size
|
||||
minmax(96px, 0.7fr) // status
|
||||
140px // updated_at
|
||||
48px; // actions
|
||||
44px // checkbox
|
||||
minmax(260px, 2.6fr) // name
|
||||
minmax(100px, 0.9fr) // tag
|
||||
minmax(96px, 0.8fr) // source
|
||||
96px // size
|
||||
minmax(96px, 0.7fr) // status
|
||||
140px // updated_at
|
||||
48px; // actions
|
||||
align-items: center;
|
||||
column-gap: 0;
|
||||
padding: 0 16px;
|
||||
@@ -572,8 +456,14 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 0 8px;
|
||||
&:first-child { padding-left: 0; }
|
||||
&:last-child { padding-right: 0; }
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-check {
|
||||
@@ -683,6 +573,7 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
|
||||
.row-tag {
|
||||
max-width: 100%;
|
||||
|
||||
:deep(.t-tag__text) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -757,11 +648,15 @@ const handleAction = (action: 'edit' | 'reparse' | 'cancel-parse' | 'move' | 'de
|
||||
.row-status-tag :deep(.t-icon) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon-spin {
|
||||
animation: doc-list-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes doc-list-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.row-more-btn {
|
||||
|
||||
@@ -21,16 +21,21 @@ func (r *knowledgeRepository) SetKnowledgeTags(
|
||||
Delete(&types.KnowledgeTagRelation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Insert new relations
|
||||
// Insert new relations (skip empty and duplicate IDs)
|
||||
if len(tagIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(tagIDs))
|
||||
now := time.Now()
|
||||
relations := make([]types.KnowledgeTagRelation, 0, len(tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
if tagID == "" {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[tagID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[tagID] = struct{}{}
|
||||
relations = append(relations, types.KnowledgeTagRelation{
|
||||
KnowledgeID: knowledgeID,
|
||||
TagID: tagID,
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const knowledgeTagTestDDL = `
|
||||
CREATE TABLE IF NOT EXISTS knowledges (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
knowledge_base_id VARCHAR(36) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL DEFAULT 'file',
|
||||
title VARCHAR(255) NOT NULL DEFAULT '',
|
||||
parse_status VARCHAR(50) NOT NULL DEFAULT 'completed',
|
||||
deleted_at DATETIME
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS knowledge_tags (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
seq_id INTEGER NOT NULL,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
knowledge_base_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
color VARCHAR(32),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS knowledge_tag_relations (
|
||||
knowledge_id VARCHAR(36) NOT NULL,
|
||||
tag_id VARCHAR(36) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (knowledge_id, tag_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
knowledge_base_id VARCHAR(36) NOT NULL,
|
||||
tag_id VARCHAR(36),
|
||||
deleted_at DATETIME
|
||||
);
|
||||
`
|
||||
|
||||
func setupKnowledgeTagTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db := setupKnowledgeTestDB(t)
|
||||
require.NoError(t, db.Exec(knowledgeTagTestDDL).Error)
|
||||
return db
|
||||
}
|
||||
|
||||
func seedKnowledgeTagFixture(t *testing.T, db *gorm.DB) (kbID, knowledgeID, tagA, tagB string) {
|
||||
t.Helper()
|
||||
kbID = uuid.New().String()
|
||||
knowledgeID = uuid.New().String()
|
||||
tagA = uuid.New().String()
|
||||
tagB = uuid.New().String()
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledges (id, tenant_id, knowledge_base_id, type, title, parse_status)
|
||||
VALUES (?, 1, ?, 'file', 'doc', 'completed')
|
||||
`, knowledgeID, kbID).Error)
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledge_tags (id, seq_id, tenant_id, knowledge_base_id, name)
|
||||
VALUES (?, 1, 1, ?, 'alpha'), (?, 2, 1, ?, 'beta')
|
||||
`, tagA, kbID, tagB, kbID).Error)
|
||||
return kbID, knowledgeID, tagA, tagB
|
||||
}
|
||||
|
||||
func TestSetKnowledgeTags_DedupesAndReplaces(t *testing.T) {
|
||||
db := setupKnowledgeTagTestDB(t)
|
||||
repo := &knowledgeRepository{db: db}
|
||||
_, knowledgeID, tagA, tagB := seedKnowledgeTagFixture(t, db)
|
||||
ctx := context.Background()
|
||||
|
||||
err := repo.SetKnowledgeTags(ctx, knowledgeID, []string{tagA, tagA, tagB})
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.KnowledgeTagRelation{}).
|
||||
Where("knowledge_id = ?", knowledgeID).Count(&count).Error)
|
||||
assert.Equal(t, int64(2), count)
|
||||
|
||||
err = repo.SetKnowledgeTags(ctx, knowledgeID, []string{tagB})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Model(&types.KnowledgeTagRelation{}).
|
||||
Where("knowledge_id = ?", knowledgeID).Count(&count).Error)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
err = repo.SetKnowledgeTags(ctx, knowledgeID, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Model(&types.KnowledgeTagRelation{}).
|
||||
Where("knowledge_id = ?", knowledgeID).Count(&count).Error)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestGetKnowledgeTags_ReturnsTagDetails(t *testing.T) {
|
||||
db := setupKnowledgeTagTestDB(t)
|
||||
repo := &knowledgeRepository{db: db}
|
||||
_, knowledgeID, tagA, tagB := seedKnowledgeTagFixture(t, db)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, repo.SetKnowledgeTags(ctx, knowledgeID, []string{tagA, tagB}))
|
||||
|
||||
tagMap, err := repo.GetKnowledgeTags(ctx, []string{knowledgeID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tagMap[knowledgeID], 2)
|
||||
|
||||
names := map[string]bool{}
|
||||
for _, tag := range tagMap[knowledgeID] {
|
||||
names[tag.Name] = true
|
||||
}
|
||||
assert.True(t, names["alpha"])
|
||||
assert.True(t, names["beta"])
|
||||
}
|
||||
|
||||
func TestDeleteKnowledgeTagRelations(t *testing.T) {
|
||||
db := setupKnowledgeTagTestDB(t)
|
||||
repo := &knowledgeRepository{db: db}
|
||||
_, knowledgeID, tagA, _ := seedKnowledgeTagFixture(t, db)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, repo.SetKnowledgeTags(ctx, knowledgeID, []string{tagA}))
|
||||
require.NoError(t, repo.DeleteKnowledgeTagRelations(ctx, knowledgeID))
|
||||
|
||||
var count int64
|
||||
require.NoError(t, db.Model(&types.KnowledgeTagRelation{}).
|
||||
Where("knowledge_id = ?", knowledgeID).Count(&count).Error)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestApplyKnowledgeListFilter_TagIDsOrSemantics(t *testing.T) {
|
||||
db := setupKnowledgeTagTestDB(t)
|
||||
repo := &knowledgeRepository{db: db}
|
||||
ctx := context.Background()
|
||||
|
||||
kbID := uuid.New().String()
|
||||
docA := uuid.New().String()
|
||||
docB := uuid.New().String()
|
||||
docC := uuid.New().String()
|
||||
tagA := uuid.New().String()
|
||||
tagB := uuid.New().String()
|
||||
|
||||
for _, row := range []struct{ id, title string }{
|
||||
{docA, "a"}, {docB, "b"}, {docC, "c"},
|
||||
} {
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledges (id, tenant_id, knowledge_base_id, type, title, parse_status)
|
||||
VALUES (?, 1, ?, 'file', ?, 'completed')
|
||||
`, row.id, kbID, row.title).Error)
|
||||
}
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledge_tags (id, seq_id, tenant_id, knowledge_base_id, name)
|
||||
VALUES (?, 1, 1, ?, 't-a'), (?, 2, 1, ?, 't-b')
|
||||
`, tagA, kbID, tagB, kbID).Error)
|
||||
require.NoError(t, repo.SetKnowledgeTags(ctx, docA, []string{tagA}))
|
||||
require.NoError(t, repo.SetKnowledgeTags(ctx, docB, []string{tagB}))
|
||||
require.NoError(t, repo.SetKnowledgeTags(ctx, docC, []string{tagA, tagB}))
|
||||
|
||||
query := db.WithContext(ctx).Model(&types.Knowledge{}).
|
||||
Where("tenant_id = ? AND knowledge_base_id = ?", uint64(1), kbID)
|
||||
query = applyKnowledgeListFilter(query, types.KnowledgeListFilter{
|
||||
TagIDs: []string{tagA, tagB},
|
||||
})
|
||||
|
||||
var ids []string
|
||||
require.NoError(t, query.Pluck("id", &ids).Error)
|
||||
assert.ElementsMatch(t, []string{docA, docB, docC}, ids)
|
||||
}
|
||||
|
||||
func TestBatchCountReferences_ScopedToKnowledgeBase(t *testing.T) {
|
||||
db := setupKnowledgeTagTestDB(t)
|
||||
knowledgeRepo := &knowledgeRepository{db: db}
|
||||
tagRepo := &knowledgeTagRepository{db: db}
|
||||
ctx := context.Background()
|
||||
|
||||
kb1 := uuid.New().String()
|
||||
kb2 := uuid.New().String()
|
||||
doc1 := uuid.New().String()
|
||||
doc2 := uuid.New().String()
|
||||
tag1 := uuid.New().String()
|
||||
tag2 := uuid.New().String()
|
||||
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledges (id, tenant_id, knowledge_base_id, type, title, parse_status)
|
||||
VALUES (?, 1, ?, 'file', 'kb1-doc', 'completed'), (?, 1, ?, 'file', 'kb2-doc', 'completed')
|
||||
`, doc1, kb1, doc2, kb2).Error)
|
||||
require.NoError(t, db.Exec(`
|
||||
INSERT INTO knowledge_tags (id, seq_id, tenant_id, knowledge_base_id, name)
|
||||
VALUES (?, 1, 1, ?, 'tag-kb1'), (?, 2, 1, ?, 'tag-kb2')
|
||||
`, tag1, kb1, tag2, kb2).Error)
|
||||
require.NoError(t, knowledgeRepo.SetKnowledgeTags(ctx, doc1, []string{tag1}))
|
||||
// Stale relation: doc in kb2 still linked to tag1 (simulates pre-fix move bug)
|
||||
require.NoError(t, knowledgeRepo.SetKnowledgeTags(ctx, doc2, []string{tag1}))
|
||||
|
||||
counts, err := tagRepo.BatchCountReferences(ctx, 1, kb1, []string{tag1})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), counts[tag1].KnowledgeCount)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (r *knowledgeTagRepository) CountReferences(
|
||||
) (knowledgeCount int64, chunkCount int64, err error) {
|
||||
if err = r.db.WithContext(ctx).
|
||||
Table("knowledge_tag_relations AS ktr").
|
||||
Joins("JOIN knowledges AS k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL").
|
||||
Joins("JOIN knowledges AS k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL AND k.tenant_id = ? AND k.knowledge_base_id = ?", tenantID, kbID).
|
||||
Where("ktr.tag_id = ?", tagID).
|
||||
Count(&knowledgeCount).Error; err != nil {
|
||||
return
|
||||
@@ -193,7 +193,7 @@ func (r *knowledgeTagRepository) BatchCountReferences(
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("knowledge_tag_relations AS ktr").
|
||||
Select("ktr.tag_id, COUNT(*) as count").
|
||||
Joins("JOIN knowledges AS k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL").
|
||||
Joins("JOIN knowledges AS k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL AND k.tenant_id = ? AND k.knowledge_base_id = ?", tenantID, kbID).
|
||||
Where("ktr.tag_id IN (?)", tagIDs).
|
||||
Group("ktr.tag_id").
|
||||
Find(&knowledgeCounts).Error; err != nil {
|
||||
@@ -230,7 +230,7 @@ func (r *knowledgeTagRepository) DeleteUnusedTags(ctx context.Context, tenantID
|
||||
// Delete tags that have no references in both knowledges and chunks tables (excluding soft-deleted records)
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("tenant_id = ? AND knowledge_base_id = ?", tenantID, kbID).
|
||||
Where("id NOT IN (SELECT DISTINCT ktr.tag_id FROM knowledge_tag_relations ktr JOIN knowledges k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL)").
|
||||
Where("id NOT IN (SELECT DISTINCT ktr.tag_id FROM knowledge_tag_relations ktr JOIN knowledges k ON ktr.knowledge_id = k.id AND k.deleted_at IS NULL AND k.tenant_id = ? AND k.knowledge_base_id = ?)", tenantID, kbID).
|
||||
Where("id NOT IN (SELECT DISTINCT tag_id FROM chunks WHERE tenant_id = ? AND knowledge_base_id = ? AND tag_id IS NOT NULL AND tag_id != '' AND deleted_at IS NULL)", tenantID, kbID).
|
||||
Delete(&types.KnowledgeTag{})
|
||||
return result.RowsAffected, result.Error
|
||||
|
||||
@@ -661,6 +661,84 @@ func (s *knowledgeService) SetKnowledgeTags(ctx context.Context, knowledgeID str
|
||||
return s.repo.SetKnowledgeTags(ctx, knowledgeID, tagIDs)
|
||||
}
|
||||
|
||||
// validateKnowledgeTagIDs ensures every tag exists and belongs to the given knowledge base.
|
||||
func (s *knowledgeService) validateKnowledgeTagIDs(
|
||||
ctx context.Context,
|
||||
tenantID uint64,
|
||||
kbID string,
|
||||
tagIDs []string,
|
||||
) error {
|
||||
unique := make([]string, 0, len(tagIDs))
|
||||
seen := make(map[string]struct{}, len(tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
if tagID == "" {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[tagID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[tagID] = struct{}{}
|
||||
unique = append(unique, tagID)
|
||||
}
|
||||
if len(unique) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tags, err := s.tagRepo.GetByIDs(ctx, tenantID, unique)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagMap := make(map[string]*types.KnowledgeTag, len(tags))
|
||||
for _, tag := range tags {
|
||||
tagMap[tag.ID] = tag
|
||||
}
|
||||
for _, tagID := range unique {
|
||||
tag, ok := tagMap[tagID]
|
||||
if !ok {
|
||||
return werrors.NewBadRequestError(fmt.Sprintf("标签 %s 不存在", tagID))
|
||||
}
|
||||
if tag.KnowledgeBaseID != kbID {
|
||||
return werrors.NewBadRequestError("标签不属于当前知识库")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// attachTagsToKnowledge populates knowledge.Tags from the join table.
|
||||
func (s *knowledgeService) attachTagsToKnowledge(ctx context.Context, knowledge *types.Knowledge) {
|
||||
if knowledge == nil {
|
||||
return
|
||||
}
|
||||
tagMap, err := s.repo.GetKnowledgeTags(ctx, []string{knowledge.ID})
|
||||
if err != nil {
|
||||
logger.Warnf(ctx, "Failed to load tags for knowledge %s: %v", knowledge.ID, err)
|
||||
return
|
||||
}
|
||||
if tags, ok := tagMap[knowledge.ID]; ok {
|
||||
knowledge.Tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
// setAndAttachKnowledgeTags validates, persists, and populates tags on a knowledge entry.
|
||||
func (s *knowledgeService) setAndAttachKnowledgeTags(
|
||||
ctx context.Context,
|
||||
tenantID uint64,
|
||||
kbID string,
|
||||
knowledge *types.Knowledge,
|
||||
tagIDs []string,
|
||||
) error {
|
||||
if err := s.validateKnowledgeTagIDs(ctx, tenantID, kbID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tagIDs) > 0 {
|
||||
if err := s.repo.SetKnowledgeTags(ctx, knowledge.ID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.attachTagsToKnowledge(ctx, knowledge)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKnowledgeTags returns tags for multiple knowledge IDs.
|
||||
func (s *knowledgeService) GetKnowledgeTags(ctx context.Context, knowledgeIDs []string) (map[string][]*types.KnowledgeTag, error) {
|
||||
return s.repo.GetKnowledgeTags(ctx, knowledgeIDs)
|
||||
@@ -675,16 +753,8 @@ func (s *knowledgeService) UpdateKnowledgeTag(ctx context.Context, knowledgeID s
|
||||
}
|
||||
|
||||
// Validate all tag IDs
|
||||
if len(tagIDs) > 0 {
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := s.tagRepo.GetByID(ctx, tenantID, tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.KnowledgeBaseID != knowledge.KnowledgeBaseID {
|
||||
return werrors.NewBadRequestError("标签不属于当前知识库")
|
||||
}
|
||||
}
|
||||
if err := s.validateKnowledgeTagIDs(ctx, tenantID, knowledge.KnowledgeBaseID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.repo.SetKnowledgeTags(ctx, knowledgeID, tagIDs)
|
||||
@@ -746,12 +816,12 @@ func (s *knowledgeService) UpdateKnowledgeTagBatch(ctx context.Context, authoriz
|
||||
for tagID := range tagIDSet {
|
||||
tagIDList = append(tagIDList, tagID)
|
||||
}
|
||||
for _, tagID := range tagIDList {
|
||||
tag, err := s.tagRepo.GetByID(ctx, tenantID, tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagMap[tagID] = tag
|
||||
tags, err := s.tagRepo.GetByIDs(ctx, tenantID, tagIDList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
tagMap[tag.ID] = tag
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1098,7 +1098,10 @@ func (s *knowledgeService) moveKnowledgeReuseVectors(
|
||||
return fmt.Errorf("failed to move chunks: %w", err)
|
||||
}
|
||||
|
||||
// 4. Update knowledge record
|
||||
// 4. Update knowledge record (tags are KB-scoped; clear relations before moving)
|
||||
if err := s.repo.DeleteKnowledgeTagRelations(ctx, knowledge.ID); err != nil {
|
||||
return fmt.Errorf("failed to clear knowledge tag relations: %w", err)
|
||||
}
|
||||
knowledge.KnowledgeBaseID = targetKB.ID
|
||||
knowledge.ParseStatus = types.ParseStatusCompleted
|
||||
knowledge.UpdatedAt = time.Now()
|
||||
@@ -1123,7 +1126,10 @@ func (s *knowledgeService) moveKnowledgeReparse(
|
||||
// Continue - partial cleanup is acceptable
|
||||
}
|
||||
|
||||
// 2. Update knowledge to belong to target KB
|
||||
// 2. Update knowledge to belong to target KB (tags are KB-scoped; clear relations)
|
||||
if err := s.repo.DeleteKnowledgeTagRelations(ctx, knowledge.ID); err != nil {
|
||||
return fmt.Errorf("failed to clear knowledge tag relations: %w", err)
|
||||
}
|
||||
knowledge.KnowledgeBaseID = targetKB.ID
|
||||
knowledge.EmbeddingModelID = targetKB.EmbeddingModelID
|
||||
knowledge.ParseStatus = types.ParseStatusPending
|
||||
|
||||
@@ -231,11 +231,9 @@ func (s *knowledgeService) CreateKnowledgeFromFile(ctx context.Context,
|
||||
}
|
||||
|
||||
// Set tag relations
|
||||
if len(tagIDs) > 0 {
|
||||
if err := s.repo.SetKnowledgeTags(ctx, knowledge.ID, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.setAndAttachKnowledgeTags(ctx, tenantID, kbID, knowledge, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enqueue document processing task to Asynq
|
||||
@@ -418,11 +416,9 @@ func (s *knowledgeService) CreateKnowledgeFromURL(ctx context.Context,
|
||||
}
|
||||
|
||||
// Set tag relations
|
||||
if len(tagIDs) > 0 {
|
||||
if err := s.repo.SetKnowledgeTags(ctx, knowledge.ID, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.setAndAttachKnowledgeTags(ctx, tenantID, kbID, knowledge, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enqueue URL processing task to Asynq
|
||||
@@ -658,11 +654,9 @@ func (s *knowledgeService) createKnowledgeFromFileURL(
|
||||
}
|
||||
|
||||
// Set tag relations
|
||||
if len(tagIDs) > 0 {
|
||||
if err := s.repo.SetKnowledgeTags(ctx, knowledge.ID, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.setAndAttachKnowledgeTags(ctx, tenantID, kbID, knowledge, tagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build async task payload
|
||||
@@ -813,11 +807,9 @@ func (s *knowledgeService) CreateKnowledgeFromManual(ctx context.Context,
|
||||
}
|
||||
|
||||
// Set tag relations
|
||||
if len(payload.TagIDs) > 0 {
|
||||
if err := s.repo.SetKnowledgeTags(ctx, knowledge.ID, payload.TagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.setAndAttachKnowledgeTags(ctx, tenantID, kbID, knowledge, payload.TagIDs); err != nil {
|
||||
logger.Errorf(ctx, "Failed to set knowledge tags, knowledge ID: %s, error: %v", knowledge.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status == types.ManualKnowledgeStatusPublish {
|
||||
|
||||
@@ -185,6 +185,9 @@ func (s *knowledgeService) DeleteKnowledge(ctx context.Context, id string) error
|
||||
if err = wg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.repo.DeleteKnowledgeTagRelations(ctx, id); err != nil {
|
||||
logger.Warnf(ctx, "Failed to delete tag relations for knowledge %s: %v", id, err)
|
||||
}
|
||||
// Delete the knowledge entry itself from the database
|
||||
return s.repo.DeleteKnowledge(ctx, ctx.Value(types.TenantIDContextKey).(uint64), id)
|
||||
}
|
||||
@@ -601,6 +604,11 @@ func (s *knowledgeService) DeleteKnowledgeList(ctx context.Context, ids []string
|
||||
if err = wg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, knowledgeID := range ids {
|
||||
if err := s.repo.DeleteKnowledgeTagRelations(ctx, knowledgeID); err != nil {
|
||||
logger.Warnf(ctx, "Failed to delete tag relations for knowledge %s: %v", knowledgeID, err)
|
||||
}
|
||||
}
|
||||
// 6. Delete the knowledge entry itself from the database
|
||||
return s.repo.DeleteKnowledgeList(ctx, tenantInfo.ID, ids)
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ func (h *KnowledgeHandler) enqueueKnowledgeListReparse(
|
||||
// @Param fileName formData string false "自定义文件名"
|
||||
// @Param metadata formData string false "元数据JSON"
|
||||
// @Param enable_multimodel formData bool false "启用多模态处理"
|
||||
// @Param tag_ids formData string false "分类ID列表,逗号分隔"
|
||||
// @Param process_config formData string false "处理配置JSON(KnowledgeProcessOverrides)"
|
||||
// @Success 200 {object} map[string]interface{} "创建的知识"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
@@ -400,7 +401,7 @@ func (h *KnowledgeHandler) CreateKnowledgeFromFile(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param request body object{url=string,file_name=string,file_type=string,enable_multimodel=bool,title=string,tag_id=string} true "URL请求"
|
||||
// @Param request body object{url=string,file_name=string,file_type=string,enable_multimodel=bool,title=string,tag_ids=[]string} true "URL请求"
|
||||
// @Success 201 {object} map[string]interface{} "创建的知识"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 409 {object} map[string]interface{} "URL重复"
|
||||
@@ -821,7 +822,7 @@ func buildSpanTree(knowledgeID string, attempt int, rows []types.KnowledgeProces
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param page query int false "页码"
|
||||
// @Param page_size query int false "每页数量"
|
||||
// @Param tag_id query string false "标签ID筛选"
|
||||
// @Param tag_ids query string false "标签ID筛选,逗号分隔(OR语义)"
|
||||
// @Param keyword query string false "关键词搜索"
|
||||
// @Param file_type query string false "文件类型筛选"
|
||||
// @Param parse_status query string false "解析状态筛选 (pending/processing/completed/failed)"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseCommaSeparatedTagIDs(t *testing.T) {
|
||||
assert.Nil(t, parseCommaSeparatedTagIDs(""))
|
||||
assert.Equal(t, []string{"a", "b"}, parseCommaSeparatedTagIDs("a,b"))
|
||||
assert.Equal(t, []string{"a", "b"}, parseCommaSeparatedTagIDs(" a , b "))
|
||||
assert.Equal(t, []string{"a"}, parseCommaSeparatedTagIDs("a,__untagged__,,"))
|
||||
}
|
||||
Reference in New Issue
Block a user