From 0abd9a519e21cad5f9d8e2a1f0ae4a05e313aff7 Mon Sep 17 00:00:00 2001 From: bingshuo <565183519@qq.com> Date: Fri, 22 May 2026 23:12:05 +0800 Subject: [PATCH] feat: update sync-ticket.py - connect to existing half-body orders database --- notion/sync-ticket.py | 173 +++++++++++++----------------------------- 1 file changed, 51 insertions(+), 122 deletions(-) diff --git a/notion/sync-ticket.py b/notion/sync-ticket.py index 3c573d7..e7fd986 100644 --- a/notion/sync-ticket.py +++ b/notion/sync-ticket.py @@ -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() \ No newline at end of file + log(f'[严重] {e}') + import traceback; traceback.print_exc() \ No newline at end of file