mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-17 17:03:22 +08:00
fix(desktop): external gateway mode and start script alignment
- Add desktop env for non-embedded gateway (OCLAW_DESKTOP_EMBED_BACKEND, base URL, skip channel). - Wire start_all/start_desktop to pass gateway URL when keeping existing gateway; fix root start_all flags. - Prefer electron.cmd for background desktop launch; admin static theme presets and chat boot timeouts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
19e51e2c12
commit
16169b3de2
12 changed files with 566 additions and 45 deletions
119
desktop/main.js
119
desktop/main.js
|
|
@ -96,6 +96,47 @@ function pickPort(preferredPort) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envFlagOff(v) {
|
||||||
|
return ["0", "false", "no", "off"].includes(String(v ?? "").trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** When false, connect to OCLAW_DESKTOP_GATEWAY_BASE_URL (or default :8787) instead of spawning a child gateway. */
|
||||||
|
function shouldEmbedBackend() {
|
||||||
|
if (envFlagOff(process.env.OCLAW_DESKTOP_EMBED_BACKEND)) return false;
|
||||||
|
return !String(process.env.OCLAW_DESKTOP_GATEWAY_BASE_URL || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGatewayBaseUrl(raw) {
|
||||||
|
const s = String(raw || "").trim();
|
||||||
|
if (!s) throw new Error("gateway base URL is empty");
|
||||||
|
const u = new URL(s);
|
||||||
|
const host = u.hostname || DEFAULT_HOST;
|
||||||
|
const port = u.port ? parseInt(u.port, 10) : u.protocol === "https:" ? 443 : 80;
|
||||||
|
const baseUrl = `${u.protocol}//${u.host}`;
|
||||||
|
return { host, port, baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachToExistingGateway() {
|
||||||
|
const raw =
|
||||||
|
String(process.env.OCLAW_DESKTOP_GATEWAY_BASE_URL || "").trim() ||
|
||||||
|
`http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
||||||
|
const parsed = normalizeGatewayBaseUrl(raw);
|
||||||
|
runtimeState = {
|
||||||
|
host: parsed.host,
|
||||||
|
port: parsed.port,
|
||||||
|
baseUrl: parsed.baseUrl,
|
||||||
|
pythonBin: resolvePythonBin(),
|
||||||
|
externalAttach: true,
|
||||||
|
};
|
||||||
|
const deadlineAtMs = Date.now() + STARTUP_TIMEOUT_MS;
|
||||||
|
await waitForBackend(`${runtimeState.baseUrl}/health`, deadlineAtMs);
|
||||||
|
return runtimeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipDesktopChannel() {
|
||||||
|
return ["1", "true", "yes", "on"].includes(String(process.env.OCLAW_DESKTOP_SKIP_CHANNEL || "").trim().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
function waitForBackend(url, deadlineAtMs) {
|
function waitForBackend(url, deadlineAtMs) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const attempt = () => {
|
const attempt = () => {
|
||||||
|
|
@ -126,6 +167,22 @@ function waitForBackend(url, deadlineAtMs) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Avoid hanging forever on session.clearStorageData / clearCache (seen on some Windows setups). */
|
||||||
|
function awaitWithTimeout(promise, timeoutMs, logLabel) {
|
||||||
|
const ms = Math.max(200, Number(timeoutMs) || 8000);
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
logDesktop(`${logLabel}:timeout_after_${ms}ms`);
|
||||||
|
} catch (_) {}
|
||||||
|
resolve();
|
||||||
|
}, ms);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
function loadingHtml(text) {
|
function loadingHtml(text) {
|
||||||
const safe = String(text || "").replace(/</g, "<").replace(/>/g, ">");
|
const safe = String(text || "").replace(/</g, "<").replace(/>/g, ">");
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
|
|
@ -190,7 +247,7 @@ async function startBackendProcess() {
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
runtimeState = { host, port, baseUrl, pythonBin };
|
runtimeState = { host, port, baseUrl, pythonBin, externalAttach: false };
|
||||||
|
|
||||||
backendProc.stdout.on("data", (chunk) => {
|
backendProc.stdout.on("data", (chunk) => {
|
||||||
logStream.write(chunk);
|
logStream.write(chunk);
|
||||||
|
|
@ -513,8 +570,13 @@ async function boot() {
|
||||||
app.setName(APP_DISPLAY_NAME);
|
app.setName(APP_DISPLAY_NAME);
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
// Show window immediately to avoid "black screen" during backend startup.
|
// Show window immediately to avoid "black screen" during backend startup.
|
||||||
|
const embedGateway = shouldEmbedBackend();
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(loadingHtml("Starting local gateway…"))}`);
|
await mainWindow.loadURL(
|
||||||
|
`data:text/html;charset=utf-8,${encodeURIComponent(
|
||||||
|
loadingHtml(embedGateway ? "Starting local gateway…" : "Connecting to existing gateway…"),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
try {
|
try {
|
||||||
|
|
@ -546,8 +608,13 @@ async function boot() {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
if (embedGateway) {
|
||||||
await startBackendProcess();
|
await startBackendProcess();
|
||||||
logDesktop(`boot:backendReady:${Date.now() - t0}ms`);
|
logDesktop(`boot:backendReady:embedded:${Date.now() - t0}ms`);
|
||||||
|
} else {
|
||||||
|
await attachToExistingGateway();
|
||||||
|
logDesktop(`boot:backendReady:external:${Date.now() - t0}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL(
|
await mainWindow.loadURL(
|
||||||
|
|
@ -555,7 +622,11 @@ async function boot() {
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
const channelOk = await startChannelProcess();
|
let channelOk = true;
|
||||||
|
if (shouldSkipDesktopChannel()) {
|
||||||
|
logDesktop(`boot:channel:skipped:${Date.now() - t0}ms`);
|
||||||
|
} else {
|
||||||
|
channelOk = await startChannelProcess();
|
||||||
logDesktop(`boot:channel:${channelOk ? "ok" : "fail"}:${Date.now() - t0}ms`);
|
logDesktop(`boot:channel:${channelOk ? "ok" : "fail"}:${Date.now() - t0}ms`);
|
||||||
if (!channelOk && !channelStartWarningShown) {
|
if (!channelOk && !channelStartWarningShown) {
|
||||||
channelStartWarningShown = true;
|
channelStartWarningShown = true;
|
||||||
|
|
@ -566,8 +637,18 @@ async function boot() {
|
||||||
detail: "你可以在 Admin -> 运行时中查看并重试服务。",
|
detail: "你可以在 Admin -> 运行时中查看并重试服务。",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await clearStoragePromise;
|
}
|
||||||
await clearCachePromise;
|
try {
|
||||||
|
await mainWindow.loadURL(
|
||||||
|
`data:text/html;charset=utf-8,${encodeURIComponent(
|
||||||
|
loadingHtml(
|
||||||
|
`Backend ready at ${runtimeState.baseUrl}\nClearing login/session storage…\nThen opening chat…`,
|
||||||
|
),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
// Do not block the window forever if Electron's clearStorageData/clearCache never settle.
|
||||||
|
await awaitWithTimeout(Promise.all([clearStoragePromise, clearCachePromise]), 12000, "boot:storageCleanup");
|
||||||
// Desktop policy: always require fresh login on every app restart.
|
// Desktop policy: always require fresh login on every app restart.
|
||||||
logDesktop(`boot:loadChat:start:${Date.now() - t0}ms`);
|
logDesktop(`boot:loadChat:start:${Date.now() - t0}ms`);
|
||||||
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
|
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
|
||||||
|
|
@ -593,6 +674,28 @@ ipcMain.handle("desktop:getRuntimeInfo", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("desktop:restartBackend", async () => {
|
ipcMain.handle("desktop:restartBackend", async () => {
|
||||||
|
if (runtimeState && runtimeState.externalAttach) {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
await mainWindow.loadURL(
|
||||||
|
`data:text/html;charset=utf-8,${encodeURIComponent(loadingHtml("Reloading chat (external gateway)…"))}`,
|
||||||
|
);
|
||||||
|
mainWindow.show();
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await waitForBackend(`${runtimeState.baseUrl}/health`, Date.now() + 15000);
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await awaitWithTimeout(mainWindow.webContents.session.clearCache(), 8000, "restartBackend:clearCache");
|
||||||
|
} catch (_) {}
|
||||||
|
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
|
||||||
|
try {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
try {
|
try {
|
||||||
await mainWindow.loadURL(
|
await mainWindow.loadURL(
|
||||||
|
|
@ -607,10 +710,12 @@ ipcMain.handle("desktop:restartBackend", async () => {
|
||||||
await stopChannelProcess();
|
await stopChannelProcess();
|
||||||
await stopBackendProcess();
|
await stopBackendProcess();
|
||||||
await startBackendProcess();
|
await startBackendProcess();
|
||||||
|
if (!shouldSkipDesktopChannel()) {
|
||||||
await startChannelProcess();
|
await startChannelProcess();
|
||||||
|
}
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
try {
|
try {
|
||||||
await mainWindow.webContents.session.clearCache();
|
await awaitWithTimeout(mainWindow.webContents.session.clearCache(), 8000, "restartBackend:clearCache");
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
|
await mainWindow.loadURL(`${runtimeState.baseUrl}/chat?v=${Date.now()}`);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
51
interfaces/admin/static/admin-theme.js
Normal file
51
interfaces/admin/static/admin-theme.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* Admin + standalone /chat: restore palette from localStorage before first paint. */
|
||||||
|
(function () {
|
||||||
|
const STORAGE_KEY = "oclaw_admin_theme";
|
||||||
|
const THEMES = ["deepseek", "github", "nord", "dracula", "forest", "catppuccin", "light"];
|
||||||
|
const LEGIT = new Set(THEMES);
|
||||||
|
|
||||||
|
function applyAdminTheme(theme) {
|
||||||
|
const t = theme && LEGIT.has(theme) ? theme : "deepseek";
|
||||||
|
if (t === "deepseek") document.body.removeAttribute("data-admin-theme");
|
||||||
|
else document.body.setAttribute("data-admin-theme", t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAdminThemeFromStorage() {
|
||||||
|
let raw = "deepseek";
|
||||||
|
try {
|
||||||
|
raw = localStorage.getItem(STORAGE_KEY) || "deepseek";
|
||||||
|
} catch (_) {
|
||||||
|
raw = "deepseek";
|
||||||
|
}
|
||||||
|
if (!LEGIT.has(raw)) raw = "deepseek";
|
||||||
|
applyAdminTheme(raw);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistAdminTheme(theme) {
|
||||||
|
const t = theme && LEGIT.has(theme) ? theme : "deepseek";
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, t);
|
||||||
|
} catch (_) {}
|
||||||
|
applyAdminTheme(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAdminTheme() {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return v && LEGIT.has(v) ? v : "deepseek";
|
||||||
|
} catch (_) {
|
||||||
|
return "deepseek";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.OclawAdminTheme = {
|
||||||
|
STORAGE_KEY,
|
||||||
|
THEMES,
|
||||||
|
applyAdminTheme,
|
||||||
|
initAdminThemeFromStorage,
|
||||||
|
persistAdminTheme,
|
||||||
|
currentAdminTheme,
|
||||||
|
};
|
||||||
|
initAdminThemeFromStorage();
|
||||||
|
})();
|
||||||
|
|
@ -232,6 +232,14 @@ const I18N = {
|
||||||
"common.notFound": "未找到页面",
|
"common.notFound": "未找到页面",
|
||||||
"common.forbidden": "无权访问该页面。",
|
"common.forbidden": "无权访问该页面。",
|
||||||
"common.error": "错误",
|
"common.error": "错误",
|
||||||
|
"theme.label": "界面配色",
|
||||||
|
"theme.deepseek": "DeepSeek 默认",
|
||||||
|
"theme.github": "GitHub 暗色",
|
||||||
|
"theme.nord": "Nord",
|
||||||
|
"theme.dracula": "Dracula",
|
||||||
|
"theme.forest": "森绿",
|
||||||
|
"theme.catppuccin": "Catppuccin Mocha",
|
||||||
|
"theme.light": "浅色",
|
||||||
"lang.switch": "English",
|
"lang.switch": "English",
|
||||||
"memory.title": "记忆治理",
|
"memory.title": "记忆治理",
|
||||||
"memory.hits": "最近命中",
|
"memory.hits": "最近命中",
|
||||||
|
|
@ -665,6 +673,14 @@ const I18N = {
|
||||||
"common.notFound": "Not found",
|
"common.notFound": "Not found",
|
||||||
"common.forbidden": "You do not have access to this page.",
|
"common.forbidden": "You do not have access to this page.",
|
||||||
"common.error": "Error",
|
"common.error": "Error",
|
||||||
|
"theme.label": "Color theme",
|
||||||
|
"theme.deepseek": "DeepSeek (default)",
|
||||||
|
"theme.github": "GitHub Dark",
|
||||||
|
"theme.nord": "Nord",
|
||||||
|
"theme.dracula": "Dracula",
|
||||||
|
"theme.forest": "Forest",
|
||||||
|
"theme.catppuccin": "Catppuccin Mocha",
|
||||||
|
"theme.light": "Light",
|
||||||
"lang.switch": "中文",
|
"lang.switch": "中文",
|
||||||
"memory.title": "Memory Governance",
|
"memory.title": "Memory Governance",
|
||||||
"memory.hits": "Recent Hits",
|
"memory.hits": "Recent Hits",
|
||||||
|
|
@ -997,6 +1013,12 @@ function applyI18nStatic() {
|
||||||
});
|
});
|
||||||
const langBtn = document.getElementById("btnLang");
|
const langBtn = document.getElementById("btnLang");
|
||||||
if (langBtn) langBtn.textContent = t("lang.switch");
|
if (langBtn) langBtn.textContent = t("lang.switch");
|
||||||
|
const themeSel = document.getElementById("adminThemeSelect");
|
||||||
|
if (themeSel && window.OclawAdminTheme) {
|
||||||
|
try {
|
||||||
|
themeSel.value = OclawAdminTheme.currentAdminTheme();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLang() {
|
function toggleLang() {
|
||||||
|
|
@ -9164,6 +9186,14 @@ if (btnLangEl) btnLangEl.addEventListener("click", () => {
|
||||||
toggleLang();
|
toggleLang();
|
||||||
router();
|
router();
|
||||||
});
|
});
|
||||||
|
const adminThemeSelectEl = document.getElementById("adminThemeSelect");
|
||||||
|
if (adminThemeSelectEl && window.OclawAdminTheme) {
|
||||||
|
adminThemeSelectEl.addEventListener("change", () => {
|
||||||
|
try {
|
||||||
|
window.OclawAdminTheme.persistAdminTheme(adminThemeSelectEl.value);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
const btnBackChatEl = document.getElementById("btnBackChat");
|
const btnBackChatEl = document.getElementById("btnBackChat");
|
||||||
if (btnBackChatEl) {
|
if (btnBackChatEl) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
||||||
<link rel="stylesheet" href="/admin/assets/styles.css" />
|
<link rel="stylesheet" href="/admin/assets/styles.css" />
|
||||||
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
|
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
|
||||||
|
<link rel="stylesheet" href="/admin/assets/theme-presets.css" />
|
||||||
<style>
|
<style>
|
||||||
body.theme-ds-body.chat-standalone-page {
|
body.theme-ds-body.chat-standalone-page {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -561,9 +562,10 @@
|
||||||
onload="(function(){try{ if (window.__oclawHydrateMermaidAll) window.__oclawHydrateMermaidAll(); }catch(e){}})()"
|
onload="(function(){try{ if (window.__oclawHydrateMermaidAll) window.__oclawHydrateMermaidAll(); }catch(e){}})()"
|
||||||
></script>
|
></script>
|
||||||
<!-- Cache-bust for desktop webview: avoid stale chat.js -->
|
<!-- Cache-bust for desktop webview: avoid stale chat.js -->
|
||||||
|
<script defer src="/admin/assets/admin-theme.js"></script>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="/admin/assets/chat.js?v=20260501-1"
|
src="/admin/assets/chat.js?v=20260507-2"
|
||||||
onerror="(function(){var s=document.getElementById('chat-boot-splash');if(s){s.querySelector('.chat-boot-splash__title span:last-child').textContent='无法加载 chat.js';s.querySelector('.chat-boot-splash__muted').textContent='请确认网关已启动且 /admin/assets/chat.js 可访问。';}})()"
|
onerror="(function(){var s=document.getElementById('chat-boot-splash');if(s){s.querySelector('.chat-boot-splash__title span:last-child').textContent='无法加载 chat.js';s.querySelector('.chat-boot-splash__muted').textContent='请确认网关已启动且 /admin/assets/chat.js 可访问。';}})()"
|
||||||
></script>
|
></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,14 @@ const I18N = {
|
||||||
"auth.chatLoginDenied": "当前账号无法使用对话(请联系管理员)。",
|
"auth.chatLoginDenied": "当前账号无法使用对话(请联系管理员)。",
|
||||||
"auth.disabled": "账号已被禁用,请联系管理员",
|
"auth.disabled": "账号已被禁用,请联系管理员",
|
||||||
"common.error": "错误",
|
"common.error": "错误",
|
||||||
|
"theme.label": "界面配色",
|
||||||
|
"theme.deepseek": "DeepSeek 默认",
|
||||||
|
"theme.github": "GitHub 暗色",
|
||||||
|
"theme.nord": "Nord",
|
||||||
|
"theme.dracula": "Dracula",
|
||||||
|
"theme.forest": "森绿",
|
||||||
|
"theme.catppuccin": "Catppuccin Mocha",
|
||||||
|
"theme.light": "浅色",
|
||||||
"lang.switch": "English",
|
"lang.switch": "English",
|
||||||
"chat.imageViewerClose": "关闭",
|
"chat.imageViewerClose": "关闭",
|
||||||
"chat.imageViewerHint": "点击查看大图,空白处或 Esc 关闭",
|
"chat.imageViewerHint": "点击查看大图,空白处或 Esc 关闭",
|
||||||
|
|
@ -234,6 +242,14 @@ const I18N = {
|
||||||
"auth.chatLoginDenied": "This account cannot use chat (contact an admin).",
|
"auth.chatLoginDenied": "This account cannot use chat (contact an admin).",
|
||||||
"auth.disabled": "Account is disabled",
|
"auth.disabled": "Account is disabled",
|
||||||
"common.error": "Error",
|
"common.error": "Error",
|
||||||
|
"theme.label": "Color theme",
|
||||||
|
"theme.deepseek": "DeepSeek (default)",
|
||||||
|
"theme.github": "GitHub Dark",
|
||||||
|
"theme.nord": "Nord",
|
||||||
|
"theme.dracula": "Dracula",
|
||||||
|
"theme.forest": "Forest",
|
||||||
|
"theme.catppuccin": "Catppuccin Mocha",
|
||||||
|
"theme.light": "Light",
|
||||||
"lang.switch": "中文",
|
"lang.switch": "中文",
|
||||||
"chat.imageViewerClose": "Close",
|
"chat.imageViewerClose": "Close",
|
||||||
"chat.imageViewerHint": "Click image to enlarge; click outside or Esc to close",
|
"chat.imageViewerHint": "Click image to enlarge; click outside or Esc to close",
|
||||||
|
|
@ -2372,6 +2388,26 @@ function syncAuthUserLabel() {
|
||||||
text: t("chat.myProfile"),
|
text: t("chat.myProfile"),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||||
|
items.push(
|
||||||
|
el("div", { class: "muted", style: "padding:6px 10px 2px;font-size:12px;", text: t("theme.label") }),
|
||||||
|
);
|
||||||
|
const themeSelMenu = el("select", {
|
||||||
|
class: "input",
|
||||||
|
style: "width:100%;margin:4px 8px 8px;max-width:calc(100% - 16px);",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
(window.OclawAdminTheme && window.OclawAdminTheme.THEMES ? window.OclawAdminTheme.THEMES : ["deepseek"]).forEach((tid) => {
|
||||||
|
themeSelMenu.appendChild(el("option", { value: tid, text: t(`theme.${tid}`) }));
|
||||||
|
});
|
||||||
|
themeSelMenu.value = window.OclawAdminTheme ? window.OclawAdminTheme.currentAdminTheme() : "deepseek";
|
||||||
|
} catch (_) {}
|
||||||
|
themeSelMenu.addEventListener("change", () => {
|
||||||
|
try {
|
||||||
|
if (window.OclawAdminTheme) window.OclawAdminTheme.persistAdminTheme(themeSelMenu.value);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
items.push(themeSelMenu);
|
||||||
const bridge = window.__chatUserMenuPrefs;
|
const bridge = window.__chatUserMenuPrefs;
|
||||||
if (bridge && typeof bridge === "object") {
|
if (bridge && typeof bridge === "object") {
|
||||||
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
items.push(el("div", { class: "chat-sess-menu-sep" }));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
|
||||||
<link rel="stylesheet" href="/admin/assets/styles.css" />
|
<link rel="stylesheet" href="/admin/assets/styles.css" />
|
||||||
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
|
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css" />
|
||||||
|
<link rel="stylesheet" href="/admin/assets/theme-presets.css" />
|
||||||
<style>
|
<style>
|
||||||
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
|
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
|
||||||
.admin-boot-splash {
|
.admin-boot-splash {
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-ds-body">
|
<body class="theme-ds-body">
|
||||||
|
<script src="/admin/assets/admin-theme.js"></script>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
|
|
@ -101,6 +103,18 @@
|
||||||
<div id="topTitle" class="topbar__title">Runtime</div>
|
<div id="topTitle" class="topbar__title">Runtime</div>
|
||||||
<div class="topbar__actions">
|
<div class="topbar__actions">
|
||||||
<span id="authUser" class="muted"></span>
|
<span id="authUser" class="muted"></span>
|
||||||
|
<label class="admin-theme-wrap" style="display: inline-flex; align-items: center; gap: 6px; margin-right: 4px">
|
||||||
|
<span class="muted" style="font-size: 12px" data-i18n="theme.label">配色</span>
|
||||||
|
<select id="adminThemeSelect" class="input input--compact" style="min-width: 132px; max-width: 200px; padding: 6px 8px" aria-label="theme">
|
||||||
|
<option value="deepseek" data-i18n="theme.deepseek">DeepSeek</option>
|
||||||
|
<option value="github" data-i18n="theme.github">GitHub</option>
|
||||||
|
<option value="nord" data-i18n="theme.nord">Nord</option>
|
||||||
|
<option value="dracula" data-i18n="theme.dracula">Dracula</option>
|
||||||
|
<option value="forest" data-i18n="theme.forest">Forest</option>
|
||||||
|
<option value="catppuccin" data-i18n="theme.catppuccin">Catppuccin</option>
|
||||||
|
<option value="light" data-i18n="theme.light">Light</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<a id="btnBackChat" class="btn" data-i18n="nav.chat" href="chat">对话</a>
|
<a id="btnBackChat" class="btn" data-i18n="nav.chat" href="chat">对话</a>
|
||||||
<button id="btnLogout" class="btn" data-i18n="auth.logout">退出登录</button>
|
<button id="btnLogout" class="btn" data-i18n="auth.logout">退出登录</button>
|
||||||
<button id="btnLang" class="btn">中文</button>
|
<button id="btnLang" class="btn">中文</button>
|
||||||
|
|
@ -120,7 +134,7 @@
|
||||||
</div>
|
</div>
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
src="/admin/assets/app.js?v=20260425-1"
|
src="/admin/assets/app.js?v=20260507-2"
|
||||||
onerror="(function(){var s=document.getElementById('admin-boot-splash');if(s){s.querySelector('.admin-boot-splash__title span:last-child').textContent='无法加载 app.js';s.querySelector('.admin-boot-splash__muted').textContent='请确认 /admin/assets/app.js 可访问。';}})()"
|
onerror="(function(){var s=document.getElementById('admin-boot-splash');if(s){s.querySelector('.admin-boot-splash__title span:last-child').textContent='无法加载 app.js';s.querySelector('.admin-boot-splash__muted').textContent='请确认 /admin/assets/app.js 可访问。';}})()"
|
||||||
></script>
|
></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,17 @@ body.theme-ds-body {
|
||||||
--ds-text-muted: rgba(232, 232, 232, 0.55);
|
--ds-text-muted: rgba(232, 232, 232, 0.55);
|
||||||
--ds-accent: #5eb3ff;
|
--ds-accent: #5eb3ff;
|
||||||
--ds-accent-soft: rgba(94, 179, 255, 0.18);
|
--ds-accent-soft: rgba(94, 179, 255, 0.18);
|
||||||
|
--ds-sidebar: #161618;
|
||||||
|
--ds-topbar-bg: rgba(22, 22, 24, 0.92);
|
||||||
|
--ds-accent-border: rgba(94, 179, 255, 0.25);
|
||||||
|
--ds-accent-border-strong: rgba(94, 179, 255, 0.45);
|
||||||
|
--ds-accent-link: rgba(94, 179, 255, 0.95);
|
||||||
|
--ds-accent-scrollbar: rgba(94, 179, 255, 0.45);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(94, 179, 255, 0.65);
|
||||||
|
--ds-panel: #222224;
|
||||||
|
--ds-chat-scroll-track: #2e2e2e;
|
||||||
|
--ds-chat-scroll-thumb: #7a7a7a;
|
||||||
|
--ds-chat-scroll-thumb-hover: #909090;
|
||||||
/* 左侧会话列表:贴近侧栏 #0d0d0d,选中仅略提亮 */
|
/* 左侧会话列表:贴近侧栏 #0d0d0d,选中仅略提亮 */
|
||||||
--chat-sess-hover-bg: rgba(255, 255, 255, 0.05);
|
--chat-sess-hover-bg: rgba(255, 255, 255, 0.05);
|
||||||
--chat-sess-active-bg: rgba(255, 255, 255, 0.062);
|
--chat-sess-active-bg: rgba(255, 255, 255, 0.062);
|
||||||
|
|
@ -21,7 +32,7 @@ body.theme-ds-body {
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-ds-body .layout .sidebar {
|
body.theme-ds-body .layout .sidebar {
|
||||||
background: #161618;
|
background: var(--ds-sidebar);
|
||||||
border-right-color: var(--ds-border);
|
border-right-color: var(--ds-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +47,7 @@ body.theme-ds-body .nav__item:hover {
|
||||||
|
|
||||||
body.theme-ds-body .nav__item--active {
|
body.theme-ds-body .nav__item--active {
|
||||||
background: var(--ds-accent-soft);
|
background: var(--ds-accent-soft);
|
||||||
border: 1px solid rgba(94, 179, 255, 0.25);
|
border: 1px solid var(--ds-accent-border);
|
||||||
color: var(--ds-text);
|
color: var(--ds-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +60,7 @@ body.theme-ds-body .main {
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-ds-body .topbar {
|
body.theme-ds-body .topbar {
|
||||||
background: rgba(22, 22, 24, 0.92);
|
background: var(--ds-topbar-bg);
|
||||||
border-bottom-color: var(--ds-border);
|
border-bottom-color: var(--ds-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +89,7 @@ body.theme-ds-body .btn:hover {
|
||||||
|
|
||||||
body.theme-ds-body .btn--primary {
|
body.theme-ds-body .btn--primary {
|
||||||
background: var(--ds-accent-soft);
|
background: var(--ds-accent-soft);
|
||||||
border-color: rgba(94, 179, 255, 0.45);
|
border-color: var(--ds-accent-border-strong);
|
||||||
color: var(--ds-text);
|
color: var(--ds-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +287,7 @@ body.theme-ds-body .card {
|
||||||
.chat-nav__link {
|
.chat-nav__link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgba(94, 179, 255, 0.95);
|
color: var(--ds-accent-link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +390,7 @@ body.theme-ds-body .card {
|
||||||
.chat-pending-files:hover,
|
.chat-pending-files:hover,
|
||||||
.table-wrap:hover,
|
.table-wrap:hover,
|
||||||
.session-monitor-modal__card:hover {
|
.session-monitor-modal__card:hover {
|
||||||
scrollbar-color: rgba(94, 179, 255, 0.45) rgba(255, 255, 255, 0.06);
|
scrollbar-color: var(--ds-accent-scrollbar) rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg__reasoning-pre::-webkit-scrollbar,
|
.chat-msg__reasoning-pre::-webkit-scrollbar,
|
||||||
|
|
@ -425,14 +436,14 @@ body.theme-ds-body .card {
|
||||||
.chat-pending-files:hover::-webkit-scrollbar-thumb,
|
.chat-pending-files:hover::-webkit-scrollbar-thumb,
|
||||||
.table-wrap:hover::-webkit-scrollbar-thumb,
|
.table-wrap:hover::-webkit-scrollbar-thumb,
|
||||||
.session-monitor-modal__card:hover::-webkit-scrollbar-thumb {
|
.session-monitor-modal__card:hover::-webkit-scrollbar-thumb {
|
||||||
background: rgba(94, 179, 255, 0.45);
|
background: var(--ds-accent-scrollbar);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg__reasoning-pre::-webkit-scrollbar-thumb:hover,
|
.chat-msg__reasoning-pre::-webkit-scrollbar-thumb:hover,
|
||||||
.chat-pending-files::-webkit-scrollbar-thumb:hover,
|
.chat-pending-files::-webkit-scrollbar-thumb:hover,
|
||||||
.table-wrap::-webkit-scrollbar-thumb:hover,
|
.table-wrap::-webkit-scrollbar-thumb:hover,
|
||||||
.session-monitor-modal__card::-webkit-scrollbar-thumb:hover {
|
.session-monitor-modal__card::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(94, 179, 255, 0.65);
|
background: var(--ds-accent-scrollbar-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg {
|
.chat-msg {
|
||||||
|
|
@ -973,7 +984,7 @@ body.chat-standalone-page .chat-nav__scroll::-webkit-scrollbar {
|
||||||
|
|
||||||
body.chat-standalone-page .chat-messages {
|
body.chat-standalone-page .chat-messages {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #7a7a7a #2e2e2e;
|
scrollbar-color: var(--ds-chat-scroll-thumb) var(--ds-chat-scroll-track);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chat-standalone-page .chat-messages::-webkit-scrollbar {
|
body.chat-standalone-page .chat-messages::-webkit-scrollbar {
|
||||||
|
|
@ -982,14 +993,14 @@ body.chat-standalone-page .chat-messages::-webkit-scrollbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chat-standalone-page .chat-messages::-webkit-scrollbar-track {
|
body.chat-standalone-page .chat-messages::-webkit-scrollbar-track {
|
||||||
background: #2e2e2e;
|
background: var(--ds-chat-scroll-track);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb {
|
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb {
|
||||||
background: #7a7a7a;
|
background: var(--ds-chat-scroll-thumb);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb:hover {
|
body.chat-standalone-page .chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
background: #909090;
|
background: var(--ds-chat-scroll-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
233
interfaces/admin/static/theme-presets.css
Normal file
233
interfaces/admin/static/theme-presets.css
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
/* Classic admin/chat palettes: override CSS variables on body.theme-ds-body[data-admin-theme="…"].
|
||||||
|
Default (no attribute or deepseek) stays in theme-deepseek.css. */
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="github"] {
|
||||||
|
--ds-bg: #0d1117;
|
||||||
|
--ds-chat-main: #010409;
|
||||||
|
--ds-surface: #161b22;
|
||||||
|
--ds-surface-2: #21262d;
|
||||||
|
--ds-border: rgba(240, 246, 252, 0.1);
|
||||||
|
--ds-text: #f0f6fc;
|
||||||
|
--ds-text-muted: rgba(240, 246, 252, 0.55);
|
||||||
|
--ds-accent: #58a6ff;
|
||||||
|
--ds-accent-soft: rgba(88, 166, 255, 0.16);
|
||||||
|
--ds-sidebar: #161b22;
|
||||||
|
--ds-topbar-bg: rgba(22, 27, 34, 0.94);
|
||||||
|
--ds-accent-border: rgba(88, 166, 255, 0.32);
|
||||||
|
--ds-accent-border-strong: rgba(88, 166, 255, 0.55);
|
||||||
|
--ds-accent-link: rgba(88, 166, 255, 0.95);
|
||||||
|
--ds-accent-scrollbar: rgba(88, 166, 255, 0.45);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(88, 166, 255, 0.68);
|
||||||
|
--ds-panel: #21262d;
|
||||||
|
--ds-chat-scroll-track: #21262d;
|
||||||
|
--ds-chat-scroll-thumb: #484f58;
|
||||||
|
--ds-chat-scroll-thumb-hover: #6e7681;
|
||||||
|
--chat-sess-hover-bg: rgba(240, 246, 252, 0.06);
|
||||||
|
--chat-sess-active-bg: rgba(240, 246, 252, 0.08);
|
||||||
|
--chat-sess-border: rgba(240, 246, 252, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="nord"] {
|
||||||
|
--ds-bg: #2e3440;
|
||||||
|
--ds-chat-main: #242933;
|
||||||
|
--ds-surface: #3b4252;
|
||||||
|
--ds-surface-2: #434c5e;
|
||||||
|
--ds-border: rgba(236, 239, 244, 0.12);
|
||||||
|
--ds-text: #eceff4;
|
||||||
|
--ds-text-muted: rgba(236, 239, 244, 0.55);
|
||||||
|
--ds-accent: #88c0d0;
|
||||||
|
--ds-accent-soft: rgba(136, 192, 208, 0.2);
|
||||||
|
--ds-sidebar: #3b4252;
|
||||||
|
--ds-topbar-bg: rgba(59, 66, 82, 0.95);
|
||||||
|
--ds-accent-border: rgba(136, 192, 208, 0.35);
|
||||||
|
--ds-accent-border-strong: rgba(136, 192, 208, 0.55);
|
||||||
|
--ds-accent-link: rgba(143, 188, 187, 0.98);
|
||||||
|
--ds-accent-scrollbar: rgba(136, 192, 208, 0.5);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(143, 188, 187, 0.75);
|
||||||
|
--ds-panel: #434c5e;
|
||||||
|
--ds-chat-scroll-track: #3b4252;
|
||||||
|
--ds-chat-scroll-thumb: #5e81ac;
|
||||||
|
--ds-chat-scroll-thumb-hover: #81a1c1;
|
||||||
|
--chat-user-bubble-bg: #a3be8c;
|
||||||
|
--chat-user-bubble-fg: #2e3440;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="dracula"] {
|
||||||
|
--ds-bg: #282a36;
|
||||||
|
--ds-chat-main: #1e1f29;
|
||||||
|
--ds-surface: #44475a;
|
||||||
|
--ds-surface-2: #6272a4;
|
||||||
|
--ds-border: rgba(248, 248, 242, 0.1);
|
||||||
|
--ds-text: #f8f8f2;
|
||||||
|
--ds-text-muted: rgba(248, 248, 242, 0.55);
|
||||||
|
--ds-accent: #bd93f9;
|
||||||
|
--ds-accent-soft: rgba(189, 147, 249, 0.22);
|
||||||
|
--ds-sidebar: #44475a;
|
||||||
|
--ds-topbar-bg: rgba(68, 71, 90, 0.95);
|
||||||
|
--ds-accent-border: rgba(189, 147, 249, 0.4);
|
||||||
|
--ds-accent-border-strong: rgba(189, 147, 249, 0.6);
|
||||||
|
--ds-accent-link: rgba(255, 121, 198, 0.95);
|
||||||
|
--ds-accent-scrollbar: rgba(189, 147, 249, 0.5);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(255, 121, 198, 0.65);
|
||||||
|
--ds-panel: #44475a;
|
||||||
|
--ds-chat-scroll-track: #21222c;
|
||||||
|
--ds-chat-scroll-thumb: #6272a4;
|
||||||
|
--ds-chat-scroll-thumb-hover: #bd93f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="forest"] {
|
||||||
|
--ds-bg: #0c1412;
|
||||||
|
--ds-chat-main: #070d0b;
|
||||||
|
--ds-surface: #13251f;
|
||||||
|
--ds-surface-2: #1a332b;
|
||||||
|
--ds-border: rgba(167, 243, 208, 0.1);
|
||||||
|
--ds-text: #ecfdf5;
|
||||||
|
--ds-text-muted: rgba(209, 250, 229, 0.55);
|
||||||
|
--ds-accent: #34d399;
|
||||||
|
--ds-accent-soft: rgba(52, 211, 153, 0.18);
|
||||||
|
--ds-sidebar: #13251f;
|
||||||
|
--ds-topbar-bg: rgba(19, 37, 31, 0.94);
|
||||||
|
--ds-accent-border: rgba(52, 211, 153, 0.35);
|
||||||
|
--ds-accent-border-strong: rgba(52, 211, 153, 0.55);
|
||||||
|
--ds-accent-link: rgba(110, 231, 183, 0.95);
|
||||||
|
--ds-accent-scrollbar: rgba(52, 211, 153, 0.45);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(110, 231, 183, 0.7);
|
||||||
|
--ds-panel: #1a332b;
|
||||||
|
--ds-chat-scroll-track: #13251f;
|
||||||
|
--ds-chat-scroll-thumb: #047857;
|
||||||
|
--ds-chat-scroll-thumb-hover: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="catppuccin"] {
|
||||||
|
--ds-bg: #1e1e2e;
|
||||||
|
--ds-chat-main: #181825;
|
||||||
|
--ds-surface: #313244;
|
||||||
|
--ds-surface-2: #45475a;
|
||||||
|
--ds-border: rgba(205, 214, 244, 0.1);
|
||||||
|
--ds-text: #cdd6f4;
|
||||||
|
--ds-text-muted: rgba(205, 214, 244, 0.52);
|
||||||
|
--ds-accent: #89b4fa;
|
||||||
|
--ds-accent-soft: rgba(137, 180, 250, 0.2);
|
||||||
|
--ds-sidebar: #313244;
|
||||||
|
--ds-topbar-bg: rgba(49, 50, 68, 0.95);
|
||||||
|
--ds-accent-border: rgba(137, 180, 250, 0.38);
|
||||||
|
--ds-accent-border-strong: rgba(137, 180, 250, 0.58);
|
||||||
|
--ds-accent-link: rgba(166, 218, 249, 0.96);
|
||||||
|
--ds-accent-scrollbar: rgba(137, 180, 250, 0.48);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(166, 218, 249, 0.72);
|
||||||
|
--ds-panel: #45475a;
|
||||||
|
--ds-chat-scroll-track: #313244;
|
||||||
|
--ds-chat-scroll-thumb: #6c7086;
|
||||||
|
--ds-chat-scroll-thumb-hover: #89b4fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light: same pipeline as dark presets (CSS variables on body) + a small set of chat/sidebar
|
||||||
|
tweaks where theme-deepseek.css still used “white mist” tints meant for dark canvases. */
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] {
|
||||||
|
--ds-bg: #f1f5f9;
|
||||||
|
--ds-chat-main: #ffffff;
|
||||||
|
--ds-surface: #ffffff;
|
||||||
|
--ds-surface-2: #e2e8f0;
|
||||||
|
--ds-border: rgba(15, 23, 42, 0.12);
|
||||||
|
--ds-text: #0f172a;
|
||||||
|
--ds-text-muted: rgba(15, 23, 42, 0.55);
|
||||||
|
--ds-accent: #2563eb;
|
||||||
|
--ds-accent-soft: rgba(37, 99, 235, 0.14);
|
||||||
|
--ds-sidebar: #f8fafc;
|
||||||
|
--ds-topbar-bg: rgba(248, 250, 252, 0.92);
|
||||||
|
--ds-accent-border: rgba(37, 99, 235, 0.35);
|
||||||
|
--ds-accent-border-strong: rgba(37, 99, 235, 0.55);
|
||||||
|
--ds-accent-link: #1d4ed8;
|
||||||
|
--ds-accent-scrollbar: rgba(37, 99, 235, 0.42);
|
||||||
|
--ds-accent-scrollbar-hover: rgba(29, 78, 216, 0.62);
|
||||||
|
--ds-panel: #ffffff;
|
||||||
|
--ds-chat-scroll-track: #e2e8f0;
|
||||||
|
--ds-chat-scroll-thumb: #94a3b8;
|
||||||
|
--ds-chat-scroll-thumb-hover: #64748b;
|
||||||
|
--chat-sess-hover-bg: rgba(15, 23, 42, 0.06);
|
||||||
|
--chat-sess-active-bg: rgba(15, 23, 42, 0.09);
|
||||||
|
--chat-sess-border: rgba(15, 23, 42, 0.1);
|
||||||
|
--chat-user-bubble-bg: #10b981;
|
||||||
|
--chat-user-bubble-fg: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .nav__groupTitle {
|
||||||
|
color: rgba(15, 23, 42, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .brand__sub {
|
||||||
|
color: rgba(15, 23, 42, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph {
|
||||||
|
background-color: rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-nav__new:hover .chat-nav__newGlyph {
|
||||||
|
background-color: rgba(15, 23, 42, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph::before,
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-nav__newGlyph::after {
|
||||||
|
background-color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-avatar--bot,
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-avatar--userBuiltin {
|
||||||
|
background: rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg--assistant {
|
||||||
|
background: rgba(15, 23, 42, 0.045);
|
||||||
|
border-color: var(--ds-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg__action-btn {
|
||||||
|
border-color: rgba(15, 23, 42, 0.2);
|
||||||
|
color: rgba(15, 23, 42, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg__action-btn:hover {
|
||||||
|
background: rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg--tool {
|
||||||
|
background: rgba(15, 23, 42, 0.06);
|
||||||
|
border-color: rgba(15, 23, 42, 0.18);
|
||||||
|
color: rgba(15, 23, 42, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg--thinking {
|
||||||
|
background: rgba(15, 23, 42, 0.04);
|
||||||
|
border-color: rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-task-stage {
|
||||||
|
border-color: rgba(15, 23, 42, 0.12);
|
||||||
|
background: rgba(15, 23, 42, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-task-stage__toggle {
|
||||||
|
border-color: rgba(15, 23, 42, 0.14);
|
||||||
|
background: rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-composer {
|
||||||
|
border-top-color: rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg__reasoning {
|
||||||
|
border-color: rgba(15, 23, 42, 0.1);
|
||||||
|
background: rgba(15, 23, 42, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg__reasoning-pre {
|
||||||
|
background: rgba(15, 23, 42, 0.05);
|
||||||
|
border-color: rgba(15, 23, 42, 0.08);
|
||||||
|
color: var(--ds-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-ds-body[data-admin-theme="light"] .chat-msg__md .mermaid {
|
||||||
|
border-color: rgba(15, 23, 42, 0.14);
|
||||||
|
background: rgba(248, 250, 252, 0.95);
|
||||||
|
}
|
||||||
|
|
@ -34,14 +34,19 @@ Write-Step "Starting gateway + desktop"
|
||||||
|
|
||||||
& "$PSScriptRoot/start_gateway.ps1" -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
|
& "$PSScriptRoot/start_gateway.ps1" -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
|
||||||
|
|
||||||
# When gateway is started in foreground, the call above blocks; desktop won't start.
|
# When gateway is started in foreground, the call above blocks; desktop / sidecars won't run in this script.
|
||||||
if (-not $Background) {
|
if (-not $Background) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "[INFO] Gateway started in foreground and will block. Use -Background to start both." -ForegroundColor Yellow
|
Write-Host "[INFO] Gateway is running in the foreground (this window blocks). Desktop was not started." -ForegroundColor Yellow
|
||||||
|
Write-Host " For gateway + desktop + sidecars in one go, run: start_all.ps1 -Background" -ForegroundColor Yellow
|
||||||
|
Write-Host " Or start desktop in another terminal after the gateway is listening: start_desktop.ps1 -Background -KeepExistingGateway" -ForegroundColor Yellow
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background -WithWikiWorker:$WithWikiWorker
|
# start_desktop.ps1 defaults to stop_gateway.ps1 to free the port when launching desktop alone.
|
||||||
|
# After start_gateway above we must keep the listener — pass -KeepExistingGateway.
|
||||||
|
$gwUrl = "http://$BindHost`:$Port"
|
||||||
|
& "$PSScriptRoot/start_desktop.ps1" -SkipInstall:$SkipInstall -Background -WithWikiWorker:$WithWikiWorker -KeepExistingGateway -GatewayBaseUrl $gwUrl
|
||||||
|
|
||||||
if (-not $WithoutWeixin) {
|
if (-not $WithoutWeixin) {
|
||||||
$gwBase = ($WeixinGatewayBaseUrl | ForEach-Object { "$_".Trim() })
|
$gwBase = ($WeixinGatewayBaseUrl | ForEach-Object { "$_".Trim() })
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
[switch]$SkipInstall = $false,
|
[switch]$SkipInstall = $false,
|
||||||
[switch]$Background = $false,
|
[switch]$Background = $false,
|
||||||
[switch]$KeepExistingGateway = $false,
|
[switch]$KeepExistingGateway = $false,
|
||||||
|
[string]$GatewayBaseUrl = "",
|
||||||
[bool]$WithWikiWorker = $true
|
[bool]$WithWikiWorker = $true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,6 +28,19 @@ $env:AIA_WORKSPACE_ROOT = $repoRoot
|
||||||
$env:OPS_WORKSPACE_ROOT = $repoRoot
|
$env:OPS_WORKSPACE_ROOT = $repoRoot
|
||||||
$env:OCLAW_WORKSPACE = $repoRoot
|
$env:OCLAW_WORKSPACE = $repoRoot
|
||||||
|
|
||||||
|
# Do not inherit attach-mode env from a parent shell when launching standalone desktop.
|
||||||
|
Remove-Item Env:OCLAW_DESKTOP_EMBED_BACKEND -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item Env:OCLAW_DESKTOP_GATEWAY_BASE_URL -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item Env:OCLAW_DESKTOP_SKIP_CHANNEL -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($KeepExistingGateway) {
|
||||||
|
$gw = if ($GatewayBaseUrl) { $GatewayBaseUrl.Trim() } else { "http://127.0.0.1:8787" }
|
||||||
|
$env:OCLAW_DESKTOP_EMBED_BACKEND = "0"
|
||||||
|
$env:OCLAW_DESKTOP_GATEWAY_BASE_URL = $gw
|
||||||
|
$env:OCLAW_DESKTOP_SKIP_CHANNEL = "1"
|
||||||
|
Write-Host "==> Desktop will use existing gateway (no second listener): $gw" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $KeepExistingGateway) {
|
if (-not $KeepExistingGateway) {
|
||||||
Write-Host "==> Cleaning previous gateway listener" -ForegroundColor Cyan
|
Write-Host "==> Cleaning previous gateway listener" -ForegroundColor Cyan
|
||||||
try {
|
try {
|
||||||
|
|
@ -48,14 +62,21 @@ if ($WithWikiWorker) {
|
||||||
|
|
||||||
Write-Host "==> Launching desktop app" -ForegroundColor Cyan
|
Write-Host "==> Launching desktop app" -ForegroundColor Cyan
|
||||||
if ($Background) {
|
if ($Background) {
|
||||||
# Avoid launching npm.ps1 directly (can be file-associated and open in Notepad on some setups).
|
# Do NOT redirect stdout/stderr here: on Windows, piping npm/electron stdio can prevent the GUI from showing.
|
||||||
# Use hidden cmd and redirect logs to avoid black console window popup.
|
# Prefer the Electron shim directly; fallback to npm run dev (still without redirects).
|
||||||
$p = Start-Process -FilePath "cmd.exe" -ArgumentList @("/c","npm.cmd","run","dev") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden -RedirectStandardOutput $outLog -RedirectStandardError $errLog
|
$electronCmd = Join-Path $desktopDir "node_modules\.bin\electron.cmd"
|
||||||
|
$p = $null
|
||||||
|
if (Test-Path $electronCmd) {
|
||||||
|
$p = Start-Process -FilePath $electronCmd -ArgumentList @(".") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden
|
||||||
|
}
|
||||||
|
if (-not $p) {
|
||||||
|
$p = Start-Process -FilePath "npm.cmd" -ArgumentList @("run","dev") -WorkingDirectory $desktopDir -PassThru -WindowStyle Hidden
|
||||||
|
}
|
||||||
Set-Content -Path $pidFile -Value "$($p.Id)" -Encoding ascii
|
Set-Content -Path $pidFile -Value "$($p.Id)" -Encoding ascii
|
||||||
Write-Host "desktop.pid = $pidFile" -ForegroundColor DarkGray
|
Write-Host "desktop.pid = $pidFile" -ForegroundColor DarkGray
|
||||||
Write-Host "desktop.out = $outLog" -ForegroundColor DarkGray
|
Write-Host "Embedded backend/chat logs (Electron): see %APPDATA%\oclaw\logs\desktop.log / backend.log" -ForegroundColor DarkGray
|
||||||
Write-Host "desktop.err = $errLog" -ForegroundColor DarkGray
|
|
||||||
Write-Host "PID = $($p.Id)" -ForegroundColor Green
|
Write-Host "PID = $($p.Id)" -ForegroundColor Green
|
||||||
|
Write-Host "==> Desktop launched in background (this script exits now)." -ForegroundColor Cyan
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
& "npm.cmd" "run" "dev"
|
& "npm.cmd" "run" "dev"
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ param(
|
||||||
[int]$Port = 8787,
|
[int]$Port = 8787,
|
||||||
[switch]$SkipInstall = $false,
|
[switch]$SkipInstall = $false,
|
||||||
[switch]$Background = $false,
|
[switch]$Background = $false,
|
||||||
[switch]$WithWeixin = $false,
|
[switch]$WithoutWeixin = $false,
|
||||||
|
[switch]$WithoutWhatsApp = $false,
|
||||||
[bool]$WithWikiWorker = $true,
|
[bool]$WithWikiWorker = $true,
|
||||||
[string]$WeixinChannelId = "oclaw-weixin",
|
[string]$WeixinChannelId = "oclaw-weixin",
|
||||||
[string]$WeixinGatewayBaseUrl = ""
|
[string]$WeixinGatewayBaseUrl = "",
|
||||||
|
[string]$WhatsAppChannelId = "whatsapp"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
@ -16,5 +18,14 @@ if (-not (Test-Path $real)) {
|
||||||
throw "Forward script target not found: $real"
|
throw "Forward script target not found: $real"
|
||||||
}
|
}
|
||||||
|
|
||||||
& $real -BindHost $BindHost -Port $Port -SkipInstall:$SkipInstall -Background:$Background -WithWeixin:$WithWeixin -WithWikiWorker:$WithWikiWorker -WeixinChannelId $WeixinChannelId -WeixinGatewayBaseUrl $WeixinGatewayBaseUrl
|
& $real `
|
||||||
|
-BindHost $BindHost `
|
||||||
|
-Port $Port `
|
||||||
|
-SkipInstall:$SkipInstall `
|
||||||
|
-Background:$Background `
|
||||||
|
-WithoutWeixin:$WithoutWeixin `
|
||||||
|
-WithoutWhatsApp:$WithoutWhatsApp `
|
||||||
|
-WithWikiWorker:$WithWikiWorker `
|
||||||
|
-WeixinChannelId $WeixinChannelId `
|
||||||
|
-WeixinGatewayBaseUrl $WeixinGatewayBaseUrl `
|
||||||
|
-WhatsAppChannelId $WhatsAppChannelId
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
param(
|
param(
|
||||||
[switch]$SkipInstall = $false,
|
[switch]$SkipInstall = $false,
|
||||||
[switch]$Background = $false,
|
[switch]$Background = $false,
|
||||||
|
[switch]$KeepExistingGateway = $false,
|
||||||
|
[string]$GatewayBaseUrl = "",
|
||||||
[bool]$WithWikiWorker = $true
|
[bool]$WithWikiWorker = $true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -11,5 +13,5 @@ if (-not (Test-Path $real)) {
|
||||||
throw "Forward script target not found: $real"
|
throw "Forward script target not found: $real"
|
||||||
}
|
}
|
||||||
|
|
||||||
& $real -SkipInstall:$SkipInstall -Background:$Background -WithWikiWorker:$WithWikiWorker
|
& $real -SkipInstall:$SkipInstall -Background:$Background -KeepExistingGateway:$KeepExistingGateway -GatewayBaseUrl $GatewayBaseUrl -WithWikiWorker:$WithWikiWorker
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue