dsh-remote-plugin 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/client.js CHANGED
@@ -20,7 +20,11 @@ window.__ModuleLoader__.load({
20
20
  }
21
21
 
22
22
  var DRAWER_STYLE = {
23
- position: 'fixed', top: 12, right: 12, bottom: 12, zIndex: 2147483000,
23
+ // Desktop's hidden title bar overlays the page (40px on Windows).
24
+ // Electron exposes its current height through the CSS environment value;
25
+ // keep a safe fallback for Desktop hosts without that value.
26
+ position: 'fixed', top: window.location && window.location.protocol === 'dsh-app:' ? 'calc(12px + env(titlebar-area-height, 40px))' : 12,
27
+ right: 12, bottom: 12, zIndex: 2147483000,
24
28
  width: 'min(520px, calc(100vw - 24px))', background: '#0b0e1a',
25
29
  border: '1px solid rgba(125, 207, 255, .22)', borderRadius: 24, overflow: 'hidden',
26
30
  boxShadow: '-16px 18px 48px rgba(0,0,0,.46), 0 0 0 1px rgba(91,140,255,.08)',
@@ -141,7 +145,13 @@ window.__ModuleLoader__.load({
141
145
  }, [])
142
146
  return React.createElement('div', {
143
147
  'aria-hidden': !open,
144
- style: Object.assign({}, DRAWER_STYLE, { transform: open ? 'translateX(0)' : 'translateX(102%)' }),
148
+ style: Object.assign({}, DRAWER_STYLE, {
149
+ transform: open ? 'translateX(0)' : 'translateX(102%)',
150
+ boxShadow: open ? DRAWER_STYLE.boxShadow : 'none',
151
+ pointerEvents: open ? 'auto' : 'none',
152
+ visibility: open ? 'visible' : 'hidden',
153
+ transition: open ? 'transform .22s ease' : 'transform .22s ease, visibility 0s linear .22s',
154
+ }),
145
155
  },
146
156
  loaded.current ? React.createElement('iframe', {
147
157
  src: '/remote/plugin.html',
package/gateway.cjs CHANGED
@@ -1703,6 +1703,12 @@ async function translateModernRpc(method, payload, rpcId) {
1703
1703
  if (!['create', 'rename', 'delete', 'insertBefore', 'insertSessionBefore', 'archiveSession'].includes(verb)) return legacyFallback('no generated RPC adapter')
1704
1704
  endpoint = 'workspace/' + verb
1705
1705
  args = { request: payload }
1706
+ if (verb === 'archiveSession') transform = value => {
1707
+ // The HTTP receipt precedes workspace/follow on another connection.
1708
+ // Publish the committed archive set before a client's immediate refresh.
1709
+ if (Array.isArray(value?.archivedSessionIds)) applyModernWorkspaceFrame({ type: 'archived', ...value })
1710
+ return value
1711
+ }
1706
1712
  } else if (method.startsWith('goal.')) {
1707
1713
  const verb = method.slice('goal.'.length)
1708
1714
  if (!['create', 'edit', 'pause', 'resume', 'complete', 'clear'].includes(verb)) return legacyFallback('no generated RPC adapter')
@@ -1767,6 +1773,31 @@ async function translateModernRpc(method, payload, rpcId) {
1767
1773
  return legacyEnvelope(rpcId, modernError(error.message, 'image-vision-failed'))
1768
1774
  }
1769
1775
  }
1776
+ if (method === 'subagent.list' && modernResponseNeedsLegacyFallback(response)) {
1777
+ // DSH 0.2 removed subagents/list; discovery is now a non-activating
1778
+ // read of the parent's durable catalog and each child's projections.
1779
+ const parent = await callUpstreamRemote('session/projections', { request: { sessionId: payload.parentSessionId } }, rpcId)
1780
+ if (modernResponseNeedsLegacyFallback(parent)) return legacyFallback('generated subagent catalog unavailable')
1781
+ if (!parent.body?.result?.ok) return parent.body || legacyEnvelope(rpcId, modernError('DSH subagent catalog read failed'))
1782
+ const catalog = parent.body.result.value?.values?.subagentCatalog
1783
+ if (!Array.isArray(catalog)) return legacyEnvelope(rpcId, modernError('DSH did not expose a subagent catalog projection', 'subagent/projections-unavailable'))
1784
+ await refreshModernSessions()
1785
+ const entries = await Promise.all(catalog.map(async child => {
1786
+ if (!['one-shot', 'continuable'].includes(child.mode)) return { kind: 'diagnostic', id: child.id, reason: 'unsupported' }
1787
+ try {
1788
+ const read = await callUpstreamRemote('session/projections', { request: { sessionId: child.id } })
1789
+ if (!read.body?.result?.ok || !read.body.result.value) return { kind: 'diagnostic', id: child.id, reason: 'unavailable' }
1790
+ const values = read.body.result.value.values
1791
+ const identity = values?.subagent
1792
+ if (!identity || identity.mode !== child.mode) return { kind: 'diagnostic', id: child.id, reason: 'corrupt' }
1793
+ return { kind: 'child', id: child.id, mode: identity.mode,
1794
+ ...(identity.label === undefined ? {} : { label: identity.label }),
1795
+ activity: modernState.sessions.get(child.id)?.running ? 'running' : 'inactive',
1796
+ hasChildren: Array.isArray(values.subagentCatalog) && values.subagentCatalog.length > 0 }
1797
+ } catch { return { kind: 'diagnostic', id: child.id, reason: 'unavailable' } }
1798
+ }))
1799
+ return legacyEnvelope(rpcId, { ok: true, value: { entries, parentAvailable: !!modernState.sessions.get(payload.parentSessionId)?.agentAvailable } })
1800
+ }
1770
1801
  if (modernResponseNeedsLegacyFallback(response)) return legacyFallback(`generated RPC ${endpoint} unavailable`)
1771
1802
  if (!response.body?.result?.ok) {
1772
1803
  recordCompatibility('generated-rpc-error', { method, status: response.status, detail: remoteFailureKind(response) })
@@ -1818,6 +1849,8 @@ function rememberCollectorReplay(kind, full, raw) {
1818
1849
  let key = ''
1819
1850
  if (payload.type === 'session/subscribed' && payload.sessionId) key = `session:${payload.sessionId}`
1820
1851
  else if (payload.type === 'session/reasoning' && payload.sessionId) key = `reasoning:${payload.sessionId}`
1852
+ else if (payload.type === 'session/queue' && payload.sessionId) key = `queue:${payload.sessionId}`
1853
+ else if (payload.type === 'session/jobs' && payload.sessionId) key = `jobs:${payload.sessionId}`
1821
1854
  else if (payload.type === 'approval/requested' && payload.approvalId) key = `approval:${payload.approvalId}`
1822
1855
  else if (payload.type === 'question/requested' && full.rpcId) key = `question:${full.rpcId}`
1823
1856
  else if (payload.type === 'approval/resolved' && payload.approvalId) replay.delete(`approval:${payload.approvalId}`)
@@ -2095,6 +2128,28 @@ function openModernSessionStream(ws, sessionId) {
2095
2128
  type: 'open', streamId, endpoint: 'session/follow',
2096
2129
  payload: { args: { request: { address: { kind: 'session', sessionId }, assistantStream: true } } },
2097
2130
  }))
2131
+ ws.send(JSON.stringify({
2132
+ type: 'open', streamId: 'jobs:' + sessionId, endpoint: 'job/list',
2133
+ payload: { args: { request: { sessionId } } },
2134
+ }))
2135
+ }
2136
+
2137
+ function applyModernProjection(sessionId, key, value, seq) {
2138
+ legacyPush('mux', { type: 'session/projection', sessionId, key, value, seq })
2139
+ if (key === 'inbox') {
2140
+ // DSH 0.2 stores queue/steer state in the durable Inbox projection.
2141
+ const items = ['next-turn', 'next-step'].flatMap(target =>
2142
+ (Array.isArray(value?.[target]) ? value[target] : []).filter(message => message?.id).map(message => ({
2143
+ id: message.id, placement: target === 'next-turn' ? 'queued' : 'context', message,
2144
+ })))
2145
+ legacyPush('mux', { type: 'session/queue', sessionId, items })
2146
+ }
2147
+ }
2148
+
2149
+ function applyModernJobFrame(sessionId, value) {
2150
+ if (value?.type === 'rows' && Array.isArray(value.jobs)) {
2151
+ legacyPush('mux', { type: 'session/jobs', sessionId, jobs: value.jobs })
2152
+ }
2098
2153
  }
2099
2154
 
2100
2155
  function applyModernControlFrame(value) {
@@ -2107,16 +2162,14 @@ function applyModernControlFrame(value) {
2107
2162
  }
2108
2163
  for (const [sessionId, block] of Object.entries(value.value?.projections || {})) {
2109
2164
  for (const [key, projection] of Object.entries(block?.values || {})) {
2110
- legacyPush('mux', { type: 'session/projection', sessionId, key, value: projection, seq: block?.asOfSeq ?? 0 })
2165
+ applyModernProjection(sessionId, key, projection, block?.asOfSeq ?? 0)
2111
2166
  }
2112
2167
  }
2113
2168
  return
2114
2169
  }
2115
2170
  if (value?.type === 'queue') legacyPush('mux', { type: 'session/queue', sessionId: value.sessionId, items: value.items || [] })
2116
2171
  else if (value?.type === 'jobs') legacyPush('mux', { type: 'session/jobs', sessionId: value.sessionId, jobs: value.jobs || [] })
2117
- else if (value?.type === 'projection') legacyPush('mux', {
2118
- type: 'session/projection', sessionId: value.sessionId, key: value.key, value: value.value, seq: value.seq,
2119
- })
2172
+ else if (value?.type === 'projection') applyModernProjection(value.sessionId, value.key, value.value, value.seq)
2120
2173
  }
2121
2174
 
2122
2175
  function applyModernWorkspaceFrame(value) {
@@ -2209,7 +2262,7 @@ function applyModernSessionFrame(sessionId, value) {
2209
2262
  if (record?.event) legacyPush('mux', { type: 'session/event', sessionId, event: record.event })
2210
2263
  }
2211
2264
  for (const [key, projection] of Object.entries(value.projections?.values || {})) {
2212
- legacyPush('mux', { type: 'session/projection', sessionId, key, value: projection, seq: value.projections?.asOfSeq ?? value.cursor })
2265
+ applyModernProjection(sessionId, key, projection, value.projections?.asOfSeq ?? value.cursor)
2213
2266
  }
2214
2267
  return
2215
2268
  }
@@ -2282,7 +2335,11 @@ function applyModernRemoteEvent(ws, value) {
2282
2335
  modernState.sessionCursors.delete(args[0])
2283
2336
  modernState.assistantStreams.delete(args[0])
2284
2337
  collectorReplay.mux.delete(`reasoning:${args[0]}`)
2338
+ collectorReplay.mux.delete(`session:${args[0]}`)
2339
+ collectorReplay.mux.delete(`queue:${args[0]}`)
2340
+ collectorReplay.mux.delete(`jobs:${args[0]}`)
2285
2341
  try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'session:' + args[0] })) } catch {}
2342
+ try { ws.send(JSON.stringify({ type: 'cancel', streamId: 'jobs:' + args[0] })) } catch {}
2286
2343
  legacyPush('host', { type: 'host/session-removed', sessionId: args[0] })
2287
2344
  } else if (value.event === 'api-session/status') {
2288
2345
  legacyPush('host', { type: 'host/session-status', sessionId: args[0], running: !!args[1] })
@@ -2371,6 +2428,9 @@ function startModernEventCollector() {
2371
2428
  const data = typeof ev.data === 'string' ? ev.data : Buffer.isBuffer(ev.data) ? ev.data.toString() : String(ev.data)
2372
2429
  const frame = JSON.parse(data)
2373
2430
  if (frame.type === 'error') {
2431
+ // Old generated-RPC Hosts may have no job namespace. This optional
2432
+ // roster must not degrade the working session and approval streams.
2433
+ if (frame.streamId?.startsWith('jobs:') && ['gateway/definition-unavailable', 'gateway/invocation-unavailable', 'gateway/service-unavailable', 'gateway/method-unavailable', 'method-unavailable', 'not-found'].includes(frame.error?.code)) return
2374
2434
  const message = frame.error?.message || 'modern stream error'
2375
2435
  for (const state of Object.values(eventCollectorState)) state.lastError = message
2376
2436
  legacyPush('mux', { type: 'stream/error', error: frame.error || { message } })
@@ -2380,6 +2440,7 @@ function startModernEventCollector() {
2380
2440
  if (frame.streamId === 'events') applyModernRemoteEvent(current, frame.value)
2381
2441
  else if (frame.streamId === 'control') applyModernControlFrame(frame.value)
2382
2442
  else if (frame.streamId === 'workspaces') applyModernWorkspaceFrame(frame.value)
2443
+ else if (frame.streamId.startsWith('jobs:')) applyModernJobFrame(frame.streamId.slice(5), frame.value)
2383
2444
  else if (frame.streamId.startsWith('session:')) applyModernSessionFrame(frame.streamId.slice(8), frame.value)
2384
2445
  } catch {}
2385
2446
  }
@@ -4197,11 +4258,14 @@ function serveHandoff(req, res, url) {
4197
4258
  fsJson(res, 400, { error: 'sessionId required' })
4198
4259
  return
4199
4260
  }
4261
+ // 白名单字段只接受字符串:对象/数字/数组等一律按空串处理,
4262
+ // 避免 String({}) 产出 '[object Object]' 之类垃圾落盘并广播到其他设备
4263
+ const str = (v) => typeof v === 'string' ? v : ''
4200
4264
  handoffSave({
4201
4265
  sessionId: sessionId.slice(0, 128),
4202
- title: String(payload.title || '').slice(0, 200),
4203
- device: String(payload.device || '').slice(0, 80),
4204
- clientId: String(payload.clientId || '').slice(0, 96),
4266
+ title: str(payload.title).slice(0, 200),
4267
+ device: str(payload.device).slice(0, 80),
4268
+ clientId: str(payload.clientId).slice(0, 96),
4205
4269
  at: Date.now()
4206
4270
  })
4207
4271
  fsJson(res, 200, { ok: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -501,9 +501,11 @@ function closeNotesModal() {
501
501
  }
502
502
  async function checkNotesOnStart() {
503
503
  try {
504
- const res = await fetch('../update.json?t=' + Date.now())
505
- if (!res.ok) return
506
- openNotesModal(await res.json())
504
+ const res = await fetch('../update.json?t=' + Date.now(), { cache: 'no-store' })
505
+ if (!res.ok || res.redirected || !String(res.headers.get('content-type') || '').includes('application/json')) return
506
+ const info = await res.json()
507
+ if (!info || typeof info !== 'object' || !info.version) return
508
+ openNotesModal(info)
507
509
  } catch {}
508
510
  }
509
511
  async function submitFeedback() {
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.7.1",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "a2c885aecf6d677b76413d0ed92a369a7045f3227a4c8b90d3365c3dfc9979f9",
5
- "releasedAt": "2026-09-24T07:05:55.044Z",
6
- "notes": "0.7.0:Windows 上未注册为服务的 DSH 可由 Remote 网关按已检查的 CLI 启动配置受控启动;已有 Windows 服务仍优先使用,网关只重启自己启动并持有句柄的进程。手机发送图片遇到 DSH 明确拒绝(当前模型不支持图片)时,可通过已启用的 dsh-image-vision 插件桥接附件;桥接失败不会提交不完整消息。保留长按插队发送、跨设备接续、鸿蒙客户端源码与平板分栏、网关 WebSocket Ping 分帧修复等 0.7.0 RC 功能。本次提供 Android APK 与 Windows/Linux 网关,不提供鸿蒙 HAP。",
4
+ "sha256": "7ca47a78d152754d4991d5605d8e368ee0e4a20b7bfe1a90055d72c04069cff1",
5
+ "releasedAt": "2026-10-02T13:59:16.334Z",
6
+ "notes": "适配 DSH 0.2.0-rc.2 的 Inbox 队列、后台任务流与子代理目录,修复子代理接口 404 和归档后刷新状态滞后,保留旧版实时通道兼容;修复 Desktop 插件面板被标题栏遮挡、收起后阴影残留及更新说明响应校验。合入鸿蒙插件中心、长按发送和主题改进,并修复切换连接时的旧确认卡、误发送及迟到回执问题;鸿蒙本次仅更新源码,暂不提供安装包。",
7
7
  "history": [
8
+ {
9
+ "version": "0.7.1",
10
+ "notes": "适配 DSH 0.2.0-rc.2 的 Inbox 队列、后台任务流与子代理目录,修复子代理接口 404 和归档后刷新状态滞后,保留旧版实时通道兼容;修复 Desktop 插件面板被标题栏遮挡、收起后阴影残留及更新说明响应校验。合入鸿蒙插件中心、长按发送和主题改进,并修复切换连接时的旧确认卡、误发送及迟到回执问题;鸿蒙本次仅更新源码,暂不提供安装包。"
11
+ },
8
12
  {
9
13
  "version": "0.7.0",
10
14
  "notes": "0.7.0:Windows 上未注册为服务的 DSH 可由 Remote 网关按已检查的 CLI 启动配置受控启动;已有 Windows 服务仍优先使用,网关只重启自己启动并持有句柄的进程。手机发送图片遇到 DSH 明确拒绝(当前模型不支持图片)时,可通过已启用的 dsh-image-vision 插件桥接附件;桥接失败不会提交不完整消息。保留长按插队发送、跨设备接续、鸿蒙客户端源码与平板分栏、网关 WebSocket Ping 分帧修复等 0.7.0 RC 功能。本次提供 Android APK 与 Windows/Linux 网关,不提供鸿蒙 HAP。"
@@ -40,10 +44,6 @@
40
44
  {
41
45
  "version": "0.6.19",
42
46
  "notes": "0.6.19:全面优化手机端、桌面端、管理页和插件页的布局、动效与响应式体验,并支持减少动态效果偏好;重做蓝色、草原、音乐厅等主题的明暗层级和文字图标对比度,新增利落的黑白配色;统一主页图标配色并修正设置齿轮中心孔偏移;目标面板支持收起为始终可见的侧边入口,保留运行状态、可随时恢复,并按会话与目标在本地记忆。"
43
- },
44
- {
45
- "version": "0.6.18",
46
- "notes": "0.6.18:新增独立 Token 量趋势统计并保留原有费用趋势;服务器连接身份改为地址加令牌,支持同地址不同令牌并兼容旧配置;投票在网关不可达时尝试直连公网收集器托底(需收集器允许 App/Web CORS);修复 Windows 下 gateway-stats 调用 zstd 时弹出终端窗口的问题,并补充回归测试;同步完善相关模块文档。"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.7.0"
2
+ "version": "0.7.1"
3
3
  }