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,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
任务链路读取器 - 解析 Agent 间的任务派发关系
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChainNodeStatus(str, Enum):
|
|
14
|
+
PENDING = "pending"
|
|
15
|
+
RUNNING = "running"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
ERROR = "error"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChainStatus(str, Enum):
|
|
21
|
+
RUNNING = "running"
|
|
22
|
+
COMPLETED = "completed"
|
|
23
|
+
ERROR = "error"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
from data.config_reader import get_openclaw_root
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_agents_config() -> Dict[str, Any]:
|
|
30
|
+
"""获取 agents 配置"""
|
|
31
|
+
config_file = get_openclaw_root() / "openclaw.json"
|
|
32
|
+
if not config_file.exists():
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(config_file, 'r', encoding='utf-8') as f:
|
|
37
|
+
return json.load(f)
|
|
38
|
+
except:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_agent_info(agent_id: str) -> Dict[str, Any]:
|
|
43
|
+
"""获取单个 agent 的信息"""
|
|
44
|
+
config = _get_agents_config()
|
|
45
|
+
agents = config.get('agents', {}).get('list', [])
|
|
46
|
+
for a in agents:
|
|
47
|
+
if a.get('id') == agent_id:
|
|
48
|
+
return a
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_session_key(session_key: str) -> Dict[str, str]:
|
|
53
|
+
"""解析 session key,提取 agent_id 等信息"""
|
|
54
|
+
# 格式: agent:agent-id:subagent:uuid 或 agent:agent-id:main
|
|
55
|
+
parts = session_key.split(':')
|
|
56
|
+
result = {'type': parts[0] if len(parts) > 0 else 'unknown'}
|
|
57
|
+
|
|
58
|
+
if len(parts) >= 2:
|
|
59
|
+
result['agent_id'] = parts[1]
|
|
60
|
+
if len(parts) >= 3:
|
|
61
|
+
result['session_type'] = parts[2] # main 或 subagent
|
|
62
|
+
if len(parts) >= 4:
|
|
63
|
+
result['uuid'] = parts[3]
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _load_runs() -> Dict[str, Any]:
|
|
69
|
+
"""加载 runs.json"""
|
|
70
|
+
runs_file = get_openclaw_root() / "subagents" / "runs.json"
|
|
71
|
+
if not runs_file.exists():
|
|
72
|
+
return {"version": 2, "runs": {}}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(runs_file, 'r', encoding='utf-8') as f:
|
|
76
|
+
return json.load(f)
|
|
77
|
+
except:
|
|
78
|
+
return {"version": 2, "runs": {}}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_workflow_state(project_id: str) -> Dict[str, Any]:
|
|
82
|
+
"""获取项目的 workflow 状态"""
|
|
83
|
+
# 尝试多个可能的项目路径
|
|
84
|
+
possible_paths = [
|
|
85
|
+
get_openclaw_root() / f"workspace-{project_id}" / ".staging" / "workflow_state.json",
|
|
86
|
+
Path.home() / "vrt-projects" / "projects" / project_id / ".staging" / "workflow_state.json",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for path in possible_paths:
|
|
90
|
+
if path.exists():
|
|
91
|
+
try:
|
|
92
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
93
|
+
return json.load(f)
|
|
94
|
+
except:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_task_chains(limit: int = 20) -> List[Dict[str, Any]]:
|
|
101
|
+
"""
|
|
102
|
+
构建任务链路列表
|
|
103
|
+
|
|
104
|
+
通过解析 runs.json 中的派发关系,构建完整的任务执行链路
|
|
105
|
+
"""
|
|
106
|
+
runs_data = _load_runs()
|
|
107
|
+
runs = runs_data.get('runs', {})
|
|
108
|
+
|
|
109
|
+
if not runs:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
# 按 requesterSessionKey 分组,构建链路
|
|
113
|
+
chains_map: Dict[str, Dict[str, Any]] = {}
|
|
114
|
+
|
|
115
|
+
for run_id, run_info in runs.items():
|
|
116
|
+
if not isinstance(run_info, dict):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
requester_key = run_info.get('requesterSessionKey', '')
|
|
120
|
+
child_key = run_info.get('childSessionKey', '')
|
|
121
|
+
|
|
122
|
+
if not requester_key or not child_key:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# 解析 requester 和 child
|
|
126
|
+
requester = _parse_session_key(requester_key)
|
|
127
|
+
child = _parse_session_key(child_key)
|
|
128
|
+
|
|
129
|
+
requester_id = requester.get('agent_id', 'main')
|
|
130
|
+
child_id = child.get('agent_id', 'unknown')
|
|
131
|
+
|
|
132
|
+
# 使用 requester 的 session 作为链路 ID(简化处理)
|
|
133
|
+
chain_id = requester_key.split(':subagent:')[0] if ':subagent:' in requester_key else requester_key
|
|
134
|
+
|
|
135
|
+
if chain_id not in chains_map:
|
|
136
|
+
chains_map[chain_id] = {
|
|
137
|
+
'chainId': chain_id,
|
|
138
|
+
'rootTask': '',
|
|
139
|
+
'startedAt': None,
|
|
140
|
+
'status': ChainStatus.RUNNING.value,
|
|
141
|
+
'nodes': {},
|
|
142
|
+
'edges': [],
|
|
143
|
+
'projectId': None
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
chain = chains_map[chain_id]
|
|
147
|
+
|
|
148
|
+
# 添加 requester 节点(如果不存在)
|
|
149
|
+
if requester_id not in chain['nodes']:
|
|
150
|
+
agent_info = _get_agent_info(requester_id)
|
|
151
|
+
chain['nodes'][requester_id] = {
|
|
152
|
+
'agentId': requester_id,
|
|
153
|
+
'agentName': agent_info.get('name', requester_id),
|
|
154
|
+
'role': requester_id.split('-')[0] if '-' in requester_id else requester_id,
|
|
155
|
+
'status': ChainNodeStatus.COMPLETED.value,
|
|
156
|
+
'startedAt': None,
|
|
157
|
+
'endedAt': None,
|
|
158
|
+
'duration': None,
|
|
159
|
+
'task': None,
|
|
160
|
+
'runId': None,
|
|
161
|
+
'input': None,
|
|
162
|
+
'output': None,
|
|
163
|
+
'artifacts': [],
|
|
164
|
+
'toolCallCount': 0,
|
|
165
|
+
'tokenUsage': {'input': 0, 'output': 0}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 添加子节点
|
|
169
|
+
agent_info = _get_agent_info(child_id)
|
|
170
|
+
started_at = run_info.get('startedAt')
|
|
171
|
+
ended_at = run_info.get('endedAt')
|
|
172
|
+
|
|
173
|
+
node_status = ChainNodeStatus.PENDING.value
|
|
174
|
+
if started_at and ended_at:
|
|
175
|
+
node_status = ChainNodeStatus.COMPLETED.value if run_info.get('outcome') == 'ok' else ChainNodeStatus.ERROR.value
|
|
176
|
+
elif started_at:
|
|
177
|
+
node_status = ChainNodeStatus.RUNNING.value
|
|
178
|
+
|
|
179
|
+
chain['nodes'][child_id] = {
|
|
180
|
+
'agentId': child_id,
|
|
181
|
+
'agentName': agent_info.get('name', child_id),
|
|
182
|
+
'role': child_id.split('-')[0] if '-' in child_id else child_id,
|
|
183
|
+
'status': node_status,
|
|
184
|
+
'startedAt': started_at,
|
|
185
|
+
'endedAt': ended_at,
|
|
186
|
+
'duration': (ended_at - started_at) if started_at and ended_at else None,
|
|
187
|
+
'task': run_info.get('task', ''),
|
|
188
|
+
'runId': run_id,
|
|
189
|
+
'input': None,
|
|
190
|
+
'output': None,
|
|
191
|
+
'artifacts': [],
|
|
192
|
+
'toolCallCount': 0,
|
|
193
|
+
'tokenUsage': {'input': 0, 'output': 0}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# 添加边
|
|
197
|
+
edge = {'from': requester_id, 'to': child_id}
|
|
198
|
+
if edge not in chain['edges']:
|
|
199
|
+
chain['edges'].append(edge)
|
|
200
|
+
|
|
201
|
+
# 更新链路开始时间
|
|
202
|
+
if started_at:
|
|
203
|
+
if chain['startedAt'] is None or started_at < chain['startedAt']:
|
|
204
|
+
chain['startedAt'] = started_at
|
|
205
|
+
|
|
206
|
+
# 设置根任务(使用第一个子节点的任务)
|
|
207
|
+
if not chain['rootTask'] and run_info.get('task'):
|
|
208
|
+
chain['rootTask'] = run_info.get('task')
|
|
209
|
+
|
|
210
|
+
# 保存 archiveAtMs(用于超时倒计时)
|
|
211
|
+
if run_info.get('archiveAtMs') and not chain.get('archiveAtMs'):
|
|
212
|
+
chain['archiveAtMs'] = run_info.get('archiveAtMs')
|
|
213
|
+
|
|
214
|
+
# 转换节点为列表并计算统计信息
|
|
215
|
+
chains = []
|
|
216
|
+
for chain_id, chain in chains_map.items():
|
|
217
|
+
nodes_list = list(chain['nodes'].values())
|
|
218
|
+
|
|
219
|
+
# 计算进度
|
|
220
|
+
completed = sum(1 for n in nodes_list if n['status'] == ChainNodeStatus.COMPLETED.value)
|
|
221
|
+
running = sum(1 for n in nodes_list if n['status'] == ChainNodeStatus.RUNNING.value)
|
|
222
|
+
total = len(nodes_list)
|
|
223
|
+
|
|
224
|
+
progress = completed / total if total > 0 else 0
|
|
225
|
+
|
|
226
|
+
# 计算总耗时
|
|
227
|
+
total_duration = sum(n['duration'] or 0 for n in nodes_list)
|
|
228
|
+
|
|
229
|
+
# 确定链路状态
|
|
230
|
+
if any(n['status'] == ChainNodeStatus.ERROR.value for n in nodes_list):
|
|
231
|
+
chain_status = ChainStatus.ERROR.value
|
|
232
|
+
elif running > 0:
|
|
233
|
+
chain_status = ChainStatus.RUNNING.value
|
|
234
|
+
else:
|
|
235
|
+
chain_status = ChainStatus.COMPLETED.value
|
|
236
|
+
|
|
237
|
+
chains.append({
|
|
238
|
+
'chainId': chain_id,
|
|
239
|
+
'projectId': chain.get('projectId'),
|
|
240
|
+
'rootTask': chain.get('rootTask', '未知任务'),
|
|
241
|
+
'startedAt': chain.get('startedAt'),
|
|
242
|
+
'archiveAtMs': chain.get('archiveAtMs'),
|
|
243
|
+
'status': chain_status,
|
|
244
|
+
'nodes': nodes_list,
|
|
245
|
+
'edges': chain['edges'],
|
|
246
|
+
'progress': progress,
|
|
247
|
+
'completedNodes': completed,
|
|
248
|
+
'totalNodes': total,
|
|
249
|
+
'totalDuration': total_duration
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
# 按开始时间排序
|
|
253
|
+
chains.sort(key=lambda x: x.get('startedAt') or 0, reverse=True)
|
|
254
|
+
|
|
255
|
+
return chains[:limit]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_task_chain(chain_id: str) -> Optional[Dict[str, Any]]:
|
|
259
|
+
"""获取单个任务链的详情"""
|
|
260
|
+
chains = build_task_chains(limit=100)
|
|
261
|
+
for chain in chains:
|
|
262
|
+
if chain['chainId'] == chain_id:
|
|
263
|
+
return chain
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_active_chain() -> Optional[Dict[str, Any]]:
|
|
268
|
+
"""获取当前活跃的任务链(正在执行的)"""
|
|
269
|
+
chains = build_task_chains(limit=50)
|
|
270
|
+
for chain in chains:
|
|
271
|
+
if chain['status'] == ChainStatus.RUNNING.value:
|
|
272
|
+
return chain
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_chains_summary() -> Dict[str, Any]:
|
|
277
|
+
"""获取任务链摘要统计"""
|
|
278
|
+
chains = build_task_chains(limit=100)
|
|
279
|
+
|
|
280
|
+
running = sum(1 for c in chains if c['status'] == ChainStatus.RUNNING.value)
|
|
281
|
+
completed = sum(1 for c in chains if c['status'] == ChainStatus.COMPLETED.value)
|
|
282
|
+
error = sum(1 for c in chains if c['status'] == ChainStatus.ERROR.value)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
'total': len(chains),
|
|
286
|
+
'running': running,
|
|
287
|
+
'completed': completed,
|
|
288
|
+
'error': error,
|
|
289
|
+
'chains': [
|
|
290
|
+
{
|
|
291
|
+
'chainId': c['chainId'],
|
|
292
|
+
'rootTask': c['rootTask'][:50] + '...' if len(c['rootTask']) > 50 else c['rootTask'],
|
|
293
|
+
'status': c['status'],
|
|
294
|
+
'progress': c['progress'],
|
|
295
|
+
'startedAt': c['startedAt']
|
|
296
|
+
}
|
|
297
|
+
for c in chains[:10]
|
|
298
|
+
]
|
|
299
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置读取器 - 读取 openclaw.json
|
|
3
|
+
支持 OPENCLAW_STATE_DIR、OPENCLAW_HOME 环境变量(跨平台,含 Windows)
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_openclaw_root() -> Path:
|
|
12
|
+
"""OpenClaw 根目录,统一解析优先级(兼容 Windows):
|
|
13
|
+
1. OPENCLAW_STATE_DIR(最高优先级)
|
|
14
|
+
2. OPENCLAW_HOME(兼容两种写法:直接指向 .openclaw,或指向用户 Home 需拼接 .openclaw)
|
|
15
|
+
3. ~/.openclaw(兜底)
|
|
16
|
+
"""
|
|
17
|
+
# 1. OPENCLAW_STATE_DIR
|
|
18
|
+
env_state = os.environ.get("OPENCLAW_STATE_DIR")
|
|
19
|
+
if env_state:
|
|
20
|
+
return Path(env_state).expanduser().resolve()
|
|
21
|
+
|
|
22
|
+
# 2. OPENCLAW_HOME
|
|
23
|
+
env_home = os.environ.get("OPENCLAW_HOME")
|
|
24
|
+
if env_home:
|
|
25
|
+
p = Path(env_home).expanduser().resolve()
|
|
26
|
+
# 兼容两种写法:直接指向 .openclaw 目录,或指向用户 Home
|
|
27
|
+
if p.name in (".openclaw", "openclaw"):
|
|
28
|
+
return p
|
|
29
|
+
return p / ".openclaw"
|
|
30
|
+
|
|
31
|
+
# 3. 兜底
|
|
32
|
+
return Path.home() / ".openclaw"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_config() -> Dict[str, Any]:
|
|
36
|
+
"""加载 openclaw.json"""
|
|
37
|
+
config_path = get_openclaw_root() / "openclaw.json"
|
|
38
|
+
if not config_path.exists():
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
42
|
+
return json.load(f)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_agents_list() -> List[Dict[str, Any]]:
|
|
46
|
+
"""获取 Agent 列表"""
|
|
47
|
+
config = load_config()
|
|
48
|
+
agents = config.get('agents')
|
|
49
|
+
if agents is None or not isinstance(agents, dict):
|
|
50
|
+
return []
|
|
51
|
+
return agents.get('list', [])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_main_agent_id() -> str:
|
|
55
|
+
"""获取主 Agent ID(配置中 id 为 main 的,或列表第一个)"""
|
|
56
|
+
agents = get_agents_list()
|
|
57
|
+
for a in agents:
|
|
58
|
+
if a.get('id') == 'main':
|
|
59
|
+
return 'main'
|
|
60
|
+
return agents[0].get('id', 'main') if agents else 'main'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_workspace_paths() -> List[Path]:
|
|
64
|
+
"""获取所有 Agent 的 workspace 路径(用于 model-failures.log 等)"""
|
|
65
|
+
agents = get_agents_list()
|
|
66
|
+
paths = []
|
|
67
|
+
seen = set()
|
|
68
|
+
for a in agents:
|
|
69
|
+
ws = a.get('workspace')
|
|
70
|
+
if ws and ws not in seen:
|
|
71
|
+
p = Path(ws).expanduser() if isinstance(ws, str) else Path(ws)
|
|
72
|
+
if p.exists():
|
|
73
|
+
paths.append(p)
|
|
74
|
+
seen.add(ws)
|
|
75
|
+
if not paths:
|
|
76
|
+
paths.append(get_openclaw_root() / "workspace-main")
|
|
77
|
+
return paths
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_agent_config(agent_id: str) -> Dict[str, Any]:
|
|
81
|
+
"""获取单个 Agent 配置"""
|
|
82
|
+
agents = get_agents_list()
|
|
83
|
+
for agent in agents:
|
|
84
|
+
if agent.get('id') == agent_id:
|
|
85
|
+
return agent
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_default_config() -> Dict[str, Any]:
|
|
90
|
+
"""获取默认配置"""
|
|
91
|
+
config = load_config()
|
|
92
|
+
agents = config.get('agents')
|
|
93
|
+
if agents is None or not isinstance(agents, dict):
|
|
94
|
+
return {}
|
|
95
|
+
return agents.get('defaults', {})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_agent_models(agent_id: str) -> Dict[str, Any]:
|
|
99
|
+
"""获取 Agent 的模型配置(primary + fallbacks)"""
|
|
100
|
+
agent = get_agent_config(agent_id)
|
|
101
|
+
model_cfg = agent.get('model') or {}
|
|
102
|
+
defaults = get_default_config()
|
|
103
|
+
default_model = defaults.get('model', {})
|
|
104
|
+
primary = model_cfg.get('primary') or default_model.get('primary') or ''
|
|
105
|
+
fallbacks = model_cfg.get('fallbacks') or default_model.get('fallbacks') or []
|
|
106
|
+
return {'primary': primary, 'fallbacks': fallbacks}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_models_configured_by_agents() -> List[str]:
|
|
110
|
+
"""
|
|
111
|
+
从配置中收集「各 Agent 实际配置使用」的模型 ID(仅 primary + fallbacks)。
|
|
112
|
+
用于协作流程右侧模型面板:只显示有 Agent 配置的模型,不含白名单中未使用的。
|
|
113
|
+
"""
|
|
114
|
+
agents = get_agents_list()
|
|
115
|
+
model_ids = set()
|
|
116
|
+
defaults = get_default_config()
|
|
117
|
+
default_model = defaults.get('model', {})
|
|
118
|
+
if default_model.get('primary'):
|
|
119
|
+
model_ids.add(default_model['primary'])
|
|
120
|
+
for fb in default_model.get('fallbacks') or []:
|
|
121
|
+
model_ids.add(fb)
|
|
122
|
+
for agent in agents:
|
|
123
|
+
cfg = get_agent_models(agent.get('id', ''))
|
|
124
|
+
if cfg.get('primary'):
|
|
125
|
+
model_ids.add(cfg['primary'])
|
|
126
|
+
for fb in cfg.get('fallbacks', []):
|
|
127
|
+
model_ids.add(fb)
|
|
128
|
+
return sorted(model_ids)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_all_models_from_agents() -> List[str]:
|
|
132
|
+
"""
|
|
133
|
+
从配置中收集模型 ID(provider/model 格式),用于无 models.providers 时的下拉选项。
|
|
134
|
+
来源(合并去重):
|
|
135
|
+
1. 各 Agent 实际配置(primary + fallbacks)
|
|
136
|
+
2. agents.defaults.models(白名单 key,确保配置过的能选)
|
|
137
|
+
"""
|
|
138
|
+
model_ids = set(get_models_configured_by_agents())
|
|
139
|
+
defaults = get_default_config()
|
|
140
|
+
models_cfg = defaults.get('models', {}) or {}
|
|
141
|
+
if isinstance(models_cfg, dict):
|
|
142
|
+
for mid in models_cfg.keys():
|
|
143
|
+
if mid and isinstance(mid, str):
|
|
144
|
+
model_ids.add(mid)
|
|
145
|
+
return sorted(model_ids)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_model_display_name(model_id: str) -> str:
|
|
149
|
+
"""获取模型显示名。展示策略:使用 id 不用别名(与 OpenClaw 白名单逻辑一致)"""
|
|
150
|
+
if not model_id:
|
|
151
|
+
return ''
|
|
152
|
+
parts = model_id.split('/')
|
|
153
|
+
return parts[-1] if len(parts) > 1 else model_id
|