dsh-config-manager 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -2209,6 +2209,8 @@ function defaultSyncState() {
2209
2209
  git: defaultChannelSyncState(),
2210
2210
  webdav: defaultChannelSyncState()
2211
2211
  },
2212
+ busy: null,
2213
+ savingConfig: false,
2212
2214
  pushReport: null,
2213
2215
  pullReport: null,
2214
2216
  confirmSession: null,
@@ -2252,8 +2254,9 @@ function defaultState() {
2252
2254
  }
2253
2255
  /**
2254
2256
  * 从视图状态提取同步切片(结构兼容:传入 SyncUiState 亦可;只保留切片字段,
2255
- * github 流程态/loading/busy 等瞬态不进入切片)。组件每次状态变化后调用,
2256
- * 把非敏感 + 仅内存字段镜像进 store;敏感字段由 toPersistedState 硬性剔除。
2257
+ * github 流程态/loading 等瞬态不进入切片;busy/savingConfig 为「内存切片」瞬态——
2258
+ * tab 由模块级单例保留,刷新时被 toPersistedState 白名单剔除)。组件每次状态
2259
+ * 变化后(含异步回调)调用,把非敏感 + 仅内存字段镜像进 store。
2257
2260
  * byChannel 的快照数组复制引用(避免跨切片共享可变数组)。
2258
2261
  */
2259
2262
  function toSyncStoreSlice(s) {
@@ -2274,6 +2277,8 @@ function toSyncStoreSlice(s) {
2274
2277
  snapshots: [...s.byChannel.webdav.snapshots]
2275
2278
  }
2276
2279
  },
2280
+ busy: s.busy,
2281
+ savingConfig: s.savingConfig,
2277
2282
  pushReport: s.pushReport,
2278
2283
  pullReport: s.pullReport,
2279
2284
  confirmSession: s.confirmSession,
@@ -2309,13 +2314,14 @@ function toSnapshotsStoreSlice(s) {
2309
2314
  * 持久化白名单:解构剔除敏感字段(password/passwordConfirm/secretInputs/decryptPassword)
2310
2315
  * 与不可序列化的实例字段(conflictCollector),以及同步面板的凭据字段
2311
2316
  * (token/webdavPassword/encryptPassword/encryptPasswordConfirm/decryptPassword ——
2312
- * 含 byChannel 内每通道的加密/解密密码),其余原样落入 sessionStorage。
2317
+ * 含 byChannel 内每通道的加密/解密密码)与瞬态字段(busy/savingConfig —— 刷新后
2318
+ * 回复空闲,不把「进行中」状态带到新页面),其余原样落入 sessionStorage。
2313
2319
  * 这是 sessionStorage 的唯一写入路径 —— 敏感值在此被硬性隔离。
2314
2320
  */
2315
2321
  function toPersistedState(state) {
2316
2322
  const { password: _password, passwordConfirm: _passwordConfirm, ...exportRest } = state.export;
2317
2323
  const { secretInputs: _secretInputs, decryptPassword: _decryptPassword, decryptRefs: _decryptRefs, archiveUnlocked: _archiveUnlocked, conflictCollector: _conflictCollector, ...importRest } = state.import;
2318
- const { token: _token, webdavPassword: _webdavPassword, ...syncRest } = state.sync;
2324
+ const { token: _token, webdavPassword: _webdavPassword, busy: _busy, savingConfig: _savingConfig, ...syncRest } = state.sync;
2319
2325
  const stripChannelSensitive = (c) => {
2320
2326
  const { encryptPassword: _ep, encryptPasswordConfirm: _epc, decryptPassword: _dp, ...rest } = c;
2321
2327
  return rest;
@@ -2586,6 +2592,8 @@ var RunStore = class {
2586
2592
  ...parsed.sync,
2587
2593
  token: "",
2588
2594
  webdavPassword: "",
2595
+ busy: null,
2596
+ savingConfig: false,
2589
2597
  byChannel: {
2590
2598
  git: {
2591
2599
  ...defaultChannelSyncState(),
@@ -2898,111 +2906,111 @@ if (typeof document !== "undefined" && document.querySelector("style[data-tag=\"
2898
2906
  document.head.appendChild(tag);
2899
2907
  }
2900
2908
  var config_manager_module_css_default = {
2909
+ "dangerButton": "_367UGW_dangerButton",
2901
2910
  "errorAction": "_367UGW_errorAction",
2902
- "errorActionLabel": "_367UGW_errorActionLabel",
2903
- "sectionTitleBlock": "_367UGW_sectionTitleBlock",
2911
+ "planScroll": "_367UGW_planScroll",
2912
+ "badgeOk": "_367UGW_badgeOk",
2913
+ "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2914
+ "viewBody": "_367UGW_viewBody",
2915
+ "progressTrack": "_367UGW_progressTrack",
2916
+ "reportView": "_367UGW_reportView",
2917
+ "severityError": "_367UGW_severityError",
2918
+ "progressBadge": "_367UGW_progressBadge",
2919
+ "conflictHead": "_367UGW_conflictHead",
2920
+ "categoryName": "_367UGW_categoryName",
2921
+ "errorTitle": "_367UGW_errorTitle",
2922
+ "banner": "_367UGW_banner",
2923
+ "pathNew": "_367UGW_pathNew",
2924
+ "warnText": "_367UGW_warnText",
2925
+ "kindTag": "_367UGW_kindTag",
2926
+ "pathOld": "_367UGW_pathOld",
2927
+ "section": "_367UGW_section",
2928
+ "sectionTitle": "_367UGW_sectionTitle",
2929
+ "modeTab": "_367UGW_modeTab",
2930
+ "progressMeta": "_367UGW_progressMeta",
2931
+ "categoryDesc": "_367UGW_categoryDesc",
2932
+ "conflictId": "_367UGW_conflictId",
2933
+ "primaryButton": "_367UGW_primaryButton",
2934
+ "pathMappingList": "_367UGW_pathMappingList",
2935
+ "pathRow": "_367UGW_pathRow",
2936
+ "diffScroll": "_367UGW_diffScroll",
2937
+ "input": "_367UGW_input",
2938
+ "errorFooter": "_367UGW_errorFooter",
2939
+ "pullScroll": "_367UGW_pullScroll",
2940
+ "reportFooter": "_367UGW_reportFooter",
2941
+ "badge": "_367UGW_badge",
2942
+ "snapshotList": "_367UGW_snapshotList",
2943
+ "viewTabs": "_367UGW_viewTabs",
2944
+ "sectionSubtitle": "_367UGW_sectionSubtitle",
2945
+ "groupNote": "_367UGW_groupNote",
2904
2946
  "radioLabel": "_367UGW_radioLabel",
2947
+ "pathValue": "_367UGW_pathValue",
2948
+ "spinner": "_367UGW_spinner",
2949
+ "reportScroll": "_367UGW_reportScroll",
2950
+ "aboutLinkRow": "_367UGW_aboutLinkRow",
2905
2951
  "aboutAuthor": "_367UGW_aboutAuthor",
2906
- "fieldLabel": "_367UGW_fieldLabel",
2907
- "empty": "_367UGW_empty",
2908
- "snapshotRow": "_367UGW_snapshotRow",
2952
+ "progressBarDone": "_367UGW_progressBarDone",
2953
+ "progressDetail": "_367UGW_progressDetail",
2909
2954
  "card": "_367UGW_card",
2910
- "section": "_367UGW_section",
2955
+ "errorActionLabel": "_367UGW_errorActionLabel",
2956
+ "conflictOptions": "_367UGW_conflictOptions",
2957
+ "pathIssueKind": "_367UGW_pathIssueKind",
2958
+ "empty": "_367UGW_empty",
2959
+ "select": "_367UGW_select",
2960
+ "secretsList": "_367UGW_secretsList",
2961
+ "fieldLabel": "_367UGW_fieldLabel",
2911
2962
  "formError": "_367UGW_formError",
2912
- "categoryItem": "_367UGW_categoryItem",
2963
+ "spinnerWrap": "_367UGW_spinnerWrap",
2913
2964
  "warnList": "_367UGW_warnList",
2914
- "progressBadge": "_367UGW_progressBadge",
2965
+ "sectionBody": "_367UGW_sectionBody",
2966
+ "groupItems": "_367UGW_groupItems",
2915
2967
  "snapshotRowHeader": "_367UGW_snapshotRowHeader",
2916
- "pathRow": "_367UGW_pathRow",
2968
+ "rowActions": "_367UGW_rowActions",
2969
+ "spinnerLabel": "_367UGW_spinnerLabel",
2917
2970
  "optionsCard": "_367UGW_optionsCard",
2918
- "reportText": "_367UGW_reportText",
2919
- "spinnerWrap": "_367UGW_spinnerWrap",
2920
- "groupLabel": "_367UGW_groupLabel",
2921
- "spinner": "_367UGW_spinner",
2922
- "ghostButton": "_367UGW_ghostButton",
2923
- "primaryButton": "_367UGW_primaryButton",
2924
- "progressPercent": "_367UGW_progressPercent",
2925
2971
  "progressBlock": "_367UGW_progressBlock",
2926
- "pathNew": "_367UGW_pathNew",
2927
- "dangerButton": "_367UGW_dangerButton",
2928
- "badge": "_367UGW_badge",
2972
+ "progressPercent": "_367UGW_progressPercent",
2929
2973
  "conflictDetail": "_367UGW_conflictDetail",
2930
- "spinnerLabel": "_367UGW_spinnerLabel",
2931
- "conflictItem": "_367UGW_conflictItem",
2932
- "sectionBody": "_367UGW_sectionBody",
2933
- "badgeWarn": "_367UGW_badgeWarn",
2934
- "input": "_367UGW_input",
2935
- "progressBadgeSection": "_367UGW_progressBadgeSection",
2936
- "errorItem": "_367UGW_errorItem",
2937
- "pathIssueKind": "_367UGW_pathIssueKind",
2938
- "progressTrack": "_367UGW_progressTrack",
2939
- "rowActions": "_367UGW_rowActions",
2940
- "progressBarDone": "_367UGW_progressBarDone",
2941
- "actionRow": "_367UGW_actionRow",
2942
- "pullScroll": "_367UGW_pullScroll",
2943
- "checkboxRow": "_367UGW_checkboxRow",
2944
2974
  "progressLabel": "_367UGW_progressLabel",
2945
- "pathMappingList": "_367UGW_pathMappingList",
2946
- "hint": "_367UGW_hint",
2947
- "sectionSubtitle": "_367UGW_sectionSubtitle",
2948
- "categoryDesc": "_367UGW_categoryDesc",
2975
+ "badgeInfo": "_367UGW_badgeInfo",
2976
+ "statRow": "_367UGW_statRow",
2977
+ "modeTabs": "_367UGW_modeTabs",
2978
+ "viewTab": "_367UGW_viewTab",
2949
2979
  "field": "_367UGW_field",
2950
- "progressMeta": "_367UGW_progressMeta",
2951
- "dshCmIndeterminate": "_367UGW_dshCmIndeterminate",
2952
- "errorBanner": "_367UGW_errorBanner",
2953
- "conflictHead": "_367UGW_conflictHead",
2954
- "banner": "_367UGW_banner",
2980
+ "sectionHeader": "_367UGW_sectionHeader",
2955
2981
  "badgeError": "_367UGW_badgeError",
2956
- "hiddenFile": "_367UGW_hiddenFile",
2957
- "modeTab": "_367UGW_modeTab",
2958
- "viewBody": "_367UGW_viewBody",
2959
- "groupCard": "_367UGW_groupCard",
2960
- "groupItems": "_367UGW_groupItems",
2961
- "viewTab": "_367UGW_viewTab",
2962
- "groupNote": "_367UGW_groupNote",
2963
- "badgeInfo": "_367UGW_badgeInfo",
2964
- "categoryName": "_367UGW_categoryName",
2965
2982
  "optionsHeader": "_367UGW_optionsHeader",
2966
- "progressDetail": "_367UGW_progressDetail",
2967
- "progressBadgeCount": "_367UGW_progressBadgeCount",
2968
2983
  "groupList": "_367UGW_groupList",
2969
- "progressBar": "_367UGW_progressBar",
2970
- "errorFooter": "_367UGW_errorFooter",
2971
- "severityError": "_367UGW_severityError",
2972
- "snapshotList": "_367UGW_snapshotList",
2973
- "warnText": "_367UGW_warnText",
2974
- "kindTag": "_367UGW_kindTag",
2975
- "secretFields": "_367UGW_secretFields",
2976
- "viewTabs": "_367UGW_viewTabs",
2977
- "select": "_367UGW_select",
2978
- "reportFooter": "_367UGW_reportFooter",
2979
- "modeTabs": "_367UGW_modeTabs",
2980
- "errorLine": "_367UGW_errorLine",
2981
- "confirmScroll": "_367UGW_confirmScroll",
2982
- "sectionHeader": "_367UGW_sectionHeader",
2983
- "reportView": "_367UGW_reportView",
2984
2984
  "conflictList": "_367UGW_conflictList",
2985
- "modeHint": "_367UGW_modeHint",
2986
- "pathValue": "_367UGW_pathValue",
2985
+ "groupHeader": "_367UGW_groupHeader",
2986
+ "errorLine": "_367UGW_errorLine",
2987
+ "conflictItem": "_367UGW_conflictItem",
2988
+ "snapshotRow": "_367UGW_snapshotRow",
2989
+ "errorBanner": "_367UGW_errorBanner",
2990
+ "hiddenFile": "_367UGW_hiddenFile",
2987
2991
  "progressIndeterminate": "_367UGW_progressIndeterminate",
2988
- "conflictOptions": "_367UGW_conflictOptions",
2989
- "diffScroll": "_367UGW_diffScroll",
2990
- "planScroll": "_367UGW_planScroll",
2991
- "errorTitle": "_367UGW_errorTitle",
2992
- "sectionTitle": "_367UGW_sectionTitle",
2993
- "conflictId": "_367UGW_conflictId",
2994
- "reportList": "_367UGW_reportList",
2995
2992
  "rollbackBox": "_367UGW_rollbackBox",
2996
- "dshCmSpin": "_367UGW_dshCmSpin",
2997
- "groupHeader": "_367UGW_groupHeader",
2998
- "errorReason": "_367UGW_errorReason",
2999
- "reportScroll": "_367UGW_reportScroll",
2993
+ "confirmScroll": "_367UGW_confirmScroll",
2994
+ "actionRow": "_367UGW_actionRow",
2995
+ "progressBar": "_367UGW_progressBar",
2996
+ "hint": "_367UGW_hint",
3000
2997
  "errorList": "_367UGW_errorList",
3001
- "pathOld": "_367UGW_pathOld",
3002
- "badgeOk": "_367UGW_badgeOk",
3003
- "statRow": "_367UGW_statRow",
3004
- "secretsList": "_367UGW_secretsList",
3005
- "aboutLinkRow": "_367UGW_aboutLinkRow"
2998
+ "reportList": "_367UGW_reportList",
2999
+ "ghostButton": "_367UGW_ghostButton",
3000
+ "groupLabel": "_367UGW_groupLabel",
3001
+ "badgeWarn": "_367UGW_badgeWarn",
3002
+ "checkboxRow": "_367UGW_checkboxRow",
3003
+ "progressBadgeCount": "_367UGW_progressBadgeCount",
3004
+ "errorReason": "_367UGW_errorReason",
3005
+ "groupCard": "_367UGW_groupCard",
3006
+ "categoryItem": "_367UGW_categoryItem",
3007
+ "modeHint": "_367UGW_modeHint",
3008
+ "reportText": "_367UGW_reportText",
3009
+ "sectionTitleBlock": "_367UGW_sectionTitleBlock",
3010
+ "dshCmSpin": "_367UGW_dshCmSpin",
3011
+ "errorItem": "_367UGW_errorItem",
3012
+ "secretFields": "_367UGW_secretFields",
3013
+ "progressBadgeSection": "_367UGW_progressBadgeSection"
3006
3014
  };
3007
3015
  //#endregion
3008
3016
  //#region src/client/common/ui.tsx
@@ -4775,83 +4783,84 @@ function initFromStore$2() {
4775
4783
  }
4776
4784
  function SnapshotsPanel({ api, t }) {
4777
4785
  const [state, setState] = (0, react.useState)(initFromStore$2);
4778
- /** 最新 state 镜像(卸载 flush 时读取,避免闭包过期值) */
4786
+ /** 最新 state 镜像(commit/卸载 flush 读取,避免闭包过期值) */
4779
4787
  const stateRef = (0, react.useRef)(state);
4780
- (0, react.useEffect)(() => {
4781
- stateRef.current = state;
4782
- }, [state]);
4783
- /** 状态镜像:任何状态变化同步进 runStore(切 tab 不丢 / 刷新恢复)。 */
4784
- (0, react.useEffect)(() => {
4785
- runStore.patch({ snapshots: toSnapshotsStoreSlice(state) });
4786
- }, [state]);
4787
- /** 卸载时最后镜像一次(防止「最后一次改动后立即切 tab」时镜像 effect 尚未 flush)。 */
4788
+ /** 挂载守卫:卸载后不再 setState(store 镜像仍执行,异步结果照常落库) */
4789
+ const mountedRef = (0, react.useRef)(true);
4790
+ /**
4791
+ * 统一提交入口:更新 stateRef 挂载时 setState **总是**镜像进 runStore。
4792
+ * 关键:镜像不依赖 effect flush —— 异步操作(dry-run 计划/执行恢复)完成回调
4793
+ * 在组件已卸载(切走 tab)时也能把结果(plan/report)写进 store,切回恢复。
4794
+ */
4795
+ const commit = (next) => {
4796
+ stateRef.current = next;
4797
+ if (mountedRef.current) setState(next);
4798
+ runStore.patch({ snapshots: toSnapshotsStoreSlice(next) });
4799
+ };
4800
+ const patch = (p) => commit({
4801
+ ...stateRef.current,
4802
+ ...p
4803
+ });
4804
+ /** 卸载时置挂载守卫 + 最后镜像一次(防止「最后一次改动后立即切 tab」时丢状态)。 */
4788
4805
  (0, react.useEffect)(() => () => {
4806
+ mountedRef.current = false;
4789
4807
  runStore.patch({ snapshots: toSnapshotsStoreSlice(stateRef.current) });
4790
4808
  }, []);
4791
4809
  const load = () => {
4792
- setState((s) => ({
4793
- ...s,
4810
+ patch({
4794
4811
  status: "loading",
4795
4812
  error: null
4796
- }));
4813
+ });
4797
4814
  api.snapshots().then((metas) => {
4798
- setState((s) => ({
4799
- ...s,
4815
+ patch({
4800
4816
  status: "ready",
4801
4817
  metas
4802
- }));
4818
+ });
4803
4819
  }, (err) => {
4804
- setState((s) => ({
4805
- ...s,
4820
+ patch({
4806
4821
  status: "error",
4807
4822
  error: err instanceof Error ? err.message : String(err)
4808
- }));
4823
+ });
4809
4824
  });
4810
4825
  };
4811
4826
  (0, react.useEffect)(load, [api]);
4812
4827
  const select = (id) => {
4813
- setState((s) => ({
4814
- ...s,
4828
+ patch({
4815
4829
  selectedId: id,
4816
4830
  plan: null,
4817
4831
  report: null,
4818
4832
  actionError: null,
4819
4833
  planning: true
4820
- }));
4834
+ });
4821
4835
  api.restoreSnapshot(id, true).then((res) => {
4822
- setState((s) => ({
4823
- ...s,
4836
+ patch({
4824
4837
  planning: false,
4825
4838
  plan: res.plan ?? null
4826
- }));
4839
+ });
4827
4840
  }, (err) => {
4828
- setState((s) => ({
4829
- ...s,
4841
+ patch({
4830
4842
  planning: false,
4831
4843
  actionError: err instanceof Error ? err.message : String(err)
4832
- }));
4844
+ });
4833
4845
  });
4834
4846
  };
4835
4847
  const execute = () => {
4836
4848
  if (state.selectedId === null || state.running) return;
4837
- setState((s) => ({
4838
- ...s,
4849
+ patch({
4839
4850
  running: true,
4840
4851
  report: null,
4841
4852
  actionError: null
4842
- }));
4853
+ });
4843
4854
  api.restoreSnapshot(state.selectedId, false).then((res) => {
4844
- setState((s) => ({
4845
- ...s,
4855
+ patch({
4846
4856
  running: false,
4847
4857
  report: res.report ?? null
4848
- }));
4858
+ });
4849
4859
  }, (err) => {
4850
- setState((s) => ({
4851
- ...s,
4860
+ patch({
4852
4861
  running: false,
4853
4862
  actionError: err instanceof Error ? err.message : String(err)
4854
- }));
4863
+ });
4855
4864
  });
4856
4865
  };
4857
4866
  const summary = () => {
@@ -5786,6 +5795,8 @@ const initial$2 = {
5786
5795
  * 从 runStore 恢复上次的同步 UI 状态(切 tab 回 / 刷新后挂载)。
5787
5796
  * 敏感字段(token/webdav 密码/加密与解密密码)只在内存切片里保留:切 tab 保留;
5788
5797
  * 刷新后已被持久化白名单清空(applyPersisted 强制归零)→ 需要时重新输入。
5798
+ * busy/savingConfig 为瞬态:切 tab 由模块级单例保留(切回仍显示进行中);
5799
+ * 刷新后白名单剔除 → 回复空闲。
5789
5800
  */
5790
5801
  function initFromStore$1() {
5791
5802
  const s = runStore.getSnapshot().sync;
@@ -5807,6 +5818,8 @@ function initFromStore$1() {
5807
5818
  ...s.byChannel.webdav
5808
5819
  }
5809
5820
  },
5821
+ busy: s.busy,
5822
+ savingConfig: s.savingConfig,
5810
5823
  pushReport: s.pushReport,
5811
5824
  pullReport: s.pullReport,
5812
5825
  confirmSession: s.confirmSession,
@@ -5818,47 +5831,46 @@ function initFromStore$1() {
5818
5831
  function SyncSettingsView({ api, t }) {
5819
5832
  const [state, setState] = (0, react.useState)(initFromStore$1);
5820
5833
  const uiT = api.t;
5821
- /** 最新 state 镜像(自动保存 flush 时读取,避免防抖回调里闭包过期值) */
5834
+ /** 最新 state 镜像(commit/自动保存 flush 读取,避免闭包过期值) */
5822
5835
  const stateRef = (0, react.useRef)(state);
5823
- const patch = (p) => {
5824
- setState((s) => {
5825
- const next = {
5826
- ...s,
5827
- ...p
5828
- };
5829
- stateRef.current = next;
5830
- return next;
5831
- });
5836
+ /** 挂载守卫:卸载后不再 setState(store 镜像仍执行,异步结果照常落库) */
5837
+ const mountedRef = (0, react.useRef)(true);
5838
+ /**
5839
+ * 统一提交入口:更新 stateRef → 挂载时 setState → **总是**镜像进 runStore。
5840
+ * 关键:镜像不依赖 effect flush —— 异步操作(push/pull/sync)完成回调在组件
5841
+ * 已卸载(切走 tab)时也能把结果写进 store,切回 tab 时 initFromStore 恢复。
5842
+ */
5843
+ const commit = (next) => {
5844
+ stateRef.current = next;
5845
+ if (mountedRef.current) setState(next);
5846
+ runStore.patch({ sync: toSyncStoreSlice(next) });
5832
5847
  };
5848
+ const patch = (p) => commit({
5849
+ ...stateRef.current,
5850
+ ...p
5851
+ });
5833
5852
  /** 更新指定通道的 byChannel 状态(子 tab 切换后 loadSnapshots 等场景用)。 */
5834
- const patchChannelState = (ch, p) => setState((s) => {
5835
- const next = {
5836
- ...s,
5837
- byChannel: {
5838
- ...s.byChannel,
5839
- [ch]: {
5840
- ...s.byChannel[ch],
5841
- ...p
5842
- }
5853
+ const patchChannelState = (ch, p) => commit({
5854
+ ...stateRef.current,
5855
+ byChannel: {
5856
+ ...stateRef.current.byChannel,
5857
+ [ch]: {
5858
+ ...stateRef.current.byChannel[ch],
5859
+ ...p
5843
5860
  }
5844
- };
5845
- stateRef.current = next;
5846
- return next;
5861
+ }
5847
5862
  });
5848
5863
  /** 更新当前激活通道的 byChannel 状态。 */
5849
5864
  const patchChannel = (p) => patchChannelState(state.channel, p);
5850
5865
  /** 当前激活通道的设置状态(自动同步/模式/加密/快照)。 */
5851
5866
  const chState = state.byChannel[state.channel];
5852
- const patchGithub = (p) => setState((s) => {
5853
- const next = {
5854
- ...s,
5855
- github: {
5856
- ...s.github,
5857
- ...p
5858
- }
5859
- };
5860
- stateRef.current = next;
5861
- return next;
5867
+ /** GitHub 流程态(不进 store 切片;commit 的镜像写幂等无害)。 */
5868
+ const patchGithub = (p) => commit({
5869
+ ...stateRef.current,
5870
+ github: {
5871
+ ...stateRef.current.github,
5872
+ ...p
5873
+ }
5862
5874
  });
5863
5875
  /** GitHub 轮询定时器(卸载/取消时清理,防止泄漏与跨流程串扰) */
5864
5876
  const githubPollTimer = (0, react.useRef)(null);
@@ -5931,26 +5943,15 @@ function SyncSettingsView({ api, t }) {
5931
5943
  const loadAutosync = async () => {
5932
5944
  try {
5933
5945
  const all = await api.autosyncStatusAll();
5934
- setState((s) => {
5935
- const next = {
5936
- ...s,
5937
- byChannel: {
5938
- git: {
5939
- ...s.byChannel.git,
5940
- autosync: all.git,
5941
- autosyncEnabled: all.git.enabled,
5942
- autosyncInterval: all.git.interval
5943
- },
5944
- webdav: {
5945
- ...s.byChannel.webdav,
5946
- autosync: all.webdav,
5947
- autosyncEnabled: all.webdav.enabled,
5948
- autosyncInterval: all.webdav.interval
5949
- }
5950
- }
5951
- };
5952
- stateRef.current = next;
5953
- return next;
5946
+ patchChannelState("git", {
5947
+ autosync: all.git,
5948
+ autosyncEnabled: all.git.enabled,
5949
+ autosyncInterval: all.git.interval
5950
+ });
5951
+ patchChannelState("webdav", {
5952
+ autosync: all.webdav,
5953
+ autosyncEnabled: all.webdav.enabled,
5954
+ autosyncInterval: all.webdav.interval
5954
5955
  });
5955
5956
  } catch (err) {
5956
5957
  patch({ error: err instanceof Error ? err.message : String(err) });
@@ -5989,14 +5990,11 @@ function SyncSettingsView({ api, t }) {
5989
5990
  (0, react.useEffect)(() => {
5990
5991
  loadStatus();
5991
5992
  }, []);
5992
- /** 状态镜像:任何 UI 状态变化同步进 runStore(模块级单例 tab 不丢;sessionStorage → 刷新恢复)。
5993
- * 切片函数剔除 github 流程态等瞬态;敏感字段(含 byChannel 密码类)由 toPersistedState 白名单硬性剔除。 */
5994
- (0, react.useEffect)(() => {
5995
- runStore.patch({ sync: toSyncStoreSlice(state) });
5996
- }, [state]);
5997
- /** 卸载时清理轮询定时器 + 补发未落盘的通道配置改动 + 最后镜像一次状态
5998
- * (组件销毁后不得再 setState/发请求;store 镜像为纯内存/白名单写,安全) */
5993
+ /** 卸载时置挂载守卫 + 清理轮询定时器 + 补发未落盘的通道配置改动 + 最后镜像一次状态
5994
+ * (组件销毁后不得再 setState/发请求;store 镜像为纯内存/白名单写,安全)。
5995
+ * 异步操作完成回调仍会走 commit store(见 commit 注释),结果不丢。 */
5999
5996
  (0, react.useEffect)(() => () => {
5997
+ mountedRef.current = false;
6000
5998
  if (githubPollTimer.current !== null) clearTimeout(githubPollTimer.current);
6001
5999
  if (saveTimer.current !== null) {
6002
6000
  clearTimeout(saveTimer.current);
@@ -6055,30 +6053,28 @@ function SyncSettingsView({ api, t }) {
6055
6053
  });
6056
6054
  try {
6057
6055
  const saved = await api.saveConfig(payloadToSave);
6058
- setState((s) => {
6059
- const next = {
6060
- ...s,
6061
- savingConfig: false
6056
+ const s = stateRef.current;
6057
+ const next = {
6058
+ ...s,
6059
+ savingConfig: false
6060
+ };
6061
+ next.webdavPassword = s.webdavPassword !== "" && s.webdavPassword !== payloadToSave.password ? s.webdavPassword : "";
6062
+ next.token = s.token !== "" && s.token !== payloadToSave.token ? s.token : "";
6063
+ if (s.statusInfo !== null) {
6064
+ const info = {
6065
+ ...s.statusInfo,
6066
+ configured: true,
6067
+ credentialConfigured: saved.credentialConfigured
6062
6068
  };
6063
- next.webdavPassword = s.webdavPassword !== "" && s.webdavPassword !== payloadToSave.password ? s.webdavPassword : "";
6064
- next.token = s.token !== "" && s.token !== payloadToSave.token ? s.token : "";
6065
- if (s.statusInfo !== null) {
6066
- const info = {
6067
- ...s.statusInfo,
6068
- configured: true,
6069
- credentialConfigured: saved.credentialConfigured
6070
- };
6071
- if (saved.webdav !== void 0) info.webdav = {
6072
- url: s.statusInfo.webdav?.url,
6073
- username: s.statusInfo.webdav?.username,
6074
- usernameConfigured: saved.webdav.usernameConfigured,
6075
- passwordConfigured: saved.webdav.passwordConfigured
6076
- };
6077
- next.statusInfo = info;
6078
- }
6079
- stateRef.current = next;
6080
- return next;
6081
- });
6069
+ if (saved.webdav !== void 0) info.webdav = {
6070
+ url: s.statusInfo.webdav?.url,
6071
+ username: s.statusInfo.webdav?.username,
6072
+ usernameConfigured: saved.webdav.usernameConfigured,
6073
+ passwordConfigured: saved.webdav.passwordConfigured
6074
+ };
6075
+ next.statusInfo = info;
6076
+ }
6077
+ commit(next);
6082
6078
  } catch (err) {
6083
6079
  patch({
6084
6080
  savingConfig: false,
@@ -8153,22 +8149,27 @@ function MarketPanel({ api, importApi, t }) {
8153
8149
  /** 发布向导开关(组件内状态,不进 sessionStorage —— 发布为一次性低频率流程) */
8154
8150
  const [publishOpen, setPublishOpen] = (0, react.useState)(false);
8155
8151
  const [state, setState] = (0, react.useState)(initFromStore);
8156
- /** 最新 state 镜像(卸载 flush 时读取,避免闭包过期值) */
8152
+ /** 最新 state 镜像(commit/卸载 flush 读取,避免闭包过期值) */
8157
8153
  const stateRef = (0, react.useRef)(state);
8158
- const patch = (p) => setState((s) => {
8159
- const next = {
8160
- ...s,
8161
- ...p
8162
- };
8154
+ /** 挂载守卫:卸载后不再 setState(store 镜像仍执行,异步结果照常落库) */
8155
+ const mountedRef = (0, react.useRef)(true);
8156
+ /**
8157
+ * 统一提交入口:更新 stateRef → 挂载时 setState → **总是**镜像进 runStore。
8158
+ * 关键:镜像不依赖 effect flush —— 异步操作(下载/确认导入)完成回调在组件
8159
+ * 已卸载(切走 tab)时也能把结果(detail/importResult)写进 store,切回恢复。
8160
+ */
8161
+ const commit = (next) => {
8163
8162
  stateRef.current = next;
8164
- return next;
8163
+ if (mountedRef.current) setState(next);
8164
+ runStore.patch({ market: toMarketStoreSlice(next) });
8165
+ };
8166
+ const patch = (p) => commit({
8167
+ ...stateRef.current,
8168
+ ...p
8165
8169
  });
8166
- /** 状态镜像:任何 UI 状态变化同步进 runStore(切 tab 不丢 / 刷新恢复)。 */
8167
- (0, react.useEffect)(() => {
8168
- runStore.patch({ market: toMarketStoreSlice(state) });
8169
- }, [state]);
8170
- /** 卸载时最后镜像一次(防止「最后一次改动后立即切 tab」时镜像 effect 尚未 flush)。 */
8170
+ /** 卸载时置挂载守卫 + 最后镜像一次(防止「最后一次改动后立即切 tab」时丢状态)。 */
8171
8171
  (0, react.useEffect)(() => () => {
8172
+ mountedRef.current = false;
8172
8173
  runStore.patch({ market: toMarketStoreSlice(stateRef.current) });
8173
8174
  }, []);
8174
8175
  /** 内置市场 URL(单一权威;来自 Host 内置常量) */