@ppdocs/mcp 2.8.4 → 3.0.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 +33 -2
- package/dist/storage/httpClient.d.ts +43 -3
- package/dist/storage/httpClient.js +163 -13
- package/dist/storage/types.d.ts +9 -0
- package/dist/tools/helpers.d.ts +4 -0
- package/dist/tools/helpers.js +11 -0
- package/dist/tools/index.js +82 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -14,19 +14,50 @@ function parseArgs(args) {
|
|
|
14
14
|
for (let i = 0; i < args.length; i++) {
|
|
15
15
|
const arg = args[i];
|
|
16
16
|
if (arg === '-p' || arg === '--project') {
|
|
17
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
18
|
+
console.error('Error: -p/--project requires a value');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
17
21
|
opts.project = args[++i];
|
|
18
22
|
}
|
|
19
23
|
else if (arg === '-k' || arg === '--key') {
|
|
24
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
25
|
+
console.error('Error: -k/--key requires a value');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
20
28
|
opts.key = args[++i];
|
|
21
29
|
}
|
|
22
30
|
else if (arg === '-u' || arg === '--user') {
|
|
31
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
32
|
+
console.error('Error: -u/--user requires a value');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
23
35
|
opts.user = args[++i];
|
|
24
36
|
}
|
|
25
37
|
else if (arg === '--port') {
|
|
26
|
-
|
|
38
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
39
|
+
console.error('Error: --port requires a value');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const portVal = parseInt(args[++i], 10);
|
|
43
|
+
if (isNaN(portVal) || portVal < 1 || portVal > 65535) {
|
|
44
|
+
console.error('Error: --port must be a valid port number (1-65535)');
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
opts.port = portVal;
|
|
27
48
|
}
|
|
28
49
|
else if (arg === '--api') {
|
|
29
|
-
|
|
50
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
51
|
+
console.error('Error: --api requires a value');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const apiVal = args[++i].trim();
|
|
55
|
+
if (!apiVal) {
|
|
56
|
+
console.error('Error: --api value cannot be empty');
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// 移除用户可能误加的协议前缀
|
|
60
|
+
opts.api = apiVal.replace(/^https?:\/\//, '');
|
|
30
61
|
}
|
|
31
62
|
else if (arg === '--codex') {
|
|
32
63
|
opts.codex = true;
|
|
@@ -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 } from './types.js';
|
|
7
|
+
import type { DocData, DocNode, DocSearchResult, Task, TaskSummary, TaskLogType, TaskExperience, FileInfo } from './types.js';
|
|
8
8
|
interface TreeNode {
|
|
9
9
|
path: string;
|
|
10
10
|
name: string;
|
|
@@ -42,7 +42,7 @@ export declare class PpdocsApiClient {
|
|
|
42
42
|
id: string;
|
|
43
43
|
name: string;
|
|
44
44
|
description: string;
|
|
45
|
-
|
|
45
|
+
updatedAt: string;
|
|
46
46
|
}[]>;
|
|
47
47
|
/** 跨项目: 列出文档 */
|
|
48
48
|
crossListDocs(target: string): Promise<DocNode[]>;
|
|
@@ -54,6 +54,30 @@ export declare class PpdocsApiClient {
|
|
|
54
54
|
crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
|
|
55
55
|
/** 跨项目: 获取规则 */
|
|
56
56
|
crossGetRules(target: string, ruleType: string): Promise<string[]>;
|
|
57
|
+
/** 列出项目文件 */
|
|
58
|
+
listFiles(dir?: string): Promise<FileInfo[]>;
|
|
59
|
+
/** 读取项目文件 */
|
|
60
|
+
readFile(filePath: string): Promise<string>;
|
|
61
|
+
/** 下载项目目录 (zip) → 自动解压到 temp 目录 */
|
|
62
|
+
downloadDir(dir: string): Promise<{
|
|
63
|
+
localPath: string;
|
|
64
|
+
fileCount: number;
|
|
65
|
+
}>;
|
|
66
|
+
/** 上传本地目录到中心服务器 (打包 zip → POST) */
|
|
67
|
+
uploadFiles(localDir: string, remoteDir?: string): Promise<{
|
|
68
|
+
fileCount: number;
|
|
69
|
+
}>;
|
|
70
|
+
/** 清空项目文件存储区 */
|
|
71
|
+
clearFiles(): Promise<void>;
|
|
72
|
+
/** 跨项目: 列出文件 */
|
|
73
|
+
crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
|
|
74
|
+
/** 跨项目: 读取文件 */
|
|
75
|
+
crossReadFile(target: string, filePath: string): Promise<string>;
|
|
76
|
+
/** 跨项目: 下载目录 */
|
|
77
|
+
crossDownloadDir(target: string, dir: string): Promise<{
|
|
78
|
+
localPath: string;
|
|
79
|
+
fileCount: number;
|
|
80
|
+
}>;
|
|
57
81
|
}
|
|
58
82
|
export declare function initClient(apiUrl: string): void;
|
|
59
83
|
export declare function listDocs(_projectId: string): Promise<DocNode[]>;
|
|
@@ -79,10 +103,26 @@ export declare function crossListProjects(): Promise<{
|
|
|
79
103
|
id: string;
|
|
80
104
|
name: string;
|
|
81
105
|
description: string;
|
|
82
|
-
|
|
106
|
+
updatedAt: string;
|
|
83
107
|
}[]>;
|
|
84
108
|
export declare function crossGetDoc(target: string, docPath: string): Promise<DocData | null>;
|
|
85
109
|
export declare function crossGetTree(target: string): Promise<TreeNode[]>;
|
|
86
110
|
export declare function crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
|
|
87
111
|
export declare function crossGetRules(target: string, ruleType: string): Promise<string[]>;
|
|
112
|
+
export declare function listFiles(dir?: string): Promise<FileInfo[]>;
|
|
113
|
+
export declare function readFile(filePath: string): Promise<string>;
|
|
114
|
+
export declare function downloadDir(dir: string): Promise<{
|
|
115
|
+
localPath: string;
|
|
116
|
+
fileCount: number;
|
|
117
|
+
}>;
|
|
118
|
+
export declare function crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
|
|
119
|
+
export declare function crossReadFile(target: string, filePath: string): Promise<string>;
|
|
120
|
+
export declare function crossDownloadDir(target: string, dir: string): Promise<{
|
|
121
|
+
localPath: string;
|
|
122
|
+
fileCount: number;
|
|
123
|
+
}>;
|
|
124
|
+
export declare function uploadFiles(localDir: string, remoteDir?: string): Promise<{
|
|
125
|
+
fileCount: number;
|
|
126
|
+
}>;
|
|
127
|
+
export declare function clearFiles(): Promise<void>;
|
|
88
128
|
export type { TreeNode };
|
|
@@ -4,6 +4,57 @@
|
|
|
4
4
|
*
|
|
5
5
|
* API URL 格式: http://localhost:20001/api/:projectId/:password/...
|
|
6
6
|
*/
|
|
7
|
+
// ============ 工具函数 ============
|
|
8
|
+
/** 清理路径前缀斜杠 */
|
|
9
|
+
function cleanPath(p) {
|
|
10
|
+
return p.replace(/^\//, '');
|
|
11
|
+
}
|
|
12
|
+
/** 排除的目录 (与 Rust 端 EXCLUDED_DIRS 保持一致) */
|
|
13
|
+
const EXCLUDED_DIRS = new Set([
|
|
14
|
+
'node_modules', '.git', 'target', 'dist', 'build', '.next',
|
|
15
|
+
'__pycache__', '.venv', 'venv', '.idea', '.vs', '.vscode',
|
|
16
|
+
]);
|
|
17
|
+
/** 下载 zip 并解压到 temp 目录 */
|
|
18
|
+
async function fetchAndExtractZip(url) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
25
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
26
|
+
}
|
|
27
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
28
|
+
const os = await import('os');
|
|
29
|
+
const path = await import('path');
|
|
30
|
+
const fs = await import('fs');
|
|
31
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
32
|
+
const tempDir = path.join(os.tmpdir(), `ppdocs-files-${Date.now()}`);
|
|
33
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
34
|
+
const zip = new AdmZip(buffer);
|
|
35
|
+
zip.extractAllTo(tempDir, true);
|
|
36
|
+
let fileCount = 0;
|
|
37
|
+
const countFiles = (dirPath) => {
|
|
38
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
countFiles(path.join(dirPath, entry.name));
|
|
41
|
+
else
|
|
42
|
+
fileCount++;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
countFiles(tempDir);
|
|
46
|
+
return { localPath: tempDir, fileCount };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
50
|
+
throw new Error('Download timeout (60s)');
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
7
58
|
/** 从扁平文档列表构建目录树 */
|
|
8
59
|
function buildTree(docs) {
|
|
9
60
|
const root = [];
|
|
@@ -84,16 +135,13 @@ export class PpdocsApiClient {
|
|
|
84
135
|
}
|
|
85
136
|
async getDoc(docPath) {
|
|
86
137
|
try {
|
|
87
|
-
|
|
88
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
89
|
-
return await this.request(`/docs/${cleanPath}`);
|
|
138
|
+
return await this.request(`/docs/${cleanPath(docPath)}`);
|
|
90
139
|
}
|
|
91
140
|
catch {
|
|
92
141
|
return null;
|
|
93
142
|
}
|
|
94
143
|
}
|
|
95
144
|
async createDoc(docPath, doc) {
|
|
96
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
97
145
|
const payload = {
|
|
98
146
|
summary: doc.summary || '',
|
|
99
147
|
content: doc.content || '',
|
|
@@ -103,17 +151,15 @@ export class PpdocsApiClient {
|
|
|
103
151
|
changes: '初始创建'
|
|
104
152
|
}],
|
|
105
153
|
bugfixes: doc.bugfixes || [],
|
|
106
|
-
status: doc.status || '已完成'
|
|
154
|
+
status: doc.status || '已完成'
|
|
107
155
|
};
|
|
108
|
-
return this.request(`/docs/${cleanPath}`, {
|
|
156
|
+
return this.request(`/docs/${cleanPath(docPath)}`, {
|
|
109
157
|
method: 'POST',
|
|
110
158
|
body: JSON.stringify(payload)
|
|
111
159
|
});
|
|
112
160
|
}
|
|
113
161
|
async updateDoc(docPath, updates) {
|
|
114
162
|
try {
|
|
115
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
116
|
-
// 先获取现有文档
|
|
117
163
|
const existing = await this.getDoc(docPath);
|
|
118
164
|
if (!existing)
|
|
119
165
|
return null;
|
|
@@ -124,7 +170,7 @@ export class PpdocsApiClient {
|
|
|
124
170
|
bugfixes: updates.bugfixes ?? existing.bugfixes,
|
|
125
171
|
status: updates.status ?? existing.status ?? '已完成'
|
|
126
172
|
};
|
|
127
|
-
return await this.request(`/docs/${cleanPath}`, {
|
|
173
|
+
return await this.request(`/docs/${cleanPath(docPath)}`, {
|
|
128
174
|
method: 'PUT',
|
|
129
175
|
body: JSON.stringify(payload)
|
|
130
176
|
});
|
|
@@ -135,8 +181,7 @@ export class PpdocsApiClient {
|
|
|
135
181
|
}
|
|
136
182
|
async deleteDoc(docPath) {
|
|
137
183
|
try {
|
|
138
|
-
|
|
139
|
-
await this.request(`/docs/${cleanPath}`, { method: 'DELETE' });
|
|
184
|
+
await this.request(`/docs/${cleanPath(docPath)}`, { method: 'DELETE' });
|
|
140
185
|
return true;
|
|
141
186
|
}
|
|
142
187
|
catch {
|
|
@@ -244,8 +289,7 @@ export class PpdocsApiClient {
|
|
|
244
289
|
/** 跨项目: 获取文档 */
|
|
245
290
|
async crossGetDoc(target, docPath) {
|
|
246
291
|
try {
|
|
247
|
-
|
|
248
|
-
return await this.request(`/cross/${encodeURIComponent(target)}/docs/${cleanPath}`);
|
|
292
|
+
return await this.request(`/cross/${encodeURIComponent(target)}/docs/${cleanPath(docPath)}`);
|
|
249
293
|
}
|
|
250
294
|
catch {
|
|
251
295
|
return null;
|
|
@@ -272,6 +316,87 @@ export class PpdocsApiClient {
|
|
|
272
316
|
return [];
|
|
273
317
|
}
|
|
274
318
|
}
|
|
319
|
+
// ============ 项目文件访问 ============
|
|
320
|
+
/** 列出项目文件 */
|
|
321
|
+
async listFiles(dir) {
|
|
322
|
+
const query = dir ? `?dir=${encodeURIComponent(dir)}` : '';
|
|
323
|
+
return this.request(`/files${query}`);
|
|
324
|
+
}
|
|
325
|
+
/** 读取项目文件 */
|
|
326
|
+
async readFile(filePath) {
|
|
327
|
+
return this.request(`/files/${cleanPath(filePath)}`);
|
|
328
|
+
}
|
|
329
|
+
/** 下载项目目录 (zip) → 自动解压到 temp 目录 */
|
|
330
|
+
async downloadDir(dir) {
|
|
331
|
+
return fetchAndExtractZip(`${this.baseUrl}/files-download/${cleanPath(dir)}`);
|
|
332
|
+
}
|
|
333
|
+
/** 上传本地目录到中心服务器 (打包 zip → POST) */
|
|
334
|
+
async uploadFiles(localDir, remoteDir) {
|
|
335
|
+
const fs = await import('fs');
|
|
336
|
+
const path = await import('path');
|
|
337
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
338
|
+
if (!fs.existsSync(localDir) || !fs.statSync(localDir).isDirectory()) {
|
|
339
|
+
throw new Error(`本地目录不存在: ${localDir}`);
|
|
340
|
+
}
|
|
341
|
+
const zip = new AdmZip();
|
|
342
|
+
const addDir = (dirPath, zipPath) => {
|
|
343
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
344
|
+
if (entry.name.startsWith('.'))
|
|
345
|
+
continue;
|
|
346
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
347
|
+
if (entry.isDirectory()) {
|
|
348
|
+
if (EXCLUDED_DIRS.has(entry.name))
|
|
349
|
+
continue;
|
|
350
|
+
addDir(fullPath, zipPath ? `${zipPath}/${entry.name}` : entry.name);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
if (fs.statSync(fullPath).size > 10_485_760)
|
|
354
|
+
continue;
|
|
355
|
+
zip.addLocalFile(fullPath, zipPath || undefined);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
addDir(localDir, '');
|
|
360
|
+
const buffer = zip.toBuffer();
|
|
361
|
+
if (buffer.length > 104_857_600) {
|
|
362
|
+
throw new Error('打包后超过 100MB 限制,请缩小目录范围');
|
|
363
|
+
}
|
|
364
|
+
const query = remoteDir ? `?dir=${encodeURIComponent(remoteDir)}` : '';
|
|
365
|
+
const response = await fetch(`${this.baseUrl}/files${query}`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
368
|
+
body: new Uint8Array(buffer),
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
372
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
373
|
+
}
|
|
374
|
+
const result = await response.json();
|
|
375
|
+
return { fileCount: result.data?.fileCount || 0 };
|
|
376
|
+
}
|
|
377
|
+
/** 清空项目文件存储区 */
|
|
378
|
+
async clearFiles() {
|
|
379
|
+
const url = `${this.baseUrl}/files`;
|
|
380
|
+
const response = await fetch(url, { method: 'DELETE' });
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
383
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ============ 跨项目文件访问 (只读) ============
|
|
387
|
+
/** 跨项目: 列出文件 */
|
|
388
|
+
async crossListFiles(target, dir) {
|
|
389
|
+
const query = dir ? `?dir=${encodeURIComponent(dir)}` : '';
|
|
390
|
+
return this.request(`/cross/${encodeURIComponent(target)}/files${query}`);
|
|
391
|
+
}
|
|
392
|
+
/** 跨项目: 读取文件 */
|
|
393
|
+
async crossReadFile(target, filePath) {
|
|
394
|
+
return this.request(`/cross/${encodeURIComponent(target)}/files/${cleanPath(filePath)}`);
|
|
395
|
+
}
|
|
396
|
+
/** 跨项目: 下载目录 */
|
|
397
|
+
async crossDownloadDir(target, dir) {
|
|
398
|
+
return fetchAndExtractZip(`${this.baseUrl}/cross/${encodeURIComponent(target)}/files-download/${cleanPath(dir)}`);
|
|
399
|
+
}
|
|
275
400
|
}
|
|
276
401
|
// ============ 模块级 API ============
|
|
277
402
|
let client = null;
|
|
@@ -348,3 +473,28 @@ export async function crossGetDocsByStatus(target, statusList) {
|
|
|
348
473
|
export async function crossGetRules(target, ruleType) {
|
|
349
474
|
return getClient().crossGetRules(target, ruleType);
|
|
350
475
|
}
|
|
476
|
+
// ============ 项目文件访问 ============
|
|
477
|
+
export async function listFiles(dir) {
|
|
478
|
+
return getClient().listFiles(dir);
|
|
479
|
+
}
|
|
480
|
+
export async function readFile(filePath) {
|
|
481
|
+
return getClient().readFile(filePath);
|
|
482
|
+
}
|
|
483
|
+
export async function downloadDir(dir) {
|
|
484
|
+
return getClient().downloadDir(dir);
|
|
485
|
+
}
|
|
486
|
+
export async function crossListFiles(target, dir) {
|
|
487
|
+
return getClient().crossListFiles(target, dir);
|
|
488
|
+
}
|
|
489
|
+
export async function crossReadFile(target, filePath) {
|
|
490
|
+
return getClient().crossReadFile(target, filePath);
|
|
491
|
+
}
|
|
492
|
+
export async function crossDownloadDir(target, dir) {
|
|
493
|
+
return getClient().crossDownloadDir(target, dir);
|
|
494
|
+
}
|
|
495
|
+
export async function uploadFiles(localDir, remoteDir) {
|
|
496
|
+
return getClient().uploadFiles(localDir, remoteDir);
|
|
497
|
+
}
|
|
498
|
+
export async function clearFiles() {
|
|
499
|
+
return getClient().clearFiles();
|
|
500
|
+
}
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface Project {
|
|
|
44
44
|
name: string;
|
|
45
45
|
description?: string;
|
|
46
46
|
updatedAt: string;
|
|
47
|
+
createdAt?: string;
|
|
48
|
+
projectPath?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface FileInfo {
|
|
51
|
+
name: string;
|
|
52
|
+
path: string;
|
|
53
|
+
isDir: boolean;
|
|
54
|
+
size: number;
|
|
55
|
+
modifiedAt?: string;
|
|
47
56
|
}
|
|
48
57
|
export type TaskLogType = 'progress' | 'issue' | 'solution' | 'reference';
|
|
49
58
|
export interface TaskLog {
|
package/dist/tools/helpers.d.ts
CHANGED
package/dist/tools/helpers.js
CHANGED
|
@@ -81,3 +81,14 @@ export function countTreeDocs(tree) {
|
|
|
81
81
|
traverse(tree);
|
|
82
82
|
return count;
|
|
83
83
|
}
|
|
84
|
+
// ==================== 文件大小格式化 ====================
|
|
85
|
+
/**
|
|
86
|
+
* 格式化文件大小
|
|
87
|
+
*/
|
|
88
|
+
export function formatFileSize(bytes) {
|
|
89
|
+
if (bytes < 1024)
|
|
90
|
+
return `${bytes} B`;
|
|
91
|
+
if (bytes < 1024 * 1024)
|
|
92
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
93
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
94
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as storage from '../storage/httpClient.js';
|
|
3
3
|
import { decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
|
|
4
|
-
import { wrap, formatDocMarkdown, formatTreeText, countTreeDocs } from './helpers.js';
|
|
4
|
+
import { wrap, formatDocMarkdown, formatTreeText, countTreeDocs, formatFileSize } from './helpers.js';
|
|
5
5
|
export function registerTools(server, projectId, _user) {
|
|
6
6
|
// ===================== 跨项目访问 =====================
|
|
7
7
|
// 0. 列出所有可访问的项目
|
|
@@ -393,4 +393,85 @@ export function registerTools(server, projectId, _user) {
|
|
|
393
393
|
}
|
|
394
394
|
return wrap(`✅ 任务已归档: ${task.title}`);
|
|
395
395
|
});
|
|
396
|
+
// ===================== 项目文件访问 =====================
|
|
397
|
+
// 上传本地目录到中心服务器
|
|
398
|
+
server.tool('project_upload', '上传本地项目目录到中心服务器(打包zip上传,自动排除node_modules/.git等)。上传后其他客户端可通过跨项目访问下载', {
|
|
399
|
+
localDir: z.string().describe('本地目录的绝对路径(如"/home/user/project/src")'),
|
|
400
|
+
remoteDir: z.string().optional().describe('上传到服务器的目标子目录(如"src"),不填则上传到根目录'),
|
|
401
|
+
}, async (args) => {
|
|
402
|
+
try {
|
|
403
|
+
const result = await storage.uploadFiles(args.localDir, args.remoteDir);
|
|
404
|
+
return wrap(`✅ 上传成功\n\n- 文件数量: ${result.fileCount}\n- 目标目录: ${args.remoteDir || '/'}\n\n其他客户端可通过 project_read_file / project_download_dir 访问`);
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
return wrap(`❌ ${String(e)}`);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// 清空项目文件存储区
|
|
411
|
+
server.tool('project_clear_files', '清空当前项目在中心服务器上的文件存储区', {}, async () => {
|
|
412
|
+
try {
|
|
413
|
+
await storage.clearFiles();
|
|
414
|
+
return wrap('✅ 文件存储已清空');
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
return wrap(`❌ ${String(e)}`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// 列出项目文件
|
|
421
|
+
server.tool('project_list_files', '列出项目源码目录下的文件。需要先上传文件或项目已关联源码目录', {
|
|
422
|
+
dir: z.string().optional().describe('子目录路径(如"src/components"),不填则列出根目录'),
|
|
423
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
424
|
+
}, async (args) => {
|
|
425
|
+
try {
|
|
426
|
+
const files = args.targetProject
|
|
427
|
+
? await storage.crossListFiles(args.targetProject, args.dir)
|
|
428
|
+
: await storage.listFiles(args.dir);
|
|
429
|
+
if (files.length === 0) {
|
|
430
|
+
return wrap('目录为空');
|
|
431
|
+
}
|
|
432
|
+
const lines = files.map(f => {
|
|
433
|
+
const icon = f.isDir ? '📁' : '📄';
|
|
434
|
+
const size = f.isDir ? '' : ` (${formatFileSize(f.size)})`;
|
|
435
|
+
return `${icon} ${f.name}${size}`;
|
|
436
|
+
});
|
|
437
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
438
|
+
const dirLabel = args.dir || '/';
|
|
439
|
+
return wrap(`${prefix}📂 ${dirLabel} (${files.length} 项)\n\n${lines.join('\n')}`);
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
return wrap(`❌ ${String(e)}`);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// 读取项目文件
|
|
446
|
+
server.tool('project_read_file', '读取项目源码文件内容(文本,限制1MB)。需要先上传文件或项目已关联源码目录', {
|
|
447
|
+
path: z.string().describe('文件路径(如"src/main.ts")'),
|
|
448
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
449
|
+
}, async (args) => {
|
|
450
|
+
try {
|
|
451
|
+
const content = args.targetProject
|
|
452
|
+
? await storage.crossReadFile(args.targetProject, args.path)
|
|
453
|
+
: await storage.readFile(args.path);
|
|
454
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
455
|
+
return wrap(`${prefix}📄 ${args.path}\n\n\`\`\`\n${content}\n\`\`\``);
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
return wrap(`❌ ${String(e)}`);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
// 下载项目目录 (zip → 自动解压)
|
|
462
|
+
server.tool('project_download_dir', '下载项目源码目录(自动打包zip并解压到本地临时目录)。需要先上传文件或项目已关联源码目录', {
|
|
463
|
+
dir: z.string().describe('目录路径(如"src")'),
|
|
464
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
465
|
+
}, async (args) => {
|
|
466
|
+
try {
|
|
467
|
+
const result = args.targetProject
|
|
468
|
+
? await storage.crossDownloadDir(args.targetProject, args.dir)
|
|
469
|
+
: await storage.downloadDir(args.dir);
|
|
470
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
471
|
+
return wrap(`${prefix}✅ 目录已下载并解压\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}\n\n可直接读取该路径下的文件`);
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
return wrap(`❌ ${String(e)}`);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
396
477
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "ppdocs MCP Server - Knowledge Graph for Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
33
|
-
"
|
|
33
|
+
"adm-zip": "^0.5.16",
|
|
34
34
|
"zod": "^4.1.13"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@types/adm-zip": "^0.5.7",
|
|
37
38
|
"@types/node": "^22.0.0",
|
|
38
|
-
"@types/proper-lockfile": "^4.1.4",
|
|
39
39
|
"typescript": "^5.7.0"
|
|
40
40
|
}
|
|
41
41
|
}
|