@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 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: 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
  }
@@ -45,6 +45,7 @@ export interface NodeData {
45
45
  dataOutput?: DataRef;
46
46
  dependencies: Dependency[];
47
47
  relatedFiles?: string[];
48
+ userStyles?: string[];
48
49
  createdAt?: string;
49
50
  updatedAt?: string;
50
51
  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,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 { 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.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 { content: [{ type: 'text', text: node ? JSON.stringify(node, null, 2) : '操作失败' }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '未找到路径' }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '没有孤立节点' }] };
133
+ return wrap(projectId, '没有孤立节点');
106
134
  }
107
- return { content: [{ type: 'text', text: JSON.stringify(orphans, null, 2) }] };
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 relations = await storage.getRelations(projectId, args.nodeId);
114
- if (relations.length === 0) {
115
- return { content: [{ type: 'text', text: '该节点没有任何连线' }] };
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
- 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) }] };
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: args.title,
136
- description: args.description,
137
- goals: args.goals || [],
138
- related_nodes: args.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 { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
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 { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
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 { content: [{ type: 'text', text: '任务不存在' }] };
212
+ return wrap(projectId, '任务不存在');
164
213
  }
165
- return { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
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
- const task = await storage.addTaskLog(projectId, args.taskId, args.log_type, args.content);
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 { content: [{ type: 'text', text: '添加失败(任务不存在或已归档)' }] };
226
+ return wrap(projectId, '添加失败(任务不存在或已归档)');
176
227
  }
177
- return { content: [{ type: 'text', text: `日志已添加,任务共有 ${task.logs.length} 条日志` }] };
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
- const task = await storage.completeTask(projectId, args.taskId, {
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 { content: [{ type: 'text', text: '完成失败(任务不存在或已归档)' }] };
250
+ return wrap(projectId, '完成失败(任务不存在或已归档)');
198
251
  }
199
- return { content: [{ type: 'text', text: `任务已完成归档: ${task.title}` }] };
252
+ return wrap(projectId, `任务已完成归档: ${task.title}`);
200
253
  });
201
254
  }
@@ -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.4",
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": ["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
  - 任务未完成开始新任务