← Back to Skills Marketplace
sorrymaker0624

Bohrium Knowledge Base Management

by Sorrymaker0624 · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ⚠ pending
58
Downloads
0
Stars
0
Active Installs
1
Versions
Install in OpenClaw
/install bohrium-knowledge-base
Description
Manage Bohrium knowledge bases via open.bohrium.com API. Use when: user asks about creating/listing/searching knowledge bases, managing literature/papers, up...
README (SKILL.md)

SKILL: Bohrium Knowledge Base Management

Overview

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.

Authentication

ACCESS_KEY is read from the OpenClaw config ~/.openclaw/openclaw.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.

Route Mapping

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)

Common Code Template

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"}

Knowledge Base Management

Create Knowledge Base

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:

Parameter Type Required Description
knowledgeBaseName string Yes Knowledge base name
cover string Yes Cover image URL (can be empty string)
introduction string Yes Knowledge base description
privilege integer Yes 1=private, 2=public

List My Knowledge Bases

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:

Parameter Type Required Description
keyword string No Search keyword
pageSize integer No Items per page (default 10)
pageNum integer No Page number (default 1)

Response Fields:

Field Description
id Knowledge base ID
name Knowledge base name
nodeId Knowledge base node ID (used for literature queries, folder operations)
introduction Description
cover Cover image URL
privilege 1=private, 2=public
isZotero Whether it's a Zotero-synced knowledge base
count Literature count
updateTime Update time (ISO 8601)
createTime Creation time (ISO 8601)
source Source identifier
favoriteCount Favorite count
sessionCount Session count

Update Knowledge Base

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."}}

Knowledge Base Details

node_id = 456
r = requests.get(f"{BASE}/knowledge_base/{node_id}", headers=HEADERS)
print(r.json())

Discover Public Knowledge Bases

r = requests.get(f"{BASE}/knowledge_base/discover", headers=HEADERS,
    params={"pageSize": 10, "pageNum": 1})

Recommended Knowledge Bases

r = requests.get(f"{BASE}/knowledge_base/recommendation", headers=HEADERS)
# data: {list, total, config_reason}

Favorite / Unfavorite

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})

Browse History / Search History

# 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})

Search Knowledge Bases

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"])

Delete Knowledge Base

There is no dedicated knowledge_base/delete endpoint. Deleting a KB = deleting its root nodesId (a KB is essentially a root folder):

r = requests.post(f"{BASE}/folder/delete", headers=HEADERS_JSON, json={
    "nodesId": 11743137   # KB's nodesID
})
# Only Owner role can delete.

Folder Management

List Root Directories (Knowledge Bases)

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:

Field Description
total Number of knowledge bases
list[].id Folder/knowledge base ID
list[].name Name
list[].docCount Document count
list[].relationship 1=owner, 2=member
docCount Total document count

List Children (One Level)

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:

Field Description
path[] Breadcrumb path {nodesId, name, depth}
folders[] Subfolders {id, name, docCount, createdTime}
files[] Literature list
files[].nodesId Literature node ID
files[].paperId Paper ID
files[].md5 File MD5
files[].enName English title
files[].zhName Chinese title
files[].fileName File name
files[].authors Author list
files[].date Publication date
files[].literatureType Literature type
fileCount Total file count

Directory Tree

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})

Create Folder

r = requests.post(f"{BASE}/folder/create", headers=HEADERS_JSON, json={
    "parentId": 456,             # Parent folder ID
    "folderName": "My Folder"
})
# {"code": 0, "data": {"message": "..."}}

Rename Folder

r = requests.post(f"{BASE}/folder/update", headers=HEADERS_JSON, json={
    "nodesId": 789,              # Folder node ID
    "folderName": "New Name"
})

Move Folder

r = requests.post(f"{BASE}/folder/move", headers=HEADERS_JSON, json={
    "sourceFolderId": 789,       # Source folder ID
    "targetFolderId": 456        # Target folder ID
})

Delete Folder

r = requests.post(f"{BASE}/folder/delete", headers=HEADERS_JSON, json={
    "nodesId": 789
})

Literature Management

List Literature

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
    })

Literature Details

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):

Field Description
id Literature ID
enName / zhName English / Chinese title
authors Author list
authorDetails[] Author details (scholarId, avatar, name, paperNums, citationNums, hIndex)
doi DOI
enAbstract / zhAbstract English / Chinese abstract
date Publication date
fileName File name
md5 File MD5
paperId Paper ID
publicationEnName Journal name
literatureType Literature type
openAccess Open access flag
existPDF PDF availability
summary[] Section summaries {title, zhTitle, content, zhContent}

Download Literature Link

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.

Upload File

Supports uploading local files (PDF, Markdown, etc.) to the knowledge base.

Upload Flow (Three Steps)

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)

Get Upload Credentials

# 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}")

Execute Binary Upload

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")

Register File to Knowledge Base

# 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}")

Complete Upload Function Example

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)

Idempotency

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.

Script-based Upload

The project also provides a scripts/bohrium-kb-upload.py script that supports command-line uploads:

# 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

Edit Literature Metadata

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:

Parameter Type Required Description
id integer Yes Literature ID
name string (JSON) Yes {"cn":"...","en":"..."} format
doi string Yes DOI
authors string (JSON array) Yes ["Author A","Author B"] format
date string Yes Date YYYY-MM-DD
journal string Yes Journal name
abstract string (JSON) Yes {"cn":"...","en":"..."} format
importance integer Yes Importance level
recallable boolean Yes Whether it can be recalled
unableRecallEnMsg string Yes Non-recall reason (English)
unableRecallZhMsg string Yes Non-recall reason (Chinese)

Delete Literature

r = requests.post(f"{BASE}/file/delete_literature", headers=HEADERS_JSON, json={
    "userResourceId": 12345    # Parameter name is userResourceId
})

Rename Literature

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
})

Move Literature

r = requests.post(f"{BASE}/file/move", headers=HEADERS_JSON, json={
    "fileNodesIdList": [12345, 12346],   # Array of file nodesIds
    "folderNodesId": 789                 # Target folder nodesId
})

Search Literature Content

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:

Field Description
total Total matches
Files[].userResourceId Literature ID
Files[].fileName File name
Files[].content Matched content snippet
Files[].knowledgeBaseName Knowledge base name

File Metadata / Auxiliary Endpoints

# 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}

Tag Management

List Tags

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)")

Create Tag

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']}")

Edit Tag

r = requests.put(f"{BASE}/tag", headers=HEADERS_JSON, json={
    "tagId": 101,
    "name": "Deep Learning"
})

Delete Tag

r = requests.delete(f"{BASE}/tag", headers=HEADERS_JSON, json={
    "tagId": 101
})

Tag Literature

r = requests.post(f"{BASE}/file/tag", headers=HEADERS_JSON, json={
    "tagId": 101,
    "resourceId": 12345
})

Untag Literature

r = requests.post(f"{BASE}/file/untag", headers=HEADERS_JSON, json={
    "tagId": 101,
    "resourceId": 12345
})

Literature Tag Statistics

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")

Notes

Get Note

r = requests.get(f"{BASE}/note", headers=HEADERS,
    params={"resourceId": 12345})
note = r.json()["data"]
print(f"Note: {note['note']}")

Create/Update Note

r = requests.post(f"{BASE}/note", headers=HEADERS_JSON, json={
    "resourceId": 12345,
    "note": "This paper introduces a novel approach to..."
})
# {"code": 0}

Literature Recall & Search

View Literature Slices

View parsed slices of a specific paper:

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
})

Recall from Specific Papers

Perform semantic recall within specified papers. Note: upstream field names are papers/text/k, not query/paperIds/topK.

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
})

Hybrid Recall (Knowledge Base Level)

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.

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
})

Permission Management

Important: All /account/* endpoints use nodesId (the KB's node ID), not knowledgeBaseId.

List Permissions

r = requests.get(f"{BASE}/account/acl", headers=HEADERS,
    params={"nodesId": 11743137})
# data: {privilege, shareMode, userList: [{id, role, isCreator, userName, ...}]}

Set Share Status

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
})

Update User Role

r = requests.post(f"{BASE}/account/user_role", headers=HEADERS_JSON, json={
    "nodesId": 11743137,
    "userId": 456,
    "role": 67801    # See role reference below
})

Remove User Permission

r = requests.delete(f"{BASE}/account/user_role", headers=HEADERS_JSON, json={
    "nodesId": 11743137,
    "userId": 456
})

Batch Add Readers

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"]
})

Request to Join Knowledge Base

r = requests.post(f"{BASE}/account/join_request", headers=HEADERS_JSON, json={
    "nodesId": 11743137
})

Query User Role

r = requests.get(f"{BASE}/account/user_knowledge_base_role", headers=HEADERS,
    params={"nodesId": 11743137})
# data: {roles: [int, ...]}

Other Permission Endpoints

Endpoint Method Purpose
/account/knowledge_base/join/tree GET Tree of all KBs the current user has joined
/account/knowledge_base/join/detail POST Join detail for a KB: {nodesId}
/account/knowledge_base/exit POST Voluntarily leave a KB: {nodesId}
/account/user_pending_join_req GET User's pending join request for a KB: ?nodesId=
/account/join_request GET / PUT Query / approve join requests
/account/join_request/personal GET Join requests you submitted
/account/join_request/manageable GET Join requests you can approve
/account/feishu_bot/send_message POST Send via Feishu bot: {type, msg}

Permission Roles Reference

Role Value Role Permissions
1 Owner Full control: CRUD, permission management, delete knowledge base
2 Editor CRUD literature, manage tags and folders
3 Reader Read-only: view, download, search literature

Knowledge Base Visibility (privilege):

Value Description
1 Private — visible only to owner and authorized users
2 Public — discoverable and viewable by everyone

Troubleshooting

Problem Cause Solution
code is non-zero API call error Check error.msg or message in the response for details
401 Unauthorized Invalid or expired accessKey Verify ACCESS_KEY is correct
Knowledge base not found Wrong ID used nodesId (node ID) and id (KB ID) are different. Permission and folder endpoints all use nodesId; list endpoints return both.
404 page not found Called a /v2/* path Gateway only proxies to /api/v1/*; all examples in this document use v1 paths and work correctly
code=230606 keywords is required recall/hybrid keywords is empty Provide at least one keyword: weight pair
code=230105 Wrong file-endpoint field name Use userResourceId (not resourceId/name/targetFolderId); see examples above
Literature search returns empty Literature not yet indexed Newly imported literature needs time for backend parsing and indexing
Edit literature name/abstract format error JSON string required name and abstract fields require JSON strings like '{"cn":"...","en":"..."}'
Folder operation permission error Insufficient role Requires Editor or Owner role; deleting a KB (folder/delete on root nodesId) requires Owner

Gateway Limitations

  • Any /openapi/v1/knowledge/\x3Cpath> request is proxied to literature-sage /api/v1/\x3Cpath>.
  • literature-sage's /api/v2/* routes cannot be reached through this gateway. However, all examples in this document use v1 paths and work correctly.
  • There is no dedicated KB delete endpoint; use POST /folder/delete {nodesId: \x3CKB_nodesID>}.
Capability Tags
requires-sensitive-credentials
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install bohrium-knowledge-base
  3. After installation, invoke the skill by name or use /bohrium-knowledge-base
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
v1.0.0
Initial release
Metadata
Slug bohrium-knowledge-base
Version 1.0.0
License MIT-0
All-time Installs 0
Active Installs 0
Total Versions 1
Frequently Asked Questions

What is Bohrium Knowledge Base Management?

Manage Bohrium knowledge bases via open.bohrium.com API. Use when: user asks about creating/listing/searching knowledge bases, managing literature/papers, up... It is an AI Agent Skill for Claude Code / OpenClaw, with 58 downloads so far.

How do I install Bohrium Knowledge Base Management?

Run "/install bohrium-knowledge-base" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is Bohrium Knowledge Base Management free?

Yes, Bohrium Knowledge Base Management is completely free, licensed under MIT-0. You can download, install and use it at no cost.

Which platforms does Bohrium Knowledge Base Management support?

Bohrium Knowledge Base Management is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).

Who created Bohrium Knowledge Base Management?

It is built and maintained by Sorrymaker0624 (@sorrymaker0624); the current version is v1.0.0.

💬 Comments