autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
269 lines (268 loc) • 8.75 kB
JavaScript
/**
* Skills API 路由
* 管理 Agent Skills 的查询、加载和创建(项目级)
*/
import express from 'express';
import { CreateSkillBody, UpdateSkillBody } from '#shared/schemas/http-requests.js';
import { createSkill, deleteSkill, listSkills, loadSkill, suggestSkills, updateSkill, } from '../../external/mcp/handlers/skill.js';
import { getServiceContainer } from '../../injection/ServiceContainer.js';
import { validate } from '../middleware/validate.js';
const router = express.Router();
/**
* GET /api/v1/skills
* 列出所有可用 Skills(内置 + 项目级)
*/
router.get('/', async (_req, res) => {
const raw = listSkills();
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from listSkills' },
});
}
if (!parsed.success) {
return void res.status(500).json(parsed);
}
res.json({ success: true, data: parsed.data });
});
/**
* GET /api/v1/skills/signal-status
* 获取 SignalCollector 后台服务状态
*/
router.get('/signal-status', async (_req, res) => {
const { _signalCollector } = global;
if (!_signalCollector) {
return void res.json({
success: true,
data: { running: false, mode: 'off', snapshot: null },
});
}
res.json({
success: true,
data: {
running: true,
mode: _signalCollector.getMode(),
snapshot: _signalCollector.getSnapshot(),
// 返回 AI 的待处理建议,前端可直接展示
suggestions: _signalCollector.getSnapshot().pendingSuggestions || [],
},
});
});
/**
* GET /api/v1/skills/suggest
* 基于使用模式分析,推荐创建 Skill
*/
router.get('/suggest', async (req, res) => {
const ctx = { container: req.app.locals?.container || null };
const raw = await suggestSkills(ctx);
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from suggestSkills' },
});
}
if (!parsed.success) {
return void res.status(500).json(parsed);
}
res.json({ success: true, data: parsed.data });
});
/**
* GET /api/v1/skills/:name
* 加载指定 Skill 的完整文档
* Query: ?section=xxx 可只返回指定章节
*/
router.get('/:name', async (req, res) => {
const { name } = req.params;
const { section } = req.query;
const raw = loadSkill(null, {
skillName: name,
section: section,
});
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from loadSkill' },
});
}
if (!parsed.success) {
const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404 : 400;
return void res.status(status).json(parsed);
}
res.json({ success: true, data: parsed.data });
});
/**
* POST /api/v1/skills
* 创建项目级 Skill
* Body: { name, description, content, overwrite? }
*/
router.post('/', validate(CreateSkillBody), async (req, res) => {
const { name, description, content, overwrite, createdBy } = req.body;
const raw = createSkill(null, {
name,
description,
content,
overwrite,
createdBy: createdBy || 'manual',
});
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from createSkill' },
});
}
if (!parsed.success) {
const status = parsed.error?.code === 'BUILTIN_CONFLICT'
? 409
: parsed.error?.code === 'ALREADY_EXISTS'
? 409
: parsed.error?.code === 'INVALID_NAME'
? 400
: 500;
return void res.status(status).json(parsed);
}
res.status(201).json({ success: true, data: parsed.data });
});
/**
* PUT /api/v1/skills/:name
* 更新项目级 Skill(description / content)
*/
router.put('/:name', validate(UpdateSkillBody), async (req, res) => {
const { name } = req.params;
const { description, content } = req.body;
const raw = updateSkill(null, { name: name, description, content });
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from updateSkill' },
});
}
if (!parsed.success) {
const status = parsed.error?.code === 'SKILL_NOT_FOUND'
? 404
: parsed.error?.code === 'BUILTIN_PROTECTED'
? 403
: 500;
return void res.status(status).json(parsed);
}
res.json({ success: true, data: parsed.data });
});
/**
* DELETE /api/v1/skills/:name
* 删除项目级 Skill
*/
router.delete('/:name', async (req, res) => {
const { name } = req.params;
const raw = deleteSkill(null, { name: name });
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
return void res.status(500).json({
success: false,
error: { code: 'PARSE_ERROR', message: 'Invalid response from deleteSkill' },
});
}
if (!parsed.success) {
const status = parsed.error?.code === 'SKILL_NOT_FOUND'
? 404
: parsed.error?.code === 'BUILTIN_PROTECTED'
? 403
: 500;
return void res.status(status).json(parsed);
}
res.json({ success: true, data: parsed.data });
});
// ── POST /api/v1/skills/feedback — 记录推荐反馈 ──
router.post('/feedback', async (req, res) => {
const { recommendationId, action, reason, source, category } = req.body || {};
if (!recommendationId || !action) {
return void res.status(400).json({
success: false,
error: { code: 'MISSING_PARAMS', message: 'recommendationId and action are required' },
});
}
const validActions = ['adopted', 'dismissed', 'expired', 'viewed', 'modified'];
if (!validActions.includes(action)) {
return void res.status(400).json({
success: false,
error: {
code: 'INVALID_ACTION',
message: `action must be one of: ${validActions.join(', ')}`,
},
});
}
try {
const container = getServiceContainer();
const feedbackStore = container?.get?.('feedbackStore');
if (!feedbackStore || typeof feedbackStore.record !== 'function') {
return void res.status(503).json({
success: false,
error: { code: 'STORE_UNAVAILABLE', message: 'FeedbackStore not initialized' },
});
}
await feedbackStore.record({
recommendationId,
action,
timestamp: new Date().toISOString(),
source,
category,
reason,
});
res.json({ success: true, data: { recorded: true, recommendationId, action } });
}
catch (err) {
res.status(500).json({
success: false,
error: { code: 'FEEDBACK_ERROR', message: err instanceof Error ? err.message : String(err) },
});
}
});
// ── GET /api/v1/skills/metrics — 推荐效果指标 ──
router.get('/metrics', (req, res) => {
try {
const container = getServiceContainer();
const metrics = container?.get?.('recommendationMetrics');
if (!metrics || typeof metrics.getGlobalSnapshot !== 'function') {
return void res.status(503).json({
success: false,
error: { code: 'METRICS_UNAVAILABLE', message: 'RecommendationMetrics not initialized' },
});
}
const sinceParam = req.query.since;
const since = sinceParam ? new Date(sinceParam) : undefined;
const snapshot = metrics.getGlobalSnapshot(since);
const session = metrics.getSessionMetrics();
res.json({
success: true,
data: { global: snapshot, session },
});
}
catch (err) {
res.status(500).json({
success: false,
error: { code: 'METRICS_ERROR', message: err instanceof Error ? err.message : String(err) },
});
}
});
export default router;