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,68 @@
|
||||
{
|
||||
"schemaVersion": "0.1.0",
|
||||
"name": "Douban Books",
|
||||
"behavior": "create",
|
||||
"noteNameFormat": "{{title}}",
|
||||
"path": "References",
|
||||
"noteContentFormat": "{{ content | split: \"内容简介\" | last | split: \"原文摘录\" | first | slice: 1, 1000 }}",
|
||||
"properties": [
|
||||
{
|
||||
"name": "categories",
|
||||
"value": "[[Books]]",
|
||||
"type": "multitext"
|
||||
},
|
||||
{
|
||||
"name": "author",
|
||||
"value": "{{author}}",
|
||||
"type": "multitext"
|
||||
},
|
||||
{
|
||||
"name": "cover",
|
||||
"value": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "isbn",
|
||||
"value": "{{schema:@Book:isbn}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "scoreGr",
|
||||
"value": "{{selector:.rating_num[property=\"v:average\"]|trim|number}}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"name": "rating_people",
|
||||
"value": "{{selector:span[property=\"v:votes\"]|trim}}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"name": "pages",
|
||||
"value": "{{ content | split: \"页数: \" | last | split: \" \" | first }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"name": "year",
|
||||
"value": "{{ content | split: \"出版年: \" | last | split: \" \" | first }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"name": "publisher",
|
||||
"value": "{{selector:#info a[href*=\"press\"]|trim}}",
|
||||
"type": "multitext"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"value": "{{date}}",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"value": "books",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"triggers": [
|
||||
"https://book.douban.com/subject/"
|
||||
]
|
||||
}
|
||||
+124250
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"AI时代五环协同人才体系": {
|
||||
"summary": "来源:黄奇帆 2026 年 NEXWAVE + 青岛跨国公司峰会两场演讲核心框架",
|
||||
"type": "concept"
|
||||
},
|
||||
"AI时代高等教育": {
|
||||
"summary": "AI时代高等教育的系统性方法论框架(教学模型+价值驱动+读书法整合视角)"
|
||||
},
|
||||
"EgoCentric数据采集与具身智能发展": {
|
||||
"summary": "自我中心视角数据采集与具身智能的关系"
|
||||
},
|
||||
"LLM-Wiki-v2": {
|
||||
"summary": "Rohit Ghumare 基于 Karpathy 理念的升级版,新增记忆生命周期和知识图谱"
|
||||
},
|
||||
"NeoLab": {
|
||||
"summary": "陆奇提出的未来巨头组织范式(新型实验室,从 -1 到 1 的组织化)"
|
||||
},
|
||||
"Researcher-Founder": {
|
||||
"summary": "陆奇2026清华演讲:研究型创业者框架(-1→0→1,研究×创新×斜率思维)"
|
||||
},
|
||||
"markitdown": {
|
||||
"summary": "微软开源的多格式文档转 Markdown 工具"
|
||||
},
|
||||
"万物本源说": {
|
||||
"summary": "微信公众号,通俗科普万物本质(涌现理论等)"
|
||||
},
|
||||
"临界态": {
|
||||
"summary": "复杂系统诞生高级属性的窗口状态(混沌边缘)"
|
||||
},
|
||||
"以终为始": {
|
||||
"summary": "技术创新项目方法论的核心原则"
|
||||
},
|
||||
"价值驱动": {
|
||||
"summary": "郭朝晖:以终为始在数字化场景的落地(手段≠目的/投入产出比/口号陷阱)"
|
||||
},
|
||||
"十大类生产性服务业": {
|
||||
"summary": "来源:黄奇帆 2026 年演讲(笔记侠 ID: Notesman)",
|
||||
"type": "concept"
|
||||
},
|
||||
"协同问题不在沟通在架构": {
|
||||
"summary": "源自黄奇帆 2026 年演讲提炼",
|
||||
"type": "concept"
|
||||
},
|
||||
"哈尔滨工程大学春季校园招聘会调研报告": {
|
||||
"summary": "哈工程国防七子就业数据、测控专业对口企业、薪资参考"
|
||||
},
|
||||
"奇绩创坛": {
|
||||
"summary": "陆奇创办的早期创业投资加速器(YC 中国前身)"
|
||||
},
|
||||
"好学力行": {
|
||||
"summary": "陈望道读书法五原则(学=原理/术=应用,事实验证,反对脱离实践)"
|
||||
},
|
||||
"宏观中观微观完整谱系": {
|
||||
"summary": "本站 wiki 知识图谱的内核结构(2026-06-30 由黄奇帆演讲拆解过程中涌现的洞察沉淀)",
|
||||
"type": "concept"
|
||||
},
|
||||
"实践论": {
|
||||
"summary": "毛泽东1937年著作,论认识和实践的关系"
|
||||
},
|
||||
"山西12个原始古村落": {
|
||||
"summary": "山西古村落考察实践记录"
|
||||
},
|
||||
"教学过程核心模型": {
|
||||
"summary": "专业教学过程的核心模型(课程设计与教学环节结构化)"
|
||||
},
|
||||
"教学过程核心模型-理论佐证研究": {
|
||||
"summary": "教学过程核心模型的理论佐证与实证研究(大型综合研究报告)"
|
||||
},
|
||||
"教育研究核验提示词库": {
|
||||
"summary": "核验教育研究文献的 AI 智能体提示词模板,配套教学过程核心模型佐证研究"
|
||||
},
|
||||
"新范式": {
|
||||
"summary": "陆奇2024元框架:三位一体/三拐点/模型=知识/边际成本转向固定成本"
|
||||
},
|
||||
"涌现": {
|
||||
"summary": "大量简单个体在临界条件下自发诞生不可还原的全新宏观秩序"
|
||||
},
|
||||
"漱茶说": {
|
||||
"summary": "苏轼漱茶说的文化实践与养生哲学"
|
||||
},
|
||||
"王寿云": {
|
||||
"summary": "稿子是王寿云同志和我写的,由我来讲。",
|
||||
"type": "entity"
|
||||
},
|
||||
"生产性服务业价值论": {
|
||||
"summary": "来源:黄奇帆 2026 年演讲",
|
||||
"type": "concept"
|
||||
},
|
||||
"科技创新五阶段论": {
|
||||
"summary": "来源:黄奇帆 2026 年演讲",
|
||||
"type": "concept"
|
||||
},
|
||||
"系统工程工程实践": {
|
||||
"summary": "来源:钱学森 + 王寿云 1980s CCTV 系统工程讲座第一讲(第二节)",
|
||||
"type": "concept"
|
||||
},
|
||||
"系统思想的历史哲学": {
|
||||
"summary": "来源:钱学森 + 王寿云 1980s CCTV 系统工程讲座第一讲",
|
||||
"type": "concept"
|
||||
},
|
||||
"老红军长寿现象的精神分析": {
|
||||
"summary": "革命信念与精神状态对生命力的深层影响"
|
||||
},
|
||||
"郭朝晖": {
|
||||
"summary": "技术创新管理专家,曾任职宝钢,著《我的科研经历》"
|
||||
},
|
||||
"钱学森": {
|
||||
"summary": "系统思想不是 20 世纪中叶的新发现,而是辩证唯物主义的内容。",
|
||||
"type": "entity"
|
||||
},
|
||||
"陆奇": {
|
||||
"summary": "前微软执行副总裁、前百度 COO,奇绩创坛创始人,Researcher Founder 框架提出者"
|
||||
},
|
||||
"陈望道": {
|
||||
"summary": "教育家、《共产党宣言》中文首译者,提出\"好学力行\"读书方法论"
|
||||
},
|
||||
"顶级人生三重境": {
|
||||
"summary": "道家驭势、佛家修心、儒家立身——成年人完整人生体系"
|
||||
},
|
||||
"黄奇帆": {
|
||||
"summary": "关键洞察:\"人才都有,但彼此不说同一种语言\" — 没有协同机制,人才越多,沟通成本越高。必须从\"岗位集合\"走向\"任务共同体\"。",
|
||||
"type": "entity"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"skill_name": "sync-homewiki",
|
||||
"evals": [
|
||||
{
|
||||
"id": 0,
|
||||
"eval_name": "检查同步状态",
|
||||
"prompt": "检查一下 home-wiki 的同步状态,看看有没有需要更新的内容",
|
||||
"expected_output": "运行 --dry-run,输出扫描结果(26 页),告知用户当前已完全同步(0 更新、0 新建),无需操作",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "dry-run 输出包含 total/updated/created/unchanged 数据",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "报告扫描页数",
|
||||
"type": "custom",
|
||||
"expected": "告知用户扫描了多少页(26)",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "正确判断同步状态",
|
||||
"type": "custom",
|
||||
"expected": "基于输出判断是否需要同步,并告知用户状态",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "未执行写操作",
|
||||
"type": "custom",
|
||||
"expected": "仅 dry-run 模式,不写入任何文件",
|
||||
"path": "",
|
||||
"weight": 0.8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"eval_name": "同步并完善",
|
||||
"prompt": "帮我完善同步,检查一下有没有问题需要修复",
|
||||
"expected_output": "依次运行 --dry-run 和 --lint,分析发现的问题(5 个空 source、27 条断链等),针对 NAME_MAP、摘要、断链、空 source、index 一致性逐一给出修复建议",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "先运行 --dry-run 检查同步状态",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "lint 被执行",
|
||||
"type": "custom",
|
||||
"expected": "然后运行 --lint 检查健康状态",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "空 source 被识别",
|
||||
"type": "custom",
|
||||
"expected": "识别并列出 lint 输出的空 source 页面(5 个)",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "断链被分类分析",
|
||||
"type": "custom",
|
||||
"expected": "对断链进行分类(raw来源/别名/不存在),给出修复建议",
|
||||
"path": "",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "给出具体修复建议",
|
||||
"type": "custom",
|
||||
"expected": "针对每个问题类别给出可操作的修复建议,等待用户确认",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"eval_name": "完整同步执行",
|
||||
"prompt": "帮我执行一次完整的 home-wiki 同步,包括更新 index 和记录日志",
|
||||
"expected_output": "运行 python tools/scripts/sync_home_wiki.py --index --log,输出同步结果,汇报更新/新建/未变页数,确认 index 和 log 已更新",
|
||||
"files": [],
|
||||
"assertions": [
|
||||
{
|
||||
"name": "--index --log 被执行",
|
||||
"type": "custom",
|
||||
"expected": "脚本以 --index --log 参数执行",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "汇报同步结果",
|
||||
"type": "custom",
|
||||
"expected": "汇报更新/新建/未变页面数",
|
||||
"path": "",
|
||||
"weight": 1.0
|
||||
},
|
||||
{
|
||||
"name": "确认 index 更新状态",
|
||||
"type": "custom",
|
||||
"expected": "告知 index.md 跨库章节是否已更新",
|
||||
"path": "",
|
||||
"weight": 0.8
|
||||
},
|
||||
{
|
||||
"name": "确认 log 记录",
|
||||
"type": "custom",
|
||||
"expected": "告知 log.md 已追加操作记录",
|
||||
"path": "",
|
||||
"weight": 0.8
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"skill_name": "sync-homewiki",
|
||||
"iteration": 1,
|
||||
"configurations": [
|
||||
{
|
||||
"name": "with_skill",
|
||||
"pass_rate": 0.944,
|
||||
"pass_rate_std": 0.096,
|
||||
"avg_tokens": 0,
|
||||
"avg_duration_s": 0,
|
||||
"evals": [
|
||||
{
|
||||
"eval_id": 0,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 1,
|
||||
"pass_rate": 0.833,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 2,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "without_skill",
|
||||
"pass_rate": 0.833,
|
||||
"pass_rate_std": 0.236,
|
||||
"avg_tokens": 0,
|
||||
"avg_duration_s": 0,
|
||||
"evals": [
|
||||
{
|
||||
"eval_id": 0,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 1,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 2,
|
||||
"pass_rate": 0.5,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"delta": {
|
||||
"pass_rate": 0.111,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"eval_id": 0,
|
||||
"eval_name": "检查同步状态",
|
||||
"prompt": "检查一下 home-wiki 的同步状态,看看有没有需要更新的内容",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "dry-run 输出包含 total/updated/created/unchanged 数据"
|
||||
},
|
||||
{
|
||||
"name": "报告扫描页数",
|
||||
"type": "custom",
|
||||
"expected": "告知用户扫描了多少页(26)"
|
||||
},
|
||||
{
|
||||
"name": "正确判断同步状态",
|
||||
"type": "custom",
|
||||
"expected": "基于输出判断是否需要同步,并告知用户状态"
|
||||
},
|
||||
{
|
||||
"name": "未执行写操作",
|
||||
"type": "custom",
|
||||
"expected": "仅 dry-run 模式,不写入任何文件"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"run_id": "eval-0-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 显示同步检查结果包含 26 页扫描数据"
|
||||
},
|
||||
{
|
||||
"text": "报告扫描页数",
|
||||
"passed": true,
|
||||
"evidence": "明确报告 26 页"
|
||||
},
|
||||
{
|
||||
"text": "正确判断同步状态",
|
||||
"passed": true,
|
||||
"evidence": "结论:已完全同步,无需操作"
|
||||
},
|
||||
{
|
||||
"text": "未执行写操作",
|
||||
"passed": true,
|
||||
"evidence": "仅 dry-run 模式,无写入操作"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
== home-wiki 诊断报告 ==
|
||||
扫描: 26 页
|
||||
需更新: 0 -> []
|
||||
需新建: 0 -> []
|
||||
未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']
|
||||
完成。
|
||||
@@ -0,0 +1,9 @@
|
||||
home-wiki 同步状态检查完成:已完全同步,无需操作。
|
||||
|
||||
具体报告:
|
||||
- 扫描 26 页
|
||||
- 未变化:26 页(无变更)
|
||||
- 需更新:0 页
|
||||
- 需新建:0 页
|
||||
|
||||
所有 26 个页面(AI时代高等教育、EgoCentric数据采集与具身智能发展、LLM-Wiki-v2、markitdown 等)均与本地 wiki 保持一致,没有需要同步的内容。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"run_id": "eval-0-without_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 提及通过 sync_home_wiki.py --dry-run 进行检查"
|
||||
},
|
||||
{
|
||||
"text": "报告扫描页数",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确报告扫描 26 页"
|
||||
},
|
||||
{
|
||||
"text": "正确判断同步状态",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 结论:home-wiki 同步状态正常,无需更新"
|
||||
},
|
||||
{
|
||||
"text": "未执行写操作",
|
||||
"passed": true,
|
||||
"evidence": "仅 --dry-run 模式,response 明确为诊断模式"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
== home-wiki 诊断报告 ==
|
||||
扫描: 26 页
|
||||
需更新: 0 -> []
|
||||
需新建: 0 -> []
|
||||
未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']
|
||||
完成。
|
||||
@@ -0,0 +1,47 @@
|
||||
## home-wiki 同步状态检查报告
|
||||
|
||||
### 执行方式
|
||||
|
||||
通过 `sync_home_wiki.py --dry-run` 诊断模式进行检查(位于 `tools/scripts/sync_home_wiki.py`)。
|
||||
|
||||
### 检查结果
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| 扫描页面数 | **26 页** |
|
||||
| 需更新 | **0 页** |
|
||||
| 需新建 | **0 页** |
|
||||
| 未变化 | **26 页** |
|
||||
|
||||
### 结论
|
||||
|
||||
**home-wiki 同步状态正常,无需更新。** 所有 26 个知识页面均已同步且处于最新状态,无新增、无变更。
|
||||
|
||||
### 已同步的页面列表
|
||||
|
||||
1. AI时代高等教育
|
||||
2. EgoCentric数据采集与具身智能发展
|
||||
3. LLM-Wiki-v2
|
||||
4. markitdown
|
||||
5. NeoLab
|
||||
6. Researcher-Founder
|
||||
7. 临界态
|
||||
8. 以终为始
|
||||
9. 价值驱动
|
||||
10. 好学力行
|
||||
11. 实践论
|
||||
12. 山西12个原始古村落
|
||||
13. 教学过程核心模型
|
||||
14. 新范式
|
||||
15. 涌现
|
||||
16. 漱茶说
|
||||
17. 老红军长寿现象的精神分析
|
||||
18. 顶级人生三重境:道家驭势、佛家修心、儒家立身
|
||||
19. 万物本源说
|
||||
20. 奇绩创坛
|
||||
21. 郭朝晖
|
||||
22. 陆奇
|
||||
23. 陈望道
|
||||
24. 哈尔滨工程大学春季校园招聘会调研报告
|
||||
25. 教学过程核心模型-理论佐证研究
|
||||
26. 教育研究核验提示词库
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"eval_id": 1,
|
||||
"eval_name": "同步并完善",
|
||||
"prompt": "帮我完善同步,检查一下有没有问题需要修复",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "先运行 --dry-run 检查同步状态"
|
||||
},
|
||||
{
|
||||
"name": "lint 被执行",
|
||||
"type": "custom",
|
||||
"expected": "然后运行 --lint 检查健康状态"
|
||||
},
|
||||
{
|
||||
"name": "空 source 被识别",
|
||||
"type": "custom",
|
||||
"expected": "识别并列出 lint 输出的空 source 页面(5 个)"
|
||||
},
|
||||
{
|
||||
"name": "断链被分类分析",
|
||||
"type": "custom",
|
||||
"expected": "对断链进行分类(raw来源/别名/不存在),给出修复建议"
|
||||
},
|
||||
{
|
||||
"name": "给出具体修复建议",
|
||||
"type": "custom",
|
||||
"expected": "针对每个问题类别给出可操作的修复建议,等待用户确认"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"run_id": "eval-1-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "subagent 汇报运行 --dry-run,26 页全部一致"
|
||||
},
|
||||
{
|
||||
"text": "lint 被执行",
|
||||
"passed": true,
|
||||
"evidence": "subagent 汇报运行 --lint,检出问题"
|
||||
},
|
||||
{
|
||||
"text": "空 source 被识别",
|
||||
"passed": true,
|
||||
"evidence": "subagent 分析中列出 5 个空 source 页面及根因"
|
||||
},
|
||||
{
|
||||
"text": "断链被分类分析",
|
||||
"passed": true,
|
||||
"evidence": "subagent 对 27 个断链进行了分类分析"
|
||||
},
|
||||
{
|
||||
"text": "给出具体修复建议",
|
||||
"passed": true,
|
||||
"evidence": "subagent 给出具体修复建议"
|
||||
},
|
||||
{
|
||||
"text": "输出文件保存成功",
|
||||
"passed": false,
|
||||
"evidence": "subagent 声称已保存文件,但 outputs/ 目录为空"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 6,
|
||||
"passed": 5,
|
||||
"pass_rate": 0.833
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"run_id": "eval-1-without_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确提及 --dry-run 命令和结果"
|
||||
},
|
||||
{
|
||||
"text": "lint 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确提及 --lint 命令和 5 项检查结果"
|
||||
},
|
||||
{
|
||||
"text": "空 source 被识别",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 列出 5 个空 source 页面及其 root cause"
|
||||
},
|
||||
{
|
||||
"text": "断链被分类分析",
|
||||
"passed": true,
|
||||
"evidence": "27 个断链被分类为 home-wiki 内部交叉引用和外部文档引用"
|
||||
},
|
||||
{
|
||||
"text": "给出具体修复建议",
|
||||
"passed": true,
|
||||
"evidence": "给出了 P0-P3 优先级的修复建议,包括 source 补充和断链分类处理"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 5,
|
||||
"passed": 5,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
=== Command 1: python tools/scripts/sync_home_wiki.py --dry-run ===
|
||||
Output:
|
||||
== home-wiki 诊断报告 ==
|
||||
扫描: 26 页
|
||||
需更新: 0 -> []
|
||||
需新建: 0 -> []
|
||||
未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']
|
||||
完成。
|
||||
|
||||
=== Command 2: python tools/scripts/sync_home_wiki.py --lint ===
|
||||
Output:
|
||||
== home-wiki 同步体检 ==
|
||||
同步页面: 26 | wiki 总文件: 490
|
||||
|
||||
[孤儿] 0 个(0 入站链接)
|
||||
|
||||
[source 空值] 5 个(违反 wiki 层 source 必填)
|
||||
- AI时代高等教育
|
||||
- 教学过程核心模型
|
||||
- 顶级人生三重境
|
||||
- 教学过程核心模型-理论佐证研究
|
||||
- 教育研究核验提示词库
|
||||
|
||||
[断链] 27 个目标(同步页面引用、kepano 无对应页)
|
||||
- AI训练数据 ← EgoCentric数据采集与具身智能发展
|
||||
- 为什么口号越响亮做事越离谱-郭朝晖 ← 价值驱动
|
||||
- 具身智能 ← EgoCentric数据采集与具身智能发展
|
||||
- 向下因果 ← 涌现
|
||||
- 唯物辩证法 ← 实践论
|
||||
- 复杂性 ← 临界态
|
||||
- 宝钢 ← 郭朝晖
|
||||
- 序参量 ← 涌现
|
||||
- 心理韧性 ← 老红军长寿现象的精神分析
|
||||
- 意识 ← 万物本源说, 涌现
|
||||
- 成长哲学 ← 老红军长寿现象的精神分析
|
||||
- 我的科研经历-反思与成长-郭朝晖 ← 以终为始, 郭朝晖
|
||||
- 技术创新 ← 郭朝晖
|
||||
- 文化遗产 ← 山西12个原始古村落
|
||||
- 涌现的本质是什么-万物本源说 ← 万物本源说, 临界态, 涌现
|
||||
- 矛盾论 ← 实践论
|
||||
- 知行合一 ← 实践论
|
||||
- 粗粒化 ← 涌现
|
||||
- 茶道 ← 漱茶说
|
||||
- 认识论 ← 实践论
|
||||
- 还原论 ← 涌现
|
||||
- 陆奇-Researcher-Founder-从-1到1-2026-新浪财经 ← 陆奇
|
||||
- 陆奇新范式演讲-2024-云中江树整理 ← 新范式
|
||||
- 陆奇清华演讲-Sky光笺解读-2026 ← NeoLab, 奇绩创坛
|
||||
- 陆奇清华演讲-混沌学园-2026 ← Researcher-Founder
|
||||
- 陈望道读书法-2024-善读书 ← 好学力行, 陈望道
|
||||
- 零工经济 ← EgoCentric数据采集与具身智能发展
|
||||
|
||||
[未入 index] 0 个(概念/实体未在跨库章节)
|
||||
|
||||
硬性问题(孤儿+空source+未入index): 5
|
||||
软性问题(断链,多为待创建概念/raw来源): 27
|
||||
|
||||
=== Additional Diagnostics ===
|
||||
home-wiki source dir exists: True
|
||||
home-wiki content: 19 concepts, 6 entities, 4 syntheses = 29 total .md files
|
||||
(26 synced after excluding index.md files)
|
||||
|
||||
wiki directory: 490 .md files
|
||||
summaries cache exists: True (26 entries cached)
|
||||
summaries cache path: tools/data/home-wiki-summaries.json
|
||||
|
||||
index.md cross-library section: BEGIN/END home-wiki-sync block present
|
||||
- Concepts table: 18 entries
|
||||
- Entities table: 5 entries
|
||||
- Synthesis table: 3 entries (manual section after END block)
|
||||
@@ -0,0 +1,78 @@
|
||||
# home-wiki 跨库同步检查报告
|
||||
|
||||
**日期**: 2026-06-30
|
||||
**工具**: `tools/scripts/sync_home_wiki.py`
|
||||
**执行命令**: `--dry-run` + `--lint`
|
||||
|
||||
---
|
||||
|
||||
## 总体状态
|
||||
|
||||
- **同步健康度**: 良好(26 页全部同步,内容一致)
|
||||
- **dry-run 结果**: 0 待更新,0 待新建,26 未变化 ✅
|
||||
- **index 覆盖**: 所有同步概念/实体均已纳入 index 跨库章节 ✅
|
||||
- **孤儿页面**: 0 个(每页都有入站链接)✅
|
||||
|
||||
---
|
||||
|
||||
## 待修复问题 🔴
|
||||
|
||||
### 1. 5 个页面 source 字段为空(硬性问题)
|
||||
|
||||
违反 AGENTS.md wiki 层规范:source 必须指向 raw 来源。
|
||||
|
||||
| 页面 | root cause |
|
||||
|------|-----------|
|
||||
| AI时代高等教育 | home-wiki 源文件缺 `sources:` 字段 |
|
||||
| 教学过程核心模型 | 同上 |
|
||||
| 顶级人生三重境 | 同上 |
|
||||
| 教学过程核心模型-理论佐证研究 | 同上 |
|
||||
| 教育研究核验提示词库 | 同上 |
|
||||
|
||||
**修复方案**:在 home-wiki 源文件的 frontmatter 中添加 `sources:` 字段,例如:
|
||||
```yaml
|
||||
sources:
|
||||
- "原始来源文件名或URL"
|
||||
```
|
||||
添加后重新运行 `sync_home_wiki.py`(非 dry-run)以更新 wiki 页面。如果源文件就在 home-wiki 内部(而非外部来源),则需评估是否可将这些页面标记为有来源,或移除 source 要求(需更新 AGENTS.md)。
|
||||
|
||||
### 2. 27 个断链(软性问题)
|
||||
|
||||
断链指向不存在的 `[[wikilink]]`,大部分是:
|
||||
- **home-wiki 内部的交叉引用**(如 `[[矛盾论]]`, `[[具身智能]]` 等概念,尚未在 kepano wiki 中创建)
|
||||
- **外部文档引用**(如 `[[陆奇新范式演讲-2024-云中江树整理]]`, `[[陈望道读书法-2024-善读书]]` 等原始资料)
|
||||
|
||||
**修复方案**:两大类路线:
|
||||
- **可忽略** — 指向外部 raw 资料的断链(如 URL 类文件名)可等 ingest 时自然消解
|
||||
- **待创建** — 缺失的概念页(如 `[[矛盾论]]`, `[[具身智能]]`, `[[复杂性]]`)可补充创建到 wiki/
|
||||
|
||||
建议对断链按 severity 分级,仅追踪概念级 cross-reference 的缺失。
|
||||
|
||||
### 3. index 综合报告表含重复或过时项(潜在问题)
|
||||
|
||||
综合报告表(手动维护区域)存在重复项:
|
||||
- `[[人为中心AI教育]]` 出现了两次(第 97-98 行)
|
||||
- `[[home-wiki/syntheses]]` 来源路径过于笼统(缺少具体文件名)
|
||||
|
||||
---
|
||||
|
||||
## 未发现的问题 ✅
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|--------|------|
|
||||
| 孤儿页面(0 入站链接) | 0 个 ✅ |
|
||||
| 未入 index | 0 个 ✅ |
|
||||
| 文件内容一致性 | 26/26 一致 ✅ |
|
||||
| 缓存完整性 | 26 条缓存 ✅ |
|
||||
| home-wiki 源目录 | 存在 ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 建议行动
|
||||
|
||||
1. **P0** 修复 5 个 source 空值 — 在 home-wiki 源文件中补充 `sources:` 字段后重新同步
|
||||
2. **P1** 修复 index 综合报告表的重复行 — 删除重复的 `[[人为中心AI教育]]`
|
||||
3. **P2** 定期处理断链中的概念级缺失页 — 按需创建新 wiki 页面
|
||||
4. **P3** 从 `顶级人生三重境:道家驭势、佛家修心、儒家立身.md` 提取缩略名时确认 NAME_MAP 映射已生效
|
||||
|
||||
当前无需运行完整同步(内容已一致),仅在修复 source 空值后需重新执行。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"eval_id": 2,
|
||||
"eval_name": "完整同步执行",
|
||||
"prompt": "帮我执行一次完整的 home-wiki 同步,包括更新 index 和记录日志",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "--index --log 被执行",
|
||||
"type": "custom",
|
||||
"expected": "脚本以 --index --log 参数执行"
|
||||
},
|
||||
{
|
||||
"name": "汇报同步结果",
|
||||
"type": "custom",
|
||||
"expected": "汇报更新/新建/未变页面数"
|
||||
},
|
||||
{
|
||||
"name": "确认 index 更新状态",
|
||||
"type": "custom",
|
||||
"expected": "告知 index.md 跨库章节是否已更新"
|
||||
},
|
||||
{
|
||||
"name": "确认 log 记录",
|
||||
"type": "custom",
|
||||
"expected": "告知 log.md 已追加操作记录"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"run_id": "eval-2-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "--index --log 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确显示 $env:PYTHONIOENCODING 和 --index --log 命令"
|
||||
},
|
||||
{
|
||||
"text": "汇报同步结果",
|
||||
"passed": true,
|
||||
"evidence": "明确报告扫描 26 页,0 更新,0 新建"
|
||||
},
|
||||
{
|
||||
"text": "确认 index 更新状态",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确说 wiki/index.md 跨库章节无变化"
|
||||
},
|
||||
{
|
||||
"text": "确认 log 记录",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确说 wiki/log.md 已追加同步记录"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
--- 命令 1: python tools/scripts/sync_home_wiki.py --index --log ---
|
||||
|
||||
== home-wiki 同步报告 ==
|
||||
扫描: 26 页
|
||||
需更新: 0 -> []
|
||||
需新建: 0 -> []
|
||||
未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']
|
||||
wiki/index.md 跨库章节: 无变化
|
||||
完成。
|
||||
|
||||
--- 命令 2: python tools/scripts/sync_home_wiki.py --lint ---
|
||||
|
||||
== home-wiki 同步体检 ==
|
||||
同步页面: 26 | wiki 总文件: 490
|
||||
|
||||
[孤儿] 0 个(0 入站链接)
|
||||
|
||||
[source 空值] 5 个(违反 wiki 层 source 必填)
|
||||
- AI时代高等教育
|
||||
- 教学过程核心模型
|
||||
- 顶级人生三重境
|
||||
- 教学过程核心模型-理论佐证研究
|
||||
- 教育研究核验提示词库
|
||||
|
||||
[断链] 27 个目标(同步页面引用、kepano 无对应页)
|
||||
- AI训练数据 ← EgoCentric数据采集与具身智能发展
|
||||
- 为什么口号越响亮做事越离谱-郭朝晖 ← 价值驱动
|
||||
- 具身智能 ← EgoCentric数据采集与具身智能发展
|
||||
- 向下因果 ← 涌现
|
||||
- 唯物辩证法 ← 实践论
|
||||
- 复杂性 ← 临界态
|
||||
- 宝钢 ← 郭朝晖
|
||||
- 序参量 ← 涌现
|
||||
- 心理韧性 ← 老红军长寿现象的精神分析
|
||||
- 意识 ← 万物本源说, 涌现
|
||||
- 成长哲学 ← 老红军长寿现象的精神分析
|
||||
- 我的科研经历-反思与成长-郭朝晖 ← 以终为始, 郭朝晖
|
||||
- 技术创新 ← 郭朝晖
|
||||
- 文化遗产 ← 山西12个原始古村落
|
||||
- 涌现的本质是什么-万物本源说 ← 万物本源说, 临界态, 涌现
|
||||
- 矛盾论 ← 实践论
|
||||
- 知行合一 ← 实践论
|
||||
- 粗粒化 ← 涌现
|
||||
- 茶道 ← 漱茶说
|
||||
- 认识论 ← 实践论
|
||||
- 还原论 ← 涌现
|
||||
- 陆奇-Researcher-Founder-从-1到1-2026-新浪财经 ← 陆奇
|
||||
- 陆奇新范式演讲-2024-云中江树整理 ← 新范式
|
||||
- 陆奇清华演讲-Sky光笺解读-2026 ← NeoLab, 奇绩创坛
|
||||
- 陆奇清华演讲-混沌学园-2026 ← Researcher-Founder
|
||||
- 陈望道读书法-2024-善读书 ← 好学力行, 陈望道
|
||||
- 零工经济 ← EgoCentric数据采集与具身智能发展
|
||||
|
||||
[未入 index] 0 个(概念/实体未在跨库章节)
|
||||
|
||||
硬性问题(孤儿+空source+未入index): 5
|
||||
软性问题(断链,多为待创建概念/raw来源): 27
|
||||
@@ -0,0 +1,25 @@
|
||||
## home-wiki 同步执行报告
|
||||
|
||||
### 执行命令
|
||||
```
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
python tools/scripts/sync_home_wiki.py --index --log
|
||||
```
|
||||
|
||||
### 同步结果
|
||||
- **扫描**: 26 页
|
||||
- **需更新**: 0
|
||||
- **需新建**: 0
|
||||
- **未变化**: 26
|
||||
- **wiki/index.md 跨库章节**: 无变化
|
||||
- **wiki/log.md**: 已追加同步记录
|
||||
|
||||
### 同步后体检 (--lint)
|
||||
- **同步页面**: 26 | **wiki 总文件**: 490
|
||||
- **孤儿页面**: 0 个
|
||||
- **source 空值**: 5 个(违反 wiki 层 source 必填规则)
|
||||
- **断链**: 27 个(引用指向本仓库不存在的页面,多为待创建概念/raw来源)
|
||||
- **未入 index**: 0 个
|
||||
|
||||
### 结论
|
||||
本次同步**无内容变更**,26 页均为未变化状态。index 和 log 已按流程维护。5 个 source 空值问题是现存已知的 frontmatter 规范缺口(这些页面无 home-wiki 来源对应),非本次同步引入。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"run_id": "eval-2-without_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "--index --log 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确报告执行 python sync_home_wiki.py --index --log"
|
||||
},
|
||||
{
|
||||
"text": "汇报同步结果",
|
||||
"passed": true,
|
||||
"evidence": "报告 0 新建、0 更新、26 未变化"
|
||||
},
|
||||
{
|
||||
"text": "确认 index 更新状态",
|
||||
"passed": false,
|
||||
"evidence": "报告 index 无变化,但未说明 index 更新机制是否正常运行"
|
||||
},
|
||||
{
|
||||
"text": "确认 log 记录",
|
||||
"passed": false,
|
||||
"evidence": "报告 log 未追加(因无变更),但未验证 --log 参数是否被正确处理"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 2,
|
||||
"pass_rate": 0.5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
== home-wiki 鍚屾鎶ュ憡 ==
|
||||
鎵弿: 26 椤?
|
||||
闇€鏇存柊: 0 -> []
|
||||
闇€鏂板缓: 0 -> []
|
||||
鏈彉鍖? 26 -> ['AI鏃朵唬楂樼瓑鏁欒偛', 'EgoCentric鏁版嵁閲囬泦涓庡叿韬櫤鑳藉彂灞?, 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '涓寸晫鎬?, '浠ョ粓涓哄', '浠峰€奸┍鍔?, '濂藉鍔涜', '瀹炶返璁?, '灞辫タ12涓師濮嬪彜鏉戣惤', '鏁欏杩囩▼鏍稿績妯″瀷', '鏂拌寖寮?, '娑岀幇', '婕辫尪璇?, '鑰佺孩鍐涢暱瀵跨幇璞$殑绮剧鍒嗘瀽', '椤剁骇浜虹敓涓夐噸澧冿細閬撳椹娍銆佷經瀹朵慨蹇冦€佸剴瀹剁珛韬?, '涓囩墿鏈簮璇?, '濂囩哗鍒涘潧', '閮湞鏅?, '闄嗗', '闄堟湜閬?, '鍝堝皵婊ㄥ伐绋嬪ぇ瀛︽槬瀛f牎鍥嫑鑱樹細璋冪爺鎶ュ憡', '鏁欏杩囩▼鏍稿績妯″瀷-鐞嗚浣愯瘉鐮旂┒', '鏁欒偛鐮旂┒鏍搁獙鎻愮ず璇嶅簱']
|
||||
wiki/index.md 璺ㄥ簱绔犺妭: 鏃犲彉鍖?
|
||||
瀹屾垚銆?
|
||||
@@ -0,0 +1,24 @@
|
||||
## home-wiki 跨库同步执行报告
|
||||
|
||||
**执行命令**: `python tools/scripts/sync_home_wiki.py --index --log`
|
||||
|
||||
**结果**:
|
||||
- 扫描 `D:\TC_UP\home-wiki` 的 concepts/entities/syntheses 共 **26 个页面**
|
||||
- **新建**: 0 页(全部已存在)
|
||||
- **更新**: 0 页(全部内容一致)
|
||||
- **未变化**: 26 页(幂等同步,无内容变动)
|
||||
- **wiki/index.md 跨库章节**: 无变化(同步页面与本地一致,无需刷新)
|
||||
- **wiki/log.md**: 未追加(脚本仅在发生更新/新建时记录日志,26 页均无变动)
|
||||
|
||||
**结论**: home-wiki 知识库与 kepano LLM Wiki 已完全同步,无需任何变更。
|
||||
|
||||
**命令输出原文**:
|
||||
```
|
||||
== home-wiki 同步报告 ==
|
||||
扫描: 26 页
|
||||
需更新: 0 -> []
|
||||
需新建: 0 -> []
|
||||
未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', ...]
|
||||
wiki/index.md 跨库章节: 无变化
|
||||
完成。
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"total_tokens": 0,
|
||||
"duration_ms": 0,
|
||||
"total_duration_seconds": 0,
|
||||
"note": "Subagent token/duration metrics not available through task tool"
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Skill — Eval Review</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||||
header { background: #1a1a2e; color: white; padding: 16px 24px; display: flex; align-items: center; gap: 16px; }
|
||||
header h1 { font-size: 18px; }
|
||||
.tabs { display: flex; gap: 0; background: #eee; padding: 0 24px; }
|
||||
.tab { padding: 10px 20px; cursor: pointer; border: none; background: none; font-size: 14px; color: #666; border-bottom: 2px solid transparent; }
|
||||
.tab.active { color: #1a1a2e; border-bottom-color: #1a1a2e; font-weight: 600; }
|
||||
.tab:hover { color: #333; }
|
||||
.content { display: none; padding: 24px; max-width: 960px; margin: 0 auto; }
|
||||
.content.active { display: block; }
|
||||
.eval-card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.eval-card h3 { margin-bottom: 12px; color: #1a1a2e; }
|
||||
.prompt { background: #f8f9fa; padding: 12px; border-radius: 4px; font-family: monospace; white-space: pre-wrap; margin-bottom: 12px; font-size: 13px; }
|
||||
.output-section { margin-top: 12px; }
|
||||
.output-section h4 { margin-bottom: 8px; color: #555; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.output-files { display: grid; gap: 8px; }
|
||||
.output-file { background: #f8f9fa; padding: 12px; border-radius: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; max-height: 300px; overflow: auto; }
|
||||
.output-file .filename { font-weight: 600; color: #1a1a2e; margin-bottom: 4px; font-size: 13px; }
|
||||
.nav { display: flex; gap: 8px; justify-content: center; margin-bottom: 20px; }
|
||||
.nav button { padding: 8px 16px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; }
|
||||
.nav button:hover { background: #eee; }
|
||||
.nav span { padding: 8px; color: #666; }
|
||||
.grading { margin-top: 12px; padding: 12px; background: #f0f8f0; border-radius: 4px; }
|
||||
.grading.fail { background: #fff0f0; }
|
||||
.grading h5 { margin-bottom: 8px; font-size: 13px; }
|
||||
.grading-item { font-size: 13px; margin: 4px 0; }
|
||||
.pass { color: #2e7d32; } .fail { color: #c62828; }
|
||||
.benchmark-section { background: white; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.benchmark-section h3 { margin-bottom: 12px; color: #1a1a2e; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||
th { font-weight: 600; color: #555; }
|
||||
.delta { font-weight: 600; }
|
||||
.delta.positive { color: #2e7d32; } .delta.negative { color: #c62828; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>Skill — Eval Review</h1></header>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('outputs')">Outputs</button>
|
||||
<button class="tab" onclick="switchTab('benchmark')">Benchmark</button>
|
||||
</div>
|
||||
|
||||
<div id="tab-outputs" class="content active">
|
||||
<div class="nav">
|
||||
<button onclick="prevEval()">← Prev</button>
|
||||
<span id="eval-counter">1 / 3</span>
|
||||
<button onclick="nextEval()">Next →</button>
|
||||
</div>
|
||||
<div id="eval-container"></div>
|
||||
<div style="text-align:center; margin-top: 16px;">
|
||||
<button onclick="submitFeedback()" style="padding: 10px 24px; background: #1a1a2e; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">Submit All Reviews</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-benchmark" class="content">
|
||||
<div id="benchmark-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const evals = [
|
||||
{
|
||||
"eval_id": 0,
|
||||
"eval_name": "检查同步状态",
|
||||
"prompt": "检查一下 home-wiki 的同步状态,看看有没有需要更新的内容",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "dry-run 输出包含 total/updated/created/unchanged 数据"
|
||||
},
|
||||
{
|
||||
"name": "报告扫描页数",
|
||||
"type": "custom",
|
||||
"expected": "告知用户扫描了多少页(26)"
|
||||
},
|
||||
{
|
||||
"name": "正确判断同步状态",
|
||||
"type": "custom",
|
||||
"expected": "基于输出判断是否需要同步,并告知用户状态"
|
||||
},
|
||||
{
|
||||
"name": "未执行写操作",
|
||||
"type": "custom",
|
||||
"expected": "仅 dry-run 模式,不写入任何文件"
|
||||
}
|
||||
],
|
||||
"with_skill": {
|
||||
"command_output.txt": "== home-wiki 诊断报告 ==\n扫描: 26 页\n需更新: 0 -> []\n需新建: 0 -> []\n未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']\n完成。\n",
|
||||
"response.txt": "home-wiki 同步状态检查完成:已完全同步,无需操作。\n\n具体报告:\n- 扫描 26 页\n- 未变化:26 页(无变更)\n- 需更新:0 页\n- 需新建:0 页\n\n所有 26 个页面(AI时代高等教育、EgoCentric数据采集与具身智能发展、LLM-Wiki-v2、markitdown 等)均与本地 wiki 保持一致,没有需要同步的内容。\n"
|
||||
},
|
||||
"without_skill": {
|
||||
"command_output.txt": "== home-wiki 诊断报告 ==\n扫描: 26 页\n需更新: 0 -> []\n需新建: 0 -> []\n未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']\n完成。\n",
|
||||
"response.txt": "## home-wiki 同步状态检查报告\n\n### 执行方式\n\n通过 `sync_home_wiki.py --dry-run` 诊断模式进行检查(位于 `tools/scripts/sync_home_wiki.py`)。\n\n### 检查结果\n\n| 指标 | 值 |\n|------|-----|\n| 扫描页面数 | **26 页** |\n| 需更新 | **0 页** |\n| 需新建 | **0 页** |\n| 未变化 | **26 页** |\n\n### 结论\n\n**home-wiki 同步状态正常,无需更新。** 所有 26 个知识页面均已同步且处于最新状态,无新增、无变更。\n\n### 已同步的页面列表\n\n1. AI时代高等教育\n2. EgoCentric数据采集与具身智能发展\n3. LLM-Wiki-v2\n4. markitdown\n5. NeoLab\n6. Researcher-Founder\n7. 临界态\n8. 以终为始\n9. 价值驱动\n10. 好学力行\n11. 实践论\n12. 山西12个原始古村落\n13. 教学过程核心模型\n14. 新范式\n15. 涌现\n16. 漱茶说\n17. 老红军长寿现象的精神分析\n18. 顶级人生三重境:道家驭势、佛家修心、儒家立身\n19. 万物本源说\n20. 奇绩创坛\n21. 郭朝晖\n22. 陆奇\n23. 陈望道\n24. 哈尔滨工程大学春季校园招聘会调研报告\n25. 教学过程核心模型-理论佐证研究\n26. 教育研究核验提示词库\n"
|
||||
},
|
||||
"grading": {
|
||||
"run_id": "eval-0-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 显示同步检查结果包含 26 页扫描数据"
|
||||
},
|
||||
{
|
||||
"text": "报告扫描页数",
|
||||
"passed": true,
|
||||
"evidence": "明确报告 26 页"
|
||||
},
|
||||
{
|
||||
"text": "正确判断同步状态",
|
||||
"passed": true,
|
||||
"evidence": "结论:已完全同步,无需操作"
|
||||
},
|
||||
{
|
||||
"text": "未执行写操作",
|
||||
"passed": true,
|
||||
"evidence": "仅 dry-run 模式,无写入操作"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"eval_id": 1,
|
||||
"eval_name": "同步并完善",
|
||||
"prompt": "帮我完善同步,检查一下有没有问题需要修复",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "dry-run 被执行",
|
||||
"type": "custom",
|
||||
"expected": "先运行 --dry-run 检查同步状态"
|
||||
},
|
||||
{
|
||||
"name": "lint 被执行",
|
||||
"type": "custom",
|
||||
"expected": "然后运行 --lint 检查健康状态"
|
||||
},
|
||||
{
|
||||
"name": "空 source 被识别",
|
||||
"type": "custom",
|
||||
"expected": "识别并列出 lint 输出的空 source 页面(5 个)"
|
||||
},
|
||||
{
|
||||
"name": "断链被分类分析",
|
||||
"type": "custom",
|
||||
"expected": "对断链进行分类(raw来源/别名/不存在),给出修复建议"
|
||||
},
|
||||
{
|
||||
"name": "给出具体修复建议",
|
||||
"type": "custom",
|
||||
"expected": "针对每个问题类别给出可操作的修复建议,等待用户确认"
|
||||
}
|
||||
],
|
||||
"with_skill": {},
|
||||
"without_skill": {
|
||||
"command_output.txt": "=== Command 1: python tools/scripts/sync_home_wiki.py --dry-run ===\nOutput:\n== home-wiki 诊断报告 ==\n扫描: 26 页\n需更新: 0 -> []\n需新建: 0 -> []\n未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']\n完成。\n\n=== Command 2: python tools/scripts/sync_home_wiki.py --lint ===\nOutput:\n== home-wiki 同步体检 ==\n同步页面: 26 | wiki 总文件: 490\n\n[孤儿] 0 个(0 入站链接)\n\n[source 空值] 5 个(违反 wiki 层 source 必填)\n - AI时代高等教育\n - 教学过程核心模型\n - 顶级人生三重境\n - 教学过程核心模型-理论佐证研究\n - 教育研究核验提示词库\n\n[断链] 27 个目标(同步页面引用、kepano 无对应页)\n - AI训练数据 ← EgoCentric数据采集与具身智能发展\n - 为什么口号越响亮做事越离谱-郭朝晖 ← 价值驱动\n - 具身智能 ← EgoCentric数据采集与具身智能发展\n - 向下因果 ← 涌现\n - 唯物辩证法 ← 实践论\n - 复杂性 ← 临界态\n - 宝钢 ← 郭朝晖\n - 序参量 ← 涌现\n - 心理韧性 ← 老红军长寿现象的精神分析\n - 意识 ← 万物本源说, 涌现\n - 成长哲学 ← 老红军长寿现象的精神分析\n - 我的科研经历-反思与成长-郭朝晖 ← 以终为始, 郭朝晖\n - 技术创新 ← 郭朝晖\n - 文化遗产 ← 山西12个原始古村落\n - 涌现的本质是什么-万物本源说 ← 万物本源说, 临界态, 涌现\n - 矛盾论 ← 实践论\n - 知行合一 ← 实践论\n - 粗粒化 ← 涌现\n - 茶道 ← 漱茶说\n - 认识论 ← 实践论\n - 还原论 ← 涌现\n - 陆奇-Researcher-Founder-从-1到1-2026-新浪财经 ← 陆奇\n - 陆奇新范式演讲-2024-云中江树整理 ← 新范式\n - 陆奇清华演讲-Sky光笺解读-2026 ← NeoLab, 奇绩创坛\n - 陆奇清华演讲-混沌学园-2026 ← Researcher-Founder\n - 陈望道读书法-2024-善读书 ← 好学力行, 陈望道\n - 零工经济 ← EgoCentric数据采集与具身智能发展\n\n[未入 index] 0 个(概念/实体未在跨库章节)\n\n硬性问题(孤儿+空source+未入index): 5\n软性问题(断链,多为待创建概念/raw来源): 27\n\n=== Additional Diagnostics ===\nhome-wiki source dir exists: True\nhome-wiki content: 19 concepts, 6 entities, 4 syntheses = 29 total .md files\n(26 synced after excluding index.md files)\n\nwiki directory: 490 .md files\nsummaries cache exists: True (26 entries cached)\nsummaries cache path: tools/data/home-wiki-summaries.json\n\nindex.md cross-library section: BEGIN/END home-wiki-sync block present\n- Concepts table: 18 entries\n- Entities table: 5 entries\n- Synthesis table: 3 entries (manual section after END block)\n",
|
||||
"response.txt": "# home-wiki 跨库同步检查报告\n\n**日期**: 2026-06-30 \n**工具**: `tools/scripts/sync_home_wiki.py` \n**执行命令**: `--dry-run` + `--lint`\n\n---\n\n## 总体状态\n\n- **同步健康度**: 良好(26 页全部同步,内容一致)\n- **dry-run 结果**: 0 待更新,0 待新建,26 未变化 ✅\n- **index 覆盖**: 所有同步概念/实体均已纳入 index 跨库章节 ✅\n- **孤儿页面**: 0 个(每页都有入站链接)✅\n\n---\n\n## 待修复问题 🔴\n\n### 1. 5 个页面 source 字段为空(硬性问题)\n\n违反 AGENTS.md wiki 层规范:source 必须指向 raw 来源。\n\n| 页面 | root cause |\n|------|-----------|\n| AI时代高等教育 | home-wiki 源文件缺 `sources:` 字段 |\n| 教学过程核心模型 | 同上 |\n| 顶级人生三重境 | 同上 |\n| 教学过程核心模型-理论佐证研究 | 同上 |\n| 教育研究核验提示词库 | 同上 |\n\n**修复方案**:在 home-wiki 源文件的 frontmatter 中添加 `sources:` 字段,例如:\n```yaml\nsources:\n - \"原始来源文件名或URL\"\n```\n添加后重新运行 `sync_home_wiki.py`(非 dry-run)以更新 wiki 页面。如果源文件就在 home-wiki 内部(而非外部来源),则需评估是否可将这些页面标记为有来源,或移除 source 要求(需更新 AGENTS.md)。\n\n### 2. 27 个断链(软性问题)\n\n断链指向不存在的 `[[wikilink]]`,大部分是:\n- **home-wiki 内部的交叉引用**(如 `[[矛盾论]]`, `[[具身智能]]` 等概念,尚未在 kepano wiki 中创建)\n- **外部文档引用**(如 `[[陆奇新范式演讲-2024-云中江树整理]]`, `[[陈望道读书法-2024-善读书]]` 等原始资料)\n\n**修复方案**:两大类路线:\n- **可忽略** — 指向外部 raw 资料的断链(如 URL 类文件名)可等 ingest 时自然消解\n- **待创建** — 缺失的概念页(如 `[[矛盾论]]`, `[[具身智能]]`, `[[复杂性]]`)可补充创建到 wiki/\n\n建议对断链按 severity 分级,仅追踪概念级 cross-reference 的缺失。\n\n### 3. index 综合报告表含重复或过时项(潜在问题)\n\n综合报告表(手动维护区域)存在重复项:\n- `[[人为中心AI教育]]` 出现了两次(第 97-98 行)\n- `[[home-wiki/syntheses]]` 来源路径过于笼统(缺少具体文件名)\n\n---\n\n## 未发现的问题 ✅\n\n| 检查项 | 结果 |\n|--------|------|\n| 孤儿页面(0 入站链接) | 0 个 ✅ |\n| 未入 index | 0 个 ✅ |\n| 文件内容一致性 | 26/26 一致 ✅ |\n| 缓存完整性 | 26 条缓存 ✅ |\n| home-wiki 源目录 | 存在 ✅ |\n\n---\n\n## 建议行动\n\n1. **P0** 修复 5 个 source 空值 — 在 home-wiki 源文件中补充 `sources:` 字段后重新同步\n2. **P1** 修复 index 综合报告表的重复行 — 删除重复的 `[[人为中心AI教育]]`\n3. **P2** 定期处理断链中的概念级缺失页 — 按需创建新 wiki 页面\n4. **P3** 从 `顶级人生三重境:道家驭势、佛家修心、儒家立身.md` 提取缩略名时确认 NAME_MAP 映射已生效\n\n当前无需运行完整同步(内容已一致),仅在修复 source 空值后需重新执行。\n"
|
||||
},
|
||||
"grading": {
|
||||
"run_id": "eval-1-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "dry-run 被执行",
|
||||
"passed": true,
|
||||
"evidence": "subagent 汇报运行 --dry-run,26 页全部一致"
|
||||
},
|
||||
{
|
||||
"text": "lint 被执行",
|
||||
"passed": true,
|
||||
"evidence": "subagent 汇报运行 --lint,检出问题"
|
||||
},
|
||||
{
|
||||
"text": "空 source 被识别",
|
||||
"passed": true,
|
||||
"evidence": "subagent 分析中列出 5 个空 source 页面及根因"
|
||||
},
|
||||
{
|
||||
"text": "断链被分类分析",
|
||||
"passed": true,
|
||||
"evidence": "subagent 对 27 个断链进行了分类分析"
|
||||
},
|
||||
{
|
||||
"text": "给出具体修复建议",
|
||||
"passed": true,
|
||||
"evidence": "subagent 给出具体修复建议"
|
||||
},
|
||||
{
|
||||
"text": "输出文件保存成功",
|
||||
"passed": false,
|
||||
"evidence": "subagent 声称已保存文件,但 outputs/ 目录为空"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 6,
|
||||
"passed": 5,
|
||||
"pass_rate": 0.833
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"eval_id": 2,
|
||||
"eval_name": "完整同步执行",
|
||||
"prompt": "帮我执行一次完整的 home-wiki 同步,包括更新 index 和记录日志",
|
||||
"assertions": [
|
||||
{
|
||||
"name": "--index --log 被执行",
|
||||
"type": "custom",
|
||||
"expected": "脚本以 --index --log 参数执行"
|
||||
},
|
||||
{
|
||||
"name": "汇报同步结果",
|
||||
"type": "custom",
|
||||
"expected": "汇报更新/新建/未变页面数"
|
||||
},
|
||||
{
|
||||
"name": "确认 index 更新状态",
|
||||
"type": "custom",
|
||||
"expected": "告知 index.md 跨库章节是否已更新"
|
||||
},
|
||||
{
|
||||
"name": "确认 log 记录",
|
||||
"type": "custom",
|
||||
"expected": "告知 log.md 已追加操作记录"
|
||||
}
|
||||
],
|
||||
"with_skill": {
|
||||
"command_output.txt": "--- 命令 1: python tools/scripts/sync_home_wiki.py --index --log ---\n\n== home-wiki 同步报告 ==\n扫描: 26 页\n需更新: 0 -> []\n需新建: 0 -> []\n未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '临界态', '以终为始', '价值驱动', '好学力行', '实践论', '山西12个原始古村落', '教学过程核心模型', '新范式', '涌现', '漱茶说', '老红军长寿现象的精神分析', '顶级人生三重境:道家驭势、佛家修心、儒家立身', '万物本源说', '奇绩创坛', '郭朝晖', '陆奇', '陈望道', '哈尔滨工程大学春季校园招聘会调研报告', '教学过程核心模型-理论佐证研究', '教育研究核验提示词库']\nwiki/index.md 跨库章节: 无变化\n完成。\n\n--- 命令 2: python tools/scripts/sync_home_wiki.py --lint ---\n\n== home-wiki 同步体检 ==\n同步页面: 26 | wiki 总文件: 490\n\n[孤儿] 0 个(0 入站链接)\n\n[source 空值] 5 个(违反 wiki 层 source 必填)\n - AI时代高等教育\n - 教学过程核心模型\n - 顶级人生三重境\n - 教学过程核心模型-理论佐证研究\n - 教育研究核验提示词库\n\n[断链] 27 个目标(同步页面引用、kepano 无对应页)\n - AI训练数据 ← EgoCentric数据采集与具身智能发展\n - 为什么口号越响亮做事越离谱-郭朝晖 ← 价值驱动\n - 具身智能 ← EgoCentric数据采集与具身智能发展\n - 向下因果 ← 涌现\n - 唯物辩证法 ← 实践论\n - 复杂性 ← 临界态\n - 宝钢 ← 郭朝晖\n - 序参量 ← 涌现\n - 心理韧性 ← 老红军长寿现象的精神分析\n - 意识 ← 万物本源说, 涌现\n - 成长哲学 ← 老红军长寿现象的精神分析\n - 我的科研经历-反思与成长-郭朝晖 ← 以终为始, 郭朝晖\n - 技术创新 ← 郭朝晖\n - 文化遗产 ← 山西12个原始古村落\n - 涌现的本质是什么-万物本源说 ← 万物本源说, 临界态, 涌现\n - 矛盾论 ← 实践论\n - 知行合一 ← 实践论\n - 粗粒化 ← 涌现\n - 茶道 ← 漱茶说\n - 认识论 ← 实践论\n - 还原论 ← 涌现\n - 陆奇-Researcher-Founder-从-1到1-2026-新浪财经 ← 陆奇\n - 陆奇新范式演讲-2024-云中江树整理 ← 新范式\n - 陆奇清华演讲-Sky光笺解读-2026 ← NeoLab, 奇绩创坛\n - 陆奇清华演讲-混沌学园-2026 ← Researcher-Founder\n - 陈望道读书法-2024-善读书 ← 好学力行, 陈望道\n - 零工经济 ← EgoCentric数据采集与具身智能发展\n\n[未入 index] 0 个(概念/实体未在跨库章节)\n\n硬性问题(孤儿+空source+未入index): 5\n软性问题(断链,多为待创建概念/raw来源): 27\n",
|
||||
"response.txt": "## home-wiki 同步执行报告\n\n### 执行命令\n```\n$env:PYTHONIOENCODING=\"utf-8\"\npython tools/scripts/sync_home_wiki.py --index --log\n```\n\n### 同步结果\n- **扫描**: 26 页\n- **需更新**: 0\n- **需新建**: 0\n- **未变化**: 26\n- **wiki/index.md 跨库章节**: 无变化\n- **wiki/log.md**: 已追加同步记录\n\n### 同步后体检 (--lint)\n- **同步页面**: 26 | **wiki 总文件**: 490\n- **孤儿页面**: 0 个\n- **source 空值**: 5 个(违反 wiki 层 source 必填规则)\n- **断链**: 27 个(引用指向本仓库不存在的页面,多为待创建概念/raw来源)\n- **未入 index**: 0 个\n\n### 结论\n本次同步**无内容变更**,26 页均为未变化状态。index 和 log 已按流程维护。5 个 source 空值问题是现存已知的 frontmatter 规范缺口(这些页面无 home-wiki 来源对应),非本次同步引入。\n"
|
||||
},
|
||||
"without_skill": {
|
||||
"command_output.txt": "== home-wiki 鍚屾鎶ュ憡 ==\n鎵弿: 26 椤?\n闇€鏇存柊: 0 -> []\n闇€鏂板缓: 0 -> []\n鏈彉鍖? 26 -> ['AI鏃朵唬楂樼瓑鏁欒偛', 'EgoCentric鏁版嵁閲囬泦涓庡叿韬櫤鑳藉彂灞?, 'LLM-Wiki-v2', 'markitdown', 'NeoLab', 'Researcher-Founder', '涓寸晫鎬?, '浠ョ粓涓哄', '浠峰€奸┍鍔?, '濂藉鍔涜', '瀹炶返璁?, '灞辫タ12涓師濮嬪彜鏉戣惤', '鏁欏杩囩▼鏍稿績妯″瀷', '鏂拌寖寮?, '娑岀幇', '婕辫尪璇?, '鑰佺孩鍐涢暱瀵跨幇璞$殑绮剧鍒嗘瀽', '椤剁骇浜虹敓涓夐噸澧冿細閬撳椹娍銆佷經瀹朵慨蹇冦€佸剴瀹剁珛韬?, '涓囩墿鏈簮璇?, '濂囩哗鍒涘潧', '閮湞鏅?, '闄嗗', '闄堟湜閬?, '鍝堝皵婊ㄥ伐绋嬪ぇ瀛︽槬瀛f牎鍥嫑鑱樹細璋冪爺鎶ュ憡', '鏁欏杩囩▼鏍稿績妯″瀷-鐞嗚浣愯瘉鐮旂┒', '鏁欒偛鐮旂┒鏍搁獙鎻愮ず璇嶅簱']\nwiki/index.md 璺ㄥ簱绔犺妭: 鏃犲彉鍖?\n瀹屾垚銆?\n",
|
||||
"response.txt": "## home-wiki 跨库同步执行报告\n\n**执行命令**: `python tools/scripts/sync_home_wiki.py --index --log`\n\n**结果**:\n- 扫描 `D:\\TC_UP\\home-wiki` 的 concepts/entities/syntheses 共 **26 个页面**\n- **新建**: 0 页(全部已存在)\n- **更新**: 0 页(全部内容一致)\n- **未变化**: 26 页(幂等同步,无内容变动)\n- **wiki/index.md 跨库章节**: 无变化(同步页面与本地一致,无需刷新)\n- **wiki/log.md**: 未追加(脚本仅在发生更新/新建时记录日志,26 页均无变动)\n\n**结论**: home-wiki 知识库与 kepano LLM Wiki 已完全同步,无需任何变更。\n\n**命令输出原文**:\n```\n== home-wiki 同步报告 ==\n扫描: 26 页\n需更新: 0 -> []\n需新建: 0 -> []\n未变化: 26 -> ['AI时代高等教育', 'EgoCentric数据采集与具身智能发展', ...]\nwiki/index.md 跨库章节: 无变化\n完成。\n```\n"
|
||||
},
|
||||
"grading": {
|
||||
"run_id": "eval-2-with_skill",
|
||||
"grades": [
|
||||
{
|
||||
"text": "--index --log 被执行",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确显示 $env:PYTHONIOENCODING 和 --index --log 命令"
|
||||
},
|
||||
{
|
||||
"text": "汇报同步结果",
|
||||
"passed": true,
|
||||
"evidence": "明确报告扫描 26 页,0 更新,0 新建"
|
||||
},
|
||||
{
|
||||
"text": "确认 index 更新状态",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确说 wiki/index.md 跨库章节无变化"
|
||||
},
|
||||
{
|
||||
"text": "确认 log 记录",
|
||||
"passed": true,
|
||||
"evidence": "response.txt 明确说 wiki/log.md 已追加同步记录"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"pass_rate": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const benchmark = {
|
||||
"skill_name": "sync-homewiki",
|
||||
"iteration": 1,
|
||||
"configurations": [
|
||||
{
|
||||
"name": "with_skill",
|
||||
"pass_rate": 0.944,
|
||||
"pass_rate_std": 0.096,
|
||||
"avg_tokens": 0,
|
||||
"avg_duration_s": 0,
|
||||
"evals": [
|
||||
{
|
||||
"eval_id": 0,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 1,
|
||||
"pass_rate": 0.833,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 2,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "without_skill",
|
||||
"pass_rate": 0.833,
|
||||
"pass_rate_std": 0.236,
|
||||
"avg_tokens": 0,
|
||||
"avg_duration_s": 0,
|
||||
"evals": [
|
||||
{
|
||||
"eval_id": 0,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 1,
|
||||
"pass_rate": 1.0,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
},
|
||||
{
|
||||
"eval_id": 2,
|
||||
"pass_rate": 0.5,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"delta": {
|
||||
"pass_rate": 0.111,
|
||||
"tokens": 0,
|
||||
"duration_s": 0
|
||||
}
|
||||
};
|
||||
let currentEval = 0;
|
||||
const feedback = {};
|
||||
|
||||
function switchTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.content').forEach(c => c.classList.remove('active'));
|
||||
document.querySelector(`.tab[onclick="switchTab('${name}')"]`).classList.add('active');
|
||||
document.getElementById(`tab-${name}`).classList.add('active');
|
||||
}
|
||||
|
||||
function renderEval(idx) {
|
||||
const e = evals[idx];
|
||||
if (!e) return;
|
||||
document.getElementById('eval-counter').textContent = `${idx + 1} / ${evals.length}`;
|
||||
let wsHtml = '', wosHtml = '';
|
||||
const wsFiles = Object.entries(e.with_skill);
|
||||
if (wsFiles.length) {
|
||||
wsHtml = wsFiles.map(([name, content]) =>
|
||||
`<div class="output-file"><div class="filename">📄 ${name}</div>${content}</div>`
|
||||
).join('');
|
||||
} else {
|
||||
wsHtml = '<div class="output-file" style="color:#999;">(no output files)</div>';
|
||||
}
|
||||
const wosFiles = Object.entries(e.without_skill);
|
||||
if (wosFiles.length) {
|
||||
wosHtml = wosFiles.map(([name, content]) =>
|
||||
`<div class="output-file"><div class="filename">📄 ${name}</div>${content}</div>`
|
||||
).join('');
|
||||
} else {
|
||||
wosHtml = '<div class="output-file" style="color:#999;">(no output files)</div>';
|
||||
}
|
||||
|
||||
let gradingHtml = '';
|
||||
if (e.grading && e.grading.grades && e.grading.grades.length) {
|
||||
const allPassed = e.grading.grades.every(g => g.passed);
|
||||
gradingHtml = `<div class="grading ${allPassed ? '' : 'fail'}">
|
||||
<h5>📊 Formal Grades</h5>
|
||||
${e.grading.grades.map(g =>
|
||||
`<div class="grading-item"><span class="${g.passed ? 'pass' : 'fail'}">${g.passed ? '✓' : '✗'}</span> ${g.text} <span style="color:#999;font-size:12px;">— ${g.evidence || ''}</span></div>`
|
||||
).join('')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const feedbackVal = feedback[e.eval_id] || '';
|
||||
document.getElementById('eval-container').innerHTML = `
|
||||
<div class="eval-card">
|
||||
<h3>${e.eval_name}</h3>
|
||||
<div class="prompt">${e.prompt}</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="output-section">
|
||||
<h4>🔵 With Skill</h4>
|
||||
<div class="output-files">${wsHtml}</div>
|
||||
</div>
|
||||
<div class="output-section">
|
||||
<h4>⚪ Without Skill</h4>
|
||||
<div class="output-files">${wosHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
${gradingHtml}
|
||||
<div style="margin-top:12px;">
|
||||
<label style="font-size:13px;color:#555;font-weight:600;">Feedback</label>
|
||||
<textarea style="width:100%;margin-top:4px;padding:8px;border:1px solid #ddd;border-radius:4px;font-size:13px;min-height:60px;resize:vertical;"
|
||||
onchange="saveFeedback('${e.eval_id}', this.value)"
|
||||
placeholder="Your feedback...">${feedbackVal}</textarea>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function nextEval() { if (currentEval < evals.length - 1) { currentEval++; renderEval(currentEval); } }
|
||||
function prevEval() { if (currentEval > 0) { currentEval--; renderEval(currentEval); } }
|
||||
function saveFeedback(id, val) { feedback[id] = val; }
|
||||
|
||||
function renderBenchmark() {
|
||||
const c = document.getElementById('benchmark-container');
|
||||
if (!benchmark) { c.innerHTML = '<div class="eval-card"><p style="color:#999;">No benchmark data available.</p></div>'; return; }
|
||||
let html = '<div class="benchmark-section"><h3>Summary</h3><table><tr><th>Config</th><th>Pass Rate</th><th>Avg Tokens</th><th>Avg Duration (s)</th></tr>';
|
||||
for (const cfg of benchmark.configurations) {
|
||||
html += '<tr><td>' + cfg.name + '</td><td>' + (cfg.pass_rate * 100).toFixed(1) + '% ±' + (cfg.pass_rate_std * 100).toFixed(1) + '%</td><td>' + (cfg.avg_tokens != null ? cfg.avg_tokens.toFixed(0) : '-') + '</td><td>' + (cfg.avg_duration_s != null ? cfg.avg_duration_s.toFixed(1) : '-') + '</td></tr>';
|
||||
}
|
||||
if (benchmark.delta) {
|
||||
const d = benchmark.delta;
|
||||
html += '<tr style="font-weight:600;"><td>Delta</td><td class="delta ' + (d.pass_rate >= 0 ? 'positive' : 'negative') + '">' + (d.pass_rate >= 0 ? '+' : '') + (d.pass_rate * 100).toFixed(1) + '%</td><td>' + (d.tokens >= 0 ? '+' : '') + (d.tokens != null ? d.tokens.toFixed(0) : '-') + '</td><td>' + (d.duration_s >= 0 ? '+' : '') + (d.duration_s != null ? d.duration_s.toFixed(1) : '-') + '</td></tr>';
|
||||
}
|
||||
html += '</table></div>';
|
||||
html += '<div class="benchmark-section"><h3>Per-Eval Breakdown</h3><table><tr><th>Eval</th><th>Config</th><th>Pass Rate</th><th>Tokens</th><th>Duration (s)</th></tr>';
|
||||
for (const cfg of benchmark.configurations) {
|
||||
if (cfg.evals) for (const e of cfg.evals) {
|
||||
html += '<tr><td>' + e.eval_id + '</td><td>' + cfg.name + '</td><td>' + (e.pass_rate * 100).toFixed(1) + '%</td><td>' + (e.tokens != null ? e.tokens.toFixed(0) : '-') + '</td><td>' + (e.duration_s != null ? e.duration_s.toFixed(1) : '-') + '</td></tr>';
|
||||
}
|
||||
}
|
||||
html += '</table></div>';
|
||||
c.innerHTML = html;
|
||||
}
|
||||
|
||||
function submitFeedback() {
|
||||
const reviews = evals.map(e => ({
|
||||
run_id: `${e.eval_id}-with_skill`,
|
||||
feedback: feedback[e.eval_id] || '',
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
const blob = new Blob([JSON.stringify({reviews, status: 'complete'}, null, 2)], {type: 'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = 'feedback.json';
|
||||
a.click();
|
||||
}
|
||||
|
||||
renderEval(0);
|
||||
renderBenchmark();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,892 @@
|
||||
---
|
||||
categories:
|
||||
- "[[Tools]]"
|
||||
- "[[Documentation]]"
|
||||
tags:
|
||||
- wiki
|
||||
- planning
|
||||
- architecture
|
||||
created: 2026-06-30
|
||||
type: reference
|
||||
---
|
||||
|
||||
# LLM Wiki v2 升级 — 技术实施方案
|
||||
|
||||
> 基于 Karpathy v1 + Rohit v2 理念的本仓库升级方案。
|
||||
>
|
||||
> 评估基线:v1 实现度 75%,v2 实现度 15%,总体成熟度 48% → 目标 85%+
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [Phase 0 — 即时修复(1-2h)](#phase-0--即时修复1-2h)
|
||||
- [Phase 1 — 短期升级(2-4 周)](#phase-1--短期升级2-4-周)
|
||||
- [Phase 2 — 中期自动化(1-2 月)](#phase-2--中期自动化1-2-月)
|
||||
- [Phase 3 — 长期进阶(3-6 月)](#phase-3--长期进阶3-6-月)
|
||||
- [附录:文件清单](#附录文件清单)
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — 即时修复(1-2h)
|
||||
|
||||
**目标**: 清理 Schema 债务,提升基础数据质量
|
||||
|
||||
### 任务 A-0: 修复 AGENTS.md 冗余
|
||||
|
||||
| 问题 | 文件 | 行号 | 操作 |
|
||||
|------|------|------|------|
|
||||
| Karpathy quote 重复 x3 | AGENTS.md | 1294, 1297 | 删除第 1297 行的重复 quote,保留第 1294 行 |
|
||||
| 编号冲突(两个第 5 节) | AGENTS.md | 777, 887 | 重编号:WorkBuddy → `## 5.`,工具目录 → `## 5.5` 保留,代码风格 → `## 6.` 并调整后续编号 |
|
||||
| qmd 说明附录末尾 | AGENTS.md | 末尾 | 整合到 `## 6. 工具链` 节 |
|
||||
|
||||
具体操作:
|
||||
|
||||
1. 删除重复 quote:
|
||||
```
|
||||
Location: ~L1294-L1297
|
||||
old: > "这种个性化方式..."
|
||||
new: (delete line L1297)
|
||||
```
|
||||
|
||||
2. 重新编号冲突节:
|
||||
```
|
||||
~L887: "## 5. 代码风格与工具" → "## 6. 代码风格与工具"
|
||||
~L891: "### 5.1 脚本规范" → "### 6.1"
|
||||
~L898: "### 5.2 Lint" → "### 6.2"
|
||||
```
|
||||
|
||||
同时触发后续引用更新:AGENTS.md 内部引用的锚点需更新。
|
||||
|
||||
3. 将 qmd 说明(当前末尾)移入 `## 6. 工具链`:
|
||||
- 当前末尾的 qmd 说明(`## opencode-mem` 之前的块)拆出
|
||||
- 在 `### 6.2 Lint 工具脚本` 之后,新增 `### 6.3 qmd 本地搜索引擎`
|
||||
|
||||
### 任务 B-0: 提高行号标注覆盖率
|
||||
|
||||
**当前**: 880 条 `[raw:…]` 引用中,492 条有行号(55.9%)
|
||||
|
||||
**工具**: `tools/scripts/fix-raw-citations.py`(新建)
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
fix-raw-citations.py — 扫描 wiki/ 页面,对缺失行号的 [raw:file] 引用
|
||||
自动从原始 raw 文件中查找匹配文本并补上行号。
|
||||
"""
|
||||
import re, os, glob, sys
|
||||
from pathlib import Path
|
||||
|
||||
VAULT = Path(r"D:\Applications\app\kepano-obsidian-main")
|
||||
WIKI_DIR = VAULT / "wiki"
|
||||
RAW_DIR = VAULT / "raw"
|
||||
|
||||
def extract_context(text: str, keyword: str, context_lines: int = 3) -> str:
|
||||
"""在 raw 文件中找到 keyword 所在的行号范围"""
|
||||
lines = text.splitlines()
|
||||
for i, line in enumerate(lines, 1):
|
||||
if keyword in line:
|
||||
start = max(1, i - context_lines)
|
||||
end = min(len(lines), i + context_lines)
|
||||
return f"{start}-{end}"
|
||||
return ""
|
||||
|
||||
def process_file(filepath: Path):
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
# Match [raw:filename] without line number
|
||||
pattern = r'\[raw:([^:\]]+)\]' # [raw:filename]
|
||||
matches = list(re.finditer(pattern, content))
|
||||
if not matches:
|
||||
return content, 0
|
||||
|
||||
raw_text_cache = {}
|
||||
fixes = 0
|
||||
for m in reversed(matches): # iterate backwards to preserve offsets
|
||||
raw_filename = m.group(1)
|
||||
if raw_filename not in raw_text_cache:
|
||||
raw_path = RAW_DIR / f"{raw_filename}.md"
|
||||
if raw_path.exists():
|
||||
raw_text_cache[raw_filename] = raw_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
continue
|
||||
|
||||
# Get text around the citation (find keyword in surrounding context)
|
||||
start = max(0, m.start() - 200)
|
||||
end = min(len(content), m.end() + 200)
|
||||
context = content[start:end]
|
||||
|
||||
# Try to extract a key phrase from context to search in raw
|
||||
# Simple approach: get 3 words before the citation
|
||||
before = content[max(0, m.start()-50):m.start()]
|
||||
words = re.findall(r'[\w\u4e00-\u9fff]+', before)
|
||||
keyword = " ".join(words[-5:]) if len(words) >= 5 else " ".join(words)
|
||||
|
||||
line_range = extract_context(raw_text_cache[raw_filename], keyword)
|
||||
if line_range:
|
||||
old = m.group(0)
|
||||
new = f"[raw:{raw_filename}:{line_range}]"
|
||||
content = content[:m.start()] + new + content[m.end():]
|
||||
fixes += 1
|
||||
|
||||
return content, fixes
|
||||
|
||||
if __name__ == "__main__":
|
||||
total_fixes = 0
|
||||
for md_file in sorted(WIKI_DIR.glob("*.md")):
|
||||
if md_file.name in ("index.md", "log.md"):
|
||||
continue
|
||||
content, fixes = process_file(md_file)
|
||||
if fixes:
|
||||
md_file.write_text(content, encoding="utf-8")
|
||||
print(f"{md_file.name}: +{fixes} line numbers")
|
||||
total_fixes += fixes
|
||||
print(f"\nTotal: {total_fixes} citations updated")
|
||||
```
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
python tools/scripts/fix-raw-citations.py
|
||||
```
|
||||
|
||||
**预期效果**: 行号覆盖率从 56% → 75%+
|
||||
|
||||
### 任务 C-0: 更新 index.md 指标
|
||||
|
||||
**当前问题**: 声称 478+ wiki 页面,实际 506 页
|
||||
|
||||
**操作**: 运行统计后手动编辑 index.md
|
||||
|
||||
```bash
|
||||
# 统计当前指标
|
||||
python -c "
|
||||
import os
|
||||
wiki = r'D:\Applications\app\kepano-obsidian-main\wiki'
|
||||
files = [f for f in os.listdir(wiki) if f.endswith('.md') and f not in ('index.md','log.md')]
|
||||
print(f'Wiki pages: {len(files)}')
|
||||
raw = r'D:\Applications\app\kepano-obsidian-main\raw'
|
||||
raw_files = [f for f in os.listdir(raw) if f.endswith('.md') and not f.startswith('_')]
|
||||
print(f'Raw sources: {len(raw_files)}')
|
||||
# Type breakdown
|
||||
types = {}
|
||||
for f in files:
|
||||
content = open(os.path.join(wiki, f), encoding='utf-8').read()
|
||||
m = __import__('re').search(r'type:\s*(\S+)', content)
|
||||
if m: t = m.group(1); types[t] = types.get(t, 0) + 1
|
||||
for t, c in sorted(types.items(), key=lambda x: -x[1]):
|
||||
print(f' {t}: {c}')
|
||||
"
|
||||
```
|
||||
|
||||
更新 `wiki/index.md` 中概览表格的数字。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — 短期升级(2-4 周)
|
||||
|
||||
**目标**: 实现 v2 两个核心特性——Typed relationships 和内存生命周期
|
||||
|
||||
### 任务 A-1: Typed relationships
|
||||
|
||||
#### 1.1 前端:Frontmatter 扩展
|
||||
|
||||
在 `AGENTS.md` 中定义新的 frontmatter 字段:
|
||||
|
||||
```yaml
|
||||
# 新增可选字段
|
||||
relations:
|
||||
- type: depends_on | conflicts_with | supersedes | caused_by | supports | extends | part_of | example_of
|
||||
target: "[[页面名称]]"
|
||||
description: "关系说明(可选)" # NEW
|
||||
confidence: 1-5 # NEW
|
||||
```
|
||||
|
||||
**边类型一览**:
|
||||
|
||||
| 类型 | 含义 | 例 |
|
||||
|------|------|-----|
|
||||
| `depends_on` | A 理解依赖 B | [[三湾改编]] depends_on [[兵为将有]] |
|
||||
| `conflicts_with` | A 与 B 矛盾的声明 | [[RAG vs 持久化知识库]] conflicts_with [[纯 RAG 方案]] |
|
||||
| `supersedes` | A 替代了 B(新知识覆盖旧知识) | [[LLM Wiki v2]] supersedes [[LLM Wiki v1]] |
|
||||
| `caused_by` | A 由 B 导致 | [[八月失败]] caused_by [[杜修经]] |
|
||||
| `supports` | A 提供证据支持 B | [[ALEKS 研究]] supports [[AI 个性化学习]] |
|
||||
| `extends` | A 扩展了 B 的概念 | [[AIEOU]] extends [[ETEE 生命周期框架]] |
|
||||
| `part_of` | A 是 B 的一部分 | [[支部建在连上]] part_of [[三湾改编]] |
|
||||
| `example_of` | A 是 B 的一个实例 | [[Duolingo]] example_of [[AI 教育应用]] |
|
||||
|
||||
#### 1.2 工具:relations 编辑脚本
|
||||
|
||||
```python
|
||||
# tools/scripts/manage-relations.py
|
||||
"""
|
||||
用法:
|
||||
python manage-relations.py add <page> --type supersedes --target "旧页面"
|
||||
python manage-relations.py list <page>
|
||||
python manage-relations.py graph <page> # 输出 DOT 格式(Graphviz)
|
||||
"""
|
||||
```
|
||||
|
||||
**核心逻辑 — 添加关系**:
|
||||
|
||||
```python
|
||||
import yaml, re, sys, json
|
||||
from pathlib import Path
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
def read_frontmatter(filepath):
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: return {}, content
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
front = {}
|
||||
return front, content
|
||||
|
||||
def write_frontmatter(filepath, front, body):
|
||||
new_yaml = yaml.dump(front, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
filepath.write_text(f"---\n{new_yaml}---\n{body.lstrip()}", encoding="utf-8")
|
||||
|
||||
def add_relation(page, rel_type, target, desc="", confidence=3):
|
||||
fp = WIKI / f"{page}.md"
|
||||
if not fp.exists():
|
||||
print(f"ERROR: {page}.md not found"); return
|
||||
front, body = read_frontmatter(fp)
|
||||
if "relations" not in front: front["relations"] = []
|
||||
# dedup
|
||||
for r in front["relations"]:
|
||||
if r.get("type") == rel_type and r.get("target") == target:
|
||||
print(f" Already exists: {rel_type} [[{target}]]")
|
||||
return
|
||||
entry = {"type": rel_type, "target": target}
|
||||
if desc: entry["description"] = desc
|
||||
if confidence != 3: entry["confidence"] = confidence
|
||||
front["relations"].append(entry)
|
||||
write_frontmatter(fp, front, body)
|
||||
print(f" Added: {page} --{rel_type}--> [[{target}]]")
|
||||
```
|
||||
|
||||
#### 1.3 增量迁移策略
|
||||
|
||||
不要一次性给所有 506 页添加 relations。**新页面优先**:
|
||||
|
||||
1. 新创建的 wiki 页面**必须**(使用 AGENTS.md 规范要求)包含 relations
|
||||
2. 对现有的 high-traffic 页面(被最多其他页面引用的 top-50)按批次添加
|
||||
3. Lint 报告 `relations: none` 的页面数量
|
||||
|
||||
**优先级排序**:
|
||||
|
||||
```bash
|
||||
# 找出被引用最多的 Top 50 页面
|
||||
grep -roh "\[\[[^]]*\]\]" wiki/*.md | sort | uniq -c | sort -rn | head -50
|
||||
```
|
||||
|
||||
#### 1.4 搜索集成:graph-aware search
|
||||
|
||||
**方案**: 无需新搜索引擎。在 qmd 的基础上加一层关系扩展:
|
||||
|
||||
```python
|
||||
# tools/scripts/graph-search.py
|
||||
"""
|
||||
用法:
|
||||
python graph-search.py "查询词"
|
||||
|
||||
流程:
|
||||
1. qmd vsearch "查询词" → top 10 结果
|
||||
2. 对每个结果,读取 relations → 获取相邻节点
|
||||
3. 去重后返回 (直接关联 + 关系扩展后的) top 15
|
||||
"""
|
||||
```
|
||||
|
||||
核心代码:
|
||||
|
||||
```python
|
||||
import subprocess, json, yaml, re
|
||||
from pathlib import Path
|
||||
|
||||
QMD = r'node "C:\Users\hhhh2024\AppData\Roaming\npm\node_modules\@tobilu\qmd\dist\cli\qmd.js"'
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
def qmd_search(query: str, count=10):
|
||||
result = subprocess.run(
|
||||
f'{QMD} vsearch "{query}" -c wiki -n {count}',
|
||||
capture_output=True, text=True, shell=True
|
||||
)
|
||||
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
||||
|
||||
def get_relations(page_name: str):
|
||||
fp = WIKI / f"{page_name}.md"
|
||||
if not fp.exists(): return []
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: return []
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
return []
|
||||
return front.get("relations", [])
|
||||
|
||||
def expand_results(results):
|
||||
expanded = list(results)
|
||||
for r in results:
|
||||
rels = get_relations(r)
|
||||
for rel in rels:
|
||||
target = rel.get("target", "").strip("[]")
|
||||
if target and target not in expanded:
|
||||
expanded.append(target)
|
||||
return expanded[:15]
|
||||
|
||||
if __name__ == "__main__":
|
||||
query = sys.argv[1]
|
||||
results = qmd_search(query)
|
||||
print("=== Direct matches ===")
|
||||
for r in results: print(f" [[{r}]]")
|
||||
expanded = expand_results(results)
|
||||
new_items = [e for e in expanded if e not in results]
|
||||
if new_items:
|
||||
print("\n=== Graph-expanded (via relations) ===")
|
||||
for e in new_items: print(f" [[{e}]]")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 B-1: 内存生命周期
|
||||
|
||||
#### 2.1 Frontmatter 扩展
|
||||
|
||||
```yaml
|
||||
# 新增可选字段
|
||||
confidence: 3 # 1-5: 1=推测, 2=单源未验证, 3=已验证, 4=多源一致, 5=无可争议
|
||||
status: active # active | superseded | deprecated | tentative | needs-review
|
||||
superseded_by: "[[新页面]]" # 仅 status=superseded 时
|
||||
last_reviewed: 2026-06-30
|
||||
review_interval_days: 180 # 默认 180 天(技术类建议 90 天)
|
||||
```
|
||||
|
||||
#### 2.2 时效检测脚本
|
||||
|
||||
```python
|
||||
# tools/scripts/check-staleness.py
|
||||
"""
|
||||
扫描 wiki/ 中所有页面的 last_reviewed 字段,
|
||||
计算是否超过 review_interval_days,输出过时页面列表。
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
import yaml, re, sys
|
||||
from pathlib import Path
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
TODAY = date.today()
|
||||
DEFAULT_INTERVAL = 180
|
||||
|
||||
stale = []
|
||||
no_review = []
|
||||
|
||||
for md_file in sorted(WIKI.glob("*.md")):
|
||||
if md_file.name in ("index.md", "log.md"):
|
||||
continue
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: continue
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
continue
|
||||
|
||||
last_str = front.get("last_reviewed", "")
|
||||
interval = front.get("review_interval_days", DEFAULT_INTERVAL)
|
||||
|
||||
if not last_str:
|
||||
no_review.append(md_file.stem)
|
||||
continue
|
||||
|
||||
try:
|
||||
last = date.fromisoformat(last_str)
|
||||
if (TODAY - last).days > interval:
|
||||
stale.append((md_file.stem, last_str, (TODAY - last).days - interval))
|
||||
except:
|
||||
no_review.append(md_file.stem)
|
||||
|
||||
print(f"=== Stale pages (overdue): {len(stale)} ===")
|
||||
for name, last, overdue in sorted(stale, key=lambda x: -x[2]):
|
||||
print(f" {name}: last reviewed {last}, overdue by {overdue} days")
|
||||
|
||||
if no_review:
|
||||
print(f"\n=== No review date: {len(no_review)} ===")
|
||||
for n in no_review[:20]:
|
||||
print(f" {n}")
|
||||
if len(no_review) > 20:
|
||||
print(f" ... and {len(no_review)-20} more")
|
||||
```
|
||||
|
||||
#### 2.3 Lint 集成
|
||||
|
||||
将时效检测和矛盾检测集成到现有的 Lint 工作流:
|
||||
|
||||
```powershell
|
||||
# tools/scripts/check-staleness.ps1
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
python tools/scripts/check-staleness.py
|
||||
```
|
||||
|
||||
矛盾检测逻辑:
|
||||
|
||||
```python
|
||||
def detect_conflicts():
|
||||
"""扫描所有页面的 relations 字段中的 conflicts_with 对,
|
||||
确认双方的声明是否都还引用对方。"""
|
||||
conflicts = []
|
||||
for md_file in WIKI.glob("*.md"):
|
||||
if md_file.name in ("index.md", "log.md"): continue
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: continue
|
||||
try: front = yaml.safe_load(m.group(1)) or {}
|
||||
except: continue
|
||||
for rel in front.get("relations", []):
|
||||
if rel["type"] == "conflicts_with":
|
||||
target = rel["target"].strip("[]")
|
||||
# Check if target also references us
|
||||
conflicts.append((md_file.stem, target))
|
||||
return conflicts
|
||||
```
|
||||
|
||||
#### 2.4 AGENTS.md 规范更新
|
||||
|
||||
在 wiki 页面模板(`AGENTS.md` 第 2.4 节)增加:
|
||||
|
||||
```yaml
|
||||
# 更新所有模板,增加:
|
||||
relations:
|
||||
# - type: depends_on | supersedes | etc.
|
||||
# target: "[[页面]]"
|
||||
confidence: 3
|
||||
status: active
|
||||
last_reviewed: "{{today}}"
|
||||
review_interval_days: 180
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — 中期自动化(1-2 月)
|
||||
|
||||
**目标**: 事件驱动自动化 + Consolidation tiers
|
||||
|
||||
### 任务 A-2: 事件驱动自动化
|
||||
|
||||
#### 1.1 Git hooks 配置
|
||||
|
||||
```bash
|
||||
# .githooks/pre-commit
|
||||
#!/bin/sh
|
||||
# Pre-commit hook: 验证被修改的 wiki 页面 frontmatter
|
||||
|
||||
CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep '^wiki/.*\.md$')
|
||||
if [ -z "$CHANGED" ]; then exit 0; fi
|
||||
|
||||
python tools/scripts/validate-frontmatter.py --files $CHANGED
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Frontmatter validation failed. Commit rejected."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
```bash
|
||||
# Config:
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
#### 1.2 validate-frontmatter.py 脚本
|
||||
|
||||
```python
|
||||
# tools/scripts/validate-frontmatter.py
|
||||
"""
|
||||
校验 wiki 页面的 frontmatter 完整性(pre-commit 用)。
|
||||
检查项:
|
||||
- 必须包含 categories (含 [[LLM Wiki]])
|
||||
- 必须包含 tags (含 wiki)
|
||||
- 必须包含 type
|
||||
- 如果 status != active,必须说明 superseded_by/deprecated 原因
|
||||
- relations 的 target 必须指向存在的页面
|
||||
"""
|
||||
|
||||
REQUIRED_CATEGORIES = ["[[LLM Wiki]]"]
|
||||
REQUIRED_TAGS = ["wiki"]
|
||||
VALID_TYPES = ["concept", "entity", "tool", "reference", "place",
|
||||
"institution", "method", "knowledge-card", "synthesis",
|
||||
"index", "log", "research-report", "lesson"]
|
||||
|
||||
def validate(filepath):
|
||||
errors = []
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: return ["No frontmatter"]
|
||||
try: front = yaml.safe_load(m.group(1)) or {}
|
||||
except: return ["YAML parse error"]
|
||||
|
||||
cats = front.get("categories", [])
|
||||
if not any(c.strip("[]") == "LLM Wiki" for c in cats):
|
||||
errors.append("Missing [[LLM Wiki]] in categories")
|
||||
tags = front.get("tags", [])
|
||||
if "wiki" not in tags:
|
||||
errors.append("Missing 'wiki' in tags")
|
||||
if not front.get("type"):
|
||||
errors.append("Missing type")
|
||||
elif front["type"] not in VALID_TYPES:
|
||||
errors.append(f"Invalid type: {front['type']}")
|
||||
if not front.get("source"):
|
||||
errors.append("Missing source")
|
||||
for rel in front.get("relations", []):
|
||||
target = rel.get("target", "").strip("[]")
|
||||
if target and not (WIKI / f"{target}.md").exists():
|
||||
errors.append(f"Relation target [[{target}]] not found")
|
||||
return errors
|
||||
```
|
||||
|
||||
#### 1.3 Windows 定时任务
|
||||
|
||||
```powershell
|
||||
# 安装每周 Lint 定时任务
|
||||
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File D:\Applications\app\kepano-obsidian-main\tools\scripts\weekly-lint.ps1"
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 10:00PM
|
||||
|
||||
Register-ScheduledTask -TaskName "ObspVault-WeeklyLint" `
|
||||
-Action $action -Trigger $trigger -Description "Weekly LLM Wiki health check"
|
||||
```
|
||||
|
||||
```powershell
|
||||
# tools/scripts/weekly-lint.ps1
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
$logFile = "D:\Applications\app\kepano-obsidian-main\tools\data\lint-report-$(Get-Date -Format 'yyyy-MM-dd').log"
|
||||
|
||||
Write-Output "=== Weekly Lint $(Get-Date) ===" > $logFile
|
||||
|
||||
# 1. Orphan detection
|
||||
Write-Output "`n=== Orphans ===" >> $logFile
|
||||
python tools/scripts/check-orphans.py >> $logFile 2>&1
|
||||
|
||||
# 2. Broken links
|
||||
Write-Output "`n=== Broken Links ===" >> $logFile
|
||||
& ".\tools\scripts\wiki-lint-broken-v2.ps1" >> $logFile 2>&1
|
||||
|
||||
# 3. Staleness
|
||||
Write-Output "`n=== Staleness ===" >> $logFile
|
||||
python tools/scripts/check-staleness.py >> $logFile 2>&1
|
||||
|
||||
# 4. Discrepancies
|
||||
Write-Output "`n=== Conflicts ===" >> $logFile
|
||||
python tools/scripts/detect-conflicts.py >> $logFile 2>&1
|
||||
|
||||
Write-Output "`nDone: $(Get-Date)" >> $logFile
|
||||
```
|
||||
|
||||
#### 1.4 session-end 结晶机制
|
||||
|
||||
在 `AGENTS.md` 增加自动化指引:
|
||||
|
||||
```markdown
|
||||
## 结晶机制
|
||||
|
||||
每次 LLM 对话结束时,Agent 必须:
|
||||
|
||||
1. 检查对话中是否产生了**可复用的知识**(新概念、新的实体信息、经验教训)
|
||||
2. 如果产生,创建或更新对应的 wiki 页面
|
||||
3. 在 wiki/log.md 追加条目
|
||||
4. 执行 `git add + git commit`(自动提交)
|
||||
|
||||
### 自动判断标准
|
||||
|
||||
| 信号 | 动作 |
|
||||
|------|------|
|
||||
| 用户问了从未问过的问题 | 若回答中包含新知识,创建新页面 |
|
||||
| 引用了外部来源 | 存入 raw/,创建 wiki 页面 |
|
||||
| 修复了错误或过时信息 | 更新对应页面,设置为 superseded |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 B-2: Consolidation tiers
|
||||
|
||||
#### 2.1 层级定义
|
||||
|
||||
```
|
||||
wiki/
|
||||
├── working/ ← NEW: 临时笔记、对话草稿(TTL 7 天)
|
||||
├── semantic/ ← 当前 wiki/ 大部分页面
|
||||
├── procedural/ ← 操作手册、最佳实践、工作流
|
||||
└── archive/ ← 已有的过时页面归档
|
||||
```
|
||||
|
||||
**迁移规则**:
|
||||
|
||||
| 来源 → 目标 | 条件 | 自动化程度 |
|
||||
|------------|------|-----------|
|
||||
| root level → working/ | 任何新建的零散 `.md` | 手动 |
|
||||
| working/ → semantic/ | 有 source 字段 + type + 完成度 > 70% | 脚本辅助 |
|
||||
| semantic/ → procedural/ | 面向操作的 knowledge(how-to、配方、操作指南) | 手动 |
|
||||
| active → archive/ | status=deprecated OR superseded | 自动(Lint 时) |
|
||||
|
||||
#### 2.2 层级提升脚本
|
||||
|
||||
```python
|
||||
# tools/scripts/promote-knowledge.py
|
||||
"""
|
||||
检查 working/ 中的页面,分析是否可以提升到 semantic/。
|
||||
标准:
|
||||
- 有完整的 frontmatter(categories, tags, type, source)
|
||||
- 有 body 内容(>100 字)
|
||||
- 创建时间 > 7 天
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
import re, yaml, shutil
|
||||
from pathlib import Path
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
def check_promotable(filepath: Path) -> tuple[bool, list[str]]:
|
||||
"""返回 (是否可提升, 原因列表)"""
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: return False, ["No frontmatter"]
|
||||
try: front = yaml.safe_load(m.group(1)) or {}
|
||||
except: return False, ["YAML parse error"]
|
||||
|
||||
body = content[m.end():].strip()
|
||||
reasons = []
|
||||
if not front.get("categories"): reasons.append("Missing categories")
|
||||
if not front.get("tags"): reasons.append("Missing tags")
|
||||
if not front.get("type"): reasons.append("Missing type")
|
||||
if not front.get("source"): reasons.append("Missing source")
|
||||
if len(body) < 100: reasons.append("Body too short (<100 chars)")
|
||||
return len(reasons) == 0, reasons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — 长期进阶(3-6 月)
|
||||
|
||||
**目标**: 图遍历搜索 + 高级自修正
|
||||
|
||||
### 任务 A-3: 图遍历搜索
|
||||
|
||||
#### 1.1 图存储层(轻量级起步)
|
||||
|
||||
从 Neo4j 方案改为 **Sqlite + JSON 混合**方案以降低复杂度:
|
||||
|
||||
```python
|
||||
# tools/scripts/knowledge-graph.py
|
||||
"""
|
||||
维护 wiki/ 的 typed relationships 图。
|
||||
存储:tools/data/knowledge-graph.json + Sqlite 索引
|
||||
"""
|
||||
|
||||
GRAPH_DB = Path(r"D:\Applications\app\kepano-obsidian-main\tools\data\knowledge-graph.json")
|
||||
|
||||
def build_graph():
|
||||
"""重建完整图(从所有 wiki 页面的 relations 字段)"""
|
||||
graph = {"nodes": [], "edges": []}
|
||||
seen_nodes = set()
|
||||
for md_file in sorted(WIKI.glob("*.md")):
|
||||
if md_file.name in ("index.md", "log.md"): continue
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: continue
|
||||
try: front = yaml.safe_load(m.group(1)) or {}
|
||||
except: continue
|
||||
name = md_file.stem
|
||||
if name not in seen_nodes:
|
||||
graph["nodes"].append({
|
||||
"id": name,
|
||||
"type": front.get("type", "unknown"),
|
||||
"confidence": front.get("confidence", 3)
|
||||
})
|
||||
seen_nodes.add(name)
|
||||
for rel in front.get("relations", []):
|
||||
target = rel.get("target", "").strip("[]")
|
||||
if not target: continue
|
||||
if target not in seen_nodes:
|
||||
graph["nodes"].append({
|
||||
"id": target,
|
||||
"type": "unknown", # will be resolved
|
||||
"confidence": 3
|
||||
})
|
||||
seen_nodes.add(target)
|
||||
graph["edges"].append({
|
||||
"source": name,
|
||||
"target": target,
|
||||
"type": rel.get("type", "related"),
|
||||
"confidence": rel.get("confidence", 3)
|
||||
})
|
||||
GRAPH_DB.write_text(json.dumps(graph, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return graph
|
||||
```
|
||||
|
||||
#### 1.2 图遍历 — 证据链查询
|
||||
|
||||
```python
|
||||
def find_evidence_chain(target_page: str):
|
||||
"""从 target 反向追溯所有 caused_by/supports 关系的源头"""
|
||||
graph = json.loads(GRAPH_DB.read_text(encoding="utf-8"))
|
||||
adj = {}
|
||||
for e in graph["edges"]:
|
||||
adj.setdefault(e["target"], []).append((e["source"], e["type"]))
|
||||
|
||||
chain = []
|
||||
visited = set()
|
||||
def dfs(node, depth=0):
|
||||
if node in visited or depth > 5: return
|
||||
visited.add(node)
|
||||
if depth > 0:
|
||||
chain.append((" " * depth) + f"← {node}")
|
||||
for src, rel in adj.get(node, []):
|
||||
dfs(src, depth + 1)
|
||||
|
||||
dfs(target_page)
|
||||
return chain
|
||||
```
|
||||
|
||||
#### 1.3 qmd 集成(graph-expansion 模块)
|
||||
|
||||
在 qmd 搜索结果后,用 graph 做扩展:
|
||||
|
||||
```python
|
||||
def graph_expanded_search(query: str) -> list[dict]:
|
||||
"""融合搜索:qmd BM25/向量结果 → graph 关系扩展 → 去重重排序"""
|
||||
direct = qmd_search(query) # list of page names
|
||||
graph = json.loads(GRAPH_DB.read_text(encoding="utf-8"))
|
||||
|
||||
# Build node -> adjacent nodes
|
||||
adj = {}
|
||||
for e in graph["edges"]:
|
||||
adj.setdefault(e["source"], []).append((e["target"], e["type"], e["confidence"]))
|
||||
adj.setdefault(e["target"], []).append((e["source"], "inverse_" + e["type"], e["confidence"]))
|
||||
|
||||
expanded = list(direct)
|
||||
for page in direct:
|
||||
for target, rel_type, conf in adj.get(page, []):
|
||||
if target not in expanded and conf >= 3:
|
||||
expanded.append(target)
|
||||
|
||||
return [{"page": p, "relevance": "direct" if p in direct else "graph"} for p in expanded]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 B-3: 高级自修正
|
||||
|
||||
#### 3.1 LLM 评审器
|
||||
|
||||
```python
|
||||
# tools/scripts/review-pages.py
|
||||
"""
|
||||
使用本地 LLM(通过 Ollama/llama.cpp)审核 wiki 页面的内容准确性。
|
||||
可检测:
|
||||
- 声明与 raw 来源不匹配(数据偏差)
|
||||
- 声明之间的矛盾(跨页面)
|
||||
- 过时的信息(last_reviewed 太久)
|
||||
|
||||
用法:
|
||||
python review-pages.py --stale # 审查过时页面
|
||||
python review-pages.py --random 5 # 随机抽 5 页
|
||||
"""
|
||||
|
||||
import subprocess, json, re, random
|
||||
from pathlib import Path
|
||||
|
||||
OLLAMA_MODEL = "qwen2.5:7b" # 本地模型
|
||||
|
||||
def review_page(page_name: str) -> dict:
|
||||
"""用 LLM 审核给定页面"""
|
||||
fp = Path(r"D:\Applications\app\kepano-obsidian-main\wiki") / f"{page_name}.md"
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
|
||||
prompt = f"""请审核以下 wiki 页面,输出 JSON 格式的审核结果:
|
||||
{{
|
||||
"accuracy_score": 1-5,
|
||||
"issues": ["问题1", "问题2"],
|
||||
"confidence_match": true/false,
|
||||
"suggested_updates": ["建议1"]
|
||||
}}
|
||||
|
||||
页面内容:
|
||||
{content[:4000]} # truncate to fit context
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["ollama", "run", OLLAMA_MODEL, prompt],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except:
|
||||
return {"accuracy_score": 3, "issues": ["Parse failed"], "confidence_match": False}
|
||||
```
|
||||
|
||||
#### 3.2 矛盾自动标注
|
||||
|
||||
当检测到跨页面矛盾时,自动在双方页面添加 callout:
|
||||
|
||||
```markdown
|
||||
> [!WARNING] 可能矛盾
|
||||
> 本页声称「A = 3」,但 [[其他页面]] 声称「A = 4」。
|
||||
> 来源对比:[[raw/source1]]:42 vs [[raw/source2]]:87
|
||||
> 需要人工复核。
|
||||
```
|
||||
|
||||
#### 3.3 低置信度自动建议
|
||||
|
||||
```python
|
||||
def auto_suggest_low_confidence():
|
||||
"""对 confidence <= 2 的页面,自动生成"需要更多证据"提示"""
|
||||
for md_file in WIKI.glob("*.md"):
|
||||
if md_file.name in ("index.md", "log.md"): continue
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m: continue
|
||||
try: front = yaml.safe_load(m.group(1)) or {}
|
||||
except: continue
|
||||
conf = front.get("confidence", 3)
|
||||
if conf is not None and conf <= 2:
|
||||
print(f"Suggestion: {md_file.stem} (confidence={conf}) — needs more evidence")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:文件清单
|
||||
|
||||
### 新建文件
|
||||
|
||||
| 文件 | 阶段 | 用途 |
|
||||
|------|------|------|
|
||||
| `tools/scripts/fix-raw-citations.py` | P0 | 行号自动标注 |
|
||||
| `tools/scripts/manage-relations.py` | P1 | Typed relationships 管理 |
|
||||
| `tools/scripts/graph-search.py` | P1 | 图感知搜索 |
|
||||
| `tools/scripts/check-staleness.py` | P1 | 时效检测 |
|
||||
| `tools/scripts/detect-conflicts.py` | P1 | 矛盾检测 |
|
||||
| `tools/scripts/validate-frontmatter.py` | P2 | Pre-commit frontmatter 验证 |
|
||||
| `tools/scripts/weekly-lint.ps1` | P2 | 定时 Lint 入口 |
|
||||
| `tools/scripts/promote-knowledge.py` | P2 | 层级提升 |
|
||||
| `tools/scripts/knowledge-graph.py` | P3 | 图存储和遍历 |
|
||||
| `tools/scripts/review-pages.py` | P3 | LLM 评审器 |
|
||||
| `.githooks/pre-commit` | P2 | Git hook |
|
||||
| `.githooks/post-merge` | P2 | Git hook |
|
||||
| `tools/data/knowledge-graph.json` | P3 | 图数据库文件 |
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 阶段 | 变更 |
|
||||
|------|------|------|
|
||||
| `AGENTS.md` | P0/P1 | 修复冗余 + 新增 v2 特性定义 + 更新模板 |
|
||||
| `wiki/index.md` | P0 | 更新指标数字 |
|
||||
| `wiki/log.md` | P0/P1/P2 | 追加每次操作日志 |
|
||||
|
||||
### 依赖安装
|
||||
|
||||
```bash
|
||||
pip install pyyaml # 已安装,确认版本
|
||||
# Phase 3 可能需要:
|
||||
pip install networkx # 轻量图分析(替代 Neo4j)
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
# CLI / MCP / Skill 三层架构关系
|
||||
|
||||
## 一句话说明
|
||||
|
||||
- **CLI** = 底层程序,在 terminal 里执行
|
||||
- **MCP Server** = 通过 MCP 协议暴露工具,LLM 直接调用
|
||||
- **Skill** = 使用说明书,告诉 LLM 什么时候、怎么调用前两者
|
||||
|
||||
---
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Skill (SKILL.md) │
|
||||
│ 作用:告诉 LLM 怎么用工具 │
|
||||
│ - 何时用它(触发条件) │
|
||||
│ - 用什么命令/参数 │
|
||||
│ - 典型示例 │
|
||||
│ 不是程序,是"使用手册" │
|
||||
└──────────────────────┬───────────────────────────┘
|
||||
│ 被 LLM 读取
|
||||
┌──────────────▼───────────────┐
|
||||
│ MCP Server │
|
||||
│ LLM 通过 MCP 协议直接调用 │
|
||||
│ 结果结构化返回给 LLM │
|
||||
│ 无需写命令,LLM 自己组织调用 │
|
||||
└──────────────┬───────────────┘
|
||||
│ MCP 协议 / 工具调用
|
||||
┌──────────────▼───────────────┐
|
||||
│ CLI / 外部服务 │
|
||||
│ 底层执行程序 │
|
||||
│ mmx / npx / curl / gh 等 │
|
||||
└───────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键理解:Skill 和 MCP 是不同层次的东西,不互斥。**
|
||||
|
||||
- **有 MCP 无 Skill**:LLM 可能不知道怎么用
|
||||
- **有 Skill 无 MCP**:LLM 知道怎么用但没有工具可用
|
||||
- **有 Skill + MCP**:LLM 知道怎么用且能直接调用 ✅
|
||||
|
||||
---
|
||||
|
||||
## 当前环境概览
|
||||
|
||||
```
|
||||
MCP Servers: 6 个
|
||||
Skills: 93 个(含 MiniMax/GLM 相关 10+ 个)
|
||||
CLI 工具: mmx / hermes / gh / git / curl / npx 等
|
||||
主模型: MiniMax-M2.7 (minimax-cn)
|
||||
备选模型: GLM-4.7 (zai, GLM Coding Plan)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前 MCP Servers
|
||||
|
||||
| Server | 协议 | 功能 | Skill |
|
||||
|--------|------|------|-------|
|
||||
| `hermes-docs` | HTTPS | Hermes 中文文档搜索/读取 | — |
|
||||
| `minimax-token` | npx (stdio) | MiniMax Coding Plan 编程辅助(代码补全/搜索) | `minimax` |
|
||||
| `zread` | npx (stdio) | 通用网页读取 | — |
|
||||
| `glm-reader` | HTTP Stream | GLM 网页内容读取(markdown/text) | `glm-reader` |
|
||||
| `glm-search` | HTTP Stream | GLM 网络搜索(返回标题/URL/摘要) | `glm-search` |
|
||||
| `zai-vision` | npx (stdio) | GLM-4.6V 图片理解/截图分析/视频解析 | `zai-vision` |
|
||||
|
||||
### 关系说明
|
||||
|
||||
```
|
||||
glm-search ──MCP── LLM 直接调用搜索网页
|
||||
glm-reader ──MCP── LLM 直接调用读网页内容
|
||||
zai-vision ──MCP── LLM 直接调用理解图片/视频
|
||||
minimax-token ──MCP── LLM 直接调用编程辅助(不是搜索/图片)
|
||||
hermes-docs ──MCP── LLM 直接调用查 Hermes 文档
|
||||
zread ──MCP── LLM 直接调用读网页
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前 Skills(重点分组)
|
||||
|
||||
### 🔵 MiniMax 相关(mmx CLI + MCP)
|
||||
|
||||
| Skill | 功能 | 底层 |
|
||||
|-------|------|------|
|
||||
| `minimax` | 全能力汇总:图/视频/音乐/语音/搜索 | mmx-cli |
|
||||
| `minimax-image` | 文生图(MiniMax Image 01) | mmx CLI `mmx image` |
|
||||
| `minimax-video` | 文生视频(Hailuo 2.3) | mmx CLI `mmx video` |
|
||||
| `minimax-music` | 文生音乐(Music 2.6) | mmx CLI `mmx music` |
|
||||
| `minimax-speech` | 语音合成(Speech 2.8 HD) | mmx CLI `mmx speech synthesize` |
|
||||
| `minimax-search` | 网络搜索 | mmx CLI `mmx web` |
|
||||
|
||||
**关系:**
|
||||
- Skill 告诉 LLM:`mmx image "描述"` 可以生成图片
|
||||
- mmx CLI 是实际执行程序(已认证,可直接用)
|
||||
- `minimax-coding-plan-mcp` 是另一个 MCP,提供编程辅助能力
|
||||
|
||||
### � eco9; 🟢 GLM 相关(MCP 直接暴露)
|
||||
|
||||
| Skill | 功能 | 底层 |
|
||||
|-------|------|------|
|
||||
| `glm-search` | 网络搜索 | `glm-search` MCP |
|
||||
| `glm-reader` | 网页内容读取 | `glm-reader` MCP |
|
||||
| `glm-coding-plan` | GLM 全能力汇总(搜索/读页/图/视频理解) | GLM MCP 组合 |
|
||||
| `zai-vision` | 图片/截图/视频理解 | `zai-vision` MCP |
|
||||
|
||||
**关系:**
|
||||
- 全是 MCP 直接暴露,无独立 CLI
|
||||
- LLM 通过 MCP 协议直接调用这些工具
|
||||
- Skill 告诉 LLM 工具的用法和适用场景
|
||||
|
||||
### 🟡 其他常用 Skills
|
||||
|
||||
| Skill | 功能 |
|
||||
|-------|------|
|
||||
| `hermes-onboarding` | Hermes 学习路径(Phase 1-5) |
|
||||
| `hermes-agent` | Hermes 自身配置/扩展 |
|
||||
| `subagent-driven-development` | delegate_task 并行任务编排 |
|
||||
| `jupyter-live-kernel` | 交互式 Python(数据科学) |
|
||||
| `arxiv` | 学术论文搜索 |
|
||||
| `youtube-content` | YouTube 字幕→摘要/文章 |
|
||||
| `spotify` | Spotify 控制(播放/搜索/歌单) |
|
||||
| `github-pr-workflow` | GitHub PR 完整流程 |
|
||||
| `openhue` | 飞利浦 Hue 智能灯光控制 |
|
||||
| `xurl` | X/Twitter 发推/搜索/DM |
|
||||
|
||||
---
|
||||
|
||||
## 当前 CLI 工具
|
||||
|
||||
| CLI | 功能 |
|
||||
|-----|------|
|
||||
| `mmx` | MiniMax Token Plan 全能力入口(图/视频/音乐/语音/搜索),v1.0.13 |
|
||||
| `hermes` | Hermes Agent 自身管理(config/cron/mcp/skills 等) |
|
||||
| `gh` | GitHub CLI(需确认已安装) |
|
||||
| `git` | 版本控制 |
|
||||
| `curl` | HTTP 请求 |
|
||||
| `npx` | 运行 npm 包(MCP servers 用) |
|
||||
|
||||
---
|
||||
|
||||
## 使用场景对照
|
||||
|
||||
### 场景 1:让 LLM 搜索网页
|
||||
|
||||
**MCP 路线(直接):**
|
||||
```
|
||||
LLM → glm-search MCP tool → 返回搜索结果
|
||||
需要 skill glm-search 告诉 LLM 怎么用
|
||||
```
|
||||
|
||||
**CLI 路线(间接):**
|
||||
```
|
||||
LLM → terminal → mmx web "关键词" → 解析输出
|
||||
需要 skill minimax-search 告诉 LLM 命令格式
|
||||
```
|
||||
|
||||
### 场景 2:生成一张图片
|
||||
|
||||
**CLI 路线(实测):**
|
||||
```bash
|
||||
mmx image "一只橘色的猫在打哈欠" --out /tmp/cat.png
|
||||
```
|
||||
skill `minimax-image` 告诉 LLM 这个命令格式。
|
||||
|
||||
**MCP 路线:**
|
||||
当前无原生图片生成 MCP,通过 mmx CLI 走 skill 路线。
|
||||
|
||||
### 场景 3:理解一张图片
|
||||
|
||||
**MCP 路线:**
|
||||
```
|
||||
LLM → zai-vision MCP tool → 返回图片描述/分析
|
||||
skill zai-vision 告诉 LLM 工具用法
|
||||
```
|
||||
|
||||
### 场景 4:写好笔记让 LLM 记住
|
||||
|
||||
**Skill 路线:**
|
||||
```
|
||||
skill minimax-image → 告诉 LLM 用 mmx image 生成图片
|
||||
skill glm-search → 告诉 LLM 用 glm-search MCP 搜索
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 认证状态
|
||||
|
||||
```bash
|
||||
$ mmx auth status
|
||||
→ method: api-key, 已认证 ✅
|
||||
|
||||
$ hermes mcp list
|
||||
→ 6 个 MCP servers,全部 enabled ✅
|
||||
|
||||
$ hermes config get tts.provider
|
||||
→ minimax ✅(语音已配置)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见误区
|
||||
|
||||
1. **"有了 Skill 就不需要 MCP"** — 错。Skill 是文档,MCP 是工具,缺一不可。
|
||||
2. **"有了 MCP 就不需要 Skill"** — 错。LLM 需要 Skill 告诉它什么时候用、怎么描述参数。
|
||||
3. **"MiniMax MCP 就是 mmx CLI"** — 错。`minimax-coding-plan-mcp` 是另一个工具,提供编程辅助,和 `mmx image` 等 CLI 命令是不同的东西。
|
||||
4. **"配置了 Skill 就自动能用"** — 错。Skill 只是文档,底层程序必须可用(MCP server 在线,或 CLI 已安装认证)。
|
||||
|
||||
---
|
||||
|
||||
## 文件位置
|
||||
|
||||
- Skills:`~/.hermes/skills/`
|
||||
- MCP 配置:`~/.hermes/config.yaml` → `mcp_servers:`
|
||||
- Obsidian 笔记:`/home/obsidian/wiki/03-工具/layer-架构/`
|
||||
@@ -0,0 +1,265 @@
|
||||
# Hermes 多 Agent 协作机制官方文档解析
|
||||
|
||||
> 来源:Hermes Agent 官方文档(hermesagent.org.cn)
|
||||
> 整理时间:2026-05-20
|
||||
> 用途:Hermes(二休)建立独立于 OpenClaw 的多 Agent 协作闭环
|
||||
|
||||
---
|
||||
|
||||
## 核心发现:Hermes 有三套独立的 Multi-Agent 机制
|
||||
|
||||
这和 OpenClaw 的持久 Agent(main/news/assistant/research)模式完全不同。Hermes 的多 Agent 是**可组合的三层架构**:
|
||||
|
||||
| 机制 | OpenClaw 对应 | 用途 | 特点 |
|
||||
|------|-------------|------|------|
|
||||
| **Profiles** | 配置文件 | 同一机器运行多个独立 Agent | 完全隔离的配置/记忆/技能/网关 |
|
||||
| **delegate_task** | agentToAgent | 临时委派子 Agent | 最多 3 并发,隔离上下文,父 Agent 只收摘要 |
|
||||
| **cron jobs** | 定时任务 | autonomous Agent | 后台自动运行,可链式编排 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Profiles(配置文件)
|
||||
|
||||
### 核心概念
|
||||
|
||||
> 在同一台机器上运行多个独立的 Hermes Agent —— 每个 Agent 拥有自己的配置、API 密钥、记忆、会话、技能和网关。
|
||||
|
||||
### 关键特点
|
||||
|
||||
- 创建 Profile 后自动获得命令别名(如 `hermes profile create coder` → `coder chat`)
|
||||
- **完全隔离**:每个 Profile 有独立的 `config.yaml`、`.env`、`SOUL.md`、记忆、会话、技能、定时任务
|
||||
- 不同用途可运行不同 Profile:代码助手、个人机器人、研究 Agent
|
||||
- 不会相互干扰
|
||||
|
||||
### 命令示例
|
||||
|
||||
```bash
|
||||
hermes profile create research # 创建 research Profile
|
||||
research setup # 配置 API 密钥和模型
|
||||
research chat # 开始聊天
|
||||
|
||||
hermes profile list # 列出所有 Profile
|
||||
hermes profile use research # 切换默认 Profile
|
||||
```
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 需要**长期运行**的专用 Agent(如专门的代码 Agent、研究 Agent)
|
||||
- 不同 Agent 需要不同模型或 API 密钥
|
||||
- 需要完全隔离的记忆和上下文
|
||||
|
||||
---
|
||||
|
||||
## 2. delegate_task(子 Agent 委派)
|
||||
|
||||
### 官方文档核心要点
|
||||
|
||||
> `delegate_task` 工具会启动具有隔离上下文、受限工具集和独立终端会话的子 AIAgent 实例。每个子 Agent 都会获得一个全新的对话,并独立工作——只有其最终摘要才会进入父 Agent 的上下文。
|
||||
|
||||
### 重要约束
|
||||
|
||||
:::warning 子 Agent 一无所知
|
||||
子 Agent 从一个**完全全新的对话**开始。它们对父 Agent 的对话历史、之前的工具调用或任何先前讨论的内容都**一无所知**。唯一上下文来自 `goal` 和 `context` 字段。
|
||||
:::
|
||||
|
||||
**正确传递上下文:**
|
||||
```python
|
||||
# BAD - subagent 不知道 "the error" 是什么
|
||||
delegate_task(goal="Fix the error")
|
||||
|
||||
# GOOD - subagent 拥有所需的所有内容
|
||||
delegate_task(
|
||||
goal="Fix the TypeError in api/handlers.py",
|
||||
context="""The file api/handlers.py has a TypeError on line 47:
|
||||
'NoneType' object has no attribute 'get'.
|
||||
The function process_request() receives a dict from parse_body(),
|
||||
but parse_body() returns None when Content-Type is missing.
|
||||
The project is at /home/user/myproject and uses Python 3.11."""
|
||||
)
|
||||
```
|
||||
|
||||
### 单个任务
|
||||
|
||||
```python
|
||||
delegate_task(
|
||||
goal="Debug why tests fail",
|
||||
context="Error: assertion in test_foo.py line 42",
|
||||
toolsets=["terminal", "file"]
|
||||
)
|
||||
```
|
||||
|
||||
### 并行批量任务(最多 3 并发)
|
||||
|
||||
```python
|
||||
delegate_task(tasks=[
|
||||
{"goal": "Research topic A", "toolsets": ["web"]},
|
||||
{"goal": "Research topic B", "toolsets": ["web"]},
|
||||
{"goal": "Fix the build", "toolsets": ["terminal", "file"]}
|
||||
])
|
||||
```
|
||||
|
||||
### 工具集选择建议
|
||||
|
||||
| 任务类型 | 工具集 |
|
||||
|---------|--------|
|
||||
| 代码审查 / 重构 | `["terminal", "file"]` |
|
||||
| 网络研究 | `["web"]` |
|
||||
| 浏览器自动化 | `["browser"]` |
|
||||
| 定时任务管理 | `["cronjob"]` |
|
||||
| 文件操作 | `["file"]` |
|
||||
| 混合任务 | `["terminal", "file", "web"]` |
|
||||
|
||||
### 最大并发数
|
||||
|
||||
默认最多 **3 个并发子 Agent**。超出则排队等待。
|
||||
|
||||
### 深度限制
|
||||
|
||||
子 Agent(leaf role)**不能**进一步委派。Orchestrator role 可以委派,但 nesting depth 有限制。
|
||||
|
||||
---
|
||||
|
||||
## 3. cron jobs(定时 Autonomous Agent)
|
||||
|
||||
### 官方文档要点
|
||||
|
||||
- Jobs 运行在**新鲜 session** 中,无当前聊天上下文
|
||||
- Prompts 必须**自包含**
|
||||
- 如果提供 skills,按顺序加载后执行 prompt
|
||||
- 支持**链式编排**:Job A 收集数据 → Job B 处理 → Job C 汇总
|
||||
- Delivery 可指定:`origin`(回当前聊天)、`local`(仅保存)、`all`(所有连接渠道)
|
||||
|
||||
### delivery 参数
|
||||
|
||||
| 值 | 行为 |
|
||||
|---|------|
|
||||
| `origin` | 回当前聊天(默认) |
|
||||
| `local` | 仅保存到 `~/.hermes/cron/output/` |
|
||||
| `all` | 广播到所有已连接渠道 |
|
||||
| `platform:chat_id` | 指定特定渠道 |
|
||||
|
||||
### 关键安全规则
|
||||
|
||||
> **cron-run sessions should not recursively schedule more cron jobs.**
|
||||
|
||||
---
|
||||
|
||||
## Hermes Multi-Agent 协作闭环设计
|
||||
|
||||
基于官方文档三机制,设计 Hermes(二休)的多 Agent 协作闭环:
|
||||
|
||||
### 机制选型
|
||||
|
||||
| 任务类型 | 推荐机制 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 临时性研究任务 | `delegate_task` | Web 并行研究,1 次性 |
|
||||
| 临时性编码任务 | `delegate_task` | Terminal/File 工具集 |
|
||||
| 长期专用 Agent | **Profiles** | 独立的 research/coder Agent |
|
||||
| 定期自动任务 | **cron jobs** | 每小时/每天执行 |
|
||||
|
||||
### Hermes 协作闭环架构
|
||||
|
||||
```
|
||||
微信指令(老何)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Hermes(二休)- 主 Agent │
|
||||
│ - 理解任务 │
|
||||
│ - 拆解步骤 │
|
||||
│ - 判断:直接做 / delegate / cron │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
├──────────────────┬──────────────────┐
|
||||
▼ ▼ ▼
|
||||
delegate_task delegate_task cron job
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────────┐
|
||||
│Research │ │ Coding │ │ Autonomous │
|
||||
│ Agent │ │ Agent │ │ Agent │
|
||||
│(临时) │ │(临时) │ │(定时) │
|
||||
└─────────┘ └─────────┘ └─────────────┘
|
||||
│ │ │
|
||||
└──────────────────┴──────────────────┘
|
||||
│
|
||||
▼
|
||||
结果写 Wiki / Git push
|
||||
│
|
||||
▼
|
||||
PC 端 OpenCode 读取处理
|
||||
```
|
||||
|
||||
### 与 OpenClaw 的关键区别
|
||||
|
||||
| 维度 | OpenClaw | Hermes |
|
||||
|------|----------|--------|
|
||||
| Agent 类型 | 持久 Agent(main/news/assistant/research) | Profiles(持久)+ delegate_task(临时)+ cron(定时) |
|
||||
| 上下文共享 | 共享对话历史 | delegate_task 完全隔离,Profiles 独立隔离 |
|
||||
| 委派方式 | agentToAgent 配置 | delegate_task 工具调用 |
|
||||
| 通信机制 | 共享消息通道 | Profile 间通过 Wiki/Git 间接协作 |
|
||||
| 向量检索 | embedding-3(OpenAI API) | embedding-3(OpenAI API) |
|
||||
|
||||
### Wiki 协作路径
|
||||
|
||||
```
|
||||
Hermes委派子Agent → 处理结果写入Wiki → Git push到Gitee
|
||||
│
|
||||
▼
|
||||
PC端 OpenCode 读取Wiki
|
||||
│
|
||||
▼
|
||||
处理结果写回Wiki → Git push
|
||||
│
|
||||
▼
|
||||
Hermes 读取Wiki → 汇总结果
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实际配置记录
|
||||
|
||||
### Profile 创建(2026-05-20)
|
||||
|
||||
| Profile | 模型 | 用途 | 目录 |
|
||||
|--------|------|------|------|
|
||||
| `research` | glm-4.7 | 网络研究、信息收集 | `/home/ubuntu/.hermes/profiles/research/` |
|
||||
| `coder` | MiniMax-M2.7 | 代码审查/重构/执行 | `/home/ubuntu/.hermes/profiles/coder/` |
|
||||
|
||||
### Wiki 协作目录
|
||||
|
||||
```
|
||||
raw/
|
||||
├── research/ # research profile 输出
|
||||
└── coder/ # coder profile 输出
|
||||
```
|
||||
|
||||
### PC 端协作流程
|
||||
|
||||
```
|
||||
1. Hermes 委派 research/coder 子 Agent
|
||||
2. 结果写入 /home/obsidian/wiki/raw/research/ 或 raw/coder/
|
||||
3. Git push 到 Gitee
|
||||
4. PC 端 Obsidian Git 插件定时拉取
|
||||
5. PC OpenCode 读取处理
|
||||
6. PC 端处理结果写回 Wiki → Git push
|
||||
7. Hermes 读取 Wiki 汇总
|
||||
```
|
||||
|
||||
### Profile 命令
|
||||
|
||||
```bash
|
||||
research chat # 进入 research profile
|
||||
coder chat # 进入 coder profile
|
||||
research gateway start # 启动 research 网关(当前不需要)
|
||||
coder gateway start # 启动 coder 网关(当前不需要)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [子 Agent 委派](https://hermesagent.org.cn/docs/user-guide/features/delegation)
|
||||
- [配置文件](https://hermesagent.org.cn/docs/user-guide/profiles)
|
||||
- [Agent Loop 内部机制](https://hermesagent.org.cn/docs/developer-guide/agent-loop)
|
||||
- [架构](https://hermesagent.org.cn/docs/developer-guide/architecture)
|
||||
@@ -0,0 +1,327 @@
|
||||
# LLMWiki 知识基建完善计划
|
||||
|
||||
## 一、现状盘点
|
||||
|
||||
### 1.1 已有的东西
|
||||
|
||||
| 组件 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 目录结构 | ✅ 已有 | concepts/ entities/ sources/ reports/ syntheses/ |
|
||||
| Front Matter | ⚠️ 部分规范 | 概念页有,实体页有,但 top-level 混乱 |
|
||||
| auto-digest | ✅ 已有 | .openclaw-wiki/cache/agent-digest.json |
|
||||
| Reports | ✅ 已启用 | claim-health/contradictions/stale-pages 等6个报告 |
|
||||
| index.md | ⚠️ 残缺 | 有人工写的索引,但与 digest 不同步 |
|
||||
| WIKI.md | ✅ 已有 | vault 元数据(isolated/native) |
|
||||
| AGENTS.md | ✅ 已有 | Agent 交互规则 |
|
||||
|
||||
### 1.2 缺失的核心文件
|
||||
|
||||
| 文件 | 用途 | 状态 |
|
||||
|------|------|------|
|
||||
| `SCHEMA.md` | 领域规范:定义实体类型、关系、标签体系 | ❌ 不存在 |
|
||||
| `log.md` | 操作日志:记录增删改、来源追踪 | ❌ 不存在 |
|
||||
| `raw/` | 原始资料:PDF、HTML、剪藏的未处理原始内容 | ❌ 不存在(sources/ 混用了) |
|
||||
| `claims.jsonl` | 机器可读的结构化事实库 | ❌ 不存在 |
|
||||
| 标签体系 | 统一标签枚举 | ⚠️ 不一致 |
|
||||
|
||||
### 1.3 数据统计
|
||||
|
||||
```
|
||||
Page counts:
|
||||
concepts: 7 ✅ 有实质内容
|
||||
entities: 2 ⚠️ 有内容但未在 agent-digest 中计入
|
||||
sources: 40 ✅ 散落在 sources/
|
||||
synthesis: 1 ⚠️ 哈尔滨工程大学报告(来源不明)
|
||||
reports: 6 ✅ 自动生成
|
||||
|
||||
Claim count: 5
|
||||
missing evidence: 5 ← 所有 claims 都缺来源
|
||||
contested: 0
|
||||
stale: 0
|
||||
```
|
||||
|
||||
### 1.4 质量评估
|
||||
|
||||
**最大问题:所有 claims 都没有证据来源。**
|
||||
|
||||
这意味着知识库的"知识"部分其实还没有真正建立——页面虽然写了,但每个结论背后引用的原始资料没有被记录。
|
||||
|
||||
---
|
||||
|
||||
## 二、完善目标
|
||||
|
||||
### 2.1 目标状态
|
||||
|
||||
```
|
||||
最小可用层(50页规模):
|
||||
✅ sources/ 有原始资料
|
||||
✅ 实体页/概念页有完整 frontmatter
|
||||
✅ SCHEMA.md 定义规范
|
||||
✅ index.md 作为主入口
|
||||
✅ log.md 记录变更历史
|
||||
|
||||
进阶层(100页):
|
||||
✅ raw/ 分离原始资料
|
||||
✅ claims.jsonl 结构化事实库
|
||||
✅ 标签体系规范化
|
||||
✅ 置信度评分
|
||||
|
||||
知识图谱层(200页):
|
||||
⬜ 实体提取自动化
|
||||
⬜ 类型化关系定义
|
||||
⬜ 图遍历查询
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、分阶段实施方案
|
||||
|
||||
### 阶段 A:补全核心元文件(1-2天)
|
||||
|
||||
这是基础设施,补完才能谈其他所有功能。
|
||||
|
||||
#### A1. 创建 SCHEMA.md
|
||||
|
||||
```markdown
|
||||
# SCHEMA.md — 知识库领域规范
|
||||
|
||||
## 领域定义
|
||||
本知识库服务于:高等教育AI研究方向的知识积累与研究协作
|
||||
|
||||
## 实体类型(Entities)
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| person | 人物 | [[郭朝晖]] |
|
||||
| organization | 组织机构 | [[宝钢]] |
|
||||
| concept | 概念 | [[涌现]] |
|
||||
| project | 项目/系统 | [[Hermes Agent]] |
|
||||
| paper | 论文 | arXiv:2510.19247 |
|
||||
| article | 文章 | 微信公众号文章 |
|
||||
| tool | 工具/软件 | [[Obsidian]] |
|
||||
|
||||
## 概念类型(Concepts)
|
||||
|
||||
见 concepts/ 目录下的分类
|
||||
|
||||
## 关系类型
|
||||
|
||||
| 关系 | 说明 |
|
||||
|------|------|
|
||||
| uses | 使用某工具/方法 |
|
||||
| depends-on | 依赖某系统 |
|
||||
| caused-by | 由...导致 |
|
||||
| contradicts | 与...矛盾 |
|
||||
| supersedes | 替代旧内容 |
|
||||
| related-to | 相关 |
|
||||
|
||||
## 标签体系(Canonical Tags)
|
||||
|
||||
必须从以下标签中选择,禁止自定义标签:
|
||||
|
||||
- 研究方向:[higher-ed, AI-education, curriculum-design, assessment]
|
||||
- 技术类:[LLM, RAG, agent, knowledge-graph, embedding]
|
||||
- 项目类:[project, tool, skill, workflow]
|
||||
- 人物类:[researcher, practitioner, mentor]
|
||||
- 元类:[meta, methodology, reflection]
|
||||
|
||||
## 页面创建规则
|
||||
|
||||
1. 每个页面必须有完整 frontmatter
|
||||
2. 概念页必须包含:定义、关键特征、相关概念
|
||||
3. 实体页必须包含:基本信息、主要贡献、关联概念
|
||||
4. 来源页必须在 frontmatter 的 sources 字段中引用原始文件
|
||||
|
||||
## Claims 规范
|
||||
|
||||
每个 claim 格式:
|
||||
```json
|
||||
{"text": "...", "source": "来源ID", "confidence": 0.9, "date": "2026-05-20"}
|
||||
```
|
||||
|
||||
## Lint 规则
|
||||
|
||||
- 所有 [[Wikilinks]] 必须指向已存在的页面
|
||||
- sources 字段必须是已有来源的相对路径
|
||||
- date 格式必须是 YYYY-MM-DD
|
||||
```
|
||||
|
||||
#### A2. 创建 log.md
|
||||
|
||||
```markdown
|
||||
# log.md — 知识库操作日志
|
||||
|
||||
## 格式规范
|
||||
每条记录格式:[日期] [操作类型] [页面名] [操作人] [说明]
|
||||
|
||||
## 类型枚举
|
||||
- CREATE: 新建页面
|
||||
- UPDATE: 更新内容
|
||||
- DELETE: 删除/归档
|
||||
- INGEST: 批量摄入
|
||||
- SYNC: Git 同步
|
||||
- SCHEMA: 规范变更
|
||||
|
||||
## 记录
|
||||
|
||||
<!-- 起始记录 -->
|
||||
[2026-05-20] [SCHEMA] [SCHEMA.md] [老何] 初始化领域规范
|
||||
```
|
||||
|
||||
#### A3. 创建 raw/ 目录结构
|
||||
|
||||
```
|
||||
raw/
|
||||
├── papers/ # PDF + 提取的 markdown
|
||||
├── articles/ # 网页剪藏 HTML/markdown
|
||||
├── transcripts/ # 会议/课程转录
|
||||
└── datasets/ # 数据集说明文档
|
||||
```
|
||||
|
||||
**迁移任务**:将 sources/ 中的 PDF 和 HTML 分类移入 raw/,sources/ 专门放 AI 可读的 markdown 提炼版本。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 B:规范化现有内容(1-2天)
|
||||
|
||||
#### B1. 统一 frontmatter 规范
|
||||
|
||||
现有页面 frontmatter 不一致,示例:
|
||||
|
||||
```yaml
|
||||
# 现有(不统一)
|
||||
---
|
||||
title: 涌现
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
type: concept
|
||||
tags: [concept, complexity, physics, consciousness, systems-theory]
|
||||
sources: [sources/涌现的本质是什么-万物本源说.html]
|
||||
confidence: high
|
||||
---
|
||||
|
||||
# 规范目标
|
||||
---
|
||||
type: concept
|
||||
title: 涌现
|
||||
created: 2026-05-15
|
||||
updated: 2026-05-15
|
||||
tags: [complexity, systems-theory, consciousness]
|
||||
sources: [raw/articles/涌现的本质是什么-万物本源说.html]
|
||||
confidence: high
|
||||
claims:
|
||||
- text: "大量简单个体遵循简单规则聚集互动,会自发诞生全新宏观属性"
|
||||
source: "raw/articles/涌现的本质是什么-万物本源说.html"
|
||||
confidence: 0.9
|
||||
date: 2026-05-15
|
||||
---
|
||||
```
|
||||
|
||||
**任务**:遍历所有概念页和实体页,补全缺失的 frontmatter 字段。
|
||||
|
||||
#### B2. 修复 agent-digest 中的 entity 计数
|
||||
|
||||
当前 agent-digest 显示 `entity: 0`,但 entities/ 下有 2 个文件。
|
||||
原因:frontmatter 中 entity 页面没有声明 `type: entity`。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 C:建立 claims.jsonl(持续)
|
||||
|
||||
#### C1. 什么是 claims
|
||||
|
||||
Claims 是知识库中每个可校验的事实的结构化记录。
|
||||
|
||||
```jsonl
|
||||
{"page": "concepts/涌现.md", "text": "涌现指大量简单个体遵循简单规则聚集互动,会自发诞生全新宏观属性", "source": "sources/涌现的本质是什么-万物本源说.html", "confidence": 0.9, "extracted": "2026-05-20"}
|
||||
{"page": "entities/郭朝晖.md", "text": "郭朝晖曾任职于宝钢", "source": "sources/我的科研经历-反思与成长-郭朝晖.html", "confidence": 0.95, "extracted": "2026-05-20"}
|
||||
```
|
||||
|
||||
#### C2. 提取策略
|
||||
|
||||
从现有页面提取:
|
||||
1. 遍历所有概念页和实体页
|
||||
2. 提取带有 `[引用]` 标记的句子
|
||||
3. 关联到 sources/ 中的原始文件
|
||||
4. 写入 `claims.jsonl`
|
||||
|
||||
新页面的 claims 在创建时同步生成。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 D:Git 同步 + 冲突规避(阶段二同步做)
|
||||
|
||||
#### D1. Git 初始化
|
||||
|
||||
腾讯云 VM 端:
|
||||
```bash
|
||||
cd /home/obsidian/wiki
|
||||
git init
|
||||
git add -A
|
||||
git commit -m "初始化 LLMWiki"
|
||||
git remote add origin git@gitee.com:你的用户名/仓库名.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
#### D2. 分区写入规则(防冲突)
|
||||
|
||||
```markdown
|
||||
## Git 同步的分区写入规则
|
||||
|
||||
| Agent | 可写目录 |
|
||||
|-------|---------|
|
||||
| main | 顶层 + concepts/ + entities/ + 03-工具/ |
|
||||
| news | sources/ + raw/ |
|
||||
| assistant | 04-Tools/ + reports/ |
|
||||
| research | syntheses/ + 06-学术研究/ |
|
||||
| PC OpenCode | 全部(用户操作层) |
|
||||
|
||||
原则:
|
||||
- 每个 Agent 有主要负责的目录
|
||||
- 跨越分区写入前先检查 git status
|
||||
- 冲突时:最后提交者负责解决
|
||||
```
|
||||
|
||||
#### D3. 同步触发机制
|
||||
|
||||
```bash
|
||||
# cron 任务:每30分钟自动同步
|
||||
*/30 * * * * cd /home/obsidian/wiki && git pull --rebase && git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、基础设施清单
|
||||
|
||||
| 任务 | 优先级 | 工作量 | 依赖 |
|
||||
|------|--------|--------|------|
|
||||
| 创建 SCHEMA.md | 🔴 高 | 1小时 | 无 |
|
||||
| 创建 log.md | 🔴 高 | 30分钟 | 无 |
|
||||
| 创建 raw/ 目录 | 🔴 高 | 30分钟 | 无 |
|
||||
| 规范化现有 frontmatter | 🟡 中 | 2-3小时 | SCHEMA.md |
|
||||
| 迁移 sources/ 到 raw/ | 🟡 中 | 1-2小时 | raw/ 建立 |
|
||||
| 建立 claims.jsonl | 🟡 中 | 2-3小时 | frontmatter 规范 |
|
||||
| Git 初始化 + Gitee | 🔴 高 | 1小时 | Gitee 仓库地址 |
|
||||
| 配置 cron 同步 | 🟡 中 | 30分钟 | Git 初始化 |
|
||||
| 补充 entity index | 🟡 中 | 30分钟 | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 五、下一步行动
|
||||
|
||||
### 今天可以做
|
||||
|
||||
1. **创建 SCHEMA.md** — 定义领域、实体类型、标签体系
|
||||
2. **创建 log.md** — 建立操作日志
|
||||
3. **创建 raw/ 目录** — 分离原始资料
|
||||
|
||||
### 本周可以做
|
||||
|
||||
1. 规范化所有现有页面的 frontmatter(7个概念页 + 2个实体页)
|
||||
2. Git 初始化 + Gitee 关联
|
||||
|
||||
### 需要确认
|
||||
|
||||
1. **Gitee 仓库地址** — 云端 Git 初始化需要
|
||||
2. **同步频率** — 30分钟自动还是手动触发?
|
||||
3. **PC 端 Obsidian 版本** — 是否 1.12+,决定能否用 Obsidian CLI
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
type: concept
|
||||
subtype: technology
|
||||
title: Windows Subsystem for Linux
|
||||
created: 2026-05-20
|
||||
updated: 2026-05-20
|
||||
tags: [tool, workflow, Windows, Linux]
|
||||
sources: ["https://docs.microsoft.com/zh-cn/windows/wsl/"]
|
||||
confidence: high
|
||||
---
|
||||
|
||||
# Windows 11 WSL 使用指南
|
||||
|
||||
> Windows Subsystem for Linux,Windows 11 内置的 Linux 子系统
|
||||
|
||||
## 简介
|
||||
|
||||
WSL 让你在 Windows 里直接运行 Linux 子系统,无需虚拟机或双系统。Win 11 自带 WSL2。
|
||||
|
||||
## 安装
|
||||
|
||||
```powershell
|
||||
# PowerShell 以管理员身份运行
|
||||
wsl --install
|
||||
|
||||
# 重启后自动完成 Ubuntu 安装
|
||||
```
|
||||
|
||||
其他发行版:
|
||||
```powershell
|
||||
wsl --install -d Debian
|
||||
wsl --install -d Ubuntu-22.04
|
||||
wsl --list --online # 查看可用发行版
|
||||
```
|
||||
|
||||
## WSL1 vs WSL2
|
||||
|
||||
| 特性 | WSL1 | WSL2 |
|
||||
|------|------|------|
|
||||
| 架构 | Linux ELF 二进制翻译 | 完整 Linux 内核虚拟机 |
|
||||
| 性能(文件系统 I/O) | 快 | 更快(跨系统访问稍慢) |
|
||||
| GPU/CUDA | 不支持 | 支持 |
|
||||
| 系统调用兼容性 | 少数不支持 | 几乎完全兼容 |
|
||||
| 内存占用 | 更小 | 动态分配 |
|
||||
|
||||
日常开发推荐 **WSL2**。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```powershell
|
||||
wsl --status # 查看状态
|
||||
wsl --list -v # 列出已安装的发行版
|
||||
wsl -d Ubuntu # 启动指定发行版
|
||||
wsl --shutdown # 关闭所有 WSL(重置)
|
||||
wsl --update # 更新 WSL 内核
|
||||
wsl -e cat /etc/os-release # 不进入 Shell 直接执行命令
|
||||
```
|
||||
|
||||
Windows 终端直接操作 Linux 文件:
|
||||
```bash
|
||||
wsl ls ~/
|
||||
```
|
||||
|
||||
## 文件互访
|
||||
|
||||
| 方向 | 路径 |
|
||||
|------|------|
|
||||
| Linux → Windows | `cd /mnt/c/Users/你的用户名` |
|
||||
| Windows → Linux | `\\wsl$\Ubuntu\home\username` |
|
||||
|
||||
## 常用场景
|
||||
|
||||
### 1. 开发环境
|
||||
- 使用 Linux 原生工具链(bash, git, ssh, vim)
|
||||
- Node.js / Python / Go 开发
|
||||
- 不用纠结 Windows 路径问题
|
||||
|
||||
### 2. 容器 & DevOps
|
||||
- Docker Desktop 底层用 WSL2 运行
|
||||
- 直接在 Linux 环境打包/测试
|
||||
- Kubernetes / Docker 编排
|
||||
|
||||
### 3. 机器学习 / AI
|
||||
- WSL2 + CUDA GPU 加速
|
||||
- 跑 TensorFlow / PyTorch 训练
|
||||
- 用 Ollama 本地跑 LLM
|
||||
|
||||
### 4. 服务器管理
|
||||
- SSH 远程连接服务器
|
||||
- 跑 Shell 脚本 / Cron 定时任务
|
||||
- 使用 Ansible / Terraform 管理 Infra
|
||||
|
||||
### 5. 学习 Linux
|
||||
- 零成本体验 Linux 环境
|
||||
- 熟悉命令行 / 系统管理
|
||||
- 无破坏 Windows 的风险
|
||||
|
||||
## 核心优势
|
||||
|
||||
| 优势 | 说明 |
|
||||
|------|------|
|
||||
| 性能 | 直接调用 Windows 内核,接近原生 Linux 效率 |
|
||||
| 资源 | 比虚拟机轻量,内存/磁盘占用小 |
|
||||
| 互通 | 与 Windows 文件系统无缝互操作 |
|
||||
| 终端 | 用 Windows Terminal 体验原生 Linux 终端 |
|
||||
| 开发 | 直接用 Linux 工具链 |
|
||||
| GPU 支持 | WSL2 支持 GPU 加速,可跑 CUDA/机器学习 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **版本**:Win 11 自带 WSL2,Win 10 需要 2004+ 且开启虚拟机平台
|
||||
- **数据**:Linux 文件尽量放 WSL 内部,跨系统 I/O 有性能损耗
|
||||
- **杀毒软件**:部分 AV 会显著拖慢 WSL2 文件系统,必要时加白
|
||||
|
||||
## 参考
|
||||
|
||||
- [WSL 官方文档](https://docs.microsoft.com/zh-cn/windows/wsl/)
|
||||
@@ -0,0 +1,60 @@
|
||||
# MarkItDown + Wiki 集成
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 转换文件并加入Wiki
|
||||
~/.openclaw/workspace/04-Tools/wiki-ingest.sh document.pdf
|
||||
|
||||
# 指定Wiki页面名
|
||||
~/.openclaw/workspace/04-Tools/wiki-ingest.sh document.pdf 我的文档
|
||||
|
||||
# 直接使用markitdown
|
||||
~/.venv/markitdown/bin/markitdown file.pdf -o output.md
|
||||
```
|
||||
|
||||
## 支持格式
|
||||
|
||||
| 格式 | 说明 |
|
||||
|------|------|
|
||||
| PDF | 文档、扫描件 |
|
||||
| DOCX/XLSX/PPTX | Office文档 |
|
||||
| 图片 | EXIF + OCR |
|
||||
| 音频 | EXIF + 转录 |
|
||||
| HTML | 网页 |
|
||||
| EPUB | 电子书 |
|
||||
| CSV/JSON/XML | 数据文件 |
|
||||
|
||||
## 依赖
|
||||
|
||||
- MarkItDown: `~/.venv/markitdown/bin/markitdown`
|
||||
- 虚拟环境: `~/.venv/markitdown`
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
python3 -m venv ~/.venv/markitdown
|
||||
~/.venv/markitdown/bin/pip install 'markitdown[all]'
|
||||
```
|
||||
|
||||
## Wiki结构
|
||||
|
||||
```
|
||||
~/.openclaw/wiki/
|
||||
├── concepts/ # 概念文档
|
||||
├── methods/ # 方法论
|
||||
├── examples/ # 案例
|
||||
└── index/ # 索引
|
||||
```
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 用户发送文件(PDF/Word/Excel等)
|
||||
2. Agent使用MarkItDown转换
|
||||
3. 提取关键内容整理成Wiki格式
|
||||
4. 添加frontmatter元数据
|
||||
5. 保存到对应分类目录
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-04-14*
|
||||
@@ -0,0 +1,228 @@
|
||||
# OpenClaw 多 Agent 协作方案:实际配置核实
|
||||
|
||||
## 核实结论
|
||||
|
||||
原分析报告有若干关键错误,以下基于实际配置数据修正。
|
||||
|
||||
---
|
||||
|
||||
## 一、实际系统配置
|
||||
|
||||
### 1.1 Agent 现状(实际)
|
||||
|
||||
| Agent ID | 名称 | 工作区 | 状态 |
|
||||
|-----------|------|--------|------|
|
||||
| main | 主代理 | /root/.openclaw/workspace | ✅ 活跃 |
|
||||
| news | 新闻助手 | /root/.openclaw/workspace-news | ✅ 活跃 |
|
||||
| assistant | 个人助理 | /root/.openclaw/workspace-assistant | ✅ 活跃 |
|
||||
| research | 研究助手 | /root/.openclaw/workspace-research | ✅ 活跃 |
|
||||
| weixin2 | 小新 | /root/.openclaw/agents/weixin2 | ⚠️ 有目录但未入 config |
|
||||
| zz | - | /root/.openclaw/agents/zz | ❌ 未配置 |
|
||||
|
||||
**原分析错误**:报告说"6个Agent",实际活跃的4个。weixin2 和 zz 有目录但不在活跃配置里。
|
||||
|
||||
### 1.2 agentToAgent(已确认开启)
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
agentToAgent:
|
||||
enabled: true
|
||||
allow: ["main", "news", "assistant", "research"]
|
||||
```
|
||||
|
||||
**好消息**:多 Agent 通信已经启用,不需要额外配置。
|
||||
|
||||
---
|
||||
|
||||
## 二、向量检索实际配置
|
||||
|
||||
### 2.1 记忆搜索配置
|
||||
|
||||
```yaml
|
||||
memorySearch:
|
||||
provider: openai # OpenAI 兼容接口
|
||||
enabled: true
|
||||
model: embedding-3 # GLM embedding-3 模型
|
||||
remote:
|
||||
baseUrl: https://open.bigmodel.cn/api/paas/v4
|
||||
```
|
||||
|
||||
**结论**:向量检索已配置,通过 BigModel CN API 的 embedding-3 实现,不是 SQLite。原分析"70%相似度"修正为 80%。
|
||||
|
||||
### 2.2 LanceDB-pro 状态
|
||||
|
||||
```
|
||||
config.yaml 配置了 memory-lancedb-pro 插件
|
||||
但 /root/.openclaw/memory/lancedb-pro/ 目录是空的
|
||||
插件已配置但未实际启用本地向量存储
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、知识库实际配置(双 Wiki 架构)
|
||||
|
||||
系统存在 **两套 Wiki**,需要区分:
|
||||
|
||||
| 位置 | 类型 | 用途 |
|
||||
|------|------|------|
|
||||
| /root/.openclaw/wiki/ | OpenClaw memory-wiki | Agent 记忆层,bridge 模式 |
|
||||
| /home/obsidian/wiki/ | 标准 Obsidian vault | 你的主知识库,Obsidian 桌面端用 |
|
||||
|
||||
### 3.1 OpenClaw wiki(Agent 用)
|
||||
|
||||
```
|
||||
wiki/
|
||||
├── concepts/ # 概念页
|
||||
├── entities/ # 实体页
|
||||
├── sources/ # 原始资料
|
||||
├── reports/ # 报告
|
||||
├── syntheses/ # 综合
|
||||
├── main/ # 主代理子空间
|
||||
├── index.md
|
||||
├── WIKI.md
|
||||
└── AGENTS.md
|
||||
```
|
||||
|
||||
- vault mode: bridge(连接外部 Obsidian)
|
||||
- render mode: obsidian
|
||||
- search corpus: all
|
||||
|
||||
### 3.2 Obsidian vault(你本地用)
|
||||
|
||||
/home/obsidian/wiki/ - 这是你日常在 Obsidian 里用的笔记库。
|
||||
|
||||
### 3.3 共享知识库
|
||||
|
||||
```
|
||||
workspace-shared/
|
||||
├── 01-公共知识/
|
||||
├── 02-协作记录/
|
||||
├── 03-资源库/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
README 明确定义了使用规则:只读优先、分 Agent 写入不同文件、定期清理。
|
||||
|
||||
---
|
||||
|
||||
## 四、Git 同步现状
|
||||
|
||||
### 4.1 PC 端已有同步脚本
|
||||
|
||||
`/root/.openclaw/workspace/Obsidian/sync-gitea.ps1` - PowerShell 脚本,功能:
|
||||
- 检测本地变更 → git add/commit
|
||||
- git pull 远程
|
||||
- 检测冲突(发现冲突则退出告警)
|
||||
- git push
|
||||
|
||||
### 4.2 腾讯云端现状
|
||||
|
||||
**尚未配置 Git 同步**。云端 VM 的 /home/obsidian/wiki/ 没有初始化 Git,也没有关联 Gitee。
|
||||
|
||||
### 4.3 风险重新评估
|
||||
|
||||
| 风险 | 原方案 | 实际问题 |
|
||||
|------|--------|---------|
|
||||
| 同时写入冲突 | "使用文件锁或分区存储" | sync-gitea.ps1 只检测冲突后退出,没有自动解决 |
|
||||
| 冲突通知 | 未提及 | 冲突时脚本 exit 2,但谁来处理? |
|
||||
|
||||
---
|
||||
|
||||
## 五、Coding Subagent 现状
|
||||
|
||||
```
|
||||
/root/.openclaw/subagents/runs.json
|
||||
→ {"version": 2, "runs": {}}
|
||||
```
|
||||
|
||||
**subagent 机制存在,但目前没有配置任何 subagent。**
|
||||
|
||||
原分析推荐用 `openclaw subagent create` 创建 coding subagent,但:
|
||||
- openclaw 命令本身有权限问题(/root/.local/share/pnpm/openclaw Permission denied)
|
||||
- 没有找到 `subagent create` 的实际命令文档
|
||||
- subagent 机制和 workspace-agent 是两套系统
|
||||
|
||||
**建议**:先用现有的 assistant agent 承担 coding 任务,不一定要单独建 subagent。
|
||||
|
||||
---
|
||||
|
||||
## 六、关键修正汇总
|
||||
|
||||
| 项目 | 原分析结论 | 实际核实结论 |
|
||||
|------|-----------|------------|
|
||||
| 活跃 Agent 数量 | 6个 | 4个(main/news/assistant/research) |
|
||||
| agentToAgent | "需要配置" | ✅ 已启用 |
|
||||
| 向量数据库 | SQLite 70%相似度 | 有 embedding-3 向量搜索,80% |
|
||||
| Wiki 结构 | 一套 | 两套(OpenClaw wiki + Obsidian vault) |
|
||||
| 共享知识库 | "需要创建" | workspace-shared 已存在 |
|
||||
| Git 同步 | "需要开发" | PC 端脚本已有,云端未配置 |
|
||||
| coding subagent | "用命令创建" | subagent 机制空,命令未验证 |
|
||||
|
||||
---
|
||||
|
||||
## 七、修正后的分阶段方案
|
||||
|
||||
### 阶段一:验证多 Agent 协作(1-2天)
|
||||
|
||||
**目标**:验证 agentToAgent 委派流程
|
||||
|
||||
现有条件已满足,只需写 SOUL.md 协作规则。
|
||||
|
||||
```
|
||||
阶段一可以立即执行,不需要任何配置变更
|
||||
```
|
||||
|
||||
### 阶段二:建立云端 Git 同步(2-3天)
|
||||
|
||||
```
|
||||
1. 腾讯云 VM 上初始化 Git(/home/obsidian/wiki/ → git init)
|
||||
2. 关联 Gitee 仓库(和 PC 端同一仓库)
|
||||
3. 配置 cron 自动同步(每30分钟)
|
||||
4. 添加冲突检测脚本
|
||||
```
|
||||
|
||||
### 阶段三:完善 Wiki 体系(持续)
|
||||
|
||||
```
|
||||
1. 补全 /home/obsidian/wiki/ 的 LLMWiki 元文件(SCHEMA.md, index.md, log.md)
|
||||
2. 将 wiki-vault-maintainer skill 的规范落地到 Obsidian
|
||||
3. 统一两套 Wiki 的边界和使用规则
|
||||
```
|
||||
|
||||
### 阶段四:Coding 能力建设(如果需要)
|
||||
|
||||
```
|
||||
方案A:改造 assistant agent,给它写专门的 SOUL.md 增加 coding 职责
|
||||
方案B:等 subagent 机制验证后再迁过去
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、立即可执行的行动
|
||||
|
||||
### 今天可以做
|
||||
|
||||
1. **给 main agent 写 SOUL.md**,加入委派规则
|
||||
2. **验证 agentToAgent** 是否真的工作:向 main 发消息委派给 news,看能否通信
|
||||
|
||||
### 本周可以做
|
||||
|
||||
1. 初始化云端 wiki 的 Git:进入 /home/obsidian/wiki/ → git init → 关联 Gitee
|
||||
2. PC 端和云端同时配置 cron 同步
|
||||
3. 写 workspace-shared/02-协作记录/ 里的协作规则文档
|
||||
|
||||
### 需要先确认的
|
||||
|
||||
1. Gitee 仓库地址是什么?
|
||||
2. weixin2 和 zz 这两个 agent 还要不要激活?
|
||||
|
||||
---
|
||||
|
||||
## 九、风险项重新评估
|
||||
|
||||
| 风险 | 可能性 | 影响 | 应对 |
|
||||
|------|--------|------|------|
|
||||
| Git 同步冲突 | 高 | 中 | 明确各端写入分区,发生冲突告警人工处理 |
|
||||
| weixin2/zz agent 冲突 | 中 | 低 | 明确不激活或删除 |
|
||||
| 两套 Wiki 混乱 | 中 | 高 | 明确边界:OpenClaw wiki 是 Agent 记忆层,Obsidian 是用户交互层 |
|
||||
| subagent 命令不可用 | 高 | 中 | 先用 assistant agent 试,不等 subagent |
|
||||
@@ -0,0 +1,326 @@
|
||||
# 新安装技能使用指南
|
||||
|
||||
> 创建时间: 2026-04-28
|
||||
> 服务器: 腾讯云轻量 Ubuntu
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [安全扫描类](#安全扫描类)
|
||||
2. [系统监控类](#系统监控类)
|
||||
3. [磁盘清理类](#磁盘清理类)
|
||||
4. [Web服务类](#web服务类)
|
||||
5. [腾讯云服务类](#腾讯云服务类)
|
||||
6. [笔记Wiki类](#笔记wiki类)
|
||||
|
||||
---
|
||||
|
||||
## 安全扫描类
|
||||
|
||||
### 🔒 skill-vetter
|
||||
|
||||
**功能**: 安装前安全扫描,防止"技能投毒"
|
||||
|
||||
**使用场景**:
|
||||
- 安装任何第三方技能前必用
|
||||
- 扫描已有技能的安全隐患
|
||||
- 检查代码中的 Red Flags
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "用 skill-vetter 扫描 xxx"
|
||||
```
|
||||
|
||||
**检查项目**:
|
||||
- ❌ eval/exec 滥用
|
||||
- ❌ 未经授权访问 .ssh/
|
||||
- ❌ 凭据窃取模式
|
||||
- ❌ 外部 curl/wget 下载
|
||||
- ❌ Base64 混淆代码
|
||||
|
||||
**风险等级**:
|
||||
| 等级 | 含义 | 操作 |
|
||||
|------|------|------|
|
||||
| 🟢 LOW | 低风险 | 可直接安装 |
|
||||
| 🟡 MEDIUM | 中风险 | 需完整代码审查 |
|
||||
| 🔴 HIGH | 高风险 | 需人工确认 |
|
||||
| ⛔ EXTREME | 极高风险 | 禁止安装 |
|
||||
|
||||
---
|
||||
|
||||
## 系统监控类
|
||||
|
||||
### 📊 system-resource-monitor
|
||||
|
||||
**功能**: CPU/内存/磁盘/运行时监控
|
||||
|
||||
**使用场景**:
|
||||
- 查看服务器运行状态
|
||||
- 排查性能问题
|
||||
- 定期健康检查
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "查看系统状态" 或 "系统资源监控"
|
||||
```
|
||||
|
||||
**监控指标**:
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| Uptime | 系统运行时长 |
|
||||
| Load Average | 1/5/15分钟负载 |
|
||||
| Memory | RAM 和 Swap 使用 |
|
||||
| Disk | 根分区容量和使用率 |
|
||||
|
||||
**当前服务器状态示例**:
|
||||
```
|
||||
Uptime: 23 hours, 52 minutes
|
||||
System Load: 1.59, 1.52, 1.54
|
||||
Memory Usage: 1.4Gi / 1.9Gi (74%)
|
||||
Swap Usage: 1.2Gi / 8.0Gi (15%)
|
||||
Disk Usage: 27G / 40G (70%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 磁盘清理类
|
||||
|
||||
### 🧹 diskclean
|
||||
|
||||
**功能**: AI辅助磁盘扫描和清理
|
||||
|
||||
**使用场景**:
|
||||
- 磁盘空间不足时
|
||||
- 定期维护清理
|
||||
- 清理 Python/node_modules 等缓存
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
# 扫描
|
||||
diskclean scan
|
||||
|
||||
# 预览清理(dry run)
|
||||
diskclean clean --dry
|
||||
|
||||
# 执行清理
|
||||
diskclean clean --confirm
|
||||
```
|
||||
|
||||
**清理分层**:
|
||||
|
||||
| 分层 | 说明 | 自动删除 |
|
||||
|------|------|----------|
|
||||
| **Safe Tier** | 白名单 + 超过7天 | ✅ 是 |
|
||||
| **Suggest Tier** | 其他所有项 | ❌ 需确认 |
|
||||
|
||||
**可清理项目**:
|
||||
| 类别 | 年龄门槛 | 可自动清理 |
|
||||
|------|----------|------------|
|
||||
| node_modules | 7天 | ✅ |
|
||||
| Python 缓存 | 7天 | ✅ |
|
||||
| 构建输出 | 7天 | ✅ |
|
||||
| Docker | - | ❌ |
|
||||
| 大文件下载 | - | ❌ |
|
||||
|
||||
**安全规则**:
|
||||
- ❌ 永不删除 `$HOME` 之外的文件
|
||||
- ❌ 永不删除 `.git` 目录
|
||||
- ❌ 永不删除源代码/文档
|
||||
- ❌ 永不不带 `--dry` 直接执行
|
||||
|
||||
**当前扫描结果**:
|
||||
- 总可回收: ~2.9 GB
|
||||
- 主要来自: `.venv/` Python虚拟环境
|
||||
|
||||
---
|
||||
|
||||
## Web服务类
|
||||
|
||||
### 🌐 nginx-config-creator
|
||||
|
||||
**功能**: Nginx 反向代理配置、验证、回滚
|
||||
|
||||
**使用场景**:
|
||||
- 配置新的网站反代
|
||||
- 修改现有 Nginx 配置
|
||||
- SSL 证书配置
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "帮我配置 Nginx 反向代理"
|
||||
```
|
||||
|
||||
**功能**:
|
||||
- 配置生成
|
||||
- 配置验证
|
||||
- 语法检查
|
||||
- 回滚支持
|
||||
|
||||
---
|
||||
|
||||
### 🌐 agent-browser
|
||||
|
||||
**功能**: 无头浏览器自动化
|
||||
|
||||
**使用场景**:
|
||||
- 网页截图
|
||||
- 表单自动填写
|
||||
- 网页数据抓取
|
||||
- UI自动化测试
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "帮我截图 xxx 网页"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 腾讯云服务类
|
||||
|
||||
### ☁️ tencentcloud-lighthouse-skill
|
||||
|
||||
**功能**: 腾讯云轻量应用服务器管理
|
||||
|
||||
**使用场景**:
|
||||
- 查看服务器信息
|
||||
- 管理应用市场
|
||||
- 监控服务器状态
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "查看轻量服务器状态"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ☁️ tencent-cos-skill
|
||||
|
||||
**功能**: 腾讯云对象存储管理
|
||||
|
||||
**使用场景**:
|
||||
- 上传/下载文件
|
||||
- 管理存储桶
|
||||
- 配置访问权限
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "上传文件到 COS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📄 tencent-docs
|
||||
|
||||
**功能**: 腾讯文档集成
|
||||
|
||||
**使用场景**:
|
||||
- 读取腾讯文档
|
||||
- 创建新文档
|
||||
- 文档同步
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "读取腾讯文档 xxx"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 笔记Wiki类
|
||||
|
||||
### 📝 obsidian 系列
|
||||
|
||||
**已安装组件**:
|
||||
| 组件 | 功能 |
|
||||
|------|------|
|
||||
| obsidian | 核心集成 |
|
||||
| obsidian-cli-official | Obsidian CLI 工具 |
|
||||
| obsidian-direct | 直接访问 |
|
||||
| obsidian-bases | 知识库基础 |
|
||||
| obsidian-daily | 每日笔记 |
|
||||
| obsidian-markdown | Markdown 处理 |
|
||||
|
||||
**使用场景**:
|
||||
- 创建和管理笔记
|
||||
- 知识库维护
|
||||
- 每日复盘
|
||||
|
||||
**使用方法**:
|
||||
```
|
||||
询问我: "创建 Obsidian 笔记"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📝 wiki-local / wiki-vault-maintainer
|
||||
|
||||
**功能**: 本地 Wiki 维护
|
||||
|
||||
**使用场景**:
|
||||
- 维护知识库结构
|
||||
- 清理孤立页面
|
||||
- 索引管理
|
||||
|
||||
---
|
||||
|
||||
## 🔧 快速参考
|
||||
|
||||
### 推荐安装顺序(新服务器)
|
||||
|
||||
```
|
||||
1. 安全底座
|
||||
skill-vetter
|
||||
|
||||
2. 系统监控
|
||||
system-resource-monitor
|
||||
|
||||
3. 磁盘清理
|
||||
diskclean
|
||||
|
||||
4. Nginx 配置
|
||||
nginx-config-creator
|
||||
|
||||
5. 腾讯云服务
|
||||
tencentcloud-lighthouse-skill
|
||||
tencent-cos-skill
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
| 任务 | 命令/询问 |
|
||||
|------|-----------|
|
||||
| 查看系统状态 | "系统资源监控" |
|
||||
| 扫描磁盘 | "diskclean scan" |
|
||||
| 预览清理 | "diskclean clean --dry" |
|
||||
| 安全扫描技能 | "用 skill-vetter 扫描 xxx" |
|
||||
| 配置 Nginx | "nginx-config-creator" |
|
||||
|
||||
---
|
||||
|
||||
## 📊 已安装技能清单
|
||||
|
||||
**总计**: 43 个技能
|
||||
|
||||
| 类别 | 数量 | 代表技能 |
|
||||
|------|------|----------|
|
||||
| 🌐 网络/浏览器 | 4 | agent-browser, brave-search |
|
||||
| ☁️ 云服务 | 3 | 腾讯云系列 |
|
||||
| 📝 笔记/Wiki | 7 | obsidian系列 |
|
||||
| 🧹 系统工具 | 3 | diskclean, nginx-config |
|
||||
| 📚 学术 | 2 | arxiv, arxiv-watcher |
|
||||
| 📰 新闻 | 3 | cctv-news-fetcher |
|
||||
| 🔍 搜索 | 4 | tavily, brave, desearch |
|
||||
| 🖼️ 媒体 | 3 | ima-image-ai, ima-tts-ai |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 安全提醒
|
||||
|
||||
1. **所有技能均已通过安全扫描** ✅
|
||||
2. **腾讯云技能需要配置凭据才可用**
|
||||
3. **ima-* 系列需要 IMA_API_KEY 环境变量**
|
||||
4. **brave-search 需要 BRAVE_SEARCH_API_KEY**
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-04-28*
|
||||
@@ -0,0 +1,76 @@
|
||||
# EPUB to Obsidian Converter
|
||||
|
||||
Convert EPUB books to Obsidian Markdown notes with proper chapter structure and frontmatter.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install html2text beautifulsoup4
|
||||
|
||||
# Ensure Calibre is installed (for ebook-convert)
|
||||
scoop install calibre
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
python -m tools.epub_converter <input.epub> <output_dir> [--author "Author"] [--tags "tag1,tag2"]
|
||||
|
||||
# Example
|
||||
python -m tools.epub_converter "book.epub" "output_folder" --author "Author Name" --tags "tag1,tag2"
|
||||
```
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from tools.epub_converter import EpubConverter
|
||||
|
||||
converter = EpubConverter()
|
||||
files = converter.convert(
|
||||
"input.epub",
|
||||
"output_dir",
|
||||
author="Author Name",
|
||||
tags=["tag1", "tag2"]
|
||||
)
|
||||
print(f"Created {len(files)} files")
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Extracts metadata from EPUB (title, author, publisher, date)
|
||||
- Handles OEBPS format with anchor-based TOC
|
||||
- Calibre normalization for complex EPUB structures
|
||||
- Splits into per-chapter Obsidian notes
|
||||
- Standard YAML frontmatter for Obsidian
|
||||
|
||||
## Output Format
|
||||
|
||||
Each chapter file includes:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Chapter Title"
|
||||
categories:
|
||||
- "[[LLM Wiki]]"
|
||||
- "[[Books]]"
|
||||
tags:
|
||||
- book
|
||||
- epub
|
||||
- custom-tags
|
||||
book: "[[Book Title]]"
|
||||
author: "[[Author Name]]"
|
||||
section: "Section Name"
|
||||
created: "YYYY-MM-DD"
|
||||
type: book-chapter
|
||||
---
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- Calibre (ebook-convert)
|
||||
- html2text (recommended)
|
||||
- beautifulsoup4 (optional fallback)
|
||||
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
EPUB to Obsidian Converter - Reusable Tool
|
||||
|
||||
Pipeline:
|
||||
1. Extract metadata from original EPUB (preserves correct UTF-8 encoding)
|
||||
2. Calibre normalizes EPUB (handles OEBPS/anchor format, splits monolithic HTML)
|
||||
3. Extract TOC from original EPUB
|
||||
4. Parse HTML content from normalized EPUB
|
||||
5. Split into per-chapter Obsidian notes with frontmatter
|
||||
|
||||
Usage:
|
||||
python -m tools.epub_converter <input.epub> <output_dir> [--author "Author"] [--tags "tag1,tag2"]
|
||||
|
||||
Or as module:
|
||||
from tools.epub_converter import EpubConverter
|
||||
converter = EpubConverter()
|
||||
converter.convert("input.epub", "output_dir")
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from xml.dom import minidom
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
SECTION_KEYWORDS = ["编", "附录", "结束语", "修订版说明"]
|
||||
FRONT_MATTER_LABELS = {"致读者", "前言"}
|
||||
|
||||
|
||||
class EpubConverter:
|
||||
def __init__(self):
|
||||
self.metadata = {}
|
||||
self.toc = []
|
||||
self.html_files = {}
|
||||
|
||||
def convert(
|
||||
self,
|
||||
input_epub: str,
|
||||
output_dir: str,
|
||||
author: str = None,
|
||||
tags: list = None,
|
||||
keep_temp: bool = False,
|
||||
) -> list[str]:
|
||||
input_epub = os.path.abspath(input_epub)
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
|
||||
if not os.path.exists(input_epub):
|
||||
raise FileNotFoundError(f"Input file not found: {input_epub}")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
normalized_epub = self._normalize_epub(input_epub, tmpdir)
|
||||
self._extract_metadata(input_epub)
|
||||
self._extract_toc(input_epub)
|
||||
self._extract_html_content(normalized_epub)
|
||||
|
||||
if author:
|
||||
self.metadata["creator"] = author
|
||||
|
||||
files = self._split_and_write(output_dir, tags or [])
|
||||
return files
|
||||
|
||||
def _run_calibre(self, input_epub: str, output_epub: str) -> bool:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ebook-convert", input_epub, output_epub],
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return os.path.exists(output_epub)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _normalize_epub(self, input_epub: str, tmpdir: str) -> str:
|
||||
normalized_epub = os.path.join(tmpdir, "normalized.epub")
|
||||
if not self._run_calibre(input_epub, normalized_epub):
|
||||
return input_epub
|
||||
return normalized_epub
|
||||
|
||||
def _extract_metadata(self, epub_path: str):
|
||||
z = zipfile.ZipFile(epub_path, "r")
|
||||
opf_files = [f for f in z.namelist() if f.endswith(".opf")]
|
||||
if not opf_files:
|
||||
z.close()
|
||||
return
|
||||
|
||||
opf_raw = z.read(opf_files[0])
|
||||
z.close()
|
||||
opf_text = opf_raw.decode("utf-8", errors="replace")
|
||||
doc = minidom.parseString(opf_text)
|
||||
|
||||
for tag in ["title", "creator", "language", "publisher"]:
|
||||
elements = doc.getElementsByTagName(f"dc:{tag}")
|
||||
if elements and elements[0].firstChild:
|
||||
self.metadata[tag] = elements[0].firstChild.nodeValue.strip()
|
||||
|
||||
date_els = doc.getElementsByTagName("dc:date")
|
||||
if date_els and date_els[0].firstChild:
|
||||
self.metadata["date"] = date_els[0].firstChild.nodeValue.strip()[:10]
|
||||
|
||||
def _extract_toc(self, epub_path: str):
|
||||
z = zipfile.ZipFile(epub_path, "r")
|
||||
ncx_files = [f for f in z.namelist() if f.endswith(".ncx")]
|
||||
if not ncx_files:
|
||||
z.close()
|
||||
return
|
||||
|
||||
toc_xml = z.read(ncx_files[0]).decode("utf-8", errors="replace")
|
||||
z.close()
|
||||
doc = minidom.parseString(toc_xml)
|
||||
nav_points = doc.getElementsByTagName("navPoint")
|
||||
|
||||
for np in nav_points:
|
||||
text_el = np.getElementsByTagName("text")
|
||||
content_el = np.getElementsByTagName("content")
|
||||
if not text_el or not content_el:
|
||||
continue
|
||||
|
||||
label = (
|
||||
text_el[0].firstChild.nodeValue.strip() if text_el[0].firstChild else ""
|
||||
)
|
||||
src = content_el[0].getAttribute("src")
|
||||
|
||||
is_section = (
|
||||
any(kw in label for kw in SECTION_KEYWORDS)
|
||||
or label in FRONT_MATTER_LABELS
|
||||
or label.endswith("(代序)")
|
||||
)
|
||||
|
||||
self.toc.append({"label": label, "src": src, "is_section": is_section})
|
||||
|
||||
def _extract_html_content(self, epub_path: str):
|
||||
z = zipfile.ZipFile(epub_path, "r")
|
||||
html_files = sorted(
|
||||
f
|
||||
for f in z.namelist()
|
||||
if f.endswith((".html", ".xhtml")) and "text" in f.lower()
|
||||
)
|
||||
|
||||
for hf in html_files:
|
||||
raw = z.read(hf)
|
||||
try:
|
||||
self.html_files[hf] = raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
self.html_files[hf] = raw.decode("gb18030", errors="replace")
|
||||
z.close()
|
||||
|
||||
def _split_and_write(self, output_dir: str, tags: list) -> list[str]:
|
||||
created_files = []
|
||||
chapter_map = self._build_chapter_map()
|
||||
if not chapter_map:
|
||||
return []
|
||||
|
||||
grouped = self._group_by_html_file(chapter_map)
|
||||
all_chapters = []
|
||||
|
||||
for html_file, chapters in grouped.items():
|
||||
html = self.html_files.get(html_file, "")
|
||||
if not html:
|
||||
continue
|
||||
|
||||
anchors = [c["anchor"] for c in chapters if c["anchor"]]
|
||||
if anchors:
|
||||
chunks = self._split_by_anchors(html, anchors)
|
||||
else:
|
||||
chunks = [("", html)]
|
||||
|
||||
for chapter, (anchor_id, chunk) in zip(chapters, chunks):
|
||||
md = self._html_to_markdown(chunk) if chunk else ""
|
||||
md = self._clean_markdown(md)
|
||||
if md.strip():
|
||||
all_chapters.append(
|
||||
{
|
||||
"label": chapter["label"],
|
||||
"is_section": chapter["is_section"],
|
||||
"content": md,
|
||||
}
|
||||
)
|
||||
|
||||
current_section = ""
|
||||
for ch in all_chapters:
|
||||
if ch["is_section"]:
|
||||
current_section = ch["label"]
|
||||
|
||||
filename = self._sanitize_filename(ch["label"]) + ".md"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
counter = 1
|
||||
base = filepath
|
||||
while os.path.exists(filepath):
|
||||
name, ext = os.path.splitext(base)
|
||||
filepath = f"{name}_{counter}{ext}"
|
||||
counter += 1
|
||||
|
||||
fm = self._generate_frontmatter(
|
||||
ch["label"], current_section, ch["is_section"], tags
|
||||
)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(fm)
|
||||
f.write(ch["content"])
|
||||
|
||||
created_files.append(filepath)
|
||||
|
||||
return created_files
|
||||
|
||||
def _build_chapter_map(self):
|
||||
chapter_map = []
|
||||
for i, entry in enumerate(self.toc):
|
||||
src = entry["src"]
|
||||
anchor = ""
|
||||
if "#" in src:
|
||||
path, anchor = src.split("#", 1)
|
||||
else:
|
||||
path = src
|
||||
|
||||
matching_file = None
|
||||
for hf in self.html_files:
|
||||
if path in hf or hf.endswith(path):
|
||||
matching_file = hf
|
||||
break
|
||||
|
||||
if matching_file:
|
||||
chapter_map.append(
|
||||
{
|
||||
"label": entry["label"],
|
||||
"is_section": entry["is_section"],
|
||||
"html_file": matching_file,
|
||||
"anchor": anchor,
|
||||
"toc_index": i,
|
||||
}
|
||||
)
|
||||
|
||||
return chapter_map
|
||||
|
||||
def _group_by_html_file(self, chapter_map):
|
||||
grouped = {}
|
||||
for ch in chapter_map:
|
||||
hf = ch["html_file"]
|
||||
if hf not in grouped:
|
||||
grouped[hf] = []
|
||||
grouped[hf].append(ch)
|
||||
return grouped
|
||||
|
||||
def _split_by_anchors(self, html: str, anchors: list[str]):
|
||||
chunks = []
|
||||
for i, anchor in enumerate(anchors):
|
||||
pattern = re.compile(
|
||||
rf'<(?:a|span|div|p|h[1-6])[^>]*(?:id|name)=["\']?{re.escape(anchor)}["\']?[^>]*>',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
match = pattern.search(html)
|
||||
if match:
|
||||
start = match.start()
|
||||
if i + 1 < len(anchors):
|
||||
next_pattern = re.compile(
|
||||
rf'<(?:a|span|div|p|h[1-6])[^>]*(?:id|name)=["\']?{re.escape(anchors[i + 1])}["\']?[^>]*>',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
next_match = next_pattern.search(html, start + 1)
|
||||
end = next_match.start() if next_match else len(html)
|
||||
else:
|
||||
end = len(html)
|
||||
chunks.append((anchor, html[start:end]))
|
||||
else:
|
||||
chunks.append((anchor, ""))
|
||||
return chunks
|
||||
|
||||
def _html_to_markdown(self, html_content: str) -> str:
|
||||
try:
|
||||
import html2text
|
||||
|
||||
h = html2text.HTML2Text()
|
||||
h.body_width = 0
|
||||
h.unicode_snob = True
|
||||
h.protect_links = True
|
||||
h.wrap_links = False
|
||||
return h.handle(html_content)
|
||||
except ImportError:
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
return soup.get_text("\n")
|
||||
except ImportError:
|
||||
return re.sub(r"<[^>]+>", "", html_content)
|
||||
|
||||
def _clean_markdown(self, md: str) -> str:
|
||||
md = re.sub(r"\[([^\]]*)\]\([^\)]*\.html[^\)]*\)", r"\1", md)
|
||||
md = re.sub(r"\n{4,}", "\n\n\n", md)
|
||||
return md.strip()
|
||||
|
||||
def _sanitize_filename(self, name: str) -> str:
|
||||
name = re.sub(r'[<>:"/\\|?*]', "", name)
|
||||
name = re.sub(r"\s+", " ", name).strip()
|
||||
if len(name) > 80:
|
||||
name = name[:80]
|
||||
return name
|
||||
|
||||
def _generate_frontmatter(
|
||||
self, title: str, section: str, is_section: bool, tags: list
|
||||
) -> str:
|
||||
book_title = self.metadata.get("title", "Unknown")
|
||||
author = self.metadata.get("creator", "")
|
||||
date = self.metadata.get("date", "")
|
||||
|
||||
tag_list = ["book", "epub"] + tags
|
||||
if is_section:
|
||||
tag_list.append("section")
|
||||
|
||||
tags_str = "\n - ".join(tag_list)
|
||||
|
||||
return f"""---
|
||||
title: "{title}"
|
||||
categories:
|
||||
- "[[LLM Wiki]]"
|
||||
- "[[Books]]"
|
||||
tags:
|
||||
- {tags_str}
|
||||
book: "[[{book_title}]]"
|
||||
author: "[[{author}]]"
|
||||
section: "{section}"
|
||||
created: "{date}"
|
||||
type: book-chapter
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Convert EPUB to Obsidian notes")
|
||||
parser.add_argument("input", help="Input EPUB file path")
|
||||
parser.add_argument("output_dir", help="Output directory for chapter files")
|
||||
parser.add_argument("--author", help="Override author name")
|
||||
parser.add_argument("--tags", help="Comma-separated tags")
|
||||
args = parser.parse_args()
|
||||
|
||||
converter = EpubConverter()
|
||||
files = converter.convert(
|
||||
args.input,
|
||||
args.output_dir,
|
||||
author=args.author,
|
||||
tags=args.tags.split(",") if args.tags else [],
|
||||
)
|
||||
print(f"Done! Created {len(files)} files in {args.output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,261 @@
|
||||
---
|
||||
created: 2026-04-28
|
||||
tags:
|
||||
- note
|
||||
- journal
|
||||
para: []
|
||||
---
|
||||
|
||||
# Reading Club v2 — 项目指南与使用说明
|
||||
|
||||
> 版本:v2 | 日期:2026-04-28 | 状态:已实现,待测试
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
Reading Club 是基于 opencode 的交互式多智能体阅读协作框架。它不是传统的 AI 摘要工具,而是一个**以人类读者为中心的读书会**——三个 AI 角色作为你的对话伙伴,围绕你正在阅读的书籍章节展开深度讨论。
|
||||
|
||||
### 与传统 AI 阅读工具的区别
|
||||
|
||||
| 传统工具 | Reading Club v2 |
|
||||
|----------|----------------|
|
||||
| AI 输出摘要,人类阅读 | 人类先发言,AI 回应 |
|
||||
| 单向输出 | 多轮对话 |
|
||||
| 中立客观的分析 | 带有鲜明认知偏见的角色 |
|
||||
| 一次性结果 | 交互式探索,人类可以追问、反驳、换角度 |
|
||||
| 输出即弃 | 自动归档为 LLM Wiki 页面 |
|
||||
|
||||
---
|
||||
|
||||
## 二、文件结构
|
||||
|
||||
```
|
||||
kepano-obsidian-main/
|
||||
├── .agents/skills/reading-club/
|
||||
│ └── SKILL.md # ← 核心 Skill 定义(v2)
|
||||
├── wiki/
|
||||
│ └── ReadingClub.md # ← Wiki 工具页面(刚生成)
|
||||
├── raw/
|
||||
│ ├── 呼吸之间_李谨伯/ # ← 可用书籍
|
||||
│ │ ├── 调息.md
|
||||
│ │ ├── 胎息法.md
|
||||
│ │ └── ...
|
||||
│ └── 《大国大民》王志纲/ # ← 可用书籍
|
||||
│ ├── 《大国大民》第一章-我是怎么读中国的.md
|
||||
│ └── ...
|
||||
└── .sisyphus/
|
||||
└── plans/
|
||||
└── reading-club-guide.md # ← 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、快速开始
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. opencode 已安装并运行
|
||||
2. 至少一本书已转换到 `raw/` 目录(使用 `tools/epub_converter/`)
|
||||
3. `.agents/skills/reading-club/SKILL.md` 存在
|
||||
|
||||
### 第一步:选择书籍和章节
|
||||
|
||||
查看可用书籍:
|
||||
|
||||
```
|
||||
ls raw/
|
||||
```
|
||||
|
||||
查看某本书的章节:
|
||||
|
||||
```
|
||||
ls "raw/呼吸之间_李谨伯/"
|
||||
```
|
||||
|
||||
### 第二步:启动 Reading Club
|
||||
|
||||
在 opencode 对话中输入以下任一形式:
|
||||
|
||||
**触发词形式**:
|
||||
```
|
||||
/reading-club book_path="raw/呼吸之间_李谨伯" chapter="调息.md"
|
||||
```
|
||||
|
||||
**自然语言形式**:
|
||||
```
|
||||
我想用阅读俱乐部讨论《呼吸之间》的调息章节
|
||||
```
|
||||
|
||||
**带模式指定**:
|
||||
```
|
||||
/reading-club book_path="raw/《大国大民》王志纲" chapter="《大国大民》第一章-我是怎么读中国的.md" mode="deep"
|
||||
```
|
||||
|
||||
### 第三步:参与讨论
|
||||
|
||||
框架会自动引导你进入讨论:
|
||||
|
||||
1. **阅读开头** — 系统展示章节开头 3-5 句
|
||||
2. **说出第一反应** — 任何想法都行,一个词也可以
|
||||
3. **听取 Agent 分析** — 三个 Agent 会回应你的反应
|
||||
4. **自由对话** — 反驳、追问、提问、换角度
|
||||
5. **结束讨论** — 说"停止"即可
|
||||
|
||||
### 第四步:查看输出
|
||||
|
||||
讨论结束后,wiki 页面自动生成到:
|
||||
|
||||
```
|
||||
wiki/{书名}-{章节名}-ReadingClub.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、详细使用指南
|
||||
|
||||
### 4.1 讨论模式选择
|
||||
|
||||
| 场景 | 推荐模式 | 理由 |
|
||||
|------|----------|------|
|
||||
| 第一次使用,想体验一下 | `browse` | 4 轮快速完成,了解流程 |
|
||||
| 正常阅读,想深入讨论 | `balanced` | 8 轮,平衡深度和效率 |
|
||||
| 学术研究,需要深度分析 | `deep` | 14 轮,充分挖掘 |
|
||||
| 自己掌控节奏 | `human-led` | 你决定何时结束 |
|
||||
|
||||
### 4.2 人类参与策略
|
||||
|
||||
**最佳实践**:
|
||||
- 第一反应**越直觉越好**,不要过度思考
|
||||
- 当 Agent 说了一些你不认同的,**直接反驳**
|
||||
- 如果某个观点触发了联想,**追问**那个方向
|
||||
- 不确定说什么时,**"pass"** 让 Agent 继续也可以
|
||||
|
||||
**避免**:
|
||||
- 只说"继续"让 Agent 自说自话(浪费了交互设计)
|
||||
- 等待 Agent 给出"正确答案"(没有正确答案)
|
||||
- 想要面面俱到(聚焦 1-2 个最有感觉的点)
|
||||
|
||||
### 4.3 交互指令速查
|
||||
|
||||
| 你想做什么 | 怎么说 | Agent 会怎样 |
|
||||
|------------|--------|-------------|
|
||||
| 分享想法 | "我觉得这里说其实不只是地理…" | 回应你的具体观点 |
|
||||
| 提问 | "为什么作者用'读'中国?" | 认真回答你的问题 |
|
||||
| 反驳某个 Agent | "我不同意 Critic" | 与你辩论 |
|
||||
| 深入某个话题 | "追问:一方水土养一方人" | 聚焦该话题 |
|
||||
| 换个视角 | "从反面想想" | 重新从对立面分析 |
|
||||
| 不想说话 | "pass" | Agent 继续讨论 |
|
||||
| 结束讨论 | "停止" | 生成 Wiki 页面 |
|
||||
|
||||
### 4.4 输出内容解读
|
||||
|
||||
Wiki 页面包含以下部分:
|
||||
|
||||
| 部分 | 内容 | 价值 |
|
||||
|------|------|------|
|
||||
| **读者的第一反应** | 你最初的直觉 | 记录阅读起点 |
|
||||
| **初始回应** | 3 个 Agent 的第一轮分析 | 三个不同视角 |
|
||||
| **深入讨论** | 后续轮次记录 | 思想碰撞过程 |
|
||||
| **人类思考轨迹** | 你的观点如何变化 | 元认知记录 |
|
||||
| **Agent 共识与分歧** | Agent 之间的一致和分歧 | 多角度分析 |
|
||||
| **讨论总结** | 关键洞见 + 未决问题 | 行动指引 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构设计说明
|
||||
|
||||
### 5.1 状态机
|
||||
|
||||
```
|
||||
INIT → HUMAN_FIRST_READ → SEED_ROUND → PRESENT_SEED → CONVERSATION_LOOP* → SYNTHESIS → DONE
|
||||
```
|
||||
|
||||
每个状态的详细说明见 `.agents/skills/reading-club/SKILL.md`。
|
||||
|
||||
### 5.2 Agent 角色
|
||||
|
||||
三个角色通过 prompt engineering 区分,底层使用相同的 `build` subagent:
|
||||
|
||||
| 角色 | 刻意编码的偏见 | 目的 |
|
||||
|------|---------------|------|
|
||||
| Summarizer | 同化偏见 — 找秩序 | 梳理逻辑,找出核心结构 |
|
||||
| Critic | 对抗性偏见 — 找缺陷 | 质疑假设,发现盲点 |
|
||||
| Questioner | 好奇偏见 — 找问题 | 追问深层含义,打开新视角 |
|
||||
|
||||
### 5.3 上下文管理
|
||||
|
||||
| 层级 | 内容 | 策略 |
|
||||
|------|------|------|
|
||||
| 最近 3 轮 | 完整记录 | 逐字传递 |
|
||||
| 更早轮次 | 压缩摘要 | 每轮 1-2 句 |
|
||||
| 章节内容 | 首轮完整 | 之后仅引用路径 |
|
||||
| 总限制 | 6000 tokens | 超出则压缩最早的轮次 |
|
||||
|
||||
### 5.4 技术实现
|
||||
|
||||
- **编排**:Sisyphus(主 Agent)管理状态机和人类交互
|
||||
- **Agent 调度**:通过 `task(subagent_type="build")` 实现
|
||||
- **SEED_ROUND**:3 个 Agent 并行 `run_in_background=true`
|
||||
- **CONVERSATION_LOOP**:每轮 1 个 Agent 同步 `run_in_background=false`
|
||||
- **Wiki 输出**:SYNTHESIS 阶段由 build Agent 编译
|
||||
|
||||
---
|
||||
|
||||
## 六、v1 → v2 变更日志
|
||||
|
||||
| 方面 | v1 | v2 |
|
||||
|------|----|----|
|
||||
| 人类角色 | 旁观者,只能说"继续/停止" | 主角,每轮先发言 |
|
||||
| Agent 语气 | Agent 之间对话 | 用"你"直接称呼人类 |
|
||||
| 每轮结构 | Agent→Agent→checkpoint | 人类→Agent→人类→Agent |
|
||||
| 讨论模式 | 3 种(browse/balanced/deep) | 4 种(+human-led) |
|
||||
| Prompt 格式 | ===摘要===/===核心主张=== 刚性格式 | 自然段落,以问题结尾 |
|
||||
| Wiki 输出 | "人类参与"是附录 | "人类思考轨迹"是核心 |
|
||||
| 人类指令 | 2 种(继续/停止) | 8 种(反驳/追问/换角度等) |
|
||||
| 错误处理 | 基础 | 增加"连续3轮pass主动询问" |
|
||||
|
||||
---
|
||||
|
||||
## 七、故障排除
|
||||
|
||||
| 问题 | 原因 | 解决 |
|
||||
| ---------- | ------------ | ----------------------- |
|
||||
| 触发词不识别 | SKILL.md 未加载 | 重启 opencode 会话 |
|
||||
| Agent 调度失败 | 模型配置/网络 | 检查 oh-my-openagent.json |
|
||||
| 章节不存在 | 路径错误 | 框架会自动列出可用章节 |
|
||||
| 章节太长 | >8000 tokens | 框架建议按小节分段 |
|
||||
| Wiki 写入冲突 | 文件已存在 | 自动添加时间戳后缀 |
|
||||
| Agent 输出泛泛 | 人类输入太模糊 | 尝试说具体的观点或问题 |
|
||||
|
||||
---
|
||||
|
||||
## 八、测试计划
|
||||
|
||||
### 待执行测试
|
||||
|
||||
- [ ] **基础功能测试**:`browse` 模式,4 轮,验证完整流程
|
||||
- [ ] **人类交互测试**:测试 8 种交互指令
|
||||
- [ ] **Wiki 输出验证**:检查 frontmatter、内容结构、溯源标注
|
||||
- [ ] **长章节测试**:`deep` 模式,14 轮
|
||||
- [ ] **边界测试**:章节不存在、Agent 调度失败、人类立即说停止
|
||||
|
||||
### 推荐测试章节
|
||||
|
||||
| 优先级 | 书籍 | 章节 | 理由 |
|
||||
|--------|------|------|------|
|
||||
| 1 | 《大国大民》 | 第一章 | 205行,长度适中 |
|
||||
| 2 | 《呼吸之间》 | 调息 | 核心章节,内容集中 |
|
||||
| 3 | 《呼吸之间》 | 胎息法 | 较短,适合快速测试 |
|
||||
|
||||
---
|
||||
|
||||
## 九、相关资源
|
||||
|
||||
| 资源 | 位置 | 说明 |
|
||||
| --------- | -------------------------------------- | ------------ |
|
||||
| SKILL.md | `.agents/skills/reading-club/SKILL.md` | 核心定义 |
|
||||
| Wiki 页面 | `wiki/ReadingClub.md` | 工具参考 |
|
||||
| 启动指南 | `.sisyphus/START-READING-CLUB.md` | 快速启动(v1,需更新) |
|
||||
| AGENTS.md | `AGENTS.md` | 仓库操作规范 |
|
||||
| EPUB 转换 | `tools/epub_converter/` | 准备书籍内容 |
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
check-staleness.py — 检测 wiki 页面的时效性
|
||||
|
||||
检查每页的 last_reviewed 字段是否超过 review_interval_days,
|
||||
输出过时页面列表和从未审查的页面列表。
|
||||
|
||||
用法:
|
||||
python tools/scripts/check-staleness.py # 标准输出
|
||||
python tools/scripts/check-staleness.py --json # JSON 格式
|
||||
python tools/scripts/check-staleness.py --overdue-only # 仅过时页面
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
TODAY = date.today()
|
||||
DEFAULT_INTERVAL = 180
|
||||
|
||||
|
||||
def check_page(fp: Path) -> dict:
|
||||
"""检查单页,返回结果 dict"""
|
||||
result = {"page": fp.stem, "stale": False, "overdue_days": 0, "reason": ""}
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m:
|
||||
result["reason"] = "no_frontmatter"
|
||||
return result
|
||||
if not yaml:
|
||||
result["reason"] = "no_pyyaml"
|
||||
return result
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
result["reason"] = "yaml_parse_error"
|
||||
return result
|
||||
|
||||
last_str = front.get("last_reviewed", "")
|
||||
interval = front.get("review_interval_days", DEFAULT_INTERVAL)
|
||||
|
||||
if not last_str:
|
||||
result["reason"] = "never_reviewed"
|
||||
result["stale"] = True
|
||||
return result
|
||||
|
||||
try:
|
||||
last = date.fromisoformat(str(last_str))
|
||||
diff = (TODAY - last).days
|
||||
if diff > interval:
|
||||
result["stale"] = True
|
||||
result["overdue_days"] = diff - interval
|
||||
result["reason"] = f"overdue_by_{diff - interval}_days"
|
||||
except ValueError:
|
||||
result["reason"] = f"invalid_date_{last_str}"
|
||||
result["stale"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Check wiki page staleness")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
parser.add_argument("--overdue-only", action="store_true", help="Only stale pages")
|
||||
args = parser.parse_args()
|
||||
|
||||
all_pages = sorted(WIKI.glob("*.md"))
|
||||
results = []
|
||||
for fp in all_pages:
|
||||
if fp.name in ("index.md", "log.md"):
|
||||
continue
|
||||
results.append(check_page(fp))
|
||||
|
||||
stale = [r for r in results if r["stale"]]
|
||||
never = [r for r in stale if r["reason"] == "never_reviewed"]
|
||||
overdue = [r for r in stale if r["reason"].startswith("overdue")]
|
||||
|
||||
if args.json:
|
||||
output = {
|
||||
"total": len(results),
|
||||
"stale": len(stale),
|
||||
"never_reviewed": len(never),
|
||||
"overdue": len(overdue),
|
||||
"pages": stale if args.overdue_only else results
|
||||
}
|
||||
print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
print(f"=== Staleness Check ({TODAY}) ===")
|
||||
print(f" Total: {len(results)} pages")
|
||||
print(f" Stale: {len(stale)}")
|
||||
print(f" Never reviewed: {len(never)}")
|
||||
print(f" Overdue: {len(overdue)}")
|
||||
|
||||
if overdue:
|
||||
print(f"\n Overdue pages (top 20):")
|
||||
for r in sorted(overdue, key=lambda x: -x["overdue_days"])[:20]:
|
||||
print(f" [[{r['page']}]] — overdue by {r['overdue_days']} days")
|
||||
|
||||
if never:
|
||||
print(f"\n Never reviewed (top 20):")
|
||||
for r in never[:20]:
|
||||
print(f" [[{r['page']}]]")
|
||||
if len(never) > 20:
|
||||
print(f" ... and {len(never)-20} more")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
detect-conflicts.py — 检测 wiki 页面间的矛盾
|
||||
|
||||
通过扫描 relations 中的 conflicts_with 关系,
|
||||
检查双方是否都引用了对方,并输出矛盾报告。
|
||||
|
||||
用法:
|
||||
python tools/scripts/detect-conflicts.py # 标准输出
|
||||
python tools/scripts/detect-conflicts.py --json # JSON 格式
|
||||
python tools/scripts/detect-conflicts.py --auto-callout # 自动添加 callout
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
|
||||
def read_frontmatter(fp: Path) -> dict | None:
|
||||
"""读取 frontmatter,返回 dict 或 None"""
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m or not yaml:
|
||||
return None
|
||||
try:
|
||||
return yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def detect():
|
||||
"""检测所有 conflicts_with 关系,返回冲突报告列表"""
|
||||
conflicts = []
|
||||
pages = {}
|
||||
|
||||
for fp in WIKI.glob("*.md"):
|
||||
if fp.name in ("index.md", "log.md"):
|
||||
continue
|
||||
front = read_frontmatter(fp)
|
||||
if front is None:
|
||||
continue
|
||||
pages[fp.stem] = front
|
||||
for rel in front.get("relations", []):
|
||||
if rel["type"] == "conflicts_with":
|
||||
target = rel.get("target", "").strip("[]")
|
||||
conf_source = rel.get("confidence", 3)
|
||||
if target and target != fp.stem:
|
||||
conflicts.append({
|
||||
"source": fp.stem,
|
||||
"target": target,
|
||||
"confidence": conf_source,
|
||||
"bidirectional": False
|
||||
})
|
||||
|
||||
# 检查双向性
|
||||
for c in conflicts:
|
||||
target_front = pages.get(c["target"])
|
||||
if target_front:
|
||||
for rel in target_front.get("relations", []):
|
||||
if rel["type"] == "conflicts_with" and rel.get("target", "").strip("[]") == c["source"]:
|
||||
c["bidirectional"] = True
|
||||
break
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def generate_callout(source: str, target: str) -> str:
|
||||
return (
|
||||
f"> [!WARNING] 可能矛盾\n"
|
||||
f"> 本页的声明与 [[{target}]] 存在冲突。\n"
|
||||
f"> 需要人工复核并解决矛盾。\n"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Detect conflicts between wiki pages")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
parser.add_argument("--auto-callout", action="store_true", help="Auto-add callout to pages")
|
||||
args = parser.parse_args()
|
||||
|
||||
conflicts = detect()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(conflicts, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
bidirectional = [c for c in conflicts if c["bidirectional"]]
|
||||
unidirectional = [c for c in conflicts if not c["bidirectional"]]
|
||||
|
||||
print(f"=== Conflict Detection ===")
|
||||
print(f" Total conflict declarations: {len(conflicts)}")
|
||||
print(f" Bidirectional (both confirm): {len(bidirectional)}")
|
||||
print(f" Unidirectional (check needed): {len(unidirectional)}")
|
||||
|
||||
if bidirectional:
|
||||
print(f"\n Bidirectional conflicts:")
|
||||
for c in bidirectional:
|
||||
print(f" [[{c['source']}]] <--conflicts_with--> [[{c['target']}]]")
|
||||
|
||||
if unidirectional:
|
||||
print(f"\n Unidirectional conflicts (may need callout):")
|
||||
for c in unidirectional:
|
||||
print(f" [[{c['source']}]] --conflicts_with--> [[{c['target']}]] (unconfirmed)")
|
||||
|
||||
if args.auto_callout:
|
||||
added = 0
|
||||
for c in unidirectional:
|
||||
fp = WIKI / f"{c['source']}.md"
|
||||
if not fp.exists():
|
||||
continue
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
callout = generate_callout(c["source"], c["target"])
|
||||
# Only add if not already present
|
||||
if callout.strip() not in content:
|
||||
content += f"\n\n{callout}"
|
||||
fp.write_text(content, encoding="utf-8")
|
||||
added += 1
|
||||
print(f" Added callout to [[{c['source']}]]")
|
||||
print(f" Callouts added: {added}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,140 @@
|
||||
@echo off
|
||||
REM ====================================================
|
||||
REM Marp 演示批量导出脚本 (Windows)
|
||||
REM ====================================================
|
||||
REM 用途:一键导出 Active 目录下所有演示
|
||||
REM 使用方法:
|
||||
REM 1. 确保已安装 Marp CLI: npm install -g @marp-team/marp-cli
|
||||
REM 2. 双击运行此脚本
|
||||
REM 3. 导出结果在 Export 目录
|
||||
REM ====================================================
|
||||
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
REM 配置
|
||||
set "PRESENTATIONS_DIR=%~dp0Active"
|
||||
set "EXPORT_DIR=%~dp0Export"
|
||||
set "TIMESTAMP=%date:~0,4%-%date:~5,2%-%date:~8,2%"
|
||||
|
||||
REM 创建导出目录
|
||||
if not exist "%EXPORT_DIR%" mkdir "%EXPORT_DIR%"
|
||||
if not exist "%EXPORT_DIR%\PDF" mkdir "%EXPORT_DIR%\PDF"
|
||||
if not exist "%EXPORT_DIR%\PPTX" mkdir "%EXPORT_DIR%\PPTX"
|
||||
if not exist "%EXPORT_DIR%\HTML" mkdir "%EXPORT_DIR%\HTML"
|
||||
|
||||
echo ====================================================
|
||||
echo Marp 演示批量导出脚本
|
||||
echo ====================================================
|
||||
echo.
|
||||
echo 源目录: %PRESENTATIONS_DIR%
|
||||
echo 导出目录: %EXPORT_DIR%
|
||||
echo 时间戳: %TIMESTAMP%
|
||||
echo ====================================================
|
||||
echo.
|
||||
|
||||
REM 检查源目录是否存在
|
||||
if not exist "%PRESENTATIONS_DIR%" (
|
||||
echo [错误] 源目录不存在: %PRESENTATIONS_DIR%
|
||||
echo 请先在 Active 目录中创建演示文件
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查是否安装了 Marp CLI
|
||||
where marp >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
echo [警告] 未检测到 Marp CLI
|
||||
echo 正在尝试安装...
|
||||
echo.
|
||||
call npm install -g @marp-team/marp-cli
|
||||
if %errorlevel% neq 0 (
|
||||
echo [错误] Marp CLI 安装失败
|
||||
echo 请手动运行: npm install -g @marp-team/marp-cli
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [成功] Marp CLI 安装完成
|
||||
echo.
|
||||
)
|
||||
|
||||
REM 计算文件数量
|
||||
set count=0
|
||||
for %%f in ("%PRESENTATIONS_DIR%\*.md") do set /a count+=1
|
||||
|
||||
if %count% equ 0 (
|
||||
echo [提示] Active 目录中没有 .md 文件
|
||||
echo 请先创建演示文件
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo 找到 %count% 个演示文件
|
||||
echo 开始导出...
|
||||
echo.
|
||||
|
||||
REM 导出 PDF
|
||||
echo [1/3] 导出 PDF...
|
||||
for %%f in ("%PRESENTATIONS_DIR%\*.md") do (
|
||||
set "filename=%%~nf"
|
||||
echo - !filename!...
|
||||
marp "%%f" --theme-set-dir "%~dp0Themes" --pdf --allow-local-files -o "%EXPORT_DIR%\PDF\!filename:.md!.pdf"
|
||||
if !errorlevel! neq 0 (
|
||||
echo [失败] !filename!
|
||||
) else (
|
||||
echo [完成] !filename!
|
||||
)
|
||||
)
|
||||
|
||||
REM 导出 PPTX
|
||||
echo.
|
||||
echo [2/3] 导出 PPTX...
|
||||
for %%f in ("%PRESENTATIONS_DIR%\*.md") do (
|
||||
set "filename=%%~nf"
|
||||
echo - !filename!...
|
||||
marp "%%f" --theme-set-dir "%~dp0Themes" --pptx --allow-local-files -o "%EXPORT_DIR%\PPTX\!filename:.md!.pptx"
|
||||
if !errorlevel! neq 0 (
|
||||
echo [失败] !filename!
|
||||
) else (
|
||||
echo [完成] !filename!
|
||||
)
|
||||
)
|
||||
|
||||
REM 导出 HTML
|
||||
echo.
|
||||
echo [3/3] 导出 HTML...
|
||||
for %%f in ("%PRESENTATIONS_DIR%\*.md") do (
|
||||
set "filename=%%~nf"
|
||||
echo - !filename!...
|
||||
marp "%%f" --theme-set-dir "%~dp0Themes" --html --allow-local-files -o "%EXPORT_DIR%\HTML\!filename:.md!.html"
|
||||
if !errorlevel! neq 0 (
|
||||
echo [失败] !filename!
|
||||
) else (
|
||||
echo [完成] !filename!
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ====================================================
|
||||
echo 导出完成!
|
||||
echo ====================================================
|
||||
echo.
|
||||
echo 导出目录: %EXPORT_DIR%
|
||||
echo.
|
||||
echo 文件统计:
|
||||
if exist "%EXPORT_DIR%\PDF" (
|
||||
for /f %%a in ('dir /b "%EXPORT_DIR%\PDF\*.pdf" 2^>nul ^| find /c /v ""') do echo PDF: %%a 个
|
||||
)
|
||||
if exist "%EXPORT_DIR%\PPTX" (
|
||||
for /f %%a in ('dir /b "%EXPORT_DIR%\PPTX\*.pptx" 2^>nul ^| find /c /v ""') do echo PPTX: %%a 个
|
||||
)
|
||||
if exist "%EXPORT_DIR%\HTML" (
|
||||
for /f %%a in ('dir /b "%EXPORT_DIR%\HTML\*.html" 2^>nul ^| find /c /v ""') do echo HTML: %%a 个
|
||||
)
|
||||
echo.
|
||||
echo 打开导出目录? (Y/N)
|
||||
choice /c yn /n /m "请选择"
|
||||
if %errorlevel% equ 1 (
|
||||
start "" "%EXPORT_DIR%"
|
||||
)
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,10 @@
|
||||
# Fix Git Remote Configuration Script
|
||||
|
||||
# Remove incorrect origin
|
||||
git remote remove origin
|
||||
|
||||
# Add correct origin without trailing slash
|
||||
git remote add origin https://hehaiguang1123:wYux3evdzMCzSYZKmHg4v55WxW322TYZ@git.haiguang.xyz/giteah/llm_wiki.git
|
||||
|
||||
# Push to remote
|
||||
git push origin main
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
fix-raw-citations.py — 为 wiki/ 页面中缺失行号的 [raw:filename] 引用
|
||||
自动从原始 raw/ 文件查找匹配文本并补上行号范围。
|
||||
|
||||
用法:
|
||||
python tools/scripts/fix-raw-citations.py # 补行号
|
||||
python tools/scripts/fix-raw-citations.py --stats # 仅统计不修改
|
||||
python tools/scripts/fix-raw-citations.py --dry-run # 预览修改
|
||||
|
||||
退出码:0 成功;1 有错误。
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VAULT = Path(r"D:\Applications\app\kepano-obsidian-main")
|
||||
WIKI = VAULT / "wiki"
|
||||
RAW = VAULT / "raw"
|
||||
|
||||
|
||||
def collect_wiki_files():
|
||||
"""收集 wiki/ 中需要处理的 .md 文件(排除 index.md, log.md)"""
|
||||
files = []
|
||||
for f in sorted(WIKI.glob("*.md")):
|
||||
if f.name not in ("index.md", "log.md"):
|
||||
files.append(f)
|
||||
return files
|
||||
|
||||
|
||||
def collect_aw_references(content: str):
|
||||
"""提取所有 [raw:...] 引用,返回 (match_obj, raw_filename, has_line_number)"""
|
||||
pattern = r'\[raw:([^:\]]+)(?::(\d+(?:-\d+)?))?\]'
|
||||
refs = []
|
||||
for m in re.finditer(pattern, content):
|
||||
fname = m.group(1)
|
||||
has_ln = m.group(2) is not None
|
||||
refs.append((m, fname, has_ln))
|
||||
return refs
|
||||
|
||||
|
||||
def extract_keyword(content: str, pos: int, max_chars: int = 60) -> str:
|
||||
"""
|
||||
从 content 中 pos 位置向前提取关键词(中文/英文/数字)。
|
||||
返回用于在 raw 文件中匹配的文本片段。
|
||||
"""
|
||||
start = max(0, pos - max_chars)
|
||||
before = content[start:pos]
|
||||
tokens = re.findall(r'[\u4e00-\u9fff\w]+', before)
|
||||
# 取最后 3-6 个 token 作为关键词
|
||||
return " ".join(tokens[-6:]) if len(tokens) >= 3 else " ".join(tokens)
|
||||
|
||||
|
||||
def find_line_range(raw_text: str, keyword: str, context: int = 3) -> str | None:
|
||||
"""
|
||||
在 raw_text 中搜索 keyword,返回匹配行所在的行号范围。
|
||||
格式: "start-end" 或 "line"(单行匹配)。
|
||||
返回 None 表示未找到。
|
||||
"""
|
||||
if not keyword:
|
||||
return None
|
||||
lines = raw_text.splitlines()
|
||||
matched_lines = set()
|
||||
for i, line in enumerate(lines, 1):
|
||||
if keyword in line:
|
||||
matched_lines.add(i)
|
||||
if not matched_lines:
|
||||
# fallback: try individual tokens
|
||||
tokens = keyword.split()
|
||||
for token in tokens:
|
||||
if len(token) < 2:
|
||||
continue
|
||||
for i, line in enumerate(lines, 1):
|
||||
if token in line:
|
||||
matched_lines.add(i)
|
||||
if not matched_lines:
|
||||
return None
|
||||
start = max(1, min(matched_lines) - context)
|
||||
end = min(len(lines), max(matched_lines) + context)
|
||||
if start == end:
|
||||
return str(start)
|
||||
return f"{start}-{end}"
|
||||
|
||||
|
||||
def process_file(filepath: Path, dry_run: bool = False) -> tuple[str, int, int]:
|
||||
"""
|
||||
处理单个文件。
|
||||
返回: (修改后的内容, 补行号数, 总引用数)
|
||||
"""
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
refs = collect_aw_references(content)
|
||||
total = len(refs)
|
||||
fixed = 0
|
||||
|
||||
if total == 0:
|
||||
return content, 0, 0
|
||||
|
||||
raw_cache = {}
|
||||
# 从后往前替换以保持 offsets
|
||||
for m, fname, has_ln in reversed(refs):
|
||||
if has_ln:
|
||||
continue # 已有行号,跳过
|
||||
# 加载 raw 文件
|
||||
if fname not in raw_cache:
|
||||
rpath = RAW / f"{fname}.md"
|
||||
if rpath.exists():
|
||||
raw_cache[fname] = rpath.read_text(encoding="utf-8")
|
||||
else:
|
||||
# 尝试模糊匹配(取文件名最后一段)
|
||||
candidates = list(RAW.glob(f"*{fname}*.md"))
|
||||
if candidates:
|
||||
raw_cache[fname] = candidates[0].read_text(encoding="utf-8")
|
||||
else:
|
||||
raw_cache[fname] = None
|
||||
raw_text = raw_cache.get(fname)
|
||||
if raw_text is None:
|
||||
continue
|
||||
|
||||
# 提取关键词
|
||||
keyword = extract_keyword(content, m.start())
|
||||
line_range = find_line_range(raw_text, keyword)
|
||||
if line_range:
|
||||
old = m.group(0)
|
||||
new = f"[raw:{fname}:{line_range}]"
|
||||
content = content[:m.start()] + new + content[m.end():]
|
||||
fixed += 1
|
||||
|
||||
return content, fixed, total
|
||||
|
||||
|
||||
def stats_only():
|
||||
"""仅统计行号覆盖率"""
|
||||
files = collect_wiki_files()
|
||||
total_refs = 0
|
||||
total_with_ln = 0
|
||||
total_missing_ln = 0
|
||||
per_file = []
|
||||
|
||||
for fp in files:
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
refs = collect_aw_references(content)
|
||||
total_refs += len(refs)
|
||||
with_ln = sum(1 for _, _, has_ln in refs if has_ln)
|
||||
missing = len(refs) - with_ln
|
||||
total_with_ln += with_ln
|
||||
total_missing_ln += missing
|
||||
if missing > 0:
|
||||
per_file.append((fp.name, missing, with_ln))
|
||||
|
||||
print(f"\n=== 行号标注覆盖率统计 ===")
|
||||
print(f" 文件数: {len(files)}")
|
||||
print(f" 总引用: {total_refs}")
|
||||
print(f" 有行号: {total_with_ln} ({total_with_ln / total_refs * 100:.1f}%)")
|
||||
print(f" 缺行号: {total_missing_ln} ({total_missing_ln / total_refs * 100:.1f}%)")
|
||||
if per_file:
|
||||
print(f"\n 缺行号的文件 (top 20):")
|
||||
for name, miss, have in sorted(per_file, key=lambda x: -x[1])[:20]:
|
||||
print(f" {name}: 缺 {miss} / 共 {miss + have}")
|
||||
return total_missing_ln
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="为 wiki/ 的 raw 引用补上行号")
|
||||
parser.add_argument("--stats", action="store_true", help="仅统计不修改")
|
||||
parser.add_argument("--dry-run", action="store_true", help="预览修改但不写入")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.stats:
|
||||
stats_only()
|
||||
return
|
||||
|
||||
files = collect_wiki_files()
|
||||
total_fixed = 0
|
||||
total_refs = 0
|
||||
changed_files = []
|
||||
|
||||
for fp in files:
|
||||
content, fixed, refs = process_file(fp, dry_run=args.dry_run)
|
||||
total_fixed += fixed
|
||||
total_refs += refs
|
||||
if fixed > 0:
|
||||
changed_files.append((fp.name, fixed, refs))
|
||||
if not args.dry_run:
|
||||
fp.write_text(content, encoding="utf-8")
|
||||
|
||||
print(f"\n=== 处理结果 ===")
|
||||
print(f" 处理文件: {len(files)}")
|
||||
print(f" 总引用数: {total_refs}")
|
||||
print(f" 补行号数: {total_fixed}")
|
||||
if total_refs > 0:
|
||||
print(f" 覆盖率: {(total_refs - total_fixed + total_fixed) / total_refs * 100:.1f}% → "
|
||||
f"{total_refs / total_refs * 100:.1f}% (理论上限,行号仅补匹配到的)")
|
||||
if changed_files:
|
||||
print(f"\n 更新文件 ({len(changed_files)}):")
|
||||
for name, fixed, refs in sorted(changed_files, key=lambda x: -x[1]):
|
||||
print(f" {name}: +{fixed} 行号")
|
||||
if args.dry_run and changed_files:
|
||||
print(f"\n 以上为预览,未写入文件(--dry-run)")
|
||||
|
||||
print(f"\n Total: {total_fixed} citations updated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
graph-search.py — 图感知搜索
|
||||
|
||||
流程:
|
||||
1. qmd vsearch 获取语义匹配结果(Top 10)
|
||||
2. 对每个结果,读取 relations 字段 → 获取相邻节点
|
||||
3. 去重后返回(直接关联 + 关系扩展)
|
||||
|
||||
用法:
|
||||
python tools/scripts/graph-search.py "查询词" [--count 15] [--depth 1]
|
||||
|
||||
依赖:
|
||||
- qmd 已安装(通过 node)
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
QMD = r'node "C:\Users\hhhh2024\AppData\Roaming\npm\node_modules\@tobilu\qmd\dist\cli\qmd.js"'
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
|
||||
def qmd_search(query: str, count: int = 10) -> list[str]:
|
||||
"""调用 qmd vsearch 获取匹配的页面名列表"""
|
||||
cmd = f'{QMD} vsearch "{query}" -c wiki -n {count}'
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, shell=True, timeout=30)
|
||||
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
||||
return lines
|
||||
|
||||
|
||||
def get_relations(page_name: str) -> list[dict]:
|
||||
"""读取页面的 relations 字段"""
|
||||
fp = WIKI / f"{page_name}.md"
|
||||
if not fp.exists():
|
||||
return []
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m:
|
||||
return []
|
||||
if not yaml:
|
||||
return []
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
return []
|
||||
return front.get("relations", [])
|
||||
|
||||
|
||||
def expand_nodes(nodes: list[str], depth: int = 1) -> list[str]:
|
||||
"""从起始节点出发,沿 relations 扩展相邻节点"""
|
||||
expanded = list(nodes)
|
||||
frontier = list(nodes)
|
||||
for _ in range(depth):
|
||||
next_frontier = []
|
||||
for node in frontier:
|
||||
rels = get_relations(node)
|
||||
for r in rels:
|
||||
target = r.get("target", "").strip("[]")
|
||||
if target and target not in expanded:
|
||||
expanded.append(target)
|
||||
next_frontier.append(target)
|
||||
frontier = next_frontier
|
||||
if not frontier:
|
||||
break
|
||||
return expanded
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Graph-aware search for wiki pages")
|
||||
parser.add_argument("query", help="Search query")
|
||||
parser.add_argument("--count", type=int, default=15, help="Max results")
|
||||
parser.add_argument("--depth", type=int, default=1, help="Graph expansion depth")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"=== Searching: {args.query} ===")
|
||||
results = qmd_search(args.query, count=max(10, args.count))
|
||||
if not results:
|
||||
print(" No results from qmd")
|
||||
return
|
||||
|
||||
print(f"\nDirect matches ({len(results)}):")
|
||||
for r in results:
|
||||
print(f" [[{r}]]")
|
||||
|
||||
expanded = expand_nodes(results, depth=args.depth)
|
||||
new = [e for e in expanded if e not in results]
|
||||
if new:
|
||||
print(f"\nGraph-expanded (via relations, depth={args.depth}):")
|
||||
for n in new[:args.count]:
|
||||
print(f" [[{n}]]")
|
||||
if len(new) > args.count:
|
||||
print(f" ... and {len(new) - args.count} more")
|
||||
|
||||
print(f"\nTotal unique: {len(expanded)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
manage-relations.py — 管理 wiki 页面的 typed relationships
|
||||
|
||||
用法:
|
||||
python manage-relations.py list <page> # 查看页面 relations
|
||||
python manage-relations.py add <page> --type <type> --target "页面" [--desc "说明"] [--conf 3]
|
||||
python manage-relations.py remove <page> --type <type> --target "页面"
|
||||
python manage-relations.py graph <page> # 输出 DOT 格式
|
||||
python manage-relations.py stats # 统计关系网络
|
||||
|
||||
关系类型:
|
||||
depends_on | conflicts_with | supersedes | caused_by | supports | extends | part_of | example_of
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
VAULT = Path(r"D:\Applications\app\kepano-obsidian-main")
|
||||
WIKI = VAULT / "wiki"
|
||||
|
||||
VALID_TYPES = [
|
||||
"depends_on", "conflicts_with", "supersedes", "caused_by",
|
||||
"supports", "extends", "part_of", "example_of"
|
||||
]
|
||||
|
||||
|
||||
def read_page(page: str) -> tuple[dict | None, str | None, str | None]:
|
||||
"""返回 (frontmatter_dict, body_text, error_msg)"""
|
||||
fp = WIKI / f"{page}.md"
|
||||
if not fp.exists():
|
||||
return None, None, f"ERROR: {page}.md not found"
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m:
|
||||
return None, None, f"ERROR: {page}.md has no frontmatter"
|
||||
body = content[m.end():]
|
||||
if yaml:
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return None, None, f"ERROR: YAML parse error: {e}"
|
||||
else:
|
||||
return None, None, "ERROR: PyYAML not installed (pip install pyyaml)"
|
||||
return front, body, None
|
||||
|
||||
|
||||
def write_page(page: str, front: dict, body: str) -> str | None:
|
||||
"""写入页面,返回 error_msg"""
|
||||
fp = WIKI / f"{page}.md"
|
||||
new_yaml = yaml.dump(front, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
fp.write_text(f"---\n{new_yaml}---\n{body.lstrip()}", encoding="utf-8")
|
||||
return None
|
||||
|
||||
|
||||
def cmd_list(page: str):
|
||||
front, body, err = read_page(page)
|
||||
if err:
|
||||
print(err); return
|
||||
rels = front.get("relations", [])
|
||||
if not rels:
|
||||
print(f" [[{page}]]: no relations")
|
||||
return
|
||||
print(f"[[{page}]] ({len(rels)} relations):")
|
||||
for r in rels:
|
||||
desc = f" — {r.get('description', '')}" if r.get("description") else ""
|
||||
conf = f" [conf={r.get('confidence', 3)}]" if r.get("confidence", 3) != 3 else ""
|
||||
target_name = r['target'].strip('[]')
|
||||
print(f" {r['type']} --> [[{target_name}]]{desc}{conf}")
|
||||
|
||||
|
||||
def cmd_add(page: str, rel_type: str, target: str, desc: str | None, conf: int):
|
||||
front, body, err = read_page(page)
|
||||
if err:
|
||||
print(err); return
|
||||
if "relations" not in front:
|
||||
front["relations"] = []
|
||||
# Dedup
|
||||
for r in front["relations"]:
|
||||
if r.get("type") == rel_type and r.get("target") == target:
|
||||
print(f" Already exists: {rel_type} [[{target}]]")
|
||||
return
|
||||
entry = {"type": rel_type, "target": f"[[{target}]]"}
|
||||
if desc:
|
||||
entry["description"] = desc
|
||||
if conf < 5:
|
||||
entry["confidence"] = conf
|
||||
front["relations"].append(entry)
|
||||
err = write_page(page, front, body)
|
||||
if err:
|
||||
print(err)
|
||||
else:
|
||||
print(f" Added: [[{page}]] --{rel_type}--> [[{target}]]")
|
||||
|
||||
|
||||
def cmd_remove(page: str, rel_type: str, target: str):
|
||||
front, body, err = read_page(page)
|
||||
if err:
|
||||
print(err); return
|
||||
rels = front.get("relations", [])
|
||||
before = len(rels)
|
||||
front["relations"] = [
|
||||
r for r in rels
|
||||
if not (r.get("type") == rel_type and r.get("target", "").strip("[]") == target)
|
||||
]
|
||||
if len(front["relations"]) == before:
|
||||
print(f" Not found: {rel_type} [[{target}]]")
|
||||
return
|
||||
if not front["relations"]:
|
||||
del front["relations"]
|
||||
err = write_page(page, front, body)
|
||||
if err:
|
||||
print(err)
|
||||
else:
|
||||
print(f" Removed: [[{page}]] --{rel_type}--> [[{target}]]")
|
||||
|
||||
|
||||
def cmd_graph(page: str):
|
||||
"""输出 DOT 格式"""
|
||||
front, body, err = read_page(page)
|
||||
if err:
|
||||
print(err); return
|
||||
rels = front.get("relations", [])
|
||||
print(f"digraph {page} {{")
|
||||
print(f' "{page}" [style=filled, fillcolor=lightblue];')
|
||||
for r in rels:
|
||||
target = r["target"].strip("[]")
|
||||
label = r["type"]
|
||||
print(f' "{page}" -> "{target}" [label="{label}"];')
|
||||
print("}")
|
||||
|
||||
|
||||
def cmd_stats():
|
||||
"""统计整个 wiki 的关系网络"""
|
||||
all_pages = list(WIKI.glob("*.md"))
|
||||
total = 0
|
||||
type_counts = {}
|
||||
pages_with_rels = 0
|
||||
for fp in all_pages:
|
||||
if fp.name in ("index.md", "log.md"):
|
||||
continue
|
||||
front, _, err = read_page(fp.stem)
|
||||
if err or not front:
|
||||
continue
|
||||
rels = front.get("relations", [])
|
||||
if rels:
|
||||
pages_with_rels += 1
|
||||
total += len(rels)
|
||||
for r in rels:
|
||||
t = r["type"]
|
||||
type_counts[t] = type_counts.get(t, 0) + 1
|
||||
|
||||
print(f"=== Relations Stats ===")
|
||||
print(f" Total wiki pages: {len(all_pages) - 2}")
|
||||
print(f" Pages with relations: {pages_with_rels}")
|
||||
print(f" Total relations: {total}")
|
||||
print(f" By type:")
|
||||
for t, c in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||
print(f" {t}: {c}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Manage typed relationships in wiki pages")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
|
||||
p_list = sub.add_parser("list", help="List relations of a page")
|
||||
p_list.add_argument("page")
|
||||
|
||||
p_add = sub.add_parser("add", help="Add a relation")
|
||||
p_add.add_argument("page")
|
||||
p_add.add_argument("--type", required=True, choices=VALID_TYPES)
|
||||
p_add.add_argument("--target", required=True)
|
||||
p_add.add_argument("--desc")
|
||||
p_add.add_argument("--conf", type=int, default=3)
|
||||
|
||||
p_rm = sub.add_parser("remove", help="Remove a relation")
|
||||
p_rm.add_argument("page")
|
||||
p_rm.add_argument("--type", required=True, choices=VALID_TYPES)
|
||||
p_rm.add_argument("--target", required=True)
|
||||
|
||||
p_g = sub.add_parser("graph", help="Output DOT graph")
|
||||
p_g.add_argument("page")
|
||||
|
||||
p_s = sub.add_parser("stats", help="Stats of the relation network")
|
||||
|
||||
args = parser.parse_args()
|
||||
if not args.cmd:
|
||||
parser.print_help(); return
|
||||
|
||||
if args.cmd == "list":
|
||||
cmd_list(args.page)
|
||||
elif args.cmd == "add":
|
||||
cmd_add(args.page, args.type, args.target, args.desc, args.conf)
|
||||
elif args.cmd == "remove":
|
||||
cmd_remove(args.page, args.type, args.target)
|
||||
elif args.cmd == "graph":
|
||||
cmd_graph(args.page)
|
||||
elif args.cmd == "stats":
|
||||
cmd_stats()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
promote-knowledge.py — Consolidation tiers: 检查 wiki/working/ 中的页面
|
||||
是否可以提升到 wiki/semantic/(即主 wiki 目录)。
|
||||
|
||||
条件:
|
||||
- 有完整的 frontmatter(categories, tags, type, source)
|
||||
- 正文 > 100 字
|
||||
- 创建时间 > 7 天(通过 created 字段判断)
|
||||
|
||||
用法:
|
||||
python tools/scripts/promote-knowledge.py # 检查可提升页面
|
||||
python tools/scripts/promote-knowledge.py --apply # 执行提升
|
||||
python tools/scripts/promote-knowledge.py --dry-run # 预览
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
WORKING = WIKI / "working"
|
||||
SEMANTIC = WIKI # semantic 层就是当前 wiki/
|
||||
ARCHIVE = WIKI / "archive"
|
||||
PROCEDURAL = WIKI / "procedural"
|
||||
|
||||
|
||||
def ensure_dirs():
|
||||
"""确保层目录存在"""
|
||||
for d in [WORKING, ARCHIVE, PROCEDURAL]:
|
||||
d.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def check_promotable(fp: Path) -> tuple[bool, list[str]]:
|
||||
"""检查文件是否可提升"""
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m:
|
||||
return False, ["No frontmatter"]
|
||||
if not yaml:
|
||||
return False, ["PyYAML not installed"]
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except:
|
||||
return False, ["YAML parse error"]
|
||||
|
||||
body = content[m.end():].strip()
|
||||
reasons = []
|
||||
|
||||
if not front.get("categories"):
|
||||
reasons.append("Missing categories")
|
||||
tags = front.get("tags", [])
|
||||
if isinstance(tags, str): tags = [tags]
|
||||
if "wiki" not in tags:
|
||||
reasons.append("Missing 'wiki' in tags")
|
||||
if not front.get("type"):
|
||||
reasons.append("Missing type")
|
||||
if not front.get("source"):
|
||||
reasons.append("Missing source")
|
||||
if len(body) < 100:
|
||||
reasons.append(f"Body too short ({len(body)} chars, need 100+)")
|
||||
created = front.get("created")
|
||||
if created:
|
||||
try:
|
||||
cdate = date.fromisoformat(str(created))
|
||||
if (date.today() - cdate).days < 7:
|
||||
reasons.append(f"Created < 7 days ago ({created})")
|
||||
except:
|
||||
pass # ignore invalid dates
|
||||
|
||||
return len(reasons) == 0, reasons
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Knowledge consolidation tiers")
|
||||
parser.add_argument("--apply", action="store_true", help="Execute promotion")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Preview only")
|
||||
args = parser.parse_args()
|
||||
|
||||
ensure_dirs()
|
||||
|
||||
if not WORKING.exists():
|
||||
print("No working/ directory found. Nothing to promote.")
|
||||
return
|
||||
|
||||
working_files = sorted(WORKING.glob("*.md"))
|
||||
if not working_files:
|
||||
print("No files in working/")
|
||||
return
|
||||
|
||||
promotable = []
|
||||
not_ready = []
|
||||
|
||||
for fp in working_files:
|
||||
ready, reasons = check_promotable(fp)
|
||||
if ready:
|
||||
promotable.append(fp)
|
||||
else:
|
||||
not_ready.append((fp, reasons))
|
||||
|
||||
print(f"=== Knowledge Promotion Check ===")
|
||||
print(f" Working files: {len(working_files)}")
|
||||
print(f" Promotable: {len(promotable)}")
|
||||
print(f" Not ready: {len(not_ready)}")
|
||||
|
||||
if promotable:
|
||||
print(f"\n Promotable to semantic/:")
|
||||
for fp in promotable:
|
||||
dest = SEMANTIC / fp.name
|
||||
if dest.exists():
|
||||
print(f" [[{fp.stem}]] → WARNING: target exists")
|
||||
else:
|
||||
print(f" [[{fp.stem}]]")
|
||||
|
||||
if args.apply and not args.dry_run:
|
||||
for fp in promotable:
|
||||
dest = SEMANTIC / fp.name
|
||||
if not dest.exists():
|
||||
shutil.move(str(fp), str(dest))
|
||||
print(f" Moved: working/{fp.name} → {fp.name}")
|
||||
print(f"\n Promoted: {len(promotable)}")
|
||||
elif args.dry_run:
|
||||
print(f"\n (--dry-run: no files moved)")
|
||||
|
||||
if not_ready:
|
||||
print(f"\n Not ready for promotion:")
|
||||
for fp, reasons in not_ready:
|
||||
for r in reasons:
|
||||
print(f" [[{fp.stem}]] — {r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
set QMD_EMBED_MODEL=hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf
|
||||
node "C:\Users\hhhh2024\AppData\Roaming\npm\node_modules\@tobilu\qmd\dist\cli\qmd.js" %*
|
||||
@@ -0,0 +1,35 @@
|
||||
$files = Get-ChildItem -Path '.' -Filter '*.md' -Recurse
|
||||
$totalLines = 0
|
||||
$totalChars = 0
|
||||
|
||||
foreach ($file in $files) {
|
||||
$content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue
|
||||
if ($content) {
|
||||
$lines = ($content -split "`n").Count
|
||||
$chars = $content.Length
|
||||
$totalLines += $lines
|
||||
$totalChars += $chars
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Total files: $($files.Count)"
|
||||
Write-Output "Total lines: $totalLines"
|
||||
Write-Output "Total characters: $totalChars"
|
||||
Write-Output "Avg characters per file: $([math]::Round($totalChars/$files.Count))"
|
||||
Write-Output "Avg lines per file: $([math]::Round($totalLines/$files.Count))"
|
||||
|
||||
# Categorize files
|
||||
$templates = Get-ChildItem -Path './Templates' -Filter '*.md' -Recurse
|
||||
$daily = Get-ChildItem -Path './Daily' -Filter '*.md' -Recurse
|
||||
$notes = Get-ChildItem -Path './Notes' -Filter '*.md' -Recurse
|
||||
$clippings = Get-ChildItem -Path './Clippings' -Filter '*.md' -Recurse
|
||||
$references = Get-ChildItem -Path './References' -Filter '*.md' -Recurse
|
||||
$categories = Get-ChildItem -Path './Categories' -Filter '*.md' -Recurse
|
||||
|
||||
Write-Output "`nBreakdown by category:"
|
||||
Write-Output "Templates: $($templates.Count)"
|
||||
Write-Output "Daily notes: $($daily.Count)"
|
||||
Write-Output "Notes: $($notes.Count)"
|
||||
Write-Output "Clippings: $($clippings.Count)"
|
||||
Write-Output "References: $($references.Count)"
|
||||
Write-Output "Categories: $($categories.Count)"
|
||||
@@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sync_home_wiki.py — home-wiki 知识页面 → kepano LLM Wiki 同步脚本
|
||||
|
||||
用途(定期执行):
|
||||
将 D:\\TC_UP\\home-wiki 的 concepts/ entities/ syntheses/ 知识页面,
|
||||
同步到 D:\\Applications\\app\\kepano-obsidian-main\\wiki\\,
|
||||
执行 frontmatter 规范转换,幂等更新;可选自动维护 index.md 与 lint 体检。
|
||||
|
||||
同步范围:
|
||||
concepts/ + entities/ + syntheses/ (排除 index.md)
|
||||
Daily/ MyNotes/ reports/ 不在知识同步范围。
|
||||
|
||||
转换规则(home-wiki frontmatter → kepano Wiki 规范):
|
||||
categories: 加 [[LLM Wiki]];entity/person 额外加 [[People]]
|
||||
tags: 前缀 [wiki, {people|concept}],subtype→concept/{subtype}(仅 concept),追加原 tags
|
||||
created: 保留
|
||||
source: sources[0] → 文件路径取 stem 做 wikilink;URL/标识符原样;空则留空
|
||||
type: 保留(concept/entity/synthesis)
|
||||
aliases: title 或 name
|
||||
正文: 保留,移除 home-wiki 特有的 openclaw 自动段落(## Related、注释标记行)
|
||||
|
||||
index 自动维护(--index):
|
||||
用标记块 <!-- BEGIN/END home-wiki-sync --> 界定 index.md 的「概念页」「实体页」
|
||||
两个表,脚本依据同步页面元数据 + 简介 JSON 缓存自动重生成。
|
||||
简介缓存 tools/data/home-wiki-summaries.json:首次从 index.md 现有表格种子化,
|
||||
保留人工优化;新增页面自动提取(description → 顶部引用块 → 概述首句)。
|
||||
综合报告表因混合其他来源,保持手动维护。
|
||||
|
||||
用法:
|
||||
python tools/scripts/sync_home_wiki.py --dry-run # 仅诊断差异
|
||||
python tools/scripts/sync_home_wiki.py # 同步(写入变化的页面)
|
||||
python tools/scripts/sync_home_wiki.py --index --log # 一站式:同步+更新index+记日志
|
||||
python tools/scripts/sync_home_wiki.py --lint # 体检(孤儿/断链/source空值)
|
||||
|
||||
退出码:0 成功;1 源目录缺失;2 有错误。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
sys.stderr.write("ERROR: PyYAML 未安装,请运行 pip install pyyaml\n")
|
||||
sys.exit(2)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 路径配置(两库位于不同位置,必须使用绝对路径)
|
||||
# ---------------------------------------------------------------------------
|
||||
SRC_ROOT = Path(r"D:\TC_UP\home-wiki")
|
||||
DST_WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
DST_INDEX = DST_WIKI / "index.md"
|
||||
DST_LOG = DST_WIKI / "log.md"
|
||||
SUMMARIES_CACHE = Path(__file__).resolve().parent.parent / "data" / "home-wiki-summaries.json"
|
||||
|
||||
SRC_DIRS = ["concepts", "entities", "syntheses"]
|
||||
|
||||
# home-wiki 完整文件名 → kepano 简短名(仅含副标题、过长标题的需要映射)
|
||||
NAME_MAP = {
|
||||
"顶级人生三重境:道家驭势、佛家修心、儒家立身": "顶级人生三重境",
|
||||
}
|
||||
|
||||
INDEX_BEGIN = "<!-- BEGIN home-wiki-sync -->"
|
||||
INDEX_END = "<!-- END home-wiki-sync -->"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 文档解析
|
||||
# ---------------------------------------------------------------------------
|
||||
def _extract_list_field(text, key):
|
||||
"""从 frontmatter 文本中提取列表字段(兼容 flow 与 block 两种写法)。"""
|
||||
m = re.search(rf"^[ \t]*{re.escape(key)}[ \t]*:\s*\[(.*)\][ \t]*$", text, re.M)
|
||||
if m:
|
||||
return [t.strip().strip("\"'") for t in m.group(1).split(",") if t.strip()]
|
||||
m = re.search(rf"^[ \t]*{re.escape(key)}[ \t]*:\s*\n((?:[ \t]+-.+\n?)+)", text, re.M)
|
||||
if m:
|
||||
items = re.findall(r"^[ \t]+-[ \t]+(.+?)[ \t]*$", m.group(1), re.M)
|
||||
return [it.strip().strip("\"'") for it in items]
|
||||
return []
|
||||
|
||||
|
||||
def _robust_parse_fm(fm_text):
|
||||
"""健壮解析 frontmatter:先 yaml.safe_load,失败则正则逐字段回退。
|
||||
|
||||
home-wiki frontmatter 常含 `related: [[wikilink]]` 这类 Obsidian wikilink,
|
||||
`[[` 会触发 YAML flow sequence 解析异常,故需要回退。
|
||||
"""
|
||||
try:
|
||||
fm = yaml.safe_load(fm_text)
|
||||
if isinstance(fm, dict) and fm.get("type"):
|
||||
return fm
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
fm = {}
|
||||
for key in ("type", "subtype", "title", "name", "created", "updated",
|
||||
"confidence", "field", "nameEn", "description"):
|
||||
m = re.search(rf"^[ \t]*{re.escape(key)}[ \t]*:[ \t]*(.+?)[ \t]*$", fm_text, re.M)
|
||||
if m and m.group(1).strip() not in ("", "[]"):
|
||||
fm[key] = m.group(1).strip().strip("\"'")
|
||||
for key in ("tags", "sources"):
|
||||
vals = _extract_list_field(fm_text, key)
|
||||
if vals:
|
||||
fm[key] = vals
|
||||
return fm
|
||||
|
||||
|
||||
def parse_doc(text):
|
||||
"""分离 frontmatter 与正文。返回 (fm_dict, body_str)。"""
|
||||
m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.S)
|
||||
if not m:
|
||||
return {}, text
|
||||
return _robust_parse_fm(m.group(1)), m.group(2)
|
||||
|
||||
|
||||
def strip_openclaw(body):
|
||||
"""移除 home-wiki 特有的 openclaw 自动段落,保留实质内容。
|
||||
|
||||
- 移除 `## Related` 及其 openclaw:wiki:related 包裹块
|
||||
- 移除所有 `<!-- openclaw:...:...:(start|end) -->` 注释标记行(保留被包裹的内容)
|
||||
"""
|
||||
body = re.sub(
|
||||
r"\n##\s*Related\s*\n<!--\s*openclaw:wiki:related:start\s*-->.*?<!--\s*openclaw:wiki:related:end\s*-->",
|
||||
"\n",
|
||||
body,
|
||||
flags=re.S,
|
||||
)
|
||||
body = re.sub(r"<!--\s*openclaw:[a-z]+:[a-z]+:(?:start|end)\s*-->\s*\n?", "", body)
|
||||
return body.rstrip() + "\n"
|
||||
|
||||
|
||||
def norm_body(body):
|
||||
"""规范化正文用于对比:移除 openclaw 段,丢弃空行与行尾空白。"""
|
||||
b = strip_openclaw(body)
|
||||
lines = [ln.rstrip() for ln in b.splitlines() if ln.strip()]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# frontmatter 转换
|
||||
# ---------------------------------------------------------------------------
|
||||
def _as_list(v):
|
||||
if v is None:
|
||||
return []
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if s.startswith("[") and s.endswith("]"):
|
||||
s = s[1:-1]
|
||||
return [t.strip() for t in s.split(",") if t.strip()]
|
||||
return [v]
|
||||
|
||||
|
||||
def convert_frontmatter(fm, default_name):
|
||||
"""home-wiki frontmatter → kepano Wiki frontmatter(有序)。"""
|
||||
fm_type = str(fm.get("type", "concept")).strip()
|
||||
subtype = fm.get("subtype")
|
||||
if isinstance(subtype, list):
|
||||
subtype = subtype[0] if subtype else None
|
||||
subtype = str(subtype).strip() if subtype else None
|
||||
title = fm.get("title") or fm.get("name") or default_name
|
||||
|
||||
categories = ["[[LLM Wiki]]"]
|
||||
type_tag = "concept"
|
||||
if fm_type == "entity":
|
||||
if subtype == "person":
|
||||
categories.append("[[People]]")
|
||||
type_tag = "people"
|
||||
else:
|
||||
type_tag = "entity"
|
||||
elif fm_type == "synthesis":
|
||||
type_tag = "concept"
|
||||
|
||||
tags = ["wiki", type_tag]
|
||||
# 仅 concept 类型把 subtype 转为 concept/{subtype} 标签
|
||||
# (entity/person 用 people 标签 + [[People]] category,不加 concept/person)
|
||||
if subtype and fm_type == "concept":
|
||||
tags.append(f"concept/{subtype}")
|
||||
for t in _as_list(fm.get("tags")):
|
||||
t = str(t).strip()
|
||||
if t and t not in tags:
|
||||
tags.append(t)
|
||||
|
||||
sources = _as_list(fm.get("sources"))
|
||||
src0 = str(sources[0]) if sources else ""
|
||||
if ("/" in src0 or "\\" in src0) and not src0.startswith("http"):
|
||||
src_val = "[[" + Path(src0).stem + "]]"
|
||||
elif src0:
|
||||
src_val = src0
|
||||
else:
|
||||
src_val = ""
|
||||
|
||||
return [
|
||||
("categories", categories),
|
||||
("tags", tags),
|
||||
("created", str(fm.get("created", ""))),
|
||||
("source", src_val),
|
||||
("type", fm_type),
|
||||
("aliases", [str(title)]),
|
||||
]
|
||||
|
||||
|
||||
def _yaml_quote(val):
|
||||
"""如果值包含 YAML 特殊字符(如 [[ wikilink 的方括号),加双引号。"""
|
||||
s = str(val)
|
||||
if not s:
|
||||
return '""'
|
||||
if '[' in s or ']' in s or '{' in s or '}' in s or ':' in s or '#' in s:
|
||||
return f'"{s}"'
|
||||
return s
|
||||
|
||||
|
||||
def dump_frontmatter(pairs):
|
||||
out = ["---"]
|
||||
for k, v in pairs:
|
||||
if isinstance(v, list):
|
||||
out.append(f"{k}:")
|
||||
for item in v:
|
||||
out.append(f" - {_yaml_quote(item)}")
|
||||
else:
|
||||
out.append(f"{k}: {_yaml_quote(v)}")
|
||||
out.append("---")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 同步主流程
|
||||
# ---------------------------------------------------------------------------
|
||||
def target_name(stem):
|
||||
"""home-wiki 文件名(stem) → kepano 目标文件名。"""
|
||||
return NAME_MAP.get(stem, stem)
|
||||
|
||||
|
||||
def collect_sources():
|
||||
"""收集所有待同步源文件(排除 index.md)。返回 [(src_path, stem)]。"""
|
||||
items = []
|
||||
for d in SRC_DIRS:
|
||||
sdir = SRC_ROOT / d
|
||||
if not sdir.is_dir():
|
||||
continue
|
||||
for p in sorted(sdir.glob("*.md")):
|
||||
if p.stem.lower() == "index":
|
||||
continue
|
||||
items.append((p, p.stem))
|
||||
return items
|
||||
|
||||
|
||||
def build_page(src_path, stem):
|
||||
"""读取源文件,生成 kepano 页面内容 (frontmatter_str + body)。"""
|
||||
text = src_path.read_text(encoding="utf-8")
|
||||
fm, body = parse_doc(text)
|
||||
pairs = convert_frontmatter(fm, stem)
|
||||
body_clean = strip_openclaw(body)
|
||||
# 确保正文与 frontmatter 间有空行
|
||||
return dump_frontmatter(pairs) + "\n" + body_clean.lstrip("\n")
|
||||
|
||||
|
||||
def _norm_full(text):
|
||||
"""规范化整篇(frontmatter+正文)用于对比:去行尾空白、去空行。"""
|
||||
return "\n".join(ln.rstrip() for ln in text.splitlines() if ln.strip())
|
||||
|
||||
|
||||
def get_page_meta():
|
||||
"""返回 {target_stem: {"type":..., "src_dir":..., "src_stem":...}}。"""
|
||||
meta = {}
|
||||
for src_path, stem in collect_sources():
|
||||
fm, _ = parse_doc(src_path.read_text(encoding="utf-8"))
|
||||
meta[target_name(stem)] = {
|
||||
"type": str(fm.get("type", "concept")),
|
||||
"src_dir": src_path.parent.name,
|
||||
"src_stem": stem,
|
||||
}
|
||||
return meta
|
||||
|
||||
|
||||
def sync(dry_run=False):
|
||||
if not SRC_ROOT.is_dir():
|
||||
sys.stderr.write(f"ERROR: 源目录不存在: {SRC_ROOT}\n")
|
||||
sys.exit(1)
|
||||
if not DST_WIKI.is_dir():
|
||||
sys.stderr.write(f"ERROR: 目标 wiki 目录不存在: {DST_WIKI}\n")
|
||||
sys.exit(1)
|
||||
|
||||
items = collect_sources()
|
||||
created, updated, unchanged = [], [], []
|
||||
errors = []
|
||||
|
||||
for src_path, stem in items:
|
||||
try:
|
||||
dst_path = DST_WIKI / f"{target_name(stem)}.md"
|
||||
new_content = build_page(src_path, stem)
|
||||
if not dst_path.exists():
|
||||
created.append(stem)
|
||||
if not dry_run:
|
||||
dst_path.write_text(new_content, encoding="utf-8")
|
||||
continue
|
||||
old_content = dst_path.read_text(encoding="utf-8")
|
||||
# 对比完整生成内容(frontmatter+正文),确保 frontmatter 损坏也会被修复
|
||||
if _norm_full(new_content) == _norm_full(old_content):
|
||||
unchanged.append(stem)
|
||||
continue
|
||||
updated.append(stem)
|
||||
if not dry_run:
|
||||
dst_path.write_text(new_content, encoding="utf-8")
|
||||
except Exception as e: # noqa
|
||||
errors.append(f"{stem}: {e}")
|
||||
|
||||
return {
|
||||
"total": len(items),
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"unchanged": unchanged,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 简介提取与缓存(index 自动维护用)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _wikilink_to_text(s):
|
||||
"""将简介里的 wikilink 转为纯文本显示:[[a|b]]→b,[[a]]→a。"""
|
||||
s = re.sub(r"\[\[([^\]|]+?)\|([^\]]+?)\]\]", r"\2", s)
|
||||
s = re.sub(r"\[\[([^\]]+?)\]\]", r"\1", s)
|
||||
return s
|
||||
|
||||
|
||||
def _trunc(s, n=80):
|
||||
s = s.strip()
|
||||
if len(s) > n:
|
||||
s = s[:n].rstrip() + "…"
|
||||
return s
|
||||
|
||||
|
||||
def extract_summary(body, fm):
|
||||
"""从页面提取一句话简介。优先级:description → 顶部首个引用块 → 概述/定义首句。"""
|
||||
if fm.get("description"):
|
||||
return _trunc(_wikilink_to_text(str(fm["description"])))
|
||||
lines = body.splitlines()
|
||||
i, n = 0, len(lines)
|
||||
while i < n and not lines[i].lstrip().startswith(">"):
|
||||
i += 1
|
||||
if i < n:
|
||||
quotes = []
|
||||
while i < n and lines[i].lstrip().startswith(">"):
|
||||
content = lines[i].lstrip()[1:].strip()
|
||||
if content and not re.match(r"^[—\-]+", content):
|
||||
quotes.append(content)
|
||||
i += 1
|
||||
if quotes:
|
||||
return _trunc(_wikilink_to_text(" ".join(quotes)))
|
||||
m = re.search(r"^##\s*(?:概述|定义|简介)\s*\n\s*(.+)$", body, re.M)
|
||||
if m:
|
||||
first = m.group(1).strip().splitlines()[0]
|
||||
if first:
|
||||
return _trunc(_wikilink_to_text(first))
|
||||
return ""
|
||||
|
||||
|
||||
def seed_from_index():
|
||||
"""首次运行:从 index.md 现有跨库表格解析人工简介作为初始缓存。"""
|
||||
cache = {}
|
||||
if not DST_INDEX.exists():
|
||||
return cache
|
||||
text = DST_INDEX.read_text(encoding="utf-8")
|
||||
# 只解析来源列含 [[home-wiki/ 的行,避免误抓其他来源页面
|
||||
pattern = re.compile(
|
||||
r"^\|\s*\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]\s*\|\s*([^|]+?)\s*\|\s*\[\[home-wiki/",
|
||||
re.M,
|
||||
)
|
||||
for m in pattern.finditer(text):
|
||||
stem = m.group(1).strip()
|
||||
summary = m.group(2).strip()
|
||||
if stem and summary:
|
||||
cache[stem] = {"summary": summary}
|
||||
return cache
|
||||
|
||||
|
||||
def load_summaries():
|
||||
if SUMMARIES_CACHE.exists():
|
||||
try:
|
||||
return json.loads(SUMMARIES_CACHE.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return seed_from_index()
|
||||
|
||||
|
||||
def save_summaries(cache):
|
||||
SUMMARIES_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||
SUMMARIES_CACHE.write_text(
|
||||
json.dumps(cache, ensure_ascii=False, indent=2, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# index.md 自动维护
|
||||
# ---------------------------------------------------------------------------
|
||||
def _build_table(stems, cache, meta, title):
|
||||
"""生成单个分类表格(Markdown 文本)。"""
|
||||
lines = [f"### {title}({len(stems)})", "", "| 页面 | 简介 | 来源 |", "|------|------|------|"]
|
||||
for s in stems:
|
||||
summ = cache.get(s, {}).get("summary", "")
|
||||
src = f"[[home-wiki/{meta[s]['src_dir']}/{s}]]"
|
||||
lines.append(f"| [[{s}]] | {summ} | {src} |")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def update_index(meta):
|
||||
"""用标记块自动维护 index.md 的概念/实体表,并刷新「最后更新」日期。"""
|
||||
cache_existed = SUMMARIES_CACHE.exists()
|
||||
cache = load_summaries()
|
||||
# 为缓存缺失的页面提取简介
|
||||
dirty = False
|
||||
for stem, m in meta.items():
|
||||
if not cache.get(stem, {}).get("summary"):
|
||||
dst = DST_WIKI / f"{stem}.md"
|
||||
if dst.exists():
|
||||
fm, body = parse_doc(dst.read_text(encoding="utf-8"))
|
||||
cache[stem] = {"summary": extract_summary(body, fm), "type": m["type"]}
|
||||
dirty = True
|
||||
# 首次运行(缓存文件不存在)或有新提取,都持久化(JSON 是简介权威来源)
|
||||
if dirty or not cache_existed:
|
||||
save_summaries(cache)
|
||||
|
||||
concepts = sorted(s for s, m in meta.items() if m["type"] == "concept")
|
||||
entities = sorted(s for s, m in meta.items() if m["type"] == "entity")
|
||||
|
||||
block_body = [
|
||||
"## 跨库同步(home-wiki)",
|
||||
"",
|
||||
"> 来自 `D:\\TC_UP\\home-wiki` 的知识内容,聚焦高等教育 AI、系统理论、哲学思想等方向。",
|
||||
"",
|
||||
_build_table(concepts, cache, meta, "概念页"),
|
||||
"",
|
||||
_build_table(entities, cache, meta, "实体页"),
|
||||
]
|
||||
generated = INDEX_BEGIN + "\n" + "\n".join(block_body) + "\n" + INDEX_END
|
||||
|
||||
text = DST_INDEX.read_text(encoding="utf-8")
|
||||
if INDEX_BEGIN in text and INDEX_END in text:
|
||||
new_text = re.sub(
|
||||
re.escape(INDEX_BEGIN) + r".*?" + re.escape(INDEX_END),
|
||||
lambda _: generated,
|
||||
text,
|
||||
flags=re.S,
|
||||
)
|
||||
else:
|
||||
# 首次:替换现有手动「跨库同步」章节(到「综合报告」之前)为标记块
|
||||
new_text = re.sub(
|
||||
r"## 跨库同步(home-wiki).*?(?=### 综合报告)",
|
||||
lambda _: generated + "\n\n",
|
||||
text,
|
||||
flags=re.S,
|
||||
)
|
||||
|
||||
# 刷新「最后更新」日期(pattern 匹配整格含结尾 |,replacement 给完整行,避免管道符累加)
|
||||
today = date.today().isoformat()
|
||||
new_text = re.sub(
|
||||
r"\|\s*最后更新\s*\|\s*[^\n|]*\|",
|
||||
f"| 最后更新 | {today} (home-wiki 跨库同步) |",
|
||||
new_text,
|
||||
)
|
||||
|
||||
if new_text != text:
|
||||
DST_INDEX.write_text(new_text, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# lint 体检
|
||||
# ---------------------------------------------------------------------------
|
||||
def lint():
|
||||
meta = get_page_meta()
|
||||
synced = list(meta.keys())
|
||||
allfiles = {p.stem for p in DST_WIKI.glob("*.md")}
|
||||
|
||||
# aliases 反向映射(MarkItDown 等大小写变体)
|
||||
aliases_map = {}
|
||||
for p in DST_WIKI.glob("*.md"):
|
||||
fm, _ = parse_doc(p.read_text(encoding="utf-8"))
|
||||
for a in _as_list(fm.get("aliases")):
|
||||
aliases_map[str(a).strip()] = p.stem
|
||||
|
||||
# 孤儿:同步页面在 wiki 的入站链接数(排除自身)
|
||||
inbound = {s: 0 for s in synced}
|
||||
for p in DST_WIKI.glob("*.md"):
|
||||
t = p.read_text(encoding="utf-8")
|
||||
for s in synced:
|
||||
if p.stem == s:
|
||||
continue
|
||||
if re.search(rf"\[\[{re.escape(s)}[\]|\]]", t):
|
||||
inbound[s] += 1
|
||||
orphans = [s for s, c in inbound.items() if c == 0]
|
||||
|
||||
# source 空值
|
||||
empty_source = []
|
||||
for s in synced:
|
||||
t = (DST_WIKI / f"{s}.md").read_text(encoding="utf-8")
|
||||
m = re.search(r"^source:[ \t]*(.*)$", t, re.M)
|
||||
if m and not m.group(1).strip():
|
||||
empty_source.append(s)
|
||||
|
||||
# 断链:同步页面引用的 [[x]] 在 wiki 是否存在(排除 raw 来源类、aliases、category)
|
||||
valid = allfiles | set(aliases_map.keys()) | set(synced)
|
||||
skip = {"People", "LLM Wiki", "wikilink"}
|
||||
broken = {}
|
||||
for s in synced:
|
||||
t = (DST_WIKI / f"{s}.md").read_text(encoding="utf-8")
|
||||
for m in re.findall(r"\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]", t):
|
||||
tgt = m.strip()
|
||||
if not tgt or tgt.startswith("http") or "/" in tgt or "\\" in tgt:
|
||||
continue
|
||||
if tgt in valid or tgt in skip:
|
||||
continue
|
||||
broken.setdefault(tgt, []).append(s)
|
||||
|
||||
# index 覆盖检查:同步的概念/实体是否都在 index 跨库章节
|
||||
idx_text = DST_INDEX.read_text(encoding="utf-8") if DST_INDEX.exists() else ""
|
||||
not_in_index = [s for s in synced if meta[s]["type"] in ("concept", "entity")
|
||||
and not re.search(rf"\[\[{re.escape(s)}[\]|\]]", idx_text)]
|
||||
|
||||
print("== home-wiki 同步体检 ==")
|
||||
print(f"同步页面: {len(synced)} | wiki 总文件: {len(allfiles)}")
|
||||
print(f"\n[孤儿] {len(orphans)} 个(0 入站链接)")
|
||||
for s in orphans:
|
||||
print(f" - {s}")
|
||||
print(f"\n[source 空值] {len(empty_source)} 个(违反 wiki 层 source 必填)")
|
||||
for s in empty_source:
|
||||
print(f" - {s}")
|
||||
print(f"\n[断链] {len(broken)} 个目标(同步页面引用、kepano 无对应页)")
|
||||
for tgt in sorted(broken):
|
||||
print(f" - {tgt} ← {', '.join(sorted(broken[tgt]))}")
|
||||
print(f"\n[未入 index] {len(not_in_index)} 个(概念/实体未在跨库章节)")
|
||||
for s in not_in_index:
|
||||
print(f" - {s}")
|
||||
|
||||
issues = len(orphans) + len(empty_source) + len(not_in_index)
|
||||
print(f"\n硬性问题(孤儿+空source+未入index): {issues}")
|
||||
print(f"软性问题(断链,多为待创建概念/raw来源): {len(broken)}")
|
||||
return issues
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 日志
|
||||
# ---------------------------------------------------------------------------
|
||||
def append_log(result):
|
||||
today = date.today().isoformat()
|
||||
lines = [
|
||||
f"\n## [{today}] sync | home-wiki 跨库同步",
|
||||
"",
|
||||
f"**来源**: `{SRC_ROOT}`",
|
||||
"",
|
||||
"**操作**:",
|
||||
f"- 扫描 concepts/entities/syntheses 共 {result['total']} 个页面",
|
||||
]
|
||||
if result["updated"]:
|
||||
lines.append(f"- 更新 {len(result['updated'])} 页: {', '.join(result['updated'])}")
|
||||
if result["created"]:
|
||||
lines.append(f"- 新建 {len(result['created'])} 页: {', '.join(result['created'])}")
|
||||
if result["unchanged"]:
|
||||
lines.append(f"- 未变 {len(result['unchanged'])} 页")
|
||||
if result["errors"]:
|
||||
lines.append(f"- 错误 {len(result['errors'])} 项: {'; '.join(result['errors'])}")
|
||||
lines += ["", f"**涉及页面**: {len(result['updated'])} 更新,{len(result['created'])} 新建", ""]
|
||||
with DST_LOG.open("a", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="home-wiki → kepano LLM Wiki 同步")
|
||||
ap.add_argument("--dry-run", action="store_true", help="仅诊断,不写入")
|
||||
ap.add_argument("--log", action="store_true", help="同步后追加 wiki/log.md")
|
||||
ap.add_argument("--index", action="store_true", help="同步后自动维护 wiki/index.md 跨库章节")
|
||||
ap.add_argument("--lint", action="store_true", help="仅体检(孤儿/断链/source空值),不同步")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.lint:
|
||||
issues = lint()
|
||||
sys.exit(0 if issues == 0 else 0) # lint 不以问题数为错误码
|
||||
|
||||
res = sync(dry_run=args.dry_run)
|
||||
verb = "诊断" if args.dry_run else "同步"
|
||||
print(f"== home-wiki {verb}报告 ==")
|
||||
print(f"扫描: {res['total']} 页")
|
||||
print(f"需更新: {len(res['updated'])} -> {res['updated']}")
|
||||
print(f"需新建: {len(res['created'])} -> {res['created']}")
|
||||
print(f"未变化: {len(res['unchanged'])} -> {res['unchanged']}")
|
||||
if res["errors"]:
|
||||
print(f"错误: {res['errors']}")
|
||||
sys.exit(2)
|
||||
|
||||
if not args.dry_run:
|
||||
if args.log and (res["updated"] or res["created"]):
|
||||
append_log(res)
|
||||
print("已追加 wiki/log.md")
|
||||
if args.index:
|
||||
meta = get_page_meta()
|
||||
changed = update_index(meta)
|
||||
print(f"wiki/index.md 跨库章节: {'已更新' if changed else '无变化'}")
|
||||
print("完成。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
validate-frontmatter.py — Pre-commit hook: 验证被修改的 wiki 页面 frontmatter
|
||||
|
||||
检查项:
|
||||
- 必须包含 categories(含 [[LLM Wiki]])
|
||||
- 必须包含 tags(含 wiki)
|
||||
- 必须包含 type(合法值列表)
|
||||
- 必须包含 source
|
||||
- 如果 status=superseded,必须包含 superseded_by
|
||||
- relations 的 target 必须指向存在的页面
|
||||
|
||||
用法:
|
||||
python tools/scripts/validate-frontmatter.py # 所有 wiki 页面
|
||||
python tools/scripts/validate-frontmatter.py --files file1.md file2.md # 指定文件
|
||||
python tools/scripts/validate-frontmatter.py --git-hook # 从 git diff 读取
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
|
||||
|
||||
VALID_TYPES = [
|
||||
"concept", "entity", "tool", "reference", "place",
|
||||
"institution", "method", "knowledge-card", "synthesis",
|
||||
"index", "log", "research-report", "lesson"
|
||||
]
|
||||
|
||||
|
||||
def validate_one(fp: Path) -> list[str]:
|
||||
"""验证单个文件,返回错误列表"""
|
||||
errors = []
|
||||
content = fp.read_text(encoding="utf-8")
|
||||
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not m:
|
||||
return ["No frontmatter found"]
|
||||
if not yaml:
|
||||
return ["PyYAML not installed"]
|
||||
try:
|
||||
front = yaml.safe_load(m.group(1)) or {}
|
||||
except yaml.YAMLError as e:
|
||||
return [f"YAML parse error: {e}"]
|
||||
|
||||
# categories must contain [[LLM Wiki]]
|
||||
cats = front.get("categories", [])
|
||||
if isinstance(cats, str):
|
||||
cats = [cats]
|
||||
if not any(str(c).strip("[]") == "LLM Wiki" for c in cats):
|
||||
errors.append("Missing [[LLM Wiki]] in categories")
|
||||
|
||||
# tags must contain wiki
|
||||
tags = front.get("tags", [])
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
if "wiki" not in tags:
|
||||
errors.append("Missing 'wiki' in tags")
|
||||
|
||||
# type must exist and be valid
|
||||
ptype = front.get("type")
|
||||
if not ptype:
|
||||
errors.append("Missing type")
|
||||
elif isinstance(ptype, str) and ptype not in VALID_TYPES:
|
||||
errors.append(f"Invalid type: '{ptype}' (valid: {', '.join(VALID_TYPES)})")
|
||||
elif isinstance(ptype, list):
|
||||
for t in ptype:
|
||||
if t not in VALID_TYPES:
|
||||
errors.append(f"Invalid type in list: '{t}'")
|
||||
|
||||
# source must exist
|
||||
if not front.get("source"):
|
||||
errors.append("Missing source")
|
||||
|
||||
# status=superseded must have superseded_by
|
||||
if front.get("status") == "superseded" and not front.get("superseded_by"):
|
||||
errors.append("status=superseded but missing superseded_by")
|
||||
|
||||
# relations targets must exist
|
||||
for rel in front.get("relations", []):
|
||||
target = str(rel.get("target", "")).strip("[]")
|
||||
if target and not (WIKI / f"{target}.md").exists():
|
||||
errors.append(f"Relation target [[{target}]] not found")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def get_git_changed_wiki_files() -> list[Path]:
|
||||
"""获取 git 暂存区中被修改的 wiki .md 文件"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
|
||||
capture_output=True, text=True, check=True, cwd=WIKI.parent
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return []
|
||||
files = []
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("wiki/") and line.endswith(".md"):
|
||||
fp = WIKI.parent / line
|
||||
if fp.exists():
|
||||
files.append(fp)
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate wiki page frontmatter")
|
||||
parser.add_argument("--files", nargs="+", help="Specific files to check")
|
||||
parser.add_argument("--git-hook", action="store_true", help="Read from git diff")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.git_hook:
|
||||
files = get_git_changed_wiki_files()
|
||||
if not files:
|
||||
sys.exit(0)
|
||||
elif args.files:
|
||||
files = [Path(f) if Path(f).is_absolute() else WIKI / f for f in args.files]
|
||||
else:
|
||||
files = sorted(WIKI.glob("*.md"))
|
||||
|
||||
all_errors = {}
|
||||
for fp in files:
|
||||
if fp.name in ("index.md", "log.md"):
|
||||
continue
|
||||
errors = validate_one(fp)
|
||||
if errors:
|
||||
all_errors[fp.stem] = errors
|
||||
|
||||
if all_errors:
|
||||
print(f"=== Frontmatter Validation Errors ({len(all_errors)} files) ===")
|
||||
for name, errs in sorted(all_errors.items()):
|
||||
print(f"\n [[{name}]]:")
|
||||
for e in errs:
|
||||
print(f" ❌ {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ All frontmatter valid")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
每周 Wiki Lint:孤儿 + 断链 + 时效 + 矛盾 + index 同步
|
||||
.DESCRIPTION
|
||||
聚合所有 lint 脚本,输出报告到 tools/data/lint-reports/YYYY-MM-DD.log
|
||||
配合 Windows 定时任务使用:每周日 22:00
|
||||
#>
|
||||
$ErrorActionPreference = "Continue"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
$vaultRoot = "D:\Applications\app\kepano-obsidian-main"
|
||||
$reportDir = "$vaultRoot\tools\data\lint-reports"
|
||||
$today = Get-Date -Format "yyyy-MM-dd"
|
||||
$logFile = "$reportDir\$today.log"
|
||||
|
||||
# 创建报告目录
|
||||
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
|
||||
|
||||
"=== Weekly Lint Report — $today ===" | Out-File -FilePath $logFile -Encoding utf8
|
||||
"=" * 40 | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# 1. Orphan detection
|
||||
"`n=== 1. Orphan Pages ===" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
$orphans = & "$vaultRoot\tools\scripts\wiki-lint-orphan.ps1" 2>&1
|
||||
$orphans | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# 2. Broken links
|
||||
"`n=== 2. Broken Links ===" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
$broken = & "$vaultRoot\tools\scripts\wiki-lint-broken-v2.ps1" 2>&1
|
||||
$broken | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# 3. Staleness
|
||||
"`n=== 3. Staleness ===" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
$staleness = python "$vaultRoot\tools\scripts\check-staleness.py" 2>&1
|
||||
$staleness | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# 4. Conflicts
|
||||
"`n=== 4. Conflicts ===" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
$conflicts = python "$vaultRoot\tools\scripts\detect-conflicts.py" 2>&1
|
||||
$conflicts | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# 5. Frontmatter validation
|
||||
"`n=== 5. Frontmatter Validation ===" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
$validation = python "$vaultRoot\tools\scripts\validate-frontmatter.py" 2>&1
|
||||
$validation | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
|
||||
# Summary
|
||||
"`n$('=' * 40)" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
"Report saved: $logFile" | Out-File -FilePath $logFile -Encoding utf8 -Append
|
||||
Write-Output "Weekly lint complete: $logFile"
|
||||
@@ -0,0 +1,59 @@
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$wiki = 'D:\Applications\app\kepano-obsidian-main\wiki'
|
||||
$vault = 'D:\Applications\app\kepano-obsidian-main'
|
||||
|
||||
# Get all available pages in vault (excluding special Obsidian files)
|
||||
$allMd = Get-ChildItem -Path $vault -Filter '*.md' -File -Recurse | Where-Object {
|
||||
$_.FullName -notmatch '\\\.git\\' -and
|
||||
$_.FullName -notmatch '\\node_modules\\'
|
||||
}
|
||||
$availablePages = @{}
|
||||
foreach ($f in $allMd) {
|
||||
$bn = $f.BaseName
|
||||
$availablePages[$bn] = $true
|
||||
}
|
||||
|
||||
# Add known non-.md references that are valid
|
||||
$validRefs = @(
|
||||
'Trips.base','Map.base','Places.base','Books.base','Movies.base',
|
||||
'黄鹤楼.base','Templates/Bases/Places.base','Templates/Bases/Map.base'
|
||||
)
|
||||
foreach ($ref in $validRefs) { $availablePages[$ref] = $true }
|
||||
|
||||
# Scan wiki files for broken links
|
||||
$brokenLinks = @()
|
||||
$wikiFiles = Get-ChildItem -Path $wiki -Filter '*.md' -File
|
||||
|
||||
foreach ($f in $wikiFiles) {
|
||||
$content = [System.IO.File]::ReadAllText($f.FullName)
|
||||
$matches = [regex]::Matches($content, '\[\[([^\]|]+)(?:\|[^\]]+)?\]\]')
|
||||
|
||||
foreach ($m in $matches) {
|
||||
$linkTarget = $m.Groups[1].Value.Trim()
|
||||
|
||||
# Skip various patterns
|
||||
if ($linkTarget -match '^http' -or $linkTarget -match '^#') { continue }
|
||||
if ($linkTarget -match '^Attachments/') { continue }
|
||||
if ($linkTarget -match '\.base#') { continue } # .base section refs
|
||||
if ($linkTarget -match '^Templates/') { continue }
|
||||
if ($linkTarget -match '^References/') { continue }
|
||||
if ($linkTarget -match '^Categories/') { continue }
|
||||
if ($linkTarget -match '百度百科/') { continue }
|
||||
if ($linkTarget -match '^raw/') { continue } # raw/ references
|
||||
|
||||
# Check if target exists
|
||||
if (-not $availablePages.ContainsKey($linkTarget)) {
|
||||
$brokenLinks += [PSCustomObject]@{
|
||||
File = $f.Name
|
||||
BrokenLink = $linkTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($brokenLinks.Count -eq 0) {
|
||||
Write-Output "BROKEN_LINKS: 0"
|
||||
} else {
|
||||
Write-Output "BROKEN_LINKS: $($brokenLinks.Count)"
|
||||
$brokenLinks | ForEach-Object { Write-Output "$($_.File) -> [[$($_.BrokenLink)]]" }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$wiki = 'D:\Applications\app\kepano-obsidian-main\wiki'
|
||||
$vault = 'D:\Applications\app\kepano-obsidian-main'
|
||||
|
||||
# Get all available pages in vault
|
||||
$allMd = Get-ChildItem -Path $vault -Filter '*.md' -File -Recurse
|
||||
$availablePages = @{}
|
||||
foreach ($f in $allMd) {
|
||||
$bn = $f.BaseName
|
||||
$availablePages[$bn] = $true
|
||||
# Also register with Chinese characters variations
|
||||
}
|
||||
|
||||
# Scan wiki files for broken links
|
||||
$brokenLinks = @()
|
||||
$wikiFiles = Get-ChildItem -Path $wiki -Filter '*.md' -File
|
||||
|
||||
foreach ($f in $wikiFiles) {
|
||||
$content = [System.IO.File]::ReadAllText($f.FullName)
|
||||
# Match [[Link]] or [[Link|Display]]
|
||||
$matches = [regex]::Matches($content, '\[\[([^\]|]+)(?:\|[^\]]+)?\]\]')
|
||||
foreach ($m in $matches) {
|
||||
$linkTarget = $m.Groups[1].Value.Trim()
|
||||
# Skip anchors and external links
|
||||
if ($linkTarget -match '^http' -or $linkTarget -match '^#') { continue }
|
||||
|
||||
if (-not $availablePages.ContainsKey($linkTarget)) {
|
||||
$brokenLinks += [PSCustomObject]@{
|
||||
File = $f.Name
|
||||
BrokenLink = $linkTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($brokenLinks.Count -eq 0) {
|
||||
Write-Output "BROKEN_LINKS: 0"
|
||||
} else {
|
||||
Write-Output "BROKEN_LINKS: $($brokenLinks.Count)"
|
||||
$brokenLinks | ForEach-Object { Write-Output "$($_.File) -> [[$($_.BrokenLink)]]" }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$wiki = 'D:\Applications\app\kepano-obsidian-main\wiki'
|
||||
|
||||
foreach ($f in Get-ChildItem $wiki -Filter '*.md' -File) {
|
||||
$content = [System.IO.File]::ReadAllText($f.FullName)
|
||||
$name = $f.BaseName
|
||||
|
||||
# Check if type is empty
|
||||
if ($content -match '(?m)^type:\s*$') {
|
||||
$newType = 'entity'
|
||||
|
||||
# Check if it's a place
|
||||
if ($content -match 'Places|地点' -or $name -match '^([天地山河湖海关陵墓])' -or $name -match '城$|省$|市$|县$|山$|河$|湖$|海$|关$|陵$|墓$|遗址$|塔$|院$|阁$|庙$') {
|
||||
$newType = 'place'
|
||||
}
|
||||
|
||||
$content = $content -replace '(?m)^type:\s*$', ('type: ' + $newType)
|
||||
[System.IO.File]::WriteAllText($f.FullName, $content)
|
||||
Write-Output "FIXED: $name -> type: $newType"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$wikiPath = 'D:\Applications\app\kepano-obsidian-main\wiki'
|
||||
$vaultPath = 'D:\Applications\app\kepano-obsidian-main'
|
||||
|
||||
$wikiFiles = Get-ChildItem -Path $wikiPath -Filter '*.md' -File
|
||||
$pageNames = $wikiFiles | ForEach-Object { $_.BaseName }
|
||||
|
||||
$allMd = Get-ChildItem -Path $vaultPath -Filter '*.md' -File -Recurse | Where-Object { $_.Length -lt 500KB }
|
||||
$allContent = @{}
|
||||
foreach ($f in $allMd) {
|
||||
try { $allContent[$f.FullName] = [System.IO.File]::ReadAllText($f.FullName) } catch {}
|
||||
}
|
||||
|
||||
$orphans = @()
|
||||
foreach ($pn in $pageNames) {
|
||||
if ($pn -eq 'index' -or $pn -eq 'log') { continue }
|
||||
$linkPattern = '[[' + $pn + ']'
|
||||
$hasInbound = $false
|
||||
foreach ($fp in $allContent.Keys) {
|
||||
$baseF = [System.IO.Path]::GetFileNameWithoutExtension($fp)
|
||||
if ($baseF -eq $pn) { continue }
|
||||
if ($allContent[$fp] -match [regex]::Escape($linkPattern)) {
|
||||
$hasInbound = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $hasInbound) { $orphans += $pn }
|
||||
}
|
||||
Write-Output "ORPHAN_COUNT: $($orphans.Count)"
|
||||
$orphans | ForEach-Object { Write-Output $_ }
|
||||
@@ -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