openclaw-agent-dashboard 1.0.4

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.
Files changed (111) hide show
  1. package/.github/workflows/release.yml +56 -0
  2. package/README.md +302 -0
  3. package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
  4. package/docs/RELEASE-LATEST.md +189 -0
  5. package/docs/RELEASE-MODEL-CONFIG.md +95 -0
  6. package/docs/release-guide.md +259 -0
  7. package/docs/release-operations-manual.md +167 -0
  8. package/docs/specs/tr3-install-system.md +580 -0
  9. package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
  10. package/frontend/index.html +12 -0
  11. package/frontend/package-lock.json +1240 -0
  12. package/frontend/package.json +19 -0
  13. package/frontend/src/App.vue +331 -0
  14. package/frontend/src/components/AgentCard.vue +796 -0
  15. package/frontend/src/components/AgentConfigPanel.vue +539 -0
  16. package/frontend/src/components/AgentDetailPanel.vue +738 -0
  17. package/frontend/src/components/ErrorAnalysisView.vue +546 -0
  18. package/frontend/src/components/ErrorCenterPanel.vue +844 -0
  19. package/frontend/src/components/PerformanceMonitor.vue +515 -0
  20. package/frontend/src/components/SettingsPanel.vue +236 -0
  21. package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
  22. package/frontend/src/components/chain/ChainEdge.vue +85 -0
  23. package/frontend/src/components/chain/ChainNode.vue +166 -0
  24. package/frontend/src/components/chain/TaskChainView.vue +425 -0
  25. package/frontend/src/components/chain/index.ts +3 -0
  26. package/frontend/src/components/chain/types.ts +70 -0
  27. package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
  28. package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
  29. package/frontend/src/components/performance/PerformancePanel.vue +119 -0
  30. package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
  31. package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
  32. package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
  33. package/frontend/src/components/timeline/TimelineRound.vue +135 -0
  34. package/frontend/src/components/timeline/TimelineStep.vue +691 -0
  35. package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
  36. package/frontend/src/components/timeline/TimelineView.vue +540 -0
  37. package/frontend/src/components/timeline/index.ts +5 -0
  38. package/frontend/src/components/timeline/types.ts +120 -0
  39. package/frontend/src/composables/index.ts +7 -0
  40. package/frontend/src/composables/useDebounce.ts +48 -0
  41. package/frontend/src/composables/useRealtime.ts +52 -0
  42. package/frontend/src/composables/useState.ts +52 -0
  43. package/frontend/src/composables/useThrottle.ts +46 -0
  44. package/frontend/src/composables/useVirtualScroll.ts +106 -0
  45. package/frontend/src/main.ts +4 -0
  46. package/frontend/src/managers/EventDispatcher.ts +127 -0
  47. package/frontend/src/managers/RealtimeDataManager.ts +293 -0
  48. package/frontend/src/managers/StateManager.ts +128 -0
  49. package/frontend/src/managers/index.ts +5 -0
  50. package/frontend/src/types/collaboration.ts +135 -0
  51. package/frontend/src/types/index.ts +20 -0
  52. package/frontend/src/types/performance.ts +105 -0
  53. package/frontend/src/types/task.ts +38 -0
  54. package/frontend/vite.config.ts +18 -0
  55. package/package.json +22 -0
  56. package/plugin/README.md +99 -0
  57. package/plugin/config.json.example +1 -0
  58. package/plugin/index.js +250 -0
  59. package/plugin/openclaw.plugin.json +17 -0
  60. package/plugin/package.json +21 -0
  61. package/scripts/build-plugin.js +67 -0
  62. package/scripts/bundle.sh +62 -0
  63. package/scripts/install-plugin.sh +162 -0
  64. package/scripts/install-python-deps.js +346 -0
  65. package/scripts/install-python-deps.sh +226 -0
  66. package/scripts/install.js +512 -0
  67. package/scripts/install.sh +367 -0
  68. package/scripts/lib/common.js +490 -0
  69. package/scripts/lib/common.sh +137 -0
  70. package/scripts/release-pack.sh +110 -0
  71. package/scripts/start.js +50 -0
  72. package/scripts/test_available_models.py +284 -0
  73. package/scripts/test_websocket_ping.py +44 -0
  74. package/src/backend/agents.py +73 -0
  75. package/src/backend/api/__init__.py +1 -0
  76. package/src/backend/api/agent_config_api.py +90 -0
  77. package/src/backend/api/agents.py +73 -0
  78. package/src/backend/api/agents_config.py +75 -0
  79. package/src/backend/api/chains.py +126 -0
  80. package/src/backend/api/collaboration.py +902 -0
  81. package/src/backend/api/debug_paths.py +39 -0
  82. package/src/backend/api/error_analysis.py +146 -0
  83. package/src/backend/api/errors.py +281 -0
  84. package/src/backend/api/performance.py +784 -0
  85. package/src/backend/api/subagents.py +770 -0
  86. package/src/backend/api/timeline.py +144 -0
  87. package/src/backend/api/websocket.py +251 -0
  88. package/src/backend/collaboration.py +405 -0
  89. package/src/backend/data/__init__.py +1 -0
  90. package/src/backend/data/agent_config_manager.py +270 -0
  91. package/src/backend/data/chain_reader.py +299 -0
  92. package/src/backend/data/config_reader.py +153 -0
  93. package/src/backend/data/error_analyzer.py +430 -0
  94. package/src/backend/data/session_reader.py +445 -0
  95. package/src/backend/data/subagent_reader.py +244 -0
  96. package/src/backend/data/task_history.py +118 -0
  97. package/src/backend/data/timeline_reader.py +981 -0
  98. package/src/backend/errors.py +63 -0
  99. package/src/backend/main.py +89 -0
  100. package/src/backend/mechanism_reader.py +131 -0
  101. package/src/backend/mechanisms.py +32 -0
  102. package/src/backend/performance.py +474 -0
  103. package/src/backend/requirements.txt +5 -0
  104. package/src/backend/session_reader.py +238 -0
  105. package/src/backend/status/__init__.py +1 -0
  106. package/src/backend/status/error_detector.py +122 -0
  107. package/src/backend/status/status_calculator.py +301 -0
  108. package/src/backend/status_calculator.py +121 -0
  109. package/src/backend/subagent_reader.py +229 -0
  110. package/src/backend/watchers/__init__.py +4 -0
  111. package/src/backend/watchers/file_watcher.py +159 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * 公共函数库 - 安装脚本共用(Node.js 版)
3
+ *
4
+ * 提供跨平台的日志、系统检测、路径解析、命令执行等功能
5
+ */
6
+
7
+ const os = require('os');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync, spawn } = require('child_process');
11
+
12
+ // ============================================
13
+ // 日志函数
14
+ // ============================================
15
+
16
+ /**
17
+ * 输出普通信息
18
+ * @param {string} msg
19
+ */
20
+ function logInfo(msg) {
21
+ console.log(msg);
22
+ }
23
+
24
+ /**
25
+ * 输出步骤标题
26
+ * @param {string} msg
27
+ */
28
+ function logStep(msg) {
29
+ console.log('\n>>> ' + msg);
30
+ }
31
+
32
+ /**
33
+ * 输出成功信息
34
+ * @param {string} msg
35
+ */
36
+ function logOk(msg) {
37
+ console.log('✓ ' + msg);
38
+ }
39
+
40
+ /**
41
+ * 输出警告信息
42
+ * @param {string} msg
43
+ */
44
+ function logWarn(msg) {
45
+ console.log('⚠ ' + msg);
46
+ }
47
+
48
+ /**
49
+ * 输出错误信息
50
+ * @param {string} msg
51
+ */
52
+ function logError(msg) {
53
+ console.error('❌ ' + msg);
54
+ }
55
+
56
+ // ============================================
57
+ // 系统检测
58
+ // ============================================
59
+
60
+ /**
61
+ * 检测操作系统
62
+ * @returns {'linux' | 'macos' | 'windows' | 'unknown'}
63
+ */
64
+ function detectOS() {
65
+ switch (process.platform) {
66
+ case 'linux':
67
+ return 'linux';
68
+ case 'darwin':
69
+ return 'macos';
70
+ case 'win32':
71
+ return 'windows';
72
+ default:
73
+ return 'unknown';
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 检查命令是否存在
79
+ * @param {string} cmd - 命令名称
80
+ * @returns {boolean}
81
+ */
82
+ function commandExists(cmd) {
83
+ try {
84
+ const isWin = process.platform === 'win32';
85
+ // Windows 使用 where,Unix 使用 which
86
+ const checkCmd = isWin ? 'where' : 'which';
87
+ execSync(`${checkCmd} ${cmd}`, { stdio: 'ignore' });
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // ============================================
95
+ // 路径解析
96
+ // ============================================
97
+
98
+ /**
99
+ * 解析 OpenClaw 配置目录
100
+ * 优先级: OPENCLAW_STATE_DIR > CLAWDBOT_STATE_DIR > OPENCLAW_HOME/.openclaw > HOME/.openclaw
101
+ * @returns {string}
102
+ */
103
+ function resolveOpenClawConfigDir() {
104
+ // 1. OPENCLAW_STATE_DIR(最高优先级)
105
+ if (process.env.OPENCLAW_STATE_DIR) {
106
+ return expandHomeDir(process.env.OPENCLAW_STATE_DIR);
107
+ }
108
+
109
+ // 2. CLAWDBOT_STATE_DIR(兼容旧名称)
110
+ if (process.env.CLAWDBOT_STATE_DIR) {
111
+ return expandHomeDir(process.env.CLAWDBOT_STATE_DIR);
112
+ }
113
+
114
+ // 3. OPENCLAW_HOME/.openclaw
115
+ let homeDir = process.env.OPENCLAW_HOME || process.env.HOME || os.homedir();
116
+ homeDir = expandHomeDir(homeDir);
117
+
118
+ return path.join(homeDir, '.openclaw');
119
+ }
120
+
121
+ /**
122
+ * 展开 ~ 前缀为用户目录
123
+ * @param {string} dir
124
+ * @returns {string}
125
+ */
126
+ function expandHomeDir(dir) {
127
+ if (!dir) return os.homedir();
128
+ if (dir === '~') return os.homedir();
129
+ if (dir.startsWith('~/')) return path.join(os.homedir(), dir.slice(2));
130
+ if (dir.startsWith('~')) return path.join(os.homedir(), dir.slice(1));
131
+ return dir;
132
+ }
133
+
134
+ /**
135
+ * 获取插件安装路径
136
+ * @returns {string}
137
+ */
138
+ function getPluginPath() {
139
+ return path.join(resolveOpenClawConfigDir(), 'extensions', 'openclaw-agent-dashboard');
140
+ }
141
+
142
+ // ============================================
143
+ // JSON 解析
144
+ // ============================================
145
+
146
+ /**
147
+ * 从 JSON 文件解析 version 字段
148
+ * @param {string} filePath - JSON 文件路径
149
+ * @returns {string | null}
150
+ */
151
+ function parseJsonVersion(filePath) {
152
+ try {
153
+ if (!fs.existsSync(filePath)) {
154
+ return null;
155
+ }
156
+ const content = fs.readFileSync(filePath, 'utf8');
157
+ const json = JSON.parse(content);
158
+ return json.version || null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ // ============================================
165
+ // 命令执行
166
+ // ============================================
167
+
168
+ /**
169
+ * 对命令参数进行 shell 转义
170
+ * @param {string} arg
171
+ * @returns {string}
172
+ */
173
+ function shellEscape(arg) {
174
+ if (!arg) return "''";
175
+ // 如果包含特殊字符,用单引号包裹
176
+ if (/[^a-zA-Z0-9_\-./:=@]/.test(arg)) {
177
+ return "'" + arg.replace(/'/g, "'\\''") + "'";
178
+ }
179
+ return arg;
180
+ }
181
+
182
+ /**
183
+ * 同步执行命令
184
+ * @param {string} cmd - 命令
185
+ * @param {string[]} args - 参数
186
+ * @param {object} options - 选项
187
+ * @param {string} [options.cwd] - 工作目录
188
+ * @param {boolean} [options.silent=true] - 是否静默(不显示输出)
189
+ * @param {number} [options.timeout=120000] - 超时时间(毫秒)
190
+ * @returns {{ success: boolean, code: number, output: string }}
191
+ */
192
+ function runCommand(cmd, args = [], options = {}) {
193
+ const { cwd, silent = true, timeout = 120000 } = options;
194
+
195
+ try {
196
+ // 构建命令字符串,对参数进行转义
197
+ const cmdStr = [cmd, ...args.map(shellEscape)].join(' ');
198
+
199
+ const result = execSync(
200
+ cmdStr,
201
+ {
202
+ cwd,
203
+ encoding: 'utf8',
204
+ timeout,
205
+ stdio: silent ? ['ignore', 'pipe', 'pipe'] : 'inherit',
206
+ shell: process.platform === 'win32',
207
+ }
208
+ );
209
+ return { success: true, code: 0, output: result || '' };
210
+ } catch (error) {
211
+ // 如果是静默模式且失败,返回错误信息
212
+ const output = silent
213
+ ? (error.stdout || error.stderr || error.message || '')
214
+ : '';
215
+ return {
216
+ success: false,
217
+ code: error.status || 1,
218
+ output,
219
+ };
220
+ }
221
+ }
222
+
223
+ /**
224
+ * 异步执行命令(实时输出到控制台)
225
+ * @param {string} cmd - 命令
226
+ * @param {string[]} args - 参数
227
+ * @param {object} options - 选项
228
+ * @param {string} [options.cwd] - 工作目录
229
+ * @returns {Promise<{ success: boolean, code: number }>}
230
+ */
231
+ function runCommandAsync(cmd, args = [], options = {}) {
232
+ return new Promise((resolve) => {
233
+ const { cwd } = options;
234
+
235
+ const child = spawn(cmd, args, {
236
+ cwd,
237
+ stdio: 'inherit',
238
+ shell: process.platform === 'win32',
239
+ });
240
+
241
+ child.on('close', (code) => {
242
+ resolve({ success: code === 0, code: code || 0 });
243
+ });
244
+
245
+ child.on('error', (err) => {
246
+ logError(`执行失败: ${err.message}`);
247
+ resolve({ success: false, code: 1 });
248
+ });
249
+ });
250
+ }
251
+
252
+ // ============================================
253
+ // 文件操作辅助
254
+ // ============================================
255
+
256
+ /**
257
+ * 递归删除目录
258
+ * @param {string} dir
259
+ */
260
+ function rmrf(dir) {
261
+ if (fs.existsSync(dir)) {
262
+ fs.rmSync(dir, { recursive: true, force: true });
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 递归复制目录
268
+ * @param {string} src - 源目录
269
+ * @param {string} dest - 目标目录
270
+ * @param {string[]} [exclude=[]] - 排除的文件/目录名
271
+ */
272
+ function copyDir(src, dest, exclude = []) {
273
+ fs.mkdirSync(dest, { recursive: true });
274
+ const entries = fs.readdirSync(src, { withFileTypes: true });
275
+
276
+ for (const entry of entries) {
277
+ // 跳过排除项
278
+ if (exclude.includes(entry.name)) continue;
279
+ // 跳过编译产物和日志
280
+ if (entry.name.endsWith('.pyc') || entry.name.endsWith('.log')) continue;
281
+ if (entry.name === '__pycache__' || entry.name === '.pytest_cache') continue;
282
+
283
+ const srcPath = path.join(src, entry.name);
284
+ const destPath = path.join(dest, entry.name);
285
+
286
+ if (entry.isDirectory()) {
287
+ copyDir(srcPath, destPath, exclude);
288
+ } else {
289
+ fs.copyFileSync(srcPath, destPath);
290
+ }
291
+ }
292
+ }
293
+
294
+ // ============================================
295
+ // 网络下载
296
+ // ============================================
297
+
298
+ const https = require('https');
299
+ const http = require('http');
300
+
301
+ /**
302
+ * 下载文件
303
+ * @param {string} url - 下载地址
304
+ * @param {string} dest - 保存路径
305
+ * @param {object} [options]
306
+ * @param {boolean} [options.verbose] - 显示进度
307
+ * @returns {Promise<boolean>}
308
+ */
309
+ function downloadFile(url, dest, options = {}) {
310
+ return new Promise((resolve) => {
311
+ logInfo(` 下载: ${url}`);
312
+
313
+ const client = url.startsWith('https') ? https : http;
314
+ const file = fs.createWriteStream(dest);
315
+
316
+ client.get(url, (res) => {
317
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
318
+ file.close();
319
+ fs.unlinkSync(dest);
320
+ return downloadFile(res.headers.location, dest, options).then(resolve);
321
+ }
322
+
323
+ if (res.statusCode !== 200) {
324
+ file.close();
325
+ fs.unlinkSync(dest);
326
+ logError(` 下载失败: HTTP ${res.statusCode}`);
327
+ resolve(false);
328
+ return;
329
+ }
330
+
331
+ const total = parseInt(res.headers['content-length'], 10);
332
+ let downloaded = 0;
333
+
334
+ res.on('data', (chunk) => {
335
+ downloaded += chunk.length;
336
+ if (options.verbose && total) {
337
+ const pct = Math.round((downloaded / total) * 100);
338
+ process.stdout.write(` 进度: ${pct}% (${formatBytes(downloaded)}/${formatBytes(total)})\r`);
339
+ }
340
+ });
341
+
342
+ res.pipe(file);
343
+
344
+ file.on('finish', () => {
345
+ file.close();
346
+ if (options.verbose) console.log('');
347
+ resolve(true);
348
+ });
349
+ }).on('error', (err) => {
350
+ file.close();
351
+ if (fs.existsSync(dest)) fs.unlinkSync(dest);
352
+ logError(` 下载失败: ${err.message}`);
353
+ resolve(false);
354
+ });
355
+ });
356
+ }
357
+
358
+ /**
359
+ * 格式化字节数
360
+ * @param {number} bytes
361
+ * @returns {string}
362
+ */
363
+ function formatBytes(bytes) {
364
+ if (bytes < 1024) return bytes + ' B';
365
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
366
+ return (bytes / 1048576).toFixed(1) + ' MB';
367
+ }
368
+
369
+ // ============================================
370
+ // 备份与恢复
371
+ // ============================================
372
+
373
+ /**
374
+ * 备份目录(移动到 .backup-{timestamp})
375
+ * @param {string} dir - 要备份的目录
376
+ * @returns {string | null} 备份目录路径,失败返回 null
377
+ */
378
+ function backupDir(dir) {
379
+ if (!fs.existsSync(dir)) return null;
380
+
381
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
382
+ const backupPath = dir + `.backup-${timestamp}`;
383
+
384
+ try {
385
+ fs.renameSync(dir, backupPath);
386
+ logInfo(` 备份: ${path.basename(backupPath)}`);
387
+ return backupPath;
388
+ } catch (err) {
389
+ logWarn(` 备份失败: ${err.message}`);
390
+ // 尝试复制后删除
391
+ try {
392
+ copyDir(dir, backupPath);
393
+ rmrf(dir);
394
+ return backupPath;
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * 恢复备份
403
+ * @param {string} backupPath - 备份目录路径
404
+ * @param {string} targetPath - 目标路径
405
+ * @returns {boolean}
406
+ */
407
+ function restoreBackup(backupPath, targetPath) {
408
+ if (!fs.existsSync(backupPath)) return false;
409
+
410
+ try {
411
+ rmrf(targetPath);
412
+ fs.renameSync(backupPath, targetPath);
413
+ return true;
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * 清理备份目录
421
+ * @param {string} backupPath
422
+ */
423
+ function cleanupBackup(backupPath) {
424
+ if (backupPath && fs.existsSync(backupPath)) {
425
+ rmrf(backupPath);
426
+ logInfo(' 已清理备份');
427
+ }
428
+ }
429
+
430
+ /**
431
+ * 清理旧备份目录(只保留最新的 N 个)
432
+ * @param {string} parentDir - 父目录
433
+ * @param {string} prefix - 备份目录前缀
434
+ * @param {number} [keep=2] - 保留数量
435
+ */
436
+ function cleanupOldBackups(parentDir, prefix, keep = 2) {
437
+ if (!fs.existsSync(parentDir)) return;
438
+
439
+ const dirs = fs.readdirSync(parentDir)
440
+ .filter(f => f.startsWith(prefix) && fs.statSync(path.join(parentDir, f)).isDirectory())
441
+ .sort()
442
+ .reverse();
443
+
444
+ // 保留最新的 keep 个,删除其余
445
+ for (let i = keep; i < dirs.length; i++) {
446
+ rmrf(path.join(parentDir, dirs[i]));
447
+ logInfo(` 清理旧备份: ${dirs[i]}`);
448
+ }
449
+ }
450
+
451
+ // ============================================
452
+ // 导出
453
+ // ============================================
454
+
455
+ module.exports = {
456
+ // 日志
457
+ logInfo,
458
+ logStep,
459
+ logOk,
460
+ logWarn,
461
+ logError,
462
+
463
+ // 系统检测
464
+ detectOS,
465
+ commandExists,
466
+
467
+ // 路径解析
468
+ resolveOpenClawConfigDir,
469
+ expandHomeDir,
470
+ getPluginPath,
471
+
472
+ // JSON
473
+ parseJsonVersion,
474
+
475
+ // 命令执行
476
+ runCommand,
477
+ runCommandAsync,
478
+
479
+ // 文件操作
480
+ rmrf,
481
+ copyDir,
482
+ backupDir,
483
+ restoreBackup,
484
+ cleanupBackup,
485
+ cleanupOldBackups,
486
+
487
+ // 网络下载
488
+ downloadFile,
489
+ formatBytes,
490
+ };
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # 公共函数库 - 安装脚本共用
4
+ # 用法: source "$(dirname "$0")/../lib/common.sh" 或 source "$SCRIPT_DIR/lib/common.sh"
5
+ #
6
+
7
+ # ============================================
8
+ # 日志函数
9
+ # ============================================
10
+
11
+ log_info() { echo "$1"; }
12
+ log_step() { echo ""; echo ">>> $1"; }
13
+ log_ok() { echo "✓ $1"; }
14
+ log_warn() { echo "⚠ $1"; }
15
+ log_error() { echo "❌ $1" >&2; }
16
+
17
+ # ============================================
18
+ # 执行辅助
19
+ # ============================================
20
+
21
+ # run_silent: 静默执行命令(VERBOSE=1 时显示输出)
22
+ # 用法: run_silent command args...
23
+ run_silent() {
24
+ if [ "${VERBOSE:-0}" = "1" ]; then
25
+ "$@"
26
+ else
27
+ "$@" 2>/dev/null
28
+ fi
29
+ }
30
+
31
+ # ============================================
32
+ # 配置目录解析(与 OpenClaw 内部逻辑一致)
33
+ # ============================================
34
+
35
+ # resolve_openclaw_config_dir: 解析 OpenClaw 配置目录
36
+ # 优先级: OPENCLAW_STATE_DIR > CLAWDBOT_STATE_DIR > OPENCLAW_HOME/.openclaw > HOME/.openclaw
37
+ # 用法: OPENCLAW_CONFIG_DIR=$(resolve_openclaw_config_dir)
38
+ resolve_openclaw_config_dir() {
39
+ if [ -n "${OPENCLAW_STATE_DIR:-}" ]; then
40
+ echo "${OPENCLAW_STATE_DIR}"
41
+ return
42
+ fi
43
+ if [ -n "${CLAWDBOT_STATE_DIR:-}" ]; then
44
+ echo "${CLAWDBOT_STATE_DIR}"
45
+ return
46
+ fi
47
+ local home_dir="${OPENCLAW_HOME:-${HOME:-${USERPROFILE:-}}}"
48
+ if [ -z "$home_dir" ]; then
49
+ home_dir="${HOME:-}"
50
+ fi
51
+ # 展开 ~ 前缀(与 openclaw 行为一致)
52
+ if [[ "$home_dir" == '~'* ]]; then
53
+ home_dir="${HOME:-}${home_dir#\~}"
54
+ fi
55
+ echo "${home_dir}/.openclaw"
56
+ }
57
+
58
+ # ============================================
59
+ # JSON 版本解析
60
+ # ============================================
61
+
62
+ # parse_json_version: 从 JSON 文件解析 version 字段
63
+ # 优先使用 jq,回退 node,最后 grep+sed
64
+ # 用法: VERSION=$(parse_json_version plugin/openclaw.plugin.json)
65
+ parse_json_version() {
66
+ local json_file="$1"
67
+ if [ ! -f "$json_file" ]; then
68
+ return 1
69
+ fi
70
+ if command -v jq &>/dev/null; then
71
+ jq -r '.version' "$json_file"
72
+ elif command -v node &>/dev/null; then
73
+ node -e "const f=require('fs');console.log(JSON.parse(f.readFileSync(process.argv[1],'utf8')).version)" "$json_file"
74
+ else
75
+ # 最后兜底:使用 grep + sed
76
+ grep '"version"' "$json_file" | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/'
77
+ fi
78
+ }
79
+
80
+ # ============================================
81
+ # 系统检测
82
+ # ============================================
83
+
84
+ # detect_os: 检测操作系统
85
+ # 返回: linux, macos, windows, unknown
86
+ detect_os() {
87
+ case "$(uname -s 2>/dev/null)" in
88
+ Linux*) echo "linux" ;;
89
+ Darwin*) echo "macos" ;;
90
+ MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
91
+ *) echo "unknown" ;;
92
+ esac
93
+ }
94
+
95
+ # validate_os: 验证操作系统是否支持
96
+ # 用法: validate_os "$(detect_os)"
97
+ validate_os() {
98
+ local os="$1"
99
+ case "$os" in
100
+ linux|macos|windows)
101
+ log_info "系统: $os"
102
+ ;;
103
+ *)
104
+ log_error "不支持的系统: $(uname -s 2>/dev/null || echo 'unknown')"
105
+ log_info "支持的系统: Linux, macOS, Windows (Git Bash)"
106
+ return 1
107
+ ;;
108
+ esac
109
+ }
110
+
111
+ # ============================================
112
+ # 下载辅助
113
+ # ============================================
114
+
115
+ # download_file: 下载文件(自动选择 curl 或 wget)
116
+ # 用法: download_file "https://example.com/file.tgz" "/tmp/file.tgz"
117
+ download_file() {
118
+ local url="$1"
119
+ local output="$2"
120
+
121
+ log_info " 下载: $url"
122
+
123
+ if command -v curl &>/dev/null; then
124
+ if ! curl -fSL --progress-bar -o "$output" "$url"; then
125
+ return 1
126
+ fi
127
+ elif command -v wget &>/dev/null; then
128
+ if ! wget -q --show-progress -O "$output" "$url"; then
129
+ return 1
130
+ fi
131
+ else
132
+ log_error "需要 curl 或 wget 来下载文件"
133
+ return 1
134
+ fi
135
+
136
+ return 0
137
+ }