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 CHANGED
@@ -5,7 +5,7 @@ OpenClaw Weiyuan Skill 一键初始化工具
5
5
  ## 一键部署
6
6
 
7
7
  ```bash
8
- npx -y @weiyuan/openclaw-weiyuan-init@latest init
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 @weiyuan/openclaw-weiyuan-init@latest init --upgrade <upgradeBaseUrl> --server <serverUrl>
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 @weiyuan/openclaw-weiyuan-init@latest init --invite <inviteToken>
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 @weiyuan/openclaw-weiyuan-init@latest init --download <zipUrl> --server <serverUrl>
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:8788')
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:8788',
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
- function decodeInviteToken(token) {
23
- if (!token || typeof token !== 'string') return null;
24
- try {
25
- const normalized = token.replace(/-/g, '+').replace(/_/g, '/');
26
- const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
27
- const raw = Buffer.from(normalized + pad, 'base64').toString('utf8');
28
- const parsed = JSON.parse(raw);
29
- if (!parsed || parsed.v !== 1) return null;
30
- if (!parsed.api || !parsed.upgrade || !parsed.projectId || !parsed.code) return null;
31
- return parsed;
32
- } catch (_) {
33
- return null;
34
- }
35
- }
36
-
37
- async function runJoin(weiyuanPath, identityPath, projectId, code) {
38
- await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'join', '--identity', identityPath, '--project', projectId, '--code', code], {
39
- cwd: weiyuanPath
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 = path.join(process.cwd(), options.workspace);
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
- const identityCreated = await createIdentityFile(identityPath, serverUrl, workspacePath);
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.warn('身份文件创建失败,请手动创建');
219
+ spinner.fail('身份文件创建失败,请检查 /v1/init 可用性');
220
+ throw new Error('identity_create_failed');
131
221
  }
132
222
 
133
- // 7. 列出目录内容
223
+ // 8. 列出目录内容
134
224
  console.log(chalk.yellow('\n📁 weiyuan 文件夹内容:'));
135
225
  await listDirectory(weiyuanPath, 10);
136
226
 
137
- // 8. 初始化 skill
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 = path.join(process.cwd(), options.workspace);
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 = path.join(process.cwd(), options.workspace);
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
- console.log(chalk.green(`✅ 身份文件: ${identity.device_id || '已配置'}`));
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
- async function createIdentityFile(identityPath, serverUrl, workspacePath) {
5
- try {
6
- if (await fs.pathExists(identityPath)) {
7
- return true;
8
- }
9
-
10
- const identity = {
11
- version: "1.0",
12
- device_id: os.hostname(),
13
- device_name: os.hostname(),
14
- created_at: new Date().toISOString(),
15
- server_url: serverUrl,
16
- workspace: workspacePath,
17
- skill_path: `${workspacePath}/weiyuan`,
18
- note: "请根据实际情况修改此文件"
19
- };
20
-
21
- await fs.writeJson(identityPath, identity, { spaces: 2 });
22
- return true;
23
- } catch (error) {
24
- return false;
25
- }
26
- }
27
-
28
- async function readIdentityFile(identityPath) {
29
- try {
30
- if (await fs.pathExists(identityPath)) {
31
- return await fs.readJson(identityPath);
32
- }
33
- return null;
34
- } catch (error) {
35
- return null;
36
- }
37
- }
38
-
39
- module.exports = { createIdentityFile, readIdentityFile };
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.5",
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"