@ppdocs/mcp 2.9.0 → 3.0.1
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/storage/httpClient.d.ts +69 -3
- package/dist/storage/httpClient.js +250 -14
- package/dist/storage/types.d.ts +15 -0
- package/dist/tools/helpers.d.ts +4 -0
- package/dist/tools/helpers.js +11 -0
- package/dist/tools/index.js +193 -12
- package/dist/utils.d.ts +4 -4
- package/dist/utils.js +13 -8
- package/package.json +3 -3
- package/templates/hooks/__pycache__/hook.cpython-314.pyc +0 -0
- package/templates/hooks/hook.py +86 -85
|
@@ -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, RuleMeta } from './types.js';
|
|
8
8
|
interface TreeNode {
|
|
9
9
|
path: string;
|
|
10
10
|
name: string;
|
|
@@ -14,6 +14,7 @@ interface TreeNode {
|
|
|
14
14
|
}
|
|
15
15
|
export declare class PpdocsApiClient {
|
|
16
16
|
private baseUrl;
|
|
17
|
+
private serverUrl;
|
|
17
18
|
constructor(apiUrl: string);
|
|
18
19
|
private request;
|
|
19
20
|
listDocs(): Promise<DocNode[]>;
|
|
@@ -28,6 +29,11 @@ export declare class PpdocsApiClient {
|
|
|
28
29
|
getTree(): Promise<TreeNode[]>;
|
|
29
30
|
getRulesApi(ruleType: string): Promise<string[]>;
|
|
30
31
|
saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
|
|
32
|
+
getRulesMeta(): Promise<Record<string, RuleMeta>>;
|
|
33
|
+
saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
|
|
34
|
+
crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
|
|
35
|
+
getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
|
|
36
|
+
saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
|
|
31
37
|
listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
|
|
32
38
|
getTask(taskId: string): Promise<Task | null>;
|
|
33
39
|
createTask(task: {
|
|
@@ -37,12 +43,20 @@ export declare class PpdocsApiClient {
|
|
|
37
43
|
}, creator: string): Promise<Task>;
|
|
38
44
|
addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
|
|
39
45
|
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
|
+
}>;
|
|
40
54
|
/** 列出所有可访问的项目 */
|
|
41
55
|
crossListProjects(): Promise<{
|
|
42
56
|
id: string;
|
|
43
57
|
name: string;
|
|
44
58
|
description: string;
|
|
45
|
-
|
|
59
|
+
updatedAt: string;
|
|
46
60
|
}[]>;
|
|
47
61
|
/** 跨项目: 列出文档 */
|
|
48
62
|
crossListDocs(target: string): Promise<DocNode[]>;
|
|
@@ -54,6 +68,30 @@ export declare class PpdocsApiClient {
|
|
|
54
68
|
crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
|
|
55
69
|
/** 跨项目: 获取规则 */
|
|
56
70
|
crossGetRules(target: string, ruleType: string): Promise<string[]>;
|
|
71
|
+
/** 列出项目文件 */
|
|
72
|
+
listFiles(dir?: string): Promise<FileInfo[]>;
|
|
73
|
+
/** 读取项目文件 */
|
|
74
|
+
readFile(filePath: string): Promise<string>;
|
|
75
|
+
/** 下载项目目录 (zip) → 自动解压到 temp 目录 */
|
|
76
|
+
downloadDir(dir: string): Promise<{
|
|
77
|
+
localPath: string;
|
|
78
|
+
fileCount: number;
|
|
79
|
+
}>;
|
|
80
|
+
/** 上传本地目录到中心服务器 (打包 zip → POST) */
|
|
81
|
+
uploadFiles(localDir: string, remoteDir?: string): Promise<{
|
|
82
|
+
fileCount: number;
|
|
83
|
+
}>;
|
|
84
|
+
/** 清空项目文件存储区 */
|
|
85
|
+
clearFiles(): Promise<void>;
|
|
86
|
+
/** 跨项目: 列出文件 */
|
|
87
|
+
crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
|
|
88
|
+
/** 跨项目: 读取文件 */
|
|
89
|
+
crossReadFile(target: string, filePath: string): Promise<string>;
|
|
90
|
+
/** 跨项目: 下载目录 */
|
|
91
|
+
crossDownloadDir(target: string, dir: string): Promise<{
|
|
92
|
+
localPath: string;
|
|
93
|
+
fileCount: number;
|
|
94
|
+
}>;
|
|
57
95
|
}
|
|
58
96
|
export declare function initClient(apiUrl: string): void;
|
|
59
97
|
export declare function listDocs(_projectId: string): Promise<DocNode[]>;
|
|
@@ -66,6 +104,11 @@ export declare function getDocsByStatus(_projectId: string, statusList: string[]
|
|
|
66
104
|
export declare function getTree(_projectId: string): Promise<TreeNode[]>;
|
|
67
105
|
export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
|
|
68
106
|
export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
|
|
107
|
+
export declare function getRulesMeta(): Promise<Record<string, RuleMeta>>;
|
|
108
|
+
export declare function saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
|
|
109
|
+
export declare function crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
|
|
110
|
+
export declare function getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
|
|
111
|
+
export declare function saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
|
|
69
112
|
export declare function listTasks(_projectId: string, status?: 'active' | 'archived'): Promise<TaskSummary[]>;
|
|
70
113
|
export declare function getTask(_projectId: string, taskId: string): Promise<Task | null>;
|
|
71
114
|
export declare function createTask(_projectId: string, task: {
|
|
@@ -75,14 +118,37 @@ export declare function createTask(_projectId: string, task: {
|
|
|
75
118
|
}, creator: string): Promise<Task>;
|
|
76
119
|
export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
|
|
77
120
|
export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
|
|
121
|
+
export declare function createProject(id: string, name: string, description?: string, projectPath?: string): Promise<{
|
|
122
|
+
project: {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
};
|
|
126
|
+
password: string;
|
|
127
|
+
}>;
|
|
78
128
|
export declare function crossListProjects(): Promise<{
|
|
79
129
|
id: string;
|
|
80
130
|
name: string;
|
|
81
131
|
description: string;
|
|
82
|
-
|
|
132
|
+
updatedAt: string;
|
|
83
133
|
}[]>;
|
|
84
134
|
export declare function crossGetDoc(target: string, docPath: string): Promise<DocData | null>;
|
|
85
135
|
export declare function crossGetTree(target: string): Promise<TreeNode[]>;
|
|
86
136
|
export declare function crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
|
|
87
137
|
export declare function crossGetRules(target: string, ruleType: string): Promise<string[]>;
|
|
138
|
+
export declare function listFiles(dir?: string): Promise<FileInfo[]>;
|
|
139
|
+
export declare function readFile(filePath: string): Promise<string>;
|
|
140
|
+
export declare function downloadDir(dir: string): Promise<{
|
|
141
|
+
localPath: string;
|
|
142
|
+
fileCount: number;
|
|
143
|
+
}>;
|
|
144
|
+
export declare function crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
|
|
145
|
+
export declare function crossReadFile(target: string, filePath: string): Promise<string>;
|
|
146
|
+
export declare function crossDownloadDir(target: string, dir: string): Promise<{
|
|
147
|
+
localPath: string;
|
|
148
|
+
fileCount: number;
|
|
149
|
+
}>;
|
|
150
|
+
export declare function uploadFiles(localDir: string, remoteDir?: string): Promise<{
|
|
151
|
+
fileCount: number;
|
|
152
|
+
}>;
|
|
153
|
+
export declare function clearFiles(): Promise<void>;
|
|
88
154
|
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 = [];
|
|
@@ -40,9 +91,12 @@ function buildTree(docs) {
|
|
|
40
91
|
// API 客户端类
|
|
41
92
|
export class PpdocsApiClient {
|
|
42
93
|
baseUrl; // http://localhost:20001/api/projectId/password
|
|
94
|
+
serverUrl; // http://localhost:20001
|
|
43
95
|
constructor(apiUrl) {
|
|
44
|
-
// 移除末尾斜杠
|
|
45
96
|
this.baseUrl = apiUrl.replace(/\/$/, '');
|
|
97
|
+
// 从 /api/projectId/password 提取服务器根地址
|
|
98
|
+
const apiIndex = this.baseUrl.indexOf('/api/');
|
|
99
|
+
this.serverUrl = apiIndex > 0 ? this.baseUrl.substring(0, apiIndex) : this.baseUrl;
|
|
46
100
|
}
|
|
47
101
|
// ============ HTTP 请求工具 ============
|
|
48
102
|
async request(path, options = {}) {
|
|
@@ -84,16 +138,13 @@ export class PpdocsApiClient {
|
|
|
84
138
|
}
|
|
85
139
|
async getDoc(docPath) {
|
|
86
140
|
try {
|
|
87
|
-
|
|
88
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
89
|
-
return await this.request(`/docs/${cleanPath}`);
|
|
141
|
+
return await this.request(`/docs/${cleanPath(docPath)}`);
|
|
90
142
|
}
|
|
91
143
|
catch {
|
|
92
144
|
return null;
|
|
93
145
|
}
|
|
94
146
|
}
|
|
95
147
|
async createDoc(docPath, doc) {
|
|
96
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
97
148
|
const payload = {
|
|
98
149
|
summary: doc.summary || '',
|
|
99
150
|
content: doc.content || '',
|
|
@@ -103,17 +154,15 @@ export class PpdocsApiClient {
|
|
|
103
154
|
changes: '初始创建'
|
|
104
155
|
}],
|
|
105
156
|
bugfixes: doc.bugfixes || [],
|
|
106
|
-
status: doc.status || '已完成'
|
|
157
|
+
status: doc.status || '已完成'
|
|
107
158
|
};
|
|
108
|
-
return this.request(`/docs/${cleanPath}`, {
|
|
159
|
+
return this.request(`/docs/${cleanPath(docPath)}`, {
|
|
109
160
|
method: 'POST',
|
|
110
161
|
body: JSON.stringify(payload)
|
|
111
162
|
});
|
|
112
163
|
}
|
|
113
164
|
async updateDoc(docPath, updates) {
|
|
114
165
|
try {
|
|
115
|
-
const cleanPath = docPath.replace(/^\//, '');
|
|
116
|
-
// 先获取现有文档
|
|
117
166
|
const existing = await this.getDoc(docPath);
|
|
118
167
|
if (!existing)
|
|
119
168
|
return null;
|
|
@@ -124,7 +173,7 @@ export class PpdocsApiClient {
|
|
|
124
173
|
bugfixes: updates.bugfixes ?? existing.bugfixes,
|
|
125
174
|
status: updates.status ?? existing.status ?? '已完成'
|
|
126
175
|
};
|
|
127
|
-
return await this.request(`/docs/${cleanPath}`, {
|
|
176
|
+
return await this.request(`/docs/${cleanPath(docPath)}`, {
|
|
128
177
|
method: 'PUT',
|
|
129
178
|
body: JSON.stringify(payload)
|
|
130
179
|
});
|
|
@@ -135,8 +184,7 @@ export class PpdocsApiClient {
|
|
|
135
184
|
}
|
|
136
185
|
async deleteDoc(docPath) {
|
|
137
186
|
try {
|
|
138
|
-
|
|
139
|
-
await this.request(`/docs/${cleanPath}`, { method: 'DELETE' });
|
|
187
|
+
await this.request(`/docs/${cleanPath(docPath)}`, { method: 'DELETE' });
|
|
140
188
|
return true;
|
|
141
189
|
}
|
|
142
190
|
catch {
|
|
@@ -182,6 +230,54 @@ export class PpdocsApiClient {
|
|
|
182
230
|
return false;
|
|
183
231
|
}
|
|
184
232
|
}
|
|
233
|
+
async getRulesMeta() {
|
|
234
|
+
try {
|
|
235
|
+
return await this.request('/rules-meta');
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async saveRulesMeta(meta) {
|
|
242
|
+
try {
|
|
243
|
+
await this.request('/rules-meta', {
|
|
244
|
+
method: 'PUT',
|
|
245
|
+
body: JSON.stringify(meta)
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async crossGetRulesMeta(target) {
|
|
254
|
+
try {
|
|
255
|
+
return await this.request(`/cross/${encodeURIComponent(target)}/rules-meta`);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async getGlobalRulesMeta() {
|
|
262
|
+
try {
|
|
263
|
+
return await this.request('/global-rules-meta');
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async saveGlobalRulesMeta(meta) {
|
|
270
|
+
try {
|
|
271
|
+
await this.request('/global-rules-meta', {
|
|
272
|
+
method: 'PUT',
|
|
273
|
+
body: JSON.stringify(meta)
|
|
274
|
+
});
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
185
281
|
// ============ 任务管理 ============
|
|
186
282
|
async listTasks(status) {
|
|
187
283
|
const query = status ? `?status=${status}` : '';
|
|
@@ -232,6 +328,22 @@ export class PpdocsApiClient {
|
|
|
232
328
|
return null;
|
|
233
329
|
}
|
|
234
330
|
}
|
|
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
|
+
}
|
|
235
347
|
// ============ 跨项目只读访问 ============
|
|
236
348
|
/** 列出所有可访问的项目 */
|
|
237
349
|
async crossListProjects() {
|
|
@@ -244,8 +356,7 @@ export class PpdocsApiClient {
|
|
|
244
356
|
/** 跨项目: 获取文档 */
|
|
245
357
|
async crossGetDoc(target, docPath) {
|
|
246
358
|
try {
|
|
247
|
-
|
|
248
|
-
return await this.request(`/cross/${encodeURIComponent(target)}/docs/${cleanPath}`);
|
|
359
|
+
return await this.request(`/cross/${encodeURIComponent(target)}/docs/${cleanPath(docPath)}`);
|
|
249
360
|
}
|
|
250
361
|
catch {
|
|
251
362
|
return null;
|
|
@@ -272,6 +383,87 @@ export class PpdocsApiClient {
|
|
|
272
383
|
return [];
|
|
273
384
|
}
|
|
274
385
|
}
|
|
386
|
+
// ============ 项目文件访问 ============
|
|
387
|
+
/** 列出项目文件 */
|
|
388
|
+
async listFiles(dir) {
|
|
389
|
+
const query = dir ? `?dir=${encodeURIComponent(dir)}` : '';
|
|
390
|
+
return this.request(`/files${query}`);
|
|
391
|
+
}
|
|
392
|
+
/** 读取项目文件 */
|
|
393
|
+
async readFile(filePath) {
|
|
394
|
+
return this.request(`/files/${cleanPath(filePath)}`);
|
|
395
|
+
}
|
|
396
|
+
/** 下载项目目录 (zip) → 自动解压到 temp 目录 */
|
|
397
|
+
async downloadDir(dir) {
|
|
398
|
+
return fetchAndExtractZip(`${this.baseUrl}/files-download/${cleanPath(dir)}`);
|
|
399
|
+
}
|
|
400
|
+
/** 上传本地目录到中心服务器 (打包 zip → POST) */
|
|
401
|
+
async uploadFiles(localDir, remoteDir) {
|
|
402
|
+
const fs = await import('fs');
|
|
403
|
+
const path = await import('path');
|
|
404
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
405
|
+
if (!fs.existsSync(localDir) || !fs.statSync(localDir).isDirectory()) {
|
|
406
|
+
throw new Error(`本地目录不存在: ${localDir}`);
|
|
407
|
+
}
|
|
408
|
+
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))
|
|
416
|
+
continue;
|
|
417
|
+
addDir(fullPath, zipPath ? `${zipPath}/${entry.name}` : entry.name);
|
|
418
|
+
}
|
|
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, '');
|
|
427
|
+
const buffer = zip.toBuffer();
|
|
428
|
+
if (buffer.length > 104_857_600) {
|
|
429
|
+
throw new Error('打包后超过 100MB 限制,请缩小目录范围');
|
|
430
|
+
}
|
|
431
|
+
const query = remoteDir ? `?dir=${encodeURIComponent(remoteDir)}` : '';
|
|
432
|
+
const response = await fetch(`${this.baseUrl}/files${query}`, {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
435
|
+
body: new Uint8Array(buffer),
|
|
436
|
+
});
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
439
|
+
throw new Error(error.error || `HTTP ${response.status}`);
|
|
440
|
+
}
|
|
441
|
+
const result = await response.json();
|
|
442
|
+
return { fileCount: result.data?.fileCount || 0 };
|
|
443
|
+
}
|
|
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
|
+
// ============ 跨项目文件访问 (只读) ============
|
|
454
|
+
/** 跨项目: 列出文件 */
|
|
455
|
+
async crossListFiles(target, dir) {
|
|
456
|
+
const query = dir ? `?dir=${encodeURIComponent(dir)}` : '';
|
|
457
|
+
return this.request(`/cross/${encodeURIComponent(target)}/files${query}`);
|
|
458
|
+
}
|
|
459
|
+
/** 跨项目: 读取文件 */
|
|
460
|
+
async crossReadFile(target, filePath) {
|
|
461
|
+
return this.request(`/cross/${encodeURIComponent(target)}/files/${cleanPath(filePath)}`);
|
|
462
|
+
}
|
|
463
|
+
/** 跨项目: 下载目录 */
|
|
464
|
+
async crossDownloadDir(target, dir) {
|
|
465
|
+
return fetchAndExtractZip(`${this.baseUrl}/cross/${encodeURIComponent(target)}/files-download/${cleanPath(dir)}`);
|
|
466
|
+
}
|
|
275
467
|
}
|
|
276
468
|
// ============ 模块级 API ============
|
|
277
469
|
let client = null;
|
|
@@ -316,6 +508,21 @@ export async function getRules(_projectId, ruleType) {
|
|
|
316
508
|
export async function saveRules(_projectId, ruleType, rules) {
|
|
317
509
|
return getClient().saveRulesApi(ruleType, rules);
|
|
318
510
|
}
|
|
511
|
+
export async function getRulesMeta() {
|
|
512
|
+
return getClient().getRulesMeta();
|
|
513
|
+
}
|
|
514
|
+
export async function saveRulesMeta(meta) {
|
|
515
|
+
return getClient().saveRulesMeta(meta);
|
|
516
|
+
}
|
|
517
|
+
export async function crossGetRulesMeta(target) {
|
|
518
|
+
return getClient().crossGetRulesMeta(target);
|
|
519
|
+
}
|
|
520
|
+
export async function getGlobalRulesMeta() {
|
|
521
|
+
return getClient().getGlobalRulesMeta();
|
|
522
|
+
}
|
|
523
|
+
export async function saveGlobalRulesMeta(meta) {
|
|
524
|
+
return getClient().saveGlobalRulesMeta(meta);
|
|
525
|
+
}
|
|
319
526
|
// ============ 任务管理 ============
|
|
320
527
|
export async function listTasks(_projectId, status) {
|
|
321
528
|
return getClient().listTasks(status);
|
|
@@ -332,6 +539,10 @@ export async function addTaskLog(_projectId, taskId, logType, content) {
|
|
|
332
539
|
export async function completeTask(_projectId, taskId, experience) {
|
|
333
540
|
return getClient().completeTask(taskId, experience);
|
|
334
541
|
}
|
|
542
|
+
// ============ 项目管理 ============
|
|
543
|
+
export async function createProject(id, name, description, projectPath) {
|
|
544
|
+
return getClient().createProject(id, name, description, projectPath);
|
|
545
|
+
}
|
|
335
546
|
// ============ 跨项目只读访问 ============
|
|
336
547
|
export async function crossListProjects() {
|
|
337
548
|
return getClient().crossListProjects();
|
|
@@ -348,3 +559,28 @@ export async function crossGetDocsByStatus(target, statusList) {
|
|
|
348
559
|
export async function crossGetRules(target, ruleType) {
|
|
349
560
|
return getClient().crossGetRules(target, ruleType);
|
|
350
561
|
}
|
|
562
|
+
// ============ 项目文件访问 ============
|
|
563
|
+
export async function listFiles(dir) {
|
|
564
|
+
return getClient().listFiles(dir);
|
|
565
|
+
}
|
|
566
|
+
export async function readFile(filePath) {
|
|
567
|
+
return getClient().readFile(filePath);
|
|
568
|
+
}
|
|
569
|
+
export async function downloadDir(dir) {
|
|
570
|
+
return getClient().downloadDir(dir);
|
|
571
|
+
}
|
|
572
|
+
export async function crossListFiles(target, dir) {
|
|
573
|
+
return getClient().crossListFiles(target, dir);
|
|
574
|
+
}
|
|
575
|
+
export async function crossReadFile(target, filePath) {
|
|
576
|
+
return getClient().crossReadFile(target, filePath);
|
|
577
|
+
}
|
|
578
|
+
export async function crossDownloadDir(target, dir) {
|
|
579
|
+
return getClient().crossDownloadDir(target, dir);
|
|
580
|
+
}
|
|
581
|
+
export async function uploadFiles(localDir, remoteDir) {
|
|
582
|
+
return getClient().uploadFiles(localDir, remoteDir);
|
|
583
|
+
}
|
|
584
|
+
export async function clearFiles() {
|
|
585
|
+
return getClient().clearFiles();
|
|
586
|
+
}
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -44,6 +44,21 @@ 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 RuleMeta {
|
|
51
|
+
label: string;
|
|
52
|
+
keywords: string[];
|
|
53
|
+
min_hits: number;
|
|
54
|
+
always?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface FileInfo {
|
|
57
|
+
name: string;
|
|
58
|
+
path: string;
|
|
59
|
+
isDir: boolean;
|
|
60
|
+
size: number;
|
|
61
|
+
modifiedAt?: string;
|
|
47
62
|
}
|
|
48
63
|
export type TaskLogType = 'progress' | 'issue' | 'solution' | 'reference';
|
|
49
64
|
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. 列出所有可访问的项目
|
|
@@ -21,6 +21,21 @@ export function registerTools(server, projectId, _user) {
|
|
|
21
21
|
return wrap(`获取项目列表失败: ${String(e)}`);
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
+
// 0.1 创建项目
|
|
25
|
+
server.tool('kg_create_project', '创建新项目。返回项目ID和密码(密码用于MCP连接)', {
|
|
26
|
+
id: z.string().describe('项目ID(英文/数字/中文,如"my-app")'),
|
|
27
|
+
name: z.string().describe('项目名称'),
|
|
28
|
+
description: z.string().optional().describe('项目简介'),
|
|
29
|
+
projectPath: z.string().optional().describe('项目源码路径(本地绝对路径)'),
|
|
30
|
+
}, async (args) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await storage.createProject(args.id, args.name, args.description, args.projectPath);
|
|
33
|
+
return wrap(`✅ 项目创建成功\n\n- 项目ID: ${result.project.id}\n- 项目名: ${result.project.name}\n- 密码: ${result.password}\n\nMCP 连接地址: http://<服务器IP>:20001/api/${result.project.id}/${result.password}`);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return wrap(`❌ ${String(e)}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
24
39
|
// ===================== 文档管理 =====================
|
|
25
40
|
// 1. 创建文档
|
|
26
41
|
server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
|
|
@@ -183,25 +198,29 @@ export function registerTools(server, projectId, _user) {
|
|
|
183
198
|
});
|
|
184
199
|
// 3.6 获取项目规则
|
|
185
200
|
server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)。支持跨项目只读访问', {
|
|
186
|
-
ruleType: z.
|
|
187
|
-
.describe('
|
|
201
|
+
ruleType: z.string().optional()
|
|
202
|
+
.describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)。不传则返回全部'),
|
|
188
203
|
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
189
204
|
}, async (args) => {
|
|
190
205
|
// 跨项目访问
|
|
191
206
|
if (args.targetProject) {
|
|
207
|
+
const crossMeta = await storage.crossGetRulesMeta(args.targetProject);
|
|
192
208
|
if (args.ruleType) {
|
|
193
209
|
const rules = await storage.crossGetRules(args.targetProject, args.ruleType);
|
|
194
210
|
if (rules.length === 0) {
|
|
195
|
-
|
|
211
|
+
const label = crossMeta[args.ruleType]?.label || args.ruleType;
|
|
212
|
+
return { content: [{ type: 'text', text: `暂无${label}规则(项目: ${args.targetProject})` }] };
|
|
196
213
|
}
|
|
197
214
|
return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${rules.join('\n')}` }] };
|
|
198
215
|
}
|
|
199
|
-
// 获取全部规则
|
|
216
|
+
// 获取全部规则 (从 meta 获取所有类型)
|
|
217
|
+
const types = Object.keys(crossMeta).length > 0 ? Object.keys(crossMeta) : Object.keys(RULE_TYPE_LABELS);
|
|
200
218
|
const allRules = [];
|
|
201
|
-
for (const type of
|
|
219
|
+
for (const type of types) {
|
|
202
220
|
const rules = await storage.crossGetRules(args.targetProject, type);
|
|
203
221
|
if (rules.length > 0) {
|
|
204
|
-
|
|
222
|
+
const label = crossMeta[type]?.label || RULE_TYPE_LABELS[type] || type;
|
|
223
|
+
allRules.push(`## ${label}\n${rules.join('\n')}`);
|
|
205
224
|
}
|
|
206
225
|
}
|
|
207
226
|
if (allRules.length === 0) {
|
|
@@ -210,17 +229,18 @@ export function registerTools(server, projectId, _user) {
|
|
|
210
229
|
return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${allRules.join('\n\n')}` }] };
|
|
211
230
|
}
|
|
212
231
|
// 当前项目
|
|
213
|
-
const rules = await getRules(projectId, args.ruleType);
|
|
232
|
+
const rules = await getRules(projectId, args.ruleType || undefined);
|
|
214
233
|
if (!rules || rules.trim() === '') {
|
|
215
|
-
const
|
|
234
|
+
const meta = await storage.getRulesMeta();
|
|
235
|
+
const typeName = args.ruleType ? (meta[args.ruleType]?.label || args.ruleType) : '项目';
|
|
216
236
|
return { content: [{ type: 'text', text: `暂无${typeName}规则` }] };
|
|
217
237
|
}
|
|
218
238
|
return { content: [{ type: 'text', text: rules }] };
|
|
219
239
|
});
|
|
220
240
|
// 3.7 保存项目规则
|
|
221
241
|
server.tool('kg_save_rules', '保存单个类型的项目规则(独立文件存储)', {
|
|
222
|
-
ruleType: z.
|
|
223
|
-
.describe('
|
|
242
|
+
ruleType: z.string()
|
|
243
|
+
.describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)'),
|
|
224
244
|
rules: z.array(z.string()).describe('规则数组')
|
|
225
245
|
}, async (args) => {
|
|
226
246
|
const decoded = decodeObjectStrings(args);
|
|
@@ -228,7 +248,87 @@ export function registerTools(server, projectId, _user) {
|
|
|
228
248
|
if (!success) {
|
|
229
249
|
return wrap('❌ 保存失败');
|
|
230
250
|
}
|
|
231
|
-
|
|
251
|
+
const meta = await storage.getRulesMeta();
|
|
252
|
+
const label = meta[decoded.ruleType]?.label || decoded.ruleType;
|
|
253
|
+
return wrap(`✅ ${label}已保存 (${decoded.rules.length} 条)`);
|
|
254
|
+
});
|
|
255
|
+
// 3.8 获取规则触发配置
|
|
256
|
+
server.tool('kg_get_rules_meta', '获取规则触发配置(所有类型的标签、关键词、触发数)。用于查看/编辑 hooks 触发条件', {}, async () => {
|
|
257
|
+
try {
|
|
258
|
+
const meta = await storage.getRulesMeta();
|
|
259
|
+
if (Object.keys(meta).length === 0) {
|
|
260
|
+
return wrap('暂无规则配置');
|
|
261
|
+
}
|
|
262
|
+
const lines = Object.entries(meta).map(([type, m]) => {
|
|
263
|
+
const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
|
|
264
|
+
const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
|
|
265
|
+
return `- **${m.label}** (${type}): ${trigger}`;
|
|
266
|
+
});
|
|
267
|
+
return wrap(`规则触发配置:\n\n${lines.join('\n')}`);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return wrap(`❌ ${String(e)}`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// 3.9 保存规则触发配置
|
|
274
|
+
server.tool('kg_save_rules_meta', '保存规则触发配置(标签、关键词、触发数)。支持新增自定义规则类型', {
|
|
275
|
+
meta: z.record(z.string(), z.object({
|
|
276
|
+
label: z.string().describe('规则显示名称'),
|
|
277
|
+
keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
|
|
278
|
+
min_hits: z.number().default(1).describe('最低触发关键词数'),
|
|
279
|
+
always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
|
|
280
|
+
})).describe('规则类型 → 触发配置的映射')
|
|
281
|
+
}, async (args) => {
|
|
282
|
+
try {
|
|
283
|
+
const decoded = decodeObjectStrings(args.meta);
|
|
284
|
+
const success = await storage.saveRulesMeta(decoded);
|
|
285
|
+
if (!success) {
|
|
286
|
+
return wrap('❌ 保存失败');
|
|
287
|
+
}
|
|
288
|
+
return wrap(`✅ 规则触发配置已保存 (${Object.keys(decoded).length} 个类型)`);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
return wrap(`❌ ${String(e)}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// 3.10 获取全局默认规则触发配置
|
|
295
|
+
server.tool('kg_get_global_rules_meta', '获取全局默认规则触发配置(新项目创建时继承此配置)', {}, async () => {
|
|
296
|
+
try {
|
|
297
|
+
const meta = await storage.getGlobalRulesMeta();
|
|
298
|
+
if (Object.keys(meta).length === 0) {
|
|
299
|
+
return wrap('暂无全局默认规则配置');
|
|
300
|
+
}
|
|
301
|
+
const lines = Object.entries(meta).map(([type, m]) => {
|
|
302
|
+
const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
|
|
303
|
+
const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
|
|
304
|
+
return `- **${m.label}** (${type}): ${trigger}`;
|
|
305
|
+
});
|
|
306
|
+
return wrap(`全局默认规则触发配置:\n\n${lines.join('\n')}`);
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
return wrap(`❌ ${String(e)}`);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
// 3.11 保存全局默认规则触发配置
|
|
313
|
+
server.tool('kg_save_global_rules_meta', '保存全局默认规则触发配置(新项目创建时继承此配置)。支持新增自定义规则类型', {
|
|
314
|
+
meta: z.record(z.string(), z.object({
|
|
315
|
+
label: z.string().describe('规则显示名称'),
|
|
316
|
+
keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
|
|
317
|
+
min_hits: z.number().default(1).describe('最低触发关键词数'),
|
|
318
|
+
always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
|
|
319
|
+
})).describe('规则类型 → 触发配置的映射')
|
|
320
|
+
}, async (args) => {
|
|
321
|
+
try {
|
|
322
|
+
const decoded = decodeObjectStrings(args.meta);
|
|
323
|
+
const success = await storage.saveGlobalRulesMeta(decoded);
|
|
324
|
+
if (!success) {
|
|
325
|
+
return wrap('❌ 保存失败');
|
|
326
|
+
}
|
|
327
|
+
return wrap(`✅ 全局默认规则触发配置已保存 (${Object.keys(decoded).length} 个类型)`);
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
return wrap(`❌ ${String(e)}`);
|
|
331
|
+
}
|
|
232
332
|
});
|
|
233
333
|
// 5. 读取单个文档详情
|
|
234
334
|
server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问', {
|
|
@@ -393,4 +493,85 @@ export function registerTools(server, projectId, _user) {
|
|
|
393
493
|
}
|
|
394
494
|
return wrap(`✅ 任务已归档: ${task.title}`);
|
|
395
495
|
});
|
|
496
|
+
// ===================== 项目文件访问 =====================
|
|
497
|
+
// 上传本地目录到中心服务器
|
|
498
|
+
server.tool('project_upload', '上传本地项目目录到中心服务器(打包zip上传,自动排除node_modules/.git等)。上传后其他客户端可通过跨项目访问下载', {
|
|
499
|
+
localDir: z.string().describe('本地目录的绝对路径(如"/home/user/project/src")'),
|
|
500
|
+
remoteDir: z.string().optional().describe('上传到服务器的目标子目录(如"src"),不填则上传到根目录'),
|
|
501
|
+
}, async (args) => {
|
|
502
|
+
try {
|
|
503
|
+
const result = await storage.uploadFiles(args.localDir, args.remoteDir);
|
|
504
|
+
return wrap(`✅ 上传成功\n\n- 文件数量: ${result.fileCount}\n- 目标目录: ${args.remoteDir || '/'}\n\n其他客户端可通过 project_read_file / project_download_dir 访问`);
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
return wrap(`❌ ${String(e)}`);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
// 清空项目文件存储区
|
|
511
|
+
server.tool('project_clear_files', '清空当前项目在中心服务器上的文件存储区', {}, async () => {
|
|
512
|
+
try {
|
|
513
|
+
await storage.clearFiles();
|
|
514
|
+
return wrap('✅ 文件存储已清空');
|
|
515
|
+
}
|
|
516
|
+
catch (e) {
|
|
517
|
+
return wrap(`❌ ${String(e)}`);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// 列出项目文件
|
|
521
|
+
server.tool('project_list_files', '列出项目源码目录下的文件。需要先上传文件或项目已关联源码目录', {
|
|
522
|
+
dir: z.string().optional().describe('子目录路径(如"src/components"),不填则列出根目录'),
|
|
523
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
524
|
+
}, async (args) => {
|
|
525
|
+
try {
|
|
526
|
+
const files = args.targetProject
|
|
527
|
+
? await storage.crossListFiles(args.targetProject, args.dir)
|
|
528
|
+
: await storage.listFiles(args.dir);
|
|
529
|
+
if (files.length === 0) {
|
|
530
|
+
return wrap('目录为空');
|
|
531
|
+
}
|
|
532
|
+
const lines = files.map(f => {
|
|
533
|
+
const icon = f.isDir ? '📁' : '📄';
|
|
534
|
+
const size = f.isDir ? '' : ` (${formatFileSize(f.size)})`;
|
|
535
|
+
return `${icon} ${f.name}${size}`;
|
|
536
|
+
});
|
|
537
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
538
|
+
const dirLabel = args.dir || '/';
|
|
539
|
+
return wrap(`${prefix}📂 ${dirLabel} (${files.length} 项)\n\n${lines.join('\n')}`);
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
return wrap(`❌ ${String(e)}`);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
// 读取项目文件
|
|
546
|
+
server.tool('project_read_file', '读取项目源码文件内容(文本,限制1MB)。需要先上传文件或项目已关联源码目录', {
|
|
547
|
+
path: z.string().describe('文件路径(如"src/main.ts")'),
|
|
548
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
549
|
+
}, async (args) => {
|
|
550
|
+
try {
|
|
551
|
+
const content = args.targetProject
|
|
552
|
+
? await storage.crossReadFile(args.targetProject, args.path)
|
|
553
|
+
: await storage.readFile(args.path);
|
|
554
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
555
|
+
return wrap(`${prefix}📄 ${args.path}\n\n\`\`\`\n${content}\n\`\`\``);
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
return wrap(`❌ ${String(e)}`);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
// 下载项目目录 (zip → 自动解压)
|
|
562
|
+
server.tool('project_download_dir', '下载项目源码目录(自动打包zip并解压到本地临时目录)。需要先上传文件或项目已关联源码目录', {
|
|
563
|
+
dir: z.string().describe('目录路径(如"src")'),
|
|
564
|
+
targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
|
|
565
|
+
}, async (args) => {
|
|
566
|
+
try {
|
|
567
|
+
const result = args.targetProject
|
|
568
|
+
? await storage.crossDownloadDir(args.targetProject, args.dir)
|
|
569
|
+
: await storage.downloadDir(args.dir);
|
|
570
|
+
const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
|
|
571
|
+
return wrap(`${prefix}✅ 目录已下载并解压\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}\n\n可直接读取该路径下的文件`);
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
return wrap(`❌ ${String(e)}`);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
396
577
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Server 工具函数
|
|
3
3
|
*/
|
|
4
|
-
export type RuleType =
|
|
5
|
-
export declare const RULE_TYPE_LABELS: Record<
|
|
4
|
+
export type RuleType = string;
|
|
5
|
+
export declare const RULE_TYPE_LABELS: Record<string, string>;
|
|
6
6
|
/**
|
|
7
|
-
* 获取指定类型的规则 (
|
|
7
|
+
* 获取指定类型的规则 (动态: 从 meta 获取标签)
|
|
8
8
|
*/
|
|
9
|
-
export declare function getRules(projectId: string, ruleType?:
|
|
9
|
+
export declare function getRules(projectId: string, ruleType?: string): Promise<string>;
|
|
10
10
|
/**
|
|
11
11
|
* 解码 Unicode 转义序列
|
|
12
12
|
* 将 \uXXXX 格式的转义序列转换为实际字符
|
package/dist/utils.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* MCP Server 工具函数
|
|
3
3
|
*/
|
|
4
4
|
import * as storage from './storage/httpClient.js';
|
|
5
|
-
//
|
|
6
|
-
|
|
5
|
+
// 默认规则类型标签 (fallback)
|
|
6
|
+
const DEFAULT_LABELS = {
|
|
7
7
|
userStyles: '用户沟通规则',
|
|
8
8
|
codeStyle: '编码风格规则',
|
|
9
9
|
reviewRules: '代码审查规则',
|
|
10
10
|
testRules: '错误分析规则',
|
|
11
11
|
unitTests: '代码测试规则',
|
|
12
12
|
};
|
|
13
|
+
// 兼容导出
|
|
14
|
+
export const RULE_TYPE_LABELS = DEFAULT_LABELS;
|
|
13
15
|
/**
|
|
14
16
|
* 将字符串数组格式化为 Markdown 列表
|
|
15
17
|
*/
|
|
@@ -19,22 +21,25 @@ function formatRulesList(rules) {
|
|
|
19
21
|
return rules.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
|
-
* 获取指定类型的规则 (
|
|
24
|
+
* 获取指定类型的规则 (动态: 从 meta 获取标签)
|
|
23
25
|
*/
|
|
24
26
|
export async function getRules(projectId, ruleType) {
|
|
25
|
-
|
|
27
|
+
const meta = await storage.getRulesMeta();
|
|
26
28
|
if (ruleType) {
|
|
27
29
|
const rules = await storage.getRules(projectId, ruleType);
|
|
28
30
|
if (!rules || rules.length === 0)
|
|
29
31
|
return '';
|
|
30
|
-
|
|
32
|
+
const label = meta[ruleType]?.label || DEFAULT_LABELS[ruleType] || ruleType;
|
|
33
|
+
return `[${label}]\n${formatRulesList(rules)}`;
|
|
31
34
|
}
|
|
32
|
-
// 返回所有规则
|
|
35
|
+
// 返回所有规则 (从 meta 获取所有类型)
|
|
36
|
+
const types = Object.keys(meta).length > 0 ? Object.keys(meta) : Object.keys(DEFAULT_LABELS);
|
|
33
37
|
const allRules = [];
|
|
34
|
-
for (const type of
|
|
38
|
+
for (const type of types) {
|
|
35
39
|
const rules = await storage.getRules(projectId, type);
|
|
36
40
|
if (rules && rules.length > 0) {
|
|
37
|
-
|
|
41
|
+
const label = meta[type]?.label || DEFAULT_LABELS[type] || type;
|
|
42
|
+
allRules.push(`[${label}]\n${formatRulesList(rules)}`);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
45
|
return allRules.join('\n\n---\n\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppdocs/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
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
|
}
|
|
Binary file
|
package/templates/hooks/hook.py
CHANGED
|
@@ -1,71 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
Claude Code Hook -
|
|
4
|
+
Claude Code Hook - 动态规则触发
|
|
5
|
+
从服务器获取触发配置 (rules-meta) → 关键词匹配 → 叠加获取规则
|
|
6
|
+
兼容 Python 2.7+ / 3.x,支持 Windows / macOS / Linux
|
|
5
7
|
"""
|
|
6
8
|
|
|
9
|
+
from __future__ import print_function, unicode_literals
|
|
10
|
+
|
|
7
11
|
import io
|
|
8
12
|
import json
|
|
9
13
|
import os
|
|
10
14
|
import sys
|
|
11
|
-
import urllib.request
|
|
12
15
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# HTTP 库兼容
|
|
17
|
+
try:
|
|
18
|
+
from urllib.request import Request, urlopen
|
|
19
|
+
except ImportError:
|
|
20
|
+
from urllib2 import Request, urlopen
|
|
21
|
+
|
|
22
|
+
# Windows 编码修复 (仅 Windows + Python 3 + 有 buffer 属性时)
|
|
23
|
+
if sys.platform == "win32" and hasattr(sys.stdin, "buffer"):
|
|
24
|
+
try:
|
|
25
|
+
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
|
|
26
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
27
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
17
30
|
|
|
18
31
|
|
|
19
32
|
# ╔══════════════════════════════════════════════════════════════╗
|
|
20
|
-
# ║
|
|
33
|
+
# ║ 绕过词 ║
|
|
21
34
|
# ╚══════════════════════════════════════════════════════════════╝
|
|
22
35
|
|
|
23
|
-
# 关键词规则 (触发关键词 → 请求对应规则类型)
|
|
24
|
-
RULES = [
|
|
25
|
-
{
|
|
26
|
-
"keywords": ["bug","修复","错误","报错","失败","异常","严重","存在","还是","有"],
|
|
27
|
-
"min_hits": 2,
|
|
28
|
-
"rule_type": "testRules", # 错误分析规则
|
|
29
|
-
"label": "错误分析规则",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"keywords": ["审查", "审核", "review", "检查"],
|
|
33
|
-
"min_hits": 1,
|
|
34
|
-
"rule_type": "reviewRules", # 审查规则
|
|
35
|
-
"label": "代码审查规则",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"keywords": ["编码", "风格", "格式", "命名", "编写", "开始", "代码"],
|
|
39
|
-
"min_hits": 2,
|
|
40
|
-
"rule_type": "codeStyle", # 编码风格
|
|
41
|
-
"label": "编码风格规则",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"keywords": ["开始", "进行", "准备", "测试", "单元", "用例", "test", "覆盖率"],
|
|
45
|
-
"min_hits": 2,
|
|
46
|
-
"rule_type": "unitTests", # 代码测试
|
|
47
|
-
"label": "代码测试规则",
|
|
48
|
-
},
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
# 绕过词列表
|
|
52
36
|
BYPASS = [
|
|
53
|
-
"补充",
|
|
54
|
-
"
|
|
55
|
-
"确认",
|
|
56
|
-
"继续",
|
|
57
|
-
"好的",
|
|
58
|
-
"可以",
|
|
59
|
-
"ok",
|
|
60
|
-
"yes",
|
|
61
|
-
"hi",
|
|
62
|
-
"hello",
|
|
63
|
-
"你好",
|
|
37
|
+
"补充", "确定", "确认", "继续", "好的", "可以",
|
|
38
|
+
"ok", "yes", "hi", "hello", "你好",
|
|
64
39
|
]
|
|
65
40
|
|
|
66
41
|
|
|
67
42
|
# ╔══════════════════════════════════════════════════════════════╗
|
|
68
|
-
# ║ API
|
|
43
|
+
# ║ API 请求 ║
|
|
69
44
|
# ╚══════════════════════════════════════════════════════════════╝
|
|
70
45
|
|
|
71
46
|
|
|
@@ -75,25 +50,42 @@ def load_ppdocs_config():
|
|
|
75
50
|
if not os.path.exists(config_path):
|
|
76
51
|
return None
|
|
77
52
|
try:
|
|
78
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
53
|
+
with io.open(config_path, "r", encoding="utf-8") as f:
|
|
79
54
|
return json.load(f)
|
|
80
|
-
except:
|
|
55
|
+
except Exception:
|
|
81
56
|
return None
|
|
82
57
|
|
|
83
58
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
url =
|
|
59
|
+
def api_get(api_base, project_id, key, path):
|
|
60
|
+
"""通用 GET 请求"""
|
|
61
|
+
url = "%s/api/%s/%s%s" % (api_base, project_id, key, path)
|
|
62
|
+
resp = None
|
|
87
63
|
try:
|
|
88
|
-
req =
|
|
64
|
+
req = Request(url)
|
|
89
65
|
req.add_header("Content-Type", "application/json")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
except:
|
|
66
|
+
resp = urlopen(req, timeout=3)
|
|
67
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
68
|
+
if data.get("success") and data.get("data") is not None:
|
|
69
|
+
return data["data"]
|
|
70
|
+
except Exception:
|
|
95
71
|
pass
|
|
96
|
-
|
|
72
|
+
finally:
|
|
73
|
+
if resp is not None:
|
|
74
|
+
try:
|
|
75
|
+
resp.close()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def fetch_rules_meta(api_base, project_id, key):
|
|
82
|
+
"""获取规则触发配置"""
|
|
83
|
+
return api_get(api_base, project_id, key, "/rules-meta") or {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def fetch_rules(api_base, project_id, key, rule_type):
|
|
87
|
+
"""获取指定类型的规则内容"""
|
|
88
|
+
return api_get(api_base, project_id, key, "/rules/" + rule_type) or []
|
|
97
89
|
|
|
98
90
|
|
|
99
91
|
# ╔══════════════════════════════════════════════════════════════╗
|
|
@@ -101,27 +93,32 @@ def fetch_rules(api_base: str, project_id: str, key: str, rule_type: str) -> lis
|
|
|
101
93
|
# ╚══════════════════════════════════════════════════════════════╝
|
|
102
94
|
|
|
103
95
|
|
|
104
|
-
def count_hits(text
|
|
96
|
+
def count_hits(text, keywords):
|
|
105
97
|
"""计算关键词命中数量"""
|
|
106
98
|
return sum(1 for kw in keywords if kw.lower() in text)
|
|
107
99
|
|
|
108
100
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
def match_all(text, meta):
|
|
102
|
+
"""匹配所有触发的规则类型,返回 [(rule_type, label), ...]"""
|
|
103
|
+
matched = []
|
|
104
|
+
for rule_type, config in meta.items():
|
|
105
|
+
if config.get("always"):
|
|
106
|
+
matched.append((rule_type, config.get("label", rule_type)))
|
|
107
|
+
continue
|
|
108
|
+
keywords = config.get("keywords", [])
|
|
109
|
+
min_hits = config.get("min_hits", 1)
|
|
110
|
+
if keywords and count_hits(text, keywords) >= min_hits:
|
|
111
|
+
matched.append((rule_type, config.get("label", rule_type)))
|
|
112
|
+
return matched
|
|
116
113
|
|
|
117
114
|
|
|
118
|
-
def format_rules(items
|
|
115
|
+
def format_rules(items, label):
|
|
119
116
|
"""格式化规则输出"""
|
|
120
117
|
if not items:
|
|
121
118
|
return ""
|
|
122
|
-
lines = [
|
|
119
|
+
lines = ["# %s\n" % label]
|
|
123
120
|
for item in items:
|
|
124
|
-
lines.append(
|
|
121
|
+
lines.append("- %s" % item)
|
|
125
122
|
return "\n".join(lines)
|
|
126
123
|
|
|
127
124
|
|
|
@@ -132,8 +129,9 @@ def format_rules(items: list, label: str) -> str:
|
|
|
132
129
|
|
|
133
130
|
def main():
|
|
134
131
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
raw = sys.stdin.read()
|
|
133
|
+
data = json.loads(raw)
|
|
134
|
+
except Exception:
|
|
137
135
|
return
|
|
138
136
|
|
|
139
137
|
if data.get("hook_event_name") != "UserPromptSubmit":
|
|
@@ -163,19 +161,22 @@ def main():
|
|
|
163
161
|
if not project_id or not key:
|
|
164
162
|
return
|
|
165
163
|
|
|
166
|
-
|
|
164
|
+
# 从服务器获取触发配置
|
|
165
|
+
meta = fetch_rules_meta(api_base, project_id, key)
|
|
166
|
+
if not meta:
|
|
167
|
+
return
|
|
167
168
|
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
if
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if
|
|
178
|
-
output_parts.append(format_rules(
|
|
169
|
+
# 匹配所有触发的规则
|
|
170
|
+
matched = match_all(user_input_lower, meta)
|
|
171
|
+
if not matched:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# 叠加获取所有命中的规则内容
|
|
175
|
+
output_parts = []
|
|
176
|
+
for rule_type, label in matched:
|
|
177
|
+
rules = fetch_rules(api_base, project_id, key, rule_type)
|
|
178
|
+
if rules:
|
|
179
|
+
output_parts.append(format_rules(rules, label))
|
|
179
180
|
|
|
180
181
|
# 输出
|
|
181
182
|
if output_parts:
|