UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

129 lines (128 loc) 4.59 kB
/** * FeedbackCollector — 用户反馈收集器 * 记录交互事件 (view/click/rate/dismiss),可持久化,支持统计汇总 * 持久化到 AutoSnippet/feedback.json(Git 友好) */ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import pathGuard from '../../shared/PathGuard.js'; import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js'; export class FeedbackCollector { #feedbackPath; #events; #maxEvents; constructor(projectRoot, options = {}) { const kbDir = options.knowledgeBaseDir || DEFAULT_KNOWLEDGE_BASE_DIR; this.#feedbackPath = join(projectRoot, kbDir, 'feedback.json'); pathGuard.assertProjectWriteSafe(this.#feedbackPath); this.#maxEvents = options.maxEvents || 1000; this.#migrateOldPath(projectRoot, options.internalDir || '.autosnippet'); this.#events = this.#load(); } /** * 记录一个交互事件 * @param data 任意附加数据 (rating, comment, etc.) */ record(type, recipeId, data = {}) { this.#events.push({ type, recipeId, data, timestamp: new Date().toISOString(), }); if (this.#events.length > this.#maxEvents) { this.#events = this.#events.slice(-this.#maxEvents); } this.#save(); } /** * 获取指定 Recipe 的事件统计 * @returns } */ getRecipeStats(recipeId) { const events = this.#events.filter((e) => e.recipeId === recipeId); const ratings = events .filter((e) => e.type === 'rate' && e.data.rating) .map((e) => e.data.rating); return { views: events.filter((e) => e.type === 'view').length, clicks: events.filter((e) => e.type === 'click').length, copies: events.filter((e) => e.type === 'copy' || e.type === 'insert').length, avgRating: ratings.length > 0 ? ratings.reduce((a, b) => a + b, 0) / ratings.length : 0, feedbackCount: events.filter((e) => e.type === 'feedback').length, totalEvents: events.length, }; } /** 获取全局统计 */ getGlobalStats() { const byType = {}; for (const e of this.#events) { byType[e.type] = (byType[e.type] || 0) + 1; } return { totalEvents: this.#events.length, byType, uniqueRecipes: new Set(this.#events.map((e) => e.recipeId)).size, }; } /** 获取热门 Recipes (by interaction count) */ getTopRecipes(n = 10) { const counts = {}; for (const e of this.#events) { counts[e.recipeId] = (counts[e.recipeId] || 0) + 1; } return Object.entries(counts) .sort(([, a], [, b]) => b - a) .slice(0, n) .map(([recipeId, count]) => ({ recipeId, count })); } /** 清空记录 */ clear() { this.#events = []; this.#save(); } // ─── 私有 ───────────────────────────────────────────── #load() { try { if (existsSync(this.#feedbackPath)) { const data = JSON.parse(readFileSync(this.#feedbackPath, 'utf-8')); return Array.isArray(data) ? data : data.events || []; } } catch { /* silent */ } return []; } #save() { try { const dir = dirname(this.#feedbackPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(this.#feedbackPath, JSON.stringify(this.#events, null, 2)); } catch { /* silent */ } } #migrateOldPath(projectRoot, internalDir) { try { const oldPath = join(projectRoot, internalDir, 'feedback.json'); if (existsSync(oldPath) && !existsSync(this.#feedbackPath)) { const content = readFileSync(oldPath, 'utf-8'); const dir = dirname(this.#feedbackPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(this.#feedbackPath, content); unlinkSync(oldPath); } } catch { /* 迁移失败不阻断启动 */ } } }