通过 Joplin 官方本地 Data API(Clipper Server,端口 41184)查询和管理笔记、笔记本、标签。本技能仅对原始 Joplin REST API 做了 AI 调用引导和中文交互封装,不引入第三方服务,所有请求直连本机 Joplin 实例。触发词:"查 Joplin...
---
name: joplin-api
description: 通过 Joplin 官方本地 Data API(Clipper Server,端口 41184)查询和管理笔记、笔记本、标签。本技能仅对原始 Joplin REST API 做了 AI 调用引导和中文交互封装,不修改数据、不引入第三方服务,所有请求直连本机 Joplin 实例,安全可靠。触发词:"查 Joplin"、"Joplin 笔记"、"joplin search"、"create note in joplin" 等。
version: 1.1.0
metadata:
openclaw:
requires:
env:
- JOPLIN_TOKEN
bins: []
primaryEnv: JOPLIN_TOKEN
---
# 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 ID | `parent_id: "folder_xyz"` 表示该笔记在 `folder_xyz` 笔记本下 |
## Configuration: Token Lifecycle
Token 存储在 OpenClaw 配置 `skills.entries.joplin-api.env.JOPLIN_TOKEN`。
每次操作 Joplin API **之前**,必须按以下流程确认 token:
### Step 0: 确认 Token(每次操作前必做)
```bash
# 从 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 后:写入配置
```bash
# 将用户提供的 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:
```bash
curl -s http://localhost:41184/ping
# Expected: "JoplinClipperServer"
```
### 完整操作示例(含 token 确认)
```bash
# 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)
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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)
```bash
# 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
```bash
# 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
```bash
# 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
```bash
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
```bash
# 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) — ⚠️ 高影响操作,需用户明确批准
在执行前向用户展示将要创建的笔记标题、内容和目标笔记本,获得确认后再执行:
```bash
# 预览后执行
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) — ⚠️ 高影响操作,需用户明确批准
在执行前向用户展示将要修改的笔记标题、当前内容摘要和拟修改内容,获得确认后再执行:
```bash
# 预览后执行
curl -s -X PUT "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN" \
--data '{"title":"New Title"}' | jq .
```
### Delete note (DELETE /notes/:id) — ⚠️ 高影响操作,需用户明确批准
默认行为:**移至回收站**(软删除),用户可恢复。
除非用户**明确请求永久删除**,否则不使用 `permanent=1`。
```bash
# 移至回收站(默认,可恢复)
curl -s -X DELETE "$JOPLIN_BASE/notes/NOTE_ID?token=$JOPLIN_TOKEN" | jq .
```
执行前必须:
1. **展示目标笔记的标题和 ID**
2. **说明操作后果**(移至回收站 / 永久删除)
3. **获得用户明确确认**后再执行
### Note tags & resources
```bash
# 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
```bash
# 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 }`.
```bash
# 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(铁律)
0. **每次操作前先确认 token** — 从配置读取 → 有值则用 → 无值则问用户 → 用户给出后写入配置 → 下次再确认
1. **GET free** — 读取笔记、笔记本、标签可自由执行,无需额外批准
2. **写/删操作必须经过用户明确批准** — POST(创建)、PUT(更新)、DELETE(删除)均为高影响操作:
- **创建**:展示标题、内容摘要、目标笔记本,确认后再执行
- **更新**:展示笔记标题、当前内容摘要、拟修改内容,确认后再执行
- **删除**:展示笔记标题和 ID,说明操作后果(移至回收站/永久删除),确认后再执行
3. **默认软删除** — 删除笔记时默认移至回收站(可恢复);除非用户明确说"永久删除",否则不加 `permanent=1`
4. **Search without body first** — 搜索时用 `fields=id,title` 提速,找到目标后再获取 `body`
5. **Use parent_id for hierarchy** — folders 和 notes 都通过 `parent_id` 构建树形关系
See `references/api-docs.md` for the complete API reference (all endpoints, properties, error handling).
don't have the plugin yet? install it then click "run inline in claude" again.