autosnippet 3.0.0 → 3.0.2

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.
Files changed (290) hide show
  1. package/README.md +230 -324
  2. package/bin/api-server.js +1 -1
  3. package/bin/cli.js +204 -244
  4. package/bin/mcp-server.js +5 -3
  5. package/config/knowledge-base.config.js +132 -132
  6. package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
  7. package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
  8. package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
  9. package/dashboard/dist/index.html +3 -3
  10. package/lib/bootstrap.js +8 -8
  11. package/lib/cli/AiScanService.js +86 -40
  12. package/lib/cli/KnowledgeSyncService.js +113 -74
  13. package/lib/cli/SetupService.js +439 -277
  14. package/lib/cli/UpgradeService.js +63 -100
  15. package/lib/core/AstAnalyzer.js +276 -597
  16. package/lib/core/ast/ProjectGraph.js +101 -40
  17. package/lib/core/ast/ensure-grammars.js +232 -0
  18. package/lib/core/ast/index.js +115 -0
  19. package/lib/core/ast/lang-dart.js +661 -0
  20. package/lib/core/ast/lang-go.js +530 -0
  21. package/lib/core/ast/lang-java.js +435 -0
  22. package/lib/core/ast/lang-javascript.js +272 -0
  23. package/lib/core/ast/lang-kotlin.js +423 -0
  24. package/lib/core/ast/lang-objc.js +388 -0
  25. package/lib/core/ast/lang-python.js +371 -0
  26. package/lib/core/ast/lang-swift.js +337 -0
  27. package/lib/core/ast/lang-typescript.js +503 -0
  28. package/lib/core/capability/CapabilityProbe.js +18 -9
  29. package/lib/core/constitution/Constitution.js +2 -3
  30. package/lib/core/constitution/ConstitutionValidator.js +65 -24
  31. package/lib/core/discovery/DartDiscoverer.js +534 -0
  32. package/lib/core/discovery/DiscovererRegistry.js +83 -0
  33. package/lib/core/discovery/GenericDiscoverer.js +225 -0
  34. package/lib/core/discovery/GoDiscoverer.js +541 -0
  35. package/lib/core/discovery/JvmDiscoverer.js +506 -0
  36. package/lib/core/discovery/NodeDiscoverer.js +466 -0
  37. package/lib/core/discovery/ProjectDiscoverer.js +93 -0
  38. package/lib/core/discovery/PythonDiscoverer.js +338 -0
  39. package/lib/core/discovery/SpmDiscoverer.js +5 -0
  40. package/lib/core/discovery/index.js +53 -0
  41. package/lib/core/enhancement/EnhancementPack.js +71 -0
  42. package/lib/core/enhancement/EnhancementRegistry.js +47 -0
  43. package/lib/core/enhancement/android-enhancement.js +102 -0
  44. package/lib/core/enhancement/django-enhancement.js +70 -0
  45. package/lib/core/enhancement/fastapi-enhancement.js +63 -0
  46. package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
  47. package/lib/core/enhancement/go-web-enhancement.js +201 -0
  48. package/lib/core/enhancement/index.js +65 -0
  49. package/lib/core/enhancement/node-server-enhancement.js +88 -0
  50. package/lib/core/enhancement/react-enhancement.js +86 -0
  51. package/lib/core/enhancement/spring-enhancement.js +112 -0
  52. package/lib/core/enhancement/vue-enhancement.js +96 -0
  53. package/lib/core/gateway/Gateway.js +8 -9
  54. package/lib/core/gateway/GatewayActionRegistry.js +1 -1
  55. package/lib/core/permission/PermissionManager.js +12 -8
  56. package/lib/domain/index.js +13 -9
  57. package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
  58. package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
  59. package/lib/domain/knowledge/Lifecycle.js +22 -22
  60. package/lib/domain/knowledge/index.js +9 -12
  61. package/lib/domain/knowledge/values/Constraints.js +31 -21
  62. package/lib/domain/knowledge/values/Content.js +21 -13
  63. package/lib/domain/knowledge/values/Quality.js +31 -18
  64. package/lib/domain/knowledge/values/Reasoning.js +20 -12
  65. package/lib/domain/knowledge/values/Relations.js +37 -25
  66. package/lib/domain/knowledge/values/Stats.js +18 -12
  67. package/lib/domain/knowledge/values/index.js +4 -3
  68. package/lib/domain/snippet/Snippet.js +35 -10
  69. package/lib/external/ai/AiFactory.js +48 -16
  70. package/lib/external/ai/AiProvider.js +184 -90
  71. package/lib/external/ai/providers/ClaudeProvider.js +25 -12
  72. package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
  73. package/lib/external/ai/providers/MockProvider.js +9 -3
  74. package/lib/external/ai/providers/OpenAiProvider.js +51 -29
  75. package/lib/external/mcp/McpServer.js +66 -36
  76. package/lib/external/mcp/errorHandler.js +23 -11
  77. package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
  78. package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
  79. package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
  80. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
  81. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
  82. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
  83. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
  84. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
  85. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
  86. package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
  87. package/lib/external/mcp/handlers/bootstrap.js +691 -168
  88. package/lib/external/mcp/handlers/browse.js +66 -22
  89. package/lib/external/mcp/handlers/candidate.js +118 -35
  90. package/lib/external/mcp/handlers/consolidated.js +49 -17
  91. package/lib/external/mcp/handlers/guard.js +104 -39
  92. package/lib/external/mcp/handlers/knowledge.js +60 -36
  93. package/lib/external/mcp/handlers/search.js +43 -14
  94. package/lib/external/mcp/handlers/skill.js +120 -45
  95. package/lib/external/mcp/handlers/structure.js +240 -86
  96. package/lib/external/mcp/handlers/system.js +42 -12
  97. package/lib/external/mcp/handlers/wiki.js +58 -33
  98. package/lib/external/mcp/tools.js +306 -123
  99. package/lib/http/HttpServer.js +72 -47
  100. package/lib/http/middleware/RateLimiter.js +5 -3
  101. package/lib/http/middleware/errorHandler.js +6 -1
  102. package/lib/http/middleware/requestLogger.js +14 -3
  103. package/lib/http/middleware/roleResolver.js +30 -23
  104. package/lib/http/routes/ai.js +387 -265
  105. package/lib/http/routes/auth.js +81 -61
  106. package/lib/http/routes/candidates.js +430 -320
  107. package/lib/http/routes/commands.js +289 -189
  108. package/lib/http/routes/extract.js +158 -125
  109. package/lib/http/routes/guardRules.js +309 -217
  110. package/lib/http/routes/knowledge.js +213 -154
  111. package/lib/http/routes/modules.js +578 -0
  112. package/lib/http/routes/monitoring.js +6 -6
  113. package/lib/http/routes/recipes.js +104 -93
  114. package/lib/http/routes/search.js +361 -305
  115. package/lib/http/routes/skills.js +145 -98
  116. package/lib/http/routes/snippets.js +42 -30
  117. package/lib/http/routes/spm.js +3 -405
  118. package/lib/http/routes/violations.js +113 -93
  119. package/lib/http/routes/wiki.js +211 -170
  120. package/lib/http/utils/routeHelpers.js +3 -1
  121. package/lib/http/utils/sse-sessions.js +16 -6
  122. package/lib/http/utils/sse.js +15 -5
  123. package/lib/infrastructure/audit/AuditLogger.js +5 -2
  124. package/lib/infrastructure/audit/AuditStore.js +10 -7
  125. package/lib/infrastructure/cache/CacheService.js +3 -1
  126. package/lib/infrastructure/cache/GraphCache.js +8 -4
  127. package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
  128. package/lib/infrastructure/config/ConfigLoader.js +9 -5
  129. package/lib/infrastructure/config/Defaults.js +30 -10
  130. package/lib/infrastructure/config/Paths.js +28 -8
  131. package/lib/infrastructure/config/TriggerSymbol.js +22 -10
  132. package/lib/infrastructure/database/DatabaseConnection.js +15 -10
  133. package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
  134. package/lib/infrastructure/external/ClipboardManager.js +6 -2
  135. package/lib/infrastructure/external/NativeUi.js +50 -43
  136. package/lib/infrastructure/external/OpenBrowser.js +14 -17
  137. package/lib/infrastructure/external/XcodeAutomation.js +14 -258
  138. package/lib/infrastructure/logging/Logger.js +46 -30
  139. package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
  140. package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
  141. package/lib/infrastructure/paths/HeaderResolver.js +25 -9
  142. package/lib/infrastructure/paths/PathFinder.js +34 -12
  143. package/lib/infrastructure/plugin/PluginManager.js +26 -8
  144. package/lib/infrastructure/realtime/RealtimeService.js +2 -2
  145. package/lib/infrastructure/vector/Chunker.js +22 -7
  146. package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
  147. package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
  148. package/lib/infrastructure/vector/VectorStore.js +28 -10
  149. package/lib/injection/ServiceContainer.js +247 -93
  150. package/lib/platform/ios/index.js +63 -0
  151. package/lib/platform/ios/routes/spm.js +437 -0
  152. package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
  153. package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
  154. package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
  155. package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
  156. package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
  157. package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
  158. package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
  159. package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
  160. package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
  161. package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
  162. package/lib/repository/base/BaseRepository.js +7 -9
  163. package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
  164. package/lib/repository/token/TokenUsageStore.js +4 -2
  165. package/lib/service/automation/ActionPipeline.js +1 -1
  166. package/lib/service/automation/AutomationOrchestrator.js +8 -4
  167. package/lib/service/automation/ContextCollector.js +7 -5
  168. package/lib/service/automation/DirectiveDetector.js +23 -16
  169. package/lib/service/automation/FileWatcher.js +112 -56
  170. package/lib/service/automation/TriggerResolver.js +6 -4
  171. package/lib/service/automation/handlers/AlinkHandler.js +24 -12
  172. package/lib/service/automation/handlers/CreateHandler.js +19 -20
  173. package/lib/service/automation/handlers/DraftHandler.js +14 -8
  174. package/lib/service/automation/handlers/GuardHandler.js +93 -63
  175. package/lib/service/automation/handlers/HeaderHandler.js +1 -6
  176. package/lib/service/automation/handlers/SearchHandler.js +155 -88
  177. package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
  178. package/lib/service/candidate/SimilarityService.js +25 -9
  179. package/lib/service/chat/AnalystAgent.js +50 -24
  180. package/lib/service/chat/CandidateGuardrail.js +143 -17
  181. package/lib/service/chat/ChatAgent.js +759 -243
  182. package/lib/service/chat/ContextWindow.js +116 -71
  183. package/lib/service/chat/ConversationStore.js +77 -36
  184. package/lib/service/chat/EpisodicConsolidator.js +47 -23
  185. package/lib/service/chat/HandoffProtocol.js +98 -22
  186. package/lib/service/chat/Memory.js +34 -14
  187. package/lib/service/chat/ProducerAgent.js +40 -20
  188. package/lib/service/chat/ProjectSemanticMemory.js +109 -78
  189. package/lib/service/chat/ReasoningLayer.js +148 -70
  190. package/lib/service/chat/ReasoningTrace.js +44 -32
  191. package/lib/service/chat/TaskPipeline.js +39 -19
  192. package/lib/service/chat/ToolRegistry.js +48 -29
  193. package/lib/service/chat/WorkingMemory.js +44 -18
  194. package/lib/service/chat/tools.js +1096 -494
  195. package/lib/service/context/RecipeExtractor.js +132 -51
  196. package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
  197. package/lib/service/cursor/KnowledgeCompressor.js +25 -22
  198. package/lib/service/cursor/RulesGenerator.js +13 -7
  199. package/lib/service/cursor/SkillsSyncer.js +77 -27
  200. package/lib/service/cursor/TokenBudget.js +2 -2
  201. package/lib/service/cursor/TopicClassifier.js +54 -20
  202. package/lib/service/guard/ComplianceReporter.js +55 -43
  203. package/lib/service/guard/ExclusionManager.js +67 -29
  204. package/lib/service/guard/GuardCheckEngine.js +381 -86
  205. package/lib/service/guard/GuardFeedbackLoop.js +22 -10
  206. package/lib/service/guard/GuardService.js +29 -19
  207. package/lib/service/guard/RuleLearner.js +55 -23
  208. package/lib/service/guard/SourceFileCollector.js +27 -20
  209. package/lib/service/guard/ViolationsStore.js +43 -38
  210. package/lib/service/knowledge/CodeEntityGraph.js +147 -82
  211. package/lib/service/knowledge/ConfidenceRouter.js +12 -10
  212. package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
  213. package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
  214. package/lib/service/knowledge/KnowledgeService.js +222 -112
  215. package/lib/service/module/ModuleService.js +969 -0
  216. package/lib/service/quality/FeedbackCollector.js +27 -15
  217. package/lib/service/quality/QualityScorer.js +78 -24
  218. package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
  219. package/lib/service/recipe/RecipeParser.js +78 -45
  220. package/lib/service/search/CoarseRanker.js +43 -28
  221. package/lib/service/search/CrossEncoderReranker.js +32 -21
  222. package/lib/service/search/InvertedIndex.js +21 -7
  223. package/lib/service/search/MultiSignalRanker.js +90 -28
  224. package/lib/service/search/RetrievalFunnel.js +45 -24
  225. package/lib/service/search/SearchEngine.js +255 -103
  226. package/lib/service/skills/EventAggregator.js +32 -15
  227. package/lib/service/skills/SignalCollector.js +140 -64
  228. package/lib/service/skills/SkillAdvisor.js +79 -42
  229. package/lib/service/skills/SkillHooks.js +16 -14
  230. package/lib/service/snippet/PlaceholderConverter.js +5 -0
  231. package/lib/service/snippet/SnippetFactory.js +116 -99
  232. package/lib/service/snippet/SnippetInstaller.js +234 -62
  233. package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
  234. package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
  235. package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
  236. package/lib/service/wiki/WikiGenerator.js +637 -263
  237. package/lib/shared/DimensionCopyRegistry.js +472 -0
  238. package/lib/shared/LanguageService.js +399 -0
  239. package/lib/shared/PathGuard.js +45 -28
  240. package/lib/shared/RecipeReadinessChecker.js +72 -12
  241. package/lib/shared/constants.js +41 -41
  242. package/lib/shared/errors/BaseError.js +2 -2
  243. package/lib/shared/errors/index.js +4 -4
  244. package/lib/shared/similarity.js +25 -8
  245. package/lib/shared/token-utils.js +6 -2
  246. package/lib/shared/utils/common.js +12 -4
  247. package/package.json +49 -13
  248. package/scripts/bench-real-projects.mjs +256 -0
  249. package/scripts/build-native-ui.js +30 -30
  250. package/scripts/clear-old-vector-index.js +5 -35
  251. package/scripts/clear-vector-cache.js +7 -37
  252. package/scripts/collect-test-project-stats.mjs +160 -0
  253. package/scripts/diagnose-mcp.js +41 -32
  254. package/scripts/ensure-parse-package.js +6 -9
  255. package/scripts/generate-recipe-drafts.js +116 -77
  256. package/scripts/init-db.js +3 -20
  257. package/scripts/init-snippets.js +305 -0
  258. package/scripts/init-vector-db.js +173 -170
  259. package/scripts/install-cursor-skill.js +148 -104
  260. package/scripts/install-full.js +8 -21
  261. package/scripts/install-vscode-copilot.js +146 -145
  262. package/scripts/migrate-md-to-knowledge.mjs +139 -151
  263. package/scripts/postinstall-safe.js +5 -17
  264. package/scripts/recipe-audit.js +106 -82
  265. package/scripts/release.js +283 -323
  266. package/scripts/setup-mcp-config.js +60 -52
  267. package/scripts/verify-context-api.js +20 -20
  268. package/skills/autosnippet-analysis/SKILL.md +10 -6
  269. package/skills/autosnippet-candidates/SKILL.md +27 -26
  270. package/skills/autosnippet-coldstart/SKILL.md +555 -38
  271. package/skills/autosnippet-concepts/SKILL.md +349 -337
  272. package/skills/autosnippet-create/SKILL.md +5 -5
  273. package/skills/autosnippet-reference-dart/SKILL.md +543 -0
  274. package/skills/autosnippet-reference-go/SKILL.md +539 -0
  275. package/skills/autosnippet-reference-java/SKILL.md +534 -0
  276. package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
  277. package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
  278. package/skills/autosnippet-reference-objc/SKILL.md +29 -6
  279. package/skills/autosnippet-reference-python/SKILL.md +800 -0
  280. package/skills/autosnippet-reference-swift/SKILL.md +70 -14
  281. package/skills/autosnippet-structure/SKILL.md +4 -4
  282. package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
  283. package/templates/recipes-setup/README.md +2 -2
  284. package/templates/recipes-setup/_template.md +1 -1
  285. package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
  286. package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
  287. package/resources/asd-entry/main.swift +0 -159
  288. package/scripts/build-asd-entry.js +0 -51
  289. package/scripts/init-xcode-snippets.js +0 -311
  290. package/template.json +0 -39
@@ -0,0 +1,969 @@
1
+ /**
2
+ * ModuleService — 多语言统一模块扫描服务
3
+ *
4
+ * 通过 DiscovererRegistry 自动检测项目类型,
5
+ * 统一 SPM / Node / Go / JVM / Python / Generic 等语言的模块扫描、依赖分析、AI 提取管线。
6
+ * 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。
7
+ */
8
+
9
+ import { basename as _pathBasename, relative, join as _pathJoin, extname as _pathExtname, isAbsolute as _pathIsAbsolute } from 'node:path';
10
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
11
+ import Logger from '../../infrastructure/logging/Logger.js';
12
+ import { getDiscovererRegistry } from '../../core/discovery/index.js';
13
+ import { inferLang } from '../../external/mcp/handlers/LanguageExtensions.js';
14
+
15
+ /** 全局排除目录 */
16
+ const SCAN_EXCLUDE_DIRS = new Set([
17
+ 'node_modules', '.git', 'dist', 'build', '.next', 'Pods',
18
+ 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__',
19
+ '.venv', 'venv', 'target', '.gradle', '.idea', 'out', 'coverage',
20
+ '.cache', '.tox', '.mypy_cache', '.pytest_cache', 'AutoSnippet',
21
+ ]);
22
+
23
+ /** 源码文件扩展名 */
24
+ const SOURCE_CODE_EXTS = new Set([
25
+ '.swift', '.m', '.mm', '.h',
26
+ '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs',
27
+ '.py', '.java', '.kt', '.kts', '.go', '.rs', '.rb',
28
+ '.vue', '.svelte', '.c', '.cpp', '.cs',
29
+ ]);
30
+
31
+ export class ModuleService {
32
+ /** @type {string} */
33
+ #projectRoot;
34
+
35
+ /** @type {import('../../core/discovery/DiscovererRegistry.js').DiscovererRegistry} */
36
+ #registry;
37
+
38
+ /** @type {Array<{ discoverer: import('../../core/discovery/ProjectDiscoverer.js').ProjectDiscoverer, confidence: number }>} */
39
+ #activeDiscoverers = [];
40
+
41
+ /** @type {boolean} */
42
+ #loaded = false;
43
+
44
+ /** @type {import('../../infrastructure/logging/Logger.js').default} */
45
+ #logger;
46
+
47
+ // AI pipeline deps
48
+ #aiFactory;
49
+ #chatAgent;
50
+ #qualityScorer;
51
+ #recipeExtractor;
52
+ #guardCheckEngine;
53
+ #violationsStore;
54
+
55
+ /**
56
+ * @param {string} projectRoot
57
+ * @param {object} [options]
58
+ * @param {object} [options.aiFactory]
59
+ * @param {object} [options.chatAgent]
60
+ * @param {object} [options.qualityScorer]
61
+ * @param {object} [options.recipeExtractor]
62
+ * @param {object} [options.guardCheckEngine]
63
+ * @param {object} [options.violationsStore]
64
+ */
65
+ constructor(projectRoot, options = {}) {
66
+ this.#projectRoot = projectRoot;
67
+ this.#registry = getDiscovererRegistry();
68
+ this.#logger = Logger.getInstance();
69
+ this.#aiFactory = options.aiFactory || null;
70
+ this.#chatAgent = options.chatAgent || null;
71
+ this.#qualityScorer = options.qualityScorer || null;
72
+ this.#recipeExtractor = options.recipeExtractor || null;
73
+ this.#guardCheckEngine = options.guardCheckEngine || null;
74
+ this.#violationsStore = options.violationsStore || null;
75
+ }
76
+
77
+ // ═══════════════════════════════════════════════════════
78
+ // Lifecycle
79
+ // ═══════════════════════════════════════════════════════
80
+
81
+ /**
82
+ * 自动检测项目类型并加载所有匹配的 Discoverer
83
+ */
84
+ async load() {
85
+ if (this.#loaded) return;
86
+
87
+ const matches = await this.#registry.detectAll(this.#projectRoot);
88
+ this.#activeDiscoverers = [];
89
+
90
+ for (const { discoverer, confidence } of matches) {
91
+ try {
92
+ await discoverer.load(this.#projectRoot);
93
+ this.#activeDiscoverers.push({ discoverer, confidence });
94
+ this.#logger.info(
95
+ `[ModuleService] Loaded discoverer: ${discoverer.displayName} (confidence=${confidence.toFixed(2)})`
96
+ );
97
+ } catch (err) {
98
+ this.#logger.warn(
99
+ `[ModuleService] Failed to load discoverer ${discoverer.id}: ${err.message}`
100
+ );
101
+ }
102
+ }
103
+
104
+ if (this.#activeDiscoverers.length === 0) {
105
+ this.#logger.warn('[ModuleService] No discoverer matched, using empty state');
106
+ }
107
+
108
+ this.#loaded = true;
109
+ }
110
+
111
+ /**
112
+ * 清除缓存,重新检测
113
+ */
114
+ async reload() {
115
+ this.#loaded = false;
116
+ this.#activeDiscoverers = [];
117
+ await this.load();
118
+ }
119
+
120
+ /**
121
+ * 确保已加载
122
+ */
123
+ async #ensureLoaded() {
124
+ if (!this.#loaded) {
125
+ await this.load();
126
+ }
127
+ }
128
+
129
+ // ═══════════════════════════════════════════════════════
130
+ // Query — 委托到 Discoverer
131
+ // ═══════════════════════════════════════════════════════
132
+
133
+ /**
134
+ * 列出所有模块/Target(合并所有 Discoverer 的结果)
135
+ * @returns {Promise<import('../../core/discovery/ProjectDiscoverer.js').DiscoveredTarget[]>}
136
+ */
137
+ async listTargets() {
138
+ await this.#ensureLoaded();
139
+
140
+ const allTargets = [];
141
+ const seenNames = new Set();
142
+ let hasRealDiscovererTargets = false;
143
+
144
+ // 第一遍:加载非 generic 的 Discoverer(真实项目结构识别器)
145
+ for (const { discoverer } of this.#activeDiscoverers) {
146
+ if (discoverer.id === 'generic') continue;
147
+ try {
148
+ const targets = await discoverer.listTargets();
149
+ for (const t of targets) {
150
+ const key = `${discoverer.id}::${t.name}`;
151
+ if (seenNames.has(key)) continue;
152
+ seenNames.add(key);
153
+ allTargets.push(this.#normalizeTarget(t, discoverer));
154
+ hasRealDiscovererTargets = true;
155
+ }
156
+ } catch (err) {
157
+ this.#logger.warn(
158
+ `[ModuleService] listTargets failed for ${discoverer.id}: ${err.message}`
159
+ );
160
+ }
161
+ }
162
+
163
+ // 第二遍:仅当没有真实 Discoverer 产出 target 时,才加载 GenericDiscoverer 的结果(兜底)
164
+ if (!hasRealDiscovererTargets) {
165
+ for (const { discoverer } of this.#activeDiscoverers) {
166
+ if (discoverer.id !== 'generic') continue;
167
+ try {
168
+ const targets = await discoverer.listTargets();
169
+ for (const t of targets) {
170
+ const key = `${discoverer.id}::${t.name}`;
171
+ if (seenNames.has(key)) continue;
172
+ seenNames.add(key);
173
+ allTargets.push(this.#normalizeTarget(t, discoverer));
174
+ }
175
+ } catch (err) {
176
+ this.#logger.warn(
177
+ `[ModuleService] listTargets failed for ${discoverer.id}: ${err.message}`
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ return allTargets;
184
+ }
185
+
186
+ /**
187
+ * 统一 target 格式 — 兼容前端 ModuleTarget 接口
188
+ * 各 Discoverer 返回 { name, path, type, language, framework, metadata }
189
+ * 前端还需要 { packageName, packagePath, targetDir, info } 等扩展字段
190
+ */
191
+ #normalizeTarget(t, discoverer) {
192
+ return {
193
+ ...t,
194
+ // 兼容字段 — 如果 discoverer 已设置则保留,否则从通用字段推导
195
+ packageName: t.packageName || t.metadata?.modulePath || t.name,
196
+ packagePath: t.packagePath || t.path || '',
197
+ targetDir: t.targetDir || t.path || '',
198
+ info: t.info || t.metadata || {},
199
+ // discoverer 来源
200
+ discovererId: discoverer.id,
201
+ discovererName: discoverer.displayName,
202
+ // 确保语言字段始终存在
203
+ language: t.language || discoverer.id || 'unknown',
204
+ };
205
+ }
206
+
207
+ /**
208
+ * 获取 Target 的文件列表
209
+ * @param {import('../../core/discovery/ProjectDiscoverer.js').DiscoveredTarget|string} target
210
+ * @returns {Promise<import('../../core/discovery/ProjectDiscoverer.js').DiscoveredFile[]>}
211
+ */
212
+ async getTargetFiles(target) {
213
+ await this.#ensureLoaded();
214
+
215
+ const targetObj = typeof target === 'string' ? { name: target } : target;
216
+ const discovererId = targetObj.discovererId;
217
+
218
+ // 虚拟目录扫描 — 直接收集文件(无需 discoverer)
219
+ if (discovererId === 'folder-scan' && targetObj.path && existsSync(targetObj.path)) {
220
+ return this.#collectFolderFiles(targetObj.path);
221
+ }
222
+
223
+ // 如果指定了 discovererId,直接找对应的 discoverer
224
+ if (discovererId) {
225
+ const entry = this.#activeDiscoverers.find((e) => e.discoverer.id === discovererId);
226
+ if (entry) {
227
+ return entry.discoverer.getTargetFiles(targetObj);
228
+ }
229
+ }
230
+
231
+ // 否则遍历所有 discoverer 找到第一个有该 target 的
232
+ for (const { discoverer } of this.#activeDiscoverers) {
233
+ try {
234
+ const targets = await discoverer.listTargets();
235
+ if (targets.some((t) => t.name === targetObj.name)) {
236
+ return discoverer.getTargetFiles(targetObj);
237
+ }
238
+ } catch {
239
+ continue;
240
+ }
241
+ }
242
+
243
+ // 兜底:如果 target 有 path 属性且目录存在,直接收集
244
+ if (targetObj.path && existsSync(targetObj.path)) {
245
+ this.#logger.info(`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`);
246
+ return this.#collectFolderFiles(targetObj.path);
247
+ }
248
+
249
+ return [];
250
+ }
251
+
252
+ /**
253
+ * 获取依赖关系图
254
+ * @param {{ level?: 'package'|'target' }} [options]
255
+ * @returns {Promise<{ nodes: any[], edges: any[] }>}
256
+ */
257
+ async getDependencyGraph(options = {}) {
258
+ await this.#ensureLoaded();
259
+
260
+ // 合并所有 Discoverer 的依赖图
261
+ const allNodes = [];
262
+ const allEdges = [];
263
+
264
+ for (const { discoverer } of this.#activeDiscoverers) {
265
+ try {
266
+ const graph = await discoverer.getDependencyGraph();
267
+ for (const n of graph.nodes || []) {
268
+ const id = typeof n === 'string' ? n : n.id || n;
269
+ allNodes.push({
270
+ id: `${discoverer.id}::${id}`,
271
+ label: typeof n === 'string' ? n : n.label || n.id,
272
+ type: (typeof n === 'object' && n.type) || options.level || 'module',
273
+ discovererId: discoverer.id,
274
+ ...(typeof n === 'object' && n.fullPath ? { fullPath: n.fullPath } : {}),
275
+ ...(typeof n === 'object' && n.indirect != null ? { indirect: n.indirect } : {}),
276
+ });
277
+ }
278
+ for (const e of graph.edges || []) {
279
+ allEdges.push({
280
+ from: `${discoverer.id}::${e.from}`,
281
+ to: `${discoverer.id}::${e.to}`,
282
+ type: e.type || 'depends_on',
283
+ source: discoverer.id,
284
+ });
285
+ }
286
+ } catch (err) {
287
+ this.#logger.warn(
288
+ `[ModuleService] getDependencyGraph failed for ${discoverer.id}: ${err.message}`
289
+ );
290
+ }
291
+ }
292
+
293
+ return {
294
+ nodes: allNodes,
295
+ edges: allEdges,
296
+ projectRoot: this.#projectRoot,
297
+ generatedAt: new Date().toISOString(),
298
+ };
299
+ }
300
+
301
+ /**
302
+ * 项目信息摘要
303
+ */
304
+ getProjectInfo() {
305
+ const discoverers = this.#activeDiscoverers.map((e) => ({
306
+ id: e.discoverer.id,
307
+ name: e.discoverer.displayName,
308
+ confidence: e.confidence,
309
+ }));
310
+
311
+ const languages = [...new Set(discoverers.map((d) => d.id).filter((id) => id !== 'generic'))];
312
+ const primaryDiscoverer = discoverers[0] || null;
313
+
314
+ return {
315
+ projectRoot: this.#projectRoot,
316
+ primaryLanguage: primaryDiscoverer
317
+ ? this.#discovererToLanguage(primaryDiscoverer.id)
318
+ : 'unknown',
319
+ discoverers,
320
+ languages,
321
+ hasSpm: this.#activeDiscoverers.some((d) => d.discoverer.id === 'spm'),
322
+ };
323
+ }
324
+
325
+ // ═══════════════════════════════════════════════════════
326
+ // Scanning — AI Pipeline
327
+ // ═══════════════════════════════════════════════════════
328
+
329
+ /**
330
+ * AI 扫描 Target 发现候选项
331
+ * 完整管线: 读文件 → AI 提取 → Header 解析 → 工具增强
332
+ */
333
+ async scanTarget(target, options = {}) {
334
+ await this.#ensureLoaded();
335
+
336
+ const targetName = typeof target === 'string' ? target : target?.name;
337
+ const onProgress = options.onProgress;
338
+
339
+ // 1. 获取源文件列表
340
+ onProgress?.({ type: 'scan:started', targetName });
341
+ const fileList = await this.getTargetFiles(target);
342
+ if (!fileList || fileList.length === 0) {
343
+ return {
344
+ recipes: [],
345
+ scannedFiles: [],
346
+ message: `No source files found for module: ${targetName}`,
347
+ };
348
+ }
349
+
350
+ const scannedFilesMeta = fileList.map((f) => {
351
+ const filePath = typeof f === 'string' ? f : f.path;
352
+ return { name: _pathBasename(filePath), path: f.relativePath || _pathBasename(filePath) };
353
+ });
354
+ onProgress?.({ type: 'scan:files-loaded', files: scannedFilesMeta, count: fileList.length });
355
+
356
+ // 2. 读取文件内容
357
+ onProgress?.({ type: 'scan:reading', count: fileList.length });
358
+ const files = fileList
359
+ .map((f) => {
360
+ const filePath = typeof f === 'string' ? f : f.path;
361
+ try {
362
+ return {
363
+ name: _pathBasename(filePath),
364
+ path: filePath,
365
+ relativePath: f.relativePath || _pathBasename(filePath),
366
+ content: readFileSync(filePath, 'utf8'),
367
+ };
368
+ } catch (err) {
369
+ this.#logger.warn(`[ModuleService] Failed to read: ${filePath} — ${err.message}`);
370
+ return null;
371
+ }
372
+ })
373
+ .filter(Boolean);
374
+
375
+ if (files.length === 0) {
376
+ return { recipes: [], scannedFiles: [], message: 'All source files unreadable' };
377
+ }
378
+
379
+ const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath }));
380
+ this.#logger.info(`[ModuleService] scanTarget: ${targetName}, ${files.length} files`);
381
+
382
+ // 3. AI 提取
383
+ if (!this.#chatAgent && !this.#aiFactory) {
384
+ return {
385
+ recipes: [],
386
+ scannedFiles,
387
+ noAi: true,
388
+ message: 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。',
389
+ };
390
+ }
391
+
392
+ onProgress?.({ type: 'scan:ai-extracting', fileCount: files.length, targetName });
393
+ let recipes = await this.#aiExtractRecipes(targetName, files);
394
+
395
+ if (!Array.isArray(recipes)) {
396
+ recipes = [];
397
+ }
398
+
399
+ // 3.5 Header 路径解析 + moduleName 注入
400
+ try {
401
+ const PathFinder = await import('../../infrastructure/paths/PathFinder.js');
402
+ const HeaderResolver = await import('../../infrastructure/paths/HeaderResolver.js');
403
+ const targetRootDir = await PathFinder.findTargetRootDir(files[0].path);
404
+ for (const recipe of recipes) {
405
+ const headerList = recipe.headers || [];
406
+ recipe.headerPaths = await Promise.all(
407
+ headerList.map((h) => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
408
+ );
409
+ recipe.moduleName = targetName;
410
+ }
411
+ } catch (err) {
412
+ this.#logger.warn(`[ModuleService] Header resolution failed: ${err.message}`);
413
+ }
414
+
415
+ // 4. 工具增强
416
+ onProgress?.({ type: 'scan:enriching', recipeCount: recipes.length });
417
+ this.#enrichRecipes(recipes);
418
+
419
+ const result = { recipes, scannedFiles };
420
+ if (recipes.length === 0) {
421
+ // 检查是否因为 API Key 缺失导致 mock / 空结果
422
+ const aiInfo = this.#aiFactory
423
+ ? (await import('../../external/ai/AiFactory.js')).getAiConfigInfo()
424
+ : null;
425
+ if (aiInfo && !aiInfo.hasKey) {
426
+ result.noAi = true;
427
+ result.message = 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。';
428
+ } else {
429
+ result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
430
+ }
431
+ }
432
+ onProgress?.({
433
+ type: 'scan:completed',
434
+ recipeCount: recipes.length,
435
+ fileCount: scannedFiles.length,
436
+ });
437
+ return result;
438
+ }
439
+
440
+ /**
441
+ * 全项目扫描 — 遍历所有 Target,AI 提取候选 + Guard 审计
442
+ */
443
+ async scanProject(options = {}) {
444
+ await this.#ensureLoaded();
445
+ this.#logger.info('[ModuleService] scanProject: starting full-project scan');
446
+
447
+ // 1. 列出所有 target
448
+ const allTargets = await this.listTargets();
449
+
450
+ // 2. 收集所有源文件(去重)
451
+ const seenPaths = new Set();
452
+ const allFiles = [];
453
+ const MAX_FILES = options.maxFiles || 200;
454
+
455
+ if (allTargets && allTargets.length > 0) {
456
+ for (const t of allTargets) {
457
+ try {
458
+ const fileList = await this.getTargetFiles(t);
459
+ for (const f of fileList) {
460
+ const fp = typeof f === 'string' ? f : f.path;
461
+ if (seenPaths.has(fp)) continue;
462
+ seenPaths.add(fp);
463
+ try {
464
+ const content = readFileSync(fp, 'utf8');
465
+ allFiles.push({
466
+ name: _pathBasename(fp),
467
+ path: fp,
468
+ relativePath: f.relativePath || _pathBasename(fp),
469
+ content,
470
+ targetName: t.name,
471
+ });
472
+ } catch { /* unreadable */ }
473
+ if (allFiles.length >= MAX_FILES) break;
474
+ }
475
+ } catch (e) {
476
+ this.#logger.warn(`[ModuleService] scanProject: skipping module ${t.name}: ${e.message}`);
477
+ }
478
+ if (allFiles.length >= MAX_FILES) break;
479
+ }
480
+ }
481
+
482
+ // 如果没有 target 收集到文件,回退到目录扫描
483
+ if (allFiles.length === 0) {
484
+ this.#logger.info('[ModuleService] scanProject: No module targets, falling back to directory scan');
485
+ this.#walkProjectForFiles(allFiles, seenPaths, MAX_FILES);
486
+ }
487
+
488
+ this.#logger.info(
489
+ `[ModuleService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} modules`
490
+ );
491
+
492
+ if (allFiles.length === 0) {
493
+ return {
494
+ targets: (allTargets || []).map((t) => t.name),
495
+ recipes: [],
496
+ guardAudit: null,
497
+ scannedFiles: [],
498
+ message: 'No readable source files',
499
+ };
500
+ }
501
+
502
+ const scannedFiles = allFiles.map((f) => ({
503
+ name: f.name,
504
+ path: f.relativePath,
505
+ targetName: f.targetName,
506
+ }));
507
+
508
+ // 3. AI 提取 Recipes
509
+ const allRecipes = [];
510
+ const PER_BATCH_TIMEOUT = options.batchTimeout || 90000;
511
+ const startTime = Date.now();
512
+ const TOTAL_TIMEOUT = options.totalTimeout || 540000;
513
+ let timedOut = false;
514
+
515
+ if (this.#chatAgent || this.#aiFactory) {
516
+ const BATCH_SIZE = options.batchSize || 20;
517
+
518
+ for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
519
+ if (Date.now() - startTime > TOTAL_TIMEOUT) {
520
+ this.#logger.warn(
521
+ `[ModuleService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s`
522
+ );
523
+ timedOut = true;
524
+ break;
525
+ }
526
+ const batch = allFiles.slice(i, i + BATCH_SIZE);
527
+ const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
528
+ try {
529
+ const recipes = await Promise.race([
530
+ this.#aiExtractRecipes(batchLabel, batch),
531
+ new Promise((_, reject) =>
532
+ setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
533
+ ),
534
+ ]);
535
+ if (Array.isArray(recipes)) {
536
+ allRecipes.push(...recipes);
537
+ }
538
+ } catch (err) {
539
+ this.#logger.warn(
540
+ `[ModuleService] scanProject batch ${batchLabel} failed: ${err.message}`
541
+ );
542
+ }
543
+ }
544
+
545
+ this.#enrichRecipes(allRecipes);
546
+ }
547
+
548
+ // 4. Guard 审计
549
+ let guardAudit = null;
550
+ if (this.#guardCheckEngine) {
551
+ try {
552
+ const guardFiles = allFiles.map((f) => ({ path: f.path, content: f.content }));
553
+ guardAudit = this.#guardCheckEngine.auditFiles(guardFiles, { scope: 'project' });
554
+
555
+ if (this.#violationsStore && guardAudit.files) {
556
+ for (const fileResult of guardAudit.files) {
557
+ if (fileResult.violations.length > 0) {
558
+ this.#violationsStore.appendRun({
559
+ filePath: fileResult.filePath,
560
+ violations: fileResult.violations,
561
+ summary: `Project scan: ${fileResult.summary.errors} errors, ${fileResult.summary.warnings} warnings`,
562
+ });
563
+ }
564
+ }
565
+ }
566
+ } catch (e) {
567
+ this.#logger.warn(`[ModuleService] Guard audit failed: ${e.message}`);
568
+ }
569
+ }
570
+
571
+ this.#logger.info(
572
+ `[ModuleService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`
573
+ );
574
+
575
+ return {
576
+ targets: allTargets.map((t) => t.name),
577
+ recipes: allRecipes,
578
+ guardAudit,
579
+ scannedFiles,
580
+ partial: timedOut,
581
+ };
582
+ }
583
+
584
+ /**
585
+ * 刷新模块映射(替代 updateDependencyMap)
586
+ */
587
+ async updateModuleMap(options = {}) {
588
+ // 重新加载 discoverer
589
+ await this.reload();
590
+ const targets = await this.listTargets();
591
+ const graph = await this.getDependencyGraph();
592
+
593
+ return {
594
+ success: true,
595
+ message: `Module map updated (${targets.length} modules)`,
596
+ targets: targets.length,
597
+ edges: (graph.edges || []).length,
598
+ projectRoot: this.#projectRoot,
599
+ };
600
+ }
601
+
602
+ // ═══════════════════════════════════════════════════════
603
+ // Folder Scanning — 目录浏览与手动扫描
604
+ // ═══════════════════════════════════════════════════════
605
+
606
+ /**
607
+ * 浏览项目目录结构 — 供前端目录选择器使用
608
+ * @param {string} [basePath=''] - 相对于项目根目录的起始路径
609
+ * @param {number} [maxDepth=2] - 最大递归深度
610
+ * @returns {Promise<Array<{ name: string, path: string, depth: number, language: string, sourceFileCount: number, hasSourceFiles: boolean }>>}
611
+ */
612
+ async browseDirectories(basePath = '', maxDepth = 2) {
613
+ const root = basePath
614
+ ? _pathJoin(this.#projectRoot, basePath)
615
+ : this.#projectRoot;
616
+
617
+ if (!existsSync(root)) {
618
+ return [];
619
+ }
620
+
621
+ const dirs = [];
622
+ this.#walkDirsForBrowse(root, dirs, 0, maxDepth);
623
+ return dirs;
624
+ }
625
+
626
+ /**
627
+ * 扫描任意文件夹 — 创建虚拟 Target 并走标准 AI 管线
628
+ * 用于 Discoverer 未覆盖的目录(自定义目录名、新语言等)
629
+ * @param {string} folderPath - 相对/绝对路径
630
+ * @param {object} [options] - scanTarget options (onProgress 等)
631
+ * @returns {Promise<{ recipes: any[], scannedFiles: any[], message?: string }>}
632
+ */
633
+ async scanFolder(folderPath, options = {}) {
634
+ await this.#ensureLoaded();
635
+
636
+ const absPath = _pathIsAbsolute(folderPath)
637
+ ? folderPath
638
+ : _pathJoin(this.#projectRoot, folderPath);
639
+
640
+ if (!existsSync(absPath)) {
641
+ throw new Error(`目录不存在: ${folderPath}`);
642
+ }
643
+
644
+ const lang = this.#detectFolderLanguage(absPath);
645
+ const folderName = _pathBasename(absPath);
646
+
647
+ // 构建虚拟 Target — 兼容 ModuleTarget 接口
648
+ const virtualTarget = {
649
+ name: folderName,
650
+ path: absPath,
651
+ packageName: folderName,
652
+ packagePath: absPath,
653
+ targetDir: absPath,
654
+ type: 'directory',
655
+ language: lang,
656
+ discovererId: 'folder-scan',
657
+ discovererName: '目录扫描',
658
+ info: { source: 'manual-folder-scan', originalPath: folderPath },
659
+ isVirtual: true,
660
+ };
661
+
662
+ this.#logger.info(`[ModuleService] scanFolder: ${folderPath} (lang=${lang})`);
663
+ return this.scanTarget(virtualTarget, options);
664
+ }
665
+
666
+ /** 静态语义标准化 */
667
+ static normalizeSemanticFields(recipe) {
668
+ return recipe;
669
+ }
670
+
671
+ // ═══════════════════════════════════════════════════════
672
+ // Private Helpers
673
+ // ═══════════════════════════════════════════════════════
674
+
675
+ /**
676
+ * Discoverer ID → 语言映射
677
+ */
678
+ #discovererToLanguage(id) {
679
+ const map = {
680
+ spm: 'swift',
681
+ node: 'javascript',
682
+ go: 'go',
683
+ jvm: 'java',
684
+ python: 'python',
685
+ generic: 'unknown',
686
+ };
687
+ return map[id] || 'unknown';
688
+ }
689
+
690
+ /**
691
+ * AI 提取 Recipes — 统一入口
692
+ */
693
+ async #aiExtractRecipes(targetName, files) {
694
+ const AI_EXTRACT_TIMEOUT = 120_000;
695
+
696
+ try {
697
+ if (this.#chatAgent) {
698
+ const extractPromise = this.#chatAgent.executeTool('extract_recipes', {
699
+ targetName,
700
+ files,
701
+ });
702
+ const timeoutPromise = new Promise((_, reject) =>
703
+ setTimeout(
704
+ () => reject(new Error(`AI extraction timeout (${AI_EXTRACT_TIMEOUT / 1000}s)`)),
705
+ AI_EXTRACT_TIMEOUT
706
+ )
707
+ );
708
+ const result = await Promise.race([extractPromise, timeoutPromise]);
709
+ if (result?.error) {
710
+ // API Key 缺失属于配置问题,降为 info 级别避免频繁报错
711
+ if (/API_KEY_MISSING|API.Key.未配置|unregistered callers/i.test(result.error)) {
712
+ this.#logger.info(`[ModuleService] AI 未启用(未配置 API Key),跳过 AI 提取。`);
713
+ } else {
714
+ this.#logger.warn(`[ModuleService] AI extraction error: ${result.error}`);
715
+ }
716
+ return [];
717
+ }
718
+ return result?.recipes || [];
719
+ }
720
+
721
+ if (this.#aiFactory) {
722
+ const {
723
+ getProviderWithFallback,
724
+ isGeoOrProviderError,
725
+ getAvailableFallbacks,
726
+ createProvider,
727
+ } = this.#aiFactory;
728
+ let ai = await getProviderWithFallback();
729
+ if (!ai) return [];
730
+
731
+ // 加载语言参考 Skill
732
+ let extractOpts = {};
733
+ try {
734
+ const { loadBootstrapSkills } = await import('../../external/mcp/handlers/bootstrap.js');
735
+ const langProfile = ai._detectLanguageProfile?.(files);
736
+ const primaryLang = langProfile?.primaryLanguage;
737
+ if (primaryLang) {
738
+ const skillCtx = loadBootstrapSkills(primaryLang);
739
+ if (skillCtx.languageSkill) {
740
+ extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
741
+ }
742
+ }
743
+ } catch { /* Skills not available */ }
744
+
745
+ try {
746
+ return await ai.extractRecipes(targetName, files, extractOpts);
747
+ } catch (primaryErr) {
748
+ if (isGeoOrProviderError(primaryErr)) {
749
+ const currentProvider = (process.env.ASD_AI_PROVIDER || 'google').toLowerCase();
750
+ const fallbacks = getAvailableFallbacks(currentProvider);
751
+ for (const fbName of fallbacks) {
752
+ try {
753
+ ai = createProvider({ provider: fbName });
754
+ return await ai.extractRecipes(targetName, files, extractOpts);
755
+ } catch (fbErr) {
756
+ this.#logger.warn(`[ModuleService] fallback "${fbName}" failed: ${fbErr.message}`);
757
+ }
758
+ }
759
+ }
760
+ throw primaryErr;
761
+ }
762
+ }
763
+ } catch (err) {
764
+ if (err.code === 'API_KEY_MISSING') {
765
+ this.#logger.info(`[ModuleService] AI 未启用(未配置 API Key),跳过 AI 提取。`);
766
+ } else {
767
+ this.#logger.warn(`[ModuleService] AI extraction failed: ${err.message}`);
768
+ }
769
+ }
770
+
771
+ return [];
772
+ }
773
+
774
+ /**
775
+ * 质量评分 enrichment
776
+ */
777
+ #enrichRecipes(recipes) {
778
+ for (const recipe of recipes) {
779
+ if (!recipe.quality && this.#qualityScorer) {
780
+ try {
781
+ const scoreResult = this.#qualityScorer.score(recipe);
782
+ recipe.quality = {
783
+ completeness: 0,
784
+ adaptation: 0,
785
+ documentation: 0,
786
+ overall: scoreResult.score ?? 0,
787
+ grade: scoreResult.grade || '',
788
+ };
789
+ } catch (e) {
790
+ this.#logger.debug(`[ModuleService] QualityScorer failed: ${e.message}`);
791
+ }
792
+ }
793
+ }
794
+ }
795
+
796
+ /**
797
+ * 目录遍历 — 浏览子目录结构
798
+ */
799
+ #walkDirsForBrowse(dir, dirs, depth, maxDepth) {
800
+ if (depth >= maxDepth) return;
801
+ try {
802
+ const entries = readdirSync(dir, { withFileTypes: true });
803
+ for (const entry of entries) {
804
+ if (!entry.isDirectory()) continue;
805
+ if (entry.name.startsWith('.')) continue;
806
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
807
+
808
+ const fullPath = _pathJoin(dir, entry.name);
809
+ const relativePath = relative(this.#projectRoot, fullPath);
810
+
811
+ // 递归统计源码文件数(覆盖 Java/Go 等深层包目录结构)
812
+ const sourceFileCount = this.#countSourceFilesDeep(fullPath, 8);
813
+
814
+ // 快速检测主要语言
815
+ const lang = sourceFileCount > 0 ? this.#detectFolderLanguage(fullPath) : 'unknown';
816
+
817
+ dirs.push({
818
+ name: entry.name,
819
+ path: relativePath,
820
+ depth,
821
+ language: lang,
822
+ sourceFileCount,
823
+ hasSourceFiles: sourceFileCount > 0,
824
+ });
825
+
826
+ this.#walkDirsForBrowse(fullPath, dirs, depth + 1, maxDepth);
827
+ }
828
+ } catch { /* skip */ }
829
+ }
830
+
831
+ /**
832
+ * 递归统计目录下源码文件数(限深度 + 上限 999 防止超大目录卡顿)
833
+ */
834
+ #countSourceFilesDeep(dir, maxDepth, depth = 0) {
835
+ if (depth >= maxDepth) return 0;
836
+ let count = 0;
837
+ try {
838
+ const entries = readdirSync(dir, { withFileTypes: true });
839
+ for (const e of entries) {
840
+ if (e.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(e.name).toLowerCase())) {
841
+ count++;
842
+ } else if (e.isDirectory() && !e.name.startsWith('.') && !SCAN_EXCLUDE_DIRS.has(e.name)) {
843
+ count += this.#countSourceFilesDeep(_pathJoin(dir, e.name), maxDepth, depth + 1);
844
+ }
845
+ if (count >= 999) return count;
846
+ }
847
+ } catch { /* skip */ }
848
+ return count;
849
+ }
850
+
851
+ /**
852
+ * 从目录收集源码文件列表
853
+ */
854
+ #collectFolderFiles(dirPath, maxDepth = 15) {
855
+ const files = [];
856
+ this.#walkCollectSourceFiles(dirPath, dirPath, files, 0, maxDepth);
857
+ return files;
858
+ }
859
+
860
+ /**
861
+ * 递归收集源码文件
862
+ */
863
+ #walkCollectSourceFiles(dir, rootDir, files, depth, maxDepth) {
864
+ if (depth > maxDepth || files.length > 500) return;
865
+ try {
866
+ const entries = readdirSync(dir, { withFileTypes: true });
867
+ for (const entry of entries) {
868
+ if (entry.name.startsWith('.')) continue;
869
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
870
+
871
+ const fullPath = _pathJoin(dir, entry.name);
872
+ if (entry.isDirectory()) {
873
+ this.#walkCollectSourceFiles(fullPath, rootDir, files, depth + 1, maxDepth);
874
+ } else if (entry.isFile()) {
875
+ const ext = _pathExtname(entry.name).toLowerCase();
876
+ if (SOURCE_CODE_EXTS.has(ext)) {
877
+ files.push({
878
+ name: entry.name,
879
+ path: fullPath,
880
+ relativePath: relative(rootDir, fullPath),
881
+ language: inferLang(entry.name) || 'unknown',
882
+ });
883
+ }
884
+ }
885
+ }
886
+ } catch { /* skip */ }
887
+ }
888
+
889
+ /**
890
+ * 检测目录主要编程语言
891
+ */
892
+ #detectFolderLanguage(dirPath) {
893
+ const langCount = {};
894
+ try {
895
+ const entries = readdirSync(dirPath, { withFileTypes: true });
896
+ for (const entry of entries) {
897
+ if (!entry.isFile()) continue;
898
+ const ext = _pathExtname(entry.name).toLowerCase();
899
+ if (!SOURCE_CODE_EXTS.has(ext)) continue;
900
+ const lang = inferLang(entry.name);
901
+ if (lang) {
902
+ langCount[lang] = (langCount[lang] || 0) + 1;
903
+ }
904
+ }
905
+ } catch { /* skip */ }
906
+
907
+ let maxLang = 'unknown';
908
+ let maxCount = 0;
909
+ for (const [lang, count] of Object.entries(langCount)) {
910
+ if (count > maxCount) {
911
+ maxCount = count;
912
+ maxLang = lang;
913
+ }
914
+ }
915
+ return maxLang;
916
+ }
917
+
918
+ /**
919
+ * 目录遍历兜底(收集源码文件)
920
+ */
921
+ #walkProjectForFiles(allFiles, seenPaths, maxFiles) {
922
+ const srcDirs = [
923
+ 'Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages',
924
+ 'cmd', 'internal', 'pkg',
925
+ ];
926
+
927
+ const walkDir = (dir, targetName) => {
928
+ if (allFiles.length >= maxFiles) return;
929
+ let entries;
930
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
931
+ for (const ent of entries) {
932
+ if (allFiles.length >= maxFiles) break;
933
+ if (ent.name.startsWith('.')) continue;
934
+ const fp = _pathJoin(dir, ent.name);
935
+ if (ent.isDirectory()) {
936
+ if (SCAN_EXCLUDE_DIRS.has(ent.name)) continue;
937
+ walkDir(fp, targetName);
938
+ } else if (ent.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(ent.name).toLowerCase())) {
939
+ if (seenPaths.has(fp)) continue;
940
+ seenPaths.add(fp);
941
+ try {
942
+ const st = statSync(fp);
943
+ if (st.size > 512 * 1024) continue;
944
+ const content = readFileSync(fp, 'utf8');
945
+ if (content.split('\n').length < 5) continue;
946
+ allFiles.push({
947
+ name: ent.name,
948
+ path: fp,
949
+ relativePath: relative(this.#projectRoot, fp),
950
+ content,
951
+ targetName,
952
+ });
953
+ } catch { /* unreadable */ }
954
+ }
955
+ }
956
+ };
957
+
958
+ for (const dir of srcDirs) {
959
+ const dirPath = _pathJoin(this.#projectRoot, dir);
960
+ if (existsSync(dirPath)) {
961
+ walkDir(dirPath, dir);
962
+ }
963
+ }
964
+
965
+ if (allFiles.length === 0) {
966
+ walkDir(this.#projectRoot, 'root');
967
+ }
968
+ }
969
+ }