@ppdocs/mcp 3.1.10 → 3.2.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/dist/agent.d.ts +6 -0
- package/dist/agent.js +130 -0
- package/dist/cli.js +9 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -40
- package/dist/index.js +1 -1
- package/dist/storage/discussion.d.ts +33 -0
- package/dist/storage/discussion.js +116 -0
- package/dist/storage/httpClient.d.ts +8 -0
- package/dist/storage/httpClient.js +29 -0
- package/dist/tools/analyzer.d.ts +8 -0
- package/dist/tools/analyzer.js +136 -0
- package/dist/tools/discussion.d.ts +7 -0
- package/dist/tools/discussion.js +111 -0
- package/dist/tools/docs.d.ts +3 -3
- package/dist/tools/docs.js +205 -175
- package/dist/tools/files.d.ts +3 -3
- package/dist/tools/files.js +61 -56
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.js +18 -3
- package/dist/tools/kg_status.d.ts +5 -0
- package/dist/tools/kg_status.js +42 -0
- package/dist/tools/projects.d.ts +3 -2
- package/dist/tools/projects.js +4 -40
- package/dist/tools/rules.d.ts +3 -3
- package/dist/tools/rules.js +88 -110
- package/dist/tools/tasks.d.ts +2 -2
- package/dist/tools/tasks.js +96 -73
- package/dist/web/server.d.ts +43 -0
- package/dist/web/server.js +611 -0
- package/dist/web/ui.d.ts +5 -0
- package/dist/web/ui.js +474 -0
- package/package.json +6 -3
package/dist/agent.d.ts
ADDED
package/dist/agent.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PPDocs Agent V2 — 多项目管理入口
|
|
4
|
+
* 功能: Web Config UI + 多 SyncBeacon + 持久后台运行
|
|
5
|
+
*/
|
|
6
|
+
import { startWebServer, loadAgentConfig, setAgentState, setProjectStatus } from './web/server.js';
|
|
7
|
+
import { initClient } from './storage/httpClient.js';
|
|
8
|
+
import { SyncBeacon } from './sync/beacon.js';
|
|
9
|
+
const DEFAULT_WEB_PORT = 20010;
|
|
10
|
+
// 多项目 SyncBeacon 管理
|
|
11
|
+
const beacons = new Map();
|
|
12
|
+
async function main() {
|
|
13
|
+
console.log('📡 PPDocs Agent V2 starting...\n');
|
|
14
|
+
// 解析端口参数
|
|
15
|
+
let webPort = DEFAULT_WEB_PORT;
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
19
|
+
webPort = parseInt(args[i + 1], 10) || DEFAULT_WEB_PORT;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// 加载配置
|
|
23
|
+
const config = loadAgentConfig();
|
|
24
|
+
if (config) {
|
|
25
|
+
webPort = config.webPort || webPort;
|
|
26
|
+
console.log(`📋 主机: ${config.host}:${config.port}`);
|
|
27
|
+
console.log(`📂 已绑定 ${config.projects.length} 个项目`);
|
|
28
|
+
// 测试主机连接
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(`http://${config.host}:${config.port}/health`);
|
|
31
|
+
setAgentState({ hostConnected: resp.ok });
|
|
32
|
+
if (resp.ok)
|
|
33
|
+
console.log('✅ 主机连接正常');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
setAgentState({ hostConnected: false });
|
|
37
|
+
console.log('⚠️ 主机不可达,将在后台重试');
|
|
38
|
+
}
|
|
39
|
+
// 为每个项目启动同步
|
|
40
|
+
for (const proj of config.projects) {
|
|
41
|
+
await startProjectSync(config, proj);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log('⚙️ 首次运行,请在浏览器中完成配置');
|
|
46
|
+
}
|
|
47
|
+
// 注册回调
|
|
48
|
+
setAgentState({
|
|
49
|
+
onBind: async (project) => {
|
|
50
|
+
const cfg = loadAgentConfig();
|
|
51
|
+
if (cfg)
|
|
52
|
+
await startProjectSync(cfg, project);
|
|
53
|
+
},
|
|
54
|
+
onUnbind: (remoteId) => {
|
|
55
|
+
stopProjectSync(remoteId);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
// 启动 Web Server
|
|
59
|
+
startWebServer(webPort);
|
|
60
|
+
// 首次运行自动打开浏览器
|
|
61
|
+
if (!config) {
|
|
62
|
+
const url = `http://localhost:${webPort}`;
|
|
63
|
+
try {
|
|
64
|
+
const { exec } = await import('child_process');
|
|
65
|
+
const cmd = process.platform === 'win32' ? `start ${url}`
|
|
66
|
+
: process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`;
|
|
67
|
+
exec(cmd);
|
|
68
|
+
}
|
|
69
|
+
catch { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function startProjectSync(config, proj) {
|
|
73
|
+
const { host, port } = config;
|
|
74
|
+
const { remote, localDir, sync } = proj;
|
|
75
|
+
// 初始化该项目的 HTTP 连接 (验证)
|
|
76
|
+
const apiUrl = `http://${host}:${port}/api/${remote.id}/${remote.password}`;
|
|
77
|
+
try {
|
|
78
|
+
const resp = await fetch(`${apiUrl}/docs`);
|
|
79
|
+
if (resp.ok) {
|
|
80
|
+
const data = await resp.json();
|
|
81
|
+
const docCount = data.data?.length || 0;
|
|
82
|
+
setProjectStatus(remote.id, { connected: true, syncStatus: '已连接', docCount });
|
|
83
|
+
console.log(` ✅ ${remote.name}: 已连接 (${docCount} 文档)`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setProjectStatus(remote.id, { connected: false, syncStatus: '连接失败' });
|
|
87
|
+
console.log(` ❌ ${remote.name}: 连接失败`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
setProjectStatus(remote.id, { connected: false, syncStatus: `错误: ${e}` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// 启动 SyncBeacon
|
|
96
|
+
if (localDir && sync?.enabled) {
|
|
97
|
+
stopProjectSync(remote.id); // 先停旧的
|
|
98
|
+
// 需要临时给 httpClient 设置对应项目的 API URL
|
|
99
|
+
initClient(apiUrl);
|
|
100
|
+
const beacon = new SyncBeacon(localDir, remote.id, (sync.intervalSec || 15) * 1000);
|
|
101
|
+
beacon.start();
|
|
102
|
+
beacons.set(remote.id, beacon);
|
|
103
|
+
setProjectStatus(remote.id, { syncStatus: '同步中', lastSync: new Date() });
|
|
104
|
+
console.log(` 📂 ${remote.name}: 文件同步已启动`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function stopProjectSync(remoteId) {
|
|
108
|
+
const beacon = beacons.get(remoteId);
|
|
109
|
+
if (beacon) {
|
|
110
|
+
beacon.stop();
|
|
111
|
+
beacons.delete(remoteId);
|
|
112
|
+
console.log(` 🛑 停止同步: ${remoteId}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 优雅退出
|
|
116
|
+
process.on('SIGINT', () => {
|
|
117
|
+
console.log('\n🛑 Agent 正在关闭...');
|
|
118
|
+
for (const [, beacon] of beacons)
|
|
119
|
+
beacon.stop();
|
|
120
|
+
process.exit(0);
|
|
121
|
+
});
|
|
122
|
+
process.on('SIGTERM', () => {
|
|
123
|
+
for (const [, beacon] of beacons)
|
|
124
|
+
beacon.stop();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
main().catch(e => {
|
|
128
|
+
console.error('❌ Agent 启动失败:', e);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -90,6 +90,7 @@ ppdocs MCP CLI
|
|
|
90
90
|
|
|
91
91
|
Commands:
|
|
92
92
|
init Initialize ppdocs config + install workflow templates
|
|
93
|
+
agent Start PPDocs Agent (Web UI + File Sync)
|
|
93
94
|
|
|
94
95
|
Usage:
|
|
95
96
|
npx @ppdocs/mcp init -p <projectId> -k <key> [options]
|
|
@@ -126,6 +127,14 @@ export function runCli(args) {
|
|
|
126
127
|
});
|
|
127
128
|
return true;
|
|
128
129
|
}
|
|
130
|
+
if (cmd === 'agent') {
|
|
131
|
+
// 动态导入 Agent 入口
|
|
132
|
+
import('./agent.js').catch(e => {
|
|
133
|
+
console.error(`❌ Agent failed: ${e}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
129
138
|
if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
130
139
|
showHelp();
|
|
131
140
|
return true;
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -48,38 +48,6 @@ async function findLocalServer() {
|
|
|
48
48
|
}
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
|
-
// ============ 自动发现 ============
|
|
52
|
-
async function autoDiscoverConfig(server) {
|
|
53
|
-
const cwd = process.cwd().replace(/\\/g, '/').toLowerCase();
|
|
54
|
-
try {
|
|
55
|
-
const res = await fetch(`${server.base}/api/projects`, { signal: AbortSignal.timeout(2000) });
|
|
56
|
-
if (!res.ok)
|
|
57
|
-
return null;
|
|
58
|
-
const { data: projects } = await res.json();
|
|
59
|
-
if (!Array.isArray(projects))
|
|
60
|
-
return null;
|
|
61
|
-
const match = projects.find((p) => p.projectPath && cwd.startsWith(p.projectPath.replace(/\\/g, '/').toLowerCase()));
|
|
62
|
-
if (!match)
|
|
63
|
-
return null;
|
|
64
|
-
const metaRes = await fetch(`${server.base}/api/projects/${match.id}/meta`, { signal: AbortSignal.timeout(2000) });
|
|
65
|
-
if (!metaRes.ok)
|
|
66
|
-
return null;
|
|
67
|
-
const { data: meta } = await metaRes.json();
|
|
68
|
-
if (!meta?.password)
|
|
69
|
-
return null;
|
|
70
|
-
console.error(`[AutoDiscovery] 匹配项目: ${match.name} (${match.id})`);
|
|
71
|
-
return {
|
|
72
|
-
apiUrl: `${server.base}/api/${match.id}/${meta.password}`,
|
|
73
|
-
projectId: match.id,
|
|
74
|
-
user: `auto-${generateUser().slice(0, 4)}`,
|
|
75
|
-
source: 'discover',
|
|
76
|
-
connection: { host: server.host, port: server.port, password: meta.password },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
51
|
// ============ 授权请求 ============
|
|
84
52
|
async function requestAuthConfig(server) {
|
|
85
53
|
try {
|
|
@@ -154,19 +122,15 @@ export async function loadConfig() {
|
|
|
154
122
|
const fileConfig = readPpdocsFile();
|
|
155
123
|
if (fileConfig)
|
|
156
124
|
return fileConfig;
|
|
157
|
-
// 3
|
|
125
|
+
// 3. 扫描本地服务器 → 请求桌面端授权
|
|
158
126
|
const server = await findLocalServer();
|
|
159
127
|
if (server) {
|
|
160
|
-
const discoverConfig = await autoDiscoverConfig(server);
|
|
161
|
-
if (discoverConfig)
|
|
162
|
-
return discoverConfig;
|
|
163
128
|
const authConfig = await requestAuthConfig(server);
|
|
164
129
|
if (authConfig)
|
|
165
130
|
return authConfig;
|
|
166
131
|
}
|
|
167
|
-
//
|
|
168
|
-
console.error('ERROR: ppdocs
|
|
169
|
-
console.error('
|
|
170
|
-
console.error(' Option 2: Ensure ppdocs desktop app is running');
|
|
132
|
+
// 全部失败
|
|
133
|
+
console.error('ERROR: ppdocs 配置未找到');
|
|
134
|
+
console.error(' 请确保 ppdocs 桌面端正在运行,或手动配置 PPDOCS_API_URL 环境变量');
|
|
171
135
|
process.exit(1);
|
|
172
136
|
}
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ if (args.length > 0 && runCli(args)) {
|
|
|
28
28
|
async function main() {
|
|
29
29
|
const config = await loadConfig();
|
|
30
30
|
// 自动持久化: 发现/授权后写入 .ppdocs + 安装模板
|
|
31
|
-
if (
|
|
31
|
+
if (config.source === 'auth' && config.connection) {
|
|
32
32
|
try {
|
|
33
33
|
writePpdocsFile(config);
|
|
34
34
|
setupProjectFiles(process.cwd(), config.apiUrl);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface DiscussionMessage {
|
|
2
|
+
id: string;
|
|
3
|
+
sender: string;
|
|
4
|
+
content: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DiscussionTopic {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
initiator: string;
|
|
11
|
+
participants: string[];
|
|
12
|
+
summary: string;
|
|
13
|
+
status: 'active' | 'completed';
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
messages: DiscussionMessage[];
|
|
17
|
+
}
|
|
18
|
+
export declare class DiscussionManager {
|
|
19
|
+
private static getFilePath;
|
|
20
|
+
private static readAll;
|
|
21
|
+
private static writeAll;
|
|
22
|
+
static listActive(): Omit<DiscussionTopic, 'messages'>[];
|
|
23
|
+
static readByIds(ids: string[]): DiscussionTopic[];
|
|
24
|
+
static create(title: string, initiator: string, participants: string[], content: string): string;
|
|
25
|
+
static reply(id: string, sender: string, content: string, newSummary?: string): boolean;
|
|
26
|
+
static getAndRemove(id: string): DiscussionTopic | null;
|
|
27
|
+
/** 按ID删除讨论(不归档,直接删除) */
|
|
28
|
+
static delete(id: string): boolean;
|
|
29
|
+
/** 清理过期讨论(默认7天不活跃) */
|
|
30
|
+
static cleanExpired(days?: number): number;
|
|
31
|
+
/** 获取活跃讨论数量 */
|
|
32
|
+
static activeCount(): number;
|
|
33
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
export class DiscussionManager {
|
|
5
|
+
static getFilePath() {
|
|
6
|
+
const dir = path.join(os.homedir(), '.ppdocs');
|
|
7
|
+
if (!fs.existsSync(dir))
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
return path.join(dir, 'discussions.json');
|
|
10
|
+
}
|
|
11
|
+
static readAll() {
|
|
12
|
+
try {
|
|
13
|
+
const file = this.getFilePath();
|
|
14
|
+
if (!fs.existsSync(file))
|
|
15
|
+
return [];
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static writeAll(data) {
|
|
23
|
+
fs.writeFileSync(this.getFilePath(), JSON.stringify(data, null, 2), 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
static listActive() {
|
|
26
|
+
return this.readAll()
|
|
27
|
+
.filter(t => t.status === 'active')
|
|
28
|
+
.map(({ messages, ...rest }) => rest);
|
|
29
|
+
}
|
|
30
|
+
static readByIds(ids) {
|
|
31
|
+
return this.readAll().filter(t => ids.includes(t.id));
|
|
32
|
+
}
|
|
33
|
+
static create(title, initiator, participants, content) {
|
|
34
|
+
const id = `req_${Math.random().toString(36).substring(2, 9)}`;
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
// 确保发起方始终在参与列表中
|
|
37
|
+
const allParticipants = participants.includes(initiator) ? [...participants] : [initiator, ...participants];
|
|
38
|
+
const topic = {
|
|
39
|
+
id,
|
|
40
|
+
title,
|
|
41
|
+
initiator,
|
|
42
|
+
participants: allParticipants,
|
|
43
|
+
summary: "等待各方回复中...",
|
|
44
|
+
status: 'active',
|
|
45
|
+
created_at: now,
|
|
46
|
+
updated_at: now,
|
|
47
|
+
messages: [{
|
|
48
|
+
id: `msg_${Math.random().toString(36).substring(2, 9)}`,
|
|
49
|
+
sender: initiator,
|
|
50
|
+
content,
|
|
51
|
+
timestamp: now
|
|
52
|
+
}]
|
|
53
|
+
};
|
|
54
|
+
const all = this.readAll();
|
|
55
|
+
all.push(topic);
|
|
56
|
+
this.writeAll(all);
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
static reply(id, sender, content, newSummary) {
|
|
60
|
+
const all = this.readAll();
|
|
61
|
+
const topic = all.find(t => t.id === id);
|
|
62
|
+
if (!topic || topic.status !== 'active')
|
|
63
|
+
return false;
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
topic.messages.push({
|
|
66
|
+
id: `msg_${Math.random().toString(36).substring(2, 9)}`,
|
|
67
|
+
sender,
|
|
68
|
+
content,
|
|
69
|
+
timestamp: now
|
|
70
|
+
});
|
|
71
|
+
topic.updated_at = now;
|
|
72
|
+
if (newSummary) {
|
|
73
|
+
topic.summary = newSummary;
|
|
74
|
+
}
|
|
75
|
+
// Auto add to participants if not exists
|
|
76
|
+
if (!topic.participants.includes(sender)) {
|
|
77
|
+
topic.participants.push(sender);
|
|
78
|
+
}
|
|
79
|
+
this.writeAll(all);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
static getAndRemove(id) {
|
|
83
|
+
const all = this.readAll();
|
|
84
|
+
const index = all.findIndex(t => t.id === id);
|
|
85
|
+
if (index === -1)
|
|
86
|
+
return null;
|
|
87
|
+
const topic = all.splice(index, 1)[0];
|
|
88
|
+
this.writeAll(all);
|
|
89
|
+
return topic;
|
|
90
|
+
}
|
|
91
|
+
/** 按ID删除讨论(不归档,直接删除) */
|
|
92
|
+
static delete(id) {
|
|
93
|
+
const all = this.readAll();
|
|
94
|
+
const index = all.findIndex(t => t.id === id);
|
|
95
|
+
if (index === -1)
|
|
96
|
+
return false;
|
|
97
|
+
all.splice(index, 1);
|
|
98
|
+
this.writeAll(all);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** 清理过期讨论(默认7天不活跃) */
|
|
102
|
+
static cleanExpired(days = 7) {
|
|
103
|
+
const all = this.readAll();
|
|
104
|
+
const cutoff = Date.now() - days * 86400000;
|
|
105
|
+
const before = all.length;
|
|
106
|
+
const filtered = all.filter(t => new Date(t.updated_at).getTime() > cutoff);
|
|
107
|
+
if (filtered.length < before) {
|
|
108
|
+
this.writeAll(filtered);
|
|
109
|
+
}
|
|
110
|
+
return before - filtered.length;
|
|
111
|
+
}
|
|
112
|
+
/** 获取活跃讨论数量 */
|
|
113
|
+
static activeCount() {
|
|
114
|
+
return this.readAll().filter(t => t.status === 'active').length;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -96,6 +96,14 @@ export declare class PpdocsApiClient {
|
|
|
96
96
|
localPath: string;
|
|
97
97
|
fileCount: number;
|
|
98
98
|
}>;
|
|
99
|
+
/** 扫描项目代码, 构建/更新索引 */
|
|
100
|
+
analyzerScan(projectPath: string, force?: boolean): Promise<unknown>;
|
|
101
|
+
/** 查询代码符号 (模糊匹配) */
|
|
102
|
+
analyzerQuery(projectPath: string, query: string): Promise<unknown>;
|
|
103
|
+
/** 分层影响分析 (BFS 递归, 可指定深度) */
|
|
104
|
+
analyzerImpactTree(projectPath: string, symbolName: string, depth?: number): Promise<unknown>;
|
|
105
|
+
/** 获取文件 360° 上下文 */
|
|
106
|
+
analyzerContext(projectPath: string, filePath: string): Promise<unknown>;
|
|
99
107
|
}
|
|
100
108
|
export declare function initClient(apiUrl: string): void;
|
|
101
109
|
export declare function getClient(): PpdocsApiClient;
|
|
@@ -480,6 +480,35 @@ export class PpdocsApiClient {
|
|
|
480
480
|
async crossDownload(target, remotePath, localPath) {
|
|
481
481
|
return fetchAndExtractZip(`${this.baseUrl}/cross/${encodeURIComponent(target)}/files-download/${cleanPath(remotePath)}`, localPath);
|
|
482
482
|
}
|
|
483
|
+
// ============ 代码分析引擎 ============
|
|
484
|
+
/** 扫描项目代码, 构建/更新索引 */
|
|
485
|
+
async analyzerScan(projectPath, force = false) {
|
|
486
|
+
return this.request('/analyzer/scan', {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
body: JSON.stringify({ project_path: projectPath, force }),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/** 查询代码符号 (模糊匹配) */
|
|
492
|
+
async analyzerQuery(projectPath, query) {
|
|
493
|
+
return this.request('/analyzer/query', {
|
|
494
|
+
method: 'POST',
|
|
495
|
+
body: JSON.stringify({ project_path: projectPath, query }),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/** 分层影响分析 (BFS 递归, 可指定深度) */
|
|
499
|
+
async analyzerImpactTree(projectPath, symbolName, depth = 2) {
|
|
500
|
+
return this.request('/analyzer/impact-tree', {
|
|
501
|
+
method: 'POST',
|
|
502
|
+
body: JSON.stringify({ project_path: projectPath, symbol_name: symbolName, depth }),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/** 获取文件 360° 上下文 */
|
|
506
|
+
async analyzerContext(projectPath, filePath) {
|
|
507
|
+
return this.request('/analyzer/context', {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
body: JSON.stringify({ project_path: projectPath, file_path: filePath }),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
483
512
|
}
|
|
484
513
|
// ============ 模块级 API ============
|
|
485
514
|
let client = null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码分析引擎工具 (4个)
|
|
3
|
+
* code_scan, code_query, code_impact, code_context
|
|
4
|
+
*
|
|
5
|
+
* 让 AI Agent 通过 MCP 工具直接查询代码结构、依赖关系和影响范围
|
|
6
|
+
*/
|
|
7
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
export declare function registerAnalyzerTools(server: McpServer, projectId: string): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码分析引擎工具 (4个)
|
|
3
|
+
* code_scan, code_query, code_impact, code_context
|
|
4
|
+
*
|
|
5
|
+
* 让 AI Agent 通过 MCP 工具直接查询代码结构、依赖关系和影响范围
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { getClient } from '../storage/httpClient.js';
|
|
9
|
+
import { wrap, safeTool } from './shared.js';
|
|
10
|
+
// 符号图标
|
|
11
|
+
const SYMBOL_ICONS = {
|
|
12
|
+
function: '𝑓', class: '𝐂', method: '𝑚', interface: '𝐈',
|
|
13
|
+
type_alias: '𝐓', enum: '𝐄', struct: '𝐒', trait: '⚡',
|
|
14
|
+
constant: '𝐊', variable: '𝑣',
|
|
15
|
+
};
|
|
16
|
+
const SEVERITY_ICONS = {
|
|
17
|
+
critical: '🔴', warning: '🟡', info: '🟢',
|
|
18
|
+
};
|
|
19
|
+
export function registerAnalyzerTools(server, projectId) {
|
|
20
|
+
const client = () => getClient();
|
|
21
|
+
// ===== code_scan: 扫描项目代码 =====
|
|
22
|
+
server.tool('code_scan', '📡 扫描项目代码, 构建索引。返回文件数、符号数、语言统计。★首次使用 code_query/code_impact/code_context 前必须先执行★', {
|
|
23
|
+
projectPath: z.string().describe('项目源码的绝对路径(如"D:/projects/myapp")'),
|
|
24
|
+
force: z.boolean().optional().describe('是否强制全量重建(默认false, 增量更新)'),
|
|
25
|
+
}, async (args) => safeTool(async () => {
|
|
26
|
+
const result = await client().analyzerScan(args.projectPath, args.force ?? false);
|
|
27
|
+
return wrap([
|
|
28
|
+
`✅ 代码扫描完成`,
|
|
29
|
+
``,
|
|
30
|
+
`📊 索引摘要:`,
|
|
31
|
+
`- 文件数: ${result.fileCount}`,
|
|
32
|
+
`- 符号数: ${result.symbolCount}`,
|
|
33
|
+
`- 语言: ${result.languages.join(', ')}`,
|
|
34
|
+
`- 索引时间: ${result.indexedAt}`,
|
|
35
|
+
].join('\n'));
|
|
36
|
+
}));
|
|
37
|
+
// ===== code_query: 搜索代码符号 =====
|
|
38
|
+
server.tool('code_query', '🔤 搜索代码符号(函数/类/方法/接口/类型)。返回匹配列表+文件路径+行号。定位代码位置的最快方式。需先运行 code_scan', {
|
|
39
|
+
projectPath: z.string().describe('项目源码的绝对路径'),
|
|
40
|
+
query: z.string().describe('搜索关键词(函数名/类名/方法名等)'),
|
|
41
|
+
}, async (args) => safeTool(async () => {
|
|
42
|
+
const results = await client().analyzerQuery(args.projectPath, args.query);
|
|
43
|
+
if (!results || results.length === 0) {
|
|
44
|
+
return wrap(`未找到匹配 "${args.query}" 的符号。请确认已运行 code_scan`);
|
|
45
|
+
}
|
|
46
|
+
const lines = results.map(r => {
|
|
47
|
+
const icon = SYMBOL_ICONS[r.symbol.kind] || '?';
|
|
48
|
+
const exp = r.symbol.exported ? ' [export]' : '';
|
|
49
|
+
const parent = r.symbol.parent ? ` ◀ ${r.symbol.parent}` : '';
|
|
50
|
+
return ` ${icon} ${r.symbol.name}${parent}${exp} → ${r.filePath}:${r.symbol.lineStart}`;
|
|
51
|
+
});
|
|
52
|
+
return wrap([
|
|
53
|
+
`🔤 找到 ${results.length} 个匹配符号:`,
|
|
54
|
+
``,
|
|
55
|
+
...lines,
|
|
56
|
+
].join('\n'));
|
|
57
|
+
}));
|
|
58
|
+
// ===== code_impact: 分层影响分析 =====
|
|
59
|
+
server.tool('code_impact', '💥 爆炸半径分析 ★修改代码前必查★ 分析修改一个函数/类/类型会影响多少文件。BFS分层输出: L1🔴直接引用=必须检查, L2🟡间接引用=建议检查, L3🟢传递引用=注意。修改任何公共接口、函数签名、类型定义前务必先运行!', {
|
|
60
|
+
projectPath: z.string().describe('项目源码的绝对路径'),
|
|
61
|
+
symbolName: z.string().describe('要分析的符号名称(如"AuthService", "handleLogin")'),
|
|
62
|
+
depth: z.number().optional().describe('分析深度层级(1-5, 默认2)。1=仅直接引用, 3=深度追踪'),
|
|
63
|
+
}, async (args) => safeTool(async () => {
|
|
64
|
+
const result = await client().analyzerImpactTree(args.projectPath, args.symbolName, args.depth ?? 2);
|
|
65
|
+
if (!result) {
|
|
66
|
+
return wrap(`未找到符号 "${args.symbolName}"。请确认名称正确且已运行 code_scan`);
|
|
67
|
+
}
|
|
68
|
+
if (result.levels.length === 0) {
|
|
69
|
+
return wrap(`✅ "${args.symbolName}" 没有被其他文件引用, 修改安全`);
|
|
70
|
+
}
|
|
71
|
+
// 生成分层树形输出
|
|
72
|
+
const lines = [
|
|
73
|
+
`🎯 ${result.symbolName} (${result.symbolKind})`,
|
|
74
|
+
`📍 定义: ${result.definedIn}:${result.lineStart}`,
|
|
75
|
+
``,
|
|
76
|
+
`📊 ${result.summary}`,
|
|
77
|
+
``,
|
|
78
|
+
];
|
|
79
|
+
for (const level of result.levels) {
|
|
80
|
+
const icon = SEVERITY_ICONS[level.severity] || '⚪';
|
|
81
|
+
const label = level.depth === 1 ? '直接引用 (必须检查)' :
|
|
82
|
+
level.depth === 2 ? '间接引用 (建议检查)' :
|
|
83
|
+
'传递引用 (注意)';
|
|
84
|
+
lines.push(`### ${icon} L${level.depth} ${label} (${level.count}个)`);
|
|
85
|
+
for (const entry of level.entries) {
|
|
86
|
+
lines.push(` ${entry.filePath}:${entry.line} [${entry.refKind}] ${entry.context}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
return wrap(lines.join('\n'));
|
|
91
|
+
}));
|
|
92
|
+
// ===== code_context: 文件360°上下文 =====
|
|
93
|
+
server.tool('code_context', '🔍 文件360°上下文 — 定义了什么符号、导入了什么、被谁引用。修改文件前使用, 快速了解所有依赖关系, 避免遗漏', {
|
|
94
|
+
projectPath: z.string().describe('项目源码的绝对路径'),
|
|
95
|
+
filePath: z.string().describe('目标文件的相对路径(如"src/services/auth.ts")'),
|
|
96
|
+
}, async (args) => safeTool(async () => {
|
|
97
|
+
const ctx = await client().analyzerContext(args.projectPath, args.filePath);
|
|
98
|
+
if (!ctx) {
|
|
99
|
+
return wrap(`未找到文件 "${args.filePath}"。请确认路径正确, 路径格式为相对路径(如 src/main.ts)`);
|
|
100
|
+
}
|
|
101
|
+
const lines = [
|
|
102
|
+
`📄 ${ctx.filePath}`,
|
|
103
|
+
`语言: ${ctx.language} | ${ctx.linesTotal} 行`,
|
|
104
|
+
``,
|
|
105
|
+
];
|
|
106
|
+
// 定义的符号
|
|
107
|
+
if (ctx.symbolsDefined.length > 0) {
|
|
108
|
+
lines.push(`### 🔤 定义的符号 (${ctx.symbolsDefined.length})`);
|
|
109
|
+
for (const sym of ctx.symbolsDefined) {
|
|
110
|
+
const icon = SYMBOL_ICONS[sym.kind] || '?';
|
|
111
|
+
const exp = sym.exported ? ' [export]' : '';
|
|
112
|
+
lines.push(` ${icon} ${sym.name}${exp} L${sym.lineStart}-L${sym.lineEnd}`);
|
|
113
|
+
}
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
// 导入
|
|
117
|
+
if (ctx.imports.length > 0) {
|
|
118
|
+
lines.push(`### 📥 导入 (${ctx.imports.length})`);
|
|
119
|
+
for (const imp of ctx.imports) {
|
|
120
|
+
const specs = imp.specifiers.length > 0 ? `{ ${imp.specifiers.join(', ')} }` : '*';
|
|
121
|
+
lines.push(` → ${imp.source} ${specs} L${imp.line}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push('');
|
|
124
|
+
}
|
|
125
|
+
// 被谁引用
|
|
126
|
+
if (ctx.importedBy.length > 0) {
|
|
127
|
+
lines.push(`### 📤 被引用 (${ctx.importedBy.length})`);
|
|
128
|
+
for (const by of ctx.importedBy) {
|
|
129
|
+
const specs = by.specifiers.length > 0 ? `{ ${by.specifiers.join(', ')} }` : '';
|
|
130
|
+
lines.push(` ← ${by.filePath} ${specs}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push('');
|
|
133
|
+
}
|
|
134
|
+
return wrap(lines.join('\n'));
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 💬 kg_discuss (6→1)
|
|
3
|
+
* 合并: kg_discussion_list, kg_discussion_read, kg_discussion_create,
|
|
4
|
+
* kg_discussion_reply, kg_discussion_close_and_archive, kg_discussion_delete
|
|
5
|
+
*/
|
|
6
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
export declare function registerDiscussionTools(server: McpServer, projectId: string): void;
|