add: zkb-search skill (Z盘知识库检索, 防UTF-8乱码假象)

This commit is contained in:
hmo
2026-08-13 09:03:16 +08:00
parent 4609a8ca63
commit 93ecbbb10c
3 changed files with 437 additions and 0 deletions
+100
View File
@@ -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
@@ -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 个损坏文件已恢复)
- 文件名是合法 UnicodePowerShell 显示乱码是 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-13Total 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(历史遗留) |
+281
View File
@@ -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()