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 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 💫\n💡 对话框输入「查看帮助」即可查看使用指南",
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
- 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
- async function downloadFile(url, outputPath, spinner = null) {
35
- try {
36
- const response = await axios({
37
- method: 'GET',
38
- url: url,
39
- responseType: 'stream',
40
- timeout: 60000,
41
- maxContentLength: Infinity,
42
- maxBodyLength: Infinity
43
- });
44
-
45
- const totalLength = parseInt(response.headers['content-length'], 10);
46
- let downloadedLength = 0;
47
-
48
- const writer = fs.createWriteStream(outputPath);
49
-
50
- response.data.on('data', (chunk) => {
51
- downloadedLength += chunk.length;
52
- if (spinner && totalLength) {
53
- const percent = (downloadedLength / totalLength * 100).toFixed(1);
54
- spinner.text = `下载中... ${percent}% (${(downloadedLength / 1024 / 1024).toFixed(1)}MB / ${(totalLength / 1024 / 1024).toFixed(1)}MB)`;
55
- }
56
- });
57
-
58
- response.data.pipe(writer);
59
-
60
- return new Promise((resolve, reject) => {
61
- writer.on('finish', () => resolve(true));
62
- writer.on('error', reject);
63
- response.data.on('error', reject);
64
- });
65
- } catch (error) {
66
- if (spinner) {
67
- spinner.text = `下载失败: ${error.message}`;
68
- }
69
- return false;
70
- }
71
- }
72
-
73
- module.exports = { downloadFile, resolvePackageSource };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-weiyuan-init",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "description": "OpenClaw Weiyuan Skill 一键初始化工具",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {