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