@ppdocs/mcp 3.2.34 → 3.2.36

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,808 +0,0 @@
1
- /**
2
- * PPDocs Agent V2 — Web Config Server
3
- * 多项目管理 + 智能匹配 + 知识图谱预览 + MCP 安装
4
- */
5
- import express from 'express';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
- import { getAgentHtml } from './ui.js';
10
- // ============ 配置管理 ============
11
- const CONFIG_DIR = path.join(os.homedir(), '.ppdocs-agent');
12
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
- export function loadAgentConfig() {
14
- try {
15
- if (!fs.existsSync(CONFIG_FILE))
16
- return null;
17
- const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
18
- // V1 → V2 自动迁移
19
- if (raw.project && !raw.projects) {
20
- raw.projects = [{
21
- localDir: raw.localDir || '',
22
- localName: path.basename(raw.localDir || ''),
23
- remote: raw.project,
24
- sync: raw.sync || { enabled: true, intervalSec: 15 },
25
- createdAt: new Date().toISOString(),
26
- }];
27
- delete raw.project;
28
- delete raw.localDir;
29
- delete raw.sync;
30
- saveAgentConfig(raw);
31
- }
32
- return raw;
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- export function saveAgentConfig(config) {
39
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
40
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
41
- }
42
- /** 写入 .ppdocs 到项目目录,供 MCP 直接读取 */
43
- function writePpdocsToProject(binding, host, port) {
44
- if (!binding.localDir || !binding.remote.password)
45
- return;
46
- const configPath = path.join(binding.localDir, '.ppdocs');
47
- const content = {
48
- api: `http://${host}:${port}`,
49
- projectId: binding.remote.id,
50
- key: binding.remote.password,
51
- user: `agent-${binding.localName.slice(0, 8)}`,
52
- };
53
- try {
54
- fs.writeFileSync(configPath, JSON.stringify(content, null, 2), 'utf-8');
55
- console.log(`[Config] 已写入 ${configPath}`);
56
- }
57
- catch (e) {
58
- console.error(`[Config] 写入 .ppdocs 失败: ${e}`);
59
- }
60
- }
61
- const state = {
62
- hostConnected: false,
63
- projectStatus: new Map(),
64
- };
65
- export function getAgentState() { return state; }
66
- export function setAgentState(partial) {
67
- Object.assign(state, partial);
68
- }
69
- export function setProjectStatus(remoteId, status) {
70
- const existing = state.projectStatus.get(remoteId) || {
71
- connected: false, syncStatus: '未启动', lastSync: null,
72
- };
73
- state.projectStatus.set(remoteId, { ...existing, ...status });
74
- }
75
- // ============ Express 服务器 ============
76
- export function startWebServer(webPort) {
77
- const app = express();
78
- app.use(express.json());
79
- app.disable('etag'); // 禁用 ETag,避免 304 缓存导致前端数据不更新
80
- // GET / → UI (禁止浏览器缓存,确保加载最新版本)
81
- app.get('/', (_req, res) => {
82
- res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
83
- res.set('Pragma', 'no-cache');
84
- res.type('html').send(getAgentHtml());
85
- });
86
- // GET /api/status → 全局+各项目状态
87
- app.get('/api/status', (_req, res) => {
88
- const config = loadAgentConfig();
89
- const projects = (config?.projects || []).map(p => {
90
- const ps = state.projectStatus.get(p.remote.id);
91
- return {
92
- localName: p.localName,
93
- localDir: p.localDir,
94
- remoteId: p.remote.id,
95
- remoteName: p.remote.name,
96
- connected: ps?.connected || false,
97
- syncStatus: ps?.syncStatus || (p.localDir ? '待启动' : '未指定目录'),
98
- docCount: ps?.docCount || 0,
99
- lastSync: ps?.lastSync ? timeSince(ps.lastSync) : null,
100
- hasPassword: !!p.remote.password,
101
- };
102
- });
103
- res.json({
104
- hostConnected: state.hostConnected,
105
- host: config?.host || '',
106
- port: config?.port || 20001,
107
- projects,
108
- });
109
- });
110
- // POST /api/host → 保存主机配置
111
- app.post('/api/host', async (req, res) => {
112
- const { host, port } = req.body;
113
- if (!host) {
114
- res.json({ ok: false, error: '缺少 host' });
115
- return;
116
- }
117
- const config = loadAgentConfig() || { host: '', port: 20001, webPort: webPort, projects: [] };
118
- config.host = host;
119
- config.port = port || 20001;
120
- saveAgentConfig(config);
121
- res.json({ ok: true });
122
- });
123
- // POST /api/test → 测试远程连接
124
- app.post('/api/test', async (req, res) => {
125
- const { host, port } = req.body;
126
- try {
127
- const controller = new AbortController();
128
- const timeout = setTimeout(() => controller.abort(), 5000);
129
- const resp = await fetch(`http://${host}:${port}/health`, { signal: controller.signal });
130
- clearTimeout(timeout);
131
- state.hostConnected = resp.ok;
132
- res.json({ ok: resp.ok });
133
- }
134
- catch {
135
- state.hostConnected = false;
136
- res.json({ ok: false, error: '连接超时或主机不可达' });
137
- }
138
- });
139
- // GET /api/projects → 远程项目列表 (保留用于兼容)
140
- app.get('/api/projects', async (req, res) => {
141
- const config = loadAgentConfig();
142
- const host = req.query.host || config?.host;
143
- const port = req.query.port || String(config?.port || 20001);
144
- if (!host) {
145
- res.json({ projects: [] });
146
- return;
147
- }
148
- try {
149
- const resp = await fetch(`http://${host}:${port}/api/projects`);
150
- if (!resp.ok) {
151
- res.json({ projects: [] });
152
- return;
153
- }
154
- const data = await resp.json();
155
- res.json({ projects: data.data || [] });
156
- }
157
- catch {
158
- res.json({ projects: [] });
159
- }
160
- });
161
- // GET /api/browse → 服务端目录浏览 (供前端目录选择器使用)
162
- app.get('/api/browse', (req, res) => {
163
- const dir = req.query.dir || '';
164
- try {
165
- // 无参数时返回根级入口
166
- if (!dir) {
167
- const home = os.homedir();
168
- // 常用快捷入口
169
- const shortcuts = [];
170
- const desktop = path.join(home, 'Desktop');
171
- if (fs.existsSync(desktop))
172
- shortcuts.push({ name: '🖥️ 桌面 (Desktop)', path: desktop });
173
- shortcuts.push({ name: '🏠 用户目录', path: home });
174
- if (process.platform === 'darwin') {
175
- const dev = path.join(home, 'Developer');
176
- const docs = path.join(home, 'Documents');
177
- if (fs.existsSync(dev))
178
- shortcuts.push({ name: '💻 Developer', path: dev });
179
- if (fs.existsSync(docs))
180
- shortcuts.push({ name: '📄 Documents', path: docs });
181
- }
182
- if (process.platform === 'win32') {
183
- // Windows: 列出盘符
184
- const drives = [];
185
- for (let i = 65; i <= 90; i++) {
186
- const d = String.fromCharCode(i) + ':\\';
187
- try {
188
- fs.accessSync(d);
189
- drives.push(d);
190
- }
191
- catch { /* skip */ }
192
- }
193
- res.json({ ok: true, dir: '', dirs: drives, shortcuts, platform: 'win32' });
194
- }
195
- else {
196
- // Mac/Linux: 列出 home 下的子目录
197
- try {
198
- const homeDirs = fs.readdirSync(home, { withFileTypes: true })
199
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
200
- .slice(0, 20)
201
- .map(e => path.join(home, e.name));
202
- res.json({ ok: true, dir: home, dirs: homeDirs, shortcuts, platform: process.platform });
203
- }
204
- catch {
205
- res.json({ ok: true, dir: '', dirs: [home], shortcuts, platform: process.platform });
206
- }
207
- }
208
- return;
209
- }
210
- // 列出指定目录的子目录
211
- if (!fs.existsSync(dir)) {
212
- res.json({ ok: false, error: '目录不存在' });
213
- return;
214
- }
215
- const entries = fs.readdirSync(dir, { withFileTypes: true })
216
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
217
- .sort((a, b) => a.name.localeCompare(b.name))
218
- .slice(0, 100)
219
- .map(e => path.join(dir, e.name));
220
- res.json({ ok: true, dir, dirs: entries, platform: process.platform });
221
- }
222
- catch (e) {
223
- res.json({ ok: false, error: String(e).slice(0, 100) });
224
- }
225
- });
226
- // GET /api/pick-dir → 调用系统原生目录选择器
227
- app.get('/api/pick-dir', async (_req, res) => {
228
- try {
229
- let cmd;
230
- if (process.platform === 'win32') {
231
- // Windows: PowerShell FolderBrowserDialog
232
- cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $d=New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description='选择项目目录'; $d.ShowNewFolderButton=$true; if($d.ShowDialog() -eq 'OK'){$d.SelectedPath}else{''}"`;
233
- }
234
- else if (process.platform === 'darwin') {
235
- // Mac: osascript choose folder
236
- cmd = `osascript -e 'try' -e 'POSIX path of (choose folder with prompt "选择项目目录")' -e 'on error' -e '""' -e 'end try'`;
237
- }
238
- else {
239
- // Linux: zenity
240
- cmd = `zenity --file-selection --directory --title="选择项目目录" 2>/dev/null || echo ""`;
241
- }
242
- const { execSync } = await import('child_process');
243
- const result = execSync(cmd, { encoding: 'utf-8', timeout: 120000 }).trim();
244
- if (result) {
245
- res.json({ ok: true, dir: result });
246
- }
247
- else {
248
- res.json({ ok: false, cancelled: true });
249
- }
250
- }
251
- catch {
252
- res.json({ ok: false, error: '目录选择器调用失败' });
253
- }
254
- });
255
- // ============ 服务端授权流程 ============
256
- // POST /api/auth/start → 发起授权请求 (代理转发到主机)
257
- app.post('/api/auth/start', async (req, res) => {
258
- const { localDir } = req.body;
259
- // localDir 可选,用于 APP 端显示来源信息
260
- const config = loadAgentConfig();
261
- if (!config?.host) {
262
- res.json({ ok: false, error: '请先配置主机' });
263
- return;
264
- }
265
- try {
266
- const resp = await fetch(`http://${config.host}:${config.port}/api/auth/request`, {
267
- method: 'POST',
268
- headers: { 'Content-Type': 'application/json' },
269
- body: JSON.stringify({
270
- cwd: localDir,
271
- hostname: os.hostname(),
272
- }),
273
- });
274
- if (!resp.ok) {
275
- res.json({ ok: false, error: '主机 auth API 不可用' });
276
- return;
277
- }
278
- const data = await resp.json();
279
- const requestId = data.data?.requestId;
280
- if (!requestId) {
281
- res.json({ ok: false, error: '未获得 requestId' });
282
- return;
283
- }
284
- res.json({ ok: true, requestId });
285
- }
286
- catch (e) {
287
- res.json({ ok: false, error: `连接主机失败: ${String(e).slice(0, 80)}` });
288
- }
289
- });
290
- // GET /api/auth/status/:id → 轮询授权结果 (代理转发)
291
- app.get('/api/auth/status/:id', async (req, res) => {
292
- const config = loadAgentConfig();
293
- if (!config?.host) {
294
- res.json({ status: 'error', error: '未配置主机' });
295
- return;
296
- }
297
- try {
298
- const resp = await fetch(`http://${config.host}:${config.port}/api/auth/poll/${req.params.id}`);
299
- if (!resp.ok) {
300
- res.json({ status: 'error' });
301
- return;
302
- }
303
- const data = await resp.json();
304
- const d = data.data;
305
- if (d?.status === 'approved' && d.result) {
306
- // 授权通过,自动绑定
307
- const result = d.result;
308
- res.json({
309
- status: 'approved',
310
- projectId: result.project_id,
311
- projectName: result.project_name,
312
- password: result.password,
313
- });
314
- }
315
- else {
316
- res.json({ status: d?.status || 'pending' });
317
- }
318
- }
319
- catch {
320
- res.json({ status: 'error' });
321
- }
322
- });
323
- // POST /api/bind → 绑定项目 (授权通过后调用)
324
- app.post('/api/bind', async (req, res) => {
325
- const { localDir, remoteId, remoteName, password } = req.body;
326
- if (!remoteId) {
327
- res.json({ ok: false, error: '缺少 remoteId' });
328
- return;
329
- }
330
- const config = loadAgentConfig();
331
- if (!config) {
332
- res.json({ ok: false, error: '请先配置主机' });
333
- return;
334
- }
335
- // 检查重复
336
- if (config.projects.some(p => p.remote.id === remoteId)) {
337
- res.json({ ok: false, error: '该远程项目已绑定' });
338
- return;
339
- }
340
- const binding = {
341
- localDir: localDir || '',
342
- localName: localDir ? path.basename(localDir) : (remoteName || remoteId),
343
- remote: { id: remoteId, name: remoteName || remoteId, password: password || '' },
344
- sync: { enabled: true, intervalSec: 15 },
345
- createdAt: new Date().toISOString(),
346
- };
347
- config.projects.push(binding);
348
- saveAgentConfig(config);
349
- // 有本地目录时才写 .ppdocs
350
- if (localDir)
351
- writePpdocsToProject(binding, config.host, config.port);
352
- // 通知 Agent 主进程启动同步
353
- if (state.onBind)
354
- state.onBind(binding);
355
- res.json({ ok: true, binding });
356
- });
357
- // DELETE /api/bind/:id → 解绑
358
- app.delete('/api/bind/:id', (req, res) => {
359
- const remoteId = req.params.id;
360
- const config = loadAgentConfig();
361
- if (!config) {
362
- res.json({ ok: false });
363
- return;
364
- }
365
- config.projects = config.projects.filter(p => p.remote.id !== remoteId);
366
- saveAgentConfig(config);
367
- state.projectStatus.delete(remoteId);
368
- if (state.onUnbind)
369
- state.onUnbind(remoteId);
370
- res.json({ ok: true });
371
- });
372
- // PATCH /api/bind/:id → 更新绑定(用于重新授权后更新密码)
373
- app.patch('/api/bind/:id', (req, res) => {
374
- const remoteId = req.params.id;
375
- const { password, remoteName } = req.body;
376
- const config = loadAgentConfig();
377
- if (!config) {
378
- res.json({ ok: false });
379
- return;
380
- }
381
- const proj = config.projects.find(p => p.remote.id === remoteId);
382
- if (!proj) {
383
- res.json({ ok: false, error: '项目不存在' });
384
- return;
385
- }
386
- if (password)
387
- proj.remote.password = password;
388
- if (remoteName)
389
- proj.remote.name = remoteName;
390
- saveAgentConfig(config);
391
- res.json({ ok: true });
392
- });
393
- // GET /api/bind/:id/tree → 项目知识图谱 (无密码时尝试跨项目只读)
394
- app.get('/api/bind/:id/tree', async (req, res) => {
395
- const remoteId = req.params.id;
396
- const config = loadAgentConfig();
397
- const proj = config?.projects.find(p => p.remote.id === remoteId);
398
- if (!config || !proj) {
399
- res.json({ tree: '项目不存在' });
400
- return;
401
- }
402
- let docsUrl;
403
- let readOnly = false;
404
- if (proj.remote.password) {
405
- // 有密码 → 直接读取(读写模式)
406
- docsUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}/docs`;
407
- }
408
- else {
409
- // 无密码 → 尝试用其他已授权项目的跨项目只读API
410
- const proxy = config.projects.find(p => p.remote.id !== remoteId && p.remote.password);
411
- if (proxy) {
412
- docsUrl = `http://${config.host}:${config.port}/api/${proxy.remote.id}/${proxy.remote.password}/cross/${remoteId}/docs`;
413
- readOnly = true;
414
- }
415
- else {
416
- // 无任何已授权项目 → 尝试公开API
417
- docsUrl = `http://${config.host}:${config.port}/api/projects/${remoteId}/docs`;
418
- readOnly = true;
419
- }
420
- }
421
- try {
422
- const resp = await fetch(docsUrl);
423
- if (!resp.ok) {
424
- res.json({ tree: readOnly ? '⚠️ 未授权,无法获取文档 — 请点击"申请授权"' : `获取失败 (HTTP ${resp.status})`, readOnly });
425
- return;
426
- }
427
- const json = await resp.json();
428
- const docs = json.data || [];
429
- if (docs.length === 0) {
430
- res.json({ tree: '知识库为空', docs: [], docCount: 0, dirCount: 0, readOnly });
431
- return;
432
- }
433
- const sortedDocs = docs.sort((a, b) => a.path.localeCompare(b.path));
434
- const lines = sortedDocs
435
- .map(d => {
436
- const depth = (d.path.match(/\//g) || []).length;
437
- const indent = ' '.repeat(Math.max(0, depth - 1));
438
- const icon = d.isDir ? '📁' : '📄';
439
- const summary = d.summary ? ` — ${d.summary}` : '';
440
- return `${indent}${icon} ${d.name}${summary}`;
441
- });
442
- const docCount = sortedDocs.filter(d => !d.isDir).length;
443
- const dirCount = sortedDocs.filter(d => d.isDir).length;
444
- res.json({ tree: lines.join('\n'), docs: sortedDocs, docCount, dirCount, readOnly });
445
- }
446
- catch (e) {
447
- res.json({ tree: `连接失败: ${String(e).slice(0, 100)}` });
448
- }
449
- });
450
- // GET /api/bind/:id/doc → 读取单篇文档内容
451
- app.get('/api/bind/:id/doc', async (req, res) => {
452
- const remoteId = req.params.id;
453
- const docPath = req.query.path;
454
- if (!docPath) {
455
- res.json({ ok: false, error: '缺少 path 参数' });
456
- return;
457
- }
458
- const config = loadAgentConfig();
459
- const proj = config?.projects.find(p => p.remote.id === remoteId);
460
- if (!config || !proj) {
461
- res.json({ ok: false, error: '项目不存在' });
462
- return;
463
- }
464
- let docUrl;
465
- if (proj.remote.password) {
466
- docUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}/docs/${encodeURIComponent(docPath)}`;
467
- }
468
- else {
469
- const proxy = config.projects.find(p => p.remote.id !== remoteId && p.remote.password);
470
- if (proxy) {
471
- docUrl = `http://${config.host}:${config.port}/api/${proxy.remote.id}/${proxy.remote.password}/cross/${remoteId}/docs/${encodeURIComponent(docPath)}`;
472
- }
473
- else {
474
- res.json({ ok: false, error: '未授权' });
475
- return;
476
- }
477
- }
478
- try {
479
- const resp = await fetch(docUrl);
480
- if (!resp.ok) {
481
- res.json({ ok: false, error: `HTTP ${resp.status}` });
482
- return;
483
- }
484
- const json = await resp.json();
485
- const d = json.data;
486
- res.json({ ok: true, content: d?.content || '', summary: d?.summary || '', status: d?.status || '' });
487
- }
488
- catch (e) {
489
- res.json({ ok: false, error: String(e).slice(0, 100) });
490
- }
491
- });
492
- // GET /api/bind/:id/prompts → 读取项目提示词文件
493
- app.get('/api/bind/:id/prompts', (req, res) => {
494
- const config = loadAgentConfig();
495
- const proj = config?.projects.find(p => p.remote.id === req.params.id);
496
- if (!proj?.localDir) {
497
- res.json({ files: [] });
498
- return;
499
- }
500
- const dir = proj.localDir;
501
- const names = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', 'CONVENTIONS.md'];
502
- const files = names.map(name => {
503
- const fp = path.join(dir, name);
504
- const exists = fs.existsSync(fp);
505
- let lines = 0;
506
- if (exists) {
507
- try {
508
- lines = fs.readFileSync(fp, 'utf-8').split('\n').length;
509
- }
510
- catch { }
511
- }
512
- return { name, exists, lines };
513
- });
514
- res.json({ files });
515
- });
516
- // GET /api/bind/:id/prompts/:name → 读取单个提示词文件
517
- app.get('/api/bind/:id/prompts/:name', (req, res) => {
518
- const config = loadAgentConfig();
519
- const proj = config?.projects.find(p => p.remote.id === req.params.id);
520
- if (!proj?.localDir) {
521
- res.json({ ok: false });
522
- return;
523
- }
524
- const fp = path.join(proj.localDir, req.params.name);
525
- if (!fs.existsSync(fp)) {
526
- res.json({ ok: false, error: '文件不存在' });
527
- return;
528
- }
529
- try {
530
- res.json({ ok: true, content: fs.readFileSync(fp, 'utf-8') });
531
- }
532
- catch (e) {
533
- res.json({ ok: false, error: String(e) });
534
- }
535
- });
536
- // PUT /api/bind/:id/prompts/:name → 保存提示词文件
537
- app.put('/api/bind/:id/prompts/:name', (req, res) => {
538
- const config = loadAgentConfig();
539
- const proj = config?.projects.find(p => p.remote.id === req.params.id);
540
- if (!proj?.localDir) {
541
- res.json({ ok: false });
542
- return;
543
- }
544
- const allowed = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', 'CONVENTIONS.md'];
545
- if (!allowed.includes(req.params.name)) {
546
- res.json({ ok: false, error: '不允许的文件名' });
547
- return;
548
- }
549
- try {
550
- fs.writeFileSync(path.join(proj.localDir, req.params.name), req.body.content || '', 'utf-8');
551
- res.json({ ok: true });
552
- }
553
- catch (e) {
554
- res.json({ ok: false, error: String(e) });
555
- }
556
- });
557
- // GET /api/bind/:id/mcp/all → 扫描项目目录下各平台的完整 MCP 服务列表
558
- app.get('/api/bind/:id/mcp/all', (req, res) => {
559
- const config = loadAgentConfig();
560
- const proj = config?.projects.find(p => p.remote.id === req.params.id);
561
- if (!proj?.localDir) {
562
- res.json({ servers: [] });
563
- return;
564
- }
565
- const dir = proj.localDir;
566
- const allServers = {}; // serverName → [platforms...]
567
- const scanTargets = [
568
- { platform: 'Cursor', filePath: path.join(dir, '.cursor', 'mcp.json') },
569
- { platform: 'Antigravity', filePath: path.join(dir, '.gemini', 'settings.json') },
570
- { platform: 'OpenCode', filePath: path.join(dir, '.vscode', 'cline_mcp_settings.json') },
571
- ];
572
- for (const t of scanTargets) {
573
- try {
574
- if (!fs.existsSync(t.filePath))
575
- continue;
576
- const content = JSON.parse(fs.readFileSync(t.filePath, 'utf-8'));
577
- const servers = content.mcpServers || content.servers || {};
578
- for (const name of Object.keys(servers)) {
579
- if (!allServers[name])
580
- allServers[name] = [];
581
- allServers[name].push(t.platform);
582
- }
583
- }
584
- catch { }
585
- }
586
- const servers = Object.entries(allServers).map(([name, platforms]) => ({ name, platforms }));
587
- res.json({ servers });
588
- });
589
- // 获取各平台 MCP 配置的路径和服务器名称
590
- const getMcpSpecs = (dir, projId) => ({
591
- cursor: { path: path.join(dir, '.cursor', 'mcp.json'), name: 'ppdocs-kg' },
592
- antigravity: { path: path.join(dir, '.gemini', 'settings.json'), name: `ppdocs-kg-${projId}` },
593
- vscode: { path: path.join(dir, '.vscode', 'cline_mcp_settings.json'), name: 'ppdocs-kg' },
594
- claude: { path: getClaudeGlobalPath(), name: `ppdocs-kg-${projId}` },
595
- lobechat: { path: '', name: 'ppdocs-kg' }, // 无本地文件,靠复制配置
596
- });
597
- // GET /api/bind/:id/mcp → MCP 安装状态检测
598
- app.get('/api/bind/:id/mcp', (req, res) => {
599
- const remoteId = req.params.id;
600
- const config = loadAgentConfig();
601
- const proj = config?.projects.find(p => p.remote.id === remoteId);
602
- if (!proj?.localDir) {
603
- res.json({ platforms: {} });
604
- return;
605
- }
606
- const specs = getMcpSpecs(proj.localDir, remoteId);
607
- const platforms = {
608
- cursor: hasMcpInJson(specs.cursor.path, specs.cursor.name),
609
- antigravity: hasMcpInJson(specs.antigravity.path, specs.antigravity.name),
610
- vscode: hasMcpInJson(specs.vscode.path, specs.vscode.name),
611
- claude: hasMcpInJson(specs.claude.path, specs.claude.name),
612
- lobechat: false, // 无法自动检测
613
- };
614
- res.json({ platforms });
615
- });
616
- // POST /api/bind/:id/mcp → 安装/获取 MCP 配置
617
- app.post('/api/bind/:id/mcp', (req, res) => {
618
- const remoteId = req.params.id;
619
- const { platform } = req.body;
620
- const config = loadAgentConfig();
621
- const proj = config?.projects.find(p => p.remote.id === remoteId);
622
- if (!config || !proj) {
623
- res.json({ ok: false, error: '项目不存在' });
624
- return;
625
- }
626
- if (!proj.localDir) {
627
- res.json({ ok: false, error: '请先指定本地目录' });
628
- return;
629
- }
630
- const apiUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}`;
631
- const dir = proj.localDir;
632
- const isWin = process.platform === 'win32';
633
- const ppdocsServer = buildMcpServerConfig(apiUrl, isWin);
634
- // 对于 LobeChat,直接返回 JSON 供用户复制,不写入本地
635
- if (platform === 'lobechat') {
636
- res.json({
637
- ok: true,
638
- message: 'LobeChat 配置生成成功',
639
- action: 'copy',
640
- config: JSON.stringify({ mcpServers: { 'ppdocs-kg': ppdocsServer } }, null, 2),
641
- });
642
- return;
643
- }
644
- try {
645
- const specs = getMcpSpecs(dir, remoteId);
646
- const spec = specs[platform];
647
- if (!spec || !spec.path) {
648
- res.json({ ok: false, error: '未知平台或不支持自动安装' });
649
- return;
650
- }
651
- writeMcpConfig(spec.path, spec.name, ppdocsServer);
652
- // 同时写入 .ppdocs 配置文件
653
- const ppdocsPath = path.join(dir, '.ppdocs');
654
- fs.writeFileSync(ppdocsPath, JSON.stringify({
655
- api: `http://${config.host}:${config.port}`,
656
- projectId: proj.remote.id,
657
- key: proj.remote.password,
658
- user: `Agent@${os.hostname()}`,
659
- }, null, 2));
660
- let extra = '';
661
- if (platform === 'antigravity') {
662
- const cmdCount = installAntigravitySlashCommands();
663
- if (cmdCount > 0)
664
- extra = ` + ${cmdCount}个斜杠命令`;
665
- }
666
- res.json({ ok: true, message: `✅ ${platform} MCP 已安装${extra}` });
667
- }
668
- catch (e) {
669
- res.json({ ok: false, error: String(e) });
670
- }
671
- });
672
- // DELETE /api/bind/:id/mcp → 卸载 MCP
673
- app.delete('/api/bind/:id/mcp', (req, res) => {
674
- const remoteId = req.params.id;
675
- const { platform } = req.body;
676
- const config = loadAgentConfig();
677
- const proj = config?.projects.find(p => p.remote.id === remoteId);
678
- if (!proj?.localDir) {
679
- res.json({ ok: false });
680
- return;
681
- }
682
- if (platform === 'lobechat') {
683
- res.json({ ok: true, message: '请在 LobeChat 界面中手动删除' });
684
- return;
685
- }
686
- try {
687
- const specs = getMcpSpecs(proj.localDir, remoteId);
688
- const spec = specs[platform];
689
- if (spec && fs.existsSync(spec.path)) {
690
- removeMcpFromConfig(spec.path, spec.name);
691
- }
692
- res.json({ ok: true, message: `✅ ${platform} MCP 已卸载` });
693
- }
694
- catch (e) {
695
- res.json({ ok: false, error: String(e) });
696
- }
697
- });
698
- app.listen(webPort, () => {
699
- console.log(`\n📡 PPDocs Agent Web UI: http://localhost:${webPort}\n`);
700
- });
701
- }
702
- function timeSince(date) {
703
- const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
704
- if (seconds < 60)
705
- return `${seconds}秒前`;
706
- const minutes = Math.floor(seconds / 60);
707
- if (minutes < 60)
708
- return `${minutes}分钟前`;
709
- return `${Math.floor(minutes / 60)}小时前`;
710
- }
711
- /** 检测 JSON 配置文件中是否有指名的 MCP */
712
- function hasMcpInJson(filePath, serverName) {
713
- try {
714
- if (!fs.existsSync(filePath))
715
- return false;
716
- const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
717
- const servers = content.mcpServers || content.servers || {};
718
- return serverName in servers;
719
- }
720
- catch {
721
- return false;
722
- }
723
- }
724
- /** 构建 MCP server 配置对象 (通过 PPDOCS_API_URL 环境变量隔离项目) */
725
- function buildMcpServerConfig(apiUrl, isWindows) {
726
- return isWindows ? {
727
- command: 'cmd',
728
- args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
729
- env: {
730
- PPDOCS_API_URL: apiUrl,
731
- },
732
- } : {
733
- command: 'npx',
734
- args: ['-y', '@ppdocs/mcp@latest'],
735
- env: {
736
- PATH: `${process.env.PATH || '/usr/bin:/bin'}:/usr/local/bin:/opt/homebrew/bin`,
737
- PPDOCS_API_URL: apiUrl,
738
- },
739
- };
740
- }
741
- /** 写入 MCP 配置到 JSON 文件 (合并已有配置) */
742
- function writeMcpConfig(filePath, serverName, serverConfig) {
743
- let existing = {};
744
- if (fs.existsSync(filePath)) {
745
- try {
746
- existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
747
- }
748
- catch { /* ignore */ }
749
- }
750
- existing.mcpServers = {
751
- ...(existing.mcpServers || {}),
752
- [serverName]: serverConfig,
753
- };
754
- const dir = path.dirname(filePath);
755
- if (!fs.existsSync(dir))
756
- fs.mkdirSync(dir, { recursive: true });
757
- fs.writeFileSync(filePath, JSON.stringify(existing, null, 2));
758
- }
759
- /** 从 JSON 配置中移除指定的 MCP */
760
- function removeMcpFromConfig(filePath, serverName) {
761
- try {
762
- const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
763
- const servers = content.mcpServers || {};
764
- delete servers[serverName];
765
- content.mcpServers = servers;
766
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
767
- }
768
- catch { /* ignore */ }
769
- }
770
- /** 安装 Antigravity 斜杠命令到全局目录 ~/.gemini/antigravity/global_workflows/ */
771
- function installAntigravitySlashCommands() {
772
- const templatesDir = path.join(__dirname, '..', 'templates', 'commands', 'pp');
773
- if (!fs.existsSync(templatesDir))
774
- return 0;
775
- const globalDir = path.join(os.homedir(), '.gemini', 'antigravity', 'global_workflows');
776
- fs.mkdirSync(globalDir, { recursive: true });
777
- const COMMAND_MAP = {
778
- 'init.md': { name: 'pp-init', desc: '初始化项目知识图谱' },
779
- 'sync.md': { name: 'pp-sync', desc: '同步代码与知识图谱' },
780
- 'review.md': { name: 'pp-shencha', desc: '审查任务成果' },
781
- 'DiagnosticProtocol.md': { name: 'pp-fenxi', desc: '深度分析错误' },
782
- 'Execution_Task.md': { name: 'pp-task', desc: '执行开发任务' },
783
- 'Sentinel_4.md': { name: 'pp-audit', desc: '多Agent代码审计' },
784
- 'SynchronizationProtocol.md': { name: 'pp-syncpro', desc: '知识图谱同步协议' },
785
- 'Zero_Defec_Genesis.md': { name: 'plan', desc: '零缺陷创生协议' },
786
- };
787
- let count = 0;
788
- for (const [file, mapping] of Object.entries(COMMAND_MAP)) {
789
- const dest = path.join(globalDir, `${mapping.name}.md`);
790
- if (fs.existsSync(dest))
791
- continue;
792
- const src = path.join(templatesDir, file);
793
- if (!fs.existsSync(src))
794
- continue;
795
- const content = fs.readFileSync(src, 'utf-8');
796
- fs.writeFileSync(dest, `---\ndescription: ${mapping.desc}\n---\n\n${content}`);
797
- count++;
798
- }
799
- return count;
800
- }
801
- /** 获取 Claude Desktop 全局配置路径 */
802
- function getClaudeGlobalPath() {
803
- if (process.platform === 'win32')
804
- return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
805
- if (process.platform === 'darwin')
806
- return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
807
- return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
808
- }