diff --git a/src/core/constants.ts b/src/core/constants.ts index 57bf3fd..2821f40 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -92,7 +92,9 @@ export const defaultHistoryRoots = [ '.kt-workspace/test-logs', '.kt-workspace/verify', '.kt-workspace/playwright-mcp', + '.kt-workspace/subagents', '.kt-workspace/db-sync', + '.kt-workspace/exports', '.kt-workspace/tmp', '.kt-workspace/logs', '.kt-workspace/deploy', diff --git a/src/registerTools.ts b/src/registerTools.ts index 8e18e27..340acf4 100644 --- a/src/registerTools.ts +++ b/src/registerTools.ts @@ -178,7 +178,7 @@ export function registerTools(server: McpServer): void { server.registerTool( 'kt_cleanup_history', { - description: '清理 KT 运行态历史产物。默认 dryRun=true,只预览;执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮,template/_templates 等模板目录永久保留。', + description: '清理 KT 运行态历史产物。默认 dryRun=true,只预览;执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮;可复用模板和参考资产应迁入 workspace-assets。', inputSchema: { dryRun: z.boolean().default(true), keep: z.number().int().min(0).max(50).default(3), diff --git a/src/selfTest.ts b/src/selfTest.ts index ce4b2f3..10f6502 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -1957,12 +1957,18 @@ export async function runSelfTest(): Promise { const workspaceHistoryRoot = cleanupRoots.find( (item) => item.root === ".kt-workspace", ); - if (!testArtifactsRoot?.preservedNames.includes("_templates")) { - throw new Error("cleanup template preservation self-check failed"); + if (testArtifactsRoot?.preservedNames.includes("_templates")) { + throw new Error("cleanup template migration self-check failed"); } if (!workspaceHistoryRoot?.preservedNames.includes("test-artifacts")) { throw new Error("cleanup workspace root preservation self-check failed"); } + if ( + workspaceHistoryRoot.preservedNames.includes("external") || + workspaceHistoryRoot.preservedNames.includes("sources") + ) { + throw new Error("cleanup workspace cache preservation self-check failed"); + } const docSyncBangDream = await buildChangeDocSync({ changedFiles: [ @@ -2040,6 +2046,21 @@ export async function runSelfTest(): Promise { throw new Error("NapCat device profile guardrail self-check failed"); } + const review = await buildGlobalCodeReview({ + includeContentScan: true, + includeRootScan: true, + maxFindingsPerProject: 10, + projects: ["mcp"], + }); + const reviewFindings = review.findings as Array<{ category?: string }>; + if ( + reviewFindings.some((finding) => + finding.category?.startsWith("workspace-assets-"), + ) + ) { + throw new Error("workspace assets review self-check failed"); + } + const data = { context: readWorkflowContext({ taskRecordCount: 2 }), guardrails: buildGuardrails({ @@ -2067,12 +2088,7 @@ export async function runSelfTest(): Promise { taskType: "mcp", userRequest: "扩展 KT 工作区 MCP 可复用能力", }), - review: await buildGlobalCodeReview({ - includeContentScan: true, - includeRootScan: true, - maxFindingsPerProject: 10, - projects: ["mcp"], - }), + review, reviewCliParser, reviewClassifier, refactorGuardrails, diff --git a/src/tools/cleanup.ts b/src/tools/cleanup.ts index fee548f..453868a 100644 --- a/src/tools/cleanup.ts +++ b/src/tools/cleanup.ts @@ -36,32 +36,31 @@ interface HistoryEntry { type: 'directory' | 'file'; } -const alwaysPreservedTemplateNames = new Set(['_template', '_templates', 'template', 'templates']); const workspaceRootPreservedNames = new Set([ 'backup', 'db-sync', 'deploy', - 'external', + 'exports', 'logs', 'playwright-mcp', - 'sources', + 'subagents', 'test-artifacts', 'test-logs', 'tmp', 'verify', ]); -function isTemplateEntryName(name: string): boolean { - return alwaysPreservedTemplateNames.has(name.toLowerCase()); -} - -function buildPreservedNames(rootRelativePath: string, childNames: string[]): Set { +/** + * Builds the directory names that represent generated artifact categories under + * `.kt-workspace`; reusable templates and references are intentionally not + * preserved here because they belong in `workspace-assets`. + * + * @param rootRelativePath - Cleanup root relative to the KT workspace. + * @returns Category names that cleanup should skip when pruning `.kt-workspace`. + */ +function buildPreservedNames(rootRelativePath: string): Set { const preservedNames = new Set(); - for (const name of childNames) { - if (isTemplateEntryName(name)) preservedNames.add(name); - } - if (rootRelativePath === '.kt-workspace') { for (const name of workspaceRootPreservedNames) { preservedNames.add(name); @@ -126,8 +125,7 @@ export function cleanupHistoryArtifacts(input: CleanupHistoryInput = {}): Record continue; } - const childNames = readdirSync(rootPath, { withFileTypes: true }).map((item) => item.name); - const preservedNames = buildPreservedNames(rootRelativePath, childNames); + const preservedNames = buildPreservedNames(rootRelativePath); const entries = listHistoryEntries(rootPath, preservedNames); const preserved = entries.slice(0, keep); const deleting = entries.slice(keep); diff --git a/src/tools/obsidian.ts b/src/tools/obsidian.ts index 90d1658..e9bee8e 100644 --- a/src/tools/obsidian.ts +++ b/src/tools/obsidian.ts @@ -172,7 +172,6 @@ const requiredCorePlugins = [ const expectedIgnoredRoots = [ '.git/', '.kt-workspace/', - '_exports/', 'Node/', 'Plugins/', 'Vue/', @@ -278,7 +277,6 @@ const ignoredDirectoryNames = new Set([ '.pnpm', '.turbo', '.vite', - '_exports', 'build', 'coverage', 'dist', diff --git a/src/tools/review.ts b/src/tools/review.ts index 4b15a65..02e1484 100644 --- a/src/tools/review.ts +++ b/src/tools/review.ts @@ -41,10 +41,15 @@ const rootGeneratedArtifactNames = new Set([ ".codex-test-logs", ".codex-verify", ".playwright-mcp", + "_exports", "codex-test-logs", "test-artifacts", ]); +const workspaceAssetBlockedFilePattern = + /(^|\/)(\.env($|\..+)|.*\.(7z|dump|gz|rar|sql|storageState[^/]*|tar|tgz|zip)|id_rsa|[^/]+\.(key|pem|pfx|p12))$/i; +const workspaceAssetMaxFileBytes = 2 * 1024 * 1024; + const sensitiveTrackedFilePattern = /(^|\/)(\.env($|\..+)|deploy\.json|secrets?\.(env|json|ya?ml)|id_rsa|[^/]+\.(pem|key))$/i; @@ -990,6 +995,64 @@ function findRootGeneratedArtifacts(): string[] { .map((entry) => entry.name); } +/** + * Finds files that would make `workspace-assets` behave like another runtime + * dump/cache directory instead of a small reusable asset store. + * + * @returns Review findings for large, archived, dump-like, or secret-like files + * found under `workspace-assets`. + */ +function findWorkspaceAssetPolicyFindings(): ReviewFinding[] { + const assetRoot = path.join(workspaceRoot, "workspace-assets"); + if (!existsSync(assetRoot)) return []; + + const findings: ReviewFinding[] = []; + + const walk = (currentPath: string) => { + for (const entry of readdirSync(currentPath, { withFileTypes: true })) { + const entryPath = path.join(currentPath, entry.name); + const relativePath = toPosix(path.relative(workspaceRoot, entryPath)); + + if (entry.isDirectory()) { + if (reviewSkipDirectoryNames.has(entry.name)) continue; + walk(entryPath); + continue; + } + + if (!entry.isFile()) continue; + + const stats = statSync(entryPath); + if (workspaceAssetBlockedFilePattern.test(relativePath)) { + findings.push({ + category: "workspace-assets-blocked-file", + file: relativePath, + level: "P1", + message: "workspace-assets 中出现禁止跟踪的归档、dump、环境或密钥类文件。", + project: "root", + suggestion: + "移回 .kt-workspace 或外部安全存储;workspace-assets 只保留小型可复用资产。", + }); + continue; + } + + if (stats.size > workspaceAssetMaxFileBytes) { + findings.push({ + category: "workspace-assets-large-file", + file: relativePath, + level: "P2", + message: "workspace-assets 中出现超过 2MB 的文件,可能是缓存或第三方源码。", + project: "root", + suggestion: + "确认是否必须 Git 管理;大体积参考材料应放到外部仓库或 .kt-workspace 缓存。", + }); + } + } + }; + + walk(assetRoot); + return findings; +} + async function reviewProject( projectKey: string, input: Required< @@ -1088,6 +1151,8 @@ export async function buildGlobalCodeReview( : findWorkspaceFilesByName(new Set(["deploy.js", "deploy.json"]), 50); const rootGeneratedArtifacts = input.includeRootScan === false ? [] : findRootGeneratedArtifacts(); + const workspaceAssetPolicyFindings = + input.includeRootScan === false ? [] : findWorkspaceAssetPolicyFindings(); const taskRecordFindings = input.includeRootScan === false ? [] @@ -1115,13 +1180,14 @@ export async function buildGlobalCodeReview( message: "根目录存在生成态临时产物。", project: "root", suggestion: - "统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。", + "生成证据迁入 .kt-workspace;可复用资产迁入 workspace-assets,并更新生成脚本或测试流程。", }), ); const allFindings = [ ...findings, ...rootFindings, ...rootGeneratedFindings, + ...workspaceAssetPolicyFindings, ...taskRecordFindings, ];