mcp-osp-prompt 1.0.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,1027 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { detectPlatformFromPath, parseRemotePath, createAuthHeaders } from './platform-utils.js';
4
+
5
+ /**
6
+ * Prompt管理器
7
+ * 职责:
8
+ * 1. 同步初始化:首次加载确保工具列表完整可用
9
+ * 2. 统一缓存:管理文件列表和内容的TTL缓存
10
+ * 3. 异步更新:使用时异步检查TTL并更新
11
+ * 4. 提供prompt获取接口:对接fetcher.js
12
+ */
13
+ class PromptManager {
14
+ constructor() {
15
+ this.cacheDir = process.env.CACHE_DIR || path.join(process.env.HOME || '.', '.cache', 'prompts');
16
+ this.ttl = parseInt(process.env.CACHE_TTL_SEC || '10800', 10) * 1000; // 转为毫秒,默认3小时
17
+ this.force = (process.env.FORCE_UPDATE || 'false').toLowerCase() === 'true';
18
+ this.token = process.env.GIT_TOKEN || process.env.GITLAB_TOKEN || process.env.GITHUB_TOKEN; // 支持多种token环境变量
19
+ this.platform = null;
20
+ this.resourcePath = null;
21
+ this.toolsConfig = [];
22
+ this.isInitialized = false;
23
+ }
24
+
25
+ /**
26
+ * 同步初始化 - 必须在MCP服务器启动时完成
27
+ * 确保工具列表完整可用
28
+ */
29
+ async initializeSync() {
30
+ if (this.isInitialized) {
31
+ return this.toolsConfig;
32
+ }
33
+
34
+ console.error('[PromptManager] Starting synchronous initialization...');
35
+
36
+ // 🟢 GREEN: Task 1.1 - 确保resourcePath正确初始化
37
+ this.resourcePath = process.env.RESOURCE_PATH;
38
+ if (!this.resourcePath) {
39
+ throw new Error('RESOURCE_PATH environment variable is required');
40
+ }
41
+
42
+ this.platform = detectPlatformFromPath(this.resourcePath);
43
+ console.error(`[PromptManager] Platform: ${this.platform}, Path: ${this.resourcePath}`);
44
+
45
+ // 确保缓存目录存在(包括子目录)
46
+ await fs.mkdir(this.cacheDir, { recursive: true });
47
+ await fs.mkdir(path.join(this.cacheDir, 'projects'), { recursive: true });
48
+ await fs.mkdir(path.join(this.cacheDir, 'rules'), { recursive: true });
49
+
50
+ // 🟢 GREEN: Task 3.1 - 多目录资源同步(如果是远程平台)
51
+ if (this.platform !== 'local') {
52
+ console.error('[PromptManager] Checking multi-directory resources...');
53
+
54
+ // 先尝试读取缓存
55
+ const cachedResources = await this.readCachedMultiDirectoryResources();
56
+ const TTL_12_HOURS = 12 * 60 * 60 * 1000; // 12小时
57
+
58
+ if (cachedResources && cachedResources.lastSync) {
59
+ const cacheAge = Date.now() - new Date(cachedResources.lastSync).getTime();
60
+
61
+ if (cacheAge < TTL_12_HOURS && !this.force) {
62
+ // 缓存未过期,直接使用
63
+ console.error(`[PromptManager] ✅ Using cached resources (age: ${Math.round(cacheAge / 1000 / 60 / 60)}h)`);
64
+ this.toolsConfig = this.generateToolsConfig(cachedResources.prompts);
65
+ } else {
66
+ // 缓存过期,先使用缓存,然后异步更新
67
+ console.error(`[PromptManager] ⏳ Using cached resources (age: ${Math.round(cacheAge / 1000 / 60 / 60)}h), triggering async update...`);
68
+ this.toolsConfig = this.generateToolsConfig(cachedResources.prompts);
69
+
70
+ // 异步更新(不阻塞)
71
+ this.fetchMultiDirectoryResources().then(() => {
72
+ console.error('[PromptManager] ✅ Async resource update complete');
73
+ }).catch(error => {
74
+ console.warn(`[PromptManager] ⚠️ Async resource update failed: ${error.message}`);
75
+ });
76
+ }
77
+ } else {
78
+ // 无缓存或缓存损坏,必须同步拉取
79
+ console.error('[PromptManager] 🔄 No cache found, fetching resources...');
80
+ try {
81
+ const resources = await this.fetchMultiDirectoryResources();
82
+ console.error(`[PromptManager] ✅ Resource fetch complete: ${resources.prompts.length} prompts, ${resources.projects.length} projects, ${resources.rules.length} rules`);
83
+ this.toolsConfig = this.generateToolsConfig(resources.prompts);
84
+ } catch (error) {
85
+ console.error(`[PromptManager] ❌ Resource fetch failed: ${error.message}`);
86
+ // 最后的fallback:使用旧的单目录逻辑
87
+ const promptFiles = await this.getPromptFilesList();
88
+ this.toolsConfig = this.generateToolsConfig(promptFiles);
89
+ }
90
+ }
91
+ } else {
92
+ // 本地模式:支持多目录资源
93
+ try {
94
+ console.error('[PromptManager] Local mode: Scanning multi-directory resources...');
95
+ const resources = await this.fetchLocalMultiDirectoryResources();
96
+ console.error(`[PromptManager] ✅ Local scan complete: ${resources.prompts.length} prompts, ${resources.projects.length} projects, ${resources.rules.length} rules`);
97
+ this.toolsConfig = this.generateToolsConfig(resources.prompts);
98
+ } catch (error) {
99
+ console.warn(`[PromptManager] ⚠️ Local multi-directory scan failed: ${error.message}, using single directory mode`);
100
+ const promptFiles = await this.getPromptFilesList();
101
+ this.toolsConfig = this.generateToolsConfig(promptFiles);
102
+ }
103
+ }
104
+
105
+ // 不再验证核心工具内容可用性,仅确保工具配置存在
106
+ this.validateToolsConfigExists();
107
+
108
+ this.isInitialized = true;
109
+ console.error(`[PromptManager] ✅ Initialization complete: ${this.toolsConfig.length} tools ready`);
110
+
111
+ return this.toolsConfig;
112
+ }
113
+
114
+ /**
115
+ * 获取prompt文件列表(带缓存)
116
+ */
117
+ async getPromptFilesList() {
118
+ if (this.platform === 'local') {
119
+ return await this.scanLocalPromptFiles();
120
+ } else {
121
+ return await this.getRemotePromptFilesList();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 扫描本地prompt文件
127
+ */
128
+ async scanLocalPromptFiles() {
129
+ try {
130
+ const files = await fs.readdir(this.resourcePath);
131
+ const promptFiles = files
132
+ .filter(file => file.endsWith('.prompt.md'))
133
+ .map(filename => {
134
+ const baseName = filename.replace('.prompt.md', '');
135
+ return {
136
+ filename,
137
+ toolName: `dev-${baseName}`,
138
+ source: filename,
139
+ platform: 'local'
140
+ };
141
+ });
142
+
143
+ console.error(`[PromptManager] Local scan: ${promptFiles.length} files found`);
144
+ return promptFiles;
145
+ } catch (error) {
146
+ throw new Error(`Failed to scan local prompt directory: ${error.message}`);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 获取远程prompt文件列表(启动阶段逻辑)
152
+ * 🟢 GREEN: Task 1.1 - 修正启动逻辑:只要有缓存就立即启动+异步更新
153
+ */
154
+ async getRemotePromptFilesList() {
155
+ const fileListCache = path.join(this.cacheDir, 'file-list.json');
156
+
157
+ // 检查缓存是否存在
158
+ if (!this.force) {
159
+ try {
160
+ const stat = await fs.stat(fileListCache);
161
+ const cacheAge = Date.now() - stat.mtimeMs;
162
+ const cacheAgeMinutes = Math.round(cacheAge / 1000 / 60);
163
+
164
+ // 读取缓存内容
165
+ const cachedList = JSON.parse(await fs.readFile(fileListCache, 'utf8'));
166
+
167
+ // 🔧 关键修正:启动时不区分缓存新鲜度,只要有缓存就立即启动+异步更新
168
+ console.error(`[PromptManager] Using cached file list for startup (age: ${cacheAgeMinutes}min), scheduling async update`);
169
+
170
+ // 启动阶段总是异步更新,确保缓存最新
171
+ this.asyncUpdateFileListCache().catch(error => {
172
+ console.warn(`[PromptManager] Async file list update failed: ${error.message}`);
173
+ });
174
+
175
+ return cachedList;
176
+ } catch (_) {
177
+ // 缓存不存在,必须同步获取
178
+ console.error('[PromptManager] No cache found, fetching synchronously for startup');
179
+ }
180
+ }
181
+
182
+ // 无缓存时必须同步获取
183
+ return await this.fetchRemoteFileListSync();
184
+ }
185
+
186
+ /**
187
+ * 🟢 GREEN: Task 1.1 - 同步获取远程文件列表
188
+ */
189
+ async fetchRemoteFileListSync() {
190
+ const fileListCache = path.join(this.cacheDir, 'file-list.json');
191
+
192
+ console.error('[PromptManager] Fetching remote file list synchronously...');
193
+ const pathInfo = parseRemotePath(this.resourcePath);
194
+ if (!pathInfo) {
195
+ throw new Error(`Unable to parse RESOURCE_PATH: ${this.resourcePath}`);
196
+ }
197
+
198
+ try {
199
+ let promptFiles;
200
+ if (pathInfo.platform === 'gitlab') {
201
+ promptFiles = await this.fetchGitLabFileList(pathInfo);
202
+ } else if (pathInfo.platform === 'github') {
203
+ promptFiles = await this.fetchGitHubFileList(pathInfo);
204
+ } else {
205
+ throw new Error(`Unsupported platform: ${pathInfo.platform}`);
206
+ }
207
+
208
+ // 缓存文件列表
209
+ await fs.writeFile(fileListCache, JSON.stringify(promptFiles, null, 2), 'utf8');
210
+ console.error(`[PromptManager] Remote scan: ${promptFiles.length} files cached`);
211
+
212
+ return promptFiles;
213
+ } catch (error) {
214
+ // API失败时尝试使用旧缓存作为最后手段
215
+ try {
216
+ const cachedList = await fs.readFile(fileListCache, 'utf8');
217
+ console.warn(`[PromptManager] API failed, using stale cache: ${error.message}`);
218
+ return JSON.parse(cachedList);
219
+ } catch (_) {
220
+ throw new Error(`Remote file list fetch failed and no cache available: ${error.message}`);
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 🟢 GREEN: Task 1.1 - 异步更新文件列表缓存
227
+ */
228
+ async asyncUpdateFileListCache() {
229
+ try {
230
+ console.error('[PromptManager] Starting async file list update...');
231
+ const promptFiles = await this.fetchRemoteFileListSync();
232
+ console.error(`[PromptManager] ✅ Async file list update completed: ${promptFiles.length} files`);
233
+ return promptFiles;
234
+ } catch (error) {
235
+ console.warn(`[PromptManager] ⚠️ Async file list update failed: ${error.message}`);
236
+ throw error;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 🟢 GREEN: Task 1.2 - 通用目录文件扫描函数
242
+ * @param {Object} pathInfo - 路径信息
243
+ * @param {String} filePattern - 文件模式 (*.prompt.md, *.md, *-code-rules.md)
244
+ * @param {String} dirType - 目录类型 (prompts, projects, rules)
245
+ * @returns {Array} 文件列表
246
+ */
247
+ async fetchDirectoryFiles(pathInfo, filePattern, dirType = 'prompts') {
248
+ if (!pathInfo || !filePattern) {
249
+ throw new Error('pathInfo and filePattern are required');
250
+ }
251
+
252
+ const platform = pathInfo.platform;
253
+ console.error(`[PromptManager] Fetching ${dirType} files (pattern: ${filePattern}) from ${platform}...`);
254
+
255
+ // 根据平台调用不同的API
256
+ let files = [];
257
+ if (platform === 'gitlab') {
258
+ files = await this.fetchGitLabDirectory(pathInfo);
259
+ } else if (platform === 'github') {
260
+ files = await this.fetchGitHubDirectory(pathInfo);
261
+ } else {
262
+ throw new Error(`Unsupported platform: ${platform}`);
263
+ }
264
+
265
+ // 根据文件模式过滤和映射
266
+ return this.filterAndMapFiles(files, filePattern, dirType, pathInfo);
267
+ }
268
+
269
+ /**
270
+ * 🟢 GREEN: Task 1.2 - 从GitLab获取目录文件(原始数据)
271
+ */
272
+ async fetchGitLabDirectory(pathInfo) {
273
+ const headers = createAuthHeaders('gitlab', process.env.GIT_TOKEN);
274
+ console.error(`[PromptManager] GitLab API call: ${pathInfo.apiUrl}`);
275
+ const requestOptions = { headers, timeout: 10000 };
276
+ const response = await fetch(pathInfo.apiUrl, requestOptions);
277
+
278
+ if (!response.ok) {
279
+ const debugHeaders = { ...headers };
280
+ const requestInfo = `
281
+ REQUEST INFO: \n
282
+ - URL: ${pathInfo.apiUrl} \n
283
+ - Method: GET \n
284
+ - Headers: ${JSON.stringify(debugHeaders, null, 2)} \n
285
+ - Timeout: ${requestOptions.timeout}ms \n
286
+ - Response Status: ${response.status} \n
287
+ - Response StatusText: ${response.statusText} \n`;
288
+ throw new Error(`GitLab API error: ${requestInfo}`);
289
+ }
290
+
291
+ return await response.json();
292
+ }
293
+
294
+ /**
295
+ * 🟢 GREEN: Task 1.2 - 从GitHub获取目录文件(原始数据)
296
+ */
297
+ async fetchGitHubDirectory(pathInfo) {
298
+ const url = `${pathInfo.apiUrl}?ref=${pathInfo.branch}`;
299
+ const headers = createAuthHeaders('github', process.env.GIT_TOKEN);
300
+ console.error(`[PromptManager] GitHub API call: ${url}`);
301
+ const requestOptions = { headers, timeout: 10000 };
302
+ const response = await fetch(url, requestOptions);
303
+
304
+ if (!response.ok) {
305
+ const debugHeaders = { ...headers };
306
+ const requestInfo = `
307
+ REQUEST INFO: \n
308
+ - URL: ${url} \n
309
+ - Method: GET \n
310
+ - Headers: ${JSON.stringify(debugHeaders, null, 2)} \n
311
+ - Timeout: ${requestOptions.timeout}ms \n
312
+ - Response Status: ${response.status} \n
313
+ - Response StatusText: ${response.statusText} \n`;
314
+ throw new Error(`GitHub API error: ${requestInfo}`);
315
+ }
316
+
317
+ return await response.json();
318
+ }
319
+
320
+ /**
321
+ * 🟢 GREEN: Task 1.2 - 根据文件模式过滤和映射文件
322
+ */
323
+ filterAndMapFiles(files, filePattern, dirType, pathInfo) {
324
+ // 转换filePattern为正则表达式 - 先转义点,再替换星号
325
+ const pattern = filePattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
326
+ const regex = new RegExp(`^${pattern}$`);
327
+
328
+ const platform = pathInfo.platform;
329
+ const fileTypeKey = platform === 'gitlab' ? 'blob' : 'file';
330
+
331
+ return files
332
+ .filter(file => {
333
+ const isFile = platform === 'gitlab' ? file.type === 'blob' : file.type === 'file';
334
+ return isFile && regex.test(file.name);
335
+ })
336
+ .map(file => {
337
+ const mapped = {
338
+ filename: file.name,
339
+ path: `${pathInfo.path ? pathInfo.path + '/' : ''}${file.name}`,
340
+ platform: platform
341
+ };
342
+
343
+ // 根据目录类型添加额外信息
344
+ if (dirType === 'prompts') {
345
+ const baseName = file.name.replace('.prompt.md', '');
346
+ mapped.toolName = `dev-${baseName}`;
347
+ mapped.source = mapped.path;
348
+ } else if (dirType === 'rules') {
349
+ // 提取语言名称 (java-code-rules.md -> java)
350
+ const langMatch = file.name.match(/^(.+)-code-rules\.md$/);
351
+ if (langMatch) {
352
+ mapped.language = langMatch[1];
353
+ }
354
+ } else if (dirType === 'projects') {
355
+ // projects目录,保留原始文件名
356
+ mapped.type = 'project-doc';
357
+ }
358
+
359
+ return mapped;
360
+ });
361
+ }
362
+
363
+ /**
364
+ * 🟢 GREEN: Task 1.2 - 从GitLab获取文件列表(重构为使用通用函数)
365
+ */
366
+ async fetchGitLabFileList(pathInfo) {
367
+ return await this.fetchDirectoryFiles(pathInfo, '*.prompt.md', 'prompts');
368
+ }
369
+
370
+ /**
371
+ * 🟢 GREEN: Task 1.2 - 从GitHub获取文件列表(重构为使用通用函数)
372
+ */
373
+ async fetchGitHubFileList(pathInfo) {
374
+ return await this.fetchDirectoryFiles(pathInfo, '*.prompt.md', 'prompts');
375
+ }
376
+
377
+ /**
378
+ * 🟢 GREEN: Task 1.4 - 缓存多目录资源
379
+ * @param {Object} resources - 资源对象 { prompts, projects, rules, lastSync }
380
+ */
381
+ async cacheMultiDirectoryResources(resources) {
382
+ const cacheFile = path.join(this.cacheDir, 'resource-cache.json');
383
+
384
+ try {
385
+ await fs.writeFile(cacheFile, JSON.stringify(resources, null, 2), 'utf8');
386
+ console.error(`[PromptManager] ✅ Resources cached to ${cacheFile}`);
387
+
388
+ // 同时保留旧格式的file-list.json用于向后兼容
389
+ const legacyCache = path.join(this.cacheDir, 'file-list.json');
390
+ await fs.writeFile(legacyCache, JSON.stringify(resources.prompts, null, 2), 'utf8');
391
+
392
+ } catch (error) {
393
+ console.warn(`[PromptManager] ⚠️ Failed to cache resources: ${error.message}`);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 🟢 GREEN: Task 1.4 - 读取缓存的多目录资源
399
+ * @returns {Object|null} 资源对象或null
400
+ */
401
+ async readCachedMultiDirectoryResources() {
402
+ const cacheFile = path.join(this.cacheDir, 'resource-cache.json');
403
+
404
+ try {
405
+ const data = await fs.readFile(cacheFile, 'utf8');
406
+ const resources = JSON.parse(data);
407
+
408
+ // 验证缓存结构
409
+ if (resources && resources.prompts && resources.projects && resources.rules) {
410
+ console.error(`[PromptManager] ✅ Loaded cached resources (${resources.prompts.length + resources.projects.length + resources.rules.length} total files)`);
411
+ return resources;
412
+ }
413
+
414
+ return null;
415
+ } catch (error) {
416
+ console.warn(`[PromptManager] No valid resource cache found: ${error.message}`);
417
+ return null;
418
+ }
419
+ }
420
+
421
+ /**
422
+ * 🟢 GREEN: Task 1.4 - 检查缓存是否过期
423
+ * @param {Object} resources - 资源对象
424
+ * @returns {boolean} true表示过期
425
+ */
426
+ isCacheExpired(resources) {
427
+ if (!resources || !resources.lastSync) {
428
+ return true;
429
+ }
430
+
431
+ const lastSyncTime = new Date(resources.lastSync).getTime();
432
+ const now = Date.now();
433
+ const ttlMs = this.ttl * 1000;
434
+ const age = now - lastSyncTime;
435
+
436
+ const expired = age >= ttlMs;
437
+ console.error(`[PromptManager] Cache age: ${Math.round(age / 1000)}s, TTL: ${this.ttl}s, Expired: ${expired}`);
438
+
439
+ return expired;
440
+ }
441
+
442
+ /**
443
+ * 🟢 GREEN: Task 1.3 - 多目录资源拉取主函数
444
+ * @returns {Object} { prompts: [], projects: [], rules: [], lastSync: ISO时间 }
445
+ */
446
+ async fetchMultiDirectoryResources() {
447
+ const directories = [
448
+ { name: 'prompts', pattern: '*.prompt.md', type: 'prompts' },
449
+ { name: 'projects', pattern: '*.md', type: 'projects' },
450
+ { name: 'rules', pattern: '*-rules.md', type: 'rules' }
451
+ ];
452
+
453
+ const results = {
454
+ prompts: [],
455
+ projects: [],
456
+ rules: [],
457
+ lastSync: new Date().toISOString()
458
+ };
459
+
460
+ console.error('[PromptManager] Fetching multi-directory resources...');
461
+
462
+ // 依次拉取各个目录(错误容错,单个失败不影响其他)
463
+ for (const dir of directories) {
464
+ try {
465
+ console.error(`[PromptManager] Fetching ${dir.name} directory...`);
466
+
467
+ // 使用parseRemotePath的子目录功能
468
+ const pathInfo = parseRemotePath(this.resourcePath, dir.name);
469
+
470
+ // 使用通用扫描函数
471
+ const files = await this.fetchDirectoryFiles(pathInfo, dir.pattern, dir.type);
472
+
473
+ results[dir.type] = files;
474
+ console.error(`[PromptManager] ✅ ${dir.name}: ${files.length} files`);
475
+
476
+ } catch (error) {
477
+ console.warn(`[PromptManager] ⚠️ Failed to fetch ${dir.name}: ${error.message}`);
478
+ results[dir.type] = []; // 失败时使用空数组
479
+ }
480
+ }
481
+
482
+ console.error(`[PromptManager] Multi-directory fetch complete. Total: ${results.prompts.length + results.projects.length + results.rules.length} files`);
483
+
484
+ // 🟢 GREEN: Task 3.1 - 保存文件内容到子目录
485
+ await this.saveResourceContents(results);
486
+
487
+ // 🟢 GREEN: Task 1.4 - 自动缓存结果
488
+ await this.cacheMultiDirectoryResources(results);
489
+
490
+ return results;
491
+ }
492
+
493
+ /**
494
+ * 🟢 GREEN: 本地多目录资源扫描
495
+ * @returns {Object} { prompts: [], projects: [], rules: [], lastSync: ISO时间 }
496
+ */
497
+ async fetchLocalMultiDirectoryResources() {
498
+ // 假设 RESOURCE_PATH 指向包含 prompts/projects/rules 子目录的基础目录
499
+ // 或者直接指向 prompts 目录,此时需要找到父目录
500
+
501
+ let baseDir = this.resourcePath;
502
+
503
+ // 如果 RESOURCE_PATH 直接指向 prompts 目录,找到父目录
504
+ if (baseDir.endsWith('/prompts') || baseDir.endsWith('\\prompts')) {
505
+ baseDir = path.dirname(baseDir);
506
+ }
507
+
508
+ const directories = [
509
+ { name: 'prompts', pattern: /\.prompt\.md$/, type: 'prompts' },
510
+ { name: 'projects', pattern: /\.md$/, type: 'projects' },
511
+ { name: 'rules', pattern: /-rules\.md$/, type: 'rules' }
512
+ ];
513
+
514
+ const results = {
515
+ prompts: [],
516
+ projects: [],
517
+ rules: [],
518
+ lastSync: new Date().toISOString()
519
+ };
520
+
521
+ console.error(`[PromptManager] Scanning local directories from base: ${baseDir}`);
522
+
523
+ for (const dir of directories) {
524
+ try {
525
+ const dirPath = path.join(baseDir, dir.name);
526
+ console.error(`[PromptManager] Scanning ${dir.name} directory: ${dirPath}`);
527
+
528
+ // 检查目录是否存在
529
+ try {
530
+ await fs.access(dirPath);
531
+ } catch {
532
+ console.warn(`[PromptManager] ⚠️ Directory not found: ${dirPath}`);
533
+ results[dir.type] = [];
534
+ continue;
535
+ }
536
+
537
+ const files = await fs.readdir(dirPath);
538
+ const matchedFiles = files
539
+ .filter(file => dir.pattern.test(file))
540
+ .map(filename => {
541
+ const fileInfo = {
542
+ filename,
543
+ type: dir.type,
544
+ path: path.join(dirPath, filename)
545
+ };
546
+
547
+ // 为 prompts 类型添加额外的工具信息
548
+ if (dir.type === 'prompts') {
549
+ const baseName = filename.replace('.prompt.md', '');
550
+ fileInfo.toolName = `dev-${baseName}`;
551
+ fileInfo.source = filename;
552
+ fileInfo.platform = 'local';
553
+ }
554
+
555
+ return fileInfo;
556
+ });
557
+
558
+ results[dir.type] = matchedFiles;
559
+ console.error(`[PromptManager] ✅ ${dir.name}: ${matchedFiles.length} files`);
560
+
561
+ } catch (error) {
562
+ console.warn(`[PromptManager] ⚠️ Failed to scan ${dir.name}: ${error.message}`);
563
+ results[dir.type] = [];
564
+ }
565
+ }
566
+
567
+ console.error(`[PromptManager] Local scan complete. Total: ${results.prompts.length + results.projects.length + results.rules.length} files`);
568
+
569
+ // 本地模式:复制文件到缓存目录
570
+ await this.saveLocalResourceContents(results, baseDir);
571
+
572
+ // 缓存结果
573
+ await this.cacheMultiDirectoryResources(results);
574
+
575
+ return results;
576
+ }
577
+
578
+ /**
579
+ * 🟢 GREEN: 保存本地资源文件内容到缓存目录
580
+ * @param {Object} resources - 资源对象
581
+ * @param {string} baseDir - 基础目录
582
+ */
583
+ async saveLocalResourceContents(resources, baseDir) {
584
+ console.error('[PromptManager] Copying local resource contents to cache...');
585
+
586
+ // 复制 projects 内容
587
+ for (const project of resources.projects) {
588
+ try {
589
+ const sourcePath = path.join(baseDir, 'projects', project.filename);
590
+ const destPath = path.join(this.cacheDir, 'projects', project.filename);
591
+ const content = await fs.readFile(sourcePath, 'utf8');
592
+ await fs.writeFile(destPath, content, 'utf8');
593
+ console.error(`[PromptManager] ✅ Copied ${project.filename} (${content.length} bytes)`);
594
+ } catch (error) {
595
+ console.warn(`[PromptManager] ⚠️ Failed to copy ${project.filename}: ${error.message}`);
596
+ }
597
+ }
598
+
599
+ // 复制 rules 内容
600
+ for (const rule of resources.rules) {
601
+ try {
602
+ const sourcePath = path.join(baseDir, 'rules', rule.filename);
603
+ const destPath = path.join(this.cacheDir, 'rules', rule.filename);
604
+ const content = await fs.readFile(sourcePath, 'utf8');
605
+ await fs.writeFile(destPath, content, 'utf8');
606
+ console.error(`[PromptManager] ✅ Copied ${rule.filename} (${content.length} bytes)`);
607
+ } catch (error) {
608
+ console.warn(`[PromptManager] ⚠️ Failed to copy ${rule.filename}: ${error.message}`);
609
+ }
610
+ }
611
+
612
+ console.error('[PromptManager] ✅ Local resource copy complete');
613
+ }
614
+
615
+ /**
616
+ * 🟢 GREEN: Task 3.1 - 保存资源文件内容到本地子目录
617
+ * @param {Object} resources - 资源对象
618
+ */
619
+ async saveResourceContents(resources) {
620
+ console.error('[PromptManager] Saving resource contents to cache...');
621
+
622
+ // 解析远程路径信息
623
+ const pathInfo = parseRemotePath(this.resourcePath);
624
+
625
+ // 保存projects内容
626
+ for (const project of resources.projects) {
627
+ try {
628
+ const content = await this.fetchFileContent(pathInfo, 'projects', project.filename);
629
+ const filePath = path.join(this.cacheDir, 'projects', project.filename);
630
+ await fs.writeFile(filePath, content, 'utf8');
631
+ console.error(`[PromptManager] ✅ Saved ${project.filename} (${content.length} bytes)`);
632
+ } catch (error) {
633
+ console.warn(`[PromptManager] ⚠️ Failed to save ${project.filename}: ${error.message}`);
634
+ }
635
+ }
636
+
637
+ // 保存rules内容
638
+ for (const rule of resources.rules) {
639
+ try {
640
+ const content = await this.fetchFileContent(pathInfo, 'rules', rule.filename);
641
+ const filePath = path.join(this.cacheDir, 'rules', rule.filename);
642
+ await fs.writeFile(filePath, content, 'utf8');
643
+ console.error(`[PromptManager] ✅ Saved ${rule.filename} (${content.length} bytes)`);
644
+ } catch (error) {
645
+ console.warn(`[PromptManager] ⚠️ Failed to save ${rule.filename}: ${error.message}`);
646
+ }
647
+ }
648
+
649
+ console.error('[PromptManager] ✅ Resource content save complete');
650
+ }
651
+
652
+ /**
653
+ * 🟢 GREEN: 从远程获取单个文件内容
654
+ * @param {Object} pathInfo - 路径信息
655
+ * @param {string} subDir - 子目录(projects或rules)
656
+ * @param {string} filename - 文件名
657
+ * @returns {Promise<string>} 文件内容
658
+ */
659
+ async fetchFileContent(pathInfo, subDir, filename) {
660
+ const { platform, project, branch, path: basePath } = pathInfo;
661
+ const filePath = `${basePath}/${subDir}/${filename}`;
662
+
663
+ if (platform === 'gitlab') {
664
+ const encodedPath = encodeURIComponent(filePath);
665
+ const url = `https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/repository/files/${encodedPath}/raw?ref=${branch}`;
666
+
667
+ const headers = createAuthHeaders(platform, this.token);
668
+ console.error(`[PromptManager] 🔐 Token available: ${this.token ? 'YES' : 'NO'}, Headers: ${JSON.stringify(Object.keys(headers))}`);
669
+
670
+ const response = await fetch(url, {
671
+ headers: headers
672
+ });
673
+
674
+ if (!response.ok) {
675
+ const errorText = await response.text().catch(() => '');
676
+ console.error(`[PromptManager] ⚠️ GitLab API error for ${filename}: ${response.status}`);
677
+ console.error(`[PromptManager] URL: ${url}`);
678
+ throw new Error(`GitLab API error: ${response.status} for ${filePath}`);
679
+ }
680
+
681
+ return await response.text();
682
+ } else if (platform === 'github') {
683
+ // GitHub uses owner/repo format (project contains it)
684
+ const url = `https://raw.githubusercontent.com/${project}/${branch}/${filePath}`;
685
+
686
+ const response = await fetch(url, {
687
+ headers: createAuthHeaders(platform, this.token)
688
+ });
689
+
690
+ if (!response.ok) {
691
+ throw new Error(`GitHub raw API error: ${response.status} for ${filePath}`);
692
+ }
693
+
694
+ return await response.text();
695
+ } else {
696
+ throw new Error(`Unsupported platform for file content fetch: ${platform}`);
697
+ }
698
+ }
699
+
700
+ /**
701
+ * 生成工具配置
702
+ */
703
+ generateToolsConfig(promptFiles) {
704
+ // Prompt工具
705
+ const promptTools = promptFiles.map(({ toolName, source }) => ({
706
+ name: toolName,
707
+ type: 'prompt',
708
+ source: source,
709
+ description: `${toolName} prompt direct`,
710
+ schema: {
711
+ type: 'object',
712
+ properties: {
713
+ source: { type: 'string' }
714
+ },
715
+ required: ['source']
716
+ }
717
+ }));
718
+
719
+ // Handler工具(固定配置)
720
+ const handlerTools = [
721
+ {
722
+ name: 'dev-feedback',
723
+ type: 'handler',
724
+ handler: 'handleDevFeedback',
725
+ description: '反馈确认工具',
726
+ schema: {
727
+ type: 'object',
728
+ properties: {
729
+ title: { type: 'string' },
730
+ message: { type: 'string' },
731
+ options: { type: 'array', items: { type: 'string' } }
732
+ },
733
+ required: ['title', 'message']
734
+ }
735
+ },
736
+ {
737
+ name: 'dev-manual',
738
+ type: 'handler',
739
+ handler: 'handleDevManual',
740
+ description: '手动选择开发类型',
741
+ schema: {
742
+ type: 'object',
743
+ properties: {
744
+ random_string: { description: 'Dummy parameter for no-parameter tools', type: 'string' }
745
+ },
746
+ required: ['random_string']
747
+ }
748
+ },
749
+ {
750
+ name: 'load-resource',
751
+ type: 'handler',
752
+ handler: 'handleLoadResource',
753
+ description: 'Load full content of a resource (project documentation or rules)',
754
+ schema: {
755
+ type: 'object',
756
+ properties: {
757
+ resourceName: {
758
+ type: 'string',
759
+ description: 'Resource filename (e.g. "java-rules.md", "kiki-framework-wiki.md")'
760
+ }
761
+ },
762
+ required: ['resourceName']
763
+ }
764
+ }
765
+ ];
766
+
767
+ return [...promptTools, ...handlerTools];
768
+ }
769
+
770
+ /**
771
+ * 验证工具配置存在(不验证内容)
772
+ */
773
+ validateToolsConfigExists() {
774
+ const promptTools = this.toolsConfig.filter(tool => tool.type === 'prompt');
775
+ if (promptTools.length === 0) {
776
+ throw new Error('No prompt tools configured after initialization');
777
+ }
778
+
779
+ console.error(`[PromptManager] Tools configured: ${promptTools.length} prompt tools available`);
780
+ console.error(`[PromptManager] Available tools: ${promptTools.map(t => t.name).join(', ')}`);
781
+ }
782
+
783
+ /**
784
+ * 获取prompt内容(运行时逻辑)
785
+ * 🟢 GREEN: Task 1.1 - 运行时区分缓存新鲜度
786
+ */
787
+ async getPromptContent(fileName) {
788
+ const cacheFile = path.join(this.cacheDir, fileName);
789
+ const versionFile = path.join(this.cacheDir, `${fileName}.version`);
790
+
791
+ try {
792
+ const stat = await fs.stat(cacheFile);
793
+ const cacheAge = Date.now() - stat.mtimeMs;
794
+
795
+ // 读取缓存内容
796
+ const content = await fs.readFile(cacheFile, 'utf8');
797
+
798
+ if (cacheAge < this.ttl && !this.force) {
799
+ // 🟢 缓存新鲜:直接返回,不进行异步更新
800
+ console.error(`[PromptManager] Using fresh cached ${fileName} (age: ${Math.round(cacheAge / 1000 / 60)}min)`);
801
+ return content;
802
+ } else {
803
+ // 🟡 缓存过期:立即返回缓存内容,同时异步更新
804
+ console.error(`[PromptManager] Using stale cached ${fileName} (age: ${Math.round(cacheAge / 1000 / 60)}min), scheduling async update`);
805
+
806
+ // 异步更新缓存(不阻塞本次返回)
807
+ this.asyncUpdatePromptCache(fileName).catch(error => {
808
+ console.warn(`[PromptManager] Async cache update failed for ${fileName}: ${error.message}`);
809
+ });
810
+
811
+ return content;
812
+ }
813
+ } catch (error) {
814
+ // 🔴 缓存不存在:必须立即获取
815
+ console.error(`[PromptManager] Cache miss for ${fileName}, fetching immediately`);
816
+ return await this.fetchPromptContent(fileName);
817
+ }
818
+ }
819
+
820
+ /**
821
+ * 从远程获取prompt内容
822
+ */
823
+ async fetchPromptContent(fileName) {
824
+ if (this.platform === 'local') {
825
+ const filePath = path.join(this.resourcePath, fileName);
826
+ return await fs.readFile(filePath, 'utf8');
827
+ } else {
828
+ console.error(`[PromptManager] Fetching prompt content: ${fileName}`);
829
+
830
+ // 🟢 GREEN: Task 1.1 - 添加更详细的错误处理
831
+ let pathInfo;
832
+ try {
833
+ pathInfo = parseRemotePath(this.promptPath);
834
+ } catch (error) {
835
+ throw new Error(`Failed to parse remote path configuration: ${error.message}`);
836
+ }
837
+ let url, headers;
838
+
839
+ if (pathInfo.platform === 'github') {
840
+ // fileName 已经包含完整相对路径,不需要再拼接 pathInfo.path
841
+ url = `https://api.github.com/repos/${pathInfo.project}/contents/${fileName}?ref=${pathInfo.branch}`;
842
+ headers = createAuthHeaders('github', process.env.GIT_TOKEN);
843
+ console.error(`[PromptManager] GitHub API call: ${url}`);
844
+ } else if (pathInfo.platform === 'gitlab') {
845
+ const projectEnc = encodeURIComponent(pathInfo.project);
846
+ // fileName 已经包含完整相对路径,不需要再拼接 pathInfo.path
847
+ const filePathEnc = encodeURIComponent(fileName);
848
+ url = `https://gitlab.com/api/v4/projects/${projectEnc}/repository/files/${filePathEnc}?ref=${pathInfo.branch}`;
849
+ headers = createAuthHeaders('gitlab', process.env.GIT_TOKEN);
850
+ console.error(`[PromptManager] GitLab API call: ${url}`);
851
+ }
852
+
853
+ const requestOptions = { headers, timeout: 10000 };
854
+ const response = await fetch(url, requestOptions);
855
+ if (!response.ok) {
856
+ // 🟢 GREEN: Task 1.2 - 增强错误信息的详细性,包含完整请求信息
857
+
858
+ // 构建详细的调试信息,隐藏敏感token信息
859
+ const debugHeaders = { ...headers };
860
+
861
+ const requestInfo = `
862
+ REQUEST INFO: \n
863
+ - URL: ${url} \n
864
+ - Method: GET \n
865
+ - Headers: ${JSON.stringify(debugHeaders, null, 2)} \n
866
+ - Timeout: ${requestOptions.timeout}ms \n
867
+ - Response Status: ${response.status} \n
868
+ - Response StatusText: ${response.statusText} \n`;
869
+
870
+ throw new Error(`fetchPromptContent Error: ${fileName}, request info: ${requestInfo}`);
871
+ }
872
+
873
+ const data = await response.json();
874
+ const content = Buffer.from(data.content, 'base64').toString('utf8');
875
+
876
+ // 获取版本信息 - 优先使用last_commit_id(GitLab)或sha(GitHub)
877
+ let version = 'unknown';
878
+ if (pathInfo.platform === 'gitlab' && data.last_commit_id) {
879
+ version = data.last_commit_id.substring(0, 8); // 前8位作为版本号
880
+ } else if (pathInfo.platform === 'github' && data.sha) {
881
+ version = data.sha.substring(0, 8);
882
+ }
883
+
884
+ // 保存到缓存
885
+ await this.saveToCacheWithVersion(fileName, content, version);
886
+
887
+ console.error(`[PromptManager] Successfully fetched ${fileName} with version ${version}`);
888
+ return content;
889
+ }
890
+ }
891
+
892
+ /**
893
+ * 异步更新prompt缓存
894
+ */
895
+ async asyncUpdatePromptCache(fileName) {
896
+ try {
897
+ console.error(`[PromptManager] Async updating cache for ${fileName}...`);
898
+ await this.fetchPromptContent(fileName);
899
+ console.error(`[PromptManager] ✅ Async cache update completed for ${fileName}`);
900
+ } catch (error) {
901
+ console.warn(`[PromptManager] ⚠️ Async cache update failed for ${fileName}: ${error.message}`);
902
+ }
903
+ }
904
+
905
+ /**
906
+ * 保存prompt到缓存,同时保存版本信息
907
+ */
908
+ async saveToCacheWithVersion(fileName, content, version) {
909
+ const cacheFile = path.join(this.cacheDir, fileName);
910
+ const versionFile = path.join(this.cacheDir, `${fileName}.version`);
911
+
912
+ try {
913
+ await fs.writeFile(cacheFile, content, 'utf8');
914
+
915
+ // 获取缓存文件的修改时间并格式化为yyyyMMddHH
916
+ const stat = await fs.stat(cacheFile);
917
+ const modTime = new Date(stat.mtimeMs);
918
+ const timeFormat = modTime.getFullYear().toString() +
919
+ (modTime.getMonth() + 1).toString().padStart(2, '0') +
920
+ modTime.getDate().toString().padStart(2, '0') +
921
+ modTime.getHours().toString().padStart(2, '0');
922
+
923
+ // 组合版本号:sha前8位-时间yyyyMMddHH
924
+ const finalVersion = version !== 'unknown' ? `${version}-${timeFormat}` : `unknown-${timeFormat}`;
925
+
926
+ const versionInfo = {
927
+ version: finalVersion,
928
+ platform: this.platform,
929
+ source: this.resourcePath
930
+ };
931
+ await fs.writeFile(versionFile, JSON.stringify(versionInfo, null, 2), 'utf8');
932
+
933
+ console.error(`[PromptManager] Cached ${fileName} with version ${finalVersion}`);
934
+ } catch (error) {
935
+ console.warn(`[PromptManager] Failed to cache ${fileName}: ${error.message}`);
936
+ }
937
+ }
938
+
939
+ /**
940
+ * 获取工具配置列表
941
+ */
942
+ getToolsConfig() {
943
+ if (!this.isInitialized) {
944
+ throw new Error('PromptManager not initialized. Call initializeSync() first.');
945
+ }
946
+ return this.toolsConfig;
947
+ }
948
+ }
949
+
950
+ // 导出单例实例
951
+ const promptManagerInstance = new PromptManager();
952
+
953
+ // 主要功能函数导出
954
+ export async function initializePrompts() {
955
+ return await promptManagerInstance.initializeSync();
956
+ }
957
+
958
+ export function getToolsConfiguration() {
959
+ return promptManagerInstance.getToolsConfig();
960
+ }
961
+
962
+ export async function getPrompt(fileName) {
963
+ return await promptManagerInstance.getPromptContent(fileName);
964
+ }
965
+
966
+ // 获取prompt版本信息
967
+ export async function getCurrentPromptVersion(fileName) {
968
+ try {
969
+ // 从缓存版本文件中获取版本信息
970
+ const versionFile = path.join(promptManagerInstance.cacheDir, `${fileName}.version`);
971
+ const versionData = await fs.readFile(versionFile, 'utf8');
972
+ const info = JSON.parse(versionData);
973
+ return `${info.version}`;
974
+ } catch {
975
+ return 'Unknown';
976
+ }
977
+ }
978
+
979
+ // 获取详细prompt信息
980
+ export async function getLocalPromptInfo(fileName) {
981
+ const platform = promptManagerInstance.platform;
982
+
983
+ if (platform === 'local') {
984
+ try {
985
+ const filePath = path.join(promptManagerInstance.promptPath, fileName);
986
+ const stat = await fs.stat(filePath);
987
+
988
+ // 本地模式也使用相同的版本格式:local-yyyyMMddHH
989
+ const modTime = new Date(stat.mtimeMs);
990
+ const timeFormat = modTime.getFullYear().toString() +
991
+ (modTime.getMonth() + 1).toString().padStart(2, '0') +
992
+ modTime.getDate().toString().padStart(2, '0') +
993
+ modTime.getHours().toString().padStart(2, '0');
994
+ const localVersion = `local-${timeFormat}`;
995
+
996
+ return {
997
+ version: localVersion,
998
+ platform: 'local',
999
+ source: promptManagerInstance.promptPath
1000
+ };
1001
+ } catch (error) {
1002
+ return {
1003
+ version: `unknown`,
1004
+ platform: 'local',
1005
+ source: promptManagerInstance.promptPath
1006
+ };
1007
+ }
1008
+ } else {
1009
+ const versionFile = path.join(promptManagerInstance.cacheDir, `${fileName}.version`);
1010
+ try {
1011
+ const versionData = await fs.readFile(versionFile, 'utf8');
1012
+ const info = JSON.parse(versionData);
1013
+ return {
1014
+ version: info.version,
1015
+ platform: info.platform,
1016
+ source: info.source
1017
+ };
1018
+ } catch (error) {
1019
+ return {
1020
+ version: 'unknown',
1021
+ platform: 'remote',
1022
+ source: promptManagerInstance.promptPath
1023
+ };
1024
+ }
1025
+ }
1026
+ }
1027
+