openclaw-weiyuan-init 1.0.69 → 1.0.73

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/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ program
14
14
  .description('初始化 weiyuan skill')
15
15
  .option('-w, --workspace <path>', '指定工作目录', 'workspace-weiyuan')
16
16
  .option('-s, --server <url>', '指定服务器地址', 'https://api.magon.com.cn/api')
17
+ .option('--server-fallbacks <urls>', '备用服务器地址,逗号分隔,如 "https://a/api,https://b/api"')
17
18
  .option('-u, --upgrade <url>', '指定升级源地址(含 LATEST_SKILL_VERSION.txt)', 'https://api.magon.com.cn/upgrade')
18
19
  .option('-d, --download <url>', '指定下载地址(可覆盖自动版本解析)')
19
20
  .option('-i, --invite <token>', '邀请码令牌(包含 server/upgrade/project/code)')
package/lib/commands.js CHANGED
@@ -16,7 +16,8 @@ const DEFAULT_CONFIG = {
16
16
  upgradeBaseUrl: 'https://api.magon.com.cn/upgrade',
17
17
  downloadUrl: '',
18
18
  serverUrl: 'https://api.magon.com.cn/api',
19
- identityFile: '.weiyuan'
19
+ identityFile: '.weiyuan',
20
+ serverFallbacks: []
20
21
  };
21
22
 
22
23
  const DEFAULT_SKILL_PACKAGE_JSON = {
@@ -109,18 +110,75 @@ function decodeInviteToken(token) {
109
110
  }
110
111
  }
111
112
 
112
- async function runJoin(weiyuanPath, identityPath, projectId, code, fromInit = false) {
113
- const extra = fromInit ? ['--fromInit'] : [];
113
+ function normalizeServerUrl(raw) {
114
+ if (!raw || typeof raw !== 'string') return '';
114
115
  try {
115
- await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'join', '--identity', identityPath, '--project', projectId, '--code', code, ...extra], {
116
- cwd: weiyuanPath
117
- });
118
- return;
116
+ const url = new URL(raw.trim());
117
+ const p = (url.pathname || '/').replace(/\/+$/, '');
118
+ if (!p || p === '/') url.pathname = '/api';
119
+ url.hash = '';
120
+ return url.toString().replace(/\/$/, '');
119
121
  } catch (_) {
122
+ return '';
120
123
  }
121
- await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'join', '--identity', identityPath, '--project', projectId, '--code', code, ...extra], {
122
- cwd: weiyuanPath
123
- });
124
+ }
125
+
126
+ function parseServerFallbacks(raw) {
127
+ if (!raw || typeof raw !== 'string') return [];
128
+ return raw
129
+ .split(',')
130
+ .map((x) => normalizeServerUrl(x))
131
+ .filter(Boolean);
132
+ }
133
+
134
+ function resolveServerCandidates(options, invite) {
135
+ const envFallbacks = parseServerFallbacks(process.env.WEIYUAN_SERVER_FALLBACKS || '');
136
+ const optionFallbacks = parseServerFallbacks(options.serverFallbacks || '');
137
+ const inviteFallbacks = Array.isArray(invite && invite.apis) ? invite.apis.map((x) => normalizeServerUrl(String(x))) : [];
138
+ const seeds = [
139
+ normalizeServerUrl(options.server || ''),
140
+ normalizeServerUrl(invite && invite.api ? invite.api : ''),
141
+ ...inviteFallbacks,
142
+ ...optionFallbacks,
143
+ ...envFallbacks,
144
+ normalizeServerUrl(DEFAULT_CONFIG.serverUrl)
145
+ ].filter(Boolean);
146
+ return [...new Set(seeds)];
147
+ }
148
+
149
+ function isRetryableGatewayError(error) {
150
+ const text = String((error && (error.stderr || error.stdout || error.message)) || '').toLowerCase();
151
+ return /502|bad gateway|econnreset|etimedout|eai_again|socket hang up|network error/.test(text);
152
+ }
153
+
154
+ async function sleep(ms) {
155
+ await new Promise((resolve) => setTimeout(resolve, ms));
156
+ }
157
+
158
+ async function runJoin(weiyuanPath, identityPath, projectId, code, fromInit = false) {
159
+ const extra = fromInit ? ['--fromInit'] : [];
160
+ const maxAttempts = 3;
161
+ let lastError = null;
162
+ for (let i = 0; i < maxAttempts; i++) {
163
+ try {
164
+ await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'join', '--identity', identityPath, '--project', projectId, '--code', code, ...extra], {
165
+ cwd: weiyuanPath
166
+ });
167
+ return;
168
+ } catch (_) {
169
+ }
170
+ try {
171
+ await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'join', '--identity', identityPath, '--project', projectId, '--code', code, ...extra], {
172
+ cwd: weiyuanPath
173
+ });
174
+ return;
175
+ } catch (error) {
176
+ lastError = error;
177
+ if (i >= maxAttempts - 1 || !isRetryableGatewayError(error)) throw error;
178
+ await sleep(1200 * (i + 1));
179
+ }
180
+ }
181
+ if (lastError) throw lastError;
124
182
  }
125
183
 
126
184
  async function ensureSkillRuntime(weiyuanPath) {
@@ -136,16 +194,25 @@ async function ensureSkillRuntime(weiyuanPath) {
136
194
  }
137
195
 
138
196
  async function runCliInit(weiyuanPath, identityPath, serverUrl) {
139
- try {
140
- await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'init', '--server', serverUrl, '--out', identityPath], {
141
- cwd: weiyuanPath
142
- });
143
- return true;
144
- } catch (_) {
197
+ const maxAttempts = 3;
198
+ for (let i = 0; i < maxAttempts; i++) {
199
+ try {
200
+ await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'init', '--server', serverUrl, '--out', identityPath], {
201
+ cwd: weiyuanPath
202
+ });
203
+ return true;
204
+ } catch (_) {
205
+ }
206
+ try {
207
+ await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'init', '--server', serverUrl, '--out', identityPath], {
208
+ cwd: weiyuanPath
209
+ });
210
+ return true;
211
+ } catch (error) {
212
+ if (i >= maxAttempts - 1 || !isRetryableGatewayError(error)) throw error;
213
+ await sleep(1200 * (i + 1));
214
+ }
145
215
  }
146
- await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'init', '--server', serverUrl, '--out', identityPath], {
147
- cwd: weiyuanPath
148
- });
149
216
  return true;
150
217
  }
151
218
 
@@ -165,7 +232,8 @@ async function runInit(options) {
165
232
  const weiyuanPath = path.join(workspacePath, 'weiyuan');
166
233
  const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
167
234
  const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
168
- const serverUrl = options.server || DEFAULT_CONFIG.serverUrl;
235
+ const serverCandidates = resolveServerCandidates(options, invite);
236
+ let serverUrl = serverCandidates[0] || DEFAULT_CONFIG.serverUrl;
169
237
  const force = options.force || false;
170
238
  const hasInviteJoin = Boolean(options.project && options.code);
171
239
  const identityPath = path.join(workspacePath, DEFAULT_CONFIG.identityFile);
@@ -201,11 +269,16 @@ async function runInit(options) {
201
269
 
202
270
  // 2. 检查服务器
203
271
  spinner = ora('检查服务器连接...').start();
204
- const serverOk = await checkServer(serverUrl);
205
- if (serverOk) {
272
+ let selectedServer = "";
273
+ for (const candidate of serverCandidates) {
274
+ const ok = await checkServer(candidate);
275
+ if (ok) { selectedServer = candidate; break; }
276
+ }
277
+ if (selectedServer) {
278
+ serverUrl = selectedServer;
206
279
  spinner.succeed(`服务器: ${serverUrl}`);
207
280
  } else {
208
- spinner.warn(`服务器连接失败,请检查网络和防火墙`);
281
+ spinner.warn(`服务器连接失败,继续尝试初始化(候选: ${serverCandidates.join(", ")})`);
209
282
  }
210
283
 
211
284
  // 3. 下载文件
@@ -257,10 +330,21 @@ async function runInit(options) {
257
330
  // 7. 创建身份文件
258
331
  spinner = ora('创建身份文件...').start();
259
332
  let identityCreated = false;
260
- try {
261
- identityCreated = await runCliInit(weiyuanPath, identityPath, serverUrl);
262
- } catch (_) {
263
- identityCreated = await createIdentityFile(identityPath, serverUrl, workspacePath);
333
+ let initError = null;
334
+ for (const candidate of serverCandidates) {
335
+ serverUrl = candidate;
336
+ try {
337
+ identityCreated = await runCliInit(weiyuanPath, identityPath, serverUrl);
338
+ if (identityCreated) break;
339
+ } catch (error) {
340
+ initError = error;
341
+ identityCreated = await createIdentityFile(identityPath, serverUrl, workspacePath);
342
+ if (identityCreated) break;
343
+ }
344
+ }
345
+ if (!identityCreated && initError) {
346
+ spinner.fail(`身份文件创建失败: ${initError.message}`);
347
+ throw initError;
264
348
  }
265
349
  if (identityCreated) {
266
350
  spinner.succeed(`身份文件: ${DEFAULT_CONFIG.identityFile}`);
@@ -280,14 +364,27 @@ async function runInit(options) {
280
364
 
281
365
  if (options.project && options.code) {
282
366
  spinner = ora('自动加入微元项目...').start();
283
- try {
284
- await runJoin(weiyuanPath, identityPath, options.project, options.code, true);
285
- spinner.succeed(`已加入项目: ${options.project}`);
286
- console.log(renderFixedMessage(fixedMessages.sceneB, { projectId: options.project }));
287
- } catch (error) {
288
- spinner.fail(`自动入组失败: ${error.message}`);
289
- throw error;
367
+ let joined = false;
368
+ let joinErr = null;
369
+ for (const candidate of serverCandidates) {
370
+ try {
371
+ const identity = await fs.readJson(identityPath);
372
+ identity.serverBaseUrl = candidate;
373
+ await fs.writeJson(identityPath, identity, { spaces: 2 });
374
+ 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'));
290
385
  }
386
+ spinner.succeed(`已加入项目: ${options.project}`);
387
+ console.log(renderFixedMessage(fixedMessages.sceneB, { projectId: options.project }));
291
388
  return;
292
389
  }
293
390
  console.log(fixedMessages.sceneA);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-weiyuan-init",
3
- "version": "1.0.69",
3
+ "version": "1.0.73",
4
4
  "description": "OpenClaw Weiyuan Skill 一键初始化工具",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {