#!/usr/bin/env node import {readFileSync} from 'node:fs'; import {randomUUID} from 'node:crypto'; const sql=readFileSync(new URL('./sql/lesson-records.sql',import.meta.url),'utf8'); if(!process.argv.includes('--apply')){console.log('上课记录迁移就绪:新增复习转交、审计和并发保护;不转交任何现有任务。使用 --apply 执行。');process.exit(0);} const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN||process.env.XIAOSHU_SESSION_TOKEN; if(!master||!token)throw Error('需要受保护环境中的主密钥和有效管理会话'); const app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''); async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:body?JSON.stringify(body):undefined});const d=await r.json();if(!r.ok||d.error)throw Error(d.error||'数据库请求失败');return d;} const sessions=(await parse('/classes/_Session?'+new URLSearchParams({where:JSON.stringify({sessionToken:token}),include:'user',limit:'2'}))).results; const s=sessions?.[0];if(sessions.length!==1||!['super-admin','ops-manager'].includes(s.user?.adminRoleKey)||s.user.isDisabled||s.user.isDeleted||new Date(s.expiresAt?.iso)${expires})return response.status(403).json({success:false});await request.user.fetch({useMasterKey:true});if(request.user.get('isDisabled')||request.user.get('isDeleted'))return response.status(403).json({success:false});const active=await Psql.one('SELECT count(*)::int n FROM "LegacyDirectRun" WHERE company=$1 AND status=$2 AND "leaseUntil">now()',[${JSON.stringify(co)},'running']);if(active.n)throw Error('夜间同步正在运行,暂缓迁移');await Psql.none(${JSON.stringify(sql)});return response.json({success:true});}catch(e){return response.status(500).json({success:false,message:e.message});}}`; id=(await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'})).objectId; const response=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{}})}),result=await response.json();if(!response.ok||!result.success)throw Error(result.message||'迁移失败');console.log('上课记录与复习转交迁移成功;现有任务未转交。'); }finally{if(id)await parse('/classes/Function/'+id,'DELETE');}