361 lines
14 KiB
Python
361 lines
14 KiB
Python
#!/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.sh(4步: 环境检查→脑同步→看门人启动→密钥显示)
|
||
• 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 协议: trigger→emergence→lock + 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()
|