ai-project-manage-cli 6.0.93 → 6.0.94

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.
Files changed (2) hide show
  1. package/dist/index.js +351 -168
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2216,6 +2216,7 @@ async function runUpdateMessageStatus(options) {
2216
2216
 
2217
2217
  // src/commands/connect.ts
2218
2218
  import WebSocket from "ws";
2219
+ import { setTimeout as delay2 } from "node:timers/promises";
2219
2220
 
2220
2221
  // src/ws/protocol.ts
2221
2222
  function nonEmptyString(v) {
@@ -3654,6 +3655,7 @@ function forceReleaseConnectLock() {
3654
3655
 
3655
3656
  // src/commands/daemon.ts
3656
3657
  import { spawnSync as spawnSync2 } from "child_process";
3658
+ import { setTimeout as delay } from "node:timers/promises";
3657
3659
  import { createRequire } from "node:module";
3658
3660
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync13 } from "fs";
3659
3661
  import { join as join15 } from "path";
@@ -3682,10 +3684,12 @@ function buildConnectPm2Ecosystem(options) {
3682
3684
  args: options.connectArgs,
3683
3685
  autorestart: true,
3684
3686
  min_uptime: "10s",
3685
- max_restarts: 100,
3687
+ max_restarts: 0,
3686
3688
  restart_delay: 3e3,
3687
3689
  exp_backoff_restart_delay: 1e3,
3688
3690
  max_memory_restart: "1G",
3691
+ kill_timeout: 5e3,
3692
+ shutdown_with_message: true,
3689
3693
  env
3690
3694
  }
3691
3695
  ]
@@ -3878,10 +3882,44 @@ async function runDaemonStart(options) {
3878
3882
  console.log("[apm] \u505C\u6B62: apm daemon stop");
3879
3883
  }
3880
3884
  async function runDaemonStop() {
3885
+ pruneStaleConnectLock();
3886
+ if (!isPm2ConnectOnline()) {
3887
+ killConnectLockProcessIfAlive();
3888
+ forceReleaseConnectLock();
3889
+ console.log(`[apm] ${PM2_APP_NAME} \u672A\u5728\u8FD0\u884C`);
3890
+ return;
3891
+ }
3881
3892
  runPm2(["stop", PM2_APP_NAME], { inherit: true });
3893
+ for (let i = 0; i < 10; i += 1) {
3894
+ if (!isPm2ConnectOnline()) break;
3895
+ await delay(500);
3896
+ }
3897
+ if (isPm2ConnectOnline()) {
3898
+ console.log(`[apm] \u4F18\u96C5\u505C\u6B62\u8D85\u65F6\uFF0C\u5F3A\u5236\u79FB\u9664 ${PM2_APP_NAME}\u2026`);
3899
+ runPm2(["delete", PM2_APP_NAME], { inherit: true });
3900
+ }
3901
+ killConnectLockProcessIfAlive();
3882
3902
  forceReleaseConnectLock();
3883
3903
  console.log(`[apm] ${PM2_APP_NAME} \u5DF2\u505C\u6B62`);
3884
3904
  }
3905
+ function killConnectLockProcessIfAlive() {
3906
+ pruneStaleConnectLock();
3907
+ const lock = readConnectLock();
3908
+ if (!lock || !isProcessAlive(lock.pid)) return;
3909
+ try {
3910
+ process.kill(lock.pid, "SIGTERM");
3911
+ } catch {
3912
+ forceReleaseConnectLock();
3913
+ return;
3914
+ }
3915
+ if (isProcessAlive(lock.pid)) {
3916
+ try {
3917
+ process.kill(lock.pid, "SIGKILL");
3918
+ } catch {
3919
+ }
3920
+ }
3921
+ forceReleaseConnectLock();
3922
+ }
3885
3923
  async function runDaemonRestart(options) {
3886
3924
  pruneStaleConnectLock();
3887
3925
  const lock = readConnectLock();
@@ -3916,6 +3954,8 @@ async function runDaemonLogs(options) {
3916
3954
 
3917
3955
  // src/commands/connect.ts
3918
3956
  var HEARTBEAT_MS = 3e4;
3957
+ var RECONNECT_MIN_MS = 1e3;
3958
+ var RECONNECT_MAX_MS = 6e4;
3919
3959
  async function updateMessageStatus(cfg, messageId, status) {
3920
3960
  const api = createApmApiClient(cfg);
3921
3961
  await api.cli.updateMessageStatus({ id: messageId, status });
@@ -4068,6 +4108,17 @@ async function handleInboundMessage(cfg, msg, signal, ctx) {
4068
4108
  }
4069
4109
  }
4070
4110
  }
4111
+ function interruptibleSleep(ms, signal) {
4112
+ if (signal.aborted) return Promise.resolve();
4113
+ return new Promise((resolve5) => {
4114
+ const timer = setTimeout(resolve5, ms);
4115
+ const onAbort = () => {
4116
+ clearTimeout(timer);
4117
+ resolve5();
4118
+ };
4119
+ signal.addEventListener("abort", onAbort, { once: true });
4120
+ });
4121
+ }
4071
4122
  function startHeartbeat(ws, clientMachineId) {
4072
4123
  const send = () => {
4073
4124
  if (ws.readyState === WebSocket.OPEN) {
@@ -4083,182 +4134,274 @@ function startHeartbeat(ws, clientMachineId) {
4083
4134
  const timer = setInterval(send, HEARTBEAT_MS);
4084
4135
  return () => clearInterval(timer);
4085
4136
  }
4086
- async function runConnect(options) {
4087
- const { didUpdate } = await runUpdate();
4088
- if (didUpdate) {
4089
- await handleConnectAfterUpdate(options);
4090
- }
4091
- const cfg = await ensureLoggedConfig();
4092
- if (options.server?.trim()) {
4093
- cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4094
- }
4095
- const clientMachineId = resolveClientMachineId(cfg);
4096
- if (!clientMachineId) {
4097
- console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4098
- process.exit(1);
4099
- }
4100
- assertConnectNotRunning();
4101
- acquireConnectLock("foreground");
4102
- process.on("exit", releaseConnectLock);
4103
- const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4104
- console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4105
- await new Promise((resolve5, reject) => {
4106
- const ws = new WebSocket(url);
4107
- let stopHeartbeat;
4108
- let shuttingDown = false;
4109
- const shutdownAbort = new AbortController();
4110
- const runSlots = createRunSlotPool();
4111
- const activeTasks = /* @__PURE__ */ new Set();
4112
- const activeRuns = /* @__PURE__ */ new Map();
4113
- const pendingCancels = /* @__PURE__ */ new Set();
4114
- const shutdown = async (code = 0) => {
4115
- if (shuttingDown) return;
4116
- shuttingDown = true;
4117
- logAbortSignalStats(
4118
- shutdownAbort.signal,
4119
- "connect:shutdown-before-abort"
4120
- );
4121
- shutdownAbort.abort();
4122
- logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4123
- stopHeartbeat?.();
4124
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4125
- ws.terminate();
4126
- }
4127
- try {
4128
- await Promise.race([
4129
- Promise.all(activeTasks),
4130
- new Promise((r) => setTimeout(r, SHUTDOWN_DRAIN_MS))
4131
- ]);
4132
- } catch {
4133
- }
4134
- releaseConnectLock();
4135
- resolve5();
4136
- process.exit(code);
4137
- };
4138
- ws.on("open", () => {
4139
- console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4140
- stopHeartbeat = startHeartbeat(ws, clientMachineId);
4141
- });
4142
- ws.on("message", (data) => {
4143
- if (shuttingDown) return;
4144
- const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4145
- const parsed = parseAgentWsMessage(text);
4146
- if (parsed === null) {
4147
- console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4148
- return;
4149
- }
4150
- if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4151
- return;
4152
- }
4153
- const validated = validateAgentWsMessage(parsed, "outbound");
4154
- if (!validated.ok) {
4155
- console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4156
- return;
4157
- }
4158
- if (validated.data.type === "cancel") {
4159
- const { messageId } = validated.data;
4160
- pendingCancels.add(messageId);
4161
- activeRuns.get(messageId)?.abort();
4162
- return;
4163
- }
4164
- if (validated.data.type === "deploy") {
4165
- const msg2 = validated.data;
4166
- const perDeployController = new AbortController();
4167
- const signal2 = AbortSignal.any([
4168
- shutdownAbort.signal,
4169
- perDeployController.signal
4170
- ]);
4171
- const task2 = (async () => {
4172
- await runSlots.acquire();
4173
- try {
4174
- await handleInboundDeploy(cfg, msg2, signal2);
4175
- } finally {
4176
- runSlots.release();
4177
- }
4178
- })();
4179
- activeTasks.add(task2);
4180
- void task2.finally(() => {
4181
- activeTasks.delete(task2);
4182
- });
4183
- return;
4184
- }
4185
- if (validated.data.type !== "message") {
4186
- return;
4187
- }
4188
- const msg = validated.data;
4189
- const perMessageController = new AbortController();
4190
- activeRuns.set(msg.messageId, perMessageController);
4191
- if (pendingCancels.has(msg.messageId)) {
4192
- activeRuns.delete(msg.messageId);
4193
- pendingCancels.delete(msg.messageId);
4194
- return;
4195
- }
4196
- const signal = AbortSignal.any([
4137
+ function attachWsHandlers(ws, ctx, onOpen) {
4138
+ const {
4139
+ cfg,
4140
+ clientMachineId,
4141
+ shutdownAbort,
4142
+ runSlots,
4143
+ activeTasks,
4144
+ activeRuns,
4145
+ pendingCancels,
4146
+ isShuttingDown
4147
+ } = ctx;
4148
+ ws.on("open", () => {
4149
+ console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
4150
+ onOpen();
4151
+ });
4152
+ ws.on("message", (data) => {
4153
+ if (isShuttingDown()) return;
4154
+ const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
4155
+ const parsed = parseAgentWsMessage(text);
4156
+ if (parsed === null) {
4157
+ console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
4158
+ return;
4159
+ }
4160
+ if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
4161
+ return;
4162
+ }
4163
+ const validated = validateAgentWsMessage(parsed, "outbound");
4164
+ if (!validated.ok) {
4165
+ console.error(`[apm] \u6536\u5230\u65E0\u6548 WS \u5305: ${validated.reason}`);
4166
+ return;
4167
+ }
4168
+ if (validated.data.type === "cancel") {
4169
+ const { messageId } = validated.data;
4170
+ pendingCancels.add(messageId);
4171
+ activeRuns.get(messageId)?.abort();
4172
+ return;
4173
+ }
4174
+ if (validated.data.type === "deploy") {
4175
+ const msg2 = validated.data;
4176
+ const perDeployController = new AbortController();
4177
+ const signal2 = AbortSignal.any([
4197
4178
  shutdownAbort.signal,
4198
- perMessageController.signal
4179
+ perDeployController.signal
4199
4180
  ]);
4200
- const ctx = {
4201
- shutdownSignal: shutdownAbort.signal,
4202
- perMessageSignal: perMessageController.signal
4203
- };
4204
- const task = (async () => {
4181
+ const task2 = (async () => {
4205
4182
  await runSlots.acquire();
4206
4183
  try {
4207
- if (signal.aborted || isUserCancelled(ctx)) return;
4208
- try {
4209
- await updateMessageStatus(cfg, msg.messageId, "TYPING");
4210
- } catch (typingErr) {
4211
- if (isUserCancelled(ctx)) return;
4212
- console.error(
4213
- "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4214
- typingErr instanceof Error ? typingErr.message : typingErr
4215
- );
4216
- try {
4217
- await setMessageError(
4218
- cfg,
4219
- msg.messageId,
4220
- typingErr instanceof Error ? typingErr.message : String(typingErr)
4221
- );
4222
- await updateMessageStatus(cfg, msg.messageId, "FAILED");
4223
- } catch (statusErr) {
4224
- console.error(
4225
- "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4226
- statusErr instanceof Error ? statusErr.message : statusErr
4227
- );
4228
- }
4229
- return;
4230
- }
4231
- await handleInboundMessage(cfg, msg, signal, ctx);
4184
+ await handleInboundDeploy(cfg, msg2, signal2);
4232
4185
  } finally {
4233
4186
  runSlots.release();
4234
- activeRuns.delete(msg.messageId);
4235
- pendingCancels.delete(msg.messageId);
4236
4187
  }
4237
4188
  })();
4238
- activeTasks.add(task);
4239
- void task.finally(() => {
4240
- activeTasks.delete(task);
4189
+ activeTasks.add(task2);
4190
+ void task2.finally(() => {
4191
+ activeTasks.delete(task2);
4241
4192
  });
4193
+ return;
4194
+ }
4195
+ if (validated.data.type !== "message") {
4196
+ return;
4197
+ }
4198
+ const msg = validated.data;
4199
+ const perMessageController = new AbortController();
4200
+ activeRuns.set(msg.messageId, perMessageController);
4201
+ if (pendingCancels.has(msg.messageId)) {
4202
+ activeRuns.delete(msg.messageId);
4203
+ pendingCancels.delete(msg.messageId);
4204
+ return;
4205
+ }
4206
+ const signal = AbortSignal.any([
4207
+ shutdownAbort.signal,
4208
+ perMessageController.signal
4209
+ ]);
4210
+ const messageCtx = {
4211
+ shutdownSignal: shutdownAbort.signal,
4212
+ perMessageSignal: perMessageController.signal
4213
+ };
4214
+ const task = (async () => {
4215
+ await runSlots.acquire();
4216
+ try {
4217
+ if (signal.aborted || isUserCancelled(messageCtx)) return;
4218
+ try {
4219
+ await updateMessageStatus(cfg, msg.messageId, "TYPING");
4220
+ } catch (typingErr) {
4221
+ if (isUserCancelled(messageCtx)) return;
4222
+ console.error(
4223
+ "[apm] \u66F4\u65B0 TYPING \u72B6\u6001\u5931\u8D25:",
4224
+ typingErr instanceof Error ? typingErr.message : typingErr
4225
+ );
4226
+ try {
4227
+ await setMessageError(
4228
+ cfg,
4229
+ msg.messageId,
4230
+ typingErr instanceof Error ? typingErr.message : String(typingErr)
4231
+ );
4232
+ await updateMessageStatus(cfg, msg.messageId, "FAILED");
4233
+ } catch (statusErr) {
4234
+ console.error(
4235
+ "[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
4236
+ statusErr instanceof Error ? statusErr.message : statusErr
4237
+ );
4238
+ }
4239
+ return;
4240
+ }
4241
+ await handleInboundMessage(cfg, msg, signal, messageCtx);
4242
+ } finally {
4243
+ runSlots.release();
4244
+ activeRuns.delete(msg.messageId);
4245
+ pendingCancels.delete(msg.messageId);
4246
+ }
4247
+ })();
4248
+ activeTasks.add(task);
4249
+ void task.finally(() => {
4250
+ activeTasks.delete(task);
4251
+ });
4252
+ });
4253
+ }
4254
+ function connectOnce(url, ctx, connectionAbort, onConnected) {
4255
+ return new Promise((resolve5, reject) => {
4256
+ const ws = new WebSocket(url);
4257
+ let stopHeartbeat;
4258
+ let settled = false;
4259
+ const finish = (fn) => {
4260
+ if (settled) return;
4261
+ settled = true;
4262
+ connectionAbort.removeEventListener("abort", onConnectionAbort);
4263
+ stopHeartbeat?.();
4264
+ fn();
4265
+ };
4266
+ const onConnectionAbort = () => {
4267
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4268
+ ws.terminate();
4269
+ }
4270
+ };
4271
+ connectionAbort.addEventListener("abort", onConnectionAbort);
4272
+ attachWsHandlers(ws, ctx, () => {
4273
+ stopHeartbeat = startHeartbeat(ws, ctx.clientMachineId);
4274
+ onConnected();
4242
4275
  });
4243
4276
  ws.on("close", (code, reason) => {
4244
4277
  console.log(
4245
4278
  `[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
4246
4279
  );
4247
- void shutdown();
4280
+ if (ctx.isShuttingDown()) {
4281
+ finish(() => reject(new Error("shutdown")));
4282
+ return;
4283
+ }
4284
+ finish(resolve5);
4248
4285
  });
4249
4286
  ws.on("error", (err) => {
4250
4287
  console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
4251
- reject(err);
4252
- });
4253
- process.on("SIGINT", () => {
4254
- console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4255
- void shutdown();
4256
- });
4257
- process.on("SIGTERM", () => {
4258
- void shutdown();
4288
+ if (ctx.isShuttingDown()) {
4289
+ finish(() => reject(new Error("shutdown")));
4290
+ return;
4291
+ }
4292
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4293
+ ws.terminate();
4294
+ }
4259
4295
  });
4260
4296
  });
4261
4297
  }
4298
+ async function runConnect(options) {
4299
+ const { didUpdate } = await runUpdate();
4300
+ if (didUpdate) {
4301
+ await handleConnectAfterUpdate(options);
4302
+ }
4303
+ const cfg = await ensureLoggedConfig();
4304
+ if (options.server?.trim()) {
4305
+ cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
4306
+ }
4307
+ const clientMachineId = resolveClientMachineId(cfg);
4308
+ if (!clientMachineId) {
4309
+ console.error("[apm] config \u7F3A\u5C11 clientMachineId\uFF0C\u8BF7\u91CD\u65B0 apm login");
4310
+ process.exit(1);
4311
+ }
4312
+ assertConnectNotRunning();
4313
+ const lockMode = isRunningUnderPm2() ? "pm2" : "foreground";
4314
+ acquireConnectLock(lockMode);
4315
+ process.on("exit", releaseConnectLock);
4316
+ let shuttingDown = false;
4317
+ let shutdownPromise = null;
4318
+ const lifecycleAbort = new AbortController();
4319
+ const shutdownAbort = new AbortController();
4320
+ const runSlots = createRunSlotPool();
4321
+ const activeTasks = /* @__PURE__ */ new Set();
4322
+ const activeRuns = /* @__PURE__ */ new Map();
4323
+ const pendingCancels = /* @__PURE__ */ new Set();
4324
+ let currentConnectionAbort = null;
4325
+ const sessionCtx = {
4326
+ cfg,
4327
+ clientMachineId,
4328
+ shutdownAbort,
4329
+ runSlots,
4330
+ activeTasks,
4331
+ activeRuns,
4332
+ pendingCancels,
4333
+ isShuttingDown: () => shuttingDown
4334
+ };
4335
+ const drainAndExit = async (code = 0) => {
4336
+ const drainMs = isRunningUnderPm2() ? 500 : SHUTDOWN_DRAIN_MS;
4337
+ lifecycleAbort.abort();
4338
+ currentConnectionAbort?.abort();
4339
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-before-abort");
4340
+ shutdownAbort.abort();
4341
+ logAbortSignalStats(shutdownAbort.signal, "connect:shutdown-after-abort");
4342
+ try {
4343
+ await Promise.race([Promise.all(activeTasks), delay2(drainMs)]);
4344
+ } catch {
4345
+ }
4346
+ releaseConnectLock();
4347
+ process.exit(code);
4348
+ };
4349
+ const shutdown = (code = 0) => {
4350
+ if (shuttingDown) return shutdownPromise ?? Promise.resolve();
4351
+ shuttingDown = true;
4352
+ shutdownPromise = drainAndExit(code);
4353
+ return shutdownPromise;
4354
+ };
4355
+ const onStopSignal = (code = 0) => {
4356
+ console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
4357
+ void shutdown(code);
4358
+ };
4359
+ process.on("SIGINT", () => onStopSignal(0));
4360
+ process.on("SIGTERM", () => onStopSignal(0));
4361
+ if (isRunningUnderPm2()) {
4362
+ process.on("message", (msg) => {
4363
+ if (msg === "shutdown") onStopSignal(0);
4364
+ });
4365
+ }
4366
+ let reconnectDelay = RECONNECT_MIN_MS;
4367
+ while (!shuttingDown) {
4368
+ currentConnectionAbort = new AbortController();
4369
+ const connectionSignal = AbortSignal.any([
4370
+ lifecycleAbort.signal,
4371
+ currentConnectionAbort.signal
4372
+ ]);
4373
+ const url = buildAgentWsUrl(cfg.baseUrl, resolveApiKey(cfg));
4374
+ console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
4375
+ try {
4376
+ await connectOnce(url, sessionCtx, connectionSignal, () => {
4377
+ reconnectDelay = RECONNECT_MIN_MS;
4378
+ });
4379
+ if (shuttingDown) break;
4380
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4381
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4382
+ if (shuttingDown) break;
4383
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4384
+ } catch (err) {
4385
+ if (shuttingDown) break;
4386
+ const detail = err instanceof Error ? err.message : String(err);
4387
+ if (detail === "shutdown") break;
4388
+ console.error("[apm] \u8FDE\u63A5\u5931\u8D25:", detail);
4389
+ console.log(`[apm] ${reconnectDelay}ms \u540E\u5C1D\u8BD5\u91CD\u8FDE\u2026`);
4390
+ await interruptibleSleep(reconnectDelay, lifecycleAbort.signal);
4391
+ if (shuttingDown) break;
4392
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
4393
+ } finally {
4394
+ currentConnectionAbort = null;
4395
+ }
4396
+ }
4397
+ if (!shuttingDown) {
4398
+ void shutdown(0);
4399
+ return;
4400
+ }
4401
+ if (shutdownPromise) {
4402
+ await shutdownPromise;
4403
+ }
4404
+ }
4262
4405
 
4263
4406
  // src/commands/create-pr-errors.ts
4264
4407
  import { ApiError as ApiError2 } from "listpage-http";
@@ -4788,6 +4931,56 @@ async function zipDirectory(distDir, zipPath) {
4788
4931
  console.error(`\u5DF2\u751F\u6210: ${zipPath} (${sizeMb} MB)`);
4789
4932
  return content.length;
4790
4933
  }
4934
+ var SFTP_UPLOAD_MAX_ATTEMPTS = 3;
4935
+ var SFTP_FAST_PUT_OPTIONS = {
4936
+ chunkSize: 64 * 1024,
4937
+ concurrency: 4
4938
+ };
4939
+ function buildSftpConnectOptions(settings) {
4940
+ return {
4941
+ host: settings.host,
4942
+ port: settings.port,
4943
+ username: settings.username,
4944
+ password: settings.password,
4945
+ readyTimeout: 3e4,
4946
+ tryKeyboard: true,
4947
+ keepaliveInterval: 1e4,
4948
+ keepaliveCountMax: 3
4949
+ };
4950
+ }
4951
+ async function sleep(ms) {
4952
+ await new Promise((resolve5) => setTimeout(resolve5, ms));
4953
+ }
4954
+ async function uploadZipWithRetry(settings, localZip, remoteZipPath) {
4955
+ let lastError;
4956
+ for (let attempt = 1; attempt <= SFTP_UPLOAD_MAX_ATTEMPTS; attempt++) {
4957
+ const sftp = new SftpClient();
4958
+ try {
4959
+ if (attempt > 1) {
4960
+ console.error(
4961
+ `SFTP \u4E0A\u4F20\u91CD\u8BD5 (${attempt}/${SFTP_UPLOAD_MAX_ATTEMPTS})...`
4962
+ );
4963
+ }
4964
+ await sftp.connect(buildSftpConnectOptions(settings));
4965
+ await ensureRemoteDir(sftp, settings.remotePath);
4966
+ await sftp.fastPut(localZip, remoteZipPath, SFTP_FAST_PUT_OPTIONS);
4967
+ return sftp;
4968
+ } catch (err) {
4969
+ lastError = err;
4970
+ try {
4971
+ await sftp.end();
4972
+ } catch {
4973
+ }
4974
+ if (attempt < SFTP_UPLOAD_MAX_ATTEMPTS) {
4975
+ const message = err instanceof Error ? err.message : String(err);
4976
+ const delaySec = attempt * 2;
4977
+ console.error(`SFTP \u4E0A\u4F20\u5931\u8D25 (${message})\uFF0C${delaySec}s \u540E\u91CD\u8BD5...`);
4978
+ await sleep(delaySec * 1e3);
4979
+ }
4980
+ }
4981
+ }
4982
+ throw lastError;
4983
+ }
4791
4984
  async function ensureRemoteDir(sftp, dir) {
4792
4985
  const parts = dir.replace(/\\/g, "/").split("/").filter(Boolean);
4793
4986
  let current = dir.startsWith("/") ? "" : ".";
@@ -4847,18 +5040,8 @@ async function uploadAndMaybeExtract(settings, localZip, extract) {
4847
5040
  console.error(
4848
5041
  `\u8FDE\u63A5 ${settings.username}@${settings.host}:${settings.port} ...`
4849
5042
  );
4850
- const sftp = new SftpClient();
5043
+ const sftp = await uploadZipWithRetry(settings, localZip, remoteZipPath);
4851
5044
  try {
4852
- await sftp.connect({
4853
- host: settings.host,
4854
- port: settings.port,
4855
- username: settings.username,
4856
- password: settings.password,
4857
- readyTimeout: 2e4,
4858
- tryKeyboard: true
4859
- });
4860
- await ensureRemoteDir(sftp, settings.remotePath);
4861
- await sftp.put(localZip, remoteZipPath);
4862
5045
  console.error(` \u2713 ${localZip} -> ${remoteZipPath}`);
4863
5046
  if (extract) {
4864
5047
  const target = settings.remotePath.replace(/\/$/, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "6.0.93",
3
+ "version": "6.0.94",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,