@ppdocs/mcp 3.2.37 → 3.2.38
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/README.md +1 -1
- package/dist/cli.js +2 -1
- package/dist/storage/httpClient.d.ts +24 -10
- package/dist/storage/httpClient.js +50 -63
- package/dist/storage/types.d.ts +42 -0
- package/dist/tools/analyzer.d.ts +1 -1
- package/dist/tools/analyzer.js +8 -7
- package/dist/tools/discussion.js +3 -7
- package/dist/tools/flowchart.js +4 -1
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js +7 -5
- package/dist/tools/refs.d.ts +2 -0
- package/dist/tools/refs.js +123 -0
- package/dist/tools/tasks.js +2 -2
- package/dist/tools/workflow.d.ts +3 -0
- package/dist/tools/workflow.js +80 -0
- package/dist/utils.d.ts +0 -6
- package/dist/utils.js +0 -44
- package/package.json +1 -1
- package/templates/AGENT.md +3 -2
- package/templates/README.md +2 -2
- package/templates/commands/pp/diagnose.md +3 -3
- package/templates/commands/pp/review.md +9 -10
- package/templates/cursorrules.md +3 -2
- package/templates/hooks/SystemPrompt.md +6 -6
- package/templates/hooks/hook.py +52 -35
- package/templates/kiro-rules/ppdocs.md +3 -2
- package/dist/tools/rules.d.ts +0 -7
- package/dist/tools/rules.js +0 -120
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ npx @ppdocs/mcp init -p <projectId> -k <key> --codex
|
|
|
114
114
|
| `kg_init` | 初始化项目连接与上下文 |
|
|
115
115
|
| `kg_status` | 查看项目仪表盘与流程图概况 |
|
|
116
116
|
| `kg_flowchart` | 统一的知识图谱入口,负责查询、更新、关系分析 |
|
|
117
|
-
| `
|
|
117
|
+
| `kg_workflow` | Markdown 文档工作流管理 |
|
|
118
118
|
| `kg_task` | 任务管理 |
|
|
119
119
|
| `kg_files` | 项目文件读写/上传下载 |
|
|
120
120
|
| `kg_discuss` | 讨论区 |
|
package/dist/cli.js
CHANGED
|
@@ -392,11 +392,12 @@ function generateMcpPermissions() {
|
|
|
392
392
|
'kg_status',
|
|
393
393
|
// 知识管理
|
|
394
394
|
'kg_projects',
|
|
395
|
-
'
|
|
395
|
+
'kg_workflow',
|
|
396
396
|
// 工作流
|
|
397
397
|
'kg_task',
|
|
398
398
|
'kg_files',
|
|
399
399
|
'kg_discuss',
|
|
400
|
+
'kg_ref',
|
|
400
401
|
// 流程图
|
|
401
402
|
'kg_flowchart',
|
|
402
403
|
// 协作
|
|
@@ -4,19 +4,31 @@
|
|
|
4
4
|
*
|
|
5
5
|
* API URL 格式: http://localhost:20099/api/:projectId/:password/...
|
|
6
6
|
*/
|
|
7
|
-
import type { Task, TaskSummary, TaskLogType, TaskExperience, FileInfo,
|
|
7
|
+
import type { Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, WorkflowSummary, WorkflowDoc, WorkflowSelector, Reference } from './types.js';
|
|
8
8
|
export declare class PpdocsApiClient {
|
|
9
9
|
private baseUrl;
|
|
10
10
|
private serverUrl;
|
|
11
11
|
constructor(apiUrl: string);
|
|
12
12
|
private request;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
listWorkflows(scope?: 'all' | 'global' | 'project'): Promise<WorkflowSummary[]>;
|
|
14
|
+
getWorkflow(workflowId: string, scope?: 'all' | 'global' | 'project'): Promise<WorkflowDoc | null>;
|
|
15
|
+
getWorkflowBatch(selectors: WorkflowSelector[]): Promise<WorkflowDoc[]>;
|
|
16
|
+
saveWorkflow(workflowId: string, input: {
|
|
17
|
+
scope: 'global' | 'project';
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
system: string;
|
|
21
|
+
keywords: string[];
|
|
22
|
+
minHits: number;
|
|
23
|
+
always: boolean;
|
|
24
|
+
content: string;
|
|
25
|
+
}): Promise<WorkflowDoc>;
|
|
26
|
+
deleteWorkflow(workflowId: string, scope: 'global' | 'project'): Promise<boolean>;
|
|
27
|
+
listReferences(): Promise<Reference[]>;
|
|
28
|
+
getReference(refId: string): Promise<Reference | null>;
|
|
29
|
+
saveReference(reference: Reference): Promise<boolean>;
|
|
30
|
+
deleteReference(refId: string): Promise<boolean>;
|
|
31
|
+
readReferenceFile(path: string): Promise<string>;
|
|
20
32
|
listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
|
|
21
33
|
getTask(taskId: string, mode?: 'smart' | 'full'): Promise<Task | null>;
|
|
22
34
|
createTask(task: {
|
|
@@ -46,8 +58,6 @@ export declare class PpdocsApiClient {
|
|
|
46
58
|
description: string;
|
|
47
59
|
updatedAt: string;
|
|
48
60
|
}[]>;
|
|
49
|
-
/** 跨项目: 获取规则 */
|
|
50
|
-
crossGetRules(target: string, ruleType: string): Promise<string[]>;
|
|
51
61
|
/** 列出项目文件 */
|
|
52
62
|
listFiles(dir?: string): Promise<FileInfo[]>;
|
|
53
63
|
/** 读取项目文件 */
|
|
@@ -95,6 +105,10 @@ export declare class PpdocsApiClient {
|
|
|
95
105
|
discussionReply(id: string, sender: string, content: string, msgSummary?: string, newSummary?: string): Promise<boolean>;
|
|
96
106
|
/** 完成讨论 (公开路由, 标记为 completed) */
|
|
97
107
|
discussionComplete(id: string): Promise<boolean>;
|
|
108
|
+
/** 归档关闭讨论 (需项目认证) */
|
|
109
|
+
discussionClose(id: string, conclusion: string): Promise<{
|
|
110
|
+
archived_path?: string;
|
|
111
|
+
}>;
|
|
98
112
|
/** 删除讨论 (公开路由) */
|
|
99
113
|
discussionDelete(id: string): Promise<boolean>;
|
|
100
114
|
/** 列出公共文件 */
|
|
@@ -99,75 +99,64 @@ export class PpdocsApiClient {
|
|
|
99
99
|
clearTimeout(timeout);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
// ============
|
|
103
|
-
async
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
102
|
+
// ============ 工作流 API ============
|
|
103
|
+
async listWorkflows(scope) {
|
|
104
|
+
const query = scope ? `?scope=${scope}` : '';
|
|
105
|
+
return this.request(`/workflows${query}`);
|
|
110
106
|
}
|
|
111
|
-
async
|
|
107
|
+
async getWorkflow(workflowId, scope) {
|
|
112
108
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
body: JSON.stringify(rules)
|
|
116
|
-
});
|
|
117
|
-
return true;
|
|
109
|
+
const query = scope ? `?scope=${scope}` : '';
|
|
110
|
+
return await this.request(`/workflows/${encodeURIComponent(workflowId)}${query}`);
|
|
118
111
|
}
|
|
119
112
|
catch {
|
|
120
|
-
return
|
|
113
|
+
return null;
|
|
121
114
|
}
|
|
122
115
|
}
|
|
123
|
-
async
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return {};
|
|
129
|
-
}
|
|
116
|
+
async getWorkflowBatch(selectors) {
|
|
117
|
+
return this.request('/workflows/batch', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
body: JSON.stringify(selectors),
|
|
120
|
+
});
|
|
130
121
|
}
|
|
131
|
-
async
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
122
|
+
async saveWorkflow(workflowId, input) {
|
|
123
|
+
return this.request(`/workflows/${encodeURIComponent(workflowId)}`, {
|
|
124
|
+
method: 'PUT',
|
|
125
|
+
body: JSON.stringify(input),
|
|
126
|
+
});
|
|
142
127
|
}
|
|
143
|
-
async
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return {};
|
|
149
|
-
}
|
|
128
|
+
async deleteWorkflow(workflowId, scope) {
|
|
129
|
+
return this.request(`/workflows/${encodeURIComponent(workflowId)}?scope=${scope}`, {
|
|
130
|
+
method: 'DELETE',
|
|
131
|
+
});
|
|
150
132
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return {};
|
|
157
|
-
}
|
|
133
|
+
// ============ 外部参考 API ============
|
|
134
|
+
async listReferences() {
|
|
135
|
+
return this.request('/refs');
|
|
158
136
|
}
|
|
159
|
-
async
|
|
137
|
+
async getReference(refId) {
|
|
160
138
|
try {
|
|
161
|
-
await this.request(
|
|
162
|
-
method: 'PUT',
|
|
163
|
-
body: JSON.stringify(meta)
|
|
164
|
-
});
|
|
165
|
-
return true;
|
|
139
|
+
return await this.request(`/refs/${encodeURIComponent(refId)}`);
|
|
166
140
|
}
|
|
167
141
|
catch {
|
|
168
|
-
return
|
|
142
|
+
return null;
|
|
169
143
|
}
|
|
170
144
|
}
|
|
145
|
+
async saveReference(reference) {
|
|
146
|
+
await this.request(`/refs/${encodeURIComponent(reference.id)}`, {
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
body: JSON.stringify(reference),
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
async deleteReference(refId) {
|
|
153
|
+
return this.request(`/refs/${encodeURIComponent(refId)}`, {
|
|
154
|
+
method: 'DELETE',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async readReferenceFile(path) {
|
|
158
|
+
return this.request(`/refs/files/${cleanPath(path)}`);
|
|
159
|
+
}
|
|
171
160
|
// ============ 任务管理 ============
|
|
172
161
|
async listTasks(status) {
|
|
173
162
|
const query = status ? `?status=${status}` : '';
|
|
@@ -285,15 +274,6 @@ export class PpdocsApiClient {
|
|
|
285
274
|
async crossListProjects() {
|
|
286
275
|
return this.request('/cross/projects');
|
|
287
276
|
}
|
|
288
|
-
/** 跨项目: 获取规则 */
|
|
289
|
-
async crossGetRules(target, ruleType) {
|
|
290
|
-
try {
|
|
291
|
-
return await this.request(`/cross/${encodeURIComponent(target)}/rules/${ruleType}`);
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
277
|
// ============ 项目文件访问 ============
|
|
298
278
|
/** 列出项目文件 */
|
|
299
279
|
async listFiles(dir) {
|
|
@@ -466,6 +446,13 @@ export class PpdocsApiClient {
|
|
|
466
446
|
method: 'POST',
|
|
467
447
|
});
|
|
468
448
|
}
|
|
449
|
+
/** 归档关闭讨论 (需项目认证) */
|
|
450
|
+
async discussionClose(id, conclusion) {
|
|
451
|
+
return this.request(`/discussions/${encodeURIComponent(id)}/close`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
body: JSON.stringify({ conclusion }),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
469
456
|
/** 删除讨论 (公开路由) */
|
|
470
457
|
async discussionDelete(id) {
|
|
471
458
|
return this.publicRequest(`/api/discussions/${encodeURIComponent(id)}`, {
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -4,6 +4,48 @@ export interface RuleMeta {
|
|
|
4
4
|
min_hits: number;
|
|
5
5
|
always?: boolean;
|
|
6
6
|
}
|
|
7
|
+
export interface WorkflowSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
scope: 'global' | 'project';
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
system: string;
|
|
13
|
+
keywords: string[];
|
|
14
|
+
minHits: number;
|
|
15
|
+
always: boolean;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkflowDoc extends WorkflowSummary {
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WorkflowSelector {
|
|
22
|
+
id: string;
|
|
23
|
+
scope?: 'global' | 'project';
|
|
24
|
+
}
|
|
25
|
+
export interface ReferenceLink {
|
|
26
|
+
url: string;
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ReferenceFile {
|
|
30
|
+
name: string;
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AdoptedNode {
|
|
34
|
+
chartId: string;
|
|
35
|
+
nodeId: string;
|
|
36
|
+
nodeLabel: string;
|
|
37
|
+
}
|
|
38
|
+
export interface Reference {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
summary: string;
|
|
42
|
+
links: ReferenceLink[];
|
|
43
|
+
files: ReferenceFile[];
|
|
44
|
+
scripts: ReferenceFile[];
|
|
45
|
+
adoptedBy: AdoptedNode[];
|
|
46
|
+
createdAt: string;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
}
|
|
7
49
|
export interface FileInfo {
|
|
8
50
|
name: string;
|
|
9
51
|
path: string;
|
package/dist/tools/analyzer.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 代码分析引擎工具 (3个)
|
|
3
3
|
* code_scan, code_smart_context, code_full_path
|
|
4
4
|
*
|
|
5
|
-
* code_smart_context: 代码+KG
|
|
5
|
+
* code_smart_context: 代码+KG文档/工作流全关联 (含影响范围)
|
|
6
6
|
* code_full_path: 两点间代码链路+共享KG文档
|
|
7
7
|
*/
|
|
8
8
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
package/dist/tools/analyzer.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 代码分析引擎工具 (3个)
|
|
3
3
|
* code_scan, code_smart_context, code_full_path
|
|
4
4
|
*
|
|
5
|
-
* code_smart_context: 代码+KG
|
|
5
|
+
* code_smart_context: 代码+KG文档/工作流全关联 (含影响范围)
|
|
6
6
|
* code_full_path: 两点间代码链路+共享KG文档
|
|
7
7
|
*/
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -33,7 +33,7 @@ export function registerAnalyzerTools(server, ctx) {
|
|
|
33
33
|
].join('\n'));
|
|
34
34
|
}));
|
|
35
35
|
// ===== code_smart_context: 代码+文档全关联上下文 =====
|
|
36
|
-
server.tool('code_smart_context', '🔍 代码+文档全关联上下文 —
|
|
36
|
+
server.tool('code_smart_context', '🔍 代码+文档全关联上下文 — 输入一个函数名,一次调用返回:代码依赖、关联文档、匹配工作流、活跃任务、影响范围摘要。需先运行 code_scan', {
|
|
37
37
|
projectPath: z.string().optional().describe('项目源码的绝对路径(不传则自动解析)'),
|
|
38
38
|
symbolName: z.string().describe('要查询的符号名称(如"handleLogin", "AuthService")'),
|
|
39
39
|
}, async (args) => safeTool(async () => {
|
|
@@ -75,11 +75,12 @@ export function registerAnalyzerTools(server, ctx) {
|
|
|
75
75
|
}
|
|
76
76
|
lines.push('');
|
|
77
77
|
}
|
|
78
|
-
//
|
|
79
|
-
if (smartCtx.
|
|
80
|
-
lines.push(`###
|
|
81
|
-
for (const
|
|
82
|
-
|
|
78
|
+
// 匹配工作流
|
|
79
|
+
if (smartCtx.matchedWorkflows.length > 0) {
|
|
80
|
+
lines.push(`### 📘 匹配工作流 (${smartCtx.matchedWorkflows.length})`);
|
|
81
|
+
for (const workflow of smartCtx.matchedWorkflows) {
|
|
82
|
+
const desc = workflow.description ? ` — ${workflow.description}` : '';
|
|
83
|
+
lines.push(` [${workflow.scope}/${workflow.system || 'general'}] ${workflow.title}${desc}`);
|
|
83
84
|
}
|
|
84
85
|
lines.push('');
|
|
85
86
|
}
|
package/dist/tools/discussion.js
CHANGED
|
@@ -233,13 +233,9 @@ export function registerDiscussionTools(server, ctx) {
|
|
|
233
233
|
if (brief && !isInitiator(brief, me, myPid)) {
|
|
234
234
|
return wrap(`🔒 无权限: 仅发起人 (${brief.initiator}) 可归档讨论\n你的身份: ${me}`);
|
|
235
235
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
catch { /* 忽略回复失败 */ }
|
|
241
|
-
await client().discussionComplete(decoded.id);
|
|
242
|
-
return wrap(`✅ 讨论已结案 (ID: ${decoded.id})\n📋 ${decoded.conclusion}`);
|
|
236
|
+
const result = await client().discussionClose(decoded.id, decoded.conclusion);
|
|
237
|
+
const archivePath = result?.archived_path ? `\n📦 已归档: ${result.archived_path}` : '';
|
|
238
|
+
return wrap(`✅ 讨论已结案并归档 (ID: ${decoded.id})\n📋 ${decoded.conclusion}${archivePath}`);
|
|
243
239
|
}
|
|
244
240
|
case 'delete': {
|
|
245
241
|
if (!decoded.id)
|
package/dist/tools/flowchart.js
CHANGED
|
@@ -249,7 +249,10 @@ function findDirectedPath(chart, fromId, toId) {
|
|
|
249
249
|
}
|
|
250
250
|
export function registerFlowchartTools(server, _ctx) {
|
|
251
251
|
const client = () => getClient();
|
|
252
|
-
server.tool('kg_flowchart', '
|
|
252
|
+
server.tool('kg_flowchart', '🔀 逻辑流程图 — 项目知识图谱的核心。每个节点代表一个模块/函数/概念,节点可绑定文件、内嵌版本化文档。\n' +
|
|
253
|
+
'⚡ 开始任何任务前必须先查图谱:search 搜关键词 → get_node 看详情 → 有 subFlowchart 则递归下探。\n' +
|
|
254
|
+
'📝 完成修改后必须回写:bind 绑定文件 → update_node 更新描述和文档 → 新模块用 batch_add。\n' +
|
|
255
|
+
'actions: list|get|search|get_relations|find_path|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
|
|
253
256
|
action: z
|
|
254
257
|
.enum(['list', 'get', 'search', 'get_relations', 'find_path', 'get_node', 'update_node', 'delete_node', 'batch_add', 'bind', 'unbind', 'orphans', 'health', 'create_chart', 'delete_chart'])
|
|
255
258
|
.describe('action type'),
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
*
|
|
3
|
+
* 13 个工具, 7 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status (1个)
|
|
7
|
-
* 📚 知识: kg_projects,
|
|
8
|
-
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区)
|
|
7
|
+
* 📚 知识: kg_projects, kg_workflow (2个)
|
|
8
|
+
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
|
|
9
9
|
* 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
|
|
10
10
|
* 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
|
|
11
11
|
* 🏛️ 协作: kg_meeting (1个)
|
package/dist/tools/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP 工具注册入口
|
|
3
|
-
*
|
|
3
|
+
* 13 个工具, 7 个子模块
|
|
4
4
|
*
|
|
5
5
|
* 🔗 初始化: kg_init (1个)
|
|
6
6
|
* 📊 导航: kg_status (1个)
|
|
7
|
-
* 📚 知识: kg_projects,
|
|
8
|
-
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区)
|
|
7
|
+
* 📚 知识: kg_projects, kg_workflow (2个)
|
|
8
|
+
* 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
|
|
9
9
|
* 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
|
|
10
10
|
* 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
|
|
11
11
|
* 🏛️ 协作: kg_meeting (1个)
|
|
@@ -14,10 +14,11 @@ import { createContext } from './shared.js';
|
|
|
14
14
|
import { registerInitTool } from './init.js';
|
|
15
15
|
import { registerStatusTool } from './kg_status.js';
|
|
16
16
|
import { registerProjectTools } from './projects.js';
|
|
17
|
-
import {
|
|
17
|
+
import { registerWorkflowTools } from './workflow.js';
|
|
18
18
|
import { registerTaskTools } from './tasks.js';
|
|
19
19
|
import { registerFileTools } from './files.js';
|
|
20
20
|
import { registerDiscussionTools } from './discussion.js';
|
|
21
|
+
import { registerReferenceTools } from './refs.js';
|
|
21
22
|
import { registerAnalyzerTools } from './analyzer.js';
|
|
22
23
|
import { registerMeetingTools } from './meeting.js';
|
|
23
24
|
import { registerFlowchartTools } from './flowchart.js';
|
|
@@ -29,11 +30,12 @@ export function registerTools(server, projectId, user, onProjectChange) {
|
|
|
29
30
|
registerStatusTool(server, ctx);
|
|
30
31
|
// 📚 知识
|
|
31
32
|
registerProjectTools(server, ctx);
|
|
32
|
-
|
|
33
|
+
registerWorkflowTools(server, ctx);
|
|
33
34
|
// 📝 工作流
|
|
34
35
|
registerTaskTools(server, ctx);
|
|
35
36
|
registerFileTools(server);
|
|
36
37
|
registerDiscussionTools(server, ctx);
|
|
38
|
+
registerReferenceTools(server);
|
|
37
39
|
// 🔬 代码分析
|
|
38
40
|
registerAnalyzerTools(server, ctx);
|
|
39
41
|
// 🏛️ 多AI协作
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getClient } from '../storage/httpClient.js';
|
|
3
|
+
import { decodeObjectStrings } from '../utils.js';
|
|
4
|
+
import { safeTool, wrap } from './shared.js';
|
|
5
|
+
const linkSchema = z.object({
|
|
6
|
+
url: z.string(),
|
|
7
|
+
label: z.string(),
|
|
8
|
+
});
|
|
9
|
+
const fileSchema = z.object({
|
|
10
|
+
name: z.string(),
|
|
11
|
+
path: z.string(),
|
|
12
|
+
});
|
|
13
|
+
const adoptedNodeSchema = z.object({
|
|
14
|
+
chartId: z.string(),
|
|
15
|
+
nodeId: z.string(),
|
|
16
|
+
nodeLabel: z.string(),
|
|
17
|
+
});
|
|
18
|
+
export function registerReferenceTools(server) {
|
|
19
|
+
const client = () => getClient();
|
|
20
|
+
server.tool('kg_ref', '📎 外部参考 — 管理项目引用的第三方资料、链接、脚本和样例文件。\n' +
|
|
21
|
+
'涉及参考资料时主动调用:list 查看已有 → get 读详情 → read_file 读附件内容 → save 新增。\n' +
|
|
22
|
+
'actions: list|get|save|delete|read_file', {
|
|
23
|
+
action: z.enum(['list', 'get', 'save', 'delete', 'read_file']).describe('操作类型'),
|
|
24
|
+
id: z.string().optional().describe('参考 ID'),
|
|
25
|
+
title: z.string().optional().describe('参考标题 (save)'),
|
|
26
|
+
summary: z.string().optional().describe('Markdown 总结 (save)'),
|
|
27
|
+
links: z.array(linkSchema).optional().describe('参考链接 (save)'),
|
|
28
|
+
files: z.array(fileSchema).optional().describe('参考文件 (save)'),
|
|
29
|
+
scripts: z.array(fileSchema).optional().describe('解析脚本 (save)'),
|
|
30
|
+
adoptedBy: z.array(adoptedNodeSchema).optional().describe('已采用该参考的节点 (save)'),
|
|
31
|
+
path: z.string().optional().describe('参考文件路径 (read_file)'),
|
|
32
|
+
}, async (args) => safeTool(async () => {
|
|
33
|
+
const decoded = decodeObjectStrings(args);
|
|
34
|
+
switch (decoded.action) {
|
|
35
|
+
case 'list': {
|
|
36
|
+
const refs = await client().listReferences();
|
|
37
|
+
if (refs.length === 0)
|
|
38
|
+
return wrap('当前没有外部参考');
|
|
39
|
+
const lines = [
|
|
40
|
+
`📎 外部参考 (${refs.length})`,
|
|
41
|
+
'',
|
|
42
|
+
'| ID | 标题 | 链接 | 文件 | 已采用 |',
|
|
43
|
+
'|:---|:---|---:|---:|---:|',
|
|
44
|
+
];
|
|
45
|
+
for (const ref of refs) {
|
|
46
|
+
lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${ref.adoptedBy.length} |`);
|
|
47
|
+
}
|
|
48
|
+
return wrap(lines.join('\n'));
|
|
49
|
+
}
|
|
50
|
+
case 'get': {
|
|
51
|
+
if (!decoded.id)
|
|
52
|
+
return wrap('❌ get 需要 id');
|
|
53
|
+
const ref = await client().getReference(decoded.id);
|
|
54
|
+
if (!ref)
|
|
55
|
+
return wrap(`未找到外部参考: ${decoded.id}`);
|
|
56
|
+
const lines = [
|
|
57
|
+
`# ${ref.title}`,
|
|
58
|
+
'',
|
|
59
|
+
`- ID: ${ref.id}`,
|
|
60
|
+
`- Links: ${ref.links.length}`,
|
|
61
|
+
`- Files: ${ref.files.length}`,
|
|
62
|
+
`- Scripts: ${ref.scripts.length}`,
|
|
63
|
+
`- AdoptedBy: ${ref.adoptedBy.length}`,
|
|
64
|
+
'',
|
|
65
|
+
ref.summary || '(无摘要)',
|
|
66
|
+
];
|
|
67
|
+
if (ref.links.length > 0) {
|
|
68
|
+
lines.push('', '## Links');
|
|
69
|
+
for (const link of ref.links) {
|
|
70
|
+
lines.push(`- ${link.label}: ${link.url}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (ref.files.length > 0) {
|
|
74
|
+
lines.push('', '## Files');
|
|
75
|
+
for (const file of ref.files) {
|
|
76
|
+
lines.push(`- ${file.name}: ${file.path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (ref.scripts.length > 0) {
|
|
80
|
+
lines.push('', '## Scripts');
|
|
81
|
+
for (const script of ref.scripts) {
|
|
82
|
+
lines.push(`- ${script.name}: ${script.path}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return wrap(lines.join('\n'));
|
|
86
|
+
}
|
|
87
|
+
case 'save': {
|
|
88
|
+
if (!decoded.id)
|
|
89
|
+
return wrap('❌ save 需要 id');
|
|
90
|
+
if (!decoded.title)
|
|
91
|
+
return wrap('❌ save 需要 title');
|
|
92
|
+
const existing = await client().getReference(decoded.id);
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
await client().saveReference({
|
|
95
|
+
id: decoded.id,
|
|
96
|
+
title: decoded.title,
|
|
97
|
+
summary: decoded.summary || existing?.summary || '',
|
|
98
|
+
links: decoded.links || existing?.links || [],
|
|
99
|
+
files: decoded.files || existing?.files || [],
|
|
100
|
+
scripts: decoded.scripts || existing?.scripts || [],
|
|
101
|
+
adoptedBy: decoded.adoptedBy || existing?.adoptedBy || [],
|
|
102
|
+
createdAt: existing?.createdAt || now,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
});
|
|
105
|
+
return wrap(`✅ 外部参考已保存 (${decoded.id})`);
|
|
106
|
+
}
|
|
107
|
+
case 'delete': {
|
|
108
|
+
if (!decoded.id)
|
|
109
|
+
return wrap('❌ delete 需要 id');
|
|
110
|
+
const ok = await client().deleteReference(decoded.id);
|
|
111
|
+
return wrap(ok ? `✅ 外部参考已删除 (${decoded.id})` : `ℹ️ 未找到外部参考 (${decoded.id})`);
|
|
112
|
+
}
|
|
113
|
+
case 'read_file': {
|
|
114
|
+
if (!decoded.path)
|
|
115
|
+
return wrap('❌ read_file 需要 path');
|
|
116
|
+
const content = await client().readReferenceFile(decoded.path);
|
|
117
|
+
return wrap(content);
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return wrap(`❌ 未知 action: ${decoded.action}`);
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
}
|
package/dist/tools/tasks.js
CHANGED
|
@@ -184,14 +184,14 @@ export function registerTaskTools(server, ctx) {
|
|
|
184
184
|
return wrap('归档失败(任务不存在或已归档)');
|
|
185
185
|
return wrap(`✅ 任务已归档: ${task.title}`);
|
|
186
186
|
}
|
|
187
|
-
default:
|
|
188
|
-
return wrap(`❌ 未知 action: ${decoded.action}`);
|
|
189
187
|
case 'delete': {
|
|
190
188
|
if (!decoded.taskId)
|
|
191
189
|
return wrap('❌ delete 需要 taskId');
|
|
192
190
|
const ok = await client().deleteTask(decoded.taskId);
|
|
193
191
|
return wrap(ok ? `✅ 任务已删除 (ID: ${decoded.taskId})` : '❌ 删除失败(任务不存在)');
|
|
194
192
|
}
|
|
193
|
+
default:
|
|
194
|
+
return wrap(`❌ 未知 action: ${decoded.action}`);
|
|
195
195
|
}
|
|
196
196
|
}));
|
|
197
197
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getClient } from '../storage/httpClient.js';
|
|
3
|
+
import { decodeObjectStrings } from '../utils.js';
|
|
4
|
+
import { safeTool, wrap } from './shared.js';
|
|
5
|
+
export function registerWorkflowTools(server, _ctx) {
|
|
6
|
+
const client = () => getClient();
|
|
7
|
+
server.tool('kg_workflow', '📘 文档工作流 — AI 的标准操作手册。每个工作流是一份 Markdown 文档,定义了特定场景下 AI 应该遵循的步骤和规范。\n' +
|
|
8
|
+
'调用方式:空参数=列出所有工作流;传 id=获取工作流正文;save/delete 需显式 action。\n' +
|
|
9
|
+
'典型用法:拿到任务后先 kg_workflow() 看有无相关工作流,有则 kg_workflow(id:"xxx") 读取并遵循执行。', {
|
|
10
|
+
action: z.enum(['list', 'get', 'save', 'delete']).optional().describe('省略时自动推断:无参数=list,有id=get'),
|
|
11
|
+
id: z.string().optional().describe('工作流 ID'),
|
|
12
|
+
scope: z.enum(['all', 'global', 'project']).optional().describe('范围:all(默认)|global|project'),
|
|
13
|
+
title: z.string().optional().describe('标题 (save 必填)'),
|
|
14
|
+
content: z.string().optional().describe('Markdown 正文 (save 必填)'),
|
|
15
|
+
}, async (args) => safeTool(async () => {
|
|
16
|
+
const decoded = decodeObjectStrings(args);
|
|
17
|
+
const action = decoded.action ?? (decoded.id ? 'get' : 'list');
|
|
18
|
+
if (!decoded.action && (decoded.title !== undefined || decoded.content !== undefined)) {
|
|
19
|
+
return wrap('❌ save/delete 需要显式传 action');
|
|
20
|
+
}
|
|
21
|
+
switch (action) {
|
|
22
|
+
case 'list': {
|
|
23
|
+
const workflows = await client().listWorkflows(decoded.scope ?? 'all');
|
|
24
|
+
if (workflows.length === 0)
|
|
25
|
+
return wrap('当前没有可用文档工作流');
|
|
26
|
+
const lines = [
|
|
27
|
+
`📘 文档工作流列表 (${workflows.length})`,
|
|
28
|
+
'',
|
|
29
|
+
'| ID | 范围 | 标题 |',
|
|
30
|
+
'|:---|:---|:---|',
|
|
31
|
+
];
|
|
32
|
+
for (const w of workflows) {
|
|
33
|
+
lines.push(`| ${w.id} | ${w.scope} | ${w.title} |`);
|
|
34
|
+
}
|
|
35
|
+
return wrap(lines.join('\n'));
|
|
36
|
+
}
|
|
37
|
+
case 'get': {
|
|
38
|
+
if (!decoded.id)
|
|
39
|
+
return wrap('❌ get 需要 id');
|
|
40
|
+
const doc = await client().getWorkflow(decoded.id, decoded.scope ?? 'all');
|
|
41
|
+
if (!doc)
|
|
42
|
+
return wrap(`未找到工作流: ${decoded.id}`);
|
|
43
|
+
return wrap([
|
|
44
|
+
`# ${doc.title}`,
|
|
45
|
+
`> ID: ${doc.id} | 范围: ${doc.scope}`,
|
|
46
|
+
'',
|
|
47
|
+
doc.content,
|
|
48
|
+
].join('\n'));
|
|
49
|
+
}
|
|
50
|
+
case 'save': {
|
|
51
|
+
if (!decoded.id)
|
|
52
|
+
return wrap('❌ save 需要 id');
|
|
53
|
+
if (!decoded.title)
|
|
54
|
+
return wrap('❌ save 需要 title');
|
|
55
|
+
if (!decoded.content)
|
|
56
|
+
return wrap('❌ save 需要 content');
|
|
57
|
+
const doc = await client().saveWorkflow(decoded.id, {
|
|
58
|
+
scope: decoded.scope === 'global' ? 'global' : 'project',
|
|
59
|
+
title: decoded.title,
|
|
60
|
+
description: '',
|
|
61
|
+
system: '',
|
|
62
|
+
keywords: [],
|
|
63
|
+
minHits: 0,
|
|
64
|
+
always: false,
|
|
65
|
+
content: decoded.content,
|
|
66
|
+
});
|
|
67
|
+
return wrap(`✅ 已保存: ${doc.scope}/${doc.id} — ${doc.title}`);
|
|
68
|
+
}
|
|
69
|
+
case 'delete': {
|
|
70
|
+
if (!decoded.id)
|
|
71
|
+
return wrap('❌ delete 需要 id');
|
|
72
|
+
const scope = decoded.scope === 'global' ? 'global' : 'project';
|
|
73
|
+
const ok = await client().deleteWorkflow(decoded.id, scope);
|
|
74
|
+
return wrap(ok ? `✅ 已删除 ${scope}/${decoded.id}` : `未找到 ${scope}/${decoded.id}`);
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return wrap(`❌ 未知 action: ${String(decoded.action)}`);
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Server 工具函数
|
|
3
3
|
*/
|
|
4
|
-
export type RuleType = string;
|
|
5
|
-
export declare const RULE_TYPE_LABELS: Record<string, string>;
|
|
6
|
-
/**
|
|
7
|
-
* 获取指定类型的规则 (动态: 从 meta 获取标签)
|
|
8
|
-
*/
|
|
9
|
-
export declare function getRules(projectId: string, ruleType?: string): Promise<string>;
|
|
10
4
|
/**
|
|
11
5
|
* 解码 Unicode 转义序列
|
|
12
6
|
* 将 \uXXXX 格式的转义序列转换为实际字符
|
package/dist/utils.js
CHANGED
|
@@ -1,50 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Server 工具函数
|
|
3
3
|
*/
|
|
4
|
-
import { getClient } from './storage/httpClient.js';
|
|
5
|
-
// 默认规则类型标签 (fallback)
|
|
6
|
-
const DEFAULT_LABELS = {
|
|
7
|
-
userStyles: '用户沟通规则',
|
|
8
|
-
codeStyle: '编码风格规则',
|
|
9
|
-
reviewRules: '代码审查规则',
|
|
10
|
-
testRules: '错误分析规则',
|
|
11
|
-
unitTests: '代码测试规则',
|
|
12
|
-
};
|
|
13
|
-
// 兼容导出
|
|
14
|
-
export const RULE_TYPE_LABELS = DEFAULT_LABELS;
|
|
15
|
-
/**
|
|
16
|
-
* 将字符串数组格式化为 Markdown 列表
|
|
17
|
-
*/
|
|
18
|
-
function formatRulesList(rules) {
|
|
19
|
-
if (!rules || rules.length === 0)
|
|
20
|
-
return '';
|
|
21
|
-
return rules.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 获取指定类型的规则 (动态: 从 meta 获取标签)
|
|
25
|
-
*/
|
|
26
|
-
export async function getRules(projectId, ruleType) {
|
|
27
|
-
const client = getClient();
|
|
28
|
-
const meta = await client.getRulesMeta();
|
|
29
|
-
if (ruleType) {
|
|
30
|
-
const rules = await client.getRulesApi(ruleType);
|
|
31
|
-
if (!rules || rules.length === 0)
|
|
32
|
-
return '';
|
|
33
|
-
const label = meta[ruleType]?.label || DEFAULT_LABELS[ruleType] || ruleType;
|
|
34
|
-
return `[${label}]\n${formatRulesList(rules)}`;
|
|
35
|
-
}
|
|
36
|
-
// 返回所有规则 (从 meta 获取所有类型)
|
|
37
|
-
const types = Object.keys(meta).length > 0 ? Object.keys(meta) : Object.keys(DEFAULT_LABELS);
|
|
38
|
-
const allRules = [];
|
|
39
|
-
for (const type of types) {
|
|
40
|
-
const rules = await client.getRulesApi(type);
|
|
41
|
-
if (rules && rules.length > 0) {
|
|
42
|
-
const label = meta[type]?.label || DEFAULT_LABELS[type] || type;
|
|
43
|
-
allRules.push(`[${label}]\n${formatRulesList(rules)}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return allRules.join('\n\n---\n\n');
|
|
47
|
-
}
|
|
48
4
|
/**
|
|
49
5
|
* 解码 Unicode 转义序列
|
|
50
6
|
* 将 \uXXXX 格式的转义序列转换为实际字符
|
package/package.json
CHANGED
package/templates/AGENT.md
CHANGED
|
@@ -12,9 +12,10 @@ kg_discuss(action:"list") -> ???????
|
|
|
12
12
|
## 1. ????????
|
|
13
13
|
|
|
14
14
|
- ??: `kg_flowchart(action:"list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart")`
|
|
15
|
-
- ??: `
|
|
15
|
+
- ??: `kg_workflow()` / `kg_workflow(id:"...")` / `kg_workflow(action:"save|delete")`
|
|
16
16
|
- ??: `kg_task(action:"create|get|update|archive|delete")`
|
|
17
17
|
- ??: `kg_files(action:"list|read|upload|download|public_*")`
|
|
18
|
+
- ??: `kg_ref(action:"list|get|save|delete|read_file")`
|
|
18
19
|
- ??/??: `kg_discuss(...)`, `kg_meeting(...)`
|
|
19
20
|
- ????: `code_scan()`, `code_smart_context(symbolName)`, `code_full_path(symbolA, symbolB)`
|
|
20
21
|
- ????: ?????????????????????????????? fallback????????????
|
|
@@ -22,7 +23,7 @@ kg_discuss(action:"list") -> ???????
|
|
|
22
23
|
## 2. ?????
|
|
23
24
|
|
|
24
25
|
### Step 1: ????
|
|
25
|
-
1. ?? `kg_flowchart(get)` ????????? `
|
|
26
|
+
1. ?? `kg_flowchart(get)` ????????? `kg_workflow()` ?????
|
|
26
27
|
2. ???????? `kg_flowchart(get_node, expand:2, includeDoc:true, includeFiles:true)`?
|
|
27
28
|
3. ????? `subFlowchart`???????????????
|
|
28
29
|
4. ??????????? `code_scan()`??? `code_smart_context` / `code_full_path`?
|
package/templates/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| 文件 | 用途 | 目标路径 |
|
|
8
8
|
|-----|------|---------|
|
|
9
|
-
| `hooks/hook.py` |
|
|
9
|
+
| `hooks/hook.py` | 动态文档工作流触发脚本 (关键词匹配 + 批量获取) | `.claude/hooks/hook.py` |
|
|
10
10
|
| `hooks/SystemPrompt.md` | 系统提示词模板 | `.claude/hooks/SystemPrompt.md` |
|
|
11
11
|
| `commands/pp/` | Claude Code 自定义命令 | `.claude/commands/pp/` |
|
|
12
12
|
| `AGENT.md` | Codex/通用 Agent 提示词 | `./AGENTS.md` |
|
|
@@ -45,7 +45,7 @@ CLI 通过 `detectIDEs()` 扫描项目目录,自动为检测到的 IDE 安装
|
|
|
45
45
|
## Hook 触发机制
|
|
46
46
|
|
|
47
47
|
```
|
|
48
|
-
用户输入 → hook.py → GET /
|
|
48
|
+
用户输入 → hook.py → GET /workflows → 关键词匹配 → POST /workflows/batch → 输出工作流
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
hooks 配置由 CLI 动态生成 (`generateHooksConfig()`),无需静态模板文件。
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
```
|
|
22
22
|
kg_flowchart(action:"list") → 知识图谱列表
|
|
23
23
|
kg_flowchart(action:"get") → 主图结构 (模块 + 连线)
|
|
24
|
-
|
|
24
|
+
kg_workflow(id:"review-audit") → 诊断工作流
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
**1.2 关键词定位模块**
|
|
@@ -68,7 +68,7 @@ Step 3: 输出疑点路径
|
|
|
68
68
|
|
|
69
69
|
```
|
|
70
70
|
code_smart_context(嫌疑符号名)
|
|
71
|
-
→ 代码依赖 + 关联文档 +
|
|
71
|
+
→ 代码依赖 + 关联文档 + 匹配工作流 + 影响范围摘要
|
|
72
72
|
|
|
73
73
|
code_impact(嫌疑符号名)
|
|
74
74
|
→ L1 直接引用 (必须检查)
|
|
@@ -164,7 +164,7 @@ L2: 2个间接调用方可能受影响
|
|
|
164
164
|
|:---|:---|:---|
|
|
165
165
|
| 锚定 | `kg_flowchart(list/get)` | 全景鸟瞰 |
|
|
166
166
|
| 锚定 | `kg_flowchart(get)` | 主图结构 |
|
|
167
|
-
| 锚定 | `
|
|
167
|
+
| 锚定 | `kg_workflow()` / `kg_workflow(id:"...")` | 分析工作流 |
|
|
168
168
|
| 下探 | `kg_flowchart(get_node, expand:N)` | 节点详情+上下游 |
|
|
169
169
|
| 下探 | `kg_flowchart(get, chartId:子图)` | 进入子图 |
|
|
170
170
|
| 半径 | `code_smart_context(symbol)` | 全关联上下文 |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| 原则 | 要求 |
|
|
6
6
|
|:---|:---|
|
|
7
|
-
|
|
|
7
|
+
| **工作流驱动** | 必须先加载 kg_workflow, 以 Markdown 工作流为审查标准 |
|
|
8
8
|
| **4维并行** | 逻辑/清洁/极简/图谱一致性 同时扫描 |
|
|
9
9
|
| **零容忍** | 任一维度 CRITICAL 即驳回, 不存在"下次再改" |
|
|
10
10
|
| **健康闭环** | 审查必须包含图谱健康检查, 确保知识库同步 |
|
|
@@ -26,11 +26,10 @@
|
|
|
26
26
|
### Phase 0: 加载审查标准
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
kg_rules(action:"get", ruleType:"errorAnalysis") → 错误分析规则
|
|
29
|
+
kg_workflow(id:"review-audit") → 审查/诊断工作流
|
|
30
|
+
kg_workflow(id:"common-general") → 通用执行工作流
|
|
32
31
|
```
|
|
33
|
-
|
|
32
|
+
将获取的文档工作流作为本次审查的量化标准。
|
|
34
33
|
|
|
35
34
|
**确定审查范围**:
|
|
36
35
|
```
|
|
@@ -75,7 +74,7 @@ Bash: git log --oneline -3 → 近期提交
|
|
|
75
74
|
|
|
76
75
|
#### C. 极简架构
|
|
77
76
|
```
|
|
78
|
-
|
|
77
|
+
对照已加载文档工作流中的编码约束逐条检查:
|
|
79
78
|
× 函数超50行 → 🟡 拆分建议
|
|
80
79
|
× 嵌套超3层 → 🟡 扁平化建议
|
|
81
80
|
× 重复造轮子 (code_query 搜索已有实现) → 🔴
|
|
@@ -144,9 +143,9 @@ kg_flowchart(action:"health")
|
|
|
144
143
|
**概览**: 评分 85/100 | 🔴 0 | 🟡 2 | 🔵 1
|
|
145
144
|
**状态**: ✅ 通过 / ❌ 驳回
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
146
|
+
**应用文档工作流**:
|
|
147
|
+
- review-audit: 已加载
|
|
148
|
+
- common-general: 已加载
|
|
150
149
|
|
|
151
150
|
**逻辑验证图** (A维度):
|
|
152
151
|
[用户输入] → [参数校验] → [业务处理] → [返回结果] ✅ 闭环
|
|
@@ -174,7 +173,7 @@ kg_flowchart(action:"health")
|
|
|
174
173
|
|
|
175
174
|
| 阶段 | 工具 | 用途 |
|
|
176
175
|
|:---|:---|:---|
|
|
177
|
-
| 加载 | `
|
|
176
|
+
| 加载 | `kg_workflow(id:"...")` | 审查/执行工作流 |
|
|
178
177
|
| 范围 | `code_context(file)` | 变更文件的依赖 |
|
|
179
178
|
| A逻辑 | 人工推演 + ASCII图 | 逻辑闭环验证 |
|
|
180
179
|
| B清洁 | `Grep(pattern)` | 死代码/调试残留扫描 |
|
package/templates/cursorrules.md
CHANGED
|
@@ -12,9 +12,10 @@ kg_discuss(action:"list") -> ???????
|
|
|
12
12
|
## 1. ????????
|
|
13
13
|
|
|
14
14
|
- ??: `kg_flowchart(action:"list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart")`
|
|
15
|
-
- ??: `
|
|
15
|
+
- ??: `kg_workflow()` / `kg_workflow(id:"...")` / `kg_workflow(action:"save|delete")`
|
|
16
16
|
- ??: `kg_task(action:"create|get|update|archive|delete")`
|
|
17
17
|
- ??: `kg_files(action:"list|read|upload|download|public_*")`
|
|
18
|
+
- ??: `kg_ref(action:"list|get|save|delete|read_file")`
|
|
18
19
|
- ??/??: `kg_discuss(...)`, `kg_meeting(...)`
|
|
19
20
|
- ????: `code_scan()`, `code_smart_context(symbolName)`, `code_full_path(symbolA, symbolB)`
|
|
20
21
|
- ????: ?????????????????????????????? fallback????????????
|
|
@@ -22,7 +23,7 @@ kg_discuss(action:"list") -> ???????
|
|
|
22
23
|
## 2. ?????
|
|
23
24
|
|
|
24
25
|
### Step 1: ????
|
|
25
|
-
1. ?? `kg_flowchart(get)` ????????? `
|
|
26
|
+
1. ?? `kg_flowchart(get)` ????????? `kg_workflow()` ?????
|
|
26
27
|
2. ???????? `kg_flowchart(get_node, expand:2, includeDoc:true, includeFiles:true)`?
|
|
27
28
|
3. ????? `subFlowchart`???????????????
|
|
28
29
|
4. ??????????? `code_scan()`??? `code_smart_context` / `code_full_path`?
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|:---|:---|
|
|
9
9
|
| **知识图谱中心** | 方案制定前检索图谱+代码双重验证;任务结束提示沉淀至图谱 |
|
|
10
10
|
| **任务驱动开发** | 复杂任务必须 `kg_task(action:"create")`;过程记录 `kg_task(action:"update")`;完成 `kg_task(action:"archive")` |
|
|
11
|
-
|
|
|
11
|
+
| **工作流引用** | 先 `kg_workflow()` 查看当前可用文档工作流;执行前优先读 `common-general`,审查/诊断前优先读 `review-audit` |
|
|
12
12
|
| **绝对真实性** | 禁用 faker/模拟数据,必须真实环境验证 |
|
|
13
13
|
| **沟通可视化** | 禁纯文字/Mermaid代码;强制 ASCII流程图 + Markdown表格 |
|
|
14
14
|
| **极简模块化** | 拆解为原子函数,清理注释旧代码/Debug日志 |
|
|
@@ -33,14 +33,14 @@ kg_task(action:"create") → kg_task(action:"update", log_type:"progress|issue|s
|
|
|
33
33
|
### Step 1: 分析与澄清
|
|
34
34
|
1. 接收需求,识别意图
|
|
35
35
|
2. `kg_flowchart(action:"search", query:"关键词")` 检索现有节点/历史坑点
|
|
36
|
-
3. `
|
|
36
|
+
3. `kg_workflow()` 获取全局/项目文档工作流,按需 `kg_workflow(id:"common-general")`
|
|
37
37
|
4. 有歧义则列选项供用户选择
|
|
38
38
|
|
|
39
39
|
**输出**: 需求确认清单 (表格)
|
|
40
40
|
|
|
41
41
|
### Step 2: 逻辑设计
|
|
42
42
|
1. 结合图谱+代码设计方案
|
|
43
|
-
2. `
|
|
43
|
+
2. `kg_workflow(id:"common-general")` 确认通用执行工作流
|
|
44
44
|
3. 检查现有复用函数,拒绝重复建设
|
|
45
45
|
4. 大型任务: `kg_task(action:"create")`
|
|
46
46
|
|
|
@@ -51,13 +51,13 @@ kg_task(action:"create") → kg_task(action:"update", log_type:"progress|issue|s
|
|
|
51
51
|
**前置**: 用户确认方案
|
|
52
52
|
1. `kg_task(action:"update", log_type:"progress", content:"开始编码")`
|
|
53
53
|
2. 优先编写/更新工具函数,再业务组装
|
|
54
|
-
3.
|
|
54
|
+
3. 遵循已加载文档工作流中的编码约束
|
|
55
55
|
4. 清理现场,无残留代码
|
|
56
56
|
|
|
57
57
|
**输出**: 结构清晰的代码
|
|
58
58
|
|
|
59
59
|
### Step 4: 真实验证
|
|
60
|
-
1. `
|
|
60
|
+
1. `kg_workflow(id:"review-audit")` 获取验证/诊断工作流
|
|
61
61
|
2. 在 `tests/` 对应目录创建测试
|
|
62
62
|
3. 真实环境运行,验证输出
|
|
63
63
|
4. 失败时: `kg_task(action:"update", log_type:"issue", content:"xxx失败")` → 回溯Step2
|
|
@@ -65,7 +65,7 @@ kg_task(action:"create") → kg_task(action:"update", log_type:"progress|issue|s
|
|
|
65
65
|
**严禁**: "测试环境所以失败"借口
|
|
66
66
|
|
|
67
67
|
### Step 5: 审查与沉淀
|
|
68
|
-
1. `
|
|
68
|
+
1. `kg_workflow(id:"review-audit")` 获取审查工作流
|
|
69
69
|
2. 审查目录结构/代码简洁度
|
|
70
70
|
3. 发现BUG → `kg_flowchart(action:"update_node")` 回写节点说明与文档
|
|
71
71
|
4. 新逻辑 → `kg_flowchart(action:"batch_add")` 新增节点,或 `kg_flowchart(action:"update_node")` 追加文档/版本
|
package/templates/hooks/hook.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
Claude Code Hook -
|
|
5
|
-
GET /
|
|
4
|
+
Claude Code Hook - 动态工作流触发
|
|
5
|
+
GET /workflows → 关键词匹配 → POST /workflows/batch 批量获取 Markdown 工作流
|
|
6
6
|
兼容 Python 2.7+ / 3.x,支持 Windows / macOS / Linux
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -123,14 +123,14 @@ def api_post(api_base, project_id, key, path, body):
|
|
|
123
123
|
return None
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def
|
|
127
|
-
"""
|
|
128
|
-
return api_get(api_base, project_id, key, "/
|
|
126
|
+
def fetch_workflows(api_base, project_id, key):
|
|
127
|
+
"""获取可用工作流摘要"""
|
|
128
|
+
return api_get(api_base, project_id, key, "/workflows") or []
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def
|
|
132
|
-
"""
|
|
133
|
-
return api_post(api_base, project_id, key, "/
|
|
131
|
+
def fetch_workflows_batch(api_base, project_id, key, items):
|
|
132
|
+
"""批量获取工作流正文"""
|
|
133
|
+
return api_post(api_base, project_id, key, "/workflows/batch", items) or []
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
# ╔══════════════════════════════════════════════════════════════╗
|
|
@@ -143,27 +143,36 @@ def count_hits(text, keywords):
|
|
|
143
143
|
return sum(1 for kw in keywords if kw.lower() in text)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def match_all(text,
|
|
147
|
-
"""
|
|
146
|
+
def match_all(text, workflows):
|
|
147
|
+
"""匹配所有触发的工作流,返回工作流摘要列表"""
|
|
148
148
|
matched = []
|
|
149
|
-
for
|
|
150
|
-
if
|
|
151
|
-
matched.append(
|
|
149
|
+
for wf in workflows:
|
|
150
|
+
if wf.get("always"):
|
|
151
|
+
matched.append(wf)
|
|
152
152
|
continue
|
|
153
|
-
keywords =
|
|
154
|
-
min_hits =
|
|
153
|
+
keywords = wf.get("keywords", [])
|
|
154
|
+
min_hits = wf.get("minHits", 1)
|
|
155
155
|
if keywords and count_hits(text, keywords) >= min_hits:
|
|
156
|
-
matched.append(
|
|
156
|
+
matched.append(wf)
|
|
157
157
|
return matched
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
162
|
-
|
|
160
|
+
def format_workflow(item):
|
|
161
|
+
"""格式化工作流输出"""
|
|
162
|
+
content = item.get("content", "")
|
|
163
|
+
if not content:
|
|
163
164
|
return ""
|
|
164
|
-
lines = ["# %s
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
lines = ["# %s" % item.get("title", item.get("id", "workflow"))]
|
|
166
|
+
meta = []
|
|
167
|
+
if item.get("scope"):
|
|
168
|
+
meta.append("scope=%s" % item.get("scope"))
|
|
169
|
+
if item.get("system"):
|
|
170
|
+
meta.append("system=%s" % item.get("system"))
|
|
171
|
+
if meta:
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.append("> %s" % " | ".join(meta))
|
|
174
|
+
lines.append("")
|
|
175
|
+
lines.append(content)
|
|
167
176
|
return "\n".join(lines)
|
|
168
177
|
|
|
169
178
|
|
|
@@ -204,27 +213,35 @@ def main():
|
|
|
204
213
|
if not project_id or not key:
|
|
205
214
|
return
|
|
206
215
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
if not
|
|
216
|
+
# 从服务器获取工作流摘要
|
|
217
|
+
workflows = fetch_workflows(api_base, project_id, key)
|
|
218
|
+
if not workflows:
|
|
210
219
|
return
|
|
211
220
|
|
|
212
|
-
#
|
|
213
|
-
matched = match_all(user_input_lower,
|
|
221
|
+
# 匹配所有触发的工作流
|
|
222
|
+
matched = match_all(user_input_lower, workflows)
|
|
214
223
|
if not matched:
|
|
215
224
|
return
|
|
216
225
|
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
# 批量获取所有命中的工作流正文
|
|
227
|
+
selectors = [{"id": wf.get("id", ""), "scope": wf.get("scope", "")} for wf in matched if wf.get("id")]
|
|
228
|
+
batch = fetch_workflows_batch(api_base, project_id, key, selectors)
|
|
229
|
+
if not batch:
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
batch_map = {}
|
|
233
|
+
for item in batch:
|
|
234
|
+
batch_map["%s:%s" % (item.get("scope", ""), item.get("id", ""))] = item
|
|
221
235
|
|
|
222
236
|
# 按匹配顺序格式化输出
|
|
223
237
|
output_parts = []
|
|
224
|
-
for
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
for wf in matched:
|
|
239
|
+
key_name = "%s:%s" % (wf.get("scope", ""), wf.get("id", ""))
|
|
240
|
+
item = batch_map.get(key_name)
|
|
241
|
+
if item:
|
|
242
|
+
rendered = format_workflow(item)
|
|
243
|
+
if rendered:
|
|
244
|
+
output_parts.append(rendered)
|
|
228
245
|
|
|
229
246
|
if output_parts:
|
|
230
247
|
print("\n\n".join(output_parts))
|
|
@@ -12,9 +12,10 @@ kg_discuss(action:"list") -> ???????
|
|
|
12
12
|
## 1. ????????
|
|
13
13
|
|
|
14
14
|
- ??: `kg_flowchart(action:"list|get|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart")`
|
|
15
|
-
- ??: `
|
|
15
|
+
- ??: `kg_workflow()` / `kg_workflow(id:"...")` / `kg_workflow(action:"save|delete")`
|
|
16
16
|
- ??: `kg_task(action:"create|get|update|archive|delete")`
|
|
17
17
|
- ??: `kg_files(action:"list|read|upload|download|public_*")`
|
|
18
|
+
- ??: `kg_ref(action:"list|get|save|delete|read_file")`
|
|
18
19
|
- ??/??: `kg_discuss(...)`, `kg_meeting(...)`
|
|
19
20
|
- ????: `code_scan()`, `code_smart_context(symbolName)`, `code_full_path(symbolA, symbolB)`
|
|
20
21
|
- ????: ?????????????????????????????? fallback????????????
|
|
@@ -22,7 +23,7 @@ kg_discuss(action:"list") -> ???????
|
|
|
22
23
|
## 2. ?????
|
|
23
24
|
|
|
24
25
|
### Step 1: ????
|
|
25
|
-
1. ?? `kg_flowchart(get)` ????????? `
|
|
26
|
+
1. ?? `kg_flowchart(get)` ????????? `kg_workflow()` ?????
|
|
26
27
|
2. ???????? `kg_flowchart(get_node, expand:2, includeDoc:true, includeFiles:true)`?
|
|
27
28
|
3. ????? `subFlowchart`???????????????
|
|
28
29
|
4. ??????????? `code_scan()`??? `code_smart_context` / `code_full_path`?
|
package/dist/tools/rules.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📏 kg_rules
|
|
3
|
-
* 统一规则入口: get | save | get_meta | save_meta | delete
|
|
4
|
-
*/
|
|
5
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
-
import { type McpContext } from './shared.js';
|
|
7
|
-
export declare function registerRuleTools(server: McpServer, ctx: McpContext): void;
|
package/dist/tools/rules.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 📏 kg_rules
|
|
3
|
-
* 统一规则入口: get | save | get_meta | save_meta | delete
|
|
4
|
-
*/
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
import { getClient } from '../storage/httpClient.js';
|
|
7
|
-
import { decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
|
|
8
|
-
import { wrap, safeTool, crossPrefix } from './shared.js';
|
|
9
|
-
export function registerRuleTools(server, ctx) {
|
|
10
|
-
const client = () => getClient();
|
|
11
|
-
server.tool('kg_rules', '📏 项目规则管理 — 读取或保存代码风格、审查规则等。action: get(读取规则)|save(保存规则)|get_meta(读取触发配置)|save_meta(保存触发配置)', {
|
|
12
|
-
action: z.enum(['get', 'save', 'get_meta', 'save_meta', 'delete'])
|
|
13
|
-
.describe('操作类型'),
|
|
14
|
-
ruleType: z.string().optional()
|
|
15
|
-
.describe('规则类型(如 userStyles, codeStyle, reviewRules)。get/save 时使用,不传则获取全部'),
|
|
16
|
-
rules: z.array(z.string()).optional()
|
|
17
|
-
.describe('save 时的规则数组'),
|
|
18
|
-
meta: z.record(z.string(), z.object({
|
|
19
|
-
label: z.string().describe('规则显示名称'),
|
|
20
|
-
keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
|
|
21
|
-
min_hits: z.number().default(1).describe('最低触发关键词数'),
|
|
22
|
-
always: z.boolean().default(false).describe('是否始终触发')
|
|
23
|
-
})).optional().describe('save_meta 时的触发配置映射'),
|
|
24
|
-
targetProject: z.string().optional()
|
|
25
|
-
.describe('跨项目读取 (get)'),
|
|
26
|
-
}, async (args) => safeTool(async () => {
|
|
27
|
-
const decoded = decodeObjectStrings(args);
|
|
28
|
-
switch (decoded.action) {
|
|
29
|
-
case 'get': {
|
|
30
|
-
if (decoded.targetProject) {
|
|
31
|
-
const crossMeta = await client().crossGetRulesMeta(decoded.targetProject);
|
|
32
|
-
if (decoded.ruleType) {
|
|
33
|
-
const rules = await client().crossGetRules(decoded.targetProject, decoded.ruleType);
|
|
34
|
-
if (rules.length === 0) {
|
|
35
|
-
const label = crossMeta[decoded.ruleType]?.label || decoded.ruleType;
|
|
36
|
-
return wrap(`暂无${label}规则(项目: ${decoded.targetProject})`);
|
|
37
|
-
}
|
|
38
|
-
return wrap(`${crossPrefix(decoded.targetProject)}${rules.join('\n')}`);
|
|
39
|
-
}
|
|
40
|
-
const types = Object.keys(crossMeta).length > 0 ? Object.keys(crossMeta) : Object.keys(RULE_TYPE_LABELS);
|
|
41
|
-
const allRules = [];
|
|
42
|
-
for (const type of types) {
|
|
43
|
-
const rules = await client().crossGetRules(decoded.targetProject, type);
|
|
44
|
-
if (rules.length > 0) {
|
|
45
|
-
const label = crossMeta[type]?.label || RULE_TYPE_LABELS[type] || type;
|
|
46
|
-
allRules.push(`## ${label}\n${rules.join('\n')}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (allRules.length === 0)
|
|
50
|
-
return wrap(`暂无项目规则(项目: ${decoded.targetProject})`);
|
|
51
|
-
return wrap(`${crossPrefix(decoded.targetProject)}${allRules.join('\n\n')}`);
|
|
52
|
-
}
|
|
53
|
-
const rules = await getRules(ctx.projectId, decoded.ruleType || undefined);
|
|
54
|
-
if (!rules || rules.trim() === '') {
|
|
55
|
-
const meta = await client().getRulesMeta();
|
|
56
|
-
const typeName = decoded.ruleType ? (meta[decoded.ruleType]?.label || decoded.ruleType) : '项目';
|
|
57
|
-
return wrap(`暂无${typeName}规则`);
|
|
58
|
-
}
|
|
59
|
-
return wrap(rules);
|
|
60
|
-
}
|
|
61
|
-
case 'save': {
|
|
62
|
-
if (!decoded.ruleType)
|
|
63
|
-
return wrap('❌ save 需要 ruleType');
|
|
64
|
-
if (!decoded.rules)
|
|
65
|
-
return wrap('❌ save 需要 rules 数组');
|
|
66
|
-
const existing = await client().getRulesApi(decoded.ruleType);
|
|
67
|
-
const existingSet = new Set(existing.map(r => r.trim()));
|
|
68
|
-
const toAdd = decoded.rules.filter((r) => !existingSet.has(r.trim()));
|
|
69
|
-
const merged = [...existing, ...toAdd];
|
|
70
|
-
const success = await client().saveRulesApi(decoded.ruleType, merged);
|
|
71
|
-
if (!success)
|
|
72
|
-
return wrap('❌ 保存失败');
|
|
73
|
-
const meta = await client().getRulesMeta();
|
|
74
|
-
const label = meta[decoded.ruleType]?.label || decoded.ruleType;
|
|
75
|
-
return wrap(`✅ ${label}已保存 (新增${toAdd.length}条, 共${merged.length}条)`);
|
|
76
|
-
}
|
|
77
|
-
case 'get_meta': {
|
|
78
|
-
const meta = await client().getRulesMeta();
|
|
79
|
-
if (Object.keys(meta).length === 0)
|
|
80
|
-
return wrap('暂无规则配置');
|
|
81
|
-
const lines = Object.entries(meta).map(([type, m]) => {
|
|
82
|
-
const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
|
|
83
|
-
const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
|
|
84
|
-
return `- **${m.label}** (${type}): ${trigger}`;
|
|
85
|
-
});
|
|
86
|
-
return wrap(`规则触发配置:\n\n${lines.join('\n')}`);
|
|
87
|
-
}
|
|
88
|
-
case 'save_meta': {
|
|
89
|
-
if (!decoded.meta)
|
|
90
|
-
return wrap('❌ save_meta 需要 meta');
|
|
91
|
-
const existing = await client().getRulesMeta();
|
|
92
|
-
const merged = { ...existing, ...decoded.meta };
|
|
93
|
-
const success = await client().saveRulesMeta(merged);
|
|
94
|
-
if (!success)
|
|
95
|
-
return wrap('❌ 保存失败');
|
|
96
|
-
return wrap(`✅ 触发配置已保存 (更新${Object.keys(decoded.meta).length}个类型, 共${Object.keys(merged).length}个类型)`);
|
|
97
|
-
}
|
|
98
|
-
case 'delete': {
|
|
99
|
-
if (!decoded.ruleType)
|
|
100
|
-
return wrap('❌ delete 需要 ruleType');
|
|
101
|
-
if (!decoded.rules || decoded.rules.length === 0)
|
|
102
|
-
return wrap('❌ delete 需要 rules 数组(要删除的规则内容)');
|
|
103
|
-
const existing = await client().getRulesApi(decoded.ruleType);
|
|
104
|
-
const toDelete = new Set(decoded.rules.map((r) => r.trim()));
|
|
105
|
-
const filtered = existing.filter((r) => !toDelete.has(r.trim()));
|
|
106
|
-
const removed = existing.length - filtered.length;
|
|
107
|
-
if (removed === 0)
|
|
108
|
-
return wrap('ℹ️ 未找到匹配的规则');
|
|
109
|
-
const success = await client().saveRulesApi(decoded.ruleType, filtered);
|
|
110
|
-
if (!success)
|
|
111
|
-
return wrap('❌ 删除失败');
|
|
112
|
-
const meta = await client().getRulesMeta();
|
|
113
|
-
const label = meta[decoded.ruleType]?.label || decoded.ruleType;
|
|
114
|
-
return wrap(`✅ ${label}: 已删除${removed}条, 剩余${filtered.length}条`);
|
|
115
|
-
}
|
|
116
|
-
default:
|
|
117
|
-
return wrap(`❌ 未知 action: ${decoded.action}`);
|
|
118
|
-
}
|
|
119
|
-
}));
|
|
120
|
-
}
|