From 60474fceea6664899e469eedac66cd1757969b02 Mon Sep 17 00:00:00 2001 From: jiangchen <1703935164@qq.com> Date: Sun, 27 Sep 2026 16:08:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(host):=20=E4=BF=AE=E6=8E=89=E4=BC=9A?= =?UTF-8?q?=E6=89=93=E6=96=AD=E6=AF=8F=E4=B8=80=E8=BD=AE=E7=9A=84=E8=87=B4?= =?UTF-8?q?=E5=91=BD=20bug=20=E2=80=94=E2=80=94=200.1.7=20=E7=9A=84?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8F=82=E6=95=B0=E5=BF=85=E9=A1=BB=E7=BB=8F?= =?UTF-8?q?=20defineTool=20=E8=BD=AC=E6=88=90=20JSON=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 事故现象:每轮 Invalid schema for function 'float_window': schema must be a JSON Schema of 'type: object', got 'type: null' - 根因:直接注册 DSL 对象,参数未做 parameterSchemaSpecToJsonSchema 转换,provider 收到 parameters.type=null - 修复:解析 @deepseek-ai/dsh-tools 的 defineTool 后再注册;解析不到就跳过注册;注册前自检 type==='object' - 自检证据:状态文件新增 toolSchema 与 toolSchemas(从真实 request/header 抓的模型视角 schema) 实测 0.1.7:float_window parametersType=object, required=[action] - 顺带:request/context 的 contextWindow 用于上下文用量;Config 增加 tool 开关 --- lib/host.js | 84 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/lib/host.js b/lib/host.js index 4cb6544..f69b9a3 100644 --- a/lib/host.js +++ b/lib/host.js @@ -33,8 +33,9 @@ function dshHome() { // 0.1.7 的设置模型是"编辑插件自身的 Config(profile 条目配置)",所以这里导出一个 // schemastery schema,DSH 的「设置 → 插件」就能渲染出悬浮窗的配置卡片。 // 难点:插件用 link: 安装时自身目录没有 node_modules,裸 import 会失败 —— -// 用 createRequire 以"宿主入口 / profile 目录"为基准去解析 schemastery。 -function loadSchemastery() { +// 用 createRequire 以"宿主入口 / profile 目录"为基准去解析。 +/** 通用"宿主侧模块"解析:link: 安装的插件自身没有 node_modules,必须借宿主的 */ +function loadHostModule(spec) { const tries = [ () => process.argv[1] && createRequire(process.argv[1]), () => createRequire(path.join(dshHome(), 'profiles', 'desktop', 'package.json')), @@ -44,14 +45,20 @@ function loadSchemastery() { try { const req = make() if (!req) continue - const m = req('@deepseek-ai/schemastery') - const z = m?.default || m - if (z?.object) return z + const m = req(spec) + // 返回模块命名空间本身:default 是类(ToolRuntime)时具名导出 defineTool 会被吞掉 + if (m && (m.defineTool || m.default || m.object)) return m } catch { } } return null } +function loadSchemastery() { + const m = loadHostModule('@deepseek-ai/schemastery') + const z = m?.default ?? m + return z?.object ? z : null +} + const z = loadSchemastery() const floatSchema = z ? z.object({ @@ -62,6 +69,7 @@ const floatSchema = z 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('背景效果'), + tool: z.boolean().default(true).description('给 Agent 注册 float_window 工具(关掉则模型看不到它)'), }) : undefined @@ -147,7 +155,7 @@ const SAMPLE_TYPES = new Set(['user/message', 'assistant/chunk', 'assistant/mess export function apply(ctx, config) { // 浮窗设置:DSH 设置页写进 profile 条目 config 的值 + 默认值 const floatCfg = Object.assign( - { topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque' }, + { topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true }, (config && typeof config === 'object') ? config : {}, ) @@ -223,6 +231,7 @@ export function apply(ctx, config) { const rows = new Map() // sessionId -> row[] const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字) const titles = new Map() // sessionId -> 会话标题(来自 session/title 事件) + const contextWindows = new Map() // sessionId -> 上下文窗口(来自 request/context 事件) let lastSessionId = null function counter(id) { @@ -243,6 +252,22 @@ export function apply(ctx, config) { (samples[type] ||= []).push(sanitize(event)) } if (type === 'turn/end') c.turns++ + else if (type === 'request/header') { + // 记录模型实际会收到的工具 schema 摘要:这是"schema 是否合法"的最终证据 + const h = (event?.data || event)?.header + if (Array.isArray(h?.tools)) { + status.toolSchemas = h.tools.slice(0, 12).map((t) => ({ + name: t?.name, + parametersType: t?.parameters?.type ?? null, + required: Array.isArray(t?.parameters?.required) ? t.parameters.required : null, + })) + } + } + else if (type === 'request/context') { + // 真机上 provider 会在这条事件里给出模型上下文窗口(实测 deepseek-flash = 1000000) + const cw = (event?.data || event)?.contextWindow + if (typeof cw === 'number' && cw > 0) contextWindows.set(id, cw) + } else if (type === 'session/title') { const t = d2(event)?.title if (t) titles.set(id, String(t)) @@ -304,7 +329,7 @@ export function apply(ctx, config) { const denom = input + read if (denom > 0) out.cacheHit = read / denom } - let capacity = modelContext() + let capacity = contextWindows.get(id) || modelContext() const used = m.surfaceTokens ?? m.totalTokens if (capacity && used) out.contextUsed = Math.min(1, used / capacity) } @@ -537,13 +562,29 @@ export function apply(ctx, config) { } // ---------------------------------------------------------------- Agent 工具 + // 踩坑(真机事故):0.1.7 的 tools 服务要求模型侧看到的是 **JSON Schema**, + // 参数必须经 `defineTool()`(内部 parameterSchemaSpecToJsonSchema)转换后再注册。 + // 直接塞 DSL 对象会让 provider 收到 `parameters.type = null`,于是**每一轮请求都被拒**: + // Invalid schema for function 'float_window': schema must be a JSON Schema of 'type: "object"' + // 所以这里:解析不到 defineTool 就**宁可不注册**;注册前再自检一次 type === 'object'。 status.shell = shellPath() writeStatus() - try { - const tools = ctx.get('tools') - if (tools?.register) { - tools.register({ + ; (async () => { + try { + const tools = ctx.get('tools') + if (!tools?.register) { log('tools service not available; agent trigger disabled'); return } + if (floatCfg.tool === false) { log('tool disabled by config (tool: false)'); return } + + let defineTool = null + try { defineTool = (await import('@deepseek-ai/dsh-tools')).defineTool } catch { } + if (!defineTool) defineTool = loadHostModule('@deepseek-ai/dsh-tools')?.defineTool + if (typeof defineTool !== 'function') { + log('defineTool unresolvable; tool NOT registered (avoid emitting an invalid schema)') + return + } + + const tool = defineTool({ name: 'float_window', description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。', parameters: { @@ -555,13 +596,22 @@ export function apply(ctx, config) { return `float-window: ${r.text}` }, }) + + // 自检:provider 只接受 object 根 schema,这里挡住任何回归 + const schema = tool?.parameters + if (!schema || schema.type !== 'object' || !schema.properties) { + log('tool schema self-check FAILED; not registering:', short(schema, 200)) + status.toolSchema = schema ?? null + writeStatus() + return + } + tools.register(tool) 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() + status.toolSchema = schema + log('tool float_window registered; schema: ' + short(schema, 240)) + } catch (e) { log('tool registration failed:', String(e?.message || e)) } + writeStatus() + })() const broadcast = (msg) => { const payload = `data: ${JSON.stringify(msg)}\n\n`