add: zkb-search skill (Z盘知识库检索, 防UTF-8乱码假象)
This commit is contained in:
@@ -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 个损坏文件已恢复)
|
||||
- 文件名是合法 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(历史遗留) |
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user