@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 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
- | `kg_rules` | 规则管理 |
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
- 'kg_rules',
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, RuleMeta } from './types.js';
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
- getRulesApi(ruleType: string): Promise<string[]>;
14
- saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
15
- getRulesMeta(): Promise<Record<string, RuleMeta>>;
16
- saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
17
- crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
18
- getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
19
- saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
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
- // ============ 规则 API ============
103
- async getRulesApi(ruleType) {
104
- try {
105
- return await this.request(`/rules/${ruleType}`);
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 saveRulesApi(ruleType, rules) {
107
+ async getWorkflow(workflowId, scope) {
112
108
  try {
113
- await this.request(`/rules/${ruleType}`, {
114
- method: 'PUT',
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 false;
113
+ return null;
121
114
  }
122
115
  }
123
- async getRulesMeta() {
124
- try {
125
- return await this.request('/rules-meta');
126
- }
127
- catch {
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 saveRulesMeta(meta) {
132
- try {
133
- await this.request('/rules-meta', {
134
- method: 'PUT',
135
- body: JSON.stringify(meta)
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 crossGetRulesMeta(target) {
144
- try {
145
- return await this.request(`/cross/${encodeURIComponent(target)}/rules-meta`);
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
- async getGlobalRulesMeta() {
152
- try {
153
- return await this.request('/global-rules-meta');
154
- }
155
- catch {
156
- return {};
157
- }
133
+ // ============ 外部参考 API ============
134
+ async listReferences() {
135
+ return this.request('/refs');
158
136
  }
159
- async saveGlobalRulesMeta(meta) {
137
+ async getReference(refId) {
160
138
  try {
161
- await this.request('/global-rules-meta', {
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 false;
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)}`, {
@@ -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;
@@ -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';
@@ -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', '🔍 代码+文档全关联上下文 — 输入一个函数名,一次调用返回:代码依赖、关联文档、匹配规则、活跃任务、影响范围摘要。需先运行 code_scan', {
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.matchedRules.length > 0) {
80
- lines.push(`### 📏 匹配规则 (${smartCtx.matchedRules.length})`);
81
- for (const rule of smartCtx.matchedRules) {
82
- lines.push(` [${rule.ruleType}] ${rule.content.substring(0, 100)}`);
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
  }
@@ -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
- try {
238
- await client().discussionReply(decoded.id, me, `📋 结案总结:\n\n${decoded.conclusion}`, '结案总结');
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)
@@ -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', 'Logical flowchart operations: list|get|search|get_relations|find_path|get_node|update_node|delete_node|batch_add|bind|unbind|orphans|health|create_chart|delete_chart', {
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'),
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 12 个工具, 6 个子模块
3
+ * 13 个工具, 7 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
- * 📚 知识: kg_projects, kg_rules (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
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个)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 12 个工具, 6 个子模块
3
+ * 13 个工具, 7 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
- * 📚 知识: kg_projects, kg_rules (2个)
8
- * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区) (3个)
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 { registerRuleTools } from './rules.js';
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
- registerRuleTools(server, ctx);
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,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerReferenceTools(server: McpServer): void;
@@ -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
+ }
@@ -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,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type McpContext } from './shared.js';
3
+ export declare function registerWorkflowTools(server: McpServer, _ctx: McpContext): void;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.2.37",
3
+ "version": "3.2.38",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- - ??: `kg_rules(action:"get|save|get_meta|save_meta")`
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)` ????????? `kg_rules(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`?
@@ -6,7 +6,7 @@
6
6
 
7
7
  | 文件 | 用途 | 目标路径 |
8
8
  |-----|------|---------|
9
- | `hooks/hook.py` | 动态规则触发脚本 (关键词匹配 + 批量获取) | `.claude/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 /rules-meta → 关键词匹配 → POST /rules/batch → 输出规则
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
- kg_rules(action:"get", ruleType:"errorAnalysis") → 错误分析规则
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
- | 锚定 | `kg_rules(get)` | 分析规则 |
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
- | **规则驱动** | 必须先加载 kg_rules, 以项目规则为审查标准 |
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
- kg_rules(action:"get", ruleType:"reviewRules") 审查规则
30
- kg_rules(action:"get", ruleType:"codeStyle") 编码风格
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
- 对照 codeStyle 规则逐条检查:
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
- - reviewRules: 已加载 N 条
149
- - codeStyle: 已加载 M 条
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
- | 加载 | `kg_rules(get)` | 审查规则/编码风格 |
176
+ | 加载 | `kg_workflow(id:"...")` | 审查/执行工作流 |
178
177
  | 范围 | `code_context(file)` | 变更文件的依赖 |
179
178
  | A逻辑 | 人工推演 + ASCII图 | 逻辑闭环验证 |
180
179
  | B清洁 | `Grep(pattern)` | 死代码/调试残留扫描 |
@@ -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
- - ??: `kg_rules(action:"get|save|get_meta|save_meta")`
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)` ????????? `kg_rules(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
- | **规则引用** | 编码前读 `codeStyle`;测试前读 `testRules`;审查前读 `reviewRules` |
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. `kg_rules(action:"get")` 获取项目规则
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. `kg_rules(action:"get", ruleType:"codeStyle")` 确认编码规范
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. 遵循 codeStyle 规则
54
+ 3. 遵循已加载文档工作流中的编码约束
55
55
  4. 清理现场,无残留代码
56
56
 
57
57
  **输出**: 结构清晰的代码
58
58
 
59
59
  ### Step 4: 真实验证
60
- 1. `kg_rules(action:"get", ruleType:"testRules")` 获取测试规则
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. `kg_rules(action:"get", ruleType:"reviewRules")` 获取审查规则
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")` 追加文档/版本
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- Claude Code Hook - 动态规则触发
5
- GET /rules-meta → 关键词匹配 → POST /rules/batch 批量获取
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 fetch_rules_meta(api_base, project_id, key):
127
- """获取规则触发配置"""
128
- return api_get(api_base, project_id, key, "/rules-meta") or {}
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 fetch_rules_batch(api_base, project_id, key, types):
132
- """批量获取多个规则类型的内容"""
133
- return api_post(api_base, project_id, key, "/rules/batch", types) or {}
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, meta):
147
- """匹配所有触发的规则类型,返回 [(rule_type, label), ...]"""
146
+ def match_all(text, workflows):
147
+ """匹配所有触发的工作流,返回工作流摘要列表"""
148
148
  matched = []
149
- for rule_type, config in meta.items():
150
- if config.get("always"):
151
- matched.append((rule_type, config.get("label", rule_type)))
149
+ for wf in workflows:
150
+ if wf.get("always"):
151
+ matched.append(wf)
152
152
  continue
153
- keywords = config.get("keywords", [])
154
- min_hits = config.get("min_hits", 1)
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((rule_type, config.get("label", rule_type)))
156
+ matched.append(wf)
157
157
  return matched
158
158
 
159
159
 
160
- def format_rules(items, label):
161
- """格式化规则输出"""
162
- if not items:
160
+ def format_workflow(item):
161
+ """格式化工作流输出"""
162
+ content = item.get("content", "")
163
+ if not content:
163
164
  return ""
164
- lines = ["# %s\n" % label]
165
- for item in items:
166
- lines.append("- %s" % item)
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
- meta = fetch_rules_meta(api_base, project_id, key)
209
- if not meta:
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, meta)
221
+ # 匹配所有触发的工作流
222
+ matched = match_all(user_input_lower, workflows)
214
223
  if not matched:
215
224
  return
216
225
 
217
- # 批量获取所有命中的规则内容 (单次请求)
218
- type_list = [rule_type for rule_type, _ in matched]
219
- label_map = {rule_type: label for rule_type, label in matched}
220
- batch = fetch_rules_batch(api_base, project_id, key, type_list)
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 rule_type in type_list:
225
- rules = batch.get(rule_type, [])
226
- if rules:
227
- output_parts.append(format_rules(rules, label_map[rule_type]))
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
- - ??: `kg_rules(action:"get|save|get_meta|save_meta")`
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)` ????????? `kg_rules(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`?
@@ -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;
@@ -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
- }