dsh-mobile-access 1.0.2 → 1.1.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 +381 -117
- package/client.js +20 -6
- package/gateway.js +368 -0
- package/index.js +23 -3
- package/locale/en.json +9 -1
- package/locale/zh.json +9 -1
- package/package.json +14 -1
- package/routes.js +28 -18
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-access",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Independent DSH bundle that pairs iPhones and iPads with the official DeepSeek Harness Web GUI over the local network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/abbaababa/dsh-mobile-access.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/abbaababa/dsh-mobile-access#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/abbaababa/dsh-mobile-access/issues"
|
|
14
|
+
},
|
|
7
15
|
"publishConfig": {
|
|
8
16
|
"access": "public"
|
|
9
17
|
},
|
|
@@ -20,6 +28,7 @@
|
|
|
20
28
|
"index.js",
|
|
21
29
|
"service.js",
|
|
22
30
|
"routes.js",
|
|
31
|
+
"gateway.js",
|
|
23
32
|
"html.js",
|
|
24
33
|
"types.js",
|
|
25
34
|
"client.js",
|
|
@@ -48,5 +57,9 @@
|
|
|
48
57
|
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
49
58
|
]
|
|
50
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^26.6.5",
|
|
63
|
+
"typescript": "6.0.3"
|
|
51
64
|
}
|
|
52
65
|
}
|
package/routes.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* can reach it; every state and rotation call stays inside Connection's fence.
|
|
5
5
|
*/
|
|
6
6
|
import { contentSecurityPolicy, renderMessagePage, renderPairingPage } from './html.js';
|
|
7
|
+
import { EMPTY_GATEWAY_STATUS } from './gateway.js';
|
|
7
8
|
import { constantTimeEqual, isLoopbackHost, } from './service.js';
|
|
8
9
|
const PAIR_PATH = '/m';
|
|
9
10
|
const STATE_PATH = '/api/mobile-access/state';
|
|
@@ -182,27 +183,36 @@ function jsonResponse(payload, status = 200) {
|
|
|
182
183
|
},
|
|
183
184
|
});
|
|
184
185
|
}
|
|
185
|
-
function
|
|
186
|
+
function publicOriginOf(gateway) {
|
|
187
|
+
const status = gateway?.status();
|
|
188
|
+
return status?.running === true && status.publicOrigin !== null ? status.publicOrigin : null;
|
|
189
|
+
}
|
|
190
|
+
function statePayload(ctx, service, gateway) {
|
|
186
191
|
const origin = originOf(ctx);
|
|
192
|
+
const gatewayStatus = gateway?.status() ?? EMPTY_GATEWAY_STATUS;
|
|
193
|
+
const publicOrigin = publicOriginOf(gateway) ?? origin;
|
|
187
194
|
const snapshot = service.snapshot();
|
|
188
195
|
return {
|
|
189
196
|
...snapshot,
|
|
190
197
|
ok: true,
|
|
191
198
|
version: 1,
|
|
192
199
|
origin,
|
|
200
|
+
publicOrigin,
|
|
193
201
|
pairPath: PAIR_PATH,
|
|
194
|
-
pairUrl: new URL(PAIR_PATH,
|
|
202
|
+
pairUrl: new URL(PAIR_PATH, publicOrigin).href,
|
|
195
203
|
bindHost: ctx.webServer.host,
|
|
196
204
|
protocol: protocolOf(ctx),
|
|
197
205
|
port: ctx.webServer.port,
|
|
198
206
|
loopback: isLoopbackHost(ctx.webServer.host),
|
|
207
|
+
gateway: gatewayStatus,
|
|
199
208
|
};
|
|
200
209
|
}
|
|
201
|
-
function pairingPage(ctx, service, request, nonce, error) {
|
|
210
|
+
function pairingPage(ctx, service, request, nonce, gateway, error) {
|
|
202
211
|
const snapshot = service.snapshot();
|
|
212
|
+
const publicOrigin = publicOriginOf(gateway);
|
|
203
213
|
return renderPairingPage({
|
|
204
214
|
...(error === undefined ? {} : { error }),
|
|
205
|
-
host: requestHost(ctx, request),
|
|
215
|
+
host: publicOrigin === null ? requestHost(ctx, request) : new URL(publicOrigin).host,
|
|
206
216
|
expiresAt: snapshot.expiresAt,
|
|
207
217
|
expired: snapshot.expired,
|
|
208
218
|
locked: snapshot.locked,
|
|
@@ -211,10 +221,10 @@ function pairingPage(ctx, service, request, nonce, error) {
|
|
|
211
221
|
nonce,
|
|
212
222
|
});
|
|
213
223
|
}
|
|
214
|
-
function sendPairing(ctx, service, request, response, error, status = 200, extraHeaders = {}) {
|
|
224
|
+
function sendPairing(ctx, service, request, response, gateway, error, status = 200, extraHeaders = {}) {
|
|
215
225
|
const nonce = pairingNonce();
|
|
216
226
|
response.setHeader('set-cookie', pairingCookie(nonce));
|
|
217
|
-
sendHtml(response, status, pairingPage(ctx, service, request, nonce, error), extraHeaders);
|
|
227
|
+
sendHtml(response, status, pairingPage(ctx, service, request, nonce, gateway, error), extraHeaders);
|
|
218
228
|
}
|
|
219
229
|
function refusalPage(title, message, detail) {
|
|
220
230
|
return renderMessagePage({
|
|
@@ -296,7 +306,7 @@ function failureMessage(reason) {
|
|
|
296
306
|
return '尝试过于频繁,请稍后再试。';
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
|
-
async function handleGet(ctx, service, request, response) {
|
|
309
|
+
async function handleGet(ctx, service, request, response, gateway) {
|
|
300
310
|
const captured = new CapturedResponse();
|
|
301
311
|
const allowed = ctx.connection.authorizeIndex({
|
|
302
312
|
method: request.method,
|
|
@@ -309,9 +319,9 @@ async function handleGet(ctx, service, request, response) {
|
|
|
309
319
|
}
|
|
310
320
|
if (replayCaptured(response, captured))
|
|
311
321
|
return;
|
|
312
|
-
sendPairing(ctx, service, request, response);
|
|
322
|
+
sendPairing(ctx, service, request, response, gateway);
|
|
313
323
|
}
|
|
314
|
-
async function handlePost(ctx, service, request, response) {
|
|
324
|
+
async function handlePost(ctx, service, request, response, gateway) {
|
|
315
325
|
const contentType = (firstHeader(request.headers, 'content-type') ?? '').toLowerCase();
|
|
316
326
|
if (!contentType.startsWith('application/x-www-form-urlencoded')) {
|
|
317
327
|
sendHtml(response, 415, refusalPage('无法处理请求', '配对请求必须使用表单提交。'));
|
|
@@ -333,7 +343,7 @@ async function handlePost(ctx, service, request, response) {
|
|
|
333
343
|
const nonce = params.get('nonce') ?? '';
|
|
334
344
|
const expectedNonce = cookieValue(request.headers, PAIR_COOKIE) ?? '';
|
|
335
345
|
if (nonce === '' || expectedNonce === '' || !constantTimeEqual(nonce, expectedNonce)) {
|
|
336
|
-
sendPairing(ctx, service, request, response, '配对页已过期,请刷新后重试。', 403);
|
|
346
|
+
sendPairing(ctx, service, request, response, gateway, '配对页已过期,请刷新后重试。', 403);
|
|
337
347
|
return;
|
|
338
348
|
}
|
|
339
349
|
const code = params.get('code') ?? '';
|
|
@@ -342,7 +352,7 @@ async function handlePost(ctx, service, request, response) {
|
|
|
342
352
|
const extra = result.reason === 'rate'
|
|
343
353
|
? { 'retry-after': String(Math.max(1, Math.ceil((result.retryAfterMs ?? 1000) / 1000))) }
|
|
344
354
|
: {};
|
|
345
|
-
sendPairing(ctx, service, request, response, failureMessage(result.reason), result.reason === 'rate' ? 429 : 200, extra);
|
|
355
|
+
sendPairing(ctx, service, request, response, gateway, failureMessage(result.reason), result.reason === 'rate' ? 429 : 200, extra);
|
|
346
356
|
return;
|
|
347
357
|
}
|
|
348
358
|
const minted = mintIndexCookie(ctx, request);
|
|
@@ -364,7 +374,7 @@ async function handlePost(ctx, service, request, response) {
|
|
|
364
374
|
* Build the single exact `/m` handler. Exported for route-level tests, which do
|
|
365
375
|
* not need a live DSH process.
|
|
366
376
|
*/
|
|
367
|
-
export function createPairingHandler(ctx, service) {
|
|
377
|
+
export function createPairingHandler(ctx, service, gateway) {
|
|
368
378
|
return async (request, response) => {
|
|
369
379
|
if (!pairingHostAllowed(ctx, request)) {
|
|
370
380
|
sendHtml(response, 403, refusalPage('请求被拒绝', '此请求未通过 Host 校验。'));
|
|
@@ -372,11 +382,11 @@ export function createPairingHandler(ctx, service) {
|
|
|
372
382
|
}
|
|
373
383
|
const method = (request.method ?? 'GET').toUpperCase();
|
|
374
384
|
if (method === 'GET') {
|
|
375
|
-
await handleGet(ctx, service, request, response);
|
|
385
|
+
await handleGet(ctx, service, request, response, gateway);
|
|
376
386
|
return;
|
|
377
387
|
}
|
|
378
388
|
if (method === 'POST') {
|
|
379
|
-
await handlePost(ctx, service, request, response);
|
|
389
|
+
await handlePost(ctx, service, request, response, gateway);
|
|
380
390
|
return;
|
|
381
391
|
}
|
|
382
392
|
response.setHeader('allow', 'GET, POST');
|
|
@@ -387,7 +397,7 @@ export function createPairingHandler(ctx, service) {
|
|
|
387
397
|
* Register all three routes. The returned disposer removes them in reverse
|
|
388
398
|
* order; every registration is attempted only once.
|
|
389
399
|
*/
|
|
390
|
-
export function registerMobileAccess(ctx, service) {
|
|
400
|
+
export function registerMobileAccess(ctx, service, gateway) {
|
|
391
401
|
let pairing;
|
|
392
402
|
let state;
|
|
393
403
|
let rotate;
|
|
@@ -395,7 +405,7 @@ export function registerMobileAccess(ctx, service) {
|
|
|
395
405
|
path: STATE_PATH,
|
|
396
406
|
methods: ['GET'],
|
|
397
407
|
requestBody: 'buffered',
|
|
398
|
-
fetch: () => Promise.resolve(jsonResponse(statePayload(ctx, service))),
|
|
408
|
+
fetch: () => Promise.resolve(jsonResponse(statePayload(ctx, service, gateway))),
|
|
399
409
|
};
|
|
400
410
|
const rotateRoute = {
|
|
401
411
|
path: ROTATE_PATH,
|
|
@@ -403,14 +413,14 @@ export function registerMobileAccess(ctx, service) {
|
|
|
403
413
|
requestBody: 'buffered',
|
|
404
414
|
fetch: () => {
|
|
405
415
|
service.rotate();
|
|
406
|
-
return Promise.resolve(jsonResponse(statePayload(ctx, service)));
|
|
416
|
+
return Promise.resolve(jsonResponse(statePayload(ctx, service, gateway)));
|
|
407
417
|
},
|
|
408
418
|
};
|
|
409
419
|
try {
|
|
410
420
|
pairing = ctx.webServer.register({
|
|
411
421
|
kind: 'exact',
|
|
412
422
|
path: PAIR_PATH,
|
|
413
|
-
handler: createPairingHandler(ctx, service),
|
|
423
|
+
handler: createPairingHandler(ctx, service, gateway),
|
|
414
424
|
});
|
|
415
425
|
state = ctx.connection.fetch.register(stateRoute);
|
|
416
426
|
rotate = ctx.connection.fetch.register(rotateRoute);
|