openclaw-weiyuan-init 1.0.79 → 1.0.81
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/lib/commands.js +50 -2
- package/lib/downloader.js +125 -73
- package/package.json +1 -1
package/lib/commands.js
CHANGED
|
@@ -53,7 +53,7 @@ const DEFAULT_SKILL_TSCONFIG = {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
const DEFAULT_FIXED_MESSAGES = {
|
|
56
|
-
sceneA: "【微元协作】✨ 已为你接入微元系统~\n让协作自然发生,Let collaboration happen naturally
|
|
56
|
+
sceneA: "【微元协作】✨ 已为你接入微元系统~\n让协作自然发生,Let collaboration happen naturally 💫",
|
|
57
57
|
sceneB: "【微元协作】✨ 已为你接入微元系统~ 并已加入对应项目~\n项目ID:{projectId}\n\n让协作自然发生,Let collaboration happen naturally 💫",
|
|
58
58
|
sceneC: "【微元协作】✨ 已加入对应项目~\n项目ID:{projectId}\n\n让协作自然发生,Let collaboration happen naturally 💫"
|
|
59
59
|
};
|
|
@@ -86,6 +86,52 @@ function renderFixedMessage(template, vars = {}) {
|
|
|
86
86
|
return String(template).replace(/\{(\w+)\}/g, (_, k) => (vars[k] !== undefined ? String(vars[k]) : ""));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function printReleaseNotes(notes) {
|
|
90
|
+
if (!notes) return;
|
|
91
|
+
const lines = Array.isArray(notes.changes) ? notes.changes.filter(Boolean).slice(0, 8) : [];
|
|
92
|
+
if (!notes.summary && lines.length === 0) return;
|
|
93
|
+
const versionText = notes.version ? `v${notes.version}` : "最新版本";
|
|
94
|
+
console.log(chalk.cyan(`\n📦 ${versionText} 变更说明`));
|
|
95
|
+
if (notes.summary) console.log(chalk.white(`- ${notes.summary}`));
|
|
96
|
+
for (const item of lines) {
|
|
97
|
+
if (notes.summary && item === notes.summary) continue;
|
|
98
|
+
console.log(chalk.gray(`- ${item}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function persistReleaseNotes(weiyuanPath, notes) {
|
|
103
|
+
const notesDir = path.join(weiyuanPath, 'release-notes');
|
|
104
|
+
const latestPath = path.join(notesDir, 'latest.json');
|
|
105
|
+
const ttlDays = 30;
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const expiresAt = new Date(now + ttlDays * 24 * 60 * 60 * 1000).toISOString();
|
|
108
|
+
await fs.ensureDir(notesDir);
|
|
109
|
+
if (notes && typeof notes === 'object') {
|
|
110
|
+
const payload = {
|
|
111
|
+
...notes,
|
|
112
|
+
fetchedAt: new Date(now).toISOString(),
|
|
113
|
+
expiresAt
|
|
114
|
+
};
|
|
115
|
+
await fs.writeJson(latestPath, payload, { spaces: 2 });
|
|
116
|
+
} else if (await fs.pathExists(latestPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const existing = await fs.readJson(latestPath);
|
|
119
|
+
const exp = Date.parse(String(existing && existing.expiresAt ? existing.expiresAt : ''));
|
|
120
|
+
if (!Number.isFinite(exp) || exp <= now) {
|
|
121
|
+
await fs.remove(latestPath);
|
|
122
|
+
}
|
|
123
|
+
} catch (_) {
|
|
124
|
+
await fs.remove(latestPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const files = await fs.readdir(notesDir);
|
|
128
|
+
for (const name of files) {
|
|
129
|
+
if (name === 'latest.json') continue;
|
|
130
|
+
if (!name.toLowerCase().endsWith('.json')) continue;
|
|
131
|
+
await fs.remove(path.join(notesDir, name));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
function resolveWorkspacePath(workspaceOption) {
|
|
90
136
|
const raw = (workspaceOption || DEFAULT_CONFIG.workspaceName).trim();
|
|
91
137
|
const target = path.isAbsolute(raw) ? raw : path.join(process.cwd(), raw);
|
|
@@ -339,10 +385,11 @@ async function runInit(options) {
|
|
|
339
385
|
spinner
|
|
340
386
|
});
|
|
341
387
|
spinner.succeed(`下载源: ${source.downloadUrl}`);
|
|
388
|
+
printReleaseNotes(source.releaseNotes);
|
|
342
389
|
|
|
343
390
|
spinner = ora('下载 weiyuan skill...').start();
|
|
344
391
|
const zipPath = path.join(weiyuanPath, source.zipFilename);
|
|
345
|
-
const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner);
|
|
392
|
+
const downloadSuccess = await downloadFile(source.downloadUrl, zipPath, spinner, source.sha256 || '');
|
|
346
393
|
if (!downloadSuccess) {
|
|
347
394
|
spinner.fail('下载失败');
|
|
348
395
|
throw new Error('下载失败');
|
|
@@ -362,6 +409,7 @@ async function runInit(options) {
|
|
|
362
409
|
spinner = ora('清理临时文件...').start();
|
|
363
410
|
try {
|
|
364
411
|
await fs.remove(zipPath);
|
|
412
|
+
await persistReleaseNotes(weiyuanPath, source.releaseNotes || null);
|
|
365
413
|
spinner.succeed('清理完成');
|
|
366
414
|
} catch (error) {
|
|
367
415
|
spinner.warn('清理失败,可手动删除');
|
package/lib/downloader.js
CHANGED
|
@@ -1,73 +1,125 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (spinner
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function trimSlash(url) {
|
|
6
|
+
return String(url || '').replace(/\/+$/, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toZipFilename(downloadUrl) {
|
|
10
|
+
const clean = String(downloadUrl || '').split('?')[0].split('#')[0];
|
|
11
|
+
const parts = clean.split('/');
|
|
12
|
+
return parts[parts.length - 1] || 'weiyuan-openclaw-skill.zip';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchLatestReleaseNotes(base, nonce) {
|
|
16
|
+
const notesUrl = `${base}/RELEASE_NOTES_LATEST.json?nocache=${nonce}`;
|
|
17
|
+
try {
|
|
18
|
+
const resp = await axios.get(notesUrl, { timeout: 8000 });
|
|
19
|
+
const payload = resp.data || {};
|
|
20
|
+
const version = String(payload.version || '').trim();
|
|
21
|
+
const summary = String(payload.summary || '').trim();
|
|
22
|
+
const changes = Array.isArray(payload.changes) ? payload.changes.map((x) => String(x || '').trim()).filter(Boolean) : [];
|
|
23
|
+
if (!version && !summary && changes.length === 0) return null;
|
|
24
|
+
return {
|
|
25
|
+
version,
|
|
26
|
+
summary,
|
|
27
|
+
changes,
|
|
28
|
+
releasedAt: String(payload.releasedAt || '').trim()
|
|
29
|
+
};
|
|
30
|
+
} catch (_e) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolvePackageSource({ downloadUrl, upgradeBaseUrl, spinner = null }) {
|
|
36
|
+
if (downloadUrl) {
|
|
37
|
+
return { downloadUrl, zipFilename: toZipFilename(downloadUrl), version: null, sha256: '', from: 'download_url', releaseNotes: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const base = trimSlash(upgradeBaseUrl);
|
|
41
|
+
const nonce = Date.now();
|
|
42
|
+
const releaseNotes = await fetchLatestReleaseNotes(base, nonce);
|
|
43
|
+
try {
|
|
44
|
+
const latestMetaUrl = `${base}/SKILL_LATEST.json?nocache=${nonce}`;
|
|
45
|
+
const latestMetaResp = await axios.get(latestMetaUrl, { timeout: 10000 });
|
|
46
|
+
const meta = latestMetaResp.data || {};
|
|
47
|
+
const version = String(meta.version || '').trim();
|
|
48
|
+
if (version) {
|
|
49
|
+
const zipFilename = String(meta.zipName || '').trim() || `weiyuan-openclaw-skill-v${version}.zip`;
|
|
50
|
+
const resolvedDownloadUrl = `${base}/${zipFilename}?nocache=${nonce}`;
|
|
51
|
+
const sha256 = String(meta.sha256 || '').trim().toLowerCase();
|
|
52
|
+
if (spinner) {
|
|
53
|
+
spinner.text = `已解析最新版本: v${version}`;
|
|
54
|
+
}
|
|
55
|
+
return { downloadUrl: resolvedDownloadUrl, zipFilename, version, sha256, from: 'latest_json', releaseNotes };
|
|
56
|
+
}
|
|
57
|
+
} catch (_e) {
|
|
58
|
+
// fallback to txt
|
|
59
|
+
}
|
|
60
|
+
const latestUrl = `${base}/LATEST_SKILL_VERSION.txt?nocache=${nonce}`;
|
|
61
|
+
const latestResp = await axios.get(latestUrl, { timeout: 10000 });
|
|
62
|
+
const version = String(latestResp.data || '').trim();
|
|
63
|
+
if (!version) throw new Error('empty_latest_version');
|
|
64
|
+
const zipFilename = `weiyuan-openclaw-skill-v${version}.zip`;
|
|
65
|
+
const resolvedDownloadUrl = `${base}/${zipFilename}?nocache=${nonce}`;
|
|
66
|
+
if (spinner) {
|
|
67
|
+
spinner.text = `已解析最新版本: v${version}`;
|
|
68
|
+
}
|
|
69
|
+
return { downloadUrl: resolvedDownloadUrl, zipFilename, version, sha256: '', from: 'latest_version', releaseNotes };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function downloadFile(url, outputPath, spinner = null, expectedSha256 = '') {
|
|
73
|
+
try {
|
|
74
|
+
const response = await axios({
|
|
75
|
+
method: 'GET',
|
|
76
|
+
url: url,
|
|
77
|
+
responseType: 'stream',
|
|
78
|
+
timeout: 60000,
|
|
79
|
+
maxContentLength: Infinity,
|
|
80
|
+
maxBodyLength: Infinity
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const totalLength = parseInt(response.headers['content-length'], 10);
|
|
84
|
+
let downloadedLength = 0;
|
|
85
|
+
|
|
86
|
+
const writer = fs.createWriteStream(outputPath);
|
|
87
|
+
|
|
88
|
+
response.data.on('data', (chunk) => {
|
|
89
|
+
downloadedLength += chunk.length;
|
|
90
|
+
if (spinner && totalLength) {
|
|
91
|
+
const percent = (downloadedLength / totalLength * 100).toFixed(1);
|
|
92
|
+
spinner.text = `下载中... ${percent}% (${(downloadedLength / 1024 / 1024).toFixed(1)}MB / ${(totalLength / 1024 / 1024).toFixed(1)}MB)`;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
response.data.pipe(writer);
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
writer.on('finish', async () => {
|
|
100
|
+
try {
|
|
101
|
+
if (expectedSha256) {
|
|
102
|
+
const fileBuffer = await fs.readFile(outputPath);
|
|
103
|
+
const actualSha256 = crypto.createHash('sha256').update(fileBuffer).digest('hex').toLowerCase();
|
|
104
|
+
const expected = String(expectedSha256).toLowerCase();
|
|
105
|
+
if (actualSha256 !== expected) {
|
|
106
|
+
return reject(new Error(`skill_package_hash_mismatch expected=${expected} got=${actualSha256}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
resolve(true);
|
|
110
|
+
} catch (hashErr) {
|
|
111
|
+
reject(hashErr);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
writer.on('error', reject);
|
|
115
|
+
response.data.on('error', reject);
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (spinner) {
|
|
119
|
+
spinner.text = `下载失败: ${error.message}`;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { downloadFile, resolvePackageSource };
|