Phase 0-2: Schema cleanup, typed relations, event-driven automation
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd) - Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py) - Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks - Fix .gitignore to track tools/ and .githooks/ - Fix git remote URL (remove plaintext token) - New wiki pages: 504 pages, 34 raw sources
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
# 知识库同步可行性分析与实施计划
|
||||
|
||||
> **任务目标**:定期将 `D:\TC_UP\home-wiki` 的笔记内容更新到 `D:\Applications\app\kepano-obsidian-main` 仓库
|
||||
|
||||
---
|
||||
|
||||
## 一、双仓库结构对比
|
||||
|
||||
### 1.1 目录结构映射
|
||||
|
||||
| 源仓库 (D:\TC_UP\home-wiki) | 目标仓库 (kepano-obsidian-main) | 兼容性 |
|
||||
|---------------------------|-------------------------------|-------|
|
||||
| `concepts/` (12 文件) | `wiki/` (464 文件,概念页) | ✅ 高度兼容 |
|
||||
| `entities/` | `References/` + `wiki/` (实体页) | ✅ 可兼容 |
|
||||
| `raw/` (48 文件) | `raw/` (480 文件) | ✅ 完全兼容 |
|
||||
| `sources/` | `raw/` (已有,需合并) | ✅ 需映射 |
|
||||
| `Daily/` | `Daily/` | ✅ 完全兼容 |
|
||||
| `Templates/` | `Templates/` | ✅ 需检查冲突 |
|
||||
| `MyNotes/` | `Notes/` | ✅ 可映射 |
|
||||
| `scripts/` | `tools/scripts/` | ✅ 可兼容 |
|
||||
| `04-Tools/` | 无对应(可作为知识卡片) | ⚠️ 需映射 |
|
||||
| `syntheses/` | `wiki/` (概念/研究报告) | ✅ 可兼容 |
|
||||
| `reports/` | `wiki/archive/` (归档报告) | ⚠️ 需过滤 |
|
||||
|
||||
### 1.2 核心规范对比
|
||||
|
||||
| 维度 | home-wiki | kepano-obsidian-main | 兼容性 |
|
||||
|------|-----------|---------------------|-------|
|
||||
| **Frontmatter** | `type`, `title`, `created`, `updated` | `categories`, `tags`, `created`, `source`, `type` | ⚠️ 格式不同 |
|
||||
| **标签体系** | Canonical Tags ( predefined list) | 自由 tags (no whitelist) | ⚠️ 需转换 |
|
||||
| **关系类型** | Wikilinks + `→`, `⊍` 符号 | 标准 Wikilinks | ⚠️ 需清理 |
|
||||
| **Claims 规范** | 严格 source 追溯 | `[raw:文件名:行号]` 格式 | ⚠️ 需转换 |
|
||||
| **文件命名** | 中文优先 | 中英文混合 | ✅ 兼容 |
|
||||
| **日期格式** | `YYYY-MM-DD` | `YYYY-MM-DD` | ✅ 兼容 |
|
||||
|
||||
---
|
||||
|
||||
## 二、可行性分析
|
||||
|
||||
### 2.1 技术可行性 ✅ **可行**
|
||||
|
||||
1. **底层技术栈一致**:两个仓库都使用 Obsidian + Markdown + Git
|
||||
2. **核心概念相似**:
|
||||
- 都使用 `raw/` 存储原始来源
|
||||
- 都使用 `Daily/` 存储日记
|
||||
- 都使用概念/实体分离的组织方式
|
||||
3. **内容高度互补**:
|
||||
- home-wiki: 专注"高等教育 AI",12 个概念页,48 个 raw 文件
|
||||
- kepano-obsidian-main: 综合性知识库,464 个 wiki 页面,480 个 raw 文件
|
||||
|
||||
### 2.2 内容冲突风险 ⚠️ **中等**
|
||||
|
||||
1. **Frontmatter 格式差异**:
|
||||
```yaml
|
||||
# home-wiki 格式
|
||||
---
|
||||
type: concept
|
||||
title: 涌现
|
||||
created: 2026-05-20
|
||||
updated: 2026-05-20
|
||||
tags: [phenomenon, systems-theory]
|
||||
sources: [raw/articles/原始资料.html]
|
||||
---
|
||||
|
||||
# kepano-obsidian-main 格式
|
||||
---
|
||||
categories:
|
||||
- "[[LLM Wiki]]"
|
||||
tags:
|
||||
- wiki
|
||||
- concept
|
||||
created: 2026-06-30
|
||||
source: "[[{source-file}]]"
|
||||
type: concept
|
||||
---
|
||||
```
|
||||
|
||||
2. **关系符号**:home-wiki 使用 `→`, `⊍` 等符号,目标仓库不使用
|
||||
3. **Claims 规范**:
|
||||
```yaml
|
||||
# home-wiki
|
||||
claims:
|
||||
- text: "事实陈述"
|
||||
source: "raw/articles/原始资料.html"
|
||||
confidence: 0.9
|
||||
|
||||
# kepano-obsidian-main
|
||||
事实陈述[raw:文件名:行号]
|
||||
```
|
||||
|
||||
### 2.3 数据规模 ⚠️ **需筛选**
|
||||
|
||||
| 类型 | home-wiki | kepano-obsidian-main | 策略 |
|
||||
|------|-----------|---------------------|------|
|
||||
| 概念页 | 12 | 177+ | ✅ 全部同步 |
|
||||
| 实体页 | ~5 | 120+ | ✅ 全部同步 |
|
||||
| 原始来源 | 48 | 480 | ⚠️ 需去重 |
|
||||
| Daily | 未知 | 未知 | ✅ 合并 |
|
||||
| 模板 | 未知 | 多个 | ⚠️ 需冲突检查 |
|
||||
| 工具文档 | 7 | 26+ | ✅ 全部同步 |
|
||||
|
||||
---
|
||||
|
||||
## 三、实施计划
|
||||
|
||||
### 3.1 第一阶段:准备工作(Week 1)
|
||||
|
||||
#### 3.1.1 冲突检查与映射表创建
|
||||
|
||||
**任务**:创建 `tools/sync-mapping.json` 映射表
|
||||
|
||||
```json
|
||||
{
|
||||
"directoryMapping": {
|
||||
"concepts/": "wiki/",
|
||||
"entities/": ["wiki/", "References/"],
|
||||
"raw/": "raw/",
|
||||
"sources/": "raw/",
|
||||
"Daily/": "Daily/",
|
||||
"Templates/": "Templates/",
|
||||
"MyNotes/": "Notes/",
|
||||
"scripts/": "tools/scripts/",
|
||||
"04-Tools/": "wiki/", // 作为工具知识卡片
|
||||
"syntheses/": "wiki/",
|
||||
"reports/": "wiki/archive/"
|
||||
},
|
||||
"tagMapping": {
|
||||
"phenomenon": "concept/phenomenon",
|
||||
"theory": "concept/theory",
|
||||
"technology": "concept/technology",
|
||||
"person": "people",
|
||||
"organization": "entity/organization",
|
||||
"AI-education": "ai/education",
|
||||
"higher-ed": "education/higher-ed"
|
||||
},
|
||||
"conflictFiles": ["AGENTS.md", "SCHEMA.md", "Templates/*.md"]
|
||||
}
|
||||
```
|
||||
|
||||
**输出**:
|
||||
- `tools/sync-mapping.json` — 目录和标签映射表
|
||||
- `tools/sync-conflicts.md` — 冲突文件清单
|
||||
|
||||
#### 3.1.2 Frontmatter 转换器
|
||||
|
||||
**任务**:创建 `tools/sync/convert-frontmatter.py`
|
||||
|
||||
```python
|
||||
# 转换规则
|
||||
# 1. type: concept → categories: "[[LLM Wiki]]", type: concept
|
||||
# 2. tags: [a, b] → tags: [wiki, a, b] (添加 wiki 标签)
|
||||
# 3. sources: [...] → source: "[[{primary-source}]]"
|
||||
# 4. updated: 删除(目标仓库不使用)
|
||||
# 5. claims → 转换为 [raw:文件名:行号] 格式
|
||||
# 6. 移除 →, ⊍ 等关系符号,替换为标准 Wikilinks
|
||||
```
|
||||
|
||||
**输出**:
|
||||
- `tools/sync/convert-frontmatter.py` — Frontmatter 转换脚本
|
||||
- `tools/sync/convert-frontmatter-test.md` — 转换测试样例
|
||||
|
||||
### 3.2 第二阶段:核心内容同步(Week 2)
|
||||
|
||||
#### 3.2.1 概念页同步
|
||||
|
||||
**任务**:同步 `concepts/` → `wiki/`
|
||||
|
||||
**步骤**:
|
||||
1. 读取 `D:\TC_UP\home-wiki\concepts\*.md`
|
||||
2. 转换 frontmatter(使用转换器)
|
||||
3. 转换关系符号(`→`, `⊍` → 标准 Wikilinks)
|
||||
4. 转换 claims(`claims[].text[raw:source]` → `[raw:文件名:行号]`)
|
||||
5. 写入 `wiki/`,避免冲突(添加后缀 `_from_homewiki`)
|
||||
6. 更新 `wiki/index.md`(添加到核心概念章节)
|
||||
|
||||
**检查点**:
|
||||
- [ ] 12 个概念页全部转换
|
||||
- [ ] Frontmatter 格式符合目标仓库规范
|
||||
- [ ] Wikilinks 无断链
|
||||
- [ ] 索引更新完成
|
||||
|
||||
#### 3.2.2 实体页同步
|
||||
|
||||
**任务**:同步 `entities/` → `wiki/` + `References/`
|
||||
|
||||
**步骤**:
|
||||
1. 读取 `D:\TC_UP\home-wiki\entities\*.md`
|
||||
2. 根据实体类型分流:
|
||||
- `subtype: person` → `wiki/` + `References/People.md` 添加条目
|
||||
- `subtype: organization` → `References/` + `wiki/`
|
||||
- `subtype: tool` → `wiki/`(工具页)
|
||||
3. 转换 frontmatter(使用转换器)
|
||||
4. 检查与现有实体页冲突(如郭朝晖是否已存在)
|
||||
5. 写入目标位置,更新索引
|
||||
|
||||
**检查点**:
|
||||
- [ ] ~5 个实体页全部转换
|
||||
- [ ] 实体类型分类正确
|
||||
- [ ] 无重复实体
|
||||
- [ ] 索引更新完成
|
||||
|
||||
### 3.3 第三阶段:原始来源同步(Week 3)
|
||||
|
||||
#### 3.3.1 raw/ 去重与合并
|
||||
|
||||
**任务**:合并 `raw/` + `sources/` → `raw/`
|
||||
|
||||
**步骤**:
|
||||
1. 扫描 `D:\TC_UP\home-wiki\raw/` + `sources/`
|
||||
2. 计算文件哈希值,去重(避免重复文件)
|
||||
3. 对比目标仓库 `raw/`,识别新文件
|
||||
4. 创建新文件到 `raw/`(按类型分类:papers/, articles/, transcripts/)
|
||||
5. 更新 Wiki 页面的 source 字段
|
||||
|
||||
**检查点**:
|
||||
- [ ] 去重完成
|
||||
- [ ] 新文件识别正确
|
||||
- [ ] Source 字段更新完成
|
||||
|
||||
#### 3.3.2 claims 追溯转换
|
||||
|
||||
**任务**:转换 claims 为 `[raw:文件名:行号]` 格式
|
||||
|
||||
**步骤**:
|
||||
1. 扫描所有 Wiki 页面中的 `claims` 字段
|
||||
2. 提取 `claims[].source` 路径
|
||||
3. 查找对应的 raw 文件行号
|
||||
4. 转换格式:`text[raw:文件名:行号]`
|
||||
5. 删除原 `claims` 字段
|
||||
|
||||
**检查点**:
|
||||
- [ ] 所有 claims 转换完成
|
||||
- [ ] 行号正确
|
||||
- [ ] 溯源格式符合目标仓库规范
|
||||
|
||||
### 3.4 第四阶段:其他内容同步(Week 4)
|
||||
|
||||
#### 3.4.1 Daily/ 合并
|
||||
|
||||
**任务**:合并 `Daily/` → `Daily/`
|
||||
|
||||
**步骤**:
|
||||
1. 扫描 `D:\TC_UP\home-wiki\Daily\*.md`
|
||||
2. 按日期去重(同名文件需要合并内容)
|
||||
3. 写入目标 `Daily/`
|
||||
|
||||
**检查点**:
|
||||
- [ ] 日记合并完成
|
||||
- [ ] 无内容丢失
|
||||
|
||||
#### 3.4.2 Templates/ 冲突处理
|
||||
|
||||
**任务**:检查模板文件冲突
|
||||
|
||||
**步骤**:
|
||||
1. 对比两个仓库的模板文件列表
|
||||
2. 识别同名文件(如 `Concept Template.md`)
|
||||
3. 决策:保留目标仓库模板,或融合两个模板
|
||||
|
||||
**检查点**:
|
||||
- [ ] 冲突识别完成
|
||||
- [ ] 融合方案确定
|
||||
|
||||
#### 3.4.3 工具文档同步
|
||||
|
||||
**任务**:同步 `04-Tools/` + `scripts/` → `tools/`
|
||||
|
||||
**步骤**:
|
||||
1. 读取 `D:\TC_UP\home-wiki\04-Tools\*.md` + `scripts/`
|
||||
2. 写入 `tools/scripts/`
|
||||
3. 更新 `wiki/index.md` 工具章节
|
||||
|
||||
**检查点**:
|
||||
- [ ] 工具文档全部同步
|
||||
- [ ] 索引更新完成
|
||||
|
||||
### 3.5 第五阶段:验证与优化(Week 5)
|
||||
|
||||
#### 3.5.1 Lint 检查
|
||||
|
||||
**任务**:运行 Wiki Lint 检查
|
||||
|
||||
**检查项**:
|
||||
- [ ] Frontmatter 一致性
|
||||
- [ ] Wikilink 连通性
|
||||
- [ ] 标签格式
|
||||
- [ ] 日期格式
|
||||
- [ ] 溯源格式
|
||||
|
||||
**修复**:
|
||||
- 运行 `tools/scripts/wiki-lint-orphan.ps1`
|
||||
- 运行 `tools/scripts/wiki-lint-broken-v2.ps1`
|
||||
- 运行 `tools/scripts/wiki-lint-fix-type.ps1`
|
||||
|
||||
#### 3.5.2 交叉引用检查
|
||||
|
||||
**任务**:检查新 Wiki 页面与现有页面的交叉引用
|
||||
|
||||
**步骤**:
|
||||
1. 扫描新 Wiki 页面的所有 Wikilinks
|
||||
2. 检查目标页面是否存在
|
||||
3. 如不存在,创建占位页面或删除链接
|
||||
4. 更新相关页面的反向链接
|
||||
|
||||
**检查点**:
|
||||
- [ ] 断链修复完成
|
||||
- [ ] 反向链接添加完成
|
||||
|
||||
#### 3.5.3 日志更新
|
||||
|
||||
**任务**:更新 `wiki/log.md`
|
||||
|
||||
**步骤**:
|
||||
1. 记录同步操作(日期、操作类型、涉及页面)
|
||||
2. 记录转换规则(frontmatter、claims、标签)
|
||||
3. 记录冲突处理(文件、解决方案)
|
||||
|
||||
**输出**:
|
||||
- `wiki/log.md` 追加同步日志条目
|
||||
|
||||
---
|
||||
|
||||
## 四、定期同步机制
|
||||
|
||||
### 4.1 同步频率
|
||||
|
||||
| 频率 | 内容 | 负担 |
|
||||
|------|------|------|
|
||||
| **每日** | Daily/ 日记 | 低 |
|
||||
| **每周** | concepts/, entities/, raw/ 新增内容 | 中 |
|
||||
| **每月** | 全量检查、去重、Lint | 高 |
|
||||
|
||||
### 4.2 自动化脚本
|
||||
|
||||
**方案 1:PowerShell 脚本**
|
||||
|
||||
```powershell
|
||||
# tools/sync/daily-sync.ps1
|
||||
# 用途:每日同步 Daily/ 日记
|
||||
|
||||
$sourcePath = "D:\TC_UP\home-wiki\Daily"
|
||||
$targetPath = "D:\Applications\app\kepano-obsidian-main\Daily"
|
||||
|
||||
Get-ChildItem $sourcePath -Filter "*.md" | ForEach-Object {
|
||||
$targetFile = Join-Path $targetPath $_.Name
|
||||
if (-not (Test-Path $targetFile)) {
|
||||
Copy-Item $_.FullName $targetFile
|
||||
Write-Host "Copied $($_.Name)"
|
||||
} else {
|
||||
Write-Host "Skipped $($_.Name) (exists)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**方案 2:Python 脚本**
|
||||
|
||||
```python
|
||||
# tools/sync/weekly-sync.py
|
||||
# 用途:每周同步 concepts/, entities/, raw/
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
MAPPINGS = {
|
||||
"concepts": "wiki",
|
||||
"entities": ["wiki", "References"],
|
||||
"raw": "raw",
|
||||
}
|
||||
|
||||
def sync_directory(source, target, converter=None):
|
||||
"""同步目录,可选转换器"""
|
||||
for file in Path(source).glob("*.md"):
|
||||
target_file = Path(target) / file.name
|
||||
if not target_file.exists():
|
||||
if converter:
|
||||
content = converter(file.read_text())
|
||||
target_file.write_text(content)
|
||||
else:
|
||||
shutil.copy(file, target_file)
|
||||
print(f"Synced {file.name}")
|
||||
```
|
||||
|
||||
### 4.3 冲突解决策略
|
||||
|
||||
| 冲突类型 | 策略 | 优先级 |
|
||||
|---------|------|--------|
|
||||
| 文件同名 | 添加后缀 `_from_homewiki` | 目标仓库 |
|
||||
| Frontmatter 格式 | 转换为目标仓库格式 | 目标仓库 |
|
||||
| 标签冲突 | 保留目标仓库标签,融合源仓库标签 | 目标仓库 |
|
||||
| Wikilink 断链 | 创建占位页面或删除链接 | 目标仓库 |
|
||||
| claims 格式 | 转换为 `[raw:文件名:行号]` | 目标仓库 |
|
||||
|
||||
---
|
||||
|
||||
## 五、风险与对策
|
||||
|
||||
### 5.1 风险识别
|
||||
|
||||
| 风险 | 概率 | 影响 | 对策 |
|
||||
|------|------|------|------|
|
||||
| **格式冲突** | 高 | 中 | 提前创建转换器,自动化处理 |
|
||||
| **内容重复** | 中 | 低 | 去重脚本,哈希值比对 |
|
||||
| **断链风险** | 中 | 高 | Lint 检查,反向链接补充 |
|
||||
| **数据丢失** | 低 | 高 | Git 备份,增量同步 |
|
||||
| **同步失败** | 中 | 中 | 错误日志,回滚机制 |
|
||||
|
||||
### 5.2 回滚机制
|
||||
|
||||
```powershell
|
||||
# Git 回滚命令
|
||||
git log --oneline -10 # 查看最近 10 次提交
|
||||
git reset --hard HEAD~1 # 回滚到上一次提交
|
||||
git reflog # 查看所有提交历史
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、成功标准
|
||||
|
||||
### 6.1 定量指标
|
||||
|
||||
| 指标 | 目标值 | 测量方法 |
|
||||
|------|--------|---------|
|
||||
| 概念页同步率 | 100% (12/12) | `Get-ChildItem wiki\*_from_homewiki.md | Measure-Object` |
|
||||
| 实体页同步率 | 100% (~5/~5) | 人工检查 + grep |
|
||||
| raw/ 去重率 | 100% | 哈希值比对 |
|
||||
| Lint 错误率 | 0% | 运行 lint 脚本 |
|
||||
| Wikilink 断链数 | 0 | 运行断链检测 |
|
||||
|
||||
### 6.2 定性标准
|
||||
|
||||
- [ ] 所有同步内容符合目标仓库 Frontmatter 规范
|
||||
- [ ] 所有 claims 正确转换为 `[raw:文件名:行号]` 格式
|
||||
- [ ] 所有 Wikilink 可正常跳转
|
||||
- [ ] 索引页面更新完成,包含新内容
|
||||
- [ ] 日志记录完整,可追溯操作历史
|
||||
|
||||
---
|
||||
|
||||
## 七、时间表
|
||||
|
||||
| 阶段 | 任务 | 时间 | 产出 |
|
||||
|------|------|------|------|
|
||||
| Week 1 | 准备工作(映射表、转换器) | 5 天 | `sync-mapping.json`, `convert-frontmatter.py` |
|
||||
| Week 2 | 核心内容同步(概念、实体) | 5 天 | 12 概念页 + ~5 实体页 |
|
||||
| Week 3 | 原始来源同步(raw/ 去重、claims 转换) | 5 天 | 48 raw 文件,claims 转换 |
|
||||
| Week 4 | 其他内容同步(Daily, Templates, Tools) | 5 天 | Daily, Templates, Tools 同步完成 |
|
||||
| Week 5 | 验证与优化(Lint, 交叉引用, 日志) | 5 天 | Lint 通过,索引更新,日志追加 |
|
||||
|
||||
**总计**:5 周(35 个工作日)
|
||||
|
||||
---
|
||||
|
||||
## 八、后续维护
|
||||
|
||||
### 8.1 日常维护任务
|
||||
|
||||
- **每日**:运行 `tools/sync/daily-sync.ps1`(同步 Daily/)
|
||||
- **每周**:运行 `tools/sync/weekly-sync.py`(同步新增内容)
|
||||
- **每月**:运行 Lint 检查 + 去重 + 索引更新
|
||||
|
||||
### 8.2 定期审查
|
||||
|
||||
- **每季度**:审查同步脚本的有效性,更新映射表
|
||||
- **每半年**:评估同步策略,优化自动化程度
|
||||
- **每年**:评估仓库结构,调整同步方向(单向→双向)
|
||||
|
||||
---
|
||||
|
||||
## 九、总结
|
||||
|
||||
### 9.1 可行性结论 ✅ **高度可行**
|
||||
|
||||
1. **技术兼容**:两个仓库底层技术栈一致(Obsidian + Markdown + Git)
|
||||
2. **内容互补**:home-wiki 专注"高等教育 AI",与目标仓库高度互补
|
||||
3. **风险可控**:通过转换器、Lint 检查、回滚机制,风险可控
|
||||
4. **收益明确**:统一知识库,减少维护负担,提高知识积累效率
|
||||
|
||||
### 9.2 关键成功因素
|
||||
|
||||
1. **提前规划**:映射表、转换器、冲突处理策略
|
||||
2. **自动化脚本**:减少人工操作,降低错误率
|
||||
3. **增量同步**:避免一次性大规模迁移,降低风险
|
||||
4. **验证机制**:Lint 检查、交叉引用、日志记录
|
||||
|
||||
### 9.3 下一步行动
|
||||
|
||||
1. **立即行动**:创建 `tools/sync-mapping.json` 映射表
|
||||
2. **本周任务**:开发 `convert-frontmatter.py` 转换器
|
||||
3. **下周任务**:开始核心内容同步(概念页)
|
||||
|
||||
---
|
||||
|
||||
*本方案由 opencode 生成,日期:2026-06-30*
|
||||
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
home-wiki → kepano-obsidian-main 同步转换脚本
|
||||
|
||||
用途:将 home-wiki 的笔记内容转换为目标仓库格式并同步。
|
||||
功能:
|
||||
1. Frontmatter 格式转换
|
||||
2. OpenClaw 标记清理
|
||||
3. Wikilink 路径保持
|
||||
4. 批量处理目录
|
||||
|
||||
用法:
|
||||
python sync_homewiki.py concepts # 同步概念页
|
||||
python sync_homewiki.py entities # 同步实体页
|
||||
python sync_sync_homewiki.py all # 同步全部
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 路径配置
|
||||
SOURCE_ROOT = Path(r"D:\TC_UP\home-wiki")
|
||||
TARGET_ROOT = Path(r"D:\Applications\app\kepano-obsidian-main")
|
||||
|
||||
# 目录映射
|
||||
DIR_MAPPING = {
|
||||
"concepts": "wiki",
|
||||
"entities": "wiki",
|
||||
"syntheses": "wiki",
|
||||
}
|
||||
|
||||
# 标签映射(home-wiki tag → 目标仓库 tag)
|
||||
TAG_MAPPING = {
|
||||
"complexity": "concept/complexity",
|
||||
"systems-theory": "concept/systems-theory",
|
||||
"consciousness": "concept/consciousness",
|
||||
"physics": "concept/physics",
|
||||
"phenomenon": "concept/phenomenon",
|
||||
"theory": "concept/theory",
|
||||
"technology": "concept/technology",
|
||||
"practice": "concept/practice",
|
||||
"person": "people",
|
||||
"innovation-management": "concept/innovation",
|
||||
"methodology": "concept/methodology",
|
||||
"higher-ed": "education/higher-ed",
|
||||
"AI-education": "ai/education",
|
||||
"LLM": "llm",
|
||||
"agent": "agent",
|
||||
"RAG": "rag",
|
||||
"knowledge-graph": "knowledge-graph",
|
||||
}
|
||||
|
||||
|
||||
def parse_frontmatter(content):
|
||||
"""解析 markdown 文件的 frontmatter,返回 (yaml_text, body)"""
|
||||
match = re.match(r'^---\n(.*?)\n---\n(.*)', content, re.DOTALL)
|
||||
if not match:
|
||||
return None, content
|
||||
return match.group(1), match.group(2)
|
||||
|
||||
|
||||
def convert_frontmatter(yaml_text, source_type="concept"):
|
||||
"""将 home-wiki frontmatter 转换为目标仓库格式"""
|
||||
lines = yaml_text.split('\n')
|
||||
converted = []
|
||||
|
||||
# 提取字段
|
||||
tags = []
|
||||
sources = []
|
||||
created = None
|
||||
page_type = source_type
|
||||
subtype = None
|
||||
title = None
|
||||
aliases = []
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# type 字段
|
||||
m = re.match(r'^type:\s*(.+)', line)
|
||||
if m:
|
||||
page_type = m.group(1).strip()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# subtype 字段
|
||||
m = re.match(r'^subtype:\s*(.+)', line)
|
||||
if m:
|
||||
subtype = m.group(1).strip()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# title 字段
|
||||
m = re.match(r'^title:\s*(.+)', line)
|
||||
if m:
|
||||
title = m.group(1).strip().strip('"').strip("'")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# created 字段
|
||||
m = re.match(r'^created:\s*(.+)', line)
|
||||
if m:
|
||||
created = m.group(1).strip()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# tags 字段(行内列表格式)
|
||||
m = re.match(r'^tags:\s*\[(.*)\]', line)
|
||||
if m:
|
||||
tag_str = m.group(1)
|
||||
tags = [t.strip().strip('"').strip("'") for t in tag_str.split(',') if t.strip()]
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# tags 字段(YAML 列表格式)
|
||||
m = re.match(r'^tags:\s*$', line)
|
||||
if m:
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].strip().startswith('-'):
|
||||
tag = lines[i].strip().lstrip('-').strip()
|
||||
tags.append(tag)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# sources 字段(行内列表格式)
|
||||
m = re.match(r'^sources:\s*\[(.*)\]', line)
|
||||
if m:
|
||||
src_str = m.group(1)
|
||||
sources = [s.strip().strip('"').strip("'") for s in src_str.split(',') if s.strip()]
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# sources 字段(YAML 列表格式)
|
||||
m = re.match(r'^sources:\s*$', line)
|
||||
if m:
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].strip().startswith('-'):
|
||||
src = lines[i].strip().lstrip('-').strip().strip('"').strip("'")
|
||||
sources.append(src)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 跳过不需要的字段
|
||||
if re.match(r'^(updated|confidence|status|description|name|nameEn|born|field):\s*', line):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# claims 字段(跳过多行)
|
||||
if re.match(r'^claims:\s*', line):
|
||||
i += 1
|
||||
while i < len(lines) and (lines[i].startswith(' ') or lines[i].startswith(' -')):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
# 构建新的 frontmatter
|
||||
if not created:
|
||||
created = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
new_lines = []
|
||||
new_lines.append('---')
|
||||
new_lines.append('categories:')
|
||||
new_lines.append(' - "[[LLM Wiki]]"')
|
||||
|
||||
# tags
|
||||
new_lines.append('tags:')
|
||||
new_lines.append(' - wiki')
|
||||
if page_type == 'entity':
|
||||
new_lines.append(' - people')
|
||||
else:
|
||||
new_lines.append(' - concept')
|
||||
|
||||
if subtype:
|
||||
mapped = TAG_MAPPING.get(subtype, subtype)
|
||||
new_lines.append(f' - {mapped}')
|
||||
|
||||
for tag in tags:
|
||||
mapped = TAG_MAPPING.get(tag, tag)
|
||||
if mapped not in ['wiki', 'concept', 'people'] and mapped not in [l.strip().lstrip('-').strip() for l in new_lines[4:]]:
|
||||
new_lines.append(f' - {mapped}')
|
||||
|
||||
new_lines.append(f'created: {created}')
|
||||
|
||||
# source 字段
|
||||
if sources:
|
||||
src = sources[0]
|
||||
# 提取文件名(去掉路径)
|
||||
src_filename = Path(src).name
|
||||
if src.startswith('http'):
|
||||
new_lines.append(f'source: "{src}"')
|
||||
else:
|
||||
src_name = Path(src_filename).stem
|
||||
new_lines.append(f'source: "[[{src_name}]]"')
|
||||
|
||||
new_lines.append(f'type: {page_type}')
|
||||
|
||||
if title and title != page_type:
|
||||
new_lines.append('aliases:')
|
||||
new_lines.append(f' - {title}')
|
||||
|
||||
new_lines.append('---')
|
||||
|
||||
return '\n'.join(new_lines)
|
||||
|
||||
|
||||
def clean_body(body):
|
||||
"""清理正文中的 OpenClaw 标记和无用章节"""
|
||||
# 移除 openclaw 注释块(单行)
|
||||
body = re.sub(r'<!-- openclaw:[^>]*-->', '', body)
|
||||
|
||||
# 移除 Related 章节(openclaw 生成的)
|
||||
body = re.sub(r'## Related\s*\n<!-- openclaw:wiki:related:start -->.*?<!-- openclaw:wiki:related:end -->',
|
||||
'', body, flags=re.DOTALL)
|
||||
|
||||
# 移除 Notes 章节(openclaw 空标记)
|
||||
body = re.sub(r'## Notes\s*\n<!-- openclaw:human:start -->\s*<!-- openclaw:human:end -->\s*',
|
||||
'', body)
|
||||
|
||||
# 移除 Summary 的 openclaw 标记(保留内容)
|
||||
body = re.sub(r'<!-- openclaw:wiki:generated:start -->\s*', '', body)
|
||||
body = re.sub(r'\s*<!-- openclaw:wiki:generated:end -->', '', body)
|
||||
|
||||
# 清理多余空行
|
||||
body = re.sub(r'\n{4,}', '\n\n\n', body)
|
||||
|
||||
# 移除末尾的 "最后更新" 行(home-wiki 特有)
|
||||
body = re.sub(r'\n\*最后更新[::][^*]*\*\s*$', '', body)
|
||||
|
||||
return body.strip() + '\n'
|
||||
|
||||
|
||||
def convert_file(source_path, target_dir, source_type="concept"):
|
||||
"""转换单个文件"""
|
||||
source_path = Path(source_path)
|
||||
target_dir = Path(target_dir)
|
||||
|
||||
# 跳过 index.md
|
||||
if source_path.name == 'index.md':
|
||||
return False, f"跳过 index.md"
|
||||
|
||||
# 读取源文件
|
||||
content = source_path.read_text(encoding='utf-8')
|
||||
|
||||
# 解析 frontmatter
|
||||
yaml_text, body = parse_frontmatter(content)
|
||||
|
||||
if yaml_text is None:
|
||||
return False, f"无 frontmatter,跳过"
|
||||
|
||||
# 转换 frontmatter
|
||||
new_yaml = convert_frontmatter(yaml_text, source_type)
|
||||
|
||||
# 清理正文
|
||||
cleaned_body = clean_body(body)
|
||||
|
||||
# 组合
|
||||
new_content = new_yaml + '\n\n' + cleaned_body
|
||||
|
||||
# 写入目标
|
||||
target_path = target_dir / source_path.name
|
||||
|
||||
# 冲突检查
|
||||
if target_path.exists():
|
||||
return False, f"目标文件已存在: {target_path.name}"
|
||||
|
||||
target_path.write_text(new_content, encoding='utf-8')
|
||||
return True, f"已同步: {source_path.name}"
|
||||
|
||||
|
||||
def sync_directory(source_subdir, target_subdir, source_type="concept"):
|
||||
"""同步整个目录"""
|
||||
source_dir = SOURCE_ROOT / source_subdir
|
||||
target_dir = TARGET_ROOT / target_subdir
|
||||
|
||||
if not source_dir.exists():
|
||||
print(f"源目录不存在: {source_dir}")
|
||||
return
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"同步: {source_subdir} → {target_subdir}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
success = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
for md_file in sorted(source_dir.glob("*.md")):
|
||||
ok, msg = convert_file(md_file, target_dir, source_type)
|
||||
if ok:
|
||||
print(f" ✅ {msg}")
|
||||
success += 1
|
||||
else:
|
||||
print(f" ⏭️ {msg}")
|
||||
skipped += 1
|
||||
|
||||
print(f"\n汇总: {success} 同步, {skipped} 跳过")
|
||||
return success
|
||||
|
||||
|
||||
def sync_raw_articles():
|
||||
"""同步 raw/articles/ 的 HTML 文件"""
|
||||
source_dir = SOURCE_ROOT / "raw" / "articles"
|
||||
target_dir = TARGET_ROOT / "raw" / "homewiki-articles"
|
||||
|
||||
if not source_dir.exists():
|
||||
print(f"源目录不存在: {source_dir}")
|
||||
return 0
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"同步: raw/articles/ → raw/homewiki-articles/")
|
||||
print(f"{'='*60}")
|
||||
|
||||
count = 0
|
||||
for file in source_dir.iterdir():
|
||||
if file.is_file():
|
||||
target_file = target_dir / file.name
|
||||
if not target_file.exists():
|
||||
shutil.copy2(file, target_file)
|
||||
print(f" ✅ 已复制: {file.name}")
|
||||
count += 1
|
||||
else:
|
||||
print(f" ⏭️ 已存在: {file.name}")
|
||||
|
||||
print(f"\n汇总: {count} 文件复制")
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python sync_homewiki.py [concepts|entities|syntheses|raw|all]")
|
||||
sys.exit(1)
|
||||
|
||||
task = sys.argv[1]
|
||||
total = 0
|
||||
|
||||
if task in ('concepts', 'all'):
|
||||
total += sync_directory("concepts", "wiki", "concept")
|
||||
|
||||
if task in ('entities', 'all'):
|
||||
total += sync_directory("entities", "wiki", "entity")
|
||||
|
||||
if task in ('syntheses', 'all'):
|
||||
total += sync_directory("syntheses", "wiki", "synthesis")
|
||||
|
||||
if task in ('raw', 'all'):
|
||||
total += sync_raw_articles()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"全部完成!共同步 {total} 个文件")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user