Skill: jackie2010/getnote-1-5-7 (version 1.0.0)

--- name: Get笔记 description: | Get笔记 - 保存、搜索、管理个人笔记和知识库。 **当以下情况时使用此 Skill**： (1) 用户要保存内容到笔记：发链接、发图片、说「记一下」「存到笔记」「保存」「收藏」 (2) 用户要搜索或查看笔记：「搜一下」「找找笔记」「最近存了什么」「看看原文」 (3) 用户要管理知识库或标签：「加到知识库」「建知识库」「加标签」「删标签」 (4) 用户要配置 Get笔记：「配置笔记」「连接 Get笔记」 metadata: {"openclaw": {"requires": {}, "optionalEnv": ["GETNOTE_API_KEY", "GETNOTE_CLIENT_ID", "GETNOTE_OWNER_ID"], "baseUrl": "https://openapi.biji.com", "homepage": "https://biji.com"}} --- # Get笔记 Skill ## ⚠️ Agent 必读约束 ### 🌐 Base URL ``` https://openapi.biji.com ``` 所有 API 请求必须使用此 Base URL，不要使用 `biji.com` 或其他地址。 ### 🔑 认证 请求头： - `Authorization: $GETNOTE_API_KEY`（格式：`gk_live_xxx`） - `X-Client-ID: $GETNOTE_CLIENT_ID`（格式：`cli_xxx`） **每次调用 API 前先检查 `$GETNOTE_API_KEY` 是否存在**。若不存在，提示用户运行 `/note config` 完成配置，配置完成后再继续执行用户原本的请求。 Scope 权限：`note.content.read`（读取）、`note.content.write`（写入）、`note.recall.read`（搜索）。完整列表见 [references/api-details.md](references/api-details.md#scope-权限列表)。 ### 🔢 笔记 ID 处理规则（重要！） 笔记 ID 是 **64 位整数（int64）**，超出 JavaScript `Number.MAX_SAFE_INTEGER`，直接 `JSON.parse` 会**静默丢失精度**。 **正确做法**：始终把 ID 当字符串处理，在 `JSON.parse` 之前替换： ```javascript const safe = text.replace(/"(id|note_id|next_cursor|parent_id|follow_id|live_id)"\s*:\s*(\d+)/g, '"$1":"$2"'); const data = JSON.parse(safe); ``` Python / Go 等语言原生支持大整数，无此问题。 ### 🔒 安全规则 - 笔记数据属于用户隐私，不在群聊中主动展示笔记内容 - 若配置了 `GETNOTE_OWNER_ID`，检查 sender_id 是否匹配；不匹配时回复「抱歉，笔记是私密的，我无法操作」 - API 返回 `error.reason: "not_member"` 或错误码 `10201` 时，引导开通会员：https://www.biji.com/checkout?product_alias=6AydVpYeKl - 创建笔记建议间隔 1 分钟以上，避免触发限流 --- ## 指令路由表 > 匹配指令后，用 **read 工具**读取对应的 `references/xxx.md` 获取完整 API 文档。 | 指令 | 角色 | 说明 | 详细文档 | |------|------|------|---------| | `/note save` 或「记一下」| 📝 速记员 | 保存文本/链接/图片笔记（含异步轮询流程） | [references/save.md](references/save.md) | | `/note search` 或「搜一下」| 🔍 搜索官 | 全局语义搜索 + 知识库语义搜索 | [references/search.md](references/search.md) | | `/note list` 或「最近的笔记」| 📋 整理师 | 浏览列表、查看详情、更新、删除 | [references/list.md](references/list.md) | | `/note kb` 或「知识库」| 📚 图书管理员 | 知识库 CRUD + 博主订阅 + 直播订阅 | [references/knowledge.md](references/knowledge.md) | | `/note tag` 或「加标签」| 🏷️ 标签员 | 添加/删除标签 | [references/tags.md](references/tags.md) | | `/note config` 或「配置笔记」| ⚙️ 配置 | 配置 API Key 和 Client ID | [references/oauth.md](references/oauth.md) | --- ## 自然语言路由 ``` 包含 URL → /note save（link 模式） 包含图片 → /note save（image 模式） 「记/存/保存/收藏」 → /note save（text 模式） 「搜/找找/有没有 XX」 → /note search 「最近/列表/看看/查笔记」 → /note list 「改/更新/编辑笔记」 → /note list（更新笔记） 「知识库」相关 → /note kb 「标签」相关 → /note tag 「配置/授权/连接笔记」 → /note config ``` **决策原则**：优先匹配最具体的意图。有 URL 就是 `/save link`，有图片就是 `/save image`，不确定时询问用户。 --- ## 通用错误处理 ```json { "success": false, "error": { "code": 10001, "message": "unauthorized", "reason": "not_member" }, "request_id": "xxx" } ``` | 错误码 | 说明 | 处理方式 | |--------|------|---------| | 10000 | 参数错误 | 检查请求参数 | | 10001 | 鉴权失败 | 检查 API Key 和 Client ID，或重新授权 | | 10100 | 数据不存在 | 确认笔记/知识库 ID 正确 | | 10201 | 非会员 | 引导开通：https://www.biji.com/checkout?product_alias=6AydVpYeKl | | 10202 | QPS 限流 | 降低频率，查看 rate_limit 字段 | | 30000 | 服务调用失败 | 稍后重试 | | 50000 | 系统错误 | 稍后重试 | 详细错误码和限流结构见 [references/api-details.md](references/api-details.md)。

path: package.json
content: {
  "name": "getnote",
  "version": "1.5.7",
  "description": "Get笔记 Skill — 保存链接/图片/文本到笔记，语义搜索，知识库管理。说「记一下」就能存，说「搜一下」就能找。",
  "license": "MIT-0",
  "author": "[REDACTED_SECRET]",
  "homepage": "https://biji.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/iswalle/getnote-openclaw.git"
  },
  "bugs": {
    "url": "https://github.com/iswalle/getnote-openclaw/issues"
  },
  "keywords": [
    "get笔记",
    "笔记",
    "notes",
    "知识库",
    "语义搜索",
    "链接保存",
    "图片OCR",
    "录音转写",
    "标签管理",
    "openclaw",
    "skill"
  ],
  "clawhub": {
    "acceptLicenseTerms": true
  },
  "openclaw": {
    "minVersion": "0.1.0",
    "baseUrl": "https://openapi.biji.com",
    "requires": {},
    "optionalEnv": [
      "GETNOTE_API_KEY",
      "GETNOTE_CLIENT_ID",
      "GETNOTE_OWNER_ID"
    ]
  }
}


path: README.md
content: # Get笔记 Skill

[![License: MIT-0](https://img.shields.io/badge/License-MIT--0-blue.svg)](https://opensource.org/licenses/MIT-0)

让 AI 成为你的第二大脑。随时记录，需要时召回。

---

## ✨ 核心能力

| 能力 | 说明 |
|------|------|
| **📎 一键保存链接** | 发个链接就能保存，自动抓取原文、生成摘要。支持小宇宙播客、小红书、微信公众号、B站、抖音及任意公开网页 |
| **🖼 图片秒变笔记** | 发张图片自动 OCR 识别文字、AI 分析图片内容 |
| **🔍 语义搜索** | 不用翻全部笔记，直接搜关键词，AI 语义召回相关内容 |
| **📚 知识库管理** | 用知识库和标签整理笔记，还能订阅博主、获取直播总结 |
| **🎤 语音笔记原文** | 录音笔记支持获取完整转写文本 |

---

## 💡 使用场景

### ✏️ 随手记录

**通勤路上想到一个点子**

> 👤 记一下笔记：支付流程可以加一个进度条，用户等待时不焦虑
>
> 🤖 已记录，自动打上「产品优化」标签。

**听播客时有感触**

> 👤 刚听到一个观点挺好的，记一下：好的产品是让用户少做选择，不是多做选择
>
> 🤖 已记录。

**开完会**

> 👤 帮我记下刚才会上定的几件事：1. 下周三前完成设计稿 2. 找小王对接接口 3. 周五前给老板汇报进度
>
> 🤖 已记录。

---

### 🔍 要用时召回

**写周报时**

> 👤 帮我找找这周我记过的工作相关的东西
>
> 🤖 找到 5 条相关笔记：周一你记了客户反馈的问题、周三记了技术方案讨论、周四......

**被领导问到**

> 👤 上次我们讨论过用户分层的事，当时怎么说的来着？
>
> 🤖 11月8号你记过：高价值用户定义为月消费超过500元，占比约12%......

**写文章找素材**

> 👤 我想写篇关于做产品的文章，帮我找找我之前的相关想法
>
> 🤖 找到 8 条相关笔记，已按时间排序整理给你。

---

### 🔗 保存链接和图片

**看到好文章**

> 👤 存到笔记 https://example.com/article
>
> 🤖 链接已提交，正在抓取分析中...
> 🤖 搞定 ✓ 已保存：《如何做好产品设计》

**拍到有用的东西**

> 👤 [发送图片]
>
> 🤖 存到笔记？
>
> 👤 对
>
> 🤖 搞定 ✓ 图片已保存。

**探店记录**

> 👤 记一下，这家店叫船歌鱼水饺，招牌是鲅鱼饺子，人均80
>
> 🤖 已记录，打上「美食」标签。

---

## 📦 安装

### 方式一：通过 ClawHub 安装（推荐）

```bash
clawhub install getnote
```

### 方式二：让 AI 助手安装

> 帮我安装 Get笔记 skill，地址是 https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/SKILL.md

### 方式三：手动安装

```bash
mkdir -p ~/.openclaw/workspace/skills/getnote
cd ~/.openclaw/workspace/skills/getnote
curl -sL https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/SKILL.md -o SKILL.md
curl -sL https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/package.json -o package.json
```

---

## 🔑 配置

### 自动配置（默认）

安装后首次使用时，AI 会自动发起 OAuth 授权：

1. 你说「存到笔记」或任何笔记相关操作
2. AI 检测到未配置，自动生成授权链接
3. 点击链接，授权
4. 自动配置完成，继续执行你的请求

无需手动配置，无需记忆任何命令。

### 手动配置（可选）

1. 前往 **[Get笔记开放平台](https://www.biji.com/openapi)** 获取 API Key 和 Client ID
2. 在 `~/.openclaw/openclaw.json` 中添加：

```json
{
  "skills": {
    "entries": {
      "getnote": {
        "apiKey": "gk_live_xxx",
        "env": {
          "GETNOTE_CLIENT_ID": "cli_xxx",
          "GETNOTE_OWNER_ID": "ou_xxx"
        }
      }
    }
  }
}
```

> 💡 `GETNOTE_OWNER_ID` 可选，配置后只有你能操作笔记（群聊安全）

> 💡 需要 [Get笔记会员](https://www.biji.com/checkout?product_alias=6AydVpYeKl) 才能使用 API

---

## 🔐 安全说明

> ⚠️ **隐私保护**：笔记是你的私密数据，AI 会严格校验身份。

- 配置 `GETNOTE_OWNER_ID` 后，只有你能操作笔记
- 群聊中其他人无法通过 AI 读取你的笔记
- **不要在聊天中发送 API Key**，请手动配置到环境变量

---

## 🛠 支持的笔记类型

| 类型 | 说明 | 支持 |
|------|------|------|
| `plain_text` | 纯文本笔记 | ✅ 读写 |
| `link` | 链接笔记（自动抓取正文） | ✅ 读写 |
| `img_text` | 图片笔记 | ✅ 读写 |
| `audio` | 即时录音 | 📖 仅读取 |
| `meeting` | 会议录音 | 📖 仅读取 |
| `local_audio` | 本地音频 | 📖 仅读取 |
| `internal_record` | 内录音频 | 📖 仅读取 |
| `class_audio` | 课堂录音 | 📖 仅读取 |
| `recorder_audio` | 录音卡长录 | 📖 仅读取 |
| `recorder_flash_audio` | 录音卡闪念 | 📖 仅读取 |

> 💡 语音类笔记可读取 AI 摘要和转写原文，需调用详情接口获取。

---

## 📜 相关链接

- [Get笔记官网](https://biji.com)
- [开放平台](https://www.biji.com/openapi)
- [ClawHub](https://clawhub.ai/iswalle/getnote)
- [开通会员](https://www.biji.com/checkout?product_alias=6AydVpYeKl)

---

## License

MIT-0 (MIT No Attribution) · Published on [ClawHub](https://clawhub.ai)


path: references/api-details.md
content: # Get笔记 API 详细参考

## 目录

1. [错误码表](#错误码表)
2. [error.reason 取值表](#errorreason-取值表)
3. [限流响应结构](#限流响应结构)
4. [笔记详情独有字段](#笔记详情独有字段)
5. [新建笔记字段说明](#新建笔记字段说明)
6. [图片上传凭证返回字段](#图片上传凭证返回字段)

---

## 错误码表

| 错误码 | 说明 |
|--------|------|
| 10000 | 参数错误 |
| 10001 | 鉴权失败 |
| 10100 | 数据不存在（笔记不存在等）|
| 10201 | 非会员 |
| 10202 | QPS 限流 |
| 30000 | 服务调用失败 |
| 42900 | 配额限流 |
| 50000 | 系统错误 |

---

## error.reason 取值表

| reason | 说明 |
|--------|------|
| not_member | 非会员，引导开通：https://www.biji.com/checkout?product_alias=6AydVpYeKl |
| qps_global | 全局 QPS 超限 |
| qps_bucket | 桶级 QPS 超限 |
| quota_day | 当日配额用尽 |
| quota_month | 当月配额用尽 |

---

## 限流响应结构

429 错误时，`error` 字段中包含 `rate_limit` 对象：

```json
{
  "success": false,
  "error": {
    "code": 42900,
    "message": "rate limited",
    "reason": "quota_day",
    "rate_limit": {
      "read": {
        "daily":   {"limit": 1000,  "used": 1000, "remaining": 0,    "reset_at": 1741190400},
        "monthly": {"limit": 10000, "used": 3000, "remaining": 7000, "reset_at": 1743811200}
      },
      "write": {
        "daily":   {"limit": 200,  "used": 200, "remaining": 0,    "reset_at": 1741190400},
        "monthly": {"limit": 2000, "used": 600, "remaining": 1400, "reset_at": 1743811200}
      },
      "write_note": {
        "daily":   {"limit": 50,  "used": 50,  "remaining": 0,   "reset_at": 1741190400},
        "monthly": {"limit": 500, "used": 150, "remaining": 350, "reset_at": 1743811200}
      }
    }
  },
  "request_id": "xxx"
}
```

字段说明：
- `limit` - 配额上限
- `used` - 已使用量
- `remaining` - 剩余量
- `reset_at` - 重置时间（Unix 时间戳，秒）

---

## 笔记详情独有字段

调用 `GET /open/api/v1/resource/note/detail?id={note_id}` 时返回，列表接口不包含这些字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `audio.original` | string | 语音转写原文 |
| `audio.play_url` | string | 音频播放地址（有效期限制） |
| `audio.duration` | int | 音频时长（秒） |
| `web_page.content` | string | 链接网页完整原文 |
| `web_page.url` | string | 原始链接地址 |
| `web_page.excerpt` | string | AI 生成的摘要 |
| `attachments[]` | array | 附件列表 |
| `attachments[].type` | string | 附件类型：audio \| image \| link \| pdf |

---

## 新建笔记字段说明

`POST /open/api/v1/resource/note/save` 请求体字段：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | string | 否 | 笔记标题 |
| `content` | string | 否 | Markdown 格式正文 |
| `note_type` | string | 否 | 笔记类型，默认 `plain_text`：<br>• `plain_text` - 纯文本（同步）<br>• `link` - 链接笔记（异步，须轮询）<br>• `img_text` - 图片笔记（异步，须轮询） |
| `tags` | string[] | 否 | 标签名称列表 |
| `parent_id` | int64 | 否 | 父笔记 ID，创建子笔记时填写，默认 0 |
| `link_url` | string | link 类型必填 | 要保存的链接 URL |
| `image_urls` | string[] | img_text 类型必填 | 图片访问地址列表，使用上传凭证中的 `access_url` |

**返回说明**：
- `plain_text`：同步返回，`data.note_id` 即为笔记 ID
- `link` / `img_text`：返回 `data.task_id`，需轮询 `/task/progress` 至 `status=success` 或 `failed`

---

## 图片上传凭证返回字段

`GET /open/api/v1/resource/image/upload_token?mime_type=jpg&count=1` 返回 `data.tokens[]`，每个 token 包含：

| 字段 | 说明 |
|------|------|
| `host` | OSS 上传地址（POST 目标 URL） |
| `object_key` | 文件在 OSS 上的路径 |
| `accessid` | OSS AccessKey ID |
| `policy` | 上传策略（Base64 编码） |
| `signature` | 请求签名 |
| `callback` | 回调参数（Base64 编码，必须包含在上传请求中） |
| `access_url` | 上传成功后的文件访问地址，**创建图片笔记时填入 `image_urls`** |
| `oss_content_type` | 上传时需设置的 Content-Type（与 mime_type 对应） |

⚠️ `mime_type` 必须与实际文件格式一致，否则 OSS 签名验证失败。

---

## Scope 权限列表

| Scope | 说明 |
|-------|------|
| note.content.read | 读取笔记（列表、详情）|
| note.content.write | 添加或修改笔记 |
| note.tag.read | 获取标签列表 |
| note.tag.write | 修改标签 |
| note.topic.read | 获取知识库笔记 |
| note.topic.write | 笔记加入或移除知识库 |
| topic.read | 获取知识库信息 |
| topic.write | 创建和编辑知识库 |
| note.recall.read | 笔记内容搜索 |
| note.topic.recall.read | 知识库内容搜索 |
| note.image.upload | 获取上传图片签名 |
| note.content.trash | 笔记移入回收站 |
| topic.blogger.read | 获取知识库订阅博主 |
| topic.live.read | 获取知识库订阅直播 |

---

## 博主内容字段说明

| 字段 | 说明 |
|------|------|
| post_id_alias | 内容 ID，查详情时必用 |
| post_name | 内容名称（原标题）|
| post_type | 类型：video / audio / article / live |
| post_cover | 封面图 |
| post_title | AI 生成标题 |
| post_summary | AI 摘要（Markdown）|
| post_url | 原文链接 |
| post_icon | 博主头像 |
| post_subtitle | 副标题 |
| post_media_text | 原文内容（仅详情接口返回）|
| post_create_time | 创建时间（YYYY-MM-DD HH:MM:SS）|
| post_publish_time | 发布时间（YYYY-MM-DD HH:MM:SS）|

---

## 直播字段说明

| 字段 | 说明 |
|------|------|
| live_id | 直播 ID，查直播详情时必用 |
| follow_id | 订阅关系 ID |
| name | 直播名称 |
| cover | 封面图 |
| sub_title | 副标题 |
| link | 直播链接 |
| platform | 平台（如 DEDAO）|
| status | 直播状态（已结束为 FINISHED）|
| follow_time | 订阅时间（YYYY-MM-DD HH:MM:SS）|

---

## 召回结果字段说明

### 笔记召回 (recall) 返回字段

| 字段 | 说明 |
|------|------|
| id | 笔记 ID |
| note_id | 笔记 ID（字符串格式）|
| title | 标题 |
| content | 摘要内容 |
| note_type | 笔记类型 |
| tags | 标签列表 |
| created_at | 创建时间 |

### 知识库召回 (recall/knowledge) 返回字段

| 字段 | 说明 |
|------|------|
| id | 内容 ID |
| content_type | 内容类型：note / blogger / live |
| title | 标题 |
| summary | 摘要 |
| source_type | 来源类型 |
| created_at | 创建时间 |

---

## OSS 上传详细示例

```bash
# 1. 获取上传凭证
curl 'https://openapi.biji.com/open/api/v1/resource/image/upload_token?mime_type=jpg&count=1' \
  -H 'X-Client-ID: {client_id}' \
  -H 'Authorization: {api_key}'

# 2. 上传到 OSS（使用返回的凭证）
curl -X POST '{host}' \
  -F 'key={object_key}' \
  -F 'policy={policy}' \
  -F 'OSSAccessKeyId={accessid}' \
  -F 'signature={signature}' \
  -F 'callback={callback}' \
  -F 'Content-Type=image/jpeg' \
  -F 'file=@/path/to/image.jpg'

# 3. 创建图片笔记
curl -X POST 'https://openapi.biji.com/open/api/v1/resource/note/save' \
  -H 'X-Client-ID: {client_id}' \
  -H 'Authorization: {api_key}' \
  -H 'Content-Type: application/json' \
  -d '{"note_type": "img_text", "content": "描述", "image_urls": ["{access_url}"]}'
```


path: references/knowledge.md
content: # 知识库管理

## 概述

知识库的 CRUD 操作，以及知识库内笔记管理、博主订阅内容、直播订阅内容的查询。

---

## 我的知识库列表

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/list?page=1
```

参数：
- `page`: 页码，从 1 开始，默认 1（固定每页 20 条）

返回：`topics[]`、`has_more`、`total`

每个 topic 包含：
- `topic_id`：知识库 ID（字符串，后续所有操作接口的 `topic_id` 参数均传此值）
- `name`、`description`、`cover`
- `created_at` / `updated_at`：时间字符串（YYYY-MM-DD HH:MM:SS）
- `stats`：统计数据
  - `note_count`：笔记数
  - `file_count`：文件数
  - `blogger_count`：订阅博主数
  - `live_count`：已完成直播数

---

## 订阅知识库列表

获取当前用户订阅（但非自己创建）的知识库列表。

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/subscribe/list?page=1
```

参数：
- `page`: 页码，从 1 开始，默认 1（固定每页 20 条）

返回字段与"我的知识库列表"相同：`topics[]`、`has_more`、`total`

> 与 `/knowledge/list` 的区别：该接口只返回他人分享/公开的知识库，不包含自己创建的。

**订阅知识库的限制**：
- ⚠️ **无法添加笔记**：订阅的知识库只读，不能向其中添加笔记
- ⚠️ **查看源内容受限**：若知识库管理员关闭了「查看源文件」，则无法获取笔记原文，只能对该知识库进行语义搜索（`/recall/knowledge`）

---

## 创建知识库

```
POST https://openapi.biji.com/open/api/v1/resource/knowledge/create
Content-Type: application/json
```

请求体：
```json
{
  "name": "知识库名称",
  "description": "描述",
  "cover": ""
}
```

⚠️ 每天最多创建 50 个知识库（北京时间 00:00 重置）。

---

## 知识库笔记列表

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/notes?topic_id=abc123&page=1
```

参数：
- `topic_id` (string, 必填) - 知识库 ID（来自 `/knowledge/list` 的 `topic_id` 字段）
- `page`: 页码，从 1 开始，默认 1（固定每页 20 条）

返回 `notes[]`，每项字段：
- `note_id`：笔记 ID（**字符串**）
- `title`、`content`、`note_type`、`tags`、`created_at`、`edit_time`

用 `has_more` 判断是否有下一页。

---

## 知识库选择逻辑

当用户说「存到对应的知识库」或「存到相关知识库」时：
1. 先调用 `GET /knowledge/list` 获取所有知识库列表
2. 根据笔记标题、内容、标签，与知识库名称和描述做模糊匹配
3. 匹配置信度高时直接执行，并告知用户存入了哪个知识库
4. 置信度低或有歧义时，列出候选知识库让用户选择
5. 用户未提及知识库时，**不要擅自存入**任何知识库

---

## 添加笔记到知识库

```
POST https://openapi.biji.com/open/api/v1/resource/knowledge/note/batch-add
Content-Type: application/json
```

请求体：
```json
{
  "topic_id": "abc123",
  "note_ids": [123456789, 123456790]
}
```

⚠️ 每批最多 20 条。已存在的笔记会跳过。

---

## 从知识库移除笔记

```
POST https://openapi.biji.com/open/api/v1/resource/knowledge/note/remove
Content-Type: application/json
```

请求体：
```json
{
  "topic_id": "abc123",
  "note_ids": [123456789]
}
```

返回：
```json
{"removed_count": 1}
```

---

## 博主订阅

### 博主列表

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/bloggers?topic_id={topic_id}&page=1
```

参数：
- `topic_id` (string, 必填) - 知识库 ID（来自 `/knowledge/list` 的 `topic_id` 字段）
- `page`: 页码，从 1 开始

每页固定 20 条，用 `has_more` 判断。

返回 `bloggers[]`，每项字段：

| 字段 | 说明 |
|------|------|
| follow_id | 订阅关系 ID，**查博主内容时必用** |
| account_name | 博主名称 |
| account_icon | 博主头像 |
| platform | 平台（如 DEDAO）|
| account_url | 博主主页链接 |
| follow_time | 订阅时间（YYYY-MM-DD HH:MM:SS）|

### 博主内容列表

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/blogger/contents?topic_id={topic_id}&follow_id={follow_id}&page=1
```

参数：`topic_id`（知识库 ID）、`follow_id`（博主订阅 ID）、`page`（页码）

返回 `contents[]`，关键字段：`post_id_alias`（详情必用）、`post_title`、`post_summary`。

> 列表不含原文，需要原文请调详情接口。完整字段见 [api-details.md](api-details.md#博主内容字段说明)。

### 博主内容详情（含原文）

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/blogger/content/detail?topic_id={topic_id}&post_id={post_id_alias}
```

参数：`topic_id`（知识库 ID）、`post_id`（内容 ID，来自列表的 `post_id_alias`）

返回完整内容，包含 `post_media_text`（原文）。

---

## 直播订阅

### 已完成直播列表

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/lives?topic_id={topic_id}&page=1
```

参数：`topic_id`（知识库 ID）、`page`（页码）

返回 `lives[]`，关键字段：
- `live_id`：直播 ID（**字符串**，详情接口传此值）
- `name`、`status`

**只返回已结束且 AI 已处理完的直播。** 完整字段见 [api-details.md](api-details.md#直播字段说明)。

### 直播详情（总结 + 原文）

```
GET https://openapi.biji.com/open/api/v1/resource/knowledge/live/detail?topic_id={topic_id}&live_id={live_id}
```

参数：
- `topic_id`（知识库 ID）
- `live_id`（**字符串**，来自列表的 `live_id` 字段）

返回完整内容，包含 `post_summary`（AI 摘要）和 `post_media_text`（原文转写）。

---

## 展示模板

知识库列表：
> 📚 你有 {N} 个知识库：
>
> 1. **{name}** — {description}（{note_count} 篇笔记，{blogger_count} 个博主）
> 2. ...

添加成功：
> ✅ 已将「{note_title}」加入知识库「{kb_name}」


path: references/list.md
content: # 笔记列表与详情

## 概述

浏览笔记列表、查看详情（含原文/转写）、更新笔记内容、删除笔记。

---

## 笔记列表

```
GET https://openapi.biji.com/open/api/v1/resource/note/list?since_id=0
```

参数：
- `since_id` (int64, 必填) - 游标，首次传 `0`，后续用上次返回的 `next_cursor`

返回：`notes[]`、`has_more`、`next_cursor`、`total`（每次固定 20 条）

> ⚠️ **响应 JSON 可能包含未转义的控制字符**（笔记 content 中的原始换行符），建议用支持容错解析的 JSON 库处理。

> ⚠️ **`id`、`next_cursor`、`parent_id` 均为 int64**，JavaScript 环境必须在 `JSON.parse` 前做字符串化处理（见主文档「笔记 ID 处理规则」）。务必用字符串保存这些值，后续操作直接透传字符串即可。

**笔记类型 note_type**：

| 值 | 说明 |
|----|------|
| `plain_text` | 纯文本 |
| `img_text` | 图片笔记 |
| `link` | 链接笔记 |
| `audio` | 即时录音 |
| `meeting` | 会议录音 |
| `local_audio` | 本地音频 |
| `internal_record` | 内录音频 |
| `class_audio` | 课堂录音 |
| `recorder_audio` | 录音卡长录 |
| `recorder_flash_audio` | 录音卡闪念 |

---

## 笔记详情

```
GET https://openapi.biji.com/open/api/v1/resource/note/detail?id={note_id}
```

参数：
- `id` (int64, 必填) - 笔记 ID
- `image_quality` (string, 可选) - 传 `original` 返回正文中图片的原图链接（无压缩）

**⚠️ 返回结构**：数据在 `data.note` 对象下，不是 `data` 直接取：
```json
{
  "data": {
    "note": {
      "id": "1234567890",
      "note_id": "1234567890",
      "title": "笔记标题",
      "content": "笔记内容",
      "tags": [...],
      ...
    }
  }
}
```

**字段说明**：
- `note_id` (string) - 笔记 ID 的字符串格式，便于 AI Agent 解析，避免 int64 精度问题
- `children_ids` (string[]) - 子笔记 ID 列表（字符串格式），仅当有子笔记时返回

**图片附件**：`attachments[]` 中每个图片包含：
- `url` - 缩略图 URL（720px 压缩）
- `original_url` - 原图 URL（无压缩，适合需要高清图的场景）

**正文原图**：传 `image_quality=original` 时，`content` 中的 markdown 图片链接会返回原图（去掉 OSS 压缩参数）。

**详情独有字段**（列表不返回）：`audio.original`、`audio.play_url`、`audio.duration`、`web_page.content`、`web_page.url`、`web_page.excerpt`、`attachments[]`、`children_ids`。

完整字段说明见 [api-details.md](api-details.md#笔记详情独有字段)。

---

## 更新笔记

```
POST https://openapi.biji.com/open/api/v1/resource/note/update
Content-Type: application/json
```

请求体：
```json
{
  "note_id": 123456789,
  "title": "新标题",
  "content": "新的 Markdown 内容",
  "tags": ["标签1", "标签2"]
}
```

参数说明：
- `note_id` (int64, **必填**) - 要更新的笔记 ID
- `title` (string, 可选) - 新标题，不传则不更新
- `content` (string, 可选) - 新内容，不传则不更新
- `tags` (string[], 可选) - 新标签列表，**替换**原有标签（不传则保持原标签）

> ⚠️ **至少需要传 title、content、tags 中的一个**，否则返回错误。

> ⚠️ **仅支持 plain_text 类型笔记**，链接笔记、图片笔记等暂不支持更新。

---

## 删除笔记

```
POST https://openapi.biji.com/open/api/v1/resource/note/delete
Content-Type: application/json
```

请求体：
```json
{"note_id": 123456789}
```

笔记移入回收站，需要 `note.content.trash` scope。

---

## 展示模板

列表展示：
> 📋 最近 {N} 条笔记：
>
> 1. **{title}** · {note_type} · {created_at}
> 2. ...
>
> （回复笔记序号或标题可查看详情）

详情展示（链接笔记）：
> 📄 **{title}**
> 🔗 来源：{web_page.url}
> 📝 摘要：{content}
> 🏷️ 标签：{tags}


path: references/oauth.md
content: # OAuth 授权配置

## 概述

使用 OAuth Device Flow 为用户授权并自动写入配置。适用于「配置 Get笔记」「连接 Get笔记」等场景，也是首次使用时自动触发的流程。

---

## 自动触发条件

每次调用 API 前，先检查 `$GETNOTE_API_KEY` 是否存在。若不存在，**自动发起 OAuth 授权流程**（无需用户主动说"配置"），告知用户需要先授权才能使用。

授权完成后，继续执行用户原本的请求。

---

## 手动配置（可选）

1. 前往 [Get笔记开放平台](https://www.biji.com/openapi) 创建应用
2. 获取 Client ID 和 API Key
3. 在 `~/.openclaw/openclaw.json` 中添加：

```json
{
  "skills": {
    "entries": {
      "getnote": {
        "apiKey": "gk_live_你的key",
        "env": {
          "GETNOTE_CLIENT_ID": "cli_你的id",
          "GETNOTE_OWNER_ID": "ou_你的飞书ID（可选，用于权限控制）"
        }
      }
    }
  }
}
```

---

## Device Flow 完整流程

### 步骤 1：申请授权码

```
POST https://openapi.biji.com/open/api/v1/oauth/device/code
Content-Type: application/json
```

请求体：
```json
{
  "client_id": "cli_a1b2c3d4e5f6789012345678abcdef90"
}
```

> ⚠️ **client_id 固定为 [REDACTED_SECRET]**，这是 Get笔记 为 OpenClaw 预注册的应用。

返回：
```json
{
  "success": true,
  "data": {
    "code": "abc123...",
    "verification_uri": "https://biji.com/openapi/oauth/authorize?code=abc123...",
    "user_code": "ABCD-1234",
    "expires_in": 600,
    "interval": 5
  }
}
```

| 字段 | 说明 |
|------|------|
| code | 授权码，轮询时使用 |
| verification_uri | 授权链接，**发送给用户点击** |
| user_code | 确认码，**必须展示给用户核对** |
| expires_in | 授权码有效期（秒），默认 600 |
| interval | 建议轮询间隔（秒），默认 5 |

### 步骤 2：展示授权链接

将 `verification_uri` 和 `user_code` 发送给用户：

> 🔗 请点击链接完成授权：
>
> {verification_uri}
>
> ⚠️ **请核对确认码**：`{user_code}`
>
> 授权页面会显示确认码，请确保与上面一致后再点击授权。授权码 10 分钟内有效。

**安全提醒**：`user_code` 用于防止钓鱼攻击。用户在授权页面看到的确认码必须与 Agent 展示的一致，不一致请勿授权。

**发送后立即启动后台轮询**（步骤 3）。

### 步骤 3：轮询等待授权

发送授权链接给用户后，**立即在后台启动轮询**，无需等待用户回复。

```
POST https://openapi.biji.com/open/api/v1/oauth/token
Content-Type: application/json
```

请求体：
```json
{
  "grant_type": "device_code",
  "client_id": "cli_a1b2c3d4e5f6789012345678abcdef90",
  "code": "{code}"
}
```

**轮询策略**：
- **间隔**：5 秒查询一次
- **超时**：最多轮询 10 分钟（与授权码有效期一致）
- **并行**：轮询在后台进行，不阻塞用户其他操作

**推荐：使用轮询脚本**

```bash
# 方式 1：后台轮询 + process poll 等待结果（OpenClaw 推荐）
exec: python scripts/oauth_poll.py "{code}"
  background: true

# 用 process poll 等待结果（最长等 10 分钟）
process: poll
  sessionId: {上一步返回的 sessionId}
  timeout: 600000

# 方式 2：简单等待（适合短时间）
result=$(python scripts/oauth_poll.py "{code}")
[REDACTED_SECRET] "$result" | jq -r '.api_key')
client_id=$(echo "$result" | jq -r '.client_id')
```

脚本会自动处理各种状态，成功时输出 JSON，失败时输出错误到 stderr。

**轮询响应状态**：

| 响应 | 说明 | 处理方式 |
|------|------|---------|
| `{"msg": "authorization_pending"}` | 用户尚未操作 | 继续轮询 |
| `{"msg": "rejected"}` | 用户拒绝授权 | **停止轮询**，告知用户已拒绝 |
| `{"msg": "expired_token"}` | 授权码已过期 | **停止轮询**，提示重新发起 |
| `{"msg": "already_consumed"}` | 授权码已使用 | **停止轮询**，可能已配置成功 |
| `{"api_key": "...", "client_id": "...", ...}` | **授权成功** | 进入步骤 4 |

**授权成功返回**：
```json
{
  "success": true,
  "data": {
    "client_id": "cli_a1b2c3d4e5f6789012345678abcdef90",
    "api_key": "gk_live_xxx",
    "key_id": "abc123",
    "expires_at": 1742000000
  }
}
```

| 字段 | 说明 |
|------|------|
| client_id | 应用 ID |
| api_key | API Key，**写入配置文件** |
| key_id | Key ID（管理用） |
| expires_at | 过期时间戳（Unix 秒），有效期 1 年 |

### 步骤 4：写入配置

将获取的凭证写入 `~/.openclaw/openclaw.json`：

```json
{
  "skills": {
    "entries": {
      "getnote": {
        "apiKey": "{api_key}",
        "env": {
          "GETNOTE_CLIENT_ID": "{client_id}"
        }
      }
    }
  }
}
```

**告知用户**：

> ✅ Get笔记 配置完成！
>
> - API Key 有效期至 {expires_at 格式化日期}
> - 现在可以使用「记一下」「查笔记」等功能了
> - 如需注销授权，请访问：https://www.biji.com/openapi?tab=keys


path: references/save.md
content: # 保存笔记

## 概述

新建文本、链接、图片三种类型的笔记。文本笔记同步完成；链接和图片笔记是异步任务，需轮询进度。

---

## 新建笔记

```
POST https://openapi.biji.com/open/api/v1/resource/note/save
Content-Type: application/json
```

**仅支持新建，不支持编辑**。

请求体：
```json
{
  "title": "笔记标题",
  "content": "Markdown 内容",
  "note_type": "plain_text",
  "tags": ["标签1", "标签2"],
  "parent_id": 0,
  "link_url": "https://...",
  "image_urls": ["https://..."]
}
```

详细字段说明见 [api-details.md](api-details.md#新建笔记字段说明)。

- `plain_text`：同步返回，立即完成，响应中包含 `note_id`（字符串）
- `link` / `img_text`：返回 `task_id`，**必须轮询** `/task/progress`

---

## 查询任务进度

```
POST https://openapi.biji.com/open/api/v1/resource/note/task/progress
Content-Type: application/json
```

请求体：
```json
{"task_id": "task_abc123xyz"}
```

返回：
- `status`: `pending` | `processing` | `success` | `failed`
- `note_id`: 成功时返回笔记 ID（**字符串**）；任务进行中时值为 `"0"`，需过滤
- `error_msg`: 失败时返回错误信息

**建议 10-30 秒间隔轮询，直到 success 或 failed**。

---

## 链接笔记完整流程

> ⚠️ **必须遵循的体验流程**

**步骤 1**：提交任务
```
POST https://openapi.biji.com/open/api/v1/resource/note/save {note_type:"link", link_url:"https://..."}
```

返回结构：
```json
{
  "data": {
    "created_count": 1,
    "tasks": [{"task_id": "69c3995e99f5a67e", "url": "https://..."}],
    "message": "链接笔记任务已创建，请通过 /note/task/progress 接口查询处理状态"
  }
}
```

⚠️ **task_id 在 `data.tasks[0].task_id`**，不是 `data.task_id`。

拿到 task_id 后，**立即发消息给用户**：
> ✅ 链接已保存，正在抓取原文和生成总结，稍后告诉你结果...

> ⚠️ **重复链接说明**：OpenAPI 层不做 URL 去重，重复保存同一链接会创建新笔记任务。App 端有去重提示，API 调用方需自行判断。

**步骤 2**：后台轮询（10-30 秒间隔）
```
POST https://openapi.biji.com/open/api/v1/resource/note/task/progress {task_id} → 直到 status=success/failed
```

**步骤 3**：任务完成后，**调详情接口展示价值**
```
GET https://openapi.biji.com/open/api/v1/resource/note/detail?id={note_id}
```
然后发第二条消息：
> ✅ 笔记生成完成！
> - 📄 **原文**：已保存 {web_page.content 字数} 字
> - 📝 **总结**：{content 内容，即 AI 生成的摘要}
> - 🔗 **来源**：{web_page.url}

---

## 图片笔记完整流程

**步骤 1**：获取上传凭证
```
GET https://openapi.biji.com/open/api/v1/resource/image/upload_token?mime_type=jpg&count=1
```

参数：
- `mime_type`: `jpg` | `png` | `gif` | `webp`，默认 `png`
- `count`: 需要的 token 数量，默认 1，最大 9

⚠️ **mime_type 必须与实际文件格式一致**，否则 OSS 签名失败。

返回字段说明见 [api-details.md](api-details.md#图片上传凭证返回字段)。

**步骤 2**：上传文件到 OSS

> ⚠️ **字段顺序必须严格遵守**，否则 OSS 签名验证失败。

字段顺序：`key → OSSAccessKeyId → policy → signature → callback → Content-Type → file`

完整示例见 [api-details.md](api-details.md#oss-上传详细示例)。

**步骤 3**：提交任务
```
POST https://openapi.biji.com/open/api/v1/resource/note/save {note_type:"img_text", image_urls:[access_url]}
```
拿到 task_id 后，**立即发消息给用户**：
> ✅ 图片已保存，正在识别内容，稍后告诉你结果...

**步骤 4**：后台轮询
```
POST https://openapi.biji.com/open/api/v1/resource/note/task/progress {task_id} → 直到 status=success/failed
```

**步骤 5**：任务完成后，**调详情接口展示价值**
```
GET https://openapi.biji.com/open/api/v1/resource/note/detail?id={note_id}
```
然后发第二条消息：
> ✅ 图片笔记生成完成！
> - 📝 **识别内容**：{content 内容}
> - 🏷️ **标签**：{tags}


path: references/search.md
content: # 语义搜索

## 概述

在笔记中进行语义召回，支持全局搜索和指定知识库搜索。无需拉取全部数据，直接返回相关片段。

---

## 全局语义搜索

> 适用场景：「搜一下」「找找我哪些笔记提到了 XX」

**所需 scope**: `note.recall.read`

```
POST https://openapi.biji.com/open/api/v1/resource/recall
Content-Type: application/json
```

请求体：
```json
{
  "query": "搜索关键词",
  "top_k": 3
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| query | string, 必填 | 搜索关键词或语义描述 |
| top_k | int, 可选 | 返回数量，默认 **3**，最大 **10** |

返回结构（结果已按相关度**从高到低**排序）：

```json
{
  "results": [
    {
      "note_id": "1896830231705320746",
      "note_type": "NOTE",
      "title": "笔记标题",
      "content": "笔记内容片段",
      "created_at": "2025-12-24 15:20:15"
    }
  ]
}
```

---

## 知识库语义搜索

> 适用场景：「在我的 XX 知识库搜一下 XX」

**所需 scope**: `note.topic.recall.read`

```
POST https://openapi.biji.com/open/api/v1/resource/recall/knowledge
Content-Type: application/json
```

请求体：
```json
{
  "topic_id": "qnNX75j0",
  "query": "搜索关键词",
  "top_k": 3
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| topic_id | string, 必填 | 知识库 ID（来自 `/knowledge/list` 的 `topic_id` 字段） |
| query | string, 必填 | 搜索关键词或语义描述 |
| top_k | int, 可选 | 返回数量，默认 **3**，最大 **10** |

返回结构同全局搜索。

---

## 召回结果字段说明

| 字段 | 说明 |
|------|------|
| note_id | 笔记 ID（string）；**仅 `NOTE` 类型有值**，其余类型均为空 |
| note_type | 内容类型：`NOTE` / `FILE` / `BLOGGER` / `LIVE` / `URL` / `DEDAO` |
| title | 笔记/文档标题 |
| content | 相关内容片段 |
| created_at | 创建/发布时间（YYYY-MM-DD HH:MM:SS）|
| page_no | `FILE` 类型时表示文件页码，其余类型省略 |

> **后续操作**：`NOTE` 类型可调详情接口获取全文，其余类型只能展示召回片段。

---

## 示例对话

> 用户：「找找我哪些笔记提到了大模型 API」
> → `POST /recall` `{ "query": "大模型 API", "top_k": 3 }`

> 用户：「在我的 AI 学习知识库里搜一下 RAG」
> → 先调 `/knowledge/list` 找到对应知识库的 `topic_id`，再 `POST /recall/knowledge` `{ "topic_id": "xxx", "query": "RAG", "top_k": 3 }`

---

## 展示模板

搜索完成后：
> 🔍 找到 {N} 条相关笔记：
>
> 1. **{title}** （{created_at}）
>    {content 片段}
>
> 2. ...
>
> （`NOTE` 类型可回复「看原文」获取完整内容）


path: references/tags.md
content: # 标签管理

## 概述

为笔记添加或删除标签。标签分为 AI 自动生成、用户手动添加、系统标签三种类型，其中系统标签不可删除。

---

## 添加标签

```
POST https://openapi.biji.com/open/api/v1/resource/note/tags/add
Content-Type: application/json
```

请求体：
```json
{
  "note_id": 123456789,
  "tags": ["工作", "重要"]
}
```

返回：`note_id`（string）+ `tags[]` 数组，每项包含：
- `id`：标签 ID（**删除时传入此值作为 `tag_id`**）
- `name`：标签名
- `type`：`ai` / `manual` / `system`

**标签类型 type**：

| 值 | 说明 |
|----|------|
| `ai` | AI 自动生成 |
| `manual` | 用户手动添加 |
| `system` | 系统标签（**不可删除**）|

---

## 删除标签

```
POST https://openapi.biji.com/open/api/v1/resource/note/tags/delete
Content-Type: application/json
```

请求体：
```json
{
  "note_id": 123456789,
  "tag_id": "123"
}
```

> `tag_id` 来自 `tags/add` 返回或 `GET /note/detail` 中 `tags[].id` 字段。

⚠️ `system` 类型标签不允许删除，尝试删除会返回错误。

---

## 展示模板

添加成功：
> 🏷️ 已为笔记「{title}」添加标签：{tags}

删除成功：
> ✅ 已删除标签「{tag_name}」


path: scripts/oauth_poll.py
content: #!/usr/bin/env python3
"""
Get笔记 OAuth 授权轮询脚本

用法:
    python oauth_poll.py <code> [client_id]

参数:
    code       - 授权码（从 /oauth/device/code 获取的 code 字段）
    client_id  - 应用 ID（可选，默认: cli_a1b2c3d4e5f6789012345678abcdef90）

返回:
    成功: 输出 JSON {"api_key": "...", "client_id": "...", "key_id": "...", "expires_at": ...}
    失败: 输出错误信息到 stderr，退出非零状态码

退出码:
    0 - 授权成功
    2 - 用户拒绝授权
    3 - 授权码已过期
    4 - 授权码已被使用
    5 - 未知错误
    6 - 轮询超时

示例:
    result=$(python oauth_poll.py "abc123...")
    [REDACTED_SECRET] "$result" | jq -r '.api_key')
"""

import sys
import time
import json
import urllib.request
import urllib.error

API_URL = "https://openapi.biji.com/open/api/v1/oauth/token"
DEFAULT_CLIENT_ID = "cli_a1b2c3d4e5f6789012345678abcdef90"
INTERVAL = 5       # 轮询间隔（秒）
MAX_ATTEMPTS = 120 # 最大尝试次数（5秒 * 120 = 10分钟）


def poll_token(code: str, client_id: str) -> dict:
    """轮询授权状态，返回授权结果或抛出异常"""
    
    payload = json.dumps({
        "grant_type": "device_code",
        "client_id": client_id,
        "code": code
    }).encode('utf-8')
    
    headers = {"Content-Type": "application/json"}
    
    for attempt in range(MAX_ATTEMPTS):
        try:
            req = urllib.request.Request(API_URL, data=payload, headers=headers, method='POST')
            with urllib.request.urlopen(req, timeout=30) as resp:
                data = json.loads(resp.read().decode('utf-8'))
        except urllib.error.URLError as e:
            print(f"网络错误: {e}", file=sys.stderr)
            time.sleep(INTERVAL)
            continue
        
        # 检查是否授权成功
        if data.get("success") and data.get("data", {}).get("api_key"):
            return data["data"]
        
        # 检查状态消息
        msg = data.get("data", {}).get("msg", "")
        
        if msg == "authorization_pending":
            time.sleep(INTERVAL)
            continue
        elif msg == "rejected":
            print("用户拒绝了授权", file=sys.stderr)
            sys.exit(2)
        elif msg == "expired_token":
            print("授权码已过期，请重新发起", file=sys.stderr)
            sys.exit(3)
        elif msg == "already_consumed":
            print("授权码已被使用", file=sys.stderr)
            sys.exit(4)
        else:
            print(f"未知响应: {data}", file=sys.stderr)
            sys.exit(5)
    
    print("轮询超时（10分钟），请重新发起授权", file=sys.stderr)
    sys.exit(6)


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <code> [client_id]", file=sys.stderr)
        sys.exit(1)
    
    code = sys.argv[1]
    client_id = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_CLIENT_ID
    
    result = poll_token(code, client_id)
    print(json.dumps(result))


if __name__ == "__main__":
    main()


path: scripts/upload_image.py
content: #!/usr/bin/env python3
"""
Get笔记图片上传脚本

用法:
    python upload_image.py <图片路径> [--api-key KEY] [--client-id ID]
    
示例:
    python upload_image.py /path/to/image.jpg
    python upload_image.py /path/to/image.jpg --api-key gk_live_xxx --client-id cli_xxx
    
环境变量:
    GETNOTE_API_KEY - API Key（如未提供 --api-key 则使用此变量）
    GETNOTE_CLIENT_ID - Client ID（可选）
"""

import os
import sys
import argparse
import mimetypes
from pathlib import Path

try:
    import requests
except ImportError:
    print("错误: 需要安装 requests 库")
    print("运行: pip install requests")
    sys.exit(1)


BASE_URL = "https://openapi.biji.com/open/api/v1/resource"
DEFAULT_CLIENT_ID = "cli_a1b2c3d4e5f6789012345678abcdef90"


def get_mime_type(file_path: str) -> str:
    """获取文件 MIME 类型"""
    mime_type, _ = mimetypes.guess_type(file_path)
    return mime_type or "image/jpeg"


def upload_image(image_path: str, api_key: str, client_id: str = DEFAULT_CLIENT_ID) -> str:
    """
    上传图片到 Get笔记 OSS
    
    Args:
        image_path: 本地图片路径
        api_key: Get笔记 API Key
        client_id: 应用 Client ID
        
    Returns:
        上传后的访问 URL
    """
    # 检查文件
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"文件不存在: {image_path}")
    
    mime_type = get_mime_type(image_path)
    headers = {
        "Authorization": api_key,
        "X-Client-ID": client_id
    }
    
    # 步骤 1: 获取上传凭证
    print("[1/3] 获取上传凭证...")
    
    resp = requests.get(
        f"{BASE_URL}/image/upload_token",
        params={"count": 1, "mime_type": mime_type},
        headers=headers
    )
    resp.raise_for_status()
    data = resp.json()
    
    if not data.get("success"):
        error = data.get("error", {})
        raise Exception(f"获取凭证失败: {error.get('message', 'Unknown error')}")
    
    # 兼容两种返回格式：tokens[] 数组 或 直接在 data 中
    token_data = data["data"]
    if "tokens" in token_data:
        tokens = token_data["tokens"]
        if not tokens:
            raise Exception("获取凭证失败: tokens 为空")
        [REDACTED_SECRET]
    else:
        # 直接在 data 中的格式
        [REDACTED_SECRET]
    
    host = token["host"]
    object_key = token["object_key"]
    accessid = token["accessid"]
    policy = token.get("policy", "")
    signature = token.get("signature", "")
    callback = token.get("callback", "")
    access_url = token["access_url"]
    oss_content_type = token.get("oss_content_type", mime_type)
    
    print("✓ 凭证获取成功")
    
    # 步骤 2: 上传到 OSS（multipart form，字段顺序必须严格遵守）
    print("[2/3] 上传图片到 OSS...")
    
    file_name = os.path.basename(image_path)
    with open(image_path, "rb") as f:
        # 字段顺序：key → OSSAccessKeyId → policy → signature → callback → Content-Type → file
        form_data = [
            ("key", object_key),
            ("OSSAccessKeyId", accessid),
            ("policy", policy),
            ("signature", signature),
            ("callback", callback),
            ("Content-Type", oss_content_type),
        ]
        files = {"file": (file_name, f, mime_type)}
        resp = requests.post(host, data=form_data, files=files)
    
    if resp.status_code == 429:
        # 限流处理
        try:
            error_data = resp.json()
            rate_limit = error_data.get("error", {}).get("rate_limit", {})
            print(f"限流: {rate_limit}", file=sys.stderr)
        except:
            pass
        raise Exception(f"上传失败: 限流 (HTTP 429)，请稍后重试")
    
    if resp.status_code not in (200, 204):
        raise Exception(f"上传失败: HTTP {resp.status_code}")
    
    print("✓ 上传成功")
    
    # 步骤 3: 返回访问 URL
    print("[3/3] 完成")
    
    return access_url


def main():
    parser = argparse.ArgumentParser(
        description="Get笔记图片上传脚本",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
    %(prog)s /path/to/image.jpg
    %(prog)s /path/to/image.jpg --api-key gk_live_xxx
    
环境变量:
    GETNOTE_API_KEY - API Key
        """
    )
    parser.add_argument("image_path", help="本地图片路径")
    parser.add_argument("--api-key", "-k", help="API Key（或设置 GETNOTE_API_KEY 环境变量）")
    parser.add_argument("--client-id", "-c", help="Client ID（可选，默认使用 OpenClaw 预注册应用）")
    
    args = parser.parse_args()
    
    # 获取 API Key
    [REDACTED_SECRET] or os.environ.get("GETNOTE_API_KEY")
    if not [REDACTED_SECRET]"错误: 请提供 API Key（--api-key 参数或 GETNOTE_API_KEY 环境变量）")
        sys.exit(1)
    
    # 获取 Client ID
    client_id = args.client_id or os.environ.get("GETNOTE_CLIENT_ID") or DEFAULT_CLIENT_ID
    
    try:
        image_url = upload_image(args.image_path, api_key, client_id)
        
        print()
        print("=" * 40)
        print("图片上传成功！")
        print("=" * 40)
        print()
        print(f"访问 URL: {image_url}")
        print()
        print("💡 创建图片笔记:")
        print(f'   curl -X POST "https://openapi.biji.com/open/api/v1/resource/note/save?task_id=..."')
        print(f'     -H "Authorization: $GETNOTE_API_KEY"')
        print(f'     -H "Content-Type: application/json"')
        print(f'     -d \'{{"type":"img_text","image_urls":["{image_url}"]}}\'')
        
    except Exception as e:
        print(f"错误: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: _meta.json
content: ownerId: kn75rbp8nyw5q0xmrzg01nfeq5829kd0
slug: getnote
version: 1.5.7
publishedAt: 1774523298598

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