diff --git a/README.md b/README.md index edf04e0..cfff404 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ | 自适应调整 | 布局随窗口尺寸重排;拖动**任意边缘和四角**都能缩放(系统级 WM_NCHITTEST) | | 全屏切换 | 标题栏 ⛶ 或 `F11`,返回时还原原来的位置与尺寸 | | 抓屏隐身 | `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、缓存命中率、上下文用量进度条 | | 外观自定义 | 背景效果:不透明 / 毛玻璃(亚克力)/ 云母 / 模糊 / 半透明;不透明度、界面缩放 | | 按钮触发 | 运行 `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))` 写。 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'` 的消息(系统提示快照),否则镜像里会混入一大段内部注入文本。 -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 配置模型) | ## 怎么验证(可复现) diff --git a/lib/host.js b/lib/host.js index 720092b..8d0d8e4 100644 --- a/lib/host.js +++ b/lib/host.js @@ -148,7 +148,30 @@ export function apply(ctx) { writeEndpoint() 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 rows = new Map() // sessionId -> row[] const counts = new Map() // sessionId -> 事件计数兜底(没有 sessionProjection 也能出数字) @@ -338,22 +361,16 @@ export function apply(ctx) { } const target = sessionId || lastSessionId try { - const api = ctx.get('apiProxy') - if (!api?.sessions?.prompt) throw new Error('apiProxy unavailable') if (!target) throw new Error('no session yet') - const r = await api.sessions.prompt({ - rpcId: randomUUID(), - payload: { - sessionId: target, - mode: 'queue', - content: [{ type: 'text', text }], - clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, + const { value, via } = await hostCall('prompt', { + sessionId: target, + mode: 'queue', + content: [{ type: 'text', text }], + clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }) - const result = r?.result ?? r - log(`chat -> session ${target}: ${JSON.stringify(result).slice(0, 160)}`) + log(`chat -> session ${target} via ${via}: ${short(value, 160)}`) 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) { const detail = e?.data || e?.message || e log('chat failed: ' + short(detail, 200)) @@ -372,24 +389,20 @@ export function apply(ctx) { } catch { } const json = (obj) => { res.writeHead(200, { ...cors, 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj)) } try { - const api = ctx.get('apiProxy') - if (!api?.sessions) throw new Error('apiProxy unavailable') if (body.action === 'create') { - const r = await api.sessions.create({ - rpcId: randomUUID(), - payload: { - sessionId: body.sessionId ? String(body.sessionId) : randomUUID(), - ...(body.cwd ? { cwd: String(body.cwd) } : {}), - ...(body.workspaceId ? { workspaceId: body.workspaceId } : {}), - }, + const { value, via } = await hostCall('create', { + sessionId: body.sessionId ? String(body.sessionId) : randomUUID(), + ...(body.cwd ? { cwd: String(body.cwd) } : {}), + ...(body.workspaceId ? { workspaceId: body.workspaceId } : {}), + ...(body.agentPreset ? { agentPreset: String(body.agentPreset) } : {}), }) - const result = r?.result ?? r - if (result?.sessionId) lastSessionId = result.sessionId - log('session created: ' + short(result, 160)) - json({ ok: true, ...result }) + const sessionId = value?.sessionId || null + if (sessionId) lastSessionId = sessionId + log(`session created via ${via}: ` + short(value, 160)) + json({ ok: true, sessionId, via, result: value }) } else if (body.action === 'cancel') { - const r = await api.sessions.cancel({ rpcId: randomUUID(), payload: { sessionId: body.sessionId || lastSessionId } }) - json({ ok: true, result: r?.result ?? r }) + const { value, via } = await hostCall('cancel', { sessionId: body.sessionId || lastSessionId }) + json({ ok: true, via, result: value }) } else { json({ ok: true, sessions: snapshot().sessions, current: lastSessionId }) }