重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
notion-sdk by terrylica/cc-skills
npx skills add https://github.com/terrylica/cc-skills --skill notion-sdk使用官方的 notion-client Python SDK 以编程方式控制 Notion。当前版本请参见 PyPI。
在以下情况下使用此技能:
在任何 Notion API 操作之前,收集集成令牌:
AskUserQuestion(questions=[{
"question": "请提供您的 Notion 集成令牌(以 ntn_ 或 secret_ 开头)",
"header": "Notion 令牌",
"options": [
{"label": "我已准备好令牌", "description": "来自 notion.so/my-integrations 的令牌"},
{"label": "需要创建一个", "description": "前往 notion.so/my-integrations → 新建集成"}
],
"multiSelect": false
}])
用户提供令牌后:
ntn_ 或 secret_ 开头)广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
scripts/notion_wrapper.py 中的 validate_token() 进行测试from notion_client import Client
from scripts.create_page import (
create_database_page,
title_property,
status_property,
date_property,
)
client = Client(auth="ntn_...")
page = create_database_page(
client,
data_source_id="abc123...", # 数据库 ID
properties={
"Name": title_property("My New Task"),
"Status": status_property("In Progress"),
"Due Date": date_property("2025-12-31"),
}
)
print(f"Created: {page['url']}")
from scripts.add_blocks import (
append_blocks,
heading,
paragraph,
bullet,
code_block,
callout,
)
blocks = [
heading("Overview", level=2),
paragraph("This page was created via the Notion API."),
callout("Remember to share the page with your integration!", emoji="⚠️"),
heading("Tasks", level=3),
bullet("First task"),
bullet("Second task"),
code_block("print('Hello, Notion!')", language="python"),
]
append_blocks(client, page["id"], blocks)
from scripts.query_database import (
query_data_source,
checkbox_filter,
status_filter,
and_filter,
sort_by_property,
)
# 查找未完成的高优先级项目
results = query_data_source(
client,
data_source_id="abc123...",
filter_obj=and_filter(
checkbox_filter("Done", False),
status_filter("Priority", "High")
),
sorts=[sort_by_property("Due Date", "ascending")]
)
for page in results:
title = page["properties"]["Name"]["title"][0]["plain_text"]
print(f"- {title}")
| 脚本 | 用途 |
|---|---|
notion_wrapper.py | 客户端设置、令牌验证、重试包装器 |
create_page.py | 创建页面、属性构建器 |
add_blocks.py | 追加块、块类型构建器 |
query_database.py | 查询、过滤、排序、搜索 |
api_call_with_retry() 进行自动速率限制处理Retry-After 头data_source_id 替代 database_iddatabase_id 仍然有效通过集成测试发现的见解(附测试引用以供验证)。
api_call_with_retry() 自动处理临时故障:
| 错误类型 | 行为 | 等待策略 |
|---|---|---|
| 429 速率限制 | 重试 | 遵循 Retry-After 头(默认 1 秒) |
| 500 服务器错误 | 重试 | 指数退避:1秒、2秒、4秒 |
| 认证/验证错误 | 立即失败 | 不重试 |
引用:test_client.py::TestRetryLogic (第 146-193 行)
新创建的块可能无法立即查询。至少添加 0.5 秒延迟:
append_blocks(client, page_id, blocks)
time.sleep(0.5) # 最终一致性延迟
children = client.blocks.children.list(page_id)
引用:test_integration.py::TestBlockAppend::test_retrieve_appended_blocks (第 298 行)
| 旧模式 | 新模式 (v2.6.0+) |
|---|---|
client.databases.query() | client.data_sources.query() |
filter: {"value": "database"} | filter: {"value": "data_source"} |
引用:test_integration.py::TestDatabaseQuery (第 110 行)
页面无法通过 API 永久删除 - 只能归档(移至回收站):
client.pages.update(page_id, archived=True) # 移至回收站,非删除
引用:test_integration.py 清理装置 (第 72-76 行)
| 输入 | 行为 | 是否有效? |
|---|---|---|
空字符串 "" | 创建空内容 | 是 |
空数组 [] | 清除多选/关系 | 是 |
数字属性为 None | 清除属性值 | 是 |
零 0 | 有效数字(非假值) | 是 |
负数 -42 | 有效数字 | 是 |
| Unicode/表情符号 | 完全保留 | 是 |
引用:test_property_builders.py::TestPropertyBuildersEdgeCases (第 302-341 行)
构建器故意设计为宽松的 - 验证在 API 层面进行:
| 属性 | 构建器接受 | API 验证 |
|---|---|---|
| 日期 | 任意字符串 | 仅限 ISO 8601 格式 |
| URL | 任意字符串 | 有效 URL 格式 |
| 复选框 | 真值 | 期望布尔值 |
最佳实践:在构建属性之前,在您的应用程序中进行验证。
引用:test_property_builders.py::TestPropertyBuildersInvalidInputs (第 347-376 行)
ntn_ 和 secret_ 有效引用:test_client.py::TestClientEdgeCases (第 196-224 行)
# 空复合过滤器(匹配所有)
and_filter() # {"and": []}
# 支持深度嵌套
and_filter(
or_filter(filter_a, filter_b),
and_filter(filter_c, filter_d)
)
引用:test_filter_builders.py::TestFilterEdgeCases (第 323-360 行)
过滤器不排除 NULL 属性 - 在 Python 中检查:
if row["properties"]["Rating"]["number"] is not None:
# 处理非空值
引用:test_integration.py::TestDatabaseQuery::test_query_database_with_filter (第 120-135 行)
| 条件 | has_more | next_cursor |
|---|---|---|
| 存在更多结果 | True | 存在,非 None |
| 没有更多结果 | False | 可能缺失/None |
在使用 next_cursor 之前,始终检查 has_more。
引用:test_integration.py::TestDatabaseQuery::test_query_database_with_pagination (第 137-151 行)
from notion_client import APIResponseError, APIErrorCode
try:
result = client.pages.create(...)
except APIResponseError as e:
if e.code == APIErrorCode.ObjectNotFound:
print("页面/数据库未找到或未与集成共享")
elif e.code == APIErrorCode.Unauthorized:
print("令牌无效或已过期")
elif e.code == APIErrorCode.RateLimited:
print(f"速率受限。{e.additional_data.get('retry_after')} 秒后重试")
else:
raise
uv pip install notion-client # 需要 v2.6+ 以支持 data_source
或使用 PEP 723 内联依赖(脚本已包含它们)。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 对象未找到 | 页面未与集成共享 | 共享页面:... 菜单 → 连接 → 添加集成 |
| 未授权 | 令牌无效或已过期 | 在 notion.so/my-integrations 生成新令牌 |
| 速率受限 (429) | 请求过多 | 使用 api_call_with_retry() 进行自动处理 |
| 查询结果为空 | 过滤器未匹配到任何内容 | 验证过滤器语法和属性名称 |
| 创建后找不到块 | 最终一致性延迟 | 写入后读取前添加 0.5 秒延迟 |
| 无效的属性类型 | 使用了错误的构建器 | 检查数据库模式中的属性类型 |
| 令牌格式被拒绝 | 前缀错误(区分大小写) | 令牌必须以 ntn_ 或 secret_ 开头(小写) |
| 数据源 ID 无效 | API 版本过旧 | 将 notion-client 升级到最新版本 |
每周安装次数
69
代码仓库
GitHub 星标
26
首次出现
2026年1月24日
安全审计
安装于
opencode64
claude-code63
codex62
gemini-cli62
github-copilot60
cursor59
Control Notion programmatically using the official notion-client Python SDK. See PyPI for current version.
Use this skill when:
Before any Notion API operation, collect the integration token:
AskUserQuestion(questions=[{
"question": "Please provide your Notion Integration Token (starts with ntn_ or secret_)",
"header": "Notion Token",
"options": [
{"label": "I have a token ready", "description": "Token from notion.so/my-integrations"},
{"label": "Need to create one", "description": "Go to notion.so/my-integrations → New integration"}
],
"multiSelect": false
}])
After user provides token:
ntn_ or secret_)validate_token() from scripts/notion_wrapper.pyfrom notion_client import Client
from scripts.create_page import (
create_database_page,
title_property,
status_property,
date_property,
)
client = Client(auth="ntn_...")
page = create_database_page(
client,
data_source_id="abc123...", # Database ID
properties={
"Name": title_property("My New Task"),
"Status": status_property("In Progress"),
"Due Date": date_property("2025-12-31"),
}
)
print(f"Created: {page['url']}")
from scripts.add_blocks import (
append_blocks,
heading,
paragraph,
bullet,
code_block,
callout,
)
blocks = [
heading("Overview", level=2),
paragraph("This page was created via the Notion API."),
callout("Remember to share the page with your integration!", emoji="⚠️"),
heading("Tasks", level=3),
bullet("First task"),
bullet("Second task"),
code_block("print('Hello, Notion!')", language="python"),
]
append_blocks(client, page["id"], blocks)
from scripts.query_database import (
query_data_source,
checkbox_filter,
status_filter,
and_filter,
sort_by_property,
)
# Find incomplete high-priority items
results = query_data_source(
client,
data_source_id="abc123...",
filter_obj=and_filter(
checkbox_filter("Done", False),
status_filter("Priority", "High")
),
sorts=[sort_by_property("Due Date", "ascending")]
)
for page in results:
title = page["properties"]["Name"]["title"][0]["plain_text"]
print(f"- {title}")
| Script | Purpose |
|---|---|
notion_wrapper.py | Client setup, token validation, retry wrapper |
create_page.py | Create pages, property builders |
add_blocks.py | Append blocks, block type builders |
query_database.py | Query, filter, sort, search |
api_call_with_retry() for automatic rate limit handlingRetry-After headerdata_source_id instead of database_id for multi-source databasesdatabase_id still works for simple databasesInsights discovered through integration testing (test citations for verification).
api_call_with_retry() handles transient failures automatically:
| Error Type | Behavior | Wait Strategy |
|---|---|---|
| 429 Rate Limited | Retries | Respects Retry-After header (default 1s) |
| 500 Server Error | Retries | Exponential backoff: 1s, 2s, 4s |
| Auth/Validation | Fails immediately | No retry |
Citation:test_client.py::TestRetryLogic (lines 146-193)
Newly created blocks may not be immediately queryable. Add 0.5s minimum delay:
append_blocks(client, page_id, blocks)
time.sleep(0.5) # Eventual consistency delay
children = client.blocks.children.list(page_id)
Citation:test_integration.py::TestBlockAppend::test_retrieve_appended_blocks (line 298)
| Old Pattern | New Pattern (v2.6.0+) |
|---|---|
client.databases.query() | client.data_sources.query() |
filter: {"value": "database"} | filter: {"value": "data_source"} |
Citation:test_integration.py::TestDatabaseQuery (line 110)
Pages cannot be permanently deleted via API - only archived (moved to trash):
client.pages.update(page_id, archived=True) # Trash, not delete
Citation:test_integration.py cleanup fixture (lines 72-76)
| Input | Behavior | Valid? |
|---|---|---|
Empty string "" | Creates empty content | Yes |
Empty array [] | Clears multi-select/relations | Yes |
None for number | Clears property value | Yes |
Zero 0 | Valid number (not falsy) | Yes |
Negative -42 | Valid number | Yes |
Citation:test_property_builders.py::TestPropertyBuildersEdgeCases (lines 302-341)
Builders are intentionally permissive - validation happens at API level:
| Property | Builder Accepts | API Validates |
|---|---|---|
| Date | Any string | ISO 8601 only |
| URL | Any string | Valid URL format |
| Checkbox | Truthy values | Boolean expected |
Best Practice : Validate in your application before building properties.
Citation:test_property_builders.py::TestPropertyBuildersInvalidInputs (lines 347-376)
ntn_ and secret_ validCitation:test_client.py::TestClientEdgeCases (lines 196-224)
# Empty compound (matches all)
and_filter() # {"and": []}
# Deep nesting supported
and_filter(
or_filter(filter_a, filter_b),
and_filter(filter_c, filter_d)
)
Citation:test_filter_builders.py::TestFilterEdgeCases (lines 323-360)
Filters don't exclude NULL properties - check in Python:
if row["properties"]["Rating"]["number"] is not None:
# Process non-null values
Citation:test_integration.py::TestDatabaseQuery::test_query_database_with_filter (lines 120-135)
| Condition | has_more | next_cursor |
|---|---|---|
| More results exist | True | Present, non-None |
| No more results | False | May be absent/None |
Always check has_more before using next_cursor.
Citation:test_integration.py::TestDatabaseQuery::test_query_database_with_pagination (lines 137-151)
from notion_client import APIResponseError, APIErrorCode
try:
result = client.pages.create(...)
except APIResponseError as e:
if e.code == APIErrorCode.ObjectNotFound:
print("Page/database not found or not shared with integration")
elif e.code == APIErrorCode.Unauthorized:
print("Token invalid or expired")
elif e.code == APIErrorCode.RateLimited:
print(f"Rate limited. Retry after {e.additional_data.get('retry_after')}s")
else:
raise
uv pip install notion-client # v2.6+ required for data_source support
Or use PEP 723 inline dependencies (scripts include them).
| Issue | Cause | Solution |
|---|---|---|
| Object not found | Page not shared with integration | Share page: ... menu → Connections → Add integration |
| Unauthorized | Token invalid or expired | Generate new token at notion.so/my-integrations |
| Rate limited (429) | Too many requests | Use api_call_with_retry() for automatic handling |
| Empty results from query | Filter matches nothing | Verify filter syntax and property names |
| Block not found after create | Eventual consistency delay | Add 0.5s delay after write before read |
| Invalid property type | Wrong builder used | Check property type in database schema |
| Token format rejected | Wrong prefix (case-sensitive) | Token must start with ntn_ or (lowercase) |
Weekly Installs
69
Repository
GitHub Stars
26
First Seen
Jan 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykFail
Installed on
opencode64
claude-code63
codex62
gemini-cli62
github-copilot60
cursor59
通过 LiteLLM 代理让 Claude Code 对接 GitHub Copilot 运行 | 高级变通方案指南
46,900 周安装
| Unicode/emoji | Fully preserved | Yes |
secret_| Data source ID not working | Old API version | Upgrade notion-client to latest version |