feat: service console auto-connect, settings panel, and batch of enhancements
- Auto-connect WebSocket on page load in service console - Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.) - UpdateConfig/ConfigUpdated protocol messages for remote config save - save_to_path() for SgClawSettings serialization - ConfigUpdated handler in sg_claw_client binary - Protocol serialization tests for new message types - HTML test assertions for auto-connect and settings UI - Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -309,6 +309,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings modal elements */
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border-color: rgba(15, 118, 110, 0.5);
|
||||
box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
body {
|
||||
padding: 16px;
|
||||
@@ -347,6 +365,7 @@
|
||||
<input id="wsUrl" value="ws://127.0.0.1:42321" />
|
||||
</div>
|
||||
<button id="connectBtn" class="ghost-btn">连接</button>
|
||||
<button id="settingsBtn" class="ghost-btn" style="margin-top: 8px;">⚙ 设置</button>
|
||||
|
||||
<p class="section-label" style="margin-top: 26px;">Composer</p>
|
||||
<div class="field">
|
||||
@@ -372,6 +391,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settingsModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
||||
<div style="background: var(--panel); border-radius: 20px; padding: 28px; width: min(520px, 90%); max-height: 85vh; overflow-y: auto; box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 20px; font-size: 1.2rem;">sgClaw 配置</h3>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingApiKey">API 密钥 *</label>
|
||||
<input id="settingApiKey" type="password" placeholder="输入模型 API 密钥" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingBaseUrl">模型服务地址 *</label>
|
||||
<input id="settingBaseUrl" type="url" placeholder="例如:https://api.deepseek.com" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingModel">模型名称 *</label>
|
||||
<input id="settingModel" type="text" placeholder="例如:deepseek-chat" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingSkillsDir">Skills 目录路径</label>
|
||||
<input id="settingSkillsDir" type="text" placeholder="例如:D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging/skills" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingDirectSubmitSkill">直接提交技能</label>
|
||||
<input id="settingDirectSubmitSkill" type="text" placeholder="例如:tq-lineloss-report.collect_lineloss" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingRuntimeProfile">运行模式</label>
|
||||
<select id="settingRuntimeProfile" style="width: 100%; border: 1px solid var(--line); border-radius: 16px; padding: 14px 16px; background: rgba(255, 255, 255, 0.92); color: var(--text); font: inherit;">
|
||||
<option value="browser-attached">browser-attached</option>
|
||||
<option value="browser-heavy">browser-heavy</option>
|
||||
<option value="general-assistant">general-assistant</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="settingBrowserBackend">浏览器后端</label>
|
||||
<select id="settingBrowserBackend" style="width: 100%; border: 1px solid var(--line); border-radius: 16px; padding: 14px 16px; background: rgba(255, 255, 255, 0.92); color: var(--text); font: inherit;">
|
||||
<option value="super-rpa">super-rpa</option>
|
||||
<option value="agent-browser">agent-browser</option>
|
||||
<option value="rust-native">rust-native</option>
|
||||
<option value="computer-use">computer-use</option>
|
||||
<option value="auto">auto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="settingsValidation" style="color: var(--error); font-size: 0.92rem; min-height: 1.4em; margin: 10px 0;"></div>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-top: 16px;">
|
||||
<button id="settingsSaveBtn" class="primary-btn" style="flex: 1;">保存</button>
|
||||
<button id="settingsCancelBtn" class="ghost-btn" style="flex: 1;">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const defaultWsUrl = "ws://127.0.0.1:42321";
|
||||
const elements = {
|
||||
@@ -592,6 +670,9 @@
|
||||
break;
|
||||
case "pong":
|
||||
break;
|
||||
case "config_updated":
|
||||
handleConfigResponse(message);
|
||||
break;
|
||||
default:
|
||||
appendRow("error", "unknown service message: " + event.data);
|
||||
}
|
||||
@@ -627,6 +708,128 @@
|
||||
});
|
||||
|
||||
updateUiState();
|
||||
|
||||
// Auto-connect on page load
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
connectOrDisconnectService(true);
|
||||
});
|
||||
|
||||
// Settings modal state
|
||||
const settingsElements = {
|
||||
modal: document.getElementById("settingsModal"),
|
||||
apiKey: document.getElementById("settingApiKey"),
|
||||
baseUrl: document.getElementById("settingBaseUrl"),
|
||||
model: document.getElementById("settingModel"),
|
||||
skillsDir: document.getElementById("settingSkillsDir"),
|
||||
directSubmitSkill: document.getElementById("settingDirectSubmitSkill"),
|
||||
runtimeProfile: document.getElementById("settingRuntimeProfile"),
|
||||
browserBackend: document.getElementById("settingBrowserBackend"),
|
||||
validation: document.getElementById("settingsValidation"),
|
||||
saveBtn: document.getElementById("settingsSaveBtn"),
|
||||
cancelBtn: document.getElementById("settingsCancelBtn"),
|
||||
};
|
||||
let settingsOpenBtn = null;
|
||||
|
||||
function openSettingsModal() {
|
||||
settingsElements.apiKey.value = "";
|
||||
settingsElements.baseUrl.value = "";
|
||||
settingsElements.model.value = "";
|
||||
settingsElements.skillsDir.value = "";
|
||||
settingsElements.directSubmitSkill.value = "";
|
||||
settingsElements.runtimeProfile.value = "browser-attached";
|
||||
settingsElements.browserBackend.value = "super-rpa";
|
||||
settingsElements.validation.textContent = "";
|
||||
settingsElements.modal.style.display = "flex";
|
||||
}
|
||||
|
||||
function closeSettingsModal() {
|
||||
settingsElements.modal.style.display = "none";
|
||||
}
|
||||
|
||||
function validateSettings() {
|
||||
const apiKey = settingsElements.apiKey.value.trim();
|
||||
const baseUrl = settingsElements.baseUrl.value.trim();
|
||||
const model = settingsElements.model.value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
return "API 密钥不能为空";
|
||||
}
|
||||
if (!model) {
|
||||
return "模型名称不能为空";
|
||||
}
|
||||
if (!baseUrl) {
|
||||
return "模型服务地址不能为空";
|
||||
}
|
||||
try {
|
||||
new URL(baseUrl);
|
||||
} catch {
|
||||
return "模型服务地址格式无效,请输入有效的 URL";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const error = validateSettings();
|
||||
if (error) {
|
||||
settingsElements.validation.textContent = error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
settingsElements.validation.textContent = "请先连接服务";
|
||||
return;
|
||||
}
|
||||
|
||||
settingsElements.validation.textContent = "";
|
||||
settingsElements.saveBtn.disabled = true;
|
||||
settingsElements.saveBtn.textContent = "保存中...";
|
||||
|
||||
const config = {
|
||||
apiKey: settingsElements.apiKey.value.trim(),
|
||||
baseUrl: settingsElements.baseUrl.value.trim(),
|
||||
model: settingsElements.model.value.trim(),
|
||||
};
|
||||
|
||||
const skillsDir = settingsElements.skillsDir.value.trim();
|
||||
if (skillsDir) config.skillsDir = skillsDir;
|
||||
|
||||
const directSubmitSkill = settingsElements.directSubmitSkill.value.trim();
|
||||
if (directSubmitSkill) config.directSubmitSkill = directSubmitSkill;
|
||||
|
||||
config.runtimeProfile = settingsElements.runtimeProfile.value;
|
||||
config.browserBackend = settingsElements.browserBackend.value;
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
type: "update_config",
|
||||
config,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleConfigResponse(message) {
|
||||
settingsElements.saveBtn.disabled = false;
|
||||
settingsElements.saveBtn.textContent = "保存";
|
||||
|
||||
if (message.success) {
|
||||
settingsElements.validation.textContent = message.message;
|
||||
settingsElements.validation.style.color = "var(--success)";
|
||||
setTimeout(closeSettingsModal, 2000);
|
||||
} else {
|
||||
settingsElements.validation.textContent = message.message;
|
||||
settingsElements.validation.style.color = "var(--error)";
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for settings
|
||||
settingsOpenBtn = document.getElementById("settingsBtn");
|
||||
settingsOpenBtn.addEventListener("click", openSettingsModal);
|
||||
settingsElements.cancelBtn.addEventListener("click", closeSettingsModal);
|
||||
settingsElements.saveBtn.addEventListener("click", saveSettings);
|
||||
|
||||
settingsElements.modal.addEventListener("click", (e) => {
|
||||
if (e.target === settingsElements.modal) {
|
||||
closeSettingsModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user