@ppdocs/mcp 3.0.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 +27 -1
- package/dist/storage/httpClient.js +87 -1
- package/dist/storage/types.d.ts +6 -0
- package/dist/tools/index.js +111 -11
- package/dist/utils.d.ts +4 -4
- package/dist/utils.js +13 -8
- package/package.json +1 -1
- 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, FileInfo } 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,6 +43,14 @@ 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;
|
|
@@ -90,6 +104,11 @@ export declare function getDocsByStatus(_projectId: string, statusList: string[]
|
|
|
90
104
|
export declare function getTree(_projectId: string): Promise<TreeNode[]>;
|
|
91
105
|
export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
|
|
92
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>;
|
|
93
112
|
export declare function listTasks(_projectId: string, status?: 'active' | 'archived'): Promise<TaskSummary[]>;
|
|
94
113
|
export declare function getTask(_projectId: string, taskId: string): Promise<Task | null>;
|
|
95
114
|
export declare function createTask(_projectId: string, task: {
|
|
@@ -99,6 +118,13 @@ export declare function createTask(_projectId: string, task: {
|
|
|
99
118
|
}, creator: string): Promise<Task>;
|
|
100
119
|
export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
|
|
101
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
|
+
}>;
|
|
102
128
|
export declare function crossListProjects(): Promise<{
|
|
103
129
|
id: string;
|
|
104
130
|
name: string;
|
|
@@ -91,9 +91,12 @@ function buildTree(docs) {
|
|
|
91
91
|
// API 客户端类
|
|
92
92
|
export class PpdocsApiClient {
|
|
93
93
|
baseUrl; // http://localhost:20001/api/projectId/password
|
|
94
|
+
serverUrl; // http://localhost:20001
|
|
94
95
|
constructor(apiUrl) {
|
|
95
|
-
// 移除末尾斜杠
|
|
96
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;
|
|
97
100
|
}
|
|
98
101
|
// ============ HTTP 请求工具 ============
|
|
99
102
|
async request(path, options = {}) {
|
|
@@ -227,6 +230,54 @@ export class PpdocsApiClient {
|
|
|
227
230
|
return false;
|
|
228
231
|
}
|
|
229
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
|
+
}
|
|
230
281
|
// ============ 任务管理 ============
|
|
231
282
|
async listTasks(status) {
|
|
232
283
|
const query = status ? `?status=${status}` : '';
|
|
@@ -277,6 +328,22 @@ export class PpdocsApiClient {
|
|
|
277
328
|
return null;
|
|
278
329
|
}
|
|
279
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
|
+
}
|
|
280
347
|
// ============ 跨项目只读访问 ============
|
|
281
348
|
/** 列出所有可访问的项目 */
|
|
282
349
|
async crossListProjects() {
|
|
@@ -441,6 +508,21 @@ export async function getRules(_projectId, ruleType) {
|
|
|
441
508
|
export async function saveRules(_projectId, ruleType, rules) {
|
|
442
509
|
return getClient().saveRulesApi(ruleType, rules);
|
|
443
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
|
+
}
|
|
444
526
|
// ============ 任务管理 ============
|
|
445
527
|
export async function listTasks(_projectId, status) {
|
|
446
528
|
return getClient().listTasks(status);
|
|
@@ -457,6 +539,10 @@ export async function addTaskLog(_projectId, taskId, logType, content) {
|
|
|
457
539
|
export async function completeTask(_projectId, taskId, experience) {
|
|
458
540
|
return getClient().completeTask(taskId, experience);
|
|
459
541
|
}
|
|
542
|
+
// ============ 项目管理 ============
|
|
543
|
+
export async function createProject(id, name, description, projectPath) {
|
|
544
|
+
return getClient().createProject(id, name, description, projectPath);
|
|
545
|
+
}
|
|
460
546
|
// ============ 跨项目只读访问 ============
|
|
461
547
|
export async function crossListProjects() {
|
|
462
548
|
return getClient().crossListProjects();
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface Project {
|
|
|
47
47
|
createdAt?: string;
|
|
48
48
|
projectPath?: string;
|
|
49
49
|
}
|
|
50
|
+
export interface RuleMeta {
|
|
51
|
+
label: string;
|
|
52
|
+
keywords: string[];
|
|
53
|
+
min_hits: number;
|
|
54
|
+
always?: boolean;
|
|
55
|
+
}
|
|
50
56
|
export interface FileInfo {
|
|
51
57
|
name: string;
|
|
52
58
|
path: string;
|
package/dist/tools/index.js
CHANGED
|
@@ -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', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问', {
|
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
|
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:
|