openclaw-agent-dashboard 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-agent-dashboard",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "多 Agent 可视化看板 - 状态、任务、API、工作流、协作流程",
5
5
  "bin": {
6
6
  "openclaw-agent-dashboard": "scripts/install.js"
@@ -13,7 +13,12 @@
13
13
  "bundle": "bash scripts/bundle.sh",
14
14
  "start": "node scripts/start.js"
15
15
  },
16
- "keywords": ["openclaw", "agent", "dashboard", "visualization"],
16
+ "keywords": [
17
+ "openclaw",
18
+ "agent",
19
+ "dashboard",
20
+ "visualization"
21
+ ],
17
22
  "license": "MIT",
18
23
  "repository": {
19
24
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-agent-dashboard",
3
3
  "name": "OpenClaw Agent Dashboard",
4
4
  "description": "多 Agent 可视化看板 - 状态、任务、API、工作流、协作流程",
5
- "version": "1.0.6",
5
+ "version": "1.0.8",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-agent-dashboard",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "多 Agent 可视化看板 - OpenClaw 插件",
5
5
  "main": "index.js",
6
6
  "openclaw": {
@@ -32,11 +32,12 @@ const {
32
32
  runCommandAsync,
33
33
  rmrf,
34
34
  copyDir,
35
- downloadFile,
36
35
  backupDir,
37
36
  restoreBackup,
38
37
  cleanupBackup,
39
38
  cleanupOldBackups,
39
+ downloadFile,
40
+ formatBytes,
40
41
  } = require('./lib/common');
41
42
 
42
43
  // ============================================
@@ -125,6 +126,108 @@ function checkPrerequisites(remoteMode) {
125
126
  return allPassed;
126
127
  }
127
128
 
129
+ // ============================================
130
+ // 远程模式:tgz 解压(纯 Node.js,跨平台)
131
+ // ============================================
132
+
133
+ /**
134
+ * 解析 tar 文件路径和大小
135
+ * @param {Buffer} buffer - tar 数据
136
+ * @returns {{name: string, size: number, type: string, offset: number}[]}
137
+ */
138
+ function parseTarEntries(buffer) {
139
+ const entries = [];
140
+ let offset = 0;
141
+
142
+ while (offset < buffer.length - 512) {
143
+ // 读取文件名(0-99)
144
+ let name = buffer.toString('utf8', offset, offset + 100).replace(/\0.*$/, '');
145
+
146
+ // 跳过空块
147
+ if (name === '') {
148
+ offset += 512;
149
+ continue;
150
+ }
151
+
152
+ // 文件大小(124-135,八进制)
153
+ const sizeStr = buffer.toString('utf8', offset + 124, offset + 135).replace(/\0.*$/, '').trim();
154
+ const size = parseInt(sizeStr, 8) || 0;
155
+
156
+ // 文件类型(156)
157
+ const type = buffer.toString('utf8', offset + 156, offset + 157);
158
+
159
+ entries.push({ name, size, type, offset });
160
+
161
+ // 跳到下一个条目(512 字节头 + 数据,数据按 512 对齐)
162
+ const dataBlocks = Math.ceil(size / 512);
163
+ offset += 512 + dataBlocks * 512;
164
+ }
165
+
166
+ return entries;
167
+ }
168
+
169
+ /**
170
+ * 解压 tgz 文件到指定目录
171
+ * @param {string} tgzFile - tgz 文件路径
172
+ * @param {string} destDir - 目标目录
173
+ * @param {boolean} verbose - 显示详细信息
174
+ * @returns {Promise<boolean>}
175
+ */
176
+ async function extractTgz(tgzFile, destDir, verbose) {
177
+ const zlib = require('zlib');
178
+ const fsPromises = require('fs').promises;
179
+
180
+ return new Promise((resolve) => {
181
+ const fileStream = fs.createReadStream(tgzFile);
182
+ const gunzip = zlib.createGunzip();
183
+ const chunks = [];
184
+
185
+ gunzip.on('data', (chunk) => chunks.push(chunk));
186
+ gunzip.on('end', () => {
187
+ try {
188
+ const tarData = Buffer.concat(chunks);
189
+ const entries = parseTarEntries(tarData);
190
+
191
+ for (const entry of entries) {
192
+ // 跳过目录类型
193
+ if (entry.type === '5') continue;
194
+ // 跳过空文件名或 pax header
195
+ if (!entry.name || entry.name.startsWith('PaxHeader')) continue;
196
+
197
+ const fullPath = path.join(destDir, entry.name);
198
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
199
+
200
+ // 写入文件数据
201
+ const dataOffset = entry.offset + 512;
202
+ const fileData = tarData.slice(dataOffset, dataOffset + entry.size);
203
+ fs.writeFileSync(fullPath, fileData);
204
+
205
+ if (verbose) {
206
+ logInfo(` ${entry.name} (${formatBytes(entry.size)})`);
207
+ }
208
+ }
209
+
210
+ resolve(true);
211
+ } catch (err) {
212
+ logError(`解压处理失败: ${err.message}`);
213
+ resolve(false);
214
+ }
215
+ });
216
+
217
+ gunzip.on('error', (err) => {
218
+ logError(`gzip 解压失败: ${err.message}`);
219
+ resolve(false);
220
+ });
221
+
222
+ fileStream.on('error', (err) => {
223
+ logError(`读取文件失败: ${err.message}`);
224
+ resolve(false);
225
+ });
226
+
227
+ fileStream.pipe(gunzip);
228
+ });
229
+ }
230
+
128
231
  // ============================================
129
232
  // 远程模式:版本解析
130
233
  // ============================================
@@ -199,37 +302,20 @@ async function remoteInstall(pluginPath, options) {
199
302
  }
200
303
  logOk('下载完成');
201
304
 
202
- // 3. 解压 tgz
305
+ // 3. 解压 tgz(纯 Node.js 实现,不依赖系统 tar)
203
306
  logStep('解压安装包...');
204
307
  const extractDir = path.join(tmpDir, 'extract');
205
308
  fs.mkdirSync(extractDir, { recursive: true });
206
309
 
207
- const tar = require('child_process');
208
- const isWin = process.platform === 'win32';
209
- // tar 命令在 Windows 10+ / Node 18+ 可用,否则用 node tar 库
210
310
  let extractOk = false;
211
-
212
- // 尝试系统 tar
213
311
  try {
214
- tar.execSync(`tar xzf "${tgzFile}" -C "${extractDir}"`, {
215
- stdio: options.verbose ? 'inherit' : 'pipe',
216
- shell: true,
217
- });
218
- extractOk = true;
219
- } catch {
220
- // tar 不可用,尝试 node 内置解压
221
- try {
222
- const zlib = require('zlib');
223
- const { Readable } = require('stream');
224
-
225
- // 简单的 tgz 解压:用 gunzip + tar(通过 node-tar 或系统命令)
226
- // 如果都失败,报错提示
227
- } catch {}
312
+ extractOk = await extractTgz(tgzFile, extractDir, options.verbose);
313
+ } catch (err) {
314
+ logError(`解压失败: ${err.message}`);
228
315
  }
229
316
 
230
317
  if (!extractOk) {
231
318
  logError('解压失败');
232
- logInfo('请确保系统支持 tar 命令');
233
319
  return false;
234
320
  }
235
321
  logOk('解压完成');
@@ -191,10 +191,24 @@ function shellEscape(arg) {
191
191
  */
192
192
  function runCommand(cmd, args = [], options = {}) {
193
193
  const { cwd, silent = true, timeout = 120000 } = options;
194
+ const isWin = process.platform === 'win32';
194
195
 
195
196
  try {
196
- // 构建命令字符串,对参数进行转义
197
- const cmdStr = [cmd, ...args.map(shellEscape)].join(' ');
197
+ // Windows 用双引号,Unix 用单引号
198
+ const esc = (arg) => {
199
+ if (!arg) return '""';
200
+ if (isWin) {
201
+ // Windows: 用双引号包裹含空格/特殊字符的路径
202
+ if (/[^a-zA-Z0-9_\-./:=@]/.test(arg)) {
203
+ return '"' + arg.replace(/"/g, '""') + '"';
204
+ }
205
+ return arg;
206
+ } else {
207
+ return shellEscape(arg);
208
+ }
209
+ };
210
+
211
+ const cmdStr = [cmd, ...args.map(esc)].join(' ');
198
212
 
199
213
  const result = execSync(
200
214
  cmdStr,
@@ -203,7 +217,7 @@ function runCommand(cmd, args = [], options = {}) {
203
217
  encoding: 'utf8',
204
218
  timeout,
205
219
  stdio: silent ? ['ignore', 'pipe', 'pipe'] : 'inherit',
206
- shell: process.platform === 'win32',
220
+ shell: true,
207
221
  }
208
222
  );
209
223
  return { success: true, code: 0, output: result || '' };