feat: update sync-ticket.py - connect to existing half-body orders database
This commit is contained in:
parent
e3b3f2c9f4
commit
0abd9a519e
@ -1,22 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Notion ↔ 代码仓库双向同步器
|
||||
对接数据库:⚡ 半体工单 · Half-Body Orders (05719bae-9e53-40f4-8233-6bf54551ac18)
|
||||
同步周期:30分钟轮询
|
||||
|
||||
依赖:
|
||||
pip install requests
|
||||
|
||||
启动:
|
||||
export NOTION_TOKEN="secret_xxx"
|
||||
export NOTION_DATABASE_ID="xxx"
|
||||
export NOTION_TOKEN="xxx"
|
||||
export NOTION_DATABASE_ID="05719bae-9e53-40f4-8233-6bf54551ac18"
|
||||
pm2 start notion/sync-ticket.py --name notion-ticket-sync
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
import os, sys, json, time, subprocess
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
@ -27,10 +24,10 @@ except ImportError:
|
||||
|
||||
# ── 配置 ──
|
||||
NOTION_TOKEN = os.environ.get('NOTION_TOKEN')
|
||||
NOTION_DATABASE_ID = os.environ.get('NOTION_DATABASE_ID')
|
||||
NOTION_DATABASE_ID = os.environ.get('NOTION_DATABASE_ID') or '05719bae-9e53-40f4-8233-6bf54551ac18'
|
||||
REPO_PATH = '/opt/guanghulab-repo'
|
||||
CACHE_FILE = os.path.join(REPO_PATH, 'notion/tickets-cache.json')
|
||||
INTERVAL = int(os.environ.get('SYNC_INTERVAL', '1800')) # 默认30分钟
|
||||
INTERVAL = int(os.environ.get('SYNC_INTERVAL', '1800')) # 30分钟
|
||||
|
||||
NOTION_API = 'https://api.notion.com/v1'
|
||||
HEADERS = {
|
||||
@ -39,114 +36,63 @@ HEADERS = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# 负责Agent选项(含铸渊)
|
||||
AGENTS = ['译典A05', '培园A04', '录册A02', '霜砚Web', '铸渊']
|
||||
|
||||
|
||||
def log(msg):
|
||||
"""带时间戳的日志"""
|
||||
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f'[{ts}] {msg}', flush=True)
|
||||
|
||||
|
||||
def get_repo_context():
|
||||
"""获取代码仓库的最新状态"""
|
||||
context = {}
|
||||
try:
|
||||
# 获取最近提交
|
||||
result = subprocess.run(
|
||||
['git', '-C', REPO_PATH, 'log', '--oneline', '-5'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
context['recent_commits'] = result.stdout.strip()
|
||||
except Exception as e:
|
||||
context['recent_commits'] = f'Error: {e}'
|
||||
|
||||
try:
|
||||
# 获取最新认知链
|
||||
result = subprocess.run(
|
||||
['ls', '-t', f'{REPO_PATH}/brain/d*-cognitive-chain.md'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
chains = [f for f in result.stdout.strip().split('\n') if f]
|
||||
context['latest_chain'] = chains[0] if chains else 'N/A'
|
||||
except Exception as e:
|
||||
context['latest_chain'] = f'Error: {e}'
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def query_notion():
|
||||
"""查询 Notion 工单数据库"""
|
||||
if not NOTION_TOKEN or not NOTION_DATABASE_ID:
|
||||
log('[跳过] 未配置 NOTION_TOKEN 或 NOTION_DATABASE_ID')
|
||||
def query_notion_tickets():
|
||||
"""查询Notion半体工单数据库"""
|
||||
if not NOTION_TOKEN:
|
||||
log('[跳过] 未配置 NOTION_TOKEN')
|
||||
return []
|
||||
|
||||
url = f'{NOTION_API}/databases/{NOTION_DATABASE_ID}/query'
|
||||
body = {
|
||||
'page_size': 50,
|
||||
'sorts': [{'timestamp': 'last_edited_time', 'direction': 'descending'}]
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.post(url, headers=HEADERS, json=body, timeout=15)
|
||||
if res.status_code == 401:
|
||||
log('[错误] Notion token 无效或已过期,请刷新')
|
||||
log('[错误] Notion token 无效或已过期')
|
||||
return []
|
||||
if not res.ok:
|
||||
log(f'[错误] Notion API: {res.status_code} {res.text[:200]}')
|
||||
log(f'[错误] Notion {res.status_code}')
|
||||
return []
|
||||
|
||||
data = res.json()
|
||||
results = data.get('results', [])
|
||||
log(f'查询到 {len(results)} 条工单')
|
||||
return results
|
||||
except Exception as e:
|
||||
log(f'[异常] 查询 Notion 失败: {e}')
|
||||
log(f'[异常] {e}')
|
||||
return []
|
||||
|
||||
|
||||
def find_pending_syncs(tickets):
|
||||
"""找出需要同步到代码仓库的工单"""
|
||||
pending = []
|
||||
for ticket in tickets:
|
||||
props = ticket.get('properties', {})
|
||||
sync_status = props.get('同期状态', props.get('同步状态', {}))
|
||||
status = sync_status.get('select', {}).get('name', '')
|
||||
if status == '待同步':
|
||||
title = props.get('标题', {}).get('title', [{}])[0].get('plain_text', '无标题')
|
||||
ticket_type = props.get('类型', {}).get('select', {}).get('name', '任务')
|
||||
pending.append({
|
||||
'page_id': ticket['id'],
|
||||
def get_my_tickets(tickets):
|
||||
"""找出分配给铸渊或未分配的工单"""
|
||||
mine = []
|
||||
for t in tickets:
|
||||
props = t.get('properties', {})
|
||||
agent = props.get('负责Agent', {}).get('select', {}).get('name', '')
|
||||
status = props.get('状态', {}).get('select', {}).get('name', '')
|
||||
title = props.get('任务标题', {}).get('title', [{}])[0].get('plain_text', '无标题')
|
||||
if agent in ('铸渊', '') and status not in ('已完成', '暂缓'):
|
||||
mine.append({
|
||||
'page_id': t['id'],
|
||||
'title': title,
|
||||
'type': ticket_type,
|
||||
'priority': props.get('优先级', {}).get('select', {}).get('name', 'P2'),
|
||||
'url': ticket.get('url', '')
|
||||
'status': status,
|
||||
'agent': agent,
|
||||
'url': t.get('url', ''),
|
||||
'code': props.get('编号', {}).get('rich_text', [{}])[0].get('plain_text', '') if not props.get('编号', {}).get('type') else ''
|
||||
})
|
||||
return pending
|
||||
return mine
|
||||
|
||||
|
||||
def create_forgejo_issue(title, body, labels):
|
||||
"""在 Forgejo 创建 Issue"""
|
||||
# Forgejo API 调用
|
||||
log(f'[创建] Issue: {title}')
|
||||
return True
|
||||
|
||||
|
||||
def mark_synced(page_id):
|
||||
"""标记 Notion 页面为已同步"""
|
||||
url = f'{NOTION_API}/pages/{page_id}'
|
||||
try:
|
||||
res = requests.patch(url, headers=HEADERS, json={
|
||||
'properties': {
|
||||
'同步状态': {'select': {'name': '已同步'}}
|
||||
}
|
||||
})
|
||||
return res.ok
|
||||
except Exception as e:
|
||||
log(f'[错误] 标记同步失败: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def save_tickets_cache(tickets):
|
||||
"""保存工单快照到本地缓存"""
|
||||
def save_cache(tickets):
|
||||
try:
|
||||
with open(CACHE_FILE, 'w') as f:
|
||||
json.dump({
|
||||
@ -154,61 +100,45 @@ def save_tickets_cache(tickets):
|
||||
'count': len(tickets),
|
||||
'tickets': [{
|
||||
'id': t['id'],
|
||||
'title': t.get('properties', {}).get('标题', {}).get('title', [{}])[0].get('plain_text', ''),
|
||||
'status': t.get('properties', {}).get('状态', {}).get('select', {}).get('name', ''),
|
||||
'url': t.get('url', '')
|
||||
'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 Exception as e:
|
||||
log(f'[警告] 保存缓存失败: {e}')
|
||||
log(f'[警告] 缓存失败: {e}')
|
||||
|
||||
|
||||
def sync_cycle():
|
||||
"""一次同步周期"""
|
||||
log('=' * 50)
|
||||
log('开始同步周期')
|
||||
|
||||
# 1. 获取仓库状态
|
||||
context = get_repo_context()
|
||||
log(f'仓库: {context.get("latest_chain", "?")}')
|
||||
|
||||
# 2. 查询 Notion
|
||||
tickets = query_notion()
|
||||
tickets = query_notion_tickets()
|
||||
if not tickets:
|
||||
log('无工单数据')
|
||||
return
|
||||
|
||||
# 3. 找出待同步条目
|
||||
pending = find_pending_syncs(tickets)
|
||||
if pending:
|
||||
log(f'待同步到仓库: {len(pending)} 条')
|
||||
for item in pending:
|
||||
log(f' → {item["title"]} ({item["priority"]})')
|
||||
# TODO: 创建 Forgejo Issue
|
||||
mark_synced(item['page_id'])
|
||||
mine = get_my_tickets(tickets)
|
||||
if mine:
|
||||
log(f'铸渊相关工单: {len(mine)} 条')
|
||||
for item in mine:
|
||||
log(f' → {item["title"]} [{item["status"]}]')
|
||||
else:
|
||||
log('无待同步条目')
|
||||
|
||||
# 4. 保存缓存
|
||||
save_tickets_cache(tickets)
|
||||
log('无铸渊相关工单')
|
||||
|
||||
save_cache(tickets)
|
||||
log('同步周期完成')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log(f'Notion ↔ 代码仓库同步器启动')
|
||||
log(f'轮询间隔: {INTERVAL}秒 = {INTERVAL//60}分钟')
|
||||
log(f'轮询间隔: {INTERVAL}秒')
|
||||
log(f'数据库: {NOTION_DATABASE_ID}')
|
||||
|
||||
if not NOTION_TOKEN or not NOTION_DATABASE_ID:
|
||||
log('⚠️ 未配置 Notion 凭证')
|
||||
log('请设置环境变量:')
|
||||
log(' export NOTION_TOKEN="secret_xxx"')
|
||||
log(' export NOTION_DATABASE_ID="your-db-id"')
|
||||
if not NOTION_TOKEN:
|
||||
log('⚠️ 未配置 NOTION_TOKEN')
|
||||
log('请设置: export NOTION_TOKEN="secret_xxx"')
|
||||
|
||||
# 首次同步
|
||||
sync_cycle()
|
||||
|
||||
# 定时轮询
|
||||
while True:
|
||||
time.sleep(INTERVAL)
|
||||
try:
|
||||
@ -217,6 +147,5 @@ if __name__ == '__main__':
|
||||
log('同步器停止')
|
||||
break
|
||||
except Exception as e:
|
||||
log(f'[严重] 同步周期异常: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
log(f'[严重] {e}')
|
||||
import traceback; traceback.print_exc()
|
||||
Loading…
x
Reference in New Issue
Block a user