chore: 收敛工作区资产清理规则

This commit is contained in:
sunlei 2026-06-25 09:27:32 +08:00
parent 1abfcbf59d
commit e2a50f03b3
6 changed files with 106 additions and 26 deletions

View File

@ -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',

View File

@ -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),

View File

@ -1957,12 +1957,18 @@ export async function runSelfTest(): Promise<void> {
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<void> {
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<void> {
taskType: "mcp",
userRequest: "扩展 KT 工作区 MCP 可复用能力",
}),
review: await buildGlobalCodeReview({
includeContentScan: true,
includeRootScan: true,
maxFindingsPerProject: 10,
projects: ["mcp"],
}),
review,
reviewCliParser,
reviewClassifier,
refactorGuardrails,

View File

@ -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<string> {
/**
* 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<string> {
const preservedNames = new Set<string>();
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);

View File

@ -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',

View File

@ -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,
];