@ppdocs/mcp 3.2.37 → 3.3.0

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.
@@ -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,26 +0,0 @@
1
- export declare class SyncBeacon {
2
- private cwd;
3
- private projectId;
4
- private debounceMs;
5
- private watcher;
6
- private isSyncing;
7
- private pendingSync;
8
- private debounceTimer;
9
- constructor(cwd: string, projectId: string, debounceMs?: number);
10
- /**
11
- * 启动同步引擎
12
- */
13
- start(): void;
14
- /**
15
- * 停止同步引擎
16
- */
17
- stop(): void;
18
- /**
19
- * 触发同步计算
20
- */
21
- private triggerSync;
22
- /**
23
- * 核心: 打包并推送全量快照
24
- */
25
- private performSync;
26
- }
@@ -1,186 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import archiver from 'archiver';
5
- import * as chokidar from 'chokidar';
6
- import { getClient } from '../storage/httpClient.js';
7
- // 需要排除的大文件/敏感目录
8
- const EXCLUDED_DIRS = [
9
- 'node_modules',
10
- '.git',
11
- '.next',
12
- 'dist',
13
- 'build',
14
- 'out',
15
- 'target',
16
- '__pycache__',
17
- '.venv',
18
- 'venv',
19
- '.idea',
20
- '.vscode',
21
- '.vs',
22
- '.ppdocs',
23
- '.cursor',
24
- '.claude',
25
- '.gemini',
26
- ];
27
- export class SyncBeacon {
28
- cwd;
29
- projectId;
30
- debounceMs;
31
- watcher = null;
32
- isSyncing = false;
33
- pendingSync = false;
34
- debounceTimer = null;
35
- constructor(cwd, projectId, debounceMs = 15000 // 默认 15 秒防抖
36
- ) {
37
- this.cwd = cwd;
38
- this.projectId = projectId;
39
- this.debounceMs = debounceMs;
40
- }
41
- /**
42
- * 启动同步引擎
43
- */
44
- start() {
45
- if (this.watcher)
46
- return;
47
- console.error(`[Code Beacon] Started monitoring project: ${this.projectId}`);
48
- // [M1 修复] 首次启动延迟 3 秒,让 MCP 工具注册和事件循环先稳定
49
- setTimeout(() => this.triggerSync(), 3000);
50
- // 配置监听器
51
- this.watcher = chokidar.watch(this.cwd, {
52
- ignored: (filePath) => {
53
- const basename = path.basename(filePath);
54
- if (EXCLUDED_DIRS.includes(basename))
55
- return true;
56
- if (basename.startsWith('.') && basename !== '.env.example' && !filePath.includes('.cursorrules') && !filePath.includes('ppdocs.md'))
57
- return true;
58
- return false;
59
- },
60
- persistent: true,
61
- ignoreInitial: true,
62
- });
63
- // 绑定事件
64
- const scheduleSync = (evt, p) => {
65
- if (this.debounceTimer) {
66
- clearTimeout(this.debounceTimer);
67
- }
68
- this.debounceTimer = setTimeout(() => {
69
- console.error(`[Code Beacon] Changes detected (${evt}: ${path.basename(p)}), scheduling sync...`);
70
- this.triggerSync();
71
- }, this.debounceMs);
72
- };
73
- this.watcher
74
- .on('add', (p) => scheduleSync('add', p))
75
- .on('change', (p) => scheduleSync('change', p))
76
- .on('unlink', (p) => scheduleSync('delete', p))
77
- .on('unlinkDir', (p) => scheduleSync('delete dir', p));
78
- }
79
- /**
80
- * 停止同步引擎
81
- */
82
- stop() {
83
- if (this.debounceTimer) {
84
- clearTimeout(this.debounceTimer);
85
- this.debounceTimer = null;
86
- }
87
- if (this.watcher) {
88
- this.watcher.close();
89
- this.watcher = null;
90
- console.error(`[Code Beacon] Stopped monitoring project: ${this.projectId}`);
91
- }
92
- }
93
- /**
94
- * 触发同步计算
95
- */
96
- async triggerSync() {
97
- if (this.isSyncing) {
98
- this.pendingSync = true;
99
- return;
100
- }
101
- this.isSyncing = true;
102
- this.pendingSync = false;
103
- try {
104
- await this.performSync();
105
- }
106
- catch (error) {
107
- console.error(`[Code Beacon] Sync error:`, error);
108
- }
109
- finally {
110
- this.isSyncing = false;
111
- if (this.pendingSync) {
112
- this.triggerSync();
113
- }
114
- }
115
- }
116
- /**
117
- * 核心: 打包并推送全量快照
118
- */
119
- async performSync() {
120
- // [C3 修复] getClient() 在未初始化时 throw,不会返回 null
121
- let client;
122
- try {
123
- client = getClient();
124
- }
125
- catch {
126
- console.warn('[Code Beacon] API Client not ready, skipping sync.');
127
- return;
128
- }
129
- return new Promise((resolve, reject) => {
130
- try {
131
- // 使用系统临时目录存放打包文件,避免 .ppdocs(是配置文件非目录)冲突
132
- const tmpFile = path.join(os.tmpdir(), `beacon-${this.projectId}-${Date.now()}.zip`);
133
- const output = fs.createWriteStream(tmpFile);
134
- const archive = archiver('zip', {
135
- zlib: { level: 3 } // 低压缩率,追求速度
136
- });
137
- // [C1 修复] 监听 WriteStream 的 error 事件,防止 Node 进程崩溃
138
- output.on('error', (err) => {
139
- console.error('[Code Beacon] WriteStream error:', err);
140
- reject(err);
141
- });
142
- output.on('close', async () => {
143
- try {
144
- const data = await fs.promises.readFile(tmpFile);
145
- // [C2 修复] 使用本项目标准上传接口,而非 crossUploadFiles
146
- await client.uploadRawZip(data);
147
- console.error(`[Code Beacon] Snapshot synced: ${archive.pointer()} bytes`);
148
- }
149
- catch (e) {
150
- console.error('[Code Beacon] Upload failed:', e);
151
- }
152
- finally {
153
- try {
154
- if (fs.existsSync(tmpFile))
155
- fs.promises.unlink(tmpFile);
156
- }
157
- catch { }
158
- resolve();
159
- }
160
- });
161
- archive.on('error', (err) => {
162
- reject(err);
163
- });
164
- archive.pipe(output);
165
- // 遍历所有非隐藏、非排除的文件打包
166
- archive.glob('**/*', {
167
- cwd: this.cwd,
168
- ignore: [
169
- ...EXCLUDED_DIRS.map(d => `${d}/**`),
170
- '.ppdocs',
171
- '.*'
172
- ],
173
- dot: false
174
- });
175
- // [A2 修复] 排除 .env(敏感凭证),仅包含安全的 IDE 配置
176
- if (fs.existsSync(path.join(this.cwd, '.cursorrules'))) {
177
- archive.file(path.join(this.cwd, '.cursorrules'), { name: '.cursorrules' });
178
- }
179
- archive.finalize();
180
- }
181
- catch (err) {
182
- reject(err);
183
- }
184
- });
185
- }
186
- }
@@ -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
- }