From d813dc69abd0c6fe14ba6f0d29b49fe537a91950 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 3 Jun 2026 18:37:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=84=E8=8C=83KT=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E4=B8=8A=E4=B8=8B=E6=96=87=E6=B2=BB=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++----- package.json | 2 +- scripts/admin-login-smoke.ts | 2 +- src/core/constants.ts | 14 +++--- src/core/workspace.ts | 32 ++++++++++++ src/registerTools.ts | 13 +++-- src/selfTest.ts | 5 +- src/tools/cleanup.ts | 4 +- src/tools/envRisk.ts | 95 ++++++++++++++++++++++++++++++++---- src/tools/guardrails.ts | 2 +- src/tools/review.ts | 60 ++++++++++++++++++++++- src/tools/task.ts | 15 +++++- src/tools/verification.ts | 7 +++ src/tools/workflow.ts | 13 ++++- src/types.ts | 4 +- 15 files changed, 249 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index bff952d..074b063 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # KT Workflow MCP -`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md`、`SKILLS.md`、`TASKS.md` 中的协作流程变成可调用工具。 +`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用 TypeScript 封装工具入参、项目别名和返回结构,用来把根目录 `AGENTS.md` 的硬规则、`SKILLS.md` 的工作流索引、`TASKS.md` 的上下文记录和 `docs/` 的详细流程变成可调用工具。 ## 能力边界 -- 读取 KT 工作区上下文:项目清单、硬性规则、协作技能、最近任务记录。 +- 读取 KT 工作区紧凑上下文:项目清单、硬性规则、skill registry、标准 skill 包索引、历史关键词、最近任务记录和详细文档入口;需要全量文档时显式开启。 - 检查子项目环境:Git/SVN、包管理器、Node 版本、env 文件、Git 状态。 - 生成防偏差工作包:把开工前检查、禁止项、风险扫描、验证计划汇总成一份可执行清单。 - 生成验证建议:按后端、前端、样式、页面、部署、MCP 等变更类型给出轻量验证命令。 @@ -15,11 +15,11 @@ - 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、Web/Playground 回跳。 - 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。 - 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。 -- 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot 的防踩坑清单和验证点。 +- 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 的防踩坑清单和验证点。 - 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 -- 清理历史产物:按目录最近修改时间只保留最近 3 轮测试/验证产物,模板目录永久保留。 -- 检查 env 策略和变更风险:提醒真实配置、锁文件、核心表格组件、部署链路等高风险改动。 -- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量和当前变更风险;任何文件改动后都要跑一遍。 +- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留。 +- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。 +- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;任何文件改动后都要跑一遍。 - 生成或写入 `TASKS.md` 改动记录:默认 `dryRun=true`,确认后再落盘。 - 生成提交前检查清单:校验 KT commit message 约定。 @@ -62,7 +62,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | 工具 | 用途 | | --- | --- | -| `kt_read_context` | 读取 KT 根目录上下文和最近任务记录 | +| `kt_read_context` | 读取 KT 根目录紧凑上下文和最近任务记录,支持显式读取完整文档 | | `kt_inspect_project` | 检查子项目仓库、包管理器、Node、env 和 Git 状态 | | `kt_inspect_all_projects` | 一次性检查所有 KT 项目,适合多仓库联动任务 | | `kt_guardrails` | 按任务类型生成开工、改动、禁止项和验证约束 | @@ -70,7 +70,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `kt_suggest_verification` | 生成轻量验证命令和注意事项 | | `kt_create_page_test_case` | 生成页面级可视化测试用例 | | `kt_api_test_plan` | 生成接口真实调用测试计划 | -| `kt_cleanup_history` | 清理历史测试/验证产物,默认预览,执行时只保留最近 3 轮 | +| `kt_cleanup_history` | 清理 `.kt-workspace` 历史测试/验证产物,默认预览,执行时只保留最近 3 轮 | | `kt_cleanup_process_plan` | 生成验证进程清理检查命令 | | `kt_env_policy` | 检查 env 文件现状和提交策略 | | `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 | @@ -101,6 +101,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `api` | `Node/kt-template-online-api` | | `admin` | `Vue/kt-template-admin` | | `blog` | `Vue/kt-blog-web` | +| `knife4j` | `Plugins/knife4j-swagger-vue3` | | `web` | `Vue/kt-template-online-web` | | `playground` | `Vue/kt-template-online-playground` | @@ -110,12 +111,13 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login - 工具入参类型集中在 `src/types.ts`,MCP 注册集中在 `src/registerTools.ts`。 - `src/core/*` 放项目别名、工作区路径、命令执行、仓库/包管理器识别等基础能力。 - `src/tools/*` 按功能拆分:检查、验证、测试、清理、风险、复审、任务记录和工作流计划。 +- 根目录上下文分层遵守 `docs/kt-context-semantics.md`:`AGENTS.md` 是硬规则,`SKILLS.md` 是 registry,标准 skill 正文放在 `skills/*/SKILL.md`。 - MCP 客户端直接通过 `node --import tsx loader` 启动 TS 入口,不再保留旧的 `src/server.mjs`。 - Node/npm/pnpm 版本异常时优先走 `nvm ls` + `nvm use`,不要用扫盘路径作为第一选择。 ## 使用建议 -- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context` 或 `kt_inspect_project`。 +- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context` 或 `kt_inspect_project`。`kt_read_context` 默认紧凑输出,只有确实需要完整 `TASKS.md` 时再传 `includeFullDocs=true`。 - 多项目联动时先调用 `kt_inspect_all_projects`。 - 要收尾时调用 `kt_finish_task`,默认只生成计划和 review;需要执行验证时显式传 `runValidation=true`。 - 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。 @@ -123,13 +125,13 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login - 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply` 等 flow。 - 远程服务排查先调用 `kt_remote_health_check`,默认只生成只读命令;需要执行时显式传 `execute=true`。 - 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、备份库和校验点。 -- 改 KtTable、BlogArgon、AdminAuth、QQBot 前调用 `kt_component_workflow`,先看专项禁区。 +- 改 KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger 前调用 `kt_component_workflow`,先看专项禁区。 - 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。 - 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。 - 页面测试前调用 `kt_create_page_test_case`,再执行 Playwright/浏览器测试。 - Admin 页面测试前可先执行 `pnpm run admin-login -- --url ` 固化登录态,输出的 `storageState` 可作为后续 Playwright 用例前置状态。 - 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。 - 验证启动过本地服务后调用 `kt_cleanup_process_plan`,清掉本次进程。 -- 每轮测试/验证结束后调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --keep=3`,让历史产物只保留最近 3 轮。 +- 每轮测试/验证结束后调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --keep=3`,让 `.kt-workspace` 历史产物只保留最近 3 轮。 - 改完文件后用 `kt_append_task_record` 先 `dryRun` 预览记录,再决定是否写入。 - 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。 diff --git a/package.json b/package.json index 937d644..2acf896 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kt/mcp-kt-workflow", - "version": "0.4.0", + "version": "0.5.0", "private": true, "type": "module", "description": "Reusable MCP workflow tools for the KT workspace.", diff --git a/scripts/admin-login-smoke.ts b/scripts/admin-login-smoke.ts index b230b86..db55fe1 100644 --- a/scripts/admin-login-smoke.ts +++ b/scripts/admin-login-smoke.ts @@ -20,7 +20,7 @@ const workspaceRoot = path.resolve( process.env.KT_WORKSPACE_ROOT || path.join(dirname, '../../..'), ); const adminRoot = path.join(workspaceRoot, 'Vue/kt-template-admin'); -const defaultArtifactDir = path.join(workspaceRoot, '.codex-verify/admin-login'); +const defaultArtifactDir = path.join(workspaceRoot, '.kt-workspace/verify/admin-login'); /** * @param value 原始命令行布尔值。 diff --git a/src/core/constants.ts b/src/core/constants.ts index 767b1a5..07f242e 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -3,6 +3,7 @@ export const projectAliases = { admin: 'Vue/kt-template-admin', api: 'Node/kt-template-online-api', blog: 'Vue/kt-blog-web', + knife4j: 'Plugins/knife4j-swagger-vue3', mcp: 'mcp/ktWorkflow', playground: 'Vue/kt-template-online-playground', root: '.', @@ -13,6 +14,7 @@ export const projectLabels = { admin: 'Vue Admin 后台', api: 'NestJS API 后端', blog: 'Blog Web 博客前台', + knife4j: 'Knife4j Swagger Vue3 插件', mcp: 'KT Workflow MCP', playground: 'Playground 编辑器', root: 'KT 协作根目录', @@ -59,10 +61,10 @@ export const registeredToolNames = [ ] as const; export const defaultHistoryRoots = [ - 'test-artifacts', - '.codex-test-logs', - 'codex-test-logs', - '.codex-verify', - '.playwright-mcp', - '.codex-db-sync', + '.kt-workspace/test-artifacts', + '.kt-workspace/test-logs', + '.kt-workspace/verify', + '.kt-workspace/playwright-mcp', + '.kt-workspace/db-sync', + '.kt-workspace/tmp', ] as const; diff --git a/src/core/workspace.ts b/src/core/workspace.ts index f1473a2..2e314ac 100644 --- a/src/core/workspace.ts +++ b/src/core/workspace.ts @@ -85,6 +85,38 @@ export function getRecentTaskRecords(count = 5): string { return records.join('\n\n').trim(); } +export function getTaskContextIndex(): string { + const tasks = readText('TASKS.md'); + const recentMarker = '\n## 最近记录'; + const historyMarker = '\n## 历史索引'; + const recentIndex = tasks.indexOf(recentMarker); + const historyIndex = tasks.indexOf(historyMarker); + const head = recentIndex === -1 ? tasks : tasks.slice(0, recentIndex); + const history = historyIndex === -1 ? '' : tasks.slice(historyIndex); + + return [head.trim(), history.trim()].filter(Boolean).join('\n\n'); +} + +export function listWorkspaceSkills(): Array<{ description: string | null; name: string; path: string }> { + const skillsRoot = resolveInsideRoot('skills'); + if (!existsSync(skillsRoot)) return []; + + return readdirSync(skillsRoot, { withFileTypes: true }) + .filter((item) => item.isDirectory()) + .map((item) => { + const skillPath = path.join(skillsRoot, item.name, 'SKILL.md'); + const content = existsSync(skillPath) ? readFileSync(skillPath, 'utf8') : ''; + const name = content.match(/^name:\s*(.+)$/m)?.[1]?.trim() || item.name; + const description = content.match(/^description:\s*(.+)$/m)?.[1]?.trim() || null; + + return { + description, + name, + path: toPosix(path.relative(workspaceRoot, skillPath)), + }; + }); +} + export function formatDateInShanghai(date = new Date()): string { const parts = new Intl.DateTimeFormat('en-CA', { day: '2-digit', diff --git a/src/registerTools.ts b/src/registerTools.ts index 4ddc90b..fe9d663 100644 --- a/src/registerTools.ts +++ b/src/registerTools.ts @@ -27,9 +27,10 @@ export function registerTools(server: McpServer): void { server.registerTool( 'kt_read_context', { - description: '读取 KT 根目录 AGENTS/SKILLS/TASKS,并返回项目清单和最近任务记录。', + description: '读取 KT 根目录紧凑上下文,并返回项目清单和最近任务记录;需要完整文档时显式 includeFullDocs=true。', inputSchema: { - taskRecordCount: z.number().int().min(0).max(20).default(5), + includeFullDocs: z.boolean().default(false), + taskRecordCount: z.number().int().min(0).max(20).default(3), }, title: 'Read KT Workflow Context', }, @@ -152,7 +153,7 @@ export function registerTools(server: McpServer): void { server.registerTool( 'kt_cleanup_history', { - description: '清理 KT 根目录历史产物。默认 dryRun=true,只预览;执行时按 LastWriteTime 每个历史目录仅保留最近 3 轮,test-artifacts/_templates 永久保留。', + description: '清理 KT 运行态历史产物。默认 dryRun=true,只预览;执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮,.kt-workspace/test-artifacts/_templates 永久保留。', inputSchema: { dryRun: z.boolean().default(true), keep: z.number().int().min(0).max(50).default(3), @@ -318,9 +319,11 @@ export function registerTools(server: McpServer): void { server.registerTool( 'kt_component_workflow', { - description: '按 KT 专项组件/链路输出防踩坑工作流:KtTable、BlogArgon、AdminAuth、QQBot。', + description: '按 KT 专项组件/链路输出防踩坑工作流:KtTable、BlogArgon、AdminAuth、QQBot、Knife4jSwagger。', inputSchema: { - target: z.enum(['AdminAuth', 'BlogArgon', 'KtTable', 'QQBot']).default('KtTable'), + target: z + .enum(['AdminAuth', 'BlogArgon', 'Knife4jSwagger', 'KtTable', 'QQBot']) + .default('KtTable'), }, title: 'KT Component Workflow', }, diff --git a/src/selfTest.ts b/src/selfTest.ts index bc3f2cf..9a9f215 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -24,6 +24,7 @@ export async function runSelfTest(): Promise { keep: 3, }), inspectAdmin: await inspectProject({ includeGit: false, project: 'admin' }), + inspectKnife4j: await inspectProject({ includeGit: false, project: 'knife4j' }), prepareTask: await prepareTask({ includeGit: false, project: 'mcp', @@ -43,7 +44,7 @@ export async function runSelfTest(): Promise { projects: ['mcp'], }), componentWorkflow: buildComponentWorkflow({ - target: 'KtTable', + target: 'Knife4jSwagger', }), dbSyncPlan: buildDbSyncPlan({}), finishTask: await buildFinishTask({ @@ -63,7 +64,7 @@ export async function runSelfTest(): Promise { project: 'mcp', }), }; - const outputDir = resolveInsideRoot('.codex-verify/ktWorkflow'); + const outputDir = resolveInsideRoot('.kt-workspace/verify/ktWorkflow'); mkdirSync(outputDir, { recursive: true }); writeFileSync( path.join(outputDir, 'self-test.json'), diff --git a/src/tools/cleanup.ts b/src/tools/cleanup.ts index 2006dae..e31095b 100644 --- a/src/tools/cleanup.ts +++ b/src/tools/cleanup.ts @@ -91,7 +91,9 @@ export function cleanupHistoryArtifacts(input: CleanupHistoryInput = {}): Record continue; } - const preservedNames = new Set(rootRelativePath === 'test-artifacts' ? ['_templates'] : []); + const preservedNames = new Set( + rootRelativePath.endsWith('test-artifacts') ? ['_templates'] : [], + ); const entries = listHistoryEntries(rootPath, preservedNames); const preserved = entries.slice(0, keep); const deleting = entries.slice(keep); diff --git a/src/tools/envRisk.ts b/src/tools/envRisk.ts index e56af00..45b24d3 100644 --- a/src/tools/envRisk.ts +++ b/src/tools/envRisk.ts @@ -1,12 +1,38 @@ -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync, statSync } from 'node:fs'; import path from 'node:path'; +import type { ProjectAlias } from '../types.js'; import type { EnvPolicyInput, RiskScanInput } from '../types.js'; import { tryExecFile } from '../core/exec.js'; import { detectRepoType, parseGitStatusFiles } from '../core/project.js'; import { readTextIfExists, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; + +const frontendEnvProjectAliases = new Set([ + 'admin', + 'blog', + 'playground', + 'web', +]); + +function allowsTrackedFrontendEnv(projectAlias: ProjectAlias | null): boolean { + return Boolean(projectAlias && frontendEnvProjectAliases.has(projectAlias)); +} + +function readSmallFileIfExists(filePath: string): string { + if (!existsSync(filePath)) return ''; + + try { + const stats = statSync(filePath); + if (!stats.isFile() || stats.size > 512 * 1024) return ''; + return readFileSync(filePath, 'utf8'); + } catch { + return ''; + } +} + export function buildEnvPolicy(input: EnvPolicyInput): Record { const project = resolveProject(input.project); + const frontendEnvAllowed = allowsTrackedFrontendEnv(project.alias); const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => { const absolutePath = path.join(project.path, file); return { @@ -24,14 +50,20 @@ export function buildEnvPolicy(input: EnvPolicyInput): Record { return { envFiles, ignoredRules: ignoredRules.filter((line) => line.includes('.env')), - policy: [ - '真实开发和生产配置只保留在本地,不提交。', - '.env.example 用于维护变量名和示例值。', - '接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。', - ], + policy: frontendEnvAllowed + ? [ + '前端项目的 .env.development/.env.production 可以跟随仓库,用于客户端构建变量。', + '前端 env 仍不得写入服务端密钥、数据库密码、WordPress 管理员密码、MinIO Secret 或 token。', + '.env.example 继续维护变量名和示例值。', + ] + : [ + '真实开发和生产配置只保留在本地,不提交。', + '.env.example 用于维护变量名和示例值。', + '接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。', + ], project, warnings: envFiles - .filter((file) => file.file !== '.env.example' && file.exists) + .filter((file) => file.file !== '.env.example' && file.exists && !frontendEnvAllowed) .map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`), }; } @@ -65,8 +97,17 @@ export async function scanTaskRisk(input: RiskScanInput): Promise\s*\{\s*(?!\{[\s\S]*?\bdefault\s*:)/.test(source)) { + addRisk( + 'medium', + file, + 'TabPane 插槽疑似未显式声明 default。', + 'Vue TSX 嵌套组件插槽使用 {{ default: () => ... }} 或等价显式 default 写法。', + ); + } + if (/\bonUpdate:[A-Za-z_$][\w$]*/.test(source)) { + addRisk( + 'medium', + file, + 'Vue TSX 中存在手写 onUpdate:* 绑定。', + '优先使用原生 v-model:value / v-model:activeKey 等写法,减少事件名偏差。', + ); + } + if (/\bdestroyOnClose\b/.test(source)) { + addRisk( + 'medium', + file, + 'Antdv Modal destroyOnClose 已废弃。', + '改用 destroyOnHidden,或优先复用 Vben Modal/Form 封装。', + ); + } + } } return { diff --git a/src/tools/guardrails.ts b/src/tools/guardrails.ts index 4692dbd..45478be 100644 --- a/src/tools/guardrails.ts +++ b/src/tools/guardrails.ts @@ -16,7 +16,7 @@ export function buildGuardrails(input: GuardrailsInput): GuardrailsResult { duringEdit: [ '改动范围收敛到本次需求相关文件。', '复杂度高的地方加简短注释,避免空泛解释。', - '真实环境配置不写入提交内容,优先只维护 .env.example。', + '后端真实环境配置不写入提交内容;前端 env 只允许客户端公开变量。', '前端实现遵循现有 antdv-next/Vben 约定,不引入无关抽象。', ], doNot: [ diff --git a/src/tools/review.ts b/src/tools/review.ts index f2c7964..14764a5 100644 --- a/src/tools/review.ts +++ b/src/tools/review.ts @@ -13,6 +13,7 @@ export const defaultReviewProjects = [ 'web', 'playground', 'mcp', + 'knife4j', ] as const; const reviewSkipDirectoryNames = new Set([ @@ -29,11 +30,27 @@ const reviewSkipDirectoryNames = new Set([ const reviewSkipFilePattern = /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|.*\.map|.*\.png|.*\.jpg|.*\.jpeg|.*\.gif|.*\.webp|.*\.zip|.*\.tar|.*\.gz)$/i; +const rootGeneratedArtifactNames = new Set([ + '.codex-db-sync', + '.codex-test-logs', + '.codex-verify', + '.playwright-mcp', + 'codex-test-logs', + 'test-artifacts', +]); + const sensitiveTrackedFilePattern = /(^|\/)(\.env($|\..+)|deploy\.json|secrets?\.(env|json|ya?ml)|id_rsa|[^/]+\.(pem|key))$/i; const allowedSensitiveTrackedFilePattern = /(^|\/)\.env\.example$/i; +const frontendEnvProjectAliases = new Set([ + 'admin', + 'blog', + 'playground', + 'web', +]); + const credentialAssignmentPattern = /\b(password|passwd|secret|token|webuiToken|accessToken|privateKey)\b\s*[:=]\s*(["'])([^"']+)\2/i; @@ -124,6 +141,14 @@ function isCredentialHintAllowed(file: string, line: string): boolean { return /(^|\/)views\/(demos|examples)\//.test(normalized); } +function isAllowedSensitiveTrackedFile(projectAlias: ProjectAlias | null, file: string): boolean { + if (allowedSensitiveTrackedFilePattern.test(file)) return true; + if (projectAlias && frontendEnvProjectAliases.has(projectAlias)) { + return /(^|\/)\.env($|\..+)/i.test(file); + } + return false; +} + function collectProjectContentFindings( project: ResolvedProject, trackedFiles: string[], @@ -168,6 +193,18 @@ function collectProjectContentFindings( } if (includeContentScan) { + if (/\.(tsx|jsx|vue)$/.test(file) && /\bfunction\s+render[A-Z0-9_]/.test(line)) { + findings.push({ + category: 'vue-tsx-render-helper', + file, + line: index + 1, + level: 'P2', + message: 'Vue TSX render helper 使用 function 声明。', + project: project.relativePath, + suggestion: '改为箭头函数,并在嵌套组件插槽中显式传 default。', + }); + } + const credentialMatch = line.match(credentialAssignmentPattern); if ( credentialMatch && @@ -227,6 +264,14 @@ function findWorkspaceFilesByName(names: Set, maxResults: number): strin return results; } +function findRootGeneratedArtifacts(): string[] { + if (!existsSync(workspaceRoot)) return []; + + return readdirSync(workspaceRoot, { withFileTypes: true }) + .filter((entry) => rootGeneratedArtifactNames.has(entry.name)) + .map((entry) => entry.name); +} + async function reviewProject( projectKey: string, input: Required>, @@ -242,7 +287,7 @@ async function reviewProject( const trackedSensitiveFiles = trackedFiles.filter( (file) => sensitiveTrackedFilePattern.test(file) && - !allowedSensitiveTrackedFilePattern.test(file), + !isAllowedSensitiveTrackedFile(project.alias, file), ); const contentFindings = collectProjectContentFindings( project, @@ -301,6 +346,8 @@ export async function buildGlobalCodeReview( input.includeRootScan === false ? [] : findWorkspaceFilesByName(new Set(['deploy.js', 'deploy.json']), 50); + const rootGeneratedArtifacts = + input.includeRootScan === false ? [] : findRootGeneratedArtifacts(); const findings = projectResults.flatMap((item) => [ ...item.findings, ...item.debugFindings, @@ -314,7 +361,15 @@ export async function buildGlobalCodeReview( project: 'root', suggestion: '确认是否仍被脚本引用;无用则删除并从 Git 索引移除。', })); - const allFindings = [...findings, ...rootFindings]; + const rootGeneratedFindings = rootGeneratedArtifacts.map((file) => ({ + category: 'root-generated-artifact', + file, + level: 'P2', + message: '根目录存在生成态临时产物。', + project: 'root', + suggestion: '统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。', + })); + const allFindings = [...findings, ...rootFindings, ...rootGeneratedFindings]; return { findings: allFindings, @@ -344,6 +399,7 @@ export async function buildGlobalCodeReview( p2: allFindings.filter((item) => item.level === 'P2').length, p3: allFindings.filter((item) => item.level === 'P3').length, reviewedProjects: projectResults.length, + rootGeneratedArtifacts, rootLegacyFiles, }, workspaceRoot, diff --git a/src/tools/task.ts b/src/tools/task.ts index 02f3b8f..f058d16 100644 --- a/src/tools/task.ts +++ b/src/tools/task.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import type { CommitChecklistInput, PrepareTaskInput, TaskRecordInput, WorkflowContextInput } from '../types.js'; import { projectAliases } from '../core/constants.js'; import { detectRepoType } from '../core/project.js'; -import { formatDateInShanghai, getRecentTaskRecords, readText, resolveInsideRoot, resolveProject, workspaceRoot } from '../core/workspace.js'; +import { formatDateInShanghai, getRecentTaskRecords, getTaskContextIndex, listWorkspaceSkills, readText, resolveInsideRoot, resolveProject, workspaceRoot } from '../core/workspace.js'; import { buildGuardrails } from './guardrails.js'; import { inspectProject } from './inspect.js'; import { scanTaskRisk } from './envRisk.js'; @@ -125,6 +125,8 @@ export async function createCommitChecklist(input: CommitChecklistInput): Promis } export function readWorkflowContext(input: WorkflowContextInput): Record { + const includeFullDocs = input.includeFullDocs === true; + const taskRecordCount = input.taskRecordCount ?? 3; const projects = Object.keys(projectAliases).map((key) => { const project = resolveProject(key); return { @@ -137,9 +139,18 @@ export function readWorkflowContext(input: WorkflowContextInput): Record kt_template > .codex-db-sync/kt_template.sql', `mysql -e "CREATE DATABASE IF NOT EXISTS ${backupName} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"`, `mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers kt_template | mysql ${backupName}`, @@ -266,6 +270,7 @@ export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record