autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
409 lines (408 loc) • 16.2 kB
JavaScript
/**
* Knowledge API 路由 (V3)
* 统一知识条目的 CRUD + 生命周期操作
* 替代 recipes.js + candidates.js (旧路由继续保留用于向后兼容)
*/
import express from 'express';
import { ioLimit } from '#shared/concurrency.js';
import { BatchDeleteBody, BatchDeprecateBody, BatchPublishBody, CreateKnowledgeBody, DeprecateKnowledgeBody, KnowledgeUsageBody, UpdateKnowledgeBody, } 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 { getContext, safeInt, sanitizeForAPI, sanitizePaginatedForAPI, } from '../utils/routeHelpers.js';
const _logger = Logger.getInstance();
const router = express.Router();
/* ═══ 权限中间件 ═════════════════════════════════════════ */
/**
* 路由级权限检查中间件
* 使用 roleResolver 已解析的 req.resolvedRole + PermissionManager 校验
* 当角色缺少指定 action:resource 权限时返回 403
*/
function requirePermission(action, resource) {
return (req, res, next) => {
const role = req.resolvedRole || 'anonymous';
try {
const container = getServiceContainer();
const permissionManager = container.get('permissionManager');
if (permissionManager) {
const result = permissionManager.check(role, action, resource);
if (!result.allowed) {
_logger.warn('Knowledge route permission denied', {
role,
action,
resource,
reason: result.reason,
});
res.status(403).json({
success: false,
error: {
message: `Permission denied: role '${role}' cannot ${action} on ${resource}. ${result.reason}`,
code: 'PERMISSION_DENIED',
},
});
return;
}
}
}
catch {
// PermissionManager 不可用时降级放行(向后兼容)
}
next();
};
}
/* ═══ 查询 ═══════════════════════════════════════════════ */
/**
* GET /api/v1/knowledge
* 获取知识条目列表(支持筛选和分页)
*/
router.get('/', async (req, res) => {
const { lifecycle, kind, category, language, knowledgeType, scope, keyword, tag, source } = req.query;
const page = safeInt(req.query.page, 1);
const pageSize = safeInt(req.query.limit, 20, 1, 1000);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
if (keyword) {
const result = await knowledgeService.search(String(keyword), { page, pageSize });
return void res.json({
success: true,
data: sanitizePaginatedForAPI(result),
});
}
const filters = {};
if (lifecycle) {
filters.lifecycle = lifecycle;
}
if (kind) {
filters.kind = kind;
}
if (category) {
filters.category = category;
}
if (language) {
filters.language = language;
}
if (knowledgeType) {
filters.knowledgeType = knowledgeType;
}
if (scope) {
filters.scope = scope;
}
if (tag) {
filters.tag = tag;
}
if (source) {
filters.source = source;
}
const result = await knowledgeService.list(filters, { page, pageSize });
res.json({
success: true,
data: sanitizePaginatedForAPI(result),
});
});
/**
* GET /api/v1/knowledge/stats
* 获取统计信息
*/
router.get('/stats', async (req, res) => {
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const stats = await knowledgeService.getStats();
res.json({ success: true, data: stats });
});
/**
* GET /api/v1/knowledge/lifecycle
* 获取六态生命周期统计 + 各状态条目列表
*/
router.get('/lifecycle', async (req, res) => {
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const stats = await knowledgeService.getStats();
const states = ['pending', 'staging', 'active', 'evolving', 'decaying', 'deprecated'];
const lifecycle = {
counts: {},
entries: {},
};
const counts = lifecycle.counts;
for (const state of states) {
counts[state] = stats?.[state] ?? 0;
}
// 仅对过渡态(staging / evolving / decaying)返回条目详情
const transitionalStates = ['staging', 'evolving', 'decaying'];
const entries = lifecycle.entries;
for (const state of transitionalStates) {
if (counts[state] > 0) {
const result = await knowledgeService.list({ lifecycle: state }, { page: 1, pageSize: 20 });
entries[state] = result.items ?? [];
}
else {
entries[state] = [];
}
}
res.json({ success: true, data: lifecycle });
});
/**
* POST /api/v1/knowledge/quality/refresh-all
* 批量重新计算所有条目的质量评分
*/
router.post('/quality/refresh-all', async (_req, res) => {
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const result = await knowledgeService.list({}, { page: 1, pageSize: 10000 });
const all = result.data;
let updated = 0;
let failed = 0;
for (const entry of all) {
try {
await knowledgeService.updateQuality(entry.id);
updated++;
}
catch {
failed++;
}
}
res.json({ success: true, data: { updated, failed, total: all.length } });
});
/**
* GET /api/v1/knowledge/:id
* 获取知识条目详情
*/
router.get('/:id', async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const entry = await knowledgeService.get(id);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/* ═══ CRUD ═══════════════════════════════════════════════ */
/**
* POST /api/v1/knowledge
* 创建知识条目(wire format 直通)
*/
router.post('/', requirePermission('knowledge', 'create'), validate(CreateKnowledgeBody), async (req, res) => {
const data = req.body;
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.create(data, context);
res.status(201).json({
success: true,
data: sanitizeForAPI(entry),
});
});
/**
* PATCH /api/v1/knowledge/:id
* 更新知识条目(白名单字段)
*/
router.patch('/:id', requirePermission('knowledge', 'update'), validate(UpdateKnowledgeBody), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.update(id, req.body, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* DELETE /api/v1/knowledge/:id
* 删除知识条目
*/
router.delete('/:id', requirePermission('knowledge', 'delete'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const result = await knowledgeService.delete(id, context);
res.json({ success: true, data: result });
});
/* ═══ 生命周期操作(6 态: pending / staging / active / evolving / decaying / deprecated)═══ */
/**
* PATCH /api/v1/knowledge/:id/publish
* 发布 (pending → active) — 仅开发者
*/
router.patch('/:id/publish', requirePermission('knowledge', 'publish'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.publish(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/deprecate
* 废弃 (pending|active → deprecated)
*/
router.patch('/:id/deprecate', requirePermission('knowledge', 'deprecate'), validate(DeprecateKnowledgeBody), async (req, res) => {
const id = String(req.params.id);
const { reason } = req.body;
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.deprecate(id, reason, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/reactivate
* 重新激活 (deprecated → pending)
*/
router.patch('/:id/reactivate', requirePermission('knowledge', 'update'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.reactivate(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/stage
* 暂存 (pending → staging)
*/
router.patch('/:id/stage', requirePermission('knowledge', 'update'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.stage(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/evolve
* 进化 (active → evolving)
*/
router.patch('/:id/evolve', requirePermission('knowledge', 'update'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.evolve(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/decay
* 衰退 (active|evolving → decaying)
*/
router.patch('/:id/decay', requirePermission('knowledge', 'update'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.decay(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/**
* PATCH /api/v1/knowledge/:id/restore
* 恢复为已发布 (decaying|evolving → active)
*/
router.patch('/:id/restore', requirePermission('knowledge', 'update'), async (req, res) => {
const id = String(req.params.id);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const entry = await knowledgeService.restore(id, context);
res.json({ success: true, data: sanitizeForAPI(entry) });
});
/* ═══ 批量操作 ═══════════════════════════════════════════ */
/**
* POST /api/v1/knowledge/batch-publish
* 批量发布 (pending → active)
* 支持 autoApprovableOnly=true 参数,只发布 autoApprovable 的条目
*/
router.post('/batch-publish', requirePermission('knowledge', 'publish'), validate(BatchPublishBody), async (req, res) => {
const { ids } = req.body;
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const results = await Promise.allSettled(ids.map((id) => ioLimit(() => knowledgeService.publish(id, context))));
const published = results
.filter((r) => r.status === 'fulfilled')
.map((r) => sanitizeForAPI(r.value));
const failed = results
.map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null))
.filter(Boolean);
res.json({
success: true,
data: {
published,
failed,
total: ids.length,
successCount: published.length,
failureCount: failed.length,
},
});
});
/**
* POST /api/v1/knowledge/batch-delete
* 批量删除知识条目
*/
router.post('/batch-delete', requirePermission('knowledge', 'delete'), validate(BatchDeleteBody), async (req, res) => {
const { ids } = req.body;
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const results = await Promise.allSettled(ids.map((id) => ioLimit(() => knowledgeService.delete(id, context))));
const deleted = results.filter((r) => r.status === 'fulfilled').length;
const failed = results
.map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null))
.filter(Boolean);
res.json({
success: true,
data: {
total: ids.length,
deletedCount: deleted,
failureCount: failed.length,
failed,
},
});
});
/**
* POST /api/v1/knowledge/batch-deprecate
* 批量废弃知识条目 (active → deprecated)
*/
router.post('/batch-deprecate', requirePermission('knowledge', 'publish'), validate(BatchDeprecateBody), async (req, res) => {
const { ids, reason } = req.body;
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const context = getContext(req);
const results = await Promise.allSettled(ids.map((id) => ioLimit(() => knowledgeService.deprecate(id, reason || 'batch deprecate', context))));
const deprecated = results
.filter((r) => r.status === 'fulfilled')
.map((r) => sanitizeForAPI(r.value));
const failed = results
.map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null))
.filter(Boolean);
res.json({
success: true,
data: {
deprecated,
failed,
total: ids.length,
successCount: deprecated.length,
failureCount: failed.length,
},
});
});
/* ═══ 使用 / 质量 ═══════════════════════════════════════ */
/**
* POST /api/v1/knowledge/:id/usage
* 记录使用(adoption / application / guard_hit / view / success)
*/
router.post('/:id/usage', validate(KnowledgeUsageBody), async (req, res) => {
const id = String(req.params.id);
const { type, feedback } = req.body;
const context = getContext(req);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
await knowledgeService.incrementUsage(id, type, { actor: context.userId, feedback });
res.json({ success: true, message: `${type} recorded` });
});
/**
* PATCH /api/v1/knowledge/:id/quality
* 重新计算质量评分
*/
router.patch('/:id/quality', async (req, res) => {
const id = String(req.params.id);
const context = getContext(req);
const container = getServiceContainer();
const knowledgeService = container.get('knowledgeService');
const result = await knowledgeService.updateQuality(id, context);
res.json({ success: true, data: result });
});
export default router;