feat: 页面按钮 + 设置分区 + 修掉同步注册事故
- fix(host): 工具/设置注册从 async 续体挪回 apply 同步段 —— cordis 的 ctx 在 apply 返回后失效,
之前真机报 'cannot create effect on inactive context',导致 float_window 没装上
- feat(host): tapIndex 往 index.html 注入『悬浮窗』按钮(自带状态点/看门狗/被 SPA 重建后自动补回),
实测带 auth cookie 拉首页 HTML 已含该按钮;config.button=false 可关
- feat(client): 客户端半 lib/client.js(ModuleLoader 格式,无需构建)注册 sidebar.footer.action
与 settings.section(设置→悬浮窗整页:打开/全屏/固定/锁定/隐藏/关闭 + 会话状态);接管后摘掉注入的备用按钮
- package.json: exports['./client'] + dsh.client{inject,platform}
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* dsh-float-window — Client 半(浏览器侧)
|
||||||
|
*
|
||||||
|
* 用 0.1.7 的客户端插件格式(window.__ModuleLoader__.load)手写,不需要构建步骤。
|
||||||
|
* 只做两件事:
|
||||||
|
* 1. 侧边栏底部(设置上方)一个「悬浮窗」按钮 —— 点一下把原生浮窗拉起来;
|
||||||
|
* 2. 设置里新增一个「悬浮窗」分区 —— 打开/关闭/隐藏 + 状态一览。
|
||||||
|
*
|
||||||
|
* 与宿主半通信:同源相对路径 POST /dsh-float-window/window(宿主插件注册的 loopback 路由),
|
||||||
|
* 状态读 GET /dsh-float-window/state。宿主路由带 CORS 头,file:// 场景也能用。
|
||||||
|
*/
|
||||||
|
window.__ModuleLoader__.load({
|
||||||
|
id: 'dsh-float-window',
|
||||||
|
factory: (require) => {
|
||||||
|
const React = require('react')
|
||||||
|
const h = React.createElement
|
||||||
|
const ROUTE = '/dsh-float-window'
|
||||||
|
|
||||||
|
const callHost = async (action) => {
|
||||||
|
const res = await fetch(`${ROUTE}/window`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action }),
|
||||||
|
})
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
const readState = async () => {
|
||||||
|
const res = await fetch(`${ROUTE}/state`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
border: '1px solid var(--dsw-alias-border-l2, #2a3040)',
|
||||||
|
background: 'var(--dsw-alias-bg-layer-3, rgba(255,255,255,.03))',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: '14px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10,
|
||||||
|
}
|
||||||
|
const btn = (primary) => ({
|
||||||
|
padding: '7px 14px',
|
||||||
|
borderRadius: 9,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 13,
|
||||||
|
border: primary ? '1px solid #4d7cfe' : '1px solid var(--dsw-alias-border-l2, #2a3040)',
|
||||||
|
background: primary ? '#4d7cfe' : 'transparent',
|
||||||
|
color: primary ? '#fff' : 'inherit',
|
||||||
|
})
|
||||||
|
const muted = { opacity: 0.7, fontSize: 12, lineHeight: 1.7 }
|
||||||
|
|
||||||
|
/** 宿主可达性 + 浮窗存活状态 */
|
||||||
|
function useStatus() {
|
||||||
|
const [status, setStatus] = React.useState({ ok: false, alive: false, text: '', session: null })
|
||||||
|
const [busy, setBusy] = React.useState(false)
|
||||||
|
const refresh = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const r = await callHost('status')
|
||||||
|
const s = await readState().catch(() => null)
|
||||||
|
setStatus({ ok: true, alive: !!r.alive, text: r.text || '', session: s })
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({ ok: false, alive: false, text: String(e && e.message || e), session: null })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
React.useEffect(() => {
|
||||||
|
refresh()
|
||||||
|
const t = setInterval(refresh, 3000)
|
||||||
|
return () => clearInterval(t)
|
||||||
|
}, [refresh])
|
||||||
|
const act = React.useCallback(async (action) => {
|
||||||
|
setBusy(true)
|
||||||
|
try { await callHost(action) } catch (e) { /* 宿主离线:状态轮询会显示 */ }
|
||||||
|
setTimeout(() => { setBusy(false); refresh() }, 800)
|
||||||
|
}, [refresh])
|
||||||
|
return { status, busy, act, refresh }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 侧边栏底部按钮(设置按钮上方那一排) */
|
||||||
|
function SidebarButton(props) {
|
||||||
|
const { status, act } = useStatus()
|
||||||
|
const wide = props && props.wide
|
||||||
|
const label = status.alive ? '悬浮窗已开' : '悬浮窗'
|
||||||
|
return h('button', {
|
||||||
|
type: 'button',
|
||||||
|
title: 'DSH 悬浮窗:始终置顶的会话镜像小窗',
|
||||||
|
onClick: () => act(status.alive ? 'open' : 'open'),
|
||||||
|
style: {
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8, width: wide ? '100%' : undefined,
|
||||||
|
padding: '6px 10px', borderRadius: 9, cursor: 'pointer', fontSize: 13,
|
||||||
|
border: '1px solid transparent', background: 'transparent', color: 'inherit', opacity: status.ok ? 1 : 0.5,
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
h('span', { key: 'i', style: { fontSize: 15, lineHeight: 1 }, }, '🪟'),
|
||||||
|
wide ? h('span', { key: 't' }, label) : null,
|
||||||
|
status.alive ? h('span', { key: 'd', style: { width: 7, height: 7, borderRadius: 4, background: '#3ecf8e', display: 'inline-block' } }) : null,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置 →「悬浮窗」整页 */
|
||||||
|
function FloatSettingsSection() {
|
||||||
|
const { status, busy, act } = useStatus()
|
||||||
|
const s = status.session || {}
|
||||||
|
const m = s.metrics || {}
|
||||||
|
return h('div', { style: { padding: '4px 2px', display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 640 } }, [
|
||||||
|
h('div', { key: 'h', style: { fontSize: 15, fontWeight: 600 } }, '悬浮窗(会话镜像小窗)'),
|
||||||
|
h('div', { key: 'd', style: muted }, '始终置顶、可固定位置/尺寸、抓屏时自动隐身的小窗;把主界面切到后台也能盯着 Agent 干活。'),
|
||||||
|
h('div', { key: 'c', style: card }, [
|
||||||
|
h('div', { key: 'line', style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, [
|
||||||
|
h('span', { key: 's', style: { width: 8, height: 8, borderRadius: 4, background: status.alive ? '#3ecf8e' : '#f0b429', display: 'inline-block' } }),
|
||||||
|
h('b', { key: 'b' }, status.alive ? '浮窗正在运行' : '浮窗未运行'),
|
||||||
|
h('span', { key: 'x', style: muted }, status.ok ? (status.text || '') : '宿主插件不可达(可能未启用或未重启)'),
|
||||||
|
]),
|
||||||
|
h('div', { key: 'btns', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, [
|
||||||
|
h('button', { key: 'open', style: btn(true), disabled: busy, onClick: () => act('open') }, '打开/前置悬浮窗'),
|
||||||
|
h('button', { key: 'full', style: btn(false), disabled: busy, onClick: () => act('fullscreen') }, '全屏切换'),
|
||||||
|
h('button', { key: 'pin', style: btn(false), disabled: busy, onClick: () => act('pin') }, '固定位置'),
|
||||||
|
h('button', { key: 'lock', style: btn(false), disabled: busy, onClick: () => act('sizeLock') }, '锁定尺寸'),
|
||||||
|
h('button', { key: 'hide', style: btn(false), disabled: busy, onClick: () => act('hide') }, '临时隐藏'),
|
||||||
|
h('button', { key: 'close', style: btn(false), disabled: busy, onClick: () => act('close') }, '关闭'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
h('div', { key: 'st', style: card }, [
|
||||||
|
h('div', { key: 't', style: { fontWeight: 600, fontSize: 13 } }, '当前会话'),
|
||||||
|
h('div', { key: 'n', style: muted }, s.title ? String(s.title) : (s.sessionId || '还没有会话')),
|
||||||
|
h('div', { key: 'm', style: muted }, `轮 ${m.turns ?? '—'} · 步 ${m.steps ?? '—'} · ${m.tokens == null ? '—' : (m.tokens / 1e6).toFixed(3)} M tok · 缓存 ${m.cacheHit == null ? '—' : Math.round(m.cacheHit * 100) + '%'} · 上下文 ${m.contextUsed == null ? '—' : Math.round(m.contextUsed * 100) + '%'}`),
|
||||||
|
]),
|
||||||
|
h('div', { key: 'tip', style: muted }, '外观(不透明度 / 毛玻璃 / 缩放)在浮窗内点 ⚙ 调整;这里的状态每 3 秒自动刷新。'),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const inject = ['slots']
|
||||||
|
|
||||||
|
/** 客户端半接管 UI 后,把宿主注入的备用按钮摘掉,避免出现两个入口 */
|
||||||
|
function claimUi() {
|
||||||
|
try {
|
||||||
|
window.__DSH_FLOAT_UI__ = true
|
||||||
|
const el = document.getElementById('dsh-float-pill')
|
||||||
|
if (el) el.remove()
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(ctx) {
|
||||||
|
let registered = 0
|
||||||
|
try {
|
||||||
|
ctx.slots.inject('sidebar.footer.action', () => {
|
||||||
|
try {
|
||||||
|
const stop = ctx.slots.register({ name: 'sidebar.footer.action', id: 'dsh-float-window', order: 40 }, SidebarButton)
|
||||||
|
registered++; claimUi()
|
||||||
|
return stop
|
||||||
|
} catch (e) { console.warn('[float-window] sidebar slot failed', e); return () => { } }
|
||||||
|
})
|
||||||
|
} catch (e) { console.warn('[float-window] sidebar inject failed', e) }
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.slots.inject('settings.section', () => {
|
||||||
|
try {
|
||||||
|
const stop = ctx.slots.register({ name: 'settings.section', id: 'float-window', order: 60, label: () => '悬浮窗' }, FloatSettingsSection)
|
||||||
|
registered++; claimUi()
|
||||||
|
return stop
|
||||||
|
} catch (e) { console.warn('[float-window] settings slot failed', e); return () => { } }
|
||||||
|
})
|
||||||
|
} catch (e) { console.warn('[float-window] settings inject failed', e) }
|
||||||
|
|
||||||
|
console.info('[float-window] client half applied; slots registered =', registered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: 'dsh-float-window', inject, apply }
|
||||||
|
},
|
||||||
|
})
|
||||||
+84
-80
@@ -155,7 +155,7 @@ const SAMPLE_TYPES = new Set(['user/message', 'assistant/chunk', 'assistant/mess
|
|||||||
export function apply(ctx, config) {
|
export function apply(ctx, config) {
|
||||||
// 浮窗设置:DSH 设置页写进 profile 条目 config 的值 + 默认值
|
// 浮窗设置:DSH 设置页写进 profile 条目 config 的值 + 默认值
|
||||||
const floatCfg = Object.assign(
|
const floatCfg = Object.assign(
|
||||||
{ topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true },
|
{ topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true, button: true },
|
||||||
(config && typeof config === 'object') ? config : {},
|
(config && typeof config === 'object') ? config : {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -562,28 +562,27 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Agent 工具
|
// ---------------------------------------------------------------- Agent 工具
|
||||||
// 踩坑(真机事故):0.1.7 的 tools 服务要求模型侧看到的是 **JSON Schema**,
|
// 踩坑(真机事故 ×2):
|
||||||
// 参数必须经 `defineTool()`(内部 parameterSchemaSpecToJsonSchema)转换后再注册。
|
// a) 0.1.7 的 tools 服务要求模型侧看到的是 **JSON Schema**,参数必须经 `defineTool()`
|
||||||
// 直接塞 DSL 对象会让 provider 收到 `parameters.type = null`,于是**每一轮请求都被拒**:
|
// (内部 parameterSchemaSpecToJsonSchema)转换。直接塞 DSL 对象会让 provider 收到
|
||||||
// Invalid schema for function 'float_window': schema must be a JSON Schema of 'type: "object"'
|
// `parameters.type = null`,于是**每一轮请求都被拒**:
|
||||||
// 所以这里:解析不到 defineTool 就**宁可不注册**;注册前再自检一次 type === 'object'。
|
// Invalid schema for function 'float_window': schema must be a JSON Schema of 'type: "object"'
|
||||||
|
// 所以:解析不到 defineTool 就**宁可不注册**;注册前自检 type === 'object'。
|
||||||
|
// b) 注册必须发生在 apply() 的**同步**阶段。放进 async 续体会拿到
|
||||||
|
// "cannot create effect on inactive context"(cordis 的 ctx 在 apply 返回后失效)。
|
||||||
status.shell = shellPath()
|
status.shell = shellPath()
|
||||||
writeStatus()
|
writeStatus()
|
||||||
|
|
||||||
; (async () => {
|
try {
|
||||||
try {
|
const tools = ctx.get('tools')
|
||||||
const tools = ctx.get('tools')
|
const defineTool = loadHostModule('@deepseek-ai/dsh-tools')?.defineTool
|
||||||
if (!tools?.register) { log('tools service not available; agent trigger disabled'); return }
|
if (floatCfg.tool === false) {
|
||||||
if (floatCfg.tool === false) { log('tool disabled by config (tool: false)'); return }
|
log('tool disabled by config (tool: false)')
|
||||||
|
} else if (!tools?.register) {
|
||||||
let defineTool = null
|
log('tools service not available; agent trigger disabled')
|
||||||
try { defineTool = (await import('@deepseek-ai/dsh-tools')).defineTool } catch { }
|
} else if (typeof defineTool !== 'function') {
|
||||||
if (!defineTool) defineTool = loadHostModule('@deepseek-ai/dsh-tools')?.defineTool
|
log('defineTool unresolvable; tool NOT registered (avoid emitting an invalid schema)')
|
||||||
if (typeof defineTool !== 'function') {
|
} else {
|
||||||
log('defineTool unresolvable; tool NOT registered (avoid emitting an invalid schema)')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tool = defineTool({
|
const tool = defineTool({
|
||||||
name: 'float_window',
|
name: 'float_window',
|
||||||
description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。',
|
description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。',
|
||||||
@@ -596,22 +595,19 @@ export function apply(ctx, config) {
|
|||||||
return `float-window: ${r.text}`
|
return `float-window: ${r.text}`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 自检:provider 只接受 object 根 schema,这里挡住任何回归
|
|
||||||
const schema = tool?.parameters
|
const schema = tool?.parameters
|
||||||
if (!schema || schema.type !== 'object' || !schema.properties) {
|
if (!schema || schema.type !== 'object' || !schema.properties) {
|
||||||
log('tool schema self-check FAILED; not registering:', short(schema, 200))
|
|
||||||
status.toolSchema = schema ?? null
|
status.toolSchema = schema ?? null
|
||||||
writeStatus()
|
log('tool schema self-check FAILED; not registering: ' + short(schema, 200))
|
||||||
return
|
} else {
|
||||||
|
tools.register(tool)
|
||||||
|
status.tool = true
|
||||||
|
status.toolSchema = schema
|
||||||
|
log('tool float_window registered; schema: ' + short(schema, 240))
|
||||||
}
|
}
|
||||||
tools.register(tool)
|
}
|
||||||
status.tool = true
|
} catch (e) { log('tool registration failed:', String(e?.message || e)) }
|
||||||
status.toolSchema = schema
|
writeStatus()
|
||||||
log('tool float_window registered; schema: ' + short(schema, 240))
|
|
||||||
} catch (e) { log('tool registration failed:', String(e?.message || e)) }
|
|
||||||
writeStatus()
|
|
||||||
})()
|
|
||||||
|
|
||||||
const broadcast = (msg) => {
|
const broadcast = (msg) => {
|
||||||
const payload = `data: ${JSON.stringify(msg)}\n\n`
|
const payload = `data: ${JSON.stringify(msg)}\n\n`
|
||||||
@@ -619,6 +615,46 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
const pushHidden = () => broadcast({ type: 'cmd', cmd: 'hide' })
|
const pushHidden = () => broadcast({ type: 'cmd', cmd: 'hide' })
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 页面按钮(index.html 注入)
|
||||||
|
// 宿主侧往 index.html 里塞一个悬浮按钮:不依赖客户端插件 roster,web/桌面壳都生效。
|
||||||
|
// 客户端半(lib/client.js)如果注册成功,会把按钮移除,避免出现两个入口。
|
||||||
|
if (floatCfg.button !== false) {
|
||||||
|
try {
|
||||||
|
const pill = `<style>
|
||||||
|
#dsh-float-pill{position:fixed;left:14px;bottom:58px;z-index:2147000000;display:flex;align-items:center;gap:7px;
|
||||||
|
padding:7px 12px;border-radius:999px;font:13px/1.2 "Segoe UI","Microsoft YaHei UI",system-ui,sans-serif;cursor:pointer;
|
||||||
|
border:1px solid rgba(120,140,180,.35);background:rgba(22,26,34,.92);color:#e6eaf2;box-shadow:0 6px 20px rgba(0,0,0,.45);backdrop-filter:blur(6px)}
|
||||||
|
#dsh-float-pill:hover{border-color:#4d7cfe}
|
||||||
|
#dsh-float-pill i{width:7px;height:7px;border-radius:50%;background:#f0b429;display:inline-block}
|
||||||
|
#dsh-float-pill.on i{background:#3ecf8e;box-shadow:0 0 6px #3ecf8e}
|
||||||
|
#dsh-float-pill.hide{display:none}
|
||||||
|
</style>
|
||||||
|
<button id="dsh-float-pill" title="DSH 悬浮窗:始终置顶的会话镜像小窗(抓屏时自动隐身)"><i></i>悬浮窗</button>
|
||||||
|
<script>(function(){
|
||||||
|
var R='/dsh-float-window';
|
||||||
|
function call(a){ return fetch(R+'/window',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:a})}).then(function(r){return r.json()}).catch(function(){return null}) }
|
||||||
|
function mount(){
|
||||||
|
if(window.__DSH_FLOAT_UI__) return; // 客户端半接管了就不重复
|
||||||
|
var b=document.getElementById('dsh-float-pill');
|
||||||
|
if(!b){ // SPA 重建 body 时自动补回
|
||||||
|
if(!document.body) return;
|
||||||
|
var wrap=document.createElement('div'); wrap.innerHTML=${JSON.stringify('<button id="dsh-float-pill" title="DSH 悬浮窗:始终置顶的会话镜像小窗(抓屏时自动隐身)"><i></i>悬浮窗</button>')};
|
||||||
|
b=wrap.firstChild; document.body.appendChild(b);
|
||||||
|
}
|
||||||
|
if(b.__bound) return; b.__bound=true;
|
||||||
|
b.addEventListener('click', function(){ b.classList.add('on'); call('open'); });
|
||||||
|
function ping(){ call('status').then(function(j){ if(!j){b.classList.add('hide');return} b.classList.remove('hide'); if(j.alive) b.classList.add('on'); }) }
|
||||||
|
ping(); setInterval(ping, 5000);
|
||||||
|
}
|
||||||
|
mount(); setInterval(mount, 3000);
|
||||||
|
})();</script>`
|
||||||
|
ctx.effect(() => ctx.webServer.tapIndex((html) => (typeof html === 'string' && html.includes('</body>')
|
||||||
|
? html.replace('</body>', pill + '</body>')
|
||||||
|
: html)))
|
||||||
|
log('index pill injected')
|
||||||
|
} catch (e) { log('index pill injection skipped:', String(e?.message || e)) }
|
||||||
|
}
|
||||||
|
|
||||||
// 抓屏前隐藏的对外门面:交给宿主的共享对象。
|
// 抓屏前隐藏的对外门面:交给宿主的共享对象。
|
||||||
// 注意 cordis 要求先 provide 再 set,否则会抛 "cannot set property ... without provide"
|
// 注意 cordis 要求先 provide 再 set,否则会抛 "cannot set property ... without provide"
|
||||||
// (实测:漏了 provide 会让整个插件树加载失败、DSH 起不来)。这里做成可选的,
|
// (实测:漏了 provide 会让整个插件树加载失败、DSH 起不来)。这里做成可选的,
|
||||||
@@ -628,62 +664,30 @@ export function apply(ctx, config) {
|
|||||||
ctx.set?.('floatWindow', { hide: pushHidden, show: () => broadcast({ type: 'cmd', cmd: 'show' }), snapshot, endpoint: () => endpointFile })
|
ctx.set?.('floatWindow', { hide: pushHidden, show: () => broadcast({ type: 'cmd', cmd: 'show' }), snapshot, endpoint: () => endpointFile })
|
||||||
} catch (e) { log('floatWindow facade skipped:', String(e?.message || e)) }
|
} catch (e) { log('floatWindow facade skipped:', String(e?.message || e)) }
|
||||||
|
|
||||||
// ---------------------------------------------------------------- DSH 设置入口
|
// ---------------------------------------------------------------- DSH 设置入口(0.1.0 的 settings 服务路径)
|
||||||
// 注册一个设置命名空间,DSH 的「设置 → 插件」会把它渲染成一张卡片。
|
// 0.1.7 走的是"插件 Config schema"(见文件顶部导出的 Config),这条是旧版兼容路径。
|
||||||
// schemastery 是从 profile 的 node_modules 解析的;link: 安装的插件解析不到时
|
// 同样必须**同步**注册:async 续体里的 ctx 已经失效(inactive context)。
|
||||||
// 只跳过这一步(浮窗自己的设置面板照常可用)。
|
|
||||||
log('services: ' + ['settings', 'sessions', 'tools', 'tokenMeter', 'sessionProjection', 'llm', 'shell', 'sandboxPolicy', 'webServer']
|
log('services: ' + ['settings', 'sessions', 'tools', 'tokenMeter', 'sessionProjection', 'llm', 'shell', 'sandboxPolicy', 'webServer']
|
||||||
.map((n) => `${n}=${(() => { try { return ctx.get(n) ? 'y' : 'n' } catch { return '?' } })()}`).join(' '))
|
.map((n) => `${n}=${(() => { try { return ctx.get(n) ? 'y' : 'n' } catch { return '?' } })()}`).join(' '))
|
||||||
; (async () => {
|
try {
|
||||||
try {
|
const settings = ctx.get('settings')
|
||||||
const settings = ctx.get('settings')
|
if (!settings?.register) {
|
||||||
if (!settings?.register) { log('settings service absent; skip settings entry'); return }
|
log('settings service absent; skip settings entry')
|
||||||
let z = null
|
} else if (!floatSchema) {
|
||||||
// link: 安装时插件自己的目录里没有 node_modules,裸导入会失败;
|
log('schemastery not resolvable; skip settings entry')
|
||||||
// 依次尝试:常规解析 -> 以宿主入口为基准解析 -> profile 目录解析
|
} else {
|
||||||
try { z = (await import('@deepseek-ai/schemastery')).default } catch { }
|
const scope = settings.register('dsh-float-window', floatSchema)
|
||||||
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(() => {
|
ctx.effect(() => {
|
||||||
const stop = scope2.watch?.((next) => {
|
const stop = scope?.watch?.((next) => {
|
||||||
try { broadcast({ type: 'settings', value: next?.value ?? next }) } catch { }
|
try { broadcast({ type: 'settings', value: next?.value ?? next }) } catch { }
|
||||||
})
|
})
|
||||||
return () => { try { stop?.() } catch { } }
|
return () => { try { stop?.() } catch { } }
|
||||||
})
|
})
|
||||||
log('settings namespace registered')
|
|
||||||
status.settings = true
|
status.settings = true
|
||||||
writeStatus()
|
log('settings namespace registered')
|
||||||
} catch (e) { log('settings registration failed:', String(e?.message || e)) }
|
}
|
||||||
})()
|
} catch (e) { log('settings registration failed:', String(e?.message || e)) }
|
||||||
|
writeStatus()
|
||||||
|
|
||||||
log('ready; endpoints:', `${ROUTE}/events`, `${ROUTE}/state`, `${ROUTE}/health`)
|
log('ready; endpoints:', `${ROUTE}/events`, `${ROUTE}/state`, `${ROUTE}/health`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
],
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "lib/host.js",
|
"main": "lib/host.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./lib/host.js"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"default": "./lib/client.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib",
|
"lib",
|
||||||
"shell",
|
"shell",
|
||||||
@@ -19,8 +28,17 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"dsh": {
|
"dsh": {
|
||||||
|
"engines": {
|
||||||
|
"dsh": ">=0.1.7-rc.1"
|
||||||
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"patch": "./cordis.patch.yml"
|
"patch": "./cordis.patch.yml"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"inject": [
|
||||||
|
"@deepseek-ai/dsh-client-ui-settings"
|
||||||
|
],
|
||||||
|
"platform": "web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
Reference in New Issue
Block a user