openclaw-weiyuan-init 1.0.75 → 1.0.77

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/lib/commands.js +115 -75
  2. package/package.json +1 -1
package/lib/commands.js CHANGED
@@ -102,9 +102,20 @@ function decodeInviteToken(token) {
102
102
  const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
103
103
  const raw = Buffer.from(normalized + pad, 'base64').toString('utf8');
104
104
  const parsed = JSON.parse(raw);
105
- if (!parsed || parsed.v !== 1) return null;
106
- if (!parsed.api || !parsed.upgrade || !parsed.projectId || !parsed.code) return null;
107
- return parsed;
105
+ if (!parsed || (parsed.v !== 1 && parsed.v !== 2)) return null;
106
+ const api = String(parsed.api || parsed.a || '');
107
+ const upgrade = String(parsed.upgrade || parsed.u || '');
108
+ const projectId = String(parsed.projectId || parsed.p || '');
109
+ const code = String(parsed.code || parsed.c || '');
110
+ if (!api || !upgrade || !projectId || !code) return null;
111
+ return {
112
+ v: parsed.v,
113
+ api,
114
+ upgrade,
115
+ projectId,
116
+ code,
117
+ apis: Array.isArray(parsed.apis || parsed.s) ? (parsed.apis || parsed.s).map((x) => String(x)).filter(Boolean) : undefined
118
+ };
108
119
  } catch (_) {
109
120
  return null;
110
121
  }
@@ -155,6 +166,29 @@ async function sleep(ms) {
155
166
  await new Promise((resolve) => setTimeout(resolve, ms));
156
167
  }
157
168
 
169
+ async function runAcrossServerCandidates(candidates, action, runner) {
170
+ const retryWindowMs = Math.max(30_000, Number(process.env.WEIYUAN_JOIN_RETRY_WINDOW_MS || 180_000));
171
+ const deadline = Date.now() + retryWindowMs;
172
+ let lastError = null;
173
+ let round = 0;
174
+ while (Date.now() < deadline) {
175
+ round += 1;
176
+ for (const candidate of candidates) {
177
+ try {
178
+ await runner(candidate);
179
+ return candidate;
180
+ } catch (error) {
181
+ lastError = error;
182
+ if (!isRetryableGatewayError(error)) throw error;
183
+ }
184
+ }
185
+ await sleep(Math.min(1500 * round, 10000));
186
+ }
187
+ const timeoutError = new Error(`${action}_retry_window_exhausted`);
188
+ timeoutError.cause = lastError || undefined;
189
+ throw (lastError || timeoutError);
190
+ }
191
+
158
192
  async function runJoin(weiyuanPath, identityPath, projectId, code, fromInit = false) {
159
193
  const extra = fromInit ? ['--fromInit'] : [];
160
194
  const maxAttempts = 3;
@@ -237,24 +271,36 @@ async function runInit(options) {
237
271
  const force = options.force || false;
238
272
  const hasInviteJoin = Boolean(options.project && options.code);
239
273
  const identityPath = path.join(workspacePath, DEFAULT_CONFIG.identityFile);
274
+ const workspaceExists = await fs.pathExists(workspacePath);
275
+ const weiyuanExists = await fs.pathExists(weiyuanPath);
276
+ const identityExists = await fs.pathExists(identityPath);
240
277
 
241
278
  // 检查是否已存在
242
- if (!force && await fs.pathExists(workspacePath)) {
243
- if (hasInviteJoin && await fs.pathExists(weiyuanPath) && await fs.pathExists(identityPath)) {
279
+ if (!force && workspaceExists) {
280
+ if (hasInviteJoin && weiyuanExists && identityExists) {
244
281
  let spinner = ora('检测到已接入微元系统,直接加入项目...').start();
245
282
  try {
246
- await runJoin(weiyuanPath, identityPath, options.project, options.code, false);
283
+ await runAcrossServerCandidates(serverCandidates, 'join_existing_workspace', async (candidate) => {
284
+ const identity = await fs.readJson(identityPath);
285
+ identity.serverBaseUrl = candidate;
286
+ await fs.writeJson(identityPath, identity, { spaces: 2 });
287
+ await runJoin(weiyuanPath, identityPath, options.project, options.code, false);
288
+ });
247
289
  spinner.succeed(`已加入项目: ${options.project}`);
248
290
  console.log(renderFixedMessage(fixedMessages.sceneC, { projectId: options.project }));
249
291
  return;
250
292
  } catch (error) {
251
- spinner.fail(`自动入组失败: ${error.message}`);
293
+ spinner.fail(`自动入组失败: ${error ? error.message : 'unknown_error'}`);
252
294
  throw error;
253
295
  }
254
296
  }
255
- console.log(chalk.yellow(`\n⚠️ 工作目录已存在: ${workspacePath}`));
256
- console.log(chalk.gray(' 使用 --force 强制重新初始化\n'));
257
- return;
297
+ if (hasInviteJoin) {
298
+ console.log(chalk.yellow('\n⚠️ 检测到已有工作目录,准备继续执行补齐流程(身份/入组)...\n'));
299
+ } else {
300
+ console.log(chalk.yellow(`\n⚠️ 工作目录已存在: ${workspacePath}`));
301
+ console.log(chalk.gray(' 使用 --force 强制重新初始化\n'));
302
+ return;
303
+ }
258
304
  }
259
305
 
260
306
  // 1. 创建工作目录
@@ -282,39 +328,44 @@ async function runInit(options) {
282
328
  }
283
329
 
284
330
  // 3. 下载文件
285
- spinner = ora('解析下载包...').start();
286
- const source = await resolvePackageSource({
287
- downloadUrl: requestedDownloadUrl,
288
- upgradeBaseUrl,
289
- spinner
290
- });
291
- spinner.succeed(`下载源: ${source.downloadUrl}`);
292
-
293
- spinner = ora('下载 weiyuan skill...').start();
294
- const zipPath = path.join(weiyuanPath, source.zipFilename);
295
- const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner);
296
- if (!downloadSuccess) {
297
- spinner.fail('下载失败');
298
- throw new Error('下载失败');
299
- }
300
- spinner.succeed(`下载完成: ${source.zipFilename}`);
301
-
302
- // 4. 解压
303
- spinner = ora('解压文件...').start();
304
- const extractSuccess = await extractZip(zipPath, weiyuanPath);
305
- if (!extractSuccess) {
306
- spinner.fail('解压失败');
307
- throw new Error('解压失败');
308
- }
309
- spinner.succeed('解压完成');
331
+ if (!force && weiyuanExists) {
332
+ spinner = ora('检测到已有 weiyuan 运行目录,跳过下载与解压...').start();
333
+ spinner.succeed('已复用现有 weiyuan 目录');
334
+ } else {
335
+ spinner = ora('解析下载包...').start();
336
+ const source = await resolvePackageSource({
337
+ downloadUrl: requestedDownloadUrl,
338
+ upgradeBaseUrl,
339
+ spinner
340
+ });
341
+ spinner.succeed(`下载源: ${source.downloadUrl}`);
342
+
343
+ spinner = ora('下载 weiyuan skill...').start();
344
+ const zipPath = path.join(weiyuanPath, source.zipFilename);
345
+ const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner);
346
+ if (!downloadSuccess) {
347
+ spinner.fail('下载失败');
348
+ throw new Error('下载失败');
349
+ }
350
+ spinner.succeed(`下载完成: ${source.zipFilename}`);
351
+
352
+ // 4. 解压
353
+ spinner = ora('解压文件...').start();
354
+ const extractSuccess = await extractZip(zipPath, weiyuanPath);
355
+ if (!extractSuccess) {
356
+ spinner.fail('解压失败');
357
+ throw new Error('解压失败');
358
+ }
359
+ spinner.succeed('解压完成');
310
360
 
311
- // 5. 清理
312
- spinner = ora('清理临时文件...').start();
313
- try {
314
- await fs.remove(zipPath);
315
- spinner.succeed('清理完成');
316
- } catch (error) {
317
- spinner.warn('清理失败,可手动删除');
361
+ // 5. 清理
362
+ spinner = ora('清理临时文件...').start();
363
+ try {
364
+ await fs.remove(zipPath);
365
+ spinner.succeed('清理完成');
366
+ } catch (error) {
367
+ spinner.warn('清理失败,可手动删除');
368
+ }
318
369
  }
319
370
 
320
371
  // 6. 准备 CLI 运行时
@@ -329,29 +380,26 @@ async function runInit(options) {
329
380
 
330
381
  // 7. 创建身份文件
331
382
  spinner = ora('创建身份文件...').start();
332
- let identityCreated = false;
333
- let initError = null;
334
- for (const candidate of serverCandidates) {
335
- serverUrl = candidate;
383
+ if (identityExists && !force) {
384
+ spinner.succeed(`身份文件已存在: ${DEFAULT_CONFIG.identityFile}`);
385
+ } else {
336
386
  try {
337
- identityCreated = await runCliInit(weiyuanPath, identityPath, serverUrl);
338
- if (identityCreated) break;
387
+ serverUrl = await runAcrossServerCandidates(serverCandidates, 'identity_init', async (candidate) => {
388
+ try {
389
+ await runCliInit(weiyuanPath, identityPath, candidate);
390
+ return;
391
+ } catch (error) {
392
+ const created = await createIdentityFile(identityPath, candidate, workspacePath);
393
+ if (created) return;
394
+ throw error;
395
+ }
396
+ });
397
+ spinner.succeed(`身份文件: ${DEFAULT_CONFIG.identityFile}`);
339
398
  } catch (error) {
340
- initError = error;
341
- identityCreated = await createIdentityFile(identityPath, serverUrl, workspacePath);
342
- if (identityCreated) break;
399
+ spinner.fail(`身份文件创建失败: ${error ? error.message : 'unknown_error'}`);
400
+ throw error;
343
401
  }
344
402
  }
345
- if (!identityCreated && initError) {
346
- spinner.fail(`身份文件创建失败: ${initError.message}`);
347
- throw initError;
348
- }
349
- if (identityCreated) {
350
- spinner.succeed(`身份文件: ${DEFAULT_CONFIG.identityFile}`);
351
- } else {
352
- spinner.fail('身份文件创建失败,请检查 /v1/init 可用性');
353
- throw new Error('identity_create_failed');
354
- }
355
403
 
356
404
  // 8. 初始化 skill
357
405
  spinner = ora('初始化 weiyuan skill...').start();
@@ -364,24 +412,16 @@ async function runInit(options) {
364
412
 
365
413
  if (options.project && options.code) {
366
414
  spinner = ora('自动加入微元项目...').start();
367
- let joined = false;
368
- let joinErr = null;
369
- for (const candidate of serverCandidates) {
370
- try {
415
+ try {
416
+ serverUrl = await runAcrossServerCandidates(serverCandidates, 'join_after_init', async (candidate) => {
371
417
  const identity = await fs.readJson(identityPath);
372
418
  identity.serverBaseUrl = candidate;
373
419
  await fs.writeJson(identityPath, identity, { spaces: 2 });
374
420
  await runJoin(weiyuanPath, identityPath, options.project, options.code, true);
375
- joined = true;
376
- serverUrl = candidate;
377
- break;
378
- } catch (error) {
379
- joinErr = error;
380
- }
381
- }
382
- if (!joined) {
383
- spinner.fail(`自动入组失败: ${joinErr ? joinErr.message : 'unknown_error'}`);
384
- throw (joinErr || new Error('join_failed'));
421
+ });
422
+ } catch (error) {
423
+ spinner.fail(`自动入组失败: ${error ? error.message : 'unknown_error'}`);
424
+ throw error;
385
425
  }
386
426
  spinner.succeed(`已加入项目: ${options.project}`);
387
427
  console.log(renderFixedMessage(fixedMessages.sceneB, { projectId: options.project }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-weiyuan-init",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
4
4
  "description": "OpenClaw Weiyuan Skill 一键初始化工具",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {