/** * dsh-float-window — Host 半(标准 DSH / cordis 插件) * * 职责: * 1. 把原生浮窗需要的接入信息写成 `%DSH_HOME%\float-window\endpoint.json` * (WebView2 壳读它来连这里;因为壳是独立进程,拿不到 ctx)。 * 2. 提供 SSE 数据通道 `/dsh-float-window/events`:会话消息 + 计费指标快照。 * 3. 提供 `/dsh-float-window/cmd`:浮窗 → 宿主的控制命令。 * 4. 注册 `float_window` 工具,让 Agent 能自己把浮窗调出来 / 收起 / 置顶 / 全屏。 * * 设计原则:宿主内部 API 一律"探测式"使用(ctx.get(...) + try/catch), * 任何一个服务缺失都只是少几个指标,绝不能让插件加载失败拖垮 DSH。 */ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { randomUUID } from 'node:crypto' import { fileURLToPath } from 'node:url' export const name = 'dsh-float-window' export const inject = ['webServer'] const HERE = path.dirname(fileURLToPath(import.meta.url)) const REPO = path.resolve(HERE, '..') const ROUTE = '/dsh-float-window' function dshHome() { return process.env.DSH_HOME || path.join(os.homedir(), '.dsh') } /** 从 content 数组/字符串里取纯文本 */ function textOf(content) { if (typeof content === 'string') return content if (Array.isArray(content)) { return content .map((c) => (typeof c === 'string' ? c : (c?.type === 'text' || c?.text ? String(c.text ?? '') : ''))) .join('') } return '' } /** * 会话事件 → 浮窗消息行的宽松映射。 * 实测(dsh 0.1.x)事件载荷在 `event.data` 下: * user/message { data: { content:[{type:'text',text}], role, source:{kind:'user'|'plugin'} , id } } * assistant/chunk { data: { turn, step, chunk:{ type, text?, reason? } } } * assistant/message{ data: { content:[...] } } * tool/call { data: { name, args } } * 插件注入的消息(source.kind === 'plugin',例如系统提示快照)不进镜像。 */ function toRow(event) { if (!event || typeof event !== 'object') return null const type = String(event.type || '') const d = event.data || event const src = d.source || event.source if (type === 'user/message') { if (src && src.kind && src.kind !== 'user') return null const text = textOf(d.content ?? d.message?.content ?? d.text) return text ? { role: 'user', text: clip(text) } : null } if (type === 'assistant/message') { const text = textOf(d.content ?? d.message?.content ?? d.text) return text ? { role: 'assistant', text: clip(text) } : null } if (type === 'assistant/chunk') { const c = d.chunk || {} const kind = String(c.type || '') const piece = c.text ?? c.delta ?? c.value ?? (kind.includes('text') ? c.content : '') if (piece) return { role: 'assistant', text: String(piece), streaming: true } if (kind === 'finish' && c.reason?.kind === 'error') { return { role: 'assistant', text: '⚠️ ' + short(c.reason.failure ?? c.reason, 240) } } return null } if (type === 'tool/call') { return { role: 'tool', text: `${d.name || d.tool || 'tool'}(${short(d.args ?? d.input ?? '', 160)})` } } return null } function clip(text, n = 4000) { const s = String(text) return s.length > n ? s.slice(0, n) + '…' : s } function short(v, n = 120) { try { const s = typeof v === 'string' ? v : JSON.stringify(v) return s && s.length > n ? s.slice(0, n) + '…' : s || '' } catch { return '' } } /** 脱敏采样:保留结构与短字符串,避免把长内容/密钥写进状态文件 */ function sanitize(v, depth = 0) { if (v == null || typeof v === 'number' || typeof v === 'boolean') return v if (typeof v === 'string') return v.length > 200 ? v.slice(0, 200) + '…' : v if (depth >= 4) return Array.isArray(v) ? `[array ${v.length}]` : '[object]' if (Array.isArray(v)) return v.slice(0, 6).map((x) => sanitize(x, depth + 1)) const out = {} for (const [k, val] of Object.entries(v)) out[k] = sanitize(val, depth + 1) return out } const SAMPLE_TYPES = new Set(['user/message', 'assistant/chunk', 'assistant/message', 'tool/call', 'tool/result', 'session/title', 'request/header', 'request/context']) export function apply(ctx) { // 运行状态与自检日志落盘:插件跑在 DSH 内部,控制台不一定看得到, // 有了这个文件(和 /health)才能在用户机器上远程排障。 const notes = [] const seenTypes = new Map() // 事件类型直方图:写进状态文件,便于在用户机器上核对字段名 const samples = {} // 每种关键事件的脱敏样本(最多 3 条),用于对齐不同 DSH 版本的字段名 let lastRow = null const statusFile0 = path.join(dshHome(), 'float-window', 'plugin-status.json') const status = { version: '0.1.0', pid: process.pid, startedAt: new Date().toISOString(), tool: false, settings: false, notes } const writeStatus = () => { try { status.notes = notes.slice(-40) status.eventTypes = Object.fromEntries([...seenTypes].slice(-40)) status.samples = samples status.lastRow = lastRow fs.mkdirSync(path.dirname(statusFile0), { recursive: true }) fs.writeFileSync(statusFile0, JSON.stringify(status, null, 2)) } catch { } } const log = (...a) => { const line = a.map((x) => (typeof x === 'string' ? x : short(x, 200))).join(' ') notes.push(`${new Date().toISOString().slice(11, 19)} ${line}`) if (notes.length > 80) notes.shift() try { (ctx.logger?.info || console.log)('[float-window]', ...a) } catch { } writeStatus() } // ---------------------------------------------------------------- endpoint const endpointFile = path.join(dshHome(), 'float-window', 'endpoint.json') const writeEndpoint = () => { try { fs.mkdirSync(path.dirname(endpointFile), { recursive: true }) const data = { baseUrl: `http://127.0.0.1:${ctx.webServer.port}`, host: ctx.webServer.host, pid: process.pid, updatedAt: new Date().toISOString(), } fs.writeFileSync(endpointFile, JSON.stringify(data, null, 2)) log('endpoint written', data.baseUrl, '->', endpointFile) } catch (e) { log('endpoint write failed:', String(e?.message || e)) } } writeEndpoint() ctx.effect(() => () => { try { fs.rmSync(endpointFile, { force: true }) } catch { } }) // ---------------------------------------------------------------- 版本自适应的宿主调用 // dsh 0.1.x 早期:ctx.apiProxy.sessions.*(信封 { rpcId, payload }) // dsh 0.1.7+ :ctx.sessionController.*(Typert Remote 的宿主面,扁平信封) // 两个都试,谁在就用谁;都没有就返回可读错误,绝不让插件抛出去。 async function hostCall(method, payload) { const sc = ctx.get('sessionController') if (sc && typeof sc[method] === 'function') { const flat = { ...payload } delete flat.rpcId if (!flat.requestId) flat.requestId = randomUUID() // 0.1.7 的 Remote 签名是 (request, signal);signal 缺失时宿主内部会 // "Cannot read properties of undefined (reading 'throwIfAborted')" const r = await sc[method](flat, new AbortController().signal) return { value: r?.value ?? r, via: 'sessionController.' + method } } const api = ctx.get('apiProxy') if (api?.sessions && typeof api.sessions[method] === 'function') { const r = await api.sessions[method]({ rpcId: randomUUID(), payload }) return { value: r?.result?.value ?? r?.result ?? r, via: 'apiProxy.sessions.' + method } } throw new Error(`no host API for "${method}" (tried sessionController / apiProxy)`) } const MAX_ROWS = 120 const rows = new Map() // sessionId -> row[] const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字) const titles = new Map() // sessionId -> 会话标题(来自 session/title 事件) let lastSessionId = null function counter(id) { let c = counts.get(id) if (!c) { c = { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, stepStart: 0, lastTps: null }; counts.set(id, c) } return c } ctx.on('session/event', (session, event) => { try { const id = session?.id || session?.sessionId || 'unknown' lastSessionId = id const c = counter(id) const type = String(event?.type || '') const d2 = () => event?.data || event || {} seenTypes.set(type, (seenTypes.get(type) || 0) + 1) if (SAMPLE_TYPES.has(type) && (samples[type]?.length ?? 0) < 3) { (samples[type] ||= []).push(sanitize(event)) } if (type === 'turn/end') c.turns++ else if (type === 'session/title') { const t = d2(event)?.title if (t) titles.set(id, String(t)) } else if (type === 'step/start') c.stepStart = Date.now() else if (type === 'step/end') c.steps++ else if (type === 'assistant/message') { const usage = event?.usage || event?.message?.usage || {} const out = usage.outputTokens ?? usage.output_tokens ?? usage.completionTokens const ms = c.stepStart ? Date.now() - c.stepStart : 0 if (out && ms > 0) { c.outTokens += out; c.decodeMs += ms; c.lastTps = out / (ms / 1000) } } const row = toRow(event) lastRow = row ? { type, row } : { type, row: null } if (!row) return const list = rows.get(id) || [] const last = list[list.length - 1] if (row.streaming && last && last.role === row.role && last.streaming) last.text += row.text else list.push(row) while (list.length > MAX_ROWS) list.shift() rows.set(id, list) } catch { } }) // ---------------------------------------------------------------- 指标 let meterKeysLogged = false function metricsFor(session) { const id = session?.id || lastSessionId || 'unknown' const c = counts.get(id) || { turns: 0, steps: 0, outTokens: 0, decodeMs: 0, lastTps: null } const out = { turns: c.turns || null, steps: c.steps || null, tps: c.lastTps, tokens: null, cacheHit: null, contextUsed: null, } // 有 session-projection 就用它(更权威),否则用事件计数兜底 try { const proj = ctx.get('sessionProjection') const st = proj?.get?.(session, 'sessionStats') || proj?.peek?.(session, 'sessionStats') if (st) { if (st.turns != null) out.turns = st.turns if (st.steps != null) out.steps = st.steps if (st.decodeTokens && st.decodeMs) out.tps = st.decodeTokens / (st.decodeMs / 1000) } } catch { } try { const meter = ctx.get('tokenMeter') const m = meter?.measure?.(session) if (m) { // 第一次把真实字段名记进状态文件,便于在用户机器上核对映射 if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter keys: ' + Object.keys(m).join(',')) } out.tokens = m.totalTokens ?? m.surfaceTokens ?? null const u = m.usage || m.providerUsage || m.usageBuckets if (u) { const read = u.cacheRead ?? u.cache_read ?? u.cacheReadTokens ?? 0 const input = u.input ?? u.inputTokens ?? 0 const denom = input + read if (denom > 0) out.cacheHit = read / denom } let capacity = modelContext() const used = m.surfaceTokens ?? m.totalTokens if (capacity && used) out.contextUsed = Math.min(1, used / capacity) } } catch (e) { if (!meterKeysLogged) { meterKeysLogged = true; log('tokenMeter measure failed: ' + String(e?.message || e)) } } return out } /** * 模型上下文容量。 * 踩坑(实测):`llm.resolveModelInfo()` 在没注册 provider 时**会抛错**,而且它返回 * Promise —— 只 try/catch 同步部分会留下 unhandled rejection,直接让 DSH 进程 * "fatal load failure" 退出(我在隔离实例里把 DSH 弄崩过一次)。 * 所以这里:同步异常、异步 reject、undefined 三种情况全部吞掉,并且只在拿到值时缓存。 */ let cachedCapacity = null function modelContext() { try { const llm = ctx.get('llm') const r = llm?.resolveModelInfo?.() if (r && typeof r.then === 'function') { r.then((v) => { cachedCapacity = v?.context ?? cachedCapacity }).catch(() => { }) return cachedCapacity } if (r?.context) cachedCapacity = r.context return cachedCapacity } catch { return cachedCapacity } } function snapshot() { const sessions = (() => { try { return ctx.get('sessions')?.list?.() || [] } catch { return [] } })() let session = sessions.find((s) => s?.id === lastSessionId) || sessions[sessions.length - 1] || null const id = session?.id || lastSessionId return { ok: true, at: Date.now(), sessionId: id || null, title: (id && titles.get(id)) || session?.meta?.title || session?.title || null, messages: (id && rows.get(id)) || [], metrics: session ? metricsFor(session) : { turns: null, steps: null, tps: null, tokens: null, cacheHit: null, contextUsed: null }, sessions: sessions.slice(-10).map((s) => ({ id: s?.id, title: s?.meta?.title || s?.title || null })), } } // ---------------------------------------------------------------- 路由 let tickCount = 0 const sseClients = new Set() const push = () => { if (!sseClients.size) return const payload = `data: ${JSON.stringify(snapshot())}\n\n` for (const res of sseClients) { try { res.write(payload) } catch { } } } const tick = setInterval(() => { push(); if (++tickCount % 10 === 0) writeStatus() }, 1000) ctx.effect(() => () => clearInterval(tick)) const handler = async (req, res) => { const url = new URL(req.url || '/', 'http://127.0.0.1') const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'content-type', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', } if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return } if (url.pathname === `${ROUTE}/events`) { res.writeHead(200, { ...cors, 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }) res.write(': connected\n\n') res.write(`data: ${JSON.stringify(snapshot())}\n\n`) sseClients.add(res) req.on('close', () => sseClients.delete(res)) return } if (url.pathname === `${ROUTE}/state`) { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify(snapshot())) return } if (url.pathname === `${ROUTE}/health`) { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ok: true, version: '0.1.0', shell: shellPath(), tool: status.tool, settings: status.settings })) return } // 悬浮窗聊天框 -> 会话。走宿主自己的网关服务 ctx.apiProxy(和浏览器 UI 同一条路), // 不经过 /api 的浏览器信任围栏,所以 file:// 的浮窗页面也能发消息。 if (url.pathname === `${ROUTE}/chat` && req.method === 'POST') { let text = '' let sessionId = null try { const chunks = [] for await (const c of req) chunks.push(c) const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}') text = String(body.text || '').slice(0, 20000) sessionId = body.sessionId || null } catch (e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ok: false, error: 'bad-body: ' + String(e?.message || e) })) return } if (!text) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ok: false, error: 'empty text' })) return } const target = sessionId || lastSessionId try { if (!target) throw new Error('no session yet') const { value, via } = await hostCall('prompt', { sessionId: target, mode: 'queue', content: [{ type: 'text', text }], clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }) log(`chat -> session ${target} via ${via}: ${short(value, 160)}`) res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ok: true, sessionId: target, result: value, via })) } catch (e) { const detail = e?.data || e?.message || e log('chat failed: ' + short(detail, 200)) res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ok: false, error: short(detail, 300) })) } return } // 会话控制:列表 / 新建 / 打断(浮窗顶部的"新会话"和"停止"将来用这里) if (url.pathname === `${ROUTE}/session` && req.method === 'POST') { let body = {} try { const chunks = [] for await (const c of req) chunks.push(c) body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}') } catch { } const json = (obj) => { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)) } try { if (body.action === 'create') { const { value, via } = await hostCall('create', { sessionId: body.sessionId ? String(body.sessionId) : randomUUID(), ...(body.cwd ? { cwd: String(body.cwd) } : {}), ...(body.workspaceId ? { workspaceId: body.workspaceId } : {}), ...(body.agentPreset ? { agentPreset: String(body.agentPreset) } : {}), }) const sessionId = value?.sessionId || null if (sessionId) lastSessionId = sessionId log(`session created via ${via}: ` + short(value, 160)) json({ ok: true, sessionId, via, result: value }) } else if (body.action === 'cancel') { const { value, via } = await hostCall('cancel', { sessionId: body.sessionId || lastSessionId }) json({ ok: true, via, result: value }) } else { json({ ok: true, sessions: snapshot().sessions, current: lastSessionId }) } } catch (e) { log('session route failed: ' + short(e?.data || e?.message || e, 200)) if (e?.stack) log('stack: ' + String(e.stack).split('\n').slice(0, 4).join(' | ')) json({ ok: false, error: short(e?.data || e?.message || e, 300) }) } return } // 浮窗动作(Agent 工具走同一段逻辑):open/close/pin/sizeLock/topmost/fullscreen/hide/show/status if (url.pathname === `${ROUTE}/window` && req.method === 'POST') { let body = {} try { const chunks = [] for await (const c of req) chunks.push(c) body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}') } catch { } const r = await windowAction(body.action) res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }) res.end(JSON.stringify({ ...r, alive: shellAlive() })) return } res.writeHead(404, { ...cors, 'Content-Type': 'text/plain; charset=utf-8' }) res.end('dsh-float-window: not found') } ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: ROUTE, handler })) // ---------------------------------------------------------------- 壳进程 const stateDir = path.join(dshHome(), 'float-window') const cmdFile = path.join(stateDir, 'command.json') const aliveFile = path.join(stateDir, 'alive.json') function shellPath() { return path.join(REPO, 'shell', 'run-float.cmd') } /** 浮窗动作的唯一入口:Agent 工具和 /window 路由都走这里 */ async function windowAction(action) { const a = String(action || 'open') if (a === 'status') { const s = snapshot() return { ok: true, text: `alive=${shellAlive()} session=${s.sessionId || 'none'} messages=${s.messages.length} turns=${s.metrics.turns ?? '?'} steps=${s.metrics.steps ?? '?'}` } } if (a === 'hide') broadcast({ type: 'cmd', cmd: 'hide' }) const r = await controlFloat(a) return { ok: !!r.ok, text: r.ok ? `${a} ok (${r.via})` : `${a} failed: ${r.error}`, via: r.via, error: r.error } } /** 壳每 700ms 写一次心跳,据此判断浮窗是否还活着 */ function shellAlive() { try { return Date.now() - fs.statSync(aliveFile).mtimeMs < 8000 } catch { return false } } async function spawnShell() { const cmd = shellPath() if (!fs.existsSync(cmd)) return { ok: false, error: `launcher not found: ${cmd}` } try { const shell = ctx.get('shell') const sp = ctx.get('sandboxPolicy') const policy = sp?.resolve?.({ mode: 'danger-full-access' }) if (shell?.resolve && shell?.run) { const spec = shell.resolve({ command: `cmd /c start "" "${cmd}"`, ...(policy ? { sandboxPolicy: policy } : {}) }) await shell.run(spec) return { ok: true, via: 'ctx.shell' } } } catch (e) { log('ctx.shell launch failed, fallback to spawn:', String(e?.message || e)) } try { const { spawn } = await import('node:child_process') spawn('cmd', ['/c', 'start', '', cmd], { detached: true, stdio: 'ignore', windowsHide: true }).unref() return { ok: true, via: 'spawn' } } catch (e) { return { ok: false, error: String(e?.message || e) } } } /** 浮窗已经在跑就投递命令,否则按需拉起 */ async function controlFloat(action) { if (action === 'open' && !shellAlive()) return spawnShell() if (action === 'close' && !shellAlive()) return { ok: true, via: 'already closed' } try { fs.mkdirSync(stateDir, { recursive: true }) fs.writeFileSync(cmdFile, JSON.stringify({ cmd: action, at: new Date().toISOString() })) return { ok: true, via: 'command file' } } catch (e) { return { ok: false, error: String(e?.message || e) } } } // ---------------------------------------------------------------- Agent 工具 status.shell = shellPath() writeStatus() try { const tools = ctx.get('tools') if (tools?.register) { tools.register({ name: 'float_window', description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。', parameters: { action: { type: 'string', required: true, description: 'open | close | pin | fullscreen | hide | status' }, }, output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] }, async execute(args) { const r = await windowAction(args?.action) return `float-window: ${r.text}` }, }) status.tool = true log('tool float_window registered') } else { log('tools service not available; agent trigger disabled') } } catch (e) { log('tool registration failed:', String(e?.message || e)) } writeStatus() const broadcast = (msg) => { const payload = `data: ${JSON.stringify(msg)}\n\n` for (const res of sseClients) { try { res.write(payload) } catch { } } } const pushHidden = () => broadcast({ type: 'cmd', cmd: 'hide' }) // 抓屏前隐藏的对外门面:交给宿主的共享对象。 // 注意 cordis 要求先 provide 再 set,否则会抛 "cannot set property ... without provide" // (实测:漏了 provide 会让整个插件树加载失败、DSH 起不来)。这里做成可选的, // 拿不到 provide 就退化成只广播 SSE 命令。 try { ctx.provide?.('floatWindow') ctx.set?.('floatWindow', { hide: pushHidden, show: () => broadcast({ type: 'cmd', cmd: 'show' }), snapshot, endpoint: () => endpointFile }) } catch (e) { log('floatWindow facade skipped:', String(e?.message || e)) } // ---------------------------------------------------------------- DSH 设置入口 // 注册一个设置命名空间,DSH 的「设置 → 插件」会把它渲染成一张卡片。 // schemastery 是从 profile 的 node_modules 解析的;link: 安装的插件解析不到时 // 只跳过这一步(浮窗自己的设置面板照常可用)。 log('services: ' + ['settings', 'sessions', 'tools', 'tokenMeter', 'sessionProjection', 'llm', 'shell', 'sandboxPolicy', 'webServer'] .map((n) => `${n}=${(() => { try { return ctx.get(n) ? 'y' : 'n' } catch { return '?' } })()}`).join(' ')) ; (async () => { try { const settings = ctx.get('settings') if (!settings?.register) { log('settings service absent; skip settings entry'); return } let z = null // link: 安装时插件自己的目录里没有 node_modules,裸导入会失败; // 依次尝试:常规解析 -> 以宿主入口为基准解析 -> profile 目录解析 try { z = (await import('@deepseek-ai/schemastery')).default } catch { } if (!z) { try { const { createRequire } = await import('node:module') const req = createRequire(process.argv[1] || path.join(dshHome(), 'index.js')) const m = req('@deepseek-ai/schemastery') z = m?.default || m } catch { } } if (!z) { try { const { createRequire } = await import('node:module') for (const prof of ['desktop', 'web']) { const req = createRequire(path.join(dshHome(), 'profiles', prof, 'package.json')) const m = req('@deepseek-ai/schemastery') z = m?.default || m if (z) break } } catch { } } if (!z) { log('schemastery not resolvable; skip settings entry'); return } const schema = z.object({ topmost: z.boolean().default(true).description('始终置顶'), posLock: z.boolean().default(false).description('固定位置'), sizeLock: z.boolean().default(false).description('锁定尺寸'), exclude: z.boolean().default(true).description('抓屏时隐藏本窗'), opacity: z.number().min(0.3).max(1).default(0.92).description('不透明度'), scale: z.number().min(0.8).max(1.5).default(1).description('界面缩放'), backdrop: z.union(['opaque', 'translucent', 'acrylic', 'mica', 'blur']).default('opaque').description('背景效果'), }) const scope = settings.register('dsh-float-window', schema) const scope2 = scope ctx.effect(() => { const stop = scope2.watch?.((next) => { try { broadcast({ type: 'settings', value: next?.value ?? next }) } catch { } }) return () => { try { stop?.() } catch { } } }) log('settings namespace registered') status.settings = true writeStatus() } catch (e) { log('settings registration failed:', String(e?.message || e)) } })() log('ready; endpoints:', `${ROUTE}/events`, `${ROUTE}/state`, `${ROUTE}/health`) }