126 lines
4.5 KiB
JavaScript
126 lines
4.5 KiB
JavaScript
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)];
|
|
}
|