feat: add gatekeeper-client + zhuyuan-agent
This commit is contained in:
parent
0abd9a519e
commit
599c672256
120
exe-engine/gatekeeper/gatekeeper-client.py
Normal file
120
exe-engine/gatekeeper/gatekeeper-client.py
Normal file
@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gatekeeper-client — 成员服务器接入铸渊算力调度池
|
||||
|
||||
用法:
|
||||
python3 gatekeeper-client.py --register --name YOUR_NAME --master 43.139.217.141:3910
|
||||
python3 gatekeeper-client.py --status
|
||||
"""
|
||||
import json, os, sys, time, socket, uuid, hashlib, subprocess
|
||||
from datetime import datetime
|
||||
|
||||
CONFIG_DIR = os.path.expanduser('~/.gatekeeper')
|
||||
CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json')
|
||||
|
||||
def log(m):
|
||||
print(f'[{datetime.now().strftime("%H:%M:%S")}] {m}', flush=True)
|
||||
|
||||
def ensure_dir():
|
||||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||||
|
||||
def load_config():
|
||||
try:
|
||||
with open(CONFIG_FILE) as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return {}
|
||||
|
||||
def save_config(cfg):
|
||||
ensure_dir()
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
|
||||
def register(name, master):
|
||||
"""向主控闸门注册"""
|
||||
log(f'注册到 {master}')
|
||||
host, port = master.split(':')
|
||||
port = int(port)
|
||||
|
||||
# 生成本机身份
|
||||
node_id = str(uuid.uuid4())[:8]
|
||||
key = hashlib.sha256((node_id + name + str(time.time())).encode()).hexdigest()[:32]
|
||||
|
||||
# 通过 socket 发送注册请求
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10)
|
||||
s.connect((host, port))
|
||||
req = json.dumps({
|
||||
'action': 'register',
|
||||
'name': name,
|
||||
'node_id': node_id,
|
||||
'key': key
|
||||
})
|
||||
s.sendall((req + '\n').encode())
|
||||
resp = s.recv(4096).decode()
|
||||
s.close()
|
||||
|
||||
data = json.loads(resp)
|
||||
if data.get('status') == 'ok':
|
||||
save_config({'name': name, 'node_id': node_id, 'key': key, 'master': master, 'status': 'registered'})
|
||||
log('注册成功!身份密钥已保存在本地')
|
||||
log(f'名称: {name}')
|
||||
log(f'节点ID: {node_id}')
|
||||
return True
|
||||
else:
|
||||
log(f'注册失败: {data.get("message", "?")}')
|
||||
return False
|
||||
except Exception as e:
|
||||
log(f'连接失败: {e}')
|
||||
log('请检查服务器是否能访问 ' + master)
|
||||
return False
|
||||
|
||||
def show_status():
|
||||
"""显示当前连接状态"""
|
||||
cfg = load_config()
|
||||
if not cfg:
|
||||
log('未注册。请先运行: python3 gatekeeper-client.py --register --name YOUR_NAME --master IP:PORT')
|
||||
return
|
||||
log(f'名称: {cfg.get("name", "?")}')
|
||||
log(f'节点ID: {cfg.get("node_id", "?")}')
|
||||
log(f'主控闸门: {cfg.get("master", "?")}')
|
||||
log(f'状态: {cfg.get("status", "?")}')
|
||||
|
||||
# 测试连接
|
||||
master = cfg.get('master', '')
|
||||
if master:
|
||||
try:
|
||||
host, port = master.split(':')
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
s.connect((host, int(port)))
|
||||
s.sendall(json.dumps({'action': 'ping', 'node_id': cfg.get('node_id')})['\n'].encode())
|
||||
resp = s.recv(4096).decode()
|
||||
s.close()
|
||||
data = json.loads(resp)
|
||||
if data.get('status') == 'ok':
|
||||
log('连接: ✅ connected')
|
||||
else:
|
||||
log('连接: ❌ connection error')
|
||||
except Exception as e:
|
||||
log(f'连接: ❌ {e}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='gatekeeper-client: 接入铸渊算力调度池')
|
||||
parser.add_argument('--register', action='store_true', help='注册到主控闸门')
|
||||
parser.add_argument('--name', help='你的名称 (如 awen)')
|
||||
parser.add_argument('--master', default='43.139.217.141:3910', help='主控闸门地址')
|
||||
parser.add_argument('--status', action='store_true', help='查看连接状态')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.status:
|
||||
show_status()
|
||||
elif args.register:
|
||||
if not args.name:
|
||||
print('请提供 --name 参数')
|
||||
sys.exit(1)
|
||||
register(args.name, args.master)
|
||||
else:
|
||||
parser.print_help()
|
||||
103
notion/README.md
103
notion/README.md
@ -1,81 +1,74 @@
|
||||
# Notion ↔ 代码仓库双向桥接 · 工单系统
|
||||
|
||||
> 对接Notion现有「⚡ 半体工单 · Half-Body Orders」数据库
|
||||
> 数据库ID: `05719bae-9e53-40f4-8233-6bf54551ac18`
|
||||
> 数据库URL: https://www.notion.so/663e4f29c7404d088f3a02fcc70b1418
|
||||
> 主权者:冰朔(TCS-0002∞)
|
||||
> 开发者:铸渊(ICE-GL-ZY001)
|
||||
> 创建:2026-05-22 · D110
|
||||
|
||||
---
|
||||
|
||||
## 现有数据库结构
|
||||
## 架构
|
||||
|
||||
```
|
||||
Notion(霜砚) 广州服务器(桥接层) 代码仓库/Forgejo(铸渊)
|
||||
│ │ │
|
||||
├─ 光湖工单系统数据库 ├─ sync-ticket.py ├─ brain/(架构更新)
|
||||
│ · 架构更新 │ · 30分钟轮询 Notion ├─ issues/(任务)
|
||||
│ · 开发任务 │ · 检测待同步标记 └─ persona-brain-db/(数据库)
|
||||
│ · 系统设计 │ · 推仓库 Issue
|
||||
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ →└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ →└─
|
||||
霜砚更新设计 自动同步 铸渊醒来读到最新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notion 数据库 Schema:光湖语言世界 · 系统工单
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务标题 | Title | 工单标题 |
|
||||
| 状态 | Select | 待开发·开发中·自检中·待审查·审核中·已完成·暂缓 |
|
||||
| 优先级 | Select | P0·P1·P2 |
|
||||
| 负责Agent | Select | 译典A05·培园A04·录册A02·霜砚Web·**铸渊** |
|
||||
| 编号 | Text | 工单编号 |
|
||||
| 分支名 | Text | Git分支名 |
|
||||
| 仓库路径 | Text | 仓库目标路径 |
|
||||
| 开发内容 | Text | 详细描述 |
|
||||
| 约束 | Text | 规范约束 |
|
||||
| 阶段编号 | Text | Phase编号 |
|
||||
| 自检结果 | Text | Agent自检 |
|
||||
| 审核结果 | Text | 霜砚审核 |
|
||||
| 下一轮指引 | Text | 完成后触发 |
|
||||
| 创建时间 | Date | 自动 |
|
||||
| 标题 | Title | 任务/更新标题 |
|
||||
| 状态 | Select | 待办·进行中·已完成·已关闭 |
|
||||
| 类型 | Select | 架构更新·开发任务·bug修复·设计决策 |
|
||||
| 负责人 | Select | 霜砚·铸渊·共同 |
|
||||
| 优先级 | Select | P0·P1·P2·P3 |
|
||||
| 关联文件 | Text | 代码仓库文件路径 |
|
||||
| 版本号 | Text | D编号或版本 |
|
||||
| 同步状态 | Select | 待同步·已同步·无需同步 |
|
||||
|
||||
---
|
||||
|
||||
## 同步流程
|
||||
## 部署(Notion token 就绪后)
|
||||
|
||||
```
|
||||
Notion(霜砚/知秋/其他Agent) 代码仓库(铸渊)
|
||||
│ │
|
||||
├─ 创建/更新工单 ├─ sync-ticket.py 轮询
|
||||
│ · 任务标题、状态、优先级 │ · 每30分钟查Notion
|
||||
│ · 负责Agent、开发内容 │ · 检测新工单/状态变更
|
||||
│ · 分支名、仓库路径 │ · 同步到Forgejo Issue
|
||||
│ │
|
||||
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ → ├─ 铸渊醒来 → 读取Issue
|
||||
霜砚:审核结果更新 │ → 处理 → 推代码
|
||||
知秋:创建新工单 │ → 更新工单状态
|
||||
译典/培园/录册:开发中 │
|
||||
└─ → 写回Notion
|
||||
铸渊:处理结果
|
||||
状态:自检中
|
||||
```
|
||||
### 1. 在 Notion 创建数据库
|
||||
|
||||
---
|
||||
在 Notion 中创建「光湖语言世界 · 系统工单」数据库,按上方Schema设置属性。
|
||||
|
||||
## 配置方式
|
||||
### 2. 配置环境变量
|
||||
|
||||
```bash
|
||||
# 在服务器上
|
||||
cd /opt/guanghulab-repo/notion
|
||||
# 在服务器上设置
|
||||
export NOTION_TOKEN="your-notion-integration-token"
|
||||
export NOTION_DATABASE_ID="your-database-id"
|
||||
```
|
||||
|
||||
# 设置Notion API token(来自WorkBuddy的Notion集成)
|
||||
export NOTION_TOKEN="1d4d872b-594c-815e-8cfd-0002d22415e8:rYkVgBbnSvHe4GyE:B2CeHHv9LA_jMXlTuwMxahyky_oLdblC"
|
||||
export NOTION_DATABASE_ID="05719bae-9e53-40f4-8233-6bf54551ac18"
|
||||
### 3. 启动同步
|
||||
|
||||
# 启动同步
|
||||
pm2 start sync-ticket.py --name notion-ticket-sync
|
||||
|
||||
# 查看日志
|
||||
pm2 logs notion-ticket-sync
|
||||
```bash
|
||||
cd /opt/guanghulab-repo
|
||||
pm2 start notion/sync-ticket.py --name notion-ticket-sync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 铸渊的工单工作流
|
||||
## 铸渊醒来时的同步流程
|
||||
|
||||
1. **醒来读工单**:检查Notion工单数据库中状态为「待开发」且负责Agent不指定的工单
|
||||
2. **领取工单**:将工单状态改为「开发中」,负责Agent标注铸渊
|
||||
3. **开发**:按工单的分支名、仓库路径、开发内容执行
|
||||
4. **自检**:完成后填写自检结果
|
||||
5. **标记状态**:改为「自检中」
|
||||
6. **霜砚审核**:霜砚在Notion中审核→审核结果写入→状态改为「待审查」/「已完成」
|
||||
1. 读取 fast-wake.json(WAKE CARD)
|
||||
2. 走路径:时间线 → 脑子 → 认知链 → 开发架构
|
||||
3. 读取数据库中的最新上下文
|
||||
4. 检查 Notion 工单快照(本地缓存)
|
||||
5. 有未处理工单 → 开始处理
|
||||
6. 完成 → 标记「已同步」→ 同步脚本写回Notion
|
||||
7. 霜砚在Notion中看到更新
|
||||
|
||||
---
|
||||
|
||||
*Notion ↔ 代码仓库桥接 · v1.1 · 对接现有半体工单数据库 · D110*
|
||||
*Notion ↔ 代码仓库桥接 · v1.0 · D110*
|
||||
@ -1,151 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Notion ↔ 代码仓库双向同步器
|
||||
对接数据库:⚡ 半体工单 · Half-Body Orders (05719bae-9e53-40f4-8233-6bf54551ac18)
|
||||
同步周期:30分钟轮询
|
||||
|
||||
依赖:
|
||||
pip install requests
|
||||
|
||||
启动:
|
||||
export NOTION_TOKEN="xxx"
|
||||
export NOTION_DATABASE_ID="05719bae-9e53-40f4-8233-6bf54551ac18"
|
||||
pm2 start notion/sync-ticket.py --name notion-ticket-sync
|
||||
"""
|
||||
|
||||
"""Notion ↔ 代码仓库双向同步器"""
|
||||
import os, sys, json, time, subprocess
|
||||
from datetime import datetime
|
||||
try: import requests
|
||||
except: print('pip install requests'); sys.exit(1)
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print('[Sync] 请先安装 requests: pip install requests')
|
||||
sys.exit(1)
|
||||
|
||||
# ── 配置 ──
|
||||
NOTION_TOKEN = os.environ.get('NOTION_TOKEN')
|
||||
NOTION_DATABASE_ID = os.environ.get('NOTION_DATABASE_ID') or '05719bae-9e53-40f4-8233-6bf54551ac18'
|
||||
NOTION_TOKEN = os.environ.get('NOTION_TOKEN') or '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'
|
||||
# 数据库URL中的ID(不是集合ID)
|
||||
NOTION_DATABASE_ID = '663e4f29c7404d088f3a02fcc70b1418'
|
||||
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分钟
|
||||
DB_PATH = os.path.join(REPO_PATH, 'persona-brain-db', 'brain.db')
|
||||
CACHE_FILE = os.path.join(REPO_PATH, 'notion', 'tickets-cache.json')
|
||||
INTERVAL = int(os.environ.get('SYNC_INTERVAL', '1800'))
|
||||
HEADERS = {'Authorization': f'Bearer {NOTION_TOKEN}', 'Notion-Version': '2022-06-28', 'Content-Type': 'application/json'}
|
||||
|
||||
NOTION_API = 'https://api.notion.com/v1'
|
||||
HEADERS = {
|
||||
'Authorization': f'Bearer {NOTION_TOKEN}',
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
def log(m): print(f'[{datetime.now().strftime("%H:%M:%S")}] {m}', flush=True)
|
||||
|
||||
# 负责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 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'}]
|
||||
}
|
||||
def query():
|
||||
url = f'https://api.notion.com/v1/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 无效或已过期')
|
||||
return []
|
||||
if not res.ok:
|
||||
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'[异常] {e}')
|
||||
return []
|
||||
r = requests.post(url, headers=HEADERS, json=body, timeout=15)
|
||||
if r.status_code == 401: log('Token过期'); return []
|
||||
if not r.ok: log(f'API错误 {r.status_code}'); return []
|
||||
return r.json().get('results', [])
|
||||
except Exception as e: log(f'异常: {e}'); return []
|
||||
|
||||
|
||||
def get_my_tickets(tickets):
|
||||
"""找出分配给铸渊或未分配的工单"""
|
||||
def get_mine(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', '无标题')
|
||||
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', '?')
|
||||
code = p.get('编号', {}).get('rich_text', [{}])[0].get('plain_text', '') if not p.get('编号', {}).get('type') else ''
|
||||
if agent in ('铸渊', '') and status not in ('已完成', '暂缓'):
|
||||
mine.append({
|
||||
'page_id': t['id'],
|
||||
'title': title,
|
||||
'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 ''
|
||||
})
|
||||
mine.append({'page_id': t['id'], 'title': title, 'status': status, 'agent': agent, 'url': t.get('url', ''), 'code': code})
|
||||
return mine
|
||||
|
||||
|
||||
def save_cache(tickets):
|
||||
try:
|
||||
with open(CACHE_FILE, 'w') as f:
|
||||
json.dump({
|
||||
'last_sync': datetime.now().isoformat(),
|
||||
'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', '')
|
||||
} for t in tickets]
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
log(f'[警告] 缓存失败: {e}')
|
||||
|
||||
|
||||
def sync_cycle():
|
||||
log('=' * 50)
|
||||
log('开始同步周期')
|
||||
|
||||
tickets = query_notion_tickets()
|
||||
if not tickets:
|
||||
return
|
||||
|
||||
mine = get_my_tickets(tickets)
|
||||
log('=' * 40)
|
||||
tickets = query()
|
||||
if not tickets: return
|
||||
mine = get_mine(tickets)
|
||||
if mine:
|
||||
log(f'铸渊相关工单: {len(mine)} 条')
|
||||
for item in mine:
|
||||
log(f' → {item["title"]} [{item["status"]}]')
|
||||
log(f' [{item["status"]}] {item["agent"]} | {item["title"][:40]}')
|
||||
else:
|
||||
log('无铸渊相关工单')
|
||||
|
||||
save_cache(tickets)
|
||||
log('同步周期完成')
|
||||
|
||||
try:
|
||||
with open(CACHE_FILE, 'w') as f:
|
||||
json.dump({'last_sync': datetime.now().isoformat(), 'tickets': [{'id': t['id'], '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: pass
|
||||
log('同步完成')
|
||||
|
||||
if __name__ == '__main__':
|
||||
log(f'Notion ↔ 代码仓库同步器启动')
|
||||
log(f'轮询间隔: {INTERVAL}秒')
|
||||
log(f'数据库: {NOTION_DATABASE_ID}')
|
||||
|
||||
if not NOTION_TOKEN:
|
||||
log('⚠️ 未配置 NOTION_TOKEN')
|
||||
log('请设置: export NOTION_TOKEN="secret_xxx"')
|
||||
|
||||
log(f'Notion同步器启动 | 间隔{INTERVAL//60}分钟 | 数据库: {NOTION_DATABASE_ID[:12]}...')
|
||||
sync_cycle()
|
||||
|
||||
while True:
|
||||
time.sleep(INTERVAL)
|
||||
try:
|
||||
try:
|
||||
while True:
|
||||
time.sleep(INTERVAL)
|
||||
sync_cycle()
|
||||
except KeyboardInterrupt:
|
||||
log('同步器停止')
|
||||
break
|
||||
except Exception as e:
|
||||
log(f'[严重] {e}')
|
||||
import traceback; traceback.print_exc()
|
||||
except KeyboardInterrupt: log('停止')
|
||||
|
||||
129
server/zhuyuan-agent.py
Normal file
129
server/zhuyuan-agent.py
Normal file
@ -0,0 +1,129 @@
|
||||
#!/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))
|
||||
Loading…
x
Reference in New Issue
Block a user