import { readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { OpencodeProjectStore } from '../opencode/project-store'; import { logger } from '../utils/logger'; import { WORKS_SUBMISSION_BINDING_FILE_NAME, WORKS_SUBMISSION_BINDING_SCHEMA_VERSION, type WorksSubmissionBindingRecord, } from '../../shared/works-submission-binding'; const LEGACY_PENDING_STATUSES = new Set([ 'armed', 'waiting_for_package', 'waiting_for_login', 'uploading', 'failed', ]); const LEGACY_RETIRED_MESSAGE = '旧版自动部署任务已停用,请在项目配置中点击“提交审核”重新提交。'; type ProjectStore = Pick; type SubmittedBindingInput = { appId: string; versionId: string; versionName: string; reviewStatus: string; zipSha256?: string; }; type LegacyBindingRecord = { schema_version: 1; project_id: string; status: string; requested_at: string; updated_at: string; app_id?: string; version_name?: string; zip_sha256?: string; version_id?: string; review_status?: string; }; function readString(value: unknown): string | null { return typeof value === 'string' && value.trim() ? value.trim() : null; } function parseCurrentRecord(value: unknown): WorksSubmissionBindingRecord | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const record = value as Record; if ( record.schema_version !== WORKS_SUBMISSION_BINDING_SCHEMA_VERSION || (record.status !== 'submitted' && record.status !== 'legacy_retired') ) return null; const projectId = readString(record.project_id); const requestedAt = readString(record.requested_at); const updatedAt = readString(record.updated_at); if (!projectId || !requestedAt || !updatedAt) return null; return record as WorksSubmissionBindingRecord; } function parseLegacyRecord(value: unknown): LegacyBindingRecord | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const record = value as Record; const projectId = readString(record.project_id); const status = readString(record.status); const requestedAt = readString(record.requested_at); const updatedAt = readString(record.updated_at); if (record.schema_version !== 1 || !projectId || !status || !requestedAt || !updatedAt) { return null; } return record as LegacyBindingRecord; } function migrateLegacyRecord(record: LegacyBindingRecord): WorksSubmissionBindingRecord { const now = new Date().toISOString(); if ( record.status === 'submitted' && readString(record.app_id) && readString(record.version_id) && readString(record.version_name) ) { return { schema_version: WORKS_SUBMISSION_BINDING_SCHEMA_VERSION, project_id: record.project_id, status: 'submitted', requested_at: record.requested_at, updated_at: now, app_id: record.app_id, version_id: record.version_id, version_name: record.version_name, ...(readString(record.review_status) ? { review_status: record.review_status } : {}), ...(readString(record.zip_sha256) ? { zip_sha256: record.zip_sha256 } : {}), }; } return { schema_version: WORKS_SUBMISSION_BINDING_SCHEMA_VERSION, project_id: record.project_id, status: 'legacy_retired', requested_at: record.requested_at, updated_at: now, error_code: 'LEGACY_AUTO_DEPLOY_RETIRED', message: LEGACY_PENDING_STATUSES.has(record.status) ? LEGACY_RETIRED_MESSAGE : '旧版部署记录已停用,请使用项目配置中的“提交审核”。', }; } async function persistRecord( projectPath: string, record: WorksSubmissionBindingRecord, ): Promise { await writeFile( join(projectPath, WORKS_SUBMISSION_BINDING_FILE_NAME), `${JSON.stringify(record, null, 2)}\n`, 'utf8', ); return record; } export async function readWorksSubmissionBinding( projectPath: string, ): Promise { let parsed: unknown; try { parsed = JSON.parse(await readFile( join(projectPath, WORKS_SUBMISSION_BINDING_FILE_NAME), 'utf8', )) as unknown; } catch { return null; } const current = parseCurrentRecord(parsed); if (current) return current; const legacy = parseLegacyRecord(parsed); if (!legacy) return null; return persistRecord(projectPath, migrateLegacyRecord(legacy)); } export function createWorksSubmissionBindingStore(projectStore: ProjectStore) { async function recordSubmitted( projectId: string, input: SubmittedBindingInput, ): Promise { const project = (await projectStore.listProjects()).find((item) => item.id === projectId); if (!project) throw new Error('Project not found'); const now = new Date().toISOString(); return persistRecord(project.path, { schema_version: WORKS_SUBMISSION_BINDING_SCHEMA_VERSION, project_id: project.id, status: 'submitted', requested_at: now, updated_at: now, app_id: input.appId, version_id: input.versionId, version_name: input.versionName, review_status: input.reviewStatus, ...(input.zipSha256 ? { zip_sha256: input.zipSha256 } : {}), }); } async function get(projectId: string): Promise { const project = (await projectStore.listProjects()).find((item) => item.id === projectId); return project ? readWorksSubmissionBinding(project.path) : null; } async function start(): Promise { for (const project of await projectStore.listProjects()) { try { await readWorksSubmissionBinding(project.path); } catch (error) { logger.warn(`[works-submission-binding] Failed to migrate ${project.id}`, error); } } } return { recordSubmitted, get, start }; }