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.
- package/README.md +230 -324
- package/bin/api-server.js +1 -1
- package/bin/cli.js +204 -244
- package/bin/mcp-server.js +5 -3
- package/config/knowledge-base.config.js +132 -132
- package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
- package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
- package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +8 -8
- package/lib/cli/AiScanService.js +86 -40
- package/lib/cli/KnowledgeSyncService.js +113 -74
- package/lib/cli/SetupService.js +439 -277
- package/lib/cli/UpgradeService.js +63 -100
- package/lib/core/AstAnalyzer.js +276 -597
- package/lib/core/ast/ProjectGraph.js +101 -40
- package/lib/core/ast/ensure-grammars.js +232 -0
- package/lib/core/ast/index.js +115 -0
- package/lib/core/ast/lang-dart.js +661 -0
- package/lib/core/ast/lang-go.js +530 -0
- package/lib/core/ast/lang-java.js +435 -0
- package/lib/core/ast/lang-javascript.js +272 -0
- package/lib/core/ast/lang-kotlin.js +423 -0
- package/lib/core/ast/lang-objc.js +388 -0
- package/lib/core/ast/lang-python.js +371 -0
- package/lib/core/ast/lang-swift.js +337 -0
- package/lib/core/ast/lang-typescript.js +503 -0
- package/lib/core/capability/CapabilityProbe.js +18 -9
- package/lib/core/constitution/Constitution.js +2 -3
- package/lib/core/constitution/ConstitutionValidator.js +65 -24
- package/lib/core/discovery/DartDiscoverer.js +534 -0
- package/lib/core/discovery/DiscovererRegistry.js +83 -0
- package/lib/core/discovery/GenericDiscoverer.js +225 -0
- package/lib/core/discovery/GoDiscoverer.js +541 -0
- package/lib/core/discovery/JvmDiscoverer.js +506 -0
- package/lib/core/discovery/NodeDiscoverer.js +466 -0
- package/lib/core/discovery/ProjectDiscoverer.js +93 -0
- package/lib/core/discovery/PythonDiscoverer.js +338 -0
- package/lib/core/discovery/SpmDiscoverer.js +5 -0
- package/lib/core/discovery/index.js +53 -0
- package/lib/core/enhancement/EnhancementPack.js +71 -0
- package/lib/core/enhancement/EnhancementRegistry.js +47 -0
- package/lib/core/enhancement/android-enhancement.js +102 -0
- package/lib/core/enhancement/django-enhancement.js +70 -0
- package/lib/core/enhancement/fastapi-enhancement.js +63 -0
- package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
- package/lib/core/enhancement/go-web-enhancement.js +201 -0
- package/lib/core/enhancement/index.js +65 -0
- package/lib/core/enhancement/node-server-enhancement.js +88 -0
- package/lib/core/enhancement/react-enhancement.js +86 -0
- package/lib/core/enhancement/spring-enhancement.js +112 -0
- package/lib/core/enhancement/vue-enhancement.js +96 -0
- package/lib/core/gateway/Gateway.js +8 -9
- package/lib/core/gateway/GatewayActionRegistry.js +1 -1
- package/lib/core/permission/PermissionManager.js +12 -8
- package/lib/domain/index.js +13 -9
- package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
- package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
- package/lib/domain/knowledge/Lifecycle.js +22 -22
- package/lib/domain/knowledge/index.js +9 -12
- package/lib/domain/knowledge/values/Constraints.js +31 -21
- package/lib/domain/knowledge/values/Content.js +21 -13
- package/lib/domain/knowledge/values/Quality.js +31 -18
- package/lib/domain/knowledge/values/Reasoning.js +20 -12
- package/lib/domain/knowledge/values/Relations.js +37 -25
- package/lib/domain/knowledge/values/Stats.js +18 -12
- package/lib/domain/knowledge/values/index.js +4 -3
- package/lib/domain/snippet/Snippet.js +35 -10
- package/lib/external/ai/AiFactory.js +48 -16
- package/lib/external/ai/AiProvider.js +184 -90
- package/lib/external/ai/providers/ClaudeProvider.js +25 -12
- package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
- package/lib/external/ai/providers/MockProvider.js +9 -3
- package/lib/external/ai/providers/OpenAiProvider.js +51 -29
- package/lib/external/mcp/McpServer.js +66 -36
- package/lib/external/mcp/errorHandler.js +23 -11
- package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
- package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
- package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
- package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
- package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
- package/lib/external/mcp/handlers/bootstrap.js +691 -168
- package/lib/external/mcp/handlers/browse.js +66 -22
- package/lib/external/mcp/handlers/candidate.js +118 -35
- package/lib/external/mcp/handlers/consolidated.js +49 -17
- package/lib/external/mcp/handlers/guard.js +104 -39
- package/lib/external/mcp/handlers/knowledge.js +60 -36
- package/lib/external/mcp/handlers/search.js +43 -14
- package/lib/external/mcp/handlers/skill.js +120 -45
- package/lib/external/mcp/handlers/structure.js +240 -86
- package/lib/external/mcp/handlers/system.js +42 -12
- package/lib/external/mcp/handlers/wiki.js +58 -33
- package/lib/external/mcp/tools.js +306 -123
- package/lib/http/HttpServer.js +72 -47
- package/lib/http/middleware/RateLimiter.js +5 -3
- package/lib/http/middleware/errorHandler.js +6 -1
- package/lib/http/middleware/requestLogger.js +14 -3
- package/lib/http/middleware/roleResolver.js +30 -23
- package/lib/http/routes/ai.js +387 -265
- package/lib/http/routes/auth.js +81 -61
- package/lib/http/routes/candidates.js +430 -320
- package/lib/http/routes/commands.js +289 -189
- package/lib/http/routes/extract.js +158 -125
- package/lib/http/routes/guardRules.js +309 -217
- package/lib/http/routes/knowledge.js +213 -154
- package/lib/http/routes/modules.js +578 -0
- package/lib/http/routes/monitoring.js +6 -6
- package/lib/http/routes/recipes.js +104 -93
- package/lib/http/routes/search.js +361 -305
- package/lib/http/routes/skills.js +145 -98
- package/lib/http/routes/snippets.js +42 -30
- package/lib/http/routes/spm.js +3 -405
- package/lib/http/routes/violations.js +113 -93
- package/lib/http/routes/wiki.js +211 -170
- package/lib/http/utils/routeHelpers.js +3 -1
- package/lib/http/utils/sse-sessions.js +16 -6
- package/lib/http/utils/sse.js +15 -5
- package/lib/infrastructure/audit/AuditLogger.js +5 -2
- package/lib/infrastructure/audit/AuditStore.js +10 -7
- package/lib/infrastructure/cache/CacheService.js +3 -1
- package/lib/infrastructure/cache/GraphCache.js +8 -4
- package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
- package/lib/infrastructure/config/ConfigLoader.js +9 -5
- package/lib/infrastructure/config/Defaults.js +30 -10
- package/lib/infrastructure/config/Paths.js +28 -8
- package/lib/infrastructure/config/TriggerSymbol.js +22 -10
- package/lib/infrastructure/database/DatabaseConnection.js +15 -10
- package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
- package/lib/infrastructure/external/ClipboardManager.js +6 -2
- package/lib/infrastructure/external/NativeUi.js +50 -43
- package/lib/infrastructure/external/OpenBrowser.js +14 -17
- package/lib/infrastructure/external/XcodeAutomation.js +14 -258
- package/lib/infrastructure/logging/Logger.js +46 -30
- package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
- package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
- package/lib/infrastructure/paths/HeaderResolver.js +25 -9
- package/lib/infrastructure/paths/PathFinder.js +34 -12
- package/lib/infrastructure/plugin/PluginManager.js +26 -8
- package/lib/infrastructure/realtime/RealtimeService.js +2 -2
- package/lib/infrastructure/vector/Chunker.js +22 -7
- package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
- package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
- package/lib/infrastructure/vector/VectorStore.js +28 -10
- package/lib/injection/ServiceContainer.js +247 -93
- package/lib/platform/ios/index.js +63 -0
- package/lib/platform/ios/routes/spm.js +437 -0
- package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
- package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
- package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
- package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
- package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
- package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
- package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
- package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
- package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
- package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
- package/lib/repository/base/BaseRepository.js +7 -9
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
- package/lib/repository/token/TokenUsageStore.js +4 -2
- package/lib/service/automation/ActionPipeline.js +1 -1
- package/lib/service/automation/AutomationOrchestrator.js +8 -4
- package/lib/service/automation/ContextCollector.js +7 -5
- package/lib/service/automation/DirectiveDetector.js +23 -16
- package/lib/service/automation/FileWatcher.js +112 -56
- package/lib/service/automation/TriggerResolver.js +6 -4
- package/lib/service/automation/handlers/AlinkHandler.js +24 -12
- package/lib/service/automation/handlers/CreateHandler.js +19 -20
- package/lib/service/automation/handlers/DraftHandler.js +14 -8
- package/lib/service/automation/handlers/GuardHandler.js +93 -63
- package/lib/service/automation/handlers/HeaderHandler.js +1 -6
- package/lib/service/automation/handlers/SearchHandler.js +155 -88
- package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
- package/lib/service/candidate/SimilarityService.js +25 -9
- package/lib/service/chat/AnalystAgent.js +50 -24
- package/lib/service/chat/CandidateGuardrail.js +143 -17
- package/lib/service/chat/ChatAgent.js +759 -243
- package/lib/service/chat/ContextWindow.js +116 -71
- package/lib/service/chat/ConversationStore.js +77 -36
- package/lib/service/chat/EpisodicConsolidator.js +47 -23
- package/lib/service/chat/HandoffProtocol.js +98 -22
- package/lib/service/chat/Memory.js +34 -14
- package/lib/service/chat/ProducerAgent.js +40 -20
- package/lib/service/chat/ProjectSemanticMemory.js +109 -78
- package/lib/service/chat/ReasoningLayer.js +148 -70
- package/lib/service/chat/ReasoningTrace.js +44 -32
- package/lib/service/chat/TaskPipeline.js +39 -19
- package/lib/service/chat/ToolRegistry.js +48 -29
- package/lib/service/chat/WorkingMemory.js +44 -18
- package/lib/service/chat/tools.js +1096 -494
- package/lib/service/context/RecipeExtractor.js +132 -51
- package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
- package/lib/service/cursor/KnowledgeCompressor.js +25 -22
- package/lib/service/cursor/RulesGenerator.js +13 -7
- package/lib/service/cursor/SkillsSyncer.js +77 -27
- package/lib/service/cursor/TokenBudget.js +2 -2
- package/lib/service/cursor/TopicClassifier.js +54 -20
- package/lib/service/guard/ComplianceReporter.js +55 -43
- package/lib/service/guard/ExclusionManager.js +67 -29
- package/lib/service/guard/GuardCheckEngine.js +381 -86
- package/lib/service/guard/GuardFeedbackLoop.js +22 -10
- package/lib/service/guard/GuardService.js +29 -19
- package/lib/service/guard/RuleLearner.js +55 -23
- package/lib/service/guard/SourceFileCollector.js +27 -20
- package/lib/service/guard/ViolationsStore.js +43 -38
- package/lib/service/knowledge/CodeEntityGraph.js +147 -82
- package/lib/service/knowledge/ConfidenceRouter.js +12 -10
- package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
- package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
- package/lib/service/knowledge/KnowledgeService.js +222 -112
- package/lib/service/module/ModuleService.js +969 -0
- package/lib/service/quality/FeedbackCollector.js +27 -15
- package/lib/service/quality/QualityScorer.js +78 -24
- package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
- package/lib/service/recipe/RecipeParser.js +78 -45
- package/lib/service/search/CoarseRanker.js +43 -28
- package/lib/service/search/CrossEncoderReranker.js +32 -21
- package/lib/service/search/InvertedIndex.js +21 -7
- package/lib/service/search/MultiSignalRanker.js +90 -28
- package/lib/service/search/RetrievalFunnel.js +45 -24
- package/lib/service/search/SearchEngine.js +255 -103
- package/lib/service/skills/EventAggregator.js +32 -15
- package/lib/service/skills/SignalCollector.js +140 -64
- package/lib/service/skills/SkillAdvisor.js +79 -42
- package/lib/service/skills/SkillHooks.js +16 -14
- package/lib/service/snippet/PlaceholderConverter.js +5 -0
- package/lib/service/snippet/SnippetFactory.js +116 -99
- package/lib/service/snippet/SnippetInstaller.js +234 -62
- package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
- package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
- package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
- package/lib/service/wiki/WikiGenerator.js +637 -263
- package/lib/shared/DimensionCopyRegistry.js +472 -0
- package/lib/shared/LanguageService.js +399 -0
- package/lib/shared/PathGuard.js +45 -28
- package/lib/shared/RecipeReadinessChecker.js +72 -12
- package/lib/shared/constants.js +41 -41
- package/lib/shared/errors/BaseError.js +2 -2
- package/lib/shared/errors/index.js +4 -4
- package/lib/shared/similarity.js +25 -8
- package/lib/shared/token-utils.js +6 -2
- package/lib/shared/utils/common.js +12 -4
- package/package.json +49 -13
- package/scripts/bench-real-projects.mjs +256 -0
- package/scripts/build-native-ui.js +30 -30
- package/scripts/clear-old-vector-index.js +5 -35
- package/scripts/clear-vector-cache.js +7 -37
- package/scripts/collect-test-project-stats.mjs +160 -0
- package/scripts/diagnose-mcp.js +41 -32
- package/scripts/ensure-parse-package.js +6 -9
- package/scripts/generate-recipe-drafts.js +116 -77
- package/scripts/init-db.js +3 -20
- package/scripts/init-snippets.js +305 -0
- package/scripts/init-vector-db.js +173 -170
- package/scripts/install-cursor-skill.js +148 -104
- package/scripts/install-full.js +8 -21
- package/scripts/install-vscode-copilot.js +146 -145
- package/scripts/migrate-md-to-knowledge.mjs +139 -151
- package/scripts/postinstall-safe.js +5 -17
- package/scripts/recipe-audit.js +106 -82
- package/scripts/release.js +283 -323
- package/scripts/setup-mcp-config.js +60 -52
- package/scripts/verify-context-api.js +20 -20
- package/skills/autosnippet-analysis/SKILL.md +10 -6
- package/skills/autosnippet-candidates/SKILL.md +27 -26
- package/skills/autosnippet-coldstart/SKILL.md +555 -38
- package/skills/autosnippet-concepts/SKILL.md +349 -337
- package/skills/autosnippet-create/SKILL.md +5 -5
- package/skills/autosnippet-reference-dart/SKILL.md +543 -0
- package/skills/autosnippet-reference-go/SKILL.md +539 -0
- package/skills/autosnippet-reference-java/SKILL.md +534 -0
- package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
- package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
- package/skills/autosnippet-reference-objc/SKILL.md +29 -6
- package/skills/autosnippet-reference-python/SKILL.md +800 -0
- package/skills/autosnippet-reference-swift/SKILL.md +70 -14
- package/skills/autosnippet-structure/SKILL.md +4 -4
- package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
- package/templates/recipes-setup/README.md +2 -2
- package/templates/recipes-setup/_template.md +1 -1
- package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
- package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
- package/resources/asd-entry/main.swift +0 -159
- package/scripts/build-asd-entry.js +0 -51
- package/scripts/init-xcode-snippets.js +0 -311
- package/template.json +0 -39
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
* 整合 PackageSwiftParser + DependencyGraph + PolicyEngine
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import {
|
|
8
|
+
basename as _pathBasename,
|
|
9
|
+
dirname,
|
|
10
|
+
resolve as pathResolve,
|
|
11
|
+
relative,
|
|
12
|
+
sep,
|
|
13
|
+
} from 'node:path';
|
|
14
|
+
import { GraphCache } from '../../../infrastructure/cache/GraphCache.js';
|
|
15
|
+
import Logger from '../../../infrastructure/logging/Logger.js';
|
|
8
16
|
import { DependencyGraph } from './DependencyGraph.js';
|
|
17
|
+
import { PackageSwiftParser } from './PackageSwiftParser.js';
|
|
9
18
|
import { PolicyEngine } from './PolicyEngine.js';
|
|
10
|
-
import { GraphCache } from '../../infrastructure/cache/GraphCache.js';
|
|
11
|
-
import { basename as _pathBasename, dirname, relative, sep, resolve as pathResolve } from 'node:path';
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
13
19
|
|
|
14
20
|
export class SpmService {
|
|
15
21
|
#parser;
|
|
@@ -62,7 +68,7 @@ export class SpmService {
|
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* 加载并解析 Package.swift,构建依赖图
|
|
65
|
-
* 支持多 Package
|
|
71
|
+
* 支持多 Package 项目
|
|
66
72
|
* 优先从磁盘缓存加载(Package.swift contentHash 匹配即命中)
|
|
67
73
|
*/
|
|
68
74
|
async load() {
|
|
@@ -72,21 +78,23 @@ export class SpmService {
|
|
|
72
78
|
|
|
73
79
|
// ── 收集所有 Package.swift 路径 + 联合 hash ──
|
|
74
80
|
const packagePath = this.#parser.findPackageSwift(this.#projectRoot);
|
|
75
|
-
const allPaths = packagePath
|
|
81
|
+
const allPaths = packagePath
|
|
82
|
+
? [packagePath]
|
|
83
|
+
: this.#parser.findAllPackageSwifts(this.#projectRoot);
|
|
76
84
|
if (allPaths.length === 0) {
|
|
77
85
|
this.#logger.warn('[SpmService] Package.swift 未找到');
|
|
78
86
|
return null;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
const combinedHash = allPaths
|
|
82
|
-
.map(p => this.#graphCache.computeFileHash(p))
|
|
83
|
-
.join(':');
|
|
89
|
+
const combinedHash = allPaths.map((p) => this.#graphCache.computeFileHash(p)).join(':');
|
|
84
90
|
|
|
85
91
|
// ── 尝试命中缓存 ──
|
|
86
92
|
const cached = this.#graphCache.load('spm-graph');
|
|
87
93
|
if (cached && cached.contentHash === combinedHash) {
|
|
88
94
|
this.#restoreFromCache(cached.data);
|
|
89
|
-
this.#logger.info(
|
|
95
|
+
this.#logger.info(
|
|
96
|
+
`[SpmService] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`
|
|
97
|
+
);
|
|
90
98
|
return cached.data.parsedResult;
|
|
91
99
|
}
|
|
92
100
|
|
|
@@ -125,7 +133,7 @@ export class SpmService {
|
|
|
125
133
|
*/
|
|
126
134
|
#loadMultiPackage(allPaths) {
|
|
127
135
|
this.#logger.info(`[SpmService] 发现 ${allPaths.length} 个 Package.swift,逐一解析...`);
|
|
128
|
-
|
|
136
|
+
const mergedTargets = [];
|
|
129
137
|
let lastName = 'multi-package';
|
|
130
138
|
const allParsed = [];
|
|
131
139
|
|
|
@@ -158,7 +166,9 @@ export class SpmService {
|
|
|
158
166
|
|
|
159
167
|
this.#buildPackageDepGraph(allParsed);
|
|
160
168
|
|
|
161
|
-
this.#logger.info(
|
|
169
|
+
this.#logger.info(
|
|
170
|
+
`[SpmService] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`
|
|
171
|
+
);
|
|
162
172
|
return {
|
|
163
173
|
name: lastName,
|
|
164
174
|
targets: mergedTargets,
|
|
@@ -172,17 +182,19 @@ export class SpmService {
|
|
|
172
182
|
#saveToCache(contentHash, parsedResult) {
|
|
173
183
|
const graphJSON = this.#graph.toJSON();
|
|
174
184
|
const targetPackageEntries = [...this.#targetPackageMap.entries()];
|
|
175
|
-
const packageDepEntries = [...this.#packageDepGraph.entries()].map(
|
|
176
|
-
|
|
185
|
+
const packageDepEntries = [...this.#packageDepGraph.entries()].map(([k, v]) => [k, [...v]]);
|
|
186
|
+
|
|
187
|
+
this.#graphCache.save(
|
|
188
|
+
'spm-graph',
|
|
189
|
+
{
|
|
190
|
+
parsedResult,
|
|
191
|
+
graphNodes: graphJSON.nodes,
|
|
192
|
+
graphEdges: graphJSON.edges,
|
|
193
|
+
targetPackageMap: targetPackageEntries,
|
|
194
|
+
packageDepGraph: packageDepEntries,
|
|
195
|
+
},
|
|
196
|
+
{ contentHash }
|
|
177
197
|
);
|
|
178
|
-
|
|
179
|
-
this.#graphCache.save('spm-graph', {
|
|
180
|
-
parsedResult,
|
|
181
|
-
graphNodes: graphJSON.nodes,
|
|
182
|
-
graphEdges: graphJSON.edges,
|
|
183
|
-
targetPackageMap: targetPackageEntries,
|
|
184
|
-
packageDepGraph: packageDepEntries,
|
|
185
|
-
}, { contentHash });
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
/**
|
|
@@ -258,17 +270,25 @@ export class SpmService {
|
|
|
258
270
|
* @returns {boolean}
|
|
259
271
|
*/
|
|
260
272
|
_canReachPackage(fromPkgPath, toPkgPath) {
|
|
261
|
-
if (fromPkgPath === toPkgPath)
|
|
273
|
+
if (fromPkgPath === toPkgPath) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
262
276
|
const visited = new Set();
|
|
263
277
|
const queue = [fromPkgPath];
|
|
264
278
|
while (queue.length > 0) {
|
|
265
279
|
const current = queue.shift();
|
|
266
|
-
if (current === toPkgPath)
|
|
267
|
-
|
|
280
|
+
if (current === toPkgPath) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (visited.has(current)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
268
286
|
visited.add(current);
|
|
269
287
|
const neighbors = this.#packageDepGraph.get(current);
|
|
270
288
|
if (neighbors) {
|
|
271
|
-
for (const n of neighbors)
|
|
289
|
+
for (const n of neighbors) {
|
|
290
|
+
queue.push(n);
|
|
291
|
+
}
|
|
272
292
|
}
|
|
273
293
|
}
|
|
274
294
|
return false;
|
|
@@ -295,7 +315,9 @@ export class SpmService {
|
|
|
295
315
|
*/
|
|
296
316
|
getFixMode() {
|
|
297
317
|
const env = (process.env.ASD_FIX_SPM_DEPS_MODE || '').toLowerCase().trim();
|
|
298
|
-
if (env === 'off' || env === 'suggest' || env === 'fix')
|
|
318
|
+
if (env === 'off' || env === 'suggest' || env === 'fix') {
|
|
319
|
+
return env;
|
|
320
|
+
}
|
|
299
321
|
return 'suggest'; // 默认仅提示模式
|
|
300
322
|
}
|
|
301
323
|
|
|
@@ -398,7 +420,7 @@ export class SpmService {
|
|
|
398
420
|
return { ok: false, changed: false, error: 'Package.swift not found' };
|
|
399
421
|
}
|
|
400
422
|
|
|
401
|
-
|
|
423
|
+
const content = readFileSync(packagePath, 'utf8');
|
|
402
424
|
|
|
403
425
|
// ── 1. 构建依赖 token ──
|
|
404
426
|
let depToken;
|
|
@@ -464,7 +486,9 @@ export class SpmService {
|
|
|
464
486
|
this.#graph.addEdge(from, to);
|
|
465
487
|
this.#parser.clearCache();
|
|
466
488
|
|
|
467
|
-
this.#logger.info(
|
|
489
|
+
this.#logger.info(
|
|
490
|
+
`[SpmService] 已自动补齐依赖: ${from} -> ${to}${isCrossPackage ? ' (跨包)' : ''} (${packagePath})`
|
|
491
|
+
);
|
|
468
492
|
return { ok: true, changed: true, file: packagePath, crossPackage: isCrossPackage };
|
|
469
493
|
} catch (err) {
|
|
470
494
|
this.#logger.error(`[SpmService] addDependency failed: ${err.message}`);
|
|
@@ -486,7 +510,10 @@ export class SpmService {
|
|
|
486
510
|
|
|
487
511
|
// 检查是否已有对该路径的 .package 声明
|
|
488
512
|
const escapedPath = relPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
489
|
-
const existsRe = new RegExp(
|
|
513
|
+
const existsRe = new RegExp(
|
|
514
|
+
`\\.package\\s*\\(\\s*(?:name\\s*:[^,]*,\\s*)?path\\s*:\\s*"${escapedPath}"`,
|
|
515
|
+
'm'
|
|
516
|
+
);
|
|
490
517
|
if (existsRe.test(content)) {
|
|
491
518
|
return { changed: false, content };
|
|
492
519
|
}
|
|
@@ -527,10 +554,14 @@ export class SpmService {
|
|
|
527
554
|
resolveCurrentTarget(filePath) {
|
|
528
555
|
try {
|
|
529
556
|
const packagePath = this.#parser.findPackageSwift(dirname(filePath));
|
|
530
|
-
if (!packagePath)
|
|
557
|
+
if (!packagePath) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
531
560
|
|
|
532
561
|
const nodes = this.#graph.getNodes();
|
|
533
|
-
if (nodes.length === 0)
|
|
562
|
+
if (nodes.length === 0) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
534
565
|
|
|
535
566
|
const packageDir = dirname(packagePath);
|
|
536
567
|
const rel = relative(packageDir, filePath);
|
|
@@ -539,7 +570,9 @@ export class SpmService {
|
|
|
539
570
|
// 从路径段反向查找第一个匹配的 target(V1 原始逻辑)
|
|
540
571
|
const nodeSet = new Set(nodes);
|
|
541
572
|
for (let i = segments.length - 1; i >= 0; i--) {
|
|
542
|
-
if (nodeSet.has(segments[i]))
|
|
573
|
+
if (nodeSet.has(segments[i])) {
|
|
574
|
+
return segments[i];
|
|
575
|
+
}
|
|
543
576
|
}
|
|
544
577
|
|
|
545
578
|
// 兜底:第一个 target
|
|
@@ -576,7 +609,7 @@ export class SpmService {
|
|
|
576
609
|
edgeCount: this.#graph.edgeCount(),
|
|
577
610
|
levels: Object.fromEntries(levels),
|
|
578
611
|
cycleCount: cycles.length,
|
|
579
|
-
cycles: cycles.map(c => c.join(' → ')),
|
|
612
|
+
cycles: cycles.map((c) => c.join(' → ')),
|
|
580
613
|
};
|
|
581
614
|
}
|
|
582
615
|
|
|
@@ -592,7 +625,7 @@ export class SpmService {
|
|
|
592
625
|
// 如果图中有节点,先用图数据
|
|
593
626
|
const nodes = this.#graph.getNodes();
|
|
594
627
|
if (nodes.length > 0) {
|
|
595
|
-
return [...nodes].map(name => ({
|
|
628
|
+
return [...nodes].map((name) => ({
|
|
596
629
|
name,
|
|
597
630
|
type: 'target',
|
|
598
631
|
}));
|
|
@@ -602,12 +635,12 @@ export class SpmService {
|
|
|
602
635
|
const allPaths = this.#parser.findAllPackageSwifts(this.#projectRoot);
|
|
603
636
|
|
|
604
637
|
if (allPaths.length > 0) {
|
|
605
|
-
const { dirname } = await import('path');
|
|
638
|
+
const { dirname } = await import('node:path');
|
|
606
639
|
const targets = [];
|
|
607
640
|
for (const pkgPath of allPaths) {
|
|
608
641
|
try {
|
|
609
642
|
const parsed = this.#parser.parse(pkgPath);
|
|
610
|
-
if (parsed
|
|
643
|
+
if (parsed?.targets) {
|
|
611
644
|
const pkgDir = dirname(pkgPath);
|
|
612
645
|
for (const t of parsed.targets) {
|
|
613
646
|
targets.push({
|
|
@@ -624,13 +657,24 @@ export class SpmService {
|
|
|
624
657
|
// Skip unparseable packages
|
|
625
658
|
}
|
|
626
659
|
}
|
|
627
|
-
if (targets.length > 0)
|
|
660
|
+
if (targets.length > 0) {
|
|
661
|
+
return targets;
|
|
662
|
+
}
|
|
628
663
|
}
|
|
629
664
|
|
|
630
665
|
// 非 SPM 项目 fallback:将常见源码目录作为虚拟 Target
|
|
631
|
-
const { existsSync } = await import('fs');
|
|
632
|
-
const { join } = await import('path');
|
|
633
|
-
const fallbackDirs = [
|
|
666
|
+
const { existsSync } = await import('node:fs');
|
|
667
|
+
const { join } = await import('node:path');
|
|
668
|
+
const fallbackDirs = [
|
|
669
|
+
'Sources',
|
|
670
|
+
'src',
|
|
671
|
+
'lib',
|
|
672
|
+
'app',
|
|
673
|
+
'pages',
|
|
674
|
+
'components',
|
|
675
|
+
'modules',
|
|
676
|
+
'packages',
|
|
677
|
+
];
|
|
634
678
|
const virtualTargets = [];
|
|
635
679
|
for (const dir of fallbackDirs) {
|
|
636
680
|
if (existsSync(join(this.#projectRoot, dir))) {
|
|
@@ -653,12 +697,12 @@ export class SpmService {
|
|
|
653
697
|
const json = this.#graph.toJSON();
|
|
654
698
|
|
|
655
699
|
return {
|
|
656
|
-
nodes: json.nodes.map(name => ({
|
|
700
|
+
nodes: json.nodes.map((name) => ({
|
|
657
701
|
id: name,
|
|
658
702
|
label: name,
|
|
659
703
|
type: options.level === 'package' ? 'package' : 'target',
|
|
660
704
|
})),
|
|
661
|
-
edges: json.edges.map(e => ({ from: e.from, to: e.to, source: 'spm' })),
|
|
705
|
+
edges: json.edges.map((e) => ({ from: e.from, to: e.to, source: 'spm' })),
|
|
662
706
|
projectRoot: this.#projectRoot,
|
|
663
707
|
generatedAt: new Date().toISOString(),
|
|
664
708
|
};
|
|
@@ -672,7 +716,9 @@ export class SpmService {
|
|
|
672
716
|
*/
|
|
673
717
|
async getTargetFiles(target) {
|
|
674
718
|
const targetName = typeof target === 'string' ? target : target?.name;
|
|
675
|
-
if (!targetName)
|
|
719
|
+
if (!targetName) {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
676
722
|
|
|
677
723
|
// 内存缓存命中
|
|
678
724
|
if (this.#targetFilesCache.has(targetName)) {
|
|
@@ -688,8 +734,8 @@ export class SpmService {
|
|
|
688
734
|
* 实际的文件遍历逻辑(拆出以供缓存用)
|
|
689
735
|
*/
|
|
690
736
|
async #walkTargetFiles(target, targetName) {
|
|
691
|
-
const { existsSync, readdirSync, statSync } = await import('fs');
|
|
692
|
-
const { join, dirname,
|
|
737
|
+
const { existsSync, readdirSync, statSync } = await import('node:fs');
|
|
738
|
+
const { join, dirname, extname } = await import('node:path');
|
|
693
739
|
|
|
694
740
|
// 判断是否为虚拟目录 target(非 SPM fallback)
|
|
695
741
|
const isDirectoryTarget = typeof target === 'object' && target?.type === 'directory';
|
|
@@ -716,9 +762,9 @@ export class SpmService {
|
|
|
716
762
|
for (const pkgPath of allPaths) {
|
|
717
763
|
try {
|
|
718
764
|
const parsed = this.#parser.parse(pkgPath);
|
|
719
|
-
if (parsed?.targets?.some(t => t.name === targetName)) {
|
|
765
|
+
if (parsed?.targets?.some((t) => t.name === targetName)) {
|
|
720
766
|
const pkgDir = dirname(pkgPath);
|
|
721
|
-
const matchTarget = parsed.targets.find(t => t.name === targetName);
|
|
767
|
+
const matchTarget = parsed.targets.find((t) => t.name === targetName);
|
|
722
768
|
if (matchTarget?.path) {
|
|
723
769
|
searchDirs.push(join(pkgDir, matchTarget.path));
|
|
724
770
|
}
|
|
@@ -742,32 +788,86 @@ export class SpmService {
|
|
|
742
788
|
break;
|
|
743
789
|
}
|
|
744
790
|
}
|
|
745
|
-
if (!sourcesDir)
|
|
791
|
+
if (!sourcesDir) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
746
794
|
|
|
747
795
|
// 文件扩展名白名单
|
|
748
796
|
const CODE_EXTS = isDirectoryTarget
|
|
749
|
-
? new Set([
|
|
797
|
+
? new Set([
|
|
798
|
+
'.swift',
|
|
799
|
+
'.m',
|
|
800
|
+
'.mm',
|
|
801
|
+
'.h',
|
|
802
|
+
'.c',
|
|
803
|
+
'.cpp',
|
|
804
|
+
'.js',
|
|
805
|
+
'.ts',
|
|
806
|
+
'.tsx',
|
|
807
|
+
'.jsx',
|
|
808
|
+
'.py',
|
|
809
|
+
'.java',
|
|
810
|
+
'.kt',
|
|
811
|
+
'.go',
|
|
812
|
+
'.rs',
|
|
813
|
+
'.rb',
|
|
814
|
+
'.vue',
|
|
815
|
+
'.mjs',
|
|
816
|
+
'.cjs',
|
|
817
|
+
])
|
|
750
818
|
: new Set(['.swift', '.m', '.h', '.c', '.cpp', '.mm']);
|
|
751
|
-
const SKIP_DIRS = new Set([
|
|
819
|
+
const SKIP_DIRS = new Set([
|
|
820
|
+
'node_modules',
|
|
821
|
+
'.git',
|
|
822
|
+
'dist',
|
|
823
|
+
'build',
|
|
824
|
+
'.next',
|
|
825
|
+
'Pods',
|
|
826
|
+
'Carthage',
|
|
827
|
+
'.build',
|
|
828
|
+
'DerivedData',
|
|
829
|
+
'vendor',
|
|
830
|
+
'__pycache__',
|
|
831
|
+
'.venv',
|
|
832
|
+
'target',
|
|
833
|
+
]);
|
|
752
834
|
const MAX_FILES = 300;
|
|
753
835
|
|
|
754
836
|
const files = [];
|
|
755
837
|
const walk = (dir, rel = '') => {
|
|
756
|
-
if (files.length >= MAX_FILES)
|
|
838
|
+
if (files.length >= MAX_FILES) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
757
841
|
let entries;
|
|
758
|
-
try {
|
|
842
|
+
try {
|
|
843
|
+
entries = readdirSync(dir);
|
|
844
|
+
} catch {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
759
847
|
for (const entry of entries) {
|
|
760
|
-
if (files.length >= MAX_FILES)
|
|
761
|
-
|
|
848
|
+
if (files.length >= MAX_FILES) {
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
if (entry.startsWith('.')) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
762
854
|
const full = join(dir, entry);
|
|
763
855
|
const relPath = rel ? `${rel}/${entry}` : entry;
|
|
764
856
|
let st;
|
|
765
|
-
try {
|
|
857
|
+
try {
|
|
858
|
+
st = statSync(full);
|
|
859
|
+
} catch {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
766
862
|
if (st.isDirectory()) {
|
|
767
|
-
if (SKIP_DIRS.has(entry))
|
|
863
|
+
if (SKIP_DIRS.has(entry)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
768
866
|
walk(full, relPath);
|
|
769
867
|
} else if (CODE_EXTS.has(extname(entry).toLowerCase())) {
|
|
770
|
-
if (st.size > 512 * 1024)
|
|
868
|
+
if (st.size > 512 * 1024) {
|
|
869
|
+
continue; // 跳过 > 512KB
|
|
870
|
+
}
|
|
771
871
|
files.push({ name: entry, path: full, relativePath: relPath, size: st.size });
|
|
772
872
|
}
|
|
773
873
|
}
|
|
@@ -816,10 +916,14 @@ export class SpmService {
|
|
|
816
916
|
onProgress?.({ type: 'scan:started', targetName });
|
|
817
917
|
const fileList = await this.getTargetFiles(target);
|
|
818
918
|
if (!fileList || fileList.length === 0) {
|
|
819
|
-
return {
|
|
919
|
+
return {
|
|
920
|
+
recipes: [],
|
|
921
|
+
scannedFiles: [],
|
|
922
|
+
message: `No source files found for target: ${targetName}`,
|
|
923
|
+
};
|
|
820
924
|
}
|
|
821
925
|
|
|
822
|
-
const scannedFilesMeta = fileList.map(f => {
|
|
926
|
+
const scannedFilesMeta = fileList.map((f) => {
|
|
823
927
|
const filePath = typeof f === 'string' ? f : f.path;
|
|
824
928
|
return { name: _pathBasename(filePath), path: f.relativePath || _pathBasename(filePath) };
|
|
825
929
|
});
|
|
@@ -827,28 +931,39 @@ export class SpmService {
|
|
|
827
931
|
|
|
828
932
|
// 2. 读取文件内容
|
|
829
933
|
onProgress?.({ type: 'scan:reading', count: fileList.length });
|
|
830
|
-
const { readFileSync } = await import('fs');
|
|
831
|
-
const { basename
|
|
832
|
-
const files = fileList
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
934
|
+
const { readFileSync } = await import('node:fs');
|
|
935
|
+
const { basename } = await import('node:path');
|
|
936
|
+
const files = fileList
|
|
937
|
+
.map((f) => {
|
|
938
|
+
const filePath = typeof f === 'string' ? f : f.path;
|
|
939
|
+
try {
|
|
940
|
+
return {
|
|
941
|
+
name: basename(filePath),
|
|
942
|
+
path: filePath,
|
|
943
|
+
relativePath: f.relativePath || basename(filePath),
|
|
944
|
+
content: readFileSync(filePath, 'utf8'),
|
|
945
|
+
};
|
|
946
|
+
} catch (err) {
|
|
947
|
+
this.#logger.warn(`[SpmService] 读取文件失败: ${filePath} — ${err.message}`);
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
.filter(Boolean);
|
|
841
952
|
|
|
842
953
|
if (files.length === 0) {
|
|
843
954
|
return { recipes: [], scannedFiles: [], message: 'All source files unreadable' };
|
|
844
955
|
}
|
|
845
956
|
|
|
846
|
-
const scannedFiles = files.map(f => ({ name: f.name, path: f.relativePath }));
|
|
957
|
+
const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath }));
|
|
847
958
|
this.#logger.info(`[SpmService] scanTarget: ${targetName}, ${files.length} files`);
|
|
848
959
|
|
|
849
960
|
// 3. AI 提取 Recipes(通过 ChatAgent 统一入口)
|
|
850
961
|
if (!this.#chatAgent && !this.#aiFactory) {
|
|
851
|
-
return {
|
|
962
|
+
return {
|
|
963
|
+
recipes: [],
|
|
964
|
+
scannedFiles,
|
|
965
|
+
message: 'AI provider not configured. Please set ASD_AI_PROVIDER.',
|
|
966
|
+
};
|
|
852
967
|
}
|
|
853
968
|
|
|
854
969
|
onProgress?.({ type: 'scan:ai-extracting', fileCount: files.length, targetName });
|
|
@@ -856,9 +971,16 @@ export class SpmService {
|
|
|
856
971
|
let recipes;
|
|
857
972
|
try {
|
|
858
973
|
if (this.#chatAgent) {
|
|
859
|
-
const extractPromise = this.#chatAgent.executeTool('extract_recipes', {
|
|
974
|
+
const extractPromise = this.#chatAgent.executeTool('extract_recipes', {
|
|
975
|
+
targetName,
|
|
976
|
+
files,
|
|
977
|
+
});
|
|
860
978
|
const timeoutPromise = new Promise((_, reject) =>
|
|
861
|
-
setTimeout(
|
|
979
|
+
setTimeout(
|
|
980
|
+
() => reject(new Error(`AI extraction timeout (${AI_EXTRACT_TIMEOUT / 1000}s)`)),
|
|
981
|
+
AI_EXTRACT_TIMEOUT
|
|
982
|
+
)
|
|
983
|
+
);
|
|
862
984
|
const result = await Promise.race([extractPromise, timeoutPromise]);
|
|
863
985
|
if (result?.error) {
|
|
864
986
|
return { recipes: [], scannedFiles, message: result.error };
|
|
@@ -866,7 +988,12 @@ export class SpmService {
|
|
|
866
988
|
recipes = result?.recipes || [];
|
|
867
989
|
} else {
|
|
868
990
|
// 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
|
|
869
|
-
const {
|
|
991
|
+
const {
|
|
992
|
+
getProviderWithFallback,
|
|
993
|
+
isGeoOrProviderError,
|
|
994
|
+
getAvailableFallbacks,
|
|
995
|
+
createProvider,
|
|
996
|
+
} = this.#aiFactory;
|
|
870
997
|
let ai = await getProviderWithFallback();
|
|
871
998
|
if (!ai) {
|
|
872
999
|
return { recipes: [], scannedFiles, message: 'AI provider not available' };
|
|
@@ -875,7 +1002,7 @@ export class SpmService {
|
|
|
875
1002
|
// 加载语言参考 Skill 注入 AI 提取 prompt
|
|
876
1003
|
let extractOpts = {};
|
|
877
1004
|
try {
|
|
878
|
-
const { loadBootstrapSkills } = await import('
|
|
1005
|
+
const { loadBootstrapSkills } = await import('../../../external/mcp/handlers/bootstrap.js');
|
|
879
1006
|
const langProfile = ai._detectLanguageProfile?.(files);
|
|
880
1007
|
const primaryLang = langProfile?.primaryLanguage;
|
|
881
1008
|
if (primaryLang) {
|
|
@@ -884,7 +1011,9 @@ export class SpmService {
|
|
|
884
1011
|
extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
|
|
885
1012
|
}
|
|
886
1013
|
}
|
|
887
|
-
} catch {
|
|
1014
|
+
} catch {
|
|
1015
|
+
/* Skills not available */
|
|
1016
|
+
}
|
|
888
1017
|
|
|
889
1018
|
try {
|
|
890
1019
|
recipes = await ai.extractRecipes(targetName, files, extractOpts);
|
|
@@ -903,7 +1032,9 @@ export class SpmService {
|
|
|
903
1032
|
this.#logger.warn(`[SpmService] fallback "${fbName}" failed: ${fbErr.message}`);
|
|
904
1033
|
}
|
|
905
1034
|
}
|
|
906
|
-
if (!fallbackOk)
|
|
1035
|
+
if (!fallbackOk) {
|
|
1036
|
+
throw primaryErr;
|
|
1037
|
+
}
|
|
907
1038
|
} else {
|
|
908
1039
|
throw primaryErr;
|
|
909
1040
|
}
|
|
@@ -914,17 +1045,19 @@ export class SpmService {
|
|
|
914
1045
|
return { recipes: [], scannedFiles, message: `AI extraction failed: ${err.message}` };
|
|
915
1046
|
}
|
|
916
1047
|
|
|
917
|
-
if (!Array.isArray(recipes))
|
|
1048
|
+
if (!Array.isArray(recipes)) {
|
|
1049
|
+
recipes = [];
|
|
1050
|
+
}
|
|
918
1051
|
|
|
919
1052
|
// 3.5 Header 路径解析 + moduleName 注入
|
|
920
1053
|
try {
|
|
921
|
-
const PathFinder = await import('
|
|
922
|
-
const HeaderResolver = await import('
|
|
1054
|
+
const PathFinder = await import('../../../infrastructure/paths/PathFinder.js');
|
|
1055
|
+
const HeaderResolver = await import('../../../infrastructure/paths/HeaderResolver.js');
|
|
923
1056
|
const targetRootDir = await PathFinder.findTargetRootDir(files[0].path);
|
|
924
1057
|
for (const recipe of recipes) {
|
|
925
1058
|
const headerList = recipe.headers || [];
|
|
926
1059
|
recipe.headerPaths = await Promise.all(
|
|
927
|
-
headerList.map(h => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
|
|
1060
|
+
headerList.map((h) => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
|
|
928
1061
|
);
|
|
929
1062
|
recipe.moduleName = targetName;
|
|
930
1063
|
}
|
|
@@ -938,9 +1071,21 @@ export class SpmService {
|
|
|
938
1071
|
|
|
939
1072
|
const result = { recipes, scannedFiles };
|
|
940
1073
|
if (recipes.length === 0) {
|
|
941
|
-
|
|
1074
|
+
const aiInfo = this.#aiFactory
|
|
1075
|
+
? (await import('../../../external/ai/AiFactory.js')).getAiConfigInfo()
|
|
1076
|
+
: null;
|
|
1077
|
+
if (aiInfo && !aiInfo.hasKey) {
|
|
1078
|
+
result.noAi = true;
|
|
1079
|
+
result.message = 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。';
|
|
1080
|
+
} else {
|
|
1081
|
+
result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
|
|
1082
|
+
}
|
|
942
1083
|
}
|
|
943
|
-
onProgress?.({
|
|
1084
|
+
onProgress?.({
|
|
1085
|
+
type: 'scan:completed',
|
|
1086
|
+
recipeCount: recipes.length,
|
|
1087
|
+
fileCount: scannedFiles.length,
|
|
1088
|
+
});
|
|
944
1089
|
return result;
|
|
945
1090
|
}
|
|
946
1091
|
|
|
@@ -951,15 +1096,15 @@ export class SpmService {
|
|
|
951
1096
|
async scanProject(options = {}) {
|
|
952
1097
|
this.#logger.info('[SpmService] scanProject: starting full-project scan');
|
|
953
1098
|
|
|
954
|
-
const { readFileSync, existsSync, readdirSync, statSync } = await import('fs');
|
|
955
|
-
const { basename: bn, join, extname, relative } = await import('path');
|
|
1099
|
+
const { readFileSync, existsSync, readdirSync, statSync } = await import('node:fs');
|
|
1100
|
+
const { basename: bn, join, extname, relative } = await import('node:path');
|
|
956
1101
|
|
|
957
1102
|
// 1. 列出所有 target
|
|
958
1103
|
const allTargets = await this.listTargets();
|
|
959
1104
|
|
|
960
1105
|
// 2. 收集所有源文件(去重)
|
|
961
1106
|
const seenPaths = new Set();
|
|
962
|
-
const allFiles = [];
|
|
1107
|
+
const allFiles = []; // { name, path, relativePath, content, targetName }
|
|
963
1108
|
const MAX_FILES = options.maxFiles || 200;
|
|
964
1109
|
|
|
965
1110
|
if (allTargets && allTargets.length > 0) {
|
|
@@ -969,7 +1114,9 @@ export class SpmService {
|
|
|
969
1114
|
const fileList = await this.getTargetFiles(t);
|
|
970
1115
|
for (const f of fileList) {
|
|
971
1116
|
const fp = typeof f === 'string' ? f : f.path;
|
|
972
|
-
if (seenPaths.has(fp))
|
|
1117
|
+
if (seenPaths.has(fp)) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
973
1120
|
seenPaths.add(fp);
|
|
974
1121
|
try {
|
|
975
1122
|
const content = readFileSync(fp, 'utf8');
|
|
@@ -980,40 +1127,105 @@ export class SpmService {
|
|
|
980
1127
|
content,
|
|
981
1128
|
targetName: t.name,
|
|
982
1129
|
});
|
|
983
|
-
} catch {
|
|
984
|
-
|
|
1130
|
+
} catch {
|
|
1131
|
+
/* unreadable */
|
|
1132
|
+
}
|
|
1133
|
+
if (allFiles.length >= MAX_FILES) {
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
985
1136
|
}
|
|
986
1137
|
} catch (e) {
|
|
987
1138
|
this.#logger.warn(`[SpmService] scanProject: skipping target ${t.name}: ${e.message}`);
|
|
988
1139
|
}
|
|
989
|
-
if (allFiles.length >= MAX_FILES)
|
|
1140
|
+
if (allFiles.length >= MAX_FILES) {
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
990
1143
|
}
|
|
991
1144
|
} else {
|
|
992
1145
|
// 非 SPM 项目:直接扫描常见源码目录(fallback)
|
|
993
1146
|
this.#logger.info('[SpmService] scanProject: No SPM targets, falling back to directory scan');
|
|
994
|
-
const CODE_EXTS = new Set([
|
|
995
|
-
|
|
996
|
-
|
|
1147
|
+
const CODE_EXTS = new Set([
|
|
1148
|
+
'.swift',
|
|
1149
|
+
'.m',
|
|
1150
|
+
'.mm',
|
|
1151
|
+
'.h',
|
|
1152
|
+
'.js',
|
|
1153
|
+
'.ts',
|
|
1154
|
+
'.tsx',
|
|
1155
|
+
'.jsx',
|
|
1156
|
+
'.py',
|
|
1157
|
+
'.java',
|
|
1158
|
+
'.kt',
|
|
1159
|
+
'.go',
|
|
1160
|
+
'.rs',
|
|
1161
|
+
'.rb',
|
|
1162
|
+
'.vue',
|
|
1163
|
+
'.mjs',
|
|
1164
|
+
'.cjs',
|
|
1165
|
+
]);
|
|
1166
|
+
const SKIP_DIRS = new Set([
|
|
1167
|
+
'node_modules',
|
|
1168
|
+
'.git',
|
|
1169
|
+
'dist',
|
|
1170
|
+
'build',
|
|
1171
|
+
'.next',
|
|
1172
|
+
'Pods',
|
|
1173
|
+
'Carthage',
|
|
1174
|
+
'.build',
|
|
1175
|
+
'DerivedData',
|
|
1176
|
+
'vendor',
|
|
1177
|
+
'__pycache__',
|
|
1178
|
+
'.venv',
|
|
1179
|
+
'target',
|
|
1180
|
+
]);
|
|
1181
|
+
const srcDirs = [
|
|
1182
|
+
'Sources',
|
|
1183
|
+
'src',
|
|
1184
|
+
'lib',
|
|
1185
|
+
'app',
|
|
1186
|
+
'pages',
|
|
1187
|
+
'components',
|
|
1188
|
+
'modules',
|
|
1189
|
+
'packages',
|
|
1190
|
+
];
|
|
997
1191
|
|
|
998
1192
|
const walkDir = (dir, targetName) => {
|
|
999
|
-
if (allFiles.length >= MAX_FILES)
|
|
1193
|
+
if (allFiles.length >= MAX_FILES) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1000
1196
|
let entries;
|
|
1001
|
-
try {
|
|
1197
|
+
try {
|
|
1198
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1199
|
+
} catch {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1002
1202
|
for (const ent of entries) {
|
|
1003
|
-
if (allFiles.length >= MAX_FILES)
|
|
1004
|
-
|
|
1203
|
+
if (allFiles.length >= MAX_FILES) {
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
if (ent.name.startsWith('.')) {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1005
1209
|
const fp = join(dir, ent.name);
|
|
1006
1210
|
if (ent.isDirectory()) {
|
|
1007
|
-
if (SKIP_DIRS.has(ent.name))
|
|
1211
|
+
if (SKIP_DIRS.has(ent.name)) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1008
1214
|
walkDir(fp, targetName);
|
|
1009
1215
|
} else if (ent.isFile() && CODE_EXTS.has(extname(ent.name).toLowerCase())) {
|
|
1010
|
-
if (seenPaths.has(fp))
|
|
1216
|
+
if (seenPaths.has(fp)) {
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1011
1219
|
seenPaths.add(fp);
|
|
1012
1220
|
try {
|
|
1013
1221
|
const st = statSync(fp);
|
|
1014
|
-
if (st.size > 512 * 1024)
|
|
1222
|
+
if (st.size > 512 * 1024) {
|
|
1223
|
+
continue; // 跳过 > 512KB
|
|
1224
|
+
}
|
|
1015
1225
|
const content = readFileSync(fp, 'utf8');
|
|
1016
|
-
if (content.split('\n').length < 5)
|
|
1226
|
+
if (content.split('\n').length < 5) {
|
|
1227
|
+
continue; // 跳过微小文件
|
|
1228
|
+
}
|
|
1017
1229
|
allFiles.push({
|
|
1018
1230
|
name: ent.name,
|
|
1019
1231
|
path: fp,
|
|
@@ -1021,7 +1233,9 @@ export class SpmService {
|
|
|
1021
1233
|
content,
|
|
1022
1234
|
targetName,
|
|
1023
1235
|
});
|
|
1024
|
-
} catch {
|
|
1236
|
+
} catch {
|
|
1237
|
+
/* unreadable */
|
|
1238
|
+
}
|
|
1025
1239
|
}
|
|
1026
1240
|
}
|
|
1027
1241
|
};
|
|
@@ -1039,16 +1253,28 @@ export class SpmService {
|
|
|
1039
1253
|
}
|
|
1040
1254
|
}
|
|
1041
1255
|
|
|
1042
|
-
this.#logger.info(
|
|
1256
|
+
this.#logger.info(
|
|
1257
|
+
`[SpmService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} targets`
|
|
1258
|
+
);
|
|
1043
1259
|
|
|
1044
1260
|
if (allFiles.length === 0) {
|
|
1045
|
-
return {
|
|
1261
|
+
return {
|
|
1262
|
+
targets: (allTargets || []).map((t) => t.name),
|
|
1263
|
+
recipes: [],
|
|
1264
|
+
guardAudit: null,
|
|
1265
|
+
scannedFiles: [],
|
|
1266
|
+
message: 'No readable source files',
|
|
1267
|
+
};
|
|
1046
1268
|
}
|
|
1047
1269
|
|
|
1048
|
-
const scannedFiles = allFiles.map(f => ({
|
|
1270
|
+
const scannedFiles = allFiles.map((f) => ({
|
|
1271
|
+
name: f.name,
|
|
1272
|
+
path: f.relativePath,
|
|
1273
|
+
targetName: f.targetName,
|
|
1274
|
+
}));
|
|
1049
1275
|
|
|
1050
1276
|
// 3. AI 提取 Recipes(全局批量,分批避免 token 超限)— 通过 ChatAgent 统一入口
|
|
1051
|
-
|
|
1277
|
+
const allRecipes = [];
|
|
1052
1278
|
const PER_BATCH_TIMEOUT = options.batchTimeout || 90000; // 每批最多 90 秒
|
|
1053
1279
|
const startTime = Date.now();
|
|
1054
1280
|
const TOTAL_TIMEOUT = options.totalTimeout || 540000; // 总超时 9 分钟(留 1 分钟给 Guard)
|
|
@@ -1061,7 +1287,9 @@ export class SpmService {
|
|
|
1061
1287
|
// 通过 ChatAgent extract_recipes 工具(内置 fallback)
|
|
1062
1288
|
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
1063
1289
|
if (Date.now() - startTime > TOTAL_TIMEOUT) {
|
|
1064
|
-
this.#logger.warn(
|
|
1290
|
+
this.#logger.warn(
|
|
1291
|
+
`[SpmService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s, returning partial results`
|
|
1292
|
+
);
|
|
1065
1293
|
timedOut = true;
|
|
1066
1294
|
break;
|
|
1067
1295
|
}
|
|
@@ -1069,47 +1297,75 @@ export class SpmService {
|
|
|
1069
1297
|
const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
|
|
1070
1298
|
try {
|
|
1071
1299
|
const result = await Promise.race([
|
|
1072
|
-
this.#chatAgent.executeTool('extract_recipes', {
|
|
1073
|
-
|
|
1300
|
+
this.#chatAgent.executeTool('extract_recipes', {
|
|
1301
|
+
targetName: batchLabel,
|
|
1302
|
+
files: batch,
|
|
1303
|
+
}),
|
|
1304
|
+
new Promise((_, reject) =>
|
|
1305
|
+
setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
|
|
1306
|
+
),
|
|
1074
1307
|
]);
|
|
1075
|
-
if (Array.isArray(result?.recipes))
|
|
1308
|
+
if (Array.isArray(result?.recipes)) {
|
|
1309
|
+
allRecipes.push(...result.recipes);
|
|
1310
|
+
}
|
|
1076
1311
|
} catch (err) {
|
|
1077
|
-
this.#logger.warn(
|
|
1312
|
+
this.#logger.warn(
|
|
1313
|
+
`[SpmService] scanProject ChatAgent batch ${batchLabel} failed: ${err.message}`
|
|
1314
|
+
);
|
|
1078
1315
|
}
|
|
1079
1316
|
}
|
|
1080
1317
|
} else {
|
|
1081
1318
|
// 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
|
|
1082
|
-
const {
|
|
1319
|
+
const {
|
|
1320
|
+
getProviderWithFallback,
|
|
1321
|
+
isGeoOrProviderError,
|
|
1322
|
+
getAvailableFallbacks,
|
|
1323
|
+
createProvider,
|
|
1324
|
+
} = this.#aiFactory;
|
|
1083
1325
|
let ai = await getProviderWithFallback();
|
|
1084
1326
|
|
|
1085
1327
|
if (ai) {
|
|
1086
1328
|
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
1087
1329
|
if (Date.now() - startTime > TOTAL_TIMEOUT) {
|
|
1088
|
-
this.#logger.warn(
|
|
1330
|
+
this.#logger.warn(
|
|
1331
|
+
`[SpmService] scanProject: total timeout reached, returning partial results`
|
|
1332
|
+
);
|
|
1089
1333
|
timedOut = true;
|
|
1090
1334
|
break;
|
|
1091
1335
|
}
|
|
1092
1336
|
const batch = allFiles.slice(i, i + BATCH_SIZE);
|
|
1093
1337
|
const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
|
|
1094
1338
|
try {
|
|
1095
|
-
|
|
1339
|
+
const recipes = await Promise.race([
|
|
1096
1340
|
ai.extractRecipes(batchLabel, batch),
|
|
1097
|
-
new Promise((_, reject) =>
|
|
1341
|
+
new Promise((_, reject) =>
|
|
1342
|
+
setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
|
|
1343
|
+
),
|
|
1098
1344
|
]);
|
|
1099
|
-
if (Array.isArray(recipes))
|
|
1345
|
+
if (Array.isArray(recipes)) {
|
|
1346
|
+
allRecipes.push(...recipes);
|
|
1347
|
+
}
|
|
1100
1348
|
} catch (err) {
|
|
1101
1349
|
if (isGeoOrProviderError(err)) {
|
|
1102
|
-
const fallbacks = getAvailableFallbacks(
|
|
1350
|
+
const fallbacks = getAvailableFallbacks(
|
|
1351
|
+
(process.env.ASD_AI_PROVIDER || 'google').toLowerCase()
|
|
1352
|
+
);
|
|
1103
1353
|
for (const fb of fallbacks) {
|
|
1104
1354
|
try {
|
|
1105
1355
|
ai = createProvider({ provider: fb });
|
|
1106
|
-
|
|
1107
|
-
if (Array.isArray(recipes))
|
|
1356
|
+
const recipes = await ai.extractRecipes(batchLabel, batch);
|
|
1357
|
+
if (Array.isArray(recipes)) {
|
|
1358
|
+
allRecipes.push(...recipes);
|
|
1359
|
+
}
|
|
1108
1360
|
break;
|
|
1109
|
-
} catch {
|
|
1361
|
+
} catch {
|
|
1362
|
+
/* next fallback */
|
|
1363
|
+
}
|
|
1110
1364
|
}
|
|
1111
1365
|
} else {
|
|
1112
|
-
this.#logger.warn(
|
|
1366
|
+
this.#logger.warn(
|
|
1367
|
+
`[SpmService] scanProject AI batch ${batchLabel} failed: ${err.message}`
|
|
1368
|
+
);
|
|
1113
1369
|
}
|
|
1114
1370
|
}
|
|
1115
1371
|
}
|
|
@@ -1124,7 +1380,7 @@ export class SpmService {
|
|
|
1124
1380
|
let guardAudit = null;
|
|
1125
1381
|
if (this.#guardCheckEngine) {
|
|
1126
1382
|
try {
|
|
1127
|
-
const guardFiles = allFiles.map(f => ({ path: f.path, content: f.content }));
|
|
1383
|
+
const guardFiles = allFiles.map((f) => ({ path: f.path, content: f.content }));
|
|
1128
1384
|
guardAudit = this.#guardCheckEngine.auditFiles(guardFiles, { scope: 'project' });
|
|
1129
1385
|
|
|
1130
1386
|
// 将有违反的文件写入 ViolationsStore
|
|
@@ -1144,10 +1400,12 @@ export class SpmService {
|
|
|
1144
1400
|
}
|
|
1145
1401
|
}
|
|
1146
1402
|
|
|
1147
|
-
this.#logger.info(
|
|
1403
|
+
this.#logger.info(
|
|
1404
|
+
`[SpmService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`
|
|
1405
|
+
);
|
|
1148
1406
|
|
|
1149
1407
|
return {
|
|
1150
|
-
targets: allTargets.map(t => t.name),
|
|
1408
|
+
targets: allTargets.map((t) => t.name),
|
|
1151
1409
|
recipes: allRecipes,
|
|
1152
1410
|
guardAudit,
|
|
1153
1411
|
scannedFiles,
|