Manage knowledge bases (Literature Sage) on the Bohrium platform. Knowledge bases provide literature management, folder organization, tagging, notes, literature content search & recall, and permission management.
No CLI support — Unlike bohrium-job/node/image, knowledge bases have no bohr CLI commands. All operations use the HTTP API.
ACCESS_KEY is read from the OpenClaw config ~/.openclaw/openclaw.json:
json
"bohrium-knowledge-base": {
"enabled": true,
"apiKey": "YOUR_ACCESS_KEY",
"env": {
"ACCESS_KEY": "YOUR_ACCESS_KEY"
}
}
OpenClaw automatically injects env.ACCESS_KEY into the runtime.
text
External call: GET/POST https://open.bohrium.com/openapi/v1/knowledge/{path}
Header: accessKey: YOUR_ACCESS_KEY
Gateway forwards: → literature-sage.bohrium.com/api/v1/{path}
Header: X-User-Id, X-Org-Id (converted from accessKey)
python
import os, requests
AK = os.environ.get("ACCESS_KEY", "")
BASE = "https://open.bohrium.com/openapi/v1/knowledge"
HEADERS = {"accessKey": AK}
HEADERS_JSON = {**HEADERS, "Content-Type": "application/json"}
python
r = requests.post(f"{BASE}/knowledge_base/create", headers=HEADERS_JSON, json={
"knowledgeBaseName": "My Knowledge Base",
"cover": "",
"introduction": "A collection of papers on molecular dynamics",
"privilege": 1 # 1=private, 2=public
})
print(r.json())
# {"code": 0, "data": {"id": 123, "msg": "Knowledge base created successfully."}}
Parameters:
ParameterTypeRequiredDescriptionknowledgeBaseNamestringYesKnowledge base namecoverstringYesCover image URL (can be empty string)introductionstringYesKnowledge base descriptionprivilegeintegerYes1=private, 2=public
python
r = requests.get(f"{BASE}/knowledge_base/list", headers=HEADERS,
params={"keyword": "", "pageSize": 10, "pageNum": 1})
data = r.json()["data"]
for kb in data["list"]:
print(f"[{kb['id']}] {kb['name']} (nodeId={kb['nodeId']}, privilege={kb['privilege']})")
Query Parameters:
ParameterTypeRequiredDescriptionkeywordstringNoSearch keywordpageSizeintegerNoItems per page (default 10)pageNumintegerNoPage number (default 1)
Response Fields:
FieldDescriptionidKnowledge base IDnameKnowledge base namenodeIdKnowledge base node ID (used for literature queries, folder operations)introductionDescriptioncoverCover image URLprivilege1=private, 2=publicisZoteroWhether it's a Zotero-synced knowledge basecountLiterature countupdateTimeUpdate time (ISO 8601)createTimeCreation time (ISO 8601)sourceSource identifierfavoriteCountFavorite countsessionCountSession count
python
r = requests.post(f"{BASE}/knowledge_base/update", headers=HEADERS_JSON, json={
"knowledgeBaseName": "Updated Name",
"cover": "",
"introduction": "Updated introduction",
"NodesId": 456, # Knowledge base node ID
"privilege": 2 # Change to public
})
# {"code": 0, "data": {"id": 123, "msg": "Knowledge base updated successfully."}}
python
node_id = 456
r = requests.get(f"{BASE}/knowledge_base/{node_id}", headers=HEADERS)
print(r.json())
python
r = requests.get(f"{BASE}/knowledge_base/discover", headers=HEADERS,
params={"pageSize": 10, "pageNum": 1})
python
r = requests.get(f"{BASE}/knowledge_base/recommendation", headers=HEADERS)
# data: {list, total, config_reason}
python
requests.post(f"{BASE}/knowledge_base/favorite", headers=HEADERS_JSON, json={"nodesId": 11743137})
requests.post(f"{BASE}/knowledge_base/unfavorite", headers=HEADERS_JSON, json={"nodesId": 11743137})
# List my favorited KBs
r = requests.get(f"{BASE}/knowledge_base/favorite", headers=HEADERS,
params={"pageNum": 1, "pageSize": 10})
python
# Record a browse event
requests.post(f"{BASE}/knowledge_base/browse/add", headers=HEADERS_JSON, json={
"fileId": 12345,
"fileType": 1,
"parentId": 11743137,
"rootFolderId": 11743137,
"nodesId": 11743137
})
# Query browse / search history
requests.post(f"{BASE}/knowledge_base/browse/query", headers=HEADERS_JSON, json={"pageNum":1,"pageSize":10})
requests.post(f"{BASE}/knowledge_base/history/query", headers=HEADERS_JSON, json={"pageNum":1,"pageSize":10})
python
r = requests.post(f"{BASE}/knowledge_base/search/name", headers=HEADERS_JSON, json={
"nodesId": 456, # KB nodesId; 0 = search across all KBs you can access
"searchText": "molecular dynamics",
"searchType": 0, # 0=all, 1=folders only, 2=files only
"pageNum": 1,
"pageSize": 10
})
data = r.json()["data"]
print(f"total: {data['total']}")
for f in data["folders"]: print("[DIR]", f["name"])
for f in data["files"]: print("[FILE]", f["fileName"])
There is no dedicated knowledge_base/delete endpoint. Deleting a KB = deleting its root nodesId (a KB is essentially a root folder):
python
r = requests.post(f"{BASE}/folder/delete", headers=HEADERS_JSON, json={
"nodesId": 11743137 # KB's nodesID
})
# Only Owner role can delete.
python
r = requests.get(f"{BASE}/folder/root", headers=HEADERS)
data = r.json()["data"]
print(f"Total: {data['total']} knowledge bases, {data['docCount']} documents")
for item in data["list"]:
print(f" [{item['id']}] {item['name']} ({item['docCount']} docs, "
f"relationship={'owner' if item['relationship'] == 1 else 'member'})")
Response Fields:
FieldDescriptiontotalNumber of knowledge baseslist[].idFolder/knowledge base IDlist[].nameNamelist[].docCountDocument countlist[].relationship1=owner, 2=memberdocCountTotal document count
python
r = requests.get(f"{BASE}/folder/children", headers=HEADERS,
params={"folderId": 456, "pageNum": 1, "pageSize": 20})
data = r.json()["data"]
# Breadcrumb path
for p in data["path"]:
print(f" {'>' * p['depth']} {p['name']} (id={p['nodesId']})")
# Subfolders
for f in data["folders"]:
print(f" [DIR] {f['name']} ({f['docCount']} docs, id={f['id']})")
# Files
for f in data["files"]:
print(f" [FILE] {f['name']} | {f['fileName']} | {f['date']}")
Response Fields:
FieldDescriptionpath[]Breadcrumb path {nodesId, name, depth}``folders[]Subfolders {id, name, docCount, createdTime}``files[]Literature listfiles[].nodesIdLiterature node IDfiles[].paperIdPaper IDfiles[].md5File MD5files[].enNameEnglish titlefiles[].zhNameChinese titlefiles[].fileNameFile namefiles[].authorsAuthor listfiles[].datePublication datefiles[].literatureTypeLiterature typefileCountTotal file count
python
r = requests.get(f"{BASE}/folder/directory", headers=HEADERS,
params={"folderId": 456})
# Recursive tree: {result: [{nodesId, name, subFolders: [...]}]}
# Full file tree (includes literature):
r = requests.get(f"{BASE}/folder/file_tree", headers=HEADERS,
params={"folderId": 456})
python
r = requests.post(f"{BASE}/folder/create", headers=HEADERS_JSON, json={
"parentId": 456, # Parent folder ID
"folderName": "My Folder"
})
# {"code": 0, "data": {"message": "..."}}
python
r = requests.post(f"{BASE}/folder/update", headers=HEADERS_JSON, json={
"nodesId": 789, # Folder node ID
"folderName": "New Name"
})
python
r = requests.post(f"{BASE}/folder/move", headers=HEADERS_JSON, json={
"sourceFolderId": 789, # Source folder ID
"targetFolderId": 456 # Target folder ID
})
python
r = requests.post(f"{BASE}/folder/delete", headers=HEADERS_JSON, json={
"nodesId": 789
})
python
r = requests.get(f"{BASE}/file", headers=HEADERS,
params={
"parentId": 456, # Folder ID
"pageNum": 1,
"pageSize": 20,
"order": 1, # 1=asc, 2=desc (integer, not string)
"orderBy": 1, # 1=createdTime, 2=updatedTime, etc.
"noTag": False # True = only untagged items
})
python
r = requests.get(f"{BASE}/file/detail", headers=HEADERS,
params={"resourceId": "12345"})
detail = r.json()["data"]
print(f"Title: {detail['enName']}")
print(f"Authors: {', '.join(a['name'] for a in detail.get('authorDetails', []))}")
print(f"DOI: {detail.get('doi', '')}")
print(f"Abstract: {detail.get('enAbstract', '')}")
Response Fields (selected):
FieldDescriptionidLiterature IDenName / zhNameEnglish / Chinese titleauthorsAuthor listauthorDetails[]Author details (scholarId, avatar, name, paperNums, citationNums, hIndex)doiDOIenAbstract / zhAbstractEnglish / Chinese abstractdatePublication datefileNameFile namemd5File MD5paperIdPaper IDpublicationEnNameJournal nameliteratureTypeLiterature typeopenAccessOpen access flagexistPDFPDF availabilitysummary[]Section summaries {title, zhTitle, content, zhContent}
python
r = requests.post(f"{BASE}/file/read", headers=HEADERS_JSON, json={
"userResourceId": [12345] # Array; parameter name is userResourceId
})
# Returns a list of download URLs, one per resource.
Supports uploading local files (PDF, Markdown, etc.) to the knowledge base.
text
1) GET /file/multipart → Get upload credentials (host, path, token)
2) POST {host}/api/upload/binary → Upload file binary content
3) POST /file/submit → Register file to knowledge base (make it visible)
python
# Calculate file MD5 and size
import hashlib
def md5_hex(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
# Get upload credentials
file_path = "paper.pdf"
file_name = "paper.pdf"
file_size = os.path.getsize(file_path)
file_md5 = md5_hex(file_path)
parent_id = 456 # NodeId of knowledge base or folder
r = requests.get(f"{BASE}/file/multipart", headers=HEADERS,
params={
"fileName": file_name,
"md5": file_md5,
"parentId": parent_id,
"size": file_size
})
multipart_data = r.json()["data"]
if multipart_data.get("fileExist"):
print("File already exists, no need to upload again")
else:
host = multipart_data["host"]
path = multipart_data["path"]
token = multipart_data["token"]
print(f"Successfully obtained upload credentials: {host}, {path}")
python
import base64
import json
import urllib.request
def make_storage_param(remote_path: str, encoded_file_name: str, content_type: str) -> str:
payload = {
"path": remote_path,
"option": {
"contentDisposition": (
f'inline; filename="{encoded_file_name}"; '
f"filename*=UTF-8''{encoded_file_name}"
),
"contentType": content_type,
},
}
b = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return base64.b64encode(b).decode("utf-8")
# Upload file to storage service
file_content = open(file_path, "rb").read()
content_type = "application/pdf" # Or other appropriate MIME type
encoded_file_name = urllib.parse.quote(file_name, safe="-_.!~*'()")
storage_param = make_storage_param(path, encoded_file_name, content_type)
upload_url = host.rstrip("/") + "/api/upload/binary"
req = urllib.request.Request(upload_url, method="POST", data=file_content)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("X-Storage-Param", storage_param)
req.add_header("Content-Type", "application/octet-stream")
with urllib.request.urlopen(req, timeout=300) as resp:
upload_result = json.loads(resp.read().decode("utf-8"))
print("Binary file upload completed")
python
# Register the uploaded file to the knowledge base to make it visible in the KB
final_path = (upload_result.get("data") or {}).get("path") or path
r = requests.post(f"{BASE}/file/submit", headers=HEADERS_JSON, json={
"parentId": parent_id,
"fileName": file_name,
"md5": file_md5,
"size": file_size,
"url": final_path # Path returned after upload completes
})
result = r.json()
if result.get("code") == 0:
print("File successfully registered to knowledge base")
elif result.get("code") == 230117: # File already exists
print("File already exists in knowledge base")
else:
print(f"Registration failed: {result}")
python
import hashlib
import base64
import json
import urllib.request
import mimetypes
import os
def guess_content_type(path):
suffix = os.path.splitext(path)[1].lower()
if suffix in {".md", ".markdown"}:
return "text/markdown; charset=utf-8"
if suffix in {".txt"}:
return "text/plain; charset=utf-8"
ctype, _ = mimetypes.guess_type(path)
if ctype is None:
return "application/octet-stream"
if ctype.startswith("text/"):
return f"{ctype}; charset=utf-8"
return ctype
def upload_file_to_knowledge_base(file_path, parent_id, custom_file_name=None):
# Prepare file info
file_name = custom_file_name or os.path.basename(file_path)
file_size = os.path.getsize(file_path)
# Calculate MD5
def md5_hex(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
file_md5 = md5_hex(file_path)
# Step 1: Get upload credentials
r = requests.get(f"{BASE}/file/multipart", headers=HEADERS,
params={
"fileName": file_name,
"md5": file_md5,
"parentId": parent_id,
"size": file_size
})
multipart_data = r.json()["data"]
if multipart_data.get("fileExist"):
print("File already exists, skipping upload but registering to knowledge base...")
# Even if file exists, still need to register to knowledge base
r_submit = requests.post(f"{BASE}/file/submit", headers=HEADERS_JSON, json={
"parentId": parent_id,
"fileName": file_name,
"md5": file_md5,
"size": file_size,
"url": multipart_data.get("path", "") # Use existing path
})
return r_submit.json()
# Get upload credentials
host = multipart_data["host"]
path = multipart_data["path"]
token = multipart_data["token"]
# Prepare upload parameters
content_type = guess_content_type(file_path)
encoded_file_name = urllib.parse.quote(file_name, safe="-_.!~*'()")
storage_param = base64.b64encode(json.dumps({
"path": path,
"option": {
"contentDisposition": (
f'inline; filename="{encoded_file_name}"; '
f"filename*=UTF-8''{encoded_file_name}"
),
"contentType": content_type,
},
}, ensure_ascii=False, separators=(",", ":")).encode("utf-8")).decode("utf-8")
# Step 2: Binary upload
file_content = open(file_path, "rb").read()
upload_url = host.rstrip("/") + "/api/upload/binary"
req = urllib.request.Request(upload_url, method="POST", data=file_content)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("X-Storage-Param", storage_param)
req.add_header("Content-Type", "application/octet-stream")
with urllib.request.urlopen(req, timeout=300) as resp:
upload_result = json.loads(resp.read().decode("utf-8"))
# Step 3: Register file to knowledge base
final_path = (upload_result.get("data") or {}).get("path") or path
r_submit = requests.post(f"{BASE}/file/submit", headers=HEADERS_JSON, json={
"parentId": parent_id,
"fileName": file_name,
"md5": file_md5,
"size": file_size,
"url": final_path
})
return r_submit.json()
# Example usage
result = upload_file_to_knowledge_base("./paper.pdf", 456)
print(result)
If the file already exists ( fileExist=true), the system skips the binary upload step but still calls the submit interface to ensure the file is visible in the knowledge base. This makes uploading the same file multiple times a safe operation.
The project also provides a scripts/bohrium-kb-upload.py script that supports command-line uploads:
bash
# Upload a single file
python3 scripts/bohrium-kb-upload.py ./paper.pdf --parent-id 456
# Batch upload all PDFs in directory
for f in pdfs/*.pdf; do
python3 scripts/bohrium-kb-upload.py "$f" --parent-id 456
done
python
r = requests.post(f"{BASE}/file/edit", headers=HEADERS_JSON, json={
"id": 12345,
"name": '{"cn":"Chinese Title","en":"English Title"}',
"doi": "10.1234/example",
"authors": '["Author A","Author B"]',
"date": "2024-01-15",
"journal": "Nature",
"abstract": '{"cn":"Chinese abstract","en":"English abstract"}',
"importance": 1,
"recallable": True,
"unableRecallEnMsg": "",
"unableRecallZhMsg": ""
})
Parameters:
ParameterTypeRequiredDescriptionidintegerYesLiterature IDnamestring (JSON)Yes{"cn":"...","en":"..."} formatdoistringYesDOIauthorsstring (JSON array)Yes["Author A","Author B"] formatdatestringYesDate YYYY-MM-DD``journalstringYesJournal nameabstractstring (JSON)Yes{"cn":"...","en":"..."} formatimportanceintegerYesImportance levelrecallablebooleanYesWhether it can be recalledunableRecallEnMsgstringYesNon-recall reason (English)unableRecallZhMsgstringYesNon-recall reason (Chinese)
python
r = requests.post(f"{BASE}/file/delete_literature", headers=HEADERS_JSON, json={
"userResourceId": 12345 # Parameter name is userResourceId
})
python
r = requests.post(f"{BASE}/file/update_literature", headers=HEADERS_JSON, json={
"userResourceId": 12345, # Parameter name is userResourceId
"fileName": "New Literature Name" # Parameter name is fileName
})
python
r = requests.post(f"{BASE}/file/move", headers=HEADERS_JSON, json={
"fileNodesIdList": [12345, 12346], # Array of file nodesIds
"folderNodesId": 789 # Target folder nodesId
})
python
r = requests.post(f"{BASE}/file/search", headers=HEADERS_JSON, json={
"queryContent": "molecular dynamics simulation",
"nodesId": 456,
"knowledgeBaseId": 123
})
data = r.json()["data"]
print(f"Found {data['total']} results")
for f in data["Files"]:
print(f" [{f['userResourceId']}] {f['fileName']}: {f['content'][:100]}...")
Response Fields:
FieldDescriptiontotalTotal matchesFiles[].userResourceIdLiterature IDFiles[].fileNameFile nameFiles[].contentMatched content snippetFiles[].knowledgeBaseNameKnowledge base name
python
# Metadata by userResourceId (md5 / paperId / publicationId etc.)
requests.post(f"{BASE}/file/fileinfo", headers=HEADERS_JSON, json={
"userResourceId": 12345,
# or use "enclosureId": 67890
})
# Get all file IDs under a folder (no pagination needed)
requests.post(f"{BASE}/file/ids", headers=HEADERS_JSON, json={"parentId": 456})
# Tag info for a single literature
requests.get(f"{BASE}/file/tagInfo", headers=HEADERS, params={"resourceId": 12345})
# Upload history / capacity
requests.get(f"{BASE}/file/upload/record", headers=HEADERS, params={"pageNum":1,"pageSize":10})
requests.get(f"{BASE}/file/capacity", headers=HEADERS) # {remainingCapacity, totalCapacity, usedCapacity}
python
r = requests.get(f"{BASE}/tag", headers=HEADERS,
params={"keyword": ""}) # Optional keyword filter
data = r.json()["data"]
for tag in data["list"]:
print(f" [{tag['id']}] {tag['name']} ({tag['count']} docs)")
python
r = requests.post(f"{BASE}/tag", headers=HEADERS_JSON, json={
"name": "Machine Learning"
})
tag = r.json()["data"]
print(f"Created tag: {tag['id']} - {tag['name']}")
python
r = requests.put(f"{BASE}/tag", headers=HEADERS_JSON, json={
"tagId": 101,
"name": "Deep Learning"
})
python
r = requests.delete(f"{BASE}/tag", headers=HEADERS_JSON, json={
"tagId": 101
})
python
r = requests.post(f"{BASE}/file/tag", headers=HEADERS_JSON, json={
"tagId": 101,
"resourceId": 12345
})
python
r = requests.post(f"{BASE}/file/untag", headers=HEADERS_JSON, json={
"tagId": 101,
"resourceId": 12345
})
python
r = requests.get(f"{BASE}/file/tag", headers=HEADERS,
params={
"parentId": 456, # Folder node ID (optional)
"rootFolderId": 123, # Knowledge base root folder ID (optional)
"query": 2, # 1=search by author, 2=search by keyword (optional)
"keyword": "ML" # Search keyword (optional)
})
data = r.json()["data"]
print(f"Total docs: {data['allDocCount']}, Untagged: {data['noTagCount']}")
for tag in data["tags"]:
print(f" [{tag['id']}] {tag['name']}: {tag['count']} docs")
python
r = requests.get(f"{BASE}/note", headers=HEADERS,
params={"resourceId": 12345})
note = r.json()["data"]
print(f"Note: {note['note']}")
python
r = requests.post(f"{BASE}/note", headers=HEADERS_JSON, json={
"resourceId": 12345,
"note": "This paper introduces a novel approach to..."
})
# {"code": 0}
View parsed slices of a specific paper:
python
r = requests.post(f"{BASE}/box/search_by_md5_paper_id", headers=HEADERS_JSON, json={
"md5": "abc123...",
"paper_id": "paper_001",
"page_num": 1,
"page_size": 10
})
Perform semantic recall within specified papers. Note: upstream field names are papers/ text/ k, not query/ paperIds/ topK.
python
r = requests.post(f"{BASE}/recall/papers", headers=HEADERS_JSON, json={
"papers": [
{"paperId": "paper_001", "md5": ""},
{"paperId": "", "md5": "abc123..."}
],
"text": "molecular dynamics force field",
"k": 5
})
Perform hybrid semantic search across an entire knowledge base. Note: snake_case field names ( knowledge_base_id); keywords is required and must be non-empty.
python
r = requests.post(f"{BASE}/recall/hybrid", headers=HEADERS_JSON, json={
"knowledge_base_id": 456, # KB nodesId
"text": "deep potential energy surface",
"k": 10,
"keywords": {"deep potential": 1.0, "energy surface": 0.5} # required, non-empty
})
Important: All
/account/*endpoints usenodesId(the KB's node ID), notknowledgeBaseId.
python
r = requests.get(f"{BASE}/account/acl", headers=HEADERS,
params={"nodesId": 11743137})
# data: {privilege, shareMode, userList: [{id, role, isCreator, userName, ...}]}
python
r = requests.post(f"{BASE}/account/share_status", headers=HEADERS_JSON, json={
"nodesId": 11743137,
"privilege": 1, # 1=public, 2=private
"shareMode": 1 # 1=no share, 2=link share
})
python
r = requests.post(f"{BASE}/account/user_role", headers=HEADERS_JSON, json={
"nodesId": 11743137,
"userId": 456,
"role": 67801 # See role reference below
})
python
r = requests.delete(f"{BASE}/account/user_role", headers=HEADERS_JSON, json={
"nodesId": 11743137,
"userId": 456
})
python
r = requests.post(f"{BASE}/account/batch_add_readers", headers=HEADERS_JSON, json={
"nodesId": 11743137,
"userList": [
{"id": 456, "role": 67801},
{"id": 789, "role": 67801}
]
# Or use one of: phones / emails / userNos
# "phones": ["13800000000"]
})
python
r = requests.post(f"{BASE}/account/join_request", headers=HEADERS_JSON, json={
"nodesId": 11743137
})
python
r = requests.get(f"{BASE}/account/user_knowledge_base_role", headers=HEADERS,
params={"nodesId": 11743137})
# data: {roles: [int, ...]}
EndpointMethodPurpose/account/knowledge_base/join/treeGETTree of all KBs the current user has joined/account/knowledge_base/join/detailPOSTJoin detail for a KB: {nodesId}``/account/knowledge_base/exitPOSTVoluntarily leave a KB: {nodesId}``/account/user_pending_join_reqGETUser's pending join request for a KB: ?nodesId=``/account/join_requestGET / PUTQuery / approve join requests/account/join_request/personalGETJoin requests you submitted/account/join_request/manageableGETJoin requests you can approve/account/feishu_bot/send_messagePOSTSend via Feishu bot: {type, msg}
Role ValueRolePermissions1OwnerFull control: CRUD, permission management, delete knowledge base2EditorCRUD literature, manage tags and folders3ReaderRead-only: view, download, search literature
Knowledge Base Visibility (privilege):
ValueDescription1Private — visible only to owner and authorized users2Public — discoverable and viewable by everyone
ProblemCauseSolutioncode is non-zeroAPI call errorCheck error.msg or message in the response for details401 UnauthorizedInvalid or expired accessKeyVerify ACCESS_KEY is correctKnowledge base not foundWrong ID usednodesId (node ID) and id (KB ID) are different. Permission and folder endpoints all use nodesId; list endpoints return both.404 page not foundCalled a /v2/* pathGateway only proxies to /api/v1/*; all examples in this document use v1 paths and work correctlycode=230606 keywords is required``recall/hybrid keywords is emptyProvide at least one keyword: weight paircode=230105Wrong file-endpoint field nameUse userResourceId (not resourceId/ name/ targetFolderId); see examples aboveLiterature search returns emptyLiterature not yet indexedNewly imported literature needs time for backend parsing and indexingEdit literature name/abstract format errorJSON string requiredname and abstract fields require JSON strings like '{"cn":"...","en":"..."}'Folder operation permission errorInsufficient roleRequires Editor or Owner role; deleting a KB (folder/delete on root nodesId) requires Owner
/openapi/v1/knowledge/<path> request is proxied to literature-sage /api/v1/<path>./api/v2/* routes cannot be reached through this gateway. However, all examples in this document use v1 paths and work correctly.POST /folder/delete {nodesId: <KB_nodesID>}.