# Mubu Integration

> 幕布（mubu）与 Obsidian 集成：将幕布大纲导入 Obsidian、把 Markdown 同步到幕布、查询/导出幕布笔记。触发词：幕布、mubu、幕布导入 Obsidian、mubu 同步、幕布笔记导出

- **Type:** Skill
- **Install:** `agentstack add skill-liuboacean-mubu-integration-mubu-integration`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [liuboacean](https://agentstack.voostack.com/s/liuboacean)
- **Installs:** 0
- **Category:** [Productivity](https://agentstack.voostack.com/c/productivity)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [liuboacean](https://github.com/liuboacean)
- **Source:** https://github.com/liuboacean/mubu-integration
- **Website:** https://clawhub.ai/liuboacean/skills/mubu-integration

## Install

```sh
agentstack add skill-liuboacean-mubu-integration-mubu-integration
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 幕布集成 Skill

幕布（mubu.com）是一款极简大纲工具，支持将大纲一键转为思维导图。本 Skill 提供 API 集成能力。

## 权限与安全边界
本 Skill 以你的幕布账号身份操作**远程真实内容**，使用前请知悉其权限边界：
- **读取**：仅读取环境变量 `MUBU_PHONE` / `MUBU_PASSWORD` / `MUBU_MEMBER_ID`（环境变量未设置时，才由仓库外的 `~/.workbuddy/.env.mubu` 补全；`MUBU_MEMBER_ID` 即幕布 colla 成员 ID，仅 `save` 写回需要，登录后自动缓存到 `~/.mubu_token`）。
- **写入**：仅在本地写入 Token 缓存文件 `~/.mubu_token`（权限 `0o600` + 跨进程 `fcntl` 锁），不写入其它文件。
- **网络**：仅访问 `api2.mubu.com`（base URL 可由 `MUBU_BASE_URL` 覆盖，但仅限 `mubu.com` 家族域名，防 MITM），**无第三方服务、无遥测、无数据外发**。
- **写操作需确认**：真实会改动幕布内容的写操作为 `create`（新建）、`rename_folder`（重命名文件夹）、`save`（保存文档，端点 `/colla/events`，需 `member_id`）、`move`（移动，端点 `/list/custom/drag`）、`rename_doc`（文档重命名，端点 `/list/rename_doc`）、`purge`（彻底删除，唯一真实调用服务端删除，不可逆）；`delete` 现为**软删除**（仅标记进本地回收站，云端副本仍在）。`save` / `move` / `rename_doc` 已在 v1.3.9 经真机验证可用。所有真实写操作均需显式传 `--yes` 才执行，否则中止并提示。
- **信任边界**：Skill 不读取你的其它本地文件、不执行与幕布无关的 shell 命令；它只做「登录 → 读写你的幕布文档」这一件事。

## 功能概览

| 功能 | 接口 | 说明 |
|------|------|------|
| 用户登录 | `POST /user/phone_login` | 手机号密码登录获取 Token |
| Token 刷新 | 自动处理 | access_token 2小时过期，临近过期自动重新登录（重登仅重试 1 次，杜绝死循环） |
| 创建文件夹 | `POST /list/create_folder` | 在指定位置创建文件夹 |
| 创建文档 | `POST /list/create_doc` | 创建新的大纲文档 |
| 获取列表 | `POST /list/get` | 获取文件夹下的文档列表 |
| 获取文档 | `POST /document/edit/get` | 获取文档详细内容（真实端点；body 为 docId+password+isFromDocDir，返回 data.definition 为 JSON 字符串需二次解析） |
| 更新文档 | `POST /colla/events` | 保存/更新文档内容（v1.3.9 起可用；colla 协同端点，`events` 承载 changeset；需 `member_id`，私人文档由 `MUBU_MEMBER_ID` 或 token 缓存提供）|
| 删除文档 | `POST /list/delete_doc` | 删除文档（按类型区分端点） |
| 删除文件夹 | `POST /list/delete_folder` | 删除文件夹（原 `/list/delete` 实测非法，已弃用） |
| 移动文档 | `POST /list/custom/drag` | 移动文档/文件夹到其他位置（v1.3.9 起可用；body `dst/src/folderId`，已真机验证）|
| 导出 Markdown | 本地转换 | 将大纲结构转换为 Markdown |

## API 基础信息

- **Base URL**: `https://api2.mubu.com/v3/api`
- **认证方式**: JWT Token，通过请求头 `Jwt-Token` 传递
- **Content-Type**: `application/json;charset=UTF-8`

## 环境变量配置

在使用前，需要配置以下环境变量：

```bash
export MUBU_PHONE="your_phone_number"    # 幕布账号手机号
export MUBU_PASSWORD="your_password"      # 幕布账号密码
# 可选：幕布 colla 成员 ID（仅 save 写回需要；登录后自动缓存到 ~/.mubu_token，一般无需手动设置）
export MUBU_MEMBER_ID="your_collab_member_id"
```

> 切勿在脚本或代码中硬编码明文密码；凭据仅通过环境变量或仓库外的
> `~/.workbuddy/.env.mubu` 提供。

---

## 使用说明

### 1. 使用 MubuClient

所有操作都通过 `scripts/mubu/client.py` 中的 `MubuClient` 类完成（`scripts/mubu_api.py` 仅为向后兼容的重新导出 shim，不再建议直接使用；**不再有**独立的
`login()` / `create_folder()` / `create_doc()` / `get_list()` / `get_doc()` / `save_doc()` /
`delete_item()` 模块级函数）。实例化时自动读取 `MUBU_PHONE` / `MUBU_PASSWORD`
环境变量（或 `~/.workbuddy/.env.mubu`）并加载本地缓存 Token：

```python
from mubu.client import MubuClient

# 登录：凭据来自环境变量；返回扁平 data（token / id / name）
client = MubuClient()
info = client.login()
print(info["user_id"], info["username"])   # 注意是扁平 data["id"]，非 data["user"]["id"]

# 按名称本地搜索文档/文件夹（递归遍历，大小写不敏感）
results = client.search("项目", max_depth=3, limit=50)["results"]
for r in results:
    print(r["type"], r["name"], r["path"])
```

> 登录返回结构为**扁平** `data`：`data["id"]`=用户 ID，`data["name"]`=用户名，
> `data["token"]`=令牌。这与旧版嵌套 `result["data"]["user"]["id"]` 不同。

---

## 大纲内容格式

幕布文档内容使用特定的 JSON 格式表示大纲结构：

```json
{
  "name": "文档标题",
  "nodes": [
    {
      "id": "node_1",
      "text": "一级标题",
      "children": [
        {
          "id": "node_1_1",
          "text": "二级标题",
          "children": []
        }
      ],
      "collapsed": false,
      "finish": false,
      "modified": 0
    },
    {
      "id": "node_2",
      "text": "另一个一级标题",
      "children": [],
      "collapsed": false,
      "finish": false,
      "modified": 0
    }
  ]
}
```

---

## Token 管理建议

由于幕布的 access_token 仅约 2 小时有效（无 refresh_token 机制，代码也无任何 refresh 逻辑），建议：

1. **本地缓存**: 将 Token 保存到本地文件（如 `~/.mubu_token`）
2. **自动刷新**: 在 Token 快过期时自动刷新
3. **错误重试**: 遇到 401 错误时重新登录

```python
import os
import time
import json
import tempfile

TOKEN_FILE = os.path.expanduser("~/.mubu_token")

def save_token(token_data):
    """原子写 + 仅属主可读写：避免中途崩溃留下残缺文件，并防止其它用户读取。"""
    token_data = dict(token_data)
    token_data["expires_at"] = time.time() + 7200  # 2小时后过期
    # 注：真实 scripts/mubu_api.py 的 _save_token 还会用跨进程 fcntl.flock
    # advisory 锁包裹整段写（M4 已做成跨平台安全：无 fcntl 平台降级为无锁）；
    # 此处省略锁，聚焦写盘逻辑。
    dir_name = os.path.dirname(TOKEN_FILE) or "."
    fd, tmp = tempfile.mkstemp(dir=dir_name, prefix=".mubu_token.", suffix=".tmp")
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(token_data, f)
        os.chmod(tmp, 0o600)        # 仅属主可读写
        os.replace(tmp, TOKEN_FILE) # 原子重命名，避免残缺文件
    except Exception:
        if os.path.exists(tmp):
            os.remove(tmp)
        raise

def load_token():
    """从本地加载未过期的 Token；已过期或损坏则返回 None。"""
    if not os.path.exists(TOKEN_FILE):
        return None
    try:
        with open(TOKEN_FILE) as f:
            data = json.load(f)
    except Exception:
        return None
    if time.time() >= data.get("expires_at", 0):  # 已过期视为无效
        return None
    return data
```

说明：原示例中朴素的 `is_token_valid` 已移除——其职责（"是否过期"）已并入 `load_token`，仅返回未过期的 token。真实实现 `scripts/mubu_api.py` 的 `_save_token` 还包含跨进程 `fcntl.flock` 锁与统一的 `TOKEN_FILE_MODE` 权限管理，此处不再重复。

---

## 导出 / 导入 Markdown

M1 已落地真正的 Markdown 导入/导出（往返保真，不再是占位）。核心纯函数位于 `scripts/mubu_api.py`：

```python
def doc_to_markdown(node, level=0):
    """将节点（及子树）渲染为 Markdown 列表片段。
    '- ' 列表项，缩进 = 2 * level；含 checked → '- [x]'/'- [ ]'；
    含 note → 子树后追加 '> {note}'。根标题由 export_markdown 负责。"""
    ...

def export_markdown(doc):
    """doc 为 get_doc() 返回的 data 层 {"node": {...}}。
    首行 '# 标题'，其余递归为 '- ' 列表。结构无效时抛 MubuError。"""
    ...

def markdown_to_doc(md):
    """Markdown 文本 → {"node": {"id": "root", "text": ..., "children": [...]}}。
    标题为顶层节点；多标题时首个为 root，其余作为 root 的 children；
    列表项用栈按缩进深度维护层级；'- [ ]'/'- [x]' 设 checked；
    '> ' 作为对应层级节点的 note。"""
    ...
```

导出示例（幕布 → Markdown）：

```
# 读书笔记
- 第一章
  - [x] 读完
  - [ ] 写笔记
> 第一章的备注
```

> 说明：根节点的 `text` 渲染为 `# 标题`，其直接子节点从缩进 0 的 `- ` 列表开始；
> note 出现在其所属节点（含子树）之后，并按缩进深度归属到对应节点。

---

## 命令参考（CLI）

| 命令 | 说明 |
|------|------|
| `login` | 手机号密码登录，Token 本地缓存 |
| `list --folder  [--include-trash]` | 获取文件夹下的文档/子文件夹列表（`--json` 输出原始 JSON；`--include-trash` 包含已软删除项） |
| `mkdir  --parent ` | 创建文件夹 |
| `create  --folder  [--content ] [--md ]` | 创建文档；`--md` 从 Markdown 文件导入 |
| `get  [--export markdown\|json]` | 获取文档；`--export markdown` 输出真实 Markdown |
| `save  [--file ] [--md ] [--content ]` | 保存文档；`--md` 从 Markdown 文件导入 |
| `delete  [--type doc\|folder] --yes` | **软删除**：移入本地回收站（云端仍在，`restore` 可恢复，`purge` 可彻底删除）；`--type` 默认 folder，必须显式 `--yes` 才执行 |
| `restore ` | 从本地回收站恢复（仅移除标记，零服务端调用） |
| `purge  --yes` | **彻底删除（不可逆）**：调用服务端真实删除 API 后移除本地标记；必须显式 `--yes` 才执行 |
| `trash` | 列出本地回收站中已软删除的项 |
| `move  --target  [--type doc\|folder]` | 移动文档/文件夹到其他位置（v1.3.9 起可用，端点 `/list/custom/drag`，已真机验证）|
| `search  [--max-depth N] [--limit N] [--include-trash]` | 按名称本地搜索文档/文件夹（递归遍历，大小写不敏感；`--include-trash` 包含已软删除项） |
| `export-tree --folder  [--output ] [--max-depth N]` | 递归导出整个文件夹树为嵌套 Markdown 文件 |
| `rename  --name  [--type doc\|folder]` | 重命名文档（`/list/rename_doc` 端点）或文件夹（已验证端点 `/list/rename_folder`，`folderId` 填自身 id）|
| `opml  [--format opml\|freeplane]` | 导出为 OPML 2.0 / FreeMind XML（兼容 XMind 等其它大纲工具）|

### 软删除 / 回收站（v1.3.5）

`delete` 现在不再是真正的服务端删除，而是**软删除**：

- `delete  [--type doc\|folder] --yes` —— **移入本地回收站**：仅把项的元数据标记进本地回收站文件 `~/.workbuddy/.mubu_trash.json`，**云端副本保持不变**，不调用任何删除 API。缺省（无 `--yes`）仅打印提示并退出，绝不软删除。
- `restore ` —— 从本地回收站恢复：仅移除本地标记，**零服务端调用**（即使云端项已不存在也安全）。未找到该项时提示「未找到回收站项」。
- `purge  --yes` —— **彻底删除（不可逆）**：唯一真正调用服务端删除 API（`delete_doc` / `delete_folder`）的操作，成功后移除本地标记。必须显式 `--yes`，否则中止。
- `trash` —— 列出本地回收站中已软删除的项（id / type / name / deleted_at）。

`list --include-trash` 与 `search  --include-trash` 可在列表中**包含**已软删除项（`get_list` / `search` 默认过滤回收站项）。

> 回收站仅存元数据快照（id / type / name / parent_id / deleted_at），作为「云端仍在、可恢复」的安全网，**不作为重建来源**。

Markdown 往返示例：

```bash
# 导出为 Markdown
python3 scripts/mubu_api.py get  --export markdown

# 从 Markdown 创建文档
python3 scripts/mubu_api.py create "我的文档" --folder  --md ./outline.md

# 从 Markdown 更新文档
python3 scripts/mubu_api.py save  --md ./outline.md

# 移动文档
python3 scripts/mubu_api.py move  --target 

# 按名称本地搜索文档/文件夹（递归遍历所有子文件夹，大小写不敏感）
python3 scripts/mubu_api.py search "项目"
python3 scripts/mubu_api.py search "项目" --json

# 递归导出整个文件夹树为嵌套 Markdown
python3 scripts/mubu_api.py export-tree --folder  --output ./backup

# 重命名文档 / 文件夹
python3 scripts/mubu_api.py rename  --name "新标题" --type doc
python3 scripts/mubu_api.py rename  --name "新文件夹名" --type folder

# 导出为 OPML / FreeMind
python3 scripts/mubu_api.py opml  --format opml
python3 scripts/mubu_api.py opml  --format freeplane
```

---

## Token 刷新策略

- access_token 有效期约 2 小时，本地以 `expires_at` 缓存于 `~/.mubu_token`。
- 每次请求发起前调用 `ensure_valid_token()`：若未持有 token，或距过期不足
  `300 + 60`（leeway）秒，则使用缓存的 `phone`/`password` **重新登录**获取新 token。
- **刷新不依赖 refresh_token**（M1 未启用）。
- **鉴权失败仅重试 1 次**：`_request` 捕获 401 / 登录失效类错误后重新登录并重试最多一次；
  第二次仍失败则抛出 `MubuError`，**不再重登**，避免密码错误/账号封禁场景下的死循环。
- 403（权限不足）或其它非 0 业务 code **不触发重登**。
- Token 写入采用原子写（先写 `.tmp` 再 `os.rename`），写完追加 `os.chmod(TOKEN_FILE, 0o600)`，
  确保 Token 文件仅属主可读写（M2 的 T5 已实现）。

---

## 配置说明

脚本通过环境变量读取凭据（优先级：环境变量 > `~/.workbuddy/.env.mubu` 文件；
两者皆无时，`login` 子命令会交互式提示输入，绝不接受明文命令行参数）：

```bash
export MUBU_PHONE="你的手机号"
export MUBU_PASSWORD="你的密码"
# 可选：幕布 colla 成员 ID（仅 save 写回需要；登录后自动缓存到 ~/.mubu_token，一般无需手动设置）
export MUBU_MEMBER_ID="你的幕布 colla 成员 ID"
```

也可在 `~/.workbuddy/.env.mubu` 中配置（由 Skill 宿主加载为环境变量，且仅属主可读写）：

```
MUBU_PHONE=你的手机号
MUBU_PASSWORD=你的密码
# 可选：幕布 colla 成员 ID（仅 save 写回需要；登录后自动缓存，一般无需手动设置）
MUBU_MEMBER_ID=你的幕布 colla 成员 ID
```

---

## 已知限制（M1）

- `expand`（幕布大纲的折叠/展开状态）不在本期往返范围内，导入后节点默认展开。
- 有序列表 `1.` 不被解析，仅支持无序列表 `- `。
- 图片 / 附件类型节点不在本期 Markdown 往返范围内（会丢失媒体内容）。
- 多个顶层标题导入时，首个为 root，其余作为 root 的 children；其后的列表项统一挂在
  root 下（规范未要求按标题再嵌套）。
- `save`（文档保存）需要幕布 colla 成员 ID：私人文档的 `memberId` 任何 API 都不暴露，由 `MUBU_MEMBER_ID` 环境变量提供，或登录后随 Token 缓存写入 `~/.mubu_token`。缺失时会明确报错（不影响 `get` / `create` / `move` / `rename` 等其它操作）。v1.3.9 已修复 `save`（`/colla/events`）、`move`（`/list/custom/drag`）、doc `rename`（`/list/rename_doc`）的真机可用性。

---

## 注意事项

1. **非官方 API**: 幕布未提供官方开放平台，此 Skill 基于逆向分析实现
2. **稳定性**: API 可能随版本更新而变化，如遇问题请反馈
3. **频率限制**: 请勿频繁调用，避免触发限流
4. **数据安全**: Token 存储在本地，请勿泄露

---

## Agent 使用指引

当用户提到幕布、mubu 相关操作（如将幕布大纲导入 Obsidian、把 Markdown 同步到幕布、查询/导出幕布笔记）时，使用本 Skill 的脚本完成操作。

### 前置检查

1. 确认系统已安装 Python 3 和 requests 库：
   ```bash
   python3 -c "import requests; print('OK')"
   ```
   如果缺少 requests：`pip3 install requests`

2. 确认环境变量已配置：
   - `MUBU_PHONE` — 幕布手机号
   - `MUBU_PASSWORD` — 幕布密码
   - 如未配置，需提示用户先设置

### 脚本路径

```
~/.workbuddy/skills/mubu-integration/scripts/mubu_api.py
```

### 常用命令速查

| 用户意图 | 执行命令 |
|---------|---------|
| 登录幕布 | `python3 scripts/mubu_api.py login` |
| 查看文档列表 | `python3 scripts/mubu_api.py list` |
| 查看某文件夹 | `python3 scripts/mubu_api.py list --folder ` |
| 创建文件夹 | `python3 scripts/mubu_api.py mkdir "文件夹名"` |
| 创建文档 | `python3 scripts/mubu_api.py create "文档名" --folder ` |
| 从 Markdown 创建文档 | `python3 scripts/mubu_api.py create "文档名" --folder  --md outline.md` |
| 获取文档内容 | `python3 scripts/mubu_api.py get ` |
| 导出为 Markdown | `python3 scripts/mubu_api.py get  --export markdown` |
| 从 Markdown 保存文档 | `python3 scripts/mubu_api.py save  --md outline.md` |
| 从文件保存文档 | `python3 scripts/mubu_api.py save  --file content.json` |
| 移动文档 | `python3 scripts/mubu_api.py move  --target ` |
| 软删除（移入回收站） | `python3 scripts/mubu_api.py delete  --type doc\|folder --yes`（云端仍在，`restore` 可恢复）|
| 从回收站恢复 | `python3 scripts/mubu_api.py restore ` |
| 彻底删除（不可逆） | `python3 scripts/mubu_api.py purge  --yes`（必须显式 `--yes`，调用服务端真实删除）|
| 查看回收站 | `python3 scripts/mubu_api.py trash` |
| 按名称搜索 | `python3 scripts/mubu_api.py search  [--max-depth N] [--limit N]` |
| 按名称搜索（含回收站） | `python3 scripts/mubu_api.py search  [--max-depth N] [--limit N] --include-trash` |
| 按名称搜索（JSON） | `python3 scripts/mubu_api.py search  [--max-depth N] [--limit N] --json` |

### 典型工作流

**场景 1：用户说"把这份大纲同步到幕布"**
1. 确认内容来源（文件或对话中直接提供）
2. 如果是 Markdown，直接用脚本创建文档并导入
3. 返回新文档 ID 和链接

**场景 2：用户说"导出我的幕布笔记"**
1. 先列出文档列表让用户选择，或按名称搜索
2. 获取文档内容
3. 转换为 Markdown 格式返回

**场景 3：用户说"在幕布建一个项目文件夹"**
1. 确认文件夹名称和层级结构
2. 批量创建文件夹
3. 返回创建结果

---

## 工作流示例

### 示例 1: 从 Markdown 创建幕布文档

```
用户: 把这份 Markdown 大纲同步到幕布
```

执行步骤：
1. 解析 Markdown 结构
2. 转换为幕布 JSON 格式
3. 登录获取 Token
4. 创建文档并保存内容

### 示例 2: 导出幕布文档为 Markdown

```
用户: 导出我的"读书笔记"文档
```

执行步骤：
1. 登录获取 Token
2. 本地搜索匹配文档：`python3 scripts/mubu_api.py search "读书笔记"`
3. 获取文档内容
4. 转换为 Markdown 并返回

### 示例 3: 批量创建文件夹结构

```
用户: 在幕布创建项目文档结构：需求分析、设计文档、开发日志、测试报告
```

执行步骤：
1. 登录获取 Token
2. 创建项目文件夹
3. 批量创建子文件夹
4. 返回创建结果

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [liuboacean](https://github.com/liuboacean)
- **Source:** [liuboacean/mubu-integration](https://github.com/liuboacean/mubu-integration)
- **License:** MIT
- **Homepage:** https://clawhub.ai/liuboacean/skills/mubu-integration

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-liuboacean-mubu-integration-mubu-integration
- Seller: https://agentstack.voostack.com/s/liuboacean
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
