diff --git a/docs/branch-disposition.md b/docs/branch-disposition.md index 3680a1f..f06449c 100644 --- a/docs/branch-disposition.md +++ b/docs/branch-disposition.md @@ -3,6 +3,31 @@ 本文件记录已经完成迁移、但仍可能因为 Git 拓扑或遗留 worktree 被误判为“尚未进入 `main`”的分支。 它是分支复用、合并、cherry-pick 和清理前的必查清单。 +## `feature/production-inspection-fixes` + +**状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。** + +审计日期:2026-08-16 +分支 HEAD:`843e4d10` +`origin/main` 对应提交:`843e4d10` + +### 原始用途 + +103 生产巡检后续修复:定时任务去重、orphan 交付契约 reconcile(HTML 缺失 mark failed)、MindSpace remote 超时 30s、103 数据/Goosed harness 修复脚本。 + +### 验证摘要 + +- `node --test scheduled-task-executor.test.mjs scheduled-task-service.test.mjs`:29 passed +- 103 快速发布 `memind-portal-runtime-20260816-085224-843e4d10`,Portal 8081 健康 +- 103 数据修复:取消唐用户 4 个重复/废弃定时任务,保留 5:30 新闻 + 8:00 天气各 1 个 +- Goosed `run-goosed-native.sh` PATH 已修补,18007–18014 plist PATH 已更新 +- memind_adm LaunchAgent 脚本已部署,8085/5174 健康 + +### 最终处置 + +- 保留本地分支名用于审计追溯。 +- 不要从该分支继续开发、merge、cherry-pick 或构建 runtime/artifact。 + ## `feature/scheduled-task-page-delivery-fix` **状态:禁止再次引用。改动已提交并进入 `origin/main`,该分支保留仅用于只读追溯,不是待合并开发分支。** diff --git a/scripts/repair-production-inspection-103-standalone.mjs b/scripts/repair-production-inspection-103-standalone.mjs new file mode 100644 index 0000000..a12641b --- /dev/null +++ b/scripts/repair-production-inspection-103-standalone.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * Standalone 103 production repair (no heavy module imports). + * Dry-run by default; pass --apply to mutate database rows. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db'; +const TASKS_TO_CANCEL = [ + '781a77e5-a787-4f37-b864-e21d7e918855', + '9df3f89a-3a06-434d-8b4c-17b1aeb8911f', + '7f953d5f-6fc3-4191-b52a-bffcef48e77f', + '9b537fe8-eea2-4e39-8bcb-e8f169986327', +]; + +function normalizeDeliveryRelativePath(value) { + const raw = String(value ?? '').trim().replace(/^\/+/, ''); + if (!raw) return ''; + return raw.startsWith('public/') ? raw : `public/${raw}`; +} + +async function loadPool() { + const databaseUrl = process.env.DATABASE_URL; + if (!databaseUrl) throw new Error('缺少 DATABASE_URL'); + const { default: mysql } = await import('mysql2/promise'); + return mysql.createPool({ uri: databaseUrl, connectionLimit: 2 }); +} + +async function markContractFailed(pool, { userId, relativePath, reason }) { + const now = Date.now(); + await pool.query( + `UPDATE h5_page_delivery_contracts + SET status = 'failed', updated_at = ?, last_error = ? + WHERE user_id = ? AND workspace_relative_path = ? AND status = 'preparing'`, + [now, reason, userId, relativePath], + ); +} + +async function markContractReady(pool, { userId, relativePath }) { + const now = Date.now(); + await pool.query( + `UPDATE h5_page_delivery_contracts + SET status = 'ready', updated_at = ?, last_error = NULL + WHERE user_id = ? AND workspace_relative_path = ? AND status = 'preparing'`, + [now, userId, relativePath], + ); +} + +async function reconcileStuckContracts(pool, h5Root, apply, limit = 50) { + const [rows] = await pool.query( + `SELECT user_id, workspace_relative_path + FROM h5_page_delivery_contracts + WHERE status = 'preparing' AND data_mode = 'static' + ORDER BY updated_at ASC + LIMIT ?`, + [limit], + ); + const summary = []; + for (const row of rows ?? []) { + const relativePath = normalizeDeliveryRelativePath(row.workspace_relative_path); + if (!relativePath) continue; + const filePath = path.join(h5Root, 'MindSpace', row.user_id, relativePath); + const exists = fs.existsSync(filePath); + const action = exists ? 'ready' : 'failed'; + summary.push({ userId: row.user_id, relativePath, fileExists: exists, action: apply ? action : `would_${action}` }); + if (apply) { + if (exists) { + await markContractReady(pool, { userId: row.user_id, relativePath }); + } else { + await markContractFailed(pool, { + userId: row.user_id, + relativePath, + reason: 'reconcile: static html missing', + }); + } + } + } + return summary; +} + +async function main() { + const apply = process.argv.includes('--apply'); + const h5Root = process.env.H5_ROOT || '/Users/john/Project/Memind'; + const pool = await loadPool(); + const now = Date.now(); + const actions = []; + + for (const taskId of TASKS_TO_CANCEL) { + const [rows] = await pool.query( + `SELECT id, user_id, title, status, hour, minute + FROM h5_scheduled_tasks WHERE id = ? LIMIT 1`, + [taskId], + ); + const row = rows?.[0]; + if (!row) { + actions.push({ taskId, action: 'skip', reason: 'not_found' }); + continue; + } + if (row.status === 'cancelled') { + actions.push({ taskId, action: 'skip', reason: 'already_cancelled', title: row.title }); + continue; + } + actions.push({ + taskId, + action: apply ? 'cancel' : 'would_cancel', + title: row.title, + status: row.status, + schedule: `${row.hour ?? '?'}:${String(row.minute ?? 0).padStart(2, '0')}`, + }); + if (apply) { + await pool.query( + `UPDATE h5_scheduled_tasks + SET status = 'cancelled', updated_at = ?, last_error = NULL + WHERE id = ? AND user_id = ?`, + [now, taskId, TANG_USER_ID], + ); + } + } + + const [activeWeather] = await pool.query( + `SELECT id, title, hour, minute, status, last_run_at + FROM h5_scheduled_tasks + WHERE user_id = ? AND title LIKE '%天气预报%' AND status = 'active' + ORDER BY hour, created_at`, + [TANG_USER_ID], + ); + + const reconcileSummary = await reconcileStuckContracts(pool, h5Root, apply); + + console.log(JSON.stringify({ + mode: apply ? 'apply' : 'dry-run', + cancelledTasks: actions, + remainingActiveWeatherTasks: activeWeather, + deliveryContracts: reconcileSummary, + }, null, 2)); + + await pool.end(); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});