dsh-config-manager 0.1.19 → 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 +319 -2
- package/lib/client.js +1271 -109
- 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 +408 -2
- package/lib/index.js.map +1 -1
- package/lib/sync/ancestor.d.ts +17 -0
- package/lib/sync/ancestor.js +64 -0
- package/lib/sync/ancestor.js.map +1 -0
- 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/merge.d.ts +42 -0
- package/lib/sync/merge.js +325 -0
- package/lib/sync/merge.js.map +1 -0
- package/lib/sync/review-queue.d.ts +51 -0
- package/lib/sync/review-queue.js +119 -0
- package/lib/sync/review-queue.js.map +1 -0
- package/lib/sync/risk.d.ts +59 -0
- package/lib/sync/risk.js +76 -0
- package/lib/sync/risk.js.map +1 -0
- package/lib/sync/sync-config.d.ts +7 -2
- package/lib/sync/sync-config.js +24 -4
- package/lib/sync/sync-config.js.map +1 -1
- package/lib/sync/sync-engine.d.ts +88 -2
- package/lib/sync/sync-engine.js +333 -8
- 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/sync/sync-state.d.ts +7 -2
- package/lib/sync/sync-state.js +9 -5
- package/lib/sync/sync-state.js.map +1 -1
- 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.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +155 -0
- package/src/client/sync/SyncSettingsView.tsx +241 -12
- package/src/client/sync/history-model.test.ts +82 -0
- package/src/client/sync/history-model.ts +112 -0
- package/src/client/sync/sync-api.test.ts +155 -0
- package/src/client/sync/sync-api.ts +219 -0
- 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 +420 -3
- package/src/sync/ancestor.test.ts +152 -0
- package/src/sync/ancestor.ts +81 -0
- 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/merge.test.ts +204 -0
- package/src/sync/merge.ts +360 -0
- package/src/sync/review-queue.test.ts +120 -0
- package/src/sync/review-queue.ts +167 -0
- package/src/sync/risk.test.ts +131 -0
- package/src/sync/risk.ts +122 -0
- package/src/sync/sync-config.test.ts +106 -0
- package/src/sync/sync-config.ts +23 -4
- package/src/sync/sync-engine.test.ts +392 -3
- package/src/sync/sync-engine.ts +383 -10
- 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/sync/sync-state.test.ts +48 -2
- package/src/sync/sync-state.ts +11 -5
- package/src/ui/i18n.ts +132 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m-sync-ui (方案 A):一键同步差异确认 + 自动同步 渲染模型纯函数测试。
|
|
3
|
+
* TDD:先写失败测试,再实现 sync-view.ts 对应函数。
|
|
4
|
+
*/
|
|
5
|
+
import test from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
|
|
8
|
+
import type { ApplyItemsResponse, AutosyncStatusResponse, SyncConfirmItem } from './sync-api.ts'
|
|
9
|
+
import {
|
|
10
|
+
applyItemsReportView, autosyncIntervalMs, autosyncStatusText, buildAdoptions,
|
|
11
|
+
computeAutosyncCountdown, summarizeConfirmItems,
|
|
12
|
+
} from './sync-view.ts'
|
|
13
|
+
|
|
14
|
+
/* ---------------------------------------------------------------- 一键同步差异确认 */
|
|
15
|
+
|
|
16
|
+
function confirmItem(overrides: Partial<SyncConfirmItem>): SyncConfirmItem {
|
|
17
|
+
return {
|
|
18
|
+
itemId: 'a', adapter: 'settings', kind: 'Update', description: 'd', severity: 'info',
|
|
19
|
+
defaultAdopt: true, adopt: true, ...overrides,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('sync-view: summarizeConfirmItems 统计 severity + 采用数 + needsReview', () => {
|
|
24
|
+
const items: SyncConfirmItem[] = [
|
|
25
|
+
confirmItem({ itemId: 'a', severity: 'info', kind: 'Update' }),
|
|
26
|
+
confirmItem({ itemId: 'b', severity: 'warning', kind: 'MissingDependency', defaultAdopt: false, adopt: false }),
|
|
27
|
+
confirmItem({ itemId: 'c', severity: 'error', kind: 'Error', defaultAdopt: false, adopt: false }),
|
|
28
|
+
confirmItem({ itemId: 'd', severity: 'info', kind: 'Conflict', defaultAdopt: false, adopt: false }),
|
|
29
|
+
]
|
|
30
|
+
const s = summarizeConfirmItems(items)
|
|
31
|
+
assert.equal(s.total, 4)
|
|
32
|
+
assert.equal(s.info, 2)
|
|
33
|
+
assert.equal(s.warning, 1)
|
|
34
|
+
assert.equal(s.error, 1)
|
|
35
|
+
assert.equal(s.adopted, 1)
|
|
36
|
+
assert.equal(s.needsReview, true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('sync-view: summarizeConfirmItems 空数组 → 全零 + 不需决策', () => {
|
|
40
|
+
const s = summarizeConfirmItems([])
|
|
41
|
+
assert.equal(s.total, 0)
|
|
42
|
+
assert.equal(s.adopted, 0)
|
|
43
|
+
assert.equal(s.needsReview, false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('sync-view: buildAdoptions 收集用户决策;未列出项视为 adopt=false', () => {
|
|
47
|
+
const items: SyncConfirmItem[] = [
|
|
48
|
+
confirmItem({ itemId: 'a', kind: 'Update' }),
|
|
49
|
+
confirmItem({ itemId: 'b', kind: 'Create' }),
|
|
50
|
+
confirmItem({ itemId: 'c', kind: 'Conflict', defaultAdopt: false }),
|
|
51
|
+
]
|
|
52
|
+
const adopted = new Map<string, boolean>([
|
|
53
|
+
['a', true],
|
|
54
|
+
['b', false],
|
|
55
|
+
['c', true],
|
|
56
|
+
])
|
|
57
|
+
const resolutions = new Map<string, 'useRemote' | 'keepLocal' | 'skip'>([['c', 'useRemote']])
|
|
58
|
+
const out = buildAdoptions(items, adopted, resolutions)
|
|
59
|
+
// 只有 adopt=true 的项进列表(a、c);b 未采纳剔除
|
|
60
|
+
const ids = out.map((o) => o.itemId)
|
|
61
|
+
assert.deepEqual(ids, ['a', 'c'])
|
|
62
|
+
assert.equal(out[1]?.resolution, 'useRemote')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('sync-view: buildAdoptions Conflict 项未给 resolution → 抛错(强制先解决)', () => {
|
|
66
|
+
const items: SyncConfirmItem[] = [
|
|
67
|
+
confirmItem({ itemId: 'c', kind: 'Conflict', defaultAdopt: false }),
|
|
68
|
+
]
|
|
69
|
+
const adopted = new Map<string, boolean>([['c', true]])
|
|
70
|
+
const resolutions = new Map<string, 'useRemote' | 'keepLocal' | 'skip'>()
|
|
71
|
+
assert.throws(() => buildAdoptions(items, adopted, resolutions), /解决/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('sync-view: applyItemsReportView 成功 → ok 头部含 applied 计数与 restoreId', () => {
|
|
75
|
+
const report: ApplyItemsResponse = {
|
|
76
|
+
ok: true, applied: ['settings', 'plugins'], skipped: [], needsRestart: false,
|
|
77
|
+
warnings: [], restoreId: 'rest-1', rolledBack: false, failed: [], result: {},
|
|
78
|
+
}
|
|
79
|
+
const view = applyItemsReportView(report)
|
|
80
|
+
assert.notEqual(view, null)
|
|
81
|
+
assert.equal(view?.kind, 'ok')
|
|
82
|
+
assert.match(view?.headline ?? '', /2/)
|
|
83
|
+
assert.equal(view?.restoreId, 'rest-1')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('sync-view: applyItemsReportView 失败且整体回滚 → rolledBack 渲染', () => {
|
|
87
|
+
const report: ApplyItemsResponse = {
|
|
88
|
+
ok: false, applied: [], skipped: [], needsRestart: false,
|
|
89
|
+
warnings: [], restoreId: 'rest-2', rolledBack: true,
|
|
90
|
+
failed: [{ itemId: 'x', message: '导入失败' }], result: {},
|
|
91
|
+
}
|
|
92
|
+
const view = applyItemsReportView(report)
|
|
93
|
+
assert.equal(view?.kind, 'rolledBack')
|
|
94
|
+
assert.equal(view?.restoreId, 'rest-2')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('sync-view: applyItemsReportView null → null', () => {
|
|
98
|
+
assert.equal(applyItemsReportView(null), null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
/* ---------------------------------------------------------------- 自动同步 */
|
|
102
|
+
|
|
103
|
+
test('sync-view: autosyncIntervalMs 换算正确', () => {
|
|
104
|
+
assert.equal(autosyncIntervalMs('5m'), 5 * 60 * 1000)
|
|
105
|
+
assert.equal(autosyncIntervalMs('30m'), 30 * 60 * 1000)
|
|
106
|
+
assert.equal(autosyncIntervalMs('24h'), 24 * 60 * 60 * 1000)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('sync-view: computeAutosyncCountdown 距上次已过超过间隔 → 0(到期立即)', () => {
|
|
110
|
+
assert.equal(computeAutosyncCountdown(30 * 60 * 1000, 30 * 60 * 1000), 0)
|
|
111
|
+
assert.equal(computeAutosyncCountdown(31 * 60 * 1000, 30 * 60 * 1000), 0)
|
|
112
|
+
assert.equal(computeAutosyncCountdown(10 * 60 * 1000, 30 * 60 * 1000), 20 * 60 * 1000)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('sync-view: autosyncStatusText 覆盖未运行 / 各状态', () => {
|
|
116
|
+
const status: AutosyncStatusResponse = {
|
|
117
|
+
enabled: true, interval: '30m', consecutiveFailures: 0, elapsedMs: -1,
|
|
118
|
+
}
|
|
119
|
+
assert.match(autosyncStatusText(status), /从未运行/)
|
|
120
|
+
const ran: AutosyncStatusResponse = {
|
|
121
|
+
enabled: true, interval: '30m', consecutiveFailures: 0, elapsedMs: 0,
|
|
122
|
+
lastRunAt: '2026-08-17T10:00:00.000Z', lastRunStatus: 'success',
|
|
123
|
+
}
|
|
124
|
+
assert.match(autosyncStatusText(ran), /成功/)
|
|
125
|
+
const failed: AutosyncStatusResponse = {
|
|
126
|
+
enabled: true, interval: '30m', consecutiveFailures: 3, elapsedMs: 0,
|
|
127
|
+
lastRunAt: '2026-08-17T10:00:00.000Z', lastRunStatus: 'failed', lastRunMessage: '认证失败',
|
|
128
|
+
}
|
|
129
|
+
assert.match(autosyncStatusText(failed), /失败/)
|
|
130
|
+
assert.match(autosyncStatusText(failed), /连续失败 3 次/)
|
|
131
|
+
})
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { PlanItem, PlanItemKind } from '../../core/types.ts';
|
|
9
9
|
import type { PullChange, SyncPullReport, SyncPushReport } from '../../sync/sync-engine.ts';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
ApplyItemsResponse, AutosyncInterval, AutosyncStatusResponse, GithubPollResponse, SyncConfirmItem,
|
|
12
|
+
SyncItemAdoption, SyncStatusResponse,
|
|
13
|
+
} from './sync-api.ts';
|
|
11
14
|
import { zhUiT, type UiT } from '../../ui/i18n.ts';
|
|
12
15
|
|
|
13
16
|
/* ---------------------------------------------------------------- 私有仓库提示 */
|
|
@@ -94,15 +97,15 @@ export interface SyncButtons {
|
|
|
94
97
|
* - 仓库地址为空 → 禁用(无仓库无从同步);
|
|
95
98
|
* - busy 时按钮文案切换为「正在推送/拉取…」(配 Spinner)。
|
|
96
99
|
*/
|
|
97
|
-
export function computeSyncButtons(busy: 'push' | 'pull' | null, repoUrl: string, t: UiT = zhUiT): SyncButtons {
|
|
100
|
+
export function computeSyncButtons(busy: 'sync' | 'push' | 'pull' | 'apply' | 'rollback' | null, repoUrl: string, t: UiT = zhUiT): SyncButtons {
|
|
98
101
|
const idle = busy === null;
|
|
99
102
|
const repoOk = repoUrl.trim() !== '';
|
|
100
103
|
const enabled = idle && repoOk;
|
|
101
104
|
return {
|
|
102
105
|
canPush: enabled,
|
|
103
106
|
canPull: enabled,
|
|
104
|
-
pushLabel: busy === 'push' ? t('sync.pushing') : t('sync.pushLabel'),
|
|
105
|
-
pullLabel: busy === 'pull' ? t('sync.pulling') : t('sync.pullLabel'),
|
|
107
|
+
pushLabel: busy === 'push' ? t('sync.pushing') : busy === 'sync' ? t('sync.syncing') : t('sync.pushLabel'),
|
|
108
|
+
pullLabel: busy === 'pull' ? t('sync.pulling') : busy === 'sync' ? t('sync.syncing') : t('sync.pullLabel'),
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -290,3 +293,147 @@ export function githubPollMessage(poll: GithubPollResponse, t: UiT = zhUiT): str
|
|
|
290
293
|
return '';
|
|
291
294
|
}
|
|
292
295
|
}
|
|
296
|
+
|
|
297
|
+
/* ---------------------------------------------------------------- 一键同步差异确认(方案 A) */
|
|
298
|
+
|
|
299
|
+
/** 需要人工决策的 PlanItemKind(与 Host /sync/sync 的 needsReview 判定对齐)。 */
|
|
300
|
+
const CONFIRM_REVIEW_KINDS: ReadonlySet<PlanItemKind> = new Set([
|
|
301
|
+
'Conflict', 'MissingSecret', 'MissingDependency', 'Install', 'Error', 'PathMapping',
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
export interface SyncConfirmSummary {
|
|
305
|
+
total: number;
|
|
306
|
+
info: number;
|
|
307
|
+
warning: number;
|
|
308
|
+
error: number;
|
|
309
|
+
/** 默认/当前采用数(adopt=true 的项数)。 */
|
|
310
|
+
adopted: number;
|
|
311
|
+
/** 是否包含任何需人工决策项。 */
|
|
312
|
+
needsReview: boolean;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 差异确认列表摘要(按 severity 计数 + 采用数 + needsReview 徽章数据源)。 */
|
|
316
|
+
export function summarizeConfirmItems(items: readonly SyncConfirmItem[]): SyncConfirmSummary {
|
|
317
|
+
let info = 0;
|
|
318
|
+
let warning = 0;
|
|
319
|
+
let error = 0;
|
|
320
|
+
let adopted = 0;
|
|
321
|
+
let needsReview = false;
|
|
322
|
+
for (const it of items) {
|
|
323
|
+
if (it.severity === 'error') error += 1;
|
|
324
|
+
else if (it.severity === 'warning') warning += 1;
|
|
325
|
+
else info += 1;
|
|
326
|
+
if (it.adopt) adopted += 1;
|
|
327
|
+
if (CONFIRM_REVIEW_KINDS.has(it.kind)) needsReview = true;
|
|
328
|
+
}
|
|
329
|
+
return { total: items.length, info, warning, error, adopted, needsReview };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 收集用户逐项决策 → apply-items 请求体 adoptions[]。
|
|
334
|
+
* 仅包含 adopt=true 的项;Conflict 项 adopt=true 且未给 resolution → 抛错(强制先解决)。
|
|
335
|
+
*/
|
|
336
|
+
export function buildAdoptions(
|
|
337
|
+
items: readonly SyncConfirmItem[],
|
|
338
|
+
adopted: ReadonlyMap<string, boolean>,
|
|
339
|
+
resolutions: ReadonlyMap<string, 'useRemote' | 'keepLocal' | 'skip'>,
|
|
340
|
+
): SyncItemAdoption[] {
|
|
341
|
+
const out: SyncItemAdoption[] = [];
|
|
342
|
+
for (const it of items) {
|
|
343
|
+
if (adopted.get(it.itemId) !== true) continue; // adopt=false / 未列出 → 跳过
|
|
344
|
+
const adoption: SyncItemAdoption = { itemId: it.itemId, adopt: true };
|
|
345
|
+
if (it.kind === 'Conflict') {
|
|
346
|
+
const resolution = resolutions.get(it.itemId);
|
|
347
|
+
if (resolution === undefined) {
|
|
348
|
+
throw new Error(`冲突项 ${it.itemId} 必须先选择解决方式(用本地 / 用远端 / 跳过)`);
|
|
349
|
+
}
|
|
350
|
+
if (resolution !== 'skip') adoption.resolution = resolution;
|
|
351
|
+
}
|
|
352
|
+
out.push(adoption);
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export type ApplyItemsViewKind = 'ok' | 'failed' | 'rolledBack';
|
|
358
|
+
|
|
359
|
+
export interface ApplyItemsView {
|
|
360
|
+
kind: ApplyItemsViewKind;
|
|
361
|
+
headline: string;
|
|
362
|
+
sections: string[];
|
|
363
|
+
warnings: string[];
|
|
364
|
+
restoreId: string;
|
|
365
|
+
needsRestart: boolean;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** apply-items 执行结果 → 渲染模型(ok / failed / 整体回滚)。 */
|
|
369
|
+
export function applyItemsReportView(
|
|
370
|
+
report: ApplyItemsResponse | null,
|
|
371
|
+
t: UiT = zhUiT,
|
|
372
|
+
): ApplyItemsView | null {
|
|
373
|
+
if (report === null) return null;
|
|
374
|
+
const failedOnly = report.failed.length > 0 && !report.ok;
|
|
375
|
+
const kind: ApplyItemsViewKind = !report.ok && report.rolledBack ? 'rolledBack' : failedOnly ? 'failed' : 'ok';
|
|
376
|
+
const headline = kind === 'ok'
|
|
377
|
+
? t('sync.importDone', { n: String(report.applied.length) })
|
|
378
|
+
: kind === 'rolledBack'
|
|
379
|
+
? t('sync.importFailed')
|
|
380
|
+
: t('sync.importFailed');
|
|
381
|
+
return {
|
|
382
|
+
kind,
|
|
383
|
+
headline,
|
|
384
|
+
sections: report.applied,
|
|
385
|
+
warnings: report.warnings,
|
|
386
|
+
restoreId: report.restoreId,
|
|
387
|
+
needsRestart: report.needsRestart,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ---------------------------------------------------------------- 自动同步(方案 A) */
|
|
392
|
+
|
|
393
|
+
/** AutosyncInterval → ms。 */
|
|
394
|
+
export function autosyncIntervalMs(interval: AutosyncInterval): number {
|
|
395
|
+
switch (interval) {
|
|
396
|
+
case '5m': return 5 * 60 * 1000;
|
|
397
|
+
case '15m': return 15 * 60 * 1000;
|
|
398
|
+
case '60m': return 60 * 60 * 1000;
|
|
399
|
+
case '6h': return 6 * 60 * 60 * 1000;
|
|
400
|
+
case '12h': return 12 * 60 * 60 * 1000;
|
|
401
|
+
case '24h': return 24 * 60 * 60 * 1000;
|
|
402
|
+
default: return 30 * 60 * 1000;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 距下次自动同步剩余 ms(已到期 → 0)。elapsedMs 为 host 计算的「距上次执行已过 ms」。 */
|
|
407
|
+
export function computeAutosyncCountdown(elapsedMs: number, intervalMs: number): number {
|
|
408
|
+
if (elapsedMs < 0) return -1; // 从未运行
|
|
409
|
+
return Math.max(0, intervalMs - elapsedMs);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 间隔 ms → 可读时长(如「30 分钟」「6 小时」)。 */
|
|
413
|
+
export function formatIntervalDuration(ms: number, t: UiT = zhUiT): string {
|
|
414
|
+
const minutes = Math.round(ms / 60000);
|
|
415
|
+
if (minutes < 60) return t('sync.interval.30m');
|
|
416
|
+
const hours = Math.round(minutes / 60);
|
|
417
|
+
if (hours === 6) return t('sync.interval.6h');
|
|
418
|
+
if (hours === 12) return t('sync.interval.12h');
|
|
419
|
+
if (hours === 24) return t('sync.interval.24h');
|
|
420
|
+
return `${hours}h`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 自动同步状态行的可读文案(未运行 / 上次状态 / 连续失败计数)。 */
|
|
424
|
+
export function autosyncStatusText(status: AutosyncStatusResponse, t: UiT = zhUiT): string {
|
|
425
|
+
if (status.lastRunAt === undefined || status.lastRunAt === '' || status.lastRunStatus === undefined) {
|
|
426
|
+
return t('sync.autosyncNever');
|
|
427
|
+
}
|
|
428
|
+
const statusText = status.lastRunStatus === 'success'
|
|
429
|
+
? t('sync.autosyncSuccess')
|
|
430
|
+
: status.lastRunStatus === 'skipped'
|
|
431
|
+
? t('sync.autosyncSkipped')
|
|
432
|
+
: status.lastRunStatus === 'partial'
|
|
433
|
+
? t('sync.autosyncPartial')
|
|
434
|
+
: t('sync.autosyncFailed');
|
|
435
|
+
const time = formatDateTime(status.lastRunAt);
|
|
436
|
+
const base = t('sync.autosyncLastRun', { time });
|
|
437
|
+
const fail = status.consecutiveFailures > 0 ? ` · ${t('sync.autosyncFailCount', { n: String(status.consecutiveFailures) })}` : '';
|
|
438
|
+
return `${statusText} · ${base}${fail}`;
|
|
439
|
+
}
|
package/src/core/run-registry.ts
CHANGED
|
@@ -21,8 +21,8 @@ import { randomBytes } from 'node:crypto'
|
|
|
21
21
|
import { zhMsg } from './messages.ts'
|
|
22
22
|
import type { MsgFunc } from './messages.ts'
|
|
23
23
|
|
|
24
|
-
/** run 类型:导出 /
|
|
25
|
-
export type RunKind = 'export' | 'import'
|
|
24
|
+
/** run 类型:导出 / 导入 / 自动同步 / 一键同步逐项应用。 */
|
|
25
|
+
export type RunKind = 'export' | 'import' | 'autosync' | 'sync-apply'
|
|
26
26
|
|
|
27
27
|
/** run 状态:进行中 / 完成 / 失败。 */
|
|
28
28
|
export type RunStatus = 'running' | 'done' | 'failed'
|
|
@@ -59,7 +59,11 @@ export class RunConflictError extends Error {
|
|
|
59
59
|
super(
|
|
60
60
|
kind === 'export'
|
|
61
61
|
? msg('run.conflict.export', { runId })
|
|
62
|
-
:
|
|
62
|
+
: kind === 'autosync'
|
|
63
|
+
? '自动同步已在进行中'
|
|
64
|
+
: kind === 'sync-apply'
|
|
65
|
+
? '一键同步已在进行中'
|
|
66
|
+
: msg('run.conflict.import', { runId }),
|
|
63
67
|
)
|
|
64
68
|
this.name = 'RunConflictError'
|
|
65
69
|
this.runId = runId
|