---
keng-manual-version: 1.2.0
updated-at: 2026-05-13
api-base: /keng/api
manual-url: /keng/api/manual
manual-meta-url: /keng/api/manual/meta
---

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

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

## 接入地址

- 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`。外网访问通过 nginx 反代 `/keng/api/`。

---

## 认证

公网或外部 AI Agent 调用 API 时使用 Bearer Token 认证，在每个请求加：

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

`{{api_key}}` 是当前用户的个人 API Key，可在 `/keng/settings.html` 查看或重新生成。重新生成后旧 Key 立即失效。

---

## 笔记字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 笔记 ID，自增主键，只读 |
| 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=普通 |
| 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。

---

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

```
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/folders
```

返回 `[{"id": 1, "name": "运维笔记"}, ...]`

---

### 新建文件夹

```
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

### 分享笔记给好友

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

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

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

### 撤销好友分享

```
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。

---

## 典型工作流

### 记录一条踩坑经验

```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

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

```bash
curl -X POST http://127.0.0.1:8910/api/import \
  -F "file=@keng_note.json"
```

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

---

# 团队笔记 API

> **重要**：团队笔记 id ≥ 10001，个人笔记 id < 10001，路由规则由服务端自动判断。

## 团队笔记字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | ≥ 10001，自增主键 |
| 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） |
| 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"}
```

## 团队笔记列表

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

支持额外参数：

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

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

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

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

## 获取单条团队笔记

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

id ≥ 10001 时自动路由到 teamNoteTb，与个人笔记接口相同。

## 新建团队笔记

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

{
  "content": "# 标题\n\n正文",
  "team_id": 1,
  "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": 1}
```

返回 `{"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 = 1

# 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"]})
```
