1. 从一句提示词到能跑的 Notion 同步程序
很多人第一次用 claude code 写 Python 项目,卡住的地方不是代码本身,而是「提示词怎么给」和「config.json 里 API 通道怎么配」。我这次要做的,是一个把 Notion 工作空间单向同步到本地硬盘的 Python 程序:支持增量同步、带 Web 界面、能登录改密码、能看业务日志和系统日志、监听端口写在 config.json 里。听起来功能不少,但拆开看就是「提示词驱动开发 + 统一 Key 接入」两件事。
先说清楚这个程序是什么、能做什么、适合谁。它是一个本地运行的 Python 服务,启动后监听一个端口,浏览器打开就能登录,登录后可以触发同步、查看同步日志和运行日志。同步逻辑是单向的:从 Notion 拉到本地目录,第二次运行只拉变化的部分,也就是增量同步。适合谁?适合手里有一堆 Notion 页面、想定期备份到本地、又不想手动导出的人;也适合想练手 claude code 提示词工程、顺便把 API 通道配置跑通的开发者。
核心检索词就三个:claude code 提示词、Notion 同步程序、config.json 配置。这三个词贯穿全文。我会先讲提示词怎么组织,再讲 config.json 骨架长什么样,然后给出可复制的配置片段,最后用真实请求验证同步是否跑通,并把常见报错一个个拆开。
提示词这块,我的经验是别一次性把需求全丢进去。claude code 在 plan 模式下更擅长「先规划再动手」,所以第一步是让它输出模块划分,而不是直接写代码。你可以这样给第一段提示词:
我要用 Python 写一个 Notion 单向同步程序,需求如下: 1. 从 Notion 工作空间拉取页面,保存到本地目录 2. 支持增量同步,第二次运行只拉变化内容 3. 带 Web 界面,可登录,默认 admin/admin123,可改密码 4. 配置界面能填 Notion 凭据,保存到 config.json 5. 显示业务日志和系统日志 6. 监听端口写在 config.json,可手动改 请先输出模块划分和文件结构,不要写完整代码。这段提示词的关键是最后一句「先输出模块划分」。如果不加这句,claude code 容易一口气生成几百行,改起来反而麻烦。等它给出结构,你再逐模块让它补代码,比如「实现 config.json 的读写模块」「实现增量同步的比对逻辑」。这样每一步都可验证,出错也好定位。
模块划分大概会是这样:config.py管配置读写,notion_client.py管 API 调用,sync.py管增量比对,web.py管界面和登录,logger.py管双日志。文件结构清楚了,后面填代码就是体力活。这里有个坑:Notion 官方 API 需要 integration token,而不是邮箱密码。excerpt 里提到「配置 notion 的 email 和密码」,实际落地时你会发现官方通道走的是 token。所以 config.json 里我保留了 email 字段做展示,但真正用于请求的是 token 字段。这一点在提示词里要提前说明,否则生成的代码会去调一个不存在的登录接口。
2. TaoToken 前置:统一 Key 与 config.json 通道设计
在写同步逻辑之前,先把 API 通道定下来。程序里所有需要调用模型能力的地方(比如让模型帮你总结页面内容、生成同步摘要),都走同一个入口,这样 Key 只需要配一次。我用的是 TaoToken 的统一 Key 方案,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 入口是 https://taotoken.net/api 。注意 API 地址后面不加任何查询参数,保持干净。
为什么要在 config.json 里单独设计一个 API 通道段?因为同步程序不只是「拉数据」,它还要在拉取后做内容处理。如果每次处理都硬编码一个地址和 Key,换环境就得改代码。把通道信息抽到 config.json,改配置不动代码,这是基本工程习惯。
config.json 的通道段我设计成三个字段:base_url、api_key、model。这三个就是常说的「三件套」,缺一不可。base_url 填 https://taotoken.net/api ,api_key 填你在控制台生成的 Key,model 填你要用的模型 ID。这里要提醒一句:model ID 必须和你账号里可用的模型一致,填错会直接报模型不存在。
获取 Key 的路径是:先打开 https://taotoken.net/api-keys ,登录后创建 Key,复制出来。这个 Key 只显示一次,建议直接粘进 config.json,别放聊天记录里。如果你还没决定用哪个模型,可以先去 https://taotoken.net/models 看看可用列表,或者在 https://taotoken.net/chat 里试一次对话,确认通道通不通。
这里有个容易混淆的点:TaoToken 是统一接入层,不是让你绕过什么。它的作用是把你对多个模型通道的调用收敛到一个 Key、一个 base_url 上。对同步程序来说,好处是配置简单、切换模型只改一个字段。我在提示词里会明确告诉 claude code:「所有模型调用统一走 config.json 里的 api 段,不要硬编码地址和 Key。」这样生成的代码天然就是可配置的。
如果你打算长期跑这个同步程序,甚至后面接 Agent 做自动整理,可以考虑 Coding Plan 这类长期方案,入口在 https://taotoken.net/coding-plan 。它的意义是把调用额度前置规划好,避免程序跑一半因为额度问题中断。同步程序如果是定时任务,这点尤其重要。
配置段设计好之后,下一步就是把它写进 config.json 骨架,并让 Python 代码能正确读取。这里我不建议用环境变量兜底,因为需求里明确说「配置保存在 config.json」,那就以文件为准,环境变量只做覆盖用。读取逻辑要处理文件不存在的情况:首次启动时如果 config.json 不存在,程序应该生成一份默认配置,而不是直接崩溃。这个默认配置里,api 段的 base_url 预填 https://taotoken.net/api ,api_key 留空,model 留一个占位值,提示用户去填。
3. 可复制 config.json 骨架与 settings 片段
这一节直接给可复制的内容。config.json 放在项目根目录,和main.py同级。骨架如下:
{ "server": { "host": "0.0.0.0", "port": 8080, "secret_key": "change-this-to-a-random-string" }, "auth": { "username": "admin", "password": "admin123" }, "notion": { "email": "you@example.com", "token": "ntn_xxxxxxxxxxxxxxxx", "root_page_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "local_dir": "./backup" }, "api": { "base_url": "https://taotoken.net/api", "api_key": "sk-xxxxxxxxxxxxxxxx", "model": "your-model-id" }, "sync": { "interval_minutes": 30, "incremental": true }, "log": { "business_log": "./logs/business.log", "system_log": "./logs/system.log", "level": "INFO" } }几个字段说明一下。server.port就是监听端口,改完重启生效,不需要动 Web 界面。auth段是登录凭据,默认 admin/admin123,程序里要提供改密码接口,改完写回这个文件。notion.token是真正的请求凭据,email只做展示。notion.root_page_id是你要同步的根页面 ID,从 Notion 页面 URL 里取最后一段。api段就是前面说的三件套,base_url 固定 https://taotoken.net/api ,api_key 换成你自己的,model 换成可用模型 ID。
如果你用的是带 settings 的框架(比如某些 Python Web 框架的配置文件),可以写成 TOML:
[server] host = "0.0.0.0" port = 8080 [api] base_url = "https://taotoken.net/api" api_key = "sk-xxxxxxxxxxxxxxxx" model = "your-model-id" [notion] token = "ntn_xxxxxxxxxxxxxxxx" local_dir = "./backup"TOML 和 JSON 二选一,关键是字段名和层级保持一致,这样代码里读取路径不用改。我实测下来,JSON 更适合让 claude code 生成,因为它对 JSON 结构的理解更稳,不容易漏括号。
配置写好后,Python 读取代码大概长这样:
import json from pathlib import Path CONFIG_PATH = Path("config.json") def load_config(): if not CONFIG_PATH.exists(): raise FileNotFoundError("config.json 不存在,请先创建") with CONFIG_PATH.open("r", encoding="utf-8") as f: cfg = json.load(f) api = cfg.get("api", {}) if not api.get("api_key"): raise ValueError("api.api_key 未配置") if not api.get("base_url"): raise ValueError("api.base_url 未配置") return cfg这段代码做了两件事:文件不存在时报明确错误,api_key 或 base_url 为空时提前拦截。别小看这两个检查,后面排错时能省很多时间。如果你在提示词里让 claude code 生成读取逻辑,记得加上「对 api 段做非空校验」这一句。
还有一个细节:secret_key用于 Web 登录的会话签名,默认值必须改。程序首次启动时可以检测它是否还是默认值,如果是就打印警告。这个逻辑也写进提示词里,让 claude code 一并生成。
4. 验证请求:从登录到增量同步跑通
配置就绪后,验证分三步:先验证 API 通道,再验证 Notion 拉取,最后验证增量同步。
第一步,验证 API 通道。写一个最小脚本,用 config.json 里的 api 段发一次请求:
import json import requests cfg = json.load(open("config.json", encoding="utf-8")) api = cfg["api"] resp = requests.post( f"{api['base_url']}/v1/chat/completions", headers={ "Authorization": f"Bearer {api['api_key']}", "Content-Type": "application/json" }, json={ "model": api["model"], "messages": [{"role": "user", "content": "ping"}] }, timeout=30 ) print(resp.status_code) print(resp.text[:500])如果返回 200 并且 body 里有 choices 字段,说明通道通了。如果返回 401,说明 Key 不对;如果返回 404,多半是 base_url 写错,检查是不是多写了斜杠或路径。这一步过了,再往下走。
第二步,验证 Notion 拉取。用 notion.token 调一次查询接口,确认能拿到页面列表:
import requests cfg = json.load(open("config.json", encoding="utf-8")) notion = cfg["notion"] resp = requests.post( "https://api.notion.com/v1/search", headers={ "Authorization": f"Bearer {notion['token']}", "Notion-Version": "2022-06-28", "Content-Type": "application/json" }, json={"page_size": 5}, timeout=30 ) print(resp.status_code) print(len(resp.json().get("results", [])))能打印出结果数量,说明 token 和权限没问题。注意 Notion integration 必须被显式授权到目标页面,否则返回空列表。这一步的坑我在下一节细说。
第三步,验证增量同步。启动 Web 服务,浏览器打开http://localhost:8080,用 admin/admin123 登录,点「开始同步」。第一次会全量拉取,日志里能看到每个页面的处理记录。等第一次跑完,手动改一个 Notion 页面的标题,再点一次同步,观察日志里是否只处理了那一个页面。如果只处理了变化页面,增量逻辑就对了。
增量比对的核心是记录每个页面的last_edited_time。本地维护一个sync_state.json,结构如下:
{ "pages": { "page-id-1": {"last_edited_time": "2024-01-01T00:00:00.000Z", "local_path": "./backup/page-1.md"}, "page-id-2": {"last_edited_time": "2024-01-02T00:00:00.000Z", "local_path": "./backup/page-2.md"} } }每次同步时,先拉 Notion 端的last_edited_time,和本地记录比对,不一致才重新拉取内容并覆盖本地文件。这个逻辑不复杂,但提示词里要写清楚「用 last_edited_time 做增量判断,状态存 sync_state.json」,否则 claude code 可能用文件修改时间做比对,那就不准了。
验证通过后,你可以把同步设成定时任务,sync.interval_minutes控制间隔。日志分两个文件:业务日志记「哪个页面同步成功/失败」,系统日志记「程序启动、异常堆栈、请求耗时」。两个日志分开,排错时一眼能看出是业务问题还是程序问题。
5. 常见报错排查:401、local proxy failed、reading choices
这一节按真实报错来。第一个,401 Unauthorized。出现在 API 请求时,说明 api_key 无效或没带上。检查三处:config.json 里 api_key 是否为空、请求头是否是Bearer加 Key、Key 是否被复制时带了空格。我踩过的坑是 Key 末尾多了个换行,肉眼看不出来,用repr()打印一下就能发现。
第二个,local proxy failed。这个报错通常出现在请求发不出去的时候,本质是网络层没通。先确认 base_url 是不是 https://taotoken.net/api ,别写成别的路径。再确认本机能不能正常访问外网。如果程序跑在容器里,检查容器网络配置。这个报错和 Key 无关,别去反复改 Key。
第三个,reading 'choices' 报错,完整信息类似KeyError: 'choices'或Cannot read property 'choices' of undefined。这说明请求返回了,但返回体里没有 choices 字段。常见原因有两个:一是 model ID 填错,服务端返回了错误信息而不是正常响应;二是请求体格式不对,比如 messages 字段拼错。排查方法:把resp.text完整打印出来,看服务端到底返回了什么。如果是模型不存在,换一个可用 model ID;如果是参数错误,对照文档改请求体。
第四个,OAuth 相关报错。如果你在配置 Notion 时看到 OAuth 字样,说明你走的是 OAuth 授权流程,而不是 integration token。两条路都行,但配置字段不同。走 token 的话,config.json 里填notion.token;走 OAuth 的话,需要额外的 client_id 和 client_secret。我建议先用 token,简单直接。如果你确实要用 OAuth,记得把回调地址配成http://localhost:8080/callback,和 server.port 保持一致。
第五个,同步后本地文件为空。这通常不是报错,而是权限问题。Notion integration 创建后,必须手动把目标页面「分享」给它,否则 search 接口返回空列表,程序以为没有页面可同步。检查方法:在 Notion 页面右上角点分享,看 integration 是否在列表里。不在就加上。
第六个,改密码后登录失败。改密码接口写回 config.json 时,如果没做原子写入,文件可能被截断。建议先写临时文件再替换:
import os, json, tempfile def save_config(cfg, path="config.json"): fd, tmp = tempfile.mkstemp(dir=".") with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(cfg, f, ensure_ascii=False, indent=2) os.replace(tmp, path)这样即使写入过程中断电,原文件也不会坏。这个细节写进提示词,让 claude code 生成安全的写回逻辑。
排错时如果拿不准是通道问题还是代码问题,可以先去 https://taotoken.net/chat 手动发一条消息,确认通道本身可用。通道可用而程序报错,那就是代码或配置字段的问题,范围就缩小了。接入相关的文档在 https://taotoken.net/doc ,字段含义和请求格式都能查到。
6. 把提示词、配置、验证串成一条流水线
到这里,整个流程其实是一条流水线:提示词驱动 claude code 生成模块 → config.json 承载所有可变配置 → 三件套(base_url、api_key、model)统一走 TaoToken → 验证请求分三步走 → 报错按类型定位。每一步都可单独验证,不用等全部写完才跑。
如果你要长期维护这个同步程序,建议把 config.json 纳入版本管理时排除敏感字段,或者用config.example.json做模板,真实文件加进.gitignore。api_key 和 notion.token 都不要提交。程序启动时读取真实文件,模板只做参考。
另外,增量同步的状态文件sync_state.json也要注意:如果本地目录被清空但状态文件还在,程序会以为页面已同步而跳过,导致本地没有文件。解决办法是启动时校验状态文件里记录的 local_path 是否存在,不存在就强制重新拉取。这个校验逻辑值得加进提示词。
最后给一个实用技巧:把同步程序做成命令行可触发,比如python main.py --sync-once,这样你可以先用命令行验证逻辑,再开 Web 界面。Web 界面只是壳,核心逻辑在命令行能跑通,排错会快很多。提示词里加一句「提供 --sync-once 参数,执行一次同步后退出」,claude code 会帮你把入口拆干净。
整套跑下来,你会发现 claude code 提示词的关键不是写得多长,而是每一步都给明确的验收标准:先要结构,再要模块,再要校验,最后要验证命令。config.json 的关键不是字段多,而是把通道信息收敛成三件套,改配置不动代码。这两点做到,Notion 同步程序就能稳定跑起来。