UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

887 lines (886 loc) 40 kB
/** * WikiGenerator — Repo Wiki 生成引擎 (V3 Content-First) * * 自动分析项目代码结构,生成结构化的项目文档 Wiki。 * 结合 AutoSnippet 的 AST 深度分析能力(ProjectGraph、CodeEntityGraph、SPM 依赖图) * 做到深层代码洞察。 * * V3 核心设计: "内容驱动 + AI 优先" * 1. 数据收集 (Scan → AST → SPM → KB) * 2. 主题发现 — 分析数据丰富度,动态决定生成哪些文章 * 3. AI 优先撰写 — 直接由 AI 写完整文章,非骨架+润色 * 4. 质量关卡 — 内容不足 MIN_ARTICLE_CHARS 则跳过该文章 * 5. 降级保底 — AI 不可用时使用丰富模板内容 * * Wiki 文档结构 (动态生成,按项目特征而异): * AutoSnippet/wiki/ * ├── index.md — 项目概述 (始终生成) * ├── architecture.md — 架构总览 (多模块项目) * ├── getting-started.md — 快速上手 (有构建系统时) * ├── modules/ * │ ├── {ModuleName}.md — 模块深度文档 (仅内容丰富的模块) * │ └── ... * ├── patterns.md — 代码模式 (有知识库 Recipe 时) * ├── patterns/ — 按分类拆分 (Recipe 较多时) * │ └── {category}.md * ├── protocols.md — 协议参考 (协议较多时) * ├── documents/ — 同步的 Cursor 端文档 * └── meta.json — Wiki 元数据 * * @module WikiGenerator */ import { createHash } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import Logger from '../../infrastructure/logging/Logger.js'; import { LanguageService } from '../../shared/LanguageService.js'; import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js'; import { buildAiSystemPrompt, buildArticlePrompt, buildFallbackArticle, } from './WikiRenderers.js'; import { dedup, detectBuildSystems, getLangTerms, getModuleSourceFiles, inferModuleFromPath, profileFolders, slug, walkDir, } from './WikiUtils.js'; const logger = Logger.getInstance(); // ─── Wiki 生成阶段 ────────────────────────────────────────── export const WikiPhase = Object.freeze({ INIT: 'init', // 初始化 & 自检 SCAN: 'scan', // 扫描项目结构 AST_ANALYZE: 'ast-analyze', // AST 深度分析 SPM_PARSE: 'spm-parse', // SPM 依赖解析 KNOWLEDGE: 'knowledge', // 整合已有 Recipes GENERATE: 'generate', // 生成 Markdown 骨架 AI_COMPOSE: 'ai-compose', // AI 合成写作增强 SYNC_DOCS: 'sync-docs', // 同步 Cursor 端 MD DEDUP: 'dedup', // 去重 FINALIZE: 'finalize', // 写入 meta.json }); // ─── 默认配置 ──────────────────────────────────────────────── const DEFAULTS = { wikiDir: `${DEFAULT_KNOWLEDGE_BASE_DIR}/wiki`, language: 'zh', // 'zh' | 'en' maxFiles: 500, includeRecipes: true, includeDepGraph: true, includeComponents: true, }; // ─── WikiGenerator ──────────────────────────────────────────── export class WikiGenerator { projectRoot; wikiDir; _aborted; aiProvider; codeEntityGraph; knowledgeService; metaPath; moduleService; onProgress; options; projectGraph; /** * @param [deps.spmService] 向后兼容 * @param [deps.onProgress] (phase, progress, message) => void */ constructor(deps) { this.projectRoot = deps.projectRoot; this.moduleService = deps.moduleService || null; this.knowledgeService = deps.knowledgeService || null; this.projectGraph = deps.projectGraph || null; this.codeEntityGraph = deps.codeEntityGraph || null; this.aiProvider = deps.aiProvider || null; this.onProgress = deps.onProgress || (() => { }); this.options = { ...DEFAULTS, ...deps.options }; this.wikiDir = path.join(this.projectRoot, this.options.wikiDir); this.metaPath = path.join(this.wikiDir, 'meta.json'); this._aborted = false; } // ═══ 公有 API ══════════════════════════════════════════════ /** 全量生成 Wiki */ async generate() { const startTime = Date.now(); this._aborted = false; try { // Phase 1: Init this._emit(WikiPhase.INIT, 0, '初始化 Wiki 生成引擎...'); this._ensureDir(this.wikiDir); // Phase 2: Scan project this._emit(WikiPhase.SCAN, 5, '扫描项目结构...'); const projectInfo = await this._scanProject(); if (this._aborted) { return this._abortedResult(); } // Phase 3: AST analyze this._emit(WikiPhase.AST_ANALYZE, 15, '执行 AST 深度分析...'); const astInfo = await this._analyzeAST(); if (this._aborted) { return this._abortedResult(); } // Phase 4: Module/SPM parse this._emit(WikiPhase.SPM_PARSE, 30, '解析模块依赖关系...'); const moduleInfo = await this._parseModules(); if (this._aborted) { return this._abortedResult(); } // Phase 5: Knowledge integration this._emit(WikiPhase.KNOWLEDGE, 45, '整合知识库 Recipes...'); const knowledgeInfo = await this._integrateKnowledge(); if (this._aborted) { return this._abortedResult(); } // Phase 6: Content-driven topic discovery (V3) this._emit(WikiPhase.GENERATE, 50, '分析项目数据,发现文档主题...'); const structuredData = { projectInfo, astInfo, moduleInfo, knowledgeInfo, }; const topics = this._discoverTopics(projectInfo, astInfo, moduleInfo, knowledgeInfo); if (this._aborted) { return this._abortedResult(); } // Phase 7: AI-first article composition (V3) this._emit(WikiPhase.AI_COMPOSE, 55, `撰写 ${topics.length} 篇文档...`); const files = await this._composeArticles(topics, structuredData); if (this._aborted) { return this._abortedResult(); } // Phase 8: Sync Cursor docs this._emit(WikiPhase.SYNC_DOCS, 80, '同步 Cursor 端文档...'); const syncedFiles = this._syncCursorDocs(); files.push(...syncedFiles); if (this._aborted) { return this._abortedResult(); } // Phase 9: Dedup this._emit(WikiPhase.DEDUP, 90, '去重检查...'); const dedupResult = dedup(files, this.wikiDir, this._emit.bind(this)); // Phase 10: Finalize this._emit(WikiPhase.FINALIZE, 95, '写入元数据...'); const meta = this._writeMeta(files, startTime, dedupResult); const duration = Date.now() - startTime; this._emit(WikiPhase.FINALIZE, 100, `Wiki 生成完成,耗时 ${(duration / 1000).toFixed(1)}s`); return { success: true, filesGenerated: files.length, aiComposed: files.filter((f) => f.polished).length, syncedDocs: syncedFiles.length, dedup: dedupResult, duration, wikiDir: this.wikiDir, meta, }; } catch (err) { logger.error('[WikiGenerator] Generation failed', { error: err.message }); this._emit('error', -1, `生成失败: ${err.message}`); return { success: false, error: err.message, duration: Date.now() - startTime }; } } /** 增量更新 — 仅重新生成变更的部分 */ async update() { const meta = this._readMeta(); if (!meta) { logger.info('[WikiGenerator] No existing meta.json — falling back to full generation'); return this.generate(); } // 简化增量策略:检查项目源文件修改时间 vs meta.generatedAt const hasChanges = this._detectChanges(meta); if (!hasChanges) { this._emit(WikiPhase.FINALIZE, 100, 'Wiki 已是最新,无需更新'); return { success: true, filesGenerated: 0, duration: 0, upToDate: true }; } return this.generate(); } /** 中止当前生成 */ abort() { this._aborted = true; } /** 获取当前 Wiki 状态 */ getStatus() { const meta = this._readMeta(); if (!meta) { return { exists: false }; } return { exists: true, generatedAt: meta.generatedAt, filesCount: meta.files?.length || 0, version: meta.version, hasChanges: this._detectChanges(meta), }; } // ═══ 阶段实现 ══════════════════════════════════════════════ /** 扫描项目基本信息 */ async _scanProject() { const info = { name: path.basename(this.projectRoot), root: this.projectRoot, // 通用构建系统检测(替代硬编码 iOS 三件套) buildSystems: [], // [{eco, buildTool}] sourceFiles: [], languages: {}, langProfile: null, // LanguageService.detectProfile() 结果 primaryLanguage: 'unknown', // 保留向后兼容字段 hasPackageSwift: false, hasPodfile: false, hasXcodeproj: false, sourceFilesByModule: {}, }; // 检测项目类型 const entries = fs.readdirSync(this.projectRoot, { withFileTypes: true }); const entryNames = entries.map((e) => e.name); // 通用构建系统检测 (支持一级子目录 monorepo) info.buildSystems = detectBuildSystems(entryNames, this.projectRoot); // 向后兼容三字段 for (const e of entries) { if (e.name === 'Package.swift') { info.hasPackageSwift = true; } if (e.name === 'Podfile') { info.hasPodfile = true; } if (e.name.endsWith('.xcodeproj') || e.name.endsWith('.xcworkspace')) { info.hasXcodeproj = true; } } // 统计源文件 const extMap = {}; for (const ext of LanguageService.sourceExts) { extMap[ext] = LanguageService.displayNameFromExt(ext) || ext; } walkDir(this.projectRoot, (filePath) => { const ext = path.extname(filePath); if (extMap[ext]) { info.sourceFiles.push(path.relative(this.projectRoot, filePath)); const displayLang = LanguageService.displayNameFromExt(ext); info.languages[displayLang] = (info.languages[displayLang] || 0) + 1; } }, this.options.maxFiles); // 按模块/Target 分组源文件 (SPM 约定: Sources/{ModuleName}/...) info.sourceFilesByModule = {}; for (const f of info.sourceFiles) { const parts = f.split('/'); const sourcesIdx = parts.indexOf('Sources'); let mod; if (sourcesIdx >= 0 && sourcesIdx + 1 < parts.length) { // SPM 标准结构: Sources/{ModuleName}/... mod = parts[sourcesIdx + 1]; } else { // 通用: 使用多语言路径推断 mod = inferModuleFromPath(f); } if (mod) { if (!info.sourceFilesByModule[mod]) { info.sourceFilesByModule[mod] = []; } info.sourceFilesByModule[mod].push(f); } } // 利用 LanguageService.detectProfile() 获取多语言画像 const bareStats = {}; for (const f of info.sourceFiles) { const ext = path.extname(f).replace('.', ''); if (ext) { bareStats[ext] = (bareStats[ext] || 0) + 1; } } info.langProfile = LanguageService.detectProfile(bareStats); info.primaryLanguage = info.langProfile.primary; this._emit(WikiPhase.SCAN, 12, `发现 ${info.sourceFiles.length} 个源文件 (${LanguageService.displayName(info.primaryLanguage)})`); return info; } /** AST 分析 — 利用已有 ProjectGraph 或重新构建 */ async _analyzeAST() { if (this.projectGraph) { const overview = await this.projectGraph.getOverview(); const allClasses = this.projectGraph.getAllClassNames(); const allProtocols = this.projectGraph.getAllProtocolNames(); // 按模块分组类名和协议名 (通过 filePath 推断所属模块) const classNamesByModule = {}; const protocolNamesByModule = {}; for (const name of allClasses) { const info = this.projectGraph.getClassInfo(name); if (info?.filePath) { const mod = inferModuleFromPath(info.filePath); if (mod) { if (!classNamesByModule[mod]) { classNamesByModule[mod] = []; } classNamesByModule[mod].push(name); } } } for (const name of allProtocols) { const info = this.projectGraph.getProtocolInfo(name); if (info?.filePath) { const mod = inferModuleFromPath(info.filePath); if (mod) { if (!protocolNamesByModule[mod]) { protocolNamesByModule[mod] = []; } protocolNamesByModule[mod].push(name); } } } this._emit(WikiPhase.AST_ANALYZE, 25, `AST 分析: ${overview.totalClasses} 个类, ${overview.totalProtocols} 个协议`); return { overview, classes: allClasses, protocols: allProtocols, classNamesByModule, protocolNamesByModule, }; } // 没有现成的 ProjectGraph — 返回空壳(不阻塞生成) return { overview: null, classes: [], protocols: [], classNamesByModule: {}, protocolNamesByModule: {}, }; } /** * 模块依赖解析 * 通过 moduleService 统一处理所有语言的模块扫描 */ async _parseModules() { if (!this.moduleService) { return { targets: [], depGraph: null }; } try { await this.moduleService.load(); const targets = await this.moduleService.listTargets(); let depGraph = null; if (this.options.includeDepGraph) { try { depGraph = await this.moduleService.getDependencyGraph?.({ level: 'target' }); } catch { /* non-critical */ } } const info = this.moduleService.getProjectInfo(); this._emit(WikiPhase.SPM_PARSE, 40, `模块: ${targets.length} 个 (${info.primaryLanguage})`); return { targets, depGraph, projectInfo: info }; } catch (err) { logger.warn('[WikiGenerator] ModuleService parse failed', { error: err.message }); return { targets: [], depGraph: null }; } } /** 整合已有知识库 Recipes */ async _integrateKnowledge() { if (!this.knowledgeService || !this.options.includeRecipes) { return { recipes: [], stats: null }; } try { const result = await this.knowledgeService.list({ lifecycle: 'active', limit: 200, offset: 0, }); const recipes = result.data || result.items || result || []; const stats = (await this.knowledgeService.getStats?.()) || null; this._emit(WikiPhase.KNOWLEDGE, 55, `知识库: ${recipes.length} 条活跃 Recipe`); return { recipes: Array.isArray(recipes) ? recipes : [], stats }; } catch (err) { logger.warn('[WikiGenerator] Knowledge integration failed', { error: err.message, }); return { recipes: [], stats: null }; } } /** * V3 内容驱动的主题发现 * * 核心原则: * - 没有固定的文件列表 — 所有文章都由数据丰富度驱动 * - 跳过数据不足的主题(避免空文档) * - 不同的项目产出不同数量/类型的文章 * * @returns >} */ _discoverTopics(projectInfo, astInfo, moduleInfo, knowledgeInfo) { const topics = []; const isZh = this.options.language === 'zh'; const langTerms = getLangTerms(projectInfo.primaryLanguage); // ── 1. 项目概览 (始终生成) ── topics.push({ id: 'overview', path: 'index.md', title: isZh ? '项目概述' : 'Project Overview', type: 'overview', priority: 100, }); // ── 2. 架构概览 (需要模块/依赖关系) ── const moduleKeys = Object.keys(astInfo.classNamesByModule || {}); const sourceModuleKeys = Object.keys(projectInfo.sourceFilesByModule || {}); const hasMultiModule = moduleInfo.targets.length >= 2 || moduleKeys.length >= 2 || sourceModuleKeys.length >= 2; const hasDepGraph = moduleInfo.depGraph != null; const hasInheritance = this.codeEntityGraph != null; if (hasMultiModule || hasDepGraph || hasInheritance) { topics.push({ id: 'architecture', path: 'architecture.md', title: isZh ? '架构总览' : 'Architecture Overview', type: 'architecture', priority: 90, }); } // ── 3. 快速上手 (需要构建配置或入口点) ── const hasEntryPoints = astInfo.overview?.entryPoints?.length ? true : false; const hasBuildSystem = projectInfo.buildSystems.length > 0 || projectInfo.hasPackageSwift || projectInfo.hasPodfile || projectInfo.hasXcodeproj; if (hasEntryPoints || hasBuildSystem) { topics.push({ id: 'getting-started', path: 'getting-started.md', title: isZh ? '快速上手' : 'Getting Started', type: 'getting-started', priority: 85, }); } // ── 4. 模块深度文档 (仅对实质性模块生成) ── const discoverers = (moduleInfo.projectInfo?.discoverers ?? []); const genericOnlyDiscovery = discoverers.length === 1 && discoverers[0]?.id === 'generic'; const monolithSingleTarget = moduleInfo.targets.length === 1 && (moduleInfo.targets[0]?.path === projectInfo.root || moduleInfo.targets[0]?.name === projectInfo.name); const shouldUseInferredModules = sourceModuleKeys.length >= 2 && (moduleInfo.targets.length === 0 || (genericOnlyDiscovery && monolithSingleTarget)); if (moduleInfo.targets.length > 0 && !shouldUseInferredModules) { // 使用 moduleService 发现的 targets for (const target of moduleInfo.targets) { const moduleFiles = getModuleSourceFiles(target, projectInfo); const classCount = (astInfo.classNamesByModule?.[target.name] || []).length; const protoCount = (astInfo.protocolNamesByModule?.[target.name] || []).length; const depCount = (target.dependencies || target.info?.dependencies || []).length; // 丰富度评分: 文件数 + 类数×2 + 协议数×2 + 依赖数 const richness = moduleFiles.length + classCount * 2 + protoCount * 2 + depCount; // 跳过过于单薄的模块 (少于3分不值得独立文档) if (richness < 3) { continue; } topics.push({ id: `module-${slug(target.name)}`, path: `modules/${slug(target.name)}.md`, title: target.name, type: 'module', priority: 50 + Math.min(richness, 30), _moduleData: { target, moduleFiles, classCount, protoCount }, }); } } else if (shouldUseInferredModules) { // 无有效模块边界(无 targets 或 generic 单 target) → 从 sourceFilesByModule 推断模块 const sfm = projectInfo.sourceFilesByModule || {}; const sorted = Object.entries(sfm).sort((a, b) => b[1].length - a[1].length); for (const [modName, modFiles] of sorted) { if (modFiles.length < 2) { continue; } const classCount = (astInfo.classNamesByModule?.[modName] || []).length; const protoCount = (astInfo.protocolNamesByModule?.[modName] || []).length; const richness = modFiles.length + classCount * 2 + protoCount * 2; if (richness < 3) { continue; } topics.push({ id: `module-${slug(modName)}`, path: `modules/${slug(modName)}.md`, title: modName, type: 'module', priority: 50 + Math.min(richness, 30), _moduleData: { target: { name: modName, type: 'inferred' }, moduleFiles: modFiles, classCount, protoCount, }, }); } } // ── 5. 代码模式/最佳实践 (来自知识库 Recipes) ── if (knowledgeInfo.recipes.length > 0) { const groups = {}; for (const r of knowledgeInfo.recipes) { const recipeObj = r; const json = recipeObj.toJSON ? recipeObj.toJSON() : recipeObj; const cat = json.category || 'Other'; if (!groups[cat]) { groups[cat] = []; } groups[cat].push(json); } const catEntries = Object.entries(groups).sort((a, b) => b[1].length - a[1].length); if (catEntries.length <= 3 || knowledgeInfo.recipes.length < 15) { // 合并为一篇 topics.push({ id: 'patterns', path: 'patterns.md', title: isZh ? '代码模式与最佳实践' : 'Code Patterns & Best Practices', type: 'patterns', priority: 40, }); } else { // 按分类拆分为多篇 for (const [cat, items] of catEntries) { if (items.length < 2) { continue; } topics.push({ id: `pattern-${slug(cat)}`, path: `patterns/${slug(cat)}.md`, title: isZh ? `${cat} 模式` : `${cat} Patterns`, type: 'pattern-category', priority: 30 + items.length, _patternData: { category: cat, recipes: items }, }); } } } // ── 6. 协议/接口参考 (数量足够多时) ── if (astInfo.protocols.length >= 8) { const ifaceLabel = isZh ? langTerms.interfaceLabel.zh : langTerms.interfaceLabel.en; topics.push({ id: 'protocols', path: 'protocols.md', title: isZh ? `${ifaceLabel}参考` : `${ifaceLabel} Reference`, type: 'reference', priority: 35, }); } // ── 7. 文件夹画像文档 ── // 触发条件 (满足任一即启用): // a) AST 稀疏: 类/协议 < 5 且无模块文档 // b) generic monolith: 仅 generic discoverer + 单 target + 多目录 // c) 核心文章过少: 当前主题 ≤ 4 篇 → 用文件夹分析补充内容丰富度 const astEntityCount = (astInfo.classes?.length || 0) + (astInfo.protocols?.length || 0); const hasModuleDocs = topics.some((t) => t.type === 'module'); const astSparse = astEntityCount < 5 && !hasModuleDocs; const shouldProfileForGenericMonolith = genericOnlyDiscovery && monolithSingleTarget && sourceModuleKeys.length >= 2; const tooFewCoreArticles = topics.length <= 4 && sourceModuleKeys.length >= 2; const shouldEnableFolderProfiling = astSparse || shouldProfileForGenericMonolith || tooFewCoreArticles; if (shouldEnableFolderProfiling) { const rawFolderProfiles = profileFolders(projectInfo, { minFiles: 3, maxFolders: 15, }); // 按 relPath 去重,避免同一路径重复产出同名文档 const folderProfiles = []; const seenFolderRelPath = new Set(); for (const fp of rawFolderProfiles) { if (seenFolderRelPath.has(fp.relPath)) { continue; } seenFolderRelPath.add(fp.relPath); folderProfiles.push(fp); } if (folderProfiles.length > 0) { // 总览文档: 文件夹结构分析 topics.push({ id: 'folder-overview', path: 'folder-structure.md', title: isZh ? '项目结构分析' : 'Project Structure Analysis', type: 'folder-overview', priority: 80, _folderProfiles: folderProfiles, }); // 为每个重要文件夹生成独立文档 (仅 fileCount ≥ 5 的大文件夹) // 限制最多 10 个 folder-profile 文档,避免碎片化 const MAX_FOLDER_DOCS = 10; let folderDocCount = 0; for (const fp of folderProfiles) { if (folderDocCount >= MAX_FOLDER_DOCS) { break; } if (fp.fileCount < 5) { continue; } const folderDocSlug = slug(fp.relPath.replaceAll('/', '-')); // 文件夹丰富度评分: 文件数 + 入口点×3 + 命名模式数×2 + imports数 + headerComments数×2 + (有README +5) const richness = fp.fileCount + fp.entryPoints.length * 3 + fp.namingPatterns.length * 2 + fp.imports.length + fp.headerComments.length * 2 + (fp.readme ? 5 : 0); if (richness < 10) { continue; // 过于单薄的文件夹不值得独立文档 } topics.push({ id: `folder-${folderDocSlug}`, path: `folders/${folderDocSlug}.md`, title: fp.relPath, type: 'folder-profile', priority: 45 + Math.min(richness, 25), _folderProfile: fp, }); folderDocCount++; } const folderProfileReason = astSparse ? 'AST sparse' : tooFewCoreArticles ? `few core articles (${topics.length - topics.filter((t) => t.type === 'folder-overview' || t.type === 'folder-profile').length} core)` : 'generic monolith'; logger.info(`[WikiGenerator] Folder profiling (${folderProfileReason}): ${folderProfiles.length} folders analyzed, ` + `${topics.filter((t) => t.type === 'folder-profile').length} folder docs planned`); } } // 按优先级排序 topics.sort((a, b) => b.priority - a.priority); logger.info(`[WikiGenerator] Discovered ${topics.length} topics: ${topics.map((t) => t.id).join(', ')}`); this._emit(WikiPhase.GENERATE, 55, `发现 ${topics.length} 个文档主题`); return topics; } /** * V3 AI-first 文章撰写 * * 对每个发现的主题: * 1. 优先使用 AI 撰写完整文章 (非骨架增强!) * 2. AI 不可用时使用丰富的模板内容 * 3. 质量关卡: 最终内容不足 MIN_ARTICLE_CHARS 则跳过 * * @param topics _discoverTopics() 的输出 * @param structuredData { projectInfo, astInfo, moduleInfo, knowledgeInfo } * @returns >>} */ async _composeArticles(topics, structuredData) { const files = []; const isZh = this.options.language === 'zh'; const MIN_ARTICLE_CHARS = 200; // 确保必要的子目录存在 this._ensureDir(this.wikiDir); const needsModulesDir = topics.some((t) => t.path.startsWith('modules/')); const needsPatternsDir = topics.some((t) => t.path.startsWith('patterns/')); const needsFoldersDir = topics.some((t) => t.path.startsWith('folders/')); if (needsModulesDir) { this._ensureDir(path.join(this.wikiDir, 'modules')); } if (needsPatternsDir) { this._ensureDir(path.join(this.wikiDir, 'patterns')); } if (needsFoldersDir) { this._ensureDir(path.join(this.wikiDir, 'folders')); } let composed = 0; const systemPrompt = buildAiSystemPrompt(isZh); // 跟踪实际写入的主题 (用于 overview 导航) const writtenTopics = []; let overviewTopicIdx = -1; for (let i = 0; i < topics.length; i++) { if (this._aborted) { break; } const topic = topics[i]; if (topic.type === 'overview') { overviewTopicIdx = i; // 先用全部主题作为占位,最后回写 topic._allTopics = topics; } const progress = 58 + Math.round((i / topics.length) * 22); this._emit(WikiPhase.AI_COMPOSE, progress, `撰写: ${topic.title}`); let content = null; // === 1. 尝试 AI 撰写完整文章 === if (this.aiProvider) { try { const prompt = buildArticlePrompt(topic, structuredData, isZh, this.codeEntityGraph); const aiResult = await Promise.race([ this.aiProvider.chat(prompt, { systemPrompt, temperature: 0.3, maxTokens: 4096 }), new Promise((_, reject) => setTimeout(() => reject(new Error('AI compose timeout')), 45_000)), ]); if (aiResult && typeof aiResult === 'string' && aiResult.length >= MIN_ARTICLE_CHARS) { content = aiResult; composed++; } } catch (err) { logger.warn(`[WikiGenerator] AI compose failed for ${topic.id}: ${err.message}`); } } // === 2. 降级: 丰富的模板内容 === if (!content) { content = buildFallbackArticle(topic, structuredData, isZh, this.codeEntityGraph); } // === 3. 质量关卡 === if (!content || content.length < MIN_ARTICLE_CHARS) { logger.info(`[WikiGenerator] Skipping thin topic: ${topic.id} (${content?.length || 0} chars)`); continue; } // 写入文件 const fileInfo = this._writeFile(topic.path, content); if (composed > 0 && content !== buildFallbackArticle(topic, structuredData, isZh, this.codeEntityGraph)) { fileInfo.polished = true; } files.push(fileInfo); writtenTopics.push(topic); } // 回写 overview: 只包含实际生成的页面导航 (避免断链) if (overviewTopicIdx >= 0 && writtenTopics.length > 0) { const overviewTopic = topics[overviewTopicIdx]; overviewTopic._allTopics = writtenTopics; let overviewContent = null; // overview 始终存在于 files 中(因为 priority 最高且始终生成) // 重新用实际 writtenTopics 渲染 overviewContent = buildFallbackArticle(overviewTopic, structuredData, isZh, this.codeEntityGraph); // 如果之前 AI compose 过 overview,保留 AI 版本(AI 版本已在初次写入时处理导航) const overviewFile = files.find((f) => f.path === overviewTopic.path); if (overviewFile && !overviewFile.polished && overviewContent) { this._writeFile(overviewTopic.path, overviewContent); } } logger.info(`[WikiGenerator] Composed ${files.length} articles (${composed} AI-enhanced)`); this._emit(WikiPhase.AI_COMPOSE, 80, `撰写完成: ${files.length} 篇文档 (${composed} 篇 AI 增强)`); return files; } // ═══ Phase 8: 同步 Cursor 端 MD ═══════════════════════════ /** * 同步 Cursor 端保存的 MD 到 wiki 目录 * * 同步源: * 1. .cursor/skills/autosnippet-devdocs/references/ (*.md) → wiki/documents/ * * @returns >} */ _syncCursorDocs() { const synced = []; const isZh = this.options.language === 'zh'; // ── Source 1: Channel D devdocs ── const devdocsDir = path.join(this.projectRoot, '.cursor', 'skills', 'autosnippet-devdocs', 'references'); if (fs.existsSync(devdocsDir)) { this._ensureDir(path.join(this.wikiDir, 'documents')); const files = fs.readdirSync(devdocsDir).filter((f) => f.endsWith('.md')); for (const file of files) { try { const content = fs.readFileSync(path.join(devdocsDir, file), 'utf-8'); const header = `<!-- synced from .cursor/skills/autosnippet-devdocs/references/${file} -->\n\n`; const result = this._writeFile(`documents/${file}`, header + content); result.source = 'cursor-devdocs'; synced.push(result); } catch { /* skip */ } } } // 生成目录索引 this._generateSyncIndex(synced, isZh); logger.info(`[WikiGenerator] Synced ${synced.length} docs from Cursor`); this._emit(WikiPhase.SYNC_DOCS, 88, `同步完成: ${synced.length} 个文档`); return synced; } /** 为同步目录生成索引页 */ _generateSyncIndex(synced, isZh) { const docFiles = synced.filter((f) => f.path.startsWith('documents/')); if (docFiles.length > 0) { const lines = [ `# ${isZh ? '开发文档' : 'Developer Documents'}`, '', `> ${isZh ? '由 Cursor Agent 创建并同步到 Wiki 的开发文档' : 'Development documents created by Cursor Agent and synced to Wiki'}`, '', `| ${isZh ? '文档' : 'Document'} | ${isZh ? '来源' : 'Source'} |`, '|--------|--------|', ]; for (const f of docFiles) { const name = path.basename(f.path, '.md'); lines.push(`| [${name}](${path.basename(f.path)}) | ${f.source} |`); } lines.push(''); this._writeFile('documents/_index.md', lines.join('\n')); } } _emit(phase, progress, message) { try { this.onProgress(phase, progress, message); } catch { /* non-critical */ } } _ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } _writeFile(relativePath, content) { const fullPath = path.join(this.wikiDir, relativePath); this._ensureDir(path.dirname(fullPath)); fs.writeFileSync(fullPath, content, 'utf-8'); const hash = createHash('sha256').update(content).digest('hex').slice(0, 12); return { path: relativePath, hash, size: Buffer.byteLength(content) }; } _writeMeta(files, startTime, dedupResult) { const meta = { version: '3.0.0', generator: 'AutoSnippet WikiGenerator V3', generatedAt: new Date().toISOString(), duration: Date.now() - startTime, projectRoot: this.projectRoot, language: this.options.language, files: files.map((f) => ({ path: f.path, hash: f.hash, size: f.size, ...(f.source ? { source: f.source } : {}), ...(f.polished ? { polished: true } : {}), })), sourceHash: this._computeSourceHash(), ...(dedupResult ? { dedup: dedupResult } : {}), }; fs.writeFileSync(this.metaPath, JSON.stringify(meta, null, 2), 'utf-8'); return meta; } _readMeta() { try { if (!fs.existsSync(this.metaPath)) { return null; } return JSON.parse(fs.readFileSync(this.metaPath, 'utf-8')); } catch { return null; } } /** 检测源码是否有变更(简化:对比 sourceHash) */ _detectChanges(meta) { if (!meta?.sourceHash) { return true; } return meta.sourceHash !== this._computeSourceHash(); } /** 计算项目源文件的简易 hash(基于文件名列表 + 总大小) */ _computeSourceHash() { try { const extSet = LanguageService.sourceExts; let totalSize = 0; const names = []; walkDir(this.projectRoot, (filePath) => { const ext = path.extname(filePath); if (extSet.has(ext)) { const stat = fs.statSync(filePath); totalSize += stat.size; names.push(path.relative(this.projectRoot, filePath)); } }, 2000); names.sort(); const payload = `${names.join('\n')}\n${totalSize}`; return createHash('sha256').update(payload).digest('hex').slice(0, 16); } catch { return 'unknown'; } } _abortedResult() { return { success: false, error: 'aborted', duration: 0 }; } } export default WikiGenerator;