@skillpie/dsh-remote 0.0.0-stage → 1.0.0
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/README.md +77 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +14 -0
- package/lib/bridge.js +377 -0
- package/lib/client.js +493 -0
- package/lib/console.js +434 -0
- package/lib/engine.js +367 -0
- package/lib/index.js +475 -0
- package/lib/pipe.js +83 -0
- package/lib/protocol.js +122 -0
- package/lib/relay-core.js +823 -0
- package/lib/remote.js +181 -0
- package/lib/ws-adapter.js +49 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +50 -4
package/lib/remote.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote — outbound control link (host role).
|
|
3
|
+
*
|
|
4
|
+
* Dials `relayUrl + HOST_UPGRADE_PATH` over one WebSocket, registers the
|
|
5
|
+
* 8-digit code and identity, and feeds every control frame to the shared
|
|
6
|
+
* HostEngine. Reconnects with jittered backoff; mints/re-persists the token
|
|
7
|
+
* whenever the relay hands one back (first registration, or after the relay
|
|
8
|
+
* lost its registry).
|
|
9
|
+
*/
|
|
10
|
+
import WebSocket from 'ws'
|
|
11
|
+
import { HOST_UPGRADE_PATH, PROTOCOL_VERSION } from './protocol.js'
|
|
12
|
+
import { adaptSocket } from './ws-adapter.js'
|
|
13
|
+
|
|
14
|
+
const BACKOFF_INITIAL_MS = 1_000
|
|
15
|
+
const BACKOFF_MAX_MS = 30_000
|
|
16
|
+
const HEARTBEAT_MS = 15_000
|
|
17
|
+
const IDLE_TIMEOUT_MS = 45_000
|
|
18
|
+
|
|
19
|
+
export class RelayLink {
|
|
20
|
+
/**
|
|
21
|
+
* @param identity { hostId, code, token, name, dsh } — read live via getters
|
|
22
|
+
* @param onToken (token) — persist a relay-minted token
|
|
23
|
+
* @param peer { attach(channel), detach(channel) } — the GUI bridge or
|
|
24
|
+
* command engine; attached on a healthy link
|
|
25
|
+
*/
|
|
26
|
+
constructor({ relayUrl, identity, onToken, peer, log = () => {} }) {
|
|
27
|
+
this.relayUrl = relayUrl
|
|
28
|
+
this.identity = identity
|
|
29
|
+
this.onToken = onToken
|
|
30
|
+
this.peer = peer
|
|
31
|
+
this.log = log
|
|
32
|
+
this.raw = undefined
|
|
33
|
+
this.backoffMs = BACKOFF_INITIAL_MS
|
|
34
|
+
this.retryTimer = undefined
|
|
35
|
+
this.hbTimer = undefined
|
|
36
|
+
this.idleTimer = undefined
|
|
37
|
+
this.stopped = true
|
|
38
|
+
this.connectedSince = undefined
|
|
39
|
+
this.lastError = undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get connected() {
|
|
43
|
+
return this.raw !== undefined && this.connectedSince !== undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
start() {
|
|
47
|
+
if (!this.stopped) return
|
|
48
|
+
this.stopped = false
|
|
49
|
+
this.connect()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
stop() {
|
|
53
|
+
this.stopped = true
|
|
54
|
+
this.clearTimers()
|
|
55
|
+
if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = undefined }
|
|
56
|
+
this.detachPeer()
|
|
57
|
+
if (this.raw) {
|
|
58
|
+
try { this.raw.close(1000) } catch { /* already gone */ }
|
|
59
|
+
this.raw = undefined
|
|
60
|
+
}
|
|
61
|
+
this.connectedSince = undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearTimers() {
|
|
65
|
+
if (this.hbTimer) { clearInterval(this.hbTimer); this.hbTimer = undefined }
|
|
66
|
+
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = undefined }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
detachPeer() {
|
|
70
|
+
if (this.peer && this.adapter) this.peer.detach(this.adapter)
|
|
71
|
+
this.adapter = undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
connect() {
|
|
75
|
+
if (this.stopped) return
|
|
76
|
+
const url = new URL(this.relayUrl)
|
|
77
|
+
url.protocol = url.protocol === 'http:' ? 'ws:' : url.protocol === 'https:' ? 'wss:' : url.protocol
|
|
78
|
+
url.pathname = HOST_UPGRADE_PATH
|
|
79
|
+
let raw
|
|
80
|
+
try {
|
|
81
|
+
raw = new WebSocket(url, { handshakeTimeout: 10_000 })
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.lastError = error?.message ?? String(error)
|
|
84
|
+
this.scheduleRetry()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
this.raw = raw
|
|
88
|
+
raw.on('open', () => {
|
|
89
|
+
raw.send(JSON.stringify({
|
|
90
|
+
t: 'hello',
|
|
91
|
+
v: PROTOCOL_VERSION,
|
|
92
|
+
hostId: this.identity.hostId,
|
|
93
|
+
code: this.identity.code,
|
|
94
|
+
token: this.identity.token ?? '',
|
|
95
|
+
name: this.identity.name,
|
|
96
|
+
dsh: this.identity.dsh,
|
|
97
|
+
}))
|
|
98
|
+
})
|
|
99
|
+
raw.on('message', (data) => this.onFrame(String(data)))
|
|
100
|
+
raw.on('close', () => {
|
|
101
|
+
const wasConnected = this.connected
|
|
102
|
+
this.clearTimers()
|
|
103
|
+
this.detachPeer()
|
|
104
|
+
this.raw = undefined
|
|
105
|
+
this.connectedSince = undefined
|
|
106
|
+
if (this.stopped) return
|
|
107
|
+
if (wasConnected) this.log('relay', '中转连接断开,准备重连')
|
|
108
|
+
this.scheduleRetry()
|
|
109
|
+
})
|
|
110
|
+
raw.on('error', (error) => {
|
|
111
|
+
this.lastError = error?.message ?? String(error)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
scheduleRetry() {
|
|
116
|
+
if (this.stopped || this.retryTimer) return
|
|
117
|
+
const jitter = Math.floor(Math.random() * this.backoffMs * 0.5)
|
|
118
|
+
const delay = this.backoffMs + jitter
|
|
119
|
+
this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS)
|
|
120
|
+
this.retryTimer = setTimeout(() => {
|
|
121
|
+
this.retryTimer = undefined
|
|
122
|
+
this.connect()
|
|
123
|
+
}, delay)
|
|
124
|
+
this.log('relay', `将在 ${Math.round(delay / 100) / 10}s 后重连中转`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onFrame(text) {
|
|
128
|
+
let frame
|
|
129
|
+
try { frame = JSON.parse(text) } catch { return }
|
|
130
|
+
if (!frame || typeof frame !== 'object') return
|
|
131
|
+
if (frame.t === 'hello') {
|
|
132
|
+
if (frame.ok !== true) {
|
|
133
|
+
this.lastError = frame.error ?? 'relay rejected registration'
|
|
134
|
+
this.log('relay', `中转拒绝注册: ${this.lastError}`)
|
|
135
|
+
// Registration refused (e.g. code taken): back off like a network failure.
|
|
136
|
+
try { this.raw?.close() } catch { /* noop */ }
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
this.backoffMs = BACKOFF_INITIAL_MS
|
|
140
|
+
this.connectedSince = Date.now()
|
|
141
|
+
this.lastError = undefined
|
|
142
|
+
if (typeof frame.token === 'string' && frame.token !== this.identity.token) {
|
|
143
|
+
this.identity.token = frame.token
|
|
144
|
+
try { this.onToken?.(frame.token) } catch (error) {
|
|
145
|
+
this.log('relay', `token 持久化失败: ${error?.message ?? error}`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
this.armIdle()
|
|
149
|
+
this.adapter = adaptSocket(this.raw)
|
|
150
|
+
this.peer.attach(this.adapter)
|
|
151
|
+
this.hbTimer = setInterval(() => {
|
|
152
|
+
if (this.raw?.readyState === 1) this.raw.send(JSON.stringify({ t: 'hb' }))
|
|
153
|
+
}, HEARTBEAT_MS)
|
|
154
|
+
this.log('relay', `已连接中转,远程码 ${this.identity.code} 生效`)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (frame.t === 'hb') {
|
|
158
|
+
this.armIdle()
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
// res/push frames belong to the engine's channel adapter; its own
|
|
162
|
+
// onMessage handler consumed them already (adapter shares this socket).
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
armIdle() {
|
|
166
|
+
if (this.idleTimer) clearTimeout(this.idleTimer)
|
|
167
|
+
this.idleTimer = setTimeout(() => {
|
|
168
|
+
this.log('relay', '中转心跳超时,主动断开')
|
|
169
|
+
try { this.raw?.close() } catch { /* noop */ }
|
|
170
|
+
}, IDLE_TIMEOUT_MS)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
status() {
|
|
174
|
+
return {
|
|
175
|
+
relayUrl: this.relayUrl,
|
|
176
|
+
connected: this.connected,
|
|
177
|
+
connectedSince: this.connectedSince,
|
|
178
|
+
lastError: this.lastError,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote — adapter from a raw `ws` socket to the small surface the
|
|
3
|
+
* plugin uses:
|
|
4
|
+
* send(obj) JSON text frame
|
|
5
|
+
* sendRaw(buffer) binary frame (GUI splice payloads)
|
|
6
|
+
* onMessage(cb) cb(text | Buffer) — text for JSON frames, Buffer for binary
|
|
7
|
+
* onClose(cb), close()
|
|
8
|
+
*/
|
|
9
|
+
export function adaptSocket(raw) {
|
|
10
|
+
const end = {
|
|
11
|
+
open: raw.readyState === 1,
|
|
12
|
+
send(obj) {
|
|
13
|
+
if (raw.readyState !== 1) return
|
|
14
|
+
raw.send(JSON.stringify(obj))
|
|
15
|
+
},
|
|
16
|
+
sendRaw(buffer) {
|
|
17
|
+
if (raw.readyState !== 1) return
|
|
18
|
+
raw.send(buffer, { binary: true })
|
|
19
|
+
},
|
|
20
|
+
sendText(text) {
|
|
21
|
+
if (raw.readyState !== 1) return
|
|
22
|
+
raw.send(text)
|
|
23
|
+
},
|
|
24
|
+
onMessage(handler) {
|
|
25
|
+
const listener = (data, isBinary) => {
|
|
26
|
+
try {
|
|
27
|
+
handler(isBinary ? data : String(data))
|
|
28
|
+
} catch {
|
|
29
|
+
// A listener bug must never crash the host process.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
raw.on('message', listener)
|
|
33
|
+
return () => raw.off('message', listener)
|
|
34
|
+
},
|
|
35
|
+
onClose(handler) {
|
|
36
|
+
const listener = () => {
|
|
37
|
+
try { handler() } catch { /* contained */ }
|
|
38
|
+
}
|
|
39
|
+
raw.on('close', listener)
|
|
40
|
+
return () => raw.off('close', listener)
|
|
41
|
+
},
|
|
42
|
+
close() {
|
|
43
|
+
try { raw.close(1000) } catch { raw.terminate() }
|
|
44
|
+
},
|
|
45
|
+
raw,
|
|
46
|
+
}
|
|
47
|
+
raw.on('close', () => { end.open = false })
|
|
48
|
+
return end
|
|
49
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillpie/dsh-remote",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote control for DeepSeek Harness: reach any host by its 8-digit code through a relay origin such as dsh.skillpie.cn.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"remote-control",
|
|
12
|
+
"relay",
|
|
13
|
+
"remote-assistance"
|
|
14
|
+
],
|
|
15
|
+
"main": "./lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./lib/index.js",
|
|
18
|
+
"./client": "./lib/client.js",
|
|
19
|
+
"./locale/*.json": "./locale/*.json",
|
|
20
|
+
"./icon": "./icon.svg"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"locale/*.json",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"icon.svg",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.13",
|
|
31
|
+
"dsh": ">=0.2.1-rc.3"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"ws": "^8.22.2"
|
|
35
|
+
},
|
|
36
|
+
"dsh": {
|
|
37
|
+
"bundle": {
|
|
38
|
+
"patch": "./cordis.patch.yml"
|
|
39
|
+
},
|
|
40
|
+
"client": {
|
|
41
|
+
"platform": "web",
|
|
42
|
+
"inject": [
|
|
43
|
+
"@deepseek-ai/dsh-client-locale",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|