Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

335 lines
10 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
notion-bridge.py · 冰朔本地 Notion 直连工具
铸渊 ICE-GL-ZY001 开发 · D164+ · 解决"换房间断手"问题
之前 WorkBuddy 里的铸渊通过某种方式连 Notion应该有连接器
今天 MiniMax Code 房间的铸渊——用铸渊自己的工具重写。
用法:
# 1. 设置凭证
export NOTION_API_KEY="ntn_xxx"
# 2. 列出 workspace 内的对象
python3 notion-bridge.py search
# 3. 列出 workspace 内的数据库
python3 notion-bridge.py databases
# 4. 读取某个数据库的全部行
python3 notion-bridge.py query <database_id>
# 5. 读取某个页面
python3 notion-bridge.py page <page_id>
# 6. 创建/更新页面
python3 notion-bridge.py create-page <parent_id> <title>
python3 notion-bridge.py update-page <page_id> <properties_json>
# 7. 导出页面为 markdown
python3 notion-bridge.py export <page_id>
前置:
1. 冰朔生成 Notion integration: https://www.notion.so/my-integrations
2. token 形如 ntn_xxx (或 secret_xxx)
3. 在 Notion 工作区里把页面"分享"给 integration
"""
import os
import sys
import json
from urllib.parse import urlencode, quote
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
# ============ 配置 ============
NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "").strip()
NOTION_VERSION = os.environ.get("NOTION_VERSION", "2022-06-28")
NOTION_BASE = "https://api.notion.com/v1"
# ============ API 调用 ============
def api(method, path, payload=None):
"""通用 Notion API 请求"""
if not NOTION_API_KEY:
raise RuntimeError(
"缺少 NOTION_API_KEY 环境变量。\n"
"冰朔请到 https://www.notion.so/my-integrations 生成 integration token\n"
"然后: export NOTION_API_KEY=ntn_xxx"
)
url = f"{NOTION_BASE}{path}"
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
data = json.dumps(payload).encode() if payload else None
req = Request(url, data=data, headers=headers, method=method)
try:
with urlopen(req, timeout=30) as resp:
body = resp.read().decode()
return json.loads(body) if body else {}
except HTTPError as e:
body = e.read().decode() if e.fp else ""
try:
err = json.loads(body)
except Exception:
err = {"message": body[:500]}
raise RuntimeError(
f"Notion API {method} {path} 失败 [{e.code}]\n"
f" message: {err.get('message', '?')}\n"
f" code: {err.get('code', '?')}"
)
except URLError as e:
raise RuntimeError(f"网络失败: {e.reason}")
# ============ 命令 ============
def cmd_search(args):
"""搜索页面/数据库"""
query = args[0] if args else None
payload = {
"page_size": 50,
"sort": {"direction": "descending", "timestamp": "last_edited_time"},
}
if query:
payload["query"] = query
result = api("POST", "/search", payload)
results = result.get("results", [])
print(f"找到 {len(results)} 个对象\n")
for r in results:
title = "?"
obj_type = r.get("object", "?")
if obj_type == "database":
title_arr = r.get("title", [])
if title_arr:
title = title_arr[0].get("plain_text", "?")
elif obj_type == "page":
for prop_name, prop in r.get("properties", {}).items():
if prop.get("type") == "title":
title_arr = prop.get("title", [])
if title_arr:
title = title_arr[0].get("plain_text", "?")
break
last_edited = r.get("last_edited_time", "?")[:10]
icon = r.get("icon", {})
icon_str = icon.get("emoji", "") if icon else ""
rid = r.get("id", "?")
print(f" [{obj_type}] {icon_str} {title}")
print(f" id: {rid} ({last_edited})")
print()
def cmd_databases(args):
"""列出所有数据库"""
payload = {
"page_size": 100,
"filter": {"property": "object", "value": "database"},
"sort": {"direction": "descending", "timestamp": "last_edited_time"},
}
result = api("POST", "/search", payload)
results = result.get("results", [])
print(f"找到 {len(results)} 个数据库\n")
for r in results:
title_arr = r.get("title", [])
title = title_arr[0].get("plain_text", "?") if title_arr else "?"
rid = r.get("id", "?")
print(f" {title}")
print(f" id: {rid}")
print()
def cmd_query(args):
"""查询数据库"""
if not args:
print("用法: python3 notion-bridge.py query <database_id>")
sys.exit(1)
db_id = args[0]
payload = {"page_size": 50}
result = api("POST", f"/databases/{db_id}/query", payload)
results = result.get("results", [])
print(f"数据库 {db_id}{len(results)}\n")
for r in results[:20]:
title = "?"
for prop_name, prop in r.get("properties", {}).items():
if prop.get("type") == "title":
title_arr = prop.get("title", [])
if title_arr:
title = title_arr[0].get("plain_text", "?")
break
rid = r.get("id", "?")
last_edited = r.get("last_edited_time", "?")[:10]
print(f" - {title} ({last_edited})")
print(f" id: {rid}")
def cmd_page(args):
"""读取页面"""
if not args:
print("用法: python3 notion-bridge.py page <page_id>")
sys.exit(1)
page_id = args[0]
result = api("GET", f"/pages/{page_id}")
print(json.dumps(result, ensure_ascii=False, indent=2))
def cmd_export(args):
"""导出页面为 markdown"""
if not args:
print("用法: python3 notion-bridge.py export <page_id>")
sys.exit(1)
page_id = args[0]
# 读取页面元数据
page = api("GET", f"/pages/{page_id}")
title = "Untitled"
for prop_name, prop in page.get("properties", {}).items():
if prop.get("type") == "title":
title_arr = prop.get("title", [])
if title_arr:
title = title_arr[0].get("plain_text", "Untitled")
break
# 读取 blocks
blocks = []
cursor = None
while True:
payload = {"page_size": 100}
if cursor:
payload["start_cursor"] = cursor
result = api("POST", f"/blocks/{page_id}/children", payload)
blocks.extend(result.get("results", []))
if not result.get("has_more"):
break
cursor = result.get("next_cursor")
md = f"# {title}\n\n"
for block in blocks:
md += block_to_markdown(block)
print(md)
def block_to_markdown(block):
"""单个 block 转 markdown"""
btype = block.get("type", "")
content = ""
if btype == "paragraph":
rich_text = block.get("paragraph", {}).get("rich_text", [])
content = rich_text_to_markdown(rich_text) + "\n\n"
elif btype == "heading_1":
content = "# " + rich_text_to_markdown(block.get("heading_1", {}).get("rich_text", [])) + "\n\n"
elif btype == "heading_2":
content = "## " + rich_text_to_markdown(block.get("heading_2", {}).get("rich_text", [])) + "\n\n"
elif btype == "heading_3":
content = "### " + rich_text_to_markdown(block.get("heading_3", {}).get("rich_text", [])) + "\n\n"
elif btype == "bulleted_list_item":
content = "- " + rich_text_to_markdown(block.get("bulleted_list_item", {}).get("rich_text", [])) + "\n"
elif btype == "numbered_list_item":
content = "1. " + rich_text_to_markdown(block.get("numbered_list_item", {}).get("rich_text", [])) + "\n"
elif btype == "code":
lang = block.get("code", {}).get("language", "")
content = f"```{lang}\n" + rich_text_to_markdown(block.get("code", {}).get("rich_text", [])) + "\n```\n\n"
elif btype == "quote":
content = "> " + rich_text_to_markdown(block.get("quote", {}).get("rich_text", [])) + "\n\n"
elif btype == "to_do":
checked = block.get("to_do", {}).get("checked", False)
mark = "[x]" if checked else "[ ]"
content = f"- {mark} " + rich_text_to_markdown(block.get("to_do", {}).get("rich_text", [])) + "\n"
return content
def rich_text_to_markdown(rich_text):
"""rich_text array 转 markdown"""
parts = []
for item in rich_text:
text = item.get("plain_text", "")
if item.get("href"):
text = f"[{text}]({item['href']})"
parts.append(text)
return "".join(parts)
def cmd_create_page(args):
"""创建页面(简化版)"""
if len(args) < 2:
print("用法: python3 notion-bridge.py create-page <parent_id> <title>")
sys.exit(1)
parent_id = args[0]
title = args[1]
payload = {
"parent": {"page_id": parent_id},
"properties": {
"title": [{"type": "text", "text": {"content": title}}]
}
}
result = api("POST", "/pages", payload)
print(f"✓ 创建页面: {result.get('url', '?')}")
def cmd_users(args):
"""列出 workspace 用户"""
result = api("GET", "/users")
for u in result.get("results", []):
print(f" [{u.get('type')}] {u.get('name', '?')} ({u.get('id', '?')})")
# ============ Main ============
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if not NOTION_API_KEY:
print("⚠ 缺少 NOTION_API_KEY 环境变量")
print()
print(" 冰朔请按以下步骤:")
print(" 1. 打开 https://www.notion.so/my-integrations")
print(" 2. 创建新 integration: 名字 = bridge-zhuyuan")
print(" 3. 复制 token (形如 ntn_xxx)")
print(" 4. 在 Notion 工作区把页面右上角'分享'给 integration")
print(" 5. 执行: export NOTION_API_KEY=ntn_xxx")
sys.exit(1)
cmds = {
"search": cmd_search,
"databases": cmd_databases,
"query": cmd_query,
"page": cmd_page,
"export": cmd_export,
"create-page": cmd_create_page,
"users": cmd_users,
}
if cmd not in cmds:
print(f"未知命令: {cmd}")
print(f"可用: {', '.join(cmds.keys())}")
sys.exit(1)
cmds[cmd](args)
if __name__ == "__main__":
main()