@ppdocs/mcp 3.0.0 → 3.0.2

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.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * API URL 格式: http://localhost:20001/api/:projectId/:password/...
6
6
  */
7
- import type { DocData, DocNode, DocSearchResult, Task, TaskSummary, TaskLogType, TaskExperience, FileInfo } from './types.js';
7
+ import type { DocData, DocNode, DocSearchResult, Task, TaskSummary, TaskLogType, TaskExperience, FileInfo, RuleMeta } from './types.js';
8
8
  interface TreeNode {
9
9
  path: string;
10
10
  name: string;
@@ -14,6 +14,7 @@ interface TreeNode {
14
14
  }
15
15
  export declare class PpdocsApiClient {
16
16
  private baseUrl;
17
+ private serverUrl;
17
18
  constructor(apiUrl: string);
18
19
  private request;
19
20
  listDocs(): Promise<DocNode[]>;
@@ -28,6 +29,11 @@ export declare class PpdocsApiClient {
28
29
  getTree(): Promise<TreeNode[]>;
29
30
  getRulesApi(ruleType: string): Promise<string[]>;
30
31
  saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
32
+ getRulesMeta(): Promise<Record<string, RuleMeta>>;
33
+ saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
34
+ crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
35
+ getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
36
+ saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
31
37
  listTasks(status?: 'active' | 'archived'): Promise<TaskSummary[]>;
32
38
  getTask(taskId: string): Promise<Task | null>;
33
39
  createTask(task: {
@@ -37,6 +43,14 @@ export declare class PpdocsApiClient {
37
43
  }, creator: string): Promise<Task>;
38
44
  addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
39
45
  completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
46
+ /** 创建项目 (公开路由,无需认证) */
47
+ createProject(id: string, name: string, description?: string, projectPath?: string): Promise<{
48
+ project: {
49
+ id: string;
50
+ name: string;
51
+ };
52
+ password: string;
53
+ }>;
40
54
  /** 列出所有可访问的项目 */
41
55
  crossListProjects(): Promise<{
42
56
  id: string;
@@ -90,6 +104,11 @@ export declare function getDocsByStatus(_projectId: string, statusList: string[]
90
104
  export declare function getTree(_projectId: string): Promise<TreeNode[]>;
91
105
  export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
92
106
  export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
107
+ export declare function getRulesMeta(): Promise<Record<string, RuleMeta>>;
108
+ export declare function saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
109
+ export declare function crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
110
+ export declare function getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
111
+ export declare function saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
93
112
  export declare function listTasks(_projectId: string, status?: 'active' | 'archived'): Promise<TaskSummary[]>;
94
113
  export declare function getTask(_projectId: string, taskId: string): Promise<Task | null>;
95
114
  export declare function createTask(_projectId: string, task: {
@@ -99,6 +118,13 @@ export declare function createTask(_projectId: string, task: {
99
118
  }, creator: string): Promise<Task>;
100
119
  export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
101
120
  export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
121
+ export declare function createProject(id: string, name: string, description?: string, projectPath?: string): Promise<{
122
+ project: {
123
+ id: string;
124
+ name: string;
125
+ };
126
+ password: string;
127
+ }>;
102
128
  export declare function crossListProjects(): Promise<{
103
129
  id: string;
104
130
  name: string;
@@ -91,9 +91,12 @@ function buildTree(docs) {
91
91
  // API 客户端类
92
92
  export class PpdocsApiClient {
93
93
  baseUrl; // http://localhost:20001/api/projectId/password
94
+ serverUrl; // http://localhost:20001
94
95
  constructor(apiUrl) {
95
- // 移除末尾斜杠
96
96
  this.baseUrl = apiUrl.replace(/\/$/, '');
97
+ // 从 /api/projectId/password 提取服务器根地址
98
+ const apiIndex = this.baseUrl.indexOf('/api/');
99
+ this.serverUrl = apiIndex > 0 ? this.baseUrl.substring(0, apiIndex) : this.baseUrl;
97
100
  }
98
101
  // ============ HTTP 请求工具 ============
99
102
  async request(path, options = {}) {
@@ -227,6 +230,54 @@ export class PpdocsApiClient {
227
230
  return false;
228
231
  }
229
232
  }
233
+ async getRulesMeta() {
234
+ try {
235
+ return await this.request('/rules-meta');
236
+ }
237
+ catch {
238
+ return {};
239
+ }
240
+ }
241
+ async saveRulesMeta(meta) {
242
+ try {
243
+ await this.request('/rules-meta', {
244
+ method: 'PUT',
245
+ body: JSON.stringify(meta)
246
+ });
247
+ return true;
248
+ }
249
+ catch {
250
+ return false;
251
+ }
252
+ }
253
+ async crossGetRulesMeta(target) {
254
+ try {
255
+ return await this.request(`/cross/${encodeURIComponent(target)}/rules-meta`);
256
+ }
257
+ catch {
258
+ return {};
259
+ }
260
+ }
261
+ async getGlobalRulesMeta() {
262
+ try {
263
+ return await this.request('/global-rules-meta');
264
+ }
265
+ catch {
266
+ return {};
267
+ }
268
+ }
269
+ async saveGlobalRulesMeta(meta) {
270
+ try {
271
+ await this.request('/global-rules-meta', {
272
+ method: 'PUT',
273
+ body: JSON.stringify(meta)
274
+ });
275
+ return true;
276
+ }
277
+ catch {
278
+ return false;
279
+ }
280
+ }
230
281
  // ============ 任务管理 ============
231
282
  async listTasks(status) {
232
283
  const query = status ? `?status=${status}` : '';
@@ -277,6 +328,22 @@ export class PpdocsApiClient {
277
328
  return null;
278
329
  }
279
330
  }
331
+ // ============ 项目管理 ============
332
+ /** 创建项目 (公开路由,无需认证) */
333
+ async createProject(id, name, description, projectPath) {
334
+ const url = `${this.serverUrl}/api/projects`;
335
+ const response = await fetch(url, {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({ id, name, description: description || '', project_path: projectPath || '' }),
339
+ });
340
+ if (!response.ok) {
341
+ const error = await response.json().catch(() => ({ error: response.statusText }));
342
+ throw new Error(error.error || `HTTP ${response.status}`);
343
+ }
344
+ const json = await response.json();
345
+ return json.data;
346
+ }
280
347
  // ============ 跨项目只读访问 ============
281
348
  /** 列出所有可访问的项目 */
282
349
  async crossListProjects() {
@@ -441,6 +508,21 @@ export async function getRules(_projectId, ruleType) {
441
508
  export async function saveRules(_projectId, ruleType, rules) {
442
509
  return getClient().saveRulesApi(ruleType, rules);
443
510
  }
511
+ export async function getRulesMeta() {
512
+ return getClient().getRulesMeta();
513
+ }
514
+ export async function saveRulesMeta(meta) {
515
+ return getClient().saveRulesMeta(meta);
516
+ }
517
+ export async function crossGetRulesMeta(target) {
518
+ return getClient().crossGetRulesMeta(target);
519
+ }
520
+ export async function getGlobalRulesMeta() {
521
+ return getClient().getGlobalRulesMeta();
522
+ }
523
+ export async function saveGlobalRulesMeta(meta) {
524
+ return getClient().saveGlobalRulesMeta(meta);
525
+ }
444
526
  // ============ 任务管理 ============
445
527
  export async function listTasks(_projectId, status) {
446
528
  return getClient().listTasks(status);
@@ -457,6 +539,10 @@ export async function addTaskLog(_projectId, taskId, logType, content) {
457
539
  export async function completeTask(_projectId, taskId, experience) {
458
540
  return getClient().completeTask(taskId, experience);
459
541
  }
542
+ // ============ 项目管理 ============
543
+ export async function createProject(id, name, description, projectPath) {
544
+ return getClient().createProject(id, name, description, projectPath);
545
+ }
460
546
  // ============ 跨项目只读访问 ============
461
547
  export async function crossListProjects() {
462
548
  return getClient().crossListProjects();
@@ -47,6 +47,12 @@ export interface Project {
47
47
  createdAt?: string;
48
48
  projectPath?: string;
49
49
  }
50
+ export interface RuleMeta {
51
+ label: string;
52
+ keywords: string[];
53
+ min_hits: number;
54
+ always?: boolean;
55
+ }
50
56
  export interface FileInfo {
51
57
  name: string;
52
58
  path: string;
@@ -21,6 +21,21 @@ export function registerTools(server, projectId, _user) {
21
21
  return wrap(`获取项目列表失败: ${String(e)}`);
22
22
  }
23
23
  });
24
+ // 0.1 创建项目
25
+ server.tool('kg_create_project', '创建新项目。返回项目ID和密码(密码用于MCP连接)', {
26
+ id: z.string().describe('项目ID(英文/数字/中文,如"my-app")'),
27
+ name: z.string().describe('项目名称'),
28
+ description: z.string().optional().describe('项目简介'),
29
+ projectPath: z.string().optional().describe('项目源码路径(本地绝对路径)'),
30
+ }, async (args) => {
31
+ try {
32
+ const result = await storage.createProject(args.id, args.name, args.description, args.projectPath || process.cwd());
33
+ return wrap(`✅ 项目创建成功\n\n- 项目ID: ${result.project.id}\n- 项目名: ${result.project.name}\n- 密码: ${result.password}\n\nMCP 连接地址: http://<服务器IP>:20001/api/${result.project.id}/${result.password}`);
34
+ }
35
+ catch (e) {
36
+ return wrap(`❌ ${String(e)}`);
37
+ }
38
+ });
24
39
  // ===================== 文档管理 =====================
25
40
  // 1. 创建文档
26
41
  server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
@@ -183,25 +198,29 @@ export function registerTools(server, projectId, _user) {
183
198
  });
184
199
  // 3.6 获取项目规则
185
200
  server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)。支持跨项目只读访问', {
186
- ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests']).optional()
187
- .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部'),
201
+ ruleType: z.string().optional()
202
+ .describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)。不传则返回全部'),
188
203
  targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
189
204
  }, async (args) => {
190
205
  // 跨项目访问
191
206
  if (args.targetProject) {
207
+ const crossMeta = await storage.crossGetRulesMeta(args.targetProject);
192
208
  if (args.ruleType) {
193
209
  const rules = await storage.crossGetRules(args.targetProject, args.ruleType);
194
210
  if (rules.length === 0) {
195
- return { content: [{ type: 'text', text: `暂无${RULE_TYPE_LABELS[args.ruleType]}规则(项目: ${args.targetProject})` }] };
211
+ const label = crossMeta[args.ruleType]?.label || args.ruleType;
212
+ return { content: [{ type: 'text', text: `暂无${label}规则(项目: ${args.targetProject})` }] };
196
213
  }
197
214
  return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${rules.join('\n')}` }] };
198
215
  }
199
- // 获取全部规则
216
+ // 获取全部规则 (从 meta 获取所有类型)
217
+ const types = Object.keys(crossMeta).length > 0 ? Object.keys(crossMeta) : Object.keys(RULE_TYPE_LABELS);
200
218
  const allRules = [];
201
- for (const type of ['userStyles', 'codeStyle', 'reviewRules', 'testRules', 'unitTests']) {
219
+ for (const type of types) {
202
220
  const rules = await storage.crossGetRules(args.targetProject, type);
203
221
  if (rules.length > 0) {
204
- allRules.push(`## ${RULE_TYPE_LABELS[type]}\n${rules.join('\n')}`);
222
+ const label = crossMeta[type]?.label || RULE_TYPE_LABELS[type] || type;
223
+ allRules.push(`## ${label}\n${rules.join('\n')}`);
205
224
  }
206
225
  }
207
226
  if (allRules.length === 0) {
@@ -210,17 +229,18 @@ export function registerTools(server, projectId, _user) {
210
229
  return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${allRules.join('\n\n')}` }] };
211
230
  }
212
231
  // 当前项目
213
- const rules = await getRules(projectId, args.ruleType);
232
+ const rules = await getRules(projectId, args.ruleType || undefined);
214
233
  if (!rules || rules.trim() === '') {
215
- const typeName = args.ruleType ? RULE_TYPE_LABELS[args.ruleType] : '项目';
234
+ const meta = await storage.getRulesMeta();
235
+ const typeName = args.ruleType ? (meta[args.ruleType]?.label || args.ruleType) : '项目';
216
236
  return { content: [{ type: 'text', text: `暂无${typeName}规则` }] };
217
237
  }
218
238
  return { content: [{ type: 'text', text: rules }] };
219
239
  });
220
240
  // 3.7 保存项目规则
221
241
  server.tool('kg_save_rules', '保存单个类型的项目规则(独立文件存储)', {
222
- ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests'])
223
- .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则'),
242
+ ruleType: z.string()
243
+ .describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)'),
224
244
  rules: z.array(z.string()).describe('规则数组')
225
245
  }, async (args) => {
226
246
  const decoded = decodeObjectStrings(args);
@@ -228,7 +248,87 @@ export function registerTools(server, projectId, _user) {
228
248
  if (!success) {
229
249
  return wrap('❌ 保存失败');
230
250
  }
231
- return wrap(`✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
251
+ const meta = await storage.getRulesMeta();
252
+ const label = meta[decoded.ruleType]?.label || decoded.ruleType;
253
+ return wrap(`✅ ${label}已保存 (${decoded.rules.length} 条)`);
254
+ });
255
+ // 3.8 获取规则触发配置
256
+ server.tool('kg_get_rules_meta', '获取规则触发配置(所有类型的标签、关键词、触发数)。用于查看/编辑 hooks 触发条件', {}, async () => {
257
+ try {
258
+ const meta = await storage.getRulesMeta();
259
+ if (Object.keys(meta).length === 0) {
260
+ return wrap('暂无规则配置');
261
+ }
262
+ const lines = Object.entries(meta).map(([type, m]) => {
263
+ const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
264
+ const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
265
+ return `- **${m.label}** (${type}): ${trigger}`;
266
+ });
267
+ return wrap(`规则触发配置:\n\n${lines.join('\n')}`);
268
+ }
269
+ catch (e) {
270
+ return wrap(`❌ ${String(e)}`);
271
+ }
272
+ });
273
+ // 3.9 保存规则触发配置
274
+ server.tool('kg_save_rules_meta', '保存规则触发配置(标签、关键词、触发数)。支持新增自定义规则类型', {
275
+ meta: z.record(z.string(), z.object({
276
+ label: z.string().describe('规则显示名称'),
277
+ keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
278
+ min_hits: z.number().default(1).describe('最低触发关键词数'),
279
+ always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
280
+ })).describe('规则类型 → 触发配置的映射')
281
+ }, async (args) => {
282
+ try {
283
+ const decoded = decodeObjectStrings(args.meta);
284
+ const success = await storage.saveRulesMeta(decoded);
285
+ if (!success) {
286
+ return wrap('❌ 保存失败');
287
+ }
288
+ return wrap(`✅ 规则触发配置已保存 (${Object.keys(decoded).length} 个类型)`);
289
+ }
290
+ catch (e) {
291
+ return wrap(`❌ ${String(e)}`);
292
+ }
293
+ });
294
+ // 3.10 获取全局默认规则触发配置
295
+ server.tool('kg_get_global_rules_meta', '获取全局默认规则触发配置(新项目创建时继承此配置)', {}, async () => {
296
+ try {
297
+ const meta = await storage.getGlobalRulesMeta();
298
+ if (Object.keys(meta).length === 0) {
299
+ return wrap('暂无全局默认规则配置');
300
+ }
301
+ const lines = Object.entries(meta).map(([type, m]) => {
302
+ const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
303
+ const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
304
+ return `- **${m.label}** (${type}): ${trigger}`;
305
+ });
306
+ return wrap(`全局默认规则触发配置:\n\n${lines.join('\n')}`);
307
+ }
308
+ catch (e) {
309
+ return wrap(`❌ ${String(e)}`);
310
+ }
311
+ });
312
+ // 3.11 保存全局默认规则触发配置
313
+ server.tool('kg_save_global_rules_meta', '保存全局默认规则触发配置(新项目创建时继承此配置)。支持新增自定义规则类型', {
314
+ meta: z.record(z.string(), z.object({
315
+ label: z.string().describe('规则显示名称'),
316
+ keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
317
+ min_hits: z.number().default(1).describe('最低触发关键词数'),
318
+ always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
319
+ })).describe('规则类型 → 触发配置的映射')
320
+ }, async (args) => {
321
+ try {
322
+ const decoded = decodeObjectStrings(args.meta);
323
+ const success = await storage.saveGlobalRulesMeta(decoded);
324
+ if (!success) {
325
+ return wrap('❌ 保存失败');
326
+ }
327
+ return wrap(`✅ 全局默认规则触发配置已保存 (${Object.keys(decoded).length} 个类型)`);
328
+ }
329
+ catch (e) {
330
+ return wrap(`❌ ${String(e)}`);
331
+ }
232
332
  });
233
333
  // 5. 读取单个文档详情
234
334
  server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问', {
package/dist/utils.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * MCP Server 工具函数
3
3
  */
4
- export type RuleType = 'userStyles' | 'testRules' | 'reviewRules' | 'codeStyle' | 'unitTests';
5
- export declare const RULE_TYPE_LABELS: Record<RuleType, string>;
4
+ export type RuleType = string;
5
+ export declare const RULE_TYPE_LABELS: Record<string, string>;
6
6
  /**
7
- * 获取指定类型的规则 (从独立文件读取)
7
+ * 获取指定类型的规则 (动态: 从 meta 获取标签)
8
8
  */
9
- export declare function getRules(projectId: string, ruleType?: RuleType): Promise<string>;
9
+ export declare function getRules(projectId: string, ruleType?: string): Promise<string>;
10
10
  /**
11
11
  * 解码 Unicode 转义序列
12
12
  * 将 \uXXXX 格式的转义序列转换为实际字符
package/dist/utils.js CHANGED
@@ -2,14 +2,16 @@
2
2
  * MCP Server 工具函数
3
3
  */
4
4
  import * as storage from './storage/httpClient.js';
5
- // 规则类型显示名
6
- export const RULE_TYPE_LABELS = {
5
+ // 默认规则类型标签 (fallback)
6
+ const DEFAULT_LABELS = {
7
7
  userStyles: '用户沟通规则',
8
8
  codeStyle: '编码风格规则',
9
9
  reviewRules: '代码审查规则',
10
10
  testRules: '错误分析规则',
11
11
  unitTests: '代码测试规则',
12
12
  };
13
+ // 兼容导出
14
+ export const RULE_TYPE_LABELS = DEFAULT_LABELS;
13
15
  /**
14
16
  * 将字符串数组格式化为 Markdown 列表
15
17
  */
@@ -19,22 +21,25 @@ function formatRulesList(rules) {
19
21
  return rules.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
20
22
  }
21
23
  /**
22
- * 获取指定类型的规则 (从独立文件读取)
24
+ * 获取指定类型的规则 (动态: 从 meta 获取标签)
23
25
  */
24
26
  export async function getRules(projectId, ruleType) {
25
- // 如果指定了类型,只返回该类型
27
+ const meta = await storage.getRulesMeta();
26
28
  if (ruleType) {
27
29
  const rules = await storage.getRules(projectId, ruleType);
28
30
  if (!rules || rules.length === 0)
29
31
  return '';
30
- return `[${RULE_TYPE_LABELS[ruleType]}]\n${formatRulesList(rules)}`;
32
+ const label = meta[ruleType]?.label || DEFAULT_LABELS[ruleType] || ruleType;
33
+ return `[${label}]\n${formatRulesList(rules)}`;
31
34
  }
32
- // 返回所有规则
35
+ // 返回所有规则 (从 meta 获取所有类型)
36
+ const types = Object.keys(meta).length > 0 ? Object.keys(meta) : Object.keys(DEFAULT_LABELS);
33
37
  const allRules = [];
34
- for (const type of Object.keys(RULE_TYPE_LABELS)) {
38
+ for (const type of types) {
35
39
  const rules = await storage.getRules(projectId, type);
36
40
  if (rules && rules.length > 0) {
37
- allRules.push(`[${RULE_TYPE_LABELS[type]}]\n${formatRulesList(rules)}`);
41
+ const label = meta[type]?.label || DEFAULT_LABELS[type] || type;
42
+ allRules.push(`[${label}]\n${formatRulesList(rules)}`);
38
43
  }
39
44
  }
40
45
  return allRules.join('\n\n---\n\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,71 +1,46 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- Claude Code Hook - 关键词触发 + API 动态获取规则
4
+ Claude Code Hook - 动态规则触发
5
+ 从服务器获取触发配置 (rules-meta) → 关键词匹配 → 叠加获取规则
6
+ 兼容 Python 2.7+ / 3.x,支持 Windows / macOS / Linux
5
7
  """
6
8
 
9
+ from __future__ import print_function, unicode_literals
10
+
7
11
  import io
8
12
  import json
9
13
  import os
10
14
  import sys
11
- import urllib.request
12
15
 
13
- # 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")
16
+ # HTTP 库兼容
17
+ try:
18
+ from urllib.request import Request, urlopen
19
+ except ImportError:
20
+ from urllib2 import Request, urlopen
21
+
22
+ # Windows 编码修复 (仅 Windows + Python 3 + 有 buffer 属性时)
23
+ if sys.platform == "win32" and hasattr(sys.stdin, "buffer"):
24
+ try:
25
+ sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
26
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
27
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
28
+ except Exception:
29
+ pass
17
30
 
18
31
 
19
32
  # ╔══════════════════════════════════════════════════════════════╗
20
- # ║ 配置区
33
+ # ║ 绕过词
21
34
  # ╚══════════════════════════════════════════════════════════════╝
22
35
 
23
- # 关键词规则 (触发关键词 → 请求对应规则类型)
24
- RULES = [
25
- {
26
- "keywords": ["bug","修复","错误","报错","失败","异常","严重","存在","还是","有"],
27
- "min_hits": 2,
28
- "rule_type": "testRules", # 错误分析规则
29
- "label": "错误分析规则",
30
- },
31
- {
32
- "keywords": ["审查", "审核", "review", "检查"],
33
- "min_hits": 1,
34
- "rule_type": "reviewRules", # 审查规则
35
- "label": "代码审查规则",
36
- },
37
- {
38
- "keywords": ["编码", "风格", "格式", "命名", "编写", "开始", "代码"],
39
- "min_hits": 2,
40
- "rule_type": "codeStyle", # 编码风格
41
- "label": "编码风格规则",
42
- },
43
- {
44
- "keywords": ["开始", "进行", "准备", "测试", "单元", "用例", "test", "覆盖率"],
45
- "min_hits": 2,
46
- "rule_type": "unitTests", # 代码测试
47
- "label": "代码测试规则",
48
- },
49
- ]
50
-
51
- # 绕过词列表
52
36
  BYPASS = [
53
- "补充",
54
- "确定",
55
- "确认",
56
- "继续",
57
- "好的",
58
- "可以",
59
- "ok",
60
- "yes",
61
- "hi",
62
- "hello",
63
- "你好",
37
+ "补充", "确定", "确认", "继续", "好的", "可以",
38
+ "ok", "yes", "hi", "hello", "你好",
64
39
  ]
65
40
 
66
41
 
67
42
  # ╔══════════════════════════════════════════════════════════════╗
68
- # ║ API 请求函数
43
+ # ║ API 请求
69
44
  # ╚══════════════════════════════════════════════════════════════╝
70
45
 
71
46
 
@@ -75,25 +50,42 @@ def load_ppdocs_config():
75
50
  if not os.path.exists(config_path):
76
51
  return None
77
52
  try:
78
- with open(config_path, "r", encoding="utf-8") as f:
53
+ with io.open(config_path, "r", encoding="utf-8") as f:
79
54
  return json.load(f)
80
- except:
55
+ except Exception:
81
56
  return None
82
57
 
83
58
 
84
- def 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}"
59
+ def api_get(api_base, project_id, key, path):
60
+ """通用 GET 请求"""
61
+ url = "%s/api/%s/%s%s" % (api_base, project_id, key, path)
62
+ resp = None
87
63
  try:
88
- req = urllib.request.Request(url, method="GET")
64
+ req = Request(url)
89
65
  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:
66
+ resp = urlopen(req, timeout=3)
67
+ data = json.loads(resp.read().decode("utf-8"))
68
+ if data.get("success") and data.get("data") is not None:
69
+ return data["data"]
70
+ except Exception:
95
71
  pass
96
- return []
72
+ finally:
73
+ if resp is not None:
74
+ try:
75
+ resp.close()
76
+ except Exception:
77
+ pass
78
+ return None
79
+
80
+
81
+ def fetch_rules_meta(api_base, project_id, key):
82
+ """获取规则触发配置"""
83
+ return api_get(api_base, project_id, key, "/rules-meta") or {}
84
+
85
+
86
+ def fetch_rules(api_base, project_id, key, rule_type):
87
+ """获取指定类型的规则内容"""
88
+ return api_get(api_base, project_id, key, "/rules/" + rule_type) or []
97
89
 
98
90
 
99
91
  # ╔══════════════════════════════════════════════════════════════╗
@@ -101,27 +93,32 @@ def fetch_rules(api_base: str, project_id: str, key: str, rule_type: str) -> lis
101
93
  # ╚══════════════════════════════════════════════════════════════╝
102
94
 
103
95
 
104
- def count_hits(text: str, keywords: list) -> int:
96
+ def count_hits(text, keywords):
105
97
  """计算关键词命中数量"""
106
98
  return sum(1 for kw in keywords if kw.lower() in text)
107
99
 
108
100
 
109
- def 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
101
+ def match_all(text, meta):
102
+ """匹配所有触发的规则类型,返回 [(rule_type, label), ...]"""
103
+ matched = []
104
+ for rule_type, config in meta.items():
105
+ if config.get("always"):
106
+ matched.append((rule_type, config.get("label", rule_type)))
107
+ continue
108
+ keywords = config.get("keywords", [])
109
+ min_hits = config.get("min_hits", 1)
110
+ if keywords and count_hits(text, keywords) >= min_hits:
111
+ matched.append((rule_type, config.get("label", rule_type)))
112
+ return matched
116
113
 
117
114
 
118
- def format_rules(items: list, label: str) -> str:
115
+ def format_rules(items, label):
119
116
  """格式化规则输出"""
120
117
  if not items:
121
118
  return ""
122
- lines = [f"# {label}\n"]
119
+ lines = ["# %s\n" % label]
123
120
  for item in items:
124
- lines.append(f"- {item}")
121
+ lines.append("- %s" % item)
125
122
  return "\n".join(lines)
126
123
 
127
124
 
@@ -132,8 +129,9 @@ def format_rules(items: list, label: str) -> str:
132
129
 
133
130
  def main():
134
131
  try:
135
- data = json.load(sys.stdin)
136
- except:
132
+ raw = sys.stdin.read()
133
+ data = json.loads(raw)
134
+ except Exception:
137
135
  return
138
136
 
139
137
  if data.get("hook_event_name") != "UserPromptSubmit":
@@ -163,19 +161,22 @@ def main():
163
161
  if not project_id or not key:
164
162
  return
165
163
 
166
- output_parts = []
164
+ # 从服务器获取触发配置
165
+ meta = fetch_rules_meta(api_base, project_id, key)
166
+ if not meta:
167
+ return
167
168
 
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"]))
169
+ # 匹配所有触发的规则
170
+ matched = match_all(user_input_lower, meta)
171
+ if not matched:
172
+ return
173
+
174
+ # 叠加获取所有命中的规则内容
175
+ output_parts = []
176
+ for rule_type, label in matched:
177
+ rules = fetch_rules(api_base, project_id, key, rule_type)
178
+ if rules:
179
+ output_parts.append(format_rules(rules, label))
179
180
 
180
181
  # 输出
181
182
  if output_parts: