@ppdocs/mcp 3.1.9 → 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.d.ts +2 -0
- package/dist/cli.js +49 -14
- package/dist/config.d.ts +12 -6
- package/dist/config.js +106 -42
- package/dist/index.js +13 -3
- 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 -36
- 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.d.ts
CHANGED
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;
|
|
@@ -181,8 +190,8 @@ async function initProject(opts) {
|
|
|
181
190
|
installClaudeTemplates(cwd); // Default to Claude templates
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
|
-
// 自动检测并注册 MCP
|
|
185
|
-
const registered = autoRegisterMcp(apiUrl, opts.user);
|
|
193
|
+
// 自动检测并注册 MCP (如果已写入 Antigravity 配置,跳过 gemini CLI 注册避免冲突)
|
|
194
|
+
const registered = autoRegisterMcp(apiUrl, opts.user, detectedIdes.includes('antigravity'));
|
|
186
195
|
// 如果没有检测到任何 AI CLI,并且也没有检测到配置文件夹,创建 .mcp.json 作为备用
|
|
187
196
|
if (!registered && !hasIdeDir) {
|
|
188
197
|
createMcpJson(cwd, apiUrl);
|
|
@@ -280,7 +289,7 @@ function execSilent(cmd) {
|
|
|
280
289
|
catch { /* ignore */ }
|
|
281
290
|
}
|
|
282
291
|
/** 自动检测 AI CLI 并注册 MCP */
|
|
283
|
-
function autoRegisterMcp(apiUrl, user) {
|
|
292
|
+
function autoRegisterMcp(apiUrl, user, skipGemini = false) {
|
|
284
293
|
const detected = [];
|
|
285
294
|
const serverName = 'ppdocs-kg';
|
|
286
295
|
// 检测 Claude CLI (不传环境变量,MCP启动时读取.ppdocs)
|
|
@@ -324,7 +333,7 @@ function autoRegisterMcp(apiUrl, user) {
|
|
|
324
333
|
}
|
|
325
334
|
}
|
|
326
335
|
// 检测 Gemini CLI
|
|
327
|
-
if (commandExists('gemini')) {
|
|
336
|
+
if (commandExists('gemini') && !skipGemini) {
|
|
328
337
|
detected.push('Gemini');
|
|
329
338
|
try {
|
|
330
339
|
console.log(`✅ Detected Gemini CLI, registering MCP...`);
|
|
@@ -344,33 +353,39 @@ function autoRegisterMcp(apiUrl, user) {
|
|
|
344
353
|
return true;
|
|
345
354
|
}
|
|
346
355
|
/** 在指定路径创建或更新 mcp.json 配置 */
|
|
347
|
-
function createMcpConfigAt(mcpPath, apiUrl) {
|
|
356
|
+
function createMcpConfigAt(mcpPath, apiUrl, options) {
|
|
348
357
|
let mcpConfig = {};
|
|
349
358
|
if (fs.existsSync(mcpPath)) {
|
|
350
359
|
try {
|
|
351
360
|
mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
352
361
|
}
|
|
353
362
|
catch {
|
|
354
|
-
//
|
|
363
|
+
// 解析失败时中止,避免清空已有配置
|
|
364
|
+
console.log(`⚠️ ${mcpPath} JSON格式无效,跳过MCP配置写入(请手动修复后重试)`);
|
|
365
|
+
return;
|
|
355
366
|
}
|
|
356
367
|
}
|
|
357
368
|
// Windows 需要 cmd /c 包装才能执行 npx
|
|
358
369
|
const isWindows = process.platform === 'win32';
|
|
359
370
|
// 对于 Mac/Linux,IDE 可能没有用户的完整 PATH,导致找不到 npx
|
|
371
|
+
// 使用实际 PATH 值拼接,JSON 中的 $PATH 不会被展开
|
|
372
|
+
const macExtraPaths = '/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin';
|
|
373
|
+
const actualPath = `${process.env.PATH || '/usr/bin:/bin'}:${macExtraPaths}`;
|
|
360
374
|
const ppdocsServer = isWindows
|
|
361
375
|
? {
|
|
362
376
|
command: 'cmd',
|
|
363
377
|
args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
|
|
364
|
-
env: { "PPDOCS_API_URL": apiUrl }
|
|
378
|
+
...(options?.noEnv ? {} : { env: { "PPDOCS_API_URL": apiUrl } })
|
|
365
379
|
}
|
|
366
380
|
: {
|
|
367
381
|
command: 'npx',
|
|
368
382
|
args: ['-y', '@ppdocs/mcp@latest'],
|
|
369
|
-
env: {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
383
|
+
...(options?.noEnv ? { env: { "PATH": actualPath } } : {
|
|
384
|
+
env: {
|
|
385
|
+
"PPDOCS_API_URL": apiUrl,
|
|
386
|
+
"PATH": actualPath
|
|
387
|
+
}
|
|
388
|
+
})
|
|
374
389
|
};
|
|
375
390
|
mcpConfig.mcpServers = {
|
|
376
391
|
...(mcpConfig.mcpServers || {}),
|
|
@@ -522,8 +537,8 @@ function installCursorTemplates(cwd, apiUrl) {
|
|
|
522
537
|
}
|
|
523
538
|
/** 安装 Antigravity (Gemini IDE) 模板 */
|
|
524
539
|
function installAntigravityTemplates(cwd, apiUrl) {
|
|
525
|
-
// 1. 填充 .gemini/settings.json
|
|
526
|
-
createMcpConfigAt(path.join(cwd, '.gemini', 'settings.json'), apiUrl);
|
|
540
|
+
// 1. 填充 .gemini/settings.json (noEnv: 依赖 .ppdocs 文件,不传 PPDOCS_API_URL 避免覆盖)
|
|
541
|
+
createMcpConfigAt(path.join(cwd, '.gemini', 'settings.json'), apiUrl, { noEnv: true });
|
|
527
542
|
// 2. 生成 AGENTS.md
|
|
528
543
|
generateAgentsMd(cwd);
|
|
529
544
|
// 3. 安装斜杠命令 → .agents/workflows/ (Antigravity 的 slash command 机制)
|
|
@@ -587,3 +602,23 @@ function detectIDEs(cwd) {
|
|
|
587
602
|
ides.push('kiro');
|
|
588
603
|
return ides;
|
|
589
604
|
}
|
|
605
|
+
/** 安装项目模板文件 (供授权后自动配置调用) */
|
|
606
|
+
export function setupProjectFiles(cwd, apiUrl) {
|
|
607
|
+
const detectedIdes = detectIDEs(cwd);
|
|
608
|
+
if (detectedIdes.includes('antigravity')) {
|
|
609
|
+
installAntigravityTemplates(cwd, apiUrl);
|
|
610
|
+
}
|
|
611
|
+
if (detectedIdes.includes('cursor')) {
|
|
612
|
+
installCursorTemplates(cwd, apiUrl);
|
|
613
|
+
}
|
|
614
|
+
if (detectedIdes.includes('kiro')) {
|
|
615
|
+
installKiroTemplates(cwd, apiUrl);
|
|
616
|
+
}
|
|
617
|
+
if (detectedIdes.includes('claude')) {
|
|
618
|
+
installClaudeTemplates(cwd);
|
|
619
|
+
}
|
|
620
|
+
// 如果没检测到任何 IDE,默认安装 Claude 模板
|
|
621
|
+
if (detectedIdes.length === 0) {
|
|
622
|
+
installClaudeTemplates(cwd);
|
|
623
|
+
}
|
|
624
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ppdocs MCP Config
|
|
3
|
-
* 读取配置: 环境变量 > .ppdocs 文件
|
|
3
|
+
* 读取配置: 环境变量 > .ppdocs 文件 > 自动发现 > 授权请求
|
|
4
4
|
*/
|
|
5
5
|
export interface PpdocsConfig {
|
|
6
6
|
apiUrl: string;
|
|
7
7
|
projectId: string;
|
|
8
8
|
user: string;
|
|
9
|
+
source: 'env' | 'file' | 'auth';
|
|
10
|
+
/** 原始连接参数 (discover/auth 填充, 供持久化使用) */
|
|
11
|
+
connection?: {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
password: string;
|
|
15
|
+
};
|
|
9
16
|
}
|
|
10
17
|
export declare const PPDOCS_CONFIG_FILE = ".ppdocs";
|
|
11
|
-
/** 生成随机用户名
|
|
18
|
+
/** 生成随机用户名 */
|
|
12
19
|
export declare function generateUser(): string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export declare function loadConfig(): PpdocsConfig;
|
|
20
|
+
/** 写入 .ppdocs 文件 (需 config.connection 存在) */
|
|
21
|
+
export declare function writePpdocsFile(config: PpdocsConfig): void;
|
|
22
|
+
export declare function loadConfig(): Promise<PpdocsConfig>;
|
package/dist/config.js
CHANGED
|
@@ -1,72 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ppdocs MCP Config
|
|
3
|
-
* 读取配置: 环境变量 > .ppdocs 文件
|
|
3
|
+
* 读取配置: 环境变量 > .ppdocs 文件 > 自动发现 > 授权请求
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
7
8
|
export const PPDOCS_CONFIG_FILE = '.ppdocs';
|
|
8
|
-
/** 生成随机用户名
|
|
9
|
+
/** 生成随机用户名 */
|
|
9
10
|
export function generateUser() {
|
|
10
11
|
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
11
12
|
return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// ============ 静态配置读取 ============
|
|
15
|
+
function readEnvConfig() {
|
|
16
|
+
const apiUrl = process.env.PPDOCS_API_URL;
|
|
17
|
+
if (!apiUrl)
|
|
18
|
+
return null;
|
|
19
|
+
const match = apiUrl.match(/\/api\/([^/]+)\/[^/]+\/?$/);
|
|
20
|
+
return { apiUrl, projectId: match?.[1] || 'unknown', user: process.env.PPDOCS_USER || generateUser(), source: 'env' };
|
|
21
|
+
}
|
|
16
22
|
function readPpdocsFile() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (!fs.existsSync(configPath)) {
|
|
23
|
+
const configPath = path.join(process.cwd(), '.ppdocs');
|
|
24
|
+
if (!fs.existsSync(configPath))
|
|
20
25
|
return null;
|
|
26
|
+
try {
|
|
27
|
+
const c = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
28
|
+
if (!c.api || !c.projectId || !c.key)
|
|
29
|
+
return null;
|
|
30
|
+
return { apiUrl: `${c.api}/api/${c.projectId}/${c.key}`, projectId: c.projectId, user: c.user || generateUser(), source: 'file' };
|
|
21
31
|
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function findLocalServer() {
|
|
37
|
+
for (const host of ['localhost', '127.0.0.1', '10.0.0.176']) {
|
|
38
|
+
for (const port of [20001]) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(2000) });
|
|
41
|
+
if (res.ok)
|
|
42
|
+
return { host, port, base: `http://${host}:${port}` };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// ============ 授权请求 ============
|
|
52
|
+
async function requestAuthConfig(server) {
|
|
22
53
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
const reqRes = await fetch(`${server.base}/api/auth/request`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ cwd: process.cwd(), hostname: os.hostname() }),
|
|
58
|
+
});
|
|
59
|
+
if (!reqRes.ok)
|
|
26
60
|
return null;
|
|
61
|
+
const { data } = await reqRes.json();
|
|
62
|
+
if (!data?.requestId)
|
|
63
|
+
return null;
|
|
64
|
+
console.error(`[Auth] 等待桌面端授权... (请在 ppdocs 桌面端确认)`);
|
|
65
|
+
for (let i = 0; i < 150; i++) {
|
|
66
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
67
|
+
const pollRes = await fetch(`${server.base}/api/auth/poll/${data.requestId}`, { signal: AbortSignal.timeout(3000) });
|
|
68
|
+
if (!pollRes.ok)
|
|
69
|
+
continue;
|
|
70
|
+
const { data: poll } = await pollRes.json();
|
|
71
|
+
if (poll?.status === 'approved' && poll.result) {
|
|
72
|
+
const r = poll.result;
|
|
73
|
+
console.error(`[Auth] 已授权: ${r.project_name} (${r.project_id})`);
|
|
74
|
+
return {
|
|
75
|
+
apiUrl: `http://${r.api_host}:${r.api_port}/api/${r.project_id}/${r.password}`,
|
|
76
|
+
projectId: r.project_id,
|
|
77
|
+
user: `auto-${generateUser().slice(0, 4)}`,
|
|
78
|
+
source: 'auth',
|
|
79
|
+
connection: { host: r.api_host, port: r.api_port, password: r.password },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (poll?.status === 'rejected') {
|
|
83
|
+
console.error('[Auth] 授权被拒绝');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (poll?.status === 'expired') {
|
|
87
|
+
console.error('[Auth] 授权超时');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
27
90
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
projectId: config.projectId,
|
|
31
|
-
user: config.user || generateUser(),
|
|
32
|
-
};
|
|
91
|
+
console.error('[Auth] 轮询超时');
|
|
92
|
+
return null;
|
|
33
93
|
}
|
|
34
94
|
catch {
|
|
35
95
|
return null;
|
|
36
96
|
}
|
|
37
97
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
// ============ 持久化 ============
|
|
99
|
+
/** 写入 .ppdocs 文件 (需 config.connection 存在) */
|
|
100
|
+
export function writePpdocsFile(config) {
|
|
101
|
+
if (!config.connection)
|
|
102
|
+
return;
|
|
103
|
+
const configPath = path.join(process.cwd(), '.ppdocs');
|
|
104
|
+
if (fs.existsSync(configPath))
|
|
105
|
+
return;
|
|
106
|
+
const { host, port, password } = config.connection;
|
|
107
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
108
|
+
api: `http://${host}:${port}`,
|
|
109
|
+
projectId: config.projectId,
|
|
110
|
+
key: password,
|
|
111
|
+
user: config.user,
|
|
112
|
+
}, null, 2), 'utf-8');
|
|
113
|
+
console.error(`[Config] 已保存 .ppdocs`);
|
|
50
114
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export function loadConfig() {
|
|
55
|
-
// 1. 尝试环境变量
|
|
115
|
+
// ============ 入口 ============
|
|
116
|
+
export async function loadConfig() {
|
|
117
|
+
// 1. 环境变量
|
|
56
118
|
const envConfig = readEnvConfig();
|
|
57
119
|
if (envConfig)
|
|
58
120
|
return envConfig;
|
|
59
|
-
// 2.
|
|
121
|
+
// 2. .ppdocs 文件
|
|
60
122
|
const fileConfig = readPpdocsFile();
|
|
61
123
|
if (fileConfig)
|
|
62
124
|
return fileConfig;
|
|
63
|
-
// 3.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
// 3. 扫描本地服务器 → 请求桌面端授权
|
|
126
|
+
const server = await findLocalServer();
|
|
127
|
+
if (server) {
|
|
128
|
+
const authConfig = await requestAuthConfig(server);
|
|
129
|
+
if (authConfig)
|
|
130
|
+
return authConfig;
|
|
131
|
+
}
|
|
132
|
+
// 全部失败
|
|
133
|
+
console.error('ERROR: ppdocs 配置未找到');
|
|
134
|
+
console.error(' 请确保 ppdocs 桌面端正在运行,或手动配置 PPDOCS_API_URL 环境变量');
|
|
71
135
|
process.exit(1);
|
|
72
136
|
}
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
14
|
import { registerTools } from './tools/index.js';
|
|
15
15
|
import { initClient } from './storage/httpClient.js';
|
|
16
|
-
import { runCli } from './cli.js';
|
|
17
|
-
import { loadConfig } from './config.js';
|
|
16
|
+
import { runCli, setupProjectFiles } from './cli.js';
|
|
17
|
+
import { loadConfig, writePpdocsFile } from './config.js';
|
|
18
18
|
// 从 package.json 读取版本号
|
|
19
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -26,7 +26,17 @@ if (args.length > 0 && runCli(args)) {
|
|
|
26
26
|
}
|
|
27
27
|
// 运行 MCP 服务
|
|
28
28
|
async function main() {
|
|
29
|
-
const config = loadConfig();
|
|
29
|
+
const config = await loadConfig();
|
|
30
|
+
// 自动持久化: 发现/授权后写入 .ppdocs + 安装模板
|
|
31
|
+
if (config.source === 'auth' && config.connection) {
|
|
32
|
+
try {
|
|
33
|
+
writePpdocsFile(config);
|
|
34
|
+
setupProjectFiles(process.cwd(), config.apiUrl);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error('[AutoSetup] 自动配置失败:', e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
30
40
|
initClient(config.apiUrl);
|
|
31
41
|
const server = new McpServer({ name: `ppdocs [${config.projectId}]`, version: VERSION }, { capabilities: { tools: {} } });
|
|
32
42
|
registerTools(server, config.projectId, config.user);
|
|
@@ -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
|
+
}
|