@ppdocs/mcp 2.6.4 → 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.
@@ -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: node.x ?? 0,
68
- y: node.y ?? 0,
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
  }
@@ -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;
@@ -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: 0, // 服务端自动布局
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 { content: [{ type: 'text', text: JSON.stringify(node, null, 2) }] };
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 { content: [{ type: 'text', text: success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)' }] };
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 { content: [{ type: 'text', text: node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)' }] };
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 { content: [{ type: 'text', text: node ? JSON.stringify(node, null, 2) : '操作失败' }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '未找到路径' }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '没有孤立节点' }] };
137
+ return wrap(projectId, '没有孤立节点');
106
138
  }
107
- return { content: [{ type: 'text', text: JSON.stringify(orphans, null, 2) }] };
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 relations = await storage.getRelations(projectId, args.nodeId);
114
- if (relations.length === 0) {
115
- return { content: [{ type: 'text', text: '该节点没有任何连线' }] };
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
- const outgoing = relations.filter(r => r.direction === 'outgoing');
119
- const incoming = relations.filter(r => r.direction === 'incoming');
120
- const output = {
121
- outgoing: outgoing.map(r => ({ id: r.nodeId, title: r.title, desc: r.description, edge: r.edgeType })),
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: args.title,
136
- description: args.description,
137
- goals: args.goals || [],
138
- related_nodes: args.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 { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '任务不存在' }] };
216
+ return wrap(projectId, '任务不存在');
164
217
  }
165
- return { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
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
- const task = await storage.addTaskLog(projectId, args.taskId, args.log_type, args.content);
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 { content: [{ type: 'text', text: '添加失败(任务不存在或已归档)' }] };
230
+ return wrap(projectId, '添加失败(任务不存在或已归档)');
176
231
  }
177
- return { content: [{ type: 'text', text: `日志已添加,任务共有 ${task.logs.length} 条日志` }] };
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
- const task = await storage.completeTask(projectId, args.taskId, {
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 { content: [{ type: 'text', text: '完成失败(任务不存在或已归档)' }] };
254
+ return wrap(projectId, '完成失败(任务不存在或已归档)');
198
255
  }
199
- return { content: [{ type: 'text', text: `任务已完成归档: ${task.title}` }] };
256
+ return wrap(projectId, `任务已完成归档: ${task.title}`);
200
257
  });
201
258
  }
@@ -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.4",
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": ["mcp", "claude", "knowledge-graph", "ppdocs"],
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": ["dist", "templates", "README.md"],
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
- 与用户交流使用 **Mermaid图表 + 表格**,禁止大段文字,仅执行阶段输出代码。
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
- | src/store.ts | 无 | 新增TokenStore | 管理令牌刷新 |
37
-
38
- ### 逻辑流程对比 (必须)
39
- ```mermaid
40
- flowchart LR
41
- subgraph 当前
42
- A1[登录] --> B1[创建Session] --> C1[存Cookie]
43
- end
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
  - 任务未完成开始新任务