2026-05-22 23:05:20 +08:00
|
|
|
|
#!/usr/bin/env python3
|
2026-05-22 23:53:12 +08:00
|
|
|
|
"""Notion ↔ 代码仓库双向同步器"""
|
2026-05-22 23:12:05 +08:00
|
|
|
|
import os, sys, json, time, subprocess
|
2026-05-22 23:05:20 +08:00
|
|
|
|
from datetime import datetime
|
2026-05-22 23:53:12 +08:00
|
|
|
|
try: import requests
|
|
|
|
|
|
except: print('pip install requests'); sys.exit(1)
|
2026-05-22 23:05:20 +08:00
|
|
|
|
|
2026-05-22 23:53:12 +08:00
|
|
|
|
NOTION_TOKEN = os.environ.get('NOTION_TOKEN') or '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'
|
|
|
|
|
|
# 数据库URL中的ID(不是集合ID)
|
|
|
|
|
|
NOTION_DATABASE_ID = '663e4f29c7404d088f3a02fcc70b1418'
|
2026-05-22 23:05:20 +08:00
|
|
|
|
REPO_PATH = '/opt/guanghulab-repo'
|
2026-05-22 23:53:12 +08:00
|
|
|
|
DB_PATH = os.path.join(REPO_PATH, 'persona-brain-db', 'brain.db')
|
|
|
|
|
|
CACHE_FILE = os.path.join(REPO_PATH, 'notion', 'tickets-cache.json')
|
|
|
|
|
|
INTERVAL = int(os.environ.get('SYNC_INTERVAL', '1800'))
|
|
|
|
|
|
HEADERS = {'Authorization': f'Bearer {NOTION_TOKEN}', 'Notion-Version': '2022-06-28', 'Content-Type': 'application/json'}
|
2026-05-22 23:05:20 +08:00
|
|
|
|
|
2026-05-22 23:53:12 +08:00
|
|
|
|
def log(m): print(f'[{datetime.now().strftime("%H:%M:%S")}] {m}', flush=True)
|
2026-05-22 23:12:05 +08:00
|
|
|
|
|
2026-05-22 23:53:12 +08:00
|
|
|
|
def query():
|
|
|
|
|
|
url = f'https://api.notion.com/v1/databases/{NOTION_DATABASE_ID}/query'
|
|
|
|
|
|
body = {'page_size': 50, 'sorts': [{'timestamp': 'last_edited_time', 'direction': 'descending'}]}
|
2026-05-22 23:05:20 +08:00
|
|
|
|
try:
|
2026-05-22 23:53:12 +08:00
|
|
|
|
r = requests.post(url, headers=HEADERS, json=body, timeout=15)
|
|
|
|
|
|
if r.status_code == 401: log('Token过期'); return []
|
|
|
|
|
|
if not r.ok: log(f'API错误 {r.status_code}'); return []
|
|
|
|
|
|
return r.json().get('results', [])
|
|
|
|
|
|
except Exception as e: log(f'异常: {e}'); return []
|
2026-05-22 23:05:20 +08:00
|
|
|
|
|
2026-05-22 23:53:12 +08:00
|
|
|
|
def get_mine(tickets):
|
2026-05-22 23:12:05 +08:00
|
|
|
|
mine = []
|
|
|
|
|
|
for t in tickets:
|
2026-05-22 23:53:12 +08:00
|
|
|
|
p = t.get('properties', {})
|
|
|
|
|
|
agent = p.get('负责Agent', {}).get('select', {}).get('name', '')
|
|
|
|
|
|
status = p.get('状态', {}).get('select', {}).get('name', '')
|
|
|
|
|
|
title = p.get('任务标题', {}).get('title', [{}])[0].get('plain_text', '?')
|
|
|
|
|
|
code = p.get('编号', {}).get('rich_text', [{}])[0].get('plain_text', '') if not p.get('编号', {}).get('type') else ''
|
2026-05-22 23:12:05 +08:00
|
|
|
|
if agent in ('铸渊', '') and status not in ('已完成', '暂缓'):
|
2026-05-22 23:53:12 +08:00
|
|
|
|
mine.append({'page_id': t['id'], 'title': title, 'status': status, 'agent': agent, 'url': t.get('url', ''), 'code': code})
|
2026-05-22 23:12:05 +08:00
|
|
|
|
return mine
|
2026-05-22 23:05:20 +08:00
|
|
|
|
|
|
|
|
|
|
def sync_cycle():
|
2026-05-22 23:53:12 +08:00
|
|
|
|
log('=' * 40)
|
|
|
|
|
|
tickets = query()
|
|
|
|
|
|
if not tickets: return
|
|
|
|
|
|
mine = get_mine(tickets)
|
2026-05-22 23:12:05 +08:00
|
|
|
|
if mine:
|
|
|
|
|
|
log(f'铸渊相关工单: {len(mine)} 条')
|
|
|
|
|
|
for item in mine:
|
2026-05-22 23:53:12 +08:00
|
|
|
|
log(f' [{item["status"]}] {item["agent"]} | {item["title"][:40]}')
|
2026-05-22 23:05:20 +08:00
|
|
|
|
else:
|
2026-05-22 23:12:05 +08:00
|
|
|
|
log('无铸渊相关工单')
|
2026-05-22 23:53:12 +08:00
|
|
|
|
try:
|
|
|
|
|
|
with open(CACHE_FILE, 'w') as f:
|
|
|
|
|
|
json.dump({'last_sync': datetime.now().isoformat(), 'tickets': [{'id': t['id'], 'title': t.get('properties',{}).get('任务标题',{}).get('title',[{}])[0].get('plain_text',''), 'status': t.get('properties',{}).get('状态',{}).get('select',{}).get('name','')} for t in tickets]}, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
except: pass
|
|
|
|
|
|
log('同步完成')
|
2026-05-22 23:05:20 +08:00
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2026-05-22 23:53:12 +08:00
|
|
|
|
log(f'Notion同步器启动 | 间隔{INTERVAL//60}分钟 | 数据库: {NOTION_DATABASE_ID[:12]}...')
|
2026-05-22 23:05:20 +08:00
|
|
|
|
sync_cycle()
|
2026-05-22 23:53:12 +08:00
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
time.sleep(INTERVAL)
|
2026-05-22 23:05:20 +08:00
|
|
|
|
sync_cycle()
|
2026-05-22 23:53:12 +08:00
|
|
|
|
except KeyboardInterrupt: log('停止')
|