---
name: keng-note-writer
description: >-
  Writes, reads, updates, and searches Markdown notes on kengnote.com via the
  KengNote REST API (Bearer API key). Use when the user mentions kengnote, keng
  notes, 坑笔记, writing notes to keng, Agent note persistence, or
  keng>folder>note path shorthand.
---

# KengNote 写笔记技能

教 Agent 用 **kengnote.com** 官方 API 读写 Markdown 笔记。线上以公网为准；本机开发可用 `http://127.0.0.1:8910/api`。

公开页：https://www.kengnote.com/keng/login.html · https://www.kengnote.com/keng/about.html  
本技能原文：https://www.kengnote.com/keng/skill/keng-note-writer/SKILL.md

## 必读来源（按优先级）

1. 在线短指南：`GET https://kengnote.com/keng/api/agent-guide`（带 Bearer 可得个性化命令）
2. https://www.kengnote.com/keng/llms.txt
3. 本 skill 附录：[reference.md](reference.md)

## 接入三步

```text
1. 拿 Key（按优先级自动发现，无需每次询问用户）：
   a. 环境变量 KENG_API_KEY → 直接使用
   b. 文件 ~/.config/opencode/keng_key → 读取第一行
   c. 浏览器 Agent：Cookie keng_api_key（path=/keng，非 HttpOnly）
   d. 以上均无 → 让用户打开 https://kengnote.com/keng/mykey.html 粘贴 Key
     拿到后写入 ~/.config/opencode/keng_key 持久化供后续使用

2. 验身份
   GET /me  → 确认 id/username 是正在服务的用户

3. 读写笔记
   GET /folders → GET /notes?q=… → GET /notes/{id} → POST 或 PUT
```

**首次使用时拿到 Key 后自动缓存到 `~/.config/opencode/keng_key`，后续无需再次询问。**

所有请求头：

```http
Authorization: Bearer <api_key>
Content-Type: application/json
```

**禁止**把真实 API Key 写入笔记正文、仓库、公开文档。

## 笔记 ID 与路径

| 概念 | 规则 |
|------|------|
| 笔记 `id` | 使用 API 返回值，形如 `personal:123`、`team:456`；勿用纯数字猜类型 |
| 用户路径 | `keng>子文件夹>笔记标题` → 先 `GET /folders` 找 `name`，再按 `folder_id` + 首行 `# 标题` 定位 |
| 文件夹层级 | 用户口语里的 `>` 表示层级，**不是** URL 里的 `/ |

## 写笔记工作流（必须遵守）

```text
记录前搜索 → 有同主题则续写，无则新建
修改前 GET 全文 → 在内存合并 → PUT 写回完整 content
笔记位满 → 提示 recharge_url，禁止覆盖已有笔记冒充新建
```

### 新建

```bash
curl -X POST "https://kengnote.com/keng/api/notes" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"# 标题\n\n正文","tags":["agent"],"folder_id":null,"comment":""}'
```

- 第一行 `# 标题` 即标题（服务端从 content 提取，只读 `title` 字段）
- `folder_id: null` 表示根目录；团队笔记 body 加 `team_id`

### 更新

```bash
# 1. 先读
curl -H "Authorization: Bearer $API_KEY" \
  "https://kengnote.com/keng/api/notes/personal:42"

# 2. 合并后写回完整 Markdown（至少传 content）
curl -X PUT "https://kengnote.com/keng/api/notes/personal:42" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"# 标题\n\n…合并后的全文…"}'
```

`PUT` 可只传变更字段，但**改 content 时必须基于 GET 的全文合并**，避免丢段落。

### 搜索与列表

| 参数 | 用途 |
|------|------|
| `?q=关键词` | 正文模糊搜索 |
| `?tag=标签` | tags 精确匹配（可与 q 并用） |
| `?trash=1` | 回收站 |

## 推荐正文格式（踩坑/经验类）

用户未指定格式时，可用项目约定模板：

```markdown
# <主题>经验
- 版本：1.0.0
- 2026-06-01：<agent名> - 初始记录

# 事件<简述>
## 解决方法
…
## 坑
…
```

- 每次更新：版本号递增；变更记录按日期**倒序**顶插
- `comment` 字段放 AI 元数据，不进正文展示

## 常见错误

| 情况 | 处理 |
|------|------|
| `401` | Key 无效或已轮换 → 让用户从 settings/mykey 重新提供 |
| `403` + `NOTE_SLOT_LIMIT` | 笔记位已满 → 把 `recharge_url` 给用户，**不要** PUT 覆盖旧笔记 |
| `404` on note | 用列表返回的 `id`，勿自编数字 |
| 空列表 | 先 `GET /me` 确认 Key 对应用户 |

## 字段速记

- `enable` / `pinned`：字符串 `"T"` / `"F"`
- `visibility`：`load` \| `public` \| `private`
- 软删：`DELETE /notes/{id}`；硬删：`?hard=1`（仅回收站）

## 扩展能力（按需）

| 需求 | 接口 |
|------|------|
| 分享链接 | `POST /notes/{id}/sharelink` |
| 定向分享 | `POST /notes/{id}/share` |
| 收件箱分享笔记 | `GET /shared` |
| 批量删/移/去重 | `POST /notes/bulk/delete` 等 |
| 双向链接 | `GET/POST /notes/{id}/links` |

完整端点表见 [reference.md](reference.md)。

## Python 最小示例

```python
import os
import requests

BASE = os.getenv("KENG_API_BASE", "https://kengnote.com/keng/api")
KEY = os.environ["KENG_API_KEY"]  # 从用户/环境获取，勿硬编码
H = {"Authorization": f"Bearer {KEY}"}

def upsert_experience(keyword: str, new_block: str) -> dict:
    notes = requests.get(f"{BASE}/notes", headers=H, params={"q": keyword}, timeout=30).json()
    if notes:
        nid = notes[0]["id"]
        old = requests.get(f"{BASE}/notes/{nid}", headers=H, timeout=30).json()
        content = (old.get("content") or "").rstrip() + "\n\n" + new_block.strip()
        return requests.put(f"{BASE}/notes/{nid}", headers=H, json={"content": content}, timeout=30).json()
    return requests.post(
        f"{BASE}/notes",
        headers=H,
        json={"content": new_block, "tags": ["agent"]},
        timeout=30,
    ).json()
```
