dsh-config-manager 0.1.20 → 0.1.21
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.d.ts +312 -46
- package/lib/client.js +1172 -205
- package/lib/client.js.map +1 -1
- package/lib/core/run-registry.d.ts +2 -2
- package/lib/core/run-registry.js +5 -1
- package/lib/core/run-registry.js.map +1 -1
- package/lib/index.js +292 -19
- package/lib/index.js.map +1 -1
- package/lib/sync/autosync-config.d.ts +34 -0
- package/lib/sync/autosync-config.js +116 -0
- package/lib/sync/autosync-config.js.map +1 -0
- package/lib/sync/autosync-scheduler.d.ts +95 -0
- package/lib/sync/autosync-scheduler.js +379 -0
- package/lib/sync/autosync-scheduler.js.map +1 -0
- package/lib/sync/sync-engine.d.ts +59 -4
- package/lib/sync/sync-engine.js +138 -12
- package/lib/sync/sync-engine.js.map +1 -1
- package/lib/sync/sync-history.d.ts +39 -0
- package/lib/sync/sync-history.js +88 -0
- package/lib/sync/sync-history.js.map +1 -0
- package/lib/sync/sync-session.d.ts +39 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-session.js.map +1 -0
- package/lib/ui/i18n.d.ts +63 -0
- package/lib/ui/i18n.js +130 -1
- package/lib/ui/i18n.js.map +1 -1
- package/package.json +1 -1
- package/src/client/config-manager.module.css +18 -0
- package/src/client/sync/SyncConfirmView.tsx +301 -0
- package/src/client/sync/SyncHistoryView.tsx +107 -25
- package/src/client/sync/SyncSettingsView.tsx +226 -83
- package/src/client/sync/history-model.test.ts +82 -0
- package/src/client/sync/history-model.ts +89 -2
- package/src/client/sync/sync-api.test.ts +155 -0
- package/src/client/sync/sync-api.ts +208 -16
- package/src/client/sync/sync-locales.ts +162 -1
- package/src/client/sync/sync-view-v2.test.ts +131 -0
- package/src/client/sync/sync-view.ts +151 -4
- package/src/core/run-registry.ts +7 -3
- package/src/index.ts +314 -22
- package/src/sync/autosync-config.test.ts +93 -0
- package/src/sync/autosync-config.ts +137 -0
- package/src/sync/autosync-scheduler.test.ts +191 -0
- package/src/sync/autosync-scheduler.ts +443 -0
- package/src/sync/sync-engine.test.ts +107 -7
- package/src/sync/sync-engine.ts +176 -14
- package/src/sync/sync-history.test.ts +85 -0
- package/src/sync/sync-history.ts +126 -0
- package/src/sync/sync-session.test.ts +137 -0
- package/src/sync/sync-session.ts +76 -0
- package/src/ui/i18n.ts +132 -3
- package/src/client/sync/SyncPullPreviewView.test.ts +0 -54
- package/src/client/sync/SyncPullPreviewView.tsx +0 -165
- package/src/client/sync/pull-preview-model.ts +0 -44
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MsgFunc } from './messages.ts';
|
|
2
|
-
/** run 类型:导出 /
|
|
3
|
-
export type RunKind = 'export' | 'import';
|
|
2
|
+
/** run 类型:导出 / 导入 / 自动同步 / 一键同步逐项应用。 */
|
|
3
|
+
export type RunKind = 'export' | 'import' | 'autosync' | 'sync-apply';
|
|
4
4
|
/** run 状态:进行中 / 完成 / 失败。 */
|
|
5
5
|
export type RunStatus = 'running' | 'done' | 'failed';
|
|
6
6
|
/** 单个 run 的实时状态(/progress 返回体;纯 JSON 可序列化)。 */
|
package/lib/core/run-registry.js
CHANGED
|
@@ -26,7 +26,11 @@ export class RunConflictError extends Error {
|
|
|
26
26
|
constructor(runId, kind, msg = zhMsg) {
|
|
27
27
|
super(kind === 'export'
|
|
28
28
|
? msg('run.conflict.export', { runId })
|
|
29
|
-
:
|
|
29
|
+
: kind === 'autosync'
|
|
30
|
+
? '自动同步已在进行中'
|
|
31
|
+
: kind === 'sync-apply'
|
|
32
|
+
? '一键同步已在进行中'
|
|
33
|
+
: msg('run.conflict.import', { runId }));
|
|
30
34
|
this.name = 'RunConflictError';
|
|
31
35
|
this.runId = runId;
|
|
32
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-registry.js","sourceRoot":"","sources":["../../src/core/run-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AAiCrC,0CAA0C;AAC1C,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,iCAAiC;IACxB,KAAK,CAAQ;IACtB,YAAY,KAAa,EAAE,IAAa,EAAE,MAAe,KAAK;QAC5D,KAAK,CACH,IAAI,KAAK,QAAQ;YACf,CAAC,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC;YACvC,CAAC,CAAC,GAAG,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"run-registry.js","sourceRoot":"","sources":["../../src/core/run-registry.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -55,9 +55,12 @@ import { createHardenedZipParser } from "./security/zip-security.js";
|
|
|
55
55
|
import { GitTransport } from "./sync/git/git-transport.js";
|
|
56
56
|
import { DeviceFlowStore, GitHubAuthClient } from "./sync/github-auth.js";
|
|
57
57
|
import { SyncEngine } from "./sync/sync-engine.js";
|
|
58
|
+
import { SyncSessionStore } from "./sync/sync-session.js";
|
|
59
|
+
import { AutoSyncScheduler } from "./sync/autosync-scheduler.js";
|
|
60
|
+
import { defaultAutosyncConfig, readAutosyncConfig, writeAutosyncConfig } from "./sync/autosync-config.js";
|
|
61
|
+
import { appendAutosyncEntry, readSyncHistory } from "./sync/sync-history.js";
|
|
58
62
|
import { loadSyncState, saveSyncState } from "./sync/sync-state.js";
|
|
59
63
|
import { readSyncConfig, writeSyncConfig, validateRepoUrl } from "./sync/sync-config.js";
|
|
60
|
-
import { classifyMergePlan } from "./sync/risk.js";
|
|
61
64
|
import { MANIFEST_FILE, parseManifest } from "./schema/manifest.js";
|
|
62
65
|
import { SECTION_IDS } from "./schema/config.js";
|
|
63
66
|
import { parseZip } from "./utils/zip.js";
|
|
@@ -101,8 +104,13 @@ const API = {
|
|
|
101
104
|
syncGithubCancel: '/api/dsh-config-manager/sync/github/cancel',
|
|
102
105
|
// P2:同步历史 / 自动应用 / 一键回滚
|
|
103
106
|
syncHistory: '/api/dsh-config-manager/sync/history',
|
|
104
|
-
syncApply: '/api/dsh-config-manager/sync/apply',
|
|
105
107
|
syncRollback: '/api/dsh-config-manager/sync/rollback',
|
|
108
|
+
// m-sync-v2:一键同步(差异确认会话)+ 自动同步 + 历史快照
|
|
109
|
+
syncSnapshotsList: '/api/dsh-config-manager/sync/snapshots-list',
|
|
110
|
+
syncSync: '/api/dsh-config-manager/sync/sync',
|
|
111
|
+
syncApplyItems: '/api/dsh-config-manager/sync/apply-items',
|
|
112
|
+
syncCancel: '/api/dsh-config-manager/sync/cancel',
|
|
113
|
+
syncAutosync: '/api/dsh-config-manager/sync/autosync',
|
|
106
114
|
};
|
|
107
115
|
/**
|
|
108
116
|
* 同步 token 的 DSH credentials 引用名(POSIX env-var 形态,满足 CredentialRef 品牌要求)。
|
|
@@ -728,6 +736,59 @@ export function writeSyncRouteError(res, error) {
|
|
|
728
736
|
const message = error instanceof Error ? error.message : String(error);
|
|
729
737
|
writeJson(res, 500, { error: message });
|
|
730
738
|
}
|
|
739
|
+
/** 需要人工决策的 PlanItemKind(一键同步 needsReview 判定 + 逐项确认标记) */
|
|
740
|
+
const REVIEW_KINDS = new Set([
|
|
741
|
+
'Conflict', 'MissingSecret', 'MissingDependency', 'Install', 'Error', 'PathMapping',
|
|
742
|
+
]);
|
|
743
|
+
/** 把 ImportPlan 投影为逐项可确认的差异项(默认采用 Create/Update/Install;人工项默认不采用)。 */
|
|
744
|
+
function planToConfirmItems(plan) {
|
|
745
|
+
return plan.items.map((item) => {
|
|
746
|
+
const manual = REVIEW_KINDS.has(item.kind);
|
|
747
|
+
let conflict;
|
|
748
|
+
if (item.kind === 'Conflict') {
|
|
749
|
+
const c = item.conflict;
|
|
750
|
+
conflict = {
|
|
751
|
+
path: c?.path ?? '$',
|
|
752
|
+
kind: c?.kind === 'file' ? 'file' : c?.kind === 'section' ? 'section' : 'key',
|
|
753
|
+
...(c?.local !== undefined ? { local: c.local } : {}),
|
|
754
|
+
...(c?.remote !== undefined ? { remote: c.remote } : {}),
|
|
755
|
+
...(c?.ancestor !== undefined ? { ancestor: c.ancestor } : {}),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
itemId: item.id,
|
|
760
|
+
adapter: item.adapter,
|
|
761
|
+
kind: item.kind,
|
|
762
|
+
description: item.description,
|
|
763
|
+
severity: item.severity,
|
|
764
|
+
defaultAdopt: !manual,
|
|
765
|
+
adopt: !manual,
|
|
766
|
+
...(conflict !== undefined ? { conflict } : {}),
|
|
767
|
+
...(item.target !== undefined ? { target: item.target } : {}),
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
/** autosync interval 类型守卫 */
|
|
772
|
+
function isAutosyncInterval(v) {
|
|
773
|
+
return v === '5m' || v === '15m' || v === '30m' || v === '60m' || v === '6h' || v === '12h' || v === '24h';
|
|
774
|
+
}
|
|
775
|
+
/** 自动同步状态响应(GET /sync/autosync 与 POST 回填;读盘计算 elapsedMs)。 */
|
|
776
|
+
async function buildAutosyncStatus(dir) {
|
|
777
|
+
const cfg = await readAutosyncConfig(dir);
|
|
778
|
+
const elapsedMs = cfg.lastRunAt === undefined || cfg.lastRunAt === ''
|
|
779
|
+
? -1
|
|
780
|
+
: Math.max(0, Date.now() - Date.parse(cfg.lastRunAt));
|
|
781
|
+
return {
|
|
782
|
+
enabled: cfg.enabled,
|
|
783
|
+
interval: cfg.interval,
|
|
784
|
+
...(cfg.lastRunAt !== undefined ? { lastRunAt: cfg.lastRunAt } : {}),
|
|
785
|
+
...(cfg.lastRunStatus !== undefined ? { lastRunStatus: cfg.lastRunStatus } : {}),
|
|
786
|
+
...(cfg.lastRunMessage !== undefined ? { lastRunMessage: cfg.lastRunMessage } : {}),
|
|
787
|
+
consecutiveFailures: cfg.consecutiveFailures,
|
|
788
|
+
elapsedMs,
|
|
789
|
+
...(cfg.lastRunHistoryId !== undefined ? { lastRunHistoryId: cfg.lastRunHistoryId } : {}),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
731
792
|
export function buildRestoreBody(body) {
|
|
732
793
|
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
733
794
|
return { ok: false, error: 'invalid JSON body' };
|
|
@@ -920,6 +981,19 @@ function makeRoutes(deps) {
|
|
|
920
981
|
msg,
|
|
921
982
|
});
|
|
922
983
|
};
|
|
984
|
+
/** 一键同步差异确认会话存储(进程内存;/sync/sync 预览 → /sync/apply-items 逐项执行解耦) */
|
|
985
|
+
const syncSessions = new SyncSessionStore();
|
|
986
|
+
/** 自动同步后台调度器(宿主进程生命周期,不依赖浏览器) */
|
|
987
|
+
let scheduler;
|
|
988
|
+
scheduler = new AutoSyncScheduler({
|
|
989
|
+
syncDir,
|
|
990
|
+
host,
|
|
991
|
+
makeSyncEngine,
|
|
992
|
+
msg,
|
|
993
|
+
runs,
|
|
994
|
+
});
|
|
995
|
+
// 启动:读 autosync-config;若 enabled 启动定时器;无条件执行一次「启动触发下载合并」(受阈值约束)
|
|
996
|
+
scheduler.start();
|
|
923
997
|
return [
|
|
924
998
|
// ------------------------------------------------------------- status
|
|
925
999
|
{
|
|
@@ -1295,6 +1369,7 @@ function makeRoutes(deps) {
|
|
|
1295
1369
|
lastSyncAt: state.lastSyncAt === '' ? undefined : state.lastSyncAt,
|
|
1296
1370
|
sectionCount: Object.keys(state.sections).length,
|
|
1297
1371
|
transport: state.transport,
|
|
1372
|
+
autosync: await buildAutosyncStatus(syncDir),
|
|
1298
1373
|
});
|
|
1299
1374
|
}
|
|
1300
1375
|
catch (error) {
|
|
@@ -1521,22 +1596,29 @@ function makeRoutes(deps) {
|
|
|
1521
1596
|
catch { /* skip */ }
|
|
1522
1597
|
}
|
|
1523
1598
|
rows.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
1524
|
-
|
|
1599
|
+
// 合并自动同步执行记录(sync-history.json)
|
|
1600
|
+
const hist = await readSyncHistory(syncDir);
|
|
1601
|
+
const merged = [
|
|
1602
|
+
...rows.map((r) => ({ ...r, kind: 'apply' })),
|
|
1603
|
+
...hist.autosyncEntries.map((e) => ({
|
|
1604
|
+
id: e.createdAt,
|
|
1605
|
+
createdAt: e.createdAt,
|
|
1606
|
+
kind: 'autosync',
|
|
1607
|
+
autosync: e,
|
|
1608
|
+
})),
|
|
1609
|
+
].sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
1610
|
+
writeJson(res, 200, { entries: merged });
|
|
1525
1611
|
}
|
|
1526
1612
|
catch (error) {
|
|
1527
1613
|
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1528
1614
|
}
|
|
1529
1615
|
},
|
|
1530
1616
|
},
|
|
1531
|
-
// ------------------------------------------------------ sync/
|
|
1532
|
-
//
|
|
1533
|
-
// 1) engine.merge 拉取远端 → 三方合并 → classifyMergePlan(firstSync)
|
|
1534
|
-
// 2) engine.applyMergePlan(apply) → backup + Importer.executeImportPlan(rollbackOnError=true)
|
|
1535
|
-
// 失败 → rollback + enqueueItems → return ApplyReport{ok:false,rolledBack:true,review}
|
|
1536
|
-
// 3) 全成功 → recordBaseline + 标记 firstSyncCompleted
|
|
1617
|
+
// ------------------------------------------------------ sync/snapshots-list
|
|
1618
|
+
// m-sync-v2:远端历史快照列表(供「选择历史快照」下拉)。
|
|
1537
1619
|
{
|
|
1538
1620
|
kind: 'exact',
|
|
1539
|
-
path: API.
|
|
1621
|
+
path: API.syncSnapshotsList,
|
|
1540
1622
|
handler: async (req, res) => {
|
|
1541
1623
|
if (!guard(req, res, 'POST'))
|
|
1542
1624
|
return;
|
|
@@ -1548,17 +1630,208 @@ function makeRoutes(deps) {
|
|
|
1548
1630
|
try {
|
|
1549
1631
|
const { repoUrl, gitBin } = await prepareSync(body);
|
|
1550
1632
|
const engine = makeSyncEngine(repoUrl, gitBin);
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1633
|
+
const metas = await withTimeout(engine.listSnapshots(), ROUTE_TIMEOUT_MS, msg('host.syncPullTimeout'));
|
|
1634
|
+
const snapshots = [...metas]
|
|
1635
|
+
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0))
|
|
1636
|
+
.map((m) => ({
|
|
1637
|
+
id: m.id,
|
|
1638
|
+
createdAt: m.createdAt,
|
|
1639
|
+
sectionCount: m.manifest.sectionIds.length,
|
|
1640
|
+
platform: m.manifest.platform,
|
|
1641
|
+
dshVersion: m.manifest.dshVersion,
|
|
1642
|
+
}));
|
|
1553
1643
|
const state = await loadSyncState(syncDir);
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1644
|
+
writeJson(res, 200, { ok: true, snapshots, currentSnapshotId: state.lastSnapshotId === '' ? undefined : state.lastSnapshotId });
|
|
1645
|
+
}
|
|
1646
|
+
catch (error) {
|
|
1647
|
+
writeSyncRouteError(res, error);
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
// ------------------------------------------------------ sync/sync
|
|
1652
|
+
// m-sync-v2:一键同步第一步 —— 拉取 → 差异确认会话(内存登记临时 ZIP + ImportPlan)。
|
|
1653
|
+
{
|
|
1654
|
+
kind: 'exact',
|
|
1655
|
+
path: API.syncSync,
|
|
1656
|
+
handler: async (req, res) => {
|
|
1657
|
+
if (!guard(req, res, 'POST'))
|
|
1658
|
+
return;
|
|
1659
|
+
const body = await readJsonBody(req);
|
|
1660
|
+
if (body === undefined) {
|
|
1661
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
const { repoUrl, gitBin } = await prepareSync(body);
|
|
1666
|
+
const engine = makeSyncEngine(repoUrl, gitBin);
|
|
1667
|
+
const snapshotId = typeof body['snapshotId'] === 'string' && body['snapshotId'] !== '' ? body['snapshotId'] : undefined;
|
|
1668
|
+
const preview = await withTimeout(engine.preview(snapshotId === undefined ? {} : { snapshotId }), ROUTE_TIMEOUT_MS, msg('host.syncPullTimeout'));
|
|
1669
|
+
if (!preview.ok || preview.plan === null || preview.analysis === null) {
|
|
1670
|
+
writeJson(res, 200, { ok: false, syncSessionId: '', snapshotId: preview.snapshotId, items: [], needsReview: false, compatibility: 'unsupported', message: preview.message ?? '同步预览失败' });
|
|
1671
|
+
return;
|
|
1559
1672
|
}
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1673
|
+
const syncSessionId = syncSessions.set({
|
|
1674
|
+
zipPath: preview.zipPath,
|
|
1675
|
+
plan: preview.plan,
|
|
1676
|
+
analysis: preview.analysis,
|
|
1677
|
+
snapshotId: preview.snapshotId,
|
|
1678
|
+
repoUrl,
|
|
1679
|
+
gitBin,
|
|
1680
|
+
});
|
|
1681
|
+
const items = planToConfirmItems(preview.plan);
|
|
1682
|
+
const needsReview = items.some((i) => REVIEW_KINDS.has(i.kind)) || preview.analysis.pathIssues.length > 0;
|
|
1683
|
+
writeJson(res, 200, {
|
|
1684
|
+
ok: true,
|
|
1685
|
+
syncSessionId,
|
|
1686
|
+
snapshotId: preview.snapshotId,
|
|
1687
|
+
items,
|
|
1688
|
+
needsReview,
|
|
1689
|
+
compatibility: preview.analysis.compatibility,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
catch (error) {
|
|
1693
|
+
writeSyncRouteError(res, error);
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
},
|
|
1697
|
+
// ------------------------------------------------------ sync/apply-items
|
|
1698
|
+
// m-sync-v2:一键同步第二步 —— 按用户对差异项的逐项决策执行导入。
|
|
1699
|
+
{
|
|
1700
|
+
kind: 'exact',
|
|
1701
|
+
path: API.syncApplyItems,
|
|
1702
|
+
handler: async (req, res) => {
|
|
1703
|
+
if (!guard(req, res, 'POST'))
|
|
1704
|
+
return;
|
|
1705
|
+
const body = await readJsonBody(req);
|
|
1706
|
+
if (body === undefined) {
|
|
1707
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
try {
|
|
1711
|
+
const syncSessionId = typeof body['syncSessionId'] === 'string' ? body['syncSessionId'] : '';
|
|
1712
|
+
const session = syncSessions.get(syncSessionId);
|
|
1713
|
+
if (session === undefined) {
|
|
1714
|
+
writeJson(res, 400, { error: '同步会话不存在或已过期,请重新拉取预览' });
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const adoptions = Array.isArray(body['adoptions']) ? body['adoptions'] : [];
|
|
1718
|
+
// 构造子计划(仅含采纳项)
|
|
1719
|
+
const byId = new Map();
|
|
1720
|
+
for (const a of adoptions) {
|
|
1721
|
+
if (typeof a?.['itemId'] !== 'string')
|
|
1722
|
+
continue;
|
|
1723
|
+
byId.set(a['itemId'], { adopt: a['adopt'] === true, resolution: typeof a['resolution'] === 'string' ? a['resolution'] : undefined });
|
|
1724
|
+
}
|
|
1725
|
+
const subItems = session.plan.items.filter((item) => {
|
|
1726
|
+
const d = byId.get(item.id);
|
|
1727
|
+
if (d === undefined || !d.adopt)
|
|
1728
|
+
return false;
|
|
1729
|
+
// Conflict 项必须有 resolution,且 keepLocal/skip 从子计划剔除
|
|
1730
|
+
if (item.kind === 'Conflict') {
|
|
1731
|
+
if (d.resolution === undefined)
|
|
1732
|
+
throw new SyncRouteError(`冲突项 ${item.id} 必须提供 resolution(useRemote/keepLocal/skip)`);
|
|
1733
|
+
if (d.resolution === 'keepLocal' || d.resolution === 'skip')
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
return true;
|
|
1737
|
+
});
|
|
1738
|
+
const subPlan = {
|
|
1739
|
+
...session.plan,
|
|
1740
|
+
items: subItems,
|
|
1741
|
+
};
|
|
1742
|
+
// 消费会话(同一 session 只允许一次 apply-items)
|
|
1743
|
+
syncSessions.delete(syncSessionId);
|
|
1744
|
+
await fs.rm(dirname(session.zipPath), { recursive: true, force: true }).catch(() => { });
|
|
1745
|
+
const engine = makeSyncEngine(session.repoUrl, session.gitBin);
|
|
1746
|
+
const report = await engine.applyItems(session.zipPath, subPlan, {
|
|
1747
|
+
onItem: (info) => { },
|
|
1748
|
+
});
|
|
1749
|
+
writeJson(res, 200, {
|
|
1750
|
+
ok: report.ok,
|
|
1751
|
+
applied: report.applied,
|
|
1752
|
+
skipped: subItems.map((i) => i.id),
|
|
1753
|
+
needsRestart: report.needsRestart === true,
|
|
1754
|
+
warnings: report.warnings,
|
|
1755
|
+
restoreId: report.restoreId,
|
|
1756
|
+
rolledBack: report.rolledBack,
|
|
1757
|
+
failed: report.failed,
|
|
1758
|
+
result: report.result,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
writeSyncRouteError(res, error);
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
},
|
|
1766
|
+
// ------------------------------------------------------ sync/cancel
|
|
1767
|
+
// m-sync-v2:取消 / 清理差异确认会话(丢弃临时 ZIP,零副作用)。
|
|
1768
|
+
{
|
|
1769
|
+
kind: 'exact',
|
|
1770
|
+
path: API.syncCancel,
|
|
1771
|
+
handler: async (req, res) => {
|
|
1772
|
+
if (!guard(req, res, 'POST'))
|
|
1773
|
+
return;
|
|
1774
|
+
const body = await readJsonBody(req);
|
|
1775
|
+
if (body === undefined) {
|
|
1776
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
const syncSessionId = typeof body['syncSessionId'] === 'string' ? body['syncSessionId'] : '';
|
|
1781
|
+
if (syncSessionId !== '') {
|
|
1782
|
+
const session = syncSessions.get(syncSessionId);
|
|
1783
|
+
if (session !== undefined) {
|
|
1784
|
+
await fs.rm(dirname(session.zipPath), { recursive: true, force: true }).catch(() => { });
|
|
1785
|
+
}
|
|
1786
|
+
syncSessions.delete(syncSessionId);
|
|
1787
|
+
}
|
|
1788
|
+
writeJson(res, 200, { ok: true });
|
|
1789
|
+
}
|
|
1790
|
+
catch (error) {
|
|
1791
|
+
writeSyncRouteError(res, error);
|
|
1792
|
+
}
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
// ------------------------------------------------------ sync/autosync
|
|
1796
|
+
// m-sync-v2:自动同步配置读写(总开关 + 间隔 + 启动阈值 + 状态)。
|
|
1797
|
+
{
|
|
1798
|
+
kind: 'exact',
|
|
1799
|
+
path: API.syncAutosync,
|
|
1800
|
+
handler: async (req, res) => {
|
|
1801
|
+
if (!guard(req, res, 'GET'))
|
|
1802
|
+
return;
|
|
1803
|
+
try {
|
|
1804
|
+
writeJson(res, 200, await buildAutosyncStatus(syncDir));
|
|
1805
|
+
}
|
|
1806
|
+
catch (error) {
|
|
1807
|
+
writeSyncRouteError(res, error);
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
kind: 'exact',
|
|
1813
|
+
path: API.syncAutosync,
|
|
1814
|
+
handler: async (req, res) => {
|
|
1815
|
+
if (!guard(req, res, 'POST'))
|
|
1816
|
+
return;
|
|
1817
|
+
const body = await readJsonBody(req);
|
|
1818
|
+
if (body === undefined) {
|
|
1819
|
+
writeJson(res, 400, { error: 'invalid JSON body' });
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
const cfg = await readAutosyncConfig(syncDir);
|
|
1824
|
+
if (typeof body['enabled'] === 'boolean')
|
|
1825
|
+
cfg.enabled = body['enabled'];
|
|
1826
|
+
if (typeof body['interval'] === 'string' && isAutosyncInterval(body['interval']))
|
|
1827
|
+
cfg.interval = body['interval'];
|
|
1828
|
+
if (typeof body['startupMinIntervalMs'] === 'number' && Number.isFinite(body['startupMinIntervalMs']) && body['startupMinIntervalMs'] > 0) {
|
|
1829
|
+
cfg.startupMinIntervalMs = body['startupMinIntervalMs'];
|
|
1830
|
+
}
|
|
1831
|
+
await writeAutosyncConfig(syncDir, cfg);
|
|
1832
|
+
if (scheduler)
|
|
1833
|
+
scheduler.reload().catch(() => { });
|
|
1834
|
+
writeJson(res, 200, await buildAutosyncStatus(syncDir));
|
|
1562
1835
|
}
|
|
1563
1836
|
catch (error) {
|
|
1564
1837
|
writeSyncRouteError(res, error);
|