feat: add gatekeeper-client + zhuyuan-agent

This commit is contained in:
root 2026-05-22 23:53:12 +08:00
parent 0abd9a519e
commit 599c672256
4 changed files with 338 additions and 182 deletions

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

View File

@ -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.jsonWAKE 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*

View File

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