@ppdocs/mcp 2.6.3 → 2.6.5
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/storage/httpClient.d.ts +18 -0
- package/dist/storage/httpClient.js +94 -5
- package/dist/storage/types.d.ts +7 -0
- package/dist/tools/index.js +97 -40
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +111 -0
- package/package.json +12 -3
- package/templates/hooks/SystemPrompt.md +32 -19
|
@@ -13,6 +13,15 @@ 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?: Array<{
|
|
20
|
+
key: string;
|
|
21
|
+
value: string;
|
|
22
|
+
category: 'config' | 'style' | 'reference';
|
|
23
|
+
}>;
|
|
24
|
+
}): Promise<NodeData | null>;
|
|
16
25
|
deleteNode(nodeId: string): Promise<boolean>;
|
|
17
26
|
lockNode(nodeId: string, locked: boolean): Promise<NodeData | null>;
|
|
18
27
|
searchNodes(keywords: string[], limit?: number): Promise<SearchResult[]>;
|
|
@@ -51,6 +60,15 @@ export declare function listNodes(_projectId: string): Promise<NodeData[]>;
|
|
|
51
60
|
export declare function getNode(_projectId: string, nodeId: string): Promise<NodeData | null>;
|
|
52
61
|
export declare function createNode(_projectId: string, node: Partial<NodeData>): Promise<NodeData>;
|
|
53
62
|
export declare function updateNode(_projectId: string, nodeId: string, updates: Partial<NodeData>): Promise<NodeData | null>;
|
|
63
|
+
export declare function updateRoot(_projectId: string, updates: {
|
|
64
|
+
title?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
userStyles?: Array<{
|
|
67
|
+
key: string;
|
|
68
|
+
value: string;
|
|
69
|
+
category: 'config' | 'style' | 'reference';
|
|
70
|
+
}>;
|
|
71
|
+
}): Promise<NodeData | null>;
|
|
54
72
|
export declare function deleteNode(_projectId: string, nodeId: string): Promise<boolean>;
|
|
55
73
|
export declare function lockNode(_projectId: string, nodeId: string, locked: boolean): Promise<NodeData | null>;
|
|
56
74
|
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
|
@@ -17,6 +17,12 @@ export interface Dependency {
|
|
|
17
17
|
description: string;
|
|
18
18
|
nodePath?: string;
|
|
19
19
|
}
|
|
20
|
+
export type UserStyleCategory = 'config' | 'style' | 'reference';
|
|
21
|
+
export interface UserStyleItem {
|
|
22
|
+
key: string;
|
|
23
|
+
value: string;
|
|
24
|
+
category: UserStyleCategory;
|
|
25
|
+
}
|
|
20
26
|
export interface VersionRecord {
|
|
21
27
|
version: number;
|
|
22
28
|
date: string;
|
|
@@ -45,6 +51,7 @@ export interface NodeData {
|
|
|
45
51
|
dataOutput?: DataRef;
|
|
46
52
|
dependencies: Dependency[];
|
|
47
53
|
relatedFiles?: string[];
|
|
54
|
+
userStyles?: UserStyleItem[];
|
|
48
55
|
createdAt?: string;
|
|
49
56
|
updatedAt?: string;
|
|
50
57
|
lastAccessedAt?: string;
|
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,37 @@ 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.object({
|
|
75
|
+
key: z.string().describe('配置键'),
|
|
76
|
+
value: z.string().describe('配置值'),
|
|
77
|
+
category: z.enum(['config', 'style', 'reference']).describe('分类: config=基本规则, style=编码风格, reference=测试参数')
|
|
78
|
+
})).optional().describe('用户风格配置(完全替换,非合并)')
|
|
79
|
+
}, async (args) => {
|
|
80
|
+
// 空参数检查
|
|
81
|
+
if (args.title === undefined && args.description === undefined && args.userStyles === undefined) {
|
|
82
|
+
return wrap(projectId, '❌ 请至少提供一个更新参数(title/description/userStyles)');
|
|
83
|
+
}
|
|
84
|
+
const node = await storage.updateRoot(projectId, {
|
|
85
|
+
title: args.title,
|
|
86
|
+
description: args.description,
|
|
87
|
+
userStyles: args.userStyles
|
|
88
|
+
});
|
|
89
|
+
if (node)
|
|
90
|
+
clearStyleCache();
|
|
91
|
+
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(根节点已锁定)');
|
|
60
92
|
});
|
|
61
93
|
// 4. 锁定节点 (只能锁定,解锁需用户在前端手动操作)
|
|
62
94
|
server.tool('kg_lock_node', '锁定节点(锁定后只能读取,解锁需用户在前端手动操作)', {
|
|
63
95
|
nodeId: z.string().describe('节点ID')
|
|
64
96
|
}, async (args) => {
|
|
65
97
|
const node = await storage.lockNode(projectId, args.nodeId, true); // 强制锁定
|
|
66
|
-
return
|
|
98
|
+
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '操作失败');
|
|
67
99
|
});
|
|
68
100
|
// 5. 搜索节点
|
|
69
101
|
server.tool('kg_search', '关键词搜索节点,按相关度排序返回', {
|
|
@@ -75,7 +107,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
75
107
|
id: r.node.id, title: r.node.title, type: r.node.type,
|
|
76
108
|
status: r.node.status, score: r.score.toFixed(1), matches: r.matches
|
|
77
109
|
}));
|
|
78
|
-
return
|
|
110
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
79
111
|
});
|
|
80
112
|
// 6. 路径查找
|
|
81
113
|
server.tool('kg_find_path', '查找两节点间的依赖路径', {
|
|
@@ -84,44 +116,63 @@ export function registerTools(server, projectId, _user) {
|
|
|
84
116
|
}, async (args) => {
|
|
85
117
|
const result = await storage.findPath(projectId, args.startId, args.endId);
|
|
86
118
|
if (!result)
|
|
87
|
-
return
|
|
119
|
+
return wrap(projectId, '未找到路径');
|
|
88
120
|
const output = {
|
|
89
121
|
pathLength: result.path.length,
|
|
90
122
|
nodes: result.path.map(n => ({ id: n.id, title: n.title, type: n.type })),
|
|
91
123
|
edges: result.edges.map(e => ({ from: e.source, to: e.target, type: e.type }))
|
|
92
124
|
};
|
|
93
|
-
return
|
|
125
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
94
126
|
});
|
|
95
127
|
// 7. 列出所有节点
|
|
96
128
|
server.tool('kg_list_nodes', '列出项目全部节点概览', {}, async () => {
|
|
97
129
|
const nodes = await storage.listNodes(projectId);
|
|
98
130
|
const output = nodes.map(n => ({ id: n.id, title: n.title, type: n.type, status: n.status, locked: n.locked }));
|
|
99
|
-
return
|
|
131
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
100
132
|
});
|
|
101
133
|
// 8. 查找孤立节点
|
|
102
134
|
server.tool('kg_find_orphans', '查找无连线的孤立节点(用于清理)', {}, async () => {
|
|
103
135
|
const orphans = await storage.findOrphans(projectId);
|
|
104
136
|
if (orphans.length === 0) {
|
|
105
|
-
return
|
|
137
|
+
return wrap(projectId, '没有孤立节点');
|
|
106
138
|
}
|
|
107
|
-
return
|
|
139
|
+
return wrap(projectId, JSON.stringify(orphans, null, 2));
|
|
108
140
|
});
|
|
109
|
-
// 9. 查询节点关系网
|
|
110
|
-
server.tool('kg_get_relations', '获取节点的上下游关系(谁依赖它/它依赖谁)', {
|
|
111
|
-
nodeId: z.string().describe('节点ID')
|
|
141
|
+
// 9. 查询节点关系网 (支持多层)
|
|
142
|
+
server.tool('kg_get_relations', '获取节点的上下游关系(谁依赖它/它依赖谁),支持多层查询', {
|
|
143
|
+
nodeId: z.string().describe('节点ID'),
|
|
144
|
+
depth: z.number().min(1).max(3).optional().describe('查询层数(默认1,最大3)')
|
|
112
145
|
}, async (args) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
const maxDepth = Math.min(args.depth || 1, 3);
|
|
147
|
+
const visited = new Set();
|
|
148
|
+
const outgoing = [];
|
|
149
|
+
const incoming = [];
|
|
150
|
+
// 递归获取关系
|
|
151
|
+
async function fetchRelations(nodeId, currentDepth, direction) {
|
|
152
|
+
if (currentDepth > maxDepth || visited.has(`${nodeId}-${direction}`))
|
|
153
|
+
return;
|
|
154
|
+
visited.add(`${nodeId}-${direction}`);
|
|
155
|
+
const relations = await storage.getRelations(projectId, nodeId);
|
|
156
|
+
for (const r of relations) {
|
|
157
|
+
if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
|
|
158
|
+
if (!outgoing.some(o => o.id === r.nodeId)) {
|
|
159
|
+
outgoing.push({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType, depth: currentDepth });
|
|
160
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'outgoing');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
|
|
164
|
+
if (!incoming.some(i => i.id === r.nodeId)) {
|
|
165
|
+
incoming.push({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType, depth: currentDepth });
|
|
166
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'incoming');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
116
170
|
}
|
|
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) }] };
|
|
171
|
+
await fetchRelations(args.nodeId, 1, 'both');
|
|
172
|
+
if (outgoing.length === 0 && incoming.length === 0) {
|
|
173
|
+
return wrap(projectId, '该节点没有任何连线');
|
|
174
|
+
}
|
|
175
|
+
return wrap(projectId, JSON.stringify({ outgoing, incoming }, null, 2));
|
|
125
176
|
});
|
|
126
177
|
// ===================== 任务管理工具 =====================
|
|
127
178
|
// 10. 创建任务
|
|
@@ -131,13 +182,15 @@ export function registerTools(server, projectId, _user) {
|
|
|
131
182
|
goals: z.array(z.string()).optional().describe('目标清单'),
|
|
132
183
|
related_nodes: z.array(z.string()).optional().describe('关联节点ID')
|
|
133
184
|
}, async (args) => {
|
|
185
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
186
|
+
const decoded = decodeObjectStrings(args);
|
|
134
187
|
const task = await storage.createTask(projectId, {
|
|
135
|
-
title:
|
|
136
|
-
description:
|
|
137
|
-
goals:
|
|
138
|
-
related_nodes:
|
|
188
|
+
title: decoded.title,
|
|
189
|
+
description: decoded.description,
|
|
190
|
+
goals: decoded.goals || [],
|
|
191
|
+
related_nodes: decoded.related_nodes
|
|
139
192
|
}, _user);
|
|
140
|
-
return
|
|
193
|
+
return wrap(projectId, JSON.stringify(task, null, 2));
|
|
141
194
|
});
|
|
142
195
|
// 11. 列出任务
|
|
143
196
|
server.tool('task_list', '列出任务(active=进行中,archived=已归档)', {
|
|
@@ -152,7 +205,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
152
205
|
created_at: t.created_at,
|
|
153
206
|
last_log: t.last_log
|
|
154
207
|
}));
|
|
155
|
-
return
|
|
208
|
+
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
156
209
|
});
|
|
157
210
|
// 12. 获取任务详情
|
|
158
211
|
server.tool('task_get', '获取任务完整信息(含全部日志)', {
|
|
@@ -160,9 +213,9 @@ export function registerTools(server, projectId, _user) {
|
|
|
160
213
|
}, async (args) => {
|
|
161
214
|
const task = await storage.getTask(projectId, args.taskId);
|
|
162
215
|
if (!task) {
|
|
163
|
-
return
|
|
216
|
+
return wrap(projectId, '任务不存在');
|
|
164
217
|
}
|
|
165
|
-
return
|
|
218
|
+
return wrap(projectId, JSON.stringify(task, null, 2));
|
|
166
219
|
});
|
|
167
220
|
// 13. 添加任务日志
|
|
168
221
|
server.tool('task_add_log', '记录任务进展/问题/方案/参考', {
|
|
@@ -170,11 +223,13 @@ export function registerTools(server, projectId, _user) {
|
|
|
170
223
|
log_type: z.enum(['progress', 'issue', 'solution', 'reference']).describe('日志类型'),
|
|
171
224
|
content: z.string().describe('日志内容(Markdown)')
|
|
172
225
|
}, async (args) => {
|
|
173
|
-
|
|
226
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
227
|
+
const decodedContent = decodeUnicodeEscapes(args.content);
|
|
228
|
+
const task = await storage.addTaskLog(projectId, args.taskId, args.log_type, decodedContent);
|
|
174
229
|
if (!task) {
|
|
175
|
-
return
|
|
230
|
+
return wrap(projectId, '添加失败(任务不存在或已归档)');
|
|
176
231
|
}
|
|
177
|
-
return
|
|
232
|
+
return wrap(projectId, `日志已添加,任务共有 ${task.logs.length} 条日志`);
|
|
178
233
|
});
|
|
179
234
|
// 14. 完成任务
|
|
180
235
|
server.tool('task_complete', '完成任务并归档,填写经验总结', {
|
|
@@ -187,15 +242,17 @@ export function registerTools(server, projectId, _user) {
|
|
|
187
242
|
url: z.string().optional()
|
|
188
243
|
})).optional().describe('参考资料')
|
|
189
244
|
}, async (args) => {
|
|
190
|
-
|
|
245
|
+
// 解码 Unicode 转义 (修复 MCP 传参中文乱码)
|
|
246
|
+
const decoded = decodeObjectStrings({
|
|
191
247
|
summary: args.summary,
|
|
192
248
|
difficulties: args.difficulties || [],
|
|
193
249
|
solutions: args.solutions || [],
|
|
194
250
|
references: args.references || []
|
|
195
251
|
});
|
|
252
|
+
const task = await storage.completeTask(projectId, args.taskId, decoded);
|
|
196
253
|
if (!task) {
|
|
197
|
-
return
|
|
254
|
+
return wrap(projectId, '完成失败(任务不存在或已归档)');
|
|
198
255
|
}
|
|
199
|
-
return
|
|
256
|
+
return wrap(projectId, `任务已完成归档: ${task.title}`);
|
|
200
257
|
});
|
|
201
258
|
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server 工具函数
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 获取根节点的用户风格 (优先 userStyles, 兼容 description)
|
|
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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server 工具函数
|
|
3
|
+
*/
|
|
4
|
+
import * as storage from './storage/httpClient.js';
|
|
5
|
+
// 缓存根节点风格 (避免每次调用都请求)
|
|
6
|
+
let cachedRootStyle = null;
|
|
7
|
+
let cacheProjectId = null;
|
|
8
|
+
// 风格分类标签
|
|
9
|
+
const STYLE_CATEGORY_LABELS = {
|
|
10
|
+
config: '配置',
|
|
11
|
+
style: '风格',
|
|
12
|
+
reference: '参考'
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 将 userStyles 数组格式化为 Markdown 文本
|
|
16
|
+
*/
|
|
17
|
+
function formatUserStyles(styles) {
|
|
18
|
+
if (!styles || styles.length === 0)
|
|
19
|
+
return '';
|
|
20
|
+
const groups = { config: [], style: [], reference: [] };
|
|
21
|
+
styles.forEach(s => {
|
|
22
|
+
if (groups[s.category])
|
|
23
|
+
groups[s.category].push(s);
|
|
24
|
+
});
|
|
25
|
+
const lines = [];
|
|
26
|
+
for (const [cat, items] of Object.entries(groups)) {
|
|
27
|
+
if (items.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
lines.push(`### ${STYLE_CATEGORY_LABELS[cat] || cat}`);
|
|
30
|
+
items.forEach(item => lines.push(`- **${item.key}**: ${item.value}`));
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
return lines.join('\n').trim();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 获取根节点的用户风格 (优先 userStyles, 兼容 description)
|
|
37
|
+
*/
|
|
38
|
+
export async function getRootStyle(projectId) {
|
|
39
|
+
// 缓存命中
|
|
40
|
+
if (cachedRootStyle !== null && cacheProjectId === projectId) {
|
|
41
|
+
return cachedRootStyle;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const rootNode = await storage.getNode(projectId, 'root');
|
|
45
|
+
if (!rootNode) {
|
|
46
|
+
cachedRootStyle = '';
|
|
47
|
+
cacheProjectId = projectId;
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
// 优先使用结构化 userStyles
|
|
51
|
+
if (rootNode.userStyles && rootNode.userStyles.length > 0) {
|
|
52
|
+
cachedRootStyle = formatUserStyles(rootNode.userStyles);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// 兼容旧版: 使用 description 字段
|
|
56
|
+
cachedRootStyle = rootNode.description || '';
|
|
57
|
+
}
|
|
58
|
+
cacheProjectId = projectId;
|
|
59
|
+
return cachedRootStyle;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 清除风格缓存 (当根节点更新时调用)
|
|
67
|
+
*/
|
|
68
|
+
export function clearStyleCache() {
|
|
69
|
+
cachedRootStyle = null;
|
|
70
|
+
cacheProjectId = null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 包装工具返回结果 (注入用户风格)
|
|
74
|
+
*/
|
|
75
|
+
export async function wrapResult(projectId, result) {
|
|
76
|
+
const style = await getRootStyle(projectId);
|
|
77
|
+
// 跳过空白或默认模板内容
|
|
78
|
+
if (!style || style.trim() === '' || /^#\s+\S+\s*\n\n(项目根节点|Project root)?\s*$/.test(style)) {
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
return `[项目风格]\n${style}\n\n---\n[结果]\n${result}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 解码 Unicode 转义序列
|
|
85
|
+
* 将 \uXXXX 格式的转义序列转换为实际字符
|
|
86
|
+
* 用于修复 MCP SDK 传参时中文被转义的问题
|
|
87
|
+
*/
|
|
88
|
+
export function decodeUnicodeEscapes(str) {
|
|
89
|
+
if (!str)
|
|
90
|
+
return str;
|
|
91
|
+
return str.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 递归解码对象中所有字符串的 Unicode 转义
|
|
95
|
+
*/
|
|
96
|
+
export function decodeObjectStrings(obj) {
|
|
97
|
+
if (typeof obj === 'string') {
|
|
98
|
+
return decodeUnicodeEscapes(obj);
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(obj)) {
|
|
101
|
+
return obj.map(item => decodeObjectStrings(item));
|
|
102
|
+
}
|
|
103
|
+
if (obj && typeof obj === 'object') {
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
106
|
+
result[key] = decodeObjectStrings(value);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
return obj;
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.5",
|
|
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
|
- 任务未完成开始新任务
|