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:
+67
-17
@@ -33,8 +33,9 @@ function dshHome() {
|
|||||||
// 0.1.7 的设置模型是"编辑插件自身的 Config(profile 条目配置)",所以这里导出一个
|
// 0.1.7 的设置模型是"编辑插件自身的 Config(profile 条目配置)",所以这里导出一个
|
||||||
// schemastery schema,DSH 的「设置 → 插件」就能渲染出悬浮窗的配置卡片。
|
// schemastery schema,DSH 的「设置 → 插件」就能渲染出悬浮窗的配置卡片。
|
||||||
// 难点:插件用 link: 安装时自身目录没有 node_modules,裸 import 会失败 ——
|
// 难点:插件用 link: 安装时自身目录没有 node_modules,裸 import 会失败 ——
|
||||||
// 用 createRequire 以"宿主入口 / profile 目录"为基准去解析 schemastery。
|
// 用 createRequire 以"宿主入口 / profile 目录"为基准去解析。
|
||||||
function loadSchemastery() {
|
/** 通用"宿主侧模块"解析:link: 安装的插件自身没有 node_modules,必须借宿主的 */
|
||||||
|
function loadHostModule(spec) {
|
||||||
const tries = [
|
const tries = [
|
||||||
() => process.argv[1] && createRequire(process.argv[1]),
|
() => process.argv[1] && createRequire(process.argv[1]),
|
||||||
() => createRequire(path.join(dshHome(), 'profiles', 'desktop', 'package.json')),
|
() => createRequire(path.join(dshHome(), 'profiles', 'desktop', 'package.json')),
|
||||||
@@ -44,14 +45,20 @@ function loadSchemastery() {
|
|||||||
try {
|
try {
|
||||||
const req = make()
|
const req = make()
|
||||||
if (!req) continue
|
if (!req) continue
|
||||||
const m = req('@deepseek-ai/schemastery')
|
const m = req(spec)
|
||||||
const z = m?.default || m
|
// 返回模块命名空间本身:default 是类(ToolRuntime)时具名导出 defineTool 会被吞掉
|
||||||
if (z?.object) return z
|
if (m && (m.defineTool || m.default || m.object)) return m
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSchemastery() {
|
||||||
|
const m = loadHostModule('@deepseek-ai/schemastery')
|
||||||
|
const z = m?.default ?? m
|
||||||
|
return z?.object ? z : null
|
||||||
|
}
|
||||||
|
|
||||||
const z = loadSchemastery()
|
const z = loadSchemastery()
|
||||||
const floatSchema = z
|
const floatSchema = z
|
||||||
? z.object({
|
? z.object({
|
||||||
@@ -62,6 +69,7 @@ const floatSchema = z
|
|||||||
opacity: z.number().min(0.3).max(1).default(0.92).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('界面缩放'),
|
scale: z.number().min(0.8).max(1.5).default(1).description('界面缩放'),
|
||||||
backdrop: z.union(['opaque', 'translucent', 'acrylic', 'mica', 'blur']).default('opaque').description('背景效果'),
|
backdrop: z.union(['opaque', 'translucent', 'acrylic', 'mica', 'blur']).default('opaque').description('背景效果'),
|
||||||
|
tool: z.boolean().default(true).description('给 Agent 注册 float_window 工具(关掉则模型看不到它)'),
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
@@ -147,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' },
|
{ topmost: true, posLock: false, sizeLock: false, exclude: true, opacity: 0.92, scale: 1, backdrop: 'opaque', tool: true },
|
||||||
(config && typeof config === 'object') ? config : {},
|
(config && typeof config === 'object') ? config : {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,6 +231,7 @@ export function apply(ctx, config) {
|
|||||||
const rows = new Map() // sessionId -> row[]
|
const rows = new Map() // sessionId -> row[]
|
||||||
const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字)
|
const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字)
|
||||||
const titles = new Map() // sessionId -> 会话标题(来自 session/title 事件)
|
const titles = new Map() // sessionId -> 会话标题(来自 session/title 事件)
|
||||||
|
const contextWindows = new Map() // sessionId -> 上下文窗口(来自 request/context 事件)
|
||||||
let lastSessionId = null
|
let lastSessionId = null
|
||||||
|
|
||||||
function counter(id) {
|
function counter(id) {
|
||||||
@@ -243,6 +252,22 @@ export function apply(ctx, config) {
|
|||||||
(samples[type] ||= []).push(sanitize(event))
|
(samples[type] ||= []).push(sanitize(event))
|
||||||
}
|
}
|
||||||
if (type === 'turn/end') c.turns++
|
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') {
|
else if (type === 'session/title') {
|
||||||
const t = d2(event)?.title
|
const t = d2(event)?.title
|
||||||
if (t) titles.set(id, String(t))
|
if (t) titles.set(id, String(t))
|
||||||
@@ -304,7 +329,7 @@ export function apply(ctx, config) {
|
|||||||
const denom = input + read
|
const denom = input + read
|
||||||
if (denom > 0) out.cacheHit = read / denom
|
if (denom > 0) out.cacheHit = read / denom
|
||||||
}
|
}
|
||||||
let capacity = modelContext()
|
let capacity = contextWindows.get(id) || modelContext()
|
||||||
const used = m.surfaceTokens ?? m.totalTokens
|
const used = m.surfaceTokens ?? m.totalTokens
|
||||||
if (capacity && used) out.contextUsed = Math.min(1, used / capacity)
|
if (capacity && used) out.contextUsed = Math.min(1, used / capacity)
|
||||||
}
|
}
|
||||||
@@ -537,13 +562,29 @@ export function apply(ctx, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Agent 工具
|
// ---------------------------------------------------------------- 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()
|
status.shell = shellPath()
|
||||||
writeStatus()
|
writeStatus()
|
||||||
|
|
||||||
try {
|
; (async () => {
|
||||||
const tools = ctx.get('tools')
|
try {
|
||||||
if (tools?.register) {
|
const tools = ctx.get('tools')
|
||||||
tools.register({
|
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',
|
name: 'float_window',
|
||||||
description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。',
|
description: '控制 DSH 悬浮窗(始终置顶的会话镜像小窗):open 打开/聚焦,close 关闭,pin 切换固定位置,fullscreen 切换全屏,hide 临时隐藏(抓屏前用)。',
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -555,13 +596,22 @@ export function apply(ctx, config) {
|
|||||||
return `float-window: ${r.text}`
|
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
|
status.tool = true
|
||||||
log('tool float_window registered')
|
status.toolSchema = schema
|
||||||
} else {
|
log('tool float_window registered; schema: ' + short(schema, 240))
|
||||||
log('tools service not available; agent trigger disabled')
|
} catch (e) { log('tool registration failed:', String(e?.message || e)) }
|
||||||
}
|
writeStatus()
|
||||||
} 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`
|
||||||
|
|||||||
Reference in New Issue
Block a user