autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
252 lines (251 loc) • 8.6 kB
JavaScript
/**
* Commands API 路由
* 执行 Module Map 刷新、Embed (重建索引) 等命令
*/
import express from 'express';
import Logger from '../../infrastructure/logging/Logger.js';
import { getServiceContainer } from '../../injection/ServiceContainer.js';
import { FileReadQuery, FileSaveBody } from '../../shared/schemas/http-requests.js';
import { validate, validateQuery } from '../middleware/validate.js';
const router = express.Router();
const logger = Logger.getInstance();
/**
* POST /api/v1/commands/spm-map
* 执行 SPM 依赖映射刷新(向后兼容)
*/
router.post('/spm-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,
});
});
/**
* POST /api/v1/commands/embed
* 全量重建语义索引
*/
router.post('/embed', async (req, res) => {
const container = getServiceContainer();
// Mock 模式下向量构建需要 embedding — 拒绝执行
const manager = container.singletons?._aiProviderManager;
if (manager?.isMock) {
res.status(400).json({
success: false,
message: 'AI Provider 未配置,当前为 Mock 模式。Embedding 不可用。',
});
return;
}
// 优先使用 VectorService (新架构), 降级到 indexingPipeline (旧架构)
const vectorService = container.services.vectorService ? container.get('vectorService') : null;
let result;
if (vectorService) {
const clearFirst = req.body?.clear !== false;
if (clearFirst) {
await vectorService.clear();
}
const buildResult = await vectorService.fullBuild({
force: req.body?.force ?? false,
});
result = {
scanned: buildResult.scanned,
chunked: buildResult.chunked,
embedded: buildResult.embedded,
upserted: buildResult.upserted,
skipped: buildResult.skipped,
errors: buildResult.errors,
};
}
else {
const indexingPipeline = container.get('indexingPipeline');
result = await indexingPipeline.run({
clear: req.body?.clear !== false,
force: req.body?.force ?? false,
});
}
logger.info('Semantic index rebuilt via dashboard', { result });
res.json({
success: true,
data: {
scanned: result.scanned || 0,
chunked: result.chunked || 0,
embedded: result.embedded || 0,
upserted: result.upserted || 0,
skipped: result.skipped || 0,
errors: result.errors || 0,
},
});
});
/**
* GET /api/v1/commands/status
* 获取命令执行状态(Snippet 同步状态、索引状态等)
*/
router.get('/status', async (req, res) => {
const container = getServiceContainer();
const status = {
index: { ready: false },
spmMap: { available: false },
};
try {
const _indexingPipeline = container.get('indexingPipeline');
status.index.ready = true; // IndexingPipeline is available
}
catch {
/* ignore */
}
try {
const moduleService = container.get('moduleService');
await moduleService.load();
status.spmMap.available = (await moduleService.listTargets()).length > 0;
}
catch {
/* ignore */
}
res.json({ success: true, data: status });
});
// ─── File Operations (for Xcode Simulator page) ─────
/**
* GET /api/v1/commands/files/tree
* Get project file tree – only .h / .m / .swift source files
*/
router.get('/files/tree', async (req, res) => {
const fs = await import('node:fs');
const path = await import('node:path');
const container = getServiceContainer();
const projectRoot = container.singletons?._projectRoot || process.cwd();
const SOURCE_EXTS = new Set(['.h', '.m', '.swift']);
const SKIP_DIRS = new Set([
'node_modules',
'.git',
'Pods',
'build',
'DerivedData',
'.build',
'dist',
'vendor',
]);
/**
* Recursively scan dir, returning FileNode or null if folder has no matching files.
*/
function scanDir(dirPath) {
const dirName = path.default.basename(dirPath);
if (SKIP_DIRS.has(dirName)) {
return null;
}
let entries;
try {
entries = fs.default.readdirSync(dirPath, { withFileTypes: true });
}
catch {
return null;
}
const children = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue; // skip hidden
}
const fullPath = path.default.join(dirPath, entry.name);
if (entry.isDirectory()) {
const sub = scanDir(fullPath);
if (sub) {
children.push(sub);
}
}
else if (entry.isFile()) {
const ext = path.default.extname(entry.name).toLowerCase();
if (SOURCE_EXTS.has(ext)) {
children.push({
type: 'file',
name: entry.name,
path: fullPath,
relativePath: path.default.relative(projectRoot, fullPath),
ext,
});
}
}
}
if (children.length === 0) {
return null;
}
// Sort: folders first, then alphabetical
children.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
return {
type: 'folder',
name: dirName,
path: dirPath,
children,
};
}
const tree = scanDir(projectRoot) || {
type: 'folder',
name: path.default.basename(projectRoot),
path: projectRoot,
children: [],
};
res.json({ success: true, data: tree });
});
/**
* GET /api/v1/commands/files/read
* Read file content (limited to projectRoot)
*/
router.get('/files/read', validateQuery(FileReadQuery), async (req, res) => {
const filePath = req.query.path;
const path = await import('node:path');
const container = getServiceContainer();
const projectRoot = container.singletons?._projectRoot || process.cwd();
const resolved = path.default.resolve(projectRoot, filePath);
// 防止路径遍历:确保解析后的路径在 projectRoot 内
if (!resolved.startsWith(projectRoot + path.default.sep) && resolved !== projectRoot) {
return void res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: 'Access denied: path outside project root' },
});
}
const fs = await import('node:fs');
try {
const content = fs.default.readFileSync(resolved, 'utf8');
res.json({ success: true, data: { content } });
}
catch {
res
.status(404)
.json({ success: false, error: { code: 'NOT_FOUND', message: 'File not found' } });
}
});
/**
* POST /api/v1/commands/files/save
* Save file content (limited to projectRoot)
*/
router.post('/files/save', validate(FileSaveBody), async (req, res) => {
const { path: filePath, content } = req.body;
const pathMod = await import('node:path');
const container = getServiceContainer();
const projectRoot = container.singletons?._projectRoot || process.cwd();
const resolved = pathMod.default.resolve(projectRoot, filePath);
// 防止路径遍历:确保解析后的路径在 projectRoot 内
if (!resolved.startsWith(projectRoot + pathMod.default.sep) && resolved !== projectRoot) {
return void res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: 'Access denied: path outside project root' },
});
}
const fs = await import('node:fs');
try {
fs.default.writeFileSync(resolved, content, 'utf8');
res.json({ success: true });
}
catch (err) {
res.status(500).json({
success: false,
error: { code: 'INTERNAL_ERROR', message: err.message },
});
}
});
export default router;