guanghulab/server/zhuyuan-agent.py

130 lines
4.7 KiB
Python
Raw Normal View History

#!/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))