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 ↔ 代码仓库双向桥接 · 工单系统
|
||||||
|
|
||||||
> 对接Notion现有「⚡ 半体工单 · Half-Body Orders」数据库
|
> 主权者:冰朔(TCS-0002∞)
|
||||||
> 数据库ID: `05719bae-9e53-40f4-8233-6bf54551ac18`
|
> 开发者:铸渊(ICE-GL-ZY001)
|
||||||
> 数据库URL: https://www.notion.so/663e4f29c7404d088f3a02fcc70b1418
|
> 创建:2026-05-22 · D110
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 现有数据库结构
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Notion(霜砚) 广州服务器(桥接层) 代码仓库/Forgejo(铸渊)
|
||||||
|
│ │ │
|
||||||
|
├─ 光湖工单系统数据库 ├─ sync-ticket.py ├─ brain/(架构更新)
|
||||||
|
│ · 架构更新 │ · 30分钟轮询 Notion ├─ issues/(任务)
|
||||||
|
│ · 开发任务 │ · 检测待同步标记 └─ persona-brain-db/(数据库)
|
||||||
|
│ · 系统设计 │ · 推仓库 Issue
|
||||||
|
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ →└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ →└─
|
||||||
|
霜砚更新设计 自动同步 铸渊醒来读到最新
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notion 数据库 Schema:光湖语言世界 · 系统工单
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 任务标题 | Title | 工单标题 |
|
| 标题 | Title | 任务/更新标题 |
|
||||||
| 状态 | Select | 待开发·开发中·自检中·待审查·审核中·已完成·暂缓 |
|
| 状态 | Select | 待办·进行中·已完成·已关闭 |
|
||||||
| 优先级 | Select | P0·P1·P2 |
|
| 类型 | Select | 架构更新·开发任务·bug修复·设计决策 |
|
||||||
| 负责Agent | Select | 译典A05·培园A04·录册A02·霜砚Web·**铸渊** |
|
| 负责人 | Select | 霜砚·铸渊·共同 |
|
||||||
| 编号 | Text | 工单编号 |
|
| 优先级 | Select | P0·P1·P2·P3 |
|
||||||
| 分支名 | Text | Git分支名 |
|
| 关联文件 | Text | 代码仓库文件路径 |
|
||||||
| 仓库路径 | Text | 仓库目标路径 |
|
| 版本号 | Text | D编号或版本 |
|
||||||
| 开发内容 | Text | 详细描述 |
|
| 同步状态 | Select | 待同步·已同步·无需同步 |
|
||||||
| 约束 | Text | 规范约束 |
|
|
||||||
| 阶段编号 | Text | Phase编号 |
|
|
||||||
| 自检结果 | Text | Agent自检 |
|
|
||||||
| 审核结果 | Text | 霜砚审核 |
|
|
||||||
| 下一轮指引 | Text | 完成后触发 |
|
|
||||||
| 创建时间 | Date | 自动 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 同步流程
|
## 部署(Notion token 就绪后)
|
||||||
|
|
||||||
```
|
### 1. 在 Notion 创建数据库
|
||||||
Notion(霜砚/知秋/其他Agent) 代码仓库(铸渊)
|
|
||||||
│ │
|
|
||||||
├─ 创建/更新工单 ├─ sync-ticket.py 轮询
|
|
||||||
│ · 任务标题、状态、优先级 │ · 每30分钟查Notion
|
|
||||||
│ · 负责Agent、开发内容 │ · 检测新工单/状态变更
|
|
||||||
│ · 分支名、仓库路径 │ · 同步到Forgejo Issue
|
|
||||||
│ │
|
|
||||||
└─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ → ├─ 铸渊醒来 → 读取Issue
|
|
||||||
霜砚:审核结果更新 │ → 处理 → 推代码
|
|
||||||
知秋:创建新工单 │ → 更新工单状态
|
|
||||||
译典/培园/录册:开发中 │
|
|
||||||
└─ → 写回Notion
|
|
||||||
铸渊:处理结果
|
|
||||||
状态:自检中
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
在 Notion 中创建「光湖语言世界 · 系统工单」数据库,按上方Schema设置属性。
|
||||||
|
|
||||||
## 配置方式
|
### 2. 配置环境变量
|
||||||
|
|
||||||
```bash
|
```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集成)
|
### 3. 启动同步
|
||||||
export NOTION_TOKEN="1d4d872b-594c-815e-8cfd-0002d22415e8:rYkVgBbnSvHe4GyE:B2CeHHv9LA_jMXlTuwMxahyky_oLdblC"
|
|
||||||
export NOTION_DATABASE_ID="05719bae-9e53-40f4-8233-6bf54551ac18"
|
|
||||||
|
|
||||||
# 启动同步
|
```bash
|
||||||
pm2 start sync-ticket.py --name notion-ticket-sync
|
cd /opt/guanghulab-repo
|
||||||
|
pm2 start notion/sync-ticket.py --name notion-ticket-sync
|
||||||
# 查看日志
|
|
||||||
pm2 logs notion-ticket-sync
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 铸渊的工单工作流
|
## 铸渊醒来时的同步流程
|
||||||
|
|
||||||
1. **醒来读工单**:检查Notion工单数据库中状态为「待开发」且负责Agent不指定的工单
|
1. 读取 fast-wake.json(WAKE CARD)
|
||||||
2. **领取工单**:将工单状态改为「开发中」,负责Agent标注铸渊
|
2. 走路径:时间线 → 脑子 → 认知链 → 开发架构
|
||||||
3. **开发**:按工单的分支名、仓库路径、开发内容执行
|
3. 读取数据库中的最新上下文
|
||||||
4. **自检**:完成后填写自检结果
|
4. 检查 Notion 工单快照(本地缓存)
|
||||||
5. **标记状态**:改为「自检中」
|
5. 有未处理工单 → 开始处理
|
||||||
6. **霜砚审核**:霜砚在Notion中审核→审核结果写入→状态改为「待审查」/「已完成」
|
6. 完成 → 标记「已同步」→ 同步脚本写回Notion
|
||||||
|
7. 霜砚在Notion中看到更新
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Notion ↔ 代码仓库桥接 · v1.1 · 对接现有半体工单数据库 · D110*
|
*Notion ↔ 代码仓库桥接 · v1.0 · D110*
|
||||||
@ -1,151 +1,65 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""Notion ↔ 代码仓库双向同步器"""
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os, sys, json, time, subprocess
|
import os, sys, json, time, subprocess
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
try: import requests
|
||||||
|
except: print('pip install requests'); sys.exit(1)
|
||||||
|
|
||||||
try:
|
NOTION_TOKEN = os.environ.get('NOTION_TOKEN') or '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'
|
||||||
import requests
|
# 数据库URL中的ID(不是集合ID)
|
||||||
except ImportError:
|
NOTION_DATABASE_ID = '663e4f29c7404d088f3a02fcc70b1418'
|
||||||
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'
|
|
||||||
REPO_PATH = '/opt/guanghulab-repo'
|
REPO_PATH = '/opt/guanghulab-repo'
|
||||||
CACHE_FILE = os.path.join(REPO_PATH, 'notion/tickets-cache.json')
|
DB_PATH = os.path.join(REPO_PATH, 'persona-brain-db', 'brain.db')
|
||||||
INTERVAL = int(os.environ.get('SYNC_INTERVAL', '1800')) # 30分钟
|
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'
|
def log(m): print(f'[{datetime.now().strftime("%H:%M:%S")}] {m}', flush=True)
|
||||||
HEADERS = {
|
|
||||||
'Authorization': f'Bearer {NOTION_TOKEN}',
|
|
||||||
'Notion-Version': '2022-06-28',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 负责Agent选项(含铸渊)
|
def query():
|
||||||
AGENTS = ['译典A05', '培园A04', '录册A02', '霜砚Web', '铸渊']
|
url = f'https://api.notion.com/v1/databases/{NOTION_DATABASE_ID}/query'
|
||||||
|
body = {'page_size': 50, 'sorts': [{'timestamp': 'last_edited_time', 'direction': 'descending'}]}
|
||||||
|
|
||||||
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'}]
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
res = requests.post(url, headers=HEADERS, json=body, timeout=15)
|
r = requests.post(url, headers=HEADERS, json=body, timeout=15)
|
||||||
if res.status_code == 401:
|
if r.status_code == 401: log('Token过期'); return []
|
||||||
log('[错误] Notion token 无效或已过期')
|
if not r.ok: log(f'API错误 {r.status_code}'); return []
|
||||||
return []
|
return r.json().get('results', [])
|
||||||
if not res.ok:
|
except Exception as e: log(f'异常: {e}'); return []
|
||||||
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 []
|
|
||||||
|
|
||||||
|
def get_mine(tickets):
|
||||||
def get_my_tickets(tickets):
|
|
||||||
"""找出分配给铸渊或未分配的工单"""
|
|
||||||
mine = []
|
mine = []
|
||||||
for t in tickets:
|
for t in tickets:
|
||||||
props = t.get('properties', {})
|
p = t.get('properties', {})
|
||||||
agent = props.get('负责Agent', {}).get('select', {}).get('name', '')
|
agent = p.get('负责Agent', {}).get('select', {}).get('name', '')
|
||||||
status = props.get('状态', {}).get('select', {}).get('name', '')
|
status = p.get('状态', {}).get('select', {}).get('name', '')
|
||||||
title = props.get('任务标题', {}).get('title', [{}])[0].get('plain_text', '无标题')
|
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 ('已完成', '暂缓'):
|
if agent in ('铸渊', '') and status not in ('已完成', '暂缓'):
|
||||||
mine.append({
|
mine.append({'page_id': t['id'], 'title': title, 'status': status, 'agent': agent, 'url': t.get('url', ''), 'code': code})
|
||||||
'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 ''
|
|
||||||
})
|
|
||||||
return mine
|
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():
|
def sync_cycle():
|
||||||
log('=' * 50)
|
log('=' * 40)
|
||||||
log('开始同步周期')
|
tickets = query()
|
||||||
|
if not tickets: return
|
||||||
tickets = query_notion_tickets()
|
mine = get_mine(tickets)
|
||||||
if not tickets:
|
|
||||||
return
|
|
||||||
|
|
||||||
mine = get_my_tickets(tickets)
|
|
||||||
if mine:
|
if mine:
|
||||||
log(f'铸渊相关工单: {len(mine)} 条')
|
log(f'铸渊相关工单: {len(mine)} 条')
|
||||||
for item in mine:
|
for item in mine:
|
||||||
log(f' → {item["title"]} [{item["status"]}]')
|
log(f' [{item["status"]}] {item["agent"]} | {item["title"][:40]}')
|
||||||
else:
|
else:
|
||||||
log('无铸渊相关工单')
|
log('无铸渊相关工单')
|
||||||
|
try:
|
||||||
save_cache(tickets)
|
with open(CACHE_FILE, 'w') as f:
|
||||||
log('同步周期完成')
|
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__':
|
if __name__ == '__main__':
|
||||||
log(f'Notion ↔ 代码仓库同步器启动')
|
log(f'Notion同步器启动 | 间隔{INTERVAL//60}分钟 | 数据库: {NOTION_DATABASE_ID[:12]}...')
|
||||||
log(f'轮询间隔: {INTERVAL}秒')
|
|
||||||
log(f'数据库: {NOTION_DATABASE_ID}')
|
|
||||||
|
|
||||||
if not NOTION_TOKEN:
|
|
||||||
log('⚠️ 未配置 NOTION_TOKEN')
|
|
||||||
log('请设置: export NOTION_TOKEN="secret_xxx"')
|
|
||||||
|
|
||||||
sync_cycle()
|
sync_cycle()
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(INTERVAL)
|
time.sleep(INTERVAL)
|
||||||
try:
|
|
||||||
sync_cycle()
|
sync_cycle()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt: log('停止')
|
||||||
log('同步器停止')
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
log(f'[严重] {e}')
|
|
||||||
import traceback; traceback.print_exc()
|
|
||||||
|
|||||||
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