diff --git a/README.md b/README.md index 3174529..60bfb6c 100644 --- a/README.md +++ b/README.md @@ -108,10 +108,12 @@ npx @deepseek-ai/dsh plugin --profile web add file:%CD%/integrations/deepseek-ha 需要 Codex Desktop、系统托盘或懒得留源码时,再下 [Windows 安装包](https://github.com/starsstreaming/beautiCode/releases/latest)。 -运行: +然后自己启动: -```text -beautiCode-Setup-1.0.1-win-x64.exe +```sh +dsh web +# 未把 dsh 装到 PATH 时: +npx @deepseek-ai/dsh web ``` 安装包自带 Node.js,不需要另外安装 Node.js、npm 或 pnpm。安装结束时会自动写入 DSH 插件。若你改过安装目录,以安装文件夹里的 `集成说明.txt` 为准。 diff --git a/apps/tray/session-host.mjs b/apps/tray/session-host.mjs index 3c044fb..e7bb49b 100644 --- a/apps/tray/session-host.mjs +++ b/apps/tray/session-host.mjs @@ -22,6 +22,7 @@ * POST /discover {} * POST /shutdown {} */ +import fs from "node:fs"; import http from "node:http"; import process from "node:process"; import path from "node:path"; @@ -118,6 +119,25 @@ if (portArg) { sessionOpts.autoDiscover = false; } if (dataRoot) sessionOpts.dataRoot = path.resolve(dataRoot); +const bundledGalleryHi = path.join( + repoRoot, + "assets", + "themes", + "internal-beyond", + "bg-canvas-4k.png", +); +const bundledGalleryLo = path.join( + repoRoot, + "assets", + "themes", + "internal-beyond", + "bg-canvas.png", +); +if (fs.existsSync(bundledGalleryHi)) { + sessionOpts.bundledGalleryImagePath = bundledGalleryHi; +} else if (fs.existsSync(bundledGalleryLo)) { + sessionOpts.bundledGalleryImagePath = bundledGalleryLo; +} const SessionClass = hostKind === "dsh" ? adapter.DshSession : adapter.BeautiSession; const session = new SessionClass(sessionOpts); @@ -172,6 +192,7 @@ function publicTheme(theme) { name: theme.name, type: theme.type, savedAt: theme.savedAt, + ...(theme.bundled ? { bundled: true } : {}), ...(typeof theme.videoPositionSec === "number" ? { videoPositionSec: theme.videoPositionSec } : {}), @@ -287,10 +308,14 @@ const server = http.createServer( send(res, 400, { ok: false, error: "必须提供 imagePath。" }); return; } - const result = await session.apply({ + const imageInput = { type: "image", imagePath: path.resolve(body.imagePath), - }); + }; + if (body.effects && typeof body.effects === "object") { + imageInput.effects = body.effects; + } + const result = await session.apply(imageInput); send(res, result.ok ? 200 : 422, result); return; } diff --git a/apps/tray/start-tray.ps1 b/apps/tray/start-tray.ps1 index 26d8f0a..2299588 100644 --- a/apps/tray/start-tray.ps1 +++ b/apps/tray/start-tray.ps1 @@ -1936,15 +1936,13 @@ function Rebuild-BcTrayMenu { }) [void]$savedMenu.DropDownItems.Add($item) } - [void]$savedMenu.DropDownItems.Add( - (New-Object System.Windows.Forms.ToolStripSeparator) - ) $deleteMenu = New-Object System.Windows.Forms.ToolStripMenuItem $deleteMenu.Text = $L.DeleteTheme foreach ($theme in $themes) { $deleteId = [string]$theme.id $deleteName = [string]$theme.name if (-not $deleteId) { continue } + if ($theme.bundled -eq $true) { continue } $deleteItem = New-Object System.Windows.Forms.ToolStripMenuItem $deleteItem.Text = $deleteName $deleteItem.Tag = $deleteId @@ -1973,7 +1971,12 @@ function Rebuild-BcTrayMenu { }) [void]$deleteMenu.DropDownItems.Add($deleteItem) } - [void]$savedMenu.DropDownItems.Add($deleteMenu) + if ($deleteMenu.DropDownItems.Count -gt 0) { + [void]$savedMenu.DropDownItems.Add( + (New-Object System.Windows.Forms.ToolStripSeparator) + ) + [void]$savedMenu.DropDownItems.Add($deleteMenu) + } } } catch { $empty = New-Object System.Windows.Forms.ToolStripMenuItem diff --git a/assets/themes/internal-beyond/NOTICE.md b/assets/themes/internal-beyond/NOTICE.md new file mode 100644 index 0000000..1bcaa71 --- /dev/null +++ b/assets/themes/internal-beyond/NOTICE.md @@ -0,0 +1,9 @@ +# Internal / Infernal atmosphere assets + +`bg-internal.jpg`, `bg-infernal.jpg`, `bg-canvas.png`, and the local +`bg-canvas-4k.png` (Real-ESRGAN 4x then 3840×2160) are visual materials +from [Internal Beyond](https://github.com/Sui-IB/InternalBeyond) by Sui. + +They remain under that project's CC BY-NC-SA 4.0 terms for visual assets. +BeautiCode ships them as bundled workspace themes for local, non-commercial use. +The project name, logo, and character identity are not licensed here. diff --git a/assets/themes/internal-beyond/bg-canvas-4k.png b/assets/themes/internal-beyond/bg-canvas-4k.png new file mode 100644 index 0000000..1755a1c Binary files /dev/null and b/assets/themes/internal-beyond/bg-canvas-4k.png differ diff --git a/assets/themes/internal-beyond/bg-canvas.png b/assets/themes/internal-beyond/bg-canvas.png new file mode 100644 index 0000000..71ca5c3 Binary files /dev/null and b/assets/themes/internal-beyond/bg-canvas.png differ diff --git a/assets/themes/internal-beyond/bg-infernal.jpg b/assets/themes/internal-beyond/bg-infernal.jpg new file mode 100644 index 0000000..80f37e7 Binary files /dev/null and b/assets/themes/internal-beyond/bg-infernal.jpg differ diff --git a/assets/themes/internal-beyond/bg-internal.jpg b/assets/themes/internal-beyond/bg-internal.jpg new file mode 100644 index 0000000..707bd96 Binary files /dev/null and b/assets/themes/internal-beyond/bg-internal.jpg differ diff --git a/integrations/deepseek-harness/README.zh-CN.md b/integrations/deepseek-harness/README.zh-CN.md index 49ed217..9d11012 100644 --- a/integrations/deepseek-harness/README.zh-CN.md +++ b/integrations/deepseek-harness/README.zh-CN.md @@ -8,7 +8,14 @@ beautiCode **不启动** DeepSeek Harness。请先自己运行 `dsh web`。 -安装包和托盘会把插件写入你的 DSH profile,不需要 pnpm。源码环境也可以手动(`dsh plugin add` 需要 pnpm): +推荐在仓库根目录一键写入插件(不需要 pnpm): + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\install-dsh-plugin.ps1 +npx @deepseek-ai/dsh web +``` + +也可以手动(`dsh plugin add` 需要 pnpm): ```sh dsh plugin --profile web add file:%CD%/integrations/deepseek-harness diff --git a/integrations/deepseek-harness/agent.mjs b/integrations/deepseek-harness/agent.mjs index b5ab556..f4b1810 100644 --- a/integrations/deepseek-harness/agent.mjs +++ b/integrations/deepseek-harness/agent.mjs @@ -6,6 +6,7 @@ import { stripPathQuotes, } from "./control-client.mjs"; import { resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs"; +import { ATMOSPHERE_PRESETS, effectsForPreset, presetImagePath } from "./presets.mjs"; const APPLY_TIMEOUT_MS = 180_000; const QUICK_TIMEOUT_MS = 15_000; @@ -98,30 +99,66 @@ export function createBeauticodeActions(dataRootOrOptions) { } return { - async applyImage(imagePath, signal) { + async applyImage(imagePath, signal, options) { const inspected = await inspectLocalMedia(imagePath); if (!inspected.ok) fail(inspected.error); if (inspected.kind !== "image") { fail("beauticode_apply_image 只接受图片文件。"); } + const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null; + const input = { type: "image", imagePath: inspected.path }; + if (effects) input.effects = effects; const resolved = await backend(); if (resolved.kind === "tray") { + const body = { imagePath: inspected.path }; + if (effects) body.effects = effects; return unwrapApply( await request({ method: "POST", path: "/apply/image", - body: { imagePath: inspected.path }, + body, signal, }), "image", "已将图片设为背景。", ); } - return unwrapApply( - await resolved.session.apply({ type: "image", imagePath: inspected.path }), - "image", - "已将图片设为背景。", - ); + return unwrapApply(await resolved.session.apply(input), "image", "已将图片设为背景。"); + }, + + async applyPreset(id, signal) { + const preset = ATMOSPHERE_PRESETS[id]; + const imagePath = presetImagePath(id); + if (!preset || !imagePath) fail("未找到内置主题文件。"); + const result = await this.applyImage(imagePath, signal, { + effects: effectsForPreset(id), + }); + try { + await this.setTone(preset.tone, signal); + } catch { + /* tone is best-effort; the wallpaper still applied */ + } + return { + ...result, + atmosphere: id, + message: `已应用 ${preset.name} 活壁纸。`, + }; + }, + + async setTone(tone, signal) { + const resolved = await backend(); + const result = + resolved.kind === "tray" + ? await request({ + method: "POST", + path: "/mode/tone", + body: { tone }, + signal, + timeoutMs: QUICK_TIMEOUT_MS, + }) + : await resolved.session.setBackgroundTone(tone); + if (result.ok === false) fail(result.error || "无法切换背景色调。"); + return { ok: true, tone: result.tone ?? tone }; }, async applyVideo(input, signal) { diff --git a/integrations/deepseek-harness/atmosphere.js b/integrations/deepseek-harness/atmosphere.js new file mode 100644 index 0000000..19338c7 --- /dev/null +++ b/integrations/deepseek-harness/atmosphere.js @@ -0,0 +1,306 @@ +(() => { + "use strict"; + + const STYLE_ID = "beauticode-atmosphere-style"; + const ROOT_ID = "beauticode-gallery-bg"; + const CANVAS_URL = + (typeof globalThis !== "undefined" && globalThis.__BEAUTICODE_CANVAS_URL) || + "/__beauticode/themes/bg-canvas.png?v=uhd"; + + function createWaterSim(simWidth, simHeight) { + const width = Math.max(24, simWidth | 0); + const height = Math.max(16, simHeight | 0); + let current = new Float32Array(width * height); + let next = new Float32Array(width * height); + let clock = 0; + + function poke(cx, cy, strength, radius) { + const x0 = cx | 0; + const y0 = cy | 0; + const rad = Math.max(1, radius); + const r2 = rad * rad; + const reach = Math.ceil(rad); + for (let y = -reach; y <= reach; y += 1) { + for (let x = -reach; x <= reach; x += 1) { + const px = x0 + x; + const py = y0 + y; + if (px < 1 || py < 1 || px >= width - 1 || py >= height - 1) continue; + const falloff = (x * x + y * y) / r2; + if (falloff > 1) continue; + current[py * width + px] += strength * (0.5 + 0.5 * Math.cos(Math.PI * Math.sqrt(falloff))); + } + } + } + + function step(dt) { + clock += dt; + for (let y = 1; y < height - 1; y += 1) { + let i = y * width + 1; + for (let x = 1; x < width - 1; x += 1, i += 1) { + next[i] = + ((current[i - 1] + current[i + 1] + current[i - width] + current[i + width]) * 0.5 - + next[i]) * + 0.9855 + + 0.0024 * Math.sin(clock * 0.7 + x * 0.05 + y * 0.021) + + 0.0019 * Math.sin(clock * 0.43 - x * 0.023 + y * 0.041); + } + } + const swap = current; + current = next; + next = swap; + } + + return { + width, + height, + poke, + step, + heights() { + return current; + }, + }; + } + + function ensureStyle() { + if (typeof document === "undefined") return null; + let style = document.getElementById(STYLE_ID); + if (!style) { + style = document.createElement("style"); + style.id = STYLE_ID; + document.head.append(style); + } + style.textContent = ` +#beauticode-gallery-bg{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;background:#0b1018} +#beauticode-gallery-bg img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center center;display:block;pointer-events:none;z-index:0;image-rendering:auto;-webkit-backface-visibility:hidden} +#beauticode-gallery-bg canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;mix-blend-mode:soft-light;opacity:.32} +html[data-bc-gallery="true"] #beauticode-bg-stage{background:transparent!important} +html[data-bc-gallery="true"] #beauticode-bg-stage::after{display:none!important;background:transparent!important} +html[data-bc-gallery="true"],html[data-bc-gallery="true"] body{background:transparent!important} +html[data-bc-gallery="true"] body{ + --dsw-alias-bg-base:rgba(17,20,27,.10); + --dsw-alias-bg-layer-1:rgba(26,30,39,.28); + --dsw-alias-bg-layer-2:rgba(35,40,51,.32); + --dsw-alias-bg-overlay:rgba(17,20,27,.12); + --dsw-specific-sidebar-fill:rgba(23,27,35,.28); +} +html[data-bc-resolved-tone="light"][data-bc-gallery="true"] body{ + --dsw-alias-bg-base:rgba(248,250,252,.12); + --dsw-alias-bg-layer-1:rgba(255,255,255,.28); + --dsw-alias-bg-layer-2:rgba(248,250,252,.32); + --dsw-alias-bg-overlay:rgba(255,255,255,.14); + --dsw-specific-sidebar-fill:rgba(255,255,255,.28); +} +html[data-bc-gallery="true"] #root{position:relative;z-index:1;background:transparent!important} +html[data-bc-gallery="true"] [class*="_fade"]{display:none!important} +html[data-bc-gallery="true"] #beauticode-bg-stage>img, +html[data-bc-gallery="true"] #beauticode-bg-stage>video{opacity:0!important} +html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;pointer-events:none!important} +`; + return style; + } + + const runtime = { + windowMode: "closed", + layer: null, + token: 0, + loop: 0, + lastTick: 0, + }; + + function stopLoop() { + if (runtime.loop) { + cancelAnimationFrame(runtime.loop); + runtime.loop = 0; + } + } + + function attachWater(canvas) { + const dpr = () => Math.max(1, Math.min(2, window.devicePixelRatio || 1)); + let sim = null; + let output = null; + let work = null; + let last = null; + + function sizeSim() { + const width = Math.max(32, canvas.clientWidth || window.innerWidth || 1); + const height = Math.max(32, canvas.clientHeight || window.innerHeight || 1); + const pixel = dpr(); + canvas.width = Math.round(width * pixel); + canvas.height = Math.round(height * pixel); + const simWidth = Math.max(80, Math.min(420, Math.round(width / 4))); + const simHeight = Math.max(45, Math.round(simWidth * (height / width))); + sim = createWaterSim(simWidth, simHeight); + output = null; + work = null; + } + + function localPoint(event) { + const rect = canvas.getBoundingClientRect(); + if (!rect.width || !rect.height || !sim) return null; + return { + x: ((event.clientX - rect.left) / rect.width) * sim.width, + y: ((event.clientY - rect.top) / rect.height) * sim.height, + }; + } + + function follow(event) { + if (!sim) return; + const point = localPoint(event); + if (!point) return; + if (last) { + const dx = point.x - last.x; + const dy = point.y - last.y; + const dist = Math.hypot(dx, dy); + const steps = Math.max(1, Math.min(8, Math.ceil(dist / 6))); + const strength = 0.55 + Math.min(1.8, dist / 18); + for (let i = 1; i <= steps; i += 1) { + const t = i / steps; + sim.poke(last.x + dx * t, last.y + dy * t, strength / steps, 2.4); + } + } else { + sim.poke(point.x, point.y, 0.7, 2.2); + } + last = point; + } + + function render() { + if (!sim) return; + sim.step(0.033); + const ctx = canvas.getContext("2d"); + if (!output) output = ctx.createImageData(sim.width, sim.height); + const dest = output.data; + const heights = sim.heights(); + for (let y = 0; y < sim.height; y += 1) { + const up = y > 0 ? y - 1 : y; + const down = y < sim.height - 1 ? y + 1 : y; + for (let x = 0; x < sim.width; x += 1) { + const i = y * sim.width + x; + const gx = heights[x > 0 ? i - 1 : i] - heights[x < sim.width - 1 ? i + 1 : i]; + const gy = heights[up * sim.width + x] - heights[down * sim.width + x]; + const di = i * 4; + let alpha = gy * 160; + if (alpha >= 0) { + dest[di] = 224; + dest[di + 1] = 238; + dest[di + 2] = 255; + dest[di + 3] = alpha > 110 ? 110 : alpha; + } else { + alpha = -alpha; + dest[di] = 8; + dest[di + 1] = 16; + dest[di + 2] = 32; + dest[di + 3] = alpha > 90 ? 90 : alpha; + } + } + } + if (!work) { + work = document.createElement("canvas"); + work.width = sim.width; + work.height = sim.height; + } + work.getContext("2d").putImageData(output, 0, 0); + const pixel = dpr(); + ctx.setTransform(pixel, 0, 0, pixel, 0, 0); + ctx.imageSmoothingEnabled = true; + if (ctx.imageSmoothingQuality) ctx.imageSmoothingQuality = "high"; + ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight); + ctx.drawImage(work, 0, 0, canvas.clientWidth, canvas.clientHeight); + } + + sizeSim(); + window.addEventListener("pointermove", follow, { passive: true }); + window.addEventListener("resize", sizeSim); + + return { + render, + destroy() { + window.removeEventListener("pointermove", follow); + window.removeEventListener("resize", sizeSim); + }, + }; + } + + function markPage(on) { + const root = document.documentElement; + if (on) { + root.dataset.bcGallery = "true"; + root.dataset.bcActive = "true"; + } else { + delete root.dataset.bcGallery; + if (!document.querySelector("#beauticode-bg-stage img, #beauticode-bg-stage video")) { + root.removeAttribute("data-bc-active"); + } + } + } + + function closeLayer() { + stopLoop(); + runtime.layer?.water?.destroy(); + runtime.layer?.node.remove(); + runtime.layer = null; + markPage(false); + } + + async function openLayer() { + const token = (runtime.token += 1); + ensureStyle(); + closeLayer(); + markPage(true); + const node = document.createElement("div"); + node.id = ROOT_ID; + node.setAttribute("aria-hidden", "true"); + const image = document.createElement("img"); + image.alt = ""; + image.decoding = "async"; + image.fetchPriority = "high"; + image.draggable = false; + const canvas = document.createElement("canvas"); + node.append(image, canvas); + document.body.prepend(node); + + await new Promise((resolve) => { + image.onload = resolve; + image.onerror = resolve; + image.src = CANVAS_URL; + }); + if (token !== runtime.token || runtime.windowMode === "closed") { + node.remove(); + return; + } + + const water = attachWater(canvas); + runtime.layer = { node, water }; + const tick = (ts) => { + if (!runtime.layer) return; + if (ts - runtime.lastTick > 32) { + runtime.lastTick = ts; + water.render(); + } + runtime.loop = requestAnimationFrame(tick); + }; + runtime.loop = requestAnimationFrame(tick); + } + + function setWindowMode(mode) { + const next = mode === "on" || mode === "window" || mode === "full" ? "on" : "closed"; + runtime.windowMode = next; + if (next === "closed") closeLayer(); + else void openLayer(); + return next; + } + + function getState() { + return { windowMode: runtime.windowMode }; + } + + const api = { + createWaterSim, + setWindowMode, + getState, + }; + + globalThis.BeauticodeAtmosphere = api; + if (typeof module !== "undefined" && module.exports) { + module.exports = api; + } +})(); diff --git a/integrations/deepseek-harness/client.js b/integrations/deepseek-harness/client.js index 7c6f34a..359332f 100644 --- a/integrations/deepseek-harness/client.js +++ b/integrations/deepseek-harness/client.js @@ -247,10 +247,16 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; } } + function syncGallery(payload) { + const on = payload?.atmosphere?.preset === "gallery"; + globalThis.BeauticodeAtmosphere?.setWindowMode?.(on ? "on" : "closed"); + } + async function applyBackground(payload) { if (!Number.isSafeInteger(payload?.generation)) return; activePayload = payload; document.documentElement.dataset.bcGeneration = String(payload.generation); + syncGallery(payload); if (payload.media === "clear") { desiredModes.fish = false; playbackBlocked = false; @@ -258,6 +264,9 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important; document.documentElement.removeAttribute("data-bc-media"); document.documentElement.removeAttribute("data-bc-fish"); document.getElementById("beauticode-bg-stage")?.remove(); + if (document.documentElement.dataset.bcGallery === "true") { + document.documentElement.dataset.bcActive = "true"; + } await acknowledgeRender(payload, true, false); await acknowledgeMode(); return; diff --git a/integrations/deepseek-harness/console.js b/integrations/deepseek-harness/console.js index 4a52384..31e6c27 100644 --- a/integrations/deepseek-harness/console.js +++ b/integrations/deepseek-harness/console.js @@ -142,8 +142,16 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back statusEl.textContent = data?.error || "未就绪"; return; } - const label = data.media === "video" ? "视频" : data.media === "image" ? "图片" : "无背景"; + const label = + data.atmosphere === "gallery" + ? "画窗" + : data.media === "video" + ? "视频" + : data.media === "image" + ? "图片" + : "无背景"; statusEl.textContent = label; + if (data.atmosphere === "gallery") currentThemeId = "builtin-gallery"; muted = data.muted !== false; soundBtn.classList.toggle("on", !muted); soundBtn.textContent = muted ? "声音" : "声音开"; @@ -229,6 +237,7 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back fileInput.click(); }); pop.querySelector('[data-act="clear"]').addEventListener("click", () => { + currentThemeId = ""; void run(() => request("/__beauticode/ui/clear", { method: "POST" })); }); soundBtn.addEventListener("click", () => { @@ -252,6 +261,9 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back currentThemeId = item.getAttribute("data-theme-id") || ""; themeList.hidden = true; themeToggle.setAttribute("aria-expanded", "false"); + globalThis.BeauticodeAtmosphere?.setWindowMode?.( + currentThemeId === "builtin-gallery" ? "on" : "closed", + ); void run(() => request("/__beauticode/ui/theme/use", { method: "POST", @@ -264,6 +276,7 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back const file = fileInput.files?.[0]; fileInput.value = ""; if (!file) return; + currentThemeId = ""; void run(() => request("/__beauticode/ui/import", { method: "POST", diff --git a/integrations/deepseek-harness/host-apply.mjs b/integrations/deepseek-harness/host-apply.mjs index 2e4d04d..87fa10d 100644 --- a/integrations/deepseek-harness/host-apply.mjs +++ b/integrations/deepseek-harness/host-apply.mjs @@ -5,6 +5,7 @@ import { readDshControlFile, readTrayClaim, } from "./control-client.mjs"; +import { canvasImagePath } from "./presets.mjs"; const sessions = new Map(); @@ -91,6 +92,7 @@ export async function ensureInProcessSession(options) { dataRoot, baseUrl, verifyDeadlineMs: 30_000, + bundledGalleryImagePath: canvasImagePath() || undefined, }); try { await session.start(); diff --git a/integrations/deepseek-harness/index.mjs b/integrations/deepseek-harness/index.mjs index a1ba26c..2284179 100644 --- a/integrations/deepseek-harness/index.mjs +++ b/integrations/deepseek-harness/index.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { registerAgentSurfaces } from "./agent.mjs"; import { resolvePluginBaseUrl } from "./host-apply.mjs"; import { createBeauticodeUi } from "./ui-host.mjs"; +import { canvasImagePath, iceFrostImagePath, normalizeAtmosphere } from "./presets.mjs"; export const name = "beauticode-bridge"; export const inject = ["webServer"]; @@ -127,13 +128,15 @@ function validLoopbackMediaUrl(value) { function validApplyPayload(body) { if (!Number.isSafeInteger(body.generation) || body.generation < 0) return false; if (!["image", "video", "clear"].includes(body.media)) return false; + if (body.atmosphere != null && !normalizeAtmosphere(body.atmosphere)) return false; if (body.media === "clear") { - return body.imageUrl == null && body.videoUrl == null && body.startAt == null; + return body.imageUrl == null && body.videoUrl == null && body.startAt == null && body.atmosphere == null; } if (!validLoopbackMediaUrl(body.imageUrl)) return false; if (body.media === "image") return body.videoUrl == null && body.startAt == null; return ( validLoopbackMediaUrl(body.videoUrl) && + body.atmosphere == null && (body.startAt == null || (Number.isFinite(body.startAt) && body.startAt >= 0)) ); } @@ -206,6 +209,7 @@ export function apply(ctx, config = {}) { const disposeTap = ctx.webServer.tapIndex((html) => { if (html.includes("data-beauticode-bridge")) return html; const script = + '' + '' + ''; return html.includes("") @@ -230,6 +234,67 @@ export function apply(ctx, config = {}) { sendJson(res, 200, { ok: true, ...identity }); }, }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/atmosphere.js", + handler: async (req, res) => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405).end(); + return; + } + const source = await fs.readFile(path.join(here, "atmosphere.js")); + res.writeHead(200, { + "content-type": "text/javascript; charset=utf-8", + "cache-control": "no-store", + "content-length": source.length, + }); + res.end(req.method === "HEAD" ? undefined : source); + }, + }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/themes/bg-canvas.png", + handler: async (req, res) => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405).end(); + return; + } + const filePath = canvasImagePath(); + if (!filePath) { + res.writeHead(404).end(); + return; + } + const source = await fs.readFile(filePath); + res.writeHead(200, { + "content-type": "image/png", + "cache-control": "public, max-age=86400", + "content-length": source.length, + }); + res.end(req.method === "HEAD" ? undefined : source); + }, + }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/themes/ice-frost.png", + handler: async (req, res) => { + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405).end(); + return; + } + const filePath = iceFrostImagePath(); + if (!filePath) { + res.writeHead(404).end(); + return; + } + const source = await fs.readFile(filePath); + res.writeHead(200, { + "content-type": "image/png", + "cache-control": "public, max-age=86400", + "content-length": source.length, + }); + res.end(req.method === "HEAD" ? undefined : source); + }, + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/client.js", @@ -289,6 +354,11 @@ export function apply(ctx, config = {}) { path: "/__beauticode/ui/theme/use", handler: (req, res) => ui.useTheme(req, res), }), + ctx.webServer.register({ + kind: "exact", + path: "/__beauticode/ui/preset", + handler: (req, res) => ui.applyPreset(req, res), + }), ctx.webServer.register({ kind: "exact", path: "/__beauticode/events", @@ -349,6 +419,8 @@ export function apply(ctx, config = {}) { videoUrl: body.media === "video" ? body.videoUrl : null, startAt: body.media === "video" ? body.startAt ?? null : null, }; + const atmosphere = normalizeAtmosphere(body.atmosphere); + if (atmosphere) current.atmosphere = atmosphere; if (body.media === "clear") modes.fish = false; for (const state of clientStates.values()) state.render = null; broadcast({ type: "apply", ...current }); diff --git a/integrations/deepseek-harness/package.json b/integrations/deepseek-harness/package.json index 2b7e906..6f45303 100644 --- a/integrations/deepseek-harness/package.json +++ b/integrations/deepseek-harness/package.json @@ -13,6 +13,8 @@ "index.mjs", "client.js", "console.js", + "atmosphere.js", + "presets.mjs", "agent.mjs", "control-client.mjs", "host-apply.mjs", diff --git a/integrations/deepseek-harness/presets.mjs b/integrations/deepseek-harness/presets.mjs new file mode 100644 index 0000000..2a01154 --- /dev/null +++ b/integrations/deepseek-harness/presets.mjs @@ -0,0 +1,67 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); + +export const ATMOSPHERE_PRESETS = Object.freeze({ + internal: Object.freeze({ + id: "internal", + name: "Internal", + file: "bg-internal.jpg", + tone: "light", + }), + infernal: Object.freeze({ + id: "infernal", + name: "Infernal", + file: "bg-infernal.jpg", + tone: "dark", + }), +}); + +export function normalizeAtmosphere(value) { + if (!value || typeof value !== "object") return null; + if ( + value.preset !== "internal" && + value.preset !== "infernal" && + value.preset !== "gallery" + ) { + return null; + } + return { preset: value.preset, rain: true, overlay: true, water: true }; +} + +export function effectsForPreset(id) { + return normalizeAtmosphere({ preset: id }); +} + +export function themeAssetsDir() { + const candidates = [ + path.join(here, "themes", "internal-beyond"), + path.join(here, "..", "..", "assets", "themes", "internal-beyond"), + ]; + for (const dir of candidates) { + if (fs.existsSync(path.join(dir, "bg-internal.jpg"))) return dir; + } + return candidates[candidates.length - 1]; +} + +export function presetImagePath(id) { + const preset = ATMOSPHERE_PRESETS[id]; + if (!preset) return null; + const filePath = path.join(themeAssetsDir(), preset.file); + return fs.existsSync(filePath) ? filePath : null; +} + +export function canvasImagePath() { + const dir = themeAssetsDir(); + const hi = path.join(dir, "bg-canvas-4k.png"); + if (fs.existsSync(hi)) return hi; + const filePath = path.join(dir, "bg-canvas.png"); + return fs.existsSync(filePath) ? filePath : null; +} + +export function iceFrostImagePath() { + const filePath = path.join(themeAssetsDir(), "ice-frost.png"); + return fs.existsSync(filePath) ? filePath : null; +} diff --git a/integrations/deepseek-harness/test/atmosphere.test.mjs b/integrations/deepseek-harness/test/atmosphere.test.mjs new file mode 100644 index 0000000..8caca93 --- /dev/null +++ b/integrations/deepseek-harness/test/atmosphere.test.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import vm from "node:vm"; +import { fileURLToPath } from "node:url"; +import { canvasImagePath, normalizeAtmosphere } from "../presets.mjs"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const atmosphereSource = fs.readFileSync(path.join(here, "../atmosphere.js"), "utf8"); +const atmosphereSandbox = { BeauticodeAtmosphere: null }; +vm.runInNewContext(atmosphereSource, atmosphereSandbox); +const atmosphere = atmosphereSandbox.BeauticodeAtmosphere; + +test("gallery canvas asset is present", () => { + const filePath = canvasImagePath(); + assert.ok(fs.existsSync(filePath)); + assert.match(filePath, /bg-canvas-4k\.png$/); +}); + +test("water sim rises under a poke so mouse follow can drive ripples", () => { + const water = atmosphere.createWaterSim(64, 36); + const before = water.heights()[18 * 64 + 32]; + water.poke(32, 18, 2, 3); + const after = water.heights()[18 * 64 + 32]; + assert.ok(after > before); + water.step(0.033); + assert.equal(water.heights().length, 64 * 36); +}); + +test("gallery is a valid atmosphere preset", () => { + assert.equal(normalizeAtmosphere({ preset: "gallery" }).preset, "gallery"); +}); + +test("atmosphere.js stays a valid browser script", () => { + const source = fs.readFileSync(path.join(here, "../atmosphere.js"), "utf8"); + assert.doesNotThrow(() => new Function(source)); +}); diff --git a/integrations/deepseek-harness/test/plugin.test.mjs b/integrations/deepseek-harness/test/plugin.test.mjs index d6e9960..8cf6e00 100644 --- a/integrations/deepseek-harness/test/plugin.test.mjs +++ b/integrations/deepseek-harness/test/plugin.test.mjs @@ -88,7 +88,12 @@ test("plugin injects its client script exactly once", async (t) => { const once = tap(""); const twice = tap(once); assert.equal((twice.match(/data-beauticode-bridge/g) || []).length, 1); + assert.match(once, /__beauticode\/atmosphere\.js/); assert.match(once, /__beauticode\/console\.js/); + const atmosphere = await fetch(`${plugin.origin}/__beauticode/atmosphere.js`); + assert.equal(atmosphere.status, 200); + const atmosphereSource = await atmosphere.text(); + assert.doesNotThrow(() => new Function(atmosphereSource)); const response = await fetch(`${plugin.origin}/__beauticode/client.js`); assert.equal(response.status, 200); const source = await response.text(); @@ -104,6 +109,8 @@ test("plugin injects its client script exactly once", async (t) => { assert.match(source, /prefers-color-scheme: dark/); assert.match(source, /new MutationObserver\(scheduleDshThemeSync\)/); assert.match(source, /function dshStructureIssue\(\)/); + assert.match(source, /function syncGallery\(/); + assert.match(source, /preset === "gallery"/); assert.match(source, /未找到 #root/); const version = await fetch(`${plugin.origin}/__beauticode/version`); @@ -264,6 +271,59 @@ test("authenticated apply reaches SSE client and same-origin ack becomes ready", }); }); +test("apply payload can carry Internal atmosphere to the browser client", async (t) => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "beauticode-dsh-plugin-")); + const tokenFile = path.join(root, "token"); + await fs.writeFile(tokenFile, TOKEN); + const plugin = await createPluginServer(tokenFile); + const events = await openEvents(plugin.origin, "client-atmosphere-01"); + t.after(async () => { + events.request.destroy(); + events.response.destroy(); + await plugin.dispose(); + await fs.rm(root, { recursive: true, force: true }); + }); + + const payload = { + generation: 21, + media: "image", + imageUrl: "http://127.0.0.1:45678/media/image?t=internal", + atmosphere: { preset: "internal", rain: true, overlay: true, water: true }, + }; + const applied = await fetch(`${plugin.origin}/__beauticode/apply`, { + method: "POST", + headers: { Authorization: `Bearer ${TOKEN}`, "content-type": "application/json" }, + body: JSON.stringify(payload), + }); + assert.equal(applied.status, 200); + await new Promise((resolve) => setTimeout(resolve, 20)); + assert.match(events.read(), /"preset":"internal"/); + + const gallery = await fetch(`${plugin.origin}/__beauticode/apply`, { + method: "POST", + headers: { Authorization: `Bearer ${TOKEN}`, "content-type": "application/json" }, + body: JSON.stringify({ + generation: 23, + media: "image", + imageUrl: "http://127.0.0.1:45678/media/image?t=gallery", + atmosphere: { preset: "gallery", rain: true, overlay: true, water: true }, + }), + }); + assert.equal(gallery.status, 200); + + const rejected = await fetch(`${plugin.origin}/__beauticode/apply`, { + method: "POST", + headers: { Authorization: `Bearer ${TOKEN}`, "content-type": "application/json" }, + body: JSON.stringify({ + generation: 22, + media: "image", + imageUrl: "http://127.0.0.1:45678/media/image?t=internal", + atmosphere: { preset: "night" }, + }), + }); + assert.equal(rejected.status, 400); +}); + test("video apply and display modes are broadcast and acknowledged", async (t) => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "beauticode-dsh-plugin-")); const tokenFile = path.join(root, "token"); diff --git a/integrations/deepseek-harness/test/ui-host.test.mjs b/integrations/deepseek-harness/test/ui-host.test.mjs index 131959e..aa0f6cd 100644 --- a/integrations/deepseek-harness/test/ui-host.test.mjs +++ b/integrations/deepseek-harness/test/ui-host.test.mjs @@ -96,6 +96,10 @@ test("plugin injects a compact sidebar console script", async (t) => { assert.doesNotMatch(source, /