Files
QMDSearch/skills/qmdsearch-agent/references/api-examples.md
T
kplam db568740c5 feat: 入库改为独立页面,更新 API 说明文档与 Agent skill
- 新增 Ingest.vue 独立入库页(文本/文件/批量 + 任务进度列表),移除入库 Drawer
- 导航栏去掉「入库进度」,新增「文档入库」入口;知识库页「入库」跳转独立页
- API 说明页接口清单与后端对齐(补齐用户管理/上传批量/任务重试删除/reingest 等,修正鉴权标注)
- 更新 qmdsearch-agent skill 与 api-examples(鉴权说明、会话 token、新端点示例)
2026-08-04 20:50:53 +08:00

6.9 KiB
Raw Blame History

QMDSearch API 示例与 Python 客户端封装

基址占位符 {BASE} 替换为实际服务地址,例如 http://localhost:8000http://<nas-ip>:8000

一、curl 示例

登录

curl -s -X POST {BASE}/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"<password>"}'

文本入库(202 + task_id

curl -s -X POST {BASE}/api/v1/documents \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"text":"QMDSearch 是面向 AI Agent 的分层信息检索服务……","title":"QMDSearch 介绍","source":"notes"}'

文件上传(202 + task_id

curl -s -X POST {BASE}/api/v1/documents/upload \
  -H "Authorization: Bearer <token>" \
  -F "file=@document.pdf" \
  -F "source=manual"

轮询任务状态

curl -s {BASE}/api/v1/documents/tasks/<task_id>

批量文件上传(202,逐文件建任务)

curl -s -X POST {BASE}/api/v1/documents/upload-batch \
  -H "Authorization: Bearer <token>" \
  -F "files=@a.pdf" -F "files=@b.md"

列出入库任务 / 重试 / 删除任务

curl -s -H "Authorization: Bearer <token>" "{BASE}/api/v1/documents/tasks?limit=20"
curl -s -X POST -H "Authorization: Bearer <token>" {BASE}/api/v1/documents/tasks/<task_id>/retry
curl -s -X DELETE -H "Authorization: Bearer <token>" {BASE}/api/v1/documents/tasks/<task_id>

重新摘要入库(读原文件 → 删旧 → 重跑)

curl -s -X POST -H "Authorization: Bearer <token>" {BASE}/api/v1/documents/<doc_id>/reingest

检索(免登录)

curl -s -X POST {BASE}/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"query":"如何配置 Redis 缓存","top_k":5,"summarize":false}'

二、Python 客户端封装

可直接在 Agent 工具代码里复用:

import time
import requests


class QMDSearchClient:
    """QMDSearch 最小客户端:登录、入库(文本/文件)、轮询、检索。"""

    def __init__(self, base_url: str, username: str, password: str,
                 poll_interval: float = 1.5, poll_timeout: float = 120.0):
        self.base = base_url.rstrip("/")
        self.username = username
        self.password = password
        self.poll_interval = poll_interval
        self.poll_timeout = poll_timeout
        self.token: str | None = None

    # ---- 鉴权 ----
    def login(self) -> str:
        r = requests.post(
            f"{self.base}/api/v1/auth/login",
            json={"username": self.username, "password": self.password},
            timeout=30,
        )
        r.raise_for_status()
        body = r.json()
        if body.get("code") != 0:
            raise RuntimeError(f"登录失败: {body}")
        self.token = body["data"]["token"]
        return self.token

    def _headers(self) -> dict:
        if not self.token:
            self.login()
        return {"Authorization": f"Bearer {self.token}"}

    def _ok(self, resp: requests.Response):
        resp.raise_for_status()
        body = resp.json()
        if body.get("code") != 0:
            raise RuntimeError(f"API 错误 code={body.get('code')} msg={body.get('message')}")
        return body["data"]

    # ---- 入库 ----
    def ingest_text(self, text: str, title: str = "", source: str = "",
                    metadata: dict | None = None) -> str:
        data = {"text": text}
        if title:
            data["title"] = title
        if source:
            data["source"] = source
        if metadata:
            data["metadata"] = {str(k): str(v) for k, v in metadata.items()}
        resp = requests.post(f"{self.base}/api/v1/documents",
                             headers=self._headers(), json=data, timeout=30)
        return self._ok(resp)["task_id"]

    def upload_file(self, path: str, title: str = "", source: str = "",
                    metadata: dict | None = None) -> str:
        files = {"file": open(path, "rb")}
        data = {}
        if title:
            data["title"] = title
        if source:
            data["source"] = source
        if metadata:
            data["metadata"] = str({str(k): str(v) for k, v in metadata.items()})
        resp = requests.post(f"{self.base}/api/v1/documents/upload",
                             headers=self._headers(), files=files, data=data, timeout=60)
        return self._ok(resp)["task_id"]

    def upload_batch(self, paths: list[str]) -> dict:
        """批量上传多个文件:返回 {tasks:[{filename,task_id}], failed:[{filename,error}]}"""
        files = [("files", open(p, "rb")) for p in paths]
        resp = requests.post(f"{self.base}/api/v1/documents/upload-batch",
                             headers=self._headers(), files=files, timeout=60)
        return self._ok(resp)

    # ---- 轮询 ----
    def wait_task(self, task_id: str) -> dict:
        deadline = time.time() + self.poll_timeout
        while time.time() < deadline:
            resp = requests.get(f"{self.base}/api/v1/documents/tasks/{task_id}", timeout=30)
            data = self._ok(resp)
            if data["status"] == "done":
                return data["result"]
            if data["status"] == "failed":
                raise RuntimeError(f"入库失败: {data.get('error')}")
            time.sleep(self.poll_interval)
        raise TimeoutError(f"任务 {task_id} 轮询超时")

    def ingest_text_wait(self, *args, **kwargs) -> dict:
        return self.wait_task(self.ingest_text(*args, **kwargs))

    def upload_file_wait(self, *args, **kwargs) -> dict:
        return self.wait_task(self.upload_file(*args, **kwargs))

    # ---- 检索 ----
    def search(self, query: str, top_k: int | None = None,
               summarize: bool = False) -> dict:
        payload = {"query": query, "summarize": summarize}
        if top_k is not None:
            payload["top_k"] = top_k
        resp = requests.post(f"{self.base}/api/v1/search",
                             headers=self._headers(), json=payload, timeout=60)
        return self._ok(resp)


# 用法
if __name__ == "__main__":
    client = QMDSearchClient("http://localhost:8000", "admin", "<password>")
    client.login()
    # 文本入库并等待完成
    result = client.ingest_text_wait("这是一篇关于分层检索的笔记……", title="笔记")
    print("document_id =", result["document_id"])
    # 检索
    hits = client.search("分层检索是什么", top_k=5)["hits"]
    for h in hits:
        print(f"[{h['score']:.3f}] {h['title']}: {h['text'][:80]}")

三、响应结构速记

  • 文本 / 文件 / 批量入库 202data = {task_id, status:"pending"[, saved_path]};批量返回 {tasks, failed}
  • 任务 donedata.result = {document_id, summary:{l1_summary,l2_outline,l3_content_outline,level}, category, chunks_count, tags, category_confidence, deduplicated}
  • 检索 → data = {query, hits:[{text,doc_id,title,section_path,score,doc_summary}], routed_categories, fallback, extracted_info, summary?}