claw-subagent-service 0.0.42 → 0.0.43

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.42",
4
- "description": "虾说静态服务",
3
+ "version": "0.0.43",
4
+ "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
7
7
  "claw-subagent-service": "cli.js"
package/service/daemon.js CHANGED
@@ -211,10 +211,17 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
211
211
  healthTimer = null;
212
212
  }
213
213
 
214
- log.error(`[DAEMON] Worker 退出,code=${code}, signal=${signal}`);
215
214
  worker = null;
216
215
  currentWorkerPid = null;
217
216
 
217
+ // 正常退出(code=0, signal=null)视为优雅关闭,不增加崩溃计数
218
+ const isNormalExit = code === 0 && signal === null;
219
+ if (isNormalExit) {
220
+ log.info(`[DAEMON] Worker 正常退出,code=${code}, signal=${signal}`);
221
+ } else {
222
+ log.error(`[DAEMON] Worker 异常退出,code=${code}, signal=${signal}`);
223
+ }
224
+
218
225
  if (isAfterUpdate && code !== 0 && currentBackupDir && !isRollingBack) {
219
226
  isRollingBack = true;
220
227
  log.error('[DAEMON] 新版 Worker 启动后异常退出,触发自动回滚!');
@@ -234,9 +241,11 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
234
241
  }
235
242
 
236
243
  if (!stopping && !isRollingBack) {
237
- // 使用指数退避延迟后重启;端口释放交由 startWorker 中的 freePortIfNeeded 处理,
238
- // 避免此处误杀刚刚由其他实例启动的 worker
239
- const delay = getRestartDelay();
244
+ // 正常退出使用固定 1 秒延迟;异常退出使用指数退避
245
+ const delay = isNormalExit ? 1000 : getRestartDelay();
246
+ if (!isNormalExit) {
247
+ log.info(`[DAEMON] Worker 连续崩溃 ${crashCount} 次,等待 ${delay / 1000}s 后重启`);
248
+ }
240
249
  setTimeout(() => startWorker(false, null), delay);
241
250
  }
242
251
  });
@@ -270,10 +279,10 @@ function restartWorkerWithUpdate(backupDir) {
270
279
  execSync(`taskkill /pid ${worker.pid} /T /F 2>nul`, { windowsHide: true });
271
280
  } catch (e) {
272
281
  // 忽略错误,使用 kill 作为后备
273
- worker.kill('SIGTERM');
282
+ try { worker.kill('SIGTERM'); } catch { /* 进程可能已退出 */ }
274
283
  }
275
284
  } else {
276
- worker.kill('SIGTERM');
285
+ try { worker.kill('SIGTERM'); } catch { /* 进程可能已退出 */ }
277
286
  }
278
287
 
279
288
  const waitKill = setInterval(() => {
@@ -288,10 +297,10 @@ function restartWorkerWithUpdate(backupDir) {
288
297
  const { execSync } = require('child_process');
289
298
  execSync(`taskkill /pid ${worker.pid} /T /F 2>nul`, { windowsHide: true });
290
299
  } catch (e) {
291
- worker.kill('SIGKILL');
300
+ try { worker.kill('SIGKILL'); } catch { /* 进程可能已退出 */ }
292
301
  }
293
302
  } else {
294
- worker.kill('SIGKILL');
303
+ try { worker.kill('SIGKILL'); } catch { /* 进程可能已退出 */ }
295
304
  }
296
305
  }
297
306
  }, 500);
@@ -15,14 +15,25 @@ class Updater {
15
15
  this.currentVersion = this.loadCurrentVersion();
16
16
  this.isUpdating = false;
17
17
  this.updateTimer = null;
18
+ this.pkgJsonPath = path.join(__dirname, '..', 'package.json');
19
+ this.isGlobalInstall = this.detectGlobalInstall();
20
+ }
21
+
22
+ detectGlobalInstall() {
23
+ const cwd = path.normalize(__dirname).toLowerCase();
24
+ const inNodeModules = cwd.includes('node_modules');
25
+ const globalPaths = [
26
+ 'npm', 'node_modules', '.nvm', 'usr/local/lib', 'usr/lib',
27
+ ];
28
+ const inGlobalPath = globalPaths.some(p => cwd.includes(p.toLowerCase()));
29
+ return inNodeModules || inGlobalPath;
18
30
  }
19
31
 
20
32
  loadCurrentVersion() {
21
33
  try {
22
- // 从全局安装的 package.json 读取版本
23
- const globalPkg = path.join(__dirname, '..', 'package.json');
24
- if (fs.existsSync(globalPkg)) {
25
- const pkg = JSON.parse(fs.readFileSync(globalPkg, 'utf8'));
34
+ const pkgPath = path.join(__dirname, '..', 'package.json');
35
+ if (fs.existsSync(pkgPath)) {
36
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
37
  return pkg.version || '0.0.0';
27
38
  }
28
39
  } catch {
@@ -77,20 +88,19 @@ class Updater {
77
88
  try {
78
89
  log.info(`[UPDATER] 开始更新: ${this.currentVersion} → ${info.version}`);
79
90
 
80
- // 使用 npm 全局安装最新版本
81
- const { stdout, stderr } = await execAsync(
82
- `npm install -g ${PACKAGE_NAME}@latest`,
83
- { timeout: 300000, windowsHide: true }
84
- );
85
-
86
- if (stderr) {
87
- log.warn(`[UPDATER] npm 安装警告: ${stderr.substring(0, 500)}`);
91
+ let updateOk = false;
92
+ if (this.isGlobalInstall) {
93
+ updateOk = await this.updateGlobalPackage(info.version);
94
+ } else {
95
+ updateOk = await this.updateLocalPackage(info.version);
88
96
  }
89
97
 
90
- log.info(`[UPDATER] npm 安装输出: ${stdout.substring(0, 500)}`);
91
- log.info(`[UPDATER] 更新完成,准备重启 Worker`);
98
+ if (!updateOk) {
99
+ return { success: false, error: '版本未变化' };
100
+ }
92
101
 
93
102
  this.currentVersion = info.version;
103
+ log.info(`[UPDATER] 更新完成,当前版本: ${this.currentVersion},准备重启 Worker`);
94
104
  return { success: true };
95
105
  } catch (err) {
96
106
  log.error(`[UPDATER] 更新失败: ${err.message}`);
@@ -100,16 +110,136 @@ class Updater {
100
110
  }
101
111
  }
102
112
 
113
+ async updateGlobalPackage(version) {
114
+ const { stdout, stderr } = await execAsync(
115
+ `npm install -g ${PACKAGE_NAME}@${version}`,
116
+ { timeout: 300000, windowsHide: true }
117
+ );
118
+
119
+ if (stderr) {
120
+ log.warn(`[UPDATER] npm 安装警告: ${stderr.substring(0, 500)}`);
121
+ }
122
+ log.info(`[UPDATER] npm 安装输出: ${stdout.substring(0, 500)}`);
123
+
124
+ // 全局安装后重新读取版本确认
125
+ const newVersion = this.loadCurrentVersion();
126
+ if (this.compareVersion(newVersion, this.currentVersion) <= 0) {
127
+ log.warn(`[UPDATER] 更新后版本未变化: ${this.currentVersion} → ${newVersion},可能安装路径不一致`);
128
+ return false;
129
+ }
130
+ return true;
131
+ }
132
+
133
+ async updateLocalPackage(version) {
134
+ const pkgDir = path.dirname(this.pkgJsonPath);
135
+ const tmpDir = path.join(pkgDir, '.update-tmp');
136
+
137
+ try {
138
+ // 1. 清理临时目录
139
+ if (fs.existsSync(tmpDir)) {
140
+ fs.rmSync(tmpDir, { recursive: true, force: true });
141
+ }
142
+ fs.mkdirSync(tmpDir, { recursive: true });
143
+
144
+ // 2. 下载 tarball
145
+ log.info(`[UPDATER] 下载 ${PACKAGE_NAME}@${version} ...`);
146
+ const { stdout } = await execAsync(
147
+ `npm pack ${PACKAGE_NAME}@${version}`,
148
+ { cwd: tmpDir, timeout: 120000, windowsHide: true }
149
+ );
150
+ const tarballName = stdout.trim().split('\n').pop();
151
+ const tarballPath = path.join(tmpDir, tarballName);
152
+ if (!fs.existsSync(tarballPath)) {
153
+ throw new Error('tarball 下载失败');
154
+ }
155
+
156
+ // 3. 解压 tarball
157
+ log.info(`[UPDATER] 解压 ${tarballName} ...`);
158
+ await execAsync(
159
+ `tar -xzf ${tarballPath} -C ${tmpDir}`,
160
+ { timeout: 30000 }
161
+ );
162
+
163
+ // 4. 复制 package/ 下的文件覆盖当前目录
164
+ const extractedDir = path.join(tmpDir, 'package');
165
+ if (!fs.existsSync(extractedDir)) {
166
+ throw new Error('解压后未找到 package 目录');
167
+ }
168
+
169
+ log.info(`[UPDATER] 覆盖本地文件...`);
170
+ this.copyDirSync(extractedDir, pkgDir);
171
+
172
+ // 5. 重新安装依赖(因为 node_modules 被覆盖)
173
+ log.info(`[UPDATER] 重新安装依赖...`);
174
+ const { stdout: installOut, stderr: installErr } = await execAsync(
175
+ 'npm install --production',
176
+ { cwd: pkgDir, timeout: 300000, windowsHide: true }
177
+ );
178
+ if (installErr) {
179
+ log.warn(`[UPDATER] npm install 警告: ${installErr.substring(0, 500)}`);
180
+ }
181
+ log.info(`[UPDATER] npm install 输出: ${installOut.substring(0, 500)}`);
182
+
183
+ // 6. 验证版本
184
+ const newVersion = this.loadCurrentVersion();
185
+ if (this.compareVersion(newVersion, this.currentVersion) <= 0) {
186
+ log.warn(`[UPDATER] 更新后版本未变化: ${this.currentVersion} → ${newVersion}`);
187
+ return false;
188
+ }
189
+
190
+ log.info(`[UPDATER] 本地更新成功: ${this.currentVersion} → ${newVersion}`);
191
+ return true;
192
+ } catch (err) {
193
+ log.error(`[UPDATER] 本地更新异常: ${err.message}`);
194
+ return false;
195
+ } finally {
196
+ // 清理临时目录
197
+ try {
198
+ if (fs.existsSync(tmpDir)) {
199
+ fs.rmSync(tmpDir, { recursive: true, force: true });
200
+ }
201
+ } catch { /* 忽略 */ }
202
+ }
203
+ }
204
+
205
+ copyDirSync(src, dest) {
206
+ const entries = fs.readdirSync(src, { withFileTypes: true });
207
+ for (const entry of entries) {
208
+ const srcPath = path.join(src, entry.name);
209
+ const destPath = path.join(dest, entry.name);
210
+
211
+ // 跳过隐藏文件、日志和临时目录
212
+ if (entry.name.startsWith('.') || entry.name === 'logs') {
213
+ continue;
214
+ }
215
+
216
+ if (entry.isDirectory()) {
217
+ if (!fs.existsSync(destPath)) {
218
+ fs.mkdirSync(destPath, { recursive: true });
219
+ }
220
+ this.copyDirSync(srcPath, destPath);
221
+ } else {
222
+ fs.copyFileSync(srcPath, destPath);
223
+ }
224
+ }
225
+ }
226
+
103
227
  startSchedule(restartWorkerCallback) {
104
228
  this.restartWorker = restartWorkerCallback;
105
229
 
230
+ // Docker / 本地开发环境可通过环境变量禁用自动更新
231
+ if (process.env.DISABLE_AUTO_UPDATE === 'true' || process.env.DISABLE_AUTO_UPDATE === '1') {
232
+ log.info('[UPDATER] 已禁用自动更新(DISABLE_AUTO_UPDATE 已设置)');
233
+ return;
234
+ }
235
+
106
236
  const run = async () => {
107
237
  try {
108
238
  const info = await this.check();
109
239
  if (info) {
110
240
  const result = await this.execute(info);
111
241
  if (result.success) {
112
- // npm 全局安装完成后,直接重启 Worker 即可加载新代码
242
+ // npm 安装完成后,直接重启 Worker 即可加载新代码
113
243
  this.restartWorker(null);
114
244
  }
115
245
  }