openclaw-weiyuan-init 1.0.75 → 1.0.76
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/lib/commands.js +115 -75
- 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
|
-
|
|
107
|
-
|
|
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 &&
|
|
243
|
-
if (hasInviteJoin &&
|
|
279
|
+
if (!force && workspaceExists) {
|
|
280
|
+
if (hasInviteJoin && weiyuanExists && identityExists) {
|
|
244
281
|
let spinner = ora('检测到已接入微元系统,直接加入项目...').start();
|
|
245
282
|
try {
|
|
246
|
-
await
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
spinner
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
spinner
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
serverUrl = candidate;
|
|
383
|
+
if (identityExists && !force) {
|
|
384
|
+
spinner.succeed(`身份文件已存在: ${DEFAULT_CONFIG.identityFile}`);
|
|
385
|
+
} else {
|
|
336
386
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 }));
|