130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
#!/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))
|