AI 工具

Glean Platform Search API:构建权限感知搜索客户端

新集成优先 Platform API:POST /api/search,Bearer token,JSON query。结果权限感知,可用 cursor 翻页。

BINBHO · 图文教程

新集成优先 Platform API:POST /api/search,Bearer token,JSON query。结果权限感知,可用 cursor 翻页。

先看清成品

  • mock 返回带 datasource 的 results。
  • 1 项 pytest 通过。

合规提醒:Glean 是企业产品,需管理员采购与部署;个人通常不能自助注册。搜索与聊天结果继承各数据源权限,禁止用管理员特权接口绕过 ACL 给无授权用户。Token 等同企业访问权,勿提交公开仓库。本文不编造报价。

运行说明:无公司 Glean 租户。A/B 用本地 mock 演示 Platform Search/Chat 形状;C 为发查询前的脱敏守卫。未连接真实 *-be.glean.com。2026-10-23(Asia/Shanghai)pytest 通过。

做完之后你会得到什么?

成果 说明
glean_search.py 搜索客户端
mock_glean.py 假服务
test_108a.py 测试

需要准备哪些环境?

项目 说明
Python 3.10+
环境变量 GLEAN_TOKEN;GLEAN_BASE_URL 形如 https://-be.glean.com

关键事实

事实 来源
Platform API 为新集成推荐面 官方 Quickstart
Search 需要 SEARCH scope 官方 Search 文档
主机名向管理员索取 Server instance(QE) 官方

文件结构

108-glean-A-src/
├── glean_search.py
├── mock_glean.py
└── test_108a.py

第 1 步:客户端

#!/usr/bin/env python3
"""Glean Platform Search: POST /api/search (permission-aware)."""
from __future__ import annotations
import argparse, json, os, sys, urllib.error, urllib.request

def search(base, token, query, page_size=10):
    req = urllib.request.Request(
        f"{base.rstrip('/')}/api/search",
        data=json.dumps({"query": query, "pageSize": page_size}).encode(),
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e

def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("query")
    ap.add_argument("--base", default=os.environ.get("GLEAN_BASE_URL", "https://example-be.glean.com"))
    ap.add_argument("--token", default=os.environ.get("GLEAN_TOKEN", ""))
    args = ap.parse_args(argv)
    if not args.token:
        print("设置 GLEAN_TOKEN(需公司管理员发放的 Platform token)", file=sys.stderr); return 2
    if not args.query.strip():
        print("query 为空", file=sys.stderr); return 2
    print(json.dumps(search(args.base, args.token, args.query), ensure_ascii=False, indent=2))
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

第 2 步:Mock

#!/usr/bin/env python3
"""Mock Glean Platform API search + chat."""
from __future__ import annotations
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

TOKEN = "glean-test-token"

class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass
    def _auth(self):
        return self.headers.get("Authorization") == f"Bearer {TOKEN}"
    def _send(self, code, obj):
        b = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)
    def do_POST(self):
        if not self._auth():
            self._send(401, {"error": "Unauthorized"}); return
        n = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(n) or b"{}")
        if self.path == "/api/search":
            q = body.get("query") or ""
            self._send(200, {
                "results": [
                    {"title": f"Doc about {q}", "url": "https://intranet.example/doc1", "datasource": "confluence", "snippet": "permission-aware mock"},
                ],
                "next_cursor": None,
            }); return
        if self.path == "/api/chat":
            msgs = body.get("messages") or body.get("query") or ""
            text = msgs if isinstance(msgs, str) else json.dumps(msgs)[:120]
            self._send(200, {"messages": [{"author": "GLEAN_AI", "fragments": [{"text": f"Grounded answer for: {text}"}]}]}); return
        self.send_error(404)

def main():
    import argparse
    ap = argparse.ArgumentParser(); ap.add_argument("--port", type=int, default=9108)
    args = ap.parse_args()
    HTTPServer(("127.0.0.1", args.port), H).serve_forever()

if __name__ == "__main__":
    main()

第 3 步:测试

import subprocess, sys, time
from pathlib import Path
HERE = Path(__file__).resolve().parent

def test_search():
    port = 19180
    proc = subprocess.Popen([sys.executable, str(HERE/"mock_glean.py"), "--port", str(port)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    try:
        time.sleep(0.3)
        r = subprocess.run([sys.executable, str(HERE/"glean_search.py"), "vacation", "--base", f"http://127.0.0.1:{port}", "--token", "glean-test-token"], capture_output=True, text=True, check=True)
        assert "confluence" in r.stdout
    finally:
        proc.terminate(); proc.wait(timeout=5)

第 4 步:运行

$ pytest glean search
.                                                                        [100%]
1 passed in 0.36s

怎么检查结果?

  • pytest 绿。
  • 正式环境向管理员申请最小权限 token。

调试示例

401/403。 检查 Bearer 是否过期、是否有 SEARCH scope,以及 base URL 是否指向本公司实例。

常见问题

现象 原因 处理
Client API 还是 Platform 新项目 Platform 旧集成可沿用 Client
过滤器 GET /api/search/filters 先发现再筛
无结果 权限或索引 找管理员

模板规则

  • 最小权限 token。
  • 不绕过 ACL。
  • 查询先过 C 篇守卫。

项目资源

用途 地址
Platform Quickstart(官方) https://developers.glean.com/api/platform-api/getting-started
Search(官方) https://developers.glean.com/api/platform-api/platform-search
Glean 官网 https://glean.com/

企业搜索的第一课:权限对了,结果才安全。