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 +7 -2
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/scripts/install.js +108 -22
- package/scripts/lib/common.js +17 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-agent-dashboard",
|
|
3
|
-
"version": "1.0.
|
|
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": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openclaw",
|
|
18
|
+
"agent",
|
|
19
|
+
"dashboard",
|
|
20
|
+
"visualization"
|
|
21
|
+
],
|
|
17
22
|
"license": "MIT",
|
|
18
23
|
"repository": {
|
|
19
24
|
"type": "git",
|
package/plugin/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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('解压完成');
|
package/scripts/lib/common.js
CHANGED
|
@@ -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
|
|
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:
|
|
220
|
+
shell: true,
|
|
207
221
|
}
|
|
208
222
|
);
|
|
209
223
|
return { success: true, code: 0, output: result || '' };
|