fix(workflow): 收紧清理闭环门禁

This commit is contained in:
sunlei 2026-06-11 15:04:25 +08:00
parent 432320dd9d
commit b5f24b3ee0
9 changed files with 165 additions and 20 deletions

View File

@ -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 <Admin登录页>` 固化登录态,输出的 `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`,确认文件范围和提交信息。

View File

@ -65,11 +65,18 @@ export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseo
const patternSet = new Set<string>(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']),

View File

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

View File

@ -101,6 +101,9 @@ export async function runSelfTest(): Promise<void> {
}
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<void> {
}
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<void> {
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<void> {
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<void> {
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<void> {
componentWorkflow: buildComponentWorkflow({
target: 'SystemLog',
}),
dbSyncPlan: buildDbSyncPlan({}),
dbSyncPlan,
finishTask: await buildFinishTask({
cleanupHistory: false,
projects: ['mcp'],

View File

@ -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);
}

View File

@ -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<string, unknown> {
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, '记录稳定解法;没有新增解法也要显式写明。'),
{

View File

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

View File

@ -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<RepoSnapshot> {
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<string, unknown> {
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 是本地 MySQL127.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 <source-conn> kt_template > .codex-db-sync/kt_template.sql',
`mysql <target-conn> -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 <source-conn> kt_template > ${dumpPath}`,
`mysql <target-conn> -e "CREATE DATABASE ${backupName} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"`,
`mysqldump --set-gtid-purged=OFF --single-transaction --routines --triggers <target-conn> kt_template | mysql <target-conn> ${backupName}`,
'mysql <target-conn> kt_template < .codex-db-sync/kt_template.sql',
`mysql <target-conn> kt_template < ${dumpPath}`,
'对比源库/目标库表数量、视图/函数/过程数量和关键表行数。',
`验证通过后再执行mysql <target-conn> -e "DROP DATABASE ${backupName};"`,
],

View File

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