ktworkflow-mcp/src/tools/task.ts

147 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { buildGuardrails } from './guardrails.js';
import { inspectProject } from './inspect.js';
import { scanTaskRisk } from './envRisk.js';
import { createPageTestCase } from './testing.js';
export async function prepareTask(input: PrepareTaskInput): Promise<Record<string, unknown>> {
const project = resolveProject(input.project);
const taskType = input.taskType || 'general';
const inspection = await inspectProject({
includeGit: input.includeGit !== false,
project: input.project,
});
const guardrails = buildGuardrails(input);
const riskScan = await scanTaskRisk({
changedFiles: input.changedFiles || [],
project: input.project,
});
const pageTestCase =
input.includePageTest || taskType === 'page'
? createPageTestCase({
entryUrl: input.entryUrl,
project: input.project,
title: input.userRequest || `${project.label} 页面级测试`,
})
: null;
return {
guardrails,
inspection,
nextSteps: [
'阅读 workPacket.readFirst 中的上下文文件。',
'按 guardrails.beforeEdit 做开工前检查。',
'按最小范围修改。',
'按 verification.commands 和 notes 做轻量验证。',
'有文件改动时更新 TASKS.md。',
'最终回复或提交前跑全局 review并处理全部 findings。',
],
pageTestCase,
project,
readFirst: [
'AGENTS.md',
'SKILLS.md',
'TASKS.md',
`${project.relativePath}/AGENTS.md`,
`${project.relativePath}/package.json`,
],
riskScan,
taskRecordDraft: buildTaskRecord({
content: input.userRequest || '待补充',
scope: [project.relativePath],
testCase: pageTestCase ? pageTestCase.title : '本次无页面测试或待补充。',
title: input.recordTitle || input.userRequest || `${project.label} 任务记录`,
verification: guardrails.guardrails.verification.commands.join('') || '待补充。',
}),
taskType,
userRequest: input.userRequest || '',
};
}
export function buildTaskRecord(input: TaskRecordInput): string {
const date = input.date || formatDateInShanghai();
const scope = (input.scope?.length ?? 0) > 0 ? input.scope!.join('、') : '待补充';
const content = input.content || '待补充';
const testCase = input.testCase || '本次无页面测试或待补充。';
const verification = input.verification || '待补充。';
return `### ${date}${input.title}\n\n- 范围:${scope}\n- 内容:${content}\n- 页面测试用例:${testCase}\n- 验证:${verification}\n`;
}
export function appendTaskRecord(input: TaskRecordInput): Record<string, unknown> {
const record = buildTaskRecord(input);
const tasksPath = resolveInsideRoot('TASKS.md');
const current = readFileSync(tasksPath, 'utf8');
const marker = '## 改动记录';
const markerIndex = current.indexOf(marker);
if (markerIndex === -1) {
throw new Error('TASKS.md does not contain ## 改动记录');
}
const insertAt = current.indexOf('\n', markerIndex);
const next = `${current.slice(0, insertAt + 1)}\n${record}\n${current.slice(insertAt + 1)}`;
if (!input.dryRun) {
writeFileSync(tasksPath, next, 'utf8');
}
return {
dryRun: input.dryRun,
record,
target: tasksPath,
};
}
export async function createCommitChecklist(input: CommitChecklistInput): Promise<Record<string, unknown>> {
const inspection = await inspectProject({
includeGit: true,
project: input.project,
});
const message = input.message || '';
const messageOk =
!message ||
/^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?: .*[-]/u.test(
message,
);
return {
checks: [
'确认只包含本次任务相关文件。',
'先运行对应轻量验证命令。',
'提交前已跑全局 review且 findings 已修复或明确记录为非阻塞风险。',
'每个独立 Git 仓库单独 commit。',
'没有用户明确要求时不 push。',
],
inspection,
message,
messageOk,
messageRule: '英文 type 前缀 + 中文描述,例如 feat: 功能性提交',
};
}
export function readWorkflowContext(input: WorkflowContextInput): Record<string, unknown> {
const projects = Object.keys(projectAliases).map((key) => {
const project = resolveProject(key);
return {
...project,
exists: existsSync(project.path),
repoType: existsSync(project.path) ? detectRepoType(project.path) : 'missing',
};
});
return {
docs: {
agents: readText('AGENTS.md'),
recentTasks: getRecentTaskRecords(input.taskRecordCount),
skills: readText('SKILLS.md'),
},
projects,
workspaceRoot,
};
}