@songsid/agend 2.1.0-beta.15 → 2.1.0-beta.17
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/dist/config-validator.js +42 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/fleet-manager.d.ts +2 -1
- package/dist/fleet-manager.js +13 -1
- package/dist/fleet-manager.js.map +1 -1
- package/dist/settings-api.d.ts +15 -1
- package/dist/settings-api.js +97 -9
- package/dist/settings-api.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/ui/settings.html +242 -53
- package/package.json +1 -1
package/dist/ui/settings.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
9
|
:root {
|
|
10
10
|
--bg:#ffffff; --card:#f9fafb; --border:#e5e7eb; --fg:#111827; --muted:#6b7280;
|
|
11
|
-
--accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626;
|
|
11
|
+
--accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626; --yellow:#b45309;
|
|
12
12
|
--radius:8px; --radius-sm:6px;
|
|
13
13
|
}
|
|
14
14
|
body { background: var(--bg); color: var(--fg); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; padding-bottom: 60px; }
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
.item .del-btn { visibility: hidden; }
|
|
33
33
|
.item:hover .del-btn { visibility: visible; }
|
|
34
34
|
.dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: #9ca3af; }
|
|
35
|
-
.dot.running { background: var(--green); } .dot.crashed { background: var(--red); } .dot.stopped { background: #9ca3af; }
|
|
35
|
+
.dot.running { background: var(--green); } .dot.paused { background:#f59e0b; } .dot.crashed { background: var(--red); } .dot.stopped { background: #9ca3af; }
|
|
36
36
|
.name { font-weight: 600; }
|
|
37
37
|
.tag { font-size: 12px; color: var(--muted); background: #eef2ff; color: #3730a3; padding: 1px 7px; border-radius: 10px; }
|
|
38
38
|
.tag.persona { background: #fdf2f8; color: #9d174d; }
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
button.danger:hover { background: #fef2f2; }
|
|
50
50
|
button.sm { padding: 4px 9px; font-size: 12px; }
|
|
51
51
|
label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin: 10px 0 4px; }
|
|
52
|
-
input[type=text], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
52
|
+
input[type=text], input[type=number], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
53
53
|
input:focus, select:focus { outline: 2px solid #bfdbfe; border-color: var(--accent); }
|
|
54
54
|
.edit { padding: 14px 16px; background: #fff; border-top: 1px solid var(--border); }
|
|
55
55
|
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
@@ -63,6 +63,19 @@
|
|
|
63
63
|
.msg { font-size: 12px; margin-left: 4px; }
|
|
64
64
|
.msg .e { color: var(--red); } .msg .w { color: #b45309; } .msg .s { color: var(--green); }
|
|
65
65
|
.hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
66
|
+
.impact { display:inline-block; font-size:11px; color:var(--muted); margin-left:6px; font-weight:400; }
|
|
67
|
+
.feedback { min-height:17px; font-size:12px; margin-top:3px; }
|
|
68
|
+
.feedback.error { color:var(--red); } .feedback.warning { color:var(--yellow); }
|
|
69
|
+
input.invalid, select.invalid { border-color:var(--red); background:#fef2f2; }
|
|
70
|
+
button:disabled { opacity:.5; cursor:not-allowed; }
|
|
71
|
+
.settings-section { border-bottom:1px solid var(--border); background:#fff; }
|
|
72
|
+
.settings-section:last-child { border-bottom:none; }
|
|
73
|
+
.settings-section > summary { cursor:pointer; list-style:none; padding:12px 16px; font-weight:650; background:#f9fafb; }
|
|
74
|
+
.settings-section > summary::-webkit-details-marker { display:none; }
|
|
75
|
+
.settings-section > summary::before { content:"▶"; display:inline-block; width:18px; color:var(--muted); font-size:10px; }
|
|
76
|
+
.settings-section[open] > summary::before { content:"▼"; }
|
|
77
|
+
.section-body { padding:0 0 2px; }
|
|
78
|
+
.inherit-row { display:flex; gap:8px; align-items:center; margin-top:6px; color:var(--muted); font-size:12px; }
|
|
66
79
|
.adv-body { display: none; padding: 14px 16px; }
|
|
67
80
|
.adv-body.open { display: block; }
|
|
68
81
|
.toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
|
@@ -84,6 +97,16 @@
|
|
|
84
97
|
.help-panel li { font-size: 13px; margin: 3px 0; }
|
|
85
98
|
.help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--muted); font-size: 22px; line-height: 1; background: none; border: none; }
|
|
86
99
|
.help-panel .tip { margin-top: 16px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
|
|
100
|
+
@media (max-width: 640px) {
|
|
101
|
+
header { padding:10px 12px; flex-wrap:wrap; } header h1 { font-size:16px; }
|
|
102
|
+
main { padding:12px 10px; } section { margin-bottom:18px; }
|
|
103
|
+
.grid2 { grid-template-columns:1fr; gap:0; }
|
|
104
|
+
.item { padding:9px 10px; } .item-row { flex-wrap:wrap; }
|
|
105
|
+
.item-row .spacer { flex-basis:100%; height:0; }
|
|
106
|
+
.sub { margin-left:0; } .field { padding:10px 12px; }
|
|
107
|
+
#agentSearch { width:130px !important; margin-right:0 !important; }
|
|
108
|
+
button { min-height:36px; } button.sm { min-height:32px; }
|
|
109
|
+
}
|
|
87
110
|
</style>
|
|
88
111
|
</head>
|
|
89
112
|
<body>
|
|
@@ -153,6 +176,7 @@
|
|
|
153
176
|
backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
|
|
154
177
|
description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
|
|
155
178
|
runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
|
|
179
|
+
detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
|
|
156
180
|
language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
|
|
157
181
|
classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
|
|
158
182
|
tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
|
|
@@ -170,6 +194,7 @@
|
|
|
170
194
|
backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
|
|
171
195
|
description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
|
|
172
196
|
runtimeResources: "執行環境與資源", autoPause: "閒置後自動暫停", minutes: "分鐘", inheritDefault: "沿用全域預設值",
|
|
197
|
+
detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
|
|
173
198
|
language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
|
|
174
199
|
classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
|
|
175
200
|
tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
|
|
@@ -253,6 +278,14 @@
|
|
|
253
278
|
else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
|
|
254
279
|
elm.innerHTML = html;
|
|
255
280
|
}
|
|
281
|
+
function showBanner(text, error = false) {
|
|
282
|
+
const b = $("banner");
|
|
283
|
+
b.style.display = "block";
|
|
284
|
+
b.style.background = error ? "#fef2f2" : "#ecfdf5";
|
|
285
|
+
b.style.color = error ? "var(--red)" : "var(--green)";
|
|
286
|
+
b.textContent = text;
|
|
287
|
+
setTimeout(() => { b.style.display = "none"; }, 6000);
|
|
288
|
+
}
|
|
256
289
|
|
|
257
290
|
// ── State ──
|
|
258
291
|
const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false };
|
|
@@ -271,6 +304,25 @@
|
|
|
271
304
|
redraw(); return box;
|
|
272
305
|
}
|
|
273
306
|
function select(value, options) { const s = el("select"); for (const o of options) { const opt = el("option", {}, o); opt.value = o; if (o === value) opt.selected = true; s.append(opt); } return s; }
|
|
307
|
+
const impact = (kind) => el("span", { class: "impact" }, kind === "now" ? "⚡ immediate" : kind === "fleet" ? "🔄🔄 restart fleet" : "🔄 restart instance");
|
|
308
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
309
|
+
function overrideControl(inst, key, input, inheritedValue, impactKind = "instance") {
|
|
310
|
+
const inherited = !hasOwn(inst, key);
|
|
311
|
+
const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
|
|
312
|
+
input.disabled = inherited;
|
|
313
|
+
if (inherited && inheritedValue !== undefined) {
|
|
314
|
+
if (input.type === "checkbox") input.checked = !!inheritedValue;
|
|
315
|
+
else input.value = inheritedValue;
|
|
316
|
+
}
|
|
317
|
+
toggle.onchange = () => { input.disabled = toggle.checked; };
|
|
318
|
+
return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactKind))) };
|
|
319
|
+
}
|
|
320
|
+
function setValidation(input, feedback, error, warning) {
|
|
321
|
+
input.classList.toggle("invalid", !!error);
|
|
322
|
+
feedback.className = "feedback" + (error ? " error" : warning ? " warning" : "");
|
|
323
|
+
feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
|
|
324
|
+
return !error;
|
|
325
|
+
}
|
|
274
326
|
|
|
275
327
|
// ── My Agents ──
|
|
276
328
|
let editing = null;
|
|
@@ -283,12 +335,13 @@
|
|
|
283
335
|
}
|
|
284
336
|
// Display name: profile display_name → else strip the -t<topicId> suffix.
|
|
285
337
|
const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
|
|
286
|
-
const STATUS_TEXT = { running: "🟢 Running", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
|
|
287
|
-
const STATUS_ORDER = { running: 0,
|
|
338
|
+
const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
|
|
339
|
+
const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
|
|
288
340
|
const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
|
|
289
341
|
function compactRow(name, inst) {
|
|
290
342
|
const st = state.live[name]?.status || "stopped";
|
|
291
343
|
const running = st === "running";
|
|
344
|
+
const paused = st === "paused";
|
|
292
345
|
const short = shortName(name, inst);
|
|
293
346
|
const chTag = agentChannelLabel(inst);
|
|
294
347
|
const item = el("div", { class: "item" });
|
|
@@ -301,7 +354,9 @@
|
|
|
301
354
|
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
302
355
|
el("span", { class: "spacer" }),
|
|
303
356
|
el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
|
|
304
|
-
el("button", { class: "sm", onclick: () =>
|
|
357
|
+
running ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "pause") }, t("pause")) : null,
|
|
358
|
+
paused ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "wake") }, t("wake")) : null,
|
|
359
|
+
!paused ? el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")) : null,
|
|
305
360
|
el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
|
|
306
361
|
));
|
|
307
362
|
if (editing === name) item.append(agentEditForm(name, inst));
|
|
@@ -335,6 +390,7 @@
|
|
|
335
390
|
}
|
|
336
391
|
function agentEditForm(name, inst) {
|
|
337
392
|
const box = el("div", { class: "edit" });
|
|
393
|
+
const defaults = state.fleet.defaults || {};
|
|
338
394
|
const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
|
|
339
395
|
const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
|
|
340
396
|
const fWd = el("input", { type: "text", value: inst.working_directory || "" });
|
|
@@ -343,43 +399,77 @@
|
|
|
343
399
|
const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
|
|
344
400
|
const tags = [...(inst.tags || [])];
|
|
345
401
|
const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
402
|
+
const auto = overrideControl(inst, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: inst.auto_pause_after ?? defaults.auto_pause_after ?? 0, placeholder: "0 = disabled" }), defaults.auto_pause_after ?? 0);
|
|
403
|
+
const hangRaw = inst.hang_detector || {};
|
|
404
|
+
const hangHolder = hasOwn(hangRaw, "timeout_minutes") ? { value: hangRaw.timeout_minutes } : {};
|
|
405
|
+
const hang = overrideControl(hangHolder, "value", el("input", { type: "number", min: "0.1", step: "0.5", value: hangRaw.timeout_minutes ?? defaults.hang_detector?.timeout_minutes ?? 15 }), defaults.hang_detector?.timeout_minutes ?? 15);
|
|
406
|
+
const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
|
|
407
|
+
const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
|
|
408
|
+
const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
|
|
409
|
+
const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
|
|
410
|
+
const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
|
|
411
|
+
const fallbackValue = Array.isArray(inst.model_failover) ? (inst.model_failover[0] || "") : (defaults.model_failover?.[0] || "");
|
|
412
|
+
const failover = overrideControl(inst, "model_failover", el("input", { type: "text", value: fallbackValue, placeholder: "e.g. sonnet" }), defaults.model_failover?.[0] || "");
|
|
413
|
+
const displayName = overrideControl(inst, "display_name", el("input", { type: "text", value: inst.display_name ?? defaults.display_name ?? "", placeholder: name }), defaults.display_name || "");
|
|
414
|
+
const autoFeedback = el("div", { class: "feedback" });
|
|
415
|
+
const hangFeedback = el("div", { class: "feedback" });
|
|
416
|
+
const failoverFeedback = el("div", { class: "feedback" });
|
|
351
417
|
const msg = el("span", { class: "msg" });
|
|
418
|
+
const saveBtn = el("button", { class: "primary sm" }, t("save"));
|
|
419
|
+
const validate = () => {
|
|
420
|
+
const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
|
|
421
|
+
const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
|
|
422
|
+
const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? "Must be greater than 0" : "");
|
|
423
|
+
setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? "Empty list disables failover" : "");
|
|
424
|
+
saveBtn.disabled = !(validAuto && validHang);
|
|
425
|
+
return !saveBtn.disabled;
|
|
426
|
+
};
|
|
427
|
+
for (const ctl of [auto, hang, agentMode, toolSet, logLevel, lightweight, failover, displayName]) {
|
|
428
|
+
ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
|
|
429
|
+
}
|
|
430
|
+
saveBtn.onclick = async () => {
|
|
431
|
+
if (!validate()) return;
|
|
432
|
+
const patch = {
|
|
433
|
+
backend: fBackend.value, model: fModel.value || undefined,
|
|
434
|
+
working_directory: fWd.value, channel_id: fChan.value || undefined,
|
|
435
|
+
description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
|
|
436
|
+
tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
|
|
437
|
+
auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
|
|
438
|
+
hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
|
|
439
|
+
agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
|
|
440
|
+
tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
|
|
441
|
+
log_level: logLevel.toggle.checked ? null : logLevel.input.value,
|
|
442
|
+
lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
|
|
443
|
+
model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
|
|
444
|
+
display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
|
|
445
|
+
};
|
|
446
|
+
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
447
|
+
resultMsg(msg, res); if (res.ok) { await reload(); showBanner(`✓ ${t("affected")}: ${name} — 🔄 restart instance`); }
|
|
448
|
+
};
|
|
352
449
|
box.append(
|
|
353
450
|
el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
|
|
354
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
|
|
355
|
-
el("label", {}, t("workingDir")), fWd,
|
|
356
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
|
|
357
|
-
el("label", {}, t("systemPrompt")), fSys,
|
|
358
|
-
el("label", {}, t("tags")), chipList(tags, null, "tag"),
|
|
359
|
-
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
|
|
451
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend"), impact("instance")), fBackend), el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
|
|
452
|
+
el("label", {}, t("workingDir"), impact("instance")), fWd,
|
|
453
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding"), impact("fleet")), fChan), el("div", {}, el("label", {}, t("description"), impact("now")), fDesc)),
|
|
454
|
+
el("label", {}, t("systemPrompt"), impact("instance")), fSys,
|
|
455
|
+
el("label", {}, t("tags"), impact("now")), chipList(tags, null, "tag"),
|
|
456
|
+
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("fleet")),
|
|
360
457
|
el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, t("runtimeResources")),
|
|
361
458
|
el("div", { class: "grid2" },
|
|
362
|
-
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`),
|
|
363
|
-
el("
|
|
459
|
+
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`), auto.row, autoFeedback),
|
|
460
|
+
el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`), hang.row, hangFeedback),
|
|
461
|
+
el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
|
|
462
|
+
el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
|
|
463
|
+
el("div", {}, el("label", {}, t("logLevel")), logLevel.row),
|
|
464
|
+
el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
|
|
465
|
+
el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback),
|
|
466
|
+
el("div", {}, el("label", {}, t("displayName")), displayName.row)),
|
|
364
467
|
el("div", { class: "actions" },
|
|
365
|
-
|
|
366
|
-
const autoPause = Number(fAutoPause.value);
|
|
367
|
-
if (!fAutoPauseInherit.checked && (!Number.isFinite(autoPause) || autoPause < 0)) {
|
|
368
|
-
msg.innerHTML = `<span class="e">✗ ${esc(t("autoPause"))}: ≥ 0</span>`; return;
|
|
369
|
-
}
|
|
370
|
-
const patch = {
|
|
371
|
-
backend: fBackend.value, model: fModel.value || undefined,
|
|
372
|
-
working_directory: fWd.value, channel_id: fChan.value || undefined,
|
|
373
|
-
description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
|
|
374
|
-
tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
|
|
375
|
-
auto_pause_after: fAutoPauseInherit.checked ? null : autoPause,
|
|
376
|
-
};
|
|
377
|
-
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
378
|
-
resultMsg(msg, res); if (res.ok) await reload();
|
|
379
|
-
} }, t("save")),
|
|
468
|
+
saveBtn,
|
|
380
469
|
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
|
|
381
470
|
msg),
|
|
382
471
|
);
|
|
472
|
+
validate();
|
|
383
473
|
return box;
|
|
384
474
|
}
|
|
385
475
|
async function toggleAgent(name, running) {
|
|
@@ -387,6 +477,11 @@
|
|
|
387
477
|
else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
388
478
|
setTimeout(reloadLive, 800);
|
|
389
479
|
}
|
|
480
|
+
async function pauseWakeAgent(name, action) {
|
|
481
|
+
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
|
|
482
|
+
if (!res.ok) alert(res.body?.error || `${action} failed`);
|
|
483
|
+
await reloadLive();
|
|
484
|
+
}
|
|
390
485
|
async function delAgent(name) {
|
|
391
486
|
if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
|
|
392
487
|
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
@@ -415,6 +510,7 @@
|
|
|
415
510
|
}
|
|
416
511
|
|
|
417
512
|
// ── Bots ──
|
|
513
|
+
let editingBot = null;
|
|
418
514
|
function renderBots() {
|
|
419
515
|
const host = $("bots"); host.innerHTML = ""; const chs = channels();
|
|
420
516
|
if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
|
|
@@ -428,12 +524,41 @@
|
|
|
428
524
|
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
429
525
|
el("span", { class: "spacer" }),
|
|
430
526
|
el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : "—"),
|
|
527
|
+
el("button", { class: "sm ghost", onclick: () => { editingBot = editingBot === i ? null : i; renderBots(); } }, editingBot === i ? t("close") : t("edit")),
|
|
431
528
|
el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
|
|
432
529
|
));
|
|
530
|
+
if (editingBot === i) item.append(botEditForm(ch, i));
|
|
433
531
|
host.append(item);
|
|
434
532
|
});
|
|
435
533
|
}
|
|
436
534
|
|
|
535
|
+
function botEditForm(ch, i) {
|
|
536
|
+
ch.access = ch.access || { mode: "locked", allowed_users: [] };
|
|
537
|
+
ch.access.allowed_users = ch.access.allowed_users || [];
|
|
538
|
+
const box = el("div", { class: "edit" });
|
|
539
|
+
const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
|
|
540
|
+
const users = [...ch.access.allowed_users];
|
|
541
|
+
const msg = el("span", { class: "msg" });
|
|
542
|
+
box.append(
|
|
543
|
+
el("div", { class: "grid2" },
|
|
544
|
+
el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode),
|
|
545
|
+
el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, null, "user id"))),
|
|
546
|
+
el("div", { class: "actions" },
|
|
547
|
+
el("button", { class: "primary sm", onclick: async () => {
|
|
548
|
+
const chs = channels();
|
|
549
|
+
chs[i] = { ...chs[i], access: { ...chs[i].access, mode: fMode.value, allowed_users: users } };
|
|
550
|
+
const res = await saveChannels(msg);
|
|
551
|
+
if (res.ok) {
|
|
552
|
+
const affected = Object.keys(state.fleet.instances || {});
|
|
553
|
+
editingBot = null; await reload();
|
|
554
|
+
showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : chLabel(i) + " bot"} — 🔄🔄 restart fleet`);
|
|
555
|
+
}
|
|
556
|
+
} }, t("save")),
|
|
557
|
+
el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
|
|
558
|
+
);
|
|
559
|
+
return box;
|
|
560
|
+
}
|
|
561
|
+
|
|
437
562
|
// ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
|
|
438
563
|
function renderClassic() {
|
|
439
564
|
const host = $("classicList"); if (!host) return; host.innerHTML = "";
|
|
@@ -502,6 +627,8 @@
|
|
|
502
627
|
state.fleet.defaults = state.fleet.defaults || {};
|
|
503
628
|
state.classic.defaults = state.classic.defaults || {};
|
|
504
629
|
const d = state.fleet.defaults, c = state.classic.defaults;
|
|
630
|
+
const hang = d.hang_detector || {};
|
|
631
|
+
const startup = d.startup || {};
|
|
505
632
|
const primary = channels()[0];
|
|
506
633
|
if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
|
|
507
634
|
const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
|
|
@@ -509,31 +636,93 @@
|
|
|
509
636
|
|
|
510
637
|
const fBackend = select(d.backend || "claude-code", BACKENDS);
|
|
511
638
|
const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
|
|
639
|
+
const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
|
|
640
|
+
const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
|
|
641
|
+
const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
|
|
512
642
|
const fLocale = el("select");
|
|
513
643
|
const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
|
|
644
|
+
const fHangEnabled = el("input", { type: "checkbox" }); fHangEnabled.checked = hang.enabled !== false;
|
|
645
|
+
const fHangTimeout = el("input", { type: "number", min: "0.1", step: "0.5", value: hang.timeout_minutes ?? 15 });
|
|
646
|
+
const fConcurrency = el("input", { type: "number", min: "1", max: "20", step: "1", value: startup.concurrency ?? 10 });
|
|
647
|
+
const fStagger = el("input", { type: "number", min: "0", max: "30000", step: "100", value: startup.stagger_delay_ms ?? 500 });
|
|
514
648
|
[["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]].forEach(([v, lbl]) => { const o = el("option", {}, lbl); o.value = v; if ((d.locale || "") === v) o.selected = true; fLocale.append(o); });
|
|
515
649
|
const msg = el("span", { class: "msg" });
|
|
650
|
+
const autoFeedback = el("div", { class: "feedback" });
|
|
651
|
+
const hangFeedback = el("div", { class: "feedback" });
|
|
652
|
+
const concurrencyFeedback = el("div", { class: "feedback" });
|
|
653
|
+
const staggerFeedback = el("div", { class: "feedback" });
|
|
654
|
+
const saveBtn = el("button", { class: "primary" }, t("save"));
|
|
655
|
+
const validate = () => {
|
|
656
|
+
const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
|
|
657
|
+
const concurrency = Number(fConcurrency.value), stagger = Number(fStagger.value);
|
|
658
|
+
const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? "Must be 0 or greater" : "");
|
|
659
|
+
const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? "Must be greater than 0" : "");
|
|
660
|
+
const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? "Must be an integer from 1 to 20" : "", concurrency > 4 ? "May exceed CPU capacity on smaller hosts" : "");
|
|
661
|
+
const validStagger = setValidation(fStagger, staggerFeedback, !Number.isFinite(stagger) || stagger < 0 || stagger > 30000 ? "Must be between 0 and 30000" : "", stagger < 100 ? "Very low values can cause startup spikes" : "");
|
|
662
|
+
saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
|
|
663
|
+
return !saveBtn.disabled;
|
|
664
|
+
};
|
|
665
|
+
for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
|
|
666
|
+
input.addEventListener("input", validate); input.addEventListener("change", validate);
|
|
667
|
+
}
|
|
668
|
+
saveBtn.onclick = async () => {
|
|
669
|
+
if (!validate()) return;
|
|
670
|
+
const defaultsPatch = {
|
|
671
|
+
backend: fBackend.value,
|
|
672
|
+
model: fModel.value || undefined,
|
|
673
|
+
locale: fLocale.value,
|
|
674
|
+
auto_pause_after: Number(fAutoPause.value),
|
|
675
|
+
agent_mode: fAgentMode.value,
|
|
676
|
+
tool_set: fToolSet.value,
|
|
677
|
+
log_level: fLogLevel.value,
|
|
678
|
+
hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
|
|
679
|
+
startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
|
|
680
|
+
};
|
|
681
|
+
const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) });
|
|
682
|
+
let r2 = { ok: true, body: {} };
|
|
683
|
+
if (primary) r2 = await saveChannels();
|
|
684
|
+
const r3 = await api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify({ backend: c.backend, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds }) });
|
|
685
|
+
const bad = [r1, r2, r3].find(r => !r.ok);
|
|
686
|
+
resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
|
|
687
|
+
if (!bad) {
|
|
688
|
+
const affected = Object.keys(state.fleet.instances || {});
|
|
689
|
+
await reload();
|
|
690
|
+
showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : "none"} — 🔄 restart instances; startup changes 🔄🔄 restart fleet`);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const section = (title, open, ...children) => {
|
|
695
|
+
const details = el("details", { class: "settings-section" }); details.open = open;
|
|
696
|
+
details.append(el("summary", {}, title), el("div", { class: "section-body" }, ...children));
|
|
697
|
+
return details;
|
|
698
|
+
};
|
|
516
699
|
host.append(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
700
|
+
section(t("runtimeResources"), true,
|
|
701
|
+
el("div", { class: "grid2" },
|
|
702
|
+
el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), fAutoPause, autoFeedback),
|
|
703
|
+
el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("instance")), fToolSet),
|
|
704
|
+
el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("instance")), fLogLevel))),
|
|
705
|
+
section(t("backendSection"), true,
|
|
706
|
+
el("div", { class: "grid2" },
|
|
707
|
+
el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("instance")), fBackend),
|
|
708
|
+
el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("instance")), fModel),
|
|
709
|
+
el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("instance")), fAgentMode))),
|
|
710
|
+
section(t("detection"), false,
|
|
711
|
+
el("div", { class: "grid2" },
|
|
712
|
+
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("instance"))),
|
|
713
|
+
el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance")), fHangTimeout, hangFeedback))),
|
|
714
|
+
section(t("startup"), false,
|
|
715
|
+
el("div", { class: "grid2" },
|
|
716
|
+
el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
|
|
717
|
+
el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
|
|
718
|
+
section(t("advancedSection"), false,
|
|
719
|
+
el("div", { class: "field" }, el("label", {}, t("language"), impact("now")), fLocale),
|
|
720
|
+
el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, null, "user id")),
|
|
721
|
+
el("div", { class: "field" }, el("label", {}, t("classicAdmins"), impact("fleet")), chipList(c.admin_users, null, "user id")),
|
|
722
|
+
el("div", { class: "field" }, el("label", {}, t("allowedGuilds"), impact("fleet")), chipList(c.allowed_guilds, null, "guild id"))),
|
|
723
|
+
el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
|
|
536
724
|
);
|
|
725
|
+
validate();
|
|
537
726
|
}
|
|
538
727
|
|
|
539
728
|
// ── What's New ──
|