@@ -22,12 +22,12 @@ const fs = require("fs");
2222const http = require ( "http" ) ;
2323const net = require ( "net" ) ;
2424const path = require ( "path" ) ;
25+ const { Worker } = require ( "worker_threads" ) ;
2526const {
27+ cleanupOutputDirectoryBackup,
2628 getDefaultOutputDirPath,
2729 getEffectiveOutputDirPath,
28- migrateOutputDirectory,
2930 normalizeDirectoryPath,
30- rollbackOutputDirectoryChange,
3131} = require ( "./output-dir.cjs" ) ;
3232
3333const DEFAULT_BACKEND_HOST = String ( process . env . WECHAT_TOOL_HOST || "127.0.0.1" ) . trim ( ) || "127.0.0.1" ;
@@ -45,6 +45,7 @@ let isQuitting = false;
4545let desktopSettings = null ;
4646let backendPortChangeInProgress = false ;
4747let outputDirChangeInProgress = false ;
48+ let outputDirChangeProgressState = null ;
4849
4950const gotSingleInstanceLock = app . requestSingleInstanceLock ( ) ;
5051if ( ! gotSingleInstanceLock ) {
@@ -279,7 +280,9 @@ function resolveOutputDir() {
279280 if ( ! dataDir ) return null ;
280281
281282 const envOutputDir = safeNormalizeDirectory ( process . env . WECHAT_TOOL_OUTPUT_DIR || "" ) ;
282- const settingsOutputDir = app . isPackaged ? safeNormalizeDirectory ( loadDesktopSettings ( ) ?. outputDir || "" ) : "" ;
283+ // Allow dev-mode desktop runs to persist the chosen output directory too.
284+ // An explicit environment variable still wins so local launch overrides keep working.
285+ const settingsOutputDir = safeNormalizeDirectory ( loadDesktopSettings ( ) ?. outputDir || "" ) ;
283286
284287 let chosen = null ;
285288 try {
@@ -705,6 +708,7 @@ function getOutputDirInfo() {
705708 const defaultPath = getDefaultOutputDir ( ) || "" ;
706709 const currentPath = resolveOutputDir ( ) || defaultPath ;
707710 const hasPending = desktopSettings . pendingOutputDir !== null ;
711+ const canChange = ! ! defaultPath && ! ! currentPath ;
708712 const pendingPath =
709713 desktopSettings . pendingOutputDir === null
710714 ? ""
@@ -718,8 +722,8 @@ function getOutputDirInfo() {
718722 pendingPath,
719723 hasPending,
720724 lastError : String ( desktopSettings . lastOutputDirError || "" ) . trim ( ) ,
721- canChange : ! ! app . isPackaged ,
722- changeUnavailableReason : app . isPackaged ? "" : "开发模式不支持界面修改 output 目录" ,
725+ canChange,
726+ changeUnavailableReason : canChange ? "" : "无法定位 output 目录" ,
723727 } ;
724728}
725729
@@ -749,10 +753,6 @@ function setIgnoredUpdateVersion(version) {
749753}
750754
751755async function applyOutputDirChange ( nextValue ) {
752- if ( ! app . isPackaged ) {
753- throw new Error ( "开发模式不支持界面修改 output 目录" ) ;
754- }
755-
756756 const defaultPath = getDefaultOutputDir ( ) ;
757757 const currentPath = resolveOutputDir ( ) ;
758758 if ( ! defaultPath || ! currentPath ) {
@@ -785,15 +785,41 @@ async function applyOutputDirChange(nextValue) {
785785 const wasBackendRunning = ! ! backendProc ;
786786 let migration = null ;
787787 let settingsSwitched = false ;
788+ let retainedBackupPath = "" ;
789+ let backupCleanupWarning = "" ;
788790
789791 try {
792+ setOutputDirChangeProgressState ( {
793+ active : true ,
794+ stage : "preparing" ,
795+ message : wasBackendRunning ? "正在暂停后端并准备迁移 output 目录" : "正在准备迁移 output 目录" ,
796+ percent : 1 ,
797+ } ) ;
798+
790799 if ( wasBackendRunning ) {
791800 await stopBackendAndWait ( { timeoutMs : 10_000 } ) ;
792801 }
793802
794- migration = migrateOutputDirectory ( {
795- currentDir : currentPath ,
796- nextDir : nextPath ,
803+ migration = await runOutputDirWorker (
804+ "migrate" ,
805+ {
806+ currentDir : currentPath ,
807+ nextDir : nextPath ,
808+ } ,
809+ ( progress ) => {
810+ setOutputDirChangeProgressState ( {
811+ active : true ,
812+ ...progress ,
813+ } ) ;
814+ }
815+ ) ;
816+
817+ setOutputDirChangeProgressState ( {
818+ active : true ,
819+ stage : "switching" ,
820+ message : "正在应用新的 output 目录设置" ,
821+ percent : 99 ,
822+ currentFile : "" ,
797823 } ) ;
798824
799825 setOutputDirSetting ( nextPath ) ;
@@ -803,28 +829,61 @@ async function applyOutputDirChange(nextValue) {
803829 ensureOutputLink ( ) ;
804830
805831 if ( wasBackendRunning ) {
832+ setOutputDirChangeProgressState ( {
833+ active : true ,
834+ stage : "restarting" ,
835+ message : "正在重启后端并应用新的 output 目录" ,
836+ percent : 99 ,
837+ } ) ;
806838 startBackend ( ) ;
807839 await waitForBackend ( { timeoutMs : 30_000 } ) ;
808840 }
809841
842+ retainedBackupPath = migration ?. backupDir || "" ;
843+ if ( retainedBackupPath ) {
844+ try {
845+ cleanupOutputDirectoryBackup ( retainedBackupPath ) ;
846+ retainedBackupPath = "" ;
847+ } catch ( cleanupErr ) {
848+ backupCleanupWarning = `;旧 output 目录未能自动删除:${ cleanupErr ?. message || cleanupErr } ` ;
849+ logMain (
850+ `[main] failed to clean output dir backup ${ retainedBackupPath } : ${ cleanupErr ?. message || cleanupErr } `
851+ ) ;
852+ }
853+ }
854+
855+ setOutputDirChangeProgressState ( {
856+ active : true ,
857+ stage : "complete" ,
858+ message : migration ?. sourceWasEmpty ? "output 目录已切换" : "output 目录已迁移并切换" ,
859+ percent : 100 ,
860+ } ) ;
810861 const info = getOutputDirInfo ( ) ;
862+ const successMessage =
863+ ( migration ?. sourceWasEmpty ? "output 目录已切换" : "output 目录已迁移并切换" ) + backupCleanupWarning ;
811864 return {
812865 success : true ,
813866 changed : true ,
814867 path : info . path ,
815868 defaultPath : info . defaultPath ,
816869 isDefault : info . isDefault ,
817870 pendingPath : info . pendingPath ,
818- backupPath : migration ?. backupDir || "" ,
871+ backupPath : retainedBackupPath ,
819872 sourceWasEmpty : ! ! migration ?. sourceWasEmpty ,
820- message : migration ?. sourceWasEmpty ? "output 目录已切换" : "output 目录已迁移并切换" ,
873+ message : successMessage ,
821874 } ;
822875 } catch ( err ) {
823876 const message = err ?. message || String ( err ) ;
824877 let rollbackMessage = "" ;
825878 if ( migration ?. changed ) {
826879 try {
827- rollbackOutputDirectoryChange ( {
880+ setOutputDirChangeProgressState ( {
881+ active : true ,
882+ stage : "rolling-back" ,
883+ message : "迁移失败,正在回滚 output 目录" ,
884+ percent : 99 ,
885+ } ) ;
886+ await runOutputDirWorker ( "rollback" , {
828887 previousDir : currentPath ,
829888 currentDir : nextPath ,
830889 backupDir : migration . backupDir ,
@@ -969,6 +1028,119 @@ function setWindowProgressBar(value) {
9691028 } catch { }
9701029}
9711030
1031+ function makeIdleOutputDirChangeProgressState ( ) {
1032+ return {
1033+ active : false ,
1034+ stage : "idle" ,
1035+ message : "" ,
1036+ percent : 0 ,
1037+ bytesTransferred : 0 ,
1038+ bytesTotal : 0 ,
1039+ itemsTransferred : 0 ,
1040+ itemsTotal : 0 ,
1041+ currentFile : "" ,
1042+ error : "" ,
1043+ } ;
1044+ }
1045+
1046+ function clampOutputDirProgressNumber ( value ) {
1047+ const n = Number ( value ) ;
1048+ if ( ! Number . isFinite ( n ) || n < 0 ) return 0 ;
1049+ return n ;
1050+ }
1051+
1052+ function normalizeOutputDirChangeProgressState ( next = { } ) {
1053+ const active = next ?. active !== false ;
1054+ const percent = Math . max ( 0 , Math . min ( 100 , Math . round ( Number ( next ?. percent || 0 ) ) ) ) ;
1055+ return {
1056+ active,
1057+ stage : String ( next ?. stage || ( active ? "running" : "idle" ) ) ,
1058+ message : String ( next ?. message || "" ) ,
1059+ percent,
1060+ bytesTransferred : clampOutputDirProgressNumber ( next ?. bytesTransferred ) ,
1061+ bytesTotal : clampOutputDirProgressNumber ( next ?. bytesTotal ) ,
1062+ itemsTransferred : clampOutputDirProgressNumber ( next ?. itemsTransferred ) ,
1063+ itemsTotal : clampOutputDirProgressNumber ( next ?. itemsTotal ) ,
1064+ currentFile : String ( next ?. currentFile || "" ) ,
1065+ error : String ( next ?. error || "" ) ,
1066+ } ;
1067+ }
1068+
1069+ function getOutputDirChangeProgressState ( ) {
1070+ if ( ! outputDirChangeProgressState ) {
1071+ outputDirChangeProgressState = makeIdleOutputDirChangeProgressState ( ) ;
1072+ }
1073+ return outputDirChangeProgressState ;
1074+ }
1075+
1076+ function setOutputDirChangeProgressState ( next = { } ) {
1077+ outputDirChangeProgressState = normalizeOutputDirChangeProgressState ( next ) ;
1078+ sendToRenderer ( "app:outputDirChangeProgress" , outputDirChangeProgressState ) ;
1079+
1080+ if ( ! outputDirChangeProgressState . active ) {
1081+ setWindowProgressBar ( - 1 ) ;
1082+ return outputDirChangeProgressState ;
1083+ }
1084+
1085+ const ratio =
1086+ outputDirChangeProgressState . percent > 0
1087+ ? Math . max ( 0.02 , Math . min ( 1 , outputDirChangeProgressState . percent / 100 ) )
1088+ : 2 ;
1089+ setWindowProgressBar ( ratio ) ;
1090+ return outputDirChangeProgressState ;
1091+ }
1092+
1093+ function clearOutputDirChangeProgressState ( ) {
1094+ return setOutputDirChangeProgressState ( { active : false } ) ;
1095+ }
1096+
1097+ function getOutputDirWorkerScriptPath ( ) {
1098+ return path . join ( __dirname , "output-dir-worker.cjs" ) ;
1099+ }
1100+
1101+ function runOutputDirWorker ( action , payload , onProgress ) {
1102+ return new Promise ( ( resolve , reject ) => {
1103+ const worker = new Worker ( getOutputDirWorkerScriptPath ( ) , {
1104+ workerData : {
1105+ action : String ( action || "migrate" ) ,
1106+ payload,
1107+ } ,
1108+ } ) ;
1109+
1110+ let settled = false ;
1111+ const finish = ( err , result ) => {
1112+ if ( settled ) return ;
1113+ settled = true ;
1114+ if ( err ) reject ( err ) ;
1115+ else resolve ( result ) ;
1116+ } ;
1117+
1118+ worker . on ( "message" , ( message ) => {
1119+ if ( ! message || typeof message !== "object" ) return ;
1120+ if ( message . type === "progress" ) {
1121+ if ( typeof onProgress === "function" ) onProgress ( message . progress || { } ) ;
1122+ return ;
1123+ }
1124+ if ( message . type === "result" ) {
1125+ finish ( null , message . result ) ;
1126+ return ;
1127+ }
1128+ if ( message . type === "error" ) {
1129+ finish ( new Error ( message . error ?. message || "output 目录迁移失败" ) ) ;
1130+ }
1131+ } ) ;
1132+
1133+ worker . once ( "error" , ( err ) => {
1134+ finish ( err ) ;
1135+ } ) ;
1136+
1137+ worker . once ( "exit" , ( code ) => {
1138+ if ( settled || code === 0 ) return ;
1139+ finish ( new Error ( `output 目录任务异常退出(code=${ code } )` ) ) ;
1140+ } ) ;
1141+ } ) ;
1142+ }
1143+
9721144function looksLikeHtml ( input ) {
9731145 if ( ! input ) return false ;
9741146 const s = String ( input ) ;
@@ -1611,23 +1783,28 @@ function startBackend() {
16111783 if ( backendProc ) return backendProc ;
16121784 startWcdbSidecar ( ) ;
16131785
1786+ const resolvedDataPath = resolveDataDir ( ) || getUserDataDir ( ) || repoRoot ( ) ;
1787+ const resolvedOutputPath = resolveOutputDir ( ) || getDefaultOutputDir ( ) || path . join ( resolvedDataPath , "output" ) ;
16141788 const env = {
16151789 ...process . env ,
16161790 WECHAT_TOOL_HOST : getBackendBindHost ( ) ,
16171791 WECHAT_TOOL_PORT : String ( getBackendPort ( ) ) ,
1792+ WECHAT_TOOL_DATA_DIR : resolvedDataPath ,
1793+ WECHAT_TOOL_OUTPUT_DIR : resolvedOutputPath ,
16181794 // Make sure Python prints UTF-8 to stdout/stderr.
16191795 PYTHONIOENCODING : process . env . PYTHONIOENCODING || "utf-8" ,
16201796 } ;
16211797 ensureWcdbSidecarEnv ( env ) ;
1798+ logMain (
1799+ `[main] startBackend packaged=${ app . isPackaged } port=${ env . WECHAT_TOOL_PORT } dataDir=${ env . WECHAT_TOOL_DATA_DIR } outputDir=${ env . WECHAT_TOOL_OUTPUT_DIR } `
1800+ ) ;
16221801
16231802 // In packaged mode we expect to provide the generated Nuxt output dir via env.
16241803 if ( app . isPackaged && ! env . WECHAT_TOOL_UI_DIR ) {
16251804 env . WECHAT_TOOL_UI_DIR = path . join ( process . resourcesPath , "ui" ) ;
16261805 }
16271806
16281807 if ( app . isPackaged ) {
1629- env . WECHAT_TOOL_DATA_DIR = resolveDataDir ( ) || app . getPath ( "userData" ) ;
1630- env . WECHAT_TOOL_OUTPUT_DIR = resolveOutputDir ( ) || getDefaultOutputDir ( ) || path . join ( env . WECHAT_TOOL_DATA_DIR , "output" ) ;
16311808 try {
16321809 fs . mkdirSync ( env . WECHAT_TOOL_DATA_DIR , { recursive : true } ) ;
16331810 fs . mkdirSync ( env . WECHAT_TOOL_OUTPUT_DIR , { recursive : true } ) ;
@@ -2156,8 +2333,8 @@ function registerWindowIpc() {
21562333 pendingPath : "" ,
21572334 hasPending : false ,
21582335 lastError : err ?. message || String ( err ) ,
2159- canChange : ! ! app . isPackaged ,
2160- changeUnavailableReason : app . isPackaged ? "" : "开发模式不支持界面修改 output 目录 ",
2336+ canChange : false ,
2337+ changeUnavailableReason : "无法读取 output 目录信息 ",
21612338 } ;
21622339 }
21632340 } ) ;
@@ -2166,6 +2343,10 @@ function registerWindowIpc() {
21662343 return resolveOutputDir ( ) || "" ;
21672344 } ) ;
21682345
2346+ ipcMain . handle ( "app:getOutputDirChangeProgress" , ( ) => {
2347+ return getOutputDirChangeProgressState ( ) ;
2348+ } ) ;
2349+
21692350 ipcMain . handle ( "app:openOutputDir" , async ( ) => {
21702351 const outDir = resolveOutputDir ( ) ;
21712352 if ( ! outDir ) throw new Error ( "无法定位 output 目录" ) ;
@@ -2202,6 +2383,7 @@ function registerWindowIpc() {
22022383 } ;
22032384 } finally {
22042385 outputDirChangeInProgress = false ;
2386+ clearOutputDirChangeProgressState ( ) ;
22052387 }
22062388 } ) ;
22072389
0 commit comments