@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.
@@ -0,0 +1,611 @@
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
+ const state = {
43
+ hostConnected: false,
44
+ projectStatus: new Map(),
45
+ };
46
+ export function getAgentState() { return state; }
47
+ export function setAgentState(partial) {
48
+ Object.assign(state, partial);
49
+ }
50
+ export function setProjectStatus(remoteId, status) {
51
+ const existing = state.projectStatus.get(remoteId) || {
52
+ connected: false, syncStatus: '未启动', lastSync: null,
53
+ };
54
+ state.projectStatus.set(remoteId, { ...existing, ...status });
55
+ }
56
+ // ============ Express 服务器 ============
57
+ export function startWebServer(webPort) {
58
+ const app = express();
59
+ app.use(express.json());
60
+ // GET / → UI
61
+ app.get('/', (_req, res) => {
62
+ res.type('html').send(getAgentHtml());
63
+ });
64
+ // GET /api/status → 全局+各项目状态
65
+ app.get('/api/status', (_req, res) => {
66
+ const config = loadAgentConfig();
67
+ const projects = (config?.projects || []).map(p => {
68
+ const ps = state.projectStatus.get(p.remote.id);
69
+ return {
70
+ localName: p.localName,
71
+ localDir: p.localDir,
72
+ remoteId: p.remote.id,
73
+ remoteName: p.remote.name,
74
+ connected: ps?.connected || false,
75
+ syncStatus: ps?.syncStatus || (p.localDir ? '待启动' : '未指定目录'),
76
+ docCount: ps?.docCount || 0,
77
+ lastSync: ps?.lastSync ? timeSince(ps.lastSync) : null,
78
+ hasPassword: !!p.remote.password,
79
+ };
80
+ });
81
+ res.json({
82
+ hostConnected: state.hostConnected,
83
+ host: config?.host || '',
84
+ port: config?.port || 20001,
85
+ projects,
86
+ });
87
+ });
88
+ // POST /api/host → 保存主机配置
89
+ app.post('/api/host', async (req, res) => {
90
+ const { host, port } = req.body;
91
+ if (!host) {
92
+ res.json({ ok: false, error: '缺少 host' });
93
+ return;
94
+ }
95
+ const config = loadAgentConfig() || { host: '', port: 20001, webPort: webPort, projects: [] };
96
+ config.host = host;
97
+ config.port = port || 20001;
98
+ saveAgentConfig(config);
99
+ res.json({ ok: true });
100
+ });
101
+ // POST /api/test → 测试远程连接
102
+ app.post('/api/test', async (req, res) => {
103
+ const { host, port } = req.body;
104
+ try {
105
+ const controller = new AbortController();
106
+ const timeout = setTimeout(() => controller.abort(), 5000);
107
+ const resp = await fetch(`http://${host}:${port}/health`, { signal: controller.signal });
108
+ clearTimeout(timeout);
109
+ state.hostConnected = resp.ok;
110
+ res.json({ ok: resp.ok });
111
+ }
112
+ catch {
113
+ state.hostConnected = false;
114
+ res.json({ ok: false, error: '连接超时或主机不可达' });
115
+ }
116
+ });
117
+ // GET /api/projects → 远程项目列表 (保留用于兼容)
118
+ app.get('/api/projects', async (req, res) => {
119
+ const config = loadAgentConfig();
120
+ const host = req.query.host || config?.host;
121
+ const port = req.query.port || String(config?.port || 20001);
122
+ if (!host) {
123
+ res.json({ projects: [] });
124
+ return;
125
+ }
126
+ try {
127
+ const resp = await fetch(`http://${host}:${port}/api/projects`);
128
+ if (!resp.ok) {
129
+ res.json({ projects: [] });
130
+ return;
131
+ }
132
+ const data = await resp.json();
133
+ res.json({ projects: data.data || [] });
134
+ }
135
+ catch {
136
+ res.json({ projects: [] });
137
+ }
138
+ });
139
+ // ============ 服务端授权流程 ============
140
+ // POST /api/auth/start → 发起授权请求 (代理转发到主机)
141
+ app.post('/api/auth/start', async (req, res) => {
142
+ const { localDir } = req.body;
143
+ if (!localDir) {
144
+ res.json({ ok: false, error: '缺少本地目录' });
145
+ return;
146
+ }
147
+ const config = loadAgentConfig();
148
+ if (!config?.host) {
149
+ res.json({ ok: false, error: '请先配置主机' });
150
+ return;
151
+ }
152
+ try {
153
+ const resp = await fetch(`http://${config.host}:${config.port}/api/auth/request`, {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({
157
+ cwd: localDir,
158
+ hostname: os.hostname(),
159
+ }),
160
+ });
161
+ if (!resp.ok) {
162
+ res.json({ ok: false, error: '主机 auth API 不可用' });
163
+ return;
164
+ }
165
+ const data = await resp.json();
166
+ const requestId = data.data?.requestId;
167
+ if (!requestId) {
168
+ res.json({ ok: false, error: '未获得 requestId' });
169
+ return;
170
+ }
171
+ res.json({ ok: true, requestId });
172
+ }
173
+ catch (e) {
174
+ res.json({ ok: false, error: `连接主机失败: ${String(e).slice(0, 80)}` });
175
+ }
176
+ });
177
+ // GET /api/auth/status/:id → 轮询授权结果 (代理转发)
178
+ app.get('/api/auth/status/:id', async (req, res) => {
179
+ const config = loadAgentConfig();
180
+ if (!config?.host) {
181
+ res.json({ status: 'error', error: '未配置主机' });
182
+ return;
183
+ }
184
+ try {
185
+ const resp = await fetch(`http://${config.host}:${config.port}/api/auth/poll/${req.params.id}`);
186
+ if (!resp.ok) {
187
+ res.json({ status: 'error' });
188
+ return;
189
+ }
190
+ const data = await resp.json();
191
+ const d = data.data;
192
+ if (d?.status === 'approved' && d.result) {
193
+ // 授权通过,自动绑定
194
+ const result = d.result;
195
+ res.json({
196
+ status: 'approved',
197
+ projectId: result.project_id,
198
+ projectName: result.project_name,
199
+ password: result.password,
200
+ });
201
+ }
202
+ else {
203
+ res.json({ status: d?.status || 'pending' });
204
+ }
205
+ }
206
+ catch {
207
+ res.json({ status: 'error' });
208
+ }
209
+ });
210
+ // POST /api/bind → 绑定项目 (授权通过后调用)
211
+ app.post('/api/bind', async (req, res) => {
212
+ const { localDir, remoteId, remoteName, password } = req.body;
213
+ if (!localDir || !remoteId) {
214
+ res.json({ ok: false, error: '缺少参数' });
215
+ return;
216
+ }
217
+ const config = loadAgentConfig();
218
+ if (!config) {
219
+ res.json({ ok: false, error: '请先配置主机' });
220
+ return;
221
+ }
222
+ // 检查重复
223
+ if (config.projects.some(p => p.remote.id === remoteId)) {
224
+ res.json({ ok: false, error: '该远程项目已绑定' });
225
+ return;
226
+ }
227
+ const binding = {
228
+ localDir,
229
+ localName: path.basename(localDir),
230
+ remote: { id: remoteId, name: remoteName || remoteId, password: password || '' },
231
+ sync: { enabled: true, intervalSec: 15 },
232
+ createdAt: new Date().toISOString(),
233
+ };
234
+ config.projects.push(binding);
235
+ saveAgentConfig(config);
236
+ // 通知 Agent 主进程启动同步
237
+ if (state.onBind)
238
+ state.onBind(binding);
239
+ res.json({ ok: true, binding });
240
+ });
241
+ // DELETE /api/bind/:id → 解绑
242
+ app.delete('/api/bind/:id', (req, res) => {
243
+ const remoteId = req.params.id;
244
+ const config = loadAgentConfig();
245
+ if (!config) {
246
+ res.json({ ok: false });
247
+ return;
248
+ }
249
+ config.projects = config.projects.filter(p => p.remote.id !== remoteId);
250
+ saveAgentConfig(config);
251
+ state.projectStatus.delete(remoteId);
252
+ if (state.onUnbind)
253
+ state.onUnbind(remoteId);
254
+ res.json({ ok: true });
255
+ });
256
+ // PATCH /api/bind/:id → 更新绑定(用于重新授权后更新密码)
257
+ app.patch('/api/bind/:id', (req, res) => {
258
+ const remoteId = req.params.id;
259
+ const { password, remoteName } = req.body;
260
+ const config = loadAgentConfig();
261
+ if (!config) {
262
+ res.json({ ok: false });
263
+ return;
264
+ }
265
+ const proj = config.projects.find(p => p.remote.id === remoteId);
266
+ if (!proj) {
267
+ res.json({ ok: false, error: '项目不存在' });
268
+ return;
269
+ }
270
+ if (password)
271
+ proj.remote.password = password;
272
+ if (remoteName)
273
+ proj.remote.name = remoteName;
274
+ saveAgentConfig(config);
275
+ res.json({ ok: true });
276
+ });
277
+ // GET /api/bind/:id/tree → 项目知识图谱 (无密码时尝试跨项目只读)
278
+ app.get('/api/bind/:id/tree', async (req, res) => {
279
+ const remoteId = req.params.id;
280
+ const config = loadAgentConfig();
281
+ const proj = config?.projects.find(p => p.remote.id === remoteId);
282
+ if (!config || !proj) {
283
+ res.json({ tree: '项目不存在' });
284
+ return;
285
+ }
286
+ let docsUrl;
287
+ let readOnly = false;
288
+ if (proj.remote.password) {
289
+ // 有密码 → 直接读取(读写模式)
290
+ docsUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}/docs`;
291
+ }
292
+ else {
293
+ // 无密码 → 尝试用其他已授权项目的跨项目只读API
294
+ const proxy = config.projects.find(p => p.remote.id !== remoteId && p.remote.password);
295
+ if (proxy) {
296
+ docsUrl = `http://${config.host}:${config.port}/api/${proxy.remote.id}/${proxy.remote.password}/cross/${remoteId}/docs`;
297
+ readOnly = true;
298
+ }
299
+ else {
300
+ // 无任何已授权项目 → 尝试公开API
301
+ docsUrl = `http://${config.host}:${config.port}/api/projects/${remoteId}/docs`;
302
+ readOnly = true;
303
+ }
304
+ }
305
+ try {
306
+ const resp = await fetch(docsUrl);
307
+ if (!resp.ok) {
308
+ res.json({ tree: readOnly ? '⚠️ 未授权,无法获取文档 — 请点击"申请授权"' : `获取失败 (HTTP ${resp.status})`, readOnly });
309
+ return;
310
+ }
311
+ const json = await resp.json();
312
+ const docs = json.data || [];
313
+ if (docs.length === 0) {
314
+ res.json({ tree: '知识库为空', docCount: 0, dirCount: 0, readOnly });
315
+ return;
316
+ }
317
+ const lines = docs
318
+ .sort((a, b) => a.path.localeCompare(b.path))
319
+ .map(d => {
320
+ const depth = (d.path.match(/\//g) || []).length;
321
+ const indent = ' '.repeat(Math.max(0, depth - 1));
322
+ const icon = d.isDir ? '📁' : '📄';
323
+ const summary = d.summary ? ` — ${d.summary}` : '';
324
+ return `${indent}${icon} ${d.name}${summary}`;
325
+ });
326
+ const docCount = docs.filter(d => !d.isDir).length;
327
+ const dirCount = docs.filter(d => d.isDir).length;
328
+ res.json({ tree: lines.join('\n'), docCount, dirCount, readOnly });
329
+ }
330
+ catch (e) {
331
+ res.json({ tree: `连接失败: ${String(e).slice(0, 100)}` });
332
+ }
333
+ });
334
+ // GET /api/bind/:id/prompts → 读取项目提示词文件
335
+ app.get('/api/bind/:id/prompts', (req, res) => {
336
+ const config = loadAgentConfig();
337
+ const proj = config?.projects.find(p => p.remote.id === req.params.id);
338
+ if (!proj?.localDir) {
339
+ res.json({ files: [] });
340
+ return;
341
+ }
342
+ const dir = proj.localDir;
343
+ const names = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', 'CONVENTIONS.md'];
344
+ const files = names.map(name => {
345
+ const fp = path.join(dir, name);
346
+ const exists = fs.existsSync(fp);
347
+ let lines = 0;
348
+ if (exists) {
349
+ try {
350
+ lines = fs.readFileSync(fp, 'utf-8').split('\n').length;
351
+ }
352
+ catch { }
353
+ }
354
+ return { name, exists, lines };
355
+ });
356
+ res.json({ files });
357
+ });
358
+ // GET /api/bind/:id/prompts/:name → 读取单个提示词文件
359
+ app.get('/api/bind/:id/prompts/:name', (req, res) => {
360
+ const config = loadAgentConfig();
361
+ const proj = config?.projects.find(p => p.remote.id === req.params.id);
362
+ if (!proj?.localDir) {
363
+ res.json({ ok: false });
364
+ return;
365
+ }
366
+ const fp = path.join(proj.localDir, req.params.name);
367
+ if (!fs.existsSync(fp)) {
368
+ res.json({ ok: false, error: '文件不存在' });
369
+ return;
370
+ }
371
+ try {
372
+ res.json({ ok: true, content: fs.readFileSync(fp, 'utf-8') });
373
+ }
374
+ catch (e) {
375
+ res.json({ ok: false, error: String(e) });
376
+ }
377
+ });
378
+ // PUT /api/bind/:id/prompts/:name → 保存提示词文件
379
+ app.put('/api/bind/:id/prompts/:name', (req, res) => {
380
+ const config = loadAgentConfig();
381
+ const proj = config?.projects.find(p => p.remote.id === req.params.id);
382
+ if (!proj?.localDir) {
383
+ res.json({ ok: false });
384
+ return;
385
+ }
386
+ const allowed = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', 'CONVENTIONS.md'];
387
+ if (!allowed.includes(req.params.name)) {
388
+ res.json({ ok: false, error: '不允许的文件名' });
389
+ return;
390
+ }
391
+ try {
392
+ fs.writeFileSync(path.join(proj.localDir, req.params.name), req.body.content || '', 'utf-8');
393
+ res.json({ ok: true });
394
+ }
395
+ catch (e) {
396
+ res.json({ ok: false, error: String(e) });
397
+ }
398
+ });
399
+ // GET /api/bind/:id/mcp/all → 扫描项目目录下各平台的完整 MCP 服务列表
400
+ app.get('/api/bind/:id/mcp/all', (req, res) => {
401
+ const config = loadAgentConfig();
402
+ const proj = config?.projects.find(p => p.remote.id === req.params.id);
403
+ if (!proj?.localDir) {
404
+ res.json({ servers: [] });
405
+ return;
406
+ }
407
+ const dir = proj.localDir;
408
+ const allServers = {}; // serverName → [platforms...]
409
+ const scanTargets = [
410
+ { platform: 'Cursor', filePath: path.join(dir, '.cursor', 'mcp.json') },
411
+ { platform: 'Antigravity', filePath: path.join(dir, '.gemini', 'settings.json') },
412
+ { platform: 'OpenCode', filePath: path.join(dir, '.vscode', 'cline_mcp_settings.json') },
413
+ ];
414
+ for (const t of scanTargets) {
415
+ try {
416
+ if (!fs.existsSync(t.filePath))
417
+ continue;
418
+ const content = JSON.parse(fs.readFileSync(t.filePath, 'utf-8'));
419
+ const servers = content.mcpServers || content.servers || {};
420
+ for (const name of Object.keys(servers)) {
421
+ if (!allServers[name])
422
+ allServers[name] = [];
423
+ allServers[name].push(t.platform);
424
+ }
425
+ }
426
+ catch { }
427
+ }
428
+ const servers = Object.entries(allServers).map(([name, platforms]) => ({ name, platforms }));
429
+ res.json({ servers });
430
+ });
431
+ // 获取各平台 MCP 配置的路径和服务器名称
432
+ const getMcpSpecs = (dir, projId) => ({
433
+ cursor: { path: path.join(dir, '.cursor', 'mcp.json'), name: 'ppdocs-kg' },
434
+ antigravity: { path: path.join(dir, '.gemini', 'settings.json'), name: 'ppdocs-kg' },
435
+ vscode: { path: path.join(dir, '.vscode', 'cline_mcp_settings.json'), name: 'ppdocs-kg' },
436
+ claude: { path: getClaudeGlobalPath(), name: `ppdocs-kg-${projId}` },
437
+ lobechat: { path: '', name: 'ppdocs-kg' }, // 无本地文件,靠复制配置
438
+ });
439
+ // GET /api/bind/:id/mcp → MCP 安装状态检测
440
+ app.get('/api/bind/:id/mcp', (req, res) => {
441
+ const remoteId = req.params.id;
442
+ const config = loadAgentConfig();
443
+ const proj = config?.projects.find(p => p.remote.id === remoteId);
444
+ if (!proj?.localDir) {
445
+ res.json({ platforms: {} });
446
+ return;
447
+ }
448
+ const specs = getMcpSpecs(proj.localDir, remoteId);
449
+ const platforms = {
450
+ cursor: hasMcpInJson(specs.cursor.path, specs.cursor.name),
451
+ antigravity: hasMcpInJson(specs.antigravity.path, specs.antigravity.name),
452
+ vscode: hasMcpInJson(specs.vscode.path, specs.vscode.name),
453
+ claude: hasMcpInJson(specs.claude.path, specs.claude.name),
454
+ lobechat: false, // 无法自动检测
455
+ };
456
+ res.json({ platforms });
457
+ });
458
+ // POST /api/bind/:id/mcp → 安装/获取 MCP 配置
459
+ app.post('/api/bind/:id/mcp', (req, res) => {
460
+ const remoteId = req.params.id;
461
+ const { platform } = req.body;
462
+ const config = loadAgentConfig();
463
+ const proj = config?.projects.find(p => p.remote.id === remoteId);
464
+ if (!config || !proj) {
465
+ res.json({ ok: false, error: '项目不存在' });
466
+ return;
467
+ }
468
+ if (!proj.localDir) {
469
+ res.json({ ok: false, error: '请先指定本地目录' });
470
+ return;
471
+ }
472
+ const apiUrl = `http://${config.host}:${config.port}/api/${proj.remote.id}/${proj.remote.password}`;
473
+ const dir = proj.localDir;
474
+ const isWin = process.platform === 'win32';
475
+ const ppdocsServer = buildMcpServerConfig(apiUrl, isWin);
476
+ // 对于 LobeChat,直接返回 JSON 供用户复制,不写入本地
477
+ if (platform === 'lobechat') {
478
+ res.json({
479
+ ok: true,
480
+ message: 'LobeChat 配置生成成功',
481
+ action: 'copy',
482
+ config: JSON.stringify({ mcpServers: { 'ppdocs-kg': ppdocsServer } }, null, 2),
483
+ });
484
+ return;
485
+ }
486
+ try {
487
+ const specs = getMcpSpecs(dir, remoteId);
488
+ const spec = specs[platform];
489
+ if (!spec || !spec.path) {
490
+ res.json({ ok: false, error: '未知平台或不支持自动安装' });
491
+ return;
492
+ }
493
+ writeMcpConfig(spec.path, spec.name, ppdocsServer);
494
+ // 同时写入 .ppdocs 配置文件
495
+ const ppdocsPath = path.join(dir, '.ppdocs');
496
+ fs.writeFileSync(ppdocsPath, JSON.stringify({
497
+ api: `http://${config.host}:${config.port}`,
498
+ projectId: proj.remote.id,
499
+ key: proj.remote.password,
500
+ user: `Agent@${os.hostname()}`,
501
+ }, null, 2));
502
+ res.json({ ok: true, message: `✅ ${platform} MCP 已安装` });
503
+ }
504
+ catch (e) {
505
+ res.json({ ok: false, error: String(e) });
506
+ }
507
+ });
508
+ // DELETE /api/bind/:id/mcp → 卸载 MCP
509
+ app.delete('/api/bind/:id/mcp', (req, res) => {
510
+ const remoteId = req.params.id;
511
+ const { platform } = req.body;
512
+ const config = loadAgentConfig();
513
+ const proj = config?.projects.find(p => p.remote.id === remoteId);
514
+ if (!proj?.localDir) {
515
+ res.json({ ok: false });
516
+ return;
517
+ }
518
+ if (platform === 'lobechat') {
519
+ res.json({ ok: true, message: '请在 LobeChat 界面中手动删除' });
520
+ return;
521
+ }
522
+ try {
523
+ const specs = getMcpSpecs(proj.localDir, remoteId);
524
+ const spec = specs[platform];
525
+ if (spec && fs.existsSync(spec.path)) {
526
+ removeMcpFromConfig(spec.path, spec.name);
527
+ }
528
+ res.json({ ok: true, message: `✅ ${platform} MCP 已卸载` });
529
+ }
530
+ catch (e) {
531
+ res.json({ ok: false, error: String(e) });
532
+ }
533
+ });
534
+ app.listen(webPort, () => {
535
+ console.log(`\n📡 PPDocs Agent Web UI: http://localhost:${webPort}\n`);
536
+ });
537
+ }
538
+ function timeSince(date) {
539
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
540
+ if (seconds < 60)
541
+ return `${seconds}秒前`;
542
+ const minutes = Math.floor(seconds / 60);
543
+ if (minutes < 60)
544
+ return `${minutes}分钟前`;
545
+ return `${Math.floor(minutes / 60)}小时前`;
546
+ }
547
+ /** 检测 JSON 配置文件中是否有指名的 MCP */
548
+ function hasMcpInJson(filePath, serverName) {
549
+ try {
550
+ if (!fs.existsSync(filePath))
551
+ return false;
552
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
553
+ const servers = content.mcpServers || content.servers || {};
554
+ return serverName in servers;
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ }
560
+ /** 构建 MCP server 配置对象 */
561
+ function buildMcpServerConfig(apiUrl, isWindows) {
562
+ return isWindows ? {
563
+ command: 'cmd',
564
+ args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
565
+ env: { PPDOCS_API_URL: apiUrl },
566
+ } : {
567
+ command: 'npx',
568
+ args: ['-y', '@ppdocs/mcp@latest'],
569
+ env: {
570
+ PPDOCS_API_URL: apiUrl,
571
+ PATH: `${process.env.PATH || '/usr/bin:/bin'}:/usr/local/bin:/opt/homebrew/bin`,
572
+ },
573
+ };
574
+ }
575
+ /** 写入 MCP 配置到 JSON 文件 (合并已有配置) */
576
+ function writeMcpConfig(filePath, serverName, serverConfig) {
577
+ let existing = {};
578
+ if (fs.existsSync(filePath)) {
579
+ try {
580
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
581
+ }
582
+ catch { /* ignore */ }
583
+ }
584
+ existing.mcpServers = {
585
+ ...(existing.mcpServers || {}),
586
+ [serverName]: serverConfig,
587
+ };
588
+ const dir = path.dirname(filePath);
589
+ if (!fs.existsSync(dir))
590
+ fs.mkdirSync(dir, { recursive: true });
591
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2));
592
+ }
593
+ /** 从 JSON 配置中移除指定的 MCP */
594
+ function removeMcpFromConfig(filePath, serverName) {
595
+ try {
596
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
597
+ const servers = content.mcpServers || {};
598
+ delete servers[serverName];
599
+ content.mcpServers = servers;
600
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
601
+ }
602
+ catch { /* ignore */ }
603
+ }
604
+ /** 获取 Claude Desktop 全局配置路径 */
605
+ function getClaudeGlobalPath() {
606
+ if (process.platform === 'win32')
607
+ return path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
608
+ if (process.platform === 'darwin')
609
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
610
+ return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
611
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * PPDocs Agent V3 — Multi-Page SPA UI
3
+ * 页面: 项目列表 / 添加项目 / 项目详情 (标签页: 概览/MCP/提示词)
4
+ */
5
+ export declare function getAgentHtml(): string;