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.
- package/.github/workflows/release.yml +56 -0
- package/README.md +302 -0
- package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
- package/docs/RELEASE-LATEST.md +189 -0
- package/docs/RELEASE-MODEL-CONFIG.md +95 -0
- package/docs/release-guide.md +259 -0
- package/docs/release-operations-manual.md +167 -0
- package/docs/specs/tr3-install-system.md +580 -0
- package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
- package/frontend/index.html +12 -0
- package/frontend/package-lock.json +1240 -0
- package/frontend/package.json +19 -0
- package/frontend/src/App.vue +331 -0
- package/frontend/src/components/AgentCard.vue +796 -0
- package/frontend/src/components/AgentConfigPanel.vue +539 -0
- package/frontend/src/components/AgentDetailPanel.vue +738 -0
- package/frontend/src/components/ErrorAnalysisView.vue +546 -0
- package/frontend/src/components/ErrorCenterPanel.vue +844 -0
- package/frontend/src/components/PerformanceMonitor.vue +515 -0
- package/frontend/src/components/SettingsPanel.vue +236 -0
- package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
- package/frontend/src/components/chain/ChainEdge.vue +85 -0
- package/frontend/src/components/chain/ChainNode.vue +166 -0
- package/frontend/src/components/chain/TaskChainView.vue +425 -0
- package/frontend/src/components/chain/index.ts +3 -0
- package/frontend/src/components/chain/types.ts +70 -0
- package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
- package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
- package/frontend/src/components/performance/PerformancePanel.vue +119 -0
- package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
- package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
- package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
- package/frontend/src/components/timeline/TimelineRound.vue +135 -0
- package/frontend/src/components/timeline/TimelineStep.vue +691 -0
- package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
- package/frontend/src/components/timeline/TimelineView.vue +540 -0
- package/frontend/src/components/timeline/index.ts +5 -0
- package/frontend/src/components/timeline/types.ts +120 -0
- package/frontend/src/composables/index.ts +7 -0
- package/frontend/src/composables/useDebounce.ts +48 -0
- package/frontend/src/composables/useRealtime.ts +52 -0
- package/frontend/src/composables/useState.ts +52 -0
- package/frontend/src/composables/useThrottle.ts +46 -0
- package/frontend/src/composables/useVirtualScroll.ts +106 -0
- package/frontend/src/main.ts +4 -0
- package/frontend/src/managers/EventDispatcher.ts +127 -0
- package/frontend/src/managers/RealtimeDataManager.ts +293 -0
- package/frontend/src/managers/StateManager.ts +128 -0
- package/frontend/src/managers/index.ts +5 -0
- package/frontend/src/types/collaboration.ts +135 -0
- package/frontend/src/types/index.ts +20 -0
- package/frontend/src/types/performance.ts +105 -0
- package/frontend/src/types/task.ts +38 -0
- package/frontend/vite.config.ts +18 -0
- package/package.json +22 -0
- package/plugin/README.md +99 -0
- package/plugin/config.json.example +1 -0
- package/plugin/index.js +250 -0
- package/plugin/openclaw.plugin.json +17 -0
- package/plugin/package.json +21 -0
- package/scripts/build-plugin.js +67 -0
- package/scripts/bundle.sh +62 -0
- package/scripts/install-plugin.sh +162 -0
- package/scripts/install-python-deps.js +346 -0
- package/scripts/install-python-deps.sh +226 -0
- package/scripts/install.js +512 -0
- package/scripts/install.sh +367 -0
- package/scripts/lib/common.js +490 -0
- package/scripts/lib/common.sh +137 -0
- package/scripts/release-pack.sh +110 -0
- package/scripts/start.js +50 -0
- package/scripts/test_available_models.py +284 -0
- package/scripts/test_websocket_ping.py +44 -0
- package/src/backend/agents.py +73 -0
- package/src/backend/api/__init__.py +1 -0
- package/src/backend/api/agent_config_api.py +90 -0
- package/src/backend/api/agents.py +73 -0
- package/src/backend/api/agents_config.py +75 -0
- package/src/backend/api/chains.py +126 -0
- package/src/backend/api/collaboration.py +902 -0
- package/src/backend/api/debug_paths.py +39 -0
- package/src/backend/api/error_analysis.py +146 -0
- package/src/backend/api/errors.py +281 -0
- package/src/backend/api/performance.py +784 -0
- package/src/backend/api/subagents.py +770 -0
- package/src/backend/api/timeline.py +144 -0
- package/src/backend/api/websocket.py +251 -0
- package/src/backend/collaboration.py +405 -0
- package/src/backend/data/__init__.py +1 -0
- package/src/backend/data/agent_config_manager.py +270 -0
- package/src/backend/data/chain_reader.py +299 -0
- package/src/backend/data/config_reader.py +153 -0
- package/src/backend/data/error_analyzer.py +430 -0
- package/src/backend/data/session_reader.py +445 -0
- package/src/backend/data/subagent_reader.py +244 -0
- package/src/backend/data/task_history.py +118 -0
- package/src/backend/data/timeline_reader.py +981 -0
- package/src/backend/errors.py +63 -0
- package/src/backend/main.py +89 -0
- package/src/backend/mechanism_reader.py +131 -0
- package/src/backend/mechanisms.py +32 -0
- package/src/backend/performance.py +474 -0
- package/src/backend/requirements.txt +5 -0
- package/src/backend/session_reader.py +238 -0
- package/src/backend/status/__init__.py +1 -0
- package/src/backend/status/error_detector.py +122 -0
- package/src/backend/status/status_calculator.py +301 -0
- package/src/backend/status_calculator.py +121 -0
- package/src/backend/subagent_reader.py +229 -0
- package/src/backend/watchers/__init__.py +4 -0
- package/src/backend/watchers/file_watcher.py +159 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 任务状态类型定义
|
|
2
|
+
|
|
3
|
+
export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
4
|
+
|
|
5
|
+
export interface Task {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
task?: string // 完整任务内容(用于显示全量)
|
|
9
|
+
status: TaskStatus
|
|
10
|
+
progress: number // 0-100
|
|
11
|
+
startTime?: number
|
|
12
|
+
endTime?: number
|
|
13
|
+
agentId?: string
|
|
14
|
+
agentName?: string
|
|
15
|
+
agentWorkspace?: string // Agent 工作区路径
|
|
16
|
+
taskPath?: string
|
|
17
|
+
subtasks?: Task[]
|
|
18
|
+
error?: string
|
|
19
|
+
output?: string // 任务成功时 Agent 的输出内容
|
|
20
|
+
generatedFiles?: string[] // 本次任务生成/修改的文件路径
|
|
21
|
+
metadata?: Record<string, unknown>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TaskStatusSummary {
|
|
25
|
+
tasks: Task[]
|
|
26
|
+
total: number
|
|
27
|
+
completed: number
|
|
28
|
+
failed: number
|
|
29
|
+
running: number
|
|
30
|
+
pending: number
|
|
31
|
+
cancelled: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TaskFilters {
|
|
35
|
+
status?: TaskStatus[]
|
|
36
|
+
search?: string
|
|
37
|
+
agentId?: string
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [vue()],
|
|
6
|
+
server: {
|
|
7
|
+
proxy: {
|
|
8
|
+
'/api': {
|
|
9
|
+
target: 'http://localhost:8000',
|
|
10
|
+
changeOrigin: true
|
|
11
|
+
},
|
|
12
|
+
'/ws': {
|
|
13
|
+
target: 'ws://localhost:8000',
|
|
14
|
+
ws: true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-agent-dashboard",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "多 Agent 可视化看板 - 状态、任务、API、工作流、协作流程",
|
|
5
|
+
"bin": {
|
|
6
|
+
"openclaw-agent-dashboard": "scripts/install.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"pack": "node scripts/build-plugin.js",
|
|
10
|
+
"install-plugin": "node scripts/install.js",
|
|
11
|
+
"deploy": "npm run pack && npm run install-plugin",
|
|
12
|
+
"upgrade": "git pull && npm run deploy",
|
|
13
|
+
"bundle": "bash scripts/bundle.sh",
|
|
14
|
+
"start": "node scripts/start.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["openclaw", "agent", "dashboard", "visualization"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/Umarchen/openclaw-agent-dashboard.git"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/plugin/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# OpenClaw Agent Dashboard 插件
|
|
2
|
+
|
|
3
|
+
多 Agent 可视化看板 - 作为 OpenClaw 插件安装后,随 OpenClaw 启动自动运行。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
克隆仓库后,在项目根目录执行:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run deploy
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
该命令会自动完成:
|
|
14
|
+
1. 检查前置条件(Node.js、Python 3、OpenClaw)
|
|
15
|
+
2. 构建前端
|
|
16
|
+
3. 打包并安装插件到 `~/.openclaw/extensions/`
|
|
17
|
+
4. 自动安装 Python 依赖(fastapi、uvicorn 等)
|
|
18
|
+
|
|
19
|
+
**前置要求**:
|
|
20
|
+
- Node.js(构建前端)
|
|
21
|
+
- Python 3.10+
|
|
22
|
+
- OpenClaw(`npm install -g openclaw`)
|
|
23
|
+
|
|
24
|
+
安装完成后,执行任意 `openclaw` 命令即可自动启动 Dashboard。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 命令说明
|
|
29
|
+
|
|
30
|
+
| 命令 | 说明 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `npm run deploy` | 打包 + 安装到 OpenClaw(首次安装或升级) |
|
|
33
|
+
| `npm run upgrade` | 拉取最新代码 + 部署(推荐用于升级) |
|
|
34
|
+
| `npm run pack` | 仅打包插件,不安装(开发调试用) |
|
|
35
|
+
| `npm run bundle` | 生成可分发的压缩包(给同事用) |
|
|
36
|
+
|
|
37
|
+
### 升级插件
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd openclaw-agent-dashboard
|
|
41
|
+
npm run upgrade
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
输出示例:
|
|
45
|
+
```
|
|
46
|
+
=== OpenClaw Agent Dashboard 插件升级 ===
|
|
47
|
+
|
|
48
|
+
1.0.0 → 1.1.0
|
|
49
|
+
|
|
50
|
+
✓ 前置条件检查通过
|
|
51
|
+
>>> 1/4 构建前端...
|
|
52
|
+
>>> 2/4 打包插件...
|
|
53
|
+
>>> 3/4 移除旧版本...
|
|
54
|
+
>>> 4/4 安装新版本...
|
|
55
|
+
>>> 检查 Python 依赖...
|
|
56
|
+
✓ Python 依赖已就绪
|
|
57
|
+
|
|
58
|
+
=== 升级完成 (1.0.0 → 1.1.0) ===
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 使用
|
|
64
|
+
|
|
65
|
+
插件加载后(执行任意 `openclaw` 命令时)会自动启动 Dashboard 服务。
|
|
66
|
+
|
|
67
|
+
**访问地址**:http://localhost:38271(或你配置的端口)
|
|
68
|
+
|
|
69
|
+
### 端口配置(便于移植,无需改 openclaw.json)
|
|
70
|
+
|
|
71
|
+
优先级从高到低:
|
|
72
|
+
|
|
73
|
+
1. **环境变量**:`DASHBOARD_PORT=38271`
|
|
74
|
+
2. **独立配置文件**:`~/.openclaw-agent-dashboard/config.json`(可与 `OPENCLAW_AGENT_DASHBOARD_DATA` 环境变量配合)
|
|
75
|
+
```json
|
|
76
|
+
{ "port": 38271 }
|
|
77
|
+
```
|
|
78
|
+
3. **openclaw.json**:`plugins.entries.openclaw-agent-dashboard.config.port`
|
|
79
|
+
4. **默认**:38271
|
|
80
|
+
|
|
81
|
+
端口被占用时会自动尝试 38272、38273...
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 手动安装(故障恢复)
|
|
86
|
+
|
|
87
|
+
若 `npm run deploy` 失败,可分步执行:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 1. 打包
|
|
91
|
+
npm run pack
|
|
92
|
+
|
|
93
|
+
# 2. 安装插件
|
|
94
|
+
openclaw plugins install ./plugin
|
|
95
|
+
|
|
96
|
+
# 3. 安装 Python 依赖(通常不需要,脚本会自动完成;Debian/Ubuntu 请用 venv)
|
|
97
|
+
cd ~/.openclaw/extensions/openclaw-agent-dashboard/dashboard
|
|
98
|
+
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
|
99
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"port":38271}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Agent Dashboard - 插件入口
|
|
3
|
+
* 仅在 Gateway 进程内自动启动 FastAPI 后端(检测 OPENCLAW_GATEWAY_PORT)
|
|
4
|
+
*
|
|
5
|
+
* 启动条件:OPENCLAW_GATEWAY_PORT 已设置(即 Gateway 进程)且 autoStart !== false
|
|
6
|
+
*
|
|
7
|
+
* 端口配置优先级(高到低):
|
|
8
|
+
* 1. 环境变量 DASHBOARD_PORT
|
|
9
|
+
* 2. ~/.openclaw-agent-dashboard/config.json(或 OPENCLAW_AGENT_DASHBOARD_DATA/config.json)
|
|
10
|
+
* 3. openclaw.json 中 plugins.entries.openclaw-agent-dashboard.config.port
|
|
11
|
+
* 4. 默认 38271
|
|
12
|
+
*/
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const net = require('net');
|
|
17
|
+
const { spawn, execFileSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
let dashboardProcess = null;
|
|
20
|
+
|
|
21
|
+
function getOpenClawHome() {
|
|
22
|
+
return process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Dashboard 数据目录,与 Python 端一致:本工程数据统一放此,不写入 ~/.openclaw */
|
|
26
|
+
function getDashboardDataDir() {
|
|
27
|
+
return process.env.OPENCLAW_AGENT_DASHBOARD_DATA || path.join(os.homedir(), '.openclaw-agent-dashboard');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDashboardDir() {
|
|
31
|
+
const pluginDir = __dirname;
|
|
32
|
+
return path.join(pluginDir, 'dashboard');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 兼容旧路径:若存在 ~/.openclaw/dashboard 或 ~/.openclaw-dashboard 下的 config.json 则迁移到新目录一次 */
|
|
36
|
+
function migrateLegacyConfigIfNeeded() {
|
|
37
|
+
const openclawHome = getOpenClawHome();
|
|
38
|
+
const newDir = getDashboardDataDir();
|
|
39
|
+
const newPath = path.join(newDir, 'config.json');
|
|
40
|
+
if (fs.existsSync(newPath)) return;
|
|
41
|
+
const legacyPaths = [
|
|
42
|
+
path.join(openclawHome, 'dashboard', 'config.json'),
|
|
43
|
+
path.join(os.homedir(), '.openclaw-dashboard', 'config.json')
|
|
44
|
+
];
|
|
45
|
+
for (const legacyPath of legacyPaths) {
|
|
46
|
+
if (!fs.existsSync(legacyPath)) continue;
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir, { recursive: true });
|
|
49
|
+
fs.copyFileSync(legacyPath, newPath);
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 加载配置,优先级:env > 用户 config.json(新路径 > 旧路径)> api.pluginConfig > 默认
|
|
57
|
+
*/
|
|
58
|
+
function loadConfig(apiPluginConfig = {}) {
|
|
59
|
+
const openclawHome = getOpenClawHome();
|
|
60
|
+
let config = { port: 38271, autoStart: true }; // 使用罕见端口避免冲突
|
|
61
|
+
|
|
62
|
+
if (apiPluginConfig && typeof apiPluginConfig.port === 'number') {
|
|
63
|
+
config.port = apiPluginConfig.port;
|
|
64
|
+
}
|
|
65
|
+
if (apiPluginConfig && typeof apiPluginConfig.autoStart === 'boolean') {
|
|
66
|
+
config.autoStart = apiPluginConfig.autoStart;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const projectConfigPath = path.join(getDashboardDir(), 'config.json');
|
|
70
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const data = JSON.parse(fs.readFileSync(projectConfigPath, 'utf8'));
|
|
73
|
+
if (typeof data.port === 'number') config.port = data.port;
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
migrateLegacyConfigIfNeeded();
|
|
78
|
+
const userConfigPath = path.join(getDashboardDataDir(), 'config.json');
|
|
79
|
+
const legacyUserConfigPath = path.join(openclawHome, 'dashboard', 'config.json');
|
|
80
|
+
const prevUserConfigPath = path.join(os.homedir(), '.openclaw-dashboard', 'config.json');
|
|
81
|
+
const pathToRead = fs.existsSync(userConfigPath) ? userConfigPath
|
|
82
|
+
: (fs.existsSync(prevUserConfigPath) ? prevUserConfigPath : legacyUserConfigPath);
|
|
83
|
+
if (pathToRead && fs.existsSync(pathToRead)) {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(fs.readFileSync(pathToRead, 'utf8'));
|
|
86
|
+
if (typeof data.port === 'number') config.port = data.port;
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const envPort = process.env.DASHBOARD_PORT;
|
|
91
|
+
if (envPort) {
|
|
92
|
+
const p = parseInt(envPort, 10);
|
|
93
|
+
if (!isNaN(p) && p > 0) config.port = p;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return config;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 检测端口是否可用 */
|
|
100
|
+
function isPortAvailable(port) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const server = net.createServer();
|
|
103
|
+
server.once('error', () => resolve(false));
|
|
104
|
+
server.once('listening', () => {
|
|
105
|
+
server.close(() => resolve(true));
|
|
106
|
+
});
|
|
107
|
+
server.listen(port, '0.0.0.0');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 检测端口是否已被占用(有进程在监听) */
|
|
112
|
+
function isPortInUse(port) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const server = net.createServer();
|
|
115
|
+
server.once('error', () => resolve(true));
|
|
116
|
+
server.once('listening', () => {
|
|
117
|
+
server.close(() => resolve(false));
|
|
118
|
+
});
|
|
119
|
+
server.listen(port, '127.0.0.1');
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 找到可用端口,从 basePort 开始尝试 */
|
|
124
|
+
async function findAvailablePort(basePort, maxAttempts = 10) {
|
|
125
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
126
|
+
const port = basePort + i;
|
|
127
|
+
if (await isPortAvailable(port)) return port;
|
|
128
|
+
}
|
|
129
|
+
return basePort;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function startDashboard(config = {}) {
|
|
133
|
+
// 仅在 Gateway 进程内自动启动(OPENCLAW_GATEWAY_PORT 由 Gateway 设置)
|
|
134
|
+
// CLI 命令(status、health 等)加载插件时不会设置此变量,故不启动
|
|
135
|
+
if (!process.env.OPENCLAW_GATEWAY_PORT?.trim()) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (dashboardProcess) {
|
|
140
|
+
console.log('[OpenClaw-Dashboard] 服务已在运行');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const dashboardDir = getDashboardDir();
|
|
145
|
+
const openclawHome = getOpenClawHome();
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(dashboardDir)) {
|
|
148
|
+
console.warn('[OpenClaw-Dashboard] dashboard 目录不存在,请先执行 npm run deploy 安装插件');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const basePort = config.port ?? 38271;
|
|
153
|
+
const isExplicitPort = basePort !== 38271; // 用户显式配置了端口(非默认 38271)
|
|
154
|
+
const autoStart = config.autoStart !== false; // 默认 true,可配置为 false 禁用自动启动
|
|
155
|
+
|
|
156
|
+
if (!autoStart) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const portPromise = isExplicitPort
|
|
161
|
+
? Promise.resolve(basePort)
|
|
162
|
+
: findAvailablePort(basePort);
|
|
163
|
+
|
|
164
|
+
portPromise.then(async (port) => {
|
|
165
|
+
if (dashboardProcess) return;
|
|
166
|
+
|
|
167
|
+
// 若端口已被占用,认为 Dashboard 已在其他进程运行,跳过启动,避免重复实例
|
|
168
|
+
if (await isPortInUse(port)) {
|
|
169
|
+
console.log(`[OpenClaw-Dashboard] 端口 ${port} 已被占用,Dashboard 可能已在运行,跳过启动`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const env = {
|
|
174
|
+
...process.env,
|
|
175
|
+
OPENCLAW_HOME: openclawHome,
|
|
176
|
+
DASHBOARD_PORT: String(port),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 优先使用插件 venv 的 Python(安装时 venv 优先,避免 PEP 668)
|
|
180
|
+
// 若 venv 存在但不完整(如缺 python3-venv 导致 ensurepip 失败、无 uvicorn),回退到 python3
|
|
181
|
+
const venvPythonUnix = path.join(dashboardDir, '.venv', 'bin', 'python');
|
|
182
|
+
const venvPythonWin = path.join(dashboardDir, '.venv', 'Scripts', 'python.exe');
|
|
183
|
+
let pythonCmd = process.env.PYTHON_CMD;
|
|
184
|
+
if (!pythonCmd) {
|
|
185
|
+
const venvPython = fs.existsSync(venvPythonUnix) ? venvPythonUnix : (fs.existsSync(venvPythonWin) ? venvPythonWin : null);
|
|
186
|
+
if (venvPython) {
|
|
187
|
+
try {
|
|
188
|
+
execFileSync(venvPython, ['-c', 'import uvicorn'], { stdio: 'ignore', timeout: 3000 });
|
|
189
|
+
pythonCmd = venvPython;
|
|
190
|
+
} catch (_) {
|
|
191
|
+
pythonCmd = 'python3'; // venv 不完整,回退到系统 python3
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
pythonCmd = 'python3';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const args = ['-m', 'uvicorn', 'main:app', '--host', '0.0.0.0', '--port', String(port)];
|
|
198
|
+
|
|
199
|
+
if (!isExplicitPort && port !== basePort) {
|
|
200
|
+
console.log(`[OpenClaw-Dashboard] 端口 ${basePort} 被占用,使用 ${port}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(`[OpenClaw-Dashboard] 插件服务已启动`);
|
|
203
|
+
console.log(`[OpenClaw-Dashboard] 访问地址: http://localhost:${port}`);
|
|
204
|
+
|
|
205
|
+
dashboardProcess = spawn(pythonCmd, args, {
|
|
206
|
+
env,
|
|
207
|
+
cwd: dashboardDir,
|
|
208
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
dashboardProcess.on('error', (err) => {
|
|
212
|
+
console.error('[OpenClaw-Dashboard] 启动失败:', err.message);
|
|
213
|
+
dashboardProcess = null;
|
|
214
|
+
});
|
|
215
|
+
dashboardProcess.on('exit', (code, signal) => {
|
|
216
|
+
dashboardProcess = null;
|
|
217
|
+
if (code !== 0 && code !== null) {
|
|
218
|
+
console.log(`[OpenClaw-Dashboard] 进程退出 code=${code} signal=${signal}`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function stopDashboard() {
|
|
225
|
+
if (dashboardProcess) {
|
|
226
|
+
dashboardProcess.kill('SIGTERM');
|
|
227
|
+
dashboardProcess = null;
|
|
228
|
+
console.log('[OpenClaw-Dashboard] 服务已停止');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* OpenClaw 插件入口
|
|
234
|
+
* @param {object} api - OpenClaw 插件 API,pluginConfig 来自 openclaw.json 的 config 字段
|
|
235
|
+
*/
|
|
236
|
+
function DashboardPlugin(api) {
|
|
237
|
+
console.log('[OpenClaw-Dashboard] 插件已加载');
|
|
238
|
+
|
|
239
|
+
const pluginConfig = (api && api.pluginConfig) || {};
|
|
240
|
+
const config = loadConfig(pluginConfig);
|
|
241
|
+
startDashboard(config);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
stop() {
|
|
245
|
+
stopDashboard();
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = DashboardPlugin;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-agent-dashboard",
|
|
3
|
+
"name": "OpenClaw Agent Dashboard",
|
|
4
|
+
"description": "多 Agent 可视化看板 - 状态、任务、API、工作流、协作流程",
|
|
5
|
+
"version": "1.0.4",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"port": {
|
|
11
|
+
"type": "number",
|
|
12
|
+
"default": 38271,
|
|
13
|
+
"description": "Dashboard 服务端口"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-agent-dashboard",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "多 Agent 可视化看板 - OpenClaw 插件",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"extensions": ["./index.js"]
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["openclaw", "dashboard", "agent", "monitoring"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"openclaw.plugin.json",
|
|
13
|
+
"index.js",
|
|
14
|
+
"dashboard",
|
|
15
|
+
"frontend-dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"pack": "node ../scripts/build-plugin.js",
|
|
19
|
+
"prepublishOnly": "npm run pack"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 插件打包脚本
|
|
4
|
+
* 1. 构建前端
|
|
5
|
+
* 2. 复制 backend 到 plugin/dashboard
|
|
6
|
+
* 3. 复制 frontend/dist 到 plugin/frontend-dist
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const ROOT = path.join(__dirname, '..');
|
|
13
|
+
const PLUGIN_DIR = path.join(ROOT, 'plugin');
|
|
14
|
+
const BACKEND_SRC = path.join(ROOT, 'src', 'backend');
|
|
15
|
+
const FRONTEND_DIR = path.join(ROOT, 'frontend');
|
|
16
|
+
const DASHBOARD_DEST = path.join(PLUGIN_DIR, 'dashboard');
|
|
17
|
+
const FRONTEND_DEST = path.join(PLUGIN_DIR, 'frontend-dist');
|
|
18
|
+
|
|
19
|
+
function rmrf(dir) {
|
|
20
|
+
if (fs.existsSync(dir)) {
|
|
21
|
+
fs.rmSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyDir(src, dest, exclude = []) {
|
|
26
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
27
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (exclude.includes(entry.name)) continue;
|
|
30
|
+
if (entry.name.endsWith('.pyc') || entry.name.endsWith('.log')) continue;
|
|
31
|
+
const srcPath = path.join(src, entry.name);
|
|
32
|
+
const destPath = path.join(dest, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
copyDir(srcPath, destPath, exclude);
|
|
35
|
+
} else {
|
|
36
|
+
fs.copyFileSync(srcPath, destPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log('[pack] 1. 构建前端...');
|
|
42
|
+
// 检查是否需要安装依赖:无 node_modules 或缺少 .bin(如 Windows 下 .bin 未生成)时都执行 install
|
|
43
|
+
const nodeModulesPath = path.join(FRONTEND_DIR, 'node_modules');
|
|
44
|
+
const binVite = path.join(nodeModulesPath, '.bin', 'vite');
|
|
45
|
+
const binViteCmd = path.join(nodeModulesPath, '.bin', 'vite.cmd'); // Windows
|
|
46
|
+
const needInstall = !fs.existsSync(nodeModulesPath) ||
|
|
47
|
+
!(fs.existsSync(binVite) || fs.existsSync(binViteCmd));
|
|
48
|
+
if (needInstall) {
|
|
49
|
+
console.log('[pack] 安装前端依赖...');
|
|
50
|
+
execSync('npm install --no-audit', { cwd: FRONTEND_DIR, stdio: 'inherit', env: process.env });
|
|
51
|
+
}
|
|
52
|
+
execSync('npm run build', { cwd: FRONTEND_DIR, stdio: 'inherit', env: process.env });
|
|
53
|
+
|
|
54
|
+
console.log('[pack] 2. 复制 backend -> plugin/dashboard');
|
|
55
|
+
rmrf(DASHBOARD_DEST);
|
|
56
|
+
copyDir(BACKEND_SRC, DASHBOARD_DEST, ['__pycache__', '.pytest_cache']);
|
|
57
|
+
|
|
58
|
+
console.log('[pack] 3. 复制 frontend/dist -> plugin/frontend-dist');
|
|
59
|
+
rmrf(FRONTEND_DEST);
|
|
60
|
+
const frontendDist = path.join(FRONTEND_DIR, 'dist');
|
|
61
|
+
if (fs.existsSync(frontendDist)) {
|
|
62
|
+
copyDir(frontendDist, FRONTEND_DEST);
|
|
63
|
+
} else {
|
|
64
|
+
console.warn('[pack] frontend/dist 不存在,跳过');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('[pack] 完成');
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# 打包项目为可分发的压缩包
|
|
4
|
+
# 用法: npm run bundle
|
|
5
|
+
#
|
|
6
|
+
set -e
|
|
7
|
+
cd "$(dirname "$0")/.."
|
|
8
|
+
ROOT=$(pwd)
|
|
9
|
+
|
|
10
|
+
VERSION=$(grep '"version"' "$ROOT/plugin/openclaw.plugin.json" | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
|
11
|
+
OUTPUT="openclaw-agent-dashboard-v$VERSION.tar.gz"
|
|
12
|
+
|
|
13
|
+
echo "=== 打包分发文件 ==="
|
|
14
|
+
echo ""
|
|
15
|
+
echo " 版本: $VERSION"
|
|
16
|
+
echo " 输出: $OUTPUT"
|
|
17
|
+
echo ""
|
|
18
|
+
|
|
19
|
+
# 临时目录
|
|
20
|
+
TMPDIR=$(mktemp -d)
|
|
21
|
+
DISTDIR="$TMPDIR/openclaw-agent-dashboard"
|
|
22
|
+
|
|
23
|
+
mkdir -p "$DISTDIR"
|
|
24
|
+
|
|
25
|
+
# 复制需要的文件
|
|
26
|
+
echo ">>> 复制文件..."
|
|
27
|
+
cp -r "$ROOT/frontend" "$DISTDIR/"
|
|
28
|
+
cp -r "$ROOT/src" "$DISTDIR/"
|
|
29
|
+
cp -r "$ROOT/plugin" "$DISTDIR/"
|
|
30
|
+
cp -r "$ROOT/scripts" "$DISTDIR/"
|
|
31
|
+
cp "$ROOT/package.json" "$DISTDIR/"
|
|
32
|
+
cp "$ROOT/README.md" "$DISTDIR/"
|
|
33
|
+
|
|
34
|
+
# 清理不需要的文件
|
|
35
|
+
echo ">>> 清理 node_modules..."
|
|
36
|
+
rm -rf "$DISTDIR/frontend/node_modules"
|
|
37
|
+
rm -rf "$DISTDIR/frontend/dist"
|
|
38
|
+
rm -rf "$DISTDIR/plugin/frontend-dist"
|
|
39
|
+
rm -rf "$DISTDIR/plugin/dashboard"
|
|
40
|
+
find "$DISTDIR" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
41
|
+
find "$DISTDIR" -name ".pytest_cache" -type d -exec rm -rf {} + 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# 打包
|
|
44
|
+
echo ">>> 压缩..."
|
|
45
|
+
cd "$TMPDIR"
|
|
46
|
+
tar -czf "$ROOT/$OUTPUT" openclaw-agent-dashboard
|
|
47
|
+
|
|
48
|
+
# 清理临时目录
|
|
49
|
+
rm -rf "$TMPDIR"
|
|
50
|
+
|
|
51
|
+
# 显示结果
|
|
52
|
+
echo ""
|
|
53
|
+
echo "=== 打包完成 ==="
|
|
54
|
+
echo ""
|
|
55
|
+
echo " 文件: $OUTPUT"
|
|
56
|
+
echo " 大小: $(du -h "$ROOT/$OUTPUT" | cut -f1)"
|
|
57
|
+
echo ""
|
|
58
|
+
echo "同事使用方式:"
|
|
59
|
+
echo " 1. 解压: tar -xzf $OUTPUT"
|
|
60
|
+
echo " 2. 进入: cd openclaw-agent-dashboard"
|
|
61
|
+
echo " 3. 安装: npm run deploy"
|
|
62
|
+
echo ""
|