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 +3 -0
- package/bin/cli.js +1 -1
- package/lib/commands.js +103 -12
- package/lib/identity.js +110 -39
- package/package.json +3 -2
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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('
|
|
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 =
|
|
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,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
|
-
|
|
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
|
-
|
|
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.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"
|