autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
509 lines (508 loc) • 17.8 kB
JavaScript
/**
* Modules API 路由 — 统一多语言模块扫描
* 替代 spm.js,提供语言无关的模块管理、依赖图、AI 扫描
*
* 所有端点通过 container.get('moduleService') 获取 ModuleService 实例
*/
import express from 'express';
import { ModuleBootstrapBody, ModuleRescanBody, ScanFolderBody, ScanProjectBody, ScanTargetBody, } from '#shared/schemas/http-requests.js';
import Logger from '../../infrastructure/logging/Logger.js';
import { getServiceContainer } from '../../injection/ServiceContainer.js';
import { validate } from '../middleware/validate.js';
import { createStreamSession, getStreamSession } from '../utils/sse-sessions.js';
const router = express.Router();
const logger = Logger.getInstance();
/**
* GET /api/v1/modules/targets
* 获取所有模块 Target 列表(多语言合并)
*/
router.get('/targets', async (req, res) => {
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const targets = await moduleService.listTargets();
res.json({
success: true,
data: {
targets,
total: targets.length,
projectInfo: moduleService.getProjectInfo(),
},
});
});
/**
* GET /api/v1/modules/dep-graph
* 获取模块依赖关系图
*/
router.get('/dep-graph', async (req, res) => {
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const level = String(req.query.level || 'package');
const _graphBase = await moduleService.getDependencyGraph({ level });
const graph = _graphBase;
if (!graph || (!graph.nodes && !graph.packages)) {
return void res.json({
success: true,
data: { nodes: [], edges: [], projectRoot: null },
});
}
// 标准化为 { nodes, edges } 格式
let nodes = [];
let edges = [];
if (graph.nodes && graph.edges) {
nodes = graph.nodes;
edges = graph.edges;
}
else if (graph.packages) {
// SPM 格式兼容:从 packages 构建图
if (level === 'target') {
for (const [pkgName, pkgInfo] of Object.entries(graph.packages)) {
const pkgRecord = pkgInfo;
const targetsInfo = (pkgRecord?.targetsInfo || {});
for (const [targetName, info] of Object.entries(targetsInfo)) {
const id = `${pkgName}::${targetName}`;
nodes.push({
id,
label: targetName,
type: 'target',
packageName: pkgName,
});
const deps = (info?.dependencies || []);
for (const d of deps) {
if (!d?.name) {
continue;
}
const depPkg = d?.package || pkgName;
edges.push({ from: id, to: `${depPkg}::${d.name}`, source: 'base' });
}
}
}
}
else {
const pkgs = graph.packages;
nodes = Object.keys(pkgs).map((id) => ({
id,
label: id,
type: 'package',
packageDir: pkgs[id]?.packageDir,
targets: pkgs[id]?.targets,
}));
for (const [from, tos] of Object.entries(graph.edges || {})) {
for (const to of tos || []) {
edges.push({ from, to, source: 'base' });
}
}
}
}
res.json({
success: true,
data: {
nodes,
edges,
projectRoot: graph.projectRoot || null,
generatedAt: graph.generatedAt || null,
},
});
});
/**
* GET /api/v1/modules/browse-dirs
* 浏览项目目录结构 — 供前端选择要扫描的文件夹
*/
router.get('/browse-dirs', async (req, res) => {
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const basePath = req.query.path || '';
const maxDepth = Math.min(Number.parseInt(req.query.depth || '3', 10), 5);
const dirs = await moduleService.browseDirectories(basePath, maxDepth);
res.json({
success: true,
data: {
directories: dirs,
total: dirs.length,
basePath: basePath || '.',
projectRoot: moduleService.getProjectInfo().projectRoot,
},
});
});
/**
* POST /api/v1/modules/scan-folder
* 扫描任意目录 — 直接走 AI 管线(无需 Discoverer 检测)
*/
router.post('/scan-folder', validate(ScanFolderBody), async (req, res) => {
const { path: folderPath, options = {} } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const result = await moduleService.scanFolder(folderPath, options);
res.json({
success: true,
data: result,
});
});
/**
* POST /api/v1/modules/scan-folder/stream
* 流式扫描任意目录 — SSE Session 架构
*/
router.post('/scan-folder/stream', validate(ScanFolderBody), async (req, res) => {
const { path: folderPath, options = {} } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const streamSession = createStreamSession('scan');
const sessionId = streamSession.sessionId;
const session = getStreamSession(sessionId);
res.json({ sessionId });
// 异步执行扫描,事件推送到 session
setImmediate(async () => {
try {
const result = await moduleService.scanFolder(folderPath, {
...options,
onProgress: (evt) => {
if (session) {
session.push(evt);
}
},
});
if (session) {
session.push({
type: 'scan:result',
recipes: result.recipes || [],
scannedFiles: result.scannedFiles || [],
message: result.message || '',
noAi: !!result.noAi,
});
session.push({ type: 'scan:done' });
}
}
catch (err) {
logger.error(`[modules] scan-folder/stream error: ${err.message}`);
if (session) {
session.push({ type: 'scan:error', message: err.message });
session.push({ type: 'scan:done' });
}
}
});
});
/**
* POST /api/v1/modules/target-files
* 获取模块的文件列表
*/
router.post('/target-files', validate(ScanTargetBody), async (req, res) => {
const { target, targetName } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
let resolvedTarget = target;
if (!resolvedTarget && targetName) {
const targets = await moduleService.listTargets();
resolvedTarget = targets.find((t) => t.name === targetName);
if (!resolvedTarget) {
return void res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: `Module not found: ${targetName}` },
});
}
}
const files = await moduleService.getTargetFiles(resolvedTarget);
res.json({
success: true,
data: {
target: resolvedTarget.name || targetName,
files,
total: files.length,
},
});
});
/**
* POST /api/v1/modules/scan
* AI 扫描模块,发现候选项
*/
router.post('/scan', validate(ScanTargetBody), async (req, res) => {
const { target, targetName, options = {} } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
let resolvedTarget = target;
if (!resolvedTarget && targetName) {
const targets = await moduleService.listTargets();
resolvedTarget = targets.find((t) => t.name === targetName);
if (!resolvedTarget) {
return void res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: `Module not found: ${targetName}` },
});
}
}
logger.info('Module scan started via dashboard', {
target: resolvedTarget.name,
discoverer: resolvedTarget.discovererId,
});
const result = await moduleService.scanTarget(resolvedTarget, options);
res.json({
success: true,
data: result,
});
});
// ── 流式 Target 扫描(SSE Session + EventSource 架构) ─────────
/**
* POST /api/v1/modules/scan/stream
* 创建流式扫描会话,后台异步执行 AI 扫描
*/
router.post('/scan/stream', validate(ScanTargetBody), async (req, res) => {
const { target, targetName, options = {} } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
let resolvedTarget = target;
if (!resolvedTarget && targetName) {
const targets = await moduleService.listTargets();
resolvedTarget = targets.find((t) => t.name === targetName);
if (!resolvedTarget) {
return void res.status(404).json({
success: false,
error: { code: 'NOT_FOUND', message: `Module not found: ${targetName}` },
});
}
}
// 创建 SSE session
const session = createStreamSession('scan');
const tName = resolvedTarget.name || targetName;
// 立即返回 sessionId
res.json({ sessionId: session.sessionId });
// 异步执行扫描,通过 session 推送进度事件
setImmediate(async () => {
try {
logger.info('Module stream scan started', {
target: tName,
sessionId: session.sessionId,
});
const result = await moduleService.scanTarget(resolvedTarget, {
...options,
onProgress(event) {
session.send(event);
},
});
// 发送最终结果
session.send({
type: 'scan:result',
recipes: result.recipes || [],
scannedFiles: result.scannedFiles || [],
message: result.message || '',
noAi: !!result.noAi,
recipeCount: (result.recipes || []).length,
fileCount: (result.scannedFiles || []).length,
});
session.end();
}
catch (err) {
logger.error('Module stream scan failed', { target: tName, error: err.message });
session.error(err.message, 'SCAN_ERROR');
}
});
});
/**
* GET /api/v1/modules/scan/events/:sessionId
* EventSource SSE 端点 — 消费扫描进度事件
*/
router.get('/scan/events/:sessionId', (req, res) => {
const session = getStreamSession(req.params.sessionId);
if (!session) {
res.status(404).json({ success: false, error: 'Session not found or expired' });
return;
}
// ─── SSE Headers ───
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders();
if (res.socket) {
res.socket.setNoDelay(true);
res.socket.setTimeout(0);
}
function writeEvent(event) {
if (res.writableEnded) {
return;
}
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
// 1) 回放缓冲区
let isDone = false;
for (const event of session.buffer) {
writeEvent(event);
if (event.type === 'stream:done' || event.type === 'stream:error') {
isDone = true;
}
}
if (isDone || session.completed) {
res.end();
return;
}
// 2) 订阅实时事件
const unsubscribe = session.on((event) => {
writeEvent(event);
if (event.type === 'stream:done' || event.type === 'stream:error') {
unsubscribe();
clearInterval(heartbeat);
res.end();
}
});
// 心跳保活 (每 15 秒)
const heartbeat = setInterval(() => {
if (res.writableEnded) {
clearInterval(heartbeat);
return;
}
res.write(`: ping ${Date.now()}\n\n`);
}, 15_000);
// 客户端断开连接时清理
res.on('close', () => {
unsubscribe();
clearInterval(heartbeat);
});
});
/**
* POST /api/v1/modules/scan-project
* 全项目扫描:AI 提取候选 + Guard 审计
*/
router.post('/scan-project', validate(ScanProjectBody), async (req, res) => {
const { options = {} } = req.body;
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
logger.info('Full project scan started via dashboard (ModuleService)');
const result = await moduleService.scanProject(options);
res.json({
success: true,
data: result,
});
});
/**
* POST /api/v1/modules/update-map
* 刷新模块映射(替代 spm-map)
*/
router.post('/update-map', async (req, res) => {
const container = getServiceContainer();
const moduleService = container.get('moduleService');
const result = await moduleService.updateModuleMap({
aggressive: true,
});
logger.info('Module map updated via dashboard', { result });
res.json({
success: true,
data: result,
});
});
/**
* GET /api/v1/modules/project-info
* 项目信息(检测到的语言、框架等)
*/
router.get('/project-info', async (req, res) => {
const container = getServiceContainer();
const moduleService = container.get('moduleService');
await moduleService.load();
const info = moduleService.getProjectInfo();
res.json({
success: true,
data: info,
});
});
/**
* POST /api/v1/modules/bootstrap
* 冷启动:快速骨架 + 异步逐维度填充
*/
router.post('/bootstrap', validate(ModuleBootstrapBody), async (req, res) => {
const { maxFiles, skipGuard, contentMaxLines } = req.body || {};
const container = getServiceContainer();
logger.info('Bootstrap cold start initiated (ModuleService path)');
// 直接调用 bootstrap-internal handler(统一编排管线)
const { bootstrapKnowledge } = await import('#external/mcp/handlers/bootstrap-internal.js');
const raw = await bootstrapKnowledge({ container, logger }, {
maxFiles: maxFiles || 500,
skipGuard: skipGuard || false,
contentMaxLines: contentMaxLines || 120,
loadSkills: true,
});
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
const bootstrapResult = parsed?.data || parsed;
res.json({
success: true,
data: {
...bootstrapResult,
asyncFill: true,
},
});
});
/**
* GET /api/v1/modules/bootstrap/status
* 查询 bootstrap 异步填充进度
*/
router.get('/bootstrap/status', async (req, res) => {
const container = getServiceContainer();
let taskManager = null;
try {
taskManager = container.get('bootstrapTaskManager');
}
catch {
/* not registered */
}
if (!taskManager) {
return void res.json({
success: true,
data: { status: 'idle', message: 'No bootstrap task manager initialized' },
});
}
res.json({
success: true,
data: taskManager.getSessionStatus(),
});
});
/**
* POST /api/v1/modules/bootstrap/cancel
* 取消正在运行的 bootstrap / rescan 异步填充会话
*/
router.post('/bootstrap/cancel', async (req, res) => {
const container = getServiceContainer();
let taskManager = null;
try {
taskManager = container.get('bootstrapTaskManager');
}
catch {
/* not registered */
}
if (!taskManager) {
return void res.json({ success: true, message: 'No bootstrap task manager initialized' });
}
if (!taskManager.isRunning) {
return void res.json({ success: true, message: 'No active bootstrap session' });
}
const reason = req.body?.reason || 'Cancelled by user via Dashboard';
taskManager.abortSession(reason);
logger.info('Bootstrap session cancelled via HTTP', { reason });
res.json({
success: true,
data: taskManager.getSessionStatus(),
});
});
/**
* POST /api/v1/modules/rescan
* 增量扫描:保留已有 Recipe,重新分析项目,补齐缺失知识
* 使用内部 Agent pipeline 自动完成知识补齐
*/
router.post('/rescan', validate(ModuleRescanBody), async (req, res) => {
const { reason, dimensions } = req.body || {};
const container = getServiceContainer();
logger.info('Rescan (internal) initiated from Dashboard', { reason, dimensions });
// 直接调用 rescan-internal handler(统一编排管线)
const { rescanInternal } = await import('#external/mcp/handlers/rescan-internal.js');
const raw = await rescanInternal({ container, logger }, { reason: reason || 'dashboard-rescan', dimensions });
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
const result = parsed?.data || parsed;
res.json({
success: true,
data: result,
});
});
export default router;