/** 任务的全部状态;消费方 switch 必须穷举。与 DB CHECK `ck_vk_tasks_status` 一致。 */ export const TASK_STATUSES = Object.freeze([ 'pending_approval', 'open', 'claimed', 'in_contest', 'submitted', 'reviewing', 'rework', 'disputed', 'settled', 'cancelled', 'refunded', ]) export const TASK_TEMPLATES = Object.freeze(['piecework', 'contest']) /** * @typedef {'pending_approval'|'open'|'claimed'|'in_contest'|'submitted'|'reviewing'|'rework'|'disputed'|'settled'|'cancelled'|'refunded'} TaskStatus * * @typedef {{ * id: string, * guild_id: string, * publisher_agent_id: string, * template: 'piecework'|'contest', * title: string, * spec: string, * reward: number, * rarity: number, * deadline_at: Date, * status: TaskStatus, * client_request_key: string, * holder_agent_id: string|null, * next_checkpoint_due_at: Date|null, * missed_checkpoints: number, * rework_count: number, * review_due_at: Date|null, * settled_at: Date|null, * created_at: Date, * updated_at: Date|null, * }} Task */ /** * DB 行 → Task 的唯一构造点。按状态保证必有字段,非法即 throw;下游读取零判空。 * @param {Record} row * @returns {Task} */ export function taskFromRow(row) { const id = requiredString(row.id, 'id', row.id) if (!TASK_TEMPLATES.includes(row.template)) throw new Error(`vk_tasks.template invalid: ${row.template}, id: ${id}`) const holder = row.holder_agent_id === null ? null : requiredString(row.holder_agent_id, 'holder_agent_id', id) const nextCheckpointDue = nullableDate(row.next_checkpoint_due_at, 'next_checkpoint_due_at', id) const reviewDue = nullableDate(row.review_due_at, 'review_due_at', id) const settledAt = nullableDate(row.settled_at, 'settled_at', id) switch (row.status) { case 'claimed': requirePiecework(row, id) if (holder === null) throw new Error(`vk_tasks.holder_agent_id missing for claimed, id: ${id}`) if (nextCheckpointDue === null) throw new Error(`vk_tasks.next_checkpoint_due_at missing for claimed, id: ${id}`) break case 'submitted': requirePiecework(row, id) if (holder === null) throw new Error(`vk_tasks.holder_agent_id missing for submitted, id: ${id}`) if (reviewDue === null) throw new Error(`vk_tasks.review_due_at missing for submitted, id: ${id}`) break case 'rework': requirePiecework(row, id) if (holder === null) throw new Error(`vk_tasks.holder_agent_id missing for rework, id: ${id}`) break case 'in_contest': requireContest(row, id) break case 'reviewing': requireContest(row, id) if (reviewDue === null) throw new Error(`vk_tasks.review_due_at missing for reviewing, id: ${id}`) break case 'settled': if (settledAt === null) throw new Error(`vk_tasks.settled_at missing for settled, id: ${id}`) break case 'pending_approval': case 'open': case 'disputed': case 'cancelled': case 'refunded': break default: throw new Error(`Unhandled status: ${row.status}, id: ${id}`) } if (row.template === 'contest' && holder !== null) throw new Error(`vk_tasks contest has holder, id: ${id}`) return { id, guild_id: requiredString(row.guild_id, 'guild_id', id), publisher_agent_id: requiredString(row.publisher_agent_id, 'publisher_agent_id', id), template: row.template, title: requiredString(row.title, 'title', id), spec: requiredString(row.spec, 'spec', id), reward: integerInRange(row.reward, 1, Number.MAX_SAFE_INTEGER, 'reward', id), rarity: integerInRange(row.rarity, 1, 10, 'rarity', id), deadline_at: requiredDate(row.deadline_at, 'deadline_at', id), status: row.status, client_request_key: requiredString(row.client_request_key, 'client_request_key', id), holder_agent_id: holder, next_checkpoint_due_at: nextCheckpointDue, missed_checkpoints: integerInRange(row.missed_checkpoints, 0, 1, 'missed_checkpoints', id), rework_count: integerInRange(row.rework_count, 0, 2, 'rework_count', id), review_due_at: reviewDue, settled_at: settledAt, created_at: requiredDate(row.created_at, 'created_at', id), updated_at: nullableDate(row.updated_at, 'updated_at', id), } } /** * @param {Record} row * @param {string} id */ function requirePiecework(row, id) { if (row.template !== 'piecework') throw new Error(`vk_tasks status ${row.status} requires piecework, id: ${id}`) } /** * @param {Record} row * @param {string} id */ function requireContest(row, id) { if (row.template !== 'contest') throw new Error(`vk_tasks status ${row.status} requires contest, id: ${id}`) } /** * @param {unknown} value * @param {string} field * @param {unknown} id * @returns {string} */ function requiredString(value, field, id) { if (typeof value === 'string' && value.length > 0) return value throw new Error(`vk_tasks.${field} invalid, id: ${String(id)}`) } /** * @param {unknown} value * @param {string} field * @param {string} id * @returns {Date} */ function requiredDate(value, field, id) { if (value instanceof Date) return value throw new Error(`vk_tasks.${field} invalid, id: ${id}`) } /** * @param {unknown} value * @param {string} field * @param {string} id * @returns {Date|null} */ function nullableDate(value, field, id) { return value === null ? null : requiredDate(value, field, id) } /** * @param {unknown} value * @param {number} min * @param {number} max * @param {string} field * @param {string} id * @returns {number} */ function integerInRange(value, min, max, field, id) { if (Number.isSafeInteger(value) && /** @type {number} */ (value) >= min && /** @type {number} */ (value) <= max) return /** @type {number} */ (value) throw new Error(`vk_tasks.${field} invalid: ${String(value)}, id: ${id}`) }