@ppdocs/mcp 3.10.0 → 3.12.0

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
@@ -90,7 +90,7 @@ function showHelp() {
90
90
  ppdocs MCP CLI
91
91
 
92
92
  Commands:
93
- init Full setup: .ppdocs + templates + MCP registration
93
+ init Full setup: .ppdocs + templates + MCP registration (Claude/Codex/OpenCode/Gemini)
94
94
  bind Lightweight: only create .ppdocs (for adding projects to existing MCP)
95
95
 
96
96
  Usage:
@@ -270,6 +270,13 @@ function autoRegisterMcp(cwd, apiUrl, user, skipGemini = false) {
270
270
  for (const pair of envPairs) {
271
271
  args.push(envFlag, pair);
272
272
  }
273
+ // macOS/Linux: 必须注入 PATH,否则 Claude Code 启动 MCP 时可能找不到 npx/node
274
+ // (Claude Code 进程的 PATH 通常不含 /opt/homebrew/bin 等 Homebrew/nvm 路径)
275
+ if (process.platform !== 'win32') {
276
+ const macExtraPaths = '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin';
277
+ const fullPath = `${process.env.PATH || '/usr/bin:/bin'}:${macExtraPaths}`;
278
+ args.push(envFlag, `PATH=${fullPath}`);
279
+ }
273
280
  args.push(serverName, '--', 'npx', '-y', '@ppdocs/mcp@latest');
274
281
  const result = spawnSync(cli, args, { stdio: 'inherit', shell: true });
275
282
  if (result.status !== 0)
@@ -303,6 +310,24 @@ function autoRegisterMcp(cwd, apiUrl, user, skipGemini = false) {
303
310
  console.log(`⚠️ Codex MCP registration failed`);
304
311
  }
305
312
  }
313
+ // OpenCode CLI (opencode mcp add 支持 -e 环境变量注入)
314
+ if (commandExists('opencode')) {
315
+ detected.push('OpenCode');
316
+ try {
317
+ execSilent(`opencode mcp remove ${serverName}`);
318
+ mcpAdd('opencode', '-e');
319
+ }
320
+ catch {
321
+ // fallback: 写入 ~/.opencode/mcp.json
322
+ try {
323
+ const opencodeConfig = path.join(os.homedir(), '.opencode', 'mcp.json');
324
+ createMcpConfigAt(opencodeConfig, apiUrl, { user, projectRoot: cwd });
325
+ }
326
+ catch {
327
+ console.log(`⚠️ OpenCode MCP registration failed`);
328
+ }
329
+ }
330
+ }
306
331
  // Gemini CLI (gemini mcp add 不支持 --env,直接写 settings.json)
307
332
  if (commandExists('gemini') && !skipGemini) {
308
333
  detected.push('Gemini');
@@ -315,7 +340,7 @@ function autoRegisterMcp(cwd, apiUrl, user, skipGemini = false) {
315
340
  }
316
341
  }
317
342
  if (detected.length === 0) {
318
- console.log(`ℹ️ No AI CLI detected (claude/codex/gemini), creating .mcp.json for manual config`);
343
+ console.log(`ℹ️ No AI CLI detected (claude/codex/opencode/gemini), creating .mcp.json for manual config`);
319
344
  return false;
320
345
  }
321
346
  console.log(`✅ Registered MCP for: ${detected.join(', ')}`);
package/dist/index.js CHANGED
@@ -19,6 +19,51 @@ import { loadConfig, generateAgentId } from './config.js';
19
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
20
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
21
21
  const VERSION = pkg.version;
22
+ /** 启动时连通性检查 — 非阻塞,3秒超时,结果输出到 stderr 供调试 */
23
+ async function checkConnectivity(apiUrl) {
24
+ // Node.js 18+ 内置 fetch; 旧版本跳过检查
25
+ if (typeof globalThis.fetch !== 'function') {
26
+ console.error(`[connectivity] ⏭️ fetch unavailable (Node.js < 18), skipping connectivity check`);
27
+ return;
28
+ }
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), 3000);
31
+ try {
32
+ const response = await fetch(apiUrl, { signal: controller.signal });
33
+ if (response.ok) {
34
+ console.error(`[connectivity] ✅ ppdocs server reachable`);
35
+ }
36
+ else {
37
+ console.error(`[connectivity] ⚠️ ppdocs server responded HTTP ${response.status} — check API key/project ID`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ const apiHost = apiUrl.replace(/\/api\/.*$/, '');
42
+ if (err instanceof Error && err.name === 'AbortError') {
43
+ console.error(`[connectivity] ❌ Connection TIMEOUT (3s) to ${apiHost}`);
44
+ console.error(`[connectivity] Possible causes:`);
45
+ console.error(`[connectivity] 1. ppdocs desktop app is not running on the target machine`);
46
+ console.error(`[connectivity] 2. Firewall blocking port (check Windows Firewall inbound rules)`);
47
+ console.error(`[connectivity] 3. IP address changed (re-run init with correct --api)`);
48
+ console.error(`[connectivity] 4. Not on the same network`);
49
+ }
50
+ else {
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ if (msg.includes('ECONNREFUSED')) {
53
+ console.error(`[connectivity] ❌ Connection REFUSED to ${apiHost}`);
54
+ console.error(`[connectivity] ppdocs desktop app is not running, or port is wrong`);
55
+ }
56
+ else {
57
+ console.error(`[connectivity] ❌ Cannot reach ${apiHost}: ${msg}`);
58
+ }
59
+ }
60
+ console.error(`[connectivity] API URL: ${apiUrl}`);
61
+ console.error(`[connectivity] MCP will still start — tools will fail until server is reachable`);
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ }
66
+ }
22
67
  // 检查是否为 CLI 命令
23
68
  const args = process.argv.slice(2);
24
69
  if (args.length > 0) {
@@ -32,7 +77,9 @@ async function main() {
32
77
  // 有配置 → 自动初始化 (环境变量 / .ppdocs)
33
78
  if (config) {
34
79
  initClient(config.apiUrl);
35
- console.error(`ppdocs MCP v${VERSION} | project: ${config.projectId} | user: ${config.user}`);
80
+ console.error(`ppdocs MCP v${VERSION} | project: ${config.projectId} | user: ${config.user} | source: ${config.source}`);
81
+ // 启动时连通性检查 (非阻塞, 3秒超时)
82
+ checkConnectivity(config.apiUrl).catch(() => { });
36
83
  }
37
84
  else {
38
85
  console.error(`ppdocs MCP v${VERSION} | ⏳ 等待 kg_init 初始化项目上下文...`);
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * API URL 格式: http://localhost:20099/api/:projectId/:password/...
6
6
  */
7
- import type { Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, WorkflowSummary, WorkflowDoc, WorkflowSelector, Reference } from './types.js';
7
+ import type { Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, WorkflowSummary, WorkflowDoc, WorkflowSelector, Reference, FetchResult, FileImportResult, Pitfall, PitfallSummary } from './types.js';
8
8
  export declare class PpdocsApiClient {
9
9
  private baseUrl;
10
10
  private serverUrl;
@@ -31,6 +31,27 @@ export declare class PpdocsApiClient {
31
31
  readReferenceFile(path: string): Promise<string>;
32
32
  /** 复制本地文件到参考文件库, 返回相对路径列表 */
33
33
  copyReferenceFiles(paths: string[], targetDir?: string): Promise<string[]>;
34
+ /** 从 URL 拉取文档内容并保存为参考 */
35
+ fetchRefUrl(url: string, refId?: string): Promise<FetchResult>;
36
+ /** 导入本地文件内容到参考系统 */
37
+ importLocalFile(refId: string, sourcePath: string): Promise<FileImportResult>;
38
+ listPitfalls(): Promise<PitfallSummary[]>;
39
+ getPitfall(pitfallId: string): Promise<Pitfall | null>;
40
+ createPitfall(input: {
41
+ title: string;
42
+ category?: string;
43
+ symptom?: string;
44
+ root_cause?: string;
45
+ solution?: string;
46
+ prevention?: string;
47
+ severity?: string;
48
+ related_nodes?: string[];
49
+ source_task?: string;
50
+ tags?: string[];
51
+ }): Promise<Pitfall>;
52
+ savePitfall(pitfall: Pitfall): Promise<boolean>;
53
+ deletePitfall(pitfallId: string): Promise<boolean>;
54
+ searchPitfalls(query: string, status?: string, category?: string): Promise<PitfallSummary[]>;
34
55
  listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
35
56
  getTask(taskId: string, mode?: 'smart' | 'full'): Promise<Task | null>;
36
57
  createTask(task: {
@@ -168,6 +168,56 @@ export class PpdocsApiClient {
168
168
  body: JSON.stringify({ paths, target_dir: targetDir }),
169
169
  });
170
170
  }
171
+ /** 从 URL 拉取文档内容并保存为参考 */
172
+ async fetchRefUrl(url, refId) {
173
+ return this.request('/refs/fetch-url', {
174
+ method: 'POST',
175
+ body: JSON.stringify({ url, ref_id: refId }),
176
+ });
177
+ }
178
+ /** 导入本地文件内容到参考系统 */
179
+ async importLocalFile(refId, sourcePath) {
180
+ return this.request('/refs/import-file', {
181
+ method: 'POST',
182
+ body: JSON.stringify({ ref_id: refId, source_path: sourcePath }),
183
+ });
184
+ }
185
+ // ============ 踩坑经验 API ============
186
+ async listPitfalls() {
187
+ return this.request('/pitfalls');
188
+ }
189
+ async getPitfall(pitfallId) {
190
+ try {
191
+ return await this.request(`/pitfalls/${encodeURIComponent(pitfallId)}`);
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
197
+ async createPitfall(input) {
198
+ return this.request('/pitfalls', {
199
+ method: 'POST',
200
+ body: JSON.stringify(input),
201
+ });
202
+ }
203
+ async savePitfall(pitfall) {
204
+ await this.request(`/pitfalls/${encodeURIComponent(pitfall.id)}`, {
205
+ method: 'PUT',
206
+ body: JSON.stringify(pitfall),
207
+ });
208
+ return true;
209
+ }
210
+ async deletePitfall(pitfallId) {
211
+ return this.request(`/pitfalls/${encodeURIComponent(pitfallId)}`, {
212
+ method: 'DELETE',
213
+ });
214
+ }
215
+ async searchPitfalls(query, status, category) {
216
+ return this.request('/pitfalls/search', {
217
+ method: 'POST',
218
+ body: JSON.stringify({ query, status, category }),
219
+ });
220
+ }
171
221
  // ============ 任务管理 ============
172
222
  async listTasks(status) {
173
223
  const query = status ? `?status=${status}` : '';
@@ -45,6 +45,41 @@ export interface Reference {
45
45
  adoptedBy: AdoptedNode[];
46
46
  createdAt: string;
47
47
  updatedAt: string;
48
+ sourceUrl?: string;
49
+ fetchHistory?: FetchRecord[];
50
+ fileImportHistory?: FileImportRecord[];
51
+ }
52
+ export interface FetchRecord {
53
+ fetchedAt: string;
54
+ url: string;
55
+ title: string;
56
+ contentFile: string;
57
+ contentLength: number;
58
+ contentPreview: string;
59
+ }
60
+ export interface FetchResult {
61
+ refId: string;
62
+ title: string;
63
+ contentFile: string;
64
+ contentLength: number;
65
+ contentPreview: string;
66
+ fetchCount: number;
67
+ }
68
+ export interface FileImportRecord {
69
+ importedAt: string;
70
+ sourcePath: string;
71
+ storedFile: string;
72
+ contentLength: number;
73
+ contentPreview: string;
74
+ fileType: string;
75
+ }
76
+ export interface FileImportResult {
77
+ refId: string;
78
+ sourcePath: string;
79
+ storedFile: string;
80
+ contentLength: number;
81
+ contentPreview: string;
82
+ importCount: number;
48
83
  }
49
84
  export interface FileInfo {
50
85
  name: string;
@@ -95,3 +130,30 @@ export interface TaskSummary {
95
130
  updated_at: string;
96
131
  last_log?: string;
97
132
  }
133
+ export interface Pitfall {
134
+ id: string;
135
+ title: string;
136
+ category: string;
137
+ symptom: string;
138
+ root_cause: string;
139
+ solution: string;
140
+ prevention: string;
141
+ related_nodes: string[];
142
+ source_task?: string;
143
+ severity: string;
144
+ status: string;
145
+ tags: string[];
146
+ created_at: string;
147
+ updated_at: string;
148
+ resolved_at?: string;
149
+ }
150
+ export interface PitfallSummary {
151
+ id: string;
152
+ title: string;
153
+ category: string;
154
+ severity: string;
155
+ status: string;
156
+ tags: string[];
157
+ created_at: string;
158
+ updated_at: string;
159
+ }
@@ -318,6 +318,7 @@ function findDirectedPath(chart, fromId, toId) {
318
318
  export function registerFlowchartTools(server, _ctx) {
319
319
  const client = () => getClient();
320
320
  server.tool('kg_flowchart', '🔀 逻辑流程图 — 项目知识图谱的核心。每个节点代表一个模块/函数/概念,节点可绑定文件、内嵌版本化文档。\n' +
321
+ '★★ 编码前必须先设计:用 batch_add/update_node 在流程图中设计逻辑并验证每个节点正确后才能编码。执行 kg_workflow(id:"design-first") 查看完整工作流 ★★\n' +
321
322
  '⚡ 开始任何任务前必须先查图谱:search 搜关键词 → get_node 看详情 → 有 subFlowchart 则递归下探。\n' +
322
323
  '📝 完成修改后必须回写:bind 绑定文件 → update_node 更新描述和文档 → 新模块用 batch_add。\n' +
323
324
  'actions: list|get|search|get_relations|find_path|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 14 个工具, 8 个子模块
3
+ * 15 个工具, 9 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
7
  * 📚 知识: kg_projects, kg_workflow (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref, kg_pitfall (5个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
10
  * 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
11
11
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 14 个工具, 8 个子模块
3
+ * 15 个工具, 9 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
7
  * 📚 知识: kg_projects, kg_workflow (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
8
+ * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref, kg_pitfall (5个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
10
  * 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
11
11
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
@@ -20,6 +20,7 @@ import { registerTaskTools } from './tasks.js';
20
20
  import { registerFileTools } from './files.js';
21
21
  import { registerDiscussionTools } from './discussion.js';
22
22
  import { registerReferenceTools } from './refs.js';
23
+ import { registerPitfallTools } from './pitfalls.js';
23
24
  import { registerAnalyzerTools } from './analyzer.js';
24
25
  import { registerMeetingTools } from './meeting.js';
25
26
  import { registerFlowchartTools } from './flowchart.js';
@@ -38,6 +39,7 @@ export function registerTools(server, projectId, user, agentId, onProjectChange)
38
39
  registerFileTools(server);
39
40
  registerDiscussionTools(server, ctx);
40
41
  registerReferenceTools(server);
42
+ registerPitfallTools(server);
41
43
  // 🔬 代码分析
42
44
  registerAnalyzerTools(server, ctx);
43
45
  // 🏛️ 多AI协作
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 💡 kg_pitfall — 踩坑经验管理
3
+ * 记录项目中遇到的问题、根因和解决方案,避免重复踩坑
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ export declare function registerPitfallTools(server: McpServer): void;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * 💡 kg_pitfall — 踩坑经验管理
3
+ * 记录项目中遇到的问题、根因和解决方案,避免重复踩坑
4
+ */
5
+ import { z } from 'zod';
6
+ import { getClient } from '../storage/httpClient.js';
7
+ import { decodeObjectStrings } from '../utils.js';
8
+ import { safeTool, wrap } from './shared.js';
9
+ const SEVERITY_ICONS = {
10
+ critical: '🔴',
11
+ warning: '🟡',
12
+ info: '🔵',
13
+ };
14
+ const STATUS_ICONS = {
15
+ open: '🔓',
16
+ resolved: '✅',
17
+ recurring: '🔄',
18
+ };
19
+ export function registerPitfallTools(server) {
20
+ const client = () => getClient();
21
+ server.tool('kg_pitfall', '💡 踩坑经验 — 记录和查询项目中的踩坑经验,避免重复踩坑。\n' +
22
+ '遇到问题时用 create 记录现象和根因,找到方案后用 resolve 标记解决。\n' +
23
+ '下次遇到类似问题先 search 查历史经验。\n' +
24
+ 'actions: create|get|update|search|resolve|list|delete', {
25
+ action: z.enum(['create', 'get', 'update', 'search', 'resolve', 'list', 'delete'])
26
+ .describe('操作类型'),
27
+ id: z.string().optional().describe('踩坑经验 ID (get/update/resolve/delete)'),
28
+ title: z.string().optional().describe('标题 (create)'),
29
+ category: z.string().optional().describe('分类: protocol/concurrency/config/logic/ui/other (create/update/search)'),
30
+ symptom: z.string().optional().describe('现象描述 Markdown (create/update)'),
31
+ root_cause: z.string().optional().describe('根因分析 Markdown (create/update)'),
32
+ solution: z.string().optional().describe('解决方案 Markdown (create/update/resolve)'),
33
+ prevention: z.string().optional().describe('预防措施 (create/update)'),
34
+ severity: z.string().optional().describe('严重程度: critical/warning/info (create/update)'),
35
+ related_nodes: z.array(z.string()).optional().describe('关联流程图节点 ID 列表 (create/update)'),
36
+ source_task: z.string().optional().describe('来源任务 ID (create)'),
37
+ tags: z.array(z.string()).optional().describe('标签列表 (create/update)'),
38
+ query: z.string().optional().describe('搜索关键词 (search)'),
39
+ status: z.string().optional().describe('状态过滤: open/resolved/recurring (search/list)'),
40
+ }, async (args) => safeTool(async () => {
41
+ const decoded = decodeObjectStrings(args);
42
+ switch (decoded.action) {
43
+ case 'list': {
44
+ const pitfalls = decoded.status || decoded.category
45
+ ? await client().searchPitfalls('', decoded.status, decoded.category)
46
+ : await client().listPitfalls();
47
+ if (pitfalls.length === 0)
48
+ return wrap('当前没有踩坑经验记录');
49
+ const lines = [
50
+ `💡 踩坑经验 (${pitfalls.length})`,
51
+ '',
52
+ '| 状态 | 严重 | 标题 | 分类 | 标签 | 更新 |',
53
+ '|:-----|:-----|:-----|:-----|:-----|:-----|',
54
+ ];
55
+ for (const p of pitfalls) {
56
+ const statusIcon = STATUS_ICONS[p.status] || p.status;
57
+ const sevIcon = SEVERITY_ICONS[p.severity] || p.severity;
58
+ const tags = p.tags.length > 0 ? p.tags.join(',') : '-';
59
+ const date = p.updated_at.substring(0, 10);
60
+ lines.push(`| ${statusIcon} | ${sevIcon} | ${p.title} | ${p.category} | ${tags} | ${date} |`);
61
+ }
62
+ return wrap(lines.join('\n'));
63
+ }
64
+ case 'get': {
65
+ if (!decoded.id)
66
+ return wrap('❌ get 需要 id');
67
+ const p = await client().getPitfall(decoded.id);
68
+ if (!p)
69
+ return wrap(`未找到踩坑经验: ${decoded.id}`);
70
+ const sevIcon = SEVERITY_ICONS[p.severity] || p.severity;
71
+ const statusIcon = STATUS_ICONS[p.status] || p.status;
72
+ const lines = [
73
+ `# ${sevIcon} ${p.title}`,
74
+ '',
75
+ `| 字段 | 值 |`,
76
+ `|:---|:---|`,
77
+ `| ID | ${p.id} |`,
78
+ `| 状态 | ${statusIcon} ${p.status} |`,
79
+ `| 严重程度 | ${sevIcon} ${p.severity} |`,
80
+ `| 分类 | ${p.category} |`,
81
+ `| 标签 | ${p.tags.join(', ') || '-'} |`,
82
+ `| 关联节点 | ${p.related_nodes.join(', ') || '-'} |`,
83
+ ];
84
+ if (p.source_task)
85
+ lines.push(`| 来源任务 | ${p.source_task} |`);
86
+ lines.push(`| 创建 | ${p.created_at.substring(0, 19)} |`);
87
+ lines.push(`| 更新 | ${p.updated_at.substring(0, 19)} |`);
88
+ if (p.resolved_at)
89
+ lines.push(`| 解决 | ${p.resolved_at.substring(0, 19)} |`);
90
+ if (p.symptom) {
91
+ lines.push('', '## 现象', '', p.symptom);
92
+ }
93
+ if (p.root_cause) {
94
+ lines.push('', '## 根因', '', p.root_cause);
95
+ }
96
+ if (p.solution) {
97
+ lines.push('', '## 解决方案', '', p.solution);
98
+ }
99
+ if (p.prevention) {
100
+ lines.push('', '## 预防措施', '', p.prevention);
101
+ }
102
+ return wrap(lines.join('\n'));
103
+ }
104
+ case 'create': {
105
+ if (!decoded.title)
106
+ return wrap('❌ create 需要 title');
107
+ const pitfall = await client().createPitfall({
108
+ title: decoded.title,
109
+ category: decoded.category,
110
+ symptom: decoded.symptom,
111
+ root_cause: decoded.root_cause,
112
+ solution: decoded.solution,
113
+ prevention: decoded.prevention,
114
+ severity: decoded.severity,
115
+ related_nodes: decoded.related_nodes,
116
+ source_task: decoded.source_task,
117
+ tags: decoded.tags,
118
+ });
119
+ return wrap(`✅ 踩坑经验已创建\n\n- ID: ${pitfall.id}\n- 标题: ${pitfall.title}\n- 分类: ${pitfall.category}\n- 严重程度: ${pitfall.severity}`);
120
+ }
121
+ case 'update': {
122
+ if (!decoded.id)
123
+ return wrap('❌ update 需要 id');
124
+ const existing = await client().getPitfall(decoded.id);
125
+ if (!existing)
126
+ return wrap(`❌ 未找到踩坑经验: ${decoded.id}`);
127
+ const updated = {
128
+ ...existing,
129
+ ...(decoded.title && { title: decoded.title }),
130
+ ...(decoded.category && { category: decoded.category }),
131
+ ...(decoded.symptom && { symptom: decoded.symptom }),
132
+ ...(decoded.root_cause && { root_cause: decoded.root_cause }),
133
+ ...(decoded.solution && { solution: decoded.solution }),
134
+ ...(decoded.prevention && { prevention: decoded.prevention }),
135
+ ...(decoded.severity && { severity: decoded.severity }),
136
+ ...(decoded.related_nodes && { related_nodes: decoded.related_nodes }),
137
+ ...(decoded.tags && { tags: decoded.tags }),
138
+ updated_at: new Date().toISOString(),
139
+ };
140
+ await client().savePitfall(updated);
141
+ return wrap(`✅ 踩坑经验已更新 (${decoded.id})`);
142
+ }
143
+ case 'resolve': {
144
+ if (!decoded.id)
145
+ return wrap('❌ resolve 需要 id');
146
+ const existing = await client().getPitfall(decoded.id);
147
+ if (!existing)
148
+ return wrap(`❌ 未找到踩坑经验: ${decoded.id}`);
149
+ const now = new Date().toISOString();
150
+ const resolved = {
151
+ ...existing,
152
+ status: 'resolved',
153
+ resolved_at: now,
154
+ updated_at: now,
155
+ ...(decoded.solution && { solution: decoded.solution }),
156
+ ...(decoded.prevention && { prevention: decoded.prevention }),
157
+ };
158
+ await client().savePitfall(resolved);
159
+ return wrap(`✅ 踩坑经验已标记为解决 (${decoded.id})`);
160
+ }
161
+ case 'search': {
162
+ const query = decoded.query || '';
163
+ const results = await client().searchPitfalls(query, decoded.status, decoded.category);
164
+ if (results.length === 0)
165
+ return wrap(`未找到匹配的踩坑经验 (搜索: "${query}")`);
166
+ const lines = [
167
+ `💡 搜索结果: "${query}" (${results.length} 条)`,
168
+ '',
169
+ '| 状态 | 严重 | 标题 | 分类 | ID |',
170
+ '|:-----|:-----|:-----|:-----|:---|',
171
+ ];
172
+ for (const p of results) {
173
+ const statusIcon = STATUS_ICONS[p.status] || p.status;
174
+ const sevIcon = SEVERITY_ICONS[p.severity] || p.severity;
175
+ lines.push(`| ${statusIcon} | ${sevIcon} | ${p.title} | ${p.category} | ${p.id} |`);
176
+ }
177
+ lines.push('', '> 用 `kg_pitfall(get, id:"xxx")` 查看详情');
178
+ return wrap(lines.join('\n'));
179
+ }
180
+ case 'delete': {
181
+ if (!decoded.id)
182
+ return wrap('❌ delete 需要 id');
183
+ const ok = await client().deletePitfall(decoded.id);
184
+ return wrap(ok ? `✅ 踩坑经验已删除 (${decoded.id})` : `ℹ️ 未找到踩坑经验 (${decoded.id})`);
185
+ }
186
+ default:
187
+ return wrap(`❌ 未知 action: ${decoded.action}`);
188
+ }
189
+ }));
190
+ }
@@ -2,6 +2,13 @@ import { z } from 'zod';
2
2
  import { getClient } from '../storage/httpClient.js';
3
3
  import { decodeObjectStrings } from '../utils.js';
4
4
  import { safeTool, wrap } from './shared.js';
5
+ function formatBytes(bytes) {
6
+ if (bytes < 1024)
7
+ return `${bytes} B`;
8
+ if (bytes < 1048576)
9
+ return `${(bytes / 1024).toFixed(1)} KB`;
10
+ return `${(bytes / 1048576).toFixed(1)} MB`;
11
+ }
5
12
  const linkSchema = z.object({
6
13
  url: z.string(),
7
14
  label: z.string(),
@@ -19,8 +26,10 @@ export function registerReferenceTools(server) {
19
26
  const client = () => getClient();
20
27
  server.tool('kg_ref', '📎 外部参考 — 管理项目引用的第三方资料、链接、脚本和样例文件。\n' +
21
28
  '涉及参考资料时主动调用:list 查看已有 → get 读详情 → read_file 读附件内容 → save 新增。\n' +
22
- 'actions: list|get|save|delete|read_file', {
23
- action: z.enum(['list', 'get', 'save', 'delete', 'read_file']).describe('操作类型'),
29
+ '★ fetch: 输入URL自动拉取网页内容到本地,生成解析报告,支持持续更新(refetch)。\n' +
30
+ ' import_file: 导入本地文件内容到参考系统(自动保存内容副本+版本追踪)。\n' +
31
+ 'actions: list|get|save|delete|read_file|fetch|refetch|import_file|reimport', {
32
+ action: z.enum(['list', 'get', 'save', 'delete', 'read_file', 'fetch', 'refetch', 'import_file', 'reimport']).describe('操作类型'),
24
33
  id: z.string().optional().describe('参考 ID'),
25
34
  title: z.string().optional().describe('参考标题 (save)'),
26
35
  summary: z.string().optional().describe('Markdown 总结 (save)'),
@@ -28,7 +37,8 @@ export function registerReferenceTools(server) {
28
37
  files: z.array(fileSchema).optional().describe('参考文件 (save)'),
29
38
  scripts: z.array(fileSchema).optional().describe('解析脚本 (save)'),
30
39
  adoptedBy: z.array(adoptedNodeSchema).optional().describe('已采用该参考的节点 (save)'),
31
- path: z.string().optional().describe('参考文件路径 (read_file)'),
40
+ path: z.string().optional().describe('参考文件路径 (read_file/import_file)'),
41
+ url: z.string().optional().describe('要拉取的文档 URL (fetch/refetch)'),
32
42
  }, async (args) => safeTool(async () => {
33
43
  const decoded = decodeObjectStrings(args);
34
44
  switch (decoded.action) {
@@ -39,11 +49,12 @@ export function registerReferenceTools(server) {
39
49
  const lines = [
40
50
  `📎 外部参考 (${refs.length})`,
41
51
  '',
42
- '| ID | 标题 | 链接 | 文件 | 已采用 |',
43
- '|:---|:---|---:|---:|---:|',
52
+ '| ID | 标题 | 链接 | 文件 | 拉取 | 已采用 |',
53
+ '|:---|:---|---:|---:|---:|---:|',
44
54
  ];
45
55
  for (const ref of refs) {
46
- lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${ref.adoptedBy.length} |`);
56
+ const fetchCount = ref.fetchHistory?.length || 0;
57
+ lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${fetchCount} | ${ref.adoptedBy.length} |`);
47
58
  }
48
59
  return wrap(lines.join('\n'));
49
60
  }
@@ -61,9 +72,17 @@ export function registerReferenceTools(server) {
61
72
  `- Files: ${ref.files.length}`,
62
73
  `- Scripts: ${ref.scripts.length}`,
63
74
  `- AdoptedBy: ${ref.adoptedBy.length}`,
64
- '',
65
- ref.summary || '(无摘要)',
66
75
  ];
76
+ if (ref.sourceUrl) {
77
+ lines.push(`- SourceURL: ${ref.sourceUrl}`);
78
+ }
79
+ if (ref.fetchHistory && ref.fetchHistory.length > 0) {
80
+ lines.push(`- FetchCount: ${ref.fetchHistory.length} 次`);
81
+ lines.push(`- LastFetch: ${ref.fetchHistory[ref.fetchHistory.length - 1].fetchedAt}`);
82
+ }
83
+ lines.push(`- Created: ${ref.createdAt}`);
84
+ lines.push(`- Updated: ${ref.updatedAt}`);
85
+ lines.push('', ref.summary || '(无摘要)');
67
86
  if (ref.links.length > 0) {
68
87
  lines.push('', '## Links');
69
88
  for (const link of ref.links) {
@@ -82,6 +101,20 @@ export function registerReferenceTools(server) {
82
101
  lines.push(`- ${script.name}: ${script.path}`);
83
102
  }
84
103
  }
104
+ if (ref.fetchHistory && ref.fetchHistory.length > 0) {
105
+ lines.push('', '## Fetch History');
106
+ lines.push('| # | 时间 | 标题 | 大小 | 文件 |');
107
+ lines.push('|:--|:-----|:-----|-----:|:-----|');
108
+ ref.fetchHistory.forEach((h, i) => {
109
+ lines.push(`| ${i + 1} | ${h.fetchedAt.substring(0, 19)} | ${h.title} | ${formatBytes(h.contentLength)} | ${h.contentFile} |`);
110
+ });
111
+ }
112
+ if (ref.adoptedBy.length > 0) {
113
+ lines.push('', '## AdoptedBy');
114
+ for (const node of ref.adoptedBy) {
115
+ lines.push(`- ${node.nodeLabel} [${node.chartId}/${node.nodeId}]`);
116
+ }
117
+ }
85
118
  return wrap(lines.join('\n'));
86
119
  }
87
120
  case 'save': {
@@ -121,6 +154,113 @@ export function registerReferenceTools(server) {
121
154
  const content = await client().readReferenceFile(decoded.path);
122
155
  return wrap(content);
123
156
  }
157
+ case 'fetch': {
158
+ if (!decoded.url)
159
+ return wrap('❌ fetch 需要 url (要拉取的文档地址)');
160
+ const result = await client().fetchRefUrl(decoded.url, decoded.id);
161
+ const lines = [
162
+ `✅ 文档已拉取并保存`,
163
+ '',
164
+ `| 字段 | 值 |`,
165
+ `|:---|:---|`,
166
+ `| 参考 ID | ${result.refId} |`,
167
+ `| 标题 | ${result.title} |`,
168
+ `| 内容文件 | ${result.contentFile} |`,
169
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
170
+ `| 累计拉取 | ${result.fetchCount} 次 |`,
171
+ '',
172
+ '### 内容预览',
173
+ '',
174
+ result.contentPreview.substring(0, 500),
175
+ '',
176
+ `> 💡 用 \`kg_ref(get, id:"${result.refId}")\` 查看完整详情`,
177
+ `> 💡 用 \`kg_ref(read_file, path:"${result.contentFile}")\` 读取完整内容`,
178
+ `> 💡 用 \`kg_ref(refetch, id:"${result.refId}")\` 重新拉取最新版本`,
179
+ ];
180
+ return wrap(lines.join('\n'));
181
+ }
182
+ case 'refetch': {
183
+ if (!decoded.id)
184
+ return wrap('❌ refetch 需要 id (参考 ID)');
185
+ const ref = await client().getReference(decoded.id);
186
+ if (!ref)
187
+ return wrap(`❌ 未找到参考: ${decoded.id}`);
188
+ const refetchUrl = decoded.url || ref.sourceUrl;
189
+ if (!refetchUrl)
190
+ return wrap(`❌ 参考 ${decoded.id} 没有 sourceUrl,请提供 url 参数`);
191
+ const result = await client().fetchRefUrl(refetchUrl, decoded.id);
192
+ const lines = [
193
+ `✅ 文档已重新拉取 (第 ${result.fetchCount} 次)`,
194
+ '',
195
+ `| 字段 | 值 |`,
196
+ `|:---|:---|`,
197
+ `| 参考 ID | ${result.refId} |`,
198
+ `| 标题 | ${result.title} |`,
199
+ `| 新内容文件 | ${result.contentFile} |`,
200
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
201
+ '',
202
+ '### 内容预览',
203
+ '',
204
+ result.contentPreview.substring(0, 300),
205
+ ];
206
+ return wrap(lines.join('\n'));
207
+ }
208
+ case 'import_file': {
209
+ if (!decoded.id)
210
+ return wrap('❌ import_file 需要 id (参考 ID)');
211
+ if (!decoded.path)
212
+ return wrap('❌ import_file 需要 path (本地文件路径)');
213
+ const result = await client().importLocalFile(decoded.id, decoded.path);
214
+ const lines = [
215
+ `✅ 文件内容已导入参考系统`,
216
+ '',
217
+ `| 字段 | 值 |`,
218
+ `|:---|:---|`,
219
+ `| 参考 ID | ${result.refId} |`,
220
+ `| 源文件 | ${result.sourcePath} |`,
221
+ `| 存储文件 | ${result.storedFile} |`,
222
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
223
+ `| 累计导入 | ${result.importCount} 次 |`,
224
+ '',
225
+ '### 内容预览',
226
+ '',
227
+ result.contentPreview.substring(0, 300),
228
+ '',
229
+ `> 💡 用 \`kg_ref(read_file, path:"${result.storedFile}")\` 读取完整内容`,
230
+ `> 💡 用 \`kg_ref(reimport, id:"${result.refId}", path:"${result.sourcePath}")\` 重新导入最新版本`,
231
+ ];
232
+ return wrap(lines.join('\n'));
233
+ }
234
+ case 'reimport': {
235
+ if (!decoded.id)
236
+ return wrap('❌ reimport 需要 id (参考 ID)');
237
+ const ref = await client().getReference(decoded.id);
238
+ if (!ref)
239
+ return wrap(`❌ 未找到参考: ${decoded.id}`);
240
+ // 获取源文件路径: 优先使用参数中的 path, 否则取最后一次导入的源路径
241
+ const reimportPath = decoded.path ||
242
+ (ref.fileImportHistory && ref.fileImportHistory.length > 0
243
+ ? ref.fileImportHistory[ref.fileImportHistory.length - 1].sourcePath
244
+ : null);
245
+ if (!reimportPath)
246
+ return wrap(`❌ 参考 ${decoded.id} 没有导入历史,请提供 path 参数`);
247
+ const result = await client().importLocalFile(decoded.id, reimportPath);
248
+ const lines = [
249
+ `✅ 文件已重新导入 (第 ${result.importCount} 次)`,
250
+ '',
251
+ `| 字段 | 值 |`,
252
+ `|:---|:---|`,
253
+ `| 参考 ID | ${result.refId} |`,
254
+ `| 源文件 | ${result.sourcePath} |`,
255
+ `| 新存储文件 | ${result.storedFile} |`,
256
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
257
+ '',
258
+ '### 内容预览',
259
+ '',
260
+ result.contentPreview.substring(0, 300),
261
+ ];
262
+ return wrap(lines.join('\n'));
263
+ }
124
264
  default:
125
265
  return wrap(`❌ 未知 action: ${decoded.action}`);
126
266
  }
@@ -8,7 +8,8 @@ import { decodeObjectStrings } from '../utils.js';
8
8
  import { wrap, safeTool } from './shared.js';
9
9
  export function registerTaskTools(server, ctx) {
10
10
  const client = () => getClient();
11
- server.tool('kg_task', '📝 任务记录 — action: create(创建,建议bindTo关联节点)|get(查询)|update(追加进度)|archive(归档)|delete(删除)。⚠️ 每完成一个步骤必须立即update!', {
11
+ server.tool('kg_task', '📝 任务记录 — action: create(创建,建议bindTo关联节点)|get(查询)|update(追加进度)|archive(归档)|delete(删除)。⚠️ 每完成一个步骤必须立即update!\n' +
12
+ '★ 涉及逻辑变更的任务,创建后必须先执行 design-first 工作流(kg_workflow(id:"design-first")),在流程图中完成设计并验证后才能编码。', {
12
13
  action: z.enum(['create', 'get', 'update', 'archive', 'delete'])
13
14
  .describe('操作类型'),
14
15
  title: z.string().optional()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",