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.
Files changed (290) hide show
  1. package/README.md +230 -324
  2. package/bin/api-server.js +1 -1
  3. package/bin/cli.js +204 -244
  4. package/bin/mcp-server.js +5 -3
  5. package/config/knowledge-base.config.js +132 -132
  6. package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
  7. package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
  8. package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
  9. package/dashboard/dist/index.html +3 -3
  10. package/lib/bootstrap.js +8 -8
  11. package/lib/cli/AiScanService.js +86 -40
  12. package/lib/cli/KnowledgeSyncService.js +113 -74
  13. package/lib/cli/SetupService.js +439 -277
  14. package/lib/cli/UpgradeService.js +63 -100
  15. package/lib/core/AstAnalyzer.js +276 -597
  16. package/lib/core/ast/ProjectGraph.js +101 -40
  17. package/lib/core/ast/ensure-grammars.js +232 -0
  18. package/lib/core/ast/index.js +115 -0
  19. package/lib/core/ast/lang-dart.js +661 -0
  20. package/lib/core/ast/lang-go.js +530 -0
  21. package/lib/core/ast/lang-java.js +435 -0
  22. package/lib/core/ast/lang-javascript.js +272 -0
  23. package/lib/core/ast/lang-kotlin.js +423 -0
  24. package/lib/core/ast/lang-objc.js +388 -0
  25. package/lib/core/ast/lang-python.js +371 -0
  26. package/lib/core/ast/lang-swift.js +337 -0
  27. package/lib/core/ast/lang-typescript.js +503 -0
  28. package/lib/core/capability/CapabilityProbe.js +18 -9
  29. package/lib/core/constitution/Constitution.js +2 -3
  30. package/lib/core/constitution/ConstitutionValidator.js +65 -24
  31. package/lib/core/discovery/DartDiscoverer.js +534 -0
  32. package/lib/core/discovery/DiscovererRegistry.js +83 -0
  33. package/lib/core/discovery/GenericDiscoverer.js +225 -0
  34. package/lib/core/discovery/GoDiscoverer.js +541 -0
  35. package/lib/core/discovery/JvmDiscoverer.js +506 -0
  36. package/lib/core/discovery/NodeDiscoverer.js +466 -0
  37. package/lib/core/discovery/ProjectDiscoverer.js +93 -0
  38. package/lib/core/discovery/PythonDiscoverer.js +338 -0
  39. package/lib/core/discovery/SpmDiscoverer.js +5 -0
  40. package/lib/core/discovery/index.js +53 -0
  41. package/lib/core/enhancement/EnhancementPack.js +71 -0
  42. package/lib/core/enhancement/EnhancementRegistry.js +47 -0
  43. package/lib/core/enhancement/android-enhancement.js +102 -0
  44. package/lib/core/enhancement/django-enhancement.js +70 -0
  45. package/lib/core/enhancement/fastapi-enhancement.js +63 -0
  46. package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
  47. package/lib/core/enhancement/go-web-enhancement.js +201 -0
  48. package/lib/core/enhancement/index.js +65 -0
  49. package/lib/core/enhancement/node-server-enhancement.js +88 -0
  50. package/lib/core/enhancement/react-enhancement.js +86 -0
  51. package/lib/core/enhancement/spring-enhancement.js +112 -0
  52. package/lib/core/enhancement/vue-enhancement.js +96 -0
  53. package/lib/core/gateway/Gateway.js +8 -9
  54. package/lib/core/gateway/GatewayActionRegistry.js +1 -1
  55. package/lib/core/permission/PermissionManager.js +12 -8
  56. package/lib/domain/index.js +13 -9
  57. package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
  58. package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
  59. package/lib/domain/knowledge/Lifecycle.js +22 -22
  60. package/lib/domain/knowledge/index.js +9 -12
  61. package/lib/domain/knowledge/values/Constraints.js +31 -21
  62. package/lib/domain/knowledge/values/Content.js +21 -13
  63. package/lib/domain/knowledge/values/Quality.js +31 -18
  64. package/lib/domain/knowledge/values/Reasoning.js +20 -12
  65. package/lib/domain/knowledge/values/Relations.js +37 -25
  66. package/lib/domain/knowledge/values/Stats.js +18 -12
  67. package/lib/domain/knowledge/values/index.js +4 -3
  68. package/lib/domain/snippet/Snippet.js +35 -10
  69. package/lib/external/ai/AiFactory.js +48 -16
  70. package/lib/external/ai/AiProvider.js +184 -90
  71. package/lib/external/ai/providers/ClaudeProvider.js +25 -12
  72. package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
  73. package/lib/external/ai/providers/MockProvider.js +9 -3
  74. package/lib/external/ai/providers/OpenAiProvider.js +51 -29
  75. package/lib/external/mcp/McpServer.js +66 -36
  76. package/lib/external/mcp/errorHandler.js +23 -11
  77. package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
  78. package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
  79. package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
  80. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
  81. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
  82. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
  83. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
  84. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
  85. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
  86. package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
  87. package/lib/external/mcp/handlers/bootstrap.js +691 -168
  88. package/lib/external/mcp/handlers/browse.js +66 -22
  89. package/lib/external/mcp/handlers/candidate.js +118 -35
  90. package/lib/external/mcp/handlers/consolidated.js +49 -17
  91. package/lib/external/mcp/handlers/guard.js +104 -39
  92. package/lib/external/mcp/handlers/knowledge.js +60 -36
  93. package/lib/external/mcp/handlers/search.js +43 -14
  94. package/lib/external/mcp/handlers/skill.js +120 -45
  95. package/lib/external/mcp/handlers/structure.js +240 -86
  96. package/lib/external/mcp/handlers/system.js +42 -12
  97. package/lib/external/mcp/handlers/wiki.js +58 -33
  98. package/lib/external/mcp/tools.js +306 -123
  99. package/lib/http/HttpServer.js +72 -47
  100. package/lib/http/middleware/RateLimiter.js +5 -3
  101. package/lib/http/middleware/errorHandler.js +6 -1
  102. package/lib/http/middleware/requestLogger.js +14 -3
  103. package/lib/http/middleware/roleResolver.js +30 -23
  104. package/lib/http/routes/ai.js +387 -265
  105. package/lib/http/routes/auth.js +81 -61
  106. package/lib/http/routes/candidates.js +430 -320
  107. package/lib/http/routes/commands.js +289 -189
  108. package/lib/http/routes/extract.js +158 -125
  109. package/lib/http/routes/guardRules.js +309 -217
  110. package/lib/http/routes/knowledge.js +213 -154
  111. package/lib/http/routes/modules.js +578 -0
  112. package/lib/http/routes/monitoring.js +6 -6
  113. package/lib/http/routes/recipes.js +104 -93
  114. package/lib/http/routes/search.js +361 -305
  115. package/lib/http/routes/skills.js +145 -98
  116. package/lib/http/routes/snippets.js +42 -30
  117. package/lib/http/routes/spm.js +3 -405
  118. package/lib/http/routes/violations.js +113 -93
  119. package/lib/http/routes/wiki.js +211 -170
  120. package/lib/http/utils/routeHelpers.js +3 -1
  121. package/lib/http/utils/sse-sessions.js +16 -6
  122. package/lib/http/utils/sse.js +15 -5
  123. package/lib/infrastructure/audit/AuditLogger.js +5 -2
  124. package/lib/infrastructure/audit/AuditStore.js +10 -7
  125. package/lib/infrastructure/cache/CacheService.js +3 -1
  126. package/lib/infrastructure/cache/GraphCache.js +8 -4
  127. package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
  128. package/lib/infrastructure/config/ConfigLoader.js +9 -5
  129. package/lib/infrastructure/config/Defaults.js +30 -10
  130. package/lib/infrastructure/config/Paths.js +28 -8
  131. package/lib/infrastructure/config/TriggerSymbol.js +22 -10
  132. package/lib/infrastructure/database/DatabaseConnection.js +15 -10
  133. package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
  134. package/lib/infrastructure/external/ClipboardManager.js +6 -2
  135. package/lib/infrastructure/external/NativeUi.js +50 -43
  136. package/lib/infrastructure/external/OpenBrowser.js +14 -17
  137. package/lib/infrastructure/external/XcodeAutomation.js +14 -258
  138. package/lib/infrastructure/logging/Logger.js +46 -30
  139. package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
  140. package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
  141. package/lib/infrastructure/paths/HeaderResolver.js +25 -9
  142. package/lib/infrastructure/paths/PathFinder.js +34 -12
  143. package/lib/infrastructure/plugin/PluginManager.js +26 -8
  144. package/lib/infrastructure/realtime/RealtimeService.js +2 -2
  145. package/lib/infrastructure/vector/Chunker.js +22 -7
  146. package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
  147. package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
  148. package/lib/infrastructure/vector/VectorStore.js +28 -10
  149. package/lib/injection/ServiceContainer.js +247 -93
  150. package/lib/platform/ios/index.js +63 -0
  151. package/lib/platform/ios/routes/spm.js +437 -0
  152. package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
  153. package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
  154. package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
  155. package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
  156. package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
  157. package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
  158. package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
  159. package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
  160. package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
  161. package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
  162. package/lib/repository/base/BaseRepository.js +7 -9
  163. package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
  164. package/lib/repository/token/TokenUsageStore.js +4 -2
  165. package/lib/service/automation/ActionPipeline.js +1 -1
  166. package/lib/service/automation/AutomationOrchestrator.js +8 -4
  167. package/lib/service/automation/ContextCollector.js +7 -5
  168. package/lib/service/automation/DirectiveDetector.js +23 -16
  169. package/lib/service/automation/FileWatcher.js +112 -56
  170. package/lib/service/automation/TriggerResolver.js +6 -4
  171. package/lib/service/automation/handlers/AlinkHandler.js +24 -12
  172. package/lib/service/automation/handlers/CreateHandler.js +19 -20
  173. package/lib/service/automation/handlers/DraftHandler.js +14 -8
  174. package/lib/service/automation/handlers/GuardHandler.js +93 -63
  175. package/lib/service/automation/handlers/HeaderHandler.js +1 -6
  176. package/lib/service/automation/handlers/SearchHandler.js +155 -88
  177. package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
  178. package/lib/service/candidate/SimilarityService.js +25 -9
  179. package/lib/service/chat/AnalystAgent.js +50 -24
  180. package/lib/service/chat/CandidateGuardrail.js +143 -17
  181. package/lib/service/chat/ChatAgent.js +655 -260
  182. package/lib/service/chat/ContextWindow.js +116 -71
  183. package/lib/service/chat/ConversationStore.js +77 -36
  184. package/lib/service/chat/EpisodicConsolidator.js +47 -23
  185. package/lib/service/chat/HandoffProtocol.js +98 -22
  186. package/lib/service/chat/Memory.js +34 -14
  187. package/lib/service/chat/ProducerAgent.js +40 -20
  188. package/lib/service/chat/ProjectSemanticMemory.js +109 -78
  189. package/lib/service/chat/ReasoningLayer.js +148 -70
  190. package/lib/service/chat/ReasoningTrace.js +44 -32
  191. package/lib/service/chat/TaskPipeline.js +39 -19
  192. package/lib/service/chat/ToolRegistry.js +48 -29
  193. package/lib/service/chat/WorkingMemory.js +44 -18
  194. package/lib/service/chat/tools.js +1096 -494
  195. package/lib/service/context/RecipeExtractor.js +132 -51
  196. package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
  197. package/lib/service/cursor/KnowledgeCompressor.js +25 -22
  198. package/lib/service/cursor/RulesGenerator.js +13 -7
  199. package/lib/service/cursor/SkillsSyncer.js +77 -27
  200. package/lib/service/cursor/TokenBudget.js +2 -2
  201. package/lib/service/cursor/TopicClassifier.js +54 -20
  202. package/lib/service/guard/ComplianceReporter.js +55 -43
  203. package/lib/service/guard/ExclusionManager.js +67 -29
  204. package/lib/service/guard/GuardCheckEngine.js +381 -86
  205. package/lib/service/guard/GuardFeedbackLoop.js +22 -10
  206. package/lib/service/guard/GuardService.js +29 -19
  207. package/lib/service/guard/RuleLearner.js +55 -23
  208. package/lib/service/guard/SourceFileCollector.js +27 -20
  209. package/lib/service/guard/ViolationsStore.js +43 -38
  210. package/lib/service/knowledge/CodeEntityGraph.js +147 -82
  211. package/lib/service/knowledge/ConfidenceRouter.js +12 -10
  212. package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
  213. package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
  214. package/lib/service/knowledge/KnowledgeService.js +222 -112
  215. package/lib/service/module/ModuleService.js +969 -0
  216. package/lib/service/quality/FeedbackCollector.js +27 -15
  217. package/lib/service/quality/QualityScorer.js +78 -24
  218. package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
  219. package/lib/service/recipe/RecipeParser.js +78 -45
  220. package/lib/service/search/CoarseRanker.js +43 -28
  221. package/lib/service/search/CrossEncoderReranker.js +32 -21
  222. package/lib/service/search/InvertedIndex.js +21 -7
  223. package/lib/service/search/MultiSignalRanker.js +90 -28
  224. package/lib/service/search/RetrievalFunnel.js +45 -24
  225. package/lib/service/search/SearchEngine.js +255 -103
  226. package/lib/service/skills/EventAggregator.js +32 -15
  227. package/lib/service/skills/SignalCollector.js +140 -64
  228. package/lib/service/skills/SkillAdvisor.js +79 -42
  229. package/lib/service/skills/SkillHooks.js +16 -14
  230. package/lib/service/snippet/PlaceholderConverter.js +5 -0
  231. package/lib/service/snippet/SnippetFactory.js +116 -99
  232. package/lib/service/snippet/SnippetInstaller.js +234 -62
  233. package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
  234. package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
  235. package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
  236. package/lib/service/wiki/WikiGenerator.js +637 -263
  237. package/lib/shared/DimensionCopyRegistry.js +472 -0
  238. package/lib/shared/LanguageService.js +399 -0
  239. package/lib/shared/PathGuard.js +45 -28
  240. package/lib/shared/RecipeReadinessChecker.js +72 -12
  241. package/lib/shared/constants.js +41 -41
  242. package/lib/shared/errors/BaseError.js +2 -2
  243. package/lib/shared/errors/index.js +4 -4
  244. package/lib/shared/similarity.js +25 -8
  245. package/lib/shared/token-utils.js +6 -2
  246. package/lib/shared/utils/common.js +12 -4
  247. package/package.json +49 -13
  248. package/scripts/bench-real-projects.mjs +256 -0
  249. package/scripts/build-native-ui.js +30 -30
  250. package/scripts/clear-old-vector-index.js +5 -35
  251. package/scripts/clear-vector-cache.js +7 -37
  252. package/scripts/collect-test-project-stats.mjs +160 -0
  253. package/scripts/diagnose-mcp.js +41 -32
  254. package/scripts/ensure-parse-package.js +6 -9
  255. package/scripts/generate-recipe-drafts.js +116 -77
  256. package/scripts/init-db.js +3 -20
  257. package/scripts/init-snippets.js +305 -0
  258. package/scripts/init-vector-db.js +173 -170
  259. package/scripts/install-cursor-skill.js +148 -104
  260. package/scripts/install-full.js +8 -21
  261. package/scripts/install-vscode-copilot.js +146 -145
  262. package/scripts/migrate-md-to-knowledge.mjs +139 -151
  263. package/scripts/postinstall-safe.js +5 -17
  264. package/scripts/recipe-audit.js +106 -82
  265. package/scripts/release.js +283 -323
  266. package/scripts/setup-mcp-config.js +60 -52
  267. package/scripts/verify-context-api.js +20 -20
  268. package/skills/autosnippet-analysis/SKILL.md +10 -6
  269. package/skills/autosnippet-candidates/SKILL.md +27 -26
  270. package/skills/autosnippet-coldstart/SKILL.md +555 -38
  271. package/skills/autosnippet-concepts/SKILL.md +349 -337
  272. package/skills/autosnippet-create/SKILL.md +5 -5
  273. package/skills/autosnippet-reference-dart/SKILL.md +543 -0
  274. package/skills/autosnippet-reference-go/SKILL.md +539 -0
  275. package/skills/autosnippet-reference-java/SKILL.md +534 -0
  276. package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
  277. package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
  278. package/skills/autosnippet-reference-objc/SKILL.md +29 -6
  279. package/skills/autosnippet-reference-python/SKILL.md +800 -0
  280. package/skills/autosnippet-reference-swift/SKILL.md +70 -14
  281. package/skills/autosnippet-structure/SKILL.md +4 -4
  282. package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
  283. package/templates/recipes-setup/README.md +2 -2
  284. package/templates/recipes-setup/_template.md +1 -1
  285. package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
  286. package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
  287. package/resources/asd-entry/main.swift +0 -159
  288. package/scripts/build-asd-entry.js +0 -51
  289. package/scripts/init-xcode-snippets.js +0 -311
  290. package/template.json +0 -39
@@ -4,10 +4,10 @@
4
4
  */
5
5
 
6
6
  import express from 'express';
7
- import { asyncHandler } from '../middleware/errorHandler.js';
7
+ import Logger from '../../infrastructure/logging/Logger.js';
8
8
  import { getServiceContainer } from '../../injection/ServiceContainer.js';
9
9
  import { ValidationError } from '../../shared/errors/index.js';
10
- import Logger from '../../infrastructure/logging/Logger.js';
10
+ import { asyncHandler } from '../middleware/errorHandler.js';
11
11
  import { createStreamSession, getStreamSession } from '../utils/sse-sessions.js';
12
12
 
13
13
  const router = express.Router();
@@ -20,127 +20,137 @@ const logger = Logger.getInstance();
20
20
  * 对若干候选条目进行 AI 语义字段补全
21
21
  * Body: { candidateIds: string[] }
22
22
  */
23
- router.post('/enrich', asyncHandler(async (req, res) => {
24
- const { candidateIds } = req.body;
25
- if (!Array.isArray(candidateIds) || candidateIds.length === 0) {
26
- throw new ValidationError('candidateIds array is required and must not be empty');
27
- }
28
- if (candidateIds.length > 20) {
29
- throw new ValidationError('Max 20 candidates per enrichment call');
30
- }
31
-
32
- const container = getServiceContainer();
33
- const knowledgeService = container.get('knowledgeService');
34
- const aiProvider = container.get('aiProvider');
35
-
36
- // 收集候选条目
37
- const candidates = [];
38
- for (const id of candidateIds) {
39
- try {
40
- const entry = await knowledgeService.get(id);
41
- if (entry) {
42
- const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
43
- candidates.push({
44
- id: json.id,
45
- title: json.title,
46
- language: json.language,
47
- category: json.category,
48
- description: json.description,
49
- code: json.content?.pattern || '',
50
- rationale: json.content?.rationale,
51
- knowledgeType: json.knowledgeType,
52
- complexity: json.complexity,
53
- scope: json.scope,
54
- steps: json.content?.steps,
55
- constraints: json.constraints,
56
- });
57
- }
58
- } catch (err) {
59
- logger.warn(`enrich: failed to load candidate ${id}`, { error: err.message });
23
+ router.post(
24
+ '/enrich',
25
+ asyncHandler(async (req, res) => {
26
+ const { candidateIds } = req.body;
27
+ if (!Array.isArray(candidateIds) || candidateIds.length === 0) {
28
+ throw new ValidationError('candidateIds array is required and must not be empty');
29
+ }
30
+ if (candidateIds.length > 20) {
31
+ throw new ValidationError('Max 20 candidates per enrichment call');
60
32
  }
61
- }
62
33
 
63
- if (candidates.length === 0) {
64
- return res.json({ success: true, data: { enriched: 0, total: 0, results: [] } });
65
- }
34
+ const container = getServiceContainer();
35
+ const knowledgeService = container.get('knowledgeService');
36
+ const aiProvider = container.get('aiProvider');
66
37
 
67
- let enrichedCount = 0;
68
- const results = [];
38
+ // 收集候选条目
39
+ const candidates = [];
40
+ for (const id of candidateIds) {
41
+ try {
42
+ const entry = await knowledgeService.get(id);
43
+ if (entry) {
44
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
45
+ candidates.push({
46
+ id: json.id,
47
+ title: json.title,
48
+ language: json.language,
49
+ category: json.category,
50
+ description: json.description,
51
+ code: json.content?.pattern || '',
52
+ rationale: json.content?.rationale,
53
+ knowledgeType: json.knowledgeType,
54
+ complexity: json.complexity,
55
+ scope: json.scope,
56
+ steps: json.content?.steps,
57
+ constraints: json.constraints,
58
+ });
59
+ }
60
+ } catch (err) {
61
+ logger.warn(`enrich: failed to load candidate ${id}`, { error: err.message });
62
+ }
63
+ }
69
64
 
70
- if (aiProvider) {
71
- let enriched = [];
72
- try {
73
- enriched = await aiProvider.enrichCandidates(candidates);
74
- } catch (err) {
75
- logger.warn('AI enrichCandidates failed', { error: err.message });
65
+ if (candidates.length === 0) {
66
+ return res.json({ success: true, data: { enriched: 0, total: 0, results: [] } });
76
67
  }
77
68
 
78
- for (const item of enriched) {
79
- // 安全的 index 映射:AI 未返回 index 时根据数组位置推断
80
- const idx = typeof item.index === 'number' ? item.index : enriched.indexOf(item);
81
- const cand = candidates[idx];
82
- if (!cand) continue;
69
+ let enrichedCount = 0;
70
+ const results = [];
83
71
 
72
+ if (aiProvider) {
73
+ let enriched = [];
84
74
  try {
85
- const updateData = {};
86
- let changed = false;
87
-
88
- // content 嵌套字段(rationale / steps)共用一次 DB 读取
89
- const needsContentMerge = (item.rationale && !cand.rationale) ||
90
- (item.steps && (!cand.steps || cand.steps.length === 0));
91
- let contentBase = null;
92
- if (needsContentMerge) {
93
- const entry = await knowledgeService.get(cand.id);
94
- const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
95
- contentBase = { ...(json.content || {}) };
96
- }
75
+ enriched = await aiProvider.enrichCandidates(candidates);
76
+ } catch (err) {
77
+ logger.warn('AI enrichCandidates failed', { error: err.message });
78
+ }
97
79
 
98
- if (item.rationale && !cand.rationale) {
99
- contentBase.rationale = item.rationale;
100
- changed = true;
101
- }
102
- if (item.steps && (!cand.steps || cand.steps.length === 0)) {
103
- contentBase.steps = item.steps;
104
- changed = true;
105
- }
106
- if (contentBase && changed) {
107
- updateData.content = contentBase;
80
+ for (const item of enriched) {
81
+ // 安全的 index 映射:AI 未返回 index 时根据数组位置推断
82
+ const idx = typeof item.index === 'number' ? item.index : enriched.indexOf(item);
83
+ const cand = candidates[idx];
84
+ if (!cand) {
85
+ continue;
108
86
  }
109
87
 
110
- if (item.knowledgeType && !cand.knowledgeType) {
111
- updateData.knowledgeType = item.knowledgeType;
112
- changed = true;
113
- }
114
- if (item.complexity && !cand.complexity) {
115
- updateData.complexity = item.complexity;
116
- changed = true;
117
- }
118
- if (item.scope && !cand.scope) {
119
- updateData.scope = item.scope;
120
- changed = true;
121
- }
122
- if (item.constraints && !cand.constraints?.preconditions?.length) {
123
- updateData.constraints = item.constraints;
124
- changed = true;
125
- }
88
+ try {
89
+ const updateData = {};
90
+ let changed = false;
91
+
92
+ // content 嵌套字段(rationale / steps)共用一次 DB 读取
93
+ const needsContentMerge =
94
+ (item.rationale && !cand.rationale) ||
95
+ (item.steps && (!cand.steps || cand.steps.length === 0));
96
+ let contentBase = null;
97
+ if (needsContentMerge) {
98
+ const entry = await knowledgeService.get(cand.id);
99
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
100
+ contentBase = { ...(json.content || {}) };
101
+ }
126
102
 
127
- if (changed) {
128
- await knowledgeService.update(cand.id, updateData, { userId: 'dashboard-enrich' });
129
- enrichedCount++;
103
+ if (item.rationale && !cand.rationale) {
104
+ contentBase.rationale = item.rationale;
105
+ changed = true;
106
+ }
107
+ if (item.steps && (!cand.steps || cand.steps.length === 0)) {
108
+ contentBase.steps = item.steps;
109
+ changed = true;
110
+ }
111
+ if (contentBase && changed) {
112
+ updateData.content = contentBase;
113
+ }
114
+
115
+ if (item.knowledgeType && !cand.knowledgeType) {
116
+ updateData.knowledgeType = item.knowledgeType;
117
+ changed = true;
118
+ }
119
+ if (item.complexity && !cand.complexity) {
120
+ updateData.complexity = item.complexity;
121
+ changed = true;
122
+ }
123
+ if (item.scope && !cand.scope) {
124
+ updateData.scope = item.scope;
125
+ changed = true;
126
+ }
127
+ if (item.constraints && !cand.constraints?.preconditions?.length) {
128
+ updateData.constraints = item.constraints;
129
+ changed = true;
130
+ }
131
+
132
+ if (changed) {
133
+ await knowledgeService.update(cand.id, updateData, { userId: 'dashboard-enrich' });
134
+ enrichedCount++;
135
+ }
136
+ results.push({
137
+ id: cand.id,
138
+ enriched: changed,
139
+ filledFields: Object.keys(item).filter((k) => k !== 'index'),
140
+ });
141
+ } catch (err) {
142
+ logger.warn(`enrich: failed to update candidate ${cand.id}`, { error: err.message });
143
+ results.push({ id: cand.id, enriched: false, filledFields: [], error: err.message });
130
144
  }
131
- results.push({ id: cand.id, enriched: changed, filledFields: Object.keys(item).filter(k => k !== 'index') });
132
- } catch (err) {
133
- logger.warn(`enrich: failed to update candidate ${cand.id}`, { error: err.message });
134
- results.push({ id: cand.id, enriched: false, filledFields: [], error: err.message });
135
145
  }
136
146
  }
137
- }
138
147
 
139
- res.json({
140
- success: true,
141
- data: { enriched: enrichedCount, total: candidates.length, results },
142
- });
143
- }));
148
+ res.json({
149
+ success: true,
150
+ data: { enriched: enrichedCount, total: candidates.length, results },
151
+ });
152
+ })
153
+ );
144
154
 
145
155
  /* ═══ Bootstrap 内容润色 ═════════════════════════════════ */
146
156
 
@@ -149,21 +159,24 @@ router.post('/enrich', asyncHandler(async (req, res) => {
149
159
  * AI 内容润色(适用于 Bootstrap 产出的批量候选)
150
160
  * Body: { candidateIds?: string[], userPrompt?: string, dryRun?: boolean }
151
161
  */
152
- router.post('/bootstrap-refine', asyncHandler(async (req, res) => {
153
- const { candidateIds, userPrompt, dryRun } = req.body;
162
+ router.post(
163
+ '/bootstrap-refine',
164
+ asyncHandler(async (req, res) => {
165
+ const { candidateIds, userPrompt, dryRun } = req.body;
154
166
 
155
- const container = getServiceContainer();
167
+ const container = getServiceContainer();
156
168
 
157
- // 复用 MCP handler 的 bootstrapRefine 逻辑
158
- const { bootstrapRefine } = await import('../../external/mcp/handlers/bootstrap.js');
159
- const ctx = { container, logger };
160
- const result = await bootstrapRefine(ctx, { candidateIds, userPrompt, dryRun });
169
+ // 复用 MCP handler 的 bootstrapRefine 逻辑
170
+ const { bootstrapRefine } = await import('../../external/mcp/handlers/bootstrap.js');
171
+ const ctx = { container, logger };
172
+ const result = await bootstrapRefine(ctx, { candidateIds, userPrompt, dryRun });
161
173
 
162
- // envelope 返回 { success, data, meta, ... },直接取 data
163
- const data = result?.data ?? { refined: 0, total: 0, errors: [], results: [] };
174
+ // envelope 返回 { success, data, meta, ... },直接取 data
175
+ const data = result?.data ?? { refined: 0, total: 0, errors: [], results: [] };
164
176
 
165
- res.json({ success: true, data });
166
- }));
177
+ res.json({ success: true, data });
178
+ })
179
+ );
167
180
 
168
181
  /* ═══ 对话式润色 — 工具函数 ═══════════════════════════════ */
169
182
 
@@ -281,27 +294,63 @@ function buildUpdateFromRefineResult(before, parsed) {
281
294
  // ─── key 别名归一化:AI 可能返回不精确的 key,统一映射到标准 key ───
282
295
  const KEY_ALIASES = {
283
296
  // description 别名
284
- summary: 'description', desc: 'description', 摘要: 'description', 描述: 'description',
297
+ summary: 'description',
298
+ desc: 'description',
299
+ 摘要: 'description',
300
+ 描述: 'description',
285
301
  // pattern 别名
286
- content: 'pattern', designPattern: 'pattern',
287
- 内容: 'pattern', 代码: 'pattern', 标准用法: 'pattern',
302
+ content: 'pattern',
303
+ designPattern: 'pattern',
304
+ 内容: 'pattern',
305
+ 代码: 'pattern',
306
+ 标准用法: 'pattern',
288
307
  // markdown 别名
289
- markdownDoc: 'markdown', Markdown文档: 'markdown', 文档: 'markdown', doc: 'markdown',
308
+ markdownDoc: 'markdown',
309
+ Markdown文档: 'markdown',
310
+ 文档: 'markdown',
311
+ doc: 'markdown',
290
312
  // rationale 别名
291
- design: 'rationale', 设计原理: 'rationale', 原理: 'rationale', design_rationale: 'rationale', designRationale: 'rationale',
313
+ design: 'rationale',
314
+ 设计原理: 'rationale',
315
+ 原理: 'rationale',
316
+ design_rationale: 'rationale',
317
+ designRationale: 'rationale',
292
318
  // tags 别名
293
- tag: 'tags', label: 'tags', labels: 'tags', 标签: 'tags',
319
+ tag: 'tags',
320
+ label: 'tags',
321
+ labels: 'tags',
322
+ 标签: 'tags',
294
323
  // confidence 别名
295
- score: 'confidence', 置信度: 'confidence', 评分: 'confidence',
324
+ score: 'confidence',
325
+ 置信度: 'confidence',
326
+ 评分: 'confidence',
296
327
  // aiInsight 别名
297
- ai_insight: 'aiInsight', insight: 'aiInsight', aiinsight: 'aiInsight', 洞察: 'aiInsight',
328
+ ai_insight: 'aiInsight',
329
+ insight: 'aiInsight',
330
+ aiinsight: 'aiInsight',
331
+ 洞察: 'aiInsight',
298
332
  // agentNotes 别名
299
- agent_notes: 'agentNotes', notes: 'agentNotes', agentnotes: 'agentNotes', 笔记: 'agentNotes',
333
+ agent_notes: 'agentNotes',
334
+ notes: 'agentNotes',
335
+ agentnotes: 'agentNotes',
336
+ 笔记: 'agentNotes',
300
337
  // relations 别名
301
- relation: 'relations', 关联: 'relations', 关联关系: 'relations',
338
+ relation: 'relations',
339
+ 关联: 'relations',
340
+ 关联关系: 'relations',
302
341
  };
303
342
 
304
- const VALID_KEYS = new Set(['description', 'pattern', 'markdown', 'rationale', 'tags', 'confidence', 'aiInsight', 'agentNotes', 'relations']);
343
+ const VALID_KEYS = new Set([
344
+ 'description',
345
+ 'pattern',
346
+ 'markdown',
347
+ 'rationale',
348
+ 'tags',
349
+ 'confidence',
350
+ 'aiInsight',
351
+ 'agentNotes',
352
+ 'relations',
353
+ ]);
305
354
  const normalized = {};
306
355
 
307
356
  for (const [key, value] of Object.entries(parsed)) {
@@ -317,7 +366,9 @@ function buildUpdateFromRefineResult(before, parsed) {
317
366
 
318
367
  // 确保未返回的字段保留 before 值
319
368
  for (const k of VALID_KEYS) {
320
- if (!(k in normalized)) normalized[k] = before[k];
369
+ if (!(k in normalized)) {
370
+ normalized[k] = before[k];
371
+ }
321
372
  }
322
373
 
323
374
  const after = { ...before };
@@ -389,38 +440,49 @@ function buildUpdateFromRefineResult(before, parsed) {
389
440
  * 直接用用户提示词调用 AI 润色,返回 before/after 对比
390
441
  * Body: { candidateId: string, userPrompt: string }
391
442
  */
392
- router.post('/refine-preview', asyncHandler(async (req, res) => {
393
- const { candidateId, userPrompt } = req.body;
394
- if (!candidateId) throw new ValidationError('candidateId is required');
395
- if (!userPrompt || !userPrompt.trim()) throw new ValidationError('userPrompt is required');
396
-
397
- const container = getServiceContainer();
398
- const knowledgeService = container.get('knowledgeService');
399
- const aiProvider = container.get('aiProvider');
400
- if (!aiProvider) throw new ValidationError('AI provider not configured');
401
-
402
- const entry = await knowledgeService.get(candidateId);
403
- if (!entry) throw new ValidationError('Candidate not found');
404
- const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
405
- const before = extractBeforeFields(json);
406
-
407
- const prompt = buildRefinePrompt(before, userPrompt.trim());
408
- const parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
409
-
410
- if (!parsed) {
411
- return res.json({
412
- success: true,
413
- data: { candidateId, before, after: before, preview: {} },
414
- });
415
- }
443
+ router.post(
444
+ '/refine-preview',
445
+ asyncHandler(async (req, res) => {
446
+ const { candidateId, userPrompt } = req.body;
447
+ if (!candidateId) {
448
+ throw new ValidationError('candidateId is required');
449
+ }
450
+ if (!userPrompt || !userPrompt.trim()) {
451
+ throw new ValidationError('userPrompt is required');
452
+ }
416
453
 
417
- const { after } = buildUpdateFromRefineResult(before, parsed);
454
+ const container = getServiceContainer();
455
+ const knowledgeService = container.get('knowledgeService');
456
+ const aiProvider = container.get('aiProvider');
457
+ if (!aiProvider) {
458
+ throw new ValidationError('AI provider not configured');
459
+ }
418
460
 
419
- res.json({
420
- success: true,
421
- data: { candidateId, before, after, preview: parsed },
422
- });
423
- }));
461
+ const entry = await knowledgeService.get(candidateId);
462
+ if (!entry) {
463
+ throw new ValidationError('Candidate not found');
464
+ }
465
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
466
+ const before = extractBeforeFields(json);
467
+
468
+ const prompt = buildRefinePrompt(before, userPrompt.trim());
469
+ const parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
470
+
471
+ if (!parsed) {
472
+ return res.json({
473
+ success: true,
474
+ data: { candidateId, before, after: before, preview: {} },
475
+ });
476
+ }
477
+
478
+ const { after } = buildUpdateFromRefineResult(before, parsed);
479
+
480
+ res.json({
481
+ success: true,
482
+ data: { candidateId, before, after, preview: parsed },
483
+ });
484
+ })
485
+ );
424
486
 
425
487
  /* ═══ 对话式润色 — 流式预览 (SSE) ═══════════════════════ */
426
488
 
@@ -436,110 +498,144 @@ router.post('/refine-preview', asyncHandler(async (req, res) => {
436
498
  *
437
499
  * Body: { candidateId: string, userPrompt: string }
438
500
  */
439
- router.post('/refine-preview-stream', asyncHandler(async (req, res) => {
440
- const { candidateId, userPrompt } = req.body;
441
- if (!candidateId) throw new ValidationError('candidateId is required');
442
- if (!userPrompt || !userPrompt.trim()) throw new ValidationError('userPrompt is required');
443
-
444
- const container = getServiceContainer();
445
- const knowledgeService = container.get('knowledgeService');
446
- const aiProvider = container.get('aiProvider');
447
- if (!aiProvider) throw new ValidationError('AI provider not configured');
448
-
449
- const entry = await knowledgeService.get(candidateId);
450
- if (!entry) throw new ValidationError('Candidate not found');
451
- const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
452
- const before = extractBeforeFields(json);
453
-
454
- // ─── Session + EventSource 架构 ───
455
- const session = createStreamSession('refine');
456
- const prompt = buildRefinePrompt(before, userPrompt.trim());
457
-
458
- // 立即返回 sessionId
459
- res.json({ sessionId: session.sessionId });
460
-
461
- // 异步执行 AI 润色,通过 session 推送进度事件
462
- setImmediate(async () => {
463
- try {
464
- // 进度事件: AI 调用开始
465
- session.send({ type: 'data:progress', stage: 'ai_calling', message: 'AI 润色中...' });
466
-
467
- // 定时进度心跳 — AI 调用是阻塞的,前端需要看到动态变化
468
- const progressMsgs = [
469
- { delay: 3000, stage: 'analyzing', message: '正在分析候选内容...' },
470
- { delay: 8000, stage: 'generating', message: '正在生成润色建议...' },
471
- { delay: 16000, stage: 'thinking', message: 'AI 深度分析中...' },
472
- { delay: 28000, stage: 'almost_done', message: '即将完成,请稍候...' },
473
- ];
474
- const progressTimers = [];
475
- let aiDone = false;
476
- for (const pm of progressMsgs) {
477
- const t = setTimeout(() => {
478
- if (!aiDone) session.send({ type: 'data:progress', stage: pm.stage, message: pm.message });
479
- }, pm.delay);
480
- progressTimers.push(t);
481
- }
482
- // 超过 35 秒后每 15 秒报一次耗时
483
- const longTimer = setInterval(() => {
484
- if (aiDone) return;
485
- const elapsed = Math.round((Date.now() - session.createdAt) / 1000);
486
- session.send({ type: 'data:progress', stage: 'waiting', message: `AI 仍在处理中 (${elapsed}s)...` });
487
- }, 15_000);
488
- const longTimerStart = setTimeout(() => {}, 35_000); // placeholder
489
- progressTimers.push(longTimerStart);
490
-
491
- function clearProgressTimers() {
492
- aiDone = true;
493
- for (const t of progressTimers) clearTimeout(t);
494
- clearInterval(longTimer);
495
- }
501
+ router.post(
502
+ '/refine-preview-stream',
503
+ asyncHandler(async (req, res) => {
504
+ const { candidateId, userPrompt } = req.body;
505
+ if (!candidateId) {
506
+ throw new ValidationError('candidateId is required');
507
+ }
508
+ if (!userPrompt || !userPrompt.trim()) {
509
+ throw new ValidationError('userPrompt is required');
510
+ }
496
511
 
497
- // 使用 chatWithStructuredOutput 获取可靠的 JSON 结果(非流式),120 秒超时
498
- let parsed;
499
- try {
500
- parsed = await Promise.race([
501
- aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 }),
502
- new Promise((_, reject) => setTimeout(() => reject(new Error('AI refine timeout (120s)')), 120_000)),
503
- ]);
504
- } finally {
505
- clearProgressTimers();
506
- }
512
+ const container = getServiceContainer();
513
+ const knowledgeService = container.get('knowledgeService');
514
+ const aiProvider = container.get('aiProvider');
515
+ if (!aiProvider) {
516
+ throw new ValidationError('AI provider not configured');
517
+ }
518
+
519
+ const entry = await knowledgeService.get(candidateId);
520
+ if (!entry) {
521
+ throw new ValidationError('Candidate not found');
522
+ }
523
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
524
+ const before = extractBeforeFields(json);
507
525
 
508
- if (parsed) {
509
- // 进度事件: 构建 diff
510
- session.send({ type: 'data:progress', stage: 'building_diff', message: '生成修改对比...' });
526
+ // ─── Session + EventSource 架构 ───
527
+ const session = createStreamSession('refine');
528
+ const prompt = buildRefinePrompt(before, userPrompt.trim());
511
529
 
512
- const { after } = buildUpdateFromRefineResult(before, parsed);
513
- session.end({ candidateId, before, after, preview: parsed });
514
- } else {
515
- // 结构化输出失败,回退到 chat() 重试
516
- session.send({ type: 'data:progress', stage: 'fallback', message: 'AI 正在重新生成...' });
517
- const fullText = await aiProvider.chat(prompt, { temperature: 0.3 });
530
+ // 立即返回 sessionId
531
+ res.json({ sessionId: session.sessionId });
518
532
 
519
- let fallbackParsed = null;
520
- try {
521
- const jsonStr = fullText.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
522
- fallbackParsed = JSON.parse(jsonStr);
523
- } catch {
524
- const match = fullText.match(/\{[\s\S]*\}/);
525
- if (match) {
526
- try { fallbackParsed = JSON.parse(match[0]); } catch { /* ignore */ }
533
+ // 异步执行 AI 润色,通过 session 推送进度事件
534
+ setImmediate(async () => {
535
+ try {
536
+ // 进度事件: AI 调用开始
537
+ session.send({ type: 'data:progress', stage: 'ai_calling', message: 'AI 润色中...' });
538
+
539
+ // 定时进度心跳 — AI 调用是阻塞的,前端需要看到动态变化
540
+ const progressMsgs = [
541
+ { delay: 3000, stage: 'analyzing', message: '正在分析候选内容...' },
542
+ { delay: 8000, stage: 'generating', message: '正在生成润色建议...' },
543
+ { delay: 16000, stage: 'thinking', message: 'AI 深度分析中...' },
544
+ { delay: 28000, stage: 'almost_done', message: '即将完成,请稍候...' },
545
+ ];
546
+ const progressTimers = [];
547
+ let aiDone = false;
548
+ for (const pm of progressMsgs) {
549
+ const t = setTimeout(() => {
550
+ if (!aiDone) {
551
+ session.send({ type: 'data:progress', stage: pm.stage, message: pm.message });
552
+ }
553
+ }, pm.delay);
554
+ progressTimers.push(t);
555
+ }
556
+ // 超过 35 秒后每 15 秒报一次耗时
557
+ const longTimer = setInterval(() => {
558
+ if (aiDone) {
559
+ return;
527
560
  }
561
+ const elapsed = Math.round((Date.now() - session.createdAt) / 1000);
562
+ session.send({
563
+ type: 'data:progress',
564
+ stage: 'waiting',
565
+ message: `AI 仍在处理中 (${elapsed}s)...`,
566
+ });
567
+ }, 15_000);
568
+ const longTimerStart = setTimeout(() => {}, 35_000); // placeholder
569
+ progressTimers.push(longTimerStart);
570
+
571
+ function clearProgressTimers() {
572
+ aiDone = true;
573
+ for (const t of progressTimers) {
574
+ clearTimeout(t);
575
+ }
576
+ clearInterval(longTimer);
577
+ }
578
+
579
+ // 使用 chatWithStructuredOutput 获取可靠的 JSON 结果(非流式),120 秒超时
580
+ let parsed;
581
+ try {
582
+ parsed = await Promise.race([
583
+ aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 }),
584
+ new Promise((_, reject) =>
585
+ setTimeout(() => reject(new Error('AI refine timeout (120s)')), 120_000)
586
+ ),
587
+ ]);
588
+ } finally {
589
+ clearProgressTimers();
528
590
  }
529
591
 
530
- if (fallbackParsed) {
531
- const { after } = buildUpdateFromRefineResult(before, fallbackParsed);
532
- session.end({ candidateId, before, after, preview: fallbackParsed });
592
+ if (parsed) {
593
+ // 进度事件: 构建 diff
594
+ session.send({
595
+ type: 'data:progress',
596
+ stage: 'building_diff',
597
+ message: '生成修改对比...',
598
+ });
599
+
600
+ const { after } = buildUpdateFromRefineResult(before, parsed);
601
+ session.end({ candidateId, before, after, preview: parsed });
533
602
  } else {
534
- session.end({ candidateId, before, after: before, preview: null, rawText: fullText });
603
+ // 结构化输出失败,回退到 chat() 重试
604
+ session.send({ type: 'data:progress', stage: 'fallback', message: 'AI 正在重新生成...' });
605
+ const fullText = await aiProvider.chat(prompt, { temperature: 0.3 });
606
+
607
+ let fallbackParsed = null;
608
+ try {
609
+ const jsonStr = fullText
610
+ .replace(/^```(?:json)?\s*\n?/m, '')
611
+ .replace(/\n?```\s*$/m, '')
612
+ .trim();
613
+ fallbackParsed = JSON.parse(jsonStr);
614
+ } catch {
615
+ const match = fullText.match(/\{[\s\S]*\}/);
616
+ if (match) {
617
+ try {
618
+ fallbackParsed = JSON.parse(match[0]);
619
+ } catch {
620
+ /* ignore */
621
+ }
622
+ }
623
+ }
624
+
625
+ if (fallbackParsed) {
626
+ const { after } = buildUpdateFromRefineResult(before, fallbackParsed);
627
+ session.end({ candidateId, before, after, preview: fallbackParsed });
628
+ } else {
629
+ session.end({ candidateId, before, after: before, preview: null, rawText: fullText });
630
+ }
535
631
  }
632
+ } catch (err) {
633
+ logger.warn('SSE refine-preview stream error', { error: err.message });
634
+ session.error(err.message, 'REFINE_ERROR');
536
635
  }
537
- } catch (err) {
538
- logger.warn('SSE refine-preview stream error', { error: err.message });
539
- session.error(err.message, 'REFINE_ERROR');
540
- }
541
- });
542
- }));
636
+ });
637
+ })
638
+ );
543
639
 
544
640
  /**
545
641
  * GET /api/v1/candidates/refine-preview/events/:sessionId
@@ -566,7 +662,9 @@ router.get('/refine-preview/events/:sessionId', (req, res) => {
566
662
  }
567
663
 
568
664
  function writeEvent(event) {
569
- if (res.writableEnded) return;
665
+ if (res.writableEnded) {
666
+ return;
667
+ }
570
668
  res.write(`data: ${JSON.stringify(event)}\n\n`);
571
669
  }
572
670
 
@@ -618,79 +716,91 @@ router.get('/refine-preview/events/:sessionId', (req, res) => {
618
716
  * 若未提供 preview 则 fallback 重新调用 AI。
619
717
  * Body: { candidateId: string, userPrompt?: string, preview?: object }
620
718
  */
621
- router.post('/refine-apply', asyncHandler(async (req, res) => {
622
- const { candidateId, userPrompt, preview } = req.body;
623
- if (!candidateId) throw new ValidationError('candidateId is required');
624
-
625
- const container = getServiceContainer();
626
- const knowledgeService = container.get('knowledgeService');
627
-
628
- const entry = await knowledgeService.get(candidateId);
629
- if (!entry) throw new ValidationError('Candidate not found');
630
- const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
631
- const before = extractBeforeFields(json);
632
-
633
- // 优先使用前端传回的 preview(与预览阶段完全一致),否则重新调 AI
634
- let parsed = preview || null;
635
- if (!parsed) {
636
- if (!userPrompt || !userPrompt.trim()) {
637
- throw new ValidationError('Either preview or userPrompt is required');
719
+ router.post(
720
+ '/refine-apply',
721
+ asyncHandler(async (req, res) => {
722
+ const { candidateId, userPrompt, preview } = req.body;
723
+ if (!candidateId) {
724
+ throw new ValidationError('candidateId is required');
638
725
  }
639
- const aiProvider = container.get('aiProvider');
640
- if (!aiProvider) throw new ValidationError('AI provider not configured');
641
- const prompt = buildRefinePrompt(before, userPrompt.trim());
642
- parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
643
- }
644
726
 
645
- if (!parsed) {
646
- return res.json({
647
- success: true,
648
- data: { refined: 0, total: 1, candidate: json },
649
- });
650
- }
727
+ const container = getServiceContainer();
728
+ const knowledgeService = container.get('knowledgeService');
651
729
 
652
- const { after, updateData, changed } = buildUpdateFromRefineResult(before, parsed);
653
-
654
- if (changed) {
655
- // 处理需要嵌套写入的字段
656
- const finalUpdate = { ...updateData };
657
- delete finalUpdate._patternChanged;
658
- delete finalUpdate._confidenceChanged;
659
- delete finalUpdate._markdownChanged;
660
- delete finalUpdate._rationaleChanged;
661
-
662
- const contentPatch = { ...(json.content || {}) };
663
- let contentChanged = false;
664
- if (updateData._patternChanged != null) {
665
- contentPatch.pattern = updateData._patternChanged;
666
- contentChanged = true;
667
- }
668
- if (updateData._markdownChanged != null) {
669
- contentPatch.markdown = updateData._markdownChanged;
670
- contentChanged = true;
730
+ const entry = await knowledgeService.get(candidateId);
731
+ if (!entry) {
732
+ throw new ValidationError('Candidate not found');
671
733
  }
672
- if (updateData._rationaleChanged != null) {
673
- contentPatch.rationale = updateData._rationaleChanged;
674
- contentChanged = true;
675
- }
676
- if (contentChanged) {
677
- finalUpdate.content = contentPatch;
734
+ const json = typeof entry.toJSON === 'function' ? entry.toJSON() : entry;
735
+ const before = extractBeforeFields(json);
736
+
737
+ // 优先使用前端传回的 preview(与预览阶段完全一致),否则重新调 AI
738
+ let parsed = preview || null;
739
+ if (!parsed) {
740
+ if (!userPrompt || !userPrompt.trim()) {
741
+ throw new ValidationError('Either preview or userPrompt is required');
742
+ }
743
+ const aiProvider = container.get('aiProvider');
744
+ if (!aiProvider) {
745
+ throw new ValidationError('AI provider not configured');
746
+ }
747
+ const prompt = buildRefinePrompt(before, userPrompt.trim());
748
+ parsed = await aiProvider.chatWithStructuredOutput(prompt, { temperature: 0.3 });
678
749
  }
679
- if (updateData._confidenceChanged != null) {
680
- finalUpdate.reasoning = { ...(json.reasoning || {}), confidence: updateData._confidenceChanged };
750
+
751
+ if (!parsed) {
752
+ return res.json({
753
+ success: true,
754
+ data: { refined: 0, total: 1, candidate: json },
755
+ });
681
756
  }
682
757
 
683
- await knowledgeService.update(candidateId, finalUpdate, { userId: 'dashboard-refine' });
684
- }
758
+ const { updateData, changed } = buildUpdateFromRefineResult(before, parsed);
759
+
760
+ if (changed) {
761
+ // 处理需要嵌套写入的字段
762
+ const finalUpdate = { ...updateData };
763
+ delete finalUpdate._patternChanged;
764
+ delete finalUpdate._confidenceChanged;
765
+ delete finalUpdate._markdownChanged;
766
+ delete finalUpdate._rationaleChanged;
767
+
768
+ const contentPatch = { ...(json.content || {}) };
769
+ let contentChanged = false;
770
+ if (updateData._patternChanged != null) {
771
+ contentPatch.pattern = updateData._patternChanged;
772
+ contentChanged = true;
773
+ }
774
+ if (updateData._markdownChanged != null) {
775
+ contentPatch.markdown = updateData._markdownChanged;
776
+ contentChanged = true;
777
+ }
778
+ if (updateData._rationaleChanged != null) {
779
+ contentPatch.rationale = updateData._rationaleChanged;
780
+ contentChanged = true;
781
+ }
782
+ if (contentChanged) {
783
+ finalUpdate.content = contentPatch;
784
+ }
785
+ if (updateData._confidenceChanged != null) {
786
+ finalUpdate.reasoning = {
787
+ ...(json.reasoning || {}),
788
+ confidence: updateData._confidenceChanged,
789
+ };
790
+ }
685
791
 
686
- // 返回更新后的条目
687
- const updated = changed ? await knowledgeService.get(candidateId) : entry;
688
- const updatedJson = typeof updated?.toJSON === 'function' ? updated.toJSON() : updated;
792
+ await knowledgeService.update(candidateId, finalUpdate, { userId: 'dashboard-refine' });
793
+ }
689
794
 
690
- res.json({
691
- success: true,
692
- data: { refined: changed ? 1 : 0, total: 1, candidate: updatedJson },
693
- });
694
- }));
795
+ // 返回更新后的条目
796
+ const updated = changed ? await knowledgeService.get(candidateId) : entry;
797
+ const updatedJson = typeof updated?.toJSON === 'function' ? updated.toJSON() : updated;
798
+
799
+ res.json({
800
+ success: true,
801
+ data: { refined: changed ? 1 : 0, total: 1, candidate: updatedJson },
802
+ });
803
+ })
804
+ );
695
805
 
696
806
  export default router;