在 Mac Mini 上快速搭建本地知识库 + RAG 搜索系统。
本 skill 不只是"搭建知识库",还包含以下高危能力:
执行能力:
持久化影响:
openclaw cron remove <id>).trash/(7 天兜底清理)安装流程: 本 skill 的 setup.sh 是 交互式安装向导:
--dry-run 选项查看会做什么如果不同意上述任何一项,请不要安装本 skill。
权限必填范围用途 / 风险exec✅python3 + bash scripts文档提取 + 飞书推送file_read✅~/.openclaw/workspace/knowledge/读取文档 + summariesfile_write✅summaries/, archives/, .trash/OCR 修复覆盖旧摘要install_packages✅brew: antiword, tesseract, pandoc, libreofficeHomebrew 包安装(用户确认)download_model✅ollama: nomic-embed-text (~274MB)Ollama 模型下载(用户确认)modify_config✅~/.openclaw/openclaw.json添加 alsoAllow: [exec, process]register_cron✅23:00 分析 + 06:00 推送持久化定时任务(用户确认)network✅飞书 webhook + Ollama 下载外部 API 调用
warning: 本 skill 会自动修改文件、安装包、注册 cron 任务(用户每步都有 y/N 确认)
disable_command: openclaw cron remove <id>
本 skill 在使用时需要以下 OpenClaw 工具能力:
json
{
"tools": {
"alsoAllow": ["exec", "process", "read", "write"]
}
}
⚠️ 执行风险:exec + cron 自动化组合可导致持续命令执行, 请在可信环境(个人 Mac)使用,不要在共享/服务器部署。
本 skill 注册 2 个 cron 任务(23:00 分析 + 06:00 推送), 运行 shell 命令并自动推送消息到飞书。
潜在风险:
建议:
~/.openclaw/workspace/knowledge 放在共享/多用户目录openclaw cron list)bash
cd ~/.openclaw/workspace/skills/knowledge-base-setup/scripts
bash setup.sh <飞书用户ID>
Step 1: 系统依赖
bash
brew install antiword tesseract pandoc
Step 2: Python 依赖
bash
pip3 install kreuzberg pytesseract pymupdf docx openpyxl python-pptx
Step 3: Ollama + embedding 模型
bash
# 安装 Ollama: https://ollama.com/download
ollama pull nomic-embed-text
Step 4: 创建目录结构
bash
mkdir -p ~/.openclaw/workspace/knowledge/.analysis/summaries/archives
mkdir -p ~/.openclaw/workspace/knowledge/temp_docs
touch ~/.openclaw/workspace/knowledge/文章目录/文章目录.md
Step 5: 部署脚本
bash
cp ~/.openclaw/workspace/skills/knowledge-base-setup/scripts/*.py \
~/.openclaw/workspace/knowledge/.analysis/
chmod +x ~/.openclaw/workspace/knowledge/.analysis/*.py
Step 6: 配置 OpenClaw
编辑 ~/.openclaw/openclaw.json,加入:
json
{
"models": {
"providers": {
"ollama": {
"baseUrl": "http://127.0.0.1:11434",
"api": "ollama",
"models": [
{"id": "nomic-embed-text", "name": "Nomic Embed Text"}
]
}
}
},
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text"
}
}
}
}
确保 tools 区块有:
json
"tools": {
"alsoAllow": ["exec", "process"]
}
然后重启: openclaw gateway restart
Step 7: 注册定时任务
bash
# 23:00 分析新文档
openclaw cron add \
--name "23:00分析新文档" \
--cron "0 23 * * *" \
--tz "Asia/Shanghai" \
--session isolated \
--timeout-seconds 600 \
--message "cd ~/.openclaw/workspace/knowledge/.analysis && python3 run_analysis.py && python3 generate_catalog.py" \
--announce --channel feishu --to "user:<飞书用户ID>"
# 08:00 发送文档摘要
openclaw cron add \
--name "08:00发送文档摘要" \
--cron "0 8 * * *" \
--tz "Asia/Shanghai" \
--session isolated \
--timeout-seconds 120 \
--message "读取 summaries/ 目录发送摘要到飞书" \
--announce --channel feishu --to "user:<飞书用户ID>"
防止 OSError: [Errno 63] File name too long(NAME_MAX=255 bytes):
python
SUMMARY_NAME_MAX = 200
def sanitize_filename(name, max_length=SUMMARY_NAME_MAX):
"""截断超长文件名,保留扩展名 + 8 位 MD5 hash 防冲突"""
name_bytes = name.encode('utf-8')
if len(name_bytes) <= max_length:
return name
base, ext = os.path.splitext(name)
ext_bytes = ext.encode('utf-8')
base_bytes = base.encode('utf-8')
import hashlib
h = hashlib.md5(name_bytes).hexdigest()[:8]
reserve = len(ext_bytes) + 1 + 8 # "_" + hash + ext
available = max_length - reserve
if available > 0 and len(base_bytes) > available:
truncated = base_bytes[:available].decode('utf-8', errors='ignore')
return f"{truncated}_{h}{ext}"
return name[:max_length]
主循环的异常捕获重试:
python
try:
with open(summary_file, 'w', encoding='utf-8') as f:
f.write(content)
except OSError as e:
if e.errno == 63: # ENAMETOOLONG
short_name = sanitize_filename(filename, max_length=180)
summary_file = os.path.join(
SUMMARY_DIR,
f"{timestamp}_{short_name}.summary.txt"
)
with open(summary_file, 'w', encoding='utf-8') as f:
f.write(content)
触发场景: 畸形 PDF 文件名(如下载错误的 _; filename_=utf-8''... 双名拼接),原文件名 244+ bytes + 时间戳超 255 bytes 限制。
实测案例: 20260730-Nomura-Asia Insights:China:The Politburo meeting indicated a shift to _countercyclical" policies-260730.pdf_; filename_=utf-8''...pdf (原 248 bytes) → sanitize 后 200 bytes + 8位 hash → 安全创建。
下载失败的 PDF 在文件名里重复了两次( _; filename_=utf-8'' 分隔),实际只需保留前半。一次性清理脚本:
python
import os, shutil
temp_docs = os.path.expanduser("~/.openclaw/workspace/knowledge/temp_docs")
trash_dir = os.path.expanduser("~/.openclaw/workspace/knowledge/.trash/temp_docs_<时间戳>")
os.makedirs(trash_dir, exist_ok=True)
for f in os.listdir(temp_docs):
if "_; filename_=utf-8''" in f:
full = os.path.join(temp_docs, f)
parts = f.split("_; filename_=utf-8''")
real_name = parts[0]
target = os.path.join(temp_docs, real_name)
if not os.path.exists(target):
shutil.move(full, target)
print(f"重命名: {real_name}")
不预设"哪个 PDF 来源会乱码"——实测 72% 的乱码来自非 lightpdf PDF(PPT 转 PDF、扫描件等), 改用 自适应检测:
python
def is_cmap_broken(text, threshold=0.03):
"""检测文本是否含异常字符(CMap 残缺/PUA 污染/未映射 CID)"""
if not text or len(text.strip()) < 50:
return False
total = len(text)
pua_count = sum(1 for c in text if 0xE000 <= ord(c) <= 0xF8FF)
cjk_ext = sum(1 for c in text if 0x20000 <= ord(c) <= 0x2EBEF)
cjk_compat = sum(1 for c in text if 0xF900 <= ord(c) <= 0xFAFF)
cid_count = text.count('(cid:')
bad_ratio = (pua_count + cjk_ext + cjk_compat + cid_count) / total
return bad_ratio > threshold or cid_count > 10
3 类乱码特征:
(cid:xxxx) 字面值 —— pdfplumber 提取失败标志集成位置: extract_pdf_text() 在 kreuzberg / pymupdf 提取后调 is_cmap_broken(),
通过即返回,失败即触发 OCR 路径。
文件大小OCR 耗时备注lightpdf PDF4 页13.5 秒CMap 残缺,自动 OCR大型 PPT 转 PDF90 页0.5 秒默认路径(无需 OCR)65MB .doc169MB 文件0.1 秒antiword 极速专线大型 docx(475K 字)562KB11.2 秒python-docx fallbackOCR 自检总开销-< 200ms3 页抽样 + 字符统计
re_ocr_corrupted.py —— 批量扫描乱码 summary,自动用新版本 utils 重新提取:
bash
# 干跑(不写文件)
python3 re_ocr_corrupted.py --dry-run --max 10
# 实际批量(处理所有乱码)
python3 re_ocr_corrupted.py --max 100
# 只处理指定 PDF
python3 re_ocr_corrupted.py --pdf-list "path1.pdf,path2.pdf"
行为:
extract_pdf_text() 重跑(自动 OCR fallback)text
┌──────────────────────────────────────┐
│ kreuzberg 统一提取层 │
│ (pypdfium2 / python-calamine / pandoc) │
└───┬────────────────────────────────┬───┘
│ │
自动判断 │ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ kreuzberg 直提 │ │ antiword 极速专线 │
│ PDF/DOCX/XLSX/ │ │ (.doc 文件专用) │
│ PPTX/MD/图片OCR │ │ 成功率 85%,<1秒 │
└─────────────────┘ └─────────────────────┘
│ │
│ ┌──────────────────────────────┐
│ │ soffice 兜底转换 │
│ │ (.doc/.xls/.ppt antiword失败) │
│ │ 60秒硬超时(消除误判watchdog)│
│ └──────────────────────────────┘
▼ │
┌──────────────────────────────────────────────┐
│ 文本输出(content) │
│ → summaries/ 摘要文件 → generate_catalog.py │
└──────────────────────────────────────────────┘
格式主方案依赖成功率单文件速度PDFkreuzberg (pypdfium2)无100%0.05-0.7sDOCXkreuzberg + pandocpandoc 3.9+100%0.12-3sXLSXkreuzberg (python-calamine)无100%0.1-0.5sPPTXkreuzberg + pandocpandoc 3.9+100%0.02-0.2sMDkreuzberg + pandocpandoc 3.9+100%<0.01s**.doc****antiword 优先antiword85%**,<1秒<0.02s.doc(失败)soffice 兜底LibreOffice15%2-21s.xlssoffice → XLSXLibreOffice95%2-10s.pptsoffice → PPTXLibreOffice95%2-10s图片kreuzberg 内置 OCRtesseract~90%3-10s
python
# 实测数据:
# 169MB 超大文件 → 26万字符,0.02秒完成
# 正常 .doc(0.1-15MB)→ <1秒
# 成功率 85%,覆盖绝大多数 .doc 文件
result = subprocess.run(['antiword', filepath], capture_output=True, timeout=10)
kreuzberg 是专业的非结构化文档文本提取库(支持 20+ 格式),内部自动路由:
中文(47个): 房产、房价、房地产、居民、消费、股市、经济、政策、利率、通胀、人民币、A股、美联储、PBOC、GDP、股票、资产、投资、债券、银行、PPI、CPI、PMI、M2、就业、失业、汽车、新能源、AI 等
英文(70+个): property、real estate、GDP、inflation、CPI、PPI、PMI、PBOC、Fed、consumer、economy、growth、housing、stock market、EV、AI 等
标签输出语言: 自动判断——英文内容匹配英文关键词输出英文标签,中文内容匹配中文关键词输出中文标签
任务ID调用方式结论23:00分析新文档f3536e18绝对路径 python3 run_analysis.py✅ 无需修改07:00生成财经早报b741c6d5Node.js 脚本❌ 不相关08:00发送财经早报a7cbaacc读取文件发送❌ 不相关09:00发送文档摘要89b4cf75读取 summaries 目录❌ 不相关
⚠️ 迁移前必读: ~/.openclaw/workspace/knowledge/ 目录可能包含:
建议:
先 du -sh ~/.openclaw/workspace/knowledge/ 看大小
排除 .trash/、 .analysis/cache/ 后再迁移
用 rsync -av --exclude='.trash' ... 而不是 scp -r
复制目录(推荐 rsync): bash
rsync -av --exclude='.trash' --exclude='.analysis/cache' \
~/.openclaw/workspace/knowledge user@new-mac:~/.openclaw/workspace/
在新电脑运行 bash setup.sh <飞书用户ID>
重新注册定时任务(Job ID 会变)
问题原因解决LibreOffice 超时watchdog 误判大文件为卡死v2.0 移除 watchdog,60秒硬超时.doc 提取慢统一走 LibreOfficeantiword 专线,169MB 文件 0.02秒DOCX/PPTX 处理失败pandoc 未安装brew install pandocPDF 提取乱码(OCR 不工作)ocrmypdf --skip-text 跳过乱码页v1.4 改为 --force-ocr 强制 OCRPDF 漏检 CMap 残缺没主动判断是否乱码v1.4 is_cmap_broken() 自检(阈值 0.03)文本被截断到 8000 字硬编码 [:8000] 太短v1.4 MAX_EXTRACT_LEN = 500_000.doc 提取失败lightpdf 处理过的 .doc 乱码v1.4 ocr_office_via_ocr() 兜底summary 文件名过长失败畸形 PDF 名 244+ bytes + 时间戳超 NAME_MAXv1.4.1 sanitize_filename() + Errno 63 重试静默失败(不知道哪个文件)不抛异常v1.4 PDFExtractError / ExtractError 含路径飞书无 exec 工具tools 策略限制添加 alsoAllow: [exec, process]BGE-M3 卡顿16GB 内存不足继续用 nomic-embed-text
内容路径Skill 目录~/.openclaw/workspace/skills/knowledge-base-setup/知识库~/.openclaw/workspace/knowledge/分析脚本~/.openclaw/workspace/knowledge/.analysis/目录缓存~/.openclaw/workspace/knowledge/.analysis/.catalog_cache.json摘要输出~/.openclaw/workspace/knowledge/.analysis/summaries/文章目录~/.openclaw/workspace/knowledge/文章目录/文章目录.mdOpenClaw 配置~/.openclaw/openclaw.json
OCR 流程会把 PDF 复制到临时目录处理。
v1.4.3 之前:使用 /tmp/ocrmypdf_work、 /tmp/office_ocr_work、
/tmp/office_convert 共享路径,存在以下风险:
v1.4.3 修复:
tempfile.mkdtemp(prefix="...") 创建 per-run 私有目录(权限 0o700)shutil.rmtree 清理tempfile.mkstemp 创建稳定输出文件(避免被 finally 误删)剩余风险:极端情况下(机器突然断电)可能残留临时目录。
建议定期清理 /Users/home/.openclaw/tmp/ 下 ocrmypdf_*、 office_* 前缀目录。
版本日期更新内容1.0.02026-05-10初始版本,PyMuPDF + LibreOffice 链路1.1.02026-05-13三步 PDF 处理,关键词库,双语标签1.2.02026-05-21分批处理优化,280秒断点1.2.12026-05-22utils.py 共享模块重构,LibreOffice 熔断机制1.3.02026-05-28kreuzberg 统一提取层 + antiword 专线 + pandoc1.4.02026-08-12CMap 残缺度自检 + 50万字完整提取 + OCR fallback 到 .doc1.4.12026-08-12run_analysis.py: sanitize_filename + Errno 63 重试 + 清理 temp_docs 畸形文件