@ppdocs/mcp 2.6.20 → 2.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -232,12 +232,10 @@ function copyDirRecursive(src, dest) {
232
232
  }
233
233
  }
234
234
  }
235
- /** 生成跨平台的 hooks 配置 */
235
+ /** 生成跨平台的 hooks 配置 (调用 python hook.py) */
236
236
  function generateHooksConfig() {
237
- const isWindows = process.platform === 'win32';
238
- const command = isWindows
239
- ? 'type ".claude\\hooks\\SystemPrompt.md"'
240
- : 'cat ".claude/hooks/SystemPrompt.md"';
237
+ // hook.py 支持关键词动态匹配,比静态读取 MD 更灵活
238
+ const command = 'python .claude/hooks/hook.py';
241
239
  return {
242
240
  hooks: {
243
241
  UserPromptSubmit: [
@@ -21,18 +21,19 @@ export function registerTools(server, projectId, _user) {
21
21
  })).optional().describe('依赖列表(自动生成连线)'),
22
22
  relatedFiles: z.array(z.string()).optional().describe('关联的源文件路径数组,如 ["src/auth.ts"]')
23
23
  }, async (args) => {
24
+ const decoded = decodeObjectStrings(args);
24
25
  const node = await storage.createNode(projectId, {
25
- title: args.title,
26
- type: args.type,
26
+ title: decoded.title,
27
+ type: decoded.type,
27
28
  status: 'incomplete',
28
- description: args.description || '',
29
+ description: decoded.description || '',
29
30
  // x, y 不传递,由 httpClient 智能计算位置
30
31
  locked: false,
31
- signature: args.signature || args.title,
32
- categories: args.tags,
33
- path: args.path,
34
- dependencies: args.dependencies || [],
35
- relatedFiles: args.relatedFiles || []
32
+ signature: decoded.signature || decoded.title,
33
+ categories: decoded.tags,
34
+ path: decoded.path,
35
+ dependencies: decoded.dependencies || [],
36
+ relatedFiles: decoded.relatedFiles || []
36
37
  });
37
38
  return wrap(projectId, JSON.stringify(node, null, 2));
38
39
  });
@@ -70,7 +71,8 @@ export function registerTools(server, projectId, _user) {
70
71
  impact: z.string().optional()
71
72
  })).optional().describe('修复记录')
72
73
  }, async (args) => {
73
- const { nodeId, tags, relatedFiles, versions, bugfixes, path, ...rest } = args;
74
+ const decoded = decodeObjectStrings(args);
75
+ const { nodeId, tags, relatedFiles, versions, bugfixes, path, ...rest } = decoded;
74
76
  // 根节点必须使用 kg_update_root 更新
75
77
  if (nodeId === 'root') {
76
78
  return wrap(projectId, '❌ 根节点请使用 kg_update_root 方法更新');
@@ -95,19 +97,20 @@ export function registerTools(server, projectId, _user) {
95
97
  title: z.string().optional().describe('项目标题'),
96
98
  description: z.string().optional().describe('项目介绍(Markdown)')
97
99
  }, async (args) => {
98
- if (args.title === undefined && args.description === undefined) {
99
- return wrap(projectId, '❌ 请至少提供 title description');
100
+ const decoded = decodeObjectStrings(args);
101
+ if (decoded.title === undefined && decoded.description === undefined) {
102
+ return wrap(projectId, '❌ ���至少提供 title 或 description');
100
103
  }
101
104
  const node = await storage.updateRoot(projectId, {
102
- title: args.title,
103
- description: args.description
105
+ title: decoded.title,
106
+ description: decoded.description
104
107
  });
105
108
  return wrap(projectId, node ? '✅ 项目介绍已更新' : '更新失败(根节点已锁定)');
106
109
  });
107
110
  // 3.6 获取项目规则 (独立方法,按需调用)
108
111
  server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)', {
109
- ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle']).optional()
110
- .describe('规则类型: userStyles=项目规则, testRules=测试规则, reviewRules=审查规则, codeStyle=编码风格。不传则返回全部')
112
+ ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests']).optional()
113
+ .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部')
111
114
  }, async (args) => {
112
115
  const rules = await getRules(projectId, args.ruleType);
113
116
  if (!rules || rules.trim() === '') {
@@ -118,15 +121,16 @@ export function registerTools(server, projectId, _user) {
118
121
  });
119
122
  // 3.7 保存项目规则 (独立存储)
120
123
  server.tool('kg_save_rules', '保存单个类型的项目规则(独立文件存储)', {
121
- ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle'])
122
- .describe('规则类型: userStyles=项目规则, testRules=测试规则, reviewRules=审查规则, codeStyle=编码风格'),
124
+ ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests'])
125
+ .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则'),
123
126
  rules: z.array(z.string()).describe('规则数组')
124
127
  }, async (args) => {
125
- const success = await storage.saveRules(projectId, args.ruleType, args.rules);
128
+ const decoded = decodeObjectStrings(args);
129
+ const success = await storage.saveRules(projectId, decoded.ruleType, decoded.rules);
126
130
  if (!success) {
127
131
  return wrap(projectId, '❌ 保存失败');
128
132
  }
129
- return wrap(projectId, `✅ ${RULE_TYPE_LABELS[args.ruleType]}已保存 (${args.rules.length} 条)`);
133
+ return wrap(projectId, `✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
130
134
  });
131
135
  // 4. 锁定节点 (只能锁定,解锁需用户在前端手动操作)
132
136
  server.tool('kg_lock_node', '锁定节点(锁定后只能读取,解锁需用户在前端手动操作)', {
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP Server 工具函数
3
3
  */
4
- export type RuleType = 'userStyles' | 'testRules' | 'reviewRules' | 'codeStyle';
4
+ export type RuleType = 'userStyles' | 'testRules' | 'reviewRules' | 'codeStyle' | 'unitTests';
5
5
  export declare const RULE_TYPE_LABELS: Record<RuleType, string>;
6
6
  /**
7
7
  * 获取指定类型的规则 (从独立文件读取)
package/dist/utils.js CHANGED
@@ -4,10 +4,11 @@
4
4
  import * as storage from './storage/httpClient.js';
5
5
  // 规则类型显示名
6
6
  export const RULE_TYPE_LABELS = {
7
- userStyles: '项目规则',
8
- testRules: '测试规则',
9
- reviewRules: '审查规则',
10
- codeStyle: '编码风格',
7
+ userStyles: '用户沟通规则',
8
+ codeStyle: '编码风格规则',
9
+ reviewRules: '代码审查规则',
10
+ testRules: '错误分析规则',
11
+ unitTests: '代码测试规则',
11
12
  };
12
13
  /**
13
14
  * 将字符串数组格式化为 Markdown 列表
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "2.6.20",
3
+ "version": "2.6.21",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Claude Code Hook - 关键词触发 + API 动态获取规则
5
+ """
6
+
7
+ import io
8
+ import json
9
+ import os
10
+ import sys
11
+ import urllib.request
12
+
13
+ # Windows 编码修复
14
+ sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
15
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
16
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
17
+
18
+
19
+ # ╔══════════════════════════════════════════════════════════════╗
20
+ # ║ 配置区 ║
21
+ # ╚══════════════════════════════════════════════════════════════╝
22
+
23
+ # 关键词规则 (触发关键词 → 请求对应规则类型)
24
+ RULES = [
25
+ {
26
+ "keywords": ["bug","修复","错误","报错","失败","异常","严重","存在","还是","有"],
27
+ "min_hits": 2,
28
+ "rule_type": "testRules", # 错误分析规则
29
+ "label": "错误分析规则",
30
+ },
31
+ {
32
+ "keywords": ["审查", "审核", "review", "检查"],
33
+ "min_hits": 1,
34
+ "rule_type": "reviewRules", # 审查规则
35
+ "label": "代码审查规则",
36
+ },
37
+ {
38
+ "keywords": ["编码", "风格", "格式", "命名", "编写", "开始", "代码"],
39
+ "min_hits": 2,
40
+ "rule_type": "codeStyle", # 编码风格
41
+ "label": "编码风格规则",
42
+ },
43
+ {
44
+ "keywords": ["开始", "进行", "准备", "测试", "单元", "用例", "test", "覆盖率"],
45
+ "min_hits": 2,
46
+ "rule_type": "unitTests", # 代码测试
47
+ "label": "代码测试规则",
48
+ },
49
+ ]
50
+
51
+ # 绕过词列表
52
+ BYPASS = [
53
+ "补充",
54
+ "确定",
55
+ "确认",
56
+ "继续",
57
+ "好的",
58
+ "可以",
59
+ "ok",
60
+ "yes",
61
+ "hi",
62
+ "hello",
63
+ "你好",
64
+ ]
65
+
66
+
67
+ # ╔══════════════════════════════════════════════════════════════╗
68
+ # ║ API 请求函数 ║
69
+ # ╚══════════════════════════════════════════════════════════════╝
70
+
71
+
72
+ def load_ppdocs_config():
73
+ """读取 .ppdocs 配置"""
74
+ config_path = os.path.join(os.getcwd(), ".ppdocs")
75
+ if not os.path.exists(config_path):
76
+ return None
77
+ try:
78
+ with open(config_path, "r", encoding="utf-8") as f:
79
+ return json.load(f)
80
+ except:
81
+ return None
82
+
83
+
84
+ def fetch_rules(api_base: str, project_id: str, key: str, rule_type: str) -> list:
85
+ """通过 HTTP API 获取指定类型的规则"""
86
+ url = f"{api_base}/api/{project_id}/{key}/rules/{rule_type}"
87
+ try:
88
+ req = urllib.request.Request(url, method="GET")
89
+ req.add_header("Content-Type", "application/json")
90
+ with urllib.request.urlopen(req, timeout=3) as resp:
91
+ data = json.loads(resp.read().decode("utf-8"))
92
+ if data.get("success") and data.get("data"):
93
+ return data["data"]
94
+ except:
95
+ pass
96
+ return []
97
+
98
+
99
+ # ╔══════════════════════════════════════════════════════════════╗
100
+ # ║ 匹配逻辑 ║
101
+ # ╚══════════════════════════════════════════════════════════════╝
102
+
103
+
104
+ def count_hits(text: str, keywords: list) -> int:
105
+ """计算关键词命中数量"""
106
+ return sum(1 for kw in keywords if kw.lower() in text)
107
+
108
+
109
+ def match_rule(text: str):
110
+ """匹配规则,返回 (rule, matched)"""
111
+ for rule in RULES:
112
+ hits = count_hits(text, rule["keywords"])
113
+ if hits >= rule.get("min_hits", 1):
114
+ return rule, True
115
+ return None, False
116
+
117
+
118
+ def format_rules(items: list, label: str) -> str:
119
+ """格式化规则输出"""
120
+ if not items:
121
+ return ""
122
+ lines = [f"# {label}\n"]
123
+ for item in items:
124
+ lines.append(f"- {item}")
125
+ return "\n".join(lines)
126
+
127
+
128
+ # ╔══════════════════════════════════════════════════════════════╗
129
+ # ║ 主入口 ║
130
+ # ╚══════════════════════════════════════════════════════════════╝
131
+
132
+
133
+ def main():
134
+ try:
135
+ data = json.load(sys.stdin)
136
+ except:
137
+ return
138
+
139
+ if data.get("hook_event_name") != "UserPromptSubmit":
140
+ return
141
+
142
+ user_input = data.get("prompt", "").strip()
143
+ if not user_input:
144
+ return
145
+
146
+ user_input_lower = user_input.lower()
147
+
148
+ # 短输入包含绕过词 → 跳过
149
+ if len(user_input) <= 15:
150
+ for word in BYPASS:
151
+ if word.lower() in user_input_lower:
152
+ return
153
+
154
+ # 读取配置
155
+ config = load_ppdocs_config()
156
+ if not config:
157
+ return
158
+
159
+ api_base = config.get("api", "http://localhost:20001")
160
+ project_id = config.get("projectId", "")
161
+ key = config.get("key", "")
162
+
163
+ if not project_id or not key:
164
+ return
165
+
166
+ output_parts = []
167
+
168
+ # 1. 强制获取 userStyles (用户沟通规则)
169
+ user_styles = fetch_rules(api_base, project_id, key, "userStyles")
170
+ if user_styles:
171
+ output_parts.append(format_rules(user_styles, "用户沟通规则"))
172
+
173
+ # 2. 根据关键词匹配额外规则
174
+ rule, matched = match_rule(user_input_lower)
175
+ if matched and rule["rule_type"] != "userStyles":
176
+ extra_rules = fetch_rules(api_base, project_id, key, rule["rule_type"])
177
+ if extra_rules:
178
+ output_parts.append(format_rules(extra_rules, rule["label"]))
179
+
180
+ # 输出
181
+ if output_parts:
182
+ print("\n\n".join(output_parts))
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()