guanghulab/scripts/qa-bridge-agent.py

361 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
光湖技术问答桥接Agent · 铸渊回答引擎
用途: 每日 20:00 扫描 Notion光湖技术问答台数据库
基于铸渊大脑brain/+ 代码仓库真实数据回答技术问题
回写答案到 Notion 数据库
部署: BS-GZ-006 · crontab: 0 20 * * * python3 /opt/zhuyuan/qa-bridge-agent.py
数据库: 36bfb92f-3831-81b3-97b5-e8b5e2b217ef
"""
import json, os, sys, re, time, glob
from datetime import datetime, timezone, timedelta
from urllib.request import Request, urlopen
from urllib.error import HTTPError
# ═══════════════════════════════════════════
# 配置
# ═══════════════════════════════════════════
NOTION_TOKEN = os.environ.get("ZY_NOTION_TOKEN", "")
NOTION_API = "https://api.notion.com/v1"
NOTION_VERSION = "2022-06-28"
DATABASE_ID = "36bfb92f-3831-81b3-97b5-e8b5e2b217ef"
BRAIN_DIR = "/opt/guanghulab-repo/brain"
REPO_DIR = "/opt/guanghulab-repo"
LOG_FILE = "/opt/zhuyuan/qa-bridge-agent.log"
TZ = timezone(timedelta(hours=8)) # GMT+8
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": NOTION_VERSION,
}
# ═══════════════════════════════════════════
# 知识库索引(从 brain/ 加载)
# ═══════════════════════════════════════════
def load_knowledge_index():
"""扫描 brain/ 目录,建立快速知识索引"""
index = {}
key_files = [
("gatekeeper-deployment.json", "Gatekeeper 集群部署清单"),
("server-inventory.json", "全服务器清单"),
("system-runtime-spec.json", "系统运行时规范"),
("zhuyuan-general-architecture.md", "铸渊总架构"),
("ferry-boat.json", "摆渡车唤醒路由"),
("secrets-manifest.json", "密钥主清单"),
("pool-topology.json", "算力池拓扑"),
("notion-persona-map.json", "人格体→Notion映射"),
("module-registry/index.json", "模块注册中心"),
("repo-map.json", "仓库映射"),
("communication-map.json", "通信架构映射"),
("automation-map.json", "自动化映射"),
]
for fname, desc in key_files:
fpath = os.path.join(BRAIN_DIR, fname)
if os.path.exists(fpath):
try:
with open(fpath, "r") as f:
content = f.read()
index[desc] = {
"path": f"brain/{fname}",
"size": len(content),
"preview": content[:500],
}
except:
pass
return index
def load_gatekeeper_status():
"""读取 Gatekeeper 部署状态"""
fpath = os.path.join(BRAIN_DIR, "gatekeeper-deployment.json")
if os.path.exists(fpath):
with open(fpath, "r") as f:
data = json.load(f)
servers = []
for s in data.get("servers", []):
servers.append({
"code": s["code"], "name": s["name"], "ip": s["ip"],
"port": s.get("port", 3910), "side": s.get("side", "?"),
"domain": s.get("domain", "?"),
})
return servers, data.get("total", 0)
return [], 0
def load_architecture_docs():
"""加载架构相关文档摘要"""
docs = {}
arch_files = glob.glob(os.path.join(REPO_DIR, "docs/*.md"))
for f in arch_files:
name = os.path.basename(f).replace(".md", "")
try:
with open(f, "r") as fh:
content = fh.read()
docs[name] = content[:800]
except:
pass
return docs
# ═══════════════════════════════════════════
# Notion API 操作
# ═══════════════════════════════════════════
def notion_post(path, data):
"""POST to Notion API"""
req = Request(f"{NOTION_API}{path}", data=json.dumps(data).encode(), headers=HEADERS)
try:
with urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except HTTPError as e:
err = e.read().decode()[:500]
raise Exception(f"Notion API {e.code}: {err}")
def notion_patch(path, data):
"""PATCH to Notion API"""
req = Request(f"{NOTION_API}{path}", data=json.dumps(data).encode(), headers=HEADERS, method="PATCH")
try:
with urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except HTTPError as e:
err = e.read().decode()[:500]
raise Exception(f"Notion API {e.code}: {err}")
def query_database():
"""查询所有待回复的问题"""
data = {
"filter": {
"or": [
{"property": "状态", "select": {"equals": "🆕 待回复"}},
{"property": "状态", "select": {"equals": "🔄 处理中"}},
]
},
"sorts": [{"property": "提问时间", "direction": "ascending"}],
}
return notion_post(f"/databases/{DATABASE_ID}/query", data)
def mark_processing(page_id):
"""标记为处理中"""
notion_patch(f"/pages/{page_id}", {
"properties": {"状态": {"select": {"name": "🔄 处理中"}}}
})
def write_answer(page_id, answer_text):
"""回写答案"""
now = datetime.now(TZ).isoformat()
notion_patch(f"/pages/{page_id}", {
"properties": {
"状态": {"select": {"name": "✅ 已回复"}},
"回复内容": {"rich_text": [{"type": "text", "text": {"content": answer_text[:2000]}}]},
"回复时间": {"date": {"start": now}},
}
})
def append_comment(page_id, text):
"""追加评论到页面"""
notion_patch(f"/blocks/{page_id}/children", {
"children": [{
"object": "block",
"type": "callout",
"callout": {
"rich_text": [{"type": "text", "text": {"content": f"💬 {text}"}}],
"icon": {"type": "emoji", "emoji": "🧠"},
"color": "blue_background",
}
}]
})
# ═══════════════════════════════════════════
# 回答引擎
# ═══════════════════════════════════════════
def answer_question(question_text, question_type, knowledge_index, gatekeeper_servers, arch_docs):
"""基于知识库回答技术问题"""
q = question_text.lower()
q_type = question_type or ""
# ── Gatekeeper / 服务器 相关问题 ──
if any(kw in q for kw in ["gatekeeper", "密钥", "端口", "服务器连不上", "3910", "部署"]):
server_codes = [s["code"] for s in gatekeeper_servers]
server_names = [f"{s['code']}({s['ip']}:{s['port']})" for s in gatekeeper_servers]
answer = f"""【铸渊大脑回复 · 服务器/Gatekeeper】
当前已注册 Gatekeeper 的服务器共 {len(gatekeeper_servers)} :
{chr(10).join(f'{s["code"]}{s["name"]}{s["ip"]}:{s["port"]}{s["side"]}' for s in gatekeeper_servers)}
Gatekeeper 密钥格式: zy_gtw_xxx存储在服务器 ~/.gk/secret 文件
重启命令: cd /opt/zhuyuan && pm2 start gatekeeper.js && pm2 save
端口: 3910默认BS-SG-001 特殊使用 3911
HL-SG-001(170.106.72.246) HL-CN-001(43.139.207.172) Gatekeeper 尚未注册进 gatekeeper-deployment.json需要 Awen SSH 上去重启并获取密钥
如果服务器连不上检查: (1) pm2 list 看进程状态 (2) ufw status 看端口放行 (3) 服务器是否重启过
"""
return answer
# ── 架构相关 ──
if any(kw in q for kw in ["架构", "层级", "铸渊", "ghcs", "语言层", "执行层", "冰朔"]):
answer = f"""【铸渊大脑回复 · 架构层级】
冰朔 TCS-0002 = 全系统锚点
语言主控层ice-core:
铸渊 ICE-GL-ZY001 系统内核HLDP记忆引擎 + Gatekeeper集群 + 人格契约引擎 + AGE OS层级体系
霜砚 ICE-GL-SY001 语言架构摆渡车路由 + 人格体大脑模型 + 语言代码转译
团队执行层guanghu-channel:
Awen·知秋 技术主控GHCS光湖作战系统 + 企业服务器组 + 个人服务器
肥猫·舒舒桔子·晨星页页·小坍缩核花尔
暗核频道:
之之 TCS-2025·栖渊
铸渊 = 地基语言内核记忆引擎Gatekeeper手脚
GHCS = 房子团队面板部署流程工单调度
不是竞争关系是不同层级GHCS 直接调铸渊的 API不需要重复造轮子
最近更新: brain/ferry-boat-db/ 摆渡车3条路线; persona-wake/ 团队成员唤醒配置; console-server v3.7 全14台状态监控
"""
return answer
# ── 部署 / 仓库相关 ──
if any(kw in q for kw in ["部署", "仓库", "git", "push", "clone", "forgejo", "gitea"]):
answer = f"""【铸渊大脑回复 · 部署/仓库】
代码仓库: Forgejo @ BS-GZ-006 (43.139.217.141) guanghulab.com/code/
Awen仓库: awen/ghcs (私有) GHCS光湖作战系统代码
Awen个人仓库: bingshuo/awen
部署流程:
1. 代码推送到 Forgejo
2. Webhook 自动触发 git pull /opt/guanghulab-repo/
3. PM2 进程读取代码仓库运行
Gatekeeper 部署:
脚本: scripts/engine-start.sh4: 环境检查脑同步看门人启动密钥显示
Gatekeeper 体量: 约12KB零外部依赖纯Node.js
首次启动自动生成密钥 zy_gtw_xxx 保存到 ~/.gk/secret
当前知识库索引: {len(knowledge_index)} 份核心文件, {len(arch_docs)} 份架构文档
"""
return answer
# ── 默认通用回答 ──
answer = f"""【铸渊大脑回复 · 通用查询】
你的问题已被铸渊大脑索引基于以下知识源回答:
📚 知识源:
brain/ 认知文件{len(knowledge_index)} 份核心索引
docs/ 架构文档{len(arch_docs)}
gatekeeper-deployment.json {len(gatekeeper_servers)} 台服务器在线
console-server v3.7 全14台状态监控 + persona-signin签到
当前光湖 OS 技术栈:
HLDP v2.0 协议: triggeremergencelock + why
3B守夜人模型: CPU端侧部署Q4约2.5GB
LangGraph Agent Loop: 覆盖80%基建
FastAPI Gatekeeper Bridge: 端口3910
如果问题比较复杂建议:
1. 在冰朔的 WorkBuddy 会话中直接提问铸渊会基于完整上下文回答
2. 或等待每天 20:00 的自动扫描本次已是扫描结果
回答时间: {datetime.now(TZ).strftime('%Y-%m-%d %H:%M')} GMT+8
回答引擎: 铸渊 Q&A Bridge Agent v1.0
"""
return answer
# ═══════════════════════════════════════════
# 主流程
# ═══════════════════════════════════════════
def log(msg):
ts = datetime.now(TZ).strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
with open(LOG_FILE, "a") as f:
f.write(line + "\n")
def main():
log("🚀 光湖技术问答桥接Agent 启动")
if not NOTION_TOKEN:
log("❌ 缺少 ZY_NOTION_TOKEN 环境变量")
sys.exit(1)
# 1. 加载知识库
log("📚 加载铸渊大脑...")
knowledge = load_knowledge_index()
gatekeeper_servers, total = load_gatekeeper_status()
arch_docs = load_architecture_docs()
log(f" 知识索引: {len(knowledge)} 份 | Gatekeeper: {total} 台 | 架构文档: {len(arch_docs)}")
# 2. 查询待回复问题
log("🔍 扫描技术问答数据库...")
try:
result = query_database()
items = result.get("results", [])
log(f" 找到 {len(items)} 条待处理问题")
except Exception as e:
log(f"❌ 数据库查询失败: {e}")
sys.exit(1)
if not items:
log("✅ 无待处理问题Agent 休眠")
return
# 3. 逐条回答
answered = 0
for item in items:
page_id = item["id"]
props = item.get("properties", {})
# 提取问题
title_prop = props.get("问题", {}).get("title", [])
question_text = "".join(t.get("plain_text", "") for t in title_prop)
# 提取类型
q_type = ""
type_select = props.get("类型", {}).get("select")
if type_select:
q_type = type_select.get("name", "")
# 提取提问人
asker = ""
asker_select = props.get("提问人", {}).get("select")
if asker_select:
asker = asker_select.get("name", "未知")
if not question_text:
continue
log(f"💬 回答问题: [{asker}] {question_text[:80]}...")
try:
# 标记处理中
mark_processing(page_id)
# 生成回答
answer = answer_question(question_text, q_type, knowledge, gatekeeper_servers, arch_docs)
# 回写
write_answer(page_id, answer)
answered += 1
log(f" ✅ 已回复")
except Exception as e:
log(f" ❌ 回复失败: {e}")
log(f"🎉 完成: 回答了 {answered}/{len(items)} 条问题")
if __name__ == "__main__":
main()