diff --git a/README.md b/README.md index 00d298c..1ed62e5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。 - 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。 - 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。 -- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。 +- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、`.kt-workspace/db-sync` 转储、备份库和行数校验。 - 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger、FnosK8s 的防踩坑清单和验证点。 - 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 - 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留;CLI 默认 dry-run,真实清理必须显式传 `--execute`。 @@ -37,7 +37,7 @@ pnpm run self-test pnpm run obsidian-context -- --module ktWorkflow pnpm run obsidian-validate pnpm run obsidian-sync -pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --problem "无新卡点" --solution "无新增稳定解法" +pnpm run workstream-closeout -- --title "发布闭环" --verification "Jenkins SUCCESS" --doc-sync "无需文档更新" --cleanup "cleanup-history dry-run deleted=0" --cleanup-final-deleted 0 --review "global-review findings=0" --problem "无新卡点" --solution "无新增稳定解法" pnpm run cleanup-history -- --dry-run pnpm run cleanup-history -- --execute pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login @@ -85,8 +85,8 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 | | `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 | | `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 | -| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、问题记录、稳定解法和 ktWorkflow 升级 | -| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理、问题固化、ktWorkflow 升级和全局 review | +| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级 | +| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review | | `kt_change_doc_sync` | 根据改动路径生成 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 文档同步清单 | | `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 | | `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 | @@ -111,7 +111,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | `pnpm run obsidian-validate` | 校验 KT Obsidian vault 结构和链接;默认 warning 不让脚本失败,需要严格模式时传 `--fail-on-warnings`。 | | `pnpm run obsidian-sync` | 审计 Obsidian 工作流入口是否连通,并联动执行 validate。 | | `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮并保留模板目录。 | -| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、问题记录、稳定解法和 ktWorkflow 升级缺口。 | +| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级缺口。 | ## 项目别名 @@ -146,12 +146,12 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login - 大方向结束前调用 `kt_workstream_closeout` 或 `pnpm run workstream-closeout`;如果识别到测试流、卡点、误报、清理、部署观测或命令模板,要先升级对应 ktWorkflow 规则再报告完成。 - 文件改动完成并验证后调用 `kt_global_code_review`,或运行 `pnpm run global-review`;它只读扫描,不删除文件、不提交代码。`findings` 先判定真实风险或工具误报,真实风险修业务代码,误报修 `mcp/ktWorkflow/src/tools/review.ts` 并复跑。 - 代码或配置改动后调用 `kt_change_doc_sync`,把需要同步的 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 入口补齐;无需同步时把原因写进收尾证据。 -- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理、问题固化、ktWorkflow 升级和全局 review 都过门。 +- 报告非平凡任务完成前调用 `kt_workflow_loop_audit`,确认测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review 都过门;清理证据必须显式传入,不能依赖缺省值。 - 要提交或推送时先调用 `kt_commit_plan` / `kt_push_plan`,按仓库分组确认范围。 - 要测真实业务链路时调用 `kt_business_test_plan`,选择 `admin-login`、`qqbot-auto-reply`、`qqbot-login-sse`、`fflogs-command`、`system-log-visualization` 等 flow。 - 遇到同一命令或同一远程步骤重复卡住时调用 `kt_blocker_resolution`;第二次仍失败时先写入 `TASKS.md` 或补成脚本,再继续。 - 远程服务排查先调用 `kt_remote_health_check`,默认只生成只读命令;需要执行时显式传 `execute=true`。 -- 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、备份库和校验点。 +- 数据库同步前调用 `kt_db_sync_plan`,先确认源库、目标库、`.kt-workspace/db-sync` 转储目录、带时间戳的备份库和校验点。 - 改 KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger、FnosK8s 前调用 `kt_component_workflow`,先看专项禁区。 - 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。 - 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。 @@ -160,7 +160,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login - Admin 页面测试前可先执行 `pnpm run admin-login -- --url ` 固化登录态,输出的 `storageState` 可作为后续 Playwright 用例前置状态。 - 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。 - 验证启动过本地服务后调用 `kt_cleanup_process_plan`,清掉本次进程。 -- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。 +- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,再复跑 dry-run 确认 `deleted=0`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。`kt_workstream_closeout` / `kt_workflow_loop_audit` 只认可明确的 `deleted=0` / `deleted=[]` 文本证据或结构化 `cleanupFinalDeleted=0`。 - 更新 Obsidian 图谱、模块页、文档矩阵或 `.obsidian` 配置后运行 `kt_obsidian_validate` / `pnpm run obsidian-validate`;整理工作流入口时再跑 `kt_obsidian_sync`。 - 改完文件后用 `kt_append_task_record` 先 `dryRun` 预览记录,再决定是否写入。 - 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。 diff --git a/src/core/cli.ts b/src/core/cli.ts index 95c5a56..5a11197 100644 --- a/src/core/cli.ts +++ b/src/core/cli.ts @@ -65,11 +65,18 @@ export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseo const patternSet = new Set(workstreamReusablePatternValues); const patterns = readOptionList(argv, ['--pattern', '--reusable-pattern']) .filter((pattern): pattern is WorkstreamReusablePattern => patternSet.has(pattern)); + const cleanupFinalDeleted = Number( + readOption(argv, ['--cleanup-final-deleted', '--cleanupFinalDeleted']), + ); return { changedProjects: readOptionList(argv, ['--project', '--changed-project']), + cleanupEvidence: readOptionList(argv, ['--cleanup', '--cleanup-evidence']), + cleanupFinalDeleted: Number.isFinite(cleanupFinalDeleted) ? cleanupFinalDeleted : undefined, + docSyncEvidence: readOptionList(argv, ['--doc-sync', '--doc-sync-evidence']), ktWorkflowUpdated: argv.includes('--kt-workflow-updated'), problemRecords: readOptionList(argv, ['--problem', '--problem-record']), + reviewEvidence: readOptionList(argv, ['--review', '--review-evidence']), reusablePatterns: patterns, stableSolutions: readOptionList(argv, ['--solution', '--stable-solution']), title: readOption(argv, ['--title']), diff --git a/src/registerTools.ts b/src/registerTools.ts index 68a3f41..7e1a483 100644 --- a/src/registerTools.ts +++ b/src/registerTools.ts @@ -249,8 +249,12 @@ export function registerTools(server: McpServer): void { description: '生成大方向完结闭环审计:开发、测试证据、问题记录、稳定解法和 ktWorkflow 升级是否齐备。', inputSchema: { changedProjects: z.array(z.string()).default([]), + cleanupEvidence: z.array(z.string()).default([]), + cleanupFinalDeleted: z.number().int().min(0).optional(), + docSyncEvidence: z.array(z.string()).default([]), ktWorkflowUpdated: z.boolean().default(false), problemRecords: z.array(z.string()).default([]), + reviewEvidence: z.array(z.string()).default([]), reusablePatterns: z.array(z.enum(workstreamReusablePatternValues)).default(['none']), stableSolutions: z.array(z.string()).default([]), title: z.string().optional(), @@ -269,7 +273,9 @@ export function registerTools(server: McpServer): void { inputSchema: { changedProjects: z.array(z.string()).default([]), cleanupExecuted: z.boolean().default(false), - cleanupPreviewDeleted: z.number().int().min(0).default(0), + cleanupEvidence: z.array(z.string()).default([]), + cleanupFinalDeleted: z.number().int().min(0).optional(), + cleanupPreviewDeleted: z.number().int().min(0).optional(), docSyncEvidence: z.array(z.string()).default([]), failureRecords: z.array(z.string()).default([]), ktWorkflowUpdated: z.boolean().default(false), diff --git a/src/selfTest.ts b/src/selfTest.ts index ccf9ab6..26449bb 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -101,6 +101,9 @@ export async function runSelfTest(): Promise { } const closeoutNeedsUpgrade = buildWorkstreamCloseout({ + cleanupEvidence: ['cleanup-history dry-run deleted=0。'], + docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], + reviewEvidence: ['global-review findings=0。'], problemRecords: ['Jenkins 状态需要结合日志和 K8s 状态确认。'], reusablePatterns: ['deploy-observation'], stableSolutions: ['收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。'], @@ -112,8 +115,11 @@ export async function runSelfTest(): Promise { } const closeoutComplete = buildWorkstreamCloseout({ + cleanupEvidence: ['cleanup-history dry-run deleted=0。'], + docSyncEvidence: ['无需文档更新,已记录原因。'], ktWorkflowUpdated: true, problemRecords: ['无新卡点。'], + reviewEvidence: ['global-review findings=0。'], reusablePatterns: ['none'], stableSolutions: ['无新增稳定解法。'], title: '文档收尾', @@ -123,6 +129,20 @@ export async function runSelfTest(): Promise { throw new Error('workstream closeout complete self-check failed'); } + const closeoutInvalidCleanup = buildWorkstreamCloseout({ + cleanupEvidence: ['cleanup-history dry-run deleted=3。'], + docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], + problemRecords: ['无新卡点。'], + reviewEvidence: ['global-review findings=0。'], + reusablePatterns: ['none'], + stableSolutions: ['无新增稳定解法。'], + title: '无效清理证据', + verificationEvidence: ['typecheck 通过。'], + }); + if ((closeoutInvalidCleanup.canReportComplete as boolean) !== false) { + throw new Error('workstream closeout cleanup evidence self-check failed'); + } + const obsidianContext = readObsidianContext({ maxDocuments: 6, module: 'ktWorkflow', @@ -178,6 +198,7 @@ export async function runSelfTest(): Promise { const loopAuditComplete = buildWorkflowLoopAudit({ cleanupExecuted: true, + cleanupFinalDeleted: 0, cleanupPreviewDeleted: 3, docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], ktWorkflowUpdated: true, @@ -201,6 +222,28 @@ export async function runSelfTest(): Promise { throw new Error('workflow loop blocked self-check failed'); } + const loopAuditMissingCleanup = buildWorkflowLoopAudit({ + docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], + reviewEvidence: ['global-review findings=0。'], + taskTitle: '缺清理证据闭环', + testCases: ['self-test'], + verificationEvidence: ['typecheck 通过。'], + }); + if ((loopAuditMissingCleanup.canReportComplete as boolean) !== false) { + throw new Error('workflow loop missing cleanup evidence self-check failed'); + } + + const dbSyncPlan = buildDbSyncPlan({}); + const dbSyncCommands = (dbSyncPlan.commands as string[]).join('\n'); + const backupName = dbSyncPlan.backupName as string; + if ( + !backupName.includes('_') || + !dbSyncCommands.includes('.kt-workspace/db-sync') || + dbSyncCommands.includes('CREATE DATABASE IF NOT EXISTS') + ) { + throw new Error('DB sync plan safety self-check failed'); + } + const data = { context: readWorkflowContext({ taskRecordCount: 2 }), guardrails: buildGuardrails({ @@ -258,7 +301,7 @@ export async function runSelfTest(): Promise { componentWorkflow: buildComponentWorkflow({ target: 'SystemLog', }), - dbSyncPlan: buildDbSyncPlan({}), + dbSyncPlan, finishTask: await buildFinishTask({ cleanupHistory: false, projects: ['mcp'], diff --git a/src/tools/cleanupEvidence.ts b/src/tools/cleanupEvidence.ts new file mode 100644 index 0000000..9c6521a --- /dev/null +++ b/src/tools/cleanupEvidence.ts @@ -0,0 +1,13 @@ +export function cleanupEvidenceShowsDeletedZero(values: string[]): boolean { + const text = values.join('\n'); + return [ + /\bdeleted\s*=\s*0\b/i, + /\bdeleted\s*:\s*0\b/i, + /["']deleted["']\s*:\s*\[\s*]/i, + /\bdeleted\s*[=:]\s*\[\s*]/i, + ].some((pattern) => pattern.test(text)); +} + +export function normalizeCleanupDeletedCount(value?: number): number | undefined { + return value === undefined ? undefined : Math.max(0, value); +} diff --git a/src/tools/closeout.ts b/src/tools/closeout.ts index fe94739..06ca875 100644 --- a/src/tools/closeout.ts +++ b/src/tools/closeout.ts @@ -1,5 +1,6 @@ import type { WorkstreamCloseoutInput, WorkstreamReusablePattern } from '../types.js'; import { workstreamReusablePatternValues } from '../core/constants.js'; +import { cleanupEvidenceShowsDeletedZero, normalizeCleanupDeletedCount } from './cleanupEvidence.js'; import { defaultReviewProjects } from './review.js'; interface UpgradeTarget { @@ -72,8 +73,14 @@ function buildPhase(name: string, evidence: string[], emptyHint: string): Record export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Record { const title = input.title?.trim() || ''; const changedProjects = normalizeList(input.changedProjects); + const cleanupEvidence = normalizeList(input.cleanupEvidence); + const cleanupFinalDeleted = normalizeCleanupDeletedCount(input.cleanupFinalDeleted); + const cleanupOk = + cleanupFinalDeleted === 0 || cleanupEvidenceShowsDeletedZero(cleanupEvidence); + const docSyncEvidence = normalizeList(input.docSyncEvidence); const verificationEvidence = normalizeList(input.verificationEvidence); const problemRecords = normalizeList(input.problemRecords); + const reviewEvidence = normalizeList(input.reviewEvidence); const stableSolutions = normalizeList(input.stableSolutions); const patterns = normalizePatterns(input.reusablePatterns); const upgradeNeeded = patterns.some((pattern) => pattern !== 'none'); @@ -81,6 +88,9 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re const missingItems = [ title ? '' : '填写本轮大方向名称或目标。', verificationEvidence.length > 0 ? '' : '补充测试或 smoke 验证证据;只有计划不算闭环完成。', + docSyncEvidence.length > 0 ? '' : '补充文档同步证据;无需更新时也要显式写明原因。', + cleanupOk ? '' : '补充历史产物清理证据;至少包含 cleanup-history dry-run deleted=0 或结构化 cleanupFinalDeleted=0。', + reviewEvidence.length > 0 ? '' : '补充全局 review 证据。', problemRecords.length > 0 ? '' : '补充问题记录;如无新卡点,显式写“无新卡点”。', stableSolutions.length > 0 ? '' : '补充稳定解法;如无新解法,显式写“无新增稳定解法”。', upgradeNeeded && !ktWorkflowUpdated @@ -98,6 +108,21 @@ export function buildWorkstreamCloseout(input: WorkstreamCloseoutInput = {}): Re closeoutOrder: [ buildPhase('development', title ? [title] : [], '写清本轮完成的开发目标。'), buildPhase('test/smoke verification', verificationEvidence, '补一条可复验的验证证据。'), + buildPhase('documentation sync', docSyncEvidence, '补充 kt_change_doc_sync 证据或无需更新说明。'), + { + evidence: [ + ...cleanupEvidence, + ...(cleanupFinalDeleted === undefined + ? [] + : [`cleanupFinalDeleted=${cleanupFinalDeleted}`]), + ], + name: 'history cleanup', + status: cleanupOk ? 'ok' : 'missing', + suggestion: cleanupOk + ? undefined + : '补充 cleanup-history dry-run deleted=0 证据或传入 cleanupFinalDeleted=0。', + }, + buildPhase('global review', reviewEvidence, '补充 kt_global_code_review 或 global-review 证据。'), buildPhase('problem record', problemRecords, '记录问题点;没有问题也要显式写明。'), buildPhase('stable solution', stableSolutions, '记录稳定解法;没有新增解法也要显式写明。'), { diff --git a/src/tools/loop.ts b/src/tools/loop.ts index cec7feb..58c75de 100644 --- a/src/tools/loop.ts +++ b/src/tools/loop.ts @@ -1,4 +1,5 @@ import type { WorkflowLoopAuditInput } from '../types.js'; +import { cleanupEvidenceShowsDeletedZero, normalizeCleanupDeletedCount } from './cleanupEvidence.js'; interface LoopGate { evidence: string[]; @@ -43,8 +44,18 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco const problemRecords = normalizeList(input.problemRecords); const stableSolutions = normalizeList(input.stableSolutions); const reviewEvidence = normalizeList(input.reviewEvidence); - const cleanupPreviewDeleted = Math.max(0, input.cleanupPreviewDeleted ?? 0); - const cleanupOk = cleanupPreviewDeleted === 0 || input.cleanupExecuted === true; + const cleanupEvidence = normalizeList(input.cleanupEvidence); + const cleanupPreviewDeleted = normalizeCleanupDeletedCount( + input.cleanupPreviewDeleted, + ); + const cleanupFinalDeleted = normalizeCleanupDeletedCount( + input.cleanupFinalDeleted, + ); + const cleanupEvidenceOk = cleanupEvidenceShowsDeletedZero(cleanupEvidence); + const cleanupOk = + cleanupEvidenceOk || + cleanupFinalDeleted === 0 || + cleanupPreviewDeleted === 0; const hasFailure = failureRecords.length > 0; const reusablePattern = input.reusablePattern === true; const needsSolidification = hasFailure || reusablePattern; @@ -59,11 +70,19 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco cleanupOk, cleanupOk ? [ - cleanupPreviewDeleted === 0 + cleanupEvidenceOk + ? 'cleanup-history evidence deleted=0' + : cleanupPreviewDeleted === 0 ? 'cleanup-history dry-run deleted=0' - : 'cleanup-history executed after stale artifacts were found', + : 'cleanup-history executed and final dry-run deleted=0', ] - : [`cleanup-history dry-run deleted=${cleanupPreviewDeleted}`], + : [ + cleanupFinalDeleted === undefined + ? cleanupPreviewDeleted === undefined + ? 'cleanup-history evidence missing' + : `cleanup-history dry-run deleted=${cleanupPreviewDeleted}; final dry-run missing` + : `cleanup-history final dry-run deleted=${cleanupFinalDeleted}`, + ], '执行 kt_cleanup_history dryRun=false 或 pnpm run cleanup-history -- --execute,并复跑 dry-run 确认 deleted=0。', ), buildRequiredGate('documentation sync evidence', docSyncEvidence, '运行 kt_change_doc_sync 并同步 README/API/AGENTS/docs/Obsidian/skills/ktWorkflow 中需要更新的入口;无需更新时写明原因。'), @@ -136,7 +155,9 @@ export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Reco stage, summary: { changedProjects, + cleanupFinalDeleted, cleanupPreviewDeleted, + cleanupEvidenceCount: cleanupEvidence.length, docSyncEvidenceCount: docSyncEvidence.length, failureCount: failureRecords.length, problemRecordCount: problemRecords.length, diff --git a/src/tools/workflow.ts b/src/tools/workflow.ts index fdb340b..a7ce103 100644 --- a/src/tools/workflow.ts +++ b/src/tools/workflow.ts @@ -1,7 +1,7 @@ import type { CommitPlanInput, ComponentWorkflowInput, DbSyncPlanInput, ExecResult, FinishTaskInput, PushPlanInput, RemoteHealthCheckInput, RepoType, ResolvedProject } from '../types.js'; import { tryExecFile, tryPowerShell } from '../core/exec.js'; import { detectRepoType, parseGitStatusFiles } from '../core/project.js'; -import { formatDateInShanghai, getGlobalReviewCommand, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; +import { getGlobalReviewCommand, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; import { cleanupHistoryArtifacts } from './cleanup.js'; import { karpathyChangedFileChecklistItem, karpathyFinishNote, karpathyStopCondition } from './karpathy.js'; import { buildGlobalCodeReview, defaultReviewProjects } from './review.js'; @@ -21,6 +21,25 @@ function normalizeProjectKeys(projects?: string[]): string[] { return Array.from(new Set(selected.filter((project) => project !== 'root'))); } +function formatShanghaiTimestamp(date = new Date()): string { + const parts = new Intl.DateTimeFormat('en-CA', { + day: '2-digit', + hour: '2-digit', + hour12: false, + minute: '2-digit', + month: '2-digit', + second: '2-digit', + timeZone: 'Asia/Shanghai', + year: 'numeric', + }).formatToParts(date); + const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); + return `${map.year}${map.month}${map.day}_${map.hour}${map.minute}${map.second}`; +} + +function normalizeDbIdentifier(value: string): string { + return value.replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').slice(0, 64); +} + async function collectRepoSnapshot(projectKey: string): Promise { const project = resolveProject(projectKey); const repoType = detectRepoType(project.path); @@ -274,17 +293,22 @@ export async function buildRemoteHealthCheck(input: RemoteHealthCheckInput = {}) export function buildDbSyncPlan(input: DbSyncPlanInput = {}): Record { const source = input.source || 'local'; const target = input.target || 'remote'; - const backupName = input.backupName || `kt_template_backup_${formatDateInShanghai().replaceAll('-', '')}`; + const backupName = normalizeDbIdentifier( + input.backupName || + `kt_template_backup_${formatShanghaiTimestamp()}_${Math.random().toString(36).slice(2, 8)}`, + ); + const dumpPath = `.kt-workspace/db-sync/${backupName}/kt_template.sql`; return { backupName, commands: [ '从 Node/kt-template-online-api/.env.development 和 .env.production 读取连接信息,只在本机 shell 环境使用,不写入 Git。', 'KT 当前约定:127.0.0.1:3306 是本地 MySQL,127.0.0.1:3307 是远程 NAS 打洞连接;3307 不通时先建立 ssh -N -L 127.0.0.1:3307:127.0.0.1:3306 nas。', - 'mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers kt_template > .codex-db-sync/kt_template.sql', - `mysql -e "CREATE DATABASE IF NOT EXISTS ${backupName} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"`, + `mkdir -p .kt-workspace/db-sync/${backupName}`, + `mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers kt_template > ${dumpPath}`, + `mysql -e "CREATE DATABASE ${backupName} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"`, `mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers kt_template | mysql ${backupName}`, - 'mysql kt_template < .codex-db-sync/kt_template.sql', + `mysql kt_template < ${dumpPath}`, '对比源库/目标库表数量、视图/函数/过程数量和关键表行数。', `验证通过后再执行:mysql -e "DROP DATABASE ${backupName};"`, ], diff --git a/src/types.ts b/src/types.ts index 572b849..73590b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -253,8 +253,12 @@ export interface WorkflowContextInput { export interface WorkstreamCloseoutInput { changedProjects?: string[]; + cleanupEvidence?: string[]; + cleanupFinalDeleted?: number; + docSyncEvidence?: string[]; ktWorkflowUpdated?: boolean; problemRecords?: string[]; + reviewEvidence?: string[]; reusablePatterns?: WorkstreamReusablePattern[]; stableSolutions?: string[]; title?: string; @@ -284,6 +288,8 @@ export interface ObsidianContextInput { export interface WorkflowLoopAuditInput { changedProjects?: string[]; cleanupExecuted?: boolean; + cleanupEvidence?: string[]; + cleanupFinalDeleted?: number; cleanupPreviewDeleted?: number; docSyncEvidence?: string[]; failureRecords?: string[];