@jerryjiao/knowflow 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/CONTRIBUTING.md +41 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/README.zh-CN.md +176 -0
- package/SECURITY.md +18 -0
- package/bin/knowflow.js +367 -0
- package/docs/README-CN.md +10 -0
- package/docs/architecture/system-architecture.md +150 -0
- package/docs/assets/knowflow-graph-demo.png +0 -0
- package/docs/assets/knowflow-social-preview.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/contributing.md +131 -0
- package/docs/methodology/llm-wiki-methodology.md +110 -0
- package/docs/reference/data-model.md +200 -0
- package/examples/quickstart.md +60 -0
- package/package.json +57 -0
- package/scripts/batch-ingest.cjs +455 -0
- package/scripts/bookmark_sync.sh +150 -0
- package/scripts/enrich-wiki.js +683 -0
- package/scripts/graph_builder.py +612 -0
- package/scripts/graph_relation_labeler.py +191 -0
- package/scripts/ingest.sh +162 -0
- package/scripts/pipeline.sh +73 -0
- package/scripts/tags-builder.mjs +75 -0
- package/scripts/vector-store.mjs +717 -0
- package/scripts/vector_store.py +225 -0
- package/scripts/wechat_sync.sh +199 -0
- package/scripts/wiki-auto-fix.sh +238 -0
- package/scripts/wiki-health.py +285 -0
- package/scripts/wiki-health.sh +335 -0
- package/templates/comparison.md +31 -0
- package/templates/concept.md +37 -0
- package/templates/entity.md +32 -0
- package/templates/source.md +36 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Wiki Health Check — validates links, file sizes, and orphan pages.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python scripts/wiki-health.py [WIKI_DIR]
|
|
6
|
+
|
|
7
|
+
Checks:
|
|
8
|
+
1. Broken links: [[wiki-links]] and [markdown](links) pointing to missing files
|
|
9
|
+
2. Tiny files: .md files under 100 bytes
|
|
10
|
+
3. Orphan pages: .md files not linked from any other page (excludes index.md, topics.md, overview.md, log.md)
|
|
11
|
+
|
|
12
|
+
Exit codes:
|
|
13
|
+
0 — all checks pass (or only warnings)
|
|
14
|
+
1 — broken links found
|
|
15
|
+
2 — errors during execution
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import urllib.request
|
|
22
|
+
import urllib.parse
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
|
|
26
|
+
# --- Configuration ---
|
|
27
|
+
WIKI_ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("wiki")
|
|
28
|
+
TINY_THRESHOLD = 100 # bytes
|
|
29
|
+
INDEX_FILES = {"index.md", "topics.md", "overview.md", "log.md"}
|
|
30
|
+
|
|
31
|
+
# --- Helpers ---
|
|
32
|
+
|
|
33
|
+
def resolve_wiki_link(link_target: str, source_file: Path, wiki_root: Path) -> Path:
|
|
34
|
+
"""Resolve a [[wiki-link]] target to an actual file path.
|
|
35
|
+
|
|
36
|
+
Wiki-links are relative to wiki_root, not to the source file.
|
|
37
|
+
Supports optional display text: [[target|Display Text]] -> target
|
|
38
|
+
"""
|
|
39
|
+
# Strip optional display text: [[target|display]] -> target
|
|
40
|
+
target = link_target.split("|")[0].strip()
|
|
41
|
+
if not target:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# Wiki-links are relative to wiki root
|
|
45
|
+
resolved = wiki_root / target
|
|
46
|
+
|
|
47
|
+
# Try exact path first
|
|
48
|
+
if resolved.is_file():
|
|
49
|
+
return resolved
|
|
50
|
+
|
|
51
|
+
# Try with .md extension
|
|
52
|
+
md_path = resolved.with_suffix(".md")
|
|
53
|
+
if md_path.is_file():
|
|
54
|
+
return md_path
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_md_link(link_target: str, source_file: Path, wiki_root: Path) -> Path:
|
|
60
|
+
"""Resolve a [markdown](link) target to an actual file path.
|
|
61
|
+
|
|
62
|
+
Markdown links are relative to the source file's directory.
|
|
63
|
+
"""
|
|
64
|
+
if not link_target or link_target.startswith(("http://", "https://", "#", "mailto:")):
|
|
65
|
+
return None # External or anchor links — skip
|
|
66
|
+
|
|
67
|
+
# URL-decode for Chinese filenames
|
|
68
|
+
decoded = urllib.parse.unquote(link_target)
|
|
69
|
+
|
|
70
|
+
# Markdown links are relative to the source file directory
|
|
71
|
+
source_dir = source_file.parent
|
|
72
|
+
resolved = (source_dir / decoded).resolve()
|
|
73
|
+
|
|
74
|
+
if resolved.is_file():
|
|
75
|
+
return resolved
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def strip_code_blocks(content: str) -> str:
|
|
81
|
+
"""Remove fenced code blocks and inline code to avoid false positive links."""
|
|
82
|
+
# Remove fenced code blocks (```...```)
|
|
83
|
+
content = re.sub(r'```.*?```', '', content, flags=re.DOTALL)
|
|
84
|
+
# Remove inline code (`...`)
|
|
85
|
+
content = re.sub(r'`[^`]+`', '', content)
|
|
86
|
+
return content
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_links(content: str, source_file: Path, wiki_root: Path):
|
|
90
|
+
"""Extract all link targets from markdown content.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
wiki_links: set of (raw_target, resolved_path_or_None) for [[]] links
|
|
94
|
+
md_links: set of (raw_target, resolved_path_or_None) for []() links
|
|
95
|
+
"""
|
|
96
|
+
wiki_links = set()
|
|
97
|
+
md_links = set()
|
|
98
|
+
|
|
99
|
+
# Strip code blocks to avoid false positives
|
|
100
|
+
clean = strip_code_blocks(content)
|
|
101
|
+
|
|
102
|
+
# [[wiki-links]] — may contain | for display text
|
|
103
|
+
for match in re.finditer(r'\[\[([^\]]+)\]\]', clean):
|
|
104
|
+
raw = match.group(1)
|
|
105
|
+
target = raw.split("|")[0].strip()
|
|
106
|
+
resolved = resolve_wiki_link(target, source_file, wiki_root)
|
|
107
|
+
wiki_links.add((target, resolved))
|
|
108
|
+
|
|
109
|
+
# [markdown](links) — standard markdown
|
|
110
|
+
for match in re.finditer(r'\[([^\]]*)\]\(([^)]+)\)', clean):
|
|
111
|
+
raw_target = match.group(2).strip()
|
|
112
|
+
resolved = resolve_md_link(raw_target, source_file, wiki_root)
|
|
113
|
+
if resolved is not None or not raw_target.startswith(("http://", "https://", "#", "mailto:")):
|
|
114
|
+
md_links.add((raw_target, resolved))
|
|
115
|
+
|
|
116
|
+
return wiki_links, md_links
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- Checks ---
|
|
120
|
+
|
|
121
|
+
def check_broken_links(wiki_root: Path) -> list[dict]:
|
|
122
|
+
"""Find all broken links across the wiki."""
|
|
123
|
+
broken = []
|
|
124
|
+
|
|
125
|
+
for md_file in sorted(wiki_root.rglob("*.md")):
|
|
126
|
+
try:
|
|
127
|
+
content = md_file.read_text(encoding="utf-8")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
broken.append({
|
|
130
|
+
"file": str(md_file.relative_to(wiki_root)),
|
|
131
|
+
"error": f"Cannot read file: {e}"
|
|
132
|
+
})
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
wiki_links, md_links = extract_links(content, md_file, wiki_root)
|
|
136
|
+
|
|
137
|
+
rel_path = str(md_file.relative_to(wiki_root))
|
|
138
|
+
|
|
139
|
+
for raw_target, resolved in wiki_links:
|
|
140
|
+
if resolved is None:
|
|
141
|
+
broken.append({
|
|
142
|
+
"type": "wiki-link",
|
|
143
|
+
"file": rel_path,
|
|
144
|
+
"target": raw_target,
|
|
145
|
+
"detail": f"[[{raw_target}]] -> file not found"
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
for raw_target, resolved in md_links:
|
|
149
|
+
# Skip external links
|
|
150
|
+
if raw_target.startswith(("http://", "https://", "#", "mailto:")):
|
|
151
|
+
continue
|
|
152
|
+
if resolved is None:
|
|
153
|
+
broken.append({
|
|
154
|
+
"type": "md-link",
|
|
155
|
+
"file": rel_path,
|
|
156
|
+
"target": raw_target,
|
|
157
|
+
"detail": f"[]({raw_target}) -> file not found"
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return broken
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_tiny_files(wiki_root: Path, threshold: int = TINY_THRESHOLD) -> list[dict]:
|
|
164
|
+
"""Find .md files smaller than threshold bytes."""
|
|
165
|
+
tiny = []
|
|
166
|
+
|
|
167
|
+
for md_file in sorted(wiki_root.rglob("*.md")):
|
|
168
|
+
try:
|
|
169
|
+
size = md_file.stat().st_size
|
|
170
|
+
except OSError:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
if size < threshold:
|
|
174
|
+
tiny.append({
|
|
175
|
+
"file": str(md_file.relative_to(wiki_root)),
|
|
176
|
+
"size": size,
|
|
177
|
+
"detail": f"{size}B < {threshold}B threshold"
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return tiny
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def check_orphan_pages(wiki_root: Path) -> list[dict]:
|
|
184
|
+
"""Find .md pages not referenced by any other page."""
|
|
185
|
+
# Collect all existing pages
|
|
186
|
+
all_pages = set()
|
|
187
|
+
for md_file in wiki_root.rglob("*.md"):
|
|
188
|
+
all_pages.add(md_file)
|
|
189
|
+
|
|
190
|
+
# Collect all link targets across all files
|
|
191
|
+
referenced = set()
|
|
192
|
+
for md_file in wiki_root.rglob("*.md"):
|
|
193
|
+
try:
|
|
194
|
+
content = md_file.read_text(encoding="utf-8")
|
|
195
|
+
except Exception:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
wiki_links, md_links = extract_links(content, md_file, wiki_root)
|
|
199
|
+
|
|
200
|
+
for _, resolved in wiki_links:
|
|
201
|
+
if resolved is not None:
|
|
202
|
+
referenced.add(resolved)
|
|
203
|
+
|
|
204
|
+
for _, resolved in md_links:
|
|
205
|
+
if resolved is not None:
|
|
206
|
+
referenced.add(resolved)
|
|
207
|
+
|
|
208
|
+
orphans = []
|
|
209
|
+
for page in sorted(all_pages):
|
|
210
|
+
rel = str(page.relative_to(wiki_root))
|
|
211
|
+
# Index files are never considered orphans
|
|
212
|
+
if page.name in INDEX_FILES:
|
|
213
|
+
continue
|
|
214
|
+
if page not in referenced:
|
|
215
|
+
orphans.append({
|
|
216
|
+
"file": rel,
|
|
217
|
+
"detail": "not linked from any other page"
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return orphans
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# --- Main ---
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
if not WIKI_ROOT.is_dir():
|
|
227
|
+
print(f"ERROR: wiki directory not found: {WIKI_ROOT}")
|
|
228
|
+
sys.exit(2)
|
|
229
|
+
|
|
230
|
+
print(f"Wiki Health Check — {WIKI_ROOT.resolve()}")
|
|
231
|
+
print(f"{'=' * 60}")
|
|
232
|
+
|
|
233
|
+
# Count files
|
|
234
|
+
md_files = list(WIKI_ROOT.rglob("*.md"))
|
|
235
|
+
print(f"Total .md files: {len(md_files)}")
|
|
236
|
+
|
|
237
|
+
exit_code = 0
|
|
238
|
+
|
|
239
|
+
# 1. Broken links
|
|
240
|
+
print(f"\n--- Broken Links ---")
|
|
241
|
+
broken = check_broken_links(WIKI_ROOT)
|
|
242
|
+
if broken:
|
|
243
|
+
errors = [b for b in broken if "error" in b]
|
|
244
|
+
link_issues = [b for b in broken if "error" not in b]
|
|
245
|
+
if link_issues:
|
|
246
|
+
print(f" BROKEN LINKS: {len(link_issues)}")
|
|
247
|
+
for item in link_issues:
|
|
248
|
+
print(f" [{item['type']}] {item['file']} -> {item['target']}")
|
|
249
|
+
exit_code = 1
|
|
250
|
+
if errors:
|
|
251
|
+
print(f" READ ERRORS: {len(errors)}")
|
|
252
|
+
for item in errors:
|
|
253
|
+
print(f" {item['file']}: {item['error']}")
|
|
254
|
+
else:
|
|
255
|
+
print(" OK — no broken links found")
|
|
256
|
+
|
|
257
|
+
# 2. Tiny files
|
|
258
|
+
print(f"\n--- Tiny Files (< {TINY_THRESHOLD}B) ---")
|
|
259
|
+
tiny = check_tiny_files(WIKI_ROOT)
|
|
260
|
+
if tiny:
|
|
261
|
+
print(f" TINY FILES: {len(tiny)}")
|
|
262
|
+
for item in tiny:
|
|
263
|
+
print(f" {item['file']} ({item['detail']})")
|
|
264
|
+
else:
|
|
265
|
+
print(" OK — no tiny files found")
|
|
266
|
+
|
|
267
|
+
# 3. Orphan pages
|
|
268
|
+
print(f"\n--- Orphan Pages ---")
|
|
269
|
+
orphans = check_orphan_pages(WIKI_ROOT)
|
|
270
|
+
if orphans:
|
|
271
|
+
print(f" ORPHAN PAGES: {len(orphans)}")
|
|
272
|
+
for item in orphans:
|
|
273
|
+
print(f" {item['file']}")
|
|
274
|
+
else:
|
|
275
|
+
print(" OK — no orphan pages found")
|
|
276
|
+
|
|
277
|
+
print(f"\n{'=' * 60}")
|
|
278
|
+
total_issues = len(broken) + len(tiny) + len(orphans)
|
|
279
|
+
print(f"Total issues: {total_issues}")
|
|
280
|
+
|
|
281
|
+
sys.exit(exit_code)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == "__main__":
|
|
285
|
+
main()
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/opt/homebrew/bin/bash
|
|
2
|
+
# knowflow health check — broken links, empty files, orphan pages
|
|
3
|
+
# Usage: bash scripts/wiki-health.sh [--wiki-root <path>] [--json]
|
|
4
|
+
#
|
|
5
|
+
# M2 改进:
|
|
6
|
+
# - 添加 --json 选项输出结构化 JSON(方便 CLI 解析)
|
|
7
|
+
# - 输出结构化格式,每行前缀统一
|
|
8
|
+
# - 保持向后兼容(默认人类可读模式不变)
|
|
9
|
+
#
|
|
10
|
+
# Checks:
|
|
11
|
+
# 1. Broken links — [[wikilinks]] and [md](path.md) that point to missing files
|
|
12
|
+
# 2. Empty files — .md files < 100 bytes
|
|
13
|
+
# 3. Orphan pages — .md files never referenced by any other page
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# ── Config ──────────────────────────────────────────────
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
WIKI_ROOT="${KNOWFLOW_ROOT:-${WIKI_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}"
|
|
20
|
+
WIKI_DIR="${KNOWFLOW_WIKI_DIR:-$WIKI_ROOT/wiki}"
|
|
21
|
+
MIN_SIZE=100
|
|
22
|
+
OUTPUT_FORMAT="text" # text | json
|
|
23
|
+
|
|
24
|
+
# Parse arguments
|
|
25
|
+
while [[ $# -gt 0 ]]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--wiki-root) WIKI_ROOT="$2"; WIKI_DIR="$2/wiki"; shift 2 ;;
|
|
28
|
+
--wiki-dir) WIKI_DIR="$2"; shift 2 ;;
|
|
29
|
+
--json) OUTPUT_FORMAT="json"; shift ;;
|
|
30
|
+
--min-size) MIN_SIZE="$2"; shift 2 ;;
|
|
31
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if [ ! -d "$WIKI_DIR" ]; then
|
|
36
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
37
|
+
echo "{\"status\":\"error\",\"message\":\"Wiki directory not found: $WIKI_DIR\"}"
|
|
38
|
+
else
|
|
39
|
+
echo "Error: wiki directory not found at $WIKI_DIR" >&2
|
|
40
|
+
fi
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── Counters ────────────────────────────────────────────
|
|
45
|
+
BROKEN_COUNT=0
|
|
46
|
+
EMPTY_COUNT=0
|
|
47
|
+
ORPHAN_COUNT=0
|
|
48
|
+
|
|
49
|
+
# JSON output accumulators (used only when --json)
|
|
50
|
+
JSON_BROKEN=""
|
|
51
|
+
JSON_EMPTY=""
|
|
52
|
+
JSON_ORPHAN=""
|
|
53
|
+
|
|
54
|
+
# ── Helper: resolve a wikilink to an absolute path ──
|
|
55
|
+
resolve_link() {
|
|
56
|
+
local mode="$1"
|
|
57
|
+
local basedir="$2"
|
|
58
|
+
local target="$3"
|
|
59
|
+
local abs_path
|
|
60
|
+
|
|
61
|
+
if [[ "$target" == /* ]]; then
|
|
62
|
+
abs_path="$WIKI_DIR${target}"
|
|
63
|
+
elif [ "$mode" = "wiki" ]; then
|
|
64
|
+
abs_path="$WIKI_DIR/$target"
|
|
65
|
+
else
|
|
66
|
+
abs_path="$WIKI_DIR/$basedir/$target"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local dir_part="$(dirname "$abs_path")"
|
|
70
|
+
local base_part="$(basename "$abs_path")"
|
|
71
|
+
local norm_dir
|
|
72
|
+
norm_dir="$(cd "$dir_part" 2>/dev/null && pwd)" 2>/dev/null || {
|
|
73
|
+
return 1
|
|
74
|
+
}
|
|
75
|
+
echo "${norm_dir}/${base_part}"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ── Helper: extract [[wikilinks]] (macOS grep compatible) ──
|
|
79
|
+
# grep -o finds every link on a line; a sed s///gp approach would only catch
|
|
80
|
+
# one per line, which silently drops references on index-style pages that put
|
|
81
|
+
# dozens of links on a single line.
|
|
82
|
+
extract_wikilinks() {
|
|
83
|
+
grep -oE '\[\[[^]]+\]\]' "$1" 2>/dev/null \
|
|
84
|
+
| sed 's/^\[\[//; s/\]\]$//; s/|.*//' \
|
|
85
|
+
| grep -v '^[[:space:]]*$' \
|
|
86
|
+
| awk '!seen[$0]++' || true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── Helper: extract [text](path.md) markdown links ──
|
|
90
|
+
extract_mdlinks() {
|
|
91
|
+
grep -oE '\]\([^)]+\.md\)' "$1" 2>/dev/null \
|
|
92
|
+
| sed 's/^\](//; s/)$//' \
|
|
93
|
+
| awk '!seen[$0]++' || true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ── Check 1: Broken Links ──────────────────────────────
|
|
97
|
+
check_broken_links() {
|
|
98
|
+
local found=0
|
|
99
|
+
local json_items=""
|
|
100
|
+
|
|
101
|
+
while IFS= read -r -d '' file; do
|
|
102
|
+
local relpath="${file#$WIKI_DIR/}"
|
|
103
|
+
local basedir
|
|
104
|
+
basedir="$(dirname "$relpath")"
|
|
105
|
+
|
|
106
|
+
# Check [[wikilinks]]
|
|
107
|
+
while IFS= read -r link; do
|
|
108
|
+
[ -z "$link" ] && continue
|
|
109
|
+
local target="${link%.md}.md"
|
|
110
|
+
local resolved
|
|
111
|
+
resolved="$(resolve_link wiki "$basedir" "$target")" || true
|
|
112
|
+
|
|
113
|
+
if [ -z "$resolved" ] || [ ! -f "$resolved" ]; then
|
|
114
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
115
|
+
json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"link\":\"$link\",\"type\":\"wikilink\"}"
|
|
116
|
+
else
|
|
117
|
+
echo " ✗ $relpath → [[$link]] (not found)"
|
|
118
|
+
fi
|
|
119
|
+
BROKEN_COUNT=$((BROKEN_COUNT + 1))
|
|
120
|
+
found=1
|
|
121
|
+
fi
|
|
122
|
+
done < <(extract_wikilinks "$file")
|
|
123
|
+
|
|
124
|
+
# Check [text](path.md) markdown links
|
|
125
|
+
while IFS= read -r link; do
|
|
126
|
+
[ -z "$link" ] && continue
|
|
127
|
+
case "$link" in http:*|https:*|mailto:*) continue ;; esac
|
|
128
|
+
local target="${link%.md}.md"
|
|
129
|
+
local resolved
|
|
130
|
+
resolved="$(resolve_link md "$basedir" "$target")" || true
|
|
131
|
+
|
|
132
|
+
if [ -z "$resolved" ] || [ ! -f "$resolved" ]; then
|
|
133
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
134
|
+
json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"link\":\"$link\",\"type:\"markdown\"}"
|
|
135
|
+
else
|
|
136
|
+
echo " ✗ $relpath → $link (not found)"
|
|
137
|
+
fi
|
|
138
|
+
BROKEN_COUNT=$((BROKEN_COUNT + 1))
|
|
139
|
+
found=1
|
|
140
|
+
fi
|
|
141
|
+
done < <(extract_mdlinks "$file")
|
|
142
|
+
|
|
143
|
+
done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
|
|
144
|
+
|
|
145
|
+
if [ "$found" -eq 0 ]; then
|
|
146
|
+
if [ "$OUTPUT_FORMAT" != "json" ]; then
|
|
147
|
+
echo " ✓ All links resolve correctly"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
JSON_BROKEN="$json_items"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ── Check 2: Empty Files ───────────────────────────────
|
|
155
|
+
check_empty_files() {
|
|
156
|
+
local found=0
|
|
157
|
+
local json_items=""
|
|
158
|
+
|
|
159
|
+
while IFS= read -r -d '' file; do
|
|
160
|
+
local relpath="${file#$WIKI_DIR/}"
|
|
161
|
+
local size
|
|
162
|
+
size="$(wc -c < "$file" | tr -d ' ')"
|
|
163
|
+
|
|
164
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
165
|
+
json_items="${json_items:+$json_items, }{\"file\":\"$relpath\",\"size\":$size}"
|
|
166
|
+
else
|
|
167
|
+
echo " ✗ $relpath ($size bytes)"
|
|
168
|
+
fi
|
|
169
|
+
EMPTY_COUNT=$((EMPTY_COUNT + 1))
|
|
170
|
+
found=1
|
|
171
|
+
done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -size -${MIN_SIZE}c -print0 2>/dev/null)
|
|
172
|
+
|
|
173
|
+
if [ "$found" -eq 0 ]; then
|
|
174
|
+
if [ "$OUTPUT_FORMAT" != "json" ]; then
|
|
175
|
+
echo " ✓ No empty files found"
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
JSON_EMPTY="$json_items"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ── Check 3: Orphan Pages ──────────────────────────────
|
|
183
|
+
# KNOWFLOW_HEALTH_EXCLUDE_ORPHAN (colon-separated relative dirs, e.g. "sources/:logs/")
|
|
184
|
+
# lists directories whose pages are expected to be unreferenced (feed/inbox
|
|
185
|
+
# pages) and should not count as orphans.
|
|
186
|
+
EXCLUDE_ORPHAN="${KNOWFLOW_HEALTH_EXCLUDE_ORPHAN:-}"
|
|
187
|
+
|
|
188
|
+
is_orphan_excluded() {
|
|
189
|
+
local rel="$1"
|
|
190
|
+
[ -z "$EXCLUDE_ORPHAN" ] && return 1
|
|
191
|
+
local parts part
|
|
192
|
+
IFS=':' read -ra parts <<< "$EXCLUDE_ORPHAN"
|
|
193
|
+
for part in "${parts[@]}"; do
|
|
194
|
+
part="${part%/}"
|
|
195
|
+
[[ -n "$part" && ( "$rel" == "$part" || "$rel" == "$part"/* ) ]] && return 0
|
|
196
|
+
done
|
|
197
|
+
return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
check_orphan_pages() {
|
|
201
|
+
local found=0
|
|
202
|
+
local json_items=""
|
|
203
|
+
|
|
204
|
+
local ref_file
|
|
205
|
+
ref_file="$(mktemp)"
|
|
206
|
+
|
|
207
|
+
while IFS= read -r -d '' file; do
|
|
208
|
+
local relpath="${file#$WIKI_DIR/}"
|
|
209
|
+
local basedir
|
|
210
|
+
basedir="$(dirname "$relpath")"
|
|
211
|
+
|
|
212
|
+
while IFS= read -r link; do
|
|
213
|
+
[ -z "$link" ] && continue
|
|
214
|
+
local target="${link%.md}.md"
|
|
215
|
+
local resolved
|
|
216
|
+
resolved="$(resolve_link wiki "$basedir" "$target")" || continue
|
|
217
|
+
echo "${resolved#$WIKI_DIR/}" >> "$ref_file"
|
|
218
|
+
done < <(extract_wikilinks "$file")
|
|
219
|
+
|
|
220
|
+
while IFS= read -r link; do
|
|
221
|
+
[ -z "$link" ] && continue
|
|
222
|
+
case "$link" in http:*|https:*|mailto:*) continue ;; esac
|
|
223
|
+
local target="${link%.md}.md"
|
|
224
|
+
local resolved
|
|
225
|
+
resolved="$(resolve_link md "$basedir" "$target")" || continue
|
|
226
|
+
echo "${resolved#$WIKI_DIR/}" >> "$ref_file"
|
|
227
|
+
done < <(extract_mdlinks "$file")
|
|
228
|
+
|
|
229
|
+
done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
|
|
230
|
+
|
|
231
|
+
while IFS= read -r -d '' file; do
|
|
232
|
+
local relpath="${file#$WIKI_DIR/}"
|
|
233
|
+
if [ "$relpath" = "index.md" ]; then
|
|
234
|
+
continue
|
|
235
|
+
fi
|
|
236
|
+
if is_orphan_excluded "$relpath"; then
|
|
237
|
+
continue
|
|
238
|
+
fi
|
|
239
|
+
if ! grep -qxF "$relpath" "$ref_file" 2>/dev/null; then
|
|
240
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
241
|
+
json_items="${json_items:+$json_items, }{\"file\":\"$relpath\"}"
|
|
242
|
+
else
|
|
243
|
+
echo " ✗ $relpath"
|
|
244
|
+
fi
|
|
245
|
+
ORPHAN_COUNT=$((ORPHAN_COUNT + 1))
|
|
246
|
+
found=1
|
|
247
|
+
fi
|
|
248
|
+
done < <(find "$WIKI_DIR" -name '*.md' -not -path '*/.understand-anything/*' -print0 2>/dev/null)
|
|
249
|
+
|
|
250
|
+
rm -f "$ref_file"
|
|
251
|
+
|
|
252
|
+
if [ "$found" -eq 0 ]; then
|
|
253
|
+
if [ "$OUTPUT_FORMAT" != "json" ]; then
|
|
254
|
+
echo " ✓ All pages are referenced"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
JSON_ORPHAN="$json_items"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ── Main ────────────────────────────────────────────────
|
|
262
|
+
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
263
|
+
TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
|
|
264
|
+
|
|
265
|
+
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
|
266
|
+
# ── JSON mode: run checks silently, output structured JSON ──
|
|
267
|
+
check_broken_links || { BROKEN_COUNT=0; }
|
|
268
|
+
check_empty_files || { EMPTY_COUNT=0; }
|
|
269
|
+
check_orphan_pages || { ORPHAN_COUNT=0; }
|
|
270
|
+
|
|
271
|
+
TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
|
|
272
|
+
|
|
273
|
+
cat << EOF
|
|
274
|
+
{
|
|
275
|
+
"status": "$([ $TOTAL -eq 0 ] && echo "ok" || echo "issues_found")",
|
|
276
|
+
"timestamp": "$TIMESTAMP",
|
|
277
|
+
"wiki_root": "$WIKI_DIR",
|
|
278
|
+
"summary": {
|
|
279
|
+
"broken_links": $BROKEN_COUNT,
|
|
280
|
+
"empty_files": $EMPTY_COUNT,
|
|
281
|
+
"orphan_pages": $ORPHAN_COUNT,
|
|
282
|
+
"total_issues": $TOTAL,
|
|
283
|
+
"min_size_bytes": $MIN_SIZE
|
|
284
|
+
},
|
|
285
|
+
"details": {
|
|
286
|
+
"broken_links": [$JSON_BROKEN],
|
|
287
|
+
"empty_files": [$JSON_EMPTY],
|
|
288
|
+
"orphan_pages": [$JSON_ORPHAN]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
EOF
|
|
292
|
+
|
|
293
|
+
[ $TOTAL -eq 0 ] && exit 0 || exit 1
|
|
294
|
+
|
|
295
|
+
else
|
|
296
|
+
# ── Text mode (default, backward compatible) ──
|
|
297
|
+
echo "🏥 KnowFlow Wiki — Health Check"
|
|
298
|
+
echo "====================================="
|
|
299
|
+
echo "Wiki root: $WIKI_DIR"
|
|
300
|
+
echo "Time: $TIMESTAMP"
|
|
301
|
+
echo ""
|
|
302
|
+
|
|
303
|
+
echo "## 🔗 Broken Links"
|
|
304
|
+
echo ""
|
|
305
|
+
check_broken_links || { echo " ⚠️ Broken links check interrupted"; BROKEN_COUNT=0; }
|
|
306
|
+
echo ""
|
|
307
|
+
|
|
308
|
+
echo "## 📄 Empty Files (< ${MIN_SIZE} bytes)"
|
|
309
|
+
echo ""
|
|
310
|
+
check_empty_files || { echo " ⚠️ Empty files check interrupted"; EMPTY_COUNT=0; }
|
|
311
|
+
echo ""
|
|
312
|
+
|
|
313
|
+
echo "## 🏝️ Orphan Pages (never referenced)"
|
|
314
|
+
echo ""
|
|
315
|
+
check_orphan_pages || { echo " ⚠️ Orphan pages check interrupted"; ORPHAN_COUNT=0; }
|
|
316
|
+
echo ""
|
|
317
|
+
|
|
318
|
+
TOTAL=$((BROKEN_COUNT + EMPTY_COUNT + ORPHAN_COUNT))
|
|
319
|
+
|
|
320
|
+
echo "## 📊 Summary"
|
|
321
|
+
echo ""
|
|
322
|
+
echo " Broken links: $BROKEN_COUNT"
|
|
323
|
+
echo " Empty files: $EMPTY_COUNT"
|
|
324
|
+
echo " Orphan pages: $ORPHAN_COUNT"
|
|
325
|
+
echo " Total issues: $TOTAL"
|
|
326
|
+
echo ""
|
|
327
|
+
|
|
328
|
+
if [ "$TOTAL" -eq 0 ]; then
|
|
329
|
+
echo "✅ All checks passed"
|
|
330
|
+
exit 0
|
|
331
|
+
else
|
|
332
|
+
echo "❌ $TOTAL issue(s) found"
|
|
333
|
+
exit 1
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# {{对比主题}}
|
|
2
|
+
|
|
3
|
+
> 对比分析 | 创建时间: {{YYYY-MM-DD}}
|
|
4
|
+
|
|
5
|
+
## 📋 对比对象
|
|
6
|
+
|
|
7
|
+
| 维度 | {{对象A}} | {{对象B}} | {{对象C(可选)}} |
|
|
8
|
+
|------|-----------|-----------|----------------|
|
|
9
|
+
| 定位/定义 | ... | ... | ... |
|
|
10
|
+
| 核心优势 | ... | ... | ... |
|
|
11
|
+
| 核心劣势 | ... | ... | ... |
|
|
12
|
+
| 适用场景 | ... | ... | ... |
|
|
13
|
+
| 学习成本 | ... | ... | ... |
|
|
14
|
+
| 生态成熟度 | ... | ... | ... |
|
|
15
|
+
| 成本 | ... | ... | ... |
|
|
16
|
+
|
|
17
|
+
## 🏆 结论
|
|
18
|
+
|
|
19
|
+
### 最适合的场景
|
|
20
|
+
- 场景 A → 推荐 **{{对象X}}**,因为...
|
|
21
|
+
- 场景 B → 推荐 **{{对象Y}}**,因为...
|
|
22
|
+
|
|
23
|
+
## 🔗 关联内容
|
|
24
|
+
- 实体: [[entities/...]], [[entities/...]]
|
|
25
|
+
- 概念: [[concepts/...]]
|
|
26
|
+
- 来源: [[sources/...]]
|
|
27
|
+
|
|
28
|
+
## ⚠️ 局限性
|
|
29
|
+
> 本次对比基于以下来源,可能存在信息偏差:
|
|
30
|
+
> - [[sources/{{来源1}}]]
|
|
31
|
+
> - [[sources/{{来源2}}]]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# {{概念名称}}
|
|
2
|
+
|
|
3
|
+
> 定义 | 置信度: EXTRACTED
|
|
4
|
+
|
|
5
|
+
## 📖 定义
|
|
6
|
+
{{一句话定义这个概念}}
|
|
7
|
+
|
|
8
|
+
## 🎯 核心观点
|
|
9
|
+
|
|
10
|
+
### 观点 1: {{标题}}
|
|
11
|
+
> 描述... (EXTRACTED)
|
|
12
|
+
>
|
|
13
|
+
> 来源: [[sources/{{来源}}]]
|
|
14
|
+
|
|
15
|
+
### 观点 2: {{标题}}
|
|
16
|
+
> 描述... (INFERRED)
|
|
17
|
+
|
|
18
|
+
### 观点 3: {{不同视角/反面观点}}
|
|
19
|
+
> 如果有争议或不同视角,记录在此
|
|
20
|
+
|
|
21
|
+
## 🔬 不同来源的视角对比
|
|
22
|
+
|
|
23
|
+
| 来源 | 视角 | 关键论点 |
|
|
24
|
+
|------|------|---------|
|
|
25
|
+
| [[sources/...]] | ... | ... |
|
|
26
|
+
| [[sources/...]] | ... | ... |
|
|
27
|
+
|
|
28
|
+
## 💡 实践应用
|
|
29
|
+
1. **应用场景 1**: ...
|
|
30
|
+
2. **应用场景 2**: ...
|
|
31
|
+
|
|
32
|
+
## 🔗 关联内容
|
|
33
|
+
- 实体: [[entities/{{相关实体}}]]
|
|
34
|
+
- 概念: [[concepts/{{相关概念}}]]
|
|
35
|
+
|
|
36
|
+
## 📚 延伸阅读
|
|
37
|
+
- [[sources/{{相关来源}}]]
|