autosnippet 3.0.1 → 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 +655 -260
- 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
|
@@ -55,37 +55,37 @@ export class ContextWindow {
|
|
|
55
55
|
*/
|
|
56
56
|
static MODEL_CONTEXT_WINDOWS = [
|
|
57
57
|
// ── Google Gemini ──
|
|
58
|
-
[/gemini-3/i,
|
|
59
|
-
[/gemini-2\.5/i,
|
|
60
|
-
[/gemini-2/i,
|
|
61
|
-
[/gemini-1\.5-pro/i,
|
|
62
|
-
[/gemini-1\.5-flash/i,
|
|
63
|
-
[/gemini-1\.0/i,
|
|
64
|
-
[/gemini/i,
|
|
58
|
+
[/gemini-3/i, 1_000_000],
|
|
59
|
+
[/gemini-2\.5/i, 1_000_000],
|
|
60
|
+
[/gemini-2/i, 1_000_000],
|
|
61
|
+
[/gemini-1\.5-pro/i, 1_000_000],
|
|
62
|
+
[/gemini-1\.5-flash/i, 1_000_000],
|
|
63
|
+
[/gemini-1\.0/i, 32_000],
|
|
64
|
+
[/gemini/i, 1_000_000], // 未知版本回退
|
|
65
65
|
// ── OpenAI ──
|
|
66
|
-
[/gpt-4o/i,
|
|
67
|
-
[/gpt-4-turbo/i,
|
|
68
|
-
[/gpt-4-(?!turbo)/i,
|
|
69
|
-
[/gpt-3\.5-turbo-16k/i,
|
|
70
|
-
[/gpt-3\.5/i,
|
|
71
|
-
[/o1|o3|o4/i,
|
|
66
|
+
[/gpt-4o/i, 128_000],
|
|
67
|
+
[/gpt-4-turbo/i, 128_000],
|
|
68
|
+
[/gpt-4-(?!turbo)/i, 8_192],
|
|
69
|
+
[/gpt-3\.5-turbo-16k/i, 16_384],
|
|
70
|
+
[/gpt-3\.5/i, 4_096],
|
|
71
|
+
[/o1|o3|o4/i, 200_000], // OpenAI reasoning models
|
|
72
72
|
// ── Anthropic ──
|
|
73
|
-
[/claude-.*sonnet-4/i,
|
|
74
|
-
[/claude-3[
|
|
75
|
-
[/claude-3[
|
|
76
|
-
[/claude-3/i,
|
|
77
|
-
[/claude/i,
|
|
73
|
+
[/claude-.*sonnet-4/i, 200_000],
|
|
74
|
+
[/claude-3[.-]5/i, 200_000],
|
|
75
|
+
[/claude-3[.-]opus/i, 200_000],
|
|
76
|
+
[/claude-3/i, 200_000],
|
|
77
|
+
[/claude/i, 200_000], // 未知 claude 回退
|
|
78
78
|
// ── DeepSeek ──
|
|
79
|
-
[/deepseek/i,
|
|
79
|
+
[/deepseek/i, 64_000],
|
|
80
80
|
// ── 本地 Ollama ──
|
|
81
|
-
[/llama3[
|
|
82
|
-
[/llama3/i,
|
|
83
|
-
[/llama/i,
|
|
84
|
-
[/mistral/i,
|
|
85
|
-
[/qwen/i,
|
|
86
|
-
[/phi/i,
|
|
81
|
+
[/llama3[.-]?[23]/i, 128_000],
|
|
82
|
+
[/llama3/i, 8_192],
|
|
83
|
+
[/llama/i, 4_096],
|
|
84
|
+
[/mistral/i, 32_000],
|
|
85
|
+
[/qwen/i, 128_000],
|
|
86
|
+
[/phi/i, 128_000],
|
|
87
87
|
// ── Mock(测试) ──
|
|
88
|
-
[/mock/i,
|
|
88
|
+
[/mock/i, 32_000],
|
|
89
89
|
];
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -233,13 +233,15 @@ export class ContextWindow {
|
|
|
233
233
|
|
|
234
234
|
// 找到最后一个 assistant-with-toolCalls 的位置
|
|
235
235
|
const lastRoundStart = this.#findLastToolRoundStart();
|
|
236
|
-
if (lastRoundStart < 0)
|
|
236
|
+
if (lastRoundStart < 0) {
|
|
237
|
+
return { level: 1, removed: 0 };
|
|
238
|
+
}
|
|
237
239
|
|
|
238
240
|
// 只截断 lastRoundStart 之前的 tool results
|
|
239
241
|
for (let i = 1; i < lastRoundStart; i++) {
|
|
240
242
|
const msg = this.#messages[i];
|
|
241
243
|
if (msg.role === 'tool' && msg.content && msg.content.length > TRUNCATE_THRESHOLD) {
|
|
242
|
-
msg.content = msg.content.substring(0, TRUNCATE_TO)
|
|
244
|
+
msg.content = `${msg.content.substring(0, TRUNCATE_TO)}\n... [truncated from ${msg.content.length} chars]`;
|
|
243
245
|
truncated++;
|
|
244
246
|
}
|
|
245
247
|
}
|
|
@@ -259,10 +261,14 @@ export class ContextWindow {
|
|
|
259
261
|
#compactL2() {
|
|
260
262
|
// 找到倒数第 2 个 tool round 的起始(保留最后 2 轮)
|
|
261
263
|
const roundStarts = this.#findAllToolRoundStarts();
|
|
262
|
-
if (roundStarts.length < 2)
|
|
264
|
+
if (roundStarts.length < 2) {
|
|
265
|
+
return { level: 2, removed: 0 };
|
|
266
|
+
}
|
|
263
267
|
|
|
264
268
|
const keepFrom = roundStarts[roundStarts.length - 2]; // 保留从倒数第 2 轮开始
|
|
265
|
-
if (keepFrom <= 1)
|
|
269
|
+
if (keepFrom <= 1) {
|
|
270
|
+
return { level: 2, removed: 0 };
|
|
271
|
+
}
|
|
266
272
|
|
|
267
273
|
return this.#spliceAndSummarize(keepFrom, 2);
|
|
268
274
|
}
|
|
@@ -310,8 +316,8 @@ export class ContextWindow {
|
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
// 计算历史统计
|
|
313
|
-
const toolCallCount = removed.filter(m => m.role === 'assistant' && m.toolCalls).length;
|
|
314
|
-
const toolResultCount = removed.filter(m => m.role === 'tool').length;
|
|
319
|
+
const toolCallCount = removed.filter((m) => m.role === 'assistant' && m.toolCalls).length;
|
|
320
|
+
const toolResultCount = removed.filter((m) => m.role === 'tool').length;
|
|
315
321
|
|
|
316
322
|
// Splice: 移除 messages[1..keepFrom-1]
|
|
317
323
|
this.#messages.splice(1, keepFrom - 1);
|
|
@@ -330,8 +336,12 @@ export class ContextWindow {
|
|
|
330
336
|
});
|
|
331
337
|
|
|
332
338
|
const removedCount = keepFrom - 1;
|
|
333
|
-
this.#compactionLog.push(
|
|
334
|
-
|
|
339
|
+
this.#compactionLog.push(
|
|
340
|
+
`L${level}: removed ${removedCount} messages (${toolCallCount} rounds)`
|
|
341
|
+
);
|
|
342
|
+
this.#logger.info(
|
|
343
|
+
`[ContextWindow] L${level} compact: removed ${removedCount} messages, kept last ${level === 2 ? 2 : 1} rounds`
|
|
344
|
+
);
|
|
335
345
|
|
|
336
346
|
return { level, removed: removedCount };
|
|
337
347
|
}
|
|
@@ -369,8 +379,12 @@ export class ContextWindow {
|
|
|
369
379
|
estimateTokens() {
|
|
370
380
|
let total = 0;
|
|
371
381
|
for (const m of this.#messages) {
|
|
372
|
-
if (m.content)
|
|
373
|
-
|
|
382
|
+
if (m.content) {
|
|
383
|
+
total += estimateTokensFast(m.content);
|
|
384
|
+
}
|
|
385
|
+
if (m.toolCalls) {
|
|
386
|
+
total += estimateTokensFast(JSON.stringify(m.toolCalls));
|
|
387
|
+
}
|
|
374
388
|
}
|
|
375
389
|
return total;
|
|
376
390
|
}
|
|
@@ -390,9 +404,15 @@ export class ContextWindow {
|
|
|
390
404
|
*/
|
|
391
405
|
getToolResultQuota() {
|
|
392
406
|
const usage = this.getTokenUsageRatio();
|
|
393
|
-
if (usage < 0.4)
|
|
394
|
-
|
|
395
|
-
|
|
407
|
+
if (usage < 0.4) {
|
|
408
|
+
return { maxChars: 6000, maxMatches: 15 };
|
|
409
|
+
}
|
|
410
|
+
if (usage < 0.6) {
|
|
411
|
+
return { maxChars: 3000, maxMatches: 8 };
|
|
412
|
+
}
|
|
413
|
+
if (usage < 0.8) {
|
|
414
|
+
return { maxChars: 1500, maxMatches: 5 };
|
|
415
|
+
}
|
|
396
416
|
return { maxChars: 800, maxMatches: 3 };
|
|
397
417
|
}
|
|
398
418
|
|
|
@@ -493,7 +513,7 @@ export function limitToolResult(toolName, result, quota) {
|
|
|
493
513
|
limited.batchResults[key] = limitSearchResultObj(sub, Math.min(maxMatches, 3), perKeyChars);
|
|
494
514
|
}
|
|
495
515
|
const raw = JSON.stringify(limited);
|
|
496
|
-
return raw.length > maxChars ? raw.substring(0, maxChars)
|
|
516
|
+
return raw.length > maxChars ? `${raw.substring(0, maxChars)}\n... [batch truncated]` : raw;
|
|
497
517
|
}
|
|
498
518
|
return limitSearchResult(result, maxMatches, maxChars);
|
|
499
519
|
}
|
|
@@ -502,7 +522,7 @@ export function limitToolResult(toolName, result, quota) {
|
|
|
502
522
|
if (toolName === 'read_project_file') {
|
|
503
523
|
if (result && typeof result === 'object' && result.batchResults) {
|
|
504
524
|
const raw = JSON.stringify(result);
|
|
505
|
-
return raw.length > maxChars ? raw.substring(0, maxChars)
|
|
525
|
+
return raw.length > maxChars ? `${raw.substring(0, maxChars)}\n... [batch truncated]` : raw;
|
|
506
526
|
}
|
|
507
527
|
return limitFileContent(result, maxChars);
|
|
508
528
|
}
|
|
@@ -510,7 +530,7 @@ export function limitToolResult(toolName, result, quota) {
|
|
|
510
530
|
// 通用: 按字符限制
|
|
511
531
|
const raw = typeof result === 'string' ? result : JSON.stringify(result);
|
|
512
532
|
if (raw.length > maxChars) {
|
|
513
|
-
return raw.substring(0, maxChars)
|
|
533
|
+
return `${raw.substring(0, maxChars)}\n... [truncated, ${raw.length} total chars]`;
|
|
514
534
|
}
|
|
515
535
|
return raw;
|
|
516
536
|
}
|
|
@@ -523,9 +543,7 @@ export function limitToolResult(toolName, result, quota) {
|
|
|
523
543
|
*/
|
|
524
544
|
function limitSearchResult(result, maxMatches, maxChars) {
|
|
525
545
|
if (typeof result === 'string') {
|
|
526
|
-
return result.length > maxChars
|
|
527
|
-
? result.substring(0, maxChars) + '\n... [truncated]'
|
|
528
|
-
: result;
|
|
546
|
+
return result.length > maxChars ? `${result.substring(0, maxChars)}\n... [truncated]` : result;
|
|
529
547
|
}
|
|
530
548
|
|
|
531
549
|
if (!result || typeof result !== 'object') {
|
|
@@ -535,13 +553,13 @@ function limitSearchResult(result, maxMatches, maxChars) {
|
|
|
535
553
|
// 深拷贝避免修改原对象
|
|
536
554
|
const limited = { ...result };
|
|
537
555
|
if (Array.isArray(limited.matches)) {
|
|
538
|
-
limited.matches = limited.matches.slice(0, maxMatches).map(m => {
|
|
556
|
+
limited.matches = limited.matches.slice(0, maxMatches).map((m) => {
|
|
539
557
|
const copy = { ...m };
|
|
540
558
|
// 截断每个匹配的 context 字段(多行文本)
|
|
541
559
|
if (copy.context && typeof copy.context === 'string') {
|
|
542
560
|
const contextLines = copy.context.split('\n');
|
|
543
561
|
if (contextLines.length > 7) {
|
|
544
|
-
copy.context = contextLines.slice(0, 7).join('\n')
|
|
562
|
+
copy.context = `${contextLines.slice(0, 7).join('\n')}\n... [truncated]`;
|
|
545
563
|
}
|
|
546
564
|
}
|
|
547
565
|
// 兼容旧格式: 也处理 lines 数组
|
|
@@ -558,7 +576,7 @@ function limitSearchResult(result, maxMatches, maxChars) {
|
|
|
558
576
|
|
|
559
577
|
const str = JSON.stringify(limited);
|
|
560
578
|
if (str.length > maxChars) {
|
|
561
|
-
return str.substring(0, maxChars)
|
|
579
|
+
return `${str.substring(0, maxChars)}\n... [truncated]`;
|
|
562
580
|
}
|
|
563
581
|
return str;
|
|
564
582
|
}
|
|
@@ -568,21 +586,25 @@ function limitSearchResult(result, maxMatches, maxChars) {
|
|
|
568
586
|
* 当源码含控制字符时,stringify→substring 截断会破坏 JSON 结构导致 parse 失败
|
|
569
587
|
*/
|
|
570
588
|
function limitSearchResultObj(result, maxMatches, maxChars) {
|
|
571
|
-
if (!result || typeof result !== 'object')
|
|
572
|
-
|
|
589
|
+
if (!result || typeof result !== 'object') {
|
|
590
|
+
return result || {};
|
|
591
|
+
}
|
|
592
|
+
if (typeof result === 'string') {
|
|
593
|
+
return { _raw: result.substring(0, maxChars) };
|
|
594
|
+
}
|
|
573
595
|
|
|
574
596
|
const limited = { ...result };
|
|
575
597
|
if (Array.isArray(limited.matches)) {
|
|
576
|
-
limited.matches = limited.matches.slice(0, maxMatches).map(m => {
|
|
598
|
+
limited.matches = limited.matches.slice(0, maxMatches).map((m) => {
|
|
577
599
|
const copy = { ...m };
|
|
578
600
|
if (copy.context && typeof copy.context === 'string') {
|
|
579
601
|
const contextLines = copy.context.split('\n');
|
|
580
602
|
if (contextLines.length > 7) {
|
|
581
|
-
copy.context = contextLines.slice(0, 7).join('\n')
|
|
603
|
+
copy.context = `${contextLines.slice(0, 7).join('\n')}\n... [truncated]`;
|
|
582
604
|
}
|
|
583
605
|
// 按字符上限截断 context(防止单个代码块过大)
|
|
584
606
|
if (copy.context.length > 500) {
|
|
585
|
-
copy.context = copy.context.substring(0, 500)
|
|
607
|
+
copy.context = `${copy.context.substring(0, 500)}\n... [truncated]`;
|
|
586
608
|
}
|
|
587
609
|
}
|
|
588
610
|
if (Array.isArray(copy.lines) && copy.lines.length > 5) {
|
|
@@ -603,9 +625,7 @@ function limitSearchResultObj(result, maxMatches, maxChars) {
|
|
|
603
625
|
*/
|
|
604
626
|
function limitFileContent(result, maxChars) {
|
|
605
627
|
if (typeof result === 'string') {
|
|
606
|
-
return result.length > maxChars
|
|
607
|
-
? result.substring(0, maxChars) + '\n... [truncated]'
|
|
608
|
-
: result;
|
|
628
|
+
return result.length > maxChars ? `${result.substring(0, maxChars)}\n... [truncated]` : result;
|
|
609
629
|
}
|
|
610
630
|
|
|
611
631
|
if (!result || typeof result !== 'object') {
|
|
@@ -617,10 +637,12 @@ function limitFileContent(result, maxChars) {
|
|
|
617
637
|
const lines = limited.content.split('\n');
|
|
618
638
|
let truncated = '';
|
|
619
639
|
for (const line of lines) {
|
|
620
|
-
if (truncated.length + line.length + 1 > maxChars)
|
|
621
|
-
|
|
640
|
+
if (truncated.length + line.length + 1 > maxChars) {
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
truncated += `${line}\n`;
|
|
622
644
|
}
|
|
623
|
-
limited.content = truncated
|
|
645
|
+
limited.content = `${truncated}... [truncated at ${maxChars} chars, total ${result.content.length}]`;
|
|
624
646
|
}
|
|
625
647
|
|
|
626
648
|
return JSON.stringify(limited);
|
|
@@ -718,7 +740,9 @@ export class PhaseRouter {
|
|
|
718
740
|
switch (this.#phase) {
|
|
719
741
|
case 'EXPLORE':
|
|
720
742
|
// 最后一轮 EXPLORE 用 auto 保底
|
|
721
|
-
if (this.#phaseRounds >= this.#budget.searchBudget - 1)
|
|
743
|
+
if (this.#phaseRounds >= this.#budget.searchBudget - 1) {
|
|
744
|
+
return 'auto';
|
|
745
|
+
}
|
|
722
746
|
return 'required';
|
|
723
747
|
case 'PRODUCE':
|
|
724
748
|
return 'auto';
|
|
@@ -735,11 +759,15 @@ export class PhaseRouter {
|
|
|
735
759
|
*/
|
|
736
760
|
shouldExit() {
|
|
737
761
|
// 已在 SUMMARIZE 阶段 → 给 2 轮输出总结后退出
|
|
738
|
-
if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2)
|
|
762
|
+
if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2) {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
739
765
|
|
|
740
766
|
// 达到 maxIterations → 不硬退出,而是强制转入 SUMMARIZE 让 AI 在完整上下文中收尾
|
|
741
767
|
if (this.#totalIterations >= this.#budget.maxIterations && this.#phase !== 'SUMMARIZE') {
|
|
742
|
-
this.#logger.info(
|
|
768
|
+
this.#logger.info(
|
|
769
|
+
`[PhaseRouter] maxIterations reached (${this.#totalIterations}/${this.#budget.maxIterations}), forcing → SUMMARIZE for graceful exit`
|
|
770
|
+
);
|
|
743
771
|
this.#forcedSummarize = true;
|
|
744
772
|
this.#transitionTo('SUMMARIZE');
|
|
745
773
|
// 返回 false 让主循环继续运行 SUMMARIZE 阶段的 2 轮收尾
|
|
@@ -747,7 +775,9 @@ export class PhaseRouter {
|
|
|
747
775
|
}
|
|
748
776
|
|
|
749
777
|
// SUMMARIZE 阶段超限兜底(maxIterations + 2 轮 grace)
|
|
750
|
-
if (this.#totalIterations >= this.#budget.maxIterations + 2)
|
|
778
|
+
if (this.#totalIterations >= this.#budget.maxIterations + 2) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
751
781
|
|
|
752
782
|
return false;
|
|
753
783
|
}
|
|
@@ -770,7 +800,7 @@ export class PhaseRouter {
|
|
|
770
800
|
* @returns {{ transitioned: boolean, newPhase: string, exitReason?: string }}
|
|
771
801
|
*/
|
|
772
802
|
update(roundResult) {
|
|
773
|
-
const {
|
|
803
|
+
const { submitCount = 0, isTextOnly = false } = roundResult;
|
|
774
804
|
this.#totalSubmits += submitCount;
|
|
775
805
|
|
|
776
806
|
// ── EXPLORE → PRODUCE/SUMMARIZE ──
|
|
@@ -783,7 +813,9 @@ export class PhaseRouter {
|
|
|
783
813
|
// 搜索轮次耗尽 → 转换
|
|
784
814
|
if (this.#phaseRounds >= this.#budget.searchBudget) {
|
|
785
815
|
const next = this.#isSkillOnly ? 'SUMMARIZE' : 'PRODUCE';
|
|
786
|
-
this.#logger.info(
|
|
816
|
+
this.#logger.info(
|
|
817
|
+
`[PhaseRouter] search budget exhausted (${this.#phaseRounds}/${this.#budget.searchBudget}) → ${next}`
|
|
818
|
+
);
|
|
787
819
|
return this.#transitionTo(next);
|
|
788
820
|
}
|
|
789
821
|
|
|
@@ -806,7 +838,9 @@ export class PhaseRouter {
|
|
|
806
838
|
|
|
807
839
|
// 硬上限
|
|
808
840
|
if (this.#budget.maxSubmits > 0 && this.#totalSubmits >= this.#budget.maxSubmits) {
|
|
809
|
-
this.#logger.info(
|
|
841
|
+
this.#logger.info(
|
|
842
|
+
`[PhaseRouter] hard submit cap reached (${this.#totalSubmits}/${this.#budget.maxSubmits}) → SUMMARIZE`
|
|
843
|
+
);
|
|
810
844
|
return this.#transitionTo('SUMMARIZE');
|
|
811
845
|
}
|
|
812
846
|
|
|
@@ -823,18 +857,24 @@ export class PhaseRouter {
|
|
|
823
857
|
// 否则视为 AI 的中间分析,不立即退出
|
|
824
858
|
if (isTextOnly) {
|
|
825
859
|
if (this.#totalSubmits >= this.#budget.softSubmitLimit) {
|
|
826
|
-
this.#logger.info(
|
|
860
|
+
this.#logger.info(
|
|
861
|
+
`[PhaseRouter] text reply after ${this.#totalSubmits} submits (≥ softLimit=${this.#budget.softSubmitLimit}) → SUMMARIZE`
|
|
862
|
+
);
|
|
827
863
|
return this.#transitionTo('SUMMARIZE');
|
|
828
864
|
}
|
|
829
865
|
// 未达 softLimit — 空转已在上方通用分支计数,不再额外递增
|
|
830
|
-
this.#logger.info(
|
|
866
|
+
this.#logger.info(
|
|
867
|
+
`[PhaseRouter] text reply in PRODUCE — idleRounds=${this.#idleRounds}, totalSubmits=${this.#totalSubmits}, continuing`
|
|
868
|
+
);
|
|
831
869
|
return { transitioned: false, newPhase: this.#phase };
|
|
832
870
|
}
|
|
833
871
|
|
|
834
872
|
// PRODUCE 阶段容忍期耗尽且 0 提交 → 强制进入 SUMMARIZE
|
|
835
873
|
// 使用 PRODUCE 阶段内的轮次计数,而非 totalIterations
|
|
836
874
|
if (this.#phaseRounds >= this.#budget.searchBudgetGrace && this.#totalSubmits === 0) {
|
|
837
|
-
this.#logger.info(
|
|
875
|
+
this.#logger.info(
|
|
876
|
+
`[PhaseRouter] PRODUCE grace exhausted (${this.#phaseRounds}/${this.#budget.searchBudgetGrace} rounds, 0 submits) → SUMMARIZE`
|
|
877
|
+
);
|
|
838
878
|
return this.#transitionTo('SUMMARIZE');
|
|
839
879
|
}
|
|
840
880
|
|
|
@@ -869,7 +909,10 @@ export class PhaseRouter {
|
|
|
869
909
|
? '你已收集足够信息,请在回复中直接输出 dimensionDigest JSON。'
|
|
870
910
|
: '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_knowledge 提交候选。不要继续搜索,直接提交。';
|
|
871
911
|
}
|
|
872
|
-
if (
|
|
912
|
+
if (
|
|
913
|
+
this.#totalSubmits >= this.#budget.softSubmitLimit &&
|
|
914
|
+
this.#budget.softSubmitLimit > 0
|
|
915
|
+
) {
|
|
873
916
|
const remaining = this.#budget.maxSubmits - this.#totalSubmits;
|
|
874
917
|
return `已提交 ${this.#totalSubmits} 个候选(上限 ${this.#budget.maxSubmits})。${remaining > 0 ? `还可提交 ${remaining} 个。` : ''}如果还有值得记录的发现可以继续提交,否则请产出 dimensionDigest 总结。\n⚠️ 如果还有未处理的信号,请在 dimensionDigest 的 remainingTasks 字段中标记,下次运行时会续传。`;
|
|
875
918
|
}
|
|
@@ -892,7 +935,9 @@ export class PhaseRouter {
|
|
|
892
935
|
this.#phase = newPhase;
|
|
893
936
|
this.#phaseRounds = 0;
|
|
894
937
|
this.#idleRounds = 0;
|
|
895
|
-
this.#logger.info(
|
|
938
|
+
this.#logger.info(
|
|
939
|
+
`[PhaseRouter] ${oldPhase} → ${newPhase} (iter=${this.#totalIterations}, submits=${this.#totalSubmits})`
|
|
940
|
+
);
|
|
896
941
|
return { transitioned: true, newPhase };
|
|
897
942
|
}
|
|
898
943
|
}
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
* {id}.jsonl — 每行一条消息 {role, content, ts}
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import crypto from 'node:crypto';
|
|
21
22
|
import fs from 'node:fs';
|
|
22
23
|
import path from 'node:path';
|
|
23
|
-
import crypto from 'node:crypto';
|
|
24
24
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
25
25
|
import pathGuard from '../../shared/PathGuard.js';
|
|
26
26
|
import { estimateTokens as _estimateTokens } from '../../shared/token-utils.js';
|
|
27
27
|
|
|
28
|
-
const DEFAULT_TOKEN_BUDGET = 12000;
|
|
29
|
-
const MAX_CONVERSATIONS = 100;
|
|
30
|
-
const
|
|
28
|
+
const DEFAULT_TOKEN_BUDGET = 12000; // ~12K tokens 留给历史, 其余给系统提示词和当前消息
|
|
29
|
+
const MAX_CONVERSATIONS = 100; // 索引最多保留 100 个对话
|
|
30
|
+
const _SUMMARY_TARGET_TOKENS = 500; // 压缩后的摘要目标 token 数
|
|
31
31
|
|
|
32
32
|
export class ConversationStore {
|
|
33
33
|
#dir;
|
|
@@ -98,11 +98,11 @@ export class ConversationStore {
|
|
|
98
98
|
content: message.content,
|
|
99
99
|
ts: new Date().toISOString(),
|
|
100
100
|
});
|
|
101
|
-
fs.appendFileSync(filePath, line
|
|
101
|
+
fs.appendFileSync(filePath, `${line}\n`, 'utf-8');
|
|
102
102
|
|
|
103
103
|
// 更新索引
|
|
104
104
|
const index = this.#loadIndex();
|
|
105
|
-
const entry = index.find(e => e.id === conversationId);
|
|
105
|
+
const entry = index.find((e) => e.id === conversationId);
|
|
106
106
|
if (entry) {
|
|
107
107
|
entry.updatedAt = new Date().toISOString();
|
|
108
108
|
entry.messageCount = (entry.messageCount || 0) + 1;
|
|
@@ -133,18 +133,25 @@ export class ConversationStore {
|
|
|
133
133
|
load(conversationId, { tokenBudget = DEFAULT_TOKEN_BUDGET } = {}) {
|
|
134
134
|
try {
|
|
135
135
|
const filePath = this.#conversationPath(conversationId);
|
|
136
|
-
if (!fs.existsSync(filePath))
|
|
136
|
+
if (!fs.existsSync(filePath)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
137
139
|
|
|
138
140
|
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
139
|
-
if (!raw)
|
|
141
|
+
if (!raw) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
140
144
|
|
|
141
|
-
const messages = raw
|
|
145
|
+
const messages = raw
|
|
146
|
+
.split('\n')
|
|
142
147
|
.filter(Boolean)
|
|
143
|
-
.map(line => {
|
|
148
|
+
.map((line) => {
|
|
144
149
|
try {
|
|
145
150
|
const parsed = JSON.parse(line);
|
|
146
151
|
return { role: parsed.role, content: parsed.content };
|
|
147
|
-
} catch {
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
148
155
|
})
|
|
149
156
|
.filter(Boolean);
|
|
150
157
|
|
|
@@ -165,7 +172,7 @@ export class ConversationStore {
|
|
|
165
172
|
const index = this.#loadIndex();
|
|
166
173
|
let results = index;
|
|
167
174
|
if (category) {
|
|
168
|
-
results = results.filter(e => e.category === category);
|
|
175
|
+
results = results.filter((e) => e.category === category);
|
|
169
176
|
}
|
|
170
177
|
return results.slice(0, limit);
|
|
171
178
|
}
|
|
@@ -177,7 +184,7 @@ export class ConversationStore {
|
|
|
177
184
|
delete(conversationId) {
|
|
178
185
|
this.#deleteConversationFile(conversationId);
|
|
179
186
|
const index = this.#loadIndex();
|
|
180
|
-
const filtered = index.filter(e => e.id !== conversationId);
|
|
187
|
+
const filtered = index.filter((e) => e.id !== conversationId);
|
|
181
188
|
this.#saveIndex(filtered);
|
|
182
189
|
}
|
|
183
190
|
|
|
@@ -191,36 +198,54 @@ export class ConversationStore {
|
|
|
191
198
|
* @returns {Promise<boolean>} 是否成功压缩
|
|
192
199
|
*/
|
|
193
200
|
async summarize(conversationId, { aiProvider }) {
|
|
194
|
-
if (!aiProvider)
|
|
201
|
+
if (!aiProvider) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
195
204
|
|
|
196
205
|
try {
|
|
197
206
|
const filePath = this.#conversationPath(conversationId);
|
|
198
|
-
if (!fs.existsSync(filePath))
|
|
207
|
+
if (!fs.existsSync(filePath)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
199
210
|
|
|
200
211
|
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
201
|
-
if (!raw)
|
|
212
|
+
if (!raw) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
202
215
|
|
|
203
|
-
const messages = raw
|
|
216
|
+
const messages = raw
|
|
217
|
+
.split('\n')
|
|
204
218
|
.filter(Boolean)
|
|
205
|
-
.map(
|
|
219
|
+
.map((line) => {
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(line);
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
})
|
|
206
226
|
.filter(Boolean);
|
|
207
227
|
|
|
208
|
-
if (messages.length < 6)
|
|
228
|
+
if (messages.length < 6) {
|
|
229
|
+
return false; // 太短不需要压缩
|
|
230
|
+
}
|
|
209
231
|
|
|
210
232
|
// 保留最近 4 条消息,压缩其余
|
|
211
233
|
const toSummarize = messages.slice(0, -4);
|
|
212
234
|
const toKeep = messages.slice(-4);
|
|
213
235
|
|
|
214
|
-
const summaryPrompt = `请用 2-3 句话总结以下对话的要点(保留关键决策、用户偏好、操作结果):\n\n${
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
const summaryPrompt = `请用 2-3 句话总结以下对话的要点(保留关键决策、用户偏好、操作结果):\n\n${toSummarize
|
|
237
|
+
.map((m) => `[${m.role}] ${m.content}`)
|
|
238
|
+
.join('\n')
|
|
239
|
+
.substring(0, 4000)}`;
|
|
217
240
|
|
|
218
241
|
const summary = await aiProvider.chat(summaryPrompt, {
|
|
219
242
|
temperature: 0.3,
|
|
220
243
|
maxTokens: 300,
|
|
221
244
|
});
|
|
222
245
|
|
|
223
|
-
if (!summary)
|
|
246
|
+
if (!summary) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
224
249
|
|
|
225
250
|
// 重写对话文件: 摘要 + 最近消息
|
|
226
251
|
const newMessages = [
|
|
@@ -230,20 +255,22 @@ export class ConversationStore {
|
|
|
230
255
|
|
|
231
256
|
fs.writeFileSync(
|
|
232
257
|
filePath,
|
|
233
|
-
newMessages.map(m => JSON.stringify(m)).join('\n')
|
|
234
|
-
'utf-8'
|
|
258
|
+
`${newMessages.map((m) => JSON.stringify(m)).join('\n')}\n`,
|
|
259
|
+
'utf-8'
|
|
235
260
|
);
|
|
236
261
|
|
|
237
262
|
// 更新索引
|
|
238
263
|
const index = this.#loadIndex();
|
|
239
|
-
const entry = index.find(e => e.id === conversationId);
|
|
264
|
+
const entry = index.find((e) => e.id === conversationId);
|
|
240
265
|
if (entry) {
|
|
241
266
|
entry.hasSummary = true;
|
|
242
267
|
entry.messageCount = newMessages.length;
|
|
243
268
|
this.#saveIndex(index);
|
|
244
269
|
}
|
|
245
270
|
|
|
246
|
-
this.#logger.info(
|
|
271
|
+
this.#logger.info(
|
|
272
|
+
`[ConversationStore] summarized conversation ${conversationId}: ${messages.length} → ${newMessages.length} messages`
|
|
273
|
+
);
|
|
247
274
|
return true;
|
|
248
275
|
} catch (err) {
|
|
249
276
|
this.#logger.warn(`[ConversationStore] summarize failed: ${err.message}`);
|
|
@@ -263,8 +290,10 @@ export class ConversationStore {
|
|
|
263
290
|
const cutoff = Date.now() - maxAgeDays * 86400000;
|
|
264
291
|
let deleted = 0;
|
|
265
292
|
|
|
266
|
-
const kept = index.filter(entry => {
|
|
267
|
-
if (category && entry.category !== category)
|
|
293
|
+
const kept = index.filter((entry) => {
|
|
294
|
+
if (category && entry.category !== category) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
268
297
|
const updatedAt = new Date(entry.updatedAt).getTime();
|
|
269
298
|
if (updatedAt < cutoff) {
|
|
270
299
|
this.#deleteConversationFile(entry.id);
|
|
@@ -300,17 +329,21 @@ export class ConversationStore {
|
|
|
300
329
|
* 策略: 保留首条摘要(如有) + 最新消息,丢弃中间旧消息
|
|
301
330
|
*/
|
|
302
331
|
#fitWithinBudget(messages, tokenBudget) {
|
|
303
|
-
if (messages.length === 0)
|
|
332
|
+
if (messages.length === 0) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
304
335
|
|
|
305
336
|
// 计算总 token
|
|
306
337
|
let totalTokens = 0;
|
|
307
|
-
const tokenCounts = messages.map(m => {
|
|
338
|
+
const tokenCounts = messages.map((m) => {
|
|
308
339
|
const tokens = this.estimateTokens(m.content);
|
|
309
340
|
totalTokens += tokens;
|
|
310
341
|
return tokens;
|
|
311
342
|
});
|
|
312
343
|
|
|
313
|
-
if (totalTokens <= tokenBudget)
|
|
344
|
+
if (totalTokens <= tokenBudget) {
|
|
345
|
+
return messages;
|
|
346
|
+
}
|
|
314
347
|
|
|
315
348
|
// 超预算 — 保留首条(摘要) + 从末尾往前取
|
|
316
349
|
const result = [];
|
|
@@ -325,7 +358,9 @@ export class ConversationStore {
|
|
|
325
358
|
// 从末尾往前填充
|
|
326
359
|
const tail = [];
|
|
327
360
|
for (let i = messages.length - 1; i >= (result.length > 0 ? 1 : 0); i--) {
|
|
328
|
-
if (used + tokenCounts[i] > tokenBudget)
|
|
361
|
+
if (used + tokenCounts[i] > tokenBudget) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
329
364
|
tail.unshift(messages[i]);
|
|
330
365
|
used += tokenCounts[i];
|
|
331
366
|
}
|
|
@@ -353,8 +388,12 @@ export class ConversationStore {
|
|
|
353
388
|
#deleteConversationFile(id) {
|
|
354
389
|
try {
|
|
355
390
|
const filePath = this.#conversationPath(id);
|
|
356
|
-
if (fs.existsSync(filePath))
|
|
357
|
-
|
|
391
|
+
if (fs.existsSync(filePath)) {
|
|
392
|
+
fs.unlinkSync(filePath);
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
/* ignore */
|
|
396
|
+
}
|
|
358
397
|
}
|
|
359
398
|
|
|
360
399
|
#loadIndex() {
|
|
@@ -362,7 +401,9 @@ export class ConversationStore {
|
|
|
362
401
|
if (fs.existsSync(this.#indexPath)) {
|
|
363
402
|
return JSON.parse(fs.readFileSync(this.#indexPath, 'utf-8'));
|
|
364
403
|
}
|
|
365
|
-
} catch {
|
|
404
|
+
} catch {
|
|
405
|
+
/* corrupt — reset */
|
|
406
|
+
}
|
|
366
407
|
return [];
|
|
367
408
|
}
|
|
368
409
|
|