openclaw-weiyuan-init 1.0.9 → 1.0.14

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
@@ -20,6 +20,8 @@ npx -y openclaw-weiyuan-init@latest init
20
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
@@ -27,6 +29,7 @@ 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
 
package/bin/cli.js CHANGED
@@ -14,7 +14,7 @@ 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/upgrade')
17
+ .option('-u, --upgrade <url>', '指定升级源地址(含 LATEST_SKILL_VERSION.txt)', 'http://121.43.119.190/upgrade')
18
18
  .option('-d, --download <url>', '指定下载地址(可覆盖自动版本解析)')
19
19
  .option('-i, --invite <token>', '邀请码令牌(包含 server/upgrade/project/code)')
20
20
  .option('-p, --project <id>', '邀请加入的项目 ID')
package/lib/commands.js CHANGED
@@ -13,12 +13,53 @@ const execFileAsync = promisify(execFile);
13
13
 
14
14
  const DEFAULT_CONFIG = {
15
15
  workspaceName: 'workspace-weiyuan',
16
- upgradeBaseUrl: 'http://121.43.119.190/upgrade',
16
+ upgradeBaseUrl: 'http://121.43.119.190/upgrade',
17
17
  downloadUrl: '',
18
18
  serverUrl: 'http://121.43.119.190:8787',
19
19
  identityFile: '.weiyuan'
20
20
  };
21
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
+
22
63
  function decodeInviteToken(token) {
23
64
  if (!token || typeof token !== 'string') return null;
24
65
  try {
@@ -35,11 +76,44 @@ function decodeInviteToken(token) {
35
76
  }
36
77
 
37
78
  async function runJoin(weiyuanPath, identityPath, projectId, code) {
38
- await execFileAsync('npm', ['--prefix', weiyuanPath, 'run', 'weiyuan', '--', 'join', '--identity', identityPath, '--project', projectId, '--code', 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], {
39
87
  cwd: weiyuanPath
40
88
  });
41
89
  }
42
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
+ }
116
+
43
117
  async function runInit(options) {
44
118
  printBanner();
45
119
 
@@ -51,7 +125,7 @@ async function runInit(options) {
51
125
  options.code = options.code || invite.code;
52
126
  }
53
127
 
54
- const workspacePath = path.join(process.cwd(), options.workspace);
128
+ const workspacePath = resolveWorkspacePath(options.workspace);
55
129
  const weiyuanPath = path.join(workspacePath, 'weiyuan');
56
130
  const upgradeBaseUrl = options.upgrade || DEFAULT_CONFIG.upgradeBaseUrl;
57
131
  const requestedDownloadUrl = options.download || DEFAULT_CONFIG.downloadUrl;
@@ -120,27 +194,43 @@ 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('初始化端点不存在,请手动调用');
233
+ spinner.warn('初始化端点不存在,可忽略(不影响 join/命令执行)');
144
234
  }
145
235
 
146
236
  if (options.project && options.code) {
@@ -159,7 +249,7 @@ async function runInit(options) {
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,7 +295,8 @@ 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
  }
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.9",
3
+ "version": "1.0.14",
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"