Skill: yangyangupday/joplin-api-china (version 1.1.0)

--- 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', ''))" } [REDACTED_SECRET]) ``` **然后判断：** | 情况 | 动作 | |------|------| | `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 [REDACTED_SECRET] -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?[REDACTED_SECRET]" | 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&[REDACTED_SECRET]" | jq . ``` ### Step 2: List notes inside a notebook ```bash # Get all notes in a specific folder curl -s "$JOPLIN_BASE/folders/FOLDER_ID/notes?[REDACTED_SECRET],title,updated_time&limit=50" | jq . ``` ### Step 3: Read note content ```bash # Get note body (Markdown) curl -s "$JOPLIN_BASE/notes/NOTE_ID?[REDACTED_SECRET],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+购买&[REDACTED_SECRET],title" | jq . # Then read the matching note curl -s "$JOPLIN_BASE/notes/NOTE_ID?[REDACTED_SECRET]" | 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?[REDACTED_SECRET],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?[REDACTED_SECRET],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&[REDACTED_SECRET],title,parent_id" | jq . ``` ## Note Operations ### List notes in a folder ```bash curl -s "$JOPLIN_BASE/folders/FOLDER_ID/notes?[REDACTED_SECRET],title,updated_time" | jq . ``` ### Read note content ```bash # Title + body only curl -s "$JOPLIN_BASE/notes/NOTE_ID?[REDACTED_SECRET],title,body" | jq . # With metadata curl -s "$JOPLIN_BASE/notes/NOTE_ID?[REDACTED_SECRET],title,body,parent_id,created_time,updated_time" | jq . ``` ### Create note (POST /notes) — ⚠️ 高影响操作，需用户明确批准 在执行前向用户展示将要创建的笔记标题、内容和目标笔记本，获得确认后再执行： ```bash # 预览后执行 curl -s -X POST "$JOPLIN_BASE/notes?[REDACTED_SECRET]" \ --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?[REDACTED_SECRET]" \ --data '{"title":"New Title"}' | jq . ``` ### Delete note (DELETE /notes/:id) — ⚠️ 高影响操作，需用户明确批准 默认行为：**移至回收站**（软删除），用户可恢复。 除非用户**明确请求永久删除**，否则不使用 `permanent=1`。 ```bash # 移至回收站（默认，可恢复） curl -s -X DELETE "$JOPLIN_BASE/notes/NOTE_ID?[REDACTED_SECRET]" | jq . ``` 执行前必须： 1. **展示目标笔记的标题和 ID** 2. **说明操作后果**（移至回收站 / 永久删除） 3. **获得用户明确确认**后再执行 ### Note tags & resources ```bash # Tags on a note curl -s "$JOPLIN_BASE/notes/NOTE_ID/tags?[REDACTED_SECRET]" | jq . # Resources (attachments) on a note curl -s "$JOPLIN_BASE/notes/NOTE_ID/resources?[REDACTED_SECRET]" | jq . ``` ## Tag Operations ```bash # List all tags curl -s "$JOPLIN_BASE/tags?[REDACTED_SECRET],title" | jq . # Search tags (supports * wildcard) curl -s "$JOPLIN_BASE/search?query=project-*&type=tag&[REDACTED_SECRET]" | jq . # Notes with a specific tag curl -s "$JOPLIN_BASE/tags/TAG_ID/notes?[REDACTED_SECRET]" | jq . ``` ## Pagination All list endpoints return `{ "items": [...], "has_more": true/false }`. ```bash # Page 2 curl -s "$JOPLIN_BASE/notes?[REDACTED_SECRET]" | 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).


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)