225 lines
8.9 KiB
Python
225 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""知识库自动检查脚本(Lint)
|
||
|
||
自动修复:
|
||
- 索引一致性:缺失的文章补条目,失效的标记 [MISSING]
|
||
- 内部链接:断链修复(同一文件名存在 → 自动修正路径)
|
||
- 原始资料引用验证
|
||
|
||
报告(不自动修):
|
||
- 孤立页面
|
||
- 缺少交叉引用的相关文章
|
||
"""
|
||
import os, re, json
|
||
from datetime import datetime
|
||
|
||
VAULT = "/home/gxd/蝶雅供应链"
|
||
WIKI = os.path.join(VAULT, "知识库")
|
||
RAW = os.path.join(VAULT, "原始资料")
|
||
INDEX_FILE = os.path.join(WIKI, "索引.md")
|
||
LOG_FILE = os.path.join(WIKI, "操作日志.md")
|
||
|
||
issues = []
|
||
auto_fixed = 0
|
||
|
||
def log(msg):
|
||
print(f" {msg}")
|
||
|
||
def get_md_files(directory):
|
||
"""获取目录下所有 md 文件(递归)"""
|
||
files = {}
|
||
for root, dirs, fnames in os.walk(directory):
|
||
for f in fnames:
|
||
if f.endswith('.md') and f not in ('索引.md', '操作日志.md'):
|
||
rel = os.path.relpath(os.path.join(root, f), directory)
|
||
files[f] = rel
|
||
return files
|
||
|
||
def check_index_consistency():
|
||
"""检查索引一致性"""
|
||
global auto_fixed
|
||
if not os.path.exists(INDEX_FILE):
|
||
issues.append("索引.md 不存在")
|
||
return
|
||
|
||
# 获取所有 wiki 文章
|
||
wiki_files = get_md_files(WIKI)
|
||
|
||
with open(INDEX_FILE, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# 解析索引中的文章路径
|
||
indexed_paths = set()
|
||
for line in content.split('\n'):
|
||
if '](' in line and line.startswith('|'):
|
||
m = re.search(r'\]\(([^)]+)\)', line)
|
||
if m:
|
||
path = m.group(1)
|
||
indexed_paths.add(path)
|
||
|
||
# 检查:文件存在但索引中没有
|
||
for fname, relpath in wiki_files.items():
|
||
if relpath not in indexed_paths:
|
||
log(f"⚠️ 文章存在但未在索引中: {relpath}")
|
||
# 尝试自动补索引(需要知道主题)
|
||
topic = relpath.split('/')[0] if '/' in relpath else None
|
||
if topic:
|
||
# 读取文章获取摘要
|
||
summary = ""
|
||
with open(os.path.join(WIKI, relpath), 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
if line.startswith('## 概述'):
|
||
next_line = next(f, '').strip()
|
||
summary = next_line[:50]
|
||
break
|
||
if line.startswith('> 来源') or line.startswith('> Source'):
|
||
continue
|
||
|
||
# 生成新的索引行
|
||
today = datetime.now().strftime('%Y-%m-%d')
|
||
new_line = f"| [{fname[:-3]}]({relpath}) | {summary or '(无摘要)'} | {today} |"
|
||
|
||
# 找到对应主题的表格最后一行,追加
|
||
topic_header = f"## {topic}"
|
||
if topic_header in content:
|
||
# 找到 topic 区域末尾,插入新行
|
||
lines = content.split('\n')
|
||
insert_pos = -1
|
||
in_topic = False
|
||
for i, line in enumerate(lines):
|
||
if line == topic_header:
|
||
in_topic = True
|
||
continue
|
||
if in_topic and line.startswith('## '):
|
||
break
|
||
if in_topic and line.startswith('|') and '暂无' in line:
|
||
# 替换"暂无"行
|
||
lines[i] = new_line
|
||
insert_pos = -2 # flag for replaced
|
||
break
|
||
if in_topic and line.startswith('| ['):
|
||
insert_pos = i + 1
|
||
|
||
if insert_pos == -2:
|
||
# 已替换
|
||
pass
|
||
elif insert_pos > 0:
|
||
lines.insert(insert_pos, new_line)
|
||
content = '\n'.join(lines)
|
||
auto_fixed += 1
|
||
|
||
# 检查:索引中有但文件不存在
|
||
for path in indexed_paths:
|
||
full_path = os.path.join(WIKI, path)
|
||
if not os.path.exists(full_path):
|
||
log(f"⚠️ 索引中的文章已删除: {path}")
|
||
# 标记为 MISSING(避免重复追加)
|
||
if '[MISSING]' not in content.split(f"]({path})")[1][:30]:
|
||
content = content.replace(f"]({path})", f"]({path}) [MISSING]")
|
||
auto_fixed += 1
|
||
auto_fixed += 1
|
||
|
||
# 写入更新后的索引
|
||
with open(INDEX_FILE, 'w', encoding='utf-8') as f:
|
||
f.write(content)
|
||
|
||
def check_internal_links():
|
||
"""检查文章内部的 Wiki 链接"""
|
||
global auto_fixed
|
||
wiki_files = {}
|
||
for root, dirs, fnames in os.walk(WIKI):
|
||
for f in fnames:
|
||
if f.endswith('.md'):
|
||
rel = os.path.relpath(os.path.join(root, f), WIKI)
|
||
wiki_files[f] = rel
|
||
|
||
for root, dirs, fnames in os.walk(WIKI):
|
||
for fname in fnames:
|
||
if not fname.endswith('.md') or fname in ('索引.md', '操作日志.md'):
|
||
continue
|
||
fpath = os.path.join(root, fname)
|
||
with open(fpath, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# 查找 markdown 链接
|
||
for m in re.finditer(r'\[([^\]]+)\]\(([^)]+)\)', content):
|
||
link_text = m.group(1)
|
||
link_target = m.group(2)
|
||
|
||
# 跳过外部链接
|
||
if link_target.startswith('http://') or link_target.startswith('https://'):
|
||
continue
|
||
|
||
# 跳过 file:// URI(本机文件链接)和页内锚点(#xxx)
|
||
if link_target.startswith('file://') or link_target.startswith('#'):
|
||
continue
|
||
|
||
# 检查路径是否有效
|
||
# 如果是相对路径,基于当前文件目录解析
|
||
base_dir = os.path.dirname(fpath)
|
||
abs_target = os.path.normpath(os.path.join(base_dir, link_target))
|
||
|
||
if not os.path.exists(abs_target):
|
||
target_file = os.path.basename(link_target)
|
||
if target_file in wiki_files:
|
||
# 找到了正确路径
|
||
correct_path = os.path.relpath(
|
||
os.path.join(WIKI, wiki_files[target_file]),
|
||
os.path.dirname(fpath)
|
||
)
|
||
if correct_path != link_target:
|
||
content = content.replace(
|
||
f']({link_target})',
|
||
f']({correct_path})'
|
||
)
|
||
log(f"🔧 修复链接: {fname} → {link_target} → {correct_path}")
|
||
auto_fixed += 1
|
||
else:
|
||
issues.append(f"断链: {fname} → {link_target} (文件不存在)")
|
||
|
||
with open(fpath, 'w', encoding='utf-8') as f:
|
||
f.write(content)
|
||
|
||
def check_orphan_pages():
|
||
"""检查孤立页面(没有其他文章引用)"""
|
||
all_content = ""
|
||
for root, dirs, fnames in os.walk(WIKI):
|
||
for fname in fnames:
|
||
if fname.endswith('.md') and fname not in ('索引.md', '操作日志.md'):
|
||
fpath = os.path.join(root, fname)
|
||
with open(fpath, 'r', encoding='utf-8') as f:
|
||
all_content += f.read() + "\n"
|
||
|
||
for root, dirs, fnames in os.walk(WIKI):
|
||
for fname in fnames:
|
||
if fname.endswith('.md') and fname not in ('索引.md', '操作日志.md'):
|
||
# 检查是否有其他文章引用了它
|
||
name_without_ext = fname[:-3]
|
||
if name_without_ext not in all_content.replace(fname, ''):
|
||
rel = os.path.relpath(os.path.join(root, fname), WIKI)
|
||
if rel not in ('供应链流程/知识库搭建架构.md', '供应链流程/2026-06-20-知识库搭建全记录.md'):
|
||
issues.append(f"孤立页面: {rel} (没有入链)")
|
||
|
||
def update_log():
|
||
"""更新操作日志"""
|
||
count_issues = len(issues)
|
||
if count_issues == 0 and auto_fixed == 0:
|
||
return
|
||
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
with open(LOG_FILE, 'a', encoding='utf-8') as f:
|
||
f.write(f"\n## [{today}] 检查 | {count_issues} 个问题,{auto_fixed} 处自动修复\n")
|
||
for issue in issues:
|
||
f.write(f"- {issue}\n")
|
||
|
||
if __name__ == '__main__':
|
||
print(f"🔍 知识库检查开始...")
|
||
check_index_consistency()
|
||
check_internal_links()
|
||
check_orphan_pages()
|
||
update_log()
|
||
print(f"\n✅ 检查完成: {len(issues)} 个问题, {auto_fixed} 处自动修复")
|
||
if issues:
|
||
for issue in issues:
|
||
print(f" ⚠️ {issue}")
|