---
keng-manual-version: 1.4.4
updated-at: 2026-05-30
api-base: /keng/api
manual-url: /keng/api/manual
manual-meta-url: /keng/api/manual/meta
---

# keng 笔记系统使用手册（给 AI Agent 看的）

keng（坑）是一个用来记录踩坑经验的笔记系统，提供 REST API，任何 AI Agent 可以直接调用。

## 三步快速接入

1. 拿 Key：浏览器 Agent 优先读 Cookie `keng_api_key`；CLI/IDE Agent 让用户打开 `https://kengnote.com/keng/mykey.html` 复制 Key。
2. 验身份：先 `GET /me`，确认返回的 `id/username/nickname` 就是正在服务的用户。
3. 再做事：先 `GET /folders`、`GET /notes?q=关键词`，修改前必须 `GET /notes/{id}` 读取原文，再 `PUT /notes/{id}` 写回完整 Markdown。

最小写入测试：

```bash
curl -H "Authorization: Bearer {{api_key}}" \
  -H "Content-Type: application/json" \
  -d '{"content":"# Agent 接入测试\n这是一条测试笔记。","tags":["agent"],"folder_id":null}' \
  https://kengnote.com/keng/api/notes
```

> 不要把真实 API Key 写入仓库、公开笔记或分析报告。只能从 Cookie、`/keng/mykey.html`、`/api/my-key` 等私密来源读取。

## 接入地址

- API Base URL：`/keng/api`
- 当前用户：`{{username}}`
- 当前 API Key：`{{api_key}}`
- 笔记接口：`/keng/api/notes`
- 文件夹接口：`/keng/api/folders`
- 健康检查：`/keng/api/health`

> 当前部署在本机时，内网地址为 `http://127.0.0.1:8910`。公网统一使用 `https://kengnote.com/keng/api`，外网访问通过 nginx 反代 `/keng/api/`。

---

## 认证

### 如何获取 API Key

**如果你能读取用户浏览器（Manus、Operator、浏览器扩展等）：**
读取 Cookie `keng_api_key`（path=/keng，非 HttpOnly，JS 可读）。已登录则直接有值。
```javascript
const apiKey = document.cookie.split(';')
  .map(c => c.trim())
  .find(c => c.startsWith('keng_api_key='))
  ?.split('=')[1];
```

**如果你是 CLI/终端/IDE Agent（Goose、Claude Code、Cursor、Aider 等）：**
告诉用户：「请打开 https://kengnote.com/keng/mykey.html ，复制页面上的 Key 后粘贴给我」。
该页面已登录则自动展示 Key + 一键复制；未登录提示先登录。

**备用：凭 session cookie 获取：**
```
GET https://kengnote.com/keng/api/my-key
```
（无需 Authorization，凭浏览器 session cookie 返回 `{"api_key":"...","username":"..."}`）

### 使用 Key

公网或外部 AI Agent 调用 API 时，在每个请求加：

```
Authorization: Bearer {{api_key}}
```

`{{api_key}}` 是当前用户的个人 API Key，可在 `/keng/settings.html` 查看或重新生成。设置页默认隐藏 Key，点小眼睛才显示；复制按钮仍复制完整 Key。重新生成后旧 Key 立即失效。访客没有账号设置能力，主界面设置入口会置灰，直接打开设置页也会返回产品页。

先核对 Key 归属：

```bash
curl -H "Authorization: Bearer {{api_key}}" https://kengnote.com/keng/api/me
```

返回的 `id/username/nickname` 必须是用户本人。keng 没有对外使用的 `apiKeyTb`；Personal API Key 存在 `userTb.api_key`，但第三方 AI 不需要也不应该直接查库。

## 笔记路径表达

用户说 `keng>坑开发日志` 时，意思是：当前用户的 `keng` 文件夹下面，标题为“坑开发日志”的笔记。处理这种命令时：

1. `GET /keng/api/folders` 找到 `name == "keng"` 的文件夹 ID。
2. `GET /keng/api/notes` 读取当前用户笔记。
3. 用 `folder_id` 和标题（正文第一行去掉 `#`）定位目标笔记。

---

## 笔记字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| id | str | 笔记复合标识，只读；个人笔记形如 `personal:123`，团队笔记形如 `team:456` |
| raw_id | int | 数据库内自增主键，只读；只用于排查，不建议作为 API 路由参数 |
| note_scope | "personal" \| "team" | 笔记所属空间 |
| title | str | 取 content 第一行前 20 个字，只读（自动生成） |
| content | str | 笔记正文，Markdown 格式 |
| folder_id | int \| null | 所属文件夹 ID，null 表示无文件夹 |
| tags | list[str] | 标签数组 |
| comment | str | 备注，供 AI Agent 写分析/标注用，不影响正文，默认 "" |
| enable | "T" \| "F" | T=正常，F=回收站 |
| pinned | "T" \| "F" | T=置顶，F=普通 |
| visibility | "load" \| "public" \| "private" | 默认 public；load=陌生人/好友/本人可读全文，public=陌生人只看标题、好友和本人可读全文，private=仅本人 |
| author_id | int | 原创作者用户 ID，复制/收下后仍保留 |
| author_username | str | 原创作者用户名，用于显示 `原创：@username (id:userid)` |
| created_at | str | 创建时间，格式 `YYYY-MM-DD HH:MM:SS` |
| updated_at | str | 最后更新时间 |

---

## 笔记 API

### 获取笔记列表

```
GET /keng/api/notes
```

返回所有 `enable=T` 的笔记，按 `pinned DESC, updated_at DESC, id DESC` 排序。

支持可选查询参数：

| 参数 | 说明 | 示例 |
|------|------|------|
| `trash=1` | 返回回收站笔记 | `?trash=1` |
| `q=关键词` | 按正文内容模糊搜索 | `?q=nginx` |
| `tag=标签` | 按 tags 数组精确匹配某个标签 | `?tag=运维` |

`q` 和 `tag` 可同时使用（AND 关系）。

```
GET /keng/api/notes?trash=1
```

返回回收站（`enable=F`）的笔记。

**响应示例：**
```json
[
  {
    "id": 42,
    "title": "nginx反代配置踩坑记录",
    "content": "# nginx反代配置踩坑记录\n\n## 问题描述\n...",
    "folder_id": 3,
    "tags": ["nginx", "运维"],
    "comment": "",
    "enable": "T",
    "pinned": "T",
    "created_at": "2026-01-01 10:00:00",
    "updated_at": "2026-05-10 20:00:00"
  }
]
```

---

### 获取单条笔记

```
GET /keng/api/notes/{id}
```

---

### 新建笔记

```
POST /keng/api/notes
Content-Type: application/json

{
  "content": "# 标题\n\n正文内容",
  "folder_id": 3,        // 可选，放入指定文件夹
  "tags": ["tag1"],      // 可选
  "comment": "AI 备注"   // 可选，留给 AI 写分析，不显示在正文
}
```

返回新建的笔记对象（含 id），HTTP 201。

如果个人笔记位已满，返回 HTTP 403：

```json
{
  "error": "当前笔记位已用完...",
  "code": "NOTE_SLOT_LIMIT",
  "limit": 2,
  "used": 2,
  "requested": 1,
  "recharge_url": "https://www.kengnote.com/keng/settings.html?section=account"
}
```

第三方 AI 或自动化工具收到 `NOTE_SLOT_LIMIT` 时，不要把新内容写进已有笔记来绕过限制；应提示用户打开 `recharge_url` 购买笔记位或充值后再新建笔记。

---

### 更新笔记（部分更新）

```
PUT /keng/api/notes/{id}
Content-Type: application/json
```

请求体只需包含要改的字段，缺省字段保留原值：

| 场景 | 请求体 |
|------|--------|
| 更新内容 | `{"content": "新内容"}` |
| 移入文件夹 | `{"folder_id": 3}` |
| 移出文件夹 | `{"folder_id": null}` |
| 软删除（移入回收站） | 用 DELETE 接口 |
| 从回收站恢复 | `{"enable": "T"}` |
| 置顶 | `{"pinned": "T"}` |
| 取消置顶 | `{"pinned": "F"}` |
| 写 AI 备注 | `{"comment": "这篇和 nginx 有关，2026-05 改"}` |
| 更新标签 | `{"tags": ["python", "bug"]}` |

---

### 删除笔记

**软删除（移入回收站）：**
```
DELETE /keng/api/notes/{id}
```

**硬删除（彻底删除，仅 enable=F 的笔记可硬删）：**
```
DELETE /keng/api/notes/{id}?hard=1
```

---

## 文件夹 API

### 核对当前身份

```
GET /keng/api/me
Authorization: Bearer {{api_key}}
```

返回当前 Key 对应的用户、等级、联系方式绑定状态、`oauth_providers` 等。第三方 AI 开工前应先调用它，确认不是过期 Key、不是其他用户的 Key。

返回中还包含 `balance_cents`，单位是分。余额来自钱包充值流水，AI Agent 只应展示或提醒，不要自行改数据库余额。

---

### 钱包和充值状态

```
GET /keng/api/wallet
GET /keng/api/wallet/qrcode?url=...
POST /keng/api/wallet/recharge/orders
GET /keng/api/wallet/recharge/orders/{order_no}
POST /keng/api/alipay/order
GET /keng/api/alipay/orders/{order_no}
GET /keng/api/alipay/return
POST /keng/api/alipay/notify
POST /keng/api/stripe/order
GET /keng/api/stripe/sessions/{session_id}
POST /keng/api/stripe/webhook
POST /keng/api/wallet/note-slots/purchase
```

`GET /wallet` 返回当前用户余额、`price_per_note_slot_cents`、分页信息和 20 条钱包流水；每条流水的金额字段会返回 number，并带 `detail` 明细（例如微信储值、支付宝储值、Stripe 储值、购买笔记位）、`pay_channel` 和 `direction`（`income`/`expense`/`pending`）。可用 `tab=income|expense|ledger&page=1&page_size=20` 切换收入、支出和流水；旧参数 `recharge|consume|all` 仍兼容。收入按 `status='paid' AND amount_cents>0` 判断，支出按 `status='paid' AND amount_cents<0` 判断，流水为二者按时间穿插。创建充值订单只接受 `amount_cents`，支付是否成功以订单状态接口和服务端回调入账为准。

充值支持微信、支付宝和 Stripe 三个渠道：微信走 `POST /wallet/recharge/orders` 返回 `code_url` 生成二维码扫码支付；支付宝走 `POST /alipay/order`（body `{"amount_cents": 100, "subject": "可选商品名"}`）使用电脑网站支付（`alipay.trade.page.pay`），返回 `pay_url` 后前端直接跳转支付宝收银台，支付完成回跳 `settings.html?section=account` 并轮询 `GET /alipay/orders/{order_no}` 确认到账；Stripe 走 `POST /stripe/order`（body `{"amount_cents": 100, "subject": "可选商品名"}`）创建 Hosted Checkout Session，返回 `checkout_url/session_id/order_no`，前端跳转 Stripe 收银台，回跳 `settings.html?section=account&pay=stripe&stripe_session_id=...` 后轮询 `GET /stripe/sessions/{session_id}`，后端会主动查 Stripe Session，只有 `payment_status='paid'` 且金额匹配才入账。Stripe Webhook 地址为 `/keng/api/stripe/webhook`，配置 `webhook_secret` 后会验签并处理 `checkout.session.completed` / `checkout.session.async_payment_succeeded`；没有 webhook secret 时仍可通过回跳轮询补偿入账。`GET /wallet/qrcode` 只接受微信 `weixin://` 和支付宝官方 `qr.alipay.com` 二维码 URL。

`POST /wallet/note-slots/purchase` 使用当前余额购买笔记位，body: `{"slots": 10}`。后端会锁定用户余额和 `note_slots`，按 `sys_config_tb.price_per_note_slot_cents` 扣费，写入 `wallet_ledger_tb` 的 `consume` 流水；余额不足返回 `{"error":"您的余额不足了"}`。

free 用户只要有任意正数金额充值成功，就会升级为 vip，并把 `user_tb.note_slots` 校准为至少 `sys_config_tb.vip_note_slots`，不会降低已购买的更高额度。adm/admin/king 不再使用无限笔记位，当前固定初始化为 100000，也可以继续购买笔记位来测试扣费和消费流水。

### King 管理接口

```
GET /keng/api/admin/users?q=&page=1&page_size=50
PUT /keng/api/admin/users/{uid}
```

仅 king 用户可调用。`GET /admin/users` 返回用户列表、等级列表和分页信息，最多每页 50 条；`q` 会模糊匹配 id、用户名、昵称、等级、手机和邮箱。接口不会返回密码 hash，只返回 `api_key_hint`。`PUT /admin/users/{uid}` 可编辑用户核心资料和权限字段，包括 `level_id`、`note_slots`、`balance_cents`、联系方式绑定状态、启用状态、用户名锁定、国内/国际标记、好友搜索权限、`secre` 和 `persona`。

---

### 秘书自定义模型配置

```
GET /keng/api/me/ai-provider
POST /keng/api/me/ai-provider
```

用户可以在设置页给 Lucy/Ken 配置自己的 OpenAI 兼容 API。读取接口只返回 `api_key_hint`，不会返回明文 API Key。保存时 body 示例：

```json
{"enabled":"T","provider_label":"custom","base_url":"https://api.example.com","model":"model-name","api_key":"sk-..."}
```

如果只是修改名称、base_url 或 model，且不想替换 Key，可以把 `api_key` 传 `null`。

---

### Lucy 对话与附件

```
POST /keng/api/lucy/chat
POST /keng/api/lucy/image
GET /keng/api/lucy/image/status
```

`POST /lucy/chat` 接收 `content/history/persona/enhanced/session_tokens/session_cost_cents`。`enhanced=false` 时只能处理 KengNote 内部事情，`spider` 外部爬虫 action 会被拦截；`enhanced=true` 时允许当前唯一增强能力：爬虫。用户找笔记时 Lucy 返回 `search` action，前端直接刷新左侧列表；搜索匹配标题和正文，聊天气泡不展示内部 action JSON 或搜索结果表。

`POST /lucy/image` 使用 `[siliconflow] model = PaddlePaddle/PaddleOCR-VL-1.5` 做附件 OCR/视觉理解，body 为 `image_base64/media_type/context`。前端附件按钮和粘贴图片都只把图片加入待处理队列（最多 5 个），不会立即识别；用户输入要求并发送后再逐个调用该接口，把识别出的附件内容合并进 `/lucy/chat`。`GET /lucy/image/status` 返回 `{enabled, model}` 供前端判断能力状态。

Lucy 的 Markdown 回复在展示和写入笔记前都会过滤 `<think>`、`[ACTION]`、`[SAVE_NOTE]`、独立 action JSON/代码块和内部搜索结果表，并规范标题行前后空行，避免 `#` 标题识别失败。

---

### king 系统参数后台

```
GET /keng/api/admin/sys-config
PUT /keng/api/admin/sys-config/{key}
DELETE /keng/api/admin/sys-config/{key}
```

这些接口只允许 king 用户通过浏览器 session 调用，用来管理 `sys_config_tb` 的系统静态参数。普通 AI Agent 不应调用这些接口，除非用户明确要求并已在 king 账号下操作。

---

### 获取文件夹列表

```
GET /keng/api/folders
```

返回 `[{"id": 1, "name": "运维笔记", "parent_folder_id": null, "system_key": "", "undeletable": "F"}, ...]`。

`system_key` 非空的是系统文件夹，例如分享收件箱；第三方 AI 写普通笔记时通常选择 `system_key == ""` 的普通文件夹。

---

### 新建文件夹

```
POST /keng/api/folders
Content-Type: application/json

{"name": "文件夹名"}
```

---

### 重命名文件夹

```
PUT /keng/api/folders/{id}
Content-Type: application/json

{"name": "新名称"}
```

---

### 删除文件夹

```
DELETE /keng/api/folders/{id}
```

> 删除文件夹后，该文件夹下的笔记 `folder_id` 自动置 null，笔记本身不会被删除。

---

## 好友分享 API

### 搜索用户和公开资料

```
GET /keng/api/users/search?q=keyword
GET /keng/api/users/{uid}/profile
```

`q` 支持中国手机号前 8 位、已绑定 email 前 8 位、用户 id、用户名/昵称任意字符。公开资料会返回 `id/username/nickname/status/friend_status/reverse_status/mutual/can_add`，用于添加好友前确认身份。手机号和 email 搜索受对方隐私开关限制；关闭后不会被对应方式搜出。

### 在已有好友中实时查找

```
GET /keng/api/friends/search?q=keyword
```

仅在「当前已是好友」范围内检索，命中 `userid/username/nickname/email/手机` 任意字段，返回 `user_id/username/nickname/match_field/match_value`。`match_field` 取值 `id|username|nickname|phone|email`，`match_value` 为命中字段原值，供前端在昵称旁浮动显示命中条并高亮命中字符。用于好友列表实时过滤（随打字变化，无需按钮）。

### 发送好友申请

```
POST /keng/api/friends/request
Content-Type: application/json

{"user_id": 123, "message": "hi，我是 ken(@ken)，我们加个好友吧"}
```

`message` 可选，最长 200 字；已有 pending 申请时仍可再次发送，会更新当前 pending 请求留言并追加一条申请历史。被拒绝后也可以再次申请，历史消息和对方回复会保留。后端会限制高频搜索和好友申请；对方关闭“接收好友申请”时返回 403，但你同意对方已发来的 pending 申请不受影响。

### 好友隐私设置

```
GET /keng/api/me/friend-privacy
POST /keng/api/me/friend-privacy
Content-Type: application/json

{"allow_friend_request":"T","allow_phone_search":"T","allow_email_search":"F"}
```

三个字段都接受 `T/F` 或布尔值，默认 `T`。`allow_friend_request=F` 会拒绝陌生人发来的新好友申请；`allow_phone_search=F` 和 `allow_email_search=F` 分别禁止别人通过手机号前缀、邮箱前缀搜到当前用户。用户名、昵称和用户 id 搜索仍可用于明确身份确认。

### 好友申请历史和处理

```
GET /keng/api/friends/requests/history?user_id=123
PUT /keng/api/friends/{request_id}/accept
PUT /keng/api/friends/{request_id}/reject
Content-Type: application/json

{"reply": "暂不添加"}
```

申请历史返回最近 5 条双方申请的 `message/reply/status/mine`。拒绝会把回复写入历史；删除好友是单向删除，只删除当前用户指向对方的一条关系。收到的请求只在请求处理区同意/拒绝，用户搜索结果不把对方发来的 pending 请求显示成“添加”动作。

### 好友请求通知

```
GET /keng/api/friends/requests/count
GET /keng/api/friends/requests
GET /keng/api/friends/sent
GET /keng/api/friends/responded
POST /keng/api/friends/responded/seen
```

`requests/count` 返回收到的 pending 请求数量与我发出的申请被对方同意/拒绝后的未读响应数量之和。`responded` 只返回当前用户发出的申请中已被对方 `accepted/rejected` 且尚未读的记录，包含 `status/reply/updated_at/user_id/username/nickname`；打开铃铛面板后调用 `responded/seen` 标为已读。后端会兼容旧库缺少 `seen_by_requester` 字段的情况，先迁移再查询，避免点击铃铛时报请求失败。

### 浏览公开标题和好友笔记

```
GET /keng/api/users/{uid}/public-notes
GET /keng/api/users/{uid}/public-notes/{note_id}
POST /keng/api/friends/notes/{note_id}/copy-to-me
```

`load` 笔记对陌生人返回全文；`public` 笔记对陌生人返回 `title_only:true`，好友或本人可读全文。能读全文的好友笔记可以用 `copy-to-me` 抄回到自己的个人笔记。

### 分享笔记给好友

```
POST /keng/api/notes/{id}/share
Content-Type: application/json

{"to_username": "friend_username", "permission": "read"}
```

`permission` 可选 `read` 或 `write`。`write` 仅允许被分享者修改正文、tags、comment，不允许删除、移动文件夹、置顶或更改可见性。

### 搜索分享对象

```
GET /keng/api/share-targets?scope=friend&q=keyword
GET /keng/api/share-targets?scope=stranger&q=keyword
```

`scope=friend` 只返回双向好友；未输入 `q` 时返回常分享好友。`scope=stranger` 只返回非好友，必须输入搜索词。搜索支持昵称、用户名、用户 id。

### 撤销好友分享

```
DELETE /keng/api/notes/{id}/share/{to_user_id}
```

### 查看别人分享给我的笔记

```
GET /keng/api/shared
```

返回的笔记会带 `shared_by`、`shared_by_name`、`share_permission`，并通过 `can_edit` 标明当前用户是否可编辑。

### 生成公开只读链接

```
POST /keng/api/notes/{id}/sharelink
Content-Type: application/json

{"expires_hours": 24}
```

`expires_hours` 可省略，省略表示长期有效。返回 `url`，当前前端只读页为 `/keng/share.html?token=...`。

### 读取公开分享内容

```
GET /keng/api/sharelinks/{token}/info
```

无需登录。链接不存在返回 404，过期返回 410。

---

## 笔记链接 API

```
GET /keng/api/notes/{id}/links
POST /keng/api/notes/{id}/links
DELETE /keng/api/notes/{id}/links/{link_id}
```

建立链接时请求体为 `{"to_note_id": "personal:456"}` 或 `{"to_note_id": "team:456"}`。源笔记必须对当前用户可编辑，目标笔记必须对当前用户可见；可见范围包括自己的个人笔记、分享给我的笔记、当前成员可见的团队笔记，以及对当前用户开放的 `load/public` 笔记。不能链接到自己，重复链接只保留一条。团队里非作者/无编辑权成员只能查看已有链接，不能新增或删除链接。

---

## 典型工作流

### 记录一条踩坑经验

```bash
curl -X POST http://127.0.0.1:8910/api/notes \
  -H 'Content-Type: application/json' \
  -d '{"content": "# pip install 报 SSL 错误\n\n## 问题\n...\n\n## 解决\n..."}'
```

### 搜索笔记

```bash
# 按关键词搜索（正文包含该词）
curl "http://127.0.0.1:8910/api/notes?q=nginx"

# 按标签精确匹配
curl "http://127.0.0.1:8910/api/notes?tag=运维"

# 组合搜索（AND）
curl "http://127.0.0.1:8910/api/notes?q=nginx&tag=运维"
```

```python
import requests
results = requests.get("http://127.0.0.1:8910/api/notes", params={"q": "nginx"}).json()
```

### 读取一条笔记的完整内容

```bash
curl http://127.0.0.1:8910/api/notes/42
```

### 更新笔记内容

```bash
curl -X PUT http://127.0.0.1:8910/api/notes/42 \
  -H 'Content-Type: application/json' \
  -d '{"content": "# 标题\n\n更新后的内容"}'
```

### 把笔记移入回收站

```bash
curl -X DELETE http://127.0.0.1:8910/api/notes/42
```

### 从回收站恢复

```bash
curl -X PUT http://127.0.0.1:8910/api/notes/42 \
  -H 'Content-Type: application/json' \
  -d '{"enable": "T"}'
```

---

## 推荐工作流：续写同主题笔记

记录新经验前，建议先搜索有无同主题笔记，有则续写，无则新建，避免重复建笔记。

```python
import requests

BASE = "http://127.0.0.1:8910"

# 1. 按关键词（或标签）搜索
notes = requests.get(f"{BASE}/api/notes", params={"q": "nginx"}).json()

if notes:
    # 2. 找到同主题笔记 → 续写
    note = notes[0]
    new_content = note["content"] + "\n\n" + 新内容
    requests.put(f"{BASE}/api/notes/{note['id']}",
                 json={"content": new_content})
else:
    # 3. 没有同主题笔记 → 新建
    requests.post(f"{BASE}/api/notes",
                  json={"content": 新内容, "tags": ["nginx"]})
```

> **提示**：`PUT` 是部分更新，只传 `content` 不会清除 `tags`、`folder_id` 等其他字段。

---

# 格式要求

所有笔记必须遵循统一的 Markdown 格式，方便 AI 按照标准流程记录和更新。

## 标准模板

```markdown
# <概括行标题>经验
- 版本：<主版本号.次版本号.修订号>
- <YYYY-MM-DD>：<写笔记的AI名称> - <10字以内简要描述>

# 事件<描述事件类型>
## 解决方法 <描述解决方法>
## 坑 <描述遇到的坑>
```

## 格式说明

### 第一行标题
- 格式：`# <主题>经验`
- 作用：成为笔记的 title（API 自动从 content 首行提取最多 20 字）

### 版本号
- 格式：三段式 `主版本号.次版本号.修订号`，如 `1.0.0`
- 规则：每次更新内容时递增版本号

### 变更记录
- 格式：`- <日期>：<作者> - <简要描述 10 字以内>`
- 规则：每次更新追加一行，**按日期倒序排列**（最新的在最上面）
- 日期格式：`YYYY-MM-DD`
- 作者：填写写这条笔记的 AI 名称（如 `manus`、`copilot`、`kimi`）

### 事件结构
- 每个事件一个 `# 事件<描述事件类型>` 标题
- 下面跟两个二级标题：
  - `## 解决方法 <描述>`：问题的解决步骤或方案
  - `## 坑 <描述>`：遇到的陷阱、注意事项
- 可有多个事件

## 完整示例

```markdown
# nginx 反代经验
- 版本：1.0.2
- 2026-05-11：copilot - 新增 HTTP2 推送配置
- 2026-05-10：manus - proxy_pass 末尾斜杠问题

# 事件 1：proxy_pass 配置导致路径翻倍

## 解决方法 proxy_pass 末尾加斜杠
proxy_pass http://127.0.0.1:8910/;  # 必须加斜杠

## 坑 不加斜杠时路径会翻倍
请求 /api/notes 会变成 /api/api/notes
```

## 更新已有笔记时

1. 读取现有 `content`
2. 在变更记录顶部追加新一行（保持日期倒序）
3. 递增版本号
4. 追加或修改事件内容
5. 通过 `PUT /api/notes/{id}` 更新

---

## 注意事项

- 笔记 `content` 使用 Markdown 格式，第一行会被截取为 `title`
- `folder_id` 为 null（JSON null）表示不属于任何文件夹
- `pinned` 和 `enable` 是字符串 `"T"` / `"F"`
- 所有时间字段格式 `YYYY-MM-DD HH:MM:SS`
- 外网调用必须使用 `Authorization: Bearer <api_key>`；不要把真实 Key 写进笔记正文或公开文档

---

# 无法操作 API 时的备用方案：文件导入

如果你所处环境无法访问 API（超时、被屏蔽等），可以使用文件导入功能：
1. 按下面的 JSON 格式生成一个 `.json` 文件
2. 把文件发给用户下载
3. 用户在网页点击「导入笔记」按钮，把文件拖进弹窗，或点击弹窗选择这个文件
4. 系统自动将内容写入一条新笔记

## JSON 文件格式

```json
{
  "content": "# 标题经验\n- 版本：1.0.0\n- 2026-05-11：kimi - 初始记录\n\n# 事件\n## 解决方法 xxx\n## 坑 xxx",
  "tags": ["tag1", "tag2"],
  "folder_id": null,
  "comment": "导入自 kimi"
}
```

字段说明：

| 字段 | 是否必填 | 说明 |
|------|---------|------|
| `content` | **是** | 笔记正文，遵循格式要求章节 |
| `tags` | 否 | 标签数组，默认 `[]` |
| `folder_id` | 否 | 文件夹 ID，不放文件夹写 `null` |
| `comment` | 否 | AI 备注，默认 `""` |

## 上传接口

```
POST /keng/api/import
Content-Type: multipart/form-data
Authorization: Bearer {{api_key}}

form-data: file=<.json 文件>
```

```bash
curl -X POST http://127.0.0.1:8910/api/import \
  -H "Authorization: Bearer {{api_key}}" \
  -F "file=@keng_note.json"
```

返回创建成功的笔记对象，HTTP 201。`content` 字段缺失时返回 400 错误。

---

# 第三方笔记搬家

用户在 `/keng/settings.html?section=notes` 的“第三方笔记”里保存自己的配置后，可在首页“笔记搬家”读取外部笔记并导入 keng。所有配置都按当前用户隔离保存，搬家过程只读第三方笔记。

## Notion

1. 打开 `https://www.notion.so/my-integrations`，创建 Internal Integration 或复制已有 Integration Token。
2. 在 Notion 页面右上角 `...` → `Add connections`，把要搬家的顶层页面或数据库授权给这个 Integration。
3. 回到 `/keng/settings.html?section=notes` 保存 token，再到首页“笔记搬家”选择 Notion、勾选笔记导入。

## Simplenote

1. 打开 `/keng/settings.html?section=notes`。
2. 在 Simplenote 区域填写 Simplenote 邮箱和密码，点击“登录并保存 Token”。
3. keng 后端用这组账号密码向 Simperium 官方鉴权接口换取 token，只保存 token，不保存 Simplenote 密码。
4. 首页“笔记搬家”选择 Simplenote 后导入。搬家只读取 Simplenote，不写回、不删除第三方笔记。

## Joplin

1. 在 Joplin 桌面端启用 Web Clipper 服务。
2. 复制本地 Base URL（通常 `http://127.0.0.1:41184`）和 token，保存到 `/keng/settings.html?section=notes`。
3. 首页“笔记搬家”选择 Joplin 后导入。

## OneNote

1. 使用 Microsoft Graph 获取 delegated access token，权限至少包含 `Notes.Read`。
2. 在 `/keng/settings.html` 保存 token。
3. 首页“笔记搬家”选择 OneNote 后导入。Graph 不支持 app-only token 读取个人 OneNote。

## 导出文件型平台

- Evernote：导出 `.enex` 文件上传；官方 API key 需要申请和审核。
- Google Keep：通过 Google Takeout 导出 Keep zip 上传；Keep 没有稳定公开读取 API。
- Standard Notes：导出解密备份 JSON/zip 上传。
- Obsidian：把 Markdown vault 压缩为 zip 上传。

---

# 团队笔记 API

> **重要**：笔记 API 不再用 10001 数字阈值判断个人/团队。个人笔记 id 形如 `personal:123`，团队笔记 id 形如 `team:456`；批量接口的 `ids`、`PUT/DELETE /notes/{id}`、链接/分享接口都应使用这个复合标识。兼容旧纯数字 ID 时服务端默认按个人笔记处理；超出数据库自增 ID 范围的临时前端 ID 会被视为无效引用，链接接口返回空结果。

## 团队笔记字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| id | str | 团队笔记复合标识，形如 `team:456` |
| raw_id | int | teamNoteTb 内部自增主键 |
| title | str | 从 content 首行自动提取，只读 |
| content | str | 笔记正文，Markdown 格式 |
| folder_id | int \| null | 所属团队文件夹 ID，null 表示无文件夹 |
| tags | list[str] | 标签数组 |
| comment | str | AI 备注，不影响正文 |
| enable | "T" \| "F" | T=正常，F=回收站 |
| pinned | "T" \| "F" | T=置顶，F=普通 |
| created_at | str | 创建时间，`YYYY-MM-DD HH:MM:SS` |
| updated_at | str | 最后更新时间 |
| team_id | int | 所属团队 ID |
| team_name | str | 团队名称 |
| author_id | int | 作者用户 ID |
| author_name | str | 作者显示名 |
| locked_by | int \| null | 锁主用户 ID，null 表示未锁 |
| team_role | str | 当前用户在该团队的角色：owner/admin/member |
| can_edit | bool | 当前用户是否可编辑正文/标签/文件夹（作者且未锁定） |
| can_move | bool | 当前用户是否可移动团队文件夹（同 can_edit） |
| can_pin | bool | 当前用户是否可置顶/取消置顶（按权限表，目前 owner 可置顶，普通 member 不可置顶） |
| can_delete | bool | 当前用户是否可删除（作者或 owner，且未锁定） |
| can_lock | bool | 当前用户是否可锁定/解锁（作者、owner、admin；已锁时仅锁主可解锁） |
| user_id | null | 固定为 null（团队笔记无个人 user_id） |

## 团队权限规则

成员关系存储在 `teamMemberTb`：`team_id + user_id + role`。`teamTb` 的 JSON 字段仍保留兼容，但权限判断以同步后的成员角色为准。

| role | 权限 |
|------|------|
| owner | 改团队设置、解散团队、踢人、任命/取消管理员、置顶任意团队笔记、删除任意未锁团队笔记、锁定任意未锁团队笔记 |
| admin | 踢普通成员、锁定任意未锁团队笔记、管理团队文件夹；不能改团队设置、不能任命管理员、不能解散团队 |
| member | 新建团队笔记；只能编辑/移动/删除自己的未锁团队笔记，不能置顶，不能管理团队文件夹 |

锁定规则：`PUT /api/notes/{id}` 传 `{"locked_by": true}` 锁定，传 `{"locked_by": null}` 解锁。锁定后正文、标签、文件夹、置顶、删除均被阻止，只有锁主能先解锁。

## 团队成员角色 API

只有 owner 可以任命或取消管理员：

```
PUT /keng/api/teams/{team_id}/members/{user_id}/role
Content-Type: application/json

{"role": "admin"}
```

取消管理员：

```
{"role": "member"}
```

## 团队管理 API

> 所有团队管理 API 支持 Bearer API Key 认证（`Authorization: Bearer <api_key>`）。

### 列出我的团队

```
GET /keng/api/teams
```

返回当前用户加入的所有团队列表，含团队基本信息和当前用户角色。

### 创建团队

```
POST /keng/api/teams
Content-Type: application/json

{"name": "团队名称", "password": "可选加入密码"}
```

返回新建团队对象，HTTP 201。`password` 可选，为空或省略则无需密码加入。

### 加入团队

```
POST /keng/api/teams/join
Content-Type: application/json

{"team_id": 10001, "password": "可选"}
```

也可通过团队名称加入：`{"name": "团队名", "password": "可选"}`。返回团队信息。

### 搜索团队

```
GET /keng/api/teams/search?q=关键词
```

按名称或 ID 搜索团队，返回匹配结果列表。

### 获取团队详情

```
GET /keng/api/teams/{team_id}
```

返回团队详细信息，包括成员列表和各成员角色。

### 修改团队信息

```
PUT /keng/api/teams/{team_id}
Content-Type: application/json

{"name": "新名称", "password": "新密码"}
```

仅 owner 可操作。可修改名称和密码，只传需要修改的字段。

### 解散团队

```
POST /keng/api/teams/{team_id}/dismiss
```

仅 owner 可操作。**不可恢复**，团队笔记将一并删除。

### 退出团队

```
POST /keng/api/teams/{team_id}/leave
```

退出指定团队。owner 不能退出，须先转让或解散。

### 踢出成员

```
DELETE /keng/api/teams/{team_id}/members/{user_id}
```

admin 可踢 member，owner 可踢任何人（含 admin）。

## 团队笔记列表

```
GET /keng/api/notes?team_id={team_id}
```

支持额外参数：

| 参数 | 说明 |
|------|------|
| `trash=1` | 返回该团队的回收站笔记 |
| `q=关键词` | 正文模糊搜索 |
| `tag=标签` | 标签精确匹配 |

```bash
# 获取团队 10001 的笔记列表
curl "http://127.0.0.1:8910/api/notes?team_id=10001"

# 搜索团队笔记
curl "http://127.0.0.1:8910/api/notes?team_id=10001&q=nginx"

# 查看团队回收站
curl "http://127.0.0.1:8910/api/notes?team_id=10001&trash=1"
```

## 获取单条团队笔记

```
GET /keng/api/notes/{id}
```

这里的 `{id}` 使用复合标识，例如 `team:456`；个人笔记使用 `personal:123`。服务端按 scope 路由到 `noteTb` 或 `teamNoteTb`，不再依赖数字大小；旧纯数字 ID 仅作为个人笔记兼容入口。

## 新建团队笔记

```
POST /keng/api/notes
Content-Type: application/json

{
  "content": "# 标题\n\n正文",
  "team_id": 10001,
  "folder_id": null,
  "tags": ["tag1"],
  "comment": "AI 备注"
}
```

返回新建的团队笔记对象，HTTP 201。

## 更新团队笔记

```
PUT /keng/api/notes/{id}
Content-Type: application/json
```

按服务端返回的 `can_edit/can_pin/can_lock` 判断权限，只传需要改的字段：

| 场景 | 请求体 |
|------|--------|
| 更新内容 | `{"content": "新内容"}` |
| 移入团队文件夹 | `{"folder_id": 3}` |
| 移出文件夹 | `{"folder_id": null}` |
| 置顶 | `{"pinned": "T"}` |
| 从回收站恢复 | `{"enable": "T"}` |
| 锁定 | `{"locked_by": true}` |
| 解锁 | `{"locked_by": null}` |

## 删除团队笔记

按 `can_delete=true` 判断是否允许删除。已锁定笔记必须先由锁主解锁：

```bash
# 软删除（移入回收站）
curl -X DELETE http://127.0.0.1:8910/api/notes/{id}

# 硬删除（彻底删除，仅回收站中的笔记可硬删）
curl -X DELETE "http://127.0.0.1:8910/api/notes/{id}?hard=1"
```

---

## 团队文件夹 API

### 获取团队文件夹列表

```
GET /keng/api/team-folders?team_id={team_id}
```

返回 `[{"id": 1, "name": "文件夹名"}, ...]`

### 新建团队文件夹

```
POST /keng/api/team-folders
Content-Type: application/json

{"name": "文件夹名", "team_id": 10001}
```

返回 `{"id": 1, "name": "文件夹名"}`，HTTP 201。

### 重命名团队文件夹

```
PUT /keng/api/team-folders/{id}
Content-Type: application/json

{"name": "新名称"}
```

### 删除团队文件夹

```
DELETE /keng/api/team-folders/{id}
```

> 删除文件夹后，该文件夹下的团队笔记 `folder_id` 自动置 null，笔记本身不删除。

---

## 团队笔记推荐工作流

```python
import requests

BASE = "http://127.0.0.1:8910"
TEAM_ID = 10001

# 1. 搜索团队笔记
notes = requests.get(f"{BASE}/api/notes", params={"team_id": TEAM_ID, "q": "nginx"}).json()

if notes:
    # 找到同主题笔记 → 续写（注意只有 can_edit=True 才能改）
    note = notes[0]
    if note["can_edit"]:
        new_content = note["content"] + "\n\n" + 新内容
        requests.put(f"{BASE}/api/notes/{note['id']}", json={"content": new_content})
    else:
        # 无编辑权限，只能新建一条
        requests.post(f"{BASE}/api/notes", json={"content": 新内容, "team_id": TEAM_ID})
else:
    # 无同主题笔记 → 新建
    requests.post(f"{BASE}/api/notes", json={"content": 新内容, "team_id": TEAM_ID, "tags": ["nginx"]})
```
