fix(host): 修掉会打断每一轮的致命 bug —— 0.1.7 的工具参数必须经 defineTool 转成 JSON Schema

- 事故现象:每轮 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 开关
This commit is contained in:
2026-09-27 16:08:45 +08:00
parent 8b74c2d2b4
commit 60474fceea
+67 -17
View File
@@ -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`