#!/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()