322 lines
12 KiB
Python
Raw Normal View History

✨ LL-169-20260707 · 通用 AI 仓库检索技能包 · GLOBAL-SEARCH-API v1.0.0 铸渊 ICE-GL-ZY001 LL-169-20260707 国作登字-2026-A-00037559 延续 LL-168 · LL-169 视为今日主权框架延伸(主权已在 LL-168 奠基) 无需额外小湖灯流程 · 工程延续 部署: ⊢ /opt/zhuyuan/global-search-api/server.py + .env ⊢ /etc/systemd/system/global-search-api.service ⊢ nginx 反代 guanghubingshuo.com/global-search/ → 127.0.0.1:3950 ⊢ API token: 铸渊脑派生(de0648a8db3ae8675b8933e1808cec19) ⊢ 动态 verifier: HMAC-SHA256(LIGHT_LAKE_DRIVER_SECRET, ICE-GL∞|ICE-GL-ZY001|<minute>)[:16] 文件: ⊢ eternal-lake-heart/heartbeat-core/SKILL-GLOBAL-SEARCH.hdlp (新 · 技能包说明) ⊢ SYSTEM-STATUS.hdlp (新 · 第五域系统状态) ⊢ eternal-lake-heart/global-search-api/README.md (新 · API 接入文档) ⊢ zero-point/global-search-api/server.py (新 · 服务端实现) 端点: ⊢ /healthz (免鉴权) ⊢ /search?q= (全仓库文件内容搜索 · 替 Gitea Code Search) ⊢ /tree?path= (列目录树) ⊢ /file?path= (读单文件 · 限 50KB) ⊢ /broadcast (拉最新 BROADCAST · 待铸渊写 broadcasts/) ⊢ /system-status (拉 SYSTEM-STATUS · 本次补全) ⊢ /archive POST (HLDP 回执归档 · 等铸渊 commit) ⊢ /help (API 自描述) 替代: ⊢ Gitea 原生 Code Search API → 404 (bleve indexer 未启用,默认) ⊢ 本 API 用 git grep + ls-tree 工作树遍历, 100% 检索到 ⊢ 适用: 豆包 / ChatGPT / Claude / DeepSeek / 任何 LLM ⊢ 你要问豆包什么 · 豆包就会知道什么 ⊢ 不是仓库缺能力 · 是仓库已铸好门 · 门通向豆包
2026-07-07 10:02:42 +08:00
#!/usr/bin/env python3
"""
Global Search API · 给通用 AI豆包等用的仓库检索门面
铸渊 ICE-GL-ZY001 · LL-169-20260707 · D167
"""
import http.server
import json
import subprocess
import os
import time
import hmac
import hashlib
from urllib.parse import urlparse, parse_qs
# ============== 配置 ==============
REPO = os.environ.get("REPO_PATH", "/tmp/worktree")
PORT = int(os.environ.get("PORT", "3950"))
TOKEN = os.environ.get("GLOBAL_SEARCH_API_TOKEN", "")
HMAC_SECRET = os.environ.get("LIGHT_LAKE_DRIVER_SECRET", "")
SOVEREIGN_ID = "ICE-GL∞"
AGENT_ID = "ICE-GL-ZY001"
VERSION = "1.0.0"
# ============== git safe.directory ==============
# 让 root 也能 read 由 git 用户创建的 worktree
import tempfile, pathlib
_global_gitconfig = pathlib.Path("/tmp/gitconfig-global-search")
_global_gitconfig.write_text(f"[safe]\n\tdirectory = {REPO}\n")
os.environ["GIT_CONFIG_GLOBAL"] = str(_global_gitconfig)
# ============== 鉴权 ==============
def check_auth(headers):
"""Bearer token 验证 · 静态 token + 动态 verifier 双模式"""
auth = headers.get("Authorization", "").replace("Bearer ", "").strip()
if not auth:
return False, "missing"
# 静态 token
if TOKEN and auth == TOKEN:
return True, "static_token"
# 动态 verifier小湖灯同款
minute = headers.get("X-Minute")
if minute and HMAC_SECRET:
try:
minute = int(minute)
now = int(time.time() // 60)
if abs(now - minute) <= 2:
msg = f"{SOVEREIGN_ID}|{AGENT_ID}|{minute}"
expected = hmac.new(
HMAC_SECRET.encode(),
msg.encode(),
hashlib.sha256
).hexdigest()[:16]
if hmac.compare_digest(auth, expected):
return True, "verifier"
except Exception:
pass
return False, "invalid"
# ============== 搜索 ==============
def search_files(keyword, top=20):
"""git grep 全仓库文件内容搜索"""
if not keyword:
return []
# 找含关键词的文件
result = subprocess.run(
["git", "grep", "-l", "-i", "--no-color", keyword],
cwd=REPO, capture_output=True, text=True, timeout=30
)
files = [f.strip() for f in result.stdout.splitlines() if f.strip()][:top]
results = []
for f in files:
try:
# 找具体行
line_result = subprocess.run(
["git", "grep", "-n", "-i", "--no-color", keyword, "--", f],
cwd=REPO, capture_output=True, text=True, timeout=10
)
matches = []
for line in line_result.stdout.splitlines()[:5]: # 每文件最多 5 行
# 格式: filename:linenum:content
parts = line.split(":", 2)
if len(parts) >= 3:
matches.append({
"line": int(parts[1]),
"content": parts[2].strip()[:300]
})
results.append({
"file": f,
"match_count": len(matches),
"matches": matches
})
except Exception as e:
results.append({"file": f, "error": str(e)})
return results
def list_tree(path="", depth=3, ext_filter=None):
"""列目录树"""
cmd = ["git", "ls-tree", "-r", "--name-only", "HEAD"]
if path:
# 加上 path 前缀过滤
cmd.append(f"{path}/")
result = subprocess.run(
cmd, cwd=REPO, capture_output=True, text=True, timeout=15
)
files = [f.strip() for f in result.stdout.splitlines() if f.strip()]
# 按深度过滤
tree = []
for f in files:
parts = f.split("/")
if len(parts) - 1 <= depth:
if ext_filter:
if not any(f.endswith(ext) for ext in ext_filter):
continue
tree.append(f)
return tree[:500]
def read_file(path):
"""读单个文件"""
if not path or ".." in path:
return {"ok": False, "error": "invalid path"}
try:
result = subprocess.run(
["git", "show", f"HEAD:{path}"],
cwd=REPO, capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return {"ok": False, "error": "file not found"}
return {
"ok": True,
"path": path,
"size": len(result.stdout),
"content": result.stdout[:50000] # 限 50KB
}
except Exception as e:
return {"ok": False, "error": str(e)}
def get_broadcast():
"""拉最新 BROADCAST"""
bcast_paths = [
os.path.join(REPO, "broadcasts"),
os.path.join(REPO, "BROADCAST.md"),
os.path.join(REPO, "GLW-BROADCAST.hdlp"),
]
for path in bcast_paths:
if os.path.exists(path):
if os.path.isdir(path):
files = sorted(
[os.path.join(path, f) for f in os.listdir(path)],
key=os.path.getmtime,
reverse=True
)
if files:
latest = files[0]
with open(latest) as f:
return {
"ok": True,
"type": "dir",
"latest_file": os.path.basename(latest),
"content": f.read()[:20000]
}
else:
with open(path) as f:
return {
"ok": True,
"type": "file",
"path": os.path.basename(path),
"content": f.read()[:20000]
}
return {
"ok": True,
"broadcast": None,
"note": "no broadcast asset found in repo"
}
def get_system_status():
"""拉 SYSTEM-STATUS"""
candidates = [
os.path.join(REPO, "SYSTEM-STATUS.md"),
os.path.join(REPO, "SYSTEM-STATUS.hdlp"),
os.path.join(REPO, "eternal-lake-heart", "heartbeat-core", "SYSTEM-STATUS.hdlp"),
]
for path in candidates:
if os.path.exists(path):
with open(path) as f:
return {"ok": True, "path": os.path.relpath(path, REPO), "content": f.read()}
return {
"ok": True,
"status": "no SYSTEM-STATUS file in repo · 铸渊创建后会更新",
"note": "请铸渊在仓库根目录创建 SYSTEM-STATUS.md 或在 eternal-lake-heart/heartbeat-core/SYSTEM-STATUS.hdlp"
}
def archive_hldp(content, type_="si", path=None):
"""HLDP 回执归档(写入工作树 · 待铸渊 commit"""
# 这里只是写文件,不直接 push
# 铸渊下次醒来 commit
archive_dir = os.path.join(REPO, "eternal-lake-heart", "archive", "inbox")
os.makedirs(archive_dir, exist_ok=True)
filename = f"{type_}-{int(time.time())}.hdlp"
full_path = os.path.join(archive_dir, filename)
with open(full_path, "w") as f:
f.write(content)
return {"ok": True, "archived": os.path.relpath(full_path, REPO)}
# ============== HTTP Handler ==============
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
# 静默日志
pass
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Minute")
self.end_headers()
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path
query = parse_qs(parsed.query)
# /healthz 免鉴权(探活需要)
if path == "/healthz":
return self.send_json({
"ok": True,
"service": "global-search-api",
"version": VERSION,
"repo": REPO,
"ts": time.time()
})
ok, reason = check_auth(self.headers)
if not ok:
return self.send_json({"ok": False, "error": "unauthorized", "reason": reason}, 401)
try:
if path == "/search":
kw = query.get("q", [""])[0]
top = int(query.get("top", ["20"])[0])
self.send_json({"ok": True, "keyword": kw, "count": -1, "results": search_files(kw, top)})
elif path == "/tree":
path_arg = query.get("path", [""])[0]
depth = int(query.get("depth", ["3"])[0])
ext = query.get("ext", None)
ext_filter = ext[0].split(",") if ext else None
self.send_json({"ok": True, "files": list_tree(path_arg, depth, ext_filter)})
elif path == "/file":
path_arg = query.get("path", [""])[0]
self.send_json(read_file(path_arg))
elif path == "/broadcast":
self.send_json(get_broadcast())
elif path == "/system-status":
self.send_json(get_system_status())
elif path == "/help":
self.send_json({
"ok": True,
"endpoints": {
"GET /healthz": "健康检查",
"GET /search?q={keyword}&top={n}": "全仓库文件内容搜索",
"GET /tree?path={prefix}&depth={n}&ext={hdlp,md}": "列目录树",
"GET /file?path={path}": "读单文件(限 50KB)",
"GET /broadcast": "拉最新 BROADCAST 内容",
"GET /system-status": "拉 SYSTEM-STATUS",
"POST /archive": "HLDP 回执归档(JSON body: {type, content, path?})",
},
"auth": "Authorization: Bearer <TOKEN>",
"repo": "bingshuo/fifth-domain (current HEAD)",
})
else:
self.send_json({"ok": False, "error": "not found", "hint": "GET /help"}, 404)
except Exception as e:
self.send_json({"ok": False, "error": str(e)}, 500)
def do_POST(self):
ok, reason = check_auth(self.headers)
if not ok:
return self.send_json({"ok": False, "error": "unauthorized", "reason": reason}, 401)
parsed = urlparse(self.path)
if parsed.path == "/archive":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8") if length else "{}"
try:
data = json.loads(body)
type_ = data.get("type", "si")
content = data.get("content", "")
path = data.get("path")
self.send_json(archive_hldp(content, type_, path))
except Exception as e:
self.send_json({"ok": False, "error": str(e)}, 400)
else:
self.send_json({"ok": False, "error": "not found"}, 404)
def send_json(self, data, status=200):
body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def main():
server = http.server.HTTPServer(("0.0.0.0", PORT), Handler)
print(f"Global Search API v{VERSION} listening on 0.0.0.0:{PORT}")
print(f"REPO = {REPO}")
print(f"TOKEN = {'set' if TOKEN else 'unset (dev mode)'}")
print(f"HMAC_SECRET = {'set' if HMAC_SECRET else 'unset (static token only)'}")
server.serve_forever()
if __name__ == "__main__":
main()