@ppdocs/mcp 3.2.20 → 3.2.21

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.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * API URL 格式: http://localhost:20001/api/:projectId/:password/...
6
6
  */
7
- import type { DocData, DocNode, DocSearchResult, Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, RuleMeta } from './types.js';
7
+ import type { DocData, DocNode, Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, RuleMeta } from './types.js';
8
8
  interface TreeNode {
9
9
  path: string;
10
10
  name: string;
@@ -22,7 +22,6 @@ export declare class PpdocsApiClient {
22
22
  createDoc(docPath: string, doc: Partial<DocData>): Promise<DocData>;
23
23
  updateDoc(docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
24
24
  deleteDoc(docPath: string): Promise<boolean>;
25
- searchDocs(keywords: string[], limit?: number): Promise<DocSearchResult[]>;
26
25
  /** 按状态筛选文档 */
27
26
  getDocsByStatus(statusList: string[]): Promise<DocNode[]>;
28
27
  /** 获取目录树结构 */
@@ -43,14 +42,6 @@ export declare class PpdocsApiClient {
43
42
  }, creator: string): Promise<Task>;
44
43
  addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
45
44
  completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
46
- /** 创建项目 (公开路由,无需认证) */
47
- createProject(id: string, name: string, description?: string, projectPath?: string): Promise<{
48
- project: {
49
- id: string;
50
- name: string;
51
- };
52
- password: string;
53
- }>;
54
45
  /** 列出所有可访问的项目 */
55
46
  crossListProjects(): Promise<{
56
47
  id: string;
@@ -77,12 +68,10 @@ export declare class PpdocsApiClient {
77
68
  localPath: string;
78
69
  fileCount: number;
79
70
  }>;
80
- /** 上传本地目录到中心服务器 (打包 zip → POST) */
81
- uploadFiles(localDir: string, remoteDir?: string): Promise<{
71
+ /** 上传本地目录或单文件到中心服务器 (打包 zip → POST) */
72
+ uploadFiles(localPath: string, remoteDir?: string): Promise<{
82
73
  fileCount: number;
83
74
  }>;
84
- /** 清空项目文件存储区 */
85
- clearFiles(): Promise<void>;
86
75
  /** 上传已打包好的 zip 数据到本项目 files/ (供 Code Beacon 使用) */
87
76
  uploadRawZip(zipData: Buffer | Uint8Array | any, remoteDir?: string): Promise<{
88
77
  fileCount: number;
@@ -112,19 +101,42 @@ export declare class PpdocsApiClient {
112
101
  /** 列出活跃讨论 (公开路由) */
113
102
  discussionList(): Promise<unknown>;
114
103
  /** 创建讨论 (公开路由) */
115
- discussionCreate(title: string, initiator: string, participants: string[], content: string): Promise<{
104
+ discussionCreate(title: string, initiator: string, participants: string[], content: string, msgSummary?: string): Promise<{
116
105
  id: string;
117
106
  }>;
118
- /** 批量读取讨论 (公开路由) */
119
- discussionReadByIds(ids: string[]): Promise<unknown>;
107
+ /** 批量读取讨论 (公开路由, 支持已读追踪) */
108
+ discussionReadByIds(ids: string[], reader?: string, mode?: string): Promise<unknown>;
109
+ /** 列出所有讨论 (含历史, 按参与方筛选) */
110
+ discussionListAll(participant?: string): Promise<unknown>;
120
111
  /** 回复讨论 (公开路由) */
121
- discussionReply(id: string, sender: string, content: string, newSummary?: string): Promise<boolean>;
112
+ discussionReply(id: string, sender: string, content: string, msgSummary?: string, newSummary?: string): Promise<boolean>;
113
+ /** 完成讨论 (公开路由, 标记为 completed) */
114
+ discussionComplete(id: string): Promise<boolean>;
122
115
  /** 删除讨论 (公开路由) */
123
116
  discussionDelete(id: string): Promise<boolean>;
124
117
  /** 结案归档讨论 (认证路由) */
125
118
  discussionClose(id: string, conclusion: string): Promise<{
126
119
  archived_path: string;
127
120
  }>;
121
+ /** 列出公共文件 */
122
+ publicFilesList(dir?: string): Promise<FileInfo[]>;
123
+ /** 读取公共文件 (文本) */
124
+ publicFilesRead(filePath: string): Promise<string>;
125
+ /** 创建公共文件池子目录 */
126
+ publicFilesMkdir(dirPath: string): Promise<boolean>;
127
+ /** 重命名公共文件或目录 */
128
+ publicFilesRename(filePath: string, newName: string): Promise<boolean>;
129
+ /** 删除公共文件 */
130
+ publicFilesDelete(filePath: string): Promise<boolean>;
131
+ /** 下载公共文件或目录 */
132
+ publicFilesDownload(remotePath: string, localPath?: string): Promise<{
133
+ localPath: string;
134
+ fileCount: number;
135
+ }>;
136
+ /** 上传本地目录或单文件到公共文件池 */
137
+ publicFilesUpload(localPath: string, remoteDir?: string): Promise<{
138
+ fileCount: number;
139
+ }>;
128
140
  meetingJoin(agentId: string, agentType: string): Promise<unknown>;
129
141
  meetingLeave(agentId: string): Promise<unknown>;
130
142
  meetingPost(agentId: string, content: string, msgType?: string): Promise<unknown>;
@@ -191,12 +191,6 @@ export class PpdocsApiClient {
191
191
  return false;
192
192
  }
193
193
  }
194
- async searchDocs(keywords, limit = 20) {
195
- return this.request('/docs/search', {
196
- method: 'POST',
197
- body: JSON.stringify({ keywords, limit })
198
- });
199
- }
200
194
  /** 按状态筛选文档 */
201
195
  async getDocsByStatus(statusList) {
202
196
  return this.request('/docs/by-status', {
@@ -328,22 +322,6 @@ export class PpdocsApiClient {
328
322
  return null;
329
323
  }
330
324
  }
331
- // ============ 项目管理 ============
332
- /** 创建项目 (公开路由,无需认证) */
333
- async createProject(id, name, description, projectPath) {
334
- const url = `${this.serverUrl}/api/projects`;
335
- const response = await fetch(url, {
336
- method: 'POST',
337
- headers: { 'Content-Type': 'application/json' },
338
- body: JSON.stringify({ id, name, description: description || '', project_path: projectPath || '' }),
339
- });
340
- if (!response.ok) {
341
- const error = await response.json().catch(() => ({ error: response.statusText }));
342
- throw new Error(error.error || `HTTP ${response.status}`);
343
- }
344
- const json = await response.json();
345
- return json.data;
346
- }
347
325
  // ============ 跨项目只读访问 ============
348
326
  /** 列出所有可访问的项目 */
349
327
  async crossListProjects() {
@@ -397,33 +375,41 @@ export class PpdocsApiClient {
397
375
  async download(remotePath, localPath) {
398
376
  return fetchAndExtractZip(`${this.baseUrl}/files-download/${cleanPath(remotePath)}`, localPath);
399
377
  }
400
- /** 上传本地目录到中心服务器 (打包 zip → POST) */
401
- async uploadFiles(localDir, remoteDir) {
378
+ /** 上传本地目录或单文件到中心服务器 (打包 zip → POST) */
379
+ async uploadFiles(localPath, remoteDir) {
402
380
  const fs = await import('fs');
403
381
  const path = await import('path');
404
382
  const AdmZip = (await import('adm-zip')).default;
405
- if (!fs.existsSync(localDir) || !fs.statSync(localDir).isDirectory()) {
406
- throw new Error(`本地目录不存在: ${localDir}`);
383
+ if (!fs.existsSync(localPath)) {
384
+ throw new Error(`本地路径不存在: ${localPath}`);
407
385
  }
408
386
  const zip = new AdmZip();
409
- const addDir = (dirPath, zipPath) => {
410
- for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
411
- if (entry.name.startsWith('.'))
412
- continue;
413
- const fullPath = path.join(dirPath, entry.name);
414
- if (entry.isDirectory()) {
415
- if (EXCLUDED_DIRS.has(entry.name))
387
+ const stat = fs.statSync(localPath);
388
+ if (stat.isDirectory()) {
389
+ const addDir = (dirPath, zipPath) => {
390
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
391
+ if (entry.name.startsWith('.'))
416
392
  continue;
417
- addDir(fullPath, zipPath ? `${zipPath}/${entry.name}` : entry.name);
393
+ const fullPath = path.join(dirPath, entry.name);
394
+ if (entry.isDirectory()) {
395
+ if (EXCLUDED_DIRS.has(entry.name))
396
+ continue;
397
+ addDir(fullPath, zipPath ? `${zipPath}/${entry.name}` : entry.name);
398
+ }
399
+ else {
400
+ if (fs.statSync(fullPath).size > 10_485_760)
401
+ continue;
402
+ zip.addLocalFile(fullPath, zipPath || undefined);
403
+ }
418
404
  }
419
- else {
420
- if (fs.statSync(fullPath).size > 10_485_760)
421
- continue;
422
- zip.addLocalFile(fullPath, zipPath || undefined);
423
- }
424
- }
425
- };
426
- addDir(localDir, '');
405
+ };
406
+ addDir(localPath, '');
407
+ }
408
+ else {
409
+ if (stat.size > 10_485_760)
410
+ throw new Error('文件超过 10MB 限制');
411
+ zip.addLocalFile(localPath);
412
+ }
427
413
  const buffer = zip.toBuffer();
428
414
  if (buffer.length > 104_857_600) {
429
415
  throw new Error('打包后超过 100MB 限制,请缩小目录范围');
@@ -441,15 +427,6 @@ export class PpdocsApiClient {
441
427
  const result = await response.json();
442
428
  return { fileCount: result.data?.fileCount || 0 };
443
429
  }
444
- /** 清空项目文件存储区 */
445
- async clearFiles() {
446
- const url = `${this.baseUrl}/files`;
447
- const response = await fetch(url, { method: 'DELETE' });
448
- if (!response.ok) {
449
- const error = await response.json().catch(() => ({ error: response.statusText }));
450
- throw new Error(error.error || `HTTP ${response.status}`);
451
- }
452
- }
453
430
  // ============ 直接上传已打包的 ZIP ============
454
431
  /** 上传已打包好的 zip 数据到本项目 files/ (供 Code Beacon 使用) */
455
432
  async uploadRawZip(zipData, remoteDir) {
@@ -544,24 +521,37 @@ export class PpdocsApiClient {
544
521
  return this.publicRequest('/api/discussions');
545
522
  }
546
523
  /** 创建讨论 (公开路由) */
547
- async discussionCreate(title, initiator, participants, content) {
524
+ async discussionCreate(title, initiator, participants, content, msgSummary) {
548
525
  return this.publicRequest('/api/discussions', {
549
526
  method: 'POST',
550
- body: JSON.stringify({ title, initiator, participants, content }),
527
+ body: JSON.stringify({ title, initiator, participants, content, msg_summary: msgSummary }),
551
528
  });
552
529
  }
553
- /** 批量读取讨论 (公开路由) */
554
- async discussionReadByIds(ids) {
530
+ /** 批量读取讨论 (公开路由, 支持已读追踪) */
531
+ async discussionReadByIds(ids, reader, mode) {
555
532
  return this.publicRequest('/api/discussions/read', {
556
533
  method: 'POST',
557
- body: JSON.stringify({ ids }),
534
+ body: JSON.stringify({ ids, reader, mode }),
535
+ });
536
+ }
537
+ /** 列出所有讨论 (含历史, 按参与方筛选) */
538
+ async discussionListAll(participant) {
539
+ return this.publicRequest('/api/discussions/all', {
540
+ method: 'POST',
541
+ body: JSON.stringify({ participant }),
558
542
  });
559
543
  }
560
544
  /** 回复讨论 (公开路由) */
561
- async discussionReply(id, sender, content, newSummary) {
545
+ async discussionReply(id, sender, content, msgSummary, newSummary) {
562
546
  return this.publicRequest(`/api/discussions/${encodeURIComponent(id)}/reply`, {
563
547
  method: 'POST',
564
- body: JSON.stringify({ sender, content, new_summary: newSummary }),
548
+ body: JSON.stringify({ sender, content, msg_summary: msgSummary, new_summary: newSummary }),
549
+ });
550
+ }
551
+ /** 完成讨论 (公开路由, 标记为 completed) */
552
+ async discussionComplete(id) {
553
+ return this.publicRequest(`/api/discussions/${encodeURIComponent(id)}/complete`, {
554
+ method: 'POST',
565
555
  });
566
556
  }
567
557
  /** 删除讨论 (公开路由) */
@@ -577,6 +567,92 @@ export class PpdocsApiClient {
577
567
  body: JSON.stringify({ conclusion }),
578
568
  });
579
569
  }
570
+ // ============ 公共文件池 ============
571
+ /** 列出公共文件 */
572
+ async publicFilesList(dir) {
573
+ const query = dir ? `?dir=${encodeURIComponent(dir)}` : '';
574
+ return this.publicRequest(`/api/public-files${query}`);
575
+ }
576
+ /** 读取公共文件 (文本) */
577
+ async publicFilesRead(filePath) {
578
+ return this.publicRequest(`/api/public-files/read/${cleanPath(filePath)}`);
579
+ }
580
+ /** 创建公共文件池子目录 */
581
+ async publicFilesMkdir(dirPath) {
582
+ return this.publicRequest(`/api/public-files/mkdir`, {
583
+ method: 'POST',
584
+ body: JSON.stringify({ path: dirPath }),
585
+ });
586
+ }
587
+ /** 重命名公共文件或目录 */
588
+ async publicFilesRename(filePath, newName) {
589
+ return this.publicRequest(`/api/public-files/rename`, {
590
+ method: 'POST',
591
+ body: JSON.stringify({ path: filePath, new_name: newName }),
592
+ });
593
+ }
594
+ /** 删除公共文件 */
595
+ async publicFilesDelete(filePath) {
596
+ return this.publicRequest(`/api/public-files/${cleanPath(filePath)}`, {
597
+ method: 'DELETE',
598
+ });
599
+ }
600
+ /** 下载公共文件或目录 */
601
+ async publicFilesDownload(remotePath, localPath) {
602
+ return fetchAndExtractZip(`${this.serverUrl}/api/public-files/download/${cleanPath(remotePath)}`, localPath);
603
+ }
604
+ /** 上传本地目录或单文件到公共文件池 */
605
+ async publicFilesUpload(localPath, remoteDir) {
606
+ const fs = await import('fs');
607
+ const path = await import('path');
608
+ const AdmZip = (await import('adm-zip')).default;
609
+ if (!fs.existsSync(localPath)) {
610
+ throw new Error(`本地路径不存在: ${localPath}`);
611
+ }
612
+ const zip = new AdmZip();
613
+ const stat = fs.statSync(localPath);
614
+ if (stat.isDirectory()) {
615
+ const addDir = (dirPath, zipPath) => {
616
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
617
+ if (entry.name.startsWith('.'))
618
+ continue;
619
+ const fullPath = path.join(dirPath, entry.name);
620
+ if (entry.isDirectory()) {
621
+ if (EXCLUDED_DIRS.has(entry.name))
622
+ continue;
623
+ addDir(fullPath, zipPath ? `${zipPath}/${entry.name}` : entry.name);
624
+ }
625
+ else {
626
+ if (fs.statSync(fullPath).size > 10_485_760)
627
+ continue;
628
+ zip.addLocalFile(fullPath, zipPath || undefined);
629
+ }
630
+ }
631
+ };
632
+ addDir(localPath, '');
633
+ }
634
+ else {
635
+ if (stat.size > 10_485_760)
636
+ throw new Error('文件超过 10MB 限制');
637
+ zip.addLocalFile(localPath);
638
+ }
639
+ const buffer = zip.toBuffer();
640
+ if (buffer.length > 104_857_600) {
641
+ throw new Error('打包后超过 100MB 限制');
642
+ }
643
+ const query = remoteDir ? `?dir=${encodeURIComponent(remoteDir)}` : '';
644
+ const response = await fetch(`${this.serverUrl}/api/public-files/upload${query}`, {
645
+ method: 'POST',
646
+ headers: { 'Content-Type': 'application/octet-stream' },
647
+ body: new Uint8Array(buffer),
648
+ });
649
+ if (!response.ok) {
650
+ const error = await response.json().catch(() => ({ error: response.statusText }));
651
+ throw new Error(error.error || `HTTP ${response.status}`);
652
+ }
653
+ const result = await response.json();
654
+ return { fileCount: result.data?.fileCount || 0 };
655
+ }
580
656
  // ============ 多AI协作会议 ============
581
657
  async meetingJoin(agentId, agentType) {
582
658
  return this.request('/meeting/join', {
@@ -26,27 +26,6 @@ export interface DocNode {
26
26
  isDir: boolean;
27
27
  status?: string;
28
28
  }
29
- /** 文档搜索结果 */
30
- export interface DocSearchResult {
31
- path: string;
32
- name: string;
33
- summary: string;
34
- score: number;
35
- }
36
- export interface ProjectMeta {
37
- projectId: string;
38
- projectName: string;
39
- updatedAt: string;
40
- password?: string;
41
- }
42
- export interface Project {
43
- id: string;
44
- name: string;
45
- description?: string;
46
- updatedAt: string;
47
- createdAt?: string;
48
- projectPath?: string;
49
- }
50
29
  export interface RuleMeta {
51
30
  label: string;
52
31
  keywords: string[];
@@ -20,10 +20,10 @@ export function registerAnalyzerTools(server, ctx) {
20
20
  const client = () => getClient();
21
21
  // ===== code_scan: 扫描项目代码 =====
22
22
  server.tool('code_scan', '📡 扫描项目代码, 构建索引。返回文件数、符号数、语言统计。★首次使用 code_query/code_impact/code_context 前必须先执行★', {
23
- projectPath: z.string().describe('项目源码的绝对路径(如"D:/projects/myapp")'),
23
+ projectPath: z.string().optional().describe('项目源码的绝对路径(如"D:/projects/myapp")。不传则自动从Beacon同步目录或项目配置解析'),
24
24
  force: z.boolean().optional().describe('是否强制全量重建(默认false, 增量更新)'),
25
25
  }, async (args) => safeTool(async () => {
26
- const result = await client().analyzerScan(args.projectPath, args.force ?? false);
26
+ const result = await client().analyzerScan(args.projectPath || '', args.force ?? false);
27
27
  return wrap([
28
28
  `✅ 代码扫描完成`,
29
29
  ``,
@@ -36,10 +36,10 @@ export function registerAnalyzerTools(server, ctx) {
36
36
  }));
37
37
  // ===== code_query: 搜索代码符号 =====
38
38
  server.tool('code_query', '🔤 搜索代码符号(函数/类/方法/接口/类型)。返回匹配列表+文件路径+行号。定位代码位置的最快方式。需先运行 code_scan', {
39
- projectPath: z.string().describe('项目源码的绝对路径'),
39
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
40
40
  query: z.string().describe('搜索关键词(函数名/类名/方法名等)'),
41
41
  }, async (args) => safeTool(async () => {
42
- const results = await client().analyzerQuery(args.projectPath, args.query);
42
+ const results = await client().analyzerQuery(args.projectPath || '', args.query);
43
43
  if (!results || results.length === 0) {
44
44
  return wrap(`未找到匹配 "${args.query}" 的符号。请确认已运行 code_scan`);
45
45
  }
@@ -57,11 +57,11 @@ export function registerAnalyzerTools(server, ctx) {
57
57
  }));
58
58
  // ===== code_impact: 分层影响分析 =====
59
59
  server.tool('code_impact', '💥 爆炸半径分析 ★修改代码前必查★ 分析修改一个函数/类/类型会影响多少文件。BFS分层输出: L1🔴直接引用=必须检查, L2🟡间接引用=建议检查, L3🟢传递引用=注意。修改任何公共接口、函数签名、类型定义前务必先运行!', {
60
- projectPath: z.string().describe('项目源码的绝对路径'),
60
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
61
61
  symbolName: z.string().describe('要分析的符号名称(如"AuthService", "handleLogin")'),
62
62
  depth: z.number().optional().describe('分析深度层级(1-5, 默认2)。1=仅直接引用, 3=深度追踪'),
63
63
  }, async (args) => safeTool(async () => {
64
- const result = await client().analyzerImpactTree(args.projectPath, args.symbolName, args.depth ?? 2);
64
+ const result = await client().analyzerImpactTree(args.projectPath || '', args.symbolName, args.depth ?? 2);
65
65
  if (!result) {
66
66
  return wrap(`未找到符号 "${args.symbolName}"。请确认名称正确且已运行 code_scan`);
67
67
  }
@@ -91,10 +91,10 @@ export function registerAnalyzerTools(server, ctx) {
91
91
  }));
92
92
  // ===== code_context: 文件360°上下文 =====
93
93
  server.tool('code_context', '🔍 文件360°上下文 — 定义了什么符号、导入了什么、被谁引用。修改文件前使用, 快速了解所有依赖关系, 避免遗漏', {
94
- projectPath: z.string().describe('项目源码的绝对路径'),
94
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
95
95
  filePath: z.string().describe('目标文件的相对路径(如"src/services/auth.ts")'),
96
96
  }, async (args) => safeTool(async () => {
97
- const fileCtx = await client().analyzerContext(args.projectPath, args.filePath);
97
+ const fileCtx = await client().analyzerContext(args.projectPath || '', args.filePath);
98
98
  if (!fileCtx) {
99
99
  return wrap(`未找到文件 "${args.filePath}"。请确认路径正确, 路径格式为相对路径(如 src/main.ts)`);
100
100
  }
@@ -135,16 +135,17 @@ export function registerAnalyzerTools(server, ctx) {
135
135
  }));
136
136
  // ===== code_path: 两点间链路追踪 =====
137
137
  server.tool('code_path', '🔗 两点链路追踪 — 给定两个符号(如 fileA.funcA 和 fileB.funcB),自动搜索中间的引用链路,返回完整路径 + 绑定的知识图谱文档', {
138
- projectPath: z.string().describe('项目源码的绝对路径'),
138
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
139
139
  symbolA: z.string().describe('起点符号名(如 "handleLogin")'),
140
140
  symbolB: z.string().describe('终点符号名(如 "AuthService")'),
141
141
  maxDepth: z.number().optional().describe('最大搜索深度(1-5, 默认3)'),
142
142
  }, async (args) => safeTool(async () => {
143
143
  const depth = Math.min(5, Math.max(1, args.maxDepth ?? 3));
144
+ const pp = args.projectPath || '';
144
145
  // 1. 获取两个符号的影响树
145
146
  const [treeA, treeB] = await Promise.all([
146
- client().analyzerImpactTree(args.projectPath, args.symbolA, depth),
147
- client().analyzerImpactTree(args.projectPath, args.symbolB, depth),
147
+ client().analyzerImpactTree(pp, args.symbolA, depth),
148
+ client().analyzerImpactTree(pp, args.symbolB, depth),
148
149
  ]);
149
150
  if (!treeA)
150
151
  return wrap(`❌ 未找到起点符号 "${args.symbolA}"。请确认名称正确且已运行 code_scan`);
@@ -262,10 +263,10 @@ export function registerAnalyzerTools(server, ctx) {
262
263
  }));
263
264
  // ===== code_smart_context: 代码+文档全关联上下文 =====
264
265
  server.tool('code_smart_context', '🔍 代码+文档全关联上下文 — 输入一个函数名,一次调用返回:代码依赖、关联文档、匹配规则、活跃任务、影响范围摘要。需先运行 code_scan', {
265
- projectPath: z.string().describe('项目源码的绝对路径'),
266
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
266
267
  symbolName: z.string().describe('要查询的符号名称(如"handleLogin", "AuthService")'),
267
268
  }, async (args) => safeTool(async () => {
268
- const smartCtx = await client().analyzerSmartContext(args.projectPath, args.symbolName);
269
+ const smartCtx = await client().analyzerSmartContext(args.projectPath || '', args.symbolName);
269
270
  if (!smartCtx) {
270
271
  return wrap(`未找到符号 "${args.symbolName}"。请确认名称正确且已运行 code_scan`);
271
272
  }
@@ -322,12 +323,12 @@ export function registerAnalyzerTools(server, ctx) {
322
323
  }));
323
324
  // ===== code_full_path: 全关联路径 =====
324
325
  server.tool('code_full_path', '🔗 全关联路径 — 两个符号之间不仅返回代码引用链路,还返回共享的KG文档、共同导入、祖先模块。需先运行 code_scan', {
325
- projectPath: z.string().describe('项目源码的绝对路径'),
326
+ projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
326
327
  symbolA: z.string().describe('起点符号名(如 "handleLogin")'),
327
328
  symbolB: z.string().describe('终点符号名(如 "AuthService")'),
328
329
  maxDepth: z.number().optional().describe('最大搜索深度(1-5, 默认3)'),
329
330
  }, async (args) => safeTool(async () => {
330
- const result = await client().analyzerFullPath(args.projectPath, args.symbolA, args.symbolB, args.maxDepth ?? 3);
331
+ const result = await client().analyzerFullPath(args.projectPath || '', args.symbolA, args.symbolB, args.maxDepth ?? 3);
331
332
  if (!result) {
332
333
  return wrap(`❌ 无法构建路径。请确认两个符号名称正确且已运行 code_scan`);
333
334
  }
@@ -41,40 +41,52 @@ function formatList(items, ctx) {
41
41
  }
42
42
  return lines.join('\n');
43
43
  }
44
- function formatDetail(d) {
44
+ function formatDetailView(d) {
45
45
  const msgs = d.messages || [];
46
46
  const lines = [
47
47
  `💬 ${d.title}`,
48
48
  `发起: ${d.initiator} | 参与: ${d.participants.length}方 | 状态: ${d.status}`,
49
49
  `📌 ${d.summary}`,
50
+ `📊 总消息: ${d.totalCount} | 未读: ${d.unreadCount}`,
50
51
  ``,
51
52
  ];
52
53
  msgs.forEach((m, i) => {
53
- lines.push(`--- 消息 ${i + 1}/${msgs.length} [${m.sender}] ${relativeTime(m.timestamp)} ---`);
54
- lines.push(m.content);
54
+ const readTag = m.isRead ? '📖' : '🆕';
55
+ lines.push(`--- ${readTag} 消息 ${i + 1}/${msgs.length} [${m.sender}] ${relativeTime(m.timestamp)} ---`);
56
+ // 已读且无 content → 显示摘要
57
+ if (m.isRead && !m.content) {
58
+ lines.push(`[摘要] ${m.summary}`);
59
+ }
60
+ else {
61
+ lines.push(m.content || m.summary || '(无内容)');
62
+ }
55
63
  lines.push('');
56
64
  });
57
65
  return lines.join('\n');
58
66
  }
59
67
  export function registerDiscussionTools(server, ctx) {
60
68
  const client = () => getClient();
61
- server.tool('kg_discuss', '💬 跨项目讨论 — 发起、回复、归档协同讨论。action: list(列出活跃讨论)|read(读取详情)|create(发起)|reply(回复)|close(结案归档)|delete(删除)', {
62
- action: z.enum(['list', 'read', 'create', 'reply', 'close', 'delete'])
69
+ server.tool('kg_discuss', '💬 跨项目讨论 — 发起、回复、归档协同讨论。action: list(列出活跃讨论)|read(读取详情,自动追踪已读)|create(发起)|reply(回复)|complete(标记完成)|close(结案归档)|delete(删除)|history(查看历史讨论)', {
70
+ action: z.enum(['list', 'read', 'create', 'reply', 'complete', 'close', 'delete', 'history'])
63
71
  .describe('操作类型'),
64
72
  id: z.string().optional()
65
- .describe('讨论哈希ID (read/reply/close/delete)'),
73
+ .describe('讨论哈希ID (read/reply/complete/close/delete)'),
66
74
  ids: z.array(z.string()).optional()
67
75
  .describe('批量读取的讨论ID数组 (read)'),
68
76
  title: z.string().optional()
69
77
  .describe('讨论标题 (create)'),
70
78
  participants: z.array(z.string()).optional()
71
79
  .describe('参与项目ID数组 (create, 不含自己)'),
80
+ summary: z.string().optional()
81
+ .describe('消息摘要 (create/reply, 不传则自动截取content前50字)'),
72
82
  content: z.string().optional()
73
- .describe('消息内容Markdown (create/reply)'),
83
+ .describe('消息详细内容Markdown (create/reply)'),
74
84
  conclusion: z.string().optional()
75
85
  .describe('结案总结 (close)'),
76
86
  newSummary: z.string().optional()
77
- .describe('更新进展摘要 (reply)'),
87
+ .describe('更新讨论进展摘要 (reply)'),
88
+ mode: z.enum(['auto', 'full']).optional()
89
+ .describe('读取模式 (read, 默认auto: 已读消息仅返回摘要)'),
78
90
  }, async (args) => safeTool(async () => {
79
91
  const decoded = decodeObjectStrings(args);
80
92
  const me = sender(ctx);
@@ -89,10 +101,11 @@ export function registerDiscussionTools(server, ctx) {
89
101
  const readIds = decoded.ids || (decoded.id ? [decoded.id] : []);
90
102
  if (readIds.length === 0)
91
103
  return wrap('❌ read 需要 id 或 ids');
92
- const discussions = await client().discussionReadByIds(readIds);
104
+ const readMode = decoded.mode || 'auto';
105
+ const discussions = await client().discussionReadByIds(readIds, me, readMode);
93
106
  if (!Array.isArray(discussions) || discussions.length === 0)
94
107
  return wrap('未找到对应的讨论记录');
95
- return wrap(discussions.map(formatDetail).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n'));
108
+ return wrap(discussions.map(formatDetailView).join('\n\n━━━━━━━━━━━━━━━━━━━━\n\n'));
96
109
  }
97
110
  case 'create': {
98
111
  if (!decoded.title)
@@ -101,7 +114,7 @@ export function registerDiscussionTools(server, ctx) {
101
114
  return wrap('❌ create 需要 participants');
102
115
  if (!decoded.content)
103
116
  return wrap('❌ create 需要 content');
104
- const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content);
117
+ const result = await client().discussionCreate(decoded.title, me, decoded.participants, decoded.content, decoded.summary);
105
118
  return wrap(`✅ 讨论已发起\nID: ${result.id}\n发起方: ${me}\n参与方: ${decoded.participants.join(', ')}`);
106
119
  }
107
120
  case 'reply': {
@@ -109,9 +122,33 @@ export function registerDiscussionTools(server, ctx) {
109
122
  return wrap('❌ reply 需要 id');
110
123
  if (!decoded.content)
111
124
  return wrap('❌ reply 需要 content');
112
- await client().discussionReply(decoded.id, me, decoded.content, decoded.newSummary);
125
+ await client().discussionReply(decoded.id, me, decoded.content, decoded.summary, decoded.newSummary);
113
126
  return wrap(`✅ 回复成功 (ID: ${decoded.id}, 身份: ${me})`);
114
127
  }
128
+ case 'history': {
129
+ const all = await client().discussionListAll(me);
130
+ if (!Array.isArray(all) || all.length === 0)
131
+ return wrap(`暂无参与过的讨论记录 (身份: ${me})`);
132
+ const lines = [
133
+ `本项目: ${me}`,
134
+ ``,
135
+ `📋 讨论历史 (${all.length}个)`,
136
+ ``,
137
+ `| ID | 标题 | 发起方 | 状态 | 消息数 | 更新 |`,
138
+ `|:---|:---|:---|:---|:---:|:---|`,
139
+ ];
140
+ for (const d of all) {
141
+ const statusIcon = d.status === 'active' ? '🟢' : '⚪';
142
+ lines.push(`| ${d.id} | ${d.title} | ${d.initiator} | ${statusIcon} ${d.status} | ${d.messageCount ?? 0} | ${relativeTime(d.updatedAt)} |`);
143
+ }
144
+ return wrap(lines.join('\n'));
145
+ }
146
+ case 'complete': {
147
+ if (!decoded.id)
148
+ return wrap('❌ complete 需要 id');
149
+ await client().discussionComplete(decoded.id);
150
+ return wrap(`✅ 讨论已标记完成 (ID: ${decoded.id})`);
151
+ }
115
152
  case 'close': {
116
153
  if (!decoded.id)
117
154
  return wrap('❌ close 需要 id');
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 📁 kg_files (5→1)
3
3
  * 合并: project_list_files, project_read_file, project_upload, project_download
4
- * 删除: project_clear_files (危险操作)
4
+ * + 公共文件池: public_list, public_read, public_upload, public_download, public_delete
5
5
  */
6
6
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  export declare function registerFileTools(server: McpServer): void;
@@ -1,26 +1,28 @@
1
1
  /**
2
2
  * 📁 kg_files (5→1)
3
3
  * 合并: project_list_files, project_read_file, project_upload, project_download
4
- * 删除: project_clear_files (危险操作)
4
+ * + 公共文件池: public_list, public_read, public_upload, public_download, public_delete
5
5
  */
6
6
  import { z } from 'zod';
7
7
  import { getClient } from '../storage/httpClient.js';
8
8
  import { wrap, safeTool, crossPrefix, formatFileSize } from './shared.js';
9
9
  export function registerFileTools(server) {
10
10
  const client = () => getClient();
11
- server.tool('kg_files', '📁 项目文件操作 — 浏览目录、读取文件、上传下载。action: list(目录浏览)|read(读取文件)|upload(上传目录)|download(下载文件)', {
12
- action: z.enum(['list', 'read', 'upload', 'download'])
11
+ server.tool('kg_files', '📁 项目文件操作 — 浏览目录、读取文件、上传下载。action: list(目录浏览)|read(读取文件)|upload(上传)|download(下载文件)|public_list(公共文件池浏览)|public_read(读取公共文件)|public_upload(上传到公共池)|public_download(下载公共文件)|public_delete(删除公共文件)|public_mkdir(创建公共池子目录)|public_rename(重命名公共文件)', {
12
+ action: z.enum(['list', 'read', 'upload', 'download', 'public_list', 'public_read', 'public_upload', 'public_download', 'public_delete', 'public_mkdir', 'public_rename'])
13
13
  .describe('操作类型'),
14
14
  path: z.string().optional()
15
- .describe('文件路径 (read/download, 如"src/main.ts")'),
15
+ .describe('文件路径 (read/download/public_read/public_download/public_delete/public_mkdir, 如"src/main.ts")'),
16
16
  dir: z.string().optional()
17
- .describe('子目录路径 (list, 如"src/components")'),
17
+ .describe('子目录路径 (list/public_list, 如"src/components")'),
18
18
  localDir: z.string().optional()
19
- .describe('本地目录绝对路径 (upload)'),
19
+ .describe('本地路径(目录或单文件) (upload/public_upload)'),
20
20
  localPath: z.string().optional()
21
- .describe('本地保存路径 (download)'),
21
+ .describe('本地保存路径 (download/public_download)'),
22
22
  remoteDir: z.string().optional()
23
- .describe('远程目标子目录 (upload)'),
23
+ .describe('远程目标子目录 (upload/public_upload)'),
24
+ newName: z.string().optional()
25
+ .describe('新名称 (public_rename)'),
24
26
  targetProject: z.string().optional()
25
27
  .describe('跨项目操作的目标项目ID'),
26
28
  }, async (args) => safeTool(async () => {
@@ -64,6 +66,57 @@ export function registerFileTools(server) {
64
66
  const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
65
67
  return wrap(`${prefix}✅ 已下载\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}`);
66
68
  }
69
+ // ===== 公共文件池 =====
70
+ case 'public_list': {
71
+ const files = await client().publicFilesList(args.dir);
72
+ if (files.length === 0)
73
+ return wrap('📦 公共文件池' + (args.dir ? ` /${args.dir}` : '') + ' 为空');
74
+ const lines = files.map(f => {
75
+ const icon = f.isDir ? '📁' : '📄';
76
+ const size = f.isDir ? '' : ` (${formatFileSize(f.size)})`;
77
+ return `${icon} ${f.name}${size}`;
78
+ });
79
+ const dirLabel = args.dir || '/';
80
+ return wrap(`📦 公共文件池 ${dirLabel} (${files.length} 项)\n\n${lines.join('\n')}`);
81
+ }
82
+ case 'public_read': {
83
+ if (!args.path)
84
+ return wrap('❌ public_read 需要 path');
85
+ const content = await client().publicFilesRead(args.path);
86
+ return wrap(`📦 公共文件 ${args.path}\n\n\`\`\`\n${content}\n\`\`\``);
87
+ }
88
+ case 'public_upload': {
89
+ if (!args.localDir)
90
+ return wrap('❌ public_upload 需要 localDir');
91
+ const result = await client().publicFilesUpload(args.localDir, args.remoteDir);
92
+ return wrap(`✅ 已上传到公共文件池\n\n- 文件数量: ${result.fileCount}\n- 目标目录: ${args.remoteDir || '/'}`);
93
+ }
94
+ case 'public_download': {
95
+ if (!args.path)
96
+ return wrap('❌ public_download 需要 path');
97
+ const result = await client().publicFilesDownload(args.path, args.localPath);
98
+ return wrap(`✅ 已从公共文件池下载\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}`);
99
+ }
100
+ case 'public_delete': {
101
+ if (!args.path)
102
+ return wrap('❌ public_delete 需要 path');
103
+ await client().publicFilesDelete(args.path);
104
+ return wrap(`✅ 已从公共文件池删除: ${args.path}`);
105
+ }
106
+ case 'public_mkdir': {
107
+ if (!args.path)
108
+ return wrap('❌ public_mkdir 需要 path');
109
+ await client().publicFilesMkdir(args.path);
110
+ return wrap(`✅ 已创建公共文件池目录: /${args.path}`);
111
+ }
112
+ case 'public_rename': {
113
+ if (!args.path)
114
+ return wrap('❌ public_rename 需要 path');
115
+ if (!args.newName)
116
+ return wrap('❌ public_rename 需要 newName');
117
+ await client().publicFilesRename(args.path, args.newName);
118
+ return wrap(`✅ 已重命名: ${args.path} → ${args.newName}`);
119
+ }
67
120
  default:
68
121
  return wrap(`❌ 未知 action: ${args.action}`);
69
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.2.20",
3
+ "version": "3.2.21",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",