feat: 接入Obsidian上下文闭环工具

This commit is contained in:
sunlei 2026-06-08 11:19:56 +08:00
parent fe5fab3479
commit 1eec05f885
15 changed files with 1717 additions and 12 deletions

View File

@ -5,6 +5,7 @@
## 能力边界
- 读取 KT 工作区紧凑上下文项目清单、硬性规则、skill registry、标准 skill 包索引、历史关键词、最近任务记录和详细文档入口;需要全量文档时显式开启。
- 读取 Obsidian 索引上下文:从 `docs/obsidian` 的总入口、模块索引、文档矩阵、工作流关系和 Canvas 摘要中按模块或关键词定位文档,并返回 `codeAnchors` / `ruleAnchors` 锚定到现有项目别名、源码路径、相关规则文档和后续上下文工具。
- 检查子项目环境Git/SVN、包管理器、Node 版本、env 文件、Git 状态。
- 生成防偏差工作包:把开工前检查、禁止项、风险扫描、验证计划汇总成一份可执行清单。
- 生成验证建议按后端、前端、样式、页面、部署、MCP 等变更类型给出轻量验证命令。
@ -15,11 +16,12 @@
- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。
- 生成业务链路测试计划:固化 Admin 登录、博客 CRUD、QQBot 扫码/自动回复、更新登录 SSE、FFLogs 命令、系统日志可视化、Web/Playground 回跳。
- 生成卡点固化记录:把超时、卡进程、远程命令误写、重复失败整理成“问题点 / 稳定解法 / 后续入口 / 验证证据”,避免原样重试。
- 生成改动文档同步计划:按变更文件自动提示需要同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 入口。
- 生成多仓库提交/推送计划:按仓库分组、建议提交信息、列出提交和推送前检查。
- 生成远程只读健康检查和数据库同步安全向导:覆盖飞牛 NAS 服务探测、GTID、备份和行数校验。
- 生成专项组件工作流KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger、FnosK8s 的防踩坑清单和验证点。
- 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。
- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留。
- 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留CLI 默认 dry-run真实清理必须显式传 `--execute`
- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。
- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。
- 生成或写入 `TASKS.md` 最近记录:默认 `dryRun=true`,确认后再落盘。
@ -32,8 +34,12 @@ cd D:/MyFiles/KT/mcp/ktWorkflow
pnpm install
pnpm run typecheck
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 cleanup-history -- --dry-run
pnpm run cleanup-history -- --execute
pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
```
@ -80,6 +86,8 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 |
| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 |
| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、问题记录、稳定解法和 ktWorkflow 升级 |
| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理、问题固化、ktWorkflow 升级和全局 review |
| `kt_change_doc_sync` | 根据改动路径生成 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 文档同步清单 |
| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 |
| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 |
| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 |
@ -87,6 +95,9 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 |
| `kt_db_sync_plan` | 生成数据库同步安全向导 |
| `kt_component_workflow` | 输出专项组件/链路防踩坑工作流,包含 FnosK8s |
| `kt_obsidian_context` | 读取 Obsidian 索引上下文,支持按 `module` / `query` 返回模块页、文档矩阵、Canvas 摘要、代码锚点和相关规则文档 |
| `kt_obsidian_validate` | 只读校验 Obsidian JSON、Canvas、Base、Wiki 链接、Markdown 相对链接和旧引用 |
| `kt_obsidian_sync` | 审计 Obsidian 工作流入口、书签、核心插件、忽略目录和校验结果 |
| `kt_append_task_record` | 预览或写入 `TASKS.md` 最近记录 |
| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message |
@ -96,6 +107,10 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
| --- | --- |
| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 |
| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告;确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
| `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 |
| `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 升级缺口。 |
## 项目别名
@ -125,10 +140,13 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
## 使用建议
- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context``kt_inspect_project`。`kt_read_context` 默认紧凑输出,只有确实需要完整 `TASKS.md` 时再传 `includeFullDocs=true`
- 需要按知识图谱定位上下文时先调用 `kt_obsidian_context`,例如 `module=Admin``query=BangDream`Obsidian 只做文档索引,返回的 `ruleAnchors` 用来读取相关规则文档,`codeAnchors` 必须继续搭配 `kt_inspect_project`、`kt_risk_scan`、`kt_suggest_verification` 锚定真实代码上下文。
- 多项目联动时先调用 `kt_inspect_all_projects`
- 要收尾时调用 `kt_finish_task`,默认只生成计划和 review需要执行验证时显式传 `runValidation=true`
- 大方向结束前调用 `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_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` 或补成脚本,再继续。
@ -141,6 +159,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 -- --keep=3`,让 `.kt-workspace` 历史产物只保留最近 3 轮。
- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。
- 更新 Obsidian 图谱、模块页、文档矩阵或 `.obsidian` 配置后运行 `kt_obsidian_validate` / `pnpm run obsidian-validate`;整理工作流入口时再跑 `kt_obsidian_sync`
- 改完文件后用 `kt_append_task_record``dryRun` 预览记录,再决定是否写入。
- 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。

View File

@ -8,6 +8,9 @@
"admin-login": "node --import ./node_modules/tsx/dist/loader.mjs scripts/admin-login-smoke.ts",
"cleanup-history": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --cleanup-history",
"global-review": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --global-review",
"obsidian-context": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-context",
"obsidian-sync": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-sync",
"obsidian-validate": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --obsidian-validate",
"self-test": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts --self-test",
"start": "node --import ./node_modules/tsx/dist/loader.mjs src/server.ts",
"typecheck": "tsc --noEmit",

View File

@ -1,4 +1,11 @@
import type { GlobalCodeReviewInput, WorkstreamCloseoutInput, WorkstreamReusablePattern } from '../types.js';
import type {
GlobalCodeReviewInput,
ObsidianContextInput,
ObsidianSyncInput,
ObsidianValidateInput,
WorkstreamCloseoutInput,
WorkstreamReusablePattern,
} from '../types.js';
import { workstreamReusablePatternValues } from './constants.js';
function parseContentScanMode(argv: string[]): 'all' | 'changed' {
@ -70,3 +77,31 @@ export function parseWorkstreamCloseoutCliArgs(argv: string[]): WorkstreamCloseo
verificationEvidence: readOptionList(argv, ['--verification', '--verification-evidence']),
};
}
export function parseObsidianCliArgs(argv: string[]): ObsidianValidateInput {
return {
checkLayout: !argv.includes('--no-layout'),
failOnWarnings: argv.includes('--fail-on-warnings'),
includeMarkdownLinks: !argv.includes('--no-markdown-links'),
includeStaleReferences: !argv.includes('--no-stale-references'),
};
}
export function parseObsidianSyncCliArgs(argv: string[]): ObsidianSyncInput {
return {
...parseObsidianCliArgs(argv),
includeValidation: !argv.includes('--no-validation'),
};
}
export function parseObsidianContextCliArgs(argv: string[]): ObsidianContextInput {
const maxDocuments = Number(readOption(argv, ['--max-documents', '--maxDocuments']));
return {
includeCanvas: !argv.includes('--no-canvas'),
includeContent: !argv.includes('--no-content'),
maxDocuments: Number.isFinite(maxDocuments) ? maxDocuments : undefined,
module: readOption(argv, ['--module']),
query: readOption(argv, ['--query']),
};
}

View File

@ -64,6 +64,8 @@ export const registeredToolNames = [
'kt_global_code_review',
'kt_finish_task',
'kt_workstream_closeout',
'kt_workflow_loop_audit',
'kt_change_doc_sync',
'kt_commit_plan',
'kt_push_plan',
'kt_business_test_plan',
@ -71,6 +73,9 @@ export const registeredToolNames = [
'kt_remote_health_check',
'kt_db_sync_plan',
'kt_component_workflow',
'kt_obsidian_context',
'kt_obsidian_validate',
'kt_obsidian_sync',
'kt_append_task_record',
'kt_commit_checklist',
] as const;
@ -82,4 +87,7 @@ export const defaultHistoryRoots = [
'.kt-workspace/playwright-mcp',
'.kt-workspace/db-sync',
'.kt-workspace/tmp',
'.kt-workspace/logs',
'.kt-workspace/deploy',
'.kt-workspace',
] as const;

View File

@ -8,11 +8,14 @@ import { createCleanupProcessPlan, cleanupHistoryArtifacts } from './tools/clean
import { buildEnvPolicy, scanTaskRisk } from './tools/envRisk.js';
import { buildGuardrails } from './tools/guardrails.js';
import { inspectAllProjects, inspectProject } from './tools/inspect.js';
import { buildWorkflowLoopAudit } from './tools/loop.js';
import { readObsidianContext, syncObsidianWorkflow, validateObsidianVault } from './tools/obsidian.js';
import { buildGlobalCodeReview, defaultReviewProjects } from './tools/review.js';
import { appendTaskRecord, createCommitChecklist, prepareTask, readWorkflowContext } from './tools/task.js';
import { buildBusinessTestPlan, createApiTestPlan, createPageTestCase } from './tools/testing.js';
import { buildVerificationPlan } from './tools/verification.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { buildChangeDocSync } from './tools/docSync.js';
import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js';
import { workstreamReusablePatternValues } from './core/constants.js';
export function response(data: unknown): McpTextResponse {
@ -156,7 +159,7 @@ export function registerTools(server: McpServer): void {
server.registerTool(
'kt_cleanup_history',
{
description: '清理 KT 运行态历史产物。默认 dryRun=true只预览执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮,.kt-workspace/test-artifacts/_templates 永久保留。',
description: '清理 KT 运行态历史产物。默认 dryRun=true只预览执行时按 LastWriteTime 每个 .kt-workspace 历史目录仅保留最近 3 轮,template/_templates 等模板目录永久保留。',
inputSchema: {
dryRun: z.boolean().default(true),
keep: z.number().int().min(0).max(50).default(3),
@ -259,6 +262,46 @@ export function registerTools(server: McpServer): void {
async (input) => response(buildWorkstreamCloseout(input)),
);
server.registerTool(
'kt_workflow_loop_audit',
{
description: '审计 KT 自动化闭环上下文锚定、测试用例、验证证据、历史清理、问题固化、ktWorkflow 升级和全局 review 是否齐备。',
inputSchema: {
changedProjects: z.array(z.string()).default([]),
cleanupExecuted: z.boolean().default(false),
cleanupPreviewDeleted: z.number().int().min(0).default(0),
docSyncEvidence: z.array(z.string()).default([]),
failureRecords: z.array(z.string()).default([]),
ktWorkflowUpdated: z.boolean().default(false),
problemRecords: z.array(z.string()).default([]),
requireCompletion: z.boolean().default(true),
reusablePattern: z.boolean().default(false),
reviewEvidence: z.array(z.string()).default([]),
stableSolutions: z.array(z.string()).default([]),
stage: z.enum(['failure', 'finish', 'start', 'test']).default('finish'),
taskTitle: z.string().optional(),
testCases: z.array(z.string()).default([]),
verificationEvidence: z.array(z.string()).default([]),
},
title: 'KT Workflow Loop Audit',
},
async (input) => response(buildWorkflowLoopAudit(input)),
);
server.registerTool(
'kt_change_doc_sync',
{
description: '根据代码改动路径生成必须检查/同步的 README、API、AGENTS、docs、Obsidian、skill 和 ktWorkflow 文档入口。',
inputSchema: {
changedFiles: z.array(z.string()).default([]),
project: z.string().default('root'),
taskType: z.enum(taskTypeValues).optional(),
},
title: 'KT Change Doc Sync',
},
async (input) => response(await buildChangeDocSync(input)),
);
server.registerTool(
'kt_commit_plan',
{
@ -387,6 +430,53 @@ export function registerTools(server: McpServer): void {
async (input) => response(buildComponentWorkflow(input)),
);
server.registerTool(
'kt_obsidian_context',
{
description: '读取 KT Obsidian 索引上下文:总入口、模块索引、文档矩阵、工作流关系,并可按 module/query 定向返回相关模块页摘要。',
inputSchema: {
includeCanvas: z.boolean().default(true),
includeContent: z.boolean().default(true),
maxDocuments: z.number().int().min(1).max(30).default(8),
module: z.string().optional(),
query: z.string().optional(),
},
title: 'KT Obsidian Context',
},
async (input) => response(readObsidianContext(input)),
);
server.registerTool(
'kt_obsidian_validate',
{
description: '只读校验 KT Obsidian vaultJSON、Canvas、Base、Wiki 链接、Markdown 相对链接和旧引用。',
inputSchema: {
checkLayout: z.boolean().default(true),
failOnWarnings: z.boolean().default(false),
includeMarkdownLinks: z.boolean().default(true),
includeStaleReferences: z.boolean().default(true),
},
title: 'KT Obsidian Validate',
},
async (input) => response(validateObsidianVault(input)),
);
server.registerTool(
'kt_obsidian_sync',
{
description: '审计 KT Obsidian 工作流入口是否连通:必备文档、书签、核心插件、忽略目录和校验结果。',
inputSchema: {
checkLayout: z.boolean().default(true),
failOnWarnings: z.boolean().default(false),
includeMarkdownLinks: z.boolean().default(true),
includeStaleReferences: z.boolean().default(true),
includeValidation: z.boolean().default(true),
},
title: 'KT Obsidian Sync',
},
async (input) => response(syncObsidianWorkflow(input)),
);
server.registerTool(
'kt_append_task_record',
{

View File

@ -4,9 +4,12 @@ import path from 'node:path';
import { parseGlobalReviewCliArgs } from './core/cli.js';
import { registeredToolNames } from './core/constants.js';
import { buildBlockerResolution } from './tools/blocker.js';
import { buildChangeDocSync } from './tools/docSync.js';
import { resolveInsideRoot } from './core/workspace.js';
import { buildGuardrails } from './tools/guardrails.js';
import { cleanupHistoryArtifacts } from './tools/cleanup.js';
import { readObsidianContext, syncObsidianWorkflow, validateObsidianVault } from './tools/obsidian.js';
import { cleanupHistoryArtifacts, parseCliCleanupArgs } from './tools/cleanup.js';
import { buildWorkflowLoopAudit } from './tools/loop.js';
import { inspectProject } from './tools/inspect.js';
import { prepareTask, readWorkflowContext } from './tools/task.js';
import { buildGlobalCodeReview, isBenignCredentialReviewValue } from './tools/review.js';
@ -71,6 +74,84 @@ export async function runSelfTest(): Promise<void> {
throw new Error('workstream closeout complete self-check failed');
}
const obsidianContext = readObsidianContext({
maxDocuments: 6,
module: 'ktWorkflow',
});
if ((obsidianContext.summary as { returnedDocuments?: number }).returnedDocuments === 0) {
throw new Error('obsidian context self-check failed');
}
if ((obsidianContext.summary as { codeAnchors?: number }).codeAnchors === 0) {
throw new Error('obsidian code anchor self-check failed');
}
const obsidianValidation = validateObsidianVault({
includeStaleReferences: false,
});
if ((obsidianValidation.summary as { errors?: number }).errors !== 0) {
throw new Error('obsidian validation self-check failed');
}
const cleanupCliParser = {
defaultDryRun: parseCliCleanupArgs(['node', 'server', '--cleanup-history']).dryRun,
executeDryRun: parseCliCleanupArgs(['node', 'server', '--cleanup-history', '--execute']).dryRun,
};
if (!cleanupCliParser.defaultDryRun || cleanupCliParser.executeDryRun) {
throw new Error('cleanup CLI dry-run parser self-check failed');
}
const cleanupPreview = cleanupHistoryArtifacts({
dryRun: true,
keep: 3,
roots: ['.kt-workspace/test-artifacts', '.kt-workspace'],
});
const cleanupRoots = cleanupPreview.roots as Array<{
preservedNames: string[];
root: string;
}>;
const testArtifactsRoot = cleanupRoots.find((item) => item.root === '.kt-workspace/test-artifacts');
const workspaceHistoryRoot = cleanupRoots.find((item) => item.root === '.kt-workspace');
if (!testArtifactsRoot?.preservedNames.includes('_templates')) {
throw new Error('cleanup template preservation self-check failed');
}
if (!workspaceHistoryRoot?.preservedNames.includes('test-artifacts')) {
throw new Error('cleanup workspace root preservation self-check failed');
}
const docSyncBangDream = await buildChangeDocSync({
changedFiles: ['src/qqbot/plugins/bangDream/application/bangdream-application.service.ts'],
project: 'api',
});
const docSyncRequiredDocs = docSyncBangDream.requiredDocs as Array<{ path: string }>;
if (!docSyncRequiredDocs.some((item) => item.path === 'Node/kt-template-online-api/API.md')) {
throw new Error('doc sync API self-check failed');
}
const loopAuditComplete = buildWorkflowLoopAudit({
cleanupExecuted: true,
cleanupPreviewDeleted: 3,
docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'],
ktWorkflowUpdated: true,
problemRecords: ['测试历史清理流程未自动执行。'],
reusablePattern: true,
reviewEvidence: ['global-review findings=0'],
stableSolutions: ['cleanup-history 默认 dry-run显式 --execute 后复验 deleted=0。'],
taskTitle: '自动化闭环审计',
testCases: ['cleanup-history dry-run/execute 回归测试'],
verificationEvidence: ['typecheck/self-test/cleanup dry-run 通过'],
});
if ((loopAuditComplete.canReportComplete as boolean) !== true) {
throw new Error('workflow loop complete self-check failed');
}
const loopAuditBlocked = buildWorkflowLoopAudit({
cleanupPreviewDeleted: 1,
taskTitle: '缺证据闭环',
});
if ((loopAuditBlocked.canReportComplete as boolean) !== false) {
throw new Error('workflow loop blocked self-check failed');
}
const data = {
context: readWorkflowContext({ taskRecordCount: 2 }),
guardrails: buildGuardrails({
@ -78,6 +159,11 @@ export async function runSelfTest(): Promise<void> {
taskType: 'mcp',
userRequest: '扩展 KT 工作区 MCP 可复用能力',
}),
cleanupCliParser,
cleanupPreview,
docSyncBangDream,
loopAuditBlocked,
loopAuditComplete,
historyCleanup: cleanupHistoryArtifacts({
dryRun: true,
keep: 3,
@ -104,6 +190,11 @@ export async function runSelfTest(): Promise<void> {
}),
closeoutComplete,
closeoutNeedsUpgrade,
obsidianContext,
obsidianSync: syncObsidianWorkflow({
includeStaleReferences: false,
}),
obsidianValidation,
blockerResolution: buildBlockerResolution({
attempts: 2,
evidence: 'self-test',

View File

@ -2,9 +2,16 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { parseGlobalReviewCliArgs, parseWorkstreamCloseoutCliArgs } from './core/cli.js';
import {
parseGlobalReviewCliArgs,
parseObsidianContextCliArgs,
parseObsidianCliArgs,
parseObsidianSyncCliArgs,
parseWorkstreamCloseoutCliArgs,
} from './core/cli.js';
import { cleanupHistoryArtifacts, parseCliCleanupArgs } from './tools/cleanup.js';
import { buildWorkstreamCloseout } from './tools/closeout.js';
import { readObsidianContext, syncObsidianWorkflow, validateObsidianVault } from './tools/obsidian.js';
import { buildGlobalCodeReview } from './tools/review.js';
import { registerTools } from './registerTools.js';
import { runSelfTest } from './selfTest.js';
@ -19,6 +26,12 @@ if (process.argv.includes('--cleanup-history')) {
2,
),
);
} else if (process.argv.includes('--obsidian-validate')) {
console.log(JSON.stringify(validateObsidianVault(parseObsidianCliArgs(process.argv)), null, 2));
} else if (process.argv.includes('--obsidian-context')) {
console.log(JSON.stringify(readObsidianContext(parseObsidianContextCliArgs(process.argv)), null, 2));
} else if (process.argv.includes('--obsidian-sync')) {
console.log(JSON.stringify(syncObsidianWorkflow(parseObsidianSyncCliArgs(process.argv)), null, 2));
} else if (process.argv.includes('--self-test')) {
await runSelfTest();
} else if (process.argv.includes('--workstream-closeout')) {

View File

@ -36,6 +36,41 @@ interface HistoryEntry {
type: 'directory' | 'file';
}
const alwaysPreservedTemplateNames = new Set(['_template', '_templates', 'template', 'templates']);
const workspaceRootPreservedNames = new Set([
'backup',
'db-sync',
'deploy',
'external',
'logs',
'playwright-mcp',
'sources',
'test-artifacts',
'test-logs',
'tmp',
'verify',
]);
function isTemplateEntryName(name: string): boolean {
return alwaysPreservedTemplateNames.has(name.toLowerCase());
}
function buildPreservedNames(rootRelativePath: string, childNames: 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);
}
}
return preservedNames;
}
function listHistoryEntries(rootPath: string, preservedNames: Set<string>): HistoryEntry[] {
if (!existsSync(rootPath)) return [];
@ -91,9 +126,8 @@ export function cleanupHistoryArtifacts(input: CleanupHistoryInput = {}): Record
continue;
}
const preservedNames = new Set(
rootRelativePath.endsWith('test-artifacts') ? ['_templates'] : [],
);
const childNames = readdirSync(rootPath, { withFileTypes: true }).map((item) => item.name);
const preservedNames = buildPreservedNames(rootRelativePath, childNames);
const entries = listHistoryEntries(rootPath, preservedNames);
const preserved = entries.slice(0, keep);
const deleting = entries.slice(keep);
@ -127,7 +161,7 @@ export function parseCliCleanupArgs(args: string[]): CleanupHistoryInput {
const rootsArg = args.find((arg) => arg.startsWith('--roots='));
return {
dryRun: args.includes('--dry-run'),
dryRun: !(args.includes('--execute') || args.includes('--no-dry-run')),
keep: keepArg ? Number.parseInt(keepArg.split('=')[1], 10) : 3,
roots: rootsArg ? rootsArg.split('=')[1].split(',').filter(Boolean) : [...defaultHistoryRoots],
};

139
src/tools/docSync.ts Normal file
View File

@ -0,0 +1,139 @@
import { existsSync } from 'node:fs';
import path from 'node:path';
import type { ChangeDocSyncInput } from '../types.js';
import { detectRepoType, parseGitStatusFiles } from '../core/project.js';
import { tryExecFile } from '../core/exec.js';
import { resolveProject, toPosix, workspaceRoot } from '../core/workspace.js';
interface DocSyncRule {
docs: string[];
match: RegExp;
reason: string;
}
const docSyncRules: DocSyncRule[] = [
{
docs: ['Node/kt-template-online-api/README.md', 'Node/kt-template-online-api/API.md', 'docs/obsidian/modules/KT 模块 - API Backend.md'],
match: /^(src\/|Node\/kt-template-online-api\/src\/).*(controller|service|dto|entity|module|guard|interceptor|filter)|^Node\/kt-template-online-api\/src\//,
reason: 'API 接口、DTO/Entity、模块或服务能力变更需要同步后端 README/API 和模块索引。',
},
{
docs: ['Node/kt-template-online-api/API.md', 'docs/qqbot-nas-runtime.md', 'docs/obsidian/modules/KT 模块 - QQBot BangDream FFLogs.md'],
match: /qqbot|bangDream|fflogs|ff14Market|napcat/i,
reason: 'QQBot/BangDream/FFLogs/NapCat 链路变更,需要同步命令、运行态和 smoke 文档。',
},
{
docs: ['Vue/kt-template-admin/README.md', 'docs/obsidian/modules/KT 模块 - Admin.md'],
match: /Vue\/kt-template-admin\/|apps\/web-antdv-next\/src\/views\/|apps\/web-antdv-next\/src\/api\//,
reason: 'Admin 页面、API 客户端或业务视图变更,需要同步 Admin README 和模块页。',
},
{
docs: ['Vue/kt-template-admin/README.md', 'docs/obsidian/modules/KT 模块 - KtTable.md', 'skills/kt-component-specialist/SKILL.md'],
match: /ktTable|KtTable|components\/ktTable/i,
reason: 'KtTable 组件或迁移规则变更,需要同步专项模块和 skill。',
},
{
docs: ['Vue/kt-blog-web/README.md', 'docs/obsidian/modules/KT 模块 - Blog Web.md', 'skills/kt-blog-argon-workflow/SKILL.md'],
match: /Vue\/kt-blog-web\/|blog|argon/i,
reason: 'Blog/Argon 能力变更,需要同步博客 README、模块页和专项 skill。',
},
{
docs: ['Plugins/fnos-k8s-dashboard-fpk/README.md', 'Plugins/fnos-k8s-dashboard-fpk/AGENTS.md', 'docs/obsidian/modules/KT 模块 - Plugins.md'],
match: /fnos-k8s-dashboard|fnosK8s|fpk|Kubernetes/i,
reason: 'fnOS K8s 插件变更,需要同步插件 README/AGENTS 和 Obsidian 模块。',
},
{
docs: ['mcp/ktWorkflow/README.md', 'AGENTS.md', 'TASKS.md', 'docs/kt-testing-workflows.md', 'docs/obsidian/KT 工作流工程化.md'],
match: /mcp\/ktWorkflow|src\/tools\/|src\/registerTools|src\/core|workflow|cleanup|review|selfTest/i,
reason: 'ktWorkflow 或自动化规则变更,需要同步 README、根规则、测试流程和当前任务记录。',
},
{
docs: ['AGENTS.md', 'SKILLS.md', 'docs/kt-context-semantics.md', 'docs/obsidian/modules/KT 模块 - Root Governance.md'],
match: /AGENTS\.md|SKILLS\.md|skills\/.*\/SKILL\.md|docs\/kt-context-semantics\.md/,
reason: '上下文治理或 skill 规则变更,需要同步根治理文档和 Obsidian 模块。',
},
{
docs: ['docs/obsidian/KT 文档矩阵.md', 'docs/obsidian/KT Obsidian 工程化.md', 'docs/obsidian/KT 关系图谱.canvas'],
match: /\.obsidian|docs\/obsidian|\.canvas$|\.base$/,
reason: 'Obsidian vault、Canvas、Base 或文档矩阵变更,需要同步图谱和校验。',
},
{
docs: ['docs/kt-testing-workflows.md', 'mcp/ktWorkflow/README.md'],
match: /\.kt-workspace|test-artifacts|test-logs|verify|playwright|smoke|Jenkinsfile|k8s\/|deploy\//i,
reason: '测试、验证、发布或运行态产物流程变更,需要同步测试流程和 ktWorkflow 文档。',
},
];
async function inferChangedFiles(projectKey: string): Promise<string[]> {
const project = resolveProject(projectKey);
if (detectRepoType(project.path) !== 'git') return [];
const status = await tryExecFile('git', ['status', '--short'], project.path);
if (!status.ok) return [];
return parseGitStatusFiles(status.stdout).map((file) =>
project.alias && project.alias !== 'root'
? `${project.relativePath}/${file.replaceAll('\\', '/')}`
: file.replaceAll('\\', '/'),
);
}
function normalizeChangedFile(file: string, projectKey?: string): string {
const project = resolveProject(projectKey || 'root');
const normalized = file.replaceAll('\\', '/');
if (project.alias && project.alias !== 'root' && !normalized.startsWith(`${project.relativePath}/`)) {
return `${project.relativePath}/${normalized}`;
}
return normalized;
}
export async function buildChangeDocSync(input: ChangeDocSyncInput = {}): Promise<Record<string, unknown>> {
const projectKey = input.project || 'root';
const changedFiles = (input.changedFiles?.length ?? 0) > 0
? input.changedFiles!.map((file) => normalizeChangedFile(file, projectKey))
: await inferChangedFiles(projectKey);
const recommendations = docSyncRules
.map((rule) => {
const matchedFiles = changedFiles.filter((file) => rule.match.test(file));
return matchedFiles.length > 0
? {
docs: rule.docs.map((file) => ({
exists: existsSync(path.resolve(workspaceRoot, file)),
path: file,
})),
matchedFiles,
reason: rule.reason,
}
: null;
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
const requiredDocs = Array.from(
new Set(recommendations.flatMap((item) => item.docs.map((doc) => doc.path))),
).map((file) => ({
exists: existsSync(path.resolve(workspaceRoot, file)),
path: file,
}));
return {
changedFiles,
commands: [
'pnpm --dir mcp/ktWorkflow run obsidian-context -- --query <module>',
'pnpm --dir mcp/ktWorkflow run obsidian-validate',
'pnpm --dir mcp/ktWorkflow run global-review',
],
requiredDocs,
recommendations,
summary: {
changedFileCount: changedFiles.length,
recommendationCount: recommendations.length,
requiredDocCount: requiredDocs.length,
},
verification: [
'确认 requiredDocs 中需要更新的文档已同步,未更新时在最终说明原因。',
'涉及 Obsidian 索引时运行 obsidian-validate。',
'涉及 ktWorkflow/规则/测试流程时运行 self-test 和 global-review。',
`当前项目:${toPosix(resolveProject(projectKey).relativePath)}`,
],
};
}

150
src/tools/loop.ts Normal file
View File

@ -0,0 +1,150 @@
import type { WorkflowLoopAuditInput } from '../types.js';
interface LoopGate {
evidence: string[];
name: string;
required: boolean;
status: 'missing' | 'not-needed' | 'ok';
suggestion?: string;
}
function normalizeList(values?: string[]): string[] {
return (values || []).map((value) => value.trim()).filter(Boolean);
}
function buildRequiredGate(name: string, evidence: string[], suggestion: string): LoopGate {
return {
evidence,
name,
required: true,
status: evidence.length > 0 ? 'ok' : 'missing',
suggestion: evidence.length > 0 ? undefined : suggestion,
};
}
function buildBooleanGate(name: string, ok: boolean, evidence: string[], suggestion: string): LoopGate {
return {
evidence,
name,
required: true,
status: ok ? 'ok' : 'missing',
suggestion: ok ? undefined : suggestion,
};
}
export function buildWorkflowLoopAudit(input: WorkflowLoopAuditInput = {}): Record<string, unknown> {
const stage = input.stage || 'finish';
const requireCompletion = input.requireCompletion !== false && stage === 'finish';
const changedProjects = normalizeList(input.changedProjects);
const testCases = normalizeList(input.testCases);
const verificationEvidence = normalizeList(input.verificationEvidence);
const failureRecords = normalizeList(input.failureRecords);
const docSyncEvidence = normalizeList(input.docSyncEvidence);
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 hasFailure = failureRecords.length > 0;
const reusablePattern = input.reusablePattern === true;
const needsSolidification = hasFailure || reusablePattern;
const ktWorkflowUpdated = input.ktWorkflowUpdated === true;
const gates: LoopGate[] = [
buildRequiredGate('task target', [input.taskTitle || ''].filter(Boolean), '写清本轮任务目标。'),
buildRequiredGate('test case before test', testCases, '测试前先生成页面/业务/接口测试用例或验证计划。'),
buildRequiredGate('verification evidence', verificationEvidence, '补充真实验证证据;只有计划不算完成。'),
buildBooleanGate(
'history cleanup',
cleanupOk,
cleanupOk
? [
cleanupPreviewDeleted === 0
? 'cleanup-history dry-run deleted=0'
: 'cleanup-history executed after stale artifacts were found',
]
: [`cleanup-history dry-run deleted=${cleanupPreviewDeleted}`],
'执行 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 中需要更新的入口;无需更新时写明原因。'),
buildRequiredGate('global review evidence', reviewEvidence, '文件改动后跑 kt_global_code_review 或 pnpm run global-review。'),
];
if (needsSolidification) {
gates.push(
buildRequiredGate('problem record', problemRecords, '写入问题点,至少包含触发条件和证据。'),
buildRequiredGate('stable solution', stableSolutions, '写入稳定解法和后续入口。'),
buildBooleanGate(
'ktWorkflow upgrade',
ktWorkflowUpdated,
ktWorkflowUpdated ? ['ktWorkflow updated or audited as not needing executable upgrade'] : [],
'可复用问题必须沉淀到 AGENTS.md、skills、docs 或 mcp/ktWorkflow能自动化的要升级 ktWorkflow。',
),
);
} else {
gates.push({
evidence: ['无失败或可复用问题。'],
name: 'problem solidification',
required: false,
status: 'not-needed',
});
}
const missingItems = requireCompletion
? gates
.filter((gate) => gate.required && gate.status === 'missing')
.map((gate) => `${gate.name}: ${gate.suggestion}`)
: [];
return {
canReportComplete: missingItems.length === 0,
commands: [
'pnpm --dir mcp/ktWorkflow run obsidian-context -- --query <keyword>',
'pnpm --dir mcp/ktWorkflow run self-test',
'pnpm --dir mcp/ktWorkflow run cleanup-history -- --dry-run',
'pnpm --dir mcp/ktWorkflow run cleanup-history -- --execute',
'pnpm --dir mcp/ktWorkflow run global-review',
'pnpm --dir mcp/ktWorkflow run workstream-closeout -- --title "<目标>" --verification "<证据>" --problem "<问题记录>" --solution "<稳定解法>"',
],
gates,
loops: {
contextLoop: [
'读取 AGENTS.md / SKILLS.md / TASKS.md。',
'用 kt_obsidian_context 获取 ruleAnchors 和 codeAnchors。',
'用 kt_inspect_project / kt_risk_scan / kt_suggest_verification 锚定代码上下文。',
],
problemSolidificationLoop: [
'失败或主动修复后,先判断是否可复用。',
'可复用则记录问题点 / 稳定解法 / 后续入口 / 验证证据。',
'能自动化的升级 mcp/ktWorkflow规则类写 AGENTS.md长流程写 docs当前证据写 TASKS.md。',
'复跑 self-test / 相关脚本 / global-review。',
],
documentationSyncLoop: [
'收集 changedFiles。',
'运行 kt_change_doc_sync 生成 requiredDocs。',
'更新必要的 README/API/AGENTS/SKILLS/docs/Obsidian/skill/ktWorkflow 入口。',
'无法或无需更新时写明原因,并保留在 TASKS.md 或最终报告。',
],
testLoop: [
'测试前先写用例或验证计划。',
'按用例执行验证并保存证据到 .kt-workspace。',
'失败最多三轮,每轮必须改变一个可验证变量。',
'每轮结束运行 cleanup-history dry-run存在待删项则执行 --execute 并复验 deleted=0。',
],
},
missingItems,
stage,
summary: {
changedProjects,
cleanupPreviewDeleted,
docSyncEvidenceCount: docSyncEvidence.length,
failureCount: failureRecords.length,
problemRecordCount: problemRecords.length,
reusablePattern,
reviewEvidenceCount: reviewEvidence.length,
stableSolutionCount: stableSolutions.length,
testCaseCount: testCases.length,
verificationEvidenceCount: verificationEvidence.length,
},
};
}

1055
src/tools/obsidian.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -155,6 +155,26 @@ export function readWorkflowContext(input: WorkflowContextInput): Record<string,
tasks: includeFullDocs ? readText('TASKS.md') : undefined,
},
includeFullDocs,
obsidianIndex: {
note: 'Obsidian 只做文档/MOC 索引;代码上下文仍以 kt_inspect_project、kt_risk_scan、kt_suggest_verification 等工具锚定。',
tools: [
{
input: {
maxDocuments: 8,
},
name: 'kt_obsidian_context',
purpose: '按模块或关键词定位 docs/obsidian 索引,并返回对应 codeAnchors。',
},
{
input: {
includeGit: true,
project: '<codeAnchors.alias>',
},
name: 'kt_inspect_project',
purpose: '读取被 Obsidian 索引锚定的真实仓库状态和代码入口。',
},
],
},
projects,
workspaceRoot,
};

View File

@ -19,7 +19,7 @@ export function createPageTestCase(input: PageTestCaseInput): PageTestCaseResult
'关闭浏览器实例',
'清理本次启动的 Node/Vite 进程',
'保存截图和 result.json',
'调用 kt_cleanup_history仅保留最近 3 轮历史产物',
'调用 kt_cleanup_history dryRun=true 预览,再执行 dryRun=false 或 pnpm run cleanup-history -- --execute,仅保留最近 3 轮历史产物并保留模板目录',
],
entryUrl: input.entryUrl || '待填写',
maxRounds: 3,
@ -179,6 +179,7 @@ export function buildBusinessTestPlan(input: BusinessTestPlanInput = {}): Record
'业务测试先写用例,再按环境 -> 账号 -> 数据 -> 接口 -> 页面预检。',
'失败必须记录复现步骤、证据和初步归因。',
'同一用例最多三轮,第三轮仍失败则停止等待建议。',
'每轮结束后执行 kt_cleanup_history 预览;如存在待删历史产物,确认范围后执行清理,只保留最近 3 轮并保留模板目录。',
],
selectedFlows: selected.map(([name, plan]) => ({
name,

View File

@ -107,6 +107,8 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
dryRun: !(input.cleanupHistory ?? false),
keep: input.keepHistory ?? 3,
});
const cleanupDeleted = Array.isArray(cleanup.deleted) ? cleanup.deleted : [];
const cleanupPendingCount = cleanup.dryRun ? cleanupDeleted.length : 0;
return {
cleanup,
@ -123,7 +125,7 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
mode: input.runValidation ? 'validation_executed' : 'plan_only',
notes: [
'默认只生成收尾结果runValidation=true 时才执行验证命令。',
'cleanupHistory=true 时才真实清理历史产物,否则仅预览。',
'cleanupHistory=true 时才真实清理历史产物,否则仅预览;预览中 deleted>0 说明测试/验证历史还未满足最近 3 轮规则。',
'提交和推送仍需用户明确要求后执行。',
karpathyFinishNote,
],
@ -132,12 +134,14 @@ export async function buildFinishTask(input: FinishTaskInput = {}): Promise<Reco
stopConditions: [
'review.findings 存在 P1/P2 时先判定真实风险或工具误报;真实风险修代码,工具误报修 ktWorkflow再复跑 kt_global_code_review。',
'validationRuns 中任一 result.ok=false 时先处理失败命令。',
'cleanup.dryRun=true 且 cleanup.deleted 非空时,先执行 kt_cleanup_history dryRun=false 或 pnpm run cleanup-history -- --execute再报告测试/验证历史已合规。',
'发现真实 env、生产 Secret 或非本次范围文件时停止提交。',
karpathyStopCondition,
],
summary: {
changedProjectCount: changedSnapshots.length,
changedProjects: changedSnapshots.map((snapshot) => snapshot.project.relativePath),
cleanupPendingCount,
validationRunCount: validationRuns.length,
},
validationRuns,

View File

@ -262,6 +262,49 @@ export interface WorkstreamCloseoutInput {
verificationEvidence?: string[];
}
export interface ObsidianValidateInput {
checkLayout?: boolean;
failOnWarnings?: boolean;
includeMarkdownLinks?: boolean;
includeStaleReferences?: boolean;
}
export interface ObsidianSyncInput extends ObsidianValidateInput {
includeValidation?: boolean;
}
export interface ObsidianContextInput {
includeCanvas?: boolean;
includeContent?: boolean;
maxDocuments?: number;
module?: string;
query?: string;
}
export interface WorkflowLoopAuditInput {
changedProjects?: string[];
cleanupExecuted?: boolean;
cleanupPreviewDeleted?: number;
docSyncEvidence?: string[];
failureRecords?: string[];
ktWorkflowUpdated?: boolean;
problemRecords?: string[];
requireCompletion?: boolean;
reusablePattern?: boolean;
reviewEvidence?: string[];
stableSolutions?: string[];
stage?: 'failure' | 'finish' | 'start' | 'test';
taskTitle?: string;
testCases?: string[];
verificationEvidence?: string[];
}
export interface ChangeDocSyncInput {
changedFiles?: string[];
project?: string;
taskType?: TaskType;
}
export interface VerificationPlanResult {
changeType: TaskType;
commands: string[];