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))
|