@ppdocs/mcp 2.6.19 → 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 +3 -5
- package/dist/storage/httpClient.d.ts +4 -0
- package/dist/storage/httpClient.js +28 -0
- package/dist/tools/index.js +72 -34
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +10 -33
- package/package.json +1 -1
- package/templates/hooks/hook.py +186 -0
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
|
-
|
|
238
|
-
const command =
|
|
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: [
|
|
@@ -52,6 +52,8 @@ export declare class PpdocsApiClient {
|
|
|
52
52
|
}, creator: string): Promise<Task>;
|
|
53
53
|
addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
|
|
54
54
|
completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
|
|
55
|
+
getRulesApi(ruleType: string): Promise<string[]>;
|
|
56
|
+
saveRulesApi(ruleType: string, rules: string[]): Promise<boolean>;
|
|
55
57
|
}
|
|
56
58
|
export declare function initClient(apiUrl: string): void;
|
|
57
59
|
export declare function listNodes(_projectId: string, filter?: ListNodesFilter): Promise<NodeData[]>;
|
|
@@ -92,3 +94,5 @@ export declare function createTask(_projectId: string, task: {
|
|
|
92
94
|
}, creator: string): Promise<Task>;
|
|
93
95
|
export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
|
|
94
96
|
export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
|
|
97
|
+
export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
|
|
98
|
+
export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
|
|
@@ -339,6 +339,27 @@ export class PpdocsApiClient {
|
|
|
339
339
|
return null;
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
+
// ============ 规则 API (独立文件存储) ============
|
|
343
|
+
async getRulesApi(ruleType) {
|
|
344
|
+
try {
|
|
345
|
+
return await this.request(`/rules/${ruleType}`);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async saveRulesApi(ruleType, rules) {
|
|
352
|
+
try {
|
|
353
|
+
await this.request(`/rules/${ruleType}`, {
|
|
354
|
+
method: 'PUT',
|
|
355
|
+
body: JSON.stringify(rules)
|
|
356
|
+
});
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
342
363
|
}
|
|
343
364
|
// ============ 模块级 API (兼容现有 tools/index.ts) ============
|
|
344
365
|
let client = null;
|
|
@@ -401,3 +422,10 @@ export async function addTaskLog(_projectId, taskId, logType, content) {
|
|
|
401
422
|
export async function completeTask(_projectId, taskId, experience) {
|
|
402
423
|
return getClient().completeTask(taskId, experience);
|
|
403
424
|
}
|
|
425
|
+
// ============ 规则管理 (独立文件存储) ============
|
|
426
|
+
export async function getRules(_projectId, ruleType) {
|
|
427
|
+
return getClient().getRulesApi(ruleType);
|
|
428
|
+
}
|
|
429
|
+
export async function saveRules(_projectId, ruleType, rules) {
|
|
430
|
+
return getClient().saveRulesApi(ruleType, rules);
|
|
431
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as storage from '../storage/httpClient.js';
|
|
3
|
-
import { decodeUnicodeEscapes, decodeObjectStrings, wrapResult,
|
|
3
|
+
import { decodeUnicodeEscapes, decodeObjectStrings, wrapResult, getRules, RULE_TYPE_LABELS } from '../utils.js';
|
|
4
4
|
// 辅助函数: 包装返回结果
|
|
5
5
|
async function wrap(projectId, text) {
|
|
6
6
|
const wrapped = await wrapResult(projectId, text);
|
|
@@ -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:
|
|
26
|
-
type:
|
|
26
|
+
title: decoded.title,
|
|
27
|
+
type: decoded.type,
|
|
27
28
|
status: 'incomplete',
|
|
28
|
-
description:
|
|
29
|
+
description: decoded.description || '',
|
|
29
30
|
// x, y 不传递,由 httpClient 智能计算位置
|
|
30
31
|
locked: false,
|
|
31
|
-
signature:
|
|
32
|
-
categories:
|
|
33
|
-
path:
|
|
34
|
-
dependencies:
|
|
35
|
-
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
|
|
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 方法更新');
|
|
@@ -90,38 +92,25 @@ export function registerTools(server, projectId, _user) {
|
|
|
90
92
|
const node = await storage.updateNode(projectId, nodeId, updates);
|
|
91
93
|
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(节点不存在或已锁定)');
|
|
92
94
|
});
|
|
93
|
-
// 3.5 更新根节点 (
|
|
94
|
-
server.tool('kg_update_root', '
|
|
95
|
+
// 3.5 更新根节点 (项目介绍)
|
|
96
|
+
server.tool('kg_update_root', '更新项目介绍(根节点描述,锁定时不可更新)', {
|
|
95
97
|
title: z.string().optional().describe('项目标题'),
|
|
96
|
-
description: z.string().optional().describe('
|
|
97
|
-
userStyles: z.array(z.string()).optional().describe('项目规则(字符串数组)'),
|
|
98
|
-
testRules: z.array(z.string()).optional().describe('测试规则(字符串数组)'),
|
|
99
|
-
reviewRules: z.array(z.string()).optional().describe('审查规则(字符串数组)'),
|
|
100
|
-
codeStyle: z.array(z.string()).optional().describe('编码风格(字符串数组)')
|
|
98
|
+
description: z.string().optional().describe('项目介绍(Markdown)')
|
|
101
99
|
}, async (args) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
args.reviewRules !== undefined || args.codeStyle !== undefined;
|
|
106
|
-
if (!hasUpdate) {
|
|
107
|
-
return wrap(projectId, '❌ 请至少提供一个更新参数');
|
|
100
|
+
const decoded = decodeObjectStrings(args);
|
|
101
|
+
if (decoded.title === undefined && decoded.description === undefined) {
|
|
102
|
+
return wrap(projectId, '❌ ���至少提供 title 或 description');
|
|
108
103
|
}
|
|
109
104
|
const node = await storage.updateRoot(projectId, {
|
|
110
|
-
title:
|
|
111
|
-
description:
|
|
112
|
-
userStyles: args.userStyles,
|
|
113
|
-
testRules: args.testRules,
|
|
114
|
-
reviewRules: args.reviewRules,
|
|
115
|
-
codeStyle: args.codeStyle
|
|
105
|
+
title: decoded.title,
|
|
106
|
+
description: decoded.description
|
|
116
107
|
});
|
|
117
|
-
|
|
118
|
-
clearStyleCache();
|
|
119
|
-
return wrap(projectId, node ? JSON.stringify(node, null, 2) : '更新失败(根节点已锁定)');
|
|
108
|
+
return wrap(projectId, node ? '✅ 项目介绍已更新' : '更新失败(根节点已锁定)');
|
|
120
109
|
});
|
|
121
110
|
// 3.6 获取项目规则 (独立方法,按需调用)
|
|
122
111
|
server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)', {
|
|
123
|
-
ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle']).optional()
|
|
124
|
-
.describe('规则类型: userStyles
|
|
112
|
+
ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests']).optional()
|
|
113
|
+
.describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部')
|
|
125
114
|
}, async (args) => {
|
|
126
115
|
const rules = await getRules(projectId, args.ruleType);
|
|
127
116
|
if (!rules || rules.trim() === '') {
|
|
@@ -130,6 +119,19 @@ export function registerTools(server, projectId, _user) {
|
|
|
130
119
|
}
|
|
131
120
|
return { content: [{ type: 'text', text: rules }] };
|
|
132
121
|
});
|
|
122
|
+
// 3.7 保存项目规则 (独立存储)
|
|
123
|
+
server.tool('kg_save_rules', '保存单个类型的项目规则(独立文件存储)', {
|
|
124
|
+
ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests'])
|
|
125
|
+
.describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则'),
|
|
126
|
+
rules: z.array(z.string()).describe('规则数组')
|
|
127
|
+
}, async (args) => {
|
|
128
|
+
const decoded = decodeObjectStrings(args);
|
|
129
|
+
const success = await storage.saveRules(projectId, decoded.ruleType, decoded.rules);
|
|
130
|
+
if (!success) {
|
|
131
|
+
return wrap(projectId, '❌ 保存失败');
|
|
132
|
+
}
|
|
133
|
+
return wrap(projectId, `✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
|
|
134
|
+
});
|
|
133
135
|
// 4. 锁定节点 (只能锁定,解锁需用户在前端手动操作)
|
|
134
136
|
server.tool('kg_lock_node', '锁定节点(锁定后只能读取,解锁需用户在前端手动操作)', {
|
|
135
137
|
nodeId: z.string().describe('节点ID')
|
|
@@ -425,4 +427,40 @@ export function registerTools(server, projectId, _user) {
|
|
|
425
427
|
}
|
|
426
428
|
return wrap(projectId, `任务已完成归档: ${task.title}`);
|
|
427
429
|
});
|
|
430
|
+
// 15. 查询文件关联的节点
|
|
431
|
+
server.tool('kg_find_by_file', '查询哪些节点绑定了指定文件(删除/重命名前检查)', {
|
|
432
|
+
filePath: z.string().describe('文件相对路径,如 src/utils/format.ts')
|
|
433
|
+
}, async (args) => {
|
|
434
|
+
const nodes = await storage.listNodes(projectId);
|
|
435
|
+
const filePath = args.filePath.replace(/\\/g, '/'); // 统一为正斜杠
|
|
436
|
+
// 查找所有绑定了该文件的节点
|
|
437
|
+
const boundBy = [];
|
|
438
|
+
for (const node of nodes) {
|
|
439
|
+
if (node.relatedFiles && node.relatedFiles.length > 0) {
|
|
440
|
+
// 检查是否包含该文件 (支持部分匹配)
|
|
441
|
+
const hasFile = node.relatedFiles.some(f => {
|
|
442
|
+
const normalizedF = f.replace(/\\/g, '/');
|
|
443
|
+
return normalizedF === filePath ||
|
|
444
|
+
normalizedF.endsWith('/' + filePath) ||
|
|
445
|
+
filePath.endsWith('/' + normalizedF);
|
|
446
|
+
});
|
|
447
|
+
if (hasFile) {
|
|
448
|
+
boundBy.push({
|
|
449
|
+
id: node.id,
|
|
450
|
+
title: node.title,
|
|
451
|
+
type: node.type,
|
|
452
|
+
status: node.status
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const result = {
|
|
458
|
+
file: filePath,
|
|
459
|
+
boundBy,
|
|
460
|
+
message: boundBy.length > 0
|
|
461
|
+
? `⚠️ 该文件被 ${boundBy.length} 个节点引用,删除前请更新节点`
|
|
462
|
+
: '✅ 该文件无 KG 引用,可安全操作'
|
|
463
|
+
};
|
|
464
|
+
return wrap(projectId, JSON.stringify(result, null, 2));
|
|
465
|
+
});
|
|
428
466
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
+
* 获取指定类型的规则 (从独立文件读取)
|
|
8
8
|
*/
|
|
9
9
|
export declare function getRules(projectId: string, ruleType?: RuleType): Promise<string>;
|
|
10
10
|
/**
|
|
@@ -12,7 +12,7 @@ export declare function getRules(projectId: string, ruleType?: RuleType): Promis
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function getRootStyle(projectId: string): Promise<string>;
|
|
14
14
|
/**
|
|
15
|
-
* 清除规则缓存 (
|
|
15
|
+
* 清除规则缓存 (保留接口兼容性,实际无需缓存)
|
|
16
16
|
*/
|
|
17
17
|
export declare function clearStyleCache(): void;
|
|
18
18
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
import * as storage from './storage/httpClient.js';
|
|
5
5
|
// 规则类型显示名
|
|
6
6
|
export const RULE_TYPE_LABELS = {
|
|
7
|
-
userStyles: '
|
|
8
|
-
|
|
9
|
-
reviewRules: '
|
|
10
|
-
|
|
7
|
+
userStyles: '用户沟通规则',
|
|
8
|
+
codeStyle: '编码风格规则',
|
|
9
|
+
reviewRules: '代码审查规则',
|
|
10
|
+
testRules: '错误分析规则',
|
|
11
|
+
unitTests: '代码测试规则',
|
|
11
12
|
};
|
|
12
|
-
// 缓存根节点 (避免每次调用都请求)
|
|
13
|
-
let cachedRootNode = null;
|
|
14
|
-
let cacheProjectId = null;
|
|
15
13
|
/**
|
|
16
14
|
* 将字符串数组格式化为 Markdown 列表
|
|
17
15
|
*/
|
|
@@ -21,32 +19,12 @@ function formatRulesList(rules) {
|
|
|
21
19
|
return rules.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
async function getCachedRoot(projectId) {
|
|
27
|
-
if (cachedRootNode !== null && cacheProjectId === projectId) {
|
|
28
|
-
return cachedRootNode;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
const rootNode = await storage.getNode(projectId, 'root');
|
|
32
|
-
cachedRootNode = rootNode;
|
|
33
|
-
cacheProjectId = projectId;
|
|
34
|
-
return cachedRootNode;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* 获取指定类型的规则
|
|
22
|
+
* 获取指定类型的规则 (从独立文件读取)
|
|
42
23
|
*/
|
|
43
24
|
export async function getRules(projectId, ruleType) {
|
|
44
|
-
const rootNode = await getCachedRoot(projectId);
|
|
45
|
-
if (!rootNode)
|
|
46
|
-
return '';
|
|
47
25
|
// 如果指定了类型,只返回该类型
|
|
48
26
|
if (ruleType) {
|
|
49
|
-
const rules =
|
|
27
|
+
const rules = await storage.getRules(projectId, ruleType);
|
|
50
28
|
if (!rules || rules.length === 0)
|
|
51
29
|
return '';
|
|
52
30
|
return `[${RULE_TYPE_LABELS[ruleType]}]\n${formatRulesList(rules)}`;
|
|
@@ -54,7 +32,7 @@ export async function getRules(projectId, ruleType) {
|
|
|
54
32
|
// 返回所有规则
|
|
55
33
|
const allRules = [];
|
|
56
34
|
for (const type of Object.keys(RULE_TYPE_LABELS)) {
|
|
57
|
-
const rules =
|
|
35
|
+
const rules = await storage.getRules(projectId, type);
|
|
58
36
|
if (rules && rules.length > 0) {
|
|
59
37
|
allRules.push(`[${RULE_TYPE_LABELS[type]}]\n${formatRulesList(rules)}`);
|
|
60
38
|
}
|
|
@@ -68,11 +46,10 @@ export async function getRootStyle(projectId) {
|
|
|
68
46
|
return getRules(projectId, 'userStyles');
|
|
69
47
|
}
|
|
70
48
|
/**
|
|
71
|
-
* 清除规则缓存 (
|
|
49
|
+
* 清除规则缓存 (保留接口兼容性,实际无需缓存)
|
|
72
50
|
*/
|
|
73
51
|
export function clearStyleCache() {
|
|
74
|
-
|
|
75
|
-
cacheProjectId = null;
|
|
52
|
+
// 独立文件存储无需缓存
|
|
76
53
|
}
|
|
77
54
|
/**
|
|
78
55
|
* 包装工具返回结果 (不再自动注入规则,使用 kg_get_rules 按需获取)
|
package/package.json
CHANGED
|
@@ -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()
|