diff --git a/README.md b/README.md index e2eb4c5f..8850e952 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\start_all.ps1 -Background - **8787 端口占用 / 重启不生效**:`powershell -ExecutionPolicy Bypass -File .\scripts\stop_gateway.ps1 -Force` 后再启动。 - **更细的从零教程**:`docs/RUNBOOK.md` →「开源快速安装(从零到跑起来)」。 +- **更换品牌 Logo(外部优先)**:见 `docs/BRANDING.md`(将资源放到 `_local/branding/`)。 ### 可选:运维专家(network_ops)与 netx @@ -110,6 +111,7 @@ Notes: Full runbook (recommended): see `docs/RUNBOOK.md` → “开源快速安装(从零到跑起来)”. Minimal onboarding guide: `docs/OPEN_SOURCE_QUICKSTART.md`. +Branding guide (external-first assets under `_local/branding`): `docs/BRANDING.md`. Chinese zero-to-running checklist: see **开箱即用(从零跑起来)** at the top of this README. diff --git a/desktop/main.js b/desktop/main.js index 977830d7..c1d40740 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -9,7 +9,20 @@ const DEFAULT_HOST = "127.0.0.1"; const DEFAULT_PORT = 8787; const APP_DISPLAY_NAME = "oclaw"; const APP_ROOT = path.resolve(__dirname, "..", ".."); -const APP_ICON_PATH = path.join(APP_ROOT, "src", "admin", "static", "oliver.svg"); +const APP_ICON_PATH = (() => { + const candidates = [ + path.join(APP_ROOT, "_local", "branding", "desktop.ico"), + path.join(APP_ROOT, "_local", "branding", "logo.png"), + path.join(APP_ROOT, "_local", "branding", "logo.svg"), + path.join(APP_ROOT, "interfaces", "admin", "static", "oliver.svg"), + ]; + for (const p of candidates) { + try { + if (fs.existsSync(p)) return p; + } catch (_) {} + } + return candidates[candidates.length - 1]; +})(); const DATA_ROOT = path.join(app.getPath("userData"), "runtime-data"); const LOG_ROOT = path.join(app.getPath("userData"), "logs"); const BACKEND_LOG_FILE = path.join(LOG_ROOT, "backend.log"); diff --git a/desktop/tools/generate-icon.cjs b/desktop/tools/generate-icon.cjs index 26bb7318..30b39ab7 100644 --- a/desktop/tools/generate-icon.cjs +++ b/desktop/tools/generate-icon.cjs @@ -6,7 +6,11 @@ const pngToIco = typeof pngToIcoModule === "function" ? pngToIcoModule : pngToIc async function main() { const desktopRoot = path.resolve(__dirname, ".."); - const svgPath = path.resolve(desktopRoot, "..", "src", "admin", "static", "oliver.svg"); + const svgCandidates = [ + path.resolve(desktopRoot, "..", "_local", "branding", "logo.svg"), + path.resolve(desktopRoot, "..", "interfaces", "admin", "static", "oliver.svg"), + ]; + const svgPath = svgCandidates.find((p) => fs.existsSync(p)) || svgCandidates[svgCandidates.length - 1]; const assetsDir = path.join(desktopRoot, "assets"); const icoPath = path.join(assetsDir, "oclaw.ico"); diff --git a/docs/BRANDING.md b/docs/BRANDING.md new file mode 100644 index 00000000..01bc4cd8 --- /dev/null +++ b/docs/BRANDING.md @@ -0,0 +1,46 @@ +# Branding Guide + +This project supports **external-first branding**. +Put your branding assets under `_local/branding`, and Oclaw will use them first. +If an external file is missing, Oclaw falls back to built-in defaults. + +## Directory + +Create this folder in the repository root: + +`_local/branding/` + +## Supported Files + +- `logo.svg` + - Used by admin sidebar logo and chat assistant logo/avatar. + - URL path: `/admin/brand-assets/logo.svg` +- `desktop.ico` (recommended on Windows) + - Preferred desktop window icon for Electron. +- `logo.png` (desktop fallback) + - Used as desktop icon fallback when `desktop.ico` is absent. + +## Resolution Order + +### Web UI (Admin + Chat) + +1. `_local/branding/logo.svg` +2. Built-in fallback: `interfaces/admin/static/oliver.svg` + +### Desktop (Electron window icon) + +1. `_local/branding/desktop.ico` +2. `_local/branding/logo.png` +3. `_local/branding/logo.svg` +4. Built-in fallback: `interfaces/admin/static/oliver.svg` + +## How to Apply + +1. Put your files into `_local/branding/`. +2. Restart gateway (and desktop app if using Electron). +3. Hard refresh browser page if needed. + +## Notes + +- `/admin/brand-assets/*` is served with no-cache headers to reduce stale asset issues. +- `_local/*` is git-ignored by default, so tenant/customer branding stays local. diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index 32287306..d0db4ce3 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -1934,7 +1934,8 @@ function prependMessageTime(node, tsIso) { node.insertBefore(el("div", { class: "chat-msg__time", text: txt }), node.firstChild); } -const CHAT_BOT_LOGO_SRC = "/admin/assets/oliver.svg"; +const CHAT_BOT_LOGO_SRC = "/admin/brand-assets/logo.svg"; +const CHAT_BOT_LOGO_FALLBACK_SRC = "/admin/assets/oliver.svg"; /** 内置默认用户头像(与助手头像同尺寸与底栏样式,SVG) */ const DEFAULT_USER_AVATAR_SRC = "/admin/assets/default-user-avatar.svg"; @@ -1950,12 +1951,22 @@ async function loadMeProfile() { } function buildBotAvatarImg() { - return el("img", { + const img = el("img", { class: "chat-avatar chat-avatar--bot", src: CHAT_BOT_LOGO_SRC, alt: "", loading: "lazy", }); + img.addEventListener( + "error", + () => { + if (img.dataset.logoFallbackApplied === "1") return; + img.dataset.logoFallbackApplied = "1"; + img.src = CHAT_BOT_LOGO_FALLBACK_SRC; + }, + { once: true }, + ); + return img; } function buildUserAvatarSlot() { @@ -2331,13 +2342,21 @@ function mount(node) { } function buildChatBrandLogoNode() { - return el("div", { class: "chat-nav__brandWrap" }, [ - el("img", { - class: "chat-nav__brandLogo", - src: "/admin/assets/oliver.svg", - alt: "oliver logo", - }), - ]); + const img = el("img", { + class: "chat-nav__brandLogo", + src: CHAT_BOT_LOGO_SRC, + alt: "site logo", + }); + img.addEventListener( + "error", + () => { + if (img.dataset.logoFallbackApplied === "1") return; + img.dataset.logoFallbackApplied = "1"; + img.src = CHAT_BOT_LOGO_FALLBACK_SRC; + }, + { once: true }, + ); + return el("div", { class: "chat-nav__brandWrap" }, [img]); } function resolveAdminHashUrl(hashPath, sessionId) { diff --git a/interfaces/admin/static/index.html b/interfaces/admin/static/index.html index 31e5e694..cfdb86a4 100644 --- a/interfaces/admin/static/index.html +++ b/interfaces/admin/static/index.html @@ -53,7 +53,12 @@