diff --git a/zkb-search/SKILL.md b/zkb-search/SKILL.md new file mode 100644 index 0000000..c7d33f4 --- /dev/null +++ b/zkb-search/SKILL.md @@ -0,0 +1,100 @@ +--- +name: zkb-search +description: "检索 Z 盘知识库(\\192.168.1.246\\Obsidian)的文章/笔记。本 skill 应在以下场景使用:需要在知识库中查找文章、笔记、概念、系列;搜索刚收录的微信文章;查询知识库结构;读取知识库文件。核心价值:①用 Python 显式 UTF-8 处理中文,避免 Windows PowerShell/GBK 造成的"乱码假象";②知晓知识库三层结构(raw 原文 / wiki 正式页 / 索引),避免在错误层级搜索导致"找不到"。" +metadata: {"omo": {"emoji": "🔍", "requires": {"bins": ["py"]}}} +--- + +# Z 盘知识库检索 (ZKB Search) + +检索 `Z:\`(= `\\192.168.1.246\Obsidian`,246 服务器的知识库)中的文章/笔记/概念/系列。 + +## 🚨 最重要:编码铁律(防"乱码"假象) + +**知识库所有文件都是合法 UTF-8,没有乱码。** 如果你看到乱码(`��`、`�г`、`MiniMax-H3-3060-2K��Ƶ`),那是 **PowerShell/cmd 的 GBK 控制台显示问题**,不是文件问题。 + +| 方式 | 结果 | 说明 | +|------|------|------| +| `Get-Content Z:\xxx.md` | ❌ 乱码 | PowerShell 5.1 默认用 GBK 读/显示 UTF-8 | +| `cmd /c dir Z:\` | ❌ 乱码 | cmd 控制台 GBK | +| **`py -X utf8` 跑脚本** | ✅ 正常 | Python 显式 UTF-8 | +| **read 工具**(本会话) | ✅ 正常 | 正确解码 | +| SSH 到 246 用 `cat`/`file` | ✅ 正常 | Linux UTF-8 | + +**铁律**: +1. 读/搜 Z 盘文件,**一律用本 skill 的 `scripts/zkb_search.py`**(已强制 UTF-8) +2. 不要用 `Get-Content` 或 `cmd dir` 输出中文文件名/内容判断编码 +3. 判断文件是否乱码,用 `py -X utf8 -c "open(r'路径',encoding='utf-8').read()"` 看是否报错或含 `\ufffd` + +## 知识库结构(三层,搜索别找错层) + +``` +Z:\ +├── index.md # 根索引(人工/agent 维护,分类链接) +├── wechat-article-links.md # 公众号链接簿(URL→raw文件→入库状态)⚠️前86行有行号污染 +├── log.md / research_queue.md / self_remediation.md +├── raw\articles\ # 【第一层】原始文章(微信抓取等)≈371个 +├── wiki\ +│ ├── index.md # 【第三层】Wiki 索引 ⚠️可能滞后数周未更新 +│ ├── raw\articles\ # 【第一层镜像】≈372个(与 raw\articles 双写) +│ ├── articles\ # 【第二层】正式文章页 ≈197个(最新到2026-07) +│ ├── concepts\ # 【第二层】概念页 ≈206个(含 MiniMax-H3-系列 等) +│ ├── categories\ # 【第三层】分类索引 19个 +│ └── entities\ +├── knowledge\ daily\ docs\ # 知识库/日报/调研/技术沉淀 +└── .obsidian\ # 插件配置(graph.json 是视图配置非缓存) +``` + +**搜索策略**: +- 找**刚收录的文章**(近期微信文章)→ 搜 `raw\articles`(最新层) +- 找**正式沉淀文章** → 搜 `wiki\articles` + `wiki\concepts` +- 找**链接/入库状态** → 搜 `wechat-article-links.md` +- **注意**:`wiki\index.md` 可能滞后(如停在 07-22),raw 层有而 wiki 正式页没有是正常现象,不代表文章不存在 + +## 使用方式 + +所有命令用 `py -X utf8` 运行: + +```bash +# 全文搜索(文件名+内容) +py -X utf8 "D:\F\NewI\opencode\daily-workspace\.opencode\skills\zkb-search\scripts\zkb_search.py" search 关键词 + +# 按标题搜索(文件名+frontmatter title) +py -X utf8 "...\zkb_search.py" title 关键词 + +# 搜索引文件(wechat-article-links 等) +py -X utf8 "...\zkb_search.py" index 关键词 + +# 最近收录 N 篇 +py -X utf8 "...\zkb_search.py" recent 10 + +# 列出知识库结构 +py -X utf8 "...\zkb_search.py" list + +# 安全读取文件(UTF-8) +py -X utf8 "...\zkb_search.py" read "raw/articles/2026-08-12-xxx.md" + +# 按文件名片段查找 +py -X utf8 "...\zkb_search.py" find 文件名片段 +``` + +## 环境验证 + +```bash +py -X utf8 -c "import sys; sys.stdout.reconfigure(encoding='utf-8'); print('UTF-8 OK: 量化交易')" +# 必须输出: UTF-8 OK: 量化交易 (不是乱码) +``` + +## 已知问题 + +1. **PowerShell 显示乱码**:非文件问题,用 `py -X utf8` 或 read 工具即可 +2. **wiki/articles 正式页滞后**:8 月文章在 raw 层 + concepts 概念页,但 wiki/articles 正式页停在 07-30(索引已更新,可搜到) +3. **部分文件名截断**:个别文件名为 `2026-08-11-minimax-h3-实战指南七玩�?md`(末尾字符截断),搜索时用关键词前缀匹配 +4. **self_remediation.md 大量待修条目**:890 行,缺 frontmatter URL(历史遗留,不影响搜索) +5. **已修复项**:wechat-article-links.md 行号污染(✅)、wiki/index.md 8 月收录(✅ 2026-08-13)、U+FFFD 编码损坏(✅ 全局 0 个) + +## 性能注意 + +Z 盘是网络挂载,读取较慢。脚本已优化: +- title/find/recent 只读文件名(快) +- search 内容匹配只读每文件前 50KB 并跳过 >500KB 文件 +- 若 search 大目录超时,先 title/find 缩小范围再 read diff --git a/zkb-search/references/knowledge-base-structure.md b/zkb-search/references/knowledge-base-structure.md new file mode 100644 index 0000000..568a1e3 --- /dev/null +++ b/zkb-search/references/knowledge-base-structure.md @@ -0,0 +1,56 @@ +# 知识库结构与索引机制(2026-08-13 修复后实测) + +## 目录树 + +``` +Z:\ (= \\192.168.1.246\Obsidian, /home/hmo/Obsidian on 246) +├── index.md # 根索引 (2026-08-06 最后更新) +├── wechat-article-links.md # 公众号链接簿 (161行, ✅已修复行号污染) +├── log.md / research_queue.md / self_remediation.md +├── docs\系列标识规范.md # series/series_no frontmatter 规范 +├── raw\articles\ # 第一层: 原始文章 371个 (8月156个) +│ └── 2026-08-12-minimax-h3-antirez-纯C引擎.md 等 +├── wiki\ +│ ├── index.md # Wiki索引 (✅2026-08-13 已更新, 收录8月) +│ ├── log.md # Wiki变更日志 +│ ├── raw\articles\ # 第一层镜像 (与 raw\articles 同文件/硬链接) +│ ├── articles\ # 第二层: 正式文章页 197个 (最新2026-07-30) +│ ├── concepts\ # 第二层: 概念页 206个 (含 MiniMax-H3-系列 等8月页) +│ ├── categories\ # 第三层: 分类索引 19个 +│ └── entities\ +├── knowledge\ daily\ docs\ # 分类知识库/日报/调研/技术沉淀 +└── .obsidian\ # 插件配置 +``` + +## 编码实测结论(2026-08-13) + +- 全部索引文件:UTF-8 无 BOM,0 非法字节,0 乱码字符 ✅ +- wiki/articles 197 个 + wiki/concepts 206 个:0 编码损坏 ✅ +- raw/articles:0 编码损坏 ✅(1 个损坏文件已恢复) +- 文件名是合法 Unicode(PowerShell 显示乱码是 GBK 控制台假象) +- 用 read 工具/Python 直接读中文路径均成功 + +## 索引维护机制 + +1. **根 index.md**:人工/agent 维护分类链接 +2. **wiki 层**:Hermes-Wiki 风格管道维护(articles/concepts/categories) +3. **raw 层**:微信抓取管道写入,标题含日期前缀 `YYYY-MM-DD-主题.md` +4. **wechat-article-links.md**:记录 公众号URL | raw文件 | 入库状态 + +## 2026-08-13 修复记录 + +| 问题 | 修复 | +|------|------| +| wechat-article-links.md 前86行 `N|N|` 行号污染 | ✅ 已修复 106 行,剩余 0 | +| wiki/index.md 未收录 8 月内容 | ✅ 已补录:Articles +156 条、Concepts +57 条,Last updated 2026-08-13,Total 522 | +| raw/articles/2026-08-01-ai-smart-education-cloud-platform.md 编码损坏(U+FFFD 50个) | ✅ 已恢复(用备份还原 + 修复"基础教育"),全局 U+FFFD = 0 | + +## 剩余已知问题(未修复) + +| 问题 | 说明 | +|------|------| +| wiki/articles 正式页无 8 月文章 | 8 月 156 篇在 raw 层 + concepts 概念页覆盖,但 wiki/articles 正式页仍停在 07-30(如需可自动生成轻量页) | +| raw/articles 与 wiki/raw/articles 双写 | 实际是同一文件(硬链接),无分叉风险 | +| 部分文件名截断 | 个别文件名为 `2026-08-11-minimax-h3-实战指南七玩�?md`(末尾字符截断),搜索用前缀匹配 | +| self_remediation.md 890行 | 大量待修文章缺 frontmatter URL(历史遗留) | + diff --git a/zkb-search/scripts/zkb_search.py b/zkb-search/scripts/zkb_search.py new file mode 100644 index 0000000..56d82b4 --- /dev/null +++ b/zkb-search/scripts/zkb_search.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Z 盘知识库检索工具 (ZKB Search) +================================ +检索 Z 盘 (挂载自 192.168.1.246 的 Obsidian 知识库) 的文章/笔记。 + +⚠️ 编码铁律: 本脚本必须用 `py -X utf8` 运行, 否则 Windows 控制台 GBK 输出会乱码! + Windows 下请始终: py -X utf8 zkb_search.py <参数> + +用法: + py -X utf8 zkb_search.py search <关键词> # 全文搜索 (文件名+内容) + py -X utf8 zkb_search.py title <关键词> # 按标题搜索 (文件名+frontmatter title) + py -X utf8 zkb_search.py index <关键词> # 搜索索引文件 (wechat-article-links.md 等) + py -X utf8 zkb_search.py recent [N] # 最近 N 篇 (默认10) + py -X utf8 zkb_search.py list # 列出知识库结构 + py -X utf8 zkb_search.py read <文件路径> # 安全读取文件 (UTF-8) + py -X utf8 zkb_search.py find <文件名片段> # 按文件名查找 + +示例: + py -X utf8 zkb_search.py search 量化交易进阶 + py -X utf8 zkb_search.py title 因子挖掘 + py -X utf8 zkb_search.py recent 5 +""" +import os +import sys +import glob +import re +import io + +# 强制 UTF-8 输出 (防止 Windows GBK 控制台乱码) +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +# Z 盘根路径 +Z_ROOT = "Z:/" + +# 索引文件 +INDEX_FILES = [ + "index.md", # 根索引 (首页) + "wechat-article-links.md", # 微信文章索引 (最重要) + "log.md", + "research_queue.md", + "self_remediation.md", +] + +# 主要搜索目录 +SEARCH_DIRS = [ + "raw/articles", # 原文文章 (微信抓取等) + "wiki/articles", # LLM Wiki 精读 + "wiki/concepts", # LLM Wiki 概念 + "wiki/categories", # LLM Wiki 分类索引 + "knowledge", # 知识库分类 + "daily", # 日报/调研/沉淀 +] + + +def eprint(msg): + print(msg, file=sys.stderr, flush=True) + + +def safe_read(path): + """安全读取文件 (UTF-8, 不报错)""" + try: + with open(path, encoding='utf-8', errors='replace') as f: + return f.read() + except Exception as e: + return f"<读取失败: {e}>" + + +def search_content(keyword, max_results=20): + """全文搜索: 遍历主要目录, 匹配文件名和内容 (限制文件大小防卡顿)""" + results = [] + kw = keyword.lower() + for d in SEARCH_DIRS: + base = os.path.join(Z_ROOT, d) + if not os.path.isdir(base): + continue + for f in glob.glob(os.path.join(base, "*.md")): + try: + # 跳过超大文件 (>500KB) 防卡顿 + if os.path.getsize(f) > 500 * 1024: + continue + rel = os.path.relpath(f, Z_ROOT).replace("\\", "/") + fname = os.path.basename(f).lower() + # 文件名匹配 + if kw in fname: + results.append((rel, "filename", "")) + if len(results) >= max_results: + return results + continue + # 内容匹配 (只读前50KB) + with open(f, encoding='utf-8', errors='replace') as fh: + content = fh.read(50 * 1024) + if kw in content.lower(): + lines = content.split('\n') + match_line = "" + for line in lines: + if kw in line.lower(): + match_line = line.strip()[:120] + break + results.append((rel, "content", match_line)) + if len(results) >= max_results: + return results + except Exception: + continue + return results + + +def search_title(keyword, max_results=20): + """按标题搜索: 只匹配文件名 (快, 不读内容)""" + results = [] + kw = keyword.lower() + for d in SEARCH_DIRS: + base = os.path.join(Z_ROOT, d) + if not os.path.isdir(base): + continue + # 只 glob 一级目录, 更快 + for f in glob.glob(os.path.join(base, "*.md")): + try: + rel = os.path.relpath(f, Z_ROOT).replace("\\", "/") + fname = os.path.basename(f).lower() + if kw in fname: + results.append((rel, "file_title", fname[:100])) + if len(results) >= max_results: + return results + except Exception: + continue + return results + + +def search_index(keyword, max_results=30): + """搜索索引文件 (wechat-article-links.md 等)""" + results = [] + kw = keyword.lower() + for idx in INDEX_FILES: + path = os.path.join(Z_ROOT, idx) + if not os.path.exists(path): + continue + content = safe_read(path) + lines = content.split('\n') + for i, line in enumerate(lines): + if kw in line.lower(): + # 提取标题部分 + title_part = line[:150] + results.append((idx, f"line {i+1}", title_part)) + if len(results) >= max_results: + return results + return results + + +def recent(n=10): + """最近收录的文章: 从文件名日期前缀排序 (快速, 不读每个文件的mtime)""" + files = [] + base = os.path.join(Z_ROOT, "raw/articles") + if os.path.isdir(base): + try: + for entry in os.scandir(base): + if entry.name.endswith(".md"): + files.append(entry.name) + except Exception: + pass + # 从文件名提取 YYYY-MM-DD 日期 + def sort_key(name): + m = re.match(r'(\d{4}-\d{2}-\d{2})', name) + return m.group(1) if m else "0000-00-00" + files.sort(key=sort_key, reverse=True) + results = [] + for fname in files[:n]: + rel = "raw/articles/" + fname + date_part = sort_key(fname) + results.append((rel, date_part, "")) + return results + + +def list_structure(): + """列出知识库顶层结构""" + print("=== Z 盘知识库结构 ===") + for d in SEARCH_DIRS: + base = os.path.join(Z_ROOT, d) + if os.path.isdir(base): + count = len(glob.glob(os.path.join(base, "*.md"))) + print(f" {d}: {count} 个 md 文件") + print(f"\n=== 索引文件 ===") + for idx in INDEX_FILES: + path = os.path.join(Z_ROOT, idx) + if os.path.exists(path): + size = os.path.getsize(path) + print(f" {idx}: {size/1024:.1f} KB") + + +def cmd_read(path): + """安全读取指定文件""" + full = path if os.path.isabs(path) else os.path.join(Z_ROOT, path) + if not os.path.exists(full): + print(f"文件不存在: {path}") + return + content = safe_read(full) + print(content[:5000]) + + +def cmd_find(fragment): + """按文件名片段查找""" + results = [] + for d in SEARCH_DIRS: + base = os.path.join(Z_ROOT, d) + if not os.path.isdir(base): + continue + for f in glob.glob(os.path.join(base, "*.md")): + if fragment.lower() in os.path.basename(f).lower(): + rel = os.path.relpath(f, Z_ROOT).replace("\\", "/") + results.append(rel) + for r in results[:30]: + print(r) + print(f"\n共 {len(results)} 个匹配") + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + return + + cmd = sys.argv[1] + + if cmd == "search" and len(sys.argv) >= 3: + kw = sys.argv[2] + print(f"=== 全文搜索: {kw} ===") + results = search_content(kw) + if not results: + print("无结果") + for rel, match_type, detail in results: + prefix = "[文件名]" if match_type == "filename" else "[内容]" + print(f" {prefix} {rel}") + if detail: + print(f" {detail}") + print(f"\n共 {len(results)} 条") + + elif cmd == "title" and len(sys.argv) >= 3: + kw = sys.argv[2] + print(f"=== 标题搜索: {kw} ===") + results = search_title(kw) + if not results: + print("无结果") + for rel, match_type, detail in results: + prefix = "[文件名]" if match_type == "file_title" else "[title字段]" + print(f" {prefix} {rel}") + if detail: + print(f" {detail}") + print(f"\n共 {len(results)} 条") + + elif cmd == "index" and len(sys.argv) >= 3: + kw = sys.argv[2] + print(f"=== 索引搜索: {kw} ===") + results = search_index(kw) + if not results: + print("无结果") + for idx, loc, line in results: + print(f" [{idx}:{loc}] {line}") + print(f"\n共 {len(results)} 条") + + elif cmd == "recent": + n = int(sys.argv[2]) if len(sys.argv) >= 3 else 10 + print(f"=== 最近 {n} 篇 ===") + for rel, ts, _ in recent(n): + print(f" {ts} {rel}") + + elif cmd == "list": + list_structure() + + elif cmd == "read" and len(sys.argv) >= 3: + cmd_read(sys.argv[2]) + + elif cmd == "find" and len(sys.argv) >= 3: + cmd_find(sys.argv[2]) + + else: + print(__doc__) + + +if __name__ == "__main__": + main()