#!/usr/bin/env python3 """铸渊常驻Agent - 自动响应Notion工单""" import os, sys, json, time, re from datetime import datetime import urllib.request, urllib.error NOTION_TOKEN = "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" NOTION_DB_ID = "663e4f29c7404d088f3a02fcc70b1418" PORTAL = "http://127.0.0.1:3000" POLL = 1800 def log(m): print(f'[{datetime.now().strftime("%H:%M:%S")}] {m}', flush=True) def notion(method, path, data=None): url = 'https://api.notion.com/v1/' + path h = {'Authorization': 'Bearer ' + NOTION_TOKEN, 'Notion-Version': '2022-06-28', 'Content-Type': 'application/json'} b = json.dumps(data).encode() if data else None try: with urllib.request.urlopen(urllib.request.Request(url, b, h, method=method), timeout=15) as r: return json.loads(r.read()) except: return None def read_ticket(page_id): """读取单个工单的完整内容""" r = notion('GET', 'pages/' + page_id) if not r: return None p = r.get('properties', {}) title = p.get('任务标题', {}).get('title', [{}])[0].get('plain_text', '?') content = '' dc = p.get('开发内容', {}).get('rich_text', []) if dc: content = dc[0].get('plain_text', '') code = '' nc = p.get('编号', {}).get('rich_text', []) if nc: code = nc[0].get('plain_text', '') return {'title': title, 'content': content, 'code': code, 'page_id': page_id} def get_tickets(): r = notion('POST', 'databases/' + NOTION_DB_ID + '/query', { 'page_size': 20, 'sorts': [{'timestamp': 'last_edited_time', 'direction': 'descending'}] }) if not r: return [] tks = [] try: with open('/opt/guanghulab-repo/notion/tickets-cache.json') as f: cache = json.load(f) seen = set(t['id'] for t in cache.get('tickets', [])) except: seen = set() for t in r.get('results', []): 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', '?') content = '' dev_content = p.get('开发内容', {}).get('rich_text', []) if dev_content: content = dev_content[0].get('plain_text', '')[:500] if agent in ('铸渊', '') and status in ('待开发', '开发中'): tks.append({ 'page_id': t['id'], 'title': title, 'status': status, 'content': content, 'new': t['id'] not in seen }) return tks def reply(page_id, text): children = [{ 'object': 'block', 'type': 'callout', 'callout': { 'rich_text': [{'type': 'text', 'text': {'content': text[:1900]}}], 'icon': {'emoji': '⚔️'}, 'color': 'orange_background' } }] return notion('PATCH', 'blocks/' + page_id + '/children', {'children': children}) is not None def chat(prompt): import subprocess body = '{"persona":"zhuyuan","engine":"deepseek","message":"' + prompt.replace('"', '\"').replace("'", "\'")[:1500] + '","history":[],"user":"铸渊Agent","isTemp":false}' try: result = subprocess.run([ 'curl', '-s', '--max-time', '60', '-X', 'POST', PORTAL + '/api/chat-v2', '-H', 'Content-Type: application/json', '-d', body ], capture_output=True, text=True, timeout=70) matches = re.findall(r'data: (.+)', result.stdout) if matches: data = json.loads(matches[-1]) return data.get('content', str(data)[:300]) return result.stdout[:500] except Exception as e: log(f'Chat异常 {e}') return None def process(t): log(f'处理: {t["title"][:30]}') p = '请你铸渊评估此工单:\n\n' + t['content'][:1000] r = chat(p) if not r: return msg = '⚔️铸渊评估 ' + datetime.now().strftime('%m-%d %H:%M') + '\n\n' + r[:1500] if reply(t['page_id'], msg): log(' ✅ 已回复') else: log(' ❌ 写入失败') if __name__ == '__main__': log('⚔️铸渊Agent启动') log('轮询: 每' + str(POLL) + '秒') tks = get_tickets() if tks: log('待处理: ' + str(len(tks)) + '条') for t in tks: process(t) else: log('无待处理工单') log('进入主循环') log('=' * 40) while True: time.sleep(POLL) try: tks = get_tickets() new = [t for t in tks if t.get('new')] if new: log('U0001f195 新工单 ' + str(len(new)) + '条') for t in new: process(t) except KeyboardInterrupt: log('停止'); break except Exception as e: log('⚠ ' + str(e))