dsh-plugin-mobile-gateway 0.10.1 → 0.10.3
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.
- package/PROTOCOL.md +56 -7
- package/README.md +15 -3
- package/docs/dsh-rc2-mobile-integration.md +9 -1
- package/docs/file-cards-mobile-integration.md +111 -0
- package/docs/voice-input-implementation.md +56 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/README.md +33 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-objc.txt +222957 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-preconfigure-annotated.asm +90 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-preconfigure.asm +90 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-session-configure-annotated.asm +131 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-session-configure.asm +131 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-string-reference-candidates.json +20 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/deepseek-touch-began.asm +133 -0
- package/docs/voice-input-ipa-evidence/startup-20261014/rea-macho-summary.json +2435 -0
- package/lib/dsh-host-adapter.mjs +19 -13
- package/lib/index.mjs +88 -16
- package/lib/session-event-details.mjs +53 -0
- package/package.json +3 -3
package/lib/dsh-host-adapter.mjs
CHANGED
|
@@ -13,7 +13,7 @@ function requireArray(value, endpoint) {
|
|
|
13
13
|
return value
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const DSH_VERSION = '0.2.
|
|
16
|
+
export const DSH_VERSION = '0.2.1-rc.2'
|
|
17
17
|
export const SESSION_FORMAT_VERSION = 4
|
|
18
18
|
|
|
19
19
|
export function readHistoryRecords(records) {
|
|
@@ -44,12 +44,12 @@ export function readSessionSnapshot(frame, sessionId) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function firstStreamFrame(
|
|
47
|
+
async function firstStreamFrame(stream, namespace, method, args, signal) {
|
|
48
48
|
const ownedAbort = new AbortController()
|
|
49
49
|
const streamSignal = signal ? AbortSignal.any([signal, ownedAbort.signal]) : ownedAbort.signal
|
|
50
50
|
let iterator
|
|
51
51
|
try {
|
|
52
|
-
const iterable = await
|
|
52
|
+
const iterable = await stream(namespace, method, args, streamSignal)
|
|
53
53
|
const iteratorFactory = iterable?.[Symbol.asyncIterator] ?? iterable?.[Symbol.iterator]
|
|
54
54
|
if (typeof iteratorFactory !== 'function') throw new Error(`${namespace}/${method} returned an invalid stream`)
|
|
55
55
|
iterator = iteratorFactory.call(iterable)
|
|
@@ -78,21 +78,28 @@ function sessionListArgs(request) {
|
|
|
78
78
|
* descriptors. Its public methods intentionally match mobile-gateway domain
|
|
79
79
|
* operations, not the upstream transport envelope.
|
|
80
80
|
*/
|
|
81
|
-
export function createDshHostAdapter(typertGateway) {
|
|
81
|
+
export function createDshHostAdapter(typertGateway, peer) {
|
|
82
82
|
if (!typertGateway || typeof typertGateway.invoke !== 'function' || typeof typertGateway.stream !== 'function') {
|
|
83
83
|
throw new Error('mobile-gateway requires the DSH Remote Gateway host service')
|
|
84
84
|
}
|
|
85
|
+
if (!peer || !(peer.signal instanceof AbortSignal)) {
|
|
86
|
+
throw new Error('mobile-gateway requires a DSH Connection Peer (DSH 0.2.1-rc.2 or later)')
|
|
87
|
+
}
|
|
85
88
|
|
|
86
89
|
const invoke = (namespace, method, args, signal) => typertGateway.invoke({
|
|
87
90
|
namespace,
|
|
88
91
|
method,
|
|
89
92
|
args,
|
|
93
|
+
peer,
|
|
90
94
|
...(signal === undefined ? {} : { signal }),
|
|
91
95
|
})
|
|
96
|
+
const stream = (namespace, method, args, signal) => typertGateway.stream({
|
|
97
|
+
namespace, method, args, peer, signal,
|
|
98
|
+
})
|
|
92
99
|
|
|
93
100
|
const sessionSnapshot = async (request, signal) => {
|
|
94
101
|
const frame = requireRecord(await firstStreamFrame(
|
|
95
|
-
|
|
102
|
+
stream,
|
|
96
103
|
'session',
|
|
97
104
|
'follow',
|
|
98
105
|
requestArgs(request),
|
|
@@ -200,7 +207,7 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
200
207
|
return {
|
|
201
208
|
speech: {
|
|
202
209
|
catalog: (signal) => invoke('speech', 'catalog', {}, signal),
|
|
203
|
-
follow: (signal) =>
|
|
210
|
+
follow: (signal) => stream('speech', 'follow', {}, signal),
|
|
204
211
|
configure: (patch, signal) => invoke('speech', 'configure', { patch }, signal),
|
|
205
212
|
prepare: (providerId, options, signal) => invoke('speech', 'prepare', {
|
|
206
213
|
providerId, ...(options === undefined ? {} : { options }),
|
|
@@ -248,7 +255,7 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
248
255
|
},
|
|
249
256
|
workspace: {
|
|
250
257
|
async list(_payload = {}, signal) {
|
|
251
|
-
const frame = requireRecord(await firstStreamFrame(
|
|
258
|
+
const frame = requireRecord(await firstStreamFrame(stream, 'workspace', 'follow', {}, signal), 'workspace/follow')
|
|
252
259
|
if (frame.type !== 'baseline') throw new Error('workspace/follow returned an invalid opening baseline')
|
|
253
260
|
return requireRecord(frame.value, 'workspace/follow baseline')
|
|
254
261
|
},
|
|
@@ -371,18 +378,17 @@ export function createDshHostAdapter(typertGateway) {
|
|
|
371
378
|
if (event.type === 'turn/start') admittedPrompts.delete(sessionId)
|
|
372
379
|
},
|
|
373
380
|
openSessionStream(sessionId, signal) {
|
|
374
|
-
return
|
|
375
|
-
namespace: 'session', method: 'follow',
|
|
381
|
+
return stream('session', 'follow',
|
|
376
382
|
// Bound the opening window at the Host; older records remain available via session.page.
|
|
377
|
-
|
|
383
|
+
requestArgs({ address: { kind: 'session', sessionId }, maxMessages: 12, assistantStream: true }),
|
|
378
384
|
signal,
|
|
379
|
-
|
|
385
|
+
)
|
|
380
386
|
},
|
|
381
387
|
openControlStream(signal) {
|
|
382
|
-
return
|
|
388
|
+
return stream('session', 'control', {}, signal)
|
|
383
389
|
},
|
|
384
390
|
openWorkspaceStream(signal) {
|
|
385
|
-
return
|
|
391
|
+
return stream('workspace', 'follow', {}, signal)
|
|
386
392
|
},
|
|
387
393
|
}
|
|
388
394
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stringifyWireFrame } from './wire-json.mjs'
|
|
2
|
+
import { assistantFinish, failureDetails, toolResultDetails, turnReasonDetails } from './session-event-details.mjs'
|
|
2
3
|
// dsh-plugin-mobile-gateway — persistent Host plugin.
|
|
3
4
|
//
|
|
4
5
|
// Two directions:
|
|
@@ -35,6 +36,7 @@ import { stringifyWireFrame } from './wire-json.mjs'
|
|
|
35
36
|
// { "type": "attachment", "sessionId", "attachmentId" }
|
|
36
37
|
// -> verified image metadata + canonical base64 bytes
|
|
37
38
|
// { "type": "file-list", "requestId"?, "sessionId", "path"? }
|
|
39
|
+
// { "type": "file-resolve", "requestId", "sessionId", "path" }
|
|
38
40
|
// { "type": "file-download-open", "requestId", "sessionId", "path" }
|
|
39
41
|
// { "type": "file-download-read", "transferId", "offset" }
|
|
40
42
|
// { "type": "file-download-cancel", "transferId" }
|
|
@@ -93,6 +95,7 @@ import { stringifyWireFrame } from './wire-json.mjs'
|
|
|
93
95
|
// { "kind": "command-selected", "sessionId", "command", "selected" }
|
|
94
96
|
// { "kind": "attachment", "sessionId", "attachment", "data" }
|
|
95
97
|
// { "kind": "file-list", "requestId"?, "sessionId", "path", "entries" }
|
|
98
|
+
// { "kind": "file-resolved", "requestId", "sessionId", "path", "name", "mediaType", "size", "modifiedAt" }
|
|
96
99
|
// { "kind": "file-download-opened", "requestId", "transferId", "sessionId",
|
|
97
100
|
// "path", "name", "mediaType", "size", "chunkBytes" }
|
|
98
101
|
// { "kind": "file-download-chunk", "transferId", "offset", "data", "eof", "sha256"? }
|
|
@@ -308,6 +311,7 @@ function buildWireEvent(session, event) {
|
|
|
308
311
|
}
|
|
309
312
|
return Object.assign(base, {
|
|
310
313
|
event: { type: 'assistant/message', turn: d.turn, step: d.step, text, reasoning, toolCalls,
|
|
314
|
+
...(assistantFinish(d) ? { finish: assistantFinish(d) } : {}),
|
|
311
315
|
...(d.interrupted === true ? { interrupted: true } : {}),
|
|
312
316
|
...(d.usage === undefined ? {} : { usage: d.usage }),
|
|
313
317
|
...(images.length ? { images } : {}),
|
|
@@ -315,7 +319,15 @@ function buildWireEvent(session, event) {
|
|
|
315
319
|
})
|
|
316
320
|
}
|
|
317
321
|
case 'assistant/attempt':
|
|
318
|
-
return Object.assign(base, { event: { type: event.type, turn: d.turn, step: d.step, stream: d.stream
|
|
322
|
+
return Object.assign(base, { event: { type: event.type, turn: d.turn, step: d.step, stream: d.stream,
|
|
323
|
+
...(assistantFinish(d) ? { finish: assistantFinish(d) } : {}) } })
|
|
324
|
+
case 'llm/retry':
|
|
325
|
+
case 'llm/retry-started':
|
|
326
|
+
return Object.assign(base, { event: { type: event.type, turn: d.turn, step: d.step,
|
|
327
|
+
retryId: d.retryId, retry: d.retry,
|
|
328
|
+
...(event.type === 'llm/retry' ? { provider: d.provider, mode: d.mode,
|
|
329
|
+
maxRetries: d.maxRetries, delayMs: d.delayMs, failure: failureDetails(d.failure) } : {}),
|
|
330
|
+
} })
|
|
319
331
|
case 'session/title':
|
|
320
332
|
return Object.assign(base, {
|
|
321
333
|
event: {
|
|
@@ -326,6 +338,16 @@ function buildWireEvent(session, event) {
|
|
|
326
338
|
})
|
|
327
339
|
case 'agent-preset/selected':
|
|
328
340
|
return Object.assign(base, { event: { type: event.type, agentPreset: d.agentPreset } })
|
|
341
|
+
case 'deliverables/presented':
|
|
342
|
+
return Object.assign(base, {
|
|
343
|
+
event: { type: event.type, turn: d.turn, callId: d.callId,
|
|
344
|
+
files: (Array.isArray(d.files) ? d.files : [])
|
|
345
|
+
.filter(file => file && typeof file.path === 'string' && file.path.trim())
|
|
346
|
+
.map(file => ({ path: file.path,
|
|
347
|
+
...(typeof file.description === 'string' ? { description: file.description } : {}),
|
|
348
|
+
})),
|
|
349
|
+
},
|
|
350
|
+
})
|
|
329
351
|
case 'tool/call':
|
|
330
352
|
return Object.assign(base, {
|
|
331
353
|
event: { type: 'tool/call', turn: d.turn, step: d.step, callId: d.callId, name: d.name, arguments: d.arguments },
|
|
@@ -347,6 +369,7 @@ function buildWireEvent(session, event) {
|
|
|
347
369
|
callId: d.message?.toolCallId ?? d.message?.source?.callId,
|
|
348
370
|
isError: d.message?.isError === true || !!d.error || ((d.message && d.message.content) || []).some(block => block?.type === 'tool-result' && block.isError === true),
|
|
349
371
|
preview,
|
|
372
|
+
...toolResultDetails(d),
|
|
350
373
|
...(d.error ? { error: d.error } : {}),
|
|
351
374
|
},
|
|
352
375
|
})
|
|
@@ -405,7 +428,8 @@ function buildWireEvent(session, event) {
|
|
|
405
428
|
case 'step/start':
|
|
406
429
|
case 'step/end':
|
|
407
430
|
return Object.assign(base, {
|
|
408
|
-
event: { type: event.type, turn: d.turn, step: d.step, reason: d.reason && d.reason.kind
|
|
431
|
+
event: { type: event.type, turn: d.turn, step: d.step, reason: d.reason && d.reason.kind,
|
|
432
|
+
...(turnReasonDetails(d.reason) ? { reasonDetails: turnReasonDetails(d.reason) } : {}) },
|
|
409
433
|
})
|
|
410
434
|
default:
|
|
411
435
|
return Object.assign(base, { event: { type: event.type } })
|
|
@@ -674,20 +698,31 @@ function createFileTransferManager(api, options) {
|
|
|
674
698
|
if (!stat.isDirectory()) {
|
|
675
699
|
return fileTransferError('file-workspace-unavailable', 'the session working directory is unavailable', requestType, sessionId)
|
|
676
700
|
}
|
|
677
|
-
return { root }
|
|
701
|
+
return { root, inputRoot: path.resolve(item.cwd) }
|
|
678
702
|
} catch {
|
|
679
703
|
return fileTransferError('file-workspace-unavailable', 'the session working directory is unavailable', requestType, sessionId)
|
|
680
704
|
}
|
|
681
705
|
}
|
|
682
706
|
|
|
683
707
|
const resolveWorkspaceTarget = async (sessionId, rawPath, requestType, inputOptions) => {
|
|
684
|
-
|
|
708
|
+
// Only the resolver accepts native absolute paths from recorded deliveries
|
|
709
|
+
// or Markdown links. Download requests remain workspace-relative.
|
|
710
|
+
const absoluteInput = inputOptions?.allowAbsolute === true
|
|
711
|
+
&& typeof rawPath === 'string' && path.isAbsolute(rawPath.trim())
|
|
712
|
+
const requested = absoluteInput
|
|
713
|
+
? (rawPath.includes('\0') || rawPath.trim().split(/[\\/]+/).includes('..') ? null : rawPath.trim())
|
|
714
|
+
: normalizedWorkspaceInput(rawPath, inputOptions)
|
|
685
715
|
if (!requested) {
|
|
686
|
-
return fileTransferError('bad-request', `${requestType} requires a relative workspace path`, requestType, sessionId)
|
|
716
|
+
return fileTransferError('bad-request', `${requestType} requires a valid ${inputOptions?.allowAbsolute ? '' : 'relative '}workspace path`, requestType, sessionId)
|
|
687
717
|
}
|
|
688
718
|
const rootResult = await resolveSessionRoot(sessionId, requestType)
|
|
689
719
|
if (rootResult.kind === 'error') return rootResult
|
|
690
|
-
|
|
720
|
+
// macOS commonly exposes one workspace as both /var/... and /private/var/...
|
|
721
|
+
// (and cwd itself may be a symlink). Map only the known cwd alias before
|
|
722
|
+
// enforcing the canonical root, then still verify the target's realpath.
|
|
723
|
+
const candidate = absoluteInput && isInsideWorkspace(rootResult.inputRoot, requested)
|
|
724
|
+
? path.resolve(rootResult.root, path.relative(rootResult.inputRoot, requested))
|
|
725
|
+
: path.resolve(rootResult.root, requested)
|
|
691
726
|
if (!isInsideWorkspace(rootResult.root, candidate)) {
|
|
692
727
|
return fileTransferError('file-not-allowed', 'path must stay inside the session working directory', requestType, sessionId)
|
|
693
728
|
}
|
|
@@ -706,6 +741,28 @@ function createFileTransferManager(api, options) {
|
|
|
706
741
|
}
|
|
707
742
|
}
|
|
708
743
|
|
|
744
|
+
const resolveFile = async (msg) => {
|
|
745
|
+
const session = requireSessionId(msg)
|
|
746
|
+
if (session.error) return session.error
|
|
747
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId.trim()) {
|
|
748
|
+
return fileTransferError('bad-request', 'file-resolve requires a requestId', 'file-resolve', session.value)
|
|
749
|
+
}
|
|
750
|
+
const resolved = await resolveWorkspaceTarget(session.value, msg.path, 'file-resolve', { allowAbsolute: true })
|
|
751
|
+
if (resolved.kind === 'error') return resolved
|
|
752
|
+
if (!resolved.stat.isFile()) {
|
|
753
|
+
return fileTransferError('file-not-regular', 'only regular files can be previewed', 'file-resolve', session.value)
|
|
754
|
+
}
|
|
755
|
+
if (resolved.stat.size > options.fileDownloadMaxBytes) {
|
|
756
|
+
return fileTransferError('file-too-large', 'file exceeds the configured download limit', 'file-resolve', session.value)
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
kind: 'file-resolved', requestId: msg.requestId, sessionId: session.value,
|
|
760
|
+
path: workspaceRelativePath(resolved.root, resolved.target),
|
|
761
|
+
name: path.basename(resolved.target), mediaType: fileMediaType(resolved.target),
|
|
762
|
+
size: resolved.stat.size, modifiedAt: resolved.stat.mtimeMs,
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
709
766
|
const fileList = async (msg) => {
|
|
710
767
|
const session = requireSessionId(msg)
|
|
711
768
|
if (session.error) return session.error
|
|
@@ -885,9 +942,11 @@ function createFileTransferManager(api, options) {
|
|
|
885
942
|
|
|
886
943
|
return {
|
|
887
944
|
async handle(client, msg) {
|
|
945
|
+
const correlation = typeof msg.requestId === 'string' ? { requestId: msg.requestId } : {}
|
|
888
946
|
if (!options.fileDownloadsEnabled) {
|
|
889
|
-
return fileTransferError('file-download-disabled', 'file downloads are disabled by gateway configuration', msg.type)
|
|
947
|
+
return { ...fileTransferError('file-download-disabled', 'file downloads are disabled by gateway configuration', msg.type), ...correlation }
|
|
890
948
|
}
|
|
949
|
+
if (msg.type === 'file-resolve') return { ...await resolveFile(msg), ...correlation }
|
|
891
950
|
if (msg.type === 'file-list') return fileList(msg)
|
|
892
951
|
if (msg.type === 'file-download-open') return open(client, msg)
|
|
893
952
|
if (msg.type === 'file-download-read') return read(client, msg)
|
|
@@ -1078,12 +1137,14 @@ function trimConversationEvent(event) {
|
|
|
1078
1137
|
case 'assistant/message':
|
|
1079
1138
|
case 'assistant/attempt': {
|
|
1080
1139
|
const { stream, ...data } = event.data || {}
|
|
1081
|
-
|
|
1140
|
+
const finish = assistantFinish(event.data || {})
|
|
1141
|
+
return { ...event, data: { ...data, ...(finish ? { finish } : {}) } }
|
|
1082
1142
|
}
|
|
1083
1143
|
case 'tool/result': {
|
|
1084
1144
|
const d = event.data || {}
|
|
1085
1145
|
const message = d.message
|
|
1086
|
-
|
|
1146
|
+
const details = toolResultDetails(d)
|
|
1147
|
+
if (!message || !Array.isArray(message.content)) return { ...event, data: { ...d, ...details } }
|
|
1087
1148
|
let changed = false
|
|
1088
1149
|
const truncateBlock = (block) => {
|
|
1089
1150
|
if (!block || typeof block !== 'object') return block
|
|
@@ -1097,8 +1158,7 @@ function trimConversationEvent(event) {
|
|
|
1097
1158
|
return block
|
|
1098
1159
|
}
|
|
1099
1160
|
const content = message.content.map(truncateBlock)
|
|
1100
|
-
|
|
1101
|
-
return { ...event, data: { ...d, message: { ...message, content } } }
|
|
1161
|
+
return { ...event, data: { ...d, ...details, ...(changed ? { message: { ...message, content } } : {}) } }
|
|
1102
1162
|
}
|
|
1103
1163
|
default:
|
|
1104
1164
|
return event
|
|
@@ -2133,13 +2193,19 @@ const plugin = {
|
|
|
2133
2193
|
// Hard dependencies: the gateway registers on the host web server and
|
|
2134
2194
|
// admits mobile messages through the Host Remote Gateway. Cordis must not
|
|
2135
2195
|
// activate this row before either service exists.
|
|
2136
|
-
inject: ['webServer', 'typertGateway', 'agentDefaultModel'],
|
|
2196
|
+
inject: ['webServer', 'typertGateway', 'connection', 'agentDefaultModel'],
|
|
2137
2197
|
// exported for local tests; Cordis ignores unknown plugin fields
|
|
2138
2198
|
admitMessage,
|
|
2139
2199
|
handleQuery,
|
|
2140
2200
|
apply(ctx, config) {
|
|
2141
2201
|
const webServer = ctx.webServer
|
|
2142
|
-
|
|
2202
|
+
if (typeof ctx.connection?.createPeer !== 'function') {
|
|
2203
|
+
throw new Error('mobile-gateway requires DSH 0.2.1-rc.2 or later with Connection.createPeer')
|
|
2204
|
+
}
|
|
2205
|
+
// 已配对设备沿用本机用户权限;身份只由 Host 创建,不能由移动端参数指定。
|
|
2206
|
+
// 问答与审批仍由本插件的 Host 监听器处理,无需另开 Remote 交互流。
|
|
2207
|
+
const peer = ctx.connection.createPeer({ principal: { kind: 'owner' }, interactions: 'none' })
|
|
2208
|
+
const api = createDshHostAdapter(ctx.typertGateway, peer)
|
|
2143
2209
|
const host = api
|
|
2144
2210
|
const agentDefaultModel = ctx.agentDefaultModel
|
|
2145
2211
|
const options = {
|
|
@@ -2952,7 +3018,7 @@ const plugin = {
|
|
|
2952
3018
|
respondToApproval(msg).then((frame) => {
|
|
2953
3019
|
if (frame && ws.readyState === 1) ws.send(stringifyWireFrame(frame))
|
|
2954
3020
|
})
|
|
2955
|
-
} else if (msg.type === 'file-list' || msg.type === 'file-download-open' ||
|
|
3021
|
+
} else if (msg.type === 'file-list' || msg.type === 'file-resolve' || msg.type === 'file-download-open' ||
|
|
2956
3022
|
msg.type === 'file-download-read' || msg.type === 'file-download-cancel') {
|
|
2957
3023
|
fileTransfers.handle(ws, msg).then((frame) => {
|
|
2958
3024
|
if (frame && ws.readyState === 1) ws.send(stringifyWireFrame(frame))
|
|
@@ -3043,7 +3109,7 @@ const plugin = {
|
|
|
3043
3109
|
'queue-control',
|
|
3044
3110
|
'session-archive',
|
|
3045
3111
|
'session-rename',
|
|
3046
|
-
...(options.fileDownloadsEnabled ? ['file-downloads'] : []),
|
|
3112
|
+
...(options.fileDownloadsEnabled ? ['file-downloads', 'file-references-v1'] : []),
|
|
3047
3113
|
],
|
|
3048
3114
|
port: transport.port || webServer.port,
|
|
3049
3115
|
clients: clients.size,
|
|
@@ -3213,7 +3279,7 @@ const plugin = {
|
|
|
3213
3279
|
|
|
3214
3280
|
const disposeApprovals = ctx.on('approval/request', (request, next) => {
|
|
3215
3281
|
const sessionId = request?.agent?.id === undefined ? null : String(request.agent.id)
|
|
3216
|
-
if (!sessionId || typeof request.toolName !== 'string' || !
|
|
3282
|
+
if (!sessionId || typeof request.toolName !== 'string' || !gatewayEnabled) return next()
|
|
3217
3283
|
const rpcId = crypto.randomUUID()
|
|
3218
3284
|
const approvalId = crypto.randomUUID()
|
|
3219
3285
|
return new Promise((resolve, reject) => {
|
|
@@ -3243,7 +3309,10 @@ const plugin = {
|
|
|
3243
3309
|
}
|
|
3244
3310
|
pendingApprovals.set(rpcId, pending)
|
|
3245
3311
|
broadcastInteractionFrame(approvalFrameFor(rpcId, pending))
|
|
3312
|
+
// 控制连接重连期间仍登记待审批,桌面端继续处理同一个请求。
|
|
3313
|
+
// 若桌面端仍在等待,后续连接可以重放;无可用答复时保持原有回退行为。
|
|
3246
3314
|
startDownstreamApproval(pending)
|
|
3315
|
+
if (!hasInteractionClient(sessionId)) fallbackApproval(pending)
|
|
3247
3316
|
log(`approval requested: rpcId=${rpcId} approvalId=${approvalId} session=${sessionId} tool=${request.toolName}`)
|
|
3248
3317
|
})
|
|
3249
3318
|
}, { prepend: true })
|
|
@@ -3408,6 +3477,9 @@ const plugin = {
|
|
|
3408
3477
|
if (waitTimer) clearTimeout(waitTimer)
|
|
3409
3478
|
clearInterval(fileTransferExpiryTimer)
|
|
3410
3479
|
backgroundStreamAbort.abort()
|
|
3480
|
+
peer.dispose().catch((error) => {
|
|
3481
|
+
log(`Host peer disposal failed: ${error && error.message ? error.message : String(error)}`)
|
|
3482
|
+
})
|
|
3411
3483
|
speechInput.dispose()
|
|
3412
3484
|
for (const pending of [...pendingQuestions.values()]) fallbackQuestion(pending)
|
|
3413
3485
|
for (const pending of [...pendingApprovals.values()]) fallbackApproval(pending)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Keep diagnostic facts when token replay and tool bodies are trimmed for mobile.
|
|
2
|
+
const TOOL_OUTPUT_LIMIT = 16_000
|
|
3
|
+
|
|
4
|
+
export function failureDetails(value) {
|
|
5
|
+
if (!value || typeof value !== 'object') return undefined
|
|
6
|
+
const result = {}
|
|
7
|
+
for (const key of ['message', 'code', 'name', 'reason', 'requestId']) {
|
|
8
|
+
if (typeof value[key] === 'string') result[key] = value[key]
|
|
9
|
+
}
|
|
10
|
+
for (const key of ['status', 'providerRetryAfterMs']) {
|
|
11
|
+
if (Number.isFinite(value[key])) result[key] = value[key]
|
|
12
|
+
}
|
|
13
|
+
return Object.keys(result).length ? result : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function assistantFinish(data) {
|
|
17
|
+
for (let i = (data.stream?.length || 0) - 1; i >= 0; i--) {
|
|
18
|
+
const record = data.stream[i]
|
|
19
|
+
if (record?.type !== 'chunk' || record.chunk?.type !== 'finish') continue
|
|
20
|
+
const reason = record.chunk.reason
|
|
21
|
+
if (typeof reason?.kind !== 'string') return undefined
|
|
22
|
+
const failure = failureDetails(reason.failure ?? reason.error)
|
|
23
|
+
return { kind: reason.kind, ...(failure ? { failure } : {}) }
|
|
24
|
+
}
|
|
25
|
+
return data.finish
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toolResultDetails(data) {
|
|
29
|
+
const texts = []
|
|
30
|
+
const visit = block => {
|
|
31
|
+
if (block?.type === 'text' && typeof block.text === 'string') texts.push(block.text)
|
|
32
|
+
else if (block?.type === 'tool-result' && Array.isArray(block.content)) block.content.forEach(visit)
|
|
33
|
+
}
|
|
34
|
+
for (const block of data.message?.content || []) visit(block)
|
|
35
|
+
const full = texts.join('')
|
|
36
|
+
const outputTruncated = full.length > TOOL_OUTPUT_LIMIT
|
|
37
|
+
// Retain the terminal status and sandbox hints at the end as well as the start.
|
|
38
|
+
const output = outputTruncated
|
|
39
|
+
? `${full.slice(0, TOOL_OUTPUT_LIMIT / 2)}\n[…]\n${full.slice(-TOOL_OUTPUT_LIMIT / 2)}`
|
|
40
|
+
: full
|
|
41
|
+
return { output, outputTruncated }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function turnReasonDetails(reason) {
|
|
45
|
+
if (!reason || typeof reason.kind !== 'string') return undefined
|
|
46
|
+
const error = failureDetails(reason.error ?? reason.failure)
|
|
47
|
+
return { kind: reason.kind, ...(error ? { error } : {}),
|
|
48
|
+
...(typeof reason.reason?.kind === 'string' ? { reason: {
|
|
49
|
+
kind: reason.reason.kind,
|
|
50
|
+
...(typeof reason.reason.reason === 'string' ? { reason: reason.reason.reason } : {}),
|
|
51
|
+
} } : {}),
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "DeepSeek Harness
|
|
3
|
+
"version": "0.10.3",
|
|
4
|
+
"description": "DeepSeek Harness 移动网关,支持安全设备配对、实时会话同步、插件管理、官方语音输入、文件卡片与安全文件预览、局域网及 Cloudflare Tunnel,以及自适应深浅色控制面板。0.10.3 适配 DSH 0.2.1-rc.2,为 Remote 请求及实时订阅补齐 Connection Peer,修复读取 signal 时的空值报错,并在网关停止时释放 Peer。",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"bin": "bin/setup-ip.mjs",
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/speech-input.test.mjs && node test/plugin-management.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
29
|
+
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/speech-input.test.mjs && node test/plugin-management.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/session-event-details.test.mjs && node test/gateway.test.mjs && node --test test/approval-replay.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
32
|
".": "./lib/index.mjs",
|