openclaw-weiyuan-init 1.0.5 → 1.0.13
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/README.md +7 -4
- package/bin/cli.js +7 -7
- package/lib/commands.js +169 -78
- package/lib/downloader.js +31 -31
- package/lib/identity.js +110 -39
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ OpenClaw Weiyuan Skill 一键初始化工具
|
|
|
5
5
|
## 一键部署
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y
|
|
8
|
+
npx -y openclaw-weiyuan-init@latest init
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## 版本化 zip 自动识别
|
|
@@ -17,19 +17,22 @@ npx -y @weiyuan/openclaw-weiyuan-init@latest init
|
|
|
17
17
|
## 常用参数
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx -y
|
|
20
|
+
npx -y openclaw-weiyuan-init@latest init --upgrade <upgradeBaseUrl> --server <serverUrl>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
工作目录会自动归一化到 `<指定目录>/workspace-weiyuan`,身份文件固定写在该目录下的 `.weiyuan`。
|
|
24
|
+
|
|
23
25
|
邀请码一键入组(推荐):
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
npx -y
|
|
28
|
+
npx -y openclaw-weiyuan-init@latest init --invite <inviteToken>
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
`inviteToken` 包含 `server/upgrade/project/code`,会自动完成安装与入组。
|
|
32
|
+
初始化过程会自动准备 `weiyuan` CLI 运行时依赖并生成完整 `.weiyuan` 身份文件。
|
|
30
33
|
|
|
31
34
|
如需指定固定 zip,可覆盖自动识别:
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
|
-
npx -y
|
|
37
|
+
npx -y openclaw-weiyuan-init@latest init --download <zipUrl> --server <serverUrl>
|
|
35
38
|
```
|
package/bin/cli.js
CHANGED
|
@@ -14,11 +14,11 @@ program
|
|
|
14
14
|
.description('初始化 weiyuan skill')
|
|
15
15
|
.option('-w, --workspace <path>', '指定工作目录', 'workspace-weiyuan')
|
|
16
16
|
.option('-s, --server <url>', '指定服务器地址', 'http://121.43.119.190:8787')
|
|
17
|
-
.option('-u, --upgrade <url>', '指定升级源地址(含 LATEST_SKILL_VERSION.txt)', 'http://121.43.119.190
|
|
18
|
-
.option('-d, --download <url>', '指定下载地址(可覆盖自动版本解析)')
|
|
19
|
-
.option('-i, --invite <token>', '邀请码令牌(包含 server/upgrade/project/code)')
|
|
20
|
-
.option('-p, --project <id>', '邀请加入的项目 ID')
|
|
21
|
-
.option('-c, --code <code>', '邀请口令')
|
|
17
|
+
.option('-u, --upgrade <url>', '指定升级源地址(含 LATEST_SKILL_VERSION.txt)', 'http://121.43.119.190/upgrade')
|
|
18
|
+
.option('-d, --download <url>', '指定下载地址(可覆盖自动版本解析)')
|
|
19
|
+
.option('-i, --invite <token>', '邀请码令牌(包含 server/upgrade/project/code)')
|
|
20
|
+
.option('-p, --project <id>', '邀请加入的项目 ID')
|
|
21
|
+
.option('-c, --code <code>', '邀请口令')
|
|
22
22
|
.option('-f, --force', '强制覆盖已有文件')
|
|
23
23
|
.action(async (options) => {
|
|
24
24
|
try {
|
|
@@ -46,7 +46,7 @@ program
|
|
|
46
46
|
.command('status')
|
|
47
47
|
.description('查看 weiyuan 状态')
|
|
48
48
|
.option('-w, --workspace <path>', '指定工作目录', 'workspace-weiyuan')
|
|
49
|
-
.option('-s, --server <url>', '指定服务器地址', 'http://121.43.119.190:8787')
|
|
49
|
+
.option('-s, --server <url>', '指定服务器地址', 'http://121.43.119.190:8787')
|
|
50
50
|
.action(async (options) => {
|
|
51
51
|
try {
|
|
52
52
|
await runStatus(options);
|
|
@@ -56,4 +56,4 @@ program
|
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
program.parse();
|
|
59
|
+
program.parse();
|
package/lib/commands.js
CHANGED
|
@@ -2,59 +2,133 @@ const chalk = require('chalk');
|
|
|
2
2
|
const ora = require('ora');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
|
-
const { execFile } = require('child_process');
|
|
6
|
-
const { promisify } = require('util');
|
|
7
|
-
const { downloadFile, resolvePackageSource } = require('./downloader');
|
|
5
|
+
const { execFile } = require('child_process');
|
|
6
|
+
const { promisify } = require('util');
|
|
7
|
+
const { downloadFile, resolvePackageSource } = require('./downloader');
|
|
8
8
|
const { extractZip } = require('./extractor');
|
|
9
9
|
const { createIdentityFile } = require('./identity');
|
|
10
10
|
const { checkServer, initSkill } = require('./server');
|
|
11
11
|
const { printBanner, printSummary, listDirectory } = require('./utils');
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
13
|
|
|
14
14
|
const DEFAULT_CONFIG = {
|
|
15
15
|
workspaceName: 'workspace-weiyuan',
|
|
16
|
-
upgradeBaseUrl: 'http://121.43.119.190
|
|
17
|
-
downloadUrl: '',
|
|
16
|
+
upgradeBaseUrl: 'http://121.43.119.190/upgrade',
|
|
17
|
+
downloadUrl: '',
|
|
18
18
|
serverUrl: 'http://121.43.119.190:8787',
|
|
19
19
|
identityFile: '.weiyuan'
|
|
20
20
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
|
|
22
|
+
const DEFAULT_SKILL_PACKAGE_JSON = {
|
|
23
|
+
name: 'weiyuan-skill-runtime',
|
|
24
|
+
private: true,
|
|
25
|
+
version: '0.0.0',
|
|
26
|
+
type: 'module',
|
|
27
|
+
scripts: {
|
|
28
|
+
weiyuan: 'tsx src/cliMain.ts',
|
|
29
|
+
'weiyuan:skill': 'tsx src/skillAdapter.ts'
|
|
30
|
+
},
|
|
31
|
+
dependencies: {
|
|
32
|
+
tweetnacl: '^1.0.3'
|
|
33
|
+
},
|
|
34
|
+
devDependencies: {
|
|
35
|
+
tsx: '^4.20.6',
|
|
36
|
+
typescript: '^5.9.3',
|
|
37
|
+
'@types/node': '^24.12.0'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_SKILL_TSCONFIG = {
|
|
42
|
+
compilerOptions: {
|
|
43
|
+
target: 'ES2022',
|
|
44
|
+
module: 'NodeNext',
|
|
45
|
+
moduleResolution: 'NodeNext',
|
|
46
|
+
strict: true,
|
|
47
|
+
esModuleInterop: true,
|
|
48
|
+
resolveJsonModule: true,
|
|
49
|
+
skipLibCheck: true
|
|
50
|
+
},
|
|
51
|
+
include: ['src']
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function resolveWorkspacePath(workspaceOption) {
|
|
55
|
+
const raw = (workspaceOption || DEFAULT_CONFIG.workspaceName).trim();
|
|
56
|
+
const target = path.isAbsolute(raw) ? raw : path.join(process.cwd(), raw);
|
|
57
|
+
if (path.basename(target).toLowerCase() === DEFAULT_CONFIG.workspaceName.toLowerCase()) {
|
|
58
|
+
return target;
|
|
59
|
+
}
|
|
60
|
+
return path.join(target, DEFAULT_CONFIG.workspaceName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function decodeInviteToken(token) {
|
|
64
|
+
if (!token || typeof token !== 'string') return null;
|
|
65
|
+
try {
|
|
66
|
+
const normalized = token.replace(/-/g, '+').replace(/_/g, '/');
|
|
67
|
+
const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
|
68
|
+
const raw = Buffer.from(normalized + pad, 'base64').toString('utf8');
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
if (!parsed || parsed.v !== 1) return null;
|
|
71
|
+
if (!parsed.api || !parsed.upgrade || !parsed.projectId || !parsed.code) return null;
|
|
72
|
+
return parsed;
|
|
73
|
+
} catch (_) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runJoin(weiyuanPath, identityPath, projectId, code) {
|
|
79
|
+
try {
|
|
80
|
+
await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'join', '--identity', identityPath, '--project', projectId, '--code', code], {
|
|
81
|
+
cwd: weiyuanPath
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
} catch (_) {
|
|
85
|
+
}
|
|
86
|
+
await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'join', '--identity', identityPath, '--project', projectId, '--code', code], {
|
|
87
|
+
cwd: weiyuanPath
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function ensureSkillRuntime(weiyuanPath) {
|
|
92
|
+
const pkgPath = path.join(weiyuanPath, 'package.json');
|
|
93
|
+
const tsconfigPath = path.join(weiyuanPath, 'tsconfig.json');
|
|
94
|
+
if (!await fs.pathExists(pkgPath)) {
|
|
95
|
+
await fs.writeJson(pkgPath, DEFAULT_SKILL_PACKAGE_JSON, { spaces: 2 });
|
|
96
|
+
}
|
|
97
|
+
if (!await fs.pathExists(tsconfigPath)) {
|
|
98
|
+
await fs.writeJson(tsconfigPath, DEFAULT_SKILL_TSCONFIG, { spaces: 2 });
|
|
99
|
+
}
|
|
100
|
+
await execFileAsync('npm', ['--prefix', weiyuanPath, 'install'], { cwd: weiyuanPath });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runCliInit(weiyuanPath, identityPath, serverUrl) {
|
|
104
|
+
try {
|
|
105
|
+
await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'init', '--server', serverUrl, '--out', identityPath], {
|
|
106
|
+
cwd: weiyuanPath
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
} catch (_) {
|
|
110
|
+
}
|
|
111
|
+
await execFileAsync('npx', ['-y', '-p', 'tsx', '-p', 'tweetnacl', 'tsx', path.join(weiyuanPath, 'src', 'cliMain.ts'), 'init', '--server', serverUrl, '--out', identityPath], {
|
|
112
|
+
cwd: weiyuanPath
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
42
116
|
|
|
43
117
|
async function runInit(options) {
|
|
44
118
|
printBanner();
|
|
45
|
-
|
|
46
|
-
const invite = decodeInviteToken(options.invite);
|
|
47
|
-
if (invite) {
|
|
48
|
-
options.server = options.server || invite.api;
|
|
49
|
-
options.upgrade = options.upgrade || invite.upgrade;
|
|
50
|
-
options.project = options.project || invite.projectId;
|
|
51
|
-
options.code = options.code || invite.code;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const workspacePath =
|
|
119
|
+
|
|
120
|
+
const invite = decodeInviteToken(options.invite);
|
|
121
|
+
if (invite) {
|
|
122
|
+
options.server = options.server || invite.api;
|
|
123
|
+
options.upgrade = options.upgrade || invite.upgrade;
|
|
124
|
+
options.project = options.project || invite.projectId;
|
|
125
|
+
options.code = options.code || invite.code;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const workspacePath = resolveWorkspacePath(options.workspace);
|
|
55
129
|
const weiyuanPath = path.join(workspacePath, 'weiyuan');
|
|
56
|
-
const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
|
|
57
|
-
const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
|
|
130
|
+
const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
|
|
131
|
+
const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
|
|
58
132
|
const serverUrl = options.server || DEFAULT_CONFIG.serverUrl;
|
|
59
133
|
const force = options.force || false;
|
|
60
134
|
|
|
@@ -85,22 +159,22 @@ async function runInit(options) {
|
|
|
85
159
|
}
|
|
86
160
|
|
|
87
161
|
// 3. 下载文件
|
|
88
|
-
spinner = ora('解析下载包...').start();
|
|
89
|
-
const source = await resolvePackageSource({
|
|
90
|
-
downloadUrl: requestedDownloadUrl,
|
|
91
|
-
upgradeBaseUrl,
|
|
92
|
-
spinner
|
|
93
|
-
});
|
|
94
|
-
spinner.succeed(`下载源: ${source.downloadUrl}`);
|
|
95
|
-
|
|
96
|
-
spinner = ora('下载 weiyuan skill...').start();
|
|
97
|
-
const zipPath = path.join(weiyuanPath, source.zipFilename);
|
|
98
|
-
const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner);
|
|
162
|
+
spinner = ora('解析下载包...').start();
|
|
163
|
+
const source = await resolvePackageSource({
|
|
164
|
+
downloadUrl: requestedDownloadUrl,
|
|
165
|
+
upgradeBaseUrl,
|
|
166
|
+
spinner
|
|
167
|
+
});
|
|
168
|
+
spinner.succeed(`下载源: ${source.downloadUrl}`);
|
|
169
|
+
|
|
170
|
+
spinner = ora('下载 weiyuan skill...').start();
|
|
171
|
+
const zipPath = path.join(weiyuanPath, source.zipFilename);
|
|
172
|
+
const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner);
|
|
99
173
|
if (!downloadSuccess) {
|
|
100
174
|
spinner.fail('下载失败');
|
|
101
175
|
throw new Error('下载失败');
|
|
102
176
|
}
|
|
103
|
-
spinner.succeed(`下载完成: ${source.zipFilename}`);
|
|
177
|
+
spinner.succeed(`下载完成: ${source.zipFilename}`);
|
|
104
178
|
|
|
105
179
|
// 4. 解压
|
|
106
180
|
spinner = ora('解压文件...').start();
|
|
@@ -120,46 +194,62 @@ async function runInit(options) {
|
|
|
120
194
|
spinner.warn('清理失败,可手动删除');
|
|
121
195
|
}
|
|
122
196
|
|
|
123
|
-
// 6.
|
|
197
|
+
// 6. 准备 CLI 运行时
|
|
198
|
+
spinner = ora('准备 CLI 运行时...').start();
|
|
199
|
+
try {
|
|
200
|
+
await ensureSkillRuntime(weiyuanPath);
|
|
201
|
+
spinner.succeed('CLI 运行时就绪');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
spinner.fail(`CLI 运行时准备失败: ${error.message}`);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 7. 创建身份文件
|
|
124
208
|
spinner = ora('创建身份文件...').start();
|
|
125
209
|
const identityPath = path.join(workspacePath, DEFAULT_CONFIG.identityFile);
|
|
126
|
-
|
|
210
|
+
let identityCreated = false;
|
|
211
|
+
try {
|
|
212
|
+
identityCreated = await runCliInit(weiyuanPath, identityPath, serverUrl);
|
|
213
|
+
} catch (_) {
|
|
214
|
+
identityCreated = await createIdentityFile(identityPath, serverUrl, workspacePath);
|
|
215
|
+
}
|
|
127
216
|
if (identityCreated) {
|
|
128
217
|
spinner.succeed(`身份文件: ${DEFAULT_CONFIG.identityFile}`);
|
|
129
218
|
} else {
|
|
130
|
-
spinner.
|
|
219
|
+
spinner.fail('身份文件创建失败,请检查 /v1/init 可用性');
|
|
220
|
+
throw new Error('identity_create_failed');
|
|
131
221
|
}
|
|
132
222
|
|
|
133
|
-
//
|
|
223
|
+
// 8. 列出目录内容
|
|
134
224
|
console.log(chalk.yellow('\n📁 weiyuan 文件夹内容:'));
|
|
135
225
|
await listDirectory(weiyuanPath, 10);
|
|
136
226
|
|
|
137
|
-
//
|
|
227
|
+
// 9. 初始化 skill
|
|
138
228
|
spinner = ora('初始化 weiyuan skill...').start();
|
|
139
229
|
const initResult = await initSkill(serverUrl, identityPath, workspacePath);
|
|
140
230
|
if (initResult) {
|
|
141
231
|
spinner.succeed('初始化成功');
|
|
142
232
|
} else {
|
|
143
|
-
spinner.warn('
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (options.project && options.code) {
|
|
147
|
-
spinner = ora('自动加入微元项目...').start();
|
|
148
|
-
try {
|
|
149
|
-
await runJoin(weiyuanPath, identityPath, options.project, options.code);
|
|
150
|
-
spinner.succeed(`已加入项目: ${options.project}`);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
spinner.fail(`自动入组失败: ${error.message}`);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
233
|
+
spinner.warn('初始化端点不存在,可忽略(不影响 join/命令执行)');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.project && options.code) {
|
|
237
|
+
spinner = ora('自动加入微元项目...').start();
|
|
238
|
+
try {
|
|
239
|
+
await runJoin(weiyuanPath, identityPath, options.project, options.code);
|
|
240
|
+
spinner.succeed(`已加入项目: ${options.project}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
spinner.fail(`自动入组失败: ${error.message}`);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
156
246
|
|
|
157
247
|
// 打印总结
|
|
158
|
-
printSummary(workspacePath, serverUrl, source.downloadUrl);
|
|
248
|
+
printSummary(workspacePath, serverUrl, source.downloadUrl);
|
|
159
249
|
}
|
|
160
250
|
|
|
161
251
|
async function runClean(options) {
|
|
162
|
-
const workspacePath =
|
|
252
|
+
const workspacePath = resolveWorkspacePath(options.workspace);
|
|
163
253
|
|
|
164
254
|
if (!await fs.pathExists(workspacePath)) {
|
|
165
255
|
console.log(chalk.yellow(`工作目录不存在: ${workspacePath}`));
|
|
@@ -177,7 +267,7 @@ async function runClean(options) {
|
|
|
177
267
|
}
|
|
178
268
|
|
|
179
269
|
async function runStatus(options) {
|
|
180
|
-
const workspacePath =
|
|
270
|
+
const workspacePath = resolveWorkspacePath(options.workspace);
|
|
181
271
|
const weiyuanPath = path.join(workspacePath, 'weiyuan');
|
|
182
272
|
const identityPath = path.join(workspacePath, DEFAULT_CONFIG.identityFile);
|
|
183
273
|
|
|
@@ -205,21 +295,22 @@ async function runStatus(options) {
|
|
|
205
295
|
// 检查身份文件
|
|
206
296
|
if (await fs.pathExists(identityPath)) {
|
|
207
297
|
const identity = await fs.readJson(identityPath);
|
|
208
|
-
|
|
298
|
+
const idLabel = identity.lobsterId || identity.device_id || '已配置';
|
|
299
|
+
console.log(chalk.green(`✅ 身份文件: ${idLabel}`));
|
|
209
300
|
} else {
|
|
210
301
|
console.log(chalk.red(`❌ 身份文件不存在`));
|
|
211
302
|
}
|
|
212
303
|
|
|
213
304
|
// 检查服务器
|
|
214
|
-
const serverUrl = options.server || DEFAULT_CONFIG.serverUrl;
|
|
215
|
-
const serverOk = await checkServer(serverUrl);
|
|
305
|
+
const serverUrl = options.server || DEFAULT_CONFIG.serverUrl;
|
|
306
|
+
const serverOk = await checkServer(serverUrl);
|
|
216
307
|
if (serverOk) {
|
|
217
|
-
console.log(chalk.green(`✅ 服务器: ${serverUrl}`));
|
|
308
|
+
console.log(chalk.green(`✅ 服务器: ${serverUrl}`));
|
|
218
309
|
} else {
|
|
219
|
-
console.log(chalk.red(`❌ 服务器不可达: ${serverUrl}`));
|
|
310
|
+
console.log(chalk.red(`❌ 服务器不可达: ${serverUrl}`));
|
|
220
311
|
}
|
|
221
312
|
|
|
222
313
|
console.log('');
|
|
223
314
|
}
|
|
224
315
|
|
|
225
|
-
module.exports = { runInit, runClean, runStatus, DEFAULT_CONFIG };
|
|
316
|
+
module.exports = { runInit, runClean, runStatus, DEFAULT_CONFIG };
|
package/lib/downloader.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
|
|
4
|
-
function trimSlash(url) {
|
|
5
|
-
return String(url || '').replace(/\/+$/, '');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function toZipFilename(downloadUrl) {
|
|
9
|
-
const clean = String(downloadUrl || '').split('?')[0].split('#')[0];
|
|
10
|
-
const parts = clean.split('/');
|
|
11
|
-
return parts[parts.length - 1] || 'weiyuan-openclaw-skill.zip';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = null }) {
|
|
15
|
-
if (downloadUrl) {
|
|
16
|
-
return { downloadUrl, zipFilename: toZipFilename(downloadUrl), version: null, from: 'download_url' };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const base = trimSlash(upgradeBaseUrl);
|
|
20
|
-
const latestUrl = `${base}/LATEST_SKILL_VERSION.txt`;
|
|
21
|
-
const latestResp = await axios.get(latestUrl, { timeout: 10000 });
|
|
22
|
-
const version = String(latestResp.data || '').trim();
|
|
23
|
-
if (!version) {
|
|
24
|
-
throw new Error('empty_latest_version');
|
|
25
|
-
}
|
|
26
|
-
const zipFilename = `weiyuan-openclaw-skill-v${version}.zip`;
|
|
27
|
-
const resolvedDownloadUrl = `${base}/${zipFilename}`;
|
|
28
|
-
if (spinner) {
|
|
29
|
-
spinner.text = `已解析最新版本: v${version}`;
|
|
30
|
-
}
|
|
31
|
-
return { downloadUrl: resolvedDownloadUrl, zipFilename, version, from: 'latest_version' };
|
|
32
|
-
}
|
|
33
|
-
|
|
4
|
+
function trimSlash(url) {
|
|
5
|
+
return String(url || '').replace(/\/+$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toZipFilename(downloadUrl) {
|
|
9
|
+
const clean = String(downloadUrl || '').split('?')[0].split('#')[0];
|
|
10
|
+
const parts = clean.split('/');
|
|
11
|
+
return parts[parts.length - 1] || 'weiyuan-openclaw-skill.zip';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = null }) {
|
|
15
|
+
if (downloadUrl) {
|
|
16
|
+
return { downloadUrl, zipFilename: toZipFilename(downloadUrl), version: null, from: 'download_url' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const base = trimSlash(upgradeBaseUrl);
|
|
20
|
+
const latestUrl = `${base}/LATEST_SKILL_VERSION.txt`;
|
|
21
|
+
const latestResp = await axios.get(latestUrl, { timeout: 10000 });
|
|
22
|
+
const version = String(latestResp.data || '').trim();
|
|
23
|
+
if (!version) {
|
|
24
|
+
throw new Error('empty_latest_version');
|
|
25
|
+
}
|
|
26
|
+
const zipFilename = `weiyuan-openclaw-skill-v${version}.zip`;
|
|
27
|
+
const resolvedDownloadUrl = `${base}/${zipFilename}`;
|
|
28
|
+
if (spinner) {
|
|
29
|
+
spinner.text = `已解析最新版本: v${version}`;
|
|
30
|
+
}
|
|
31
|
+
return { downloadUrl: resolvedDownloadUrl, zipFilename, version, from: 'latest_version' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
34
|
async function downloadFile(url, outputPath, spinner = null) {
|
|
35
35
|
try {
|
|
36
36
|
const response = await axios({
|
|
@@ -70,4 +70,4 @@ async function downloadFile(url, outputPath, spinner = null) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
module.exports = { downloadFile, resolvePackageSource };
|
|
73
|
+
module.exports = { downloadFile, resolvePackageSource };
|
package/lib/identity.js
CHANGED
|
@@ -1,39 +1,110 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const naclImport = require('tweetnacl');
|
|
6
|
+
|
|
7
|
+
const nacl = naclImport.default || naclImport;
|
|
8
|
+
|
|
9
|
+
function sha256Hex(input) {
|
|
10
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sha256Base64(input) {
|
|
14
|
+
return crypto.createHash('sha256').update(input).digest('base64');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalRequest(method, pathWithQuery, timestampMs, nonce, bodySha256Base64) {
|
|
18
|
+
return [String(method || 'POST').toUpperCase(), pathWithQuery, timestampMs, nonce, bodySha256Base64].join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function signEd25519Base64(messageUtf8, secretKeyBase64) {
|
|
22
|
+
const sk = Buffer.from(secretKeyBase64, 'base64');
|
|
23
|
+
const sig = nacl.sign.detached(Buffer.from(messageUtf8, 'utf8'), sk);
|
|
24
|
+
return Buffer.from(sig).toString('base64');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function newKeyPair() {
|
|
28
|
+
const kp = nacl.sign.keyPair();
|
|
29
|
+
const publicKeyBase64 = Buffer.from(kp.publicKey).toString('base64');
|
|
30
|
+
const secretKeyBase64 = Buffer.from(kp.secretKey).toString('base64');
|
|
31
|
+
const lobsterId = `lob_${sha256Hex(kp.publicKey).slice(0, 12)}`;
|
|
32
|
+
return { publicKeyBase64, secretKeyBase64, lobsterId };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function registerIdentity(serverUrl, identity) {
|
|
36
|
+
const body = {
|
|
37
|
+
lobsterId: identity.lobsterId,
|
|
38
|
+
publicKeyBase64: identity.publicKeyBase64,
|
|
39
|
+
identityHash: identity.identityHash
|
|
40
|
+
};
|
|
41
|
+
const timestampMs = String(Date.now());
|
|
42
|
+
const nonce = `nonce_${Math.random().toString(16).slice(2)}`;
|
|
43
|
+
const bodySha256Base64 = sha256Base64(JSON.stringify(body));
|
|
44
|
+
const canonical = canonicalRequest('POST', '/v1/init', timestampMs, nonce, bodySha256Base64);
|
|
45
|
+
const signature = signEd25519Base64(canonical, identity.secretKeyBase64);
|
|
46
|
+
|
|
47
|
+
const res = await axios.post(`${serverUrl.replace(/\/$/, '')}/v1/init`, body, {
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'X-Weiyuan-Lobster-Id': identity.lobsterId,
|
|
51
|
+
'X-Weiyuan-Timestamp': timestampMs,
|
|
52
|
+
'X-Weiyuan-Nonce': nonce,
|
|
53
|
+
'X-Weiyuan-Signature': signature
|
|
54
|
+
},
|
|
55
|
+
timeout: 12000,
|
|
56
|
+
validateStatus: () => true
|
|
57
|
+
});
|
|
58
|
+
if (res.status !== 200) {
|
|
59
|
+
throw new Error(`identity_register_failed_status_${res.status}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createIdentityFile(identityPath, serverUrl, workspacePath) {
|
|
64
|
+
try {
|
|
65
|
+
if (await fs.pathExists(identityPath)) {
|
|
66
|
+
const existing = await fs.readJson(identityPath).catch(() => ({}));
|
|
67
|
+
if (existing && existing.version === 1 && existing.lobsterId && existing.secretKeyBase64 && existing.publicKeyBase64) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const kp = newKeyPair();
|
|
73
|
+
const identityHash = sha256Hex(JSON.stringify({ lobsterId: kp.lobsterId, publicKeyBase64: kp.publicKeyBase64 }));
|
|
74
|
+
const identity = {
|
|
75
|
+
version: 1,
|
|
76
|
+
serverBaseUrl: serverUrl,
|
|
77
|
+
lobsterId: kp.lobsterId,
|
|
78
|
+
publicKeyBase64: kp.publicKeyBase64,
|
|
79
|
+
secretKeyBase64: kp.secretKeyBase64,
|
|
80
|
+
identityHash,
|
|
81
|
+
projectCursors: {},
|
|
82
|
+
meta: {
|
|
83
|
+
device_id: os.hostname(),
|
|
84
|
+
device_name: os.hostname(),
|
|
85
|
+
created_at: new Date().toISOString(),
|
|
86
|
+
workspace: workspacePath,
|
|
87
|
+
skill_path: `${workspacePath}/weiyuan`
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
await registerIdentity(serverUrl, identity);
|
|
92
|
+
await fs.writeJson(identityPath, identity, { spaces: 2 });
|
|
93
|
+
return true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readIdentityFile(identityPath) {
|
|
100
|
+
try {
|
|
101
|
+
if (await fs.pathExists(identityPath)) {
|
|
102
|
+
return await fs.readJson(identityPath);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { createIdentityFile, readIdentityFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-weiyuan-init",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "OpenClaw Weiyuan Skill 一键初始化工具",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"chalk": "^4.1.2",
|
|
21
21
|
"commander": "^11.1.0",
|
|
22
22
|
"fs-extra": "^11.1.1",
|
|
23
|
-
"ora": "^5.4.1"
|
|
23
|
+
"ora": "^5.4.1",
|
|
24
|
+
"tweetnacl": "^1.0.3"
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|