157 lines
6.1 KiB
Python
157 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
从 local.settings.json 加载环境变量,将 ww 目录下所有内容上传到知识库:
|
|
- .md:正文写入 content,入索引
|
|
- 图片(.png/.jpg/.jpeg/.gif/.webp):先上传到 Blob,再在索引中建文档,metadata 含 image_url
|
|
- .docx/.pptx:在索引中建文档(标题+文件名可搜),可选上传到 Blob 存 file_url
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# 加载 local.settings.json
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
SETTINGS_PATH = os.path.join(SCRIPT_DIR, "local.settings.json")
|
|
if not os.path.exists(SETTINGS_PATH):
|
|
print("未找到 local.settings.json", file=sys.stderr)
|
|
sys.exit(1)
|
|
with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
|
|
for k, v in json.load(f).get("Values", {}).items():
|
|
os.environ.setdefault(k, v)
|
|
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
|
|
WW_ROOT = os.path.normpath(os.path.join(SCRIPT_DIR, "../../../../ww"))
|
|
BLOB_CONTAINER = os.environ.get("INDEXER_BLOB_CONTAINER", "documents")
|
|
BLOB_PREFIX = "kb/ww/"
|
|
INDEX_NAME = "openclaw-resources"
|
|
|
|
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
|
OFFICE_EXT = {".docx", ".pptx"}
|
|
|
|
|
|
def _safe_id(path: str) -> str:
|
|
"""相对路径转成唯一 id,仅保留字母数字、下划线、横线、等号(Azure Search key 要求,不含点)。"""
|
|
rel = path.replace(os.path.sep, "-").replace(".", "_")
|
|
rel = re.sub(r"[^a-zA-Z0-9_\-=]", "_", rel)
|
|
# 去掉连续下划线,避免过长
|
|
rel = re.sub(r"_+", "_", rel).strip("_")
|
|
return "ww-" + (rel[:200] if rel else path.encode("utf-8").hex()[:64])
|
|
|
|
|
|
def _upload_file_to_blob(local_path: str, blob_path: str) -> str:
|
|
"""上传本地文件到 Blob,返回可访问的 URL(不含 SAS)。"""
|
|
conn_str = (
|
|
os.environ.get("INDEXER_BLOB_CONNECTION_STRING")
|
|
or os.environ.get("BLOB_STORAGE_CONNECTION_STRING")
|
|
or os.environ.get("AzureWebJobsStorage")
|
|
)
|
|
if not conn_str:
|
|
raise RuntimeError("未配置 Blob 连接字符串")
|
|
from azure.storage.blob import BlobServiceClient
|
|
|
|
client = BlobServiceClient.from_connection_string(conn_str)
|
|
container = client.get_container_client(BLOB_CONTAINER)
|
|
blob = container.get_blob_client(blob_path)
|
|
with open(local_path, "rb") as f:
|
|
blob.upload_blob(f, overwrite=True)
|
|
return blob.url
|
|
|
|
|
|
def _collect_ww_docs():
|
|
"""遍历 ww 目录,收集要写入索引的文档列表(含图片先上传到 Blob 的 URL)。"""
|
|
docs = []
|
|
if not os.path.isdir(WW_ROOT):
|
|
return docs
|
|
|
|
for root, _dirs, files in os.walk(WW_ROOT):
|
|
for name in files:
|
|
local_path = os.path.join(root, name)
|
|
rel_path = os.path.relpath(local_path, WW_ROOT)
|
|
ext = os.path.splitext(name)[1].lower()
|
|
doc_id = _safe_id(rel_path)
|
|
|
|
if ext == ".md":
|
|
with open(local_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
content = f.read()
|
|
# 对接示例下 API 文档保留原 id 便于兼容
|
|
if "对接示例" in rel_path and "API_DOCUMENTATION" in name:
|
|
doc_id = "ww-api-doc-intelligent-search-agent"
|
|
docs.append({
|
|
"id": doc_id,
|
|
"title": name.replace(ext, "") or name,
|
|
"content": content,
|
|
"project": "openclaw",
|
|
"category": "对接示例" if "对接示例" in rel_path else ("产品" if "产品" in rel_path else "PO图"),
|
|
"tags": "API,文档" if ext == ".md" else "",
|
|
"source": "human",
|
|
"author": "ww",
|
|
})
|
|
continue
|
|
|
|
if ext in IMAGE_EXT:
|
|
blob_path = BLOB_PREFIX + rel_path.replace(os.path.sep, "/")
|
|
try:
|
|
url = _upload_file_to_blob(local_path, blob_path)
|
|
except Exception as e:
|
|
print(f" [跳过] Blob 上传失败 {rel_path}: {e}", file=sys.stderr)
|
|
continue
|
|
title = name.replace(ext, "") or name
|
|
docs.append({
|
|
"id": doc_id,
|
|
"title": title,
|
|
"content": f"图片: {title}",
|
|
"project": "openclaw",
|
|
"category": "产品" if "产品" in rel_path else "PO图",
|
|
"tags": "图片,ww",
|
|
"source": "human",
|
|
"author": "ww",
|
|
"metadata": json.dumps({"image_url": url, "file_path": rel_path}, ensure_ascii=False),
|
|
})
|
|
continue
|
|
|
|
if ext in OFFICE_EXT:
|
|
docs.append({
|
|
"id": doc_id,
|
|
"title": name.replace(ext, "") or name,
|
|
"content": name,
|
|
"project": "openclaw",
|
|
"category": "产品" if "产品" in rel_path else "PO图",
|
|
"tags": "文档,Office",
|
|
"source": "human",
|
|
"author": "ww",
|
|
"metadata": json.dumps({"file_path": rel_path}, ensure_ascii=False),
|
|
})
|
|
continue
|
|
|
|
return docs
|
|
|
|
|
|
async def main():
|
|
from src.server.mcp_server import upload_documents
|
|
|
|
docs = _collect_ww_docs()
|
|
if not docs:
|
|
print("ww 目录下未发现可上传的 .md / 图片 / .docx/.pptx 文件")
|
|
return
|
|
|
|
print(f"共 {len(docs)} 条文档(含图片等)待上传到索引 {INDEX_NAME}。")
|
|
batch_size = 20
|
|
total_ok, total_fail = 0, 0
|
|
for i in range(0, len(docs), batch_size):
|
|
batch = docs[i : i + batch_size]
|
|
result = await upload_documents(documents=json.dumps(batch), index_name=INDEX_NAME)
|
|
out = json.loads(result)
|
|
total_ok += out.get("uploaded", 0)
|
|
total_fail += out.get("failed", 0)
|
|
if out.get("errors"):
|
|
for e in out["errors"]:
|
|
print(f" 错误: {e}", file=sys.stderr)
|
|
print(json.dumps({"success": total_fail == 0, "uploaded": total_ok, "failed": total_fail}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|