autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
162 lines (161 loc) • 6.28 kB
JavaScript
/**
* Extract API 路由
* 从路径或文本提取 Recipe 候选
*/
import { basename } from 'node:path';
import express from 'express';
import Logger from '../../infrastructure/logging/Logger.js';
import { getServiceContainer } from '../../injection/ServiceContainer.js';
import { LanguageService } from '../../shared/LanguageService.js';
import { ExtractPathBody, ExtractTextBody } from '../../shared/schemas/http-requests.js';
import { validate } from '../middleware/validate.js';
const router = express.Router();
const logger = Logger.getInstance();
/**
* POST /api/v1/extract/path
* 从文件路径提取代码片段
* 管线: RecipeParser(MD解析) → AI 提取(AgentRuntime) → 原始兜底
*/
router.post('/path', validate(ExtractPathBody), async (req, res) => {
const { relativePath, projectRoot: bodyRoot } = req.body;
const container = getServiceContainer();
const recipeParser = container.get('recipeParser');
// 优先用请求体的 projectRoot,其次用 ServiceContainer 中注册的全局值
const projectRoot = bodyRoot || container.singletons?._projectRoot || process.cwd();
logger.debug('extract/path: resolved projectRoot', {
relativePath,
projectRoot,
source: bodyRoot ? 'body' : container.singletons?._projectRoot ? 'container' : 'cwd',
});
// 1. RecipeParser 解析(对 Recipe MD 文件有效)
const result = await recipeParser.extractFromPath(relativePath, {
projectRoot,
});
const items = result.items || result;
// 2. 判断是否为"原始兜底"结果(无 frontmatter → summary/usageGuide 全空)
const isRawFallback = Array.isArray(items) &&
items.length > 0 &&
!items[0].summary &&
!items[0].usageGuide &&
!items[0].frontmatter?.title;
if (isRawFallback) {
// 3. 尝试 AI 提取
try {
const agentFactory = container.get('agentFactory');
const file = items[0];
const fileName = basename(relativePath); // 保留扩展名: BDMineViewController.m
const aiResult = await agentFactory.scanKnowledge({
label: fileName,
files: [{ name: fileName, content: file.code || '' }],
task: 'extract',
comprehensive: true,
});
if (aiResult &&
!aiResult.error &&
Array.isArray(aiResult.recipes) &&
aiResult.recipes.length > 0) {
logger.info('extract/path: AI extraction succeeded', { count: aiResult.recipes.length });
return void res.json({
success: true,
data: {
result: aiResult.recipes,
isMarked: false,
},
});
}
}
catch (err) {
logger.debug('extract/path: AI extraction failed, using raw fallback', {
error: err.message,
});
}
}
// 4. 返回 RecipeParser 结果(MD 文件或 AI 不可用时的原始兜底)
res.json({
success: true,
data: {
result: items,
isMarked: result.isMarked || false,
},
});
});
/**
* POST /api/v1/extract/text
* 从文本内容提取代码片段(剪贴板等)
* 管线: RecipeParser(MD解析) → AI 提取(AgentRuntime) → 基础兜底
*/
router.post('/text', validate(ExtractTextBody), async (req, res) => {
const { text, language, relativePath, projectRoot: bodyRoot } = req.body;
const container = getServiceContainer();
const recipeParser = container.get('recipeParser');
const _projectRoot = bodyRoot || container.singletons?._projectRoot || process.cwd();
// 1. 先尝试解析为 Recipe Markdown 格式
let result;
try {
result = await recipeParser.parseFromText(text, {
language,
relativePath,
});
// 解析成功,直接返回
return void res.json({
success: true,
data: {
result: Array.isArray(result) ? result : [result],
source: 'text',
},
});
}
catch (error) {
logger.debug('Recipe MD parse failed, trying AI extraction', {
error: error.message,
});
}
// 2. Recipe MD 解析失败 → 尝试 AI 提取
try {
const agentFactory = container.get('agentFactory');
if (agentFactory) {
const lang = language ||
(relativePath ? LanguageService.inferLang(relativePath) || 'unknown' : 'unknown');
const ext = LanguageService.extForLang(lang) || '.txt';
const fileName = relativePath ? basename(relativePath) : `clipboard${ext}`;
const aiResult = await agentFactory.scanKnowledge({
label: fileName,
files: [{ name: fileName, content: text }],
task: 'extract',
comprehensive: true,
});
if (aiResult &&
!aiResult.error &&
Array.isArray(aiResult.recipes) &&
aiResult.recipes.length > 0) {
logger.info('extract/text: AI extraction succeeded', { count: aiResult.recipes.length });
// 多条 Recipe 时在第一条上标记总数(供前端提示)
if (aiResult.recipes.length > 1) {
aiResult.recipes[0]._multipleCount = aiResult.recipes.length;
}
return void res.json({
success: true,
data: {
result: aiResult.recipes,
source: 'text',
},
});
}
}
}
catch (err) {
logger.debug('extract/text: AI extraction failed, using basic fallback', {
error: err.message,
});
}
// 3. AI 也失败 → 基础代码块提取兜底
result = await recipeParser.extractFromText(text, { language });
res.json({
success: true,
data: {
result: Array.isArray(result) ? result : [result],
source: 'text',
},
});
});
export default router;