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.
Files changed (3) hide show
  1. package/README.md +19 -17
  2. package/lib/index.js +287 -287
  3. 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 github:bilibiliUID1480494301/dsh-relayhub-bridge
60
+ dsh plugin --profile <profile> add dsh-relayhub-bridge
63
61
 
64
62
  # concrete examples
65
- dsh plugin --profile desktop add github:bilibiliUID1480494301/dsh-relayhub-bridge
66
- dsh plugin --profile web add github:bilibiliUID1480494301/dsh-relayhub-bridge
63
+ dsh plugin --profile desktop add dsh-relayhub-bridge
64
+ dsh plugin --profile web add dsh-relayhub-bridge
67
65
  ```
68
66
 
69
- From a local checkout:
67
+ Or install straight from this repository (no registry involved):
70
68
 
71
69
  ```bash
72
- dsh plugin --profile <profile> add /absolute/path/to/dsh-relayhub-bridge
70
+ dsh plugin --profile <profile> add github:bilibiliUID1480494301/dsh-relayhub-bridge
73
71
  ```
74
72
 
75
- Once the package is on npm, the short form works too:
76
- `dsh plugin --profile <profile> add dsh-relayhub-bridge`.
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 github:bilibiliUID1480494301/dsh-relayhub-bridge
247
+ dsh plugin --profile <profile> add dsh-relayhub-bridge
249
248
 
250
249
  # 可直接复制的例子
251
- dsh plugin --profile desktop add github:bilibiliUID1480494301/dsh-relayhub-bridge
252
- dsh plugin --profile web add github:bilibiliUID1480494301/dsh-relayhub-bridge
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.0'
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.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
+ }