Skip to content

Commit 7e01db8

Browse files
toughhouCopilot
andcommitted
feat(skill): add ingest dependencies, LLM client, docs
- Port llm-client.ts: OpenAI-compatible SSE streaming (native fetch) - Port detect-language.ts: Unicode script-based language detection - Port output-language.ts: LLM output language directive builder - Port frontmatter.ts: js-yaml frontmatter parser - Port sources-merge.ts: frontmatter array field union merging - Port page-merge.ts: LLM-assisted wiki page merging - Port ingest-sanitize.ts: LLM output sanitization - Port ingest-cache.ts: SHA256 incremental ingest cache (Node.js crypto) - Port project-mutex.ts: per-project async mutex - Port web-search.ts: Tavily API web search wrapper - Add skill/docs/skill-mcp-progress.md: project plan and progress doc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 36ae487 commit 7e01db8

12 files changed

Lines changed: 953 additions & 7 deletions

SKILL.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ metadata:
3131

3232
| 能力 | 本技能 | llm-wiki-skill |
3333
|------|-------|----------------|
34-
| **图谱相关度** | 4 信号模型(直接链接×3 + 来源重叠×4 + Adamic-Adar×1.5 + 类型亲和×1)| 仅 wikilink,无权重 |
35-
| **社区检测** | Louvain 算法 + 凝聚度评分 | 主题页→社区(启发式|
36-
| **图谱洞察** | 惊人连接 + 知识缺口 + 桥节点检测 | |
37-
| **搜索** | RRF 混合(BM25 + 向量) | Grep 关键词 |
34+
| **图谱相关度** | 4 信号模型(直接链接×3 + 来源重叠×4 + Adamic-Adar×1.5 + 类型亲和×1)| 3 信号模型(共引强度 + 来源重叠 + 类型亲和度)|
35+
| **社区检测** | Louvain 算法 + 凝聚度评分 | Louvain 算法(graph-analysis.js|
36+
| **图谱洞察** | 惊人连接 + 知识缺口 + 桥节点检测 | 惊人连接 + 桥节点 + 孤立节点 + 稀疏社区(大图自动降级)|
37+
| **搜索** | RRF 混合(BM25 + 向量) | Grep + 别名展开 + 段落上限 |
3838
| **深度研究** | 网络搜索→LLM 综合→自动消化 ||
3939
| **审核队列** | 异步异步 sweep-reviews 系统 ||
4040
| **图像处理** | 视觉 API 图像标注管线 ||
41+
| **数字山水可视化** | 无(sigma.js 通用图谱)| ✅ 东方编辑部 × 数字山水风交互式 HTML |
42+
| **置信度标注** || ✅ EXTRACTED / INFERRED / AMBIGUOUS / UNVERIFIED |
43+
| **SessionStart hook** || ✅ 会话自动注入 wiki 上下文 |
4144

4245
---
4346

@@ -272,11 +275,13 @@ node ${SKILL_DIR}/skill/cli.js status <wiki_root>
272275

273276
| 场景 | 推荐方案 |
274277
|------|---------|
275-
| 日常 ingest(速度优先)| llm-wiki-skill(Shell,零开销)|
276-
| 图谱质量分析 | 本技能(graph + insights 命令)|
278+
| 日常 ingest(速度优先)| llm-wiki-skill(Shell,零开销,SHA256 缓存)|
279+
| 高精度图谱分析(Adamic-Adar) | 本技能(graph + insights 命令,4 信号模型)|
280+
| RRF 混合搜索 | 本技能(search 命令,BM25+向量)|
277281
| 深度研究专项 | 本技能(deep-research 命令)|
282+
| 基础图谱分析与可视化 | llm-wiki-skill(3 信号 + Louvain + 数字山水 HTML)|
278283
| 中文内容源(微信/知乎/小红书)| llm-wiki-skill |
279-
| Hermes Runtime 集成 | llm-wiki-skill(已有 HERMES.md)|
284+
| Hermes Runtime 集成 | llm-wiki-skill(已有 HERMES.md + SessionStart hook|
280285
| 本技能 Hermes 集成 | 参见 HERMES.md(需手动适配)|
281286

282287
---

skill/docs/skill-mcp-progress.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# llm_wiki Node.js Skill + MCP Server — 方案与进度
2+
3+
> 文档生成日期:2026-05-02
4+
> 状态:**进行中** — ingest / deep-research 实现中,PR 待更新
5+
6+
---
7+
8+
## 一、背景与目标
9+
10+
### 项目来源
11+
12+
[nashsu/llm_wiki](https://github.com/nashsu/llm_wiki) 是一个基于 Tauri v2(Rust + React/TypeScript)的桌面应用,核心功能是把本地源文件(Markdown/PDF/DOCX)通过 LLM 自动整理成结构化 Wiki。
13+
14+
### 需求
15+
16+
bid-sys 项目需要其后台核心逻辑,但 **不需要 GUI(Tauri 桌面应用)**,目标是:
17+
18+
1. **Node.js Skill** — 纯命令行可调用的 wiki 管理工具
19+
2. **MCP Server** — 将 wiki 操作暴露为 AI 可调用的工具(供 Claude Desktop / VS Code Copilot Chat 使用)
20+
3. **贡献 MCP** — 向 nashsu/llm_wiki 提交 PR,将 MCP 服务器作为官方插件
21+
22+
---
23+
24+
## 二、架构分析
25+
26+
### nashsu/llm_wiki 技术栈
27+
28+
```
29+
llm_wiki/
30+
├── src/ # React + TypeScript 前端(GUI 层)
31+
│ ├── lib/ # 核心业务逻辑(纯 TypeScript)⬅ 我们需要的
32+
│ ├── stores/ # Zustand React 状态管理
33+
│ └── commands/ # Tauri IPC 桥接层
34+
├── src-tauri/ # Rust 后端(文件 I/O、PDF 提取、系统集成)
35+
```
36+
37+
### 两个注入点
38+
39+
所有 `src/lib/*.ts` 通过两个抽象层与 Tauri 交互:
40+
41+
| 原始导入 | 功能 | Node.js 替代 |
42+
|---------|------|-------------|
43+
| `@/commands/fs` | 文件读写/列举 | `shims/fs-node.ts` |
44+
| `@/stores/*` | 应用状态(LLM 配置等)| `shims/stores-node.ts` |
45+
46+
Tauri HTTP 代理(`tauri-fetch.ts`)已内置 `isNodeEnv` 检测,直接降级到 `globalThis.fetch`,无需额外适配。
47+
48+
---
49+
50+
## 三、实现方案
51+
52+
### 方案选择:自包含副本(Self-Contained Copy)
53+
54+
`src/lib/*.ts` 复制并修补到 `skill/src/lib/`,修改所有 `@/` 路径别名为相对路径,完全独立于原始项目结构。
55+
56+
**优点:** 不依赖 tsconfig 路径别名,构建简单,易于移植
57+
**缺点:** 需手工同步上游更新
58+
59+
### 目录结构
60+
61+
```
62+
skill/
63+
├── src/
64+
│ ├── cli.ts # CLI 入口(8 个命令)
65+
│ ├── mcp-server.ts # MCP 服务器(7 个工具)
66+
│ ├── lib/ # 从 nashsu/llm_wiki 移植的核心库
67+
│ │ ├── graph-relevance.ts
68+
│ │ ├── wiki-graph.ts
69+
│ │ ├── graph-insights.ts
70+
│ │ ├── search.ts
71+
│ │ ├── path-utils.ts
72+
│ │ ├── llm-client.ts # LLM SSE 流式调用
73+
│ │ ├── detect-language.ts # Unicode 脚本语言检测
74+
│ │ ├── output-language.ts # 输出语言指令构建
75+
│ │ ├── frontmatter.ts # YAML frontmatter 解析器
76+
│ │ ├── sources-merge.ts # Frontmatter 数组字段合并
77+
│ │ ├── page-merge.ts # Wiki 页面内容合并(LLM)
78+
│ │ ├── ingest-sanitize.ts # LLM 输出清理
79+
│ │ ├── ingest-cache.ts # SHA256 内容缓存
80+
│ │ ├── project-mutex.ts # 按项目路径的异步互斥锁
81+
│ │ ├── ingest.ts # 核心 ingest 流水线(待完成)
82+
│ │ └── web-search.ts # Tavily 搜索 API
83+
│ ├── shims/ # Tauri → Node.js 适配层
84+
│ │ ├── fs-node.ts
85+
│ │ ├── stores-node.ts
86+
│ │ └── embedding-stub.ts
87+
│ └── types/
88+
│ └── wiki.ts
89+
├── package.json
90+
└── tsconfig.json
91+
92+
mcp-server/ # 独立 MCP 包(用于 PR 提交)
93+
├── src/index.ts
94+
├── package.json
95+
└── README.md
96+
```
97+
98+
---
99+
100+
## 四、功能清单
101+
102+
### CLI 命令
103+
104+
| 命令 | 状态 | 说明 |
105+
|------|------|------|
106+
| `status` || 统计 wiki 页面数量/类型 |
107+
| `search <query>` || BM25+RRF 全文搜索 |
108+
| `graph` || 构建并输出知识图谱(Louvain 社区检测)|
109+
| `insights` || 发现意外关联 + 知识盲点 |
110+
| `lint` || 检测孤立页面/断链/缺失字段 |
111+
| `init` || 初始化 wiki 目录结构 |
112+
| `ingest <file>` | 🔄 | LLM 自动摄入源文件 → wiki 页面 |
113+
| `deep-research <topic>` | 🔄 | 网络搜索 → LLM 综合 → 自动摄入 |
114+
115+
### MCP 工具
116+
117+
| 工具 | 状态 | 说明 |
118+
|------|------|------|
119+
| `wiki_status` || 获取 wiki 统计 |
120+
| `wiki_search` || 搜索 wiki 页面 |
121+
| `wiki_graph` || 获取知识图谱 |
122+
| `wiki_insights` || 获取 AI 见解 |
123+
| `wiki_lint` || 检查 wiki 健康度 |
124+
| `wiki_ingest` | 🔄 | 摄入源文件 |
125+
| `wiki_deep_research` | 🔄 | 深度研究 |
126+
127+
---
128+
129+
## 五、环境变量配置
130+
131+
```bash
132+
# LLM 配置(ingest / deep-research 必需)
133+
export LLM_PROVIDER=openai # openai | anthropic | ollama | deepseek
134+
export OPENAI_API_KEY=sk-...
135+
export LLM_MODEL=gpt-4o-mini
136+
export LLM_BASE_URL= # 自定义端点(可选)
137+
138+
# 网络搜索(deep-research 必需)
139+
export TAVILY_API_KEY=tvly-...
140+
141+
# 输出语言(可选,默认 auto 自动检测)
142+
export WIKI_OUTPUT_LANGUAGE=auto # auto | English | Chinese | Japanese | ...
143+
144+
# 调试
145+
export SKILL_VERBOSE=1 # 输出详细日志到 stderr
146+
```
147+
148+
---
149+
150+
## 六、依赖
151+
152+
```json
153+
{
154+
"dependencies": {
155+
"graphology": "^0.25.4",
156+
"graphology-communities-louvain": "^2.0.0",
157+
"@modelcontextprotocol/sdk": "^1.1.0",
158+
"js-yaml": "^4.1.0"
159+
}
160+
}
161+
```
162+
163+
---
164+
165+
## 七、开发进度
166+
167+
### 已完成
168+
169+
- [x] 分析 nashsu/llm_wiki 架构,识别 Tauri 注入点
170+
- [x] 创建 `shims/fs-node.ts` — Tauri IPC → Node.js fs 适配
171+
- [x] 创建 `shims/stores-node.ts` — Zustand → 模块级状态,支持 env 配置 LLM
172+
- [x] 创建 `shims/embedding-stub.ts` — 向量搜索优雅降级
173+
- [x] 移植并修补所有图谱库(graph-relevance, wiki-graph, graph-insights)
174+
- [x] 移植搜索库(BM25+RRF,向量可选)
175+
- [x] 移植 path-utils(纯工具函数)
176+
- [x] 实现 CLI 6 个命令:status/search/graph/insights/lint/init
177+
- [x] 实现 MCP 服务器 5 个工具
178+
- [x] npm install + tsc 构建通过
179+
- [x] 端到端测试:合成 wiki 数据验证所有命令
180+
- [x] Fork nashsu/llm_wiki → toughhou/llm_wiki
181+
- [x] 移植 llm-client.ts(OpenAI 兼容 SSE 流式调用)
182+
- [x] 移植 detect-language.ts(Unicode 脚本检测)
183+
- [x] 移植 output-language.ts
184+
- [x] 移植 frontmatter.ts(js-yaml 解析)
185+
- [x] 移植 sources-merge.ts(数组字段合并)
186+
- [x] 移植 page-merge.ts(LLM 辅助页面合并)
187+
- [x] 移植 ingest-sanitize.ts(LLM 输出清洗)
188+
- [x] 移植 ingest-cache.ts(SHA256 增量缓存)
189+
- [x] 移植 project-mutex.ts(并发保护)
190+
- [x] 移植 web-search.ts(Tavily API)
191+
- [x] 提交 PR #117 到 nashsu/llm_wiki
192+
193+
### 进行中
194+
195+
- [ ] 完成 ingest.ts — 两阶段 LLM 流水线(分析 → 生成 → 写文件)
196+
- [ ] 完成 deep-research.ts — 网络搜索 → LLM 综合 → auto-ingest
197+
- [ ] CLI 添加 ingest / deep-research 命令
198+
- [ ] MCP 服务器添加 wiki_ingest / wiki_deep_research 工具
199+
- [ ] 端到端测试(需真实 LLM API Key)
200+
- [ ] 更新 PR #117
201+
202+
### 待完成
203+
204+
- [ ] sweep-reviews(批量审核 wiki 页面)
205+
- [ ] 嵌入向量搜索(可选,需 embedding API)
206+
207+
---
208+
209+
## 八、PR 提交记录
210+
211+
| PR | 仓库 | 分支 | 状态 |
212+
|----|------|------|------|
213+
| #117 | nashsu/llm_wiki | feat/mcp-server | 开放中,待更新 |
214+
215+
---
216+
217+
## 九、本地测试方法
218+
219+
```bash
220+
cd skill && npm install && npm run build
221+
222+
# 测试基础命令
223+
node dist/cli.js status /path/to/wiki-project
224+
node dist/cli.js search "machine learning" /path/to/wiki-project
225+
node dist/cli.js graph /path/to/wiki-project
226+
node dist/cli.js insights /path/to/wiki-project
227+
node dist/cli.js lint /path/to/wiki-project
228+
229+
# 测试 ingest(需 LLM API Key)
230+
export OPENAI_API_KEY=sk-xxx
231+
node dist/cli.js ingest /path/to/source.md /path/to/wiki-project
232+
233+
# 测试 deep-research(需 LLM + Tavily)
234+
export TAVILY_API_KEY=tvly-xxx
235+
node dist/cli.js deep-research "transformer architecture" /path/to/wiki-project
236+
237+
# 启动 MCP 服务器
238+
node dist/mcp-server.js
239+
```
240+
241+
---
242+
243+
## 十、相关资源
244+
245+
- 上游仓库:https://github.com/nashsu/llm_wiki
246+
- 本仓库(fork):https://github.com/toughhou/llm_wiki
247+
- PR #117https://github.com/nashsu/llm_wiki/pull/117
248+
- bid-sys 项目:https://github.com/toughhou/bid-sys

skill/src/lib/detect-language.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Language detection — ported from nashsu/llm_wiki src/lib/detect-language.ts
3+
* Pure function, no external dependencies.
4+
*/
5+
export function detectLanguage(text: string): string {
6+
const counts: Record<string, number> = {}
7+
for (const ch of text) {
8+
const cp = ch.codePointAt(0)
9+
if (!cp || cp < 0x80) continue
10+
const script = getScript(cp)
11+
if (script) counts[script] = (counts[script] ?? 0) + 1
12+
}
13+
14+
if ((counts.Japanese ?? 0) > 0 && (counts.Chinese ?? 0) > 0) return "Japanese"
15+
16+
let maxScript = ""; let maxCount = 0
17+
for (const [script, count] of Object.entries(counts)) {
18+
if (count > maxCount) { maxScript = script; maxCount = count }
19+
}
20+
if (maxScript && maxCount >= 2) return maxScript
21+
22+
const latinLang = detectLatinLanguage(text)
23+
if (latinLang) return latinLang
24+
return "English"
25+
}
26+
27+
function getScript(cp: number): string | null {
28+
if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) || (cp >= 0x20000 && cp <= 0x2A6DF) || (cp >= 0xF900 && cp <= 0xFAFF)) return "Chinese"
29+
if ((cp >= 0x3040 && cp <= 0x309F) || (cp >= 0x30A0 && cp <= 0x30FF) || (cp >= 0x31F0 && cp <= 0x31FF) || (cp >= 0xFF65 && cp <= 0xFF9F)) return "Japanese"
30+
if ((cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0x1100 && cp <= 0x11FF) || (cp >= 0x3130 && cp <= 0x318F)) return "Korean"
31+
if ((cp >= 0x0600 && cp <= 0x06FF) || (cp >= 0x0750 && cp <= 0x077F) || (cp >= 0x08A0 && cp <= 0x08FF) || (cp >= 0xFB50 && cp <= 0xFDFF) || (cp >= 0xFE70 && cp <= 0xFEFF)) return "Arabic"
32+
if ((cp >= 0x0590 && cp <= 0x05FF) || (cp >= 0xFB1D && cp <= 0xFB4F)) return "Hebrew"
33+
if (cp >= 0x0E00 && cp <= 0x0E7F) return "Thai"
34+
if (cp >= 0x0900 && cp <= 0x097F) return "Hindi"
35+
if ((cp >= 0x0400 && cp <= 0x04FF) || (cp >= 0x0500 && cp <= 0x052F)) return "Russian"
36+
if ((cp >= 0x0370 && cp <= 0x03FF) || (cp >= 0x1F00 && cp <= 0x1FFF)) return "Greek"
37+
return null
38+
}
39+
40+
function detectLatinLanguage(text: string): string | null {
41+
const lower = text.toLowerCase()
42+
if (/[đếĩơũư]/.test(lower)) return "Vietnamese"
43+
if (/[ğış]/.test(lower) && /\b(bir|ve|için|ile|bu|da|de|değil|ama)\b/.test(lower)) return "Turkish"
44+
if (/[ąćęłńóśźż]/.test(lower)) return "Polish"
45+
if (/[ěšžřďťňů]/.test(lower)) return "Czech"
46+
if (/[äöüß]/.test(lower) && /\b(und|der|die|das|ist)\b/.test(lower)) return "German"
47+
if (/[àâçéèêëïîôùûüÿœæ]/.test(lower) && /\b(le|la|les|est|une|des)\b/.test(lower)) return "French"
48+
if (/[ãõç]/.test(lower) && /\b(o|a|os|as|de|do|da|é|em|um|uma|não|que)\b/.test(lower)) return "Portuguese"
49+
if ((/[áéíóúñ¿¡]/.test(lower) || /\b(el|la|los|las|de|del|es|en)\b/.test(lower)) && (/\b(el|los|las|del|por)\b/.test(lower) || /[ñ¿¡]/.test(lower))) return "Spanish"
50+
if (/\b(il|della|gli|che|è)\b/.test(lower)) return "Italian"
51+
return null
52+
}

0 commit comments

Comments
 (0)