@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.
- package/README.md +1 -1
- package/dist/cli.js +2 -1
- package/dist/index.js +1 -15
- package/dist/storage/httpClient.d.ts +24 -22
- package/dist/storage/httpClient.js +50 -188
- package/dist/storage/types.d.ts +42 -0
- package/dist/tools/analyzer.d.ts +1 -1
- package/dist/tools/analyzer.js +8 -7
- package/dist/tools/discussion.js +3 -7
- package/dist/tools/files.d.ts +2 -3
- package/dist/tools/files.js +4 -21
- package/dist/tools/flowchart.js +4 -1
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js +7 -5
- package/dist/tools/refs.d.ts +2 -0
- package/dist/tools/refs.js +123 -0
- package/dist/tools/tasks.js +2 -2
- package/dist/tools/workflow.d.ts +3 -0
- package/dist/tools/workflow.js +80 -0
- package/dist/utils.d.ts +0 -6
- package/dist/utils.js +0 -44
- package/package.json +1 -5
- package/templates/AGENT.md +3 -2
- package/templates/README.md +2 -2
- package/templates/commands/pp/diagnose.md +3 -3
- package/templates/commands/pp/review.md +9 -10
- package/templates/cursorrules.md +3 -2
- package/templates/hooks/SystemPrompt.md +6 -6
- package/templates/hooks/hook.py +52 -35
- package/templates/kiro-rules/ppdocs.md +3 -2
- package/dist/sync/beacon.d.ts +0 -26
- package/dist/sync/beacon.js +0 -186
- package/dist/tools/rules.d.ts +0 -7
- package/dist/tools/rules.js +0 -120
package/templates/hooks/hook.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
Claude Code Hook -
|
|
5
|
-
GET /
|
|
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
|
|
127
|
-
"""
|
|
128
|
-
return api_get(api_base, project_id, key, "/
|
|
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
|
|
132
|
-
"""
|
|
133
|
-
return api_post(api_base, project_id, key, "/
|
|
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,
|
|
147
|
-
"""
|
|
146
|
+
def match_all(text, workflows):
|
|
147
|
+
"""匹配所有触发的工作流,返回工作流摘要列表"""
|
|
148
148
|
matched = []
|
|
149
|
-
for
|
|
150
|
-
if
|
|
151
|
-
matched.append(
|
|
149
|
+
for wf in workflows:
|
|
150
|
+
if wf.get("always"):
|
|
151
|
+
matched.append(wf)
|
|
152
152
|
continue
|
|
153
|
-
keywords =
|
|
154
|
-
min_hits =
|
|
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(
|
|
156
|
+
matched.append(wf)
|
|
157
157
|
return matched
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
162
|
-
|
|
160
|
+
def format_workflow(item):
|
|
161
|
+
"""格式化工作流输出"""
|
|
162
|
+
content = item.get("content", "")
|
|
163
|
+
if not content:
|
|
163
164
|
return ""
|
|
164
|
-
lines = ["# %s
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
209
|
-
if not
|
|
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,
|
|
221
|
+
# 匹配所有触发的工作流
|
|
222
|
+
matched = match_all(user_input_lower, workflows)
|
|
214
223
|
if not matched:
|
|
215
224
|
return
|
|
216
225
|
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
- ??: `
|
|
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)` ????????? `
|
|
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`?
|
package/dist/sync/beacon.d.ts
DELETED
|
@@ -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
|
-
}
|
package/dist/sync/beacon.js
DELETED
|
@@ -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
|
-
}
|
package/dist/tools/rules.d.ts
DELETED
|
@@ -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;
|
package/dist/tools/rules.js
DELETED
|
@@ -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
|
-
}
|