Joplin API(中文)

通过 Joplin 官方本地 Data API(Clipper Server,端口 41184)查询和管理笔记、笔记本、标签。本技能仅对原始 Joplin REST API 做了 AI 调用引导和中文交互封装,不修改数据、不引入第三方服务,所有请求直连本机 Joplin 实例,安全可靠。触发词:"查 Joplin"、"Joplin 笔记"、"joplin search"、"create note in joplin" 等。

Audits

Pending

Install

openclaw skills install joplin-api-china

Joplin Data API Skill

Access Joplin data via the local HTTP REST API (clipper server).

Architecture

Joplin data model:

Folders (笔记本/Notebooks)          Notes (笔记)
├── 顶层笔记本                       ├── 笔记属于某个 folder (parent_id → folder.id)
│   ├── 子笔记本 (parent_id→父ID)    ├── 笔记内容是 Markdown 格式 (body 字段)
│   │   ├── 孙笔记本                  └── 可附加标签(tags)、资源(resources)
│   │   │   └── ...无限层级
│   │   └── 孙笔记本
│   └── 子笔记本
└── 顶层笔记本

Key Concepts

  • Folders(笔记本) = 目录/文件夹,通过 parent_id 形成无限层级的树形结构。每个 folder 可以有任意多个子 folder,没有层级深度限制。顶级 folder 的 parent_id 为空或指向一个不在当前列表中的父 ID。
  • Notes(笔记) = 内容载体,每条笔记通过 parent_id 归属到一个且仅一个 folder 下。笔记内容是 Markdown 格式(body 字段)。
  • Tags(标签) = 跨笔记本的分类标记,可附加到任意笔记上,不受 folder 层级限制。
  • Resources(资源) = 附件文件(图片、文档等),可关联到笔记上。

parent_id 关系说明

对象parent_id 含义示例
Folder指向父 folder ID;顶级 folder 为空/根parent_id: "abc123..." 表示该笔记本是 abc123... 的子目录
Note指向所属 folder IDparent_id: "folder_xyz" 表示该笔记在 folder_xyz 笔记本下

Configuration: Token Lifecycle

Token 存储在 OpenClaw 配置 skills.entries.joplin-api.env.JOPLIN_TOKEN。 每次操作 Joplin API 之前,必须按以下流程确认 token:

Step 0: 确认 Token(每次操作前必做)

# 从 OpenClaw 配置读取 token
_jtoken() {
  python3 -c "import json; d=json.load(open('$HOME/.openclaw/openclaw.json')); print(d['skills']['entries']['joplin-api']['env'].get('JOPLIN_TOKEN', ''))"
}

TOKEN=$(_jtoken)

然后判断:

情况动作
TOKEN 有值(非空字符串)→ 继续调用 API,用此 token
TOKEN 为空问用户要 token(见下方提示文案)

Token 缺失时的用户提示

当 token 为空时,告知用户以下内容:

Joplin API Token 尚未配置。请按以下步骤获取:

  1. 打开 Joplin 桌面端
  2. 进入 选项/偏好设置Web Clipper
  3. 找到 Access password(访问密码),这就是 token
  4. 把这段密码发给我,我会写入配置文件

同时请确认 Joplin Clipper Server 已启用(同一页面有开关)。

安全防护说明

本技能直接调用 Joplin 官方本地 REST API,具备完整的 CRUD 能力。为保护数据安全:

  • 读取操作(GET):自由执行,无需批准
  • 写入/删除操作(POST/PUT/DELETE):必须在用户明确确认后才执行
  • 默认软删除:删除笔记默认移至回收站,除非用户明确要求永久删除

用户给出 Token 后:写入配置

# 将用户提供的 token 写入 OpenClaw 配置
_jset_token <TOKEN_VALUE> {
  python3 -c "
import json, sys
path = '$HOME/.openclaw/openclaw.json'
d = json.load(open(path))
d.setdefault('skills', {}).setdefault('entries', {}).setdefault('joplin-api', {}).setdefault('env', {})
d['skills']['entries']['joplin-api']['env']['JOPLIN_TOKEN'] = sys.argv[1]
json.dump(d, open(path, 'w'), indent=2)
print('Token saved.')
" "$1"
}

写入完成后向用户确认 "Token 已保存,下次操作自动使用。"

后续每次操作

重复 Step 0:先读配置确认 token → 有值则用,无值则再问。


Base URL: http://localhost:41184

Verify service:

curl -s http://localhost:41184/ping
# Expected: "JoplinClipperServer"

完整操作示例(含 token 确认)

# 1. 确认 token
TOKEN=$(python3 -c "import json; d=json.load(open('$HOME/.openclaw/openclaw.json')); print(d['skills']['entries']['joplin-api']['env'].get('JOPLIN_TOKEN', ''))")

# 2. 检查是否有值
if [ -z "$TOKEN" ]; then
  echo "TOKEN_MISSING"
fi

# 3. 有 token 则调用 API
curl -s "http://localhost:41184/notes?token=$TOKEN&limit=5" | jq .

Workflow: Search for a Topic

The recommended workflow to find content about a topic:

Step 1: Find relevant notebooks (folders)

# Search folders by title (case-insensitive, supports * wildcard)
curl -s "$JOPLIN_BASE/search?query=r730&type=folder&token=$JOPLIN_TOKEN" | jq .

Step 2: List notes inside a notebook

# Get all notes in a specific folder
curl -s "$JOPLIN_BASE/folders/FOLDER_ID/notes?token=$JOPLIN_TOKEN&fields=id,title,updated_time&limit=50" | jq .

Step 3: Read note content

# Get note body (Markdown)
curl -s "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN&fields=id,title,body" | jq .

Alternative: Full-text search across all notes

# Search notes by keyword (full-text, returns id/title only for speed)
curl -s "$JOPLIN_BASE/search?query=r730+购买&token=$JOPLIN_TOKEN&fields=id,title" | jq .

# Then read the matching note
curl -s "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN&fields=body" | jq .

Tip: When searching with body field, response can be large. Search with id,title first, then fetch body for the specific note you want.

Folder Hierarchy Operations

List all folders (flat list with parent_id)

# Returns flat list; use parent_id to reconstruct tree
curl -s "$JOPLIN_BASE/folders?token=$JOPLIN_TOKEN&fields=id,title,parent_id" | jq .

Each folder has:

  • id — folder ID
  • title — folder name
  • parent_id — parent folder ID (empty/null = top-level)
  • deleted_time — 0 = active, >0 = soft-deleted

Reconstruct folder tree in code

# Get all folders and build tree structure
curl -s "$JOPLIN_BASE/folders?token=$JOPLIN_TOKEN&fields=id,title,parent_id" | python3 -c "
import sys, json
folders = json.load(sys.stdin)
if isinstance(folders, dict):
    folders = folders.get('items', folders)

# Build lookup
by_id = {f['id']: f for f in folders if f.get('deleted_time', 0) == 0}

# Attach children
for f in by_id.values():
    pid = f.get('parent_id', '')
    if pid and pid in by_id:
        by_id[pid].setdefault('children', []).append(f)

# Print tree (top-level only parents)
def show(items, indent=0):
    for item in sorted(items, key=lambda x: x['title']):
        print('  ' * indent + f'[{item[\"id\"][:8]}] {item[\"title\"]}')
        show(item.get('children', []), indent + 1)

roots = [f for f in by_id.values() if not f.get('parent_id') or f['parent_id'] not in by_id]
show(roots)
"

Find folder ID by name

# Search for a notebook by title
curl -s "$JOPLIN_BASE/search?query=r730&type=folder&token=$JOPLIN_TOKEN&fields=id,title,parent_id" | jq .

Note Operations

List notes in a folder

curl -s "$JOPLIN_BASE/folders/FOLDER_ID/notes?token=$JOPLIN_TOKEN&limit=100&order_by=updated_time&order_dir=DESC&fields=id,title,updated_time" | jq .

Read note content

# Title + body only
curl -s "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN&fields=id,title,body" | jq .

# With metadata
curl -s "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN&fields=id,title,body,parent_id,created_time,updated_time" | jq .

Create note (POST /notes) — ⚠️ 高影响操作,需用户明确批准

在执行前向用户展示将要创建的笔记标题、内容和目标笔记本,获得确认后再执行:

# 预览后执行
curl -s -X POST "$JOPLIN_BASE/notes?token=$JOPLIN_TOKEN" \
  --data '{"title":"Title","body":"Markdown content","parent_id":"FOLDER_ID"}' | jq .

Update note (PUT /notes/:id) — ⚠️ 高影响操作,需用户明确批准

在执行前向用户展示将要修改的笔记标题、当前内容摘要和拟修改内容,获得确认后再执行:

# 预览后执行
curl -s -X PUT "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN" \
  --data '{"title":"New Title"}' | jq .

Delete note (DELETE /notes/:id) — ⚠️ 高影响操作,需用户明确批准

默认行为:移至回收站(软删除),用户可恢复。 除非用户明确请求永久删除,否则不使用 permanent=1

# 移至回收站(默认,可恢复)
curl -s -X DELETE "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN" | jq .

执行前必须:

  1. 展示目标笔记的标题和 ID
  2. 说明操作后果(移至回收站 / 永久删除)
  3. 获得用户明确确认后再执行

Note tags & resources

# Tags on a note
curl -s "$JOPLIN_BASE/notes/NOTE_ID/tags?token=$JOPLIN_TOKEN" | jq .

# Resources (attachments) on a note
curl -s "$JOPLIN_BASE/notes/NOTE_ID/resources?token=$JOPLIN_TOKEN" | jq .

Tag Operations

# List all tags
curl -s "$JOPLIN_BASE/tags?token=$JOPLIN_TOKEN&fields=id,title" | jq .

# Search tags (supports * wildcard)
curl -s "$JOPLIN_BASE/search?query=project-*&type=tag&token=$JOPLIN_TOKEN" | jq .

# Notes with a specific tag
curl -s "$JOPLIN_BASE/tags/TAG_ID/notes?token=$JOPLIN_TOKEN" | jq .

Pagination

All list endpoints return { "items": [...], "has_more": true/false }.

# Page 2
curl -s "$JOPLIN_BASE/notes?token=$JOPLIN_TOKEN&limit=10&page=2" | jq .

Parameters: page (starts at 1), limit (max 100), order_by, order_dir (ASC/DESC).

Data Types

  • Text: UTF-8
  • Dates/times: Unix timestamps in milliseconds
  • Booleans: Integer 0 or 1
  • deleted_time: 0 = active, >0 = soft-deleted

Iron Rules(铁律)

  1. 每次操作前先确认 token — 从配置读取 → 有值则用 → 无值则问用户 → 用户给出后写入配置 → 下次再确认
  2. GET free — 读取笔记、笔记本、标签可自由执行,无需额外批准
  3. 写/删操作必须经过用户明确批准 — POST(创建)、PUT(更新)、DELETE(删除)均为高影响操作:
    • 创建:展示标题、内容摘要、目标笔记本,确认后再执行
    • 更新:展示笔记标题、当前内容摘要、拟修改内容,确认后再执行
    • 删除:展示笔记标题和 ID,说明操作后果(移至回收站/永久删除),确认后再执行
  4. 默认软删除 — 删除笔记时默认移至回收站(可恢复);除非用户明确说"永久删除",否则不加 permanent=1
  5. Search without body first — 搜索时用 fields=id,title 提速,找到目标后再获取 body
  6. Use parent_id for hierarchy — folders 和 notes 都通过 parent_id 构建树形关系

See references/api-docs.md for the complete API reference (all endpoints, properties, error handling).