@ppdocs/mcp 2.7.2 → 2.8.1

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.
@@ -4,29 +4,26 @@
4
4
  *
5
5
  * API URL 格式: http://localhost:20001/api/:projectId/:password/...
6
6
  */
7
- import type { NodeData, Task, TaskSummary, TaskLogType, TaskExperience } from './types.js';
8
- export interface ListNodesFilter {
9
- status?: 'incomplete' | 'complete' | 'fixing' | 'refactoring' | 'deprecated';
10
- minEdges?: number;
11
- maxEdges?: number;
7
+ import type { DocData, DocNode, DocSearchResult, Task, TaskSummary, TaskLogType, TaskExperience } from './types.js';
8
+ interface TreeNode {
9
+ path: string;
10
+ name: string;
11
+ summary?: string;
12
+ isDir: boolean;
13
+ children?: TreeNode[];
12
14
  }
13
15
  export declare class PpdocsApiClient {
14
16
  private baseUrl;
15
17
  constructor(apiUrl: string);
16
18
  private request;
17
- listNodes(filter?: ListNodesFilter): Promise<NodeData[]>;
18
- getNode(nodeId: string): Promise<NodeData | null>;
19
- createNode(node: Partial<NodeData>): Promise<NodeData>;
20
- updateNode(nodeId: string, updates: Partial<NodeData>): Promise<NodeData | null>;
21
- updateRoot(updates: {
22
- title?: string;
23
- description?: string;
24
- userStyles?: string[];
25
- testRules?: string[];
26
- reviewRules?: string[];
27
- codeStyle?: string[];
28
- }): Promise<NodeData | null>;
29
- deleteNode(nodeId: string): Promise<boolean>;
19
+ listDocs(): Promise<DocNode[]>;
20
+ getDoc(docPath: string): Promise<DocData | null>;
21
+ createDoc(docPath: string, doc: Partial<DocData>): Promise<DocData>;
22
+ updateDoc(docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
23
+ deleteDoc(docPath: string): Promise<boolean>;
24
+ searchDocs(keywords: string[], limit?: number): Promise<DocSearchResult[]>;
25
+ /** 获取目录树结构 */
26
+ getTree(): Promise<TreeNode[]>;
30
27
  getRulesApi(ruleType: string): Promise<string[]>;
31
28
  saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
32
29
  listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
@@ -40,19 +37,13 @@ export declare class PpdocsApiClient {
40
37
  completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
41
38
  }
42
39
  export declare function initClient(apiUrl: string): void;
43
- export declare function listNodes(_projectId: string, filter?: ListNodesFilter): Promise<NodeData[]>;
44
- export declare function getNode(_projectId: string, nodeId: string): Promise<NodeData | null>;
45
- export declare function createNode(_projectId: string, node: Partial<NodeData>): Promise<NodeData>;
46
- export declare function updateNode(_projectId: string, nodeId: string, updates: Partial<NodeData>): Promise<NodeData | null>;
47
- export declare function updateRoot(_projectId: string, updates: {
48
- title?: string;
49
- description?: string;
50
- userStyles?: string[];
51
- testRules?: string[];
52
- reviewRules?: string[];
53
- codeStyle?: string[];
54
- }): Promise<NodeData | null>;
55
- export declare function deleteNode(_projectId: string, nodeId: string): Promise<boolean>;
40
+ export declare function listDocs(_projectId: string): Promise<DocNode[]>;
41
+ export declare function getDoc(_projectId: string, docPath: string): Promise<DocData | null>;
42
+ export declare function createDoc(_projectId: string, docPath: string, doc: Partial<DocData>): Promise<DocData>;
43
+ export declare function updateDoc(_projectId: string, docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
44
+ export declare function deleteDoc(_projectId: string, docPath: string): Promise<boolean>;
45
+ export declare function searchDocs(_projectId: string, keywords: string[], limit?: number): Promise<DocSearchResult[]>;
46
+ export declare function getTree(_projectId: string): Promise<TreeNode[]>;
56
47
  export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
57
48
  export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
58
49
  export declare function listTasks(_projectId: string, status?: 'active' | 'archived'): Promise<TaskSummary[]>;
@@ -64,3 +55,4 @@ export declare function createTask(_projectId: string, task: {
64
55
  }, creator: string): Promise<Task>;
65
56
  export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
66
57
  export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
58
+ export type { TreeNode };
@@ -4,58 +4,38 @@
4
4
  *
5
5
  * API URL 格式: http://localhost:20001/api/:projectId/:password/...
6
6
  */
7
- // ============ 智能定位算法 ============
8
- const SMART_NODE_GAP = 180;
9
- const SMART_SEARCH_RADIUS = 10;
10
- /**
11
- * 计算新节点的智能位置 (关联中心 + 螺旋搜索空位)
12
- */
13
- function computeSmartPosition(deps, nodes) {
14
- // 1. 找关联节点
15
- const related = [];
16
- if (deps && deps.length > 0) {
17
- const depNames = deps.map(d => d.name.toLowerCase());
18
- nodes.forEach(n => {
19
- if (n.signature && depNames.some(name => n.signature.toLowerCase().startsWith(name))) {
20
- related.push(n);
21
- }
22
- });
23
- }
24
- // 2. 计算中心点
25
- let cx, cy;
26
- if (related.length > 0) {
27
- cx = related.reduce((s, n) => s + n.x, 0) / related.length;
28
- cy = related.reduce((s, n) => s + n.y, 0) / related.length + SMART_NODE_GAP;
29
- }
30
- else {
31
- const root = nodes.find(n => n.isOrigin);
32
- cx = root?.x ?? 0;
33
- cy = (root?.y ?? 0) + SMART_NODE_GAP;
34
- }
35
- // 3. 碰撞检测
36
- const collides = (x, y) => nodes.some(n => Math.abs(n.x - x) < SMART_NODE_GAP && Math.abs(n.y - y) < SMART_NODE_GAP * 0.6);
37
- if (!collides(cx, cy))
38
- return { x: Math.round(cx), y: Math.round(cy) };
39
- // 4. 螺旋搜索
40
- const dirs = [[1, 0], [0, 1], [-1, 0], [0, -1]];
41
- let x = cx, y = cy, step = 1, dir = 0, steps = 0, turns = 0;
42
- for (let i = 0; i < SMART_SEARCH_RADIUS * SMART_SEARCH_RADIUS * 4; i++) {
43
- x += dirs[dir][0] * SMART_NODE_GAP;
44
- y += dirs[dir][1] * SMART_NODE_GAP * 0.6;
45
- steps++;
46
- if (!collides(x, y))
47
- return { x: Math.round(x), y: Math.round(y) };
48
- if (steps >= step) {
49
- steps = 0;
50
- dir = (dir + 1) % 4;
51
- if (++turns >= 2) {
52
- turns = 0;
53
- step++;
54
- }
7
+ /** 从扁平文档列表构建目录树 */
8
+ function buildTree(docs) {
9
+ const root = [];
10
+ const pathMap = new Map();
11
+ // 按路径深度排序,确保父目录先处理
12
+ const sorted = [...docs].sort((a, b) => {
13
+ const depthA = (a.path.match(/\//g) || []).length;
14
+ const depthB = (b.path.match(/\//g) || []).length;
15
+ return depthA - depthB;
16
+ });
17
+ for (const doc of sorted) {
18
+ const node = {
19
+ path: doc.path,
20
+ name: doc.name,
21
+ summary: doc.summary,
22
+ isDir: doc.isDir,
23
+ children: doc.isDir ? [] : undefined
24
+ };
25
+ // 找父路径
26
+ const lastSlash = doc.path.lastIndexOf('/');
27
+ const parentPath = lastSlash > 0 ? doc.path.substring(0, lastSlash) : '';
28
+ if (parentPath && pathMap.has(parentPath)) {
29
+ pathMap.get(parentPath).children.push(node);
30
+ }
31
+ else {
32
+ root.push(node);
33
+ }
34
+ if (doc.isDir) {
35
+ pathMap.set(doc.path, node);
55
36
  }
56
37
  }
57
- const maxX = Math.max(...nodes.map(n => n.x), 0);
58
- return { x: Math.round(maxX + SMART_NODE_GAP), y: Math.round(cy) };
38
+ return root;
59
39
  }
60
40
  // API 客户端类
61
41
  export class PpdocsApiClient {
@@ -98,91 +78,51 @@ export class PpdocsApiClient {
98
78
  clearTimeout(timeout);
99
79
  }
100
80
  }
101
- // ============ 节点操作 ============
102
- async listNodes(filter) {
103
- const nodes = await this.request('/nodes');
104
- if (!filter)
105
- return nodes;
106
- // 计算每个节点的连接数 (入边+出边)
107
- const edgeCounts = new Map();
108
- nodes.forEach(n => edgeCounts.set(n.id, 0));
109
- nodes.forEach(n => {
110
- (n.dependencies || []).forEach(dep => {
111
- // 出边: 当前节点依赖别人
112
- edgeCounts.set(n.id, (edgeCounts.get(n.id) || 0) + 1);
113
- // 入边: 被依赖的节点 (按 signature 匹配)
114
- const target = nodes.find(t => t.signature?.toLowerCase() === dep.name.toLowerCase());
115
- if (target)
116
- edgeCounts.set(target.id, (edgeCounts.get(target.id) || 0) + 1);
117
- });
118
- });
119
- // 过滤
120
- return nodes.filter(n => {
121
- if (filter.status && n.status !== filter.status)
122
- return false;
123
- const edges = edgeCounts.get(n.id) || 0;
124
- if (filter.minEdges !== undefined && edges < filter.minEdges)
125
- return false;
126
- if (filter.maxEdges !== undefined && edges > filter.maxEdges)
127
- return false;
128
- return true;
129
- });
81
+ // ============ 文档操作 ============
82
+ async listDocs() {
83
+ return this.request('/docs');
130
84
  }
131
- async getNode(nodeId) {
85
+ async getDoc(docPath) {
132
86
  try {
133
- return await this.request(`/nodes/${nodeId}`);
87
+ // 移除开头的斜杠,API 路径会自动处理
88
+ const cleanPath = docPath.replace(/^\//, '');
89
+ return await this.request(`/docs/${cleanPath}`);
134
90
  }
135
91
  catch {
136
92
  return null;
137
93
  }
138
94
  }
139
- async createNode(node) {
140
- // 智能定位: 计算新节点位置
141
- let x = node.x ?? 0;
142
- let y = node.y ?? 0;
143
- // 如果未指定位置,自动计算
144
- if (node.x === undefined && node.y === undefined) {
145
- const existingNodes = await this.listNodes();
146
- const pos = computeSmartPosition(node.dependencies, existingNodes);
147
- x = pos.x;
148
- y = pos.y;
149
- }
95
+ async createDoc(docPath, doc) {
96
+ const cleanPath = docPath.replace(/^\//, '');
150
97
  const payload = {
151
- id: '', // 服务端自动生成
152
- title: node.title || '',
153
- type: node.type || 'logic',
154
- status: node.status || 'incomplete',
155
- x,
156
- y,
157
- locked: false,
158
- signature: node.signature || node.title || '',
159
- categories: node.categories || [],
160
- description: node.description || '',
161
- dependencies: node.dependencies || [],
162
- relatedFiles: node.relatedFiles || [],
163
- createdAt: new Date().toISOString(),
164
- updatedAt: new Date().toISOString(),
165
- lastAccessedAt: new Date().toISOString(),
166
- versions: [],
167
- bugfixes: []
98
+ summary: doc.summary || '',
99
+ content: doc.content || '',
100
+ versions: doc.versions || [{
101
+ version: 0.1,
102
+ date: new Date().toISOString(),
103
+ changes: '初始创建'
104
+ }],
105
+ bugfixes: doc.bugfixes || []
168
106
  };
169
- return this.request('/nodes', {
107
+ return this.request(`/docs/${cleanPath}`, {
170
108
  method: 'POST',
171
109
  body: JSON.stringify(payload)
172
110
  });
173
111
  }
174
- async updateNode(nodeId, updates) {
175
- // 先获取现有节点,合并更新
176
- const existing = await this.getNode(nodeId);
177
- if (!existing)
178
- return null;
179
- const payload = {
180
- ...existing,
181
- ...updates,
182
- updatedAt: new Date().toISOString()
183
- };
112
+ async updateDoc(docPath, updates) {
184
113
  try {
185
- return await this.request(`/nodes/${nodeId}`, {
114
+ const cleanPath = docPath.replace(/^\//, '');
115
+ // 先获取现有文档
116
+ const existing = await this.getDoc(docPath);
117
+ if (!existing)
118
+ return null;
119
+ const payload = {
120
+ summary: updates.summary ?? existing.summary,
121
+ content: updates.content ?? existing.content,
122
+ versions: updates.versions ?? existing.versions,
123
+ bugfixes: updates.bugfixes ?? existing.bugfixes
124
+ };
125
+ return await this.request(`/docs/${cleanPath}`, {
186
126
  method: 'PUT',
187
127
  body: JSON.stringify(payload)
188
128
  });
@@ -191,47 +131,28 @@ export class PpdocsApiClient {
191
131
  return null;
192
132
  }
193
133
  }
194
- async updateRoot(updates) {
195
- // 专用根节点更新,支持所有规则字段
196
- const root = await this.getNode('root');
197
- if (!root)
198
- return null;
199
- if (root.locked)
200
- return null; // 锁定时拒绝
201
- // 构建更新载荷 (只传入有值的字段)
202
- const payload = { updatedAt: new Date().toISOString() };
203
- if (updates.title !== undefined)
204
- payload.title = updates.title;
205
- if (updates.description !== undefined)
206
- payload.description = updates.description;
207
- if (updates.userStyles !== undefined)
208
- payload.userStyles = updates.userStyles;
209
- if (updates.testRules !== undefined)
210
- payload.testRules = updates.testRules;
211
- if (updates.reviewRules !== undefined)
212
- payload.reviewRules = updates.reviewRules;
213
- if (updates.codeStyle !== undefined)
214
- payload.codeStyle = updates.codeStyle;
215
- try {
216
- return await this.request('/nodes/root', {
217
- method: 'PUT',
218
- body: JSON.stringify({ ...root, ...payload })
219
- });
220
- }
221
- catch {
222
- return null;
223
- }
224
- }
225
- async deleteNode(nodeId) {
134
+ async deleteDoc(docPath) {
226
135
  try {
227
- await this.request(`/nodes/${nodeId}`, { method: 'DELETE' });
136
+ const cleanPath = docPath.replace(/^\//, '');
137
+ await this.request(`/docs/${cleanPath}`, { method: 'DELETE' });
228
138
  return true;
229
139
  }
230
140
  catch {
231
141
  return false;
232
142
  }
233
143
  }
234
- // ============ 规则 API (独立文件存储) ============
144
+ async searchDocs(keywords, limit = 20) {
145
+ return this.request('/docs/search', {
146
+ method: 'POST',
147
+ body: JSON.stringify({ keywords, limit })
148
+ });
149
+ }
150
+ /** 获取目录树结构 */
151
+ async getTree() {
152
+ const docs = await this.listDocs();
153
+ return buildTree(docs);
154
+ }
155
+ // ============ 规则 API ============
235
156
  async getRulesApi(ruleType) {
236
157
  try {
237
158
  return await this.request(`/rules/${ruleType}`);
@@ -303,7 +224,7 @@ export class PpdocsApiClient {
303
224
  }
304
225
  }
305
226
  }
306
- // ============ 模块级 API (兼容现有 tools/index.ts) ============
227
+ // ============ 模块级 API ============
307
228
  let client = null;
308
229
  export function initClient(apiUrl) {
309
230
  client = new PpdocsApiClient(apiUrl);
@@ -314,26 +235,29 @@ function getClient() {
314
235
  }
315
236
  return client;
316
237
  }
317
- // 导出与 fileStorage 相同的函数签名
318
- export async function listNodes(_projectId, filter) {
319
- return getClient().listNodes(filter);
238
+ // ============ 文档管理 ============
239
+ export async function listDocs(_projectId) {
240
+ return getClient().listDocs();
241
+ }
242
+ export async function getDoc(_projectId, docPath) {
243
+ return getClient().getDoc(docPath);
320
244
  }
321
- export async function getNode(_projectId, nodeId) {
322
- return getClient().getNode(nodeId);
245
+ export async function createDoc(_projectId, docPath, doc) {
246
+ return getClient().createDoc(docPath, doc);
323
247
  }
324
- export async function createNode(_projectId, node) {
325
- return getClient().createNode(node);
248
+ export async function updateDoc(_projectId, docPath, updates) {
249
+ return getClient().updateDoc(docPath, updates);
326
250
  }
327
- export async function updateNode(_projectId, nodeId, updates) {
328
- return getClient().updateNode(nodeId, updates);
251
+ export async function deleteDoc(_projectId, docPath) {
252
+ return getClient().deleteDoc(docPath);
329
253
  }
330
- export async function updateRoot(_projectId, updates) {
331
- return getClient().updateRoot(updates);
254
+ export async function searchDocs(_projectId, keywords, limit) {
255
+ return getClient().searchDocs(keywords, limit);
332
256
  }
333
- export async function deleteNode(_projectId, nodeId) {
334
- return getClient().deleteNode(nodeId);
257
+ export async function getTree(_projectId) {
258
+ return getClient().getTree();
335
259
  }
336
- // ============ 规则管理 (独立文件存储) ============
260
+ // ============ 规则管理 ============
337
261
  export async function getRules(_projectId, ruleType) {
338
262
  return getClient().getRulesApi(ruleType);
339
263
  }
@@ -1,80 +1,48 @@
1
- export type EdgeType = 'contains' | 'uses' | 'extends' | 'implements';
2
- export interface Edge {
3
- source: string;
4
- target: string;
5
- type: EdgeType;
6
- auto?: boolean;
7
- }
8
- export type NodeType = 'logic' | 'data' | 'intro';
9
- export type NodeStatus = 'incomplete' | 'complete' | 'fixing' | 'refactoring' | 'deprecated';
10
- export interface DataRef {
11
- type: string;
12
- description: string;
13
- formatPath?: string;
14
- }
15
- export interface Dependency {
16
- name: string;
17
- description: string;
18
- nodePath?: string;
19
- }
1
+ /** 版本记录 */
20
2
  export interface VersionRecord {
21
3
  version: number;
22
4
  date: string;
23
5
  changes: string;
24
6
  }
7
+ /** 错误修复记录 */
25
8
  export interface BugfixRecord {
26
- id: string;
27
9
  date: string;
28
10
  issue: string;
29
11
  solution: string;
30
- impact?: string;
31
12
  }
32
- export interface NodeData {
33
- id: string;
34
- title: string;
35
- type: NodeType;
36
- status: NodeStatus;
37
- x: number;
38
- y: number;
39
- locked: boolean;
40
- isOrigin?: boolean;
41
- signature: string;
42
- categories: string[];
13
+ /** 文档数据 (4个核心字段) */
14
+ export interface DocData {
15
+ summary: string;
16
+ content: string;
17
+ versions: VersionRecord[];
18
+ bugfixes: BugfixRecord[];
19
+ }
20
+ /** 文档节点 (含路径信息,用于列表展示) */
21
+ export interface DocNode {
43
22
  path: string;
44
- description: string;
45
- summary?: string;
46
- dataInput?: DataRef;
47
- dataOutput?: DataRef;
48
- dependencies: Dependency[];
49
- relatedFiles?: string[];
50
- userStyles?: string[];
51
- testRules?: string[];
52
- reviewRules?: string[];
53
- codeStyle?: string[];
54
- unitTests?: string[];
55
- createdAt?: string;
56
- updatedAt?: string;
57
- lastAccessedAt?: string;
58
- lastSyncAt?: string;
59
- versions?: VersionRecord[];
60
- bugfixes?: BugfixRecord[];
23
+ name: string;
24
+ summary: string;
25
+ isDir: boolean;
26
+ }
27
+ /** 文档搜索结果 */
28
+ export interface DocSearchResult {
29
+ path: string;
30
+ name: string;
31
+ summary: string;
32
+ score: number;
61
33
  }
62
34
  export interface ProjectMeta {
63
35
  projectId: string;
64
36
  projectName: string;
65
37
  updatedAt: string;
66
- edges: Edge[];
38
+ password?: string;
67
39
  }
68
40
  export interface Project {
69
41
  id: string;
70
42
  name: string;
43
+ description?: string;
71
44
  updatedAt: string;
72
45
  }
73
- export interface SearchResult {
74
- node: NodeData;
75
- score: number;
76
- matches: string[];
77
- }
78
46
  export type TaskLogType = 'progress' | 'issue' | 'solution' | 'reference';
79
47
  export interface TaskLog {
80
48
  time: string;
@@ -1,2 +1,2 @@
1
- // 知识图谱类型定义 - UI 和 Tauri 后端保持同步
1
+ // ppdocs - 文档类型定义 (简化版)
2
2
  export {};
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * MCP 工具辅助函数
3
- * 提取自 index.ts,避免重复代码
4
3
  */
5
- import type { NodeData } from '../storage/types.js';
4
+ import type { DocData } from '../storage/types.js';
6
5
  /**
7
6
  * 包装返回结果为 MCP 格式
8
7
  */
@@ -13,26 +12,22 @@ export declare function wrap(text: string): {
13
12
  }[];
14
13
  };
15
14
  /**
16
- * 格式化节点为 Markdown 输出
15
+ * 格式化文档为 Markdown 输出
17
16
  */
18
- export declare function formatNodeMarkdown(node: NodeData): string[];
17
+ export declare function formatDocMarkdown(doc: DocData, path: string): string[];
19
18
  interface TreeNode {
19
+ path: string;
20
20
  name: string;
21
- type: 'folder' | 'node';
22
- nodeId?: string;
23
- status?: string;
21
+ summary?: string;
22
+ isDir: boolean;
24
23
  children?: TreeNode[];
25
24
  }
26
- /**
27
- * 构建目录树结构
28
- */
29
- export declare function buildDirectoryTree(nodes: NodeData[]): TreeNode;
30
25
  /**
31
26
  * 格式化树为文本
32
27
  */
33
- export declare function formatTreeText(tree: TreeNode): string;
28
+ export declare function formatTreeText(tree: TreeNode[]): string;
34
29
  /**
35
- * 统计树中的节点数
30
+ * 统计树中的文档数
36
31
  */
37
- export declare function countTreeNodes(tree: TreeNode): number;
32
+ export declare function countTreeDocs(tree: TreeNode[]): number;
38
33
  export {};
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * MCP 工具辅助函数
3
- * 提取自 index.ts,避免重复代码
4
3
  */
5
4
  // ==================== 通用包装 ====================
6
5
  /**
@@ -9,149 +8,74 @@
9
8
  export function wrap(text) {
10
9
  return { content: [{ type: 'text', text }] };
11
10
  }
12
- // ==================== 节点格式化 ====================
11
+ // ==================== 文档格式化 ====================
13
12
  /**
14
- * 格式化节点为 Markdown 输出
13
+ * 格式化文档为 Markdown 输出
15
14
  */
16
- export function formatNodeMarkdown(node) {
15
+ export function formatDocMarkdown(doc, path) {
17
16
  const lines = [];
18
- // 基础信息
19
- lines.push(`## ${node.title}\n`);
20
- if (node.summary) {
21
- lines.push(`> ${node.summary}\n`);
17
+ const name = path.split('/').pop() || path;
18
+ // 标题和简介
19
+ lines.push(`## ${name}\n`);
20
+ if (doc.summary) {
21
+ lines.push(`> ${doc.summary}\n`);
22
22
  }
23
- lines.push('**基础信息**');
24
- lines.push(`| 字段 | 值 |`);
25
- lines.push(`|:---|:---|`);
26
- lines.push(`| ID | ${node.id} |`);
27
- lines.push(`| 类型 | ${node.type} |`);
28
- lines.push(`| 状态 | ${node.status} |`);
29
- lines.push(`| 签名 | ${node.signature} |`);
30
- if (node.categories && node.categories.length > 0) {
31
- lines.push(`| 标签 | ${node.categories.join(', ')} |`);
32
- }
33
- lines.push('');
34
- // 关联文件
35
- if (node.relatedFiles && node.relatedFiles.length > 0) {
36
- lines.push('**关联文件**');
37
- node.relatedFiles.forEach((f) => lines.push(`- ${f}`));
38
- lines.push('');
39
- }
40
- // 依赖关系
41
- if (node.dependencies && node.dependencies.length > 0) {
42
- lines.push('**依赖关系**');
43
- node.dependencies.forEach((d) => lines.push(`- ${d.name}: ${d.description}`));
44
- lines.push('');
45
- }
46
- // 描述内容
47
- if (node.description) {
48
- lines.push('**描述内容**');
49
- lines.push(node.description);
23
+ // 内容
24
+ if (doc.content) {
25
+ lines.push('**内容**');
26
+ lines.push(doc.content);
50
27
  lines.push('');
51
28
  }
52
29
  // 更新历史
53
- if (node.versions && node.versions.length > 0) {
30
+ if (doc.versions && doc.versions.length > 0) {
54
31
  lines.push('**更新历史**');
55
32
  lines.push('| 版本 | 日期 | 变更 |');
56
33
  lines.push('|:---|:---|:---|');
57
- node.versions.forEach((v) => lines.push(`| ${v.version} | ${v.date} | ${v.changes} |`));
34
+ doc.versions.forEach((v) => lines.push(`| ${v.version} | ${v.date} | ${v.changes} |`));
58
35
  lines.push('');
59
36
  }
60
37
  // 修复历史
61
- if (node.bugfixes && node.bugfixes.length > 0) {
38
+ if (doc.bugfixes && doc.bugfixes.length > 0) {
62
39
  lines.push('**修复历史**');
63
- lines.push('| ID | 日期 | 问题 | 方案 |');
64
- lines.push('|:---|:---|:---|:---|');
65
- node.bugfixes.forEach((b) => lines.push(`| ${b.id} | ${b.date} | ${b.issue} | ${b.solution} |`));
40
+ lines.push('| 日期 | 问题 | 方案 |');
41
+ lines.push('|:---|:---|:---|');
42
+ doc.bugfixes.forEach((b) => lines.push(`| ${b.date} | ${b.issue} | ${b.solution} |`));
66
43
  lines.push('');
67
44
  }
68
45
  return lines;
69
46
  }
70
- /**
71
- * 构建目录树结构
72
- */
73
- export function buildDirectoryTree(nodes) {
74
- const root = { name: '/', type: 'folder', children: [] };
75
- // 过滤掉根节点
76
- const docNodes = nodes.filter(n => !n.isOrigin && n.id !== 'root');
77
- for (const node of docNodes) {
78
- const pathSegments = (node.path || '').split('/').filter(Boolean);
79
- let current = root;
80
- // 遍历路径段,创建目录结构
81
- for (const segment of pathSegments) {
82
- if (!current.children)
83
- current.children = [];
84
- let child = current.children.find(c => c.name === segment && c.type === 'folder');
85
- if (!child) {
86
- child = { name: segment, type: 'folder', children: [] };
87
- current.children.push(child);
88
- }
89
- current = child;
90
- }
91
- // 添加节点
92
- if (!current.children)
93
- current.children = [];
94
- current.children.push({
95
- name: node.title,
96
- type: 'node',
97
- nodeId: node.id,
98
- status: node.status
99
- });
100
- }
101
- // 递归排序 (目录在前,节点在后)
102
- sortTree(root);
103
- return root;
104
- }
105
- function sortTree(node) {
106
- if (!node.children)
107
- return;
108
- node.children.sort((a, b) => {
109
- if (a.type === 'folder' && b.type !== 'folder')
110
- return -1;
111
- if (a.type !== 'folder' && b.type === 'folder')
112
- return 1;
113
- return a.name.localeCompare(b.name, 'zh-CN');
114
- });
115
- for (const child of node.children) {
116
- if (child.type === 'folder')
117
- sortTree(child);
118
- }
119
- }
120
47
  /**
121
48
  * 格式化树为文本
122
49
  */
123
50
  export function formatTreeText(tree) {
124
- function format(node, prefix = '', isLast = true) {
51
+ function format(nodes, prefix = '') {
125
52
  const lines = [];
126
- const connector = isLast ? '└── ' : '├── ';
127
- const childPrefix = isLast ? ' ' : '│ ';
128
- if (node.name !== '/') {
129
- const icon = node.type === 'folder' ? '📁' : '📄';
130
- const status = node.status ? ` [${node.status}]` : '';
131
- lines.push(`${prefix}${connector}${icon} ${node.name}${status}`);
132
- }
133
- if (node.children) {
134
- for (let i = 0; i < node.children.length; i++) {
135
- const child = node.children[i];
136
- const childIsLast = i === node.children.length - 1;
137
- const newPrefix = node.name === '/' ? '' : prefix + childPrefix;
138
- lines.push(...format(child, newPrefix, childIsLast));
53
+ nodes.forEach((node, index) => {
54
+ const isLast = index === nodes.length - 1;
55
+ const connector = isLast ? '└── ' : '├── ';
56
+ const childPrefix = isLast ? ' ' : '';
57
+ const icon = node.isDir ? '📁' : '📄';
58
+ const summary = node.summary ? ` - ${node.summary}` : '';
59
+ lines.push(`${prefix}${connector}${icon} ${node.name}${summary}`);
60
+ if (node.children && node.children.length > 0) {
61
+ lines.push(...format(node.children, prefix + childPrefix));
139
62
  }
140
- }
63
+ });
141
64
  return lines;
142
65
  }
143
66
  return format(tree).join('\n');
144
67
  }
145
68
  /**
146
- * 统计树中的节点数
69
+ * 统计树中的文档数
147
70
  */
148
- export function countTreeNodes(tree) {
71
+ export function countTreeDocs(tree) {
149
72
  let count = 0;
150
- function traverse(node) {
151
- if (node.type === 'node')
152
- count++;
153
- if (node.children) {
154
- node.children.forEach(traverse);
73
+ function traverse(nodes) {
74
+ for (const node of nodes) {
75
+ if (!node.isDir)
76
+ count++;
77
+ if (node.children)
78
+ traverse(node.children);
155
79
  }
156
80
  }
157
81
  traverse(tree);
@@ -1,105 +1,142 @@
1
1
  import { z } from 'zod';
2
2
  import * as storage from '../storage/httpClient.js';
3
3
  import { decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
4
- // 向量搜索已迁移至 Tauri 后端,通过 HTTP API 调用
5
- import { wrap, formatNodeMarkdown, buildDirectoryTree, formatTreeText, countTreeNodes } from './helpers.js';
4
+ import { wrap, formatDocMarkdown, formatTreeText, countTreeDocs } from './helpers.js';
6
5
  export function registerTools(server, projectId, _user) {
7
- // 1. 创建节点
8
- server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro=概念介绍。⚠️ tags至少提供3个分类标签', {
9
- title: z.string().describe('节点标题'),
10
- type: z.enum(['logic', 'data', 'intro']).describe('节点类型(logic/data/intro)'),
11
- description: z.string().describe('Markdown描述(用Mermaid流程图+表格,禁止纯文字)'),
12
- path: z.string().min(1).describe('目录路径(必填,如"/前端/组件")'),
13
- tags: z.array(z.string()).min(3).describe('⚠️ 分类标签(必填,至少3个)'),
14
- signature: z.string().optional().describe('唯一签名(用于依赖匹配,默认=title)'),
15
- summary: z.string().optional().describe('一句话简介(显示在标题下方)'),
16
- dependencies: z.array(z.object({
17
- name: z.string().describe('目标节点的signature'),
18
- description: z.string().describe('依赖说明')
19
- })).optional().describe('依赖列表(自动生成连线)'),
20
- relatedFiles: z.array(z.string()).optional().describe('关联的源文件路径数组,如 ["src/auth.ts"]')
6
+ // ===================== 文档管理 =====================
7
+ // 1. 创建文档
8
+ server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
9
+ path: z.string().min(1).describe('完整文档路径(如"/前端/组件/Modal")'),
10
+ summary: z.string().optional().describe('一句话简介'),
11
+ content: z.string().describe('Markdown内容')
21
12
  }, async (args) => {
22
13
  const decoded = decodeObjectStrings(args);
23
- const node = await storage.createNode(projectId, {
24
- title: decoded.title,
25
- type: decoded.type,
26
- status: 'incomplete',
27
- description: decoded.description || '',
14
+ const doc = await storage.createDoc(projectId, decoded.path, {
28
15
  summary: decoded.summary || '',
29
- // x, y 不传递,由 httpClient 智能计算位置
30
- locked: false,
31
- signature: decoded.signature || decoded.title,
32
- categories: decoded.tags,
33
- path: decoded.path,
34
- dependencies: decoded.dependencies || [],
35
- relatedFiles: decoded.relatedFiles || []
16
+ content: decoded.content,
17
+ versions: [{
18
+ version: 0.1,
19
+ date: new Date().toISOString(),
20
+ changes: '初始创建'
21
+ }],
22
+ bugfixes: []
36
23
  });
37
- // 向量索引由 Tauri 后端自动维护,MCP 不再手动更新
38
- return wrap(JSON.stringify(node, null, 2));
24
+ return wrap(`✅ 文档已创建: ${decoded.path}\n\n${JSON.stringify(doc, null, 2)}`);
39
25
  });
40
- // 2. 删除节点
41
- server.tool('kg_delete_node', '删除节点(锁定节点和根节点不可删除)', { nodeId: z.string().describe('节点ID') }, async (args) => {
42
- const success = await storage.deleteNode(projectId, args.nodeId);
43
- // 向量索引由 Tauri 后端自动维护,MCP 不再手动删除
44
- return wrap(success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
26
+ // 2. 删除文档 (支持批量)
27
+ server.tool('kg_delete_node', '删除文档(支持批量,根文档不可删除)', { nodeId: z.union([z.string(), z.array(z.string())]).describe('文档路径或路径数组') }, async (args) => {
28
+ const paths = Array.isArray(args.nodeId) ? args.nodeId : [args.nodeId];
29
+ const results = [];
30
+ for (const path of paths) {
31
+ const success = await storage.deleteDoc(projectId, path);
32
+ results.push({ path, success });
33
+ }
34
+ const successCount = results.filter(r => r.success).length;
35
+ const failedPaths = results.filter(r => !r.success).map(r => r.path);
36
+ if (paths.length === 1) {
37
+ return wrap(results[0].success ? '删除成功' : '删除失败(文档不存在或是根文档)');
38
+ }
39
+ let msg = `✅ 批量删除完成: ${successCount}/${paths.length} 成功`;
40
+ if (failedPaths.length > 0) {
41
+ msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
42
+ }
43
+ return wrap(msg);
45
44
  });
46
- // 3. 更新节点
47
- server.tool('kg_update_node', '更新节点内容(锁定节点不可更新)。⚠️ 更新tags时至少提供3个分类标签', {
48
- nodeId: z.string().describe('节点ID'),
49
- title: z.string().optional().describe('新标题'),
50
- signature: z.string().optional().describe('新签名'),
51
- description: z.string().optional().describe('新描述(Markdown)'),
52
- summary: z.string().optional().describe('一句话简介(显示在标题下方)'),
53
- path: z.string().optional().describe('目录路径(如"/前端/组件")'),
54
- x: z.number().optional().describe('X坐标'),
55
- y: z.number().optional().describe('Y坐标'),
56
- status: z.enum(['incomplete', 'complete', 'fixing', 'refactoring', 'deprecated']).optional().describe('状态'),
57
- tags: z.array(z.string()).min(3).optional().describe('⚠️ 分类标签(更新时至少3个)'),
58
- dependencies: z.array(z.object({
59
- name: z.string(),
60
- description: z.string()
61
- })).optional().describe('依赖列表'),
62
- relatedFiles: z.array(z.string()).optional().describe('关联的源文件路径数组'),
45
+ // 3. 更新文档 (支持批量)
46
+ server.tool('kg_update_node', '更新文档内容(支持批量)', {
47
+ nodeId: z.union([
48
+ z.string(),
49
+ z.array(z.string())
50
+ ]).describe('文档路径或路径数组'),
51
+ summary: z.string().optional().describe('一句话简介'),
52
+ content: z.string().optional().describe('Markdown内容'),
63
53
  versions: z.array(z.object({
64
54
  version: z.number(),
65
55
  date: z.string(),
66
56
  changes: z.string()
67
- })).optional().describe('版本记录(0.1起始,AI控制递增)'),
57
+ })).optional().describe('版本记录'),
68
58
  bugfixes: z.array(z.object({
69
- id: z.string(),
70
59
  date: z.string(),
71
60
  issue: z.string(),
72
- solution: z.string(),
73
- impact: z.string().optional()
61
+ solution: z.string()
74
62
  })).optional().describe('修复记录'),
75
- lastSyncAt: z.string().optional().describe('代码↔图谱最后同步时间(ISO时间戳)')
63
+ updates: z.array(z.object({
64
+ nodeId: z.string().describe('文档路径'),
65
+ summary: z.string().optional(),
66
+ content: z.string().optional(),
67
+ versions: z.array(z.object({
68
+ version: z.number(),
69
+ date: z.string(),
70
+ changes: z.string()
71
+ })).optional(),
72
+ bugfixes: z.array(z.object({
73
+ date: z.string(),
74
+ issue: z.string(),
75
+ solution: z.string()
76
+ })).optional()
77
+ })).optional().describe('批量更新数组(每项独立配置)')
76
78
  }, async (args) => {
77
79
  const decoded = decodeObjectStrings(args);
78
- const { nodeId, tags, relatedFiles, versions, bugfixes, path, lastSyncAt, ...rest } = decoded;
79
- // 根节点必须使用 kg_update_root 更新
80
- if (nodeId === 'root') {
81
- return wrap('❌ 根节点请使用 kg_update_root 方法更新');
80
+ // 批量更新模式: 使用 updates 数组
81
+ if (decoded.updates && Array.isArray(decoded.updates)) {
82
+ const results = [];
83
+ for (const item of decoded.updates) {
84
+ if (item.nodeId === '/' || item.nodeId === '_root') {
85
+ results.push({ path: item.nodeId, success: false });
86
+ continue;
87
+ }
88
+ const existing = await storage.getDoc(projectId, item.nodeId);
89
+ if (!existing) {
90
+ results.push({ path: item.nodeId, success: false });
91
+ continue;
92
+ }
93
+ const updates = {};
94
+ if (item.summary !== undefined)
95
+ updates.summary = item.summary;
96
+ if (item.content !== undefined)
97
+ updates.content = item.content;
98
+ if (item.versions !== undefined)
99
+ updates.versions = item.versions;
100
+ if (item.bugfixes !== undefined)
101
+ updates.bugfixes = item.bugfixes;
102
+ const doc = await storage.updateDoc(projectId, item.nodeId, updates);
103
+ results.push({ path: item.nodeId, success: !!doc });
104
+ }
105
+ const successCount = results.filter(r => r.success).length;
106
+ const failedPaths = results.filter(r => !r.success).map(r => r.path);
107
+ let msg = `✅ 批量更新完成: ${successCount}/${decoded.updates.length} 成功`;
108
+ if (failedPaths.length > 0) {
109
+ msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
110
+ }
111
+ return wrap(msg);
112
+ }
113
+ // 单个更新模式
114
+ const { nodeId, summary, content, versions, bugfixes } = decoded;
115
+ const singleNodeId = Array.isArray(nodeId) ? nodeId[0] : nodeId;
116
+ // 根文档必须使用 kg_update_root 更新
117
+ if (singleNodeId === '/' || singleNodeId === '_root') {
118
+ return wrap('❌ 根文档请使用 kg_update_root 方法更新');
82
119
  }
83
- // API 参数转换
84
- let updates = { ...rest };
85
- if (tags !== undefined)
86
- updates.categories = tags;
87
- if (relatedFiles !== undefined)
88
- updates.relatedFiles = relatedFiles;
120
+ // 获取现有文档
121
+ const existing = await storage.getDoc(projectId, singleNodeId);
122
+ if (!existing) {
123
+ return wrap('更新失败(文档不存在)');
124
+ }
125
+ // 构建更新内容
126
+ const updates = {};
127
+ if (summary !== undefined)
128
+ updates.summary = summary;
129
+ if (content !== undefined)
130
+ updates.content = content;
89
131
  if (versions !== undefined)
90
132
  updates.versions = versions;
91
133
  if (bugfixes !== undefined)
92
134
  updates.bugfixes = bugfixes;
93
- if (path !== undefined)
94
- updates.path = path;
95
- if (lastSyncAt !== undefined)
96
- updates.lastSyncAt = lastSyncAt;
97
- const node = await storage.updateNode(projectId, nodeId, updates);
98
- // 向量索引由 Tauri 后端自动维护,MCP 不再手动更新
99
- return wrap(node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
135
+ const doc = await storage.updateDoc(projectId, singleNodeId, updates);
136
+ return wrap(doc ? JSON.stringify(doc, null, 2) : '更新失败');
100
137
  });
101
- // 3.5 更新根节点 (项目介绍)
102
- server.tool('kg_update_root', '更新项目介绍(根节点描述,锁定时不可更新)', {
138
+ // 3.5 更新根文档 (项目介绍)
139
+ server.tool('kg_update_root', '更新项目介绍(根文档)', {
103
140
  title: z.string().optional().describe('项目标题'),
104
141
  description: z.string().optional().describe('项目介绍(Markdown)')
105
142
  }, async (args) => {
@@ -107,13 +144,19 @@ export function registerTools(server, projectId, _user) {
107
144
  if (decoded.title === undefined && decoded.description === undefined) {
108
145
  return wrap('❌ 请至少提供 title 或 description');
109
146
  }
110
- const node = await storage.updateRoot(projectId, {
111
- title: decoded.title,
112
- description: decoded.description
113
- });
114
- return wrap(node ? '✅ 项目介绍已更新' : '更新失败(根节点已锁定)');
147
+ const existing = await storage.getDoc(projectId, '/');
148
+ if (!existing) {
149
+ return wrap('更新失败(根文档不存在)');
150
+ }
151
+ const updates = {};
152
+ if (decoded.title !== undefined)
153
+ updates.summary = decoded.title;
154
+ if (decoded.description !== undefined)
155
+ updates.content = decoded.description;
156
+ const doc = await storage.updateDoc(projectId, '/', updates);
157
+ return wrap(doc ? '✅ 项目介绍已更新' : '更新失败');
115
158
  });
116
- // 3.6 获取项目规则 (独立方法,按需调用)
159
+ // 3.6 获取项目规则
117
160
  server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)', {
118
161
  ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests']).optional()
119
162
  .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部')
@@ -125,7 +168,7 @@ export function registerTools(server, projectId, _user) {
125
168
  }
126
169
  return { content: [{ type: 'text', text: rules }] };
127
170
  });
128
- // 3.7 保存项目规则 (独立存储)
171
+ // 3.7 保存项目规则
129
172
  server.tool('kg_save_rules', '保存单个类型的项目规则(独立文件存储)', {
130
173
  ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests'])
131
174
  .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则'),
@@ -138,26 +181,28 @@ export function registerTools(server, projectId, _user) {
138
181
  }
139
182
  return wrap(`✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
140
183
  });
141
- // 5. 读取单个节点详情
142
- server.tool('kg_read_node', '读取单个节点的完整内容(描述、关联文件、依赖、历史记录)', {
143
- nodeId: z.string().describe('节点ID')
184
+ // 5. 读取单个文档详情
185
+ server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)', {
186
+ nodeId: z.string().describe('文档路径')
144
187
  }, async (args) => {
145
- const node = await storage.getNode(projectId, args.nodeId);
146
- if (!node) {
147
- return wrap('节点不存在');
188
+ const doc = await storage.getDoc(projectId, args.nodeId);
189
+ if (!doc) {
190
+ return wrap('文档不存在');
148
191
  }
149
- // 格式化节点基础信息
150
- const lines = formatNodeMarkdown(node);
192
+ // 格式化文档
193
+ const lines = formatDocMarkdown(doc, args.nodeId);
151
194
  return wrap(lines.join('\n'));
152
195
  });
153
196
  // 10. 获取知识库树状图
154
197
  server.tool('kg_get_tree', '获取知识库的目录树结构(按 path 分组)', {}, async () => {
155
198
  try {
156
- const nodes = await storage.listNodes(projectId);
157
- const tree = buildDirectoryTree(nodes);
158
- const treeText = formatTreeText(tree);
159
- const nodeCount = countTreeNodes(tree);
160
- return wrap(`共 ${nodeCount} 个节点\n\n${treeText}`);
199
+ const tree = await storage.getTree(projectId);
200
+ if (tree && tree.length > 0) {
201
+ const treeText = formatTreeText(tree);
202
+ const docCount = countTreeDocs(tree);
203
+ return wrap(`共 ${docCount} 个文档\n\n${treeText}`);
204
+ }
205
+ return wrap('知识库为空');
161
206
  }
162
207
  catch (e) {
163
208
  return wrap(JSON.stringify({
@@ -182,7 +227,7 @@ export function registerTools(server, projectId, _user) {
182
227
  }, _user);
183
228
  return wrap(JSON.stringify(task, null, 2));
184
229
  });
185
- // 8. 读取任务 (按名字搜索,支持当前/历史)
230
+ // 8. 读取任务
186
231
  server.tool('task_get', '读取任务详情(按名字搜索,支持当前任务和历史任务)', {
187
232
  title: z.string().describe('任务名称(模糊匹配)'),
188
233
  status: z.enum(['active', 'archived', 'all']).optional().describe('状态筛选: active=进行中, archived=已归档, all=全部(默认)')
@@ -221,7 +266,7 @@ export function registerTools(server, projectId, _user) {
221
266
  }));
222
267
  return wrap(`找到 ${matched.length} 个匹配任务:\n${JSON.stringify(list, null, 2)}\n\n请提供更精确的任务名称`);
223
268
  });
224
- // 9. 更新任务 (添加日志)
269
+ // 9. 更新任务
225
270
  server.tool('task_update', '更新任务(添加进展日志)', {
226
271
  taskId: z.string().describe('任务ID'),
227
272
  log_type: z.enum(['progress', 'issue', 'solution', 'reference']).describe('日志类型: progress=进展, issue=问题, solution=方案, reference=参考'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "2.7.2",
3
+ "version": "2.8.1",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",