agent-wiki 0.1.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/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Agent-Wiki
2
+
3
+ 卡帕西智能知识库 MCP 插件。一行安装,纯对话交互。
4
+
5
+ 帮你把收藏的文章自动整理成维基、打标签、生成摘要,还能基于知识库写有人味儿的原创文章。
6
+
7
+ ## 安装
8
+
9
+ **Claude Code:**
10
+ ```bash
11
+ claude mcp add agent-wiki -- npx agent-wiki
12
+ ```
13
+
14
+ **Cursor / Windsurf**(编辑 MCP 配置):
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "agent-wiki": {
19
+ "command": "npx",
20
+ "args": ["agent-wiki"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ 装完重启 Agent 就行,不需要别的配置。
27
+
28
+ ## 首次使用
29
+
30
+ 装完后打开 Agent,它会主动引导你:
31
+
32
+ ```
33
+ Agent:你好!我是 agent-wiki 知识库助手。
34
+ 我可以帮你搭一个个人知识库。还能基于你的知识库
35
+ 帮你写有人味儿的原创文章,适合直接发微信公众号。
36
+
37
+ 先设置一下:
38
+ 1. 你想把知识库放在哪个目录?
39
+ 2. 你主要关注什么方向?
40
+ ```
41
+
42
+ 你回答两个问题就建好了。
43
+
44
+ ## 日常使用
45
+
46
+ 纯对话,不需要记任何命令:
47
+
48
+ ```
49
+ 你:把 ~/Downloads/这篇文章.md 加到知识库
50
+ 你:整理一下知识库
51
+ 你:搜一下 RAG 相关的内容
52
+ 你:帮我写一篇 Agent 工程化的综述
53
+ 你:检查一下知识库健不健康
54
+ ```
55
+
56
+ ## 知识库结构
57
+
58
+ ```
59
+ 你的知识库目录/
60
+ ├── SCHEMA.md # 知识库说明书(Agent 自动读取)
61
+ ├── raw/ # 原始文章(不可变)
62
+ ├── wiki/ # AI 编译后的维基页面
63
+ │ ├── INDEX.md # 分类索引
64
+ │ ├── LOG.md # 操作日志
65
+ │ └── *.md # Wiki 页面
66
+ └── outputs/ # 原创文章(可直接发微信公众号)
67
+ ```
68
+
69
+ ## 工具列表
70
+
71
+ | 工具 | 功能 |
72
+ |------|------|
73
+ | wiki_status | 查询知识库状态,首次使用自动引导 |
74
+ | wiki_init | 初始化知识库(创建目录 + SCHEMA.md) |
75
+ | wiki_ingest | 摄入文章到 raw/ |
76
+ | wiki_tag | 写入标签到 frontmatter |
77
+ | wiki_compile | 生成 Wiki 页面 + 更新索引 + 记录日志 |
78
+ | wiki_search | 标签 + 全文搜索 |
79
+ | wiki_article | 保存原创文章到 outputs/ |
80
+ | wiki_lint | 自检死链接、缺摘要等 |
81
+
82
+ ## 写文章风格
83
+
84
+ 插件内置了去 AI 味规则,写出来的文章:
85
+ - 像跟朋友聊天,不像写论文
86
+ - 有自己的观点和态度
87
+ - 适合直接发微信公众号
88
+
89
+ ## 技术栈
90
+
91
+ - TypeScript + @modelcontextprotocol/sdk
92
+ - 零外部依赖(只用 Node.js 标准库)
93
+ - LLM 由宿主 Agent 提供,插件不碰 LLM
94
+ - MCP stdio 协议
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent-Wiki MCP Server
4
+ *
5
+ * 卡帕西智能知识库 MCP 插件
6
+ * 一行安装,纯对话交互,Agent 自动帮你管理知识库、写有人味儿的原创文章
7
+ *
8
+ * 零 LLM、零 HTTP、零外部依赖
9
+ * 所有智能工作由宿主 Agent 的 LLM 完成
10
+ * 本插件只负责文件操作(搬文件、读写元数据、搜索、生成模板)
11
+ */
12
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent-Wiki MCP Server
4
+ *
5
+ * 卡帕西智能知识库 MCP 插件
6
+ * 一行安装,纯对话交互,Agent 自动帮你管理知识库、写有人味儿的原创文章
7
+ *
8
+ * 零 LLM、零 HTTP、零外部依赖
9
+ * 所有智能工作由宿主 Agent 的 LLM 完成
10
+ * 本插件只负责文件操作(搬文件、读写元数据、搜索、生成模板)
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import { handleStatus } from "./tools/status.js";
16
+ import { handleInit } from "./tools/init.js";
17
+ import { handleIngest } from "./tools/ingest.js";
18
+ import { handleTag } from "./tools/tag.js";
19
+ import { handleCompile } from "./tools/compile.js";
20
+ import { handleSearch } from "./tools/search.js";
21
+ import { handleArticle } from "./tools/article.js";
22
+ import { handleLint } from "./tools/lint.js";
23
+ const server = new McpServer({
24
+ name: "agent-wiki",
25
+ version: "0.1.0",
26
+ });
27
+ // ─── 注册 8 个 MCP 工具 ───
28
+ /**
29
+ * wiki_status — 查询知识库状态
30
+ * 首次调用返回 initialized: false + 引导提示
31
+ * Agent 应根据 hint 主动引导用户完成初始化
32
+ */
33
+ server.tool("wiki_status", "查询知识库状态。首次调用如果返回 initialized: false,你应该主动引导用户:1. 问知识库目录 2. 问兴趣方向 3. 调用 wiki_init。不要等用户说'帮我建知识库',你主动开始。", {}, async () => handleStatus());
34
+ /**
35
+ * wiki_init — 初始化知识库
36
+ * 创建目录结构(raw/、wiki/、outputs/)
37
+ * 生成 SCHEMA.md(知识库说明书 + Agent 操作指南 + 写作规则)
38
+ */
39
+ server.tool("wiki_init", "初始化知识库。创建目录结构和 SCHEMA.md。需要用户提供:目录路径、关注主题、兴趣方向。", {
40
+ dir: z.string().describe("知识库根目录路径,如 ~/Documents/知识库"),
41
+ topics: z.array(z.string()).describe("关注主题列表,如 ['AI', '产品管理']"),
42
+ interests: z.array(z.string()).describe("兴趣方向列表,如 ['LLM', '创业', 'RAG']"),
43
+ }, async (params) => handleInit(params));
44
+ /**
45
+ * wiki_ingest — 摄入 MD 文件到知识库
46
+ * 从指定路径复制 MD 文件到 raw/(增量)
47
+ * 返回每个文件的前 500 字预览供 Agent 用 LLM 处理
48
+ */
49
+ server.tool("wiki_ingest", "摄入 MD 文件到知识库。从指定路径复制 .md 文件到 raw/ 目录(增量,已摄入的跳过)。返回新文件内容预览,供你用 LLM 提取标签和摘要。", {
50
+ source: z.string().describe("源文件路径或目录路径"),
51
+ force: z.boolean().optional().describe("强制重新摄入已存在的文件,默认 false"),
52
+ }, async (params) => handleIngest(params));
53
+ /**
54
+ * wiki_tag — 写入标签到文件 frontmatter
55
+ * Agent 用 LLM 提取标签后,调用此工具写入
56
+ */
57
+ server.tool("wiki_tag", "将标签写入文件的 frontmatter。你应该先用 LLM 为文件提取 5-10 个标签,然后调用此工具写入。", {
58
+ file: z.string().describe("raw/ 下的文件名"),
59
+ tags: z.array(z.string()).describe("标签列表,如 ['AI', 'RAG', '知识管理']"),
60
+ }, async (params) => handleTag(params));
61
+ /**
62
+ * wiki_compile — 生成 Wiki 页面
63
+ * 接收 Agent 用 LLM 生成的结构化内容,生成 Wiki 页面
64
+ * 自动更新 INDEX.md 和 LOG.md
65
+ */
66
+ server.tool("wiki_compile", "根据你用 LLM 生成的摘要、洞察、分类生成 Wiki 页面。同时更新 INDEX.md 和 LOG.md。你应该先读 SCHEMA.md 了解 Wiki 页面格式,用 LLM 生成内容后再调用。", {
67
+ file: z.string().describe("raw/ 下的源文件名"),
68
+ title: z.string().describe("Wiki 页面标题"),
69
+ summary: z.string().describe("2-3 句话的核心内容摘要"),
70
+ insights: z.array(z.string()).describe("3-5 个核心洞察"),
71
+ category: z.string().describe("分类名,如 'AI工程化'、'知识管理'"),
72
+ tags: z.array(z.string()).describe("5-10 个标签"),
73
+ source_file: z.string().describe("raw/ 下的源文件名(同 file 参数)"),
74
+ }, async (params) => handleCompile(params));
75
+ /**
76
+ * wiki_search — 搜索知识库
77
+ * 支持标签匹配 + 全文关键词搜索
78
+ */
79
+ server.tool("wiki_search", "搜索知识库。支持按标签和关键词搜索,返回匹配的 Wiki 页面列表。标签匹配权重 0.6 + 全文匹配权重 0.4。", {
80
+ tags: z.array(z.string()).optional().describe("按标签搜索"),
81
+ query: z.string().optional().describe("全文关键词搜索"),
82
+ match: z.enum(["any", "all"]).optional().describe("标签匹配模式:any=任一匹配,all=全部匹配。默认 any"),
83
+ top_k: z.number().optional().describe("返回结果数量,默认 10"),
84
+ }, async (params) => handleSearch(params));
85
+ /**
86
+ * wiki_article — 保存原创文章
87
+ * Agent 写好文章后调用此工具保存到 outputs/
88
+ * 写作前必须读 SCHEMA.md 的"写原创文章规则"
89
+ */
90
+ server.tool("wiki_article", "保存原创文章到 outputs/ 目录。保存前,你必须先读 SCHEMA.md 的'写原创文章规则',确保文章是有人味儿的微信公众号风格,没有 AI 味。", {
91
+ topic: z.string().describe("文章主题"),
92
+ content: z.string().describe("文章正文(Markdown 格式)"),
93
+ refs: z.array(z.string()).optional().describe("参考资料文件名列表"),
94
+ }, async (params) => handleArticle(params));
95
+ /**
96
+ * wiki_lint — 自检知识库健康状态
97
+ * 检测死链接、缺摘要、内容过短等问题
98
+ */
99
+ server.tool("wiki_lint", "自检知识库健康状态。检测死链接、缺失摘要、内容过短、缺少标签等问题,返回问题列表。", {
100
+ check_only: z.boolean().optional().describe("仅检测不修复,默认 true"),
101
+ }, async (params) => handleLint(params));
102
+ // ─── 启动 MCP Server ───
103
+ async function main() {
104
+ const transport = new StdioServerTransport();
105
+ await server.connect(transport);
106
+ }
107
+ main().catch((err) => {
108
+ console.error("Agent-Wiki MCP Server 启动失败:", err);
109
+ process.exit(1);
110
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 状态管理:全局配置 + 知识库状态
3
+ *
4
+ * 全局配置存放在 ~/.agent-wiki/config.json,记录知识库目录路径
5
+ * 知识库状态存放在 {wikiDir}/state.json,记录已摄入/已打标签/已编译的文件列表
6
+ */
7
+ /** 全局配置(~/.agent-wiki/config.json) */
8
+ export interface GlobalConfig {
9
+ /** 知识库根目录(绝对路径) */
10
+ wikiDir: string;
11
+ }
12
+ /** 知识库状态({wikiDir}/state.json) */
13
+ export interface WikiState {
14
+ /** 已摄入到 raw/ 的文件名列表 */
15
+ ingested: string[];
16
+ /** 已打标签的文件名列表 */
17
+ tagged: string[];
18
+ /** 已编译为 Wiki 页面的源文件名列表 */
19
+ compiled: string[];
20
+ }
21
+ /**
22
+ * 获取全局配置目录路径
23
+ */
24
+ export declare function getGlobalConfigDir(): string;
25
+ /**
26
+ * 读取全局配置,未初始化返回 null
27
+ */
28
+ export declare function getConfig(): GlobalConfig | null;
29
+ /**
30
+ * 保存全局配置
31
+ */
32
+ export declare function setConfig(config: GlobalConfig): void;
33
+ /**
34
+ * 检查是否已初始化(全局配置存在且 wikiDir 目录存在)
35
+ */
36
+ export declare function isInitialized(): boolean;
37
+ /**
38
+ * 获取知识库根目录
39
+ * 优先从全局配置读取,如果没有则返回 null
40
+ */
41
+ export declare function getWikiDir(): string | null;
42
+ /**
43
+ * 获取 state.json 文件路径
44
+ */
45
+ export declare function getStatePath(wikiDir: string): string;
46
+ /**
47
+ * 读取知识库状态,state.json 不存在则返回空状态
48
+ */
49
+ export declare function getState(wikiDir: string): WikiState;
50
+ /**
51
+ * 保存知识库状态
52
+ */
53
+ export declare function setState(wikiDir: string, state: WikiState): void;
54
+ export declare function getRawDir(wikiDir: string): string;
55
+ export declare function getWikiPagesDir(wikiDir: string): string;
56
+ export declare function getOutputsDir(wikiDir: string): string;
package/dist/state.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 状态管理:全局配置 + 知识库状态
3
+ *
4
+ * 全局配置存放在 ~/.agent-wiki/config.json,记录知识库目录路径
5
+ * 知识库状态存放在 {wikiDir}/state.json,记录已摄入/已打标签/已编译的文件列表
6
+ */
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ import * as fs from "fs";
10
+ import { ensureDir, readJSON, writeJSON } from "./utils.js";
11
+ // ─── 路径常量 ───
12
+ const GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".agent-wiki");
13
+ const GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, "config.json");
14
+ // ─── 全局配置操作 ───
15
+ /**
16
+ * 获取全局配置目录路径
17
+ */
18
+ export function getGlobalConfigDir() {
19
+ return GLOBAL_CONFIG_DIR;
20
+ }
21
+ /**
22
+ * 读取全局配置,未初始化返回 null
23
+ */
24
+ export function getConfig() {
25
+ return readJSON(GLOBAL_CONFIG_PATH);
26
+ }
27
+ /**
28
+ * 保存全局配置
29
+ */
30
+ export function setConfig(config) {
31
+ ensureDir(GLOBAL_CONFIG_DIR);
32
+ writeJSON(GLOBAL_CONFIG_PATH, config);
33
+ }
34
+ /**
35
+ * 检查是否已初始化(全局配置存在且 wikiDir 目录存在)
36
+ */
37
+ export function isInitialized() {
38
+ const config = getConfig();
39
+ if (!config)
40
+ return false;
41
+ return fs.existsSync(config.wikiDir);
42
+ }
43
+ // ─── 知识库状态操作 ───
44
+ /**
45
+ * 获取知识库根目录
46
+ * 优先从全局配置读取,如果没有则返回 null
47
+ */
48
+ export function getWikiDir() {
49
+ const config = getConfig();
50
+ return config?.wikiDir ?? null;
51
+ }
52
+ /**
53
+ * 获取 state.json 文件路径
54
+ */
55
+ export function getStatePath(wikiDir) {
56
+ return path.join(wikiDir, "state.json");
57
+ }
58
+ /**
59
+ * 读取知识库状态,state.json 不存在则返回空状态
60
+ */
61
+ export function getState(wikiDir) {
62
+ const state = readJSON(getStatePath(wikiDir));
63
+ return state ?? { ingested: [], tagged: [], compiled: [] };
64
+ }
65
+ /**
66
+ * 保存知识库状态
67
+ */
68
+ export function setState(wikiDir, state) {
69
+ writeJSON(getStatePath(wikiDir), state);
70
+ }
71
+ // ─── 知识库子目录路径 ───
72
+ export function getRawDir(wikiDir) {
73
+ return path.join(wikiDir, "raw");
74
+ }
75
+ export function getWikiPagesDir(wikiDir) {
76
+ return path.join(wikiDir, "wiki");
77
+ }
78
+ export function getOutputsDir(wikiDir) {
79
+ return path.join(wikiDir, "outputs");
80
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * wiki_article — 保存 Agent 写好的原创文章到 outputs/
3
+ *
4
+ * 文件名格式:YYYYMMDD-主题.md
5
+ * 自动添加 frontmatter(title, date, tags)
6
+ * 追加 LOG.md 记录
7
+ */
8
+ import { ToolResult } from "../types.js";
9
+ export declare function handleArticle(params: {
10
+ topic: string;
11
+ content: string;
12
+ refs?: string[];
13
+ }): Promise<ToolResult>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * wiki_article — 保存 Agent 写好的原创文章到 outputs/
3
+ *
4
+ * 文件名格式:YYYYMMDD-主题.md
5
+ * 自动添加 frontmatter(title, date, tags)
6
+ * 追加 LOG.md 记录
7
+ */
8
+ import * as path from "path";
9
+ import * as fs from "fs";
10
+ import { getWikiDir, getOutputsDir, getWikiPagesDir } from "../state.js";
11
+ import { safeFilename, todayStr, dateCompact, nowStr, ensureDir } from "../utils.js";
12
+ import { textResult } from "../types.js";
13
+ export async function handleArticle(params) {
14
+ const wikiDir = getWikiDir();
15
+ if (!wikiDir) {
16
+ return textResult(JSON.stringify({ error: "知识库未初始化" }));
17
+ }
18
+ const { topic, content, refs = [] } = params;
19
+ const outputsDir = getOutputsDir(wikiDir);
20
+ ensureDir(outputsDir);
21
+ // 生成文件名:YYYYMMDD-主题.md
22
+ const fileName = `${dateCompact()}-${safeFilename(topic)}.md`;
23
+ const filePath = path.join(outputsDir, fileName);
24
+ // 添加 frontmatter
25
+ const refsStr = refs.length > 0 ? `\nrefs:\n${refs.map((r) => ` - "[[${r}]]"`).join("\n")}` : "";
26
+ const fullContent = `---
27
+ title: "${topic}"
28
+ date: ${todayStr()}
29
+ type: article${refsStr}
30
+ ---
31
+
32
+ ${content}
33
+ `;
34
+ fs.writeFileSync(filePath, fullContent, "utf-8");
35
+ // 追加 LOG.md
36
+ const logPath = path.join(getWikiPagesDir(wikiDir), "LOG.md");
37
+ const logEntry = `\n## [${nowStr()}] article | ${topic}\n输出文件: outputs/${fileName}${refs.length > 0 ? ` | 参考资料: ${refs.join(", ")}` : ""}\n`;
38
+ if (fs.existsSync(logPath)) {
39
+ fs.appendFileSync(logPath, logEntry, "utf-8");
40
+ }
41
+ return textResult(JSON.stringify({
42
+ status: "ok",
43
+ file: `outputs/${fileName}`,
44
+ topic,
45
+ }));
46
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * wiki_compile — 根据 Agent 传入的内容生成 Wiki 页面
3
+ *
4
+ * 接收 Agent 用 LLM 生成的结构化内容(摘要、洞察、分类、标签)
5
+ * 生成 Wiki 页面 MD 文件
6
+ * 更新 wiki/INDEX.md(按分类分组)
7
+ * 追加 wiki/LOG.md(append-only)
8
+ * 更新 state.json
9
+ */
10
+ import { ToolResult } from "../types.js";
11
+ export declare function handleCompile(params: {
12
+ file: string;
13
+ title: string;
14
+ summary: string;
15
+ insights: string[];
16
+ category: string;
17
+ tags: string[];
18
+ source_file: string;
19
+ }): Promise<ToolResult>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * wiki_compile — 根据 Agent 传入的内容生成 Wiki 页面
3
+ *
4
+ * 接收 Agent 用 LLM 生成的结构化内容(摘要、洞察、分类、标签)
5
+ * 生成 Wiki 页面 MD 文件
6
+ * 更新 wiki/INDEX.md(按分类分组)
7
+ * 追加 wiki/LOG.md(append-only)
8
+ * 更新 state.json
9
+ */
10
+ import * as path from "path";
11
+ import * as fs from "fs";
12
+ import { getWikiDir, getWikiPagesDir, getState, setState } from "../state.js";
13
+ import { safeFilename, todayStr, nowStr } from "../utils.js";
14
+ import { textResult } from "../types.js";
15
+ export async function handleCompile(params) {
16
+ const wikiDir = getWikiDir();
17
+ if (!wikiDir) {
18
+ return textResult(JSON.stringify({ error: "知识库未初始化" }));
19
+ }
20
+ const { file, title, summary, insights, category, tags, source_file } = params;
21
+ const wikiPagesDir = getWikiPagesDir(wikiDir);
22
+ // 生成 Wiki 页面文件名
23
+ const wikiFileName = safeFilename(title) + ".md";
24
+ const wikiFilePath = path.join(wikiPagesDir, wikiFileName);
25
+ // 生成 Wiki 页面内容
26
+ const wikiContent = `---
27
+ title: "${title}"
28
+ date: ${todayStr()}
29
+ tags: [${tags.join(", ")}]
30
+ category: ${category}
31
+ sources:
32
+ - "[[raw/${source_file}]]"
33
+ ---
34
+
35
+ # ${title}
36
+
37
+ ## 摘要
38
+ ${summary}
39
+
40
+ ## 核心洞察
41
+ ${insights.map((i) => `- ${i}`).join("\n")}
42
+
43
+ ## 标签
44
+ ${tags.join(", ")}
45
+
46
+ ---
47
+ *来源: raw/${source_file} · 编译时间: ${todayStr()} ${nowStr().split(" ")[1] || ""}*
48
+ `;
49
+ // 写入 Wiki 页面
50
+ fs.writeFileSync(wikiFilePath, wikiContent, "utf-8");
51
+ // 更新 INDEX.md
52
+ const indexPath = path.join(wikiPagesDir, "INDEX.md");
53
+ updateIndex(indexPath, title, category, tags.slice(0, 3), wikiFileName);
54
+ // 追加 LOG.md
55
+ const logPath = path.join(wikiPagesDir, "LOG.md");
56
+ appendLog(logPath, "compile", title, source_file, category, tags);
57
+ // 更新状态
58
+ const state = getState(wikiDir);
59
+ if (!state.compiled.includes(file)) {
60
+ state.compiled.push(file);
61
+ setState(wikiDir, state);
62
+ }
63
+ return textResult(JSON.stringify({
64
+ status: "ok",
65
+ wiki_file: wikiFileName,
66
+ title,
67
+ index_updated: true,
68
+ log_appended: true,
69
+ }));
70
+ }
71
+ /**
72
+ * 更新 INDEX.md:按分类分组,每个条目一行描述
73
+ */
74
+ function updateIndex(indexPath, title, category, previewTags, wikiFileName) {
75
+ let content = "";
76
+ if (fs.existsSync(indexPath)) {
77
+ content = fs.readFileSync(indexPath, "utf-8");
78
+ }
79
+ // 解析已有索引,按分类收集条目
80
+ const sections = {};
81
+ const lines = content.split("\n");
82
+ let currentCategory = "";
83
+ for (const line of lines) {
84
+ if (line.startsWith("## ") && !line.includes("知识库索引")) {
85
+ currentCategory = line.replace("## ", "").trim();
86
+ if (!sections[currentCategory])
87
+ sections[currentCategory] = [];
88
+ }
89
+ else if (line.startsWith("- [[") && currentCategory) {
90
+ // 解析:- [[页面标题]] (标签1, 标签2)
91
+ const linkMatch = line.match(/- \[\[(.+?)\]\]\s*\((.+?)\)/);
92
+ if (linkMatch) {
93
+ if (!sections[currentCategory])
94
+ sections[currentCategory] = [];
95
+ sections[currentCategory].push({
96
+ title: linkMatch[1],
97
+ tags: linkMatch[2].split(",").map((t) => t.trim()),
98
+ file: "",
99
+ });
100
+ }
101
+ }
102
+ }
103
+ // 添加新条目
104
+ if (!sections[category])
105
+ sections[category] = [];
106
+ // 去重
107
+ const existing = sections[category].find((e) => e.title === title);
108
+ if (!existing) {
109
+ sections[category].push({ title, tags: previewTags, file: wikiFileName });
110
+ }
111
+ // 重新生成 INDEX.md
112
+ const totalEntries = Object.values(sections).reduce((s, e) => s + e.length, 0);
113
+ const newLines = [
114
+ "---",
115
+ `title: 知识库索引`,
116
+ `date: ${todayStr()}`,
117
+ "---",
118
+ "",
119
+ "# 知识库索引",
120
+ "",
121
+ ];
122
+ for (const [cat, entries] of Object.entries(sections)) {
123
+ if (entries.length === 0)
124
+ continue;
125
+ newLines.push(`## ${cat}`);
126
+ for (const entry of entries) {
127
+ newLines.push(`- [[${entry.title}]] (${entry.tags.join(", ")})`);
128
+ }
129
+ newLines.push("");
130
+ }
131
+ newLines.push(`---\n*共 ${totalEntries} 篇 · 更新于 ${todayStr()}*`);
132
+ fs.writeFileSync(indexPath, newLines.join("\n"), "utf-8");
133
+ }
134
+ /**
135
+ * 追加操作日志到 LOG.md(append-only)
136
+ */
137
+ function appendLog(logPath, action, title, sourceFile, category, tags) {
138
+ const logEntry = `\n## [${nowStr()}] ${action} | ${title}\n来源: ${sourceFile} | 分类: ${category} | 标签: ${tags.join(", ")}\n`;
139
+ if (fs.existsSync(logPath)) {
140
+ fs.appendFileSync(logPath, logEntry, "utf-8");
141
+ }
142
+ else {
143
+ fs.writeFileSync(logPath, `---\ntitle: Wiki 操作日志\ntags: [log, wiki]\n---\n\n# Wiki Log\n\n> Append-only 时间线,记录 wiki 的每次变更。\n${logEntry}`, "utf-8");
144
+ }
145
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * wiki_ingest — 从指定路径复制 MD 文件到 raw/(增量)
3
+ *
4
+ * source 可以是单个文件或目录
5
+ * 已摄入的文件跳过(除非 force=true)
6
+ * 返回每个文件的名称和前 500 字预览(供 Agent 用 LLM 处理)
7
+ */
8
+ import { ToolResult } from "../types.js";
9
+ export declare function handleIngest(params: {
10
+ source: string;
11
+ force?: boolean;
12
+ }): Promise<ToolResult>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * wiki_ingest — 从指定路径复制 MD 文件到 raw/(增量)
3
+ *
4
+ * source 可以是单个文件或目录
5
+ * 已摄入的文件跳过(除非 force=true)
6
+ * 返回每个文件的名称和前 500 字预览(供 Agent 用 LLM 处理)
7
+ */
8
+ import * as path from "path";
9
+ import * as fs from "fs";
10
+ import { getWikiDir, getRawDir, getState, setState } from "../state.js";
11
+ import { readFile_safe, ensureDir } from "../utils.js";
12
+ import { textResult } from "../types.js";
13
+ export async function handleIngest(params) {
14
+ const wikiDir = getWikiDir();
15
+ if (!wikiDir) {
16
+ return textResult(JSON.stringify({ error: "知识库未初始化,请先调用 wiki_init" }));
17
+ }
18
+ const rawDir = getRawDir(wikiDir);
19
+ ensureDir(rawDir);
20
+ const state = getState(wikiDir);
21
+ const source = params.source.replace(/^~/, process.env.HOME || "~");
22
+ const force = params.force ?? false;
23
+ // 收集要摄入的文件列表
24
+ let filesToIngest = [];
25
+ if (fs.statSync(source).isDirectory()) {
26
+ filesToIngest = fs
27
+ .readdirSync(source)
28
+ .filter((f) => f.endsWith(".md"))
29
+ .map((f) => path.join(source, f));
30
+ }
31
+ else if (source.endsWith(".md")) {
32
+ filesToIngest = [source];
33
+ }
34
+ else {
35
+ return textResult(JSON.stringify({ error: "source 必须是 .md 文件或包含 .md 文件的目录" }));
36
+ }
37
+ // 增量过滤
38
+ const results = [];
39
+ let ingested = 0;
40
+ for (const filePath of filesToIngest) {
41
+ const fileName = path.basename(filePath);
42
+ const destPath = path.join(rawDir, fileName);
43
+ // 已摄入则跳过(除非 force)
44
+ if (!force && state.ingested.includes(fileName)) {
45
+ results.push({ name: fileName, content_preview: "(已摄入,跳过)", skipped: true });
46
+ continue;
47
+ }
48
+ // 复制到 raw/
49
+ const content = readFile_safe(filePath);
50
+ if (!content) {
51
+ results.push({ name: fileName, content_preview: "(无法读取文件)", skipped: true });
52
+ continue;
53
+ }
54
+ fs.writeFileSync(destPath, content, "utf-8");
55
+ // 更新状态
56
+ if (!state.ingested.includes(fileName)) {
57
+ state.ingested.push(fileName);
58
+ }
59
+ results.push({
60
+ name: fileName,
61
+ content_preview: content.substring(0, 500),
62
+ });
63
+ ingested++;
64
+ }
65
+ // 保存状态
66
+ setState(wikiDir, state);
67
+ return textResult(JSON.stringify({
68
+ status: "ok",
69
+ ingested,
70
+ total: filesToIngest.length,
71
+ files: results,
72
+ }));
73
+ }