@yeaft/webchat-agent 1.0.674 → 1.0.676

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.
@@ -104,14 +104,14 @@ async function sendNow(msg, queuedSocket) {
104
104
  const socket = ctx.ws;
105
105
  if (!socket || socket.readyState !== WebSocket.OPEN) return bufferMessage(msg, 'Disconnected');
106
106
  // File transfers are request/connection scoped, unlike replayable chat output.
107
- if (['file_content_chunk', 'video_chunk'].includes(msg.type) && queuedSocket !== socket) return 'dropped';
107
+ if (['file_content_chunk', 'video_chunk', 'upgrade_agent_ack'].includes(msg.type) && queuedSocket !== socket) return 'dropped';
108
108
  const payload = ctx.serverEncryptionRequired && ctx.sessionKey
109
109
  ? await encrypt(msg, ctx.sessionKey) : msg;
110
110
  if (socket !== ctx.ws || socket.readyState !== WebSocket.OPEN) {
111
111
  return bufferMessage(msg, 'Connection changed');
112
112
  }
113
113
  const data = JSON.stringify(payload);
114
- if (['file_content_chunk', 'video_chunk'].includes(msg.type)) return sendTransferChunk(socket, data);
114
+ if (['file_content_chunk', 'video_chunk', 'upgrade_agent_ack'].includes(msg.type)) return sendTransferChunk(socket, data);
115
115
  socket.send(data);
116
116
  return 'sent';
117
117
  }
@@ -161,7 +161,7 @@ export async function sendToServer(msg) {
161
161
  return 'dropped';
162
162
  }
163
163
  const promise = new Promise((resolve, reject) => {
164
- const connectionScoped = ['file_content_chunk', 'video_chunk'].includes(msg.type);
164
+ const connectionScoped = ['file_content_chunk', 'video_chunk', 'upgrade_agent_ack'].includes(msg.type);
165
165
  ctx.outboundSendQueue.push({ msg, bytes, resolve, reject, socket: connectionScoped ? ctx.ws : null });
166
166
  ctx.outboundSendQueueBytes = Number(ctx.outboundSendQueueBytes || 0) + bytes;
167
167
  });
@@ -21,6 +21,7 @@ import {
21
21
  } from '../upgrade-command.js';
22
22
  import { sendToServer } from './buffer.js';
23
23
  import { stopAgentHeartbeat } from './heartbeat.js';
24
+ import { createUnixUpgradeRun, getUnixUpgradeLockPath, launchUnixUpgrade, shellQuote } from '../unix-upgrade.js';
24
25
 
25
26
  /**
26
27
  * Resolve the local service instance id for this running agent. Each agent
@@ -63,8 +64,11 @@ export function isUpgradeAvailable(currentVersion, latestVersion) {
63
64
  }
64
65
 
65
66
  // Shared cleanup logic for restart/upgrade
66
- function cleanupAndExit(exitCode) {
67
+ function cleanupAndExit(exitCode, { maxCleanupMs = null } = {}) {
67
68
  setTimeout(async () => {
69
+ // The updater waits for this process. Do not let an unbounded async cleanup
70
+ // outlive the handoff and strand an on-failure service after exit(0).
71
+ if (maxCleanupMs) setTimeout(() => process.exit(exitCode), maxCleanupMs).unref();
68
72
  for (const [, term] of ctx.terminals) {
69
73
  if (term.pty) { try { term.pty.kill(); } catch {} }
70
74
  if (term.timer) clearTimeout(term.timer);
@@ -282,7 +286,7 @@ export async function handleUpgradeAgent(correlation = {}) {
282
286
  }
283
287
 
284
288
  // 清理并退出,让升级脚本接管
285
- cleanupAndExit(0);
289
+ cleanupAndExit(0, { maxCleanupMs: 20_000 });
286
290
  } catch (e) {
287
291
  console.error('[Agent] Upgrade failed:', e.message);
288
292
  await sendUpgradeAck({ success: false, error: e.message });
@@ -416,11 +420,12 @@ export function buildUnixUpgradeScript({
416
420
  safePath,
417
421
  instanceId = DEFAULT_INSTANCE_ID,
418
422
  isDarwin = platform() === 'darwin',
423
+ run,
424
+ lockPath,
425
+ nodePath = process.execPath,
426
+ isSystemd = existsSync(join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`)),
419
427
  }) {
420
- const shPath = join(configDir, 'upgrade.sh');
421
428
  const serviceName = getServiceName(instanceId);
422
- const systemdUnitPath = join(homedir(), '.config', 'systemd', 'user', `${serviceName}.service`);
423
- const isSystemd = existsSync(systemdUnitPath);
424
429
  const plistPath = getLaunchdPlistPath(instanceId);
425
430
  const isLaunchd = isDarwin && existsSync(plistPath);
426
431
  const cwd = isGlobalInstall ? undefined : installDir;
@@ -428,16 +433,47 @@ export function buildUnixUpgradeScript({
428
433
  const shLines = [
429
434
  '#!/bin/bash',
430
435
  `PID=${pid}`,
431
- `PKG="${pkgName}@${targetVersion}"`,
432
- `NPM="${npmPath}"`,
433
- `LOGFILE="${join(configDir, 'logs', 'upgrade.log')}"`,
434
- `export PATH="${safePath}"`,
436
+ `PKG=${shellQuote(`${pkgName}@${targetVersion}`)}`,
437
+ `NPM=${shellQuote(npmPath)}`,
438
+ `LOGFILE=${shellQuote(join(configDir, 'logs', 'upgrade.log'))}`,
439
+ `export PATH=${shellQuote(safePath)}`,
435
440
  '',
436
441
  '# Redirect all output to log file',
437
- 'exec > "$LOGFILE" 2>&1',
442
+ 'exec >> "$LOGFILE" 2>&1 || exit 1',
438
443
  'echo "[Upgrade] Started at $(date)"',
439
444
  '',
440
- ...(cwd ? [`INSTALL_DIR="${cwd}"`] : []),
445
+ ...(cwd ? [`INSTALL_DIR=${shellQuote(cwd)}`] : []),
446
+ ...(run ? [
447
+ `RUN_DIR=${shellQuote(run.dir)}`,
448
+ `READY=${shellQuote(run.readyPath)}`,
449
+ `AUTHORIZED=${shellQuote(run.authorizePath)}`,
450
+ `ACCEPTED=${shellQuote(run.acceptedPath)}`,
451
+ `CANCELLED=${shellQuote(run.cancelPath)}`,
452
+ `ERROR_FILE=${shellQuote(run.errorPath)}`,
453
+ ...(lockPath ? [
454
+ `LOCK=${shellQuote(lockPath)}`,
455
+ 'if ! command -v flock >/dev/null || ! exec 9>"$LOCK"; then',
456
+ ' echo "Cannot acquire installation upgrade lock; Agent remains online" > "$ERROR_FILE"',
457
+ ' exit 1',
458
+ 'fi',
459
+ 'if ! flock -n 9; then',
460
+ ' echo "Another Agent is upgrading this shared installation; retry after it finishes" > "$ERROR_FILE"',
461
+ ' exit 1',
462
+ 'fi',
463
+ ] : []),
464
+ 'echo "$$" > "$READY" || exit 1',
465
+ 'COUNT=0',
466
+ 'while [ ! -f "$AUTHORIZED" ]; do',
467
+ ' if [ -f "$CANCELLED" ] || ! kill -0 "$PID" 2>/dev/null || [ "$COUNT" -ge 60 ]; then',
468
+ ' echo "[Upgrade] Handoff cancelled; installation unchanged"',
469
+ ' exit 1',
470
+ ' fi',
471
+ ' COUNT=$((COUNT+1))',
472
+ ' sleep 1',
473
+ 'done',
474
+ '[ ! -f "$CANCELLED" ] || exit 1',
475
+ 'touch "$ACCEPTED" || exit 1',
476
+ ] : []),
441
477
  '',
442
478
  '# Wait for current process to exit',
443
479
  'COUNT=0',
@@ -445,10 +481,11 @@ export function buildUnixUpgradeScript({
445
481
  ' COUNT=$((COUNT+1))',
446
482
  ' if [ $COUNT -ge 30 ]; then',
447
483
  ' echo "[Upgrade] Timeout waiting for PID $PID to exit"',
448
- ' break',
484
+ ' exit 1',
449
485
  ' fi',
450
486
  ' sleep 2',
451
487
  'done',
488
+ ...(run ? ['[ ! -f "$CANCELLED" ] || exit 1'] : []),
452
489
  '',
453
490
  ];
454
491
 
@@ -468,7 +505,10 @@ export function buildUnixUpgradeScript({
468
505
  'export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/user/$YEAFT_UID/bus}"',
469
506
  '',
470
507
  '# Stop systemd service to prevent restart loop',
471
- `systemctl --user stop "${serviceName}" 2>/dev/null`,
508
+ `if ! systemctl --user stop "${serviceName}"; then`,
509
+ ' echo "[Upgrade] Could not stop selected service; refusing to modify installation"',
510
+ ' exit 1',
511
+ 'fi',
472
512
  'sleep 1',
473
513
  '',
474
514
  );
@@ -487,10 +527,19 @@ export function buildUnixUpgradeScript({
487
527
  ? npmArgs.replace(/^npm /, '"$NPM" ')
488
528
  : `cd "$INSTALL_DIR" && ${npmArgs.replace(/^npm /, '"$NPM" ')}`;
489
529
 
530
+ // Recheck after acquiring the shared lock. An older queued process must not
531
+ // replace a package that another instance has already upgraded.
532
+ const installedManifest = join(installDir, 'node_modules', pkgName, 'package.json');
533
+ const versionCheck = `try { const fs = require('node:fs'); const v = JSON.parse(fs.readFileSync(process.argv[1])).version; const a = v.split('.').map(Number); const b = process.argv[2].split('.').map(Number); let d = 0; for (let i = 0; i < 3 && !d; i++) d = a[i] - b[i]; process.exit(d >= 0 ? 0 : 1); } catch { process.exit(1); }`;
490
534
  shLines.push(
535
+ `if ${shellQuote(nodePath)} -e ${shellQuote(versionCheck)} ${shellQuote(installedManifest)} ${shellQuote(targetVersion)}; then`,
536
+ ' echo "[Upgrade] Shared installation already has this version or newer; restarting selected instance only"',
537
+ ' EXIT_CODE=0',
538
+ 'else',
491
539
  'echo "[Upgrade] Installing $PKG..."',
492
540
  npmCmd,
493
541
  'EXIT_CODE=$?',
542
+ 'fi',
494
543
  'if [ $EXIT_CODE -ne 0 ]; then',
495
544
  ' echo "[Upgrade] npm install failed with exit code $EXIT_CODE"',
496
545
  'else',
@@ -527,8 +576,8 @@ export function buildUnixUpgradeScript({
527
576
  );
528
577
  }
529
578
 
530
- // 清理脚本自身
531
- shLines.push('', `rm -f "${shPath}"`);
579
+ // Keep failed handoffs for diagnosis; successful runs remove only their own files.
580
+ shLines.push('', run ? 'rm -rf -- "$RUN_DIR"' : `rm -f ${shellQuote(join(configDir, 'upgrade.sh'))}`);
532
581
 
533
582
  return shLines.join('\n');
534
583
  }
@@ -538,7 +587,16 @@ async function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, late
538
587
  // Create logs/ too: the generated script's `exec > "$LOGFILE" 2>&1` aborts
539
588
  // the whole upgrade silently if that directory is missing.
540
589
  mkdirSync(join(configDir, 'logs'), { recursive: true });
541
- const shPath = join(configDir, 'upgrade.sh');
590
+ const run = createUnixUpgradeRun(configDir);
591
+ const isSystemd = platform() === 'linux' && (
592
+ !!process.env.INVOCATION_ID
593
+ || existsSync(join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`))
594
+ );
595
+ // Linux flock is kernel-owned: it releases automatically even when an updater
596
+ // crashes. Never unlink the shared lock inode while another runner may use it.
597
+ const lockPath = platform() === 'linux'
598
+ ? getUnixUpgradeLockPath(installDir, join(getConfigDir(DEFAULT_INSTANCE_ID), 'upgrade-locks'))
599
+ : null;
542
600
 
543
601
  const script = buildUnixUpgradeScript({
544
602
  pkgName,
@@ -550,14 +608,20 @@ async function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, late
550
608
  npmPath,
551
609
  safePath,
552
610
  instanceId,
611
+ run,
612
+ lockPath,
613
+ isSystemd,
553
614
  });
554
615
 
555
- writeFileSync(shPath, script, { mode: 0o755 });
556
- const child = spawn('bash', [shPath], {
557
- detached: true,
558
- stdio: 'ignore',
616
+ writeFileSync(run.scriptPath, script, { mode: 0o700 });
617
+ await launchUnixUpgrade({
618
+ run,
619
+ instanceId,
620
+ isSystemd,
621
+ env: safeEnv,
622
+ serviceRunnerPath: fileURLToPath(new URL('../yeaft/systemd-service-runner.js', import.meta.url)),
623
+ sendUpgradeAck,
624
+ version: latestVersion,
559
625
  });
560
- child.unref();
561
- console.log(`[Agent] Spawned upgrade script: ${shPath}`);
562
- await sendUpgradeAck({ success: true, version: latestVersion, pendingRestart: true });
626
+ console.log(`[Agent] Upgrade runner ready outside service cgroup: ${run.scriptPath}`);
563
627
  }
@@ -1 +1 @@
1
- {"version":"1.0.674"}
1
+ {"version":"1.0.676"}
@@ -6,7 +6,7 @@
6
6
  <title>Yeaft</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%232c2c2c'/><path d='M16 22V13M16 13L10 8M16 13L22 8' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' fill='none'/><circle cx='10' cy='8' r='2.4' fill='white'/><circle cx='22' cy='8' r='2.4' fill='white'/><circle cx='16' cy='22' r='2.4' fill='white'/></svg>">
8
8
  <link rel="stylesheet" href="vendor/katex/katex.min.css?v=b0d91614">
9
- <link rel="stylesheet" href="style.bundle.css?v=33c9f008">
9
+ <link rel="stylesheet" href="style.bundle.css?v=e3fe0ff3">
10
10
  <script src="vendor.bundle.js?v=b4d2c175"></script>
11
11
  <script defer src="jszip.min.js"></script>
12
12
  <script defer src="docx-preview.min.js"></script>