@ppdocs/mcp 2.6.4 → 2.6.6
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/dist/cli.js +16 -5
- package/dist/storage/httpClient.d.ts +10 -0
- package/dist/storage/httpClient.js +94 -5
- package/dist/storage/types.d.ts +1 -0
- package/dist/tools/index.js +93 -40
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +90 -0
- package/package.json +12 -3
- package/templates/hooks/SystemPrompt.md +32 -19
package/dist/cli.js
CHANGED
|
@@ -135,39 +135,50 @@ function commandExists(cmd) {
|
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
/** 静默执行命令 (忽略错误) */
|
|
139
|
+
function execSilent(cmd) {
|
|
140
|
+
try {
|
|
141
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
}
|
|
138
145
|
/** 自动检测 AI CLI 并注册 MCP */
|
|
139
146
|
function autoRegisterMcp(apiUrl, user) {
|
|
140
147
|
const detected = [];
|
|
148
|
+
const serverName = 'ppdocs-kg';
|
|
141
149
|
// 检测 Claude CLI
|
|
142
150
|
if (commandExists('claude')) {
|
|
143
151
|
detected.push('Claude');
|
|
144
152
|
try {
|
|
145
|
-
const cmd = `claude mcp add ppdocs-kg -e PPDOCS_API_URL=${apiUrl} -e PPDOCS_USER=${user} -- npx -y @ppdocs/mcp@latest`;
|
|
146
153
|
console.log(`✅ Detected Claude CLI, registering MCP...`);
|
|
154
|
+
execSilent(`claude mcp remove ${serverName}`);
|
|
155
|
+
const cmd = `claude mcp add ${serverName} -e PPDOCS_API_URL=${apiUrl} -e PPDOCS_USER=${user} -- npx -y @ppdocs/mcp@latest`;
|
|
147
156
|
execSync(cmd, { stdio: 'inherit' });
|
|
148
157
|
}
|
|
149
158
|
catch (e) {
|
|
150
159
|
console.log(`⚠️ Claude MCP registration failed: ${e}`);
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
|
-
// 检测 Codex CLI (OpenAI)
|
|
162
|
+
// 检测 Codex CLI (OpenAI) - 使用 --env 而非 -e
|
|
154
163
|
if (commandExists('codex')) {
|
|
155
164
|
detected.push('Codex');
|
|
156
165
|
try {
|
|
157
|
-
const cmd = `codex mcp add ppdocs-kg -e PPDOCS_API_URL=${apiUrl} -e PPDOCS_USER=${user} -- npx -y @ppdocs/mcp@latest`;
|
|
158
166
|
console.log(`✅ Detected Codex CLI, registering MCP...`);
|
|
167
|
+
execSilent(`codex mcp remove ${serverName}`);
|
|
168
|
+
const cmd = `codex mcp add ${serverName} --env PPDOCS_API_URL=${apiUrl} --env PPDOCS_USER=${user} -- npx -y @ppdocs/mcp@latest`;
|
|
159
169
|
execSync(cmd, { stdio: 'inherit' });
|
|
160
170
|
}
|
|
161
171
|
catch (e) {
|
|
162
172
|
console.log(`⚠️ Codex MCP registration failed: ${e}`);
|
|
163
173
|
}
|
|
164
174
|
}
|
|
165
|
-
// 检测 Gemini CLI
|
|
175
|
+
// 检测 Gemini CLI - 命令在位置参数,-e 在后面
|
|
166
176
|
if (commandExists('gemini')) {
|
|
167
177
|
detected.push('Gemini');
|
|
168
178
|
try {
|
|
169
|
-
const cmd = `gemini mcp add ppdocs-kg -e PPDOCS_API_URL=${apiUrl} -e PPDOCS_USER=${user} -- npx -y @ppdocs/mcp@latest`;
|
|
170
179
|
console.log(`✅ Detected Gemini CLI, registering MCP...`);
|
|
180
|
+
execSilent(`gemini mcp remove ${serverName}`);
|
|
181
|
+
const cmd = `gemini mcp add ${serverName} "npx -y @ppdocs/mcp@latest" -e PPDOCS_API_URL=${apiUrl} -e PPDOCS_USER=${user}`;
|
|
171
182
|
execSync(cmd, { stdio: 'inherit' });
|
|
172
183
|
}
|
|
173
184
|
catch (e) {
|
|
@@ -13,6 +13,11 @@ export declare class PpdocsApiClient {
|
|
|
13
13
|
getNode(nodeId: string): Promise<NodeData | null>;
|
|
14
14
|
createNode(node: Partial<NodeData>): Promise<NodeData>;
|
|
15
15
|
updateNode(nodeId: string, updates: Partial<NodeData>): Promise<NodeData | null>;
|
|
16
|
+
updateRoot(updates: {
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
userStyles?: string[];
|
|
20
|
+
}): Promise<NodeData | null>;
|
|
16
21
|
deleteNode(nodeId: string): Promise<boolean>;
|
|
17
22
|
lockNode(nodeId: string, locked: boolean): Promise<NodeData | null>;
|
|
18
23
|
searchNodes(keywords: string[], limit?: number): Promise<SearchResult[]>;
|
|
@@ -51,6 +56,11 @@ export declare function listNodes(_projectId: string): Promise<NodeData[]>;
|
|
|
51
56
|
export declare function getNode(_projectId: string, nodeId: string): Promise<NodeData | null>;
|
|
52
57
|
export declare function createNode(_projectId: string, node: Partial<NodeData>): Promise<NodeData>;
|
|
53
58
|
export declare function updateNode(_projectId: string, nodeId: string, updates: Partial<NodeData>): Promise<NodeData | null>;
|
|
59
|
+
export declare function updateRoot(_projectId: string, updates: {
|
|
60
|
+
title?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
userStyles?: string[];
|
|
63
|
+
}): Promise<NodeData | null>;
|
|
54
64
|
export declare function deleteNode(_projectId: string, nodeId: string): Promise<boolean>;
|
|
55
65
|
export declare function lockNode(_projectId: string, nodeId: string, locked: boolean): Promise<NodeData | null>;
|
|
56
66
|
export declare function searchNodes(_projectId: string, keywords: string[], limit?: number): Promise<SearchResult[]>;
|
|
@@ -4,6 +4,59 @@
|
|
|
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
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const maxX = Math.max(...nodes.map(n => n.x), 0);
|
|
58
|
+
return { x: Math.round(maxX + SMART_NODE_GAP), y: Math.round(cy) };
|
|
59
|
+
}
|
|
7
60
|
// API 客户端类
|
|
8
61
|
export class PpdocsApiClient {
|
|
9
62
|
baseUrl; // http://localhost:20001/api/projectId/password
|
|
@@ -58,20 +111,29 @@ export class PpdocsApiClient {
|
|
|
58
111
|
}
|
|
59
112
|
}
|
|
60
113
|
async createNode(node) {
|
|
61
|
-
//
|
|
114
|
+
// 智能定位: 计算新节点位置
|
|
115
|
+
let x = node.x ?? 0;
|
|
116
|
+
let y = node.y ?? 0;
|
|
117
|
+
// 如果未指定位置,自动计算
|
|
118
|
+
if (node.x === undefined && node.y === undefined) {
|
|
119
|
+
const existingNodes = await this.listNodes();
|
|
120
|
+
const pos = computeSmartPosition(node.dependencies, existingNodes);
|
|
121
|
+
x = pos.x;
|
|
122
|
+
y = pos.y;
|
|
123
|
+
}
|
|
62
124
|
const payload = {
|
|
63
125
|
id: '', // 服务端自动生成
|
|
64
126
|
title: node.title || '',
|
|
65
127
|
type: node.type || 'logic',
|
|
66
128
|
status: node.status || 'incomplete',
|
|
67
|
-
x
|
|
68
|
-
y
|
|
129
|
+
x,
|
|
130
|
+
y,
|
|
69
131
|
locked: false,
|
|
70
132
|
signature: node.signature || node.title || '',
|
|
71
133
|
categories: node.categories || [],
|
|
72
134
|
description: node.description || '',
|
|
73
135
|
dependencies: node.dependencies || [],
|
|
74
|
-
relatedFiles: [],
|
|
136
|
+
relatedFiles: node.relatedFiles || [],
|
|
75
137
|
createdAt: new Date().toISOString(),
|
|
76
138
|
updatedAt: new Date().toISOString(),
|
|
77
139
|
lastAccessedAt: new Date().toISOString(),
|
|
@@ -103,6 +165,31 @@ export class PpdocsApiClient {
|
|
|
103
165
|
return null;
|
|
104
166
|
}
|
|
105
167
|
}
|
|
168
|
+
async updateRoot(updates) {
|
|
169
|
+
// 专用根节点更新,支持 userStyles
|
|
170
|
+
const root = await this.getNode('root');
|
|
171
|
+
if (!root)
|
|
172
|
+
return null;
|
|
173
|
+
if (root.locked)
|
|
174
|
+
return null; // 锁定时拒绝
|
|
175
|
+
// 构建更新载荷 (只传入有值的字段)
|
|
176
|
+
const payload = { updatedAt: new Date().toISOString() };
|
|
177
|
+
if (updates.title !== undefined)
|
|
178
|
+
payload.title = updates.title;
|
|
179
|
+
if (updates.description !== undefined)
|
|
180
|
+
payload.description = updates.description;
|
|
181
|
+
if (updates.userStyles !== undefined)
|
|
182
|
+
payload.userStyles = updates.userStyles;
|
|
183
|
+
try {
|
|
184
|
+
return await this.request('/nodes/root', {
|
|
185
|
+
method: 'PUT',
|
|
186
|
+
body: JSON.stringify({ ...root, ...payload })
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
106
193
|
async deleteNode(nodeId) {
|
|
107
194
|
try {
|
|
108
195
|
await this.request(`/nodes/${nodeId}`, { method: 'DELETE' });
|
|
@@ -187,7 +274,6 @@ export class PpdocsApiClient {
|
|
|
187
274
|
}
|
|
188
275
|
}
|
|
189
276
|
async createTask(task, creator) {
|
|
190
|
-
const now = new Date().toISOString();
|
|
191
277
|
const payload = {
|
|
192
278
|
title: task.title,
|
|
193
279
|
creator,
|
|
@@ -249,6 +335,9 @@ export async function createNode(_projectId, node) {
|
|
|
249
335
|
export async function updateNode(_projectId, nodeId, updates) {
|
|
250
336
|
return getClient().updateNode(nodeId, updates);
|
|
251
337
|
}
|
|
338
|
+
export async function updateRoot(_projectId, updates) {
|
|
339
|
+
return getClient().updateRoot(updates);
|
|
340
|
+
}
|
|
252
341
|
export async function deleteNode(_projectId, nodeId) {
|
|
253
342
|
return getClient().deleteNode(nodeId);
|
|
254
343
|
}
|
package/dist/storage/types.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as storage from '../storage/httpClient.js';
|
|
3
|
+
import { decodeUnicodeEscapes, decodeObjectStrings, wrapResult, clearStyleCache } from '../utils.js';
|
|
4
|
+
// 辅助函数: 包装返回结果
|
|
5
|
+
async function wrap(projectId, text) {
|
|
6
|
+
const wrapped = await wrapResult(projectId, text);
|
|
7
|
+
return { content: [{ type: 'text', text: wrapped }] };
|
|
8
|
+
}
|
|
3
9
|
export function registerTools(server, projectId, _user) {
|
|
4
10
|
// 1. 创建节点
|
|
5
11
|
server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro=概念介绍', {
|
|
@@ -19,20 +25,19 @@ export function registerTools(server, projectId, _user) {
|
|
|
19
25
|
type: args.type,
|
|
20
26
|
status: 'incomplete',
|
|
21
27
|
description: args.description || '',
|
|
22
|
-
x
|
|
23
|
-
y: 0,
|
|
28
|
+
// x, y 不传递,由 httpClient 智能计算位置
|
|
24
29
|
locked: false,
|
|
25
30
|
signature: args.signature || args.title,
|
|
26
31
|
categories: args.tags || [],
|
|
27
32
|
dependencies: args.dependencies || [],
|
|
28
33
|
relatedFiles: args.relatedFiles || []
|
|
29
34
|
});
|
|
30
|
-
return
|
|
35
|
+
return wrap(projectId, JSON.stringify(node, null, 2));
|
|
31
36
|
});
|
|
32
37
|
// 2. 删除节点
|
|
33
38
|
server.tool('kg_delete_node', '删除节点(锁定节点和根节点不可删除)', { nodeId: z.string().describe('节点ID') }, async (args) => {
|
|
34
39
|
const success = await storage.deleteNode(projectId, args.nodeId);
|
|
35
|
-
return
|
|
40
|
+
return wrap(projectId, success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
|
|
36
41
|
});
|
|
37
42
|
// 3. 更新节点
|
|
38
43
|
server.tool('kg_update_node', '更新节点内容(锁定节点不可更新)', {
|
|
@@ -49,6 +54,10 @@ export function registerTools(server, projectId, _user) {
|
|
|
49
54
|
relatedFiles: z.array(z.string()).optional().describe('关联的源文件路径数组')
|
|
50
55
|
}, async (args) => {
|
|
51
56
|
const { nodeId, tags, relatedFiles, ...rest } = args;
|
|
57
|
+
// 根节点必须使用 kg_update_root 更新
|
|
58
|
+
if (nodeId === 'root') {
|
|
59
|
+
return wrap(projectId, '❌ 根节点请使用 kg_update_root 方法更新');
|
|
60
|
+
}
|
|
52
61
|
// API 参数 tags 转换为内部字段 categories
|
|
53
62
|
let updates = { ...rest };
|
|
54
63
|
if (tags !== undefined)
|
|
@@ -56,14 +65,33 @@ export function registerTools(server, projectId, _user) {
|
|
|
56
65
|
if (relatedFiles !== undefined)
|
|
57
66
|
updates.relatedFiles = relatedFiles;
|
|
58
67
|
const node = await storage.updateNode(projectId, nodeId, updates);
|
|
59
|
-
return
|
|
68
|
+
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
|
|
69
|
+
});
|
|
70
|
+
// 3.5 更新根节点 (专用方法,支持 userStyles)
|
|
71
|
+
server.tool('kg_update_root', '更新根节点(项目规则,锁定时不可更新)', {
|
|
72
|
+
title: z.string().optional().describe('项目标题'),
|
|
73
|
+
description: z.string().optional().describe('项目描述(Markdown)'),
|
|
74
|
+
userStyles: z.array(z.string()).optional().describe('项目规则列表(字符串数组,每条一个规则)')
|
|
75
|
+
}, async (args) => {
|
|
76
|
+
// 空参数检查
|
|
77
|
+
if (args.title === undefined && args.description === undefined && args.userStyles === undefined) {
|
|
78
|
+
return wrap(projectId, '❌ 请至少提供一个更新参数(title/description/userStyles)');
|
|
79
|
+
}
|
|
80
|
+
const node = await storage.updateRoot(projectId, {
|
|
81
|
+
title: args.title,
|
|
82
|
+
description: args.description,
|
|
83
|
+
userStyles: args.userStyles
|
|
84
|
+
});
|
|
85
|
+
if (node)
|
|
86
|
+
clearStyleCache();
|
|
87
|
+
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(根节点已锁定)');
|
|
60
88
|
});
|
|
61
89
|
// 4. 锁定节点 (只能锁定,解锁需用户在前端手动操作)
|
|
62
90
|
server.tool('kg_lock_node', '锁定节点(锁定后只能读取,解锁需用户在前端手动操作)', {
|
|
63
91
|
nodeId: z.string().describe('节点ID')
|
|
64
92
|
}, async (args) => {
|
|
65
93
|
const node = await storage.lockNode(projectId, args.nodeId, true); // 强制锁定
|
|
66
|
-
return
|
|
94
|
+
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '操作失败');
|
|
67
95
|
});
|
|
68
96
|
// 5. 搜索节点
|
|
69
97
|
server.tool('kg_search', '关键词搜索节点,按相关度排序返回', {
|
|
@@ -75,7 +103,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
75
103
|
id: r.node.id, title: r.node.title, type: r.node.type,
|
|
76
104
|
status: r.node.status, score: r.score.toFixed(1), matches: r.matches
|
|
77
105
|
}));
|
|
78
|
-
return
|
|
106
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
79
107
|
});
|
|
80
108
|
// 6. 路径查找
|
|
81
109
|
server.tool('kg_find_path', '查找两节点间的依赖路径', {
|
|
@@ -84,44 +112,63 @@ export function registerTools(server, projectId, _user) {
|
|
|
84
112
|
}, async (args) => {
|
|
85
113
|
const result = await storage.findPath(projectId, args.startId, args.endId);
|
|
86
114
|
if (!result)
|
|
87
|
-
return
|
|
115
|
+
return wrap(projectId, '未找到路径');
|
|
88
116
|
const output = {
|
|
89
117
|
pathLength: result.path.length,
|
|
90
118
|
nodes: result.path.map(n => ({ id: n.id, title: n.title, type: n.type })),
|
|
91
119
|
edges: result.edges.map(e => ({ from: e.source, to: e.target, type: e.type }))
|
|
92
120
|
};
|
|
93
|
-
return
|
|
121
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
94
122
|
});
|
|
95
123
|
// 7. 列出所有节点
|
|
96
124
|
server.tool('kg_list_nodes', '列出项目全部节点概览', {}, async () => {
|
|
97
125
|
const nodes = await storage.listNodes(projectId);
|
|
98
126
|
const output = nodes.map(n => ({ id: n.id, title: n.title, type: n.type, status: n.status, locked: n.locked }));
|
|
99
|
-
return
|
|
127
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
100
128
|
});
|
|
101
129
|
// 8. 查找孤立节点
|
|
102
130
|
server.tool('kg_find_orphans', '查找无连线的孤立节点(用于清理)', {}, async () => {
|
|
103
131
|
const orphans = await storage.findOrphans(projectId);
|
|
104
132
|
if (orphans.length === 0) {
|
|
105
|
-
return
|
|
133
|
+
return wrap(projectId, '没有孤立节点');
|
|
106
134
|
}
|
|
107
|
-
return
|
|
135
|
+
return wrap(projectId, JSON.stringify(orphans, null, 2));
|
|
108
136
|
});
|
|
109
|
-
// 9. 查询节点关系网
|
|
110
|
-
server.tool('kg_get_relations', '获取节点的上下游关系(谁依赖它/它依赖谁)', {
|
|
111
|
-
nodeId: z.string().describe('节点ID')
|
|
137
|
+
// 9. 查询节点关系网 (支持多层)
|
|
138
|
+
server.tool('kg_get_relations', '获取节点的上下游关系(谁依赖它/它依赖谁),支持多层查询', {
|
|
139
|
+
nodeId: z.string().describe('节点ID'),
|
|
140
|
+
depth: z.number().min(1).max(3).optional().describe('查询层数(默认1,最大3)')
|
|
112
141
|
}, async (args) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
const maxDepth = Math.min(args.depth || 1, 3);
|
|
143
|
+
const visited = new Set();
|
|
144
|
+
const outgoing = [];
|
|
145
|
+
const incoming = [];
|
|
146
|
+
// 递归获取关系
|
|
147
|
+
async function fetchRelations(nodeId, currentDepth, direction) {
|
|
148
|
+
if (currentDepth > maxDepth || visited.has(`${nodeId}-${direction}`))
|
|
149
|
+
return;
|
|
150
|
+
visited.add(`${nodeId}-${direction}`);
|
|
151
|
+
const relations = await storage.getRelations(projectId, nodeId);
|
|
152
|
+
for (const r of relations) {
|
|
153
|
+
if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
|
|
154
|
+
if (!outgoing.some(o => o.id === r.nodeId)) {
|
|
155
|
+
outgoing.push({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType, depth: currentDepth });
|
|
156
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'outgoing');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
|
|
160
|
+
if (!incoming.some(i => i.id === r.nodeId)) {
|
|
161
|
+
incoming.push({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType, depth: currentDepth });
|
|
162
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'incoming');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
116
166
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
incoming: incoming.map(r => ({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType }))
|
|
123
|
-
};
|
|
124
|
-
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
167
|
+
await fetchRelations(args.nodeId, 1, 'both');
|
|
168
|
+
if (outgoing.length === 0 && incoming.length === 0) {
|
|
169
|
+
return wrap(projectId, '该节点没有任何连线');
|
|
170
|
+
}
|
|
171
|
+
return wrap(projectId, JSON.stringify({ outgoing, incoming }, null, 2));
|
|
125
172
|
});
|
|
126
173
|
// ===================== 任务管理工具 =====================
|
|
127
174
|
// 10. 创建任务
|
|
@@ -131,13 +178,15 @@ export function registerTools(server, projectId, _user) {
|
|
|
131
178
|
goals: z.array(z.string()).optional().describe('目标清单'),
|
|
132
179
|
related_nodes: z.array(z.string()).optional().describe('关联节点ID')
|
|
133
180
|
}, async (args) => {
|
|
181
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
182
|
+
const decoded = decodeObjectStrings(args);
|
|
134
183
|
const task = await storage.createTask(projectId, {
|
|
135
|
-
title:
|
|
136
|
-
description:
|
|
137
|
-
goals:
|
|
138
|
-
related_nodes:
|
|
184
|
+
title: decoded.title,
|
|
185
|
+
description: decoded.description,
|
|
186
|
+
goals: decoded.goals || [],
|
|
187
|
+
related_nodes: decoded.related_nodes
|
|
139
188
|
}, _user);
|
|
140
|
-
return
|
|
189
|
+
return wrap(projectId, JSON.stringify(task, null, 2));
|
|
141
190
|
});
|
|
142
191
|
// 11. 列出任务
|
|
143
192
|
server.tool('task_list', '列出任务(active=进行中,archived=已归档)', {
|
|
@@ -152,7 +201,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
152
201
|
created_at: t.created_at,
|
|
153
202
|
last_log: t.last_log
|
|
154
203
|
}));
|
|
155
|
-
return
|
|
204
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
156
205
|
});
|
|
157
206
|
// 12. 获取任务详情
|
|
158
207
|
server.tool('task_get', '获取任务完整信息(含全部日志)', {
|
|
@@ -160,9 +209,9 @@ export function registerTools(server, projectId, _user) {
|
|
|
160
209
|
}, async (args) => {
|
|
161
210
|
const task = await storage.getTask(projectId, args.taskId);
|
|
162
211
|
if (!task) {
|
|
163
|
-
return
|
|
212
|
+
return wrap(projectId, '任务不存在');
|
|
164
213
|
}
|
|
165
|
-
return
|
|
214
|
+
return wrap(projectId, JSON.stringify(task, null, 2));
|
|
166
215
|
});
|
|
167
216
|
// 13. 添加任务日志
|
|
168
217
|
server.tool('task_add_log', '记录任务进展/问题/方案/参考', {
|
|
@@ -170,11 +219,13 @@ export function registerTools(server, projectId, _user) {
|
|
|
170
219
|
log_type: z.enum(['progress', 'issue', 'solution', 'reference']).describe('日志类型'),
|
|
171
220
|
content: z.string().describe('日志内容(Markdown)')
|
|
172
221
|
}, async (args) => {
|
|
173
|
-
|
|
222
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
223
|
+
const decodedContent = decodeUnicodeEscapes(args.content);
|
|
224
|
+
const task = await storage.addTaskLog(projectId, args.taskId, args.log_type, decodedContent);
|
|
174
225
|
if (!task) {
|
|
175
|
-
return
|
|
226
|
+
return wrap(projectId, '添加失败(任务不存在或已归档)');
|
|
176
227
|
}
|
|
177
|
-
return
|
|
228
|
+
return wrap(projectId, `日志已添加,任务共有 ${task.logs.length} 条日志`);
|
|
178
229
|
});
|
|
179
230
|
// 14. 完成任务
|
|
180
231
|
server.tool('task_complete', '完成任务并归档,填写经验总结', {
|
|
@@ -187,15 +238,17 @@ export function registerTools(server, projectId, _user) {
|
|
|
187
238
|
url: z.string().optional()
|
|
188
239
|
})).optional().describe('参考资料')
|
|
189
240
|
}, async (args) => {
|
|
190
|
-
|
|
241
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
242
|
+
const decoded = decodeObjectStrings({
|
|
191
243
|
summary: args.summary,
|
|
192
244
|
difficulties: args.difficulties || [],
|
|
193
245
|
solutions: args.solutions || [],
|
|
194
246
|
references: args.references || []
|
|
195
247
|
});
|
|
248
|
+
const task = await storage.completeTask(projectId, args.taskId, decoded);
|
|
196
249
|
if (!task) {
|
|
197
|
-
return
|
|
250
|
+
return wrap(projectId, '完成失败(任务不存在或已归档)');
|
|
198
251
|
}
|
|
199
|
-
return
|
|
252
|
+
return wrap(projectId, `任务已完成归档: ${task.title}`);
|
|
200
253
|
});
|
|
201
254
|
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server 工具函数
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 获取根节点的用户风格
|
|
6
|
+
*/
|
|
7
|
+
export declare function getRootStyle(projectId: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* 清除风格缓存 (当根节点更新时调用)
|
|
10
|
+
*/
|
|
11
|
+
export declare function clearStyleCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* 包装工具返回结果 (注入用户风格)
|
|
14
|
+
*/
|
|
15
|
+
export declare function wrapResult(projectId: string, result: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* 解码 Unicode 转义序列
|
|
18
|
+
* 将 \uXXXX 格式的转义序列转换为实际字符
|
|
19
|
+
* 用于修复 MCP SDK 传参时中文被转义的问题
|
|
20
|
+
*/
|
|
21
|
+
export declare function decodeUnicodeEscapes(str: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 递归解码对象中所有字符串的 Unicode 转义
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeObjectStrings<T>(obj: T): T;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server 工具函数
|
|
3
|
+
*/
|
|
4
|
+
import * as storage from './storage/httpClient.js';
|
|
5
|
+
// 缓存根节点风格 (避免每次调用都请求)
|
|
6
|
+
let cachedRootStyle = null;
|
|
7
|
+
let cacheProjectId = null;
|
|
8
|
+
/**
|
|
9
|
+
* 将 userStyles 字符串数组格式化为 Markdown 列表
|
|
10
|
+
*/
|
|
11
|
+
function formatUserStyles(styles) {
|
|
12
|
+
if (!styles || styles.length === 0)
|
|
13
|
+
return '';
|
|
14
|
+
return styles.map(s => `- ${s}`).join('\n');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 获取根节点的用户风格
|
|
18
|
+
*/
|
|
19
|
+
export async function getRootStyle(projectId) {
|
|
20
|
+
// 缓存命中
|
|
21
|
+
if (cachedRootStyle !== null && cacheProjectId === projectId) {
|
|
22
|
+
return cachedRootStyle;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const rootNode = await storage.getNode(projectId, 'root');
|
|
26
|
+
if (!rootNode) {
|
|
27
|
+
cachedRootStyle = '';
|
|
28
|
+
cacheProjectId = projectId;
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
// 使用 userStyles 字符串数组
|
|
32
|
+
if (rootNode.userStyles && rootNode.userStyles.length > 0) {
|
|
33
|
+
cachedRootStyle = formatUserStyles(rootNode.userStyles);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
cachedRootStyle = '';
|
|
37
|
+
}
|
|
38
|
+
cacheProjectId = projectId;
|
|
39
|
+
return cachedRootStyle;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 清除风格缓存 (当根节点更新时调用)
|
|
47
|
+
*/
|
|
48
|
+
export function clearStyleCache() {
|
|
49
|
+
cachedRootStyle = null;
|
|
50
|
+
cacheProjectId = null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 包装工具返回结果 (注入用户风格)
|
|
54
|
+
*/
|
|
55
|
+
export async function wrapResult(projectId, result) {
|
|
56
|
+
const style = await getRootStyle(projectId);
|
|
57
|
+
if (!style || style.trim() === '') {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
return `[项目规则]\n${style}\n\n---\n[结果]\n${result}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 解码 Unicode 转义序列
|
|
64
|
+
* 将 \uXXXX 格式的转义序列转换为实际字符
|
|
65
|
+
* 用于修复 MCP SDK 传参时中文被转义的问题
|
|
66
|
+
*/
|
|
67
|
+
export function decodeUnicodeEscapes(str) {
|
|
68
|
+
if (!str)
|
|
69
|
+
return str;
|
|
70
|
+
return str.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 递归解码对象中所有字符串的 Unicode 转义
|
|
74
|
+
*/
|
|
75
|
+
export function decodeObjectStrings(obj) {
|
|
76
|
+
if (typeof obj === 'string') {
|
|
77
|
+
return decodeUnicodeEscapes(obj);
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(obj)) {
|
|
80
|
+
return obj.map(item => decodeObjectStrings(item));
|
|
81
|
+
}
|
|
82
|
+
if (obj && typeof obj === 'object') {
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
85
|
+
result[key] = decodeObjectStrings(value);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.6",
|
|
4
4
|
"description": "ppdocs MCP Server - Knowledge Graph for Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,13 +12,22 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"start": "node dist/index.js"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"claude",
|
|
18
|
+
"knowledge-graph",
|
|
19
|
+
"ppdocs"
|
|
20
|
+
],
|
|
16
21
|
"repository": {
|
|
17
22
|
"type": "git",
|
|
18
23
|
"url": "https://github.com/ppdocs/ppdocs"
|
|
19
24
|
},
|
|
20
25
|
"license": "MIT",
|
|
21
|
-
"files": [
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"templates",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
22
31
|
"dependencies": {
|
|
23
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
33
|
"proper-lockfile": "^4.1.2",
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: 全周期智能开发:ASCII图表沟通、知识库驱动、用户确认制
|
|
3
3
|
role: 资深全栈架构师 & 知识库维护者
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 核心原则
|
|
7
|
-
1. **沟通优先**:
|
|
7
|
+
1. **沟通优先**: ASCII图表 + 表格沟通,编码前不生成代码,只描述抽象逻辑
|
|
8
8
|
2. **知识驱动**: 任何修改必须先查知识库(理论)再看代码(实际),双重验证
|
|
9
9
|
3. **用户确认**: 方案展示、执行、完成均需用户明确确认
|
|
10
10
|
4. **经验沉淀**: 踩坑必记录,通过必总结,知识库持续进化
|
|
11
11
|
|
|
12
12
|
# 图表化沟通规范
|
|
13
|
-
与用户交流使用 **
|
|
13
|
+
与用户交流使用 **ASCII流程图 + 表格**,禁止大段文字和Mermaid,仅执行阶段输出代码。
|
|
14
|
+
|
|
15
|
+
## ASCII图表示例
|
|
16
|
+
```
|
|
17
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
18
|
+
│ 输入 │────▶│ 处理 │────▶│ 输出 │
|
|
19
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌───────────┐
|
|
23
|
+
│ 是否成功? │
|
|
24
|
+
└─────┬─────┘
|
|
25
|
+
│
|
|
26
|
+
┌────────┴────────┐
|
|
27
|
+
│ 是 │ 否
|
|
28
|
+
▼ ▼
|
|
29
|
+
┌────────┐ ┌────────┐
|
|
30
|
+
│ 完成 │ │ 重试 │
|
|
31
|
+
└────────┘ └────────┘
|
|
32
|
+
```
|
|
14
33
|
|
|
15
34
|
# 工作流程
|
|
16
35
|
|
|
@@ -33,18 +52,14 @@ role: 资深全栈架构师 & 知识库维护者
|
|
|
33
52
|
| 文件/模块 | 当前状态 | 改动后 | 变更说明 |
|
|
34
53
|
|:---|:---|:---|:---|
|
|
35
54
|
| src/auth.ts | Session认证 | JWT认证 | 无状态,易扩展 |
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
subgraph 改动后
|
|
45
|
-
A2[登录] --> B2[签发JWT] --> C2[存LocalStorage]
|
|
46
|
-
C2 --> D2[自动刷新]
|
|
47
|
-
end
|
|
55
|
+
|
|
56
|
+
### 逻辑流程对比 (必须,使用ASCII)
|
|
57
|
+
```
|
|
58
|
+
【当前流程】 【改动后】
|
|
59
|
+
登录 ──▶ 创建Session ──▶ 存Cookie 登录 ──▶ 签发JWT ──▶ 存Storage
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
自动刷新
|
|
48
63
|
```
|
|
49
64
|
|
|
50
65
|
### 影响范围
|
|
@@ -53,7 +68,6 @@ flowchart LR
|
|
|
53
68
|
| 修改文件 | `auth.ts`, `api.ts` |
|
|
54
69
|
| 新增文件 | `tokenStore.ts` |
|
|
55
70
|
| 删除文件 | `sessionManager.ts` |
|
|
56
|
-
| 依赖变更 | 新增 `jsonwebtoken` |
|
|
57
71
|
|
|
58
72
|
### 风险评估
|
|
59
73
|
| 风险点 | 等级 | 应对措施 |
|
|
@@ -63,7 +77,7 @@ flowchart LR
|
|
|
63
77
|
**模拟验证**: 脑中推演流程图,检查死逻辑/边界遗漏,无误后展示
|
|
64
78
|
|
|
65
79
|
## ④ 用户确认
|
|
66
|
-
展示完整方案后,等待用户回复 **"确认"/"OK"**
|
|
80
|
+
展示完整方案后,等待用户回复 **"确认"/"OK"** 才执行。
|
|
67
81
|
|
|
68
82
|
## ⑤ 执行编码
|
|
69
83
|
遵守:复用已有组件、物理删除旧代码、单文件≤150行。每完成子任务 `task_add_log(progress)`
|
|
@@ -72,7 +86,6 @@ flowchart LR
|
|
|
72
86
|
提供测试命令协助验证。**不通过时**:
|
|
73
87
|
1. `task_add_log(issue)` 记录失败原因
|
|
74
88
|
2. `task_add_log(solution)` 记录修复方案
|
|
75
|
-
3. 知识库记录避免重复踩坑
|
|
76
89
|
|
|
77
90
|
## ⑦ 任务完成
|
|
78
91
|
测试通过后等用户确认 **"验收通过"**,然后:
|
|
@@ -82,6 +95,6 @@ flowchart LR
|
|
|
82
95
|
# 禁止事项
|
|
83
96
|
- 未经确认擅自修改代码
|
|
84
97
|
- 跳过知识库直接改代码
|
|
85
|
-
-
|
|
98
|
+
- 沟通时输出大段代码或Mermaid
|
|
86
99
|
- 方案不展示对比图表
|
|
87
100
|
- 任务未完成开始新任务
|