openclaw-agent-dashboard 1.0.5 → 1.0.7

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.5",
3
+ "version": "1.0.7",
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.5",
5
+ "version": "1.0.7",
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.5",
3
+ "version": "1.0.7",
4
4
  "description": "多 Agent 可视化看板 - OpenClaw 插件",
5
5
  "main": "index.js",
6
6
  "openclaw": {
@@ -56,12 +56,24 @@ function parseArgs() {
56
56
  // Python 环境检测
57
57
  // ============================================
58
58
 
59
+ /**
60
+ * 获取 Python 命令(跨平台)
61
+ * Windows 通常是 python,Linux/macOS 通常是 python3
62
+ * @returns {string}
63
+ */
64
+ function getPythonCmd() {
65
+ if (runCommand('python3', ['--version'], { silent: true }).success) return 'python3';
66
+ if (runCommand('python', ['--version'], { silent: true }).success) return 'python';
67
+ return 'python3'; // 默认
68
+ }
69
+
59
70
  /**
60
71
  * 检查 venv 模块是否可用
61
72
  * @returns {boolean}
62
73
  */
63
74
  function checkVenvModule() {
64
- const result = runCommand('python3', ['-c', 'import venv'], { silent: true });
75
+ const pythonCmd = getPythonCmd();
76
+ const result = runCommand(pythonCmd, ['-c', 'import venv'], { silent: true });
65
77
  return result.success;
66
78
  }
67
79
 
@@ -70,14 +82,13 @@ function checkVenvModule() {
70
82
  * @returns {boolean}
71
83
  */
72
84
  function checkPipModule() {
73
- // 尝试 python3 -m pip
74
- let result = runCommand('python3', ['-m', 'pip', '--version'], { silent: true });
85
+ const pythonCmd = getPythonCmd();
86
+ // 尝试 python -m pip
87
+ let result = runCommand(pythonCmd, ['-m', 'pip', '--version'], { silent: true });
75
88
  if (result.success) return true;
76
89
 
77
- // 尝试 pip3
90
+ // 尝试 pip3 / pip
78
91
  if (commandExists('pip3')) return true;
79
-
80
- // 尝试 pip
81
92
  if (commandExists('pip')) return true;
82
93
 
83
94
  return false;
@@ -121,7 +132,8 @@ function installWithVenv(reqFile, venvDir, silent) {
121
132
 
122
133
  // 创建 venv
123
134
  logInfo(' 创建虚拟环境...');
124
- const createResult = runCommand('python3', ['-m', 'venv', venvDir], { silent });
135
+ const pythonCmd = getPythonCmd();
136
+ const createResult = runCommand(pythonCmd, ['-m', 'venv', venvDir], { silent });
125
137
  if (!createResult.success) {
126
138
  logWarn(' venv 创建失败');
127
139
  if (!silent) {
@@ -169,8 +181,8 @@ function installWithPipUser(reqFile, silent) {
169
181
  logInfo(' 尝试: pip --user(PEP 668 兜底)');
170
182
 
171
183
  const pipCommands = [
172
- { cmd: 'python3', args: ['-m', 'pip', 'install', '-r', reqFile, '-q', '--user'], name: 'python3 -m pip --user' },
173
- { cmd: 'python3', args: ['-m', 'pip', 'install', '-r', reqFile, '-q'], name: 'python3 -m pip' },
184
+ { cmd: getPythonCmd(), args: ['-m', 'pip', 'install', '-r', reqFile, '-q', '--user'], name: 'pip --user' },
185
+ { cmd: getPythonCmd(), args: ['-m', 'pip', 'install', '-r', reqFile, '-q'], name: 'pip' },
174
186
  { cmd: 'pip', args: ['install', '-r', reqFile, '-q', '--user'], name: 'pip --user' },
175
187
  { cmd: 'pip3', args: ['install', '-r', reqFile, '-q', '--user'], name: 'pip3 --user' },
176
188
  ];
@@ -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('解压完成');
@@ -3,3 +3,4 @@ uvicorn[standard]==0.27.0
3
3
  pydantic==2.5.0
4
4
  python-multipart==0.0.6
5
5
  watchdog>=3.0.0
6
+ tzdata