@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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PPDocs Agent V2 — 多项目管理入口
4
+ * 功能: Web Config UI + 多 SyncBeacon + 持久后台运行
5
+ */
6
+ export {};
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
@@ -3,3 +3,5 @@
3
3
  * 命令: init - 初始化项目配置 + 安装工作流模板 + 自动注册 MCP
4
4
  */
5
5
  export declare function runCli(args: string[]): boolean;
6
+ /** 安装项目模板文件 (供授权后自动配置调用) */
7
+ export declare function setupProjectFiles(cwd: string, apiUrl: string): void;
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
- // ignore parse error
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
- "PPDOCS_API_URL": apiUrl,
371
- // 强行注入常见路径,防止找不到 node/npx
372
- "PATH": "$PATH:/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:$HOME/.nvm/versions/node/current/bin:$HOME/.local/share/fnm/aliases/default/bin"
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
- /** 生成随机用户名 (8位字母数字) */
18
+ /** 生成随机用户名 */
12
19
  export declare function generateUser(): string;
13
- /**
14
- * 加载配置 (优先级: 环境变量 > .ppdocs 文件)
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
- /** 生成随机用户名 (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
- * 从 .ppdocs 文件读取配置
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 cwd = process.cwd();
18
- const configPath = path.join(cwd, '.ppdocs');
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 content = fs.readFileSync(configPath, 'utf-8');
24
- const config = JSON.parse(content);
25
- if (!config.api || !config.projectId || !config.key) {
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
- return {
29
- apiUrl: `${config.api}/api/${config.projectId}/${config.key}`,
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
- function readEnvConfig() {
42
- const apiUrl = process.env.PPDOCS_API_URL;
43
- if (!apiUrl)
44
- return null;
45
- // URL 格式: http://localhost:20001/api/{projectId}/{password}
46
- const match = apiUrl.match(/\/api\/([^/]+)\/[^/]+\/?$/);
47
- const projectId = match?.[1] || 'unknown';
48
- const user = process.env.PPDOCS_USER || generateUser();
49
- return { apiUrl, projectId, user };
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
- * 加载配置 (优先级: 环境变量 > .ppdocs 文件)
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. 尝试 .ppdocs 文件
121
+ // 2. .ppdocs 文件
60
122
  const fileConfig = readPpdocsFile();
61
123
  if (fileConfig)
62
124
  return fileConfig;
63
- // 3. 报错
64
- console.error('ERROR: ppdocs config not found');
65
- console.error('');
66
- console.error('Option 1: Run init command in your project directory:');
67
- console.error(' npx @ppdocs/mcp init -p <projectId> -k <key>');
68
- console.error('');
69
- console.error('Option 2: Set environment variable:');
70
- console.error(' PPDOCS_API_URL=http://localhost:20001/api/{projectId}/{key}');
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
+ }