openclaw-weiyuan-init 1.0.118 → 1.0.127
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 +2 -1
- package/lib/commands.js +118 -43
- package/lib/identity.js +63 -6
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const { runInit, runClean, runStatus } = require('../lib/commands');
|
|
6
|
+
const { version } = require('../package.json');
|
|
6
7
|
|
|
7
8
|
program
|
|
8
9
|
.name('openclaw-weiyuan-init')
|
|
9
10
|
.description('OpenClaw Weiyuan Skill 一键初始化工具')
|
|
10
|
-
.version(
|
|
11
|
+
.version(version);
|
|
11
12
|
|
|
12
13
|
program
|
|
13
14
|
.command('init')
|
package/lib/commands.js
CHANGED
|
@@ -60,6 +60,15 @@ const DEFAULT_SKILL_TSCONFIG = {
|
|
|
60
60
|
|
|
61
61
|
const REQUIRED_SKILL_ACTIONS = ['project.list', 'task.list_all', 'context.candidates'];
|
|
62
62
|
|
|
63
|
+
function cleanCliRawText(raw) {
|
|
64
|
+
if (raw === undefined || raw === null) return '';
|
|
65
|
+
let text = String(raw).replace(/\r|\n/g, ' ').replace(/`/g, '').trim();
|
|
66
|
+
while ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
67
|
+
text = text.slice(1, -1).trim();
|
|
68
|
+
}
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function bundledSkillMetadataPath(fileName) {
|
|
64
73
|
return path.join(__dirname, '..', 'workspace-weiyuan', 'weiyuan', fileName);
|
|
65
74
|
}
|
|
@@ -447,7 +456,19 @@ async function persistReleaseNotes(weiyuanPath, notes) {
|
|
|
447
456
|
|
|
448
457
|
function resolveWorkspacePath(workspaceOption) {
|
|
449
458
|
const raw = (workspaceOption || DEFAULT_CONFIG.workspaceName).trim();
|
|
450
|
-
const
|
|
459
|
+
const cwd = process.cwd();
|
|
460
|
+
const workspaceName = DEFAULT_CONFIG.workspaceName.toLowerCase();
|
|
461
|
+
const cwdBase = path.basename(cwd).toLowerCase();
|
|
462
|
+
const rawParts = raw.replace(/\\/g, '/').split('/').map((x) => String(x || '').trim()).filter(Boolean);
|
|
463
|
+
if (cwdBase === workspaceName) {
|
|
464
|
+
if (!raw || raw === '.' || raw === './' || raw === '.\\') {
|
|
465
|
+
return cwd;
|
|
466
|
+
}
|
|
467
|
+
if (rawParts.length > 0 && rawParts.every((part) => part.toLowerCase() === workspaceName)) {
|
|
468
|
+
return cwd;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const target = path.isAbsolute(raw) ? raw : path.join(cwd, raw);
|
|
451
472
|
if (path.basename(target).toLowerCase() === DEFAULT_CONFIG.workspaceName.toLowerCase()) {
|
|
452
473
|
return target;
|
|
453
474
|
}
|
|
@@ -457,7 +478,7 @@ function resolveWorkspacePath(workspaceOption) {
|
|
|
457
478
|
function decodeInviteToken(token) {
|
|
458
479
|
if (!token || typeof token !== 'string') return null;
|
|
459
480
|
try {
|
|
460
|
-
const normalized = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
481
|
+
const normalized = cleanCliRawText(token).replace(/-/g, '+').replace(/_/g, '/');
|
|
461
482
|
const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
|
462
483
|
const raw = Buffer.from(normalized + pad, 'base64').toString('utf8');
|
|
463
484
|
const parsed = JSON.parse(raw);
|
|
@@ -481,9 +502,10 @@ function decodeInviteToken(token) {
|
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
function normalizeServerUrl(raw) {
|
|
484
|
-
|
|
505
|
+
const cleaned = cleanCliRawText(raw);
|
|
506
|
+
if (!cleaned) return '';
|
|
485
507
|
try {
|
|
486
|
-
const url = new URL(
|
|
508
|
+
const url = new URL(cleaned);
|
|
487
509
|
const p = (url.pathname || '/').replace(/\/+$/, '');
|
|
488
510
|
if (!p || p === '/') url.pathname = '/api';
|
|
489
511
|
url.hash = '';
|
|
@@ -493,6 +515,30 @@ function normalizeServerUrl(raw) {
|
|
|
493
515
|
}
|
|
494
516
|
}
|
|
495
517
|
|
|
518
|
+
function normalizeUpgradeUrl(raw) {
|
|
519
|
+
const cleaned = cleanCliRawText(raw);
|
|
520
|
+
if (!cleaned) return '';
|
|
521
|
+
try {
|
|
522
|
+
const url = new URL(cleaned);
|
|
523
|
+
url.hash = '';
|
|
524
|
+
return url.toString().replace(/\/+$/, '');
|
|
525
|
+
} catch (_) {
|
|
526
|
+
return '';
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeDownloadUrl(raw) {
|
|
531
|
+
const cleaned = cleanCliRawText(raw);
|
|
532
|
+
if (!cleaned) return '';
|
|
533
|
+
try {
|
|
534
|
+
const url = new URL(cleaned);
|
|
535
|
+
url.hash = '';
|
|
536
|
+
return url.toString();
|
|
537
|
+
} catch (_) {
|
|
538
|
+
return '';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
496
542
|
function isAgentRegisterServerUrl(raw) {
|
|
497
543
|
if (!raw || typeof raw !== 'string') return false;
|
|
498
544
|
try {
|
|
@@ -641,6 +687,7 @@ async function printAgentConversationGuide(identityPath, projectId) {
|
|
|
641
687
|
async function ensureSkillRuntime(weiyuanPath) {
|
|
642
688
|
const pkgPath = path.join(weiyuanPath, 'package.json');
|
|
643
689
|
const tsconfigPath = path.join(weiyuanPath, 'tsconfig.json');
|
|
690
|
+
const bundledManifestPath = bundledSkillMetadataPath('manifest.json');
|
|
644
691
|
const capabilityCompatContent = `export const CAPABILITY_ACTION = {
|
|
645
692
|
PROJECT_DELETE: "project.delete",
|
|
646
693
|
TASK_DELETE: "task.delete",
|
|
@@ -656,8 +703,15 @@ async function ensureSkillRuntime(weiyuanPath) {
|
|
|
656
703
|
|
|
657
704
|
export const CAPABILITY_ACTION_ALLOWLIST = new Set<string>(Object.values(CAPABILITY_ACTION))
|
|
658
705
|
`;
|
|
706
|
+
let fallbackPackageJson = { ...DEFAULT_SKILL_PACKAGE_JSON };
|
|
707
|
+
try {
|
|
708
|
+
const bundledManifest = await fs.readJson(bundledManifestPath);
|
|
709
|
+
const bundledVersion = String(bundledManifest && bundledManifest.version || '').trim();
|
|
710
|
+
if (bundledVersion) fallbackPackageJson = { ...fallbackPackageJson, version: bundledVersion };
|
|
711
|
+
} catch (_) {
|
|
712
|
+
}
|
|
659
713
|
if (!await fs.pathExists(pkgPath)) {
|
|
660
|
-
await fs.writeJson(pkgPath,
|
|
714
|
+
await fs.writeJson(pkgPath, fallbackPackageJson, { spaces: 2 });
|
|
661
715
|
}
|
|
662
716
|
if (!await fs.pathExists(tsconfigPath)) {
|
|
663
717
|
await fs.writeJson(tsconfigPath, DEFAULT_SKILL_TSCONFIG, { spaces: 2 });
|
|
@@ -706,10 +760,16 @@ async function runInit(options) {
|
|
|
706
760
|
printBanner();
|
|
707
761
|
const fixedMessages = getFixedMessages();
|
|
708
762
|
let initIdentityInfo = null;
|
|
763
|
+
options = options || {};
|
|
764
|
+
options.server = cleanCliRawText(options.server || '');
|
|
765
|
+
options.upgrade = cleanCliRawText(options.upgrade || '');
|
|
766
|
+
options.download = cleanCliRawText(options.download || '');
|
|
767
|
+
options.invite = cleanCliRawText(options.invite || '');
|
|
768
|
+
options.project = cleanCliRawText(options.project || '');
|
|
769
|
+
options.code = cleanCliRawText(options.code || '');
|
|
709
770
|
|
|
710
771
|
const invite = decodeInviteToken(options.invite);
|
|
711
772
|
if (invite) {
|
|
712
|
-
options.server = options.server || invite.api;
|
|
713
773
|
options.upgrade = options.upgrade || invite.upgrade;
|
|
714
774
|
options.project = options.project || invite.projectId;
|
|
715
775
|
options.code = options.code || invite.code;
|
|
@@ -717,8 +777,8 @@ async function runInit(options) {
|
|
|
717
777
|
|
|
718
778
|
const workspacePath = resolveWorkspacePath(options.workspace);
|
|
719
779
|
const weiyuanPath = path.join(workspacePath, 'weiyuan');
|
|
720
|
-
const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
|
|
721
|
-
const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
|
|
780
|
+
const upgradeBaseUrl = normalizeUpgradeUrl(options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl) || DEFAULT_CONFIG.upgradeBaseUrl;
|
|
781
|
+
const requestedDownloadUrl = normalizeDownloadUrl(options.download || DEFAULT_CONFIG.downloadUrl);
|
|
722
782
|
const serverCandidates = resolveServerCandidates(options, invite);
|
|
723
783
|
let serverUrl = serverCandidates[0] || DEFAULT_CONFIG.serverUrl;
|
|
724
784
|
const force = options.force || false;
|
|
@@ -810,42 +870,57 @@ async function runInit(options) {
|
|
|
810
870
|
spinner = ora('检测到已有 weiyuan 运行目录,跳过下载与解压...').start();
|
|
811
871
|
spinner.succeed('已复用现有 weiyuan 目录');
|
|
812
872
|
} else {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
873
|
+
const maxDownloadAttempts = 2;
|
|
874
|
+
let downloadCompleted = false;
|
|
875
|
+
let lastDownloadError = null;
|
|
876
|
+
for (let attempt = 1; attempt <= maxDownloadAttempts; attempt += 1) {
|
|
877
|
+
let source = null;
|
|
878
|
+
let zipPath = '';
|
|
879
|
+
try {
|
|
880
|
+
spinner = ora(attempt > 1 ? `重新解析下载包(第 ${attempt} 次)...` : '解析下载包...').start();
|
|
881
|
+
source = await resolvePackageSource({
|
|
882
|
+
downloadUrl: requestedDownloadUrl,
|
|
883
|
+
upgradeBaseUrl,
|
|
884
|
+
spinner
|
|
885
|
+
});
|
|
886
|
+
spinner.succeed(`下载源: ${source.downloadUrl}`);
|
|
887
|
+
printReleaseNotes(source.releaseNotes);
|
|
888
|
+
|
|
889
|
+
spinner = ora(attempt > 1 ? `重新下载 weiyuan skill(第 ${attempt} 次)...` : '下载 weiyuan skill...').start();
|
|
890
|
+
zipPath = path.join(weiyuanPath, source.zipFilename);
|
|
891
|
+
await fs.remove(zipPath).catch(() => {});
|
|
892
|
+
const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner, source.sha256 || '');
|
|
893
|
+
if (!downloadSuccess) throw new Error('下载失败');
|
|
894
|
+
spinner.succeed(`下载完成: ${source.zipFilename}`);
|
|
895
|
+
|
|
896
|
+
spinner = ora('解压文件...').start();
|
|
897
|
+
const extractSuccess = await extractZip(zipPath, weiyuanPath);
|
|
898
|
+
if (!extractSuccess) throw new Error('解压失败');
|
|
899
|
+
spinner.succeed('解压完成');
|
|
900
|
+
|
|
901
|
+
spinner = ora('清理临时文件...').start();
|
|
902
|
+
try {
|
|
903
|
+
await fs.remove(zipPath);
|
|
904
|
+
await persistReleaseNotes(weiyuanPath, source.releaseNotes || null);
|
|
905
|
+
spinner.succeed('清理完成');
|
|
906
|
+
} catch (error) {
|
|
907
|
+
spinner.warn('清理失败,可手动删除');
|
|
908
|
+
}
|
|
909
|
+
downloadCompleted = true;
|
|
910
|
+
lastDownloadError = null;
|
|
911
|
+
break;
|
|
912
|
+
} catch (error) {
|
|
913
|
+
lastDownloadError = error;
|
|
914
|
+
if (zipPath) await fs.remove(zipPath).catch(() => {});
|
|
915
|
+
const msg = String(error && error.message || error || '');
|
|
916
|
+
const canRetry = attempt < maxDownloadAttempts && msg.includes('skill_package_hash_mismatch');
|
|
917
|
+
if (spinner) {
|
|
918
|
+
spinner.warn(canRetry ? '下载包哈希校验未通过,正在重新获取最新版本后重试...' : `下载失败:${msg || 'unknown_error'}`);
|
|
919
|
+
}
|
|
920
|
+
if (!canRetry) throw error;
|
|
921
|
+
}
|
|
848
922
|
}
|
|
923
|
+
if (!downloadCompleted && lastDownloadError) throw lastDownloadError;
|
|
849
924
|
}
|
|
850
925
|
|
|
851
926
|
// 6. 准备 CLI 运行时
|
package/lib/identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
3
4
|
const crypto = require('crypto');
|
|
4
5
|
const axios = require('axios');
|
|
5
6
|
const naclImport = require('tweetnacl');
|
|
@@ -64,12 +65,66 @@ function normalizeBusinessServerUrl(serverUrl) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
function applyIdentityProxyMetadata(identity, initInfo) {
|
|
69
|
+
const next = identity && typeof identity === 'object' ? { ...identity } : {};
|
|
70
|
+
const lobsterId = String(next.lobsterId || '').trim();
|
|
71
|
+
const effectiveLobsterId = String(initInfo && initInfo.effectiveLobsterId || '').trim();
|
|
72
|
+
const ownerLobsterId = String(initInfo && initInfo.ownerLobsterId || '').trim();
|
|
73
|
+
const accountModeRaw = String(initInfo && initInfo.accountMode || initInfo && initInfo.mode || '').trim();
|
|
74
|
+
const accountMode = accountModeRaw === 'agent' || accountModeRaw === 'owner' ? accountModeRaw : '';
|
|
75
|
+
const accountTypeRaw = String(initInfo && initInfo.accountType || '').trim().toLowerCase();
|
|
76
|
+
const accountType = accountTypeRaw === 'auto' || accountTypeRaw === 'weiyuan' || accountTypeRaw === 'lob' ? accountTypeRaw : '';
|
|
77
|
+
const displayAccountId = String((initInfo && (initInfo.displayAccountId || initInfo.account)) || '').trim();
|
|
78
|
+
if (effectiveLobsterId && effectiveLobsterId !== lobsterId) next.effectiveLobsterId = effectiveLobsterId;
|
|
79
|
+
else delete next.effectiveLobsterId;
|
|
80
|
+
if (ownerLobsterId && ownerLobsterId !== lobsterId) next.ownerLobsterId = ownerLobsterId;
|
|
81
|
+
else delete next.ownerLobsterId;
|
|
82
|
+
if (accountMode) next.accountMode = accountMode;
|
|
83
|
+
else delete next.accountMode;
|
|
84
|
+
if (accountType) next.accountType = accountType;
|
|
85
|
+
else delete next.accountType;
|
|
86
|
+
if (displayAccountId) next.displayAccountId = displayAccountId;
|
|
87
|
+
else delete next.displayAccountId;
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeSkillVersionText(raw) {
|
|
92
|
+
const text = String(raw || '').trim().replace(/^v/i, '');
|
|
93
|
+
return text && text !== '0.0.0' ? text : '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readLocalSkillVersion(workspacePath) {
|
|
97
|
+
const weiyuanPath = path.join(String(workspacePath || ''), 'weiyuan');
|
|
98
|
+
const candidates = [
|
|
99
|
+
path.join(weiyuanPath, 'package.json'),
|
|
100
|
+
path.join(weiyuanPath, 'release-notes', 'latest.json'),
|
|
101
|
+
path.join(weiyuanPath, 'manifest.json'),
|
|
102
|
+
];
|
|
103
|
+
for (const file of candidates) {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = await fs.readJson(file);
|
|
106
|
+
const version = normalizeSkillVersionText(parsed && parsed.version);
|
|
107
|
+
if (version) return version;
|
|
108
|
+
} catch (_) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const plain = await fs.readFile(path.join(weiyuanPath, '.version'), 'utf8');
|
|
113
|
+
const version = normalizeSkillVersionText(plain);
|
|
114
|
+
if (version) return version;
|
|
115
|
+
} catch (_) {
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function registerIdentity(serverUrl, identity, workspacePath) {
|
|
121
|
+
const skillVersion = await readLocalSkillVersion(workspacePath);
|
|
68
122
|
const body = {
|
|
69
123
|
lobsterId: identity.lobsterId,
|
|
70
124
|
publicKeyBase64: identity.publicKeyBase64,
|
|
71
125
|
identityHash: identity.identityHash,
|
|
72
|
-
recoveryHint: identity && identity.meta ? identity.meta.recoveryHint : undefined
|
|
126
|
+
recoveryHint: identity && identity.meta ? identity.meta.recoveryHint : undefined,
|
|
127
|
+
skillVersion: skillVersion || undefined
|
|
73
128
|
};
|
|
74
129
|
const timestampMs = String(Date.now());
|
|
75
130
|
const nonce = `nonce_${Math.random().toString(16).slice(2)}`;
|
|
@@ -127,7 +182,7 @@ async function createIdentityFile(identityPath, serverUrl, workspacePath) {
|
|
|
127
182
|
recoveryHint: (existing.meta && existing.meta.recoveryHint) || buildRecoveryHint(workspacePath)
|
|
128
183
|
}
|
|
129
184
|
};
|
|
130
|
-
const initInfo = await registerIdentity(serverUrl, identity);
|
|
185
|
+
const initInfo = await registerIdentity(serverUrl, identity, workspacePath);
|
|
131
186
|
const serverLobsterId = String(initInfo && initInfo.lobsterId || '').trim();
|
|
132
187
|
if (serverLobsterId) {
|
|
133
188
|
identity.lobsterId = serverLobsterId;
|
|
@@ -151,7 +206,8 @@ async function createIdentityFile(identityPath, serverUrl, workspacePath) {
|
|
|
151
206
|
.filter(([k, v]) => k && Number.isFinite(v))
|
|
152
207
|
)
|
|
153
208
|
: {};
|
|
154
|
-
|
|
209
|
+
const nextIdentity = applyIdentityProxyMetadata(identity, initInfo);
|
|
210
|
+
await fs.writeJson(identityPath, nextIdentity, { spaces: 2 });
|
|
155
211
|
return { created: true, initInfo: initInfo || null };
|
|
156
212
|
}
|
|
157
213
|
}
|
|
@@ -176,7 +232,7 @@ async function createIdentityFile(identityPath, serverUrl, workspacePath) {
|
|
|
176
232
|
}
|
|
177
233
|
};
|
|
178
234
|
|
|
179
|
-
const initInfo = await registerIdentity(serverUrl, identity);
|
|
235
|
+
const initInfo = await registerIdentity(serverUrl, identity, workspacePath);
|
|
180
236
|
const serverLobsterId = String(initInfo && initInfo.lobsterId || '').trim();
|
|
181
237
|
if (serverLobsterId) {
|
|
182
238
|
identity.lobsterId = serverLobsterId;
|
|
@@ -200,7 +256,8 @@ async function createIdentityFile(identityPath, serverUrl, workspacePath) {
|
|
|
200
256
|
.filter(([k, v]) => k && Number.isFinite(v))
|
|
201
257
|
)
|
|
202
258
|
: {};
|
|
203
|
-
|
|
259
|
+
const nextIdentity = applyIdentityProxyMetadata(identity, initInfo);
|
|
260
|
+
await fs.writeJson(identityPath, nextIdentity, { spaces: 2 });
|
|
204
261
|
return { created: true, initInfo: initInfo || null };
|
|
205
262
|
} catch (error) {
|
|
206
263
|
return { created: false, initInfo: null, error: error && error.message ? String(error.message) : 'identity_create_failed' };
|