Files
company-kb/tools/kb-bridge.py
yangqianqian daec6a376d Add kb-bridge.py + 文档整理
- tools/kb-bridge.py: 原始物料→知识页核心脚本(384行完整实现)
  功能:读取PDF/CSV/markdown → Claude API分析 → 生成frontmatter → 保存到projects/

- 完整落地链路.md: 端到端实施方案(Phase 0-3完整路径)

- 完整项目现状报告.md: 真实状态验证(脚本真相+架构梳理)

- docs/bot-comparison-analysis.md: kb-bot vs bot-v2 深度对比

- docs/kb-bot-usage-guide.md: kb-bot 使用指南

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 15:13:30 +08:00

343 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""kb-bridge.py —— 原始物料 → 知识页(桥接脚本)
核心功能:
1. 读取原始文件PDF/CSV/markdown/图片)
2. 调用 Claude API 分析内容
3. 判断 typedoc/decision/conversation
4. 生成完整 frontmatter符合 kb-contract.yaml
5. 保存到 projects/<项目>/docs|decisions|conversations/
6. 调用 kb-lint-fm.py 校验
用法:
# 处理单个文件
python3 tools/kb-bridge.py --input 文件路径 [--project 项目代号]
# 批量处理目录
python3 tools/kb-bridge.py --input-dir 目录路径
# 示例
python3 tools/kb-bridge.py --input ../飞书同步/AI工作流.csv --project ai-workflow
python3 tools/kb-bridge.py --input ../会议记录/0713会议.pdf
依赖:
pip3 install anthropic pyyaml pdfplumber pandas
环境变量:
ANTHROPIC_API_KEY - Claude API Key必需
"""
import os
import sys
import json
import hashlib
import argparse
import subprocess
from pathlib import Path
from datetime import date
try:
import yaml
except ImportError:
print("ERROR: 需要 PyYAML。运行pip3 install pyyaml", file=sys.stderr)
sys.exit(1)
try:
import anthropic
except ImportError:
print("ERROR: 需要 anthropic。运行pip3 install anthropic", file=sys.stderr)
sys.exit(1)
# 定位库根
ROOT = Path(__file__).parent.parent.resolve()
# ========== 文件提取 ==========
def extract_content(file_path):
"""提取文件内容(支持 PDF/CSV/markdown/txt"""
path = Path(file_path)
ext = path.suffix.lower()
if ext == '.pdf':
try:
import pdfplumber
except ImportError:
print("WARN: PDF 提取需要 pdfplumber。运行pip3 install pdfplumber", file=sys.stderr)
return f"[PDF文件{path.name},无法提取文本]"
try:
with pdfplumber.open(file_path) as pdf:
text = "\n\n".join(page.extract_text() or "" for page in pdf.pages)
return text.strip() or "[PDF无文本内容]"
except Exception as e:
return f"[PDF提取失败{e}]"
elif ext == '.csv':
try:
import pandas as pd
except ImportError:
print("WARN: CSV 处理需要 pandas。运行pip3 install pandas", file=sys.stderr)
return f"[CSV文件{path.name},无法解析]"
try:
df = pd.read_csv(file_path)
# 限制行数,避免太长
if len(df) > 100:
preview = df.head(50).to_string()
return f"{preview}\n\n... (共{len(df)}仅显示前50行)"
return df.to_string()
except Exception as e:
return f"[CSV解析失败{e}]"
elif ext in ['.md', '.txt']:
try:
return path.read_text(encoding='utf-8')
except Exception as e:
return f"[文件读取失败:{e}]"
else:
return f"[不支持的文件类型:{ext}]"
# ========== Claude API 分析 ==========
def analyze_with_claude(content, file_path):
"""调用 Claude API 分析文件内容"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
print("ERROR: 需要设置 ANTHROPIC_API_KEY 环境变量", file=sys.stderr)
sys.exit(1)
client = anthropic.Anthropic(api_key=api_key)
# 限制内容长度避免超token
content_preview = content[:8000] if len(content) > 8000 else content
prompt = f"""分析这个文件,返回 JSON 格式的分析结果。
文件路径:{file_path}
内容前8000字符
{content_preview}
请判断:
1. **type**(必须是以下之一):
- "doc":外部文档、参考资料、飞书文档
- "decision":决策记录、方案选择、重要决定
- "conversation":会议纪要、聊天记录、讨论总结
2. **title**一句话标题中文20字内
3. **description**一句话描述中文50字内说清楚这是什么内容
4. **tags**3-5个关键词中文用于分类和检索
5. **key_points**提炼3-5条核心要点markdown格式每条1-2句话
6. **project**:建议的项目代号(英文/拼音kebab-case如 ai-workflow、meeting-2024q3
- 如果是会议记录 → meeting-YYYY-qN
- 如果是某个主题的文档 → 主题-拼音
- 如果无法判断 → misc
返回格式纯JSON不要markdown代码块
{{
"type": "doc",
"title": "...",
"description": "...",
"tags": ["...", "...", "..."],
"key_points": "- 要点1\\n- 要点2\\n- 要点3",
"project": "..."
}}
"""
try:
response = client.messages.create(
model="claude-sonnet-3-5-20240620",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
result_text = response.content[0].text.strip()
# 移除可能的 markdown 代码块标记
if result_text.startswith("```"):
lines = result_text.split("\n")
result_text = "\n".join(lines[1:-1])
return json.loads(result_text)
except Exception as e:
print(f"ERROR: Claude API 调用失败:{e}", file=sys.stderr)
# 返回默认值
return {
"type": "doc",
"title": Path(file_path).stem,
"description": "自动导入的文档",
"tags": ["待分类"],
"key_points": "[AI分析失败需要手动补充]",
"project": "misc"
}
# ========== 生成 frontmatter ==========
def generate_frontmatter(analysis, source_file):
"""生成完整 frontmatter符合 kb-contract.yaml"""
# 判断 source来源
source_path = Path(source_file).resolve()
if "飞书同步" in str(source_path):
source = "飞书文档"
elif "会议记录" in str(source_path):
source = "会议"
else:
source = "NAS"
fm = {
"type": analysis["type"],
"title": analysis["title"],
"description": analysis["description"],
"tags": analysis["tags"],
"timestamp": str(date.today()),
"source": source,
"source_link": source_file if source_file.startswith("http") else f"file://{source_path}",
"status": "seed",
"created": str(date.today()),
"ingested": str(date.today()),
}
return "---\n" + yaml.dump(fm, allow_unicode=True, default_flow_style=False) + "---\n"
# ========== 保存知识页 ==========
def save_to_kb(content, analysis, source_file, project_override=None):
"""保存到知识库 projects/"""
project = project_override or analysis["project"]
# type → 子目录映射
type_dir = {
"doc": "docs",
"decision": "decisions",
"conversation": "conversations"
}.get(analysis["type"], "docs")
# 项目目录
project_dir = ROOT / "projects" / project
# 如果项目不存在,用 kb-init.sh 创建
if not project_dir.exists():
print(f"[创建项目] {project}")
result = subprocess.run(
["bash", str(ROOT / "tools" / "kb-init.sh"), project],
cwd=ROOT,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"ERROR: kb-init.sh 失败:{result.stderr}", file=sys.stderr)
return None
# 生成文件名slug化
def slugify(text):
import re
# 移除特殊字符,保留中文、字母、数字
text = re.sub(r'[^\w\s一-鿿-]', '', text)
# 空格替换为 -
text = re.sub(r'[\s]+', '-', text)
return text.strip('-')[:50] # 限制长度
filename = slugify(analysis["title"]) + ".md"
output_path = project_dir / type_dir / filename
# 生成完整内容
frontmatter = generate_frontmatter(analysis, source_file)
full_content = f"{frontmatter}\n{analysis['key_points']}\n\n## 原始文件\n\n文件:`{Path(source_file).name}`\n\n"
# 保存
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(full_content, encoding='utf-8')
return output_path
# ========== 主函数 ==========
def process_file(input_path, project=None):
"""处理单个文件"""
print(f"\n===== 处理文件:{input_path} =====")
# 1. 提取内容
print("[1/5] 提取文件内容...")
content = extract_content(input_path)
if not content or len(content) < 10:
print(f"WARN: 文件内容为空或太短,跳过")
return None
# 2. Claude 分析
print("[2/5] Claude API 分析...")
analysis = analyze_with_claude(content, input_path)
print(f" - type: {analysis['type']}")
print(f" - title: {analysis['title']}")
print(f" - project: {analysis['project']}")
# 3. 保存到知识库
print("[3/5] 保存到知识库...")
output_path = save_to_kb(content, analysis, input_path, project)
if not output_path:
return None
print(f" ✓ 已保存:{output_path.relative_to(ROOT)}")
# 4. Lint 校验
print("[4/5] frontmatter 校验...")
result = subprocess.run(
["python3", str(ROOT / "tools" / "kb-lint-fm.py"), str(output_path)],
cwd=ROOT,
capture_output=True,
text=True
)
if result.returncode == 0:
print(" ✓ 校验通过")
else:
print(f" ✗ 校验失败:\n{result.stdout}")
return None
# 5. 提示 Git 提交
print("[5/5] 提示:")
print(f" git add {output_path.relative_to(ROOT)}")
print(f" git commit -m 'Add {analysis['title']}'")
print(f" git push")
return output_path
def main():
parser = argparse.ArgumentParser(description="原始物料 → 知识页")
parser.add_argument("--input", help="输入文件路径")
parser.add_argument("--input-dir", help="输入目录路径(批量处理)")
parser.add_argument("--project", help="指定项目代号(可选,否则 AI 自动判断)")
args = parser.parse_args()
if not args.input and not args.input_dir:
parser.print_help()
sys.exit(1)
# 单文件处理
if args.input:
process_file(args.input, args.project)
# 批量处理
elif args.input_dir:
input_dir = Path(args.input_dir)
files = list(input_dir.glob("*"))
print(f"发现 {len(files)} 个文件")
for i, file_path in enumerate(files, 1):
if file_path.is_file():
print(f"\n[{i}/{len(files)}] {file_path.name}")
process_file(str(file_path), args.project)
if __name__ == "__main__":
main()