feat(host): 版本自适应宿主调用(0.1.7 sessionController / 0.1.0 apiProxy),在 DSH Desktop 内置 0.1.7-rc.2 上实测镜像与发消息全通
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
| 自适应调整 | 布局随窗口尺寸重排;拖动**任意边缘和四角**都能缩放(系统级 WM_NCHITTEST) |
|
| 自适应调整 | 布局随窗口尺寸重排;拖动**任意边缘和四角**都能缩放(系统级 WM_NCHITTEST) |
|
||||||
| 全屏切换 | 标题栏 ⛶ 或 `F11`,返回时还原原来的位置与尺寸 |
|
| 全屏切换 | 标题栏 ⛶ 或 `F11`,返回时还原原来的位置与尺寸 |
|
||||||
| 抓屏隐身 | `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)`,Agent 截屏里彻底看不到这个窗 |
|
| 抓屏隐身 | `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)`,Agent 截屏里彻底看不到这个窗 |
|
||||||
| 底部聊天框 | 直接发消息进当前会话(浮窗 POST 给插件,插件调宿主网关 `ctx.apiProxy.sessions.prompt`,与浏览器 UI 同一条路) |
|
| 底部聊天框 | 直接发消息进当前会话(浮窗 POST 给插件,插件调宿主网关:0.1.7 走 `ctx.sessionController.prompt`,0.1.0 走 `ctx.apiProxy.sessions.prompt`,与浏览器 UI 同一条路) |
|
||||||
| 计费/状态条 | 轮数、步数、tok/s、累计 MTOK、缓存命中率、上下文用量进度条 |
|
| 计费/状态条 | 轮数、步数、tok/s、累计 MTOK、缓存命中率、上下文用量进度条 |
|
||||||
| 外观自定义 | 背景效果:不透明 / 毛玻璃(亚克力)/ 云母 / 模糊 / 半透明;不透明度、界面缩放 |
|
| 外观自定义 | 背景效果:不透明 / 毛玻璃(亚克力)/ 云母 / 模糊 / 半透明;不透明度、界面缩放 |
|
||||||
| 按钮触发 | 运行 `shell\run-float.cmd`,或 DSH 里点插件入口 |
|
| 按钮触发 | 运行 `shell\run-float.cmd`,或 DSH 里点插件入口 |
|
||||||
@@ -111,7 +111,17 @@ shell\run-float.cmd -Action pin # 给正在运行的实例
|
|||||||
10. **profile 的 `package.json` 不能带 BOM**:Node 读 JSON 会报 `SyntaxError: Unexpected token '锘?`。用 `[IO.File]::WriteAllText(..., UTF8Encoding($false))` 写。
|
10. **profile 的 `package.json` 不能带 BOM**:Node 读 JSON 会报 `SyntaxError: Unexpected token '锘?`。用 `[IO.File]::WriteAllText(..., UTF8Encoding($false))` 写。
|
||||||
11. **宿主里可能返回 Promise 的 API,必须 `.catch()`**:`ctx.get('llm').resolveModelInfo()` 在没注册 provider 时会抛错,而且它返回 Promise——只 `try/catch` 同步部分会留下 **unhandled rejection,直接把 DSH 进程 `fatal load failure` 干掉**(我在隔离实例里真把 DSH 搞崩过一次,1 秒一次的 SSE tick 是触发器)。
|
11. **宿主里可能返回 Promise 的 API,必须 `.catch()`**:`ctx.get('llm').resolveModelInfo()` 在没注册 provider 时会抛错,而且它返回 Promise——只 `try/catch` 同步部分会留下 **unhandled rejection,直接把 DSH 进程 `fatal load failure` 干掉**(我在隔离实例里真把 DSH 搞崩过一次,1 秒一次的 SSE tick 是触发器)。
|
||||||
12. **会话事件载荷在 `event.data` 下**,不在顶层:`user/message` → `data.content[{type:'text',text}]`、`assistant/chunk` → `data.chunk{type,text,reason}`、`session/title` → `data.title`、`tool/call` → `data.name/args`。另外要过滤 `data.source.kind === 'plugin'` 的消息(系统提示快照),否则镜像里会混入一大段内部注入文本。
|
12. **会话事件载荷在 `event.data` 下**,不在顶层:`user/message` → `data.content[{type:'text',text}]`、`assistant/chunk` → `data.chunk{type,text,reason}`、`session/title` → `data.title`、`tool/call` → `data.name/args`。另外要过滤 `data.source.kind === 'plugin'` 的消息(系统提示快照),否则镜像里会混入一大段内部注入文本。
|
||||||
13. **宿主 RPC 的信封是 `{ rpcId, payload }`**:`ctx.apiProxy.sessions.prompt({ rpcId, payload: { sessionId, mode, content } })`。把字段平铺在顶层会得到 `Cannot read properties of undefined (reading 'sessionId')`。
|
13. **宿主 RPC 的信封是 `{ rpcId, payload }`**(0.1.0 的 `ctx.apiProxy.sessions.*`):把字段平铺在顶层会得到 `Cannot read properties of undefined (reading 'sessionId')`。
|
||||||
|
14. **0.1.7 起宿主面换成了 `ctx.sessionController`**(`ctx.apiProxy` 不再存在),并且 Remote 签名是 `(request, signal)`——**漏了 signal 会报 `Cannot read properties of undefined (reading 'throwIfAborted')`**;`create` 还必须带一个合法的 `agentPreset`(否则 `Unknown agent preset: xxx`)。插件对两套 API 都自动适配(见 `hostCall`)。
|
||||||
|
|
||||||
|
## 版本支持
|
||||||
|
|
||||||
|
本插件在两种真实宿主上都实测跑通过(同一份代码,运行时自适应):
|
||||||
|
|
||||||
|
| 宿主 | 来源 | 会话 API | 结果 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| dsh **0.1.0-rc.6** | npx 缓存(`dsh web`) | `ctx.apiProxy.sessions.*`(`{rpcId,payload}` 信封) | 插件加载、工具注册、路由、镜像、发消息全部通过 |
|
||||||
|
| dsh **0.1.7-rc.2** | `DSH Desktop.exe` 内置 | `ctx.sessionController.*`(`(request, signal)`) | 同上全部通过(`settings` 服务在 0.1.7 存在,设置入口走 profile 配置模型) |
|
||||||
|
|
||||||
## 怎么验证(可复现)
|
## 怎么验证(可复现)
|
||||||
|
|
||||||
|
|||||||
+42
-29
@@ -148,7 +148,30 @@ export function apply(ctx) {
|
|||||||
writeEndpoint()
|
writeEndpoint()
|
||||||
ctx.effect(() => () => { try { fs.rmSync(endpointFile, { force: true }) } catch { } })
|
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 MAX_ROWS = 120
|
||||||
const rows = new Map() // sessionId -> row[]
|
const rows = new Map() // sessionId -> row[]
|
||||||
const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字)
|
const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字)
|
||||||
@@ -338,22 +361,16 @@ export function apply(ctx) {
|
|||||||
}
|
}
|
||||||
const target = sessionId || lastSessionId
|
const target = sessionId || lastSessionId
|
||||||
try {
|
try {
|
||||||
const api = ctx.get('apiProxy')
|
|
||||||
if (!api?.sessions?.prompt) throw new Error('apiProxy unavailable')
|
|
||||||
if (!target) throw new Error('no session yet')
|
if (!target) throw new Error('no session yet')
|
||||||
const r = await api.sessions.prompt({
|
const { value, via } = await hostCall('prompt', {
|
||||||
rpcId: randomUUID(),
|
sessionId: target,
|
||||||
payload: {
|
mode: 'queue',
|
||||||
sessionId: target,
|
content: [{ type: 'text', text }],
|
||||||
mode: 'queue',
|
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
content: [{ type: 'text', text }],
|
|
||||||
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const result = r?.result ?? r
|
log(`chat -> session ${target} via ${via}: ${short(value, 160)}`)
|
||||||
log(`chat -> session ${target}: ${JSON.stringify(result).slice(0, 160)}`)
|
|
||||||
res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' })
|
res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' })
|
||||||
res.end(JSON.stringify({ ok: true, sessionId: target, result }))
|
res.end(JSON.stringify({ ok: true, sessionId: target, result: value, via }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const detail = e?.data || e?.message || e
|
const detail = e?.data || e?.message || e
|
||||||
log('chat failed: ' + short(detail, 200))
|
log('chat failed: ' + short(detail, 200))
|
||||||
@@ -372,24 +389,20 @@ export function apply(ctx) {
|
|||||||
} catch { }
|
} catch { }
|
||||||
const json = (obj) => { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)) }
|
const json = (obj) => { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)) }
|
||||||
try {
|
try {
|
||||||
const api = ctx.get('apiProxy')
|
|
||||||
if (!api?.sessions) throw new Error('apiProxy unavailable')
|
|
||||||
if (body.action === 'create') {
|
if (body.action === 'create') {
|
||||||
const r = await api.sessions.create({
|
const { value, via } = await hostCall('create', {
|
||||||
rpcId: randomUUID(),
|
sessionId: body.sessionId ? String(body.sessionId) : randomUUID(),
|
||||||
payload: {
|
...(body.cwd ? { cwd: String(body.cwd) } : {}),
|
||||||
sessionId: body.sessionId ? String(body.sessionId) : randomUUID(),
|
...(body.workspaceId ? { workspaceId: body.workspaceId } : {}),
|
||||||
...(body.cwd ? { cwd: String(body.cwd) } : {}),
|
...(body.agentPreset ? { agentPreset: String(body.agentPreset) } : {}),
|
||||||
...(body.workspaceId ? { workspaceId: body.workspaceId } : {}),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const result = r?.result ?? r
|
const sessionId = value?.sessionId || null
|
||||||
if (result?.sessionId) lastSessionId = result.sessionId
|
if (sessionId) lastSessionId = sessionId
|
||||||
log('session created: ' + short(result, 160))
|
log(`session created via ${via}: ` + short(value, 160))
|
||||||
json({ ok: true, ...result })
|
json({ ok: true, sessionId, via, result: value })
|
||||||
} else if (body.action === 'cancel') {
|
} else if (body.action === 'cancel') {
|
||||||
const r = await api.sessions.cancel({ rpcId: randomUUID(), payload: { sessionId: body.sessionId || lastSessionId } })
|
const { value, via } = await hostCall('cancel', { sessionId: body.sessionId || lastSessionId })
|
||||||
json({ ok: true, result: r?.result ?? r })
|
json({ ok: true, via, result: value })
|
||||||
} else {
|
} else {
|
||||||
json({ ok: true, sessions: snapshot().sessions, current: lastSessionId })
|
json({ ok: true, sessions: snapshot().sessions, current: lastSessionId })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user