@ppdocs/mcp 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/index.js +15 -0
- package/dist/storage/httpClient.d.ts +5 -0
- package/dist/storage/httpClient.js +17 -1
- package/dist/sync/beacon.d.ts +26 -0
- package/dist/sync/beacon.js +186 -0
- package/dist/tools/index.js +59 -0
- package/package.json +5 -1
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -33,6 +33,21 @@ async function main() {
|
|
|
33
33
|
const transport = new StdioServerTransport();
|
|
34
34
|
await server.connect(transport);
|
|
35
35
|
console.error(`ppdocs MCP v${VERSION} | project: ${config.projectId} | user: ${config.user}`);
|
|
36
|
+
// [M2 修复] beacon 变量提升到外部,支持优雅关闭
|
|
37
|
+
let beacon = null;
|
|
38
|
+
// 启动后台代码同步引擎 (Code Beacon)
|
|
39
|
+
try {
|
|
40
|
+
const { SyncBeacon } = await import('./sync/beacon.js');
|
|
41
|
+
beacon = new SyncBeacon(process.cwd(), config.projectId);
|
|
42
|
+
beacon.start();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(`[Code Beacon] Failed to start:`, err);
|
|
46
|
+
}
|
|
47
|
+
// 优雅关闭:进程退出时停止 watcher,防止上传截断
|
|
48
|
+
const shutdown = () => { beacon?.stop(); process.exit(0); };
|
|
49
|
+
process.on('SIGINT', shutdown);
|
|
50
|
+
process.on('SIGTERM', shutdown);
|
|
36
51
|
}
|
|
37
52
|
main().catch((err) => {
|
|
38
53
|
console.error('Fatal error:', err);
|
|
@@ -83,6 +83,10 @@ export declare class PpdocsApiClient {
|
|
|
83
83
|
}>;
|
|
84
84
|
/** 清空项目文件存储区 */
|
|
85
85
|
clearFiles(): Promise<void>;
|
|
86
|
+
/** 上传已打包好的 zip 数据到本项目 files/ (供 Code Beacon 使用) */
|
|
87
|
+
uploadRawZip(zipData: Buffer, remoteDir?: string): Promise<{
|
|
88
|
+
fileCount: number;
|
|
89
|
+
}>;
|
|
86
90
|
/** 跨项目: 列出文件 */
|
|
87
91
|
crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
|
|
88
92
|
/** 跨项目: 读取文件 */
|
|
@@ -94,6 +98,7 @@ export declare class PpdocsApiClient {
|
|
|
94
98
|
}>;
|
|
95
99
|
}
|
|
96
100
|
export declare function initClient(apiUrl: string): void;
|
|
101
|
+
export declare function getClient(): PpdocsApiClient;
|
|
97
102
|
export declare function listDocs(_projectId: string): Promise<DocNode[]>;
|
|
98
103
|
export declare function getDoc(_projectId: string, docPath: string): Promise<DocData | null>;
|
|
99
104
|
export declare function createDoc(_projectId: string, docPath: string, doc: Partial<DocData>): Promise<DocData>;
|
|
@@ -450,6 +450,22 @@ export class PpdocsApiClient {
|
|
|
450
450
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
|
+
// ============ 直接上传已打包的 ZIP ============
|
|
454
|
+
/** 上传已打包好的 zip 数据到本项目 files/ (供 Code Beacon 使用) */
|
|
455
|
+
async uploadRawZip(zipData, remoteDir) {
|
|
456
|
+
const query = remoteDir ? `?dir=${encodeURIComponent(remoteDir)}` : '';
|
|
457
|
+
const response = await fetch(`${this.baseUrl}/files${query}`, {
|
|
458
|
+
method: 'POST',
|
|
459
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
460
|
+
body: new Uint8Array(zipData),
|
|
461
|
+
});
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
464
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
465
|
+
}
|
|
466
|
+
const result = await response.json();
|
|
467
|
+
return { fileCount: result.data?.fileCount || 0 };
|
|
468
|
+
}
|
|
453
469
|
// ============ 跨项目文件访问 (只读) ============
|
|
454
470
|
/** 跨项目: 列出文件 */
|
|
455
471
|
async crossListFiles(target, dir) {
|
|
@@ -470,7 +486,7 @@ let client = null;
|
|
|
470
486
|
export function initClient(apiUrl) {
|
|
471
487
|
client = new PpdocsApiClient(apiUrl);
|
|
472
488
|
}
|
|
473
|
-
function getClient() {
|
|
489
|
+
export function getClient() {
|
|
474
490
|
if (!client) {
|
|
475
491
|
throw new Error('API client not initialized. Call initClient(apiUrl) first.');
|
|
476
492
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class SyncBeacon {
|
|
2
|
+
private cwd;
|
|
3
|
+
private projectId;
|
|
4
|
+
private debounceMs;
|
|
5
|
+
private watcher;
|
|
6
|
+
private isSyncing;
|
|
7
|
+
private pendingSync;
|
|
8
|
+
private debounceTimer;
|
|
9
|
+
constructor(cwd: string, projectId: string, debounceMs?: number);
|
|
10
|
+
/**
|
|
11
|
+
* 启动同步引擎
|
|
12
|
+
*/
|
|
13
|
+
start(): void;
|
|
14
|
+
/**
|
|
15
|
+
* 停止同步引擎
|
|
16
|
+
*/
|
|
17
|
+
stop(): void;
|
|
18
|
+
/**
|
|
19
|
+
* 触发同步计算
|
|
20
|
+
*/
|
|
21
|
+
private triggerSync;
|
|
22
|
+
/**
|
|
23
|
+
* 核心: 打包并推送全量快照
|
|
24
|
+
*/
|
|
25
|
+
private performSync;
|
|
26
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
import * as chokidar from 'chokidar';
|
|
6
|
+
import { getClient } from '../storage/httpClient.js';
|
|
7
|
+
// 需要排除的大文件/敏感目录
|
|
8
|
+
const EXCLUDED_DIRS = [
|
|
9
|
+
'node_modules',
|
|
10
|
+
'.git',
|
|
11
|
+
'.next',
|
|
12
|
+
'dist',
|
|
13
|
+
'build',
|
|
14
|
+
'out',
|
|
15
|
+
'target',
|
|
16
|
+
'__pycache__',
|
|
17
|
+
'.venv',
|
|
18
|
+
'venv',
|
|
19
|
+
'.idea',
|
|
20
|
+
'.vscode',
|
|
21
|
+
'.vs',
|
|
22
|
+
'.ppdocs',
|
|
23
|
+
'.cursor',
|
|
24
|
+
'.claude',
|
|
25
|
+
'.gemini',
|
|
26
|
+
];
|
|
27
|
+
export class SyncBeacon {
|
|
28
|
+
cwd;
|
|
29
|
+
projectId;
|
|
30
|
+
debounceMs;
|
|
31
|
+
watcher = null;
|
|
32
|
+
isSyncing = false;
|
|
33
|
+
pendingSync = false;
|
|
34
|
+
debounceTimer = null;
|
|
35
|
+
constructor(cwd, projectId, debounceMs = 15000 // 默认 15 秒防抖
|
|
36
|
+
) {
|
|
37
|
+
this.cwd = cwd;
|
|
38
|
+
this.projectId = projectId;
|
|
39
|
+
this.debounceMs = debounceMs;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 启动同步引擎
|
|
43
|
+
*/
|
|
44
|
+
start() {
|
|
45
|
+
if (this.watcher)
|
|
46
|
+
return;
|
|
47
|
+
console.error(`[Code Beacon] Started monitoring project: ${this.projectId}`);
|
|
48
|
+
// [M1 修复] 首次启动延迟 3 秒,让 MCP 工具注册和事件循环先稳定
|
|
49
|
+
setTimeout(() => this.triggerSync(), 3000);
|
|
50
|
+
// 配置监听器
|
|
51
|
+
this.watcher = chokidar.watch(this.cwd, {
|
|
52
|
+
ignored: (filePath) => {
|
|
53
|
+
const basename = path.basename(filePath);
|
|
54
|
+
if (EXCLUDED_DIRS.includes(basename))
|
|
55
|
+
return true;
|
|
56
|
+
if (basename.startsWith('.') && basename !== '.env.example' && !filePath.includes('.cursorrules') && !filePath.includes('ppdocs.md'))
|
|
57
|
+
return true;
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
persistent: true,
|
|
61
|
+
ignoreInitial: true,
|
|
62
|
+
});
|
|
63
|
+
// 绑定事件
|
|
64
|
+
const scheduleSync = (evt, p) => {
|
|
65
|
+
if (this.debounceTimer) {
|
|
66
|
+
clearTimeout(this.debounceTimer);
|
|
67
|
+
}
|
|
68
|
+
this.debounceTimer = setTimeout(() => {
|
|
69
|
+
console.error(`[Code Beacon] Changes detected (${evt}: ${path.basename(p)}), scheduling sync...`);
|
|
70
|
+
this.triggerSync();
|
|
71
|
+
}, this.debounceMs);
|
|
72
|
+
};
|
|
73
|
+
this.watcher
|
|
74
|
+
.on('add', (p) => scheduleSync('add', p))
|
|
75
|
+
.on('change', (p) => scheduleSync('change', p))
|
|
76
|
+
.on('unlink', (p) => scheduleSync('delete', p))
|
|
77
|
+
.on('unlinkDir', (p) => scheduleSync('delete dir', p));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 停止同步引擎
|
|
81
|
+
*/
|
|
82
|
+
stop() {
|
|
83
|
+
if (this.debounceTimer) {
|
|
84
|
+
clearTimeout(this.debounceTimer);
|
|
85
|
+
this.debounceTimer = null;
|
|
86
|
+
}
|
|
87
|
+
if (this.watcher) {
|
|
88
|
+
this.watcher.close();
|
|
89
|
+
this.watcher = null;
|
|
90
|
+
console.error(`[Code Beacon] Stopped monitoring project: ${this.projectId}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 触发同步计算
|
|
95
|
+
*/
|
|
96
|
+
async triggerSync() {
|
|
97
|
+
if (this.isSyncing) {
|
|
98
|
+
this.pendingSync = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.isSyncing = true;
|
|
102
|
+
this.pendingSync = false;
|
|
103
|
+
try {
|
|
104
|
+
await this.performSync();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(`[Code Beacon] Sync error:`, error);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.isSyncing = false;
|
|
111
|
+
if (this.pendingSync) {
|
|
112
|
+
this.triggerSync();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 核心: 打包并推送全量快照
|
|
118
|
+
*/
|
|
119
|
+
async performSync() {
|
|
120
|
+
// [C3 修复] getClient() 在未初始化时 throw,不会返回 null
|
|
121
|
+
let client;
|
|
122
|
+
try {
|
|
123
|
+
client = getClient();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
console.warn('[Code Beacon] API Client not ready, skipping sync.');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
try {
|
|
131
|
+
// 使用系统临时目录存放打包文件,避免 .ppdocs(是配置文件非目录)冲突
|
|
132
|
+
const tmpFile = path.join(os.tmpdir(), `beacon-${this.projectId}-${Date.now()}.zip`);
|
|
133
|
+
const output = fs.createWriteStream(tmpFile);
|
|
134
|
+
const archive = archiver('zip', {
|
|
135
|
+
zlib: { level: 3 } // 低压缩率,追求速度
|
|
136
|
+
});
|
|
137
|
+
// [C1 修复] 监听 WriteStream 的 error 事件,防止 Node 进程崩溃
|
|
138
|
+
output.on('error', (err) => {
|
|
139
|
+
console.error('[Code Beacon] WriteStream error:', err);
|
|
140
|
+
reject(err);
|
|
141
|
+
});
|
|
142
|
+
output.on('close', async () => {
|
|
143
|
+
try {
|
|
144
|
+
const data = fs.readFileSync(tmpFile);
|
|
145
|
+
// [C2 修复] 使用本项目标准上传接口,而非 crossUploadFiles
|
|
146
|
+
await client.uploadRawZip(data);
|
|
147
|
+
console.error(`[Code Beacon] Snapshot synced: ${archive.pointer()} bytes`);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error('[Code Beacon] Upload failed:', e);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
try {
|
|
154
|
+
if (fs.existsSync(tmpFile))
|
|
155
|
+
fs.unlinkSync(tmpFile);
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
resolve();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
archive.on('error', (err) => {
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
archive.pipe(output);
|
|
165
|
+
// 遍历所有非隐藏、非排除的文件打包
|
|
166
|
+
archive.glob('**/*', {
|
|
167
|
+
cwd: this.cwd,
|
|
168
|
+
ignore: [
|
|
169
|
+
...EXCLUDED_DIRS.map(d => `${d}/**`),
|
|
170
|
+
'.ppdocs',
|
|
171
|
+
'.*'
|
|
172
|
+
],
|
|
173
|
+
dot: false
|
|
174
|
+
});
|
|
175
|
+
// [A2 修复] 排除 .env(敏感凭证),仅包含安全的 IDE 配置
|
|
176
|
+
if (fs.existsSync(path.join(this.cwd, '.cursorrules'))) {
|
|
177
|
+
archive.file(path.join(this.cwd, '.cursorrules'), { name: '.cursorrules' });
|
|
178
|
+
}
|
|
179
|
+
archive.finalize();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
reject(err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -407,6 +407,65 @@ export function registerTools(server, projectId, _user) {
|
|
|
407
407
|
const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
|
|
408
408
|
return wrap(`找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
|
|
409
409
|
});
|
|
410
|
+
// 12. 跨项目复制文档到当前项目
|
|
411
|
+
server.tool('kg_copy_node', '从其他项目复制文档到当前项目。支持单个或批量复制,可指定新路径。用于跨项目协作:拉取别的项目的知识文档到当前项目', {
|
|
412
|
+
sourceProject: z.string().describe('源项目ID或名称(从kg_list_projects获取)'),
|
|
413
|
+
sourcePath: z.union([z.string(), z.array(z.string())]).describe('源文档路径(单个或数组,如"/架构/数据流"或["/架构/数据流","/MCP/tools"])'),
|
|
414
|
+
targetPath: z.string().optional().describe('目标路径前缀(如"/参考/项目B")。不填则保持原路径'),
|
|
415
|
+
}, async (args) => {
|
|
416
|
+
const decoded = decodeObjectStrings(args);
|
|
417
|
+
const paths = Array.isArray(decoded.sourcePath) ? decoded.sourcePath : [decoded.sourcePath];
|
|
418
|
+
const results = [];
|
|
419
|
+
for (const srcPath of paths) {
|
|
420
|
+
try {
|
|
421
|
+
// 从源项目读取文档
|
|
422
|
+
const doc = await storage.crossGetDoc(decoded.sourceProject, srcPath);
|
|
423
|
+
if (!doc) {
|
|
424
|
+
results.push({ path: srcPath, success: false, error: '源文档不存在' });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
// 计算目标路径
|
|
428
|
+
const destPath = decoded.targetPath
|
|
429
|
+
? `${decoded.targetPath.replace(/\/$/, '')}${srcPath}`
|
|
430
|
+
: srcPath;
|
|
431
|
+
// 检查目标是否已存在
|
|
432
|
+
const existing = await storage.getDoc(projectId, destPath);
|
|
433
|
+
if (existing) {
|
|
434
|
+
// 已存在则更新
|
|
435
|
+
await storage.updateDoc(projectId, destPath, {
|
|
436
|
+
summary: doc.summary,
|
|
437
|
+
content: doc.content,
|
|
438
|
+
});
|
|
439
|
+
results.push({ path: destPath, success: true });
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// 不存在则创建
|
|
443
|
+
await storage.createDoc(projectId, destPath, {
|
|
444
|
+
summary: doc.summary,
|
|
445
|
+
content: doc.content,
|
|
446
|
+
status: doc.status,
|
|
447
|
+
});
|
|
448
|
+
results.push({ path: destPath, success: true });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
results.push({ path: srcPath, success: false, error: String(e) });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const successCount = results.filter(r => r.success).length;
|
|
456
|
+
const failedItems = results.filter(r => !r.success);
|
|
457
|
+
if (paths.length === 1) {
|
|
458
|
+
if (results[0].success) {
|
|
459
|
+
return wrap(`✅ 已复制文档: ${results[0].path}\n\n来源: [${decoded.sourceProject}] ${paths[0]}`);
|
|
460
|
+
}
|
|
461
|
+
return wrap(`❌ 复制失败: ${results[0].error}`);
|
|
462
|
+
}
|
|
463
|
+
let msg = `✅ 批量复制完成: ${successCount}/${paths.length} 成功`;
|
|
464
|
+
if (failedItems.length > 0) {
|
|
465
|
+
msg += `\n❌ 失败:\n${failedItems.map(f => ` - ${f.path}: ${f.error}`).join('\n')}`;
|
|
466
|
+
}
|
|
467
|
+
return wrap(msg);
|
|
468
|
+
});
|
|
410
469
|
// ===================== 任务管理 =====================
|
|
411
470
|
// 7. 创建任务
|
|
412
471
|
server.tool('task_create', '创建开发任务', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.5",
|
|
4
4
|
"description": "ppdocs MCP Server - Knowledge Graph for Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,10 +31,14 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
33
33
|
"adm-zip": "^0.5.16",
|
|
34
|
+
"archiver": "^7.0.1",
|
|
35
|
+
"chokidar": "^5.0.0",
|
|
34
36
|
"zod": "^4.1.13"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@types/adm-zip": "^0.5.7",
|
|
40
|
+
"@types/archiver": "^7.0.0",
|
|
41
|
+
"@types/chokidar": "^1.7.5",
|
|
38
42
|
"@types/node": "^22.0.0",
|
|
39
43
|
"typescript": "^5.7.0"
|
|
40
44
|
}
|