guanghulab/scripts/qa-bridge-agent.py
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

361 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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