import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; const allowedKeys = new Set([ 'type', 'title', 'background', 'acceptance_criteria', 'priority', 'environment', 'reproduction_steps', 'actual_result', 'expected_result', 'impact_scope', 'desired_at', 'references', 'attachments', ]); export async function readStdin() { let input = ''; process.stdin.setEncoding('utf8'); for await (const chunk of process.stdin) input += chunk; if (!input.trim()) throw new Error('请通过 stdin 传入 JSON'); return JSON.parse(input); } export function normalize(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('输入必须是 JSON 对象'); const unknown = Object.keys(input).filter((key) => !allowedKeys.has(key)); if (unknown.length) throw new Error(`存在禁止或未知字段: ${unknown.join(', ')}`); const value = { type: requiredEnum(input.type, '类型', ['需求', 'Bug']), title: requiredText(input.title, '标题'), background: requiredText(input.background, '背景与目标'), acceptance_criteria: requiredList(input.acceptance_criteria, '验收标准'), priority: requiredEnum(input.priority, '优先级', ['P0', 'P1', 'P2', 'P3']), impact_scope: optionalEnumList(input.impact_scope, '影响范围', ['前端', '后端', '全栈', '部署']), attachments: optionalList(input.attachments, '附件'), }; for (const [key, label] of [ ['environment', '发生环境'], ['actual_result', '实际结果'], ['expected_result', '期望结果'], ['desired_at', '期望完成时间'], ['references', '参考资料'], ]) { if (input[key] !== undefined) value[key] = requiredText(input[key], label); } if (input.reproduction_steps !== undefined) value.reproduction_steps = requiredList(input.reproduction_steps, '复现步骤'); if (value.type === 'Bug') { for (const [key, label] of [ ['environment', '发生环境'], ['actual_result', '实际结果'], ['expected_result', '期望结果'], ]) if (!value[key]) throw new Error(`Bug 缺少${label}`); if (!value.reproduction_steps?.length) throw new Error('Bug 缺少复现步骤'); } return value; } export function canonical(value) { if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`; if (value && typeof value === 'object') return `{${Object.keys(value) .sort() .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`) .join(',')}}`; return JSON.stringify(value); } export function tokens(value) { const digest = createHash('sha256').update(canonical(value)).digest('hex'); return { confirmation_token: digest, submission_key: `intake-${digest.slice(0, 24)}` }; } export function preview(value) { return { '类型': value.type, '标题': value.title, '背景与目标': value.background, '验收标准': value.acceptance_criteria, '优先级': value.priority, ...(value.environment ? { '发生环境': value.environment } : {}), ...(value.reproduction_steps ? { '复现步骤': value.reproduction_steps } : {}), ...(value.actual_result ? { '实际结果': value.actual_result } : {}), ...(value.expected_result ? { '期望结果': value.expected_result } : {}), ...(value.impact_scope.length ? { '影响范围': value.impact_scope } : {}), ...(value.attachments.length ? { '附件': value.attachments } : {}), }; } export async function readJson(path, fallback) { try { return JSON.parse(await readFile(path, 'utf8')); } catch (error) { if (error?.code === 'ENOENT') return fallback; throw error; } } function requiredText(value, label) { if (typeof value !== 'string' || !value.trim()) throw new Error(`${label}缺失`); return value.trim(); } function requiredList(value, label) { if (!Array.isArray(value) || !value.length) throw new Error(`${label}缺失`); return value.map((item) => requiredText(item, label)); } function optionalList(value, label) { return value === undefined || (Array.isArray(value) && value.length === 0) ? [] : requiredList(value, label); } function requiredEnum(value, label, options) { if (!options.includes(value)) throw new Error(`${label}必须是: ${options.join(' / ')}`); return value; } function optionalEnumList(value, label, options) { const list = value === undefined ? [] : requiredList(value, label); for (const item of list) requiredEnum(item, label, options); return [...new Set(list)]; }