nexus-agentd 0.2.3 → 0.2.6
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/LICENSE +22 -22
- package/README.md +223 -202
- package/dist/a2a/runtime.d.ts +3 -0
- package/dist/a2a/runtime.js +119 -12
- package/dist/acp/runtime.d.ts +5 -1
- package/dist/acp/runtime.js +180 -36
- package/dist/auth.js +29 -8
- package/dist/completion-contract.js +1 -1
- package/dist/control-plane.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23 -5
- package/dist/run-store.d.ts +5 -0
- package/dist/run-store.js +10 -1
- package/dist/server.js +58 -8
- package/dist/session-contract.d.ts +1 -0
- package/dist/session.d.ts +14 -2
- package/dist/session.js +153 -74
- package/dist/types.d.ts +14 -0
- package/dist/webui/app/sources.d.ts +1 -0
- package/dist/webui/app/sources.js +17 -0
- package/dist/webui/icons.d.ts +17 -0
- package/dist/webui/icons.js +23 -6
- package/dist/webui/index.d.ts +1 -0
- package/dist/webui/index.js +32 -12
- package/dist/webui/markup.js +67 -65
- package/dist/webui/styles.js +746 -256
- package/dist/workspace.d.ts +30 -0
- package/dist/workspace.js +67 -2
- package/nexus-agentd.example.json +40 -40
- package/package.json +54 -52
- package/dist/webui/app.d.ts +0 -1
- package/dist/webui/app.js +0 -914
package/dist/webui/app.js
DELETED
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
export const app = String.raw `
|
|
2
|
-
(() => {
|
|
3
|
-
'use strict'
|
|
4
|
-
|
|
5
|
-
const state = {
|
|
6
|
-
page: 'overview',
|
|
7
|
-
config: { workspaceRoots: [], driverKinds: [], agents: [] },
|
|
8
|
-
readiness: [],
|
|
9
|
-
apiKeys: [],
|
|
10
|
-
runs: [],
|
|
11
|
-
runTotal: 0,
|
|
12
|
-
sessions: 0,
|
|
13
|
-
search: '',
|
|
14
|
-
protocol: 'all',
|
|
15
|
-
status: 'all',
|
|
16
|
-
runSearch: '',
|
|
17
|
-
runAgent: 'all',
|
|
18
|
-
runStatus: 'all',
|
|
19
|
-
authenticated: false
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const pageMeta = {
|
|
23
|
-
overview: ['总览', '查看已配置智能体运行时的实时状态。'],
|
|
24
|
-
runs: ['运行记录', '追踪当前任务进度,并查看已完成或失败的历史任务。'],
|
|
25
|
-
agents: ['智能体', '配置本地 ACP 进程和远程 A2A 智能体。'],
|
|
26
|
-
workspaces: ['工作区', '管理 ACP 智能体允许访问的路径。'],
|
|
27
|
-
keys: ['API 密钥', '管理 Agent Nexus 数据接口的客户端凭据。'],
|
|
28
|
-
settings: ['运行设置', '调整会话生命周期、任务超时和后台清理周期。']
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const actionIcons = {
|
|
32
|
-
copy: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="10" height="10" rx="2"></rect><path d="M15 9V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"></path></svg>',
|
|
33
|
-
disable: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"></circle><path d="m6.3 6.3 11.4 11.4"></path></svg>',
|
|
34
|
-
enable: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"></circle><path d="m8.5 12 2.3 2.3 4.8-5"></path></svg>',
|
|
35
|
-
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20h8"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z"></path></svg>',
|
|
36
|
-
more: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle></svg>',
|
|
37
|
-
reveal: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"></path><circle cx="12" cy="12" r="2.5"></circle></svg>',
|
|
38
|
-
scope: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 5 6v5c0 4.7 2.9 8 7 10 4.1-2 7-5.3 7-10V6Z"></path><path d="M9.5 12 11 13.5l3.5-4"></path></svg>',
|
|
39
|
-
regenerate: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 7v5h-5"></path><path d="M19 12a7 7 0 1 0-2 5"></path></svg>',
|
|
40
|
-
delete: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"></path><path d="M9 7V4h6v3"></path><path d="m6 7 1 13h10l1-13"></path><path d="M10 11v5M14 11v5"></path></svg>'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const byId = (id) => document.getElementById(id)
|
|
44
|
-
const setupScreen = byId('setup-screen')
|
|
45
|
-
const loginScreen = byId('login-screen')
|
|
46
|
-
const appRoot = byId('app')
|
|
47
|
-
const content = byId('page-content')
|
|
48
|
-
const actions = byId('page-actions')
|
|
49
|
-
const drawer = byId('drawer')
|
|
50
|
-
const drawerBackdrop = byId('drawer-backdrop')
|
|
51
|
-
const drawerForm = byId('drawer-form')
|
|
52
|
-
const keyActionMenu = document.createElement('div')
|
|
53
|
-
keyActionMenu.className = 'key-action-menu hidden'
|
|
54
|
-
keyActionMenu.setAttribute('role', 'menu')
|
|
55
|
-
document.body.appendChild(keyActionMenu)
|
|
56
|
-
let drawerSubmit = null
|
|
57
|
-
|
|
58
|
-
function escapeHtml(value) {
|
|
59
|
-
return String(value == null ? '' : value)
|
|
60
|
-
.replace(/&/g, '&')
|
|
61
|
-
.replace(/</g, '<')
|
|
62
|
-
.replace(/>/g, '>')
|
|
63
|
-
.replace(/"/g, '"')
|
|
64
|
-
.replace(/'/g, ''')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function selected(value, expected) {
|
|
68
|
-
return value === expected ? ' selected' : ''
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function checked(value) {
|
|
72
|
-
return value ? ' checked' : ''
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function api(path, options) {
|
|
76
|
-
const settings = Object.assign({ credentials: 'same-origin' }, options || {})
|
|
77
|
-
settings.headers = Object.assign({}, settings.headers || {})
|
|
78
|
-
if (settings.body && typeof settings.body !== 'string') {
|
|
79
|
-
settings.headers['Content-Type'] = 'application/json'
|
|
80
|
-
settings.body = JSON.stringify(settings.body)
|
|
81
|
-
}
|
|
82
|
-
const response = await fetch(path, settings)
|
|
83
|
-
let value = null
|
|
84
|
-
try { value = await response.json() } catch {}
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
if (response.status === 401 && path.indexOf('/v1/admin/') === 0) showLogin()
|
|
87
|
-
const message = value && value.error ? value.error : '请求失败(HTTP ' + response.status + ')'
|
|
88
|
-
const error = new Error(localizeError(message))
|
|
89
|
-
error.status = response.status
|
|
90
|
-
throw error
|
|
91
|
-
}
|
|
92
|
-
return value
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function localizeError(message) {
|
|
96
|
-
const messages = {
|
|
97
|
-
'Invalid Console Password': '控制台密码不正确',
|
|
98
|
-
'Console setup is required': '请先完成控制台初始化',
|
|
99
|
-
'Console setup is already complete': '控制台已完成初始化',
|
|
100
|
-
'Console Password confirmation does not match': '两次输入的控制台密码不一致',
|
|
101
|
-
'Console Password must contain at least 12 characters': '控制台密码至少需要 12 个字符',
|
|
102
|
-
'Console Password must not exceed 1024 bytes': '控制台密码不能超过 1024 字节',
|
|
103
|
-
'Console Password must not contain NUL bytes': '控制台密码包含不支持的字符',
|
|
104
|
-
'Current Console Password is incorrect': '当前控制台密码不正确',
|
|
105
|
-
'Gateway setup is required': '请先完成网关初始化',
|
|
106
|
-
'Admin session is required': '管理员会话已失效,请重新登录',
|
|
107
|
-
'Bearer API Key is required': '缺少 Bearer API 密钥',
|
|
108
|
-
'Invalid API Key': 'API 密钥无效',
|
|
109
|
-
'API Key is not authorized for this agent': '该 API 密钥无权访问此智能体',
|
|
110
|
-
'API Key already exists': 'API 密钥已存在',
|
|
111
|
-
'API Key not found': '未找到 API 密钥',
|
|
112
|
-
'API Key name is required': '请输入 API 密钥名称',
|
|
113
|
-
'API Key name is too long': 'API 密钥名称过长',
|
|
114
|
-
'Custom API Key must contain at least 16 characters': '自定义 API 密钥至少需要 16 个字符',
|
|
115
|
-
'Custom API Key must not exceed 512 bytes': '自定义 API 密钥不能超过 512 字节',
|
|
116
|
-
'Custom API Key contains an unsupported value': '自定义 API 密钥包含不支持的内容',
|
|
117
|
-
'workspaceRoots must contain at least one path': '至少需要保留一个工作区根目录',
|
|
118
|
-
'A2A authentication value is required': '请输入 A2A 认证凭据',
|
|
119
|
-
'A2A authentication value is invalid': 'A2A 认证凭据无效',
|
|
120
|
-
'A2A Agent Card URL is required': '请输入 Agent Card URL',
|
|
121
|
-
'A2A Agent Card URL is invalid': 'Agent Card URL 无效',
|
|
122
|
-
'A2A Agent Card URL must use http(s) without credentials or a fragment': 'Agent Card URL 必须使用 HTTP(S),且不能包含凭据或片段',
|
|
123
|
-
'preferredTransport must be auto, jsonrpc, or http-json': '首选传输必须是自动、JSON-RPC 或 HTTP+JSON',
|
|
124
|
-
'A valid A2A header name is required': '请输入有效的 A2A 请求头名称',
|
|
125
|
-
'Too many authentication attempts': '认证失败次数过多,请稍后再试',
|
|
126
|
-
'Request body is too large': '请求内容过大',
|
|
127
|
-
'Request requires application/json': '请求必须使用 application/json',
|
|
128
|
-
'Same-origin request is required': '此操作必须从当前控制台页面发起',
|
|
129
|
-
'Request origin is invalid': '请求来源无效',
|
|
130
|
-
'Cross-origin admin request is not allowed': '不允许跨域管理请求',
|
|
131
|
-
'Control plane is unavailable': '管理控制面当前不可用',
|
|
132
|
-
'Agent Nexus run not found': '未找到运行记录',
|
|
133
|
-
'Internal server error': '服务器内部错误'
|
|
134
|
-
}
|
|
135
|
-
if (messages[message]) return messages[message]
|
|
136
|
-
const missingAgent = /^Configured agent not found: (.+)$/.exec(message)
|
|
137
|
-
if (missingAgent) return '未找到已配置的智能体:' + missingAgent[1]
|
|
138
|
-
const timeout = /^timeout must be between (\d+) and (\d+)$/.exec(message)
|
|
139
|
-
if (timeout) return '超时时间必须介于 ' + Math.round(Number(timeout[1]) / 1000) + ' 和 ' + Math.round(Number(timeout[2]) / 1000) + ' 秒之间'
|
|
140
|
-
const runtimeRange = /^(sessionTtlMs|promptTimeoutMs|cleanupIntervalMs) must be between (\d+) and (\d+)$/.exec(message)
|
|
141
|
-
if (runtimeRange) {
|
|
142
|
-
const units = {
|
|
143
|
-
sessionTtlMs: ['Session 空闲有效期', '小时', 3_600_000],
|
|
144
|
-
promptTimeoutMs: ['ACP 任务超时', '分钟', 60_000],
|
|
145
|
-
cleanupIntervalMs: ['清理任务周期', '秒', 1000]
|
|
146
|
-
}
|
|
147
|
-
const [label, unit, divisor] = units[runtimeRange[1]]
|
|
148
|
-
return label + '必须介于 ' + Math.round(Number(runtimeRange[2]) / divisor) + ' 和 ' + Math.round(Number(runtimeRange[3]) / divisor) + ' ' + unit + '之间'
|
|
149
|
-
}
|
|
150
|
-
return message
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function showOnly(element) {
|
|
154
|
-
setupScreen.classList.add('hidden')
|
|
155
|
-
loginScreen.classList.add('hidden')
|
|
156
|
-
appRoot.classList.add('hidden')
|
|
157
|
-
element.classList.remove('hidden')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function showLogin() {
|
|
161
|
-
state.authenticated = false
|
|
162
|
-
showOnly(loginScreen)
|
|
163
|
-
byId('login-form').reset()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function boot() {
|
|
167
|
-
applyTheme(localStorage.getItem('agent-nexus-theme') || 'system')
|
|
168
|
-
try {
|
|
169
|
-
const bootstrap = await api('/v1/bootstrap/status')
|
|
170
|
-
if (bootstrap.adminSetupRequired) {
|
|
171
|
-
showOnly(setupScreen)
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
const auth = await api('/v1/admin/auth/status')
|
|
175
|
-
if (!auth.authenticated) {
|
|
176
|
-
showLogin()
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
await enterApp()
|
|
180
|
-
} catch (error) {
|
|
181
|
-
showLogin()
|
|
182
|
-
toast(error.message, true)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function enterApp() {
|
|
187
|
-
state.authenticated = true
|
|
188
|
-
showOnly(appRoot)
|
|
189
|
-
await loadAll()
|
|
190
|
-
render()
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function loadAll(force) {
|
|
194
|
-
const suffix = force ? '?refresh=1' : ''
|
|
195
|
-
const values = await Promise.all([
|
|
196
|
-
api('/v1/admin/config'),
|
|
197
|
-
api('/v1/admin/api-keys'),
|
|
198
|
-
api('/v1/admin/overview' + suffix),
|
|
199
|
-
api('/v1/admin/runs?limit=200')
|
|
200
|
-
])
|
|
201
|
-
state.config = values[0]
|
|
202
|
-
state.apiKeys = values[1].apiKeys || []
|
|
203
|
-
state.readiness = values[2].agents || []
|
|
204
|
-
state.sessions = values[2].sessions || 0
|
|
205
|
-
state.runs = values[3].runs || []
|
|
206
|
-
state.runTotal = values[3].total || 0
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function refreshRuns(showNotice) {
|
|
210
|
-
try {
|
|
211
|
-
const value = await api('/v1/admin/runs?limit=200')
|
|
212
|
-
state.runs = value.runs || []
|
|
213
|
-
state.runTotal = value.total || 0
|
|
214
|
-
if (state.page === 'runs') renderRuns()
|
|
215
|
-
if (showNotice) toast('运行记录已刷新')
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (showNotice) toast(error.message, true)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function refreshReadiness(showNotice) {
|
|
222
|
-
try {
|
|
223
|
-
const overview = await api('/v1/admin/overview?refresh=1')
|
|
224
|
-
state.readiness = overview.agents || []
|
|
225
|
-
state.sessions = overview.sessions || 0
|
|
226
|
-
render()
|
|
227
|
-
if (showNotice) toast('运行状态已刷新')
|
|
228
|
-
} catch (error) {
|
|
229
|
-
if (showNotice) toast(error.message, true)
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function render() {
|
|
234
|
-
const meta = pageMeta[state.page]
|
|
235
|
-
byId('page-title').textContent = meta[0]
|
|
236
|
-
byId('page-description').textContent = meta[1]
|
|
237
|
-
document.querySelectorAll('.nav-item').forEach((item) => {
|
|
238
|
-
item.classList.toggle('active', item.dataset.page === state.page)
|
|
239
|
-
})
|
|
240
|
-
if (state.page === 'overview') renderOverview()
|
|
241
|
-
if (state.page === 'runs') renderRuns()
|
|
242
|
-
if (state.page === 'agents') renderAgents()
|
|
243
|
-
if (state.page === 'workspaces') renderWorkspaces()
|
|
244
|
-
if (state.page === 'keys') renderApiKeys()
|
|
245
|
-
if (state.page === 'settings') renderSettings()
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function renderSettings() {
|
|
249
|
-
const sessionTtlHours = Math.round((state.config.sessionTtlMs || 24 * 60 * 60 * 1000) / 3_600_000)
|
|
250
|
-
const promptTimeoutMinutes = Math.round((state.config.promptTimeoutMs || 30 * 60 * 1000) / 60_000)
|
|
251
|
-
const cleanupIntervalSeconds = Math.round((state.config.cleanupIntervalMs || 60_000) / 1000)
|
|
252
|
-
actions.innerHTML = '<span class="muted">修改后立即生效,新任务按新参数执行</span>'
|
|
253
|
-
content.innerHTML =
|
|
254
|
-
'<div class="settings-layout"><section class="panel settings-form"><div class="panel-header"><div><h2>运行生命周期</h2><p class="settings-subtitle">这些参数由网关统一管理,重启后仍会保留。</p></div></div>' +
|
|
255
|
-
'<div class="settings-fields">' +
|
|
256
|
-
'<label>Session 空闲有效期(小时)<input id="session-ttl-hours" type="number" min="1" max="720" step="1" value="' + escapeHtml(sessionTtlHours) + '"><small class="field-help">Session 在没有新消息、授权或输入交互后,超过此时间会被释放。默认 24 小时。</small></label>' +
|
|
257
|
-
'<label>单次 ACP 任务超时(分钟)<input id="prompt-timeout-minutes" type="number" min="1" max="1440" step="1" value="' + escapeHtml(promptTimeoutMinutes) + '"><small class="field-help">单次 ACP prompt 的最长运行时间。默认 30 分钟;超时后任务会记录为失败并释放运行资源。</small></label>' +
|
|
258
|
-
'<label>清理任务周期(秒)<input id="cleanup-interval-seconds" type="number" min="5" max="3600" step="1" value="' + escapeHtml(cleanupIntervalSeconds) + '"><small class="field-help">网关扫描空闲 Session、过期运行记录和临时输入文件的间隔。默认每 60 秒执行一次。</small></label>' +
|
|
259
|
-
'</div><div class="settings-divider"></div><div class="settings-note"><strong>A2A 请求超时</strong><span>在“智能体 → 编辑 → A2A → 请求超时”中单独配置。默认 60 秒,最大 30 分钟,不受 ACP 全局任务超时字段覆盖。</span></div><div class="settings-actions"><button id="save-runtime-settings" class="button primary">保存运行设置</button></div></section></div>'
|
|
260
|
-
byId('save-runtime-settings').onclick = async () => {
|
|
261
|
-
const values = {
|
|
262
|
-
sessionTtlMs: Number(byId('session-ttl-hours').value) * 3_600_000,
|
|
263
|
-
promptTimeoutMs: Number(byId('prompt-timeout-minutes').value) * 60_000,
|
|
264
|
-
cleanupIntervalMs: Number(byId('cleanup-interval-seconds').value) * 1000
|
|
265
|
-
}
|
|
266
|
-
if (!Object.values(values).every((value) => Number.isInteger(value) && value > 0)) {
|
|
267
|
-
toast('请输入有效的整数运行参数。', true)
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
await runAction(async () => {
|
|
271
|
-
state.config = await api('/v1/admin/config/runtime', { method: 'PUT', body: values })
|
|
272
|
-
render()
|
|
273
|
-
toast('运行设置已保存并立即生效')
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function renderRuns() {
|
|
279
|
-
const activeStates = new Set(['running', 'input_required', 'permission_required'])
|
|
280
|
-
const filtered = state.runs.filter((run) => {
|
|
281
|
-
const haystack = (run.task + ' ' + run.agentName + ' ' + run.id).toLowerCase()
|
|
282
|
-
return haystack.indexOf(state.runSearch.toLowerCase()) >= 0 &&
|
|
283
|
-
(state.runAgent === 'all' || run.agentId === state.runAgent) &&
|
|
284
|
-
(state.runStatus === 'all' || run.state === state.runStatus)
|
|
285
|
-
})
|
|
286
|
-
const active = filtered.filter((run) => activeStates.has(run.state))
|
|
287
|
-
const history = filtered.filter((run) => !activeStates.has(run.state))
|
|
288
|
-
const completed = state.runs.filter((run) => run.state === 'completed').length
|
|
289
|
-
const failed = state.runs.filter((run) => run.state === 'failed').length
|
|
290
|
-
const agentOptions = state.config.agents.map((agent) => '<option value="' + escapeHtml(agent.id) + '"' + selected(state.runAgent, agent.id) + '>' + escapeHtml(agent.name) + '</option>').join('')
|
|
291
|
-
actions.innerHTML = '<span class="muted">每 5 秒自动刷新</span><button id="refresh-runs" class="button">立即刷新</button>'
|
|
292
|
-
content.innerHTML =
|
|
293
|
-
'<div class="run-summary">' +
|
|
294
|
-
runSummary('当前运行', state.runs.filter((run) => activeStates.has(run.state)).length) +
|
|
295
|
-
runSummary('已完成', completed) +
|
|
296
|
-
runSummary('失败', failed) +
|
|
297
|
-
runSummary('保留记录', state.runTotal) +
|
|
298
|
-
'</div>' +
|
|
299
|
-
'<div class="toolbar"><input id="run-search" type="search" placeholder="搜索任务、智能体或运行 ID" value="' + escapeHtml(state.runSearch) + '">' +
|
|
300
|
-
'<select id="run-agent-filter"><option value="all">全部智能体</option>' + agentOptions + '</select>' +
|
|
301
|
-
'<select id="run-status-filter"><option value="all">全部状态</option><option value="running"' + selected(state.runStatus, 'running') + '>运行中</option><option value="input_required"' + selected(state.runStatus, 'input_required') + '>等待输入</option><option value="permission_required"' + selected(state.runStatus, 'permission_required') + '>等待授权</option><option value="completed"' + selected(state.runStatus, 'completed') + '>已完成</option><option value="failed"' + selected(state.runStatus, 'failed') + '>失败</option><option value="canceled"' + selected(state.runStatus, 'canceled') + '>已取消</option></select></div>' +
|
|
302
|
-
'<section class="run-section"><div class="run-section-title"><h2>当前运行</h2><span>' + active.length + ' 项</span></div>' +
|
|
303
|
-
(active.length ? '<div class="run-live-grid">' + active.map(runCard).join('') + '</div>' : '<div class="panel"><div class="empty">当前没有正在执行或等待处理的任务。</div></div>') +
|
|
304
|
-
'</section>' +
|
|
305
|
-
'<section class="run-section"><div class="run-section-title"><h2>历史记录</h2><span>' + history.length + ' 项</span></div>' +
|
|
306
|
-
'<div class="table-wrap"><table class="run-table"><thead><tr><th>智能体</th><th>任务</th><th>结果</th><th>状态</th><th>开始时间</th><th>耗时</th></tr></thead><tbody>' +
|
|
307
|
-
(history.length ? history.map(runRow).join('') : '<tr><td colspan="6" class="empty">没有符合条件的历史记录。</td></tr>') +
|
|
308
|
-
'</tbody></table></div></section>'
|
|
309
|
-
byId('refresh-runs').onclick = () => refreshRuns(true)
|
|
310
|
-
byId('run-search').oninput = (event) => { state.runSearch = event.target.value; renderRuns() }
|
|
311
|
-
byId('run-agent-filter').onchange = (event) => { state.runAgent = event.target.value; renderRuns() }
|
|
312
|
-
byId('run-status-filter').onchange = (event) => { state.runStatus = event.target.value; renderRuns() }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function runSummary(label, value) {
|
|
316
|
-
return '<div class="run-summary-item"><span>' + escapeHtml(label) + '</span><strong>' + escapeHtml(value) + '</strong></div>'
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function runCard(run) {
|
|
320
|
-
const waiting = run.state === 'input_required' || run.state === 'permission_required'
|
|
321
|
-
return '<article class="run-card' + (waiting ? ' waiting' : '') + '" data-run-detail="' + escapeHtml(run.id) + '" tabindex="0"><div class="run-card-head"><div class="run-card-agent"><span class="badge">' + escapeHtml(run.protocol.toUpperCase()) + '</span><strong>' + escapeHtml(run.agentName) + '</strong></div>' + runStatus(run.state) + '</div><div class="run-task">' + escapeHtml(run.task) + '</div><div class="run-progress"><strong>' + escapeHtml(run.progress && run.progress.phase || '运行中') + '</strong><small>' + escapeHtml(run.progress && run.progress.message || '等待最新进度') + '</small></div><div class="run-meta"><span>' + escapeHtml(shortId(run.id)) + '</span><span>' + escapeHtml(formatDuration(run)) + '</span></div></article>'
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function runRow(run) {
|
|
325
|
-
return '<tr data-run-detail="' + escapeHtml(run.id) + '"><td><div class="agent-name"><strong>' + escapeHtml(run.agentName) + '</strong><small>' + escapeHtml(run.protocol.toUpperCase()) + '</small></div></td><td class="run-task-cell"><strong>' + escapeHtml(run.task) + '</strong><small class="run-id">' + escapeHtml(shortId(run.id)) + '</small></td><td class="run-task-cell"><strong>' + escapeHtml(run.resultSummary || run.error || '—') + '</strong><small>' + escapeHtml(run.progress && run.progress.phase || '') + '</small></td><td>' + runStatus(run.state) + '</td><td>' + escapeHtml(formatDate(run.startedAt)) + '</td><td>' + escapeHtml(formatDuration(run)) + '</td></tr>'
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function runStatus(value) {
|
|
329
|
-
const labels = { created: '已创建', running: '运行中', input_required: '等待输入', permission_required: '等待授权', completed: '已完成', failed: '失败', canceled: '已取消' }
|
|
330
|
-
const style = value === 'completed' ? ' completed' : value === 'failed' ? ' failed' : value === 'running' ? ' running' : (value === 'input_required' || value === 'permission_required') ? ' waiting' : ' canceled'
|
|
331
|
-
return '<span class="status' + style + '">' + escapeHtml(labels[value] || value) + '</span>'
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function shortId(value) {
|
|
335
|
-
return String(value || '').slice(0, 8)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function formatDuration(run) {
|
|
339
|
-
const ms = run.durationMs != null ? run.durationMs : Math.max(0, Date.now() - run.startedAt)
|
|
340
|
-
if (ms < 1000) return ms + ' 毫秒'
|
|
341
|
-
if (ms < 60_000) return Math.round(ms / 1000) + ' 秒'
|
|
342
|
-
if (ms < 3_600_000) return Math.floor(ms / 60_000) + ' 分 ' + Math.round(ms % 60_000 / 1000) + ' 秒'
|
|
343
|
-
return Math.floor(ms / 3_600_000) + ' 小时 ' + Math.round(ms % 3_600_000 / 60_000) + ' 分'
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async function openRunDrawer(id) {
|
|
347
|
-
const run = await api('/v1/admin/runs/' + encodeURIComponent(id))
|
|
348
|
-
const artifacts = run.artifacts && run.artifacts.length
|
|
349
|
-
? '<div class="run-detail-block"><h3>产物</h3><div class="overview-list">' + run.artifacts.map((artifact) => '<div class="list-row"><div class="list-row-main"><strong>' + escapeHtml(artifact.name || artifact.filename || artifact.id || '未命名产物') + '</strong><small>' + escapeHtml(artifact.mediaType || artifact.url || '') + '</small></div></div>').join('') + '</div></div>'
|
|
350
|
-
: ''
|
|
351
|
-
const body = '<div class="run-detail-grid">' +
|
|
352
|
-
detailItem('智能体', run.agentName) + detailItem('状态', runStatusLabel(run.state)) +
|
|
353
|
-
detailItem('开始时间', formatDate(run.startedAt)) + detailItem('耗时', formatDuration(run)) +
|
|
354
|
-
detailItem('协议', run.protocol.toUpperCase()) + detailItem('运行 ID', run.id) +
|
|
355
|
-
detailItem('当前阶段', run.progress && run.progress.phase || '—') + detailItem('输入附件', run.inputAttachmentCount || 0) + detailItem('产物', run.artifactCount || 0) +
|
|
356
|
-
'</div><div class="run-detail-block"><h3>用户任务</h3><pre class="run-detail-code">' + escapeHtml(run.task) + (run.taskTruncated ? '\n\n[记录已截断]' : '') + '</pre></div>' +
|
|
357
|
-
(run.progress && run.progress.message ? '<div class="run-detail-block"><h3>最近进度</h3><pre class="run-detail-code">' + escapeHtml(run.progress.message) + '</pre></div>' : '') +
|
|
358
|
-
(run.output ? '<div class="run-detail-block"><h3>智能体结果</h3><pre class="run-detail-code">' + escapeHtml(run.output) + '</pre></div>' : '') +
|
|
359
|
-
(run.error ? '<div class="run-detail-block"><h3>错误</h3><pre class="run-detail-code">' + escapeHtml(run.error) + '</pre></div>' : '') + artifacts
|
|
360
|
-
openDrawer('运行详情 · ' + run.agentName, body, '', null)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function detailItem(label, value) {
|
|
364
|
-
return '<div class="run-detail-item"><span>' + escapeHtml(label) + '</span><strong>' + escapeHtml(value) + '</strong></div>'
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function runStatusLabel(value) {
|
|
368
|
-
return { created: '已创建', running: '运行中', input_required: '等待输入', permission_required: '等待授权', completed: '已完成', failed: '失败', canceled: '已取消' }[value] || value
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function renderOverview() {
|
|
372
|
-
const agents = readinessRows()
|
|
373
|
-
const ready = agents.filter((agent) => agent.ready).length
|
|
374
|
-
actions.innerHTML = '<button id="refresh-overview" class="button">刷新</button>'
|
|
375
|
-
content.innerHTML =
|
|
376
|
-
'<div class="stats">' +
|
|
377
|
-
stat('智能体', agents.length) +
|
|
378
|
-
stat('就绪', ready) +
|
|
379
|
-
stat('会话', state.sessions) +
|
|
380
|
-
'</div>' +
|
|
381
|
-
'<div class="panel"><div class="panel-header"><h2>智能体</h2><span class="muted">每 20 秒自动检查</span></div>' +
|
|
382
|
-
(agents.length
|
|
383
|
-
? '<div class="panel-body overview-list">' + agents.map(overviewAgent).join('') + '</div>'
|
|
384
|
-
: '<div class="empty">尚未配置智能体。</div>') +
|
|
385
|
-
'</div>'
|
|
386
|
-
byId('refresh-overview').onclick = () => refreshReadiness(true)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function stat(label, value) {
|
|
390
|
-
return '<div class="stat"><span>' + escapeHtml(label) + '</span><strong>' + escapeHtml(value) + '</strong></div>'
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function readinessRows() {
|
|
394
|
-
const readiness = new Map(state.readiness.map((agent) => [agent.id, agent]))
|
|
395
|
-
return state.config.agents.map((agent) => readiness.get(agent.id) || {
|
|
396
|
-
id: agent.id,
|
|
397
|
-
name: agent.name,
|
|
398
|
-
protocol: agent.protocol,
|
|
399
|
-
driver: agent.driver,
|
|
400
|
-
enabled: agent.enabled,
|
|
401
|
-
ready: false,
|
|
402
|
-
checking: true
|
|
403
|
-
})
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function overviewAgent(agent) {
|
|
407
|
-
return '<div class="list-row"><div class="list-row-main"><strong>' + escapeHtml(agent.name) + '</strong><small>' +
|
|
408
|
-
escapeHtml(agent.protocol.toUpperCase() + (agent.driver ? ' · ' + agent.driver : '')) +
|
|
409
|
-
(agent.error ? ' · ' + escapeHtml(localizeError(agent.error)) : '') + '</small></div>' + statusMarkup(agent) + '</div>'
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function renderAgents() {
|
|
413
|
-
actions.innerHTML = '<button id="refresh-agents" class="button">刷新</button><button id="add-agent" class="button primary">添加智能体</button>'
|
|
414
|
-
const readiness = new Map(state.readiness.map((agent) => [agent.id, agent]))
|
|
415
|
-
const rows = state.config.agents.filter((agent) => {
|
|
416
|
-
const live = readiness.get(agent.id) || {}
|
|
417
|
-
const haystack = (agent.name + ' ' + agent.id + ' ' + (agent.description || '')).toLowerCase()
|
|
418
|
-
const status = !readiness.has(agent.id) ? 'checking' : live.ready ? 'ready' : agent.enabled ? 'failed' : 'disabled'
|
|
419
|
-
return haystack.indexOf(state.search.toLowerCase()) >= 0 &&
|
|
420
|
-
(state.protocol === 'all' || agent.protocol === state.protocol) &&
|
|
421
|
-
(state.status === 'all' || status === state.status)
|
|
422
|
-
})
|
|
423
|
-
content.innerHTML =
|
|
424
|
-
'<div class="toolbar"><input id="agent-search" type="search" placeholder="搜索智能体" value="' + escapeHtml(state.search) + '">' +
|
|
425
|
-
'<select id="protocol-filter"><option value="all">全部协议</option><option value="acp"' + selected(state.protocol, 'acp') + '>ACP</option><option value="a2a"' + selected(state.protocol, 'a2a') + '>A2A</option></select>' +
|
|
426
|
-
'<select id="status-filter"><option value="all">全部状态</option><option value="ready"' + selected(state.status, 'ready') + '>就绪</option><option value="failed"' + selected(state.status, 'failed') + '>不可用</option><option value="disabled"' + selected(state.status, 'disabled') + '>已禁用</option></select></div>' +
|
|
427
|
-
'<div class="table-wrap"><table><thead><tr><th>智能体</th><th>协议</th><th>驱动</th><th>状态</th><th>工作区</th><th>权限策略</th><th></th></tr></thead><tbody>' +
|
|
428
|
-
(rows.length ? rows.map((agent) => agentRow(agent, readiness.get(agent.id))).join('') : '<tr><td colspan="7" class="empty">没有符合条件的智能体。</td></tr>') +
|
|
429
|
-
'</tbody></table></div>'
|
|
430
|
-
byId('agent-search').oninput = (event) => { state.search = event.target.value; renderAgents() }
|
|
431
|
-
byId('protocol-filter').onchange = (event) => { state.protocol = event.target.value; renderAgents() }
|
|
432
|
-
byId('status-filter').onchange = (event) => { state.status = event.target.value; renderAgents() }
|
|
433
|
-
byId('refresh-agents').onclick = () => refreshReadiness(true)
|
|
434
|
-
byId('add-agent').onclick = () => openAgentDrawer()
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function agentRow(agent, live) {
|
|
438
|
-
const statusAgent = live || { ready: false, enabled: agent.enabled, checking: true }
|
|
439
|
-
return '<tr><td><div class="agent-name"><strong>' + escapeHtml(agent.name) + '</strong><small>' + escapeHtml(agent.id) + '</small>' +
|
|
440
|
-
(statusAgent.error && agent.enabled ? '<span class="error-detail">' + escapeHtml(localizeError(statusAgent.error)) + '</span>' : '') +
|
|
441
|
-
'</div></td><td><span class="badge">' + escapeHtml(agent.protocol.toUpperCase()) + '</span></td><td>' + escapeHtml(agent.driver || '—') + '</td><td>' + statusMarkup(statusAgent) + '</td><td>' + escapeHtml(agent.workspace || '—') + '</td><td>' + escapeHtml(permissionLabel(agent.permissionPolicy)) + '</td><td><div class="row-actions"><button class="button small" data-agent-edit="' + escapeHtml(agent.id) + '">编辑</button><button class="button small danger" data-agent-delete="' + escapeHtml(agent.id) + '">删除</button></div></td></tr>'
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function permissionLabel(value) {
|
|
445
|
-
if (value === 'ask') return '询问'
|
|
446
|
-
if (value === 'allow') return '始终允许'
|
|
447
|
-
if (value === 'deny') return '拒绝'
|
|
448
|
-
return value || '—'
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function statusMarkup(agent) {
|
|
452
|
-
if (!agent.enabled) return '<span class="status">已禁用</span>'
|
|
453
|
-
if (agent.checking) return '<span class="status checking">检查中</span>'
|
|
454
|
-
return agent.ready ? '<span class="status ready">就绪</span>' : '<span class="status failed">不可用</span>'
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function renderWorkspaces() {
|
|
458
|
-
actions.innerHTML = '<button id="add-workspace" class="button primary">添加工作区</button>'
|
|
459
|
-
const roots = state.config.workspaceRoots || []
|
|
460
|
-
content.innerHTML = '<div class="workspace-list">' +
|
|
461
|
-
roots.map((root, index) => '<div class="list-row"><div class="list-row-main"><strong class="path">' + escapeHtml(root) + '</strong><small>ACP 会话允许访问的根目录</small></div><div class="row-actions"><button class="button small" data-workspace-edit="' + index + '">编辑</button><button class="button small danger" data-workspace-delete="' + index + '"' + (roots.length === 1 ? ' disabled' : '') + '>删除</button></div></div>').join('') +
|
|
462
|
-
'</div><div class="tip">建议仅添加必要的最小目录范围。启动 ACP 会话前,Agent Nexus 会解析并校验真实路径。</div>'
|
|
463
|
-
byId('add-workspace').onclick = () => openWorkspaceDrawer()
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function renderApiKeys() {
|
|
467
|
-
closeKeyActionMenu()
|
|
468
|
-
actions.innerHTML = '<button id="create-key" class="button primary">创建 API 密钥</button>'
|
|
469
|
-
content.innerHTML = state.apiKeys.length
|
|
470
|
-
? '<div class="table-wrap key-table-wrap"><table class="key-table"><thead><tr><th>名称</th><th>状态</th><th>API 密钥</th><th>授权范围</th><th>最后使用</th><th class="key-actions-heading">操作</th></tr></thead><tbody>' + state.apiKeys.map(keyRow).join('') + '</tbody></table></div>'
|
|
471
|
-
: '<div class="panel"><div class="empty">尚未创建 API 密钥。客户端需要访问时再创建即可。</div></div>'
|
|
472
|
-
byId('create-key').onclick = () => openKeyDrawer()
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function keyRow(key) {
|
|
476
|
-
const scope = key.scope.allAgents ? '全部智能体' : (key.scope.agentIds.length ? key.scope.agentIds.join(', ') : '未授权智能体')
|
|
477
|
-
const name = key.legacy && key.name === 'Legacy Access Key' ? '旧版访问密钥' : key.name
|
|
478
|
-
const id = escapeHtml(key.id)
|
|
479
|
-
const toggleLabel = key.enabled ? '禁用密钥' : '启用密钥'
|
|
480
|
-
return '<tr><td><div class="agent-name"><strong>' + escapeHtml(name) + '</strong><small>' + (key.legacy ? '从旧版配置迁移' : '创建于 ' + formatDate(key.createdAt)) + '</small></div></td>' +
|
|
481
|
-
'<td>' + (key.enabled ? '<span class="status ready">已启用</span>' : '<span class="status">已禁用</span>') + '</td>' +
|
|
482
|
-
'<td><div class="key-secret-cell"><code>••••' + escapeHtml(key.suffix) + '</code><button class="key-copy-button" data-key-action="copy" data-key-id="' + id + '" aria-label="复制完整密钥" title="复制完整密钥">' + actionIcons.copy + '</button></div></td>' +
|
|
483
|
-
'<td><span class="key-scope">' + escapeHtml(scope) + '</span></td>' +
|
|
484
|
-
'<td><span class="key-last-used">' + escapeHtml(key.lastUsedAt ? formatDate(key.lastUsedAt) : '从未使用') + '</span></td>' +
|
|
485
|
-
'<td class="key-actions-cell"><div class="key-actions"><button class="key-icon-button' + (key.enabled ? ' danger-action' : '') + '" data-key-action="toggle" data-key-id="' + id + '" aria-label="' + toggleLabel + '" title="' + toggleLabel + '">' + (key.enabled ? actionIcons.disable : actionIcons.enable) + '</button><button class="key-icon-button" data-key-action="rename" data-key-id="' + id + '" aria-label="重命名" title="重命名">' + actionIcons.edit + '</button><button class="key-icon-button" data-key-menu="' + id + '" aria-label="更多操作" title="更多操作" aria-expanded="false">' + actionIcons.more + '</button></div></td></tr>'
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function toggleKeyActionMenu(anchor, id) {
|
|
489
|
-
if (!keyActionMenu.classList.contains('hidden') && keyActionMenu.dataset.keyId === id) {
|
|
490
|
-
closeKeyActionMenu()
|
|
491
|
-
return
|
|
492
|
-
}
|
|
493
|
-
closeKeyActionMenu()
|
|
494
|
-
keyActionMenu.dataset.keyId = id
|
|
495
|
-
keyActionMenu.innerHTML =
|
|
496
|
-
'<button type="button" role="menuitem" data-key-action="reveal" data-key-id="' + escapeHtml(id) + '">' + actionIcons.reveal + '<span>显示完整密钥</span></button>' +
|
|
497
|
-
'<button type="button" role="menuitem" data-key-action="scope" data-key-id="' + escapeHtml(id) + '">' + actionIcons.scope + '<span>编辑授权范围</span></button>' +
|
|
498
|
-
'<button type="button" role="menuitem" data-key-action="regenerate" data-key-id="' + escapeHtml(id) + '">' + actionIcons.regenerate + '<span>重新生成密钥</span></button>' +
|
|
499
|
-
'<button type="button" role="menuitem" class="danger-text" data-key-action="delete" data-key-id="' + escapeHtml(id) + '">' + actionIcons.delete + '<span>删除密钥</span></button>'
|
|
500
|
-
keyActionMenu.classList.remove('hidden')
|
|
501
|
-
anchor.setAttribute('aria-expanded', 'true')
|
|
502
|
-
const anchorRect = anchor.getBoundingClientRect()
|
|
503
|
-
const menuRect = keyActionMenu.getBoundingClientRect()
|
|
504
|
-
const left = Math.max(8, Math.min(window.innerWidth - menuRect.width - 8, anchorRect.right - menuRect.width))
|
|
505
|
-
const below = anchorRect.bottom + 6
|
|
506
|
-
const top = below + menuRect.height <= window.innerHeight - 8
|
|
507
|
-
? below
|
|
508
|
-
: Math.max(8, anchorRect.top - menuRect.height - 6)
|
|
509
|
-
keyActionMenu.style.left = left + 'px'
|
|
510
|
-
keyActionMenu.style.top = top + 'px'
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function closeKeyActionMenu() {
|
|
514
|
-
keyActionMenu.classList.add('hidden')
|
|
515
|
-
keyActionMenu.innerHTML = ''
|
|
516
|
-
keyActionMenu.dataset.keyId = ''
|
|
517
|
-
document.querySelectorAll('[data-key-menu][aria-expanded="true"]').forEach((button) => button.setAttribute('aria-expanded', 'false'))
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function openDrawer(title, body, submitLabel, onSubmit) {
|
|
521
|
-
byId('drawer-title').textContent = title
|
|
522
|
-
drawerForm.innerHTML = body + '<div class="drawer-footer"><button type="button" class="button" data-close-drawer>取消</button>' + (submitLabel ? '<button type="submit" class="button primary">' + escapeHtml(submitLabel) + '</button>' : '') + '</div><p class="form-error" data-form-error></p>'
|
|
523
|
-
drawerSubmit = onSubmit || null
|
|
524
|
-
drawer.classList.remove('hidden')
|
|
525
|
-
drawerBackdrop.classList.remove('hidden')
|
|
526
|
-
const first = drawerForm.querySelector('input, select, textarea')
|
|
527
|
-
if (first) setTimeout(() => first.focus(), 0)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function closeDrawer() {
|
|
531
|
-
drawer.classList.add('hidden')
|
|
532
|
-
drawerBackdrop.classList.add('hidden')
|
|
533
|
-
drawerForm.innerHTML = ''
|
|
534
|
-
drawerSubmit = null
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function openAgentDrawer(agent) {
|
|
538
|
-
const editing = Boolean(agent)
|
|
539
|
-
const current = agent || { protocol: 'acp', enabled: true, permissionPolicy: 'ask', permissionTimeoutMs: 900000, timeoutMs: 60000 }
|
|
540
|
-
const permissionTimeoutSeconds = Math.max(1, Math.round(Number(current.permissionTimeoutMs || 900000) / 1000))
|
|
541
|
-
const requestTimeoutSeconds = Math.max(1, Math.round(Number(current.timeoutMs || 60000) / 1000))
|
|
542
|
-
const drivers = state.config.driverKinds.map((driver) => '<option value="' + escapeHtml(driver) + '"' + selected(current.driver || state.config.driverKinds[0], driver) + '>' + escapeHtml(driver) + '</option>').join('')
|
|
543
|
-
const roots = state.config.workspaceRoots.map((root) => '<option value="' + escapeHtml(root) + '"></option>').join('')
|
|
544
|
-
const body =
|
|
545
|
-
'<label>智能体 ID<input name="id" value="' + escapeHtml(current.id || '') + '" pattern="[a-z0-9][a-z0-9._\\-]{0,63}" required' + (editing ? ' disabled' : '') + '></label>' +
|
|
546
|
-
'<div class="field-row"><label>协议<select name="protocol"><option value="acp"' + selected(current.protocol, 'acp') + '>ACP</option><option value="a2a"' + selected(current.protocol, 'a2a') + '>A2A</option></select></label><label class="checkbox"><input name="enabled" type="checkbox"' + checked(current.enabled) + '>启用</label></div>' +
|
|
547
|
-
'<label>名称<input name="name" value="' + escapeHtml(current.name || '') + '" required></label>' +
|
|
548
|
-
'<label>描述<textarea name="description">' + escapeHtml(current.description || '') + '</textarea></label>' +
|
|
549
|
-
'<div data-protocol-section="acp"><label>驱动<select name="driver">' + drivers + '</select></label><label>工作区<input name="workspace" list="workspace-roots" value="' + escapeHtml(current.workspace || state.config.workspaceRoots[0] || '') + '" required><datalist id="workspace-roots">' + roots + '</datalist></label><div class="field-row"><label>权限策略<select name="permissionPolicy"><option value="ask"' + selected(current.permissionPolicy, 'ask') + '>询问</option><option value="allow"' + selected(current.permissionPolicy, 'allow') + '>始终允许</option><option value="deny"' + selected(current.permissionPolicy, 'deny') + '>拒绝</option></select></label><label>权限确认超时(秒)<input name="permissionTimeoutMs" type="number" min="1" max="86400" step="1" value="' + escapeHtml(permissionTimeoutSeconds) + '"><small class="field-help">单位:秒;默认 900 秒,最长 24 小时;“始终允许”不会等待确认。</small></label></div></div>' +
|
|
550
|
-
'<div data-protocol-section="a2a"><label>Agent Card URL<input name="agentCardUrl" type="url" value="' + escapeHtml(current.agentCardUrl || '') + '" placeholder="http://agent.local:8080/.well-known/agent-card.json" required><small class="field-help">填写完整的 Agent Card JSON 地址;调用地址和能力将从 Card 自动发现。</small></label><div class="field-row"><label>首选传输<select name="preferredTransport"><option value="auto"' + selected(current.preferredTransport || 'auto', 'auto') + '>自动(按 Card)</option><option value="jsonrpc"' + selected(current.preferredTransport, 'jsonrpc') + '>JSON-RPC</option><option value="http-json"' + selected(current.preferredTransport, 'http-json') + '>HTTP+JSON</option></select></label><label>认证方式<select name="authType"><option value="none"' + selected(current.auth && current.auth.type || 'none', 'none') + '>无认证</option><option value="bearer"' + selected(current.auth && current.auth.type, 'bearer') + '>Bearer Token</option><option value="header"' + selected(current.auth && current.auth.type, 'header') + '>自定义请求头</option></select></label></div><label data-auth-header>请求头名称<input name="authHeaderName" value="' + escapeHtml(current.auth && current.auth.headerName || '') + '" placeholder="X-API-Key"></label><label data-auth-value><span data-auth-value-label>认证凭据</span><input name="authValue" type="password" autocomplete="off" placeholder="' + (editing && current.auth && current.auth.configured ? '留空以保留当前凭据' : '') + '"></label><label>请求超时(秒)<input name="timeoutMs" type="number" min="1" max="1800" step="1" value="' + escapeHtml(requestTimeoutSeconds) + '"><small class="field-help">单位:秒;默认 60 秒;用于限制这个 A2A Agent 的单次请求,最大 1800 秒(30 分钟)。</small></label></div>'
|
|
551
|
-
openDrawer(editing ? '编辑智能体' : '添加智能体', body, editing ? '保存修改' : '添加智能体', async (form) => {
|
|
552
|
-
const data = new FormData(form)
|
|
553
|
-
const protocol = data.get('protocol')
|
|
554
|
-
const id = editing ? current.id : String(data.get('id') || '').trim().toLowerCase()
|
|
555
|
-
const payload = {
|
|
556
|
-
protocol,
|
|
557
|
-
name: String(data.get('name') || '').trim(),
|
|
558
|
-
description: String(data.get('description') || '').trim(),
|
|
559
|
-
enabled: data.get('enabled') === 'on'
|
|
560
|
-
}
|
|
561
|
-
if (protocol === 'acp') {
|
|
562
|
-
payload.driver = data.get('driver')
|
|
563
|
-
payload.workspace = String(data.get('workspace') || '').trim()
|
|
564
|
-
payload.permissionPolicy = data.get('permissionPolicy')
|
|
565
|
-
payload.permissionTimeoutMs = Number(data.get('permissionTimeoutMs')) * 1000
|
|
566
|
-
} else {
|
|
567
|
-
payload.agentCardUrl = String(data.get('agentCardUrl') || '').trim()
|
|
568
|
-
payload.preferredTransport = data.get('preferredTransport')
|
|
569
|
-
payload.authType = data.get('authType')
|
|
570
|
-
payload.authHeaderName = String(data.get('authHeaderName') || '').trim()
|
|
571
|
-
const authValue = String(data.get('authValue') || '')
|
|
572
|
-
if (authValue) payload.authValue = authValue
|
|
573
|
-
payload.timeoutMs = Number(data.get('timeoutMs')) * 1000
|
|
574
|
-
}
|
|
575
|
-
await api('/v1/admin/agents/' + encodeURIComponent(id), { method: 'PUT', body: payload })
|
|
576
|
-
closeDrawer()
|
|
577
|
-
await loadAll(true)
|
|
578
|
-
render()
|
|
579
|
-
toast(editing ? '智能体已更新' : '智能体已添加')
|
|
580
|
-
})
|
|
581
|
-
const protocolSelect = drawerForm.elements.protocol
|
|
582
|
-
const authSelect = drawerForm.elements.authType
|
|
583
|
-
const sync = () => {
|
|
584
|
-
drawerForm.querySelectorAll('[data-protocol-section]').forEach((section) => {
|
|
585
|
-
section.classList.toggle('hidden', section.dataset.protocolSection !== protocolSelect.value)
|
|
586
|
-
section.querySelectorAll('input,select,textarea').forEach((field) => { field.disabled = section.classList.contains('hidden') })
|
|
587
|
-
})
|
|
588
|
-
if (protocolSelect.value === 'a2a') {
|
|
589
|
-
const auth = authSelect.value
|
|
590
|
-
drawerForm.querySelector('[data-auth-header]').classList.toggle('hidden', auth !== 'header')
|
|
591
|
-
drawerForm.querySelector('[data-auth-value]').classList.toggle('hidden', auth === 'none')
|
|
592
|
-
drawerForm.querySelector('[data-auth-value-label]').textContent = auth === 'bearer' ? 'Bearer Token' : '认证凭据'
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
protocolSelect.onchange = sync
|
|
596
|
-
authSelect.onchange = sync
|
|
597
|
-
sync()
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function openWorkspaceDrawer(index) {
|
|
601
|
-
const editing = Number.isInteger(index)
|
|
602
|
-
const current = editing ? state.config.workspaceRoots[index] : ''
|
|
603
|
-
openDrawer(editing ? '编辑工作区' : '添加工作区', '<label>允许访问的根目录<input name="path" value="' + escapeHtml(current) + '" required></label>', editing ? '保存修改' : '添加工作区', async (form) => {
|
|
604
|
-
const roots = state.config.workspaceRoots.slice()
|
|
605
|
-
const value = String(new FormData(form).get('path') || '').trim()
|
|
606
|
-
if (editing) roots[index] = value
|
|
607
|
-
else roots.push(value)
|
|
608
|
-
const result = await api('/v1/admin/config/workspace-roots', { method: 'PUT', body: { workspaceRoots: roots } })
|
|
609
|
-
state.config = result
|
|
610
|
-
closeDrawer()
|
|
611
|
-
render()
|
|
612
|
-
toast(editing ? '工作区已更新' : '工作区已添加')
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function openKeyDrawer() {
|
|
617
|
-
const scopeRows = state.config.agents.map((agent) => '<label class="checkbox"><input type="checkbox" name="agentId" value="' + escapeHtml(agent.id) + '">' + escapeHtml(agent.name) + ' <span class="muted">(' + escapeHtml(agent.protocol.toUpperCase()) + ')</span></label>').join('')
|
|
618
|
-
const body = '<label>名称<input name="name" required placeholder="例如:开发客户端"></label><label class="checkbox"><input id="all-agents" name="allAgents" type="checkbox" checked>允许访问全部智能体</label><div id="agent-scope" class="agent-scope hidden">' + (scopeRows || '<span class="muted">尚未配置智能体。</span>') + '</div><label>自定义密钥(可选)<input name="customSecret" minlength="16" autocomplete="off" placeholder="留空则自动生成 nx_sk_ 密钥"></label>'
|
|
619
|
-
openDrawer('创建 API 密钥', body, '创建 API 密钥', async (form) => {
|
|
620
|
-
const data = new FormData(form)
|
|
621
|
-
const allAgents = data.get('allAgents') === 'on'
|
|
622
|
-
const agentIds = data.getAll('agentId').map(String)
|
|
623
|
-
if (!allAgents && !agentIds.length) throw new Error('请至少选择一个智能体')
|
|
624
|
-
const customSecret = String(data.get('customSecret') || '').trim()
|
|
625
|
-
const result = await api('/v1/admin/api-keys', {
|
|
626
|
-
method: 'POST',
|
|
627
|
-
body: {
|
|
628
|
-
name: String(data.get('name') || '').trim(),
|
|
629
|
-
scope: { allAgents, agentIds },
|
|
630
|
-
customSecret: customSecret || undefined
|
|
631
|
-
}
|
|
632
|
-
})
|
|
633
|
-
state.apiKeys.unshift(result.key)
|
|
634
|
-
showSecret('API 密钥已创建', result.secret)
|
|
635
|
-
render()
|
|
636
|
-
})
|
|
637
|
-
const all = byId('all-agents')
|
|
638
|
-
all.onchange = () => byId('agent-scope').classList.toggle('hidden', all.checked)
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function showSecret(title, secret) {
|
|
642
|
-
openDrawer(title, '<div class="secret-box"><span class="muted">请立即复制此密钥;之后也可在 API 密钥页面再次显示。</span><code class="secret-value" id="secret-value">' + escapeHtml(secret) + '</code><button class="button" type="button" id="copy-secret">复制</button></div>', '', null)
|
|
643
|
-
byId('copy-secret').onclick = async () => {
|
|
644
|
-
try {
|
|
645
|
-
await copySecret(secret)
|
|
646
|
-
toast('API 密钥已复制')
|
|
647
|
-
} catch (error) { toast(error.message, true) }
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async function copySecret(secret) {
|
|
652
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
653
|
-
try {
|
|
654
|
-
await navigator.clipboard.writeText(secret)
|
|
655
|
-
return
|
|
656
|
-
} catch {}
|
|
657
|
-
}
|
|
658
|
-
const temporary = document.createElement('textarea')
|
|
659
|
-
temporary.value = secret
|
|
660
|
-
temporary.setAttribute('readonly', '')
|
|
661
|
-
temporary.style.position = 'fixed'
|
|
662
|
-
temporary.style.opacity = '0'
|
|
663
|
-
temporary.style.pointerEvents = 'none'
|
|
664
|
-
document.body.appendChild(temporary)
|
|
665
|
-
temporary.select()
|
|
666
|
-
const copied = document.execCommand('copy')
|
|
667
|
-
temporary.remove()
|
|
668
|
-
if (!copied) throw new Error('无法自动复制,请在“显示完整密钥”中手动复制。')
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function openPasswordDrawer() {
|
|
672
|
-
openDrawer('修改控制台密码', '<label>当前密码<input name="currentPassword" type="password" autocomplete="current-password" required></label><label>新密码<input name="newPassword" type="password" minlength="12" autocomplete="new-password" required></label><label>确认新密码<input name="confirmPassword" type="password" minlength="12" autocomplete="new-password" required></label>', '修改密码', async (form) => {
|
|
673
|
-
const data = new FormData(form)
|
|
674
|
-
await api('/v1/admin/password', { method: 'PUT', body: { currentPassword: String(data.get('currentPassword') || ''), newPassword: String(data.get('newPassword') || ''), confirmPassword: String(data.get('confirmPassword') || '') } })
|
|
675
|
-
closeDrawer()
|
|
676
|
-
showLogin()
|
|
677
|
-
toast('控制台密码已修改,请重新登录。')
|
|
678
|
-
})
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
async function handleContentClick(event) {
|
|
682
|
-
const runTarget = event.target.closest('[data-run-detail]')
|
|
683
|
-
if (runTarget) {
|
|
684
|
-
await runAction(() => openRunDrawer(runTarget.dataset.runDetail))
|
|
685
|
-
return
|
|
686
|
-
}
|
|
687
|
-
const button = event.target.closest('button')
|
|
688
|
-
if (!button) return
|
|
689
|
-
if (button.dataset.keyMenu) {
|
|
690
|
-
toggleKeyActionMenu(button, button.dataset.keyMenu)
|
|
691
|
-
return
|
|
692
|
-
}
|
|
693
|
-
if (button.dataset.agentEdit) {
|
|
694
|
-
openAgentDrawer(state.config.agents.find((agent) => agent.id === button.dataset.agentEdit))
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
if (button.dataset.agentDelete) {
|
|
698
|
-
if (!confirm('确定删除此智能体吗?已有会话不会被修改。')) return
|
|
699
|
-
await runAction(async () => {
|
|
700
|
-
state.config = await api('/v1/admin/agents/' + encodeURIComponent(button.dataset.agentDelete), { method: 'DELETE' })
|
|
701
|
-
await loadAll(true)
|
|
702
|
-
render()
|
|
703
|
-
toast('智能体已删除')
|
|
704
|
-
})
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
if (button.dataset.workspaceEdit !== undefined) {
|
|
708
|
-
openWorkspaceDrawer(Number(button.dataset.workspaceEdit))
|
|
709
|
-
return
|
|
710
|
-
}
|
|
711
|
-
if (button.dataset.workspaceDelete !== undefined) {
|
|
712
|
-
const index = Number(button.dataset.workspaceDelete)
|
|
713
|
-
if (!confirm('确定移除此工作区根目录吗?')) return
|
|
714
|
-
await runAction(async () => {
|
|
715
|
-
const roots = state.config.workspaceRoots.filter((_, itemIndex) => itemIndex !== index)
|
|
716
|
-
state.config = await api('/v1/admin/config/workspace-roots', { method: 'PUT', body: { workspaceRoots: roots } })
|
|
717
|
-
render()
|
|
718
|
-
toast('工作区已移除')
|
|
719
|
-
})
|
|
720
|
-
return
|
|
721
|
-
}
|
|
722
|
-
if (button.dataset.keyAction) await handleKeyAction(button.dataset.keyAction, button.dataset.keyId)
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
async function handleKeyAction(action, id) {
|
|
726
|
-
const key = state.apiKeys.find((item) => item.id === id)
|
|
727
|
-
if (!key) return
|
|
728
|
-
await runAction(async () => {
|
|
729
|
-
if (action === 'copy') {
|
|
730
|
-
const value = await api('/v1/admin/api-keys/' + encodeURIComponent(id) + '/reveal', { method: 'POST' })
|
|
731
|
-
await copySecret(value.secret)
|
|
732
|
-
toast('API 密钥已复制')
|
|
733
|
-
}
|
|
734
|
-
if (action === 'reveal') {
|
|
735
|
-
const value = await api('/v1/admin/api-keys/' + encodeURIComponent(id) + '/reveal', { method: 'POST' })
|
|
736
|
-
showSecret('显示 API 密钥', value.secret)
|
|
737
|
-
}
|
|
738
|
-
if (action === 'rename') {
|
|
739
|
-
const name = prompt('API 密钥名称', key.name)
|
|
740
|
-
if (!name || name === key.name) return
|
|
741
|
-
await api('/v1/admin/api-keys/' + encodeURIComponent(id), { method: 'PATCH', body: { name } })
|
|
742
|
-
await reloadKeys()
|
|
743
|
-
toast('API 密钥已重命名')
|
|
744
|
-
}
|
|
745
|
-
if (action === 'scope') {
|
|
746
|
-
openKeyScopeDrawer(key)
|
|
747
|
-
}
|
|
748
|
-
if (action === 'toggle') {
|
|
749
|
-
await api('/v1/admin/api-keys/' + encodeURIComponent(id), { method: 'PATCH', body: { enabled: !key.enabled } })
|
|
750
|
-
await reloadKeys()
|
|
751
|
-
toast(key.enabled ? 'API 密钥已禁用' : 'API 密钥已启用')
|
|
752
|
-
}
|
|
753
|
-
if (action === 'regenerate') {
|
|
754
|
-
if (!confirm('确定重新生成此 API 密钥吗?当前密钥将立即失效。')) return
|
|
755
|
-
const value = await api('/v1/admin/api-keys/' + encodeURIComponent(id) + '/regenerate', { method: 'POST' })
|
|
756
|
-
await reloadKeys()
|
|
757
|
-
showSecret('API 密钥已重新生成', value.secret)
|
|
758
|
-
}
|
|
759
|
-
if (action === 'delete') {
|
|
760
|
-
if (!confirm('确定删除此 API 密钥吗?此操作无法撤销。')) return
|
|
761
|
-
await api('/v1/admin/api-keys/' + encodeURIComponent(id), { method: 'DELETE' })
|
|
762
|
-
await reloadKeys()
|
|
763
|
-
toast('API 密钥已删除')
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function openKeyScopeDrawer(key) {
|
|
769
|
-
const selectedIds = new Set(key.scope.agentIds || [])
|
|
770
|
-
const scopeRows = state.config.agents.map((agent) => '<label class="checkbox"><input type="checkbox" name="agentId" value="' + escapeHtml(agent.id) + '"' + checked(selectedIds.has(agent.id)) + '>' + escapeHtml(agent.name) + ' <span class="muted">(' + escapeHtml(agent.protocol.toUpperCase()) + ')</span></label>').join('')
|
|
771
|
-
const body = '<label class="checkbox"><input id="edit-all-agents" name="allAgents" type="checkbox"' + checked(key.scope.allAgents) + '>允许访问全部智能体</label><div id="edit-agent-scope" class="agent-scope' + (key.scope.allAgents ? ' hidden' : '') + '">' + (scopeRows || '<span class="muted">尚未配置智能体。</span>') + '</div>'
|
|
772
|
-
openDrawer('编辑 API 密钥授权范围', body, '保存授权范围', async (form) => {
|
|
773
|
-
const data = new FormData(form)
|
|
774
|
-
const allAgents = data.get('allAgents') === 'on'
|
|
775
|
-
const agentIds = data.getAll('agentId').map(String)
|
|
776
|
-
if (!allAgents && !agentIds.length) throw new Error('请至少选择一个智能体')
|
|
777
|
-
await api('/v1/admin/api-keys/' + encodeURIComponent(key.id), { method: 'PATCH', body: { scope: { allAgents, agentIds } } })
|
|
778
|
-
closeDrawer()
|
|
779
|
-
await reloadKeys()
|
|
780
|
-
toast('API 密钥授权范围已更新')
|
|
781
|
-
})
|
|
782
|
-
const all = byId('edit-all-agents')
|
|
783
|
-
all.onchange = () => byId('edit-agent-scope').classList.toggle('hidden', all.checked)
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async function reloadKeys() {
|
|
787
|
-
const value = await api('/v1/admin/api-keys')
|
|
788
|
-
state.apiKeys = value.apiKeys || []
|
|
789
|
-
render()
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
async function runAction(task) {
|
|
793
|
-
try { await task() } catch (error) { toast(error.message, true) }
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function formatDate(timestamp) {
|
|
797
|
-
if (!timestamp) return '—'
|
|
798
|
-
return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(timestamp))
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function toast(message, error) {
|
|
802
|
-
const item = document.createElement('div')
|
|
803
|
-
item.className = 'toast' + (error ? ' error' : '')
|
|
804
|
-
item.textContent = message
|
|
805
|
-
byId('toast-region').appendChild(item)
|
|
806
|
-
setTimeout(() => item.remove(), 4200)
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function applyTheme(preference) {
|
|
810
|
-
const theme = preference === 'system'
|
|
811
|
-
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
812
|
-
: preference
|
|
813
|
-
document.documentElement.dataset.theme = theme
|
|
814
|
-
const select = byId('theme-select')
|
|
815
|
-
if (select) select.value = preference
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
byId('setup-form').onsubmit = async (event) => {
|
|
819
|
-
event.preventDefault()
|
|
820
|
-
const form = event.currentTarget
|
|
821
|
-
const error = form.querySelector('[data-form-error]')
|
|
822
|
-
error.textContent = ''
|
|
823
|
-
const data = new FormData(form)
|
|
824
|
-
try {
|
|
825
|
-
await api('/v1/bootstrap/initialize', { method: 'POST', body: { password: String(data.get('password') || ''), confirmPassword: String(data.get('confirmPassword') || '') } })
|
|
826
|
-
form.reset()
|
|
827
|
-
showLogin()
|
|
828
|
-
toast('初始化完成,请使用控制台密码登录。')
|
|
829
|
-
} catch (reason) { error.textContent = reason.message }
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
byId('login-form').onsubmit = async (event) => {
|
|
833
|
-
event.preventDefault()
|
|
834
|
-
const form = event.currentTarget
|
|
835
|
-
const error = form.querySelector('[data-form-error]')
|
|
836
|
-
error.textContent = ''
|
|
837
|
-
try {
|
|
838
|
-
const data = new FormData(form)
|
|
839
|
-
await api('/v1/admin/auth/login', { method: 'POST', body: { password: String(data.get('password') || '') } })
|
|
840
|
-
form.reset()
|
|
841
|
-
await enterApp()
|
|
842
|
-
} catch (reason) { error.textContent = reason.message }
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
document.querySelectorAll('.nav-item').forEach((item) => {
|
|
846
|
-
item.onclick = () => {
|
|
847
|
-
state.page = item.dataset.page
|
|
848
|
-
render()
|
|
849
|
-
if (state.page === 'runs') void refreshRuns(false)
|
|
850
|
-
}
|
|
851
|
-
})
|
|
852
|
-
content.addEventListener('click', (event) => { void handleContentClick(event) })
|
|
853
|
-
keyActionMenu.addEventListener('click', (event) => {
|
|
854
|
-
const button = event.target.closest('button[data-key-action]')
|
|
855
|
-
if (!button) return
|
|
856
|
-
closeKeyActionMenu()
|
|
857
|
-
void handleKeyAction(button.dataset.keyAction, button.dataset.keyId)
|
|
858
|
-
})
|
|
859
|
-
drawerForm.onsubmit = async (event) => {
|
|
860
|
-
event.preventDefault()
|
|
861
|
-
if (!drawerSubmit) return
|
|
862
|
-
const error = drawerForm.querySelector('[data-form-error]')
|
|
863
|
-
error.textContent = ''
|
|
864
|
-
try { await drawerSubmit(drawerForm) } catch (reason) { error.textContent = reason.message }
|
|
865
|
-
}
|
|
866
|
-
drawerForm.addEventListener('click', (event) => {
|
|
867
|
-
if (event.target.closest('[data-close-drawer]')) closeDrawer()
|
|
868
|
-
})
|
|
869
|
-
byId('drawer-close').onclick = closeDrawer
|
|
870
|
-
drawerBackdrop.onclick = closeDrawer
|
|
871
|
-
|
|
872
|
-
byId('admin-menu').onclick = () => {
|
|
873
|
-
const popover = byId('admin-popover')
|
|
874
|
-
const hidden = popover.classList.toggle('hidden')
|
|
875
|
-
byId('admin-menu').setAttribute('aria-expanded', String(!hidden))
|
|
876
|
-
}
|
|
877
|
-
byId('theme-select').onchange = (event) => {
|
|
878
|
-
localStorage.setItem('agent-nexus-theme', event.target.value)
|
|
879
|
-
applyTheme(event.target.value)
|
|
880
|
-
}
|
|
881
|
-
byId('change-password').onclick = () => { byId('admin-popover').classList.add('hidden'); openPasswordDrawer() }
|
|
882
|
-
byId('logout').onclick = async () => {
|
|
883
|
-
await runAction(async () => {
|
|
884
|
-
await api('/v1/admin/auth/logout', { method: 'POST' })
|
|
885
|
-
showLogin()
|
|
886
|
-
})
|
|
887
|
-
}
|
|
888
|
-
document.addEventListener('click', (event) => {
|
|
889
|
-
if (!event.target.closest('.admin-menu-wrap')) byId('admin-popover').classList.add('hidden')
|
|
890
|
-
if (!event.target.closest('.key-action-menu') && !event.target.closest('[data-key-menu]')) closeKeyActionMenu()
|
|
891
|
-
})
|
|
892
|
-
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
893
|
-
if ((localStorage.getItem('agent-nexus-theme') || 'system') === 'system') applyTheme('system')
|
|
894
|
-
})
|
|
895
|
-
document.addEventListener('keydown', (event) => {
|
|
896
|
-
if (event.key === 'Escape') {
|
|
897
|
-
closeKeyActionMenu()
|
|
898
|
-
closeDrawer()
|
|
899
|
-
}
|
|
900
|
-
})
|
|
901
|
-
window.addEventListener('resize', closeKeyActionMenu)
|
|
902
|
-
window.addEventListener('scroll', closeKeyActionMenu, true)
|
|
903
|
-
|
|
904
|
-
setInterval(() => {
|
|
905
|
-
if (state.authenticated) void refreshReadiness(false)
|
|
906
|
-
}, 20_000)
|
|
907
|
-
|
|
908
|
-
setInterval(() => {
|
|
909
|
-
if (state.authenticated && state.page === 'runs') void refreshRuns(false)
|
|
910
|
-
}, 5_000)
|
|
911
|
-
|
|
912
|
-
void boot()
|
|
913
|
-
})()
|
|
914
|
-
`;
|