dsh-relayhub-bridge 0.1.1 → 0.1.2
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 +19 -17
- package/lib/index.js +287 -287
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,25 +55,26 @@ them is how plugins break on upgrade.
|
|
|
55
55
|
|
|
56
56
|
## Install
|
|
57
57
|
|
|
58
|
-
Not yet published to npm, so install straight from the repository:
|
|
59
|
-
|
|
60
58
|
```bash
|
|
61
59
|
# <profile> is your Harness profile name — e.g. desktop, web
|
|
62
|
-
dsh plugin --profile <profile> add
|
|
60
|
+
dsh plugin --profile <profile> add dsh-relayhub-bridge
|
|
63
61
|
|
|
64
62
|
# concrete examples
|
|
65
|
-
dsh plugin --profile desktop add
|
|
66
|
-
dsh plugin --profile web add
|
|
63
|
+
dsh plugin --profile desktop add dsh-relayhub-bridge
|
|
64
|
+
dsh plugin --profile web add dsh-relayhub-bridge
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
Or install straight from this repository (no registry involved):
|
|
70
68
|
|
|
71
69
|
```bash
|
|
72
|
-
dsh plugin --profile <profile> add /
|
|
70
|
+
dsh plugin --profile <profile> add github:bilibiliUID1480494301/dsh-relayhub-bridge
|
|
73
71
|
```
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
From a local checkout (an absolute path is required):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
dsh plugin --profile <profile> add /absolute/path/to/dsh-relayhub-bridge
|
|
77
|
+
```
|
|
77
78
|
|
|
78
79
|
Verify it was picked up:
|
|
79
80
|
|
|
@@ -241,15 +242,19 @@ export const inject = ['llm']
|
|
|
241
242
|
|
|
242
243
|
## 安装
|
|
243
244
|
|
|
244
|
-
尚未发布到 npm,因此直接从仓库安装:
|
|
245
|
-
|
|
246
245
|
```bash
|
|
247
246
|
# <profile> 是你的 Harness profile 名,例如 desktop、web
|
|
248
|
-
dsh plugin --profile <profile> add
|
|
247
|
+
dsh plugin --profile <profile> add dsh-relayhub-bridge
|
|
249
248
|
|
|
250
249
|
# 可直接复制的例子
|
|
251
|
-
dsh plugin --profile desktop add
|
|
252
|
-
dsh plugin --profile web add
|
|
250
|
+
dsh plugin --profile desktop add dsh-relayhub-bridge
|
|
251
|
+
dsh plugin --profile web add dsh-relayhub-bridge
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
也可以直接从本仓库安装(不经过 npm registry):
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
dsh plugin --profile <profile> add github:bilibiliUID1480494301/dsh-relayhub-bridge
|
|
253
258
|
```
|
|
254
259
|
|
|
255
260
|
从本地目录安装(路径必须绝对):
|
|
@@ -258,9 +263,6 @@ dsh plugin --profile web add github:bilibiliUID1480494301/dsh-relayhub-bridge
|
|
|
258
263
|
dsh plugin --profile <profile> add /absolute/path/to/dsh-relayhub-bridge
|
|
259
264
|
```
|
|
260
265
|
|
|
261
|
-
发布到 npm 之后,短名写法同样可用:
|
|
262
|
-
`dsh plugin --profile <profile> add dsh-relayhub-bridge`。
|
|
263
|
-
|
|
264
266
|
自检:
|
|
265
267
|
|
|
266
268
|
```bash
|
package/lib/index.js
CHANGED
|
@@ -1,287 +1,287 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-relayhub-bridge — DeepSeek Harness plugin for relay-hub stations.
|
|
3
|
-
*
|
|
4
|
-
* What it does, in the order a user meets it:
|
|
5
|
-
*
|
|
6
|
-
* 1. **Joins a station.** The user supplies a station address (or lets LAN
|
|
7
|
-
* discovery find one) plus a TOIP dynamic code or enrollment ticket. The
|
|
8
|
-
* bridge exchanges it for a session token and the station's model list.
|
|
9
|
-
* 2. **Becomes a provider.** It registers the `relayhub` provider route with
|
|
10
|
-
* `ctx.llm`, so the joined station shows up as a selectable set of models in
|
|
11
|
-
* Harness just like a first-party provider.
|
|
12
|
-
* 3. **Tags its traffic.** Every outbound request carries `X-DSH-Plugin-Id`, so
|
|
13
|
-
* the station can keep a per-plugin log next to its global request log.
|
|
14
|
-
* That is what makes "how is this plugin behaving?" answerable on the
|
|
15
|
-
* station side without reading the raw API log.
|
|
16
|
-
*
|
|
17
|
-
* Design notes that are load-bearing:
|
|
18
|
-
*
|
|
19
|
-
* * **Community plugin, minimal imports.** Only `@deepseek-ai/cordis` and
|
|
20
|
-
* `@deepseek-ai/schemastery` are touched, and both are optional peers. The
|
|
21
|
-
* harness-internal helper `registerDeepSeekProvider` is deliberately NOT
|
|
22
|
-
* imported: it lives in a first-party package that a profile-installed plugin
|
|
23
|
-
* cannot resolve, and depending on internals is how plugins break on upgrade.
|
|
24
|
-
* * **No credentials are logged.** The session token is a real credential; it
|
|
25
|
-
* goes into configuration, never into the client-side log or a console line.
|
|
26
|
-
* * **The station is the authority on codes.** This plugin computes a code only
|
|
27
|
-
* to self-check; it never decides whether a code is valid.
|
|
28
|
-
*
|
|
29
|
-
* @module dsh-relayhub-bridge
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import Schema from '@deepseek-ai/schemastery'
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
PLUGIN_ID,
|
|
36
|
-
PROTOCOL,
|
|
37
|
-
PROTOCOL_VERSION,
|
|
38
|
-
ToipError,
|
|
39
|
-
classifyJoinFailure,
|
|
40
|
-
fetchSession,
|
|
41
|
-
fetchStation,
|
|
42
|
-
identityHeaders,
|
|
43
|
-
join as joinStation,
|
|
44
|
-
normalizeRoot,
|
|
45
|
-
providerBaseURL,
|
|
46
|
-
secondsLeft,
|
|
47
|
-
totp,
|
|
48
|
-
} from './protocol.js'
|
|
49
|
-
import { discover } from './discovery.js'
|
|
50
|
-
|
|
51
|
-
/** The provider route this plugin owns. */
|
|
52
|
-
export const PROVIDER = 'relayhub'
|
|
53
|
-
|
|
54
|
-
/** Plugin name as Cordis sees it. */
|
|
55
|
-
export const name = 'relayhub-bridge'
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Services this plugin needs. `llm` is the provider registry; `settings` is
|
|
59
|
-
* optional and only used to persist a successful join.
|
|
60
|
-
*/
|
|
61
|
-
export const inject = ['llm']
|
|
62
|
-
|
|
63
|
-
/** Semantic version of this plugin, used in the identity header. */
|
|
64
|
-
export const PLUGIN_VERSION = '0.1.
|
|
65
|
-
|
|
66
|
-
/** Configuration schema, rendered by the Harness plugin settings page. */
|
|
67
|
-
export const Config = Schema.object({
|
|
68
|
-
/** Station HTTP root, e.g. `http://192.168.1.10:8799`. */
|
|
69
|
-
stationURL: Schema.string().default(''),
|
|
70
|
-
/**
|
|
71
|
-
* Session token issued by the station. Written by a successful join; may also
|
|
72
|
-
* be pasted by hand. Marked `secret` so the settings UI renders it write-only
|
|
73
|
-
* and never echoes it back over the wire.
|
|
74
|
-
*/
|
|
75
|
-
apiKey: Schema.string().role('secret').default(''),
|
|
76
|
-
/** Station id, recorded for the identity header. */
|
|
77
|
-
stationID: Schema.string().default(''),
|
|
78
|
-
/** Advertised models; each entry is `{ id, contextWindow }`. */
|
|
79
|
-
models: Schema.array(
|
|
80
|
-
Schema.object({
|
|
81
|
-
id: Schema.string().required(),
|
|
82
|
-
contextWindow: Schema.number(),
|
|
83
|
-
}),
|
|
84
|
-
).default([]),
|
|
85
|
-
/** Headers added to every upstream request (advanced escape hatch). */
|
|
86
|
-
extraHeaders: Schema.dict(Schema.string()).default({}),
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Models advertised by a station payload.
|
|
91
|
-
*
|
|
92
|
-
* @param {object} payload - a TOIP join response.
|
|
93
|
-
* @returns {{id: string, contextWindow?: number}[]} normalised model entries.
|
|
94
|
-
*/
|
|
95
|
-
export function modelsFromJoin(payload) {
|
|
96
|
-
const entries = payload?.dsh?.models ?? payload?.models ?? []
|
|
97
|
-
const out = []
|
|
98
|
-
const seen = new Set()
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
const id = String(entry?.id ?? entry?.model_id ?? '').trim()
|
|
101
|
-
if (!id || seen.has(id)) continue
|
|
102
|
-
seen.add(id)
|
|
103
|
-
const window = Number(entry?.contextWindow ?? entry?.context_window)
|
|
104
|
-
out.push(Number.isFinite(window) && window > 0 ? { id, contextWindow: window } : { id })
|
|
105
|
-
}
|
|
106
|
-
return out
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Join a station and produce the configuration patch that completes the bridge.
|
|
111
|
-
*
|
|
112
|
-
* Exported separately from {@link apply} so it can be driven from a test, a
|
|
113
|
-
* script, or a future command surface without booting a Cordis context.
|
|
114
|
-
*
|
|
115
|
-
* @param {object} input - what the user supplied.
|
|
116
|
-
* @param {string} input.stationURL - station address, in any pasted shape.
|
|
117
|
-
* @param {string} [input.code] - current TOIP dynamic code.
|
|
118
|
-
* @param {string} [input.ticket] - enrollment ticket (first contact).
|
|
119
|
-
* @param {string} [input.name] - device name to show in the station's token list.
|
|
120
|
-
* @param {{timeoutMs?: number}} [options]
|
|
121
|
-
* @returns {Promise<object>} `{ root, baseURL, apiKey, stationID, models, session, raw }`.
|
|
122
|
-
*/
|
|
123
|
-
export async function joinStationWith(input, options = {}) {
|
|
124
|
-
const root = normalizeRoot(input?.stationURL)
|
|
125
|
-
if (!input?.code && !input?.ticket) {
|
|
126
|
-
throw new ToipError('supply either a dynamic code or an enrollment ticket')
|
|
127
|
-
}
|
|
128
|
-
const payload = await joinStation(
|
|
129
|
-
root,
|
|
130
|
-
{ code: input.code, ticket: input.ticket, name: input.name },
|
|
131
|
-
{ pluginId: PLUGIN_ID, pluginVersion: PLUGIN_VERSION, timeoutMs: options.timeoutMs },
|
|
132
|
-
)
|
|
133
|
-
return {
|
|
134
|
-
root,
|
|
135
|
-
// Provider baseURL must carry /v1: the Messages adapter appends it only when
|
|
136
|
-
// the path lacks it.
|
|
137
|
-
baseURL: payload?.dsh?.baseURL ?? providerBaseURL(root),
|
|
138
|
-
apiKey: payload?.session?.token ?? payload?.dsh?.apiKey ?? '',
|
|
139
|
-
stationID: payload?.station?.id ?? '',
|
|
140
|
-
models: modelsFromJoin(payload),
|
|
141
|
-
session: payload?.session ?? {},
|
|
142
|
-
raw: payload,
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Resolve the provider settings snapshot, tolerating volatile accessors.
|
|
148
|
-
*
|
|
149
|
-
* @param {object} config - live plugin configuration.
|
|
150
|
-
* @returns {object} plain values.
|
|
151
|
-
*/
|
|
152
|
-
function plain(config) {
|
|
153
|
-
const get = (key, fallback) => {
|
|
154
|
-
const value = config?.[key]
|
|
155
|
-
if (value === undefined) return fallback
|
|
156
|
-
return typeof value?.get === 'function' ? value.get() : value
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
stationURL: get('stationURL', ''),
|
|
160
|
-
apiKey: get('apiKey', ''),
|
|
161
|
-
stationID: get('stationID', ''),
|
|
162
|
-
models: get('models', []) ?? [],
|
|
163
|
-
extraHeaders: get('extraHeaders', {}) ?? {},
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Cordis plugin entry point.
|
|
169
|
-
*
|
|
170
|
-
* @param {import('@deepseek-ai/cordis').Context} ctx - plugin context.
|
|
171
|
-
* @param {object} config - parsed configuration.
|
|
172
|
-
*/
|
|
173
|
-
export function apply(ctx, config) {
|
|
174
|
-
const resolve = () => plain(config)
|
|
175
|
-
const settingsNamespace = ctx?.fiber?.entry?.options?.id ?? PLUGIN_ID
|
|
176
|
-
|
|
177
|
-
// Declare the route so the Harness Models surface can configure it and the
|
|
178
|
-
// agent model selector can list it.
|
|
179
|
-
const registration = ctx.llm.registerConfigurableProviders([
|
|
180
|
-
{
|
|
181
|
-
provider: PROVIDER,
|
|
182
|
-
displayName: 'relay-hub',
|
|
183
|
-
settingsNs: settingsNamespace,
|
|
184
|
-
settingsPath: [],
|
|
185
|
-
},
|
|
186
|
-
])
|
|
187
|
-
|
|
188
|
-
const adapter = {
|
|
189
|
-
/**
|
|
190
|
-
* Human-facing provider metadata.
|
|
191
|
-
* @param {string} provider - the route id.
|
|
192
|
-
* @returns {{id: string, name: string}} metadata.
|
|
193
|
-
*/
|
|
194
|
-
providerInfo(provider) {
|
|
195
|
-
return { id: provider, name: 'relay-hub' }
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Models this route can serve, from the last successful join.
|
|
200
|
-
* @returns {object[]} catalog entries.
|
|
201
|
-
*/
|
|
202
|
-
discoverModels() {
|
|
203
|
-
return resolve().models.map((entry) => ({
|
|
204
|
-
provider: PROVIDER,
|
|
205
|
-
id: String(entry.id),
|
|
206
|
-
name: String(entry.id),
|
|
207
|
-
inputModalities: ['text'],
|
|
208
|
-
}))
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Authentication headers for every upstream request.
|
|
213
|
-
*
|
|
214
|
-
* The plugin identity headers ride along here because this is the one place
|
|
215
|
-
* the adapter merges caller-supplied headers into the outgoing Messages
|
|
216
|
-
* request. That is what makes the station's per-plugin log accounting work
|
|
217
|
-
* without any Harness-side change.
|
|
218
|
-
*
|
|
219
|
-
* @returns {{headers: Record<string,string>}} headers to merge.
|
|
220
|
-
*/
|
|
221
|
-
resolveAuth() {
|
|
222
|
-
const current = resolve()
|
|
223
|
-
if (!current.apiKey) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
`${PLUGIN_ID}: no session token yet for provider "${PROVIDER}". Run a join against a ` +
|
|
226
|
-
'relay-hub station (station address + TOIP dynamic code), which fills in the token.',
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
headers: {
|
|
231
|
-
'x-api-key': current.apiKey,
|
|
232
|
-
...identityHeaders({
|
|
233
|
-
pluginId: PLUGIN_ID,
|
|
234
|
-
pluginVersion: PLUGIN_VERSION,
|
|
235
|
-
stationId: current.stationID,
|
|
236
|
-
}),
|
|
237
|
-
...current.extraHeaders,
|
|
238
|
-
},
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
ctx.llm.registerAdapter([PROVIDER], adapter)
|
|
244
|
-
|
|
245
|
-
// Expose the bridge on the context so other plugins (or a command surface)
|
|
246
|
-
// can drive a join without importing this module directly.
|
|
247
|
-
ctx.set?.('relayhubBridge', {
|
|
248
|
-
provider: PROVIDER,
|
|
249
|
-
/** Join a station and return the configuration patch to persist. */
|
|
250
|
-
join: (input) => joinStationWith(input),
|
|
251
|
-
/** Ask a station whether it offers TOIP, without joining. */
|
|
252
|
-
probe: async (stationURL) => {
|
|
253
|
-
const root = normalizeRoot(stationURL)
|
|
254
|
-
const payload = await fetchStation(root)
|
|
255
|
-
return { root, ...payload }
|
|
256
|
-
},
|
|
257
|
-
/** Report the current session's standing on the station. */
|
|
258
|
-
session: async () => {
|
|
259
|
-
const current = resolve()
|
|
260
|
-
if (!current.stationURL || !current.apiKey) {
|
|
261
|
-
throw new ToipError('the bridge has not joined a station yet')
|
|
262
|
-
}
|
|
263
|
-
return fetchSession(normalizeRoot(current.stationURL), current.apiKey)
|
|
264
|
-
},
|
|
265
|
-
/** Broadcast-probe the local network for stations. */
|
|
266
|
-
discover: (options) => discover(options),
|
|
267
|
-
/** Current code and seconds remaining, for a UI to display. */
|
|
268
|
-
code: (secret) => (secret ? { code: totp(secret), secondsLeft: secondsLeft() } : null),
|
|
269
|
-
/** Turn any failure into user-facing guidance. */
|
|
270
|
-
explain: classifyJoinFailure,
|
|
271
|
-
/** The persisted configuration, without the token. */
|
|
272
|
-
describe: () => {
|
|
273
|
-
const current = resolve()
|
|
274
|
-
return {
|
|
275
|
-
protocol: PROTOCOL,
|
|
276
|
-
version: PROTOCOL_VERSION,
|
|
277
|
-
stationURL: current.stationURL,
|
|
278
|
-
stationID: current.stationID,
|
|
279
|
-
hasToken: Boolean(current.apiKey),
|
|
280
|
-
models: current.models.map((entry) => entry.id),
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// Returning the registration handle lets Cordis dispose it with the fiber.
|
|
286
|
-
void registration
|
|
287
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-relayhub-bridge — DeepSeek Harness plugin for relay-hub stations.
|
|
3
|
+
*
|
|
4
|
+
* What it does, in the order a user meets it:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Joins a station.** The user supplies a station address (or lets LAN
|
|
7
|
+
* discovery find one) plus a TOIP dynamic code or enrollment ticket. The
|
|
8
|
+
* bridge exchanges it for a session token and the station's model list.
|
|
9
|
+
* 2. **Becomes a provider.** It registers the `relayhub` provider route with
|
|
10
|
+
* `ctx.llm`, so the joined station shows up as a selectable set of models in
|
|
11
|
+
* Harness just like a first-party provider.
|
|
12
|
+
* 3. **Tags its traffic.** Every outbound request carries `X-DSH-Plugin-Id`, so
|
|
13
|
+
* the station can keep a per-plugin log next to its global request log.
|
|
14
|
+
* That is what makes "how is this plugin behaving?" answerable on the
|
|
15
|
+
* station side without reading the raw API log.
|
|
16
|
+
*
|
|
17
|
+
* Design notes that are load-bearing:
|
|
18
|
+
*
|
|
19
|
+
* * **Community plugin, minimal imports.** Only `@deepseek-ai/cordis` and
|
|
20
|
+
* `@deepseek-ai/schemastery` are touched, and both are optional peers. The
|
|
21
|
+
* harness-internal helper `registerDeepSeekProvider` is deliberately NOT
|
|
22
|
+
* imported: it lives in a first-party package that a profile-installed plugin
|
|
23
|
+
* cannot resolve, and depending on internals is how plugins break on upgrade.
|
|
24
|
+
* * **No credentials are logged.** The session token is a real credential; it
|
|
25
|
+
* goes into configuration, never into the client-side log or a console line.
|
|
26
|
+
* * **The station is the authority on codes.** This plugin computes a code only
|
|
27
|
+
* to self-check; it never decides whether a code is valid.
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-relayhub-bridge
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
PLUGIN_ID,
|
|
36
|
+
PROTOCOL,
|
|
37
|
+
PROTOCOL_VERSION,
|
|
38
|
+
ToipError,
|
|
39
|
+
classifyJoinFailure,
|
|
40
|
+
fetchSession,
|
|
41
|
+
fetchStation,
|
|
42
|
+
identityHeaders,
|
|
43
|
+
join as joinStation,
|
|
44
|
+
normalizeRoot,
|
|
45
|
+
providerBaseURL,
|
|
46
|
+
secondsLeft,
|
|
47
|
+
totp,
|
|
48
|
+
} from './protocol.js'
|
|
49
|
+
import { discover } from './discovery.js'
|
|
50
|
+
|
|
51
|
+
/** The provider route this plugin owns. */
|
|
52
|
+
export const PROVIDER = 'relayhub'
|
|
53
|
+
|
|
54
|
+
/** Plugin name as Cordis sees it. */
|
|
55
|
+
export const name = 'relayhub-bridge'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Services this plugin needs. `llm` is the provider registry; `settings` is
|
|
59
|
+
* optional and only used to persist a successful join.
|
|
60
|
+
*/
|
|
61
|
+
export const inject = ['llm']
|
|
62
|
+
|
|
63
|
+
/** Semantic version of this plugin, used in the identity header. */
|
|
64
|
+
export const PLUGIN_VERSION = '0.1.2'
|
|
65
|
+
|
|
66
|
+
/** Configuration schema, rendered by the Harness plugin settings page. */
|
|
67
|
+
export const Config = Schema.object({
|
|
68
|
+
/** Station HTTP root, e.g. `http://192.168.1.10:8799`. */
|
|
69
|
+
stationURL: Schema.string().default(''),
|
|
70
|
+
/**
|
|
71
|
+
* Session token issued by the station. Written by a successful join; may also
|
|
72
|
+
* be pasted by hand. Marked `secret` so the settings UI renders it write-only
|
|
73
|
+
* and never echoes it back over the wire.
|
|
74
|
+
*/
|
|
75
|
+
apiKey: Schema.string().role('secret').default(''),
|
|
76
|
+
/** Station id, recorded for the identity header. */
|
|
77
|
+
stationID: Schema.string().default(''),
|
|
78
|
+
/** Advertised models; each entry is `{ id, contextWindow }`. */
|
|
79
|
+
models: Schema.array(
|
|
80
|
+
Schema.object({
|
|
81
|
+
id: Schema.string().required(),
|
|
82
|
+
contextWindow: Schema.number(),
|
|
83
|
+
}),
|
|
84
|
+
).default([]),
|
|
85
|
+
/** Headers added to every upstream request (advanced escape hatch). */
|
|
86
|
+
extraHeaders: Schema.dict(Schema.string()).default({}),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Models advertised by a station payload.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} payload - a TOIP join response.
|
|
93
|
+
* @returns {{id: string, contextWindow?: number}[]} normalised model entries.
|
|
94
|
+
*/
|
|
95
|
+
export function modelsFromJoin(payload) {
|
|
96
|
+
const entries = payload?.dsh?.models ?? payload?.models ?? []
|
|
97
|
+
const out = []
|
|
98
|
+
const seen = new Set()
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const id = String(entry?.id ?? entry?.model_id ?? '').trim()
|
|
101
|
+
if (!id || seen.has(id)) continue
|
|
102
|
+
seen.add(id)
|
|
103
|
+
const window = Number(entry?.contextWindow ?? entry?.context_window)
|
|
104
|
+
out.push(Number.isFinite(window) && window > 0 ? { id, contextWindow: window } : { id })
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Join a station and produce the configuration patch that completes the bridge.
|
|
111
|
+
*
|
|
112
|
+
* Exported separately from {@link apply} so it can be driven from a test, a
|
|
113
|
+
* script, or a future command surface without booting a Cordis context.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} input - what the user supplied.
|
|
116
|
+
* @param {string} input.stationURL - station address, in any pasted shape.
|
|
117
|
+
* @param {string} [input.code] - current TOIP dynamic code.
|
|
118
|
+
* @param {string} [input.ticket] - enrollment ticket (first contact).
|
|
119
|
+
* @param {string} [input.name] - device name to show in the station's token list.
|
|
120
|
+
* @param {{timeoutMs?: number}} [options]
|
|
121
|
+
* @returns {Promise<object>} `{ root, baseURL, apiKey, stationID, models, session, raw }`.
|
|
122
|
+
*/
|
|
123
|
+
export async function joinStationWith(input, options = {}) {
|
|
124
|
+
const root = normalizeRoot(input?.stationURL)
|
|
125
|
+
if (!input?.code && !input?.ticket) {
|
|
126
|
+
throw new ToipError('supply either a dynamic code or an enrollment ticket')
|
|
127
|
+
}
|
|
128
|
+
const payload = await joinStation(
|
|
129
|
+
root,
|
|
130
|
+
{ code: input.code, ticket: input.ticket, name: input.name },
|
|
131
|
+
{ pluginId: PLUGIN_ID, pluginVersion: PLUGIN_VERSION, timeoutMs: options.timeoutMs },
|
|
132
|
+
)
|
|
133
|
+
return {
|
|
134
|
+
root,
|
|
135
|
+
// Provider baseURL must carry /v1: the Messages adapter appends it only when
|
|
136
|
+
// the path lacks it.
|
|
137
|
+
baseURL: payload?.dsh?.baseURL ?? providerBaseURL(root),
|
|
138
|
+
apiKey: payload?.session?.token ?? payload?.dsh?.apiKey ?? '',
|
|
139
|
+
stationID: payload?.station?.id ?? '',
|
|
140
|
+
models: modelsFromJoin(payload),
|
|
141
|
+
session: payload?.session ?? {},
|
|
142
|
+
raw: payload,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the provider settings snapshot, tolerating volatile accessors.
|
|
148
|
+
*
|
|
149
|
+
* @param {object} config - live plugin configuration.
|
|
150
|
+
* @returns {object} plain values.
|
|
151
|
+
*/
|
|
152
|
+
function plain(config) {
|
|
153
|
+
const get = (key, fallback) => {
|
|
154
|
+
const value = config?.[key]
|
|
155
|
+
if (value === undefined) return fallback
|
|
156
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
stationURL: get('stationURL', ''),
|
|
160
|
+
apiKey: get('apiKey', ''),
|
|
161
|
+
stationID: get('stationID', ''),
|
|
162
|
+
models: get('models', []) ?? [],
|
|
163
|
+
extraHeaders: get('extraHeaders', {}) ?? {},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Cordis plugin entry point.
|
|
169
|
+
*
|
|
170
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - plugin context.
|
|
171
|
+
* @param {object} config - parsed configuration.
|
|
172
|
+
*/
|
|
173
|
+
export function apply(ctx, config) {
|
|
174
|
+
const resolve = () => plain(config)
|
|
175
|
+
const settingsNamespace = ctx?.fiber?.entry?.options?.id ?? PLUGIN_ID
|
|
176
|
+
|
|
177
|
+
// Declare the route so the Harness Models surface can configure it and the
|
|
178
|
+
// agent model selector can list it.
|
|
179
|
+
const registration = ctx.llm.registerConfigurableProviders([
|
|
180
|
+
{
|
|
181
|
+
provider: PROVIDER,
|
|
182
|
+
displayName: 'relay-hub',
|
|
183
|
+
settingsNs: settingsNamespace,
|
|
184
|
+
settingsPath: [],
|
|
185
|
+
},
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
const adapter = {
|
|
189
|
+
/**
|
|
190
|
+
* Human-facing provider metadata.
|
|
191
|
+
* @param {string} provider - the route id.
|
|
192
|
+
* @returns {{id: string, name: string}} metadata.
|
|
193
|
+
*/
|
|
194
|
+
providerInfo(provider) {
|
|
195
|
+
return { id: provider, name: 'relay-hub' }
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Models this route can serve, from the last successful join.
|
|
200
|
+
* @returns {object[]} catalog entries.
|
|
201
|
+
*/
|
|
202
|
+
discoverModels() {
|
|
203
|
+
return resolve().models.map((entry) => ({
|
|
204
|
+
provider: PROVIDER,
|
|
205
|
+
id: String(entry.id),
|
|
206
|
+
name: String(entry.id),
|
|
207
|
+
inputModalities: ['text'],
|
|
208
|
+
}))
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Authentication headers for every upstream request.
|
|
213
|
+
*
|
|
214
|
+
* The plugin identity headers ride along here because this is the one place
|
|
215
|
+
* the adapter merges caller-supplied headers into the outgoing Messages
|
|
216
|
+
* request. That is what makes the station's per-plugin log accounting work
|
|
217
|
+
* without any Harness-side change.
|
|
218
|
+
*
|
|
219
|
+
* @returns {{headers: Record<string,string>}} headers to merge.
|
|
220
|
+
*/
|
|
221
|
+
resolveAuth() {
|
|
222
|
+
const current = resolve()
|
|
223
|
+
if (!current.apiKey) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`${PLUGIN_ID}: no session token yet for provider "${PROVIDER}". Run a join against a ` +
|
|
226
|
+
'relay-hub station (station address + TOIP dynamic code), which fills in the token.',
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
headers: {
|
|
231
|
+
'x-api-key': current.apiKey,
|
|
232
|
+
...identityHeaders({
|
|
233
|
+
pluginId: PLUGIN_ID,
|
|
234
|
+
pluginVersion: PLUGIN_VERSION,
|
|
235
|
+
stationId: current.stationID,
|
|
236
|
+
}),
|
|
237
|
+
...current.extraHeaders,
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
ctx.llm.registerAdapter([PROVIDER], adapter)
|
|
244
|
+
|
|
245
|
+
// Expose the bridge on the context so other plugins (or a command surface)
|
|
246
|
+
// can drive a join without importing this module directly.
|
|
247
|
+
ctx.set?.('relayhubBridge', {
|
|
248
|
+
provider: PROVIDER,
|
|
249
|
+
/** Join a station and return the configuration patch to persist. */
|
|
250
|
+
join: (input) => joinStationWith(input),
|
|
251
|
+
/** Ask a station whether it offers TOIP, without joining. */
|
|
252
|
+
probe: async (stationURL) => {
|
|
253
|
+
const root = normalizeRoot(stationURL)
|
|
254
|
+
const payload = await fetchStation(root)
|
|
255
|
+
return { root, ...payload }
|
|
256
|
+
},
|
|
257
|
+
/** Report the current session's standing on the station. */
|
|
258
|
+
session: async () => {
|
|
259
|
+
const current = resolve()
|
|
260
|
+
if (!current.stationURL || !current.apiKey) {
|
|
261
|
+
throw new ToipError('the bridge has not joined a station yet')
|
|
262
|
+
}
|
|
263
|
+
return fetchSession(normalizeRoot(current.stationURL), current.apiKey)
|
|
264
|
+
},
|
|
265
|
+
/** Broadcast-probe the local network for stations. */
|
|
266
|
+
discover: (options) => discover(options),
|
|
267
|
+
/** Current code and seconds remaining, for a UI to display. */
|
|
268
|
+
code: (secret) => (secret ? { code: totp(secret), secondsLeft: secondsLeft() } : null),
|
|
269
|
+
/** Turn any failure into user-facing guidance. */
|
|
270
|
+
explain: classifyJoinFailure,
|
|
271
|
+
/** The persisted configuration, without the token. */
|
|
272
|
+
describe: () => {
|
|
273
|
+
const current = resolve()
|
|
274
|
+
return {
|
|
275
|
+
protocol: PROTOCOL,
|
|
276
|
+
version: PROTOCOL_VERSION,
|
|
277
|
+
stationURL: current.stationURL,
|
|
278
|
+
stationID: current.stationID,
|
|
279
|
+
hasToken: Boolean(current.apiKey),
|
|
280
|
+
models: current.models.map((entry) => entry.id),
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Returning the registration handle lets Cordis dispose it with the fiber.
|
|
286
|
+
void registration
|
|
287
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-relayhub-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DeepSeek Harness bridge to a relay-hub station: TOIP dynamic-password onboarding, LAN discovery, and per-plugin log accounting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -78,4 +78,4 @@
|
|
|
78
78
|
"bugs": {
|
|
79
79
|
"url": "https://github.com/bilibiliUID1480494301/dsh-relayhub-bridge/issues"
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|