@tachybase/module-backup 1.5.1 → 1.6.1

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.
@@ -0,0 +1,286 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var progress_tracker_exports = {};
29
+ __export(progress_tracker_exports, {
30
+ ProgressManager: () => ProgressManager
31
+ });
32
+ module.exports = __toCommonJS(progress_tracker_exports);
33
+ var import_promises = __toESM(require("node:fs/promises"));
34
+ var import_node_path = __toESM(require("node:path"));
35
+ var import_server = require("@tego/server");
36
+ const _ProgressManager = class _ProgressManager {
37
+ constructor(backupStorageDir, workDir, app) {
38
+ this.backupStorageDir = backupStorageDir;
39
+ this.workDir = workDir;
40
+ this.app = app;
41
+ }
42
+ /**
43
+ * 获取进度文件路径(静态方法,用于不需要实例的场景)
44
+ */
45
+ static getProgressFilePath(filePath) {
46
+ return filePath + ".progress";
47
+ }
48
+ /**
49
+ * 获取进度文件路径
50
+ */
51
+ progressFilePath(fileName, appName) {
52
+ const progressFile = fileName + ".progress";
53
+ const dirname = this.backupStorageDir(appName);
54
+ return import_node_path.default.resolve(dirname, progressFile);
55
+ }
56
+ /**
57
+ * 通过 WebSocket 推送进度更新
58
+ * @param fileName 备份文件名
59
+ * @param progress 进度信息
60
+ * @param userId 用户ID(可选,如果未提供则不会推送,适用于自动备份场景)
61
+ * @param appName 应用名称
62
+ */
63
+ pushProgressViaWebSocket(fileName, progress, userId, appName) {
64
+ var _a, _b, _c, _d, _e;
65
+ if (!this.app) {
66
+ return;
67
+ }
68
+ if (!userId || userId <= 0) {
69
+ if ((_a = this.app) == null ? void 0 : _a.logger) {
70
+ this.app.logger.debug(`[ProgressManager] Skipping WebSocket push for ${fileName}: no userId`);
71
+ }
72
+ return;
73
+ }
74
+ try {
75
+ const gateway = import_server.Gateway.getInstance();
76
+ const ws = gateway["wsServer"];
77
+ if (!ws) {
78
+ if (!_ProgressManager.wsUnavailableWarned) {
79
+ _ProgressManager.wsUnavailableWarned = true;
80
+ if ((_b = this.app) == null ? void 0 : _b.logger) {
81
+ this.app.logger.warn(
82
+ "[ProgressManager] WebSocket server not available (this is normal in worker threads, progress will be saved to file only)"
83
+ );
84
+ }
85
+ }
86
+ return;
87
+ }
88
+ const finalAppName = appName || this.app.name;
89
+ const tagPrefix = `app:${finalAppName}`;
90
+ const tagValue = `${userId}`;
91
+ const expectedTag = `${tagPrefix}#${tagValue}`;
92
+ if ((_c = this.app) == null ? void 0 : _c.logger) {
93
+ const matchingConnections = Array.from(ws.webSocketClients.values()).filter((client) => {
94
+ var _a2, _b2;
95
+ return (_b2 = (_a2 = client.tags) == null ? void 0 : _a2.has) == null ? void 0 : _b2.call(_a2, expectedTag);
96
+ });
97
+ this.app.logger.debug(
98
+ `[ProgressManager] Sending backup progress via WebSocket: fileName=${fileName}, userId=${userId}, appName=${finalAppName}, tag=${expectedTag}, progress=${progress.percent}%, matchingConnections=${matchingConnections.length}`
99
+ );
100
+ if (matchingConnections.length === 0) {
101
+ const allTags = Array.from(ws.webSocketClients.values()).flatMap((client) => Array.from(client.tags || [])).filter((tag) => typeof tag === "string" && tag.startsWith("app:"));
102
+ this.app.logger.warn(
103
+ `[ProgressManager] No matching connections found for tag ${expectedTag}. Available tags: ${allTags.join(", ")}`
104
+ );
105
+ }
106
+ }
107
+ try {
108
+ ws.sendToConnectionsByTag(tagPrefix, tagValue, {
109
+ type: "backup:progress",
110
+ payload: {
111
+ fileName,
112
+ progress
113
+ }
114
+ });
115
+ } catch (sendError) {
116
+ if ((_d = this.app) == null ? void 0 : _d.logger) {
117
+ this.app.logger.error(
118
+ `[ProgressManager] Failed to send WebSocket message: tagPrefix=${tagPrefix}, tagValue=${tagValue}, error=`,
119
+ sendError
120
+ );
121
+ }
122
+ throw sendError;
123
+ }
124
+ } catch (error) {
125
+ if ((_e = this.app) == null ? void 0 : _e.logger) {
126
+ this.app.logger.warn(`[ProgressManager] WebSocket push error for ${fileName} (userId=${userId}):`, error);
127
+ } else {
128
+ console.error(`[ProgressManager] WebSocket push error for ${fileName} (userId=${userId}):`, error);
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * 写入进度信息
134
+ */
135
+ async writeProgress(fileName, progress, appName, userId) {
136
+ const filePath = this.progressFilePath(fileName, appName);
137
+ await import_promises.default.writeFile(filePath, JSON.stringify(progress), "utf8");
138
+ this.pushProgressViaWebSocket(fileName, progress, userId, appName);
139
+ }
140
+ /**
141
+ * 读取进度信息
142
+ */
143
+ async readProgress(fileName, appName) {
144
+ const filePath = this.progressFilePath(fileName, appName);
145
+ try {
146
+ const content = await import_promises.default.readFile(filePath, "utf8");
147
+ return JSON.parse(content);
148
+ } catch (error) {
149
+ if (error.code === "ENOENT") {
150
+ return null;
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+ /**
156
+ * 清理进度文件
157
+ */
158
+ async cleanProgressFile(fileName, appName) {
159
+ const filePath = this.progressFilePath(fileName, appName);
160
+ try {
161
+ await import_promises.default.unlink(filePath);
162
+ } catch (error) {
163
+ if (error.code !== "ENOENT") {
164
+ throw error;
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * 创建进度跟踪器
170
+ */
171
+ createProgressTracker(backupFileName, appName, userId) {
172
+ return {
173
+ /**
174
+ * 更新进度
175
+ */
176
+ update: async (percent, currentStep) => {
177
+ await this.writeProgress(backupFileName, { percent, currentStep }, appName, userId);
178
+ },
179
+ /**
180
+ * 计算集合备份阶段的进度 (5-70%)
181
+ */
182
+ getCollectionProgress: (currentIndex, totalCollections) => {
183
+ return 5 + Math.floor((currentIndex + 1) / totalCollections * 65);
184
+ },
185
+ /**
186
+ * 计算数据库内容备份阶段的进度 (80-88%)
187
+ */
188
+ getDbContentProgress: (processedCollections, totalCollections) => {
189
+ if (totalCollections === 0) return 80;
190
+ return 80 + Math.floor(processedCollections / totalCollections * 8);
191
+ }
192
+ };
193
+ }
194
+ /**
195
+ * 统计目录中的文件总数
196
+ */
197
+ async countFiles(dir) {
198
+ let count = 0;
199
+ try {
200
+ const entries = await import_promises.default.readdir(dir, { withFileTypes: true });
201
+ for (const entry of entries) {
202
+ const fullPath = import_node_path.default.join(dir, entry.name);
203
+ if (entry.isDirectory()) {
204
+ count += await this.countFiles(fullPath);
205
+ } else {
206
+ count++;
207
+ }
208
+ }
209
+ } catch (error) {
210
+ }
211
+ return count;
212
+ }
213
+ /**
214
+ * 设置打包阶段的进度更新
215
+ */
216
+ setupPackingProgress(archive, progressTracker) {
217
+ let processedEntries = 0;
218
+ let totalEntries = 0;
219
+ let startTime = Date.now();
220
+ let progressInterval = null;
221
+ let currentProgress = 90;
222
+ let isFinished = false;
223
+ this.countFiles(this.workDir).then((count) => {
224
+ totalEntries = count || 1;
225
+ }).catch(() => {
226
+ totalEntries = 1;
227
+ });
228
+ archive.on("entry", () => {
229
+ processedEntries++;
230
+ });
231
+ const handleFinish = async () => {
232
+ var _a;
233
+ if (isFinished) {
234
+ return;
235
+ }
236
+ isFinished = true;
237
+ if (progressInterval) {
238
+ clearInterval(progressInterval);
239
+ progressInterval = null;
240
+ }
241
+ try {
242
+ await progressTracker.update(99, "Packing backup file...");
243
+ } catch (error) {
244
+ if ((_a = this.app) == null ? void 0 : _a.logger) {
245
+ this.app.logger.warn("[ProgressManager] Failed to update progress after packing:", error);
246
+ }
247
+ }
248
+ };
249
+ archive.on("end", handleFinish);
250
+ archive.on("finish", handleFinish);
251
+ progressInterval = setInterval(async () => {
252
+ if (isFinished) {
253
+ if (progressInterval) {
254
+ clearInterval(progressInterval);
255
+ progressInterval = null;
256
+ }
257
+ return;
258
+ }
259
+ const elapsed = Date.now() - startTime;
260
+ if (processedEntries > 0 && totalEntries > 0) {
261
+ const fileBasedProgress = 90 + Math.floor(processedEntries / totalEntries * 9);
262
+ const timeBasedProgress = Math.min(90 + Math.floor(elapsed / 6e4 * 9), 99);
263
+ currentProgress = Math.max(currentProgress, Math.min(fileBasedProgress, timeBasedProgress, 99));
264
+ } else {
265
+ currentProgress = Math.min(90 + Math.floor(elapsed / 6e4 * 9), 99);
266
+ }
267
+ if (currentProgress < 99) {
268
+ await progressTracker.update(currentProgress, "Packing backup file...");
269
+ }
270
+ }, 300);
271
+ return () => {
272
+ isFinished = true;
273
+ if (progressInterval) {
274
+ clearInterval(progressInterval);
275
+ progressInterval = null;
276
+ }
277
+ };
278
+ }
279
+ };
280
+ // 静态标志,用于避免重复警告 WebSocket 不可用(在 worker 线程中很常见)
281
+ _ProgressManager.wsUnavailableWarned = false;
282
+ let ProgressManager = _ProgressManager;
283
+ // Annotate the CommonJS export names for ESM import in node:
284
+ 0 && (module.exports = {
285
+ ProgressManager
286
+ });
@@ -57,15 +57,15 @@ var backup_files_default = {
57
57
  actions: {
58
58
  async list(ctx, next) {
59
59
  const { page = import_server.DEFAULT_PAGE, pageSize = import_server.DEFAULT_PER_PAGE } = ctx.action.params;
60
- const dumper = new import_dumper.Dumper(ctx.app);
60
+ const dumper = new import_dumper.Dumper(ctx.tego);
61
61
  const backupFiles = await dumper.allBackUpFilePaths({
62
62
  includeInProgress: true,
63
- appName: ctx.app.name
63
+ appName: ctx.tego.name
64
64
  });
65
65
  const count = backupFiles.length;
66
66
  const rows = await Promise.all(
67
67
  backupFiles.slice((page - 1) * pageSize, page * pageSize).map(async (file) => {
68
- return await import_dumper.Dumper.getFileStatus(file.endsWith(".lock") ? file.replace(".lock", "") : file);
68
+ return await import_dumper.Dumper.getFileStatus(file.endsWith(".lock") ? file.replace(".lock", "") : file, ctx.tego.name);
69
69
  })
70
70
  );
71
71
  ctx.body = {
@@ -79,8 +79,8 @@ var backup_files_default = {
79
79
  },
80
80
  async get(ctx, next) {
81
81
  const { filterByTk } = ctx.action.params;
82
- const dumper = new import_dumper.Dumper(ctx.app);
83
- const filePath = dumper.backUpFilePath(filterByTk, ctx.app.name);
82
+ const dumper = new import_dumper.Dumper(ctx.tego);
83
+ const filePath = dumper.backUpFilePath(filterByTk, ctx.tego.name);
84
84
  async function sendError(message, status = 404) {
85
85
  ctx.body = { status: "error", message };
86
86
  ctx.status = status;
@@ -90,7 +90,7 @@ var backup_files_default = {
90
90
  if (fileState.status !== "ok") {
91
91
  await sendError(`Backup file ${filterByTk} not found`);
92
92
  } else {
93
- const restorer = new import_restorer.Restorer(ctx.app, {
93
+ const restorer = new import_restorer.Restorer(ctx.tego, {
94
94
  backUpFilePath: filePath
95
95
  });
96
96
  const restoreMeta = await restorer.parseBackupFile();
@@ -112,24 +112,26 @@ var backup_files_default = {
112
112
  * @param next
113
113
  */
114
114
  async create(ctx, next) {
115
- var _a, _b;
115
+ var _a, _b, _c;
116
116
  const data = ctx.request.body;
117
- const app = ctx.app;
118
- if (data.method === "worker" && !((_a = app.worker) == null ? void 0 : _a.available)) {
117
+ const app = ctx.tego;
118
+ const userId = (_a = ctx.state.currentUser) == null ? void 0 : _a.id;
119
+ if (data.method === "worker" && !((_b = app.worker) == null ? void 0 : _b.available)) {
119
120
  ctx.throw(500, ctx.t("No worker thread", { ns: "worker-thread" }));
120
121
  return next();
121
122
  }
122
- let useWorker = data.method === "worker" || data.method === "priority" && ((_b = app.worker) == null ? void 0 : _b.available);
123
- const dumper = new import_dumper.Dumper(ctx.app);
124
- const taskId = await dumper.getLockFile(ctx.app.name);
123
+ let useWorker = data.method === "worker" || data.method === "priority" && ((_c = app.worker) == null ? void 0 : _c.available);
124
+ const dumper = new import_dumper.Dumper(ctx.tego);
125
+ const taskId = await dumper.getLockFile(ctx.tego.name);
125
126
  if (useWorker) {
126
127
  app.worker.callPluginMethod({
127
128
  plugin: import_server2.default,
128
129
  method: "workerCreateBackUp",
129
130
  params: {
130
131
  dataTypes: data.dataTypes,
131
- appName: ctx.app.name,
132
- filename: taskId
132
+ appName: ctx.tego.name,
133
+ filename: taskId,
134
+ userId
133
135
  },
134
136
  // 目前限制方法并发为1
135
137
  concurrency: 1
@@ -138,20 +140,21 @@ var backup_files_default = {
138
140
  }).catch((error) => {
139
141
  app.noticeManager.notify("backup", { level: "error", msg: error.message });
140
142
  }).finally(() => {
141
- dumper.cleanLockFile(taskId, ctx.app.name);
143
+ dumper.cleanLockFile(taskId, ctx.tego.name);
142
144
  });
143
145
  } else {
144
146
  const plugin = app.pm.get(import_server2.default);
145
147
  plugin.workerCreateBackUp({
146
148
  dataTypes: data.dataTypes,
147
- appName: ctx.app.name,
148
- filename: taskId
149
+ appName: ctx.tego.name,
150
+ filename: taskId,
151
+ userId
149
152
  }).then((res) => {
150
153
  app.noticeManager.notify("backup", { level: "info", msg: ctx.t("Done", { ns: "backup" }) });
151
154
  }).catch((error) => {
152
155
  app.noticeManager.notify("backup", { level: "error", msg: error.message });
153
156
  }).finally(() => {
154
- dumper.cleanLockFile(taskId, ctx.app.name);
157
+ dumper.cleanLockFile(taskId, ctx.tego.name);
155
158
  });
156
159
  }
157
160
  ctx.body = {
@@ -166,8 +169,8 @@ var backup_files_default = {
166
169
  */
167
170
  async download(ctx, next) {
168
171
  const { filterByTk } = ctx.action.params;
169
- const dumper = new import_dumper.Dumper(ctx.app);
170
- const filePath = dumper.backUpFilePath(filterByTk, ctx.app.name);
172
+ const dumper = new import_dumper.Dumper(ctx.tego);
173
+ const filePath = dumper.backUpFilePath(filterByTk, ctx.tego.name);
171
174
  const fileState = await import_dumper.Dumper.getFileStatus(filePath);
172
175
  if (fileState.status !== "ok") {
173
176
  throw new Error(`Backup file ${filterByTk} not found`);
@@ -187,8 +190,8 @@ var backup_files_default = {
187
190
  return import_node_path.default.resolve(tmpDir, key);
188
191
  }
189
192
  if (filterByTk) {
190
- const dumper = new import_dumper.Dumper(ctx.app);
191
- return dumper.backUpFilePath(filterByTk, ctx.app.name);
193
+ const dumper = new import_dumper.Dumper(ctx.tego);
194
+ return dumper.backUpFilePath(filterByTk, ctx.tego.name);
192
195
  }
193
196
  })();
194
197
  if (!filePath) {
@@ -198,13 +201,13 @@ var backup_files_default = {
198
201
  for (const dataType of dataTypes) {
199
202
  args.push("-g", dataType);
200
203
  }
201
- await ctx.app.runCommand(...args);
204
+ await ctx.tego.runCommand(...args);
202
205
  await next();
203
206
  },
204
207
  async destroy(ctx, next) {
205
208
  const { filterByTk } = ctx.action.params;
206
- const dumper = new import_dumper.Dumper(ctx.app);
207
- const filePath = dumper.backUpFilePath(filterByTk, ctx.app.name);
209
+ const dumper = new import_dumper.Dumper(ctx.tego);
210
+ const filePath = dumper.backUpFilePath(filterByTk, ctx.tego.name);
208
211
  await import_promises.default.unlink(filePath);
209
212
  ctx.body = {
210
213
  status: "ok"
@@ -214,7 +217,7 @@ var backup_files_default = {
214
217
  async upload(ctx, next) {
215
218
  const file = ctx.file;
216
219
  const fileName = file.filename;
217
- const restorer = new import_restorer.Restorer(ctx.app, {
220
+ const restorer = new import_restorer.Restorer(ctx.tego, {
218
221
  backUpFilePath: file.path
219
222
  });
220
223
  const restoreMeta = await restorer.parseBackupFile();
@@ -226,7 +229,7 @@ var backup_files_default = {
226
229
  },
227
230
  async dumpableCollections(ctx, next) {
228
231
  ctx.withoutDataWrapping = true;
229
- const dumper = new import_dumper.Dumper(ctx.app);
232
+ const dumper = new import_dumper.Dumper(ctx.tego);
230
233
  ctx.body = await dumper.dumpableCollectionsGroupByGroup();
231
234
  await next();
232
235
  }
@@ -15,5 +15,6 @@ export default class PluginBackupRestoreServer extends Plugin {
15
15
  dataTypes: string[];
16
16
  appName: string;
17
17
  filename: string;
18
+ userId?: number;
18
19
  }): Promise<void>;
19
20
  }
@@ -57,6 +57,53 @@ const _PluginBackupRestoreServer = class _PluginBackupRestoreServer extends impo
57
57
  }
58
58
  async load() {
59
59
  this.app.resourcer.define(import_backup_files.default);
60
+ const gateway = import_server.Gateway.getInstance();
61
+ const ws = gateway["wsServer"];
62
+ if (ws == null ? void 0 : ws.wss) {
63
+ const appName = this.app.name;
64
+ ws.wss.on(
65
+ "connection",
66
+ async (websocket) => {
67
+ websocket.on("message", async (data) => {
68
+ var _a, _b, _c;
69
+ if (data.toString() !== "ping") {
70
+ try {
71
+ const userMeg = JSON.parse(data.toString());
72
+ if (userMeg.type === "signIn") {
73
+ if (!((_a = userMeg.payload) == null ? void 0 : _a.token)) {
74
+ return;
75
+ }
76
+ try {
77
+ const analysis = await ((_c = (_b = this.app.authManager) == null ? void 0 : _b.jwt) == null ? void 0 : _c.verifyToken(userMeg.payload.token));
78
+ const userId = analysis.userId;
79
+ const client = ws.webSocketClients.get(websocket.id);
80
+ if (client) {
81
+ client.tags.forEach((tag2) => {
82
+ if (tag2.startsWith("app:")) {
83
+ client.tags.delete(tag2);
84
+ }
85
+ });
86
+ const tag = `app:${appName}#${userId}`;
87
+ client.tags.add(tag);
88
+ this.app.logger.debug(`[Backup] WebSocket signIn: set tag ${tag} for connection ${websocket.id}`);
89
+ } else {
90
+ this.app.logger.warn(
91
+ `[Backup] WebSocket signIn: client not found for connection ${websocket.id}`
92
+ );
93
+ }
94
+ } catch (error) {
95
+ this.app.logger.warn("[Backup] WebSocket signIn message connection error:", error);
96
+ }
97
+ }
98
+ } catch (error) {
99
+ }
100
+ }
101
+ });
102
+ }
103
+ );
104
+ } else {
105
+ this.app.logger.warn("[Backup] WebSocket server not available, backup progress will not be pushed via WebSocket");
106
+ }
60
107
  this.app.on("afterStart", async (app) => {
61
108
  const cronJobs = await app.db.getRepository(import_constants.COLLECTION_AUTOBACKUP).find({
62
109
  filter: { enabled: true }
@@ -228,7 +275,8 @@ const _PluginBackupRestoreServer = class _PluginBackupRestoreServer extends impo
228
275
  await new import_dumper.Dumper(this.app).runDumpTask({
229
276
  groups: new Set(data.dataTypes),
230
277
  appName: data.appName,
231
- fileName: data.filename
278
+ fileName: data.filename,
279
+ userId: data.userId
232
280
  });
233
281
  }
234
282
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tachybase/module-backup",
3
3
  "displayName": "App backup & restore",
4
- "version": "1.5.1",
4
+ "version": "1.6.1",
5
5
  "description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.",
6
6
  "keywords": [
7
7
  "System management"
@@ -9,12 +9,12 @@
9
9
  "license": "Apache-2.0",
10
10
  "main": "./dist/server/index.js",
11
11
  "devDependencies": {
12
- "@ant-design/icons": "^5.6.1",
12
+ "@ant-design/icons": "^6.1.0",
13
13
  "@hapi/topo": "^6.0.2",
14
- "@tachybase/schema": "1.3.52",
15
- "@tachybase/test": "1.3.52",
16
- "@tego/client": "1.3.52",
17
- "@tego/server": "1.3.52",
14
+ "@tachybase/schema": "1.6.0-alpha.9",
15
+ "@tachybase/test": "1.6.0-alpha.9",
16
+ "@tego/client": "1.6.0-alpha.9",
17
+ "@tego/server": "1.6.0-alpha.9",
18
18
  "@types/archiver": "^5.3.4",
19
19
  "@types/file-saver": "^2.0.7",
20
20
  "@types/lodash": "^4.17.20",
@@ -34,8 +34,8 @@
34
34
  "semver": "7.7.2",
35
35
  "tar": "^7.4.3",
36
36
  "yauzl": "^3.2.0",
37
- "@tachybase/client": "1.5.1",
38
- "@tachybase/module-worker-thread": "1.5.1"
37
+ "@tachybase/client": "1.6.1",
38
+ "@tachybase/module-worker-thread": "1.6.1"
39
39
  },
40
40
  "description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。",
41
41
  "displayName.zh-CN": "应用的备份与还原",